@syncular/server 0.21.1 → 0.23.0
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 +20 -6
- package/dist/admin.d.ts +3 -1
- package/dist/admin.js +7 -1
- package/dist/blob-handlers.js +1 -1
- package/dist/context.d.ts +8 -1
- package/dist/d1-storage.d.ts +11 -3
- package/dist/d1-storage.js +84 -2
- package/dist/errors.js +10 -0
- package/dist/handler.js +50 -4
- package/dist/operations.js +3 -3
- package/dist/pg-executor.d.ts +11 -1
- package/dist/postgres-storage.d.ts +20 -5
- package/dist/postgres-storage.js +345 -9
- package/dist/pull.js +9 -5
- package/dist/push.js +18 -0
- package/dist/readiness.d.ts +9 -2
- package/dist/readiness.js +27 -2
- package/dist/realtime.d.ts +40 -1
- package/dist/realtime.js +116 -19
- package/dist/relational-rows.d.ts +20 -0
- package/dist/relational-rows.js +29 -0
- package/dist/s3-segment-store.d.ts +20 -8
- package/dist/s3-segment-store.js +216 -68
- package/dist/schema.js +11 -0
- package/dist/segment-download.d.ts +1 -1
- package/dist/segment-download.js +50 -17
- package/dist/segment-store.d.ts +86 -0
- package/dist/segment-store.js +156 -17
- package/dist/signed-url.d.ts +7 -1
- package/dist/signed-url.js +10 -1
- package/dist/sqlite-dialect.d.ts +7 -1
- package/dist/sqlite-dialect.js +23 -0
- package/dist/sqlite-segment-store.js +153 -42
- package/dist/sqlite-storage.d.ts +11 -3
- package/dist/sqlite-storage.js +346 -34
- package/dist/storage-errors.d.ts +1 -1
- package/dist/storage-errors.js +4 -0
- package/dist/storage.d.ts +152 -2
- package/dist/storage.js +40 -1
- package/dist/validate.d.ts +20 -1
- package/package.json +2 -2
- package/src/admin.ts +13 -1
- package/src/blob-handlers.ts +1 -1
- package/src/context.ts +8 -0
- package/src/d1-storage.ts +143 -1
- package/src/errors.ts +10 -0
- package/src/handler.ts +62 -4
- package/src/operations.ts +17 -13
- package/src/pg-executor.ts +11 -1
- package/src/postgres-storage.ts +520 -5
- package/src/pull.ts +9 -1
- package/src/push.ts +18 -0
- package/src/readiness.ts +39 -4
- package/src/realtime.ts +117 -18
- package/src/relational-rows.ts +36 -0
- package/src/s3-segment-store.ts +275 -84
- package/src/schema.ts +15 -0
- package/src/segment-download.ts +79 -28
- package/src/segment-store.ts +250 -18
- package/src/signed-url.ts +12 -2
- package/src/sqlite-dialect.ts +23 -0
- package/src/sqlite-segment-store.ts +245 -65
- package/src/sqlite-storage.ts +494 -36
- package/src/storage-errors.ts +13 -1
- package/src/storage.ts +243 -1
- package/src/validate.ts +20 -1
package/src/storage.ts
CHANGED
|
@@ -31,6 +31,120 @@ export interface CommitPruneResult {
|
|
|
31
31
|
readonly removedCommits: number;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** RFC 0007 backfill lifecycle. `activated` is terminal within a schema version. */
|
|
35
|
+
export type CheckpointState = 'declared' | 'backfilling' | 'activated';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Host-owned checkpoint installation carried on `ensureSchema`: the schema
|
|
39
|
+
* bump, the `declared` row, and the raised writer fence commit together.
|
|
40
|
+
*/
|
|
41
|
+
export interface CheckpointDeclaration {
|
|
42
|
+
readonly partition: string;
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly schemaVersion: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One host-storage backfill checkpoint row. Never a synced application row. */
|
|
48
|
+
export interface StoredCheckpoint {
|
|
49
|
+
readonly partition: string;
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly schemaVersion: number;
|
|
52
|
+
readonly state: CheckpointState;
|
|
53
|
+
/** Highest source `commit_seq` the projection was derived from. */
|
|
54
|
+
readonly watermark: number;
|
|
55
|
+
/** Monotonic claim counter; a batch is stale when it does not match. */
|
|
56
|
+
readonly ownerEpoch: number;
|
|
57
|
+
readonly observedRows: number;
|
|
58
|
+
readonly updatedAtMs: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* RFC 0007 serve-gate token, read inside the transaction that produces the
|
|
63
|
+
* result. `storedSchemaVersion` is the published marker; `logEpoch` is the
|
|
64
|
+
* partition's log generation, so a restore that presents V -> other -> V is
|
|
65
|
+
* still distinguishable. `pending` names every incomplete declared checkpoint
|
|
66
|
+
* at the running schema version, across partitions.
|
|
67
|
+
*/
|
|
68
|
+
export interface ServeGate {
|
|
69
|
+
readonly storedSchemaVersion: number;
|
|
70
|
+
readonly logEpoch: string | undefined;
|
|
71
|
+
readonly pending: readonly {
|
|
72
|
+
readonly partition: string;
|
|
73
|
+
readonly name: string;
|
|
74
|
+
}[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Why `serveGate` refuses, or `undefined` when this request may serve. */
|
|
78
|
+
export type ServeGateRefusal =
|
|
79
|
+
| {
|
|
80
|
+
readonly kind: 'schema';
|
|
81
|
+
readonly stored: number;
|
|
82
|
+
readonly running: number;
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
readonly kind: 'checkpoint';
|
|
86
|
+
readonly partition: string;
|
|
87
|
+
readonly name: string;
|
|
88
|
+
}
|
|
89
|
+
| {
|
|
90
|
+
/** The token changed between two reads: a restore rotated the epoch. */
|
|
91
|
+
readonly kind: 'epoch';
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Evaluate the RFC 0007 predicate against a gate token. A process that
|
|
96
|
+
* declares the pending checkpoint may serve while it backfills; any other
|
|
97
|
+
* process is refused, and a database newer than the running build is always
|
|
98
|
+
* refused.
|
|
99
|
+
*/
|
|
100
|
+
export function serveGateRefusal(
|
|
101
|
+
gate: ServeGate,
|
|
102
|
+
runningSchemaVersion: number,
|
|
103
|
+
checkpoints: readonly CheckpointDeclaration[] | undefined,
|
|
104
|
+
): ServeGateRefusal | undefined {
|
|
105
|
+
if (gate.storedSchemaVersion > runningSchemaVersion) {
|
|
106
|
+
return {
|
|
107
|
+
kind: 'schema',
|
|
108
|
+
stored: gate.storedSchemaVersion,
|
|
109
|
+
running: runningSchemaVersion,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
for (const pending of gate.pending) {
|
|
113
|
+
const declared = (checkpoints ?? []).some(
|
|
114
|
+
(declaration) =>
|
|
115
|
+
declaration.partition === pending.partition &&
|
|
116
|
+
declaration.name === pending.name,
|
|
117
|
+
);
|
|
118
|
+
if (!declared) {
|
|
119
|
+
return {
|
|
120
|
+
kind: 'checkpoint',
|
|
121
|
+
partition: pending.partition,
|
|
122
|
+
name: pending.name,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* RFC 0007: compare two gate tokens read around a data read. The token binds
|
|
131
|
+
* the published schema version and the partition log epoch, so a restore that
|
|
132
|
+
* presents V → other → V is distinguishable. Returns a refusal when either
|
|
133
|
+
* changed across the read.
|
|
134
|
+
*/
|
|
135
|
+
export function serveGateTokenChanged(
|
|
136
|
+
before: ServeGate,
|
|
137
|
+
after: ServeGate,
|
|
138
|
+
): ServeGateRefusal | undefined {
|
|
139
|
+
if (
|
|
140
|
+
before.storedSchemaVersion !== after.storedSchemaVersion ||
|
|
141
|
+
before.logEpoch !== after.logEpoch
|
|
142
|
+
) {
|
|
143
|
+
return { kind: 'epoch' };
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
34
148
|
/** The current stored state of a synced row. */
|
|
35
149
|
export interface StoredRow {
|
|
36
150
|
readonly rowId: string;
|
|
@@ -406,6 +520,27 @@ export interface StorageTransaction {
|
|
|
406
520
|
clientCommitId: string,
|
|
407
521
|
result: StoredPushResult,
|
|
408
522
|
): Promise<void>;
|
|
523
|
+
/**
|
|
524
|
+
* Advance this partition's checkpoint only while `owner_epoch` still
|
|
525
|
+
* matches, so the owner-epoch CAS rides inside the caller's transaction.
|
|
526
|
+
* The host calls this from `processPushOperationsWithTrace`'s operation
|
|
527
|
+
* builder: the backfill is an ordinary server-authoritative write, and this
|
|
528
|
+
* CAS commits with that write's row versions and commit-log append. False
|
|
529
|
+
* means abort and write nothing, rather than a throw for a superseded owner.
|
|
530
|
+
*/
|
|
531
|
+
advanceCheckpoint(
|
|
532
|
+
name: string,
|
|
533
|
+
ownerEpoch: number,
|
|
534
|
+
watermark: number,
|
|
535
|
+
observedRows: number,
|
|
536
|
+
nowMs: number,
|
|
537
|
+
): Promise<boolean>;
|
|
538
|
+
/**
|
|
539
|
+
* RFC 0007 serve gate on the caller's own connection, for write paths that
|
|
540
|
+
* already hold the partition lock: the gate and the write share one
|
|
541
|
+
* transaction, so a migration cannot interleave between them.
|
|
542
|
+
*/
|
|
543
|
+
readServeGate(runningSchemaVersion: number): Promise<ServeGate>;
|
|
409
544
|
/**
|
|
410
545
|
* Blob reference index (§5.9.4) — ADDITIVE, optional. Set the blobIds a
|
|
411
546
|
* row currently references (empty = clear), replacing any prior entries
|
|
@@ -438,7 +573,10 @@ export interface ServerStorage {
|
|
|
438
573
|
* before binding a public port; protocol handlers still call this method
|
|
439
574
|
* lazily as a defensive backstop.
|
|
440
575
|
*/
|
|
441
|
-
ensureSchema(
|
|
576
|
+
ensureSchema(
|
|
577
|
+
schema: CompiledSchema,
|
|
578
|
+
checkpoints?: readonly CheckpointDeclaration[],
|
|
579
|
+
): Promise<void>;
|
|
442
580
|
|
|
443
581
|
/** Create or refresh the authenticated partition registry row (§2.1). */
|
|
444
582
|
touchPartition(
|
|
@@ -458,6 +596,19 @@ export interface ServerStorage {
|
|
|
458
596
|
/** Read continuity without refreshing authenticated activity. */
|
|
459
597
|
getPartitionLogEpoch(partition: string): Promise<string | undefined>;
|
|
460
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Open a storage transaction. This is the raw primitive, not a command
|
|
601
|
+
* boundary: it acquires NO partition write lock. In-tree SQLite begins
|
|
602
|
+
* `IMMEDIATE`, so a hand-rolled command on SQLite happens to be serialized
|
|
603
|
+
* against a concurrent push; PostgreSQL's `BEGIN` takes no row lock, so the
|
|
604
|
+
* same command races a push on Postgres and a SQLite-only test conceals it.
|
|
605
|
+
*
|
|
606
|
+
* A trusted command must therefore run through `registerRemoteCommand`,
|
|
607
|
+
* which builds after `lockPartitionForPush` and the idempotency recheck and
|
|
608
|
+
* shares the committed transaction, or call `lockPartitionForPush` before
|
|
609
|
+
* its first authority, version, or replay read. SSP2 request handling does
|
|
610
|
+
* this for you; only host-authored commands choose.
|
|
611
|
+
*/
|
|
461
612
|
begin(partition: string): Promise<StorageTransaction>;
|
|
462
613
|
|
|
463
614
|
getMaxCommitSeq(partition: string): Promise<number>;
|
|
@@ -478,6 +629,84 @@ export interface ServerStorage {
|
|
|
478
629
|
createdBeforeMs: number,
|
|
479
630
|
): Promise<number>;
|
|
480
631
|
|
|
632
|
+
/** Every declared backfill checkpoint for the partition, ordered by name. */
|
|
633
|
+
readCheckpoints(partition: string): Promise<StoredCheckpoint[]>;
|
|
634
|
+
/**
|
|
635
|
+
* RFC 0007 serve gate: read the published schema version, the partition's
|
|
636
|
+
* log epoch, and every incomplete declared checkpoint at
|
|
637
|
+
* `runningSchemaVersion`. Never memoized; the serve paths call it per
|
|
638
|
+
* request so an already-serving process notices a migration.
|
|
639
|
+
*/
|
|
640
|
+
readServeGate(
|
|
641
|
+
partition: string,
|
|
642
|
+
runningSchemaVersion: number,
|
|
643
|
+
): Promise<ServeGate>;
|
|
644
|
+
/**
|
|
645
|
+
* Atomically take ownership of a declared or backfilling checkpoint. Increments
|
|
646
|
+
* `owner_epoch` and moves `declared` to `backfilling`. Throws
|
|
647
|
+
* `sync.storage.checkpoint_not_declared` when no claimable row exists (absent,
|
|
648
|
+
* or already `activated`).
|
|
649
|
+
*/
|
|
650
|
+
claimCheckpoint(
|
|
651
|
+
partition: string,
|
|
652
|
+
name: string,
|
|
653
|
+
schemaVersion: number,
|
|
654
|
+
nowMs: number,
|
|
655
|
+
): Promise<StoredCheckpoint>;
|
|
656
|
+
/**
|
|
657
|
+
* Advance the watermark only while `owner_epoch` still matches. Returns false
|
|
658
|
+
* for a superseded owner; the stale case never throws.
|
|
659
|
+
*/
|
|
660
|
+
advanceCheckpoint(
|
|
661
|
+
partition: string,
|
|
662
|
+
name: string,
|
|
663
|
+
ownerEpoch: number,
|
|
664
|
+
watermark: number,
|
|
665
|
+
observedRows: number,
|
|
666
|
+
nowMs: number,
|
|
667
|
+
): Promise<boolean>;
|
|
668
|
+
/** Highest commitSeq carrying a change to one of `tables`; 0 when none. */
|
|
669
|
+
sourceCoverageSeq(
|
|
670
|
+
partition: string,
|
|
671
|
+
tables: readonly string[],
|
|
672
|
+
): Promise<number>;
|
|
673
|
+
/**
|
|
674
|
+
* Whether any source change sits above `seq`. `unverifiable` when the pruning
|
|
675
|
+
* horizon has passed `seq`, so the history that would answer the question is
|
|
676
|
+
* gone; a pruned window is never reported as `clean`.
|
|
677
|
+
*/
|
|
678
|
+
hasSourceChangesAbove(
|
|
679
|
+
partition: string,
|
|
680
|
+
tables: readonly string[],
|
|
681
|
+
seq: number,
|
|
682
|
+
): Promise<'clean' | 'changed' | 'unverifiable'>;
|
|
683
|
+
/**
|
|
684
|
+
* Install the barrier: insert the checkpoint in `declared` and raise
|
|
685
|
+
* `required_writer_version` for the partition in one transaction. The fence
|
|
686
|
+
* is raised at declaration, before any backfill work, so old writers are
|
|
687
|
+
* rejected for the whole backfill window. Idempotent on an existing declared
|
|
688
|
+
* or backfilling row.
|
|
689
|
+
*/
|
|
690
|
+
declareCheckpoint(
|
|
691
|
+
partition: string,
|
|
692
|
+
name: string,
|
|
693
|
+
schemaVersion: number,
|
|
694
|
+
nowMs: number,
|
|
695
|
+
): Promise<StoredCheckpoint>;
|
|
696
|
+
/**
|
|
697
|
+
* Activate a declared checkpoint under the partition write lock. Refuses when
|
|
698
|
+
* the fence is not already raised at the checkpoint's schema version, when a
|
|
699
|
+
* source change sits above `watermark` (`stale`), or when the pruning horizon
|
|
700
|
+
* has passed `watermark` (`unverifiable`). `activated` is terminal.
|
|
701
|
+
*/
|
|
702
|
+
activateCheckpoint(
|
|
703
|
+
partition: string,
|
|
704
|
+
name: string,
|
|
705
|
+
ownerEpoch: number,
|
|
706
|
+
watermark: number,
|
|
707
|
+
sources: readonly string[],
|
|
708
|
+
nowMs: number,
|
|
709
|
+
): Promise<'activated' | 'stale' | 'unverifiable'>;
|
|
481
710
|
getRow(
|
|
482
711
|
partition: string,
|
|
483
712
|
table: string,
|
|
@@ -560,10 +789,23 @@ export interface ServerStorage {
|
|
|
560
789
|
* Optional registered-query capability. The implementation MUST replace
|
|
561
790
|
* every generated app-table relation with a partition-filtered relation and
|
|
562
791
|
* return rows plus maxCommitSeq from one consistent database snapshot.
|
|
792
|
+
*
|
|
793
|
+
* The storage serializes this call behind an open transaction on a
|
|
794
|
+
* single-connection executor (SQLite, PGlite). A validator runs inside the
|
|
795
|
+
* push transaction, so on that executor a validator that calls
|
|
796
|
+
* `queryAuthoritative` waits on the transaction it is already inside and
|
|
797
|
+
* the push never completes. A pool-backed executor (`pg`, `Bun.sql`) hands
|
|
798
|
+
* the call a second connection instead: the validator does not deadlock,
|
|
799
|
+
* but it reads committed state without the candidate write. On either
|
|
800
|
+
* executor read candidate state through the whole-commit `CommitValidator`
|
|
801
|
+
* reader (§6.8). A call made from any other context waits for the open
|
|
802
|
+
* transaction to finish on a single-connection executor, and runs
|
|
803
|
+
* concurrently on a pool-backed one.
|
|
563
804
|
*/
|
|
564
805
|
queryAuthoritative?(
|
|
565
806
|
partition: string,
|
|
566
807
|
query: AuthoritativeQueryRequest,
|
|
808
|
+
checkpoints?: readonly CheckpointDeclaration[],
|
|
567
809
|
): Promise<AuthoritativeQueryResult>;
|
|
568
810
|
|
|
569
811
|
/**
|
package/src/validate.ts
CHANGED
|
@@ -123,7 +123,19 @@ export type CommitValidator = (
|
|
|
123
123
|
input: ValidateCommitInput,
|
|
124
124
|
) => void | Promise<void>;
|
|
125
125
|
|
|
126
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Ambient context a validator may consult (§6.7).
|
|
128
|
+
*
|
|
129
|
+
* A validator runs inside the push transaction. On a single-connection
|
|
130
|
+
* executor (SQLite, PGlite) the storage serializes authoritative queries
|
|
131
|
+
* behind that transaction, so a validator that calls `queryAuthoritative`
|
|
132
|
+
* waits for the transaction it is already inside and the push never
|
|
133
|
+
* completes. On a pool-backed executor (`pg`, `Bun.sql`) the query takes a
|
|
134
|
+
* second connection and returns committed state without the candidate write.
|
|
135
|
+
* Either way a rule that must read rows belongs in the whole-commit
|
|
136
|
+
* `CommitValidator`, whose reader runs get and scan operations on the same
|
|
137
|
+
* transaction (§6.8).
|
|
138
|
+
*/
|
|
127
139
|
export interface ValidateContext {
|
|
128
140
|
/** Host-authenticated actor (§1.1) performing the write. */
|
|
129
141
|
readonly actorId: string;
|
|
@@ -139,6 +151,13 @@ export interface ValidateContext {
|
|
|
139
151
|
* any other error is treated as a rejection with a generic
|
|
140
152
|
* `sync.constraint_violation` code (a validator SHOULD throw
|
|
141
153
|
* `ValidationRejection` to control the code). MUST NOT mutate the row.
|
|
154
|
+
*
|
|
155
|
+
* The hook runs inside the push transaction and cannot read candidate rows
|
|
156
|
+
* from an authoritative query: on a single-connection executor
|
|
157
|
+
* `queryAuthoritative` serializes behind that transaction and the push never
|
|
158
|
+
* completes, and on a pool-backed executor it reads committed state without
|
|
159
|
+
* the candidate write. Read rows with the whole-commit `CommitValidator`
|
|
160
|
+
* reader instead (§6.8).
|
|
142
161
|
*/
|
|
143
162
|
export type Validator = (
|
|
144
163
|
op: ValidateOperation,
|