ngx-signal-query 1.5.0-dev.1 → 1.5.0-dev.3

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.
@@ -232,6 +232,39 @@ function isValidTimeout(value) {
232
232
  function isPromiseLike(value) {
233
233
  return typeof value?.then === 'function';
234
234
  }
235
+ // Resolves once every promise has settled, and never rejects. It resolves
236
+ // with nothing: what the promises brought isn't the caller's to see.
237
+ /** @internal */
238
+ function whenAllSettled(promises) {
239
+ return Promise.allSettled(promises).then(() => undefined);
240
+ }
241
+ /** @internal */
242
+ function deferred() {
243
+ let resolve;
244
+ let reject;
245
+ const promise = new Promise((res, rej) => {
246
+ resolve = res;
247
+ reject = rej;
248
+ });
249
+ return { promise, resolve, reject };
250
+ }
251
+
252
+ /**
253
+ * The error a promise of a query's data rejects with when the fetch stops
254
+ * with no data to give: it was cancelled before the query had any
255
+ * (`cancelQueries()`, or the last component showing the query went away), or
256
+ * the query was removed (`removeQueries()`, e.g. on sign-out). The request
257
+ * didn't fail, so tell the two apart with `instanceof CancelledError`.
258
+ *
259
+ * A mutation's `mutateAsync()` rejects with one too, when the mutation cache
260
+ * is cleared while its run is in flight.
261
+ */
262
+ class CancelledError extends Error {
263
+ constructor(message = 'The query fetch was cancelled') {
264
+ super(message);
265
+ this.name = 'CancelledError';
266
+ }
267
+ }
235
268
 
236
269
  // Exponential backoff capped at 30s, matching TanStack's default.
237
270
  /** @internal */
@@ -273,6 +306,9 @@ class Query {
273
306
  // eslint-disable-next-line @typescript-eslint/member-ordering
274
307
  state = this.#state.asReadonly();
275
308
  #subscription = null;
309
+ // The promise of the fetch in flight, which every caller that joins the
310
+ // fetch shares, and how to settle it; null while nothing is in flight.
311
+ #outcome = null;
276
312
  // The structuralSharing of the fetch whose response the query holds.
277
313
  #fetchedStructuralSharing;
278
314
  #observers = new Set();
@@ -306,15 +342,17 @@ class Query {
306
342
  this.#scheduleGc();
307
343
  }
308
344
  }
309
- // How to refetch this query on its observers' behalf: the first observer
310
- // that wants it fetched decides, as in TanStack; null if none does.
311
- activeFetchOptions() {
345
+ // Refetches the query if a component shows it: the first observer that
346
+ // wants it fetched decides how, as in TanStack. A fetch in flight is
347
+ // replaced, as it may predate the change the caller is after. Returns the
348
+ // fetch's promise, or undefined if no observer wants the query fetched.
349
+ refetchIfActive() {
312
350
  for (const observer of this.#observers) {
313
351
  const options = observer.fetchOptions();
314
352
  if (options)
315
- return options;
353
+ return this.fetch(options, { cancelRefetch: true });
316
354
  }
317
- return null;
355
+ return;
318
356
  }
319
357
  // Refetches the query now that the user is back on the page, if one of its
320
358
  // observers wants that: its refetchOnWindowFocus is 'always', or true while
@@ -338,14 +376,28 @@ class Query {
338
376
  }
339
377
  // Without a structuralSharing setting the response is stored as is: the
340
378
  // default is the client's to resolve.
379
+ //
380
+ // Returns a promise of the data as the query stores it, which every caller
381
+ // that joins the fetch shares. A fetch that replaces this one settles it
382
+ // with its own outcome; one stopped with nothing in its place settles it as
383
+ // cancel() or destroy() says. Nobody has to wait for it.
341
384
  fetch({ queryFn, retry: retry$1 = 0, retryDelay = defaultRetryDelay, structuralSharing = false, }, { cancelRefetch = false } = {}) {
342
- if (this.#inFlight()) {
385
+ const running = this.#outcome;
386
+ if (running) {
343
387
  // Already in-flight: dedupe unless the caller explicitly wants a fresh
344
- // fetch (e.g. invalidate/refetch) — then cancel the old one and restart.
388
+ // fetch (e.g. invalidate/refetch) — then replace the old one.
345
389
  if (!cancelRefetch)
346
- return;
347
- this.cancel();
390
+ return running.promise;
391
+ this.#subscription?.unsubscribe();
348
392
  }
393
+ const outcome = deferred();
394
+ // Its result or error lands in the state either way, so a caller may
395
+ // ignore the promise: handled here, a failure doesn't surface as an
396
+ // unhandled rejection, and whoever awaits the promise still gets it.
397
+ outcome.promise.catch(() => undefined);
398
+ // Whoever waits for the fetch this one replaces gets this one's outcome.
399
+ running?.resolve(outcome.promise);
400
+ this.#outcome = outcome;
349
401
  this.#state.update((state) => ({
350
402
  ...state,
351
403
  isFetching: true,
@@ -377,6 +429,7 @@ class Query {
377
429
  map((data) => replaceData(this.state().data, data, structuralSharing)))
378
430
  .subscribe({
379
431
  next: (data) => {
432
+ this.#outcome = null;
380
433
  this.#fetchedStructuralSharing = structuralSharing;
381
434
  this.#state.set({
382
435
  data,
@@ -388,14 +441,20 @@ class Query {
388
441
  failureReason: null,
389
442
  updatedAt: Date.now(),
390
443
  });
444
+ outcome.resolve(data);
445
+ },
446
+ error: (err) => {
447
+ this.#outcome = null;
448
+ this.#state.update((state) => ({
449
+ ...state,
450
+ status: 'error',
451
+ error: err,
452
+ isFetching: false,
453
+ }));
454
+ outcome.reject(err);
391
455
  },
392
- error: (err) => this.#state.update((state) => ({
393
- ...state,
394
- status: 'error',
395
- error: err,
396
- isFetching: false,
397
- })),
398
456
  });
457
+ return outcome.promise;
399
458
  }
400
459
  // Without a structuralSharing setting the data is stored as is: which one
401
460
  // a write uses is the client's to resolve.
@@ -424,17 +483,18 @@ class Query {
424
483
  state.isInvalidated ||
425
484
  this.#isStale(staleTime));
426
485
  }
486
+ // Stops the fetch in flight. Whoever waits for it gets the data the query
487
+ // holds, as in TanStack, or a CancelledError if it holds none.
427
488
  cancel() {
428
- this.#subscription?.unsubscribe();
429
- this.#subscription = null;
430
- // The fetch is no longer running; clear the in-flight flag so isFetching
431
- // doesn't stay stuck true after a cancellation.
432
- if (this.state().isFetching) {
433
- this.#state.update((state) => ({ ...state, isFetching: false }));
434
- }
489
+ const { data } = this.state();
490
+ this.#stop((outcome) => data === undefined
491
+ ? outcome.reject(new CancelledError())
492
+ : outcome.resolve(data));
435
493
  }
494
+ // The query is thrown away with its data, so whoever waits for its fetch
495
+ // gets a CancelledError, as in TanStack.
436
496
  destroy() {
437
- this.cancel();
497
+ this.#stop((outcome) => outcome.reject(new CancelledError()));
438
498
  this.#clearGcTimer();
439
499
  }
440
500
  // Inclusive: data fetched exactly `staleTime` ago is already stale. This
@@ -445,7 +505,21 @@ class Query {
445
505
  return Date.now() - this.state().updatedAt >= staleTime;
446
506
  }
447
507
  #inFlight() {
448
- return this.#subscription !== null && !this.#subscription.closed;
508
+ return this.#outcome !== null;
509
+ }
510
+ // Stops the fetch in flight, if any, and settles its promise with settle.
511
+ #stop(settle) {
512
+ const outcome = this.#outcome;
513
+ this.#subscription?.unsubscribe();
514
+ this.#subscription = null;
515
+ this.#outcome = null;
516
+ if (outcome)
517
+ settle(outcome);
518
+ // The fetch is no longer running; clear the in-flight flag so isFetching
519
+ // doesn't stay stuck true after a cancellation.
520
+ if (this.state().isFetching) {
521
+ this.#state.update((state) => ({ ...state, isFetching: false }));
522
+ }
449
523
  }
450
524
  // A fetch in flight is kept, as in TanStack, and the observers aren't even
451
525
  // asked: fetch() would drop the refetch anyway.
@@ -551,6 +625,9 @@ class Mutation {
551
625
  // eslint-disable-next-line @typescript-eslint/member-ordering
552
626
  state = this.#state.asReadonly();
553
627
  #subscription = null;
628
+ // The promise of the run in flight and how to settle it; null once it has
629
+ // settled, or before the run.
630
+ #run = null;
554
631
  #observers = 0;
555
632
  #options;
556
633
  #cache;
@@ -577,8 +654,14 @@ class Mutation {
577
654
  this.#observers--;
578
655
  this.#disposeIfDone();
579
656
  }
580
- /** Runs the mutation once. `variables` are passed straight to `mutationFn`. */
657
+ /**
658
+ * Runs the mutation once. `variables` are passed straight to `mutationFn`.
659
+ * Returns a promise of the run, settled as the state is, once its last hook
660
+ * is done: with the data, or with the error.
661
+ */
581
662
  execute(variables) {
663
+ const run = deferred();
664
+ this.#run = run;
582
665
  this.#state.set({
583
666
  status: 'pending',
584
667
  data: undefined,
@@ -607,12 +690,24 @@ class Mutation {
607
690
  })), concatMap((outcome) => this.#runHooks(outcome, variables).pipe(endWith(outcome))))
608
691
  .subscribe((outcome) => {
609
692
  this.#state.update((state) => ({ ...state, ...outcome }));
693
+ this.#run = null;
610
694
  this.#disposeIfDone();
695
+ if (outcome.status === 'success') {
696
+ run.resolve(outcome.data);
697
+ }
698
+ else {
699
+ run.reject(outcome.error);
700
+ }
611
701
  });
702
+ return run.promise;
612
703
  }
704
+ // Whoever waits for the run hears that it was cancelled, so its promise
705
+ // doesn't hang.
613
706
  cancel() {
614
707
  this.#subscription?.unsubscribe();
615
708
  this.#subscription = null;
709
+ this.#run?.reject(new CancelledError('The mutation was cancelled'));
710
+ this.#run = null;
616
711
  }
617
712
  /** Sends the write, retrying as `retry` allows, and emits its data once. */
618
713
  #request(variables) {
@@ -804,24 +899,58 @@ class QueryClient {
804
899
  /**
805
900
  * Imperatively fetches and caches a query, unless fresh data already exists
806
901
  * (governed by `staleTime`). Prefer {@link injectQuery} in components; use
807
- * this to prefetch, from an event handler or an effect.
902
+ * this to prefetch, from an event handler or an effect, or to wait for the
903
+ * data, e.g. in a route resolver.
904
+ *
905
+ * Resolves with the data as the cache holds it: at once if it's fresh,
906
+ * otherwise once the fetch lands. A key already fetching is waited for,
907
+ * not fetched again, unless `cancelRefetch` is set. Rejects with the error
908
+ * if the fetch fails. A cancelled fetch resolves with the data the cache
909
+ * holds, or rejects with a {@link CancelledError} if it holds none; a
910
+ * removed query always rejects with one. Nothing has to wait for the
911
+ * promise: a prefetch that fails only leaves the query in `'error'`.
912
+ *
913
+ * Unlike a component's query, it doesn't retry a failure unless `retry` is
914
+ * set here or in {@link withDefaultOptions}, so whoever waits for the data
915
+ * hears of the failure at once.
808
916
  *
809
917
  * @param key - The query key to fetch and cache under.
810
918
  * @param queryFn - Function returning the data as an `Observable` or `Promise`.
811
919
  * @param options - Fetch tuning (`staleTime`, `retry`, `retryDelay`,
812
920
  * `structuralSharing`, `cancelRefetch`).
921
+ * @returns A promise of the data.
922
+ *
923
+ * @example
924
+ * ```ts
925
+ * export const todoResolver: ResolveFn<Todo> = (route) => {
926
+ * const http = inject(HttpClient)
927
+ * const id = Number(route.paramMap.get('id'))
928
+ *
929
+ * return injectQueryClient().fetchQuery(['todos', id], () =>
930
+ * http.get<Todo>(`/api/todos/${id}`),
931
+ * )
932
+ * }
933
+ * ```
813
934
  */
814
935
  fetchQuery(key, queryFn, options = {}) {
815
- untracked(() => {
816
- const defaults = this.#config.defaultOptions?.queries;
817
- const staleTime = options.staleTime ?? defaults?.staleTime ?? 0;
818
- const retry = options.retry ?? defaults?.retry ?? 3;
819
- const retryDelay = options.retryDelay ?? defaults?.retryDelay ?? defaultRetryDelay;
820
- const structuralSharing = options.structuralSharing ?? this.#defaultStructuralSharing();
936
+ return untracked(() => {
937
+ // Resolved as a query's options are, so one set of defaults serves
938
+ // both, except that a failure isn't retried unless asked: whoever waits
939
+ // for the data hears of it at once. cancelRefetch is about this call,
940
+ // not an option of the query.
941
+ const { cancelRefetch, ...queryOptions } = options;
942
+ const defaulted = this.defaultQueryOptions({
943
+ ...queryOptions,
944
+ queryKey: key,
945
+ queryFn,
946
+ retry: queryOptions.retry ??
947
+ this.#config.defaultOptions?.queries?.retry ??
948
+ false,
949
+ });
821
950
  const query = this.#cache.getOrCreate(key);
822
- if (query.shouldFetch(staleTime)) {
823
- query.fetch({ queryFn, retry, retryDelay, structuralSharing }, { cancelRefetch: options.cancelRefetch });
824
- }
951
+ return query.shouldFetch(defaulted.staleTime)
952
+ ? query.fetch(defaulted, { cancelRefetch })
953
+ : Promise.resolve(query.state().data);
825
954
  });
826
955
  }
827
956
  /**
@@ -872,11 +1001,16 @@ class QueryClient {
872
1001
  * Marks matching queries as stale and refetches the actively observed ones,
873
1002
  * once per query however many components observe it. The refetch starts
874
1003
  * right away and replaces a fetch already in flight, which may predate the
875
- * change; this method doesn't wait for it. A query with no enabled observer
876
- * keeps the stale mark and refetches as soon as one appears. Commonly called
877
- * after a mutation succeeds.
1004
+ * change. A query with no enabled observer keeps the stale mark and
1005
+ * refetches as soon as one appears. Commonly called after a mutation
1006
+ * succeeds.
1007
+ *
1008
+ * Resolves once those refetches have settled, so a mutation hook that
1009
+ * returns it keeps the mutation pending until the data is fresh. It never
1010
+ * rejects: a refetch that fails shows in its query's `error`.
878
1011
  *
879
1012
  * @param filters - Which queries to invalidate; omit to invalidate all.
1013
+ * `refetchType: 'none'` only marks them, and resolves at once.
880
1014
  *
881
1015
  * @example
882
1016
  * ```ts
@@ -884,21 +1018,36 @@ class QueryClient {
884
1018
  * ```
885
1019
  */
886
1020
  invalidateQueries(filters) {
887
- untracked(() => {
1021
+ return untracked(() => {
888
1022
  const queries = this.#cache.findAll(filters);
889
1023
  queries.forEach((query) => query.invalidate());
890
- // Refetch each active query once, however many observers watch it; an
891
- // inactive one refetches when next observed, as shouldFetch sees the
892
- // flag. cancelRefetch: a fetch already in flight started before the
893
- // invalidation, so it may have missed the change.
894
- queries.forEach((query) => {
895
- const options = query.activeFetchOptions();
896
- if (options) {
897
- query.fetch(options, { cancelRefetch: true });
898
- }
899
- });
1024
+ // An inactive query refetches when next observed, as shouldFetch sees
1025
+ // the flag.
1026
+ return filters?.refetchType === 'none'
1027
+ ? Promise.resolve()
1028
+ : whenAllSettled(queries.map((query) => query.refetchIfActive()));
900
1029
  });
901
1030
  }
1031
+ /**
1032
+ * Refetches the matching queries that a component shows, whether their
1033
+ * data is stale or not, once per query however many components show it. A
1034
+ * fetch already in flight is replaced. Queries nobody shows are left as
1035
+ * they are, not even marked stale: to have them refetch once they're shown,
1036
+ * invalidate them with `invalidateQueries()`.
1037
+ *
1038
+ * Resolves once the refetches have settled. It never rejects: a refetch
1039
+ * that fails shows in its query's `error`.
1040
+ *
1041
+ * @param filters - Which queries to refetch; omit to refetch all.
1042
+ *
1043
+ * @example
1044
+ * ```ts
1045
+ * await client.refetchQueries({ queryKey: ['todos'] })
1046
+ * ```
1047
+ */
1048
+ refetchQueries(filters) {
1049
+ return untracked(() => whenAllSettled(this.#cache.findAll(filters).map((query) => query.refetchIfActive())));
1050
+ }
902
1051
  /**
903
1052
  * Cancels any in-flight fetches for matching queries.
904
1053
  *
@@ -1235,18 +1384,19 @@ function createQueryObserver(optionsFn, injector) {
1235
1384
  // Invalidation isn't a trigger: invalidateQueries() refetches each active
1236
1385
  // query once itself. A query it skipped (unobserved, or its observers
1237
1386
  // disabled) stays invalidated, so it fetches here once observed or enabled:
1238
- // shouldFetch sees the flag.
1387
+ // shouldFetch sees the flag. The key and queryFn come from one read of the
1388
+ // options, so the fetch can't go to another key's query, whatever order the
1389
+ // effects run in.
1239
1390
  track(() => {
1240
1391
  query();
1241
1392
  if (!enabled())
1242
1393
  return;
1243
- const { queryKey, queryFn, staleTime, retry, retryDelay, structuralSharing, } = untracked(defaultedOptions);
1244
- untracked(() => client.fetchQuery(queryKey, queryFn, {
1245
- staleTime,
1246
- retry,
1247
- retryDelay,
1248
- structuralSharing,
1249
- }));
1394
+ untracked(() => {
1395
+ const options = defaultedOptions();
1396
+ const q = resolve(options.queryKey);
1397
+ if (q.shouldFetch(options.staleTime))
1398
+ q.fetch(options);
1399
+ });
1250
1400
  });
1251
1401
  // The polling interval this observer asks for, or false for none. Memoized,
1252
1402
  // so the polling effect wakes only when the value changes. The function form
@@ -1314,34 +1464,37 @@ function createQueryObserver(optionsFn, injector) {
1314
1464
  }
1315
1465
  return { data: state.data, status: state.status, isPlaceholderData: false };
1316
1466
  }, ...(ngDevMode ? [{ debugName: "resolved" }] : []));
1467
+ const result = {
1468
+ data: computed(() => resolved().data),
1469
+ status: computed(() => resolved().status),
1470
+ error: computed(() => query().state().error),
1471
+ isFetching: computed(() => query().state().isFetching),
1472
+ // First fetch in-flight: fetching with no resolved data yet. An active
1473
+ // placeholder counts as data — the UI shows it, not a loading state.
1474
+ isLoading: computed(() => query().state().isFetching && resolved().status === 'pending'),
1475
+ isPending: computed(() => resolved().status === 'pending'),
1476
+ isSuccess: computed(() => resolved().status === 'success'),
1477
+ isError: computed(() => resolved().status === 'error'),
1478
+ isPlaceholderData: computed(() => resolved().isPlaceholderData),
1479
+ failureCount: computed(() => query().state().failureCount),
1480
+ failureReason: computed(() => query().state().failureReason),
1481
+ // Force a fresh fetch regardless of staleTime and enabled, cancelling
1482
+ // any in-flight request (explicit user intent — get current data).
1483
+ // Untracked, as the client's methods are: called from an effect, it
1484
+ // mustn't make the effect depend on the options or on the query. However
1485
+ // the fetch ends, the signals show it, so the promise resolves with their
1486
+ // data and error and never rejects.
1487
+ refetch: () => untracked(() => {
1488
+ const options = defaultedOptions();
1489
+ return resolve(options.queryKey).fetch(options, {
1490
+ cancelRefetch: true,
1491
+ });
1492
+ })
1493
+ .catch(() => undefined)
1494
+ .then(() => ({ data: result.data(), error: result.error() })),
1495
+ };
1317
1496
  return {
1318
- result: {
1319
- data: computed(() => resolved().data),
1320
- status: computed(() => resolved().status),
1321
- error: computed(() => query().state().error),
1322
- isFetching: computed(() => query().state().isFetching),
1323
- // First fetch in-flight: fetching with no resolved data yet. An active
1324
- // placeholder counts as data — the UI shows it, not a loading state.
1325
- isLoading: computed(() => query().state().isFetching && resolved().status === 'pending'),
1326
- isPending: computed(() => resolved().status === 'pending'),
1327
- isSuccess: computed(() => resolved().status === 'success'),
1328
- isError: computed(() => resolved().status === 'error'),
1329
- isPlaceholderData: computed(() => resolved().isPlaceholderData),
1330
- failureCount: computed(() => query().state().failureCount),
1331
- failureReason: computed(() => query().state().failureReason),
1332
- // Force a fresh fetch regardless of staleTime, cancelling any in-flight
1333
- // request (explicit user intent — get current data).
1334
- refetch: () => {
1335
- const { queryKey, queryFn, retry, retryDelay, structuralSharing } = untracked(defaultedOptions);
1336
- client.fetchQuery(queryKey, queryFn, {
1337
- staleTime: 0,
1338
- retry,
1339
- retryDelay,
1340
- structuralSharing,
1341
- cancelRefetch: true,
1342
- });
1343
- },
1344
- },
1497
+ result,
1345
1498
  destroy: () => refs.forEach((ref) => ref.destroy()),
1346
1499
  };
1347
1500
  }
@@ -1517,9 +1670,11 @@ function injectQueries(optionsFn, options) {
1517
1670
  * until it settles. Mutations do not retry by default (a retried write is not
1518
1671
  * idempotent); opt in via `options.retry`. Whatever of `retry`, `retryDelay`
1519
1672
  * and the `onSuccess` / `onError` / `onSettled` hooks a mutation leaves unset
1520
- * comes from {@link withDefaultOptions}. `mutate()` and `reset()` are safe
1521
- * to call from an effect: neither makes the effect depend on what it reads,
1522
- * such as the options or the previous run. Bound to the current injection
1673
+ * comes from {@link withDefaultOptions}. `mutateAsync(variables)` runs it
1674
+ * the same way and returns a promise of the run's data. `mutate()`,
1675
+ * `mutateAsync()` and `reset()` are safe to call from an effect: none makes
1676
+ * the effect depend on what it reads, such as the options or the previous
1677
+ * run. Bound to the current injection
1523
1678
  * context. Destroying it doesn't cancel a run in flight: the write has most
1524
1679
  * likely reached the server, so the run finishes and fires its hooks.
1525
1680
  *
@@ -1531,8 +1686,8 @@ function injectQueries(optionsFn, options) {
1531
1686
  * @typeParam TContext - Type returned by `onMutate`, passed to later hooks.
1532
1687
  * @param optionsFn - Factory returning the {@link MutationOptions}.
1533
1688
  * @param options - Optional `injector` to use outside an injection context.
1534
- * @returns A {@link MutationResult}: `mutate()`, `reset()`, and state signals
1535
- * (`status`, `data`, `error`, `isPending`, …).
1689
+ * @returns A {@link MutationResult}: `mutate()`, `mutateAsync()`, `reset()`,
1690
+ * and state signals (`status`, `data`, `error`, `isPending`, …).
1536
1691
  *
1537
1692
  * @example
1538
1693
  * ```ts
@@ -1564,23 +1719,32 @@ function injectMutation(optionsFn, options) {
1564
1719
  inject(DestroyRef).onDestroy(() => untracked(current)?.removeObserver());
1565
1720
  const state = computed(() => current()?.state() ??
1566
1721
  getInitialState(), ...(ngDevMode ? [{ debugName: "state" }] : []));
1722
+ // mutate(), mutateAsync() and reset() run untracked, as the QueryClient's
1723
+ // methods do. A call made from an effect mustn't make the effect depend on
1724
+ // what they read to do their job: the previous run's state, the options,
1725
+ // or what onMutate and mutationFn read. Otherwise a change to any of them
1726
+ // re-runs the effect, and the effect repeats the call.
1727
+ //
1728
+ // Starts a run for mutate() and mutateAsync(), makes it the run the
1729
+ // signals below follow, and returns its promise.
1730
+ const start = (variables) => untracked(() => {
1731
+ // Stop observing the previous run. It isn't cancelled — if it's
1732
+ // still in flight it finishes and fires its hooks; it just no longer
1733
+ // feeds the signals below.
1734
+ current()?.removeObserver();
1735
+ // Options are read per call, so a signal used in them stays live.
1736
+ const run = cache.build(client.defaultMutationOptions(optionsFn()));
1737
+ run.addObserver();
1738
+ current.set(run);
1739
+ return run.execute(variables);
1740
+ });
1567
1741
  return {
1568
- // mutate() and reset() run untracked, as the QueryClient's methods do. A
1569
- // call made from an effect mustn't make the effect depend on what they
1570
- // read to do their job: the previous run's state, the options, or what
1571
- // onMutate and mutationFn read. Otherwise a change to any of them
1572
- // re-runs the effect, and the effect repeats the call.
1573
- mutate: (variables) => untracked(() => {
1574
- // Stop observing the previous run. It isn't cancelled — if it's
1575
- // still in flight it finishes and fires its hooks; it just no longer
1576
- // feeds the signals below.
1577
- current()?.removeObserver();
1578
- // Options are read per call, so a signal used in them stays live.
1579
- const run = cache.build(client.defaultMutationOptions(optionsFn()));
1580
- run.addObserver();
1581
- current.set(run);
1582
- run.execute(variables);
1583
- }),
1742
+ // The outcome shows in the signals; a failure mustn't surface as an
1743
+ // unhandled rejection too.
1744
+ mutate: (variables) => {
1745
+ start(variables).catch(() => undefined);
1746
+ },
1747
+ mutateAsync: start,
1584
1748
  reset: () => untracked(() => {
1585
1749
  // "Forget the result", not "stop the request". A run still in
1586
1750
  // flight keeps going and fires its hooks, so the cache stays in step
@@ -1773,5 +1937,5 @@ function withDefaultOptions(options) {
1773
1937
  * Generated bundle index. Do not edit.
1774
1938
  */
1775
1939
 
1776
- export { QueryClient, QueryClientFeatureKind, injectIsFetching, injectIsMutating, injectMutation, injectQueries, injectQuery, injectQueryClient, keepPreviousData, mutationOptions, provideQueryClient, queryOptions, withDefaultOptions };
1940
+ export { CancelledError, QueryClient, QueryClientFeatureKind, injectIsFetching, injectIsMutating, injectMutation, injectQueries, injectQuery, injectQueryClient, keepPreviousData, mutationOptions, provideQueryClient, queryOptions, withDefaultOptions };
1777
1941
  //# sourceMappingURL=ngx-signal-query.mjs.map