ngx-signal-plus 2.6.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,10 +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`, `sp().debug(label)`
73
+ - Developer experience: `spCombine`, `spAll`, `spAny`, `spEffect`, `spDebug`, `spMonitor`, `sp().debug(label)`
74
74
  - Forms and groups: `spForm`, `spFormGroup`
75
75
  - Async helpers: `spAsync`, `spCollection`
76
- - Reactive queries: `spQuery`, `spMutation`, `QueryClient`, `setGlobalQueryClient`
76
+ - Reactive queries: `spQuery`, `createDependentQuery`, `spInfiniteQuery`, `spMutation`, `QueryClient`, `setGlobalQueryClient`, `getGlobalQueryClient`
77
77
  - Transactions: `spTransaction`, `spBatch`
78
78
  - Schema validation: `spSchema`, `spSchemaValidator`
79
79
  - Middleware: `spUseMiddleware`, `spRemoveMiddleware`, `spLoggerMiddleware`, `spAnalyticsMiddleware`
@@ -118,3 +118,6 @@ export class CounterComponent {
118
118
  ## License
119
119
 
120
120
  MIT
121
+
122
+
123
+
@@ -264,7 +264,7 @@ function safeAddEventListener(event, handler) {
264
264
  }
265
265
 
266
266
  const states = new Map();
267
- let globalEnabled = true;
267
+ let globalEnabled$1 = true;
268
268
  const ensureState = (name) => {
269
269
  const existing = states.get(name);
270
270
  if (existing) {
@@ -291,7 +291,7 @@ const spDebug = {
291
291
  recordUpdate(name, value) {
292
292
  const state = ensureState(name);
293
293
  state.lastValue = value;
294
- if (!globalEnabled || !state.enabled) {
294
+ if (!globalEnabled$1 || !state.enabled) {
295
295
  return;
296
296
  }
297
297
  state.updates += 1;
@@ -304,10 +304,10 @@ const spDebug = {
304
304
  ensureState(name).enabled = false;
305
305
  },
306
306
  enableAll() {
307
- globalEnabled = true;
307
+ globalEnabled$1 = true;
308
308
  },
309
309
  disableAll() {
310
- globalEnabled = false;
310
+ globalEnabled$1 = false;
311
311
  },
312
312
  getActiveSignals() {
313
313
  return Array.from(states.values())
@@ -319,7 +319,7 @@ const spDebug = {
319
319
  },
320
320
  clear() {
321
321
  states.clear();
322
- globalEnabled = true;
322
+ globalEnabled$1 = true;
323
323
  },
324
324
  };
325
325
 
@@ -4180,6 +4180,77 @@ function spComputed(fn, options = {}) {
4180
4180
  };
4181
4181
  }
4182
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
+
4183
4254
  function spEffect(callback, options = {}) {
4184
4255
  const paused = signal(false, ...(ngDevMode ? [{ debugName: "paused" }] : []));
4185
4256
  let timeoutId = null;
@@ -5739,6 +5810,7 @@ function spMutation(options) {
5739
5810
  catch {
5740
5811
  // Not in injection context - cleanup will be manual via reset()
5741
5812
  }
5813
+ const queryClient = getGlobalQueryClient();
5742
5814
  const dataSignal = signal(undefined, ...(ngDevMode ? [{ debugName: "dataSignal" }] : []));
5743
5815
  const errorSignal = signal(null, ...(ngDevMode ? [{ debugName: "errorSignal" }] : []));
5744
5816
  const isLoadingSignal = signal(false, ...(ngDevMode ? [{ debugName: "isLoadingSignal" }] : []));
@@ -5782,6 +5854,12 @@ function spMutation(options) {
5782
5854
  const { retry = 0, retryDelay = 10 } = options;
5783
5855
  let attempt = 0;
5784
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
+ }
5785
5863
  if (options.onMutate) {
5786
5864
  await options.onMutate(variables);
5787
5865
  }
@@ -5801,6 +5879,9 @@ function spMutation(options) {
5801
5879
  if (options.onSettled) {
5802
5880
  options.onSettled(result, null, variables);
5803
5881
  }
5882
+ if (optimistic?.invalidateOnSettled) {
5883
+ queryClient.invalidateQueries(optimistic.queryKey);
5884
+ }
5804
5885
  return result;
5805
5886
  }
5806
5887
  catch (error) {
@@ -5810,6 +5891,9 @@ function spMutation(options) {
5810
5891
  ? retry(attempt, lastError)
5811
5892
  : attempt <= retry;
5812
5893
  if (!shouldRetry) {
5894
+ if (optimistic && optimistic.rollbackOnError !== false) {
5895
+ queryClient.setQueryData(optimistic.queryKey, optimisticPreviousData);
5896
+ }
5813
5897
  updateState({
5814
5898
  error: lastError,
5815
5899
  isLoading: false,
@@ -5823,6 +5907,9 @@ function spMutation(options) {
5823
5907
  if (options.onSettled) {
5824
5908
  options.onSettled(undefined, lastError, variables);
5825
5909
  }
5910
+ if (optimistic?.invalidateOnSettled) {
5911
+ queryClient.invalidateQueries(optimistic.queryKey);
5912
+ }
5826
5913
  throw lastError;
5827
5914
  }
5828
5915
  const delay = typeof retryDelay === 'function'
@@ -6066,6 +6153,18 @@ function createQuery(queryKey, queryFn, options) {
6066
6153
  ...options,
6067
6154
  });
6068
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
+ }
6069
6168
 
6070
6169
  /**
6071
6170
  * Reactive Queries Interfaces
@@ -6073,6 +6172,138 @@ function createQuery(queryKey, queryFn, options) {
6073
6172
  * This module exports all interface definitions for reactive queries
6074
6173
  */
6075
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
+
6076
6307
  /**
6077
6308
  * Public API Surface of ngx-signal-plus
6078
6309
  *
@@ -6085,5 +6316,5 @@ function createQuery(queryKey, queryFn, options) {
6085
6316
  * Generated bundle index. Do not edit.
6086
6317
  */
6087
6318
 
6088
- export { QueryClient, SP_ERRORS, SpError, 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, 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 };
6089
6320
  //# sourceMappingURL=ngx-signal-plus.mjs.map