@mocanvas/store 1.0.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/dist/index.js ADDED
@@ -0,0 +1,1261 @@
1
+ import { nanoid } from 'nanoid';
2
+ import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing';
3
+ import { computed, unsafe__withoutCapture, atom, transact } from '@mocanvas/state';
4
+
5
+ // src/ids.ts
6
+ var UNIQUE_ID_LENGTH = 21;
7
+ function uniqueId(size = UNIQUE_ID_LENGTH) {
8
+ return nanoid(size);
9
+ }
10
+ var RecordType = class _RecordType {
11
+ constructor(typeName, config) {
12
+ this.config = config;
13
+ this.typeName = typeName;
14
+ this.scope = config.scope;
15
+ this.validator = config.validator;
16
+ this.ephemeralKeys = config.ephemeralKeys;
17
+ const ephemeral = /* @__PURE__ */ new Set();
18
+ if (config.ephemeralKeys) {
19
+ for (const [key, value] of Object.entries(config.ephemeralKeys)) {
20
+ if (value) ephemeral.add(key);
21
+ }
22
+ }
23
+ this.ephemeralKeySet = ephemeral;
24
+ }
25
+ config;
26
+ typeName;
27
+ scope;
28
+ validator;
29
+ ephemeralKeys;
30
+ ephemeralKeySet;
31
+ /** Create a new record with defaults applied. A fresh id is generated when none is given. */
32
+ create(properties) {
33
+ const result = {
34
+ ...this.config.createDefaultProperties(),
35
+ ...properties
36
+ };
37
+ if (result["id"] === void 0) result["id"] = this.createId();
38
+ result["typeName"] = this.typeName;
39
+ return result;
40
+ }
41
+ /** Shallow-clone a record (props/meta are shared). */
42
+ clone(record) {
43
+ return { ...record };
44
+ }
45
+ /** Make an id of this type: `${typeName}:${uniquePart}`. */
46
+ createId(customUniquePart) {
47
+ return `${this.typeName}:${customUniquePart ?? uniqueId()}`;
48
+ }
49
+ /** Recover the unique part of an id of this type. */
50
+ parseId(id) {
51
+ if (!this.isId(id)) {
52
+ throw new Error(`Id ${JSON.stringify(id)} is not a ${this.typeName} id`);
53
+ }
54
+ return id.slice(this.typeName.length + 1);
55
+ }
56
+ isId(id) {
57
+ if (typeof id !== "string") return false;
58
+ if (id.length <= this.typeName.length + 1) return false;
59
+ if (id.charCodeAt(this.typeName.length) !== 58) return false;
60
+ return id.startsWith(this.typeName);
61
+ }
62
+ isInstance(record) {
63
+ return typeof record === "object" && record !== null && record.typeName === this.typeName;
64
+ }
65
+ /**
66
+ * Return a new RecordType whose `create()` fills in the given defaults, so
67
+ * those properties become optional for callers.
68
+ */
69
+ withDefaultProperties(createDefaultProperties) {
70
+ return new _RecordType(this.typeName, {
71
+ scope: this.scope,
72
+ validator: this.validator,
73
+ ephemeralKeys: this.ephemeralKeys,
74
+ createDefaultProperties
75
+ });
76
+ }
77
+ /** Run the validator (if any). Throws on invalid input. */
78
+ validate(record, recordBefore) {
79
+ if (!this.validator) return record;
80
+ if (recordBefore !== void 0 && this.validator.validateUsingKnownGoodVersion) {
81
+ return this.validator.validateUsingKnownGoodVersion(recordBefore, record);
82
+ }
83
+ return this.validator.validate(record);
84
+ }
85
+ };
86
+ function createRecordType(typeName, config) {
87
+ return new RecordType(typeName, {
88
+ scope: config.scope,
89
+ validator: config.validator,
90
+ ephemeralKeys: config.ephemeralKeys,
91
+ createDefaultProperties: () => ({})
92
+ });
93
+ }
94
+ function parseRecordId(id) {
95
+ const colon = id.indexOf(":");
96
+ if (colon <= 0 || colon === id.length - 1) {
97
+ throw new Error(`Malformed record id ${JSON.stringify(id)}`);
98
+ }
99
+ return { typeName: id.slice(0, colon), uniquePart: id.slice(colon + 1) };
100
+ }
101
+ function isRecordLike(value) {
102
+ return typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.typeName === "string";
103
+ }
104
+ var ZERO_INDEX_KEY = "a0";
105
+ function assertOrdered(below, above) {
106
+ if (below !== void 0 && above !== void 0 && !(below < above)) {
107
+ throw new Error(`Index keys out of order: ${JSON.stringify(below)} must be below ${JSON.stringify(above)}`);
108
+ }
109
+ }
110
+ function getIndexBetween(below, above) {
111
+ assertOrdered(below, above);
112
+ return generateKeyBetween(below ?? null, above ?? null);
113
+ }
114
+ function getIndexAbove(below) {
115
+ return generateKeyBetween(below ?? null, null);
116
+ }
117
+ function getIndexBelow(above) {
118
+ return generateKeyBetween(null, above ?? null);
119
+ }
120
+ function getIndicesBetween(below, above, n) {
121
+ assertOrdered(below, above);
122
+ return generateNKeysBetween(below ?? null, above ?? null, n);
123
+ }
124
+ function getIndicesAbove(below, n) {
125
+ return generateNKeysBetween(below ?? null, null, n);
126
+ }
127
+ function getIndicesBelow(above, n) {
128
+ return generateNKeysBetween(null, above ?? null, n);
129
+ }
130
+ function getIndices(n, start = ZERO_INDEX_KEY) {
131
+ if (n <= 0) return [];
132
+ validateIndexKey(start);
133
+ return [start, ...getIndicesAbove(start, n - 1)];
134
+ }
135
+ function sortByIndex(items) {
136
+ return items.map((item, i) => [item, i]).sort(([a, ai], [b, bi]) => {
137
+ if (a.index < b.index) return -1;
138
+ if (a.index > b.index) return 1;
139
+ return ai - bi;
140
+ }).map(([item]) => item);
141
+ }
142
+ function compareIndexKeys(a, b) {
143
+ return a < b ? -1 : a > b ? 1 : 0;
144
+ }
145
+ var BASE_62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
146
+ var IS_DIGIT = new Uint8Array(128);
147
+ for (let i = 0; i < BASE_62.length; i++) IS_DIGIT[BASE_62.charCodeAt(i)] = 1;
148
+ function integerPartLength(head) {
149
+ if (head >= 97 && head <= 122) return head - 97 + 2;
150
+ if (head >= 65 && head <= 90) return 90 - head + 2;
151
+ return -1;
152
+ }
153
+ function validateIndexKey(key) {
154
+ const fail = (why) => {
155
+ throw new Error(`Invalid index key ${JSON.stringify(key)}: ${why}`);
156
+ };
157
+ if (typeof key !== "string" || key.length === 0) fail("empty");
158
+ const intLen = integerPartLength(key.charCodeAt(0));
159
+ if (intLen < 0) fail("bad head marker");
160
+ if (key.length < intLen) fail(`integer part needs ${intLen - 1} digits`);
161
+ for (let i = 1; i < key.length; i++) {
162
+ const c = key.charCodeAt(i);
163
+ if (c > 127 || IS_DIGIT[c] !== 1) fail(`bad digit at ${i}`);
164
+ }
165
+ if (key.length > intLen && key.endsWith("0")) fail("fraction ends in 0");
166
+ }
167
+ function isIndexKey(key) {
168
+ if (typeof key !== "string") return false;
169
+ try {
170
+ validateIndexKey(key);
171
+ return true;
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
176
+
177
+ // src/zkey.ts
178
+ var BASE_622 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
179
+ var HEADS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
180
+ var ZKEY_SIGNIFICANT_DIGITS = 10;
181
+ var DIGIT_VALUE = new Int8Array(128).fill(-1);
182
+ for (let i = 0; i < BASE_622.length; i++) DIGIT_VALUE[BASE_622.charCodeAt(i)] = i;
183
+ var HEAD_RANK = new Int8Array(128).fill(-1);
184
+ for (let i = 0; i < HEADS.length; i++) HEAD_RANK[HEADS.charCodeAt(i)] = i;
185
+ var B = 62n;
186
+ var B10 = B ** BigInt(ZKEY_SIGNIFICANT_DIGITS);
187
+ var MASK_32 = 0xffffffffn;
188
+ function indexKeyToZKey(key) {
189
+ if (key.length === 0) throw new Error("Cannot convert an empty index key");
190
+ const headRank = HEAD_RANK[key.charCodeAt(0)] ?? -1;
191
+ if (headRank < 0) throw new Error(`Invalid index key head in ${JSON.stringify(key)}`);
192
+ let fraction = 0n;
193
+ const n = Math.min(key.length - 1, ZKEY_SIGNIFICANT_DIGITS);
194
+ for (let i = 0; i < n; i++) {
195
+ const d = DIGIT_VALUE[key.charCodeAt(i + 1)] ?? -1;
196
+ if (d < 0) throw new Error(`Invalid index key digit in ${JSON.stringify(key)}`);
197
+ fraction = fraction * B + BigInt(d);
198
+ }
199
+ for (let i = n; i < ZKEY_SIGNIFICANT_DIGITS; i++) fraction *= B;
200
+ const value = BigInt(headRank) * B10 + fraction;
201
+ const lo = Number(value & MASK_32);
202
+ const hi = Number(value >> 32n & MASK_32);
203
+ return [lo, hi];
204
+ }
205
+ function zKeyToBigInt([lo, hi]) {
206
+ return BigInt(hi) << 32n | BigInt(lo);
207
+ }
208
+ function compareZKeys(a, b) {
209
+ if (a[1] !== b[1]) return a[1] < b[1] ? -1 : 1;
210
+ if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
211
+ return 0;
212
+ }
213
+
214
+ // src/RecordsDiff.ts
215
+ function createEmptyRecordsDiff() {
216
+ return { added: {}, updated: {}, removed: {} };
217
+ }
218
+ function isRecordsDiffEmpty(diff) {
219
+ for (const _ in diff.added) return false;
220
+ for (const _ in diff.updated) return false;
221
+ for (const _ in diff.removed) return false;
222
+ return true;
223
+ }
224
+ function reverseRecordsDiff(diff) {
225
+ const result = createEmptyRecordsDiff();
226
+ for (const id in diff.added) {
227
+ result.removed[id] = diff.added[id];
228
+ }
229
+ for (const id in diff.removed) {
230
+ result.added[id] = diff.removed[id];
231
+ }
232
+ for (const id in diff.updated) {
233
+ const [from, to] = diff.updated[id];
234
+ result.updated[id] = [to, from];
235
+ }
236
+ return result;
237
+ }
238
+ function applyChangeToDiff(target, id, before, after) {
239
+ if (before === void 0 && after === void 0) return;
240
+ if (id in target.added) {
241
+ if (after === void 0) {
242
+ delete target.added[id];
243
+ } else {
244
+ target.added[id] = after;
245
+ }
246
+ return;
247
+ }
248
+ if (id in target.updated) {
249
+ const [from] = target.updated[id];
250
+ if (after === void 0) {
251
+ delete target.updated[id];
252
+ target.removed[id] = from;
253
+ } else if (from === after) {
254
+ delete target.updated[id];
255
+ } else {
256
+ target.updated[id] = [from, after];
257
+ }
258
+ return;
259
+ }
260
+ if (id in target.removed) {
261
+ const original = target.removed[id];
262
+ if (after === void 0) return;
263
+ delete target.removed[id];
264
+ if (original !== after) target.updated[id] = [original, after];
265
+ return;
266
+ }
267
+ if (before === void 0) {
268
+ if (after !== void 0) target.added[id] = after;
269
+ } else if (after === void 0) {
270
+ target.removed[id] = before;
271
+ } else if (before !== after) {
272
+ target.updated[id] = [before, after];
273
+ }
274
+ }
275
+ function squashRecordDiffsMutable(target, diff) {
276
+ for (const id in diff.added) {
277
+ applyChangeToDiff(target, id, void 0, diff.added[id]);
278
+ }
279
+ for (const id in diff.updated) {
280
+ const [from, to] = diff.updated[id];
281
+ applyChangeToDiff(target, id, from, to);
282
+ }
283
+ for (const id in diff.removed) {
284
+ applyChangeToDiff(target, id, diff.removed[id], void 0);
285
+ }
286
+ }
287
+ function squashRecordDiffs(diffs) {
288
+ const result = createEmptyRecordsDiff();
289
+ for (const diff of diffs) squashRecordDiffsMutable(result, diff);
290
+ return result;
291
+ }
292
+ function cloneRecordsDiff(diff) {
293
+ return {
294
+ added: { ...diff.added },
295
+ updated: { ...diff.updated },
296
+ removed: { ...diff.removed }
297
+ };
298
+ }
299
+
300
+ // src/migrate.ts
301
+ function createMigrationIds(sequenceId, versions) {
302
+ const result = {};
303
+ for (const [name, version] of Object.entries(versions)) {
304
+ result[name] = `${sequenceId}/${version}`;
305
+ }
306
+ return result;
307
+ }
308
+ function parseMigrationId(id) {
309
+ const slash = id.lastIndexOf("/");
310
+ if (slash <= 0) throw new Error(`Malformed migration id ${JSON.stringify(id)}`);
311
+ const version = Number(id.slice(slash + 1));
312
+ if (!Number.isInteger(version) || version < 1) {
313
+ throw new Error(`Malformed migration id ${JSON.stringify(id)}: version must be a positive integer`);
314
+ }
315
+ return { sequenceId: id.slice(0, slash), version };
316
+ }
317
+ function createMigrationSequence(options) {
318
+ const { sequenceId, retroactive = true, sequence } = options;
319
+ if (!sequenceId || sequenceId.includes("/")) {
320
+ throw new Error(`Invalid sequenceId ${JSON.stringify(sequenceId)}: must be non-empty and not contain "/"`);
321
+ }
322
+ sequence.forEach((migration, i) => {
323
+ const id = migration.id;
324
+ const parsed = parseMigrationId(id);
325
+ if (parsed.sequenceId !== sequenceId) {
326
+ throw new Error(`Migration ${id} does not belong to sequence ${sequenceId}`);
327
+ }
328
+ if (parsed.version !== i + 1) {
329
+ throw new Error(`Migration ${id} is out of order: expected version ${i + 1}`);
330
+ }
331
+ const scope = migration.scope;
332
+ if (scope !== "record" && scope !== "store") {
333
+ throw new Error(`Migration ${id} has invalid scope ${JSON.stringify(scope)}`);
334
+ }
335
+ });
336
+ return { sequenceId, retroactive, sequence: [...sequence] };
337
+ }
338
+ function createRecordMigrationSequence(options) {
339
+ const { recordType, filter } = options;
340
+ const combinedFilter = (record) => record.typeName === recordType && (filter ? filter(record) : true);
341
+ return createMigrationSequence({
342
+ sequenceId: options.sequenceId,
343
+ retroactive: options.retroactive,
344
+ sequence: options.sequence.map(
345
+ (m) => ({ id: m.id, scope: "record", filter: combinedFilter, up: m.up, down: m.down })
346
+ )
347
+ });
348
+ }
349
+ function applyRecordMigration(migration, record, direction) {
350
+ if (migration.filter && !migration.filter(record)) return record;
351
+ const fn = direction === "up" ? migration.up : migration.down;
352
+ if (!fn) throw new Error(`Migration ${migration.id} has no ${direction} function`);
353
+ const result = fn(record);
354
+ return result === void 0 ? record : result;
355
+ }
356
+ function applyMigrationToStore(migration, store, direction) {
357
+ if (migration.scope === "store") {
358
+ const fn = direction === "up" ? migration.up : migration.down;
359
+ if (!fn) throw new Error(`Migration ${migration.id} has no ${direction} function`);
360
+ const result = fn(store);
361
+ return result === void 0 ? store : result;
362
+ }
363
+ for (const id in store) {
364
+ const record = store[id];
365
+ const next = applyRecordMigration(migration, record, direction);
366
+ if (next !== record) store[id] = next;
367
+ }
368
+ return store;
369
+ }
370
+
371
+ // src/StoreSchema.ts
372
+ var StoreSchema = class _StoreSchema {
373
+ constructor(types, options) {
374
+ this.types = types;
375
+ this.options = options;
376
+ const byName = /* @__PURE__ */ new Map();
377
+ for (const [name, type] of Object.entries(types)) {
378
+ if (type.typeName !== name) {
379
+ throw new Error(`Record type registered under "${name}" has typeName "${type.typeName}"`);
380
+ }
381
+ byName.set(name, type);
382
+ }
383
+ this.typeByName = byName;
384
+ const migrations = {};
385
+ const sorted = [];
386
+ const seenIds = /* @__PURE__ */ new Set();
387
+ for (const sequence of options.migrations ?? []) {
388
+ if (migrations[sequence.sequenceId]) {
389
+ throw new Error(`Duplicate migration sequence "${sequence.sequenceId}"`);
390
+ }
391
+ migrations[sequence.sequenceId] = sequence;
392
+ for (const migration of sequence.sequence) {
393
+ if (seenIds.has(migration.id)) throw new Error(`Duplicate migration id "${migration.id}"`);
394
+ seenIds.add(migration.id);
395
+ sorted.push(migration);
396
+ }
397
+ }
398
+ this.migrations = migrations;
399
+ this.sortedMigrations = sorted;
400
+ }
401
+ types;
402
+ options;
403
+ static create(types, options) {
404
+ return new _StoreSchema(types, options ?? {});
405
+ }
406
+ migrations;
407
+ /** All migrations in application order (sequence registration order, then version). */
408
+ sortedMigrations;
409
+ typeByName;
410
+ getType(typeName) {
411
+ return this.typeByName.get(typeName);
412
+ }
413
+ /** Scope of a record type; unknown types are treated as `document`. */
414
+ getScope(typeName) {
415
+ return this.typeByName.get(typeName)?.scope ?? "document";
416
+ }
417
+ /**
418
+ * Validate a record, delegating to its record type's validator. Records of
419
+ * unknown types are passed through untouched so that foreign data survives
420
+ * a load/save round-trip.
421
+ */
422
+ validateRecord(store, record, phase, recordBefore) {
423
+ const type = this.typeByName.get(record.typeName);
424
+ if (!type) return record;
425
+ try {
426
+ return type.validate(record, recordBefore);
427
+ } catch (error) {
428
+ if (this.options.onValidationFailure) {
429
+ return this.options.onValidationFailure({
430
+ error,
431
+ store,
432
+ record,
433
+ phase,
434
+ recordBefore: recordBefore ?? null
435
+ });
436
+ }
437
+ throw error;
438
+ }
439
+ }
440
+ /** The current version of every sequence. */
441
+ serialize() {
442
+ const sequences = {};
443
+ for (const sequence of Object.values(this.migrations)) {
444
+ sequences[sequence.sequenceId] = sequence.sequence.length;
445
+ }
446
+ return { schemaVersion: 2, sequences };
447
+ }
448
+ /** A schema at version 0 of every sequence (all migrations still pending). */
449
+ serializeEarliestVersion() {
450
+ const sequences = {};
451
+ for (const sequence of Object.values(this.migrations)) sequences[sequence.sequenceId] = 0;
452
+ return { schemaVersion: 2, sequences };
453
+ }
454
+ /**
455
+ * The migrations that must run to bring data saved under `persistedSchema`
456
+ * up to this schema, in order. Sequences the persisted schema knows but we
457
+ * do not are ignored with a warning.
458
+ */
459
+ getMigrationsSince(persistedSchema) {
460
+ if (persistedSchema.schemaVersion !== 2) {
461
+ return {
462
+ type: "error",
463
+ reason: `Unsupported schema version ${String(persistedSchema.schemaVersion)}`
464
+ };
465
+ }
466
+ const persisted = persistedSchema.sequences ?? {};
467
+ for (const sequenceId of Object.keys(persisted)) {
468
+ if (!this.migrations[sequenceId]) {
469
+ console.warn(`[store] ignoring unknown migration sequence "${sequenceId}" in persisted schema`);
470
+ }
471
+ }
472
+ const result = [];
473
+ for (const sequence of Object.values(this.migrations)) {
474
+ const persistedVersion = persisted[sequence.sequenceId];
475
+ let startAt;
476
+ if (persistedVersion === void 0) {
477
+ if (!sequence.retroactive) continue;
478
+ startAt = 0;
479
+ } else {
480
+ if (!Number.isInteger(persistedVersion) || persistedVersion < 0) {
481
+ return { type: "error", reason: `Invalid version ${String(persistedVersion)} for sequence "${sequence.sequenceId}"` };
482
+ }
483
+ if (persistedVersion > sequence.sequence.length) {
484
+ return {
485
+ type: "error",
486
+ reason: `Sequence "${sequence.sequenceId}" is at version ${persistedVersion} but this schema only knows ${sequence.sequence.length}: data comes from a newer version`
487
+ };
488
+ }
489
+ startAt = persistedVersion;
490
+ }
491
+ for (let i = startAt; i < sequence.sequence.length; i++) result.push(sequence.sequence[i]);
492
+ }
493
+ return { type: "success", value: result };
494
+ }
495
+ /**
496
+ * Migrate a single record. Only record-scoped migrations can be applied;
497
+ * encountering a store-scoped one is an error. `down` runs the migrations
498
+ * in reverse (from this schema to `persistedSchema`).
499
+ */
500
+ migratePersistedRecord(record, persistedSchema, direction = "up") {
501
+ const migrations = this.getMigrationsSince(persistedSchema);
502
+ if (migrations.type === "error") return migrations;
503
+ const ordered = direction === "up" ? migrations.value : [...migrations.value].reverse();
504
+ let current = structuredClone(record);
505
+ try {
506
+ for (const migration of ordered) {
507
+ if (migration.scope !== "record") {
508
+ return { type: "error", reason: `Migration ${migration.id} is store-scoped and cannot be applied to a single record` };
509
+ }
510
+ if (direction === "down" && !migration.down) {
511
+ return { type: "error", reason: `Migration ${migration.id} has no down migration` };
512
+ }
513
+ current = applyRecordMigration(migration, current, direction);
514
+ }
515
+ } catch (error) {
516
+ return { type: "error", reason: `Migration failed: ${error instanceof Error ? error.message : String(error)}` };
517
+ }
518
+ return { type: "success", value: current };
519
+ }
520
+ /**
521
+ * Bring a whole persisted store up to date. The input is not mutated.
522
+ * Records of types this schema does not know are preserved as-is.
523
+ */
524
+ migrateStoreSnapshot(snapshot) {
525
+ const migrations = this.getMigrationsSince(snapshot.schema);
526
+ if (migrations.type === "error") return migrations;
527
+ let store = structuredClone(snapshot.store);
528
+ if (migrations.value.length === 0) return { type: "success", value: store };
529
+ try {
530
+ for (const migration of migrations.value) {
531
+ store = applyMigrationToStore(migration, store, "up");
532
+ }
533
+ } catch (error) {
534
+ return { type: "error", reason: `Migration failed: ${error instanceof Error ? error.message : String(error)}` };
535
+ }
536
+ for (const id in store) {
537
+ const record = store[id];
538
+ if (!record || record.id !== id) {
539
+ return { type: "error", reason: `Migration produced a record whose id does not match its key (${id})` };
540
+ }
541
+ }
542
+ return { type: "success", value: store };
543
+ }
544
+ };
545
+
546
+ // src/Store.ts
547
+ var StoreSideEffects = class {
548
+ byType = /* @__PURE__ */ new Map();
549
+ operationComplete = /* @__PURE__ */ new Set();
550
+ enabled = true;
551
+ isEnabled() {
552
+ return this.enabled;
553
+ }
554
+ setIsEnabled(enabled) {
555
+ this.enabled = enabled;
556
+ }
557
+ sets(typeName) {
558
+ let sets = this.byType.get(typeName);
559
+ if (!sets) {
560
+ sets = {
561
+ beforeCreate: /* @__PURE__ */ new Set(),
562
+ afterCreate: /* @__PURE__ */ new Set(),
563
+ beforeChange: /* @__PURE__ */ new Set(),
564
+ afterChange: /* @__PURE__ */ new Set(),
565
+ beforeDelete: /* @__PURE__ */ new Set(),
566
+ afterDelete: /* @__PURE__ */ new Set()
567
+ };
568
+ this.byType.set(typeName, sets);
569
+ }
570
+ return sets;
571
+ }
572
+ add(typeName, kind, handler) {
573
+ const set = this.sets(typeName)[kind];
574
+ set.add(handler);
575
+ return () => {
576
+ set.delete(handler);
577
+ };
578
+ }
579
+ /** Register several handlers for several types at once. Returns a disposer for all of them. */
580
+ register(handlers) {
581
+ const disposers = [];
582
+ for (const [typeName, h] of Object.entries(handlers)) {
583
+ if (!h) continue;
584
+ if (h.beforeCreate) disposers.push(this.add(typeName, "beforeCreate", h.beforeCreate));
585
+ if (h.afterCreate) disposers.push(this.add(typeName, "afterCreate", h.afterCreate));
586
+ if (h.beforeChange) disposers.push(this.add(typeName, "beforeChange", h.beforeChange));
587
+ if (h.afterChange) disposers.push(this.add(typeName, "afterChange", h.afterChange));
588
+ if (h.beforeDelete) disposers.push(this.add(typeName, "beforeDelete", h.beforeDelete));
589
+ if (h.afterDelete) disposers.push(this.add(typeName, "afterDelete", h.afterDelete));
590
+ }
591
+ return () => disposers.forEach((d) => d());
592
+ }
593
+ registerBeforeCreateHandler(typeName, handler) {
594
+ return this.add(typeName, "beforeCreate", handler);
595
+ }
596
+ registerAfterCreateHandler(typeName, handler) {
597
+ return this.add(typeName, "afterCreate", handler);
598
+ }
599
+ registerBeforeChangeHandler(typeName, handler) {
600
+ return this.add(typeName, "beforeChange", handler);
601
+ }
602
+ registerAfterChangeHandler(typeName, handler) {
603
+ return this.add(typeName, "afterChange", handler);
604
+ }
605
+ registerBeforeDeleteHandler(typeName, handler) {
606
+ return this.add(typeName, "beforeDelete", handler);
607
+ }
608
+ registerAfterDeleteHandler(typeName, handler) {
609
+ return this.add(typeName, "afterDelete", handler);
610
+ }
611
+ registerOperationCompleteHandler(handler) {
612
+ this.operationComplete.add(handler);
613
+ return () => {
614
+ this.operationComplete.delete(handler);
615
+ };
616
+ }
617
+ /** @internal */
618
+ handleBeforeCreate(record, source) {
619
+ const sets = this.byType.get(record.typeName);
620
+ if (!sets) return record;
621
+ let result = record;
622
+ for (const handler of sets.beforeCreate) result = handler(result, source);
623
+ return result;
624
+ }
625
+ /** @internal */
626
+ handleAfterCreate(record, source) {
627
+ const sets = this.byType.get(record.typeName);
628
+ if (!sets) return;
629
+ for (const handler of sets.afterCreate) handler(record, source);
630
+ }
631
+ /** @internal */
632
+ handleBeforeChange(prev, next, source) {
633
+ const sets = this.byType.get(next.typeName);
634
+ if (!sets) return next;
635
+ let result = next;
636
+ for (const handler of sets.beforeChange) result = handler(prev, result, source);
637
+ return result;
638
+ }
639
+ /** @internal */
640
+ handleAfterChange(prev, next, source) {
641
+ const sets = this.byType.get(next.typeName);
642
+ if (!sets) return;
643
+ for (const handler of sets.afterChange) handler(prev, next, source);
644
+ }
645
+ /** @internal Returns false when a handler vetoed the delete. */
646
+ handleBeforeDelete(record, source) {
647
+ const sets = this.byType.get(record.typeName);
648
+ if (!sets) return true;
649
+ for (const handler of sets.beforeDelete) {
650
+ if (handler(record, source) === false) return false;
651
+ }
652
+ return true;
653
+ }
654
+ /** @internal */
655
+ handleAfterDelete(record, source) {
656
+ const sets = this.byType.get(record.typeName);
657
+ if (!sets) return;
658
+ for (const handler of sets.afterDelete) handler(record, source);
659
+ }
660
+ /** @internal */
661
+ handleOperationComplete(source) {
662
+ for (const handler of this.operationComplete) handler(source);
663
+ }
664
+ };
665
+ function isPlainObject(value) {
666
+ if (typeof value !== "object" || value === null) return false;
667
+ const proto = Object.getPrototypeOf(value);
668
+ return proto === Object.prototype || proto === null;
669
+ }
670
+ function shallowEqualObjects(a, b) {
671
+ if (a === b) return true;
672
+ const aKeys = Object.keys(a);
673
+ const bKeys = Object.keys(b);
674
+ if (aKeys.length !== bKeys.length) return false;
675
+ for (const key of aKeys) {
676
+ if (!(key in b) || a[key] !== b[key]) return false;
677
+ }
678
+ return true;
679
+ }
680
+ function isRecordShallowEqual(a, b) {
681
+ if (a === b) return true;
682
+ const ao = a;
683
+ const bo = b;
684
+ const aKeys = Object.keys(ao);
685
+ const bKeys = Object.keys(bo);
686
+ if (aKeys.length !== bKeys.length) return false;
687
+ for (const key of aKeys) {
688
+ if (!(key in bo)) return false;
689
+ const av = ao[key];
690
+ const bv = bo[key];
691
+ if (av === bv) continue;
692
+ if ((key === "props" || key === "meta") && isPlainObject(av) && isPlainObject(bv)) {
693
+ if (!shallowEqualObjects(av, bv)) return false;
694
+ continue;
695
+ }
696
+ return false;
697
+ }
698
+ return true;
699
+ }
700
+ function freezeRecord(record) {
701
+ const r = record;
702
+ if (isPlainObject(r["props"]) && !Object.isFrozen(r["props"])) Object.freeze(r["props"]);
703
+ if (isPlainObject(r["meta"]) && !Object.isFrozen(r["meta"])) Object.freeze(r["meta"]);
704
+ return Object.freeze(record);
705
+ }
706
+ var StoreQueries = class {
707
+ constructor(store) {
708
+ this.store = store;
709
+ }
710
+ store;
711
+ idsCache = /* @__PURE__ */ new Map();
712
+ recordsCache = /* @__PURE__ */ new Map();
713
+ /** The set of ids of every record of `typeName`. Maintained incrementally. */
714
+ ids(typeName) {
715
+ let c = this.idsCache.get(typeName);
716
+ if (!c) {
717
+ const index = this.store.getTypeIndex(typeName);
718
+ c = computed(`store:${this.store.id}:ids:${typeName}`, () => {
719
+ index.epoch.get();
720
+ return new Set(index.live);
721
+ });
722
+ this.idsCache.set(typeName, c);
723
+ }
724
+ return c;
725
+ }
726
+ /** Every record of `typeName`, in insertion order. */
727
+ records(typeName) {
728
+ let c = this.recordsCache.get(typeName);
729
+ if (!c) {
730
+ const ids = this.ids(typeName);
731
+ c = computed(`store:${this.store.id}:records:${typeName}`, () => {
732
+ const result = [];
733
+ for (const id of ids.get()) {
734
+ const record = this.store.get(id);
735
+ if (record !== void 0) result.push(record);
736
+ }
737
+ return result;
738
+ });
739
+ this.recordsCache.set(typeName, c);
740
+ }
741
+ return c;
742
+ }
743
+ /** The first record of `typeName` matching `predicate` (or the first record, when omitted). */
744
+ record(typeName, predicate) {
745
+ const records = this.records(typeName);
746
+ return computed(`store:${this.store.id}:record:${typeName}`, () => {
747
+ const all = records.get();
748
+ if (!predicate) return all[0];
749
+ for (const record of all) if (predicate(record)) return record;
750
+ return void 0;
751
+ });
752
+ }
753
+ /** Non-reactive filter over the records of `typeName`. */
754
+ exec(typeName, predicate) {
755
+ return unsafe__withoutCapture(() => this.records(typeName).get().filter(predicate));
756
+ }
757
+ };
758
+ var Store = class {
759
+ id;
760
+ schema;
761
+ props;
762
+ scopedTypes;
763
+ sideEffects = new StoreSideEffects();
764
+ query;
765
+ /** Bumped once per completed operation that changed something. */
766
+ history;
767
+ records = /* @__PURE__ */ new Map();
768
+ typeIndexes = /* @__PURE__ */ new Map();
769
+ listeners = /* @__PURE__ */ new Set();
770
+ pendingEntries = [];
771
+ extractStack = [];
772
+ depth = 0;
773
+ source = "user";
774
+ runCallbacks = true;
775
+ inOperationComplete = false;
776
+ disposed = false;
777
+ constructor(options) {
778
+ this.id = options.id ?? uniqueId();
779
+ this.schema = options.schema;
780
+ this.props = options.props;
781
+ this.history = atom(`store:${this.id}:history`, 0);
782
+ this.query = new StoreQueries(this);
783
+ const scoped = { document: /* @__PURE__ */ new Set(), session: /* @__PURE__ */ new Set(), presence: /* @__PURE__ */ new Set() };
784
+ for (const type of Object.values(this.schema.types)) {
785
+ scoped[type.scope].add(type.typeName);
786
+ }
787
+ this.scopedTypes = scoped;
788
+ if (options.initialData) {
789
+ const records = Object.values(options.initialData);
790
+ this.atomic(() => this.put(records, "initialize"), { runCallbacks: false });
791
+ }
792
+ }
793
+ /* ---- reading ---------------------------------------------------------- */
794
+ /** @internal */
795
+ getTypeIndex(typeName) {
796
+ let index = this.typeIndexes.get(typeName);
797
+ if (!index) {
798
+ index = { live: /* @__PURE__ */ new Set(), epoch: atom(`store:${this.id}:index:${typeName}`, 0) };
799
+ this.typeIndexes.set(typeName, index);
800
+ }
801
+ return index;
802
+ }
803
+ /** Get a record (reactive: subscribes to the record, or to its type's membership when absent). */
804
+ get(id) {
805
+ const a = this.records.get(id);
806
+ if (a) return a.get();
807
+ this.getTypeIndex(typeNameOfId(id)).epoch.get();
808
+ return void 0;
809
+ }
810
+ /** Get a record without registering a reactive dependency. */
811
+ unsafeGetWithoutCapture(id) {
812
+ const a = this.records.get(id);
813
+ return a ? unsafe__withoutCapture(() => a.get()) : void 0;
814
+ }
815
+ has(id) {
816
+ return this.get(id) !== void 0;
817
+ }
818
+ /** All records (reactive over every record and every type's membership). */
819
+ allRecords() {
820
+ for (const index of this.typeIndexes.values()) index.epoch.get();
821
+ const result = [];
822
+ for (const a of this.records.values()) {
823
+ const record = a.get();
824
+ if (record !== void 0) result.push(record);
825
+ }
826
+ return result;
827
+ }
828
+ /** Scope of a record type; unknown types are `document`. */
829
+ getScope(typeName) {
830
+ return this.schema.getScope(typeName);
831
+ }
832
+ /* ---- writing ---------------------------------------------------------- */
833
+ /**
834
+ * Insert or update records. Records are validated, passed through `before*`
835
+ * side effects, frozen, and written. `after*` side effects run once every
836
+ * record in the call has been written.
837
+ */
838
+ put(records, phaseOverride) {
839
+ this.atomic(() => {
840
+ const source = this.source;
841
+ const callbacks = this.runCallbacks && this.sideEffects.isEnabled();
842
+ const created = [];
843
+ const changed = [];
844
+ for (const record of records) {
845
+ const id = record.id;
846
+ const existing = this.records.get(id);
847
+ const before = existing?.get();
848
+ if (before !== void 0) {
849
+ if (before === record) continue;
850
+ let next = this.schema.validateRecord(this, record, phaseOverride ?? "updateRecord", before);
851
+ if (callbacks) next = this.sideEffects.handleBeforeChange(before, next, source);
852
+ if (next === before || isRecordShallowEqual(before, next)) continue;
853
+ if (next.id !== id) {
854
+ throw new Error(`Cannot change the id of a record (${id} -> ${next.id})`);
855
+ }
856
+ freezeRecord(next);
857
+ if (before.typeName !== next.typeName) {
858
+ this.removeFromIndex(before.typeName, id);
859
+ this.addToIndex(next.typeName, id);
860
+ }
861
+ existing.set(next);
862
+ this.recordChange(id, before, next);
863
+ changed.push([before, next]);
864
+ } else {
865
+ let next = this.schema.validateRecord(this, record, phaseOverride ?? "createRecord", void 0);
866
+ if (callbacks) next = this.sideEffects.handleBeforeCreate(next, source);
867
+ if (next.id !== id) {
868
+ throw new Error(`Cannot change the id of a record (${id} -> ${next.id})`);
869
+ }
870
+ freezeRecord(next);
871
+ const a = existing ?? atom(`store:${this.id}:record:${id}`, void 0);
872
+ a.set(next);
873
+ this.records.set(id, a);
874
+ this.addToIndex(next.typeName, id);
875
+ this.recordChange(id, void 0, next);
876
+ created.push(next);
877
+ }
878
+ }
879
+ if (callbacks) {
880
+ for (const record of created) this.sideEffects.handleAfterCreate(record, source);
881
+ for (const [prev, next] of changed) this.sideEffects.handleAfterChange(prev, next, source);
882
+ }
883
+ });
884
+ }
885
+ /** Remove records by id. Missing ids are ignored. `beforeDelete` handlers may veto. */
886
+ remove(ids) {
887
+ this.atomic(() => {
888
+ const source = this.source;
889
+ const callbacks = this.runCallbacks && this.sideEffects.isEnabled();
890
+ const toRemove = [];
891
+ for (const id of ids) {
892
+ const a = this.records.get(id);
893
+ if (!a) continue;
894
+ const record = a.get();
895
+ if (record === void 0) continue;
896
+ if (callbacks && !this.sideEffects.handleBeforeDelete(record, source)) continue;
897
+ toRemove.push(record);
898
+ }
899
+ const removed = [];
900
+ for (const record of toRemove) {
901
+ const a = this.records.get(record.id);
902
+ if (!a) continue;
903
+ const current = a.get();
904
+ if (current === void 0) continue;
905
+ a.set(void 0);
906
+ this.records.delete(record.id);
907
+ this.removeFromIndex(current.typeName, record.id);
908
+ this.recordChange(record.id, current, void 0);
909
+ removed.push(current);
910
+ }
911
+ if (callbacks) {
912
+ for (const record of removed) this.sideEffects.handleAfterDelete(record, source);
913
+ }
914
+ });
915
+ }
916
+ /** Remove every record. */
917
+ clear() {
918
+ this.remove(Array.from(this.records.keys()));
919
+ }
920
+ /**
921
+ * Update one record with a function. No-op when the record does not exist.
922
+ */
923
+ update(id, updater) {
924
+ const current = this.unsafeGetWithoutCapture(id);
925
+ if (current === void 0) return;
926
+ this.put([updater(current)]);
927
+ }
928
+ /* ---- transactions ----------------------------------------------------- */
929
+ /**
930
+ * Run `fn` as one operation: side effects' `operationComplete` handlers run
931
+ * once at the end, and listeners get a single squashed history entry.
932
+ */
933
+ atomic(fn, options) {
934
+ const prevSource = this.source;
935
+ const prevRunCallbacks = this.runCallbacks;
936
+ if (options?.source !== void 0) this.source = options.source;
937
+ if (options?.runCallbacks !== void 0) this.runCallbacks = options.runCallbacks;
938
+ const source = this.source;
939
+ const runCallbacks = this.runCallbacks;
940
+ this.depth++;
941
+ try {
942
+ return transact(() => unsafe__withoutCapture(fn));
943
+ } finally {
944
+ this.depth--;
945
+ if (this.depth === 0) {
946
+ try {
947
+ this.completeOperation(source, runCallbacks);
948
+ } finally {
949
+ this.source = prevSource;
950
+ this.runCallbacks = prevRunCallbacks;
951
+ }
952
+ } else {
953
+ this.source = prevSource;
954
+ this.runCallbacks = prevRunCallbacks;
955
+ }
956
+ }
957
+ }
958
+ /** Changes made inside `fn` are reported to listeners with source `remote`. */
959
+ mergeRemoteChanges(fn) {
960
+ this.atomic(fn, { source: "remote" });
961
+ }
962
+ /** Run `fn` and return the squashed diff of everything it changed. Listeners are still notified. */
963
+ extractingChanges(fn) {
964
+ const diff = createEmptyRecordsDiff();
965
+ this.extractStack.push(diff);
966
+ try {
967
+ this.atomic(fn);
968
+ } finally {
969
+ this.extractStack.pop();
970
+ }
971
+ return diff;
972
+ }
973
+ /**
974
+ * Apply a diff (e.g. from `extractingChanges` or `reverseRecordsDiff`).
975
+ * With `ignoreEphemeralKeys`, ephemeral keys of updated records keep their
976
+ * current store values instead of the diff's.
977
+ */
978
+ applyDiff(diff, options) {
979
+ const runCallbacks = options?.runCallbacks ?? true;
980
+ const ignoreEphemeralKeys = options?.ignoreEphemeralKeys ?? false;
981
+ this.atomic(
982
+ () => {
983
+ const toPut = [];
984
+ for (const id in diff.added) toPut.push(diff.added[id]);
985
+ for (const id in diff.updated) {
986
+ let [, to] = diff.updated[id];
987
+ if (ignoreEphemeralKeys) {
988
+ const current = this.unsafeGetWithoutCapture(id);
989
+ const type = this.schema.getType(to.typeName);
990
+ if (current !== void 0 && type && type.ephemeralKeySet.size > 0) {
991
+ const merged = { ...to };
992
+ const cur = current;
993
+ for (const key of type.ephemeralKeySet) {
994
+ if (key in cur) merged[key] = cur[key];
995
+ else delete merged[key];
996
+ }
997
+ to = merged;
998
+ }
999
+ }
1000
+ toPut.push(to);
1001
+ }
1002
+ this.put(toPut);
1003
+ const toRemove = Object.keys(diff.removed);
1004
+ if (toRemove.length > 0) this.remove(toRemove);
1005
+ },
1006
+ { runCallbacks }
1007
+ );
1008
+ }
1009
+ /* ---- listening -------------------------------------------------------- */
1010
+ /**
1011
+ * Subscribe to history entries. Called after each outermost operation with
1012
+ * the squashed changes, filtered by source and record scope.
1013
+ */
1014
+ listen(onHistory, filters) {
1015
+ const listener = {
1016
+ onHistory,
1017
+ filters: { source: filters?.source ?? "all", scope: filters?.scope ?? "all" }
1018
+ };
1019
+ this.listeners.add(listener);
1020
+ return () => {
1021
+ this.listeners.delete(listener);
1022
+ };
1023
+ }
1024
+ /* ---- persistence ------------------------------------------------------ */
1025
+ /** Plain-object snapshot of the records in `scope` (default `document`). */
1026
+ serialize(scope = "document") {
1027
+ const result = {};
1028
+ unsafe__withoutCapture(() => {
1029
+ for (const [id, a] of this.records) {
1030
+ const record = a.get();
1031
+ if (record === void 0) continue;
1032
+ if (scope === "all" || this.getScope(record.typeName) === scope) result[id] = record;
1033
+ }
1034
+ });
1035
+ return result;
1036
+ }
1037
+ getStoreSnapshot(scope = "document") {
1038
+ return { store: this.serialize(scope), schema: this.schema.serialize() };
1039
+ }
1040
+ /**
1041
+ * Replace the store's contents with a snapshot (migrating it first).
1042
+ * Existing records in `document` scope and in every scope present in the
1043
+ * snapshot are removed unless the snapshot contains them; other scopes are
1044
+ * left alone. Side effects do not run; listeners are notified.
1045
+ */
1046
+ loadStoreSnapshot(snapshot) {
1047
+ const migrated = this.schema.migrateStoreSnapshot(snapshot);
1048
+ if (migrated.type === "error") {
1049
+ throw new Error(`Failed to migrate snapshot: ${migrated.reason}`);
1050
+ }
1051
+ const incoming = migrated.value;
1052
+ const records = Object.values(incoming);
1053
+ this.atomic(
1054
+ () => {
1055
+ const scopes = /* @__PURE__ */ new Set(["document"]);
1056
+ for (const record of records) scopes.add(this.getScope(record.typeName));
1057
+ const toRemove = [];
1058
+ for (const [id, a] of this.records) {
1059
+ const record = a.get();
1060
+ if (record === void 0) continue;
1061
+ if (scopes.has(this.getScope(record.typeName)) && !(id in incoming)) toRemove.push(id);
1062
+ }
1063
+ this.remove(toRemove);
1064
+ this.put(records, "initialize");
1065
+ },
1066
+ { runCallbacks: false }
1067
+ );
1068
+ }
1069
+ /* ---- derived caches --------------------------------------------------- */
1070
+ /**
1071
+ * A per-record derived value, recomputed only when that record changes.
1072
+ * Entries are dropped automatically when records are removed.
1073
+ */
1074
+ createComputedCache(name, derive, options) {
1075
+ const cache = /* @__PURE__ */ new WeakMap();
1076
+ return {
1077
+ get: (id) => {
1078
+ const a = this.records.get(id);
1079
+ if (!a) {
1080
+ this.getTypeIndex(typeNameOfId(id)).epoch.get();
1081
+ return void 0;
1082
+ }
1083
+ let c = cache.get(a);
1084
+ if (!c) {
1085
+ c = computed(
1086
+ `${name}:${id}`,
1087
+ () => {
1088
+ const record = a.get();
1089
+ return record === void 0 ? void 0 : derive(record);
1090
+ },
1091
+ options?.isEqual ? { isEqual: (x, y) => x === void 0 || y === void 0 ? x === y : options.isEqual(x, y) } : void 0
1092
+ );
1093
+ cache.set(a, c);
1094
+ }
1095
+ return c.get();
1096
+ }
1097
+ };
1098
+ }
1099
+ /* ---- lifecycle -------------------------------------------------------- */
1100
+ isDisposed() {
1101
+ return this.disposed;
1102
+ }
1103
+ dispose() {
1104
+ this.disposed = true;
1105
+ this.listeners.clear();
1106
+ }
1107
+ /* ---- internals -------------------------------------------------------- */
1108
+ addToIndex(typeName, id) {
1109
+ const index = this.getTypeIndex(typeName);
1110
+ if (index.live.has(id)) return;
1111
+ index.live.add(id);
1112
+ index.epoch.update((n) => n + 1);
1113
+ }
1114
+ removeFromIndex(typeName, id) {
1115
+ const index = this.typeIndexes.get(typeName);
1116
+ if (!index || !index.live.delete(id)) return;
1117
+ index.epoch.update((n) => n + 1);
1118
+ }
1119
+ recordChange(id, before, after) {
1120
+ const last = this.pendingEntries[this.pendingEntries.length - 1];
1121
+ let entry;
1122
+ if (last && last.source === this.source) {
1123
+ entry = last;
1124
+ } else {
1125
+ entry = { changes: createEmptyRecordsDiff(), source: this.source };
1126
+ this.pendingEntries.push(entry);
1127
+ }
1128
+ applyChangeToDiff(entry.changes, id, before, after);
1129
+ for (const diff of this.extractStack) applyChangeToDiff(diff, id, before, after);
1130
+ }
1131
+ completeOperation(source, runCallbacks) {
1132
+ if (!this.pendingEntries.some((e) => !isRecordsDiffEmpty(e.changes))) {
1133
+ this.pendingEntries = [];
1134
+ return;
1135
+ }
1136
+ if (runCallbacks && this.sideEffects.isEnabled() && !this.inOperationComplete) {
1137
+ this.inOperationComplete = true;
1138
+ const prevSource = this.source;
1139
+ this.source = source;
1140
+ this.depth++;
1141
+ try {
1142
+ transact(() => unsafe__withoutCapture(() => this.sideEffects.handleOperationComplete(source)));
1143
+ } finally {
1144
+ this.depth--;
1145
+ this.source = prevSource;
1146
+ this.inOperationComplete = false;
1147
+ }
1148
+ }
1149
+ this.history.update((n) => n + 1);
1150
+ this.flushHistory();
1151
+ }
1152
+ flushHistory() {
1153
+ const entries = this.pendingEntries;
1154
+ this.pendingEntries = [];
1155
+ if (this.listeners.size === 0) return;
1156
+ for (const entry of entries) {
1157
+ if (isRecordsDiffEmpty(entry.changes)) continue;
1158
+ for (const listener of Array.from(this.listeners)) {
1159
+ if (listener.filters.source !== "all" && listener.filters.source !== entry.source) continue;
1160
+ const changes = listener.filters.scope === "all" ? entry.changes : this.filterDiffByScope(entry.changes, listener.filters.scope);
1161
+ if (isRecordsDiffEmpty(changes)) continue;
1162
+ listener.onHistory({ changes, source: entry.source });
1163
+ }
1164
+ }
1165
+ }
1166
+ filterDiffByScope(diff, scope) {
1167
+ const result = createEmptyRecordsDiff();
1168
+ for (const id in diff.added) {
1169
+ const record = diff.added[id];
1170
+ if (this.getScope(record.typeName) === scope) result.added[id] = record;
1171
+ }
1172
+ for (const id in diff.updated) {
1173
+ const pair = diff.updated[id];
1174
+ if (this.getScope(pair[1].typeName) === scope) result.updated[id] = pair;
1175
+ }
1176
+ for (const id in diff.removed) {
1177
+ const record = diff.removed[id];
1178
+ if (this.getScope(record.typeName) === scope) result.removed[id] = record;
1179
+ }
1180
+ return result;
1181
+ }
1182
+ };
1183
+ function typeNameOfId(id) {
1184
+ const colon = id.indexOf(":");
1185
+ return colon > 0 ? id.slice(0, colon) : parseRecordId(id).typeName;
1186
+ }
1187
+
1188
+ // src/tldr.ts
1189
+ var TLDR_FILE_FORMAT_VERSION = 1;
1190
+ function isPlainObject2(value) {
1191
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1192
+ }
1193
+ function isSerializedSchema(value) {
1194
+ return isPlainObject2(value) && typeof value["schemaVersion"] === "number";
1195
+ }
1196
+ function parseTldrFile(json) {
1197
+ let data = json;
1198
+ if (typeof json === "string") {
1199
+ try {
1200
+ data = JSON.parse(json);
1201
+ } catch (cause) {
1202
+ return { ok: false, error: "notATldrFile", cause };
1203
+ }
1204
+ }
1205
+ if (!isPlainObject2(data)) return { ok: false, error: "notATldrFile" };
1206
+ if (!("tldrawFileFormatVersion" in data)) {
1207
+ const legacyDocument = data["document"];
1208
+ if (isPlainObject2(legacyDocument) && ("pages" in legacyDocument || "version" in legacyDocument)) {
1209
+ return { ok: false, error: "v1File" };
1210
+ }
1211
+ return { ok: false, error: "notATldrFile" };
1212
+ }
1213
+ const version = data["tldrawFileFormatVersion"];
1214
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
1215
+ return { ok: false, error: "notATldrFile" };
1216
+ }
1217
+ if (version > TLDR_FILE_FORMAT_VERSION) return { ok: false, error: "futureVersion" };
1218
+ if (!isSerializedSchema(data["schema"])) return { ok: false, error: "notATldrFile" };
1219
+ const records = data["records"];
1220
+ if (!Array.isArray(records)) return { ok: false, error: "invalidRecords" };
1221
+ const seen = /* @__PURE__ */ new Set();
1222
+ for (const record of records) {
1223
+ if (!isRecordLike(record)) return { ok: false, error: "invalidRecords" };
1224
+ if (seen.has(record.id)) return { ok: false, error: "invalidRecords" };
1225
+ seen.add(record.id);
1226
+ }
1227
+ return { ok: true, schema: data["schema"], records };
1228
+ }
1229
+ function sortKeysDeep(value) {
1230
+ if (Array.isArray(value)) return value.map(sortKeysDeep);
1231
+ if (isPlainObject2(value)) {
1232
+ const out = {};
1233
+ for (const key of Object.keys(value).sort()) {
1234
+ const v = value[key];
1235
+ if (v !== void 0) out[key] = sortKeysDeep(v);
1236
+ }
1237
+ return out;
1238
+ }
1239
+ return value;
1240
+ }
1241
+ function serializeTldrFile(schema, records) {
1242
+ const envelope = {
1243
+ tldrawFileFormatVersion: TLDR_FILE_FORMAT_VERSION,
1244
+ schema: sortKeysDeep(schema),
1245
+ records: records.map(sortKeysDeep)
1246
+ };
1247
+ return JSON.stringify(envelope, null, 2);
1248
+ }
1249
+ function tldrFileToStoreSnapshot(file) {
1250
+ const store = {};
1251
+ for (const record of file.records) store[record.id] = record;
1252
+ return { store, schema: file.schema };
1253
+ }
1254
+ function storeSnapshotToTldrFile(snapshot) {
1255
+ const records = Object.values(snapshot.store).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
1256
+ return serializeTldrFile(snapshot.schema, records);
1257
+ }
1258
+
1259
+ export { RecordType, Store, StoreQueries, StoreSchema, StoreSideEffects, TLDR_FILE_FORMAT_VERSION, UNIQUE_ID_LENGTH, ZERO_INDEX_KEY, ZKEY_SIGNIFICANT_DIGITS, applyChangeToDiff, applyMigrationToStore, applyRecordMigration, cloneRecordsDiff, compareIndexKeys, compareZKeys, createEmptyRecordsDiff, createMigrationIds, createMigrationSequence, createRecordMigrationSequence, createRecordType, freezeRecord, getIndexAbove, getIndexBelow, getIndexBetween, getIndices, getIndicesAbove, getIndicesBelow, getIndicesBetween, indexKeyToZKey, isIndexKey, isRecordLike, isRecordShallowEqual, isRecordsDiffEmpty, parseMigrationId, parseRecordId, parseTldrFile, reverseRecordsDiff, serializeTldrFile, sortByIndex, squashRecordDiffs, squashRecordDiffsMutable, storeSnapshotToTldrFile, tldrFileToStoreSnapshot, uniqueId, validateIndexKey, zKeyToBigInt };
1260
+ //# sourceMappingURL=index.js.map
1261
+ //# sourceMappingURL=index.js.map