@syncular/server 0.15.44 → 0.15.46

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 (93) hide show
  1. package/README.md +135 -5
  2. package/dist/admin.d.ts +12 -6
  3. package/dist/admin.js +11 -1
  4. package/dist/authoritative-query.d.ts +20 -0
  5. package/dist/authoritative-query.js +184 -0
  6. package/dist/blob-store.d.ts +2 -2
  7. package/dist/context.d.ts +11 -2
  8. package/dist/context.js +2 -0
  9. package/dist/d1-storage.d.ts +10 -1
  10. package/dist/d1-storage.js +219 -3
  11. package/dist/errors.d.ts +1 -1
  12. package/dist/errors.js +43 -1
  13. package/dist/events-ring.d.ts +1 -1
  14. package/dist/events-ring.js +1 -1
  15. package/dist/events.d.ts +52 -3
  16. package/dist/handler.js +6 -3
  17. package/dist/index.d.ts +5 -1
  18. package/dist/index.js +5 -1
  19. package/dist/operations-realtime.d.ts +16 -0
  20. package/dist/operations-realtime.js +196 -0
  21. package/dist/operations.d.ts +97 -0
  22. package/dist/operations.js +392 -0
  23. package/dist/pg-executor.d.ts +1 -1
  24. package/dist/pg-executor.js +1 -1
  25. package/dist/postgres-fanout.d.ts +1 -1
  26. package/dist/postgres-storage.d.ts +11 -2
  27. package/dist/postgres-storage.js +223 -3
  28. package/dist/pull.js +1 -1
  29. package/dist/push.d.ts +8 -2
  30. package/dist/push.js +75 -21
  31. package/dist/reactions.d.ts +167 -0
  32. package/dist/reactions.js +442 -0
  33. package/dist/realtime.js +4 -1
  34. package/dist/relational-rows.d.ts +9 -9
  35. package/dist/relational-rows.js +9 -9
  36. package/dist/s3-blob-store.js +1 -1
  37. package/dist/s3-segment-store.js +1 -1
  38. package/dist/schema.d.ts +4 -5
  39. package/dist/schema.js +3 -3
  40. package/dist/seed.js +1 -1
  41. package/dist/segment-store.d.ts +3 -3
  42. package/dist/signed-url.js +2 -2
  43. package/dist/sigv4.d.ts +2 -4
  44. package/dist/sigv4.js +3 -3
  45. package/dist/sqlite-blob-store.d.ts +1 -1
  46. package/dist/sqlite-blob-store.js +1 -1
  47. package/dist/sqlite-dialect.d.ts +3 -4
  48. package/dist/sqlite-dialect.js +21 -1
  49. package/dist/sqlite-image.d.ts +1 -1
  50. package/dist/sqlite-lease-store.d.ts +1 -1
  51. package/dist/sqlite-lease-store.js +1 -1
  52. package/dist/sqlite-segment-store.d.ts +2 -2
  53. package/dist/sqlite-segment-store.js +2 -2
  54. package/dist/sqlite-storage.d.ts +11 -2
  55. package/dist/sqlite-storage.js +219 -4
  56. package/dist/storage.d.ts +112 -3
  57. package/dist/validate.js +1 -0
  58. package/package.json +2 -2
  59. package/src/admin.ts +30 -6
  60. package/src/authoritative-query.ts +218 -0
  61. package/src/blob-store.ts +0 -0
  62. package/src/context.ts +12 -2
  63. package/src/d1-storage.ts +355 -3
  64. package/src/errors.ts +43 -1
  65. package/src/events-ring.ts +1 -1
  66. package/src/events.ts +64 -2
  67. package/src/handler.ts +15 -3
  68. package/src/index.ts +33 -1
  69. package/src/operations-realtime.ts +272 -0
  70. package/src/operations.ts +720 -0
  71. package/src/pg-executor.ts +1 -1
  72. package/src/postgres-fanout.ts +1 -1
  73. package/src/postgres-storage.ts +355 -4
  74. package/src/pull.ts +1 -1
  75. package/src/push.ts +97 -29
  76. package/src/reactions.ts +741 -0
  77. package/src/realtime.ts +7 -1
  78. package/src/relational-rows.ts +9 -9
  79. package/src/s3-blob-store.ts +1 -1
  80. package/src/s3-segment-store.ts +1 -1
  81. package/src/schema.ts +6 -7
  82. package/src/seed.ts +1 -1
  83. package/src/segment-store.ts +3 -3
  84. package/src/signed-url.ts +2 -2
  85. package/src/sigv4.ts +3 -5
  86. package/src/sqlite-blob-store.ts +1 -1
  87. package/src/sqlite-dialect.ts +22 -3
  88. package/src/sqlite-image.ts +1 -1
  89. package/src/sqlite-lease-store.ts +1 -1
  90. package/src/sqlite-segment-store.ts +2 -2
  91. package/src/sqlite-storage.ts +369 -4
  92. package/src/storage.ts +168 -3
  93. package/src/validate.ts +1 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Relational current-row storage (DESIGN-relational-server-storage.md).
2
+ * Relational current-row storage.
3
3
  *
4
4
  * Every synced table is a REAL table in the server database — the app's
5
5
  * columns with proper type affinities, queryable with plain SQL/joins/BI —
@@ -53,7 +53,7 @@ export const SYNC_PAYLOAD_COLUMN = '_sync_payload';
53
53
  export function quoteIdent(name) {
54
54
  return `"${name.replaceAll('"', '""')}"`;
55
55
  }
56
- /** §5.3-style type affinities, per dialect (DESIGN type-mapping table). */
56
+ /** §5.3-style type affinities, per dialect. */
57
57
  export function columnSqlType(column, dialect) {
58
58
  if (dialect === 'postgres') {
59
59
  switch (column.type) {
@@ -176,11 +176,11 @@ export function physicalIndexName(declaredName) {
176
176
  return `${SYNC_INDEX_PREFIX}${fnv1a64Hex(declaredName)}`;
177
177
  }
178
178
  /**
179
- * CREATE INDEX IF NOT EXISTS for the table's user-declared indexes
180
- * (DESIGN "user indexes" — the same declared names/columns the client
181
- * materializes; cross-table index-name uniqueness is the user's schema
182
- * concern, exactly as it is client-side). Server-side the physical name
183
- * carries the {@link SYNC_INDEX_PREFIX} ownership marker.
179
+ * CREATE INDEX IF NOT EXISTS for the table's user-declared indexes. These use
180
+ * the same declared names and columns the client materializes. Cross-table
181
+ * index-name uniqueness is the user's schema concern, as it is client-side.
182
+ * Server-side the physical name carries the {@link SYNC_INDEX_PREFIX}
183
+ * ownership marker.
184
184
  */
185
185
  export function createIndexDdl(table) {
186
186
  // User indexes name app columns — nothing to index without the projection.
@@ -402,8 +402,8 @@ export function deleteRowSql(table, dialect) {
402
402
  return `DELETE FROM ${quoteIdent(table.name)} WHERE ${quoteIdent(SYNC_PARTITION_COLUMN)}=${p[0]} AND ${quoteIdent(SYNC_ROW_ID_COLUMN)}=${p[1]}`;
403
403
  }
404
404
  /**
405
- * The schema-version marker table gating DDL work (DESIGN "server-side
406
- * schema migration"): `ensureSchema` compares the stored version and skips
405
+ * The schema-version marker table gates DDL work. `ensureSchema` compares the
406
+ * stored version and skips
407
407
  * all introspection/DDL when it matches — one cheap read per storage
408
408
  * instance (relevant for D1's per-request instantiation).
409
409
  *
@@ -23,7 +23,7 @@ function parseStatsJson(json) {
23
23
  bytes: Number(parsed.bytes ?? 0),
24
24
  };
25
25
  }
26
- // Runtime-neutral base64url (TODO §4.2): `Buffer` is not present on
26
+ // Runtime-neutral base64url: `Buffer` is not present on
27
27
  // Cloudflare Workers without `nodejs_compat`, so the metadata header is
28
28
  // (de)coded with `btoa`/`atob`, available in every runtime.
29
29
  function utf8ToBase64url(text) {
@@ -32,7 +32,7 @@ function parseStatsJson(json) {
32
32
  sqliteSegments: Number(parsed.sqliteSegments ?? 0),
33
33
  };
34
34
  }
35
- // Runtime-neutral base64url (TODO §4.2): `Buffer` is not present on
35
+ // Runtime-neutral base64url: `Buffer` is not present on
36
36
  // Cloudflare Workers without `nodejs_compat`, so the object-metadata record
37
37
  // header is (de)coded with `btoa`/`atob`, available in every runtime.
38
38
  function utf8ToBase64url(text) {
package/dist/schema.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The server is configured with a schema IR: tables, columns with the six
5
5
  * §2.4 column types, scope patterns per §3.1, and a schema version. Codegen
6
- * (B5) will emit this shape later; tests hand-write it.
6
+ * Typegen emits this shape; tests hand-write it.
7
7
  */
8
8
  import { type RowColumn } from '@syncular/core';
9
9
  /** `'prefix:{variable}'` shorthand (column name = variable) or explicit. */
@@ -12,9 +12,8 @@ export type ScopePatternSpec = string | {
12
12
  column: string;
13
13
  };
14
14
  /**
15
- * A user-declared index (the migration subset's CREATE INDEX). With
16
- * relational server storage these apply server-side too (DESIGN
17
- * "user indexes") — the same declaration the client materializes.
15
+ * A user-declared index (the migration subset's CREATE INDEX). Relational
16
+ * server storage applies the same declaration the client materializes.
18
17
  */
19
18
  export interface IndexSchema {
20
19
  readonly name: string;
@@ -32,7 +31,7 @@ export interface TableSchema {
32
31
  /** User indexes (optional) — created on the server's relational tables. */
33
32
  readonly indexes?: readonly IndexSchema[];
34
33
  /**
35
- * Server-side column materialization (DESIGN-relational-server-storage.md
34
+ * Server-side column materialization
36
35
  * "optional materialization"). When `true` (the usual default) the server's
37
36
  * row table carries the app's typed columns as a queryable projection;
38
37
  * when `false` it carries only the `_sync_*` meta columns — same storage
package/dist/schema.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The server is configured with a schema IR: tables, columns with the six
5
5
  * §2.4 column types, scope patterns per §3.1, and a schema version. Codegen
6
- * (B5) will emit this shape later; tests hand-write it.
6
+ * Typegen emits this shape; tests hand-write it.
7
7
  */
8
8
  import { validatePortableRelationalIdentifier, } from '@syncular/core';
9
9
  const PATTERN_RE = /^([^{}]+):\{([^{}:]+)\}$/;
@@ -37,8 +37,8 @@ export function compileSchema(schema) {
37
37
  if (tables.has(table.name)) {
38
38
  throw new Error(`duplicate table ${JSON.stringify(table.name)}`);
39
39
  }
40
- // Identifier rules (DESIGN "current-row tables") live in core's
41
- // validatePortableRelationalIdentifier, shared with typegen: app tables
40
+ // Identifier rules live in core's validatePortableRelationalIdentifier,
41
+ // shared with typegen: app tables
42
42
  // share a namespace with the sync infrastructure tables (`sync_*`) and
43
43
  // carry `_sync_*` meta columns, so both prefixes are reserved, and
44
44
  // Postgres silently truncates identifiers over 63 bytes.
package/dist/seed.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The public seeding helper (RFC 0002 §2.5): push app-shaped mutations
2
+ * The public seeding helper: push app-shaped mutations
3
3
  * through the REAL push path — one `handleSyncRequest` round built from the
4
4
  * server schema — so demos, dev servers, and ops scripts seed data with one
5
5
  * supported call. This is the same §6 pipeline every client write takes
@@ -30,8 +30,8 @@ export interface SegmentFindKey {
30
30
  readonly asOfCommitSeq: number;
31
31
  }
32
32
  /**
33
- * Coarse store-level counters for the admin/console read surface (TODO
34
- * §2.5). Optional — a store that omits `stats()` simply cannot report them.
33
+ * Coarse store-level counters for the admin/console read surface. Optional:
34
+ * a store that omits `stats()` simply cannot report them.
35
35
  * All counts include expired-but-not-yet-evicted entries (the store's own
36
36
  * bytes on disk), split by media type.
37
37
  */
@@ -62,7 +62,7 @@ export interface SegmentStore {
62
62
  * rebuilding sqlite images while one exists (§5.3).
63
63
  */
64
64
  find(key: SegmentFindKey, nowMs: number): Promise<SegmentRecord | undefined>;
65
- /** Admin/console counters (TODO §2.5) — ADDITIVE, optional. */
65
+ /** Admin/console counters: ADDITIVE, optional. */
66
66
  stats?(): Promise<SegmentStoreStats>;
67
67
  }
68
68
  export declare function segmentIdFor(bytes: Uint8Array): Promise<string>;
@@ -60,8 +60,8 @@ export async function issueBlobUploadUrl(config, args) {
60
60
  return config.presign({ ...args, ttlSeconds });
61
61
  }
62
62
  const encoder = new TextEncoder();
63
- // Runtime-neutral base64url (no `Buffer` Workers/Deno/browser safe; TODO
64
- // §4.2): `btoa`/`atob` operate on binary strings, present in every runtime.
63
+ // Runtime-neutral base64url: `btoa`/`atob` operate on binary strings and are
64
+ // present in every supported runtime.
65
65
  function base64url(bytes) {
66
66
  let binary = '';
67
67
  for (const byte of bytes)
package/dist/sigv4.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Minimal AWS Signature Version 4 — exactly the subset the S3 segment
3
3
  * store needs (header-signed requests and presigned GET URLs), hand-rolled
4
- * per the dependency-light doctrine (REVISE): SigV4 is small and
4
+ * per the dependency-light doctrine: SigV4 is small and
5
5
  * well-specified, so no SDK.
6
6
  *
7
- * Runtime-neutral (TODO §4.2): the crypto primitives are Web Crypto
7
+ * Runtime-neutral: the crypto primitives are Web Crypto
8
8
  * (`crypto.subtle`), not `node:crypto`, so this module runs unchanged on
9
9
  * Cloudflare Workers / Deno / browsers as well as Bun/Node. `crypto.subtle`
10
10
  * is async, so signing is async throughout; the `S3SegmentStore` consumers
@@ -36,8 +36,6 @@ export declare function amzTimestamps(nowMs: number): {
36
36
  amzDate: string;
37
37
  dateStamp: string;
38
38
  };
39
- /** Sorted, fully re-encoded canonical query string from *decoded* pairs. */
40
- export declare function canonicalQuery(params: Iterable<readonly [string, string]>): string;
41
39
  export interface CanonicalRequestArgs {
42
40
  readonly method: string;
43
41
  /** The URI-encoded absolute path exactly as sent on the request line. */
package/dist/sigv4.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Minimal AWS Signature Version 4 — exactly the subset the S3 segment
3
3
  * store needs (header-signed requests and presigned GET URLs), hand-rolled
4
- * per the dependency-light doctrine (REVISE): SigV4 is small and
4
+ * per the dependency-light doctrine: SigV4 is small and
5
5
  * well-specified, so no SDK.
6
6
  *
7
- * Runtime-neutral (TODO §4.2): the crypto primitives are Web Crypto
7
+ * Runtime-neutral: the crypto primitives are Web Crypto
8
8
  * (`crypto.subtle`), not `node:crypto`, so this module runs unchanged on
9
9
  * Cloudflare Workers / Deno / browsers as well as Bun/Node. `crypto.subtle`
10
10
  * is async, so signing is async throughout; the `S3SegmentStore` consumers
@@ -57,7 +57,7 @@ export function amzTimestamps(nowMs) {
57
57
  return { amzDate, dateStamp: amzDate.slice(0, 8) };
58
58
  }
59
59
  /** Sorted, fully re-encoded canonical query string from *decoded* pairs. */
60
- export function canonicalQuery(params) {
60
+ function canonicalQuery(params) {
61
61
  return [...params]
62
62
  .map(([k, v]) => [uriEncode(k, true), uriEncode(v, true)])
63
63
  .sort(([ak, av], [bk, bv]) => ak < bk ? -1 : ak > bk ? 1 : av < bv ? -1 : av > bv ? 1 : 0)
@@ -3,7 +3,7 @@
3
3
  * dependency-free). Bun-specific by design (top-level `bun:sqlite` import),
4
4
  * so it lives in its own module — the runtime-neutral `BlobStore` interface,
5
5
  * `MemoryBlobStore`, `blobIdFor`, and `isBlobId` stay in `blob-store.ts` for
6
- * the Workers/edge core (TODO §4.2 neutrality discipline; enforced by
6
+ * the Workers/edge core (runtime neutrality is enforced by
7
7
  * `test/runtime-neutrality.test.ts`).
8
8
  */
9
9
  import { Database } from 'bun:sqlite';
@@ -3,7 +3,7 @@
3
3
  * dependency-free). Bun-specific by design (top-level `bun:sqlite` import),
4
4
  * so it lives in its own module — the runtime-neutral `BlobStore` interface,
5
5
  * `MemoryBlobStore`, `blobIdFor`, and `isBlobId` stay in `blob-store.ts` for
6
- * the Workers/edge core (TODO §4.2 neutrality discipline; enforced by
6
+ * the Workers/edge core (runtime neutrality is enforced by
7
7
  * `test/runtime-neutrality.test.ts`).
8
8
  */
9
9
  import { Database } from 'bun:sqlite';
@@ -11,8 +11,7 @@
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
13
13
  * thin storage classes that each speak their driver's native shape while
14
- * importing the same SQL text and codecs from here (TODO §4.2 judgment
15
- * call — "clean parallel implementation against the storage contract").
14
+ * importing the same SQL text and codecs from here.
16
15
  * Both classes run the identical `test/storage-contract.ts`, so the
17
16
  * behavior is held key-for-key regardless.
18
17
  */
@@ -28,10 +27,10 @@ import type { StoredChange, StoredCommit, StoredPushResult, StoredRow } from './
28
27
  * The inverted scope index carries the ordered column (`commit_seq` /
29
28
  * `row_id`) last in the PRIMARY KEY, so the candidate scan is an index
30
29
  * range that returns already-ordered rows — the same covering-index shape
31
- * the Postgres storage documents (§3.1, REVISE B2 performance-by-
30
+ * the Postgres storage documents (§3.1, performance-by-
32
31
  * construction).
33
32
  */
34
- 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";
35
34
  /** Split the DDL into individual statements (D1 applies them one by one). */
36
35
  export declare function sqliteDdlStatements(): string[];
37
36
  /** `?,?,…` for an `IN (…)` clause of `count` positional parameters. */
@@ -9,7 +9,7 @@ import { matchesEffective } from './scopes.js';
9
9
  * The inverted scope index carries the ordered column (`commit_seq` /
10
10
  * `row_id`) last in the PRIMARY KEY, so the candidate scan is an index
11
11
  * range that returns already-ordered rows — the same covering-index shape
12
- * the Postgres storage documents (§3.1, REVISE B2 performance-by-
12
+ * the Postgres storage documents (§3.1, performance-by-
13
13
  * construction).
14
14
  */
15
15
  export const SQLITE_DDL = `
@@ -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,
@@ -13,7 +13,7 @@ export interface SqliteImageInput {
13
13
  }
14
14
  /**
15
15
  * The §5.3 image-builder capability, injected through
16
- * `SyncServerConfig.sqliteImageBuilder` (TODO §4.2). Building an image needs
16
+ * `SyncServerConfig.sqliteImageBuilder`. Building an image needs
17
17
  * a real SQLite engine (`bun:sqlite` here), which is not available on every
18
18
  * runtime — Cloudflare Workers has none. So the core takes the builder as an
19
19
  * optional capability rather than importing `bun:sqlite` on the pull path:
@@ -3,7 +3,7 @@
3
3
  * dependency-free). Bun-specific by design (top-level `bun:sqlite` import),
4
4
  * so it lives in its own module — the runtime-neutral `LeaseStore` interface,
5
5
  * `LeaseRecord`, and `MemoryLeaseStore` stay in `lease-store.ts` for the
6
- * Workers/edge core (TODO §4.2 neutrality discipline; enforced by
6
+ * Workers/edge core (runtime neutrality is enforced by
7
7
  * `test/runtime-neutrality.test.ts`).
8
8
  */
9
9
  import { Database } from 'bun:sqlite';
@@ -3,7 +3,7 @@
3
3
  * dependency-free). Bun-specific by design (top-level `bun:sqlite` import),
4
4
  * so it lives in its own module — the runtime-neutral `LeaseStore` interface,
5
5
  * `LeaseRecord`, and `MemoryLeaseStore` stay in `lease-store.ts` for the
6
- * Workers/edge core (TODO §4.2 neutrality discipline; enforced by
6
+ * Workers/edge core (runtime neutrality is enforced by
7
7
  * `test/runtime-neutrality.test.ts`).
8
8
  */
9
9
  import { Database } from 'bun:sqlite';
@@ -4,8 +4,8 @@
4
4
  * top level, so it lives in its own module — importing it opts into the Bun
5
5
  * runtime. The runtime-neutral `SegmentStore` interface, `MemorySegmentStore`,
6
6
  * and `segmentIdFor` stay in `segment-store.ts` so the Workers/edge core can
7
- * import them without pulling in `bun:sqlite` (TODO §4.2 neutrality
8
- * discipline; enforced by `test/runtime-neutrality.test.ts`).
7
+ * import them without pulling in `bun:sqlite` (runtime neutrality is enforced
8
+ * by `test/runtime-neutrality.test.ts`).
9
9
  */
10
10
  import { Database } from 'bun:sqlite';
11
11
  import { type SegmentFindKey, type SegmentMetadata, type SegmentRecord, type SegmentStore, type SegmentStoreStats } from './segment-store.js';
@@ -4,8 +4,8 @@
4
4
  * top level, so it lives in its own module — importing it opts into the Bun
5
5
  * runtime. The runtime-neutral `SegmentStore` interface, `MemorySegmentStore`,
6
6
  * and `segmentIdFor` stay in `segment-store.ts` so the Workers/edge core can
7
- * import them without pulling in `bun:sqlite` (TODO §4.2 neutrality
8
- * discipline; enforced by `test/runtime-neutrality.test.ts`).
7
+ * import them without pulling in `bun:sqlite` (runtime neutrality is enforced
8
+ * by `test/runtime-neutrality.test.ts`).
9
9
  */
10
10
  import { Database } from 'bun:sqlite';
11
11
  import { DEFAULT_SEGMENT_TTL_MS, segmentIdFor, } from './segment-store.js';
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * SQLite storage via `bun:sqlite` (dev-speed, dependency-free).
3
3
  *
4
- * Scope fanout is index-first (REVISE B2): both the commit log and the
4
+ * Scope fanout is index-first: both the commit log and the
5
5
  * current-row table carry a (table, variable, value) inverted index; reads
6
6
  * select candidates from the index and verify the full multi-variable
7
7
  * match against the stored scope map — never a log scan.
8
8
  */
9
9
  import { Database } from 'bun:sqlite';
10
10
  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';
11
+ 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
12
  export declare class SqliteServerStorage implements ServerStorage {
13
13
  #private;
14
14
  readonly db: Database;
@@ -20,12 +20,21 @@ export declare class SqliteServerStorage implements ServerStorage {
20
20
  /** Internal: write a row + refresh its scope-index entries. */
21
21
  writeRow(partition: string, table: string, row: StoredRow): void;
22
22
  getMaxCommitSeq(partition: string): Promise<number>;
23
+ queryAuthoritative(partition: string, query: AuthoritativeQueryRequest): Promise<AuthoritativeQueryResult>;
23
24
  getHorizonSeq(partition: string): Promise<number>;
24
25
  setHorizonSeq(partition: string, seq: number): Promise<void>;
25
26
  pruneCommitsThrough(partition: string, seq: number): Promise<number>;
26
27
  getCommitSeqBefore(partition: string, createdBeforeMs: number): Promise<number>;
27
28
  getRow(partition: string, table: string, rowId: string): Promise<StoredRow | undefined>;
28
29
  getPushResult(partition: string, clientId: string, clientCommitId: string): Promise<StoredPushResult | undefined>;
30
+ claimReactions(partition: string, query: ReactionClaimQuery): Promise<StoredReaction[]>;
31
+ completeReaction(partition: string, idempotencyKey: string, leaseOwner: string, completedAtMs: number): Promise<boolean>;
32
+ extendReactionLease(partition: string, idempotencyKey: string, leaseOwner: string, leaseExpiresAtMs: number): Promise<boolean>;
33
+ failReaction(partition: string, idempotencyKey: string, update: ReactionFailureUpdate): Promise<boolean>;
34
+ retryReaction(partition: string, idempotencyKey: string, nowMs: number): Promise<boolean>;
35
+ getReaction(partition: string, idempotencyKey: string): Promise<StoredReaction | undefined>;
36
+ listReactions(partition: string, query: ReactionListQuery): Promise<StoredReaction[]>;
37
+ pruneReactions(partition: string, query: ReactionPruneQuery): Promise<PrunedReactionCounts>;
29
38
  readCommitWindow(partition: string, query: CommitWindowQuery): Promise<StoredCommit[]>;
30
39
  scanRows(partition: string, query: RowScanQuery): Promise<StoredRow[]>;
31
40
  scanRowsByIndex(partition: string, query: IndexRowScanQuery): Promise<StoredRow[]>;