@zeix/cause-effect 0.15.1 → 0.16.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.
Files changed (48) hide show
  1. package/.ai-context.md +254 -0
  2. package/.cursorrules +54 -0
  3. package/.github/copilot-instructions.md +132 -0
  4. package/CLAUDE.md +319 -0
  5. package/README.md +167 -159
  6. package/eslint.config.js +1 -1
  7. package/index.dev.js +528 -407
  8. package/index.js +1 -1
  9. package/index.ts +36 -25
  10. package/package.json +1 -1
  11. package/src/computed.ts +41 -30
  12. package/src/diff.ts +57 -44
  13. package/src/effect.ts +15 -16
  14. package/src/errors.ts +64 -0
  15. package/src/match.ts +2 -2
  16. package/src/resolve.ts +2 -2
  17. package/src/signal.ts +27 -49
  18. package/src/state.ts +27 -19
  19. package/src/store.ts +410 -209
  20. package/src/system.ts +122 -0
  21. package/src/util.ts +45 -6
  22. package/test/batch.test.ts +18 -11
  23. package/test/benchmark.test.ts +4 -4
  24. package/test/computed.test.ts +508 -72
  25. package/test/diff.test.ts +321 -4
  26. package/test/effect.test.ts +61 -61
  27. package/test/match.test.ts +38 -28
  28. package/test/resolve.test.ts +16 -16
  29. package/test/signal.test.ts +19 -147
  30. package/test/state.test.ts +212 -25
  31. package/test/store.test.ts +1370 -134
  32. package/test/util/dependency-graph.ts +1 -1
  33. package/types/index.d.ts +10 -9
  34. package/types/src/collection.d.ts +26 -0
  35. package/types/src/computed.d.ts +9 -9
  36. package/types/src/diff.d.ts +5 -3
  37. package/types/src/effect.d.ts +3 -3
  38. package/types/src/errors.d.ts +22 -0
  39. package/types/src/match.d.ts +1 -1
  40. package/types/src/resolve.d.ts +1 -1
  41. package/types/src/signal.d.ts +12 -19
  42. package/types/src/state.d.ts +5 -5
  43. package/types/src/store.d.ts +40 -36
  44. package/types/src/system.d.ts +44 -0
  45. package/types/src/util.d.ts +7 -5
  46. package/index.d.ts +0 -36
  47. package/src/scheduler.ts +0 -172
  48. package/types/test-new-effect.d.ts +0 -1
package/index.dev.js CHANGED
@@ -1,380 +1,82 @@
1
- // src/scheduler.ts
2
- var active;
3
- var pending = new Set;
4
- var batchDepth = 0;
5
- var updateMap = new Map;
6
- var requestId;
7
- var updateDOM = () => {
8
- requestId = undefined;
9
- const updates = Array.from(updateMap.values());
10
- updateMap.clear();
11
- for (const update of updates) {
12
- update();
1
+ // src/errors.ts
2
+ class CircularDependencyError extends Error {
3
+ constructor(where) {
4
+ super(`Circular dependency detected in ${where}`);
5
+ this.name = "CircularDependencyError";
13
6
  }
14
- };
15
- var requestTick = () => {
16
- if (requestId)
17
- cancelAnimationFrame(requestId);
18
- requestId = requestAnimationFrame(updateDOM);
19
- };
20
- queueMicrotask(updateDOM);
21
- var watch = (notice) => {
22
- const cleanups = new Set;
23
- const w = notice;
24
- w.off = (on) => {
25
- cleanups.add(on);
26
- };
27
- w.cleanup = () => {
28
- for (const cleanup of cleanups) {
29
- cleanup();
30
- }
31
- cleanups.clear();
32
- };
33
- return w;
34
- };
35
- var subscribe = (watchers) => {
36
- if (active && !watchers.has(active)) {
37
- const watcher = active;
38
- watchers.add(watcher);
39
- active.off(() => {
40
- watchers.delete(watcher);
41
- });
7
+ }
8
+
9
+ class InvalidCallbackError extends TypeError {
10
+ constructor(where, value) {
11
+ super(`Invalid ${where} callback ${value}`);
12
+ this.name = "InvalidCallbackError";
42
13
  }
43
- };
44
- var notify = (watchers) => {
45
- for (const watcher of watchers) {
46
- if (batchDepth)
47
- pending.add(watcher);
48
- else
49
- watcher();
14
+ }
15
+
16
+ class InvalidSignalValueError extends TypeError {
17
+ constructor(where, value) {
18
+ super(`Invalid signal value ${value} in ${where}`);
19
+ this.name = "InvalidSignalValueError";
50
20
  }
51
- };
52
- var flush = () => {
53
- while (pending.size) {
54
- const watchers = Array.from(pending);
55
- pending.clear();
56
- for (const watcher of watchers) {
57
- watcher();
58
- }
21
+ }
22
+
23
+ class NullishSignalValueError extends TypeError {
24
+ constructor(where) {
25
+ super(`Nullish signal values are not allowed in ${where}`);
26
+ this.name = "NullishSignalValueError";
59
27
  }
60
- };
61
- var batch = (fn) => {
62
- batchDepth++;
63
- try {
64
- fn();
65
- } finally {
66
- flush();
67
- batchDepth--;
28
+ }
29
+
30
+ class StoreKeyExistsError extends Error {
31
+ constructor(key, value) {
32
+ super(`Could not add store key "${key}" with value ${value} because it already exists`);
33
+ this.name = "StoreKeyExistsError";
68
34
  }
69
- };
70
- var observe = (run, watcher) => {
71
- const prev = active;
72
- active = watcher;
73
- try {
74
- run();
75
- } finally {
76
- active = prev;
35
+ }
36
+
37
+ class StoreKeyRangeError extends RangeError {
38
+ constructor(index) {
39
+ super(`Could not remove store index ${String(index)} because it is out of range`);
40
+ this.name = "StoreKeyRangeError";
77
41
  }
78
- };
79
- var enqueue = (fn, dedupe) => new Promise((resolve, reject) => {
80
- updateMap.set(dedupe || Symbol(), () => {
81
- try {
82
- resolve(fn());
83
- } catch (error) {
84
- reject(error);
85
- }
86
- });
87
- requestTick();
88
- });
42
+ }
43
+
44
+ class StoreKeyReadonlyError extends Error {
45
+ constructor(key, value) {
46
+ super(`Could not set store key "${key}" to ${value} because it is readonly`);
47
+ this.name = "StoreKeyReadonlyError";
48
+ }
49
+ }
89
50
 
90
51
  // src/util.ts
52
+ var UNSET = Symbol();
91
53
  var isString = (value) => typeof value === "string";
92
54
  var isNumber = (value) => typeof value === "number";
55
+ var isSymbol = (value) => typeof value === "symbol";
93
56
  var isFunction = (fn) => typeof fn === "function";
94
57
  var isAsyncFunction = (fn) => isFunction(fn) && fn.constructor.name === "AsyncFunction";
95
58
  var isObjectOfType = (value, type) => Object.prototype.toString.call(value) === `[object ${type}]`;
96
59
  var isRecord = (value) => isObjectOfType(value, "Object");
60
+ var isRecordOrArray = (value) => isRecord(value) || Array.isArray(value);
97
61
  var validArrayIndexes = (keys) => {
98
62
  if (!keys.length)
99
63
  return null;
100
64
  const indexes = keys.map((k) => isString(k) ? parseInt(k, 10) : isNumber(k) ? k : NaN);
101
65
  return indexes.every((index) => Number.isFinite(index) && index >= 0) ? indexes.sort((a, b) => a - b) : null;
102
66
  };
103
- var hasMethod = (obj, methodName) => (methodName in obj) && isFunction(obj[methodName]);
104
67
  var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError";
105
68
  var toError = (reason) => reason instanceof Error ? reason : Error(String(reason));
106
-
107
- class CircularDependencyError extends Error {
108
- constructor(where) {
109
- super(`Circular dependency in ${where} detected`);
110
- this.name = "CircularDependencyError";
111
- }
112
- }
113
-
114
- // src/state.ts
115
- var TYPE_STATE = "State";
116
- var state = (initialValue) => {
117
- const watchers = new Set;
118
- let value = initialValue;
119
- const s = {
120
- [Symbol.toStringTag]: TYPE_STATE,
121
- get: () => {
122
- subscribe(watchers);
123
- return value;
124
- },
125
- set: (v) => {
126
- if (isEqual(value, v))
127
- return;
128
- value = v;
129
- notify(watchers);
130
- if (UNSET === value)
131
- watchers.clear();
132
- },
133
- update: (fn) => {
134
- s.set(fn(value));
135
- }
136
- };
137
- return s;
138
- };
139
- var isState = (value) => isObjectOfType(value, TYPE_STATE);
140
-
141
- // src/effect.ts
142
- var effect = (callback) => {
143
- const isAsync = isAsyncFunction(callback);
144
- let running = false;
145
- let controller;
146
- const run = watch(() => observe(() => {
147
- if (running)
148
- throw new CircularDependencyError("effect");
149
- running = true;
150
- controller?.abort();
151
- controller = undefined;
152
- let cleanup;
153
- try {
154
- if (isAsync) {
155
- controller = new AbortController;
156
- const currentController = controller;
157
- callback(controller.signal).then((cleanup2) => {
158
- if (isFunction(cleanup2) && controller === currentController)
159
- run.off(cleanup2);
160
- }).catch((error) => {
161
- if (!isAbortError(error))
162
- console.error("Async effect error:", error);
163
- });
164
- } else {
165
- cleanup = callback();
166
- if (isFunction(cleanup))
167
- run.off(cleanup);
168
- }
169
- } catch (error) {
170
- if (!isAbortError(error))
171
- console.error("Effect callback error:", error);
172
- }
173
- running = false;
174
- }, run));
175
- run();
176
- return () => {
177
- controller?.abort();
178
- run.cleanup();
179
- };
180
- };
181
-
182
- // src/store.ts
183
- var TYPE_STORE = "Store";
184
- var store = (initialValue) => {
185
- const watchers = new Set;
186
- const eventTarget = new EventTarget;
187
- const signals = new Map;
188
- const cleanups = new Map;
189
- const size = state(0);
190
- const current = () => {
191
- const keys = Array.from(signals.keys());
192
- const arrayIndexes = validArrayIndexes(keys);
193
- if (arrayIndexes)
194
- return arrayIndexes.map((index) => signals.get(String(index))?.get());
195
- const record = {};
196
- for (const [key, signal] of signals) {
197
- record[key] = signal.get();
198
- }
69
+ var recordToArray = (record) => {
70
+ const indexes = validArrayIndexes(Object.keys(record));
71
+ if (indexes === null)
199
72
  return record;
200
- };
201
- const emit = (type, detail) => eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
202
- const addProperty = (key, value) => {
203
- const stringKey = String(key);
204
- const signal = toMutableSignal(value);
205
- signals.set(stringKey, signal);
206
- const cleanup = effect(() => {
207
- const currentValue = signal.get();
208
- if (currentValue != null)
209
- emit("store-change", { [key]: currentValue });
210
- });
211
- cleanups.set(stringKey, cleanup);
212
- };
213
- const removeProperty = (key) => {
214
- const stringKey = String(key);
215
- signals.delete(stringKey);
216
- const cleanup = cleanups.get(stringKey);
217
- if (cleanup)
218
- cleanup();
219
- cleanups.delete(stringKey);
220
- };
221
- const reconcile = (oldValue, newValue) => {
222
- const changes = diff(oldValue, newValue);
223
- batch(() => {
224
- if (Object.keys(changes.add).length) {
225
- for (const key in changes.add) {
226
- const value = changes.add[key];
227
- if (value != null)
228
- addProperty(key, value);
229
- }
230
- emit("store-add", changes.add);
231
- }
232
- if (Object.keys(changes.change).length) {
233
- for (const key in changes.change) {
234
- const signal = signals.get(key);
235
- const value = changes.change[key];
236
- if (signal && value != null && hasMethod(signal, "set"))
237
- signal.set(value);
238
- }
239
- emit("store-change", changes.change);
240
- }
241
- if (Object.keys(changes.remove).length) {
242
- for (const key in changes.remove) {
243
- removeProperty(key);
244
- }
245
- emit("store-remove", changes.remove);
246
- }
247
- size.set(signals.size);
248
- });
249
- return changes.changed;
250
- };
251
- reconcile({}, initialValue);
252
- setTimeout(() => {
253
- const initialAdditionsEvent = new CustomEvent("store-add", {
254
- detail: initialValue
255
- });
256
- eventTarget.dispatchEvent(initialAdditionsEvent);
257
- }, 0);
258
- const storeProps = [
259
- "add",
260
- "get",
261
- "remove",
262
- "set",
263
- "update",
264
- "addEventListener",
265
- "removeEventListener",
266
- "dispatchEvent",
267
- "size"
268
- ];
269
- return new Proxy({}, {
270
- get(_target, prop) {
271
- switch (prop) {
272
- case "add":
273
- return (k, v) => {
274
- if (!signals.has(k)) {
275
- addProperty(k, v);
276
- notify(watchers);
277
- emit("store-add", {
278
- [k]: v
279
- });
280
- size.set(signals.size);
281
- }
282
- };
283
- case "get":
284
- return () => {
285
- subscribe(watchers);
286
- return current();
287
- };
288
- case "remove":
289
- return (k) => {
290
- if (signals.has(k)) {
291
- removeProperty(k);
292
- notify(watchers);
293
- emit("store-remove", { [k]: UNSET });
294
- size.set(signals.size);
295
- }
296
- };
297
- case "set":
298
- return (v) => {
299
- if (reconcile(current(), v)) {
300
- notify(watchers);
301
- if (UNSET === v)
302
- watchers.clear();
303
- }
304
- };
305
- case "update":
306
- return (fn) => {
307
- const oldValue = current();
308
- const newValue = fn(oldValue);
309
- if (reconcile(oldValue, newValue)) {
310
- notify(watchers);
311
- if (UNSET === newValue)
312
- watchers.clear();
313
- }
314
- };
315
- case "addEventListener":
316
- return eventTarget.addEventListener.bind(eventTarget);
317
- case "removeEventListener":
318
- return eventTarget.removeEventListener.bind(eventTarget);
319
- case "dispatchEvent":
320
- return eventTarget.dispatchEvent.bind(eventTarget);
321
- case "size":
322
- return size;
323
- }
324
- if (prop === Symbol.toStringTag)
325
- return TYPE_STORE;
326
- if (prop === Symbol.iterator) {
327
- return function* () {
328
- for (const [key, signal] of signals) {
329
- yield [key, signal];
330
- }
331
- };
332
- }
333
- return signals.get(String(prop));
334
- },
335
- has(_target, prop) {
336
- const key = String(prop);
337
- return signals.has(key) || storeProps.includes(key) || prop === Symbol.toStringTag || prop === Symbol.iterator;
338
- },
339
- ownKeys() {
340
- return Array.from(signals.keys()).map((key) => String(key));
341
- },
342
- getOwnPropertyDescriptor(_target, prop) {
343
- const signal = signals.get(String(prop));
344
- return signal ? {
345
- enumerable: true,
346
- configurable: true,
347
- writable: true,
348
- value: signal
349
- } : undefined;
350
- }
351
- });
73
+ const array = [];
74
+ for (const index of indexes) {
75
+ array.push(record[String(index)]);
76
+ }
77
+ return array;
352
78
  };
353
- var isStore = (value) => isObjectOfType(value, TYPE_STORE);
354
-
355
- // src/signal.ts
356
- var UNSET = Symbol();
357
- var isSignal = (value) => isState(value) || isComputed(value) || isStore(value);
358
- function toSignal(value) {
359
- if (isSignal(value))
360
- return value;
361
- if (isComputedCallback(value))
362
- return computed(value);
363
- if (Array.isArray(value))
364
- return store(value);
365
- if (Array.isArray(value) || isRecord(value))
366
- return store(value);
367
- return state(value);
368
- }
369
- function toMutableSignal(value) {
370
- if (isState(value) || isStore(value))
371
- return value;
372
- if (Array.isArray(value))
373
- return store(value);
374
- if (isRecord(value))
375
- return store(value);
376
- return state(value);
377
- }
79
+ var valueString = (value) => isString(value) ? `"${value}"` : !!value && typeof value === "object" ? JSON.stringify(value) : String(value);
378
80
 
379
81
  // src/diff.ts
380
82
  var isEqual = (a, b, visited) => {
@@ -422,45 +124,118 @@ var isEqual = (a, b, visited) => {
422
124
  }
423
125
  };
424
126
  var diff = (oldObj, newObj) => {
425
- const visited = new WeakSet;
426
- const diffRecords = (oldRecord, newRecord) => {
427
- const add = {};
428
- const change = {};
429
- const remove = {};
430
- const oldKeys = Object.keys(oldRecord);
431
- const newKeys = Object.keys(newRecord);
432
- const allKeys = new Set([...oldKeys, ...newKeys]);
433
- for (const key of allKeys) {
434
- const oldHas = key in oldRecord;
435
- const newHas = key in newRecord;
436
- if (!oldHas && newHas) {
437
- add[key] = newRecord[key];
438
- continue;
439
- } else if (oldHas && !newHas) {
440
- remove[key] = UNSET;
441
- continue;
442
- }
443
- const oldValue = oldRecord[key];
444
- const newValue = newRecord[key];
445
- if (!isEqual(oldValue, newValue, visited))
446
- change[key] = newValue;
447
- }
448
- const changed = Object.keys(add).length > 0 || Object.keys(change).length > 0 || Object.keys(remove).length > 0;
127
+ const oldValid = isRecordOrArray(oldObj);
128
+ const newValid = isRecordOrArray(newObj);
129
+ if (!oldValid || !newValid) {
130
+ const changed2 = !Object.is(oldObj, newObj);
449
131
  return {
450
- changed,
451
- add,
452
- change,
453
- remove
132
+ changed: changed2,
133
+ add: changed2 && newValid ? newObj : {},
134
+ change: {},
135
+ remove: changed2 && oldValid ? oldObj : {}
454
136
  };
137
+ }
138
+ const visited = new WeakSet;
139
+ const add = {};
140
+ const change = {};
141
+ const remove = {};
142
+ const oldKeys = Object.keys(oldObj);
143
+ const newKeys = Object.keys(newObj);
144
+ const allKeys = new Set([...oldKeys, ...newKeys]);
145
+ for (const key of allKeys) {
146
+ const oldHas = key in oldObj;
147
+ const newHas = key in newObj;
148
+ if (!oldHas && newHas) {
149
+ add[key] = newObj[key];
150
+ continue;
151
+ } else if (oldHas && !newHas) {
152
+ remove[key] = UNSET;
153
+ continue;
154
+ }
155
+ const oldValue = oldObj[key];
156
+ const newValue = newObj[key];
157
+ if (!isEqual(oldValue, newValue, visited))
158
+ change[key] = newValue;
159
+ }
160
+ const changed = Object.keys(add).length > 0 || Object.keys(change).length > 0 || Object.keys(remove).length > 0;
161
+ return {
162
+ changed,
163
+ add,
164
+ change,
165
+ remove
455
166
  };
456
- return diffRecords(oldObj, newObj);
167
+ };
168
+
169
+ // src/system.ts
170
+ var activeWatcher;
171
+ var pendingWatchers = new Set;
172
+ var batchDepth = 0;
173
+ var createWatcher = (watch) => {
174
+ const cleanups = new Set;
175
+ const w = watch;
176
+ w.unwatch = (cleanup) => {
177
+ cleanups.add(cleanup);
178
+ };
179
+ w.cleanup = () => {
180
+ for (const cleanup of cleanups)
181
+ cleanup();
182
+ cleanups.clear();
183
+ };
184
+ return w;
185
+ };
186
+ var subscribe = (watchers) => {
187
+ if (activeWatcher && !watchers.has(activeWatcher)) {
188
+ const watcher = activeWatcher;
189
+ watcher.unwatch(() => {
190
+ watchers.delete(watcher);
191
+ });
192
+ watchers.add(watcher);
193
+ }
194
+ };
195
+ var notify = (watchers) => {
196
+ for (const watcher of watchers) {
197
+ if (batchDepth)
198
+ pendingWatchers.add(watcher);
199
+ else
200
+ watcher();
201
+ }
202
+ };
203
+ var flush = () => {
204
+ while (pendingWatchers.size) {
205
+ const watchers = Array.from(pendingWatchers);
206
+ pendingWatchers.clear();
207
+ for (const watcher of watchers)
208
+ watcher();
209
+ }
210
+ };
211
+ var batch = (fn) => {
212
+ batchDepth++;
213
+ try {
214
+ fn();
215
+ } finally {
216
+ flush();
217
+ batchDepth--;
218
+ }
219
+ };
220
+ var observe = (run, watcher) => {
221
+ const prev = activeWatcher;
222
+ activeWatcher = watcher;
223
+ try {
224
+ run();
225
+ } finally {
226
+ activeWatcher = prev;
227
+ }
457
228
  };
458
229
 
459
230
  // src/computed.ts
460
231
  var TYPE_COMPUTED = "Computed";
461
- var computed = (fn) => {
232
+ var createComputed = (callback, initialValue = UNSET) => {
233
+ if (!isComputedCallback(callback))
234
+ throw new InvalidCallbackError("computed", valueString(callback));
235
+ if (initialValue == null)
236
+ throw new NullishSignalValueError("computed");
462
237
  const watchers = new Set;
463
- let value = UNSET;
238
+ let value = initialValue;
464
239
  let error;
465
240
  let controller;
466
241
  let dirty = true;
@@ -485,29 +260,29 @@ var computed = (fn) => {
485
260
  value = UNSET;
486
261
  error = newError;
487
262
  };
488
- const settle = (settleFn) => (arg) => {
263
+ const settle = (fn) => (arg) => {
489
264
  computing = false;
490
265
  controller = undefined;
491
- settleFn(arg);
266
+ fn(arg);
492
267
  if (changed)
493
268
  notify(watchers);
494
269
  };
495
- const mark = watch(() => {
270
+ const watcher = createWatcher(() => {
496
271
  dirty = true;
497
272
  controller?.abort();
498
273
  if (watchers.size)
499
274
  notify(watchers);
500
275
  else
501
- mark.cleanup();
276
+ watcher.cleanup();
502
277
  });
503
- mark.off(() => {
278
+ watcher.unwatch(() => {
504
279
  controller?.abort();
505
280
  });
506
281
  const compute = () => observe(() => {
507
282
  if (computing)
508
283
  throw new CircularDependencyError("computed");
509
284
  changed = false;
510
- if (isAsyncFunction(fn)) {
285
+ if (isAsyncFunction(callback)) {
511
286
  if (controller)
512
287
  return value;
513
288
  controller = new AbortController;
@@ -522,7 +297,7 @@ var computed = (fn) => {
522
297
  let result;
523
298
  computing = true;
524
299
  try {
525
- result = controller ? fn(controller.signal) : fn();
300
+ result = controller ? callback(value, controller.signal) : callback(value);
526
301
  } catch (e) {
527
302
  if (isAbortError(e))
528
303
  nil();
@@ -538,8 +313,8 @@ var computed = (fn) => {
538
313
  else
539
314
  ok(result);
540
315
  computing = false;
541
- }, mark);
542
- const c = {
316
+ }, watcher);
317
+ return {
543
318
  [Symbol.toStringTag]: TYPE_COMPUTED,
544
319
  get: () => {
545
320
  subscribe(watchers);
@@ -551,10 +326,51 @@ var computed = (fn) => {
551
326
  return value;
552
327
  }
553
328
  };
554
- return c;
555
329
  };
556
330
  var isComputed = (value) => isObjectOfType(value, TYPE_COMPUTED);
557
- var isComputedCallback = (value) => isFunction(value) && value.length < 2;
331
+ var isComputedCallback = (value) => isFunction(value) && value.length < 3;
332
+ // src/effect.ts
333
+ var createEffect = (callback) => {
334
+ if (!isFunction(callback) || callback.length > 1)
335
+ throw new InvalidCallbackError("effect", valueString(callback));
336
+ const isAsync = isAsyncFunction(callback);
337
+ let running = false;
338
+ let controller;
339
+ const watcher = createWatcher(() => observe(() => {
340
+ if (running)
341
+ throw new CircularDependencyError("effect");
342
+ running = true;
343
+ controller?.abort();
344
+ controller = undefined;
345
+ let cleanup;
346
+ try {
347
+ if (isAsync) {
348
+ controller = new AbortController;
349
+ const currentController = controller;
350
+ callback(controller.signal).then((cleanup2) => {
351
+ if (isFunction(cleanup2) && controller === currentController)
352
+ watcher.unwatch(cleanup2);
353
+ }).catch((error) => {
354
+ if (!isAbortError(error))
355
+ console.error("Async effect error:", error);
356
+ });
357
+ } else {
358
+ cleanup = callback();
359
+ if (isFunction(cleanup))
360
+ watcher.unwatch(cleanup);
361
+ }
362
+ } catch (error) {
363
+ if (!isAbortError(error))
364
+ console.error("Effect callback error:", error);
365
+ }
366
+ running = false;
367
+ }, watcher));
368
+ watcher();
369
+ return () => {
370
+ controller?.abort();
371
+ watcher.cleanup();
372
+ };
373
+ };
558
374
  // src/match.ts
559
375
  function match(result, handlers) {
560
376
  try {
@@ -562,8 +378,8 @@ function match(result, handlers) {
562
378
  handlers.nil?.();
563
379
  else if (result.errors)
564
380
  handlers.err?.(result.errors);
565
- else
566
- handlers.ok?.(result.values);
381
+ else if (result.ok)
382
+ handlers.ok(result.values);
567
383
  } catch (error) {
568
384
  if (handlers.err && (!result.errors || !result.errors.includes(toError(error))))
569
385
  handlers.err(result.errors ? [...result.errors, toError(error)] : [toError(error)]);
@@ -574,42 +390,339 @@ function match(result, handlers) {
574
390
  // src/resolve.ts
575
391
  function resolve(signals) {
576
392
  const errors = [];
577
- let pending2 = false;
393
+ let pending = false;
578
394
  const values = {};
579
395
  for (const [key, signal] of Object.entries(signals)) {
580
396
  try {
581
397
  const value = signal.get();
582
398
  if (value === UNSET)
583
- pending2 = true;
399
+ pending = true;
584
400
  else
585
401
  values[key] = value;
586
402
  } catch (e) {
587
403
  errors.push(toError(e));
588
404
  }
589
405
  }
590
- if (pending2)
406
+ if (pending)
591
407
  return { ok: false, pending: true };
592
408
  if (errors.length > 0)
593
409
  return { ok: false, errors };
594
410
  return { ok: true, values };
595
411
  }
412
+ // src/state.ts
413
+ var TYPE_STATE = "State";
414
+ var createState = (initialValue) => {
415
+ if (initialValue == null)
416
+ throw new NullishSignalValueError("state");
417
+ const watchers = new Set;
418
+ let value = initialValue;
419
+ const state = {
420
+ [Symbol.toStringTag]: TYPE_STATE,
421
+ get: () => {
422
+ subscribe(watchers);
423
+ return value;
424
+ },
425
+ set: (newValue) => {
426
+ if (newValue == null)
427
+ throw new NullishSignalValueError("state");
428
+ if (isEqual(value, newValue))
429
+ return;
430
+ value = newValue;
431
+ notify(watchers);
432
+ if (UNSET === value)
433
+ watchers.clear();
434
+ },
435
+ update: (updater) => {
436
+ if (!isFunction(updater))
437
+ throw new InvalidCallbackError("state update", valueString(updater));
438
+ state.set(updater(value));
439
+ }
440
+ };
441
+ return state;
442
+ };
443
+ var isState = (value) => isObjectOfType(value, TYPE_STATE);
444
+
445
+ // src/store.ts
446
+ var TYPE_STORE = "Store";
447
+ var createStore = (initialValue) => {
448
+ if (initialValue == null)
449
+ throw new NullishSignalValueError("store");
450
+ const watchers = new Set;
451
+ const listeners = {
452
+ add: new Set,
453
+ change: new Set,
454
+ remove: new Set,
455
+ sort: new Set
456
+ };
457
+ const signals = new Map;
458
+ const signalWatchers = new Map;
459
+ const isArrayLike = Array.isArray(initialValue);
460
+ const size = createState(0);
461
+ const current = () => {
462
+ const record = {};
463
+ for (const [key, signal] of signals) {
464
+ record[key] = signal.get();
465
+ }
466
+ return record;
467
+ };
468
+ const emit = (key, changes) => {
469
+ Object.freeze(changes);
470
+ for (const listener of listeners[key]) {
471
+ listener(changes);
472
+ }
473
+ };
474
+ const getSortedIndexes = () => Array.from(signals.keys()).map((k) => Number(k)).filter((n) => Number.isInteger(n)).sort((a, b) => a - b);
475
+ const isValidValue = (key, value) => {
476
+ if (value == null)
477
+ throw new NullishSignalValueError(`store for key "${key}"`);
478
+ if (value === UNSET)
479
+ return true;
480
+ if (isSymbol(value) || isFunction(value) || isComputed(value))
481
+ throw new InvalidSignalValueError(`store for key "${key}"`, valueString(value));
482
+ return true;
483
+ };
484
+ const addProperty = (key, value, single = false) => {
485
+ if (!isValidValue(key, value))
486
+ return false;
487
+ const signal = isState(value) || isStore(value) ? value : isRecord(value) || Array.isArray(value) ? createStore(value) : createState(value);
488
+ signals.set(key, signal);
489
+ const watcher = createWatcher(() => observe(() => {
490
+ emit("change", {
491
+ [key]: signal.get()
492
+ });
493
+ }, watcher));
494
+ watcher();
495
+ signalWatchers.set(key, watcher);
496
+ if (single) {
497
+ size.set(signals.size);
498
+ notify(watchers);
499
+ emit("add", {
500
+ [key]: value
501
+ });
502
+ }
503
+ return true;
504
+ };
505
+ const removeProperty = (key, single = false) => {
506
+ const ok = signals.delete(key);
507
+ if (ok) {
508
+ const watcher = signalWatchers.get(key);
509
+ if (watcher)
510
+ watcher.cleanup();
511
+ signalWatchers.delete(key);
512
+ }
513
+ if (single) {
514
+ size.set(signals.size);
515
+ notify(watchers);
516
+ emit("remove", {
517
+ [key]: UNSET
518
+ });
519
+ }
520
+ return ok;
521
+ };
522
+ const reconcile = (oldValue, newValue, initialRun) => {
523
+ const changes = diff(oldValue, newValue);
524
+ batch(() => {
525
+ if (Object.keys(changes.add).length) {
526
+ for (const key in changes.add) {
527
+ const value = changes.add[key] ?? UNSET;
528
+ addProperty(key, value);
529
+ }
530
+ if (initialRun) {
531
+ setTimeout(() => {
532
+ emit("add", changes.add);
533
+ }, 0);
534
+ } else {
535
+ emit("add", changes.add);
536
+ }
537
+ }
538
+ if (Object.keys(changes.change).length) {
539
+ for (const key in changes.change) {
540
+ const value = changes.change[key];
541
+ if (!isValidValue(key, value))
542
+ continue;
543
+ const signal = signals.get(key);
544
+ if (isMutableSignal(signal))
545
+ signal.set(value);
546
+ else
547
+ throw new StoreKeyReadonlyError(key, valueString(value));
548
+ }
549
+ emit("change", changes.change);
550
+ }
551
+ if (Object.keys(changes.remove).length) {
552
+ for (const key in changes.remove)
553
+ removeProperty(key);
554
+ emit("remove", changes.remove);
555
+ }
556
+ size.set(signals.size);
557
+ });
558
+ return changes.changed;
559
+ };
560
+ reconcile({}, initialValue, true);
561
+ const store = {
562
+ add: isArrayLike ? (v) => {
563
+ const nextIndex = signals.size;
564
+ const key = String(nextIndex);
565
+ addProperty(key, v, true);
566
+ } : (k, v) => {
567
+ if (!signals.has(k))
568
+ addProperty(k, v, true);
569
+ else
570
+ throw new StoreKeyExistsError(k, valueString(v));
571
+ },
572
+ get: () => {
573
+ subscribe(watchers);
574
+ return recordToArray(current());
575
+ },
576
+ remove: isArrayLike ? (index) => {
577
+ const currentArray = recordToArray(current());
578
+ const currentLength = signals.size;
579
+ if (!Array.isArray(currentArray) || index <= -currentLength || index >= currentLength)
580
+ throw new StoreKeyRangeError(index);
581
+ const newArray = [...currentArray];
582
+ newArray.splice(index, 1);
583
+ if (reconcile(currentArray, newArray))
584
+ notify(watchers);
585
+ } : (k) => {
586
+ if (signals.has(k))
587
+ removeProperty(k, true);
588
+ },
589
+ set: (v) => {
590
+ if (reconcile(current(), v)) {
591
+ notify(watchers);
592
+ if (UNSET === v)
593
+ watchers.clear();
594
+ }
595
+ },
596
+ update: (fn) => {
597
+ const oldValue = current();
598
+ const newValue = fn(recordToArray(oldValue));
599
+ if (reconcile(oldValue, newValue)) {
600
+ notify(watchers);
601
+ if (UNSET === newValue)
602
+ watchers.clear();
603
+ }
604
+ },
605
+ sort: (compareFn) => {
606
+ const entries = Array.from(signals.entries()).map(([key, signal]) => [key, signal.get()]).sort(compareFn ? (a, b) => compareFn(a[1], b[1]) : (a, b) => String(a[1]).localeCompare(String(b[1])));
607
+ const newOrder = entries.map(([key]) => String(key));
608
+ const newSignals = new Map;
609
+ entries.forEach(([key], newIndex) => {
610
+ const oldKey = String(key);
611
+ const newKey = isArrayLike ? String(newIndex) : String(key);
612
+ const signal = signals.get(oldKey);
613
+ if (signal)
614
+ newSignals.set(newKey, signal);
615
+ });
616
+ signals.clear();
617
+ newSignals.forEach((signal, key) => signals.set(key, signal));
618
+ notify(watchers);
619
+ emit("sort", newOrder);
620
+ },
621
+ on: (type, listener) => {
622
+ listeners[type].add(listener);
623
+ return () => listeners[type].delete(listener);
624
+ },
625
+ size
626
+ };
627
+ return new Proxy({}, {
628
+ get(_target, prop) {
629
+ if (prop === Symbol.toStringTag)
630
+ return TYPE_STORE;
631
+ if (prop === Symbol.isConcatSpreadable)
632
+ return isArrayLike;
633
+ if (prop === Symbol.iterator)
634
+ return isArrayLike ? function* () {
635
+ const indexes = getSortedIndexes();
636
+ for (const index of indexes) {
637
+ const signal = signals.get(String(index));
638
+ if (signal)
639
+ yield signal;
640
+ }
641
+ } : function* () {
642
+ for (const [key, signal] of signals)
643
+ yield [key, signal];
644
+ };
645
+ if (isSymbol(prop))
646
+ return;
647
+ if (prop in store)
648
+ return store[prop];
649
+ if (prop === "length" && isArrayLike) {
650
+ subscribe(watchers);
651
+ return size.get();
652
+ }
653
+ return signals.get(prop);
654
+ },
655
+ has(_target, prop) {
656
+ const stringProp = String(prop);
657
+ return stringProp && signals.has(stringProp) || Object.keys(store).includes(stringProp) || prop === Symbol.toStringTag || prop === Symbol.iterator || prop === Symbol.isConcatSpreadable || prop === "length" && isArrayLike;
658
+ },
659
+ ownKeys() {
660
+ return isArrayLike ? getSortedIndexes().map((key) => String(key)).concat(["length"]) : Array.from(signals.keys()).map((key) => String(key));
661
+ },
662
+ getOwnPropertyDescriptor(_target, prop) {
663
+ const nonEnumerable = (value) => ({
664
+ enumerable: false,
665
+ configurable: true,
666
+ writable: false,
667
+ value
668
+ });
669
+ if (prop === "length" && isArrayLike)
670
+ return {
671
+ enumerable: true,
672
+ configurable: true,
673
+ writable: false,
674
+ value: size.get()
675
+ };
676
+ if (prop === Symbol.isConcatSpreadable)
677
+ return nonEnumerable(isArrayLike);
678
+ if (prop === Symbol.toStringTag)
679
+ return nonEnumerable(TYPE_STORE);
680
+ if (isSymbol(prop))
681
+ return;
682
+ if (Object.keys(store).includes(prop))
683
+ return nonEnumerable(store[prop]);
684
+ const signal = signals.get(prop);
685
+ return signal ? {
686
+ enumerable: true,
687
+ configurable: true,
688
+ writable: true,
689
+ value: signal
690
+ } : undefined;
691
+ }
692
+ });
693
+ };
694
+ var isStore = (value) => isObjectOfType(value, TYPE_STORE);
695
+
696
+ // src/signal.ts
697
+ var isSignal = (value) => isState(value) || isComputed(value) || isStore(value);
698
+ var isMutableSignal = (value) => isState(value) || isStore(value);
699
+ function toSignal(value) {
700
+ if (isSignal(value))
701
+ return value;
702
+ if (isComputedCallback(value))
703
+ return createComputed(value);
704
+ if (Array.isArray(value) || isRecord(value))
705
+ return createStore(value);
706
+ return createState(value);
707
+ }
596
708
  export {
597
- watch,
709
+ valueString,
598
710
  toSignal,
599
- toMutableSignal,
600
711
  toError,
601
712
  subscribe,
602
- store,
603
- state,
604
713
  resolve,
605
714
  observe,
606
715
  notify,
607
716
  match,
717
+ isSymbol,
608
718
  isString,
609
719
  isStore,
610
720
  isState,
611
721
  isSignal,
722
+ isRecordOrArray,
723
+ isRecord,
612
724
  isNumber,
725
+ isMutableSignal,
613
726
  isFunction,
614
727
  isEqual,
615
728
  isComputedCallback,
@@ -617,14 +730,22 @@ export {
617
730
  isAsyncFunction,
618
731
  isAbortError,
619
732
  flush,
620
- enqueue,
621
- effect,
622
733
  diff,
623
- computed,
734
+ createWatcher,
735
+ createStore,
736
+ createState,
737
+ createEffect,
738
+ createComputed,
624
739
  batch,
625
740
  UNSET,
626
741
  TYPE_STORE,
627
742
  TYPE_STATE,
628
743
  TYPE_COMPUTED,
744
+ StoreKeyReadonlyError,
745
+ StoreKeyRangeError,
746
+ StoreKeyExistsError,
747
+ NullishSignalValueError,
748
+ InvalidSignalValueError,
749
+ InvalidCallbackError,
629
750
  CircularDependencyError
630
751
  };