@syncular/server 0.15.44 → 0.15.45
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 +1 -1
- package/dist/admin.d.ts +2 -2
- package/dist/admin.js +1 -1
- package/dist/blob-store.d.ts +2 -2
- package/dist/context.d.ts +2 -2
- package/dist/d1-storage.js +3 -3
- package/dist/events-ring.d.ts +1 -1
- package/dist/events-ring.js +1 -1
- package/dist/handler.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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.js +3 -3
- package/dist/pull.js +1 -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 +2 -3
- package/dist/sqlite-dialect.js +1 -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 +1 -1
- package/dist/sqlite-storage.js +4 -4
- package/dist/storage.d.ts +3 -3
- package/package.json +2 -2
- package/src/admin.ts +3 -3
- package/src/blob-store.ts +0 -0
- package/src/context.ts +2 -2
- package/src/d1-storage.ts +3 -3
- package/src/events-ring.ts +1 -1
- package/src/handler.ts +2 -2
- package/src/index.ts +1 -1
- package/src/pg-executor.ts +1 -1
- package/src/postgres-fanout.ts +1 -1
- package/src/postgres-storage.ts +4 -4
- package/src/pull.ts +1 -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 +2 -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 +4 -4
- package/src/storage.ts +3 -3
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ pruning (§4.6), and signed-URL token issuance (§5.4). `SPEC.md` is
|
|
|
8
8
|
normative for everything on the wire; this README covers the **host
|
|
9
9
|
surface** — in particular the ops seam and the pruning runbook.
|
|
10
10
|
|
|
11
|
-
## Deployment matrix (runtime adapters
|
|
11
|
+
## Deployment matrix (runtime adapters)
|
|
12
12
|
|
|
13
13
|
The server core is **runtime-neutral TypeScript** — `handleSyncRequest` and
|
|
14
14
|
the realtime session speak only Web `Request`/`Response`/`fetch`/Web-Crypto,
|
package/dist/admin.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `SyncularAdmin`
|
|
3
|
-
*
|
|
2
|
+
* `SyncularAdmin` is the operator-facing read surface over the server core.
|
|
3
|
+
* It is a read-only, partition-scoped, JSON-able query layer over
|
|
4
4
|
* `ServerStorage`, the optional segment/blob store stats, and an in-memory
|
|
5
5
|
* event ring. It delivers the 80% operator value (who's connected, what's
|
|
6
6
|
* flowing, horizon health, the event tail) as a handful of queries in the
|
package/dist/admin.js
CHANGED
|
@@ -16,7 +16,7 @@ function percentile(sample, q) {
|
|
|
16
16
|
}
|
|
17
17
|
function required(value, what) {
|
|
18
18
|
if (value === undefined) {
|
|
19
|
-
throw new Error(`SyncularAdmin: the configured ${what} does not implement this
|
|
19
|
+
throw new Error(`SyncularAdmin: the configured ${what} does not implement this optional read`);
|
|
20
20
|
}
|
|
21
21
|
return value;
|
|
22
22
|
}
|
package/dist/blob-store.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface BlobRecord {
|
|
|
20
20
|
readonly createdAtMs: number;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Coarse blob-store counters for the admin/console read surface (
|
|
23
|
+
* Coarse blob-store counters for the admin/console read surface (work item
|
|
24
24
|
* §2.5), partition-scoped. Optional. `count`/`bytes` are total stored
|
|
25
25
|
* blobs; a store that omits `stats()` cannot report them.
|
|
26
26
|
*/
|
|
@@ -53,7 +53,7 @@ export interface BlobStore {
|
|
|
53
53
|
* Returns the deleted blobIds.
|
|
54
54
|
*/
|
|
55
55
|
sweepOrphans(partition: string, olderThanMs: number, referencedBlobIds: ReadonlySet<string>): Promise<string[]>;
|
|
56
|
-
/** Admin/console counters (
|
|
56
|
+
/** Admin/console counters (work item §2.5) — ADDITIVE, optional. */
|
|
57
57
|
stats?(partition: string): Promise<BlobStoreStats>;
|
|
58
58
|
}
|
|
59
59
|
/** `"sha256:" + hex` of the bytes — the content address (§5.9.1). */
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The host context for `handleSyncRequest
|
|
2
|
+
* The host context for `handleSyncRequest`: storage, the
|
|
3
3
|
* `resolveScopes` host callback (runs in the host process — the moat),
|
|
4
4
|
* clock, and a segment store. Framework-free; adapters supply `partition`
|
|
5
5
|
* and `actorId` from host authentication (§1.1).
|
|
@@ -136,7 +136,7 @@ export interface SyncServerConfig {
|
|
|
136
136
|
*/
|
|
137
137
|
readonly blobUploadUrls?: BlobUploadPresignConfig;
|
|
138
138
|
/**
|
|
139
|
-
* §5.3 sqlite-image builder
|
|
139
|
+
* §5.3 sqlite-image builder, injected so the pull path never
|
|
140
140
|
* statically imports `bun:sqlite`. Absent ⇒ the sqlite-image lane is off
|
|
141
141
|
* (bit-2 clients are served the rows lane) — the Workers/edge posture. A
|
|
142
142
|
* Bun/Node host wires `buildSqliteImage` from `@syncular/server`.
|
package/dist/d1-storage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
var _a;
|
|
2
2
|
/**
|
|
3
|
-
* Cloudflare D1 server storage
|
|
3
|
+
* Cloudflare D1 server storage for Workers deployments.
|
|
4
4
|
*
|
|
5
5
|
* D1 *is* SQLite exposed over an async, statement-at-a-time API
|
|
6
6
|
* (`prepare(sql).bind(...).all()` / `.first()` / `.run()`, plus `batch([…])`
|
|
@@ -448,7 +448,7 @@ export class D1ServerStorage {
|
|
|
448
448
|
for (const table of schema.tables.values()) {
|
|
449
449
|
const bindCount = tableColumnNames(table).length;
|
|
450
450
|
if (bindCount > D1_MAX_BIND_PARAMS) {
|
|
451
|
-
throw new Error(`table ${JSON.stringify(table.name)} needs ${bindCount} bound parameters per upsert
|
|
451
|
+
throw new Error(`table ${JSON.stringify(table.name)} needs ${bindCount} bound parameters per upsert; D1 caps statements at ${D1_MAX_BIND_PARAMS}`);
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
454
|
await this.#db.exec(`${SCHEMA_META_DDL_SQLITE.replace(/\s+/g, ' ')};`);
|
|
@@ -757,7 +757,7 @@ export class D1ServerStorage {
|
|
|
757
757
|
.all();
|
|
758
758
|
return results.map((r) => r.blob_id);
|
|
759
759
|
}
|
|
760
|
-
// -- admin/console read surface
|
|
760
|
+
// -- admin/console read surface --------------------------------------------
|
|
761
761
|
async listClientRecords(partition) {
|
|
762
762
|
const { results } = await this.#db
|
|
763
763
|
.prepare('SELECT client_id, actor_id, cursor, subscriptions, updated_at_ms FROM sync_clients WHERE partition=? ORDER BY updated_at_ms DESC')
|
package/dist/events-ring.d.ts
CHANGED
package/dist/events-ring.js
CHANGED
package/dist/handler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `handleSyncRequest(bytes, ctx) → bytes` (SPEC.md §1
|
|
2
|
+
* `handleSyncRequest(bytes, ctx) → bytes` (SPEC.md §1).
|
|
3
3
|
*
|
|
4
4
|
* Internally streaming-friendly (§1.4): `createSyncResponseStream` returns
|
|
5
5
|
* an async iterable of encoded chunks — one per frame — after performing
|
|
@@ -455,7 +455,7 @@ async function createStreamCore(bytes, ctx, events, startedAtMs = 0) {
|
|
|
455
455
|
throw error;
|
|
456
456
|
}
|
|
457
457
|
const schema = compileSchema(ctx.schema);
|
|
458
|
-
// Relational row tables
|
|
458
|
+
// Relational row tables: create/
|
|
459
459
|
// migrate on first contact; memoized per storage instance thereafter.
|
|
460
460
|
await ctx.storage.ensureSchema(schema);
|
|
461
461
|
const plan = await planRequest(request, ctx, schema);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @syncular/server — framework-free embeddable SSP2 protocol library
|
|
3
|
-
* (SPEC.md is normative
|
|
3
|
+
* (SPEC.md is normative).
|
|
4
4
|
*
|
|
5
5
|
* Core surface: `handleSyncRequest(bytes, ctx) → bytes` over host-provided
|
|
6
6
|
* storage / scope-resolution / segment-store interfaces, plus a
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @syncular/server — framework-free embeddable SSP2 protocol library
|
|
3
|
-
* (SPEC.md is normative
|
|
3
|
+
* (SPEC.md is normative).
|
|
4
4
|
*
|
|
5
5
|
* Core surface: `handleSyncRequest(bytes, ctx) → bytes` over host-provided
|
|
6
6
|
* storage / scope-resolution / segment-store interfaces, plus a
|
package/dist/pg-executor.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The minimal Postgres executor seam
|
|
2
|
+
* The minimal Postgres executor seam has zero runtime dependencies.
|
|
3
3
|
*
|
|
4
4
|
* `PostgresServerStorage` is written entirely against this interface so the
|
|
5
5
|
* server library never imports a specific driver. Production wires one of:
|
package/dist/pg-executor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The minimal Postgres executor seam
|
|
2
|
+
* The minimal Postgres executor seam has zero runtime dependencies.
|
|
3
3
|
*
|
|
4
4
|
* `PostgresServerStorage` is written entirely against this interface so the
|
|
5
5
|
* server library never imports a specific driver. Production wires one of:
|
package/dist/postgres-storage.js
CHANGED
|
@@ -191,8 +191,8 @@ function toStoredChange(record) {
|
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
193
|
/**
|
|
194
|
-
* Migration rewrite
|
|
195
|
-
*
|
|
194
|
+
* Migration rewrite: keyset-paged walk of a row table inside the migration
|
|
195
|
+
* transaction. When `oldLayout` is
|
|
196
196
|
* given every payload re-encodes under the current columns, and the
|
|
197
197
|
* projection (when materialized) refreshes from the payload either way.
|
|
198
198
|
*/
|
|
@@ -818,7 +818,7 @@ export class PostgresServerStorage {
|
|
|
818
818
|
const { rows } = await this.#exec.query('SELECT DISTINCT blob_id FROM sync_blob_refs WHERE partition=$1', [partition]);
|
|
819
819
|
return rows.map((r) => r.blob_id);
|
|
820
820
|
}
|
|
821
|
-
// -- admin/console read surface
|
|
821
|
+
// -- admin/console read surface --------------------------------------------
|
|
822
822
|
async listClientRecords(partition) {
|
|
823
823
|
const { rows } = await this.#exec.query('SELECT client_id, actor_id, cursor, subscriptions, updated_at_ms FROM sync_clients WHERE partition=$1 ORDER BY updated_at_ms DESC', [partition]);
|
|
824
824
|
return rows.map((r) => ({
|
package/dist/pull.js
CHANGED
|
@@ -170,7 +170,7 @@ async function* sqliteImageSegment(ctx, schema, limits, plan, asOf, digest, trac
|
|
|
170
170
|
// builder through `sqliteImageBuilder`; when omitted we default to the
|
|
171
171
|
// in-tree `buildSqliteImage` ONLY on a Bun runtime, reached by a *dynamic*
|
|
172
172
|
// import so `bun:sqlite` is never a static dependency of the pull path
|
|
173
|
-
// (
|
|
173
|
+
// (runtime neutrality is enforced by test/runtime-neutrality.test.ts). On
|
|
174
174
|
// Workers/edge (no `Bun`) this yields the rows lane for a bit-2 client — a
|
|
175
175
|
// support floor, not a fallback (§5.3: sqlite is an *accept*, not a demand).
|
|
176
176
|
const buildImage = await resolveImageBuilder(ctx);
|
|
@@ -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 —
|
|
@@ -54,7 +54,7 @@ export declare const SYNC_SCOPES_COLUMN = "_sync_scopes";
|
|
|
54
54
|
export declare const SYNC_PAYLOAD_COLUMN = "_sync_payload";
|
|
55
55
|
/** SQL-standard identifier quoting (both dialects). */
|
|
56
56
|
export declare function quoteIdent(name: string): string;
|
|
57
|
-
/** §5.3-style type affinities, per dialect
|
|
57
|
+
/** §5.3-style type affinities, per dialect. */
|
|
58
58
|
export declare function columnSqlType(column: RowColumn, dialect: RelationalDialect): string;
|
|
59
59
|
/**
|
|
60
60
|
* The full column list of a relational row table, in INSERT order:
|
|
@@ -86,11 +86,11 @@ export declare const SYNC_INDEX_PREFIX = "sync_ix_";
|
|
|
86
86
|
*/
|
|
87
87
|
export declare function physicalIndexName(declaredName: string): string;
|
|
88
88
|
/**
|
|
89
|
-
* CREATE INDEX IF NOT EXISTS for the table's user-declared indexes
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
89
|
+
* CREATE INDEX IF NOT EXISTS for the table's user-declared indexes. These use
|
|
90
|
+
* the same declared names and columns the client materializes. Cross-table
|
|
91
|
+
* index-name uniqueness is the user's schema concern, as it is client-side.
|
|
92
|
+
* Server-side the physical name carries the {@link SYNC_INDEX_PREFIX}
|
|
93
|
+
* ownership marker.
|
|
94
94
|
*/
|
|
95
95
|
export declare function createIndexDdl(table: CompiledTable): string[];
|
|
96
96
|
/** Idempotent removal of one Syncular-owned relational projection index. */
|
|
@@ -182,8 +182,8 @@ export declare function selectRowScopesSql(table: CompiledTable, dialect: Relati
|
|
|
182
182
|
/** DELETE one stored row. Params: [partition, rowId]. */
|
|
183
183
|
export declare function deleteRowSql(table: CompiledTable, dialect: RelationalDialect): string;
|
|
184
184
|
/**
|
|
185
|
-
* The schema-version marker table
|
|
186
|
-
*
|
|
185
|
+
* The schema-version marker table gates DDL work. `ensureSchema` compares the
|
|
186
|
+
* stored version and skips
|
|
187
187
|
* all introspection/DDL when it matches — one cheap read per storage
|
|
188
188
|
* instance (relevant for D1's per-request instantiation).
|
|
189
189
|
*
|
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,7 +27,7 @@ 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
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";
|
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 = `
|
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,7 +1,7 @@
|
|
|
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.
|
package/dist/sqlite-storage.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.
|
|
@@ -240,8 +240,8 @@ export class SqliteServerStorage {
|
|
|
240
240
|
this.#schemaVersion = schema.version;
|
|
241
241
|
}
|
|
242
242
|
/**
|
|
243
|
-
* Migration rewrite
|
|
244
|
-
*
|
|
243
|
+
* Migration rewrite: keyset-paged walk of a row table. When `oldLayout` is
|
|
244
|
+
* given every payload re-encodes under
|
|
245
245
|
* the current columns, and the projection (when materialized) refreshes
|
|
246
246
|
* from the payload either way.
|
|
247
247
|
*/
|
|
@@ -498,7 +498,7 @@ export class SqliteServerStorage {
|
|
|
498
498
|
.all(partition);
|
|
499
499
|
return rows.map((r) => r.blob_id);
|
|
500
500
|
}
|
|
501
|
-
// -- admin/console read surface
|
|
501
|
+
// -- admin/console read surface --------------------------------------------
|
|
502
502
|
async listClientRecords(partition) {
|
|
503
503
|
const records = this.db
|
|
504
504
|
.query('SELECT client_id, actor_id, cursor, subscriptions, updated_at_ms FROM sync_clients WHERE partition=? ORDER BY updated_at_ms DESC')
|
package/dist/storage.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Requirements it encodes:
|
|
5
5
|
* - commit log with per-partition `commitSeq` (§2.1) and a commit→scope
|
|
6
|
-
* inverted index
|
|
6
|
+
* inverted index (§3.1): pulls filter
|
|
7
7
|
* by scope via the index, never by scanning the log;
|
|
8
8
|
* - idempotency results per §2.3 persisted in the same transaction as the
|
|
9
9
|
* commit's writes (§6.4);
|
|
@@ -241,7 +241,7 @@ export interface StorageTransaction {
|
|
|
241
241
|
export interface ServerStorage {
|
|
242
242
|
/**
|
|
243
243
|
* Create/migrate the relational per-app row tables for `schema`
|
|
244
|
-
*
|
|
244
|
+
* Idempotent and cheap when the
|
|
245
245
|
* stored schema version already matches (one marker read, memoized per
|
|
246
246
|
* instance); on first use it creates the tables, on a version bump it
|
|
247
247
|
* applies the migration subset (CREATE TABLE / ADD COLUMN / CREATE
|
|
@@ -305,7 +305,7 @@ export interface ServerStorage {
|
|
|
305
305
|
listRowsReferencingBlob?(partition: string, blobId: string): Promise<BlobReferencingRow[]>;
|
|
306
306
|
listReferencedBlobIds?(partition: string): Promise<string[]>;
|
|
307
307
|
/**
|
|
308
|
-
* Admin/console read surface (`SyncularAdmin
|
|
308
|
+
* Admin/console read surface (`SyncularAdmin`): ADDITIVE,
|
|
309
309
|
* optional. All read-only, partition-scoped, and JSON-able. A backend
|
|
310
310
|
* that omits these simply cannot serve the console; the sync path never
|
|
311
311
|
* calls them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/server",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.45",
|
|
4
4
|
"description": "Syncular server: handleSyncRequest + storage/auth interfaces for the sync protocol",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"!dist/**/*.test.d.ts"
|
|
54
54
|
],
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@syncular/core": "0.15.
|
|
56
|
+
"@syncular/core": "0.15.45"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@electric-sql/pglite": "^0.5.4"
|
package/src/admin.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `SyncularAdmin`
|
|
3
|
-
*
|
|
2
|
+
* `SyncularAdmin` is the operator-facing read surface over the server core.
|
|
3
|
+
* It is a read-only, partition-scoped, JSON-able query layer over
|
|
4
4
|
* `ServerStorage`, the optional segment/blob store stats, and an in-memory
|
|
5
5
|
* event ring. It delivers the 80% operator value (who's connected, what's
|
|
6
6
|
* flowing, horizon health, the event tail) as a handful of queries in the
|
|
@@ -179,7 +179,7 @@ function percentile(sample: readonly number[], q: number): number {
|
|
|
179
179
|
function required<T>(value: T | undefined, what: string): T {
|
|
180
180
|
if (value === undefined) {
|
|
181
181
|
throw new Error(
|
|
182
|
-
`SyncularAdmin: the configured ${what} does not implement this
|
|
182
|
+
`SyncularAdmin: the configured ${what} does not implement this optional read`,
|
|
183
183
|
);
|
|
184
184
|
}
|
|
185
185
|
return value;
|
package/src/blob-store.ts
CHANGED
|
Binary file
|
package/src/context.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The host context for `handleSyncRequest
|
|
2
|
+
* The host context for `handleSyncRequest`: storage, the
|
|
3
3
|
* `resolveScopes` host callback (runs in the host process — the moat),
|
|
4
4
|
* clock, and a segment store. Framework-free; adapters supply `partition`
|
|
5
5
|
* and `actorId` from host authentication (§1.1).
|
|
@@ -154,7 +154,7 @@ export interface SyncServerConfig {
|
|
|
154
154
|
*/
|
|
155
155
|
readonly blobUploadUrls?: BlobUploadPresignConfig;
|
|
156
156
|
/**
|
|
157
|
-
* §5.3 sqlite-image builder
|
|
157
|
+
* §5.3 sqlite-image builder, injected so the pull path never
|
|
158
158
|
* statically imports `bun:sqlite`. Absent ⇒ the sqlite-image lane is off
|
|
159
159
|
* (bit-2 clients are served the rows lane) — the Workers/edge posture. A
|
|
160
160
|
* Bun/Node host wires `buildSqliteImage` from `@syncular/server`.
|
package/src/d1-storage.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cloudflare D1 server storage
|
|
2
|
+
* Cloudflare D1 server storage for Workers deployments.
|
|
3
3
|
*
|
|
4
4
|
* D1 *is* SQLite exposed over an async, statement-at-a-time API
|
|
5
5
|
* (`prepare(sql).bind(...).all()` / `.first()` / `.run()`, plus `batch([…])`
|
|
@@ -687,7 +687,7 @@ export class D1ServerStorage implements ServerStorage {
|
|
|
687
687
|
const bindCount = tableColumnNames(table).length;
|
|
688
688
|
if (bindCount > D1_MAX_BIND_PARAMS) {
|
|
689
689
|
throw new Error(
|
|
690
|
-
`table ${JSON.stringify(table.name)} needs ${bindCount} bound parameters per upsert
|
|
690
|
+
`table ${JSON.stringify(table.name)} needs ${bindCount} bound parameters per upsert; D1 caps statements at ${D1_MAX_BIND_PARAMS}`,
|
|
691
691
|
);
|
|
692
692
|
}
|
|
693
693
|
}
|
|
@@ -1137,7 +1137,7 @@ export class D1ServerStorage implements ServerStorage {
|
|
|
1137
1137
|
return results.map((r) => r.blob_id);
|
|
1138
1138
|
}
|
|
1139
1139
|
|
|
1140
|
-
// -- admin/console read surface
|
|
1140
|
+
// -- admin/console read surface --------------------------------------------
|
|
1141
1141
|
|
|
1142
1142
|
async listClientRecords(partition: string): Promise<ClientRecord[]> {
|
|
1143
1143
|
const { results } = await this.#db
|
package/src/events-ring.ts
CHANGED
package/src/handler.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `handleSyncRequest(bytes, ctx) → bytes` (SPEC.md §1
|
|
2
|
+
* `handleSyncRequest(bytes, ctx) → bytes` (SPEC.md §1).
|
|
3
3
|
*
|
|
4
4
|
* Internally streaming-friendly (§1.4): `createSyncResponseStream` returns
|
|
5
5
|
* an async iterable of encoded chunks — one per frame — after performing
|
|
@@ -629,7 +629,7 @@ async function createStreamCore(
|
|
|
629
629
|
throw error;
|
|
630
630
|
}
|
|
631
631
|
const schema = compileSchema(ctx.schema);
|
|
632
|
-
// Relational row tables
|
|
632
|
+
// Relational row tables: create/
|
|
633
633
|
// migrate on first contact; memoized per storage instance thereafter.
|
|
634
634
|
await ctx.storage.ensureSchema(schema);
|
|
635
635
|
const plan = await planRequest(request, ctx, schema);
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @syncular/server — framework-free embeddable SSP2 protocol library
|
|
3
|
-
* (SPEC.md is normative
|
|
3
|
+
* (SPEC.md is normative).
|
|
4
4
|
*
|
|
5
5
|
* Core surface: `handleSyncRequest(bytes, ctx) → bytes` over host-provided
|
|
6
6
|
* storage / scope-resolution / segment-store interfaces, plus a
|
package/src/pg-executor.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The minimal Postgres executor seam
|
|
2
|
+
* The minimal Postgres executor seam has zero runtime dependencies.
|
|
3
3
|
*
|
|
4
4
|
* `PostgresServerStorage` is written entirely against this interface so the
|
|
5
5
|
* server library never imports a specific driver. Production wires one of:
|
package/src/postgres-fanout.ts
CHANGED
package/src/postgres-storage.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Postgres server storage
|
|
2
|
+
* Postgres server storage: the production database path.
|
|
3
3
|
*
|
|
4
4
|
* Semantics mirror `SqliteServerStorage` exactly (both run the shared
|
|
5
5
|
* storage contract in `test/storage-contract.ts`); the difference is that
|
|
@@ -336,8 +336,8 @@ function toStoredChange(record: ChangeRecord): StoredChange {
|
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
/**
|
|
339
|
-
* Migration rewrite
|
|
340
|
-
*
|
|
339
|
+
* Migration rewrite: keyset-paged walk of a row table inside the migration
|
|
340
|
+
* transaction. When `oldLayout` is
|
|
341
341
|
* given every payload re-encodes under the current columns, and the
|
|
342
342
|
* projection (when materialized) refreshes from the payload either way.
|
|
343
343
|
*/
|
|
@@ -1251,7 +1251,7 @@ export class PostgresServerStorage implements ServerStorage {
|
|
|
1251
1251
|
return rows.map((r) => r.blob_id);
|
|
1252
1252
|
}
|
|
1253
1253
|
|
|
1254
|
-
// -- admin/console read surface
|
|
1254
|
+
// -- admin/console read surface --------------------------------------------
|
|
1255
1255
|
|
|
1256
1256
|
async listClientRecords(partition: string): Promise<ClientRecord[]> {
|
|
1257
1257
|
const { rows } = await this.#exec.query<{
|
package/src/pull.ts
CHANGED
|
@@ -253,7 +253,7 @@ async function* sqliteImageSegment(
|
|
|
253
253
|
// builder through `sqliteImageBuilder`; when omitted we default to the
|
|
254
254
|
// in-tree `buildSqliteImage` ONLY on a Bun runtime, reached by a *dynamic*
|
|
255
255
|
// import so `bun:sqlite` is never a static dependency of the pull path
|
|
256
|
-
// (
|
|
256
|
+
// (runtime neutrality is enforced by test/runtime-neutrality.test.ts). On
|
|
257
257
|
// Workers/edge (no `Bun`) this yields the rows lane for a bit-2 client — a
|
|
258
258
|
// support floor, not a fallback (§5.3: sqlite is an *accept*, not a demand).
|
|
259
259
|
const buildImage = await resolveImageBuilder(ctx);
|
package/src/relational-rows.ts
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 —
|
|
@@ -65,7 +65,7 @@ export function quoteIdent(name: string): string {
|
|
|
65
65
|
return `"${name.replaceAll('"', '""')}"`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/** §5.3-style type affinities, per dialect
|
|
68
|
+
/** §5.3-style type affinities, per dialect. */
|
|
69
69
|
export function columnSqlType(
|
|
70
70
|
column: RowColumn,
|
|
71
71
|
dialect: RelationalDialect,
|
|
@@ -204,11 +204,11 @@ export function physicalIndexName(declaredName: string): string {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
|
-
* CREATE INDEX IF NOT EXISTS for the table's user-declared indexes
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
207
|
+
* CREATE INDEX IF NOT EXISTS for the table's user-declared indexes. These use
|
|
208
|
+
* the same declared names and columns the client materializes. Cross-table
|
|
209
|
+
* index-name uniqueness is the user's schema concern, as it is client-side.
|
|
210
|
+
* Server-side the physical name carries the {@link SYNC_INDEX_PREFIX}
|
|
211
|
+
* ownership marker.
|
|
212
212
|
*/
|
|
213
213
|
export function createIndexDdl(table: CompiledTable): string[] {
|
|
214
214
|
// User indexes name app columns — nothing to index without the projection.
|
|
@@ -478,8 +478,8 @@ export function deleteRowSql(
|
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
/**
|
|
481
|
-
* The schema-version marker table
|
|
482
|
-
*
|
|
481
|
+
* The schema-version marker table gates DDL work. `ensureSchema` compares the
|
|
482
|
+
* stored version and skips
|
|
483
483
|
* all introspection/DDL when it matches — one cheap read per storage
|
|
484
484
|
* instance (relevant for D1's per-request instantiation).
|
|
485
485
|
*
|
package/src/s3-blob-store.ts
CHANGED
|
@@ -98,7 +98,7 @@ function parseStatsJson(json: string): StatsAccumulator {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// Runtime-neutral base64url
|
|
101
|
+
// Runtime-neutral base64url: `Buffer` is not present on
|
|
102
102
|
// Cloudflare Workers without `nodejs_compat`, so the metadata header is
|
|
103
103
|
// (de)coded with `btoa`/`atob`, available in every runtime.
|
|
104
104
|
function utf8ToBase64url(text: string): string {
|
package/src/s3-segment-store.ts
CHANGED
|
@@ -112,7 +112,7 @@ function parseStatsJson(json: string): StatsAccumulator {
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
// Runtime-neutral base64url
|
|
115
|
+
// Runtime-neutral base64url: `Buffer` is not present on
|
|
116
116
|
// Cloudflare Workers without `nodejs_compat`, so the object-metadata record
|
|
117
117
|
// header is (de)coded with `btoa`/`atob`, available in every runtime.
|
|
118
118
|
function utf8ToBase64url(text: string): string {
|
package/src/schema.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 {
|
|
9
9
|
type RowColumn,
|
|
@@ -14,9 +14,8 @@ import {
|
|
|
14
14
|
export type ScopePatternSpec = string | { pattern: string; column: string };
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* A user-declared index (the migration subset's CREATE INDEX).
|
|
18
|
-
*
|
|
19
|
-
* "user indexes") — the same declaration the client materializes.
|
|
17
|
+
* A user-declared index (the migration subset's CREATE INDEX). Relational
|
|
18
|
+
* server storage applies the same declaration the client materializes.
|
|
20
19
|
*/
|
|
21
20
|
export interface IndexSchema {
|
|
22
21
|
readonly name: string;
|
|
@@ -35,7 +34,7 @@ export interface TableSchema {
|
|
|
35
34
|
/** User indexes (optional) — created on the server's relational tables. */
|
|
36
35
|
readonly indexes?: readonly IndexSchema[];
|
|
37
36
|
/**
|
|
38
|
-
* Server-side column materialization
|
|
37
|
+
* Server-side column materialization
|
|
39
38
|
* "optional materialization"). When `true` (the usual default) the server's
|
|
40
39
|
* row table carries the app's typed columns as a queryable projection;
|
|
41
40
|
* when `false` it carries only the `_sync_*` meta columns — same storage
|
|
@@ -142,8 +141,8 @@ export function compileSchema(schema: ServerSchema): CompiledSchema {
|
|
|
142
141
|
if (tables.has(table.name)) {
|
|
143
142
|
throw new Error(`duplicate table ${JSON.stringify(table.name)}`);
|
|
144
143
|
}
|
|
145
|
-
// Identifier rules
|
|
146
|
-
//
|
|
144
|
+
// Identifier rules live in core's validatePortableRelationalIdentifier,
|
|
145
|
+
// shared with typegen: app tables
|
|
147
146
|
// share a namespace with the sync infrastructure tables (`sync_*`) and
|
|
148
147
|
// carry `_sync_*` meta columns, so both prefixes are reserved, and
|
|
149
148
|
// Postgres silently truncates identifiers over 63 bytes.
|
package/src/seed.ts
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/src/segment-store.ts
CHANGED
|
@@ -44,8 +44,8 @@ export interface SegmentFindKey {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Coarse store-level counters for the admin/console read surface
|
|
48
|
-
*
|
|
47
|
+
* Coarse store-level counters for the admin/console read surface. Optional:
|
|
48
|
+
* a store that omits `stats()` simply cannot report them.
|
|
49
49
|
* All counts include expired-but-not-yet-evicted entries (the store's own
|
|
50
50
|
* bytes on disk), split by media type.
|
|
51
51
|
*/
|
|
@@ -80,7 +80,7 @@ export interface SegmentStore {
|
|
|
80
80
|
* rebuilding sqlite images while one exists (§5.3).
|
|
81
81
|
*/
|
|
82
82
|
find(key: SegmentFindKey, nowMs: number): Promise<SegmentRecord | undefined>;
|
|
83
|
-
/** Admin/console counters
|
|
83
|
+
/** Admin/console counters: ADDITIVE, optional. */
|
|
84
84
|
stats?(): Promise<SegmentStoreStats>;
|
|
85
85
|
}
|
|
86
86
|
|
package/src/signed-url.ts
CHANGED
|
@@ -198,8 +198,8 @@ export interface SegmentTokenClaims {
|
|
|
198
198
|
|
|
199
199
|
const encoder = new TextEncoder();
|
|
200
200
|
|
|
201
|
-
// Runtime-neutral base64url
|
|
202
|
-
//
|
|
201
|
+
// Runtime-neutral base64url: `btoa`/`atob` operate on binary strings and are
|
|
202
|
+
// present in every supported runtime.
|
|
203
203
|
function base64url(bytes: Uint8Array): string {
|
|
204
204
|
let binary = '';
|
|
205
205
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
package/src/sigv4.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
|
|
@@ -94,9 +94,7 @@ export function amzTimestamps(nowMs: number): {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** Sorted, fully re-encoded canonical query string from *decoded* pairs. */
|
|
97
|
-
|
|
98
|
-
params: Iterable<readonly [string, string]>,
|
|
99
|
-
): string {
|
|
97
|
+
function canonicalQuery(params: Iterable<readonly [string, string]>): string {
|
|
100
98
|
return [...params]
|
|
101
99
|
.map(([k, v]) => [uriEncode(k, true), uriEncode(v, true)] as const)
|
|
102
100
|
.sort(([ak, av], [bk, bv]) =>
|
package/src/sqlite-blob-store.ts
CHANGED
|
@@ -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/src/sqlite-dialect.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
|
*/
|
|
@@ -35,7 +34,7 @@ import type {
|
|
|
35
34
|
* The inverted scope index carries the ordered column (`commit_seq` /
|
|
36
35
|
* `row_id`) last in the PRIMARY KEY, so the candidate scan is an index
|
|
37
36
|
* range that returns already-ordered rows — the same covering-index shape
|
|
38
|
-
* the Postgres storage documents (§3.1,
|
|
37
|
+
* the Postgres storage documents (§3.1, performance-by-
|
|
39
38
|
* construction).
|
|
40
39
|
*/
|
|
41
40
|
export const SQLITE_DDL = `
|
package/src/sqlite-image.ts
CHANGED
|
@@ -62,7 +62,7 @@ export interface SqliteImageInput {
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* The §5.3 image-builder capability, injected through
|
|
65
|
-
* `SyncServerConfig.sqliteImageBuilder
|
|
65
|
+
* `SyncServerConfig.sqliteImageBuilder`. Building an image needs
|
|
66
66
|
* a real SQLite engine (`bun:sqlite` here), which is not available on every
|
|
67
67
|
* runtime — Cloudflare Workers has none. So the core takes the builder as an
|
|
68
68
|
* 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';
|
|
@@ -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 {
|
package/src/sqlite-storage.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.
|
|
@@ -396,8 +396,8 @@ export class SqliteServerStorage implements ServerStorage {
|
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
/**
|
|
399
|
-
* Migration rewrite
|
|
400
|
-
*
|
|
399
|
+
* Migration rewrite: keyset-paged walk of a row table. When `oldLayout` is
|
|
400
|
+
* given every payload re-encodes under
|
|
401
401
|
* the current columns, and the projection (when materialized) refreshes
|
|
402
402
|
* from the payload either way.
|
|
403
403
|
*/
|
|
@@ -808,7 +808,7 @@ export class SqliteServerStorage implements ServerStorage {
|
|
|
808
808
|
return rows.map((r) => r.blob_id);
|
|
809
809
|
}
|
|
810
810
|
|
|
811
|
-
// -- admin/console read surface
|
|
811
|
+
// -- admin/console read surface --------------------------------------------
|
|
812
812
|
|
|
813
813
|
async listClientRecords(partition: string): Promise<ClientRecord[]> {
|
|
814
814
|
const records = this.db
|
package/src/storage.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Requirements it encodes:
|
|
5
5
|
* - commit log with per-partition `commitSeq` (§2.1) and a commit→scope
|
|
6
|
-
* inverted index
|
|
6
|
+
* inverted index (§3.1): pulls filter
|
|
7
7
|
* by scope via the index, never by scanning the log;
|
|
8
8
|
* - idempotency results per §2.3 persisted in the same transaction as the
|
|
9
9
|
* commit's writes (§6.4);
|
|
@@ -276,7 +276,7 @@ export interface StorageTransaction {
|
|
|
276
276
|
export interface ServerStorage {
|
|
277
277
|
/**
|
|
278
278
|
* Create/migrate the relational per-app row tables for `schema`
|
|
279
|
-
*
|
|
279
|
+
* Idempotent and cheap when the
|
|
280
280
|
* stored schema version already matches (one marker read, memoized per
|
|
281
281
|
* instance); on first use it creates the tables, on a version bump it
|
|
282
282
|
* applies the migration subset (CREATE TABLE / ADD COLUMN / CREATE
|
|
@@ -373,7 +373,7 @@ export interface ServerStorage {
|
|
|
373
373
|
listReferencedBlobIds?(partition: string): Promise<string[]>;
|
|
374
374
|
|
|
375
375
|
/**
|
|
376
|
-
* Admin/console read surface (`SyncularAdmin
|
|
376
|
+
* Admin/console read surface (`SyncularAdmin`): ADDITIVE,
|
|
377
377
|
* optional. All read-only, partition-scoped, and JSON-able. A backend
|
|
378
378
|
* that omits these simply cannot serve the console; the sync path never
|
|
379
379
|
* calls them.
|