round-core 0.1.2 → 0.1.3

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