@syncular/server 0.16.1 → 0.17.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 CHANGED
@@ -110,6 +110,11 @@ Log the cause for operators and stop startup. Do not catch readiness errors in
110
110
  authentication or convert them to a 401; request-time schema checks are only a
111
111
  defensive fallback.
112
112
 
113
+ For D1, finish `storage.migrateSchema(compileSchema(schema))` across separate
114
+ Worker invocations before admitting traffic. Each call returns `complete` and
115
+ `statementsExecuted`; the default budget is 50 statements. See
116
+ [D1 schema migration](https://syncular.dev/server-workers/#schema-migration).
117
+
113
118
  After restoring an authoritative database, keep traffic stopped and call
114
119
  `rotatePartitionLogEpoch({ storage, partition })` for every restored
115
120
  partition. The rotation clears stale client cursors and requires version 2
@@ -1111,13 +1116,19 @@ cannot silently return.
1111
1116
  ### commitSeq allocation under concurrency
1112
1117
 
1113
1118
  Per-partition `commitSeq` is dense and gap-free (§2.1). `appendCommit`
1114
- allocates it with `UPDATE sync_partitions SET max_commit_seq =
1115
- max_commit_seq + 1 RETURNING`, which takes a row-level write lock on the
1116
- partition row for the transaction's duration concurrent pushes to the
1117
- same partition serialize on that row; cross-partition pushes never
1118
- contend. A Postgres `SEQUENCE` is deliberately **not** used: it would leave
1119
+ allocates it with an upsert that increments `sync_partitions.max_commit_seq`
1120
+ and returns the allocated value. A common table expression feeds that value
1121
+ into the commit metadata insert in the same SQL statement. The upsert takes a
1122
+ row-level write lock on the partition for the transaction duration. Concurrent
1123
+ pushes to that partition serialize; separate partitions use separate locks. A Postgres `SEQUENCE` is deliberately **not** used: it would leave
1119
1124
  gaps on rollback, which the §4.5 pull-window arithmetic does not tolerate.
1120
1125
 
1126
+ Each change insert expands its scope object into the inverted scope entries in
1127
+ the same statement. Serialized scopes bind as text before JSONB parsing; this
1128
+ avoids driver-specific JSON string encoding. Historical string-form scopes
1129
+ remain readable. Empty scopes still produce a change row, and repeated scopes
1130
+ within a commit retain one index entry.
1131
+
1121
1132
  ### Multi-instance fanout (LISTEN/NOTIFY)
1122
1133
 
1123
1134
  Behind a load balancer, a commit applied on instance A fans out to A's
@@ -1186,3 +1197,12 @@ identity after authorization. Sharing is local to one process. Signed URL
1186
1197
  grants remain per request. The first eligibility probe has at most
1187
1198
  `limitSnapshotRows + 1` rows; subsequent builder batches have at most 5,000
1188
1199
  rows. The image database and serialized output still consume memory.
1200
+
1201
+
1202
+ Realtime acknowledgements call `advanceClientCursor(partition, clientId,
1203
+ actorId, logEpoch, cursor, updatedAtMs)`. Custom storage adapters must implement
1204
+ this atomic update: advance the cursor and activity timestamp to their respective
1205
+ maxima, preserve registration fields, and require a matching actor and current
1206
+ partition log epoch. Leave missing records unchanged. SQLite, Postgres, and D1
1207
+ perform one update without reading or serializing the subscription list. HTTP
1208
+ registration keeps its existing cursor and subscription replacement rules.
@@ -37,6 +37,13 @@ export declare class D1ServerStorage implements ServerStorage {
37
37
  /** Resolve a table's compiled schema; row operations require `ensureSchema`. */
38
38
  table(name: string): CompiledTable;
39
39
  ensureSchema(schema: CompiledSchema): Promise<void>;
40
+ /** Run once per Worker invocation. Resume an incomplete result in another invocation. */
41
+ migrateSchema(schema: CompiledSchema, options?: {
42
+ readonly maxStatements?: number;
43
+ }): Promise<{
44
+ readonly complete: boolean;
45
+ readonly statementsExecuted: number;
46
+ }>;
40
47
  touchPartition(partition: string, authenticatedAtMs: number, initialLogEpoch: string): Promise<PartitionRegistryEntry>;
41
48
  rotatePartitionLogEpoch(partition: string, logEpoch: string, authenticatedAtMs: number): Promise<PartitionRegistryEntry>;
42
49
  listPartitionRegistry(): Promise<PartitionRegistryEntry[]>;
@@ -63,6 +70,8 @@ export declare class D1ServerStorage implements ServerStorage {
63
70
  scanRowsByIndex(partition: string, query: IndexRowScanQuery): Promise<StoredRow[]>;
64
71
  getClientRecord(partition: string, clientId: string): Promise<ClientRecord | undefined>;
65
72
  putClientRecord(partition: string, record: ClientRecord): Promise<void>;
73
+ advanceClientCursor(partition: string, clientId: string, actorId: string, logEpoch: string, cursor: number, updatedAtMs: number): Promise<void>;
74
+ updateClientCursor(partition: string, clientId: string, cursor: number, updatedAtMs: number): Promise<void>;
66
75
  getActiveClientCursorFloor(partition: string, cutoffMs: number): Promise<number | null>;
67
76
  listClientCursors(partition: string): Promise<ClientCursorInfo[]>;
68
77
  listRowsReferencingBlob(partition: string, blobId: string): Promise<{