@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.
@@ -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 };