@korajs/core 1.0.0-beta.12 → 1.0.0-beta.14
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-CItP1gsz.d.cts → build-scope-map-C9zBTJ1R.d.cts} +1 -1
- package/dist/{build-scope-map-Cv5HfCQP.d.ts → build-scope-map-CZaMyZLV.d.ts} +1 -1
- package/dist/{chunk-5IICSH6H.js → chunk-QUD3DMSD.js} +668 -24
- package/dist/chunk-QUD3DMSD.js.map +1 -0
- package/dist/events-Da29Dt_3.d.cts +1737 -0
- package/dist/events-Da29Dt_3.d.ts +1737 -0
- package/dist/index.cjs +2548 -207
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +810 -220
- package/dist/index.d.ts +810 -220
- package/dist/index.js +2028 -334
- 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-BOeuiyqu.d.cts +622 -0
- package/dist/sql-gen-CXTA6w-w.d.ts +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
|
@@ -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 with this
|
|
1319
|
+
* deprecation in beta.13 and beta.14. `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 };
|