memorio 4.9.10 → 4.9.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -135,14 +135,79 @@ observer('state.user.name', callback)
135
135
  `dispatch` is the event mechanism underneath it, if you need to hook in lower-level:
136
136
 
137
137
  ```ts
138
- memorio.dispatch.listen('state.user', event => console.debug(event.detail))
138
+ const off = memorio.dispatch.listen('state.user', event => console.debug(event.detail))
139
139
  memorio.dispatch.set('state.user', { detail: { name: 'Sara' } })
140
+
141
+ off() // remove only this subscription
142
+ memorio.dispatch.remove('state.user') // remove all subscriptions on a name
140
143
  ```
141
144
 
145
+ Multiple subscribers on the same path each keep firing — each `listen` returns
146
+ its own unsubscribe. Prefer the returned `off()` over `remove()` for per-caller
147
+ teardown, since `remove()` clears *every* listener registered under that name.
148
+
142
149
  `observer` paths are runtime strings — for compiler-checked access, see [typed state](#typed-state--schema-validation).
143
150
 
144
151
  React apps get a dedicated hook, `useObserver` — see [React integration](#react-integration).
145
152
 
153
+ ## Redux / state-manager integration
154
+
155
+ Memorio ships its Redux integration as a **separate, optional entrypoint** (`memorio/redux`). It is
156
+ *not* part of core and is **never imported by `import memorio from 'memorio'`** — so applications
157
+ that don't use Redux pay nothing. Redux is neither a dependency of Memorio nor of the adapter: the
158
+ adapter talks to any store that satisfies a tiny structural shape (`getState` / `subscribe` /
159
+ `dispatch`), so `redux`, Redux Toolkit, Zustand, NgRx, etc. are all accepted without importing the
160
+ `redux` types.
161
+
162
+ > **Ownership rule:** *Redux owns application state. Memorio owns memory.* The adapter only writes
163
+ > **selected** values (declared opt-in mappings) into Memorio memory, and only recalls them once,
164
+ > at bootstrap. It never mirrors the whole Redux tree and never two-way syncs.
165
+
166
+ ```ts
167
+ import memorio from 'memorio'
168
+ import { createMemorioReduxBridge } from 'memorio/redux'
169
+
170
+ type AppState = { user: { name: string; preferences?: { theme?: string } } }
171
+
172
+ const memorioRedux = createMemorioReduxBridge<AppState>({
173
+ // Opt-in: only these paths become durable memory.
174
+ mappings: {
175
+ 'user.preferences': {
176
+ selector: (s) => s.user.preferences,
177
+ type: 'preference',
178
+ scope: 'local', // never 'hot' for Redux mappings
179
+ tags: ['app', 'user']
180
+ },
181
+ 'user.name': (s) => s.user.name // a bare selector is also accepted
182
+ },
183
+ // Performance: only consider the action types that can mutate mapped state.
184
+ whitelist: ['user/preferencesChanged', 'user/nameChanged'],
185
+ // Diagnostics
186
+ debug: true
187
+ })
188
+
189
+ const store = createStore(reducer, applyMiddleware(memorioRedux.middleware))
190
+
191
+ // Bootstrap hydration (Memorio -> Redux, one-shot). The bridge arms a
192
+ // re-entrancy guard so the restore dispatch does not echo back to Memorio.
193
+ await memorioRedux.hydrate(store, {
194
+ onHydration: (values) => store.dispatch(restorePreferences(values))
195
+ })
196
+ ```
197
+
198
+ Directional flow, no loops:
199
+
200
+ | Direction | Mechanism | Ownership |
201
+ | --- | --- | --- |
202
+ | Redux -> Memorio | middleware persists only `mappings` whose selector changed **and** whose action type isn't blacklisted (#30) | Memorio is the durable owner (#31) |
203
+ | Memorio -> Redux | `hydrate()` recalls once at bootstrap; the app dispatches its own restore actions | Redux owns runtime state |
204
+
205
+ - **No store mirroring:** only mapped selectors are persisted — never `store.getState()` wholesale.
206
+ - **No synchronization loops:** hydration arms `setHydrating(true)` + seeds the change-detection cache, so the restore dispatch is seen as "no change" and is not re-persisted.
207
+ - **Async is off the reducer path:** middleware defers `memorio.memory.remember` to a microtask and coerces a burst of actions into a single flush.
208
+ - **Failure-isolated:** Memorio errors are swallowed by default (`failSoft: true`, default) and routed to `onError`; Redux dispatch always completes normally.
209
+ - **Production-safe:** nothing in the adapter exposes Memorio's database on `globalThis` / DevTools.
210
+
146
211
  ## The layers
147
212
 
148
213
  ### `store` — persistent key/value
package/examples/basic.ts CHANGED
@@ -81,7 +81,7 @@ console.debug('Username:', store.get('username'))
81
81
  console.debug('Preferences:', store.get('preferences'))
82
82
 
83
83
  // Get storage size
84
- console.debug('Storage size:', store.size(), 'bytes')
84
+ console.debug('Storage size:', store.size(), 'kilobytes')
85
85
 
86
86
  // ============================================
87
87
  // SESSION - Temporary session storage
@@ -73,7 +73,7 @@ console.debug('Cart total:', total)
73
73
  // ============================================
74
74
 
75
75
  // Get session storage size
76
- console.debug('Session size:', session.size(), 'bytes')
76
+ console.debug('Session size:', session.size(), 'kilobytes')
77
77
 
78
78
  // ============================================
79
79
  // CLEANUP
@@ -51,7 +51,7 @@ if (savedPrefs) {
51
51
 
52
52
  // Get storage size
53
53
  const currentSize = store.size()
54
- console.debug('Current storage size:', currentSize, 'bytes')
54
+ console.debug('Current storage size:', currentSize, 'kilobytes')
55
55
 
56
56
  // ============================================
57
57
  // ALIAS METHODS
package/index.cjs CHANGED
@@ -645,24 +645,29 @@ var init_store = __esm({
645
645
  }, "value")
646
646
  },
647
647
  /**
648
- * Calculates total size of all memorio items (characters).
648
+ * Calculates total size of all memorio items in kilobytes.
649
+ * Counts each prefixed key and its JSON-serialized value, summing their
650
+ * byte lengths and converting to whole kilobytes (Math.ceil). Replaces
651
+ * the previous character-only total which did not express real usage.
652
+ * @returns Total size in kilobytes (0 when empty / no memorio items).
649
653
  */
650
654
  size: {
651
655
  value: /* @__PURE__ */ __name(() => {
652
- let totalSize = 0;
656
+ let totalBytes = 0;
653
657
  if (_hasStorage) {
654
- Object.keys(_localStorage).forEach((key) => {
655
- if (key.startsWith(_getPrefix())) {
658
+ for (let i = 0; i < _localStorage.length; i++) {
659
+ const key = _localStorage.key(i);
660
+ if (key && key.startsWith(_getPrefix())) {
656
661
  const item = _localStorage.getItem(key);
657
- if (item) totalSize += item.length;
662
+ if (item) totalBytes += key.length + item.length;
658
663
  }
659
- });
664
+ }
660
665
  } else {
661
- _getFallbackStorage().forEach((value) => {
662
- totalSize += value.length;
666
+ _getFallbackStorage().forEach((value, key) => {
667
+ totalBytes += key.length + value.length;
663
668
  });
664
669
  }
665
- return totalSize;
670
+ return totalBytes === 0 ? 0 : Math.ceil(totalBytes / 1024);
666
671
  }, "value")
667
672
  },
668
673
  /**
@@ -1733,30 +1738,51 @@ var dispatch = {
1733
1738
  }, "set"),
1734
1739
  ///
1735
1740
  /**
1736
- * Listens for the specified event names and executes the callback when the event is triggered.
1741
+ * Subscribes a callback to an event name. Maintains a *list* of handlers per
1742
+ * name, so multiple subscribers on the same path (e.g. several
1743
+ * `useObserver` calls observing `state.report`) each keep firing and each
1744
+ * get an independent unsubscribe. The previous single-slot-per-name
1745
+ * behaviour silently dropped the second subscriber and let any unsubscribe
1746
+ * cancel every other subscriber on that path.
1737
1747
  * @param name The name of the event to listen for.
1738
1748
  * @param cb The callback function to execute when the event is triggered.
1749
+ * @returns An unsubscribe function for this subscriber, or void if no callback.
1739
1750
  */
1740
1751
  listen: /* @__PURE__ */ __name((name, cb = null, _flag) => {
1741
- if (globalThis.events?.[name]) return;
1742
- const exec = cb ? (e) => {
1752
+ if (!cb) return;
1753
+ const wrapped = /* @__PURE__ */ __name((e) => {
1743
1754
  const target = e;
1744
- return globalThis.queueMicrotask ? globalThis.queueMicrotask(() => cb(target)) : Promise.resolve().then(() => cb(target));
1745
- } : void 0;
1746
- if (exec) globalThis.addEventListener(name, exec);
1747
- if (exec) globalThis.events[name] = exec;
1755
+ if (globalThis.queueMicrotask) {
1756
+ globalThis.queueMicrotask(() => cb(target));
1757
+ } else {
1758
+ Promise.resolve().then(() => cb(target));
1759
+ }
1760
+ }, "wrapped");
1761
+ globalThis.addEventListener(name, wrapped);
1762
+ const list = globalThis.events[name] = globalThis.events[name] ?? [];
1763
+ list.push(wrapped);
1764
+ return () => {
1765
+ globalThis.removeEventListener(name, wrapped);
1766
+ const idx = list.indexOf(wrapped);
1767
+ if (idx > -1) list.splice(idx, 1);
1768
+ if (!list.length) delete globalThis.events[name];
1769
+ };
1748
1770
  }, "listen"),
1749
1771
  ///
1750
1772
  /**
1751
- * Removes the event listener for the specified event names.
1752
- * @param name The name of the event to remove the listener for.
1773
+ * Removes ALL handlers for the specified event name (imperative bulk
1774
+ * removal). For per-subscriber teardown, use the unsubscribe function
1775
+ * returned by `listen`.
1776
+ * @param name The name of the event to remove the listeners for.
1753
1777
  */
1754
1778
  remove: /* @__PURE__ */ __name((name) => {
1755
- const listener = globalThis.events?.[name];
1756
- if (listener) {
1757
- globalThis.removeEventListener(name, listener);
1758
- delete globalThis.events[name];
1779
+ const entry = globalThis.events?.[name];
1780
+ if (Array.isArray(entry)) {
1781
+ entry.forEach((h) => globalThis.removeEventListener(name, h));
1782
+ } else if (typeof entry === "function") {
1783
+ globalThis.removeEventListener(name, entry);
1759
1784
  }
1785
+ delete globalThis.events[name];
1760
1786
  }, "remove")
1761
1787
  };
1762
1788
 
@@ -2285,27 +2311,28 @@ if (globalThis[SESSION_INSTANCE_KEY]) {
2285
2311
  }, "value")
2286
2312
  },
2287
2313
  /**
2288
- * Gets total size of all sessionStorage items.
2289
- * @returns Total size in characters.
2314
+ * Calculates total size of all memorio session items in kilobytes.
2315
+ * Counts each prefixed key and its JSON-serialized value, summing their
2316
+ * byte lengths and converting to whole kilobytes (Math.ceil).
2317
+ * @returns Total size in kilobytes (0 when empty / no memorio items).
2290
2318
  */
2291
2319
  size: {
2292
2320
  value: /* @__PURE__ */ __name(() => {
2293
- let totalSize = 0;
2321
+ let totalBytes = 0;
2294
2322
  if (_hasStorage2) {
2295
- Object.keys(_sessionStorage).forEach(
2296
- (key) => {
2297
- if (key.startsWith(_getPrefix2())) {
2298
- const item = _sessionStorage.getItem(key);
2299
- if (item) totalSize += item.length;
2300
- }
2323
+ for (let i = 0; i < _sessionStorage.length; i++) {
2324
+ const key = _sessionStorage.key(i);
2325
+ if (key && key.startsWith(_getPrefix2())) {
2326
+ const item = _sessionStorage.getItem(key);
2327
+ if (item) totalBytes += key.length + item.length;
2301
2328
  }
2302
- );
2329
+ }
2303
2330
  } else {
2304
- _getFallbackStorage2().forEach((value) => {
2305
- totalSize += value.length;
2331
+ _getFallbackStorage2().forEach((value, key) => {
2332
+ totalBytes += key.length + value.length;
2306
2333
  });
2307
2334
  }
2308
- return totalSize;
2335
+ return totalBytes === 0 ? 0 : Math.ceil(totalBytes / 1024);
2309
2336
  }, "value")
2310
2337
  },
2311
2338
  /**
@@ -2524,6 +2551,7 @@ var useObserver = /* @__PURE__ */ __name(((func, deps = []) => {
2524
2551
  };
2525
2552
  }
2526
2553
  const handlers = [];
2554
+ const unsubs = [];
2527
2555
  const depList = Array.isArray(deps) && !deps.__path ? deps : [deps];
2528
2556
  try {
2529
2557
  depList.forEach((dep) => {
@@ -2537,8 +2565,11 @@ var useObserver = /* @__PURE__ */ __name(((func, deps = []) => {
2537
2565
  if (t) {
2538
2566
  t = t.replace(/^(\(\)\s*=>\s*)/, "").replace(/^globalThis\./, "").replace(/^state\./, "");
2539
2567
  const path = t.startsWith("store.") || t.startsWith("state.") ? t : "state." + t;
2540
- dispatch.listen(path, () => func());
2541
- handlers.push(path);
2568
+ if (!handlers.includes(path)) {
2569
+ handlers.push(path);
2570
+ const off = dispatch.listen(path, () => func());
2571
+ if (typeof off === "function") unsubs.push({ path, off });
2572
+ }
2542
2573
  } else {
2543
2574
  exports.internal.clearTrackedPaths();
2544
2575
  exports.internal.tracking = true;
@@ -2550,8 +2581,9 @@ var useObserver = /* @__PURE__ */ __name(((func, deps = []) => {
2550
2581
  exports.internal.trackedPaths.forEach((path) => {
2551
2582
  if (!handlers.includes(path)) {
2552
2583
  message_default(`useObserver: [Auto-Discovery] -> "${path}"`);
2553
- dispatch.listen(path, () => func());
2554
2584
  handlers.push(path);
2585
+ const off = dispatch.listen(path, () => func());
2586
+ if (typeof off === "function") unsubs.push({ path, off });
2555
2587
  }
2556
2588
  });
2557
2589
  }
@@ -2562,11 +2594,9 @@ var useObserver = /* @__PURE__ */ __name(((func, deps = []) => {
2562
2594
  console.warn(`useObserver: No paths resolved for monitoring. Check your dependencies!`);
2563
2595
  }
2564
2596
  return () => {
2565
- handlers.forEach((path) => {
2566
- if (typeof dispatch.remove === "function") {
2567
- dispatch.remove(path);
2568
- message_default(`useObserver: Stopped monitoring "${path}"`);
2569
- }
2597
+ unsubs.forEach(({ path, off }) => {
2598
+ off();
2599
+ message_default(`useObserver: Stopped monitoring "${path}"`);
2570
2600
  });
2571
2601
  };
2572
2602
  } catch (err) {
@@ -3273,7 +3303,7 @@ init_internal();
3273
3303
 
3274
3304
  // package.json
3275
3305
  var package_default = {
3276
- version: "4.9.10"};
3306
+ version: "4.9.30"};
3277
3307
 
3278
3308
  // core/global.ts
3279
3309
  init_env();
package/index.js CHANGED
@@ -642,24 +642,29 @@ var init_store = __esm({
642
642
  }, "value")
643
643
  },
644
644
  /**
645
- * Calculates total size of all memorio items (characters).
645
+ * Calculates total size of all memorio items in kilobytes.
646
+ * Counts each prefixed key and its JSON-serialized value, summing their
647
+ * byte lengths and converting to whole kilobytes (Math.ceil). Replaces
648
+ * the previous character-only total which did not express real usage.
649
+ * @returns Total size in kilobytes (0 when empty / no memorio items).
646
650
  */
647
651
  size: {
648
652
  value: /* @__PURE__ */ __name(() => {
649
- let totalSize = 0;
653
+ let totalBytes = 0;
650
654
  if (_hasStorage) {
651
- Object.keys(_localStorage).forEach((key) => {
652
- if (key.startsWith(_getPrefix())) {
655
+ for (let i = 0; i < _localStorage.length; i++) {
656
+ const key = _localStorage.key(i);
657
+ if (key && key.startsWith(_getPrefix())) {
653
658
  const item = _localStorage.getItem(key);
654
- if (item) totalSize += item.length;
659
+ if (item) totalBytes += key.length + item.length;
655
660
  }
656
- });
661
+ }
657
662
  } else {
658
- _getFallbackStorage().forEach((value) => {
659
- totalSize += value.length;
663
+ _getFallbackStorage().forEach((value, key) => {
664
+ totalBytes += key.length + value.length;
660
665
  });
661
666
  }
662
- return totalSize;
667
+ return totalBytes === 0 ? 0 : Math.ceil(totalBytes / 1024);
663
668
  }, "value")
664
669
  },
665
670
  /**
@@ -1730,30 +1735,51 @@ var dispatch = {
1730
1735
  }, "set"),
1731
1736
  ///
1732
1737
  /**
1733
- * Listens for the specified event names and executes the callback when the event is triggered.
1738
+ * Subscribes a callback to an event name. Maintains a *list* of handlers per
1739
+ * name, so multiple subscribers on the same path (e.g. several
1740
+ * `useObserver` calls observing `state.report`) each keep firing and each
1741
+ * get an independent unsubscribe. The previous single-slot-per-name
1742
+ * behaviour silently dropped the second subscriber and let any unsubscribe
1743
+ * cancel every other subscriber on that path.
1734
1744
  * @param name The name of the event to listen for.
1735
1745
  * @param cb The callback function to execute when the event is triggered.
1746
+ * @returns An unsubscribe function for this subscriber, or void if no callback.
1736
1747
  */
1737
1748
  listen: /* @__PURE__ */ __name((name, cb = null, _flag) => {
1738
- if (globalThis.events?.[name]) return;
1739
- const exec = cb ? (e) => {
1749
+ if (!cb) return;
1750
+ const wrapped = /* @__PURE__ */ __name((e) => {
1740
1751
  const target = e;
1741
- return globalThis.queueMicrotask ? globalThis.queueMicrotask(() => cb(target)) : Promise.resolve().then(() => cb(target));
1742
- } : void 0;
1743
- if (exec) globalThis.addEventListener(name, exec);
1744
- if (exec) globalThis.events[name] = exec;
1752
+ if (globalThis.queueMicrotask) {
1753
+ globalThis.queueMicrotask(() => cb(target));
1754
+ } else {
1755
+ Promise.resolve().then(() => cb(target));
1756
+ }
1757
+ }, "wrapped");
1758
+ globalThis.addEventListener(name, wrapped);
1759
+ const list = globalThis.events[name] = globalThis.events[name] ?? [];
1760
+ list.push(wrapped);
1761
+ return () => {
1762
+ globalThis.removeEventListener(name, wrapped);
1763
+ const idx = list.indexOf(wrapped);
1764
+ if (idx > -1) list.splice(idx, 1);
1765
+ if (!list.length) delete globalThis.events[name];
1766
+ };
1745
1767
  }, "listen"),
1746
1768
  ///
1747
1769
  /**
1748
- * Removes the event listener for the specified event names.
1749
- * @param name The name of the event to remove the listener for.
1770
+ * Removes ALL handlers for the specified event name (imperative bulk
1771
+ * removal). For per-subscriber teardown, use the unsubscribe function
1772
+ * returned by `listen`.
1773
+ * @param name The name of the event to remove the listeners for.
1750
1774
  */
1751
1775
  remove: /* @__PURE__ */ __name((name) => {
1752
- const listener = globalThis.events?.[name];
1753
- if (listener) {
1754
- globalThis.removeEventListener(name, listener);
1755
- delete globalThis.events[name];
1776
+ const entry = globalThis.events?.[name];
1777
+ if (Array.isArray(entry)) {
1778
+ entry.forEach((h) => globalThis.removeEventListener(name, h));
1779
+ } else if (typeof entry === "function") {
1780
+ globalThis.removeEventListener(name, entry);
1756
1781
  }
1782
+ delete globalThis.events[name];
1757
1783
  }, "remove")
1758
1784
  };
1759
1785
 
@@ -2282,27 +2308,28 @@ if (globalThis[SESSION_INSTANCE_KEY]) {
2282
2308
  }, "value")
2283
2309
  },
2284
2310
  /**
2285
- * Gets total size of all sessionStorage items.
2286
- * @returns Total size in characters.
2311
+ * Calculates total size of all memorio session items in kilobytes.
2312
+ * Counts each prefixed key and its JSON-serialized value, summing their
2313
+ * byte lengths and converting to whole kilobytes (Math.ceil).
2314
+ * @returns Total size in kilobytes (0 when empty / no memorio items).
2287
2315
  */
2288
2316
  size: {
2289
2317
  value: /* @__PURE__ */ __name(() => {
2290
- let totalSize = 0;
2318
+ let totalBytes = 0;
2291
2319
  if (_hasStorage2) {
2292
- Object.keys(_sessionStorage).forEach(
2293
- (key) => {
2294
- if (key.startsWith(_getPrefix2())) {
2295
- const item = _sessionStorage.getItem(key);
2296
- if (item) totalSize += item.length;
2297
- }
2320
+ for (let i = 0; i < _sessionStorage.length; i++) {
2321
+ const key = _sessionStorage.key(i);
2322
+ if (key && key.startsWith(_getPrefix2())) {
2323
+ const item = _sessionStorage.getItem(key);
2324
+ if (item) totalBytes += key.length + item.length;
2298
2325
  }
2299
- );
2326
+ }
2300
2327
  } else {
2301
- _getFallbackStorage2().forEach((value) => {
2302
- totalSize += value.length;
2328
+ _getFallbackStorage2().forEach((value, key) => {
2329
+ totalBytes += key.length + value.length;
2303
2330
  });
2304
2331
  }
2305
- return totalSize;
2332
+ return totalBytes === 0 ? 0 : Math.ceil(totalBytes / 1024);
2306
2333
  }, "value")
2307
2334
  },
2308
2335
  /**
@@ -2521,6 +2548,7 @@ var useObserver = /* @__PURE__ */ __name(((func, deps = []) => {
2521
2548
  };
2522
2549
  }
2523
2550
  const handlers = [];
2551
+ const unsubs = [];
2524
2552
  const depList = Array.isArray(deps) && !deps.__path ? deps : [deps];
2525
2553
  try {
2526
2554
  depList.forEach((dep) => {
@@ -2534,8 +2562,11 @@ var useObserver = /* @__PURE__ */ __name(((func, deps = []) => {
2534
2562
  if (t) {
2535
2563
  t = t.replace(/^(\(\)\s*=>\s*)/, "").replace(/^globalThis\./, "").replace(/^state\./, "");
2536
2564
  const path2 = t.startsWith("store.") || t.startsWith("state.") ? t : "state." + t;
2537
- dispatch.listen(path2, () => func());
2538
- handlers.push(path2);
2565
+ if (!handlers.includes(path2)) {
2566
+ handlers.push(path2);
2567
+ const off = dispatch.listen(path2, () => func());
2568
+ if (typeof off === "function") unsubs.push({ path: path2, off });
2569
+ }
2539
2570
  } else {
2540
2571
  internal_default.clearTrackedPaths();
2541
2572
  internal_default.tracking = true;
@@ -2547,8 +2578,9 @@ var useObserver = /* @__PURE__ */ __name(((func, deps = []) => {
2547
2578
  internal_default.trackedPaths.forEach((path2) => {
2548
2579
  if (!handlers.includes(path2)) {
2549
2580
  message_default(`useObserver: [Auto-Discovery] -> "${path2}"`);
2550
- dispatch.listen(path2, () => func());
2551
2581
  handlers.push(path2);
2582
+ const off = dispatch.listen(path2, () => func());
2583
+ if (typeof off === "function") unsubs.push({ path: path2, off });
2552
2584
  }
2553
2585
  });
2554
2586
  }
@@ -2559,11 +2591,9 @@ var useObserver = /* @__PURE__ */ __name(((func, deps = []) => {
2559
2591
  console.warn(`useObserver: No paths resolved for monitoring. Check your dependencies!`);
2560
2592
  }
2561
2593
  return () => {
2562
- handlers.forEach((path2) => {
2563
- if (typeof dispatch.remove === "function") {
2564
- dispatch.remove(path2);
2565
- message_default(`useObserver: Stopped monitoring "${path2}"`);
2566
- }
2594
+ unsubs.forEach(({ path: path2, off }) => {
2595
+ off();
2596
+ message_default(`useObserver: Stopped monitoring "${path2}"`);
2567
2597
  });
2568
2598
  };
2569
2599
  } catch (err) {
@@ -3270,7 +3300,7 @@ init_internal();
3270
3300
 
3271
3301
  // package.json
3272
3302
  var package_default = {
3273
- version: "4.9.10"};
3303
+ version: "4.9.30"};
3274
3304
 
3275
3305
  // core/global.ts
3276
3306
  init_env();
package/llms.txt CHANGED
@@ -100,7 +100,7 @@ store.removeAll()
100
100
  store.clearAll() // alias
101
101
 
102
102
  // Check size
103
- console.debug(store.size(), 'chars stored')
103
+ console.debug(store.size(), 'kilobytes')
104
104
 
105
105
  // Check if persistent (real localStorage vs memory fallback)
106
106
  console.debug(store.isPersistent) // true → real localStorage
@@ -134,7 +134,7 @@ session.removeAll()
134
134
  session.clearAll() // alias
135
135
 
136
136
  // Check size
137
- console.debug(session.size(), 'chars stored')
137
+ console.debug(session.size(), 'kilobytes')
138
138
 
139
139
  // Check if persistent
140
140
  console.debug(session.isPersistent)
@@ -466,15 +466,23 @@ MIT © Dario Passariello (BigLogic Inc Canada)
466
466
 
467
467
  Internal event system used by state changes; also usable directly for custom events.
468
468
 
469
+ Listeners are stored as a *list* per event name, so multiple subscribers on the
470
+ same name each keep firing. `dispatch.listen` returns an unsubscribe function
471
+ for that specific subscriber — call it to remove only that one listener without
472
+ affecting others registered under the same name.
473
+
469
474
  ```javascript
470
475
  // Dispatch a custom event
471
476
  memorio.dispatch.set('custom:event', { detail: { data: 'value' } })
472
477
 
473
- // Listen for an event
474
- memorio.dispatch.listen('custom:event', (e) => {
478
+ // Listen for an event (returns an unsubscribe function)
479
+ const off = memorio.dispatch.listen('custom:event', (e) => {
475
480
  console.debug('Event triggered:', e.detail)
476
481
  })
477
482
 
478
- // Remove listener
483
+ // Remove only this listener via the returned unsubscribe
484
+ off()
485
+
486
+ // Remove *all* listeners for a name (imperative bulk removal)
479
487
  memorio.dispatch.remove('custom:event')
480
488
  ```