@alwatr/signal 5.1.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /* @alwatr/signal v5.1.0 */
1
+ /* @alwatr/signal v5.2.1 */
2
2
  "use strict";
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -39,7 +39,8 @@ var SignalBase = class {
39
39
  constructor(config_) {
40
40
  this.config_ = config_;
41
41
  /**
42
- * The unique identifier for this signal instance. Useful for debugging.
42
+ * The unique identifier for this signal instance.
43
+ * Useful for debugging and logging.
43
44
  */
44
45
  this.signalId = this.config_.signalId;
45
46
  /**
@@ -47,38 +48,33 @@ var SignalBase = class {
47
48
  * @protected
48
49
  */
49
50
  this.observers_ = [];
50
- this.isDestroyed_ = false;
51
51
  /**
52
- * Throws an error if the signal has been destroyed.
53
- * This is a safeguard to prevent interaction with a defunct signal.
54
- * @protected
52
+ * A flag indicating whether the signal has been destroyed.
53
+ * @private
55
54
  */
56
- this.checkDestroyed_ = () => {
57
- if (this.isDestroyed_) {
58
- this.logger_.accident("checkDestroyed_", "attempt_to_use_destroyed_signal");
59
- throw new Error(`Cannot interact with a destroyed signal (id: ${this.signalId})`);
60
- }
61
- };
55
+ this.isDestroyed__ = false;
62
56
  }
63
57
  /**
64
58
  * Indicates whether the signal has been destroyed.
65
59
  * A destroyed signal cannot be used and will throw an error if interacted with.
60
+ *
66
61
  * @returns `true` if the signal is destroyed, `false` otherwise.
67
62
  */
68
63
  get isDestroyed() {
69
- return this.isDestroyed_;
64
+ return this.isDestroyed__;
70
65
  }
71
66
  /**
72
67
  * Removes a specific observer from the observers list.
68
+ *
73
69
  * @param observer The observer instance to remove.
74
70
  * @protected
75
71
  */
76
72
  removeObserver_(observer) {
77
- if (this.isDestroyed_) {
73
+ this.logger_.logMethod?.("removeObserver_");
74
+ if (this.isDestroyed__) {
78
75
  this.logger_.incident?.("removeObserver_", "remove_observer_on_destroyed_signal");
79
76
  return;
80
77
  }
81
- this.logger_.logMethod?.("removeObserver_");
82
78
  const index = this.observers_.indexOf(observer);
83
79
  if (index !== -1) {
84
80
  this.observers_.splice(index, 1);
@@ -102,8 +98,9 @@ var SignalBase = class {
102
98
  } else {
103
99
  this.observers_.push(observer);
104
100
  }
105
- const unsubscribe = () => this.removeObserver_(observer);
106
- return { unsubscribe };
101
+ return {
102
+ unsubscribe: () => this.removeObserver_(observer)
103
+ };
107
104
  }
108
105
  /**
109
106
  * Notifies all registered observers about a new value.
@@ -115,11 +112,11 @@ var SignalBase = class {
115
112
  * @protected
116
113
  */
117
114
  notify_(value) {
118
- if (this.isDestroyed_) {
115
+ this.logger_.logMethodArgs?.("notify_", value);
116
+ if (this.isDestroyed__) {
119
117
  this.logger_.incident?.("notify_", "notify_on_destroyed_signal");
120
118
  return;
121
119
  }
122
- this.logger_.logMethodArgs?.("notify_", value);
123
120
  const currentObservers = [...this.observers_];
124
121
  for (const observer of currentObservers) {
125
122
  if (observer.options?.once) {
@@ -128,9 +125,7 @@ var SignalBase = class {
128
125
  try {
129
126
  const result = observer.callback(value);
130
127
  if (result instanceof Promise) {
131
- result.catch((err) => {
132
- this.logger_.error("notify_", "async_callback_failed", err, { observer });
133
- });
128
+ result.catch((err) => this.logger_.error("notify_", "async_callback_failed", err, { observer }));
134
129
  }
135
130
  } catch (err) {
136
131
  this.logger_.error("notify_", "sync_callback_failed", err);
@@ -172,12 +167,26 @@ var SignalBase = class {
172
167
  */
173
168
  destroy() {
174
169
  this.logger_.logMethod?.("destroy");
175
- if (this.isDestroyed_) return;
176
- this.isDestroyed_ = true;
170
+ if (this.isDestroyed__) {
171
+ this.logger_.incident?.("destroy_", "double_destroy_attempt");
172
+ return;
173
+ }
174
+ this.isDestroyed__ = true;
177
175
  this.observers_.length = 0;
178
176
  this.config_.onDestroy?.();
179
177
  this.config_ = null;
180
178
  }
179
+ /**
180
+ * Throws an error if the signal has been destroyed.
181
+ * This is a safeguard to prevent interaction with a defunct signal.
182
+ * @protected
183
+ */
184
+ checkDestroyed_() {
185
+ if (this.isDestroyed__) {
186
+ this.logger_.accident("checkDestroyed_", "attempt_to_use_destroyed_signal");
187
+ throw new Error(`Cannot interact with a destroyed signal (id: ${this.signalId})`);
188
+ }
189
+ }
181
190
  };
182
191
 
183
192
  // src/core/event-signal.ts
@@ -186,6 +195,10 @@ var import_logger = require("@alwatr/logger");
186
195
  var EventSignal = class extends SignalBase {
187
196
  constructor(config) {
188
197
  super(config);
198
+ /**
199
+ * The logger instance for this signal.
200
+ * @protected
201
+ */
189
202
  this.logger_ = (0, import_logger.createLogger)(`event-signal: ${this.signalId}`);
190
203
  this.logger_.logMethod?.("constructor");
191
204
  }
@@ -197,11 +210,9 @@ var EventSignal = class extends SignalBase {
197
210
  * @param payload The data to send with the event.
198
211
  */
199
212
  dispatch(payload) {
200
- this.logger_.logMethodArgs?.("dispatch", payload);
213
+ this.logger_.logMethodArgs?.("dispatch", { payload });
201
214
  this.checkDestroyed_();
202
- import_delay.delay.nextMicrotask().then(() => {
203
- this.notify_(payload);
204
- });
215
+ import_delay.delay.nextMicrotask().then(() => this.notify_(payload));
205
216
  }
206
217
  };
207
218
 
@@ -211,6 +222,10 @@ var import_logger2 = require("@alwatr/logger");
211
222
  var StateSignal = class extends SignalBase {
212
223
  constructor(config) {
213
224
  super(config);
225
+ /**
226
+ * The logger instance for this signal.
227
+ * @protected
228
+ */
214
229
  this.logger_ = (0, import_logger2.createLogger)(`state-signal: ${this.signalId}`);
215
230
  this.value__ = config.initialValue;
216
231
  this.logger_.logMethodArgs?.("constructor", { initialValue: this.value__ });
@@ -221,9 +236,9 @@ var StateSignal = class extends SignalBase {
221
236
  * @returns The current value.
222
237
  *
223
238
  * @example
224
- * console.log(mySignal.value);
239
+ * console.log(mySignal.get());
225
240
  */
226
- get value() {
241
+ get() {
227
242
  this.checkDestroyed_();
228
243
  return this.value__;
229
244
  }
@@ -240,16 +255,16 @@ var StateSignal = class extends SignalBase {
240
255
  * mySignal.set(42);
241
256
  *
242
257
  * // For object types, it's best practice to set an immutable new object.
243
- * mySignal.set({ ...mySignal.value, property: 'new-value' });
258
+ * mySignal.set({ ...mySignal.get(), property: 'new-value' });
244
259
  */
245
260
  set(newValue) {
246
261
  this.logger_.logMethodArgs?.("set", { newValue });
247
262
  this.checkDestroyed_();
248
- if (Object.is(this.value__, newValue) && (typeof newValue !== "object" || newValue === null)) return;
263
+ if (Object.is(this.value__, newValue) && (typeof newValue !== "object" || newValue === null)) {
264
+ return;
265
+ }
249
266
  this.value__ = newValue;
250
- import_delay2.delay.nextMicrotask().then(() => {
251
- this.notify_(newValue);
252
- });
267
+ import_delay2.delay.nextMicrotask().then(() => this.notify_(newValue));
253
268
  }
254
269
  /**
255
270
  * Updates the signal's value based on its previous value.
@@ -284,11 +299,8 @@ var StateSignal = class extends SignalBase {
284
299
  subscribe(callback, options = {}) {
285
300
  this.logger_.logMethodArgs?.("subscribe", { options });
286
301
  this.checkDestroyed_();
287
- const receivePrevious = options.receivePrevious !== false;
288
- if (receivePrevious) {
289
- import_delay2.delay.nextMicrotask().then(() => callback(this.value__)).catch((err) => {
290
- this.logger_.error("subscribe", "run_callback_immediate_failed", err);
291
- });
302
+ if (options.receivePrevious !== false) {
303
+ import_delay2.delay.nextMicrotask().then(() => callback(this.value__)).catch((err) => this.logger_.error("subscribe", "immediate_callback_failed", err));
292
304
  if (options.once) {
293
305
  return { unsubscribe: () => {
294
306
  } };
@@ -312,25 +324,38 @@ var import_logger3 = require("@alwatr/logger");
312
324
  var ComputedSignal = class {
313
325
  constructor(config_) {
314
326
  this.config_ = config_;
327
+ /**
328
+ * The unique identifier for this signal instance.
329
+ */
315
330
  this.signalId = this.config_.signalId;
331
+ /**
332
+ * The logger instance for this signal.
333
+ * @protected
334
+ */
316
335
  this.logger_ = (0, import_logger3.createLogger)(`computed-signal: ${this.signalId}`);
317
336
  /**
318
337
  * The internal `StateSignal` that holds the computed value.
319
- * This is how the computed signal provides `.value` and `.subscribe()` methods.
338
+ * This is how the computed signal provides `.get()` and `.subscribe()` methods.
320
339
  * @protected
321
340
  */
322
341
  this.internalSignal_ = new StateSignal({
323
- signalId: this.signalId + "-internal",
342
+ signalId: `${this.signalId}-internal`,
324
343
  initialValue: this.config_.get()
325
344
  });
326
- this.subscriptionList__ = [];
345
+ /**
346
+ * A list of subscriptions to dependency signals.
347
+ * @private
348
+ */
349
+ this.dependencySubscriptions__ = [];
350
+ /**
351
+ * A flag to prevent concurrent recalculations.
352
+ * @private
353
+ */
327
354
  this.isRecalculating__ = false;
328
- this.subscribe = this.internalSignal_.subscribe.bind(this.internalSignal_);
329
- this.untilNext = this.internalSignal_.untilNext.bind(this.internalSignal_);
330
355
  this.logger_.logMethod?.("constructor");
331
356
  this.recalculate_ = this.recalculate_.bind(this);
332
357
  for (const signal of config_.deps) {
333
- this.subscriptionList__.push(signal.subscribe(this.recalculate_, { receivePrevious: false }));
358
+ this.dependencySubscriptions__.push(signal.subscribe(this.recalculate_, { receivePrevious: false }));
334
359
  }
335
360
  }
336
361
  /**
@@ -340,8 +365,8 @@ var ComputedSignal = class {
340
365
  * @returns The current computed value.
341
366
  * @throws {Error} If accessed after the signal has been destroyed.
342
367
  */
343
- get value() {
344
- return this.internalSignal_.value;
368
+ get() {
369
+ return this.internalSignal_.get();
345
370
  }
346
371
  /**
347
372
  * Indicates whether the computed signal has been destroyed.
@@ -351,6 +376,25 @@ var ComputedSignal = class {
351
376
  get isDestroyed() {
352
377
  return this.internalSignal_.isDestroyed;
353
378
  }
379
+ /**
380
+ * Subscribes a listener to this signal.
381
+ * The listener will be called whenever the computed value changes.
382
+ *
383
+ * @param callback The function to be called with the new value.
384
+ * @param options Subscription options.
385
+ * @returns A `SubscribeResult` object with an `unsubscribe` method.
386
+ */
387
+ subscribe(callback, options) {
388
+ return this.internalSignal_.subscribe(callback, options);
389
+ }
390
+ /**
391
+ * Returns a Promise that resolves with the next computed value.
392
+ *
393
+ * @returns A Promise that resolves with the next value.
394
+ */
395
+ untilNext() {
396
+ return this.internalSignal_.untilNext();
397
+ }
354
398
  /**
355
399
  * Permanently disposes of the computed signal.
356
400
  *
@@ -358,18 +402,18 @@ var ComputedSignal = class {
358
402
  * stopping future recalculations and allowing the signal to be garbage collected.
359
403
  * Failure to call `destroy()` will result in memory leaks.
360
404
  *
361
- * After `destroy()` is called, any attempt to access `.value` or `.subscribe()` will throw an error.
405
+ * After `destroy()` is called, any attempt to access `.get()` or `.subscribe()` will throw an error.
362
406
  */
363
407
  destroy() {
364
408
  this.logger_.logMethod?.("destroy");
365
- if (this.internalSignal_.isDestroyed) {
409
+ if (this.isDestroyed) {
366
410
  this.logger_.incident?.("destroy", "already_destroyed");
367
411
  return;
368
412
  }
369
- for (const subscription of this.subscriptionList__) {
413
+ for (const subscription of this.dependencySubscriptions__) {
370
414
  subscription.unsubscribe();
371
415
  }
372
- this.subscriptionList__.length = 0;
416
+ this.dependencySubscriptions__.length = 0;
373
417
  this.internalSignal_.destroy();
374
418
  this.config_.onDestroy?.();
375
419
  this.config_ = null;
@@ -383,23 +427,24 @@ var ComputedSignal = class {
383
427
  * @protected
384
428
  */
385
429
  async recalculate_() {
430
+ this.logger_.logMethod?.("recalculate_");
386
431
  if (this.internalSignal_.isDestroyed) {
387
- this.logger_.incident?.("recalculate", "recalculate_on_destroyed_signal");
432
+ this.logger_.incident?.("recalculate_", "recalculate_on_destroyed_signal");
388
433
  return;
389
434
  }
390
435
  if (this.isRecalculating__) {
391
- this.logger_.logMethod?.("recalculate_//skipped");
436
+ this.logger_.logStep?.("recalculate_", "skipping_recalculation_already_scheduled");
392
437
  return;
393
438
  }
394
- this.logger_.logMethod?.("recalculate_//scheduled");
395
439
  this.isRecalculating__ = true;
396
440
  try {
397
441
  await import_delay3.delay.nextMacrotask();
398
- if (this.internalSignal_.isDestroyed) {
399
- this.logger_.incident?.("recalculate", "destroyed_during_delay");
442
+ if (this.isDestroyed) {
443
+ this.logger_.incident?.("recalculate_", "destroyed_during_delay");
444
+ this.isRecalculating__ = false;
400
445
  return;
401
446
  }
402
- this.logger_.logMethod?.("recalculate_//executing");
447
+ this.logger_.logStep?.("recalculate_", "recalculating_value");
403
448
  this.internalSignal_.set(this.config_.get());
404
449
  } catch (err) {
405
450
  this.logger_.error("recalculate_", "recalculation_failed", err);
@@ -414,22 +459,43 @@ var import_logger4 = require("@alwatr/logger");
414
459
  var EffectSignal = class {
415
460
  constructor(config_) {
416
461
  this.config_ = config_;
417
- this.logger_ = (0, import_logger4.createLogger)(`effect-signal`);
418
- this.subscriptionList__ = [];
462
+ /**
463
+ * The unique identifier for this signal instance.
464
+ */
465
+ this.signalId = this.config_.signalId ? this.config_.signalId : `[${this.config_.deps.map((dep) => dep.signalId).join(", ")}]`;
466
+ /**
467
+ * The logger instance for this signal.
468
+ * @protected
469
+ */
470
+ this.logger_ = (0, import_logger4.createLogger)(`effect-signal: ${this.signalId}`);
471
+ /**
472
+ * A list of subscriptions to dependency signals.
473
+ * @private
474
+ */
475
+ this.dependencySubscriptions__ = [];
476
+ /**
477
+ * A flag to prevent concurrent executions of the effect.
478
+ * @private
479
+ */
419
480
  this.isRunning__ = false;
481
+ /**
482
+ * A flag indicating whether the effect has been destroyed.
483
+ * @private
484
+ */
420
485
  this.isDestroyed__ = false;
421
486
  this.logger_.logMethod?.("constructor");
422
- this.run_ = this.run_.bind(this);
487
+ this.scheduleExecution_ = this.scheduleExecution_.bind(this);
423
488
  for (const signal of config_.deps) {
424
- this.subscriptionList__.push(signal.subscribe(this.run_, { receivePrevious: false }));
489
+ this.dependencySubscriptions__.push(signal.subscribe(this.scheduleExecution_, { receivePrevious: false }));
425
490
  }
426
491
  if (config_.runImmediately === true) {
427
- void this.run_();
492
+ void this.scheduleExecution_();
428
493
  }
429
494
  }
430
495
  /**
431
496
  * Indicates whether the effect signal has been destroyed.
432
- * A destroyed signal cannot be used and will throw an error if interacted with.
497
+ * A destroyed signal will no longer execute its effect and cannot be reused.
498
+ *
433
499
  * @returns `true` if the signal is destroyed, `false` otherwise.
434
500
  */
435
501
  get isDestroyed() {
@@ -443,30 +509,30 @@ var EffectSignal = class {
443
509
  * dependencies change simultaneously.
444
510
  * @protected
445
511
  */
446
- async run_() {
512
+ async scheduleExecution_() {
513
+ this.logger_.logMethod?.("scheduleExecution_");
447
514
  if (this.isDestroyed__) {
448
- this.logger_.incident?.("run_", "run_on_destroyed_signal");
515
+ this.logger_.incident?.("scheduleExecution_", "schedule_execution_on_destroyed_signal");
449
516
  return;
450
517
  }
451
518
  if (this.isRunning__) {
452
- this.logger_.logMethod?.("run_//skipped");
519
+ this.logger_.logStep?.("scheduleExecution_", "skipped_because_already_running");
453
520
  return;
454
521
  }
455
- this.logger_.logMethod?.("run_//scheduled");
456
522
  this.isRunning__ = true;
457
523
  try {
458
524
  await import_delay4.delay.nextMacrotask();
459
525
  if (this.isDestroyed__) {
460
- this.logger_.incident?.("run_", "destroyed_during_delay");
526
+ this.logger_.incident?.("scheduleExecution_", "destroyed_during_delay");
527
+ this.isRunning__ = false;
461
528
  return;
462
529
  }
463
- this.logger_.logMethod?.("run_//executing");
530
+ this.logger_.logStep?.("scheduleExecution_", "executing_effect");
464
531
  await this.config_.run();
465
532
  } catch (err) {
466
- this.logger_.error("run_", "effect_failed", err);
467
- } finally {
468
- this.isRunning__ = false;
533
+ this.logger_.error("scheduleExecution_", "effect_failed", err);
469
534
  }
535
+ this.isRunning__ = false;
470
536
  }
471
537
  /**
472
538
  * Permanently disposes of the effect signal.
@@ -482,10 +548,10 @@ var EffectSignal = class {
482
548
  return;
483
549
  }
484
550
  this.isDestroyed__ = true;
485
- for (const subscription of this.subscriptionList__) {
551
+ for (const subscription of this.dependencySubscriptions__) {
486
552
  subscription.unsubscribe();
487
553
  }
488
- this.subscriptionList__.length = 0;
554
+ this.dependencySubscriptions__.length = 0;
489
555
  this.config_.onDestroy?.();
490
556
  this.config_ = null;
491
557
  }
@@ -517,7 +583,7 @@ function createDebouncedSignal(sourceSignal, config) {
517
583
  const signalId = config.signalId ?? `${sourceSignal.signalId}-debounced`;
518
584
  const internalSignal = new StateSignal({
519
585
  signalId: `${signalId}-internal`,
520
- initialValue: sourceSignal.value
586
+ initialValue: sourceSignal.get()
521
587
  });
522
588
  const debouncer = (0, import_debounce.createDebouncer)({
523
589
  ...config,
@@ -529,7 +595,7 @@ function createDebouncedSignal(sourceSignal, config) {
529
595
  return createComputedSignal({
530
596
  signalId,
531
597
  deps: [internalSignal],
532
- get: () => internalSignal.value,
598
+ get: () => internalSignal.get(),
533
599
  onDestroy: () => {
534
600
  if (internalSignal.isDestroyed) return;
535
601
  subscription.unsubscribe();