@korajs/server 1.0.0-beta.2 → 1.0.0-beta.5
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.cjs +1012 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +388 -3
- package/dist/index.d.ts +388 -3
- package/dist/index.js +1018 -146
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
|
-
import {
|
|
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.js';
|
|
5
5
|
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
6
6
|
import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Predicate evaluation for conditional route mutations.
|
|
10
|
+
*
|
|
11
|
+
* A predicate is a small, total comparison language evaluated against the current
|
|
12
|
+
* materialized state of a single record at admission time. It is deliberately not
|
|
13
|
+
* a general query language: it exists so a server route can express an atomic
|
|
14
|
+
* admission guard (for example "accept this response only while the form is still
|
|
15
|
+
* below its cap") without pulling a query DSL into the write path.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Comparison operators applied to one field. All operators present on a field
|
|
19
|
+
* must hold for the field to match. Numeric operators (`$lt`, `$lte`, `$gt`,
|
|
20
|
+
* `$gte`) require the record value to be a number; a non-numeric value fails.
|
|
21
|
+
*/
|
|
22
|
+
interface RoutePredicateOperators {
|
|
23
|
+
$eq?: unknown;
|
|
24
|
+
$ne?: unknown;
|
|
25
|
+
$lt?: number;
|
|
26
|
+
$lte?: number;
|
|
27
|
+
$gt?: number;
|
|
28
|
+
$gte?: number;
|
|
29
|
+
$in?: readonly unknown[];
|
|
30
|
+
}
|
|
31
|
+
/** A predicate: every listed field must satisfy all of its operators. */
|
|
32
|
+
type RoutePredicate = Record<string, RoutePredicateOperators>;
|
|
33
|
+
|
|
8
34
|
/**
|
|
9
35
|
* Per-collection scope map from auth context.
|
|
10
36
|
*/
|
|
@@ -18,6 +44,70 @@ interface MaterializedRecord {
|
|
|
18
44
|
id: string;
|
|
19
45
|
[key: string]: unknown;
|
|
20
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
|
+
}
|
|
21
111
|
/**
|
|
22
112
|
* Options for querying a materialized collection table.
|
|
23
113
|
*/
|
|
@@ -44,6 +134,41 @@ interface ServerStore extends SyncStore {
|
|
|
44
134
|
close(): Promise<void>;
|
|
45
135
|
/** Get the total number of stored operations */
|
|
46
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;
|
|
47
172
|
/**
|
|
48
173
|
* Set the schema for materialized collection tables.
|
|
49
174
|
* Creates collection tables and indexes based on the schema definition.
|
|
@@ -343,7 +468,37 @@ declare class KoraSyncServer {
|
|
|
343
468
|
private readonly serverVersion;
|
|
344
469
|
private wsServer;
|
|
345
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;
|
|
346
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;
|
|
347
502
|
/**
|
|
348
503
|
* The trusted, scoped data-plane context (`apply` / `query` / `findById`).
|
|
349
504
|
* Shared with the production server so HTTP routes, the operation validator,
|
|
@@ -406,6 +561,15 @@ declare class KoraSyncServer {
|
|
|
406
561
|
* @returns The apply result, including any server-generated side-effect ops
|
|
407
562
|
*/
|
|
408
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;
|
|
409
573
|
/**
|
|
410
574
|
* Collect every blob reference still reachable from live records on the server.
|
|
411
575
|
*
|
|
@@ -463,6 +627,61 @@ type RouteApplyResult = {
|
|
|
463
627
|
message: string;
|
|
464
628
|
retriable: boolean;
|
|
465
629
|
};
|
|
630
|
+
/**
|
|
631
|
+
* A conditional, multi-collection admission gate applied atomically per instance.
|
|
632
|
+
*
|
|
633
|
+
* The gate reads the current state of the target record (`collection` + `id`),
|
|
634
|
+
* checks the `if` predicate against it, and only then applies the `update` to the
|
|
635
|
+
* target plus every mutation in `also` as one set. If the predicate fails it
|
|
636
|
+
* applies nothing and returns the structured `reject`. `idempotencyKey` names a
|
|
637
|
+
* record whose prior existence proves the set already committed, so a retry
|
|
638
|
+
* returns the earlier outcome instead of applying the set (and its non-idempotent
|
|
639
|
+
* counter increments) again.
|
|
640
|
+
*
|
|
641
|
+
* Atomicity note: within one server instance the check and the writes do not
|
|
642
|
+
* interleave with other route mutations. Cross-instance, race-free admission and
|
|
643
|
+
* all-or-nothing across a mid-set crash require the store-level conditional
|
|
644
|
+
* transaction (a Postgres `WHERE ... < cap` commit), which is the next increment.
|
|
645
|
+
*/
|
|
646
|
+
interface RouteConditionalApply {
|
|
647
|
+
/** Collection of the record the predicate reads and the `update` targets. */
|
|
648
|
+
collection: string;
|
|
649
|
+
/** Id of the target record. */
|
|
650
|
+
id: string;
|
|
651
|
+
/** Admission predicate evaluated against the target's current state. */
|
|
652
|
+
if?: RoutePredicate;
|
|
653
|
+
/** Partial update applied to the target record when the predicate holds. */
|
|
654
|
+
update?: Record<string, unknown> | null;
|
|
655
|
+
/** Additional mutations committed atomically with the target update. */
|
|
656
|
+
also?: RouteMutation[];
|
|
657
|
+
/** Structured rejection returned when the predicate does not hold. */
|
|
658
|
+
reject?: {
|
|
659
|
+
code: string;
|
|
660
|
+
message: string;
|
|
661
|
+
};
|
|
662
|
+
/**
|
|
663
|
+
* Names a record whose existence means this set already committed. When it is
|
|
664
|
+
* present the set is skipped and the prior outcome returned. Use a stable,
|
|
665
|
+
* client-generated id (for example the accepted record's own id).
|
|
666
|
+
*/
|
|
667
|
+
idempotencyKey?: {
|
|
668
|
+
collection: string;
|
|
669
|
+
id: string;
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
/** Result of {@link ProductionHttpRouteContext.applyConditional}. */
|
|
673
|
+
type RouteConditionalResult = {
|
|
674
|
+
ok: true;
|
|
675
|
+
/** True when the set was already committed and this call was a no-op. */
|
|
676
|
+
idempotent: boolean;
|
|
677
|
+
operations: Operation[];
|
|
678
|
+
records: (MaterializedRecord | null)[];
|
|
679
|
+
} | {
|
|
680
|
+
ok: false;
|
|
681
|
+
code: string;
|
|
682
|
+
message: string;
|
|
683
|
+
retriable: boolean;
|
|
684
|
+
};
|
|
466
685
|
/**
|
|
467
686
|
* Scoped, validated data-plane access handed to custom HTTP route handlers.
|
|
468
687
|
*
|
|
@@ -477,6 +696,12 @@ interface ProductionHttpRouteContext {
|
|
|
477
696
|
* resulting operation(s) to connected clients.
|
|
478
697
|
*/
|
|
479
698
|
apply(mutation: RouteMutation, options?: RouteScopeOptions): Promise<RouteApplyResult>;
|
|
699
|
+
/**
|
|
700
|
+
* Conditionally apply a multi-collection mutation set as one admission gate:
|
|
701
|
+
* read the target, check the predicate, and commit the update plus every `also`
|
|
702
|
+
* mutation only if it holds, with at-most-once semantics via `idempotencyKey`.
|
|
703
|
+
*/
|
|
704
|
+
applyConditional(spec: RouteConditionalApply, options?: RouteScopeOptions): Promise<RouteConditionalResult>;
|
|
480
705
|
/** Read materialized records from a collection, optionally scoped. */
|
|
481
706
|
query(collection: string, options?: CollectionQueryOptions & RouteScopeOptions): Promise<MaterializedRecord[]>;
|
|
482
707
|
/** Read a single materialized record by id, optionally scoped. */
|
|
@@ -849,6 +1074,19 @@ interface ClientSessionOptions {
|
|
|
849
1074
|
persistBlobChunk?: PersistBlobChunk;
|
|
850
1075
|
/** Called when this session closes */
|
|
851
1076
|
onClose?: (sessionId: string) => void;
|
|
1077
|
+
/**
|
|
1078
|
+
* Called on close with relay operations this client never acknowledged, so the
|
|
1079
|
+
* server can buffer them per node id and redeliver on the client's next
|
|
1080
|
+
* connection. Closes the window where a relay dropped just before a reconnect
|
|
1081
|
+
* would otherwise be lost (the per-session retransmit tick never fires in time).
|
|
1082
|
+
*/
|
|
1083
|
+
onOrphanedRelays?: (nodeId: string, ops: Operation[]) => void;
|
|
1084
|
+
/**
|
|
1085
|
+
* Called once this session reaches streaming to pull any relay operations buffered
|
|
1086
|
+
* for its node id while it was disconnected. They are re-sent through the normal
|
|
1087
|
+
* relay path (re-filtered by this session's current scope).
|
|
1088
|
+
*/
|
|
1089
|
+
takeOrphanedRelays?: (nodeId: string) => Operation[];
|
|
852
1090
|
/** Maximum serialized operation size in bytes. Defaults to 256 KiB. */
|
|
853
1091
|
maxOperationBytes?: number;
|
|
854
1092
|
/** Maximum operations accepted per minute for this session. Defaults to 600. */
|
|
@@ -882,6 +1120,35 @@ declare class ClientSession {
|
|
|
882
1120
|
private authContext;
|
|
883
1121
|
private syncQuerySubsets;
|
|
884
1122
|
private resumeDeltaCursor;
|
|
1123
|
+
/**
|
|
1124
|
+
* The delivery watermark the client reported at handshake, or null when the client
|
|
1125
|
+
* does not use the delivery watermark (old client, or no watermark yet). When set,
|
|
1126
|
+
* the server drives the gap-free server->client stream from delivery sequences
|
|
1127
|
+
* instead of the version-vector delta.
|
|
1128
|
+
*/
|
|
1129
|
+
private clientDeliveryWatermark;
|
|
1130
|
+
/**
|
|
1131
|
+
* The highest delivery sequence this client has ACKNOWLEDGED (its confirmed
|
|
1132
|
+
* watermark, as reported in acks). Incremental streaming pushes resume from here, not
|
|
1133
|
+
* from the highest sequence sent, so a dropped or unapplied batch is always
|
|
1134
|
+
* re-included by the next push (which re-scans from the acknowledged position). This
|
|
1135
|
+
* is what makes streaming recovery independent of any bounded retransmit buffer: there
|
|
1136
|
+
* is no sent-but-unacked window that a buffer eviction could strand. Seeded from the
|
|
1137
|
+
* client's reported watermark at handshake.
|
|
1138
|
+
*/
|
|
1139
|
+
private lastAckedDeliverySeq;
|
|
1140
|
+
/** Serializes incremental delivery pushes so their batches never interleave. */
|
|
1141
|
+
private deliveryPushChain;
|
|
1142
|
+
/**
|
|
1143
|
+
* Relay batches sent to this client but not yet acknowledged, keyed by messageId.
|
|
1144
|
+
* The client acks every operation-batch it applies; on ack the entry is cleared.
|
|
1145
|
+
* {@link retransmitPendingRelays} re-sends anything still unacked, so a relay
|
|
1146
|
+
* dropped by a lossy transport is redelivered instead of leaving the client with a
|
|
1147
|
+
* permanent version-vector gap (a lost operation) that delta sync cannot recover.
|
|
1148
|
+
* Bounded so a silent client cannot grow it without limit.
|
|
1149
|
+
*/
|
|
1150
|
+
private readonly pendingRelays;
|
|
1151
|
+
private static readonly MAX_PENDING_RELAYS;
|
|
885
1152
|
private readonly sessionId;
|
|
886
1153
|
private readonly transport;
|
|
887
1154
|
private readonly store;
|
|
@@ -898,6 +1165,8 @@ declare class ClientSession {
|
|
|
898
1165
|
private readonly onBlobChunkResponse;
|
|
899
1166
|
private readonly persistBlobChunk;
|
|
900
1167
|
private readonly onClose;
|
|
1168
|
+
private readonly onOrphanedRelays;
|
|
1169
|
+
private readonly takeOrphanedRelays;
|
|
901
1170
|
private readonly maxOperationBytes;
|
|
902
1171
|
private readonly maxOpsPerMinute;
|
|
903
1172
|
private readonly rateLimiter;
|
|
@@ -913,9 +1182,30 @@ declare class ClientSession {
|
|
|
913
1182
|
* Only relays if the session is in streaming state and transport is connected.
|
|
914
1183
|
*/
|
|
915
1184
|
relayOperations(operations: Operation[]): void;
|
|
1185
|
+
/**
|
|
1186
|
+
* Push newly-available in-scope operations to a delivery-watermark client, resuming
|
|
1187
|
+
* from the highest delivery sequence already sent. Pushes are serialized so their
|
|
1188
|
+
* batches never interleave (which would break the base/max chain).
|
|
1189
|
+
*/
|
|
1190
|
+
private pushDeliveryStream;
|
|
1191
|
+
private relayVisibleOperations;
|
|
1192
|
+
/** Record a relay batch as awaiting acknowledgment, evicting the oldest if full. */
|
|
1193
|
+
private trackPendingRelay;
|
|
1194
|
+
/**
|
|
1195
|
+
* Retransmit relay batches this client has not acknowledged within `staleMs`.
|
|
1196
|
+
* Called on a periodic tick by the server (and directly by tests). Redelivering an
|
|
1197
|
+
* already-applied op is harmless: the client dedups by content-addressed id.
|
|
1198
|
+
*/
|
|
1199
|
+
retransmitPendingRelays(staleMs?: number): void;
|
|
916
1200
|
/**
|
|
917
1201
|
* Close this session.
|
|
918
1202
|
*/
|
|
1203
|
+
/**
|
|
1204
|
+
* Hand any unacknowledged relays to the server so they survive this session and are
|
|
1205
|
+
* redelivered when this client reconnects (per-node buffer), then clear them. Called
|
|
1206
|
+
* from every session-teardown path (explicit close and transport close).
|
|
1207
|
+
*/
|
|
1208
|
+
private flushOrphanedRelays;
|
|
919
1209
|
close(reason?: string): void;
|
|
920
1210
|
getState(): SessionState;
|
|
921
1211
|
getSessionId(): string;
|
|
@@ -942,7 +1232,34 @@ declare class ClientSession {
|
|
|
942
1232
|
private handleHandshake;
|
|
943
1233
|
private handleOperationBatch;
|
|
944
1234
|
private sendDelta;
|
|
1235
|
+
/**
|
|
1236
|
+
* Send the gap-free server->client delivery stream, resuming just after
|
|
1237
|
+
* `fromDeliverySeq`. Operations are scanned in server delivery-sequence order
|
|
1238
|
+
* (commit order), scope-filtered for this session, and sent in batches that chain:
|
|
1239
|
+
* each batch's `baseDeliverySequence` equals the previous batch's
|
|
1240
|
+
* `maxDeliverySequence` (the first batch bases on `fromDeliverySeq`). The client
|
|
1241
|
+
* applies a batch only when its watermark equals the base and advances the
|
|
1242
|
+
* watermark to the max, so a dropped batch stalls the watermark and is recovered by
|
|
1243
|
+
* the next handshake resend. Because delivery-sequence order respects causal order
|
|
1244
|
+
* (a dependency is always committed, and thus sequenced, before its dependent), no
|
|
1245
|
+
* topological sort is needed and the client never defers on a missing dependency
|
|
1246
|
+
* that is itself in this stream.
|
|
1247
|
+
*
|
|
1248
|
+
* The final batch advances the watermark to the highest delivery sequence scanned,
|
|
1249
|
+
* not merely the last in-scope one, so an out-of-scope tail is not re-scanned on the
|
|
1250
|
+
* next reconnect. Returns the number of operations actually sent.
|
|
1251
|
+
*/
|
|
1252
|
+
private sendDeliveryStream;
|
|
1253
|
+
/** Build, track, and send one chained delivery-stream batch. */
|
|
1254
|
+
private sendDeliveryBatch;
|
|
945
1255
|
private operationVisibleToClient;
|
|
1256
|
+
/**
|
|
1257
|
+
* Read a record's current field values from the materialized store for scope /
|
|
1258
|
+
* query-subset backfill. Includes soft-deleted rows so a relayed delete (whose op
|
|
1259
|
+
* carries no fields) is still judged against the record's actual scope. Returns
|
|
1260
|
+
* undefined when the record cannot be read (never materialized, or no schema).
|
|
1261
|
+
*/
|
|
1262
|
+
private lookupRecordFields;
|
|
946
1263
|
private handleAwarenessUpdate;
|
|
947
1264
|
private handleYjsDocUpdate;
|
|
948
1265
|
private sendError;
|
|
@@ -1295,6 +1612,14 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1295
1612
|
private readonly operations;
|
|
1296
1613
|
private readonly operationIndex;
|
|
1297
1614
|
private readonly versionVector;
|
|
1615
|
+
/**
|
|
1616
|
+
* Server-assigned delivery sequence per operation id. Single-process, so a plain
|
|
1617
|
+
* counter incremented at insert time gives commit-order delivery sequence with no
|
|
1618
|
+
* gaps. `operations` is kept in insertion (== delivery) order, so a delivery scan
|
|
1619
|
+
* is a forward walk.
|
|
1620
|
+
*/
|
|
1621
|
+
private readonly deliverySeqByOpId;
|
|
1622
|
+
private deliverySeqCounter;
|
|
1298
1623
|
private schema;
|
|
1299
1624
|
/** Materialized records: collection -> recordId -> record data */
|
|
1300
1625
|
private readonly materializedRecords;
|
|
@@ -1307,6 +1632,8 @@ declare class MemoryServerStore implements ServerStore {
|
|
|
1307
1632
|
applyRemoteOperation(op: Operation): Promise<ApplyResult>;
|
|
1308
1633
|
getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
|
|
1309
1634
|
getOperationCount(): Promise<number>;
|
|
1635
|
+
getMaxDeliverySequence(): Promise<number>;
|
|
1636
|
+
getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
|
|
1310
1637
|
materializeCollection(collection: string): Promise<MaterializedRecord[]>;
|
|
1311
1638
|
queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
|
|
1312
1639
|
findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
|
|
@@ -1347,14 +1674,44 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1347
1674
|
private readonly ready;
|
|
1348
1675
|
private schema;
|
|
1349
1676
|
private closed;
|
|
1350
|
-
|
|
1677
|
+
/**
|
|
1678
|
+
* Monotonic counter for this node's server-originated sequence numbers. Lazily
|
|
1679
|
+
* seeded from the persisted version vector on first use, then advanced in memory
|
|
1680
|
+
* (a synchronous `++`, atomic on the single JS thread) so concurrent conditional
|
|
1681
|
+
* applies never receive the same number. Null means "not yet seeded"; reset to
|
|
1682
|
+
* null after a backup import so it re-seeds from the restored version vector.
|
|
1683
|
+
*/
|
|
1684
|
+
private sequenceCounter;
|
|
1685
|
+
/**
|
|
1686
|
+
* Time source for HLC timestamps on server-originated operations. Injectable so a
|
|
1687
|
+
* test can freeze wall-clock time and prove the conditional-apply ordering holds
|
|
1688
|
+
* under same-millisecond commits (the case the advisory lock and clock advance
|
|
1689
|
+
* exist to make correct). Defaults to the system clock in production.
|
|
1690
|
+
*/
|
|
1691
|
+
private readonly timeSource;
|
|
1692
|
+
constructor(db: PostgresJsDatabase, nodeId?: string, timeSource?: TimeSource);
|
|
1351
1693
|
getVersionVector(): VersionVector;
|
|
1694
|
+
/**
|
|
1695
|
+
* Atomically reserve this node's next server-operation sequence number. The
|
|
1696
|
+
* synchronous increment cannot interleave on the single JS thread, so two
|
|
1697
|
+
* concurrent conditional applies are always handed distinct numbers.
|
|
1698
|
+
*/
|
|
1699
|
+
reserveSequenceNumber(): number;
|
|
1352
1700
|
getNodeId(): string;
|
|
1353
1701
|
getSchema(): SchemaDefinition | null;
|
|
1354
1702
|
setSchema(schema: SchemaDefinition): Promise<void>;
|
|
1355
1703
|
applyRemoteOperation(op: Operation): Promise<ApplyResult>;
|
|
1704
|
+
/**
|
|
1705
|
+
* Conditionally apply operations atomically, serialized across server instances
|
|
1706
|
+
* on the target record via a transaction-scoped advisory lock. The lock makes
|
|
1707
|
+
* the read-decide-write cycle atomic against concurrent instances, so a cap
|
|
1708
|
+
* check like `responseCount < max` cannot be passed by two admissions at once.
|
|
1709
|
+
*/
|
|
1710
|
+
applyConditional(input: ConditionalApplyInput): Promise<ConditionalApplyResult>;
|
|
1356
1711
|
getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
|
|
1357
1712
|
getOperationCount(): Promise<number>;
|
|
1713
|
+
getMaxDeliverySequence(): Promise<number>;
|
|
1714
|
+
getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
|
|
1358
1715
|
materializeCollection(collection: string): Promise<MaterializedRecord[]>;
|
|
1359
1716
|
queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
|
|
1360
1717
|
findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
|
|
@@ -1365,6 +1722,13 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1365
1722
|
operationsRestored: number;
|
|
1366
1723
|
success: boolean;
|
|
1367
1724
|
}>;
|
|
1725
|
+
/**
|
|
1726
|
+
* The HLC timestamp of the most recent operation on a record, by total order
|
|
1727
|
+
* (wallTime, logical, nodeId), or null when the record has no operations. Used
|
|
1728
|
+
* by {@link applyConditional} to advance its clock past the record's latest
|
|
1729
|
+
* write so newly built operations sort strictly after it.
|
|
1730
|
+
*/
|
|
1731
|
+
private latestOperationTimestamp;
|
|
1368
1732
|
/**
|
|
1369
1733
|
* Rebuild a single record in the materialized collection table by replaying
|
|
1370
1734
|
* all operations for that record.
|
|
@@ -1388,6 +1752,12 @@ declare class PostgresServerStore implements ServerStore {
|
|
|
1388
1752
|
private materializeFromOpsLog;
|
|
1389
1753
|
private initialize;
|
|
1390
1754
|
private ensureTables;
|
|
1755
|
+
/**
|
|
1756
|
+
* Reserve the next delivery sequence inside an open append transaction. The
|
|
1757
|
+
* `UPDATE ... RETURNING` locks the counter row until this transaction commits,
|
|
1758
|
+
* serializing assignment into commit order (see the counter table comment).
|
|
1759
|
+
*/
|
|
1760
|
+
private nextDeliverySeq;
|
|
1391
1761
|
private serializeOperation;
|
|
1392
1762
|
private deserializeOperation;
|
|
1393
1763
|
private assertOpen;
|
|
@@ -1415,6 +1785,12 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1415
1785
|
private readonly db;
|
|
1416
1786
|
private schema;
|
|
1417
1787
|
private closed;
|
|
1788
|
+
/**
|
|
1789
|
+
* Next delivery sequence to assign. Single-writer (better-sqlite3 is synchronous),
|
|
1790
|
+
* so a plain in-memory counter, seeded from MAX(delivery_seq) at startup, gives
|
|
1791
|
+
* commit-order delivery sequence. Assigned inside the append transaction.
|
|
1792
|
+
*/
|
|
1793
|
+
private deliverySeqCounter;
|
|
1418
1794
|
constructor(db: BetterSQLite3Database, nodeId?: string);
|
|
1419
1795
|
getVersionVector(): VersionVector;
|
|
1420
1796
|
getNodeId(): string;
|
|
@@ -1423,6 +1799,8 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1423
1799
|
applyRemoteOperation(op: Operation): Promise<ApplyResult>;
|
|
1424
1800
|
getOperationRange(nodeId: string, fromSeq: number, toSeq: number): Promise<Operation[]>;
|
|
1425
1801
|
getOperationCount(): Promise<number>;
|
|
1802
|
+
getMaxDeliverySequence(): Promise<number>;
|
|
1803
|
+
getOperationsAfterDelivery(afterDeliverySequence: number, limit: number): Promise<DeliveredOperation[]>;
|
|
1426
1804
|
materializeCollection(collection: string): Promise<MaterializedRecord[]>;
|
|
1427
1805
|
queryCollection(collection: string, options?: CollectionQueryOptions): Promise<MaterializedRecord[]>;
|
|
1428
1806
|
findRecord(collection: string, id: string): Promise<MaterializedRecord | null>;
|
|
@@ -1461,6 +1839,13 @@ declare class SqliteServerStore implements ServerStore {
|
|
|
1461
1839
|
* Create the operations and sync_state tables if they don't exist.
|
|
1462
1840
|
*/
|
|
1463
1841
|
private ensureTables;
|
|
1842
|
+
/**
|
|
1843
|
+
* Assign delivery sequences to any operations written before the column existed,
|
|
1844
|
+
* then seed the in-memory counter from the current max. Ordered by
|
|
1845
|
+
* (received_at, sequence_number, id) so the backfill is deterministic and stable
|
|
1846
|
+
* across restarts. A one-time migration: after it runs, all rows have a value.
|
|
1847
|
+
*/
|
|
1848
|
+
private backfillDeliverySequence;
|
|
1464
1849
|
private serializeOperation;
|
|
1465
1850
|
private deserializeOperation;
|
|
1466
1851
|
private assertOpen;
|
|
@@ -1562,4 +1947,4 @@ declare class AwarenessRelay {
|
|
|
1562
1947
|
*/
|
|
1563
1948
|
declare function createKoraServer(config: KoraSyncServerConfig): KoraSyncServer;
|
|
1564
1949
|
|
|
1565
|
-
export { type AuthContext, type AuthProvider, AwarenessRelay, type AwarenessRelayCallback, type ClientMetrics, ClientSession, type ClientSessionOptions, type CollectionQueryOptions, type HttpPollResponse, HttpServerTransport, type HttpSyncRequest, type HttpSyncResponse, KoraAuthProvider, type KoraAuthProviderOptions, KoraSyncServer, type KoraSyncServerConfig, type LogEntry, type LogLevel, type Logger, type MaterializedRecord, MemoryServerStore, MixedAuthProvider, type MixedAuthProviderOptions, NoAuthProvider, type OperationDecision, type OperationRejection, type OperationValidationContext, type OperationValidator, PostgresServerStore, type ProductionHttpRoute, type ProductionHttpRouteContext, type ProductionHttpRouteRequest, type ProductionHttpRouteResponse, type ProductionServer, type ProductionServerConfig, RETRIABLE_REJECTION_CODES, type RelayCallback, type RouteApplyResult, type RouteMutation, type RouteScopeOptions, ServerCloseHandler, ServerErrorHandler, ServerMessageHandler, ServerMetricsCollector, type ServerMetricsSnapshot, type ServerStatus, type ServerStore, ServerTransport, type SessionState, SqliteServerStore, TokenAuthProvider, type TokenAuthProviderOptions, type WsServerConstructor, type WsServerLike, WsServerTransport, type WsServerTransportOptions, type WsWebSocket, createDefaultLogger, createJsonLogger, createKoraServer, createPostgresServerStore, createPrettyLogger, createProductionServer, createSilentLogger, createSqliteServerStore, isRetriableRejection };
|
|
1950
|
+
export { type AuthContext, type AuthProvider, AwarenessRelay, type AwarenessRelayCallback, type ClientMetrics, ClientSession, type ClientSessionOptions, type CollectionQueryOptions, type HttpPollResponse, HttpServerTransport, type HttpSyncRequest, type HttpSyncResponse, KoraAuthProvider, type KoraAuthProviderOptions, KoraSyncServer, type KoraSyncServerConfig, type LogEntry, type LogLevel, type Logger, type MaterializedRecord, MemoryServerStore, MixedAuthProvider, type MixedAuthProviderOptions, NoAuthProvider, type OperationDecision, type OperationRejection, type OperationValidationContext, type OperationValidator, PostgresServerStore, type ProductionHttpRoute, type ProductionHttpRouteContext, type ProductionHttpRouteRequest, type ProductionHttpRouteResponse, type ProductionServer, type ProductionServerConfig, RETRIABLE_REJECTION_CODES, type RelayCallback, type RouteApplyResult, type RouteConditionalApply, type RouteConditionalResult, type RouteMutation, type RoutePredicate, type RoutePredicateOperators, type RouteScopeOptions, ServerCloseHandler, ServerErrorHandler, ServerMessageHandler, ServerMetricsCollector, type ServerMetricsSnapshot, type ServerStatus, type ServerStore, ServerTransport, type SessionState, SqliteServerStore, TokenAuthProvider, type TokenAuthProviderOptions, type WsServerConstructor, type WsServerLike, WsServerTransport, type WsServerTransportOptions, type WsWebSocket, createDefaultLogger, createJsonLogger, createKoraServer, createPostgresServerStore, createPrettyLogger, createProductionServer, createSilentLogger, createSqliteServerStore, isRetriableRejection };
|