@sigil-dev/runtime 0.8.0 → 0.8.3
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/index.test.ts +1618 -1616
- package/index.ts +453 -437
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
export { Boundary } from "./boundary";
|
|
2
2
|
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
3
|
+
easeInCubic,
|
|
4
|
+
easeInOutCubic,
|
|
5
|
+
easeInOutQuad,
|
|
6
|
+
easeInOutQuart,
|
|
7
|
+
easeInOutQuint,
|
|
8
|
+
easeInOutSine,
|
|
9
|
+
easeInQuad,
|
|
10
|
+
easeInQuart,
|
|
11
|
+
easeInQuint,
|
|
12
|
+
easeInSine,
|
|
13
|
+
easeOutCubic,
|
|
14
|
+
easeOutQuad,
|
|
15
|
+
easeOutQuart,
|
|
16
|
+
easeOutQuint,
|
|
17
|
+
easeOutSine,
|
|
18
|
+
linear,
|
|
19
19
|
} from "./easing.ts";
|
|
20
20
|
export { ReactiveMap } from "./reactivity/map.ts";
|
|
21
21
|
export { ReactiveSet } from "./reactivity/set.ts";
|
|
22
22
|
|
|
23
23
|
export const SIGIL_SAFE = Symbol.for("sigil.safe");
|
|
24
24
|
export class SafeHtml {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
[SIGIL_SAFE] = true;
|
|
26
|
+
constructor(public v: string) {}
|
|
27
|
+
toString() {
|
|
28
|
+
return this.v;
|
|
29
|
+
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
let currentEffect: (() => void) | null = null;
|
|
@@ -38,348 +38,361 @@ const pendingEffects = new Set<() => void>();
|
|
|
38
38
|
const pendingPreEffects = new Set<() => void>();
|
|
39
39
|
|
|
40
40
|
function flush() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
scheduledFlush = false;
|
|
42
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
|
|
43
|
+
pendingPreEffects.forEach((e) => e());
|
|
44
|
+
pendingPreEffects.clear();
|
|
45
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: effects returning cleanup is intended
|
|
46
|
+
pendingEffects.forEach((e) => e());
|
|
47
|
+
pendingEffects.clear();
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function scheduleEffect(fn: () => void) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
const target = (fn as any).__sigil_pre ? pendingPreEffects : pendingEffects;
|
|
52
|
+
target.add(fn);
|
|
53
|
+
if (batchDepth === 0 && !scheduledFlush) {
|
|
54
|
+
scheduledFlush = true;
|
|
55
|
+
queueMicrotask(flush);
|
|
56
|
+
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const targetDepsMap = new WeakMap<
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
object,
|
|
61
|
+
Map<string | symbol, Set<() => void>>
|
|
62
62
|
>();
|
|
63
63
|
|
|
64
64
|
function getDeps(target: object, key: string | symbol): Set<() => void> {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
let deps = targetDepsMap.get(target);
|
|
66
|
+
if (!deps) {
|
|
67
|
+
deps = new Map();
|
|
68
|
+
targetDepsMap.set(target, deps);
|
|
69
|
+
}
|
|
70
|
+
let set = deps.get(key);
|
|
71
|
+
if (!set) {
|
|
72
|
+
set = new Set();
|
|
73
|
+
deps.set(key, set);
|
|
74
|
+
}
|
|
75
|
+
return set;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
function track(target: object, key: string | symbol) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
if (currentEffect && currentDeps) {
|
|
80
|
+
const deps = getDeps(target, key);
|
|
81
|
+
deps.add(currentEffect);
|
|
82
|
+
currentDeps.add(deps); // Set, deduplicates automatically
|
|
83
|
+
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function trigger(target: object, key: string | symbol) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
const deps = targetDepsMap.get(target);
|
|
88
|
+
if (!deps) return;
|
|
89
|
+
const set = deps.get(key);
|
|
90
|
+
if (!set) return;
|
|
91
|
+
for (const fn of [...set]) {
|
|
92
|
+
scheduleEffect(fn);
|
|
93
|
+
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
const proxyCache = new WeakMap<object, object>();
|
|
97
97
|
const proxyToTarget = new WeakMap<object, object>();
|
|
98
98
|
const reactiveTargets = new WeakSet<object>();
|
|
99
99
|
const ARRAY_MUTATORS = new Set([
|
|
100
|
-
|
|
100
|
+
"push",
|
|
101
|
+
"pop",
|
|
102
|
+
"shift",
|
|
103
|
+
"unshift",
|
|
104
|
+
"splice",
|
|
105
|
+
"sort",
|
|
106
|
+
"reverse",
|
|
107
|
+
"fill",
|
|
101
108
|
]);
|
|
102
109
|
|
|
103
110
|
function createReactiveProxy<T extends object>(target: T): T {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
111
|
+
const existing = proxyCache.get(target);
|
|
112
|
+
if (existing) return existing as T;
|
|
113
|
+
|
|
114
|
+
const proxy = new Proxy(target, {
|
|
115
|
+
get(target, key, receiver) {
|
|
116
|
+
if (
|
|
117
|
+
Array.isArray(target) &&
|
|
118
|
+
typeof key === "string" &&
|
|
119
|
+
ARRAY_MUTATORS.has(key)
|
|
120
|
+
) {
|
|
121
|
+
return (...args: any[]) => {
|
|
122
|
+
const result = (target as any)[key](...args);
|
|
123
|
+
trigger(target, "length");
|
|
124
|
+
// trigger all numeric keys that changed — for splice/sort
|
|
125
|
+
// triggering length is sufficient for most reactive patterns
|
|
126
|
+
return result;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (key === "set") {
|
|
130
|
+
return (newValue: T) => {
|
|
131
|
+
const oldKeys = new Set(Object.keys(target));
|
|
132
|
+
const newKeys = new Set(Object.keys(newValue));
|
|
133
|
+
for (const k of oldKeys) {
|
|
134
|
+
if (!newKeys.has(k)) {
|
|
135
|
+
delete (target as any)[k];
|
|
136
|
+
trigger(target, k);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const k of newKeys) {
|
|
140
|
+
(target as any)[k] = (newValue as any)[k];
|
|
141
|
+
trigger(target, k);
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(target)) {
|
|
144
|
+
trigger(target, "length");
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (key === "peek") {
|
|
150
|
+
return () => target;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (typeof key === "symbol") {
|
|
154
|
+
return Reflect.get(target, key, receiver);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
track(target, key);
|
|
158
|
+
const value = target[key as keyof T];
|
|
159
|
+
|
|
160
|
+
if (typeof value === "object" && value !== null) {
|
|
161
|
+
return createReactiveProxy(value);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return value;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
set(target, key, newValue, receiver) {
|
|
168
|
+
const oldValue = target[key as keyof T];
|
|
169
|
+
const result = Reflect.set(target, key, newValue, receiver);
|
|
170
|
+
if (oldValue !== newValue) {
|
|
171
|
+
trigger(target, key as string | symbol);
|
|
172
|
+
if (
|
|
173
|
+
Array.isArray(target) &&
|
|
174
|
+
typeof key === "string" &&
|
|
175
|
+
!Number.isNaN(Number(key))
|
|
176
|
+
) {
|
|
177
|
+
trigger(target, "length");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
deleteProperty(target, key) {
|
|
184
|
+
const result = Reflect.deleteProperty(target, key);
|
|
185
|
+
trigger(target, key as string | symbol);
|
|
186
|
+
return result;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
proxyCache.set(target, proxy);
|
|
191
|
+
proxyToTarget.set(proxy, target);
|
|
192
|
+
reactiveTargets.add(target);
|
|
193
|
+
return proxy as T;
|
|
183
194
|
}
|
|
184
195
|
|
|
185
196
|
// ─── createSignal (branches on type) ───
|
|
186
197
|
|
|
187
198
|
export type Signal<T> = {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
199
|
+
(): T;
|
|
200
|
+
set: (next: T) => void;
|
|
201
|
+
peek: () => T;
|
|
191
202
|
};
|
|
192
203
|
|
|
193
204
|
export function batch(fn: () => void) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
205
|
+
batchDepth++;
|
|
206
|
+
fn();
|
|
207
|
+
batchDepth--;
|
|
208
|
+
if (batchDepth === 0) {
|
|
209
|
+
scheduledFlush = false;
|
|
210
|
+
flush();
|
|
211
|
+
}
|
|
201
212
|
}
|
|
202
213
|
|
|
203
214
|
export function tick(): Promise<void> {
|
|
204
|
-
|
|
215
|
+
return new Promise((resolve) => queueMicrotask(resolve));
|
|
205
216
|
}
|
|
206
217
|
|
|
207
218
|
function createPrimitiveSignal<T>(initial: T): Signal<T> {
|
|
208
|
-
|
|
209
|
-
|
|
219
|
+
const subs = new Set<() => void>();
|
|
220
|
+
let value = initial;
|
|
210
221
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
222
|
+
const getter = () => {
|
|
223
|
+
if (currentEffect && currentDeps) {
|
|
224
|
+
subs.add(currentEffect);
|
|
225
|
+
currentDeps.add(subs);
|
|
226
|
+
}
|
|
227
|
+
return value;
|
|
228
|
+
};
|
|
218
229
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
230
|
+
getter.set = (next: T) => {
|
|
231
|
+
value = next;
|
|
232
|
+
for (const fn of [...subs]) {
|
|
233
|
+
scheduleEffect(fn);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
225
236
|
|
|
226
|
-
|
|
237
|
+
getter.peek = () => value;
|
|
227
238
|
|
|
228
|
-
|
|
239
|
+
return getter;
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
export function createSignal<T>(initial: T): Signal<T> {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
if (typeof initial === "object" && initial !== null) {
|
|
244
|
+
const proxy = createReactiveProxy(initial);
|
|
245
|
+
const getter = () => {
|
|
246
|
+
// array signals: auto-track length so push/pop/splice re-run effects
|
|
247
|
+
if (Array.isArray(initial)) {
|
|
248
|
+
track(initial, "length");
|
|
249
|
+
}
|
|
250
|
+
return proxy;
|
|
251
|
+
};
|
|
252
|
+
getter.set = (next: T) => (proxy as any).set(next);
|
|
253
|
+
getter.peek = () => initial;
|
|
254
|
+
return getter;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return createPrimitiveSignal(initial);
|
|
247
258
|
}
|
|
248
259
|
|
|
249
260
|
// A1: $state.raw — create signal without Proxy wrapping for objects/arrays
|
|
250
261
|
export function createRawSignal<T>(initial: T): Signal<T> {
|
|
251
|
-
|
|
262
|
+
return createPrimitiveSignal(initial);
|
|
252
263
|
}
|
|
253
264
|
|
|
254
265
|
// A2: $state.snapshot — deep un-proxy a signal value
|
|
255
266
|
function deepUnproxy<T>(value: T): T {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
if (typeof value !== "object" || value === null) return value;
|
|
268
|
+
const raw = proxyToTarget.get(value as object);
|
|
269
|
+
if (!raw) return value; // not a proxy, return as-is
|
|
270
|
+
if (Array.isArray(raw)) {
|
|
271
|
+
return raw.map(deepUnproxy) as T;
|
|
272
|
+
}
|
|
273
|
+
const result: Record<string, unknown> = {};
|
|
274
|
+
for (const key of Object.keys(raw)) {
|
|
275
|
+
result[key] = deepUnproxy((raw as Record<string, unknown>)[key]);
|
|
276
|
+
}
|
|
277
|
+
return result as T;
|
|
267
278
|
}
|
|
268
279
|
|
|
269
280
|
export function snapshot<T>(value: T): T {
|
|
270
|
-
|
|
281
|
+
return deepUnproxy(value);
|
|
271
282
|
}
|
|
272
283
|
|
|
273
284
|
// A3: $effect.tracking() — check if inside reactive context
|
|
274
285
|
export function tracking(): boolean {
|
|
275
|
-
|
|
286
|
+
return currentEffect !== null;
|
|
276
287
|
}
|
|
277
288
|
|
|
278
289
|
// A4: $inspect — dev-only effect logging
|
|
279
290
|
export function createInspectEffect(
|
|
280
|
-
|
|
281
|
-
|
|
291
|
+
getValues: () => unknown[],
|
|
292
|
+
cb?: (vals: unknown[]) => void,
|
|
282
293
|
): void {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
294
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "production")
|
|
295
|
+
return;
|
|
296
|
+
createEffect(() => {
|
|
297
|
+
const vals = getValues();
|
|
298
|
+
if (cb) cb(vals);
|
|
299
|
+
else console.log(...vals);
|
|
300
|
+
});
|
|
290
301
|
}
|
|
291
302
|
|
|
292
303
|
// A5: $effect.pre — effect that flushes before DOM update
|
|
293
|
-
export function createEffectPre(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
304
|
+
export function createEffectPre(
|
|
305
|
+
fn: () => undefined | (() => void),
|
|
306
|
+
): () => void {
|
|
307
|
+
let cleanup: (() => void) | undefined;
|
|
308
|
+
const ownDeps = new Set<Set<() => void>>();
|
|
309
|
+
|
|
310
|
+
const effect = () => {
|
|
311
|
+
if (cleanup) cleanup();
|
|
312
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
313
|
+
ownDeps.clear();
|
|
314
|
+
|
|
315
|
+
const prevEffect = currentEffect;
|
|
316
|
+
const prevDeps = currentDeps;
|
|
317
|
+
currentEffect = effect;
|
|
318
|
+
currentDeps = ownDeps;
|
|
319
|
+
try {
|
|
320
|
+
cleanup = fn();
|
|
321
|
+
} finally {
|
|
322
|
+
currentEffect = prevEffect;
|
|
323
|
+
currentDeps = prevDeps;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
(effect as any).__sigil_pre = true;
|
|
328
|
+
effect();
|
|
329
|
+
|
|
330
|
+
const dispose = () => {
|
|
331
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
332
|
+
ownDeps.clear();
|
|
333
|
+
if (cleanup) cleanup();
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
|
|
337
|
+
dispose,
|
|
338
|
+
);
|
|
339
|
+
return dispose;
|
|
327
340
|
}
|
|
328
341
|
|
|
329
342
|
// A9: Profiler effect timing (dev)
|
|
330
343
|
export function createProfilerEffect(
|
|
331
|
-
|
|
332
|
-
|
|
344
|
+
name: string,
|
|
345
|
+
fn: () => undefined | (() => void),
|
|
333
346
|
): () => void {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
+
return createEffect(() => {
|
|
348
|
+
if (
|
|
349
|
+
typeof process !== "undefined" &&
|
|
350
|
+
process.env.NODE_ENV === "production"
|
|
351
|
+
) {
|
|
352
|
+
return fn();
|
|
353
|
+
}
|
|
354
|
+
const start = performance.now();
|
|
355
|
+
const cleanup = fn();
|
|
356
|
+
const end = performance.now();
|
|
357
|
+
console.log(`[Profiler] ${name}: ${(end - start).toFixed(2)}ms`);
|
|
358
|
+
return cleanup;
|
|
359
|
+
});
|
|
347
360
|
}
|
|
348
361
|
|
|
349
362
|
// biome-ignore lint/suspicious/noConfusingVoidType: please shut up its a weird type
|
|
350
363
|
export function createEffect(fn: () => void | (() => void)): () => void {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
364
|
+
let cleanup: (() => void) | undefined;
|
|
365
|
+
const ownDeps = new Set<Set<() => void>>();
|
|
366
|
+
|
|
367
|
+
const effect = () => {
|
|
368
|
+
if (cleanup) cleanup();
|
|
369
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
370
|
+
ownDeps.clear();
|
|
371
|
+
|
|
372
|
+
const prevEffect = currentEffect;
|
|
373
|
+
const prevDeps = currentDeps;
|
|
374
|
+
currentEffect = effect;
|
|
375
|
+
currentDeps = ownDeps;
|
|
376
|
+
try {
|
|
377
|
+
cleanup = fn();
|
|
378
|
+
} finally {
|
|
379
|
+
currentEffect = prevEffect;
|
|
380
|
+
currentDeps = prevDeps;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
effect();
|
|
385
|
+
|
|
386
|
+
const dispose = () => {
|
|
387
|
+
for (const depSet of ownDeps) depSet.delete(effect);
|
|
388
|
+
ownDeps.clear();
|
|
389
|
+
if (cleanup) cleanup();
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
((globalThis as any).__sigilEffectScope as (() => void)[] | undefined)?.push(
|
|
393
|
+
dispose,
|
|
394
|
+
);
|
|
395
|
+
return dispose;
|
|
383
396
|
}
|
|
384
397
|
|
|
385
398
|
/**
|
|
@@ -388,24 +401,24 @@ export function createEffect(fn: () => void | (() => void)): () => void {
|
|
|
388
401
|
* Supports nesting: inner scopes don't leak to outer ones.
|
|
389
402
|
*/
|
|
390
403
|
export function withEffectScope(fn: () => void): () => void {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
404
|
+
const prev = (globalThis as any).__sigilEffectScope;
|
|
405
|
+
const disposers: (() => void)[] = [];
|
|
406
|
+
(globalThis as any).__sigilEffectScope = disposers;
|
|
407
|
+
try {
|
|
408
|
+
fn();
|
|
409
|
+
} finally {
|
|
410
|
+
(globalThis as any).__sigilEffectScope = prev;
|
|
411
|
+
}
|
|
412
|
+
return () => {
|
|
413
|
+
for (const d of disposers) d();
|
|
414
|
+
disposers.length = 0;
|
|
415
|
+
};
|
|
403
416
|
}
|
|
404
417
|
|
|
405
418
|
export function createMemo<T>(fn: () => T): Signal<T> {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
419
|
+
const inner = createSignal<T>(undefined as T);
|
|
420
|
+
createEffect(() => inner.set(fn()));
|
|
421
|
+
return inner;
|
|
409
422
|
}
|
|
410
423
|
|
|
411
424
|
/**
|
|
@@ -414,23 +427,23 @@ export function createMemo<T>(fn: () => T): Signal<T> {
|
|
|
414
427
|
* When falling back to createElement, appends to parent if provided (SPA mode).
|
|
415
428
|
*/
|
|
416
429
|
export function claim(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
430
|
+
nodes: ChildNode[],
|
|
431
|
+
tag: string,
|
|
432
|
+
parent?: Element,
|
|
420
433
|
): Element {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
435
|
+
const node = nodes[i];
|
|
436
|
+
if (
|
|
437
|
+
node instanceof Element &&
|
|
438
|
+
node.tagName.toLowerCase() === tag.toLowerCase()
|
|
439
|
+
) {
|
|
440
|
+
nodes.splice(i, 1);
|
|
441
|
+
return node;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const el = document.createElement(tag);
|
|
445
|
+
if (parent) parent.appendChild(el);
|
|
446
|
+
return el;
|
|
434
447
|
}
|
|
435
448
|
|
|
436
449
|
/**
|
|
@@ -439,16 +452,16 @@ export function claim(
|
|
|
439
452
|
* When falling back to createTextNode, appends to parent if provided (SPA mode).
|
|
440
453
|
*/
|
|
441
454
|
export function claimText(nodes: ChildNode[], parent?: Element): Text {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
455
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
456
|
+
const node = nodes[i];
|
|
457
|
+
if (node instanceof Text) {
|
|
458
|
+
nodes.splice(i, 1);
|
|
459
|
+
return node;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const text = document.createTextNode("");
|
|
463
|
+
if (parent) parent.appendChild(text);
|
|
464
|
+
return text;
|
|
452
465
|
}
|
|
453
466
|
|
|
454
467
|
/**
|
|
@@ -457,20 +470,20 @@ export function claimText(nodes: ChildNode[], parent?: Element): Text {
|
|
|
457
470
|
* When falling back to createComment, appends to parent if provided (SPA mode).
|
|
458
471
|
*/
|
|
459
472
|
export function claimComment(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
473
|
+
nodes: ChildNode[],
|
|
474
|
+
text: string = "",
|
|
475
|
+
parent?: Element,
|
|
463
476
|
): Comment {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
477
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
478
|
+
const node = nodes[i];
|
|
479
|
+
if (node instanceof Comment && node.data === text) {
|
|
480
|
+
nodes.splice(i, 1);
|
|
481
|
+
return node;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const comment = document.createComment(text);
|
|
485
|
+
if (parent) parent.appendChild(comment);
|
|
486
|
+
return comment;
|
|
474
487
|
}
|
|
475
488
|
|
|
476
489
|
/**
|
|
@@ -481,33 +494,33 @@ export function claimComment(
|
|
|
481
494
|
* or null to discard.
|
|
482
495
|
*/
|
|
483
496
|
export function reconcile<T>(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
497
|
+
anchor: Partial<Comment>,
|
|
498
|
+
keyMap: Map<unknown, Node>,
|
|
499
|
+
newItems: T[],
|
|
500
|
+
getKey: (item: T) => unknown,
|
|
501
|
+
createNode: (item: T) => Node,
|
|
502
|
+
// is this meant to be unused? claimNode?: (element: Element, key: unknown) => Element | null,
|
|
490
503
|
): void {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
504
|
+
const newKeys = new Set(newItems.map(getKey));
|
|
505
|
+
|
|
506
|
+
// Remove nodes no longer in list (or claim them for hydration)
|
|
507
|
+
for (const [key, node] of keyMap) {
|
|
508
|
+
if (!newKeys.has(key)) {
|
|
509
|
+
node.parentNode?.removeChild(node);
|
|
510
|
+
keyMap.delete(key);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Insert/move in order — anchor.before() handles both cases
|
|
515
|
+
for (const item of newItems) {
|
|
516
|
+
const key = getKey(item);
|
|
517
|
+
let node = keyMap.get(key);
|
|
518
|
+
if (!node) {
|
|
519
|
+
node = createNode(item);
|
|
520
|
+
keyMap.set(key, node);
|
|
521
|
+
}
|
|
522
|
+
anchor.before?.(node);
|
|
523
|
+
}
|
|
511
524
|
}
|
|
512
525
|
|
|
513
526
|
/**
|
|
@@ -518,63 +531,63 @@ export function reconcile<T>(
|
|
|
518
531
|
* builds the keyMap, removes from DOM, then reconcile re-inserts in order.
|
|
519
532
|
*/
|
|
520
533
|
export function hydrateKeyedList<T>(
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
534
|
+
nodesVar: ChildNode[],
|
|
535
|
+
signalItems: T[],
|
|
536
|
+
getKey: (item: T) => unknown,
|
|
537
|
+
createNode: (item: T) => Node,
|
|
538
|
+
parent?: Element,
|
|
526
539
|
): { anchor: Comment; keyMap: Map<unknown, Node> } {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
540
|
+
// 1. Claim the <!--g--> opening delimiter as anchor
|
|
541
|
+
const anchor = claimComment(nodesVar, "g", parent);
|
|
542
|
+
|
|
543
|
+
// 2. Walk siblings after anchor, collect elements up to <!--/g-->, build keyMap
|
|
544
|
+
const keyMap = new Map<unknown, Node>();
|
|
545
|
+
const anchorParent = anchor.parentNode;
|
|
546
|
+
if (anchorParent && signalItems.length > 0) {
|
|
547
|
+
const claimed: Element[] = [];
|
|
548
|
+
let next = anchor.nextSibling;
|
|
549
|
+
while (next) {
|
|
550
|
+
const current = next;
|
|
551
|
+
next = next.nextSibling;
|
|
552
|
+
// Stop at closing <!--/g--> delimiter and consume it
|
|
553
|
+
if (current instanceof Comment && current.data === "/g") {
|
|
554
|
+
current.remove();
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
if (current instanceof Element) {
|
|
558
|
+
current.remove();
|
|
559
|
+
claimed.push(current);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// Map claimed elements to keys by index
|
|
563
|
+
for (let i = 0; i < claimed.length && i < signalItems.length; i++) {
|
|
564
|
+
//@ts-expect-error i is always in bounds
|
|
565
|
+
const key = getKey(signalItems[i]);
|
|
566
|
+
//@ts-expect-error i is always in bounds
|
|
567
|
+
keyMap.set(key, claimed[i]);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// 3. Reconcile — keyMap is pre-populated with claimed nodes
|
|
572
|
+
reconcile(anchor, keyMap, signalItems, getKey, createNode);
|
|
573
|
+
return { anchor, keyMap };
|
|
561
574
|
}
|
|
562
575
|
|
|
563
576
|
export function insert(parent: Element, value: unknown): void {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
577
|
+
if (value === false || value === null || value === undefined) return;
|
|
578
|
+
if (typeof value === "function") {
|
|
579
|
+
insert(parent, value());
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (Array.isArray(value)) {
|
|
583
|
+
for (const item of value) insert(parent, item);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (value instanceof Node) {
|
|
587
|
+
parent.appendChild(value);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
parent.append(String(value));
|
|
578
591
|
}
|
|
579
592
|
|
|
580
593
|
/**
|
|
@@ -582,19 +595,22 @@ export function insert(parent: Element, value: unknown): void {
|
|
|
582
595
|
* The directive function receives (element, params) and may return a cleanup function.
|
|
583
596
|
* Cleanup runs when the enclosing $effect re-runs or the component is destroyed.
|
|
584
597
|
*/
|
|
585
|
-
export type Directive = (
|
|
598
|
+
export type Directive = (
|
|
599
|
+
node: Element,
|
|
600
|
+
params: unknown,
|
|
601
|
+
) => undefined | (() => void);
|
|
586
602
|
|
|
587
603
|
export function applyDirective(
|
|
588
|
-
|
|
589
|
-
|
|
604
|
+
node: Element,
|
|
605
|
+
directive: Directive | [Directive, unknown],
|
|
590
606
|
): void {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
607
|
+
const [fn, params] = Array.isArray(directive)
|
|
608
|
+
? directive
|
|
609
|
+
: [directive, undefined];
|
|
610
|
+
const cleanup = fn(node, params);
|
|
611
|
+
if (typeof cleanup === "function") {
|
|
612
|
+
createEffect(() => () => cleanup());
|
|
613
|
+
}
|
|
598
614
|
}
|
|
599
615
|
|
|
600
616
|
export type ContextKey<T> = symbol & { __type: T };
|
|
@@ -603,35 +619,35 @@ const clientContextStore = new Map<symbol, unknown>();
|
|
|
603
619
|
|
|
604
620
|
let storeProvider: (() => Map<symbol, unknown>) | null = null;
|
|
605
621
|
export function __setStoreProvider(fn: () => Map<symbol, unknown>) {
|
|
606
|
-
|
|
622
|
+
storeProvider = fn;
|
|
607
623
|
}
|
|
608
624
|
|
|
609
625
|
function getStore(): Map<symbol, unknown> {
|
|
610
|
-
|
|
626
|
+
return storeProvider?.() ?? clientContextStore;
|
|
611
627
|
}
|
|
612
628
|
|
|
613
629
|
export function createContext<T>(): ContextKey<T> {
|
|
614
|
-
|
|
630
|
+
return Symbol() as ContextKey<T>;
|
|
615
631
|
}
|
|
616
632
|
|
|
617
633
|
export function setContext<T>(key: ContextKey<T>, value: T): void {
|
|
618
|
-
|
|
634
|
+
getStore().set(key as unknown as symbol, value);
|
|
619
635
|
}
|
|
620
636
|
|
|
621
637
|
export function getContext<T>(key: ContextKey<T>): T {
|
|
622
|
-
|
|
638
|
+
return getStore().get(key as unknown as symbol) as T;
|
|
623
639
|
}
|
|
624
640
|
|
|
625
641
|
const _hydrationStack: ChildNode[][] = [];
|
|
626
642
|
|
|
627
643
|
export function pushHydrationNodes(nodes: ChildNode[]): void {
|
|
628
|
-
|
|
644
|
+
_hydrationStack.push(nodes);
|
|
629
645
|
}
|
|
630
646
|
|
|
631
647
|
export function popHydrationNodes(): void {
|
|
632
|
-
|
|
648
|
+
_hydrationStack.pop();
|
|
633
649
|
}
|
|
634
650
|
|
|
635
651
|
export function getHydrationNodes(): ChildNode[] {
|
|
636
|
-
|
|
652
|
+
return _hydrationStack[_hydrationStack.length - 1] ?? [];
|
|
637
653
|
}
|