{"_id":"@assebc/ng-signal-http","_rev":"4-43771bc4a18434aef172ed5fdd20e165","name":"@assebc/ng-signal-http","dist-tags":{"latest":"2.0.0"},"versions":{"1.0.0":{"name":"@assebc/ng-signal-http","version":"1.0.0","keywords":["angular","signals","http","zoneless","fetch","rxjs-free"],"license":"MIT","_id":"@assebc/ng-signal-http@1.0.0","maintainers":[{"name":"assebc","email":"claudioafbessa@gmail.com"}],"homepage":"https://github.com/assebc/ng-signal-http#readme","bugs":{"url":"https://github.com/assebc/ng-signal-http/issues"},"dist":{"shasum":"cd3c3e5c14c9528f3b3289bdcc766877d3b86411","tarball":"https://registry.npmjs.org/@assebc/ng-signal-http/-/ng-signal-http-1.0.0.tgz","fileCount":5,"integrity":"sha512-PlOJzgkWYCQCDLKq/Dz4rmqyvn/nASumaxeXUiAJcCFGphRYgsEUGzdBTLwei9vfQZs8S+Owc6bto8gSSVT+sg==","signatures":[{"sig":"MEYCIQDQv/Yd22LN+e8WGMny8b7/qKpD2AuswMxxGTrGl/GingIhAMWzH16UX9LLAvXj2Vp1D1f9hxS3vIMeG+r1IWxAA7jX","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":82755},"type":"module","module":"fesm2022/assebc-ng-signal-http.mjs","exports":{".":{"types":"./types/assebc-ng-signal-http.d.ts","default":"./fesm2022/assebc-ng-signal-http.mjs"},"./package.json":{"default":"./package.json"}},"gitHead":"b09a896879ba912725b2a2d264a5b4da356b0bc1","typings":"types/assebc-ng-signal-http.d.ts","_npmUser":{"name":"assebc","email":"claudioafbessa@gmail.com"},"repository":{"url":"git+https://github.com/assebc/ng-signal-http.git","type":"git"},"_npmVersion":"10.9.8","description":"[![npm version](https://img.shields.io/npm/v/ng-signal-http)](https://www.npmjs.com/package/ng-signal-http) [![bundle size](https://img.shields.io/bundlephobia/minzip/ng-signal-http)](https://bundlephobia.com/package/ng-signal-http) [![license](https://im","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","dependencies":{"tslib":"^2.3.0"},"_hasShrinkwrap":false,"peerDependencies":{"@angular/core":"^21.2.0","@angular/common":"^21.2.0"},"_npmOperationalInternal":{"tmp":"tmp/ng-signal-http_1.0.0_1782198707321_0.9650796988625443","host":"s3://npm-registry-packages-npm-production"}},"1.0.4":{"name":"@assebc/ng-signal-http","version":"1.0.4","keywords":["angular","signals","http","zoneless","fetch","rxjs-free"],"license":"MIT","_id":"@assebc/ng-signal-http@1.0.4","maintainers":[{"name":"assebc","email":"claudioafbessa@gmail.com"}],"homepage":"https://github.com/assebc/ng-signal-http#readme","bugs":{"url":"https://github.com/assebc/ng-signal-http/issues"},"dist":{"shasum":"0c1afb1d4991f89d2acf601fb0f2ca8a775cad77","tarball":"https://registry.npmjs.org/@assebc/ng-signal-http/-/ng-signal-http-1.0.4.tgz","fileCount":5,"integrity":"sha512-Ufbhn9FcYCQFmQSPgDfiNLS1GvdqLbBuY675J6xfVBZoy+mOGl3FYjo0nFnkWSi4dNmmj1O0FbwVHEfF4HF6XA==","signatures":[{"sig":"MEUCIGncCFeQ4jSIKUrqd8efDVf3yKZIUxDgG6rb4jsve6GPAiEA+SEWCyPHoAeIx3FRgRivcN040nhkD2upZP7RlBG2H5k=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":82915},"type":"module","module":"fesm2022/assebc-ng-signal-http.mjs","exports":{".":{"types":"./types/assebc-ng-signal-http.d.ts","default":"./fesm2022/assebc-ng-signal-http.mjs"},"./package.json":{"default":"./package.json"}},"typings":"types/assebc-ng-signal-http.d.ts","_npmUser":{"name":"assebc","email":"claudioafbessa@gmail.com"},"repository":{"url":"git+https://github.com/assebc/ng-signal-http.git","type":"git"},"_npmVersion":"10.9.8","description":"Signal-native HTTP client for Angular. Wraps the native Fetch API and returns Angular signals directly — no toSignal(), no RxJS required.","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","dependencies":{"tslib":"^2.3.0"},"_hasShrinkwrap":false,"peerDependencies":{"@angular/core":"^21.2.0","@angular/common":"^21.2.0"},"_npmOperationalInternal":{"tmp":"tmp/ng-signal-http_1.0.4_1782199721380_0.8366316440171098","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@assebc/ng-signal-http","version":"2.0.0","description":"Signal-native HTTP client for Angular. Wraps the native Fetch API and returns Angular signals directly — no toSignal(), no RxJS required.","peerDependencies":{"@angular/common":"^21.2.0","@angular/core":"^21.2.0"},"sideEffects":false,"repository":{"type":"git","url":"git+https://github.com/assebc/ng-signal-http.git"},"bugs":{"url":"https://github.com/assebc/ng-signal-http/issues"},"homepage":"https://github.com/assebc/ng-signal-http#readme","license":"MIT","keywords":["angular","signals","http","zoneless","fetch","rxjs-free"],"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"module":"fesm2022/assebc-ng-signal-http.mjs","typings":"types/assebc-ng-signal-http.d.ts","exports":{"./package.json":{"default":"./package.json"},".":{"types":"./types/assebc-ng-signal-http.d.ts","default":"./fesm2022/assebc-ng-signal-http.mjs"}},"type":"module","dependencies":{"tslib":"^2.3.0"},"gitHead":"4c9aa6b9822e36fdfd94a9d1c6fbbf359f6341d9","_id":"@assebc/ng-signal-http@2.0.0","_nodeVersion":"26.4.0","_npmVersion":"11.12.0","dist":{"integrity":"sha512-ktbHGxV6jTw75/V5nSK7MUCkDHhjfs1ntzWisxqSre5lNLbEEyBrBQDmfQZXIGlydbG3iwje0grhg8Q56nDi5w==","shasum":"651dcd31a6d069a694d93edda017df0b346fa56e","tarball":"https://registry.npmjs.org/@assebc/ng-signal-http/-/ng-signal-http-2.0.0.tgz","fileCount":5,"unpackedSize":132923,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCF2OzYjez9jJYxT+DHvSTUEDkTT/1UXm63q7Qho4HOxwIhAMX+zk9qSjBh6m8iNgc7Flhi4vGDSq0TY42a5QiIaRfc"}]},"_npmUser":{"name":"cbessa","email":"claudioafbessa@gmail.com"},"directories":{},"maintainers":[{"name":"cbessa","email":"claudioafbessa@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ng-signal-http_2.0.0_1785718587598_0.27239967117482333"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-23T07:11:47.077Z","modified":"2026-08-03T00:56:27.928Z","1.0.0":"2026-06-23T07:11:47.467Z","1.0.4":"2026-06-23T07:28:41.516Z","2.0.0":"2026-08-03T00:56:27.731Z"},"bugs":{"url":"https://github.com/assebc/ng-signal-http/issues"},"license":"MIT","homepage":"https://github.com/assebc/ng-signal-http#readme","keywords":["angular","signals","http","zoneless","fetch","rxjs-free"],"repository":{"type":"git","url":"git+https://github.com/assebc/ng-signal-http.git"},"description":"Signal-native HTTP client for Angular. Wraps the native Fetch API and returns Angular signals directly — no toSignal(), no RxJS required.","maintainers":[{"name":"cbessa","email":"claudioafbessa@gmail.com"}],"readme":"# ng-signal-http\n\n[![npm version](https://img.shields.io/npm/v/@assebc/ng-signal-http)](https://www.npmjs.com/package/@assebc/ng-signal-http)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/@assebc/ng-signal-http)](https://bundlephobia.com/package/@assebc/ng-signal-http)\n[![license](https://img.shields.io/npm/l/@assebc/ng-signal-http)](https://github.com/assebc/ng-signal-http/blob/master/LICENSE)\n[![Angular](https://img.shields.io/badge/Angular-17%2B-red)](https://angular.dev)\n\nSignal-native HTTP client for Angular. Wraps the native Fetch API and returns Angular signals directly — no `toSignal()`, no RxJS required.\n\nBuilt for the post-zoneless Angular era using only `@angular/core` primitives.\n\n---\n\n## Why ng-signal-http?\n\n| Feature | `@angular/common/http` | `ng-signal-http` |\n|---|---|---|\n| Returns | `Observable` | `Signal` |\n| RxJS required | Yes | No |\n| Loading state | Manual | Built-in |\n| Error state | Manual | Built-in |\n| Reactive refetch | Manual (`switchMap`) | Automatic |\n| Request cancellation | Manual (`takeUntil`) | Automatic |\n| Retry | Manual (`retryWhen`) | Built-in |\n| Caching / SWR | Manual | Built-in |\n| WebSocket | Separate library | Built-in |\n| GraphQL | Separate library | Built-in |\n| Persistent cache | Manual | Built-in (IndexedDB) |\n\n**Before**\n\n```typescript\nexport class UsersComponent {\n  private http = inject(HttpClient);\n  users = toSignal(this.http.get<User[]>('/api/users'), { initialValue: [] });\n  // loading? error? refetch? — all manual.\n}\n```\n\n**After**\n\n```typescript\nexport class UsersComponent {\n  users = querySignal<User[]>('/api/users');\n  // users.data(), users.loading(), users.error(), users.refetch() — built-in.\n}\n```\n\n---\n\n## Install\n\n```bash\nnpm install @assebc/ng-signal-http\n```\n\nPeer dependencies: `@angular/core` and `@angular/common` ≥ 17.\n\n---\n\n## Setup\n\nCall `provideSignalHttp()` once in `app.config.ts`:\n\n```typescript\nimport { ApplicationConfig } from '@angular/core';\nimport { provideSignalHttp } from '@assebc/ng-signal-http';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideSignalHttp({\n      baseUrl: 'https://api.example.com',\n      timeout: 10_000,\n    }),\n  ],\n};\n```\n\n---\n\n## Basic usage\n\n### GET — `querySignal`\n\n```typescript\nimport { Component } from '@angular/core';\nimport { querySignal } from '@assebc/ng-signal-http';\n\ninterface User { id: number; name: string; }\n\n@Component({\n  template: `\n    @if (user.loading()) { <p>Loading…</p> }\n    @if (user.error()) { <p>Error: {{ user.error()?.message }}</p> }\n    @if (user.data()) { <p>{{ user.data()?.name }}</p> }\n  `,\n})\nexport class UserComponent {\n  user = querySignal<User>('/users/1');\n}\n```\n\n### POST / PUT / PATCH / DELETE — `mutationSignal`\n\n```typescript\nimport { Component } from '@angular/core';\nimport { mutationSignal } from '@assebc/ng-signal-http';\n\ninterface CreateUser { name: string; email: string; }\ninterface User { id: number; name: string; email: string; }\n\n@Component({\n  template: `\n    <button (click)=\"submit()\" [disabled]=\"newUser.isPending()\">Create</button>\n    @if (newUser.data()) { <p>Created: {{ newUser.data()?.id }}</p> }\n    @if (newUser.error()) { <p>{{ newUser.error()?.message }}</p> }\n  `,\n})\nexport class CreateUserComponent {\n  newUser = mutationSignal<CreateUser, User>(\n    (input) => ({ url: '/users', method: 'POST', body: input }),\n  );\n\n  submit() {\n    this.newUser.mutate({ name: 'Alice', email: 'alice@example.com' });\n  }\n}\n```\n\n---\n\n## Reactive queries\n\n`querySignal` tracks every signal read inside the URL factory and automatically refetches when any of them change. The previous in-flight request is cancelled.\n\n```typescript\n@Component({ template: `<p>{{ user.data()?.name }}</p>` })\nexport class UserComponent {\n  userId = signal(1);\n  user = querySignal<User>(() => `/users/${this.userId()}`);\n}\n```\n\n### Lazy queries\n\n```typescript\nsearch = querySignal<Result[]>(() => `/search?q=${this.query()}`, { lazy: true });\n\n// fetch only when explicitly triggered\nthis.search.refetch();\n```\n\n### Polling\n\n```typescript\nstats = querySignal('/dashboard/stats', { refetchInterval: 30_000 });\n```\n\n### Refetch on focus / reconnect\n\n```typescript\nfeed = querySignal('/feed', {\n  staleTime: 60_000,\n  refetchOnFocus: true,\n  refetchOnReconnect: true,\n});\n```\n\n### Skip on server (SSR)\n\n```typescript\n// This query will not fire during server-side rendering.\nuserData = querySignal('/me', { skipOnServer: true });\n```\n\n### Response transform\n\n```typescript\n// Use `select` to reshape the raw response before setting the data signal.\nconst names = querySignal<string[]>('/users', {\n  select: (raw) => (raw as User[]).map(u => u.name),\n});\n```\n\n---\n\n## Caching (stale-while-revalidate)\n\n```typescript\nquerySignal('/users', {\n  staleTime: 60_000,        // data is fresh for 60 s after fetch\n  refetchOnFocus: true,     // revalidate when window regains focus (only if stale)\n  refetchOnReconnect: true, // revalidate when network reconnects\n})\n```\n\n- **Fresh hit** — cached data served immediately, no network request.\n- **Stale hit** — cached data served immediately; a background request updates the signal when done.\n- **Miss** — normal fetch; result stored for future hits.\n\nUse `result.isStale()` to check whether current data is past its `staleTime`.  \nUse `result.invalidate()` to mark data as stale and trigger a background refetch next time.\n\n### Request deduplication\n\nMultiple `querySignal` instances with the same URL/body share a single in-flight request. No extra configuration needed.\n\n### Persistent cache (IndexedDB)\n\n```typescript\n// app.config.ts\nproviders: [\n  provideSignalHttp({ baseUrl: 'https://api.example.com' }),\n  providePersistentCache({ dbName: 'my-app-cache' }),\n]\n```\n\nOn startup the in-memory cache is hydrated from IndexedDB — components see stale data instantly before any network request fires. All subsequent writes are stored through to IndexedDB. SSR-safe (no IDB access on the server).\n\n### Pre-fetch\n\n```typescript\n// In a route resolver — prime the cache before the component mounts\nawait prefetchQuery('/users', { staleTime: 60_000 });\n```\n\n---\n\n## Mutations\n\n```typescript\nupdatePost = mutationSignal<{ id: number; title: string }, Post>(\n  ({ id, ...body }) => ({ url: `/posts/${id}`, method: 'PUT', body }),\n  {\n    onSuccess: (post) => console.log('Updated:', post.title),\n    onError: (err) => console.error('Failed:', err.message),\n    onSettled: (data, err) => console.log('Done', data, err),\n  },\n);\n\nawait this.updatePost.mutate({ id: 1, title: 'New title' });\n```\n\nCalling `mutate()` while a previous request is in flight cancels the previous one.\n\n### Optimistic updates\n\n```typescript\nconst todoList = inject(TodoStore);\n\ndeleteTodo = mutationSignal<number, void>(\n  (id) => ({ url: `/todos/${id}`, method: 'DELETE' }),\n  {\n    onMutate: (id) => {\n      const prev = todoList.snapshot(); // save current state\n      todoList.remove(id);              // apply optimistic update\n      return prev;                      // this becomes the rollback context\n    },\n    onError: (err, id, prev) => {\n      todoList.restore(prev);           // roll back on failure\n    },\n  },\n);\n```\n\n---\n\n## WebSocket\n\n```typescript\nimport { websocketSignal } from '@assebc/ng-signal-http';\n\n@Component({ template: `<p>{{ feed.data()?.price }}</p>` })\nexport class TickerComponent {\n  feed = websocketSignal<StockTick>('wss://api.example.com/ticker', {\n    reconnect: { maxAttempts: 5, delay: (n) => 1000 * 2 ** n },\n    onOpen: () => this.feed.send({ type: 'subscribe', symbol: 'BTC' }),\n  });\n}\n```\n\n- `feed.data()` — latest message (`T | null`)\n- `feed.status()` — `'connecting' | 'open' | 'closed' | 'error'`\n- `feed.send(value)` — JSON-serialises objects; no-op when not open\n- `feed.close()` — manual close, suppresses reconnect\n- `feed.reconnect()` — manual reconnect, resets attempt counter\n\nThe URL can be a signal-reading factory — changing signals reconnect to the new URL automatically and cancel the previous connection.\n\n---\n\n## GraphQL\n\n```typescript\nimport { graphqlQuery, graphqlMutation } from '@assebc/ng-signal-http';\n\n// Query\nconst users = graphqlQuery<{ users: User[] }>(\n  '/graphql',\n  'query { users { id name } }',\n);\n\n// Reactive variables — re-fetches when userId() changes\nconst user = graphqlQuery<{ user: User }, { id: number }>(\n  '/graphql',\n  'query GetUser($id: ID!) { user(id: $id) { id name } }',\n  { variables: () => ({ id: userId() }) },\n);\n\n// Mutation\nconst createUser = graphqlMutation<{ createUser: User }, { name: string }>(\n  '/graphql',\n  'mutation CreateUser($name: String!) { createUser(name: $name) { id } }',\n);\nconst { createUser: created } = await createUser.mutate({ name: 'Alice' });\n```\n\nGraphQL `errors` are surfaced as `GraphQLRequestError` on the `error` signal and cause `mutate()` to reject. The `data` signal holds the unwrapped `response.data` value directly.\n\n---\n\n## Pagination\n\n```typescript\nimport { paginatedQuerySignal } from '@assebc/ng-signal-http';\n\nconst posts = paginatedQuerySignal<Post[]>(\n  (cursor) => ({ url: '/posts', method: 'GET', params: { cursor: String(cursor ?? '') } }),\n  {\n    getNextPageParam: (lastPage) => lastPage.at(-1)?.id ?? undefined,\n    onError: (err) => console.error(err),\n  },\n);\n\n// Template\nposts.pages()             // Post[][]\nposts.loading()           // initial load\nposts.isFetchingNextPage()\nposts.hasNextPage()\nawait posts.fetchNextPage()\nposts.reset()\n```\n\n---\n\n## Parallel queries\n\n```typescript\nimport { parallelQueries } from '@assebc/ng-signal-http';\n\nconst result = parallelQueries<unknown>([\n  () => '/products',\n  () => '/categories',\n  () => '/tags',\n]);\n\nresult.data()    // (unknown | null)[] — one entry per factory, in order\nresult.loading() // true while any query is loading\nresult.errors()  // (Error | null)[]\nresult.status()  // 'loading' | 'success' | 'error'\n```\n\n---\n\n## Interceptors\n\nAll hooks are optional and may return a `Promise`. They run in registration order.\n\n```typescript\nprovideSignalHttp({\n  interceptors: [\n    {\n      request: async (config) => ({\n        ...config,\n        headers: { ...config.headers, Authorization: `Bearer ${getToken()}` },\n      }),\n      response: async (res) => { console.log('←', res.status); return res; },\n      error: async (err) => {\n        if (err instanceof HttpError && err.isUnauthorized) await refreshToken();\n        return err;\n      },\n    },\n  ],\n});\n```\n\n---\n\n## Plugin system\n\nA plugin bundles interceptors and cache lifecycle hooks under one named unit.\n\n```typescript\nimport { SignalHttpPlugin, provideSignalHttp } from '@assebc/ng-signal-http';\n\nconst analyticsPlugin: SignalHttpPlugin = {\n  name: 'analytics',\n  interceptors: [timingInterceptor],\n  onCacheSet: (key, data) => analytics.track('cache_set', { key }),\n  onCacheDelete: (key) => analytics.track('cache_delete', { key }),\n  onCacheClear: () => analytics.track('cache_clear'),\n};\n\n// app.config.ts\nprovideSignalHttp({ plugins: [analyticsPlugin] })\n```\n\n---\n\n## Devtools\n\n```typescript\nimport { withRequestLogging, provideSignalHttp } from '@assebc/ng-signal-http';\n\nprovideSignalHttp({\n  interceptors: [withRequestLogging({ verbose: true })],\n})\n// Logs: → GET /users, ← 200 /users (42ms), etc.\n```\n\n---\n\n## Error handling\n\n```typescript\nimport { HttpError, querySignal } from '@assebc/ng-signal-http';\n\nconst post = querySignal<Post>('/posts/1');\n\neffect(() => {\n  const err = post.error();\n  if (!err) return;\n  if (err instanceof HttpError) {\n    if (err.isNotFound)     router.navigate(['/404']);\n    if (err.isUnauthorized) router.navigate(['/login']);\n    if (err.isServerError)  console.error(`Server error ${err.status}`);\n  }\n});\n```\n\n### Retry\n\n```typescript\n// Retry 3 times immediately\nquerySignal('/data', { retry: 3 });\n\n// Custom backoff\nquerySignal('/data', {\n  retry: {\n    count: 4,\n    delay: (attempt) => 1000 * 2 ** (attempt - 1),\n    shouldRetry: (err) => !(err instanceof HttpError && err.isClientError),\n  },\n});\n```\n\n`AbortError` is never retried.\n\n---\n\n## Full API reference\n\n### `provideSignalHttp(config?)`\n\n```typescript\nprovideSignalHttp(config?: SignalHttpConfig): EnvironmentProviders\n```\n\n| `SignalHttpConfig` | Type | Description |\n|---|---|---|\n| `baseUrl` | `string` | Prefix prepended to all relative URLs |\n| `headers` | `Record<string, string>` | Default headers for every request |\n| `timeout` | `number` | Global timeout in ms |\n| `interceptors` | `HttpInterceptor[]` | Global request/response/error hooks |\n| `plugins` | `SignalHttpPlugin[]` | Plugins (interceptors + cache hooks) |\n\n### `providePersistentCache(options?)`\n\n```typescript\nprovidePersistentCache(options?: IdbCacheOptions): EnvironmentProviders\n```\n\n| `IdbCacheOptions` | Type | Default |\n|---|---|---|\n| `dbName` | `string` | `'ng-signal-http-cache'` |\n| `storeName` | `string` | `'cache'` |\n\n---\n\n### `querySignal<T>(url, options?)`\n\n```typescript\nquerySignal<T>(url: string | UrlFactory, options?: HttpClientOptions<T>): HttpClientResult<T>\n```\n\n**Options (`HttpClientOptions<T>`)**\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `initialValue` | `T` | `null` | Signal value before first success |\n| `lazy` | `boolean` | `false` | Skip the initial fetch |\n| `retry` | `number \\| RetryConfig` | — | Retry on failure |\n| `staleTime` | `number` | — | Ms after which cached data is stale |\n| `refetchInterval` | `number` | — | Poll interval in ms |\n| `refetchOnFocus` | `boolean` | `false` | Revalidate on window focus (if stale) |\n| `refetchOnReconnect` | `boolean` | `false` | Revalidate on network reconnect |\n| `skipOnServer` | `boolean` | `false` | Skip all fetches during SSR |\n| `select` | `(raw: unknown) => T` | — | Transform raw response before setting signal |\n| `onSuccess` | `(data: T) => void` | — | Called after successful fetch |\n| `onError` | `(error: Error) => void` | — | Called after failed fetch |\n\n**Return value (`HttpClientResult<T>`)**\n\n| Property | Type | Description |\n|---|---|---|\n| `data` | `Signal<T \\| null>` | Response data |\n| `loading` | `Signal<boolean>` | `true` while in flight |\n| `error` | `Signal<Error \\| null>` | Last error; cleared on new fetch |\n| `status` | `Signal<HttpClientStatus>` | `'idle' \\| 'loading' \\| 'success' \\| 'error'` |\n| `isStale` | `Signal<boolean>` | `true` if data is past `staleTime` |\n| `refetch()` | `() => Promise<void>` | Trigger a new fetch |\n| `invalidate()` | `() => void` | Mark data stale, clear cache entry |\n| `reset()` | `() => void` | Abort + restore initial state |\n\n---\n\n### `mutationSignal<TInput, TOutput, TContext>(factory, options?)`\n\n```typescript\nmutationSignal<TInput, TOutput, TContext = unknown>(\n  requestFactory: (input: TInput) => RequestConfig,\n  options?: MutationOptions<TInput, TOutput, TContext>\n): MutationResult<TInput, TOutput>\n```\n\n**Options (`MutationOptions<TInput, TOutput, TContext>`)**\n\n| Option | Type | Description |\n|---|---|---|\n| `onMutate` | `(input: TInput) => TContext \\| Promise<TContext>` | Runs before network request; return value is rollback context |\n| `onSuccess` | `(data: TOutput, input: TInput) => void` | Called on success |\n| `onError` | `(error: Error, input: TInput, context: TContext \\| undefined) => void` | Called on failure |\n| `onSettled` | `(data: TOutput \\| null, error: Error \\| null, input: TInput) => void` | Called after either outcome |\n| `select` | `(raw: unknown) => TOutput` | Transform raw response before setting signal |\n\n**Return value (`MutationResult<TInput, TOutput>`)**\n\n| Property | Type | Description |\n|---|---|---|\n| `isPending` | `Signal<boolean>` | `true` while in flight |\n| `data` | `Signal<TOutput \\| null>` | Last successful response |\n| `error` | `Signal<Error \\| null>` | Last error |\n| `mutate(input)` | `(input: TInput) => Promise<TOutput>` | Trigger the request |\n| `reset()` | `() => void` | Clear all state |\n\n---\n\n### `websocketSignal<T>(url, options?)`\n\n```typescript\nwebsocketSignal<T>(url: string | (() => string), options?: WebSocketOptions<T>): WebSocketResult<T>\n```\n\n**Options (`WebSocketOptions<T>`)**\n\n| Option | Type | Description |\n|---|---|---|\n| `deserialize` | `(event: MessageEvent) => T` | Custom message parser (default: `JSON.parse`) |\n| `reconnect` | `boolean \\| ReconnectConfig` | Auto-reconnect on unexpected close |\n| `initialValue` | `T` | Signal value before first message |\n| `onOpen` | `() => void` | Called when socket opens |\n| `onClose` | `(event: CloseEvent) => void` | Called when socket closes |\n| `onError` | `(event: Event) => void` | Called on socket error |\n| `onMessage` | `(data: T) => void` | Called for each message |\n\n**`ReconnectConfig`**\n\n| Option | Type | Default |\n|---|---|---|\n| `maxAttempts` | `number` | `5` |\n| `delay` | `number \\| ((attempt: number) => number)` | `1000 * 2^(attempt-1)` |\n\n**Return value (`WebSocketResult<T>`)**\n\n| Property | Type | Description |\n|---|---|---|\n| `data` | `Signal<T \\| null>` | Latest message |\n| `status` | `Signal<WebSocketStatus>` | `'connecting' \\| 'open' \\| 'closed' \\| 'error'` |\n| `error` | `Signal<Event \\| null>` | Last error event |\n| `send(data)` | `(data: unknown) => void` | Send a message (JSON-serialises objects) |\n| `close()` | `() => void` | Close and disable reconnect |\n| `reconnect()` | `() => void` | Reconnect and reset attempt counter |\n\n---\n\n### `graphqlQuery<TData, TVariables>(endpoint, document, options?)`\n\n```typescript\ngraphqlQuery<TData, TVariables>(\n  endpoint: string,\n  document: string,\n  options?: GraphQLQueryOptions<TData, TVariables>\n): HttpClientResult<TData>\n```\n\n`GraphQLQueryOptions` extends `HttpClientOptions` (minus `select`) and adds:\n\n| Option | Type | Description |\n|---|---|---|\n| `variables` | `TVariables \\| (() => TVariables)` | Query variables; factory tracks signal reads |\n\n---\n\n### `graphqlMutation<TData, TVariables>(endpoint, document, options?)`\n\n```typescript\ngraphqlMutation<TData, TVariables, TContext>(\n  endpoint: string,\n  document: string,\n  options?: GraphQLMutationOptions<TData, TVariables, TContext>\n): MutationResult<TVariables, TData>\n```\n\n`GraphQLMutationOptions` extends `MutationOptions` (minus `select`).\n\n---\n\n### `paginatedQuerySignal<T>(urlFactory, options?)`\n\n```typescript\npaginatedQuerySignal<T>(\n  urlFactory: (pageParam: unknown) => string | RequestConfig,\n  options?: PaginatedOptions<T>\n): PaginatedResult<T>\n```\n\n**Options (`PaginatedOptions<T>`)**\n\n| Option | Type | Description |\n|---|---|---|\n| `getNextPageParam` | `(lastPage: T, allPages: T[]) => unknown` | Returns the next page param; `undefined` means no more pages |\n| `lazy` | `boolean` | Skip the initial fetch |\n| `retry` | `number \\| RetryConfig` | Retry on failure |\n| `onSuccess` | `(data: T, pageParam: unknown) => void` | Called after each page loads |\n| `onError` | `(error: Error) => void` | Called on failure |\n\n**Return value (`PaginatedResult<T>`)**\n\n| Property | Type | Description |\n|---|---|---|\n| `pages` | `Signal<T[]>` | All loaded pages in order |\n| `loading` | `Signal<boolean>` | `true` during initial load |\n| `isFetchingNextPage` | `Signal<boolean>` | `true` while loading next page |\n| `hasNextPage` | `Signal<boolean>` | `false` when `getNextPageParam` returns `undefined` |\n| `error` | `Signal<Error \\| null>` | Last error |\n| `fetchNextPage()` | `() => Promise<void>` | Load the next page |\n| `reset()` | `() => void` | Clear all pages and reset state |\n\n---\n\n### `parallelQueries<T>(factories, options?)`\n\n```typescript\nparallelQueries<T>(\n  factories: Array<() => string | RequestConfig>,\n  options?: HttpClientOptions<T>\n): ParallelQueriesResult<T>\n```\n\n**Return value (`ParallelQueriesResult<T>`)**\n\n| Property | Type | Description |\n|---|---|---|\n| `data` | `Signal<(T \\| null)[]>` | Results in factory order |\n| `loading` | `Signal<boolean>` | `true` while any query is loading |\n| `errors` | `Signal<(Error \\| null)[]>` | Per-query errors |\n| `status` | `Signal<HttpClientStatus>` | Aggregate status |\n| `refetchAll()` | `() => Promise<void>` | Re-trigger all queries |\n| `resetAll()` | `() => void` | Reset all queries |\n\n---\n\n### `SignalHttpClient`\n\n```typescript\n@Injectable({ providedIn: 'root' })\nclass SignalHttpClient {\n  get<T>(url: string, options?: Partial<RequestConfig>): Signal<T | null>\n  post<T>(url: string, body?: unknown, options?: Partial<RequestConfig>): Signal<T | null>\n  put<T>(url: string, body?: unknown, options?: Partial<RequestConfig>): Signal<T | null>\n  patch<T>(url: string, body?: unknown, options?: Partial<RequestConfig>): Signal<T | null>\n  delete<T>(url: string, options?: Partial<RequestConfig>): Signal<T | null>\n  executeRequest<T>(config: RequestConfig): Promise<T>\n}\n```\n\n---\n\n### `HttpError`\n\n```typescript\nclass HttpError extends Error {\n  readonly status: number;\n  readonly response?: Response;\n\n  get isClientError(): boolean   // 4xx\n  get isServerError(): boolean   // 5xx\n  get isTimeout(): boolean       // 408\n  get isNotFound(): boolean      // 404\n  get isUnauthorized(): boolean  // 401\n  get isForbidden(): boolean     // 403\n}\n```\n\n---\n\n### `GraphQLRequestError`\n\n```typescript\nclass GraphQLRequestError extends Error {\n  readonly errors: GraphQLError[];\n}\n```\n\nThrown (and set on the `error` signal) when a GraphQL response contains `errors` or has no `data`.\n\n---\n\n### `RequestConfig`\n\n```typescript\ninterface RequestConfig {\n  url: string;\n  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n  headers?: Record<string, string>;\n  body?: unknown;\n  params?: Record<string, string | number | boolean>;\n  timeout?: number;      // overrides global timeout\n  signal?: AbortSignal;  // merged with the internal AbortController\n}\n```\n\n---\n\n### `RetryConfig`\n\n```typescript\ninterface RetryConfig {\n  count: number;\n  delay?: number | ((attempt: number) => number);\n  shouldRetry?: (error: Error, attempt: number) => boolean;\n}\n```\n\n---\n\n## Migration from `HttpClient`\n\n| Before (`@angular/common/http`) | After (`ng-signal-http`) |\n|---|---|\n| `imports: [HttpClientModule]` | `providers: [provideSignalHttp()]` |\n| `inject(HttpClient).get<T>(url)` → `Observable<T>` | `querySignal<T>(url)` → `HttpClientResult<T>` |\n| `async pipe` + manual loading flag | `result.data()` + `result.loading()` |\n| `pipe(takeUntil(destroy$))` | automatic — cancelled on destroy |\n| `pipe(switchMap(...))` | reactive factory: `` () => `/users/${id()}` `` |\n| `pipe(retry(3))` | `{ retry: 3 }` option |\n| `pipe(catchError(...))` | `result.error()` + `onError` callback |\n| `http.post<T>(url, body)` → `Observable` | `mutationSignal(...)` |\n\nSee the full [MIGRATION.md](https://github.com/assebc/ng-signal-http/blob/master/MIGRATION.md) for a step-by-step guide.\n\n---\n\n## Browser support\n\nAny browser with native `fetch`: Chrome/Edge 90+, Firefox 88+, Safari 14+. No IE11.\n\nSSR is fully supported — window events (`focus`, `online`) and WebSocket connections are skipped on the server. Use `skipOnServer: true` to also skip fetches during SSR.\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md"}