@syncular/server 0.15.45 → 0.15.47

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.
Files changed (88) hide show
  1. package/README.md +144 -7
  2. package/dist/admin.d.ts +10 -4
  3. package/dist/admin.js +10 -0
  4. package/dist/authoritative-query.d.ts +20 -0
  5. package/dist/authoritative-query.js +184 -0
  6. package/dist/context.d.ts +11 -1
  7. package/dist/context.js +2 -0
  8. package/dist/d1-storage.d.ts +10 -1
  9. package/dist/d1-storage.js +216 -0
  10. package/dist/errors.d.ts +1 -1
  11. package/dist/errors.js +43 -1
  12. package/dist/events.d.ts +52 -3
  13. package/dist/handler.js +4 -1
  14. package/dist/index-bun.d.ts +2 -0
  15. package/dist/index-bun.js +2 -0
  16. package/dist/index-node.d.ts +2 -0
  17. package/dist/index-node.js +2 -0
  18. package/dist/index.d.ts +7 -2
  19. package/dist/index.js +7 -6
  20. package/dist/operations-realtime.d.ts +16 -0
  21. package/dist/operations-realtime.js +196 -0
  22. package/dist/operations.d.ts +97 -0
  23. package/dist/operations.js +392 -0
  24. package/dist/postgres-storage.d.ts +11 -2
  25. package/dist/postgres-storage.js +220 -0
  26. package/dist/pull.js +1 -1
  27. package/dist/push.d.ts +8 -2
  28. package/dist/push.js +75 -21
  29. package/dist/reactions.d.ts +167 -0
  30. package/dist/reactions.js +442 -0
  31. package/dist/realtime.js +4 -1
  32. package/dist/sqlite-blob-store.d.ts +4 -9
  33. package/dist/sqlite-blob-store.js +5 -10
  34. package/dist/sqlite-bun-driver.d.ts +11 -0
  35. package/dist/sqlite-bun-driver.js +27 -0
  36. package/dist/sqlite-bun.d.ts +24 -0
  37. package/dist/sqlite-bun.js +40 -0
  38. package/dist/sqlite-dialect.d.ts +8 -8
  39. package/dist/sqlite-dialect.js +22 -2
  40. package/dist/sqlite-driver.d.ts +26 -0
  41. package/dist/sqlite-driver.js +8 -0
  42. package/dist/sqlite-image.d.ts +7 -9
  43. package/dist/sqlite-image.js +26 -28
  44. package/dist/sqlite-lease-store.d.ts +4 -9
  45. package/dist/sqlite-lease-store.js +5 -10
  46. package/dist/sqlite-node-driver.d.ts +10 -0
  47. package/dist/sqlite-node-driver.js +30 -0
  48. package/dist/sqlite-node.d.ts +24 -0
  49. package/dist/sqlite-node.js +50 -0
  50. package/dist/sqlite-segment-store.d.ts +4 -10
  51. package/dist/sqlite-segment-store.js +6 -9
  52. package/dist/sqlite-storage.d.ts +13 -12
  53. package/dist/sqlite-storage.js +223 -5
  54. package/dist/storage-errors.js +4 -1
  55. package/dist/storage.d.ts +109 -0
  56. package/dist/validate.js +1 -0
  57. package/package.json +18 -3
  58. package/src/admin.ts +27 -3
  59. package/src/authoritative-query.ts +218 -0
  60. package/src/context.ts +12 -1
  61. package/src/d1-storage.ts +352 -0
  62. package/src/errors.ts +43 -1
  63. package/src/events.ts +64 -2
  64. package/src/handler.ts +13 -1
  65. package/src/index-bun.ts +9 -0
  66. package/src/index-node.ts +9 -0
  67. package/src/index.ts +40 -6
  68. package/src/operations-realtime.ts +272 -0
  69. package/src/operations.ts +720 -0
  70. package/src/postgres-storage.ts +351 -0
  71. package/src/pull.ts +1 -1
  72. package/src/push.ts +97 -29
  73. package/src/reactions.ts +741 -0
  74. package/src/realtime.ts +7 -1
  75. package/src/sqlite-blob-store.ts +11 -10
  76. package/src/sqlite-bun-driver.ts +42 -0
  77. package/src/sqlite-bun.ts +53 -0
  78. package/src/sqlite-dialect.ts +27 -7
  79. package/src/sqlite-driver.ts +44 -0
  80. package/src/sqlite-image.ts +44 -49
  81. package/src/sqlite-lease-store.ts +11 -10
  82. package/src/sqlite-node-driver.ts +46 -0
  83. package/src/sqlite-node.ts +62 -0
  84. package/src/sqlite-segment-store.ts +11 -11
  85. package/src/sqlite-storage.ts +378 -7
  86. package/src/storage-errors.ts +4 -1
  87. package/src/storage.ts +165 -0
  88. package/src/validate.ts +1 -0
@@ -0,0 +1,27 @@
1
+ import { Database } from 'bun:sqlite';
2
+ export class BunSqliteDatabase {
3
+ native;
4
+ constructor(path = ':memory:') {
5
+ this.native = new Database(path);
6
+ }
7
+ exec(sql) {
8
+ this.native.exec(sql);
9
+ }
10
+ run(sql, bindings = []) {
11
+ return this.native.run(sql, [...bindings]);
12
+ }
13
+ query(sql) {
14
+ const statement = this.native.query(sql);
15
+ return {
16
+ run: (...params) => statement.run(...params),
17
+ get: (...params) => statement.get(...params),
18
+ all: (...params) => statement.all(...params),
19
+ };
20
+ }
21
+ serialize() {
22
+ return new Uint8Array(this.native.serialize());
23
+ }
24
+ close() {
25
+ this.native.close();
26
+ }
27
+ }
@@ -0,0 +1,24 @@
1
+ import { SqliteBlobStore as SharedSqliteBlobStore } from './sqlite-blob-store.js';
2
+ import type { SqliteDatabase } from './sqlite-driver.js';
3
+ import { type SqliteImageBuilder } from './sqlite-image.js';
4
+ import { SqliteLeaseStore as SharedSqliteLeaseStore } from './sqlite-lease-store.js';
5
+ import { SqliteSegmentStore as SharedSqliteSegmentStore } from './sqlite-segment-store.js';
6
+ import { SqliteServerStorage as SharedSqliteServerStorage } from './sqlite-storage.js';
7
+ export declare class SqliteServerStorage extends SharedSqliteServerStorage {
8
+ constructor(value?: SqliteDatabase | string);
9
+ }
10
+ export declare class SqliteSegmentStore extends SharedSqliteSegmentStore {
11
+ constructor(value?: SqliteDatabase | string, options?: {
12
+ ttlMs?: number;
13
+ });
14
+ }
15
+ export declare class SqliteBlobStore extends SharedSqliteBlobStore {
16
+ constructor(value?: SqliteDatabase | string);
17
+ }
18
+ export declare class SqliteLeaseStore extends SharedSqliteLeaseStore {
19
+ constructor(value?: SqliteDatabase | string, options?: {
20
+ readonly leaseId?: () => string;
21
+ });
22
+ }
23
+ export declare const buildSqliteImage: SqliteImageBuilder;
24
+ export { BunSqliteDatabase } from './sqlite-bun-driver.js';
@@ -0,0 +1,40 @@
1
+ import { SqliteBlobStore as SharedSqliteBlobStore } from './sqlite-blob-store.js';
2
+ import { BunSqliteDatabase } from './sqlite-bun-driver.js';
3
+ import { writeSqliteImage } from './sqlite-image.js';
4
+ import { SqliteLeaseStore as SharedSqliteLeaseStore } from './sqlite-lease-store.js';
5
+ import { SqliteSegmentStore as SharedSqliteSegmentStore } from './sqlite-segment-store.js';
6
+ import { SqliteServerStorage as SharedSqliteServerStorage } from './sqlite-storage.js';
7
+ function database(value) {
8
+ return typeof value === 'string' ? new BunSqliteDatabase(value) : value;
9
+ }
10
+ export class SqliteServerStorage extends SharedSqliteServerStorage {
11
+ constructor(value = ':memory:') {
12
+ super(database(value));
13
+ }
14
+ }
15
+ export class SqliteSegmentStore extends SharedSqliteSegmentStore {
16
+ constructor(value = ':memory:', options) {
17
+ super(database(value), options);
18
+ }
19
+ }
20
+ export class SqliteBlobStore extends SharedSqliteBlobStore {
21
+ constructor(value = ':memory:') {
22
+ super(database(value));
23
+ }
24
+ }
25
+ export class SqliteLeaseStore extends SharedSqliteLeaseStore {
26
+ constructor(value = ':memory:', options) {
27
+ super(database(value), options);
28
+ }
29
+ }
30
+ export const buildSqliteImage = (input) => {
31
+ const db = new BunSqliteDatabase();
32
+ try {
33
+ writeSqliteImage(db, input);
34
+ return db.serialize();
35
+ }
36
+ finally {
37
+ db.close();
38
+ }
39
+ };
40
+ export { BunSqliteDatabase } from './sqlite-bun-driver.js';
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Shared SQLite dialect for the two SQLite-family storages: `bun:sqlite`
3
- * (synchronous, `SqliteServerStorage`) and Cloudflare D1 (async,
4
- * `D1ServerStorage`). D1 *is* SQLite — same DDL, same statement grammar,
2
+ * Shared SQLite dialect for synchronous `SqliteServerStorage` on Bun or Node
3
+ * and asynchronous `D1ServerStorage` on Cloudflare Workers. D1 is SQLite:
4
+ * same DDL, same statement grammar,
5
5
  * same `?` positional placeholders, same `INSERT ... ON CONFLICT` / `INSERT
6
6
  * OR IGNORE` upsert idioms — so the schema and the value (de)serialization are
7
7
  * genuinely common ground and live here.
8
8
  *
9
- * What is NOT shared: statement *execution*. `bun:sqlite` is sync
9
+ * What is not shared: statement execution. The server SQLite driver is sync
10
10
  * (`db.query(sql).get(...)`) and D1 is async (`await
11
11
  * db.prepare(sql).bind(...).all()`); a shared execution layer would have to
12
12
  * pick one calling convention and adapt the other, which is uglier than two
@@ -18,7 +18,7 @@
18
18
  import type { ScopeMap } from '@syncular/core';
19
19
  import type { StoredChange, StoredCommit, StoredPushResult, StoredRow } from './storage.js';
20
20
  /**
21
- * Schema DDL one statement per `;`-delimited chunk. `bun:sqlite` applies
21
+ * Schema DDL, one statement per `;`-delimited chunk. Native SQLite applies
22
22
  * the whole string via `db.exec(SQLITE_DDL)`; D1 applies each statement
23
23
  * separately (its `prepare`/`batch` API is one statement per call). Types
24
24
  * are SQLite's: `INTEGER`/`TEXT`/`BLOB`. Scopes are stored as JSON `TEXT`
@@ -30,7 +30,7 @@ import type { StoredChange, StoredCommit, StoredPushResult, StoredRow } from './
30
30
  * the Postgres storage documents (§3.1, performance-by-
31
31
  * construction).
32
32
  */
33
- export declare const SQLITE_DDL = "\nCREATE TABLE IF NOT EXISTS sync_partitions(\n partition TEXT PRIMARY KEY,\n max_commit_seq INTEGER NOT NULL DEFAULT 0,\n horizon_seq INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS sync_row_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, row_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, row_id)\n);\nCREATE TABLE IF NOT EXISTS sync_commits(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n client_id TEXT NOT NULL, client_commit_id TEXT NOT NULL,\n actor_id TEXT NOT NULL, created_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, commit_seq)\n);\nCREATE INDEX IF NOT EXISTS sync_commits_by_time\n ON sync_commits(partition, created_at_ms);\nCREATE TABLE IF NOT EXISTS sync_changes(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL, idx INTEGER NOT NULL,\n tbl TEXT NOT NULL, row_id TEXT NOT NULL, op INTEGER NOT NULL,\n row_version INTEGER, scopes TEXT NOT NULL, payload BLOB,\n PRIMARY KEY(partition, commit_seq, idx)\n);\nCREATE INDEX IF NOT EXISTS sync_changes_by_table\n ON sync_changes(partition, commit_seq, tbl, idx);\nCREATE TABLE IF NOT EXISTS sync_change_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, commit_seq)\n);\nCREATE TABLE IF NOT EXISTS sync_push_results(\n partition TEXT NOT NULL, client_id TEXT NOT NULL,\n client_commit_id TEXT NOT NULL, result TEXT NOT NULL,\n PRIMARY KEY(partition, client_id, client_commit_id)\n);\nCREATE TABLE IF NOT EXISTS sync_clients(\n partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,\n cursor INTEGER NOT NULL, subscriptions TEXT NOT NULL,\n updated_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, client_id)\n);\nCREATE TABLE IF NOT EXISTS sync_blob_refs(\n partition TEXT NOT NULL, tbl TEXT NOT NULL, row_id TEXT NOT NULL,\n blob_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, row_id, blob_id)\n);\nCREATE INDEX IF NOT EXISTS sync_blob_refs_by_blob\n ON sync_blob_refs(partition, blob_id);\n";
33
+ export declare const SQLITE_DDL = "\nCREATE TABLE IF NOT EXISTS sync_partitions(\n partition TEXT PRIMARY KEY,\n max_commit_seq INTEGER NOT NULL DEFAULT 0,\n horizon_seq INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS sync_row_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, row_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, row_id)\n);\nCREATE TABLE IF NOT EXISTS sync_commits(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n client_id TEXT NOT NULL, client_commit_id TEXT NOT NULL,\n actor_id TEXT NOT NULL, created_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, commit_seq)\n);\nCREATE INDEX IF NOT EXISTS sync_commits_by_time\n ON sync_commits(partition, created_at_ms);\nCREATE TABLE IF NOT EXISTS sync_changes(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL, idx INTEGER NOT NULL,\n tbl TEXT NOT NULL, row_id TEXT NOT NULL, op INTEGER NOT NULL,\n row_version INTEGER, scopes TEXT NOT NULL, payload BLOB,\n PRIMARY KEY(partition, commit_seq, idx)\n);\nCREATE INDEX IF NOT EXISTS sync_changes_by_table\n ON sync_changes(partition, commit_seq, tbl, idx);\nCREATE TABLE IF NOT EXISTS sync_change_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, commit_seq)\n);\nCREATE TABLE IF NOT EXISTS sync_push_results(\n partition TEXT NOT NULL, client_id TEXT NOT NULL,\n client_commit_id TEXT NOT NULL, result TEXT NOT NULL,\n PRIMARY KEY(partition, client_id, client_commit_id)\n);\nCREATE TABLE IF NOT EXISTS sync_reactions(\n partition TEXT NOT NULL, idempotency_key TEXT NOT NULL,\n type TEXT NOT NULL, version INTEGER NOT NULL, payload TEXT NOT NULL,\n source_client_id TEXT NOT NULL, source_client_commit_id TEXT NOT NULL,\n source_commit_seq INTEGER NOT NULL, created_at_ms INTEGER NOT NULL,\n available_at_ms INTEGER NOT NULL, status TEXT NOT NULL,\n attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,\n lease_owner TEXT, lease_expires_at_ms INTEGER, completed_at_ms INTEGER,\n last_failure TEXT,\n PRIMARY KEY(partition, idempotency_key),\n CHECK(status IN ('pending', 'leased', 'completed', 'dead-letter'))\n);\nCREATE INDEX IF NOT EXISTS sync_reactions_due\n ON sync_reactions(partition, status, available_at_ms, created_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_lease\n ON sync_reactions(partition, status, lease_expires_at_ms);\nCREATE INDEX IF NOT EXISTS sync_reactions_completed\n ON sync_reactions(partition, status, completed_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_dead_letter\n ON sync_reactions(partition, status, available_at_ms, idempotency_key);\nCREATE TABLE IF NOT EXISTS sync_clients(\n partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,\n cursor INTEGER NOT NULL, subscriptions TEXT NOT NULL,\n updated_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, client_id)\n);\nCREATE TABLE IF NOT EXISTS sync_blob_refs(\n partition TEXT NOT NULL, tbl TEXT NOT NULL, row_id TEXT NOT NULL,\n blob_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, row_id, blob_id)\n);\nCREATE INDEX IF NOT EXISTS sync_blob_refs_by_blob\n ON sync_blob_refs(partition, blob_id);\n";
34
34
  /** Split the DDL into individual statements (D1 applies them one by one). */
35
35
  export declare function sqliteDdlStatements(): string[];
36
36
  /** `?,?,…` for an `IN (…)` clause of `count` positional parameters. */
@@ -53,7 +53,7 @@ export interface SqliteChangeRecord {
53
53
  scopes: string;
54
54
  payload: Uint8Array | null;
55
55
  }
56
- /** `bun:sqlite` returns `Uint8Array`; D1 returns `ArrayBuffer` for BLOBs. */
56
+ /** Native SQLite returns `Uint8Array`; D1 returns `ArrayBuffer` for BLOBs. */
57
57
  export declare function asUint8Array(value: unknown): Uint8Array;
58
58
  export declare function toStoredRow(record: SqliteRowRecord): StoredRow;
59
59
  export declare function toStoredChange(record: SqliteChangeRecord): StoredChange;
@@ -61,7 +61,7 @@ export declare function toStoredChange(record: SqliteChangeRecord): StoredChange
61
61
  * One result row of `commitWindowPageSql` (candidate LEFT JOIN commit meta
62
62
  * LEFT JOIN changes): meta/change columns are NULL when the joined row
63
63
  * vanished (see the builder's LEFT JOIN contract). `payload` is a BLOB —
64
- * `bun:sqlite` hands back `Uint8Array`, D1 `ArrayBuffer`; `toStoredChange`
64
+ * Native SQLite hands back `Uint8Array`, D1 `ArrayBuffer`; `toStoredChange`
65
65
  * normalizes via `asUint8Array`.
66
66
  */
67
67
  export interface SqliteCommitWindowRecord {
@@ -1,6 +1,6 @@
1
1
  import { matchesEffective } from './scopes.js';
2
2
  /**
3
- * Schema DDL one statement per `;`-delimited chunk. `bun:sqlite` applies
3
+ * Schema DDL, one statement per `;`-delimited chunk. Native SQLite applies
4
4
  * the whole string via `db.exec(SQLITE_DDL)`; D1 applies each statement
5
5
  * separately (its `prepare`/`batch` API is one statement per call). Types
6
6
  * are SQLite's: `INTEGER`/`TEXT`/`BLOB`. Scopes are stored as JSON `TEXT`
@@ -49,6 +49,26 @@ CREATE TABLE IF NOT EXISTS sync_push_results(
49
49
  client_commit_id TEXT NOT NULL, result TEXT NOT NULL,
50
50
  PRIMARY KEY(partition, client_id, client_commit_id)
51
51
  );
52
+ CREATE TABLE IF NOT EXISTS sync_reactions(
53
+ partition TEXT NOT NULL, idempotency_key TEXT NOT NULL,
54
+ type TEXT NOT NULL, version INTEGER NOT NULL, payload TEXT NOT NULL,
55
+ source_client_id TEXT NOT NULL, source_client_commit_id TEXT NOT NULL,
56
+ source_commit_seq INTEGER NOT NULL, created_at_ms INTEGER NOT NULL,
57
+ available_at_ms INTEGER NOT NULL, status TEXT NOT NULL,
58
+ attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,
59
+ lease_owner TEXT, lease_expires_at_ms INTEGER, completed_at_ms INTEGER,
60
+ last_failure TEXT,
61
+ PRIMARY KEY(partition, idempotency_key),
62
+ CHECK(status IN ('pending', 'leased', 'completed', 'dead-letter'))
63
+ );
64
+ CREATE INDEX IF NOT EXISTS sync_reactions_due
65
+ ON sync_reactions(partition, status, available_at_ms, created_at_ms, idempotency_key);
66
+ CREATE INDEX IF NOT EXISTS sync_reactions_lease
67
+ ON sync_reactions(partition, status, lease_expires_at_ms);
68
+ CREATE INDEX IF NOT EXISTS sync_reactions_completed
69
+ ON sync_reactions(partition, status, completed_at_ms, idempotency_key);
70
+ CREATE INDEX IF NOT EXISTS sync_reactions_dead_letter
71
+ ON sync_reactions(partition, status, available_at_ms, idempotency_key);
52
72
  CREATE TABLE IF NOT EXISTS sync_clients(
53
73
  partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,
54
74
  cursor INTEGER NOT NULL, subscriptions TEXT NOT NULL,
@@ -161,7 +181,7 @@ export function deserializePushResult(text) {
161
181
  results,
162
182
  };
163
183
  }
164
- /** `bun:sqlite` returns `Uint8Array`; D1 returns `ArrayBuffer` for BLOBs. */
184
+ /** Native SQLite returns `Uint8Array`; D1 returns `ArrayBuffer` for BLOBs. */
165
185
  export function asUint8Array(value) {
166
186
  if (value instanceof Uint8Array)
167
187
  return value;
@@ -0,0 +1,26 @@
1
+ /** Values accepted by the synchronous SQLite adapters. */
2
+ export type SqliteValue = string | number | bigint | boolean | Uint8Array | null;
3
+ /** Result of a SQLite statement that does not return rows. */
4
+ export interface SqliteRunResult {
5
+ readonly changes: number | bigint;
6
+ readonly lastInsertRowid: number | bigint;
7
+ }
8
+ /** Prepared synchronous SQLite statement used by the server stores. */
9
+ export interface SqliteStatement<Row, Params extends readonly SqliteValue[]> {
10
+ run(...params: Params): SqliteRunResult;
11
+ get(...params: Params): Row | null;
12
+ all(...params: Params): Row[];
13
+ }
14
+ /** Runtime-neutral database surface shared by the Bun and Node adapters. */
15
+ export interface SqliteDatabase {
16
+ exec(sql: string): void;
17
+ run(sql: string, bindings?: readonly SqliteValue[]): SqliteRunResult;
18
+ query<Row = Record<string, SqliteValue>, Params extends readonly SqliteValue[] = SqliteValue[]>(sql: string): SqliteStatement<Row, Params>;
19
+ close(): void;
20
+ }
21
+ /** Raised when a neutral-runtime import is used without a SQLite adapter. */
22
+ export declare class SqliteAdapterRequiredError extends Error {
23
+ readonly name = "SqliteAdapterRequiredError";
24
+ readonly code = "sync.sqlite_adapter_required";
25
+ constructor();
26
+ }
@@ -0,0 +1,8 @@
1
+ /** Raised when a neutral-runtime import is used without a SQLite adapter. */
2
+ export class SqliteAdapterRequiredError extends Error {
3
+ name = 'SqliteAdapterRequiredError';
4
+ code = 'sync.sqlite_adapter_required';
5
+ constructor() {
6
+ super('SQLite paths require the @syncular/server/sqlite runtime adapter');
7
+ }
8
+ }
@@ -1,4 +1,5 @@
1
1
  import type { CompiledTable } from './schema.js';
2
+ import type { SqliteDatabase } from './sqlite-driver.js';
2
3
  import type { StoredRow } from './storage.js';
3
4
  /** The §5.6 version column as it appears inside a sqlite image (§5.3). */
4
5
  export declare const IMAGE_VERSION_COLUMN = "_syncular_version";
@@ -14,14 +15,11 @@ export interface SqliteImageInput {
14
15
  /**
15
16
  * The §5.3 image-builder capability, injected through
16
17
  * `SyncServerConfig.sqliteImageBuilder`. Building an image needs
17
- * a real SQLite engine (`bun:sqlite` here), which is not available on every
18
- * runtime Cloudflare Workers has none. So the core takes the builder as an
19
- * optional capability rather than importing `bun:sqlite` on the pull path:
20
- * a Bun/Node host passes `buildSqliteImage`; a Workers host omits it and the
21
- * pull serves the rows lane (§5.3 clients advertise sqlite as an *accept*,
22
- * never a requirement — the host chooses the served format from what it can
23
- * produce; this is a support floor, not a fallback).
18
+ * a real SQLite engine, which is not available on every runtime. The core
19
+ * takes the builder as an optional capability rather than importing a driver
20
+ * on the pull path. A Bun or Node host passes `buildSqliteImage`; a Workers
21
+ * host omits it and serves the rows lane.
24
22
  */
25
23
  export type SqliteImageBuilder = (input: SqliteImageInput) => Uint8Array;
26
- /** Build the §5.3 image bytes for a whole-table snapshot. */
27
- export declare const buildSqliteImage: SqliteImageBuilder;
24
+ /** Populate a §5.3 image database for a whole-table snapshot. */
25
+ export declare function writeSqliteImage(db: SqliteDatabase, input: SqliteImageInput): void;
@@ -2,13 +2,12 @@
2
2
  * SQLite-image segment generation (SPEC.md §5.3): a complete SQLite
3
3
  * database file carrying one table's whole effective-scope snapshot at
4
4
  * the bootstrap pin, plus the single-row `_syncular_segment` metadata
5
- * table. Built in memory on bun:sqlite dependency-free.
5
+ * table. Runtime entries provide the concrete SQLite database.
6
6
  *
7
7
  * Images are NOT byte-deterministic (§5.3): the content address pins the
8
8
  * served bytes, and cross-client dedup comes from the segment store's
9
9
  * metadata lookup (`SegmentStore.find`), not from hash convergence.
10
10
  */
11
- import { Database } from 'bun:sqlite';
12
11
  import { decodeRow } from '@syncular/core';
13
12
  /** The §5.6 version column as it appears inside a sqlite image (§5.3). */
14
13
  export const IMAGE_VERSION_COLUMN = '_syncular_version';
@@ -45,39 +44,38 @@ function toSql(value) {
45
44
  return value ? 1 : 0;
46
45
  return value;
47
46
  }
48
- /** Build the §5.3 image bytes for a whole-table snapshot. */
49
- export const buildSqliteImage = (input) => {
47
+ /** Populate a §5.3 image database for a whole-table snapshot. */
48
+ export function writeSqliteImage(db, input) {
50
49
  const { table, rows } = input;
51
50
  const primaryKey = table.columns[table.primaryKeyIndex]?.name;
52
- const db = new Database(':memory:');
51
+ const columnDefs = table.columns.map((column) => {
52
+ const notNull = column.nullable ? '' : ' NOT NULL';
53
+ const pk = column.name === primaryKey ? ' PRIMARY KEY' : '';
54
+ return `${quoteIdent(column.name)} ${sqlType(column)}${notNull}${pk}`;
55
+ });
56
+ columnDefs.push(`${quoteIdent(IMAGE_VERSION_COLUMN)} INTEGER NOT NULL`);
57
+ db.exec(`CREATE TABLE ${quoteIdent(table.name)} (${columnDefs.join(', ')})`);
58
+ db.exec(`CREATE TABLE ${IMAGE_METADATA_TABLE} (
59
+ format INTEGER NOT NULL, "table" TEXT NOT NULL,
60
+ "schemaVersion" INTEGER NOT NULL, "asOfCommitSeq" INTEGER NOT NULL,
61
+ "scopeDigest" TEXT NOT NULL, "rowCount" INTEGER NOT NULL)`);
62
+ db.query(`INSERT INTO ${IMAGE_METADATA_TABLE} VALUES (1, ?, ?, ?, ?, ?)`).run(table.name, input.schemaVersion, input.asOfCommitSeq, input.scopeDigest, rows.length);
63
+ const names = [
64
+ ...table.columns.map((column) => quoteIdent(column.name)),
65
+ quoteIdent(IMAGE_VERSION_COLUMN),
66
+ ];
67
+ const insert = db.query(`INSERT INTO ${quoteIdent(table.name)} (${names.join(', ')})
68
+ VALUES (${names.map(() => '?').join(', ')})`);
69
+ db.exec('BEGIN');
53
70
  try {
54
- const columnDefs = table.columns.map((column) => {
55
- const notNull = column.nullable ? '' : ' NOT NULL';
56
- const pk = column.name === primaryKey ? ' PRIMARY KEY' : '';
57
- return `${quoteIdent(column.name)} ${sqlType(column)}${notNull}${pk}`;
58
- });
59
- columnDefs.push(`${quoteIdent(IMAGE_VERSION_COLUMN)} INTEGER NOT NULL`);
60
- db.exec(`CREATE TABLE ${quoteIdent(table.name)} (${columnDefs.join(', ')})`);
61
- db.exec(`CREATE TABLE ${IMAGE_METADATA_TABLE} (
62
- format INTEGER NOT NULL, "table" TEXT NOT NULL,
63
- "schemaVersion" INTEGER NOT NULL, "asOfCommitSeq" INTEGER NOT NULL,
64
- "scopeDigest" TEXT NOT NULL, "rowCount" INTEGER NOT NULL)`);
65
- db.query(`INSERT INTO ${IMAGE_METADATA_TABLE} VALUES (1, ?, ?, ?, ?, ?)`).run(table.name, input.schemaVersion, input.asOfCommitSeq, input.scopeDigest, rows.length);
66
- const names = [
67
- ...table.columns.map((column) => quoteIdent(column.name)),
68
- quoteIdent(IMAGE_VERSION_COLUMN),
69
- ];
70
- const insert = db.query(`INSERT INTO ${quoteIdent(table.name)} (${names.join(', ')})
71
- VALUES (${names.map(() => '?').join(', ')})`);
72
- db.exec('BEGIN');
73
71
  for (const row of rows) {
74
72
  const values = decodeRow(table.columns, row.payload);
75
73
  insert.run(...values.map(toSql), row.serverVersion);
76
74
  }
77
75
  db.exec('COMMIT');
78
- return new Uint8Array(db.serialize());
79
76
  }
80
- finally {
81
- db.close();
77
+ catch (error) {
78
+ db.exec('ROLLBACK');
79
+ throw error;
82
80
  }
83
- };
81
+ }
@@ -1,18 +1,13 @@
1
1
  /**
2
- * SQLite-backed auth-lease store via `bun:sqlite` (§7.3.1 reference store,
3
- * dependency-free). Bun-specific by design (top-level `bun:sqlite` import),
4
- * so it lives in its own module — the runtime-neutral `LeaseStore` interface,
5
- * `LeaseRecord`, and `MemoryLeaseStore` stay in `lease-store.ts` for the
6
- * Workers/edge core (runtime neutrality is enforced by
7
- * `test/runtime-neutrality.test.ts`).
2
+ * SQLite-backed auth-lease store over the shared synchronous driver.
8
3
  */
9
- import { Database } from 'bun:sqlite';
10
4
  import type { ScopeMap } from '@syncular/core';
11
5
  import type { LeaseIdFactory, LeaseRecord, LeaseStore } from './lease-store.js';
6
+ import { type SqliteDatabase } from './sqlite-driver.js';
12
7
  export declare class SqliteLeaseStore implements LeaseStore {
13
8
  #private;
14
- readonly db: Database;
15
- constructor(db?: Database | string, options?: {
9
+ readonly db: SqliteDatabase;
10
+ constructor(db?: SqliteDatabase | string, options?: {
16
11
  readonly leaseId?: LeaseIdFactory;
17
12
  });
18
13
  get(partition: string, clientId: string): Promise<LeaseRecord | undefined>;
@@ -1,12 +1,4 @@
1
- /**
2
- * SQLite-backed auth-lease store via `bun:sqlite` (§7.3.1 reference store,
3
- * dependency-free). Bun-specific by design (top-level `bun:sqlite` import),
4
- * so it lives in its own module — the runtime-neutral `LeaseStore` interface,
5
- * `LeaseRecord`, and `MemoryLeaseStore` stay in `lease-store.ts` for the
6
- * Workers/edge core (runtime neutrality is enforced by
7
- * `test/runtime-neutrality.test.ts`).
8
- */
9
- import { Database } from 'bun:sqlite';
1
+ import { SqliteAdapterRequiredError, } from './sqlite-driver.js';
10
2
  function defaultLeaseId() {
11
3
  return `lease_${crypto.randomUUID()}`;
12
4
  }
@@ -14,7 +6,10 @@ export class SqliteLeaseStore {
14
6
  db;
15
7
  #newId;
16
8
  constructor(db = ':memory:', options) {
17
- this.db = typeof db === 'string' ? new Database(db) : db;
9
+ if (typeof db === 'string') {
10
+ throw new SqliteAdapterRequiredError();
11
+ }
12
+ this.db = db;
18
13
  this.#newId = options?.leaseId ?? defaultLeaseId;
19
14
  this.db.exec(`
20
15
  CREATE TABLE IF NOT EXISTS sync_leases(
@@ -0,0 +1,10 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import type { SqliteDatabase, SqliteRunResult, SqliteStatement, SqliteValue } from './sqlite-driver.js';
3
+ export declare class NodeSqliteDatabase implements SqliteDatabase {
4
+ readonly native: DatabaseSync;
5
+ constructor(path?: string);
6
+ exec(sql: string): void;
7
+ run(sql: string, bindings?: readonly SqliteValue[]): SqliteRunResult;
8
+ query<Row, Params extends readonly SqliteValue[]>(sql: string): SqliteStatement<Row, Params>;
9
+ close(): void;
10
+ }
@@ -0,0 +1,30 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ function bind(value) {
3
+ return typeof value === 'boolean' ? (value ? 1 : 0) : value;
4
+ }
5
+ function row(value) {
6
+ return (value ?? null);
7
+ }
8
+ export class NodeSqliteDatabase {
9
+ native;
10
+ constructor(path = ':memory:') {
11
+ this.native = new DatabaseSync(path);
12
+ }
13
+ exec(sql) {
14
+ this.native.exec(sql);
15
+ }
16
+ run(sql, bindings = []) {
17
+ return this.native.prepare(sql).run(...bindings.map(bind));
18
+ }
19
+ query(sql) {
20
+ const statement = this.native.prepare(sql);
21
+ return {
22
+ run: (...params) => statement.run(...params.map(bind)),
23
+ get: (...params) => row(statement.get(...params.map(bind))),
24
+ all: (...params) => statement.all(...params.map(bind)),
25
+ };
26
+ }
27
+ close() {
28
+ this.native.close();
29
+ }
30
+ }
@@ -0,0 +1,24 @@
1
+ import { SqliteBlobStore as SharedSqliteBlobStore } from './sqlite-blob-store.js';
2
+ import type { SqliteDatabase } from './sqlite-driver.js';
3
+ import { type SqliteImageBuilder } from './sqlite-image.js';
4
+ import { SqliteLeaseStore as SharedSqliteLeaseStore } from './sqlite-lease-store.js';
5
+ import { SqliteSegmentStore as SharedSqliteSegmentStore } from './sqlite-segment-store.js';
6
+ import { SqliteServerStorage as SharedSqliteServerStorage } from './sqlite-storage.js';
7
+ export declare class SqliteServerStorage extends SharedSqliteServerStorage {
8
+ constructor(value?: SqliteDatabase | string);
9
+ }
10
+ export declare class SqliteSegmentStore extends SharedSqliteSegmentStore {
11
+ constructor(value?: SqliteDatabase | string, options?: {
12
+ ttlMs?: number;
13
+ });
14
+ }
15
+ export declare class SqliteBlobStore extends SharedSqliteBlobStore {
16
+ constructor(value?: SqliteDatabase | string);
17
+ }
18
+ export declare class SqliteLeaseStore extends SharedSqliteLeaseStore {
19
+ constructor(value?: SqliteDatabase | string, options?: {
20
+ readonly leaseId?: () => string;
21
+ });
22
+ }
23
+ export declare const buildSqliteImage: SqliteImageBuilder;
24
+ export { NodeSqliteDatabase } from './sqlite-node-driver.js';
@@ -0,0 +1,50 @@
1
+ import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { SqliteBlobStore as SharedSqliteBlobStore } from './sqlite-blob-store.js';
5
+ import { writeSqliteImage } from './sqlite-image.js';
6
+ import { SqliteLeaseStore as SharedSqliteLeaseStore } from './sqlite-lease-store.js';
7
+ import { NodeSqliteDatabase } from './sqlite-node-driver.js';
8
+ import { SqliteSegmentStore as SharedSqliteSegmentStore } from './sqlite-segment-store.js';
9
+ import { SqliteServerStorage as SharedSqliteServerStorage } from './sqlite-storage.js';
10
+ function database(value) {
11
+ return typeof value === 'string' ? new NodeSqliteDatabase(value) : value;
12
+ }
13
+ export class SqliteServerStorage extends SharedSqliteServerStorage {
14
+ constructor(value = ':memory:') {
15
+ super(database(value));
16
+ }
17
+ }
18
+ export class SqliteSegmentStore extends SharedSqliteSegmentStore {
19
+ constructor(value = ':memory:', options) {
20
+ super(database(value), options);
21
+ }
22
+ }
23
+ export class SqliteBlobStore extends SharedSqliteBlobStore {
24
+ constructor(value = ':memory:') {
25
+ super(database(value));
26
+ }
27
+ }
28
+ export class SqliteLeaseStore extends SharedSqliteLeaseStore {
29
+ constructor(value = ':memory:', options) {
30
+ super(database(value), options);
31
+ }
32
+ }
33
+ export const buildSqliteImage = (input) => {
34
+ const directory = mkdtempSync(join(tmpdir(), 'syncular-server-image-'));
35
+ const path = join(directory, 'segment.db');
36
+ const db = new NodeSqliteDatabase(path);
37
+ try {
38
+ try {
39
+ writeSqliteImage(db, input);
40
+ }
41
+ finally {
42
+ db.close();
43
+ }
44
+ return new Uint8Array(readFileSync(path));
45
+ }
46
+ finally {
47
+ rmSync(directory, { recursive: true, force: true });
48
+ }
49
+ };
50
+ export { NodeSqliteDatabase } from './sqlite-node-driver.js';
@@ -1,18 +1,12 @@
1
1
  /**
2
- * SQLite-backed segment store via `bun:sqlite` (dev/bench convenience,
3
- * dependency-free). Bun-specific by design: it imports `bun:sqlite` at the
4
- * top level, so it lives in its own module — importing it opts into the Bun
5
- * runtime. The runtime-neutral `SegmentStore` interface, `MemorySegmentStore`,
6
- * and `segmentIdFor` stay in `segment-store.ts` so the Workers/edge core can
7
- * import them without pulling in `bun:sqlite` (runtime neutrality is enforced
8
- * by `test/runtime-neutrality.test.ts`).
2
+ * SQLite-backed segment store over the shared synchronous driver.
9
3
  */
10
- import { Database } from 'bun:sqlite';
11
4
  import { type SegmentFindKey, type SegmentMetadata, type SegmentRecord, type SegmentStore, type SegmentStoreStats } from './segment-store.js';
5
+ import { type SqliteDatabase } from './sqlite-driver.js';
12
6
  export declare class SqliteSegmentStore implements SegmentStore {
13
7
  #private;
14
- readonly db: Database;
15
- constructor(db?: Database | string, options?: {
8
+ readonly db: SqliteDatabase;
9
+ constructor(db?: SqliteDatabase | string, options?: {
16
10
  ttlMs?: number;
17
11
  });
18
12
  put(metadata: SegmentMetadata, bytes: Uint8Array, nowMs: number): Promise<SegmentRecord>;
@@ -1,19 +1,16 @@
1
1
  /**
2
- * SQLite-backed segment store via `bun:sqlite` (dev/bench convenience,
3
- * dependency-free). Bun-specific by design: it imports `bun:sqlite` at the
4
- * top level, so it lives in its own module — importing it opts into the Bun
5
- * runtime. The runtime-neutral `SegmentStore` interface, `MemorySegmentStore`,
6
- * and `segmentIdFor` stay in `segment-store.ts` so the Workers/edge core can
7
- * import them without pulling in `bun:sqlite` (runtime neutrality is enforced
8
- * by `test/runtime-neutrality.test.ts`).
2
+ * SQLite-backed segment store over the shared synchronous driver.
9
3
  */
10
- import { Database } from 'bun:sqlite';
11
4
  import { DEFAULT_SEGMENT_TTL_MS, segmentIdFor, } from './segment-store.js';
5
+ import { SqliteAdapterRequiredError, } from './sqlite-driver.js';
12
6
  export class SqliteSegmentStore {
13
7
  db;
14
8
  #ttlMs;
15
9
  constructor(db = ':memory:', options) {
16
- this.db = typeof db === 'string' ? new Database(db) : db;
10
+ if (typeof db === 'string') {
11
+ throw new SqliteAdapterRequiredError();
12
+ }
13
+ this.db = db;
17
14
  this.#ttlMs = options?.ttlMs ?? DEFAULT_SEGMENT_TTL_MS;
18
15
  this.db.exec(`
19
16
  CREATE TABLE IF NOT EXISTS sync_segments(
@@ -1,18 +1,10 @@
1
- /**
2
- * SQLite storage via `bun:sqlite` (dev-speed, dependency-free).
3
- *
4
- * Scope fanout is index-first: both the commit log and the
5
- * current-row table carry a (table, variable, value) inverted index; reads
6
- * select candidates from the index and verify the full multi-variable
7
- * match against the stored scope map — never a log scan.
8
- */
9
- import { Database } from 'bun:sqlite';
10
1
  import type { CompiledSchema, CompiledTable } from './schema.js';
11
- import type { ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, StorageTransaction, StoredCommit, StoredPushResult, StoredRow } from './storage.js';
2
+ import { type SqliteDatabase } from './sqlite-driver.js';
3
+ import type { AuthoritativeQueryRequest, AuthoritativeQueryResult, ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, PrunedReactionCounts, ReactionClaimQuery, ReactionFailureUpdate, ReactionListQuery, ReactionPruneQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, StorageTransaction, StoredCommit, StoredPushResult, StoredReaction, StoredRow } from './storage.js';
12
4
  export declare class SqliteServerStorage implements ServerStorage {
13
5
  #private;
14
- readonly db: Database;
15
- constructor(db?: Database | string);
6
+ readonly db: SqliteDatabase;
7
+ constructor(db?: SqliteDatabase | string);
16
8
  /** Resolve a table's compiled schema; row operations require `ensureSchema`. */
17
9
  table(name: string): CompiledTable;
18
10
  ensureSchema(schema: CompiledSchema): Promise<void>;
@@ -20,12 +12,21 @@ export declare class SqliteServerStorage implements ServerStorage {
20
12
  /** Internal: write a row + refresh its scope-index entries. */
21
13
  writeRow(partition: string, table: string, row: StoredRow): void;
22
14
  getMaxCommitSeq(partition: string): Promise<number>;
15
+ queryAuthoritative(partition: string, query: AuthoritativeQueryRequest): Promise<AuthoritativeQueryResult>;
23
16
  getHorizonSeq(partition: string): Promise<number>;
24
17
  setHorizonSeq(partition: string, seq: number): Promise<void>;
25
18
  pruneCommitsThrough(partition: string, seq: number): Promise<number>;
26
19
  getCommitSeqBefore(partition: string, createdBeforeMs: number): Promise<number>;
27
20
  getRow(partition: string, table: string, rowId: string): Promise<StoredRow | undefined>;
28
21
  getPushResult(partition: string, clientId: string, clientCommitId: string): Promise<StoredPushResult | undefined>;
22
+ claimReactions(partition: string, query: ReactionClaimQuery): Promise<StoredReaction[]>;
23
+ completeReaction(partition: string, idempotencyKey: string, leaseOwner: string, completedAtMs: number): Promise<boolean>;
24
+ extendReactionLease(partition: string, idempotencyKey: string, leaseOwner: string, leaseExpiresAtMs: number): Promise<boolean>;
25
+ failReaction(partition: string, idempotencyKey: string, update: ReactionFailureUpdate): Promise<boolean>;
26
+ retryReaction(partition: string, idempotencyKey: string, nowMs: number): Promise<boolean>;
27
+ getReaction(partition: string, idempotencyKey: string): Promise<StoredReaction | undefined>;
28
+ listReactions(partition: string, query: ReactionListQuery): Promise<StoredReaction[]>;
29
+ pruneReactions(partition: string, query: ReactionPruneQuery): Promise<PrunedReactionCounts>;
29
30
  readCommitWindow(partition: string, query: CommitWindowQuery): Promise<StoredCommit[]>;
30
31
  scanRows(partition: string, query: RowScanQuery): Promise<StoredRow[]>;
31
32
  scanRowsByIndex(partition: string, query: IndexRowScanQuery): Promise<StoredRow[]>;