Mongodb Datastore
MongoDB-backed swamp datastore — distributed locks with TTL, heartbeat, and nonce fencing, plus a manifest+content-addressed-blob sync protocol that streams the datastore tier as inline-bytes cursor batches. Dirty tracking rides an append-only journal with ancestor coalescing (O(1) markDirty) and pushes reconcile dirty roots in batches rather than one round-trip each. Blobs over 15 MB are transparently chunked across multiple docs so they fit under MongoDB's 16 MB BSON limit. Ships a companion maintenance model and workflow that inventory, sweep orphaned blobs and aged tombstones, and compact reclaimed space — fanning out over every namespace in one execution. Scoped by tenant + repo namespace. Requires MongoDB 4.0+ as a replica set.
2026.09.03.1
Feature parity with @swamp/s3-datastore: the sync service now advertises
namespacedSync, twoPhaseSync, controlPlane and configRefresh on top of
scopedSync and lazyHydration. On this datastore swamp serve reports
deployment mode durable instead of local: heartbeats, active and pending
runs, cron fire records, reconcile claims and token secrets live in a new
_control collection (putIfAbsent = insertOne + duplicate-key), so runs
survive instance replacement and two instances never fire one schedule twice.
Added
- Control plane.
controlPlaneStore()overt_<tenant>_r_<ns>_control, keys stored as<coreNamespace>/_control/<key>(or_control/<key>). Namespace registration (listNamespaces, manifest-onlyregisterNamespace). - Two-phase push.
preparePushuploads blobs outside swamp's global lock;commitPushre-reads the remote, merges path docs, tombstones, and releases only the dirty roots it consumed (amarkDirtybetween the phases survives;bulkInvalidatedclears only if the sidecar's newbulkSeqis unchanged). subdirspulls for serve's config and access pollers, watermark-bounded and never advancing the watermark.- Journaled, revertable migrations on the maintenance model:
fold_namespace_prefix(retire legacy<ns>/…ids; guarded against a still- writing old client by a recent-writer window and per-client version stamps),prefix_namespace(its inverse for the post-fold delta),import_control_records(copy serve's filesystem_control/tree, hashes-only journal) andrevert_migration(after-image conflict detection,forceopt-in).sweepprunes the journal past the tombstone window. - Verifier reports config vs core namespace and warns when the control plane travels over a plaintext non-loopback URI.
Changed
- Path layout. Remote ids are tier-relative; core's
datastore.namespace(options.namespace, withconfig.namespaceas the fallback the previous release used) shapes only the local cache. Pull tolerates legacy<ns>/ids until folded: newerupdatedAtwins, a legacy tombstone never outranks a bare doc, and the twin outside a watermark window is fetched before deciding. Migration and rollback runbooks are in the README. - Every push stamps
_control/clients/<user@hostname>with the extension version (the same identity the lock documents carry).
From upstream keeb 2026.09.02.1
- One
MongoClientshared per cluster + repo for the life of the process, cached at module scope (core builds a fresh provider per operation, so the old per-factory cache never hit and every operation opened a new pool); connections stamped withappNameswamp:<tenantId>/<namespace>#<pid>; a failed connect is not cached. This fork keeps itsmaxPoolSizedefault of 500 and itsserverSelectionTimeoutMSoption.
Kept
The two fork guards from 2026.09.01.2 — resolveWithinCache on every
remote-supplied path and the namespaced tier root — are unchanged and still
pinned by their wiring tests.
Global Arguments
| Argument | Type | Description |
|---|---|---|
| uri | string | MongoDB URI for the cluster backing the datastore (no auth baked in) |
| username | string | MongoDB username |
| password | string | MongoDB password — supply a vault reference, never a literal |
| database | string | Database shared by all tenants/repos |
| tenantId | string | Tenant identifier; the collection prefix is t_<tenantId>_r_<namespace> |
| Argument | Type | Description |
|---|---|---|
| namespaces | array | Namespaces to fold; empty means every namespace found |
| dryRun | boolean | Report counts without writing. Defaults true — opt in to the migration. |
| legacyPrefix | string | The core datastore.namespace old clients used as the id prefix (e.g. dev-tmp-swamp). Defaults to the config namespace, which is only right when the two are equal. |
| recentWriterMinutes | number | Refuse if any prefixed path doc was updated within this window (an old client is still writing) |
| force | boolean | Override the recent-writer and client-version guards |
| Argument | Type | Description |
|---|---|---|
| namespaces | array | Namespaces to prefix; empty means every namespace found |
| legacyPrefix | string | Core namespace to prefix with; defaults to the config namespace |
| since | string | ISO timestamp; docs updated after it are prefixed (use the fold run's startedAt) |
| dryRun | boolean |
| Argument | Type | Description |
|---|---|---|
| namespace | string | Namespace whose _control collection receives the records (the serve repo's core namespace, or the config namespace when core's is unset) |
| controlDir | string | Absolute path of the filesystem _control directory to import |
| coreNamespace | string | Core datastore.namespace of the serve repo; keys are stored under <coreNamespace>/_control/ when set, _control/ otherwise |
| dryRun | boolean | |
| maxBytes | number | Reject files larger than this |
| Argument | Type | Description |
|---|---|---|
| namespace | string | Namespace the run belongs to |
| runId | string | Run id from a previous migration result |
| dryRun | boolean | |
| force | boolean | Overwrite records that changed after the migration. Echoes the conflict count; use only after a dry run. |
| Argument | Type | Description |
|---|---|---|
| namespaces | array | Namespaces to inspect; empty means every namespace found |
| Argument | Type | Description |
|---|---|---|
| namespaces | array | Namespaces to sweep; empty means every namespace found |
| dryRun | boolean | Report without deleting. Defaults true — opt in to deletion. |
| graceMinutes | number | Spare blobs created within this many minutes (in-flight pushes) |
| tombstoneDays | number | Spare tombstones deleted within this many days. Must exceed the longest gap between any peer's syncs, or a dormant peer resurrects deleted files. |
| skipBlobs | boolean | Prune tombstones only, leaving bytes for a maintenance window |
| activeIdleHours | number | A namespace written more recently than this counts as active; its blobs are swept only if they carry createdAt |
| Argument | Type | Description |
|---|---|---|
| namespaces | array | Namespaces to compact; empty means every namespace found |
| minReusableMb | number | Skip collections holding less reusable space than this |
Resources
Reclaim space in a MongoDB-backed swamp datastore: snapshot every namespace, prune tombstones past grace and blobs no live path references, then return the freed space to the filesystem. Requires a @magistr/mongodb-datastore/maintenance instance named datastore-maintenance.
Stores swamp runtime coordination and datastore bytes in MongoDB — distributed locks with TTL + heartbeat + nonce fencing, a control-plane store for serve HA (heartbeats, cron de-dup, token secrets), and manifest + content-addressed blob sync of the datastore tier between local cache and MongoDB with two-phase push. Blobs over 15MB are transparently chunked across multiple docs so they fit under MongoDB's 16MB BSON limit. Scoped by tenant + repo namespace. Requires MongoDB 4.0+ running as a repl
2026.09.01.2
Merges upstream keeb 2026.08.19.2 into the @magistr fork, keeping two fork
guards the upstream tree does not carry and fixing a third defect found by
live testing.
Fixed: the incremental push was inert (pre-existing, also on 2026.09.01.1)
Core calls markDirty and hydrateFile with a path that ALREADY carries the
namespace (<ns>/data/...), while tierRoot scopes only the LOCAL side — every
remote _id, the local walk and isSecretsPath speak tier-relative paths.
swamp-club#1554 records the same asymmetry for @swamp/s3-datastore's
lazy-hydration hook.
One prefix broke three things: the dirty journal recorded paths no walk could
match, so ordinary writes NEVER reached the remote and only a full walk
persisted; isSecretsPath/isExcludedPath stopped matching, so the vault tier
was no longer filtered out of the journal; and hydrateFile looked up an _id
no remote doc carries.
toTierRelative() normalizes at both hooks, before the secrets check. It strips
only when the remainder begins with a real tier directory, so a tier-relative
path is never mangled. Measured live: a namespaced repo held 37 local files and
0 remote paths; after the fix, live paths went 70 -> 100 -> 144 over two runs.
Two fork guards kept, NOT inherited from upstream
- Path-traversal confinement. Upstream composes
`${cachePath}/${relPath}`by raw concatenation. One database is shared by every repo and tenant (isolation is only a collection prefix), so a remote_idis untrusted: a doc named../../../../.ssh/authorized_keyswould be written through — or unlinked when carryingdeletedAt.resolveWithinCache/isSafeRelPathrestored and re-applied to all five merged write sites. - Namespace tier-root scoping. Upstream roots reads/writes at the bare cache
path. Core reads the tier one segment deeper, so that builds a second,
invisible tier the reader never sees; push then refuses with "un-migrated data
found at root level" and
namespace migratecannot recover (swamp-club#1458/#1554). Worse,pushChangedtombstones every remote path absent from its local walk — rooted wrongly, that walk finds nothing and one push tombstones the whole namespace.tierRoot()restored inconfig.tsand applied increateSyncService.
Both carry a comment naming them as fork guards so a later merge cannot quietly drop them again.
From upstream
Dirty tracking rides an append-only journal with ancestor coalescing (O(1)
markDirty); push reconciles dirty roots in batches rather than one round-trip
each. Sidecar becomes an interned getSidecar() plus reconcileWatermark.
Blob docs gain createdAt for the orphan sweep's grace window. Adds
@magistr/mongodb-datastore/maintenance and the datastore-maintenance
workflow — inventory, sweep orphaned blobs and aged tombstones, compact — over
every namespace in one execution. manifest.yaml gains models:/workflows:;
quality.yaml's methods suite moves na -> present.
Tests
89 across seven suites. Master's four newer suites are KEPT, not replaced by
upstream's — the branch predates them and would have deleted them. New tests pin
the guards' WIRING, not just their helpers: every prior test exercised
tierRoot() and resolveWithinCache() as functions, which is exactly how the
upstream merge regressed both while all 17 stayed green. Each new test is
mutation-verified.
Added 1 models. Added 1 workflows
2026.09.01.1
Re-release of 2026.08.31.1, whose publish never reached the registry — the
extension-publish job only fires on a version bump, so a version that fails to
publish stays stranded until the next one. Content is unchanged from
2026.08.31.1; its notes are carried forward below so the registry keeps a full
record.
Fixed
Sync tier is now rooted at the namespaced cache path. Swamp core hands
createSyncServicethe bare cache path (the repoId-keyed default, sinceresolveCachePathreturnsundefinedas it does for every remote datastore) but reads and writes the tier throughDefaultDatastorePathResolver.datastorePath(), which prepends the namespace as the outermost segment —{cache}/{namespace}/data/.... The sync service walked and wrote the bare path, so everysync --pullbuilt a second, invisible copy of the whole tier at the cache root. Artifacts stayed queryable through the catalog whileswamp data getreported "Data not found";sync --pushthen refused with "un-migrated data found at root level", anddatastore namespace migratecannot recover once both layouts exist.Same defect fixed in
@swamp/s3-datastorefor swamp-club#1458 and #1554. S3 was less exposed because its remote keys embed the namespace, so the segment round-trips; this extension partitions by collection prefix (t_<tenant>_r_<ns>_*) with tier-relative_ids, so nothing reintroduced it.tierRoot()scopes the local side only — remote_ids stay tier-relative, so existing MongoDB collections need no migration. The sidecar moves alongside the tier it describes; a cold-start sidecar haslastPulledAt: null, which already suppresses the reconciliation tombstone pass infullWalkPush, so the first push after upgrading uploads without deleting. An empty namespace returns the bare cache path, byte-identical to a non-namespaced repo.Upgrading does not repair a cache that is already split. Merge the root tier into
{cache}/{namespace}/(rsync -a --ignore-existing, version directories are immutable so a merge is safe), remove the root tier directories, then push.Pull no longer writes or deletes outside the cache root.
pullChangedtook its local target straight from a remote path doc's_idand interpolated it into${cachePath}/${_id}for the write, the hash pre-check, and thedeletedAtunlink — with no containment check. One MongoDB database holds every repo's tier and isolation is only a collection prefix, so an_idis untrusted input: a doc named../../../../.ssh/authorized_keyswas written through, and withdeletedAtset, unlinked, anywhere the process could reach.resolveWithinCache()now rejects absolute paths,./..segments, empty segments, backslashes and NUL at every join. Unsafe docs are skipped at admission (with a warning) rather than thrown on, so one planted document cannot wedge every future pull.deno task testnow grants the permissions its own suite needs. The task was baredeno test, so all 17 sidecar tests failed withNotCapable: Requires write access to <TMP>— the suite could never pass in CI. Now runs with--allow-read --allow-write --allow-env.
Added
config_test.tscoveringtierRoot: namespaced paths, solo mode (empty namespace), whitespace-only namespace, trailing-slash normalisation, composition with a tier-relative path, and that the namespace used for collection prefixing is never mutated.sync_adversarial_test.ts— path traversal, absolute paths, backslash/NUL injection, secrets-tier boundaries, and regex-state reuse.sync_coverage_test.ts— regression cover for the load-bearing guards: thelastPulledAt !== null && !lazyPullActivegate that keeps a cold-start push from tombstoning the datastore, and the tier-root/sidecar scoping.sync_property_test.ts— fast-check properties (gated byFC_NUM_RUNS) for tier placement and the containment invariant: an accepted path can never resolve outside the cache root, for arbitrary remote input.quality.yaml, and this changelog.
- Has README or module doc2/2earned
- README has a code example1/1earned
- README is substantive1/1earned
- Most symbols documented1/1earned
- No slow types (deprecated)1/1earned
- Dependencies pass trust audit2/2earned
- Has description1/1earned
- Platform support declared (or universal)2/2earned
- License declared1/1earned
- Verified public repository2/2earned