@ibodr/store 0.2.5 → 0.2.7

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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { registerDrawLibraryVersion, assert, STRUCTURED_CLONE_OBJECT_PROTOTYPE, objectMapEntries, structuredClone, uniqueId, objectMapValues, areArraysShallowEqual, isEqual, throttleToNextFrame, filterEntries, getOwnProperty, objectMapKeys, WeakCache, Result, exhaustiveSwitchError } from '@ibodr/utils';
1
+ import { registerDrawLibraryVersion, assert, STRUCTURED_CLONE_OBJECT_PROTOTYPE, objectMapEntries, objectMapValues, structuredClone, uniqueId, areArraysShallowEqual, isEqual, throttleToNextFrame, filterEntries, getOwnProperty, objectMapKeys, WeakCache, Result, exhaustiveSwitchError, objectMapFromEntries } from '@ibodr/utils';
2
2
  import { atom, UNINITIALIZED, transact, computed, isUninitialized, RESET_VALUE, withDiff, EMPTY_ARRAY, reactor } from '@ibodr/state';
3
3
 
4
4
  // src/index.ts
@@ -64,7 +64,7 @@ function cachedHashString(string) {
64
64
  hashed = hashString(string);
65
65
  if (stringHashCacheCount === STRING_HASH_CACHE_SIZE) {
66
66
  stringHashCacheCount = 0;
67
- stringHashCache = {};
67
+ stringHashCache = /* @__PURE__ */ Object.create(null);
68
68
  }
69
69
  stringHashCache[string] = hashed;
70
70
  stringHashCacheCount++;
@@ -109,7 +109,7 @@ function nextHash() {
109
109
  var weakMap = /* @__PURE__ */ new WeakMap();
110
110
  var symbolMap = /* @__PURE__ */ Object.create(null);
111
111
  var _objHashUID = 0;
112
- var stringHashCache = {};
112
+ var stringHashCache = /* @__PURE__ */ Object.create(null);
113
113
  var stringHashCacheCount = 0;
114
114
  var STRING_HASH_CACHE_SIZE = 24e3;
115
115
  var SHIFT = 5;
@@ -127,6 +127,9 @@ function SetRef(ref) {
127
127
  function arrCopy(arr, offset = 0) {
128
128
  return arr.slice(offset);
129
129
  }
130
+ function is(a, b) {
131
+ return a === b || a !== a && b !== b;
132
+ }
130
133
  var OwnerID = class {
131
134
  };
132
135
  var ImmutableMap = class _ImmutableMap {
@@ -164,19 +167,6 @@ var ImmutableMap = class _ImmutableMap {
164
167
  }
165
168
  });
166
169
  }
167
- /**
168
- * Gets the value associated with the specified key, with a fallback value.
169
- *
170
- * @param k - The key to look up
171
- * @param notSetValue - The value to return if the key is not found
172
- * @returns The value associated with the key, or the fallback value if not found
173
- * @example
174
- * ```ts
175
- * const map = new ImmutableMap([['key1', 'value1']])
176
- * console.log(map.get('key1', 'default')) // 'value1'
177
- * console.log(map.get('missing', 'default')) // 'default'
178
- * ```
179
- */
180
170
  get(k, notSetValue) {
181
171
  return this._root ? this._root.get(0, void 0, k, notSetValue) : notSetValue;
182
172
  }
@@ -353,7 +343,7 @@ var ArrayMapNode = class _ArrayMapNode {
353
343
  get(_shift, _keyHash, key, notSetValue) {
354
344
  const entries = this.entries;
355
345
  for (let ii = 0, len = entries.length; ii < len; ii++) {
356
- if (Object.is(key, entries[ii][0])) {
346
+ if (is(key, entries[ii][0])) {
357
347
  return entries[ii][1];
358
348
  }
359
349
  }
@@ -365,7 +355,7 @@ var ArrayMapNode = class _ArrayMapNode {
365
355
  let idx = 0;
366
356
  const len = entries.length;
367
357
  for (; idx < len; idx++) {
368
- if (Object.is(key, entries[idx][0])) {
358
+ if (is(key, entries[idx][0])) {
369
359
  break;
370
360
  }
371
361
  }
@@ -539,7 +529,7 @@ var HashCollisionNode = class _HashCollisionNode {
539
529
  get(shift, keyHash, key, notSetValue) {
540
530
  const entries = this.entries;
541
531
  for (let ii = 0, len = entries.length; ii < len; ii++) {
542
- if (Object.is(key, entries[ii][0])) {
532
+ if (is(key, entries[ii][0])) {
543
533
  return entries[ii][1];
544
534
  }
545
535
  }
@@ -562,7 +552,7 @@ var HashCollisionNode = class _HashCollisionNode {
562
552
  let idx = 0;
563
553
  const len = entries.length;
564
554
  for (; idx < len; idx++) {
565
- if (Object.is(key, entries[idx][0])) {
555
+ if (is(key, entries[idx][0])) {
566
556
  break;
567
557
  }
568
558
  }
@@ -607,11 +597,11 @@ var ValueNode = class _ValueNode {
607
597
  keyHash;
608
598
  entry;
609
599
  get(shift, keyHash, key, notSetValue) {
610
- return Object.is(key, this.entry[0]) ? this.entry[1] : notSetValue;
600
+ return is(key, this.entry[0]) ? this.entry[1] : notSetValue;
611
601
  }
612
602
  update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) {
613
603
  const removed = value === NOT_SET;
614
- const keyMatch = Object.is(key, this.entry[0]);
604
+ const keyMatch = is(key, this.entry[0]);
615
605
  if (keyMatch ? value === this.entry[1] : removed) {
616
606
  return this;
617
607
  }
@@ -999,6 +989,47 @@ var AtomMap = class {
999
989
  }
1000
990
  return this;
1001
991
  }
992
+ /**
993
+ * Gets the value associated with a key, inserting `defaultValue` first if the key doesn't exist.
994
+ * This method is reactive and will cause reactive contexts to update when the value changes.
995
+ *
996
+ * @param key - The key to retrieve the value for
997
+ * @param defaultValue - The value to insert if the key doesn't exist
998
+ * @returns The existing value, or `defaultValue` if the key was missing
999
+ * @example
1000
+ * ```ts
1001
+ * const map = new AtomMap('myMap')
1002
+ * console.log(map.getOrInsert('count', 0)) // 0, and 'count' is now set
1003
+ * console.log(map.getOrInsert('count', 10)) // 0
1004
+ * ```
1005
+ */
1006
+ getOrInsert(key, defaultValue) {
1007
+ return this.getOrInsertComputed(key, () => defaultValue);
1008
+ }
1009
+ /**
1010
+ * Gets the value associated with a key, inserting the result of `callback` first if the key
1011
+ * doesn't exist. The callback only runs when the key is missing.
1012
+ * This method is reactive and will cause reactive contexts to update when the value changes.
1013
+ *
1014
+ * @param key - The key to retrieve the value for
1015
+ * @param callback - Called with the key to produce the value to insert
1016
+ * @returns The existing value, or the newly computed value if the key was missing
1017
+ * @example
1018
+ * ```ts
1019
+ * const map = new AtomMap<string, string[]>('myMap')
1020
+ * map.getOrInsertComputed('tags', () => []).push('new')
1021
+ * ```
1022
+ */
1023
+ getOrInsertComputed(key, callback) {
1024
+ const valueAtom = this.getAtom(key);
1025
+ if (valueAtom) {
1026
+ const value2 = valueAtom.get();
1027
+ if (value2 !== UNINITIALIZED) return value2;
1028
+ }
1029
+ const value = callback(key);
1030
+ this.set(key, value);
1031
+ return value;
1032
+ }
1002
1033
  /**
1003
1034
  * Updates an existing value using an updater function.
1004
1035
  *
@@ -1116,7 +1147,7 @@ var AtomMap = class {
1116
1147
  *entries() {
1117
1148
  for (const [key, valueAtom] of this.atoms.get()) {
1118
1149
  const value = valueAtom.get();
1119
- assert(value !== UNINITIALIZED);
1150
+ if (value === UNINITIALIZED) continue;
1120
1151
  yield [key, value];
1121
1152
  }
1122
1153
  }
@@ -1135,7 +1166,8 @@ var AtomMap = class {
1135
1166
  * ```
1136
1167
  */
1137
1168
  *keys() {
1138
- for (const key of this.atoms.get().keys()) {
1169
+ for (const [key, valueAtom] of this.atoms.get()) {
1170
+ if (valueAtom.__unsafe__getWithoutCapture() === UNINITIALIZED) continue;
1139
1171
  yield key;
1140
1172
  }
1141
1173
  }
@@ -1156,7 +1188,7 @@ var AtomMap = class {
1156
1188
  *values() {
1157
1189
  for (const valueAtom of this.atoms.get().values()) {
1158
1190
  const value = valueAtom.get();
1159
- assert(value !== UNINITIALIZED);
1191
+ if (value === UNINITIALIZED) continue;
1160
1192
  yield value;
1161
1193
  }
1162
1194
  }
@@ -1361,6 +1393,8 @@ var IncrementalSetConstructor = class {
1361
1393
  if (numRemoved === 0 && numAdded === 0) {
1362
1394
  return void 0;
1363
1395
  }
1396
+ if (numAdded === 0) delete this.diff.added;
1397
+ if (numRemoved === 0) delete this.diff.removed;
1364
1398
  return { value: this.nextValue, diff: this.diff };
1365
1399
  }
1366
1400
  /**
@@ -1500,13 +1534,11 @@ function createRecordMigrationSequence(opts) {
1500
1534
  return createMigrationSequence({
1501
1535
  sequenceId,
1502
1536
  retroactive: opts.retroactive ?? true,
1503
- sequence: opts.sequence.map(
1504
- (m) => "id" in m ? {
1505
- ...m,
1506
- scope: "record",
1507
- filter: (r) => r.typeName === opts.recordType && (m.filter?.(r) ?? true) && (opts.filter?.(r) ?? true)
1508
- } : m
1509
- )
1537
+ sequence: opts.sequence.map((m) => ({
1538
+ ...m,
1539
+ scope: "record",
1540
+ filter: (r) => r.typeName === opts.recordType && (m.filter?.(r) ?? true) && (opts.filter?.(r) ?? true)
1541
+ }))
1510
1542
  });
1511
1543
  }
1512
1544
  function sortMigrations(migrations) {
@@ -1529,11 +1561,12 @@ function sortMigrations(migrations) {
1529
1561
  }
1530
1562
  if (m.dependsOn) {
1531
1563
  for (const depId of m.dependsOn) {
1532
- if (byId.has(depId)) {
1533
- dependents.get(depId).add(m.id);
1534
- explicitDeps.get(m.id).add(depId);
1535
- inDegree.set(m.id, inDegree.get(m.id) + 1);
1536
- }
1564
+ if (!byId.has(depId)) continue;
1565
+ explicitDeps.get(m.id).add(depId);
1566
+ const dependentsOfDep = dependents.get(depId);
1567
+ if (dependentsOfDep.has(m.id)) continue;
1568
+ dependentsOfDep.add(m.id);
1569
+ inDegree.set(m.id, inDegree.get(m.id) + 1);
1537
1570
  }
1538
1571
  }
1539
1572
  }
@@ -1629,59 +1662,115 @@ function createEmptyRecordsDiff() {
1629
1662
  return { added: {}, updated: {}, removed: {} };
1630
1663
  }
1631
1664
  function reverseRecordsDiff(diff) {
1632
- const result = { added: diff.removed, removed: diff.added, updated: {} };
1633
- for (const [from, to] of Object.values(diff.updated)) {
1665
+ const result = {
1666
+ added: { ...diff.removed },
1667
+ removed: { ...diff.added },
1668
+ updated: {}
1669
+ };
1670
+ for (const [from, to] of objectMapValues(diff.updated)) {
1634
1671
  result.updated[from.id] = [to, from];
1635
1672
  }
1636
1673
  return result;
1637
1674
  }
1638
1675
  function isRecordsDiffEmpty(diff) {
1639
- return Object.keys(diff.added).length === 0 && Object.keys(diff.updated).length === 0 && Object.keys(diff.removed).length === 0;
1676
+ return !hasAnyKey(diff.added) && !hasAnyKey(diff.updated) && !hasAnyKey(diff.removed);
1677
+ }
1678
+ function hasAnyKey(obj) {
1679
+ for (const _ in obj) return true;
1680
+ return false;
1640
1681
  }
1641
1682
  function squashRecordDiffs(diffs, options) {
1642
- const result = options?.mutateFirstDiff ? diffs[0] : { added: {}, removed: {}, updated: {} };
1643
- squashRecordDiffsMutable(result, options?.mutateFirstDiff ? diffs.slice(1) : diffs);
1683
+ if (options?.mutateFirstDiff) {
1684
+ const result2 = diffs[0];
1685
+ if (!result2) return createEmptyRecordsDiff();
1686
+ for (const _id in result2.updated) {
1687
+ const id = _id;
1688
+ const [from, to] = result2.updated[id];
1689
+ result2.updated[id] = [from, to];
1690
+ }
1691
+ squashRecordDiffsMutable(result2, diffs, 1);
1692
+ return result2;
1693
+ }
1694
+ const result = { added: {}, removed: {}, updated: {} };
1695
+ squashRecordDiffsMutable(result, diffs);
1644
1696
  return result;
1645
1697
  }
1646
- function squashRecordDiffsMutable(target, diffs) {
1647
- for (const diff of diffs) {
1648
- for (const [id, value] of objectMapEntries(diff.added)) {
1649
- if (target.removed[id]) {
1698
+ function squashRecordDiffsMutable(target, diffs, fromIndex = 0) {
1699
+ squashRecordDiffsMutableImpl(target, diffs, fromIndex);
1700
+ }
1701
+ function squashRecordDiffsMutableByType(target, diffs, typeName) {
1702
+ return squashRecordDiffsMutableImpl(target, diffs, 0, typeName);
1703
+ }
1704
+ function squashRecordDiffsMutableImpl(target, diffs, fromIndex, typeName) {
1705
+ let sizeChange = 0;
1706
+ const trackSize = typeName !== void 0;
1707
+ for (let i = fromIndex; i < diffs.length; i++) {
1708
+ const diff = diffs[i];
1709
+ const targetHasRemoved = hasAnyKey(target.removed);
1710
+ for (const _id in diff.added) {
1711
+ const id = _id;
1712
+ const value = diff.added[id];
1713
+ if (typeName !== void 0 && value.typeName !== typeName) continue;
1714
+ if (targetHasRemoved && target.removed[id]) {
1650
1715
  const original = target.removed[id];
1651
1716
  delete target.removed[id];
1717
+ if (trackSize) sizeChange--;
1652
1718
  if (original !== value) {
1653
1719
  target.updated[id] = [original, value];
1720
+ if (trackSize) sizeChange++;
1654
1721
  }
1655
1722
  } else {
1723
+ if (trackSize && !target.added[id]) sizeChange++;
1656
1724
  target.added[id] = value;
1657
1725
  }
1658
1726
  }
1659
- for (const [id, [_from, to]] of objectMapEntries(diff.updated)) {
1660
- if (target.added[id]) {
1727
+ const targetHasAdded = hasAnyKey(target.added);
1728
+ for (const _id in diff.updated) {
1729
+ const id = _id;
1730
+ const to = diff.updated[id][1];
1731
+ if (typeName !== void 0 && to.typeName !== typeName) continue;
1732
+ if (targetHasAdded && target.added[id]) {
1661
1733
  target.added[id] = to;
1734
+ if (trackSize && target.updated[id]) sizeChange--;
1662
1735
  delete target.updated[id];
1663
- delete target.removed[id];
1736
+ if (targetHasRemoved) {
1737
+ if (trackSize && target.removed[id]) sizeChange--;
1738
+ delete target.removed[id];
1739
+ }
1664
1740
  continue;
1665
1741
  }
1666
- if (target.updated[id]) {
1667
- target.updated[id] = [target.updated[id][0], to];
1668
- delete target.removed[id];
1742
+ const existing = target.updated[id];
1743
+ if (existing) {
1744
+ existing[1] = to;
1745
+ if (targetHasRemoved) {
1746
+ if (trackSize && target.removed[id]) sizeChange--;
1747
+ delete target.removed[id];
1748
+ }
1669
1749
  continue;
1670
1750
  }
1671
- target.updated[id] = diff.updated[id];
1672
- delete target.removed[id];
1751
+ target.updated[id] = [diff.updated[id][0], to];
1752
+ if (trackSize) sizeChange++;
1753
+ if (targetHasRemoved) {
1754
+ if (trackSize && target.removed[id]) sizeChange--;
1755
+ delete target.removed[id];
1756
+ }
1673
1757
  }
1674
- for (const [id, value] of objectMapEntries(diff.removed)) {
1758
+ for (const _id in diff.removed) {
1759
+ const id = _id;
1760
+ if (typeName !== void 0 && diff.removed[id].typeName !== typeName) continue;
1675
1761
  if (target.added[id]) {
1676
1762
  delete target.added[id];
1763
+ if (trackSize) sizeChange--;
1677
1764
  } else if (target.updated[id]) {
1678
1765
  target.removed[id] = target.updated[id][0];
1679
1766
  delete target.updated[id];
1680
1767
  } else {
1681
- target.removed[id] = value;
1768
+ if (trackSize && !target.removed[id]) sizeChange++;
1769
+ target.removed[id] = diff.removed[id];
1682
1770
  }
1683
1771
  }
1684
1772
  }
1773
+ return sizeChange;
1685
1774
  }
1686
1775
  var RecordType = class _RecordType {
1687
1776
  /**
@@ -1758,9 +1847,9 @@ var RecordType = class _RecordType {
1758
1847
  create(properties) {
1759
1848
  const result = {
1760
1849
  ...this.createDefaultProperties(),
1761
- id: "id" in properties ? properties.id : this.createId()
1850
+ id: properties.id ?? this.createId()
1762
1851
  };
1763
- for (const [k, v] of Object.entries(properties)) {
1852
+ for (const [k, v] of objectMapEntries(properties)) {
1764
1853
  if (v !== void 0) {
1765
1854
  result[k] = v;
1766
1855
  }
@@ -1988,7 +2077,7 @@ function objectMatchesQuery(query, object) {
1988
2077
  const value = object[key];
1989
2078
  if (isQueryValueMatcher(matcher)) {
1990
2079
  if ("eq" in matcher && value !== matcher.eq) return false;
1991
- if ("neq" in matcher && value === matcher.neq) return false;
2080
+ if ("neq" in matcher && (value === matcher.neq || value === void 0)) return false;
1992
2081
  if ("gt" in matcher && (typeof value !== "number" || value <= matcher.gt)) return false;
1993
2082
  continue;
1994
2083
  }
@@ -2001,7 +2090,9 @@ function objectMatchesQuery(query, object) {
2001
2090
  }
2002
2091
  function executeQuery(store, typeName, query) {
2003
2092
  const matcherPaths = extractMatcherPaths(query);
2004
- const matchIds = Object.fromEntries(matcherPaths.map(({ path }) => [path, /* @__PURE__ */ new Set()]));
2093
+ const matchIds = objectMapFromEntries(
2094
+ matcherPaths.map(({ path }) => [path, /* @__PURE__ */ new Set()])
2095
+ );
2005
2096
  for (const { path, matcher } of matcherPaths) {
2006
2097
  const index = store.index(typeName, path);
2007
2098
  if ("eq" in matcher) {
@@ -2032,7 +2123,7 @@ function executeQuery(store, typeName, query) {
2032
2123
  return /* @__PURE__ */ new Set();
2033
2124
  }
2034
2125
  }
2035
- return intersectSets(Object.values(matchIds));
2126
+ return intersectSets(objectMapValues(matchIds));
2036
2127
  }
2037
2128
 
2038
2129
  // src/lib/StoreQueries.ts
@@ -2103,17 +2194,19 @@ var StoreQueries = class {
2103
2194
  * have been added, updated, or removed.
2104
2195
  *
2105
2196
  * @param typeName - The type name to filter the history by
2106
- * @returns A computed value containing the current epoch and diffs of changes for the specified type
2197
+ * @returns A computed value containing a change counter and diffs of changes for the specified type
2107
2198
  *
2108
2199
  * @example
2109
2200
  * ```ts
2110
2201
  * // Track changes to book records only
2111
2202
  * const bookHistory = store.query.filterHistory('book')
2112
2203
  *
2113
- * // React to book changes
2114
- * react('book-changes', () => {
2115
- * const currentEpoch = bookHistory.get()
2116
- * console.log('Books updated at epoch:', currentEpoch)
2204
+ * // Log the books added since the last run
2205
+ * react('book-changes', (lastReactedEpoch) => {
2206
+ * const diffs = bookHistory.getDiffSince(lastReactedEpoch)
2207
+ * // no diffs to give, e.g. on the first run: rebuild anything derived from scratch
2208
+ * if (diffs === RESET_VALUE) return
2209
+ * for (const diff of diffs) console.log('Added:', Object.keys(diff.added))
2117
2210
  * })
2118
2211
  * ```
2119
2212
  *
@@ -2129,60 +2222,13 @@ var StoreQueries = class {
2129
2222
  if (isUninitialized(lastValue)) {
2130
2223
  return this.history.get();
2131
2224
  }
2225
+ const next = Math.max(this.history.get(), lastValue + 1);
2132
2226
  const diff = this.history.getDiffSince(lastComputedEpoch);
2133
- if (diff === RESET_VALUE) return this.history.get();
2227
+ if (diff === RESET_VALUE) return next;
2134
2228
  const res = { added: {}, removed: {}, updated: {} };
2135
- let numAdded = 0;
2136
- let numRemoved = 0;
2137
- let numUpdated = 0;
2138
- for (const changes of diff) {
2139
- for (const added of objectMapValues(changes.added)) {
2140
- if (added.typeName === typeName) {
2141
- if (res.removed[added.id]) {
2142
- const original = res.removed[added.id];
2143
- delete res.removed[added.id];
2144
- numRemoved--;
2145
- if (original !== added) {
2146
- res.updated[added.id] = [original, added];
2147
- numUpdated++;
2148
- }
2149
- } else {
2150
- res.added[added.id] = added;
2151
- numAdded++;
2152
- }
2153
- }
2154
- }
2155
- for (const [from, to] of objectMapValues(changes.updated)) {
2156
- if (to.typeName === typeName) {
2157
- if (res.added[to.id]) {
2158
- res.added[to.id] = to;
2159
- } else if (res.updated[to.id]) {
2160
- res.updated[to.id] = [res.updated[to.id][0], to];
2161
- } else {
2162
- res.updated[to.id] = [from, to];
2163
- numUpdated++;
2164
- }
2165
- }
2166
- }
2167
- for (const removed of objectMapValues(changes.removed)) {
2168
- if (removed.typeName === typeName) {
2169
- if (res.added[removed.id]) {
2170
- delete res.added[removed.id];
2171
- numAdded--;
2172
- } else if (res.updated[removed.id]) {
2173
- res.removed[removed.id] = res.updated[removed.id][0];
2174
- delete res.updated[removed.id];
2175
- numUpdated--;
2176
- numRemoved++;
2177
- } else {
2178
- res.removed[removed.id] = removed;
2179
- numRemoved++;
2180
- }
2181
- }
2182
- }
2183
- }
2184
- if (numAdded || numRemoved || numUpdated) {
2185
- return withDiff(this.history.get(), res);
2229
+ const size = squashRecordDiffsMutableByType(res, diff, typeName);
2230
+ if (size) {
2231
+ return withDiff(next, res);
2186
2232
  } else {
2187
2233
  return lastValue;
2188
2234
  }
@@ -2278,7 +2324,7 @@ var StoreQueries = class {
2278
2324
  setConstructor.add(id);
2279
2325
  setConstructors.set(value, setConstructor);
2280
2326
  };
2281
- const remove2 = (value, id) => {
2327
+ const remove = (value, id) => {
2282
2328
  let set = setConstructors.get(value);
2283
2329
  if (!set) set = new IncrementalSetConstructor(prevValue.get(value) ?? /* @__PURE__ */ new Set());
2284
2330
  set.remove(id);
@@ -2299,7 +2345,7 @@ var StoreQueries = class {
2299
2345
  const next = getPropertyValue(to);
2300
2346
  if (prev !== next) {
2301
2347
  if (prev !== void 0) {
2302
- remove2(prev, to.id);
2348
+ remove(prev, to.id);
2303
2349
  }
2304
2350
  if (next !== void 0) {
2305
2351
  add(next, to.id);
@@ -2311,7 +2357,7 @@ var StoreQueries = class {
2311
2357
  if (record.typeName === typeName) {
2312
2358
  const value = getPropertyValue(record);
2313
2359
  if (value !== void 0) {
2314
- remove2(value, record.id);
2360
+ remove(value, record.id);
2315
2361
  }
2316
2362
  }
2317
2363
  }
@@ -2360,7 +2406,7 @@ var StoreQueries = class {
2360
2406
  *
2361
2407
  * @public
2362
2408
  */
2363
- record(typeName, queryCreator = () => ({}), name = "record:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
2409
+ record(typeName, queryCreator = () => ({}), name = "record:" + typeName + ":" + queryCreator.toString()) {
2364
2410
  const ids = this.ids(typeName, queryCreator, name);
2365
2411
  return computed(name, () => {
2366
2412
  for (const id of ids.get()) {
@@ -2393,7 +2439,7 @@ var StoreQueries = class {
2393
2439
  *
2394
2440
  * @public
2395
2441
  */
2396
- records(typeName, queryCreator = () => ({}), name = "records:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
2442
+ records(typeName, queryCreator = () => ({}), name = "records:" + typeName + ":" + queryCreator.toString()) {
2397
2443
  const ids = this.ids(typeName, queryCreator, "ids:" + name);
2398
2444
  return computed(
2399
2445
  name,
@@ -2430,12 +2476,12 @@ var StoreQueries = class {
2430
2476
  *
2431
2477
  * @public
2432
2478
  */
2433
- ids(typeName, queryCreator = () => ({}), name = "ids:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
2479
+ ids(typeName, queryCreator = () => ({}), name = "ids:" + typeName + ":" + queryCreator.toString()) {
2434
2480
  const typeHistory = this.filterHistory(typeName);
2435
2481
  const fromScratch = () => {
2436
2482
  typeHistory.get();
2437
2483
  const query = queryCreator();
2438
- if (Object.keys(query).length === 0) {
2484
+ if (!hasAnyKey(query)) {
2439
2485
  return this.getAllIdsForType(typeName);
2440
2486
  }
2441
2487
  return executeQuery(this, typeName, query);
@@ -2449,7 +2495,7 @@ var StoreQueries = class {
2449
2495
  return prevValue;
2450
2496
  }
2451
2497
  };
2452
- const cachedQuery = computed("ids_query:" + name, queryCreator, {
2498
+ const cachedQuery = computed("ids_query:" + name, () => queryCreator(), {
2453
2499
  isEqual
2454
2500
  });
2455
2501
  return computed(
@@ -2548,6 +2594,15 @@ var StoreSideEffects = class {
2548
2594
  _afterDeleteHandlers = {};
2549
2595
  _operationCompleteHandlers = [];
2550
2596
  _isEnabled = true;
2597
+ // Handler arrays are replaced, never mutated in place: dispatch iterates the array it looked up,
2598
+ // so a handler that removes itself (or registers another) while running must not shift the
2599
+ // entries out from under that iteration.
2600
+ addHandler(handlers, typeName, handler) {
2601
+ handlers[typeName] = [...handlers[typeName] ?? [], handler];
2602
+ return () => {
2603
+ handlers[typeName] = withoutFirst(handlers[typeName] ?? [], handler);
2604
+ };
2605
+ }
2551
2606
  /**
2552
2607
  * Checks whether side effects are currently enabled.
2553
2608
  * When disabled, all side effect handlers are bypassed.
@@ -2779,10 +2834,7 @@ var StoreSideEffects = class {
2779
2834
  * @returns A callback that removes the handler.
2780
2835
  */
2781
2836
  registerBeforeCreateHandler(typeName, handler) {
2782
- const handlers = this._beforeCreateHandlers[typeName];
2783
- if (!handlers) this._beforeCreateHandlers[typeName] = [];
2784
- this._beforeCreateHandlers[typeName].push(handler);
2785
- return () => remove(this._beforeCreateHandlers[typeName], handler);
2837
+ return this.addHandler(this._beforeCreateHandlers, typeName, handler);
2786
2838
  }
2787
2839
  /**
2788
2840
  * Register a handler to be called after a record is created. This is useful for side-effects
@@ -2807,10 +2859,7 @@ var StoreSideEffects = class {
2807
2859
  * @returns A callback that removes the handler.
2808
2860
  */
2809
2861
  registerAfterCreateHandler(typeName, handler) {
2810
- const handlers = this._afterCreateHandlers[typeName];
2811
- if (!handlers) this._afterCreateHandlers[typeName] = [];
2812
- this._afterCreateHandlers[typeName].push(handler);
2813
- return () => remove(this._afterCreateHandlers[typeName], handler);
2862
+ return this.addHandler(this._afterCreateHandlers, typeName, handler);
2814
2863
  }
2815
2864
  /**
2816
2865
  * Register a handler to be called before a record is changed. The handler is given the old and
@@ -2839,10 +2888,7 @@ var StoreSideEffects = class {
2839
2888
  * @returns A callback that removes the handler.
2840
2889
  */
2841
2890
  registerBeforeChangeHandler(typeName, handler) {
2842
- const handlers = this._beforeChangeHandlers[typeName];
2843
- if (!handlers) this._beforeChangeHandlers[typeName] = [];
2844
- this._beforeChangeHandlers[typeName].push(handler);
2845
- return () => remove(this._beforeChangeHandlers[typeName], handler);
2891
+ return this.addHandler(this._beforeChangeHandlers, typeName, handler);
2846
2892
  }
2847
2893
  /**
2848
2894
  * Register a handler to be called after a record is changed. This is useful for side-effects
@@ -2866,10 +2912,7 @@ var StoreSideEffects = class {
2866
2912
  * @returns A callback that removes the handler.
2867
2913
  */
2868
2914
  registerAfterChangeHandler(typeName, handler) {
2869
- const handlers = this._afterChangeHandlers[typeName];
2870
- if (!handlers) this._afterChangeHandlers[typeName] = [];
2871
- this._afterChangeHandlers[typeName].push(handler);
2872
- return () => remove(this._afterChangeHandlers[typeName], handler);
2915
+ return this.addHandler(this._afterChangeHandlers, typeName, handler);
2873
2916
  }
2874
2917
  /**
2875
2918
  * Register a handler to be called before a record is deleted. The handler can return `false` to
@@ -2895,10 +2938,7 @@ var StoreSideEffects = class {
2895
2938
  * @returns A callback that removes the handler.
2896
2939
  */
2897
2940
  registerBeforeDeleteHandler(typeName, handler) {
2898
- const handlers = this._beforeDeleteHandlers[typeName];
2899
- if (!handlers) this._beforeDeleteHandlers[typeName] = [];
2900
- this._beforeDeleteHandlers[typeName].push(handler);
2901
- return () => remove(this._beforeDeleteHandlers[typeName], handler);
2941
+ return this.addHandler(this._beforeDeleteHandlers, typeName, handler);
2902
2942
  }
2903
2943
  /**
2904
2944
  * Register a handler to be called after a record is deleted. This is useful for side-effects
@@ -2925,10 +2965,7 @@ var StoreSideEffects = class {
2925
2965
  * @returns A callback that removes the handler.
2926
2966
  */
2927
2967
  registerAfterDeleteHandler(typeName, handler) {
2928
- const handlers = this._afterDeleteHandlers[typeName];
2929
- if (!handlers) this._afterDeleteHandlers[typeName] = [];
2930
- this._afterDeleteHandlers[typeName].push(handler);
2931
- return () => remove(this._afterDeleteHandlers[typeName], handler);
2968
+ return this.addHandler(this._afterDeleteHandlers, typeName, handler);
2932
2969
  }
2933
2970
  /**
2934
2971
  * Register a handler to be called when a store completes an atomic operation.
@@ -2957,15 +2994,16 @@ var StoreSideEffects = class {
2957
2994
  * @public
2958
2995
  */
2959
2996
  registerOperationCompleteHandler(handler) {
2960
- this._operationCompleteHandlers.push(handler);
2961
- return () => remove(this._operationCompleteHandlers, handler);
2997
+ this._operationCompleteHandlers = [...this._operationCompleteHandlers, handler];
2998
+ return () => {
2999
+ this._operationCompleteHandlers = withoutFirst(this._operationCompleteHandlers, handler);
3000
+ };
2962
3001
  }
2963
3002
  };
2964
- function remove(array, item) {
3003
+ function withoutFirst(array, item) {
2965
3004
  const index = array.indexOf(item);
2966
- if (index >= 0) {
2967
- array.splice(index, 1);
2968
- }
3005
+ if (index < 0) return array;
3006
+ return [...array.slice(0, index), ...array.slice(index + 1)];
2969
3007
  }
2970
3008
 
2971
3009
  // src/lib/Store.ts
@@ -3031,12 +3069,11 @@ var Store = class {
3031
3069
  */
3032
3070
  historyReactor;
3033
3071
  /**
3034
- * Function to dispose of any in-flight timeouts.
3072
+ * Cancels the history flush scheduled for the next frame, if any.
3035
3073
  *
3036
3074
  * @internal
3037
3075
  */
3038
- cancelHistoryReactor() {
3039
- }
3076
+ cancelHistoryReactor = null;
3040
3077
  /**
3041
3078
  * The schema that defines the structure and validation rules for records in this store.
3042
3079
  *
@@ -3109,52 +3146,51 @@ var Store = class {
3109
3146
  },
3110
3147
  { scheduleEffect: (cb) => this.cancelHistoryReactor = throttleToNextFrame(cb) }
3111
3148
  );
3112
- this.scopedTypes = {
3113
- document: new Set(
3114
- objectMapValues(this.schema.types).filter((t) => t.scope === "document").map((t) => t.typeName)
3115
- ),
3116
- session: new Set(
3117
- objectMapValues(this.schema.types).filter((t) => t.scope === "session").map((t) => t.typeName)
3118
- ),
3119
- presence: new Set(
3120
- objectMapValues(this.schema.types).filter((t) => t.scope === "presence").map((t) => t.typeName)
3121
- )
3149
+ const scopedTypes = {
3150
+ document: /* @__PURE__ */ new Set(),
3151
+ session: /* @__PURE__ */ new Set(),
3152
+ presence: /* @__PURE__ */ new Set()
3122
3153
  };
3154
+ for (const type of objectMapValues(this.schema.types)) {
3155
+ scopedTypes[type.scope].add(type.typeName);
3156
+ }
3157
+ this.scopedTypes = scopedTypes;
3123
3158
  }
3124
3159
  _flushHistory() {
3125
3160
  if (this.historyAccumulator.hasChanges()) {
3126
3161
  const entries = this.historyAccumulator.flush();
3162
+ const errors = [];
3127
3163
  for (const { changes, source } of entries) {
3128
- let instanceChanges = null;
3129
- let documentChanges = null;
3130
- let presenceChanges = null;
3164
+ const scopedChanges = /* @__PURE__ */ new Map();
3131
3165
  for (const { onHistory, filters } of this.listeners) {
3132
3166
  if (filters.source !== "all" && filters.source !== source) {
3133
3167
  continue;
3134
3168
  }
3169
+ let listenerChanges = changes;
3135
3170
  if (filters.scope !== "all") {
3136
- if (filters.scope === "document") {
3137
- documentChanges ??= this.filterChangesByScope(changes, "document");
3138
- if (!documentChanges) continue;
3139
- onHistory({ changes: documentChanges, source });
3140
- } else if (filters.scope === "session") {
3141
- instanceChanges ??= this.filterChangesByScope(changes, "session");
3142
- if (!instanceChanges) continue;
3143
- onHistory({ changes: instanceChanges, source });
3144
- } else {
3145
- presenceChanges ??= this.filterChangesByScope(changes, "presence");
3146
- if (!presenceChanges) continue;
3147
- onHistory({ changes: presenceChanges, source });
3171
+ if (!scopedChanges.has(filters.scope)) {
3172
+ scopedChanges.set(filters.scope, this.filterChangesByScope(changes, filters.scope));
3148
3173
  }
3149
- } else {
3150
- onHistory({ changes, source });
3174
+ const filtered = scopedChanges.get(filters.scope);
3175
+ if (!filtered) continue;
3176
+ listenerChanges = filtered;
3177
+ }
3178
+ try {
3179
+ onHistory({ changes: listenerChanges, source });
3180
+ } catch (error) {
3181
+ errors.push(error);
3151
3182
  }
3152
3183
  }
3153
3184
  }
3185
+ if (errors.length > 0) throw errors[0];
3154
3186
  }
3155
3187
  }
3156
3188
  dispose() {
3157
- this.cancelHistoryReactor();
3189
+ try {
3190
+ this._flushHistory();
3191
+ } finally {
3192
+ this.cancelHistoryReactor?.();
3193
+ }
3158
3194
  }
3159
3195
  /**
3160
3196
  * Filters out non-document changes from a diff. Returns null if there are no changes left.
@@ -3168,7 +3204,7 @@ var Store = class {
3168
3204
  updated: filterEntries(change.updated, (_, r) => this.scopedTypes[scope].has(r[1].typeName)),
3169
3205
  removed: filterEntries(change.removed, (_, r) => this.scopedTypes[scope].has(r.typeName))
3170
3206
  };
3171
- if (Object.keys(result.added).length === 0 && Object.keys(result.updated).length === 0 && Object.keys(result.removed).length === 0) {
3207
+ if (isRecordsDiffEmpty(result)) {
3172
3208
  return null;
3173
3209
  }
3174
3210
  return result;
@@ -3228,7 +3264,7 @@ var Store = class {
3228
3264
  initialValue
3229
3265
  );
3230
3266
  if (validated === initialValue) continue;
3231
- record = devFreeze(record);
3267
+ record = devFreeze(validated);
3232
3268
  this.records.set(record.id, record);
3233
3269
  didChange = true;
3234
3270
  updates[record.id] = [initialValue, record];
@@ -3430,7 +3466,7 @@ var Store = class {
3430
3466
  this.clear();
3431
3467
  this.put(Object.values(migrationResult.value));
3432
3468
  this.ensureStoreIsUsable();
3433
- });
3469
+ }, false);
3434
3470
  } finally {
3435
3471
  this.sideEffects.setIsEnabled(prevSideEffectsEnabled);
3436
3472
  }
@@ -3617,6 +3653,11 @@ var Store = class {
3617
3653
  if (!changed) changed = { ...existing };
3618
3654
  changed[key] = value;
3619
3655
  }
3656
+ for (const key of Object.keys(existing)) {
3657
+ if (type.ephemeralKeySet.has(key) || Object.hasOwn(to, key)) continue;
3658
+ if (!changed) changed = { ...existing };
3659
+ delete changed[key];
3660
+ }
3620
3661
  if (changed) toPut.push(changed);
3621
3662
  } else {
3622
3663
  toPut.push(to);
@@ -3722,9 +3763,8 @@ var Store = class {
3722
3763
  }
3723
3764
  if (!this.pendingAfterEvents) {
3724
3765
  this.sideEffects.handleOperationComplete(source);
3725
- } else {
3726
- source = "user";
3727
3766
  }
3767
+ source = "user";
3728
3768
  }
3729
3769
  }
3730
3770
  _isInAtomicOp = false;
@@ -3746,7 +3786,7 @@ var Store = class {
3746
3786
  }
3747
3787
  this.pendingAfterEvents = /* @__PURE__ */ new Map();
3748
3788
  const prevSideEffectsEnabled = this.sideEffects.isEnabled();
3749
- this.sideEffects.setIsEnabled(runCallbacks ?? prevSideEffectsEnabled);
3789
+ this.sideEffects.setIsEnabled(runCallbacks && prevSideEffectsEnabled);
3750
3790
  this._isInAtomicOp = true;
3751
3791
  if (isMergingRemoteChanges) {
3752
3792
  this.isMergingRemoteChanges = true;
@@ -3788,7 +3828,8 @@ function squashHistoryEntries(entries) {
3788
3828
  return devFreeze(
3789
3829
  chunked.map((chunk2) => ({
3790
3830
  source: chunk2[0].source,
3791
- changes: squashRecordDiffs(chunk2.map((e) => e.changes))
3831
+ // a single-entry chunk needs no squashing — skip the O(N) copy of its diff
3832
+ changes: chunk2.length === 1 ? chunk2[0].changes : squashRecordDiffs(chunk2.map((e) => e.changes))
3792
3833
  }))
3793
3834
  );
3794
3835
  }
@@ -4007,9 +4048,13 @@ var StoreSchema = class _StoreSchema {
4007
4048
  if (cached) {
4008
4049
  return cached;
4009
4050
  }
4051
+ const result = this.computeMigrationsSince(persistedSchema);
4052
+ this.migrationCache.set(persistedSchema, result);
4053
+ return result;
4054
+ }
4055
+ computeMigrationsSince(persistedSchema) {
4010
4056
  const upgradeResult = upgradeSchema(persistedSchema);
4011
4057
  if (!upgradeResult.ok) {
4012
- this.migrationCache.set(persistedSchema, upgradeResult);
4013
4058
  return upgradeResult;
4014
4059
  }
4015
4060
  const schema = upgradeResult.value;
@@ -4023,9 +4068,7 @@ var StoreSchema = class _StoreSchema {
4023
4068
  }
4024
4069
  }
4025
4070
  if (sequenceIdsToInclude.size === 0) {
4026
- const result2 = Result.ok([]);
4027
- this.migrationCache.set(persistedSchema, result2);
4028
- return result2;
4071
+ return Result.ok([]);
4029
4072
  }
4030
4073
  const allMigrationsToInclude = /* @__PURE__ */ new Set();
4031
4074
  for (const sequenceId of sequenceIdsToInclude) {
@@ -4039,19 +4082,13 @@ var StoreSchema = class _StoreSchema {
4039
4082
  const theirVersionId = `${sequenceId}/${theirVersion}`;
4040
4083
  const idx = this.migrations[sequenceId].sequence.findIndex((m) => m.id === theirVersionId);
4041
4084
  if (idx === -1) {
4042
- const result2 = Result.err("Incompatible schema?");
4043
- this.migrationCache.set(persistedSchema, result2);
4044
- return result2;
4085
+ return Result.err("Incompatible schema?");
4045
4086
  }
4046
4087
  for (const migration of this.migrations[sequenceId].sequence.slice(idx + 1)) {
4047
4088
  allMigrationsToInclude.add(migration.id);
4048
4089
  }
4049
4090
  }
4050
- const result = Result.ok(
4051
- this.sortedMigrations.filter(({ id }) => allMigrationsToInclude.has(id))
4052
- );
4053
- this.migrationCache.set(persistedSchema, result);
4054
- return result;
4091
+ return Result.ok(this.sortedMigrations.filter(({ id }) => allMigrationsToInclude.has(id)));
4055
4092
  }
4056
4093
  /**
4057
4094
  * Migrates a single persisted record to match the current schema version.
@@ -4177,11 +4214,15 @@ var StoreSchema = class _StoreSchema {
4177
4214
  exhaustiveSwitchError(migration);
4178
4215
  }
4179
4216
  }
4217
+ const idsToDelete = [];
4180
4218
  for (const [id, state] of storage.entries()) {
4181
4219
  if (this.getType(state.typeName).scope !== "document") {
4182
- storage.delete(id);
4220
+ idsToDelete.push(id);
4183
4221
  }
4184
4222
  }
4223
+ for (const id of idsToDelete) {
4224
+ storage.delete(id);
4225
+ }
4185
4226
  }
4186
4227
  /**
4187
4228
  * Migrates an entire store snapshot to match the current schema version.
@@ -4345,7 +4386,7 @@ var StoreSchema = class _StoreSchema {
4345
4386
  */
4346
4387
  getType(typeName) {
4347
4388
  const type = getOwnProperty(this.types, typeName);
4348
- assert(type, "record type does not exists");
4389
+ assert(type, "record type does not exist");
4349
4390
  return type;
4350
4391
  }
4351
4392
  };
@@ -4353,7 +4394,7 @@ var StoreSchema = class _StoreSchema {
4353
4394
  // src/index.ts
4354
4395
  registerDrawLibraryVersion(
4355
4396
  "@ibodr/store",
4356
- "0.2.5",
4397
+ "0.2.7",
4357
4398
  "esm"
4358
4399
  );
4359
4400
  /*!