tannijs 0.1.0 → 0.1.2
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/dist/chunk-B42XHE7V.js +318 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +28 -0
- package/dist/internals.d.ts +1 -0
- package/dist/internals.js +14 -0
- package/package.json +16 -9
- package/src/dom.test.ts +0 -101
- package/src/dom.ts +0 -195
- package/src/index.ts +0 -10
- package/src/internals.ts +0 -3
- package/src/reactivity.test.ts +0 -135
- package/src/reactivity.ts +0 -199
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// src/reactivity.ts
|
|
2
|
+
var Signal = class {
|
|
3
|
+
constructor(value) {
|
|
4
|
+
this.value = value;
|
|
5
|
+
}
|
|
6
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
7
|
+
read() {
|
|
8
|
+
trackDependency(this);
|
|
9
|
+
return this.value;
|
|
10
|
+
}
|
|
11
|
+
peek() {
|
|
12
|
+
return this.value;
|
|
13
|
+
}
|
|
14
|
+
write(next) {
|
|
15
|
+
if (Object.is(this.value, next)) {
|
|
16
|
+
return this.value;
|
|
17
|
+
}
|
|
18
|
+
this.value = next;
|
|
19
|
+
notifySubscribers(this.subscribers);
|
|
20
|
+
return this.value;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var Memo = class {
|
|
24
|
+
constructor(fn) {
|
|
25
|
+
this.fn = fn;
|
|
26
|
+
this.execute();
|
|
27
|
+
}
|
|
28
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
29
|
+
deps = /* @__PURE__ */ new Set();
|
|
30
|
+
cleanups = [];
|
|
31
|
+
value;
|
|
32
|
+
read() {
|
|
33
|
+
trackDependency(this);
|
|
34
|
+
return this.value;
|
|
35
|
+
}
|
|
36
|
+
execute() {
|
|
37
|
+
cleanupComputation(this);
|
|
38
|
+
const previous = currentComputation;
|
|
39
|
+
currentComputation = this;
|
|
40
|
+
let nextValue;
|
|
41
|
+
try {
|
|
42
|
+
nextValue = this.fn();
|
|
43
|
+
} finally {
|
|
44
|
+
currentComputation = previous;
|
|
45
|
+
}
|
|
46
|
+
if (!Object.is(nextValue, this.value)) {
|
|
47
|
+
this.value = nextValue;
|
|
48
|
+
notifySubscribers(this.subscribers);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var pendingComputations = /* @__PURE__ */ new Set();
|
|
53
|
+
var batchDepth = 0;
|
|
54
|
+
var currentComputation = null;
|
|
55
|
+
function trackDependency(source) {
|
|
56
|
+
if (!currentComputation) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
source.subscribers.add(currentComputation);
|
|
60
|
+
currentComputation.deps.add(source);
|
|
61
|
+
}
|
|
62
|
+
function cleanupComputation(computation) {
|
|
63
|
+
for (const source of computation.deps) {
|
|
64
|
+
source.subscribers.delete(computation);
|
|
65
|
+
}
|
|
66
|
+
computation.deps.clear();
|
|
67
|
+
const cleanups = computation.cleanups;
|
|
68
|
+
computation.cleanups = [];
|
|
69
|
+
for (const cleanup of cleanups) {
|
|
70
|
+
cleanup();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function runComputation(computation) {
|
|
74
|
+
if (batchDepth > 0) {
|
|
75
|
+
pendingComputations.add(computation);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
computation.execute();
|
|
79
|
+
}
|
|
80
|
+
function notifySubscribers(subscribers) {
|
|
81
|
+
const queue = Array.from(subscribers);
|
|
82
|
+
for (const subscriber of queue) {
|
|
83
|
+
runComputation(subscriber);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function flushPending() {
|
|
87
|
+
if (pendingComputations.size === 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const queue = Array.from(pendingComputations);
|
|
91
|
+
pendingComputations.clear();
|
|
92
|
+
for (const computation of queue) {
|
|
93
|
+
computation.execute();
|
|
94
|
+
}
|
|
95
|
+
if (pendingComputations.size > 0) {
|
|
96
|
+
flushPending();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function createSignal(initialValue) {
|
|
100
|
+
const signal = new Signal(initialValue);
|
|
101
|
+
const accessor = () => signal.read();
|
|
102
|
+
const setter = (value) => {
|
|
103
|
+
const nextValue = typeof value === "function" ? value(signal.peek()) : value;
|
|
104
|
+
return signal.write(nextValue);
|
|
105
|
+
};
|
|
106
|
+
return [accessor, setter];
|
|
107
|
+
}
|
|
108
|
+
function createEffect(fn) {
|
|
109
|
+
const effect2 = {
|
|
110
|
+
deps: /* @__PURE__ */ new Set(),
|
|
111
|
+
cleanups: [],
|
|
112
|
+
execute() {
|
|
113
|
+
cleanupComputation(effect2);
|
|
114
|
+
const previous = currentComputation;
|
|
115
|
+
currentComputation = effect2;
|
|
116
|
+
try {
|
|
117
|
+
fn();
|
|
118
|
+
} finally {
|
|
119
|
+
currentComputation = previous;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
effect2.execute();
|
|
124
|
+
}
|
|
125
|
+
function createMemo(fn) {
|
|
126
|
+
const memo = new Memo(fn);
|
|
127
|
+
return () => memo.read();
|
|
128
|
+
}
|
|
129
|
+
function batch(fn) {
|
|
130
|
+
batchDepth += 1;
|
|
131
|
+
try {
|
|
132
|
+
return fn();
|
|
133
|
+
} finally {
|
|
134
|
+
batchDepth -= 1;
|
|
135
|
+
if (batchDepth === 0) {
|
|
136
|
+
flushPending();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function untrack(fn) {
|
|
141
|
+
const previous = currentComputation;
|
|
142
|
+
currentComputation = null;
|
|
143
|
+
try {
|
|
144
|
+
return fn();
|
|
145
|
+
} finally {
|
|
146
|
+
currentComputation = previous;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function onCleanup(fn) {
|
|
150
|
+
if (!currentComputation) {
|
|
151
|
+
throw new Error("onCleanup must be called inside a tracked computation.");
|
|
152
|
+
}
|
|
153
|
+
currentComputation.cleanups.push(fn);
|
|
154
|
+
}
|
|
155
|
+
function onMount(fn) {
|
|
156
|
+
queueMicrotask(fn);
|
|
157
|
+
}
|
|
158
|
+
var effect = createEffect;
|
|
159
|
+
|
|
160
|
+
// src/dom.ts
|
|
161
|
+
var delegatedEvents = /* @__PURE__ */ new Set();
|
|
162
|
+
var listeningEvents = /* @__PURE__ */ new Set();
|
|
163
|
+
var directListeners = /* @__PURE__ */ new WeakMap();
|
|
164
|
+
function template(html) {
|
|
165
|
+
const tpl = document.createElement("template");
|
|
166
|
+
tpl.innerHTML = html.trim();
|
|
167
|
+
if (tpl.content.childNodes.length === 1) {
|
|
168
|
+
return tpl.content.firstChild;
|
|
169
|
+
}
|
|
170
|
+
return tpl.content;
|
|
171
|
+
}
|
|
172
|
+
function insert(parent, value, marker = null) {
|
|
173
|
+
if (typeof value === "function") {
|
|
174
|
+
let currentNodes = [];
|
|
175
|
+
createEffect(() => {
|
|
176
|
+
const resolved = value();
|
|
177
|
+
const nextNodes2 = normalizeNodes(resolved);
|
|
178
|
+
currentNodes = replaceNodes(parent, currentNodes, nextNodes2, marker);
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const nextNodes = normalizeNodes(value);
|
|
183
|
+
replaceNodes(parent, [], nextNodes, marker);
|
|
184
|
+
}
|
|
185
|
+
function spread(element, props) {
|
|
186
|
+
for (const [key, value] of Object.entries(props)) {
|
|
187
|
+
if (key === "children") {
|
|
188
|
+
insert(element, value);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (key === "style" && value && typeof value === "object") {
|
|
192
|
+
Object.assign(element.style, value);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const eventName = toEventName(key);
|
|
196
|
+
if (eventName) {
|
|
197
|
+
applyEventHandler(element, eventName, value);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
applyProperty(element, key, value);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function delegateEvents(eventNames) {
|
|
204
|
+
for (const eventName of eventNames) {
|
|
205
|
+
const normalized = eventName.toLowerCase();
|
|
206
|
+
delegatedEvents.add(normalized);
|
|
207
|
+
if (listeningEvents.has(normalized)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
listeningEvents.add(normalized);
|
|
211
|
+
document.addEventListener(normalized, handleDelegatedEvent);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function handleDelegatedEvent(event) {
|
|
215
|
+
const type = event.type.toLowerCase();
|
|
216
|
+
let node = event.target;
|
|
217
|
+
while (node && node !== document) {
|
|
218
|
+
if (node instanceof Element) {
|
|
219
|
+
const handlers = node.__tnDelegatedHandlers;
|
|
220
|
+
const handler = handlers?.[type];
|
|
221
|
+
if (handler) {
|
|
222
|
+
handler(event);
|
|
223
|
+
if (event.cancelBubble) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
node = node.parentNode;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function applyEventHandler(element, eventName, value) {
|
|
232
|
+
if (typeof value !== "function") {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const handler = value;
|
|
236
|
+
if (delegatedEvents.has(eventName)) {
|
|
237
|
+
const delegatedElement = element;
|
|
238
|
+
delegatedElement.__tnDelegatedHandlers ??= {};
|
|
239
|
+
delegatedElement.__tnDelegatedHandlers[eventName] = handler;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
let listenersForElement = directListeners.get(element);
|
|
243
|
+
if (!listenersForElement) {
|
|
244
|
+
listenersForElement = /* @__PURE__ */ new Map();
|
|
245
|
+
directListeners.set(element, listenersForElement);
|
|
246
|
+
}
|
|
247
|
+
const previous = listenersForElement.get(eventName);
|
|
248
|
+
if (previous) {
|
|
249
|
+
element.removeEventListener(eventName, previous);
|
|
250
|
+
}
|
|
251
|
+
listenersForElement.set(eventName, handler);
|
|
252
|
+
element.addEventListener(eventName, handler);
|
|
253
|
+
}
|
|
254
|
+
function applyProperty(element, key, value) {
|
|
255
|
+
const writableElement = element;
|
|
256
|
+
if (value === false || value == null) {
|
|
257
|
+
element.removeAttribute(key);
|
|
258
|
+
if (key in element) {
|
|
259
|
+
writableElement[key] = "";
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (key in element && !key.startsWith("aria-") && !key.startsWith("data-")) {
|
|
264
|
+
writableElement[key] = value;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
element.setAttribute(key, String(value));
|
|
268
|
+
}
|
|
269
|
+
function toEventName(key) {
|
|
270
|
+
if (key.startsWith("on:")) {
|
|
271
|
+
return key.slice(3).toLowerCase();
|
|
272
|
+
}
|
|
273
|
+
if (key.startsWith("@")) {
|
|
274
|
+
return key.slice(1).toLowerCase();
|
|
275
|
+
}
|
|
276
|
+
if (key.startsWith("on") && key.length > 2) {
|
|
277
|
+
return key.slice(2).toLowerCase();
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
function normalizeNodes(value) {
|
|
282
|
+
if (value == null || value === false || value === true) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
if (Array.isArray(value)) {
|
|
286
|
+
return value.flatMap((entry) => normalizeNodes(entry));
|
|
287
|
+
}
|
|
288
|
+
if (value instanceof Node) {
|
|
289
|
+
return [value];
|
|
290
|
+
}
|
|
291
|
+
return [document.createTextNode(String(value))];
|
|
292
|
+
}
|
|
293
|
+
function replaceNodes(parent, currentNodes, nextNodes, marker) {
|
|
294
|
+
for (const node of currentNodes) {
|
|
295
|
+
if (node.parentNode === parent) {
|
|
296
|
+
parent.removeChild(node);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const node of nextNodes) {
|
|
300
|
+
parent.insertBefore(node, marker);
|
|
301
|
+
}
|
|
302
|
+
return nextNodes;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export {
|
|
306
|
+
createSignal,
|
|
307
|
+
createEffect,
|
|
308
|
+
createMemo,
|
|
309
|
+
batch,
|
|
310
|
+
untrack,
|
|
311
|
+
onCleanup,
|
|
312
|
+
onMount,
|
|
313
|
+
effect,
|
|
314
|
+
template,
|
|
315
|
+
insert,
|
|
316
|
+
spread,
|
|
317
|
+
delegateEvents
|
|
318
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type Accessor<T> = () => T;
|
|
2
|
+
type Setter<T> = (value: T | ((prev: T) => T)) => T;
|
|
3
|
+
type CleanupFn = () => void;
|
|
4
|
+
declare function createSignal<T>(initialValue: T): [Accessor<T>, Setter<T>];
|
|
5
|
+
declare function createEffect(fn: () => void): void;
|
|
6
|
+
declare function createMemo<T>(fn: () => T): Accessor<T>;
|
|
7
|
+
declare function batch<T>(fn: () => T): T;
|
|
8
|
+
declare function untrack<T>(fn: () => T): T;
|
|
9
|
+
declare function onCleanup(fn: CleanupFn): void;
|
|
10
|
+
declare function onMount(fn: () => void): void;
|
|
11
|
+
declare const effect: typeof createEffect;
|
|
12
|
+
|
|
13
|
+
type InsertValue = Node | string | number | boolean | null | undefined | InsertValue[] | (() => InsertValue);
|
|
14
|
+
declare function template(html: string): Node;
|
|
15
|
+
declare function insert(parent: Node, value: InsertValue, marker?: Node | null): void;
|
|
16
|
+
declare function spread(element: Element, props: Record<string, unknown>): void;
|
|
17
|
+
declare function delegateEvents(eventNames: string[]): void;
|
|
18
|
+
|
|
19
|
+
export { type Accessor, type InsertValue, type Setter, batch, createEffect, createMemo, createSignal, delegateEvents, effect, insert, onCleanup, onMount, spread, template, untrack };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
batch,
|
|
3
|
+
createEffect,
|
|
4
|
+
createMemo,
|
|
5
|
+
createSignal,
|
|
6
|
+
delegateEvents,
|
|
7
|
+
effect,
|
|
8
|
+
insert,
|
|
9
|
+
onCleanup,
|
|
10
|
+
onMount,
|
|
11
|
+
spread,
|
|
12
|
+
template,
|
|
13
|
+
untrack
|
|
14
|
+
} from "./chunk-B42XHE7V.js";
|
|
15
|
+
export {
|
|
16
|
+
batch,
|
|
17
|
+
createEffect,
|
|
18
|
+
createMemo,
|
|
19
|
+
createSignal,
|
|
20
|
+
delegateEvents,
|
|
21
|
+
effect,
|
|
22
|
+
insert,
|
|
23
|
+
onCleanup,
|
|
24
|
+
onMount,
|
|
25
|
+
spread,
|
|
26
|
+
template,
|
|
27
|
+
untrack
|
|
28
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { InsertValue, createEffect, delegateEvents, insert, spread, template } from './index.js';
|
package/package.json
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tannijs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Signal-based reactive runtime for the Tanni framework",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Sebastijan Zindl",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./
|
|
11
|
-
"default": "./
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"./internals": {
|
|
14
|
-
"types": "./
|
|
15
|
-
"default": "./
|
|
14
|
+
"types": "./dist/internals.d.ts",
|
|
15
|
+
"default": "./dist/internals.js"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
|
-
"main": "./
|
|
19
|
-
"types": "./
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
20
|
"files": [
|
|
21
|
-
"src",
|
|
22
21
|
"dist"
|
|
23
|
-
]
|
|
22
|
+
],
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.5.1",
|
|
25
|
+
"typescript": "^5.9.3"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup src/index.ts src/internals.ts --format esm --dts --clean --out-dir dist",
|
|
29
|
+
"dev": "tsup src/index.ts src/internals.ts --format esm --dts --out-dir dist --watch"
|
|
30
|
+
}
|
|
24
31
|
}
|
package/src/dom.test.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { createSignal } from './reactivity';
|
|
4
|
-
import { delegateEvents, insert, spread, template } from './dom';
|
|
5
|
-
|
|
6
|
-
describe('dom helpers', () => {
|
|
7
|
-
it('creates cloneable template nodes', () => {
|
|
8
|
-
const node = template('<button class="btn">Click</button>');
|
|
9
|
-
const clone = node.cloneNode(true) as HTMLElement;
|
|
10
|
-
|
|
11
|
-
expect(clone.tagName).toBe('BUTTON');
|
|
12
|
-
expect(clone.className).toBe('btn');
|
|
13
|
-
expect(clone.textContent).toBe('Click');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('inserts reactive text updates', () => {
|
|
17
|
-
const host = document.createElement('div');
|
|
18
|
-
const [count, setCount] = createSignal(0);
|
|
19
|
-
|
|
20
|
-
insert(host, () => `Count: ${count()}`);
|
|
21
|
-
expect(host.textContent).toBe('Count: 0');
|
|
22
|
-
|
|
23
|
-
setCount(2);
|
|
24
|
-
expect(host.textContent).toBe('Count: 2');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('replaces only content before marker in reactive inserts', () => {
|
|
28
|
-
const host = document.createElement('div');
|
|
29
|
-
const marker = document.createComment('marker');
|
|
30
|
-
host.append(document.createTextNode('prefix-'));
|
|
31
|
-
host.append(marker);
|
|
32
|
-
host.append(document.createTextNode('-suffix'));
|
|
33
|
-
|
|
34
|
-
const [value, setValue] = createSignal('one');
|
|
35
|
-
insert(host, () => value(), marker);
|
|
36
|
-
|
|
37
|
-
expect(host.textContent).toBe('prefix-one-suffix');
|
|
38
|
-
setValue('two');
|
|
39
|
-
expect(host.textContent).toBe('prefix-two-suffix');
|
|
40
|
-
expect(host.lastChild).not.toBe(marker);
|
|
41
|
-
expect(host.childNodes[1]?.nodeType).toBe(Node.TEXT_NODE);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('applies properties and delegated events via spread', () => {
|
|
45
|
-
delegateEvents(['click']);
|
|
46
|
-
|
|
47
|
-
const host = document.createElement('div');
|
|
48
|
-
const button = document.createElement('button');
|
|
49
|
-
const onClick = vi.fn();
|
|
50
|
-
|
|
51
|
-
spread(button, {
|
|
52
|
-
id: 'counter-btn',
|
|
53
|
-
'@click': onClick,
|
|
54
|
-
children: 'Tap',
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
host.append(button);
|
|
58
|
-
document.body.append(host);
|
|
59
|
-
button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
60
|
-
|
|
61
|
-
expect(button.id).toBe('counter-btn');
|
|
62
|
-
expect(button.textContent).toBe('Tap');
|
|
63
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('supports direct event listeners and replaces previous handlers', () => {
|
|
67
|
-
const button = document.createElement('button');
|
|
68
|
-
const first = vi.fn();
|
|
69
|
-
const second = vi.fn();
|
|
70
|
-
|
|
71
|
-
spread(button, { onMouseover: first });
|
|
72
|
-
button.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
|
73
|
-
|
|
74
|
-
spread(button, { onMouseover: second });
|
|
75
|
-
button.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
|
76
|
-
|
|
77
|
-
expect(first).toHaveBeenCalledTimes(1);
|
|
78
|
-
expect(second).toHaveBeenCalledTimes(1);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('supports delegated bubbling and stopPropagation semantics', () => {
|
|
82
|
-
delegateEvents(['click']);
|
|
83
|
-
const host = document.createElement('div');
|
|
84
|
-
const parent = document.createElement('div');
|
|
85
|
-
const button = document.createElement('button');
|
|
86
|
-
const parentHandler = vi.fn();
|
|
87
|
-
const childHandler = vi.fn((event: Event) => event.stopPropagation());
|
|
88
|
-
|
|
89
|
-
spread(parent, { '@click': parentHandler });
|
|
90
|
-
spread(button, { '@click': childHandler });
|
|
91
|
-
|
|
92
|
-
parent.append(button);
|
|
93
|
-
host.append(parent);
|
|
94
|
-
document.body.append(host);
|
|
95
|
-
|
|
96
|
-
button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
97
|
-
|
|
98
|
-
expect(childHandler).toHaveBeenCalledTimes(1);
|
|
99
|
-
expect(parentHandler).toHaveBeenCalledTimes(0);
|
|
100
|
-
});
|
|
101
|
-
});
|
package/src/dom.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { createEffect } from './reactivity';
|
|
2
|
-
|
|
3
|
-
export type InsertValue =
|
|
4
|
-
| Node
|
|
5
|
-
| string
|
|
6
|
-
| number
|
|
7
|
-
| boolean
|
|
8
|
-
| null
|
|
9
|
-
| undefined
|
|
10
|
-
| InsertValue[]
|
|
11
|
-
| (() => InsertValue);
|
|
12
|
-
|
|
13
|
-
type EventHandler = (event: Event) => void;
|
|
14
|
-
|
|
15
|
-
const delegatedEvents = new Set<string>();
|
|
16
|
-
const listeningEvents = new Set<string>();
|
|
17
|
-
const directListeners = new WeakMap<Element, Map<string, EventHandler>>();
|
|
18
|
-
|
|
19
|
-
export function template(html: string): Node {
|
|
20
|
-
const tpl = document.createElement('template');
|
|
21
|
-
tpl.innerHTML = html.trim();
|
|
22
|
-
|
|
23
|
-
if (tpl.content.childNodes.length === 1) {
|
|
24
|
-
return tpl.content.firstChild as Node;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return tpl.content;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function insert(parent: Node, value: InsertValue, marker: Node | null = null): void {
|
|
31
|
-
if (typeof value === 'function') {
|
|
32
|
-
let currentNodes: Node[] = [];
|
|
33
|
-
createEffect(() => {
|
|
34
|
-
const resolved = (value as () => InsertValue)();
|
|
35
|
-
const nextNodes = normalizeNodes(resolved);
|
|
36
|
-
currentNodes = replaceNodes(parent, currentNodes, nextNodes, marker);
|
|
37
|
-
});
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const nextNodes = normalizeNodes(value);
|
|
42
|
-
replaceNodes(parent, [], nextNodes, marker);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function spread(element: Element, props: Record<string, unknown>): void {
|
|
46
|
-
for (const [key, value] of Object.entries(props)) {
|
|
47
|
-
if (key === 'children') {
|
|
48
|
-
insert(element, value as InsertValue);
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (key === 'style' && value && typeof value === 'object') {
|
|
53
|
-
Object.assign((element as HTMLElement).style, value as Record<string, string>);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const eventName = toEventName(key);
|
|
58
|
-
if (eventName) {
|
|
59
|
-
applyEventHandler(element, eventName, value);
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
applyProperty(element, key, value);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function delegateEvents(eventNames: string[]): void {
|
|
68
|
-
for (const eventName of eventNames) {
|
|
69
|
-
const normalized = eventName.toLowerCase();
|
|
70
|
-
delegatedEvents.add(normalized);
|
|
71
|
-
|
|
72
|
-
if (listeningEvents.has(normalized)) {
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
listeningEvents.add(normalized);
|
|
77
|
-
document.addEventListener(normalized, handleDelegatedEvent);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function handleDelegatedEvent(event: Event): void {
|
|
82
|
-
const type = event.type.toLowerCase();
|
|
83
|
-
let node: Node | null = event.target as Node | null;
|
|
84
|
-
|
|
85
|
-
while (node && node !== document) {
|
|
86
|
-
if (node instanceof Element) {
|
|
87
|
-
const handlers = (node as DelegatedElement).__tnDelegatedHandlers;
|
|
88
|
-
const handler = handlers?.[type];
|
|
89
|
-
if (handler) {
|
|
90
|
-
handler(event);
|
|
91
|
-
if (event.cancelBubble) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
node = node.parentNode;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function applyEventHandler(element: Element, eventName: string, value: unknown): void {
|
|
101
|
-
if (typeof value !== 'function') {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const handler = value as EventHandler;
|
|
106
|
-
if (delegatedEvents.has(eventName)) {
|
|
107
|
-
const delegatedElement = element as DelegatedElement;
|
|
108
|
-
delegatedElement.__tnDelegatedHandlers ??= {};
|
|
109
|
-
delegatedElement.__tnDelegatedHandlers[eventName] = handler;
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let listenersForElement = directListeners.get(element);
|
|
114
|
-
if (!listenersForElement) {
|
|
115
|
-
listenersForElement = new Map<string, EventHandler>();
|
|
116
|
-
directListeners.set(element, listenersForElement);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const previous = listenersForElement.get(eventName);
|
|
120
|
-
if (previous) {
|
|
121
|
-
element.removeEventListener(eventName, previous);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
listenersForElement.set(eventName, handler);
|
|
125
|
-
element.addEventListener(eventName, handler);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function applyProperty(element: Element, key: string, value: unknown): void {
|
|
129
|
-
const writableElement = element as unknown as Record<string, unknown>;
|
|
130
|
-
|
|
131
|
-
if (value === false || value == null) {
|
|
132
|
-
element.removeAttribute(key);
|
|
133
|
-
if (key in element) {
|
|
134
|
-
writableElement[key] = '';
|
|
135
|
-
}
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (key in element && !key.startsWith('aria-') && !key.startsWith('data-')) {
|
|
140
|
-
writableElement[key] = value;
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
element.setAttribute(key, String(value));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function toEventName(key: string): string | null {
|
|
148
|
-
if (key.startsWith('on:')) {
|
|
149
|
-
return key.slice(3).toLowerCase();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (key.startsWith('@')) {
|
|
153
|
-
return key.slice(1).toLowerCase();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (key.startsWith('on') && key.length > 2) {
|
|
157
|
-
return key.slice(2).toLowerCase();
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function normalizeNodes(value: InsertValue): Node[] {
|
|
164
|
-
if (value == null || value === false || value === true) {
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (Array.isArray(value)) {
|
|
169
|
-
return value.flatMap((entry) => normalizeNodes(entry));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (value instanceof Node) {
|
|
173
|
-
return [value];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return [document.createTextNode(String(value))];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function replaceNodes(parent: Node, currentNodes: Node[], nextNodes: Node[], marker: Node | null): Node[] {
|
|
180
|
-
for (const node of currentNodes) {
|
|
181
|
-
if (node.parentNode === parent) {
|
|
182
|
-
parent.removeChild(node);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
for (const node of nextNodes) {
|
|
187
|
-
parent.insertBefore(node, marker);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return nextNodes;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
interface DelegatedElement extends Element {
|
|
194
|
-
__tnDelegatedHandlers?: Record<string, EventHandler>;
|
|
195
|
-
}
|
package/src/index.ts
DELETED
package/src/internals.ts
DELETED
package/src/reactivity.test.ts
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { batch, createEffect, createMemo, createSignal, onCleanup, untrack } from './reactivity';
|
|
4
|
-
|
|
5
|
-
describe('reactivity core', () => {
|
|
6
|
-
it('tracks signal reads and reruns effects on updates', () => {
|
|
7
|
-
const [count, setCount] = createSignal(0);
|
|
8
|
-
const values: number[] = [];
|
|
9
|
-
|
|
10
|
-
createEffect(() => {
|
|
11
|
-
values.push(count());
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
setCount(1);
|
|
15
|
-
setCount((prev) => prev + 1);
|
|
16
|
-
|
|
17
|
-
expect(values).toEqual([0, 1, 2]);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('batches updates and runs effects once', () => {
|
|
21
|
-
const [count, setCount] = createSignal(0);
|
|
22
|
-
const spy = vi.fn();
|
|
23
|
-
|
|
24
|
-
createEffect(() => {
|
|
25
|
-
count();
|
|
26
|
-
spy();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
batch(() => {
|
|
30
|
-
setCount(1);
|
|
31
|
-
setCount(2);
|
|
32
|
-
setCount(3);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
expect(spy).toHaveBeenCalledTimes(2);
|
|
36
|
-
expect(count()).toBe(3);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('supports memoized derived values', () => {
|
|
40
|
-
const [count, setCount] = createSignal(2);
|
|
41
|
-
const doubled = createMemo(() => count() * 2);
|
|
42
|
-
|
|
43
|
-
expect(doubled()).toBe(4);
|
|
44
|
-
setCount(5);
|
|
45
|
-
expect(doubled()).toBe(10);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('recomputes memo only when dependencies change', () => {
|
|
49
|
-
const [count, setCount] = createSignal(1);
|
|
50
|
-
const compute = vi.fn(() => count() * 10);
|
|
51
|
-
const value = createMemo(compute);
|
|
52
|
-
const effectSpy = vi.fn();
|
|
53
|
-
|
|
54
|
-
createEffect(() => {
|
|
55
|
-
value();
|
|
56
|
-
effectSpy();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect(value()).toBe(10);
|
|
60
|
-
expect(compute).toHaveBeenCalledTimes(1);
|
|
61
|
-
expect(effectSpy).toHaveBeenCalledTimes(1);
|
|
62
|
-
|
|
63
|
-
setCount(1);
|
|
64
|
-
expect(compute).toHaveBeenCalledTimes(1);
|
|
65
|
-
expect(effectSpy).toHaveBeenCalledTimes(1);
|
|
66
|
-
|
|
67
|
-
setCount(2);
|
|
68
|
-
expect(value()).toBe(20);
|
|
69
|
-
expect(compute).toHaveBeenCalledTimes(2);
|
|
70
|
-
expect(effectSpy).toHaveBeenCalledTimes(2);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('supports untrack and effect cleanups', () => {
|
|
74
|
-
const [count, setCount] = createSignal(0);
|
|
75
|
-
const sideEffect = vi.fn();
|
|
76
|
-
const cleanup = vi.fn();
|
|
77
|
-
|
|
78
|
-
createEffect(() => {
|
|
79
|
-
sideEffect(untrack(() => count()));
|
|
80
|
-
onCleanup(cleanup);
|
|
81
|
-
count();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
setCount(1);
|
|
85
|
-
|
|
86
|
-
expect(sideEffect).toHaveBeenCalledTimes(2);
|
|
87
|
-
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('runs previous cleanup before the next effect pass', () => {
|
|
91
|
-
const [value, setValue] = createSignal('a');
|
|
92
|
-
const callOrder: string[] = [];
|
|
93
|
-
|
|
94
|
-
createEffect(() => {
|
|
95
|
-
const current = value();
|
|
96
|
-
callOrder.push(`effect:${current}`);
|
|
97
|
-
onCleanup(() => {
|
|
98
|
-
callOrder.push(`cleanup:${current}`);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
setValue('b');
|
|
103
|
-
setValue('c');
|
|
104
|
-
|
|
105
|
-
expect(callOrder).toEqual([
|
|
106
|
-
'effect:a',
|
|
107
|
-
'cleanup:a',
|
|
108
|
-
'effect:b',
|
|
109
|
-
'cleanup:b',
|
|
110
|
-
'effect:c',
|
|
111
|
-
]);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('supports nested batching with a single downstream rerun', () => {
|
|
115
|
-
const [count, setCount] = createSignal(0);
|
|
116
|
-
const spy = vi.fn();
|
|
117
|
-
|
|
118
|
-
createEffect(() => {
|
|
119
|
-
count();
|
|
120
|
-
spy();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
batch(() => {
|
|
124
|
-
setCount(1);
|
|
125
|
-
batch(() => {
|
|
126
|
-
setCount(2);
|
|
127
|
-
setCount(3);
|
|
128
|
-
});
|
|
129
|
-
setCount(4);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
expect(count()).toBe(4);
|
|
133
|
-
expect(spy).toHaveBeenCalledTimes(2);
|
|
134
|
-
});
|
|
135
|
-
});
|
package/src/reactivity.ts
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
export type Accessor<T> = () => T;
|
|
2
|
-
export type Setter<T> = (value: T | ((prev: T) => T)) => T;
|
|
3
|
-
|
|
4
|
-
type CleanupFn = () => void;
|
|
5
|
-
|
|
6
|
-
interface Computation {
|
|
7
|
-
execute: () => void;
|
|
8
|
-
deps: Set<Source>;
|
|
9
|
-
cleanups: CleanupFn[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface Source {
|
|
13
|
-
subscribers: Set<Computation>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
class Signal<T> implements Source {
|
|
17
|
-
public readonly subscribers = new Set<Computation>();
|
|
18
|
-
|
|
19
|
-
public constructor(private value: T) {}
|
|
20
|
-
|
|
21
|
-
public read(): T {
|
|
22
|
-
trackDependency(this);
|
|
23
|
-
return this.value;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
public peek(): T {
|
|
27
|
-
return this.value;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
public write(next: T): T {
|
|
31
|
-
if (Object.is(this.value, next)) {
|
|
32
|
-
return this.value;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
this.value = next;
|
|
36
|
-
notifySubscribers(this.subscribers);
|
|
37
|
-
return this.value;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
class Memo<T> implements Source, Computation {
|
|
42
|
-
public readonly subscribers = new Set<Computation>();
|
|
43
|
-
public readonly deps = new Set<Source>();
|
|
44
|
-
public cleanups: CleanupFn[] = [];
|
|
45
|
-
public value!: T;
|
|
46
|
-
|
|
47
|
-
public constructor(private readonly fn: () => T) {
|
|
48
|
-
this.execute();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
public read(): T {
|
|
52
|
-
trackDependency(this);
|
|
53
|
-
return this.value;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
public execute(): void {
|
|
57
|
-
cleanupComputation(this);
|
|
58
|
-
const previous = currentComputation;
|
|
59
|
-
currentComputation = this;
|
|
60
|
-
|
|
61
|
-
let nextValue!: T;
|
|
62
|
-
try {
|
|
63
|
-
nextValue = this.fn();
|
|
64
|
-
} finally {
|
|
65
|
-
currentComputation = previous;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (!Object.is(nextValue, this.value)) {
|
|
69
|
-
this.value = nextValue;
|
|
70
|
-
notifySubscribers(this.subscribers);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const pendingComputations = new Set<Computation>();
|
|
76
|
-
let batchDepth = 0;
|
|
77
|
-
let currentComputation: Computation | null = null;
|
|
78
|
-
|
|
79
|
-
function trackDependency(source: Source): void {
|
|
80
|
-
if (!currentComputation) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
source.subscribers.add(currentComputation);
|
|
85
|
-
currentComputation.deps.add(source);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function cleanupComputation(computation: Computation): void {
|
|
89
|
-
for (const source of computation.deps) {
|
|
90
|
-
source.subscribers.delete(computation);
|
|
91
|
-
}
|
|
92
|
-
computation.deps.clear();
|
|
93
|
-
|
|
94
|
-
const cleanups = computation.cleanups;
|
|
95
|
-
computation.cleanups = [];
|
|
96
|
-
for (const cleanup of cleanups) {
|
|
97
|
-
cleanup();
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function runComputation(computation: Computation): void {
|
|
102
|
-
if (batchDepth > 0) {
|
|
103
|
-
pendingComputations.add(computation);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
computation.execute();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function notifySubscribers(subscribers: Set<Computation>): void {
|
|
111
|
-
const queue = Array.from(subscribers);
|
|
112
|
-
for (const subscriber of queue) {
|
|
113
|
-
runComputation(subscriber);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function flushPending(): void {
|
|
118
|
-
if (pendingComputations.size === 0) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const queue = Array.from(pendingComputations);
|
|
123
|
-
pendingComputations.clear();
|
|
124
|
-
for (const computation of queue) {
|
|
125
|
-
computation.execute();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (pendingComputations.size > 0) {
|
|
129
|
-
flushPending();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function createSignal<T>(initialValue: T): [Accessor<T>, Setter<T>] {
|
|
134
|
-
const signal = new Signal(initialValue);
|
|
135
|
-
|
|
136
|
-
const accessor: Accessor<T> = () => signal.read();
|
|
137
|
-
const setter: Setter<T> = (value) => {
|
|
138
|
-
const nextValue = typeof value === 'function' ? (value as (prev: T) => T)(signal.peek()) : value;
|
|
139
|
-
return signal.write(nextValue);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
return [accessor, setter];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function createEffect(fn: () => void): void {
|
|
146
|
-
const effect: Computation = {
|
|
147
|
-
deps: new Set<Source>(),
|
|
148
|
-
cleanups: [],
|
|
149
|
-
execute() {
|
|
150
|
-
cleanupComputation(effect);
|
|
151
|
-
const previous = currentComputation;
|
|
152
|
-
currentComputation = effect;
|
|
153
|
-
try {
|
|
154
|
-
fn();
|
|
155
|
-
} finally {
|
|
156
|
-
currentComputation = previous;
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
effect.execute();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function createMemo<T>(fn: () => T): Accessor<T> {
|
|
165
|
-
const memo = new Memo(fn);
|
|
166
|
-
return () => memo.read();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function batch<T>(fn: () => T): T {
|
|
170
|
-
batchDepth += 1;
|
|
171
|
-
try {
|
|
172
|
-
return fn();
|
|
173
|
-
} finally {
|
|
174
|
-
batchDepth -= 1;
|
|
175
|
-
if (batchDepth === 0) {
|
|
176
|
-
flushPending();
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function untrack<T>(fn: () => T): T {
|
|
182
|
-
const previous = currentComputation;
|
|
183
|
-
currentComputation = null;
|
|
184
|
-
try {
|
|
185
|
-
return fn();
|
|
186
|
-
} finally {
|
|
187
|
-
currentComputation = previous;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function onCleanup(fn: CleanupFn): void {
|
|
192
|
-
if (!currentComputation) {
|
|
193
|
-
throw new Error('onCleanup must be called inside a tracked computation.');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
currentComputation.cleanups.push(fn);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export const effect = createEffect;
|