ngx-signal-plus 2.5.0 → 2.7.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Angular 16-21](https://img.shields.io/badge/Angular-16--21-dd0031)](https://angular.dev/)
4
4
  [![npm version](https://img.shields.io/npm/v/ngx-signal-plus.svg)](https://www.npmjs.com/package/ngx-signal-plus)
5
- ![Coverage](https://img.shields.io/badge/coverage-90.44%25-brightgreen)
5
+ ![Coverage](https://img.shields.io/badge/coverage-90.92%25-brightgreen)
6
6
 
7
7
  Bring validation, persistence, undo/redo, and reactive queries to Angular Signals on Angular 16+.
8
8
 
@@ -70,9 +70,10 @@ 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
74
  - Forms and groups: `spForm`, `spFormGroup`
74
75
  - Async helpers: `spAsync`, `spCollection`
75
- - Reactive queries: `spQuery`, `spMutation`, `QueryClient`, `setGlobalQueryClient`
76
+ - Reactive queries: `spQuery`, `createDependentQuery`, `spInfiniteQuery`, `spMutation`, `QueryClient`, `setGlobalQueryClient`, `getGlobalQueryClient`
76
77
  - Transactions: `spTransaction`, `spBatch`
77
78
  - Schema validation: `spSchema`, `spSchemaValidator`
78
79
  - Middleware: `spUseMiddleware`, `spRemoveMiddleware`, `spLoggerMiddleware`, `spAnalyticsMiddleware`
@@ -117,3 +118,6 @@ export class CounterComponent {
117
118
  ## License
118
119
 
119
120
  MIT
121
+
122
+
123
+
@@ -263,6 +263,66 @@ function safeAddEventListener(event, handler) {
263
263
  };
264
264
  }
265
265
 
266
+ const states = new Map();
267
+ let globalEnabled$1 = true;
268
+ const ensureState = (name) => {
269
+ const existing = states.get(name);
270
+ if (existing) {
271
+ return existing;
272
+ }
273
+ const created = {
274
+ name,
275
+ updates: 0,
276
+ enabled: true,
277
+ lastValue: undefined,
278
+ lastUpdated: null,
279
+ };
280
+ states.set(name, created);
281
+ return created;
282
+ };
283
+ const spDebug = {
284
+ trackSignal(name, initialValue) {
285
+ const state = ensureState(name);
286
+ state.lastValue = initialValue;
287
+ if (state.lastUpdated === null) {
288
+ state.lastUpdated = Date.now();
289
+ }
290
+ },
291
+ recordUpdate(name, value) {
292
+ const state = ensureState(name);
293
+ state.lastValue = value;
294
+ if (!globalEnabled$1 || !state.enabled) {
295
+ return;
296
+ }
297
+ state.updates += 1;
298
+ state.lastUpdated = Date.now();
299
+ },
300
+ enable(name) {
301
+ ensureState(name).enabled = true;
302
+ },
303
+ disable(name) {
304
+ ensureState(name).enabled = false;
305
+ },
306
+ enableAll() {
307
+ globalEnabled$1 = true;
308
+ },
309
+ disableAll() {
310
+ globalEnabled$1 = false;
311
+ },
312
+ getActiveSignals() {
313
+ return Array.from(states.values())
314
+ .filter((state) => state.enabled)
315
+ .map((state) => state.name);
316
+ },
317
+ exportState() {
318
+ return Array.from(states.values()).map((state) => ({ ...state }));
319
+ },
320
+ clear() {
321
+ states.clear();
322
+ globalEnabled$1 = true;
323
+ },
324
+ };
325
+
266
326
  /**
267
327
  * @fileoverview Builder class for creating enhanced Angular signals
268
328
  * Provides a fluent API for configuring signals with features like:
@@ -379,6 +439,15 @@ class SignalBuilder {
379
439
  this.options.errorHandlers.push(handler);
380
440
  return this;
381
441
  }
442
+ /**
443
+ * Enables debug tracking for this signal instance.
444
+ * @param label Human-readable identifier used by spDebug state exports
445
+ * @returns Builder instance for chaining
446
+ */
447
+ debug(label) {
448
+ this.options.debugLabel = label;
449
+ return this;
450
+ }
382
451
  /**
383
452
  * Transforms signal value to a different type
384
453
  * @param fn Transform function from T to R
@@ -648,6 +717,9 @@ class SignalBuilder {
648
717
  const notifySubscribers = (value) => {
649
718
  if (isCleanedUp)
650
719
  return;
720
+ if (this.options.debugLabel) {
721
+ spDebug.recordUpdate(this.options.debugLabel, value);
722
+ }
651
723
  subscribers.forEach((callback) => {
652
724
  try {
653
725
  callback(value);
@@ -776,6 +848,9 @@ class SignalBuilder {
776
848
  };
777
849
  // Set initial value without applying transforms
778
850
  writable.set(initialValue);
851
+ if (this.options.debugLabel) {
852
+ spDebug.trackSignal(this.options.debugLabel, writable());
853
+ }
779
854
  const processValue = (value) => {
780
855
  // Prevent setValue after destroy
781
856
  if (isCleanedUp) {
@@ -3705,6 +3780,19 @@ function spAsync(options) {
3705
3780
  };
3706
3781
  }
3707
3782
 
3783
+ function spCombine(signals, combiner) {
3784
+ return computed(() => {
3785
+ const values = signals.map((source) => source());
3786
+ return combiner(...values);
3787
+ });
3788
+ }
3789
+ function spAll(signals) {
3790
+ return computed(() => signals.every((source) => source()));
3791
+ }
3792
+ function spAny(signals) {
3793
+ return computed(() => signals.some((source) => source()));
3794
+ }
3795
+
3708
3796
  function spCollection(options) {
3709
3797
  const idField = options.idField;
3710
3798
  const persistKey = options.persist;
@@ -4092,6 +4180,131 @@ function spComputed(fn, options = {}) {
4092
4180
  };
4093
4181
  }
4094
4182
 
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
+ function spEffect(callback, options = {}) {
4255
+ const paused = signal(false, ...(ngDevMode ? [{ debugName: "paused" }] : []));
4256
+ let timeoutId = null;
4257
+ const runCallback = () => {
4258
+ if (paused()) {
4259
+ return;
4260
+ }
4261
+ if (options.condition && !options.condition()) {
4262
+ return;
4263
+ }
4264
+ const debounce = options.debounce ?? 0;
4265
+ if (debounce > 0) {
4266
+ if (timeoutId !== null) {
4267
+ safeClearTimeout(timeoutId);
4268
+ }
4269
+ timeoutId = safeSetTimeout(() => {
4270
+ timeoutId = null;
4271
+ if (paused()) {
4272
+ return;
4273
+ }
4274
+ if (options.condition && !options.condition()) {
4275
+ return;
4276
+ }
4277
+ callback();
4278
+ }, debounce);
4279
+ return;
4280
+ }
4281
+ callback();
4282
+ };
4283
+ const effectRef = effect(() => {
4284
+ runCallback();
4285
+ }, ...(ngDevMode ? [{ debugName: "effectRef" }] : []));
4286
+ return {
4287
+ pause: () => {
4288
+ paused.set(true);
4289
+ if (timeoutId !== null) {
4290
+ safeClearTimeout(timeoutId);
4291
+ timeoutId = null;
4292
+ }
4293
+ },
4294
+ resume: () => {
4295
+ paused.set(false);
4296
+ },
4297
+ destroy: () => {
4298
+ if (timeoutId !== null) {
4299
+ safeClearTimeout(timeoutId);
4300
+ timeoutId = null;
4301
+ }
4302
+ effectRef.destroy();
4303
+ },
4304
+ isPaused: computed(() => paused()),
4305
+ };
4306
+ }
4307
+
4095
4308
  function isFormGroup(
4096
4309
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4097
4310
  control) {
@@ -5597,6 +5810,7 @@ function spMutation(options) {
5597
5810
  catch {
5598
5811
  // Not in injection context - cleanup will be manual via reset()
5599
5812
  }
5813
+ const queryClient = getGlobalQueryClient();
5600
5814
  const dataSignal = signal(undefined, ...(ngDevMode ? [{ debugName: "dataSignal" }] : []));
5601
5815
  const errorSignal = signal(null, ...(ngDevMode ? [{ debugName: "errorSignal" }] : []));
5602
5816
  const isLoadingSignal = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingSignal" }] : []));
@@ -5640,6 +5854,12 @@ function spMutation(options) {
5640
5854
  const { retry = 0, retryDelay = 10 } = options;
5641
5855
  let attempt = 0;
5642
5856
  let lastError = null;
5857
+ let optimisticPreviousData;
5858
+ const optimistic = options.optimisticUpdate;
5859
+ if (optimistic) {
5860
+ optimisticPreviousData = queryClient.getQueryData(optimistic.queryKey);
5861
+ queryClient.setQueryData(optimistic.queryKey, (oldValue) => optimistic.updater(oldValue, variables));
5862
+ }
5643
5863
  if (options.onMutate) {
5644
5864
  await options.onMutate(variables);
5645
5865
  }
@@ -5659,6 +5879,9 @@ function spMutation(options) {
5659
5879
  if (options.onSettled) {
5660
5880
  options.onSettled(result, null, variables);
5661
5881
  }
5882
+ if (optimistic?.invalidateOnSettled) {
5883
+ queryClient.invalidateQueries(optimistic.queryKey);
5884
+ }
5662
5885
  return result;
5663
5886
  }
5664
5887
  catch (error) {
@@ -5668,6 +5891,9 @@ function spMutation(options) {
5668
5891
  ? retry(attempt, lastError)
5669
5892
  : attempt <= retry;
5670
5893
  if (!shouldRetry) {
5894
+ if (optimistic && optimistic.rollbackOnError !== false) {
5895
+ queryClient.setQueryData(optimistic.queryKey, optimisticPreviousData);
5896
+ }
5671
5897
  updateState({
5672
5898
  error: lastError,
5673
5899
  isLoading: false,
@@ -5681,6 +5907,9 @@ function spMutation(options) {
5681
5907
  if (options.onSettled) {
5682
5908
  options.onSettled(undefined, lastError, variables);
5683
5909
  }
5910
+ if (optimistic?.invalidateOnSettled) {
5911
+ queryClient.invalidateQueries(optimistic.queryKey);
5912
+ }
5684
5913
  throw lastError;
5685
5914
  }
5686
5915
  const delay = typeof retryDelay === 'function'
@@ -5924,6 +6153,18 @@ function createQuery(queryKey, queryFn, options) {
5924
6153
  ...options,
5925
6154
  });
5926
6155
  }
6156
+ function createDependentQuery(queryKey, queryFn, dependencies, options) {
6157
+ const enabled = computed(() => dependencies.every((dependency) => {
6158
+ const value = dependency();
6159
+ return value !== undefined && value !== null && value !== false;
6160
+ }), ...(ngDevMode ? [{ debugName: "enabled" }] : []));
6161
+ return spQuery({
6162
+ queryKey,
6163
+ queryFn,
6164
+ ...options,
6165
+ enabled,
6166
+ });
6167
+ }
5927
6168
 
5928
6169
  /**
5929
6170
  * Reactive Queries Interfaces
@@ -5931,6 +6172,138 @@ function createQuery(queryKey, queryFn, options) {
5931
6172
  * This module exports all interface definitions for reactive queries
5932
6173
  */
5933
6174
 
6175
+ function spInfiniteQuery(options) {
6176
+ const queryClient = getGlobalQueryClient();
6177
+ let destroyRef = null;
6178
+ try {
6179
+ destroyRef = inject(DestroyRef, { optional: true });
6180
+ }
6181
+ catch {
6182
+ // Not in injection context
6183
+ }
6184
+ const queryKey = Array.isArray(options.queryKey)
6185
+ ? options.queryKey
6186
+ : options.queryKey.key;
6187
+ const pagesSignal = signal([], ...(ngDevMode ? [{ debugName: "pagesSignal" }] : []));
6188
+ const errorSignal = signal(null, ...(ngDevMode ? [{ debugName: "errorSignal" }] : []));
6189
+ const isLoadingSignal = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingSignal" }] : []));
6190
+ const isFetchingSignal = signal(false, ...(ngDevMode ? [{ debugName: "isFetchingSignal" }] : []));
6191
+ const isFetchingNextPageSignal = signal(false, ...(ngDevMode ? [{ debugName: "isFetchingNextPageSignal" }] : []));
6192
+ const hasNextPageSignal = signal(true, ...(ngDevMode ? [{ debugName: "hasNextPageSignal" }] : []));
6193
+ const setPages = (pages) => {
6194
+ untracked(() => {
6195
+ pagesSignal.set(pages);
6196
+ const lastPage = pages.length > 0 ? pages[pages.length - 1] : undefined;
6197
+ hasNextPageSignal.set(lastPage !== undefined
6198
+ ? options.getNextPageParam(lastPage, pages) !== undefined
6199
+ : true);
6200
+ });
6201
+ };
6202
+ const runPageFetch = async (pageParam) => {
6203
+ isFetchingSignal.set(true);
6204
+ errorSignal.set(null);
6205
+ try {
6206
+ return await options.queryFn(pageParam);
6207
+ }
6208
+ catch (error) {
6209
+ errorSignal.set(error);
6210
+ throw error;
6211
+ }
6212
+ finally {
6213
+ isFetchingSignal.set(false);
6214
+ }
6215
+ };
6216
+ const refetch = async () => {
6217
+ isLoadingSignal.set(true);
6218
+ try {
6219
+ const firstPage = await runPageFetch(options.initialPageParam);
6220
+ setPages([firstPage]);
6221
+ queryClient.setQueryData(queryKey, [firstPage]);
6222
+ }
6223
+ finally {
6224
+ isLoadingSignal.set(false);
6225
+ }
6226
+ };
6227
+ const fetchNextPage = async () => {
6228
+ const pages = pagesSignal();
6229
+ const lastPage = pages[pages.length - 1];
6230
+ const nextPageParam = pages.length === 0
6231
+ ? options.initialPageParam
6232
+ : options.getNextPageParam(lastPage, pages);
6233
+ if (nextPageParam === undefined) {
6234
+ hasNextPageSignal.set(false);
6235
+ return;
6236
+ }
6237
+ isFetchingNextPageSignal.set(true);
6238
+ try {
6239
+ const page = await runPageFetch(nextPageParam);
6240
+ const nextPages = [...pagesSignal(), page];
6241
+ setPages(nextPages);
6242
+ queryClient.setQueryData(queryKey, nextPages);
6243
+ }
6244
+ finally {
6245
+ isFetchingNextPageSignal.set(false);
6246
+ }
6247
+ };
6248
+ let enabledWatcher = null;
6249
+ let previousEnabledState = false;
6250
+ const getEnabled = () => {
6251
+ if (typeof options.enabled === 'boolean') {
6252
+ return options.enabled;
6253
+ }
6254
+ if (options.enabled) {
6255
+ return options.enabled();
6256
+ }
6257
+ return true;
6258
+ };
6259
+ const runIfEnabled = () => {
6260
+ const enabled = getEnabled();
6261
+ if (!enabled) {
6262
+ previousEnabledState = false;
6263
+ return;
6264
+ }
6265
+ if (previousEnabledState) {
6266
+ return;
6267
+ }
6268
+ previousEnabledState = true;
6269
+ const cached = queryClient.getQueryData(queryKey);
6270
+ if (cached && cached.length > 0) {
6271
+ setPages(cached);
6272
+ return;
6273
+ }
6274
+ refetch().catch(() => undefined);
6275
+ };
6276
+ runIfEnabled();
6277
+ if (typeof options.enabled !== 'boolean' && options.enabled) {
6278
+ enabledWatcher = setInterval(runIfEnabled, 100);
6279
+ }
6280
+ if (destroyRef) {
6281
+ destroyRef.onDestroy(() => {
6282
+ if (enabledWatcher) {
6283
+ clearInterval(enabledWatcher);
6284
+ enabledWatcher = null;
6285
+ }
6286
+ });
6287
+ }
6288
+ return {
6289
+ pages: computed(() => pagesSignal()),
6290
+ error: computed(() => errorSignal()),
6291
+ isLoading: computed(() => isLoadingSignal()),
6292
+ isFetching: computed(() => isFetchingSignal()),
6293
+ isFetchingNextPage: computed(() => isFetchingNextPageSignal()),
6294
+ hasNextPage: computed(() => hasNextPageSignal()),
6295
+ refetch,
6296
+ fetchNextPage,
6297
+ };
6298
+ }
6299
+ function createInfiniteQuery(queryKey, queryFn, options) {
6300
+ return spInfiniteQuery({
6301
+ ...options,
6302
+ queryKey,
6303
+ queryFn,
6304
+ });
6305
+ }
6306
+
5934
6307
  /**
5935
6308
  * Public API Surface of ngx-signal-plus
5936
6309
  *
@@ -5943,5 +6316,5 @@ function createQuery(queryKey, queryFn, options) {
5943
6316
  * Generated bundle index. Do not edit.
5944
6317
  */
5945
6318
 
5946
- export { QueryClient, SP_ERRORS, SpError, createMutation, createQuery, enhance, formatSpError, getGlobalQueryClient, setGlobalQueryClient, sp, spAnalyticsMiddleware, spAsync, spBatch, spClearMiddleware, spCollection, combineLatest as spCombineLatest, spComputed, spCounter, spCreateError, debounceTime as spDebounceTime, delay$1 as spDelay, distinctUntilChanged as spDistinctUntilChanged, filter as spFilter, spForm, spFormGroup, spGetMiddlewareCount, spGetModifiedSignals, HistoryManager as spHistoryManager, spIsInBatch, spIsInTransaction, spIsTransactionActive, spLoggerMiddleware, map as spMap, merge as spMerge, spMutation, presets as spPresets, spQuery, spRemoveMiddleware, spSchema, spSchemaValidator, spSchemaWithErrors, SignalBuilder as spSignalBuilder, SignalPlusComponent as spSignalPlusComponent, SignalPlusService as spSignalPlusService, skip as spSkip, StorageManager as spStorageManager, take as spTake, throttleTime as spThrottleTime, spToggle, spTransaction, spUseMiddleware, validators as spValidators };
6319
+ export { QueryClient, SP_ERRORS, SpError, createDependentQuery, createInfiniteQuery, createMutation, createQuery, enhance, formatSpError, getGlobalQueryClient, setGlobalQueryClient, sp, spAll, spAnalyticsMiddleware, spAny, spAsync, spBatch, spClearMiddleware, spCollection, spCombine, combineLatest as spCombineLatest, spComputed, spCounter, spCreateError, debounceTime as spDebounceTime, spDebug, delay$1 as spDelay, distinctUntilChanged as spDistinctUntilChanged, spEffect, filter as spFilter, spForm, spFormGroup, spGetMiddlewareCount, spGetModifiedSignals, HistoryManager as spHistoryManager, spInfiniteQuery, spIsInBatch, spIsInTransaction, spIsTransactionActive, spLoggerMiddleware, map as spMap, merge as spMerge, spMonitor, spMutation, presets as spPresets, spQuery, spRemoveMiddleware, spSchema, spSchemaValidator, spSchemaWithErrors, SignalBuilder as spSignalBuilder, SignalPlusComponent as spSignalPlusComponent, SignalPlusService as spSignalPlusService, skip as spSkip, StorageManager as spStorageManager, take as spTake, throttleTime as spThrottleTime, spToggle, spTransaction, spUseMiddleware, validators as spValidators };
5947
6320
  //# sourceMappingURL=ngx-signal-plus.mjs.map