@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.
- package/README.md +135 -5
- package/dist/admin.d.ts +12 -6
- package/dist/admin.js +11 -1
- package/dist/authoritative-query.d.ts +20 -0
- package/dist/authoritative-query.js +184 -0
- package/dist/blob-store.d.ts +2 -2
- package/dist/context.d.ts +11 -2
- package/dist/context.js +2 -0
- package/dist/d1-storage.d.ts +10 -1
- package/dist/d1-storage.js +219 -3
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +43 -1
- package/dist/events-ring.d.ts +1 -1
- package/dist/events-ring.js +1 -1
- package/dist/events.d.ts +52 -3
- package/dist/handler.js +6 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/operations-realtime.d.ts +16 -0
- package/dist/operations-realtime.js +196 -0
- package/dist/operations.d.ts +97 -0
- package/dist/operations.js +392 -0
- package/dist/pg-executor.d.ts +1 -1
- package/dist/pg-executor.js +1 -1
- package/dist/postgres-fanout.d.ts +1 -1
- package/dist/postgres-storage.d.ts +11 -2
- package/dist/postgres-storage.js +223 -3
- package/dist/pull.js +1 -1
- package/dist/push.d.ts +8 -2
- package/dist/push.js +75 -21
- package/dist/reactions.d.ts +167 -0
- package/dist/reactions.js +442 -0
- package/dist/realtime.js +4 -1
- package/dist/relational-rows.d.ts +9 -9
- package/dist/relational-rows.js +9 -9
- package/dist/s3-blob-store.js +1 -1
- package/dist/s3-segment-store.js +1 -1
- package/dist/schema.d.ts +4 -5
- package/dist/schema.js +3 -3
- package/dist/seed.js +1 -1
- package/dist/segment-store.d.ts +3 -3
- package/dist/signed-url.js +2 -2
- package/dist/sigv4.d.ts +2 -4
- package/dist/sigv4.js +3 -3
- package/dist/sqlite-blob-store.d.ts +1 -1
- package/dist/sqlite-blob-store.js +1 -1
- package/dist/sqlite-dialect.d.ts +3 -4
- package/dist/sqlite-dialect.js +21 -1
- package/dist/sqlite-image.d.ts +1 -1
- package/dist/sqlite-lease-store.d.ts +1 -1
- package/dist/sqlite-lease-store.js +1 -1
- package/dist/sqlite-segment-store.d.ts +2 -2
- package/dist/sqlite-segment-store.js +2 -2
- package/dist/sqlite-storage.d.ts +11 -2
- package/dist/sqlite-storage.js +219 -4
- package/dist/storage.d.ts +112 -3
- package/dist/validate.js +1 -0
- package/package.json +2 -2
- package/src/admin.ts +30 -6
- package/src/authoritative-query.ts +218 -0
- package/src/blob-store.ts +0 -0
- package/src/context.ts +12 -2
- package/src/d1-storage.ts +355 -3
- package/src/errors.ts +43 -1
- package/src/events-ring.ts +1 -1
- package/src/events.ts +64 -2
- package/src/handler.ts +15 -3
- package/src/index.ts +33 -1
- package/src/operations-realtime.ts +272 -0
- package/src/operations.ts +720 -0
- package/src/pg-executor.ts +1 -1
- package/src/postgres-fanout.ts +1 -1
- package/src/postgres-storage.ts +355 -4
- package/src/pull.ts +1 -1
- package/src/push.ts +97 -29
- package/src/reactions.ts +741 -0
- package/src/realtime.ts +7 -1
- package/src/relational-rows.ts +9 -9
- package/src/s3-blob-store.ts +1 -1
- package/src/s3-segment-store.ts +1 -1
- package/src/schema.ts +6 -7
- package/src/seed.ts +1 -1
- package/src/segment-store.ts +3 -3
- package/src/signed-url.ts +2 -2
- package/src/sigv4.ts +3 -5
- package/src/sqlite-blob-store.ts +1 -1
- package/src/sqlite-dialect.ts +22 -3
- package/src/sqlite-image.ts +1 -1
- package/src/sqlite-lease-store.ts +1 -1
- package/src/sqlite-segment-store.ts +2 -2
- package/src/sqlite-storage.ts +369 -4
- package/src/storage.ts +168 -3
- package/src/validate.ts +1 -0
package/dist/relational-rows.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Relational current-row storage
|
|
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
|
|
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
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
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
|
|
406
|
-
*
|
|
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
|
*
|
package/dist/s3-blob-store.js
CHANGED
|
@@ -23,7 +23,7 @@ function parseStatsJson(json) {
|
|
|
23
23
|
bytes: Number(parsed.bytes ?? 0),
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
// Runtime-neutral base64url
|
|
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) {
|
package/dist/s3-segment-store.js
CHANGED
|
@@ -32,7 +32,7 @@ function parseStatsJson(json) {
|
|
|
32
32
|
sqliteSegments: Number(parsed.sqliteSegments ?? 0),
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
// Runtime-neutral base64url
|
|
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
|
-
*
|
|
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).
|
|
16
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
41
|
-
//
|
|
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
|
|
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
|
package/dist/segment-store.d.ts
CHANGED
|
@@ -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
|
|
34
|
-
*
|
|
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
|
|
65
|
+
/** Admin/console counters: ADDITIVE, optional. */
|
|
66
66
|
stats?(): Promise<SegmentStoreStats>;
|
|
67
67
|
}
|
|
68
68
|
export declare function segmentIdFor(bytes: Uint8Array): Promise<string>;
|
package/dist/signed-url.js
CHANGED
|
@@ -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
|
|
64
|
-
//
|
|
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
|
|
4
|
+
* per the dependency-light doctrine: SigV4 is small and
|
|
5
5
|
* well-specified, so no SDK.
|
|
6
6
|
*
|
|
7
|
-
* Runtime-neutral
|
|
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
|
|
4
|
+
* per the dependency-light doctrine: SigV4 is small and
|
|
5
5
|
* well-specified, so no SDK.
|
|
6
6
|
*
|
|
7
|
-
* Runtime-neutral
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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';
|
package/dist/sqlite-dialect.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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. */
|
package/dist/sqlite-dialect.js
CHANGED
|
@@ -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,
|
|
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,
|
package/dist/sqlite-image.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface SqliteImageInput {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* The §5.3 image-builder capability, injected through
|
|
16
|
-
* `SyncServerConfig.sqliteImageBuilder
|
|
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 (
|
|
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 (
|
|
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` (
|
|
8
|
-
*
|
|
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` (
|
|
8
|
-
*
|
|
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';
|
package/dist/sqlite-storage.d.ts
CHANGED
|
@@ -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
|
|
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[]>;
|