@pyreon/reactivity 0.16.0 → 0.19.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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +136 -27
- package/lib/types/index.d.ts +53 -3
- package/package.json +1 -1
- package/src/computed.ts +44 -14
- package/src/effect.ts +5 -0
- package/src/index.ts +2 -0
- package/src/manifest.ts +22 -1
- package/src/reactive-trace.ts +142 -0
- package/src/reconcile.ts +12 -0
- package/src/signal.ts +34 -21
- package/src/store.ts +11 -0
- package/src/tests/computed.test.ts +31 -0
- package/src/tests/coverage-hardening.test.ts +471 -0
- package/src/tests/manifest-snapshot.test.ts +4 -3
- package/src/tests/reactive-trace.test.ts +102 -0
- package/src/tests/reconcile-security.test.ts +45 -0
- package/src/tests/signal.test.ts +35 -9
|
@@ -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":"3d8fcc8f-1","name":"batch.ts"},{"uid":"3d8fcc8f-3","name":"cell.ts"},{"uid":"3d8fcc8f-5","name":"scope.ts"},{"uid":"3d8fcc8f-7","name":"tracking.ts"},{"uid":"3d8fcc8f-9","name":"effect.ts"},{"uid":"3d8fcc8f-11","name":"computed.ts"},{"uid":"3d8fcc8f-13","name":"createSelector.ts"},{"uid":"3d8fcc8f-15","name":"debug.ts"},{"uid":"3d8fcc8f-17","name":"reactive-trace.ts"},{"uid":"3d8fcc8f-19","name":"signal.ts"},{"uid":"3d8fcc8f-21","name":"store.ts"},{"uid":"3d8fcc8f-23","name":"reconcile.ts"},{"uid":"3d8fcc8f-25","name":"resource.ts"},{"uid":"3d8fcc8f-27","name":"watch.ts"},{"uid":"3d8fcc8f-29","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"3d8fcc8f-1":{"renderedLength":3016,"gzipLength":1167,"brotliLength":0,"metaUid":"3d8fcc8f-0"},"3d8fcc8f-3":{"renderedLength":1636,"gzipLength":786,"brotliLength":0,"metaUid":"3d8fcc8f-2"},"3d8fcc8f-5":{"renderedLength":3026,"gzipLength":1226,"brotliLength":0,"metaUid":"3d8fcc8f-4"},"3d8fcc8f-7":{"renderedLength":2227,"gzipLength":858,"brotliLength":0,"metaUid":"3d8fcc8f-6"},"3d8fcc8f-9":{"renderedLength":7391,"gzipLength":2397,"brotliLength":0,"metaUid":"3d8fcc8f-8"},"3d8fcc8f-11":{"renderedLength":4716,"gzipLength":1476,"brotliLength":0,"metaUid":"3d8fcc8f-10"},"3d8fcc8f-13":{"renderedLength":2244,"gzipLength":981,"brotliLength":0,"metaUid":"3d8fcc8f-12"},"3d8fcc8f-15":{"renderedLength":2469,"gzipLength":1092,"brotliLength":0,"metaUid":"3d8fcc8f-14"},"3d8fcc8f-17":{"renderedLength":2721,"gzipLength":1363,"brotliLength":0,"metaUid":"3d8fcc8f-16"},"3d8fcc8f-19":{"renderedLength":3408,"gzipLength":1476,"brotliLength":0,"metaUid":"3d8fcc8f-18"},"3d8fcc8f-21":{"renderedLength":5232,"gzipLength":1867,"brotliLength":0,"metaUid":"3d8fcc8f-20"},"3d8fcc8f-23":{"renderedLength":2278,"gzipLength":940,"brotliLength":0,"metaUid":"3d8fcc8f-22"},"3d8fcc8f-25":{"renderedLength":1205,"gzipLength":524,"brotliLength":0,"metaUid":"3d8fcc8f-24"},"3d8fcc8f-27":{"renderedLength":1249,"gzipLength":582,"brotliLength":0,"metaUid":"3d8fcc8f-26"},"3d8fcc8f-29":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"3d8fcc8f-28"}},"nodeMetas":{"3d8fcc8f-0":{"id":"/src/batch.ts","moduleParts":{"index.js":"3d8fcc8f-1"},"imported":[],"importedBy":[{"uid":"3d8fcc8f-28"},{"uid":"3d8fcc8f-10"},{"uid":"3d8fcc8f-18"},{"uid":"3d8fcc8f-6"}]},"3d8fcc8f-2":{"id":"/src/cell.ts","moduleParts":{"index.js":"3d8fcc8f-3"},"imported":[],"importedBy":[{"uid":"3d8fcc8f-28"}]},"3d8fcc8f-4":{"id":"/src/scope.ts","moduleParts":{"index.js":"3d8fcc8f-5"},"imported":[],"importedBy":[{"uid":"3d8fcc8f-28"},{"uid":"3d8fcc8f-10"},{"uid":"3d8fcc8f-8"}]},"3d8fcc8f-6":{"id":"/src/tracking.ts","moduleParts":{"index.js":"3d8fcc8f-7"},"imported":[{"uid":"3d8fcc8f-0"}],"importedBy":[{"uid":"3d8fcc8f-28"},{"uid":"3d8fcc8f-10"},{"uid":"3d8fcc8f-12"},{"uid":"3d8fcc8f-8"},{"uid":"3d8fcc8f-24"},{"uid":"3d8fcc8f-18"}]},"3d8fcc8f-8":{"id":"/src/effect.ts","moduleParts":{"index.js":"3d8fcc8f-9"},"imported":[{"uid":"3d8fcc8f-4"},{"uid":"3d8fcc8f-6"}],"importedBy":[{"uid":"3d8fcc8f-28"},{"uid":"3d8fcc8f-10"},{"uid":"3d8fcc8f-12"},{"uid":"3d8fcc8f-24"},{"uid":"3d8fcc8f-26"}]},"3d8fcc8f-10":{"id":"/src/computed.ts","moduleParts":{"index.js":"3d8fcc8f-11"},"imported":[{"uid":"3d8fcc8f-0"},{"uid":"3d8fcc8f-8"},{"uid":"3d8fcc8f-4"},{"uid":"3d8fcc8f-6"}],"importedBy":[{"uid":"3d8fcc8f-28"}]},"3d8fcc8f-12":{"id":"/src/createSelector.ts","moduleParts":{"index.js":"3d8fcc8f-13"},"imported":[{"uid":"3d8fcc8f-8"},{"uid":"3d8fcc8f-6"}],"importedBy":[{"uid":"3d8fcc8f-28"}]},"3d8fcc8f-14":{"id":"/src/debug.ts","moduleParts":{"index.js":"3d8fcc8f-15"},"imported":[],"importedBy":[{"uid":"3d8fcc8f-28"},{"uid":"3d8fcc8f-18"}]},"3d8fcc8f-16":{"id":"/src/reactive-trace.ts","moduleParts":{"index.js":"3d8fcc8f-17"},"imported":[],"importedBy":[{"uid":"3d8fcc8f-28"},{"uid":"3d8fcc8f-18"}]},"3d8fcc8f-18":{"id":"/src/signal.ts","moduleParts":{"index.js":"3d8fcc8f-19"},"imported":[{"uid":"3d8fcc8f-0"},{"uid":"3d8fcc8f-14"},{"uid":"3d8fcc8f-16"},{"uid":"3d8fcc8f-6"}],"importedBy":[{"uid":"3d8fcc8f-28"},{"uid":"3d8fcc8f-24"},{"uid":"3d8fcc8f-20"}]},"3d8fcc8f-20":{"id":"/src/store.ts","moduleParts":{"index.js":"3d8fcc8f-21"},"imported":[{"uid":"3d8fcc8f-18"}],"importedBy":[{"uid":"3d8fcc8f-28"},{"uid":"3d8fcc8f-22"}]},"3d8fcc8f-22":{"id":"/src/reconcile.ts","moduleParts":{"index.js":"3d8fcc8f-23"},"imported":[{"uid":"3d8fcc8f-20"}],"importedBy":[{"uid":"3d8fcc8f-28"}]},"3d8fcc8f-24":{"id":"/src/resource.ts","moduleParts":{"index.js":"3d8fcc8f-25"},"imported":[{"uid":"3d8fcc8f-8"},{"uid":"3d8fcc8f-18"},{"uid":"3d8fcc8f-6"}],"importedBy":[{"uid":"3d8fcc8f-28"}]},"3d8fcc8f-26":{"id":"/src/watch.ts","moduleParts":{"index.js":"3d8fcc8f-27"},"imported":[{"uid":"3d8fcc8f-8"}],"importedBy":[{"uid":"3d8fcc8f-28"}]},"3d8fcc8f-28":{"id":"/src/index.ts","moduleParts":{"index.js":"3d8fcc8f-29"},"imported":[{"uid":"3d8fcc8f-0"},{"uid":"3d8fcc8f-2"},{"uid":"3d8fcc8f-10"},{"uid":"3d8fcc8f-12"},{"uid":"3d8fcc8f-14"},{"uid":"3d8fcc8f-16"},{"uid":"3d8fcc8f-8"},{"uid":"3d8fcc8f-22"},{"uid":"3d8fcc8f-24"},{"uid":"3d8fcc8f-4"},{"uid":"3d8fcc8f-18"},{"uid":"3d8fcc8f-20"},{"uid":"3d8fcc8f-6"},{"uid":"3d8fcc8f-26"}],"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
|
@@ -615,7 +615,7 @@ function computedLazy(fn) {
|
|
|
615
615
|
if (disposed || dirty) return;
|
|
616
616
|
dirty = true;
|
|
617
617
|
if (host._s) notifySubscribers(host._s);
|
|
618
|
-
if (directFns) for (const f of directFns) f
|
|
618
|
+
if (directFns) for (const f of directFns) f();
|
|
619
619
|
};
|
|
620
620
|
_markRecompute(recompute);
|
|
621
621
|
const read = () => {
|
|
@@ -649,13 +649,16 @@ function computedLazy(fn) {
|
|
|
649
649
|
},
|
|
650
650
|
enumerable: false
|
|
651
651
|
});
|
|
652
|
+
Object.defineProperty(read, "_d", {
|
|
653
|
+
get: () => directFns,
|
|
654
|
+
enumerable: false
|
|
655
|
+
});
|
|
652
656
|
read.direct = (updater) => {
|
|
653
|
-
if (!directFns) directFns =
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
arr.push(updater);
|
|
657
|
+
if (!directFns) directFns = /* @__PURE__ */ new Set();
|
|
658
|
+
const set = directFns;
|
|
659
|
+
set.add(updater);
|
|
657
660
|
return () => {
|
|
658
|
-
|
|
661
|
+
set.delete(updater);
|
|
659
662
|
};
|
|
660
663
|
};
|
|
661
664
|
getCurrentScope()?.add({ dispose: read.dispose });
|
|
@@ -690,7 +693,7 @@ function computedWithEquals(fn, equals) {
|
|
|
690
693
|
return;
|
|
691
694
|
}
|
|
692
695
|
if (host._s) notifySubscribers(host._s);
|
|
693
|
-
if (directFns) for (const f of directFns) f
|
|
696
|
+
if (directFns) for (const f of directFns) f();
|
|
694
697
|
};
|
|
695
698
|
_markRecompute(recompute);
|
|
696
699
|
const read = () => {
|
|
@@ -720,13 +723,16 @@ function computedWithEquals(fn, equals) {
|
|
|
720
723
|
},
|
|
721
724
|
enumerable: false
|
|
722
725
|
});
|
|
726
|
+
Object.defineProperty(read, "_d", {
|
|
727
|
+
get: () => directFns,
|
|
728
|
+
enumerable: false
|
|
729
|
+
});
|
|
723
730
|
read.direct = (updater) => {
|
|
724
|
-
if (!directFns) directFns =
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
arr.push(updater);
|
|
731
|
+
if (!directFns) directFns = /* @__PURE__ */ new Set();
|
|
732
|
+
const set = directFns;
|
|
733
|
+
set.add(updater);
|
|
728
734
|
return () => {
|
|
729
|
-
|
|
735
|
+
set.delete(updater);
|
|
730
736
|
};
|
|
731
737
|
};
|
|
732
738
|
getCurrentScope()?.add({ dispose: read.dispose });
|
|
@@ -909,6 +915,93 @@ function inspectSignal(sig) {
|
|
|
909
915
|
return info;
|
|
910
916
|
}
|
|
911
917
|
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/reactive-trace.ts
|
|
920
|
+
/**
|
|
921
|
+
* Ring-buffer capacity. 50 entries is enough to see the causal chain
|
|
922
|
+
* for a crash (the writes in the few ticks before the throw) without
|
|
923
|
+
* the buffer itself becoming a memory concern — each entry is a small
|
|
924
|
+
* object with two short strings.
|
|
925
|
+
*/
|
|
926
|
+
const CAP = 50;
|
|
927
|
+
let _buf = null;
|
|
928
|
+
let _count = 0;
|
|
929
|
+
/** Max characters of a value preview before truncation. Keeps the buffer + serialized report small. */
|
|
930
|
+
const PREVIEW_MAX = 80;
|
|
931
|
+
/**
|
|
932
|
+
* Safe, bounded stringification. Never throws (a getter or `toJSON`
|
|
933
|
+
* that throws must not break the trace recorder), never returns more
|
|
934
|
+
* than `PREVIEW_MAX` chars + an ellipsis marker.
|
|
935
|
+
*/
|
|
936
|
+
function preview(v) {
|
|
937
|
+
let s;
|
|
938
|
+
try {
|
|
939
|
+
if (v === null) return "null";
|
|
940
|
+
if (v === void 0) return "undefined";
|
|
941
|
+
const t = typeof v;
|
|
942
|
+
if (t === "string") s = JSON.stringify(v);
|
|
943
|
+
else if (t === "number" || t === "boolean" || t === "bigint") s = String(v);
|
|
944
|
+
else if (t === "function") s = `[Function ${v.name || "anonymous"}]`;
|
|
945
|
+
else if (t === "symbol") s = v.toString();
|
|
946
|
+
else if (Array.isArray(v)) s = `Array(${v.length})`;
|
|
947
|
+
else {
|
|
948
|
+
const ctor = v.constructor?.name;
|
|
949
|
+
const keys = (() => {
|
|
950
|
+
try {
|
|
951
|
+
return Object.keys(v).slice(0, 4);
|
|
952
|
+
} catch {
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
})();
|
|
956
|
+
s = `${ctor && ctor !== "Object" ? ctor + " " : ""}{${keys.join(", ")}${keys.length === 4 ? ", …" : ""}}`;
|
|
957
|
+
}
|
|
958
|
+
} catch {
|
|
959
|
+
s = "[unstringifiable]";
|
|
960
|
+
}
|
|
961
|
+
return s.length > PREVIEW_MAX ? s.slice(0, PREVIEW_MAX) + "…" : s;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Record one signal write. Called from `signal.ts` `_set`, already
|
|
965
|
+
* inside the prod-gate, so this never runs in production builds.
|
|
966
|
+
*
|
|
967
|
+
* @internal
|
|
968
|
+
*/
|
|
969
|
+
function _recordSignalWrite(name, prev, next) {
|
|
970
|
+
if (_buf === null) _buf = new Array(CAP);
|
|
971
|
+
_buf[_count % CAP] = {
|
|
972
|
+
name,
|
|
973
|
+
prev: preview(prev),
|
|
974
|
+
next: preview(next),
|
|
975
|
+
timestamp: typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now()
|
|
976
|
+
};
|
|
977
|
+
_count++;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Returns the recorded writes in chronological order (oldest → newest),
|
|
981
|
+
* at most `CAP` entries. Empty array when nothing has been recorded.
|
|
982
|
+
* The returned array is a fresh copy — safe to retain / serialize
|
|
983
|
+
* without pinning the ring buffer.
|
|
984
|
+
*
|
|
985
|
+
* Consumed by `@pyreon/core`'s `reportError` to attach `reactiveTrace`
|
|
986
|
+
* to the error context.
|
|
987
|
+
*/
|
|
988
|
+
function getReactiveTrace() {
|
|
989
|
+
if (_buf === null || _count === 0) return [];
|
|
990
|
+
if (_count <= CAP) return _buf.slice(0, _count);
|
|
991
|
+
const start = _count % CAP;
|
|
992
|
+
const out = [];
|
|
993
|
+
for (let i = 0; i < CAP; i++) {
|
|
994
|
+
const e = _buf[(start + i) % CAP];
|
|
995
|
+
if (e) out.push(e);
|
|
996
|
+
}
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
/** Clears the buffer. For test isolation; not part of the app-facing API. */
|
|
1000
|
+
function clearReactiveTrace() {
|
|
1001
|
+
_buf = null;
|
|
1002
|
+
_count = 0;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
912
1005
|
//#endregion
|
|
913
1006
|
//#region src/signal.ts
|
|
914
1007
|
const _countSink = globalThis;
|
|
@@ -920,6 +1013,7 @@ function _set(newValue) {
|
|
|
920
1013
|
if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("reactivity.signalWrite");
|
|
921
1014
|
const prev = this._v;
|
|
922
1015
|
this._v = newValue;
|
|
1016
|
+
if (process.env.NODE_ENV !== "production") _recordSignalWrite(this.label, prev, newValue);
|
|
923
1017
|
if (isTracing()) try {
|
|
924
1018
|
_notifyTraceListeners(this, prev, newValue);
|
|
925
1019
|
} catch (err) {
|
|
@@ -943,28 +1037,33 @@ function _subscribe(listener) {
|
|
|
943
1037
|
}
|
|
944
1038
|
/**
|
|
945
1039
|
* Register a direct updater — lighter than subscribe().
|
|
946
|
-
* Uses a flat array instead of Set. Disposal nulls the slot (no Set.delete overhead).
|
|
947
1040
|
* Used by compiler-emitted _bindText/_bindDirect for zero-overhead DOM bindings.
|
|
1041
|
+
*
|
|
1042
|
+
* Backed by a `Set` (same as `_s`), NOT a flat array. The array form
|
|
1043
|
+
* disposed by nulling the slot (`arr[idx] = null`) but never compacted —
|
|
1044
|
+
* so a long-lived signal (theme/locale/auth, or a signal read inside
|
|
1045
|
+
* `<For>` rows) bound by churning components accumulated one permanent
|
|
1046
|
+
* dead slot per ever-mounted binding. That is an app-lifetime memory
|
|
1047
|
+
* leak AND degrades the signal-write hot path: `notifyDirect` iterated
|
|
1048
|
+
* O(total-ever-registered), not O(live). A Set bounds growth to the live
|
|
1049
|
+
* set and keeps disposal + iteration O(live); the "Set.delete overhead"
|
|
1050
|
+
* the array form optimised for is negligible against an unbounded array.
|
|
948
1051
|
*/
|
|
949
1052
|
function _directFn(updater) {
|
|
950
|
-
if (!this._d) this._d =
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
arr.push(updater);
|
|
1053
|
+
if (!this._d) this._d = /* @__PURE__ */ new Set();
|
|
1054
|
+
const set = this._d;
|
|
1055
|
+
set.add(updater);
|
|
954
1056
|
return () => {
|
|
955
|
-
|
|
1057
|
+
set.delete(updater);
|
|
956
1058
|
};
|
|
957
1059
|
}
|
|
958
1060
|
/**
|
|
959
|
-
* Notify direct updaters —
|
|
960
|
-
*
|
|
1061
|
+
* Notify direct updaters — set iteration, batch-aware. Disposed updaters
|
|
1062
|
+
* are already absent from the set (O(1) delete on disposal).
|
|
961
1063
|
*/
|
|
962
1064
|
function notifyDirect(updaters) {
|
|
963
|
-
if (isBatching()) for (
|
|
964
|
-
|
|
965
|
-
if (fn) enqueuePendingNotification(fn);
|
|
966
|
-
}
|
|
967
|
-
else for (let i = 0; i < updaters.length; i++) updaters[i]?.();
|
|
1065
|
+
if (isBatching()) for (const fn of updaters) enqueuePendingNotification(fn);
|
|
1066
|
+
else for (const fn of updaters) fn();
|
|
968
1067
|
}
|
|
969
1068
|
function _debug() {
|
|
970
1069
|
return {
|
|
@@ -1111,6 +1210,7 @@ function wrap(raw, shallow) {
|
|
|
1111
1210
|
target[key] = value;
|
|
1112
1211
|
return true;
|
|
1113
1212
|
}
|
|
1213
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") return true;
|
|
1114
1214
|
const prevLength = isArray ? target.length : 0;
|
|
1115
1215
|
target[key] = value;
|
|
1116
1216
|
if (isArray && key === "length") {
|
|
@@ -1164,6 +1264,11 @@ function wrap(raw, shallow) {
|
|
|
1164
1264
|
* Arrays are reconciled by index — elements at the same index are recursively
|
|
1165
1265
|
* diffed rather than replaced wholesale. Excess old elements are removed.
|
|
1166
1266
|
*/
|
|
1267
|
+
const DANGEROUS_KEYS = new Set([
|
|
1268
|
+
"__proto__",
|
|
1269
|
+
"constructor",
|
|
1270
|
+
"prototype"
|
|
1271
|
+
]);
|
|
1167
1272
|
function reconcile(source, target) {
|
|
1168
1273
|
_reconcileInner(source, target, /* @__PURE__ */ new WeakSet());
|
|
1169
1274
|
}
|
|
@@ -1188,6 +1293,7 @@ function _reconcileObject(source, target, seen) {
|
|
|
1188
1293
|
const sourceKeys = Object.keys(source);
|
|
1189
1294
|
const targetKeys = new Set(Object.keys(target));
|
|
1190
1295
|
for (const key of sourceKeys) {
|
|
1296
|
+
if (DANGEROUS_KEYS.has(key)) continue;
|
|
1191
1297
|
const sv = source[key];
|
|
1192
1298
|
const tv = target[key];
|
|
1193
1299
|
if (sv !== null && typeof sv === "object" && tv !== null && typeof tv === "object") if (isStore(tv)) _reconcileInner(sv, tv, seen);
|
|
@@ -1195,7 +1301,10 @@ function _reconcileObject(source, target, seen) {
|
|
|
1195
1301
|
else target[key] = sv;
|
|
1196
1302
|
targetKeys.delete(key);
|
|
1197
1303
|
}
|
|
1198
|
-
for (const key of targetKeys)
|
|
1304
|
+
for (const key of targetKeys) {
|
|
1305
|
+
if (DANGEROUS_KEYS.has(key)) continue;
|
|
1306
|
+
delete target[key];
|
|
1307
|
+
}
|
|
1199
1308
|
}
|
|
1200
1309
|
|
|
1201
1310
|
//#endregion
|
|
@@ -1307,5 +1416,5 @@ function watch(source, callback, opts = {}) {
|
|
|
1307
1416
|
}
|
|
1308
1417
|
|
|
1309
1418
|
//#endregion
|
|
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 };
|
|
1419
|
+
export { Cell, EffectScope, _bind, batch, cell, clearReactiveTrace, computed, createResource, createSelector, createStore, effect, effectScope, getCurrentScope, getReactiveTrace, inspectSignal, isStore, markRaw, nextTick, onCleanup, onScopeDispose, onSignalUpdate, reconcile, renderEffect, runUntracked, runUntracked as untrack, setCurrentScope, setErrorHandler, setSnapshotCapture, shallowReactive, signal, watch, why };
|
|
1311
1420
|
//# sourceMappingURL=index.js.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -133,9 +133,9 @@ interface Signal<T> {
|
|
|
133
133
|
subscribe(listener: () => void): () => void;
|
|
134
134
|
/**
|
|
135
135
|
* Register a direct updater — even lighter than subscribe().
|
|
136
|
-
* Uses a flat array instead of Set. Disposal nulls the slot (no Set.delete).
|
|
137
136
|
* Intended for compiler-emitted DOM bindings (_bindText, _bindDirect).
|
|
138
|
-
* Returns a disposer that
|
|
137
|
+
* Returns a disposer that removes the updater (O(1)); the live set
|
|
138
|
+
* stays bounded under register/dispose churn.
|
|
139
139
|
*/
|
|
140
140
|
direct(updater: () => void): () => void;
|
|
141
141
|
/**
|
|
@@ -209,6 +209,56 @@ declare function why(): void;
|
|
|
209
209
|
*/
|
|
210
210
|
declare function inspectSignal<T>(sig: Signal<T>): SignalDebugInfo<T>;
|
|
211
211
|
//#endregion
|
|
212
|
+
//#region src/reactive-trace.d.ts
|
|
213
|
+
/**
|
|
214
|
+
* Reactive trace — a bounded, dev-only ring buffer of recent signal
|
|
215
|
+
* writes. When a signal-based UI throws, the single most useful
|
|
216
|
+
* debugging question is "what reactive state changed in the run-up to
|
|
217
|
+
* the crash" — a point-in-time snapshot of every signal value can't
|
|
218
|
+
* answer that (it shows the end state, not the causal sequence). The
|
|
219
|
+
* ring buffer records the last N writes so an error report can attach
|
|
220
|
+
* the sequence that led into the bad state.
|
|
221
|
+
*
|
|
222
|
+
* Design constraints:
|
|
223
|
+
*
|
|
224
|
+
* - **Bounded memory.** Fixed-size circular buffer (`CAP` entries).
|
|
225
|
+
* Never grows. Old entries overwrite oldest-first.
|
|
226
|
+
* - **No value retention.** Stores a TRUNCATED STRING preview of
|
|
227
|
+
* prev / next, never the raw value. Holding raw references would
|
|
228
|
+
* retain large arrays / detached DOM / closures in the buffer and
|
|
229
|
+
* leak them for the buffer's lifetime. The preview is also what
|
|
230
|
+
* makes the trace safely serializable into an error report.
|
|
231
|
+
* - **Cheap.** No stack capture (that's the expensive part of the
|
|
232
|
+
* `onSignalUpdate` debug path — this is deliberately lighter so it
|
|
233
|
+
* can record every write in dev without a perf cost). One object
|
|
234
|
+
* literal + one array slot write per signal write.
|
|
235
|
+
* - **Zero production cost.** The single call site in `signal.ts`
|
|
236
|
+
* is inside the existing `process.env.NODE_ENV !== 'production'`
|
|
237
|
+
* gate, so the whole module tree-shakes out of prod bundles.
|
|
238
|
+
*/
|
|
239
|
+
interface ReactiveTraceEntry {
|
|
240
|
+
/** Signal `label` (set via `signal(v, { name })` or the vite plugin's dev auto-naming). `undefined` for anonymous signals. */
|
|
241
|
+
name: string | undefined;
|
|
242
|
+
/** Bounded string preview of the value before the write. */
|
|
243
|
+
prev: string;
|
|
244
|
+
/** Bounded string preview of the value after the write. */
|
|
245
|
+
next: string;
|
|
246
|
+
/** `performance.now()` at write time (monotonic; survives clock changes). */
|
|
247
|
+
timestamp: number;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Returns the recorded writes in chronological order (oldest → newest),
|
|
251
|
+
* at most `CAP` entries. Empty array when nothing has been recorded.
|
|
252
|
+
* The returned array is a fresh copy — safe to retain / serialize
|
|
253
|
+
* without pinning the ring buffer.
|
|
254
|
+
*
|
|
255
|
+
* Consumed by `@pyreon/core`'s `reportError` to attach `reactiveTrace`
|
|
256
|
+
* to the error context.
|
|
257
|
+
*/
|
|
258
|
+
declare function getReactiveTrace(): ReactiveTraceEntry[];
|
|
259
|
+
/** Clears the buffer. For test isolation; not part of the app-facing API. */
|
|
260
|
+
declare function clearReactiveTrace(): void;
|
|
261
|
+
//#endregion
|
|
212
262
|
//#region src/effect.d.ts
|
|
213
263
|
interface Effect {
|
|
214
264
|
dispose(): void;
|
|
@@ -465,5 +515,5 @@ interface WatchOptions {
|
|
|
465
515
|
*/
|
|
466
516
|
declare function watch<T>(source: () => T, callback: (newVal: T, oldVal: T | undefined) => void | (() => void), opts?: WatchOptions): () => void;
|
|
467
517
|
//#endregion
|
|
468
|
-
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, markRaw, nextTick, onCleanup, onScopeDispose, onSignalUpdate, reconcile, renderEffect, runUntracked, runUntracked as untrack, setCurrentScope, setErrorHandler, setSnapshotCapture, shallowReactive, signal, watch, why };
|
|
518
|
+
export { Cell, type Computed, type ComputedOptions, type Effect, EffectScope, type ReactiveSnapshotCapture, type ReactiveTraceEntry, type ReadonlySignal, type Resource, type Signal, type SignalDebugInfo, type SignalOptions, type WatchOptions, _bind, batch, cell, clearReactiveTrace, computed, createResource, createSelector, createStore, effect, effectScope, getCurrentScope, getReactiveTrace, inspectSignal, isStore, markRaw, nextTick, onCleanup, onScopeDispose, onSignalUpdate, reconcile, renderEffect, runUntracked, runUntracked as untrack, setCurrentScope, setErrorHandler, setSnapshotCapture, shallowReactive, signal, watch, why };
|
|
469
519
|
//# sourceMappingURL=index2.d.ts.map
|
package/package.json
CHANGED
package/src/computed.ts
CHANGED
|
@@ -87,13 +87,21 @@ function computedLazy<T>(fn: () => T): Computed<T> {
|
|
|
87
87
|
let tracked = false
|
|
88
88
|
const deps: Set<() => void>[] = []
|
|
89
89
|
const host: { _s: Set<() => void> | null } = { _s: null }
|
|
90
|
-
|
|
90
|
+
// Set, not a never-compacted flat array. The array form's disposal
|
|
91
|
+
// only nulled the slot (`arr[idx] = null`) and never shrank, so a
|
|
92
|
+
// long-lived computed (a derived theme/locale/auth value, or one read
|
|
93
|
+
// inside churning `<For>` rows) bound by mount/unmount churn grew one
|
|
94
|
+
// permanent dead slot per ever-registered binding — app-lifetime
|
|
95
|
+
// memory growth AND `recompute` iterating O(total-ever) instead of
|
|
96
|
+
// O(live). Identical bug class already fixed for `signal._d`
|
|
97
|
+
// (signal.ts `_directFn`); `computed` was left on the broken pattern.
|
|
98
|
+
let directFns: Set<() => void> | null = null
|
|
91
99
|
|
|
92
100
|
const recompute = () => {
|
|
93
101
|
if (disposed || dirty) return
|
|
94
102
|
dirty = true
|
|
95
103
|
if (host._s) notifySubscribers(host._s)
|
|
96
|
-
if (directFns) for (const f of directFns) f
|
|
104
|
+
if (directFns) for (const f of directFns) f()
|
|
97
105
|
}
|
|
98
106
|
_markRecompute(recompute)
|
|
99
107
|
|
|
@@ -136,13 +144,20 @@ function computedLazy<T>(fn: () => T): Computed<T> {
|
|
|
136
144
|
enumerable: false,
|
|
137
145
|
})
|
|
138
146
|
|
|
147
|
+
// @internal — mirrors `signal._d`. Lets tests deterministically assert
|
|
148
|
+
// the live direct-updater set stays BOUNDED under register/dispose
|
|
149
|
+
// churn (the never-compacted-array leak this fix removes).
|
|
150
|
+
Object.defineProperty(read, '_d', {
|
|
151
|
+
get: () => directFns,
|
|
152
|
+
enumerable: false,
|
|
153
|
+
})
|
|
154
|
+
|
|
139
155
|
read.direct = (updater: () => void): (() => void) => {
|
|
140
|
-
if (!directFns) directFns =
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
arr.push(updater)
|
|
156
|
+
if (!directFns) directFns = new Set()
|
|
157
|
+
const set = directFns
|
|
158
|
+
set.add(updater)
|
|
144
159
|
return () => {
|
|
145
|
-
|
|
160
|
+
set.delete(updater)
|
|
146
161
|
}
|
|
147
162
|
}
|
|
148
163
|
|
|
@@ -163,7 +178,15 @@ function computedWithEquals<T>(fn: () => T, equals: (prev: T, next: T) => boolea
|
|
|
163
178
|
let disposed = false
|
|
164
179
|
const deps: Set<() => void>[] = []
|
|
165
180
|
const host: { _s: Set<() => void> | null } = { _s: null }
|
|
166
|
-
|
|
181
|
+
// Set, not a never-compacted flat array. The array form's disposal
|
|
182
|
+
// only nulled the slot (`arr[idx] = null`) and never shrank, so a
|
|
183
|
+
// long-lived computed (a derived theme/locale/auth value, or one read
|
|
184
|
+
// inside churning `<For>` rows) bound by mount/unmount churn grew one
|
|
185
|
+
// permanent dead slot per ever-registered binding — app-lifetime
|
|
186
|
+
// memory growth AND `recompute` iterating O(total-ever) instead of
|
|
187
|
+
// O(live). Identical bug class already fixed for `signal._d`
|
|
188
|
+
// (signal.ts `_directFn`); `computed` was left on the broken pattern.
|
|
189
|
+
let directFns: Set<() => void> | null = null
|
|
167
190
|
|
|
168
191
|
const recompute = () => {
|
|
169
192
|
if (disposed) return
|
|
@@ -181,7 +204,7 @@ function computedWithEquals<T>(fn: () => T, equals: (prev: T, next: T) => boolea
|
|
|
181
204
|
return
|
|
182
205
|
}
|
|
183
206
|
if (host._s) notifySubscribers(host._s)
|
|
184
|
-
if (directFns) for (const f of directFns) f
|
|
207
|
+
if (directFns) for (const f of directFns) f()
|
|
185
208
|
}
|
|
186
209
|
_markRecompute(recompute)
|
|
187
210
|
|
|
@@ -216,13 +239,20 @@ function computedWithEquals<T>(fn: () => T, equals: (prev: T, next: T) => boolea
|
|
|
216
239
|
enumerable: false,
|
|
217
240
|
})
|
|
218
241
|
|
|
242
|
+
// @internal — mirrors `signal._d`. Lets tests deterministically assert
|
|
243
|
+
// the live direct-updater set stays BOUNDED under register/dispose
|
|
244
|
+
// churn (the never-compacted-array leak this fix removes).
|
|
245
|
+
Object.defineProperty(read, '_d', {
|
|
246
|
+
get: () => directFns,
|
|
247
|
+
enumerable: false,
|
|
248
|
+
})
|
|
249
|
+
|
|
219
250
|
read.direct = (updater: () => void): (() => void) => {
|
|
220
|
-
if (!directFns) directFns =
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
arr.push(updater)
|
|
251
|
+
if (!directFns) directFns = new Set()
|
|
252
|
+
const set = directFns
|
|
253
|
+
set.add(updater)
|
|
224
254
|
return () => {
|
|
225
|
-
|
|
255
|
+
set.delete(updater)
|
|
226
256
|
}
|
|
227
257
|
}
|
|
228
258
|
|
package/src/effect.ts
CHANGED
|
@@ -102,6 +102,11 @@ interface PyreonErrorBridge {
|
|
|
102
102
|
const _errorBridge = globalThis as PyreonErrorBridge
|
|
103
103
|
|
|
104
104
|
function _defaultErrorHandler(err: unknown): void {
|
|
105
|
+
// Last-resort unhandled-effect-error reporter — MUST fire in
|
|
106
|
+
// production (silently swallowing uncaught effect errors is a
|
|
107
|
+
// serious bug; React/Vue/Solid all log uncaught errors in prod).
|
|
108
|
+
// Deliberately not __DEV__-gated.
|
|
109
|
+
// pyreon-lint-disable-next-line pyreon/dev-guard-warnings
|
|
105
110
|
console.error('[pyreon] Unhandled effect error:', err)
|
|
106
111
|
}
|
|
107
112
|
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export { Cell, cell } from './cell'
|
|
|
5
5
|
export { type Computed, type ComputedOptions, computed } from './computed'
|
|
6
6
|
export { createSelector } from './createSelector'
|
|
7
7
|
export { inspectSignal, onSignalUpdate, why } from './debug'
|
|
8
|
+
export type { ReactiveTraceEntry } from './reactive-trace'
|
|
9
|
+
export { clearReactiveTrace, getReactiveTrace } from './reactive-trace'
|
|
8
10
|
export {
|
|
9
11
|
_bind,
|
|
10
12
|
type Effect,
|
package/src/manifest.ts
CHANGED
|
@@ -58,7 +58,7 @@ effect(() => {
|
|
|
58
58
|
'createStore() / reconcile() / isStore() — deeply reactive proxy stores + structural diff',
|
|
59
59
|
'effectScope() / getCurrentScope() — scope-based lifecycle management',
|
|
60
60
|
'untrack() — read without subscribing',
|
|
61
|
-
'onSignalUpdate() / inspectSignal() / why() — debug instrumentation',
|
|
61
|
+
'onSignalUpdate() / inspectSignal() / why() / getReactiveTrace() — debug instrumentation',
|
|
62
62
|
'setErrorHandler() — global hook for unhandled effect errors',
|
|
63
63
|
'Standalone — zero DOM, zero JSX, zero framework dependency',
|
|
64
64
|
],
|
|
@@ -553,6 +553,27 @@ why() // disarm + dump transcript:
|
|
|
553
553
|
],
|
|
554
554
|
seeAlso: ['onSignalUpdate', 'inspectSignal'],
|
|
555
555
|
},
|
|
556
|
+
{
|
|
557
|
+
name: 'getReactiveTrace',
|
|
558
|
+
kind: 'function',
|
|
559
|
+
signature:
|
|
560
|
+
'() => Array<{ name: string | undefined; prev: string; next: string; timestamp: number }>',
|
|
561
|
+
summary:
|
|
562
|
+
'Returns the last ~50 signal writes (chronological, oldest → newest) from a bounded dev-only ring buffer — the causal SEQUENCE of reactive state changes, not a point-in-time snapshot. `@pyreon/core` attaches this to `ErrorContext.reactiveTrace` automatically so error reports carry "what changed in the run-up to the crash". Entries hold bounded string previews of values (never raw refs — no memory pinning, always serializable). **Dev-only**: the recorder feeding the buffer is behind the production dead-code gate and tree-shakes out, so this returns `[]` in prod builds. Distinct from `onSignalUpdate` — that is opt-in and captures stacks; this is always-on, deliberately cheap, and exists to enrich error reports. `clearReactiveTrace()` resets it (test isolation).',
|
|
563
|
+
example: `import { getReactiveTrace, clearReactiveTrace, signal } from '@pyreon/reactivity'
|
|
564
|
+
|
|
565
|
+
const status = signal('idle', { name: 'status' })
|
|
566
|
+
status.set('submitting')
|
|
567
|
+
getReactiveTrace()
|
|
568
|
+
// [{ name: 'status', prev: '"idle"', next: '"submitting"', timestamp: 1234.5 }]
|
|
569
|
+
clearReactiveTrace() // → []`,
|
|
570
|
+
mistakes: [
|
|
571
|
+
'Expecting it to return signal VALUES — it returns string PREVIEWS (truncated, safely stringified). For live values inspect the signal directly',
|
|
572
|
+
'Relying on it in production — returns `[]` (the recorder is dev-gated and tree-shaken). Use it for dev tooling / error-report enrichment, not runtime logic',
|
|
573
|
+
'Treating it as a snapshot of all signals — it is a bounded ring of recent WRITES; signals never written (or written before the ~50-entry window) are absent',
|
|
574
|
+
],
|
|
575
|
+
seeAlso: ['onSignalUpdate', 'inspectSignal'],
|
|
576
|
+
},
|
|
556
577
|
{
|
|
557
578
|
name: 'setErrorHandler',
|
|
558
579
|
kind: 'function',
|