@vielzeug/ripple 1.2.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 +101 -0
- package/dist/_dev.cjs +2 -0
- package/dist/_dev.cjs.map +1 -0
- package/dist/_dev.d.ts +2 -0
- package/dist/_dev.d.ts.map +1 -0
- package/dist/_dev.js +9 -0
- package/dist/_dev.js.map +1 -0
- package/dist/_error-utils.cjs +2 -0
- package/dist/_error-utils.cjs.map +1 -0
- package/dist/_error-utils.d.ts +24 -0
- package/dist/_error-utils.d.ts.map +1 -0
- package/dist/_error-utils.js +20 -0
- package/dist/_error-utils.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +7 -0
- package/dist/async-computed.cjs +2 -0
- package/dist/async-computed.cjs.map +1 -0
- package/dist/async-computed.d.ts +36 -0
- package/dist/async-computed.d.ts.map +1 -0
- package/dist/async-computed.js +58 -0
- package/dist/async-computed.js.map +1 -0
- package/dist/computed.cjs +2 -0
- package/dist/computed.cjs.map +1 -0
- package/dist/computed.d.ts +26 -0
- package/dist/computed.d.ts.map +1 -0
- package/dist/computed.js +113 -0
- package/dist/computed.js.map +1 -0
- package/dist/devtools-hook.cjs +2 -0
- package/dist/devtools-hook.cjs.map +1 -0
- package/dist/devtools-hook.d.ts +4 -0
- package/dist/devtools-hook.d.ts.map +1 -0
- package/dist/devtools-hook.js +8 -0
- package/dist/devtools-hook.js.map +1 -0
- package/dist/devtools.d.ts +39 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/effect.cjs +2 -0
- package/dist/effect.cjs.map +1 -0
- package/dist/effect.d.ts +104 -0
- package/dist/effect.d.ts.map +1 -0
- package/dist/effect.js +173 -0
- package/dist/effect.js.map +1 -0
- package/dist/errors.cjs +2 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/reactive-base.cjs +2 -0
- package/dist/reactive-base.cjs.map +1 -0
- package/dist/reactive-base.d.ts +60 -0
- package/dist/reactive-base.d.ts.map +1 -0
- package/dist/reactive-base.js +56 -0
- package/dist/reactive-base.js.map +1 -0
- package/dist/ripple.cjs +2 -0
- package/dist/ripple.cjs.map +1 -0
- package/dist/ripple.iife.js +2 -0
- package/dist/ripple.iife.js.map +1 -0
- package/dist/ripple.js +2 -0
- package/dist/ripple.js.map +1 -0
- package/dist/scheduling.cjs +2 -0
- package/dist/scheduling.cjs.map +1 -0
- package/dist/scheduling.d.ts +5 -0
- package/dist/scheduling.d.ts.map +1 -0
- package/dist/scheduling.js +57 -0
- package/dist/scheduling.js.map +1 -0
- package/dist/signal.cjs +2 -0
- package/dist/signal.cjs.map +1 -0
- package/dist/signal.d.ts +24 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +54 -0
- package/dist/signal.js.map +1 -0
- package/dist/src/devtools.cjs +2 -0
- package/dist/src/devtools.cjs.map +1 -0
- package/dist/src/devtools.js +36 -0
- package/dist/src/devtools.js.map +1 -0
- package/dist/src/ssr/index.cjs +2 -0
- package/dist/src/ssr/index.cjs.map +1 -0
- package/dist/src/ssr/index.js +38 -0
- package/dist/src/ssr/index.js.map +1 -0
- package/dist/ssr/index.d.ts +56 -0
- package/dist/ssr/index.d.ts.map +1 -0
- package/dist/store-history.cjs +2 -0
- package/dist/store-history.cjs.map +1 -0
- package/dist/store-history.d.ts +42 -0
- package/dist/store-history.d.ts.map +1 -0
- package/dist/store-history.js +60 -0
- package/dist/store-history.js.map +1 -0
- package/dist/store.cjs +2 -0
- package/dist/store.cjs.map +1 -0
- package/dist/store.d.ts +45 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +204 -0
- package/dist/store.js.map +1 -0
- package/dist/subscription.cjs +2 -0
- package/dist/subscription.cjs.map +1 -0
- package/dist/subscription.d.ts +22 -0
- package/dist/subscription.d.ts.map +1 -0
- package/dist/subscription.js +47 -0
- package/dist/subscription.js.map +1 -0
- package/dist/symbols.cjs +2 -0
- package/dist/symbols.cjs.map +1 -0
- package/dist/symbols.d.ts +6 -0
- package/dist/symbols.d.ts.map +1 -0
- package/dist/symbols.js +6 -0
- package/dist/symbols.js.map +1 -0
- package/dist/tracking.cjs +2 -0
- package/dist/tracking.cjs.map +1 -0
- package/dist/tracking.d.ts +82 -0
- package/dist/tracking.d.ts.map +1 -0
- package/dist/tracking.js +76 -0
- package/dist/tracking.js.map +1 -0
- package/dist/types.d.ts +315 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utilities.cjs +2 -0
- package/dist/utilities.cjs.map +1 -0
- package/dist/utilities.d.ts +55 -0
- package/dist/utilities.d.ts.map +1 -0
- package/dist/utilities.js +25 -0
- package/dist/utilities.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduling.cjs","names":[],"sources":["../src/scheduling.ts"],"sourcesContent":["import type { ComputedBase, ReactiveBase } from './reactive-base';\nimport type { Subscriber } from './types';\n\nimport { warn } from './_dev';\nimport { runAll } from './_error-utils';\nimport { RippleInfiniteLoopError } from './errors';\nimport { getSchedulingState, hasContextHook, type SchedulingState } from './tracking';\n\nexport const DEFAULT_MAX_ITERATIONS = 100;\n\n// batchDepth, pendingSubscribers, and the dirty-computed sets live in `SchedulingState`\n// (see tracking.ts) rather than as module-level variables — this lets the /ripple/ssr\n// sub-path isolate the flush queue per request (via the same AsyncLocalStorage-backed\n// context hook that already isolates tracking), closing the gap where concurrent SSR\n// requests previously shared one flush queue. Outside SSR (no hook installed), this is\n// a single module-level singleton with identical behavior and cost to before.\nlet ssrWarnFired = false;\n\n// ── F1: Simplified flush — no topological sort ─────────────────────────────\n//\n// Push phase (propagateDirty):\n// Mark all downstream computeds dirty in a BFS pass.\n// Directly collect effect subscribers of the changed signal.\n// For computeds with direct effect subscribers, defer to the pull phase.\n//\n// Pull phase (recomputeWithEffectSubs):\n// For each dirty computed that has direct effect subscribers, recompute it.\n// If the value changed, queue its effect subscribers.\n// Any further downstream dirty computeds with effect subs are handled in the\n// same loop (re-queued into dirtyWithEffectSubs if they become dirty during\n// a parent recompute).\n//\n// Effect phase: run all queued effect subscribers.\n//\n// This eliminates the O(n²) topo sort while preserving the equality-suppression\n// guarantee: effect subs of a computed are only notified when the computed's\n// value actually changes.\n//\n// Computeds without direct effect subs (pure lazy nodes) are still pulled\n// lazily when an effect reads them during re-run.\n\n// Module-level scratch state — safe to share even under SSR isolation. Both are only\n// touched within a single synchronous call stack (no `await` inside propagateDirty /\n// recomputeWithEffectSubs / flushEffects), which JS's run-to-completion model already\n// isolates; they never carry information across two separate top-level signal writes.\nlet _propEpoch = 0;\nconst _propagateStack: ComputedBase<unknown>[] = [];\n\n/** Returns (and possibly swaps) the currently-active dirty-with-effect-subs set. */\nconst activeDirtySet = (s: SchedulingState): Set<ComputedBase<unknown>> =>\n s.activeDirty === 'a' ? s.dirtyWithEffectSubsA : s.dirtyWithEffectSubsB;\n\nconst propagateDirty = (\n s: SchedulingState,\n node: {\n computedSubs(): Generator<ComputedBase<unknown>>;\n effectSubs(): ReadonlySet<Subscriber>;\n },\n): void => {\n const epoch = ++_propEpoch;\n\n // Queue direct effect subs of the changed node\n for (const sub of node.effectSubs()) {\n s.pendingSubscribers.add(sub);\n }\n\n for (const c of node.computedSubs()) {\n _propagateStack.push(c);\n }\n\n try {\n while (_propagateStack.length > 0) {\n const c = _propagateStack.pop()!;\n\n if (c.lastPropEpoch_ === epoch) continue;\n\n c.lastPropEpoch_ = epoch;\n\n if (!c.markDirty()) continue; // already dirty — already processed\n\n // If this computed has effect subs, defer recompute to the pull phase.\n // We cannot queue them immediately because the computed might not change.\n if (c.effectSubs().size > 0) {\n activeDirtySet(s).add(c);\n }\n\n for (const downstream of c.computedSubs()) {\n _propagateStack.push(downstream);\n }\n }\n } finally {\n _propagateStack.length = 0;\n }\n};\n\nconst recomputeWithEffectSubs = (s: SchedulingState): void => {\n // Iterate until stable — recomputing one computed may dirty others.\n // Set-swap pattern: swap active/idle sets each pass so new dirty nodes accumulate\n // in the idle set while we iterate the current batch — zero array allocations.\n while (activeDirtySet(s).size > 0) {\n const toProcess = activeDirtySet(s);\n\n // Swap: new dirty nodes from this iteration go into the other set\n s.activeDirty = s.activeDirty === 'a' ? 'b' : 'a';\n activeDirtySet(s).clear();\n\n for (const c of toProcess) {\n if (!c.hasSubscribers()) continue;\n\n const changed = c.refreshIfDirty();\n\n if (!changed) continue;\n\n // Value changed — queue effect subs\n for (const sub of c.effectSubs()) {\n s.pendingSubscribers.add(sub);\n }\n\n // Downstream computeds may now need recomputing too\n for (const downstream of c.computedSubs()) {\n if (downstream.markDirty() && downstream.effectSubs().size > 0) {\n activeDirtySet(s).add(downstream);\n }\n }\n }\n\n toProcess.clear();\n }\n};\n\nconst flushEffects = (s: SchedulingState): void => {\n let iterations = 0;\n\n while (s.pendingSubscribers.size > 0 || activeDirtySet(s).size > 0) {\n if (++iterations > DEFAULT_MAX_ITERATIONS) {\n throw new RippleInfiniteLoopError(`infinite flush loop (> ${DEFAULT_MAX_ITERATIONS} iterations)`);\n }\n\n if (activeDirtySet(s).size > 0) recomputeWithEffectSubs(s);\n\n if (s.pendingSubscribers.size === 0) continue;\n\n const subscribersToRun = [...s.pendingSubscribers];\n\n s.pendingSubscribers.clear();\n runAll(subscribersToRun, 'subscriber errors');\n }\n};\n\n// Fire the warning once per module load in environments that look like Node.js,\n// and only when no SSR context hook is installed (once installed, the flush queue\n// is genuinely request-isolated via SchedulingState — see tracking.ts).\n// window-check is unreliable (jsdom sets window in tests); check for process.versions instead.\n// Access entirely via globalThis to avoid requiring @types/node in downstream tsconfigs.\nconst isNodeLike = (globalThis as { process?: { versions?: unknown } }).process?.versions != null;\n\nexport const notifyNodeChange = (node: ReactiveBase<unknown>): void => {\n if (!node.hasSubscribers()) return;\n\n if (!ssrWarnFired && isNodeLike && !hasContextHook()) {\n ssrWarnFired = true;\n warn(\n 'Signal updated in a Node.js-like environment. The module-level flush queue is shared across concurrent requests ' +\n '— use per-request worker isolation or the @vielzeug/ripple/ssr sub-path for request-isolated scheduling.',\n );\n }\n\n const s = getSchedulingState();\n\n propagateDirty(s, node);\n\n if (s.batchDepth === 0) flushEffects(s);\n};\n\nexport const batch = <T>(fn: () => T): T => {\n const s = getSchedulingState();\n\n s.batchDepth++;\n\n let result: T;\n\n try {\n result = fn();\n } catch (e) {\n s.batchDepth--;\n\n // Outermost batch: clear the pending flush queue on body error.\n // Leaving stale subscribers in the queue would flush them on the next unrelated\n // signal write, surfacing errors in unexpected contexts.\n if (s.batchDepth === 0) {\n s.pendingSubscribers.clear();\n s.dirtyWithEffectSubsA.clear();\n s.dirtyWithEffectSubsB.clear();\n }\n\n throw e;\n }\n\n s.batchDepth--;\n\n if (s.batchDepth === 0) flushEffects(s);\n\n return result;\n};\n"],"mappings":"oHAgBA,IAAI,EAAe,GA6Bf,EAAa,EACX,EAA2C,CAAC,EAG5C,EAAkB,GACtB,EAAE,cAAgB,IAAM,EAAE,qBAAuB,EAAE,qBAE/C,GACJ,EACA,IAIS,CACT,IAAM,EAAQ,EAAE,EAGhB,IAAK,IAAM,KAAO,EAAK,WAAW,EAChC,EAAE,mBAAmB,IAAI,CAAG,EAG9B,IAAK,IAAM,KAAK,EAAK,aAAa,EAChC,EAAgB,KAAK,CAAC,EAGxB,GAAI,CACF,KAAO,EAAgB,OAAS,GAAG,CACjC,IAAM,EAAI,EAAgB,IAAI,EAE1B,KAAE,iBAAmB,IAEzB,EAAE,eAAiB,EAEd,EAAE,UAAU,GAIjB,CAAI,EAAE,WAAW,CAAC,CAAC,KAAO,GACxB,EAAe,CAAC,CAAC,CAAC,IAAI,CAAC,EAGzB,IAAK,IAAM,KAAc,EAAE,aAAa,EACtC,EAAgB,KAAK,CAAU,CAJR,CAM3B,CACF,QAAU,CACR,EAAgB,OAAS,CAC3B,CACF,EAEM,EAA2B,GAA6B,CAI5D,KAAO,EAAe,CAAC,CAAC,CAAC,KAAO,GAAG,CACjC,IAAM,EAAY,EAAe,CAAC,EAGlC,EAAE,YAAc,EAAE,cAAgB,IAAM,IAAM,IAC9C,EAAe,CAAC,CAAC,CAAC,MAAM,EAExB,IAAK,IAAM,KAAK,EACT,KAAE,eAAe,GAEN,EAAE,eAEb,EAGL,KAAK,IAAM,KAAO,EAAE,WAAW,EAC7B,EAAE,mBAAmB,IAAI,CAAG,EAI9B,IAAK,IAAM,KAAc,EAAE,aAAa,EAClC,EAAW,UAAU,GAAK,EAAW,WAAW,CAAC,CAAC,KAAO,GAC3D,EAAe,CAAC,CAAC,CAAC,IAAI,CAAU,CANN,CAWhC,EAAU,MAAM,CAClB,CACF,EAEM,EAAgB,GAA6B,CACjD,IAAI,EAAa,EAEjB,KAAO,EAAE,mBAAmB,KAAO,GAAK,EAAe,CAAC,CAAC,CAAC,KAAO,GAAG,CAClE,GAAI,EAAE,EAAA,IACJ,MAAM,IAAI,EAAA,wBAAwB,wCAA8D,EAKlG,GAFI,EAAe,CAAC,CAAC,CAAC,KAAO,GAAG,EAAwB,CAAC,EAErD,EAAE,mBAAmB,OAAS,EAAG,SAErC,IAAM,EAAmB,CAAC,GAAG,EAAE,kBAAkB,EAEjD,EAAE,mBAAmB,MAAM,EAC3B,EAAA,OAAO,EAAkB,mBAAmB,CAC9C,CACF,EAOM,EAAc,WAAoD,SAAS,UAAY,KAEhF,EAAoB,GAAsC,CACrE,GAAI,CAAC,EAAK,eAAe,EAAG,OAExB,CAAC,GAAgB,GAAc,CAAC,EAAA,eAAe,IACjD,EAAe,GACf,EAAA,KACE,0NAEF,GAGF,IAAM,EAAI,EAAA,mBAAmB,EAE7B,EAAe,EAAG,CAAI,EAElB,EAAE,aAAe,GAAG,EAAa,CAAC,CACxC,EAEa,EAAY,GAAmB,CAC1C,IAAM,EAAI,EAAA,mBAAmB,EAE7B,EAAE,aAEF,IAAI,EAEJ,GAAI,CACF,EAAS,EAAG,CACd,OAAS,EAAG,CAYV,KAXA,GAAE,aAKE,EAAE,aAAe,IACnB,EAAE,mBAAmB,MAAM,EAC3B,EAAE,qBAAqB,MAAM,EAC7B,EAAE,qBAAqB,MAAM,GAGzB,CACR,CAMA,MAJA,GAAE,aAEE,EAAE,aAAe,GAAG,EAAa,CAAC,EAE/B,CACT"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ReactiveBase } from './reactive-base';
|
|
2
|
+
export declare const DEFAULT_MAX_ITERATIONS = 100;
|
|
3
|
+
export declare const notifyNodeChange: (node: ReactiveBase<unknown>) => void;
|
|
4
|
+
export declare const batch: <T>(fn: () => T) => T;
|
|
5
|
+
//# sourceMappingURL=scheduling.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduling.d.ts","sourceRoot":"","sources":["../src/scheduling.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAQlE,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAoJ1C,eAAO,MAAM,gBAAgB,GAAI,MAAM,YAAY,CAAC,OAAO,CAAC,KAAG,IAgB9D,CAAC;AAEF,eAAO,MAAM,KAAK,GAAI,CAAC,EAAE,IAAI,MAAM,CAAC,KAAG,CA6BtC,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { runAll as e } from "./_error-utils.js";
|
|
2
|
+
import { RippleInfiniteLoopError as t } from "./errors.js";
|
|
3
|
+
import { warn as n } from "./_dev.js";
|
|
4
|
+
import { getSchedulingState as r, hasContextHook as i } from "./tracking.js";
|
|
5
|
+
var a = !1, o = 0, s = [], c = (e) => e.activeDirty === "a" ? e.dirtyWithEffectSubsA : e.dirtyWithEffectSubsB, l = (e, t) => {
|
|
6
|
+
let n = ++o;
|
|
7
|
+
for (let n of t.effectSubs()) e.pendingSubscribers.add(n);
|
|
8
|
+
for (let e of t.computedSubs()) s.push(e);
|
|
9
|
+
try {
|
|
10
|
+
for (; s.length > 0;) {
|
|
11
|
+
let t = s.pop();
|
|
12
|
+
if (t.lastPropEpoch_ !== n && (t.lastPropEpoch_ = n, t.markDirty())) {
|
|
13
|
+
t.effectSubs().size > 0 && c(e).add(t);
|
|
14
|
+
for (let e of t.computedSubs()) s.push(e);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
} finally {
|
|
18
|
+
s.length = 0;
|
|
19
|
+
}
|
|
20
|
+
}, u = (e) => {
|
|
21
|
+
for (; c(e).size > 0;) {
|
|
22
|
+
let t = c(e);
|
|
23
|
+
e.activeDirty = e.activeDirty === "a" ? "b" : "a", c(e).clear();
|
|
24
|
+
for (let n of t) if (n.hasSubscribers() && n.refreshIfDirty()) {
|
|
25
|
+
for (let t of n.effectSubs()) e.pendingSubscribers.add(t);
|
|
26
|
+
for (let t of n.computedSubs()) t.markDirty() && t.effectSubs().size > 0 && c(e).add(t);
|
|
27
|
+
}
|
|
28
|
+
t.clear();
|
|
29
|
+
}
|
|
30
|
+
}, d = (n) => {
|
|
31
|
+
let r = 0;
|
|
32
|
+
for (; n.pendingSubscribers.size > 0 || c(n).size > 0;) {
|
|
33
|
+
if (++r > 100) throw new t("infinite flush loop (> 100 iterations)");
|
|
34
|
+
if (c(n).size > 0 && u(n), n.pendingSubscribers.size === 0) continue;
|
|
35
|
+
let i = [...n.pendingSubscribers];
|
|
36
|
+
n.pendingSubscribers.clear(), e(i, "subscriber errors");
|
|
37
|
+
}
|
|
38
|
+
}, f = globalThis.process?.versions != null, p = (e) => {
|
|
39
|
+
if (!e.hasSubscribers()) return;
|
|
40
|
+
!a && f && !i() && (a = !0, n("Signal updated in a Node.js-like environment. The module-level flush queue is shared across concurrent requests — use per-request worker isolation or the @vielzeug/ripple/ssr sub-path for request-isolated scheduling."));
|
|
41
|
+
let t = r();
|
|
42
|
+
l(t, e), t.batchDepth === 0 && d(t);
|
|
43
|
+
}, m = (e) => {
|
|
44
|
+
let t = r();
|
|
45
|
+
t.batchDepth++;
|
|
46
|
+
let n;
|
|
47
|
+
try {
|
|
48
|
+
n = e();
|
|
49
|
+
} catch (e) {
|
|
50
|
+
throw t.batchDepth--, t.batchDepth === 0 && (t.pendingSubscribers.clear(), t.dirtyWithEffectSubsA.clear(), t.dirtyWithEffectSubsB.clear()), e;
|
|
51
|
+
}
|
|
52
|
+
return t.batchDepth--, t.batchDepth === 0 && d(t), n;
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { m as batch, p as notifyNodeChange };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=scheduling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduling.js","names":[],"sources":["../src/scheduling.ts"],"sourcesContent":["import type { ComputedBase, ReactiveBase } from './reactive-base';\nimport type { Subscriber } from './types';\n\nimport { warn } from './_dev';\nimport { runAll } from './_error-utils';\nimport { RippleInfiniteLoopError } from './errors';\nimport { getSchedulingState, hasContextHook, type SchedulingState } from './tracking';\n\nexport const DEFAULT_MAX_ITERATIONS = 100;\n\n// batchDepth, pendingSubscribers, and the dirty-computed sets live in `SchedulingState`\n// (see tracking.ts) rather than as module-level variables — this lets the /ripple/ssr\n// sub-path isolate the flush queue per request (via the same AsyncLocalStorage-backed\n// context hook that already isolates tracking), closing the gap where concurrent SSR\n// requests previously shared one flush queue. Outside SSR (no hook installed), this is\n// a single module-level singleton with identical behavior and cost to before.\nlet ssrWarnFired = false;\n\n// ── F1: Simplified flush — no topological sort ─────────────────────────────\n//\n// Push phase (propagateDirty):\n// Mark all downstream computeds dirty in a BFS pass.\n// Directly collect effect subscribers of the changed signal.\n// For computeds with direct effect subscribers, defer to the pull phase.\n//\n// Pull phase (recomputeWithEffectSubs):\n// For each dirty computed that has direct effect subscribers, recompute it.\n// If the value changed, queue its effect subscribers.\n// Any further downstream dirty computeds with effect subs are handled in the\n// same loop (re-queued into dirtyWithEffectSubs if they become dirty during\n// a parent recompute).\n//\n// Effect phase: run all queued effect subscribers.\n//\n// This eliminates the O(n²) topo sort while preserving the equality-suppression\n// guarantee: effect subs of a computed are only notified when the computed's\n// value actually changes.\n//\n// Computeds without direct effect subs (pure lazy nodes) are still pulled\n// lazily when an effect reads them during re-run.\n\n// Module-level scratch state — safe to share even under SSR isolation. Both are only\n// touched within a single synchronous call stack (no `await` inside propagateDirty /\n// recomputeWithEffectSubs / flushEffects), which JS's run-to-completion model already\n// isolates; they never carry information across two separate top-level signal writes.\nlet _propEpoch = 0;\nconst _propagateStack: ComputedBase<unknown>[] = [];\n\n/** Returns (and possibly swaps) the currently-active dirty-with-effect-subs set. */\nconst activeDirtySet = (s: SchedulingState): Set<ComputedBase<unknown>> =>\n s.activeDirty === 'a' ? s.dirtyWithEffectSubsA : s.dirtyWithEffectSubsB;\n\nconst propagateDirty = (\n s: SchedulingState,\n node: {\n computedSubs(): Generator<ComputedBase<unknown>>;\n effectSubs(): ReadonlySet<Subscriber>;\n },\n): void => {\n const epoch = ++_propEpoch;\n\n // Queue direct effect subs of the changed node\n for (const sub of node.effectSubs()) {\n s.pendingSubscribers.add(sub);\n }\n\n for (const c of node.computedSubs()) {\n _propagateStack.push(c);\n }\n\n try {\n while (_propagateStack.length > 0) {\n const c = _propagateStack.pop()!;\n\n if (c.lastPropEpoch_ === epoch) continue;\n\n c.lastPropEpoch_ = epoch;\n\n if (!c.markDirty()) continue; // already dirty — already processed\n\n // If this computed has effect subs, defer recompute to the pull phase.\n // We cannot queue them immediately because the computed might not change.\n if (c.effectSubs().size > 0) {\n activeDirtySet(s).add(c);\n }\n\n for (const downstream of c.computedSubs()) {\n _propagateStack.push(downstream);\n }\n }\n } finally {\n _propagateStack.length = 0;\n }\n};\n\nconst recomputeWithEffectSubs = (s: SchedulingState): void => {\n // Iterate until stable — recomputing one computed may dirty others.\n // Set-swap pattern: swap active/idle sets each pass so new dirty nodes accumulate\n // in the idle set while we iterate the current batch — zero array allocations.\n while (activeDirtySet(s).size > 0) {\n const toProcess = activeDirtySet(s);\n\n // Swap: new dirty nodes from this iteration go into the other set\n s.activeDirty = s.activeDirty === 'a' ? 'b' : 'a';\n activeDirtySet(s).clear();\n\n for (const c of toProcess) {\n if (!c.hasSubscribers()) continue;\n\n const changed = c.refreshIfDirty();\n\n if (!changed) continue;\n\n // Value changed — queue effect subs\n for (const sub of c.effectSubs()) {\n s.pendingSubscribers.add(sub);\n }\n\n // Downstream computeds may now need recomputing too\n for (const downstream of c.computedSubs()) {\n if (downstream.markDirty() && downstream.effectSubs().size > 0) {\n activeDirtySet(s).add(downstream);\n }\n }\n }\n\n toProcess.clear();\n }\n};\n\nconst flushEffects = (s: SchedulingState): void => {\n let iterations = 0;\n\n while (s.pendingSubscribers.size > 0 || activeDirtySet(s).size > 0) {\n if (++iterations > DEFAULT_MAX_ITERATIONS) {\n throw new RippleInfiniteLoopError(`infinite flush loop (> ${DEFAULT_MAX_ITERATIONS} iterations)`);\n }\n\n if (activeDirtySet(s).size > 0) recomputeWithEffectSubs(s);\n\n if (s.pendingSubscribers.size === 0) continue;\n\n const subscribersToRun = [...s.pendingSubscribers];\n\n s.pendingSubscribers.clear();\n runAll(subscribersToRun, 'subscriber errors');\n }\n};\n\n// Fire the warning once per module load in environments that look like Node.js,\n// and only when no SSR context hook is installed (once installed, the flush queue\n// is genuinely request-isolated via SchedulingState — see tracking.ts).\n// window-check is unreliable (jsdom sets window in tests); check for process.versions instead.\n// Access entirely via globalThis to avoid requiring @types/node in downstream tsconfigs.\nconst isNodeLike = (globalThis as { process?: { versions?: unknown } }).process?.versions != null;\n\nexport const notifyNodeChange = (node: ReactiveBase<unknown>): void => {\n if (!node.hasSubscribers()) return;\n\n if (!ssrWarnFired && isNodeLike && !hasContextHook()) {\n ssrWarnFired = true;\n warn(\n 'Signal updated in a Node.js-like environment. The module-level flush queue is shared across concurrent requests ' +\n '— use per-request worker isolation or the @vielzeug/ripple/ssr sub-path for request-isolated scheduling.',\n );\n }\n\n const s = getSchedulingState();\n\n propagateDirty(s, node);\n\n if (s.batchDepth === 0) flushEffects(s);\n};\n\nexport const batch = <T>(fn: () => T): T => {\n const s = getSchedulingState();\n\n s.batchDepth++;\n\n let result: T;\n\n try {\n result = fn();\n } catch (e) {\n s.batchDepth--;\n\n // Outermost batch: clear the pending flush queue on body error.\n // Leaving stale subscribers in the queue would flush them on the next unrelated\n // signal write, surfacing errors in unexpected contexts.\n if (s.batchDepth === 0) {\n s.pendingSubscribers.clear();\n s.dirtyWithEffectSubsA.clear();\n s.dirtyWithEffectSubsB.clear();\n }\n\n throw e;\n }\n\n s.batchDepth--;\n\n if (s.batchDepth === 0) flushEffects(s);\n\n return result;\n};\n"],"mappings":";;;;AAgBA,IAAI,IAAe,IA6Bf,IAAa,GACX,IAA2C,CAAC,GAG5C,KAAkB,MACtB,EAAE,gBAAgB,MAAM,EAAE,uBAAuB,EAAE,sBAE/C,KACJ,GACA,MAIS;CACT,IAAM,IAAQ,EAAE;CAGhB,KAAK,IAAM,KAAO,EAAK,WAAW,GAChC,EAAE,mBAAmB,IAAI,CAAG;CAG9B,KAAK,IAAM,KAAK,EAAK,aAAa,GAChC,EAAgB,KAAK,CAAC;CAGxB,IAAI;EACF,OAAO,EAAgB,SAAS,IAAG;GACjC,IAAM,IAAI,EAAgB,IAAI;GAE1B,MAAE,mBAAmB,MAEzB,EAAE,iBAAiB,GAEd,EAAE,UAAU,IAIjB;IAAI,EAAE,WAAW,CAAC,CAAC,OAAO,KACxB,EAAe,CAAC,CAAC,CAAC,IAAI,CAAC;IAGzB,KAAK,IAAM,KAAc,EAAE,aAAa,GACtC,EAAgB,KAAK,CAAU;GAJR;EAM3B;CACF,UAAU;EACR,EAAgB,SAAS;CAC3B;AACF,GAEM,KAA2B,MAA6B;CAI5D,OAAO,EAAe,CAAC,CAAC,CAAC,OAAO,IAAG;EACjC,IAAM,IAAY,EAAe,CAAC;EAIlC,AADA,EAAE,cAAc,EAAE,gBAAgB,MAAM,MAAM,KAC9C,EAAe,CAAC,CAAC,CAAC,MAAM;EAExB,KAAK,IAAM,KAAK,GACT,MAAE,eAAe,KAEN,EAAE,eAEb,GAGL;QAAK,IAAM,KAAO,EAAE,WAAW,GAC7B,EAAE,mBAAmB,IAAI,CAAG;GAI9B,KAAK,IAAM,KAAc,EAAE,aAAa,GACtC,AAAI,EAAW,UAAU,KAAK,EAAW,WAAW,CAAC,CAAC,OAAO,KAC3D,EAAe,CAAC,CAAC,CAAC,IAAI,CAAU;EANN;EAWhC,EAAU,MAAM;CAClB;AACF,GAEM,KAAgB,MAA6B;CACjD,IAAI,IAAa;CAEjB,OAAO,EAAE,mBAAmB,OAAO,KAAK,EAAe,CAAC,CAAC,CAAC,OAAO,IAAG;EAClE,IAAI,EAAE,IAAA,KACJ,MAAM,IAAI,EAAwB,wCAA8D;EAKlG,IAFI,EAAe,CAAC,CAAC,CAAC,OAAO,KAAG,EAAwB,CAAC,GAErD,EAAE,mBAAmB,SAAS,GAAG;EAErC,IAAM,IAAmB,CAAC,GAAG,EAAE,kBAAkB;EAGjD,AADA,EAAE,mBAAmB,MAAM,GAC3B,EAAO,GAAkB,mBAAmB;CAC9C;AACF,GAOM,IAAc,WAAoD,SAAS,YAAY,MAEhF,KAAoB,MAAsC;CACrE,IAAI,CAAC,EAAK,eAAe,GAAG;CAE5B,AAAI,CAAC,KAAgB,KAAc,CAAC,EAAe,MACjD,IAAe,IACf,EACE,0NAEF;CAGF,IAAM,IAAI,EAAmB;CAI7B,AAFA,EAAe,GAAG,CAAI,GAElB,EAAE,eAAe,KAAG,EAAa,CAAC;AACxC,GAEa,KAAY,MAAmB;CAC1C,IAAM,IAAI,EAAmB;CAE7B,EAAE;CAEF,IAAI;CAEJ,IAAI;EACF,IAAS,EAAG;CACd,SAAS,GAAG;EAYV,MAXA,EAAE,cAKE,EAAE,eAAe,MACnB,EAAE,mBAAmB,MAAM,GAC3B,EAAE,qBAAqB,MAAM,GAC7B,EAAE,qBAAqB,MAAM,IAGzB;CACR;CAMA,OAJA,EAAE,cAEE,EAAE,eAAe,KAAG,EAAa,CAAC,GAE/B;AACT"}
|
package/dist/signal.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./devtools-hook.cjs"),t=require("./tracking.cjs"),n=require("./scheduling.cjs"),r=require("./subscription.cjs"),i=require("./reactive-base.cjs");var a=class extends i.ReactiveBase{value_;equals_;disposed_;constructor(e,t,n){super(n),this.value_=e,this.equals_=t??Object.is,this.disposed_=!1}get value(){return this.disposed_||t.trackSource(this),this.value_}set value(r){if(this.disposed_||this.equals_(this.value_,r))return;let i=this.value_;this.value_=r,this.version=t.tickRevision(),e.getDevToolsHook()?.write?.({name:this.name,newValue:r,oldValue:i}),n.notifyNodeChange(this)}peek(){return this.value_}subscribe=e=>{if(this.disposed_){let e=new r.SubscriptionImpl(()=>{});return e.dispose(),e}return this.addEffectSub(e),new r.SubscriptionImpl(()=>{this.removeEffectSub(e)})};get disposed(){return this.disposed_}dispose(){this.disposed_||(this.disposed_=!0,this.clearSubscribers(),e.getDevToolsHook()?.dispose?.({kind:`signal`,name:this.name}))}[Symbol.dispose](){this.dispose()}},o=(e,t)=>new a(e,t?.equals,t?.name);exports.SignalImpl=a,exports.signal=o;
|
|
2
|
+
//# sourceMappingURL=signal.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal.cjs","names":[],"sources":["../src/signal.ts"],"sourcesContent":["import type { Signal, SignalOptions, Subscription } from './types';\n\nimport { getDevToolsHook } from './devtools-hook';\nimport { ReactiveBase } from './reactive-base';\nimport { notifyNodeChange } from './scheduling';\nimport { SubscriptionImpl } from './subscription';\nimport { tickRevision, trackSource } from './tracking';\n\nexport class SignalImpl<T> extends ReactiveBase<T> implements Signal<T> {\n private value_: T;\n private equals_: (a: T, b: T) => boolean;\n private disposed_: boolean;\n\n constructor(initial: T, equals?: (a: T, b: T) => boolean, name?: string) {\n super(name);\n this.value_ = initial;\n this.equals_ = equals ?? Object.is;\n this.disposed_ = false;\n }\n\n get value(): T {\n if (!this.disposed_) trackSource(this);\n\n return this.value_;\n }\n\n set value(next: T) {\n if (this.disposed_) return;\n\n if (this.equals_(this.value_, next)) return;\n\n const oldValue = this.value_;\n\n this.value_ = next;\n this.version = tickRevision();\n\n getDevToolsHook()?.write?.({ name: this.name, newValue: next, oldValue });\n notifyNodeChange(this);\n }\n\n peek(): T {\n return this.value_;\n }\n\n readonly subscribe = (listener: () => void): Subscription => {\n if (this.disposed_) {\n const noop = new SubscriptionImpl(() => {});\n\n noop.dispose();\n\n return noop;\n }\n\n this.addEffectSub(listener);\n\n return new SubscriptionImpl(() => {\n this.removeEffectSub(listener);\n });\n };\n\n /**\n * Marks the signal as permanently inert. After disposal:\n * - reads still return the last known value (but no longer track dependencies)\n * - writes are silently ignored\n * - new subscriptions are immediately unsubscribed\n * - all existing subscriber and computed-subscriber references are dropped\n */\n get disposed(): boolean {\n return this.disposed_;\n }\n\n dispose(): void {\n if (this.disposed_) return;\n\n this.disposed_ = true;\n this.clearSubscribers();\n getDevToolsHook()?.dispose?.({ kind: 'signal', name: this.name });\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\nexport const signal = <T>(initial: T, options?: SignalOptions<T>): Signal<T> =>\n new SignalImpl(initial, options?.equals, options?.name);\n"],"mappings":"kKAQA,IAAa,EAAb,cAAmC,EAAA,YAAqC,CACtE,OACA,QACA,UAEA,YAAY,EAAY,EAAkC,EAAe,CACvE,MAAM,CAAI,EACV,KAAK,OAAS,EACd,KAAK,QAAU,GAAU,OAAO,GAChC,KAAK,UAAY,EACnB,CAEA,IAAI,OAAW,CAGb,OAFK,KAAK,WAAW,EAAA,YAAY,IAAI,EAE9B,KAAK,MACd,CAEA,IAAI,MAAM,EAAS,CAGjB,GAFI,KAAK,WAEL,KAAK,QAAQ,KAAK,OAAQ,CAAI,EAAG,OAErC,IAAM,EAAW,KAAK,OAEtB,KAAK,OAAS,EACd,KAAK,QAAU,EAAA,aAAa,EAE5B,EAAA,gBAAgB,CAAC,EAAE,QAAQ,CAAE,KAAM,KAAK,KAAM,SAAU,EAAM,UAAS,CAAC,EACxE,EAAA,iBAAiB,IAAI,CACvB,CAEA,MAAU,CACR,OAAO,KAAK,MACd,CAEA,UAAsB,GAAuC,CAC3D,GAAI,KAAK,UAAW,CAClB,IAAM,EAAO,IAAI,EAAA,qBAAuB,CAAC,CAAC,EAI1C,OAFA,EAAK,QAAQ,EAEN,CACT,CAIA,OAFA,KAAK,aAAa,CAAQ,EAEnB,IAAI,EAAA,qBAAuB,CAChC,KAAK,gBAAgB,CAAQ,CAC/B,CAAC,CACH,EASA,IAAI,UAAoB,CACtB,OAAO,KAAK,SACd,CAEA,SAAgB,CACV,KAAK,YAET,KAAK,UAAY,GACjB,KAAK,iBAAiB,EACtB,EAAA,gBAAgB,CAAC,EAAE,UAAU,CAAE,KAAM,SAAU,KAAM,KAAK,IAAK,CAAC,EAClE,CAEA,CAAC,OAAO,UAAiB,CACvB,KAAK,QAAQ,CACf,CACF,EAEa,GAAa,EAAY,IACpC,IAAI,EAAW,EAAS,GAAS,OAAQ,GAAS,IAAI"}
|
package/dist/signal.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Signal, SignalOptions, Subscription } from './types';
|
|
2
|
+
import { ReactiveBase } from './reactive-base';
|
|
3
|
+
export declare class SignalImpl<T> extends ReactiveBase<T> implements Signal<T> {
|
|
4
|
+
private value_;
|
|
5
|
+
private equals_;
|
|
6
|
+
private disposed_;
|
|
7
|
+
constructor(initial: T, equals?: (a: T, b: T) => boolean, name?: string);
|
|
8
|
+
get value(): T;
|
|
9
|
+
set value(next: T);
|
|
10
|
+
peek(): T;
|
|
11
|
+
readonly subscribe: (listener: () => void) => Subscription;
|
|
12
|
+
/**
|
|
13
|
+
* Marks the signal as permanently inert. After disposal:
|
|
14
|
+
* - reads still return the last known value (but no longer track dependencies)
|
|
15
|
+
* - writes are silently ignored
|
|
16
|
+
* - new subscriptions are immediately unsubscribed
|
|
17
|
+
* - all existing subscriber and computed-subscriber references are dropped
|
|
18
|
+
*/
|
|
19
|
+
get disposed(): boolean;
|
|
20
|
+
dispose(): void;
|
|
21
|
+
[Symbol.dispose](): void;
|
|
22
|
+
}
|
|
23
|
+
export declare const signal: <T>(initial: T, options?: SignalOptions<T>) => Signal<T>;
|
|
24
|
+
//# sourceMappingURL=signal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGnE,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAK/C,qBAAa,UAAU,CAAC,CAAC,CAAE,SAAQ,YAAY,CAAC,CAAC,CAAE,YAAW,MAAM,CAAC,CAAC,CAAC;IACrE,OAAO,CAAC,MAAM,CAAI;IAClB,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,SAAS,CAAU;gBAEf,OAAO,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM;IAOvE,IAAI,KAAK,IAAI,CAAC,CAIb;IAED,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,EAYhB;IAED,IAAI,IAAI,CAAC;IAIT,QAAQ,CAAC,SAAS,GAAI,UAAU,MAAM,IAAI,KAAG,YAAY,CAcvD;IAEF;;;;;;OAMG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,OAAO,IAAI,IAAI;IAQf,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAGzB;AAED,eAAO,MAAM,MAAM,GAAI,CAAC,EAAE,SAAS,CAAC,EAAE,UAAU,aAAa,CAAC,CAAC,CAAC,KAAG,MAAM,CAAC,CAAC,CAClB,CAAC"}
|
package/dist/signal.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getDevToolsHook as e } from "./devtools-hook.js";
|
|
2
|
+
import { tickRevision as t, trackSource as n } from "./tracking.js";
|
|
3
|
+
import { notifyNodeChange as r } from "./scheduling.js";
|
|
4
|
+
import { SubscriptionImpl as i } from "./subscription.js";
|
|
5
|
+
import { ReactiveBase as a } from "./reactive-base.js";
|
|
6
|
+
//#region src/signal.ts
|
|
7
|
+
var o = class extends a {
|
|
8
|
+
value_;
|
|
9
|
+
equals_;
|
|
10
|
+
disposed_;
|
|
11
|
+
constructor(e, t, n) {
|
|
12
|
+
super(n), this.value_ = e, this.equals_ = t ?? Object.is, this.disposed_ = !1;
|
|
13
|
+
}
|
|
14
|
+
get value() {
|
|
15
|
+
return this.disposed_ || n(this), this.value_;
|
|
16
|
+
}
|
|
17
|
+
set value(n) {
|
|
18
|
+
if (this.disposed_ || this.equals_(this.value_, n)) return;
|
|
19
|
+
let i = this.value_;
|
|
20
|
+
this.value_ = n, this.version = t(), e()?.write?.({
|
|
21
|
+
name: this.name,
|
|
22
|
+
newValue: n,
|
|
23
|
+
oldValue: i
|
|
24
|
+
}), r(this);
|
|
25
|
+
}
|
|
26
|
+
peek() {
|
|
27
|
+
return this.value_;
|
|
28
|
+
}
|
|
29
|
+
subscribe = (e) => {
|
|
30
|
+
if (this.disposed_) {
|
|
31
|
+
let e = new i(() => {});
|
|
32
|
+
return e.dispose(), e;
|
|
33
|
+
}
|
|
34
|
+
return this.addEffectSub(e), new i(() => {
|
|
35
|
+
this.removeEffectSub(e);
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
get disposed() {
|
|
39
|
+
return this.disposed_;
|
|
40
|
+
}
|
|
41
|
+
dispose() {
|
|
42
|
+
this.disposed_ || (this.disposed_ = !0, this.clearSubscribers(), e()?.dispose?.({
|
|
43
|
+
kind: "signal",
|
|
44
|
+
name: this.name
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
[Symbol.dispose]() {
|
|
48
|
+
this.dispose();
|
|
49
|
+
}
|
|
50
|
+
}, s = (e, t) => new o(e, t?.equals, t?.name);
|
|
51
|
+
//#endregion
|
|
52
|
+
export { o as SignalImpl, s as signal };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=signal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal.js","names":[],"sources":["../src/signal.ts"],"sourcesContent":["import type { Signal, SignalOptions, Subscription } from './types';\n\nimport { getDevToolsHook } from './devtools-hook';\nimport { ReactiveBase } from './reactive-base';\nimport { notifyNodeChange } from './scheduling';\nimport { SubscriptionImpl } from './subscription';\nimport { tickRevision, trackSource } from './tracking';\n\nexport class SignalImpl<T> extends ReactiveBase<T> implements Signal<T> {\n private value_: T;\n private equals_: (a: T, b: T) => boolean;\n private disposed_: boolean;\n\n constructor(initial: T, equals?: (a: T, b: T) => boolean, name?: string) {\n super(name);\n this.value_ = initial;\n this.equals_ = equals ?? Object.is;\n this.disposed_ = false;\n }\n\n get value(): T {\n if (!this.disposed_) trackSource(this);\n\n return this.value_;\n }\n\n set value(next: T) {\n if (this.disposed_) return;\n\n if (this.equals_(this.value_, next)) return;\n\n const oldValue = this.value_;\n\n this.value_ = next;\n this.version = tickRevision();\n\n getDevToolsHook()?.write?.({ name: this.name, newValue: next, oldValue });\n notifyNodeChange(this);\n }\n\n peek(): T {\n return this.value_;\n }\n\n readonly subscribe = (listener: () => void): Subscription => {\n if (this.disposed_) {\n const noop = new SubscriptionImpl(() => {});\n\n noop.dispose();\n\n return noop;\n }\n\n this.addEffectSub(listener);\n\n return new SubscriptionImpl(() => {\n this.removeEffectSub(listener);\n });\n };\n\n /**\n * Marks the signal as permanently inert. After disposal:\n * - reads still return the last known value (but no longer track dependencies)\n * - writes are silently ignored\n * - new subscriptions are immediately unsubscribed\n * - all existing subscriber and computed-subscriber references are dropped\n */\n get disposed(): boolean {\n return this.disposed_;\n }\n\n dispose(): void {\n if (this.disposed_) return;\n\n this.disposed_ = true;\n this.clearSubscribers();\n getDevToolsHook()?.dispose?.({ kind: 'signal', name: this.name });\n }\n\n [Symbol.dispose](): void {\n this.dispose();\n }\n}\n\nexport const signal = <T>(initial: T, options?: SignalOptions<T>): Signal<T> =>\n new SignalImpl(initial, options?.equals, options?.name);\n"],"mappings":";;;;;;AAQA,IAAa,IAAb,cAAmC,EAAqC;CACtE;CACA;CACA;CAEA,YAAY,GAAY,GAAkC,GAAe;EAIvE,AAHA,MAAM,CAAI,GACV,KAAK,SAAS,GACd,KAAK,UAAU,KAAU,OAAO,IAChC,KAAK,YAAY;CACnB;CAEA,IAAI,QAAW;EAGb,OAFK,KAAK,aAAW,EAAY,IAAI,GAE9B,KAAK;CACd;CAEA,IAAI,MAAM,GAAS;EAGjB,IAFI,KAAK,aAEL,KAAK,QAAQ,KAAK,QAAQ,CAAI,GAAG;EAErC,IAAM,IAAW,KAAK;EAMtB,AAJA,KAAK,SAAS,GACd,KAAK,UAAU,EAAa,GAE5B,EAAgB,CAAC,EAAE,QAAQ;GAAE,MAAM,KAAK;GAAM,UAAU;GAAM;EAAS,CAAC,GACxE,EAAiB,IAAI;CACvB;CAEA,OAAU;EACR,OAAO,KAAK;CACd;CAEA,aAAsB,MAAuC;EAC3D,IAAI,KAAK,WAAW;GAClB,IAAM,IAAO,IAAI,QAAuB,CAAC,CAAC;GAI1C,OAFA,EAAK,QAAQ,GAEN;EACT;EAIA,OAFA,KAAK,aAAa,CAAQ,GAEnB,IAAI,QAAuB;GAChC,KAAK,gBAAgB,CAAQ;EAC/B,CAAC;CACH;CASA,IAAI,WAAoB;EACtB,OAAO,KAAK;CACd;CAEA,UAAgB;EACV,KAAK,cAET,KAAK,YAAY,IACjB,KAAK,iBAAiB,GACtB,EAAgB,CAAC,EAAE,UAAU;GAAE,MAAM;GAAU,MAAM,KAAK;EAAK,CAAC;CAClE;CAEA,CAAC,OAAO,WAAiB;EACvB,KAAK,QAAQ;CACf;AACF,GAEa,KAAa,GAAY,MACpC,IAAI,EAAW,GAAS,GAAS,QAAQ,GAAS,IAAI"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../devtools-hook.cjs"),t=require("../tracking.cjs"),n=require("../effect.cjs");var r=t=>{e.setDevToolsHook(t),typeof globalThis<`u`&&(globalThis.__RIPPLE_DEVTOOLS__=t??void 0)},i=(e,r)=>{let i=r?.name??`anonymous`,a=[];return n.effect(()=>{let n=[],r=t.withSourceObserver(e=>{n.push({source:e,version:e.version})},e);if(a.length===0){if(n.length>0){console.group(`[ripple:debug] "${i}" initial deps:`);for(let e of n)console.debug(` ${e.source.name??`(unnamed)`} (v${e.source.version})`);console.groupEnd()}}else{let e=a.filter(e=>e.source.version!==e.version);if(e.length>0){console.group(`[ripple:debug] "${i}" re-running — changed sources:`);for(let t of e)console.debug(` ${t.source.name??`(unnamed)`} (v${t.version} -> v${t.source.version})`);console.groupEnd()}}return a=n,r},r)};exports.debugEffect=i,exports.installDevTools=r;
|
|
2
|
+
//# sourceMappingURL=devtools.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools.cjs","names":[],"sources":["../../src/devtools.ts"],"sourcesContent":["// ── DevTools — sub-path entry (@vielzeug/ripple/devtools) ────────────────────\n//\n// Import from the dedicated sub-path so this module is tree-shaken from\n// production bundles when DevTools is not used:\n//\n// import { installDevTools, debugEffect } from '@vielzeug/ripple/devtools';\n//\n// The hot-path stub (getDevToolsHook) lives in devtools-hook.ts and is the only\n// part that ships in the core bundle.\n\nimport type { DepEntry } from './tracking';\nimport type { CleanupFn, EffectCallback, EffectHandle, EffectOptions, RippleDevToolsHook } from './types';\n\nimport { setDevToolsHook } from './devtools-hook';\nimport { effect } from './effect';\nimport { withSourceObserver } from './tracking';\n\n// Re-export all hook types from the core types module so consumers of the\n// sub-path don't need to import from two places.\nexport type { DisposeEvent, MutateEvent, NamedEvent, RippleDevToolsHook, WriteEvent } from './types';\n\n// ── installDevTools ───────────────────────────────────────────────────────────\n\n/**\n * Installs a DevTools hook for observing ripple internals.\n * Pass `null` to uninstall.\n *\n * Import from the dedicated sub-path to keep this out of production bundles:\n * ```ts\n * import { installDevTools } from '@vielzeug/ripple/devtools';\n *\n * installDevTools({\n * write({ name, oldValue, newValue }) {\n * console.log(`[ripple] ${name ?? '(unnamed)'}: ${String(oldValue)} → ${String(newValue)}`);\n * },\n * dispose({ kind, name }) {\n * console.log(`[ripple] ${kind} \"${name ?? '(unnamed)'}\" disposed`);\n * },\n * });\n * ```\n */\nexport const installDevTools = (hook: RippleDevToolsHook | null): void => {\n setDevToolsHook(hook);\n\n // Keep the globalThis mirror in sync for browser-extension DevTools.\n // Use assignment (not delete) — configurable property semantics vary by environment.\n if (typeof globalThis !== 'undefined') {\n (globalThis as Record<string, unknown>)['__RIPPLE_DEVTOOLS__'] = hook ?? undefined;\n }\n};\n\n// ── debugEffect ───────────────────────────────────────────────────────────────\n\n/**\n * Wraps `effect()` and logs reactive dependency information on every run:\n * - **Initial run**: logs all deps the effect subscribed to.\n * - **Re-runs**: logs which deps changed (with old and new version numbers).\n *\n * Use instead of `effect()` when debugging unexpected re-renders.\n *\n * @example\n * ```ts\n * import { debugEffect } from '@vielzeug/ripple/devtools';\n *\n * const stop = debugEffect(() => {\n * renderUser(userId.value, name.value);\n * }, { name: 'renderUser' });\n * ```\n */\nexport const debugEffect = (fn: EffectCallback, options?: Omit<EffectOptions, 'trace'>): EffectHandle => {\n const label = options?.name ?? 'anonymous';\n\n // Track versions seen on the last run so we can diff on the next run.\n let prevDeps: DepEntry[] = [];\n\n const wrappedFn = (): CleanupFn | void => {\n const currentDeps: DepEntry[] = [];\n\n // SourceObserver is scoped to the current TrackingCtx — nested computed\n // recomputes run in their own context (no observer), so no identity check needed.\n const result = withSourceObserver((source) => {\n currentDeps.push({ source, version: source.version });\n }, fn);\n\n if (prevDeps.length === 0) {\n // First run — log initial subscriptions.\n if (currentDeps.length > 0) {\n console.group(`[ripple:debug] \"${label}\" initial deps:`);\n\n for (const dep of currentDeps) {\n console.debug(` ${dep.source.name ?? '(unnamed)'} (v${dep.source.version})`);\n }\n\n console.groupEnd();\n }\n } else {\n // Re-run — log which sources changed.\n const changed = prevDeps.filter((d) => d.source.version !== d.version);\n\n if (changed.length > 0) {\n console.group(`[ripple:debug] \"${label}\" re-running — changed sources:`);\n\n for (const dep of changed) {\n console.debug(` ${dep.source.name ?? '(unnamed)'} (v${dep.version} -> v${dep.source.version})`);\n }\n\n console.groupEnd();\n }\n }\n\n prevDeps = currentDeps;\n\n return result;\n };\n\n return effect(wrappedFn, options);\n};\n"],"mappings":"mKAyCA,IAAa,EAAmB,GAA0C,CACxE,EAAA,gBAAgB,CAAI,EAIhB,OAAO,WAAe,MACxB,WAAwC,oBAAyB,GAAQ,IAAA,GAE7E,EAoBa,GAAe,EAAoB,IAAyD,CACvG,IAAM,EAAQ,GAAS,MAAQ,YAG3B,EAAuB,CAAC,EA0C5B,OAAO,EAAA,WAxCmC,CACxC,IAAM,EAA0B,CAAC,EAI3B,EAAS,EAAA,mBAAoB,GAAW,CAC5C,EAAY,KAAK,CAAE,SAAQ,QAAS,EAAO,OAAQ,CAAC,CACtD,EAAG,CAAE,EAEL,GAAI,EAAS,SAAW,MAElB,EAAY,OAAS,EAAG,CAC1B,QAAQ,MAAM,mBAAmB,EAAM,gBAAgB,EAEvD,IAAK,IAAM,KAAO,EAChB,QAAQ,MAAM,KAAK,EAAI,OAAO,MAAQ,YAAY,KAAK,EAAI,OAAO,QAAQ,EAAE,EAG9E,QAAQ,SAAS,CACnB,MACK,CAEL,IAAM,EAAU,EAAS,OAAQ,GAAM,EAAE,OAAO,UAAY,EAAE,OAAO,EAErE,GAAI,EAAQ,OAAS,EAAG,CACtB,QAAQ,MAAM,mBAAmB,EAAM,gCAAgC,EAEvE,IAAK,IAAM,KAAO,EAChB,QAAQ,MAAM,KAAK,EAAI,OAAO,MAAQ,YAAY,KAAK,EAAI,QAAQ,OAAO,EAAI,OAAO,QAAQ,EAAE,EAGjG,QAAQ,SAAS,CACnB,CACF,CAIA,MAFA,GAAW,EAEJ,CACT,EAEyB,CAAO,CAClC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { setDevToolsHook as e } from "../devtools-hook.js";
|
|
2
|
+
import { withSourceObserver as t } from "../tracking.js";
|
|
3
|
+
import { effect as n } from "../effect.js";
|
|
4
|
+
//#region src/devtools.ts
|
|
5
|
+
var r = (t) => {
|
|
6
|
+
e(t), typeof globalThis < "u" && (globalThis.__RIPPLE_DEVTOOLS__ = t ?? void 0);
|
|
7
|
+
}, i = (e, r) => {
|
|
8
|
+
let i = r?.name ?? "anonymous", a = [];
|
|
9
|
+
return n(() => {
|
|
10
|
+
let n = [], r = t((e) => {
|
|
11
|
+
n.push({
|
|
12
|
+
source: e,
|
|
13
|
+
version: e.version
|
|
14
|
+
});
|
|
15
|
+
}, e);
|
|
16
|
+
if (a.length === 0) {
|
|
17
|
+
if (n.length > 0) {
|
|
18
|
+
console.group(`[ripple:debug] "${i}" initial deps:`);
|
|
19
|
+
for (let e of n) console.debug(` ${e.source.name ?? "(unnamed)"} (v${e.source.version})`);
|
|
20
|
+
console.groupEnd();
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
let e = a.filter((e) => e.source.version !== e.version);
|
|
24
|
+
if (e.length > 0) {
|
|
25
|
+
console.group(`[ripple:debug] "${i}" re-running — changed sources:`);
|
|
26
|
+
for (let t of e) console.debug(` ${t.source.name ?? "(unnamed)"} (v${t.version} -> v${t.source.version})`);
|
|
27
|
+
console.groupEnd();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return a = n, r;
|
|
31
|
+
}, r);
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { i as debugEffect, r as installDevTools };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=devtools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools.js","names":[],"sources":["../../src/devtools.ts"],"sourcesContent":["// ── DevTools — sub-path entry (@vielzeug/ripple/devtools) ────────────────────\n//\n// Import from the dedicated sub-path so this module is tree-shaken from\n// production bundles when DevTools is not used:\n//\n// import { installDevTools, debugEffect } from '@vielzeug/ripple/devtools';\n//\n// The hot-path stub (getDevToolsHook) lives in devtools-hook.ts and is the only\n// part that ships in the core bundle.\n\nimport type { DepEntry } from './tracking';\nimport type { CleanupFn, EffectCallback, EffectHandle, EffectOptions, RippleDevToolsHook } from './types';\n\nimport { setDevToolsHook } from './devtools-hook';\nimport { effect } from './effect';\nimport { withSourceObserver } from './tracking';\n\n// Re-export all hook types from the core types module so consumers of the\n// sub-path don't need to import from two places.\nexport type { DisposeEvent, MutateEvent, NamedEvent, RippleDevToolsHook, WriteEvent } from './types';\n\n// ── installDevTools ───────────────────────────────────────────────────────────\n\n/**\n * Installs a DevTools hook for observing ripple internals.\n * Pass `null` to uninstall.\n *\n * Import from the dedicated sub-path to keep this out of production bundles:\n * ```ts\n * import { installDevTools } from '@vielzeug/ripple/devtools';\n *\n * installDevTools({\n * write({ name, oldValue, newValue }) {\n * console.log(`[ripple] ${name ?? '(unnamed)'}: ${String(oldValue)} → ${String(newValue)}`);\n * },\n * dispose({ kind, name }) {\n * console.log(`[ripple] ${kind} \"${name ?? '(unnamed)'}\" disposed`);\n * },\n * });\n * ```\n */\nexport const installDevTools = (hook: RippleDevToolsHook | null): void => {\n setDevToolsHook(hook);\n\n // Keep the globalThis mirror in sync for browser-extension DevTools.\n // Use assignment (not delete) — configurable property semantics vary by environment.\n if (typeof globalThis !== 'undefined') {\n (globalThis as Record<string, unknown>)['__RIPPLE_DEVTOOLS__'] = hook ?? undefined;\n }\n};\n\n// ── debugEffect ───────────────────────────────────────────────────────────────\n\n/**\n * Wraps `effect()` and logs reactive dependency information on every run:\n * - **Initial run**: logs all deps the effect subscribed to.\n * - **Re-runs**: logs which deps changed (with old and new version numbers).\n *\n * Use instead of `effect()` when debugging unexpected re-renders.\n *\n * @example\n * ```ts\n * import { debugEffect } from '@vielzeug/ripple/devtools';\n *\n * const stop = debugEffect(() => {\n * renderUser(userId.value, name.value);\n * }, { name: 'renderUser' });\n * ```\n */\nexport const debugEffect = (fn: EffectCallback, options?: Omit<EffectOptions, 'trace'>): EffectHandle => {\n const label = options?.name ?? 'anonymous';\n\n // Track versions seen on the last run so we can diff on the next run.\n let prevDeps: DepEntry[] = [];\n\n const wrappedFn = (): CleanupFn | void => {\n const currentDeps: DepEntry[] = [];\n\n // SourceObserver is scoped to the current TrackingCtx — nested computed\n // recomputes run in their own context (no observer), so no identity check needed.\n const result = withSourceObserver((source) => {\n currentDeps.push({ source, version: source.version });\n }, fn);\n\n if (prevDeps.length === 0) {\n // First run — log initial subscriptions.\n if (currentDeps.length > 0) {\n console.group(`[ripple:debug] \"${label}\" initial deps:`);\n\n for (const dep of currentDeps) {\n console.debug(` ${dep.source.name ?? '(unnamed)'} (v${dep.source.version})`);\n }\n\n console.groupEnd();\n }\n } else {\n // Re-run — log which sources changed.\n const changed = prevDeps.filter((d) => d.source.version !== d.version);\n\n if (changed.length > 0) {\n console.group(`[ripple:debug] \"${label}\" re-running — changed sources:`);\n\n for (const dep of changed) {\n console.debug(` ${dep.source.name ?? '(unnamed)'} (v${dep.version} -> v${dep.source.version})`);\n }\n\n console.groupEnd();\n }\n }\n\n prevDeps = currentDeps;\n\n return result;\n };\n\n return effect(wrappedFn, options);\n};\n"],"mappings":";;;;AAyCA,IAAa,KAAmB,MAA0C;CAKxE,AAJA,EAAgB,CAAI,GAIhB,OAAO,aAAe,QACxB,WAAwC,sBAAyB,KAAQ,KAAA;AAE7E,GAoBa,KAAe,GAAoB,MAAyD;CACvG,IAAM,IAAQ,GAAS,QAAQ,aAG3B,IAAuB,CAAC;CA0C5B,OAAO,QAxCmC;EACxC,IAAM,IAA0B,CAAC,GAI3B,IAAS,GAAoB,MAAW;GAC5C,EAAY,KAAK;IAAE;IAAQ,SAAS,EAAO;GAAQ,CAAC;EACtD,GAAG,CAAE;EAEL,IAAI,EAAS,WAAW;OAElB,EAAY,SAAS,GAAG;IAC1B,QAAQ,MAAM,mBAAmB,EAAM,gBAAgB;IAEvD,KAAK,IAAM,KAAO,GAChB,QAAQ,MAAM,KAAK,EAAI,OAAO,QAAQ,YAAY,KAAK,EAAI,OAAO,QAAQ,EAAE;IAG9E,QAAQ,SAAS;GACnB;SACK;GAEL,IAAM,IAAU,EAAS,QAAQ,MAAM,EAAE,OAAO,YAAY,EAAE,OAAO;GAErE,IAAI,EAAQ,SAAS,GAAG;IACtB,QAAQ,MAAM,mBAAmB,EAAM,gCAAgC;IAEvE,KAAK,IAAM,KAAO,GAChB,QAAQ,MAAM,KAAK,EAAI,OAAO,QAAQ,YAAY,KAAK,EAAI,QAAQ,OAAO,EAAI,OAAO,QAAQ,EAAE;IAGjG,QAAQ,SAAS;GACnB;EACF;EAIA,OAFA,IAAW,GAEJ;CACT,GAEyB,CAAO;AAClC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../../errors.cjs"),t=require("../../tracking.cjs");var n={scheduling:t.createSchedulingState(),scopeCleanups:null,tracking:null},r=()=>({scheduling:t.createSchedulingState(),scopeCleanups:null,tracking:null}),i=null,a={get:()=>i?.get()??n,run:(e,t)=>i?i.run(e,t):t()},o=e=>{i=e,t._installContextHook(e===null?null:a)},s=()=>{if(typeof require>`u`)throw new e.RippleEnvironmentError(`createAsyncProvider() requires Node.js (async_hooks) and cannot be used in browser environments.`);let{AsyncLocalStorage:t}=require("async_hooks"),r=new t;return{get:()=>r.getStore()??n,run:(e,t)=>r.run(e,t)}},c=(e,t)=>e.run(r(),t),l=(e,n)=>{let o=i;i=e;let s=t._installContextHook(a);try{return e.run(r(),n)}finally{i=o,t._installContextHook(s)}};exports.createAsyncProvider=s,exports.runWithProvider=c,exports.setTrackingProvider=o,exports.withProvider=l;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/ssr/index.ts"],"sourcesContent":["/**\n * /ripple/ssr\n *\n * SSR helpers for installing a custom tracking context provider.\n * Import this sub-entry **only** in server-side code — it is not needed for browser builds.\n *\n * @example\n * ```ts\n * // frameworks/hono.ts\n * import { createAsyncProvider, setTrackingProvider } from '@vielzeug/ripple/ssr';\n *\n * const provider = createAsyncProvider();\n * setTrackingProvider(provider);\n *\n * // Inside a request handler:\n * await runWithProvider(provider, () => renderToString(App));\n * ```\n */\n\nimport { RippleEnvironmentError } from '../errors';\nimport { _installContextHook, createSchedulingState, type ContextHook, type ExecutionContext } from '../tracking';\n\n// eslint-disable-next-line no-var\ndeclare var require: (id: string) => unknown;\n\ninterface AsyncLocalStorageType<T> {\n getStore(): T | undefined;\n run<R>(store: T, callback: () => R): R;\n}\n\n// ── Public interfaces ─────────────────────────────────────────────────────────\n\n/**\n * The clean starting context used as a fallback when no request is active\n * (e.g. a signal write outside any `runWithProvider`/`withProvider` call) —\n * a single shared instance is fine here since there's no concurrent request to isolate from.\n */\nconst EMPTY_CTX: ExecutionContext = { scheduling: createSchedulingState(), scopeCleanups: null, tracking: null };\n\n/**\n * Builds a fresh, isolated starting context for one request — no tracking, no scope,\n * and its own `SchedulingState` so this request's `batch()`/flush queue never shares\n * state with a concurrently-running request. Must be called anew per `run()` — reusing\n * a single context object across requests would defeat scheduling isolation.\n */\nconst freshRequestContext = (): ExecutionContext => ({\n scheduling: createSchedulingState(),\n scopeCleanups: null,\n tracking: null,\n});\n\nexport interface TrackingProvider {\n get(): ExecutionContext;\n run<T>(ctx: ExecutionContext, fn: () => T): T;\n}\n\n// ── Sync provider (simple stack-based) ───────────────────────────────────────\n\nlet _currentProvider: TrackingProvider | null = null;\n\nconst syncHook: ContextHook = {\n get: () => _currentProvider?.get() ?? EMPTY_CTX,\n run: <T>(ctx: ExecutionContext, fn: () => T): T => {\n if (_currentProvider) return _currentProvider.run(ctx, fn);\n\n return fn();\n },\n};\n\nexport const setTrackingProvider = (provider: TrackingProvider | null): void => {\n _currentProvider = provider;\n _installContextHook(provider !== null ? syncHook : null);\n};\n\n// ── Async provider (AsyncLocalStorage-based) ─────────────────────────────────\n\n/**\n * Creates a provider that uses `AsyncLocalStorage` to carry the full execution\n * context (tracking + scope cleanups) across async boundaries.\n * Requires Node.js 16+ or a compatible runtime.\n *\n * @example\n * ```ts\n * const provider = createAsyncProvider();\n * setTrackingProvider(provider);\n *\n * // In a request handler:\n * await runWithProvider(provider, requestHandler);\n * ```\n */\nexport const createAsyncProvider = (): TrackingProvider => {\n // Dynamic require so this module can be bundled for environments without AsyncLocalStorage.\n // In browser builds, this code path should never be reached.\n if (typeof require === 'undefined') {\n throw new RippleEnvironmentError(\n 'createAsyncProvider() requires Node.js (async_hooks) and cannot be used in browser environments.',\n );\n }\n\n const { AsyncLocalStorage } = require('async_hooks') as { AsyncLocalStorage: new <T>() => AsyncLocalStorageType<T> };\n const storage = new AsyncLocalStorage<ExecutionContext>();\n\n return {\n get: () => storage.getStore() ?? EMPTY_CTX,\n run: <T>(ctx: ExecutionContext, fn: () => T): T => storage.run(ctx, fn),\n };\n};\n\n/**\n * Runs `fn` inside the given provider with a clean (no tracking, no scope) context.\n * Signals read inside `fn` will not register any tracking deps — equivalent to\n * starting a clean reactive root for each request.\n *\n * @example\n * ```ts\n * const html = await runWithProvider(provider, () => renderToString(App));\n * ```\n */\nexport const runWithProvider = <T>(provider: TrackingProvider, fn: () => T): T =>\n provider.run(freshRequestContext(), fn);\n\n// ── Convenience: run once without installing a persistent provider ─────────────\n\n/**\n * Temporarily installs a provider, runs `fn`, then restores the previous provider.\n * Useful for one-off server renders without modifying global state.\n */\nexport const withProvider = <T>(provider: TrackingProvider, fn: () => T): T => {\n const prevProvider = _currentProvider;\n\n _currentProvider = provider;\n\n const prevHook = _installContextHook(syncHook);\n\n try {\n return provider.run(freshRequestContext(), fn);\n } finally {\n _currentProvider = prevProvider;\n _installContextHook(prevHook);\n }\n};\n"],"mappings":"uIAqCA,IAAM,EAA8B,CAAE,WAAY,EAAA,sBAAsB,EAAG,cAAe,KAAM,SAAU,IAAK,EAQzG,OAA+C,CACnD,WAAY,EAAA,sBAAsB,EAClC,cAAe,KACf,SAAU,IACZ,GASI,EAA4C,KAE1C,EAAwB,CAC5B,QAAW,GAAkB,IAAI,GAAK,EACtC,KAAS,EAAuB,IAC1B,EAAyB,EAAiB,IAAI,EAAK,CAAE,EAElD,EAAG,CAEd,EAEa,EAAuB,GAA4C,CAC9E,EAAmB,EACnB,EAAA,oBAAoB,IAAa,KAAkB,KAAX,CAAe,CACzD,EAkBa,MAA8C,CAGzD,GAAI,OAAO,QAAY,IACrB,MAAM,IAAI,EAAA,uBACR,kGACF,EAGF,GAAM,CAAE,qBAAsB,QAAQ,aAAa,EAC7C,EAAU,IAAI,EAEpB,MAAO,CACL,QAAW,EAAQ,SAAS,GAAK,EACjC,KAAS,EAAuB,IAAmB,EAAQ,IAAI,EAAK,CAAE,CACxE,CACF,EAYa,GAAsB,EAA4B,IAC7D,EAAS,IAAI,EAAoB,EAAG,CAAE,EAQ3B,GAAmB,EAA4B,IAAmB,CAC7E,IAAM,EAAe,EAErB,EAAmB,EAEnB,IAAM,EAAW,EAAA,oBAAoB,CAAQ,EAE7C,GAAI,CACF,OAAO,EAAS,IAAI,EAAoB,EAAG,CAAE,CAC/C,QAAU,CACR,EAAmB,EACnB,EAAA,oBAAoB,CAAQ,CAC9B,CACF"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { __require as e } from "../../_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { RippleEnvironmentError as t } from "../../errors.js";
|
|
3
|
+
import { _installContextHook as n, createSchedulingState as r } from "../../tracking.js";
|
|
4
|
+
//#region src/ssr/index.ts
|
|
5
|
+
var i = {
|
|
6
|
+
scheduling: r(),
|
|
7
|
+
scopeCleanups: null,
|
|
8
|
+
tracking: null
|
|
9
|
+
}, a = () => ({
|
|
10
|
+
scheduling: r(),
|
|
11
|
+
scopeCleanups: null,
|
|
12
|
+
tracking: null
|
|
13
|
+
}), o = null, s = {
|
|
14
|
+
get: () => o?.get() ?? i,
|
|
15
|
+
run: (e, t) => o ? o.run(e, t) : t()
|
|
16
|
+
}, c = (e) => {
|
|
17
|
+
o = e, n(e === null ? null : s);
|
|
18
|
+
}, l = () => {
|
|
19
|
+
if (e === void 0) throw new t("createAsyncProvider() requires Node.js (async_hooks) and cannot be used in browser environments.");
|
|
20
|
+
let { AsyncLocalStorage: n } = e("async_hooks"), r = new n();
|
|
21
|
+
return {
|
|
22
|
+
get: () => r.getStore() ?? i,
|
|
23
|
+
run: (e, t) => r.run(e, t)
|
|
24
|
+
};
|
|
25
|
+
}, u = (e, t) => e.run(a(), t), d = (e, t) => {
|
|
26
|
+
let r = o;
|
|
27
|
+
o = e;
|
|
28
|
+
let i = n(s);
|
|
29
|
+
try {
|
|
30
|
+
return e.run(a(), t);
|
|
31
|
+
} finally {
|
|
32
|
+
o = r, n(i);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { l as createAsyncProvider, u as runWithProvider, c as setTrackingProvider, d as withProvider };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/ssr/index.ts"],"sourcesContent":["/**\n * /ripple/ssr\n *\n * SSR helpers for installing a custom tracking context provider.\n * Import this sub-entry **only** in server-side code — it is not needed for browser builds.\n *\n * @example\n * ```ts\n * // frameworks/hono.ts\n * import { createAsyncProvider, setTrackingProvider } from '@vielzeug/ripple/ssr';\n *\n * const provider = createAsyncProvider();\n * setTrackingProvider(provider);\n *\n * // Inside a request handler:\n * await runWithProvider(provider, () => renderToString(App));\n * ```\n */\n\nimport { RippleEnvironmentError } from '../errors';\nimport { _installContextHook, createSchedulingState, type ContextHook, type ExecutionContext } from '../tracking';\n\n// eslint-disable-next-line no-var\ndeclare var require: (id: string) => unknown;\n\ninterface AsyncLocalStorageType<T> {\n getStore(): T | undefined;\n run<R>(store: T, callback: () => R): R;\n}\n\n// ── Public interfaces ─────────────────────────────────────────────────────────\n\n/**\n * The clean starting context used as a fallback when no request is active\n * (e.g. a signal write outside any `runWithProvider`/`withProvider` call) —\n * a single shared instance is fine here since there's no concurrent request to isolate from.\n */\nconst EMPTY_CTX: ExecutionContext = { scheduling: createSchedulingState(), scopeCleanups: null, tracking: null };\n\n/**\n * Builds a fresh, isolated starting context for one request — no tracking, no scope,\n * and its own `SchedulingState` so this request's `batch()`/flush queue never shares\n * state with a concurrently-running request. Must be called anew per `run()` — reusing\n * a single context object across requests would defeat scheduling isolation.\n */\nconst freshRequestContext = (): ExecutionContext => ({\n scheduling: createSchedulingState(),\n scopeCleanups: null,\n tracking: null,\n});\n\nexport interface TrackingProvider {\n get(): ExecutionContext;\n run<T>(ctx: ExecutionContext, fn: () => T): T;\n}\n\n// ── Sync provider (simple stack-based) ───────────────────────────────────────\n\nlet _currentProvider: TrackingProvider | null = null;\n\nconst syncHook: ContextHook = {\n get: () => _currentProvider?.get() ?? EMPTY_CTX,\n run: <T>(ctx: ExecutionContext, fn: () => T): T => {\n if (_currentProvider) return _currentProvider.run(ctx, fn);\n\n return fn();\n },\n};\n\nexport const setTrackingProvider = (provider: TrackingProvider | null): void => {\n _currentProvider = provider;\n _installContextHook(provider !== null ? syncHook : null);\n};\n\n// ── Async provider (AsyncLocalStorage-based) ─────────────────────────────────\n\n/**\n * Creates a provider that uses `AsyncLocalStorage` to carry the full execution\n * context (tracking + scope cleanups) across async boundaries.\n * Requires Node.js 16+ or a compatible runtime.\n *\n * @example\n * ```ts\n * const provider = createAsyncProvider();\n * setTrackingProvider(provider);\n *\n * // In a request handler:\n * await runWithProvider(provider, requestHandler);\n * ```\n */\nexport const createAsyncProvider = (): TrackingProvider => {\n // Dynamic require so this module can be bundled for environments without AsyncLocalStorage.\n // In browser builds, this code path should never be reached.\n if (typeof require === 'undefined') {\n throw new RippleEnvironmentError(\n 'createAsyncProvider() requires Node.js (async_hooks) and cannot be used in browser environments.',\n );\n }\n\n const { AsyncLocalStorage } = require('async_hooks') as { AsyncLocalStorage: new <T>() => AsyncLocalStorageType<T> };\n const storage = new AsyncLocalStorage<ExecutionContext>();\n\n return {\n get: () => storage.getStore() ?? EMPTY_CTX,\n run: <T>(ctx: ExecutionContext, fn: () => T): T => storage.run(ctx, fn),\n };\n};\n\n/**\n * Runs `fn` inside the given provider with a clean (no tracking, no scope) context.\n * Signals read inside `fn` will not register any tracking deps — equivalent to\n * starting a clean reactive root for each request.\n *\n * @example\n * ```ts\n * const html = await runWithProvider(provider, () => renderToString(App));\n * ```\n */\nexport const runWithProvider = <T>(provider: TrackingProvider, fn: () => T): T =>\n provider.run(freshRequestContext(), fn);\n\n// ── Convenience: run once without installing a persistent provider ─────────────\n\n/**\n * Temporarily installs a provider, runs `fn`, then restores the previous provider.\n * Useful for one-off server renders without modifying global state.\n */\nexport const withProvider = <T>(provider: TrackingProvider, fn: () => T): T => {\n const prevProvider = _currentProvider;\n\n _currentProvider = provider;\n\n const prevHook = _installContextHook(syncHook);\n\n try {\n return provider.run(freshRequestContext(), fn);\n } finally {\n _currentProvider = prevProvider;\n _installContextHook(prevHook);\n }\n};\n"],"mappings":";;;;AAqCA,IAAM,IAA8B;CAAE,YAAY,EAAsB;CAAG,eAAe;CAAM,UAAU;AAAK,GAQzG,WAA+C;CACnD,YAAY,EAAsB;CAClC,eAAe;CACf,UAAU;AACZ,IASI,IAA4C,MAE1C,IAAwB;CAC5B,WAAW,GAAkB,IAAI,KAAK;CACtC,MAAS,GAAuB,MAC1B,IAAyB,EAAiB,IAAI,GAAK,CAAE,IAElD,EAAG;AAEd,GAEa,KAAuB,MAA4C;CAE9E,AADA,IAAmB,GACnB,EAAoB,MAAa,OAAkB,OAAX,CAAe;AACzD,GAkBa,UAA8C;CAGzD,IAAI,MAAmB,QACrB,MAAM,IAAI,EACR,kGACF;CAGF,IAAM,EAAE,yBAAA,EAA8B,aAAa,GAC7C,IAAU,IAAI,EAAoC;CAExD,OAAO;EACL,WAAW,EAAQ,SAAS,KAAK;EACjC,MAAS,GAAuB,MAAmB,EAAQ,IAAI,GAAK,CAAE;CACxE;AACF,GAYa,KAAsB,GAA4B,MAC7D,EAAS,IAAI,EAAoB,GAAG,CAAE,GAQ3B,KAAmB,GAA4B,MAAmB;CAC7E,IAAM,IAAe;CAErB,IAAmB;CAEnB,IAAM,IAAW,EAAoB,CAAQ;CAE7C,IAAI;EACF,OAAO,EAAS,IAAI,EAAoB,GAAG,CAAE;CAC/C,UAAU;EAER,AADA,IAAmB,GACnB,EAAoB,CAAQ;CAC9B;AACF"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /ripple/ssr
|
|
3
|
+
*
|
|
4
|
+
* SSR helpers for installing a custom tracking context provider.
|
|
5
|
+
* Import this sub-entry **only** in server-side code — it is not needed for browser builds.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // frameworks/hono.ts
|
|
10
|
+
* import { createAsyncProvider, setTrackingProvider } from '@vielzeug/ripple/ssr';
|
|
11
|
+
*
|
|
12
|
+
* const provider = createAsyncProvider();
|
|
13
|
+
* setTrackingProvider(provider);
|
|
14
|
+
*
|
|
15
|
+
* // Inside a request handler:
|
|
16
|
+
* await runWithProvider(provider, () => renderToString(App));
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import { type ExecutionContext } from '../tracking';
|
|
20
|
+
export interface TrackingProvider {
|
|
21
|
+
get(): ExecutionContext;
|
|
22
|
+
run<T>(ctx: ExecutionContext, fn: () => T): T;
|
|
23
|
+
}
|
|
24
|
+
export declare const setTrackingProvider: (provider: TrackingProvider | null) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Creates a provider that uses `AsyncLocalStorage` to carry the full execution
|
|
27
|
+
* context (tracking + scope cleanups) across async boundaries.
|
|
28
|
+
* Requires Node.js 16+ or a compatible runtime.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const provider = createAsyncProvider();
|
|
33
|
+
* setTrackingProvider(provider);
|
|
34
|
+
*
|
|
35
|
+
* // In a request handler:
|
|
36
|
+
* await runWithProvider(provider, requestHandler);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare const createAsyncProvider: () => TrackingProvider;
|
|
40
|
+
/**
|
|
41
|
+
* Runs `fn` inside the given provider with a clean (no tracking, no scope) context.
|
|
42
|
+
* Signals read inside `fn` will not register any tracking deps — equivalent to
|
|
43
|
+
* starting a clean reactive root for each request.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const html = await runWithProvider(provider, () => renderToString(App));
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare const runWithProvider: <T>(provider: TrackingProvider, fn: () => T) => T;
|
|
51
|
+
/**
|
|
52
|
+
* Temporarily installs a provider, runs `fn`, then restores the previous provider.
|
|
53
|
+
* Useful for one-off server renders without modifying global state.
|
|
54
|
+
*/
|
|
55
|
+
export declare const withProvider: <T>(provider: TrackingProvider, fn: () => T) => T;
|
|
56
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ssr/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAgE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA+BlH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,IAAI,gBAAgB,CAAC;IACxB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;CAC/C;AAeD,eAAO,MAAM,mBAAmB,GAAI,UAAU,gBAAgB,GAAG,IAAI,KAAG,IAGvE,CAAC;AAIF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,QAAO,gBAgBtC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,EAAE,UAAU,gBAAgB,EAAE,IAAI,MAAM,CAAC,KAAG,CACpC,CAAC;AAI1C;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,EAAE,UAAU,gBAAgB,EAAE,IAAI,MAAM,CAAC,KAAG,CAazE,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./signal.cjs"),t=require("./store.cjs"),n=require("./utilities.cjs");var r=(e,t)=>({label:t,state:Object.freeze(structuredClone({...e}))}),i=(i,a)=>{let o=a?.maxHistory??50,s=!n.isStore(i),c=s?t.store(i,{name:a?.name}):i,l=[r(c.peek())],u=e.signal(0,{name:a?.name?`${a.name}.cursor`:void 0}),d=!1,f=e=>{d||(l.splice(u.peek()+1),l.push(r(c.peek(),e)),l.length>o&&l.splice(0,l.length-o),u.value=l.length-1)},p=()=>{d||u.peek()<=0||(u.value=u.peek()-1,c.replace(()=>l[u.peek()].state))},m=()=>{d||u.peek()>=l.length-1||(u.value=u.peek()+1,c.replace(()=>l[u.peek()].state))},h=e=>{if(!(e<0||e>=l.length))return l[e]},g=()=>{d||(d=!0,u.dispose(),s&&c.dispose())};return{get canRedo(){return u.value<l.length-1},get canUndo(){return u.value>0},dispose:g,get disposed(){return d},historyAt:h,get historyLength(){return l.length},lens:e=>c.lens(e),get name(){return c.name},patch:e=>c.patch(e),peek:()=>c.peek(),push:()=>f(),pushNamed:e=>f(e),redo:m,replace:e=>c.replace(e),reset:()=>c.reset(),get store(){return c},subscribe:e=>c.subscribe(e),[Symbol.dispose]:g,undo:p,get value(){return c.value}}};exports.storeWithHistory=i;
|
|
2
|
+
//# sourceMappingURL=store-history.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-history.cjs","names":[],"sources":["../src/store-history.ts"],"sourcesContent":["import type { HistoryEntry, Store, StoreWithHistory } from './types';\n\nimport { signal } from './signal';\nimport { store } from './store';\nimport { isStore } from './utilities';\n\nconst snapshot = <T extends object>(state: Readonly<T>, label?: string): HistoryEntry<T> => ({\n label,\n state: Object.freeze(structuredClone({ ...state })) as Readonly<T>,\n});\n\n/**\n * Wraps a store (or creates one from an initial value) with explicit snapshot history\n * for undo/redo / time-travel.\n *\n * **Ownership:** if called with an initial value, the adapter creates and owns\n * the underlying store — `dispose()` will also dispose it. If called with an\n * existing `Store<T>`, the adapter does NOT own it — `dispose()` leaves the\n * store alive.\n *\n * History is **explicit** — call `push()` or `pushNamed()` after mutations you want\n * to be undoable. This gives you full control over snapshot granularity: you can\n * batch multiple mutations before pushing, or avoid pushing for programmatic\n * operations like server sync or reset-and-reload.\n *\n * This is a pure external adapter — it does not access any store internals.\n *\n * @example\n * ```ts\n * const h = storeWithHistory({ count: 0 }, { maxHistory: 10 });\n *\n * h.patch({ count: 1 });\n * h.push(); // save checkpoint\n * h.patch({ count: 2 });\n * h.pushNamed('increment');\n *\n * h.undo(); // back to { count: 1 }\n * h.redo(); // forward to { count: 2 }\n * h.historyAt(0); // { label: undefined, state: { count: 0 } }\n * h.canUndo; // true\n *\n * // Wrap an existing store:\n * const existingStore = store({ x: 0 });\n * const h2 = storeWithHistory(existingStore);\n * h2.dispose(); // existingStore is NOT disposed\n * ```\n */\nexport const storeWithHistory = <T extends object>(\n storeOrInitial: Store<T> | T,\n options?: { maxHistory?: number; name?: string },\n): StoreWithHistory<T> => {\n const maxHistory = options?.maxHistory ?? 50;\n const ownsStore = !isStore(storeOrInitial);\n const base: Store<T> = ownsStore ? store(storeOrInitial as T, { name: options?.name }) : (storeOrInitial as Store<T>);\n\n const snapshots: HistoryEntry<T>[] = [snapshot(base.peek())];\n\n // cursor is a reactive signal so canUndo/canRedo participate in the reactive graph.\n const cursor = signal(0, { name: options?.name ? `${options.name}.cursor` : undefined });\n\n let disposed = false;\n\n const pushSnapshot = (label?: string): void => {\n if (disposed) return;\n\n snapshots.splice(cursor.peek() + 1);\n snapshots.push(snapshot(base.peek(), label));\n\n if (snapshots.length > maxHistory) {\n snapshots.splice(0, snapshots.length - maxHistory);\n }\n\n cursor.value = snapshots.length - 1;\n };\n\n const undo = (): void => {\n if (disposed || cursor.peek() <= 0) return;\n\n cursor.value = cursor.peek() - 1;\n base.replace(() => snapshots[cursor.peek()]!.state as T);\n };\n\n const redo = (): void => {\n if (disposed || cursor.peek() >= snapshots.length - 1) return;\n\n cursor.value = cursor.peek() + 1;\n base.replace(() => snapshots[cursor.peek()]!.state as T);\n };\n\n const historyAt = (index: number): HistoryEntry<T> | undefined => {\n if (index < 0 || index >= snapshots.length) return undefined;\n\n return snapshots[index];\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n cursor.dispose();\n\n if (ownsStore) base.dispose();\n };\n\n // Explicit delegation: all Store<T> methods are forwarded directly to base.\n // History-specific properties are defined inline.\n const adapter: StoreWithHistory<T> = {\n get canRedo() {\n return cursor.value < snapshots.length - 1;\n },\n // History-specific\n get canUndo() {\n return cursor.value > 0;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n historyAt,\n get historyLength() {\n return snapshots.length;\n },\n lens: <P extends string>(path: P) => base.lens(path),\n // Store<T> delegation\n get name() {\n return base.name;\n },\n patch: (partial) => base.patch(partial),\n peek: () => base.peek(),\n push: () => pushSnapshot(),\n pushNamed: (label: string) => pushSnapshot(label),\n redo,\n replace: (fn) => base.replace(fn),\n reset: () => base.reset(),\n get store() {\n return base;\n },\n subscribe: (listener) => base.subscribe(listener),\n [Symbol.dispose]: dispose,\n undo,\n get value() {\n return base.value;\n },\n };\n\n return adapter;\n};\n"],"mappings":"sFAMA,IAAM,GAA8B,EAAoB,KAAqC,CAC3F,QACA,MAAO,OAAO,OAAO,gBAAgB,CAAE,GAAG,CAAM,CAAC,CAAC,CACpD,GAsCa,GACX,EACA,IACwB,CACxB,IAAM,EAAa,GAAS,YAAc,GACpC,EAAY,CAAC,EAAA,QAAQ,CAAc,EACnC,EAAiB,EAAY,EAAA,MAAM,EAAqB,CAAE,KAAM,GAAS,IAAK,CAAC,EAAK,EAEpF,EAA+B,CAAC,EAAS,EAAK,KAAK,CAAC,CAAC,EAGrD,EAAS,EAAA,OAAO,EAAG,CAAE,KAAM,GAAS,KAAO,GAAG,EAAQ,KAAK,SAAW,IAAA,EAAU,CAAC,EAEnF,EAAW,GAET,EAAgB,GAAyB,CACzC,IAEJ,EAAU,OAAO,EAAO,KAAK,EAAI,CAAC,EAClC,EAAU,KAAK,EAAS,EAAK,KAAK,EAAG,CAAK,CAAC,EAEvC,EAAU,OAAS,GACrB,EAAU,OAAO,EAAG,EAAU,OAAS,CAAU,EAGnD,EAAO,MAAQ,EAAU,OAAS,EACpC,EAEM,MAAmB,CACnB,GAAY,EAAO,KAAK,GAAK,IAEjC,EAAO,MAAQ,EAAO,KAAK,EAAI,EAC/B,EAAK,YAAc,EAAU,EAAO,KAAK,EAAE,CAAE,KAAU,EACzD,EAEM,MAAmB,CACnB,GAAY,EAAO,KAAK,GAAK,EAAU,OAAS,IAEpD,EAAO,MAAQ,EAAO,KAAK,EAAI,EAC/B,EAAK,YAAc,EAAU,EAAO,KAAK,EAAE,CAAE,KAAU,EACzD,EAEM,EAAa,GAA+C,CAC5D,OAAQ,GAAK,GAAS,EAAU,QAEpC,OAAO,EAAU,EACnB,EAEM,MAAsB,CACtB,IAEJ,EAAW,GACX,EAAO,QAAQ,EAEX,GAAW,EAAK,QAAQ,EAC9B,EA2CA,MAAO,CAtCL,IAAI,SAAU,CACZ,OAAO,EAAO,MAAQ,EAAU,OAAS,CAC3C,EAEA,IAAI,SAAU,CACZ,OAAO,EAAO,MAAQ,CACxB,EACA,UACA,IAAI,UAAW,CACb,OAAO,CACT,EACA,YACA,IAAI,eAAgB,CAClB,OAAO,EAAU,MACnB,EACA,KAAyB,GAAY,EAAK,KAAK,CAAI,EAEnD,IAAI,MAAO,CACT,OAAO,EAAK,IACd,EACA,MAAQ,GAAY,EAAK,MAAM,CAAO,EACtC,SAAY,EAAK,KAAK,EACtB,SAAY,EAAa,EACzB,UAAY,GAAkB,EAAa,CAAK,EAChD,OACA,QAAU,GAAO,EAAK,QAAQ,CAAE,EAChC,UAAa,EAAK,MAAM,EACxB,IAAI,OAAQ,CACV,OAAO,CACT,EACA,UAAY,GAAa,EAAK,UAAU,CAAQ,GAC/C,OAAO,SAAU,EAClB,OACA,IAAI,OAAQ,CACV,OAAO,EAAK,KACd,CAGK,CACT"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Store, StoreWithHistory } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps a store (or creates one from an initial value) with explicit snapshot history
|
|
4
|
+
* for undo/redo / time-travel.
|
|
5
|
+
*
|
|
6
|
+
* **Ownership:** if called with an initial value, the adapter creates and owns
|
|
7
|
+
* the underlying store — `dispose()` will also dispose it. If called with an
|
|
8
|
+
* existing `Store<T>`, the adapter does NOT own it — `dispose()` leaves the
|
|
9
|
+
* store alive.
|
|
10
|
+
*
|
|
11
|
+
* History is **explicit** — call `push()` or `pushNamed()` after mutations you want
|
|
12
|
+
* to be undoable. This gives you full control over snapshot granularity: you can
|
|
13
|
+
* batch multiple mutations before pushing, or avoid pushing for programmatic
|
|
14
|
+
* operations like server sync or reset-and-reload.
|
|
15
|
+
*
|
|
16
|
+
* This is a pure external adapter — it does not access any store internals.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const h = storeWithHistory({ count: 0 }, { maxHistory: 10 });
|
|
21
|
+
*
|
|
22
|
+
* h.patch({ count: 1 });
|
|
23
|
+
* h.push(); // save checkpoint
|
|
24
|
+
* h.patch({ count: 2 });
|
|
25
|
+
* h.pushNamed('increment');
|
|
26
|
+
*
|
|
27
|
+
* h.undo(); // back to { count: 1 }
|
|
28
|
+
* h.redo(); // forward to { count: 2 }
|
|
29
|
+
* h.historyAt(0); // { label: undefined, state: { count: 0 } }
|
|
30
|
+
* h.canUndo; // true
|
|
31
|
+
*
|
|
32
|
+
* // Wrap an existing store:
|
|
33
|
+
* const existingStore = store({ x: 0 });
|
|
34
|
+
* const h2 = storeWithHistory(existingStore);
|
|
35
|
+
* h2.dispose(); // existingStore is NOT disposed
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const storeWithHistory: <T extends object>(storeOrInitial: Store<T> | T, options?: {
|
|
39
|
+
maxHistory?: number;
|
|
40
|
+
name?: string;
|
|
41
|
+
}) => StoreWithHistory<T>;
|
|
42
|
+
//# sourceMappingURL=store-history.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-history.d.ts","sourceRoot":"","sources":["../src/store-history.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAWrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,MAAM,EAC/C,gBAAgB,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,EAC5B,UAAU;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,KAC/C,gBAAgB,CAAC,CAAC,CAgGpB,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { signal as e } from "./signal.js";
|
|
2
|
+
import { store as t } from "./store.js";
|
|
3
|
+
import { isStore as n } from "./utilities.js";
|
|
4
|
+
//#region src/store-history.ts
|
|
5
|
+
var r = (e, t) => ({
|
|
6
|
+
label: t,
|
|
7
|
+
state: Object.freeze(structuredClone({ ...e }))
|
|
8
|
+
}), i = (i, a) => {
|
|
9
|
+
let o = a?.maxHistory ?? 50, s = !n(i), c = s ? t(i, { name: a?.name }) : i, l = [r(c.peek())], u = e(0, { name: a?.name ? `${a.name}.cursor` : void 0 }), d = !1, f = (e) => {
|
|
10
|
+
d || (l.splice(u.peek() + 1), l.push(r(c.peek(), e)), l.length > o && l.splice(0, l.length - o), u.value = l.length - 1);
|
|
11
|
+
}, p = () => {
|
|
12
|
+
d || u.peek() <= 0 || (u.value = u.peek() - 1, c.replace(() => l[u.peek()].state));
|
|
13
|
+
}, m = () => {
|
|
14
|
+
d || u.peek() >= l.length - 1 || (u.value = u.peek() + 1, c.replace(() => l[u.peek()].state));
|
|
15
|
+
}, h = (e) => {
|
|
16
|
+
if (!(e < 0 || e >= l.length)) return l[e];
|
|
17
|
+
}, g = () => {
|
|
18
|
+
d || (d = !0, u.dispose(), s && c.dispose());
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
get canRedo() {
|
|
22
|
+
return u.value < l.length - 1;
|
|
23
|
+
},
|
|
24
|
+
get canUndo() {
|
|
25
|
+
return u.value > 0;
|
|
26
|
+
},
|
|
27
|
+
dispose: g,
|
|
28
|
+
get disposed() {
|
|
29
|
+
return d;
|
|
30
|
+
},
|
|
31
|
+
historyAt: h,
|
|
32
|
+
get historyLength() {
|
|
33
|
+
return l.length;
|
|
34
|
+
},
|
|
35
|
+
lens: (e) => c.lens(e),
|
|
36
|
+
get name() {
|
|
37
|
+
return c.name;
|
|
38
|
+
},
|
|
39
|
+
patch: (e) => c.patch(e),
|
|
40
|
+
peek: () => c.peek(),
|
|
41
|
+
push: () => f(),
|
|
42
|
+
pushNamed: (e) => f(e),
|
|
43
|
+
redo: m,
|
|
44
|
+
replace: (e) => c.replace(e),
|
|
45
|
+
reset: () => c.reset(),
|
|
46
|
+
get store() {
|
|
47
|
+
return c;
|
|
48
|
+
},
|
|
49
|
+
subscribe: (e) => c.subscribe(e),
|
|
50
|
+
[Symbol.dispose]: g,
|
|
51
|
+
undo: p,
|
|
52
|
+
get value() {
|
|
53
|
+
return c.value;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
export { i as storeWithHistory };
|
|
59
|
+
|
|
60
|
+
//# sourceMappingURL=store-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-history.js","names":[],"sources":["../src/store-history.ts"],"sourcesContent":["import type { HistoryEntry, Store, StoreWithHistory } from './types';\n\nimport { signal } from './signal';\nimport { store } from './store';\nimport { isStore } from './utilities';\n\nconst snapshot = <T extends object>(state: Readonly<T>, label?: string): HistoryEntry<T> => ({\n label,\n state: Object.freeze(structuredClone({ ...state })) as Readonly<T>,\n});\n\n/**\n * Wraps a store (or creates one from an initial value) with explicit snapshot history\n * for undo/redo / time-travel.\n *\n * **Ownership:** if called with an initial value, the adapter creates and owns\n * the underlying store — `dispose()` will also dispose it. If called with an\n * existing `Store<T>`, the adapter does NOT own it — `dispose()` leaves the\n * store alive.\n *\n * History is **explicit** — call `push()` or `pushNamed()` after mutations you want\n * to be undoable. This gives you full control over snapshot granularity: you can\n * batch multiple mutations before pushing, or avoid pushing for programmatic\n * operations like server sync or reset-and-reload.\n *\n * This is a pure external adapter — it does not access any store internals.\n *\n * @example\n * ```ts\n * const h = storeWithHistory({ count: 0 }, { maxHistory: 10 });\n *\n * h.patch({ count: 1 });\n * h.push(); // save checkpoint\n * h.patch({ count: 2 });\n * h.pushNamed('increment');\n *\n * h.undo(); // back to { count: 1 }\n * h.redo(); // forward to { count: 2 }\n * h.historyAt(0); // { label: undefined, state: { count: 0 } }\n * h.canUndo; // true\n *\n * // Wrap an existing store:\n * const existingStore = store({ x: 0 });\n * const h2 = storeWithHistory(existingStore);\n * h2.dispose(); // existingStore is NOT disposed\n * ```\n */\nexport const storeWithHistory = <T extends object>(\n storeOrInitial: Store<T> | T,\n options?: { maxHistory?: number; name?: string },\n): StoreWithHistory<T> => {\n const maxHistory = options?.maxHistory ?? 50;\n const ownsStore = !isStore(storeOrInitial);\n const base: Store<T> = ownsStore ? store(storeOrInitial as T, { name: options?.name }) : (storeOrInitial as Store<T>);\n\n const snapshots: HistoryEntry<T>[] = [snapshot(base.peek())];\n\n // cursor is a reactive signal so canUndo/canRedo participate in the reactive graph.\n const cursor = signal(0, { name: options?.name ? `${options.name}.cursor` : undefined });\n\n let disposed = false;\n\n const pushSnapshot = (label?: string): void => {\n if (disposed) return;\n\n snapshots.splice(cursor.peek() + 1);\n snapshots.push(snapshot(base.peek(), label));\n\n if (snapshots.length > maxHistory) {\n snapshots.splice(0, snapshots.length - maxHistory);\n }\n\n cursor.value = snapshots.length - 1;\n };\n\n const undo = (): void => {\n if (disposed || cursor.peek() <= 0) return;\n\n cursor.value = cursor.peek() - 1;\n base.replace(() => snapshots[cursor.peek()]!.state as T);\n };\n\n const redo = (): void => {\n if (disposed || cursor.peek() >= snapshots.length - 1) return;\n\n cursor.value = cursor.peek() + 1;\n base.replace(() => snapshots[cursor.peek()]!.state as T);\n };\n\n const historyAt = (index: number): HistoryEntry<T> | undefined => {\n if (index < 0 || index >= snapshots.length) return undefined;\n\n return snapshots[index];\n };\n\n const dispose = (): void => {\n if (disposed) return;\n\n disposed = true;\n cursor.dispose();\n\n if (ownsStore) base.dispose();\n };\n\n // Explicit delegation: all Store<T> methods are forwarded directly to base.\n // History-specific properties are defined inline.\n const adapter: StoreWithHistory<T> = {\n get canRedo() {\n return cursor.value < snapshots.length - 1;\n },\n // History-specific\n get canUndo() {\n return cursor.value > 0;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n historyAt,\n get historyLength() {\n return snapshots.length;\n },\n lens: <P extends string>(path: P) => base.lens(path),\n // Store<T> delegation\n get name() {\n return base.name;\n },\n patch: (partial) => base.patch(partial),\n peek: () => base.peek(),\n push: () => pushSnapshot(),\n pushNamed: (label: string) => pushSnapshot(label),\n redo,\n replace: (fn) => base.replace(fn),\n reset: () => base.reset(),\n get store() {\n return base;\n },\n subscribe: (listener) => base.subscribe(listener),\n [Symbol.dispose]: dispose,\n undo,\n get value() {\n return base.value;\n },\n };\n\n return adapter;\n};\n"],"mappings":";;;;AAMA,IAAM,KAA8B,GAAoB,OAAqC;CAC3F;CACA,OAAO,OAAO,OAAO,gBAAgB,EAAE,GAAG,EAAM,CAAC,CAAC;AACpD,IAsCa,KACX,GACA,MACwB;CACxB,IAAM,IAAa,GAAS,cAAc,IACpC,IAAY,CAAC,EAAQ,CAAc,GACnC,IAAiB,IAAY,EAAM,GAAqB,EAAE,MAAM,GAAS,KAAK,CAAC,IAAK,GAEpF,IAA+B,CAAC,EAAS,EAAK,KAAK,CAAC,CAAC,GAGrD,IAAS,EAAO,GAAG,EAAE,MAAM,GAAS,OAAO,GAAG,EAAQ,KAAK,WAAW,KAAA,EAAU,CAAC,GAEnF,IAAW,IAET,KAAgB,MAAyB;EACzC,MAEJ,EAAU,OAAO,EAAO,KAAK,IAAI,CAAC,GAClC,EAAU,KAAK,EAAS,EAAK,KAAK,GAAG,CAAK,CAAC,GAEvC,EAAU,SAAS,KACrB,EAAU,OAAO,GAAG,EAAU,SAAS,CAAU,GAGnD,EAAO,QAAQ,EAAU,SAAS;CACpC,GAEM,UAAmB;EACnB,KAAY,EAAO,KAAK,KAAK,MAEjC,EAAO,QAAQ,EAAO,KAAK,IAAI,GAC/B,EAAK,cAAc,EAAU,EAAO,KAAK,EAAE,CAAE,KAAU;CACzD,GAEM,UAAmB;EACnB,KAAY,EAAO,KAAK,KAAK,EAAU,SAAS,MAEpD,EAAO,QAAQ,EAAO,KAAK,IAAI,GAC/B,EAAK,cAAc,EAAU,EAAO,KAAK,EAAE,CAAE,KAAU;CACzD,GAEM,KAAa,MAA+C;EAC5D,UAAQ,KAAK,KAAS,EAAU,SAEpC,OAAO,EAAU;CACnB,GAEM,UAAsB;EACtB,MAEJ,IAAW,IACX,EAAO,QAAQ,GAEX,KAAW,EAAK,QAAQ;CAC9B;CA2CA,OAAO;EAtCL,IAAI,UAAU;GACZ,OAAO,EAAO,QAAQ,EAAU,SAAS;EAC3C;EAEA,IAAI,UAAU;GACZ,OAAO,EAAO,QAAQ;EACxB;EACA;EACA,IAAI,WAAW;GACb,OAAO;EACT;EACA;EACA,IAAI,gBAAgB;GAClB,OAAO,EAAU;EACnB;EACA,OAAyB,MAAY,EAAK,KAAK,CAAI;EAEnD,IAAI,OAAO;GACT,OAAO,EAAK;EACd;EACA,QAAQ,MAAY,EAAK,MAAM,CAAO;EACtC,YAAY,EAAK,KAAK;EACtB,YAAY,EAAa;EACzB,YAAY,MAAkB,EAAa,CAAK;EAChD;EACA,UAAU,MAAO,EAAK,QAAQ,CAAE;EAChC,aAAa,EAAK,MAAM;EACxB,IAAI,QAAQ;GACV,OAAO;EACT;EACA,YAAY,MAAa,EAAK,UAAU,CAAQ;GAC/C,OAAO,UAAU;EAClB;EACA,IAAI,QAAQ;GACV,OAAO,EAAK;EACd;CAGK;AACT"}
|
package/dist/store.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./devtools-hook.cjs"),t=require("./errors.cjs"),n=require("./scheduling.cjs"),r=require("./symbols.cjs"),i=require("./computed.cjs"),a=require("./signal.cjs");var o=e=>{if(e===`__proto__`||e===`constructor`||e===`prototype`)throw new t.RippleInvalidStoreError(`Unsafe key "${e}" rejected to prevent prototype pollution.`)},s=(e,t)=>{let n=e;for(let e of t){if(typeof n!=`object`||!n)return;n=n[e]}return n},c=(e,n,r)=>{if(n.length===0)return r;if(typeof e!=`object`||!e)throw new t.RippleInvalidStoreError(`Cannot write to a nested path through a null or non-object intermediate value.`);let[i,...a]=n,o=e;return{...o,[i]:c(o[i],a,r)}},l=class{[r.IS_SIGNAL]=!0;source_;write_;evict_;disposeSource_;name_;get name(){return this.name_??this.source_.name}constructor(e){this.source_=e.source,this.write_=e.write,this.evict_=e.evict,this.disposeSource_=e.disposeSource,this.name_=e.name}get value(){return this.source_.value}set value(e){this.write_(e)}peek(){return this.source_.peek()}subscribe(e){return this.source_.subscribe(e)}get disposed(){return this.source_.disposed}dispose(){this.evict_(),this.disposeSource_?.()}[Symbol.dispose](){this.dispose()}},u=class{[r.IS_SIGNAL]=!0;[r.IS_STORE]=!0;name;current_;disposed_;initial_;lensCache_;propSignals_;readonlyProxy_;version_;constructor(e,n){this.name=n,this.current_=structuredClone(e),this.disposed_=!1,this.initial_=structuredClone(e),this.lensCache_=new Map,this.propSignals_=new Map,this.version_=new a.SignalImpl(0,void 0,n?`${n}.version`:void 0),this.readonlyProxy_=new Proxy(this.current_,{deleteProperty(e,n){throw new t.RippleInvalidStoreError(`Direct deletion from store.value is not allowed. Use store.patch() or store.lens(). (key: "${String(n)}")`)},set(e,n){throw new t.RippleInvalidStoreError(`Direct mutation of store.value is not allowed. Use store.patch(), store.lens(), or store.replace(). (key: "${String(n)}")`)}})}propSignalFor_(e){let t=this.propSignals_.get(e);return t===void 0&&(t=new a.SignalImpl(this.current_[e],void 0,this.name===void 0?e:`${this.name}.${e}`),this.propSignals_.set(e,t)),t}setPath_(e,t){let[n,...r]=e,i=this.current_[n],a=c(i,r,t);this.applyTopLevelChange_(n,a)}applyTopLevelChange_(e,t){o(e);let n=this.current_[e];Object.is(n,t)||(this.current_[e]=t,this.propSignalFor_(e).value=t,this.version_.value=this.version_.peek()+1)}deleteTopLevelKey_(e){Object.hasOwn(this.current_,e)&&(delete this.current_[e],this.propSignalFor_(e).value=void 0,this.version_.value=this.version_.peek()+1)}applyFullState_(e){let t=this.current_,r=new Set([...Object.keys(t),...Object.keys(e)]);for(let e of r)o(e);n.batch(()=>{for(let t of r)Object.hasOwn(e,t)?this.applyTopLevelChange_(t,e[t]):this.deleteTopLevelKey_(t)})}get value(){return this.version_.value,this.readonlyProxy_}peek(){return Object.freeze(structuredClone(this.current_))}subscribe=e=>this.version_.subscribe(e);patch(r){if(typeof r!=`object`||!r||Array.isArray(r))throw new t.RippleInvalidStoreError(`store.patch() requires a plain object partial.`);let i=Object.keys(r);if(i.length!==0){for(let e of i)o(e);n.batch(()=>{for(let e of i)this.applyTopLevelChange_(e,r[e])}),e.getDevToolsHook()?.mutate?.({kind:`patch`,name:this.name})}}replace(t){let n=structuredClone(this.current_),r=t(n);r!==n&&(this.applyFullState_(r),e.getDevToolsHook()?.mutate?.({kind:`replace`,name:this.name}))}reset(){this.applyFullState_(this.initial_),e.getDevToolsHook()?.mutate?.({kind:`reset`,name:this.name})}lens(r){let a=this.lensCache_.get(r);if(a!==void 0)return a;let o=()=>{this.lensCache_.delete(r)},c=r.split(`.`);if(c.length>32)throw new t.RippleInvalidStoreError(`Lens path exceeds maximum depth of 32 segments: "${r}".`);for(let e of c){if(e===``)throw new t.RippleInvalidStoreError(`Empty path segment in lens path "${r}". Check for consecutive dots.`);if(e===`__proto__`||e===`constructor`||e===`prototype`)throw new t.RippleInvalidStoreError(`Unsafe path segment "${e}" in lens path "${r}".`)}let u;if(c.length===1)u=new l({evict:o,source:this.propSignalFor_(c[0]),write:t=>{n.batch(()=>this.applyTopLevelChange_(c[0],t)),e.getDevToolsHook()?.mutate?.({kind:`lens`,name:this.name,path:r})}});else{let t=i.computed(()=>s(this.propSignalFor_(c[0]).value,c.slice(1)));u=new l({disposeSource:()=>t.dispose(),evict:o,name:this.name?`${this.name}.${r}`:r,source:t,write:i=>{let a=t.peek();Object.is(a,i)||(n.batch(()=>this.setPath_(c,i)),e.getDevToolsHook()?.mutate?.({kind:`lens`,name:this.name,path:r}))}})}return this.lensCache_.set(r,u),u}get disposed(){return this.disposed_}dispose(){if(!this.disposed_){this.disposed_=!0;for(let e of[...this.lensCache_.values()])e.dispose();this.lensCache_.clear();for(let e of this.propSignals_.values())e.dispose();this.propSignals_.clear(),this.version_.dispose(),e.getDevToolsHook()?.dispose?.({kind:`store`,name:this.name})}}[Symbol.dispose](){this.dispose()}},d=(e,n)=>{if(typeof e!=`object`||!e||Array.isArray(e))throw new t.RippleInvalidStoreError(`store() requires a plain object initial state.`);for(let t of Object.keys(e))o(t);return new u(e,n?.name)};exports.StoreImpl=u,exports.store=d;
|
|
2
|
+
//# sourceMappingURL=store.cjs.map
|