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.tsThe file path is relative to the extensions/datastores/ directory within the
extension root.