round-core 0.0.1
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 +354 -0
- package/Round.png +0 -0
- package/bun.lock +414 -0
- package/cli.js +2 -0
- package/index.html +19 -0
- package/index.js +2 -0
- package/package.json +40 -0
- package/src/cli.js +599 -0
- package/src/compiler/index.js +2 -0
- package/src/compiler/transformer.js +395 -0
- package/src/compiler/vite-plugin.js +461 -0
- package/src/index.js +45 -0
- package/src/runtime/context.js +62 -0
- package/src/runtime/dom.js +345 -0
- package/src/runtime/error-boundary.js +48 -0
- package/src/runtime/error-reporter.js +13 -0
- package/src/runtime/error-store.js +85 -0
- package/src/runtime/errors.js +152 -0
- package/src/runtime/lifecycle.js +142 -0
- package/src/runtime/markdown.js +72 -0
- package/src/runtime/router.js +371 -0
- package/src/runtime/signals.js +510 -0
- package/src/runtime/store.js +208 -0
- package/src/runtime/suspense.js +106 -0
- package/vite.config.js +10 -0
- package/vitest.config.js +8 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { onMount, triggerUpdate, getCurrentComponent } from './lifecycle.js';
|
|
2
|
+
import { reportErrorSafe } from './error-reporter.js';
|
|
3
|
+
|
|
4
|
+
let context = [];
|
|
5
|
+
|
|
6
|
+
function isPromiseLike(v) {
|
|
7
|
+
return v && (typeof v === 'object' || typeof v === 'function') && typeof v.then === 'function';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function subscribe(running, subscriptions) {
|
|
11
|
+
subscriptions.add(running);
|
|
12
|
+
running.dependencies.add(subscriptions);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function untrack(fn) {
|
|
16
|
+
context.push(null);
|
|
17
|
+
try {
|
|
18
|
+
return typeof fn === 'function' ? fn() : undefined;
|
|
19
|
+
} finally {
|
|
20
|
+
context.pop();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function effect(arg1, arg2, arg3) {
|
|
25
|
+
let callback;
|
|
26
|
+
let explicitDeps = null;
|
|
27
|
+
let options = { onLoad: true };
|
|
28
|
+
|
|
29
|
+
let owner = getCurrentComponent();
|
|
30
|
+
|
|
31
|
+
if (typeof arg1 === 'function') {
|
|
32
|
+
callback = arg1;
|
|
33
|
+
if (arg2 && typeof arg2 === 'object') {
|
|
34
|
+
options = { ...options, ...arg2 };
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
explicitDeps = arg1;
|
|
38
|
+
callback = arg2;
|
|
39
|
+
if (arg3 && typeof arg3 === 'object') {
|
|
40
|
+
options = { ...options, ...arg3 };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const execute = () => {
|
|
45
|
+
if (typeof execute._cleanup === 'function') {
|
|
46
|
+
try {
|
|
47
|
+
execute._cleanup();
|
|
48
|
+
} catch (e) {
|
|
49
|
+
const name = owner ? (owner.name ?? 'Anonymous') : null;
|
|
50
|
+
reportErrorSafe(e, { phase: 'effect.cleanup', component: name });
|
|
51
|
+
}
|
|
52
|
+
execute._cleanup = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
cleanup(execute);
|
|
56
|
+
context.push(execute);
|
|
57
|
+
try {
|
|
58
|
+
if (explicitDeps) {
|
|
59
|
+
if (Array.isArray(explicitDeps)) {
|
|
60
|
+
explicitDeps.forEach(dep => {
|
|
61
|
+
if (typeof dep === 'function') dep();
|
|
62
|
+
});
|
|
63
|
+
} else if (typeof explicitDeps === 'function') {
|
|
64
|
+
explicitDeps();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (typeof callback === 'function') {
|
|
68
|
+
const res = callback();
|
|
69
|
+
if (typeof res === 'function') {
|
|
70
|
+
execute._cleanup = res;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (owner && owner.isMounted) triggerUpdate(owner);
|
|
75
|
+
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (isPromiseLike(e)) throw e;
|
|
78
|
+
const name = owner ? (owner.name ?? 'Anonymous') : null;
|
|
79
|
+
reportErrorSafe(e, { phase: 'effect', component: name });
|
|
80
|
+
} finally {
|
|
81
|
+
context.pop();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
execute.dependencies = new Set();
|
|
86
|
+
execute._cleanup = null;
|
|
87
|
+
|
|
88
|
+
if (options.onLoad) {
|
|
89
|
+
onMount(execute);
|
|
90
|
+
} else {
|
|
91
|
+
execute();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
if (typeof execute._cleanup === 'function') {
|
|
96
|
+
try {
|
|
97
|
+
execute._cleanup();
|
|
98
|
+
} catch (e) {
|
|
99
|
+
const name = owner ? (owner.name ?? 'Anonymous') : null;
|
|
100
|
+
reportErrorSafe(e, { phase: 'effect.cleanup', component: name });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
execute._cleanup = null;
|
|
104
|
+
cleanup(execute);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cleanup(running) {
|
|
109
|
+
running.dependencies.forEach(dep => dep.delete(running));
|
|
110
|
+
running.dependencies.clear();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function defineBindMarkerIfNeeded(source, target) {
|
|
114
|
+
if (source && source.bind === true) {
|
|
115
|
+
try {
|
|
116
|
+
Object.defineProperty(target, 'bind', {
|
|
117
|
+
enumerable: true,
|
|
118
|
+
configurable: false,
|
|
119
|
+
writable: false,
|
|
120
|
+
value: true
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
try { target.bind = true; } catch { }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function attachHelpers(s) {
|
|
129
|
+
if (!s || typeof s !== 'function') return s;
|
|
130
|
+
if (typeof s.transform === 'function' && typeof s.validate === 'function' && typeof s.$pick === 'function') return s;
|
|
131
|
+
|
|
132
|
+
s.$pick = (p) => {
|
|
133
|
+
return pick(s, p);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
s.transform = (fromInput, toOutput) => {
|
|
137
|
+
const fromFn = typeof fromInput === 'function' ? fromInput : (v) => v;
|
|
138
|
+
const toFn = typeof toOutput === 'function' ? toOutput : (v) => v;
|
|
139
|
+
|
|
140
|
+
const wrapped = function (...args) {
|
|
141
|
+
if (args.length > 0) {
|
|
142
|
+
return s(fromFn(args[0]));
|
|
143
|
+
}
|
|
144
|
+
return toFn(s());
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
wrapped.peek = () => toFn(s.peek());
|
|
148
|
+
Object.defineProperty(wrapped, 'value', {
|
|
149
|
+
enumerable: true,
|
|
150
|
+
get() {
|
|
151
|
+
return wrapped.peek();
|
|
152
|
+
},
|
|
153
|
+
set(v) {
|
|
154
|
+
wrapped(v);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
defineBindMarkerIfNeeded(s, wrapped);
|
|
159
|
+
return attachHelpers(wrapped);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
s.validate = (validator, options = {}) => {
|
|
163
|
+
const validateFn = typeof validator === 'function' ? validator : null;
|
|
164
|
+
const error = signal(null);
|
|
165
|
+
const validateOn = (options && typeof options === 'object' && typeof options.validateOn === 'string')
|
|
166
|
+
? options.validateOn
|
|
167
|
+
: 'input';
|
|
168
|
+
const validateInitial = Boolean(options && typeof options === 'object' && options.validateInitial);
|
|
169
|
+
|
|
170
|
+
const wrapped = function (...args) {
|
|
171
|
+
if (args.length > 0) {
|
|
172
|
+
const next = args[0];
|
|
173
|
+
if (validateFn) {
|
|
174
|
+
let res = true;
|
|
175
|
+
try {
|
|
176
|
+
res = validateFn(next, s.peek());
|
|
177
|
+
} catch {
|
|
178
|
+
res = 'Invalid value';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (res === true || res === undefined || res === null) {
|
|
182
|
+
error(null);
|
|
183
|
+
return s(next);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (typeof res === 'string' && res.length) {
|
|
187
|
+
error(res);
|
|
188
|
+
} else {
|
|
189
|
+
error('Invalid value');
|
|
190
|
+
}
|
|
191
|
+
return s.peek();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
error(null);
|
|
195
|
+
return s(next);
|
|
196
|
+
}
|
|
197
|
+
return s();
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
wrapped.check = () => {
|
|
201
|
+
if (!validateFn) {
|
|
202
|
+
error(null);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
const cur = s.peek();
|
|
206
|
+
let res = true;
|
|
207
|
+
try {
|
|
208
|
+
res = validateFn(cur, cur);
|
|
209
|
+
} catch {
|
|
210
|
+
res = 'Invalid value';
|
|
211
|
+
}
|
|
212
|
+
if (res === true || res === undefined || res === null) {
|
|
213
|
+
error(null);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if (typeof res === 'string' && res.length) error(res);
|
|
217
|
+
else error('Invalid value');
|
|
218
|
+
return false;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
wrapped.peek = () => s.peek();
|
|
222
|
+
Object.defineProperty(wrapped, 'value', {
|
|
223
|
+
enumerable: true,
|
|
224
|
+
get() {
|
|
225
|
+
return wrapped.peek();
|
|
226
|
+
},
|
|
227
|
+
set(v) {
|
|
228
|
+
wrapped(v);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
wrapped.error = error;
|
|
233
|
+
wrapped.__round_validateOn = validateOn;
|
|
234
|
+
if (validateInitial) {
|
|
235
|
+
try { wrapped.check(); } catch { }
|
|
236
|
+
}
|
|
237
|
+
defineBindMarkerIfNeeded(s, wrapped);
|
|
238
|
+
return attachHelpers(wrapped);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return s;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function signal(initialValue) {
|
|
245
|
+
let value = initialValue;
|
|
246
|
+
const subscriptions = new Set();
|
|
247
|
+
|
|
248
|
+
const read = () => {
|
|
249
|
+
const running = context[context.length - 1];
|
|
250
|
+
if (running) {
|
|
251
|
+
subscribe(running, subscriptions);
|
|
252
|
+
}
|
|
253
|
+
return value;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const peek = () => value;
|
|
257
|
+
|
|
258
|
+
const write = (newValue) => {
|
|
259
|
+
if (value !== newValue) {
|
|
260
|
+
value = newValue;
|
|
261
|
+
[...subscriptions].forEach(sub => sub());
|
|
262
|
+
}
|
|
263
|
+
return value;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const signal = function (...args) {
|
|
267
|
+
if (args.length > 0) {
|
|
268
|
+
return write(args[0]);
|
|
269
|
+
}
|
|
270
|
+
return read();
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
Object.defineProperty(signal, 'value', {
|
|
274
|
+
enumerable: true,
|
|
275
|
+
get() {
|
|
276
|
+
return peek();
|
|
277
|
+
},
|
|
278
|
+
set(v) {
|
|
279
|
+
write(v);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
signal.peek = peek;
|
|
284
|
+
|
|
285
|
+
return attachHelpers(signal);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function bindable(initialValue) {
|
|
289
|
+
const s = signal(initialValue);
|
|
290
|
+
try {
|
|
291
|
+
Object.defineProperty(s, 'bind', {
|
|
292
|
+
enumerable: true,
|
|
293
|
+
configurable: false,
|
|
294
|
+
writable: false,
|
|
295
|
+
value: true
|
|
296
|
+
});
|
|
297
|
+
} catch {
|
|
298
|
+
// Fallback if defineProperty fails
|
|
299
|
+
try { s.bind = true; } catch { }
|
|
300
|
+
}
|
|
301
|
+
return attachHelpers(s);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function isSignalLike(v) {
|
|
305
|
+
return typeof v === 'function' && typeof v.peek === 'function' && ('value' in v);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function getIn(obj, path) {
|
|
309
|
+
let cur = obj;
|
|
310
|
+
for (const key of path) {
|
|
311
|
+
if (cur == null) return undefined;
|
|
312
|
+
cur = cur[key];
|
|
313
|
+
}
|
|
314
|
+
return cur;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function setIn(obj, path, value) {
|
|
318
|
+
if (!Array.isArray(path) || path.length === 0) return value;
|
|
319
|
+
|
|
320
|
+
const root = (obj && typeof obj === 'object') ? obj : {};
|
|
321
|
+
const out = Array.isArray(root) ? root.slice() : { ...root };
|
|
322
|
+
|
|
323
|
+
let curOut = out;
|
|
324
|
+
let curIn = root;
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
327
|
+
const key = path[i];
|
|
328
|
+
const nextIn = (curIn && typeof curIn === 'object') ? curIn[key] : undefined;
|
|
329
|
+
const nextOut = (nextIn && typeof nextIn === 'object')
|
|
330
|
+
? (Array.isArray(nextIn) ? nextIn.slice() : { ...nextIn })
|
|
331
|
+
: {};
|
|
332
|
+
|
|
333
|
+
curOut[key] = nextOut;
|
|
334
|
+
curOut = nextOut;
|
|
335
|
+
curIn = nextIn;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
curOut[path[path.length - 1]] = value;
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function parsePath(path) {
|
|
343
|
+
if (Array.isArray(path)) return path.map(p => String(p));
|
|
344
|
+
if (typeof path === 'string') return path.split('.').filter(Boolean);
|
|
345
|
+
return [String(path)];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function pick(root, path) {
|
|
349
|
+
if (!isSignalLike(root)) {
|
|
350
|
+
throw new Error('[round] pick(root, path) expects root to be a signal (use bindable.object(...) or signal({...})).');
|
|
351
|
+
}
|
|
352
|
+
const pathArr = parsePath(path);
|
|
353
|
+
|
|
354
|
+
const view = function (...args) {
|
|
355
|
+
if (args.length > 0) {
|
|
356
|
+
const nextRoot = setIn(root.peek(), pathArr, args[0]);
|
|
357
|
+
return root(nextRoot);
|
|
358
|
+
}
|
|
359
|
+
const v = root();
|
|
360
|
+
return getIn(v, pathArr);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
view.peek = () => getIn(root.peek(), pathArr);
|
|
364
|
+
Object.defineProperty(view, 'value', {
|
|
365
|
+
enumerable: true,
|
|
366
|
+
get() {
|
|
367
|
+
return view.peek();
|
|
368
|
+
},
|
|
369
|
+
set(v) {
|
|
370
|
+
view(v);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (root.bind === true) {
|
|
375
|
+
try {
|
|
376
|
+
Object.defineProperty(view, 'bind', {
|
|
377
|
+
enumerable: true,
|
|
378
|
+
configurable: false,
|
|
379
|
+
writable: false,
|
|
380
|
+
value: true
|
|
381
|
+
});
|
|
382
|
+
} catch {
|
|
383
|
+
try { view.bind = true; } catch { }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return view;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function createBindableObjectProxy(root, basePath) {
|
|
391
|
+
const cache = new Map();
|
|
392
|
+
|
|
393
|
+
const handler = {
|
|
394
|
+
get(_target, prop) {
|
|
395
|
+
if (prop === Symbol.toStringTag) return 'BindableObject';
|
|
396
|
+
if (prop === Symbol.iterator) return undefined;
|
|
397
|
+
if (prop === 'peek') return () => (basePath.length ? pick(root, basePath).peek() : root.peek());
|
|
398
|
+
if (prop === 'value') return (basePath.length ? pick(root, basePath).peek() : root.peek());
|
|
399
|
+
if (prop === 'bind') return true;
|
|
400
|
+
if (prop === '$pick') {
|
|
401
|
+
return (p) => {
|
|
402
|
+
const nextPath = basePath.concat(parsePath(p));
|
|
403
|
+
return createBindableObjectProxy(root, nextPath);
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (prop === '_root') return root;
|
|
407
|
+
if (prop === '_path') return basePath.slice();
|
|
408
|
+
|
|
409
|
+
// Allow calling the proxy (it's a function proxy below)
|
|
410
|
+
if (prop === 'call' || prop === 'apply') {
|
|
411
|
+
return Reflect.get(_target, prop);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const key = String(prop);
|
|
415
|
+
const nextPath = basePath.concat(key);
|
|
416
|
+
const cacheKey = nextPath.join('.');
|
|
417
|
+
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
|
418
|
+
|
|
419
|
+
// If the stored value at this path is itself a signal/bindable, return it directly.
|
|
420
|
+
// This enables bindable.object({ email: bindable('').validate(...) }) patterns.
|
|
421
|
+
try {
|
|
422
|
+
const stored = getIn(root.peek(), nextPath);
|
|
423
|
+
if (isSignalLike(stored)) {
|
|
424
|
+
cache.set(cacheKey, stored);
|
|
425
|
+
return stored;
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const next = createBindableObjectProxy(root, nextPath);
|
|
431
|
+
cache.set(cacheKey, next);
|
|
432
|
+
return next;
|
|
433
|
+
},
|
|
434
|
+
set(_target, prop, value) {
|
|
435
|
+
const key = String(prop);
|
|
436
|
+
const nextPath = basePath.concat(key);
|
|
437
|
+
try {
|
|
438
|
+
const stored = getIn(root.peek(), nextPath);
|
|
439
|
+
if (isSignalLike(stored)) {
|
|
440
|
+
stored(value);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
pick(root, nextPath)(value);
|
|
446
|
+
return true;
|
|
447
|
+
},
|
|
448
|
+
has(_target, prop) {
|
|
449
|
+
// IMPORTANT: Proxy invariants require that if the target has a non-configurable
|
|
450
|
+
// property, the `has` trap must return true.
|
|
451
|
+
try {
|
|
452
|
+
if (Reflect.has(_target, prop)) return true;
|
|
453
|
+
} catch {
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const v = basePath.length ? pick(root, basePath).peek() : root.peek();
|
|
457
|
+
return v != null && Object.prototype.hasOwnProperty.call(v, prop);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Function proxy so you can do user() / user.name() etc.
|
|
462
|
+
const fn = function (...args) {
|
|
463
|
+
if (args.length > 0) {
|
|
464
|
+
if (basePath.length) return pick(root, basePath)(args[0]);
|
|
465
|
+
return root(args[0]);
|
|
466
|
+
}
|
|
467
|
+
if (basePath.length) return pick(root, basePath)();
|
|
468
|
+
return root();
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// Make it signal-like
|
|
472
|
+
fn.peek = () => (basePath.length ? pick(root, basePath).peek() : root.peek());
|
|
473
|
+
Object.defineProperty(fn, 'value', {
|
|
474
|
+
enumerable: true,
|
|
475
|
+
get() {
|
|
476
|
+
return fn.peek();
|
|
477
|
+
},
|
|
478
|
+
set(v) {
|
|
479
|
+
fn(v);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
Object.defineProperty(fn, 'bind', {
|
|
485
|
+
enumerable: true,
|
|
486
|
+
configurable: false,
|
|
487
|
+
writable: false,
|
|
488
|
+
value: true
|
|
489
|
+
});
|
|
490
|
+
} catch {
|
|
491
|
+
try { fn.bind = true; } catch { }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return new Proxy(fn, handler);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
bindable.object = function (initialObject = {}) {
|
|
498
|
+
const root = bindable((initialObject && typeof initialObject === 'object') ? initialObject : {});
|
|
499
|
+
return createBindableObjectProxy(root, []);
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
export function derive(fn) {
|
|
503
|
+
const derived = signal();
|
|
504
|
+
|
|
505
|
+
effect(() => {
|
|
506
|
+
derived(fn());
|
|
507
|
+
}, { onLoad: false });
|
|
508
|
+
|
|
509
|
+
return () => derived();
|
|
510
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { bindable, effect } from './signals.js';
|
|
2
|
+
import { reportErrorSafe } from './error-reporter.js';
|
|
3
|
+
|
|
4
|
+
function hasWindow() {
|
|
5
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createStore(initialState = {}, actions = null) {
|
|
9
|
+
const state = (initialState && typeof initialState === 'object') ? initialState : {};
|
|
10
|
+
const signals = Object.create(null);
|
|
11
|
+
const persistState = {
|
|
12
|
+
enabled: false,
|
|
13
|
+
key: null,
|
|
14
|
+
storage: null,
|
|
15
|
+
persisting: false,
|
|
16
|
+
persistNow: null,
|
|
17
|
+
watchers: new Set()
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (const k of Object.keys(state)) {
|
|
21
|
+
signals[k] = bindable(state[k]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function setKey(k, v) {
|
|
25
|
+
const key = String(k);
|
|
26
|
+
if (!Object.prototype.hasOwnProperty.call(signals, key)) {
|
|
27
|
+
signals[key] = bindable(state[key]);
|
|
28
|
+
}
|
|
29
|
+
state[key] = v;
|
|
30
|
+
signals[key](v);
|
|
31
|
+
if (persistState.enabled && typeof persistState.persistNow === 'function') {
|
|
32
|
+
persistState.persistNow();
|
|
33
|
+
}
|
|
34
|
+
return v;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function patch(obj) {
|
|
38
|
+
if (!obj || typeof obj !== 'object') return;
|
|
39
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
40
|
+
setKey(k, v);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getSnapshot(reactive = false) {
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const k of Object.keys(signals)) {
|
|
47
|
+
out[k] = reactive ? signals[k]() : signals[k].peek();
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const store = {
|
|
53
|
+
use(key) {
|
|
54
|
+
const k = String(key);
|
|
55
|
+
if (!Object.prototype.hasOwnProperty.call(signals, k)) {
|
|
56
|
+
signals[k] = bindable(state[k]);
|
|
57
|
+
if (!Object.prototype.hasOwnProperty.call(state, k)) {
|
|
58
|
+
try {
|
|
59
|
+
reportErrorSafe(new Error(`Store key not found: ${k}`), { phase: 'store.use', component: 'createStore' });
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (persistState.enabled) {
|
|
66
|
+
const sig = signals[k];
|
|
67
|
+
if (sig && typeof sig === 'function' && !persistState.watchers.has(k)) {
|
|
68
|
+
persistState.watchers.add(k);
|
|
69
|
+
effect(() => {
|
|
70
|
+
sig();
|
|
71
|
+
if (persistState.persisting) return;
|
|
72
|
+
if (typeof persistState.persistNow === 'function') persistState.persistNow();
|
|
73
|
+
}, { onLoad: false });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return signals[k];
|
|
78
|
+
},
|
|
79
|
+
set(key, value) {
|
|
80
|
+
return setKey(key, value);
|
|
81
|
+
},
|
|
82
|
+
patch,
|
|
83
|
+
snapshot(options = {}) {
|
|
84
|
+
const reactive = options && typeof options === 'object' && options.reactive === true;
|
|
85
|
+
return getSnapshot(reactive);
|
|
86
|
+
},
|
|
87
|
+
actions: {}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (actions && typeof actions === 'object') {
|
|
91
|
+
Object.entries(actions).forEach(([name, reducer]) => {
|
|
92
|
+
if (typeof reducer !== 'function') return;
|
|
93
|
+
const fn = (...args) => {
|
|
94
|
+
try {
|
|
95
|
+
const next = reducer(getSnapshot(false), ...args);
|
|
96
|
+
if (next && typeof next === 'object') {
|
|
97
|
+
patch(next);
|
|
98
|
+
}
|
|
99
|
+
return next;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
reportErrorSafe(e, { phase: 'store.action', component: String(name) });
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
store.actions[name] = fn;
|
|
105
|
+
store[name] = fn;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
store.persist = (storageKey, optionsOrStorage) => {
|
|
110
|
+
if (typeof storageKey !== 'string' || !storageKey.length) return store;
|
|
111
|
+
|
|
112
|
+
const isStorageLike = optionsOrStorage
|
|
113
|
+
&& (typeof optionsOrStorage.getItem === 'function')
|
|
114
|
+
&& (typeof optionsOrStorage.setItem === 'function');
|
|
115
|
+
|
|
116
|
+
const opts = (!isStorageLike && optionsOrStorage && typeof optionsOrStorage === 'object')
|
|
117
|
+
? optionsOrStorage
|
|
118
|
+
: {};
|
|
119
|
+
|
|
120
|
+
const st = isStorageLike
|
|
121
|
+
? optionsOrStorage
|
|
122
|
+
: (opts.storage ?? (hasWindow() ? window.localStorage : null));
|
|
123
|
+
|
|
124
|
+
if (!st || typeof st.getItem !== 'function' || typeof st.setItem !== 'function') return store;
|
|
125
|
+
|
|
126
|
+
const debounceMs = Number.isFinite(Number(opts.debounce)) ? Number(opts.debounce) : 0;
|
|
127
|
+
const exclude = Array.isArray(opts.exclude) ? opts.exclude.map(String) : [];
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const raw = st.getItem(storageKey);
|
|
131
|
+
if (raw) {
|
|
132
|
+
const parsed = JSON.parse(raw);
|
|
133
|
+
if (parsed && typeof parsed === 'object') {
|
|
134
|
+
const filtered = exclude.length
|
|
135
|
+
? Object.fromEntries(Object.entries(parsed).filter(([k]) => !exclude.includes(String(k))))
|
|
136
|
+
: parsed;
|
|
137
|
+
patch(filtered);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const persistNow = () => {
|
|
144
|
+
try {
|
|
145
|
+
persistState.persisting = true;
|
|
146
|
+
const snap = getSnapshot(false);
|
|
147
|
+
const out = exclude.length
|
|
148
|
+
? Object.fromEntries(Object.entries(snap).filter(([k]) => !exclude.includes(String(k))))
|
|
149
|
+
: snap;
|
|
150
|
+
st.setItem(storageKey, JSON.stringify(out));
|
|
151
|
+
} catch {
|
|
152
|
+
} finally {
|
|
153
|
+
persistState.persisting = false;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let debounceId = null;
|
|
158
|
+
const schedulePersist = () => {
|
|
159
|
+
if (debounceMs <= 0) return persistNow();
|
|
160
|
+
try {
|
|
161
|
+
if (debounceId != null) clearTimeout(debounceId);
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
debounceId = setTimeout(() => {
|
|
165
|
+
debounceId = null;
|
|
166
|
+
persistNow();
|
|
167
|
+
}, debounceMs);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
persistState.enabled = true;
|
|
171
|
+
persistState.key = storageKey;
|
|
172
|
+
persistState.storage = st;
|
|
173
|
+
persistState.persistNow = schedulePersist;
|
|
174
|
+
|
|
175
|
+
const origSet = store.set;
|
|
176
|
+
store.set = (k, v) => {
|
|
177
|
+
const res = origSet(k, v);
|
|
178
|
+
schedulePersist();
|
|
179
|
+
return res;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const origPatch = store.patch;
|
|
183
|
+
store.patch = (obj) => {
|
|
184
|
+
origPatch(obj);
|
|
185
|
+
schedulePersist();
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
Object.keys(store.actions).forEach((name) => {
|
|
189
|
+
const orig = store.actions[name];
|
|
190
|
+
if (typeof orig !== 'function') return;
|
|
191
|
+
store.actions[name] = (...args) => {
|
|
192
|
+
const res = orig(...args);
|
|
193
|
+
schedulePersist();
|
|
194
|
+
return res;
|
|
195
|
+
};
|
|
196
|
+
store[name] = store.actions[name];
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
Object.keys(signals).forEach((k) => {
|
|
200
|
+
try { store.use(k); } catch { }
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
schedulePersist();
|
|
204
|
+
return store;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return store;
|
|
208
|
+
}
|