{"version":3,"file":"inject-mutation.mjs","sources":["../src/inject-mutation.ts"],"sourcesContent":["import {\n  Injector,\n  NgZone,\n  assertInInjectionContext,\n  computed,\n  effect,\n  inject,\n  signal,\n  untracked,\n} from '@angular/core'\nimport {\n  MutationObserver,\n  QueryClient,\n  noop,\n  notifyManager,\n  shouldThrowError,\n} from '@tanstack/query-core'\nimport { signalProxy } from './signal-proxy'\nimport { PENDING_TASKS } from './pending-tasks-compat'\nimport type { PendingTaskRef } from './pending-tasks-compat'\nimport type { DefaultError, MutationObserverResult } from '@tanstack/query-core'\nimport type {\n  CreateMutateFunction,\n  CreateMutationOptions,\n  CreateMutationResult,\n} from './types'\n\nexport interface InjectMutationOptions {\n  /**\n   * The `Injector` in which to create the mutation.\n   *\n   * If this is not provided, the current injection context will be used instead (via `inject`).\n   */\n  injector?: Injector\n}\n\n/**\n * Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects.\n * `injectMutation` is the function for that. Unlike queries, mutations are not run automatically.\n *\n * @remarks `mutate`/`mutateAsync` also accept per-call `onSuccess`/`onError`/`onSettled` callbacks as a\n * second argument, useful for triggering call-site side effects (e.g. navigation) without coupling them to\n * the shared mutation definition. Callbacks defined in `injectMutationFn` fire for every mutation; per-call\n * callbacks fire only for the latest call you've made — `mutateAsync` gives you a promise per call instead,\n * so you can await `Promise.all`/`Promise.allSettled` over several calls and see each one's outcome.\n * @see {@link mutationOptions} to share these options across multiple `injectMutation` call sites, or to look\n * the mutation up elsewhere via its `mutationKey` (e.g. with `injectMutationState`).\n * @param injectMutationFn - A function that returns mutation options. Similar to `computed` from Angular,\n * this function runs in the reactive context, so signals read inside it drive the mutation's options.\n * @param options - Additional configuration\n * @returns The mutation result. Value fields are exposed as a `Signal` — read `data`/`error` by calling them\n * (e.g. `mutation.data()`) — while function fields (`mutate`, `mutateAsync`, `reset`) are called directly,\n * unchanged. `isSuccess`/`isError`/`isPending`/`isIdle` are type-guard methods you can call to narrow whether\n * `data` is defined.\n *\n * @example\n * ```angular-ts\n * @Component({\n *   selector: 'todos',\n *   template: `\n *     @if (addMutation.isPending()) {\n *       <span>Adding todo...</span>\n *     } @else if (addMutation.isError()) {\n *       <div>An error occurred: {{ addMutation.error()?.message }}</div>\n *     }\n *     <button (click)=\"addMutation.mutate('Item')\">Add</button>\n *   `,\n * })\n * export class Todos {\n *   readonly #queryClient = inject(QueryClient)\n *\n *   readonly addMutation = injectMutation(() => ({\n *     mutationFn: addTodo,\n *     onSuccess: () => this.#queryClient.invalidateQueries({ queryKey: ['todos'] }),\n *   }))\n * }\n * ```\n *\n * @example\n * Optimistic update via `onMutate`, rolling back on `onError`:\n * ```angular-ts\n * @Component({\n *   selector: 'todos',\n *   template: `<button (click)=\"addMutation.mutate('Item')\">Add</button>`,\n * })\n * export class Todos {\n *   readonly #queryClient = inject(QueryClient)\n *\n *   readonly addMutation = injectMutation(() => ({\n *     mutationFn: addTodo,\n *     onMutate: async (newTodo) => {\n *       await this.#queryClient.cancelQueries({ queryKey: ['todos'] })\n *       const previousTodos = this.#queryClient.getQueryData<Array<string>>(['todos'])\n *\n *       this.#queryClient.setQueryData<Array<string>>(['todos'], (old) => [\n *         ...(old ?? []),\n *         newTodo,\n *       ])\n *\n *       // Passed to `onError` as `onMutateResult` if the mutation fails.\n *       return { previousTodos }\n *     },\n *     onError: (_err, _newTodo, onMutateResult) => {\n *       this.#queryClient.setQueryData(['todos'], onMutateResult?.previousTodos)\n *     },\n *     onSettled: () => {\n *       this.#queryClient.invalidateQueries({ queryKey: ['todos'] })\n *     },\n *   }))\n * }\n * ```\n *\n * @example\n * Callbacks passed per call to `mutate` only fire for the last call — `mutateAsync` gives you a promise per\n * call instead, so you can wait for all of them when they succeed:\n * ```angular-ts\n * @Component({\n *   selector: 'todos',\n *   template: `\n *     <button (click)=\"handleAddAll(['Todo 1', 'Todo 2', 'Todo 3'])\">Add all</button>\n *   `,\n * })\n * export class Todos {\n *   readonly #queryClient = inject(QueryClient)\n *\n *   readonly addMutation = injectMutation(() => ({\n *     mutationFn: addTodo,\n *     onSuccess: () => this.#queryClient.invalidateQueries({ queryKey: ['todos'] }),\n *   }))\n *\n *   async handleAddAll(todos: Array<string>) {\n *     try {\n *       await Promise.all(todos.map((todo) => this.addMutation.mutateAsync(todo)))\n *     } catch (error) {\n *       console.error('Failed to add todos:', error)\n *     }\n *   }\n * }\n * ```\n *\n * @example\n * If some of the mutations above can fail independently of the others, and you want to know which ones did —\n * rather than losing that information the moment the first one rejects — swap `Promise.all` for\n * `Promise.allSettled`:\n * ```angular-ts\n * @Component({\n *   selector: 'todos',\n *   template: `\n *     <button (click)=\"handleAddAll(['Todo 1', 'Todo 2', 'Todo 3'])\">Add all</button>\n *   `,\n * })\n * export class Todos {\n *   readonly #queryClient = inject(QueryClient)\n *\n *   readonly addMutation = injectMutation(() => ({\n *     mutationFn: addTodo,\n *     onSuccess: () => this.#queryClient.invalidateQueries({ queryKey: ['todos'] }),\n *   }))\n *\n *   async handleAddAll(todos: Array<string>) {\n *     const addResults = await Promise.allSettled(\n *       todos.map((todo) => this.addMutation.mutateAsync(todo)),\n *     )\n *\n *     addResults.forEach((addResult, index) => {\n *       if (addResult.status === 'rejected') {\n *         console.error(`Failed to add \"${todos[index]}\":`, addResult.reason)\n *       }\n *     })\n *   }\n * }\n * ```\n */\nexport function injectMutation<\n  TData = unknown,\n  TError = DefaultError,\n  TVariables = void,\n  TOnMutateResult = unknown,\n>(\n  injectMutationFn: () => CreateMutationOptions<\n    TData,\n    TError,\n    TVariables,\n    TOnMutateResult\n  >,\n  options?: InjectMutationOptions,\n): CreateMutationResult<TData, TError, TVariables, TOnMutateResult> {\n  !options?.injector && assertInInjectionContext(injectMutation)\n  const injector = options?.injector ?? inject(Injector)\n  const ngZone = injector.get(NgZone)\n  const pendingTasks = injector.get(PENDING_TASKS)\n  const queryClient = injector.get(QueryClient)\n\n  /**\n   * computed() is used so signals can be inserted into the options\n   * making it reactive. Wrapping options in a function ensures embedded expressions\n   * are preserved and can keep being applied after signal changes\n   */\n  const optionsSignal = computed(injectMutationFn)\n\n  const observerSignal = (() => {\n    let instance: MutationObserver<\n      TData,\n      TError,\n      TVariables,\n      TOnMutateResult\n    > | null = null\n\n    return computed(() => {\n      return (instance ||= new MutationObserver(queryClient, optionsSignal()))\n    })\n  })()\n\n  const mutateFnSignal = computed<\n    CreateMutateFunction<TData, TError, TVariables, TOnMutateResult>\n  >(() => {\n    const observer = observerSignal()\n    return (\n      ...args: Parameters<\n        CreateMutateFunction<TData, TError, TVariables, TOnMutateResult>\n      >\n    ) => {\n      observer.mutate(args[0] as TVariables, args[1]).catch(noop)\n    }\n  })\n\n  /**\n   * Computed signal that gets result from mutation cache based on passed options\n   */\n  const resultFromInitialOptionsSignal = computed(() => {\n    const observer = observerSignal()\n    return observer.getCurrentResult()\n  })\n\n  /**\n   * Signal that contains result set by subscriber\n   */\n  const resultFromSubscriberSignal = signal<MutationObserverResult<\n    TData,\n    TError,\n    TVariables,\n    TOnMutateResult\n  > | null>(null)\n\n  effect(\n    () => {\n      const observer = observerSignal()\n      const observerOptions = optionsSignal()\n\n      untracked(() => {\n        observer.setOptions(observerOptions)\n      })\n    },\n    {\n      injector,\n    },\n  )\n\n  effect(\n    (onCleanup) => {\n      // observer.trackResult is not used as this optimization is not needed for Angular\n      const observer = observerSignal()\n      let pendingTaskRef: PendingTaskRef | null = null\n\n      untracked(() => {\n        const unsubscribe = ngZone.runOutsideAngular(() =>\n          observer.subscribe(\n            notifyManager.batchCalls((state) => {\n              ngZone.run(() => {\n                // Track pending task when mutation is pending\n                if (state.isPending && !pendingTaskRef) {\n                  pendingTaskRef = pendingTasks.add()\n                }\n\n                // Clear pending task when mutation is no longer pending\n                if (!state.isPending && pendingTaskRef) {\n                  pendingTaskRef()\n                  pendingTaskRef = null\n                }\n\n                if (\n                  state.isError &&\n                  shouldThrowError(observer.options.throwOnError, [state.error])\n                ) {\n                  ngZone.onError.emit(state.error)\n                  throw state.error\n                }\n\n                resultFromSubscriberSignal.set(state)\n              })\n            }),\n          ),\n        )\n        onCleanup(() => {\n          // Clean up any pending task on destroy\n          if (pendingTaskRef) {\n            pendingTaskRef()\n            pendingTaskRef = null\n          }\n          unsubscribe()\n        })\n      })\n    },\n    {\n      injector,\n    },\n  )\n\n  const resultSignal = computed(() => {\n    const resultFromSubscriber = resultFromSubscriberSignal()\n    const resultFromInitialOptions = resultFromInitialOptionsSignal()\n\n    const result = resultFromSubscriber ?? resultFromInitialOptions\n\n    return {\n      ...result,\n      mutate: mutateFnSignal(),\n      mutateAsync: result.mutate,\n    }\n  })\n\n  return signalProxy(resultSignal) as CreateMutationResult<\n    TData,\n    TError,\n    TVariables,\n    TOnMutateResult\n  >\n}\n"],"names":[],"mappings":";;;;AA6KO,SAAS,eAMd,kBAMA,SACkE;AAClE,IAAC,mCAAS,aAAY,yBAAyB,cAAc;AAC7D,QAAM,YAAW,mCAAS,aAAY,OAAO,QAAQ;AACrD,QAAM,SAAS,SAAS,IAAI,MAAM;AAClC,QAAM,eAAe,SAAS,IAAI,aAAa;AAC/C,QAAM,cAAc,SAAS,IAAI,WAAW;AAO5C,QAAM,gBAAgB,SAAS,gBAAgB;AAE/C,QAAM,kBAAkB,MAAM;AAC5B,QAAI,WAKO;AAEX,WAAO,SAAS,MAAM;AACpB,aAAQ,wBAAa,IAAI,iBAAiB,aAAa,eAAe;AAAA,IACxE,CAAC;AAAA,EACH,GAAA;AAEA,QAAM,iBAAiB,SAErB,MAAM;AACN,UAAM,WAAW,eAAA;AACjB,WAAO,IACF,SAGA;AACH,eAAS,OAAO,KAAK,CAAC,GAAiB,KAAK,CAAC,CAAC,EAAE,MAAM,IAAI;AAAA,IAC5D;AAAA,EACF,CAAC;AAKD,QAAM,iCAAiC,SAAS,MAAM;AACpD,UAAM,WAAW,eAAA;AACjB,WAAO,SAAS,iBAAA;AAAA,EAClB,CAAC;AAKD,QAAM,6BAA6B,OAKzB,IAAI;AAEd;AAAA,IACE,MAAM;AACJ,YAAM,WAAW,eAAA;AACjB,YAAM,kBAAkB,cAAA;AAExB,gBAAU,MAAM;AACd,iBAAS,WAAW,eAAe;AAAA,MACrC,CAAC;AAAA,IACH;AAAA,IACA;AAAA,MACE;AAAA,IAAA;AAAA,EACF;AAGF;AAAA,IACE,CAAC,cAAc;AAEb,YAAM,WAAW,eAAA;AACjB,UAAI,iBAAwC;AAE5C,gBAAU,MAAM;AACd,cAAM,cAAc,OAAO;AAAA,UAAkB,MAC3C,SAAS;AAAA,YACP,cAAc,WAAW,CAAC,UAAU;AAClC,qBAAO,IAAI,MAAM;AAEf,oBAAI,MAAM,aAAa,CAAC,gBAAgB;AACtC,mCAAiB,aAAa,IAAA;AAAA,gBAChC;AAGA,oBAAI,CAAC,MAAM,aAAa,gBAAgB;AACtC,iCAAA;AACA,mCAAiB;AAAA,gBACnB;AAEA,oBACE,MAAM,WACN,iBAAiB,SAAS,QAAQ,cAAc,CAAC,MAAM,KAAK,CAAC,GAC7D;AACA,yBAAO,QAAQ,KAAK,MAAM,KAAK;AAC/B,wBAAM,MAAM;AAAA,gBACd;AAEA,2CAA2B,IAAI,KAAK;AAAA,cACtC,CAAC;AAAA,YACH,CAAC;AAAA,UAAA;AAAA,QACH;AAEF,kBAAU,MAAM;AAEd,cAAI,gBAAgB;AAClB,2BAAA;AACA,6BAAiB;AAAA,UACnB;AACA,sBAAA;AAAA,QACF,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,IACA;AAAA,MACE;AAAA,IAAA;AAAA,EACF;AAGF,QAAM,eAAe,SAAS,MAAM;AAClC,UAAM,uBAAuB,2BAAA;AAC7B,UAAM,2BAA2B,+BAAA;AAEjC,UAAM,SAAS,wBAAwB;AAEvC,WAAO;AAAA,MACL,GAAG;AAAA,MACH,QAAQ,eAAA;AAAA,MACR,aAAa,OAAO;AAAA,IAAA;AAAA,EAExB,CAAC;AAED,SAAO,YAAY,YAAY;AAMjC;"}