@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.
@@ -0,0 +1,1737 @@
1
+ /**
2
+ * Atomic field operations for intent-preserving updates.
3
+ *
4
+ * Instead of read-modify-write patterns, developers express intent:
5
+ * ```typescript
6
+ * await app.stock.update(id, { quantity: op.increment(-5) })
7
+ * ```
8
+ *
9
+ * The operation intent is preserved in the Operation for merge:
10
+ * concurrent increments compose (sum of deltas) instead of LWW.
11
+ */
12
+
13
+ /**
14
+ * Sentinel marker for detecting atomic op objects in update data.
15
+ * Uses Symbol.for() so the sentinel survives module boundary crossings.
16
+ */
17
+ declare const KORA_ATOMIC_OP: unique symbol;
18
+ /** String key used for the sentinel symbol — exported for internal use */
19
+ declare const KORA_ATOMIC_OP_KEY: typeof KORA_ATOMIC_OP;
20
+ /**
21
+ * Sentinel object returned by op.* helpers.
22
+ * Detected by Collection.update() and resolved to concrete values.
23
+ *
24
+ * The type parameters carry the helper's operation and operand type (RT-113), so an
25
+ * update input accepts numeric helpers only on number and timestamp fields and array
26
+ * helpers, with the element type, only on array fields. The bare `AtomicOpSentinel` is
27
+ * any sentinel.
28
+ */
29
+ interface AtomicOpSentinel<Type extends AtomicOpType = AtomicOpType, Value = unknown> {
30
+ readonly [KORA_ATOMIC_OP_KEY]: true;
31
+ readonly type: Type;
32
+ readonly value: Value;
33
+ }
34
+ /** The numeric helpers (`op.increment`, `op.decrement`, `op.max`, `op.min`). */
35
+ type NumericAtomicOpSentinel = AtomicOpSentinel<'increment' | 'max' | 'min', number>;
36
+ /** The array helpers (`op.append`, `op.remove`) for an array of `Item`. */
37
+ type ArrayAtomicOpSentinel<Item> = AtomicOpSentinel<'append' | 'remove', Item>;
38
+ /**
39
+ * Constraint of an array helper's item: listing the primitive types keeps a literal
40
+ * argument's literal type (`op.append('high')` is `'high'`, assignable to an enum array),
41
+ * while objects are inferred as written (not readonly).
42
+ */
43
+ type AtomicItem = string | number | boolean | bigint | symbol | object | null | undefined;
44
+ /**
45
+ * Type guard: checks whether a value is an atomic op sentinel.
46
+ *
47
+ * @param value - Any value to check
48
+ * @returns true if the value is an AtomicOpSentinel
49
+ */
50
+ declare function isAtomicOp(value: unknown): value is AtomicOpSentinel;
51
+ /**
52
+ * Apply a plain atomic op (`{ type, value }`) to a current value, producing the
53
+ * concrete result. This is the single source of truth for atomic-op semantics:
54
+ * {@link resolveAtomicOp} (sentinel form, used on the write path) delegates here,
55
+ * and the server's materialization replays stored atomic ops through it, so the
56
+ * server's materialized view composes concurrent atomic writes identically to the
57
+ * client. Folding this over a field's writes in HLC order yields, for a same-type
58
+ * atomic chain, exactly the composed intent (sum of increments, max of maxes, ...).
59
+ *
60
+ * @param currentValue - The current value of the field
61
+ * @param atomicOp - The atomic op to apply (type + operand)
62
+ * @returns The resulting concrete value
63
+ */
64
+ declare function applyAtomicOp(currentValue: unknown, atomicOp: AtomicOp): unknown;
65
+ /**
66
+ * Resolve an atomic op sentinel against a current value to produce a concrete result.
67
+ *
68
+ * @param currentValue - The current value of the field in the database
69
+ * @param sentinel - The atomic op sentinel from the developer's update call
70
+ * @returns The resolved concrete value
71
+ */
72
+ declare function resolveAtomicOp(currentValue: unknown, sentinel: AtomicOpSentinel): unknown;
73
+ /**
74
+ * Atomic operation helpers. Use these in collection.update() calls
75
+ * to express intent-preserving mutations.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * import { op } from 'korajs'
80
+ *
81
+ * // Increment a counter (works correctly with concurrent updates)
82
+ * await app.stock.update(id, { quantity: op.increment(-5) })
83
+ *
84
+ * // Keep the maximum value
85
+ * await app.scores.update(id, { highScore: op.max(newScore) })
86
+ *
87
+ * // Append to an array
88
+ * await app.todos.update(id, { tags: op.append('urgent') })
89
+ * ```
90
+ */
91
+ declare const op: {
92
+ /**
93
+ * Increment a number field by the given amount.
94
+ * Concurrent increments compose: the sum of all deltas is applied to the base.
95
+ *
96
+ * @param n - The amount to increment (use negative values to decrement)
97
+ */
98
+ increment(n: number): AtomicOpSentinel<"increment", number>;
99
+ /**
100
+ * Decrement a number field by the given amount.
101
+ * Syntactic sugar for `op.increment(-n)`.
102
+ *
103
+ * @param n - The amount to decrement
104
+ */
105
+ decrement(n: number): AtomicOpSentinel<"increment", number>;
106
+ /**
107
+ * Set the field to the maximum of the current value and the given value.
108
+ * Concurrent max operations take the maximum of all values.
109
+ *
110
+ * @param n - The value to compare against the current value
111
+ */
112
+ max(n: number): AtomicOpSentinel<"max", number>;
113
+ /**
114
+ * Set the field to the minimum of the current value and the given value.
115
+ * Concurrent min operations take the minimum of all values.
116
+ *
117
+ * @param n - The value to compare against the current value
118
+ */
119
+ min(n: number): AtomicOpSentinel<"min", number>;
120
+ /**
121
+ * Append an item to an array field.
122
+ * Concurrent appends include all appended items.
123
+ *
124
+ * @param item - The item to append to the array
125
+ */
126
+ append<Item extends AtomicItem>(item: Item): AtomicOpSentinel<"append", Item>;
127
+ /**
128
+ * Remove an item from an array field (by value equality).
129
+ * Concurrent removes of the same item are idempotent.
130
+ *
131
+ * @param item - The item to remove from the array
132
+ */
133
+ remove<Item extends AtomicItem>(item: Item): AtomicOpSentinel<"remove", Item>;
134
+ };
135
+ /**
136
+ * Extract the serializable AtomicOp from a sentinel (strips the Symbol marker).
137
+ *
138
+ * @param sentinel - The atomic op sentinel
139
+ * @returns A plain object suitable for JSON serialization
140
+ */
141
+ declare function toAtomicOp(sentinel: AtomicOpSentinel): AtomicOp;
142
+
143
+ /**
144
+ * Type-level inference utilities for Kora schemas.
145
+ *
146
+ * These mapped types read the structural `'~field'` brand every FieldBuilder carries
147
+ * (kind, requiredness, auto flag, read and write value types) and turn a collection's
148
+ * builders into record, insert and update types, enabling autocomplete and type checking
149
+ * from defineSchema() through createApp() to collection methods.
150
+ *
151
+ * Nullability rules (they mirror the runtime):
152
+ * - Record (read): a required field is `T`; an auto `timestamp` is `number` (the framework
153
+ * always fills it); every other field is `T | null` (optional and defaulted fields can be
154
+ * cleared with `update(id, { field: null })`, and rows written before a field existed
155
+ * hold null).
156
+ * - Insert: a required field must be provided; optional and defaulted fields may be
157
+ * omitted (not `null`: insert refuses null); auto fields cannot be provided at all.
158
+ * - Update: every non-auto field is optional; optional and defaulted fields accept `null`
159
+ * to clear them; number and timestamp fields also accept the numeric `op.*` helpers, and
160
+ * array fields `op.append` / `op.remove` with an item of the element type.
161
+ *
162
+ * Zero runtime cost — these are purely compile-time constructs.
163
+ */
164
+
165
+ /**
166
+ * Maps a FieldKind string literal to the TypeScript type a read returns for it, when
167
+ * nothing more specific is known (a compiled {@link FieldDescriptor} rather than a
168
+ * builder). Builders carry exact types: enum literal unions, array item types, object
169
+ * shapes and json `T`.
170
+ */
171
+ interface FieldKindToType {
172
+ string: string;
173
+ number: number;
174
+ boolean: boolean;
175
+ timestamp: number;
176
+ richtext: Uint8Array;
177
+ enum: string;
178
+ array: unknown[];
179
+ object: Record<string, unknown>;
180
+ json: unknown;
181
+ blob: BlobRef;
182
+ secret: string;
183
+ }
184
+ /** Any field builder. */
185
+ type AnyField = FieldBuilder;
186
+ /** The field map of one collection, as written in `defineSchema({ collections: { x: { fields } } })`. */
187
+ type FieldMap = Record<string, AnyField>;
188
+ /**
189
+ * Infers the TypeScript type a read returns for a single field builder or compiled
190
+ * field descriptor (before nullability). For a builder this is exact: enum literal
191
+ * unions, typed array items, object shapes and json `T` are preserved.
192
+ */
193
+ type InferFieldType<F> = F extends {
194
+ readonly '~field': unknown;
195
+ } ? FieldOutput<F> : F extends {
196
+ kind: 'enum';
197
+ enumValues: infer V;
198
+ } ? V extends readonly (infer S)[] ? S : string : F extends {
199
+ kind: 'array';
200
+ itemKind: infer K extends FieldKind;
201
+ } ? FieldKindToType[K][] : F extends {
202
+ kind: infer K extends FieldKind;
203
+ } ? FieldKindToType[K] : unknown;
204
+ /**
205
+ * Infers the TypeScript type a write accepts for a single field builder (before
206
+ * optionality). Equal to {@link InferFieldType} except for richtext, which accepts a
207
+ * string or Yjs bytes.
208
+ */
209
+ type InferFieldInput<F> = F extends {
210
+ readonly '~field': unknown;
211
+ } ? FieldInput<F> : F extends {
212
+ kind: 'richtext';
213
+ } ? RichtextInput : InferFieldType<F>;
214
+ type IsRequired<F> = F extends {
215
+ readonly '~field': {
216
+ readonly required: true;
217
+ };
218
+ } ? true : false;
219
+ type IsAuto<F> = F extends {
220
+ readonly '~field': {
221
+ readonly auto: true;
222
+ };
223
+ } ? true : false;
224
+ type KindOf<F> = F extends {
225
+ readonly '~field': {
226
+ readonly kind: infer K;
227
+ };
228
+ } ? K : FieldKind;
229
+ type RequiredInsertKeys<F extends FieldMap> = {
230
+ [K in keyof F]: IsAuto<F[K]> extends true ? never : IsRequired<F[K]> extends true ? K : never;
231
+ }[keyof F];
232
+ type OptionalInsertKeys<F extends FieldMap> = {
233
+ [K in keyof F]: IsAuto<F[K]> extends true ? never : IsRequired<F[K]> extends true ? never : K;
234
+ }[keyof F];
235
+ type WritableKeys<F extends FieldMap> = {
236
+ [K in keyof F]: IsAuto<F[K]> extends true ? never : K;
237
+ }[keyof F];
238
+ /** The read type of one field in a record, nullability applied. */
239
+ type InferRecordField<F> = IsRequired<F> extends true ? InferFieldType<F> : IsAuto<F> extends true ? KindOf<F> extends 'timestamp' ? InferFieldType<F> : InferFieldType<F> | null : InferFieldType<F> | null;
240
+ /**
241
+ * Infers the full record type returned from queries.
242
+ * Includes `id`, `createdAt`, `updatedAt` metadata fields.
243
+ * Optional and defaulted fields include `| null` in their type.
244
+ */
245
+ type InferRecord<Fields extends FieldMap> = Simplify<{
246
+ readonly id: string;
247
+ readonly createdAt: number;
248
+ readonly updatedAt: number;
249
+ } & {
250
+ readonly [K in keyof Fields]: InferRecordField<Fields[K]>;
251
+ }>;
252
+ /**
253
+ * Infers the insert input type.
254
+ * - Required non-auto fields are required keys
255
+ * - Optional/defaulted non-auto fields are optional keys (omit them; `null` is refused)
256
+ * - Auto fields are excluded entirely
257
+ */
258
+ type InferInsertInput<Fields extends FieldMap> = Simplify<{
259
+ [K in RequiredInsertKeys<Fields>]: InferFieldInput<Fields[K]>;
260
+ } & {
261
+ [K in OptionalInsertKeys<Fields>]?: InferFieldInput<Fields[K]>;
262
+ }>;
263
+ /** Alias of {@link InferInsertInput}. */
264
+ type InferInsert<Fields extends FieldMap> = InferInsertInput<Fields>;
265
+ /** The element type of an array field's value. */
266
+ type ArrayItemOf<T> = T extends readonly (infer Item)[] ? Item : never;
267
+ /**
268
+ * The `op.*` helpers one field accepts (RT-113): the numeric helpers (`increment`,
269
+ * `decrement`, `max`, `min`) on number and timestamp fields, the array helpers (`append`,
270
+ * `remove`) with the field's element type on array fields, none elsewhere. The runtime
271
+ * refuses every other combination, since the resolved value leaves the field's domain.
272
+ */
273
+ type InferAtomicOp<F> = KindOf<F> extends 'number' | 'timestamp' ? NumericAtomicOpSentinel : KindOf<F> extends 'array' ? ArrayAtomicOpSentinel<ArrayItemOf<InferFieldType<F>>> : never;
274
+ /** The values an update accepts for one field. */
275
+ type InferUpdateField<F> = InferFieldInput<F> | (IsRequired<F> extends true ? never : null) | InferAtomicOp<F>;
276
+ /**
277
+ * Infers the update input type.
278
+ * All non-auto fields are optional (partial update semantics). Optional and defaulted
279
+ * fields accept `null`; number and timestamp fields accept the numeric `op.*` helpers and
280
+ * array fields the array helpers with their element type ({@link InferAtomicOp}).
281
+ */
282
+ type InferUpdateInput<Fields extends FieldMap> = Simplify<{
283
+ [K in WritableKeys<Fields>]?: InferUpdateField<Fields[K]>;
284
+ }>;
285
+ /** Alias of {@link InferUpdateInput}. */
286
+ type InferUpdate<Fields extends FieldMap> = InferUpdateInput<Fields>;
287
+
288
+ /**
289
+ * A value a `t.richtext()` field accepts on write: a plain string (stored as a fresh
290
+ * Yjs document holding that text) or Yjs update bytes. Reads always return the Yjs
291
+ * update bytes (`Uint8Array`).
292
+ */
293
+ type RichtextInput = string | Uint8Array | ArrayBuffer;
294
+ /**
295
+ * Compile-time description of a field, carried by every {@link FieldBuilder} under the
296
+ * `'~field'` key. It exists only in the type system (the property is `declare`d, so it is
297
+ * never present at runtime) and is what makes builders structurally distinct:
298
+ * `t.string()` and `t.string().optional()` are different types, so inference can tell a
299
+ * required field from an optional one.
300
+ */
301
+ interface FieldTypeInfo<Kind extends FieldKind, Req extends boolean, Auto extends boolean, Output, Input> {
302
+ /** The field kind. */
303
+ readonly kind: Kind;
304
+ /** Whether the developer must provide the field on insert. */
305
+ readonly required: Req;
306
+ /** Whether the framework fills the field (it cannot be written). */
307
+ readonly auto: Auto;
308
+ /** The type a read returns for the field (before nullability). */
309
+ readonly output: Output;
310
+ /** The type a write accepts for the field (before optionality). */
311
+ readonly input: Input;
312
+ }
313
+ /**
314
+ * The value types of each scalar field kind: what a read returns. `timestamp` is integer
315
+ * milliseconds since the epoch (a `Date` is not accepted; use `date.getTime()`).
316
+ */
317
+ interface ScalarKindToType {
318
+ string: string;
319
+ number: number;
320
+ boolean: boolean;
321
+ timestamp: number;
322
+ richtext: Uint8Array;
323
+ blob: BlobRef;
324
+ secret: string;
325
+ }
326
+ /** The type a scalar field kind accepts on write. */
327
+ interface ScalarKindToInput {
328
+ string: string;
329
+ number: number;
330
+ boolean: boolean;
331
+ timestamp: number;
332
+ richtext: RichtextInput;
333
+ blob: BlobRef;
334
+ secret: string;
335
+ }
336
+ /** A scalar field kind: one whose builder is a plain {@link FieldBuilder}. */
337
+ type ScalarFieldKind = keyof ScalarKindToType;
338
+ /** The read type a field kind has when its builder declares nothing more specific. */
339
+ type DefaultFieldOutput<Kind extends FieldKind> = FieldKindToType[Kind];
340
+ /** The write type a field kind has when its builder declares nothing more specific. */
341
+ type DefaultFieldInput<Kind extends FieldKind> = Kind extends 'richtext' ? RichtextInput : FieldKindToType[Kind];
342
+ /**
343
+ * Base field builder implementing the builder pattern for schema field definitions.
344
+ * Each builder is immutable — modifier methods return new builder instances.
345
+ *
346
+ * Type parameters track field metadata at the type level for inference:
347
+ * - Kind: the field kind ('string', 'number', etc.)
348
+ * - Req: whether the field is required (true = required on insert)
349
+ * - Auto: whether the field is auto-populated (true = excluded from insert input)
350
+ * - Output: the type a read returns
351
+ * - Input: the type a write accepts (differs from Output only for richtext)
352
+ *
353
+ * A bare `FieldBuilder` (no type arguments) means "any field builder".
354
+ *
355
+ * @example
356
+ * ```typescript
357
+ * t.string() // required string field
358
+ * t.string().optional() // optional string field
359
+ * t.string().default('hello') // string with default value
360
+ * t.timestamp().auto() // auto-populated timestamp
361
+ * ```
362
+ */
363
+ declare class FieldBuilder<Kind extends FieldKind = FieldKind, Req extends boolean = boolean, Auto extends boolean = boolean, Output = DefaultFieldOutput<Kind>, Input = Kind extends 'richtext' ? DefaultFieldInput<Kind> : Output> {
364
+ /**
365
+ * Type-only brand (never present at runtime). It makes the builder's kind,
366
+ * requiredness, auto flag and value types part of its structure, which is what
367
+ * schema inference reads.
368
+ */
369
+ readonly '~field': FieldTypeInfo<Kind, Req, Auto, Output, Input>;
370
+ protected readonly _kind: Kind;
371
+ protected readonly _required: boolean;
372
+ protected readonly _defaultValue: unknown;
373
+ protected readonly _auto: boolean;
374
+ protected readonly _mergeStrategy: FieldMergeStrategy | null;
375
+ constructor(kind: Kind, required?: boolean, defaultValue?: unknown, auto?: boolean, mergeStrategy?: FieldMergeStrategy | null);
376
+ /** Mark this field as optional (not required on insert; reads may return `null`). */
377
+ optional(): FieldBuilder<Kind, false, Auto, Output, Input>;
378
+ /**
379
+ * Set a default value for this field. Implicitly makes the field optional on insert.
380
+ * The value must be of the field's type (`t.number().default('x')` is a type error).
381
+ */
382
+ default(value: Input): FieldBuilder<Kind, false, Auto, Output, Input>;
383
+ /**
384
+ * Mark this field as auto-populated (e.g., createdAt timestamps). Developers cannot
385
+ * set auto fields; the framework fills `t.timestamp().auto()` with the insert time.
386
+ */
387
+ auto(): FieldBuilder<Kind, false, true, Output, Input>;
388
+ /**
389
+ * Declare a merge strategy for this field.
390
+ * Controls how concurrent modifications are resolved during sync.
391
+ *
392
+ * @param strategy - The merge strategy to use:
393
+ * - `'lww'`: Last-write-wins (default for scalar fields)
394
+ * - `'counter'`: Sum of deltas from base (for numbers)
395
+ * - `'max'`: Keep the maximum value (for numbers/timestamps)
396
+ * - `'min'`: Keep the minimum value (for numbers/timestamps)
397
+ * - `'union'`: Set-union merge (default for arrays)
398
+ * - `'append-only'`: Concatenate additions (for arrays)
399
+ * - `'server-authoritative'`: Always prefer the remote/server value
400
+ */
401
+ merge(strategy: FieldMergeStrategy): FieldBuilder<Kind, Req, Auto, Output, Input>;
402
+ /** @internal Build the final FieldDescriptor. Used by defineSchema(). */
403
+ _build(): FieldDescriptor;
404
+ }
405
+ /**
406
+ * Field builder for enum fields with constrained string values.
407
+ * Preserves the literal enum tuple type for inference.
408
+ */
409
+ declare class EnumFieldBuilder<Values extends readonly string[] = readonly string[], Req extends boolean = boolean, Auto extends boolean = boolean> extends FieldBuilder<'enum', Req, Auto, Values[number]> {
410
+ private readonly _enumValues;
411
+ private readonly _transitions;
412
+ constructor(values: Values, required?: boolean, defaultValue?: unknown, auto?: boolean, mergeStrategy?: FieldMergeStrategy | null, transitions?: TransitionMap | null);
413
+ optional(): EnumFieldBuilder<Values, false, Auto>;
414
+ default(value: Values[number]): EnumFieldBuilder<Values, false, Auto>;
415
+ auto(): EnumFieldBuilder<Values, false, true>;
416
+ merge(strategy: FieldMergeStrategy): EnumFieldBuilder<Values, Req, Auto>;
417
+ /**
418
+ * Declare allowed state transitions for this enum field.
419
+ * Enables state machine validation during mutations and merges.
420
+ *
421
+ * @param map - Map of state to allowed next states
422
+ *
423
+ * @example
424
+ * ```typescript
425
+ * t.enum(['draft', 'pending', 'confirmed', 'cancelled']).transitions({
426
+ * draft: ['pending', 'cancelled'],
427
+ * pending: ['confirmed', 'cancelled'],
428
+ * confirmed: [],
429
+ * cancelled: [],
430
+ * })
431
+ * ```
432
+ */
433
+ transitions(map: Partial<Record<Values[number], Values[number][]>>): EnumFieldBuilder<Values, Req, Auto>;
434
+ _build(): FieldDescriptor;
435
+ }
436
+ /** The read type of the values a field builder holds (before nullability). */
437
+ type FieldOutput<F> = F extends {
438
+ readonly '~field': {
439
+ readonly output: infer O;
440
+ };
441
+ } ? O : unknown;
442
+ /** The write type a field builder accepts (before optionality). */
443
+ type FieldInput<F> = F extends {
444
+ readonly '~field': {
445
+ readonly input: infer I;
446
+ };
447
+ } ? I : unknown;
448
+ /**
449
+ * Field builder for array fields. Carries the item builder's type, so
450
+ * `t.array(t.enum(['a', 'b']))` infers `('a' | 'b')[]`.
451
+ */
452
+ declare class ArrayFieldBuilder<Item extends FieldBuilder = FieldBuilder, Req extends boolean = boolean, Auto extends boolean = boolean> extends FieldBuilder<'array', Req, Auto, FieldOutput<Item>[], FieldInput<Item>[]> {
453
+ private readonly _item;
454
+ constructor(itemBuilder: Item, required?: boolean, defaultValue?: unknown, auto?: boolean, mergeStrategy?: FieldMergeStrategy | null);
455
+ optional(): ArrayFieldBuilder<Item, false, Auto>;
456
+ default(value: FieldInput<Item>[]): ArrayFieldBuilder<Item, false, Auto>;
457
+ auto(): ArrayFieldBuilder<Item, false, true>;
458
+ merge(strategy: FieldMergeStrategy): ArrayFieldBuilder<Item, Req, Auto>;
459
+ _build(): FieldDescriptor;
460
+ }
461
+ /** Flattens an intersection of object types into one object type (for readable hovers). */
462
+ type Simplify<T> = {
463
+ [K in keyof T]: T[K];
464
+ } & {};
465
+ type RequiredNestedKeys<F> = {
466
+ [K in keyof F]: F[K] extends {
467
+ readonly '~field': {
468
+ readonly required: true;
469
+ };
470
+ } ? K : never;
471
+ }[keyof F];
472
+ /**
473
+ * The value type of a `t.object({...})` field built from its nested builders. A required
474
+ * nested key is required; an optional or defaulted nested key may be absent or `null`.
475
+ */
476
+ type ObjectFieldValue<F extends Record<string, FieldBuilder>, Mode extends 'output' | 'input'> = Simplify<{
477
+ [K in RequiredNestedKeys<F>]: Mode extends 'output' ? FieldOutput<F[K]> : FieldInput<F[K]>;
478
+ } & {
479
+ [K in Exclude<keyof F, RequiredNestedKeys<F>>]?: (Mode extends 'output' ? FieldOutput<F[K]> : FieldInput<F[K]>) | null;
480
+ }>;
481
+ /**
482
+ * Field builder for structured object fields with a nested field schema. Carries the
483
+ * nested builders' types, so `t.object({ theme: t.string() })` infers `{ theme: string }`.
484
+ *
485
+ * Each nested key merges by its own declared kind (scalars via LWW, nested
486
+ * arrays via add-wins, nested objects recursively), so two devices editing
487
+ * different keys of the same object offline both converge on reconnect instead
488
+ * of one clobbering the other.
489
+ */
490
+ declare class ObjectFieldBuilder<Fields extends Record<string, FieldBuilder> = Record<string, FieldBuilder>, Req extends boolean = boolean, Auto extends boolean = boolean> extends FieldBuilder<'object', Req, Auto, ObjectFieldValue<Fields, 'output'>, ObjectFieldValue<Fields, 'input'>> {
491
+ private readonly _fields;
492
+ constructor(fields: Fields, required?: boolean, defaultValue?: unknown, auto?: boolean, mergeStrategy?: FieldMergeStrategy | null);
493
+ optional(): ObjectFieldBuilder<Fields, false, Auto>;
494
+ default(value: ObjectFieldValue<Fields, 'input'>): ObjectFieldBuilder<Fields, false, Auto>;
495
+ auto(): ObjectFieldBuilder<Fields, false, true>;
496
+ merge(strategy: FieldMergeStrategy): ObjectFieldBuilder<Fields, Req, Auto>;
497
+ _build(): FieldDescriptor;
498
+ }
499
+ /**
500
+ * Field builder for dynamic-key JSON values. Carries a compile-time shape `T`
501
+ * for inference while merging structurally as a convergent CRDT: a plain-object
502
+ * value recurses as a map, an array merges add-wins, any other value is a scalar
503
+ * leaf under last-write-wins.
504
+ *
505
+ * A `Date` nested inside a json value is stored as its ISO string (the canonical
506
+ * operation form), so declare such members as `string` in `T`.
507
+ */
508
+ declare class JsonFieldBuilder<T = unknown, Req extends boolean = boolean, Auto extends boolean = boolean> extends FieldBuilder<'json', Req, Auto, T> {
509
+ optional(): JsonFieldBuilder<T, false, Auto>;
510
+ default(value: T): JsonFieldBuilder<T, false, Auto>;
511
+ auto(): JsonFieldBuilder<T, false, true>;
512
+ merge(strategy: FieldMergeStrategy): JsonFieldBuilder<T, Req, Auto>;
513
+ _build(): FieldDescriptor;
514
+ }
515
+ /**
516
+ * Field builder for secret fields (passwords, tokens, API keys).
517
+ *
518
+ * A secret field's value is never exposed in merge traces, DevTools, or logs
519
+ * (it is redacted at the point traces are built). Its at-rest protection is
520
+ * chosen with `.hashed()` (one-way, for passwords) or `.encrypted()` (reversible,
521
+ * for tokens); the default is `encrypted`.
522
+ */
523
+ declare class SecretFieldBuilder<Req extends boolean = boolean, Auto extends boolean = boolean> extends FieldBuilder<'secret', Req, Auto, string> {
524
+ private readonly _secretMode;
525
+ constructor(secretMode?: SecretMode, required?: boolean, defaultValue?: unknown, auto?: boolean, mergeStrategy?: FieldMergeStrategy | null);
526
+ /** Store this secret as a one-way salted hash (passwords: verify, never read back). */
527
+ hashed(): SecretFieldBuilder<Req, Auto>;
528
+ /** Store this secret as reversible ciphertext (tokens/keys: decrypt to use). */
529
+ encrypted(): SecretFieldBuilder<Req, Auto>;
530
+ optional(): SecretFieldBuilder<false, Auto>;
531
+ default(value: string): SecretFieldBuilder<false, Auto>;
532
+ auto(): SecretFieldBuilder<false, true>;
533
+ merge(strategy: FieldMergeStrategy): SecretFieldBuilder<Req, Auto>;
534
+ _build(): FieldDescriptor;
535
+ }
536
+ /** The builder `t.<kind>()` returns for a scalar kind: required, not auto. */
537
+ type ScalarFieldBuilder<K extends ScalarFieldKind> = FieldBuilder<K, true, false, ScalarKindToType[K], ScalarKindToInput[K]>;
538
+ /**
539
+ * Type builder namespace. The developer's primary interface for defining field types.
540
+ *
541
+ * Value types: `string` and `secret` are strings, `number` is a finite number,
542
+ * `timestamp` is integer milliseconds since the epoch (`Date` is not accepted; pass
543
+ * `date.getTime()`), `richtext` reads as Yjs update bytes and accepts a string or bytes,
544
+ * `blob` is a `BlobRef`.
545
+ *
546
+ * @example
547
+ * ```typescript
548
+ * import { t } from '@korajs/core'
549
+ *
550
+ * const fields = {
551
+ * title: t.string(),
552
+ * count: t.number(),
553
+ * active: t.boolean().default(true),
554
+ * notes: t.richtext(),
555
+ * tags: t.array(t.string()).default([]),
556
+ * priority: t.enum(['low', 'medium', 'high']).default('medium'),
557
+ * settings: t.object({ theme: t.string(), fontSize: t.number() }),
558
+ * metadata: t.json<{ source: string }>(),
559
+ * createdAt: t.timestamp().auto(),
560
+ * }
561
+ * ```
562
+ */
563
+ declare const t: {
564
+ /** A string field. */
565
+ string(): ScalarFieldBuilder<"string">;
566
+ /** A finite-number field. */
567
+ number(): ScalarFieldBuilder<"number">;
568
+ /** A boolean field. */
569
+ boolean(): ScalarFieldBuilder<"boolean">;
570
+ /** A timestamp field: integer milliseconds since the epoch (not a `Date`). */
571
+ timestamp(): ScalarFieldBuilder<"timestamp">;
572
+ /** A collaborative rich-text field (Yjs). Reads return the Yjs update bytes. */
573
+ richtext(): ScalarFieldBuilder<"richtext">;
574
+ /** An enum field whose value is one of `values`. */
575
+ enum<const V extends readonly string[]>(values: V): EnumFieldBuilder<V, true, false>;
576
+ /** An array field whose items have the type of `itemBuilder`. */
577
+ array<Item extends FieldBuilder>(itemBuilder: Item): ArrayFieldBuilder<Item, true, false>;
578
+ /** A structured object field whose keys are declared by nested builders. */
579
+ object<const F extends Record<string, FieldBuilder>>(fields: F): ObjectFieldBuilder<F, true, false>;
580
+ /** A dynamic JSON field typed as `T` (compile-time only; not validated against `T`). */
581
+ json<T = unknown>(): JsonFieldBuilder<T, true, false>;
582
+ /** A blob reference field (`BlobRef`); the bytes live in the blob store. */
583
+ blob(): ScalarFieldBuilder<"blob">;
584
+ /** A secret field (password, token): redacted from traces, hashed or encrypted at rest. */
585
+ secret(): SecretFieldBuilder<true, false>;
586
+ };
587
+
588
+ /**
589
+ * A single migration step describing a schema change.
590
+ */
591
+ type MigrationStep = {
592
+ type: 'addField';
593
+ collection: string;
594
+ field: string;
595
+ descriptor: FieldDescriptor;
596
+ } | {
597
+ type: 'removeField';
598
+ collection: string;
599
+ field: string;
600
+ descriptor?: FieldDescriptor;
601
+ } | {
602
+ type: 'renameField';
603
+ collection: string;
604
+ from: string;
605
+ to: string;
606
+ } | {
607
+ type: 'addIndex';
608
+ collection: string;
609
+ field: string;
610
+ } | {
611
+ type: 'removeIndex';
612
+ collection: string;
613
+ field: string;
614
+ } | {
615
+ type: 'backfill';
616
+ collection: string;
617
+ transform: (record: Record<string, unknown>) => Record<string, unknown>;
618
+ /** Reverse transform for rollback. If not provided, backfill is not safely reversible. */
619
+ reverseTransform?: (record: Record<string, unknown>) => Record<string, unknown>;
620
+ /**
621
+ * Rewrite rows on this device only, without writing operations (so nothing
622
+ * syncs). Default false: every changed record is written through the normal
623
+ * local write path as an update operation (mutation name `migration:v<N>`).
624
+ */
625
+ localOnly?: boolean;
626
+ };
627
+ /** Options for {@link MigrationBuilder.backfill}. */
628
+ interface BackfillOptions {
629
+ /** Reverse transform for rollback support. */
630
+ reverseTransform?: (record: Record<string, unknown>) => Record<string, unknown>;
631
+ /**
632
+ * Rewrite rows on this device only, without operations, so the change never syncs.
633
+ * Use it for device-local derived data. Default false: backfilled values are written
634
+ * as update operations and sync like any other write.
635
+ */
636
+ localOnly?: boolean;
637
+ }
638
+ /**
639
+ * A completed migration definition containing ordered steps and optional rollback steps.
640
+ */
641
+ interface MigrationDefinition {
642
+ readonly steps: readonly MigrationStep[];
643
+ /** Explicitly defined rollback steps. If undefined, inverse steps are auto-generated. */
644
+ readonly rollbackSteps: readonly MigrationStep[] | undefined;
645
+ /** Whether this migration can be safely rolled back. */
646
+ readonly safelyReversible: boolean;
647
+ }
648
+ /**
649
+ * Builder for defining rollback steps explicitly.
650
+ * Used with the `.down()` method on MigrationBuilder.
651
+ *
652
+ * @example
653
+ * ```typescript
654
+ * migrate()
655
+ * .addField('todos', 'priority', t.enum(['low', 'medium', 'high']).default('medium'))
656
+ * .addIndex('todos', 'priority')
657
+ * .down((rollback) => {
658
+ * rollback
659
+ * .removeIndex('todos', 'priority')
660
+ * .removeField('todos', 'priority')
661
+ * })
662
+ * ```
663
+ */
664
+ declare class RollbackBuilder {
665
+ private _steps;
666
+ /**
667
+ * Add a field during rollback (to reverse a removeField).
668
+ */
669
+ addField(collection: string, field: string, builder: FieldBuilder): RollbackBuilder;
670
+ /**
671
+ * Remove a field during rollback (to reverse an addField).
672
+ */
673
+ removeField(collection: string, field: string): RollbackBuilder;
674
+ /**
675
+ * Rename a field during rollback (to reverse a renameField).
676
+ */
677
+ renameField(collection: string, from: string, to: string): RollbackBuilder;
678
+ /**
679
+ * Add an index during rollback (to reverse a removeIndex).
680
+ */
681
+ addIndex(collection: string, field: string): RollbackBuilder;
682
+ /**
683
+ * Remove an index during rollback (to reverse an addIndex).
684
+ */
685
+ removeIndex(collection: string, field: string): RollbackBuilder;
686
+ /**
687
+ * Run a backfill during rollback (to reverse a previous backfill).
688
+ */
689
+ backfill(collection: string, transform: (record: Record<string, unknown>) => Record<string, unknown>): RollbackBuilder;
690
+ /**
691
+ * Get the accumulated rollback steps.
692
+ */
693
+ _getSteps(): readonly MigrationStep[];
694
+ }
695
+ /**
696
+ * Fluent builder for defining schema migrations.
697
+ *
698
+ * Each method returns a new builder instance (immutable).
699
+ *
700
+ * @example
701
+ * ```typescript
702
+ * migrate()
703
+ * .addField('products', 'taxInclusive', t.boolean().default(false))
704
+ * .renameField('products', 'cost', 'costPrice')
705
+ * .backfill('products', (record) => ({
706
+ * taxInclusive: record.taxRate > 0,
707
+ * }))
708
+ * ```
709
+ */
710
+ declare class MigrationBuilder implements MigrationDefinition {
711
+ readonly steps: readonly MigrationStep[];
712
+ readonly rollbackSteps: readonly MigrationStep[] | undefined;
713
+ readonly safelyReversible: boolean;
714
+ constructor(steps?: readonly MigrationStep[], rollbackSteps?: readonly MigrationStep[], safelyReversible?: boolean);
715
+ /**
716
+ * Add a new field to a collection.
717
+ * The field builder provides the type and default value.
718
+ */
719
+ addField(collection: string, field: string, builder: FieldBuilder): MigrationBuilder;
720
+ /**
721
+ * Remove a field from a collection.
722
+ * Optionally accepts a field builder to preserve the descriptor for rollback.
723
+ * Without the descriptor, rollback of this step requires a custom `.down()`.
724
+ *
725
+ * @param collection - The collection name
726
+ * @param field - The field name to remove
727
+ * @param builder - Optional field builder preserving the type info for rollback
728
+ */
729
+ removeField(collection: string, field: string, builder?: FieldBuilder): MigrationBuilder;
730
+ /**
731
+ * Rename a field in a collection.
732
+ * Implemented as ALTER TABLE RENAME COLUMN (SQLite 3.25+).
733
+ */
734
+ renameField(collection: string, from: string, to: string): MigrationBuilder;
735
+ /**
736
+ * Add an index on a field.
737
+ */
738
+ addIndex(collection: string, field: string): MigrationBuilder;
739
+ /**
740
+ * Remove an index on a field.
741
+ */
742
+ removeIndex(collection: string, field: string): MigrationBuilder;
743
+ /**
744
+ * Backfill records in a collection using a transform function.
745
+ * The transform receives each live record (typed values, as the app reads them) and
746
+ * returns the fields to change. Runs after structural changes (addField, renameField,
747
+ * etc.), in the same transaction as them and the schema version write.
748
+ *
749
+ * Changed records are written as update operations (mutation name `migration:v<N>`)
750
+ * through the normal local write path, so backfilled values sync. Pass
751
+ * `{ localOnly: true }` for device-local data that must not sync.
752
+ *
753
+ * @param collection - The collection name
754
+ * @param transform - Forward transform function
755
+ * @param reverseOrOptions - Reverse transform for rollback support, or options
756
+ */
757
+ backfill(collection: string, transform: (record: Record<string, unknown>) => Record<string, unknown>, reverseOrOptions?: ((record: Record<string, unknown>) => Record<string, unknown>) | BackfillOptions): MigrationBuilder;
758
+ /**
759
+ * Define explicit rollback steps for this migration.
760
+ * If not called, inverse steps are auto-generated from forward steps.
761
+ *
762
+ * @param fn - Function that receives a RollbackBuilder to define rollback steps
763
+ * @returns A new MigrationBuilder with the rollback steps attached
764
+ *
765
+ * @example
766
+ * ```typescript
767
+ * migrate()
768
+ * .addField('todos', 'priority', t.enum(['low', 'medium', 'high']).default('medium'))
769
+ * .addIndex('todos', 'priority')
770
+ * .down((rollback) => {
771
+ * rollback
772
+ * .removeIndex('todos', 'priority')
773
+ * .removeField('todos', 'priority')
774
+ * })
775
+ * ```
776
+ */
777
+ down(fn: (rollback: RollbackBuilder) => void): MigrationBuilder;
778
+ /**
779
+ * Determine if a migration is safely reversible based on its steps.
780
+ * A migration is not safely reversible if:
781
+ * - It has a backfill step without a reverseTransform and no explicit rollback
782
+ * - It has a removeField without a stored descriptor and no explicit rollback
783
+ */
784
+ private _computeSafelyReversible;
785
+ }
786
+ /**
787
+ * Start building a migration.
788
+ *
789
+ * @returns A new MigrationBuilder
790
+ *
791
+ * @example
792
+ * ```typescript
793
+ * import { migrate, t } from '@korajs/core'
794
+ *
795
+ * const migration = migrate()
796
+ * .addField('products', 'taxInclusive', t.boolean().default(false))
797
+ * .renameField('products', 'cost', 'costPrice')
798
+ * ```
799
+ */
800
+ declare function migrate(): MigrationBuilder;
801
+
802
+ /**
803
+ * Hybrid Logical Clock timestamp.
804
+ * Provides a total order that respects causality without requiring synchronized clocks.
805
+ */
806
+ interface HLCTimestamp {
807
+ /** Physical wall-clock time in milliseconds */
808
+ wallTime: number;
809
+ /** Logical counter. Increments when wallTime hasn't changed since last event. */
810
+ logical: number;
811
+ /** Node ID for tie-breaking. Ensures total order even with identical wall+logical. */
812
+ nodeId: string;
813
+ }
814
+ /** The three mutation types an operation can represent */
815
+ type OperationType = 'insert' | 'update' | 'delete';
816
+ /** Supported atomic operation types for intent-preserving field updates */
817
+ type AtomicOpType = 'increment' | 'max' | 'min' | 'append' | 'remove';
818
+ /**
819
+ * Atomic operation stored in an Operation's atomicOps field.
820
+ * This is the serializable form that persists in the operation log.
821
+ */
822
+ interface AtomicOp {
823
+ readonly type: AtomicOpType;
824
+ readonly value: unknown;
825
+ }
826
+ /**
827
+ * The atomic unit of the entire system. Every mutation produces an Operation.
828
+ * Operations are IMMUTABLE and CONTENT-ADDRESSED.
829
+ */
830
+ interface Operation {
831
+ /**
832
+ * Content-addressed SHA-256 hash of the operation's canonical body. Which fields it
833
+ * covers depends on `hashVersion` (version 2, the default for new operations, adds
834
+ * previousData, sequenceNumber, causalDeps and schemaVersion to version 1's fields).
835
+ * Verify with `verifyOperationId`.
836
+ */
837
+ id: string;
838
+ /** UUID v7 of the originating device. Time-sortable. */
839
+ nodeId: string;
840
+ /** What happened */
841
+ type: OperationType;
842
+ /** Which collection (from schema) */
843
+ collection: string;
844
+ /** ID of the affected record. UUID v7 for inserts, existing ID for update/delete. */
845
+ recordId: string;
846
+ /** Field values. null for delete. For updates, contains ONLY changed fields. */
847
+ data: Record<string, unknown> | null;
848
+ /** For updates: previous values of changed fields (enables 3-way merge). null for insert/delete. */
849
+ previousData: Record<string, unknown> | null;
850
+ /** Hybrid Logical Clock timestamp. Used for causal ordering. */
851
+ timestamp: HLCTimestamp;
852
+ /** Monotonically increasing per node. Used in version vectors. */
853
+ sequenceNumber: number;
854
+ /** Operation IDs this operation causally depends on (direct parents in the DAG). */
855
+ causalDeps: string[];
856
+ /** Schema version at time of creation. Used for migration transforms. */
857
+ schemaVersion: number;
858
+ /** Atomic operation intents for fields in data (e.g., increment, max). Present only when atomic ops were used. */
859
+ atomicOps?: Record<string, AtomicOp>;
860
+ /** Groups this operation with others in an atomic transaction. Not part of the content hash. */
861
+ transactionId?: string;
862
+ /** Human-readable name for the mutation group (e.g., 'complete-sale'). For DevTools display. */
863
+ mutationName?: string;
864
+ /**
865
+ * Per-field HLC versions, present only on server-synthesized scope-entry inserts
866
+ * (RT-27). The entry restates a record's current values; each field carries the
867
+ * version of the write that produced it, so a receiver resolves every field by
868
+ * last-write-wins against its own field version instead of one whole-row stamp.
869
+ * `timestamp` is then the record's creation time. Not part of the content hash.
870
+ */
871
+ fieldVersions?: Record<string, HLCTimestamp>;
872
+ /**
873
+ * Content-hash version of `id` (CORE-1). Absent means 1: the id covers type,
874
+ * collection, recordId, data, timestamp, nodeId and atomicOps. 2: it also
875
+ * covers previousData, sequenceNumber, causalDeps and schemaVersion. Verify
876
+ * with `verifyOperationId`. New operations are version 2 (protocol v2); the
877
+ * field is carried on the wire (protobuf operation field 14) and persisted.
878
+ */
879
+ hashVersion?: 1 | 2;
880
+ /**
881
+ * Server-authored serialized fold state (`serializeFoldState`) of the record (W7),
882
+ * carried on scope-entry inserts instead of `fieldVersions`, and present only on
883
+ * operations from a node the handshake named authoritative
884
+ * (`HandshakeResponseMessage.authoritativeNodeIds`). A receiver joins it into its own
885
+ * state (`joinStates`), so every field kind (richtext, counters, resolvers, element
886
+ * sets) enters with its full merge state. Opaque to the protocol layer. NOT part of
887
+ * the content hash; the server strips it from every device upload. Protobuf
888
+ * operation field 15.
889
+ */
890
+ foldState?: string;
891
+ /**
892
+ * End-to-end encryption envelope (protocol v2, W9). When present the operation's
893
+ * `data`, `previousData` and `atomicOps` travel only inside this envelope as
894
+ * ciphertext; `data` is null (or holds only the documented cleartext scope fields)
895
+ * and `previousData`/`atomicOps` are absent. The id is the version-2 hash of the
896
+ * PLAINTEXT operation and is verified after decryption. Not part of the content hash.
897
+ * Protobuf operation field 16 (JSON).
898
+ */
899
+ encrypted?: EncryptedOperationEnvelope;
900
+ }
901
+ /**
902
+ * One AES-GCM ciphertext inside an {@link EncryptedOperationEnvelope}.
903
+ */
904
+ interface EncryptedEnvelopeField {
905
+ /** Base64 12-byte IV, unique per field encryption. */
906
+ iv: string;
907
+ /** Base64 AES-GCM ciphertext including the 16-byte tag. */
908
+ ct: string;
909
+ }
910
+ /**
911
+ * Versioned end-to-end encryption envelope of an operation (protocol v2, ENC-3,
912
+ * NEW-ENC-1). Each encrypted member is bound by AES-GCM additional authenticated data
913
+ * to canonical(nodeId, collection, recordId, type, timestamp, sequenceNumber, field,
914
+ * keyVersion), so a ciphertext cannot be moved to another operation, record or field.
915
+ */
916
+ interface EncryptedOperationEnvelope {
917
+ /** Envelope format version. 2 is the only defined value. */
918
+ v: 2;
919
+ /** Cipher. Only AES-256-GCM is defined. */
920
+ alg: 'aes-256-gcm';
921
+ /**
922
+ * Identifier of the key material (key-derivation salt fingerprint), so a device
923
+ * holding different key material reports a diagnosable mismatch instead of a bare
924
+ * authentication failure (ENC-1 key distribution is still to come).
925
+ */
926
+ keyId: string;
927
+ /** Key version used (rotation). */
928
+ keyVersion: number;
929
+ /** Ciphertext of `data`, or null when the plaintext `data` is null. */
930
+ data: EncryptedEnvelopeField | null;
931
+ /** Ciphertext of `previousData`, or null when it is null. */
932
+ previousData: EncryptedEnvelopeField | null;
933
+ /** Ciphertext of `atomicOps`, present only when the plaintext operation had them. */
934
+ atomicOps?: EncryptedEnvelopeField;
935
+ }
936
+ /**
937
+ * Input for creating an operation (before id and timestamp are assigned).
938
+ */
939
+ interface OperationInput {
940
+ nodeId: string;
941
+ type: OperationType;
942
+ collection: string;
943
+ recordId: string;
944
+ data: Record<string, unknown> | null;
945
+ previousData: Record<string, unknown> | null;
946
+ sequenceNumber: number;
947
+ causalDeps: string[];
948
+ schemaVersion: number;
949
+ /** Atomic operation intents for fields in data (e.g., increment, max). */
950
+ atomicOps?: Record<string, AtomicOp>;
951
+ /** Groups this operation with others in an atomic transaction. Not part of the content hash. */
952
+ transactionId?: string;
953
+ /** Human-readable name for the mutation group (e.g., 'complete-sale'). For DevTools display. */
954
+ mutationName?: string;
955
+ }
956
+ /** Version vector: maps nodeId to the max sequence number seen from that node */
957
+ type VersionVector = Map<string, number>;
958
+ /** Field kinds supported by the schema system */
959
+ type FieldKind = 'string' | 'number' | 'boolean' | 'timestamp' | 'richtext' | 'enum' | 'array' | 'object' | 'json' | 'blob' | 'secret';
960
+ /**
961
+ * How a `secret` field's value is protected at rest.
962
+ * - `hashed`: one-way (passwords). Stored as a salted hash; never read back.
963
+ * - `encrypted`: reversible (tokens, keys). Stored as ciphertext; decrypted to use.
964
+ */
965
+ type SecretMode = 'hashed' | 'encrypted';
966
+ /**
967
+ * A content-addressed reference to binary data stored out of band.
968
+ *
969
+ * A `blob` field never carries the bytes in the operation log; it carries this
970
+ * small reference. The bytes live in a content-addressed blob store, keyed by
971
+ * `hash`, so identical content is stored once and never re-synced to a peer that
972
+ * already has that hash.
973
+ */
974
+ interface BlobRef {
975
+ /** Hex-encoded SHA-256 hash of the bytes. The content address. */
976
+ hash: string;
977
+ /** Size of the bytes in bytes. */
978
+ size: number;
979
+ /** Optional MIME type (for example "image/png"). */
980
+ mimeType?: string;
981
+ /** Optional original filename. */
982
+ filename?: string;
983
+ /**
984
+ * Hex-encoded SHA-256 hash of this blob's manifest — the small chunk index a
985
+ * peer fetches first to learn how to pull the bytes out of band. Present when
986
+ * the blob was stored for transfer; absent for a bare reference created without
987
+ * chunking. Because it is a content address like `hash`, the manifest is fetched
988
+ * and integrity-verified over the same channel as the chunks.
989
+ */
990
+ manifestHash?: string;
991
+ }
992
+ /**
993
+ * Comprehensive sync diagnostics snapshot.
994
+ * Provides connection, latency, throughput, queue, and error metrics
995
+ * for monitoring and DevTools integration.
996
+ */
997
+ interface SyncDiagnosticsSnapshot {
998
+ /** Current developer-facing sync status */
999
+ status: 'connected' | 'reconnecting' | 'syncing' | 'synced' | 'offline' | 'error' | 'schema-mismatch' | 'clock-error' | 'auth-required' | 'encryption-locked';
1000
+ /** Timestamp when the current connection was established, or null if disconnected */
1001
+ connectedAt: number | null;
1002
+ /** Timestamp when the last disconnection occurred, or null if never disconnected */
1003
+ disconnectedAt: number | null;
1004
+ /** Number of reconnection attempts since last successful connection */
1005
+ reconnectAttempts: number;
1006
+ /** Current round-trip time in milliseconds */
1007
+ rttMs: number;
1008
+ /** Median (50th percentile) RTT over the sliding window */
1009
+ rttP50Ms: number;
1010
+ /** 95th percentile RTT over the sliding window */
1011
+ rttP95Ms: number;
1012
+ /** 99th percentile RTT over the sliding window */
1013
+ rttP99Ms: number;
1014
+ /** Total operations sent during this session */
1015
+ operationsSent: number;
1016
+ /** Total operations received during this session */
1017
+ operationsReceived: number;
1018
+ /** Total bytes sent during this session */
1019
+ bytesSent: number;
1020
+ /** Total bytes received during this session */
1021
+ bytesReceived: number;
1022
+ /** Number of operations waiting to be sent */
1023
+ pendingOperations: number;
1024
+ /** Estimated bytes in the outbound queue */
1025
+ outboundQueueSize: number;
1026
+ /** Timestamp of the last successful sync, or null if never synced */
1027
+ lastSyncedAt: number | null;
1028
+ /** Duration of the last complete sync cycle in ms, or null if no cycle completed */
1029
+ syncDuration: number | null;
1030
+ /** Whether the initial sync (full delta exchange) has completed */
1031
+ initialSyncComplete: boolean;
1032
+ /** Progress of the initial sync as a 0-1 ratio */
1033
+ initialSyncProgress: number;
1034
+ /** Description of the last error, or null if no error occurred */
1035
+ lastError: string | null;
1036
+ /** Total number of errors during this session */
1037
+ errorCount: number;
1038
+ /** Assessed connection quality level */
1039
+ quality: 'excellent' | 'good' | 'fair' | 'poor' | 'offline';
1040
+ /** Estimated effective bandwidth in bytes per second, or null if not yet measured */
1041
+ effectiveBandwidth: number | null;
1042
+ }
1043
+ /** Built-in field merge strategies that can be declared in the schema. */
1044
+ type FieldMergeStrategy = 'lww' | 'counter' | 'max' | 'min' | 'union' | 'append-only' | 'server-authoritative';
1045
+ /** Map of state to allowed next states for state machine constraints */
1046
+ type TransitionMap = Record<string, string[]>;
1047
+ /**
1048
+ * A state machine constraint extracted from the schema.
1049
+ * Used by merge and validation to enforce valid state transitions.
1050
+ */
1051
+ interface StateMachineConstraint {
1052
+ /** The enum field this constraint controls */
1053
+ field: string;
1054
+ /** The collection this constraint applies to */
1055
+ collection: string;
1056
+ /** Map of state to allowed next states */
1057
+ transitions: TransitionMap;
1058
+ }
1059
+ /**
1060
+ * Result of validating a state transition.
1061
+ */
1062
+ interface TransitionValidationResult {
1063
+ /** Whether the transition is allowed */
1064
+ valid: boolean;
1065
+ /** The source state */
1066
+ from: string;
1067
+ /** The target state */
1068
+ to: string;
1069
+ /** The field being transitioned */
1070
+ field: string;
1071
+ /** The collection containing the field */
1072
+ collection: string;
1073
+ /** All allowed target states from the source state */
1074
+ allowedTargets: string[];
1075
+ }
1076
+ /**
1077
+ * Descriptor produced by the type builder (t.string(), t.number(), etc.).
1078
+ * Represents a fully configured field definition.
1079
+ */
1080
+ interface FieldDescriptor {
1081
+ kind: FieldKind;
1082
+ required: boolean;
1083
+ defaultValue: unknown;
1084
+ auto: boolean;
1085
+ enumValues: readonly string[] | null;
1086
+ itemKind: FieldKind | null;
1087
+ /** Declared merge strategy. Defaults to kind-appropriate strategy when undefined. */
1088
+ mergeStrategy: FieldMergeStrategy | null;
1089
+ /** State machine transition map for enum fields. Null if no transitions declared. */
1090
+ transitions: TransitionMap | null;
1091
+ /**
1092
+ * Nested field descriptors for `object` fields (each key merges by its own
1093
+ * kind). Null for `json` fields (dynamic keys, resolved structurally) and for
1094
+ * all non-structured kinds.
1095
+ */
1096
+ nestedFields?: Record<string, FieldDescriptor> | null;
1097
+ /**
1098
+ * At-rest protection mode for `secret` fields (`hashed` or `encrypted`).
1099
+ * Null/absent for every other kind.
1100
+ */
1101
+ secretMode?: SecretMode | null;
1102
+ }
1103
+ /**
1104
+ * Defines a state machine on an enum field, constraining valid state transitions.
1105
+ *
1106
+ * When a state machine is defined on a collection, mutations and merges
1107
+ * enforce that the controlled field only moves along declared transitions.
1108
+ *
1109
+ * @example
1110
+ * ```typescript
1111
+ * stateMachine: {
1112
+ * field: 'status',
1113
+ * transitions: {
1114
+ * draft: ['pending', 'cancelled'],
1115
+ * pending: ['confirmed', 'cancelled'],
1116
+ * confirmed: ['shipped'],
1117
+ * shipped: ['delivered'],
1118
+ * delivered: [],
1119
+ * cancelled: [],
1120
+ * },
1121
+ * onInvalidTransition: 'reject',
1122
+ * }
1123
+ * ```
1124
+ */
1125
+ interface StateMachineDefinition {
1126
+ /** The enum field this state machine controls */
1127
+ field: string;
1128
+ /** Map of state to allowed next states */
1129
+ transitions: Record<string, string[]>;
1130
+ /** What to do when an invalid transition is attempted */
1131
+ onInvalidTransition: 'reject' | 'last-valid-state';
1132
+ }
1133
+ /**
1134
+ * Definition of a collection within the schema.
1135
+ */
1136
+ interface CollectionDefinition {
1137
+ fields: Record<string, FieldDescriptor>;
1138
+ indexes: string[];
1139
+ constraints: Constraint[];
1140
+ resolvers: Record<string, CustomResolver>;
1141
+ /** Scope fields for sync filtering. Only records matching the client's scope values are synced. */
1142
+ scope: string[];
1143
+ /** Optional state machine constraining transitions on an enum field */
1144
+ stateMachine?: StateMachineDefinition;
1145
+ }
1146
+ /** Custom resolver function for tier 3 merge resolution */
1147
+ type CustomResolver = (local: unknown, remote: unknown, base: unknown) => unknown;
1148
+ /**
1149
+ * Constraint for tier 2 conflict resolution.
1150
+ */
1151
+ interface Constraint {
1152
+ type: 'unique' | 'capacity' | 'referential';
1153
+ fields: string[];
1154
+ where?: Record<string, unknown>;
1155
+ onConflict: 'first-write-wins' | 'last-write-wins' | 'priority-field' | 'server-decides' | 'custom';
1156
+ priorityField?: string;
1157
+ resolve?: (local: unknown, remote: unknown, base: unknown) => unknown;
1158
+ }
1159
+ /** Relation type between collections */
1160
+ type RelationType = 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many';
1161
+ /** On-delete behavior for relations */
1162
+ type OnDeleteAction = 'cascade' | 'set-null' | 'restrict' | 'no-action';
1163
+ /**
1164
+ * Definition of a relation between two collections.
1165
+ */
1166
+ interface RelationDefinition {
1167
+ from: string;
1168
+ to: string;
1169
+ type: RelationType;
1170
+ field: string;
1171
+ onDelete: OnDeleteAction;
1172
+ }
1173
+ /**
1174
+ * Declarative sync filter for a collection.
1175
+ *
1176
+ * `where` maps record field names to scope value keys. Use `true` to bind a field
1177
+ * to a scope value with the same name (e.g. `{ userId: true }`).
1178
+ */
1179
+ interface SyncRuleDefinition {
1180
+ where: Record<string, string>;
1181
+ }
1182
+ /**
1183
+ * The complete schema definition produced by defineSchema().
1184
+ */
1185
+ interface SchemaDefinition {
1186
+ version: number;
1187
+ collections: Record<string, CollectionDefinition>;
1188
+ relations: Record<string, RelationDefinition>;
1189
+ /** Schema migrations keyed by target version. */
1190
+ migrations: Record<number, MigrationDefinition>;
1191
+ /**
1192
+ * Declarative partial-sync rules per collection.
1193
+ * When present, only listed collections (plus legacy `scope` collections) sync.
1194
+ */
1195
+ sync?: Record<string, SyncRuleDefinition>;
1196
+ }
1197
+ /**
1198
+ * Merge strategies available for auto-merge and constraints.
1199
+ */
1200
+ declare const MERGE_STRATEGIES: readonly ["auto-merge", "lww", "first-write-wins", "server-decides", "custom"];
1201
+ type MergeStrategy = (typeof MERGE_STRATEGIES)[number];
1202
+ /**
1203
+ * Connection quality levels for adaptive sync.
1204
+ */
1205
+ declare const CONNECTION_QUALITIES: readonly ["excellent", "good", "fair", "poor", "offline"];
1206
+ type ConnectionQuality = (typeof CONNECTION_QUALITIES)[number];
1207
+ /**
1208
+ * Injectable time source for deterministic testing of clocks.
1209
+ */
1210
+ interface TimeSource {
1211
+ now(): number;
1212
+ }
1213
+ /**
1214
+ * Injectable random source for deterministic testing of UUID generation.
1215
+ */
1216
+ interface RandomSource {
1217
+ getRandomValues<T extends ArrayBufferView>(array: T): T;
1218
+ }
1219
+ /**
1220
+ * Configuration for an offline-safe sequence.
1221
+ *
1222
+ * Sequences produce monotonically increasing, collision-free identifiers
1223
+ * that work across offline devices. Each device maintains its own counter
1224
+ * scoped by (name, scope, nodeId).
1225
+ *
1226
+ * Format tokens available:
1227
+ * - `{date}` → YYYYMMDD
1228
+ * - `{node4}` → first 4 chars of nodeId
1229
+ * - `{node8}` → first 8 chars of nodeId
1230
+ * - `{seq}` → zero-padded counter (4 digits)
1231
+ * - `{seq:N}` → zero-padded counter with N digits
1232
+ *
1233
+ * @example
1234
+ * ```typescript
1235
+ * await app.sequences.next('receipt', {
1236
+ * scope: storeId,
1237
+ * format: 'S-{date}-{node4}-{seq}',
1238
+ * })
1239
+ * // → "S-20260508-a1b2-0042"
1240
+ * ```
1241
+ */
1242
+ interface SequenceConfig {
1243
+ /** Namespace within the sequence (e.g., a storeId or orgId). Defaults to ''. */
1244
+ scope?: string;
1245
+ /** Format template. Defaults to `{name}-{seq:4}`. */
1246
+ format?: string;
1247
+ /** Starting counter value. Defaults to 1. */
1248
+ startAt?: number;
1249
+ }
1250
+
1251
+ interface SyncApplyFailureEvent {
1252
+ operationId: string;
1253
+ collection: string;
1254
+ recordId: string;
1255
+ code: string;
1256
+ message: string;
1257
+ retriable: boolean;
1258
+ firstSeenAt: number;
1259
+ retryCount: number;
1260
+ }
1261
+ /**
1262
+ * Trace of a merge decision. Records all inputs and outputs for debugging and DevTools.
1263
+ */
1264
+ interface MergeTrace {
1265
+ operationA: Operation;
1266
+ operationB: Operation;
1267
+ field: string;
1268
+ strategy: string;
1269
+ inputA: unknown;
1270
+ inputB: unknown;
1271
+ base: unknown | null;
1272
+ output: unknown;
1273
+ tier: 1 | 2 | 3;
1274
+ constraintViolated: string | null;
1275
+ duration: number;
1276
+ }
1277
+ /**
1278
+ * All events emitted by the Kora framework.
1279
+ * These are consumed by DevTools and can be observed by the developer.
1280
+ */
1281
+ type KoraEvent = {
1282
+ type: 'operation:created';
1283
+ operation: Operation;
1284
+ } | {
1285
+ type: 'operation:applied';
1286
+ operation: Operation;
1287
+ duration: number;
1288
+ } | {
1289
+ type: 'merge:started';
1290
+ operationA: Operation;
1291
+ operationB: Operation;
1292
+ } | {
1293
+ type: 'merge:completed';
1294
+ trace: MergeTrace;
1295
+ } | {
1296
+ type: 'merge:conflict';
1297
+ trace: MergeTrace;
1298
+ } | {
1299
+ type: 'constraint:violated';
1300
+ constraint: string;
1301
+ trace: MergeTrace;
1302
+ } | {
1303
+ type: 'sync:connected';
1304
+ nodeId: string;
1305
+ } | {
1306
+ type: 'sync:disconnected';
1307
+ reason: string;
1308
+ } | {
1309
+ type: 'sync:schema-mismatch';
1310
+ clientSchemaVersion: number;
1311
+ serverSchemaVersion: number;
1312
+ supportedMin: number;
1313
+ supportedMax: number;
1314
+ reason: string;
1315
+ } | {
1316
+ /**
1317
+ * A sync peer speaks an older, deprecated protocol version (protocol v2): a
1318
+ * server emits it for a protocol-1 client (Kora <= beta.12), accepted in
1319
+ * beta.13 only. `message` says what to upgrade.
1320
+ */
1321
+ type: 'sync:protocol-deprecated';
1322
+ nodeId: string;
1323
+ clientProtocolVersion: number;
1324
+ serverProtocolVersion: number;
1325
+ message: string;
1326
+ } | {
1327
+ /**
1328
+ * A server stored a protocol-1 (Kora <= beta.12) client's operation whose
1329
+ * version-1 id it could not verify (RT-71: beta.12 hashed `undefined` members
1330
+ * as `null`, which the JSON upload no longer holds). Counted in metrics.
1331
+ */
1332
+ type: 'sync:unverified-legacy-operation';
1333
+ nodeId: string;
1334
+ operationId: string;
1335
+ collection: string;
1336
+ message: string;
1337
+ } | {
1338
+ /**
1339
+ * A server refused an upload that reused a stored operation's id with different
1340
+ * content (RT-77). Operation ids are content hashes, so this is tampering; the
1341
+ * upload had no effect. Counted in metrics.
1342
+ */
1343
+ type: 'sync:forged-duplicate';
1344
+ nodeId: string;
1345
+ operationId: string;
1346
+ collection: string;
1347
+ message: string;
1348
+ } | {
1349
+ type: 'sync:auth-failed';
1350
+ reason: string;
1351
+ } | {
1352
+ type: 'sync:suspended';
1353
+ reason: 'auth-loading' | 'auth-required' | 'auth-rejected' | 'device-revoked' | string;
1354
+ } | {
1355
+ type: 'sync:apply-blocked';
1356
+ failure: SyncApplyFailureEvent;
1357
+ } | {
1358
+ type: 'sync:apply-retrying';
1359
+ failure: SyncApplyFailureEvent;
1360
+ } | {
1361
+ type: 'sync:apply-recovered';
1362
+ failure: SyncApplyFailureEvent;
1363
+ } | {
1364
+ type: 'sync:apply-abandoned';
1365
+ failure: SyncApplyFailureEvent;
1366
+ } | {
1367
+ /**
1368
+ * End-to-end encryption keyring status changed (ENC-1): locked (no passphrase,
1369
+ * a wrong one, or a key record that must not be used), unlocking, unlocked.
1370
+ * While not unlocked, sync is paused; local reads and writes continue.
1371
+ */
1372
+ type: 'encryption:status';
1373
+ status: {
1374
+ state: 'locked' | 'unlocking' | 'unlocked' | 'error';
1375
+ keyring: string;
1376
+ keyVersion: number | null;
1377
+ code?: string;
1378
+ message?: string;
1379
+ };
1380
+ } | {
1381
+ type: 'sync:clock-skew';
1382
+ /** serverTime - localTime in ms. Negative = this device's clock is fast. */
1383
+ skewMs: number;
1384
+ severity: 'info' | 'slow-warning' | 'fast-blocked';
1385
+ source: 'handshake' | 'server-reject';
1386
+ } | {
1387
+ /**
1388
+ * The server refused this device's node id (`NODE_ID_CLAIMED`, RT-21), so the
1389
+ * device moved to a fresh node id and re-queued its unsynced writes under it.
1390
+ */
1391
+ type: 'sync:node-id-rotated';
1392
+ previousNodeId: string;
1393
+ nodeId: string;
1394
+ /** Unsynced operations re-authored under the new node id. */
1395
+ reenqueuedCount: number;
1396
+ /**
1397
+ * Unsynced operations left under the previous node id, held for the principal
1398
+ * that owns it (RT-38): they upload when that user signs in again on this device.
1399
+ */
1400
+ heldCount?: number;
1401
+ } | {
1402
+ /**
1403
+ * Sync bookkeeping of one of this database's own node ids (Phase 2):
1404
+ * - `history-behind`: the server holds more of this node's operations than the
1405
+ * device (its log lost a tail, RT-35); the counter was raised past them and a
1406
+ * full resync fetches them back.
1407
+ * - `adoption-started` / `adoption-completed` / `adoption-refused`: the engine
1408
+ * uploads the unsynced writes of a node no live tab uses (RT-40).
1409
+ * - `held`: the server refused this node for the signed-in principal; its unsynced
1410
+ * writes wait for the principal that owns it (RT-38).
1411
+ * - `server-behind`: the server holds fewer of this node's operations than the
1412
+ * device had acknowledged (a server restored from a backup, RT-45); the device
1413
+ * re-uploads them from the server's position.
1414
+ * - `adoption-parked`: an adopted node made no upload progress for a whole session
1415
+ * (for example a write the server keeps deferring); the next session tries the
1416
+ * other nodes first (RT-46).
1417
+ * - `clone-detected`: another live copy of this database uses the same node id
1418
+ * (copied app data, a restored image); this copy moved to a fresh node id (RT-44).
1419
+ * - `principal-switched`: the signed-in user changed; local writes from now on are
1420
+ * authored under that user's own node (RT-42).
1421
+ * - `held-assigned` / `held-discarded`: the app assigned a held node's writes to
1422
+ * the signed-in user, or discarded them from sync (RT-50).
1423
+ */
1424
+ type: 'sync:local-node';
1425
+ nodeId: string;
1426
+ action: 'history-behind' | 'server-behind' | 'adoption-started' | 'adoption-completed' | 'adoption-refused' | 'adoption-parked' | 'held' | 'clone-detected' | 'principal-switched' | 'held-assigned' | 'held-discarded';
1427
+ /** For `history-behind` / `server-behind`: the device's sequence and the server's. */
1428
+ localSequence?: number;
1429
+ serverSequence?: number;
1430
+ /** Unsynced operations concerned, when known. */
1431
+ operationCount?: number;
1432
+ } | {
1433
+ type: 'sync:clock-rebase';
1434
+ /** Number of unsynced operations that were re-stamped. */
1435
+ rebasedCount: number;
1436
+ /** How far ahead of server time the most future queued operation was, in ms. */
1437
+ maxSkewMs: number;
1438
+ } | {
1439
+ type: 'sync:sent';
1440
+ operations: Operation[];
1441
+ batchSize: number;
1442
+ } | {
1443
+ type: 'sync:received';
1444
+ operations: Operation[];
1445
+ batchSize: number;
1446
+ /** Server-ingest outcomes. Omitted for ordinary client-side receive events. */
1447
+ uniqueOperations?: number;
1448
+ duplicateOperations?: number;
1449
+ rejectedOperations?: number;
1450
+ } | {
1451
+ type: 'sync:acknowledged';
1452
+ sequenceNumber: number;
1453
+ } | {
1454
+ type: 'sync:scope-retracted';
1455
+ collection: string;
1456
+ recordId: string;
1457
+ quarantinedOperationIds: string[];
1458
+ } | {
1459
+ type: 'sync:apply-failed';
1460
+ operationId: string;
1461
+ collection: string;
1462
+ recordId: string;
1463
+ code: string;
1464
+ message: string;
1465
+ retriable: boolean;
1466
+ } | {
1467
+ /**
1468
+ * The server rejected one of THIS client's outbound operations before it
1469
+ * became authoritative. The op has been diverted out of the pending sync
1470
+ * queue into the durable rejected store (kept, not retried); the app can
1471
+ * surface the reason and decide whether to roll back the optimistic local
1472
+ * write or let the user edit and resubmit.
1473
+ */
1474
+ type: 'sync:operation-rejected';
1475
+ operationId: string;
1476
+ collection: string;
1477
+ recordId: string;
1478
+ code: string;
1479
+ message: string;
1480
+ retriable: boolean;
1481
+ } | {
1482
+ type: 'sync:delivery-gap';
1483
+ expectedBase: number;
1484
+ receivedBase: number;
1485
+ currentWatermark: number;
1486
+ messageId: string;
1487
+ repeatCount: number;
1488
+ } | {
1489
+ type: 'sync:delivery-stalled';
1490
+ sessionId: string;
1491
+ watermark: number;
1492
+ outstandingMaxDeliverySequence: number;
1493
+ repeatCount: number;
1494
+ reason: 'unacknowledged-delivery';
1495
+ } | {
1496
+ type: 'query:subscribed';
1497
+ queryId: string;
1498
+ collection: string;
1499
+ } | {
1500
+ type: 'query:invalidated';
1501
+ queryId: string;
1502
+ trigger: Operation;
1503
+ } | {
1504
+ type: 'query:executed';
1505
+ queryId: string;
1506
+ duration: number;
1507
+ resultCount: number;
1508
+ } | {
1509
+ /**
1510
+ * A reactive query subscription failed (STORE-12). `phase` is `initial` (the
1511
+ * first run), `refresh` (a re-run after a write) or `callback` (the
1512
+ * subscriber's callback threw). The subscription stays registered and keeps
1513
+ * its last results; the subscriber's `onError` receives the same failure.
1514
+ */
1515
+ type: 'query:error';
1516
+ queryId: string;
1517
+ collection: string;
1518
+ phase: 'initial' | 'refresh' | 'callback';
1519
+ code: string;
1520
+ message: string;
1521
+ } | {
1522
+ /**
1523
+ * Durable-storage (`navigator.storage.persist`) state (NEW-STORE-4).
1524
+ * `checked`: the boot-time `persisted()` read, which never prompts.
1525
+ * `requested`: the outcome of an explicit `app.storage.persistence.request()`.
1526
+ * `unsupported`: the runtime has no StorageManager persistence API.
1527
+ * `error`: the check or request threw (`message` says why).
1528
+ */
1529
+ type: 'storage:persistence';
1530
+ state: 'checked' | 'requested' | 'unsupported' | 'error';
1531
+ persisted: boolean;
1532
+ message?: string;
1533
+ } | {
1534
+ type: 'connection:quality';
1535
+ quality: ConnectionQuality;
1536
+ } | {
1537
+ type: 'sync:diagnostics';
1538
+ diagnostics: SyncDiagnosticsSnapshot;
1539
+ } | {
1540
+ type: 'sync:bandwidth';
1541
+ bytesPerSecond: number;
1542
+ direction: 'in' | 'out';
1543
+ } | {
1544
+ type: 'sync:initial-sync-progress';
1545
+ progress: number;
1546
+ totalBatches: number;
1547
+ receivedBatches: number;
1548
+ } | {
1549
+ type: 'awareness:updated';
1550
+ states: Map<number, unknown>;
1551
+ } | {
1552
+ type: 'state-machine:transition';
1553
+ collection: string;
1554
+ recordId: string;
1555
+ from: string;
1556
+ to: string;
1557
+ valid: boolean;
1558
+ } | {
1559
+ type: 'state-machine:rejected';
1560
+ collection: string;
1561
+ recordId: string;
1562
+ from: string;
1563
+ to: string;
1564
+ allowed: string[];
1565
+ } | {
1566
+ type: 'store:persistence-error';
1567
+ dbName: string;
1568
+ message: string;
1569
+ code: string;
1570
+ } | {
1571
+ /**
1572
+ * The local database could not be made durable before an upload several times in
1573
+ * a row (storage quota exceeded, IndexedDB broken; RT-49). Uploads no longer wait
1574
+ * for it, so the server holds the only durable copy of new writes (a reload
1575
+ * recovers them from it) until `sync:durability-restored`. Writes are still
1576
+ * accepted; warn the user (free up storage, stay online).
1577
+ */
1578
+ type: 'sync:durability-degraded';
1579
+ message: string;
1580
+ failedAttempts: number;
1581
+ } | {
1582
+ type: 'sync:durability-restored';
1583
+ } | {
1584
+ type: 'store:quota-exceeded';
1585
+ dbName: string;
1586
+ message: string;
1587
+ } | {
1588
+ /**
1589
+ * The log-integrity scan (W8 step 0) changed the operation log: it repaired rows
1590
+ * written by an earlier release (a JSON-encoded timestamp from a beta.12 backup
1591
+ * restore) and/or moved rows it could not repair to the quarantine table, where
1592
+ * no fold reads them. `clean` is false while anything is quarantined or this
1593
+ * database's own nodes have sequence gaps. See `store.verifyLogIntegrity()`.
1594
+ */
1595
+ type: 'store:log-integrity';
1596
+ dbName: string;
1597
+ repaired: number;
1598
+ quarantined: number;
1599
+ gaps: number;
1600
+ clean: boolean;
1601
+ message: string;
1602
+ } | {
1603
+ /**
1604
+ * The database was re-materialized with the W7 per-field fold (once, on the
1605
+ * first open with fold-state version N, or after a replace-mode restore).
1606
+ * `mode`: `'log'` rebuilt every record from its clean log (repairing
1607
+ * pre-W7 divergence); `'snapshot+log'` used each row as a base snapshot
1608
+ * because the log was compacted or has gaps; `'kept'` left every row as it
1609
+ * was because the log has quarantined rows (never rebuilt from).
1610
+ */
1611
+ type: 'store:rematerialized';
1612
+ dbName: string;
1613
+ mode: 'log' | 'snapshot+log' | 'kept';
1614
+ records: number;
1615
+ changedRows: number;
1616
+ message: string;
1617
+ } | {
1618
+ /**
1619
+ * OPFS persistence was unavailable, so the store fell back to a
1620
+ * NON-PERSISTENT in-memory database. Anything written this session is lost
1621
+ * on reload. This is emitted instead of failing silently so the condition
1622
+ * is observable rather than a data-loss trap.
1623
+ */
1624
+ type: 'store:opfs-unavailable';
1625
+ dbName: string;
1626
+ /**
1627
+ * Why OPFS could not be used. `lock-conflict` means another runtime on this
1628
+ * origin already holds the OPFS pool for this database; `timeout` means the
1629
+ * VFS install did not complete in time (common in headless CI); `unsupported`
1630
+ * means the runtime has no usable OPFS.
1631
+ */
1632
+ reason: 'lock-conflict' | 'timeout' | 'unsupported';
1633
+ message: string;
1634
+ } | {
1635
+ /**
1636
+ * BLOCKING. The local database was already migrated by a NEWER build of the app
1637
+ * (its stored schema version is above this code's `schema.version`), for example
1638
+ * an old build served offline after an update ran (RT-109). The store refuses to
1639
+ * open it (`SchemaVersionAheadError`, `app.ready` rejects) and changes nothing in
1640
+ * it. Apps should ask the user to reload online so the newer build runs.
1641
+ */
1642
+ type: 'store:schema-ahead';
1643
+ dbName: string;
1644
+ /** The schema version stored in the database */
1645
+ storedVersion: number;
1646
+ /** This code's `schema.version` */
1647
+ codeVersion: number;
1648
+ message: string;
1649
+ } | {
1650
+ /**
1651
+ * A preferred storage backend could not provide durable storage, so Kora
1652
+ * promoted the app to another durable backend before user code observed the
1653
+ * store. This is informational: data still survives reloads.
1654
+ */
1655
+ type: 'store:storage-fallback';
1656
+ dbName: string;
1657
+ from: 'opfs' | 'sqlite-wasm';
1658
+ to: 'indexeddb';
1659
+ reason: 'lock-conflict' | 'timeout' | 'unsupported';
1660
+ message: string;
1661
+ } | {
1662
+ /**
1663
+ * BLOCKING. The store could not obtain durable storage when it opened or when
1664
+ * this tab was promoted to storage leader, so it is running on a
1665
+ * non-persistent in-memory database. Writes are refused with
1666
+ * `StorageDurabilityError` (unless the app opted into non-durable storage)
1667
+ * instead of being accepted and lost on reload. Apps should show a blocking
1668
+ * state, for example asking the user to close other tabs and reload.
1669
+ */
1670
+ type: 'store:durability-lost';
1671
+ dbName: string;
1672
+ phase: 'open' | 'promotion';
1673
+ reason: 'lock-conflict' | 'timeout' | 'unsupported' | 'open-failed';
1674
+ message: string;
1675
+ } | {
1676
+ /**
1677
+ * BLOCKING while `state` is `waiting`. Another holder (a previous storage
1678
+ * owner still shutting down, or a tab running an older Kora that does not
1679
+ * take part in the ownership protocol) has the database's OPFS storage, so
1680
+ * the open waits instead of falling back to non-durable storage. Apps
1681
+ * should show a "close other tabs of this app" state; `resolved` follows
1682
+ * when the wait ends.
1683
+ */
1684
+ type: 'store:storage-blocked';
1685
+ dbName: string;
1686
+ /** `pool`: the database's own pool; `legacy-pool`: the pre-W8a shared pool. */
1687
+ resource: 'pool' | 'legacy-pool';
1688
+ state: 'waiting' | 'resolved';
1689
+ waitedMs?: number;
1690
+ message: string;
1691
+ } | {
1692
+ /**
1693
+ * Informational. The database's data was moved between storage locations
1694
+ * explicitly (the pre-W8a shared OPFS pool to the database's own pool, or
1695
+ * between OPFS and IndexedDB), so there is still exactly one copy.
1696
+ */
1697
+ type: 'store:storage-migrated';
1698
+ dbName: string;
1699
+ from: 'legacy-opfs-pool' | 'opfs' | 'indexeddb';
1700
+ to: 'opfs' | 'indexeddb';
1701
+ message: string;
1702
+ } | {
1703
+ /**
1704
+ * Another runtime on this origin was already using this database name, so
1705
+ * this runtime attached to it as a follower and now SHARES that one
1706
+ * database. That is intended for multiple tabs of the SAME app; it is a bug
1707
+ * if these are logically separate apps, which should each use a distinct
1708
+ * store name (`store: { name: '...' }`) to stay isolated.
1709
+ */
1710
+ type: 'store:db-name-collision';
1711
+ dbName: string;
1712
+ message: string;
1713
+ } | {
1714
+ type: 'replay:completed';
1715
+ targetOperationId: string;
1716
+ operationsApplied: number;
1717
+ duration: number;
1718
+ };
1719
+ /** Extract the event type string union from KoraEvent */
1720
+ type KoraEventType = KoraEvent['type'];
1721
+ /** Extract a specific event by its type */
1722
+ type KoraEventByType<T extends KoraEventType> = Extract<KoraEvent, {
1723
+ type: T;
1724
+ }>;
1725
+ /** Listener function for a specific event type */
1726
+ type KoraEventListener<T extends KoraEventType> = (event: KoraEventByType<T>) => void;
1727
+ /**
1728
+ * Event emitter interface for the Kora framework.
1729
+ * All packages that emit events must implement this interface.
1730
+ */
1731
+ interface KoraEventEmitter {
1732
+ on<T extends KoraEventType>(type: T, listener: KoraEventListener<T>): () => void;
1733
+ off<T extends KoraEventType>(type: T, listener: KoraEventListener<T>): void;
1734
+ emit<T extends KoraEventType>(event: KoraEventByType<T>): void;
1735
+ }
1736
+
1737
+ export { type MergeStrategy as $, type AtomicOp as A, type BlobRef as B, type CollectionDefinition as C, type InferInsert as D, type EncryptedEnvelopeField as E, FieldBuilder as F, type InferInsertInput as G, type HLCTimestamp as H, type InferFieldInput as I, type InferRecord as J, type InferRecordField as K, type InferUpdate as L, type MergeTrace as M, type InferUpdateField as N, type OperationInput as O, type InferUpdateInput as P, JsonFieldBuilder as Q, type RandomSource as R, type SchemaDefinition as S, type TransitionMap as T, type KoraEvent as U, type VersionVector as V, type KoraEventByType as W, type KoraEventEmitter as X, type KoraEventListener as Y, type KoraEventType as Z, MERGE_STRATEGIES as _, type Operation as a, MigrationBuilder as a0, type NumericAtomicOpSentinel as a1, ObjectFieldBuilder as a2, type ObjectFieldValue as a3, type OnDeleteAction as a4, type RelationDefinition as a5, type RelationType as a6, type RichtextInput as a7, RollbackBuilder as a8, type ScalarFieldBuilder as a9, type ScalarFieldKind as aa, SecretFieldBuilder as ab, type SecretMode as ac, type SequenceConfig as ad, type Simplify as ae, type StateMachineDefinition as af, type SyncDiagnosticsSnapshot as ag, type SyncRuleDefinition as ah, type TimeSource as ai, applyAtomicOp as aj, isAtomicOp as ak, migrate as al, op as am, resolveAtomicOp as an, t as ao, toAtomicOp as ap, type CustomResolver as b, type MigrationDefinition as c, type FieldDescriptor as d, type OperationType as e, type MigrationStep as f, type StateMachineConstraint as g, type TransitionValidationResult as h, type ArrayAtomicOpSentinel as i, ArrayFieldBuilder as j, type AtomicOpSentinel as k, type AtomicOpType as l, type BackfillOptions as m, CONNECTION_QUALITIES as n, type ConnectionQuality as o, type Constraint as p, type EncryptedOperationEnvelope as q, EnumFieldBuilder as r, type FieldInput as s, type FieldKind as t, type FieldKindToType as u, type FieldMap as v, type FieldMergeStrategy as w, type FieldOutput as x, type FieldTypeInfo as y, type InferFieldType as z };