@pyreon/reactivity 0.14.0 → 0.16.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/README.md CHANGED
@@ -56,7 +56,8 @@ batch(() => {
56
56
  ### Scopes
57
57
 
58
58
  - **`effectScope(): EffectScope`** -- Creates a scope that collects effects for bulk disposal. Internal arrays (`_effects`, `_updateHooks`) are lazy-allocated on first use -- scopes with no effects cost only the object itself.
59
- - **`getCurrentScope(): EffectScope | undefined`** -- Returns the active effect scope.
59
+ - **`getCurrentScope(): EffectScope | null`** -- Returns the active effect scope, or `null` if none.
60
+ - **`onScopeDispose(fn)`** -- Register a callback to run when the current scope stops (Vue 3 parity).
60
61
  - **`setCurrentScope(scope)`** -- Manually sets the current effect scope.
61
62
 
62
63
  ### Selectors and Resources
@@ -69,6 +70,8 @@ batch(() => {
69
70
  - **`createStore(initial)`** -- Creates a deeply reactive store object.
70
71
  - **`isStore(value): boolean`** -- Checks whether a value is a reactive store.
71
72
  - **`reconcile(target, source)`** -- Efficiently patches a store to match a new value.
73
+ - **`shallowReactive<T>(initial): T`** -- Creates a SHALLOWLY reactive store: top-level property writes notify, but nested object mutations don't (Vue 3 parity). Use for large object graphs where deep proxying would be wasteful.
74
+ - **`markRaw<T>(value): T`** -- Mark an object as RAW so `createStore` and `shallowReactive` return it unwrapped (Vue 3 parity). Useful for class instances, third-party objects, DOM nodes, or any shape that shouldn't be deeply proxied. Marking is one-way (no `unmarkRaw`); mark BEFORE the object enters a store.
72
75
 
73
76
  ## License
74
77
 
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"1ddc8440-1","name":"batch.ts"},{"uid":"1ddc8440-3","name":"cell.ts"},{"uid":"1ddc8440-5","name":"scope.ts"},{"uid":"1ddc8440-7","name":"tracking.ts"},{"uid":"1ddc8440-9","name":"effect.ts"},{"uid":"1ddc8440-11","name":"computed.ts"},{"uid":"1ddc8440-13","name":"createSelector.ts"},{"uid":"1ddc8440-15","name":"debug.ts"},{"uid":"1ddc8440-17","name":"signal.ts"},{"uid":"1ddc8440-19","name":"store.ts"},{"uid":"1ddc8440-21","name":"reconcile.ts"},{"uid":"1ddc8440-23","name":"resource.ts"},{"uid":"1ddc8440-25","name":"watch.ts"},{"uid":"1ddc8440-27","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"1ddc8440-1":{"renderedLength":1079,"gzipLength":520,"brotliLength":0,"metaUid":"1ddc8440-0"},"1ddc8440-3":{"renderedLength":1636,"gzipLength":786,"brotliLength":0,"metaUid":"1ddc8440-2"},"1ddc8440-5":{"renderedLength":1977,"gzipLength":786,"brotliLength":0,"metaUid":"1ddc8440-4"},"1ddc8440-7":{"renderedLength":2227,"gzipLength":858,"brotliLength":0,"metaUid":"1ddc8440-6"},"1ddc8440-9":{"renderedLength":5389,"gzipLength":1826,"brotliLength":0,"metaUid":"1ddc8440-8"},"1ddc8440-11":{"renderedLength":4068,"gzipLength":1253,"brotliLength":0,"metaUid":"1ddc8440-10"},"1ddc8440-13":{"renderedLength":1810,"gzipLength":833,"brotliLength":0,"metaUid":"1ddc8440-12"},"1ddc8440-15":{"renderedLength":2469,"gzipLength":1092,"brotliLength":0,"metaUid":"1ddc8440-14"},"1ddc8440-17":{"renderedLength":2671,"gzipLength":1169,"brotliLength":0,"metaUid":"1ddc8440-16"},"1ddc8440-19":{"renderedLength":2879,"gzipLength":1056,"brotliLength":0,"metaUid":"1ddc8440-18"},"1ddc8440-21":{"renderedLength":2109,"gzipLength":867,"brotliLength":0,"metaUid":"1ddc8440-20"},"1ddc8440-23":{"renderedLength":1029,"gzipLength":475,"brotliLength":0,"metaUid":"1ddc8440-22"},"1ddc8440-25":{"renderedLength":1249,"gzipLength":582,"brotliLength":0,"metaUid":"1ddc8440-24"},"1ddc8440-27":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"1ddc8440-26"}},"nodeMetas":{"1ddc8440-0":{"id":"/src/batch.ts","moduleParts":{"index.js":"1ddc8440-1"},"imported":[],"importedBy":[{"uid":"1ddc8440-26"},{"uid":"1ddc8440-16"},{"uid":"1ddc8440-6"}]},"1ddc8440-2":{"id":"/src/cell.ts","moduleParts":{"index.js":"1ddc8440-3"},"imported":[],"importedBy":[{"uid":"1ddc8440-26"}]},"1ddc8440-4":{"id":"/src/scope.ts","moduleParts":{"index.js":"1ddc8440-5"},"imported":[],"importedBy":[{"uid":"1ddc8440-26"},{"uid":"1ddc8440-10"},{"uid":"1ddc8440-8"}]},"1ddc8440-6":{"id":"/src/tracking.ts","moduleParts":{"index.js":"1ddc8440-7"},"imported":[{"uid":"1ddc8440-0"}],"importedBy":[{"uid":"1ddc8440-26"},{"uid":"1ddc8440-10"},{"uid":"1ddc8440-12"},{"uid":"1ddc8440-8"},{"uid":"1ddc8440-22"},{"uid":"1ddc8440-16"}]},"1ddc8440-8":{"id":"/src/effect.ts","moduleParts":{"index.js":"1ddc8440-9"},"imported":[{"uid":"1ddc8440-4"},{"uid":"1ddc8440-6"}],"importedBy":[{"uid":"1ddc8440-26"},{"uid":"1ddc8440-10"},{"uid":"1ddc8440-12"},{"uid":"1ddc8440-22"},{"uid":"1ddc8440-24"}]},"1ddc8440-10":{"id":"/src/computed.ts","moduleParts":{"index.js":"1ddc8440-11"},"imported":[{"uid":"1ddc8440-8"},{"uid":"1ddc8440-4"},{"uid":"1ddc8440-6"}],"importedBy":[{"uid":"1ddc8440-26"}]},"1ddc8440-12":{"id":"/src/createSelector.ts","moduleParts":{"index.js":"1ddc8440-13"},"imported":[{"uid":"1ddc8440-8"},{"uid":"1ddc8440-6"}],"importedBy":[{"uid":"1ddc8440-26"}]},"1ddc8440-14":{"id":"/src/debug.ts","moduleParts":{"index.js":"1ddc8440-15"},"imported":[],"importedBy":[{"uid":"1ddc8440-26"},{"uid":"1ddc8440-16"}]},"1ddc8440-16":{"id":"/src/signal.ts","moduleParts":{"index.js":"1ddc8440-17"},"imported":[{"uid":"1ddc8440-0"},{"uid":"1ddc8440-14"},{"uid":"1ddc8440-6"}],"importedBy":[{"uid":"1ddc8440-26"},{"uid":"1ddc8440-22"},{"uid":"1ddc8440-18"}]},"1ddc8440-18":{"id":"/src/store.ts","moduleParts":{"index.js":"1ddc8440-19"},"imported":[{"uid":"1ddc8440-16"}],"importedBy":[{"uid":"1ddc8440-26"},{"uid":"1ddc8440-20"}]},"1ddc8440-20":{"id":"/src/reconcile.ts","moduleParts":{"index.js":"1ddc8440-21"},"imported":[{"uid":"1ddc8440-18"}],"importedBy":[{"uid":"1ddc8440-26"}]},"1ddc8440-22":{"id":"/src/resource.ts","moduleParts":{"index.js":"1ddc8440-23"},"imported":[{"uid":"1ddc8440-8"},{"uid":"1ddc8440-16"},{"uid":"1ddc8440-6"}],"importedBy":[{"uid":"1ddc8440-26"}]},"1ddc8440-24":{"id":"/src/watch.ts","moduleParts":{"index.js":"1ddc8440-25"},"imported":[{"uid":"1ddc8440-8"}],"importedBy":[{"uid":"1ddc8440-26"}]},"1ddc8440-26":{"id":"/src/index.ts","moduleParts":{"index.js":"1ddc8440-27"},"imported":[{"uid":"1ddc8440-0"},{"uid":"1ddc8440-2"},{"uid":"1ddc8440-10"},{"uid":"1ddc8440-12"},{"uid":"1ddc8440-14"},{"uid":"1ddc8440-8"},{"uid":"1ddc8440-20"},{"uid":"1ddc8440-22"},{"uid":"1ddc8440-4"},{"uid":"1ddc8440-16"},{"uid":"1ddc8440-18"},{"uid":"1ddc8440-6"},{"uid":"1ddc8440-24"}],"importedBy":[],"isEntry":true}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"a1cf068b-1","name":"batch.ts"},{"uid":"a1cf068b-3","name":"cell.ts"},{"uid":"a1cf068b-5","name":"scope.ts"},{"uid":"a1cf068b-7","name":"tracking.ts"},{"uid":"a1cf068b-9","name":"effect.ts"},{"uid":"a1cf068b-11","name":"computed.ts"},{"uid":"a1cf068b-13","name":"createSelector.ts"},{"uid":"a1cf068b-15","name":"debug.ts"},{"uid":"a1cf068b-17","name":"signal.ts"},{"uid":"a1cf068b-19","name":"store.ts"},{"uid":"a1cf068b-21","name":"reconcile.ts"},{"uid":"a1cf068b-23","name":"resource.ts"},{"uid":"a1cf068b-25","name":"watch.ts"},{"uid":"a1cf068b-27","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"a1cf068b-1":{"renderedLength":3016,"gzipLength":1167,"brotliLength":0,"metaUid":"a1cf068b-0"},"a1cf068b-3":{"renderedLength":1636,"gzipLength":786,"brotliLength":0,"metaUid":"a1cf068b-2"},"a1cf068b-5":{"renderedLength":3026,"gzipLength":1226,"brotliLength":0,"metaUid":"a1cf068b-4"},"a1cf068b-7":{"renderedLength":2227,"gzipLength":858,"brotliLength":0,"metaUid":"a1cf068b-6"},"a1cf068b-9":{"renderedLength":7391,"gzipLength":2397,"brotliLength":0,"metaUid":"a1cf068b-8"},"a1cf068b-11":{"renderedLength":4548,"gzipLength":1464,"brotliLength":0,"metaUid":"a1cf068b-10"},"a1cf068b-13":{"renderedLength":2244,"gzipLength":981,"brotliLength":0,"metaUid":"a1cf068b-12"},"a1cf068b-15":{"renderedLength":2469,"gzipLength":1092,"brotliLength":0,"metaUid":"a1cf068b-14"},"a1cf068b-17":{"renderedLength":2818,"gzipLength":1191,"brotliLength":0,"metaUid":"a1cf068b-16"},"a1cf068b-19":{"renderedLength":5143,"gzipLength":1835,"brotliLength":0,"metaUid":"a1cf068b-18"},"a1cf068b-21":{"renderedLength":2109,"gzipLength":867,"brotliLength":0,"metaUid":"a1cf068b-20"},"a1cf068b-23":{"renderedLength":1205,"gzipLength":524,"brotliLength":0,"metaUid":"a1cf068b-22"},"a1cf068b-25":{"renderedLength":1249,"gzipLength":582,"brotliLength":0,"metaUid":"a1cf068b-24"},"a1cf068b-27":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"a1cf068b-26"}},"nodeMetas":{"a1cf068b-0":{"id":"/src/batch.ts","moduleParts":{"index.js":"a1cf068b-1"},"imported":[],"importedBy":[{"uid":"a1cf068b-26"},{"uid":"a1cf068b-10"},{"uid":"a1cf068b-16"},{"uid":"a1cf068b-6"}]},"a1cf068b-2":{"id":"/src/cell.ts","moduleParts":{"index.js":"a1cf068b-3"},"imported":[],"importedBy":[{"uid":"a1cf068b-26"}]},"a1cf068b-4":{"id":"/src/scope.ts","moduleParts":{"index.js":"a1cf068b-5"},"imported":[],"importedBy":[{"uid":"a1cf068b-26"},{"uid":"a1cf068b-10"},{"uid":"a1cf068b-8"}]},"a1cf068b-6":{"id":"/src/tracking.ts","moduleParts":{"index.js":"a1cf068b-7"},"imported":[{"uid":"a1cf068b-0"}],"importedBy":[{"uid":"a1cf068b-26"},{"uid":"a1cf068b-10"},{"uid":"a1cf068b-12"},{"uid":"a1cf068b-8"},{"uid":"a1cf068b-22"},{"uid":"a1cf068b-16"}]},"a1cf068b-8":{"id":"/src/effect.ts","moduleParts":{"index.js":"a1cf068b-9"},"imported":[{"uid":"a1cf068b-4"},{"uid":"a1cf068b-6"}],"importedBy":[{"uid":"a1cf068b-26"},{"uid":"a1cf068b-10"},{"uid":"a1cf068b-12"},{"uid":"a1cf068b-22"},{"uid":"a1cf068b-24"}]},"a1cf068b-10":{"id":"/src/computed.ts","moduleParts":{"index.js":"a1cf068b-11"},"imported":[{"uid":"a1cf068b-0"},{"uid":"a1cf068b-8"},{"uid":"a1cf068b-4"},{"uid":"a1cf068b-6"}],"importedBy":[{"uid":"a1cf068b-26"}]},"a1cf068b-12":{"id":"/src/createSelector.ts","moduleParts":{"index.js":"a1cf068b-13"},"imported":[{"uid":"a1cf068b-8"},{"uid":"a1cf068b-6"}],"importedBy":[{"uid":"a1cf068b-26"}]},"a1cf068b-14":{"id":"/src/debug.ts","moduleParts":{"index.js":"a1cf068b-15"},"imported":[],"importedBy":[{"uid":"a1cf068b-26"},{"uid":"a1cf068b-16"}]},"a1cf068b-16":{"id":"/src/signal.ts","moduleParts":{"index.js":"a1cf068b-17"},"imported":[{"uid":"a1cf068b-0"},{"uid":"a1cf068b-14"},{"uid":"a1cf068b-6"}],"importedBy":[{"uid":"a1cf068b-26"},{"uid":"a1cf068b-22"},{"uid":"a1cf068b-18"}]},"a1cf068b-18":{"id":"/src/store.ts","moduleParts":{"index.js":"a1cf068b-19"},"imported":[{"uid":"a1cf068b-16"}],"importedBy":[{"uid":"a1cf068b-26"},{"uid":"a1cf068b-20"}]},"a1cf068b-20":{"id":"/src/reconcile.ts","moduleParts":{"index.js":"a1cf068b-21"},"imported":[{"uid":"a1cf068b-18"}],"importedBy":[{"uid":"a1cf068b-26"}]},"a1cf068b-22":{"id":"/src/resource.ts","moduleParts":{"index.js":"a1cf068b-23"},"imported":[{"uid":"a1cf068b-8"},{"uid":"a1cf068b-16"},{"uid":"a1cf068b-6"}],"importedBy":[{"uid":"a1cf068b-26"}]},"a1cf068b-24":{"id":"/src/watch.ts","moduleParts":{"index.js":"a1cf068b-25"},"imported":[{"uid":"a1cf068b-8"}],"importedBy":[{"uid":"a1cf068b-26"}]},"a1cf068b-26":{"id":"/src/index.ts","moduleParts":{"index.js":"a1cf068b-27"},"imported":[{"uid":"a1cf068b-0"},{"uid":"a1cf068b-2"},{"uid":"a1cf068b-10"},{"uid":"a1cf068b-12"},{"uid":"a1cf068b-14"},{"uid":"a1cf068b-8"},{"uid":"a1cf068b-20"},{"uid":"a1cf068b-22"},{"uid":"a1cf068b-4"},{"uid":"a1cf068b-16"},{"uid":"a1cf068b-18"},{"uid":"a1cf068b-6"},{"uid":"a1cf068b-24"}],"importedBy":[],"isEntry":true}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -1,24 +1,65 @@
1
1
  //#region src/batch.ts
2
+ const __DEV__ = process.env.NODE_ENV !== "production";
2
3
  let batchDepth = 0;
3
- const setA = /* @__PURE__ */ new Set();
4
- const setB = /* @__PURE__ */ new Set();
5
- let pendingNotifications = setA;
4
+ const pendingRecomputes = /* @__PURE__ */ new Set();
5
+ const pendingEffects = /* @__PURE__ */ new Set();
6
+ const _nextEffectPass = /* @__PURE__ */ new Set();
7
+ let _visitedThisPass = null;
8
+ const _recomputes = /* @__PURE__ */ new WeakSet();
9
+ const MAX_PASSES = 32;
10
+ /**
11
+ * Mark a callback as a computed recompute (called from computed.ts at
12
+ * creation time). Routes future enqueues into the recompute queue so they
13
+ * settle before any effects fire.
14
+ */
15
+ function _markRecompute(fn) {
16
+ _recomputes.add(fn);
17
+ }
6
18
  function batch(fn) {
7
19
  batchDepth++;
8
20
  try {
9
21
  fn();
10
22
  } finally {
11
23
  batchDepth--;
12
- if (batchDepth === 0 && pendingNotifications.size > 0) {
24
+ if (batchDepth === 0 && (pendingRecomputes.size > 0 || pendingEffects.size > 0)) {
13
25
  batchDepth = 1;
14
26
  try {
15
- while (pendingNotifications.size > 0) {
16
- const flush = pendingNotifications;
17
- pendingNotifications = flush === setA ? setB : setA;
18
- for (const notify of flush) notify();
19
- flush.clear();
27
+ let effectPass = 0;
28
+ while (pendingRecomputes.size > 0 || pendingEffects.size > 0) {
29
+ for (const r of pendingRecomputes) r();
30
+ pendingRecomputes.clear();
31
+ if (pendingEffects.size > 0) {
32
+ if (++effectPass > MAX_PASSES) {
33
+ if (__DEV__) {
34
+ const droppedCount = pendingEffects.size;
35
+ const labels = [];
36
+ for (const notify of pendingEffects) {
37
+ const label = notify._label;
38
+ if (label) labels.push(label);
39
+ if (labels.length >= 5) break;
40
+ }
41
+ const labelHint = labels.length ? ` Sample labels: ${labels.join(", ")}${droppedCount > labels.length ? `, …${droppedCount - labels.length} more` : ""}.` : "";
42
+ console.warn(`[pyreon] batch effect flush exceeded MAX_PASSES (32) — possible infinite re-enqueue loop. ${droppedCount} pending effects dropped.${labelHint} Common cause: an effect that writes to a signal it also reads, without a guard. See packages/core/reactivity/src/batch.ts for the multi-pass flush contract.`);
43
+ }
44
+ pendingEffects.clear();
45
+ _nextEffectPass.clear();
46
+ break;
47
+ }
48
+ _visitedThisPass = /* @__PURE__ */ new Set();
49
+ for (const notify of pendingEffects) {
50
+ _visitedThisPass.add(notify);
51
+ notify();
52
+ }
53
+ pendingEffects.clear();
54
+ for (const next of _nextEffectPass) pendingEffects.add(next);
55
+ _nextEffectPass.clear();
56
+ }
20
57
  }
21
58
  } finally {
59
+ pendingRecomputes.clear();
60
+ pendingEffects.clear();
61
+ _nextEffectPass.clear();
62
+ _visitedThisPass = null;
22
63
  batchDepth = 0;
23
64
  }
24
65
  }
@@ -28,7 +69,9 @@ function isBatching() {
28
69
  return batchDepth > 0;
29
70
  }
30
71
  function enqueuePendingNotification(notify) {
31
- pendingNotifications.add(notify);
72
+ if (_recomputes.has(notify)) pendingRecomputes.add(notify);
73
+ else if (_visitedThisPass !== null && _visitedThisPass.has(notify)) _nextEffectPass.add(notify);
74
+ else pendingEffects.add(notify);
32
75
  }
33
76
  /**
34
77
  * Returns a Promise that resolves after all currently-pending microtasks have flushed.
@@ -136,6 +179,7 @@ var EffectScope = class {
136
179
  }
137
180
  /** Register a callback to run after any reactive update in this scope. */
138
181
  addUpdateHook(fn) {
182
+ if (!this._active) return;
139
183
  if (this._updateHooks === null) this._updateHooks = [];
140
184
  this._updateHooks.push(fn);
141
185
  }
@@ -177,6 +221,31 @@ function setCurrentScope(scope) {
177
221
  function effectScope() {
178
222
  return new EffectScope();
179
223
  }
224
+ /**
225
+ * Register a callback to run when the current `EffectScope` stops. Vue 3
226
+ * parity. Must be called inside `scope.runInScope(fn)` — the registration
227
+ * captures the ambient scope, so calling outside any scope is a no-op (with
228
+ * a dev warning to surface the missing scope).
229
+ *
230
+ * Use to clean up resources tied to a scope's lifetime: timers, listeners,
231
+ * external subscriptions. Equivalent to calling `getCurrentScope()?.add({
232
+ * dispose: fn })` but with the scope capture handled.
233
+ *
234
+ * @example
235
+ * scope.runInScope(() => {
236
+ * const ws = new WebSocket(url)
237
+ * onScopeDispose(() => ws.close())
238
+ * // ws.close() runs when scope.stop() is called
239
+ * })
240
+ */
241
+ function onScopeDispose(fn) {
242
+ const scope = _currentScope;
243
+ if (!scope) {
244
+ if (process.env.NODE_ENV !== "production") console.warn("[pyreon] onScopeDispose() called without an active EffectScope — callback will never run. Wrap the call in `scope.runInScope(() => { ... })` or check `getCurrentScope()` before calling.");
245
+ return;
246
+ }
247
+ scope.add({ dispose: fn });
248
+ }
180
249
 
181
250
  //#endregion
182
251
  //#region src/tracking.ts
@@ -273,6 +342,17 @@ function runUntracked(fn) {
273
342
  //#endregion
274
343
  //#region src/effect.ts
275
344
  const _countSink$2 = globalThis;
345
+ let _snapshotCapture = null;
346
+ /**
347
+ * Register a capture/restore pair so reactivity-layer effects (`_bind`,
348
+ * `renderEffect`, `effect`) can preserve external context (e.g. the core
349
+ * provide/useContext stack) across signal-driven re-runs. Called by
350
+ * `@pyreon/core`'s context module at import time. Idempotent — calling again
351
+ * replaces the previously registered hook.
352
+ */
353
+ function setSnapshotCapture(hook) {
354
+ _snapshotCapture = hook;
355
+ }
276
356
  let _cleanupCollector = null;
277
357
  /**
278
358
  * Register a cleanup function inside an effect. The cleanup runs:
@@ -295,11 +375,17 @@ function onCleanup(fn) {
295
375
  if (_cleanupCollector) _cleanupCollector.push(fn);
296
376
  }
297
377
  let _innerEffectCollector = null;
298
- let _errorHandler = (err) => {
378
+ const _errorBridge = globalThis;
379
+ function _defaultErrorHandler(err) {
299
380
  console.error("[pyreon] Unhandled effect error:", err);
381
+ }
382
+ let _userErrorHandler;
383
+ const _errorHandler = (err) => {
384
+ (_userErrorHandler ?? _defaultErrorHandler)(err);
385
+ _errorBridge.__pyreon_report_error__?.(err, "effect");
300
386
  };
301
387
  function setErrorHandler(fn) {
302
- _errorHandler = fn;
388
+ _userErrorHandler = fn;
303
389
  }
304
390
  /** Remove an effect from all dependency subscriber sets (local deps array). */
305
391
  function cleanupLocalDeps$1(deps, fn) {
@@ -312,7 +398,11 @@ function cleanupLocalDeps$1(deps, fn) {
312
398
  }
313
399
  }
314
400
  function effect(fn) {
401
+ if (process.env.NODE_ENV !== "production") {
402
+ if (fn.constructor && fn.constructor.name === "AsyncFunction") console.warn("[pyreon] effect() received an async function. Signal reads after the first `await` are NOT tracked — only the synchronous prefix is. Read every tracked signal BEFORE any await, or split into separate effects, or use `watch(source, asyncCb)` for async-in-callback patterns.");
403
+ }
315
404
  const scope = getCurrentScope();
405
+ const snapshot = _snapshotCapture ? _snapshotCapture.capture() : null;
316
406
  let disposed = false;
317
407
  let isFirstRun = true;
318
408
  let cleanup;
@@ -347,7 +437,7 @@ function effect(fn) {
347
437
  };
348
438
  const run = () => {
349
439
  if (disposed) return;
350
- if (import.meta.env?.DEV === true) _countSink$2.__pyreon_count__?.("reactivity.effectRun");
440
+ if (process.env.NODE_ENV !== "production") _countSink$2.__pyreon_count__?.("reactivity.effectRun");
351
441
  runCleanup();
352
442
  const outerCollector = _innerEffectCollector;
353
443
  const myInners = [];
@@ -357,7 +447,7 @@ function effect(fn) {
357
447
  setDepsCollector(deps);
358
448
  const collected = [];
359
449
  _cleanupCollector = collected;
360
- cleanup = withTracking(run, fn) || void 0;
450
+ cleanup = withTracking(run, isFirstRun || snapshot === null || _snapshotCapture === null ? fn : () => _snapshotCapture.restore(snapshot, fn)) || void 0;
361
451
  _cleanupCollector = null;
362
452
  if (collected.length > 0) cleanups = collected;
363
453
  setDepsCollector(null);
@@ -411,9 +501,11 @@ function effect(fn) {
411
501
  function _bind(fn) {
412
502
  const deps = [];
413
503
  let disposed = false;
504
+ const snapshot = _snapshotCapture ? _snapshotCapture.capture() : null;
414
505
  const run = () => {
415
506
  if (disposed) return;
416
- fn();
507
+ if (snapshot !== null && _snapshotCapture) _snapshotCapture.restore(snapshot, fn);
508
+ else fn();
417
509
  };
418
510
  setDepsCollector(deps);
419
511
  withTracking(run, fn);
@@ -446,9 +538,14 @@ function renderEffectFullTrack(deps, run, fn) {
446
538
  }
447
539
  }
448
540
  function renderEffect(fn) {
541
+ if (process.env.NODE_ENV !== "production") {
542
+ if (fn.constructor && fn.constructor.name === "AsyncFunction") console.warn("[pyreon] renderEffect() received an async function. Signal reads after the first `await` are NOT tracked — only the synchronous prefix is. Read every tracked signal BEFORE any await, or split into separate effects, or use `watch(source, asyncCb)` for async-in-callback patterns.");
543
+ }
449
544
  const deps = [];
450
545
  let disposed = false;
451
546
  let isFirstRun = true;
547
+ const snapshot = _snapshotCapture ? _snapshotCapture.capture() : null;
548
+ const trackedFn = snapshot !== null && _snapshotCapture ? () => _snapshotCapture.restore(snapshot, fn) : fn;
452
549
  const run = () => {
453
550
  if (disposed) return;
454
551
  if (isFirstRun) {
@@ -461,7 +558,7 @@ function renderEffect(fn) {
461
558
  _restoreActiveEffect();
462
559
  setDepsCollector(null);
463
560
  }
464
- } else renderEffectFullTrack(deps, run, fn);
561
+ } else renderEffectFullTrack(deps, run, trackedFn);
465
562
  };
466
563
  run();
467
564
  const dispose = () => {
@@ -491,6 +588,9 @@ function trackWithLocalDeps(deps, effect, fn) {
491
588
  return result;
492
589
  }
493
590
  function computed(fn, options) {
591
+ if (process.env.NODE_ENV !== "production") {
592
+ if (fn.constructor && fn.constructor.name === "AsyncFunction") console.warn("[pyreon] computed() received an async function. The result type becomes `Computed<Promise<T>>`, and signal reads after the first `await` are NOT tracked. Use `createResource` for async-derived state, or compute synchronously over a signal that holds the awaited value.");
593
+ }
494
594
  return options?.equals ? computedWithEquals(fn, options.equals) : computedLazy(fn);
495
595
  }
496
596
  /**
@@ -517,10 +617,11 @@ function computedLazy(fn) {
517
617
  if (host._s) notifySubscribers(host._s);
518
618
  if (directFns) for (const f of directFns) f?.();
519
619
  };
620
+ _markRecompute(recompute);
520
621
  const read = () => {
521
622
  trackSubscriber(host);
522
623
  if (dirty) {
523
- if (import.meta.env?.DEV === true) _countSink$1.__pyreon_count__?.("reactivity.computedRecompute");
624
+ if (process.env.NODE_ENV !== "production") _countSink$1.__pyreon_count__?.("reactivity.computedRecompute");
524
625
  try {
525
626
  if (tracked) {
526
627
  setSkipDepsCollection(true);
@@ -576,7 +677,7 @@ function computedWithEquals(fn, equals) {
576
677
  let directFns = null;
577
678
  const recompute = () => {
578
679
  if (disposed) return;
579
- if (import.meta.env?.DEV === true) _countSink$1.__pyreon_count__?.("reactivity.computedRecompute");
680
+ if (process.env.NODE_ENV !== "production") _countSink$1.__pyreon_count__?.("reactivity.computedRecompute");
580
681
  cleanupLocalDeps(deps, recompute);
581
682
  try {
582
683
  const next = trackWithLocalDeps(deps, recompute, fn);
@@ -591,10 +692,11 @@ function computedWithEquals(fn, equals) {
591
692
  if (host._s) notifySubscribers(host._s);
592
693
  if (directFns) for (const f of directFns) f?.();
593
694
  };
695
+ _markRecompute(recompute);
594
696
  const read = () => {
595
697
  trackSubscriber(host);
596
698
  if (dirty) {
597
- if (import.meta.env?.DEV === true) _countSink$1.__pyreon_count__?.("reactivity.computedRecompute");
699
+ if (process.env.NODE_ENV !== "production") _countSink$1.__pyreon_count__?.("reactivity.computedRecompute");
598
700
  cleanupLocalDeps(deps, recompute);
599
701
  try {
600
702
  value = trackWithLocalDeps(deps, recompute, fn);
@@ -664,12 +766,18 @@ function notifyBucket(bucket) {
664
766
  * const isSelected = createSelector(selectedId)
665
767
  * // In each row:
666
768
  * class: () => (isSelected(row.id) ? "selected" : "")
769
+ *
770
+ * @example
771
+ * // Dynamic value spaces — call dispose() to release the per-value cache:
772
+ * const isCurrentTab = createSelector(() => currentTabId())
773
+ * onUnmount(() => isCurrentTab.dispose())
667
774
  */
668
775
  function createSelector(source) {
669
776
  const subs = /* @__PURE__ */ new Map();
670
777
  let current;
671
778
  let initialized = false;
672
- effect(() => {
779
+ let disposed = false;
780
+ const sourceEffect = effect(() => {
673
781
  const next = source();
674
782
  if (!initialized) {
675
783
  initialized = true;
@@ -685,20 +793,30 @@ function createSelector(source) {
685
793
  if (newBucket) notifyBucket(newBucket);
686
794
  });
687
795
  const hosts = /* @__PURE__ */ new Map();
688
- return (value) => {
689
- let host = hosts.get(value);
690
- if (!host) {
691
- let bucket = subs.get(value);
692
- if (!bucket) {
693
- bucket = /* @__PURE__ */ new Set();
694
- subs.set(value, bucket);
796
+ const selector = ((value) => {
797
+ if (!disposed) {
798
+ let host = hosts.get(value);
799
+ if (!host) {
800
+ let bucket = subs.get(value);
801
+ if (!bucket) {
802
+ bucket = /* @__PURE__ */ new Set();
803
+ subs.set(value, bucket);
804
+ }
805
+ host = { _s: bucket };
806
+ hosts.set(value, host);
695
807
  }
696
- host = { _s: bucket };
697
- hosts.set(value, host);
808
+ trackSubscriber(host);
698
809
  }
699
- trackSubscriber(host);
700
810
  return Object.is(current, value);
811
+ });
812
+ selector.dispose = () => {
813
+ if (disposed) return;
814
+ disposed = true;
815
+ sourceEffect.dispose();
816
+ subs.clear();
817
+ hosts.clear();
701
818
  };
819
+ return selector;
702
820
  }
703
821
 
704
822
  //#endregion
@@ -793,17 +911,20 @@ function inspectSignal(sig) {
793
911
 
794
912
  //#endregion
795
913
  //#region src/signal.ts
796
- const __DEV__ = typeof process !== "undefined" && process?.env?.NODE_ENV !== "production";
797
914
  const _countSink = globalThis;
798
915
  function _peek() {
799
916
  return this._v;
800
917
  }
801
918
  function _set(newValue) {
802
919
  if (Object.is(this._v, newValue)) return;
803
- if (import.meta.env?.DEV === true) _countSink.__pyreon_count__?.("reactivity.signalWrite");
920
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("reactivity.signalWrite");
804
921
  const prev = this._v;
805
922
  this._v = newValue;
806
- if (isTracing()) _notifyTraceListeners(this, prev, newValue);
923
+ if (isTracing()) try {
924
+ _notifyTraceListeners(this, prev, newValue);
925
+ } catch (err) {
926
+ if (process.env.NODE_ENV !== "production") console.error("[pyreon] signal trace listener threw — listener is buggy. Subscribers continue uninterrupted.", err);
927
+ }
807
928
  if (isBatching()) {
808
929
  if (this._d) notifyDirect(this._d);
809
930
  if (this._s) notifySubscribers(this._s);
@@ -860,9 +981,9 @@ function _debug() {
860
981
  * update, subscribe) are shared across all signals — not per-signal closures.
861
982
  */
862
983
  function signal(initialValue, options) {
863
- if (import.meta.env?.DEV === true) _countSink.__pyreon_count__?.("reactivity.signalCreate");
984
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("reactivity.signalCreate");
864
985
  const read = ((...args) => {
865
- if (__DEV__ && args.length > 0) console.warn("[Pyreon] signal() was called with an argument. Use signal.set(value) or signal.update(fn) to write. signal(value) only reads — the argument is ignored.");
986
+ if (process.env.NODE_ENV !== "production" && args.length > 0) console.warn("[Pyreon] signal() was called with an argument. Use signal.set(value) or signal.update(fn) to write. signal(value) only reads — the argument is ignored.");
866
987
  trackSubscriber(read);
867
988
  return read._v;
868
989
  });
@@ -896,7 +1017,38 @@ function signal(initialValue, options) {
896
1017
  * state.items[0].text = "world" // only text-tracking effects re-run
897
1018
  */
898
1019
  const proxyCache = /* @__PURE__ */ new WeakMap();
1020
+ const shallowProxyCache = /* @__PURE__ */ new WeakMap();
899
1021
  const IS_STORE = Symbol("pyreon.store");
1022
+ const IS_RAW = Symbol("pyreon.raw");
1023
+ /**
1024
+ * Mark an object as RAW — `createStore` and `shallowReactive` will return it
1025
+ * unwrapped. Useful when storing class instances, third-party objects, or
1026
+ * other shapes that shouldn't be deeply proxied (Vue 3 parity).
1027
+ *
1028
+ * @example
1029
+ * const cm = markRaw(new CodeMirrorView(...))
1030
+ * const store = createStore({ editor: cm })
1031
+ * store.editor === cm // true (not wrapped)
1032
+ *
1033
+ * Note: marking is one-way — there's no `unmarkRaw`. Mark BEFORE the object
1034
+ * enters a store; marking after wrap doesn't unwrap an existing proxy.
1035
+ */
1036
+ function markRaw(value) {
1037
+ Object.defineProperty(value, IS_RAW, {
1038
+ value: true,
1039
+ enumerable: false,
1040
+ configurable: true,
1041
+ writable: false
1042
+ });
1043
+ return value;
1044
+ }
1045
+ /** Returns true if the value was marked with `markRaw()`. */
1046
+ function isMarkedRaw(value) {
1047
+ return value[IS_RAW] === true;
1048
+ }
1049
+ function isBuiltinNonProxiable(obj) {
1050
+ return obj instanceof Map || obj instanceof Set || obj instanceof WeakMap || obj instanceof WeakSet || obj instanceof Date || obj instanceof RegExp || obj instanceof Promise || obj instanceof Error;
1051
+ }
900
1052
  /** Returns true if the value is a createStore proxy. */
901
1053
  function isStore(value) {
902
1054
  return value !== null && typeof value === "object" && value[IS_STORE] === true;
@@ -906,10 +1058,33 @@ function isStore(value) {
906
1058
  * Returns a proxy — mutations to the proxy trigger fine-grained reactive updates.
907
1059
  */
908
1060
  function createStore(initial) {
909
- return wrap(initial);
1061
+ return wrap(initial, false);
1062
+ }
1063
+ /**
1064
+ * Create a SHALLOW reactive store — only top-level mutations trigger updates.
1065
+ * Nested objects are NOT auto-wrapped; reading a nested object returns the
1066
+ * raw reference. Use when:
1067
+ * - the nested objects are immutable (frozen API responses)
1068
+ * - you want explicit control over which subtrees are reactive
1069
+ * - you need to store class instances or third-party objects without
1070
+ * paying the deep-proxy overhead
1071
+ *
1072
+ * @example
1073
+ * const store = shallowReactive({ user: { name: 'Alice' }, count: 0 })
1074
+ * effect(() => console.log(store.user)) // tracks store.user reference
1075
+ * effect(() => console.log(store.count)) // tracks store.count
1076
+ * store.user.name = 'Bob' // does NOT trigger any effect
1077
+ * store.count = 5 // triggers the count effect
1078
+ * store.user = { name: 'Bob' } // triggers the user effect
1079
+ */
1080
+ function shallowReactive(initial) {
1081
+ return wrap(initial, true);
910
1082
  }
911
- function wrap(raw) {
912
- const cached = proxyCache.get(raw);
1083
+ function wrap(raw, shallow) {
1084
+ if (isBuiltinNonProxiable(raw)) return raw;
1085
+ if (isMarkedRaw(raw)) return raw;
1086
+ const cache = shallow ? shallowProxyCache : proxyCache;
1087
+ const cached = cache.get(raw);
913
1088
  if (cached) return cached;
914
1089
  const propSignals = /* @__PURE__ */ new Map();
915
1090
  const isArray = Array.isArray(raw);
@@ -923,9 +1098,12 @@ function wrap(raw) {
923
1098
  if (key === IS_STORE) return true;
924
1099
  if (typeof key === "symbol") return target[key];
925
1100
  if (isArray && key === "length") return lengthSig?.();
926
- if (!Object.hasOwn(target, key)) return target[key];
1101
+ if (!Object.hasOwn(target, key)) {
1102
+ if (propSignals.has(key)) return propSignals.get(key)?.();
1103
+ return target[key];
1104
+ }
927
1105
  const value = getOrCreateSignal(key)();
928
- if (value !== null && typeof value === "object") return wrap(value);
1106
+ if (!shallow && value !== null && typeof value === "object") return wrap(value, false);
929
1107
  return value;
930
1108
  },
931
1109
  set(target, key, value) {
@@ -946,10 +1124,7 @@ function wrap(raw) {
946
1124
  },
947
1125
  deleteProperty(target, key) {
948
1126
  delete target[key];
949
- if (typeof key !== "symbol" && propSignals.has(key)) {
950
- propSignals.get(key)?.set(void 0);
951
- propSignals.delete(key);
952
- }
1127
+ if (typeof key !== "symbol" && propSignals.has(key)) propSignals.get(key)?.set(void 0);
953
1128
  if (isArray) lengthSig?.set(target.length);
954
1129
  return true;
955
1130
  },
@@ -963,7 +1138,7 @@ function wrap(raw) {
963
1138
  return Reflect.getOwnPropertyDescriptor(target, key);
964
1139
  }
965
1140
  });
966
- proxyCache.set(raw, proxy);
1141
+ cache.set(raw, proxy);
967
1142
  return proxy;
968
1143
  }
969
1144
 
@@ -1054,7 +1229,8 @@ function createResource(source, fetcher) {
1054
1229
  loading.set(false);
1055
1230
  });
1056
1231
  };
1057
- effect(() => {
1232
+ let disposed = false;
1233
+ const sourceEffect = effect(() => {
1058
1234
  const param = source();
1059
1235
  runUntracked(() => doFetch(param));
1060
1236
  });
@@ -1063,7 +1239,14 @@ function createResource(source, fetcher) {
1063
1239
  loading,
1064
1240
  error,
1065
1241
  refetch() {
1242
+ if (disposed) return;
1066
1243
  runUntracked(() => doFetch(source()));
1244
+ },
1245
+ dispose() {
1246
+ if (disposed) return;
1247
+ disposed = true;
1248
+ requestId++;
1249
+ sourceEffect.dispose();
1067
1250
  }
1068
1251
  };
1069
1252
  }
@@ -1124,5 +1307,5 @@ function watch(source, callback, opts = {}) {
1124
1307
  }
1125
1308
 
1126
1309
  //#endregion
1127
- export { Cell, EffectScope, _bind, batch, cell, computed, createResource, createSelector, createStore, effect, effectScope, getCurrentScope, inspectSignal, isStore, nextTick, onCleanup, onSignalUpdate, reconcile, renderEffect, runUntracked, runUntracked as untrack, setCurrentScope, setErrorHandler, signal, watch, why };
1310
+ export { Cell, EffectScope, _bind, batch, cell, computed, createResource, createSelector, createStore, effect, effectScope, getCurrentScope, inspectSignal, isStore, markRaw, nextTick, onCleanup, onScopeDispose, onSignalUpdate, reconcile, renderEffect, runUntracked, runUntracked as untrack, setCurrentScope, setErrorHandler, setSnapshotCapture, shallowReactive, signal, watch, why };
1128
1311
  //# sourceMappingURL=index.js.map