ngx-signal-plus 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/fesm2022/ngx-signal-plus.mjs +170 -140
- package/fesm2022/ngx-signal-plus.mjs.map +1 -1
- package/index.d.ts +15 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://angular.dev/)
|
|
4
4
|
[](https://www.npmjs.com/package/ngx-signal-plus)
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|
Bring validation, persistence, undo/redo, and reactive queries to Angular Signals on Angular 16+.
|
|
8
8
|
|
|
@@ -70,7 +70,7 @@ export class CounterComponent {
|
|
|
70
70
|
- Signal creation: `sp`, `spCounter`, `spToggle`, `spForm`, `spComputed`
|
|
71
71
|
- Signal enhancement: `enhance`
|
|
72
72
|
- Operators: `spMap`, `spFilter`, `spDebounceTime`, `spThrottleTime`, `spDelay`, `spDistinctUntilChanged`
|
|
73
|
-
- Developer experience: `spCombine`, `spAll`, `spAny`, `spEffect`, `spDebug`, `spMonitor`, `sp().debug(label)`
|
|
73
|
+
- Developer experience: `spCombine`, `spAll`, `spAny`, `spEffect`, `spDebug`, `spMonitor`, `sp().debug(label)`, `sp().monitor(options)`
|
|
74
74
|
- Forms and groups: `spForm`, `spFormGroup`
|
|
75
75
|
- Async helpers: `spAsync`, `spCollection`
|
|
76
76
|
- Reactive queries: `spQuery`, `createDependentQuery`, `spInfiniteQuery`, `spMutation`, `QueryClient`, `setGlobalQueryClient`, `getGlobalQueryClient`
|
|
@@ -82,6 +82,8 @@ export class CounterComponent {
|
|
|
82
82
|
|
|
83
83
|
- `spComputed()` now exposes a read-only surface via `ReadonlySignalPlus<T>`.
|
|
84
84
|
- `SignalPlus<T>` now includes `errors: Signal<string[]>` for consistent validation error access.
|
|
85
|
+
- Builder monitoring is available via `sp().monitor(options)` and records updates through `spMonitor`.
|
|
86
|
+
- Registered middleware now runs in normal `set`/`setValue`/`update` runtime paths for built signals.
|
|
85
87
|
|
|
86
88
|
## Comparisons
|
|
87
89
|
|
|
@@ -121,3 +123,5 @@ MIT
|
|
|
121
123
|
|
|
122
124
|
|
|
123
125
|
|
|
126
|
+
|
|
127
|
+
|
|
@@ -323,6 +323,138 @@ const spDebug = {
|
|
|
323
323
|
},
|
|
324
324
|
};
|
|
325
325
|
|
|
326
|
+
const metrics = new Map();
|
|
327
|
+
let globalEnabled = true;
|
|
328
|
+
const ensureMetric = (name) => {
|
|
329
|
+
const existing = metrics.get(name);
|
|
330
|
+
if (existing) {
|
|
331
|
+
return existing;
|
|
332
|
+
}
|
|
333
|
+
const created = {
|
|
334
|
+
name,
|
|
335
|
+
updates: 0,
|
|
336
|
+
enabled: true,
|
|
337
|
+
totalDurationMs: 0,
|
|
338
|
+
averageDurationMs: 0,
|
|
339
|
+
maxDurationMs: 0,
|
|
340
|
+
lastDurationMs: 0,
|
|
341
|
+
lastUpdated: null,
|
|
342
|
+
};
|
|
343
|
+
metrics.set(name, created);
|
|
344
|
+
return created;
|
|
345
|
+
};
|
|
346
|
+
const spMonitor = {
|
|
347
|
+
trackSignal(name) {
|
|
348
|
+
ensureMetric(name);
|
|
349
|
+
},
|
|
350
|
+
recordUpdate(name, durationMs = 0) {
|
|
351
|
+
const metric = ensureMetric(name);
|
|
352
|
+
if (!globalEnabled || !metric.enabled) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
metric.updates += 1;
|
|
356
|
+
metric.lastDurationMs = durationMs;
|
|
357
|
+
metric.totalDurationMs += durationMs;
|
|
358
|
+
metric.averageDurationMs = metric.totalDurationMs / metric.updates;
|
|
359
|
+
metric.maxDurationMs = Math.max(metric.maxDurationMs, durationMs);
|
|
360
|
+
metric.lastUpdated = Date.now();
|
|
361
|
+
},
|
|
362
|
+
enable(name) {
|
|
363
|
+
ensureMetric(name).enabled = true;
|
|
364
|
+
},
|
|
365
|
+
disable(name) {
|
|
366
|
+
ensureMetric(name).enabled = false;
|
|
367
|
+
},
|
|
368
|
+
enableAll() {
|
|
369
|
+
globalEnabled = true;
|
|
370
|
+
},
|
|
371
|
+
disableAll() {
|
|
372
|
+
globalEnabled = false;
|
|
373
|
+
},
|
|
374
|
+
getHotSignals(limit = 10) {
|
|
375
|
+
return Array.from(metrics.values())
|
|
376
|
+
.filter((metric) => metric.enabled)
|
|
377
|
+
.sort((a, b) => b.updates - a.updates)
|
|
378
|
+
.slice(0, limit)
|
|
379
|
+
.map((metric) => ({ ...metric }));
|
|
380
|
+
},
|
|
381
|
+
getSlowSignals(thresholdMs = 16) {
|
|
382
|
+
return Array.from(metrics.values())
|
|
383
|
+
.filter((metric) => metric.enabled && metric.averageDurationMs >= thresholdMs)
|
|
384
|
+
.sort((a, b) => b.averageDurationMs - a.averageDurationMs)
|
|
385
|
+
.map((metric) => ({ ...metric }));
|
|
386
|
+
},
|
|
387
|
+
exportMetrics(format = 'object') {
|
|
388
|
+
const exported = Array.from(metrics.values()).map((metric) => ({ ...metric }));
|
|
389
|
+
return format === 'json' ? JSON.stringify(exported) : exported;
|
|
390
|
+
},
|
|
391
|
+
clear() {
|
|
392
|
+
metrics.clear();
|
|
393
|
+
globalEnabled = true;
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Middleware system for intercepting signal operations.
|
|
399
|
+
*/
|
|
400
|
+
const middlewareRegistry = [];
|
|
401
|
+
function spUseMiddleware(middleware) {
|
|
402
|
+
if (!middlewareRegistry.some((m) => m.name === middleware.name)) {
|
|
403
|
+
middlewareRegistry.push(middleware);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function spRemoveMiddleware(name) {
|
|
407
|
+
const index = middlewareRegistry.findIndex((m) => m.name === name);
|
|
408
|
+
if (index === -1)
|
|
409
|
+
return false;
|
|
410
|
+
middlewareRegistry.splice(index, 1);
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
function spClearMiddleware() {
|
|
414
|
+
middlewareRegistry.length = 0;
|
|
415
|
+
}
|
|
416
|
+
function spGetMiddlewareCount() {
|
|
417
|
+
return middlewareRegistry.length;
|
|
418
|
+
}
|
|
419
|
+
function spRunMiddleware(context) {
|
|
420
|
+
for (const m of middlewareRegistry) {
|
|
421
|
+
try {
|
|
422
|
+
m.onSet?.(context);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
/* ignore */
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function spRunMiddlewareError(error, context) {
|
|
430
|
+
for (const m of middlewareRegistry) {
|
|
431
|
+
try {
|
|
432
|
+
m.onError?.(error, context);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
/* ignore */
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function spLoggerMiddleware(prefix = '[Signal]') {
|
|
440
|
+
return {
|
|
441
|
+
name: 'sp-logger',
|
|
442
|
+
onSet: (ctx) => console.log(`${prefix} ${ctx.signalName || 'signal'}: ${JSON.stringify(ctx.oldValue)} -> ${JSON.stringify(ctx.newValue)}`),
|
|
443
|
+
onError: (error, ctx) => console.error(`${prefix} Error in ${ctx.signalName || 'signal'}:`, error.message),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function spAnalyticsMiddleware(tracker) {
|
|
447
|
+
return {
|
|
448
|
+
name: 'sp-analytics',
|
|
449
|
+
onSet: (ctx) => tracker({
|
|
450
|
+
name: ctx.signalName || 'unknown',
|
|
451
|
+
oldValue: ctx.oldValue,
|
|
452
|
+
newValue: ctx.newValue,
|
|
453
|
+
timestamp: ctx.timestamp,
|
|
454
|
+
}),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
326
458
|
/**
|
|
327
459
|
* @fileoverview Builder class for creating enhanced Angular signals
|
|
328
460
|
* Provides a fluent API for configuring signals with features like:
|
|
@@ -345,6 +477,7 @@ const spDebug = {
|
|
|
345
477
|
*/
|
|
346
478
|
class SignalBuilder {
|
|
347
479
|
options;
|
|
480
|
+
static monitorCounter = 0;
|
|
348
481
|
/**
|
|
349
482
|
* Creates a new SignalBuilder instance
|
|
350
483
|
* @param initialValue Initial value for the signal
|
|
@@ -448,6 +581,10 @@ class SignalBuilder {
|
|
|
448
581
|
this.options.debugLabel = label;
|
|
449
582
|
return this;
|
|
450
583
|
}
|
|
584
|
+
monitor(options = {}) {
|
|
585
|
+
this.options.monitorOptions = { ...options };
|
|
586
|
+
return this;
|
|
587
|
+
}
|
|
451
588
|
/**
|
|
452
589
|
* Transforms signal value to a different type
|
|
453
590
|
* @param fn Transform function from T to R
|
|
@@ -479,6 +616,9 @@ class SignalBuilder {
|
|
|
479
616
|
newBuilder.options.storageKey = this.options.storageKey;
|
|
480
617
|
newBuilder.options.debounceTime = this.options.debounceTime;
|
|
481
618
|
newBuilder.options.autoCleanup = this.options.autoCleanup;
|
|
619
|
+
newBuilder.options.monitorOptions = this.options.monitorOptions
|
|
620
|
+
? { ...this.options.monitorOptions }
|
|
621
|
+
: undefined;
|
|
482
622
|
return newBuilder;
|
|
483
623
|
}
|
|
484
624
|
/**
|
|
@@ -549,6 +689,20 @@ class SignalBuilder {
|
|
|
549
689
|
const asyncErrorsSignal = signal([], ...(ngDevMode ? [{ debugName: "asyncErrorsSignal" }] : []));
|
|
550
690
|
const enforceHistorySize = (histArray) => this.limitByHistorySize(histArray, this.options.historySize);
|
|
551
691
|
const enforceRedoStackSize = (redoArray) => this.limitByHistorySize(redoArray, this.options.historySize);
|
|
692
|
+
const monitorOptions = this.options.monitorOptions;
|
|
693
|
+
const monitorEnabled = Boolean(monitorOptions &&
|
|
694
|
+
((monitorOptions.trackUpdates ?? true) || monitorOptions.trackPerformance));
|
|
695
|
+
const trackPerformance = Boolean(monitorOptions?.trackPerformance);
|
|
696
|
+
const signalName = monitorOptions?.label ||
|
|
697
|
+
this.options.debugLabel ||
|
|
698
|
+
this.options.storageKey ||
|
|
699
|
+
`signal-${SignalBuilder.monitorCounter++}`;
|
|
700
|
+
const createMiddlewareContext = (oldValue, newValue) => ({
|
|
701
|
+
signalName,
|
|
702
|
+
oldValue,
|
|
703
|
+
newValue,
|
|
704
|
+
timestamp: Date.now(),
|
|
705
|
+
});
|
|
552
706
|
/**
|
|
553
707
|
* Helper function to run async validation with debouncing and cancellation
|
|
554
708
|
* @param value The value to validate
|
|
@@ -803,21 +957,20 @@ class SignalBuilder {
|
|
|
803
957
|
}
|
|
804
958
|
// Only update if value has changed
|
|
805
959
|
if (hasChanged) {
|
|
806
|
-
|
|
960
|
+
const oldValue = conditionalClone(writable());
|
|
961
|
+
spRunMiddleware(createMiddlewareContext(oldValue, conditionalClone(transformedValue)));
|
|
962
|
+
const monitorStart = monitorEnabled && trackPerformance ? performance.now() : 0;
|
|
963
|
+
previousValue = oldValue;
|
|
807
964
|
writable.set(transformedValue);
|
|
808
|
-
// Update history if enabled and value has changed
|
|
809
965
|
if (this.options.enableHistory && !isProcessingDebounce) {
|
|
810
|
-
// Clear redo stack when new value is set
|
|
811
966
|
redoStack = [];
|
|
812
967
|
const currentHistory = history();
|
|
813
968
|
const newHistory = [
|
|
814
969
|
...currentHistory,
|
|
815
970
|
conditionalClone(transformedValue),
|
|
816
971
|
];
|
|
817
|
-
// Enforce history size limit using helper function
|
|
818
972
|
history.set(enforceHistorySize(newHistory));
|
|
819
973
|
}
|
|
820
|
-
// Handle storage
|
|
821
974
|
if (this.options.storageKey && isBrowser()) {
|
|
822
975
|
try {
|
|
823
976
|
isProcessingStorage = true;
|
|
@@ -835,7 +988,10 @@ class SignalBuilder {
|
|
|
835
988
|
isProcessingStorage = false;
|
|
836
989
|
}
|
|
837
990
|
}
|
|
838
|
-
|
|
991
|
+
if (monitorEnabled) {
|
|
992
|
+
const duration = trackPerformance ? performance.now() - monitorStart : 0;
|
|
993
|
+
spMonitor.recordUpdate(signalName, duration);
|
|
994
|
+
}
|
|
839
995
|
if (!isCleanedUp) {
|
|
840
996
|
notifySubscribers(transformedValue);
|
|
841
997
|
}
|
|
@@ -851,6 +1007,9 @@ class SignalBuilder {
|
|
|
851
1007
|
if (this.options.debugLabel) {
|
|
852
1008
|
spDebug.trackSignal(this.options.debugLabel, writable());
|
|
853
1009
|
}
|
|
1010
|
+
if (monitorEnabled) {
|
|
1011
|
+
spMonitor.trackSignal(signalName);
|
|
1012
|
+
}
|
|
854
1013
|
const processValue = (value) => {
|
|
855
1014
|
// Prevent setValue after destroy
|
|
856
1015
|
if (isCleanedUp) {
|
|
@@ -896,6 +1055,7 @@ class SignalBuilder {
|
|
|
896
1055
|
}
|
|
897
1056
|
}
|
|
898
1057
|
catch (error) {
|
|
1058
|
+
spRunMiddlewareError(error, createMiddlewareContext(conditionalClone(writable()), conditionalClone(value)));
|
|
899
1059
|
this.handleError(error);
|
|
900
1060
|
throw error;
|
|
901
1061
|
}
|
|
@@ -915,14 +1075,16 @@ class SignalBuilder {
|
|
|
915
1075
|
set: processValue,
|
|
916
1076
|
setValue: processValue,
|
|
917
1077
|
update: (fn) => {
|
|
1078
|
+
let newValue;
|
|
918
1079
|
try {
|
|
919
|
-
|
|
920
|
-
processValue(newValue);
|
|
1080
|
+
newValue = fn(writable());
|
|
921
1081
|
}
|
|
922
1082
|
catch (error) {
|
|
1083
|
+
spRunMiddlewareError(error, createMiddlewareContext(conditionalClone(writable()), conditionalClone(writable())));
|
|
923
1084
|
this.handleError(error);
|
|
924
1085
|
throw error;
|
|
925
1086
|
}
|
|
1087
|
+
processValue(newValue);
|
|
926
1088
|
},
|
|
927
1089
|
reset: () => {
|
|
928
1090
|
try {
|
|
@@ -4180,77 +4342,6 @@ function spComputed(fn, options = {}) {
|
|
|
4180
4342
|
};
|
|
4181
4343
|
}
|
|
4182
4344
|
|
|
4183
|
-
const metrics = new Map();
|
|
4184
|
-
let globalEnabled = true;
|
|
4185
|
-
const ensureMetric = (name) => {
|
|
4186
|
-
const existing = metrics.get(name);
|
|
4187
|
-
if (existing) {
|
|
4188
|
-
return existing;
|
|
4189
|
-
}
|
|
4190
|
-
const created = {
|
|
4191
|
-
name,
|
|
4192
|
-
updates: 0,
|
|
4193
|
-
enabled: true,
|
|
4194
|
-
totalDurationMs: 0,
|
|
4195
|
-
averageDurationMs: 0,
|
|
4196
|
-
maxDurationMs: 0,
|
|
4197
|
-
lastDurationMs: 0,
|
|
4198
|
-
lastUpdated: null,
|
|
4199
|
-
};
|
|
4200
|
-
metrics.set(name, created);
|
|
4201
|
-
return created;
|
|
4202
|
-
};
|
|
4203
|
-
const spMonitor = {
|
|
4204
|
-
trackSignal(name) {
|
|
4205
|
-
ensureMetric(name);
|
|
4206
|
-
},
|
|
4207
|
-
recordUpdate(name, durationMs = 0) {
|
|
4208
|
-
const metric = ensureMetric(name);
|
|
4209
|
-
if (!globalEnabled || !metric.enabled) {
|
|
4210
|
-
return;
|
|
4211
|
-
}
|
|
4212
|
-
metric.updates += 1;
|
|
4213
|
-
metric.lastDurationMs = durationMs;
|
|
4214
|
-
metric.totalDurationMs += durationMs;
|
|
4215
|
-
metric.averageDurationMs = metric.totalDurationMs / metric.updates;
|
|
4216
|
-
metric.maxDurationMs = Math.max(metric.maxDurationMs, durationMs);
|
|
4217
|
-
metric.lastUpdated = Date.now();
|
|
4218
|
-
},
|
|
4219
|
-
enable(name) {
|
|
4220
|
-
ensureMetric(name).enabled = true;
|
|
4221
|
-
},
|
|
4222
|
-
disable(name) {
|
|
4223
|
-
ensureMetric(name).enabled = false;
|
|
4224
|
-
},
|
|
4225
|
-
enableAll() {
|
|
4226
|
-
globalEnabled = true;
|
|
4227
|
-
},
|
|
4228
|
-
disableAll() {
|
|
4229
|
-
globalEnabled = false;
|
|
4230
|
-
},
|
|
4231
|
-
getHotSignals(limit = 10) {
|
|
4232
|
-
return Array.from(metrics.values())
|
|
4233
|
-
.filter((metric) => metric.enabled)
|
|
4234
|
-
.sort((a, b) => b.updates - a.updates)
|
|
4235
|
-
.slice(0, limit)
|
|
4236
|
-
.map((metric) => ({ ...metric }));
|
|
4237
|
-
},
|
|
4238
|
-
getSlowSignals(thresholdMs = 16) {
|
|
4239
|
-
return Array.from(metrics.values())
|
|
4240
|
-
.filter((metric) => metric.enabled && metric.averageDurationMs >= thresholdMs)
|
|
4241
|
-
.sort((a, b) => b.averageDurationMs - a.averageDurationMs)
|
|
4242
|
-
.map((metric) => ({ ...metric }));
|
|
4243
|
-
},
|
|
4244
|
-
exportMetrics(format = 'object') {
|
|
4245
|
-
const exported = Array.from(metrics.values()).map((metric) => ({ ...metric }));
|
|
4246
|
-
return format === 'json' ? JSON.stringify(exported) : exported;
|
|
4247
|
-
},
|
|
4248
|
-
clear() {
|
|
4249
|
-
metrics.clear();
|
|
4250
|
-
globalEnabled = true;
|
|
4251
|
-
},
|
|
4252
|
-
};
|
|
4253
|
-
|
|
4254
4345
|
function spEffect(callback, options = {}) {
|
|
4255
4346
|
const paused = signal(false, ...(ngDevMode ? [{ debugName: "paused" }] : []));
|
|
4256
4347
|
let timeoutId = null;
|
|
@@ -4627,67 +4718,6 @@ function spFormGroup(config, options) {
|
|
|
4627
4718
|
};
|
|
4628
4719
|
}
|
|
4629
4720
|
|
|
4630
|
-
/**
|
|
4631
|
-
* Middleware system for intercepting signal operations.
|
|
4632
|
-
*/
|
|
4633
|
-
const middlewareRegistry = [];
|
|
4634
|
-
function spUseMiddleware(middleware) {
|
|
4635
|
-
if (!middlewareRegistry.some((m) => m.name === middleware.name)) {
|
|
4636
|
-
middlewareRegistry.push(middleware);
|
|
4637
|
-
}
|
|
4638
|
-
}
|
|
4639
|
-
function spRemoveMiddleware(name) {
|
|
4640
|
-
const index = middlewareRegistry.findIndex((m) => m.name === name);
|
|
4641
|
-
if (index === -1)
|
|
4642
|
-
return false;
|
|
4643
|
-
middlewareRegistry.splice(index, 1);
|
|
4644
|
-
return true;
|
|
4645
|
-
}
|
|
4646
|
-
function spClearMiddleware() {
|
|
4647
|
-
middlewareRegistry.length = 0;
|
|
4648
|
-
}
|
|
4649
|
-
function spGetMiddlewareCount() {
|
|
4650
|
-
return middlewareRegistry.length;
|
|
4651
|
-
}
|
|
4652
|
-
function spRunMiddleware(context) {
|
|
4653
|
-
for (const m of middlewareRegistry) {
|
|
4654
|
-
try {
|
|
4655
|
-
m.onSet?.(context);
|
|
4656
|
-
}
|
|
4657
|
-
catch {
|
|
4658
|
-
/* ignore */
|
|
4659
|
-
}
|
|
4660
|
-
}
|
|
4661
|
-
}
|
|
4662
|
-
function spRunMiddlewareError(error, context) {
|
|
4663
|
-
for (const m of middlewareRegistry) {
|
|
4664
|
-
try {
|
|
4665
|
-
m.onError?.(error, context);
|
|
4666
|
-
}
|
|
4667
|
-
catch {
|
|
4668
|
-
/* ignore */
|
|
4669
|
-
}
|
|
4670
|
-
}
|
|
4671
|
-
}
|
|
4672
|
-
function spLoggerMiddleware(prefix = '[Signal]') {
|
|
4673
|
-
return {
|
|
4674
|
-
name: 'sp-logger',
|
|
4675
|
-
onSet: (ctx) => console.log(`${prefix} ${ctx.signalName || 'signal'}: ${JSON.stringify(ctx.oldValue)} -> ${JSON.stringify(ctx.newValue)}`),
|
|
4676
|
-
onError: (error, ctx) => console.error(`${prefix} Error in ${ctx.signalName || 'signal'}:`, error.message),
|
|
4677
|
-
};
|
|
4678
|
-
}
|
|
4679
|
-
function spAnalyticsMiddleware(tracker) {
|
|
4680
|
-
return {
|
|
4681
|
-
name: 'sp-analytics',
|
|
4682
|
-
onSet: (ctx) => tracker({
|
|
4683
|
-
name: ctx.signalName || 'unknown',
|
|
4684
|
-
oldValue: ctx.oldValue,
|
|
4685
|
-
newValue: ctx.newValue,
|
|
4686
|
-
timestamp: ctx.timestamp,
|
|
4687
|
-
}),
|
|
4688
|
-
};
|
|
4689
|
-
}
|
|
4690
|
-
|
|
4691
4721
|
function extractErrorMessages(error) {
|
|
4692
4722
|
if (!error) {
|
|
4693
4723
|
return [];
|