{"_id":"@accreation/rtkx","_rev":"2-6ba0efb10ac6d767f1e2b6b2e737a500","name":"@accreation/rtkx","dist-tags":{"latest":"1.0.6"},"versions":{"1.0.4":{"name":"@accreation/rtkx","version":"1.0.4","keywords":["redux","redux-toolkit","rtk-query","websocket","react","chained-queries"],"license":"MIT","_id":"@accreation/rtkx@1.0.4","maintainers":[{"name":"anar-latifov","email":"e.latifov.anar@gmail.com"}],"homepage":"https://github.com/accreation/rtkx#readme","bugs":{"url":"https://github.com/accreation/rtkx/issues"},"dist":{"shasum":"19071ec6d293895f5d183fe656ebd57885a9ecfc","tarball":"https://registry.npmjs.org/@accreation/rtkx/-/rtkx-1.0.4.tgz","fileCount":6,"integrity":"sha512-QifsgnGSS+RXD5rhEpGI1ngwKhb4fCM+ffxrn18hDuYO6e9vQjHv5rqdOHFNiikJrpN/tfxVrzZ758D3BwPiIg==","signatures":[{"sig":"MEUCIDKkVC9f07iL1MvmPnHbFNWhR0b90YZNGhAtCl5ofUKVAiEAs/+ZcxfDe8ugmRKZE/mIooYVFmWXkL6d4PXcgLf/Sf4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":185406},"main":"./dist/index.js","types":"./dist/index.d.ts","module":"./dist/index.mjs","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"8e0c6b2b722c82da953dbec9fb3ddf96ed856a44","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run build"},"_npmUser":{"name":"anar-latifov","email":"e.latifov.anar@gmail.com"},"repository":{"url":"git+https://github.com/accreation/rtkx.git","type":"git"},"_npmVersion":"11.6.2","description":"Extensions for Redux Toolkit Query — WebSocket support and chained queries","directories":{},"sideEffects":false,"_nodeVersion":"24.11.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.0","react":"^18.0.0","vitest":"^2.0.0","typescript":"^5.6.0","react-redux":"^9.0.0","@types/react":"^18.0.0","@reduxjs/toolkit":"^2.3.0"},"peerDependencies":{"react":"^17.0.0 || ^18.0.0 || ^19.0.0","react-redux":"^9.0.0","@reduxjs/toolkit":"^2.3.0"},"peerDependenciesMeta":{"react":{"optional":true},"react-redux":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/rtkx_1.0.4_1785840294683_0.9836409507021071","host":"s3://npm-registry-packages-npm-production"}},"1.0.6":{"name":"@accreation/rtkx","version":"1.0.6","description":"Extensions for Redux Toolkit Query — WebSocket support and chained queries","repository":{"type":"git","url":"git+https://github.com/accreation/rtkx.git"},"homepage":"https://github.com/accreation/rtkx#readme","bugs":{"url":"https://github.com/accreation/rtkx/issues"},"main":"./dist/index.js","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","require":"./dist/index.js","import":"./dist/index.mjs"}},"sideEffects":false,"publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"engines":{"node":">=18.0.0"},"scripts":{"build":"tsup","dev":"tsup --watch","typecheck":"tsc --noEmit","test":"vitest run","prepublishOnly":"npm run typecheck && npm run build"},"keywords":["redux","redux-toolkit","rtk-query","websocket","react","chained-queries"],"license":"MIT","peerDependencies":{"@reduxjs/toolkit":"^2.3.0","react":"^17.0.0 || ^18.0.0 || ^19.0.0","react-redux":"^9.0.0"},"peerDependenciesMeta":{"react":{"optional":true},"react-redux":{"optional":true}},"devDependencies":{"@reduxjs/toolkit":"^2.3.0","@types/react":"^18.0.0","react":"^18.0.0","react-redux":"^9.0.0","tsup":"^8.3.0","typescript":"^5.6.0","vitest":"^2.0.0"},"gitHead":"7bec6a62959ae5d2b565953ac6b0f24f85214303","_id":"@accreation/rtkx@1.0.6","_nodeVersion":"24.11.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-X3KXSvcE3jcstZru3yECJ54G4mNM+QEhGrZqt8F4NWQNz8BjiLArJ3tGykXC08/bLSSI7jNASRlLbYOBUwagYw==","shasum":"e9a1c66f16fa3e0b0c11fc8bf72cdefc1e71b454","tarball":"https://registry.npmjs.org/@accreation/rtkx/-/rtkx-1.0.6.tgz","fileCount":6,"unpackedSize":184886,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFOCDdfFw6pI4jH2pWA8h695yc4e8lMRILvPTIkUV5HtAiBHkcz7iQTpgw4VKXulJnF6GS/iyaZ9iyHX+vtWFIDyBw=="}]},"_npmUser":{"name":"anar-latifov","email":"e.latifov.anar@gmail.com"},"directories":{},"maintainers":[{"name":"anar-latifov","email":"e.latifov.anar@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/rtkx_1.0.6_1785843129586_0.7345287825700764"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-04T10:44:54.450Z","modified":"2026-08-04T11:32:09.931Z","1.0.4":"2026-08-04T10:44:54.823Z","1.0.6":"2026-08-04T11:32:09.737Z"},"bugs":{"url":"https://github.com/accreation/rtkx/issues"},"license":"MIT","homepage":"https://github.com/accreation/rtkx#readme","keywords":["redux","redux-toolkit","rtk-query","websocket","react","chained-queries"],"repository":{"type":"git","url":"git+https://github.com/accreation/rtkx.git"},"description":"Extensions for Redux Toolkit Query — WebSocket support and chained queries","maintainers":[{"name":"anar-latifov","email":"e.latifov.anar@gmail.com"}],"readme":"﻿# RTKX\r\n\r\n> A modular extension layer for [Redux Toolkit Query](https://redux-toolkit.js.org/rtk-query/overview) - first-class WebSocket support, sequential chained queries, and opt-in notifications. Everything ships from a single import.\r\n\r\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.6-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)\r\n[![RTK Query](https://img.shields.io/badge/RTK_Query-^2.3.0-764abc?logo=redux&logoColor=white)](https://redux-toolkit.js.org/rtk-query/overview)\r\n[![React](https://img.shields.io/badge/React-17_%7C_18-61dafb?logo=react&logoColor=black)](https://react.dev/)\r\n[![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)\r\n\r\n---\r\n\r\n## Features\r\n\r\n| | Feature | Description |\r\n|---|---|---|\r\n| 🔌 | `createWsEndpoint` | Pure WebSocket RTK Query endpoint — supports `providesTags`, `initialData`, and WS lifecycle notifications |\r\n| 🌊 | `createStreamingEndpoint` | HTTP seed + WebSocket updates in one endpoint — supports `providesTags` and WS lifecycle notifications |\r\n| 🔧 | `WebSocketManager` | Low-level WS class with exponential back-off reconnect |\r\n| 📨 | `useWebSocketSend` | Send messages over an already-open managed WS connection; optionally invalidates RTK Query tags after each send |\r\n| 🔖 | `createWsEndpointRef` | Create a ref that links an endpoint to `useWebSocketSend` |\r\n| 🔗 | `createQueryChain` | Fluent builder for sequential chains of RTK Query **query** endpoints |\r\n| ⚛️ | `useQueryChain` | React hook that runs a query chain and tracks per-step progress |\r\n| 🔀 | `createMutationChain` | Fluent builder for sequential chains of RTK Query **mutation** endpoints |\r\n| 🚀 | `useMutationChain` | React hook that executes a mutation chain imperatively |\r\n| 🔔 | `createNotificationMiddleware` | Library-agnostic opt-in toast/notification middleware |\r\n| 🔀 | `useParallelQueries` | Fire one endpoint against N args in parallel, results keyed by position |\r\n| 🔀 | `useQueries` | Fire N different endpoints in parallel, results keyed by name |\r\n\r\n---\r\n\r\n## Table of contents\r\n\r\n- [Installation](#installation)\r\n- [Single import source](#single-import-source)\r\n- [WebSocket endpoints](#websocket-endpoints)\r\n  - [createWsEndpoint - pure WebSocket](#createwsendpoint--pure-websocket)\r\n  - [createStreamingEndpoint - HTTP + WebSocket](#createstreamingendpoint--http--websocket)\r\n  - [WebSocketManager - standalone](#websocketmanager--standalone)\r\n  - [useWebSocketSend - sending messages](#usewebsocketsend--sending-messages)\r\n  - [WebSocket tags - providesTags / invalidatesTags](#websocket-tags--providestags--invalidatestags)\r\n- [Chained queries](#chained-queries)\r\n  - [createQueryChain](#createquerychain)\r\n  - [useQueryChain](#usequerychain)\r\n  - [createMutationChain](#createmutationchain)\r\n  - [useMutationChain](#usemutationchain)\r\n- [Notifications (opt-in)](#notifications-opt-in)\r\n  - [createNotificationMiddleware](#createnotificationmiddleware)\r\n  - [Per-endpoint control](#per-endpoint-control)\r\n  - [WebSocket lifecycle notifications](#websocket-lifecycle-notifications)\r\n  - [Custom messages](#custom-messages)\r\n- [Parallel queries](#parallel-queries)\r\n  - [useParallelQueries](#useparallelqueries)\r\n  - [useQueries](#usequeries)\r\n- [Reconnect options](#reconnect-options)\r\n- [TypeScript generics reference](#typescript-generics-reference)\r\n- [Utilities](#utilities)\r\n  - [getBaseQueryWithAuthorization](#getbasequerywithauthorization)\r\n  - [configureBaseQueryAuth](#configurebasequerycauth)\r\n  - [createAuthBaseQuery](#createauthbasequery)\r\n\r\n---\r\n\r\n## Installation\r\n\r\nInstall the package and its peer dependencies:\r\n\r\n```bash\r\nnpm install @accreation/rtkx\r\n# peer deps\r\nnpm install @reduxjs/toolkit react react-redux\r\n```\r\n\r\n**Peer dependency requirements** enforced at install time:\r\n\r\n| Package | Required version |\r\n|---|---|\r\n| `@reduxjs/toolkit` | `^2.3.0` |\r\n| `react` | `^17.0.0 \\|\\| ^18.0.0` |\r\n| `react-redux` | `^9.0.0` |\r\n\r\n---\r\n\r\n## Single import source\r\n\r\nrtkx re-exports everything from `@reduxjs/toolkit/query/react`, `@reduxjs/toolkit`, and `react-redux` so your project never needs to import from those packages directly. This eliminates version mismatches - npm resolves a single copy by following rtkx's peer dependency constraints.\r\n\r\n```ts\r\n// Everything from one place\r\nimport {\r\n  // RTK Query\r\n  createApi, fetchBaseQuery, skipToken,\r\n  // Redux\r\n  configureStore,\r\n  // React-Redux\r\n  Provider, useSelector, useDispatch,\r\n  // rtkx extensions\r\n  createWsEndpoint, createStreamingEndpoint,\r\n  useWebSocketSend, createWsEndpointRef,\r\n  createQueryChain, useQueryChain,\r\n  createMutationChain, useMutationChain,\r\n  createNotificationMiddleware,\r\n  useParallelQueries, useQueries,\r\n  getBaseQueryWithAuthorization, configureBaseQueryAuth,\r\n} from '@accreation/rtkx';\r\n```\r\n\r\nNothing is bundled - these are pure pass-throughs. `@reduxjs/toolkit` and `react-redux` remain external (peer deps) and are never duplicated in your bundle.\r\n\r\n---\r\n\r\n## WebSocket endpoints\r\n\r\n### `createWsEndpoint` - pure WebSocket\r\n\r\nUse when all data arrives over a socket with no initial HTTP request.\r\n\r\n```ts\r\n// store/api/chat.ts\r\nimport { createApi, fetchBaseQuery, createWsEndpoint } from 'rtkx';\r\n\r\ninterface ChatMessage {\r\n  id: string;\r\n  author: string;\r\n  text: string;\r\n  timestamp: number;\r\n}\r\n\r\nexport const chatApi = createApi({\r\n  reducerPath: 'chatApi',\r\n  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),\r\n  endpoints: (builder) => ({\r\n\r\n    chatMessages: createWsEndpoint<\r\n      ChatMessage[],  // TCache   - what's stored in the RTK Query cache\r\n      ChatMessage,    // TMessage - what each WS frame delivers\r\n      string          // TArg    - hook argument (room name)\r\n    >(builder, {\r\n      url: 'wss://example.com/chat',\r\n\r\n      // Optional: seed the cache before the first message arrives\r\n      initialData: [],\r\n\r\n      // Transform raw MessageEvent into TMessage.\r\n      // Return null to silently skip a frame (useful for filtering by event name).\r\n      transformMessage: (event) => JSON.parse(event.data),\r\n\r\n      // Mutate the Immer draft to update the cache\r\n      onMessage: (msg, { updateCachedData }) => {\r\n        updateCachedData((draft) => {\r\n          draft.push(msg);\r\n          if (draft.length > 100) draft.shift(); // keep rolling window\r\n        });\r\n      },\r\n\r\n      reconnect: { maxAttempts: 10, delay: 500 },\r\n\r\n      // Tie this endpoint into createNotificationMiddleware so WS lifecycle\r\n      // events (error / connect / disconnect) fire through the same handler.\r\n      // Use the same string you use as the object key in createApi.\r\n      endpointName: 'chatMessages',\r\n\r\n      // Tag the cache entry so other mutations can invalidate it.\r\n      providesTags: (result, _err, room) => [{ type: 'ChatRoom', id: room }],\r\n    }),\r\n\r\n  }),\r\n});\r\n\r\nexport const { useChatMessagesQuery } = chatApi;\r\n```\r\n\r\n```tsx\r\nfunction ChatRoom({ room }: { room: string }) {\r\n  const { data: messages = [] } = useChatMessagesQuery(room);\r\n  return (\r\n    <ul>\r\n      {messages.map((m) => (\r\n        <li key={m.id}><strong>{m.author}</strong>: {m.text}</li>\r\n      ))}\r\n    </ul>\r\n  );\r\n}\r\n```\r\n\r\n#### Filtering frames by event name\r\n\r\nWhen a single socket carries multiple event types (e.g. a `{ event, data }` envelope), return `null` from `transformMessage` to skip non-matching frames:\r\n\r\n```ts\r\nfunction parseEvent<T>(eventName: string) {\r\n  return (raw: MessageEvent): T | null => {\r\n    try {\r\n      const msg = JSON.parse(raw.data) as { event: string; data: T };\r\n      return msg.event === eventName ? msg.data : null; // null = skip\r\n    } catch { return null; }\r\n  };\r\n}\r\n\r\n// Each endpoint only processes its own event type\r\nchatMessages: createWsEndpoint(builder, { url: 'ws://...', transformMessage: parseEvent('chat-message') }),\r\nuserPresence: createWsEndpoint(builder, { url: 'ws://...', transformMessage: parseEvent('presence') }),\r\n```\r\n\r\n---\r\n\r\n### `createStreamingEndpoint` - HTTP + WebSocket\r\n\r\nUse when the initial data comes from REST and subsequent updates stream in over a socket.\r\n\r\n```ts\r\ninterface Todo { id: string; title: string; completed: boolean; }\r\n\r\ntodoList: createStreamingEndpoint<\r\n  Todo[],   // TCache   - stored in cache (shape after HTTP + WS updates)\r\n  Todo[],   // TMessage - shape of each WS frame\r\n  void      // TArg\r\n>(builder, {\r\n  // HTTP: seed cache on mount\r\n  query: () => '/todos',\r\n\r\n  // WebSocket: stream live updates\r\n  url: 'wss://example.com/todos/live',\r\n\r\n  // Replace cache with the latest snapshot on each push\r\n  onMessage: (updated, { updateCachedData }) => {\r\n    updateCachedData((draft) => {\r\n      draft.splice(0, draft.length, ...updated);\r\n    });\r\n  },\r\n\r\n  reconnect: true, // enable with defaults\r\n\r\n  // Middleware integration & tag support\r\n  endpointName: 'todoList',\r\n  providesTags: ['Todo'],\r\n}),\r\n```\r\n\r\n---\r\n\r\n### `WebSocketManager` - standalone\r\n\r\nFor cases where you need a WebSocket connection outside of RTK Query (e.g. presence tracking, custom protocols):\r\n\r\n```ts\r\nimport { WebSocketManager } from 'rtkx';\r\n\r\nconst ws = new WebSocketManager<{ type: string; payload: unknown }>(\r\n  'wss://example.com/notifications',\r\n  { reconnect: { maxAttempts: 5 } },\r\n);\r\n\r\nconst unsubscribe = ws.subscribe((msg) => console.log(msg));\r\n\r\nws.connect();\r\nws.send({ type: 'subscribe', payload: { channel: 'team-updates' } });\r\n\r\n// Clean up\r\nunsubscribe();\r\nws.disconnect();\r\n```\r\n\r\n> **Note:** `createWsEndpoint` and `createStreamingEndpoint` use `WebSocketManager` internally - you only need this for lower-level scenarios.\r\n\r\n---\r\n\r\n### `useWebSocketSend` - sending messages\r\n\r\n`useWebSocketSend` returns a stable `send` callback that writes to the **already-open** WebSocket connection that rtkx manages for a given endpoint + arg combination. This is the recommended way to send messages bidirectionally without managing the socket yourself.\r\n\r\n**Setup - three steps:**\r\n\r\n**1.** Create a ref once, outside `createApi`:\r\n\r\n```ts\r\n// store/api/chat.ts\r\nimport { createApi, createWsEndpoint, createWsEndpointRef } from 'rtkx';\r\n\r\nexport const chatMessagesRef = createWsEndpointRef<string>(); // TArg = room name\r\n```\r\n\r\n**2.** Pass the ref to the endpoint's `options.ref`:\r\n\r\n```ts\r\nexport const chatApi = createApi({\r\n  reducerPath: 'chatApi',\r\n  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),\r\n  endpoints: (builder) => ({\r\n    chatMessages: createWsEndpoint<ChatMessage[], ChatMessage, string>(builder, {\r\n      ref: chatMessagesRef,          // ← link the endpoint to the ref\r\n      url: 'wss://example.com/chat',\r\n      initialData: [],\r\n      transformMessage: parseEvent<ChatMessage>('chat-message'),\r\n      onMessage: (msg, { updateCachedData }) => {\r\n        updateCachedData((draft) => { draft.push(msg); });\r\n      },\r\n    }),\r\n  }),\r\n});\r\n```\r\n\r\n**3.** Use the ref in any component to send messages:\r\n\r\n```tsx\r\nimport { useWebSocketSend } from 'rtkx';\r\nimport { chatMessagesRef, useChatMessagesQuery } from '../store/api/chat';\r\n\r\ninterface OutgoingMessage {\r\n  event: string;\r\n  data: { author: string; text: string; room: string };\r\n}\r\n\r\nfunction ChatInput({ room, author }: { room: string; author: string }) {\r\n  // The query must be subscribed before send() is called - subscribe here\r\n  // or in a parent component.\r\n  useChatMessagesQuery(room);\r\n\r\n  const send = useWebSocketSend<OutgoingMessage, string>(chatMessagesRef, room);\r\n\r\n  const [text, setText] = useState('');\r\n\r\n  const handleSubmit = () => {\r\n    send({ event: 'chat-message', data: { author, text, room } });\r\n    setText('');\r\n  };\r\n\r\n  return (\r\n    <div>\r\n      <input value={text} onChange={(e) => setText(e.target.value)} />\r\n      <button onClick={handleSubmit}>Send</button>\r\n    </div>\r\n  );\r\n}\r\n```\r\n\r\n**How it works:**\r\n\r\n- `createWsEndpointRef<TArg>()` returns an opaque handle that starts unpopulated.\r\n- When `createApi` evaluates your endpoints callback, `createWsEndpoint` populates the ref with an internal endpoint ID synchronously.\r\n- rtkx maintains an internal registry mapping `endpointId + arg` to the live `WebSocketManager` instance. The manager is registered when the first subscriber mounts and unregistered when the last one unmounts.\r\n- `useWebSocketSend` looks up the manager from that registry and returns a `useCallback`-stable `send` function.\r\n\r\n> **Requirement:** The matching query (`useChatMessagesQuery(room)`) must be subscribed (mounted somewhere in the tree) before `send()` is called - rtkx opens the socket when the query subscribes and closes it when the last subscriber unmounts. If `send()` is called with no active subscription a warning is logged and the message is dropped.\r\n\r\n> **Serialisation:** Objects are JSON-serialised automatically. Pass a plain `string` to send a raw text frame.\r\n\r\n---\r\n\r\n### WebSocket tags — `providesTags` / `invalidatesTags`\r\n\r\nBoth `createWsEndpoint` and `createStreamingEndpoint` accept a `providesTags` option, identical to a regular `builder.query()` definition.\r\n\r\n```ts\r\nchatMessages: createWsEndpoint<ChatMessage[], ChatMessage, string>(builder, {\r\n  url: 'wss://example.com/chat',\r\n  onMessage: (msg, { updateCachedData }) => {\r\n    updateCachedData((draft) => { draft.push(msg); });\r\n  },\r\n  providesTags: (result, _err, room) => [{ type: 'ChatRoom', id: room }],\r\n}),\r\n```\r\n\r\n`useWebSocketSend` accepts an optional third argument with `api` and `invalidatesTags`. When provided, the tags are invalidated immediately after each successful `send()` call:\r\n\r\n```ts\r\nimport { useWebSocketSend } from 'rtkx';\r\nimport { chatMessagesRef, chatApi } from '../store/api/chat';\r\n\r\nfunction ChatInput({ room }: { room: string }) {\r\n  const send = useWebSocketSend<OutgoingMessage, string>(chatMessagesRef, room, {\r\n    api: chatApi,\r\n    invalidatesTags: [{ type: 'MessageCount', id: room }],\r\n  });\r\n\r\n  // Calling send() will automatically dispatch:\r\n  // chatApi.util.invalidateTags([{ type: 'MessageCount', id: room }])\r\n  return <button onClick={() => send({ event: 'ping' })}>Ping</button>;\r\n}\r\n```\r\n\r\n**`createWsEndpointRef` works with `createStreamingEndpoint` too:**\r\n\r\n```ts\r\nexport const livePostsRef = createWsEndpointRef<void>();\r\n\r\nlivePosts: createStreamingEndpoint<Post[], Post, void>(builder, {\r\n  ref: livePostsRef,\r\n  query: () => '/posts',\r\n  url: 'wss://example.com/posts/live',\r\n  onMessage: (post, { updateCachedData }) => {\r\n    updateCachedData((draft) => { draft.push(post); });\r\n  },\r\n}),\r\n```\r\n\r\n---\r\n\r\n## Chained queries\r\n\r\nWhen one endpoint's response contains the argument for the next request, `createQueryChain` + `useQueryChain` handle the sequencing cleanly - no nested `useEffect`, no manual loading gates.\r\n\r\nResults are keyed by the string name you give each step, so you get `data.getAuthor` instead of a positional index.\r\n\r\n> **Key uniqueness**: every step key within a single chain **must be unique**. Duplicate keys will silently overwrite the earlier step's result in the `data` object.\r\n\r\n### `createQueryChain`\r\n\r\nDefine the chain once, outside your component:\r\n\r\n```ts\r\n// store/chains.ts\r\nimport { createQueryChain } from 'rtkx';\r\nimport { api } from './api';\r\n\r\n// Imagine these endpoints exist in your api:\r\n//   getAuthor(authorId: string)         -> Author      { id, publicationId, name }\r\n//   getPublication(pubId: string)       -> Publication { id, name }\r\n//   getSubscribers(pubId: string)       -> Subscriber[]\r\n\r\nexport const publicationSubscribersChain = createQueryChain('getAuthor', api.endpoints.getAuthor)\r\n  .next('getPublication', api.endpoints.getPublication,\r\n    (author) => author.publicationId,    // step 2: derive arg from step 1 result\r\n  )\r\n  .next('getSubscribers', api.endpoints.getSubscribers,\r\n    (pub) => pub.id,                     // step 3: derive arg from step 2 result\r\n  )\r\n  .build();\r\n```\r\n\r\nEach `.next()` call's `deriveArg` receives two arguments:\r\n- `prev` - the **immediately previous** step's result\r\n- `all` - a **record of every completed step's result** keyed by name\r\n\r\nThis lets any step reach back to any earlier result:\r\n\r\n```ts\r\nexport const chain = createQueryChain('getAuthor', api.endpoints.getAuthor)\r\n  .next('getPublication', api.endpoints.getPublication, (author) => author.publicationId)\r\n  .next('getAnalytics', api.endpoints.getAnalytics,\r\n    //               prev = Publication     all.getAuthor = Author from step 1\r\n    (pub, all) => ({ pubId: pub.id, authorId: all.getAuthor.id }),\r\n  )\r\n  .build();\r\n```\r\n\r\n### `useQueryChain`\r\n\r\n```tsx\r\n// components/PublicationSubscribers.tsx\r\nimport { useQueryChain } from 'rtkx';\r\nimport { publicationSubscribersChain } from '../store/chains';\r\n\r\nfunction PublicationSubscribers({ authorId }: { authorId: string }) {\r\n  const {\r\n    data,         // { getAuthor: Author, getPublication: Publication, getSubscribers: Subscriber[] } | undefined\r\n    finalData,    // Subscriber[] - last step's result (shorthand)\r\n    isLoading,\r\n    isError,\r\n    error,\r\n    currentStep,  // 0-based index of the step currently running\r\n    totalSteps,   // 3\r\n    reset,        // re-run chain from scratch\r\n  } = useQueryChain(publicationSubscribersChain, authorId);\r\n\r\n  if (isLoading) return <p>Step {currentStep + 1} / {totalSteps}...</p>;\r\n  if (isError)   return <p>Failed: {String(error)}</p>;\r\n\r\n  const { getAuthor, getPublication, getSubscribers } = data!;\r\n\r\n  return (\r\n    <section>\r\n      <h2>{getAuthor.name} - {getPublication.name}</h2>\r\n      <ul>{getSubscribers.map((s) => <li key={s.id}>{s.email}</li>)}</ul>\r\n      <button onClick={reset}>Refresh</button>\r\n    </section>\r\n  );\r\n}\r\n```\r\n\r\n**Behaviours:**\r\n- Re-runs automatically when `authorId` changes (shallow equality check)\r\n- Pass `{ skip: true }` as the third argument to pause execution\r\n- Each step dispatches via RTK Query - results are cached and deduped normally\r\n\r\n---\r\n\r\n### `createMutationChain`\r\n\r\nLike `createQueryChain` but for **mutations** - executed imperatively via a trigger function returned by `useMutationChain`. Useful for multi-step write flows where each step's output feeds the next.\r\n\r\n> **Key uniqueness**: step keys must be unique within each chain definition.\r\n\r\n```ts\r\n// store/chains.ts\r\nimport { createMutationChain } from 'rtkx';\r\nimport { postsApi } from './api/posts';\r\n\r\nexport const publishPostChain = createMutationChain(\r\n  'createPost',\r\n  postsApi.endpoints.createPost,\r\n)\r\n  .next('uploadAssets', postsApi.endpoints.uploadAssets,\r\n    // prev = CreatePostResult (step 1 output)\r\n    (prev) => ({ postId: prev.postId, files: prev.pendingFiles }),\r\n  )\r\n  .next('publish', postsApi.endpoints.publishPost,\r\n    (prev) => ({ postId: prev.postId }),\r\n  )\r\n  .build();\r\n```\r\n\r\nThe same `all` cross-step access is available just as in `createQueryChain`:\r\n\r\n```ts\r\n.next('notifyFollowers', postsApi.endpoints.notifyFollowers,\r\n  (prev, all) => ({ postId: prev.postId, authorId: all.createPost.authorId }),\r\n)\r\n```\r\n\r\n---\r\n\r\n### `useMutationChain`\r\n\r\n```tsx\r\n// components/NewPost.tsx\r\nimport { useMutationChain } from 'rtkx';\r\nimport { publishPostChain } from '../store/chains';\r\n\r\nfunction NewPost() {\r\n  const [publish, {\r\n    isLoading,\r\n    isSuccess,\r\n    isError,\r\n    error,\r\n    finalData,    // PublishPostResult - last step's result\r\n    data,         // { createPost: CreatePostResult, uploadAssets: UploadResult, publish: PublishPostResult }\r\n    currentStep,  // 0-based index of the step currently executing\r\n    totalSteps,   // 3\r\n    reset,        // reset state back to idle\r\n  }] = useMutationChain(publishPostChain);\r\n\r\n  const handleClick = () => {\r\n    publish({ title: 'Hello World', body: '...', tags: ['intro'] });\r\n  };\r\n\r\n  if (isLoading) return <p>Step {currentStep + 1} / {totalSteps}…</p>;\r\n  if (isError)   return <p>Failed at step {currentStep + 1}: {String(error)}</p>;\r\n  if (isSuccess) return <p>✓ Published: {finalData?.url}</p>;\r\n\r\n  return <button onClick={handleClick}>Publish post</button>;\r\n}\r\n```\r\n\r\n**Behaviours:**\r\n- `execute(arg)` returns a `Promise` that resolves with the last step's result\r\n- Calling `execute()` again while a chain is running cancels the in-flight chain first\r\n- Steps are executed sequentially with `dispatch(endpoint.initiate(...))` - each step's result feeds the next via `deriveArg`\r\n- `reset()` clears all state back to idle without re-running\r\n\r\n---\r\n\r\n## Parallel queries\r\n\r\nBoth hooks are zero-config - no chain definition file, no builder. Just pass the endpoint(s) and args directly in the component.\r\n\r\n### `useParallelQueries`\r\n\r\nFires **one endpoint** against an **array of args** simultaneously and returns a single aggregated state. Perfect for loading a batch of items by ID when no server-side bulk endpoint exists.\r\n\r\nSubscriptions are managed incrementally: adding an ID subscribes a new query; removing an ID tears down that subscription immediately.\r\n\r\n```ts\r\nimport { useParallelQueries } from 'rtkx';\r\nimport { postsApi } from './store/api';\r\n\r\nfunction PostBatch({ postIds }: { postIds: string[] }) {\r\n  const {\r\n    data,       // (Post | undefined)[] - same order as postIds\r\n    isLoading,  // true while ANY query is still in its initial load\r\n    isFetching, // true while ANY query is re-fetching\r\n    isSuccess,  // true only when EVERY query has succeeded\r\n    isError,    // true when at least one query has errored\r\n    errors,     // (unknown | undefined)[] - per ID, same order as postIds\r\n    refetchAll, // re-fetches every active query immediately\r\n  } = useParallelQueries(\r\n    postsApi.endpoints.getPost,  // single endpoint\r\n    postIds,                      // string[] - one request per element\r\n    // optional shared options:\r\n    // { skip, pollingInterval, refetchOnMountOrArgChange, concurrent }\r\n  );\r\n\r\n  if (isLoading) return <p>Loading {postIds.length} posts…</p>;\r\n  if (isError)   return <p>Some requests failed</p>;\r\n  // isSuccess gates the whole view - data is complete when true\r\n  return (\r\n    <ul>\r\n      {data.map((post, i) =>\r\n        post ? <li key={postIds[i]}>{post.title}</li> : null,\r\n      )}\r\n    </ul>\r\n  );\r\n}\r\n```\r\n\r\n**Behaviours:**\r\n- `data` entries are `undefined` while their query is still loading.\r\n- `isSuccess` is `true` only when **every** query has resolved - use it to gate a combined view that should appear all at once.\r\n- An empty `postIds` array short-circuits with `isSuccess: true` immediately.\r\n- Dynamically responds if the array changes length between renders.\r\n\r\n**Concurrency limiting (`concurrent`):**\r\n\r\nBy default all requests fire simultaneously. Pass `concurrent` to cap how many can be in-flight at once. As each request settles (succeeds *or* errors), the next queued request starts automatically - sliding-window semantics.\r\n\r\n```ts\r\n// Fire at most 10 requests at a time; the remaining 90 queue up and\r\n// start as earlier requests complete.\r\nconst { data, isSuccess } = useParallelQueries(\r\n  postsApi.endpoints.getPost,\r\n  hundredPostIds,\r\n  { concurrent: 10 },\r\n);\r\n```\r\n\r\n---\r\n\r\n### `useQueries`\r\n\r\nFires **N different endpoints** in parallel. Each entry is labelled with a string key; results are returned as a named object so you destructure by key instead of accessing by index.\r\n\r\nThe key type-propagates through a mapped conditional type (`DataRecord<T>`), so each field of `data` carries the exact TypeScript type for its endpoint - no casting required.\r\n\r\n```ts\r\nimport { useQueries } from 'rtkx';\r\nimport { statsApi, postsApi, commentsApi } from './store/api';\r\n\r\nfunction Dashboard() {\r\n  const {\r\n    data,       // named record - each key typed to its endpoint's result\r\n    isLoading,\r\n    isSuccess,\r\n    isError,\r\n    errors,     // Partial<{ stats: unknown; posts: unknown; comments: unknown }>\r\n    refetchAll,\r\n  } = useQueries([\r\n    ['stats',    statsApi.endpoints.getSiteStats,    undefined  ],\r\n    ['posts',    postsApi.endpoints.getRecentPosts,  undefined  ],\r\n    ['comments', commentsApi.endpoints.getComments,  'latest'   ],\r\n  ] as const);\r\n\r\n  // Destructure by name - fully typed, no data[0] / data[1]\r\n  const { stats, posts, comments } = data;\r\n  //       ^ SiteStats | undefined\r\n  //               ^ Post[] | undefined\r\n  //                        ^ Comment[] | undefined\r\n\r\n  if (isLoading) return <p>Loading…</p>;\r\n  // isSuccess gates the entire view\r\n  if (!isSuccess) return null;\r\n\r\n  return (\r\n    <section>\r\n      <p>Posts published: {stats!.totalPosts} | Comments today: {stats!.commentsToday}</p>\r\n      <ul>{posts!.map((p) => <li key={p.id}>{p.title}</li>)}</ul>\r\n      <ul>{comments!.map((c) => <li key={c.id}>{c.author}: {c.text}</li>)}</ul>\r\n    </section>\r\n  );\r\n}\r\n```\r\n\r\nPer-entry options are passed as a **fourth tuple element**:\r\n\r\n```ts\r\nuseQueries([\r\n  ['author',  api.endpoints.getAuthor,  authorId, { skip: !authorId }               ],\r\n  ['posts',   api.endpoints.getPosts,   authorId, { pollingInterval: 30_000 }        ],\r\n  ['drafts',  api.endpoints.getDrafts,  'mine',   { refetchOnMountOrArgChange: true } ],\r\n] as const);\r\n```\r\n\r\n**Behaviours:**\r\n- `isLoading` - any non-skipped query is in its initial load.\r\n- `isSuccess` - all non-skipped queries have succeeded; skipped entries are treated as already settled.\r\n- `errors` is a partial named record matching the input keys.\r\n- Subscriptions are established on mount and torn down on unmount.\r\n\r\n| Entry position | Content |\r\n|---|---|\r\n| `[0]` | Unique string key - becomes the property name on `data` and `errors` |\r\n| `[1]` | RTK Query endpoint (`api.endpoints.<name>`) |\r\n| `[2]` | Arg passed to the endpoint (use `undefined` for void endpoints) |\r\n| `[3]` _(optional)_ | `{ skip?, pollingInterval?, refetchOnMountOrArgChange? }` |\r\n\r\n---\r\n\r\n## Notifications (opt-in)\r\n\r\n`createNotificationMiddleware` is completely independent - add it to `configureStore` only if you want it. It fires a typed callback on every RTK Query success or failure. **No UI library is included or required** - you plug in whatever toast system you use.\r\n\r\n### `createNotificationMiddleware`\r\n\r\n```ts\r\n// store/store.ts\r\nimport { configureStore, createNotificationMiddleware } from 'rtkx';\r\nimport { toast } from 'react-toastify'; // or notistack, sonner, anything\r\nimport { api } from './api';\r\n\r\nconst notifMiddleware = createNotificationMiddleware({\r\n  // Your toast library - or any callback you like\r\n  handler: (n) => {\r\n    // n is a fully-typed RtkxNotification:\r\n    // { type, message, endpointName, operationType, error?, isNetworkError, isAuthError }\r\n    toast(n.message, { type: n.type });                    // react-toastify\r\n    // enqueueSnackbar(n.message, { variant: n.type });    // notistack\r\n    // sonnerToast[n.type](n.message);                     // sonner\r\n  },\r\n\r\n  defaults: {\r\n    onError:   true,             // show errors for ALL endpoints (default: true)\r\n    onSuccess: 'mutations-only', // 'mutations-only' | true | false  (default)\r\n  },\r\n});\r\n\r\nexport const store = configureStore({\r\n  middleware: (getDefault) =>\r\n    getDefault().concat(api.middleware, notifMiddleware.middleware),\r\n});\r\n```\r\n\r\n> **How mutation detection works:** rtkx checks `action.meta.arg.type === 'mutation'` - RTK Query's own flag, not name heuristics.\r\n\r\n> **Network & auth errors always fire** (`FETCH_ERROR` and HTTP 401), bypassing all per-endpoint config.\r\n\r\n### Per-endpoint control\r\n\r\n```ts\r\nconst notifMiddleware = createNotificationMiddleware({\r\n  handler: (n) => toast(n.message, { type: n.type }),\r\n\r\n  endpoints: {\r\n    // Suppress errors from a noisy polling endpoint\r\n    syncComments: { onError: false },\r\n\r\n    // Custom success message for a specific mutation\r\n    createPost:   { onSuccess: 'Post published!' },\r\n\r\n    // Force-show success even if global onSuccess is false\r\n    deleteAccount: { onSuccess: true },\r\n\r\n    // Suppress WS connection errors for a chatty socket\r\n    getTimeline:  { onError: false },\r\n  },\r\n});\r\n```\r\n\r\n| Value for `onError` / `onSuccess` | Behaviour |\r\n|---|---|\r\n| `true` | Always show |\r\n| `false` | Always suppress |\r\n| `'some string'` | Always show with this custom message |\r\n| *(omitted)* | Follows `defaults` |\r\n\r\n---\r\n\r\n### WebSocket lifecycle notifications\r\n\r\nWhen you provide `endpointName` on a `createWsEndpoint` or `createStreamingEndpoint`, the endpoint dispatches Redux actions (`wsConnectedAction`, `wsDisconnectedAction`, `wsErrorAction`) on lifecycle events. `createNotificationMiddleware` intercepts these actions automatically — **no extra wiring needed**.\r\n\r\n```ts\r\n// 1. Add endpointName to the endpoint definition\r\nendpoints: (builder) => ({\r\n  getTimeline: createStreamingEndpoint(builder, {\r\n    endpointName: 'getTimeline',   // must match the object key\r\n    query: () => '/timeline',\r\n    url: 'wss://example.com/timeline/live',\r\n    onMessage: (update, { updateCachedData }) => {\r\n      updateCachedData((draft) => { draft.push(update); });\r\n    },\r\n  }),\r\n}),\r\n\r\n// 2. Configure notifications in one place — same config for HTTP and WS\r\nconst notifMiddleware = createNotificationMiddleware({\r\n  handler: (n) => toast(n.message, { type: n.type }),\r\n\r\n  // Per-endpoint override — applies to BOTH HTTP errors and WS errors\r\n  endpoints: {\r\n    getTimeline: { onError: false }, // silence WS connection errors\r\n  },\r\n\r\n  // Global WS lifecycle defaults\r\n  ws: {\r\n    onError:      true,   // default: true  — show WS errors globally\r\n    onConnect:    false,  // default: false\r\n    onDisconnect: 'Connection lost — reconnecting…', // custom message\r\n  },\r\n});\r\n```\r\n\r\n**WS event → notification mapping:**\r\n\r\n| Event | Default | Respects per-endpoint `onError`? |\r\n|---|---|---|\r\n| WS error | `ws.onError` (default `true`) | ✅ yes |\r\n| WS connected | `ws.onConnect` (default `false`) | — |\r\n| WS disconnected | `ws.onDisconnect` (default `false`) | — |\r\n\r\nThe action creators are also exported from the package if you need to dispatch them manually or write custom middleware:\r\n\r\n```ts\r\nimport { wsConnectedAction, wsDisconnectedAction, wsErrorAction } from 'rtkx';\r\n```\r\n\r\n### Custom messages\r\n\r\nOverride the default message generators for full control:\r\n\r\n```ts\r\nconst notifMiddleware = createNotificationMiddleware({\r\n  handler: (n) => toast(n.message, { type: n.type }),\r\n\r\n  messages: {\r\n    // Return empty string to fall back to the built-in default\r\n    error:     (endpointName, operationType, error) => {\r\n                 const msg = (error as any)?.data?.message;\r\n                 return msg ? `${operationType} failed: ${msg}` : '';\r\n               },\r\n    success:   (endpointName, operationType) => `${operationType} successful`,\r\n    network:   'Check your internet connection',\r\n    authError: 'Your session has expired - please log in again',\r\n    unknown:   'Something went wrong',\r\n  },\r\n});\r\n```\r\n\r\n### `RtkxNotification` type\r\n\r\n```ts\r\ninterface RtkxNotification {\r\n  type:           'success' | 'error' | 'warning' | 'info';\r\n  message:        string;\r\n  endpointName:   string;\r\n  operationType:  'create' | 'update' | 'delete' | 'fetch' | 'other';\r\n  error?:         unknown;   // raw RTK Query error payload\r\n  isNetworkError: boolean;\r\n  isAuthError:    boolean;\r\n}\r\n```\r\n\r\n---\r\n\r\n## Reconnect options\r\n\r\nApplies to `createWsEndpoint`, `createStreamingEndpoint`, and `WebSocketManager`.\r\n\r\n| Option | Type | Default | Description |\r\n|---|---|---|---|\r\n| `enabled` | `boolean` | `true` | Enable auto-reconnect |\r\n| `maxAttempts` | `number` | `5` | Give up after this many attempts |\r\n| `delay` | `number` | `1000` | Initial delay in ms |\r\n| `maxDelay` | `number` | `30000` | Delay is capped at this value |\r\n| `factor` | `number` | `2` | Multiply delay by this on each attempt (exponential back-off) |\r\n\r\n```ts\r\nreconnect: { maxAttempts: 10, delay: 500, maxDelay: 15_000, factor: 1.5 }\r\nreconnect: true   // enable with all defaults\r\nreconnect: false  // disable entirely\r\n```\r\n\r\n---\r\n\r\n## TypeScript generics reference\r\n\r\n| Function | Generics | Notes |\r\n|---|---|---|\r\n| `createWsEndpoint<TCache, TMessage, TArg>` | `TCache` - cache shape; `TMessage` - per-frame type; `TArg` - hook arg | `transformMessage` may return `TMessage \\| null`; `providesTags` mirrors `builder.query()` |\r\n| `createStreamingEndpoint<TCache, TMessage, TArg>` | same as above | `query` provides the HTTP seed; `providesTags` mirrors `builder.query()` |\r\n| `createWsEndpointRef<TArg>()` | `TArg` - query-arg type | Returns a `WsEndpointRef<TArg>`; pass to `options.ref` and `useWebSocketSend` |\r\n| `useWebSocketSend<TMessage, TArg>(ref, arg, opts?)` | `TMessage` - outgoing message type; `TArg` - inferred from ref | `opts.invalidatesTags` + `opts.api` dispatch tag invalidation after each send |\r\n| `createQueryChain(key, endpoint)` | `key` - unique step name; `endpoint` - first RTK Query endpoint | Returns a `QueryChainBuilder`; add steps with `.next(key, endpoint, deriveArg)` |\r\n| `.next(key, endpoint, deriveArg)` | all inferred from the endpoint | `key` must be unique within the chain; `deriveArg` receives `prev` result + full `all` record |\r\n| `createMutationChain(key, endpoint)` | same as `createQueryChain` | Same fluent API; executed imperatively via `useMutationChain`'s trigger |\r\n| `useMutationChain(chain)` | inferred from chain | Returns `[execute, state]` tuple |\r\n| `useParallelQueries(endpoint, args, opts?)` | inferred from `endpoint` | `data` is `(TResult \\| undefined)[]` in arg order; `errors` is `(unknown \\| undefined)[]`; `opts.concurrent` limits in-flight requests |\r\n| `useQueries(entries)` | inferred from the `as const` tuple | `data` is `DataRecord<T>` - each key typed to its endpoint's result; `errors` is a matching partial record |\r\n\r\n---\r\n\r\n## License\r\n\r\nMIT\r\n","readmeFilename":"README.md"}