@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 +4 -1
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +230 -47
- package/lib/types/index.d.ts +95 -3
- package/package.json +5 -4
- package/src/batch.ts +155 -32
- package/src/computed.ts +21 -6
- package/src/createSelector.ts +44 -12
- package/src/effect.ts +149 -14
- package/src/env.d.ts +6 -0
- package/src/index.ts +18 -3
- package/src/manifest.ts +372 -5
- package/src/reconcile.ts +9 -1
- package/src/resource.ts +19 -1
- package/src/scope.ts +38 -0
- package/src/signal.ts +29 -12
- package/src/store.ts +111 -11
- package/src/tests/batch.test.ts +605 -0
- package/src/tests/computed.test.ts +86 -0
- package/src/tests/createSelector.test.ts +59 -0
- package/src/tests/effect.test.ts +65 -0
- package/src/tests/fanout-repro.test.ts +179 -0
- package/src/tests/manifest-snapshot.test.ts +17 -1
- package/src/tests/resource.test.ts +93 -0
- package/src/tests/scope.test.ts +29 -0
- package/src/tests/signal.test.ts +108 -0
- package/src/tests/store.test.ts +54 -0
- package/src/tests/vue-parity.test.ts +191 -0
- package/lib/index.js.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
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 |
|
|
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":"
|
|
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
|
|
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 &&
|
|
24
|
+
if (batchDepth === 0 && (pendingRecomputes.size > 0 || pendingEffects.size > 0)) {
|
|
13
25
|
batchDepth = 1;
|
|
14
26
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
|
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 (
|
|
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())
|
|
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 (
|
|
984
|
+
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("reactivity.signalCreate");
|
|
864
985
|
const read = ((...args) => {
|
|
865
|
-
if (
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|