@mocanvas/store 1.0.0 → 4.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/ARCHITECTURE.md +421 -0
- package/BENCHMARK.md +519 -0
- package/CLEAN_ROOM.md +50 -0
- package/COMPAT.md +282 -0
- package/CUSTOM_SHAPES.md +880 -0
- package/LICENSE +110 -16
- package/MIGRATION.md +807 -0
- package/README.md +24 -4
- package/UI.md +256 -0
- package/dist/index.d.ts +484 -15
- package/dist/index.js +416 -22
- package/dist/index.js.map +1 -1
- package/package.json +12 -14
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { nanoid } from 'nanoid';
|
|
2
2
|
import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing';
|
|
3
|
-
import { computed, unsafe__withoutCapture, atom, transact } from '@mocanvas/state';
|
|
3
|
+
import { computed, unsafe__withoutCapture, isUninitialized, RESET_VALUE, withDiff, atom, transact } from '@mocanvas/state';
|
|
4
|
+
export { AtomMap, AtomSet } from '@mocanvas/state';
|
|
4
5
|
|
|
5
6
|
// src/ids.ts
|
|
6
7
|
var UNIQUE_ID_LENGTH = 21;
|
|
@@ -368,6 +369,25 @@ function applyMigrationToStore(migration, store, direction) {
|
|
|
368
369
|
return store;
|
|
369
370
|
}
|
|
370
371
|
|
|
372
|
+
// src/legacy.ts
|
|
373
|
+
function isSerializedSchemaV1(schema) {
|
|
374
|
+
return schema.schemaVersion === 1;
|
|
375
|
+
}
|
|
376
|
+
var MigrationFailureReason = {
|
|
377
|
+
/** The persisted schema names a sequence version higher than this schema knows. */
|
|
378
|
+
TargetVersionTooNew: "target-version-too-new",
|
|
379
|
+
/** The persisted data is older than the oldest migration that survives. */
|
|
380
|
+
TargetVersionTooOld: "target-version-too-old",
|
|
381
|
+
/** A record's type is not registered in this schema and cannot be migrated. */
|
|
382
|
+
UnrecognizedType: "unrecognized-type",
|
|
383
|
+
/** A migration function threw. */
|
|
384
|
+
MigrationError: "migration-error",
|
|
385
|
+
/** The persisted schema itself is malformed. */
|
|
386
|
+
IncompatibleSubtype: "incompatible-subtype",
|
|
387
|
+
/** The persisted schema version is not one this store understands. */
|
|
388
|
+
UnknownSchemaVersion: "unknown-schema-version"
|
|
389
|
+
};
|
|
390
|
+
|
|
371
391
|
// src/StoreSchema.ts
|
|
372
392
|
var StoreSchema = class _StoreSchema {
|
|
373
393
|
constructor(types, options) {
|
|
@@ -437,7 +457,7 @@ var StoreSchema = class _StoreSchema {
|
|
|
437
457
|
throw error;
|
|
438
458
|
}
|
|
439
459
|
}
|
|
440
|
-
/** The current version of every sequence. */
|
|
460
|
+
/** The current version of every sequence. Always the v2 shape — mocanvas never writes v1. */
|
|
441
461
|
serialize() {
|
|
442
462
|
const sequences = {};
|
|
443
463
|
for (const sequence of Object.values(this.migrations)) {
|
|
@@ -457,13 +477,21 @@ var StoreSchema = class _StoreSchema {
|
|
|
457
477
|
* do not are ignored with a warning.
|
|
458
478
|
*/
|
|
459
479
|
getMigrationsSince(persistedSchema) {
|
|
480
|
+
if (isSerializedSchemaV1(persistedSchema)) {
|
|
481
|
+
return {
|
|
482
|
+
type: "error",
|
|
483
|
+
reason: "Schema version 1 (per-record-type versions) predates migration sequences and cannot be migrated automatically"
|
|
484
|
+
};
|
|
485
|
+
}
|
|
460
486
|
if (persistedSchema.schemaVersion !== 2) {
|
|
461
487
|
return {
|
|
462
488
|
type: "error",
|
|
463
489
|
reason: `Unsupported schema version ${String(persistedSchema.schemaVersion)}`
|
|
464
490
|
};
|
|
465
491
|
}
|
|
466
|
-
|
|
492
|
+
return this.migrationsSince(persistedSchema.sequences ?? {});
|
|
493
|
+
}
|
|
494
|
+
migrationsSince(persisted) {
|
|
467
495
|
for (const sequenceId of Object.keys(persisted)) {
|
|
468
496
|
if (!this.migrations[sequenceId]) {
|
|
469
497
|
console.warn(`[store] ignoring unknown migration sequence "${sequenceId}" in persisted schema`);
|
|
@@ -543,6 +571,36 @@ var StoreSchema = class _StoreSchema {
|
|
|
543
571
|
}
|
|
544
572
|
};
|
|
545
573
|
|
|
574
|
+
// src/query.ts
|
|
575
|
+
function matchesQueryValue(matcher, value) {
|
|
576
|
+
if ("eq" in matcher) return Object.is(matcher.eq, value);
|
|
577
|
+
if ("neq" in matcher) return !Object.is(matcher.neq, value);
|
|
578
|
+
return typeof value === "number" && value > matcher.gt;
|
|
579
|
+
}
|
|
580
|
+
function matchesQuery(query, record) {
|
|
581
|
+
for (const key of Object.keys(query)) {
|
|
582
|
+
const matcher = query[key];
|
|
583
|
+
if (matcher === void 0) continue;
|
|
584
|
+
if (!matchesQueryValue(matcher, record[key])) return false;
|
|
585
|
+
}
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
function getIndexablePropertyOf(query) {
|
|
589
|
+
for (const key of Object.keys(query)) {
|
|
590
|
+
const matcher = query[key];
|
|
591
|
+
if (matcher && "eq" in matcher) return key;
|
|
592
|
+
}
|
|
593
|
+
return void 0;
|
|
594
|
+
}
|
|
595
|
+
function applyCollectionDiff(set, diff) {
|
|
596
|
+
if (diff.removed) for (const value of diff.removed) set.delete(value);
|
|
597
|
+
if (diff.added) for (const value of diff.added) set.add(value);
|
|
598
|
+
return set;
|
|
599
|
+
}
|
|
600
|
+
function isCollectionDiffEmpty(diff) {
|
|
601
|
+
return (diff.added?.size ?? 0) === 0 && (diff.removed?.size ?? 0) === 0;
|
|
602
|
+
}
|
|
603
|
+
|
|
546
604
|
// src/Store.ts
|
|
547
605
|
var StoreSideEffects = class {
|
|
548
606
|
byType = /* @__PURE__ */ new Map();
|
|
@@ -703,6 +761,9 @@ function freezeRecord(record) {
|
|
|
703
761
|
if (isPlainObject(r["meta"]) && !Object.isFrozen(r["meta"])) Object.freeze(r["meta"]);
|
|
704
762
|
return Object.freeze(record);
|
|
705
763
|
}
|
|
764
|
+
function toPredicate(filter) {
|
|
765
|
+
return typeof filter === "function" ? filter : (record) => matchesQuery(filter, record);
|
|
766
|
+
}
|
|
706
767
|
var StoreQueries = class {
|
|
707
768
|
constructor(store) {
|
|
708
769
|
this.store = store;
|
|
@@ -710,8 +771,21 @@ var StoreQueries = class {
|
|
|
710
771
|
store;
|
|
711
772
|
idsCache = /* @__PURE__ */ new Map();
|
|
712
773
|
recordsCache = /* @__PURE__ */ new Map();
|
|
713
|
-
|
|
714
|
-
|
|
774
|
+
indexCache = /* @__PURE__ */ new Map();
|
|
775
|
+
historyCache = /* @__PURE__ */ new Map();
|
|
776
|
+
/**
|
|
777
|
+
* The set of ids of every record of `typeName`, optionally narrowed by a
|
|
778
|
+
* filter. Maintained incrementally.
|
|
779
|
+
*/
|
|
780
|
+
ids(typeName, filter) {
|
|
781
|
+
if (filter) {
|
|
782
|
+
const records = this.records(typeName, filter);
|
|
783
|
+
return computed(`store:${this.store.id}:ids:${typeName}:filtered`, () => {
|
|
784
|
+
const set = /* @__PURE__ */ new Set();
|
|
785
|
+
for (const record of records.get()) set.add(record.id);
|
|
786
|
+
return set;
|
|
787
|
+
});
|
|
788
|
+
}
|
|
715
789
|
let c = this.idsCache.get(typeName);
|
|
716
790
|
if (!c) {
|
|
717
791
|
const index = this.store.getTypeIndex(typeName);
|
|
@@ -723,8 +797,16 @@ var StoreQueries = class {
|
|
|
723
797
|
}
|
|
724
798
|
return c;
|
|
725
799
|
}
|
|
726
|
-
/**
|
|
727
|
-
|
|
800
|
+
/**
|
|
801
|
+
* Every record of `typeName`, in insertion order, optionally narrowed by a
|
|
802
|
+
* filter.
|
|
803
|
+
*
|
|
804
|
+
* A {@link QueryExpression} with an `eq` clause is answered from the index on
|
|
805
|
+
* that property, so a page with ten thousand shapes does not have to be
|
|
806
|
+
* walked to find the twelve on one frame.
|
|
807
|
+
*/
|
|
808
|
+
records(typeName, filter) {
|
|
809
|
+
if (filter) return this.filteredRecords(typeName, filter);
|
|
728
810
|
let c = this.recordsCache.get(typeName);
|
|
729
811
|
if (!c) {
|
|
730
812
|
const ids = this.ids(typeName);
|
|
@@ -740,19 +822,166 @@ var StoreQueries = class {
|
|
|
740
822
|
}
|
|
741
823
|
return c;
|
|
742
824
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
return
|
|
751
|
-
|
|
825
|
+
filteredRecords(typeName, filter) {
|
|
826
|
+
const predicate = toPredicate(filter);
|
|
827
|
+
const indexedProperty = typeof filter === "function" ? void 0 : getIndexablePropertyOf(filter);
|
|
828
|
+
if (indexedProperty !== void 0) {
|
|
829
|
+
const clause = filter[indexedProperty];
|
|
830
|
+
const wanted = clause && "eq" in clause ? clause.eq : void 0;
|
|
831
|
+
const index = this.index(typeName, indexedProperty);
|
|
832
|
+
return computed(`store:${this.store.id}:records:${typeName}:${String(indexedProperty)}`, () => {
|
|
833
|
+
const bucket = index.get().get(wanted);
|
|
834
|
+
if (!bucket) return [];
|
|
835
|
+
const result = [];
|
|
836
|
+
for (const id of bucket) {
|
|
837
|
+
const record = this.store.get(id);
|
|
838
|
+
if (record !== void 0 && predicate(record)) result.push(record);
|
|
839
|
+
}
|
|
840
|
+
return result;
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
const all = this.records(typeName);
|
|
844
|
+
return computed(`store:${this.store.id}:records:${typeName}:filtered`, () => all.get().filter(predicate));
|
|
845
|
+
}
|
|
846
|
+
/** The first record of `typeName` matching `filter` (or the first record, when omitted). */
|
|
847
|
+
record(typeName, filter) {
|
|
848
|
+
const records = filter ? this.filteredRecords(typeName, filter) : this.records(typeName);
|
|
849
|
+
return computed(`store:${this.store.id}:record:${typeName}`, () => records.get()[0]);
|
|
752
850
|
}
|
|
753
851
|
/** Non-reactive filter over the records of `typeName`. */
|
|
754
|
-
exec(typeName,
|
|
755
|
-
return unsafe__withoutCapture(() => this.
|
|
852
|
+
exec(typeName, filter) {
|
|
853
|
+
return unsafe__withoutCapture(() => this.filteredRecords(typeName, filter).get());
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* A live index from the values of one property to the ids of the records
|
|
857
|
+
* holding them.
|
|
858
|
+
*
|
|
859
|
+
* The index is cached per type and property, and it carries diffs: a
|
|
860
|
+
* dependent that already built something from it can ask
|
|
861
|
+
* `index.getDiffSince(epoch)` and patch, instead of walking the whole map
|
|
862
|
+
* again. That is what makes "every shape whose parentId is this frame" cheap
|
|
863
|
+
* enough to recompute on every pointer move.
|
|
864
|
+
*/
|
|
865
|
+
index(typeName, property) {
|
|
866
|
+
const key = `${typeName}:${property}`;
|
|
867
|
+
const cached = this.indexCache.get(key);
|
|
868
|
+
if (cached) return cached;
|
|
869
|
+
const history = this.filterHistory(typeName);
|
|
870
|
+
const index = computed(
|
|
871
|
+
`store:${this.store.id}:index:${key}`,
|
|
872
|
+
(previous, lastComputedEpoch) => {
|
|
873
|
+
if (isUninitialized(previous)) {
|
|
874
|
+
history.get();
|
|
875
|
+
return this.buildIndex(typeName, property);
|
|
876
|
+
}
|
|
877
|
+
const diffs = history.getDiffSince(lastComputedEpoch);
|
|
878
|
+
if (diffs === RESET_VALUE) return this.buildIndex(typeName, property);
|
|
879
|
+
const nextMap = new Map(previous);
|
|
880
|
+
const indexDiff = /* @__PURE__ */ new Map();
|
|
881
|
+
let changed = false;
|
|
882
|
+
const remove = (value, id) => {
|
|
883
|
+
const bucket = nextMap.get(value);
|
|
884
|
+
if (!bucket?.has(id)) return;
|
|
885
|
+
const next = new Set(bucket);
|
|
886
|
+
next.delete(id);
|
|
887
|
+
if (next.size === 0) nextMap.delete(value);
|
|
888
|
+
else nextMap.set(value, next);
|
|
889
|
+
const entry = indexDiff.get(value) ?? {};
|
|
890
|
+
(entry.removed ??= /* @__PURE__ */ new Set()).add(id);
|
|
891
|
+
indexDiff.set(value, entry);
|
|
892
|
+
changed = true;
|
|
893
|
+
};
|
|
894
|
+
const add = (value, id) => {
|
|
895
|
+
const bucket = nextMap.get(value);
|
|
896
|
+
if (bucket?.has(id)) return;
|
|
897
|
+
nextMap.set(value, new Set(bucket).add(id));
|
|
898
|
+
const entry = indexDiff.get(value) ?? {};
|
|
899
|
+
(entry.added ??= /* @__PURE__ */ new Set()).add(id);
|
|
900
|
+
indexDiff.set(value, entry);
|
|
901
|
+
changed = true;
|
|
902
|
+
};
|
|
903
|
+
for (const diff of diffs) {
|
|
904
|
+
for (const id in diff.added) {
|
|
905
|
+
const record = diff.added[id];
|
|
906
|
+
if (record?.typeName === typeName) add(record[property], record.id);
|
|
907
|
+
}
|
|
908
|
+
for (const id in diff.updated) {
|
|
909
|
+
const [before, after] = diff.updated[id];
|
|
910
|
+
if (after.typeName !== typeName) continue;
|
|
911
|
+
if (Object.is(before[property], after[property])) continue;
|
|
912
|
+
remove(before[property], before.id);
|
|
913
|
+
add(after[property], after.id);
|
|
914
|
+
}
|
|
915
|
+
for (const id in diff.removed) {
|
|
916
|
+
const record = diff.removed[id];
|
|
917
|
+
if (record?.typeName === typeName) remove(record[property], record.id);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (!changed) return previous;
|
|
921
|
+
return withDiff(nextMap, indexDiff);
|
|
922
|
+
},
|
|
923
|
+
{ historyLength: 128 }
|
|
924
|
+
);
|
|
925
|
+
this.indexCache.set(key, index);
|
|
926
|
+
return index;
|
|
927
|
+
}
|
|
928
|
+
buildIndex(typeName, property) {
|
|
929
|
+
const map = /* @__PURE__ */ new Map();
|
|
930
|
+
for (const record of this.records(typeName).get()) {
|
|
931
|
+
const value = record[property];
|
|
932
|
+
const bucket = map.get(value);
|
|
933
|
+
if (bucket) bucket.add(record.id);
|
|
934
|
+
else map.set(value, /* @__PURE__ */ new Set([record.id]));
|
|
935
|
+
}
|
|
936
|
+
return map;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* The store's history, narrowed to one record type.
|
|
940
|
+
*
|
|
941
|
+
* Its *value* is only a counter — what it is for is the diffs it carries.
|
|
942
|
+
* `filterHistory("shape").getDiffSince(epoch)` is every change to shapes
|
|
943
|
+
* since `epoch`, with changes to other record types dropped, which is how a
|
|
944
|
+
* derived collection stays incremental without re-reading the store.
|
|
945
|
+
*/
|
|
946
|
+
filterHistory(typeName) {
|
|
947
|
+
const cached = this.historyCache.get(typeName);
|
|
948
|
+
if (cached) return cached;
|
|
949
|
+
const filtered = computed(
|
|
950
|
+
`store:${this.store.id}:history:${typeName}`,
|
|
951
|
+
(previous, lastComputedEpoch) => {
|
|
952
|
+
const epoch = this.store.history.get();
|
|
953
|
+
if (isUninitialized(previous)) return epoch;
|
|
954
|
+
const diffs = this.store.history.getDiffSince(lastComputedEpoch);
|
|
955
|
+
if (diffs === RESET_VALUE) return epoch;
|
|
956
|
+
const merged = createEmptyRecordsDiff();
|
|
957
|
+
let any = false;
|
|
958
|
+
for (const diff of diffs) {
|
|
959
|
+
for (const id in diff.added) {
|
|
960
|
+
const record = diff.added[id];
|
|
961
|
+
if (record.typeName !== typeName) continue;
|
|
962
|
+
merged.added[id] = record;
|
|
963
|
+
any = true;
|
|
964
|
+
}
|
|
965
|
+
for (const id in diff.updated) {
|
|
966
|
+
const pair = diff.updated[id];
|
|
967
|
+
if (pair[1].typeName !== typeName) continue;
|
|
968
|
+
merged.updated[id] = pair;
|
|
969
|
+
any = true;
|
|
970
|
+
}
|
|
971
|
+
for (const id in diff.removed) {
|
|
972
|
+
const record = diff.removed[id];
|
|
973
|
+
if (record.typeName !== typeName) continue;
|
|
974
|
+
merged.removed[id] = record;
|
|
975
|
+
any = true;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (!any) return previous;
|
|
979
|
+
return withDiff(epoch, merged);
|
|
980
|
+
},
|
|
981
|
+
{ historyLength: 128 }
|
|
982
|
+
);
|
|
983
|
+
this.historyCache.set(typeName, filtered);
|
|
984
|
+
return filtered;
|
|
756
985
|
}
|
|
757
986
|
};
|
|
758
987
|
var Store = class {
|
|
@@ -762,7 +991,15 @@ var Store = class {
|
|
|
762
991
|
scopedTypes;
|
|
763
992
|
sideEffects = new StoreSideEffects();
|
|
764
993
|
query;
|
|
765
|
-
/**
|
|
994
|
+
/**
|
|
995
|
+
* Bumped once per completed operation that changed something.
|
|
996
|
+
*
|
|
997
|
+
* The counter itself carries no information; the diffs do. The atom keeps a
|
|
998
|
+
* bounded history of the squashed {@link RecordsDiff} of each operation, so a
|
|
999
|
+
* derived collection can ask `history.getDiffSince(epoch)` and patch itself
|
|
1000
|
+
* instead of rebuilding. `store.query.filterHistory(typeName)` is the same
|
|
1001
|
+
* thing narrowed to one record type.
|
|
1002
|
+
*/
|
|
766
1003
|
history;
|
|
767
1004
|
records = /* @__PURE__ */ new Map();
|
|
768
1005
|
typeIndexes = /* @__PURE__ */ new Map();
|
|
@@ -774,11 +1011,22 @@ var Store = class {
|
|
|
774
1011
|
runCallbacks = true;
|
|
775
1012
|
inOperationComplete = false;
|
|
776
1013
|
disposed = false;
|
|
1014
|
+
/**
|
|
1015
|
+
* The diff of the operation currently being committed, handed to the history
|
|
1016
|
+
* atom's `computeDiff` as it is written. The atom only sees two counter
|
|
1017
|
+
* values, so the diff has to be staged here for the one write that follows.
|
|
1018
|
+
*/
|
|
1019
|
+
pendingHistoryDiff = null;
|
|
777
1020
|
constructor(options) {
|
|
778
1021
|
this.id = options.id ?? uniqueId();
|
|
779
1022
|
this.schema = options.schema;
|
|
780
1023
|
this.props = options.props;
|
|
781
|
-
this.history = atom(`store:${this.id}:history`, 0
|
|
1024
|
+
this.history = atom(`store:${this.id}:history`, 0, {
|
|
1025
|
+
// 128 operations is deep enough that a dependent which rendered a frame
|
|
1026
|
+
// ago can still patch, and shallow enough that the buffer costs nothing.
|
|
1027
|
+
historyLength: 128,
|
|
1028
|
+
computeDiff: () => this.pendingHistoryDiff ?? RESET_VALUE
|
|
1029
|
+
});
|
|
782
1030
|
this.query = new StoreQueries(this);
|
|
783
1031
|
const scoped = { document: /* @__PURE__ */ new Set(), session: /* @__PURE__ */ new Set(), presence: /* @__PURE__ */ new Set() };
|
|
784
1032
|
for (const type of Object.values(this.schema.types)) {
|
|
@@ -1037,6 +1285,28 @@ var Store = class {
|
|
|
1037
1285
|
getStoreSnapshot(scope = "document") {
|
|
1038
1286
|
return { store: this.serialize(scope), schema: this.schema.serialize() };
|
|
1039
1287
|
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Bring a snapshot saved by an older document up to this store's schema,
|
|
1290
|
+
* without loading it.
|
|
1291
|
+
*
|
|
1292
|
+
* Every migration sequence the schema knows is run — including the ones
|
|
1293
|
+
* `createStore` derives from the shape and binding utils, so a board saved
|
|
1294
|
+
* before a prop existed is backfilled here rather than failing validation on
|
|
1295
|
+
* load. The input is not mutated: the result is a new snapshot carrying this
|
|
1296
|
+
* schema's serialized version, ready for {@link Store.loadStoreSnapshot} (or
|
|
1297
|
+
* for a caller that wants to inspect the migrated records first).
|
|
1298
|
+
*
|
|
1299
|
+
* A snapshot that cannot be migrated — an unknown schema version, a sequence
|
|
1300
|
+
* from a NEWER build than this one, a migration that throws — raises rather
|
|
1301
|
+
* than returning half-migrated data, so a caller can fail closed on it.
|
|
1302
|
+
*/
|
|
1303
|
+
migrateSnapshot(snapshot) {
|
|
1304
|
+
const migrated = this.schema.migrateStoreSnapshot(snapshot);
|
|
1305
|
+
if (migrated.type === "error") {
|
|
1306
|
+
throw new Error(`Failed to migrate snapshot: ${migrated.reason}`);
|
|
1307
|
+
}
|
|
1308
|
+
return { store: migrated.value, schema: this.schema.serialize() };
|
|
1309
|
+
}
|
|
1040
1310
|
/**
|
|
1041
1311
|
* Replace the store's contents with a snapshot (migrating it first).
|
|
1042
1312
|
* Existing records in `document` scope and in every scope present in the
|
|
@@ -1146,9 +1416,20 @@ var Store = class {
|
|
|
1146
1416
|
this.inOperationComplete = false;
|
|
1147
1417
|
}
|
|
1148
1418
|
}
|
|
1149
|
-
this.
|
|
1419
|
+
this.pendingHistoryDiff = this.squashPendingEntries();
|
|
1420
|
+
try {
|
|
1421
|
+
this.history.update((n) => n + 1);
|
|
1422
|
+
} finally {
|
|
1423
|
+
this.pendingHistoryDiff = null;
|
|
1424
|
+
}
|
|
1150
1425
|
this.flushHistory();
|
|
1151
1426
|
}
|
|
1427
|
+
/** One diff describing everything the operation just committed changed. */
|
|
1428
|
+
squashPendingEntries() {
|
|
1429
|
+
const diffs = this.pendingEntries.map((entry) => entry.changes).filter((diff) => !isRecordsDiffEmpty(diff));
|
|
1430
|
+
if (diffs.length === 1) return diffs[0];
|
|
1431
|
+
return squashRecordDiffs(diffs);
|
|
1432
|
+
}
|
|
1152
1433
|
flushHistory() {
|
|
1153
1434
|
const entries = this.pendingEntries;
|
|
1154
1435
|
this.pendingEntries = [];
|
|
@@ -1256,6 +1537,119 @@ function storeSnapshotToTldrFile(snapshot) {
|
|
|
1256
1537
|
return serializeTldrFile(snapshot.schema, records);
|
|
1257
1538
|
}
|
|
1258
1539
|
|
|
1259
|
-
|
|
1540
|
+
// src/computedCache.ts
|
|
1541
|
+
function storeOf(context) {
|
|
1542
|
+
if (context instanceof Store) return context;
|
|
1543
|
+
const store = context?.store;
|
|
1544
|
+
if (store instanceof Store) return store;
|
|
1545
|
+
throw new Error("createComputedCache: context is neither a Store nor an object holding one");
|
|
1546
|
+
}
|
|
1547
|
+
function createComputedCache(name, derive, options) {
|
|
1548
|
+
const perContext = /* @__PURE__ */ new WeakMap();
|
|
1549
|
+
return {
|
|
1550
|
+
get(context, id) {
|
|
1551
|
+
const key = context;
|
|
1552
|
+
if (key === null || typeof key !== "object" && typeof key !== "function") {
|
|
1553
|
+
throw new Error("createComputedCache: context must be an object");
|
|
1554
|
+
}
|
|
1555
|
+
let cache = perContext.get(key);
|
|
1556
|
+
if (!cache) {
|
|
1557
|
+
const areRecordsEqual = options?.areRecordsEqual;
|
|
1558
|
+
const previous = /* @__PURE__ */ new Map();
|
|
1559
|
+
const derivation = areRecordsEqual ? (record) => {
|
|
1560
|
+
const next = record;
|
|
1561
|
+
const last = previous.get(record.id);
|
|
1562
|
+
if (last && areRecordsEqual(last.record, next)) return last.result;
|
|
1563
|
+
const result = derive(context, next);
|
|
1564
|
+
previous.set(record.id, { record: next, result });
|
|
1565
|
+
return result;
|
|
1566
|
+
} : (record) => derive(context, record);
|
|
1567
|
+
cache = storeOf(context).createComputedCache(
|
|
1568
|
+
name,
|
|
1569
|
+
derivation,
|
|
1570
|
+
options?.isEqual ? { isEqual: options.isEqual } : void 0
|
|
1571
|
+
);
|
|
1572
|
+
perContext.set(key, cache);
|
|
1573
|
+
}
|
|
1574
|
+
return cache.get(id);
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/devFreeze.ts
|
|
1580
|
+
var IS_DEV = typeof process !== "undefined" && typeof process.env === "object" && process.env["NODE_ENV"] !== "production";
|
|
1581
|
+
function devFreeze(object) {
|
|
1582
|
+
if (!IS_DEV) return object;
|
|
1583
|
+
return deepFreeze(object);
|
|
1584
|
+
}
|
|
1585
|
+
function deepFreeze(object) {
|
|
1586
|
+
if (object === null || typeof object !== "object") return object;
|
|
1587
|
+
if (Object.isFrozen(object)) return object;
|
|
1588
|
+
Object.freeze(object);
|
|
1589
|
+
for (const value of Object.values(object)) deepFreeze(value);
|
|
1590
|
+
if (Array.isArray(object)) for (const value of object) deepFreeze(value);
|
|
1591
|
+
return object;
|
|
1592
|
+
}
|
|
1593
|
+
function assertIdType(id, type) {
|
|
1594
|
+
if (!type.isId(id)) {
|
|
1595
|
+
throw new Error(`Expected ${type.typeName} id, got ${JSON.stringify(id)}`);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/storage.ts
|
|
1600
|
+
function createInMemoryStorage() {
|
|
1601
|
+
const records = /* @__PURE__ */ new Map();
|
|
1602
|
+
let schema;
|
|
1603
|
+
return {
|
|
1604
|
+
get: (id) => records.get(id),
|
|
1605
|
+
getAll: () => [...records.values()],
|
|
1606
|
+
set: (record) => {
|
|
1607
|
+
records.set(record.id, record);
|
|
1608
|
+
},
|
|
1609
|
+
delete: (id) => {
|
|
1610
|
+
records.delete(id);
|
|
1611
|
+
},
|
|
1612
|
+
clear: () => records.clear(),
|
|
1613
|
+
getSchema: () => schema,
|
|
1614
|
+
setSchema: (next) => {
|
|
1615
|
+
schema = next;
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/graphemes.ts
|
|
1621
|
+
var segmenter;
|
|
1622
|
+
var segmenterChecked = false;
|
|
1623
|
+
function getSegmenter() {
|
|
1624
|
+
if (!segmenterChecked) {
|
|
1625
|
+
segmenterChecked = true;
|
|
1626
|
+
try {
|
|
1627
|
+
if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
|
|
1628
|
+
segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
1629
|
+
}
|
|
1630
|
+
} catch {
|
|
1631
|
+
segmenter = void 0;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return segmenter;
|
|
1635
|
+
}
|
|
1636
|
+
function* iterateGraphemes(str) {
|
|
1637
|
+
const seg = getSegmenter();
|
|
1638
|
+
if (seg) {
|
|
1639
|
+
for (const { segment } of seg.segment(str)) yield segment;
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
for (const codePoint of str) yield codePoint;
|
|
1643
|
+
}
|
|
1644
|
+
function getGraphemes(str) {
|
|
1645
|
+
return [...iterateGraphemes(str)];
|
|
1646
|
+
}
|
|
1647
|
+
function getGraphemeLength(str) {
|
|
1648
|
+
let n = 0;
|
|
1649
|
+
for (const _ of iterateGraphemes(str)) n++;
|
|
1650
|
+
return n;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
export { MigrationFailureReason, RecordType, Store, StoreQueries, StoreSchema, StoreSideEffects, TLDR_FILE_FORMAT_VERSION, UNIQUE_ID_LENGTH, ZERO_INDEX_KEY, ZKEY_SIGNIFICANT_DIGITS, applyChangeToDiff, applyCollectionDiff, applyMigrationToStore, applyRecordMigration, assertIdType, cloneRecordsDiff, compareIndexKeys, compareZKeys, createComputedCache, createEmptyRecordsDiff, createInMemoryStorage, createMigrationIds, createMigrationSequence, createRecordMigrationSequence, createRecordType, devFreeze, freezeRecord, getGraphemeLength, getGraphemes, getIndexAbove, getIndexBelow, getIndexBetween, getIndexablePropertyOf, getIndices, getIndicesAbove, getIndicesBelow, getIndicesBetween, indexKeyToZKey, isCollectionDiffEmpty, isIndexKey, isRecordLike, isRecordShallowEqual, isRecordsDiffEmpty, isSerializedSchemaV1, iterateGraphemes, matchesQuery, matchesQueryValue, parseMigrationId, parseRecordId, parseTldrFile, reverseRecordsDiff, serializeTldrFile, sortByIndex, squashRecordDiffs, squashRecordDiffsMutable, storeSnapshotToTldrFile, tldrFileToStoreSnapshot, uniqueId, validateIndexKey, zKeyToBigInt };
|
|
1260
1654
|
//# sourceMappingURL=index.js.map
|
|
1261
1655
|
//# sourceMappingURL=index.js.map
|