@korajs/core 1.0.0-beta.11 → 1.0.0-beta.13
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 +21 -13
- package/dist/bindings/index.d.cts +26 -6
- package/dist/bindings/index.d.ts +26 -6
- package/dist/{build-scope-map-B3R_emUU.d.ts → build-scope-map-CIFa58oA.d.ts} +1 -1
- package/dist/{build-scope-map--0H8l6Tw.d.cts → build-scope-map-ClIq_doj.d.cts} +1 -1
- package/dist/{chunk-5IICSH6H.js → chunk-QUD3DMSD.js} +668 -24
- package/dist/chunk-QUD3DMSD.js.map +1 -0
- package/dist/events-BSU-0z5O.d.cts +1737 -0
- package/dist/events-BSU-0z5O.d.ts +1737 -0
- package/dist/index.cjs +2539 -207
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +810 -220
- package/dist/index.d.ts +810 -220
- package/dist/index.js +2025 -340
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +857 -85
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +251 -21
- package/dist/internal.d.ts +251 -21
- package/dist/internal.js +447 -1
- package/dist/internal.js.map +1 -1
- package/dist/sql-gen-ByOZIQU-.d.ts +622 -0
- package/dist/sql-gen-DdOkMojr.d.cts +622 -0
- package/package.json +2 -1
- package/dist/chunk-5IICSH6H.js.map +0 -1
- package/dist/events-BPbYUbrE.d.cts +0 -999
- package/dist/events-BPbYUbrE.d.ts +0 -999
- package/dist/operation-DbynlVcp.d.cts +0 -164
- package/dist/operation-Dt0AtT8u.d.ts +0 -164
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { ai as TimeSource, H as HLCTimestamp, O as OperationInput, A as AtomicOp, a as Operation, M as MergeTrace, S as SchemaDefinition, C as CollectionDefinition, a5 as RelationDefinition } from './events-BSU-0z5O.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Highest logical counter value that fits the 5-digit zero-padded slot in the
|
|
5
|
+
* serialized timestamp form. The serialized string must sort lexicographically
|
|
6
|
+
* in exactly the same order as {@link HybridLogicalClock.compare}; a 6-digit
|
|
7
|
+
* logical value would shift the padding and silently break that invariant for
|
|
8
|
+
* every stored `_version` column and operation timestamp. When the counter
|
|
9
|
+
* would exceed this cap, the clock carries into wallTime instead
|
|
10
|
+
* (wallTime + 1, logical = 0), which preserves both monotonicity and the
|
|
11
|
+
* serialized ordering.
|
|
12
|
+
*/
|
|
13
|
+
declare const MAX_LOGICAL = 99999;
|
|
14
|
+
/**
|
|
15
|
+
* Hybrid Logical Clock implementation based on Kulkarni et al.
|
|
16
|
+
*
|
|
17
|
+
* Provides a total order that respects causality without requiring synchronized clocks.
|
|
18
|
+
* Each call to now() returns a timestamp strictly greater than the previous one.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const clock = new HybridLogicalClock('node-1')
|
|
23
|
+
* const ts1 = clock.now()
|
|
24
|
+
* const ts2 = clock.now()
|
|
25
|
+
* // HybridLogicalClock.compare(ts1, ts2) < 0 (ts1 is earlier)
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
declare class HybridLogicalClock {
|
|
29
|
+
private readonly nodeId;
|
|
30
|
+
private readonly timeSource;
|
|
31
|
+
private readonly onDriftWarning?;
|
|
32
|
+
private readonly onDriftError?;
|
|
33
|
+
private wallTime;
|
|
34
|
+
private logical;
|
|
35
|
+
private referenceOffsetMs;
|
|
36
|
+
constructor(nodeId: string, timeSource?: TimeSource, onDriftWarning?: ((driftMs: number) => void) | undefined, onDriftError?: ((driftMs: number) => void) | undefined);
|
|
37
|
+
/**
|
|
38
|
+
* Records the known offset between an external time reference (usually the
|
|
39
|
+
* sync server, learned at handshake) and this device's physical clock:
|
|
40
|
+
* `referenceTime - localPhysicalTime`. Once set, drift evaluation and remote
|
|
41
|
+
* timestamp validation are performed against reference-corrected time, so a
|
|
42
|
+
* device with a wrong local clock still validates remote timestamps correctly.
|
|
43
|
+
*/
|
|
44
|
+
setReferenceOffset(offsetMs: number): void;
|
|
45
|
+
getReferenceOffset(): number | null;
|
|
46
|
+
/** Physical time corrected by the known reference offset, when available. */
|
|
47
|
+
private effectiveTime;
|
|
48
|
+
/**
|
|
49
|
+
* Generate a new timestamp for a local event.
|
|
50
|
+
* Guarantees monotonicity: each call returns a timestamp strictly greater than the previous.
|
|
51
|
+
*
|
|
52
|
+
* Never throws and never blocks a local write: if the physical clock has
|
|
53
|
+
* fallen behind the HLC (e.g. the user corrected a fast clock), the HLC
|
|
54
|
+
* freezes wallTime and advances the logical counter, and drift is reported
|
|
55
|
+
* through the onDriftWarning / onDriftError callbacks instead.
|
|
56
|
+
*/
|
|
57
|
+
now(): HLCTimestamp;
|
|
58
|
+
/**
|
|
59
|
+
* Update clock on receiving a remote timestamp.
|
|
60
|
+
* Merges the remote clock state with the local state to maintain causal ordering.
|
|
61
|
+
*
|
|
62
|
+
* @throws {InvalidTimestampError} If the remote timestamp has non-integer or
|
|
63
|
+
* negative wallTime/logical, or logical beyond {@link MAX_LOGICAL}. Checked
|
|
64
|
+
* BEFORE any state change, so malformed input cannot corrupt this clock.
|
|
65
|
+
* @throws {RemoteClockDriftError} If the remote timestamp is more than 5 minutes
|
|
66
|
+
* ahead of reference-corrected local time. Validation happens BEFORE any state
|
|
67
|
+
* is adopted, so a rejected timestamp cannot poison this clock. When no
|
|
68
|
+
* reference offset is known and this clock is uninitialized (cold start with
|
|
69
|
+
* a possibly-wrong local clock), validation is skipped, matching the previous
|
|
70
|
+
* cold-start behavior but now without a corrupting failure mode afterward.
|
|
71
|
+
*/
|
|
72
|
+
receive(remote: HLCTimestamp): HLCTimestamp;
|
|
73
|
+
/**
|
|
74
|
+
* Advance this clock to at least the given timestamp.
|
|
75
|
+
*
|
|
76
|
+
* Used after a timestamp rebase rewrites unsynced operations: future `now()`
|
|
77
|
+
* timestamps must sort after every rebased operation, otherwise a write issued
|
|
78
|
+
* immediately after the rebase could interleave with (or precede) rebased ops
|
|
79
|
+
* and break the log's total order. Never moves the clock backward — a
|
|
80
|
+
* timestamp at or before the current state is a no-op, preserving the
|
|
81
|
+
* monotonicity guarantee of `now()`.
|
|
82
|
+
*
|
|
83
|
+
* Inputs with logical > MAX_LOGICAL are normalized deterministically by
|
|
84
|
+
* carrying the excess into wallTime, so the adopted state always leaves room
|
|
85
|
+
* for the next increment inside the serializable range.
|
|
86
|
+
*/
|
|
87
|
+
advanceTo(ts: HLCTimestamp): void;
|
|
88
|
+
/**
|
|
89
|
+
* Carry the logical counter into wallTime when an increment pushed it past
|
|
90
|
+
* MAX_LOGICAL. Bumping wallTime by 1ms keeps the timestamp strictly greater
|
|
91
|
+
* than everything issued before (monotonicity) while keeping the logical
|
|
92
|
+
* counter inside the 5-digit slot the serialized form depends on.
|
|
93
|
+
*/
|
|
94
|
+
private carryLogicalOverflow;
|
|
95
|
+
/**
|
|
96
|
+
* Compare two timestamps. Returns negative if a < b, positive if a > b, zero if equal.
|
|
97
|
+
* Total order: wallTime first, then logical, then nodeId (lexicographic).
|
|
98
|
+
*/
|
|
99
|
+
static compare(a: HLCTimestamp, b: HLCTimestamp): number;
|
|
100
|
+
/**
|
|
101
|
+
* Serialize an HLC timestamp to a string that sorts lexicographically.
|
|
102
|
+
* Format: zero-padded wallTime:logical:nodeId
|
|
103
|
+
*
|
|
104
|
+
* @throws {InvalidTimestampError} If wallTime does not fit 15 digits or
|
|
105
|
+
* logical does not fit 5 digits (or either is negative/non-integer). Such a
|
|
106
|
+
* value would overflow its zero-padded slot and the serialized string would
|
|
107
|
+
* no longer sort in the same order as {@link HybridLogicalClock.compare} —
|
|
108
|
+
* silently corrupting every LWW comparison on stored `_version` columns.
|
|
109
|
+
* Internal clocks can no longer produce such values (the logical counter
|
|
110
|
+
* carries into wallTime); this guards against hand-built timestamps.
|
|
111
|
+
*/
|
|
112
|
+
static serialize(ts: HLCTimestamp): string;
|
|
113
|
+
/**
|
|
114
|
+
* Deserialize an HLC timestamp from its serialized string form.
|
|
115
|
+
*/
|
|
116
|
+
static deserialize(s: string): HLCTimestamp;
|
|
117
|
+
/**
|
|
118
|
+
* Reports drift between the HLC and (reference-corrected) physical time.
|
|
119
|
+
* Reporting only: local timestamp generation is never blocked, because the
|
|
120
|
+
* user's data always outranks the quality of its timestamps.
|
|
121
|
+
*/
|
|
122
|
+
private checkDrift;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Content-hash versions for operation ids.
|
|
127
|
+
*
|
|
128
|
+
* - **1** (Kora <= beta.12): hashes type, collection, recordId,
|
|
129
|
+
* data, timestamp, nodeId and atomicOps. previousData, sequenceNumber,
|
|
130
|
+
* causalDeps and schemaVersion are NOT covered (CORE-1).
|
|
131
|
+
* - **2**: additionally hashes previousData, sequenceNumber, causalDeps (as a
|
|
132
|
+
* set) and schemaVersion, with the version itself as a domain tag, and binary
|
|
133
|
+
* values in their canonical tagged form. Operations carry `hashVersion: 2`.
|
|
134
|
+
*/
|
|
135
|
+
type OperationHashVersion = 1 | 2;
|
|
136
|
+
/** Hash version new operations get: 2 since protocol v2 (beta.13). */
|
|
137
|
+
declare const DEFAULT_OPERATION_HASH_VERSION: OperationHashVersion;
|
|
138
|
+
/** The operation fields a content hash can cover. */
|
|
139
|
+
interface HashableOperation {
|
|
140
|
+
nodeId: string;
|
|
141
|
+
type: OperationInput['type'];
|
|
142
|
+
collection: string;
|
|
143
|
+
recordId: string;
|
|
144
|
+
data: Record<string, unknown> | null;
|
|
145
|
+
previousData: Record<string, unknown> | null;
|
|
146
|
+
timestamp: HLCTimestamp;
|
|
147
|
+
sequenceNumber: number;
|
|
148
|
+
causalDeps: readonly string[];
|
|
149
|
+
schemaVersion: number;
|
|
150
|
+
atomicOps?: Record<string, AtomicOp>;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Compute the content-addressed ID for an operation using SHA-256.
|
|
154
|
+
* The same operation content always produces the same hash, ensuring deduplication.
|
|
155
|
+
*
|
|
156
|
+
* Two call forms:
|
|
157
|
+
* - `computeOperationId(input, serializedTimestamp)`: the version-1 hash (legacy
|
|
158
|
+
* form, used by the store's rebase and node-id rotation).
|
|
159
|
+
* - `computeOperationId(op, version)`: the hash of the given version over a full
|
|
160
|
+
* operation (with its HLC timestamp).
|
|
161
|
+
*
|
|
162
|
+
* @returns A hex-encoded SHA-256 hash
|
|
163
|
+
*/
|
|
164
|
+
declare function computeOperationId(input: OperationInput, timestamp: string): Promise<string>;
|
|
165
|
+
declare function computeOperationId(op: HashableOperation, version: OperationHashVersion): Promise<string>;
|
|
166
|
+
/**
|
|
167
|
+
* Deterministic JSON serialization with sorted keys.
|
|
168
|
+
* Ensures identical objects always produce identical strings regardless of property insertion order.
|
|
169
|
+
*
|
|
170
|
+
* @param obj - The value to serialize
|
|
171
|
+
* @returns A deterministic JSON string
|
|
172
|
+
*/
|
|
173
|
+
declare function canonicalize(obj: unknown): string;
|
|
174
|
+
|
|
175
|
+
/** Options for {@link createOperation}. */
|
|
176
|
+
interface CreateOperationOptions {
|
|
177
|
+
/**
|
|
178
|
+
* Content-hash version of the new operation's id. Defaults to 2 (protocol v2),
|
|
179
|
+
* which commits the id to every semantic field (CORE-1); 1 is the legacy hash.
|
|
180
|
+
*/
|
|
181
|
+
hashVersion?: OperationHashVersion;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Creates an immutable, content-addressed Operation from the given parameters.
|
|
185
|
+
* The operation is deep-frozen after creation — it cannot be modified.
|
|
186
|
+
*
|
|
187
|
+
* @param input - The operation parameters (without id, which is computed)
|
|
188
|
+
* @param clock - The HLC clock to generate the timestamp
|
|
189
|
+
* @param options - Optional hash version (default 2)
|
|
190
|
+
* @returns A frozen Operation with a content-addressed id
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* const op = await createOperation({
|
|
195
|
+
* nodeId: 'device-1',
|
|
196
|
+
* type: 'insert',
|
|
197
|
+
* collection: 'todos',
|
|
198
|
+
* recordId: 'rec-1',
|
|
199
|
+
* data: { title: 'Ship it' },
|
|
200
|
+
* previousData: null,
|
|
201
|
+
* sequenceNumber: 1,
|
|
202
|
+
* causalDeps: [],
|
|
203
|
+
* schemaVersion: 1,
|
|
204
|
+
* }, clock)
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
declare function createOperation(rawInput: OperationInput, clock: HybridLogicalClock, options?: CreateOperationOptions): Promise<Operation>;
|
|
208
|
+
/**
|
|
209
|
+
* Validates operation input parameters. Throws OperationError with
|
|
210
|
+
* contextual information on validation failure.
|
|
211
|
+
*/
|
|
212
|
+
declare function validateOperationParams(input: OperationInput): void;
|
|
213
|
+
/**
|
|
214
|
+
* Verify an operation's id against its content, using the hash version the
|
|
215
|
+
* operation declares (`hashVersion`, absent = 1). A version-1 id does not cover
|
|
216
|
+
* previousData, sequenceNumber, causalDeps or schemaVersion (CORE-1), so only a
|
|
217
|
+
* version-2 operation is protected against rewriting those.
|
|
218
|
+
*
|
|
219
|
+
* Returns false for an unknown hash version.
|
|
220
|
+
*
|
|
221
|
+
* @param op - The operation to verify
|
|
222
|
+
* @returns true when `op.id` is the content hash of `op`
|
|
223
|
+
*/
|
|
224
|
+
declare function verifyOperationId(op: Operation): Promise<boolean>;
|
|
225
|
+
/**
|
|
226
|
+
* Verify the integrity of an operation by recomputing its content hash.
|
|
227
|
+
* Returns true if the id matches the recomputed hash. Same as
|
|
228
|
+
* {@link verifyOperationId}.
|
|
229
|
+
*/
|
|
230
|
+
declare function verifyOperationIntegrity(op: Operation): Promise<boolean>;
|
|
231
|
+
/**
|
|
232
|
+
* Type guard for Operation interface.
|
|
233
|
+
*/
|
|
234
|
+
declare function isValidOperation(value: unknown): value is Operation;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Transforms operations created under an older schema version so they are valid
|
|
238
|
+
* after a schema upgrade. Used during sync when client and server schema versions differ
|
|
239
|
+
* but remain within the server's supported range.
|
|
240
|
+
*/
|
|
241
|
+
interface OperationTransform {
|
|
242
|
+
/** Schema version the operation was authored against */
|
|
243
|
+
readonly fromVersion: number;
|
|
244
|
+
/** Target schema version after transformation */
|
|
245
|
+
readonly toVersion: number;
|
|
246
|
+
/**
|
|
247
|
+
* Transform a single operation. Return `null` to drop operations that cannot be migrated.
|
|
248
|
+
*/
|
|
249
|
+
transform(operation: Operation): Operation | null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Version of the serialized fold-state format ({@link FoldState}). Bumped on any
|
|
254
|
+
* change to the on-disk shape. A reader that sees a different version must refuse
|
|
255
|
+
* the state and re-fold the record from its operation log.
|
|
256
|
+
*/
|
|
257
|
+
declare const FOLD_STATE_VERSION = 2;
|
|
258
|
+
/**
|
|
259
|
+
* A write's position in the record's total order: the operation's HLC timestamp
|
|
260
|
+
* (serialized, so it compares as a plain string exactly like
|
|
261
|
+
* `HybridLogicalClock.compare`), then the operation id as a tie-breaker. Two
|
|
262
|
+
* distinct operations never share a stamp, even when a buggy or forged node reuses
|
|
263
|
+
* an HLC, so every "latest wins" comparison in the fold is a strict total order.
|
|
264
|
+
*
|
|
265
|
+
* A write to a `merge('server-authoritative')` field by an authoritative node
|
|
266
|
+
* ({@link FoldOptions.authoritativeNodeIds}) carries class `c: 1`, which orders
|
|
267
|
+
* before the HLC: the order is lexicographic on (class, HLC, op id).
|
|
268
|
+
*/
|
|
269
|
+
interface Stamp {
|
|
270
|
+
/** `HybridLogicalClock.serialize(timestamp)` of the writing operation (or field version). */
|
|
271
|
+
t: string;
|
|
272
|
+
/** Id of the writing operation. */
|
|
273
|
+
o: string;
|
|
274
|
+
/** Authority class (1 = authoritative write). Absent means 0. */
|
|
275
|
+
c?: 1;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* One write in a field's embedded log ({@link RegisterFieldState},
|
|
279
|
+
* {@link ResolverFieldState}).
|
|
280
|
+
*/
|
|
281
|
+
interface FieldLogEntry {
|
|
282
|
+
/** Position of the write in the total order. */
|
|
283
|
+
s: Stamp;
|
|
284
|
+
/** The written value (for atomic writes, the author's resolved value). */
|
|
285
|
+
v: unknown;
|
|
286
|
+
/** Atomic intent, when the write was `op.increment()`, `op.max()`, ... */
|
|
287
|
+
a?: AtomicOp;
|
|
288
|
+
/** For resolver fields: the writer's base (`previousData[field]`), null for inserts. */
|
|
289
|
+
b?: unknown;
|
|
290
|
+
/**
|
|
291
|
+
* Resolver fields only: a snapshot entry ({@link createSnapshotState}). It resets
|
|
292
|
+
* the value without calling the resolver, and every older entry is pruned.
|
|
293
|
+
*/
|
|
294
|
+
z?: 1;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Scalar fields (string, number, boolean, enum, timestamp, blob, secret, and any
|
|
298
|
+
* field declared `merge('lww')` or `merge('server-authoritative')`): a last-write-
|
|
299
|
+
* wins register generalized to the atomic-op chain.
|
|
300
|
+
*
|
|
301
|
+
* `e` holds the writes since (and including) the newest plain write, sorted by
|
|
302
|
+
* stamp. A plain write resets the field unconditionally, so everything older is
|
|
303
|
+
* irrelevant and is pruned. With no atomic writes `e` has exactly one entry: an
|
|
304
|
+
* LWW register. With atomic writes `e` is the chain the value is folded from (a
|
|
305
|
+
* same-type atomic chain composes; any other write takes its resolved value).
|
|
306
|
+
*/
|
|
307
|
+
interface RegisterFieldState {
|
|
308
|
+
k: 'reg';
|
|
309
|
+
e: FieldLogEntry[];
|
|
310
|
+
/** Cached `chainFold(e)`. Always equal to it; stored so materialization is O(1). */
|
|
311
|
+
val: unknown;
|
|
312
|
+
}
|
|
313
|
+
/** One element occurrence of an {@link ElementSetFieldState}. */
|
|
314
|
+
interface ElementState {
|
|
315
|
+
/** The element value (canonical JSON parsed back, so key order is normalized). */
|
|
316
|
+
v: unknown;
|
|
317
|
+
/** Occurrence number: this is the n-th copy (from 0) of the value in the array. */
|
|
318
|
+
n: number;
|
|
319
|
+
/** Newest add. */
|
|
320
|
+
a: Stamp | null;
|
|
321
|
+
/** Oldest add plus the element's index in that write (its position in the array). */
|
|
322
|
+
f: {
|
|
323
|
+
s: Stamp;
|
|
324
|
+
i: number;
|
|
325
|
+
} | null;
|
|
326
|
+
/** Newest removal. */
|
|
327
|
+
r: Stamp | null;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Array fields: an LWW element set. Each element (identified by its canonical JSON)
|
|
331
|
+
* carries its newest add and newest removal; it is present iff its newest add is
|
|
332
|
+
* later than its newest removal and later than the newest write that replaced the
|
|
333
|
+
* whole array with a non-array (`clr`). Order is by first add.
|
|
334
|
+
*/
|
|
335
|
+
interface ElementSetFieldState {
|
|
336
|
+
k: 'set';
|
|
337
|
+
/** Append-only (`merge('append-only')`): removals are ignored. */
|
|
338
|
+
ao: boolean;
|
|
339
|
+
/** Whole-value shape register: is the field an array, or a non-array value (e.g. null)? */
|
|
340
|
+
sh: {
|
|
341
|
+
s: Stamp;
|
|
342
|
+
arr: boolean;
|
|
343
|
+
v?: unknown;
|
|
344
|
+
} | null;
|
|
345
|
+
/** Newest write that set the field to a non-array value: elements added before it are cleared. */
|
|
346
|
+
clr: Stamp | null;
|
|
347
|
+
/** Elements keyed by canonical JSON. */
|
|
348
|
+
el: Record<string, ElementState>;
|
|
349
|
+
}
|
|
350
|
+
/** One top-level key of a {@link KeyMapFieldState}. */
|
|
351
|
+
interface KeyState {
|
|
352
|
+
s: Stamp;
|
|
353
|
+
/** true = removal marker. */
|
|
354
|
+
del: boolean;
|
|
355
|
+
v?: unknown;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Object and json fields: per-top-level-key LWW registers with removal markers.
|
|
359
|
+
* Nested values are whole-value LWW per top-level key. A write that replaces the
|
|
360
|
+
* whole value with a non-object (null, a scalar, a json array) is a shape write;
|
|
361
|
+
* keys written before the newest such write are cleared.
|
|
362
|
+
*/
|
|
363
|
+
interface KeyMapFieldState {
|
|
364
|
+
k: 'map';
|
|
365
|
+
sh: {
|
|
366
|
+
s: Stamp;
|
|
367
|
+
obj: boolean;
|
|
368
|
+
v?: unknown;
|
|
369
|
+
} | null;
|
|
370
|
+
clr: Stamp | null;
|
|
371
|
+
keys: Record<string, KeyState>;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* `merge('counter')` fields: a base register (inserts and non-numeric writes) plus
|
|
375
|
+
* the deltas written after it. Value = base + sum of deltas in stamp order.
|
|
376
|
+
*/
|
|
377
|
+
interface CounterFieldState {
|
|
378
|
+
k: 'ctr';
|
|
379
|
+
base: {
|
|
380
|
+
s: Stamp;
|
|
381
|
+
v: unknown;
|
|
382
|
+
} | null;
|
|
383
|
+
/** Deltas written after `base`, sorted by stamp (older ones are pruned). */
|
|
384
|
+
d: Array<{
|
|
385
|
+
s: Stamp;
|
|
386
|
+
n: number;
|
|
387
|
+
}>;
|
|
388
|
+
/** Cached base + deltas, summed left to right in stamp order. */
|
|
389
|
+
val: unknown;
|
|
390
|
+
}
|
|
391
|
+
/** `merge('max')` / `merge('min')` fields: the extremum of every numeric write. */
|
|
392
|
+
interface ExtremumFieldState {
|
|
393
|
+
k: 'max' | 'min';
|
|
394
|
+
best: {
|
|
395
|
+
s: Stamp;
|
|
396
|
+
v: number;
|
|
397
|
+
} | null;
|
|
398
|
+
/** LWW register over non-numeric writes, used only while no numeric write exists. */
|
|
399
|
+
reg: {
|
|
400
|
+
s: Stamp;
|
|
401
|
+
v: unknown;
|
|
402
|
+
} | null;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Fields with a custom resolver (tier 3): the full write log in stamp order and
|
|
406
|
+
* the value folded through the resolver (`local` = the merged value so far).
|
|
407
|
+
*/
|
|
408
|
+
interface ResolverFieldState {
|
|
409
|
+
k: 'res';
|
|
410
|
+
e: FieldLogEntry[];
|
|
411
|
+
val: unknown;
|
|
412
|
+
/** Message of the newest resolver exception (the fold fell back to LWW there), if any. */
|
|
413
|
+
err?: string;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Richtext fields: a set of opaque Yjs updates (base64) plus a reset register for
|
|
417
|
+
* plain-string (whole-value) writes. Updates written before the newest reset are
|
|
418
|
+
* hidden.
|
|
419
|
+
*/
|
|
420
|
+
interface RichtextFieldState {
|
|
421
|
+
k: 'rt';
|
|
422
|
+
reset: {
|
|
423
|
+
s: Stamp;
|
|
424
|
+
v: unknown;
|
|
425
|
+
} | null;
|
|
426
|
+
/** base64 Yjs update -> newest stamp that wrote it. */
|
|
427
|
+
u: Record<string, Stamp>;
|
|
428
|
+
}
|
|
429
|
+
/** Per-field merge state. Every variant is a state-based CRDT (join-semilattice). */
|
|
430
|
+
type FieldState = RegisterFieldState | ElementSetFieldState | KeyMapFieldState | CounterFieldState | ExtremumFieldState | ResolverFieldState | RichtextFieldState;
|
|
431
|
+
/** The kind tag of a {@link FieldState}. */
|
|
432
|
+
type FoldFieldKind = FieldState['k'];
|
|
433
|
+
/**
|
|
434
|
+
* The merge state of one record: the snapshot every replica (client store, server
|
|
435
|
+
* stores, backup restore, compaction) persists. It is a pure function of the set of
|
|
436
|
+
* operations merged into it, so two replicas holding the same operations hold an
|
|
437
|
+
* identical state, whatever order they arrived in.
|
|
438
|
+
*
|
|
439
|
+
* Serialized with {@link serializeFoldState}; see that function for the on-disk
|
|
440
|
+
* format.
|
|
441
|
+
*/
|
|
442
|
+
interface FoldState {
|
|
443
|
+
/** Format version, {@link FOLD_STATE_VERSION}. */
|
|
444
|
+
v: number;
|
|
445
|
+
/** Collection of the record. */
|
|
446
|
+
c: string;
|
|
447
|
+
/** Record id. */
|
|
448
|
+
r: string;
|
|
449
|
+
/** Oldest insert (the record's creation). null until an insert is merged. */
|
|
450
|
+
cr: Stamp | null;
|
|
451
|
+
/** Newest insert or update (any write keeps the record alive). */
|
|
452
|
+
w: Stamp | null;
|
|
453
|
+
/** Newest delete (the tombstone register). */
|
|
454
|
+
d: Stamp | null;
|
|
455
|
+
/** Newest operation of any type. */
|
|
456
|
+
u: Stamp | null;
|
|
457
|
+
/** Per-field states. */
|
|
458
|
+
f: Record<string, FieldState>;
|
|
459
|
+
}
|
|
460
|
+
/** Merges the opaque Yjs updates of a richtext field into one update. */
|
|
461
|
+
type RichtextUpdateMerger = (updates: Uint8Array[]) => Uint8Array;
|
|
462
|
+
/** True when update `a`'s content is contained in update `b`'s (a adds nothing to b). */
|
|
463
|
+
type RichtextSubsumes = (a: Uint8Array, b: Uint8Array) => boolean;
|
|
464
|
+
/** Which merge traces {@link mergeOp} emits. */
|
|
465
|
+
type FoldTraceMode = 'conflicts' | 'all' | 'none';
|
|
466
|
+
/** Options shared by the fold entry points. */
|
|
467
|
+
interface FoldOptions {
|
|
468
|
+
/**
|
|
469
|
+
* Operations to leave out: the server terminally rejected them (or they are
|
|
470
|
+
* otherwise known never to have been stored). Ids, or a predicate.
|
|
471
|
+
*/
|
|
472
|
+
exclude?: ReadonlySet<string> | ((op: Operation) => boolean);
|
|
473
|
+
/**
|
|
474
|
+
* Merges richtext Yjs updates. Required to materialize a richtext field with
|
|
475
|
+
* more than one live update (pass `mergeYjsUpdates` from `@korajs/store`).
|
|
476
|
+
* Core has no Yjs dependency, so it cannot supply one itself.
|
|
477
|
+
*/
|
|
478
|
+
richtext?: RichtextUpdateMerger;
|
|
479
|
+
/**
|
|
480
|
+
* Optional richtext space bound: when given, an update whose content another
|
|
481
|
+
* update of the field contains, and whose stamp is not later, is dropped (it can
|
|
482
|
+
* never change a materialization). Devices persist a full Yjs snapshot per edit,
|
|
483
|
+
* so without it a field's state keeps every snapshot. Materializations are the
|
|
484
|
+
* same with or without it; every replica of one database must use the same setting
|
|
485
|
+
* for its serialized states to be byte-identical.
|
|
486
|
+
*/
|
|
487
|
+
richtextSubsumes?: RichtextSubsumes;
|
|
488
|
+
/** Which traces to emit. Default `'conflicts'`. */
|
|
489
|
+
traces?: FoldTraceMode;
|
|
490
|
+
/**
|
|
491
|
+
* Additional node ids whose writes are authoritative for
|
|
492
|
+
* `merge('server-authoritative')` fields: legacy (randomly generated) server node
|
|
493
|
+
* ids the server keeps advertising at the handshake. Every node id in the reserved
|
|
494
|
+
* `kora:server:` namespace is authoritative without being listed. Authoritative
|
|
495
|
+
* writes beat every non-authoritative write of such a field regardless of HLC;
|
|
496
|
+
* within a class the later write wins. Every replica must fold with the same set.
|
|
497
|
+
*/
|
|
498
|
+
authoritativeNodeIds?: ReadonlySet<string>;
|
|
499
|
+
/**
|
|
500
|
+
* Schema transforms applied at fold time (RT-84): an operation authored under another
|
|
501
|
+
* schema version is merged as `operationSchemaView(op, schema.version, transforms)`
|
|
502
|
+
* reads it. Stored operations are never rewritten. Transforms must be pure and
|
|
503
|
+
* deterministic, and every replica of a deployment must fold with the same ones (they
|
|
504
|
+
* are part of the fold plan fingerprint). Without transforms an operation folds as
|
|
505
|
+
* written.
|
|
506
|
+
*/
|
|
507
|
+
transforms?: readonly OperationTransform[];
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* A per-field merge decision made by {@link mergeOp}. It carries every
|
|
511
|
+
* {@link MergeTrace} field except `operationA`: the fold state keeps only the id of
|
|
512
|
+
* the operation that produced the prior value (`priorOperationId`), so the caller,
|
|
513
|
+
* which owns the operation log, turns it into a `MergeTrace` with
|
|
514
|
+
* {@link toMergeTrace}.
|
|
515
|
+
*/
|
|
516
|
+
interface FoldTrace extends Omit<MergeTrace, 'operationA' | 'operationB'> {
|
|
517
|
+
/** The operation being merged (`MergeTrace.operationB`). */
|
|
518
|
+
operation: Operation;
|
|
519
|
+
/** Id of the operation that last wrote the field before this merge, or null. */
|
|
520
|
+
priorOperationId: string | null;
|
|
521
|
+
/**
|
|
522
|
+
* True when the incoming write was concurrent with the field's prior value (the
|
|
523
|
+
* writer's base differs from it, or the write lost to a newer one).
|
|
524
|
+
*/
|
|
525
|
+
conflict: boolean;
|
|
526
|
+
/** Set when a custom resolver threw: the fold fell back to last-write-wins. */
|
|
527
|
+
error?: string;
|
|
528
|
+
}
|
|
529
|
+
/** The result of merging one operation. */
|
|
530
|
+
interface MergeOpResult {
|
|
531
|
+
state: FoldState;
|
|
532
|
+
traces: FoldTrace[];
|
|
533
|
+
/** False when the operation changed nothing (a duplicate, an excluded op, a no-op write). */
|
|
534
|
+
changed: boolean;
|
|
535
|
+
}
|
|
536
|
+
/** The result of folding a set of operations from scratch. */
|
|
537
|
+
interface FoldRecordResult {
|
|
538
|
+
/** null when no (non-excluded) operation was given. */
|
|
539
|
+
state: FoldState | null;
|
|
540
|
+
traces: FoldTrace[];
|
|
541
|
+
}
|
|
542
|
+
/** Per-field versions of a live record, the fold's equivalent of `_field_versions`. */
|
|
543
|
+
interface FoldFieldVersions {
|
|
544
|
+
/** Field name -> HLC of the newest write that affected the field. */
|
|
545
|
+
fields: Record<string, HLCTimestamp>;
|
|
546
|
+
/** HLC of the record's creation (oldest insert). */
|
|
547
|
+
created: HLCTimestamp;
|
|
548
|
+
/** HLC of the newest operation of any type. */
|
|
549
|
+
latest: HLCTimestamp;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Generate CREATE TABLE and CREATE INDEX SQL for a single collection.
|
|
554
|
+
*
|
|
555
|
+
* @param collectionName - The collection name
|
|
556
|
+
* @param collection - The collection definition
|
|
557
|
+
* @param relations - Optional relations for FK references
|
|
558
|
+
* @returns An array of SQL statements (CREATE TABLE + CREATE INDEX)
|
|
559
|
+
*/
|
|
560
|
+
declare function generateSQL(collectionName: string, collection: CollectionDefinition, relations?: Record<string, RelationDefinition>): string[];
|
|
561
|
+
/**
|
|
562
|
+
* Name of the index on `field` of `collection`.
|
|
563
|
+
*
|
|
564
|
+
* The collection name's length is part of the name, so two (collection, field)
|
|
565
|
+
* pairs can never produce the same index name. The previous scheme,
|
|
566
|
+
* `idx_<collection>_<field>`, gave `a_b`.`c` and `a`.`b_c` the same name, and the
|
|
567
|
+
* second `CREATE INDEX IF NOT EXISTS` silently did nothing (STORE-15).
|
|
568
|
+
*
|
|
569
|
+
* @param collection - Collection (table) name
|
|
570
|
+
* @param field - Indexed field (column) name
|
|
571
|
+
* @returns A collision-free index name
|
|
572
|
+
*/
|
|
573
|
+
declare function collectionIndexName(collection: string, field: string): string;
|
|
574
|
+
/**
|
|
575
|
+
* Name the pre-beta.13 scheme gave the index on `field` of `collection`. Only
|
|
576
|
+
* used to find and drop those indexes once their replacements exist.
|
|
577
|
+
*/
|
|
578
|
+
declare function legacyCollectionIndexName(collection: string, field: string): string;
|
|
579
|
+
/**
|
|
580
|
+
* Prefix of the `generateFullDDL` statement `--kora:schema-ceiling <version>` (RT-109).
|
|
581
|
+
* It is a SQL comment, so executing it does nothing; Kora's storage adapters read the
|
|
582
|
+
* database's stored schema version there and refuse to run the rest of the DDL against a
|
|
583
|
+
* database a newer build already migrated.
|
|
584
|
+
*/
|
|
585
|
+
declare const SCHEMA_CEILING_DIRECTIVE = "--kora:schema-ceiling";
|
|
586
|
+
/**
|
|
587
|
+
* Generate the full DDL for all collections plus metadata tables.
|
|
588
|
+
*
|
|
589
|
+
* @param schema - The complete schema definition
|
|
590
|
+
* @returns An array of all SQL statements needed to initialize the database
|
|
591
|
+
*/
|
|
592
|
+
declare function generateFullDDL(schema: SchemaDefinition): string[];
|
|
593
|
+
/**
|
|
594
|
+
* Render a string as a SQL string literal, doubling embedded single quotes
|
|
595
|
+
* (SEC-9b). Valid in SQLite and in Postgres with standard_conforming_strings
|
|
596
|
+
* (the default since 9.1), where a backslash is an ordinary character.
|
|
597
|
+
*
|
|
598
|
+
* @param value - The string to quote
|
|
599
|
+
* @returns The quoted literal, e.g. `'don''t know'`
|
|
600
|
+
*/
|
|
601
|
+
declare function sqlStringLiteral(value: string): string;
|
|
602
|
+
/**
|
|
603
|
+
* Render a schema default value as a SQL literal for a `DEFAULT` clause. Strings
|
|
604
|
+
* and JSON-serialized arrays/objects are quoted with `sqlStringLiteral`, so a
|
|
605
|
+
* default containing a quote yields valid DDL instead of breaking it (SEC-9b).
|
|
606
|
+
*
|
|
607
|
+
* @param value - The field's default value
|
|
608
|
+
* @returns A SQL literal
|
|
609
|
+
*/
|
|
610
|
+
declare function sqlDefaultLiteral(value: unknown): string;
|
|
611
|
+
/**
|
|
612
|
+
* Render the `CHECK (col IN (...))` constraint of an enum field, each value
|
|
613
|
+
* quoted with `sqlStringLiteral` (SEC-9b). No longer emitted by Kora's DDL (RT-101:
|
|
614
|
+
* enum membership is enforced by validation only); kept for tooling and tests.
|
|
615
|
+
*
|
|
616
|
+
* @param fieldName - The enum column
|
|
617
|
+
* @param values - The enum's allowed values
|
|
618
|
+
* @returns The CHECK clause
|
|
619
|
+
*/
|
|
620
|
+
declare function enumCheckConstraint(fieldName: string, values: readonly string[]): string;
|
|
621
|
+
|
|
622
|
+
export { verifyOperationIntegrity as A, SCHEMA_CEILING_DIRECTIVE as B, type CounterFieldState as C, DEFAULT_OPERATION_HASH_VERSION as D, type ElementSetFieldState as E, type FoldState as F, canonicalize as G, type HashableOperation as H, collectionIndexName as I, enumCheckConstraint as J, type KeyMapFieldState as K, legacyCollectionIndexName as L, type MergeOpResult as M, sqlDefaultLiteral as N, type OperationTransform as O, sqlStringLiteral as P, validateOperationParams as Q, type RegisterFieldState as R, type Stamp as S, type FoldOptions as a, type FoldRecordResult as b, type FoldFieldVersions as c, type FoldTrace as d, type CreateOperationOptions as e, type ElementState as f, type ExtremumFieldState as g, FOLD_STATE_VERSION as h, type FieldLogEntry as i, type FieldState as j, type FoldFieldKind as k, type FoldTraceMode as l, HybridLogicalClock as m, type KeyState as n, MAX_LOGICAL as o, type OperationHashVersion as p, type ResolverFieldState as q, type RichtextFieldState as r, type RichtextSubsumes as s, type RichtextUpdateMerger as t, computeOperationId as u, createOperation as v, generateFullDDL as w, generateSQL as x, isValidOperation as y, verifyOperationId as z };
|