@granularjs/core 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 +576 -0
- package/dist/granular.min.js +2 -0
- package/dist/granular.min.js.map +7 -0
- package/package.json +54 -0
- package/src/core/bootstrap.js +63 -0
- package/src/core/collections/observable-array.js +204 -0
- package/src/core/component/function-component.js +82 -0
- package/src/core/context.js +172 -0
- package/src/core/dom/dom.js +25 -0
- package/src/core/dom/element.js +725 -0
- package/src/core/dom/error-boundary.js +111 -0
- package/src/core/dom/input-format.js +82 -0
- package/src/core/dom/list.js +185 -0
- package/src/core/dom/portal.js +57 -0
- package/src/core/dom/tags.js +182 -0
- package/src/core/dom/virtual-list.js +242 -0
- package/src/core/dom/when.js +138 -0
- package/src/core/events/event-hub.js +97 -0
- package/src/core/forms/form.js +127 -0
- package/src/core/internal/symbols.js +5 -0
- package/src/core/network/websocket.js +165 -0
- package/src/core/query/query-client.js +529 -0
- package/src/core/reactivity/after-flush.js +20 -0
- package/src/core/reactivity/computed.js +51 -0
- package/src/core/reactivity/concat.js +89 -0
- package/src/core/reactivity/dirty-host.js +162 -0
- package/src/core/reactivity/observe.js +421 -0
- package/src/core/reactivity/persist.js +180 -0
- package/src/core/reactivity/resolve.js +8 -0
- package/src/core/reactivity/signal.js +97 -0
- package/src/core/reactivity/state.js +294 -0
- package/src/core/renderable/render-string.js +51 -0
- package/src/core/renderable/renderable.js +21 -0
- package/src/core/renderable/renderer.js +66 -0
- package/src/core/router/router.js +865 -0
- package/src/core/runtime.js +28 -0
- package/src/index.js +42 -0
- package/types/core/bootstrap.d.ts +11 -0
- package/types/core/collections/observable-array.d.ts +25 -0
- package/types/core/component/function-component.d.ts +14 -0
- package/types/core/context.d.ts +29 -0
- package/types/core/dom/dom.d.ts +13 -0
- package/types/core/dom/element.d.ts +10 -0
- package/types/core/dom/error-boundary.d.ts +8 -0
- package/types/core/dom/input-format.d.ts +6 -0
- package/types/core/dom/list.d.ts +8 -0
- package/types/core/dom/portal.d.ts +8 -0
- package/types/core/dom/tags.d.ts +114 -0
- package/types/core/dom/virtual-list.d.ts +8 -0
- package/types/core/dom/when.d.ts +13 -0
- package/types/core/events/event-hub.d.ts +48 -0
- package/types/core/forms/form.d.ts +9 -0
- package/types/core/internal/symbols.d.ts +4 -0
- package/types/core/network/websocket.d.ts +18 -0
- package/types/core/query/query-client.d.ts +73 -0
- package/types/core/reactivity/after-flush.d.ts +4 -0
- package/types/core/reactivity/computed.d.ts +1 -0
- package/types/core/reactivity/concat.d.ts +1 -0
- package/types/core/reactivity/dirty-host.d.ts +42 -0
- package/types/core/reactivity/observe.d.ts +10 -0
- package/types/core/reactivity/persist.d.ts +1 -0
- package/types/core/reactivity/resolve.d.ts +1 -0
- package/types/core/reactivity/signal.d.ts +11 -0
- package/types/core/reactivity/state.d.ts +14 -0
- package/types/core/renderable/render-string.d.ts +2 -0
- package/types/core/renderable/renderable.d.ts +15 -0
- package/types/core/renderable/renderer.d.ts +38 -0
- package/types/core/router/router.d.ts +57 -0
- package/types/core/runtime.d.ts +26 -0
- package/types/index.d.ts +2 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { isSignal, readSignal, subscribeSignal, setSignal, signal } from './signal.js';
|
|
2
|
+
import { isObservableArray } from '../collections/observable-array.js';
|
|
3
|
+
import { createStateFromAdapter, isState, isStatePath, readState, readStateFromRoot, subscribeState, setStateValue } from './state.js';
|
|
4
|
+
|
|
5
|
+
function freezeValue(value) {
|
|
6
|
+
if (!value || typeof value !== 'object') return value;
|
|
7
|
+
try {
|
|
8
|
+
return Object.freeze(value);
|
|
9
|
+
} catch {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeTargets(targets) {
|
|
15
|
+
if (targets.length === 1 && Array.isArray(targets[0]) && !isObservableArray(targets[0])) return targets[0];
|
|
16
|
+
return targets;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readTargetValue(target) {
|
|
20
|
+
if (isState(target) || isStatePath(target)) return readState(target);
|
|
21
|
+
if (isSignal(target)) return readSignal(target);
|
|
22
|
+
if (isObservableArray(target)) return target;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function subscribeAfterTarget(target, fn) {
|
|
27
|
+
if (isState(target) || isStatePath(target)) {
|
|
28
|
+
return subscribeState(target, (next, prev) => fn(freezeValue(next), freezeValue(prev), null));
|
|
29
|
+
}
|
|
30
|
+
if (isSignal(target)) {
|
|
31
|
+
return subscribeSignal(target, (next, prev) => fn(freezeValue(next), freezeValue(prev), null));
|
|
32
|
+
}
|
|
33
|
+
if (isObservableArray(target)) {
|
|
34
|
+
return target.after().any((patch, ctx) => {
|
|
35
|
+
const prevLen = ctx?.prevLength ?? target.length;
|
|
36
|
+
const nextLen = ctx?.nextLength ?? target.length;
|
|
37
|
+
const { next, prev } = makeArraySnapshots(target, patch, ctx, 'after');
|
|
38
|
+
fn(next, prev, { patch, prevLength: prevLen, nextLength: nextLen, array: target });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
throw new Error('after(x).change: unsupported target');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function subscribeBeforeTarget(target, fn) {
|
|
45
|
+
if (isState(target) || isStatePath(target)) {
|
|
46
|
+
return target.before?.((prevRoot, nextRoot) => {
|
|
47
|
+
const prev = readStateFromRoot(target, prevRoot);
|
|
48
|
+
const next = nextRoot !== undefined ? readStateFromRoot(target, nextRoot) : prev;
|
|
49
|
+
if (next === prev) return true;
|
|
50
|
+
const res = fn(freezeValue(next), freezeValue(prev), null);
|
|
51
|
+
return res !== false;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (isSignal(target)) {
|
|
55
|
+
return target.before((prev, next) => fn(freezeValue(next), freezeValue(prev), null));
|
|
56
|
+
}
|
|
57
|
+
if (isObservableArray(target)) {
|
|
58
|
+
return target.before().any((patch, ctx) => {
|
|
59
|
+
const prevLen = target.length;
|
|
60
|
+
const nextLen = ctx?.nextLength ?? prevLen;
|
|
61
|
+
const { next, prev } = makeArraySnapshots(target, patch, ctx, 'before');
|
|
62
|
+
const res = fn(next, prev, { patch, prevLength: prevLen, nextLength: nextLen, array: target });
|
|
63
|
+
return res !== false;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
throw new Error('before(x).change: unsupported target');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createComputedState() {
|
|
70
|
+
const rootSignal = signal(undefined);
|
|
71
|
+
const adapter = {
|
|
72
|
+
kind: 'computed',
|
|
73
|
+
get: () => readSignal(rootSignal),
|
|
74
|
+
set: () => {
|
|
75
|
+
throw new Error('Computed values are read-only.');
|
|
76
|
+
},
|
|
77
|
+
subscribe: (fn) => subscribeSignal(rootSignal, fn),
|
|
78
|
+
before: undefined,
|
|
79
|
+
};
|
|
80
|
+
const proxy = createStateFromAdapter(adapter);
|
|
81
|
+
const setValue = (next) => setSignal(rootSignal, next, true);
|
|
82
|
+
return { value: proxy, setValue };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function applyPatch(baseArray, patch, ctx) {
|
|
86
|
+
if (!Array.isArray(baseArray)) return [];
|
|
87
|
+
const out = baseArray.slice();
|
|
88
|
+
if (!patch || !patch.type) return out;
|
|
89
|
+
if (patch.type === 'insert') {
|
|
90
|
+
out.splice(patch.index, 0, ...(patch.items || []));
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
if (patch.type === 'remove') {
|
|
94
|
+
out.splice(patch.index, patch.count || 0);
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
if (patch.type === 'set') {
|
|
98
|
+
out[patch.index] = patch.value;
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
if (patch.type === 'reset') {
|
|
102
|
+
return Array.isArray(patch.items) ? patch.items.slice() : [];
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function applyInversePatch(baseArray, patch, ctx) {
|
|
108
|
+
if (!Array.isArray(baseArray)) return [];
|
|
109
|
+
const out = baseArray.slice();
|
|
110
|
+
if (!patch || !patch.type) return out;
|
|
111
|
+
if (patch.type === 'insert') {
|
|
112
|
+
out.splice(patch.index, (patch.items || []).length);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
if (patch.type === 'remove') {
|
|
116
|
+
const items = patch.items || [];
|
|
117
|
+
out.splice(patch.index, 0, ...items);
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
if (patch.type === 'set') {
|
|
121
|
+
out[patch.index] = patch.prev;
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
if (patch.type === 'reset') {
|
|
125
|
+
return Array.isArray(patch.prevItems) ? patch.prevItems.slice() : [];
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function makeArraySnapshots(target, patch, ctx, phase) {
|
|
131
|
+
const cached = { prev: null, next: null };
|
|
132
|
+
const prev = () => {
|
|
133
|
+
if (cached.prev) return cached.prev;
|
|
134
|
+
cached.prev = phase === 'after' ? applyInversePatch(target, patch, ctx) : target.slice();
|
|
135
|
+
return cached.prev;
|
|
136
|
+
};
|
|
137
|
+
const next = () => {
|
|
138
|
+
if (cached.next) return cached.next;
|
|
139
|
+
cached.next = phase === 'after' ? target.slice() : applyPatch(target, patch, ctx);
|
|
140
|
+
return cached.next;
|
|
141
|
+
};
|
|
142
|
+
return { prev, next };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function valueForTarget(target) {
|
|
146
|
+
if (isObservableArray(target)) return () => target.slice();
|
|
147
|
+
return readTargetValue(target);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function after(...targets) {
|
|
151
|
+
const list = normalizeTargets(targets);
|
|
152
|
+
if (!list.length) {
|
|
153
|
+
throw new Error('after(...targets): at least one target is required');
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
change(fn) {
|
|
157
|
+
const unsubs = list.map((target) =>
|
|
158
|
+
subscribeAfterTarget(target, (next, prev, ctx) => fn(next, prev, ctx))
|
|
159
|
+
);
|
|
160
|
+
return () => {
|
|
161
|
+
for (const unsub of unsubs) {
|
|
162
|
+
if (typeof unsub === 'function') unsub();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
compute(fn, options = {}) {
|
|
167
|
+
const { value, setValue } = createComputedState();
|
|
168
|
+
let runId = 0;
|
|
169
|
+
let lastHash = undefined;
|
|
170
|
+
let lastValue = undefined;
|
|
171
|
+
let scheduled = null;
|
|
172
|
+
let disposed = false;
|
|
173
|
+
let lastValues = list.map(valueForTarget);
|
|
174
|
+
const equals = typeof options.equals === 'function' ? options.equals : Object.is;
|
|
175
|
+
const handleError = (err) => {
|
|
176
|
+
if (typeof options.onError === 'function') {
|
|
177
|
+
options.onError(err);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
181
|
+
console.error(err);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const computeNow = (nextValues, prevValues, ctxs) => {
|
|
185
|
+
if (disposed) return;
|
|
186
|
+
const current = ++runId;
|
|
187
|
+
const args = nextValues;
|
|
188
|
+
if (typeof options.hash === 'function') {
|
|
189
|
+
let nextHash = undefined;
|
|
190
|
+
try {
|
|
191
|
+
nextHash = list.length === 1 ? options.hash(args[0], prevValues[0], ctxs[0]) : options.hash(args, prevValues, ctxs);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
handleError(err);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (nextHash === lastHash) return;
|
|
197
|
+
lastHash = nextHash;
|
|
198
|
+
}
|
|
199
|
+
let result;
|
|
200
|
+
try {
|
|
201
|
+
result = list.length === 1 ? fn(args[0], prevValues[0], ctxs[0]) : fn(args, prevValues, ctxs);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
handleError(err);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (result && typeof result.then === 'function') {
|
|
207
|
+
result
|
|
208
|
+
.then((next) => {
|
|
209
|
+
if (current !== runId || disposed) return;
|
|
210
|
+
if (equals(lastValue, next)) return;
|
|
211
|
+
lastValue = next;
|
|
212
|
+
setValue(next);
|
|
213
|
+
})
|
|
214
|
+
.catch((err) => handleError(err));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (equals(lastValue, result)) return;
|
|
218
|
+
lastValue = result;
|
|
219
|
+
setValue(result);
|
|
220
|
+
};
|
|
221
|
+
const scheduleRun = (nextValues, prevValues, ctxs) => {
|
|
222
|
+
if (disposed) return;
|
|
223
|
+
const delay = Math.max(0, options.debounce ?? 0);
|
|
224
|
+
if (!delay) {
|
|
225
|
+
computeNow(nextValues, prevValues, ctxs);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (scheduled) clearTimeout(scheduled);
|
|
229
|
+
scheduled = setTimeout(() => {
|
|
230
|
+
scheduled = null;
|
|
231
|
+
computeNow(nextValues, prevValues, ctxs);
|
|
232
|
+
}, delay);
|
|
233
|
+
};
|
|
234
|
+
scheduleRun(lastValues, lastValues, list.map(() => null));
|
|
235
|
+
const unsubs = list.map((target, index) =>
|
|
236
|
+
subscribeAfterTarget(target, (next, prev, ctx) => {
|
|
237
|
+
const nextValues = list.map(valueForTarget);
|
|
238
|
+
nextValues[index] = next;
|
|
239
|
+
const prevValues = lastValues.slice();
|
|
240
|
+
prevValues[index] = prev;
|
|
241
|
+
lastValues = nextValues;
|
|
242
|
+
const ctxs = list.map(() => null);
|
|
243
|
+
ctxs[index] = ctx;
|
|
244
|
+
scheduleRun(nextValues, prevValues, ctxs);
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
Object.defineProperty(value, 'dispose', {
|
|
248
|
+
value: () => {
|
|
249
|
+
disposed = true;
|
|
250
|
+
runId++;
|
|
251
|
+
if (scheduled) clearTimeout(scheduled);
|
|
252
|
+
for (const unsub of unsubs) {
|
|
253
|
+
if (typeof unsub === 'function') unsub();
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
enumerable: false,
|
|
257
|
+
});
|
|
258
|
+
return value;
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function before(...targets) {
|
|
264
|
+
const list = normalizeTargets(targets);
|
|
265
|
+
if (!list.length) {
|
|
266
|
+
throw new Error('before(...targets): at least one target is required');
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
change(fn) {
|
|
270
|
+
const unsubs = list.map((target) =>
|
|
271
|
+
subscribeBeforeTarget(target, (next, prev, ctx) => fn(next, prev, ctx))
|
|
272
|
+
);
|
|
273
|
+
return () => {
|
|
274
|
+
for (const unsub of unsubs) {
|
|
275
|
+
if (typeof unsub === 'function') unsub();
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
compute(fn, options = {}) {
|
|
280
|
+
const { value, setValue } = createComputedState();
|
|
281
|
+
let runId = 0;
|
|
282
|
+
let lastHash = undefined;
|
|
283
|
+
let lastValue = undefined;
|
|
284
|
+
let scheduled = null;
|
|
285
|
+
let disposed = false;
|
|
286
|
+
const equals = typeof options.equals === 'function' ? options.equals : Object.is;
|
|
287
|
+
const handleError = (err) => {
|
|
288
|
+
if (typeof options.onError === 'function') {
|
|
289
|
+
options.onError(err);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
293
|
+
console.error(err);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const computeNow = (nextValues, prevValues, ctxs) => {
|
|
297
|
+
if (disposed) return;
|
|
298
|
+
const current = ++runId;
|
|
299
|
+
if (typeof options.hash === 'function') {
|
|
300
|
+
let nextHash = undefined;
|
|
301
|
+
try {
|
|
302
|
+
nextHash =
|
|
303
|
+
list.length === 1
|
|
304
|
+
? options.hash(nextValues[0], prevValues[0], ctxs[0])
|
|
305
|
+
: options.hash(nextValues, prevValues, ctxs);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
handleError(err);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (nextHash === lastHash) return;
|
|
311
|
+
lastHash = nextHash;
|
|
312
|
+
}
|
|
313
|
+
let result;
|
|
314
|
+
try {
|
|
315
|
+
result =
|
|
316
|
+
list.length === 1
|
|
317
|
+
? fn(nextValues[0], prevValues[0], ctxs[0])
|
|
318
|
+
: fn(nextValues, prevValues, ctxs);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
handleError(err);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (result && typeof result.then === 'function') {
|
|
324
|
+
result
|
|
325
|
+
.then((next) => {
|
|
326
|
+
if (current !== runId || disposed) return;
|
|
327
|
+
if (equals(lastValue, next)) return;
|
|
328
|
+
lastValue = next;
|
|
329
|
+
setValue(next);
|
|
330
|
+
})
|
|
331
|
+
.catch((err) => handleError(err));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (equals(lastValue, result)) return;
|
|
335
|
+
lastValue = result;
|
|
336
|
+
setValue(result);
|
|
337
|
+
};
|
|
338
|
+
const scheduleRun = (nextValues, prevValues, ctxs) => {
|
|
339
|
+
if (disposed) return;
|
|
340
|
+
const delay = Math.max(0, options.debounce ?? 0);
|
|
341
|
+
if (!delay) {
|
|
342
|
+
computeNow(nextValues, prevValues, ctxs);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (scheduled) clearTimeout(scheduled);
|
|
346
|
+
scheduled = setTimeout(() => {
|
|
347
|
+
scheduled = null;
|
|
348
|
+
computeNow(nextValues, prevValues, ctxs);
|
|
349
|
+
}, delay);
|
|
350
|
+
};
|
|
351
|
+
const unsubs = list.map((target, index) =>
|
|
352
|
+
subscribeBeforeTarget(target, (next, prev, ctx) => {
|
|
353
|
+
const currentValues = list.map(valueForTarget);
|
|
354
|
+
const nextValues = currentValues.slice();
|
|
355
|
+
nextValues[index] = next;
|
|
356
|
+
const prevValues = currentValues.slice();
|
|
357
|
+
prevValues[index] = prev;
|
|
358
|
+
const ctxs = list.map(() => null);
|
|
359
|
+
ctxs[index] = ctx;
|
|
360
|
+
scheduleRun(nextValues, prevValues, ctxs);
|
|
361
|
+
})
|
|
362
|
+
);
|
|
363
|
+
Object.defineProperty(value, 'dispose', {
|
|
364
|
+
value: () => {
|
|
365
|
+
disposed = true;
|
|
366
|
+
runId++;
|
|
367
|
+
if (scheduled) clearTimeout(scheduled);
|
|
368
|
+
for (const unsub of unsubs) {
|
|
369
|
+
if (typeof unsub === 'function') unsub();
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
enumerable: false,
|
|
373
|
+
});
|
|
374
|
+
return value;
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function set(target, value) {
|
|
380
|
+
if (isState(target) || isStatePath(target)) {
|
|
381
|
+
setStateValue(target, value);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (isSignal(target)) {
|
|
385
|
+
setSignal(target, value);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (isObservableArray(target)) {
|
|
389
|
+
if (typeof target.reset !== 'function') {
|
|
390
|
+
throw new Error('set(array, value): observableArray must implement reset');
|
|
391
|
+
}
|
|
392
|
+
target.reset(value);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
throw new Error('set(target, value): unsupported target');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function resolveValue(value) {
|
|
399
|
+
return typeof value === 'function' ? value() : value;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function subscribe(target, selector, listener, equalityFn) {
|
|
403
|
+
if (typeof selector !== 'function') {
|
|
404
|
+
throw new Error('subscribe(target, selector, listener?): selector must be a function');
|
|
405
|
+
}
|
|
406
|
+
if (listener === undefined) {
|
|
407
|
+
return after(target).compute((next) => selector(resolveValue(next)));
|
|
408
|
+
}
|
|
409
|
+
if (typeof listener !== 'function') {
|
|
410
|
+
throw new Error('subscribe(target, selector, listener): listener must be a function');
|
|
411
|
+
}
|
|
412
|
+
const eq = typeof equalityFn === 'function' ? equalityFn : Object.is;
|
|
413
|
+
let prevSelected = selector(resolveValue(readTargetValue(target)));
|
|
414
|
+
return after(target).change((next) => {
|
|
415
|
+
const nextSelected = selector(resolveValue(next));
|
|
416
|
+
if (eq(prevSelected, nextSelected)) return;
|
|
417
|
+
const p = prevSelected;
|
|
418
|
+
prevSelected = nextSelected;
|
|
419
|
+
listener(nextSelected, p);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { isState, isStatePath, readState, setStateValue } from './state.js';
|
|
2
|
+
import { after } from './observe.js';
|
|
3
|
+
|
|
4
|
+
function isStoreLike(value) {
|
|
5
|
+
return (
|
|
6
|
+
!!value &&
|
|
7
|
+
typeof value === 'object' &&
|
|
8
|
+
typeof value.getState === 'function' &&
|
|
9
|
+
typeof value.setState === 'function' &&
|
|
10
|
+
typeof value.subscribe === 'function'
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isStateLike(value) {
|
|
15
|
+
return isState(value) || isStatePath(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getAtPath(obj, path) {
|
|
19
|
+
let cur = obj;
|
|
20
|
+
for (const key of path) {
|
|
21
|
+
if (!cur) return undefined;
|
|
22
|
+
cur = cur[key];
|
|
23
|
+
}
|
|
24
|
+
return cur;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setAtPath(obj, path, value) {
|
|
28
|
+
if (!path.length) return value;
|
|
29
|
+
const root = Array.isArray(obj) ? obj.slice() : { ...(obj || {}) };
|
|
30
|
+
let cur = root;
|
|
31
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
32
|
+
const key = path[i];
|
|
33
|
+
const next = cur[key];
|
|
34
|
+
const cloned = Array.isArray(next) ? next.slice() : { ...(next || {}) };
|
|
35
|
+
cur[key] = cloned;
|
|
36
|
+
cur = cloned;
|
|
37
|
+
}
|
|
38
|
+
cur[path[path.length - 1]] = value;
|
|
39
|
+
return root;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizePaths(paths) {
|
|
43
|
+
if (!paths || !paths.length) return null;
|
|
44
|
+
return paths.map((p) => String(p).split('.').map((s) => s.trim()).filter(Boolean));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function pickPaths(value, pathList) {
|
|
48
|
+
if (!pathList) return value;
|
|
49
|
+
let next = value;
|
|
50
|
+
for (const path of pathList) {
|
|
51
|
+
const v = getAtPath(value, path);
|
|
52
|
+
next = setAtPath(next, path, v);
|
|
53
|
+
}
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function defaultSerialize(value) {
|
|
58
|
+
return JSON.stringify(value, (_key, v) => {
|
|
59
|
+
if (typeof v === 'function') return undefined;
|
|
60
|
+
if (typeof v === 'symbol') return undefined;
|
|
61
|
+
return v;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function defaultDeserialize(text) {
|
|
66
|
+
return JSON.parse(text);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readSnapshot(target, pathList) {
|
|
70
|
+
if (isStateLike(target)) return pickPaths(readState(target), pathList);
|
|
71
|
+
if (isStoreLike(target)) return pickPaths(target.getState(), pathList);
|
|
72
|
+
throw new Error('persist(target): unsupported target');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function applySnapshot(target, snapshot) {
|
|
76
|
+
if (isStateLike(target)) {
|
|
77
|
+
setStateValue(target, snapshot);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (isStoreLike(target)) {
|
|
81
|
+
target.setState(snapshot, true);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
throw new Error('persist(target): unsupported target');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function subscribeChanges(target, fn) {
|
|
88
|
+
if (isStateLike(target)) return after(target).change(fn);
|
|
89
|
+
if (isStoreLike(target)) return target.subscribe(fn);
|
|
90
|
+
throw new Error('persist(target): unsupported target');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function safeStorage(storage) {
|
|
94
|
+
try {
|
|
95
|
+
if (!storage || typeof storage.getItem !== 'function') return null;
|
|
96
|
+
return storage;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function persist(target, options = {}) {
|
|
103
|
+
const key = options.key;
|
|
104
|
+
if (!key) throw new Error('persist(target): options.key is required');
|
|
105
|
+
|
|
106
|
+
const storage = safeStorage(options.storage ?? (typeof localStorage !== 'undefined' ? localStorage : null));
|
|
107
|
+
const pathList = normalizePaths(options.paths);
|
|
108
|
+
const serialize = options.serialize || defaultSerialize;
|
|
109
|
+
const deserialize = options.deserialize || defaultDeserialize;
|
|
110
|
+
const version = options.version ?? 1;
|
|
111
|
+
const migrate = options.migrate || null;
|
|
112
|
+
const reconcile = options.reconcile || null;
|
|
113
|
+
const throttleMs = Math.max(0, options.throttle ?? 0);
|
|
114
|
+
|
|
115
|
+
if (storage) {
|
|
116
|
+
const raw = storage.getItem(key);
|
|
117
|
+
if (raw != null) {
|
|
118
|
+
let payload = null;
|
|
119
|
+
try {
|
|
120
|
+
payload = deserialize(raw);
|
|
121
|
+
} catch {
|
|
122
|
+
payload = null;
|
|
123
|
+
}
|
|
124
|
+
if (payload != null) {
|
|
125
|
+
let data = payload;
|
|
126
|
+
let v = null;
|
|
127
|
+
if (payload && typeof payload === 'object' && 'data' in payload && 'v' in payload) {
|
|
128
|
+
data = payload.data;
|
|
129
|
+
v = payload.v;
|
|
130
|
+
}
|
|
131
|
+
if (v != null && v !== version && typeof migrate === 'function') {
|
|
132
|
+
data = migrate(data, v);
|
|
133
|
+
}
|
|
134
|
+
if (typeof reconcile === 'function') {
|
|
135
|
+
data = reconcile(data);
|
|
136
|
+
}
|
|
137
|
+
if (data !== undefined) {
|
|
138
|
+
applySnapshot(target, data);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let scheduled = false;
|
|
145
|
+
let lastTimer = null;
|
|
146
|
+
|
|
147
|
+
const write = () => {
|
|
148
|
+
scheduled = false;
|
|
149
|
+
const snapshot = readSnapshot(target, pathList);
|
|
150
|
+
const payload = { v: version, data: snapshot };
|
|
151
|
+
try {
|
|
152
|
+
storage?.setItem?.(key, serialize(payload));
|
|
153
|
+
} catch {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const scheduleWrite = () => {
|
|
159
|
+
if (!storage) return;
|
|
160
|
+
if (throttleMs <= 0) {
|
|
161
|
+
write();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (scheduled) return;
|
|
165
|
+
scheduled = true;
|
|
166
|
+
lastTimer = setTimeout(write, throttleMs);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const unsubscribe = subscribeChanges(target, scheduleWrite);
|
|
170
|
+
|
|
171
|
+
Object.defineProperty(target, 'persistDispose', {
|
|
172
|
+
value: () => {
|
|
173
|
+
if (lastTimer) clearTimeout(lastTimer);
|
|
174
|
+
if (typeof unsubscribe === 'function') unsubscribe();
|
|
175
|
+
},
|
|
176
|
+
enumerable: false,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return target;
|
|
180
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isSignal, readSignal } from './signal.js';
|
|
2
|
+
import { isState, isStatePath, readState } from './state.js';
|
|
3
|
+
|
|
4
|
+
export function resolve(value) {
|
|
5
|
+
if (isSignal(value)) return readSignal(value);
|
|
6
|
+
if (isState(value) || isStatePath(value)) return readState(value);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const SIGNAL = Symbol('zb.signal');
|
|
2
|
+
const SIGNAL_MAP = Symbol('zb.signal.map');
|
|
3
|
+
|
|
4
|
+
function isObject(value) {
|
|
5
|
+
return value !== null && typeof value === 'object';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function signal(initial) {
|
|
9
|
+
const state = {
|
|
10
|
+
[SIGNAL]: true,
|
|
11
|
+
value: initial,
|
|
12
|
+
subs: new Set(),
|
|
13
|
+
before: new Set(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const notify = (prev) => {
|
|
17
|
+
for (const fn of state.subs) fn(state.value, prev);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const api = {
|
|
21
|
+
get() {
|
|
22
|
+
return state.value;
|
|
23
|
+
},
|
|
24
|
+
set(next, force = false) {
|
|
25
|
+
const prev = state.value;
|
|
26
|
+
if (!force && prev === next) return true;
|
|
27
|
+
for (const fn of state.before) {
|
|
28
|
+
const res = fn(prev, next);
|
|
29
|
+
if (res === false) return false;
|
|
30
|
+
}
|
|
31
|
+
state.value = next;
|
|
32
|
+
notify(prev);
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
subscribe(fn) {
|
|
36
|
+
state.subs.add(fn);
|
|
37
|
+
return () => state.subs.delete(fn);
|
|
38
|
+
},
|
|
39
|
+
before(fn) {
|
|
40
|
+
state.before.add(fn);
|
|
41
|
+
return () => state.before.delete(fn);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const proxy = new Proxy(api, {
|
|
46
|
+
get(_target, prop) {
|
|
47
|
+
if (prop === SIGNAL) return true;
|
|
48
|
+
if (prop === 'value') return state.value;
|
|
49
|
+
if (prop === 'get') return api.get;
|
|
50
|
+
if (prop === 'set') return api.set;
|
|
51
|
+
if (prop === 'subscribe') return api.subscribe;
|
|
52
|
+
if (prop === 'before') return api.before;
|
|
53
|
+
if (prop === Symbol.toPrimitive) return () => state.value;
|
|
54
|
+
if (prop === 'valueOf') return () => state.value;
|
|
55
|
+
if (prop === 'toString') return () => String(state.value);
|
|
56
|
+
|
|
57
|
+
const value = state.value;
|
|
58
|
+
if (Array.isArray(value) && prop === 'map') {
|
|
59
|
+
return (fn) => {
|
|
60
|
+
const out = value.map(fn);
|
|
61
|
+
Object.defineProperty(out, SIGNAL_MAP, { value: { signal: proxy, mapFn: fn } });
|
|
62
|
+
return out;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isObject(value)) {
|
|
67
|
+
const v = value[prop];
|
|
68
|
+
if (typeof v === 'function') return v.bind(value);
|
|
69
|
+
return v;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return proxy;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isSignal(value) {
|
|
79
|
+
return !!value && value[SIGNAL] === true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function subscribeSignal(sig, fn) {
|
|
83
|
+
return sig?.subscribe?.(fn);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readSignal(sig) {
|
|
87
|
+
return sig?.get?.();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function setSignal(sig, next, force = false) {
|
|
91
|
+
return sig?.set?.(next, force);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getMappedArrayMeta(value) {
|
|
95
|
+
if (!Array.isArray(value)) return null;
|
|
96
|
+
return value[SIGNAL_MAP] || null;
|
|
97
|
+
}
|