@zeix/cause-effect 0.17.1 → 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 (48) hide show
  1. package/.ai-context.md +7 -0
  2. package/.github/copilot-instructions.md +4 -0
  3. package/CLAUDE.md +96 -1
  4. package/README.md +44 -7
  5. package/archive/collection.ts +23 -25
  6. package/archive/computed.ts +3 -2
  7. package/archive/list.ts +21 -28
  8. package/archive/memo.ts +2 -1
  9. package/archive/state.ts +2 -1
  10. package/archive/store.ts +21 -32
  11. package/archive/task.ts +4 -7
  12. package/index.dev.js +356 -198
  13. package/index.js +1 -1
  14. package/index.ts +15 -6
  15. package/package.json +1 -1
  16. package/src/classes/collection.ts +69 -53
  17. package/src/classes/composite.ts +28 -33
  18. package/src/classes/computed.ts +87 -28
  19. package/src/classes/list.ts +31 -26
  20. package/src/classes/ref.ts +33 -5
  21. package/src/classes/state.ts +41 -8
  22. package/src/classes/store.ts +47 -30
  23. package/src/diff.ts +2 -1
  24. package/src/effect.ts +19 -9
  25. package/src/errors.ts +10 -1
  26. package/src/resolve.ts +1 -1
  27. package/src/signal.ts +0 -1
  28. package/src/system.ts +159 -43
  29. package/src/util.ts +0 -6
  30. package/test/collection.test.ts +279 -20
  31. package/test/computed.test.ts +268 -11
  32. package/test/effect.test.ts +2 -2
  33. package/test/list.test.ts +249 -21
  34. package/test/ref.test.ts +154 -0
  35. package/test/state.test.ts +13 -13
  36. package/test/store.test.ts +473 -28
  37. package/types/index.d.ts +3 -3
  38. package/types/src/classes/collection.d.ts +8 -7
  39. package/types/src/classes/composite.d.ts +4 -4
  40. package/types/src/classes/computed.d.ts +17 -0
  41. package/types/src/classes/list.d.ts +2 -2
  42. package/types/src/classes/ref.d.ts +10 -1
  43. package/types/src/classes/state.d.ts +9 -0
  44. package/types/src/classes/store.d.ts +4 -4
  45. package/types/src/effect.d.ts +1 -2
  46. package/types/src/errors.d.ts +4 -1
  47. package/types/src/system.d.ts +40 -24
  48. package/types/src/util.d.ts +1 -2
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";
@@ -14,6 +13,143 @@ var isUniformArray = (value, guard = (item) => item != null) => Array.isArray(va
14
13
  var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError";
15
14
  var valueString = (value) => isString(value) ? `"${value}"` : !!value && typeof value === "object" ? JSON.stringify(value) : String(value);
16
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
+
17
153
  // src/diff.ts
18
154
  var isEqual = (a, b, visited) => {
19
155
  if (Object.is(a, b))
@@ -101,73 +237,6 @@ var diff = (oldObj, newObj) => {
101
237
  };
102
238
  };
103
239
 
104
- // src/system.ts
105
- var activeWatcher;
106
- var pendingReactions = new Set;
107
- var batchDepth = 0;
108
- var createWatcher = (react) => {
109
- const cleanups = new Set;
110
- const watcher = react;
111
- watcher.onCleanup = (cleanup) => {
112
- cleanups.add(cleanup);
113
- };
114
- watcher.stop = () => {
115
- for (const cleanup of cleanups)
116
- cleanup();
117
- cleanups.clear();
118
- };
119
- return watcher;
120
- };
121
- var subscribeActiveWatcher = (watchers) => {
122
- if (activeWatcher && !watchers.has(activeWatcher)) {
123
- const watcher = activeWatcher;
124
- watcher.onCleanup(() => watchers.delete(watcher));
125
- watchers.add(watcher);
126
- }
127
- };
128
- var notifyWatchers = (watchers) => {
129
- for (const react of watchers) {
130
- if (batchDepth)
131
- pendingReactions.add(react);
132
- else
133
- react();
134
- }
135
- };
136
- var flushPendingReactions = () => {
137
- while (pendingReactions.size) {
138
- const watchers = Array.from(pendingReactions);
139
- pendingReactions.clear();
140
- for (const watcher of watchers)
141
- watcher();
142
- }
143
- };
144
- var batchSignalWrites = (callback) => {
145
- batchDepth++;
146
- try {
147
- callback();
148
- } finally {
149
- flushPendingReactions();
150
- batchDepth--;
151
- }
152
- };
153
- var trackSignalReads = (watcher, run) => {
154
- const prev = activeWatcher;
155
- activeWatcher = watcher || undefined;
156
- try {
157
- run();
158
- } finally {
159
- activeWatcher = prev;
160
- }
161
- };
162
- var emitNotification = (listeners, payload) => {
163
- for (const listener of listeners) {
164
- if (batchDepth)
165
- pendingReactions.add(() => listener(payload));
166
- else
167
- listener(payload);
168
- }
169
- };
170
-
171
240
  // src/classes/computed.ts
172
241
  var TYPE_COMPUTED = "Computed";
173
242
 
@@ -179,27 +248,35 @@ class Memo {
179
248
  #dirty = true;
180
249
  #computing = false;
181
250
  #watcher;
251
+ #watchHookCallbacks;
182
252
  constructor(callback, initialValue = UNSET) {
183
- validateCallback("memo", callback, isMemoCallback);
184
- validateSignalValue("memo", initialValue);
253
+ validateCallback(this.constructor.name, callback, isMemoCallback);
254
+ validateSignalValue(this.constructor.name, initialValue);
185
255
  this.#callback = callback;
186
256
  this.#value = initialValue;
187
- this.#watcher = createWatcher(() => {
188
- this.#dirty = true;
189
- if (this.#watchers.size)
190
- notifyWatchers(this.#watchers);
191
- else
192
- this.#watcher.stop();
193
- });
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;
194
270
  }
195
271
  get [Symbol.toStringTag]() {
196
272
  return TYPE_COMPUTED;
197
273
  }
198
274
  get() {
199
- subscribeActiveWatcher(this.#watchers);
275
+ subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks);
200
276
  flushPendingReactions();
201
277
  if (this.#dirty) {
202
- trackSignalReads(this.#watcher, () => {
278
+ const watcher = this.#getWatcher();
279
+ trackSignalReads(watcher, () => {
203
280
  if (this.#computing)
204
281
  throw new CircularDependencyError("memo");
205
282
  let result;
@@ -227,6 +304,16 @@ class Memo {
227
304
  throw this.#error;
228
305
  return this.#value;
229
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
+ }
230
317
  }
231
318
 
232
319
  class Task {
@@ -239,28 +326,34 @@ class Task {
239
326
  #changed = false;
240
327
  #watcher;
241
328
  #controller;
329
+ #watchHookCallbacks;
242
330
  constructor(callback, initialValue = UNSET) {
243
- validateCallback("task", callback, isTaskCallback);
244
- validateSignalValue("task", initialValue);
331
+ validateCallback(this.constructor.name, callback, isTaskCallback);
332
+ validateSignalValue(this.constructor.name, initialValue);
245
333
  this.#callback = callback;
246
334
  this.#value = initialValue;
247
- this.#watcher = createWatcher(() => {
248
- this.#dirty = true;
249
- this.#controller?.abort();
250
- if (this.#watchers.size)
251
- notifyWatchers(this.#watchers);
252
- else
253
- this.#watcher.stop();
254
- });
255
- this.#watcher.onCleanup(() => {
256
- this.#controller?.abort();
257
- });
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;
258
351
  }
259
352
  get [Symbol.toStringTag]() {
260
353
  return TYPE_COMPUTED;
261
354
  }
262
355
  get() {
263
- subscribeActiveWatcher(this.#watchers);
356
+ subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks);
264
357
  flushPendingReactions();
265
358
  const ok = (v) => {
266
359
  if (!isEqual(v, this.#value)) {
@@ -285,10 +378,10 @@ class Task {
285
378
  this.#computing = false;
286
379
  this.#controller = undefined;
287
380
  fn(arg);
288
- if (this.#changed)
289
- notifyWatchers(this.#watchers);
381
+ if (this.#changed && !notifyWatchers(this.#watchers))
382
+ this.#watcher?.stop();
290
383
  };
291
- const compute = () => trackSignalReads(this.#watcher, () => {
384
+ const compute = () => trackSignalReads(this.#getWatcher(), () => {
292
385
  if (this.#computing)
293
386
  throw new CircularDependencyError("task");
294
387
  this.#changed = false;
@@ -328,6 +421,16 @@ class Task {
328
421
  throw this.#error;
329
422
  return this.#value;
330
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
+ }
331
434
  }
332
435
  var createComputed = (callback, initialValue = UNSET) => isAsyncFunction(callback) ? new Task(callback, initialValue) : new Memo(callback, initialValue);
333
436
  var isComputed = (value) => isObjectOfType(value, TYPE_COMPUTED);
@@ -340,11 +443,7 @@ class Composite {
340
443
  #validate;
341
444
  #create;
342
445
  #watchers = new Map;
343
- #listeners = {
344
- add: new Set,
345
- change: new Set,
346
- remove: new Set
347
- };
446
+ #hookCallbacks = {};
348
447
  #batching = false;
349
448
  constructor(values, validate, create) {
350
449
  this.#validate = validate;
@@ -361,7 +460,7 @@ class Composite {
361
460
  trackSignalReads(watcher, () => {
362
461
  this.signals.get(key)?.get();
363
462
  if (!this.#batching)
364
- emitNotification(this.#listeners.change, [key]);
463
+ triggerHook(this.#hookCallbacks.change, [key]);
365
464
  });
366
465
  });
367
466
  this.#watchers.set(key, watcher);
@@ -371,10 +470,10 @@ class Composite {
371
470
  if (!this.#validate(key, value))
372
471
  return false;
373
472
  this.signals.set(key, this.#create(value));
374
- if (this.#listeners.change.size)
473
+ if (this.#hookCallbacks.change?.size)
375
474
  this.#addWatcher(key);
376
475
  if (!this.#batching)
377
- emitNotification(this.#listeners.add, [key]);
476
+ triggerHook(this.#hookCallbacks.add, [key]);
378
477
  return true;
379
478
  }
380
479
  remove(key) {
@@ -387,7 +486,7 @@ class Composite {
387
486
  this.#watchers.delete(key);
388
487
  }
389
488
  if (!this.#batching)
390
- emitNotification(this.#listeners.remove, [key]);
489
+ triggerHook(this.#hookCallbacks.remove, [key]);
391
490
  return true;
392
491
  }
393
492
  change(changes, initialRun) {
@@ -395,7 +494,7 @@ class Composite {
395
494
  if (Object.keys(changes.add).length) {
396
495
  for (const key in changes.add)
397
496
  this.add(key, changes.add[key]);
398
- const notify = () => emitNotification(this.#listeners.add, Object.keys(changes.add));
497
+ const notify = () => triggerHook(this.#hookCallbacks.add, Object.keys(changes.add));
399
498
  if (initialRun)
400
499
  setTimeout(notify, 0);
401
500
  else
@@ -412,12 +511,12 @@ class Composite {
412
511
  signal.set(value);
413
512
  }
414
513
  });
415
- emitNotification(this.#listeners.change, Object.keys(changes.change));
514
+ triggerHook(this.#hookCallbacks.change, Object.keys(changes.change));
416
515
  }
417
516
  if (Object.keys(changes.remove).length) {
418
517
  for (const key in changes.remove)
419
518
  this.remove(key);
420
- emitNotification(this.#listeners.remove, Object.keys(changes.remove));
519
+ triggerHook(this.#hookCallbacks.remove, Object.keys(changes.remove));
421
520
  }
422
521
  this.#batching = false;
423
522
  return changes.changed;
@@ -426,20 +525,23 @@ class Composite {
426
525
  const keys = Array.from(this.signals.keys());
427
526
  this.signals.clear();
428
527
  this.#watchers.clear();
429
- emitNotification(this.#listeners.remove, keys);
528
+ triggerHook(this.#hookCallbacks.remove, keys);
430
529
  return true;
431
530
  }
432
- on(type, listener) {
433
- this.#listeners[type].add(listener);
434
- 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) {
435
537
  this.#batching = true;
436
538
  for (const key of this.signals.keys())
437
539
  this.#addWatcher(key);
438
540
  this.#batching = false;
439
541
  }
440
542
  return () => {
441
- this.#listeners[type].delete(listener);
442
- if (type === "change" && !this.#listeners.change.size) {
543
+ this.#hookCallbacks[type]?.delete(callback);
544
+ if (type === HOOK_CHANGE && !this.#hookCallbacks.change?.size) {
443
545
  if (this.#watchers.size) {
444
546
  for (const watcher of this.#watchers.values())
445
547
  watcher.stop();
@@ -456,30 +558,42 @@ var TYPE_STATE = "State";
456
558
  class State {
457
559
  #watchers = new Set;
458
560
  #value;
561
+ #watchHookCallbacks;
459
562
  constructor(initialValue) {
460
- validateSignalValue("state", initialValue);
563
+ validateSignalValue(TYPE_STATE, initialValue);
461
564
  this.#value = initialValue;
462
565
  }
463
566
  get [Symbol.toStringTag]() {
464
567
  return TYPE_STATE;
465
568
  }
466
569
  get() {
467
- subscribeActiveWatcher(this.#watchers);
570
+ subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks);
468
571
  return this.#value;
469
572
  }
470
573
  set(newValue) {
471
- validateSignalValue("state", newValue);
574
+ validateSignalValue(TYPE_STATE, newValue);
472
575
  if (isEqual(this.#value, newValue))
473
576
  return;
474
577
  this.#value = newValue;
475
- notifyWatchers(this.#watchers);
578
+ if (this.#watchers.size)
579
+ notifyWatchers(this.#watchers);
476
580
  if (UNSET === this.#value)
477
581
  this.#watchers.clear();
478
582
  }
479
583
  update(updater) {
480
- validateCallback("state update", updater);
584
+ validateCallback(`${TYPE_STATE} update`, updater);
481
585
  this.set(updater(this.#value));
482
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
+ }
483
597
  }
484
598
  var isState = (value) => isObjectOfType(value, TYPE_STATE);
485
599
 
@@ -489,17 +603,15 @@ var TYPE_LIST = "List";
489
603
  class List {
490
604
  #composite;
491
605
  #watchers = new Set;
492
- #listeners = {
493
- sort: new Set
494
- };
606
+ #hookCallbacks = {};
495
607
  #order = [];
496
608
  #generateKey;
497
609
  constructor(initialValue, keyConfig) {
498
- validateSignalValue("list", initialValue, Array.isArray);
610
+ validateSignalValue(TYPE_LIST, initialValue, Array.isArray);
499
611
  let keyCounter = 0;
500
612
  this.#generateKey = isString(keyConfig) ? () => `${keyConfig}${keyCounter++}` : isFunction(keyConfig) ? (item) => keyConfig(item) : () => String(keyCounter++);
501
613
  this.#composite = new Composite(this.#toRecord(initialValue), (key, value) => {
502
- validateSignalValue(`list for key "${key}"`, value);
614
+ validateSignalValue(`${TYPE_LIST} for key "${key}"`, value);
503
615
  return true;
504
616
  }, (value) => new State(value));
505
617
  }
@@ -535,11 +647,11 @@ class List {
535
647
  }
536
648
  }
537
649
  get length() {
538
- subscribeActiveWatcher(this.#watchers);
650
+ subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH]);
539
651
  return this.#order.length;
540
652
  }
541
653
  get() {
542
- subscribeActiveWatcher(this.#watchers);
654
+ subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH]);
543
655
  return this.#value;
544
656
  }
545
657
  set(newValue) {
@@ -609,7 +721,7 @@ class List {
609
721
  if (!isEqual(this.#order, newOrder)) {
610
722
  this.#order = newOrder;
611
723
  notifyWatchers(this.#watchers);
612
- emitNotification(this.#listeners.sort, this.#order);
724
+ triggerHook(this.#hookCallbacks.sort, this.#order);
613
725
  }
614
726
  }
615
727
  splice(start, deleteCount, ...items) {
@@ -647,14 +759,17 @@ class List {
647
759
  }
648
760
  return Object.values(remove);
649
761
  }
650
- on(type, listener) {
651
- if (type === "sort") {
652
- this.#listeners.sort.add(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);
653
766
  return () => {
654
- this.#listeners.sort.delete(listener);
767
+ this.#hookCallbacks[type]?.delete(callback);
655
768
  };
769
+ } else if (isHandledHook(type, [HOOK_ADD, HOOK_CHANGE, HOOK_REMOVE])) {
770
+ return this.#composite.on(type, callback);
656
771
  }
657
- return this.#composite.on(type, listener);
772
+ throw new InvalidHookError(TYPE_LIST, type);
658
773
  }
659
774
  deriveCollection(callback) {
660
775
  return new DerivedCollection(this, callback);
@@ -668,10 +783,11 @@ var TYPE_STORE = "Store";
668
783
  class BaseStore {
669
784
  #composite;
670
785
  #watchers = new Set;
786
+ #watchHookCallbacks;
671
787
  constructor(initialValue) {
672
- validateSignalValue("store", initialValue, isRecord);
788
+ validateSignalValue(TYPE_STORE, initialValue, isRecord);
673
789
  this.#composite = new Composite(initialValue, (key, value) => {
674
- validateSignalValue(`store for key "${key}"`, value);
790
+ validateSignalValue(`${TYPE_STORE} for key "${key}"`, value);
675
791
  return true;
676
792
  }, (value) => createMutableSignal(value));
677
793
  }
@@ -691,8 +807,14 @@ class BaseStore {
691
807
  for (const [key, signal] of this.#composite.signals.entries())
692
808
  yield [key, signal];
693
809
  }
810
+ keys() {
811
+ return this.#composite.signals.keys();
812
+ }
813
+ byKey(key) {
814
+ return this.#composite.signals.get(key);
815
+ }
694
816
  get() {
695
- subscribeActiveWatcher(this.#watchers);
817
+ subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks);
696
818
  return this.#value;
697
819
  }
698
820
  set(newValue) {
@@ -707,18 +829,12 @@ class BaseStore {
707
829
  if (changed)
708
830
  notifyWatchers(this.#watchers);
709
831
  }
710
- keys() {
711
- return this.#composite.signals.keys();
712
- }
713
- byKey(key) {
714
- return this.#composite.signals.get(key);
715
- }
716
832
  update(fn) {
717
833
  this.set(fn(this.get()));
718
834
  }
719
835
  add(key, value) {
720
836
  if (this.#composite.signals.has(key))
721
- throw new DuplicateKeyError("store", key, value);
837
+ throw new DuplicateKeyError(TYPE_STORE, key, value);
722
838
  const ok = this.#composite.add(key, value);
723
839
  if (ok)
724
840
  notifyWatchers(this.#watchers);
@@ -729,8 +845,17 @@ class BaseStore {
729
845
  if (ok)
730
846
  notifyWatchers(this.#watchers);
731
847
  }
732
- on(type, listener) {
733
- 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);
734
859
  }
735
860
  }
736
861
  var createStore = (initialValue) => {
@@ -820,6 +945,13 @@ class InvalidCollectionSourceError extends TypeError {
820
945
  }
821
946
  }
822
947
 
948
+ class InvalidHookError extends TypeError {
949
+ constructor(where, type) {
950
+ super(`Invalid hook "${type}" in ${where}`);
951
+ this.name = "InvalidHookError";
952
+ }
953
+ }
954
+
823
955
  class InvalidSignalValueError extends TypeError {
824
956
  constructor(where, value) {
825
957
  super(`Invalid signal value ${valueString(value)} in ${where}`);
@@ -866,19 +998,14 @@ class DerivedCollection {
866
998
  #callback;
867
999
  #signals = new Map;
868
1000
  #ownWatchers = new Map;
869
- #listeners = {
870
- add: new Set,
871
- change: new Set,
872
- remove: new Set,
873
- sort: new Set
874
- };
1001
+ #hookCallbacks = {};
875
1002
  #order = [];
876
1003
  constructor(source, callback) {
877
- validateCallback("collection", callback);
1004
+ validateCallback(TYPE_COLLECTION, callback);
878
1005
  if (isFunction(source))
879
1006
  source = source();
880
1007
  if (!isCollectionSource(source))
881
- throw new InvalidCollectionSourceError("derived collection", source);
1008
+ throw new InvalidCollectionSourceError(TYPE_COLLECTION, source);
882
1009
  this.#source = source;
883
1010
  this.#callback = callback;
884
1011
  for (let i = 0;i < this.#source.length; i++) {
@@ -887,7 +1014,9 @@ class DerivedCollection {
887
1014
  continue;
888
1015
  this.#add(key);
889
1016
  }
890
- this.#source.on("add", (additions) => {
1017
+ this.#source.on(HOOK_ADD, (additions) => {
1018
+ if (!additions)
1019
+ return;
891
1020
  for (const key of additions) {
892
1021
  if (!this.#signals.has(key)) {
893
1022
  this.#add(key);
@@ -897,9 +1026,11 @@ class DerivedCollection {
897
1026
  }
898
1027
  }
899
1028
  notifyWatchers(this.#watchers);
900
- emitNotification(this.#listeners.add, additions);
1029
+ triggerHook(this.#hookCallbacks.add, additions);
901
1030
  });
902
- this.#source.on("remove", (removals) => {
1031
+ this.#source.on(HOOK_REMOVE, (removals) => {
1032
+ if (!removals)
1033
+ return;
903
1034
  for (const key of removals) {
904
1035
  if (!this.#signals.has(key))
905
1036
  continue;
@@ -915,28 +1046,23 @@ class DerivedCollection {
915
1046
  }
916
1047
  this.#order = this.#order.filter(() => true);
917
1048
  notifyWatchers(this.#watchers);
918
- emitNotification(this.#listeners.remove, removals);
1049
+ triggerHook(this.#hookCallbacks.remove, removals);
919
1050
  });
920
- this.#source.on("sort", (newOrder) => {
921
- this.#order = [...newOrder];
1051
+ this.#source.on(HOOK_SORT, (newOrder) => {
1052
+ if (newOrder)
1053
+ this.#order = [...newOrder];
922
1054
  notifyWatchers(this.#watchers);
923
- emitNotification(this.#listeners.sort, newOrder);
1055
+ triggerHook(this.#hookCallbacks.sort, newOrder);
924
1056
  });
925
1057
  }
926
1058
  #add(key) {
927
1059
  const computedCallback = isAsyncCollectionCallback(this.#callback) ? async (_, abort) => {
928
- const sourceSignal = this.#source.byKey(key);
929
- if (!sourceSignal)
930
- return UNSET;
931
- const sourceValue = sourceSignal.get();
1060
+ const sourceValue = this.#source.byKey(key)?.get();
932
1061
  if (sourceValue === UNSET)
933
1062
  return UNSET;
934
1063
  return this.#callback(sourceValue, abort);
935
1064
  } : () => {
936
- const sourceSignal = this.#source.byKey(key);
937
- if (!sourceSignal)
938
- return UNSET;
939
- const sourceValue = sourceSignal.get();
1065
+ const sourceValue = this.#source.byKey(key)?.get();
940
1066
  if (sourceValue === UNSET)
941
1067
  return UNSET;
942
1068
  return this.#callback(sourceValue);
@@ -945,7 +1071,7 @@ class DerivedCollection {
945
1071
  this.#signals.set(key, signal);
946
1072
  if (!this.#order.includes(key))
947
1073
  this.#order.push(key);
948
- if (this.#listeners.change.size)
1074
+ if (this.#hookCallbacks.change?.size)
949
1075
  this.#addWatcher(key);
950
1076
  return true;
951
1077
  }
@@ -971,20 +1097,16 @@ class DerivedCollection {
971
1097
  yield signal;
972
1098
  }
973
1099
  }
974
- get length() {
975
- subscribeActiveWatcher(this.#watchers);
976
- return this.#order.length;
1100
+ keys() {
1101
+ return this.#order.values();
977
1102
  }
978
1103
  get() {
979
- subscribeActiveWatcher(this.#watchers);
1104
+ subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH]);
980
1105
  return this.#order.map((key) => this.#signals.get(key)?.get()).filter((v) => v != null && v !== UNSET);
981
1106
  }
982
1107
  at(index) {
983
1108
  return this.#signals.get(this.#order[index]);
984
1109
  }
985
- keys() {
986
- return this.#order.values();
987
- }
988
1110
  byKey(key) {
989
1111
  return this.#signals.get(key);
990
1112
  }
@@ -994,26 +1116,40 @@ class DerivedCollection {
994
1116
  indexOfKey(key) {
995
1117
  return this.#order.indexOf(key);
996
1118
  }
997
- on(type, listener) {
998
- this.#listeners[type].add(listener);
999
- if (type === "change" && !this.#ownWatchers.size) {
1000
- for (const key of this.#signals.keys())
1001
- this.#addWatcher(key);
1002
- }
1003
- return () => {
1004
- this.#listeners[type].delete(listener);
1005
- if (type === "change" && !this.#listeners.change.size) {
1006
- if (this.#ownWatchers.size) {
1007
- for (const watcher of this.#ownWatchers.values())
1008
- watcher.stop();
1009
- this.#ownWatchers.clear();
1010
- }
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);
1011
1132
  }
1012
- };
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);
1013
1145
  }
1014
1146
  deriveCollection(callback) {
1015
1147
  return new DerivedCollection(this, callback);
1016
1148
  }
1149
+ get length() {
1150
+ subscribeActiveWatcher(this.#watchers, this.#hookCallbacks[HOOK_WATCH]);
1151
+ return this.#order.length;
1152
+ }
1017
1153
  }
1018
1154
  var isCollection = (value) => isObjectOfType(value, TYPE_COLLECTION);
1019
1155
  var isCollectionSource = (value) => isList(value) || isCollection(value);
@@ -1024,20 +1160,31 @@ var TYPE_REF = "Ref";
1024
1160
  class Ref {
1025
1161
  #watchers = new Set;
1026
1162
  #value;
1163
+ #watchHookCallbacks;
1027
1164
  constructor(value, guard) {
1028
- validateSignalValue("ref", value, guard);
1165
+ validateSignalValue(TYPE_REF, value, guard);
1029
1166
  this.#value = value;
1030
1167
  }
1031
1168
  get [Symbol.toStringTag]() {
1032
1169
  return TYPE_REF;
1033
1170
  }
1034
1171
  get() {
1035
- subscribeActiveWatcher(this.#watchers);
1172
+ subscribeActiveWatcher(this.#watchers, this.#watchHookCallbacks);
1036
1173
  return this.#value;
1037
1174
  }
1038
1175
  notify() {
1039
1176
  notifyWatchers(this.#watchers);
1040
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
+ }
1041
1188
  }
1042
1189
  var isRef = (value) => isObjectOfType(value, TYPE_REF);
1043
1190
  // src/effect.ts
@@ -1060,26 +1207,30 @@ var createEffect = (callback) => {
1060
1207
  const currentController = controller;
1061
1208
  callback(controller.signal).then((cleanup2) => {
1062
1209
  if (isFunction(cleanup2) && controller === currentController)
1063
- watcher.onCleanup(cleanup2);
1210
+ watcher.on(HOOK_CLEANUP, cleanup2);
1064
1211
  }).catch((error) => {
1065
1212
  if (!isAbortError(error))
1066
- console.error("Async effect error:", error);
1213
+ console.error("Error in async effect callback:", error);
1067
1214
  });
1068
1215
  } else {
1069
1216
  cleanup = callback();
1070
1217
  if (isFunction(cleanup))
1071
- watcher.onCleanup(cleanup);
1218
+ watcher.on(HOOK_CLEANUP, cleanup);
1072
1219
  }
1073
1220
  } catch (error) {
1074
1221
  if (!isAbortError(error))
1075
- console.error("Effect callback error:", error);
1222
+ console.error("Error in effect callback:", error);
1076
1223
  }
1077
1224
  running = false;
1078
1225
  }));
1079
1226
  watcher();
1080
1227
  return () => {
1081
1228
  controller?.abort();
1082
- watcher.stop();
1229
+ try {
1230
+ watcher.stop();
1231
+ } catch (error) {
1232
+ console.error("Error in effect cleanup:", error);
1233
+ }
1083
1234
  };
1084
1235
  };
1085
1236
  // src/match.ts
@@ -1125,6 +1276,7 @@ export {
1125
1276
  valueString,
1126
1277
  validateSignalValue,
1127
1278
  validateCallback,
1279
+ triggerHook,
1128
1280
  trackSignalReads,
1129
1281
  subscribeActiveWatcher,
1130
1282
  resolve,
@@ -1144,6 +1296,7 @@ export {
1144
1296
  isMutableSignal,
1145
1297
  isMemoCallback,
1146
1298
  isList,
1299
+ isHandledHook,
1147
1300
  isFunction,
1148
1301
  isEqual,
1149
1302
  isComputed,
@@ -1152,7 +1305,6 @@ export {
1152
1305
  isAbortError,
1153
1306
  guardMutableSignal,
1154
1307
  flushPendingReactions,
1155
- emitNotification,
1156
1308
  diff,
1157
1309
  createWatcher,
1158
1310
  createStore,
@@ -1178,6 +1330,12 @@ export {
1178
1330
  InvalidSignalValueError,
1179
1331
  InvalidCollectionSourceError,
1180
1332
  InvalidCallbackError,
1333
+ HOOK_WATCH,
1334
+ HOOK_SORT,
1335
+ HOOK_REMOVE,
1336
+ HOOK_CLEANUP,
1337
+ HOOK_CHANGE,
1338
+ HOOK_ADD,
1181
1339
  DuplicateKeyError,
1182
1340
  DerivedCollection,
1183
1341
  CircularDependencyError,