forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
1import {
2 endBatch,
3 setActiveSub,
4 signal as alienSignal,
5 startBatch,
6} from "alien-signals";
7
8export * from "alien-signals";
9
10/**
11 * @import {Signal, SignalReader, SignalWriter} from "./signal.d.ts"
12 */
13
14/**
15 * @param {function(): void} fn
16 *
17 * @example Defers effect execution until batch completes
18 * ```js
19 * import { signal, effect, batch } from "~/common/signal.js";
20 *
21 * const a = signal(0);
22 * const b = signal(0);
23 * const values = [0]; values.length = 0; // typed as number[]
24 *
25 * effect(() => { values.push(a.get() + b.get()); });
26 *
27 * const before = [...values]; // [0]
28 * batch(() => { a.set(1); b.set(2); });
29 *
30 * if (before.join(",") !== "0") throw new Error("expected [0] before batch");
31 * if (values.join(",") !== "0,3") throw new Error("expected exactly one update after batch, got " + values.join(","));
32 * ```
33 */
34export const batch = (fn) => {
35 startBatch();
36 try {
37 fn();
38 } finally {
39 endBatch();
40 }
41};
42
43/**
44 * @template T
45 * @param {T} initialValue
46 * @param {{ compare?: (a: T, b: T) => boolean }} [options]
47 * @returns {Signal<T>}
48 *
49 * @example get/set and value getter/setter return and update the value
50 * ```js
51 * import { signal } from "~/common/signal.js";
52 *
53 * const num = signal(42);
54 * if (num.get() !== 42) throw new Error("get should return initial value");
55 * if (num.value !== 42) throw new Error("value getter should return initial value");
56 *
57 * num.set(99);
58 * if (num.get() !== 99) throw new Error("get should return updated value");
59 *
60 * const str = signal("a");
61 * str.value = "b";
62 * if (str.value !== "b") throw new Error("value setter should update value");
63 * ```
64 *
65 * @example compare option skips update when values are equal by custom comparator
66 * ```js
67 * import { signal, effect } from "~/common/signal.js";
68 *
69 * let runCount = 0;
70 * const s = signal({ x: 1 }, { compare: (a, b) => a.x === b.x });
71 *
72 * effect(() => { s.get(); runCount++; });
73 *
74 * const before = runCount;
75 * s.set({ x: 1 }); // same by compare
76 * if (runCount !== before) throw new Error("effect should not re-run when value is equal by compare");
77 *
78 * s.set({ x: 2 }); // different by compare
79 * if (runCount !== before + 1) throw new Error("effect should re-run when value differs by compare");
80 * ```
81 */
82export function signal(initialValue, options) {
83 const s = alienSignal(initialValue);
84 if (options?.compare) {
85 const compare = options.compare;
86
87 return _signal({
88 get: () => s(),
89 set: (b) => {
90 const a = untracked(() => s());
91 if (!compare(a, b)) s(b);
92 },
93 });
94 }
95
96 return _signal({
97 get: () => s(),
98 set: (v) => s(v),
99 });
100}
101
102/**
103 * @template T
104 * @param {function(): T} fn
105 * @returns {T}
106 *
107 * @example Reads a signal without tracking it as a dependency
108 * ```js
109 * import { signal, effect, untracked } from "~/common/signal.js";
110 *
111 * const a = signal(1);
112 * const b = signal(10);
113 * let runCount = 0;
114 *
115 * effect(() => {
116 * a.get(); // tracked
117 * untracked(() => b.get()); // not tracked
118 * runCount++;
119 * });
120 *
121 * const before = runCount; // 1
122 * b.set(20); // should NOT re-run effect
123 * if (runCount !== before) throw new Error("untracked read should not trigger re-run");
124 *
125 * a.set(2); // SHOULD re-run effect
126 * if (runCount !== before + 1) throw new Error("tracked read should trigger re-run");
127 *
128 * if (untracked(() => a.get()) !== 2) throw new Error("untracked should return the value");
129 * ```
130 */
131export const untracked = (fn) => {
132 const sub = setActiveSub(void 0);
133 try {
134 return fn();
135 } finally {
136 setActiveSub(sub);
137 }
138};
139
140/**
141 * @template T
142 * @param {function(): Promise<T>} fn
143 * @returns {Promise<T>}
144 *
145 * @example Returns the resolved value from the async callback
146 * ```js
147 * import { untrackedAsync } from "~/common/signal.js";
148 *
149 * const result = await untrackedAsync(async () => 99);
150 * if (result !== 99) throw new Error("untrackedAsync should return resolved value");
151 * ```
152 */
153export const untrackedAsync = async (fn) => {
154 const sub = setActiveSub(void 0);
155 try {
156 return await fn();
157 } finally {
158 setActiveSub(sub);
159 }
160};
161
162/**
163 * @template T
164 * @param {{ get: SignalReader<T>; set: SignalWriter<T> }} _
165 * @returns {Signal<T>}
166 */
167function _signal({ get, set }) {
168 return {
169 get,
170 set,
171
172 get value() {
173 return get();
174 },
175
176 set value(v) {
177 set(v);
178 },
179 };
180}