@pyreon/reactivity 0.13.1 → 0.15.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
@@ -55,7 +55,7 @@ batch(() => {
55
55
 
56
56
  ### Scopes
57
57
 
58
- - **`effectScope(): EffectScope`** -- Creates a scope that collects effects for bulk disposal.
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
59
  - **`getCurrentScope(): EffectScope | undefined`** -- Returns the active effect scope.
60
60
  - **`setCurrentScope(scope)`** -- Manually sets the current effect scope.
61
61
 
@@ -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":"20eb7817-1","name":"batch.ts"},{"uid":"20eb7817-3","name":"cell.ts"},{"uid":"20eb7817-5","name":"scope.ts"},{"uid":"20eb7817-7","name":"tracking.ts"},{"uid":"20eb7817-9","name":"effect.ts"},{"uid":"20eb7817-11","name":"computed.ts"},{"uid":"20eb7817-13","name":"createSelector.ts"},{"uid":"20eb7817-15","name":"debug.ts"},{"uid":"20eb7817-17","name":"signal.ts"},{"uid":"20eb7817-19","name":"store.ts"},{"uid":"20eb7817-21","name":"reconcile.ts"},{"uid":"20eb7817-23","name":"resource.ts"},{"uid":"20eb7817-25","name":"watch.ts"},{"uid":"20eb7817-27","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"20eb7817-1":{"renderedLength":953,"gzipLength":489,"brotliLength":0,"metaUid":"20eb7817-0"},"20eb7817-3":{"renderedLength":1636,"gzipLength":786,"brotliLength":0,"metaUid":"20eb7817-2"},"20eb7817-5":{"renderedLength":1787,"gzipLength":764,"brotliLength":0,"metaUid":"20eb7817-4"},"20eb7817-7":{"renderedLength":2227,"gzipLength":858,"brotliLength":0,"metaUid":"20eb7817-6"},"20eb7817-9":{"renderedLength":4535,"gzipLength":1610,"brotliLength":0,"metaUid":"20eb7817-8"},"20eb7817-11":{"renderedLength":3761,"gzipLength":1177,"brotliLength":0,"metaUid":"20eb7817-10"},"20eb7817-13":{"renderedLength":1810,"gzipLength":833,"brotliLength":0,"metaUid":"20eb7817-12"},"20eb7817-15":{"renderedLength":2469,"gzipLength":1092,"brotliLength":0,"metaUid":"20eb7817-14"},"20eb7817-17":{"renderedLength":2322,"gzipLength":1077,"brotliLength":0,"metaUid":"20eb7817-16"},"20eb7817-19":{"renderedLength":2879,"gzipLength":1056,"brotliLength":0,"metaUid":"20eb7817-18"},"20eb7817-21":{"renderedLength":2109,"gzipLength":867,"brotliLength":0,"metaUid":"20eb7817-20"},"20eb7817-23":{"renderedLength":1029,"gzipLength":475,"brotliLength":0,"metaUid":"20eb7817-22"},"20eb7817-25":{"renderedLength":1249,"gzipLength":582,"brotliLength":0,"metaUid":"20eb7817-24"},"20eb7817-27":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"20eb7817-26"}},"nodeMetas":{"20eb7817-0":{"id":"/src/batch.ts","moduleParts":{"index.js":"20eb7817-1"},"imported":[],"importedBy":[{"uid":"20eb7817-26"},{"uid":"20eb7817-16"},{"uid":"20eb7817-6"}]},"20eb7817-2":{"id":"/src/cell.ts","moduleParts":{"index.js":"20eb7817-3"},"imported":[],"importedBy":[{"uid":"20eb7817-26"}]},"20eb7817-4":{"id":"/src/scope.ts","moduleParts":{"index.js":"20eb7817-5"},"imported":[],"importedBy":[{"uid":"20eb7817-26"},{"uid":"20eb7817-10"},{"uid":"20eb7817-8"}]},"20eb7817-6":{"id":"/src/tracking.ts","moduleParts":{"index.js":"20eb7817-7"},"imported":[{"uid":"20eb7817-0"}],"importedBy":[{"uid":"20eb7817-26"},{"uid":"20eb7817-10"},{"uid":"20eb7817-12"},{"uid":"20eb7817-8"},{"uid":"20eb7817-22"},{"uid":"20eb7817-16"}]},"20eb7817-8":{"id":"/src/effect.ts","moduleParts":{"index.js":"20eb7817-9"},"imported":[{"uid":"20eb7817-4"},{"uid":"20eb7817-6"}],"importedBy":[{"uid":"20eb7817-26"},{"uid":"20eb7817-10"},{"uid":"20eb7817-12"},{"uid":"20eb7817-22"},{"uid":"20eb7817-24"}]},"20eb7817-10":{"id":"/src/computed.ts","moduleParts":{"index.js":"20eb7817-11"},"imported":[{"uid":"20eb7817-8"},{"uid":"20eb7817-4"},{"uid":"20eb7817-6"}],"importedBy":[{"uid":"20eb7817-26"}]},"20eb7817-12":{"id":"/src/createSelector.ts","moduleParts":{"index.js":"20eb7817-13"},"imported":[{"uid":"20eb7817-8"},{"uid":"20eb7817-6"}],"importedBy":[{"uid":"20eb7817-26"}]},"20eb7817-14":{"id":"/src/debug.ts","moduleParts":{"index.js":"20eb7817-15"},"imported":[],"importedBy":[{"uid":"20eb7817-26"},{"uid":"20eb7817-16"}]},"20eb7817-16":{"id":"/src/signal.ts","moduleParts":{"index.js":"20eb7817-17"},"imported":[{"uid":"20eb7817-0"},{"uid":"20eb7817-14"},{"uid":"20eb7817-6"}],"importedBy":[{"uid":"20eb7817-26"},{"uid":"20eb7817-22"},{"uid":"20eb7817-18"}]},"20eb7817-18":{"id":"/src/store.ts","moduleParts":{"index.js":"20eb7817-19"},"imported":[{"uid":"20eb7817-16"}],"importedBy":[{"uid":"20eb7817-26"},{"uid":"20eb7817-20"}]},"20eb7817-20":{"id":"/src/reconcile.ts","moduleParts":{"index.js":"20eb7817-21"},"imported":[{"uid":"20eb7817-18"}],"importedBy":[{"uid":"20eb7817-26"}]},"20eb7817-22":{"id":"/src/resource.ts","moduleParts":{"index.js":"20eb7817-23"},"imported":[{"uid":"20eb7817-8"},{"uid":"20eb7817-16"},{"uid":"20eb7817-6"}],"importedBy":[{"uid":"20eb7817-26"}]},"20eb7817-24":{"id":"/src/watch.ts","moduleParts":{"index.js":"20eb7817-25"},"imported":[{"uid":"20eb7817-8"}],"importedBy":[{"uid":"20eb7817-26"}]},"20eb7817-26":{"id":"/src/index.ts","moduleParts":{"index.js":"20eb7817-27"},"imported":[{"uid":"20eb7817-0"},{"uid":"20eb7817-2"},{"uid":"20eb7817-10"},{"uid":"20eb7817-12"},{"uid":"20eb7817-14"},{"uid":"20eb7817-8"},{"uid":"20eb7817-20"},{"uid":"20eb7817-22"},{"uid":"20eb7817-4"},{"uid":"20eb7817-16"},{"uid":"20eb7817-18"},{"uid":"20eb7817-6"},{"uid":"20eb7817-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":"22680e1b-1","name":"batch.ts"},{"uid":"22680e1b-3","name":"cell.ts"},{"uid":"22680e1b-5","name":"scope.ts"},{"uid":"22680e1b-7","name":"tracking.ts"},{"uid":"22680e1b-9","name":"effect.ts"},{"uid":"22680e1b-11","name":"computed.ts"},{"uid":"22680e1b-13","name":"createSelector.ts"},{"uid":"22680e1b-15","name":"debug.ts"},{"uid":"22680e1b-17","name":"signal.ts"},{"uid":"22680e1b-19","name":"store.ts"},{"uid":"22680e1b-21","name":"reconcile.ts"},{"uid":"22680e1b-23","name":"resource.ts"},{"uid":"22680e1b-25","name":"watch.ts"},{"uid":"22680e1b-27","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"22680e1b-1":{"renderedLength":2427,"gzipLength":977,"brotliLength":0,"metaUid":"22680e1b-0"},"22680e1b-3":{"renderedLength":1636,"gzipLength":786,"brotliLength":0,"metaUid":"22680e1b-2"},"22680e1b-5":{"renderedLength":1977,"gzipLength":786,"brotliLength":0,"metaUid":"22680e1b-4"},"22680e1b-7":{"renderedLength":2227,"gzipLength":858,"brotliLength":0,"metaUid":"22680e1b-6"},"22680e1b-9":{"renderedLength":7391,"gzipLength":2397,"brotliLength":0,"metaUid":"22680e1b-8"},"22680e1b-11":{"renderedLength":4548,"gzipLength":1464,"brotliLength":0,"metaUid":"22680e1b-10"},"22680e1b-13":{"renderedLength":1810,"gzipLength":833,"brotliLength":0,"metaUid":"22680e1b-12"},"22680e1b-15":{"renderedLength":2469,"gzipLength":1092,"brotliLength":0,"metaUid":"22680e1b-14"},"22680e1b-17":{"renderedLength":2626,"gzipLength":1125,"brotliLength":0,"metaUid":"22680e1b-16"},"22680e1b-19":{"renderedLength":2879,"gzipLength":1056,"brotliLength":0,"metaUid":"22680e1b-18"},"22680e1b-21":{"renderedLength":2109,"gzipLength":867,"brotliLength":0,"metaUid":"22680e1b-20"},"22680e1b-23":{"renderedLength":1029,"gzipLength":475,"brotliLength":0,"metaUid":"22680e1b-22"},"22680e1b-25":{"renderedLength":1249,"gzipLength":582,"brotliLength":0,"metaUid":"22680e1b-24"},"22680e1b-27":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"22680e1b-26"}},"nodeMetas":{"22680e1b-0":{"id":"/src/batch.ts","moduleParts":{"index.js":"22680e1b-1"},"imported":[],"importedBy":[{"uid":"22680e1b-26"},{"uid":"22680e1b-10"},{"uid":"22680e1b-16"},{"uid":"22680e1b-6"}]},"22680e1b-2":{"id":"/src/cell.ts","moduleParts":{"index.js":"22680e1b-3"},"imported":[],"importedBy":[{"uid":"22680e1b-26"}]},"22680e1b-4":{"id":"/src/scope.ts","moduleParts":{"index.js":"22680e1b-5"},"imported":[],"importedBy":[{"uid":"22680e1b-26"},{"uid":"22680e1b-10"},{"uid":"22680e1b-8"}]},"22680e1b-6":{"id":"/src/tracking.ts","moduleParts":{"index.js":"22680e1b-7"},"imported":[{"uid":"22680e1b-0"}],"importedBy":[{"uid":"22680e1b-26"},{"uid":"22680e1b-10"},{"uid":"22680e1b-12"},{"uid":"22680e1b-8"},{"uid":"22680e1b-22"},{"uid":"22680e1b-16"}]},"22680e1b-8":{"id":"/src/effect.ts","moduleParts":{"index.js":"22680e1b-9"},"imported":[{"uid":"22680e1b-4"},{"uid":"22680e1b-6"}],"importedBy":[{"uid":"22680e1b-26"},{"uid":"22680e1b-10"},{"uid":"22680e1b-12"},{"uid":"22680e1b-22"},{"uid":"22680e1b-24"}]},"22680e1b-10":{"id":"/src/computed.ts","moduleParts":{"index.js":"22680e1b-11"},"imported":[{"uid":"22680e1b-0"},{"uid":"22680e1b-8"},{"uid":"22680e1b-4"},{"uid":"22680e1b-6"}],"importedBy":[{"uid":"22680e1b-26"}]},"22680e1b-12":{"id":"/src/createSelector.ts","moduleParts":{"index.js":"22680e1b-13"},"imported":[{"uid":"22680e1b-8"},{"uid":"22680e1b-6"}],"importedBy":[{"uid":"22680e1b-26"}]},"22680e1b-14":{"id":"/src/debug.ts","moduleParts":{"index.js":"22680e1b-15"},"imported":[],"importedBy":[{"uid":"22680e1b-26"},{"uid":"22680e1b-16"}]},"22680e1b-16":{"id":"/src/signal.ts","moduleParts":{"index.js":"22680e1b-17"},"imported":[{"uid":"22680e1b-0"},{"uid":"22680e1b-14"},{"uid":"22680e1b-6"}],"importedBy":[{"uid":"22680e1b-26"},{"uid":"22680e1b-22"},{"uid":"22680e1b-18"}]},"22680e1b-18":{"id":"/src/store.ts","moduleParts":{"index.js":"22680e1b-19"},"imported":[{"uid":"22680e1b-16"}],"importedBy":[{"uid":"22680e1b-26"},{"uid":"22680e1b-20"}]},"22680e1b-20":{"id":"/src/reconcile.ts","moduleParts":{"index.js":"22680e1b-21"},"imported":[{"uid":"22680e1b-18"}],"importedBy":[{"uid":"22680e1b-26"}]},"22680e1b-22":{"id":"/src/resource.ts","moduleParts":{"index.js":"22680e1b-23"},"imported":[{"uid":"22680e1b-8"},{"uid":"22680e1b-16"},{"uid":"22680e1b-6"}],"importedBy":[{"uid":"22680e1b-26"}]},"22680e1b-24":{"id":"/src/watch.ts","moduleParts":{"index.js":"22680e1b-25"},"imported":[{"uid":"22680e1b-8"}],"importedBy":[{"uid":"22680e1b-26"}]},"22680e1b-26":{"id":"/src/index.ts","moduleParts":{"index.js":"22680e1b-27"},"imported":[{"uid":"22680e1b-0"},{"uid":"22680e1b-2"},{"uid":"22680e1b-10"},{"uid":"22680e1b-12"},{"uid":"22680e1b-14"},{"uid":"22680e1b-8"},{"uid":"22680e1b-20"},{"uid":"22680e1b-22"},{"uid":"22680e1b-4"},{"uid":"22680e1b-16"},{"uid":"22680e1b-18"},{"uid":"22680e1b-6"},{"uid":"22680e1b-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,19 +1,55 @@
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) {
13
- const flush = pendingNotifications;
14
- pendingNotifications = flush === setA ? setB : setA;
15
- for (const notify of flush) notify();
16
- flush.clear();
24
+ if (batchDepth === 0 && (pendingRecomputes.size > 0 || pendingEffects.size > 0)) {
25
+ batchDepth = 1;
26
+ try {
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__) console.warn(`[pyreon] batch effect flush exceeded MAX_PASSES (32) — possible infinite re-enqueue loop. ${pendingEffects.size} pending effects dropped. See packages/core/reactivity/src/batch.ts for the multi-pass flush contract.`);
34
+ break;
35
+ }
36
+ _visitedThisPass = /* @__PURE__ */ new Set();
37
+ for (const notify of pendingEffects) {
38
+ _visitedThisPass.add(notify);
39
+ notify();
40
+ }
41
+ pendingEffects.clear();
42
+ for (const next of _nextEffectPass) pendingEffects.add(next);
43
+ _nextEffectPass.clear();
44
+ }
45
+ }
46
+ } finally {
47
+ pendingRecomputes.clear();
48
+ pendingEffects.clear();
49
+ _nextEffectPass.clear();
50
+ _visitedThisPass = null;
51
+ batchDepth = 0;
52
+ }
17
53
  }
18
54
  }
19
55
  }
@@ -21,7 +57,9 @@ function isBatching() {
21
57
  return batchDepth > 0;
22
58
  }
23
59
  function enqueuePendingNotification(notify) {
24
- pendingNotifications.add(notify);
60
+ if (_recomputes.has(notify)) pendingRecomputes.add(notify);
61
+ else if (_visitedThisPass !== null && _visitedThisPass.has(notify)) _nextEffectPass.add(notify);
62
+ else pendingEffects.add(notify);
25
63
  }
26
64
  /**
27
65
  * Returns a Promise that resolves after all currently-pending microtasks have flushed.
@@ -102,13 +140,15 @@ function cell(value) {
102
140
  //#endregion
103
141
  //#region src/scope.ts
104
142
  var EffectScope = class {
105
- _effects = [];
143
+ _effects = null;
106
144
  _active = true;
107
- _updateHooks = [];
145
+ _updateHooks = null;
108
146
  _updatePending = false;
109
147
  /** Register an effect/computed to be disposed when this scope stops. */
110
148
  add(e) {
111
- if (this._active) this._effects.push(e);
149
+ if (!this._active) return;
150
+ if (this._effects === null) this._effects = [];
151
+ this._effects.push(e);
112
152
  }
113
153
  /**
114
154
  * Temporarily re-activate this scope so effects created inside `fn` are
@@ -127,6 +167,7 @@ var EffectScope = class {
127
167
  }
128
168
  /** Register a callback to run after any reactive update in this scope. */
129
169
  addUpdateHook(fn) {
170
+ if (this._updateHooks === null) this._updateHooks = [];
130
171
  this._updateHooks.push(fn);
131
172
  }
132
173
  /**
@@ -134,11 +175,11 @@ var EffectScope = class {
134
175
  * Schedules onUpdate hooks via microtask so all synchronous effects settle first.
135
176
  */
136
177
  notifyEffectRan() {
137
- if (!this._active || this._updateHooks.length === 0 || this._updatePending) return;
178
+ if (!this._active || !this._updateHooks || this._updateHooks.length === 0 || this._updatePending) return;
138
179
  this._updatePending = true;
139
180
  queueMicrotask(() => {
140
181
  this._updatePending = false;
141
- if (!this._active) return;
182
+ if (!this._active || !this._updateHooks) return;
142
183
  for (const fn of this._updateHooks) try {
143
184
  fn();
144
185
  } catch (err) {
@@ -149,9 +190,9 @@ var EffectScope = class {
149
190
  /** Dispose all tracked effects. */
150
191
  stop() {
151
192
  if (!this._active) return;
152
- for (const e of this._effects) e.dispose();
153
- this._effects = [];
154
- this._updateHooks = [];
193
+ if (this._effects) for (const e of this._effects) e.dispose();
194
+ this._effects = null;
195
+ this._updateHooks = null;
155
196
  this._updatePending = false;
156
197
  this._active = false;
157
198
  }
@@ -262,6 +303,18 @@ function runUntracked(fn) {
262
303
 
263
304
  //#endregion
264
305
  //#region src/effect.ts
306
+ const _countSink$2 = globalThis;
307
+ let _snapshotCapture = null;
308
+ /**
309
+ * Register a capture/restore pair so reactivity-layer effects (`_bind`,
310
+ * `renderEffect`, `effect`) can preserve external context (e.g. the core
311
+ * provide/useContext stack) across signal-driven re-runs. Called by
312
+ * `@pyreon/core`'s context module at import time. Idempotent — calling again
313
+ * replaces the previously registered hook.
314
+ */
315
+ function setSnapshotCapture(hook) {
316
+ _snapshotCapture = hook;
317
+ }
265
318
  let _cleanupCollector = null;
266
319
  /**
267
320
  * Register a cleanup function inside an effect. The cleanup runs:
@@ -283,11 +336,18 @@ let _cleanupCollector = null;
283
336
  function onCleanup(fn) {
284
337
  if (_cleanupCollector) _cleanupCollector.push(fn);
285
338
  }
286
- let _errorHandler = (err) => {
339
+ let _innerEffectCollector = null;
340
+ const _errorBridge = globalThis;
341
+ function _defaultErrorHandler(err) {
287
342
  console.error("[pyreon] Unhandled effect error:", err);
343
+ }
344
+ let _userErrorHandler;
345
+ const _errorHandler = (err) => {
346
+ (_userErrorHandler ?? _defaultErrorHandler)(err);
347
+ _errorBridge.__pyreon_report_error__?.(err, "effect");
288
348
  };
289
349
  function setErrorHandler(fn) {
290
- _errorHandler = fn;
350
+ _userErrorHandler = fn;
291
351
  }
292
352
  /** Remove an effect from all dependency subscriber sets (local deps array). */
293
353
  function cleanupLocalDeps$1(deps, fn) {
@@ -300,13 +360,26 @@ function cleanupLocalDeps$1(deps, fn) {
300
360
  }
301
361
  }
302
362
  function effect(fn) {
363
+ if (process.env.NODE_ENV !== "production") {
364
+ 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.");
365
+ }
303
366
  const scope = getCurrentScope();
367
+ const snapshot = _snapshotCapture ? _snapshotCapture.capture() : null;
304
368
  let disposed = false;
305
369
  let isFirstRun = true;
306
370
  let cleanup;
307
371
  const deps = [];
308
372
  let cleanups;
373
+ let innerEffects = null;
309
374
  const runCleanup = () => {
375
+ if (innerEffects) {
376
+ for (const ie of innerEffects) try {
377
+ ie.dispose();
378
+ } catch (err) {
379
+ _errorHandler(err);
380
+ }
381
+ innerEffects = null;
382
+ }
310
383
  if (cleanups) {
311
384
  for (const c of cleanups) try {
312
385
  c();
@@ -326,13 +399,17 @@ function effect(fn) {
326
399
  };
327
400
  const run = () => {
328
401
  if (disposed) return;
402
+ if (process.env.NODE_ENV !== "production") _countSink$2.__pyreon_count__?.("reactivity.effectRun");
329
403
  runCleanup();
404
+ const outerCollector = _innerEffectCollector;
405
+ const myInners = [];
406
+ _innerEffectCollector = myInners;
330
407
  try {
331
408
  cleanupLocalDeps$1(deps, run);
332
409
  setDepsCollector(deps);
333
410
  const collected = [];
334
411
  _cleanupCollector = collected;
335
- cleanup = withTracking(run, fn) || void 0;
412
+ cleanup = withTracking(run, isFirstRun || snapshot === null || _snapshotCapture === null ? fn : () => _snapshotCapture.restore(snapshot, fn)) || void 0;
336
413
  _cleanupCollector = null;
337
414
  if (collected.length > 0) cleanups = collected;
338
415
  setDepsCollector(null);
@@ -340,7 +417,10 @@ function effect(fn) {
340
417
  _cleanupCollector = null;
341
418
  setDepsCollector(null);
342
419
  _errorHandler(err);
420
+ } finally {
421
+ _innerEffectCollector = outerCollector;
343
422
  }
423
+ if (myInners.length > 0) innerEffects = myInners;
344
424
  if (!isFirstRun) scope?.notifyEffectRan();
345
425
  isFirstRun = false;
346
426
  };
@@ -350,7 +430,8 @@ function effect(fn) {
350
430
  disposed = true;
351
431
  cleanupLocalDeps$1(deps, run);
352
432
  } };
353
- getCurrentScope()?.add(e);
433
+ if (_innerEffectCollector !== null) _innerEffectCollector.push(e);
434
+ else getCurrentScope()?.add(e);
354
435
  return e;
355
436
  }
356
437
  /**
@@ -382,9 +463,11 @@ function effect(fn) {
382
463
  function _bind(fn) {
383
464
  const deps = [];
384
465
  let disposed = false;
466
+ const snapshot = _snapshotCapture ? _snapshotCapture.capture() : null;
385
467
  const run = () => {
386
468
  if (disposed) return;
387
- fn();
469
+ if (snapshot !== null && _snapshotCapture) _snapshotCapture.restore(snapshot, fn);
470
+ else fn();
388
471
  };
389
472
  setDepsCollector(deps);
390
473
  withTracking(run, fn);
@@ -417,11 +500,27 @@ function renderEffectFullTrack(deps, run, fn) {
417
500
  }
418
501
  }
419
502
  function renderEffect(fn) {
503
+ if (process.env.NODE_ENV !== "production") {
504
+ 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.");
505
+ }
420
506
  const deps = [];
421
507
  let disposed = false;
508
+ let isFirstRun = true;
509
+ const snapshot = _snapshotCapture ? _snapshotCapture.capture() : null;
510
+ const trackedFn = snapshot !== null && _snapshotCapture ? () => _snapshotCapture.restore(snapshot, fn) : fn;
422
511
  const run = () => {
423
512
  if (disposed) return;
424
- renderEffectFullTrack(deps, run, fn);
513
+ if (isFirstRun) {
514
+ isFirstRun = false;
515
+ setDepsCollector(deps);
516
+ _setActiveEffect(run);
517
+ try {
518
+ fn();
519
+ } finally {
520
+ _restoreActiveEffect();
521
+ setDepsCollector(null);
522
+ }
523
+ } else renderEffectFullTrack(deps, run, trackedFn);
425
524
  };
426
525
  run();
427
526
  const dispose = () => {
@@ -437,6 +536,7 @@ function renderEffect(fn) {
437
536
 
438
537
  //#endregion
439
538
  //#region src/computed.ts
539
+ const _countSink$1 = globalThis;
440
540
  /** Remove a computed from all dependency subscriber sets (local deps array). */
441
541
  function cleanupLocalDeps(deps, fn) {
442
542
  for (let i = 0; i < deps.length; i++) deps[i].delete(fn);
@@ -450,6 +550,9 @@ function trackWithLocalDeps(deps, effect, fn) {
450
550
  return result;
451
551
  }
452
552
  function computed(fn, options) {
553
+ if (process.env.NODE_ENV !== "production") {
554
+ 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.");
555
+ }
453
556
  return options?.equals ? computedWithEquals(fn, options.equals) : computedLazy(fn);
454
557
  }
455
558
  /**
@@ -476,9 +579,11 @@ function computedLazy(fn) {
476
579
  if (host._s) notifySubscribers(host._s);
477
580
  if (directFns) for (const f of directFns) f?.();
478
581
  };
582
+ _markRecompute(recompute);
479
583
  const read = () => {
480
584
  trackSubscriber(host);
481
585
  if (dirty) {
586
+ if (process.env.NODE_ENV !== "production") _countSink$1.__pyreon_count__?.("reactivity.computedRecompute");
482
587
  try {
483
588
  if (tracked) {
484
589
  setSkipDepsCollection(true);
@@ -489,7 +594,6 @@ function computedLazy(fn) {
489
594
  tracked = true;
490
595
  }
491
596
  } catch (err) {
492
- setSkipDepsCollection(false);
493
597
  _errorHandler(err);
494
598
  }
495
599
  dirty = false;
@@ -535,6 +639,7 @@ function computedWithEquals(fn, equals) {
535
639
  let directFns = null;
536
640
  const recompute = () => {
537
641
  if (disposed) return;
642
+ if (process.env.NODE_ENV !== "production") _countSink$1.__pyreon_count__?.("reactivity.computedRecompute");
538
643
  cleanupLocalDeps(deps, recompute);
539
644
  try {
540
645
  const next = trackWithLocalDeps(deps, recompute, fn);
@@ -549,9 +654,11 @@ function computedWithEquals(fn, equals) {
549
654
  if (host._s) notifySubscribers(host._s);
550
655
  if (directFns) for (const f of directFns) f?.();
551
656
  };
657
+ _markRecompute(recompute);
552
658
  const read = () => {
553
659
  trackSubscriber(host);
554
660
  if (dirty) {
661
+ if (process.env.NODE_ENV !== "production") _countSink$1.__pyreon_count__?.("reactivity.computedRecompute");
555
662
  cleanupLocalDeps(deps, recompute);
556
663
  try {
557
664
  value = trackWithLocalDeps(deps, recompute, fn);
@@ -750,17 +857,23 @@ function inspectSignal(sig) {
750
857
 
751
858
  //#endregion
752
859
  //#region src/signal.ts
753
- const __DEV__ = typeof process !== "undefined" && process?.env?.NODE_ENV !== "production";
860
+ const _countSink = globalThis;
754
861
  function _peek() {
755
862
  return this._v;
756
863
  }
757
864
  function _set(newValue) {
758
865
  if (Object.is(this._v, newValue)) return;
866
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("reactivity.signalWrite");
759
867
  const prev = this._v;
760
868
  this._v = newValue;
761
869
  if (isTracing()) _notifyTraceListeners(this, prev, newValue);
762
- if (this._d) notifyDirect(this._d);
763
- if (this._s) notifySubscribers(this._s);
870
+ if (isBatching()) {
871
+ if (this._d) notifyDirect(this._d);
872
+ if (this._s) notifySubscribers(this._s);
873
+ } else batch(() => {
874
+ if (this._d) notifyDirect(this._d);
875
+ if (this._s) notifySubscribers(this._s);
876
+ });
764
877
  }
765
878
  function _update(fn) {
766
879
  _set.call(this, fn(this._v));
@@ -810,8 +923,9 @@ function _debug() {
810
923
  * update, subscribe) are shared across all signals — not per-signal closures.
811
924
  */
812
925
  function signal(initialValue, options) {
926
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("reactivity.signalCreate");
813
927
  const read = ((...args) => {
814
- 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.");
928
+ 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.");
815
929
  trackSubscriber(read);
816
930
  return read._v;
817
931
  });
@@ -1073,5 +1187,5 @@ function watch(source, callback, opts = {}) {
1073
1187
  }
1074
1188
 
1075
1189
  //#endregion
1076
- 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 };
1190
+ 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, setSnapshotCapture, signal, watch, why };
1077
1191
  //# sourceMappingURL=index.js.map
@@ -191,6 +191,19 @@ declare function inspectSignal<T>(sig: Signal<T>): SignalDebugInfo<T>;
191
191
  interface Effect {
192
192
  dispose(): void;
193
193
  }
194
+ interface ReactiveSnapshotCapture {
195
+ capture: () => unknown;
196
+ /** Run `fn` with the previously-captured snapshot active. */
197
+ restore: <T>(snap: unknown, fn: () => T) => T;
198
+ }
199
+ /**
200
+ * Register a capture/restore pair so reactivity-layer effects (`_bind`,
201
+ * `renderEffect`, `effect`) can preserve external context (e.g. the core
202
+ * provide/useContext stack) across signal-driven re-runs. Called by
203
+ * `@pyreon/core`'s context module at import time. Idempotent — calling again
204
+ * replaces the previously registered hook.
205
+ */
206
+ declare function setSnapshotCapture(hook: ReactiveSnapshotCapture | null): void;
194
207
  /**
195
208
  * Register a cleanup function inside an effect. The cleanup runs:
196
209
  * - Before the effect re-runs (when dependencies change)
@@ -373,5 +386,5 @@ interface WatchOptions {
373
386
  */
374
387
  declare function watch<T>(source: () => T, callback: (newVal: T, oldVal: T | undefined) => void | (() => void), opts?: WatchOptions): () => void;
375
388
  //#endregion
376
- export { Cell, type Computed, type ComputedOptions, type Effect, EffectScope, type ReadonlySignal, type Resource, type Signal, type SignalDebugInfo, type SignalOptions, type WatchOptions, _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 };
389
+ export { Cell, type Computed, type ComputedOptions, type Effect, EffectScope, type ReactiveSnapshotCapture, type ReadonlySignal, type Resource, type Signal, type SignalDebugInfo, type SignalOptions, type WatchOptions, _bind, batch, cell, computed, createResource, createSelector, createStore, effect, effectScope, getCurrentScope, inspectSignal, isStore, nextTick, onCleanup, onSignalUpdate, reconcile, renderEffect, runUntracked, runUntracked as untrack, setCurrentScope, setErrorHandler, setSnapshotCapture, signal, watch, why };
377
390
  //# sourceMappingURL=index2.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/reactivity",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "Signals-based reactivity system for Pyreon",
5
5
  "homepage": "https://github.com/pyreon/pyreon/tree/main/packages/reactivity#readme",
6
6
  "bugs": {
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "files": [
16
16
  "lib",
17
+ "!lib/**/*.map",
17
18
  "src",
18
19
  "README.md",
19
20
  "LICENSE"
@@ -33,9 +34,6 @@
33
34
  "publishConfig": {
34
35
  "access": "public"
35
36
  },
36
- "devDependencies": {
37
- "@pyreon/manifest": "0.13.1"
38
- },
39
37
  "scripts": {
40
38
  "build": "vl_rolldown_build",
41
39
  "dev": "vl_rolldown_build-watch",
@@ -43,5 +41,8 @@
43
41
  "typecheck": "tsc --noEmit",
44
42
  "lint": "oxlint .",
45
43
  "prepublishOnly": "bun run build"
44
+ },
45
+ "devDependencies": {
46
+ "@pyreon/manifest": "0.13.1"
46
47
  }
47
48
  }