@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.mjs CHANGED
@@ -1,11 +1,12 @@
1
- /* @alwatr/signal v5.1.0 */
1
+ /* @alwatr/signal v5.2.1 */
2
2
 
3
3
  // src/core/signal-base.ts
4
4
  var SignalBase = class {
5
5
  constructor(config_) {
6
6
  this.config_ = config_;
7
7
  /**
8
- * The unique identifier for this signal instance. Useful for debugging.
8
+ * The unique identifier for this signal instance.
9
+ * Useful for debugging and logging.
9
10
  */
10
11
  this.signalId = this.config_.signalId;
11
12
  /**
@@ -13,38 +14,33 @@ var SignalBase = class {
13
14
  * @protected
14
15
  */
15
16
  this.observers_ = [];
16
- this.isDestroyed_ = false;
17
17
  /**
18
- * Throws an error if the signal has been destroyed.
19
- * This is a safeguard to prevent interaction with a defunct signal.
20
- * @protected
18
+ * A flag indicating whether the signal has been destroyed.
19
+ * @private
21
20
  */
22
- this.checkDestroyed_ = () => {
23
- if (this.isDestroyed_) {
24
- this.logger_.accident("checkDestroyed_", "attempt_to_use_destroyed_signal");
25
- throw new Error(`Cannot interact with a destroyed signal (id: ${this.signalId})`);
26
- }
27
- };
21
+ this.isDestroyed__ = false;
28
22
  }
29
23
  /**
30
24
  * Indicates whether the signal has been destroyed.
31
25
  * A destroyed signal cannot be used and will throw an error if interacted with.
26
+ *
32
27
  * @returns `true` if the signal is destroyed, `false` otherwise.
33
28
  */
34
29
  get isDestroyed() {
35
- return this.isDestroyed_;
30
+ return this.isDestroyed__;
36
31
  }
37
32
  /**
38
33
  * Removes a specific observer from the observers list.
34
+ *
39
35
  * @param observer The observer instance to remove.
40
36
  * @protected
41
37
  */
42
38
  removeObserver_(observer) {
43
- if (this.isDestroyed_) {
39
+ this.logger_.logMethod?.("removeObserver_");
40
+ if (this.isDestroyed__) {
44
41
  this.logger_.incident?.("removeObserver_", "remove_observer_on_destroyed_signal");
45
42
  return;
46
43
  }
47
- this.logger_.logMethod?.("removeObserver_");
48
44
  const index = this.observers_.indexOf(observer);
49
45
  if (index !== -1) {
50
46
  this.observers_.splice(index, 1);
@@ -68,8 +64,9 @@ var SignalBase = class {
68
64
  } else {
69
65
  this.observers_.push(observer);
70
66
  }
71
- const unsubscribe = () => this.removeObserver_(observer);
72
- return { unsubscribe };
67
+ return {
68
+ unsubscribe: () => this.removeObserver_(observer)
69
+ };
73
70
  }
74
71
  /**
75
72
  * Notifies all registered observers about a new value.
@@ -81,11 +78,11 @@ var SignalBase = class {
81
78
  * @protected
82
79
  */
83
80
  notify_(value) {
84
- if (this.isDestroyed_) {
81
+ this.logger_.logMethodArgs?.("notify_", value);
82
+ if (this.isDestroyed__) {
85
83
  this.logger_.incident?.("notify_", "notify_on_destroyed_signal");
86
84
  return;
87
85
  }
88
- this.logger_.logMethodArgs?.("notify_", value);
89
86
  const currentObservers = [...this.observers_];
90
87
  for (const observer of currentObservers) {
91
88
  if (observer.options?.once) {
@@ -94,9 +91,7 @@ var SignalBase = class {
94
91
  try {
95
92
  const result = observer.callback(value);
96
93
  if (result instanceof Promise) {
97
- result.catch((err) => {
98
- this.logger_.error("notify_", "async_callback_failed", err, { observer });
99
- });
94
+ result.catch((err) => this.logger_.error("notify_", "async_callback_failed", err, { observer }));
100
95
  }
101
96
  } catch (err) {
102
97
  this.logger_.error("notify_", "sync_callback_failed", err);
@@ -138,12 +133,26 @@ var SignalBase = class {
138
133
  */
139
134
  destroy() {
140
135
  this.logger_.logMethod?.("destroy");
141
- if (this.isDestroyed_) return;
142
- this.isDestroyed_ = true;
136
+ if (this.isDestroyed__) {
137
+ this.logger_.incident?.("destroy_", "double_destroy_attempt");
138
+ return;
139
+ }
140
+ this.isDestroyed__ = true;
143
141
  this.observers_.length = 0;
144
142
  this.config_.onDestroy?.();
145
143
  this.config_ = null;
146
144
  }
145
+ /**
146
+ * Throws an error if the signal has been destroyed.
147
+ * This is a safeguard to prevent interaction with a defunct signal.
148
+ * @protected
149
+ */
150
+ checkDestroyed_() {
151
+ if (this.isDestroyed__) {
152
+ this.logger_.accident("checkDestroyed_", "attempt_to_use_destroyed_signal");
153
+ throw new Error(`Cannot interact with a destroyed signal (id: ${this.signalId})`);
154
+ }
155
+ }
147
156
  };
148
157
 
149
158
  // src/core/event-signal.ts
@@ -152,6 +161,10 @@ import { createLogger } from "@alwatr/logger";
152
161
  var EventSignal = class extends SignalBase {
153
162
  constructor(config) {
154
163
  super(config);
164
+ /**
165
+ * The logger instance for this signal.
166
+ * @protected
167
+ */
155
168
  this.logger_ = createLogger(`event-signal: ${this.signalId}`);
156
169
  this.logger_.logMethod?.("constructor");
157
170
  }
@@ -163,11 +176,9 @@ var EventSignal = class extends SignalBase {
163
176
  * @param payload The data to send with the event.
164
177
  */
165
178
  dispatch(payload) {
166
- this.logger_.logMethodArgs?.("dispatch", payload);
179
+ this.logger_.logMethodArgs?.("dispatch", { payload });
167
180
  this.checkDestroyed_();
168
- delay.nextMicrotask().then(() => {
169
- this.notify_(payload);
170
- });
181
+ delay.nextMicrotask().then(() => this.notify_(payload));
171
182
  }
172
183
  };
173
184
 
@@ -177,6 +188,10 @@ import { createLogger as createLogger2 } from "@alwatr/logger";
177
188
  var StateSignal = class extends SignalBase {
178
189
  constructor(config) {
179
190
  super(config);
191
+ /**
192
+ * The logger instance for this signal.
193
+ * @protected
194
+ */
180
195
  this.logger_ = createLogger2(`state-signal: ${this.signalId}`);
181
196
  this.value__ = config.initialValue;
182
197
  this.logger_.logMethodArgs?.("constructor", { initialValue: this.value__ });
@@ -187,9 +202,9 @@ var StateSignal = class extends SignalBase {
187
202
  * @returns The current value.
188
203
  *
189
204
  * @example
190
- * console.log(mySignal.value);
205
+ * console.log(mySignal.get());
191
206
  */
192
- get value() {
207
+ get() {
193
208
  this.checkDestroyed_();
194
209
  return this.value__;
195
210
  }
@@ -206,16 +221,16 @@ var StateSignal = class extends SignalBase {
206
221
  * mySignal.set(42);
207
222
  *
208
223
  * // For object types, it's best practice to set an immutable new object.
209
- * mySignal.set({ ...mySignal.value, property: 'new-value' });
224
+ * mySignal.set({ ...mySignal.get(), property: 'new-value' });
210
225
  */
211
226
  set(newValue) {
212
227
  this.logger_.logMethodArgs?.("set", { newValue });
213
228
  this.checkDestroyed_();
214
- if (Object.is(this.value__, newValue) && (typeof newValue !== "object" || newValue === null)) return;
229
+ if (Object.is(this.value__, newValue) && (typeof newValue !== "object" || newValue === null)) {
230
+ return;
231
+ }
215
232
  this.value__ = newValue;
216
- delay2.nextMicrotask().then(() => {
217
- this.notify_(newValue);
218
- });
233
+ delay2.nextMicrotask().then(() => this.notify_(newValue));
219
234
  }
220
235
  /**
221
236
  * Updates the signal's value based on its previous value.
@@ -250,11 +265,8 @@ var StateSignal = class extends SignalBase {
250
265
  subscribe(callback, options = {}) {
251
266
  this.logger_.logMethodArgs?.("subscribe", { options });
252
267
  this.checkDestroyed_();
253
- const receivePrevious = options.receivePrevious !== false;
254
- if (receivePrevious) {
255
- delay2.nextMicrotask().then(() => callback(this.value__)).catch((err) => {
256
- this.logger_.error("subscribe", "run_callback_immediate_failed", err);
257
- });
268
+ if (options.receivePrevious !== false) {
269
+ delay2.nextMicrotask().then(() => callback(this.value__)).catch((err) => this.logger_.error("subscribe", "immediate_callback_failed", err));
258
270
  if (options.once) {
259
271
  return { unsubscribe: () => {
260
272
  } };
@@ -278,25 +290,38 @@ import { createLogger as createLogger3 } from "@alwatr/logger";
278
290
  var ComputedSignal = class {
279
291
  constructor(config_) {
280
292
  this.config_ = config_;
293
+ /**
294
+ * The unique identifier for this signal instance.
295
+ */
281
296
  this.signalId = this.config_.signalId;
297
+ /**
298
+ * The logger instance for this signal.
299
+ * @protected
300
+ */
282
301
  this.logger_ = createLogger3(`computed-signal: ${this.signalId}`);
283
302
  /**
284
303
  * The internal `StateSignal` that holds the computed value.
285
- * This is how the computed signal provides `.value` and `.subscribe()` methods.
304
+ * This is how the computed signal provides `.get()` and `.subscribe()` methods.
286
305
  * @protected
287
306
  */
288
307
  this.internalSignal_ = new StateSignal({
289
- signalId: this.signalId + "-internal",
308
+ signalId: `${this.signalId}-internal`,
290
309
  initialValue: this.config_.get()
291
310
  });
292
- this.subscriptionList__ = [];
311
+ /**
312
+ * A list of subscriptions to dependency signals.
313
+ * @private
314
+ */
315
+ this.dependencySubscriptions__ = [];
316
+ /**
317
+ * A flag to prevent concurrent recalculations.
318
+ * @private
319
+ */
293
320
  this.isRecalculating__ = false;
294
- this.subscribe = this.internalSignal_.subscribe.bind(this.internalSignal_);
295
- this.untilNext = this.internalSignal_.untilNext.bind(this.internalSignal_);
296
321
  this.logger_.logMethod?.("constructor");
297
322
  this.recalculate_ = this.recalculate_.bind(this);
298
323
  for (const signal of config_.deps) {
299
- this.subscriptionList__.push(signal.subscribe(this.recalculate_, { receivePrevious: false }));
324
+ this.dependencySubscriptions__.push(signal.subscribe(this.recalculate_, { receivePrevious: false }));
300
325
  }
301
326
  }
302
327
  /**
@@ -306,8 +331,8 @@ var ComputedSignal = class {
306
331
  * @returns The current computed value.
307
332
  * @throws {Error} If accessed after the signal has been destroyed.
308
333
  */
309
- get value() {
310
- return this.internalSignal_.value;
334
+ get() {
335
+ return this.internalSignal_.get();
311
336
  }
312
337
  /**
313
338
  * Indicates whether the computed signal has been destroyed.
@@ -317,6 +342,25 @@ var ComputedSignal = class {
317
342
  get isDestroyed() {
318
343
  return this.internalSignal_.isDestroyed;
319
344
  }
345
+ /**
346
+ * Subscribes a listener to this signal.
347
+ * The listener will be called whenever the computed value changes.
348
+ *
349
+ * @param callback The function to be called with the new value.
350
+ * @param options Subscription options.
351
+ * @returns A `SubscribeResult` object with an `unsubscribe` method.
352
+ */
353
+ subscribe(callback, options) {
354
+ return this.internalSignal_.subscribe(callback, options);
355
+ }
356
+ /**
357
+ * Returns a Promise that resolves with the next computed value.
358
+ *
359
+ * @returns A Promise that resolves with the next value.
360
+ */
361
+ untilNext() {
362
+ return this.internalSignal_.untilNext();
363
+ }
320
364
  /**
321
365
  * Permanently disposes of the computed signal.
322
366
  *
@@ -324,18 +368,18 @@ var ComputedSignal = class {
324
368
  * stopping future recalculations and allowing the signal to be garbage collected.
325
369
  * Failure to call `destroy()` will result in memory leaks.
326
370
  *
327
- * After `destroy()` is called, any attempt to access `.value` or `.subscribe()` will throw an error.
371
+ * After `destroy()` is called, any attempt to access `.get()` or `.subscribe()` will throw an error.
328
372
  */
329
373
  destroy() {
330
374
  this.logger_.logMethod?.("destroy");
331
- if (this.internalSignal_.isDestroyed) {
375
+ if (this.isDestroyed) {
332
376
  this.logger_.incident?.("destroy", "already_destroyed");
333
377
  return;
334
378
  }
335
- for (const subscription of this.subscriptionList__) {
379
+ for (const subscription of this.dependencySubscriptions__) {
336
380
  subscription.unsubscribe();
337
381
  }
338
- this.subscriptionList__.length = 0;
382
+ this.dependencySubscriptions__.length = 0;
339
383
  this.internalSignal_.destroy();
340
384
  this.config_.onDestroy?.();
341
385
  this.config_ = null;
@@ -349,23 +393,24 @@ var ComputedSignal = class {
349
393
  * @protected
350
394
  */
351
395
  async recalculate_() {
396
+ this.logger_.logMethod?.("recalculate_");
352
397
  if (this.internalSignal_.isDestroyed) {
353
- this.logger_.incident?.("recalculate", "recalculate_on_destroyed_signal");
398
+ this.logger_.incident?.("recalculate_", "recalculate_on_destroyed_signal");
354
399
  return;
355
400
  }
356
401
  if (this.isRecalculating__) {
357
- this.logger_.logMethod?.("recalculate_//skipped");
402
+ this.logger_.logStep?.("recalculate_", "skipping_recalculation_already_scheduled");
358
403
  return;
359
404
  }
360
- this.logger_.logMethod?.("recalculate_//scheduled");
361
405
  this.isRecalculating__ = true;
362
406
  try {
363
407
  await delay3.nextMacrotask();
364
- if (this.internalSignal_.isDestroyed) {
365
- this.logger_.incident?.("recalculate", "destroyed_during_delay");
408
+ if (this.isDestroyed) {
409
+ this.logger_.incident?.("recalculate_", "destroyed_during_delay");
410
+ this.isRecalculating__ = false;
366
411
  return;
367
412
  }
368
- this.logger_.logMethod?.("recalculate_//executing");
413
+ this.logger_.logStep?.("recalculate_", "recalculating_value");
369
414
  this.internalSignal_.set(this.config_.get());
370
415
  } catch (err) {
371
416
  this.logger_.error("recalculate_", "recalculation_failed", err);
@@ -380,22 +425,43 @@ import { createLogger as createLogger4 } from "@alwatr/logger";
380
425
  var EffectSignal = class {
381
426
  constructor(config_) {
382
427
  this.config_ = config_;
383
- this.logger_ = createLogger4(`effect-signal`);
384
- this.subscriptionList__ = [];
428
+ /**
429
+ * The unique identifier for this signal instance.
430
+ */
431
+ this.signalId = this.config_.signalId ? this.config_.signalId : `[${this.config_.deps.map((dep) => dep.signalId).join(", ")}]`;
432
+ /**
433
+ * The logger instance for this signal.
434
+ * @protected
435
+ */
436
+ this.logger_ = createLogger4(`effect-signal: ${this.signalId}`);
437
+ /**
438
+ * A list of subscriptions to dependency signals.
439
+ * @private
440
+ */
441
+ this.dependencySubscriptions__ = [];
442
+ /**
443
+ * A flag to prevent concurrent executions of the effect.
444
+ * @private
445
+ */
385
446
  this.isRunning__ = false;
447
+ /**
448
+ * A flag indicating whether the effect has been destroyed.
449
+ * @private
450
+ */
386
451
  this.isDestroyed__ = false;
387
452
  this.logger_.logMethod?.("constructor");
388
- this.run_ = this.run_.bind(this);
453
+ this.scheduleExecution_ = this.scheduleExecution_.bind(this);
389
454
  for (const signal of config_.deps) {
390
- this.subscriptionList__.push(signal.subscribe(this.run_, { receivePrevious: false }));
455
+ this.dependencySubscriptions__.push(signal.subscribe(this.scheduleExecution_, { receivePrevious: false }));
391
456
  }
392
457
  if (config_.runImmediately === true) {
393
- void this.run_();
458
+ void this.scheduleExecution_();
394
459
  }
395
460
  }
396
461
  /**
397
462
  * Indicates whether the effect signal has been destroyed.
398
- * A destroyed signal cannot be used and will throw an error if interacted with.
463
+ * A destroyed signal will no longer execute its effect and cannot be reused.
464
+ *
399
465
  * @returns `true` if the signal is destroyed, `false` otherwise.
400
466
  */
401
467
  get isDestroyed() {
@@ -409,30 +475,30 @@ var EffectSignal = class {
409
475
  * dependencies change simultaneously.
410
476
  * @protected
411
477
  */
412
- async run_() {
478
+ async scheduleExecution_() {
479
+ this.logger_.logMethod?.("scheduleExecution_");
413
480
  if (this.isDestroyed__) {
414
- this.logger_.incident?.("run_", "run_on_destroyed_signal");
481
+ this.logger_.incident?.("scheduleExecution_", "schedule_execution_on_destroyed_signal");
415
482
  return;
416
483
  }
417
484
  if (this.isRunning__) {
418
- this.logger_.logMethod?.("run_//skipped");
485
+ this.logger_.logStep?.("scheduleExecution_", "skipped_because_already_running");
419
486
  return;
420
487
  }
421
- this.logger_.logMethod?.("run_//scheduled");
422
488
  this.isRunning__ = true;
423
489
  try {
424
490
  await delay4.nextMacrotask();
425
491
  if (this.isDestroyed__) {
426
- this.logger_.incident?.("run_", "destroyed_during_delay");
492
+ this.logger_.incident?.("scheduleExecution_", "destroyed_during_delay");
493
+ this.isRunning__ = false;
427
494
  return;
428
495
  }
429
- this.logger_.logMethod?.("run_//executing");
496
+ this.logger_.logStep?.("scheduleExecution_", "executing_effect");
430
497
  await this.config_.run();
431
498
  } catch (err) {
432
- this.logger_.error("run_", "effect_failed", err);
433
- } finally {
434
- this.isRunning__ = false;
499
+ this.logger_.error("scheduleExecution_", "effect_failed", err);
435
500
  }
501
+ this.isRunning__ = false;
436
502
  }
437
503
  /**
438
504
  * Permanently disposes of the effect signal.
@@ -448,10 +514,10 @@ var EffectSignal = class {
448
514
  return;
449
515
  }
450
516
  this.isDestroyed__ = true;
451
- for (const subscription of this.subscriptionList__) {
517
+ for (const subscription of this.dependencySubscriptions__) {
452
518
  subscription.unsubscribe();
453
519
  }
454
- this.subscriptionList__.length = 0;
520
+ this.dependencySubscriptions__.length = 0;
455
521
  this.config_.onDestroy?.();
456
522
  this.config_ = null;
457
523
  }
@@ -483,7 +549,7 @@ function createDebouncedSignal(sourceSignal, config) {
483
549
  const signalId = config.signalId ?? `${sourceSignal.signalId}-debounced`;
484
550
  const internalSignal = new StateSignal({
485
551
  signalId: `${signalId}-internal`,
486
- initialValue: sourceSignal.value
552
+ initialValue: sourceSignal.get()
487
553
  });
488
554
  const debouncer = createDebouncer({
489
555
  ...config,
@@ -495,7 +561,7 @@ function createDebouncedSignal(sourceSignal, config) {
495
561
  return createComputedSignal({
496
562
  signalId,
497
563
  deps: [internalSignal],
498
- get: () => internalSignal.value,
564
+ get: () => internalSignal.get(),
499
565
  onDestroy: () => {
500
566
  if (internalSignal.isDestroyed) return;
501
567
  subscription.unsubscribe();