Skip to main content

DATASTORE PROVIDER

A datastore provider is a TypeScript module that implements data persistence for a custom backend (cloud object storage, networked filesystem, etc.). The file lives in extensions/datastores/ and exports a datastore object that swamp loads at runtime.

For datastore configuration and usage (setup, sync, locking, CLI commands), see the Datastore Configuration reference.

Export Shape

import { z } from "npm:zod@4";

export const datastore = {
  type: "@mycollective/my-datastore",
  name: "My Datastore",
  description: "Store data in my backend",
  configSchema: z.object({
    bucket: z.string().describe("Storage bucket name"),
    region: z.string().optional().describe("Region"),
  }),
  createProvider(config: Record<string, unknown>): DatastoreProvider {
    // validate config, return provider instance
  },
};

Fields

Field Type Description
type string Unique identifier in @collective/name format. Reserved collectives: swamp, si.
name string Human-readable display name.
description string Short description shown in swamp datastore output.
configSchema z.ZodTypeAny Zod schema that validates the config object in .swamp.yaml.
createProvider (config: Record<string, unknown>) => DatastoreProvider Factory that returns a provider instance.

DatastoreProvider Interface

The object returned by createProvider must implement the following methods.

Method Signature Required Description
createLock (datastorePath: string, options?: LockOptions) => DistributedLock Yes Create a distributed lock for the given path.
createVerifier () => DatastoreVerifier Yes Create a health-check verifier.
resolveDatastorePath (repoDir: string) => string Yes Resolve the datastore path for a given repo.
createSyncService (repoDir: string, cachePath: string) => DatastoreSyncService No Create a sync service for bidirectional transfer.
resolveCachePath (repoDir: string) => string | undefined No Resolve the local cache path. Return undefined if no cache is used.

Supporting Interfaces

DistributedLock

Coordinates exclusive access to prevent concurrent writers from corrupting data.

Method Signature Description
acquire () => Promise<void> Acquire the lock. Retries until timeout.
release () => Promise<void> Release the lock.
withLock <T>(fn: () => Promise<T>) => Promise<T> Acquire, run fn, release. Releases on error.
inspect () => Promise<LockInfo | null> Return current lock metadata, or null if unheld.
forceRelease (expectedNonce: string) => Promise<boolean> Force-release a stale lock matching the nonce.

LockOptions

Field Type Default Description
lockKey string Custom lock key (overrides default).
ttlMs number 30000 Lock lifetime before considered stale.
retryIntervalMs number 1000 Retry interval when lock is held.
maxWaitMs number 60000 Maximum wait before giving up.

DatastoreVerifier

Method Signature Description
verify () => Promise<DatastoreHealthResult> Run a health check.

DatastoreHealthResult

Field Type Description
healthy boolean Whether the backend is reachable.
message string Human-readable status message.
latencyMs number Round-trip latency in milliseconds.
datastoreType string The provider type identifier.
details Record<string, string> (optional) Extra diagnostic data.

DatastoreSyncService

Implement this if the backend supports bidirectional file synchronization.

Method Signature Required Description
pullChanged (options?: DatastoreSyncOptions) => Promise<number | void> Yes Download changed files from the remote.
pushChanged (options?: DatastoreSyncOptions) => Promise<number | void> Yes Upload changed files to the remote.
markDirty (options?: DatastoreSyncOptions) => Promise<void> Yes Signal that the local cache has uncommitted work.
capabilities () => SyncCapabilities No Advertise what this sync service supports.
hydrateFile (relPath: string, options?: DatastoreSyncOptions) => Promise<boolean> No Download a single file on demand. Returns true on success.

pullChanged and pushChanged return the number of files transferred, or void.

markDirty is called before every cache write. Implementations must be idempotent and cheap. When options.relPath is present, it identifies the cache-relative path about to be written. When absent, a bulk mutation occurred and the next pushChanged must perform a full walk. See the markDirty contract in the swamp source for the complete set of rules.

capabilities returns a SyncCapabilities object advertising optional features the sync service supports. Omit this method when no optional features are implemented.

hydrateFile downloads a single content file from the remote backend to the local cache. The relPath argument is forward-slash-normalized and cache-relative. Implementations must write atomically (write to a temporary path, then rename). Required only when the extension advertises lazyHydration: true in its capabilities.

DatastoreSyncOptions

Options passed to pullChanged, pushChanged, markDirty, and hydrateFile.

Field Type Description
signal AbortSignal Optional. Abort signal for bounding sync duration.
relPath string Optional. Cache-relative, forward-slash-normalized path of the file being written. Set only on markDirty calls.
context SyncContext Optional. Domain-level sync context. Passed when the extension advertises scopedSync in its capabilities.
metadataOnly boolean Optional. When true, pullChanged downloads only metadata files and skips content files under data/.

SyncContext

Describes what a sync operation is about.

Field Type Description
models ReadonlyArray<{ modelType: string; modelId: string }> Models relevant to the current operation.

SyncCapabilities

Capabilities a sync service advertises to swamp core.

Field Type Description
scopedSync boolean Optional. When true, pullChanged and pushChanged receive context.models for scoped transfers.
lazyHydration boolean Optional. When true, the extension supports metadataOnly pulls and the hydrateFile method.

Conformance Testing

The @systeminit/swamp-testing package provides conformance assertions for datastore providers and sync services.

assertSyncServiceConformance

import { assertSyncServiceConformance } from "@systeminit/swamp-testing";

Asserts that a DatastoreSyncService implementation satisfies the behavioral contract. Tests that pullChanged, pushChanged, and markDirty exist and are callable. When capabilities() is present, validates it returns a well-formed SyncCapabilities object.

Deno.test("s3 sync service contract", async () => {
  const syncService = provider.createSyncService!("/repo", "/cache");
  await assertSyncServiceConformance(syncService);
});

With capability assertions:

Deno.test("s3 sync service with scoped sync", async () => {
  const syncService = provider.createSyncService!("/repo", "/cache");
  await assertSyncServiceConformance(syncService, { expectScopedSync: true });
});

SyncServiceConformanceOptions

Field Type Description
expectScopedSync boolean Optional. When true, asserts capabilities().scopedSync === true.

Packaging

Declare the datastore in your extension's manifest:

datastores:
  - my_datastore.ts

The file path is relative to the extensions/datastores/ directory within the extension root.