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/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
+ }