@korajs/core 0.5.0 → 1.0.0-beta.0
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/LICENSE +21 -0
- package/dist/bindings/index.cjs +105 -0
- package/dist/bindings/index.cjs.map +1 -0
- package/dist/bindings/index.d.cts +103 -0
- package/dist/bindings/index.d.ts +103 -0
- package/dist/bindings/index.js +78 -0
- package/dist/bindings/index.js.map +1 -0
- package/dist/build-scope-map-BIeawJzC.d.cts +33 -0
- package/dist/build-scope-map-DOf4JLTh.d.ts +33 -0
- package/dist/{chunk-H4FXU5OP.js → chunk-5IICSH6H.js} +159 -7
- package/dist/chunk-5IICSH6H.js.map +1 -0
- package/dist/{events-BeIEDJBW.d.cts → events-BynBOsO3.d.cts} +14 -93
- package/dist/{events-BeIEDJBW.d.ts → events-BynBOsO3.d.ts} +14 -93
- package/dist/index.cjs +284 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -33
- package/dist/index.d.ts +79 -33
- package/dist/index.js +125 -3
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +142 -17
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +2 -2
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +1 -1
- package/dist/operation-BpZlYSpe.d.cts +164 -0
- package/dist/operation-D5WOZYvy.d.ts +164 -0
- package/package.json +11 -1
- package/dist/chunk-H4FXU5OP.js.map +0 -1
package/dist/internal.cjs
CHANGED
|
@@ -121,6 +121,7 @@ var KORA_ERROR_FIX_SUGGESTIONS = {
|
|
|
121
121
|
SYNC_ERROR: "Verify the sync server URL, auth token, and that the server accepts your schema version.",
|
|
122
122
|
STORAGE_ERROR: "Confirm the storage adapter is supported in this environment and the database path is writable.",
|
|
123
123
|
CLOCK_DRIFT: "Sync device wall-clock time (NTP). Kora refuses new timestamps when drift exceeds five minutes.",
|
|
124
|
+
INVALID_TIMESTAMP_FIELDS: "HLC timestamps need non-negative integer wallTime (< 10^15) and logical (<= 99999). Generate timestamps through HybridLogicalClock instead of building them by hand.",
|
|
124
125
|
PERSISTENCE_ERROR: "IndexedDB persistence failed; check browser storage settings and available disk quota.",
|
|
125
126
|
MISSING_WORKER_URL: "Pass store.workerUrl pointing to sqlite-wasm-worker.js (see create-kora-app templates).",
|
|
126
127
|
INVALID_SYNC_URL: "Use ws:// or wss:// for WebSocket transport, or http(s):// for HTTP long-polling.",
|
|
@@ -157,42 +158,79 @@ var OperationError = class extends KoraError {
|
|
|
157
158
|
this.name = "OperationError";
|
|
158
159
|
}
|
|
159
160
|
};
|
|
160
|
-
var
|
|
161
|
-
constructor(
|
|
162
|
-
const
|
|
161
|
+
var RemoteClockDriftError = class extends KoraError {
|
|
162
|
+
constructor(remoteWallTime, localReferenceTime) {
|
|
163
|
+
const aheadSeconds = Math.round((remoteWallTime - localReferenceTime) / 1e3);
|
|
163
164
|
super(
|
|
164
|
-
`
|
|
165
|
-
"
|
|
166
|
-
{
|
|
165
|
+
`Rejected remote timestamp ${aheadSeconds}s ahead of local reference time. The sending device's clock is set too far in the future.`,
|
|
166
|
+
"REMOTE_CLOCK_DRIFT",
|
|
167
|
+
{ remoteWallTime, localReferenceTime, aheadSeconds }
|
|
167
168
|
);
|
|
168
|
-
this.
|
|
169
|
-
this.
|
|
170
|
-
this.name = "
|
|
169
|
+
this.remoteWallTime = remoteWallTime;
|
|
170
|
+
this.localReferenceTime = localReferenceTime;
|
|
171
|
+
this.name = "RemoteClockDriftError";
|
|
171
172
|
}
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
remoteWallTime;
|
|
174
|
+
localReferenceTime;
|
|
175
|
+
};
|
|
176
|
+
var InvalidTimestampError = class extends KoraError {
|
|
177
|
+
constructor(message, wallTime, logical) {
|
|
178
|
+
super(message, "INVALID_TIMESTAMP_FIELDS", { wallTime, logical });
|
|
179
|
+
this.wallTime = wallTime;
|
|
180
|
+
this.logical = logical;
|
|
181
|
+
this.name = "InvalidTimestampError";
|
|
182
|
+
}
|
|
183
|
+
wallTime;
|
|
184
|
+
logical;
|
|
174
185
|
};
|
|
175
186
|
|
|
176
187
|
// src/clock/hlc.ts
|
|
177
188
|
var systemTimeSource = { now: () => Date.now() };
|
|
189
|
+
var MAX_LOGICAL = 99999;
|
|
190
|
+
var LOGICAL_MODULUS = MAX_LOGICAL + 1;
|
|
191
|
+
var WALL_TIME_LIMIT = 10 ** 15;
|
|
178
192
|
var DRIFT_WARN_MS = 6e4;
|
|
179
193
|
var DRIFT_ERROR_MS = 5 * 6e4;
|
|
180
|
-
var
|
|
181
|
-
|
|
194
|
+
var MAX_REMOTE_FUTURE_MS = 5 * 6e4;
|
|
195
|
+
var HybridLogicalClock = class _HybridLogicalClock {
|
|
196
|
+
constructor(nodeId, timeSource = systemTimeSource, onDriftWarning, onDriftError) {
|
|
182
197
|
this.nodeId = nodeId;
|
|
183
198
|
this.timeSource = timeSource;
|
|
184
199
|
this.onDriftWarning = onDriftWarning;
|
|
200
|
+
this.onDriftError = onDriftError;
|
|
185
201
|
}
|
|
186
202
|
nodeId;
|
|
187
203
|
timeSource;
|
|
188
204
|
onDriftWarning;
|
|
205
|
+
onDriftError;
|
|
189
206
|
wallTime = 0;
|
|
190
207
|
logical = 0;
|
|
208
|
+
referenceOffsetMs = null;
|
|
209
|
+
/**
|
|
210
|
+
* Records the known offset between an external time reference (usually the
|
|
211
|
+
* sync server, learned at handshake) and this device's physical clock:
|
|
212
|
+
* `referenceTime - localPhysicalTime`. Once set, drift evaluation and remote
|
|
213
|
+
* timestamp validation are performed against reference-corrected time, so a
|
|
214
|
+
* device with a wrong local clock still validates remote timestamps correctly.
|
|
215
|
+
*/
|
|
216
|
+
setReferenceOffset(offsetMs) {
|
|
217
|
+
this.referenceOffsetMs = offsetMs;
|
|
218
|
+
}
|
|
219
|
+
getReferenceOffset() {
|
|
220
|
+
return this.referenceOffsetMs;
|
|
221
|
+
}
|
|
222
|
+
/** Physical time corrected by the known reference offset, when available. */
|
|
223
|
+
effectiveTime(physicalTime) {
|
|
224
|
+
return physicalTime + (this.referenceOffsetMs ?? 0);
|
|
225
|
+
}
|
|
191
226
|
/**
|
|
192
227
|
* Generate a new timestamp for a local event.
|
|
193
228
|
* Guarantees monotonicity: each call returns a timestamp strictly greater than the previous.
|
|
194
229
|
*
|
|
195
|
-
*
|
|
230
|
+
* Never throws and never blocks a local write: if the physical clock has
|
|
231
|
+
* fallen behind the HLC (e.g. the user corrected a fast clock), the HLC
|
|
232
|
+
* freezes wallTime and advances the logical counter, and drift is reported
|
|
233
|
+
* through the onDriftWarning / onDriftError callbacks instead.
|
|
196
234
|
*/
|
|
197
235
|
now() {
|
|
198
236
|
const physicalTime = this.timeSource.now();
|
|
@@ -203,17 +241,40 @@ var HybridLogicalClock = class {
|
|
|
203
241
|
} else {
|
|
204
242
|
this.logical++;
|
|
205
243
|
}
|
|
244
|
+
this.carryLogicalOverflow();
|
|
206
245
|
return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
|
|
207
246
|
}
|
|
208
247
|
/**
|
|
209
248
|
* Update clock on receiving a remote timestamp.
|
|
210
249
|
* Merges the remote clock state with the local state to maintain causal ordering.
|
|
211
250
|
*
|
|
212
|
-
* @throws {
|
|
251
|
+
* @throws {InvalidTimestampError} If the remote timestamp has non-integer or
|
|
252
|
+
* negative wallTime/logical, or logical beyond {@link MAX_LOGICAL}. Checked
|
|
253
|
+
* BEFORE any state change, so malformed input cannot corrupt this clock.
|
|
254
|
+
* @throws {RemoteClockDriftError} If the remote timestamp is more than 5 minutes
|
|
255
|
+
* ahead of reference-corrected local time. Validation happens BEFORE any state
|
|
256
|
+
* is adopted, so a rejected timestamp cannot poison this clock. When no
|
|
257
|
+
* reference offset is known and this clock is uninitialized (cold start with
|
|
258
|
+
* a possibly-wrong local clock), validation is skipped, matching the previous
|
|
259
|
+
* cold-start behavior but now without a corrupting failure mode afterward.
|
|
213
260
|
*/
|
|
214
261
|
receive(remote) {
|
|
215
262
|
const physicalTime = this.timeSource.now();
|
|
216
263
|
const wasColdStart = this.wallTime === 0;
|
|
264
|
+
if (!Number.isInteger(remote.wallTime) || remote.wallTime < 0 || !Number.isInteger(remote.logical) || remote.logical < 0 || remote.logical > MAX_LOGICAL) {
|
|
265
|
+
throw new InvalidTimestampError(
|
|
266
|
+
`Rejected remote HLC timestamp with invalid fields (wallTime=${remote.wallTime}, logical=${remote.logical}). wallTime and logical must be non-negative integers and logical must not exceed ${MAX_LOGICAL}.`,
|
|
267
|
+
remote.wallTime,
|
|
268
|
+
remote.logical
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const canValidate = this.referenceOffsetMs !== null || !wasColdStart;
|
|
272
|
+
if (canValidate) {
|
|
273
|
+
const reference = this.effectiveTime(physicalTime);
|
|
274
|
+
if (remote.wallTime > reference + MAX_REMOTE_FUTURE_MS) {
|
|
275
|
+
throw new RemoteClockDriftError(remote.wallTime, reference);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
217
278
|
if (physicalTime > this.wallTime && physicalTime > remote.wallTime) {
|
|
218
279
|
this.wallTime = physicalTime;
|
|
219
280
|
this.logical = 0;
|
|
@@ -225,11 +286,54 @@ var HybridLogicalClock = class {
|
|
|
225
286
|
} else {
|
|
226
287
|
this.logical++;
|
|
227
288
|
}
|
|
289
|
+
this.carryLogicalOverflow();
|
|
228
290
|
if (!wasColdStart) {
|
|
229
291
|
this.checkDrift(physicalTime);
|
|
230
292
|
}
|
|
231
293
|
return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
|
|
232
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Advance this clock to at least the given timestamp.
|
|
297
|
+
*
|
|
298
|
+
* Used after a timestamp rebase rewrites unsynced operations: future `now()`
|
|
299
|
+
* timestamps must sort after every rebased operation, otherwise a write issued
|
|
300
|
+
* immediately after the rebase could interleave with (or precede) rebased ops
|
|
301
|
+
* and break the log's total order. Never moves the clock backward — a
|
|
302
|
+
* timestamp at or before the current state is a no-op, preserving the
|
|
303
|
+
* monotonicity guarantee of `now()`.
|
|
304
|
+
*
|
|
305
|
+
* Inputs with logical > MAX_LOGICAL are normalized deterministically by
|
|
306
|
+
* carrying the excess into wallTime, so the adopted state always leaves room
|
|
307
|
+
* for the next increment inside the serializable range.
|
|
308
|
+
*/
|
|
309
|
+
advanceTo(ts) {
|
|
310
|
+
const normalized = {
|
|
311
|
+
wallTime: ts.wallTime + Math.floor(ts.logical / LOGICAL_MODULUS),
|
|
312
|
+
logical: ts.logical % LOGICAL_MODULUS,
|
|
313
|
+
nodeId: ts.nodeId
|
|
314
|
+
};
|
|
315
|
+
const current = {
|
|
316
|
+
wallTime: this.wallTime,
|
|
317
|
+
logical: this.logical,
|
|
318
|
+
nodeId: this.nodeId
|
|
319
|
+
};
|
|
320
|
+
if (_HybridLogicalClock.compare(normalized, current) > 0) {
|
|
321
|
+
this.wallTime = normalized.wallTime;
|
|
322
|
+
this.logical = normalized.logical;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Carry the logical counter into wallTime when an increment pushed it past
|
|
327
|
+
* MAX_LOGICAL. Bumping wallTime by 1ms keeps the timestamp strictly greater
|
|
328
|
+
* than everything issued before (monotonicity) while keeping the logical
|
|
329
|
+
* counter inside the 5-digit slot the serialized form depends on.
|
|
330
|
+
*/
|
|
331
|
+
carryLogicalOverflow() {
|
|
332
|
+
if (this.logical > MAX_LOGICAL) {
|
|
333
|
+
this.wallTime += 1;
|
|
334
|
+
this.logical = 0;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
233
337
|
/**
|
|
234
338
|
* Compare two timestamps. Returns negative if a < b, positive if a > b, zero if equal.
|
|
235
339
|
* Total order: wallTime first, then logical, then nodeId (lexicographic).
|
|
@@ -244,8 +348,23 @@ var HybridLogicalClock = class {
|
|
|
244
348
|
/**
|
|
245
349
|
* Serialize an HLC timestamp to a string that sorts lexicographically.
|
|
246
350
|
* Format: zero-padded wallTime:logical:nodeId
|
|
351
|
+
*
|
|
352
|
+
* @throws {InvalidTimestampError} If wallTime does not fit 15 digits or
|
|
353
|
+
* logical does not fit 5 digits (or either is negative/non-integer). Such a
|
|
354
|
+
* value would overflow its zero-padded slot and the serialized string would
|
|
355
|
+
* no longer sort in the same order as {@link HybridLogicalClock.compare} —
|
|
356
|
+
* silently corrupting every LWW comparison on stored `_version` columns.
|
|
357
|
+
* Internal clocks can no longer produce such values (the logical counter
|
|
358
|
+
* carries into wallTime); this guards against hand-built timestamps.
|
|
247
359
|
*/
|
|
248
360
|
static serialize(ts) {
|
|
361
|
+
if (!Number.isInteger(ts.wallTime) || ts.wallTime < 0 || ts.wallTime >= WALL_TIME_LIMIT || !Number.isInteger(ts.logical) || ts.logical < 0 || ts.logical > MAX_LOGICAL) {
|
|
362
|
+
throw new InvalidTimestampError(
|
|
363
|
+
`Cannot serialize HLC timestamp (wallTime=${ts.wallTime}, logical=${ts.logical}): wallTime must be an integer in [0, 10^15) and logical an integer in [0, ${MAX_LOGICAL}]. Values outside these ranges overflow the zero-padded serialized form, which must sort lexicographically identically to HybridLogicalClock.compare.`,
|
|
364
|
+
ts.wallTime,
|
|
365
|
+
ts.logical
|
|
366
|
+
);
|
|
367
|
+
}
|
|
249
368
|
const wall = ts.wallTime.toString().padStart(15, "0");
|
|
250
369
|
const log = ts.logical.toString().padStart(5, "0");
|
|
251
370
|
return `${wall}:${log}:${ts.nodeId}`;
|
|
@@ -265,10 +384,16 @@ var HybridLogicalClock = class {
|
|
|
265
384
|
nodeId: parts.slice(2).join(":")
|
|
266
385
|
};
|
|
267
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Reports drift between the HLC and (reference-corrected) physical time.
|
|
389
|
+
* Reporting only: local timestamp generation is never blocked, because the
|
|
390
|
+
* user's data always outranks the quality of its timestamps.
|
|
391
|
+
*/
|
|
268
392
|
checkDrift(physicalTime) {
|
|
269
|
-
const drift = this.wallTime - physicalTime;
|
|
393
|
+
const drift = this.wallTime - this.effectiveTime(physicalTime);
|
|
270
394
|
if (drift > DRIFT_ERROR_MS) {
|
|
271
|
-
|
|
395
|
+
this.onDriftError?.(drift);
|
|
396
|
+
return;
|
|
272
397
|
}
|
|
273
398
|
if (drift > DRIFT_WARN_MS) {
|
|
274
399
|
this.onDriftWarning?.(drift);
|
package/dist/internal.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/internal.ts","../src/operations/content-hash.ts","../src/events/event-emitter.ts","../src/errors/error-fixes.ts","../src/errors/errors.ts","../src/clock/hlc.ts","../src/operations/operation.ts","../src/version-vector/topological-sort.ts"],"sourcesContent":["// Internal exports — shared within @kora packages but NOT part of the public API.\n// Other @kora packages can import from '@korajs/core/internal' if needed.\n\nexport { canonicalize, computeOperationId } from './operations/content-hash'\nexport { SimpleEventEmitter } from './events/event-emitter'\nexport { validateOperationParams } from './operations/operation'\nexport { topologicalSort } from './version-vector/topological-sort'\n","import type { OperationInput } from '../types'\n\n/**\n * Compute the content-addressed ID for an operation using SHA-256.\n * The same operation content always produces the same hash, ensuring deduplication.\n *\n * @param input - The operation input (without id/timestamp, which are assigned separately)\n * @param timestamp - The HLC timestamp serialized as a string\n * @returns A hex-encoded SHA-256 hash\n */\nexport async function computeOperationId(\n\tinput: OperationInput,\n\ttimestamp: string,\n): Promise<string> {\n\t// Only include atomicOps when present — ensures backward compatibility\n\t// (existing operations without atomicOps produce identical hashes).\n\tconst hashInput: Record<string, unknown> = {\n\t\ttype: input.type,\n\t\tcollection: input.collection,\n\t\trecordId: input.recordId,\n\t\tdata: input.data,\n\t\ttimestamp,\n\t\tnodeId: input.nodeId,\n\t}\n\tif (input.atomicOps !== undefined && Object.keys(input.atomicOps).length > 0) {\n\t\thashInput.atomicOps = input.atomicOps\n\t}\n\tconst canonical = canonicalize(hashInput)\n\tconst encoded = new TextEncoder().encode(canonical)\n\tconst hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', encoded)\n\treturn bufferToHex(hashBuffer)\n}\n\n/**\n * Deterministic JSON serialization with sorted keys.\n * Ensures identical objects always produce identical strings regardless of property insertion order.\n *\n * @param obj - The value to serialize\n * @returns A deterministic JSON string\n */\nexport function canonicalize(obj: unknown): string {\n\tif (obj === null) {\n\t\treturn 'null'\n\t}\n\n\tif (obj === undefined) {\n\t\treturn 'null'\n\t}\n\n\tif (typeof obj !== 'object') {\n\t\treturn JSON.stringify(obj)\n\t}\n\n\tif (Array.isArray(obj)) {\n\t\tconst items = obj.map((item) => canonicalize(item))\n\t\treturn `[${items.join(',')}]`\n\t}\n\n\tconst keys = Object.keys(obj as Record<string, unknown>).sort()\n\tconst pairs = keys.map((key) => {\n\t\tconst value = (obj as Record<string, unknown>)[key]\n\t\t// Serialize undefined values as null for deterministic output\n\t\treturn `${JSON.stringify(key)}:${canonicalize(value === undefined ? null : value)}`\n\t})\n\treturn `{${pairs.join(',')}}`\n}\n\nfunction bufferToHex(buffer: ArrayBuffer): string {\n\tconst bytes = new Uint8Array(buffer)\n\treturn Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n","import type { KoraEventByType, KoraEventEmitter, KoraEventListener, KoraEventType } from './events'\n\ntype AnyListener = (event: never) => void\n\n/**\n * Concrete implementation of KoraEventEmitter.\n * Simple, synchronous event emitter for internal use across @kora packages.\n *\n * @example\n * ```typescript\n * const emitter = new SimpleEventEmitter()\n * const unsub = emitter.on('operation:created', (event) => {\n * console.log(event.operation.id)\n * })\n * emitter.emit({ type: 'operation:created', operation: someOp })\n * unsub() // unsubscribe\n * ```\n */\nexport class SimpleEventEmitter implements KoraEventEmitter {\n\tprivate listeners = new Map<string, Set<AnyListener>>()\n\n\ton<T extends KoraEventType>(type: T, listener: KoraEventListener<T>): () => void {\n\t\tlet set = this.listeners.get(type)\n\t\tif (!set) {\n\t\t\tset = new Set()\n\t\t\tthis.listeners.set(type, set)\n\t\t}\n\t\tset.add(listener as AnyListener)\n\n\t\treturn () => {\n\t\t\tthis.off(type, listener)\n\t\t}\n\t}\n\n\toff<T extends KoraEventType>(type: T, listener: KoraEventListener<T>): void {\n\t\tconst set = this.listeners.get(type)\n\t\tif (set) {\n\t\t\tset.delete(listener as AnyListener)\n\t\t\tif (set.size === 0) {\n\t\t\t\tthis.listeners.delete(type)\n\t\t\t}\n\t\t}\n\t}\n\n\temit<T extends KoraEventType>(event: KoraEventByType<T>): void {\n\t\tconst set = this.listeners.get(event.type)\n\t\tif (!set) return\n\t\tfor (const listener of set) {\n\t\t\t;(listener as (event: KoraEventByType<T>) => void)(event)\n\t\t}\n\t}\n\n\t/** Remove all listeners for all event types. */\n\tclear(): void {\n\t\tthis.listeners.clear()\n\t}\n\n\t/** Get the number of listeners for a specific event type. */\n\tlistenerCount(type: KoraEventType): number {\n\t\treturn this.listeners.get(type)?.size ?? 0\n\t}\n}\n","/**\n * Human-readable remediation hints keyed by {@link KoraError} `code`.\n */\nexport const KORA_ERROR_FIX_SUGGESTIONS: Record<string, string> = {\n\tSCHEMA_VALIDATION:\n\t\t'Review your defineSchema() definition: every collection needs at least one field and a positive version.',\n\tOPERATION_ERROR:\n\t\t'Check the operation payload matches your schema field types and required fields.',\n\tMERGE_CONFLICT:\n\t\t'Add a custom resolver for the conflicting field in defineSchema(), or adjust constraint onConflict rules.',\n\tSYNC_ERROR:\n\t\t'Verify the sync server URL, auth token, and that the server accepts your schema version.',\n\tSTORAGE_ERROR:\n\t\t'Confirm the storage adapter is supported in this environment and the database path is writable.',\n\tCLOCK_DRIFT:\n\t\t'Sync device wall-clock time (NTP). Kora refuses new timestamps when drift exceeds five minutes.',\n\tPERSISTENCE_ERROR:\n\t\t'IndexedDB persistence failed; check browser storage settings and available disk quota.',\n\tMISSING_WORKER_URL:\n\t\t'Pass store.workerUrl pointing to sqlite-wasm-worker.js (see create-kora-app templates).',\n\tINVALID_SYNC_URL:\n\t\t'Use ws:// or wss:// for WebSocket transport, or http(s):// for HTTP long-polling.',\n\tAPP_NOT_READY:\n\t\t'Await app.ready before querying or mutating collections, or wrap the tree in <KoraProvider app={app}>.',\n}\n\n/**\n * Returns a suggested fix for a Kora error code, if one is known.\n */\nexport function getKoraErrorFix(code: string): string | undefined {\n\treturn KORA_ERROR_FIX_SUGGESTIONS[code]\n}\n","import { getKoraErrorFix } from './error-fixes'\n\n/**\n * Base error class for all Kora errors.\n * Every error includes a machine-readable code and optional context for debugging.\n */\nexport class KoraError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly code: string,\n\t\tpublic readonly context?: Record<string, unknown>,\n\t) {\n\t\tsuper(message)\n\t\tthis.name = 'KoraError'\n\t}\n\n\t/**\n\t * Actionable hint for resolving this error (from registry or error context).\n\t */\n\tget fix(): string | undefined {\n\t\tconst fromContext = this.context?.fix\n\t\tif (typeof fromContext === 'string' && fromContext.length > 0) {\n\t\t\treturn fromContext\n\t\t}\n\t\treturn getKoraErrorFix(this.code)\n\t}\n}\n\n/**\n * Thrown when schema validation fails during defineSchema() or at app initialization.\n */\nexport class SchemaValidationError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'SCHEMA_VALIDATION', context)\n\t\tthis.name = 'SchemaValidationError'\n\t}\n}\n\n/**\n * Thrown when an operation is invalid or cannot be created.\n */\nexport class OperationError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'OPERATION_ERROR', context)\n\t\tthis.name = 'OperationError'\n\t}\n}\n\n/**\n * Thrown when a merge conflict cannot be automatically resolved.\n */\nexport class MergeConflictError extends KoraError {\n\tconstructor(\n\t\tpublic readonly operationA: { id: string; collection: string },\n\t\tpublic readonly operationB: { id: string; collection: string },\n\t\tpublic readonly field: string,\n\t) {\n\t\tsuper(\n\t\t\t`Merge conflict on field \"${field}\" in collection \"${operationA.collection}\"`,\n\t\t\t'MERGE_CONFLICT',\n\t\t\t{ operationA: operationA.id, operationB: operationB.id, field },\n\t\t)\n\t\tthis.name = 'MergeConflictError'\n\t}\n}\n\n/**\n * Thrown when a sync error occurs.\n */\nexport class SyncError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'SYNC_ERROR', context)\n\t\tthis.name = 'SyncError'\n\t}\n}\n\n/**\n * Thrown when a storage operation fails.\n */\nexport class StorageError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'STORAGE_ERROR', context)\n\t\tthis.name = 'StorageError'\n\t}\n}\n\n/**\n * Thrown when the HLC detects excessive clock drift.\n * Drift > 60s: warning. Drift > 5min: this error is thrown, refusing to generate timestamps.\n */\nexport class ClockDriftError extends KoraError {\n\tconstructor(\n\t\tpublic readonly currentHlcTime: number,\n\t\tpublic readonly physicalTime: number,\n\t) {\n\t\tconst driftSeconds = Math.round((currentHlcTime - physicalTime) / 1000)\n\t\tsuper(\n\t\t\t`Clock drift of ${driftSeconds}s detected. Physical time is behind HLC by more than 5 minutes. This indicates a severe clock issue.`,\n\t\t\t'CLOCK_DRIFT',\n\t\t\t{ currentHlcTime, physicalTime, driftSeconds },\n\t\t)\n\t\tthis.name = 'ClockDriftError'\n\t}\n}\n","import { ClockDriftError } from '../errors/errors'\nimport type { HLCTimestamp, TimeSource } from '../types'\n\n/** Default time source using the system clock */\nconst systemTimeSource: TimeSource = { now: () => Date.now() }\n\n/** Maximum allowed drift before warning (60 seconds) */\nconst DRIFT_WARN_MS = 60_000\n\n/** Maximum allowed drift before refusing to generate timestamps (5 minutes) */\nconst DRIFT_ERROR_MS = 5 * 60_000\n\n/**\n * Hybrid Logical Clock implementation based on Kulkarni et al.\n *\n * Provides a total order that respects causality without requiring synchronized clocks.\n * Each call to now() returns a timestamp strictly greater than the previous one.\n *\n * @example\n * ```typescript\n * const clock = new HybridLogicalClock('node-1')\n * const ts1 = clock.now()\n * const ts2 = clock.now()\n * // HybridLogicalClock.compare(ts1, ts2) < 0 (ts1 is earlier)\n * ```\n */\nexport class HybridLogicalClock {\n\tprivate wallTime = 0\n\tprivate logical = 0\n\n\tconstructor(\n\t\tprivate readonly nodeId: string,\n\t\tprivate readonly timeSource: TimeSource = systemTimeSource,\n\t\tprivate readonly onDriftWarning?: (driftMs: number) => void,\n\t) {}\n\n\t/**\n\t * Generate a new timestamp for a local event.\n\t * Guarantees monotonicity: each call returns a timestamp strictly greater than the previous.\n\t *\n\t * @throws {ClockDriftError} If physical time is more than 5 minutes behind the HLC wallTime\n\t */\n\tnow(): HLCTimestamp {\n\t\tconst physicalTime = this.timeSource.now()\n\t\tthis.checkDrift(physicalTime)\n\n\t\tif (physicalTime > this.wallTime) {\n\t\t\tthis.wallTime = physicalTime\n\t\t\tthis.logical = 0\n\t\t} else {\n\t\t\tthis.logical++\n\t\t}\n\n\t\treturn { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId }\n\t}\n\n\t/**\n\t * Update clock on receiving a remote timestamp.\n\t * Merges the remote clock state with the local state to maintain causal ordering.\n\t *\n\t * @throws {ClockDriftError} If physical time is more than 5 minutes behind the resulting wallTime\n\t */\n\treceive(remote: HLCTimestamp): HLCTimestamp {\n\t\tconst physicalTime = this.timeSource.now()\n\t\tconst wasColdStart = this.wallTime === 0\n\n\t\tif (physicalTime > this.wallTime && physicalTime > remote.wallTime) {\n\t\t\tthis.wallTime = physicalTime\n\t\t\tthis.logical = 0\n\t\t} else if (remote.wallTime > this.wallTime) {\n\t\t\tthis.wallTime = remote.wallTime\n\t\t\tthis.logical = remote.logical + 1\n\t\t} else if (this.wallTime === remote.wallTime) {\n\t\t\tthis.logical = Math.max(this.logical, remote.logical) + 1\n\t\t} else {\n\t\t\t// this.wallTime > remote.wallTime && this.wallTime >= physicalTime\n\t\t\tthis.logical++\n\t\t}\n\n\t\t// Skip drift check on cold start (wallTime was 0, uninitialized) to avoid\n\t\t// false positives when the first event is a remote timestamp with a wallTime\n\t\t// that differs significantly from local physical time. After initialization,\n\t\t// all subsequent calls to now() and receive() enforce drift protection normally.\n\t\tif (!wasColdStart) {\n\t\t\tthis.checkDrift(physicalTime)\n\t\t}\n\n\t\treturn { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId }\n\t}\n\n\t/**\n\t * Compare two timestamps. Returns negative if a < b, positive if a > b, zero if equal.\n\t * Total order: wallTime first, then logical, then nodeId (lexicographic).\n\t */\n\tstatic compare(a: HLCTimestamp, b: HLCTimestamp): number {\n\t\tif (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime\n\t\tif (a.logical !== b.logical) return a.logical - b.logical\n\t\tif (a.nodeId < b.nodeId) return -1\n\t\tif (a.nodeId > b.nodeId) return 1\n\t\treturn 0\n\t}\n\n\t/**\n\t * Serialize an HLC timestamp to a string that sorts lexicographically.\n\t * Format: zero-padded wallTime:logical:nodeId\n\t */\n\tstatic serialize(ts: HLCTimestamp): string {\n\t\tconst wall = ts.wallTime.toString().padStart(15, '0')\n\t\tconst log = ts.logical.toString().padStart(5, '0')\n\t\treturn `${wall}:${log}:${ts.nodeId}`\n\t}\n\n\t/**\n\t * Deserialize an HLC timestamp from its serialized string form.\n\t */\n\tstatic deserialize(s: string): HLCTimestamp {\n\t\tconst parts = s.split(':')\n\t\tif (parts.length < 3) {\n\t\t\tthrow new Error(`Invalid HLC timestamp string: \"${s}\"`)\n\t\t}\n\t\treturn {\n\t\t\twallTime: Number.parseInt(parts[0] ?? '0', 10),\n\t\t\tlogical: Number.parseInt(parts[1] ?? '0', 10),\n\t\t\t// nodeId may contain colons, so rejoin remaining parts\n\t\t\tnodeId: parts.slice(2).join(':'),\n\t\t}\n\t}\n\n\tprivate checkDrift(physicalTime: number): void {\n\t\tconst drift = this.wallTime - physicalTime\n\t\tif (drift > DRIFT_ERROR_MS) {\n\t\t\tthrow new ClockDriftError(this.wallTime, physicalTime)\n\t\t}\n\t\tif (drift > DRIFT_WARN_MS) {\n\t\t\tthis.onDriftWarning?.(drift)\n\t\t}\n\t}\n}\n","import { HybridLogicalClock } from '../clock/hlc'\nimport { OperationError } from '../errors/errors'\nimport type { HLCTimestamp, Operation, OperationInput } from '../types'\nimport { computeOperationId } from './content-hash'\n\n/**\n * Creates an immutable, content-addressed Operation from the given parameters.\n * The operation is deep-frozen after creation — it cannot be modified.\n *\n * @param input - The operation parameters (without id, which is computed)\n * @param clock - The HLC clock to generate the timestamp\n * @returns A frozen Operation with a content-addressed id\n *\n * @example\n * ```typescript\n * const op = await createOperation({\n * nodeId: 'device-1',\n * type: 'insert',\n * collection: 'todos',\n * recordId: 'rec-1',\n * data: { title: 'Ship it' },\n * previousData: null,\n * sequenceNumber: 1,\n * causalDeps: [],\n * schemaVersion: 1,\n * }, clock)\n * ```\n */\nexport async function createOperation(\n\tinput: OperationInput,\n\tclock: HybridLogicalClock,\n): Promise<Operation> {\n\tvalidateOperationParams(input)\n\n\tconst timestamp = clock.now()\n\tconst serializedTs = HybridLogicalClock.serialize(timestamp)\n\tconst id = await computeOperationId(input, serializedTs)\n\n\tconst operation: Operation = {\n\t\tid,\n\t\tnodeId: input.nodeId,\n\t\ttype: input.type,\n\t\tcollection: input.collection,\n\t\trecordId: input.recordId,\n\t\tdata: input.data ? { ...input.data } : null,\n\t\tpreviousData: input.previousData ? { ...input.previousData } : null,\n\t\ttimestamp,\n\t\tsequenceNumber: input.sequenceNumber,\n\t\tcausalDeps: [...input.causalDeps],\n\t\tschemaVersion: input.schemaVersion,\n\t\t...(input.atomicOps !== undefined && Object.keys(input.atomicOps).length > 0\n\t\t\t? { atomicOps: { ...input.atomicOps } }\n\t\t\t: {}),\n\t\t...(input.transactionId !== undefined ? { transactionId: input.transactionId } : {}),\n\t\t...(input.mutationName !== undefined ? { mutationName: input.mutationName } : {}),\n\t}\n\n\treturn deepFreeze(operation)\n}\n\n/**\n * Validates operation input parameters. Throws OperationError with\n * contextual information on validation failure.\n */\nexport function validateOperationParams(input: OperationInput): void {\n\tif (!input.nodeId || typeof input.nodeId !== 'string') {\n\t\tthrow new OperationError('nodeId is required and must be a non-empty string', {\n\t\t\treceived: input.nodeId,\n\t\t})\n\t}\n\n\tif (!input.type || !['insert', 'update', 'delete'].includes(input.type)) {\n\t\tthrow new OperationError('type must be \"insert\", \"update\", or \"delete\"', {\n\t\t\treceived: input.type,\n\t\t})\n\t}\n\n\tif (!input.collection || typeof input.collection !== 'string') {\n\t\tthrow new OperationError('collection is required and must be a non-empty string', {\n\t\t\treceived: input.collection,\n\t\t})\n\t}\n\n\tif (!input.recordId || typeof input.recordId !== 'string') {\n\t\tthrow new OperationError('recordId is required and must be a non-empty string', {\n\t\t\treceived: input.recordId,\n\t\t})\n\t}\n\n\tif (input.type === 'insert' && input.data === null) {\n\t\tthrow new OperationError('insert operations must include data', {\n\t\t\ttype: input.type,\n\t\t\tcollection: input.collection,\n\t\t})\n\t}\n\n\tif (input.type === 'update' && input.data === null) {\n\t\tthrow new OperationError('update operations must include data with changed fields', {\n\t\t\ttype: input.type,\n\t\t\tcollection: input.collection,\n\t\t})\n\t}\n\n\tif (input.type === 'update' && input.previousData === null) {\n\t\tthrow new OperationError(\n\t\t\t'update operations must include previousData for 3-way merge support',\n\t\t\t{\n\t\t\t\ttype: input.type,\n\t\t\t\tcollection: input.collection,\n\t\t\t},\n\t\t)\n\t}\n\n\tif (input.type === 'delete' && input.data !== null) {\n\t\tthrow new OperationError('delete operations must have null data', {\n\t\t\ttype: input.type,\n\t\t\tcollection: input.collection,\n\t\t})\n\t}\n\n\tif (typeof input.sequenceNumber !== 'number' || input.sequenceNumber < 0) {\n\t\tthrow new OperationError('sequenceNumber must be a non-negative number', {\n\t\t\treceived: input.sequenceNumber,\n\t\t})\n\t}\n\n\tif (!Array.isArray(input.causalDeps)) {\n\t\tthrow new OperationError('causalDeps must be an array of operation IDs', {\n\t\t\treceived: typeof input.causalDeps,\n\t\t})\n\t}\n\n\tif (typeof input.schemaVersion !== 'number' || input.schemaVersion < 1) {\n\t\tthrow new OperationError('schemaVersion must be a positive number', {\n\t\t\treceived: input.schemaVersion,\n\t\t})\n\t}\n}\n\n/**\n * Verify the integrity of an operation by recomputing its content hash.\n * Returns true if the id matches the recomputed hash.\n */\nexport async function verifyOperationIntegrity(op: Operation): Promise<boolean> {\n\tconst input: OperationInput = {\n\t\tnodeId: op.nodeId,\n\t\ttype: op.type,\n\t\tcollection: op.collection,\n\t\trecordId: op.recordId,\n\t\tdata: op.data,\n\t\tpreviousData: op.previousData,\n\t\tsequenceNumber: op.sequenceNumber,\n\t\tcausalDeps: op.causalDeps,\n\t\tschemaVersion: op.schemaVersion,\n\t\t...(op.atomicOps !== undefined ? { atomicOps: op.atomicOps } : {}),\n\t}\n\tconst serializedTs = HybridLogicalClock.serialize(op.timestamp)\n\tconst expectedId = await computeOperationId(input, serializedTs)\n\treturn op.id === expectedId\n}\n\n/**\n * Type guard for Operation interface.\n */\nexport function isValidOperation(value: unknown): value is Operation {\n\tif (typeof value !== 'object' || value === null) return false\n\tconst op = value as Record<string, unknown>\n\treturn (\n\t\ttypeof op.id === 'string' &&\n\t\ttypeof op.nodeId === 'string' &&\n\t\t(op.type === 'insert' || op.type === 'update' || op.type === 'delete') &&\n\t\ttypeof op.collection === 'string' &&\n\t\ttypeof op.recordId === 'string' &&\n\t\ttypeof op.sequenceNumber === 'number' &&\n\t\tArray.isArray(op.causalDeps) &&\n\t\ttypeof op.schemaVersion === 'number' &&\n\t\ttypeof op.timestamp === 'object' &&\n\t\top.timestamp !== null\n\t)\n}\n\nfunction deepFreeze<T>(obj: T): T {\n\tif (typeof obj !== 'object' || obj === null) return obj\n\t// Typed arrays (e.g. richtext Yjs blobs) cannot be frozen in JS engines.\n\tif (ArrayBuffer.isView(obj)) return obj\n\tObject.freeze(obj)\n\tfor (const value of Object.values(obj)) {\n\t\tif (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {\n\t\t\tdeepFreeze(value)\n\t\t}\n\t}\n\treturn obj\n}\n","import { HybridLogicalClock } from '../clock/hlc'\nimport { OperationError } from '../errors/errors'\nimport type { Operation } from '../types'\n\n/**\n * Topological sort of operations based on their causal dependency DAG.\n * Uses Kahn's algorithm with a binary heap for O(V log V + E) performance.\n * Deterministic tie-breaking via HLC timestamp ensures identical output\n * regardless of input order.\n *\n * @param operations - The operations to sort\n * @returns Operations in causal order (dependencies before dependents)\n * @throws {OperationError} If a cycle is detected in the dependency graph\n */\nexport function topologicalSort(operations: Operation[]): Operation[] {\n\tif (operations.length <= 1) return [...operations]\n\n\t// Build adjacency list and in-degree map\n\tconst opMap = new Map<string, Operation>()\n\tfor (const op of operations) {\n\t\topMap.set(op.id, op)\n\t}\n\n\t// Only count edges where both ends are in the operation set\n\tconst inDegree = new Map<string, number>()\n\tconst dependents = new Map<string, string[]>()\n\n\tfor (const op of operations) {\n\t\tif (!inDegree.has(op.id)) {\n\t\t\tinDegree.set(op.id, 0)\n\t\t}\n\t\tif (!dependents.has(op.id)) {\n\t\t\tdependents.set(op.id, [])\n\t\t}\n\n\t\tfor (const depId of op.causalDeps) {\n\t\t\tif (opMap.has(depId)) {\n\t\t\t\t// depId -> op.id edge (depId must come before op.id)\n\t\t\t\tinDegree.set(op.id, (inDegree.get(op.id) ?? 0) + 1)\n\t\t\t\tconst deps = dependents.get(depId)\n\t\t\t\tif (deps) {\n\t\t\t\t\tdeps.push(op.id)\n\t\t\t\t} else {\n\t\t\t\t\tdependents.set(depId, [op.id])\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Initialize min-heap with nodes that have no in-set dependencies\n\tconst heap = new MinHeap(compareByTimestamp)\n\tfor (const op of operations) {\n\t\tif ((inDegree.get(op.id) ?? 0) === 0) {\n\t\t\theap.push(op)\n\t\t}\n\t}\n\n\tconst result: Operation[] = []\n\n\twhile (heap.size > 0) {\n\t\t// Extract the earliest operation (deterministic tie-breaking by HLC)\n\t\tconst current = heap.pop()\n\t\tresult.push(current)\n\n\t\tconst deps = dependents.get(current.id) ?? []\n\n\t\tfor (const depId of deps) {\n\t\t\tconst deg = (inDegree.get(depId) ?? 0) - 1\n\t\t\tinDegree.set(depId, deg)\n\t\t\tif (deg === 0) {\n\t\t\t\tconst op = opMap.get(depId)\n\t\t\t\tif (op) heap.push(op)\n\t\t\t}\n\t\t}\n\t}\n\n\tif (result.length !== operations.length) {\n\t\tthrow new OperationError(\n\t\t\t`Cycle detected in operation dependency graph. Sorted ${result.length} of ${operations.length} operations.`,\n\t\t\t{\n\t\t\t\tsortedCount: result.length,\n\t\t\t\ttotalCount: operations.length,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn result\n}\n\nfunction compareByTimestamp(a: Operation, b: Operation): number {\n\treturn HybridLogicalClock.compare(a.timestamp, b.timestamp)\n}\n\n/**\n * Binary min-heap for efficient priority queue operations.\n * push: O(log n), pop: O(log n) — replaces the O(n) sorted array approach.\n */\nclass MinHeap {\n\tprivate readonly data: Operation[] = []\n\tprivate readonly cmp: (a: Operation, b: Operation) => number\n\n\tconstructor(comparator: (a: Operation, b: Operation) => number) {\n\t\tthis.cmp = comparator\n\t}\n\n\tget size(): number {\n\t\treturn this.data.length\n\t}\n\n\tpush(item: Operation): void {\n\t\tthis.data.push(item)\n\t\tthis.bubbleUp(this.data.length - 1)\n\t}\n\n\tpop(): Operation {\n\t\tconst top = this.data[0] as Operation\n\t\tconst last = this.data.pop() as Operation\n\t\tif (this.data.length > 0) {\n\t\t\tthis.data[0] = last\n\t\t\tthis.sinkDown(0)\n\t\t}\n\t\treturn top\n\t}\n\n\tprivate bubbleUp(index: number): void {\n\t\tlet current = index\n\t\twhile (current > 0) {\n\t\t\tconst parentIndex = (current - 1) >> 1\n\t\t\tif (this.cmp(this.data[current] as Operation, this.data[parentIndex] as Operation) >= 0) break\n\t\t\tthis.swap(current, parentIndex)\n\t\t\tcurrent = parentIndex\n\t\t}\n\t}\n\n\tprivate sinkDown(index: number): void {\n\t\tconst length = this.data.length\n\t\tlet current = index\n\t\twhile (true) {\n\t\t\tlet smallest = current\n\t\t\tconst left = 2 * current + 1\n\t\t\tconst right = 2 * current + 2\n\n\t\t\tif (\n\t\t\t\tleft < length &&\n\t\t\t\tthis.cmp(this.data[left] as Operation, this.data[smallest] as Operation) < 0\n\t\t\t) {\n\t\t\t\tsmallest = left\n\t\t\t}\n\t\t\tif (\n\t\t\t\tright < length &&\n\t\t\t\tthis.cmp(this.data[right] as Operation, this.data[smallest] as Operation) < 0\n\t\t\t) {\n\t\t\t\tsmallest = right\n\t\t\t}\n\n\t\t\tif (smallest === current) break\n\t\t\tthis.swap(current, smallest)\n\t\t\tcurrent = smallest\n\t\t}\n\t}\n\n\tprivate swap(i: number, j: number): void {\n\t\tconst tmp = this.data[i] as Operation\n\t\tthis.data[i] = this.data[j] as Operation\n\t\tthis.data[j] = tmp\n\t}\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/internal.ts","../src/operations/content-hash.ts","../src/events/event-emitter.ts","../src/errors/error-fixes.ts","../src/errors/errors.ts","../src/clock/hlc.ts","../src/operations/operation.ts","../src/version-vector/topological-sort.ts"],"sourcesContent":["// Internal exports — shared within @kora packages but NOT part of the public API.\n// Other @kora packages can import from '@korajs/core/internal' if needed.\n\nexport { canonicalize, computeOperationId } from './operations/content-hash'\nexport { SimpleEventEmitter } from './events/event-emitter'\nexport { validateOperationParams } from './operations/operation'\nexport { topologicalSort } from './version-vector/topological-sort'\n","import type { OperationInput } from '../types'\n\n/**\n * Compute the content-addressed ID for an operation using SHA-256.\n * The same operation content always produces the same hash, ensuring deduplication.\n *\n * @param input - The operation input (without id/timestamp, which are assigned separately)\n * @param timestamp - The HLC timestamp serialized as a string\n * @returns A hex-encoded SHA-256 hash\n */\nexport async function computeOperationId(\n\tinput: OperationInput,\n\ttimestamp: string,\n): Promise<string> {\n\t// Only include atomicOps when present — ensures backward compatibility\n\t// (existing operations without atomicOps produce identical hashes).\n\tconst hashInput: Record<string, unknown> = {\n\t\ttype: input.type,\n\t\tcollection: input.collection,\n\t\trecordId: input.recordId,\n\t\tdata: input.data,\n\t\ttimestamp,\n\t\tnodeId: input.nodeId,\n\t}\n\tif (input.atomicOps !== undefined && Object.keys(input.atomicOps).length > 0) {\n\t\thashInput.atomicOps = input.atomicOps\n\t}\n\tconst canonical = canonicalize(hashInput)\n\tconst encoded = new TextEncoder().encode(canonical)\n\tconst hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', encoded)\n\treturn bufferToHex(hashBuffer)\n}\n\n/**\n * Deterministic JSON serialization with sorted keys.\n * Ensures identical objects always produce identical strings regardless of property insertion order.\n *\n * @param obj - The value to serialize\n * @returns A deterministic JSON string\n */\nexport function canonicalize(obj: unknown): string {\n\tif (obj === null) {\n\t\treturn 'null'\n\t}\n\n\tif (obj === undefined) {\n\t\treturn 'null'\n\t}\n\n\tif (typeof obj !== 'object') {\n\t\treturn JSON.stringify(obj)\n\t}\n\n\tif (Array.isArray(obj)) {\n\t\tconst items = obj.map((item) => canonicalize(item))\n\t\treturn `[${items.join(',')}]`\n\t}\n\n\tconst keys = Object.keys(obj as Record<string, unknown>).sort()\n\tconst pairs = keys.map((key) => {\n\t\tconst value = (obj as Record<string, unknown>)[key]\n\t\t// Serialize undefined values as null for deterministic output\n\t\treturn `${JSON.stringify(key)}:${canonicalize(value === undefined ? null : value)}`\n\t})\n\treturn `{${pairs.join(',')}}`\n}\n\nfunction bufferToHex(buffer: ArrayBuffer): string {\n\tconst bytes = new Uint8Array(buffer)\n\treturn Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n","import type { KoraEventByType, KoraEventEmitter, KoraEventListener, KoraEventType } from './events'\n\ntype AnyListener = (event: never) => void\n\n/**\n * Concrete implementation of KoraEventEmitter.\n * Simple, synchronous event emitter for internal use across @kora packages.\n *\n * @example\n * ```typescript\n * const emitter = new SimpleEventEmitter()\n * const unsub = emitter.on('operation:created', (event) => {\n * console.log(event.operation.id)\n * })\n * emitter.emit({ type: 'operation:created', operation: someOp })\n * unsub() // unsubscribe\n * ```\n */\nexport class SimpleEventEmitter implements KoraEventEmitter {\n\tprivate listeners = new Map<string, Set<AnyListener>>()\n\n\ton<T extends KoraEventType>(type: T, listener: KoraEventListener<T>): () => void {\n\t\tlet set = this.listeners.get(type)\n\t\tif (!set) {\n\t\t\tset = new Set()\n\t\t\tthis.listeners.set(type, set)\n\t\t}\n\t\tset.add(listener as AnyListener)\n\n\t\treturn () => {\n\t\t\tthis.off(type, listener)\n\t\t}\n\t}\n\n\toff<T extends KoraEventType>(type: T, listener: KoraEventListener<T>): void {\n\t\tconst set = this.listeners.get(type)\n\t\tif (set) {\n\t\t\tset.delete(listener as AnyListener)\n\t\t\tif (set.size === 0) {\n\t\t\t\tthis.listeners.delete(type)\n\t\t\t}\n\t\t}\n\t}\n\n\temit<T extends KoraEventType>(event: KoraEventByType<T>): void {\n\t\tconst set = this.listeners.get(event.type)\n\t\tif (!set) return\n\t\tfor (const listener of set) {\n\t\t\t;(listener as (event: KoraEventByType<T>) => void)(event)\n\t\t}\n\t}\n\n\t/** Remove all listeners for all event types. */\n\tclear(): void {\n\t\tthis.listeners.clear()\n\t}\n\n\t/** Get the number of listeners for a specific event type. */\n\tlistenerCount(type: KoraEventType): number {\n\t\treturn this.listeners.get(type)?.size ?? 0\n\t}\n}\n","/**\n * Human-readable remediation hints keyed by {@link KoraError} `code`.\n */\nexport const KORA_ERROR_FIX_SUGGESTIONS: Record<string, string> = {\n\tSCHEMA_VALIDATION:\n\t\t'Review your defineSchema() definition: every collection needs at least one field and a positive version.',\n\tOPERATION_ERROR:\n\t\t'Check the operation payload matches your schema field types and required fields.',\n\tMERGE_CONFLICT:\n\t\t'Add a custom resolver for the conflicting field in defineSchema(), or adjust constraint onConflict rules.',\n\tSYNC_ERROR:\n\t\t'Verify the sync server URL, auth token, and that the server accepts your schema version.',\n\tSTORAGE_ERROR:\n\t\t'Confirm the storage adapter is supported in this environment and the database path is writable.',\n\tCLOCK_DRIFT:\n\t\t'Sync device wall-clock time (NTP). Kora refuses new timestamps when drift exceeds five minutes.',\n\tINVALID_TIMESTAMP_FIELDS:\n\t\t'HLC timestamps need non-negative integer wallTime (< 10^15) and logical (<= 99999). Generate timestamps through HybridLogicalClock instead of building them by hand.',\n\tPERSISTENCE_ERROR:\n\t\t'IndexedDB persistence failed; check browser storage settings and available disk quota.',\n\tMISSING_WORKER_URL:\n\t\t'Pass store.workerUrl pointing to sqlite-wasm-worker.js (see create-kora-app templates).',\n\tINVALID_SYNC_URL:\n\t\t'Use ws:// or wss:// for WebSocket transport, or http(s):// for HTTP long-polling.',\n\tAPP_NOT_READY:\n\t\t'Await app.ready before querying or mutating collections, or wrap the tree in <KoraProvider app={app}>.',\n}\n\n/**\n * Returns a suggested fix for a Kora error code, if one is known.\n */\nexport function getKoraErrorFix(code: string): string | undefined {\n\treturn KORA_ERROR_FIX_SUGGESTIONS[code]\n}\n","import { getKoraErrorFix } from './error-fixes'\n\n/**\n * Base error class for all Kora errors.\n * Every error includes a machine-readable code and optional context for debugging.\n */\nexport class KoraError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly code: string,\n\t\tpublic readonly context?: Record<string, unknown>,\n\t) {\n\t\tsuper(message)\n\t\tthis.name = 'KoraError'\n\t}\n\n\t/**\n\t * Actionable hint for resolving this error (from registry or error context).\n\t */\n\tget fix(): string | undefined {\n\t\tconst fromContext = this.context?.fix\n\t\tif (typeof fromContext === 'string' && fromContext.length > 0) {\n\t\t\treturn fromContext\n\t\t}\n\t\treturn getKoraErrorFix(this.code)\n\t}\n}\n\n/**\n * Thrown when schema validation fails during defineSchema() or at app initialization.\n */\nexport class SchemaValidationError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'SCHEMA_VALIDATION', context)\n\t\tthis.name = 'SchemaValidationError'\n\t}\n}\n\n/**\n * Thrown when an operation is invalid or cannot be created.\n */\nexport class OperationError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'OPERATION_ERROR', context)\n\t\tthis.name = 'OperationError'\n\t}\n}\n\n/**\n * Thrown when a merge conflict cannot be automatically resolved.\n */\nexport class MergeConflictError extends KoraError {\n\tconstructor(\n\t\tpublic readonly operationA: { id: string; collection: string },\n\t\tpublic readonly operationB: { id: string; collection: string },\n\t\tpublic readonly field: string,\n\t) {\n\t\tsuper(\n\t\t\t`Merge conflict on field \"${field}\" in collection \"${operationA.collection}\"`,\n\t\t\t'MERGE_CONFLICT',\n\t\t\t{ operationA: operationA.id, operationB: operationB.id, field },\n\t\t)\n\t\tthis.name = 'MergeConflictError'\n\t}\n}\n\n/**\n * Thrown when a sync error occurs.\n */\nexport class SyncError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'SYNC_ERROR', context)\n\t\tthis.name = 'SyncError'\n\t}\n}\n\n/**\n * Thrown when a storage operation fails.\n */\nexport class StorageError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'STORAGE_ERROR', context)\n\t\tthis.name = 'StorageError'\n\t}\n}\n\n/**\n * Thrown when collection/query APIs are used before {@link KoraApp.ready} resolves.\n */\nexport class AppNotReadyError extends KoraError {\n\tconstructor(detail: string) {\n\t\tsuper(detail, 'APP_NOT_READY', {\n\t\t\tfix: 'Await app.ready, or wrap your UI in <KoraProvider app={app}> before calling useQuery().',\n\t\t})\n\t\tthis.name = 'AppNotReadyError'\n\t}\n}\n\n/**\n * Thrown when the HLC detects excessive clock drift.\n * Drift > 60s: warning. Drift > 5min: this error is thrown, refusing to generate timestamps.\n */\nexport class RemoteClockDriftError extends KoraError {\n\tconstructor(\n\t\tpublic readonly remoteWallTime: number,\n\t\tpublic readonly localReferenceTime: number,\n\t) {\n\t\tconst aheadSeconds = Math.round((remoteWallTime - localReferenceTime) / 1000)\n\t\tsuper(\n\t\t\t`Rejected remote timestamp ${aheadSeconds}s ahead of local reference time. The sending device's clock is set too far in the future.`,\n\t\t\t'REMOTE_CLOCK_DRIFT',\n\t\t\t{ remoteWallTime, localReferenceTime, aheadSeconds },\n\t\t)\n\t\tthis.name = 'RemoteClockDriftError'\n\t}\n}\n\n/**\n * Thrown when an HLC timestamp has structurally invalid fields: non-integer or\n * negative wallTime/logical, or a logical counter beyond the serializable cap.\n * Rejected BEFORE any clock state changes, so a malformed remote timestamp can\n * never corrupt a replica's clock or break the lexicographic ordering of the\n * serialized form.\n */\nexport class InvalidTimestampError extends KoraError {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly wallTime: number,\n\t\tpublic readonly logical: number,\n\t) {\n\t\tsuper(message, 'INVALID_TIMESTAMP_FIELDS', { wallTime, logical })\n\t\tthis.name = 'InvalidTimestampError'\n\t}\n}\n\nexport class ClockDriftError extends KoraError {\n\tconstructor(\n\t\tpublic readonly currentHlcTime: number,\n\t\tpublic readonly physicalTime: number,\n\t) {\n\t\tconst driftSeconds = Math.round((currentHlcTime - physicalTime) / 1000)\n\t\tsuper(\n\t\t\t`Clock drift of ${driftSeconds}s detected. Physical time is behind HLC by more than 5 minutes. This indicates a severe clock issue.`,\n\t\t\t'CLOCK_DRIFT',\n\t\t\t{ currentHlcTime, physicalTime, driftSeconds },\n\t\t)\n\t\tthis.name = 'ClockDriftError'\n\t}\n}\n","import { ClockDriftError, InvalidTimestampError, RemoteClockDriftError } from '../errors/errors'\nimport type { HLCTimestamp, TimeSource } from '../types'\n\n/** Default time source using the system clock */\nconst systemTimeSource: TimeSource = { now: () => Date.now() }\n\n/**\n * Highest logical counter value that fits the 5-digit zero-padded slot in the\n * serialized timestamp form. The serialized string must sort lexicographically\n * in exactly the same order as {@link HybridLogicalClock.compare}; a 6-digit\n * logical value would shift the padding and silently break that invariant for\n * every stored `_version` column and operation timestamp. When the counter\n * would exceed this cap, the clock carries into wallTime instead\n * (wallTime + 1, logical = 0), which preserves both monotonicity and the\n * serialized ordering.\n */\nexport const MAX_LOGICAL = 99_999\n\n/** Number of distinct logical values; the modulus used when carrying into wallTime. */\nconst LOGICAL_MODULUS = MAX_LOGICAL + 1\n\n/**\n * Exclusive upper bound on serializable wallTime: the serialized form\n * zero-pads wallTime to 15 digits, so a 16-digit value would break the\n * lexicographic ordering. 10^15 ms is roughly the year 33658 — unreachable by\n * honest clocks, only by hand-built timestamps.\n */\nconst WALL_TIME_LIMIT = 10 ** 15\n\n/** Maximum allowed drift before warning (60 seconds) */\nconst DRIFT_WARN_MS = 60_000\n\n/** Drift beyond which the error-severity callback fires (5 minutes) */\nconst DRIFT_ERROR_MS = 5 * 60_000\n\n/** Maximum future skew accepted from a remote timestamp before it is rejected (5 minutes) */\nconst MAX_REMOTE_FUTURE_MS = 5 * 60_000\n\n/**\n * Hybrid Logical Clock implementation based on Kulkarni et al.\n *\n * Provides a total order that respects causality without requiring synchronized clocks.\n * Each call to now() returns a timestamp strictly greater than the previous one.\n *\n * @example\n * ```typescript\n * const clock = new HybridLogicalClock('node-1')\n * const ts1 = clock.now()\n * const ts2 = clock.now()\n * // HybridLogicalClock.compare(ts1, ts2) < 0 (ts1 is earlier)\n * ```\n */\nexport class HybridLogicalClock {\n\tprivate wallTime = 0\n\tprivate logical = 0\n\tprivate referenceOffsetMs: number | null = null\n\n\tconstructor(\n\t\tprivate readonly nodeId: string,\n\t\tprivate readonly timeSource: TimeSource = systemTimeSource,\n\t\tprivate readonly onDriftWarning?: (driftMs: number) => void,\n\t\tprivate readonly onDriftError?: (driftMs: number) => void,\n\t) {}\n\n\t/**\n\t * Records the known offset between an external time reference (usually the\n\t * sync server, learned at handshake) and this device's physical clock:\n\t * `referenceTime - localPhysicalTime`. Once set, drift evaluation and remote\n\t * timestamp validation are performed against reference-corrected time, so a\n\t * device with a wrong local clock still validates remote timestamps correctly.\n\t */\n\tsetReferenceOffset(offsetMs: number): void {\n\t\tthis.referenceOffsetMs = offsetMs\n\t}\n\n\tgetReferenceOffset(): number | null {\n\t\treturn this.referenceOffsetMs\n\t}\n\n\t/** Physical time corrected by the known reference offset, when available. */\n\tprivate effectiveTime(physicalTime: number): number {\n\t\treturn physicalTime + (this.referenceOffsetMs ?? 0)\n\t}\n\n\t/**\n\t * Generate a new timestamp for a local event.\n\t * Guarantees monotonicity: each call returns a timestamp strictly greater than the previous.\n\t *\n\t * Never throws and never blocks a local write: if the physical clock has\n\t * fallen behind the HLC (e.g. the user corrected a fast clock), the HLC\n\t * freezes wallTime and advances the logical counter, and drift is reported\n\t * through the onDriftWarning / onDriftError callbacks instead.\n\t */\n\tnow(): HLCTimestamp {\n\t\tconst physicalTime = this.timeSource.now()\n\t\tthis.checkDrift(physicalTime)\n\n\t\tif (physicalTime > this.wallTime) {\n\t\t\tthis.wallTime = physicalTime\n\t\t\tthis.logical = 0\n\t\t} else {\n\t\t\tthis.logical++\n\t\t}\n\t\tthis.carryLogicalOverflow()\n\n\t\treturn { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId }\n\t}\n\n\t/**\n\t * Update clock on receiving a remote timestamp.\n\t * Merges the remote clock state with the local state to maintain causal ordering.\n\t *\n\t * @throws {InvalidTimestampError} If the remote timestamp has non-integer or\n\t * negative wallTime/logical, or logical beyond {@link MAX_LOGICAL}. Checked\n\t * BEFORE any state change, so malformed input cannot corrupt this clock.\n\t * @throws {RemoteClockDriftError} If the remote timestamp is more than 5 minutes\n\t * ahead of reference-corrected local time. Validation happens BEFORE any state\n\t * is adopted, so a rejected timestamp cannot poison this clock. When no\n\t * reference offset is known and this clock is uninitialized (cold start with\n\t * a possibly-wrong local clock), validation is skipped, matching the previous\n\t * cold-start behavior but now without a corrupting failure mode afterward.\n\t */\n\treceive(remote: HLCTimestamp): HLCTimestamp {\n\t\tconst physicalTime = this.timeSource.now()\n\t\tconst wasColdStart = this.wallTime === 0\n\n\t\t// Validate before adopting anything. Malformed fields would corrupt the\n\t\t// clock's arithmetic (NaN/fractional wallTime) or overflow the serialized\n\t\t// form's 5-digit logical slot, so they are rejected outright.\n\t\tif (\n\t\t\t!Number.isInteger(remote.wallTime) ||\n\t\t\tremote.wallTime < 0 ||\n\t\t\t!Number.isInteger(remote.logical) ||\n\t\t\tremote.logical < 0 ||\n\t\t\tremote.logical > MAX_LOGICAL\n\t\t) {\n\t\t\tthrow new InvalidTimestampError(\n\t\t\t\t`Rejected remote HLC timestamp with invalid fields (wallTime=${remote.wallTime}, logical=${remote.logical}). wallTime and logical must be non-negative integers and logical must not exceed ${MAX_LOGICAL}.`,\n\t\t\t\tremote.wallTime,\n\t\t\t\tremote.logical,\n\t\t\t)\n\t\t}\n\n\t\t// A far-future remote timestamp must never become this clock's state.\n\t\tconst canValidate = this.referenceOffsetMs !== null || !wasColdStart\n\t\tif (canValidate) {\n\t\t\tconst reference = this.effectiveTime(physicalTime)\n\t\t\tif (remote.wallTime > reference + MAX_REMOTE_FUTURE_MS) {\n\t\t\t\tthrow new RemoteClockDriftError(remote.wallTime, reference)\n\t\t\t}\n\t\t}\n\n\t\tif (physicalTime > this.wallTime && physicalTime > remote.wallTime) {\n\t\t\tthis.wallTime = physicalTime\n\t\t\tthis.logical = 0\n\t\t} else if (remote.wallTime > this.wallTime) {\n\t\t\tthis.wallTime = remote.wallTime\n\t\t\tthis.logical = remote.logical + 1\n\t\t} else if (this.wallTime === remote.wallTime) {\n\t\t\tthis.logical = Math.max(this.logical, remote.logical) + 1\n\t\t} else {\n\t\t\t// this.wallTime > remote.wallTime && this.wallTime >= physicalTime\n\t\t\tthis.logical++\n\t\t}\n\t\tthis.carryLogicalOverflow()\n\n\t\t// Report (never throw) drift after merging. Cold start is exempt from\n\t\t// reporting to avoid false positives when the local clock is simply wrong.\n\t\tif (!wasColdStart) {\n\t\t\tthis.checkDrift(physicalTime)\n\t\t}\n\n\t\treturn { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId }\n\t}\n\n\t/**\n\t * Advance this clock to at least the given timestamp.\n\t *\n\t * Used after a timestamp rebase rewrites unsynced operations: future `now()`\n\t * timestamps must sort after every rebased operation, otherwise a write issued\n\t * immediately after the rebase could interleave with (or precede) rebased ops\n\t * and break the log's total order. Never moves the clock backward — a\n\t * timestamp at or before the current state is a no-op, preserving the\n\t * monotonicity guarantee of `now()`.\n\t *\n\t * Inputs with logical > MAX_LOGICAL are normalized deterministically by\n\t * carrying the excess into wallTime, so the adopted state always leaves room\n\t * for the next increment inside the serializable range.\n\t */\n\tadvanceTo(ts: HLCTimestamp): void {\n\t\t// Deterministic normalization: adopting given values verbatim could plant\n\t\t// an unserializable logical counter inside the clock.\n\t\tconst normalized: HLCTimestamp = {\n\t\t\twallTime: ts.wallTime + Math.floor(ts.logical / LOGICAL_MODULUS),\n\t\t\tlogical: ts.logical % LOGICAL_MODULUS,\n\t\t\tnodeId: ts.nodeId,\n\t\t}\n\t\tconst current: HLCTimestamp = {\n\t\t\twallTime: this.wallTime,\n\t\t\tlogical: this.logical,\n\t\t\tnodeId: this.nodeId,\n\t\t}\n\t\tif (HybridLogicalClock.compare(normalized, current) > 0) {\n\t\t\tthis.wallTime = normalized.wallTime\n\t\t\tthis.logical = normalized.logical\n\t\t}\n\t}\n\n\t/**\n\t * Carry the logical counter into wallTime when an increment pushed it past\n\t * MAX_LOGICAL. Bumping wallTime by 1ms keeps the timestamp strictly greater\n\t * than everything issued before (monotonicity) while keeping the logical\n\t * counter inside the 5-digit slot the serialized form depends on.\n\t */\n\tprivate carryLogicalOverflow(): void {\n\t\tif (this.logical > MAX_LOGICAL) {\n\t\t\tthis.wallTime += 1\n\t\t\tthis.logical = 0\n\t\t}\n\t}\n\n\t/**\n\t * Compare two timestamps. Returns negative if a < b, positive if a > b, zero if equal.\n\t * Total order: wallTime first, then logical, then nodeId (lexicographic).\n\t */\n\tstatic compare(a: HLCTimestamp, b: HLCTimestamp): number {\n\t\tif (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime\n\t\tif (a.logical !== b.logical) return a.logical - b.logical\n\t\tif (a.nodeId < b.nodeId) return -1\n\t\tif (a.nodeId > b.nodeId) return 1\n\t\treturn 0\n\t}\n\n\t/**\n\t * Serialize an HLC timestamp to a string that sorts lexicographically.\n\t * Format: zero-padded wallTime:logical:nodeId\n\t *\n\t * @throws {InvalidTimestampError} If wallTime does not fit 15 digits or\n\t * logical does not fit 5 digits (or either is negative/non-integer). Such a\n\t * value would overflow its zero-padded slot and the serialized string would\n\t * no longer sort in the same order as {@link HybridLogicalClock.compare} —\n\t * silently corrupting every LWW comparison on stored `_version` columns.\n\t * Internal clocks can no longer produce such values (the logical counter\n\t * carries into wallTime); this guards against hand-built timestamps.\n\t */\n\tstatic serialize(ts: HLCTimestamp): string {\n\t\tif (\n\t\t\t!Number.isInteger(ts.wallTime) ||\n\t\t\tts.wallTime < 0 ||\n\t\t\tts.wallTime >= WALL_TIME_LIMIT ||\n\t\t\t!Number.isInteger(ts.logical) ||\n\t\t\tts.logical < 0 ||\n\t\t\tts.logical > MAX_LOGICAL\n\t\t) {\n\t\t\tthrow new InvalidTimestampError(\n\t\t\t\t`Cannot serialize HLC timestamp (wallTime=${ts.wallTime}, logical=${ts.logical}): wallTime must be an integer in [0, 10^15) and logical an integer in [0, ${MAX_LOGICAL}]. Values outside these ranges overflow the zero-padded serialized form, which must sort lexicographically identically to HybridLogicalClock.compare.`,\n\t\t\t\tts.wallTime,\n\t\t\t\tts.logical,\n\t\t\t)\n\t\t}\n\t\tconst wall = ts.wallTime.toString().padStart(15, '0')\n\t\tconst log = ts.logical.toString().padStart(5, '0')\n\t\treturn `${wall}:${log}:${ts.nodeId}`\n\t}\n\n\t/**\n\t * Deserialize an HLC timestamp from its serialized string form.\n\t */\n\tstatic deserialize(s: string): HLCTimestamp {\n\t\tconst parts = s.split(':')\n\t\tif (parts.length < 3) {\n\t\t\tthrow new Error(`Invalid HLC timestamp string: \"${s}\"`)\n\t\t}\n\t\treturn {\n\t\t\twallTime: Number.parseInt(parts[0] ?? '0', 10),\n\t\t\tlogical: Number.parseInt(parts[1] ?? '0', 10),\n\t\t\t// nodeId may contain colons, so rejoin remaining parts\n\t\t\tnodeId: parts.slice(2).join(':'),\n\t\t}\n\t}\n\n\t/**\n\t * Reports drift between the HLC and (reference-corrected) physical time.\n\t * Reporting only: local timestamp generation is never blocked, because the\n\t * user's data always outranks the quality of its timestamps.\n\t */\n\tprivate checkDrift(physicalTime: number): void {\n\t\tconst drift = this.wallTime - this.effectiveTime(physicalTime)\n\t\tif (drift > DRIFT_ERROR_MS) {\n\t\t\tthis.onDriftError?.(drift)\n\t\t\treturn\n\t\t}\n\t\tif (drift > DRIFT_WARN_MS) {\n\t\t\tthis.onDriftWarning?.(drift)\n\t\t}\n\t}\n}\n","import { HybridLogicalClock } from '../clock/hlc'\nimport { OperationError } from '../errors/errors'\nimport type { HLCTimestamp, Operation, OperationInput } from '../types'\nimport { computeOperationId } from './content-hash'\n\n/**\n * Creates an immutable, content-addressed Operation from the given parameters.\n * The operation is deep-frozen after creation — it cannot be modified.\n *\n * @param input - The operation parameters (without id, which is computed)\n * @param clock - The HLC clock to generate the timestamp\n * @returns A frozen Operation with a content-addressed id\n *\n * @example\n * ```typescript\n * const op = await createOperation({\n * nodeId: 'device-1',\n * type: 'insert',\n * collection: 'todos',\n * recordId: 'rec-1',\n * data: { title: 'Ship it' },\n * previousData: null,\n * sequenceNumber: 1,\n * causalDeps: [],\n * schemaVersion: 1,\n * }, clock)\n * ```\n */\nexport async function createOperation(\n\tinput: OperationInput,\n\tclock: HybridLogicalClock,\n): Promise<Operation> {\n\tvalidateOperationParams(input)\n\n\tconst timestamp = clock.now()\n\tconst serializedTs = HybridLogicalClock.serialize(timestamp)\n\tconst id = await computeOperationId(input, serializedTs)\n\n\tconst operation: Operation = {\n\t\tid,\n\t\tnodeId: input.nodeId,\n\t\ttype: input.type,\n\t\tcollection: input.collection,\n\t\trecordId: input.recordId,\n\t\tdata: input.data ? { ...input.data } : null,\n\t\tpreviousData: input.previousData ? { ...input.previousData } : null,\n\t\ttimestamp,\n\t\tsequenceNumber: input.sequenceNumber,\n\t\tcausalDeps: [...input.causalDeps],\n\t\tschemaVersion: input.schemaVersion,\n\t\t...(input.atomicOps !== undefined && Object.keys(input.atomicOps).length > 0\n\t\t\t? { atomicOps: { ...input.atomicOps } }\n\t\t\t: {}),\n\t\t...(input.transactionId !== undefined ? { transactionId: input.transactionId } : {}),\n\t\t...(input.mutationName !== undefined ? { mutationName: input.mutationName } : {}),\n\t}\n\n\treturn deepFreeze(operation)\n}\n\n/**\n * Validates operation input parameters. Throws OperationError with\n * contextual information on validation failure.\n */\nexport function validateOperationParams(input: OperationInput): void {\n\tif (!input.nodeId || typeof input.nodeId !== 'string') {\n\t\tthrow new OperationError('nodeId is required and must be a non-empty string', {\n\t\t\treceived: input.nodeId,\n\t\t})\n\t}\n\n\tif (!input.type || !['insert', 'update', 'delete'].includes(input.type)) {\n\t\tthrow new OperationError('type must be \"insert\", \"update\", or \"delete\"', {\n\t\t\treceived: input.type,\n\t\t})\n\t}\n\n\tif (!input.collection || typeof input.collection !== 'string') {\n\t\tthrow new OperationError('collection is required and must be a non-empty string', {\n\t\t\treceived: input.collection,\n\t\t})\n\t}\n\n\tif (!input.recordId || typeof input.recordId !== 'string') {\n\t\tthrow new OperationError('recordId is required and must be a non-empty string', {\n\t\t\treceived: input.recordId,\n\t\t})\n\t}\n\n\tif (input.type === 'insert' && input.data === null) {\n\t\tthrow new OperationError('insert operations must include data', {\n\t\t\ttype: input.type,\n\t\t\tcollection: input.collection,\n\t\t})\n\t}\n\n\tif (input.type === 'update' && input.data === null) {\n\t\tthrow new OperationError('update operations must include data with changed fields', {\n\t\t\ttype: input.type,\n\t\t\tcollection: input.collection,\n\t\t})\n\t}\n\n\tif (input.type === 'update' && input.previousData === null) {\n\t\tthrow new OperationError(\n\t\t\t'update operations must include previousData for 3-way merge support',\n\t\t\t{\n\t\t\t\ttype: input.type,\n\t\t\t\tcollection: input.collection,\n\t\t\t},\n\t\t)\n\t}\n\n\tif (input.type === 'delete' && input.data !== null) {\n\t\tthrow new OperationError('delete operations must have null data', {\n\t\t\ttype: input.type,\n\t\t\tcollection: input.collection,\n\t\t})\n\t}\n\n\tif (typeof input.sequenceNumber !== 'number' || input.sequenceNumber < 0) {\n\t\tthrow new OperationError('sequenceNumber must be a non-negative number', {\n\t\t\treceived: input.sequenceNumber,\n\t\t})\n\t}\n\n\tif (!Array.isArray(input.causalDeps)) {\n\t\tthrow new OperationError('causalDeps must be an array of operation IDs', {\n\t\t\treceived: typeof input.causalDeps,\n\t\t})\n\t}\n\n\tif (typeof input.schemaVersion !== 'number' || input.schemaVersion < 1) {\n\t\tthrow new OperationError('schemaVersion must be a positive number', {\n\t\t\treceived: input.schemaVersion,\n\t\t})\n\t}\n}\n\n/**\n * Verify the integrity of an operation by recomputing its content hash.\n * Returns true if the id matches the recomputed hash.\n */\nexport async function verifyOperationIntegrity(op: Operation): Promise<boolean> {\n\tconst input: OperationInput = {\n\t\tnodeId: op.nodeId,\n\t\ttype: op.type,\n\t\tcollection: op.collection,\n\t\trecordId: op.recordId,\n\t\tdata: op.data,\n\t\tpreviousData: op.previousData,\n\t\tsequenceNumber: op.sequenceNumber,\n\t\tcausalDeps: op.causalDeps,\n\t\tschemaVersion: op.schemaVersion,\n\t\t...(op.atomicOps !== undefined ? { atomicOps: op.atomicOps } : {}),\n\t}\n\tconst serializedTs = HybridLogicalClock.serialize(op.timestamp)\n\tconst expectedId = await computeOperationId(input, serializedTs)\n\treturn op.id === expectedId\n}\n\n/**\n * Type guard for Operation interface.\n */\nexport function isValidOperation(value: unknown): value is Operation {\n\tif (typeof value !== 'object' || value === null) return false\n\tconst op = value as Record<string, unknown>\n\treturn (\n\t\ttypeof op.id === 'string' &&\n\t\ttypeof op.nodeId === 'string' &&\n\t\t(op.type === 'insert' || op.type === 'update' || op.type === 'delete') &&\n\t\ttypeof op.collection === 'string' &&\n\t\ttypeof op.recordId === 'string' &&\n\t\ttypeof op.sequenceNumber === 'number' &&\n\t\tArray.isArray(op.causalDeps) &&\n\t\ttypeof op.schemaVersion === 'number' &&\n\t\ttypeof op.timestamp === 'object' &&\n\t\top.timestamp !== null\n\t)\n}\n\nfunction deepFreeze<T>(obj: T): T {\n\tif (typeof obj !== 'object' || obj === null) return obj\n\t// Typed arrays (e.g. richtext Yjs blobs) cannot be frozen in JS engines.\n\tif (ArrayBuffer.isView(obj)) return obj\n\tObject.freeze(obj)\n\tfor (const value of Object.values(obj)) {\n\t\tif (typeof value === 'object' && value !== null && !Object.isFrozen(value)) {\n\t\t\tdeepFreeze(value)\n\t\t}\n\t}\n\treturn obj\n}\n","import { HybridLogicalClock } from '../clock/hlc'\nimport { OperationError } from '../errors/errors'\nimport type { Operation } from '../types'\n\n/**\n * Topological sort of operations based on their causal dependency DAG.\n * Uses Kahn's algorithm with a binary heap for O(V log V + E) performance.\n * Deterministic tie-breaking via HLC timestamp ensures identical output\n * regardless of input order.\n *\n * @param operations - The operations to sort\n * @returns Operations in causal order (dependencies before dependents)\n * @throws {OperationError} If a cycle is detected in the dependency graph\n */\nexport function topologicalSort(operations: Operation[]): Operation[] {\n\tif (operations.length <= 1) return [...operations]\n\n\t// Build adjacency list and in-degree map\n\tconst opMap = new Map<string, Operation>()\n\tfor (const op of operations) {\n\t\topMap.set(op.id, op)\n\t}\n\n\t// Only count edges where both ends are in the operation set\n\tconst inDegree = new Map<string, number>()\n\tconst dependents = new Map<string, string[]>()\n\n\tfor (const op of operations) {\n\t\tif (!inDegree.has(op.id)) {\n\t\t\tinDegree.set(op.id, 0)\n\t\t}\n\t\tif (!dependents.has(op.id)) {\n\t\t\tdependents.set(op.id, [])\n\t\t}\n\n\t\tfor (const depId of op.causalDeps) {\n\t\t\tif (opMap.has(depId)) {\n\t\t\t\t// depId -> op.id edge (depId must come before op.id)\n\t\t\t\tinDegree.set(op.id, (inDegree.get(op.id) ?? 0) + 1)\n\t\t\t\tconst deps = dependents.get(depId)\n\t\t\t\tif (deps) {\n\t\t\t\t\tdeps.push(op.id)\n\t\t\t\t} else {\n\t\t\t\t\tdependents.set(depId, [op.id])\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Initialize min-heap with nodes that have no in-set dependencies\n\tconst heap = new MinHeap(compareByTimestamp)\n\tfor (const op of operations) {\n\t\tif ((inDegree.get(op.id) ?? 0) === 0) {\n\t\t\theap.push(op)\n\t\t}\n\t}\n\n\tconst result: Operation[] = []\n\n\twhile (heap.size > 0) {\n\t\t// Extract the earliest operation (deterministic tie-breaking by HLC)\n\t\tconst current = heap.pop()\n\t\tresult.push(current)\n\n\t\tconst deps = dependents.get(current.id) ?? []\n\n\t\tfor (const depId of deps) {\n\t\t\tconst deg = (inDegree.get(depId) ?? 0) - 1\n\t\t\tinDegree.set(depId, deg)\n\t\t\tif (deg === 0) {\n\t\t\t\tconst op = opMap.get(depId)\n\t\t\t\tif (op) heap.push(op)\n\t\t\t}\n\t\t}\n\t}\n\n\tif (result.length !== operations.length) {\n\t\tthrow new OperationError(\n\t\t\t`Cycle detected in operation dependency graph. Sorted ${result.length} of ${operations.length} operations.`,\n\t\t\t{\n\t\t\t\tsortedCount: result.length,\n\t\t\t\ttotalCount: operations.length,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn result\n}\n\nfunction compareByTimestamp(a: Operation, b: Operation): number {\n\treturn HybridLogicalClock.compare(a.timestamp, b.timestamp)\n}\n\n/**\n * Binary min-heap for efficient priority queue operations.\n * push: O(log n), pop: O(log n) — replaces the O(n) sorted array approach.\n */\nclass MinHeap {\n\tprivate readonly data: Operation[] = []\n\tprivate readonly cmp: (a: Operation, b: Operation) => number\n\n\tconstructor(comparator: (a: Operation, b: Operation) => number) {\n\t\tthis.cmp = comparator\n\t}\n\n\tget size(): number {\n\t\treturn this.data.length\n\t}\n\n\tpush(item: Operation): void {\n\t\tthis.data.push(item)\n\t\tthis.bubbleUp(this.data.length - 1)\n\t}\n\n\tpop(): Operation {\n\t\tconst top = this.data[0] as Operation\n\t\tconst last = this.data.pop() as Operation\n\t\tif (this.data.length > 0) {\n\t\t\tthis.data[0] = last\n\t\t\tthis.sinkDown(0)\n\t\t}\n\t\treturn top\n\t}\n\n\tprivate bubbleUp(index: number): void {\n\t\tlet current = index\n\t\twhile (current > 0) {\n\t\t\tconst parentIndex = (current - 1) >> 1\n\t\t\tif (this.cmp(this.data[current] as Operation, this.data[parentIndex] as Operation) >= 0) break\n\t\t\tthis.swap(current, parentIndex)\n\t\t\tcurrent = parentIndex\n\t\t}\n\t}\n\n\tprivate sinkDown(index: number): void {\n\t\tconst length = this.data.length\n\t\tlet current = index\n\t\twhile (true) {\n\t\t\tlet smallest = current\n\t\t\tconst left = 2 * current + 1\n\t\t\tconst right = 2 * current + 2\n\n\t\t\tif (\n\t\t\t\tleft < length &&\n\t\t\t\tthis.cmp(this.data[left] as Operation, this.data[smallest] as Operation) < 0\n\t\t\t) {\n\t\t\t\tsmallest = left\n\t\t\t}\n\t\t\tif (\n\t\t\t\tright < length &&\n\t\t\t\tthis.cmp(this.data[right] as Operation, this.data[smallest] as Operation) < 0\n\t\t\t) {\n\t\t\t\tsmallest = right\n\t\t\t}\n\n\t\t\tif (smallest === current) break\n\t\t\tthis.swap(current, smallest)\n\t\t\tcurrent = smallest\n\t\t}\n\t}\n\n\tprivate swap(i: number, j: number): void {\n\t\tconst tmp = this.data[i] as Operation\n\t\tthis.data[i] = this.data[j] as Operation\n\t\tthis.data[j] = tmp\n\t}\n}\n"],"mappings":"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","names":[]}
|
package/dist/internal.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { x as OperationInput, p as KoraEventEmitter, r as KoraEventType, q as KoraEventListener, o as KoraEventByType, f as Operation } from './events-BynBOsO3.cjs';
|
|
2
|
+
export { a as validateOperationParams } from './operation-BpZlYSpe.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Compute the content-addressed ID for an operation using SHA-256.
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { x as OperationInput, p as KoraEventEmitter, r as KoraEventType, q as KoraEventListener, o as KoraEventByType, f as Operation } from './events-BynBOsO3.js';
|
|
2
|
+
export { a as validateOperationParams } from './operation-D5WOZYvy.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Compute the content-addressed ID for an operation using SHA-256.
|
package/dist/internal.js
CHANGED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { J as TimeSource, H as HLCTimestamp, x as OperationInput, f as Operation } from './events-BynBOsO3.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Highest logical counter value that fits the 5-digit zero-padded slot in the
|
|
5
|
+
* serialized timestamp form. The serialized string must sort lexicographically
|
|
6
|
+
* in exactly the same order as {@link HybridLogicalClock.compare}; a 6-digit
|
|
7
|
+
* logical value would shift the padding and silently break that invariant for
|
|
8
|
+
* every stored `_version` column and operation timestamp. When the counter
|
|
9
|
+
* would exceed this cap, the clock carries into wallTime instead
|
|
10
|
+
* (wallTime + 1, logical = 0), which preserves both monotonicity and the
|
|
11
|
+
* serialized ordering.
|
|
12
|
+
*/
|
|
13
|
+
declare const MAX_LOGICAL = 99999;
|
|
14
|
+
/**
|
|
15
|
+
* Hybrid Logical Clock implementation based on Kulkarni et al.
|
|
16
|
+
*
|
|
17
|
+
* Provides a total order that respects causality without requiring synchronized clocks.
|
|
18
|
+
* Each call to now() returns a timestamp strictly greater than the previous one.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const clock = new HybridLogicalClock('node-1')
|
|
23
|
+
* const ts1 = clock.now()
|
|
24
|
+
* const ts2 = clock.now()
|
|
25
|
+
* // HybridLogicalClock.compare(ts1, ts2) < 0 (ts1 is earlier)
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
declare class HybridLogicalClock {
|
|
29
|
+
private readonly nodeId;
|
|
30
|
+
private readonly timeSource;
|
|
31
|
+
private readonly onDriftWarning?;
|
|
32
|
+
private readonly onDriftError?;
|
|
33
|
+
private wallTime;
|
|
34
|
+
private logical;
|
|
35
|
+
private referenceOffsetMs;
|
|
36
|
+
constructor(nodeId: string, timeSource?: TimeSource, onDriftWarning?: ((driftMs: number) => void) | undefined, onDriftError?: ((driftMs: number) => void) | undefined);
|
|
37
|
+
/**
|
|
38
|
+
* Records the known offset between an external time reference (usually the
|
|
39
|
+
* sync server, learned at handshake) and this device's physical clock:
|
|
40
|
+
* `referenceTime - localPhysicalTime`. Once set, drift evaluation and remote
|
|
41
|
+
* timestamp validation are performed against reference-corrected time, so a
|
|
42
|
+
* device with a wrong local clock still validates remote timestamps correctly.
|
|
43
|
+
*/
|
|
44
|
+
setReferenceOffset(offsetMs: number): void;
|
|
45
|
+
getReferenceOffset(): number | null;
|
|
46
|
+
/** Physical time corrected by the known reference offset, when available. */
|
|
47
|
+
private effectiveTime;
|
|
48
|
+
/**
|
|
49
|
+
* Generate a new timestamp for a local event.
|
|
50
|
+
* Guarantees monotonicity: each call returns a timestamp strictly greater than the previous.
|
|
51
|
+
*
|
|
52
|
+
* Never throws and never blocks a local write: if the physical clock has
|
|
53
|
+
* fallen behind the HLC (e.g. the user corrected a fast clock), the HLC
|
|
54
|
+
* freezes wallTime and advances the logical counter, and drift is reported
|
|
55
|
+
* through the onDriftWarning / onDriftError callbacks instead.
|
|
56
|
+
*/
|
|
57
|
+
now(): HLCTimestamp;
|
|
58
|
+
/**
|
|
59
|
+
* Update clock on receiving a remote timestamp.
|
|
60
|
+
* Merges the remote clock state with the local state to maintain causal ordering.
|
|
61
|
+
*
|
|
62
|
+
* @throws {InvalidTimestampError} If the remote timestamp has non-integer or
|
|
63
|
+
* negative wallTime/logical, or logical beyond {@link MAX_LOGICAL}. Checked
|
|
64
|
+
* BEFORE any state change, so malformed input cannot corrupt this clock.
|
|
65
|
+
* @throws {RemoteClockDriftError} If the remote timestamp is more than 5 minutes
|
|
66
|
+
* ahead of reference-corrected local time. Validation happens BEFORE any state
|
|
67
|
+
* is adopted, so a rejected timestamp cannot poison this clock. When no
|
|
68
|
+
* reference offset is known and this clock is uninitialized (cold start with
|
|
69
|
+
* a possibly-wrong local clock), validation is skipped, matching the previous
|
|
70
|
+
* cold-start behavior but now without a corrupting failure mode afterward.
|
|
71
|
+
*/
|
|
72
|
+
receive(remote: HLCTimestamp): HLCTimestamp;
|
|
73
|
+
/**
|
|
74
|
+
* Advance this clock to at least the given timestamp.
|
|
75
|
+
*
|
|
76
|
+
* Used after a timestamp rebase rewrites unsynced operations: future `now()`
|
|
77
|
+
* timestamps must sort after every rebased operation, otherwise a write issued
|
|
78
|
+
* immediately after the rebase could interleave with (or precede) rebased ops
|
|
79
|
+
* and break the log's total order. Never moves the clock backward — a
|
|
80
|
+
* timestamp at or before the current state is a no-op, preserving the
|
|
81
|
+
* monotonicity guarantee of `now()`.
|
|
82
|
+
*
|
|
83
|
+
* Inputs with logical > MAX_LOGICAL are normalized deterministically by
|
|
84
|
+
* carrying the excess into wallTime, so the adopted state always leaves room
|
|
85
|
+
* for the next increment inside the serializable range.
|
|
86
|
+
*/
|
|
87
|
+
advanceTo(ts: HLCTimestamp): void;
|
|
88
|
+
/**
|
|
89
|
+
* Carry the logical counter into wallTime when an increment pushed it past
|
|
90
|
+
* MAX_LOGICAL. Bumping wallTime by 1ms keeps the timestamp strictly greater
|
|
91
|
+
* than everything issued before (monotonicity) while keeping the logical
|
|
92
|
+
* counter inside the 5-digit slot the serialized form depends on.
|
|
93
|
+
*/
|
|
94
|
+
private carryLogicalOverflow;
|
|
95
|
+
/**
|
|
96
|
+
* Compare two timestamps. Returns negative if a < b, positive if a > b, zero if equal.
|
|
97
|
+
* Total order: wallTime first, then logical, then nodeId (lexicographic).
|
|
98
|
+
*/
|
|
99
|
+
static compare(a: HLCTimestamp, b: HLCTimestamp): number;
|
|
100
|
+
/**
|
|
101
|
+
* Serialize an HLC timestamp to a string that sorts lexicographically.
|
|
102
|
+
* Format: zero-padded wallTime:logical:nodeId
|
|
103
|
+
*
|
|
104
|
+
* @throws {InvalidTimestampError} If wallTime does not fit 15 digits or
|
|
105
|
+
* logical does not fit 5 digits (or either is negative/non-integer). Such a
|
|
106
|
+
* value would overflow its zero-padded slot and the serialized string would
|
|
107
|
+
* no longer sort in the same order as {@link HybridLogicalClock.compare} —
|
|
108
|
+
* silently corrupting every LWW comparison on stored `_version` columns.
|
|
109
|
+
* Internal clocks can no longer produce such values (the logical counter
|
|
110
|
+
* carries into wallTime); this guards against hand-built timestamps.
|
|
111
|
+
*/
|
|
112
|
+
static serialize(ts: HLCTimestamp): string;
|
|
113
|
+
/**
|
|
114
|
+
* Deserialize an HLC timestamp from its serialized string form.
|
|
115
|
+
*/
|
|
116
|
+
static deserialize(s: string): HLCTimestamp;
|
|
117
|
+
/**
|
|
118
|
+
* Reports drift between the HLC and (reference-corrected) physical time.
|
|
119
|
+
* Reporting only: local timestamp generation is never blocked, because the
|
|
120
|
+
* user's data always outranks the quality of its timestamps.
|
|
121
|
+
*/
|
|
122
|
+
private checkDrift;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates an immutable, content-addressed Operation from the given parameters.
|
|
127
|
+
* The operation is deep-frozen after creation — it cannot be modified.
|
|
128
|
+
*
|
|
129
|
+
* @param input - The operation parameters (without id, which is computed)
|
|
130
|
+
* @param clock - The HLC clock to generate the timestamp
|
|
131
|
+
* @returns A frozen Operation with a content-addressed id
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* const op = await createOperation({
|
|
136
|
+
* nodeId: 'device-1',
|
|
137
|
+
* type: 'insert',
|
|
138
|
+
* collection: 'todos',
|
|
139
|
+
* recordId: 'rec-1',
|
|
140
|
+
* data: { title: 'Ship it' },
|
|
141
|
+
* previousData: null,
|
|
142
|
+
* sequenceNumber: 1,
|
|
143
|
+
* causalDeps: [],
|
|
144
|
+
* schemaVersion: 1,
|
|
145
|
+
* }, clock)
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
declare function createOperation(input: OperationInput, clock: HybridLogicalClock): Promise<Operation>;
|
|
149
|
+
/**
|
|
150
|
+
* Validates operation input parameters. Throws OperationError with
|
|
151
|
+
* contextual information on validation failure.
|
|
152
|
+
*/
|
|
153
|
+
declare function validateOperationParams(input: OperationInput): void;
|
|
154
|
+
/**
|
|
155
|
+
* Verify the integrity of an operation by recomputing its content hash.
|
|
156
|
+
* Returns true if the id matches the recomputed hash.
|
|
157
|
+
*/
|
|
158
|
+
declare function verifyOperationIntegrity(op: Operation): Promise<boolean>;
|
|
159
|
+
/**
|
|
160
|
+
* Type guard for Operation interface.
|
|
161
|
+
*/
|
|
162
|
+
declare function isValidOperation(value: unknown): value is Operation;
|
|
163
|
+
|
|
164
|
+
export { HybridLogicalClock as H, MAX_LOGICAL as M, validateOperationParams as a, createOperation as c, isValidOperation as i, verifyOperationIntegrity as v };
|