@korajs/core 1.0.0-beta.12 → 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-Cv5HfCQP.d.ts → build-scope-map-CIFa58oA.d.ts} +1 -1
- package/dist/{build-scope-map-CItP1gsz.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 +2526 -204
- 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 +2012 -337
- 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-D7JCtQSD.d.cts +0 -1010
- package/dist/events-D7JCtQSD.d.ts +0 -1010
- package/dist/operation-BX4ok7U4.d.ts +0 -164
- package/dist/operation-BXvJ6RiA.d.cts +0 -164
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { R as RandomSource, B as BlobRef, C as CollectionDefinition,
|
|
2
|
-
export { j as
|
|
3
|
-
|
|
4
|
-
export { S as
|
|
1
|
+
import { R as RandomSource, B as BlobRef, C as CollectionDefinition, O as OperationInput, A as AtomicOp, a as Operation, H as HLCTimestamp, S as SchemaDefinition, M as MergeTrace, F as FieldBuilder, b as CustomResolver, c as MigrationDefinition, d as FieldDescriptor, e as OperationType, V as VersionVector, f as MigrationStep, g as StateMachineConstraint, T as TransitionMap, h as TransitionValidationResult } from './events-BSU-0z5O.js';
|
|
2
|
+
export { i as ArrayAtomicOpSentinel, j as ArrayFieldBuilder, k as AtomicOpSentinel, l as AtomicOpType, m as BackfillOptions, n as CONNECTION_QUALITIES, o as ConnectionQuality, p as Constraint, E as EncryptedEnvelopeField, q as EncryptedOperationEnvelope, r as EnumFieldBuilder, s as FieldInput, t as FieldKind, u as FieldKindToType, v as FieldMap, w as FieldMergeStrategy, x as FieldOutput, y as FieldTypeInfo, I as InferFieldInput, z as InferFieldType, D as InferInsert, G as InferInsertInput, J as InferRecord, K as InferRecordField, L as InferUpdate, N as InferUpdateField, P as InferUpdateInput, Q as JsonFieldBuilder, U as KoraEvent, W as KoraEventByType, X as KoraEventEmitter, Y as KoraEventListener, Z as KoraEventType, _ as MERGE_STRATEGIES, $ as MergeStrategy, a0 as MigrationBuilder, a1 as NumericAtomicOpSentinel, a2 as ObjectFieldBuilder, a3 as ObjectFieldValue, a4 as OnDeleteAction, a5 as RelationDefinition, a6 as RelationType, a7 as RichtextInput, a8 as RollbackBuilder, a9 as ScalarFieldBuilder, aa as ScalarFieldKind, ab as SecretFieldBuilder, ac as SecretMode, ad as SequenceConfig, ae as Simplify, af as StateMachineDefinition, ag as SyncDiagnosticsSnapshot, ah as SyncRuleDefinition, ai as TimeSource, aj as applyAtomicOp, ak as isAtomicOp, al as migrate, am as op, an as resolveAtomicOp, ao as t, ap as toAtomicOp } from './events-BSU-0z5O.js';
|
|
3
|
+
import { F as FoldState, a as FoldOptions, b as FoldRecordResult, c as FoldFieldVersions, M as MergeOpResult, O as OperationTransform, d as FoldTrace } from './sql-gen-ByOZIQU-.js';
|
|
4
|
+
export { C as CounterFieldState, e as CreateOperationOptions, D as DEFAULT_OPERATION_HASH_VERSION, E as ElementSetFieldState, f as ElementState, g as ExtremumFieldState, h as FOLD_STATE_VERSION, i as FieldLogEntry, j as FieldState, k as FoldFieldKind, l as FoldTraceMode, H as HashableOperation, m as HybridLogicalClock, K as KeyMapFieldState, n as KeyState, o as MAX_LOGICAL, p as OperationHashVersion, R as RegisterFieldState, q as ResolverFieldState, r as RichtextFieldState, s as RichtextSubsumes, t as RichtextUpdateMerger, S as Stamp, u as computeOperationId, v as createOperation, w as generateFullDDL, x as generateSQL, y as isValidOperation, z as verifyOperationId, A as verifyOperationIntegrity } from './sql-gen-ByOZIQU-.js';
|
|
5
|
+
import { S as ScopeMap } from './build-scope-map-CIFa58oA.js';
|
|
6
|
+
export { b as buildScopeMap } from './build-scope-map-CIFa58oA.js';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Base error class for all Kora errors.
|
|
@@ -28,6 +30,18 @@ declare class SchemaValidationError extends KoraError {
|
|
|
28
30
|
declare class OperationError extends KoraError {
|
|
29
31
|
constructor(message: string, context?: Record<string, unknown>);
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Thrown by the local write path when an operation would exceed `maxOperationBytes`
|
|
35
|
+
* (RT-86): the server refuses such an operation, so the write is refused before it is
|
|
36
|
+
* accepted, with nothing written.
|
|
37
|
+
*/
|
|
38
|
+
declare class OperationTooLargeError extends KoraError {
|
|
39
|
+
readonly collection: string;
|
|
40
|
+
readonly recordId: string;
|
|
41
|
+
readonly bytes: number;
|
|
42
|
+
readonly maxBytes: number;
|
|
43
|
+
constructor(collection: string, recordId: string, bytes: number, maxBytes: number);
|
|
44
|
+
}
|
|
31
45
|
/**
|
|
32
46
|
* Thrown when a merge conflict cannot be automatically resolved.
|
|
33
47
|
*/
|
|
@@ -325,131 +339,113 @@ declare function isApplyFailure(result: ApplyResult): boolean;
|
|
|
325
339
|
declare function defaultApplyFailureReason(result: Exclude<ApplyResult, 'applied' | 'duplicate'>, overrides?: Partial<ApplyFailureReason>): ApplyFailureReason;
|
|
326
340
|
|
|
327
341
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* ```typescript
|
|
332
|
-
* await app.stock.update(id, { quantity: op.increment(-5) })
|
|
333
|
-
* ```
|
|
334
|
-
*
|
|
335
|
-
* The operation intent is preserved in the Operation for merge:
|
|
336
|
-
* concurrent increments compose (sum of deltas) instead of LWW.
|
|
337
|
-
*/
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Sentinel marker for detecting atomic op objects in update data.
|
|
341
|
-
* Uses Symbol.for() so the sentinel survives module boundary crossings.
|
|
342
|
-
*/
|
|
343
|
-
declare const KORA_ATOMIC_OP: unique symbol;
|
|
344
|
-
/** String key used for the sentinel symbol — exported for internal use */
|
|
345
|
-
declare const KORA_ATOMIC_OP_KEY: typeof KORA_ATOMIC_OP;
|
|
346
|
-
/**
|
|
347
|
-
* Sentinel object returned by op.* helpers.
|
|
348
|
-
* Detected by Collection.update() and resolved to concrete values.
|
|
342
|
+
* Thrown when an operation (or a record value) holds a value that has no canonical
|
|
343
|
+
* JSON form: the op log and the wire are JSON, so such a value would be stored and
|
|
344
|
+
* synced as something other than what was written and hashed.
|
|
349
345
|
*/
|
|
350
|
-
|
|
351
|
-
readonly
|
|
352
|
-
readonly
|
|
353
|
-
|
|
346
|
+
declare class NonCanonicalValueError extends KoraError {
|
|
347
|
+
readonly path: string;
|
|
348
|
+
readonly receivedType: string;
|
|
349
|
+
constructor(message: string, path: string, receivedType: string);
|
|
354
350
|
}
|
|
355
351
|
/**
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
*
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
*
|
|
398
|
-
* // Append to an array
|
|
399
|
-
* await app.todos.update(id, { tags: op.append('urgent') })
|
|
400
|
-
* ```
|
|
401
|
-
*/
|
|
402
|
-
declare const op: {
|
|
403
|
-
/**
|
|
404
|
-
* Increment a number field by the given amount.
|
|
405
|
-
* Concurrent increments compose: the sum of all deltas is applied to the base.
|
|
406
|
-
*
|
|
407
|
-
* @param n - The amount to increment (use negative values to decrement)
|
|
408
|
-
*/
|
|
409
|
-
increment(n: number): AtomicOpSentinel;
|
|
410
|
-
/**
|
|
411
|
-
* Decrement a number field by the given amount.
|
|
412
|
-
* Syntactic sugar for `op.increment(-n)`.
|
|
413
|
-
*
|
|
414
|
-
* @param n - The amount to decrement
|
|
415
|
-
*/
|
|
416
|
-
decrement(n: number): AtomicOpSentinel;
|
|
417
|
-
/**
|
|
418
|
-
* Set the field to the maximum of the current value and the given value.
|
|
419
|
-
* Concurrent max operations take the maximum of all values.
|
|
420
|
-
*
|
|
421
|
-
* @param n - The value to compare against the current value
|
|
422
|
-
*/
|
|
423
|
-
max(n: number): AtomicOpSentinel;
|
|
424
|
-
/**
|
|
425
|
-
* Set the field to the minimum of the current value and the given value.
|
|
426
|
-
* Concurrent min operations take the minimum of all values.
|
|
427
|
-
*
|
|
428
|
-
* @param n - The value to compare against the current value
|
|
429
|
-
*/
|
|
430
|
-
min(n: number): AtomicOpSentinel;
|
|
431
|
-
/**
|
|
432
|
-
* Append an item to an array field.
|
|
433
|
-
* Concurrent appends include all appended items.
|
|
434
|
-
*
|
|
435
|
-
* @param item - The item to append to the array
|
|
436
|
-
*/
|
|
437
|
-
append(item: unknown): AtomicOpSentinel;
|
|
438
|
-
/**
|
|
439
|
-
* Remove an item from an array field (by value equality).
|
|
440
|
-
* Concurrent removes of the same item are idempotent.
|
|
441
|
-
*
|
|
442
|
-
* @param item - The item to remove from the array
|
|
443
|
-
*/
|
|
444
|
-
remove(item: unknown): AtomicOpSentinel;
|
|
445
|
-
};
|
|
352
|
+
* The one canonical form of an operation body (RT-72 family: RT-79, RT-80, RT-83).
|
|
353
|
+
*
|
|
354
|
+
* The op log, the wire (JSON and protobuf, whose data is JSON) and every store hold an
|
|
355
|
+
* operation as JSON. An operation's id is a hash of its body. So the body is put in its
|
|
356
|
+
* canonical form ONCE, when the operation is created (and when a legacy operation is
|
|
357
|
+
* ingested or rebuilt), and the canonical body is what is hashed, stored, sent and
|
|
358
|
+
* folded: the four can never disagree, by construction.
|
|
359
|
+
*
|
|
360
|
+
* | Value | Canonical form |
|
|
361
|
+
* |-----------------------------------------|-------------------------------------------------|
|
|
362
|
+
* | `undefined` member of an insert's data | absent |
|
|
363
|
+
* | `undefined` member of an update's data | `null`: the field is CLEARED (beta.12 meaning) |
|
|
364
|
+
* | `undefined` member of an update's `previousData` | `null` (the field held no value) |
|
|
365
|
+
* | `undefined` member inside an object value | absent (as JSON drops it); a `t.json()` field refuses it at validation |
|
|
366
|
+
* | `undefined` array element | `null` (as JSON writes it) |
|
|
367
|
+
* | `undefined` atomic op | absent |
|
|
368
|
+
* | `-0` | `0` (JSON writes `0`) |
|
|
369
|
+
* | valid `Date` | its ISO 8601 string (`toISOString()`), as JSON |
|
|
370
|
+
* | `Uint8Array` / `ArrayBuffer` | `{ $koraBytes: <base64> }` (the op-log binary form) |
|
|
371
|
+
* | plain object (`Object.prototype` or `null` prototype) | plain object, members canonical |
|
|
372
|
+
* | array (dense) | array, elements canonical |
|
|
373
|
+
* | string, finite number, boolean, `null` | itself |
|
|
374
|
+
* | invalid `Date`, `NaN`, `±Infinity`, `BigInt`, function, symbol, `Map`, `Set`, `WeakMap`, `RegExp`, `Promise`, any other class instance, an object with a `toJSON` method, an own `__proto__` key, a circular reference | refused: {@link NonCanonicalValueError} naming the path and the fix |
|
|
375
|
+
*
|
|
376
|
+
* Canonicalization is idempotent: a canonical body is returned unchanged (same
|
|
377
|
+
* references where nothing changed).
|
|
378
|
+
*
|
|
379
|
+
* @param input - The operation body (as given to `createOperation`, or a stored operation)
|
|
380
|
+
* @returns The body with `data`, `previousData` and `atomicOps` canonical
|
|
381
|
+
* @throws {NonCanonicalValueError} For a value with no canonical form
|
|
382
|
+
*/
|
|
383
|
+
declare function canonicalizeOperationBody<T extends CanonicalizableBody>(input: T): T;
|
|
384
|
+
/** The members of an operation body that {@link canonicalizeOperationBody} reads. */
|
|
385
|
+
interface CanonicalizableBody {
|
|
386
|
+
type: OperationInput['type'];
|
|
387
|
+
collection: string;
|
|
388
|
+
recordId: string;
|
|
389
|
+
data: Record<string, unknown> | null;
|
|
390
|
+
previousData: Record<string, unknown> | null;
|
|
391
|
+
atomicOps?: Record<string, AtomicOp>;
|
|
392
|
+
}
|
|
446
393
|
/**
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
|
|
452
|
-
|
|
394
|
+
* The canonical form of one value inside an operation body or a record field (see
|
|
395
|
+
* {@link canonicalizeOperationBody} for the table). Exported so schema validation
|
|
396
|
+
* produces exactly the values an operation will carry.
|
|
397
|
+
*
|
|
398
|
+
* @param value - Any value
|
|
399
|
+
* @param path - Where the value is, for the error message (e.g. `notes.extra.when`)
|
|
400
|
+
* @returns The canonical value (the same reference when it already is)
|
|
401
|
+
* @throws {NonCanonicalValueError} For a value with no canonical form
|
|
402
|
+
*/
|
|
403
|
+
declare function canonicalValue(value: unknown, path: string): unknown;
|
|
404
|
+
/**
|
|
405
|
+
* A legacy update in its canonical body when its id PROVES the beta.12 clear (RT-85):
|
|
406
|
+
* the id is the content hash of the body with every `previousData` key absent from
|
|
407
|
+
* `data` restored as `null` (version 1, or version 2 with a lost declaration), and not
|
|
408
|
+
* of the body as it is. Such an id can only come from a writer that hashed the clear,
|
|
409
|
+
* so the restored body is the one its author wrote and applied; a body a schema
|
|
410
|
+
* transform or anything else rewrote never proves it.
|
|
411
|
+
*
|
|
412
|
+
* Used where a legacy body enters a replica without a known provenance: a device's
|
|
413
|
+
* inbound delivery, and the one-time canonicalization of stored logs. The server's
|
|
414
|
+
* ingest of a protocol-1 upload applies the same rule (plus the unverifiable
|
|
415
|
+
* protocol-1 case, for the session's own node).
|
|
416
|
+
*
|
|
417
|
+
* @param op - Any operation
|
|
418
|
+
* @returns The canonical operation, or `op` itself when nothing is proven
|
|
419
|
+
*/
|
|
420
|
+
declare function canonicalizeProvenLegacyClear<T extends Operation>(op: T): Promise<T>;
|
|
421
|
+
/**
|
|
422
|
+
* The canonical body of a legacy (version-1, beta.12) update (RT-71, RT-83): every
|
|
423
|
+
* `previousData` key absent from `data` is a clear, restored as `null`. beta.12 wrote
|
|
424
|
+
* `previousData[key]` for every key it validated, so such a key held `undefined` in
|
|
425
|
+
* `data`: beta.12 hashed it as `null`, applied it as a cleared field, and its JSON log
|
|
426
|
+
* and upload dropped it. `null` and `undefined` hash identically under version 1, so
|
|
427
|
+
* the id is unchanged.
|
|
428
|
+
*
|
|
429
|
+
* Applied ONCE, where a genuine beta.12 body is identified (RT-85), never at fold time:
|
|
430
|
+
* the server's ingest of a protocol-1 upload, a device's own beta.12 log at the
|
|
431
|
+
* upgrade, and any body whose id proves the clear
|
|
432
|
+
* ({@link canonicalizeProvenLegacyClear}). The result is stored, so the server, peers
|
|
433
|
+
* and the upgraded writer fold the same body. A no-op
|
|
434
|
+
* for anything else (a declared version 2, an envelope, an insert or delete, an update
|
|
435
|
+
* whose data covers its previousData). Idempotent.
|
|
436
|
+
*
|
|
437
|
+
* @param op - Any operation
|
|
438
|
+
* @returns The operation with the restored data, or `op` itself
|
|
439
|
+
*/
|
|
440
|
+
declare function canonicalizeLegacyOperation<T extends LegacyBody>(op: T): T;
|
|
441
|
+
/** The members of an operation {@link canonicalizeLegacyOperation} reads. */
|
|
442
|
+
interface LegacyBody {
|
|
443
|
+
type: Operation['type'];
|
|
444
|
+
data: Record<string, unknown> | null;
|
|
445
|
+
previousData: Record<string, unknown> | null;
|
|
446
|
+
hashVersion?: Operation['hashVersion'];
|
|
447
|
+
encrypted?: Operation['encrypted'];
|
|
448
|
+
}
|
|
453
449
|
|
|
454
450
|
/** One operation's contribution to a record's materialized state. */
|
|
455
451
|
interface ReplayOperation {
|
|
@@ -462,7 +458,35 @@ interface ReplayOperation {
|
|
|
462
458
|
* last-write-wins.
|
|
463
459
|
*/
|
|
464
460
|
atomicOps?: Record<string, AtomicOp> | null;
|
|
461
|
+
/**
|
|
462
|
+
* For updates: the values the writer saw before its change. Array fields use it
|
|
463
|
+
* as the base of a three-way set merge (see {@link mergeArraySet}). Absent means
|
|
464
|
+
* array fields resolve by last-write-wins.
|
|
465
|
+
*/
|
|
466
|
+
previousData?: Record<string, unknown> | null;
|
|
465
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Three-way set merge for array fields, shared by the client merge engine
|
|
470
|
+
* (`addWinsSet`) and the record fold, so client and server agree.
|
|
471
|
+
*
|
|
472
|
+
* result = (local ∩ remote) ∪ (local − base) ∪ (remote − base)
|
|
473
|
+
*
|
|
474
|
+
* A base element is kept only if BOTH sides kept it (a one-sided removal is never
|
|
475
|
+
* undone by the other side's unchanged copy); a non-base element is kept if EITHER
|
|
476
|
+
* side has it. Elements are compared by `JSON.stringify`.
|
|
477
|
+
*
|
|
478
|
+
* Order is role-independent: kept base elements first, in base order, then every
|
|
479
|
+
* other kept element sorted by its serialized form. Swapping `local` and `remote`
|
|
480
|
+
* yields the identical array.
|
|
481
|
+
*
|
|
482
|
+
* Interim (S1): W7 replaces this pairwise rule with a per-element LWW set.
|
|
483
|
+
*
|
|
484
|
+
* @param local - One side's array after its modifications
|
|
485
|
+
* @param remote - The other side's array after its modifications
|
|
486
|
+
* @param base - The common ancestor both sides edited from
|
|
487
|
+
* @returns The merged array
|
|
488
|
+
*/
|
|
489
|
+
declare function mergeArraySet(local: unknown[], remote: unknown[], base: unknown[]): unknown[];
|
|
466
490
|
/**
|
|
467
491
|
* Replay a list of operations (MUST be pre-sorted in HLC total order —
|
|
468
492
|
* wallTime, then logical, then nodeId) to produce the current state of a single
|
|
@@ -476,6 +500,10 @@ interface ReplayOperation {
|
|
|
476
500
|
* the current op always wins that last-write-wins comparison, so the result is
|
|
477
501
|
* deterministic and identical on every replica that folds the same operation set.
|
|
478
502
|
*
|
|
503
|
+
* Plain array writes that carry `previousData` are merged as a set against that
|
|
504
|
+
* base (see {@link mergeArraySet}) when a concurrent writer changed the field since
|
|
505
|
+
* the base, so the fold and the client merge agree on one-sided removals.
|
|
506
|
+
*
|
|
479
507
|
* This is the single definition of atomic materialization shared by the client
|
|
480
508
|
* (`@korajs/store` apply pipeline) and the server (`@korajs/server` materialization),
|
|
481
509
|
* so both converge to the same value for any number of concurrent atomic writers.
|
|
@@ -484,6 +512,446 @@ interface ReplayOperation {
|
|
|
484
512
|
* or never inserted.
|
|
485
513
|
*/
|
|
486
514
|
declare function replayOperationsForRecord(ops: ReplayOperation[]): Record<string, unknown> | null;
|
|
515
|
+
/** A {@link ReplayOperation} with the HLC timestamp it was written at. */
|
|
516
|
+
interface VersionedReplayOperation {
|
|
517
|
+
type: string;
|
|
518
|
+
data: Record<string, unknown> | null;
|
|
519
|
+
timestamp: HLCTimestamp;
|
|
520
|
+
}
|
|
521
|
+
/** Per-field versions of a live record, as folded by {@link replayFieldVersionsForRecord}. */
|
|
522
|
+
interface RecordFieldVersions {
|
|
523
|
+
/** Field name -> HLC of the write that produced the field's current value. */
|
|
524
|
+
fields: Record<string, HLCTimestamp>;
|
|
525
|
+
/** The record's first operation (its creation; the server's `_created_at`). */
|
|
526
|
+
created: HLCTimestamp;
|
|
527
|
+
/** The record's newest operation. */
|
|
528
|
+
latest: HLCTimestamp;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Fold the per-field versions of one record, mirroring {@link replayOperationsForRecord}
|
|
532
|
+
* exactly (RT-27): an insert resets the field set and stamps every field it carries,
|
|
533
|
+
* an update stamps the fields it writes (composed atomic and array writes included:
|
|
534
|
+
* the field's version is its last writer's), a delete hides the record until a later
|
|
535
|
+
* write revives it. Operations MUST be pre-sorted in HLC total order.
|
|
536
|
+
*
|
|
537
|
+
* @param ops - The record's operations in HLC order
|
|
538
|
+
* @returns The live record's field versions, or null when it is deleted or absent
|
|
539
|
+
*/
|
|
540
|
+
declare function replayFieldVersionsForRecord(ops: readonly VersionedReplayOperation[]): RecordFieldVersions | null;
|
|
541
|
+
/**
|
|
542
|
+
* Expand operations for a record fold (RT-27). A scope-entry insert carrying
|
|
543
|
+
* per-field versions restates the record's CURRENT values, each produced at its own
|
|
544
|
+
* version; folding it as one insert at a single timestamp would misplace every field
|
|
545
|
+
* relative to the receiver's own writes. It is expanded into an insert at its
|
|
546
|
+
* `timestamp` (the record's creation) followed by one update per newer field version,
|
|
547
|
+
* so a fold in HLC order puts each field exactly where its writer did. Other
|
|
548
|
+
* operations pass through unchanged. The result is sorted in HLC total order (stable,
|
|
549
|
+
* so an expansion keeps insert-before-update on equal timestamps).
|
|
550
|
+
*
|
|
551
|
+
* @param ops - Operations of one record, in any order
|
|
552
|
+
* @returns The operations to fold, in HLC order
|
|
553
|
+
*/
|
|
554
|
+
declare function expandFieldVersionedOperations(ops: readonly Operation[]): Operation[];
|
|
555
|
+
|
|
556
|
+
/** `FoldTrace.field` of a record-level (delete vs write) decision. */
|
|
557
|
+
declare const FOLD_RECORD_TRACE_FIELD = "*";
|
|
558
|
+
/**
|
|
559
|
+
* An empty fold state for one record: no operation merged yet.
|
|
560
|
+
*
|
|
561
|
+
* @param collection - The record's collection
|
|
562
|
+
* @param recordId - The record's id
|
|
563
|
+
*/
|
|
564
|
+
declare function createFoldState(collection: string, recordId: string): FoldState;
|
|
565
|
+
/**
|
|
566
|
+
* Merge one operation into a record's fold state.
|
|
567
|
+
*
|
|
568
|
+
* The merge is a join on a per-field CRDT, so it is commutative, associative and
|
|
569
|
+
* idempotent: any replica that has merged the same set of operations, in any
|
|
570
|
+
* order, any number of times, holds an identical state. Cost is O(fields the
|
|
571
|
+
* operation touches) except for the out-of-order cases documented per kind (a late
|
|
572
|
+
* write into an atomic or resolver chain refolds that field's chain).
|
|
573
|
+
*
|
|
574
|
+
* Record-level semantics:
|
|
575
|
+
* - The record exists once an insert has been merged.
|
|
576
|
+
* - Delete vs write is last-write-wins on the record: the record is live iff its
|
|
577
|
+
* newest insert/update is later (HLC, then op id) than its newest delete. A
|
|
578
|
+
* delete keeps the field states; a later write revives the record with every
|
|
579
|
+
* field's merged value (the same visible result as the pre-W7 replay for an
|
|
580
|
+
* update after a delete).
|
|
581
|
+
* - An insert onto an existing row merges per field; it is not a reset.
|
|
582
|
+
*
|
|
583
|
+
* @param state - The record's current state (not mutated)
|
|
584
|
+
* @param op - The operation (must belong to the same collection and record)
|
|
585
|
+
* @param schema - The schema; resolvers and merge strategies come from it
|
|
586
|
+
* @param options - Exclusion, richtext merger (traces only), trace mode, schema
|
|
587
|
+
* transforms (the operation is merged as `operationSchemaView` reads it for
|
|
588
|
+
* `schema.version`)
|
|
589
|
+
* @returns The new state, the merge traces, and whether anything changed
|
|
590
|
+
*/
|
|
591
|
+
declare function mergeOp(state: FoldState, input: Operation, schema: SchemaDefinition, options?: FoldOptions): MergeOpResult;
|
|
592
|
+
/**
|
|
593
|
+
* Fold a record's operations from scratch: the reference definition of the
|
|
594
|
+
* record's state. Equal to merging the operations one by one with
|
|
595
|
+
* {@link mergeOp} in any order.
|
|
596
|
+
*
|
|
597
|
+
* @param ops - The record's operations, in any order, duplicates allowed
|
|
598
|
+
* @param schema - The schema
|
|
599
|
+
* @param options - `exclude` leaves out ops the server terminally rejected; traces
|
|
600
|
+
* default to `'none'` here (pass `traces` to get them)
|
|
601
|
+
* @returns The state (null if no op remains after exclusion) and the traces
|
|
602
|
+
*/
|
|
603
|
+
declare function foldRecord(ops: readonly Operation[], schema: SchemaDefinition, options?: FoldOptions): FoldRecordResult;
|
|
604
|
+
/**
|
|
605
|
+
* The record's materialized field values (without `id`), or null when it does not
|
|
606
|
+
* exist (no insert merged) or is deleted. Keys are in sorted order.
|
|
607
|
+
*
|
|
608
|
+
* @param state - The record's fold state
|
|
609
|
+
* @param options - `richtext` is required when a richtext field has concurrent updates
|
|
610
|
+
*/
|
|
611
|
+
declare function materialize(state: FoldState, options?: Pick<FoldOptions, 'richtext'>): Record<string, unknown> | null;
|
|
612
|
+
/** True when the record exists and its newest write is later than its newest delete. */
|
|
613
|
+
declare function isFoldStateLive(state: FoldState): boolean;
|
|
614
|
+
/**
|
|
615
|
+
* Join two replicas' states of the same record (state-based CRDT merge):
|
|
616
|
+
* `joinStates(foldRecord(A).state, foldRecord(B).state) = foldRecord(A ∪ B).state`.
|
|
617
|
+
* Commutative, associative and idempotent. Used to merge a restored backup or a
|
|
618
|
+
* peer's snapshot without replaying its operations.
|
|
619
|
+
*
|
|
620
|
+
* @param a - One state
|
|
621
|
+
* @param b - The other state (same collection and record)
|
|
622
|
+
* @param schema - The schema (resolver fields refold through their resolver)
|
|
623
|
+
*/
|
|
624
|
+
declare function joinStates(a: FoldState, b: FoldState, schema: SchemaDefinition): FoldState;
|
|
625
|
+
/**
|
|
626
|
+
* Each field's newest affecting write as a serialized HLC (`HybridLogicalClock.
|
|
627
|
+
* serialize`), regardless of liveness: the `_field_versions` column of a stored row,
|
|
628
|
+
* without a deserialize/serialize round trip. Keys in sorted order.
|
|
629
|
+
*/
|
|
630
|
+
declare function getFoldFieldVersionStrings(state: FoldState): Record<string, string>;
|
|
631
|
+
/**
|
|
632
|
+
* Per-field versions of a live record (the fold's `_field_versions`): each field's
|
|
633
|
+
* newest affecting write, plus the record's creation and newest operation. null
|
|
634
|
+
* when the record is not live.
|
|
635
|
+
*/
|
|
636
|
+
declare function getFoldFieldVersions(state: FoldState): FoldFieldVersions | null;
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Serialize a record's fold state for storage (the client `_fold_state` column,
|
|
640
|
+
* the server stores' fold-record rows, backups, compaction snapshots).
|
|
641
|
+
*
|
|
642
|
+
* The output is canonical JSON (sorted keys), so two replicas holding the same
|
|
643
|
+
* operations write byte-identical strings. Format, version 2 (version 1, Stage A,
|
|
644
|
+
* keyed array elements by value only and is refused: such a state is re-folded):
|
|
645
|
+
*
|
|
646
|
+
* ```
|
|
647
|
+
* {
|
|
648
|
+
* "v": 2, // format version
|
|
649
|
+
* "c": "todos", "r": "<id>", // collection, record id
|
|
650
|
+
* "cr": Stamp|null, // oldest insert (creation)
|
|
651
|
+
* "w": Stamp|null, // newest insert/update
|
|
652
|
+
* "d": Stamp|null, // newest delete (tombstone register)
|
|
653
|
+
* "u": Stamp|null, // newest operation of any type
|
|
654
|
+
* "f": { "<field>": FieldState }
|
|
655
|
+
* }
|
|
656
|
+
* Stamp = { "t": "<HLC serialized>", "o": "<op id>", "c"?: 1 } // c: authority class
|
|
657
|
+
* FieldState =
|
|
658
|
+
* { "k":"reg", "e":[{ "s":Stamp, "v":value, "a"?:AtomicOp }], "val":value }
|
|
659
|
+
* { "k":"set", "ao":bool, "sh":{ "s":Stamp, "arr":bool, "v"?:value }|null, "clr":Stamp|null,
|
|
660
|
+
* "el":{ "<canonical JSON>#<k>":{ "v":value, "n":k, "a":Stamp|null, "f":{ "s":Stamp, "i":n }|null, "r":Stamp|null } } }
|
|
661
|
+
* { "k":"map", "sh":{ "s":Stamp, "obj":bool, "v"?:value }|null, "clr":Stamp|null,
|
|
662
|
+
* "keys":{ "<key>":{ "s":Stamp, "del":bool, "v"?:value } } }
|
|
663
|
+
* { "k":"ctr", "base":{ "s":Stamp, "v":value }|null, "d":[{ "s":Stamp, "n":number }], "val":value }
|
|
664
|
+
* { "k":"max"|"min", "best":{ "s":Stamp, "v":number }|null, "reg":{ "s":Stamp, "v":value }|null }
|
|
665
|
+
* { "k":"res", "e":[{ "s":Stamp, "v":value, "b"?:value, "z"?:1 }], "val":value, "err"?:string }
|
|
666
|
+
* { "k":"rt", "reset":{ "s":Stamp, "v":value }|null, "u":{ "<base64 Yjs update>":Stamp } }
|
|
667
|
+
* ```
|
|
668
|
+
*
|
|
669
|
+
* Values are JSON-safe (binary as `{ "$koraBytes": base64 }`). The state is
|
|
670
|
+
* self-contained: kinds that need history embed exactly the history they need
|
|
671
|
+
* ('reg' keeps the atomic chain since the newest plain write, 'res' its whole
|
|
672
|
+
* write log, 'ctr' the deltas since its base), so a compactor may treat the state
|
|
673
|
+
* as the record's snapshot and drop operations it covers.
|
|
674
|
+
*
|
|
675
|
+
* @param state - The fold state
|
|
676
|
+
* @returns Canonical JSON
|
|
677
|
+
*/
|
|
678
|
+
declare function serializeFoldState(state: FoldState): string;
|
|
679
|
+
/**
|
|
680
|
+
* Parse a stored fold state. Throws {@link FoldStateError} for an unknown format
|
|
681
|
+
* version or a malformed state; the caller then re-folds the record from its log.
|
|
682
|
+
*
|
|
683
|
+
* @param json - Output of {@link serializeFoldState}
|
|
684
|
+
*/
|
|
685
|
+
declare function deserializeFoldState(json: string): FoldState;
|
|
686
|
+
|
|
687
|
+
/** A materialized row and its versions, the input of {@link createSnapshotState}. */
|
|
688
|
+
interface FoldSnapshotInput {
|
|
689
|
+
collection: string;
|
|
690
|
+
recordId: string;
|
|
691
|
+
/** Field values of the row (record or op-data form; binary is normalized). */
|
|
692
|
+
values: Record<string, unknown>;
|
|
693
|
+
/** Per-field version: the HLC of the newest write of each field. */
|
|
694
|
+
fieldVersions: Record<string, HLCTimestamp>;
|
|
695
|
+
/** HLC of the row's creation. */
|
|
696
|
+
created: HLCTimestamp;
|
|
697
|
+
/** HLC of the newest operation the row reflects (its `_version`). */
|
|
698
|
+
latest: HLCTimestamp;
|
|
699
|
+
/** The row is a domain tombstone. */
|
|
700
|
+
deleted: boolean;
|
|
701
|
+
}
|
|
702
|
+
/** Options of {@link createSnapshotState}. */
|
|
703
|
+
interface FoldSnapshotOptions {
|
|
704
|
+
/**
|
|
705
|
+
* A fold state this replica still holds for the record (its stored
|
|
706
|
+
* `_kora_fold_state`, or the base of a compacted record). Every field whose stored
|
|
707
|
+
* kind still matches the schema is taken from it as-is (exact: element, delta and
|
|
708
|
+
* update stamps included); only the other fields are rebuilt from the row.
|
|
709
|
+
*/
|
|
710
|
+
seed?: FoldState | null;
|
|
711
|
+
/**
|
|
712
|
+
* Legacy authoritative node ids (`FoldOptions.authoritativeNodeIds`). A
|
|
713
|
+
* `merge('server-authoritative')` field whose version was written by an
|
|
714
|
+
* authoritative node (a `kora:server:` node or one of these) is stamped with
|
|
715
|
+
* authority class 1, so a client write it beat cannot overturn it when merged again.
|
|
716
|
+
*/
|
|
717
|
+
authoritativeNodeIds?: ReadonlySet<string>;
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Build a record's fold state from a materialized row: the base snapshot used when
|
|
721
|
+
* the operations that produced the row are no longer all in the log (a database
|
|
722
|
+
* compacted before W7, or a record that owns quarantined log rows).
|
|
723
|
+
*
|
|
724
|
+
* Exact where the replica still has the data: fields of `options.seed` (a stored
|
|
725
|
+
* fold state) whose kind is unchanged are used verbatim, and the record stamps come
|
|
726
|
+
* from it. Every other field is written at its own version with a sentinel
|
|
727
|
+
* operation id that sorts after any real id, so the snapshot dominates every
|
|
728
|
+
* operation it reflects: re-merging such an operation changes nothing (the record's
|
|
729
|
+
* remaining log is folded on top of it again on every re-fold), while a later
|
|
730
|
+
* operation merges on top as usual. Arrays and objects also clear elements / keys
|
|
731
|
+
* written before the snapshot (so an older add the row no longer shows is not
|
|
732
|
+
* resurrected), richtext hides older updates, counters drop older deltas and
|
|
733
|
+
* custom-resolver fields start a fresh log at the snapshot value. A
|
|
734
|
+
* `merge('server-authoritative')` field whose version is a server node's write
|
|
735
|
+
* keeps its authority class.
|
|
736
|
+
*
|
|
737
|
+
* Approximate for a row-only field (RT-68 residual): a concurrent write OLDER than
|
|
738
|
+
* the field's version that arrives later folds against the snapshot value, not
|
|
739
|
+
* against the original operations (a late counter delta or array add is dropped).
|
|
740
|
+
* It cannot be otherwise without the original operations: the row alone does not
|
|
741
|
+
* say whether an older write is already in its value. The store marks such records
|
|
742
|
+
* approximate, and the server's fold state replaces the snapshot when the record
|
|
743
|
+
* next enters the device's scope.
|
|
744
|
+
*
|
|
745
|
+
* @param input - The row's values and versions
|
|
746
|
+
* @param schema - The schema (selects each field's fold kind)
|
|
747
|
+
* @param options - Seed state and authoritative node ids
|
|
748
|
+
*/
|
|
749
|
+
declare function createSnapshotState(input: FoldSnapshotInput, schema: SchemaDefinition, options?: FoldSnapshotOptions): FoldState;
|
|
750
|
+
/** Result of {@link adaptFoldState}. */
|
|
751
|
+
interface AdaptedFoldState {
|
|
752
|
+
state: FoldState;
|
|
753
|
+
/** Fields rebuilt as snapshots (their stored kind no longer matched the schema). */
|
|
754
|
+
adapted: string[];
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Make a stored fold state mergeable under the current schema after a change of a
|
|
758
|
+
* field's fold kind (RT-63): every field whose stored kind no longer matches the
|
|
759
|
+
* plan is replaced by a snapshot of its materialized value at its newest write (as
|
|
760
|
+
* {@link createSnapshotState} builds from a row), keeping its authority class.
|
|
761
|
+
* Fields whose kind is unchanged are kept exactly. Used for compacted base states,
|
|
762
|
+
* whose operations are no longer in the log to re-fold with the new plan.
|
|
763
|
+
*
|
|
764
|
+
* @param state - A stored fold state (base or record state)
|
|
765
|
+
* @param schema - The current schema
|
|
766
|
+
* @param options - `richtext` merger, to materialize a richtext field with several updates
|
|
767
|
+
*/
|
|
768
|
+
declare function adaptFoldState(state: FoldState, schema: SchemaDefinition, options?: Pick<FoldOptions, 'richtext'>): AdaptedFoldState;
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Fingerprint of how a schema folds records (W7): the fold-state format version
|
|
772
|
+
* plus every collection's {@link collectionFoldPlanFingerprint}. When it changes
|
|
773
|
+
* (a field becomes `merge('counter')`, an array becomes append-only, a resolver is
|
|
774
|
+
* added or edited), stored fold states may hold fields of another kind and must be
|
|
775
|
+
* re-folded. The same definition as the server stores' `fold_plan_fingerprint`.
|
|
776
|
+
*
|
|
777
|
+
* Schema transforms (RT-84) are part of the plan when any is registered: an operation
|
|
778
|
+
* of another schema version folds as its transformed view, so changing a transform or
|
|
779
|
+
* the target version re-folds. Without transforms the fingerprint is unchanged.
|
|
780
|
+
*
|
|
781
|
+
* @param schema - The schema
|
|
782
|
+
* @param transforms - The schema transforms the replica folds with
|
|
783
|
+
*/
|
|
784
|
+
declare function foldPlanFingerprint(schema: SchemaDefinition, transforms?: readonly OperationTransform[]): string;
|
|
785
|
+
/**
|
|
786
|
+
* Per-collection fold plan fingerprints, to find which collections a schema change
|
|
787
|
+
* re-plans (only their records need re-folding).
|
|
788
|
+
*
|
|
789
|
+
* @param schema - The schema
|
|
790
|
+
* @param transforms - The schema transforms the replica folds with (see {@link foldPlanFingerprint})
|
|
791
|
+
* @returns Collection name -> fingerprint (format version included)
|
|
792
|
+
*/
|
|
793
|
+
declare function foldPlanFingerprints(schema: SchemaDefinition, transforms?: readonly OperationTransform[]): Record<string, string>;
|
|
794
|
+
/**
|
|
795
|
+
* The fields of a stored fold state whose kind no longer matches the schema (empty
|
|
796
|
+
* when the state can be merged into as-is).
|
|
797
|
+
*
|
|
798
|
+
* @param state - A stored fold state
|
|
799
|
+
* @param schema - The current schema
|
|
800
|
+
*/
|
|
801
|
+
declare function mismatchedFoldFields(state: FoldState, schema: SchemaDefinition): string[];
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Node-id namespaces Kora reserves for itself.
|
|
805
|
+
*
|
|
806
|
+
* Every id starting with {@link RESERVED_NODE_ID_PREFIX} belongs to Kora: devices
|
|
807
|
+
* never author under one (a client refuses to adopt such an id, and the server
|
|
808
|
+
* refuses it at the handshake). Inside it, {@link SERVER_NODE_ID_PREFIX} names the
|
|
809
|
+
* server's own nodes (`kora:server:<stableId>`): their writes are authoritative for
|
|
810
|
+
* `merge('server-authoritative')` fields on every replica, whatever list of node
|
|
811
|
+
* ids a given handshake advertised. That keeps authority stable across server
|
|
812
|
+
* restarts and instances without any replica having to learn the ids first.
|
|
813
|
+
*/
|
|
814
|
+
declare const RESERVED_NODE_ID_PREFIX = "kora:";
|
|
815
|
+
/** Prefix of every server-authored node id: `kora:server:<stableId>`. */
|
|
816
|
+
declare const SERVER_NODE_ID_PREFIX = "kora:server:";
|
|
817
|
+
/** True when `nodeId` is in Kora's reserved `kora:` namespace. */
|
|
818
|
+
declare function isReservedNodeId(nodeId: string): boolean;
|
|
819
|
+
/** True when `nodeId` names a server node (`kora:server:<stableId>`). */
|
|
820
|
+
declare function isServerNodeId(nodeId: string): boolean;
|
|
821
|
+
/**
|
|
822
|
+
* Whether a node's writes are authoritative for `merge('server-authoritative')`
|
|
823
|
+
* fields: a server node by prefix, or one of `explicit` (legacy, randomly generated
|
|
824
|
+
* server node ids a server keeps advertising in its handshake).
|
|
825
|
+
*
|
|
826
|
+
* @param nodeId - The writing node
|
|
827
|
+
* @param explicit - Additional authoritative node ids (`FoldOptions.authoritativeNodeIds`)
|
|
828
|
+
*/
|
|
829
|
+
declare function isAuthoritativeNodeId(nodeId: string, explicit?: ReadonlySet<string>): boolean;
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Turn a {@link FoldTrace} into the {@link MergeTrace} DevTools and `merge:*`
|
|
833
|
+
* events consume. `operationA` is the operation that last wrote the field before
|
|
834
|
+
* the merge; the fold state only keeps its id (`trace.priorOperationId`), so the
|
|
835
|
+
* caller looks it up in its operation log. When it is unavailable (compacted, or
|
|
836
|
+
* the field had no prior value) the incoming operation stands in for it, and
|
|
837
|
+
* `inputA` still carries the prior value.
|
|
838
|
+
*
|
|
839
|
+
* @param trace - A trace from `mergeOp`
|
|
840
|
+
* @param operationA - The operation that produced the field's prior value, if known
|
|
841
|
+
*/
|
|
842
|
+
declare function toMergeTrace(trace: FoldTrace, operationA?: Operation | null): MergeTrace;
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Lossless string encoding for SQL text columns (RT-65), shared by every store that
|
|
846
|
+
* binds JavaScript strings into a database: the server's Postgres and SQLite stores
|
|
847
|
+
* and the client's SQLite stores (better-sqlite3, SQLite WASM on OPFS, and the
|
|
848
|
+
* IndexedDB fallback, which runs SQLite WASM in memory).
|
|
849
|
+
*
|
|
850
|
+
* A JavaScript string is a sequence of UTF-16 code units. SQL text is UTF-8, which
|
|
851
|
+
* cannot hold two kinds of code unit sequence a JS string can:
|
|
852
|
+
*
|
|
853
|
+
* - A lone UTF-16 surrogate: better-sqlite3, SQLite WASM (TextEncoder) and the
|
|
854
|
+
* Postgres driver all encode it as U+FFFD: silent corruption.
|
|
855
|
+
* - U+0000: Postgres TEXT refuses it; SQLite WASM reads a TEXT column up to the first
|
|
856
|
+
* NUL.
|
|
857
|
+
*
|
|
858
|
+
* The codec maps those code units to an escape introduced by U+FFFF, a Unicode
|
|
859
|
+
* noncharacter (valid UTF-8, never assigned):
|
|
860
|
+
*
|
|
861
|
+
* | Input | Stored |
|
|
862
|
+
* |---|---|
|
|
863
|
+
* | U+0000 | U+FFFF `0` |
|
|
864
|
+
* | U+FFFF | U+FFFF `F` |
|
|
865
|
+
* | lone surrogate `\uXXXX` | U+FFFF `s` + 4 lowercase hex digits |
|
|
866
|
+
*
|
|
867
|
+
* Every other string is stored unchanged, so existing rows, equality filters and
|
|
868
|
+
* ordering are unaffected for ordinary text. The map is injective, and
|
|
869
|
+
* `decodeStoredText(encodeStoredText(s)) === s` for every JS string. Equality is
|
|
870
|
+
* preserved: `encode(a) === encode(b)` iff `a === b`, so an equality filter binds the
|
|
871
|
+
* encoded value.
|
|
872
|
+
*
|
|
873
|
+
* Ordering of escaped strings: range filters (`$gt`, `$lt`, ...) and `orderBy` compare
|
|
874
|
+
* the stored (encoded) form, in the database's collation (UTF-8 byte order on SQLite and
|
|
875
|
+
* on Postgres with `C.UTF-8`). A string containing U+0000, U+FFFF or a lone surrogate
|
|
876
|
+
* therefore sorts by its escape, U+FFFF, at the position of the first such code unit:
|
|
877
|
+
* after every other BMP character there, and before supplementary-plane characters.
|
|
878
|
+
* For example `"a\u0000"` sorts after `"ab"` (in JavaScript it sorts before), and a
|
|
879
|
+
* lone surrogate sorts after `"\uFFFE"`. Such strings are rare and equality is exact;
|
|
880
|
+
* do not rely on their relative order in range queries.
|
|
881
|
+
*
|
|
882
|
+
* Columns holding `JSON.stringify` output need no codec: well-formed `JSON.stringify`
|
|
883
|
+
* escapes U+0000 and lone surrogates as `\u` sequences.
|
|
884
|
+
*/
|
|
885
|
+
/** The escape introducer: U+FFFF, a noncharacter. */
|
|
886
|
+
declare const STORED_TEXT_ESCAPE = "\uFFFF";
|
|
887
|
+
/** True when `s` contains a code unit a SQL text column cannot hold as is. */
|
|
888
|
+
declare function needsStoredTextEncoding(s: string): boolean;
|
|
889
|
+
/**
|
|
890
|
+
* Encode a JS string for a SQL TEXT column (or a string inside a JSONB value).
|
|
891
|
+
*
|
|
892
|
+
* @param s - Any JavaScript string
|
|
893
|
+
* @returns `s` itself when it holds no NUL, U+FFFF or lone surrogate; otherwise the escaped form
|
|
894
|
+
*/
|
|
895
|
+
declare function encodeStoredText(s: string): string;
|
|
896
|
+
/**
|
|
897
|
+
* Decode a string written by {@link encodeStoredText}.
|
|
898
|
+
*
|
|
899
|
+
* @param s - A stored string
|
|
900
|
+
* @returns The original JavaScript string. A U+FFFF not followed by a valid tag is kept as is.
|
|
901
|
+
*/
|
|
902
|
+
declare function decodeStoredText(s: string): string;
|
|
903
|
+
/**
|
|
904
|
+
* Apply {@link encodeStoredText} to every string (and object key) inside a JSON value.
|
|
905
|
+
*
|
|
906
|
+
* @param value - A JSON-compatible value
|
|
907
|
+
*/
|
|
908
|
+
declare function encodeStoredJsonValue(value: unknown): unknown;
|
|
909
|
+
/**
|
|
910
|
+
* Apply {@link decodeStoredText} to every string (and object key) inside a JSON value.
|
|
911
|
+
*
|
|
912
|
+
* @param value - A JSON-compatible value
|
|
913
|
+
*/
|
|
914
|
+
declare function decodeStoredJsonValue(value: unknown): unknown;
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Deterministic id for a side-effect operation (a referential cascade, a
|
|
918
|
+
* set-null, a constraint correction) generated while applying `parentOpId`.
|
|
919
|
+
*
|
|
920
|
+
* Every replica that applies the same parent operation and generates the same
|
|
921
|
+
* effect on the same record derives the same id, so the effect is stored once and
|
|
922
|
+
* deduplicates everywhere instead of multiplying (one copy per replica) and racing
|
|
923
|
+
* (W7 step 3). Cascades keep working offline: a client derives the same id the
|
|
924
|
+
* server will.
|
|
925
|
+
*
|
|
926
|
+
* Callers must also make the side-effect's CONTENT deterministic (timestamp and
|
|
927
|
+
* data derived from the parent), because dedup by id keeps whichever copy arrives
|
|
928
|
+
* first. Ids are SHA-256 over canonical JSON of
|
|
929
|
+
* `{ domain, parent, rule, target }`, hex encoded.
|
|
930
|
+
*
|
|
931
|
+
* @param parentOpId - Id of the operation whose application produced the effect
|
|
932
|
+
* @param ruleId - Stable id of the rule (for example `relation:<name>:cascade`)
|
|
933
|
+
* @param targetRecordId - Id of the record the effect writes
|
|
934
|
+
* @returns 64-char hex id
|
|
935
|
+
*/
|
|
936
|
+
declare function deriveSideEffectOpId(parentOpId: string, ruleId: string, targetRecordId: string): Promise<string>;
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* A fold state cannot be used: unknown format version, malformed JSON, a state
|
|
940
|
+
* for a different record, or a field whose stored kind no longer matches the
|
|
941
|
+
* schema. The fix is always the same: re-fold the record from its operation log
|
|
942
|
+
* (`foldRecord`), which is the source of truth.
|
|
943
|
+
*/
|
|
944
|
+
declare class FoldStateError extends KoraError {
|
|
945
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* The fold was asked for something its configuration cannot provide, for example
|
|
949
|
+
* materializing a richtext field with several concurrent Yjs updates without a
|
|
950
|
+
* richtext merger.
|
|
951
|
+
*/
|
|
952
|
+
declare class FoldConfigurationError extends KoraError {
|
|
953
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
954
|
+
}
|
|
487
955
|
|
|
488
956
|
/**
|
|
489
957
|
* Canonical JSON-safe encoding of binary bytes stored inside `op.data` /
|
|
@@ -578,7 +1046,7 @@ interface StateMachineInput {
|
|
|
578
1046
|
onInvalidTransition: 'reject' | 'last-valid-state';
|
|
579
1047
|
}
|
|
580
1048
|
interface CollectionInput {
|
|
581
|
-
fields: Record<string, FieldBuilder
|
|
1049
|
+
fields: Record<string, FieldBuilder>;
|
|
582
1050
|
indexes?: string[];
|
|
583
1051
|
constraints?: ConstraintInput[];
|
|
584
1052
|
resolve?: Record<string, CustomResolver>;
|
|
@@ -636,23 +1104,6 @@ type TypedSchemaDefinition<T extends SchemaInput = SchemaInput> = SchemaDefiniti
|
|
|
636
1104
|
};
|
|
637
1105
|
declare function defineSchema<const T extends SchemaInput>(input: T): TypedSchemaDefinition<T>;
|
|
638
1106
|
|
|
639
|
-
/**
|
|
640
|
-
* Generate CREATE TABLE and CREATE INDEX SQL for a single collection.
|
|
641
|
-
*
|
|
642
|
-
* @param collectionName - The collection name
|
|
643
|
-
* @param collection - The collection definition
|
|
644
|
-
* @param relations - Optional relations for FK references
|
|
645
|
-
* @returns An array of SQL statements (CREATE TABLE + CREATE INDEX)
|
|
646
|
-
*/
|
|
647
|
-
declare function generateSQL(collectionName: string, collection: CollectionDefinition, relations?: Record<string, RelationDefinition>): string[];
|
|
648
|
-
/**
|
|
649
|
-
* Generate the full DDL for all collections plus metadata tables.
|
|
650
|
-
*
|
|
651
|
-
* @param schema - The complete schema definition
|
|
652
|
-
* @returns An array of all SQL statements needed to initialize the database
|
|
653
|
-
*/
|
|
654
|
-
declare function generateFullDDL(schema: SchemaDefinition): string[];
|
|
655
|
-
|
|
656
1107
|
/**
|
|
657
1108
|
* Quote a SQL identifier (table or column name) for safe interpolation into
|
|
658
1109
|
* generated SQL.
|
|
@@ -697,76 +1148,105 @@ declare function quoteIdent(name: string): string;
|
|
|
697
1148
|
* @throws {SchemaValidationError} If validation fails
|
|
698
1149
|
*/
|
|
699
1150
|
declare function validateRecord(collection: string, collectionDef: CollectionDefinition, data: Record<string, unknown>, operationType: OperationType): Record<string, unknown>;
|
|
1151
|
+
/**
|
|
1152
|
+
* The domain check of one resolved field value (value-domain.ts), for values produced
|
|
1153
|
+
* after validation: an atomic op's resolved result (`op.increment(0.5)` on a timestamp,
|
|
1154
|
+
* an increment past the largest finite number).
|
|
1155
|
+
*
|
|
1156
|
+
* @param collection - The collection (for the message)
|
|
1157
|
+
* @param fieldName - The field
|
|
1158
|
+
* @param descriptor - The field's descriptor
|
|
1159
|
+
* @param value - The resolved value
|
|
1160
|
+
* @throws {SchemaValidationError} When the value is outside the field's domain
|
|
1161
|
+
*/
|
|
1162
|
+
declare function assertResolvedFieldValue(collection: string, fieldName: string, descriptor: FieldDescriptor, value: unknown): void;
|
|
700
1163
|
|
|
701
1164
|
/**
|
|
702
|
-
*
|
|
1165
|
+
* One value domain (RT-86, RT-87): every value the local API accepts must be storable and
|
|
1166
|
+
* transmissible, unchanged, by every built-in store (client SQLite and IndexedDB; server
|
|
1167
|
+
* memory, SQLite and Postgres) and every transport (JSON and protobuf). The domain is
|
|
1168
|
+
* defined here once, enforced where values are written (`validateRecord`, after atomic
|
|
1169
|
+
* ops resolve) and re-checked by the server on ingest (`operationValueViolation`), so a
|
|
1170
|
+
* value outside it is refused with a clear error before anything stores it, never by a
|
|
1171
|
+
* database half-way through a sync.
|
|
703
1172
|
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
1173
|
+
* | Field type | Domain | Client SQLite / IndexedDB | Server SQLite | Server Postgres | Wire (JSON / protobuf) |
|
|
1174
|
+
* |----------------|-----------------------------------------------------------|---------------------------|---------------|--------------------------|------------------------|
|
|
1175
|
+
* | `string` | any string (U+0000 and lone surrogates included) | TEXT / string | TEXT | TEXT, lossless text codec (RT-65) | JSON string (escaped) |
|
|
1176
|
+
* | `enum` | one of the declared values | TEXT / string | TEXT | TEXT | JSON string |
|
|
1177
|
+
* | `secret` | string (stored hashed or encrypted) | TEXT | TEXT | TEXT | JSON string |
|
|
1178
|
+
* | `number` | finite double; `-0` is `0` | REAL / number | REAL | DOUBLE PRECISION | JSON number |
|
|
1179
|
+
* | `boolean` | `true` / `false` | INTEGER 0/1 / boolean | INTEGER | INTEGER | JSON boolean |
|
|
1180
|
+
* | `timestamp` | integer milliseconds in [-8.64e15, 8.64e15] (the `Date` range; a safe integer). Fractions are refused, not rounded | INTEGER / number | INTEGER | BIGINT | JSON number |
|
|
1181
|
+
* | `array` | dense array of the item kind's domain, nesting <= {@link MAX_VALUE_DEPTH} | TEXT (JSON) / array | TEXT (JSON) | JSONB (text codec) | JSON array |
|
|
1182
|
+
* | `object` | plain object; declared keys in their kind's domain | TEXT (JSON) / object | TEXT (JSON) | JSONB | JSON object |
|
|
1183
|
+
* | `json` | any JSON value: finite numbers, no `__proto__` key, nesting <= {@link MAX_VALUE_DEPTH}, not exactly `{ __kora_bytes__ }` (the wire's binary form) | TEXT (JSON) / value | TEXT (JSON) | JSONB | JSON |
|
|
1184
|
+
* | `blob` | a `BlobRef` | TEXT (JSON) | TEXT | TEXT | JSON object |
|
|
1185
|
+
* | `richtext` | Yjs update bytes (`Uint8Array` / `ArrayBuffer`) or a string | BLOB / bytes | BLOB | BYTEA | `{ $koraBytes }` / bytes |
|
|
1186
|
+
* | any operation | serialized size <= `maxOperationBytes` (default {@link DEFAULT_MAX_OPERATION_BYTES}) | | | | |
|
|
707
1187
|
*
|
|
708
|
-
*
|
|
1188
|
+
* Every value additionally has a canonical JSON form (core canonical-body): no `NaN`,
|
|
1189
|
+
* `±Infinity`, `BigInt`, `Map`, `Set`, class instances or cycles.
|
|
709
1190
|
*/
|
|
710
|
-
|
|
1191
|
+
/** Smallest `t.timestamp()` value: the earliest instant a JavaScript `Date` can hold. */
|
|
1192
|
+
declare const TIMESTAMP_MIN_MS = -8640000000000000;
|
|
1193
|
+
/** Largest `t.timestamp()` value: the latest instant a JavaScript `Date` can hold. */
|
|
1194
|
+
declare const TIMESTAMP_MAX_MS = 8640000000000000;
|
|
711
1195
|
/**
|
|
712
|
-
*
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
richtext: string;
|
|
720
|
-
enum: string;
|
|
721
|
-
array: unknown[];
|
|
722
|
-
object: Record<string, unknown>;
|
|
723
|
-
json: unknown;
|
|
724
|
-
blob: BlobRef;
|
|
725
|
-
secret: string;
|
|
726
|
-
}
|
|
1196
|
+
* Deepest nesting of arrays and objects inside one field value. Postgres parses JSONB
|
|
1197
|
+
* recursively with a stack limit, and every replica folds values recursively; 64 levels
|
|
1198
|
+
* is far beyond any real document.
|
|
1199
|
+
*/
|
|
1200
|
+
declare const MAX_VALUE_DEPTH = 64;
|
|
1201
|
+
/** Default largest serialized operation, shared by the local write path and the server. */
|
|
1202
|
+
declare const DEFAULT_MAX_OPERATION_BYTES: number;
|
|
727
1203
|
/**
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
enumValues: infer V;
|
|
734
|
-
} ? V extends readonly (infer S)[] ? S : string : F extends {
|
|
735
|
-
kind: 'array';
|
|
736
|
-
itemKind: infer K extends FieldKind;
|
|
737
|
-
} ? FieldKindToType[K][] : F extends {
|
|
738
|
-
kind: infer K extends FieldKind;
|
|
739
|
-
} ? FieldKindToType[K] : unknown;
|
|
740
|
-
/**
|
|
741
|
-
* Infers the full record type returned from queries.
|
|
742
|
-
* Includes `id`, `createdAt`, `updatedAt` metadata fields.
|
|
743
|
-
* Optional/defaulted fields include `| null` in their type.
|
|
744
|
-
*/
|
|
745
|
-
type InferRecord<Fields extends Record<string, FieldBuilder<any, any, any>>> = {
|
|
746
|
-
readonly id: string;
|
|
747
|
-
readonly createdAt: number;
|
|
748
|
-
readonly updatedAt: number;
|
|
749
|
-
} & {
|
|
750
|
-
readonly [K in keyof Fields]: Fields[K] extends FieldBuilder<any, true, any> ? InferFieldType<Fields[K]> : InferFieldType<Fields[K]> | null;
|
|
751
|
-
};
|
|
1204
|
+
* Why `value` is not a valid `t.timestamp()` value, or null when it is.
|
|
1205
|
+
*
|
|
1206
|
+
* @param value - The value
|
|
1207
|
+
*/
|
|
1208
|
+
declare function timestampDomainViolation(value: unknown): string | null;
|
|
752
1209
|
/**
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
* -
|
|
1210
|
+
* The path of the first container nested deeper than `maxDepth`, or null.
|
|
1211
|
+
*
|
|
1212
|
+
* @param value - Any value
|
|
1213
|
+
* @param maxDepth - The deepest allowed nesting of arrays and objects
|
|
757
1214
|
*/
|
|
758
|
-
|
|
759
|
-
[K in keyof Fields as Fields[K] extends FieldBuilder<any, any, true> ? never : Fields[K] extends FieldBuilder<any, true, false> ? K : never]: InferFieldType<Fields[K]>;
|
|
760
|
-
} & {
|
|
761
|
-
[K in keyof Fields as Fields[K] extends FieldBuilder<any, any, true> ? never : Fields[K] extends FieldBuilder<any, true, false> ? never : K]?: InferFieldType<Fields[K]>;
|
|
762
|
-
};
|
|
1215
|
+
declare function valueDepthViolation(value: unknown, maxDepth?: number): string | null;
|
|
763
1216
|
/**
|
|
764
|
-
*
|
|
765
|
-
*
|
|
1217
|
+
* The path of the first own `__proto__` key in a value, or null. JSON can carry such a
|
|
1218
|
+
* key, but most JavaScript code (including object spreads into a fresh `{}` with
|
|
1219
|
+
* assignment) turns it into a prototype and loses it, so it is refused everywhere.
|
|
1220
|
+
*
|
|
1221
|
+
* @param value - Any value
|
|
766
1222
|
*/
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1223
|
+
declare function protoKeyViolation(value: unknown): string | null;
|
|
1224
|
+
/**
|
|
1225
|
+
* Why a field value, as an OPERATION carries it (op-data form: richtext as
|
|
1226
|
+
* `{ $koraBytes }` or a string, secrets in their at-rest string form), is outside the
|
|
1227
|
+
* field's domain, or null when it is inside. The server checks every uploaded and
|
|
1228
|
+
* route-written operation's data with it, so every server store and every peer holds
|
|
1229
|
+
* the same values (RT-86, RT-87). `null` is always accepted (a cleared field).
|
|
1230
|
+
*
|
|
1231
|
+
* @param descriptor - The field's descriptor in the schema that reads the operation
|
|
1232
|
+
* @param value - The value from `data`
|
|
1233
|
+
*/
|
|
1234
|
+
declare function operationValueViolation(descriptor: FieldDescriptor, value: unknown): string | null;
|
|
1235
|
+
/**
|
|
1236
|
+
* Why a json or object field value is reserved by the wire, or null: an object whose
|
|
1237
|
+
* only member is `__kora_bytes__`.
|
|
1238
|
+
*
|
|
1239
|
+
* @param value - A field value
|
|
1240
|
+
*/
|
|
1241
|
+
declare function reservedWireShapeViolation(value: unknown): string | null;
|
|
1242
|
+
/**
|
|
1243
|
+
* Serialized size of an operation in bytes (UTF-8 of its JSON), the measure the server
|
|
1244
|
+
* enforces `maxOperationBytes` with and the local write path checks before a write is
|
|
1245
|
+
* accepted (RT-86).
|
|
1246
|
+
*
|
|
1247
|
+
* @param op - The operation
|
|
1248
|
+
*/
|
|
1249
|
+
declare function measureOperationBytes(op: Operation): number;
|
|
770
1250
|
|
|
771
1251
|
/**
|
|
772
1252
|
* Create an empty version vector.
|
|
@@ -837,6 +1317,45 @@ declare function isCollectionSyncScoped(schema: SchemaDefinition, collectionName
|
|
|
837
1317
|
*/
|
|
838
1318
|
declare function collectSchemaScopeValueKeys(schema: SchemaDefinition): string[];
|
|
839
1319
|
|
|
1320
|
+
/**
|
|
1321
|
+
* Reserved key in a server scope grant that carries VERIFIED claim values
|
|
1322
|
+
* (for example `{ userId: <jwt sub>, orgId: <from your user store> }`).
|
|
1323
|
+
*
|
|
1324
|
+
* An auth provider usually cannot know the sync schema, so instead of listing
|
|
1325
|
+
* every collection it returns `{ $claims: { userId } }` and the sync server binds
|
|
1326
|
+
* each schema-scoped collection from those values when the session starts. A
|
|
1327
|
+
* collection whose scope bindings cannot all be resolved from the claims is
|
|
1328
|
+
* denied (fail closed), never widened to "everything".
|
|
1329
|
+
*
|
|
1330
|
+
* Collection names must start with a letter, so `$claims` can never collide with
|
|
1331
|
+
* a real collection. Any consumer that does not expand it sees a key that matches
|
|
1332
|
+
* no collection, which is also fail-closed.
|
|
1333
|
+
*/
|
|
1334
|
+
declare const SCOPE_CLAIMS_KEY: "$claims";
|
|
1335
|
+
/**
|
|
1336
|
+
* Build a server scope grant from verified claim values, optionally combined
|
|
1337
|
+
* with explicit per-collection grants. Explicit entries win over claim-derived
|
|
1338
|
+
* bindings for the same collection.
|
|
1339
|
+
*
|
|
1340
|
+
* @param claims - Verified scope values, keyed by the scope binding names your
|
|
1341
|
+
* schema uses (`scope: ['userId']` binds the `userId` value)
|
|
1342
|
+
* @param explicit - Optional explicit per-collection predicates
|
|
1343
|
+
* @returns A scope grant suitable for `AuthContext.scopes`
|
|
1344
|
+
*
|
|
1345
|
+
* @example
|
|
1346
|
+
* ```typescript
|
|
1347
|
+
* return { userId: payload.sub, scopes: claimScopes({ userId: payload.sub, orgId }) }
|
|
1348
|
+
* ```
|
|
1349
|
+
*/
|
|
1350
|
+
declare function claimScopes(claims: Record<string, unknown>, explicit?: ScopeMap): ScopeMap;
|
|
1351
|
+
/**
|
|
1352
|
+
* Read the verified claim values carried by a scope grant, if any.
|
|
1353
|
+
*
|
|
1354
|
+
* @param grant - A scope grant (possibly containing {@link SCOPE_CLAIMS_KEY})
|
|
1355
|
+
* @returns The claim values, or null when the grant has none
|
|
1356
|
+
*/
|
|
1357
|
+
declare function getScopeClaims(grant: ScopeMap | undefined): Record<string, unknown> | null;
|
|
1358
|
+
|
|
840
1359
|
/**
|
|
841
1360
|
* Collect every unique scope field name declared across all collections.
|
|
842
1361
|
*
|
|
@@ -882,28 +1401,99 @@ declare function formatSequenceValue(template: string, counter: number, nodeId:
|
|
|
882
1401
|
*/
|
|
883
1402
|
declare function defaultSequenceFormat(name: string): string;
|
|
884
1403
|
|
|
885
|
-
/**
|
|
886
|
-
* Transforms operations created under an older schema version so they are valid
|
|
887
|
-
* after a schema upgrade. Used during sync when client and server schema versions differ
|
|
888
|
-
* but remain within the server's supported range.
|
|
889
|
-
*/
|
|
890
|
-
interface OperationTransform {
|
|
891
|
-
/** Schema version the operation was authored against */
|
|
892
|
-
readonly fromVersion: number;
|
|
893
|
-
/** Target schema version after transformation */
|
|
894
|
-
readonly toVersion: number;
|
|
895
|
-
/**
|
|
896
|
-
* Transform a single operation. Return `null` to drop operations that cannot be migrated.
|
|
897
|
-
*/
|
|
898
|
-
transform(operation: Operation): Operation | null;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
1404
|
/**
|
|
902
1405
|
* Apply registered transforms until the operation matches `targetSchemaVersion`.
|
|
903
1406
|
* Returns null when a transform drops the operation or no path exists.
|
|
904
1407
|
*/
|
|
905
1408
|
declare function applyOperationTransforms(operation: Operation, targetSchemaVersion: number, transforms: readonly OperationTransform[]): Operation | null;
|
|
906
1409
|
|
|
1410
|
+
/**
|
|
1411
|
+
* Thrown when a schema transform breaks its contract: it changed an operation's
|
|
1412
|
+
* identity (id, node, type, collection, record or timestamp), or produced a body with
|
|
1413
|
+
* no canonical JSON form.
|
|
1414
|
+
*/
|
|
1415
|
+
declare class OperationTransformError extends KoraError {
|
|
1416
|
+
constructor(message: string, context: Record<string, unknown>);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* The operation as a schema reads it: its schema view (transforms at fold time).
|
|
1420
|
+
*
|
|
1421
|
+
* Operations are immutable and content-addressed, so a schema transform never rewrites
|
|
1422
|
+
* a stored operation. Every replica (server stores and devices alike) stores and sends
|
|
1423
|
+
* the operation exactly as its author wrote it, and applies the transform chain only
|
|
1424
|
+
* when it judges or folds the operation, through this one function:
|
|
1425
|
+
* - the server authorizes and validates the view, and its stores fold the view;
|
|
1426
|
+
* - a device folds the view; an operation with no view (no transform path, e.g. one
|
|
1427
|
+
* authored under a NEWER schema than the device's) is kept aside and replayed after
|
|
1428
|
+
* the device upgrades.
|
|
1429
|
+
*
|
|
1430
|
+
* Contract, enforced here:
|
|
1431
|
+
* - transforms are PURE and DETERMINISTIC: the same operation and transforms give the
|
|
1432
|
+
* same view on every replica, at every time (no clock, randomness or I/O). Every
|
|
1433
|
+
* replica must register the same transforms for the schema versions its log holds;
|
|
1434
|
+
* the fold plan fingerprint includes them, so changing a transform re-folds;
|
|
1435
|
+
* - a transform rewrites `data`, `previousData`, `atomicOps` and `schemaVersion` only. A
|
|
1436
|
+
* view whose id, node, type, collection, record or timestamp differs is refused
|
|
1437
|
+
* ({@link OperationTransformError});
|
|
1438
|
+
* - the view's body is canonical (core canonical-body), like any written body.
|
|
1439
|
+
*
|
|
1440
|
+
* Returned unchanged: an operation already at `targetSchemaVersion`, any operation
|
|
1441
|
+
* when no transform is registered (it folds as written), and an encryption envelope the
|
|
1442
|
+
* caller cannot read (`encrypted` set: the server folds its cleartext header as is;
|
|
1443
|
+
* devices transform after decryption).
|
|
1444
|
+
*
|
|
1445
|
+
* @param op - The operation as stored
|
|
1446
|
+
* @param targetSchemaVersion - The schema version of the replica judging or folding it
|
|
1447
|
+
* @param transforms - The registered transforms
|
|
1448
|
+
* @returns The view, or null when a transform drops the operation or no path exists
|
|
1449
|
+
* @throws {OperationTransformError} When a transform breaks the contract above
|
|
1450
|
+
*/
|
|
1451
|
+
declare function operationSchemaView(op: Operation, targetSchemaVersion: number, transforms: readonly OperationTransform[] | undefined): Operation | null;
|
|
1452
|
+
/**
|
|
1453
|
+
* Fingerprint of a transform chain for a target schema version: '' when no transform
|
|
1454
|
+
* is registered (operations fold as written, whatever the target), otherwise the
|
|
1455
|
+
* target version and each transform's versions and source text. Part of the fold plan
|
|
1456
|
+
* fingerprint, so a replica re-folds when its views change.
|
|
1457
|
+
*
|
|
1458
|
+
* @param targetSchemaVersion - The replica's schema version
|
|
1459
|
+
* @param transforms - The registered transforms
|
|
1460
|
+
*/
|
|
1461
|
+
declare function operationTransformsFingerprint(targetSchemaVersion: number, transforms: readonly OperationTransform[] | undefined): string;
|
|
1462
|
+
/**
|
|
1463
|
+
* Thrown when stored operations have a schema version the registered transforms cannot
|
|
1464
|
+
* read (RT-103): starting would fold those operations as absent and silently erase them
|
|
1465
|
+
* from every record they belong to.
|
|
1466
|
+
*/
|
|
1467
|
+
declare class OperationTransformCoverageError extends KoraError {
|
|
1468
|
+
readonly versions: readonly number[];
|
|
1469
|
+
readonly targetSchemaVersion: number;
|
|
1470
|
+
constructor(message: string, versions: readonly number[], targetSchemaVersion: number, context?: Record<string, unknown>);
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* The stored schema versions the transforms have no path from (RT-103). Mirrors the
|
|
1474
|
+
* path {@link operationSchemaView} follows (`fromVersion` to `toVersion`, at most 32
|
|
1475
|
+
* steps). An empty transform list reads everything (operations fold as written).
|
|
1476
|
+
*
|
|
1477
|
+
* @param storedVersions - Distinct schema versions of the stored operations
|
|
1478
|
+
* @param targetSchemaVersion - The replica's schema version
|
|
1479
|
+
* @param transforms - The registered transforms
|
|
1480
|
+
* @returns Each version with no path and the version whose transform is missing, ascending
|
|
1481
|
+
*/
|
|
1482
|
+
declare function missingTransformPaths(storedVersions: Iterable<number>, targetSchemaVersion: number, transforms: readonly OperationTransform[] | undefined): Array<{
|
|
1483
|
+
version: number;
|
|
1484
|
+
missingFrom: number;
|
|
1485
|
+
}>;
|
|
1486
|
+
/**
|
|
1487
|
+
* Refuse to start a replica whose transforms cannot read operations it stores (RT-103).
|
|
1488
|
+
*
|
|
1489
|
+
* @param storedVersions - Distinct schema versions of the stored operations
|
|
1490
|
+
* @param targetSchemaVersion - The replica's schema version
|
|
1491
|
+
* @param transforms - The registered transforms
|
|
1492
|
+
* @param replica - Names the replica in the message ("server store", "local database")
|
|
1493
|
+
* @throws {OperationTransformCoverageError} When a stored version has no path
|
|
1494
|
+
*/
|
|
1495
|
+
declare function assertOperationTransformCoverage(storedVersions: Iterable<number>, targetSchemaVersion: number, transforms: readonly OperationTransform[] | undefined, replica: string): void;
|
|
1496
|
+
|
|
907
1497
|
/**
|
|
908
1498
|
* Convert migration steps to SQL statements.
|
|
909
1499
|
*
|
|
@@ -933,7 +1523,7 @@ declare function migrationStepsToSQL(steps: readonly MigrationStep[]): string[];
|
|
|
933
1523
|
* .addIndex('todos', 'priority')
|
|
934
1524
|
*
|
|
935
1525
|
* const rollbackSQL = rollbackStepsToSQL(migration)
|
|
936
|
-
* // ['DROP INDEX IF EXISTS
|
|
1526
|
+
* // ['DROP INDEX IF EXISTS "idx_5_todos_priority"',
|
|
937
1527
|
* // 'ALTER TABLE "todos" DROP COLUMN "priority"']
|
|
938
1528
|
* ```
|
|
939
1529
|
*/
|
|
@@ -1125,4 +1715,4 @@ interface ProtoOutput {
|
|
|
1125
1715
|
*/
|
|
1126
1716
|
declare function generateProtoDefinitions(schema: SchemaDefinition): ProtoOutput;
|
|
1127
1717
|
|
|
1128
|
-
export { APPLY_FAILURE_CODES, APPLY_RESULTS, AppNotReadyError, type ApplyFailureReason, type ApplyResult,
|
|
1718
|
+
export { APPLY_FAILURE_CODES, APPLY_RESULTS, type AdaptedFoldState, AppNotReadyError, type ApplyFailureReason, type ApplyResult, AtomicOp, BlobRef, type BlobRefMetadata, type CanonicalizableBody, CausalTracker, ClockDriftError, CollectionDefinition, type CollectionInput, type ConstraintInput, CustomResolver, DEFAULT_MAX_OPERATION_BYTES, FOLD_RECORD_TRACE_FIELD, FieldBuilder, FieldDescriptor, FoldConfigurationError, FoldFieldVersions, FoldOptions, FoldRecordResult, type FoldSnapshotInput, type FoldSnapshotOptions, FoldState, FoldStateError, FoldTrace, HLCTimestamp, InvalidTimestampError, KORA_ERROR_FIX_SUGGESTIONS, type KoraBytesValue, KoraError, type LegacyBody, MAX_VALUE_DEPTH, MergeConflictError, MergeOpResult, MergeTrace, MigrationDefinition, MigrationRollbackError, MigrationStep, MissingSecretKeyError, NonCanonicalValueError, Operation, OperationError, OperationInput, type OperationLog, OperationTooLargeError, OperationTransform, OperationTransformCoverageError, OperationTransformError, OperationType, type ProtoOutput, RESERVED_NODE_ID_PREFIX, RandomSource, type RecordFieldVersions, type RelationInput, RemoteClockDriftError, type ReplayOperation, type ReversibleMigration, SCOPE_CLAIMS_KEY, SERVER_NODE_ID_PREFIX, STORED_TEXT_ESCAPE, SchemaDefinition, type SchemaInput, SchemaValidationError, ScopeMap, type SecretKeyProvider, StateMachineConstraint, type StateMachineInput, StorageError, SyncError, TIMESTAMP_MAX_MS, TIMESTAMP_MIN_MS, TransitionMap, TransitionValidationResult, type TypedSchemaDefinition, VersionVector, type VersionedReplayOperation, adaptFoldState, advanceVector, applyOperationTransforms, assertOperationTransformCoverage, assertResolvedFieldValue, base64ToBytes, buildStateMachineConstraints, bytesToBase64, canAutoRollback, canonicalValue, canonicalizeLegacyOperation, canonicalizeOperationBody, canonicalizeProvenLegacyClear, claimScopes, collectSchemaScopeFields, collectSchemaScopeValueKeys, computeDelta, createBlobRef, createFoldState, createReversibleMigration, createSnapshotState, createVersionVector, decodeBytesFromOpData, decodeStoredJsonValue, decodeStoredText, decryptSecret, defaultApplyFailureReason, defaultSequenceFormat, defineSchema, deriveSideEffectOpId, deserializeFoldState, deserializeVector, dominates, encodeBytesForOpData, encodeStoredJsonValue, encodeStoredText, encryptSecret, expandFieldVersionedOperations, extractScopeValuesFromClaims, extractTimestamp, foldPlanFingerprint, foldPlanFingerprints, foldRecord, formatSequenceValue, generateProtoDefinitions, generateRollbackSteps, generateUUIDv7, getCollectionScopeBindings, getFoldFieldVersionStrings, getFoldFieldVersions, getKoraErrorFix, getScopeClaims, getTransitionMap, hasSchemaSyncRules, hashBlob, hashSecret, isApplyFailure, isAuthoritativeNodeId, isBlobRef, isCollectionSyncScoped, isFoldStateLive, isKoraBytesValue, isLegacyNumericByteObject, isReservedNodeId, isServerNodeId, isValidUUIDv7, joinStates, materialize, measureOperationBytes, mergeArraySet, mergeOp, mergeVectors, migrationStepsToSQL, mismatchedFoldFields, missingTransformPaths, needsStoredTextEncoding, operationSchemaView, operationTransformsFingerprint, operationValueViolation, protoKeyViolation, quoteIdent, replayFieldVersionsForRecord, replayOperationsForRecord, reservedWireShapeViolation, revealSecret, rollbackStepsToSQL, serializeFoldState, serializeVector, timestampDomainViolation, toMergeTrace, transformSecretFieldsForWrite, validateRecord, validateTransition, valueDepthViolation, vectorsEqual, verifySecret, verifySecretValue };
|