@korajs/server 1.0.0-beta.4 → 1.0.0-beta.6

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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { SchemaDefinition, Operation, BlobRef, KoraEventEmitter, VersionVector } from '@korajs/core';
1
+ import { Operation, HybridLogicalClock, SchemaDefinition, BlobRef, KoraEventEmitter, VersionVector, TimeSource } from '@korajs/core';
2
2
  import * as _korajs_sync from '@korajs/sync';
3
3
  import { SyncStore, ApplyResult, MessageSerializer, SyncMessage, AwarenessUpdateMessage, YjsDocUpdateMessage, BlobChunkRequestMessage, BlobChunkResponseMessage } from '@korajs/sync';
4
4
  import { S as ServerTransport, a as ServerMessageHandler, b as ServerCloseHandler, c as ServerErrorHandler } from './server-transport-CU1BLWgr.cjs';
@@ -44,6 +44,70 @@ interface MaterializedRecord {
44
44
  id: string;
45
45
  [key: string]: unknown;
46
46
  }
47
+ /**
48
+ * A conditional, atomic multi-operation apply. The store reads the target under a
49
+ * cross-instance lock, lets the caller decide admission and build the operations
50
+ * against that locked state, and applies them in one transaction, or applies
51
+ * nothing. `admit` and `buildOperations` run inside the locked transaction so the
52
+ * predicate check and any atomic-op resolution see the authoritative current value
53
+ * even when multiple server instances race on the same target.
54
+ */
55
+ interface ConditionalApplyInput {
56
+ /** Record the admission lock and predicate are keyed on. */
57
+ target: {
58
+ collection: string;
59
+ id: string;
60
+ };
61
+ /** Decides admission against the target's locked current state. */
62
+ admit: (current: MaterializedRecord | null) => boolean;
63
+ /**
64
+ * Builds the operations to apply, given the locked current state and a clock the
65
+ * store has already advanced past the target record's latest committed operation.
66
+ * Stamping the built operations with this clock guarantees they sort strictly
67
+ * after every prior write to the target, so last-write-wins materialization
68
+ * reflects the serialized commit order even when two instances commit in the same
69
+ * millisecond. Without it, a same-millisecond tie could let materialization pick
70
+ * an earlier resolved value and undercount an atomic counter, admitting past a cap.
71
+ */
72
+ buildOperations: (current: MaterializedRecord | null, context: ConditionalApplyContext) => Promise<Operation[]>;
73
+ /**
74
+ * When set, a record whose prior existence proves this set already committed.
75
+ * Checked under the same lock as admission, so a retry is idempotent even across
76
+ * instances: it returns `idempotent: true` without re-running the operations.
77
+ */
78
+ idempotencyKey?: {
79
+ collection: string;
80
+ id: string;
81
+ };
82
+ }
83
+ /** Context passed to {@link ConditionalApplyInput.buildOperations}. */
84
+ interface ConditionalApplyContext {
85
+ /**
86
+ * A clock the store has advanced past the target record's latest committed
87
+ * operation. Operations built here must be stamped with it (route context threads
88
+ * it into operation creation) so they sort strictly after every prior write.
89
+ */
90
+ clock: HybridLogicalClock;
91
+ }
92
+ /** Result of {@link ServerStore.applyConditional}. */
93
+ interface ConditionalApplyResult {
94
+ /** True when the predicate held (or the set was already committed). */
95
+ admitted: boolean;
96
+ /** True when the idempotency key already existed and nothing was re-applied. */
97
+ idempotent: boolean;
98
+ /** The operations that were applied (empty when not admitted or idempotent). */
99
+ applied: Operation[];
100
+ }
101
+ /**
102
+ * A stored operation paired with its server-assigned delivery sequence. The
103
+ * delivery sequence is monotonic in commit order, so a batch of these can be
104
+ * chained (each batch's base links to the previous batch's max) to give the
105
+ * client a gap-free, scope-agnostic recovery stream.
106
+ */
107
+ interface DeliveredOperation {
108
+ operation: Operation;
109
+ deliverySequence: number;
110
+ }
47
111
  /**
48
112
  * Options for querying a materialized collection table.
49
113
  */
@@ -70,6 +134,41 @@ interface ServerStore extends SyncStore {
70
134
  close(): Promise<void>;
71
135
  /** Get the total number of stored operations */
72
136
  getOperationCount(): Promise<number>;
137
+ /**
138
+ * The highest delivery sequence currently visible in the store (0 when empty).
139
+ * Used to initialize a fresh client's watermark baseline and for diagnostics.
140
+ */
141
+ getMaxDeliverySequence(): Promise<number>;
142
+ /**
143
+ * Operations with `deliverySequence > afterDeliverySequence`, ordered ascending
144
+ * by delivery sequence, at most `limit` of them. This is the substrate for the
145
+ * gap-free server->client stream: the caller (a client session) applies its own
146
+ * scope filter and chains the results into batches, advancing the client's
147
+ * durable watermark as batches are acknowledged. Because delivery sequence is
148
+ * assigned in commit order, a lower sequence is always visible before any higher
149
+ * one, so this scan can never skip an operation that later appears below the
150
+ * cursor.
151
+ */
152
+ getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
153
+ /**
154
+ * Conditionally apply a set of operations atomically, serialized across server
155
+ * instances on the target record. Optional: stores that can only run in a single
156
+ * process (or do not back a shared database) may omit it, and callers fall back
157
+ * to per-instance serialization. The Postgres store implements it with a
158
+ * transaction-scoped advisory lock so concurrent admissions cannot both pass a
159
+ * cap check (`applyConditional` returns `admitted: false` for the loser).
160
+ */
161
+ applyConditional?(input: ConditionalApplyInput): Promise<ConditionalApplyResult>;
162
+ /**
163
+ * Atomically reserve the next sequence number for a server-originated operation
164
+ * on this store's own node. Stores that serve conditional applies concurrently
165
+ * (the Postgres store, which does not serialize behind a single mutation tail)
166
+ * must implement this so two in-flight server operations cannot be handed the
167
+ * same sequence number, which would let one shadow the other during version-vector
168
+ * delta sync. Stores whose server writes are fully serialized may omit it, and
169
+ * callers fall back to reading the version vector.
170
+ */
171
+ reserveSequenceNumber?(): number;
73
172
  /**
74
173
  * Set the schema for materialized collection tables.
75
174
  * Creates collection tables and indexes based on the schema definition.
@@ -369,7 +468,37 @@ declare class KoraSyncServer {
369
468
  private readonly serverVersion;
370
469
  private wsServer;
371
470
  private running;
471
+ /**
472
+ * Periodic tick that retransmits relay batches connected clients have not
473
+ * acknowledged, so a relay dropped by a lossy transport is redelivered instead of
474
+ * leaving that client with a permanent version-vector gap (a lost operation).
475
+ */
476
+ private relayRetransmitTimer;
477
+ /**
478
+ * Relay operations a client never acknowledged before it disconnected, buffered by
479
+ * client node id so they can be redelivered on its next connection. Bounded per
480
+ * node and expired by age so a client that never returns cannot grow this forever.
481
+ */
482
+ private readonly orphanedRelaysByNode;
483
+ private static readonly MAX_ORPHANED_RELAY_OPS_PER_NODE;
484
+ private static readonly ORPHANED_RELAY_TTL_MS;
372
485
  constructor(config: KoraSyncServerConfig);
486
+ /**
487
+ * Retransmit relay batches that connected clients have not acknowledged within
488
+ * `staleMs`. Called on a periodic tick and directly by tests. Redelivering an
489
+ * already-applied operation is harmless (clients dedup by content-addressed id).
490
+ */
491
+ retransmitPendingRelays(staleMs?: number): void;
492
+ /**
493
+ * Buffer relay operations a disconnected client never acknowledged, keyed by its
494
+ * node id, deduped by operation id and bounded by count. Redelivered when the
495
+ * client reconnects (see {@link takeOrphanedRelays}).
496
+ */
497
+ private bufferOrphanedRelays;
498
+ /** Remove and return a node's buffered orphaned relay operations, if any. */
499
+ private takeOrphanedRelays;
500
+ /** Drop buffered orphaned relays older than the TTL (client never returned). */
501
+ private expireOrphanedRelays;
373
502
  /**
374
503
  * The trusted, scoped data-plane context (`apply` / `query` / `findById`).
375
504
  * Shared with the production server so HTTP routes, the operation validator,
@@ -432,6 +561,15 @@ declare class KoraSyncServer {
432
561
  * @returns The apply result, including any server-generated side-effect ops
433
562
  */
434
563
  applyLocalOperation(op: Operation): Promise<ApplyServerOperationResult>;
564
+ /**
565
+ * Relay already-applied, server-originated operations to every connected client.
566
+ * Used by the conditional route apply, which commits its operations atomically
567
+ * through the store (bypassing {@link applyLocalOperation}) and then fans them
568
+ * out. Each session still enforces its own per-scope visibility filter.
569
+ *
570
+ * @param operations - Operations that have already been committed to the store
571
+ */
572
+ relayServerOperations(operations: Operation[]): void;
435
573
  /**
436
574
  * Collect every blob reference still reachable from live records on the server.
437
575
  *
@@ -913,6 +1051,8 @@ interface ClientSessionOptions {
913
1051
  serializer?: MessageSerializer;
914
1052
  /** Event emitter for DevTools integration */
915
1053
  emitter?: KoraEventEmitter;
1054
+ /** Structured logger used for protocol and persistence failures */
1055
+ logger?: Logger;
916
1056
  /** Max operations per sync batch */
917
1057
  batchSize?: number;
918
1058
  /** Schema version the server expects */
@@ -936,6 +1076,19 @@ interface ClientSessionOptions {
936
1076
  persistBlobChunk?: PersistBlobChunk;
937
1077
  /** Called when this session closes */
938
1078
  onClose?: (sessionId: string) => void;
1079
+ /**
1080
+ * Called on close with relay operations this client never acknowledged, so the
1081
+ * server can buffer them per node id and redeliver on the client's next
1082
+ * connection. Closes the window where a relay dropped just before a reconnect
1083
+ * would otherwise be lost (the per-session retransmit tick never fires in time).
1084
+ */
1085
+ onOrphanedRelays?: (nodeId: string, ops: Operation[]) => void;
1086
+ /**
1087
+ * Called once this session reaches streaming to pull any relay operations buffered
1088
+ * for its node id while it was disconnected. They are re-sent through the normal
1089
+ * relay path (re-filtered by this session's current scope).
1090
+ */
1091
+ takeOrphanedRelays?: (nodeId: string) => Operation[];
939
1092
  /** Maximum serialized operation size in bytes. Defaults to 256 KiB. */
940
1093
  maxOperationBytes?: number;
941
1094
  /** Maximum operations accepted per minute for this session. Defaults to 600. */
@@ -969,12 +1122,42 @@ declare class ClientSession {
969
1122
  private authContext;
970
1123
  private syncQuerySubsets;
971
1124
  private resumeDeltaCursor;
1125
+ /**
1126
+ * The delivery watermark the client reported at handshake, or null when the client
1127
+ * does not use the delivery watermark (old client, or no watermark yet). When set,
1128
+ * the server drives the gap-free server->client stream from delivery sequences
1129
+ * instead of the version-vector delta.
1130
+ */
1131
+ private clientDeliveryWatermark;
1132
+ /**
1133
+ * The highest delivery sequence this client has ACKNOWLEDGED (its confirmed
1134
+ * watermark, as reported in acks). Incremental streaming pushes resume from here, not
1135
+ * from the highest sequence sent, so a dropped or unapplied batch is always
1136
+ * re-included by the next push (which re-scans from the acknowledged position). This
1137
+ * is what makes streaming recovery independent of any bounded retransmit buffer: there
1138
+ * is no sent-but-unacked window that a buffer eviction could strand. Seeded from the
1139
+ * client's reported watermark at handshake.
1140
+ */
1141
+ private lastAckedDeliverySeq;
1142
+ /** Serializes incremental delivery pushes so their batches never interleave. */
1143
+ private deliveryPushChain;
1144
+ /**
1145
+ * Relay batches sent to this client but not yet acknowledged, keyed by messageId.
1146
+ * The client acks every operation-batch it applies; on ack the entry is cleared.
1147
+ * {@link retransmitPendingRelays} re-sends anything still unacked, so a relay
1148
+ * dropped by a lossy transport is redelivered instead of leaving the client with a
1149
+ * permanent version-vector gap (a lost operation) that delta sync cannot recover.
1150
+ * Bounded so a silent client cannot grow it without limit.
1151
+ */
1152
+ private readonly pendingRelays;
1153
+ private static readonly MAX_PENDING_RELAYS;
972
1154
  private readonly sessionId;
973
1155
  private readonly transport;
974
1156
  private readonly store;
975
1157
  private readonly auth;
976
1158
  private readonly serializer;
977
1159
  private readonly emitter;
1160
+ private readonly logger;
978
1161
  private readonly batchSize;
979
1162
  private readonly schemaVersion;
980
1163
  private readonly supportedSchemaVersions;
@@ -985,6 +1168,8 @@ declare class ClientSession {
985
1168
  private readonly onBlobChunkResponse;
986
1169
  private readonly persistBlobChunk;
987
1170
  private readonly onClose;
1171
+ private readonly onOrphanedRelays;
1172
+ private readonly takeOrphanedRelays;
988
1173
  private readonly maxOperationBytes;
989
1174
  private readonly maxOpsPerMinute;
990
1175
  private readonly rateLimiter;
@@ -1000,9 +1185,30 @@ declare class ClientSession {
1000
1185
  * Only relays if the session is in streaming state and transport is connected.
1001
1186
  */
1002
1187
  relayOperations(operations: Operation[]): void;
1188
+ /**
1189
+ * Push newly-available in-scope operations to a delivery-watermark client, resuming
1190
+ * from the highest delivery sequence already sent. Pushes are serialized so their
1191
+ * batches never interleave (which would break the base/max chain).
1192
+ */
1193
+ private pushDeliveryStream;
1194
+ private relayVisibleOperations;
1195
+ /** Record a relay batch as awaiting acknowledgment, evicting the oldest if full. */
1196
+ private trackPendingRelay;
1197
+ /**
1198
+ * Retransmit relay batches this client has not acknowledged within `staleMs`.
1199
+ * Called on a periodic tick by the server (and directly by tests). Redelivering an
1200
+ * already-applied op is harmless: the client dedups by content-addressed id.
1201
+ */
1202
+ retransmitPendingRelays(staleMs?: number): void;
1003
1203
  /**
1004
1204
  * Close this session.
1005
1205
  */
1206
+ /**
1207
+ * Hand any unacknowledged relays to the server so they survive this session and are
1208
+ * redelivered when this client reconnects (per-node buffer), then clear them. Called
1209
+ * from every session-teardown path (explicit close and transport close).
1210
+ */
1211
+ private flushOrphanedRelays;
1006
1212
  close(reason?: string): void;
1007
1213
  getState(): SessionState;
1008
1214
  getSessionId(): string;
@@ -1029,7 +1235,34 @@ declare class ClientSession {
1029
1235
  private handleHandshake;
1030
1236
  private handleOperationBatch;
1031
1237
  private sendDelta;
1238
+ /**
1239
+ * Send the gap-free server->client delivery stream, resuming just after
1240
+ * `fromDeliverySeq`. Operations are scanned in server delivery-sequence order
1241
+ * (commit order), scope-filtered for this session, and sent in batches that chain:
1242
+ * each batch's `baseDeliverySequence` equals the previous batch's
1243
+ * `maxDeliverySequence` (the first batch bases on `fromDeliverySeq`). The client
1244
+ * applies a batch only when its watermark equals the base and advances the
1245
+ * watermark to the max, so a dropped batch stalls the watermark and is recovered by
1246
+ * the next handshake resend. Because delivery-sequence order respects causal order
1247
+ * (a dependency is always committed, and thus sequenced, before its dependent), no
1248
+ * topological sort is needed and the client never defers on a missing dependency
1249
+ * that is itself in this stream.
1250
+ *
1251
+ * The final batch advances the watermark to the highest delivery sequence scanned,
1252
+ * not merely the last in-scope one, so an out-of-scope tail is not re-scanned on the
1253
+ * next reconnect. Returns the number of operations actually sent.
1254
+ */
1255
+ private sendDeliveryStream;
1256
+ /** Build, track, and send one chained delivery-stream batch. */
1257
+ private sendDeliveryBatch;
1032
1258
  private operationVisibleToClient;
1259
+ /**
1260
+ * Read a record's current field values from the materialized store for scope /
1261
+ * query-subset backfill. Includes soft-deleted rows so a relayed delete (whose op
1262
+ * carries no fields) is still judged against the record's actual scope. Returns
1263
+ * undefined when the record cannot be read (never materialized, or no schema).
1264
+ */
1265
+ private lookupRecordFields;
1033
1266
  private handleAwarenessUpdate;
1034
1267
  private handleYjsDocUpdate;
1035
1268
  private sendError;
@@ -1059,6 +1292,13 @@ interface ProductionServerConfig {
1059
1292
  syncPath?: string;
1060
1293
  /** Additional KoraSyncServer options */
1061
1294
  syncOptions?: Omit<KoraSyncServerConfig, 'store' | 'port' | 'host' | 'path'>;
1295
+ /**
1296
+ * Cross-Origin-Embedder-Policy for static and route responses. Defaults to
1297
+ * `credentialless`, which keeps third-party embeds usable while allowing
1298
+ * capable browsers to enable cross-origin isolation. Use `require-corp` only
1299
+ * when every embedded resource explicitly opts in with CORP/CORS headers.
1300
+ */
1301
+ crossOriginEmbedderPolicy?: 'credentialless' | 'require-corp' | 'unsafe-none';
1062
1302
  /**
1063
1303
  * Optional HTTP route handlers mounted before static file serving.
1064
1304
  *
@@ -1382,6 +1622,14 @@ declare class MemoryServerStore implements ServerStore {
1382
1622
  private readonly operations;
1383
1623
  private readonly operationIndex;
1384
1624
  private readonly versionVector;
1625
+ /**
1626
+ * Server-assigned delivery sequence per operation id. Single-process, so a plain
1627
+ * counter incremented at insert time gives commit-order delivery sequence with no
1628
+ * gaps. `operations` is kept in insertion (== delivery) order, so a delivery scan
1629
+ * is a forward walk.
1630
+ */
1631
+ private readonly deliverySeqByOpId;
1632
+ private deliverySeqCounter;
1385
1633
  private schema;
1386
1634
  /** Materialized records: collection -> recordId -> record data */
1387
1635
  private readonly materializedRecords;
@@ -1394,6 +1642,8 @@ declare class MemoryServerStore implements ServerStore {
1394
1642
  applyRemoteOperation(op: Operation): Promise<ApplyResult>;
1395
1643
  getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
1396
1644
  getOperationCount(): Promise<number>;
1645
+ getMaxDeliverySequence(): Promise<number>;
1646
+ getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
1397
1647
  materializeCollection(collection: string): Promise<MaterializedRecord[]>;
1398
1648
  queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
1399
1649
  findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
@@ -1434,14 +1684,44 @@ declare class PostgresServerStore implements ServerStore {
1434
1684
  private readonly ready;
1435
1685
  private schema;
1436
1686
  private closed;
1437
- constructor(db: PostgresJsDatabase, nodeId?: string);
1687
+ /**
1688
+ * Monotonic counter for this node's server-originated sequence numbers. Lazily
1689
+ * seeded from the persisted version vector on first use, then advanced in memory
1690
+ * (a synchronous `++`, atomic on the single JS thread) so concurrent conditional
1691
+ * applies never receive the same number. Null means "not yet seeded"; reset to
1692
+ * null after a backup import so it re-seeds from the restored version vector.
1693
+ */
1694
+ private sequenceCounter;
1695
+ /**
1696
+ * Time source for HLC timestamps on server-originated operations. Injectable so a
1697
+ * test can freeze wall-clock time and prove the conditional-apply ordering holds
1698
+ * under same-millisecond commits (the case the advisory lock and clock advance
1699
+ * exist to make correct). Defaults to the system clock in production.
1700
+ */
1701
+ private readonly timeSource;
1702
+ constructor(db: PostgresJsDatabase, nodeId?: string, timeSource?: TimeSource);
1438
1703
  getVersionVector(): VersionVector;
1704
+ /**
1705
+ * Atomically reserve this node's next server-operation sequence number. The
1706
+ * synchronous increment cannot interleave on the single JS thread, so two
1707
+ * concurrent conditional applies are always handed distinct numbers.
1708
+ */
1709
+ reserveSequenceNumber(): number;
1439
1710
  getNodeId(): string;
1440
1711
  getSchema(): SchemaDefinition | null;
1441
1712
  setSchema(schema: SchemaDefinition): Promise<void>;
1442
1713
  applyRemoteOperation(op: Operation): Promise<ApplyResult>;
1714
+ /**
1715
+ * Conditionally apply operations atomically, serialized across server instances
1716
+ * on the target record via a transaction-scoped advisory lock. The lock makes
1717
+ * the read-decide-write cycle atomic against concurrent instances, so a cap
1718
+ * check like `responseCount < max` cannot be passed by two admissions at once.
1719
+ */
1720
+ applyConditional(input: ConditionalApplyInput): Promise<ConditionalApplyResult>;
1443
1721
  getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
1444
1722
  getOperationCount(): Promise<number>;
1723
+ getMaxDeliverySequence(): Promise<number>;
1724
+ getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
1445
1725
  materializeCollection(collection: string): Promise<MaterializedRecord[]>;
1446
1726
  queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
1447
1727
  findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
@@ -1452,6 +1732,13 @@ declare class PostgresServerStore implements ServerStore {
1452
1732
  operationsRestored: number;
1453
1733
  success: boolean;
1454
1734
  }>;
1735
+ /**
1736
+ * The HLC timestamp of the most recent operation on a record, by total order
1737
+ * (wallTime, logical, nodeId), or null when the record has no operations. Used
1738
+ * by {@link applyConditional} to advance its clock past the record's latest
1739
+ * write so newly built operations sort strictly after it.
1740
+ */
1741
+ private latestOperationTimestamp;
1455
1742
  /**
1456
1743
  * Rebuild a single record in the materialized collection table by replaying
1457
1744
  * all operations for that record.
@@ -1475,6 +1762,12 @@ declare class PostgresServerStore implements ServerStore {
1475
1762
  private materializeFromOpsLog;
1476
1763
  private initialize;
1477
1764
  private ensureTables;
1765
+ /**
1766
+ * Reserve the next delivery sequence inside an open append transaction. The
1767
+ * `UPDATE ... RETURNING` locks the counter row until this transaction commits,
1768
+ * serializing assignment into commit order (see the counter table comment).
1769
+ */
1770
+ private nextDeliverySeq;
1478
1771
  private serializeOperation;
1479
1772
  private deserializeOperation;
1480
1773
  private assertOpen;
@@ -1502,6 +1795,12 @@ declare class SqliteServerStore implements ServerStore {
1502
1795
  private readonly db;
1503
1796
  private schema;
1504
1797
  private closed;
1798
+ /**
1799
+ * Next delivery sequence to assign. Single-writer (better-sqlite3 is synchronous),
1800
+ * so a plain in-memory counter, seeded from MAX(delivery_seq) at startup, gives
1801
+ * commit-order delivery sequence. Assigned inside the append transaction.
1802
+ */
1803
+ private deliverySeqCounter;
1505
1804
  constructor(db: BetterSQLite3Database, nodeId?: string);
1506
1805
  getVersionVector(): VersionVector;
1507
1806
  getNodeId(): string;
@@ -1510,6 +1809,8 @@ declare class SqliteServerStore implements ServerStore {
1510
1809
  applyRemoteOperation(op: Operation): Promise<ApplyResult>;
1511
1810
  getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
1512
1811
  getOperationCount(): Promise<number>;
1812
+ getMaxDeliverySequence(): Promise<number>;
1813
+ getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
1513
1814
  materializeCollection(collection: string): Promise<MaterializedRecord[]>;
1514
1815
  queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
1515
1816
  findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
@@ -1548,6 +1849,13 @@ declare class SqliteServerStore implements ServerStore {
1548
1849
  * Create the operations and sync_state tables if they don't exist.
1549
1850
  */
1550
1851
  private ensureTables;
1852
+ /**
1853
+ * Assign delivery sequences to any operations written before the column existed,
1854
+ * then seed the in-memory counter from the current max. Ordered by
1855
+ * (received_at, sequence_number, id) so the backfill is deterministic and stable
1856
+ * across restarts. A one-time migration: after it runs, all rows have a value.
1857
+ */
1858
+ private backfillDeliverySequence;
1551
1859
  private serializeOperation;
1552
1860
  private deserializeOperation;
1553
1861
  private assertOpen;