jotai-solid-api 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -0
- package/dist/index.d.ts +861 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1470 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { atom, createStore as createJotaiStore } from "jotai/vanilla";
|
|
4
|
+
const collectorStack = [];
|
|
5
|
+
let batchDepth = 0;
|
|
6
|
+
const pendingSubscribers = new Set();
|
|
7
|
+
function notifySubscriber(subscriber) {
|
|
8
|
+
if (batchDepth > 0) {
|
|
9
|
+
pendingSubscribers.add(subscriber);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
subscriber();
|
|
13
|
+
}
|
|
14
|
+
function flushSubscribers() {
|
|
15
|
+
while (pendingSubscribers.size > 0) {
|
|
16
|
+
const queued = Array.from(pendingSubscribers);
|
|
17
|
+
pendingSubscribers.clear();
|
|
18
|
+
for (const subscriber of queued) {
|
|
19
|
+
subscriber();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function currentCollector() {
|
|
24
|
+
return collectorStack[collectorStack.length - 1];
|
|
25
|
+
}
|
|
26
|
+
function pushCollector(collector) {
|
|
27
|
+
collectorStack.push(collector);
|
|
28
|
+
}
|
|
29
|
+
function popCollector() {
|
|
30
|
+
collectorStack.pop();
|
|
31
|
+
}
|
|
32
|
+
function trackDependency(dep) {
|
|
33
|
+
currentCollector()?.addDependency(dep);
|
|
34
|
+
}
|
|
35
|
+
class DependencyTracker {
|
|
36
|
+
constructor(onDependencyChange) {
|
|
37
|
+
this.onDependencyChange = onDependencyChange;
|
|
38
|
+
this.subscriptions = new Map();
|
|
39
|
+
this.collecting = new Set();
|
|
40
|
+
}
|
|
41
|
+
addDependency(dep) {
|
|
42
|
+
this.collecting.add(dep);
|
|
43
|
+
}
|
|
44
|
+
collect(fn) {
|
|
45
|
+
this.collecting = new Set();
|
|
46
|
+
pushCollector(this);
|
|
47
|
+
try {
|
|
48
|
+
return fn();
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
popCollector();
|
|
52
|
+
this.reconcileSubscriptions(this.collecting);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
dispose() {
|
|
56
|
+
for (const unsubscribe of this.subscriptions.values()) {
|
|
57
|
+
unsubscribe();
|
|
58
|
+
}
|
|
59
|
+
this.subscriptions.clear();
|
|
60
|
+
}
|
|
61
|
+
reconcileSubscriptions(nextDeps) {
|
|
62
|
+
for (const [dep, unsubscribe] of this.subscriptions) {
|
|
63
|
+
if (!nextDeps.has(dep)) {
|
|
64
|
+
unsubscribe();
|
|
65
|
+
this.subscriptions.delete(dep);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const dep of nextDeps) {
|
|
69
|
+
if (!this.subscriptions.has(dep)) {
|
|
70
|
+
const unsubscribe = dep.subscribe(this.onDependencyChange);
|
|
71
|
+
this.subscriptions.set(dep, unsubscribe);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
class Scope {
|
|
77
|
+
constructor() {
|
|
78
|
+
this.store = createJotaiStore();
|
|
79
|
+
this.disposables = new Set();
|
|
80
|
+
this.scopeCleanups = new Set();
|
|
81
|
+
this.layoutStarters = [];
|
|
82
|
+
this.effectStarters = [];
|
|
83
|
+
this.layoutStarted = false;
|
|
84
|
+
this.effectsStarted = false;
|
|
85
|
+
}
|
|
86
|
+
register(disposable) {
|
|
87
|
+
this.disposables.add(disposable);
|
|
88
|
+
}
|
|
89
|
+
registerCleanup(cleanup) {
|
|
90
|
+
this.scopeCleanups.add(cleanup);
|
|
91
|
+
}
|
|
92
|
+
registerLayoutStarter(starter) {
|
|
93
|
+
if (this.layoutStarted) {
|
|
94
|
+
starter();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.layoutStarters.push(starter);
|
|
98
|
+
}
|
|
99
|
+
registerEffectStarter(starter) {
|
|
100
|
+
if (this.effectsStarted) {
|
|
101
|
+
starter();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.effectStarters.push(starter);
|
|
105
|
+
}
|
|
106
|
+
startLayoutEffects() {
|
|
107
|
+
if (this.layoutStarted) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.layoutStarted = true;
|
|
111
|
+
for (const starter of this.layoutStarters) {
|
|
112
|
+
starter();
|
|
113
|
+
}
|
|
114
|
+
this.layoutStarters.length = 0;
|
|
115
|
+
}
|
|
116
|
+
startEffects() {
|
|
117
|
+
if (this.effectsStarted) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.effectsStarted = true;
|
|
121
|
+
for (const starter of this.effectStarters) {
|
|
122
|
+
starter();
|
|
123
|
+
}
|
|
124
|
+
this.effectStarters.length = 0;
|
|
125
|
+
}
|
|
126
|
+
dispose() {
|
|
127
|
+
for (const cleanup of this.scopeCleanups) {
|
|
128
|
+
cleanup();
|
|
129
|
+
}
|
|
130
|
+
this.scopeCleanups.clear();
|
|
131
|
+
for (const disposable of this.disposables) {
|
|
132
|
+
disposable.dispose();
|
|
133
|
+
}
|
|
134
|
+
this.disposables.clear();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const scopeStack = [];
|
|
138
|
+
function withScope(scope, fn) {
|
|
139
|
+
scopeStack.push(scope);
|
|
140
|
+
try {
|
|
141
|
+
return fn();
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
scopeStack.pop();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function activeScope() {
|
|
148
|
+
const scope = scopeStack[scopeStack.length - 1];
|
|
149
|
+
if (!scope) {
|
|
150
|
+
throw new Error("No active reactive scope. Wrap your component with component(...) before calling reactive APIs.");
|
|
151
|
+
}
|
|
152
|
+
return scope;
|
|
153
|
+
}
|
|
154
|
+
class SignalSource {
|
|
155
|
+
constructor(store, initialValue) {
|
|
156
|
+
this.store = store;
|
|
157
|
+
this.subscribers = new Set();
|
|
158
|
+
this.signalAtom = atom(initialValue);
|
|
159
|
+
}
|
|
160
|
+
get() {
|
|
161
|
+
trackDependency(this);
|
|
162
|
+
return this.store.get(this.signalAtom);
|
|
163
|
+
}
|
|
164
|
+
peek() {
|
|
165
|
+
return this.store.get(this.signalAtom);
|
|
166
|
+
}
|
|
167
|
+
set(nextValue) {
|
|
168
|
+
const previous = this.store.get(this.signalAtom);
|
|
169
|
+
const resolvedValue = typeof nextValue === "function"
|
|
170
|
+
? nextValue(previous)
|
|
171
|
+
: nextValue;
|
|
172
|
+
this.store.set(this.signalAtom, resolvedValue);
|
|
173
|
+
if (!Object.is(previous, resolvedValue)) {
|
|
174
|
+
for (const subscriber of this.subscribers) {
|
|
175
|
+
notifySubscriber(subscriber);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return resolvedValue;
|
|
179
|
+
}
|
|
180
|
+
subscribe(callback) {
|
|
181
|
+
this.subscribers.add(callback);
|
|
182
|
+
return () => {
|
|
183
|
+
this.subscribers.delete(callback);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
class MemoSource {
|
|
188
|
+
constructor(compute) {
|
|
189
|
+
this.compute = compute;
|
|
190
|
+
this.subscribers = new Set();
|
|
191
|
+
this.hasValue = false;
|
|
192
|
+
this.computing = false;
|
|
193
|
+
this.tracker = new DependencyTracker(() => {
|
|
194
|
+
this.recompute();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
get() {
|
|
198
|
+
trackDependency(this);
|
|
199
|
+
if (!this.hasValue) {
|
|
200
|
+
this.recompute();
|
|
201
|
+
}
|
|
202
|
+
return this.value;
|
|
203
|
+
}
|
|
204
|
+
subscribe(callback) {
|
|
205
|
+
this.subscribers.add(callback);
|
|
206
|
+
if (!this.hasValue) {
|
|
207
|
+
this.recompute();
|
|
208
|
+
}
|
|
209
|
+
return () => {
|
|
210
|
+
this.subscribers.delete(callback);
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
dispose() {
|
|
214
|
+
this.tracker.dispose();
|
|
215
|
+
this.subscribers.clear();
|
|
216
|
+
}
|
|
217
|
+
recompute() {
|
|
218
|
+
if (this.computing) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
this.computing = true;
|
|
222
|
+
try {
|
|
223
|
+
const nextValue = this.tracker.collect(this.compute);
|
|
224
|
+
const changed = !this.hasValue || !Object.is(this.value, nextValue);
|
|
225
|
+
this.value = nextValue;
|
|
226
|
+
this.hasValue = true;
|
|
227
|
+
if (changed) {
|
|
228
|
+
for (const subscriber of this.subscribers) {
|
|
229
|
+
notifySubscriber(subscriber);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
this.computing = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
let activeEffect = null;
|
|
239
|
+
class EffectComputation {
|
|
240
|
+
constructor(effect) {
|
|
241
|
+
this.effect = effect;
|
|
242
|
+
this.scheduled = false;
|
|
243
|
+
this.running = false;
|
|
244
|
+
this.disposed = false;
|
|
245
|
+
this.cleanups = [];
|
|
246
|
+
this.tracker = new DependencyTracker(() => {
|
|
247
|
+
this.schedule();
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
start() {
|
|
251
|
+
this.schedule();
|
|
252
|
+
}
|
|
253
|
+
registerCleanup(cleanup) {
|
|
254
|
+
this.cleanups.push(cleanup);
|
|
255
|
+
}
|
|
256
|
+
dispose() {
|
|
257
|
+
if (this.disposed) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
this.disposed = true;
|
|
261
|
+
this.runCleanups();
|
|
262
|
+
this.tracker.dispose();
|
|
263
|
+
}
|
|
264
|
+
schedule() {
|
|
265
|
+
if (this.disposed) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (this.running) {
|
|
269
|
+
this.scheduled = true;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.execute();
|
|
273
|
+
}
|
|
274
|
+
execute() {
|
|
275
|
+
if (this.disposed) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
this.running = true;
|
|
279
|
+
this.scheduled = false;
|
|
280
|
+
this.runCleanups();
|
|
281
|
+
const previousEffect = activeEffect;
|
|
282
|
+
activeEffect = this;
|
|
283
|
+
try {
|
|
284
|
+
const maybeCleanup = this.tracker.collect(this.effect);
|
|
285
|
+
if (typeof maybeCleanup === "function") {
|
|
286
|
+
this.cleanups.push(maybeCleanup);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
activeEffect = previousEffect;
|
|
291
|
+
this.running = false;
|
|
292
|
+
}
|
|
293
|
+
if (this.scheduled) {
|
|
294
|
+
this.execute();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
runCleanups() {
|
|
298
|
+
const pending = this.cleanups;
|
|
299
|
+
this.cleanups = [];
|
|
300
|
+
for (const cleanup of pending) {
|
|
301
|
+
cleanup();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Creates a writable signal.
|
|
307
|
+
*
|
|
308
|
+
* @param initialValue Initial value stored in the signal.
|
|
309
|
+
* @returns Tuple of `[getter, setter]`.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* const [count, setCount] = createSignal(0)
|
|
314
|
+
* setCount((n) => n + 1)
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
export function createSignal(initialValue) {
|
|
318
|
+
const scope = activeScope();
|
|
319
|
+
const source = new SignalSource(scope.store, initialValue);
|
|
320
|
+
return [
|
|
321
|
+
() => source.get(),
|
|
322
|
+
(nextValue) => source.set(nextValue),
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Alias for {@link createSignal}.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* const [count, setCount] = signal(0)
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
export const signal = createSignal;
|
|
334
|
+
/**
|
|
335
|
+
* Adapts a Solid-compatible signal pair into this library's strict setter shape.
|
|
336
|
+
*
|
|
337
|
+
* @param solidSignal Signal tuple `[get, set]` from Solid or compatible runtimes.
|
|
338
|
+
* @returns Tuple `[get, set]` where `set` returns the current value.
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* ```ts
|
|
342
|
+
* const [count, setCount] = fromSolidSignal(otherSignal)
|
|
343
|
+
* setCount((n) => n + 1)
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
export function fromSolidSignal(solidSignal) {
|
|
347
|
+
const [get, set] = solidSignal;
|
|
348
|
+
const normalizedSet = (nextValue) => {
|
|
349
|
+
set(nextValue);
|
|
350
|
+
return get();
|
|
351
|
+
};
|
|
352
|
+
return [get, normalizedSet];
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Adapts this library's signal pair to a Solid-compatible tuple shape.
|
|
356
|
+
*
|
|
357
|
+
* @param reactiveSignal Tuple `[get, set]` from this library.
|
|
358
|
+
* @returns Solid-compatible signal tuple.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```ts
|
|
362
|
+
* const solidPair = toSolidSignal(createSignal(0))
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
export function toSolidSignal(reactiveSignal) {
|
|
366
|
+
const [get, set] = reactiveSignal;
|
|
367
|
+
const solidSetter = (nextValue) => {
|
|
368
|
+
set(nextValue);
|
|
369
|
+
};
|
|
370
|
+
return [get, solidSetter];
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Alias for {@link fromSolidSignal}.
|
|
374
|
+
*/
|
|
375
|
+
export const fromSignal = fromSolidSignal;
|
|
376
|
+
/**
|
|
377
|
+
* Alias for {@link toSolidSignal}.
|
|
378
|
+
*/
|
|
379
|
+
export const toSignal = toSolidSignal;
|
|
380
|
+
/**
|
|
381
|
+
* Creates a cached derived accessor.
|
|
382
|
+
*
|
|
383
|
+
* @param compute Derivation function. Reads inside this function become dependencies.
|
|
384
|
+
* @returns Read-only accessor for the derived value.
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```ts
|
|
388
|
+
* const total = createMemo(() => items().length)
|
|
389
|
+
* ```
|
|
390
|
+
*/
|
|
391
|
+
export function createMemo(compute) {
|
|
392
|
+
const scope = activeScope();
|
|
393
|
+
const source = new MemoSource(compute);
|
|
394
|
+
scope.register(source);
|
|
395
|
+
return () => source.get();
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Alias for {@link createMemo}.
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```ts
|
|
402
|
+
* const total = memo(() => items().length)
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
export const memo = createMemo;
|
|
406
|
+
/**
|
|
407
|
+
* Registers an effect that runs after React commit.
|
|
408
|
+
*
|
|
409
|
+
* @param effect Effect callback. Return a cleanup function to dispose previous run resources.
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* ```ts
|
|
413
|
+
* createEffect(() => {
|
|
414
|
+
* console.log(count())
|
|
415
|
+
* })
|
|
416
|
+
* ```
|
|
417
|
+
*/
|
|
418
|
+
export function createEffect(effect) {
|
|
419
|
+
const scope = activeScope();
|
|
420
|
+
const computation = new EffectComputation(effect);
|
|
421
|
+
scope.register(computation);
|
|
422
|
+
scope.registerEffectStarter(() => {
|
|
423
|
+
computation.start();
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Alias for {@link createEffect}.
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* ```ts
|
|
431
|
+
* effect(() => console.log(count()))
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
export const effect = createEffect;
|
|
435
|
+
/**
|
|
436
|
+
* Registers an effect that runs in layout phase.
|
|
437
|
+
*
|
|
438
|
+
* @param effect Layout effect callback. Return a cleanup function to dispose previous run resources.
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```ts
|
|
442
|
+
* createLayoutEffect(() => {
|
|
443
|
+
* measureLayout()
|
|
444
|
+
* })
|
|
445
|
+
* ```
|
|
446
|
+
*/
|
|
447
|
+
export function createLayoutEffect(effect) {
|
|
448
|
+
const scope = activeScope();
|
|
449
|
+
const computation = new EffectComputation(effect);
|
|
450
|
+
scope.register(computation);
|
|
451
|
+
scope.registerLayoutStarter(() => {
|
|
452
|
+
computation.start();
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Alias for {@link createLayoutEffect}.
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```ts
|
|
460
|
+
* layoutEffect(() => measure())
|
|
461
|
+
* ```
|
|
462
|
+
*/
|
|
463
|
+
export const layoutEffect = createLayoutEffect;
|
|
464
|
+
/**
|
|
465
|
+
* Creates a reactive computation for side effects.
|
|
466
|
+
* Alias of {@link createEffect} for Solid-style API compatibility.
|
|
467
|
+
*
|
|
468
|
+
* @param compute Side-effect function.
|
|
469
|
+
*
|
|
470
|
+
* @example
|
|
471
|
+
* ```ts
|
|
472
|
+
* createComputed(() => {
|
|
473
|
+
* syncExternalStore(count())
|
|
474
|
+
* })
|
|
475
|
+
* ```
|
|
476
|
+
*/
|
|
477
|
+
export function createComputed(compute) {
|
|
478
|
+
createEffect(compute);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Alias for {@link createComputed}.
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* ```ts
|
|
485
|
+
* computed(() => sync(count()))
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
export const computed = createComputed;
|
|
489
|
+
/**
|
|
490
|
+
* Runs a callback once on mount and disposes it on unmount if cleanup is returned.
|
|
491
|
+
*
|
|
492
|
+
* @param callback Mount callback.
|
|
493
|
+
*
|
|
494
|
+
* @example
|
|
495
|
+
* ```ts
|
|
496
|
+
* onMount(() => {
|
|
497
|
+
* const ws = connect()
|
|
498
|
+
* return () => ws.close()
|
|
499
|
+
* })
|
|
500
|
+
* ```
|
|
501
|
+
*/
|
|
502
|
+
export function onMount(callback) {
|
|
503
|
+
createEffect(() => {
|
|
504
|
+
const maybeCleanup = untrack(callback);
|
|
505
|
+
if (typeof maybeCleanup === "function") {
|
|
506
|
+
onCleanup(maybeCleanup);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Alias for {@link onMount}.
|
|
512
|
+
*
|
|
513
|
+
* @example
|
|
514
|
+
* ```ts
|
|
515
|
+
* mount(() => console.log("mounted"))
|
|
516
|
+
* ```
|
|
517
|
+
*/
|
|
518
|
+
export const mount = onMount;
|
|
519
|
+
/**
|
|
520
|
+
* Registers cleanup in setup scope or currently running effect.
|
|
521
|
+
*
|
|
522
|
+
* @param cleanup Cleanup callback invoked on re-run or scope disposal.
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* ```ts
|
|
526
|
+
* createEffect(() => {
|
|
527
|
+
* const id = setInterval(tick, 1000)
|
|
528
|
+
* onCleanup(() => clearInterval(id))
|
|
529
|
+
* })
|
|
530
|
+
* ```
|
|
531
|
+
*/
|
|
532
|
+
export function onCleanup(cleanup) {
|
|
533
|
+
if (activeEffect) {
|
|
534
|
+
activeEffect.registerCleanup(cleanup);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const scope = scopeStack[scopeStack.length - 1];
|
|
538
|
+
if (scope) {
|
|
539
|
+
scope.registerCleanup(cleanup);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
throw new Error("onCleanup must run inside component setup or an effect.");
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Alias for {@link onCleanup}.
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```ts
|
|
549
|
+
* cleanup(() => console.log("disposed"))
|
|
550
|
+
* ```
|
|
551
|
+
*/
|
|
552
|
+
export const cleanup = onCleanup;
|
|
553
|
+
/**
|
|
554
|
+
* Batches reactive notifications and flushes once at the end.
|
|
555
|
+
*
|
|
556
|
+
* @param fn Function containing grouped signal/store writes.
|
|
557
|
+
* @returns Result of `fn()`.
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* ```ts
|
|
561
|
+
* batch(() => {
|
|
562
|
+
* setFirst("Ada")
|
|
563
|
+
* setLast("Lovelace")
|
|
564
|
+
* })
|
|
565
|
+
* ```
|
|
566
|
+
*/
|
|
567
|
+
export function batch(fn) {
|
|
568
|
+
batchDepth += 1;
|
|
569
|
+
try {
|
|
570
|
+
return fn();
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
batchDepth -= 1;
|
|
574
|
+
if (batchDepth === 0) {
|
|
575
|
+
flushSubscribers();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Executes a function without dependency tracking.
|
|
581
|
+
*
|
|
582
|
+
* @param fn Function to execute without capturing dependencies.
|
|
583
|
+
* @returns Result of `fn()`.
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```ts
|
|
587
|
+
* const stable = untrack(() => expensiveSnapshot())
|
|
588
|
+
* ```
|
|
589
|
+
*/
|
|
590
|
+
export function untrack(fn) {
|
|
591
|
+
const previous = collectorStack.pop();
|
|
592
|
+
try {
|
|
593
|
+
return fn();
|
|
594
|
+
}
|
|
595
|
+
finally {
|
|
596
|
+
if (previous) {
|
|
597
|
+
collectorStack.push(previous);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function resolveResourceArgs(sourceOrFetcher, maybeFetcher, maybeOptions) {
|
|
602
|
+
if (maybeFetcher) {
|
|
603
|
+
return {
|
|
604
|
+
source: sourceOrFetcher,
|
|
605
|
+
fetcher: maybeFetcher,
|
|
606
|
+
options: maybeOptions ?? {},
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
source: null,
|
|
611
|
+
fetcher: sourceOrFetcher,
|
|
612
|
+
options: maybeOptions ?? {},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
export function createResource(sourceOrFetcher, maybeFetcher, maybeOptions) {
|
|
616
|
+
const isSourceMode = typeof maybeFetcher === "function";
|
|
617
|
+
const parsed = isSourceMode
|
|
618
|
+
? resolveResourceArgs(sourceOrFetcher, maybeFetcher, maybeOptions)
|
|
619
|
+
: resolveResourceArgs(sourceOrFetcher, undefined, maybeFetcher);
|
|
620
|
+
const [value, setValue] = createSignal(parsed.options.initialValue);
|
|
621
|
+
const [latest, setLatest] = createSignal(parsed.options.initialValue);
|
|
622
|
+
const [loading, setLoading] = createSignal(false);
|
|
623
|
+
const [error, setError] = createSignal(undefined);
|
|
624
|
+
const [state, setState] = createSignal(parsed.options.initialValue === undefined ? "unresolved" : "ready");
|
|
625
|
+
const [refetchCount, setRefetchCount] = createSignal(0);
|
|
626
|
+
let runId = 0;
|
|
627
|
+
createEffect(() => {
|
|
628
|
+
const sourceValue = parsed.source ? parsed.source() : undefined;
|
|
629
|
+
const currentRefetchCount = refetchCount();
|
|
630
|
+
const refetching = currentRefetchCount > 0;
|
|
631
|
+
const requestId = ++runId;
|
|
632
|
+
setLoading(true);
|
|
633
|
+
setError(undefined);
|
|
634
|
+
setState("pending");
|
|
635
|
+
const execute = async () => {
|
|
636
|
+
try {
|
|
637
|
+
const nextValue = parsed.source
|
|
638
|
+
? await parsed.fetcher(sourceValue, { value: latest(), refetching })
|
|
639
|
+
: await parsed.fetcher();
|
|
640
|
+
if (requestId !== runId) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
setValue(nextValue);
|
|
644
|
+
setLatest(nextValue);
|
|
645
|
+
setState("ready");
|
|
646
|
+
}
|
|
647
|
+
catch (nextError) {
|
|
648
|
+
if (requestId !== runId) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
setError(nextError);
|
|
652
|
+
setState("errored");
|
|
653
|
+
}
|
|
654
|
+
finally {
|
|
655
|
+
if (requestId === runId) {
|
|
656
|
+
setLoading(false);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
void execute();
|
|
661
|
+
});
|
|
662
|
+
const resource = (() => value());
|
|
663
|
+
Object.defineProperties(resource, {
|
|
664
|
+
loading: {
|
|
665
|
+
value: () => loading(),
|
|
666
|
+
enumerable: true,
|
|
667
|
+
},
|
|
668
|
+
error: {
|
|
669
|
+
value: () => error(),
|
|
670
|
+
enumerable: true,
|
|
671
|
+
},
|
|
672
|
+
latest: {
|
|
673
|
+
value: () => latest(),
|
|
674
|
+
enumerable: true,
|
|
675
|
+
},
|
|
676
|
+
state: {
|
|
677
|
+
value: () => state(),
|
|
678
|
+
enumerable: true,
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
return [
|
|
682
|
+
resource,
|
|
683
|
+
{
|
|
684
|
+
mutate: setValue,
|
|
685
|
+
refetch: () => {
|
|
686
|
+
setRefetchCount((n) => n + 1);
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
];
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Alias for {@link createResource}.
|
|
693
|
+
*
|
|
694
|
+
* @example
|
|
695
|
+
* ```ts
|
|
696
|
+
* const [user] = resource(() => fetchUser())
|
|
697
|
+
* ```
|
|
698
|
+
*/
|
|
699
|
+
export const resource = createResource;
|
|
700
|
+
/**
|
|
701
|
+
* Shortcut for `createResource(fetcher)[0]`.
|
|
702
|
+
*
|
|
703
|
+
* @param compute Async or sync function that resolves the value.
|
|
704
|
+
* @param options Optional resource options such as `initialValue`.
|
|
705
|
+
* @returns Resource accessor only.
|
|
706
|
+
*
|
|
707
|
+
* @example
|
|
708
|
+
* ```ts
|
|
709
|
+
* const profile = createAsync(() => fetchProfile())
|
|
710
|
+
* ```
|
|
711
|
+
*/
|
|
712
|
+
export function createAsync(compute, options) {
|
|
713
|
+
const [resource] = createResource(compute, options);
|
|
714
|
+
return resource;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Alias for {@link createAsync}.
|
|
718
|
+
*
|
|
719
|
+
* @example
|
|
720
|
+
* ```ts
|
|
721
|
+
* const profile = asyncSignal(() => fetchProfile())
|
|
722
|
+
* ```
|
|
723
|
+
*/
|
|
724
|
+
export const asyncSignal = createAsync;
|
|
725
|
+
const promiseState = new WeakMap();
|
|
726
|
+
export function use(value) {
|
|
727
|
+
if (typeof value === "function") {
|
|
728
|
+
return value();
|
|
729
|
+
}
|
|
730
|
+
const existing = promiseState.get(value);
|
|
731
|
+
if (!existing) {
|
|
732
|
+
const state = { status: "pending" };
|
|
733
|
+
promiseState.set(value, state);
|
|
734
|
+
Promise.resolve(value).then((resolvedValue) => {
|
|
735
|
+
promiseState.set(value, {
|
|
736
|
+
status: "resolved",
|
|
737
|
+
value: resolvedValue,
|
|
738
|
+
});
|
|
739
|
+
}, (rejectedError) => {
|
|
740
|
+
promiseState.set(value, {
|
|
741
|
+
status: "rejected",
|
|
742
|
+
error: rejectedError,
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
throw value;
|
|
746
|
+
}
|
|
747
|
+
if (existing.status === "pending") {
|
|
748
|
+
throw value;
|
|
749
|
+
}
|
|
750
|
+
if (existing.status === "rejected") {
|
|
751
|
+
throw existing.error;
|
|
752
|
+
}
|
|
753
|
+
return existing.value;
|
|
754
|
+
}
|
|
755
|
+
const MUTATING_ARRAY_METHODS = new Set([
|
|
756
|
+
"copyWithin",
|
|
757
|
+
"fill",
|
|
758
|
+
"pop",
|
|
759
|
+
"push",
|
|
760
|
+
"reverse",
|
|
761
|
+
"shift",
|
|
762
|
+
"sort",
|
|
763
|
+
"splice",
|
|
764
|
+
"unshift",
|
|
765
|
+
]);
|
|
766
|
+
function isObjectLike(value) {
|
|
767
|
+
return typeof value === "object" && value !== null;
|
|
768
|
+
}
|
|
769
|
+
function shallowClone(value) {
|
|
770
|
+
if (Array.isArray(value)) {
|
|
771
|
+
return [...value];
|
|
772
|
+
}
|
|
773
|
+
if (isObjectLike(value)) {
|
|
774
|
+
return { ...value };
|
|
775
|
+
}
|
|
776
|
+
return value;
|
|
777
|
+
}
|
|
778
|
+
function getAtPath(root, path) {
|
|
779
|
+
let cursor = root;
|
|
780
|
+
for (const segment of path) {
|
|
781
|
+
if (!isObjectLike(cursor) && !Array.isArray(cursor)) {
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
cursor = cursor[segment];
|
|
785
|
+
}
|
|
786
|
+
return cursor;
|
|
787
|
+
}
|
|
788
|
+
function setAtPath(root, path, value) {
|
|
789
|
+
if (path.length === 0) {
|
|
790
|
+
return value;
|
|
791
|
+
}
|
|
792
|
+
const [head, ...tail] = path;
|
|
793
|
+
const clone = shallowClone(root);
|
|
794
|
+
const container = clone;
|
|
795
|
+
const currentValue = container[head];
|
|
796
|
+
container[head] =
|
|
797
|
+
tail.length === 0
|
|
798
|
+
? value
|
|
799
|
+
: setAtPath(isObjectLike(currentValue) || Array.isArray(currentValue)
|
|
800
|
+
? currentValue
|
|
801
|
+
: typeof tail[0] === "number"
|
|
802
|
+
? []
|
|
803
|
+
: {}, tail, value);
|
|
804
|
+
return clone;
|
|
805
|
+
}
|
|
806
|
+
function pathToKey(path) {
|
|
807
|
+
return path
|
|
808
|
+
.map((segment) => typeof segment === "symbol" ? `s:${String(segment.description ?? "")}` : `k:${String(segment)}`)
|
|
809
|
+
.join("|");
|
|
810
|
+
}
|
|
811
|
+
function createReactiveProxy(source, mutable) {
|
|
812
|
+
const proxyCache = new Map();
|
|
813
|
+
const readNode = (path) => getAtPath(source.get(), path);
|
|
814
|
+
const peekNode = (path) => getAtPath(source.peek(), path);
|
|
815
|
+
const createAtPath = (path) => {
|
|
816
|
+
const key = pathToKey(path);
|
|
817
|
+
const cached = proxyCache.get(key);
|
|
818
|
+
if (cached) {
|
|
819
|
+
return cached;
|
|
820
|
+
}
|
|
821
|
+
const initialNode = peekNode(path);
|
|
822
|
+
const target = Array.isArray(initialNode) ? [] : {};
|
|
823
|
+
const proxy = new Proxy(target, {
|
|
824
|
+
get(_target, prop) {
|
|
825
|
+
const node = readNode(path);
|
|
826
|
+
if (prop === Symbol.toStringTag && Array.isArray(node)) {
|
|
827
|
+
return "Array";
|
|
828
|
+
}
|
|
829
|
+
if (!isObjectLike(node) && !Array.isArray(node)) {
|
|
830
|
+
return undefined;
|
|
831
|
+
}
|
|
832
|
+
if (mutable &&
|
|
833
|
+
Array.isArray(node) &&
|
|
834
|
+
typeof prop === "string" &&
|
|
835
|
+
MUTATING_ARRAY_METHODS.has(prop)) {
|
|
836
|
+
return (...args) => {
|
|
837
|
+
const previousRoot = source.peek();
|
|
838
|
+
const currentArray = getAtPath(previousRoot, path);
|
|
839
|
+
if (!Array.isArray(currentArray)) {
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
const nextArray = [...currentArray];
|
|
843
|
+
const method = nextArray[prop];
|
|
844
|
+
const result = method.apply(nextArray, args);
|
|
845
|
+
const nextRoot = setAtPath(previousRoot, path, nextArray);
|
|
846
|
+
source.set(nextRoot);
|
|
847
|
+
return result;
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
const value = node[prop];
|
|
851
|
+
if (Array.isArray(node) && prop === Symbol.iterator) {
|
|
852
|
+
return value.bind(node);
|
|
853
|
+
}
|
|
854
|
+
if (isObjectLike(value) || Array.isArray(value)) {
|
|
855
|
+
return createAtPath([...path, prop]);
|
|
856
|
+
}
|
|
857
|
+
if (typeof value === "function") {
|
|
858
|
+
return value.bind(node);
|
|
859
|
+
}
|
|
860
|
+
return value;
|
|
861
|
+
},
|
|
862
|
+
set(_target, prop, value) {
|
|
863
|
+
if (!mutable) {
|
|
864
|
+
throw new Error("Cannot mutate immutable store. Use setStore instead.");
|
|
865
|
+
}
|
|
866
|
+
const previousRoot = source.peek();
|
|
867
|
+
const nextRoot = setAtPath(previousRoot, [...path, prop], value);
|
|
868
|
+
source.set(nextRoot);
|
|
869
|
+
return true;
|
|
870
|
+
},
|
|
871
|
+
deleteProperty(_target, prop) {
|
|
872
|
+
if (!mutable) {
|
|
873
|
+
throw new Error("Cannot mutate immutable store. Use setStore instead.");
|
|
874
|
+
}
|
|
875
|
+
const previousRoot = source.peek();
|
|
876
|
+
const node = getAtPath(previousRoot, path);
|
|
877
|
+
if (!isObjectLike(node) && !Array.isArray(node)) {
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
const clone = shallowClone(node);
|
|
881
|
+
delete clone[prop];
|
|
882
|
+
source.set(setAtPath(previousRoot, path, clone));
|
|
883
|
+
return true;
|
|
884
|
+
},
|
|
885
|
+
ownKeys() {
|
|
886
|
+
const node = readNode(path);
|
|
887
|
+
if (!isObjectLike(node) && !Array.isArray(node)) {
|
|
888
|
+
return [];
|
|
889
|
+
}
|
|
890
|
+
return Reflect.ownKeys(node);
|
|
891
|
+
},
|
|
892
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
893
|
+
const node = readNode(path);
|
|
894
|
+
if (!isObjectLike(node) && !Array.isArray(node)) {
|
|
895
|
+
return undefined;
|
|
896
|
+
}
|
|
897
|
+
const descriptor = Object.getOwnPropertyDescriptor(node, prop);
|
|
898
|
+
if (!descriptor) {
|
|
899
|
+
return {
|
|
900
|
+
configurable: true,
|
|
901
|
+
enumerable: true,
|
|
902
|
+
writable: true,
|
|
903
|
+
value: undefined,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
...descriptor,
|
|
908
|
+
configurable: true,
|
|
909
|
+
};
|
|
910
|
+
},
|
|
911
|
+
has(_target, prop) {
|
|
912
|
+
const node = readNode(path);
|
|
913
|
+
if (!isObjectLike(node) && !Array.isArray(node)) {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
return prop in node;
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
proxyCache.set(key, proxy);
|
|
920
|
+
return proxy;
|
|
921
|
+
};
|
|
922
|
+
return createAtPath([]);
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Creates an immutable reactive object store.
|
|
926
|
+
*
|
|
927
|
+
* @param initialValue Initial object state.
|
|
928
|
+
* @returns Tuple of `[storeProxy, setStore]`.
|
|
929
|
+
*
|
|
930
|
+
* @example
|
|
931
|
+
* ```ts
|
|
932
|
+
* const [store, setStore] = createStore({ count: 0 })
|
|
933
|
+
* setStore({ count: 1 })
|
|
934
|
+
* ```
|
|
935
|
+
*/
|
|
936
|
+
export function createStore(initialValue) {
|
|
937
|
+
const scope = activeScope();
|
|
938
|
+
const source = new SignalSource(scope.store, initialValue);
|
|
939
|
+
const store = createReactiveProxy(source, false);
|
|
940
|
+
const setStore = (next) => {
|
|
941
|
+
const previous = source.peek();
|
|
942
|
+
const resolved = typeof next === "function" ? next(previous) : next;
|
|
943
|
+
const merged = isObjectLike(previous) && isObjectLike(resolved)
|
|
944
|
+
? { ...previous, ...resolved }
|
|
945
|
+
: resolved;
|
|
946
|
+
return source.set(merged);
|
|
947
|
+
};
|
|
948
|
+
return [store, setStore];
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Alias for {@link createStore}.
|
|
952
|
+
*
|
|
953
|
+
* @example
|
|
954
|
+
* ```ts
|
|
955
|
+
* const [state, setState] = store({ count: 0 })
|
|
956
|
+
* ```
|
|
957
|
+
*/
|
|
958
|
+
export const store = createStore;
|
|
959
|
+
/**
|
|
960
|
+
* Typo-friendly alias for {@link createStore}.
|
|
961
|
+
*
|
|
962
|
+
* @example
|
|
963
|
+
* ```ts
|
|
964
|
+
* const [state, setState] = sotre({ count: 0 })
|
|
965
|
+
* ```
|
|
966
|
+
*/
|
|
967
|
+
export const sotre = createStore;
|
|
968
|
+
/**
|
|
969
|
+
* Creates a mutable reactive object.
|
|
970
|
+
*
|
|
971
|
+
* @param initialValue Initial object state.
|
|
972
|
+
* @returns Mutable reactive proxy.
|
|
973
|
+
*
|
|
974
|
+
* @example
|
|
975
|
+
* ```ts
|
|
976
|
+
* const state = createMutable({ count: 0 })
|
|
977
|
+
* state.count += 1
|
|
978
|
+
* ```
|
|
979
|
+
*/
|
|
980
|
+
export function createMutable(initialValue) {
|
|
981
|
+
const scope = activeScope();
|
|
982
|
+
const source = new SignalSource(scope.store, initialValue);
|
|
983
|
+
return createReactiveProxy(source, true);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Alias for {@link createMutable}.
|
|
987
|
+
*
|
|
988
|
+
* @example
|
|
989
|
+
* ```ts
|
|
990
|
+
* const state = mutable({ count: 0 })
|
|
991
|
+
* ```
|
|
992
|
+
*/
|
|
993
|
+
export const mutable = createMutable;
|
|
994
|
+
/**
|
|
995
|
+
* Alias for {@link createMutable}.
|
|
996
|
+
*
|
|
997
|
+
* @param initialValue Initial object state.
|
|
998
|
+
* @returns Mutable reactive proxy.
|
|
999
|
+
*
|
|
1000
|
+
* @example
|
|
1001
|
+
* ```ts
|
|
1002
|
+
* const state = createMutableStore({ value: 1 })
|
|
1003
|
+
* ```
|
|
1004
|
+
*/
|
|
1005
|
+
export function createMutableStore(initialValue) {
|
|
1006
|
+
return createMutable(initialValue);
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Creates a mutable reactive array.
|
|
1010
|
+
*
|
|
1011
|
+
* @param initialValue Initial list values.
|
|
1012
|
+
* @returns Mutable reactive array proxy.
|
|
1013
|
+
*
|
|
1014
|
+
* @example
|
|
1015
|
+
* ```ts
|
|
1016
|
+
* const list = createReactiveArray([1, 2])
|
|
1017
|
+
* list.push(3)
|
|
1018
|
+
* ```
|
|
1019
|
+
*/
|
|
1020
|
+
export function createReactiveArray(initialValue = []) {
|
|
1021
|
+
return createMutable(initialValue);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Alias for {@link createReactiveArray}.
|
|
1025
|
+
*
|
|
1026
|
+
* @param initialValue Initial list values.
|
|
1027
|
+
* @returns Mutable reactive array proxy.
|
|
1028
|
+
*
|
|
1029
|
+
* @example
|
|
1030
|
+
* ```ts
|
|
1031
|
+
* const list = createArrayStore(["a"])
|
|
1032
|
+
* ```
|
|
1033
|
+
*/
|
|
1034
|
+
export function createArrayStore(initialValue = []) {
|
|
1035
|
+
return createReactiveArray(initialValue);
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Creates a stable projected array with keyed move/insert/remove updates.
|
|
1039
|
+
*
|
|
1040
|
+
* @param source Source list accessor to project from.
|
|
1041
|
+
* @param options Projection behavior (`key`, `map`, optional `update`).
|
|
1042
|
+
* @returns Stable mutable projected array.
|
|
1043
|
+
*
|
|
1044
|
+
* @example
|
|
1045
|
+
* ```ts
|
|
1046
|
+
* const rows = createArrayProjection(users, {
|
|
1047
|
+
* key: (u) => u.id,
|
|
1048
|
+
* map: (u) => ({ id: u.id, name: u.name }),
|
|
1049
|
+
* update: (row, u) => { row.name = u.name },
|
|
1050
|
+
* })
|
|
1051
|
+
* ```
|
|
1052
|
+
*/
|
|
1053
|
+
export function createArrayProjection(source, options) {
|
|
1054
|
+
const keyFn = options.key ?? ((_, index) => index);
|
|
1055
|
+
let keys = [];
|
|
1056
|
+
return createProjection(source, (initialSource) => {
|
|
1057
|
+
const list = initialSource ?? [];
|
|
1058
|
+
keys = list.map((item, index) => keyFn(item, index));
|
|
1059
|
+
return list.map((item, index) => options.map(item, index));
|
|
1060
|
+
}, (target, nextSource) => {
|
|
1061
|
+
const list = nextSource ?? [];
|
|
1062
|
+
const nextKeys = list.map((item, index) => keyFn(item, index));
|
|
1063
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
1064
|
+
const nextKey = nextKeys[index];
|
|
1065
|
+
if (index < keys.length && Object.is(keys[index], nextKey)) {
|
|
1066
|
+
if (options.update) {
|
|
1067
|
+
options.update(target[index], list[index], index);
|
|
1068
|
+
}
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
let foundIndex = -1;
|
|
1072
|
+
for (let cursor = index + 1; cursor < keys.length; cursor += 1) {
|
|
1073
|
+
if (Object.is(keys[cursor], nextKey)) {
|
|
1074
|
+
foundIndex = cursor;
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (foundIndex >= 0) {
|
|
1079
|
+
const [movedItem] = target.splice(foundIndex, 1);
|
|
1080
|
+
const [movedKey] = keys.splice(foundIndex, 1);
|
|
1081
|
+
target.splice(index, 0, movedItem);
|
|
1082
|
+
keys.splice(index, 0, movedKey);
|
|
1083
|
+
if (options.update) {
|
|
1084
|
+
options.update(target[index], list[index], index);
|
|
1085
|
+
}
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
const projected = options.map(list[index], index);
|
|
1089
|
+
target.splice(index, 0, projected);
|
|
1090
|
+
keys.splice(index, 0, nextKey);
|
|
1091
|
+
}
|
|
1092
|
+
while (target.length > list.length) {
|
|
1093
|
+
target.pop();
|
|
1094
|
+
}
|
|
1095
|
+
while (keys.length > list.length) {
|
|
1096
|
+
keys.pop();
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Alias for {@link createArrayProjection}.
|
|
1102
|
+
*
|
|
1103
|
+
* @example
|
|
1104
|
+
* ```ts
|
|
1105
|
+
* const rows = arrayProjection(items, { map: (i) => i })
|
|
1106
|
+
* ```
|
|
1107
|
+
*/
|
|
1108
|
+
export const arrayProjection = createArrayProjection;
|
|
1109
|
+
/**
|
|
1110
|
+
* Creates a writable derived signal that resets when derivation inputs change.
|
|
1111
|
+
*
|
|
1112
|
+
* @param derive Function that computes the default value from reactive dependencies.
|
|
1113
|
+
* @returns Linked signal state with `value`, `set`, `reset`, and `isOverridden`.
|
|
1114
|
+
*
|
|
1115
|
+
* @example
|
|
1116
|
+
* ```ts
|
|
1117
|
+
* const selected = createLinkedSignal(() => items()[0]?.id ?? null)
|
|
1118
|
+
* selected.set("custom")
|
|
1119
|
+
* ```
|
|
1120
|
+
*/
|
|
1121
|
+
export function createLinkedSignal(derive) {
|
|
1122
|
+
const [value, setValue] = createSignal(untrack(derive));
|
|
1123
|
+
const [isOverridden, setIsOverridden] = createSignal(false);
|
|
1124
|
+
createEffect(() => {
|
|
1125
|
+
const nextDefault = derive();
|
|
1126
|
+
setValue(nextDefault);
|
|
1127
|
+
setIsOverridden(false);
|
|
1128
|
+
});
|
|
1129
|
+
const set = (next) => {
|
|
1130
|
+
setIsOverridden(true);
|
|
1131
|
+
return setValue(next);
|
|
1132
|
+
};
|
|
1133
|
+
const reset = () => {
|
|
1134
|
+
const nextDefault = untrack(derive);
|
|
1135
|
+
setIsOverridden(false);
|
|
1136
|
+
return setValue(nextDefault);
|
|
1137
|
+
};
|
|
1138
|
+
return {
|
|
1139
|
+
value,
|
|
1140
|
+
set,
|
|
1141
|
+
reset,
|
|
1142
|
+
isOverridden,
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Alias for {@link createLinkedSignal}.
|
|
1147
|
+
*
|
|
1148
|
+
* @example
|
|
1149
|
+
* ```ts
|
|
1150
|
+
* const selected = linkedSignal(() => "default")
|
|
1151
|
+
* ```
|
|
1152
|
+
*/
|
|
1153
|
+
export const linkedSignal = createLinkedSignal;
|
|
1154
|
+
/**
|
|
1155
|
+
* Creates a mutable projection with a stable reference.
|
|
1156
|
+
*
|
|
1157
|
+
* @param source Source accessor that drives projection updates.
|
|
1158
|
+
* @param initialize Initializes projection state from the first source value.
|
|
1159
|
+
* @param mutate Applies granular updates to the existing projection object.
|
|
1160
|
+
* @returns Stable mutable projection object.
|
|
1161
|
+
*
|
|
1162
|
+
* @example
|
|
1163
|
+
* ```ts
|
|
1164
|
+
* const projection = createProjection(source, (s) => ({ ...s }), (target, next) => {
|
|
1165
|
+
* Object.assign(target, next)
|
|
1166
|
+
* })
|
|
1167
|
+
* ```
|
|
1168
|
+
*/
|
|
1169
|
+
export function createProjection(source, initialize, mutate) {
|
|
1170
|
+
const firstSource = source();
|
|
1171
|
+
const projected = createMutable(initialize(firstSource));
|
|
1172
|
+
let previousSource = firstSource;
|
|
1173
|
+
createEffect(() => {
|
|
1174
|
+
const nextSource = source();
|
|
1175
|
+
untrack(() => {
|
|
1176
|
+
mutate(projected, nextSource, previousSource);
|
|
1177
|
+
});
|
|
1178
|
+
previousSource = nextSource;
|
|
1179
|
+
});
|
|
1180
|
+
return projected;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Alias for {@link createProjection}.
|
|
1184
|
+
*
|
|
1185
|
+
* @example
|
|
1186
|
+
* ```ts
|
|
1187
|
+
* const state = projection(source, (s) => ({ ...s }), (t, s) => Object.assign(t, s))
|
|
1188
|
+
* ```
|
|
1189
|
+
*/
|
|
1190
|
+
export const projection = createProjection;
|
|
1191
|
+
/**
|
|
1192
|
+
* Re-export of `React.Suspense` for API consistency.
|
|
1193
|
+
*
|
|
1194
|
+
* @example
|
|
1195
|
+
* ```tsx
|
|
1196
|
+
* <Suspense fallback={<p>Loading...</p>}><View /></Suspense>
|
|
1197
|
+
* ```
|
|
1198
|
+
*/
|
|
1199
|
+
export const Suspense = React.Suspense;
|
|
1200
|
+
/**
|
|
1201
|
+
* Wrapper around `React.lazy` that also accepts direct component loaders.
|
|
1202
|
+
*
|
|
1203
|
+
* @param loader Async loader returning either a module with default export or a component.
|
|
1204
|
+
* @returns Lazy React component suitable for Suspense boundaries.
|
|
1205
|
+
*
|
|
1206
|
+
* @example
|
|
1207
|
+
* ```ts
|
|
1208
|
+
* const Settings = lazy(() => import("./Settings"))
|
|
1209
|
+
* ```
|
|
1210
|
+
*/
|
|
1211
|
+
export function lazy(loader) {
|
|
1212
|
+
return React.lazy(async () => {
|
|
1213
|
+
const loaded = await loader();
|
|
1214
|
+
if (typeof loaded === "function") {
|
|
1215
|
+
return { default: loaded };
|
|
1216
|
+
}
|
|
1217
|
+
return loaded;
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
class ComponentInstance {
|
|
1221
|
+
constructor(initialProps, setup, forceUpdate) {
|
|
1222
|
+
this.forceUpdate = forceUpdate;
|
|
1223
|
+
this.scope = new Scope();
|
|
1224
|
+
this.disposed = false;
|
|
1225
|
+
this.suppressRenderInvalidation = false;
|
|
1226
|
+
this.propsSource = new SignalSource(this.scope.store, initialProps);
|
|
1227
|
+
this.renderTracker = new DependencyTracker(() => {
|
|
1228
|
+
if (!this.suppressRenderInvalidation) {
|
|
1229
|
+
this.forceUpdate();
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
this.scope.register(this.renderTracker);
|
|
1233
|
+
const result = withScope(this.scope, () => setup(() => this.propsSource.get()));
|
|
1234
|
+
this.render =
|
|
1235
|
+
typeof result === "function"
|
|
1236
|
+
? result
|
|
1237
|
+
: () => result;
|
|
1238
|
+
}
|
|
1239
|
+
updateProps(nextProps) {
|
|
1240
|
+
this.suppressRenderInvalidation = true;
|
|
1241
|
+
try {
|
|
1242
|
+
this.propsSource.set(nextProps);
|
|
1243
|
+
}
|
|
1244
|
+
finally {
|
|
1245
|
+
this.suppressRenderInvalidation = false;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
renderNode() {
|
|
1249
|
+
return withScope(this.scope, () => this.renderTracker.collect(this.render));
|
|
1250
|
+
}
|
|
1251
|
+
startLayoutEffects() {
|
|
1252
|
+
this.scope.startLayoutEffects();
|
|
1253
|
+
}
|
|
1254
|
+
startEffects() {
|
|
1255
|
+
this.scope.startEffects();
|
|
1256
|
+
}
|
|
1257
|
+
dispose() {
|
|
1258
|
+
if (this.disposed) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
this.disposed = true;
|
|
1262
|
+
this.scope.dispose();
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Wraps a setup function so Solid-style primitives can be used without custom hooks.
|
|
1267
|
+
*
|
|
1268
|
+
* Supports both setup styles:
|
|
1269
|
+
* - no-props: `component(() => () => <div />)`
|
|
1270
|
+
* - props accessor: `component<{ id: string }>((props) => () => <div>{props().id}</div>)`
|
|
1271
|
+
*/
|
|
1272
|
+
export function component(setup, options = {}) {
|
|
1273
|
+
const normalizedSetup = setup.length === 0
|
|
1274
|
+
? () => setup()
|
|
1275
|
+
: setup;
|
|
1276
|
+
const Wrapped = (props) => {
|
|
1277
|
+
const [, setTick] = React.useState(0);
|
|
1278
|
+
const forceUpdate = React.useCallback(() => {
|
|
1279
|
+
setTick((tick) => tick + 1);
|
|
1280
|
+
}, []);
|
|
1281
|
+
const instanceRef = React.useRef(null);
|
|
1282
|
+
if (!instanceRef.current) {
|
|
1283
|
+
instanceRef.current = new ComponentInstance(props, normalizedSetup, forceUpdate);
|
|
1284
|
+
}
|
|
1285
|
+
const instance = instanceRef.current;
|
|
1286
|
+
instance.updateProps(props);
|
|
1287
|
+
React.useLayoutEffect(() => {
|
|
1288
|
+
instance.startLayoutEffects();
|
|
1289
|
+
return () => {
|
|
1290
|
+
instance.dispose();
|
|
1291
|
+
};
|
|
1292
|
+
}, [instance]);
|
|
1293
|
+
React.useEffect(() => {
|
|
1294
|
+
instance.startEffects();
|
|
1295
|
+
}, [instance]);
|
|
1296
|
+
return _jsx(_Fragment, { children: instance.renderNode() });
|
|
1297
|
+
};
|
|
1298
|
+
const name = options.displayName ?? setup.name ?? "SolidLikeComponent";
|
|
1299
|
+
Wrapped.displayName = name;
|
|
1300
|
+
if (!options.memo) {
|
|
1301
|
+
return Wrapped;
|
|
1302
|
+
}
|
|
1303
|
+
const memoized = typeof options.memo === "function"
|
|
1304
|
+
? React.memo(Wrapped, options.memo)
|
|
1305
|
+
: React.memo(Wrapped);
|
|
1306
|
+
memoized.displayName = name;
|
|
1307
|
+
return memoized;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Alias for {@link component}.
|
|
1311
|
+
*
|
|
1312
|
+
* @example
|
|
1313
|
+
* ```ts
|
|
1314
|
+
* const View = defineComponent(() => <p>Hello</p>)
|
|
1315
|
+
* ```
|
|
1316
|
+
*/
|
|
1317
|
+
export const defineComponent = component;
|
|
1318
|
+
function readMaybeAccessor(value) {
|
|
1319
|
+
return resolveMaybeAccessor(value);
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Conditionally renders content when `when` is truthy.
|
|
1323
|
+
*
|
|
1324
|
+
* @param props Show control-flow props.
|
|
1325
|
+
* @returns Matching branch or fallback.
|
|
1326
|
+
*
|
|
1327
|
+
* @example
|
|
1328
|
+
* ```tsx
|
|
1329
|
+
* <Show when={ready()} fallback={<p>Loading</p>}><p>Ready</p></Show>
|
|
1330
|
+
* ```
|
|
1331
|
+
*/
|
|
1332
|
+
export function Show(props) {
|
|
1333
|
+
const value = readMaybeAccessor(props.when);
|
|
1334
|
+
if (!value) {
|
|
1335
|
+
return props.fallback ?? null;
|
|
1336
|
+
}
|
|
1337
|
+
if (typeof props.children === "function") {
|
|
1338
|
+
return props.children(value);
|
|
1339
|
+
}
|
|
1340
|
+
return props.children;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Renders each item in a list.
|
|
1344
|
+
*
|
|
1345
|
+
* @param props For control-flow props.
|
|
1346
|
+
* @returns Rendered list or fallback.
|
|
1347
|
+
*
|
|
1348
|
+
* @example
|
|
1349
|
+
* ```tsx
|
|
1350
|
+
* <For each={todos()} fallback={<p>Empty</p>}>{(todo) => <p>{todo.title}</p>}</For>
|
|
1351
|
+
* ```
|
|
1352
|
+
*/
|
|
1353
|
+
export function For(props) {
|
|
1354
|
+
const list = readMaybeAccessor(props.each) ?? [];
|
|
1355
|
+
if (list.length === 0) {
|
|
1356
|
+
return props.fallback ?? null;
|
|
1357
|
+
}
|
|
1358
|
+
return (_jsx(_Fragment, { children: list.map((item, index) => (_jsx(React.Fragment, { children: props.children(item, () => index) }, index))) }));
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Renders a list where each child receives an item accessor.
|
|
1362
|
+
*
|
|
1363
|
+
* @param props Index control-flow props.
|
|
1364
|
+
* @returns Rendered list or fallback.
|
|
1365
|
+
*
|
|
1366
|
+
* @example
|
|
1367
|
+
* ```tsx
|
|
1368
|
+
* <Index each={rows()}>{(row) => <Row data={row()} />}</Index>
|
|
1369
|
+
* ```
|
|
1370
|
+
*/
|
|
1371
|
+
export function Index(props) {
|
|
1372
|
+
const list = readMaybeAccessor(props.each) ?? [];
|
|
1373
|
+
if (list.length === 0) {
|
|
1374
|
+
return props.fallback ?? null;
|
|
1375
|
+
}
|
|
1376
|
+
return (_jsx(_Fragment, { children: list.map((_, index) => (_jsx(React.Fragment, { children: props.children(() => list[index], () => index) }, index))) }));
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Switch branch marker consumed by {@link Switch}.
|
|
1380
|
+
*
|
|
1381
|
+
* @param _props Match branch props consumed by `Switch`.
|
|
1382
|
+
* @returns `null` when rendered standalone.
|
|
1383
|
+
*
|
|
1384
|
+
* @example
|
|
1385
|
+
* ```tsx
|
|
1386
|
+
* <Switch><Match when={ok()}>OK</Match></Switch>
|
|
1387
|
+
* ```
|
|
1388
|
+
*/
|
|
1389
|
+
export function Match(_props) {
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Type guard for accessors.
|
|
1394
|
+
*
|
|
1395
|
+
* @param value Value to check.
|
|
1396
|
+
* @returns `true` when value is an accessor function.
|
|
1397
|
+
*
|
|
1398
|
+
* @example
|
|
1399
|
+
* ```ts
|
|
1400
|
+
* if (isAccessor(value)) {
|
|
1401
|
+
* console.log(value())
|
|
1402
|
+
* }
|
|
1403
|
+
* ```
|
|
1404
|
+
*/
|
|
1405
|
+
export function isAccessor(value) {
|
|
1406
|
+
return typeof value === "function";
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Resolves plain values or accessors into a value.
|
|
1410
|
+
*
|
|
1411
|
+
* @param value Plain value or accessor.
|
|
1412
|
+
* @returns Resolved value.
|
|
1413
|
+
*
|
|
1414
|
+
* @example
|
|
1415
|
+
* ```ts
|
|
1416
|
+
* const enabled = resolveMaybeAccessor(props.enabled)
|
|
1417
|
+
* ```
|
|
1418
|
+
*/
|
|
1419
|
+
export function resolveMaybeAccessor(value) {
|
|
1420
|
+
return isAccessor(value) ? value() : value;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Alias for {@link resolveMaybeAccessor}.
|
|
1424
|
+
*/
|
|
1425
|
+
export const toValue = resolveMaybeAccessor;
|
|
1426
|
+
/**
|
|
1427
|
+
* Creates a keyed selector helper for efficient equality checks.
|
|
1428
|
+
*
|
|
1429
|
+
* @param source Source accessor containing the selected value.
|
|
1430
|
+
* @param equals Optional comparison function. Defaults to `Object.is`.
|
|
1431
|
+
* @returns Function that compares keys to the current source value.
|
|
1432
|
+
*
|
|
1433
|
+
* @example
|
|
1434
|
+
* ```ts
|
|
1435
|
+
* const isSelected = createSelector(selectedId)
|
|
1436
|
+
* const active = isSelected(row.id)
|
|
1437
|
+
* ```
|
|
1438
|
+
*/
|
|
1439
|
+
export function createSelector(source, equals = Object.is) {
|
|
1440
|
+
return (key) => equals(source(), key);
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Renders the first truthy {@link Match}, else `fallback`.
|
|
1444
|
+
*
|
|
1445
|
+
* @param props Switch control-flow props.
|
|
1446
|
+
* @returns First matched branch or fallback.
|
|
1447
|
+
*
|
|
1448
|
+
* @example
|
|
1449
|
+
* ```tsx
|
|
1450
|
+
* <Switch fallback={<p>idle</p>}><Match when={loading()}>loading</Match></Switch>
|
|
1451
|
+
* ```
|
|
1452
|
+
*/
|
|
1453
|
+
export function Switch(props) {
|
|
1454
|
+
const children = React.Children.toArray(props.children);
|
|
1455
|
+
for (const child of children) {
|
|
1456
|
+
if (!React.isValidElement(child) || child.type !== Match) {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
const matchProps = child.props;
|
|
1460
|
+
const value = readMaybeAccessor(matchProps.when);
|
|
1461
|
+
if (!value) {
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (typeof matchProps.children === "function") {
|
|
1465
|
+
return matchProps.children(value);
|
|
1466
|
+
}
|
|
1467
|
+
return matchProps.children;
|
|
1468
|
+
}
|
|
1469
|
+
return props.fallback ?? null;
|
|
1470
|
+
}
|