@starkeep/sync-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sync-state-sqlite.ts","../src/change-notifier.ts","../src/watermarks.ts","../src/file-sync-engine.ts","../src/sync-engine.ts","../src/residency.ts","../src/transports/in-process-transport.ts","../src/errors.ts","../src/transports/http-transport.ts","../src/transports/http-server.ts"],"sourcesContent":["import type { DatabaseSync } from \"node:sqlite\";\nimport type { SyncStateStore, Watermarks } from \"./types.js\";\n\nconst CREATE_TABLE_SQL = `\n CREATE TABLE IF NOT EXISTS sync_state (\n key TEXT PRIMARY KEY,\n value_json TEXT NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))\n )\n`;\n\nconst WATERMARKS = \"watermarks\";\nconst PEER_WATERMARKS = \"peer_watermarks\";\nconst HLC_CLOCK = \"hlc_clock\";\n\nexport interface SqliteSyncStateStoreOptions {\n readonly db: DatabaseSync;\n}\n\nexport function createSqliteSyncStateStore(\n options: SqliteSyncStateStoreOptions,\n): SyncStateStore {\n const { db } = options;\n db.exec(CREATE_TABLE_SQL);\n\n const getStmt = db.prepare(\n \"SELECT value_json FROM sync_state WHERE key = ?\",\n );\n const setStmt = db.prepare(\n `INSERT INTO sync_state (key, value_json, updated_at)\n VALUES (?, ?, strftime('%s','now'))\n ON CONFLICT(key) DO UPDATE SET\n value_json = excluded.value_json,\n updated_at = excluded.updated_at`,\n );\n\n function getJson<T>(key: string): T | null {\n const row = getStmt.get(key) as { value_json: string } | undefined;\n if (!row) return null;\n return JSON.parse(row.value_json) as T;\n }\n\n function setJson<T>(key: string, value: T): void {\n setStmt.run(key, JSON.stringify(value));\n }\n\n return {\n async getWatermarks(): Promise<Watermarks> {\n return getJson<Watermarks>(WATERMARKS) ?? {};\n },\n async setWatermarks(watermarks: Watermarks): Promise<void> {\n setJson(WATERMARKS, watermarks);\n },\n async getPeerWatermarks(): Promise<Watermarks> {\n return getJson<Watermarks>(PEER_WATERMARKS) ?? {};\n },\n async setPeerWatermarks(watermarks: Watermarks): Promise<void> {\n setJson(PEER_WATERMARKS, watermarks);\n },\n async getHlcClockState(): Promise<\n { wallTime: number; counter: number } | null\n > {\n return getJson<{ wallTime: number; counter: number }>(HLC_CLOCK);\n },\n async setHlcClockState(state: {\n wallTime: number;\n counter: number;\n }): Promise<void> {\n setJson(HLC_CLOCK, state);\n },\n };\n}\n","import type { ChangeNotifier, ChangeListener, ChangeEvent } from \"./types.js\";\n\nexport function createChangeNotifier(): ChangeNotifier {\n const listeners = new Set<ChangeListener>();\n\n return {\n subscribe(listener: ChangeListener): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n\n emit(event: ChangeEvent): void {\n for (const listener of listeners) {\n listener(event);\n }\n },\n };\n}\n","import { ZERO_HLC, compareHLC, maxHLC, type HLCTimestamp } from \"@starkeep/protocol-primitives\";\nimport type { AnyRecord } from \"@starkeep/protocol-primitives\";\nimport type { Watermarks, AppSyncableRowEntry } from \"./types.js\";\n\n/**\n * Compute the responder's watermarks across a set of records — the\n * `MAX(updated_at)` per nodeId. Caller advertises these on the next exchange\n * round so the responder ships only records the caller hasn't seen yet.\n */\nexport function computeRecordWatermarks(records: Iterable<AnyRecord>): Watermarks {\n const out: Watermarks = {};\n for (const r of records) {\n advanceWatermark(out, r.updatedAt);\n }\n return out;\n}\n\nexport function computeAppSyncableWatermarks(\n rows: Iterable<AppSyncableRowEntry>,\n): Watermarks {\n const out: Watermarks = {};\n for (const r of rows) {\n advanceWatermark(out, r.timestamp);\n }\n return out;\n}\n\n/** Advance `watermarks[hlc.nodeId]` to `max(current, hlc)`. */\nexport function advanceWatermark(watermarks: Watermarks, hlc: HLCTimestamp): void {\n const node = hlc.nodeId;\n const existing = watermarks[node];\n if (!existing || compareHLC(hlc, existing) > 0) {\n watermarks[node] = hlc;\n }\n}\n\n/** Merge `incoming` into `into`, taking the max per nodeId. */\nexport function mergeWatermarks(into: Watermarks, incoming: Watermarks): Watermarks {\n const out: Watermarks = { ...into };\n for (const [node, hlc] of Object.entries(incoming)) {\n const existing = out[node];\n out[node] = existing ? maxHLC(existing, hlc) : hlc;\n }\n return out;\n}\n\n/** Watermark for `nodeId`, or `ZERO_HLC` if unseen. */\nexport function watermarkFor(watermarks: Watermarks, nodeId: string): HLCTimestamp {\n return watermarks[nodeId] ?? ZERO_HLC;\n}\n\n/**\n * Return records the peer hasn't seen yet, judged against `peerWatermarks`:\n * `record.updatedAt > peerWatermarks[record.updatedAt.nodeId] ?? ZERO_HLC`.\n */\nexport function selectUnseen<T extends { updatedAt: HLCTimestamp }>(\n records: T[],\n peerWatermarks: Watermarks,\n): T[] {\n return records.filter(\n (r) => compareHLC(r.updatedAt, watermarkFor(peerWatermarks, r.updatedAt.nodeId)) > 0,\n );\n}\n\nexport function selectUnseenAppSyncable(\n rows: AppSyncableRowEntry[],\n peerWatermarks: Watermarks,\n): AppSyncableRowEntry[] {\n return rows.filter(\n (r) => compareHLC(r.timestamp, watermarkFor(peerWatermarks, r.timestamp.nodeId)) > 0,\n );\n}\n","import type { ObjectStorageAdapter } from \"@starkeep/storage-adapter\";\nimport type { FileSyncEngine, FileSyncManifest, FileEntry } from \"./types.js\";\n\nexport function createFileSyncEngine(): FileSyncEngine {\n // Object-storage keys currently being transferred in this process. Each\n // transferFile call acquires the key on entry and releases on exit. Used by\n // the retry pass to skip records whose transfer is already in flight.\n const inFlightKeys = new Set<string>();\n\n return {\n isTransferInFlight(key: string): boolean {\n return inFlightKeys.has(key);\n },\n\n async getFilesToPush(\n localStorage: ObjectStorageAdapter,\n remoteStorage: ObjectStorageAdapter,\n entries: FileEntry[],\n ): Promise<FileSyncManifest[]> {\n const manifests: FileSyncManifest[] = [];\n\n for (const entry of entries) {\n const existsRemotely = await remoteStorage.has(entry.key);\n if (!existsRemotely) {\n const localFile = await localStorage.get(entry.key);\n if (localFile) {\n manifests.push({\n fileHash: entry.key,\n objectStorageKey: entry.key,\n sizeBytes: localFile.size,\n mimeType: entry.mimeType,\n });\n }\n }\n }\n\n return manifests;\n },\n\n async getFilesToPull(\n localStorage: ObjectStorageAdapter,\n remoteStorage: ObjectStorageAdapter,\n entries: FileEntry[],\n ): Promise<FileSyncManifest[]> {\n const manifests: FileSyncManifest[] = [];\n\n for (const entry of entries) {\n const existsLocally = await localStorage.has(entry.key);\n if (!existsLocally) {\n const remoteFile = await remoteStorage.get(entry.key);\n if (remoteFile) {\n manifests.push({\n fileHash: entry.key,\n objectStorageKey: entry.key,\n sizeBytes: remoteFile.size,\n mimeType: entry.mimeType,\n });\n }\n }\n }\n\n return manifests;\n },\n\n async transferFile(\n manifest: FileSyncManifest,\n source: ObjectStorageAdapter,\n destination: ObjectStorageAdapter,\n ): Promise<boolean> {\n const key = manifest.objectStorageKey;\n if (inFlightKeys.has(key)) {\n return false;\n }\n inFlightKeys.add(key);\n try {\n // Destination already has it — no-op success. Lets callers fire-and-\n // forget transferFile without needing to HEAD first.\n if (await destination.has(key)) {\n return true;\n }\n const file = await source.get(key);\n if (!file) {\n return false;\n }\n await destination.put(key, file.data, {\n contentType: manifest.mimeType,\n });\n return true;\n } finally {\n inFlightKeys.delete(key);\n }\n },\n };\n}\n","import {\n compareHLC,\n serializeHLC,\n ZERO_HLC,\n type AnyRecord,\n type HLCTimestamp,\n type StarkeepId,\n} from \"@starkeep/protocol-primitives\";\nimport type { ObjectStorageAdapter } from \"@starkeep/storage-adapter\";\n\n// Mirror of `FILE_RECORDS_TABLE` from `@starkeep/shared-space-api`. The sync\n// engine cannot import that package (cycle), but it needs the table name to\n// recognize which app-syncable rows carry blobs. Keep these in sync.\nconst FILE_RECORDS_TABLE = \"_starkeep_sync_records\";\nimport type {\n AppSyncableRowEntry,\n ExchangeResult,\n FileSyncEngine,\n FileSyncManifest,\n SyncEngine,\n SyncEngineOptions,\n Watermarks,\n} from \"./types.js\";\nimport { createChangeNotifier } from \"./change-notifier.js\";\nimport { createFileSyncEngine } from \"./file-sync-engine.js\";\nimport { advanceWatermark } from \"./watermarks.js\";\n\n/**\n * Sync engine: drives one version-vector exchange round per tick.\n *\n * Blob transfer is gated on the same watermark that drives metadata transfer.\n * A record's blob is pushed before its metadata ships; a record's blob is\n * pulled before its receipt is acknowledged. If either fails, the watermark\n * doesn't advance past it, and the next round naturally retries.\n *\n * Shared records (SR) and app-record rows in the reserved `_starkeep_sync_records`\n * table (AR) are interleaved per nodeId in HLC order so the contiguous-prefix\n * watermark rule covers both streams: a blob failure on an AR row blocks any\n * later SR record on the same nodeId from shipping in the same round (and vice\n * versa). Without that, the per-nodeId watermark could leapfrog a failed item.\n *\n * There is no scan-everything reconciliation pass. There is no `sync_status`.\n * Steady state issues zero storage HEAD requests: the watermark delta tells\n * us exactly which records (and therefore which blobs) need attention.\n */\nexport function createSyncEngine(options: SyncEngineOptions): SyncEngine {\n const {\n localDatabaseAdapter,\n localObjectStorage,\n remoteObjectStorage,\n transport,\n clock,\n syncState,\n appSyncableSource,\n syncSharedRecords = true,\n pageLimit = 1000,\n scanPageSize = 500,\n } = options;\n\n const changeNotifier = createChangeNotifier();\n const fileSyncEngine = createFileSyncEngine();\n\n async function loadOwnWatermarks(): Promise<Watermarks> {\n if (!syncState) return {};\n return syncState.getWatermarks();\n }\n\n async function loadPeerWatermarks(): Promise<Watermarks> {\n if (!syncState) return {};\n return syncState.getPeerWatermarks();\n }\n\n return {\n async exchange(): Promise<ExchangeResult> {\n const ownWatermarks = await loadOwnWatermarks();\n const peerWatermarks = await loadPeerWatermarks();\n\n // ---------------------------------------------------------------------\n // Outbound: gather SR records and AR/AW rows the peer hasn't seen, then\n // walk per nodeId in HLC order with a contiguous-prefix rule. Blobs\n // (SR or AR) are pushed before their owning item is allowed to ship.\n // ---------------------------------------------------------------------\n //\n // Both the SR scan and the AR/AW scanSince path below are cursor-\n // paginated so records past any fixed window are reachable: we iterate\n // the DB in its default order and apply the per-nodeId watermark\n // filter inline, advancing the cursor across all rows (even the ones\n // we skip). This means future rounds don't get stuck re-scanning a\n // head-of-table window that's already been shipped.\n //\n // Performance follow-up: production storage adapters should push the\n // watermark filter into the query — e.g. a per-nodeId index plus\n // `WHERE updated_at > peerWatermark[nodeId]` — so steady-state syncs\n // don't read every row to find nothing. The current loop is O(N) per\n // round when the watermark is at the latest record. Acceptable for\n // current poll volumes; revisit if scans get hot. Same caveat applies\n // to the responder-side scan in in-process-transport.ts.\n const recordCandidates: AnyRecord[] = [];\n // Only the Drive channel ships shared records. Per-app channels\n // set syncSharedRecords=false and leave this scan empty — they carry only\n // app-specific rows.\n if (syncSharedRecords) {\n let scanCursor: string | undefined = undefined;\n let scanHasMore = true;\n while (recordCandidates.length < pageLimit && scanHasMore) {\n const page = await localDatabaseAdapter.query({\n limit: scanPageSize,\n ...(scanCursor !== undefined ? { cursor: scanCursor } : {}),\n });\n if (page.records.length === 0) break;\n for (const r of page.records) {\n const peerHlc = peerWatermarks[r.updatedAt.nodeId];\n if (!peerHlc || compareHLC(r.updatedAt, peerHlc) > 0) {\n recordCandidates.push(r);\n if (recordCandidates.length >= pageLimit) break;\n }\n }\n scanHasMore = page.hasMore;\n scanCursor = page.nextCursor ?? undefined;\n }\n }\n\n // AR/AW scan: same cursor pattern as the SR loop above. scanSince\n // paginates by `updated_at` (serialized HLC), so each page advances\n // the cursor across both filtered and selected rows. Combined cap of\n // `pageLimit` is enforced across all (namespace, table) pairs.\n const appRowCandidates: AppSyncableRowEntry[] = [];\n if (appSyncableSource) {\n const zeroStr = serializeHLC(ZERO_HLC);\n outer: for (const ns of appSyncableSource.namespaces.list()) {\n for (const tableInfo of ns.tables) {\n let appScanCursor: string | undefined = undefined;\n let appScanHasMore = true;\n while (\n recordCandidates.length + appRowCandidates.length < pageLimit &&\n appScanHasMore\n ) {\n let page: { rows: AppSyncableRowEntry[]; nextCursor: string | null; hasMore: boolean };\n try {\n page = await appSyncableSource.applier.scanSince(\n ns.appId,\n tableInfo.name,\n zeroStr,\n {\n limit: scanPageSize,\n ...(appScanCursor !== undefined ? { cursor: appScanCursor } : {}),\n },\n );\n } catch (err) {\n console.warn(\n `[sync] exchange scanSince failed for ${ns.appId}.${tableInfo.name}: ${(err as Error).message}`,\n );\n break;\n }\n if (page.rows.length === 0) break;\n for (const r of page.rows) {\n const peerHlc = peerWatermarks[r.timestamp.nodeId];\n if (!peerHlc || compareHLC(r.timestamp, peerHlc) > 0) {\n appRowCandidates.push(r);\n if (\n recordCandidates.length + appRowCandidates.length >=\n pageLimit\n ) {\n break;\n }\n }\n }\n appScanHasMore = page.hasMore;\n appScanCursor = page.nextCursor ?? undefined;\n }\n if (\n recordCandidates.length + appRowCandidates.length >=\n pageLimit\n ) {\n break outer;\n }\n }\n }\n }\n\n // SR side is already capped at pageLimit by the cursor loop above.\n // If we also collected AR/AW rows, the combined set may exceed\n // pageLimit, so take the globally-earliest-HLC pageLimit items.\n // Items deferred here ship next round because the peer's watermarks\n // won't have advanced past them.\n const cappedRecords: AnyRecord[] = [];\n const cappedAppRows: AppSyncableRowEntry[] = [];\n if (\n recordCandidates.length + appRowCandidates.length <= pageLimit\n ) {\n cappedRecords.push(...recordCandidates);\n cappedAppRows.push(...appRowCandidates);\n } else {\n type Tagged =\n | { kind: \"r\"; rec: AnyRecord; hlc: HLCTimestamp }\n | { kind: \"a\"; row: AppSyncableRowEntry; hlc: HLCTimestamp };\n const tagged: Tagged[] = [\n ...recordCandidates.map(\n (r): Tagged => ({ kind: \"r\", rec: r, hlc: r.updatedAt }),\n ),\n ...appRowCandidates.map(\n (e): Tagged => ({ kind: \"a\", row: e, hlc: e.timestamp }),\n ),\n ];\n tagged.sort((a, b) => compareHLC(a.hlc, b.hlc));\n for (const t of tagged.slice(0, pageLimit)) {\n if (t.kind === \"r\") cappedRecords.push(t.rec);\n else cappedAppRows.push(t.row);\n }\n }\n\n const outboundByNode = groupOutboundByNodeId(\n cappedRecords,\n cappedAppRows,\n );\n\n const outboundRecords: AnyRecord[] = [];\n const outboundAppRows: AppSyncableRowEntry[] = [];\n const peerSafeAdvance = new Map<string, HLCTimestamp>();\n\n for (const [nodeId, items] of outboundByNode) {\n for (const item of items) {\n const manifest = outboundManifest(item);\n if (manifest) {\n const ok = await transferBlobSafe(\n manifest,\n localObjectStorage,\n remoteObjectStorage,\n fileSyncEngine,\n \"upload\",\n outboundItemId(item),\n );\n if (!ok) break;\n }\n if (item.kind === \"record\") {\n outboundRecords.push(item.record);\n peerSafeAdvance.set(nodeId, item.record.updatedAt);\n } else {\n outboundAppRows.push(item.entry);\n peerSafeAdvance.set(nodeId, item.entry.timestamp);\n }\n }\n }\n\n const response = await transport.exchange({\n watermarks: ownWatermarks,\n records: outboundRecords.length > 0 ? outboundRecords : undefined,\n appSyncableRows: outboundAppRows.length > 0 ? outboundAppRows : undefined,\n limit: pageLimit,\n });\n\n // ---------------------------------------------------------------------\n // Inbound: apply records (and pull their blobs) per nodeId in HLC order,\n // interleaving SR snapshots and AR/AW rows. Own watermark advances only\n // past items that fully landed locally; peerWatermarks also advances\n // past *every* item we received — the peer demonstrated it has them by\n // shipping them — which prevents us re-shipping items that originated\n // on the peer's side.\n // ---------------------------------------------------------------------\n // A per-app channel (syncSharedRecords=false) must never apply shared\n // records. The responder shouldn't ship them, but guard inbound too so\n // the channel split holds even if a peer over-ships.\n if (!syncSharedRecords && (response.records?.length ?? 0) > 0) {\n console.warn(\n `[sync] dropped ${response.records?.length ?? 0} shared record(s) received on a per-app channel (syncSharedRecords=false)`,\n );\n }\n const inboundByNode = groupInboundByNodeId(\n syncSharedRecords ? response.records : [],\n response.appSyncableRows,\n );\n const appliedIds: StarkeepId[] = [];\n const ownSafeAdvance = new Map<string, HLCTimestamp>();\n\n for (const [nodeId, items] of inboundByNode) {\n let contiguous = true;\n for (const item of items) {\n const itemHlc = inboundItemHlc(item);\n\n // The peer has this item (it sent it to us) — peerWatermarks\n // can advance past it regardless of our local apply outcome.\n const existing = peerSafeAdvance.get(nodeId);\n if (!existing || compareHLC(itemHlc, existing) > 0) {\n peerSafeAdvance.set(nodeId, itemHlc);\n }\n\n if (item.kind === \"record\") {\n const snapshot = item.record;\n const current = await localDatabaseAdapter.get(snapshot.id);\n const metadataAlreadyApplied =\n current !== null &&\n compareHLC(current.updatedAt, snapshot.updatedAt) >= 0;\n\n if (!metadataAlreadyApplied) {\n clock.receive(snapshot.updatedAt);\n await localDatabaseAdapter.put(snapshot);\n }\n\n // Always attempt blob pull when the record needs one. The\n // \"metadata already applied\" branch covers the case where a\n // prior round landed the row but failed the blob pull (Staged\n // residency) — without this, the watermark would advance past\n // the failed blob in round 2 and the record would be stuck.\n const manifest = manifestForRecord(snapshot);\n const blobOk = await transferBlobSafe(\n manifest,\n remoteObjectStorage,\n localObjectStorage,\n fileSyncEngine,\n \"download\",\n snapshot.id,\n );\n if (!blobOk) {\n // Metadata applied (or already was), but blob fetch failed.\n // Don't advance own watermark past this item — next round the\n // responder still ships it (because our advertised watermarks\n // haven't moved past it) and we'll retry the blob.\n contiguous = false;\n continue;\n }\n\n // Only fire the change notifier when metadata was newly applied\n // this round. A blob-retry on already-applied metadata isn't a\n // user-visible \"data change.\"\n if (!metadataAlreadyApplied) appliedIds.push(snapshot.id);\n if (contiguous) ownSafeAdvance.set(nodeId, snapshot.updatedAt);\n } else {\n const entry = item.entry;\n if (!appSyncableSource) {\n // No applier configured — skip without advancing own watermark\n // (we have no way to durably accept this row).\n contiguous = false;\n continue;\n }\n clock.receive(entry.timestamp);\n try {\n await appSyncableSource.applier.apply(entry);\n } catch (err) {\n console.warn(\n `[sync] appSyncableRow apply failed (app=${entry.appId} table=${entry.table}): ${(err as Error).message}`,\n );\n contiguous = false;\n continue;\n }\n\n const manifest = manifestForAppRow(entry);\n const blobOk = await transferBlobSafe(\n manifest,\n remoteObjectStorage,\n localObjectStorage,\n fileSyncEngine,\n \"download\",\n `${entry.appId}.${entry.table}`,\n );\n if (!blobOk) {\n contiguous = false;\n continue;\n }\n\n if (contiguous) ownSafeAdvance.set(nodeId, entry.timestamp);\n }\n }\n }\n\n // ---------------------------------------------------------------------\n // Persist updated watermarks.\n // ---------------------------------------------------------------------\n if (syncState) {\n const nextOwnWatermarks: Watermarks = { ...ownWatermarks };\n for (const hlc of ownSafeAdvance.values()) {\n advanceWatermark(nextOwnWatermarks, hlc);\n }\n await syncState.setWatermarks(nextOwnWatermarks);\n\n const nextPeerWatermarks: Watermarks = { ...peerWatermarks };\n for (const hlc of peerSafeAdvance.values()) {\n advanceWatermark(nextPeerWatermarks, hlc);\n }\n await syncState.setPeerWatermarks(nextPeerWatermarks);\n }\n\n if (appliedIds.length > 0) {\n changeNotifier.emit({\n eventType: \"local-data-synced\",\n recordIds: appliedIds,\n timestamp: clock.now(),\n });\n }\n\n return {\n applied: appliedIds.length,\n shipped: outboundRecords.length + outboundAppRows.length,\n hasMore: response.hasMore,\n };\n },\n\n changeNotifier,\n };\n}\n\ntype OutboundItem =\n | { kind: \"record\"; record: AnyRecord }\n | { kind: \"appRow\"; entry: AppSyncableRowEntry };\n\ntype InboundItem =\n | { kind: \"record\"; record: AnyRecord }\n | { kind: \"appRow\"; entry: AppSyncableRowEntry };\n\nfunction outboundItemHlc(item: OutboundItem): HLCTimestamp {\n return item.kind === \"record\" ? item.record.updatedAt : item.entry.timestamp;\n}\n\nfunction inboundItemHlc(item: InboundItem): HLCTimestamp {\n return item.kind === \"record\" ? item.record.updatedAt : item.entry.timestamp;\n}\n\nfunction outboundItemId(item: OutboundItem): string {\n return item.kind === \"record\"\n ? item.record.id\n : `${item.entry.appId}.${item.entry.table}`;\n}\n\n/**\n * Merge SR records and AR/AW rows into per-nodeId buckets sorted in HLC order.\n * The contiguous-prefix watermark rule walks these buckets and stops on the\n * first failure, regardless of which stream that failure came from.\n */\nfunction groupOutboundByNodeId(\n records: AnyRecord[],\n appRows: AppSyncableRowEntry[],\n): Map<string, OutboundItem[]> {\n const out = new Map<string, OutboundItem[]>();\n for (const r of records) {\n pushToBucket(out, r.updatedAt.nodeId, { kind: \"record\", record: r });\n }\n for (const e of appRows) {\n pushToBucket(out, e.timestamp.nodeId, { kind: \"appRow\", entry: e });\n }\n for (const arr of out.values()) {\n arr.sort((a, b) => compareHLC(outboundItemHlc(a), outboundItemHlc(b)));\n }\n return out;\n}\n\nfunction groupInboundByNodeId(\n records: readonly AnyRecord[],\n appRows: readonly AppSyncableRowEntry[],\n): Map<string, InboundItem[]> {\n const out = new Map<string, InboundItem[]>();\n for (const r of records) {\n pushToBucket(out, r.updatedAt.nodeId, { kind: \"record\", record: r });\n }\n for (const e of appRows) {\n pushToBucket(out, e.timestamp.nodeId, { kind: \"appRow\", entry: e });\n }\n for (const arr of out.values()) {\n arr.sort((a, b) => compareHLC(inboundItemHlc(a), inboundItemHlc(b)));\n }\n return out;\n}\n\nfunction pushToBucket<T>(map: Map<string, T[]>, key: string, value: T): void {\n const arr = map.get(key) ?? [];\n arr.push(value);\n map.set(key, arr);\n}\n\nfunction outboundManifest(item: OutboundItem): FileSyncManifest | null {\n return item.kind === \"record\"\n ? manifestForRecord(item.record)\n : manifestForAppRow(item.entry);\n}\n\nfunction manifestForRecord(record: AnyRecord): FileSyncManifest | null {\n if (!record.objectStorageKey || record.deletedAt) return null;\n return {\n fileHash: record.contentHash || record.objectStorageKey,\n objectStorageKey: record.objectStorageKey,\n sizeBytes: record.sizeBytes,\n mimeType: record.mimeType,\n };\n}\n\n/**\n * Derive a blob manifest from an app-syncable row entry. Only the reserved\n * `_starkeep_sync_records` table carries blobs at the protocol level; plain\n * app-row tables (AW) never do. Tombstones return null — blob retention on\n * delete is a GC concern, not a sync concern.\n */\nfunction manifestForAppRow(entry: AppSyncableRowEntry): FileSyncManifest | null {\n if (entry.table !== FILE_RECORDS_TABLE) return null;\n if (entry.op === \"delete\") return null;\n const row = entry.row;\n if (!row) return null;\n const key = row[\"object_storage_key\"];\n if (typeof key !== \"string\" || key.length === 0) return null;\n const contentHash = row[\"content_hash\"];\n const mimeType = row[\"mime_type\"];\n const sizeBytes = row[\"size_bytes\"];\n return {\n fileHash:\n typeof contentHash === \"string\" && contentHash.length > 0\n ? contentHash\n : key,\n objectStorageKey: key,\n sizeBytes: typeof sizeBytes === \"number\" ? sizeBytes : Number(sizeBytes) || 0,\n mimeType: typeof mimeType === \"string\" ? mimeType : undefined,\n };\n}\n\n/**\n * Run a blob transfer through the file-sync engine, swallowing exceptions as a\n * false return so the caller can apply the contiguous-prefix rule uniformly.\n * Returns true when there is no blob to transfer.\n *\n * `transferFile` short-circuits to true if the destination already has the\n * key, so repeated invocations across ticks cost at most one HEAD per item.\n */\nasync function transferBlobSafe(\n manifest: FileSyncManifest | null,\n source: ObjectStorageAdapter,\n destination: ObjectStorageAdapter,\n fileSyncEngine: FileSyncEngine,\n direction: \"upload\" | \"download\",\n itemId: string,\n): Promise<boolean> {\n if (!manifest) return true;\n try {\n return await fileSyncEngine.transferFile(manifest, source, destination);\n } catch (err) {\n console.warn(\n `[sync] blob ${direction} failed for ${itemId} (${manifest.objectStorageKey}): ${(err as Error).message}`,\n );\n return false;\n }\n}\n","import type { ObjectStorageAdapter } from \"@starkeep/storage-adapter\";\nimport type { FileRecordRow } from \"./types.js\";\n\n/**\n * Per-record state on a single side, derived from facts already on disk.\n * There is intentionally no persisted `sync_status` column; this type names\n * what the combination of (row presence, blob presence, deletedAt) means.\n *\n * See system-design.md \"Per-record residency\" for the full rationale and how\n * the watermark serves as the durable backstop for the Staged state.\n *\n * - absent — no row for this id on this side.\n * - staged — row present, blob required, blob not yet present locally.\n * - resident — row present, blob present locally.\n * - tombstoned — `deletedAt` is set. Propagates like resident; blob GC is a\n * separate concern.\n */\nexport type RecordResidency = \"absent\" | \"staged\" | \"resident\" | \"tombstoned\";\n\n/**\n * Classify a record's residency on this side. Pass `null` for `recordRow` to\n * model \"row not present\" (returns `absent`).\n *\n * This is the single canonical derivation. Code and tests should call it\n * rather than reconstructing the predicate from `localStorage.has(key)` etc.\n *\n * Note: rows in `_starkeep_sync_records` always have a blob (the table's\n * purpose). Records that opt out of file storage live in app-syncable\n * metadata tables instead and don't reach this function.\n */\nexport async function residencyOf(\n recordRow: FileRecordRow | null,\n localStorage: ObjectStorageAdapter,\n): Promise<RecordResidency> {\n if (!recordRow) return \"absent\";\n if (recordRow.deleted_at) return \"tombstoned\";\n const blobHere = await localStorage.has(recordRow.object_storage_key);\n return blobHere ? \"resident\" : \"staged\";\n}\n","import {\n compareHLC,\n serializeHLC,\n ZERO_HLC,\n type AnyRecord,\n type HLCClock,\n} from \"@starkeep/protocol-primitives\";\nimport type { DatabaseAdapter, ObjectStorageAdapter } from \"@starkeep/storage-adapter\";\nimport type {\n SyncTransport,\n SyncExchangeRequest,\n SyncExchangeResponse,\n AppSyncableRowEntry,\n AppSyncableNamespaceStore,\n AppSyncableApplier,\n ScanCapableApplier,\n} from \"../types.js\";\n\nexport interface InProcessTransportOptions {\n readonly databaseAdapter: DatabaseAdapter;\n readonly clock: HLCClock;\n /**\n * When provided, the transport synthesizes app-syncable row entries on\n * exchange (by scanning updated_at per table) and applies incoming rows on\n * apply (LWW UPSERT).\n */\n readonly appSyncableSource?: {\n readonly namespaces: AppSyncableNamespaceStore;\n readonly applier: AppSyncableApplier;\n };\n /**\n * Object storage backing the records this transport serves. Used only as a\n * reference for the file-transfer pass elsewhere; the exchange protocol\n * itself does no blob inspection.\n */\n readonly objectStorage: ObjectStorageAdapter;\n /**\n * Channel split (responder side). When true (default), this transport\n * applies and scans shared records (the `shared.records` table). The\n * cloud-side Drive channel sets this true with no `appSyncableSource`; per-app\n * channels set it false and serve only that app's app-specific rows. Mirrors\n * `SyncEngineOptions.syncSharedRecords` on the requester side.\n */\n readonly syncSharedRecords?: boolean;\n}\n\n/**\n * `SyncTransport` that talks directly to an in-process database adapter.\n * Used for tests and for running a \"cloud\" side in the same Node process.\n *\n * Exchange semantics:\n * - Apply incoming records via `put(snapshot)` with HLC LWW.\n * - Scan local records the caller hasn't seen (per-nodeId watermark filter).\n * - Return `responderWatermarks` = MAX(updated_at) per nodeId.\n *\n * Conflict resolution is pure HLC LWW — no rejected[], no OCC.\n */\nexport function createInProcessSyncTransport(\n options: InProcessTransportOptions,\n): SyncTransport {\n const { databaseAdapter, clock, appSyncableSource, syncSharedRecords = true } = options;\n\n return {\n async exchange(request: SyncExchangeRequest): Promise<SyncExchangeResponse> {\n // 1. Apply incoming records — pure put(snapshot). HLC LWW: skip if local\n // copy is at-or-ahead of incoming. Only the Drive channel\n // (syncSharedRecords=true) applies shared records.\n if (syncSharedRecords) {\n for (const snapshot of request.records ?? []) {\n const current = await databaseAdapter.get(snapshot.id);\n if (current && compareHLC(current.updatedAt, snapshot.updatedAt) >= 0) {\n continue;\n }\n clock.receive(snapshot.updatedAt);\n await databaseAdapter.put(snapshot);\n }\n } else if ((request.records?.length ?? 0) > 0) {\n // Per-app channel received shared records — a channel-split violation\n // on the requester side. Drop them (the channel-split guard) but warn\n // so the misbehaving peer is discoverable.\n console.warn(\n `[sync] in-process transport dropped ${request.records?.length ?? 0} shared record(s) on a per-app channel (syncSharedRecords=false)`,\n );\n }\n\n // 2. Apply incoming app-syncable rows.\n for (const entry of request.appSyncableRows ?? []) {\n if (!appSyncableSource) continue;\n const ns = appSyncableSource.namespaces.get(entry.appId);\n if (!ns) continue;\n clock.receive(entry.timestamp);\n try {\n await appSyncableSource.applier.apply(entry);\n } catch (err) {\n console.warn(\n `[sync] exchange apply appSyncableRow failed (app=${entry.appId} table=${entry.table}): ${(err as Error).message}`,\n );\n }\n }\n\n // 3. Scan local records the caller hasn't seen yet, paginated by\n // cursor so records past any fixed scan window are still reachable.\n // Collect up to `limit + 1` matches so we can set `hasMore`\n // correctly without an additional probe. Same performance follow-up\n // as the outbound scan in sync-engine.ts: production should push\n // the per-nodeId watermark filter into the query.\n const limit = request.limit ?? 1000;\n const SCAN_PAGE = 500;\n const collected: AnyRecord[] = [];\n let cursor: string | undefined = undefined;\n // Per-app channels (syncSharedRecords=false) never scan or ship shared\n // records.\n let scanHasMore = syncSharedRecords;\n let overflowed = false;\n while (!overflowed && scanHasMore) {\n const page = await databaseAdapter.query({\n limit: SCAN_PAGE,\n ...(cursor !== undefined ? { cursor } : {}),\n });\n if (page.records.length === 0) break;\n for (const r of page.records) {\n const peerHlc = request.watermarks[r.updatedAt.nodeId];\n if (!peerHlc || compareHLC(r.updatedAt, peerHlc) > 0) {\n if (collected.length >= limit) {\n overflowed = true;\n break;\n }\n collected.push(r);\n }\n }\n if (overflowed) break;\n scanHasMore = page.hasMore;\n cursor = page.nextCursor ?? undefined;\n }\n const records = collected;\n\n // 4. App-syncable rows: same per-nodeId filtering across known tables,\n // cursor-paginated for the same reason as the SR scan above —\n // records past any fixed scan window stay reachable.\n const appSyncableRows: AppSyncableRowEntry[] = [];\n if (appSyncableSource && records.length < limit) {\n const scanCapable = appSyncableSource.applier as ScanCapableApplier;\n if (typeof scanCapable.scanSince === \"function\") {\n const zeroStr = serializeHLC(ZERO_HLC);\n outer: for (const ns of appSyncableSource.namespaces.list()) {\n for (const tableInfo of ns.tables) {\n let appCursor: string | undefined = undefined;\n let appHasMore = true;\n while (\n records.length + appSyncableRows.length < limit &&\n appHasMore\n ) {\n let page: { rows: AppSyncableRowEntry[]; nextCursor: string | null; hasMore: boolean };\n try {\n page = await scanCapable.scanSince(\n ns.appId,\n tableInfo.name,\n zeroStr,\n {\n limit: SCAN_PAGE,\n ...(appCursor !== undefined ? { cursor: appCursor } : {}),\n },\n );\n } catch (err) {\n console.warn(\n `[sync] in-process transport scanSince failed for ${ns.appId}.${tableInfo.name}: ${(err as Error).message}`,\n );\n break;\n }\n if (page.rows.length === 0) break;\n for (const r of page.rows) {\n const peerHlc = request.watermarks[r.timestamp.nodeId];\n if (!peerHlc || compareHLC(r.timestamp, peerHlc) > 0) {\n appSyncableRows.push(r);\n if (records.length + appSyncableRows.length >= limit) break;\n }\n }\n appHasMore = page.hasMore;\n appCursor = page.nextCursor ?? undefined;\n }\n if (records.length + appSyncableRows.length >= limit) break outer;\n }\n }\n }\n }\n\n // hasMore reflects: (a) the SR scan overflowed past `limit`, or\n // (b) the combined SR + app-syncable payload hit `limit` and there\n // are still untraversed app rows. (a) is captured by `overflowed`;\n // (b) is approximated by the app-syncable collection loop breaking\n // out early — i.e. records.length + appSyncableRows.length >= limit.\n const hasMore =\n overflowed ||\n records.length + appSyncableRows.length >= limit;\n\n return { records, appSyncableRows, hasMore };\n },\n };\n}\n","import { StarkeepError } from \"@starkeep/protocol-primitives\";\n\nexport class SyncError extends StarkeepError {\n constructor(message: string, cause?: unknown) {\n super(message, \"SYNC_ERROR\", cause);\n this.name = \"SyncError\";\n }\n}\n","import type {\n SyncTransport,\n SyncExchangeRequest,\n SyncExchangeResponse,\n} from \"../types.js\";\nimport { SyncError } from \"../errors.js\";\n\nexport interface HttpSyncTransportOptions {\n readonly baseUrl: string;\n readonly fetch?: typeof globalThis.fetch;\n readonly getAuthHeader?: () => string | undefined;\n}\n\n/**\n * `SyncTransport` that talks to a remote Starkeep-compatible HTTP server\n * over `fetch`. Single endpoint: `POST {baseUrl}/sync/exchange`.\n */\nexport function createHttpSyncTransport(\n options: HttpSyncTransportOptions,\n): SyncTransport {\n const { baseUrl, fetch: fetchImpl = globalThis.fetch, getAuthHeader } = options;\n const trimmed = baseUrl.replace(/\\/+$/, \"\");\n\n async function postJson<TRequest, TResponse>(\n path: string,\n body: TRequest,\n ): Promise<TResponse> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n const auth = getAuthHeader?.();\n if (auth) headers[\"Authorization\"] = auth;\n\n const response = await fetchImpl(`${trimmed}${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n throw new SyncError(\n `${path} failed: ${response.status} ${response.statusText} ${text}`,\n );\n }\n\n return (await response.json()) as TResponse;\n }\n\n return {\n async exchange(request: SyncExchangeRequest): Promise<SyncExchangeResponse> {\n return postJson<SyncExchangeRequest, SyncExchangeResponse>(\n \"/sync/exchange\",\n request,\n );\n },\n };\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { HLCClock } from \"@starkeep/protocol-primitives\";\nimport type { DatabaseAdapter, ObjectStorageAdapter } from \"@starkeep/storage-adapter\";\nimport { createInProcessSyncTransport } from \"./in-process-transport.js\";\nimport type { SyncExchangeRequest, SyncTransport } from \"../types.js\";\n\nexport interface HttpSyncServerOptions {\n readonly databaseAdapter: DatabaseAdapter;\n readonly objectStorageAdapter: ObjectStorageAdapter;\n readonly clock: HLCClock;\n /**\n * Optional transport override — if provided, takes precedence over the\n * default in-process transport.\n */\n readonly transport?: SyncTransport;\n}\n\ntype Handler = (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;\n\n/**\n * Request handler that recognizes the Starkeep sync + file routes.\n * Returns `true` if the request was handled; callers can compose this with\n * their own routing layer.\n */\nexport function createHttpSyncHandler(\n options: HttpSyncServerOptions,\n): Handler {\n const transport =\n options.transport ??\n createInProcessSyncTransport({\n databaseAdapter: options.databaseAdapter,\n clock: options.clock,\n objectStorage: options.objectStorageAdapter,\n });\n\n return async (req, res) => {\n const url = new URL(\n req.url || \"/\",\n `http://${req.headers.host ?? \"localhost\"}`,\n );\n\n if (req.method === \"POST\" && url.pathname === \"/sync/exchange\") {\n const body = await readJson<SyncExchangeRequest>(req);\n const response = await transport.exchange(body);\n sendJson(res, 200, response);\n return true;\n }\n\n const fileMatch = url.pathname.match(/^\\/files\\/(.+)$/);\n if (fileMatch) {\n const key = decodeURIComponent(fileMatch[1]!);\n const storage = options.objectStorageAdapter;\n\n if (req.method === \"HEAD\") {\n const exists = await storage.has(key);\n res.writeHead(exists ? 200 : 404);\n res.end();\n return true;\n }\n if (req.method === \"GET\") {\n const file = await storage.get(key);\n if (!file) {\n res.writeHead(404);\n res.end();\n return true;\n }\n res.writeHead(200, {\n \"Content-Type\": file.contentType ?? \"application/octet-stream\",\n \"Content-Length\": String(file.size),\n });\n res.end(Buffer.from(file.data));\n return true;\n }\n if (req.method === \"PUT\") {\n const bytes = await readBinary(req);\n const contentType = req.headers[\"content-type\"];\n await storage.put(key, bytes, {\n contentType:\n typeof contentType === \"string\" ? contentType : undefined,\n });\n res.writeHead(200);\n res.end();\n return true;\n }\n if (req.method === \"DELETE\") {\n await storage.delete(key);\n res.writeHead(204);\n res.end();\n return true;\n }\n }\n\n return false;\n };\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n\nasync function readJson<T>(req: IncomingMessage): Promise<T> {\n const buf = await readBinary(req);\n return JSON.parse(buf.toString(\"utf-8\")) as T;\n}\n\nfunction readBinary(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n"],"mappings":"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","names":["compareHLC","ZERO_HLC","compareHLC","ZERO_HLC","compareHLC","serializeHLC","ZERO_HLC"]}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@starkeep/sync-engine",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/starkeep-dev/starkeep-core.git",
26
+ "directory": "packages/sync-engine"
27
+ },
28
+ "homepage": "https://github.com/starkeep-dev/starkeep-core/tree/main/packages/sync-engine",
29
+ "dependencies": {
30
+ "@starkeep/protocol-primitives": "0.1.0",
31
+ "@starkeep/storage-adapter": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^25.4.0",
35
+ "tsup": "^8.0.0",
36
+ "typescript": "^5.7.0",
37
+ "vitest": "^3.0.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "test": "vitest run",
42
+ "typecheck": "tsc --noEmit",
43
+ "lint": "eslint src/"
44
+ }
45
+ }