tempest-react-sdk 0.61.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"dev-mode.cjs","names":[],"sources":["../../src/utils/dev-mode.ts"],"sourcesContent":["/**\n * {@link isDevBuild} stays internal, imported by path (`../utils/dev-mode`)\n * rather than through the `utils` barrel: re-exporting it would make a one-line\n * env read part of the package's public API, with the semver weight that\n * carries, for something no consumer asked for. Every dev-only diagnostic in\n * the SDK routes through it — `grep -rn \"dev-mode\" src/` for the current list,\n * which an enumeration written here would only drift away from.\n *\n * {@link setDevBuild} is public, because the environment the SDK cannot read is\n * the one it most needs to: a Vite app. See its doc for why.\n */\n\n/**\n * Whether the consuming app was built for development.\n *\n * Reads `process.env.NODE_ENV`, which every supported bundler (Vite, webpack,\n * Rspack, Parcel) replaces with a literal **while building the app**. That\n * timing is the whole point: `import.meta.env.DEV` looks equivalent and is not,\n * because Vite replaces it while building *this package*, so a published\n * artifact carries the constant `false` and every guard behind it becomes dead\n * code the app's own dev server can no longer switch on.\n *\n * The expression is written out in full, and the failure is caught rather than\n * guarded against. A `typeof process === \"undefined\"` check would read as the\n * careful version and quietly reintroduce the bug: bundlers substitute the\n * member expression `process.env.NODE_ENV` and nothing else, so in a browser —\n * where the identifier `process` does not existthe guard would return early\n * while the literal right after it had already been replaced with\n * `\"development\"`.\n *\n * Returns `false` when the read throws, which is the environment that defines\n * neither symbol: a raw service-worker context, a plain\n * `<script type=\"module\">`, a bundler substituting nothing. **A Vite app is in\n * that set**, `vite dev` included — Vite replaces neither `process` nor\n * `process.env.NODE_ENV` in the app it builds, so the read throws there like\n * anywhere else. Staying quiet is deliberate — a dev-only warning that cannot\n * prove it is in development is better silent than shouting in someone's\n * production console.\n *\n * {@link setDevBuild} is how that app says so, and it is checked first. The\n * automatic read stays exactly as written underneath, bare member expression\n * and all: a `typeof process === \"undefined\"` guard in front of it would return\n * early in a webpack build whose literal had already been substituted, which is\n * the one environment this currently gets right.\n *\n * @returns Whether development-only diagnostics should run.\n *\n * @example\n * if (isDevBuild()) console.warn(\"[my-app] this prop combination does nothing\");\n *\n * @tempest-limits empty-catch — the only thing the read can throw is the\n * environment answering \"not defined\", which is the return value, not an error\n * worth reporting. Logging it would print on every call in exactly the context\n * that has nowhere to print.\n */\nlet configuredDevBuild: boolean | undefined;\n\n/**\n * Tell the SDK whether the app around it was built for development.\n *\n * Call it once, at bootstrap, from a build the SDK cannot inspect:\n *\n * ```ts\n * import { setDevBuild } from \"tempest-react-sdk\";\n *\n * setDevBuild(import.meta.env.DEV);\n * ```\n *\n * **Why the SDK cannot work this out on its own in a Vite app.** The automatic\n * detection reads `process.env.NODE_ENV`, which webpack, Rspack and Parcel\n * replace with a literal while building the *app*. Vite replaces neither half:\n * `process` is not defined in a browser bundle, so the read throws and the\n * answer is `false` including under `vite dev`. Its own signal,\n * `import.meta.env.DEV`, cannot be used here either, because Vite would replace\n * it while building *this package* and the published artifact would ship the\n * constant. Only the app is compiled at the moment the answer is knowable, so\n * only the app can supply it.\n *\n * The default stays `false` on purpose. `parseResponse` puts the raw response\n * payload in its message when this is on, so a wrong guess in the other\n * direction leaks a payload into a production error string. Silence is the safe\n * default; the report is one line away for anyone who wants it.\n *\n * Passing `undefined` clears the override and returns to automatic detection,\n * which is what a test that set it should do on the way out.\n *\n * @param value - `true` for a development build, `false` for production,\n * `undefined` to go back to detecting it.\n *\n * @example\n * // Vite the case this exists for\n * setDevBuild(import.meta.env.DEV);\n *\n * @example\n * // A test that flips it, and puts it back\n * afterEach(() => setDevBuild(undefined));\n */\nexport function setDevBuild(value: boolean | undefined): void {\n configuredDevBuild = value;\n}\n\nexport function isDevBuild(): boolean {\n if (configuredDevBuild !== undefined) return configuredDevBuild;\n try {\n return process.env.NODE_ENV !== \"production\";\n } catch {\n return false;\n }\n}\n"],"mappings":"AAuDA,IAAI,EA0CJ,SAAgB,EAAY,EAAkC,CAC1D,EAAqB,CACzB,CAEA,SAAgB,GAAsB,CAClC,GAAI,IAAuB,IAAA,GAAW,OAAO,EAC7C,GAAI,CACA,OAAA,QAAA,IAAA,WAAgC,YACpC,MAAQ,CACJ,MAAO,EACX,CACJ"}
1
+ {"version":3,"file":"dev-mode.cjs","names":[],"sources":["../../src/utils/dev-mode.ts"],"sourcesContent":["/**\n * {@link isDevBuild} stays internal, imported by path (`../utils/dev-mode`)\n * rather than through the `utils` barrel: re-exporting it would make a one-line\n * env read part of the package's public API, with the semver weight that\n * carries, for something no consumer asked for. Every dev-only diagnostic in\n * the SDK routes through it — `grep -rn \"dev-mode\" src/` for the current list,\n * which an enumeration written here would only drift away from.\n *\n * {@link setDevBuild} is public, because a context nothing compiles cannot be\n * detected from the inside. See its doc for when that is.\n */\n\n/**\n * Whether the consuming app was built for development.\n *\n * Reads `process.env.NODE_ENV`, which every supported bundler replaces with a\n * literal **while building the app** — Vite included. That last word is a\n * correction: this doc used to claim Vite substitutes neither half, so every\n * dev-only diagnostic in the SDK was unreachable under `vite dev`. Measured on\n * 2026-09-07 with 0.61.0 installed into a probe app, both from a packed tarball\n * (a real copy under `node_modules`, which Vite pre-bundles) and from a `file:`\n * link (a symlink it does not), reading back the module the dev server actually\n * served:\n *\n * | Vite | `vite dev` | `vite build` |\n * | --- | --- | --- |\n * | 5.4.21, 6.4.3, 7.3.6 | folded to `true` | folded to `false` |\n * | 8.2.2 | `\"development\" !== \"production\"` | folded to `false` |\n *\n * So this answers correctly in a Vite app on its own, in development and in\n * production, pre-bundled or served through the dev server's transform. The\n * wrong belief survived releases because nobody read the served module — the\n * expression is not substituted in a *browser console*, which is where it is\n * natural to go looking.\n *\n * `import.meta.env.DEV` still cannot be used here: Vite would replace it while\n * building *this package*, so the published artifact would carry the constant\n * and every guard behind it would be dead code no app could switch back on.\n *\n * The expression is written out in full, and the failure is caught rather than\n * guarded against. A `typeof process === \"undefined\"` check would read as the\n * careful version and quietly break the environments that work: substitution\n * replaces the member expression `process.env.NODE_ENV` and nothing else, so\n * the guard would return early in front of a literal that had already been\n * swapped in. The identifier itself never exists at runtime `typeof process`\n * is `\"undefined\"` in the page, measured in the same probe which is exactly\n * why the read is wrapped in `try` instead.\n *\n * Returns `false` when the read throws, which is the context nothing\n * transformed: a raw service-worker script, a plain `<script type=\"module\">`, a\n * bundler substituting nothing. Staying quiet there is deliberate — a dev-only\n * warning that cannot prove it is in development is better silent than shouting\n * in someone's production console.\n *\n * {@link setDevBuild} overrides all of it and is checked first.\n *\n * @returns Whether development-only diagnostics should run.\n *\n * @example\n * if (isDevBuild()) console.warn(\"[my-app] this prop combination does nothing\");\n *\n * @tempest-limits empty-catch — the only thing the read can throw is the\n * environment answering \"not defined\", which is the return value, not an error\n * worth reporting. Logging it would print on every call in exactly the context\n * that has nowhere to print.\n */\nlet configuredDevBuild: boolean | undefined;\n\n/**\n * Tell the SDK whether the app around it was built for development.\n *\n * Call it once, at bootstrap, from a context {@link isDevBuild} cannot read:\n *\n * ```ts\n * import { setDevBuild } from \"tempest-react-sdk\";\n *\n * setDevBuild(import.meta.env.DEV);\n * ```\n *\n * **When you need it.** Not for an ordinary Vite, webpack, Rspack or Parcel\n * app: all of them substitute `process.env.NODE_ENV` while building the app, so\n * the automatic read already answers correctly there — measured for Vite 5\n * through 8 in {@link isDevBuild}, whose doc carries the table. What is left is\n * the context nothing compiles or nothing configures:\n *\n * - code no bundler transformed a raw service-worker script registered as a\n * file of its own, a plain `<script type=\"module\">`;\n * - a staging or QA build that never sets `NODE_ENV=production`, where the\n * automatic answer is `true` and `parseResponse` would put the raw response\n * payload in an error string seen by real users. `setDevBuild(false)` closes\n * that;\n * - a test that wants the other branch, and puts it back on the way out.\n *\n * `import.meta.env.DEV` cannot be read by the SDK on your behalf, which is why\n * the signal is a parameter: Vite would replace it while building *this\n * package*, and the published artifact would ship the constant.\n *\n * The default stays `false` when the read throws. `parseResponse` puts the raw\n * response payload in its message when this is on, so guessing `true` in a\n * context that cannot prove it leaks a payload into a production error string.\n * Silence is the safe default; the report is one line away for anyone who wants\n * it.\n *\n * Passing `undefined` clears the override and returns to automatic detection,\n * which is what a test that set it should do on the way out.\n *\n * @param value - `true` for a development build, `false` for production,\n * `undefined` to go back to detecting it.\n *\n * @example\n * // A service worker, or any context no bundler transformed\n * setDevBuild(false);\n *\n * @example\n * // A test that flips it, and puts it back\n * afterEach(() => setDevBuild(undefined));\n */\nexport function setDevBuild(value: boolean | undefined): void {\n configuredDevBuild = value;\n}\n\nexport function isDevBuild(): boolean {\n if (configuredDevBuild !== undefined) return configuredDevBuild;\n try {\n return process.env.NODE_ENV !== \"production\";\n } catch {\n return false;\n }\n}\n"],"mappings":"AAkEA,IAAI,EAmDJ,SAAgB,EAAY,EAAkC,CAC1D,EAAqB,CACzB,CAEA,SAAgB,GAAsB,CAClC,GAAI,IAAuB,IAAA,GAAW,OAAO,EAC7C,GAAI,CACA,OAAA,QAAA,IAAA,WAAgC,YACpC,MAAQ,CACJ,MAAO,EACX,CACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"dev-mode.js","names":[],"sources":["../../src/utils/dev-mode.ts"],"sourcesContent":["/**\n * {@link isDevBuild} stays internal, imported by path (`../utils/dev-mode`)\n * rather than through the `utils` barrel: re-exporting it would make a one-line\n * env read part of the package's public API, with the semver weight that\n * carries, for something no consumer asked for. Every dev-only diagnostic in\n * the SDK routes through it — `grep -rn \"dev-mode\" src/` for the current list,\n * which an enumeration written here would only drift away from.\n *\n * {@link setDevBuild} is public, because the environment the SDK cannot read is\n * the one it most needs to: a Vite app. See its doc for why.\n */\n\n/**\n * Whether the consuming app was built for development.\n *\n * Reads `process.env.NODE_ENV`, which every supported bundler (Vite, webpack,\n * Rspack, Parcel) replaces with a literal **while building the app**. That\n * timing is the whole point: `import.meta.env.DEV` looks equivalent and is not,\n * because Vite replaces it while building *this package*, so a published\n * artifact carries the constant `false` and every guard behind it becomes dead\n * code the app's own dev server can no longer switch on.\n *\n * The expression is written out in full, and the failure is caught rather than\n * guarded against. A `typeof process === \"undefined\"` check would read as the\n * careful version and quietly reintroduce the bug: bundlers substitute the\n * member expression `process.env.NODE_ENV` and nothing else, so in a browser —\n * where the identifier `process` does not existthe guard would return early\n * while the literal right after it had already been replaced with\n * `\"development\"`.\n *\n * Returns `false` when the read throws, which is the environment that defines\n * neither symbol: a raw service-worker context, a plain\n * `<script type=\"module\">`, a bundler substituting nothing. **A Vite app is in\n * that set**, `vite dev` included — Vite replaces neither `process` nor\n * `process.env.NODE_ENV` in the app it builds, so the read throws there like\n * anywhere else. Staying quiet is deliberate — a dev-only warning that cannot\n * prove it is in development is better silent than shouting in someone's\n * production console.\n *\n * {@link setDevBuild} is how that app says so, and it is checked first. The\n * automatic read stays exactly as written underneath, bare member expression\n * and all: a `typeof process === \"undefined\"` guard in front of it would return\n * early in a webpack build whose literal had already been substituted, which is\n * the one environment this currently gets right.\n *\n * @returns Whether development-only diagnostics should run.\n *\n * @example\n * if (isDevBuild()) console.warn(\"[my-app] this prop combination does nothing\");\n *\n * @tempest-limits empty-catch — the only thing the read can throw is the\n * environment answering \"not defined\", which is the return value, not an error\n * worth reporting. Logging it would print on every call in exactly the context\n * that has nowhere to print.\n */\nlet configuredDevBuild: boolean | undefined;\n\n/**\n * Tell the SDK whether the app around it was built for development.\n *\n * Call it once, at bootstrap, from a build the SDK cannot inspect:\n *\n * ```ts\n * import { setDevBuild } from \"tempest-react-sdk\";\n *\n * setDevBuild(import.meta.env.DEV);\n * ```\n *\n * **Why the SDK cannot work this out on its own in a Vite app.** The automatic\n * detection reads `process.env.NODE_ENV`, which webpack, Rspack and Parcel\n * replace with a literal while building the *app*. Vite replaces neither half:\n * `process` is not defined in a browser bundle, so the read throws and the\n * answer is `false` including under `vite dev`. Its own signal,\n * `import.meta.env.DEV`, cannot be used here either, because Vite would replace\n * it while building *this package* and the published artifact would ship the\n * constant. Only the app is compiled at the moment the answer is knowable, so\n * only the app can supply it.\n *\n * The default stays `false` on purpose. `parseResponse` puts the raw response\n * payload in its message when this is on, so a wrong guess in the other\n * direction leaks a payload into a production error string. Silence is the safe\n * default; the report is one line away for anyone who wants it.\n *\n * Passing `undefined` clears the override and returns to automatic detection,\n * which is what a test that set it should do on the way out.\n *\n * @param value - `true` for a development build, `false` for production,\n * `undefined` to go back to detecting it.\n *\n * @example\n * // Vite the case this exists for\n * setDevBuild(import.meta.env.DEV);\n *\n * @example\n * // A test that flips it, and puts it back\n * afterEach(() => setDevBuild(undefined));\n */\nexport function setDevBuild(value: boolean | undefined): void {\n configuredDevBuild = value;\n}\n\nexport function isDevBuild(): boolean {\n if (configuredDevBuild !== undefined) return configuredDevBuild;\n try {\n return process.env.NODE_ENV !== \"production\";\n } catch {\n return false;\n }\n}\n"],"mappings":";AAuDA,IAAI;AA0CJ,SAAgB,EAAY,GAAkC;CAC1D,IAAqB;AACzB;AAEA,SAAgB,IAAsB;CAClC,IAAI,MAAuB,KAAA,GAAW,OAAO;CAC7C,IAAI;EACA,OAAA,QAAA,IAAA,aAAgC;CACpC,QAAQ;EACJ,OAAO;CACX;AACJ"}
1
+ {"version":3,"file":"dev-mode.js","names":[],"sources":["../../src/utils/dev-mode.ts"],"sourcesContent":["/**\n * {@link isDevBuild} stays internal, imported by path (`../utils/dev-mode`)\n * rather than through the `utils` barrel: re-exporting it would make a one-line\n * env read part of the package's public API, with the semver weight that\n * carries, for something no consumer asked for. Every dev-only diagnostic in\n * the SDK routes through it — `grep -rn \"dev-mode\" src/` for the current list,\n * which an enumeration written here would only drift away from.\n *\n * {@link setDevBuild} is public, because a context nothing compiles cannot be\n * detected from the inside. See its doc for when that is.\n */\n\n/**\n * Whether the consuming app was built for development.\n *\n * Reads `process.env.NODE_ENV`, which every supported bundler replaces with a\n * literal **while building the app** — Vite included. That last word is a\n * correction: this doc used to claim Vite substitutes neither half, so every\n * dev-only diagnostic in the SDK was unreachable under `vite dev`. Measured on\n * 2026-09-07 with 0.61.0 installed into a probe app, both from a packed tarball\n * (a real copy under `node_modules`, which Vite pre-bundles) and from a `file:`\n * link (a symlink it does not), reading back the module the dev server actually\n * served:\n *\n * | Vite | `vite dev` | `vite build` |\n * | --- | --- | --- |\n * | 5.4.21, 6.4.3, 7.3.6 | folded to `true` | folded to `false` |\n * | 8.2.2 | `\"development\" !== \"production\"` | folded to `false` |\n *\n * So this answers correctly in a Vite app on its own, in development and in\n * production, pre-bundled or served through the dev server's transform. The\n * wrong belief survived releases because nobody read the served module — the\n * expression is not substituted in a *browser console*, which is where it is\n * natural to go looking.\n *\n * `import.meta.env.DEV` still cannot be used here: Vite would replace it while\n * building *this package*, so the published artifact would carry the constant\n * and every guard behind it would be dead code no app could switch back on.\n *\n * The expression is written out in full, and the failure is caught rather than\n * guarded against. A `typeof process === \"undefined\"` check would read as the\n * careful version and quietly break the environments that work: substitution\n * replaces the member expression `process.env.NODE_ENV` and nothing else, so\n * the guard would return early in front of a literal that had already been\n * swapped in. The identifier itself never exists at runtime `typeof process`\n * is `\"undefined\"` in the page, measured in the same probe which is exactly\n * why the read is wrapped in `try` instead.\n *\n * Returns `false` when the read throws, which is the context nothing\n * transformed: a raw service-worker script, a plain `<script type=\"module\">`, a\n * bundler substituting nothing. Staying quiet there is deliberate — a dev-only\n * warning that cannot prove it is in development is better silent than shouting\n * in someone's production console.\n *\n * {@link setDevBuild} overrides all of it and is checked first.\n *\n * @returns Whether development-only diagnostics should run.\n *\n * @example\n * if (isDevBuild()) console.warn(\"[my-app] this prop combination does nothing\");\n *\n * @tempest-limits empty-catch — the only thing the read can throw is the\n * environment answering \"not defined\", which is the return value, not an error\n * worth reporting. Logging it would print on every call in exactly the context\n * that has nowhere to print.\n */\nlet configuredDevBuild: boolean | undefined;\n\n/**\n * Tell the SDK whether the app around it was built for development.\n *\n * Call it once, at bootstrap, from a context {@link isDevBuild} cannot read:\n *\n * ```ts\n * import { setDevBuild } from \"tempest-react-sdk\";\n *\n * setDevBuild(import.meta.env.DEV);\n * ```\n *\n * **When you need it.** Not for an ordinary Vite, webpack, Rspack or Parcel\n * app: all of them substitute `process.env.NODE_ENV` while building the app, so\n * the automatic read already answers correctly there — measured for Vite 5\n * through 8 in {@link isDevBuild}, whose doc carries the table. What is left is\n * the context nothing compiles or nothing configures:\n *\n * - code no bundler transformed a raw service-worker script registered as a\n * file of its own, a plain `<script type=\"module\">`;\n * - a staging or QA build that never sets `NODE_ENV=production`, where the\n * automatic answer is `true` and `parseResponse` would put the raw response\n * payload in an error string seen by real users. `setDevBuild(false)` closes\n * that;\n * - a test that wants the other branch, and puts it back on the way out.\n *\n * `import.meta.env.DEV` cannot be read by the SDK on your behalf, which is why\n * the signal is a parameter: Vite would replace it while building *this\n * package*, and the published artifact would ship the constant.\n *\n * The default stays `false` when the read throws. `parseResponse` puts the raw\n * response payload in its message when this is on, so guessing `true` in a\n * context that cannot prove it leaks a payload into a production error string.\n * Silence is the safe default; the report is one line away for anyone who wants\n * it.\n *\n * Passing `undefined` clears the override and returns to automatic detection,\n * which is what a test that set it should do on the way out.\n *\n * @param value - `true` for a development build, `false` for production,\n * `undefined` to go back to detecting it.\n *\n * @example\n * // A service worker, or any context no bundler transformed\n * setDevBuild(false);\n *\n * @example\n * // A test that flips it, and puts it back\n * afterEach(() => setDevBuild(undefined));\n */\nexport function setDevBuild(value: boolean | undefined): void {\n configuredDevBuild = value;\n}\n\nexport function isDevBuild(): boolean {\n if (configuredDevBuild !== undefined) return configuredDevBuild;\n try {\n return process.env.NODE_ENV !== \"production\";\n } catch {\n return false;\n }\n}\n"],"mappings":";AAkEA,IAAI;AAmDJ,SAAgB,EAAY,GAAkC;CAC1D,IAAqB;AACzB;AAEA,SAAgB,IAAsB;CAClC,IAAI,MAAuB,KAAA,GAAW,OAAO;CAC7C,IAAI;EACA,OAAA,QAAA,IAAA,aAAgC;CACpC,QAAQ;EACJ,OAAO;CACX;AACJ"}
@@ -1,2 +1,2 @@
1
- const e=require("./dev-mode.cjs");var t=new Set;function n(n){e.isDevBuild()&&!t.has(n)&&(t.add(n),console.warn(`[tempest-react-sdk] ${n}: a frame was not valid JSON, so the raw string is being delivered as if it were your message type. Pass \`parser\` to decode it, or \`onParseError\` to drop it and handle the failure. This warning appears once.`))}function r(e,t,r,i){if(t)return{delivered:!0,data:t(e)};try{return{delivered:!0,data:JSON.parse(e)}}catch(t){return r?(r(t,e),{delivered:!1,data:void 0}):(n(i),{delivered:!0,data:e})}}exports.decodeFrame=r;
1
+ const e=require("./dev-mode.cjs"),t=require("./schema-like.cjs");var n=new Set;function r(t){e.isDevBuild()&&!n.has(t)&&(n.add(t),console.warn(`[tempest-react-sdk] ${t}: a frame was not valid JSON, so the raw string is being delivered as if it were your message type. Pass \`parser\` to decode it, or \`onParseError\` to drop it and handle the failure. This warning appears once.`))}function i(t,r){let i=`${t}:schema`;if(!e.isDevBuild()||n.has(i))return;n.add(i);let a=r.map(e=>`${e.path}: ${e.message}`).join(`; `);console.warn(`[tempest-react-sdk] ${t}: a frame did not match \`schema\` and was dropped (${a}). Pass \`onValidationError\` to handle it yourself. This warning appears once.`)}function a(e,n,a){let{parser:o,onParseError:s,schema:c,onValidationError:l}=a;function u(r){if(!c)return{delivered:!0,data:r};let a=t.validateWithSchema(c,r);return a.ok?{delivered:!0,data:a.data}:(l?l(a.issues,e):i(n,a.issues),{delivered:!1,data:void 0})}if(o)return u(o(e));let d;try{d=JSON.parse(e)}catch(t){return s?(s(t,e),{delivered:!1,data:void 0}):c?u(e):(r(n),{delivered:!0,data:e})}return u(d)}exports.decodeFrame=a;
2
2
  //# sourceMappingURL=json-frame.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"json-frame.cjs","names":[],"sources":["../../src/utils/json-frame.ts"],"sourcesContent":["import { isDevBuild } from \"./dev-mode\";\n\n/**\n * The one decoder behind `createWebSocket`, `useWebSocket` and\n * `createEventStream`.\n *\n * Internal, and imported by path rather than through the `utils` barrel: it\n * exists so the three transports share one answer to \"the frame is not JSON\",\n * not so consumers can call it.\n *\n * That question used to have three identical copies of the same wrong answer —\n * `return raw as unknown as T`, which hands the consumer a `string` announced as\n * `T`. The failure never surfaced at the parse; it surfaced later, on the first\n * `message.id`, with nothing left to say the frame had not been JSON.\n */\n\n/** Outcome of decoding one frame. */\nexport interface DecodedFrame<T> {\n /** Whether the message should reach `onMessage`. */\n delivered: boolean;\n /** The decoded payload. Only meaningful when `delivered` is `true`. */\n data: T;\n}\n\nconst warned = new Set<string>();\n\n/**\n * Warn once per transport that a frame arrived which was not JSON.\n *\n * Once, because a stream sending text frames sends many, and a console line per\n * frame buries the one that mattered. Development builds only.\n *\n * @param transport - Label used in the message, e.g. `\"createWebSocket\"`.\n * @returns Nothing.\n */\nfunction warnOnce(transport: string): void {\n if (!isDevBuild() || warned.has(transport)) return;\n warned.add(transport);\n console.warn(\n `[tempest-react-sdk] ${transport}: a frame was not valid JSON, so the raw string is ` +\n `being delivered as if it were your message type. Pass \\`parser\\` to decode it, or ` +\n `\\`onParseError\\` to drop it and handle the failure. This warning appears once.`,\n );\n}\n\n/**\n * Decode one frame, reporting whether it should be delivered.\n *\n * A caller-supplied `parser` owns the frame completely and its result is always\n * delivered — decoding text, binary-as-base64 or a protocol of its own is the\n * point of that option.\n *\n * Without one, the frame is parsed as JSON. When that throws:\n *\n * - with `onParseError`, the callback fires and the frame is **not** delivered,\n * because a consumer that asked to hear about failures did not ask to also\n * receive the broken frame;\n * - without it, the raw string is delivered as `T` — the behaviour every version\n * before this one had, kept so nothing breaks — and development builds warn\n * once that it happened.\n *\n * @param raw - The frame body as text.\n * @param parser - Caller-supplied decoder, if any.\n * @param onParseError - Caller-supplied failure handler, if any.\n * @param transport - Label used in the development warning.\n * @returns Whether to deliver, and the payload.\n */\nexport function decodeFrame<T>(\n raw: string,\n parser: ((raw: string) => T) | undefined,\n onParseError: ((error: unknown, raw: string) => void) | undefined,\n transport: string,\n): DecodedFrame<T> {\n if (parser) return { delivered: true, data: parser(raw) };\n try {\n return { delivered: true, data: JSON.parse(raw) as T };\n } catch (error) {\n if (onParseError) {\n onParseError(error, raw);\n return { delivered: false, data: undefined as T };\n }\n warnOnce(transport);\n return { delivered: true, data: raw as unknown as T };\n }\n}\n\n/**\n * Forget which transports have already warned.\n *\n * Exists for tests, which would otherwise see the first case swallow the\n * warning for every case after it.\n *\n * @returns Nothing.\n */\nexport function resetFrameWarnings(): void {\n warned.clear();\n}\n"],"mappings":"kCAwBA,IAAM,EAAS,IAAI,IAWnB,SAAS,EAAS,EAAyB,CAClC,EAAA,WAAW,GAAK,GAAO,IAAI,CAAS,IACzC,EAAO,IAAI,CAAS,EACpB,QAAQ,KACJ,uBAAuB,EAAU,oNAGrC,EACJ,CAwBA,SAAgB,EACZ,EACA,EACA,EACA,EACe,CACf,GAAI,EAAQ,MAAO,CAAE,UAAW,GAAM,KAAM,EAAO,CAAG,CAAE,EACxD,GAAI,CACA,MAAO,CAAE,UAAW,GAAM,KAAM,KAAK,MAAM,CAAG,CAAO,CACzD,OAAS,EAAO,CAMZ,OALI,GACA,EAAa,EAAO,CAAG,EAChB,CAAE,UAAW,GAAO,KAAM,IAAA,EAAe,IAEpD,EAAS,CAAS,EACX,CAAE,UAAW,GAAM,KAAM,CAAoB,EACxD,CACJ"}
1
+ {"version":3,"file":"json-frame.cjs","names":[],"sources":["../../src/utils/json-frame.ts"],"sourcesContent":["import { isDevBuild } from \"./dev-mode\";\nimport { validateWithSchema, type SchemaIssue, type SchemaLike } from \"./schema-like\";\n\n/**\n * The one decoder behind `createWebSocket`, `useWebSocket` and\n * `createEventStream`.\n *\n * Internal, and imported by path rather than through the `utils` barrel: it\n * exists so the three transports share one answer to \"the frame is not JSON\",\n * not so consumers can call it.\n *\n * That question used to have three identical copies of the same wrong answer —\n * `return raw as unknown as T`, which hands the consumer a `string` announced as\n * `T`. The failure never surfaced at the parse; it surfaced later, on the first\n * `message.id`, with nothing left to say the frame had not been JSON.\n */\n\n/** Outcome of decoding one frame. */\nexport interface DecodedFrame<T> {\n /** Whether the message should reach `onMessage`. */\n delivered: boolean;\n /** The decoded payload. Only meaningful when `delivered` is `true`. */\n data: T;\n}\n\n/** How one frame should be turned into `T`, and who hears about failures. */\nexport interface DecodeFrameOptions<T> {\n /** Caller-supplied decoder, which owns the frame completely. */\n parser?: (raw: string) => T;\n /** Caller-supplied handler for a frame that is not valid JSON. */\n onParseError?: (error: unknown, raw: string) => void;\n /** Caller-supplied schema the decoded payload must satisfy. */\n schema?: SchemaLike<T>;\n /** Caller-supplied handler for a payload the schema refused. */\n onValidationError?: (issues: SchemaIssue[], raw: string) => void;\n}\n\nconst warned = new Set<string>();\n\n/**\n * Warn once per transport that a frame arrived which was not JSON.\n *\n * Once, because a stream sending text frames sends many, and a console line per\n * frame buries the one that mattered. Development builds only.\n *\n * @param transport - Label used in the message, e.g. `\"createWebSocket\"`.\n * @returns Nothing.\n */\nfunction warnOnce(transport: string): void {\n if (!isDevBuild() || warned.has(transport)) return;\n warned.add(transport);\n console.warn(\n `[tempest-react-sdk] ${transport}: a frame was not valid JSON, so the raw string is ` +\n `being delivered as if it were your message type. Pass \\`parser\\` to decode it, or ` +\n `\\`onParseError\\` to drop it and handle the failure. This warning appears once.`,\n );\n}\n\n/**\n * Warn once per transport that a frame was dropped by the schema.\n *\n * A dropped frame with no `onValidationError` is otherwise completely silent —\n * the stream looks healthy and the payload simply never arrives, which is the\n * hardest shape of failure to notice. Development builds only, once, for the\n * same reason as {@link warnOnce}.\n *\n * @param transport - Label used in the message, e.g. `\"createEventStream\"`.\n * @param issues - The issues the schema reported, summarized into the message.\n * @returns Nothing.\n */\nfunction warnValidationOnce(transport: string, issues: SchemaIssue[]): void {\n const key = `${transport}:schema`;\n if (!isDevBuild() || warned.has(key)) return;\n warned.add(key);\n const summary = issues.map((issue) => `${issue.path}: ${issue.message}`).join(\"; \");\n console.warn(\n `[tempest-react-sdk] ${transport}: a frame did not match \\`schema\\` and was dropped ` +\n `(${summary}). Pass \\`onValidationError\\` to handle it yourself. This warning ` +\n `appears once.`,\n );\n}\n\n/**\n * Decode one frame, reporting whether it should be delivered.\n *\n * A caller-supplied `parser` owns the frame completely: its result is delivered\n * as it is, or validated when a `schema` was also supplied — decoding text,\n * binary-as-base64 or a protocol of its own is the point of that option.\n *\n * Without one, the frame is parsed as JSON. When that throws:\n *\n * - with `onParseError`, the callback fires and the frame is **not** delivered,\n * because a consumer that asked to hear about failures did not ask to also\n * receive the broken frame;\n * - with `schema` and no `onParseError`, the raw string goes to the schema,\n * which refuses it — a caller who asked for validation never receives an\n * unvalidated payload, and a frame the server sent empty is exactly this case;\n * - with neither, the raw string is delivered as `T` — the behaviour every\n * version before this one had, kept so nothing breaks — and development builds\n * warn once that it happened.\n *\n * With a `schema`, a payload the schema refuses is not delivered, and\n * `onValidationError` hears the issues. The value delivered is the schema's\n * **output**, so a schema that coerces or defaults is honoured.\n *\n * @param raw - The frame body as text.\n * @param transport - Label used in the development warnings.\n * @param options - Caller-supplied decoder, schema and failure handlers.\n * @returns Whether to deliver, and the payload.\n */\nexport function decodeFrame<T>(\n raw: string,\n transport: string,\n options: DecodeFrameOptions<T>,\n): DecodedFrame<T> {\n const { parser, onParseError, schema, onValidationError } = options;\n\n /**\n * Put one decoded payload through the schema, when there is one.\n *\n * @param value - The payload as parsing produced it.\n * @returns Whether to deliver, and the payload the consumer should see.\n */\n function gate(value: unknown): DecodedFrame<T> {\n if (!schema) return { delivered: true, data: value as T };\n const result = validateWithSchema(schema, value);\n if (result.ok) return { delivered: true, data: result.data };\n if (onValidationError) onValidationError(result.issues, raw);\n else warnValidationOnce(transport, result.issues);\n return { delivered: false, data: undefined as T };\n }\n\n if (parser) return gate(parser(raw));\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n if (onParseError) {\n onParseError(error, raw);\n return { delivered: false, data: undefined as T };\n }\n if (schema) return gate(raw);\n warnOnce(transport);\n return { delivered: true, data: raw as unknown as T };\n }\n return gate(parsed);\n}\n\n/**\n * Forget which transports have already warned.\n *\n * Exists for tests, which would otherwise see the first case swallow the\n * warning for every case after it.\n *\n * @returns Nothing.\n */\nexport function resetFrameWarnings(): void {\n warned.clear();\n}\n"],"mappings":"iEAqCA,IAAM,EAAS,IAAI,IAWnB,SAAS,EAAS,EAAyB,CAClC,EAAA,WAAW,GAAK,GAAO,IAAI,CAAS,IACzC,EAAO,IAAI,CAAS,EACpB,QAAQ,KACJ,uBAAuB,EAAU,oNAGrC,EACJ,CAcA,SAAS,EAAmB,EAAmB,EAA6B,CACxE,IAAM,EAAM,GAAG,EAAU,SACzB,GAAI,CAAC,EAAA,WAAW,GAAK,EAAO,IAAI,CAAG,EAAG,OACtC,EAAO,IAAI,CAAG,EACd,IAAM,EAAU,EAAO,IAAK,GAAU,GAAG,EAAM,KAAK,IAAI,EAAM,SAAS,CAAC,CAAC,KAAK,IAAI,EAClF,QAAQ,KACJ,uBAAuB,EAAU,sDACzB,EAAQ,gFAEpB,CACJ,CA8BA,SAAgB,EACZ,EACA,EACA,EACe,CACf,GAAM,CAAE,SAAQ,eAAc,SAAQ,qBAAsB,EAQ5D,SAAS,EAAK,EAAiC,CAC3C,GAAI,CAAC,EAAQ,MAAO,CAAE,UAAW,GAAM,KAAM,CAAW,EACxD,IAAM,EAAS,EAAA,mBAAmB,EAAQ,CAAK,EAI/C,OAHI,EAAO,GAAW,CAAE,UAAW,GAAM,KAAM,EAAO,IAAK,GACvD,EAAmB,EAAkB,EAAO,OAAQ,CAAG,EACtD,EAAmB,EAAW,EAAO,MAAM,EACzC,CAAE,UAAW,GAAO,KAAM,IAAA,EAAe,EACpD,CAEA,GAAI,EAAQ,OAAO,EAAK,EAAO,CAAG,CAAC,EACnC,IAAI,EACJ,GAAI,CACA,EAAS,KAAK,MAAM,CAAG,CAC3B,OAAS,EAAO,CAOZ,OANI,GACA,EAAa,EAAO,CAAG,EAChB,CAAE,UAAW,GAAO,KAAM,IAAA,EAAe,GAEhD,EAAe,EAAK,CAAG,GAC3B,EAAS,CAAS,EACX,CAAE,UAAW,GAAM,KAAM,CAAoB,EACxD,CACA,OAAO,EAAK,CAAM,CACtB"}
@@ -1,30 +1,49 @@
1
1
  import { isDevBuild as e } from "./dev-mode.js";
2
+ import { validateWithSchema as t } from "./schema-like.js";
2
3
  //#region src/utils/json-frame.ts
3
- var t = /* @__PURE__ */ new Set();
4
- function n(n) {
5
- e() && !t.has(n) && (t.add(n), console.warn(`[tempest-react-sdk] ${n}: a frame was not valid JSON, so the raw string is being delivered as if it were your message type. Pass \`parser\` to decode it, or \`onParseError\` to drop it and handle the failure. This warning appears once.`));
4
+ var n = /* @__PURE__ */ new Set();
5
+ function r(t) {
6
+ e() && !n.has(t) && (n.add(t), console.warn(`[tempest-react-sdk] ${t}: a frame was not valid JSON, so the raw string is being delivered as if it were your message type. Pass \`parser\` to decode it, or \`onParseError\` to drop it and handle the failure. This warning appears once.`));
6
7
  }
7
- function r(e, t, r, i) {
8
- if (t) return {
9
- delivered: !0,
10
- data: t(e)
11
- };
12
- try {
13
- return {
8
+ function i(t, r) {
9
+ let i = `${t}:schema`;
10
+ if (!e() || n.has(i)) return;
11
+ n.add(i);
12
+ let a = r.map((e) => `${e.path}: ${e.message}`).join("; ");
13
+ console.warn(`[tempest-react-sdk] ${t}: a frame did not match \`schema\` and was dropped (${a}). Pass \`onValidationError\` to handle it yourself. This warning appears once.`);
14
+ }
15
+ function a(e, n, a) {
16
+ let { parser: o, onParseError: s, schema: c, onValidationError: l } = a;
17
+ function u(r) {
18
+ if (!c) return {
14
19
  delivered: !0,
15
- data: JSON.parse(e)
20
+ data: r
16
21
  };
22
+ let a = t(c, r);
23
+ return a.ok ? {
24
+ delivered: !0,
25
+ data: a.data
26
+ } : (l ? l(a.issues, e) : i(n, a.issues), {
27
+ delivered: !1,
28
+ data: void 0
29
+ });
30
+ }
31
+ if (o) return u(o(e));
32
+ let d;
33
+ try {
34
+ d = JSON.parse(e);
17
35
  } catch (t) {
18
- return r ? (r(t, e), {
36
+ return s ? (s(t, e), {
19
37
  delivered: !1,
20
38
  data: void 0
21
- }) : (n(i), {
39
+ }) : c ? u(e) : (r(n), {
22
40
  delivered: !0,
23
41
  data: e
24
42
  });
25
43
  }
44
+ return u(d);
26
45
  }
27
46
  //#endregion
28
- export { r as decodeFrame };
47
+ export { a as decodeFrame };
29
48
 
30
49
  //# sourceMappingURL=json-frame.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"json-frame.js","names":[],"sources":["../../src/utils/json-frame.ts"],"sourcesContent":["import { isDevBuild } from \"./dev-mode\";\n\n/**\n * The one decoder behind `createWebSocket`, `useWebSocket` and\n * `createEventStream`.\n *\n * Internal, and imported by path rather than through the `utils` barrel: it\n * exists so the three transports share one answer to \"the frame is not JSON\",\n * not so consumers can call it.\n *\n * That question used to have three identical copies of the same wrong answer —\n * `return raw as unknown as T`, which hands the consumer a `string` announced as\n * `T`. The failure never surfaced at the parse; it surfaced later, on the first\n * `message.id`, with nothing left to say the frame had not been JSON.\n */\n\n/** Outcome of decoding one frame. */\nexport interface DecodedFrame<T> {\n /** Whether the message should reach `onMessage`. */\n delivered: boolean;\n /** The decoded payload. Only meaningful when `delivered` is `true`. */\n data: T;\n}\n\nconst warned = new Set<string>();\n\n/**\n * Warn once per transport that a frame arrived which was not JSON.\n *\n * Once, because a stream sending text frames sends many, and a console line per\n * frame buries the one that mattered. Development builds only.\n *\n * @param transport - Label used in the message, e.g. `\"createWebSocket\"`.\n * @returns Nothing.\n */\nfunction warnOnce(transport: string): void {\n if (!isDevBuild() || warned.has(transport)) return;\n warned.add(transport);\n console.warn(\n `[tempest-react-sdk] ${transport}: a frame was not valid JSON, so the raw string is ` +\n `being delivered as if it were your message type. Pass \\`parser\\` to decode it, or ` +\n `\\`onParseError\\` to drop it and handle the failure. This warning appears once.`,\n );\n}\n\n/**\n * Decode one frame, reporting whether it should be delivered.\n *\n * A caller-supplied `parser` owns the frame completely and its result is always\n * delivered — decoding text, binary-as-base64 or a protocol of its own is the\n * point of that option.\n *\n * Without one, the frame is parsed as JSON. When that throws:\n *\n * - with `onParseError`, the callback fires and the frame is **not** delivered,\n * because a consumer that asked to hear about failures did not ask to also\n * receive the broken frame;\n * - without it, the raw string is delivered as `T` — the behaviour every version\n * before this one had, kept so nothing breaks — and development builds warn\n * once that it happened.\n *\n * @param raw - The frame body as text.\n * @param parser - Caller-supplied decoder, if any.\n * @param onParseError - Caller-supplied failure handler, if any.\n * @param transport - Label used in the development warning.\n * @returns Whether to deliver, and the payload.\n */\nexport function decodeFrame<T>(\n raw: string,\n parser: ((raw: string) => T) | undefined,\n onParseError: ((error: unknown, raw: string) => void) | undefined,\n transport: string,\n): DecodedFrame<T> {\n if (parser) return { delivered: true, data: parser(raw) };\n try {\n return { delivered: true, data: JSON.parse(raw) as T };\n } catch (error) {\n if (onParseError) {\n onParseError(error, raw);\n return { delivered: false, data: undefined as T };\n }\n warnOnce(transport);\n return { delivered: true, data: raw as unknown as T };\n }\n}\n\n/**\n * Forget which transports have already warned.\n *\n * Exists for tests, which would otherwise see the first case swallow the\n * warning for every case after it.\n *\n * @returns Nothing.\n */\nexport function resetFrameWarnings(): void {\n warned.clear();\n}\n"],"mappings":";;AAwBA,IAAM,oBAAS,IAAI,IAAY;AAW/B,SAAS,EAAS,GAAyB;CACnC,AAAC,EAAW,KAAK,GAAO,IAAI,CAAS,MACzC,EAAO,IAAI,CAAS,GACpB,QAAQ,KACJ,uBAAuB,EAAU,oNAGrC;AACJ;AAwBA,SAAgB,EACZ,GACA,GACA,GACA,GACe;CACf,IAAI,GAAQ,OAAO;EAAE,WAAW;EAAM,MAAM,EAAO,CAAG;CAAE;CACxD,IAAI;EACA,OAAO;GAAE,WAAW;GAAM,MAAM,KAAK,MAAM,CAAG;EAAO;CACzD,SAAS,GAAO;EAMZ,OALI,KACA,EAAa,GAAO,CAAG,GAChB;GAAE,WAAW;GAAO,MAAM,KAAA;EAAe,MAEpD,EAAS,CAAS,GACX;GAAE,WAAW;GAAM,MAAM;EAAoB;CACxD;AACJ"}
1
+ {"version":3,"file":"json-frame.js","names":[],"sources":["../../src/utils/json-frame.ts"],"sourcesContent":["import { isDevBuild } from \"./dev-mode\";\nimport { validateWithSchema, type SchemaIssue, type SchemaLike } from \"./schema-like\";\n\n/**\n * The one decoder behind `createWebSocket`, `useWebSocket` and\n * `createEventStream`.\n *\n * Internal, and imported by path rather than through the `utils` barrel: it\n * exists so the three transports share one answer to \"the frame is not JSON\",\n * not so consumers can call it.\n *\n * That question used to have three identical copies of the same wrong answer —\n * `return raw as unknown as T`, which hands the consumer a `string` announced as\n * `T`. The failure never surfaced at the parse; it surfaced later, on the first\n * `message.id`, with nothing left to say the frame had not been JSON.\n */\n\n/** Outcome of decoding one frame. */\nexport interface DecodedFrame<T> {\n /** Whether the message should reach `onMessage`. */\n delivered: boolean;\n /** The decoded payload. Only meaningful when `delivered` is `true`. */\n data: T;\n}\n\n/** How one frame should be turned into `T`, and who hears about failures. */\nexport interface DecodeFrameOptions<T> {\n /** Caller-supplied decoder, which owns the frame completely. */\n parser?: (raw: string) => T;\n /** Caller-supplied handler for a frame that is not valid JSON. */\n onParseError?: (error: unknown, raw: string) => void;\n /** Caller-supplied schema the decoded payload must satisfy. */\n schema?: SchemaLike<T>;\n /** Caller-supplied handler for a payload the schema refused. */\n onValidationError?: (issues: SchemaIssue[], raw: string) => void;\n}\n\nconst warned = new Set<string>();\n\n/**\n * Warn once per transport that a frame arrived which was not JSON.\n *\n * Once, because a stream sending text frames sends many, and a console line per\n * frame buries the one that mattered. Development builds only.\n *\n * @param transport - Label used in the message, e.g. `\"createWebSocket\"`.\n * @returns Nothing.\n */\nfunction warnOnce(transport: string): void {\n if (!isDevBuild() || warned.has(transport)) return;\n warned.add(transport);\n console.warn(\n `[tempest-react-sdk] ${transport}: a frame was not valid JSON, so the raw string is ` +\n `being delivered as if it were your message type. Pass \\`parser\\` to decode it, or ` +\n `\\`onParseError\\` to drop it and handle the failure. This warning appears once.`,\n );\n}\n\n/**\n * Warn once per transport that a frame was dropped by the schema.\n *\n * A dropped frame with no `onValidationError` is otherwise completely silent —\n * the stream looks healthy and the payload simply never arrives, which is the\n * hardest shape of failure to notice. Development builds only, once, for the\n * same reason as {@link warnOnce}.\n *\n * @param transport - Label used in the message, e.g. `\"createEventStream\"`.\n * @param issues - The issues the schema reported, summarized into the message.\n * @returns Nothing.\n */\nfunction warnValidationOnce(transport: string, issues: SchemaIssue[]): void {\n const key = `${transport}:schema`;\n if (!isDevBuild() || warned.has(key)) return;\n warned.add(key);\n const summary = issues.map((issue) => `${issue.path}: ${issue.message}`).join(\"; \");\n console.warn(\n `[tempest-react-sdk] ${transport}: a frame did not match \\`schema\\` and was dropped ` +\n `(${summary}). Pass \\`onValidationError\\` to handle it yourself. This warning ` +\n `appears once.`,\n );\n}\n\n/**\n * Decode one frame, reporting whether it should be delivered.\n *\n * A caller-supplied `parser` owns the frame completely: its result is delivered\n * as it is, or validated when a `schema` was also supplied — decoding text,\n * binary-as-base64 or a protocol of its own is the point of that option.\n *\n * Without one, the frame is parsed as JSON. When that throws:\n *\n * - with `onParseError`, the callback fires and the frame is **not** delivered,\n * because a consumer that asked to hear about failures did not ask to also\n * receive the broken frame;\n * - with `schema` and no `onParseError`, the raw string goes to the schema,\n * which refuses it — a caller who asked for validation never receives an\n * unvalidated payload, and a frame the server sent empty is exactly this case;\n * - with neither, the raw string is delivered as `T` — the behaviour every\n * version before this one had, kept so nothing breaks — and development builds\n * warn once that it happened.\n *\n * With a `schema`, a payload the schema refuses is not delivered, and\n * `onValidationError` hears the issues. The value delivered is the schema's\n * **output**, so a schema that coerces or defaults is honoured.\n *\n * @param raw - The frame body as text.\n * @param transport - Label used in the development warnings.\n * @param options - Caller-supplied decoder, schema and failure handlers.\n * @returns Whether to deliver, and the payload.\n */\nexport function decodeFrame<T>(\n raw: string,\n transport: string,\n options: DecodeFrameOptions<T>,\n): DecodedFrame<T> {\n const { parser, onParseError, schema, onValidationError } = options;\n\n /**\n * Put one decoded payload through the schema, when there is one.\n *\n * @param value - The payload as parsing produced it.\n * @returns Whether to deliver, and the payload the consumer should see.\n */\n function gate(value: unknown): DecodedFrame<T> {\n if (!schema) return { delivered: true, data: value as T };\n const result = validateWithSchema(schema, value);\n if (result.ok) return { delivered: true, data: result.data };\n if (onValidationError) onValidationError(result.issues, raw);\n else warnValidationOnce(transport, result.issues);\n return { delivered: false, data: undefined as T };\n }\n\n if (parser) return gate(parser(raw));\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n if (onParseError) {\n onParseError(error, raw);\n return { delivered: false, data: undefined as T };\n }\n if (schema) return gate(raw);\n warnOnce(transport);\n return { delivered: true, data: raw as unknown as T };\n }\n return gate(parsed);\n}\n\n/**\n * Forget which transports have already warned.\n *\n * Exists for tests, which would otherwise see the first case swallow the\n * warning for every case after it.\n *\n * @returns Nothing.\n */\nexport function resetFrameWarnings(): void {\n warned.clear();\n}\n"],"mappings":";;;AAqCA,IAAM,oBAAS,IAAI,IAAY;AAW/B,SAAS,EAAS,GAAyB;CACnC,AAAC,EAAW,KAAK,GAAO,IAAI,CAAS,MACzC,EAAO,IAAI,CAAS,GACpB,QAAQ,KACJ,uBAAuB,EAAU,oNAGrC;AACJ;AAcA,SAAS,EAAmB,GAAmB,GAA6B;CACxE,IAAM,IAAM,GAAG,EAAU;CACzB,IAAI,CAAC,EAAW,KAAK,EAAO,IAAI,CAAG,GAAG;CACtC,EAAO,IAAI,CAAG;CACd,IAAM,IAAU,EAAO,KAAK,MAAU,GAAG,EAAM,KAAK,IAAI,EAAM,SAAS,CAAC,CAAC,KAAK,IAAI;CAClF,QAAQ,KACJ,uBAAuB,EAAU,sDACzB,EAAQ,gFAEpB;AACJ;AA8BA,SAAgB,EACZ,GACA,GACA,GACe;CACf,IAAM,EAAE,WAAQ,iBAAc,WAAQ,yBAAsB;CAQ5D,SAAS,EAAK,GAAiC;EAC3C,IAAI,CAAC,GAAQ,OAAO;GAAE,WAAW;GAAM,MAAM;EAAW;EACxD,IAAM,IAAS,EAAmB,GAAQ,CAAK;EAI/C,OAHI,EAAO,KAAW;GAAE,WAAW;GAAM,MAAM,EAAO;EAAK,KACvD,IAAmB,EAAkB,EAAO,QAAQ,CAAG,IACtD,EAAmB,GAAW,EAAO,MAAM,GACzC;GAAE,WAAW;GAAO,MAAM,KAAA;EAAe;CACpD;CAEA,IAAI,GAAQ,OAAO,EAAK,EAAO,CAAG,CAAC;CACnC,IAAI;CACJ,IAAI;EACA,IAAS,KAAK,MAAM,CAAG;CAC3B,SAAS,GAAO;EAOZ,OANI,KACA,EAAa,GAAO,CAAG,GAChB;GAAE,WAAW;GAAO,MAAM,KAAA;EAAe,KAEhD,IAAe,EAAK,CAAG,KAC3B,EAAS,CAAS,GACX;GAAE,WAAW;GAAM,MAAM;EAAoB;CACxD;CACA,OAAO,EAAK,CAAM;AACtB"}
@@ -0,0 +1,2 @@
1
+ var e={path:`<root>`,message:`the schema validated asynchronously, and a frame is decoded synchronously — there is nowhere to await it. Use a synchronous schema, or validate inside your own handler.`};function t(e){return!e||e.length===0?`<root>`:e.map(e=>String(typeof e==`object`&&e?e.key:e)).join(`.`)}function n(e){return{path:t(e.path),message:e.message}}function r(t,r){if(`~standard`in t){let i=t[`~standard`].validate(r);if(typeof i.then==`function`)return{ok:!1,issues:[e]};let a=i;return a.issues?{ok:!1,issues:a.issues.map(n)}:{ok:!0,data:a.value}}let i=t.safeParse(r);return i.success?{ok:!0,data:i.data}:{ok:!1,issues:i.error.issues.map(n)}}exports.validateWithSchema=r;
2
+ //# sourceMappingURL=schema-like.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-like.cjs","names":[],"sources":["../../src/utils/schema-like.ts"],"sourcesContent":["/**\n * The SDK's answer to \"the caller handed me a schema\" — one normalizer behind\n * every option that takes one.\n *\n * Internal, and imported by path rather than through the `utils` barrel: the\n * function exists so `decodeFrame` and anything else that validates a payload\n * share one reading of the two shapes below, not so consumers can call it. The\n * types are public, because an option typed `SchemaLike<T>` is a name the\n * consumer has to be able to write down.\n *\n * Two shapes are accepted on purpose:\n *\n * - [Standard Schema](https://standardschema.dev) (`~standard`), which zod\n * (>=3.24), valibot and arktype all implement, so the SDK validates against\n * any of them without depending on one;\n * - `.safeParse`, because the SDK's own zod range starts at `^3.23.0`, which\n * predates `~standard`, and because it is the method every zod user already\n * knows.\n */\n\n/** One field-level complaint from a schema validation. */\nexport interface SchemaIssue {\n /** Dotted path to the offending field, or `\"<root>\"` for the value itself. */\n path: string;\n /** What the validator said was wrong. */\n message: string;\n}\n\n/** A path segment as Standard Schema reports it: a key, or an object holding one. */\ntype IssuePathSegment = PropertyKey | { readonly key: PropertyKey };\n\n/** One issue as either supported shape reports it. */\ninterface RawIssue {\n readonly message: string;\n readonly path?: readonly IssuePathSegment[] | undefined;\n}\n\n/** A schema exposing the [Standard Schema](https://standardschema.dev) interface. */\nexport interface StandardSchemaLike<T> {\n readonly \"~standard\": {\n readonly validate: (\n value: unknown,\n ) =>\n | { readonly value: T; readonly issues?: undefined }\n | { readonly issues: readonly RawIssue[] }\n | Promise<\n | { readonly value: T; readonly issues?: undefined }\n | { readonly issues: readonly RawIssue[] }\n >;\n };\n}\n\n/** A schema exposing zod's `.safeParse`, including versions older than `~standard`. */\nexport interface SafeParseSchemaLike<T> {\n readonly safeParse: (\n value: unknown,\n ) =>\n | { readonly success: true; readonly data: T }\n | { readonly success: false; readonly error: { readonly issues: readonly RawIssue[] } };\n}\n\n/** Anything the SDK can validate a payload against. */\nexport type SchemaLike<T> = StandardSchemaLike<T> | SafeParseSchemaLike<T>;\n\n/** Outcome of validating one value against a {@link SchemaLike}. */\nexport type SchemaValidation<T> = { ok: true; data: T } | { ok: false; issues: SchemaIssue[] };\n\n/**\n * The issue reported when a schema answers asynchronously.\n *\n * A frame is decoded inside the transport's `message` handler and delivered from\n * it, so there is nowhere to await: awaiting would deliver frames in whatever\n * order their validations settled, which is worse than refusing. Reported as a\n * validation failure rather than thrown, so it arrives through the same\n * `onValidationError` the caller already registered.\n */\nconst ASYNC_ISSUE: SchemaIssue = {\n path: \"<root>\",\n message:\n \"the schema validated asynchronously, and a frame is decoded synchronously — \" +\n \"there is nowhere to await it. Use a synchronous schema, or validate inside your \" +\n \"own handler.\",\n};\n\n/**\n * Format a Standard Schema issue path as a dotted string.\n *\n * @param path - The reported path, if any.\n * @returns The dotted path, or `\"<root>\"` when the value itself is at fault.\n */\nfunction formatPath(path: readonly IssuePathSegment[] | undefined): string {\n if (!path || path.length === 0) return \"<root>\";\n return path\n .map((segment) =>\n typeof segment === \"object\" && segment !== null ? String(segment.key) : String(segment),\n )\n .join(\".\");\n}\n\n/**\n * Normalize one issue from either supported shape.\n *\n * @param issue - The issue as the validator reported it.\n * @returns The issue with a dotted path.\n */\nfunction toIssue(issue: RawIssue): SchemaIssue {\n return { path: formatPath(issue.path), message: issue.message };\n}\n\n/**\n * Validate a value against a schema, whichever of the two shapes it has.\n *\n * @param schema - A Standard Schema or a `.safeParse` schema.\n * @param value - The value to validate.\n * @returns The validated output, or the issues explaining why it was refused.\n */\nexport function validateWithSchema<T>(schema: SchemaLike<T>, value: unknown): SchemaValidation<T> {\n if (\"~standard\" in schema) {\n const result = schema[\"~standard\"].validate(value);\n if (typeof (result as { then?: unknown }).then === \"function\") {\n return { ok: false, issues: [ASYNC_ISSUE] };\n }\n const settled = result as\n | { readonly value: T; readonly issues?: undefined }\n | { readonly issues: readonly RawIssue[] };\n if (settled.issues) return { ok: false, issues: settled.issues.map(toIssue) };\n return { ok: true, data: settled.value };\n }\n const result = schema.safeParse(value);\n if (result.success) return { ok: true, data: result.data };\n return { ok: false, issues: result.error.issues.map(toIssue) };\n}\n"],"mappings":"AA4EA,IAAM,EAA2B,CAC7B,KAAM,SACN,QACI,0KAGR,EAQA,SAAS,EAAW,EAAuD,CAEvE,MADI,CAAC,GAAQ,EAAK,SAAW,EAAU,SAChC,EACF,IAAK,GACgD,OAAlD,OAAO,GAAY,UAAY,EAA0B,EAAQ,IAAc,CAAO,CAC1F,CAAC,CACA,KAAK,GAAG,CACjB,CAQA,SAAS,EAAQ,EAA8B,CAC3C,MAAO,CAAE,KAAM,EAAW,EAAM,IAAI,EAAG,QAAS,EAAM,OAAQ,CAClE,CASA,SAAgB,EAAsB,EAAuB,EAAqC,CAC9F,GAAI,cAAe,EAAQ,CACvB,IAAM,EAAS,EAAO,YAAY,CAAC,SAAS,CAAK,EACjD,GAAI,OAAQ,EAA8B,MAAS,WAC/C,MAAO,CAAE,GAAI,GAAO,OAAQ,CAAC,CAAW,CAAE,EAE9C,IAAM,EAAU,EAIhB,OADI,EAAQ,OAAe,CAAE,GAAI,GAAO,OAAQ,EAAQ,OAAO,IAAI,CAAO,CAAE,EACrE,CAAE,GAAI,GAAM,KAAM,EAAQ,KAAM,CAC3C,CACA,IAAM,EAAS,EAAO,UAAU,CAAK,EAErC,OADI,EAAO,QAAgB,CAAE,GAAI,GAAM,KAAM,EAAO,IAAK,EAClD,CAAE,GAAI,GAAO,OAAQ,EAAO,MAAM,OAAO,IAAI,CAAO,CAAE,CACjE"}
@@ -0,0 +1,43 @@
1
+ //#region src/utils/schema-like.ts
2
+ var e = {
3
+ path: "<root>",
4
+ message: "the schema validated asynchronously, and a frame is decoded synchronously — there is nowhere to await it. Use a synchronous schema, or validate inside your own handler."
5
+ };
6
+ function t(e) {
7
+ return !e || e.length === 0 ? "<root>" : e.map((e) => String(typeof e == "object" && e ? e.key : e)).join(".");
8
+ }
9
+ function n(e) {
10
+ return {
11
+ path: t(e.path),
12
+ message: e.message
13
+ };
14
+ }
15
+ function r(t, r) {
16
+ if ("~standard" in t) {
17
+ let i = t["~standard"].validate(r);
18
+ if (typeof i.then == "function") return {
19
+ ok: !1,
20
+ issues: [e]
21
+ };
22
+ let a = i;
23
+ return a.issues ? {
24
+ ok: !1,
25
+ issues: a.issues.map(n)
26
+ } : {
27
+ ok: !0,
28
+ data: a.value
29
+ };
30
+ }
31
+ let i = t.safeParse(r);
32
+ return i.success ? {
33
+ ok: !0,
34
+ data: i.data
35
+ } : {
36
+ ok: !1,
37
+ issues: i.error.issues.map(n)
38
+ };
39
+ }
40
+ //#endregion
41
+ export { r as validateWithSchema };
42
+
43
+ //# sourceMappingURL=schema-like.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-like.js","names":[],"sources":["../../src/utils/schema-like.ts"],"sourcesContent":["/**\n * The SDK's answer to \"the caller handed me a schema\" — one normalizer behind\n * every option that takes one.\n *\n * Internal, and imported by path rather than through the `utils` barrel: the\n * function exists so `decodeFrame` and anything else that validates a payload\n * share one reading of the two shapes below, not so consumers can call it. The\n * types are public, because an option typed `SchemaLike<T>` is a name the\n * consumer has to be able to write down.\n *\n * Two shapes are accepted on purpose:\n *\n * - [Standard Schema](https://standardschema.dev) (`~standard`), which zod\n * (>=3.24), valibot and arktype all implement, so the SDK validates against\n * any of them without depending on one;\n * - `.safeParse`, because the SDK's own zod range starts at `^3.23.0`, which\n * predates `~standard`, and because it is the method every zod user already\n * knows.\n */\n\n/** One field-level complaint from a schema validation. */\nexport interface SchemaIssue {\n /** Dotted path to the offending field, or `\"<root>\"` for the value itself. */\n path: string;\n /** What the validator said was wrong. */\n message: string;\n}\n\n/** A path segment as Standard Schema reports it: a key, or an object holding one. */\ntype IssuePathSegment = PropertyKey | { readonly key: PropertyKey };\n\n/** One issue as either supported shape reports it. */\ninterface RawIssue {\n readonly message: string;\n readonly path?: readonly IssuePathSegment[] | undefined;\n}\n\n/** A schema exposing the [Standard Schema](https://standardschema.dev) interface. */\nexport interface StandardSchemaLike<T> {\n readonly \"~standard\": {\n readonly validate: (\n value: unknown,\n ) =>\n | { readonly value: T; readonly issues?: undefined }\n | { readonly issues: readonly RawIssue[] }\n | Promise<\n | { readonly value: T; readonly issues?: undefined }\n | { readonly issues: readonly RawIssue[] }\n >;\n };\n}\n\n/** A schema exposing zod's `.safeParse`, including versions older than `~standard`. */\nexport interface SafeParseSchemaLike<T> {\n readonly safeParse: (\n value: unknown,\n ) =>\n | { readonly success: true; readonly data: T }\n | { readonly success: false; readonly error: { readonly issues: readonly RawIssue[] } };\n}\n\n/** Anything the SDK can validate a payload against. */\nexport type SchemaLike<T> = StandardSchemaLike<T> | SafeParseSchemaLike<T>;\n\n/** Outcome of validating one value against a {@link SchemaLike}. */\nexport type SchemaValidation<T> = { ok: true; data: T } | { ok: false; issues: SchemaIssue[] };\n\n/**\n * The issue reported when a schema answers asynchronously.\n *\n * A frame is decoded inside the transport's `message` handler and delivered from\n * it, so there is nowhere to await: awaiting would deliver frames in whatever\n * order their validations settled, which is worse than refusing. Reported as a\n * validation failure rather than thrown, so it arrives through the same\n * `onValidationError` the caller already registered.\n */\nconst ASYNC_ISSUE: SchemaIssue = {\n path: \"<root>\",\n message:\n \"the schema validated asynchronously, and a frame is decoded synchronously — \" +\n \"there is nowhere to await it. Use a synchronous schema, or validate inside your \" +\n \"own handler.\",\n};\n\n/**\n * Format a Standard Schema issue path as a dotted string.\n *\n * @param path - The reported path, if any.\n * @returns The dotted path, or `\"<root>\"` when the value itself is at fault.\n */\nfunction formatPath(path: readonly IssuePathSegment[] | undefined): string {\n if (!path || path.length === 0) return \"<root>\";\n return path\n .map((segment) =>\n typeof segment === \"object\" && segment !== null ? String(segment.key) : String(segment),\n )\n .join(\".\");\n}\n\n/**\n * Normalize one issue from either supported shape.\n *\n * @param issue - The issue as the validator reported it.\n * @returns The issue with a dotted path.\n */\nfunction toIssue(issue: RawIssue): SchemaIssue {\n return { path: formatPath(issue.path), message: issue.message };\n}\n\n/**\n * Validate a value against a schema, whichever of the two shapes it has.\n *\n * @param schema - A Standard Schema or a `.safeParse` schema.\n * @param value - The value to validate.\n * @returns The validated output, or the issues explaining why it was refused.\n */\nexport function validateWithSchema<T>(schema: SchemaLike<T>, value: unknown): SchemaValidation<T> {\n if (\"~standard\" in schema) {\n const result = schema[\"~standard\"].validate(value);\n if (typeof (result as { then?: unknown }).then === \"function\") {\n return { ok: false, issues: [ASYNC_ISSUE] };\n }\n const settled = result as\n | { readonly value: T; readonly issues?: undefined }\n | { readonly issues: readonly RawIssue[] };\n if (settled.issues) return { ok: false, issues: settled.issues.map(toIssue) };\n return { ok: true, data: settled.value };\n }\n const result = schema.safeParse(value);\n if (result.success) return { ok: true, data: result.data };\n return { ok: false, issues: result.error.issues.map(toIssue) };\n}\n"],"mappings":";AA4EA,IAAM,IAA2B;CAC7B,MAAM;CACN,SACI;AAGR;AAQA,SAAS,EAAW,GAAuD;CAEvE,OADI,CAAC,KAAQ,EAAK,WAAW,IAAU,WAChC,EACF,KAAK,MACgD,OAAlD,OAAO,KAAY,YAAY,IAA0B,EAAQ,MAAc,CAAO,CAC1F,CAAC,CACA,KAAK,GAAG;AACjB;AAQA,SAAS,EAAQ,GAA8B;CAC3C,OAAO;EAAE,MAAM,EAAW,EAAM,IAAI;EAAG,SAAS,EAAM;CAAQ;AAClE;AASA,SAAgB,EAAsB,GAAuB,GAAqC;CAC9F,IAAI,eAAe,GAAQ;EACvB,IAAM,IAAS,EAAO,YAAY,CAAC,SAAS,CAAK;EACjD,IAAI,OAAQ,EAA8B,QAAS,YAC/C,OAAO;GAAE,IAAI;GAAO,QAAQ,CAAC,CAAW;EAAE;EAE9C,IAAM,IAAU;EAIhB,OADI,EAAQ,SAAe;GAAE,IAAI;GAAO,QAAQ,EAAQ,OAAO,IAAI,CAAO;EAAE,IACrE;GAAE,IAAI;GAAM,MAAM,EAAQ;EAAM;CAC3C;CACA,IAAM,IAAS,EAAO,UAAU,CAAK;CAErC,OADI,EAAO,UAAgB;EAAE,IAAI;EAAM,MAAM,EAAO;CAAK,IAClD;EAAE,IAAI;EAAO,QAAQ,EAAO,MAAM,OAAO,IAAI,CAAO;CAAE;AACjE"}
@@ -1,2 +1,2 @@
1
- const e=require("../utils/json-frame.cjs"),t=require("./resilience.cjs");function n(n,r={}){let{protocols:i,maxRetries:a=10,initialBackoff:o=1e3,maxBackoff:s=3e4,jitter:ee=.3,handshakeTimeout:c=8e3,silenceTimeout:l=0,waitForOnline:u=!0,pingInterval:d=0,pingPayload:f=JSON.stringify({type:`ping`}),respondToPing:p=!0,pongPayload:m=JSON.stringify({type:`pong`}),queueWhileClosed:h=!1,maxQueuedMessages:te=100,parser:ne,onOpen:g,onMessage:_,onClose:v,onError:y,onParseError:b,onStatusChange:re,onReconnecting:ie,onReconnected:x,onLost:S}=r,C=null,w=null,T=null,E=null,D=null,O=null,k=l,A=0,j=`idle`,M=!1,N=!1,P=[],F=null,I=null,L=new Promise((e,t)=>{F=e,I=t});L.catch(()=>void 0);function R(e){return typeof e==`object`&&!!e&&e.type===`ping`}function z(e){for(;P.length>0&&e.readyState===WebSocket.OPEN;)e.send(P.shift())}function B(e){j!==e&&(j=e,re?.(e))}function V(){T&&=(clearInterval(T),null)}function H(){E&&=(clearTimeout(E),null)}function U(){D&&=(clearTimeout(D),null)}function W(){!d||d<=0||(V(),T=setInterval(()=>{C?.readyState===WebSocket.OPEN&&C.send(f)},d))}function G(){U(),!(M||k<=0)&&(D=setTimeout(K,k))}function K(){U(),!M&&C&&(J(C),C=null,B(`closed`),X())}function q(e){H(),!(M||e.readyState!==WebSocket.CONNECTING)&&(J(e),C===e&&(C=null),B(`closed`),X())}function J(e){e.onopen=null,e.onmessage=null,e.onerror=null,e.onclose=null;try{e.close()}catch{}}function Y(e){if(U(),H(),Z(),e===`rejected`&&(M=!0),B(`error`),!N&&I){let t=I;I=null,F=null,t(Error(`websocket_${e}`))}S?.(e)}function X(){if(M)return;if(A>=a){Y(`exhausted`);return}let e=t.backoffDelay(A,{initialBackoff:o,maxBackoff:s,jitter:ee});if(A+=1,ie?.(A,a),u&&typeof navigator<`u`&&navigator.onLine===!1){ae();return}w=setTimeout(Q,e)}function ae(){if(O||typeof window>`u`)return;let e=()=>{Z(),M||Q()};O=e,window.addEventListener(`online`,e)}function Z(){!O||typeof window>`u`||(window.removeEventListener(`online`,O),O=null)}function Q(){if(M)return;if(w=null,H(),C){let e=C;e.onmessage=null,e.onclose=null,e.onerror=null,e.readyState!==WebSocket.CONNECTING&&(e.onopen=null),$(e)}B(`connecting`);let r=new WebSocket(n,i);C=r,c>0&&(E=setTimeout(()=>q(r),c)),r.onopen=e=>{H();let t=A>0;if(A=0,N=!0,B(`open`),W(),G(),z(r),F){let e=F;F=null,I=null,e()}g?.(e),t&&x?.()},r.onmessage=t=>{G();let n=typeof t.data==`string`?t.data:``,i=e.decodeFrame(n,ne,b,`createWebSocket`);if(!i.delivered)return;let a=i.data;p&&R(a)&&r.readyState===WebSocket.OPEN&&r.send(m),_?.({data:a,raw:t})},r.onerror=e=>{y?.(e)},r.onclose=e=>{if(H(),V(),U(),v?.(e),C=null,B(`closed`),!M){if(t.isRejectionCloseCode(e.code)){Y(`rejected`);return}if(t.shouldRetryClose(e.code,e.wasClean)){X();return}N||Y(`rejected`)}}}function oe(e){return C?.readyState===WebSocket.OPEN?(C.send(e),!0):!h||M?!1:(P.length>=te&&P.shift(),P.push(e),!0)}function se(e,t){if(M=!0,w&&=(clearTimeout(w),null),V(),H(),U(),Z(),A=0,P.length=0,I){let e=I;I=null,F=null,e(Error(`websocket_closed`))}C&&=(B(`closing`),$(C,e,t),null),B(`closed`)}function $(e,t,n){if(e.readyState===WebSocket.CONNECTING){e.onopen=()=>e.close(t,n),e.onmessage=null,e.onerror=null,e.onclose=null;return}e.close(t,n)}function ce(){w&&=(clearTimeout(w),null),Z(),A=0,M=!1,Q()}function le(e){k=Number.isFinite(e)&&e>0?e:0,C?.readyState===WebSocket.OPEN?G():U()}return Q(),{send:oe,close:se,reconnect:ce,setSilenceTimeout:le,opened:L,get status(){return j}}}exports.createWebSocket=n;
1
+ const e=require("../utils/json-frame.cjs"),t=require("./resilience.cjs");function n(n,r={}){let{protocols:i,maxRetries:a=10,initialBackoff:o=1e3,maxBackoff:s=3e4,jitter:ee=.3,handshakeTimeout:c=8e3,silenceTimeout:l=0,waitForOnline:u=!0,pingInterval:d=0,pingPayload:f=JSON.stringify({type:`ping`}),respondToPing:p=!0,pongPayload:m=JSON.stringify({type:`pong`}),queueWhileClosed:h=!1,maxQueuedMessages:te=100,parser:ne,onOpen:g,onMessage:_,onClose:v,onError:y,onParseError:b,schema:re,onValidationError:ie,onStatusChange:ae,onReconnecting:x,onReconnected:S,onLost:C}=r,w=null,T=null,E=null,D=null,O=null,k=null,A=l,j=0,M=`idle`,N=!1,P=!1,F=[],I=null,L=null,R=new Promise((e,t)=>{I=e,L=t});R.catch(()=>void 0);function z(e){return typeof e==`object`&&!!e&&e.type===`ping`}function B(e){if(!e.includes(`"ping"`))return!1;try{return z(JSON.parse(e))}catch{return!1}}function V(e){for(;F.length>0&&e.readyState===WebSocket.OPEN;)e.send(F.shift())}function H(e){M!==e&&(M=e,ae?.(e))}function U(){E&&=(clearInterval(E),null)}function W(){D&&=(clearTimeout(D),null)}function G(){O&&=(clearTimeout(O),null)}function K(){!d||d<=0||(U(),E=setInterval(()=>{w?.readyState===WebSocket.OPEN&&w.send(f)},d))}function q(){G(),!(N||A<=0)&&(O=setTimeout(oe,A))}function oe(){G(),!N&&w&&(J(w),w=null,H(`closed`),X())}function se(e){W(),!(N||e.readyState!==WebSocket.CONNECTING)&&(J(e),w===e&&(w=null),H(`closed`),X())}function J(e){e.onopen=null,e.onmessage=null,e.onerror=null,e.onclose=null;try{e.close()}catch{}}function Y(e){if(G(),W(),Z(),e===`rejected`&&(N=!0),H(`error`),!P&&L){let t=L;L=null,I=null,t(Error(`websocket_${e}`))}C?.(e)}function X(){if(N)return;if(j>=a){Y(`exhausted`);return}let e=t.backoffDelay(j,{initialBackoff:o,maxBackoff:s,jitter:ee});if(j+=1,x?.(j,a),u&&typeof navigator<`u`&&navigator.onLine===!1){ce();return}T=setTimeout(Q,e)}function ce(){if(k||typeof window>`u`)return;let e=()=>{Z(),N||Q()};k=e,window.addEventListener(`online`,e)}function Z(){!k||typeof window>`u`||(window.removeEventListener(`online`,k),k=null)}function Q(){if(N)return;if(T=null,W(),w){let e=w;e.onmessage=null,e.onclose=null,e.onerror=null,e.readyState!==WebSocket.CONNECTING&&(e.onopen=null),$(e)}H(`connecting`);let r=new WebSocket(n,i);w=r,c>0&&(D=setTimeout(()=>se(r),c)),r.onopen=e=>{W();let t=j>0;if(j=0,P=!0,H(`open`),K(),q(),V(r),I){let e=I;I=null,L=null,e()}g?.(e),t&&S?.()},r.onmessage=t=>{q();let n=typeof t.data==`string`?t.data:``,i=e.decodeFrame(n,`createWebSocket`,{parser:ne,onParseError:b,schema:re,onValidationError:ie});if(!i.delivered){p&&r.readyState===WebSocket.OPEN&&B(n)&&r.send(m);return}let a=i.data;p&&z(a)&&r.readyState===WebSocket.OPEN&&r.send(m),_?.({data:a,raw:t})},r.onerror=e=>{y?.(e)},r.onclose=e=>{if(W(),U(),G(),v?.(e),w=null,H(`closed`),!N){if(t.isRejectionCloseCode(e.code)){Y(`rejected`);return}if(t.shouldRetryClose(e.code,e.wasClean)){X();return}P||Y(`rejected`)}}}function le(e){return w?.readyState===WebSocket.OPEN?(w.send(e),!0):!h||N?!1:(F.length>=te&&F.shift(),F.push(e),!0)}function ue(e,t){if(N=!0,T&&=(clearTimeout(T),null),U(),W(),G(),Z(),j=0,F.length=0,L){let e=L;L=null,I=null,e(Error(`websocket_closed`))}w&&=(H(`closing`),$(w,e,t),null),H(`closed`)}function $(e,t,n){if(e.readyState===WebSocket.CONNECTING){e.onopen=()=>e.close(t,n),e.onmessage=null,e.onerror=null,e.onclose=null;return}e.close(t,n)}function de(){T&&=(clearTimeout(T),null),Z(),j=0,N=!1,Q()}function fe(e){A=Number.isFinite(e)&&e>0?e:0,w?.readyState===WebSocket.OPEN?q():G()}return Q(),{send:le,close:ue,reconnect:de,setSilenceTimeout:fe,opened:R,get status(){return M}}}exports.createWebSocket=n;
2
2
  //# sourceMappingURL=create-web-socket.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-web-socket.cjs","names":[],"sources":["../../src/ws/create-web-socket.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — reconnect with backoff, heartbeat,\n * the handshake and silence timers that detect a link which never fails out loud,\n * the send queue that survives a disconnect and the listener set that must be re-\n * attached to each new socket — one connection's lifetime, one closure. The queue\n * and the reconnect timer are the same decision seen twice.\n */\nimport {\n backoffDelay,\n isRejectionCloseCode,\n shouldRetryClose,\n type WebSocketLostReason,\n} from \"./resilience\";\nimport { decodeFrame } from \"../utils/json-frame\";\n\nexport type WebSocketStatus = \"idle\" | \"connecting\" | \"open\" | \"closing\" | \"closed\" | \"error\";\n\nexport interface WebSocketMessage<T> {\n /** Parsed payload — JSON-decoded when possible, raw string otherwise. */\n data: T;\n /** The original `MessageEvent`. */\n raw: MessageEvent;\n}\n\nexport interface CreateWebSocketOptions<T> {\n /** Subprotocol(s) forwarded to the `WebSocket` constructor. */\n protocols?: string | string[];\n /** Max reconnect attempts. Default: 10. Pass 0 to disable. */\n maxRetries?: number;\n /** Initial backoff (ms). Doubles each attempt, capped at `maxBackoff`. Default: 1000. */\n initialBackoff?: number;\n /** Maximum backoff (ms). Default: 30000. */\n maxBackoff?: number;\n /**\n * Fraction of each backoff delay added at random, 0–1. Default: 0.3.\n *\n * Matters when the *server* is what went down: every client retries on the\n * same schedule, so the box comes back up into a synchronized stampede. Pass\n * `0` for a fixed schedule.\n */\n jitter?: number;\n /**\n * How long one handshake may stay in `CONNECTING` before the attempt is\n * abandoned and retried (ms). Default: 8000. Pass 0 to disable.\n *\n * A `WebSocket` that cannot reach its server does not necessarily fail: it\n * sits in `CONNECTING` firing neither `open` nor `close` nor `error`. A retry\n * chain built only on those events stops on its first hung attempt and never\n * moves again — and hung, rather than refused, is precisely how a bad mobile\n * link behaves, which is the case reconnection exists for.\n */\n handshakeTimeout?: number;\n /**\n * Silence tolerated on an open socket before the link is treated as dead (ms).\n * Default: 0 (off).\n *\n * The socket only reports a connection that closes cleanly. A link that dies\n * mid-flight leaves `readyState` at `OPEN` on this side with nothing ever\n * arriving again, so silence is the only symptom available. The timer is\n * re-armed by **any** inbound frame, not just by pings — traffic is traffic.\n *\n * Set it to a comfortable multiple of the server's ping interval (2.5× is a\n * good default) so one dropped ping is not mistaken for an outage. When the\n * server announces its own interval in the handshake, feed that back with\n * {@link WebSocketController.setSilenceTimeout} instead of hard-coding the\n * value on both ends.\n */\n silenceTimeout?: number;\n /**\n * Suspend the retry schedule while `navigator.onLine` is false, and resume on\n * the `online` event. Default: true.\n *\n * Burning retries against a radio that is switched off is how a phone\n * exhausts its budget inside a tunnel and gives up exactly when it comes out\n * the other side.\n */\n waitForOnline?: boolean;\n /**\n * Ping interval (ms). When set, the client sends `pingPayload` periodically\n * to keep the socket alive. Default: 0 (disabled).\n *\n * Leave it off against a `tempest-fastapi-sdk` server: that server pings on\n * its own and answers a client-sent `{\"type\":\"ping\"}` with nothing, while a\n * strict handler rejects the unknown frame. What it needs from the client\n * is the `pong` reply, which `respondToPing` sends for you.\n */\n pingInterval?: number;\n /** Payload sent on each ping. Default: `JSON.stringify({ type: \"ping\" })`. */\n pingPayload?: string | Blob | BufferSource;\n /**\n * Reply to a server `{\"type\":\"ping\"}` with `pongPayload`. Default: true.\n *\n * `tempest-fastapi-sdk` closes a socket with code `4408` when no `pong`\n * arrives within `WS_HEARTBEAT_TIMEOUT_SECONDS`, so a client that stays\n * silent is dropped once per timeout. The ping is still forwarded to\n * `onMessage` — the reply is sent before your handler runs.\n */\n respondToPing?: boolean;\n /** Payload sent in reply to a server ping. Default: `JSON.stringify({ type: \"pong\" })`. */\n pongPayload?: string | Blob | BufferSource;\n /**\n * Buffer payloads sent while the socket is not open and flush them on the\n * next `open`. Default: false — `send()` returns false and drops.\n *\n * Without it, an action fired during reconnect backoff vanishes and the UI\n * cannot tell \"never sent\" from \"sent and ignored\".\n */\n queueWhileClosed?: boolean;\n /** Cap on buffered payloads when `queueWhileClosed` is on. Default: 100. */\n maxQueuedMessages?: number;\n /** Parse incoming frames. Default: JSON with raw-string fallback. */\n parser?: (raw: string) => T;\n /**\n * A frame arrived that is not valid JSON, and no `parser` was supplied.\n *\n * Registering this drops the frame instead of delivering it: the previous\n * behaviour handed `onMessage` the raw `string` announced as your message\n * type, so the failure surfaced later, at the first property access, with\n * nothing left pointing at the parse. Leave it out and that behaviour is\n * kept, with a one-time warning in development builds.\n */\n onParseError?: (error: unknown, raw: string) => void;\n onOpen?: (event: Event) => void;\n onMessage?: (message: WebSocketMessage<T>) => void;\n onClose?: (event: CloseEvent) => void;\n onError?: (event: Event) => void;\n onStatusChange?: (status: WebSocketStatus) => void;\n /**\n * A retry has been scheduled. `attempt` is 1-based, `total` is `maxRetries`.\n *\n * Reconnecting is not an error and reads badly as one: announcing every\n * attempt puts a fresh \"the connection dropped\" in front of someone whose\n * session is in the middle of coming back on its own. Show a quiet\n * reconnecting state here and treat {@link CreateWebSocketOptions.onLost} as\n * the failure.\n */\n onReconnecting?: (attempt: number, total: number) => void;\n /**\n * The socket is back up after at least one retry.\n *\n * Nothing is resumed for you: a server that keys state by connection sees a\n * brand-new client, so this is where the caller re-subscribes, re-joins or\n * refetches whatever the gap invalidated.\n */\n onReconnected?: () => void;\n /**\n * No further attempt will be made — `\"rejected\"` when the server refused the\n * client outright (close code 4400–4499, minus the 4408 heartbeat timeout),\n * `\"exhausted\"` when the schedule ran out.\n *\n * This is the one that deserves UI, because it is the only state the caller\n * can act on: offer a \"try again\" that calls\n * {@link WebSocketController.reconnect}.\n */\n onLost?: (reason: WebSocketLostReason) => void;\n}\n\nexport interface WebSocketController {\n /** Send a payload over the current connection. No-op when not open. */\n send: (payload: string | Blob | BufferSource) => boolean;\n /** Close the connection and stop reconnecting. */\n close: (code?: number, reason?: string) => void;\n /** Force an immediate reconnect, resetting the retry counter. */\n reconnect: () => void;\n /**\n * Change the silence watchdog at runtime, in ms. `0` disables it.\n *\n * For the common case where the server announces its heartbeat interval in\n * the first frame, so the tolerated silence is not hard-coded on both ends:\n *\n * ```ts\n * onMessage: ({ data }) => {\n * if (data.type === \"welcome\") socket.setSilenceTimeout(data.heartbeat_seconds * 2500);\n * }\n * ```\n */\n setSilenceTimeout: (ms: number) => void;\n /**\n * Resolves on the first successful open, rejects when the socket is lost\n * before ever opening.\n *\n * Joining and dropping are different events: a call that never connected has\n * to be reported, while one that dropped mid-session should reconnect\n * quietly. Await this for the join, handle\n * {@link CreateWebSocketOptions.onLost} for the drop. Pair it with\n * `maxRetries: 0` when the first attempt should fail fast instead of\n * spending the whole schedule on a server that is not there.\n */\n opened: Promise<void>;\n /** Current connection status. */\n readonly status: WebSocketStatus;\n}\n\n/**\n * Open a WebSocket that survives a bad network: exponential backoff with jitter,\n * a handshake timeout, a silence watchdog, optional heartbeat pings and typed\n * JSON parsing.\n *\n * Three failure modes are covered that an event-driven retry loop misses on its\n * own, because none of them fire an event: a handshake that hangs instead of\n * failing, an open socket whose link died mid-flight, and a device with its\n * radio off burning the retry budget. See `handshakeTimeout`, `silenceTimeout`\n * and `waitForOnline`.\n *\n * @param url - Full ws:// or wss:// URL.\n * @param options - Connection configuration and callbacks.\n * @returns Controller exposing `send`, `close`, `reconnect`, `setSilenceTimeout`,\n * `opened` and `status`.\n *\n * @example\n * const socket = createWebSocket(url, {\n * silenceTimeout: 75_000,\n * onReconnecting: (n, total) => setBanner(`Reconectando ${n}/${total}…`),\n * onReconnected: () => refetchEverything(),\n * onLost: (reason) => setBanner(reason === \"rejected\" ? \"Acesso negado\" : \"Sem conexão\"),\n * });\n * await socket.opened;\n */\nexport function createWebSocket<T = unknown>(\n url: string,\n options: CreateWebSocketOptions<T> = {},\n): WebSocketController {\n const {\n protocols,\n maxRetries = 10,\n initialBackoff = 1000,\n maxBackoff = 30000,\n jitter = 0.3,\n handshakeTimeout = 8000,\n silenceTimeout = 0,\n waitForOnline = true,\n pingInterval = 0,\n pingPayload = JSON.stringify({ type: \"ping\" }),\n respondToPing = true,\n pongPayload = JSON.stringify({ type: \"pong\" }),\n queueWhileClosed = false,\n maxQueuedMessages = 100,\n parser,\n onOpen,\n onMessage,\n onClose,\n onError,\n onParseError,\n onStatusChange,\n onReconnecting,\n onReconnected,\n onLost,\n } = options;\n\n let socket: WebSocket | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let pingTimer: ReturnType<typeof setInterval> | null = null;\n let handshakeTimer: ReturnType<typeof setTimeout> | null = null;\n let silenceTimer: ReturnType<typeof setTimeout> | null = null;\n let onlineListener: (() => void) | null = null;\n let silenceWindow = silenceTimeout;\n let retries = 0;\n let status: WebSocketStatus = \"idle\";\n let closed = false;\n let everOpened = false;\n const outbox: Array<string | Blob | BufferSource> = [];\n\n let settleOpen: (() => void) | null = null;\n let failOpen: ((error: Error) => void) | null = null;\n const opened = new Promise<void>((resolve, reject) => {\n settleOpen = resolve;\n failOpen = reject;\n });\n opened.catch(() => undefined);\n\n /** True for a decoded frame that is the server's heartbeat ping. */\n function isServerPing(data: unknown): boolean {\n return (\n typeof data === \"object\" &&\n data !== null &&\n (data as { type?: unknown }).type === \"ping\"\n );\n }\n\n /** Send everything buffered while the socket was down, oldest first. */\n function flushOutbox(ws: WebSocket): void {\n while (outbox.length > 0 && ws.readyState === WebSocket.OPEN) {\n ws.send(outbox.shift()!);\n }\n }\n\n function setStatus(next: WebSocketStatus): void {\n if (status === next) return;\n status = next;\n onStatusChange?.(next);\n }\n\n function clearPing(): void {\n if (pingTimer) {\n clearInterval(pingTimer);\n pingTimer = null;\n }\n }\n\n function clearHandshake(): void {\n if (handshakeTimer) {\n clearTimeout(handshakeTimer);\n handshakeTimer = null;\n }\n }\n\n function clearSilence(): void {\n if (silenceTimer) {\n clearTimeout(silenceTimer);\n silenceTimer = null;\n }\n }\n\n function startPing(): void {\n if (!pingInterval || pingInterval <= 0) return;\n clearPing();\n pingTimer = setInterval(() => {\n if (socket?.readyState === WebSocket.OPEN) {\n socket.send(pingPayload);\n }\n }, pingInterval);\n }\n\n /**\n * Restart the silence timer, because something just arrived.\n *\n * Armed off any inbound frame rather than off pongs alone: a busy exchange\n * already proves the link is carrying data, and a protocol whose pings the\n * client never sees would otherwise reconnect in the middle of working\n * traffic.\n */\n function armSilence(): void {\n clearSilence();\n if (closed || silenceWindow <= 0) return;\n silenceTimer = setTimeout(onSilence, silenceWindow);\n }\n\n /**\n * Treat a socket that went quiet as dead and start reconnecting.\n *\n * Handlers are detached before closing so the synthetic `close` does not also\n * schedule a retry — that would advance the backoff twice for one failure and\n * halve the time the connection is given to recover.\n */\n function onSilence(): void {\n clearSilence();\n if (closed || !socket) return;\n detach(socket);\n socket = null;\n setStatus(\"closed\");\n scheduleReconnect();\n }\n\n /**\n * Abandon a handshake that never resolved either way.\n *\n * The socket is closed while still `CONNECTING`, which is the one case the\n * console warns about — and the right trade here, because the alternative is\n * deferring the close to an `open` event that by definition is not coming.\n */\n function abandonHandshake(ws: WebSocket): void {\n clearHandshake();\n if (closed || ws.readyState !== WebSocket.CONNECTING) return;\n detach(ws);\n if (socket === ws) socket = null;\n setStatus(\"closed\");\n scheduleReconnect();\n }\n\n /** Drop every handler and close, so the socket can die without being heard. */\n function detach(ws: WebSocket): void {\n ws.onopen = null;\n ws.onmessage = null;\n ws.onerror = null;\n ws.onclose = null;\n try {\n ws.close();\n } catch {\n /* already unusable — nothing to release and nothing to report */\n }\n }\n\n /** Stop for good, telling the caller which of the two dead ends it is. */\n function lose(reason: WebSocketLostReason): void {\n clearSilence();\n clearHandshake();\n clearNetworkWait();\n if (reason === \"rejected\") closed = true;\n setStatus(\"error\");\n if (!everOpened && failOpen) {\n const reject = failOpen;\n failOpen = null;\n settleOpen = null;\n reject(new Error(`websocket_${reason}`));\n }\n onLost?.(reason);\n }\n\n /**\n * Queue the next attempt, or wait for the network when there is none.\n *\n * While the browser reports no connectivity the schedule is suspended and the\n * `online` event drives the next attempt instead, so a device in a tunnel\n * does not spend its whole budget before coming out the other side.\n */\n function scheduleReconnect(): void {\n if (closed) return;\n if (retries >= maxRetries) {\n lose(\"exhausted\");\n return;\n }\n const delay = backoffDelay(retries, { initialBackoff, maxBackoff, jitter });\n retries += 1;\n onReconnecting?.(retries, maxRetries);\n\n if (waitForOnline && typeof navigator !== \"undefined\" && navigator.onLine === false) {\n waitForNetwork();\n return;\n }\n retryTimer = setTimeout(connect, delay);\n }\n\n function waitForNetwork(): void {\n if (onlineListener || typeof window === \"undefined\") return;\n const listener = (): void => {\n clearNetworkWait();\n if (!closed) connect();\n };\n onlineListener = listener;\n window.addEventListener(\"online\", listener);\n }\n\n function clearNetworkWait(): void {\n if (!onlineListener || typeof window === \"undefined\") return;\n window.removeEventListener(\"online\", onlineListener);\n onlineListener = null;\n }\n\n /**\n * Open a socket, replacing whatever is there.\n *\n * The handshake timer is cleared first because it belongs to the socket\n * being replaced, and it holds a reference to it: left armed, it fires later\n * against a connection nobody is waiting for, clears the *new* socket's\n * timer on its way through, and schedules a retry that drops a connection\n * still in flight. `reconnect()` on a hung socket is the path that reaches\n * it.\n */\n function connect(): void {\n if (closed) return;\n retryTimer = null;\n clearHandshake();\n if (socket) {\n const previous = socket;\n previous.onmessage = null;\n previous.onclose = null;\n previous.onerror = null;\n if (previous.readyState !== WebSocket.CONNECTING) previous.onopen = null;\n closeSocket(previous);\n }\n setStatus(\"connecting\");\n\n const ws = new WebSocket(url, protocols);\n socket = ws;\n\n if (handshakeTimeout > 0) {\n handshakeTimer = setTimeout(() => abandonHandshake(ws), handshakeTimeout);\n }\n\n ws.onopen = (event) => {\n clearHandshake();\n const recovered = retries > 0;\n retries = 0;\n everOpened = true;\n setStatus(\"open\");\n startPing();\n armSilence();\n flushOutbox(ws);\n if (settleOpen) {\n const resolve = settleOpen;\n settleOpen = null;\n failOpen = null;\n resolve();\n }\n onOpen?.(event);\n if (recovered) onReconnected?.();\n };\n\n ws.onmessage = (event) => {\n armSilence();\n const raw = typeof event.data === \"string\" ? event.data : \"\";\n const decoded = decodeFrame<T>(raw, parser, onParseError, \"createWebSocket\");\n if (!decoded.delivered) return;\n const data = decoded.data;\n if (respondToPing && isServerPing(data) && ws.readyState === WebSocket.OPEN) {\n ws.send(pongPayload);\n }\n onMessage?.({ data, raw: event });\n };\n\n ws.onerror = (event) => {\n onError?.(event);\n };\n\n /**\n * Classify the close before deciding anything.\n *\n * Three outcomes, in order: a refusal never gets better by trying again;\n * a died-in-flight or temporarily-unavailable close is retried; an\n * ordinary goodbye (a clean 1000) is the session ending on purpose and\n * deserves no error. The one exception is a goodbye on a socket that\n * never opened — the server hung up during the handshake, which the\n * caller awaiting `opened` has to hear about.\n */\n ws.onclose = (event) => {\n clearHandshake();\n clearPing();\n clearSilence();\n onClose?.(event);\n socket = null;\n setStatus(\"closed\");\n if (closed) return;\n if (isRejectionCloseCode(event.code)) {\n lose(\"rejected\");\n return;\n }\n if (shouldRetryClose(event.code, event.wasClean)) {\n scheduleReconnect();\n return;\n }\n if (!everOpened) lose(\"rejected\");\n };\n }\n\n function send(payload: string | Blob | BufferSource): boolean {\n if (socket?.readyState === WebSocket.OPEN) {\n socket.send(payload);\n return true;\n }\n if (!queueWhileClosed || closed) return false;\n if (outbox.length >= maxQueuedMessages) outbox.shift();\n outbox.push(payload);\n return true;\n }\n\n function close(code?: number, reason?: string): void {\n closed = true;\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n clearPing();\n clearHandshake();\n clearSilence();\n clearNetworkWait();\n retries = 0;\n outbox.length = 0;\n if (failOpen) {\n const reject = failOpen;\n failOpen = null;\n settleOpen = null;\n reject(new Error(\"websocket_closed\"));\n }\n if (socket) {\n setStatus(\"closing\");\n closeSocket(socket, code, reason);\n socket = null;\n }\n setStatus(\"closed\");\n }\n\n /**\n * Close a socket without the \"closed before the connection is established\"\n * console warning.\n *\n * A socket still in `CONNECTING` cannot be closed cleanly — the browser\n * logs that warning on every attempt. React's StrictMode mounts, unmounts\n * and remounts each component in development, so the first socket is\n * always torn down mid-handshake and the message shows up in every dev\n * session of every app using the hook. Deferring the close to `onopen`\n * costs one round trip and keeps the console usable.\n */\n function closeSocket(ws: WebSocket, code?: number, reason?: string): void {\n if (ws.readyState === WebSocket.CONNECTING) {\n ws.onopen = () => ws.close(code, reason);\n ws.onmessage = null;\n ws.onerror = null;\n ws.onclose = null;\n return;\n }\n ws.close(code, reason);\n }\n\n function reconnect(): void {\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n clearNetworkWait();\n retries = 0;\n closed = false;\n connect();\n }\n\n function setSilenceTimeout(ms: number): void {\n silenceWindow = Number.isFinite(ms) && ms > 0 ? ms : 0;\n if (socket?.readyState === WebSocket.OPEN) armSilence();\n else clearSilence();\n }\n\n connect();\n\n return {\n send,\n close,\n reconnect,\n setSilenceTimeout,\n opened,\n get status() {\n return status;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-web-socket.cjs","names":[],"sources":["../../src/ws/create-web-socket.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — reconnect with backoff, heartbeat,\n * the handshake and silence timers that detect a link which never fails out loud,\n * the send queue that survives a disconnect and the listener set that must be re-\n * attached to each new socket — one connection's lifetime, one closure. The queue\n * and the reconnect timer are the same decision seen twice.\n */\nimport {\n backoffDelay,\n isRejectionCloseCode,\n shouldRetryClose,\n type WebSocketLostReason,\n} from \"./resilience\";\nimport { decodeFrame } from \"../utils/json-frame\";\nimport type { SchemaIssue, SchemaLike } from \"../utils/schema-like\";\n\nexport type WebSocketStatus = \"idle\" | \"connecting\" | \"open\" | \"closing\" | \"closed\" | \"error\";\n\nexport interface WebSocketMessage<T> {\n /** Parsed payload — validated when `schema` is set, JSON-decoded when possible, raw string otherwise. */\n data: T;\n /** The original `MessageEvent`. */\n raw: MessageEvent;\n}\n\nexport interface CreateWebSocketOptions<T> {\n /** Subprotocol(s) forwarded to the `WebSocket` constructor. */\n protocols?: string | string[];\n /** Max reconnect attempts. Default: 10. Pass 0 to disable. */\n maxRetries?: number;\n /** Initial backoff (ms). Doubles each attempt, capped at `maxBackoff`. Default: 1000. */\n initialBackoff?: number;\n /** Maximum backoff (ms). Default: 30000. */\n maxBackoff?: number;\n /**\n * Fraction of each backoff delay added at random, 0–1. Default: 0.3.\n *\n * Matters when the *server* is what went down: every client retries on the\n * same schedule, so the box comes back up into a synchronized stampede. Pass\n * `0` for a fixed schedule.\n */\n jitter?: number;\n /**\n * How long one handshake may stay in `CONNECTING` before the attempt is\n * abandoned and retried (ms). Default: 8000. Pass 0 to disable.\n *\n * A `WebSocket` that cannot reach its server does not necessarily fail: it\n * sits in `CONNECTING` firing neither `open` nor `close` nor `error`. A retry\n * chain built only on those events stops on its first hung attempt and never\n * moves again — and hung, rather than refused, is precisely how a bad mobile\n * link behaves, which is the case reconnection exists for.\n */\n handshakeTimeout?: number;\n /**\n * Silence tolerated on an open socket before the link is treated as dead (ms).\n * Default: 0 (off).\n *\n * The socket only reports a connection that closes cleanly. A link that dies\n * mid-flight leaves `readyState` at `OPEN` on this side with nothing ever\n * arriving again, so silence is the only symptom available. The timer is\n * re-armed by **any** inbound frame, not just by pings — traffic is traffic.\n *\n * Set it to a comfortable multiple of the server's ping interval (2.5× is a\n * good default) so one dropped ping is not mistaken for an outage. When the\n * server announces its own interval in the handshake, feed that back with\n * {@link WebSocketController.setSilenceTimeout} instead of hard-coding the\n * value on both ends.\n */\n silenceTimeout?: number;\n /**\n * Suspend the retry schedule while `navigator.onLine` is false, and resume on\n * the `online` event. Default: true.\n *\n * Burning retries against a radio that is switched off is how a phone\n * exhausts its budget inside a tunnel and gives up exactly when it comes out\n * the other side.\n */\n waitForOnline?: boolean;\n /**\n * Ping interval (ms). When set, the client sends `pingPayload` periodically\n * to keep the socket alive. Default: 0 (disabled).\n *\n * Leave it off against a `tempest-fastapi-sdk` server: that server pings on\n * its own and answers a client-sent `{\"type\":\"ping\"}` with nothing, while a\n * strict handler rejects the unknown frame. What it needs from the client\n * is the `pong` reply, which `respondToPing` sends for you.\n */\n pingInterval?: number;\n /** Payload sent on each ping. Default: `JSON.stringify({ type: \"ping\" })`. */\n pingPayload?: string | Blob | BufferSource;\n /**\n * Reply to a server `{\"type\":\"ping\"}` with `pongPayload`. Default: true.\n *\n * `tempest-fastapi-sdk` closes a socket with code `4408` when no `pong`\n * arrives within `WS_HEARTBEAT_TIMEOUT_SECONDS`, so a client that stays\n * silent is dropped once per timeout. The ping is still forwarded to\n * `onMessage` — the reply is sent before your handler runs.\n */\n respondToPing?: boolean;\n /** Payload sent in reply to a server ping. Default: `JSON.stringify({ type: \"pong\" })`. */\n pongPayload?: string | Blob | BufferSource;\n /**\n * Buffer payloads sent while the socket is not open and flush them on the\n * next `open`. Default: false — `send()` returns false and drops.\n *\n * Without it, an action fired during reconnect backoff vanishes and the UI\n * cannot tell \"never sent\" from \"sent and ignored\".\n */\n queueWhileClosed?: boolean;\n /** Cap on buffered payloads when `queueWhileClosed` is on. Default: 100. */\n maxQueuedMessages?: number;\n /** Parse incoming frames. Default: JSON with raw-string fallback. */\n parser?: (raw: string) => T;\n /**\n * A frame arrived that is not valid JSON, and no `parser` was supplied.\n *\n * Registering this drops the frame instead of delivering it: the previous\n * behaviour handed `onMessage` the raw `string` announced as your message\n * type, so the failure surfaced later, at the first property access, with\n * nothing left pointing at the parse. Leave it out and that behaviour is\n * kept, with a one-time warning in development builds.\n */\n onParseError?: (error: unknown, raw: string) => void;\n /**\n * Schema every decoded frame must satisfy, from zod, valibot, arktype or\n * anything else exposing `~standard` or `.safeParse`.\n *\n * Without it nothing changes: the payload reaches `onMessage` announced as\n * `T` on the strength of the type argument alone, which is a promise about\n * the server that TypeScript cannot keep. With it, a frame that does not\n * match is **not** delivered — the same rule `onParseError` already follows —\n * and `onValidationError` hears why. The value delivered is the schema's\n * output, so coercions and defaults are honoured.\n *\n * When `parser` is also supplied, it decodes first and the schema validates\n * what it returned.\n *\n * A server ping is still answered when the schema drops it: the heartbeat is\n * the transport's contract with the server, not the app's with its payload,\n * and a socket that stops sending `pong` is closed with `4408` once per\n * timeout. The validation itself must be synchronous — a frame is decoded\n * inside the `message` handler and delivered from it, so an async schema\n * would deliver frames in whatever order their validations settled; that\n * case is reported through `onValidationError` instead of awaited.\n */\n schema?: SchemaLike<T>;\n /**\n * A frame was decoded but the `schema` refused it, so it was dropped.\n *\n * The one signal that does not depend on how the app's bundler resolves\n * `process`: the one-time development warning behind `onParseError` needs\n * `isDevBuild()` to be able to answer, and this callback is the app's own.\n */\n onValidationError?: (issues: SchemaIssue[], raw: string) => void;\n onOpen?: (event: Event) => void;\n onMessage?: (message: WebSocketMessage<T>) => void;\n onClose?: (event: CloseEvent) => void;\n onError?: (event: Event) => void;\n onStatusChange?: (status: WebSocketStatus) => void;\n /**\n * A retry has been scheduled. `attempt` is 1-based, `total` is `maxRetries`.\n *\n * Reconnecting is not an error and reads badly as one: announcing every\n * attempt puts a fresh \"the connection dropped\" in front of someone whose\n * session is in the middle of coming back on its own. Show a quiet\n * reconnecting state here and treat {@link CreateWebSocketOptions.onLost} as\n * the failure.\n */\n onReconnecting?: (attempt: number, total: number) => void;\n /**\n * The socket is back up after at least one retry.\n *\n * Nothing is resumed for you: a server that keys state by connection sees a\n * brand-new client, so this is where the caller re-subscribes, re-joins or\n * refetches whatever the gap invalidated.\n */\n onReconnected?: () => void;\n /**\n * No further attempt will be made — `\"rejected\"` when the server refused the\n * client outright (close code 4400–4499, minus the 4408 heartbeat timeout),\n * `\"exhausted\"` when the schedule ran out.\n *\n * This is the one that deserves UI, because it is the only state the caller\n * can act on: offer a \"try again\" that calls\n * {@link WebSocketController.reconnect}.\n */\n onLost?: (reason: WebSocketLostReason) => void;\n}\n\nexport interface WebSocketController {\n /** Send a payload over the current connection. No-op when not open. */\n send: (payload: string | Blob | BufferSource) => boolean;\n /** Close the connection and stop reconnecting. */\n close: (code?: number, reason?: string) => void;\n /** Force an immediate reconnect, resetting the retry counter. */\n reconnect: () => void;\n /**\n * Change the silence watchdog at runtime, in ms. `0` disables it.\n *\n * For the common case where the server announces its heartbeat interval in\n * the first frame, so the tolerated silence is not hard-coded on both ends:\n *\n * ```ts\n * onMessage: ({ data }) => {\n * if (data.type === \"welcome\") socket.setSilenceTimeout(data.heartbeat_seconds * 2500);\n * }\n * ```\n */\n setSilenceTimeout: (ms: number) => void;\n /**\n * Resolves on the first successful open, rejects when the socket is lost\n * before ever opening.\n *\n * Joining and dropping are different events: a call that never connected has\n * to be reported, while one that dropped mid-session should reconnect\n * quietly. Await this for the join, handle\n * {@link CreateWebSocketOptions.onLost} for the drop. Pair it with\n * `maxRetries: 0` when the first attempt should fail fast instead of\n * spending the whole schedule on a server that is not there.\n */\n opened: Promise<void>;\n /** Current connection status. */\n readonly status: WebSocketStatus;\n}\n\n/**\n * Open a WebSocket that survives a bad network: exponential backoff with jitter,\n * a handshake timeout, a silence watchdog, optional heartbeat pings and typed\n * JSON parsing.\n *\n * Three failure modes are covered that an event-driven retry loop misses on its\n * own, because none of them fire an event: a handshake that hangs instead of\n * failing, an open socket whose link died mid-flight, and a device with its\n * radio off burning the retry budget. See `handshakeTimeout`, `silenceTimeout`\n * and `waitForOnline`.\n *\n * @param url - Full ws:// or wss:// URL.\n * @param options - Connection configuration and callbacks.\n * @returns Controller exposing `send`, `close`, `reconnect`, `setSilenceTimeout`,\n * `opened` and `status`.\n *\n * @example\n * const socket = createWebSocket(url, {\n * silenceTimeout: 75_000,\n * onReconnecting: (n, total) => setBanner(`Reconectando ${n}/${total}…`),\n * onReconnected: () => refetchEverything(),\n * onLost: (reason) => setBanner(reason === \"rejected\" ? \"Acesso negado\" : \"Sem conexão\"),\n * });\n * await socket.opened;\n */\nexport function createWebSocket<T = unknown>(\n url: string,\n options: CreateWebSocketOptions<T> = {},\n): WebSocketController {\n const {\n protocols,\n maxRetries = 10,\n initialBackoff = 1000,\n maxBackoff = 30000,\n jitter = 0.3,\n handshakeTimeout = 8000,\n silenceTimeout = 0,\n waitForOnline = true,\n pingInterval = 0,\n pingPayload = JSON.stringify({ type: \"ping\" }),\n respondToPing = true,\n pongPayload = JSON.stringify({ type: \"pong\" }),\n queueWhileClosed = false,\n maxQueuedMessages = 100,\n parser,\n onOpen,\n onMessage,\n onClose,\n onError,\n onParseError,\n schema,\n onValidationError,\n onStatusChange,\n onReconnecting,\n onReconnected,\n onLost,\n } = options;\n\n let socket: WebSocket | null = null;\n let retryTimer: ReturnType<typeof setTimeout> | null = null;\n let pingTimer: ReturnType<typeof setInterval> | null = null;\n let handshakeTimer: ReturnType<typeof setTimeout> | null = null;\n let silenceTimer: ReturnType<typeof setTimeout> | null = null;\n let onlineListener: (() => void) | null = null;\n let silenceWindow = silenceTimeout;\n let retries = 0;\n let status: WebSocketStatus = \"idle\";\n let closed = false;\n let everOpened = false;\n const outbox: Array<string | Blob | BufferSource> = [];\n\n let settleOpen: (() => void) | null = null;\n let failOpen: ((error: Error) => void) | null = null;\n const opened = new Promise<void>((resolve, reject) => {\n settleOpen = resolve;\n failOpen = reject;\n });\n opened.catch(() => undefined);\n\n /** True for a decoded frame that is the server's heartbeat ping. */\n function isServerPing(data: unknown): boolean {\n return (\n typeof data === \"object\" &&\n data !== null &&\n (data as { type?: unknown }).type === \"ping\"\n );\n }\n\n /**\n * Whether an undelivered frame was a server ping, read from its raw text.\n *\n * The delivered path tests the decoded payload, which is what `parser` was\n * given the frame to produce. A frame `schema` refused never reaches that\n * path, and a heartbeat the app's schema does not describe is the normal\n * case rather than an exotic one — so the reply is decided from the wire\n * text instead, since the server closes with `4408` when no `pong` arrives.\n *\n * The substring test in front keeps the ordinary frame at one scan: only a\n * frame that mentions `\"ping\"` at all is worth parsing a second time.\n *\n * @param raw - The frame body as text.\n * @returns Whether a `pong` is owed.\n */\n function isServerPingFrame(raw: string): boolean {\n if (!raw.includes('\"ping\"')) return false;\n try {\n return isServerPing(JSON.parse(raw));\n } catch {\n return false;\n }\n }\n\n /** Send everything buffered while the socket was down, oldest first. */\n function flushOutbox(ws: WebSocket): void {\n while (outbox.length > 0 && ws.readyState === WebSocket.OPEN) {\n ws.send(outbox.shift()!);\n }\n }\n\n function setStatus(next: WebSocketStatus): void {\n if (status === next) return;\n status = next;\n onStatusChange?.(next);\n }\n\n function clearPing(): void {\n if (pingTimer) {\n clearInterval(pingTimer);\n pingTimer = null;\n }\n }\n\n function clearHandshake(): void {\n if (handshakeTimer) {\n clearTimeout(handshakeTimer);\n handshakeTimer = null;\n }\n }\n\n function clearSilence(): void {\n if (silenceTimer) {\n clearTimeout(silenceTimer);\n silenceTimer = null;\n }\n }\n\n function startPing(): void {\n if (!pingInterval || pingInterval <= 0) return;\n clearPing();\n pingTimer = setInterval(() => {\n if (socket?.readyState === WebSocket.OPEN) {\n socket.send(pingPayload);\n }\n }, pingInterval);\n }\n\n /**\n * Restart the silence timer, because something just arrived.\n *\n * Armed off any inbound frame rather than off pongs alone: a busy exchange\n * already proves the link is carrying data, and a protocol whose pings the\n * client never sees would otherwise reconnect in the middle of working\n * traffic.\n */\n function armSilence(): void {\n clearSilence();\n if (closed || silenceWindow <= 0) return;\n silenceTimer = setTimeout(onSilence, silenceWindow);\n }\n\n /**\n * Treat a socket that went quiet as dead and start reconnecting.\n *\n * Handlers are detached before closing so the synthetic `close` does not also\n * schedule a retry — that would advance the backoff twice for one failure and\n * halve the time the connection is given to recover.\n */\n function onSilence(): void {\n clearSilence();\n if (closed || !socket) return;\n detach(socket);\n socket = null;\n setStatus(\"closed\");\n scheduleReconnect();\n }\n\n /**\n * Abandon a handshake that never resolved either way.\n *\n * The socket is closed while still `CONNECTING`, which is the one case the\n * console warns about — and the right trade here, because the alternative is\n * deferring the close to an `open` event that by definition is not coming.\n */\n function abandonHandshake(ws: WebSocket): void {\n clearHandshake();\n if (closed || ws.readyState !== WebSocket.CONNECTING) return;\n detach(ws);\n if (socket === ws) socket = null;\n setStatus(\"closed\");\n scheduleReconnect();\n }\n\n /** Drop every handler and close, so the socket can die without being heard. */\n function detach(ws: WebSocket): void {\n ws.onopen = null;\n ws.onmessage = null;\n ws.onerror = null;\n ws.onclose = null;\n try {\n ws.close();\n } catch {\n /* already unusable — nothing to release and nothing to report */\n }\n }\n\n /** Stop for good, telling the caller which of the two dead ends it is. */\n function lose(reason: WebSocketLostReason): void {\n clearSilence();\n clearHandshake();\n clearNetworkWait();\n if (reason === \"rejected\") closed = true;\n setStatus(\"error\");\n if (!everOpened && failOpen) {\n const reject = failOpen;\n failOpen = null;\n settleOpen = null;\n reject(new Error(`websocket_${reason}`));\n }\n onLost?.(reason);\n }\n\n /**\n * Queue the next attempt, or wait for the network when there is none.\n *\n * While the browser reports no connectivity the schedule is suspended and the\n * `online` event drives the next attempt instead, so a device in a tunnel\n * does not spend its whole budget before coming out the other side.\n */\n function scheduleReconnect(): void {\n if (closed) return;\n if (retries >= maxRetries) {\n lose(\"exhausted\");\n return;\n }\n const delay = backoffDelay(retries, { initialBackoff, maxBackoff, jitter });\n retries += 1;\n onReconnecting?.(retries, maxRetries);\n\n if (waitForOnline && typeof navigator !== \"undefined\" && navigator.onLine === false) {\n waitForNetwork();\n return;\n }\n retryTimer = setTimeout(connect, delay);\n }\n\n function waitForNetwork(): void {\n if (onlineListener || typeof window === \"undefined\") return;\n const listener = (): void => {\n clearNetworkWait();\n if (!closed) connect();\n };\n onlineListener = listener;\n window.addEventListener(\"online\", listener);\n }\n\n function clearNetworkWait(): void {\n if (!onlineListener || typeof window === \"undefined\") return;\n window.removeEventListener(\"online\", onlineListener);\n onlineListener = null;\n }\n\n /**\n * Open a socket, replacing whatever is there.\n *\n * The handshake timer is cleared first because it belongs to the socket\n * being replaced, and it holds a reference to it: left armed, it fires later\n * against a connection nobody is waiting for, clears the *new* socket's\n * timer on its way through, and schedules a retry that drops a connection\n * still in flight. `reconnect()` on a hung socket is the path that reaches\n * it.\n */\n function connect(): void {\n if (closed) return;\n retryTimer = null;\n clearHandshake();\n if (socket) {\n const previous = socket;\n previous.onmessage = null;\n previous.onclose = null;\n previous.onerror = null;\n if (previous.readyState !== WebSocket.CONNECTING) previous.onopen = null;\n closeSocket(previous);\n }\n setStatus(\"connecting\");\n\n const ws = new WebSocket(url, protocols);\n socket = ws;\n\n if (handshakeTimeout > 0) {\n handshakeTimer = setTimeout(() => abandonHandshake(ws), handshakeTimeout);\n }\n\n ws.onopen = (event) => {\n clearHandshake();\n const recovered = retries > 0;\n retries = 0;\n everOpened = true;\n setStatus(\"open\");\n startPing();\n armSilence();\n flushOutbox(ws);\n if (settleOpen) {\n const resolve = settleOpen;\n settleOpen = null;\n failOpen = null;\n resolve();\n }\n onOpen?.(event);\n if (recovered) onReconnected?.();\n };\n\n ws.onmessage = (event) => {\n armSilence();\n const raw = typeof event.data === \"string\" ? event.data : \"\";\n const decoded = decodeFrame<T>(raw, \"createWebSocket\", {\n parser,\n onParseError,\n schema,\n onValidationError,\n });\n if (!decoded.delivered) {\n if (respondToPing && ws.readyState === WebSocket.OPEN && isServerPingFrame(raw)) {\n ws.send(pongPayload);\n }\n return;\n }\n const data = decoded.data;\n if (respondToPing && isServerPing(data) && ws.readyState === WebSocket.OPEN) {\n ws.send(pongPayload);\n }\n onMessage?.({ data, raw: event });\n };\n\n ws.onerror = (event) => {\n onError?.(event);\n };\n\n /**\n * Classify the close before deciding anything.\n *\n * Three outcomes, in order: a refusal never gets better by trying again;\n * a died-in-flight or temporarily-unavailable close is retried; an\n * ordinary goodbye (a clean 1000) is the session ending on purpose and\n * deserves no error. The one exception is a goodbye on a socket that\n * never opened — the server hung up during the handshake, which the\n * caller awaiting `opened` has to hear about.\n */\n ws.onclose = (event) => {\n clearHandshake();\n clearPing();\n clearSilence();\n onClose?.(event);\n socket = null;\n setStatus(\"closed\");\n if (closed) return;\n if (isRejectionCloseCode(event.code)) {\n lose(\"rejected\");\n return;\n }\n if (shouldRetryClose(event.code, event.wasClean)) {\n scheduleReconnect();\n return;\n }\n if (!everOpened) lose(\"rejected\");\n };\n }\n\n function send(payload: string | Blob | BufferSource): boolean {\n if (socket?.readyState === WebSocket.OPEN) {\n socket.send(payload);\n return true;\n }\n if (!queueWhileClosed || closed) return false;\n if (outbox.length >= maxQueuedMessages) outbox.shift();\n outbox.push(payload);\n return true;\n }\n\n function close(code?: number, reason?: string): void {\n closed = true;\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n clearPing();\n clearHandshake();\n clearSilence();\n clearNetworkWait();\n retries = 0;\n outbox.length = 0;\n if (failOpen) {\n const reject = failOpen;\n failOpen = null;\n settleOpen = null;\n reject(new Error(\"websocket_closed\"));\n }\n if (socket) {\n setStatus(\"closing\");\n closeSocket(socket, code, reason);\n socket = null;\n }\n setStatus(\"closed\");\n }\n\n /**\n * Close a socket without the \"closed before the connection is established\"\n * console warning.\n *\n * A socket still in `CONNECTING` cannot be closed cleanly — the browser\n * logs that warning on every attempt. React's StrictMode mounts, unmounts\n * and remounts each component in development, so the first socket is\n * always torn down mid-handshake and the message shows up in every dev\n * session of every app using the hook. Deferring the close to `onopen`\n * costs one round trip and keeps the console usable.\n */\n function closeSocket(ws: WebSocket, code?: number, reason?: string): void {\n if (ws.readyState === WebSocket.CONNECTING) {\n ws.onopen = () => ws.close(code, reason);\n ws.onmessage = null;\n ws.onerror = null;\n ws.onclose = null;\n return;\n }\n ws.close(code, reason);\n }\n\n function reconnect(): void {\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n clearNetworkWait();\n retries = 0;\n closed = false;\n connect();\n }\n\n function setSilenceTimeout(ms: number): void {\n silenceWindow = Number.isFinite(ms) && ms > 0 ? ms : 0;\n if (socket?.readyState === WebSocket.OPEN) armSilence();\n else clearSilence();\n }\n\n connect();\n\n return {\n send,\n close,\n reconnect,\n setSilenceTimeout,\n opened,\n get status() {\n return status;\n },\n };\n}\n"],"mappings":"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"}