@zeix/cause-effect 0.17.0 → 0.17.2

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 (50) hide show
  1. package/.ai-context.md +26 -5
  2. package/.cursorrules +8 -3
  3. package/.github/copilot-instructions.md +13 -4
  4. package/CLAUDE.md +191 -262
  5. package/README.md +268 -420
  6. package/archive/collection.ts +23 -25
  7. package/archive/computed.ts +5 -4
  8. package/archive/list.ts +21 -28
  9. package/archive/memo.ts +4 -2
  10. package/archive/state.ts +2 -1
  11. package/archive/store.ts +21 -32
  12. package/archive/task.ts +6 -9
  13. package/index.dev.js +411 -220
  14. package/index.js +1 -1
  15. package/index.ts +25 -8
  16. package/package.json +1 -1
  17. package/src/classes/collection.ts +103 -77
  18. package/src/classes/composite.ts +28 -33
  19. package/src/classes/computed.ts +90 -31
  20. package/src/classes/list.ts +39 -33
  21. package/src/classes/ref.ts +96 -0
  22. package/src/classes/state.ts +41 -8
  23. package/src/classes/store.ts +47 -30
  24. package/src/diff.ts +2 -1
  25. package/src/effect.ts +19 -9
  26. package/src/errors.ts +31 -1
  27. package/src/match.ts +5 -12
  28. package/src/resolve.ts +3 -2
  29. package/src/signal.ts +0 -1
  30. package/src/system.ts +159 -43
  31. package/src/util.ts +0 -10
  32. package/test/collection.test.ts +383 -67
  33. package/test/computed.test.ts +268 -11
  34. package/test/effect.test.ts +2 -2
  35. package/test/list.test.ts +249 -21
  36. package/test/ref.test.ts +381 -0
  37. package/test/state.test.ts +13 -13
  38. package/test/store.test.ts +473 -28
  39. package/types/index.d.ts +6 -5
  40. package/types/src/classes/collection.d.ts +27 -12
  41. package/types/src/classes/composite.d.ts +4 -4
  42. package/types/src/classes/computed.d.ts +17 -0
  43. package/types/src/classes/list.d.ts +6 -6
  44. package/types/src/classes/ref.d.ts +48 -0
  45. package/types/src/classes/state.d.ts +9 -0
  46. package/types/src/classes/store.d.ts +4 -4
  47. package/types/src/effect.d.ts +1 -2
  48. package/types/src/errors.d.ts +9 -1
  49. package/types/src/system.d.ts +40 -24
  50. package/types/src/util.d.ts +1 -3
package/index.dev.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // src/util.ts
2
- var UNSET = Symbol();
3
2
  var isString = (value) => typeof value === "string";
4
3
  var isNumber = (value) => typeof value === "number";
5
4
  var isSymbol = (value) => typeof value === "symbol";
@@ -12,9 +11,145 @@ var isRecord = (value) => isObjectOfType(value, "Object");
12
11
  var isRecordOrArray = (value) => isRecord(value) || Array.isArray(value);
13
12
  var isUniformArray = (value, guard = (item) => item != null) => Array.isArray(value) && value.every(guard);
14
13
  var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError";
15
- var toError = (reason) => reason instanceof Error ? reason : Error(String(reason));
16
14
  var valueString = (value) => isString(value) ? `"${value}"` : !!value && typeof value === "object" ? JSON.stringify(value) : String(value);
17
15
 
16
+ // src/system.ts
17
+ var activeWatcher;
18
+ var unwatchMap = new WeakMap;
19
+ var pendingReactions = new Set;
20
+ var batchDepth = 0;
21
+ var UNSET = Symbol();
22
+ var HOOK_ADD = "add";
23
+ var HOOK_CHANGE = "change";
24
+ var HOOK_CLEANUP = "cleanup";
25
+ var HOOK_REMOVE = "remove";
26
+ var HOOK_SORT = "sort";
27
+ var HOOK_WATCH = "watch";
28
+ var createWatcher = (react) => {
29
+ const cleanups = new Set;
30
+ const watcher = react;
31
+ watcher.on = (type, cleanup) => {
32
+ if (type === HOOK_CLEANUP)
33
+ cleanups.add(cleanup);
34
+ else
35
+ throw new InvalidHookError("watcher", type);
36
+ };
37
+ watcher.stop = () => {
38
+ try {
39
+ for (const cleanup of cleanups)
40
+ cleanup();
41
+ } finally {
42
+ cleanups.clear();
43
+ }
44
+ };
45
+ return watcher;
46
+ };
47
+ var subscribeActiveWatcher = (watchers, watchHookCallbacks) => {
48
+ if (!watchers.size && watchHookCallbacks?.size) {
49
+ const unwatch = triggerHook(watchHookCallbacks);
50
+ if (unwatch) {
51
+ const unwatchCallbacks = unwatchMap.get(watchers) ?? new Set;
52
+ unwatchCallbacks.add(unwatch);
53
+ if (!unwatchMap.has(watchers))
54
+ unwatchMap.set(watchers, unwatchCallbacks);
55
+ }
56
+ }
57
+ if (activeWatcher && !watchers.has(activeWatcher)) {
58
+ const watcher = activeWatcher;
59
+ watcher.on(HOOK_CLEANUP, () => {
60
+ watchers.delete(watcher);
61
+ if (!watchers.size) {
62
+ const unwatchCallbacks = unwatchMap.get(watchers);
63
+ if (unwatchCallbacks) {
64
+ try {
65
+ for (const unwatch of unwatchCallbacks)
66
+ unwatch();
67
+ } finally {
68
+ unwatchCallbacks.clear();
69
+ unwatchMap.delete(watchers);
70
+ }
71
+ }
72
+ }
73
+ });
74
+ watchers.add(watcher);
75
+ }
76
+ };
77
+ var notifyWatchers = (watchers) => {
78
+ if (!watchers.size)
79
+ return false;
80
+ for (const react of watchers) {
81
+ if (batchDepth)
82
+ pendingReactions.add(react);
83
+ else
84
+ react();
85
+ }
86
+ return true;
87
+ };
88
+ var flushPendingReactions = () => {
89
+ while (pendingReactions.size) {
90
+ const watchers = Array.from(pendingReactions);
91
+ pendingReactions.clear();
92
+ for (const watcher of watchers)
93
+ watcher();
94
+ }
95
+ };
96
+ var batchSignalWrites = (callback) => {
97
+ batchDepth++;
98
+ try {
99
+ callback();
100
+ } finally {
101
+ flushPendingReactions();
102
+ batchDepth--;
103
+ }
104
+ };
105
+ var trackSignalReads = (watcher, run) => {
106
+ const prev = activeWatcher;
107
+ activeWatcher = watcher || undefined;
108
+ try {
109
+ run();
110
+ } finally {
111
+ activeWatcher = prev;
112
+ }
113
+ };
114
+ var triggerHook = (callbacks, payload) => {
115
+ if (!callbacks)
116
+ return;
117
+ const cleanups = [];
118
+ const errors = [];
119
+ const throwError = (inCleanup) => {
120
+ if (errors.length) {
121
+ if (errors.length === 1)
122
+ throw errors[0];
123
+ throw new AggregateError(errors, `Errors in hook ${inCleanup ? "cleanup" : "callback"}:`);
124
+ }
125
+ };
126
+ for (const callback of callbacks) {
127
+ try {
128
+ const cleanup = callback(payload);
129
+ if (isFunction(cleanup))
130
+ cleanups.push(cleanup);
131
+ } catch (error) {
132
+ errors.push(createError(error));
133
+ }
134
+ }
135
+ throwError();
136
+ if (!cleanups.length)
137
+ return;
138
+ if (cleanups.length === 1)
139
+ return cleanups[0];
140
+ return () => {
141
+ for (const cleanup of cleanups) {
142
+ try {
143
+ cleanup();
144
+ } catch (error) {
145
+ errors.push(createError(error));
146
+ }
147
+ }
148
+ throwError(true);
149
+ };
150
+ };
151
+ var isHandledHook = (type, handled) => handled.includes(type);
152
+
18
153
  // src/diff.ts
19
154
  var isEqual = (a, b, visited) => {
20
155
  if (Object.is(a, b))
@@ -102,73 +237,6 @@ var diff = (oldObj, newObj) => {
102
237
  };
103
238
  };
104
239
 
105
- // src/system.ts
106
- var activeWatcher;
107
- var pendingReactions = new Set;
108
- var batchDepth = 0;
109
- var createWatcher = (react) => {
110
- const cleanups = new Set;
111
- const watcher = react;
112
- watcher.onCleanup = (cleanup) => {
113
- cleanups.add(cleanup);
114
- };
115
- watcher.stop = () => {
116
- for (const cleanup of cleanups)
117
- cleanup();
118
- cleanups.clear();
119
- };
120
- return watcher;
121
- };
122
- var subscribeActiveWatcher = (watchers) => {
123
- if (activeWatcher && !watchers.has(activeWatcher)) {
124
- const watcher = activeWatcher;
125
- watcher.onCleanup(() => watchers.delete(watcher));
126
- watchers.add(watcher);
127
- }
128
- };
129
- var notifyWatchers = (watchers) => {
130
- for (const react of watchers) {
131
- if (batchDepth)
132
- pendingReactions.add(react);
133
- else
134
- react();
135
- }
136
- };
137
- var flushPendingReactions = () => {
138
- while (pendingReactions.size) {
139
- const watchers = Array.from(pendingReactions);
140
- pendingReactions.clear();
141
- for (const watcher of watchers)
142
- watcher();
143
- }
144
- };
145
- var batchSignalWrites = (callback) => {
146
- batchDepth++;
147
- try {
148
- callback();
149
- } finally {
150
- flushPendingReactions();
151
- batchDepth--;
152
- }
153
- };
154
- var trackSignalReads = (watcher, run) => {
155
- const prev = activeWatcher;
156
- activeWatcher = watcher || undefined;
157
- try {
158
- run();
159
- } finally {
160
- activeWatcher = prev;
161
- }
162
- };
163
- var emitNotification = (listeners, payload) => {
164
- for (const listener of listeners) {
165
- if (batchDepth)
166
- pendingReactions.add(() => listener(payload));
167
- else
168
- listener(payload);
169
- }
170
- };
171
-
172
240
  // src/classes/computed.ts
173
241
  var TYPE_COMPUTED = "Computed";
174
242
 
@@ -180,27 +248,35 @@ class Memo {
180
248
  #dirty = true;
181
249
  #computing = false;
182
250
  #watcher;
251
+ #watchHookCallbacks;
183
252
  constructor(callback, initialValue = UNSET) {
184
- validateCallback("memo", callback, isMemoCallback);
185
- validateSignalValue("memo", initialValue);
253
+ validateCallback(this.constructor.name, callback, isMemoCallback);
254
+ validateSignalValue(this.constructor.name, initialValue);
186
255
  this.#callback = callback;
187
256
  this.#value = initialValue;
188
- this.#watcher = createWatcher(() => {
189
- this.#dirty = true;
190
- if (this.#watchers.size)
191
- notifyWatchers(this.#watchers);
192
- else
193
- this.#watcher.stop();
194
- });
257
+ }
258
+ #getWatcher() {
259
+ if (!this.#watcher) {
260
+ this.#watcher = createWatcher(() => {
261
+ this.#dirty = true;
262
+ if (!notifyWatchers(this.#watchers))
263
+ this.#watcher?.stop();
264
+ });
265
+ this.#watcher.on(HOOK_CLEANUP, () => {
266
+ this.#watcher = undefined;
267
+ });
268
+ }
269
+ return this.#watcher;
195
270
  }
196
271
  get [Symbol.toStringTag]() {
197
272
  return TYPE_COMPUTED;
198
273
  }
199
274
  get() {
200
- subscribeActiveWatcher(this.#watchers);
275
+ subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks);
201
276
  flushPendingReactions();
202
277
  if (this.#dirty) {
203
- trackSignalReads(this.#watcher, () => {
278
+ const watcher = this.#getWatcher();
279
+ trackSignalReads(watcher, () => {
204
280
  if (this.#computing)
205
281
  throw new CircularDependencyError("memo");
206
282
  let result;
@@ -209,7 +285,7 @@ class Memo {
209
285
  result = this.#callback(this.#value);
210
286
  } catch (e) {
211
287
  this.#value = UNSET;
212
- this.#error = toError(e);
288
+ this.#error = createError(e);
213
289
  this.#computing = false;
214
290
  return;
215
291
  }
@@ -228,6 +304,16 @@ class Memo {
228
304
  throw this.#error;
229
305
  return this.#value;
230
306
  }
307
+ on(type, callback) {
308
+ if (type === HOOK_WATCH) {
309
+ this.#watchHookCallbacks ||= new Set;
310
+ this.#watchHookCallbacks.add(callback);
311
+ return () => {
312
+ this.#watchHookCallbacks?.delete(callback);
313
+ };
314
+ }
315
+ throw new InvalidHookError(this.constructor.name, type);
316
+ }
231
317
  }
232
318
 
233
319
  class Task {
@@ -240,28 +326,34 @@ class Task {
240
326
  #changed = false;
241
327
  #watcher;
242
328
  #controller;
329
+ #watchHookCallbacks;
243
330
  constructor(callback, initialValue = UNSET) {
244
- validateCallback("task", callback, isTaskCallback);
245
- validateSignalValue("task", initialValue);
331
+ validateCallback(this.constructor.name, callback, isTaskCallback);
332
+ validateSignalValue(this.constructor.name, initialValue);
246
333
  this.#callback = callback;
247
334
  this.#value = initialValue;
248
- this.#watcher = createWatcher(() => {
249
- this.#dirty = true;
250
- this.#controller?.abort();
251
- if (this.#watchers.size)
252
- notifyWatchers(this.#watchers);
253
- else
254
- this.#watcher.stop();
255
- });
256
- this.#watcher.onCleanup(() => {
257
- this.#controller?.abort();
258
- });
335
+ }
336
+ #getWatcher() {
337
+ if (!this.#watcher) {
338
+ this.#watcher = createWatcher(() => {
339
+ this.#dirty = true;
340
+ this.#controller?.abort();
341
+ if (!notifyWatchers(this.#watchers))
342
+ this.#watcher?.stop();
343
+ });
344
+ this.#watcher.on(HOOK_CLEANUP, () => {
345
+ this.#controller?.abort();
346
+ this.#controller = undefined;
347
+ this.#watcher = undefined;
348
+ });
349
+ }
350
+ return this.#watcher;
259
351
  }
260
352
  get [Symbol.toStringTag]() {
261
353
  return TYPE_COMPUTED;
262
354
  }
263
355
  get() {
264
- subscribeActiveWatcher(this.#watchers);
356
+ subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks);
265
357
  flushPendingReactions();
266
358
  const ok = (v) => {
267
359
  if (!isEqual(v, this.#value)) {
@@ -277,7 +369,7 @@ class Task {
277
369
  this.#error = undefined;
278
370
  };
279
371
  const err = (e) => {
280
- const newError = toError(e);
372
+ const newError = createError(e);
281
373
  this.#changed = !this.#error || newError.name !== this.#error.name || newError.message !== this.#error.message;
282
374
  this.#value = UNSET;
283
375
  this.#error = newError;
@@ -286,10 +378,10 @@ class Task {
286
378
  this.#computing = false;
287
379
  this.#controller = undefined;
288
380
  fn(arg);
289
- if (this.#changed)
290
- notifyWatchers(this.#watchers);
381
+ if (this.#changed && !notifyWatchers(this.#watchers))
382
+ this.#watcher?.stop();
291
383
  };
292
- const compute = () => trackSignalReads(this.#watcher, () => {
384
+ const compute = () => trackSignalReads(this.#getWatcher(), () => {
293
385
  if (this.#computing)
294
386
  throw new CircularDependencyError("task");
295
387
  this.#changed = false;
@@ -329,6 +421,16 @@ class Task {
329
421
  throw this.#error;
330
422
  return this.#value;
331
423
  }
424
+ on(type, callback) {
425
+ if (type === HOOK_WATCH) {
426
+ this.#watchHookCallbacks ||= new Set;
427
+ this.#watchHookCallbacks.add(callback);
428
+ return () => {
429
+ this.#watchHookCallbacks?.delete(callback);
430
+ };
431
+ }
432
+ throw new InvalidHookError(this.constructor.name, type);
433
+ }
332
434
  }
333
435
  var createComputed = (callback, initialValue = UNSET) => isAsyncFunction(callback) ? new Task(callback, initialValue) : new Memo(callback, initialValue);
334
436
  var isComputed = (value) => isObjectOfType(value, TYPE_COMPUTED);
@@ -341,11 +443,7 @@ class Composite {
341
443
  #validate;
342
444
  #create;
343
445
  #watchers = new Map;
344
- #listeners = {
345
- add: new Set,
346
- change: new Set,
347
- remove: new Set
348
- };
446
+ #hookCallbacks = {};
349
447
  #batching = false;
350
448
  constructor(values, validate, create) {
351
449
  this.#validate = validate;
@@ -362,7 +460,7 @@ class Composite {
362
460
  trackSignalReads(watcher, () => {
363
461
  this.signals.get(key)?.get();
364
462
  if (!this.#batching)
365
- emitNotification(this.#listeners.change, [key]);
463
+ triggerHook(this.#hookCallbacks.change, [key]);
366
464
  });
367
465
  });
368
466
  this.#watchers.set(key, watcher);
@@ -372,10 +470,10 @@ class Composite {
372
470
  if (!this.#validate(key, value))
373
471
  return false;
374
472
  this.signals.set(key, this.#create(value));
375
- if (this.#listeners.change.size)
473
+ if (this.#hookCallbacks.change?.size)
376
474
  this.#addWatcher(key);
377
475
  if (!this.#batching)
378
- emitNotification(this.#listeners.add, [key]);
476
+ triggerHook(this.#hookCallbacks.add, [key]);
379
477
  return true;
380
478
  }
381
479
  remove(key) {
@@ -388,7 +486,7 @@ class Composite {
388
486
  this.#watchers.delete(key);
389
487
  }
390
488
  if (!this.#batching)
391
- emitNotification(this.#listeners.remove, [key]);
489
+ triggerHook(this.#hookCallbacks.remove, [key]);
392
490
  return true;
393
491
  }
394
492
  change(changes, initialRun) {
@@ -396,7 +494,7 @@ class Composite {
396
494
  if (Object.keys(changes.add).length) {
397
495
  for (const key in changes.add)
398
496
  this.add(key, changes.add[key]);
399
- const notify = () => emitNotification(this.#listeners.add, Object.keys(changes.add));
497
+ const notify = () => triggerHook(this.#hookCallbacks.add, Object.keys(changes.add));
400
498
  if (initialRun)
401
499
  setTimeout(notify, 0);
402
500
  else
@@ -413,12 +511,12 @@ class Composite {
413
511
  signal.set(value);
414
512
  }
415
513
  });
416
- emitNotification(this.#listeners.change, Object.keys(changes.change));
514
+ triggerHook(this.#hookCallbacks.change, Object.keys(changes.change));
417
515
  }
418
516
  if (Object.keys(changes.remove).length) {
419
517
  for (const key in changes.remove)
420
518
  this.remove(key);
421
- emitNotification(this.#listeners.remove, Object.keys(changes.remove));
519
+ triggerHook(this.#hookCallbacks.remove, Object.keys(changes.remove));
422
520
  }
423
521
  this.#batching = false;
424
522
  return changes.changed;
@@ -427,20 +525,23 @@ class Composite {
427
525
  const keys = Array.from(this.signals.keys());
428
526
  this.signals.clear();
429
527
  this.#watchers.clear();
430
- emitNotification(this.#listeners.remove, keys);
528
+ triggerHook(this.#hookCallbacks.remove, keys);
431
529
  return true;
432
530
  }
433
- on(type, listener) {
434
- this.#listeners[type].add(listener);
435
- if (type === "change" && !this.#watchers.size) {
531
+ on(type, callback) {
532
+ if (!isHandledHook(type, [HOOK_ADD, HOOK_CHANGE, HOOK_REMOVE]))
533
+ throw new InvalidHookError("Composite", type);
534
+ this.#hookCallbacks[type] ||= new Set;
535
+ this.#hookCallbacks[type].add(callback);
536
+ if (type === HOOK_CHANGE && !this.#watchers.size) {
436
537
  this.#batching = true;
437
538
  for (const key of this.signals.keys())
438
539
  this.#addWatcher(key);
439
540
  this.#batching = false;
440
541
  }
441
542
  return () => {
442
- this.#listeners[type].delete(listener);
443
- if (type === "change" && !this.#listeners.change.size) {
543
+ this.#hookCallbacks[type]?.delete(callback);
544
+ if (type === HOOK_CHANGE && !this.#hookCallbacks.change?.size) {
444
545
  if (this.#watchers.size) {
445
546
  for (const watcher of this.#watchers.values())
446
547
  watcher.stop();
@@ -457,30 +558,42 @@ var TYPE_STATE = "State";
457
558
  class State {
458
559
  #watchers = new Set;
459
560
  #value;
561
+ #watchHookCallbacks;
460
562
  constructor(initialValue) {
461
- validateSignalValue("state", initialValue);
563
+ validateSignalValue(TYPE_STATE, initialValue);
462
564
  this.#value = initialValue;
463
565
  }
464
566
  get [Symbol.toStringTag]() {
465
567
  return TYPE_STATE;
466
568
  }
467
569
  get() {
468
- subscribeActiveWatcher(this.#watchers);
570
+ subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks);
469
571
  return this.#value;
470
572
  }
471
573
  set(newValue) {
472
- validateSignalValue("state", newValue);
574
+ validateSignalValue(TYPE_STATE, newValue);
473
575
  if (isEqual(this.#value, newValue))
474
576
  return;
475
577
  this.#value = newValue;
476
- notifyWatchers(this.#watchers);
578
+ if (this.#watchers.size)
579
+ notifyWatchers(this.#watchers);
477
580
  if (UNSET === this.#value)
478
581
  this.#watchers.clear();
479
582
  }
480
583
  update(updater) {
481
- validateCallback("state update", updater);
584
+ validateCallback(`${TYPE_STATE} update`, updater);
482
585
  this.set(updater(this.#value));
483
586
  }
587
+ on(type, callback) {
588
+ if (type === HOOK_WATCH) {
589
+ this.#watchHookCallbacks ||= new Set;
590
+ this.#watchHookCallbacks.add(callback);
591
+ return () => {
592
+ this.#watchHookCallbacks?.delete(callback);
593
+ };
594
+ }
595
+ throw new InvalidHookError(this.constructor.name, type);
596
+ }
484
597
  }
485
598
  var isState = (value) => isObjectOfType(value, TYPE_STATE);
486
599
 
@@ -490,17 +603,15 @@ var TYPE_LIST = "List";
490
603
  class List {
491
604
  #composite;
492
605
  #watchers = new Set;
493
- #listeners = {
494
- sort: new Set
495
- };
606
+ #hookCallbacks = {};
496
607
  #order = [];
497
608
  #generateKey;
498
609
  constructor(initialValue, keyConfig) {
499
- validateSignalValue("list", initialValue, Array.isArray);
610
+ validateSignalValue(TYPE_LIST, initialValue, Array.isArray);
500
611
  let keyCounter = 0;
501
612
  this.#generateKey = isString(keyConfig) ? () => `${keyConfig}${keyCounter++}` : isFunction(keyConfig) ? (item) => keyConfig(item) : () => String(keyCounter++);
502
613
  this.#composite = new Composite(this.#toRecord(initialValue), (key, value) => {
503
- validateSignalValue(`list for key "${key}"`, value);
614
+ validateSignalValue(`${TYPE_LIST} for key "${key}"`, value);
504
615
  return true;
505
616
  }, (value) => new State(value));
506
617
  }
@@ -536,11 +647,11 @@ class List {
536
647
  }
537
648
  }
538
649
  get length() {
539
- subscribeActiveWatcher(this.#watchers);
650
+ subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH]);
540
651
  return this.#order.length;
541
652
  }
542
653
  get() {
543
- subscribeActiveWatcher(this.#watchers);
654
+ subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH]);
544
655
  return this.#value;
545
656
  }
546
657
  set(newValue) {
@@ -610,7 +721,7 @@ class List {
610
721
  if (!isEqual(this.#order, newOrder)) {
611
722
  this.#order = newOrder;
612
723
  notifyWatchers(this.#watchers);
613
- emitNotification(this.#listeners.sort, this.#order);
724
+ triggerHook(this.#hookCallbacks.sort, this.#order);
614
725
  }
615
726
  }
616
727
  splice(start, deleteCount, ...items) {
@@ -648,15 +759,20 @@ class List {
648
759
  }
649
760
  return Object.values(remove);
650
761
  }
651
- on(type, listener) {
652
- if (type === "sort") {
653
- this.#listeners.sort.add(listener);
654
- return () => this.#listeners.sort.delete(listener);
762
+ on(type, callback) {
763
+ if (isHandledHook(type, [HOOK_SORT, HOOK_WATCH])) {
764
+ this.#hookCallbacks[type] ||= new Set;
765
+ this.#hookCallbacks[type].add(callback);
766
+ return () => {
767
+ this.#hookCallbacks[type]?.delete(callback);
768
+ };
769
+ } else if (isHandledHook(type, [HOOK_ADD, HOOK_CHANGE, HOOK_REMOVE])) {
770
+ return this.#composite.on(type, callback);
655
771
  }
656
- return this.#composite.on(type, listener);
772
+ throw new InvalidHookError(TYPE_LIST, type);
657
773
  }
658
774
  deriveCollection(callback) {
659
- return new Collection(this, callback);
775
+ return new DerivedCollection(this, callback);
660
776
  }
661
777
  }
662
778
  var isList = (value) => isObjectOfType(value, TYPE_LIST);
@@ -667,10 +783,11 @@ var TYPE_STORE = "Store";
667
783
  class BaseStore {
668
784
  #composite;
669
785
  #watchers = new Set;
786
+ #watchHookCallbacks;
670
787
  constructor(initialValue) {
671
- validateSignalValue("store", initialValue, isRecord);
788
+ validateSignalValue(TYPE_STORE, initialValue, isRecord);
672
789
  this.#composite = new Composite(initialValue, (key, value) => {
673
- validateSignalValue(`store for key "${key}"`, value);
790
+ validateSignalValue(`${TYPE_STORE} for key "${key}"`, value);
674
791
  return true;
675
792
  }, (value) => createMutableSignal(value));
676
793
  }
@@ -690,8 +807,14 @@ class BaseStore {
690
807
  for (const [key, signal] of this.#composite.signals.entries())
691
808
  yield [key, signal];
692
809
  }
810
+ keys() {
811
+ return this.#composite.signals.keys();
812
+ }
813
+ byKey(key) {
814
+ return this.#composite.signals.get(key);
815
+ }
693
816
  get() {
694
- subscribeActiveWatcher(this.#watchers);
817
+ subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks);
695
818
  return this.#value;
696
819
  }
697
820
  set(newValue) {
@@ -706,18 +829,12 @@ class BaseStore {
706
829
  if (changed)
707
830
  notifyWatchers(this.#watchers);
708
831
  }
709
- keys() {
710
- return this.#composite.signals.keys();
711
- }
712
- byKey(key) {
713
- return this.#composite.signals.get(key);
714
- }
715
832
  update(fn) {
716
833
  this.set(fn(this.get()));
717
834
  }
718
835
  add(key, value) {
719
836
  if (this.#composite.signals.has(key))
720
- throw new DuplicateKeyError("store", key, value);
837
+ throw new DuplicateKeyError(TYPE_STORE, key, value);
721
838
  const ok = this.#composite.add(key, value);
722
839
  if (ok)
723
840
  notifyWatchers(this.#watchers);
@@ -728,8 +845,17 @@ class BaseStore {
728
845
  if (ok)
729
846
  notifyWatchers(this.#watchers);
730
847
  }
731
- on(type, listener) {
732
- return this.#composite.on(type, listener);
848
+ on(type, callback) {
849
+ if (type === HOOK_WATCH) {
850
+ this.#watchHookCallbacks ||= new Set;
851
+ this.#watchHookCallbacks.add(callback);
852
+ return () => {
853
+ this.#watchHookCallbacks?.delete(callback);
854
+ };
855
+ } else if (isHandledHook(type, [HOOK_ADD, HOOK_CHANGE, HOOK_REMOVE])) {
856
+ return this.#composite.on(type, callback);
857
+ }
858
+ throw new InvalidHookError(TYPE_STORE, type);
733
859
  }
734
860
  }
735
861
  var createStore = (initialValue) => {
@@ -812,6 +938,20 @@ class InvalidCallbackError extends TypeError {
812
938
  }
813
939
  }
814
940
 
941
+ class InvalidCollectionSourceError extends TypeError {
942
+ constructor(where, value) {
943
+ super(`Invalid ${where} source ${valueString(value)}`);
944
+ this.name = "InvalidCollectionSourceError";
945
+ }
946
+ }
947
+
948
+ class InvalidHookError extends TypeError {
949
+ constructor(where, type) {
950
+ super(`Invalid hook "${type}" in ${where}`);
951
+ this.name = "InvalidHookError";
952
+ }
953
+ }
954
+
815
955
  class InvalidSignalValueError extends TypeError {
816
956
  constructor(where, value) {
817
957
  super(`Invalid signal value ${valueString(value)} in ${where}`);
@@ -832,6 +972,7 @@ class ReadonlySignalError extends Error {
832
972
  this.name = "ReadonlySignalError";
833
973
  }
834
974
  }
975
+ var createError = (reason) => reason instanceof Error ? reason : Error(String(reason));
835
976
  var validateCallback = (where, value, guard = isFunction) => {
836
977
  if (!guard(value))
837
978
  throw new InvalidCallbackError(where, value);
@@ -851,25 +992,20 @@ var guardMutableSignal = (what, value, signal) => {
851
992
  // src/classes/collection.ts
852
993
  var TYPE_COLLECTION = "Collection";
853
994
 
854
- class Collection {
995
+ class DerivedCollection {
855
996
  #watchers = new Set;
856
997
  #source;
857
998
  #callback;
858
999
  #signals = new Map;
859
1000
  #ownWatchers = new Map;
860
- #listeners = {
861
- add: new Set,
862
- change: new Set,
863
- remove: new Set,
864
- sort: new Set
865
- };
1001
+ #hookCallbacks = {};
866
1002
  #order = [];
867
1003
  constructor(source, callback) {
868
- validateCallback("collection", callback);
1004
+ validateCallback(TYPE_COLLECTION, callback);
869
1005
  if (isFunction(source))
870
1006
  source = source();
871
1007
  if (!isCollectionSource(source))
872
- throw new Error("Invalid collection source");
1008
+ throw new InvalidCollectionSourceError(TYPE_COLLECTION, source);
873
1009
  this.#source = source;
874
1010
  this.#callback = callback;
875
1011
  for (let i = 0;i < this.#source.length; i++) {
@@ -878,7 +1014,9 @@ class Collection {
878
1014
  continue;
879
1015
  this.#add(key);
880
1016
  }
881
- this.#source.on("add", (additions) => {
1017
+ this.#source.on(HOOK_ADD, (additions) => {
1018
+ if (!additions)
1019
+ return;
882
1020
  for (const key of additions) {
883
1021
  if (!this.#signals.has(key)) {
884
1022
  this.#add(key);
@@ -888,9 +1026,11 @@ class Collection {
888
1026
  }
889
1027
  }
890
1028
  notifyWatchers(this.#watchers);
891
- emitNotification(this.#listeners.add, additions);
1029
+ triggerHook(this.#hookCallbacks.add, additions);
892
1030
  });
893
- this.#source.on("remove", (removals) => {
1031
+ this.#source.on(HOOK_REMOVE, (removals) => {
1032
+ if (!removals)
1033
+ return;
894
1034
  for (const key of removals) {
895
1035
  if (!this.#signals.has(key))
896
1036
  continue;
@@ -898,35 +1038,31 @@ class Collection {
898
1038
  const index = this.#order.indexOf(key);
899
1039
  if (index >= 0)
900
1040
  this.#order.splice(index, 1);
901
- this.#removeWatcher(key);
1041
+ const watcher = this.#ownWatchers.get(key);
1042
+ if (watcher) {
1043
+ watcher.stop();
1044
+ this.#ownWatchers.delete(key);
1045
+ }
902
1046
  }
903
1047
  this.#order = this.#order.filter(() => true);
904
1048
  notifyWatchers(this.#watchers);
905
- emitNotification(this.#listeners.remove, removals);
1049
+ triggerHook(this.#hookCallbacks.remove, removals);
906
1050
  });
907
- this.#source.on("sort", (newOrder) => {
908
- this.#order = [...newOrder];
1051
+ this.#source.on(HOOK_SORT, (newOrder) => {
1052
+ if (newOrder)
1053
+ this.#order = [...newOrder];
909
1054
  notifyWatchers(this.#watchers);
910
- emitNotification(this.#listeners.sort, newOrder);
1055
+ triggerHook(this.#hookCallbacks.sort, newOrder);
911
1056
  });
912
1057
  }
913
- get #value() {
914
- return this.#order.map((key) => this.#signals.get(key)?.get()).filter((v) => v != null && v !== UNSET);
915
- }
916
1058
  #add(key) {
917
1059
  const computedCallback = isAsyncCollectionCallback(this.#callback) ? async (_, abort) => {
918
- const sourceSignal = this.#source.byKey(key);
919
- if (!sourceSignal)
920
- return UNSET;
921
- const sourceValue = sourceSignal.get();
1060
+ const sourceValue = this.#source.byKey(key)?.get();
922
1061
  if (sourceValue === UNSET)
923
1062
  return UNSET;
924
1063
  return this.#callback(sourceValue, abort);
925
1064
  } : () => {
926
- const sourceSignal = this.#source.byKey(key);
927
- if (!sourceSignal)
928
- return UNSET;
929
- const sourceValue = sourceSignal.get();
1065
+ const sourceValue = this.#source.byKey(key)?.get();
930
1066
  if (sourceValue === UNSET)
931
1067
  return UNSET;
932
1068
  return this.#callback(sourceValue);
@@ -935,7 +1071,7 @@ class Collection {
935
1071
  this.#signals.set(key, signal);
936
1072
  if (!this.#order.includes(key))
937
1073
  this.#order.push(key);
938
- if (this.#listeners.change.size)
1074
+ if (this.#hookCallbacks.change?.size)
939
1075
  this.#addWatcher(key);
940
1076
  return true;
941
1077
  }
@@ -948,13 +1084,6 @@ class Collection {
948
1084
  this.#ownWatchers.set(key, watcher);
949
1085
  watcher();
950
1086
  }
951
- #removeWatcher(key) {
952
- const watcher = this.#ownWatchers.get(key);
953
- if (watcher) {
954
- watcher.stop();
955
- this.#ownWatchers.delete(key);
956
- }
957
- }
958
1087
  get [Symbol.toStringTag]() {
959
1088
  return TYPE_COLLECTION;
960
1089
  }
@@ -968,20 +1097,16 @@ class Collection {
968
1097
  yield signal;
969
1098
  }
970
1099
  }
971
- get length() {
972
- subscribeActiveWatcher(this.#watchers);
973
- return this.#order.length;
1100
+ keys() {
1101
+ return this.#order.values();
974
1102
  }
975
1103
  get() {
976
- subscribeActiveWatcher(this.#watchers);
977
- return this.#value;
1104
+ subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH]);
1105
+ return this.#order.map((key) => this.#signals.get(key)?.get()).filter((v) => v != null && v !== UNSET);
978
1106
  }
979
1107
  at(index) {
980
1108
  return this.#signals.get(this.#order[index]);
981
1109
  }
982
- keys() {
983
- return this.#order.values();
984
- }
985
1110
  byKey(key) {
986
1111
  return this.#signals.get(key);
987
1112
  }
@@ -991,30 +1116,77 @@ class Collection {
991
1116
  indexOfKey(key) {
992
1117
  return this.#order.indexOf(key);
993
1118
  }
994
- on(type, listener) {
995
- this.#listeners[type].add(listener);
996
- if (type === "change" && !this.#ownWatchers.size) {
997
- for (const key of this.#signals.keys())
998
- this.#addWatcher(key);
999
- }
1000
- return () => {
1001
- this.#listeners[type].delete(listener);
1002
- if (type === "change" && !this.#listeners.change.size) {
1003
- if (this.#ownWatchers.size) {
1004
- for (const watcher of this.#ownWatchers.values())
1005
- watcher.stop();
1006
- this.#ownWatchers.clear();
1007
- }
1119
+ on(type, callback) {
1120
+ if (isHandledHook(type, [
1121
+ HOOK_ADD,
1122
+ HOOK_CHANGE,
1123
+ HOOK_REMOVE,
1124
+ HOOK_SORT,
1125
+ HOOK_WATCH
1126
+ ])) {
1127
+ this.#hookCallbacks[type] ||= new Set;
1128
+ this.#hookCallbacks[type].add(callback);
1129
+ if (type === HOOK_CHANGE && !this.#ownWatchers.size) {
1130
+ for (const key of this.#signals.keys())
1131
+ this.#addWatcher(key);
1008
1132
  }
1009
- };
1133
+ return () => {
1134
+ this.#hookCallbacks[type]?.delete(callback);
1135
+ if (type === HOOK_CHANGE && !this.#hookCallbacks.change?.size) {
1136
+ if (this.#ownWatchers.size) {
1137
+ for (const watcher of this.#ownWatchers.values())
1138
+ watcher.stop();
1139
+ this.#ownWatchers.clear();
1140
+ }
1141
+ }
1142
+ };
1143
+ }
1144
+ throw new InvalidHookError(TYPE_COLLECTION, type);
1010
1145
  }
1011
1146
  deriveCollection(callback) {
1012
- return new Collection(this, callback);
1147
+ return new DerivedCollection(this, callback);
1148
+ }
1149
+ get length() {
1150
+ subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH]);
1151
+ return this.#order.length;
1013
1152
  }
1014
1153
  }
1015
1154
  var isCollection = (value) => isObjectOfType(value, TYPE_COLLECTION);
1016
1155
  var isCollectionSource = (value) => isList(value) || isCollection(value);
1017
1156
  var isAsyncCollectionCallback = (callback) => isAsyncFunction(callback);
1157
+ // src/classes/ref.ts
1158
+ var TYPE_REF = "Ref";
1159
+
1160
+ class Ref {
1161
+ #watchers = new Set;
1162
+ #value;
1163
+ #watchHookCallbacks;
1164
+ constructor(value, guard) {
1165
+ validateSignalValue(TYPE_REF, value, guard);
1166
+ this.#value = value;
1167
+ }
1168
+ get [Symbol.toStringTag]() {
1169
+ return TYPE_REF;
1170
+ }
1171
+ get() {
1172
+ subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks);
1173
+ return this.#value;
1174
+ }
1175
+ notify() {
1176
+ notifyWatchers(this.#watchers);
1177
+ }
1178
+ on(type, callback) {
1179
+ if (type === HOOK_WATCH) {
1180
+ this.#watchHookCallbacks ||= new Set;
1181
+ this.#watchHookCallbacks.add(callback);
1182
+ return () => {
1183
+ this.#watchHookCallbacks?.delete(callback);
1184
+ };
1185
+ }
1186
+ throw new InvalidHookError(TYPE_REF, type);
1187
+ }
1188
+ }
1189
+ var isRef = (value) => isObjectOfType(value, TYPE_REF);
1018
1190
  // src/effect.ts
1019
1191
  var createEffect = (callback) => {
1020
1192
  if (!isFunction(callback) || callback.length > 1)
@@ -1035,26 +1207,30 @@ var createEffect = (callback) => {
1035
1207
  const currentController = controller;
1036
1208
  callback(controller.signal).then((cleanup2) => {
1037
1209
  if (isFunction(cleanup2) && controller === currentController)
1038
- watcher.onCleanup(cleanup2);
1210
+ watcher.on(HOOK_CLEANUP, cleanup2);
1039
1211
  }).catch((error) => {
1040
1212
  if (!isAbortError(error))
1041
- console.error("Async effect error:", error);
1213
+ console.error("Error in async effect callback:", error);
1042
1214
  });
1043
1215
  } else {
1044
1216
  cleanup = callback();
1045
1217
  if (isFunction(cleanup))
1046
- watcher.onCleanup(cleanup);
1218
+ watcher.on(HOOK_CLEANUP, cleanup);
1047
1219
  }
1048
1220
  } catch (error) {
1049
1221
  if (!isAbortError(error))
1050
- console.error("Effect callback error:", error);
1222
+ console.error("Error in effect callback:", error);
1051
1223
  }
1052
1224
  running = false;
1053
1225
  }));
1054
1226
  watcher();
1055
1227
  return () => {
1056
1228
  controller?.abort();
1057
- watcher.stop();
1229
+ try {
1230
+ watcher.stop();
1231
+ } catch (error) {
1232
+ console.error("Error in effect cleanup:", error);
1233
+ }
1058
1234
  };
1059
1235
  };
1060
1236
  // src/match.ts
@@ -1066,9 +1242,10 @@ function match(result, handlers) {
1066
1242
  handlers.err?.(result.errors);
1067
1243
  else if (result.ok)
1068
1244
  handlers.ok(result.values);
1069
- } catch (error) {
1070
- if (handlers.err && (!result.errors || !result.errors.includes(toError(error))))
1071
- handlers.err(result.errors ? [...result.errors, toError(error)] : [toError(error)]);
1245
+ } catch (e) {
1246
+ const error = createError(e);
1247
+ if (handlers.err && (!result.errors || !result.errors.includes(error)))
1248
+ handlers.err(result.errors ? [...result.errors, error] : [error]);
1072
1249
  else
1073
1250
  throw error;
1074
1251
  }
@@ -1086,7 +1263,7 @@ function resolve(signals) {
1086
1263
  else
1087
1264
  values[key] = value;
1088
1265
  } catch (e) {
1089
- errors.push(toError(e));
1266
+ errors.push(createError(e));
1090
1267
  }
1091
1268
  }
1092
1269
  if (pending)
@@ -1097,8 +1274,10 @@ function resolve(signals) {
1097
1274
  }
1098
1275
  export {
1099
1276
  valueString,
1277
+ validateSignalValue,
1278
+ validateCallback,
1279
+ triggerHook,
1100
1280
  trackSignalReads,
1101
- toError,
1102
1281
  subscribeActiveWatcher,
1103
1282
  resolve,
1104
1283
  notifyWatchers,
@@ -1109,6 +1288,7 @@ export {
1109
1288
  isStore,
1110
1289
  isState,
1111
1290
  isSignal,
1291
+ isRef,
1112
1292
  isRecordOrArray,
1113
1293
  isRecord,
1114
1294
  isObjectOfType,
@@ -1116,18 +1296,20 @@ export {
1116
1296
  isMutableSignal,
1117
1297
  isMemoCallback,
1118
1298
  isList,
1299
+ isHandledHook,
1119
1300
  isFunction,
1120
1301
  isEqual,
1121
1302
  isComputed,
1122
1303
  isCollection,
1123
1304
  isAsyncFunction,
1124
1305
  isAbortError,
1306
+ guardMutableSignal,
1125
1307
  flushPendingReactions,
1126
- emitNotification,
1127
1308
  diff,
1128
1309
  createWatcher,
1129
1310
  createStore,
1130
1311
  createSignal,
1312
+ createError,
1131
1313
  createEffect,
1132
1314
  createComputed,
1133
1315
  batchSignalWrites,
@@ -1135,18 +1317,27 @@ export {
1135
1317
  Task,
1136
1318
  TYPE_STORE,
1137
1319
  TYPE_STATE,
1320
+ TYPE_REF,
1138
1321
  TYPE_LIST,
1139
1322
  TYPE_COMPUTED,
1140
1323
  TYPE_COLLECTION,
1141
1324
  State,
1325
+ Ref,
1142
1326
  ReadonlySignalError,
1143
1327
  NullishSignalValueError,
1144
1328
  Memo,
1145
1329
  List,
1146
1330
  InvalidSignalValueError,
1331
+ InvalidCollectionSourceError,
1147
1332
  InvalidCallbackError,
1333
+ HOOK_WATCH,
1334
+ HOOK_SORT,
1335
+ HOOK_REMOVE,
1336
+ HOOK_CLEANUP,
1337
+ HOOK_CHANGE,
1338
+ HOOK_ADD,
1148
1339
  DuplicateKeyError,
1149
- Collection,
1340
+ DerivedCollection,
1150
1341
  CircularDependencyError,
1151
1342
  BaseStore
1152
1343
  };