@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 +1 -1
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +143 -29
- package/lib/types/index.d.ts +14 -1
- package/package.json +5 -4
- package/src/batch.ts +142 -17
- package/src/computed.ts +31 -1
- package/src/effect.ts +203 -8
- package/src/env.d.ts +6 -0
- package/src/index.ts +10 -1
- package/src/scope.ts +13 -8
- package/src/signal.ts +31 -9
- package/src/tests/batch.test.ts +418 -0
- package/src/tests/computed.test.ts +32 -0
- package/src/tests/effect.test.ts +65 -0
- package/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
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":"
|
|
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
|
|
4
|
-
const
|
|
5
|
-
|
|
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 &&
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
763
|
-
|
|
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 (
|
|
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
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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.
|
|
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
|
}
|