@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/src/realtime.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
type WakeReason,
|
|
32
32
|
} from '@syncular/core';
|
|
33
33
|
import type { SyncRequestContext, SyncServerConfig } from './context';
|
|
34
|
-
import { RESOLVER_OUTAGE } from './context';
|
|
34
|
+
import { REMOTE_COMMAND_CLIENT_ID_PREFIX, RESOLVER_OUTAGE } from './context';
|
|
35
35
|
import { SyncError, syncError } from './errors';
|
|
36
36
|
import { emitEvent, type SyncularServerEvents } from './events';
|
|
37
37
|
import { createSyncResponseStream } from './handler';
|
|
@@ -963,6 +963,12 @@ export class RealtimeHub {
|
|
|
963
963
|
async connect(options: RealtimeConnectOptions): Promise<RealtimeSession> {
|
|
964
964
|
const { storage } = this.#config;
|
|
965
965
|
const clock = this.#config.clock ?? Date.now;
|
|
966
|
+
if (options.clientId.startsWith(REMOTE_COMMAND_CLIENT_ID_PREFIX)) {
|
|
967
|
+
throw syncError(
|
|
968
|
+
'sync.invalid_client_id',
|
|
969
|
+
'clientId uses a reserved server-command namespace (§1.5)',
|
|
970
|
+
);
|
|
971
|
+
}
|
|
966
972
|
const record = await storage.getClientRecord(
|
|
967
973
|
options.partition,
|
|
968
974
|
options.clientId,
|
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 = `
|
|
@@ -75,6 +74,26 @@ CREATE TABLE IF NOT EXISTS sync_push_results(
|
|
|
75
74
|
client_commit_id TEXT NOT NULL, result TEXT NOT NULL,
|
|
76
75
|
PRIMARY KEY(partition, client_id, client_commit_id)
|
|
77
76
|
);
|
|
77
|
+
CREATE TABLE IF NOT EXISTS sync_reactions(
|
|
78
|
+
partition TEXT NOT NULL, idempotency_key TEXT NOT NULL,
|
|
79
|
+
type TEXT NOT NULL, version INTEGER NOT NULL, payload TEXT NOT NULL,
|
|
80
|
+
source_client_id TEXT NOT NULL, source_client_commit_id TEXT NOT NULL,
|
|
81
|
+
source_commit_seq INTEGER NOT NULL, created_at_ms INTEGER NOT NULL,
|
|
82
|
+
available_at_ms INTEGER NOT NULL, status TEXT NOT NULL,
|
|
83
|
+
attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,
|
|
84
|
+
lease_owner TEXT, lease_expires_at_ms INTEGER, completed_at_ms INTEGER,
|
|
85
|
+
last_failure TEXT,
|
|
86
|
+
PRIMARY KEY(partition, idempotency_key),
|
|
87
|
+
CHECK(status IN ('pending', 'leased', 'completed', 'dead-letter'))
|
|
88
|
+
);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS sync_reactions_due
|
|
90
|
+
ON sync_reactions(partition, status, available_at_ms, created_at_ms, idempotency_key);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS sync_reactions_lease
|
|
92
|
+
ON sync_reactions(partition, status, lease_expires_at_ms);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS sync_reactions_completed
|
|
94
|
+
ON sync_reactions(partition, status, completed_at_ms, idempotency_key);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS sync_reactions_dead_letter
|
|
96
|
+
ON sync_reactions(partition, status, available_at_ms, idempotency_key);
|
|
78
97
|
CREATE TABLE IF NOT EXISTS sync_clients(
|
|
79
98
|
partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,
|
|
80
99
|
cursor INTEGER NOT NULL, subscriptions TEXT NOT NULL,
|
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 {
|