@vorra/core 0.3.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/CHANGELOG.md +15 -0
- package/dist/chunk-X_3F6bsP.js +31 -0
- package/dist/di.cjs +273 -0
- package/dist/di.cjs.map +1 -0
- package/dist/di.d.ts +175 -0
- package/dist/di.d.ts.map +1 -0
- package/dist/di.js +261 -0
- package/dist/di.js.map +1 -0
- package/dist/dom.cjs +302 -0
- package/dist/dom.cjs.map +1 -0
- package/dist/dom.d.ts +136 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +285 -0
- package/dist/dom.js.map +1 -0
- package/dist/index.cjs +38 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/reactivity.cjs +232 -0
- package/dist/reactivity.cjs.map +1 -0
- package/dist/reactivity.d.ts +88 -0
- package/dist/reactivity.d.ts.map +1 -0
- package/dist/reactivity.js +226 -0
- package/dist/reactivity.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/reactivity.ts
|
|
3
|
+
/**
|
|
4
|
+
* The currently-executing reactive context (effect or computed).
|
|
5
|
+
* When a signal is read, it registers itself as a dependency of this context.
|
|
6
|
+
*/
|
|
7
|
+
let activeContext = null;
|
|
8
|
+
/**
|
|
9
|
+
* Batch depth counter. When > 0, signal writes are queued rather than
|
|
10
|
+
* flushed immediately.
|
|
11
|
+
*/
|
|
12
|
+
let batchDepth = 0;
|
|
13
|
+
/** Queue of effects to re-run after the current batch completes. */
|
|
14
|
+
const pendingEffects = /* @__PURE__ */ new Set();
|
|
15
|
+
function trackDep(node) {
|
|
16
|
+
if (activeContext === null) return;
|
|
17
|
+
node.subscribers.add(activeContext);
|
|
18
|
+
activeContext.deps.add(node);
|
|
19
|
+
}
|
|
20
|
+
function unsubscribeContext(ctx) {
|
|
21
|
+
for (const dep of ctx.deps) dep.subscribers.delete(ctx);
|
|
22
|
+
ctx.deps.clear();
|
|
23
|
+
}
|
|
24
|
+
function notifySubscribers(node) {
|
|
25
|
+
for (const sub of [...node.subscribers]) sub.notify();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Groups multiple signal writes into a single flush, preventing intermediate
|
|
29
|
+
* effect executions. Effects only run once after the outermost batch ends.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* batch(() => {
|
|
33
|
+
* firstName.set('Jane');
|
|
34
|
+
* lastName.set('Doe');
|
|
35
|
+
* });
|
|
36
|
+
* // Effects depending on either signal run exactly once.
|
|
37
|
+
*/
|
|
38
|
+
function batch(fn) {
|
|
39
|
+
batchDepth++;
|
|
40
|
+
try {
|
|
41
|
+
fn();
|
|
42
|
+
} finally {
|
|
43
|
+
batchDepth--;
|
|
44
|
+
if (batchDepth === 0) flushEffects();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function flushEffects() {
|
|
48
|
+
while (pendingEffects.size > 0) {
|
|
49
|
+
const snapshot = [...pendingEffects];
|
|
50
|
+
pendingEffects.clear();
|
|
51
|
+
for (const node of snapshot) if (!node.destroyed) runEffect(node);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function scheduleEffect(node) {
|
|
55
|
+
if (node.scheduled || node.destroyed) return;
|
|
56
|
+
node.scheduled = true;
|
|
57
|
+
pendingEffects.add(node);
|
|
58
|
+
if (batchDepth === 0) flushEffects();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Creates a reactive signal — a piece of state that automatically notifies
|
|
62
|
+
* any effects or computed values that read it when it changes.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const count = signal(0);
|
|
66
|
+
* count(); // read → 0
|
|
67
|
+
* count.set(1); // write
|
|
68
|
+
* count.update(n => n + 1); // functional update
|
|
69
|
+
*/
|
|
70
|
+
function signal(initialValue, options) {
|
|
71
|
+
const node = {
|
|
72
|
+
value: initialValue,
|
|
73
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
74
|
+
equals: options?.equals ?? Object.is
|
|
75
|
+
};
|
|
76
|
+
function getter() {
|
|
77
|
+
trackDep(node);
|
|
78
|
+
return node.value;
|
|
79
|
+
}
|
|
80
|
+
function setter(value) {
|
|
81
|
+
if (node.equals(node.value, value)) return;
|
|
82
|
+
node.value = value;
|
|
83
|
+
notifySubscribers(node);
|
|
84
|
+
}
|
|
85
|
+
const writableSignal = getter;
|
|
86
|
+
Object.defineProperty(writableSignal, "__type", { value: "signal" });
|
|
87
|
+
writableSignal.set = setter;
|
|
88
|
+
writableSignal.update = (fn) => {
|
|
89
|
+
setter(fn(node.value));
|
|
90
|
+
};
|
|
91
|
+
writableSignal.asReadonly = () => {
|
|
92
|
+
const ro = (() => {
|
|
93
|
+
trackDep(node);
|
|
94
|
+
return node.value;
|
|
95
|
+
});
|
|
96
|
+
Object.defineProperty(ro, "__type", { value: "signal" });
|
|
97
|
+
return ro;
|
|
98
|
+
};
|
|
99
|
+
return writableSignal;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Creates a lazily-evaluated derived value. Re-evaluates only when one of
|
|
103
|
+
* its signal dependencies changes, and only when read.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* const count = signal(2);
|
|
107
|
+
* const doubled = computed(() => count() * 2);
|
|
108
|
+
* doubled(); // → 4
|
|
109
|
+
*/
|
|
110
|
+
function computed(compute, options) {
|
|
111
|
+
const node = {
|
|
112
|
+
dirty: true,
|
|
113
|
+
value: void 0,
|
|
114
|
+
deps: /* @__PURE__ */ new Set(),
|
|
115
|
+
compute,
|
|
116
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
117
|
+
equals: options?.equals ?? Object.is,
|
|
118
|
+
notify() {
|
|
119
|
+
if (!node.dirty) {
|
|
120
|
+
node.dirty = true;
|
|
121
|
+
notifySubscribers(node);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
function getter() {
|
|
126
|
+
if (activeContext !== null) {
|
|
127
|
+
node.subscribers.add(activeContext);
|
|
128
|
+
activeContext.deps.add(node);
|
|
129
|
+
}
|
|
130
|
+
if (node.dirty) {
|
|
131
|
+
unsubscribeContext(node);
|
|
132
|
+
const prevContext = activeContext;
|
|
133
|
+
activeContext = node;
|
|
134
|
+
try {
|
|
135
|
+
const newValue = compute();
|
|
136
|
+
if (node.value === void 0 || !node.equals(node.value, newValue)) node.value = newValue;
|
|
137
|
+
} finally {
|
|
138
|
+
activeContext = prevContext;
|
|
139
|
+
node.dirty = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return node.value;
|
|
143
|
+
}
|
|
144
|
+
const computedSignal = getter;
|
|
145
|
+
Object.defineProperty(computedSignal, "__type", { value: "signal" });
|
|
146
|
+
return computedSignal;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Runs a side-effect function immediately and re-runs it whenever any signal
|
|
150
|
+
* read inside it changes.
|
|
151
|
+
*
|
|
152
|
+
* The function may optionally return a cleanup function that runs before the
|
|
153
|
+
* next execution or when the effect is destroyed.
|
|
154
|
+
*
|
|
155
|
+
* @returns An EffectHandle with a `destroy()` method to stop the effect.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* const count = signal(0);
|
|
159
|
+
* const handle = effect(() => {
|
|
160
|
+
* console.log('count is', count());
|
|
161
|
+
* return () => console.log('cleanup');
|
|
162
|
+
* });
|
|
163
|
+
* handle.destroy(); // stops the effect
|
|
164
|
+
*/
|
|
165
|
+
function effect(fn) {
|
|
166
|
+
const node = {
|
|
167
|
+
fn,
|
|
168
|
+
cleanup: void 0,
|
|
169
|
+
deps: /* @__PURE__ */ new Set(),
|
|
170
|
+
scheduled: false,
|
|
171
|
+
destroyed: false,
|
|
172
|
+
notify() {
|
|
173
|
+
scheduleEffect(node);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
runEffect(node);
|
|
177
|
+
return { destroy() {
|
|
178
|
+
if (node.destroyed) return;
|
|
179
|
+
node.destroyed = true;
|
|
180
|
+
if (typeof node.cleanup === "function") node.cleanup();
|
|
181
|
+
unsubscribeContext(node);
|
|
182
|
+
pendingEffects.delete(node);
|
|
183
|
+
} };
|
|
184
|
+
}
|
|
185
|
+
function runEffect(node) {
|
|
186
|
+
if (node.destroyed) return;
|
|
187
|
+
if (typeof node.cleanup === "function") {
|
|
188
|
+
node.cleanup();
|
|
189
|
+
node.cleanup = void 0;
|
|
190
|
+
}
|
|
191
|
+
unsubscribeContext(node);
|
|
192
|
+
node.scheduled = false;
|
|
193
|
+
const prevContext = activeContext;
|
|
194
|
+
activeContext = node;
|
|
195
|
+
try {
|
|
196
|
+
node.cleanup = node.fn();
|
|
197
|
+
} finally {
|
|
198
|
+
activeContext = prevContext;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Reads signals inside `fn` without registering them as dependencies of the
|
|
203
|
+
* current reactive context. Useful for reading state in an effect without
|
|
204
|
+
* creating subscriptions.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* effect(() => {
|
|
208
|
+
* triggerSignal(); // subscribed
|
|
209
|
+
* const val = untrack(() => otherSignal()); // NOT subscribed
|
|
210
|
+
* });
|
|
211
|
+
*/
|
|
212
|
+
function untrack(fn) {
|
|
213
|
+
const prevContext = activeContext;
|
|
214
|
+
activeContext = null;
|
|
215
|
+
try {
|
|
216
|
+
return fn();
|
|
217
|
+
} finally {
|
|
218
|
+
activeContext = prevContext;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function isSignal(value) {
|
|
222
|
+
return typeof value === "function" && value.__type === "signal";
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
exports.batch = batch;
|
|
226
|
+
exports.computed = computed;
|
|
227
|
+
exports.effect = effect;
|
|
228
|
+
exports.isSignal = isSignal;
|
|
229
|
+
exports.signal = signal;
|
|
230
|
+
exports.untrack = untrack;
|
|
231
|
+
|
|
232
|
+
//# sourceMappingURL=reactivity.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reactivity.cjs","names":[],"sources":["../src/reactivity.ts"],"sourcesContent":["// =============================================================================\r\n// Vorra Reactivity Core\r\n// signal() / computed() / effect() / batch() / untrack()\r\n// =============================================================================\r\n\r\n// ---------------------------------------------------------------------------\r\n// Types\r\n// ---------------------------------------------------------------------------\r\n\r\nexport type SignalGetter<T> = () => T;\r\nexport type SignalSetter<T> = (value: T | ((prev: T) => T)) => void;\r\nexport type Signal<T> = [SignalGetter<T>, SignalSetter<T>];\r\n\r\nexport interface ReadonlySignal<T> {\r\n (): T;\r\n readonly __type: 'signal';\r\n}\r\n\r\nexport interface WritableSignal<T> extends ReadonlySignal<T> {\r\n set(value: T): void;\r\n update(fn: (prev: T) => T): void;\r\n asReadonly(): ReadonlySignal<T>;\r\n}\r\n\r\nexport interface ComputedSignal<T> extends ReadonlySignal<T> {\r\n readonly __type: 'signal';\r\n}\r\n\r\nexport interface EffectHandle {\r\n /** Stops the effect from running again and releases all subscriptions. */\r\n destroy(): void;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal tracking state\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * The currently-executing reactive context (effect or computed).\r\n * When a signal is read, it registers itself as a dependency of this context.\r\n */\r\nlet activeContext: ReactiveContext | null = null;\r\n\r\n/**\r\n * Batch depth counter. When > 0, signal writes are queued rather than\r\n * flushed immediately.\r\n */\r\nlet batchDepth = 0;\r\n\r\n/** Queue of effects to re-run after the current batch completes. */\r\nconst pendingEffects = new Set<EffectNode>();\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal node types\r\n// ---------------------------------------------------------------------------\r\n\r\ninterface ReactiveContext {\r\n /** Called when a dependency notifies this context of a change. */\r\n notify(): void;\r\n /** The set of signal nodes this context is currently subscribed to. */\r\n deps: Set<SignalNode<unknown>>;\r\n}\r\n\r\ninterface SignalNode<T> {\r\n value: T;\r\n /** All reactive contexts currently subscribed to this signal. */\r\n subscribers: Set<ReactiveContext>;\r\n /** Equality check — defaults to Object.is */\r\n equals: (a: T, b: T) => boolean;\r\n}\r\n\r\ninterface ComputedNode<T> extends ReactiveContext {\r\n dirty: boolean;\r\n value: T | undefined;\r\n deps: Set<SignalNode<unknown>>;\r\n compute: () => T;\r\n subscribers: Set<ReactiveContext>;\r\n equals: (a: T, b: T) => boolean;\r\n}\r\n\r\ninterface EffectNode extends ReactiveContext {\r\n fn: () => void | (() => void);\r\n cleanup: (() => void) | void;\r\n deps: Set<SignalNode<unknown>>;\r\n scheduled: boolean;\r\n destroyed: boolean;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Dependency tracking helpers\r\n// ---------------------------------------------------------------------------\r\n\r\nfunction trackDep<T>(node: SignalNode<T>): void {\r\n if (activeContext === null) return;\r\n node.subscribers.add(activeContext);\r\n activeContext.deps.add(node as SignalNode<unknown>);\r\n}\r\n\r\nfunction unsubscribeContext(ctx: ReactiveContext): void {\r\n for (const dep of ctx.deps) {\r\n dep.subscribers.delete(ctx);\r\n }\r\n ctx.deps.clear();\r\n}\r\n\r\nfunction notifySubscribers(node: SignalNode<unknown>): void {\r\n // Copy subscribers before iterating — a subscriber's notify() could\r\n // mutate the set (e.g. a computed re-subscribing).\r\n for (const sub of [...node.subscribers]) {\r\n sub.notify();\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Batch / flush\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Groups multiple signal writes into a single flush, preventing intermediate\r\n * effect executions. Effects only run once after the outermost batch ends.\r\n *\r\n * @example\r\n * batch(() => {\r\n * firstName.set('Jane');\r\n * lastName.set('Doe');\r\n * });\r\n * // Effects depending on either signal run exactly once.\r\n */\r\nexport function batch(fn: () => void): void {\r\n batchDepth++;\r\n try {\r\n fn();\r\n } finally {\r\n batchDepth--;\r\n if (batchDepth === 0) flushEffects();\r\n }\r\n}\r\n\r\nfunction flushEffects(): void {\r\n // Drain the pending set. Effects may schedule new effects during flush,\r\n // so we loop until the set is empty.\r\n while (pendingEffects.size > 0) {\r\n const snapshot = [...pendingEffects];\r\n pendingEffects.clear();\r\n for (const node of snapshot) {\r\n if (!node.destroyed) runEffect(node);\r\n }\r\n }\r\n}\r\n\r\nfunction scheduleEffect(node: EffectNode): void {\r\n if (node.scheduled || node.destroyed) return;\r\n node.scheduled = true;\r\n pendingEffects.add(node);\r\n if (batchDepth === 0) flushEffects();\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// signal()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Creates a reactive signal — a piece of state that automatically notifies\r\n * any effects or computed values that read it when it changes.\r\n *\r\n * @example\r\n * const count = signal(0);\r\n * count(); // read → 0\r\n * count.set(1); // write\r\n * count.update(n => n + 1); // functional update\r\n */\r\nexport function signal<T>(\r\n initialValue: T,\r\n options?: { equals?: (a: T, b: T) => boolean }\r\n): WritableSignal<T> {\r\n const node: SignalNode<T> = {\r\n value: initialValue,\r\n subscribers: new Set(),\r\n equals: options?.equals ?? Object.is,\r\n };\r\n\r\n function getter(): T {\r\n trackDep(node);\r\n return node.value;\r\n }\r\n\r\n function setter(value: T): void {\r\n if (node.equals(node.value, value)) return;\r\n node.value = value;\r\n notifySubscribers(node as SignalNode<unknown>);\r\n }\r\n\r\n const writableSignal = getter as WritableSignal<T>;\r\n\r\n Object.defineProperty(writableSignal, '__type', { value: 'signal' });\r\n\r\n writableSignal.set = setter;\r\n\r\n writableSignal.update = (fn: (prev: T) => T): void => {\r\n setter(fn(node.value));\r\n };\r\n\r\n writableSignal.asReadonly = (): ReadonlySignal<T> => {\r\n const ro = (() => {\r\n trackDep(node);\r\n return node.value;\r\n }) as ReadonlySignal<T>;\r\n Object.defineProperty(ro, '__type', { value: 'signal' });\r\n return ro;\r\n };\r\n\r\n return writableSignal;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// computed()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Creates a lazily-evaluated derived value. Re-evaluates only when one of\r\n * its signal dependencies changes, and only when read.\r\n *\r\n * @example\r\n * const count = signal(2);\r\n * const doubled = computed(() => count() * 2);\r\n * doubled(); // → 4\r\n */\r\nexport function computed<T>(\r\n compute: () => T,\r\n options?: { equals?: (a: T, b: T) => boolean }\r\n): ComputedSignal<T> {\r\n const node: ComputedNode<T> = {\r\n dirty: true,\r\n value: undefined,\r\n deps: new Set(),\r\n compute,\r\n subscribers: new Set(),\r\n equals: options?.equals ?? Object.is,\r\n notify() {\r\n if (!node.dirty) {\r\n node.dirty = true;\r\n // Propagate dirtiness to downstream subscribers without re-evaluating.\r\n notifySubscribers(node as unknown as SignalNode<unknown>);\r\n }\r\n },\r\n };\r\n\r\n function getter(): T {\r\n // Register this computed as a dependency of the outer context.\r\n if (activeContext !== null) {\r\n (node as unknown as SignalNode<unknown>).subscribers.add(activeContext);\r\n activeContext.deps.add(node as unknown as SignalNode<unknown>);\r\n }\r\n\r\n if (node.dirty) {\r\n // Unsubscribe from old deps before re-running.\r\n unsubscribeContext(node);\r\n\r\n const prevContext = activeContext;\r\n activeContext = node;\r\n try {\r\n const newValue = compute();\r\n if (node.value === undefined || !node.equals(node.value as T, newValue)) {\r\n node.value = newValue;\r\n }\r\n } finally {\r\n activeContext = prevContext;\r\n node.dirty = false;\r\n }\r\n }\r\n\r\n return node.value as T;\r\n }\r\n\r\n const computedSignal = getter as ComputedSignal<T>;\r\n Object.defineProperty(computedSignal, '__type', { value: 'signal' });\r\n return computedSignal;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// effect()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Runs a side-effect function immediately and re-runs it whenever any signal\r\n * read inside it changes.\r\n *\r\n * The function may optionally return a cleanup function that runs before the\r\n * next execution or when the effect is destroyed.\r\n *\r\n * @returns An EffectHandle with a `destroy()` method to stop the effect.\r\n *\r\n * @example\r\n * const count = signal(0);\r\n * const handle = effect(() => {\r\n * console.log('count is', count());\r\n * return () => console.log('cleanup');\r\n * });\r\n * handle.destroy(); // stops the effect\r\n */\r\nexport function effect(fn: () => void | (() => void)): EffectHandle {\r\n const node: EffectNode = {\r\n fn,\r\n cleanup: undefined,\r\n deps: new Set(),\r\n scheduled: false,\r\n destroyed: false,\r\n notify() {\r\n scheduleEffect(node);\r\n },\r\n };\r\n\r\n // Run immediately (synchronously).\r\n runEffect(node);\r\n\r\n return {\r\n destroy() {\r\n if (node.destroyed) return;\r\n node.destroyed = true;\r\n if (typeof node.cleanup === 'function') node.cleanup();\r\n unsubscribeContext(node);\r\n pendingEffects.delete(node);\r\n },\r\n };\r\n}\r\n\r\nfunction runEffect(node: EffectNode): void {\r\n if (node.destroyed) return;\r\n\r\n // Run previous cleanup.\r\n if (typeof node.cleanup === 'function') {\r\n node.cleanup();\r\n node.cleanup = undefined;\r\n }\r\n\r\n // Unsubscribe from previous deps before re-tracking.\r\n unsubscribeContext(node);\r\n\r\n node.scheduled = false;\r\n\r\n const prevContext = activeContext;\r\n activeContext = node;\r\n try {\r\n node.cleanup = node.fn() as (() => void) | void;\r\n } finally {\r\n activeContext = prevContext;\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// untrack()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Reads signals inside `fn` without registering them as dependencies of the\r\n * current reactive context. Useful for reading state in an effect without\r\n * creating subscriptions.\r\n *\r\n * @example\r\n * effect(() => {\r\n * triggerSignal(); // subscribed\r\n * const val = untrack(() => otherSignal()); // NOT subscribed\r\n * });\r\n */\r\nexport function untrack<T>(fn: () => T): T {\r\n const prevContext = activeContext;\r\n activeContext = null;\r\n try {\r\n return fn();\r\n } finally {\r\n activeContext = prevContext;\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// isSignal() type guard\r\n// ---------------------------------------------------------------------------\r\n\r\nexport function isSignal(value: unknown): value is ReadonlySignal<unknown> {\r\n return typeof value === 'function' && (value as ReadonlySignal<unknown>).__type === 'signal';\r\n}\r\n"],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type SignalGetter<T> = () => T;
|
|
2
|
+
export type SignalSetter<T> = (value: T | ((prev: T) => T)) => void;
|
|
3
|
+
export type Signal<T> = [SignalGetter<T>, SignalSetter<T>];
|
|
4
|
+
export interface ReadonlySignal<T> {
|
|
5
|
+
(): T;
|
|
6
|
+
readonly __type: 'signal';
|
|
7
|
+
}
|
|
8
|
+
export interface WritableSignal<T> extends ReadonlySignal<T> {
|
|
9
|
+
set(value: T): void;
|
|
10
|
+
update(fn: (prev: T) => T): void;
|
|
11
|
+
asReadonly(): ReadonlySignal<T>;
|
|
12
|
+
}
|
|
13
|
+
export interface ComputedSignal<T> extends ReadonlySignal<T> {
|
|
14
|
+
readonly __type: 'signal';
|
|
15
|
+
}
|
|
16
|
+
export interface EffectHandle {
|
|
17
|
+
/** Stops the effect from running again and releases all subscriptions. */
|
|
18
|
+
destroy(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Groups multiple signal writes into a single flush, preventing intermediate
|
|
22
|
+
* effect executions. Effects only run once after the outermost batch ends.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* batch(() => {
|
|
26
|
+
* firstName.set('Jane');
|
|
27
|
+
* lastName.set('Doe');
|
|
28
|
+
* });
|
|
29
|
+
* // Effects depending on either signal run exactly once.
|
|
30
|
+
*/
|
|
31
|
+
export declare function batch(fn: () => void): void;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a reactive signal — a piece of state that automatically notifies
|
|
34
|
+
* any effects or computed values that read it when it changes.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const count = signal(0);
|
|
38
|
+
* count(); // read → 0
|
|
39
|
+
* count.set(1); // write
|
|
40
|
+
* count.update(n => n + 1); // functional update
|
|
41
|
+
*/
|
|
42
|
+
export declare function signal<T>(initialValue: T, options?: {
|
|
43
|
+
equals?: (a: T, b: T) => boolean;
|
|
44
|
+
}): WritableSignal<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Creates a lazily-evaluated derived value. Re-evaluates only when one of
|
|
47
|
+
* its signal dependencies changes, and only when read.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const count = signal(2);
|
|
51
|
+
* const doubled = computed(() => count() * 2);
|
|
52
|
+
* doubled(); // → 4
|
|
53
|
+
*/
|
|
54
|
+
export declare function computed<T>(compute: () => T, options?: {
|
|
55
|
+
equals?: (a: T, b: T) => boolean;
|
|
56
|
+
}): ComputedSignal<T>;
|
|
57
|
+
/**
|
|
58
|
+
* Runs a side-effect function immediately and re-runs it whenever any signal
|
|
59
|
+
* read inside it changes.
|
|
60
|
+
*
|
|
61
|
+
* The function may optionally return a cleanup function that runs before the
|
|
62
|
+
* next execution or when the effect is destroyed.
|
|
63
|
+
*
|
|
64
|
+
* @returns An EffectHandle with a `destroy()` method to stop the effect.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* const count = signal(0);
|
|
68
|
+
* const handle = effect(() => {
|
|
69
|
+
* console.log('count is', count());
|
|
70
|
+
* return () => console.log('cleanup');
|
|
71
|
+
* });
|
|
72
|
+
* handle.destroy(); // stops the effect
|
|
73
|
+
*/
|
|
74
|
+
export declare function effect(fn: () => void | (() => void)): EffectHandle;
|
|
75
|
+
/**
|
|
76
|
+
* Reads signals inside `fn` without registering them as dependencies of the
|
|
77
|
+
* current reactive context. Useful for reading state in an effect without
|
|
78
|
+
* creating subscriptions.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* effect(() => {
|
|
82
|
+
* triggerSignal(); // subscribed
|
|
83
|
+
* const val = untrack(() => otherSignal()); // NOT subscribed
|
|
84
|
+
* });
|
|
85
|
+
*/
|
|
86
|
+
export declare function untrack<T>(fn: () => T): T;
|
|
87
|
+
export declare function isSignal(value: unknown): value is ReadonlySignal<unknown>;
|
|
88
|
+
//# sourceMappingURL=reactivity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reactivity.d.ts","sourceRoot":"","sources":["../src/reactivity.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC;AACtC,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC;AACpE,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;AAE3D,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,IAAI,CAAC,CAAC;IACN,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC;IAC1D,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IACjC,UAAU,IAAI,cAAc,CAAC,CAAC,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,cAAc,CAAC,CAAC,CAAC;IAC1D,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,0EAA0E;IAC1E,OAAO,IAAI,IAAI,CAAC;CACjB;AAsFD;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAQ1C;AAyBD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,CAAC,EACtB,YAAY,EAAE,CAAC,EACf,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;CAAE,GAC7C,cAAc,CAAC,CAAC,CAAC,CAsCnB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,OAAO,EAAE,MAAM,CAAC,EAChB,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;CAAE,GAC7C,cAAc,CAAC,CAAC,CAAC,CA+CnB;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,YAAY,CAwBlE;AA6BD;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAQzC;AAMD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAAC,OAAO,CAAC,CAEzE"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
//#region src/reactivity.ts
|
|
2
|
+
/**
|
|
3
|
+
* The currently-executing reactive context (effect or computed).
|
|
4
|
+
* When a signal is read, it registers itself as a dependency of this context.
|
|
5
|
+
*/
|
|
6
|
+
let activeContext = null;
|
|
7
|
+
/**
|
|
8
|
+
* Batch depth counter. When > 0, signal writes are queued rather than
|
|
9
|
+
* flushed immediately.
|
|
10
|
+
*/
|
|
11
|
+
let batchDepth = 0;
|
|
12
|
+
/** Queue of effects to re-run after the current batch completes. */
|
|
13
|
+
const pendingEffects = /* @__PURE__ */ new Set();
|
|
14
|
+
function trackDep(node) {
|
|
15
|
+
if (activeContext === null) return;
|
|
16
|
+
node.subscribers.add(activeContext);
|
|
17
|
+
activeContext.deps.add(node);
|
|
18
|
+
}
|
|
19
|
+
function unsubscribeContext(ctx) {
|
|
20
|
+
for (const dep of ctx.deps) dep.subscribers.delete(ctx);
|
|
21
|
+
ctx.deps.clear();
|
|
22
|
+
}
|
|
23
|
+
function notifySubscribers(node) {
|
|
24
|
+
for (const sub of [...node.subscribers]) sub.notify();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Groups multiple signal writes into a single flush, preventing intermediate
|
|
28
|
+
* effect executions. Effects only run once after the outermost batch ends.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* batch(() => {
|
|
32
|
+
* firstName.set('Jane');
|
|
33
|
+
* lastName.set('Doe');
|
|
34
|
+
* });
|
|
35
|
+
* // Effects depending on either signal run exactly once.
|
|
36
|
+
*/
|
|
37
|
+
function batch(fn) {
|
|
38
|
+
batchDepth++;
|
|
39
|
+
try {
|
|
40
|
+
fn();
|
|
41
|
+
} finally {
|
|
42
|
+
batchDepth--;
|
|
43
|
+
if (batchDepth === 0) flushEffects();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function flushEffects() {
|
|
47
|
+
while (pendingEffects.size > 0) {
|
|
48
|
+
const snapshot = [...pendingEffects];
|
|
49
|
+
pendingEffects.clear();
|
|
50
|
+
for (const node of snapshot) if (!node.destroyed) runEffect(node);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function scheduleEffect(node) {
|
|
54
|
+
if (node.scheduled || node.destroyed) return;
|
|
55
|
+
node.scheduled = true;
|
|
56
|
+
pendingEffects.add(node);
|
|
57
|
+
if (batchDepth === 0) flushEffects();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Creates a reactive signal — a piece of state that automatically notifies
|
|
61
|
+
* any effects or computed values that read it when it changes.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const count = signal(0);
|
|
65
|
+
* count(); // read → 0
|
|
66
|
+
* count.set(1); // write
|
|
67
|
+
* count.update(n => n + 1); // functional update
|
|
68
|
+
*/
|
|
69
|
+
function signal(initialValue, options) {
|
|
70
|
+
const node = {
|
|
71
|
+
value: initialValue,
|
|
72
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
73
|
+
equals: options?.equals ?? Object.is
|
|
74
|
+
};
|
|
75
|
+
function getter() {
|
|
76
|
+
trackDep(node);
|
|
77
|
+
return node.value;
|
|
78
|
+
}
|
|
79
|
+
function setter(value) {
|
|
80
|
+
if (node.equals(node.value, value)) return;
|
|
81
|
+
node.value = value;
|
|
82
|
+
notifySubscribers(node);
|
|
83
|
+
}
|
|
84
|
+
const writableSignal = getter;
|
|
85
|
+
Object.defineProperty(writableSignal, "__type", { value: "signal" });
|
|
86
|
+
writableSignal.set = setter;
|
|
87
|
+
writableSignal.update = (fn) => {
|
|
88
|
+
setter(fn(node.value));
|
|
89
|
+
};
|
|
90
|
+
writableSignal.asReadonly = () => {
|
|
91
|
+
const ro = (() => {
|
|
92
|
+
trackDep(node);
|
|
93
|
+
return node.value;
|
|
94
|
+
});
|
|
95
|
+
Object.defineProperty(ro, "__type", { value: "signal" });
|
|
96
|
+
return ro;
|
|
97
|
+
};
|
|
98
|
+
return writableSignal;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Creates a lazily-evaluated derived value. Re-evaluates only when one of
|
|
102
|
+
* its signal dependencies changes, and only when read.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* const count = signal(2);
|
|
106
|
+
* const doubled = computed(() => count() * 2);
|
|
107
|
+
* doubled(); // → 4
|
|
108
|
+
*/
|
|
109
|
+
function computed(compute, options) {
|
|
110
|
+
const node = {
|
|
111
|
+
dirty: true,
|
|
112
|
+
value: void 0,
|
|
113
|
+
deps: /* @__PURE__ */ new Set(),
|
|
114
|
+
compute,
|
|
115
|
+
subscribers: /* @__PURE__ */ new Set(),
|
|
116
|
+
equals: options?.equals ?? Object.is,
|
|
117
|
+
notify() {
|
|
118
|
+
if (!node.dirty) {
|
|
119
|
+
node.dirty = true;
|
|
120
|
+
notifySubscribers(node);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
function getter() {
|
|
125
|
+
if (activeContext !== null) {
|
|
126
|
+
node.subscribers.add(activeContext);
|
|
127
|
+
activeContext.deps.add(node);
|
|
128
|
+
}
|
|
129
|
+
if (node.dirty) {
|
|
130
|
+
unsubscribeContext(node);
|
|
131
|
+
const prevContext = activeContext;
|
|
132
|
+
activeContext = node;
|
|
133
|
+
try {
|
|
134
|
+
const newValue = compute();
|
|
135
|
+
if (node.value === void 0 || !node.equals(node.value, newValue)) node.value = newValue;
|
|
136
|
+
} finally {
|
|
137
|
+
activeContext = prevContext;
|
|
138
|
+
node.dirty = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return node.value;
|
|
142
|
+
}
|
|
143
|
+
const computedSignal = getter;
|
|
144
|
+
Object.defineProperty(computedSignal, "__type", { value: "signal" });
|
|
145
|
+
return computedSignal;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Runs a side-effect function immediately and re-runs it whenever any signal
|
|
149
|
+
* read inside it changes.
|
|
150
|
+
*
|
|
151
|
+
* The function may optionally return a cleanup function that runs before the
|
|
152
|
+
* next execution or when the effect is destroyed.
|
|
153
|
+
*
|
|
154
|
+
* @returns An EffectHandle with a `destroy()` method to stop the effect.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* const count = signal(0);
|
|
158
|
+
* const handle = effect(() => {
|
|
159
|
+
* console.log('count is', count());
|
|
160
|
+
* return () => console.log('cleanup');
|
|
161
|
+
* });
|
|
162
|
+
* handle.destroy(); // stops the effect
|
|
163
|
+
*/
|
|
164
|
+
function effect(fn) {
|
|
165
|
+
const node = {
|
|
166
|
+
fn,
|
|
167
|
+
cleanup: void 0,
|
|
168
|
+
deps: /* @__PURE__ */ new Set(),
|
|
169
|
+
scheduled: false,
|
|
170
|
+
destroyed: false,
|
|
171
|
+
notify() {
|
|
172
|
+
scheduleEffect(node);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
runEffect(node);
|
|
176
|
+
return { destroy() {
|
|
177
|
+
if (node.destroyed) return;
|
|
178
|
+
node.destroyed = true;
|
|
179
|
+
if (typeof node.cleanup === "function") node.cleanup();
|
|
180
|
+
unsubscribeContext(node);
|
|
181
|
+
pendingEffects.delete(node);
|
|
182
|
+
} };
|
|
183
|
+
}
|
|
184
|
+
function runEffect(node) {
|
|
185
|
+
if (node.destroyed) return;
|
|
186
|
+
if (typeof node.cleanup === "function") {
|
|
187
|
+
node.cleanup();
|
|
188
|
+
node.cleanup = void 0;
|
|
189
|
+
}
|
|
190
|
+
unsubscribeContext(node);
|
|
191
|
+
node.scheduled = false;
|
|
192
|
+
const prevContext = activeContext;
|
|
193
|
+
activeContext = node;
|
|
194
|
+
try {
|
|
195
|
+
node.cleanup = node.fn();
|
|
196
|
+
} finally {
|
|
197
|
+
activeContext = prevContext;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Reads signals inside `fn` without registering them as dependencies of the
|
|
202
|
+
* current reactive context. Useful for reading state in an effect without
|
|
203
|
+
* creating subscriptions.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* effect(() => {
|
|
207
|
+
* triggerSignal(); // subscribed
|
|
208
|
+
* const val = untrack(() => otherSignal()); // NOT subscribed
|
|
209
|
+
* });
|
|
210
|
+
*/
|
|
211
|
+
function untrack(fn) {
|
|
212
|
+
const prevContext = activeContext;
|
|
213
|
+
activeContext = null;
|
|
214
|
+
try {
|
|
215
|
+
return fn();
|
|
216
|
+
} finally {
|
|
217
|
+
activeContext = prevContext;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function isSignal(value) {
|
|
221
|
+
return typeof value === "function" && value.__type === "signal";
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
export { batch, computed, effect, isSignal, signal, untrack };
|
|
225
|
+
|
|
226
|
+
//# sourceMappingURL=reactivity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reactivity.js","names":[],"sources":["../src/reactivity.ts"],"sourcesContent":["// =============================================================================\r\n// Vorra Reactivity Core\r\n// signal() / computed() / effect() / batch() / untrack()\r\n// =============================================================================\r\n\r\n// ---------------------------------------------------------------------------\r\n// Types\r\n// ---------------------------------------------------------------------------\r\n\r\nexport type SignalGetter<T> = () => T;\r\nexport type SignalSetter<T> = (value: T | ((prev: T) => T)) => void;\r\nexport type Signal<T> = [SignalGetter<T>, SignalSetter<T>];\r\n\r\nexport interface ReadonlySignal<T> {\r\n (): T;\r\n readonly __type: 'signal';\r\n}\r\n\r\nexport interface WritableSignal<T> extends ReadonlySignal<T> {\r\n set(value: T): void;\r\n update(fn: (prev: T) => T): void;\r\n asReadonly(): ReadonlySignal<T>;\r\n}\r\n\r\nexport interface ComputedSignal<T> extends ReadonlySignal<T> {\r\n readonly __type: 'signal';\r\n}\r\n\r\nexport interface EffectHandle {\r\n /** Stops the effect from running again and releases all subscriptions. */\r\n destroy(): void;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal tracking state\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * The currently-executing reactive context (effect or computed).\r\n * When a signal is read, it registers itself as a dependency of this context.\r\n */\r\nlet activeContext: ReactiveContext | null = null;\r\n\r\n/**\r\n * Batch depth counter. When > 0, signal writes are queued rather than\r\n * flushed immediately.\r\n */\r\nlet batchDepth = 0;\r\n\r\n/** Queue of effects to re-run after the current batch completes. */\r\nconst pendingEffects = new Set<EffectNode>();\r\n\r\n// ---------------------------------------------------------------------------\r\n// Internal node types\r\n// ---------------------------------------------------------------------------\r\n\r\ninterface ReactiveContext {\r\n /** Called when a dependency notifies this context of a change. */\r\n notify(): void;\r\n /** The set of signal nodes this context is currently subscribed to. */\r\n deps: Set<SignalNode<unknown>>;\r\n}\r\n\r\ninterface SignalNode<T> {\r\n value: T;\r\n /** All reactive contexts currently subscribed to this signal. */\r\n subscribers: Set<ReactiveContext>;\r\n /** Equality check — defaults to Object.is */\r\n equals: (a: T, b: T) => boolean;\r\n}\r\n\r\ninterface ComputedNode<T> extends ReactiveContext {\r\n dirty: boolean;\r\n value: T | undefined;\r\n deps: Set<SignalNode<unknown>>;\r\n compute: () => T;\r\n subscribers: Set<ReactiveContext>;\r\n equals: (a: T, b: T) => boolean;\r\n}\r\n\r\ninterface EffectNode extends ReactiveContext {\r\n fn: () => void | (() => void);\r\n cleanup: (() => void) | void;\r\n deps: Set<SignalNode<unknown>>;\r\n scheduled: boolean;\r\n destroyed: boolean;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Dependency tracking helpers\r\n// ---------------------------------------------------------------------------\r\n\r\nfunction trackDep<T>(node: SignalNode<T>): void {\r\n if (activeContext === null) return;\r\n node.subscribers.add(activeContext);\r\n activeContext.deps.add(node as SignalNode<unknown>);\r\n}\r\n\r\nfunction unsubscribeContext(ctx: ReactiveContext): void {\r\n for (const dep of ctx.deps) {\r\n dep.subscribers.delete(ctx);\r\n }\r\n ctx.deps.clear();\r\n}\r\n\r\nfunction notifySubscribers(node: SignalNode<unknown>): void {\r\n // Copy subscribers before iterating — a subscriber's notify() could\r\n // mutate the set (e.g. a computed re-subscribing).\r\n for (const sub of [...node.subscribers]) {\r\n sub.notify();\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Batch / flush\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Groups multiple signal writes into a single flush, preventing intermediate\r\n * effect executions. Effects only run once after the outermost batch ends.\r\n *\r\n * @example\r\n * batch(() => {\r\n * firstName.set('Jane');\r\n * lastName.set('Doe');\r\n * });\r\n * // Effects depending on either signal run exactly once.\r\n */\r\nexport function batch(fn: () => void): void {\r\n batchDepth++;\r\n try {\r\n fn();\r\n } finally {\r\n batchDepth--;\r\n if (batchDepth === 0) flushEffects();\r\n }\r\n}\r\n\r\nfunction flushEffects(): void {\r\n // Drain the pending set. Effects may schedule new effects during flush,\r\n // so we loop until the set is empty.\r\n while (pendingEffects.size > 0) {\r\n const snapshot = [...pendingEffects];\r\n pendingEffects.clear();\r\n for (const node of snapshot) {\r\n if (!node.destroyed) runEffect(node);\r\n }\r\n }\r\n}\r\n\r\nfunction scheduleEffect(node: EffectNode): void {\r\n if (node.scheduled || node.destroyed) return;\r\n node.scheduled = true;\r\n pendingEffects.add(node);\r\n if (batchDepth === 0) flushEffects();\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// signal()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Creates a reactive signal — a piece of state that automatically notifies\r\n * any effects or computed values that read it when it changes.\r\n *\r\n * @example\r\n * const count = signal(0);\r\n * count(); // read → 0\r\n * count.set(1); // write\r\n * count.update(n => n + 1); // functional update\r\n */\r\nexport function signal<T>(\r\n initialValue: T,\r\n options?: { equals?: (a: T, b: T) => boolean }\r\n): WritableSignal<T> {\r\n const node: SignalNode<T> = {\r\n value: initialValue,\r\n subscribers: new Set(),\r\n equals: options?.equals ?? Object.is,\r\n };\r\n\r\n function getter(): T {\r\n trackDep(node);\r\n return node.value;\r\n }\r\n\r\n function setter(value: T): void {\r\n if (node.equals(node.value, value)) return;\r\n node.value = value;\r\n notifySubscribers(node as SignalNode<unknown>);\r\n }\r\n\r\n const writableSignal = getter as WritableSignal<T>;\r\n\r\n Object.defineProperty(writableSignal, '__type', { value: 'signal' });\r\n\r\n writableSignal.set = setter;\r\n\r\n writableSignal.update = (fn: (prev: T) => T): void => {\r\n setter(fn(node.value));\r\n };\r\n\r\n writableSignal.asReadonly = (): ReadonlySignal<T> => {\r\n const ro = (() => {\r\n trackDep(node);\r\n return node.value;\r\n }) as ReadonlySignal<T>;\r\n Object.defineProperty(ro, '__type', { value: 'signal' });\r\n return ro;\r\n };\r\n\r\n return writableSignal;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// computed()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Creates a lazily-evaluated derived value. Re-evaluates only when one of\r\n * its signal dependencies changes, and only when read.\r\n *\r\n * @example\r\n * const count = signal(2);\r\n * const doubled = computed(() => count() * 2);\r\n * doubled(); // → 4\r\n */\r\nexport function computed<T>(\r\n compute: () => T,\r\n options?: { equals?: (a: T, b: T) => boolean }\r\n): ComputedSignal<T> {\r\n const node: ComputedNode<T> = {\r\n dirty: true,\r\n value: undefined,\r\n deps: new Set(),\r\n compute,\r\n subscribers: new Set(),\r\n equals: options?.equals ?? Object.is,\r\n notify() {\r\n if (!node.dirty) {\r\n node.dirty = true;\r\n // Propagate dirtiness to downstream subscribers without re-evaluating.\r\n notifySubscribers(node as unknown as SignalNode<unknown>);\r\n }\r\n },\r\n };\r\n\r\n function getter(): T {\r\n // Register this computed as a dependency of the outer context.\r\n if (activeContext !== null) {\r\n (node as unknown as SignalNode<unknown>).subscribers.add(activeContext);\r\n activeContext.deps.add(node as unknown as SignalNode<unknown>);\r\n }\r\n\r\n if (node.dirty) {\r\n // Unsubscribe from old deps before re-running.\r\n unsubscribeContext(node);\r\n\r\n const prevContext = activeContext;\r\n activeContext = node;\r\n try {\r\n const newValue = compute();\r\n if (node.value === undefined || !node.equals(node.value as T, newValue)) {\r\n node.value = newValue;\r\n }\r\n } finally {\r\n activeContext = prevContext;\r\n node.dirty = false;\r\n }\r\n }\r\n\r\n return node.value as T;\r\n }\r\n\r\n const computedSignal = getter as ComputedSignal<T>;\r\n Object.defineProperty(computedSignal, '__type', { value: 'signal' });\r\n return computedSignal;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// effect()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Runs a side-effect function immediately and re-runs it whenever any signal\r\n * read inside it changes.\r\n *\r\n * The function may optionally return a cleanup function that runs before the\r\n * next execution or when the effect is destroyed.\r\n *\r\n * @returns An EffectHandle with a `destroy()` method to stop the effect.\r\n *\r\n * @example\r\n * const count = signal(0);\r\n * const handle = effect(() => {\r\n * console.log('count is', count());\r\n * return () => console.log('cleanup');\r\n * });\r\n * handle.destroy(); // stops the effect\r\n */\r\nexport function effect(fn: () => void | (() => void)): EffectHandle {\r\n const node: EffectNode = {\r\n fn,\r\n cleanup: undefined,\r\n deps: new Set(),\r\n scheduled: false,\r\n destroyed: false,\r\n notify() {\r\n scheduleEffect(node);\r\n },\r\n };\r\n\r\n // Run immediately (synchronously).\r\n runEffect(node);\r\n\r\n return {\r\n destroy() {\r\n if (node.destroyed) return;\r\n node.destroyed = true;\r\n if (typeof node.cleanup === 'function') node.cleanup();\r\n unsubscribeContext(node);\r\n pendingEffects.delete(node);\r\n },\r\n };\r\n}\r\n\r\nfunction runEffect(node: EffectNode): void {\r\n if (node.destroyed) return;\r\n\r\n // Run previous cleanup.\r\n if (typeof node.cleanup === 'function') {\r\n node.cleanup();\r\n node.cleanup = undefined;\r\n }\r\n\r\n // Unsubscribe from previous deps before re-tracking.\r\n unsubscribeContext(node);\r\n\r\n node.scheduled = false;\r\n\r\n const prevContext = activeContext;\r\n activeContext = node;\r\n try {\r\n node.cleanup = node.fn() as (() => void) | void;\r\n } finally {\r\n activeContext = prevContext;\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// untrack()\r\n// ---------------------------------------------------------------------------\r\n\r\n/**\r\n * Reads signals inside `fn` without registering them as dependencies of the\r\n * current reactive context. Useful for reading state in an effect without\r\n * creating subscriptions.\r\n *\r\n * @example\r\n * effect(() => {\r\n * triggerSignal(); // subscribed\r\n * const val = untrack(() => otherSignal()); // NOT subscribed\r\n * });\r\n */\r\nexport function untrack<T>(fn: () => T): T {\r\n const prevContext = activeContext;\r\n activeContext = null;\r\n try {\r\n return fn();\r\n } finally {\r\n activeContext = prevContext;\r\n }\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// isSignal() type guard\r\n// ---------------------------------------------------------------------------\r\n\r\nexport function isSignal(value: unknown): value is ReadonlySignal<unknown> {\r\n return typeof value === 'function' && (value as ReadonlySignal<unknown>).__type === 'signal';\r\n}\r\n"],"mappings":";;;;;AAyCA,IAAI,gBAAwC;;;;;AAM5C,IAAI,aAAa;;AAGjB,MAAM,iCAAiB,IAAI,KAAiB;AA0C5C,SAAS,SAAY,MAA2B;AAC9C,KAAI,kBAAkB,KAAM;AAC5B,MAAK,YAAY,IAAI,cAAc;AACnC,eAAc,KAAK,IAAI,KAA4B;;AAGrD,SAAS,mBAAmB,KAA4B;AACtD,MAAK,MAAM,OAAO,IAAI,KACpB,KAAI,YAAY,OAAO,IAAI;AAE7B,KAAI,KAAK,OAAO;;AAGlB,SAAS,kBAAkB,MAAiC;AAG1D,MAAK,MAAM,OAAO,CAAC,GAAG,KAAK,YAAY,CACrC,KAAI,QAAQ;;;;;;;;;;;;;AAmBhB,SAAgB,MAAM,IAAsB;AAC1C;AACA,KAAI;AACF,MAAI;WACI;AACR;AACA,MAAI,eAAe,EAAG,eAAc;;;AAIxC,SAAS,eAAqB;AAG5B,QAAO,eAAe,OAAO,GAAG;EAC9B,MAAM,WAAW,CAAC,GAAG,eAAe;AACpC,iBAAe,OAAO;AACtB,OAAK,MAAM,QAAQ,SACjB,KAAI,CAAC,KAAK,UAAW,WAAU,KAAK;;;AAK1C,SAAS,eAAe,MAAwB;AAC9C,KAAI,KAAK,aAAa,KAAK,UAAW;AACtC,MAAK,YAAY;AACjB,gBAAe,IAAI,KAAK;AACxB,KAAI,eAAe,EAAG,eAAc;;;;;;;;;;;;AAiBtC,SAAgB,OACd,cACA,SACmB;CACnB,MAAM,OAAsB;EAC1B,OAAO;EACP,6BAAa,IAAI,KAAK;EACtB,QAAQ,SAAS,UAAU,OAAO;EACnC;CAED,SAAS,SAAY;AACnB,WAAS,KAAK;AACd,SAAO,KAAK;;CAGd,SAAS,OAAO,OAAgB;AAC9B,MAAI,KAAK,OAAO,KAAK,OAAO,MAAM,CAAE;AACpC,OAAK,QAAQ;AACb,oBAAkB,KAA4B;;CAGhD,MAAM,iBAAiB;AAEvB,QAAO,eAAe,gBAAgB,UAAU,EAAE,OAAO,UAAU,CAAC;AAEpE,gBAAe,MAAM;AAErB,gBAAe,UAAU,OAA6B;AACpD,SAAO,GAAG,KAAK,MAAM,CAAC;;AAGxB,gBAAe,mBAAsC;EACnD,MAAM,YAAY;AAChB,YAAS,KAAK;AACd,UAAO,KAAK;;AAEd,SAAO,eAAe,IAAI,UAAU,EAAE,OAAO,UAAU,CAAC;AACxD,SAAO;;AAGT,QAAO;;;;;;;;;;;AAgBT,SAAgB,SACd,SACA,SACmB;CACnB,MAAM,OAAwB;EAC5B,OAAO;EACP,OAAO,KAAA;EACP,sBAAM,IAAI,KAAK;EACf;EACA,6BAAa,IAAI,KAAK;EACtB,QAAQ,SAAS,UAAU,OAAO;EAClC,SAAS;AACP,OAAI,CAAC,KAAK,OAAO;AACf,SAAK,QAAQ;AAEb,sBAAkB,KAAuC;;;EAG9D;CAED,SAAS,SAAY;AAEnB,MAAI,kBAAkB,MAAM;AACzB,QAAwC,YAAY,IAAI,cAAc;AACvE,iBAAc,KAAK,IAAI,KAAuC;;AAGhE,MAAI,KAAK,OAAO;AAEd,sBAAmB,KAAK;GAExB,MAAM,cAAc;AACpB,mBAAgB;AAChB,OAAI;IACF,MAAM,WAAW,SAAS;AAC1B,QAAI,KAAK,UAAU,KAAA,KAAa,CAAC,KAAK,OAAO,KAAK,OAAY,SAAS,CACrE,MAAK,QAAQ;aAEP;AACR,oBAAgB;AAChB,SAAK,QAAQ;;;AAIjB,SAAO,KAAK;;CAGd,MAAM,iBAAiB;AACvB,QAAO,eAAe,gBAAgB,UAAU,EAAE,OAAO,UAAU,CAAC;AACpE,QAAO;;;;;;;;;;;;;;;;;;;AAwBT,SAAgB,OAAO,IAA6C;CAClE,MAAM,OAAmB;EACvB;EACA,SAAS,KAAA;EACT,sBAAM,IAAI,KAAK;EACf,WAAW;EACX,WAAW;EACX,SAAS;AACP,kBAAe,KAAK;;EAEvB;AAGD,WAAU,KAAK;AAEf,QAAO,EACL,UAAU;AACR,MAAI,KAAK,UAAW;AACpB,OAAK,YAAY;AACjB,MAAI,OAAO,KAAK,YAAY,WAAY,MAAK,SAAS;AACtD,qBAAmB,KAAK;AACxB,iBAAe,OAAO,KAAK;IAE9B;;AAGH,SAAS,UAAU,MAAwB;AACzC,KAAI,KAAK,UAAW;AAGpB,KAAI,OAAO,KAAK,YAAY,YAAY;AACtC,OAAK,SAAS;AACd,OAAK,UAAU,KAAA;;AAIjB,oBAAmB,KAAK;AAExB,MAAK,YAAY;CAEjB,MAAM,cAAc;AACpB,iBAAgB;AAChB,KAAI;AACF,OAAK,UAAU,KAAK,IAAI;WAChB;AACR,kBAAgB;;;;;;;;;;;;;;AAmBpB,SAAgB,QAAW,IAAgB;CACzC,MAAM,cAAc;AACpB,iBAAgB;AAChB,KAAI;AACF,SAAO,IAAI;WACH;AACR,kBAAgB;;;AAQpB,SAAgB,SAAS,OAAkD;AACzE,QAAO,OAAO,UAAU,cAAe,MAAkC,WAAW"}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vorra/core",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Vorra core — reactivity primitives and dependency injection",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/ubergeoff/vorra.git",
|
|
9
|
+
"directory": "packages/core"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/ubergeoff/vorra/tree/main/packages/core#readme",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"vorra",
|
|
14
|
+
"signals",
|
|
15
|
+
"reactivity",
|
|
16
|
+
"dependency-injection",
|
|
17
|
+
"framework"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.cjs",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": "./dist/index.js",
|
|
34
|
+
"require": "./dist/index.cjs",
|
|
35
|
+
"types": "./dist/index.d.ts"
|
|
36
|
+
},
|
|
37
|
+
"./reactivity": {
|
|
38
|
+
"import": "./dist/reactivity.js",
|
|
39
|
+
"require": "./dist/reactivity.cjs",
|
|
40
|
+
"types": "./dist/reactivity.d.ts"
|
|
41
|
+
},
|
|
42
|
+
"./di": {
|
|
43
|
+
"import": "./dist/di.js",
|
|
44
|
+
"require": "./dist/di.cjs",
|
|
45
|
+
"types": "./dist/di.d.ts"
|
|
46
|
+
},
|
|
47
|
+
"./dom": {
|
|
48
|
+
"import": "./dist/dom.js",
|
|
49
|
+
"require": "./dist/dom.cjs",
|
|
50
|
+
"types": "./dist/dom.d.ts"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "rolldown -c rolldown.config.mjs",
|
|
55
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"rolldown": "*",
|
|
59
|
+
"typescript": "*",
|
|
60
|
+
"vitest": "*"
|
|
61
|
+
},
|
|
62
|
+
"sideEffects": false
|
|
63
|
+
}
|