@gusnips/react 0.1.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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +180 -0
  3. package/dist/api-client.d.ts +139 -0
  4. package/dist/api-client.d.ts.map +1 -0
  5. package/dist/api-client.js +133 -0
  6. package/dist/api-client.js.map +1 -0
  7. package/dist/api-error.d.ts +45 -0
  8. package/dist/api-error.d.ts.map +1 -0
  9. package/dist/api-error.js +52 -0
  10. package/dist/api-error.js.map +1 -0
  11. package/dist/auth-store.d.ts +49 -0
  12. package/dist/auth-store.d.ts.map +1 -0
  13. package/dist/auth-store.js +27 -0
  14. package/dist/auth-store.js.map +1 -0
  15. package/dist/chunk-reload.d.ts +61 -0
  16. package/dist/chunk-reload.d.ts.map +1 -0
  17. package/dist/chunk-reload.js +92 -0
  18. package/dist/chunk-reload.js.map +1 -0
  19. package/dist/cn.d.ts +13 -0
  20. package/dist/cn.d.ts.map +1 -0
  21. package/dist/cn.js +16 -0
  22. package/dist/cn.js.map +1 -0
  23. package/dist/describe-error.d.ts +74 -0
  24. package/dist/describe-error.d.ts.map +1 -0
  25. package/dist/describe-error.js +70 -0
  26. package/dist/describe-error.js.map +1 -0
  27. package/dist/error-boundary.d.ts +61 -0
  28. package/dist/error-boundary.d.ts.map +1 -0
  29. package/dist/error-boundary.js +32 -0
  30. package/dist/error-boundary.js.map +1 -0
  31. package/dist/guards.d.ts +65 -0
  32. package/dist/guards.d.ts.map +1 -0
  33. package/dist/guards.js +52 -0
  34. package/dist/guards.js.map +1 -0
  35. package/dist/hydrate.d.ts +30 -0
  36. package/dist/hydrate.d.ts.map +1 -0
  37. package/dist/hydrate.js +25 -0
  38. package/dist/hydrate.js.map +1 -0
  39. package/dist/i18n.d.ts +70 -0
  40. package/dist/i18n.d.ts.map +1 -0
  41. package/dist/i18n.js +66 -0
  42. package/dist/i18n.js.map +1 -0
  43. package/dist/index.d.ts +12 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +31 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/prerender-contract.d.ts +30 -0
  48. package/dist/prerender-contract.d.ts.map +1 -0
  49. package/dist/prerender-contract.js +30 -0
  50. package/dist/prerender-contract.js.map +1 -0
  51. package/dist/query.d.ts +28 -0
  52. package/dist/query.d.ts.map +1 -0
  53. package/dist/query.js +51 -0
  54. package/dist/query.js.map +1 -0
  55. package/dist/sse.d.ts +64 -0
  56. package/dist/sse.d.ts.map +1 -0
  57. package/dist/sse.js +105 -0
  58. package/dist/sse.js.map +1 -0
  59. package/dist/states.d.ts +69 -0
  60. package/dist/states.d.ts.map +1 -0
  61. package/dist/states.js +2 -0
  62. package/dist/states.js.map +1 -0
  63. package/dist/ui/class-name.d.ts +16 -0
  64. package/dist/ui/class-name.d.ts.map +1 -0
  65. package/dist/ui/class-name.js +13 -0
  66. package/dist/ui/class-name.js.map +1 -0
  67. package/dist/ui/combobox.d.ts +81 -0
  68. package/dist/ui/combobox.d.ts.map +1 -0
  69. package/dist/ui/combobox.js +70 -0
  70. package/dist/ui/combobox.js.map +1 -0
  71. package/dist/ui/dialog.d.ts +39 -0
  72. package/dist/ui/dialog.d.ts.map +1 -0
  73. package/dist/ui/dialog.js +32 -0
  74. package/dist/ui/dialog.js.map +1 -0
  75. package/dist/ui/drawer.d.ts +63 -0
  76. package/dist/ui/drawer.d.ts.map +1 -0
  77. package/dist/ui/drawer.js +55 -0
  78. package/dist/ui/drawer.js.map +1 -0
  79. package/dist/ui/glyphs.d.ts +4 -0
  80. package/dist/ui/glyphs.d.ts.map +1 -0
  81. package/dist/ui/glyphs.js +20 -0
  82. package/dist/ui/glyphs.js.map +1 -0
  83. package/dist/ui/index.d.ts +9 -0
  84. package/dist/ui/index.d.ts.map +1 -0
  85. package/dist/ui/index.js +48 -0
  86. package/dist/ui/index.js.map +1 -0
  87. package/dist/ui/input.d.ts +45 -0
  88. package/dist/ui/input.d.ts.map +1 -0
  89. package/dist/ui/input.js +108 -0
  90. package/dist/ui/input.js.map +1 -0
  91. package/dist/ui/menu.d.ts +70 -0
  92. package/dist/ui/menu.d.ts.map +1 -0
  93. package/dist/ui/menu.js +85 -0
  94. package/dist/ui/menu.js.map +1 -0
  95. package/dist/ui/select.d.ts +69 -0
  96. package/dist/ui/select.d.ts.map +1 -0
  97. package/dist/ui/select.js +48 -0
  98. package/dist/ui/select.js.map +1 -0
  99. package/dist/ui/tabs.d.ts +52 -0
  100. package/dist/ui/tabs.d.ts.map +1 -0
  101. package/dist/ui/tabs.js +62 -0
  102. package/dist/ui/tabs.js.map +1 -0
  103. package/package.json +124 -0
  104. package/src/api-client.test.ts +256 -0
  105. package/src/api-client.ts +305 -0
  106. package/src/api-error.ts +60 -0
  107. package/src/auth-store.ts +59 -0
  108. package/src/chunk-reload.test.ts +46 -0
  109. package/src/chunk-reload.ts +94 -0
  110. package/src/cn.ts +16 -0
  111. package/src/describe-error.test.ts +102 -0
  112. package/src/describe-error.ts +140 -0
  113. package/src/error-boundary.tsx +88 -0
  114. package/src/guards.tsx +99 -0
  115. package/src/hydrate.ts +36 -0
  116. package/src/i18n.test.ts +68 -0
  117. package/src/i18n.ts +101 -0
  118. package/src/index.ts +63 -0
  119. package/src/prerender-contract.ts +31 -0
  120. package/src/query.test.ts +54 -0
  121. package/src/query.ts +79 -0
  122. package/src/sse.test.ts +50 -0
  123. package/src/sse.ts +117 -0
  124. package/src/states.ts +71 -0
  125. package/src/ui/class-name.ts +23 -0
  126. package/src/ui/combobox.test.ts +32 -0
  127. package/src/ui/combobox.tsx +242 -0
  128. package/src/ui/dialog.tsx +87 -0
  129. package/src/ui/drawer.tsx +161 -0
  130. package/src/ui/focus.test.ts +82 -0
  131. package/src/ui/glyphs.tsx +35 -0
  132. package/src/ui/index.ts +145 -0
  133. package/src/ui/input.test.ts +22 -0
  134. package/src/ui/input.tsx +181 -0
  135. package/src/ui/menu.test.ts +73 -0
  136. package/src/ui/menu.tsx +186 -0
  137. package/src/ui/select.tsx +191 -0
  138. package/src/ui/tabs.tsx +95 -0
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The browser half of prerendering: deciding whether to hydrate or to mount fresh.
3
+ *
4
+ * Three separate repos invented all of this independently — same constant name, same string
5
+ * value, same decision, and near-identical explaining comments. That is not a coincidence worth
6
+ * deduping; it is one lesson learned three times, and it belongs somewhere it can be learned
7
+ * once.
8
+ *
9
+ * The constants themselves live in `prerender-contract.ts`, which imports nothing, because the
10
+ * BUILD needs them too and must not pull `react-dom/client` into a Node process to get a string.
11
+ */
12
+ import type { ReactNode } from "react";
13
+ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.ts";
14
+ /**
15
+ * Hydrate the render this file IS, and mount fresh over anything else.
16
+ *
17
+ * "Does the root have children" is the wrong question, and getting it wrong is silent. A static
18
+ * host answers every address it does not publish with the nearest `404.html` — and that file has
19
+ * the not-found page rendered INTO it — so an address served from a shell arrives at a full root
20
+ * holding somebody else's markup. Hydrating that is React reconciling two different pages: it
21
+ * recovers by throwing the whole tree away and says so in the console, which is a page that
22
+ * works and a bug nobody sees. The file names the route it is a render of, so we can simply ask.
23
+ *
24
+ * `route` is passed in rather than read from `window.location` on purpose. A localized app
25
+ * serves `/pt/pricing` from a file rendered for `/pricing`, with the locale carried in the
26
+ * router's `basename` — so the address and the route are different strings, and only the caller
27
+ * knows which one the build wrote.
28
+ */
29
+ export declare function hydrateOrMount(rootEl: Element, tree: ReactNode, route: string): void;
30
+ //# sourceMappingURL=hydrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAGpF"}
@@ -0,0 +1,25 @@
1
+ import { createRoot, hydrateRoot } from "react-dom/client";
2
+ import { PRERENDERED_ROUTE_ATTR } from "./prerender-contract.js";
3
+ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.js";
4
+ /**
5
+ * Hydrate the render this file IS, and mount fresh over anything else.
6
+ *
7
+ * "Does the root have children" is the wrong question, and getting it wrong is silent. A static
8
+ * host answers every address it does not publish with the nearest `404.html` — and that file has
9
+ * the not-found page rendered INTO it — so an address served from a shell arrives at a full root
10
+ * holding somebody else's markup. Hydrating that is React reconciling two different pages: it
11
+ * recovers by throwing the whole tree away and says so in the console, which is a page that
12
+ * works and a bug nobody sees. The file names the route it is a render of, so we can simply ask.
13
+ *
14
+ * `route` is passed in rather than read from `window.location` on purpose. A localized app
15
+ * serves `/pt/pricing` from a file rendered for `/pricing`, with the locale carried in the
16
+ * router's `basename` — so the address and the route are different strings, and only the caller
17
+ * knows which one the build wrote.
18
+ */
19
+ export function hydrateOrMount(rootEl, tree, route) {
20
+ if (rootEl.getAttribute(PRERENDERED_ROUTE_ATTR) === route)
21
+ hydrateRoot(rootEl, tree);
22
+ else
23
+ createRoot(rootEl).render(tree);
24
+ }
25
+ //# sourceMappingURL=hydrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrate.js","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAAC,MAAe,EAAE,IAAe,EAAE,KAAa;IAC5E,IAAI,MAAM,CAAC,YAAY,CAAC,sBAAsB,CAAC,KAAK,KAAK;QAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;;QAChF,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC"}
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The i18next bootstrap two donors wrote nearly comment-for-comment.
3
+ *
4
+ * Each browser surface spreads these into its own init with its own bundled resources:
5
+ *
6
+ * ```ts
7
+ * i18n.use(LanguageDetector).use(initReactI18next).init({ resources, ...i18nInitOptions({…}) })
8
+ * ```
9
+ *
10
+ * Keeping the options here is what stops a console, a storefront and a docs site in the same
11
+ * product from drifting apart on the details below — each of which is a bug somebody already
12
+ * shipped.
13
+ */
14
+ export interface I18nInitOptions {
15
+ /** The language everything falls back to. */
16
+ fallbackLng: string;
17
+ /** Every language this app ships, as the exact tags used in the catalogs. */
18
+ supportedLngs: readonly string[];
19
+ /**
20
+ * Where the reader's choice is remembered.
21
+ *
22
+ * One key everywhere is a convention, not a mechanism: `localStorage` is scoped to an ORIGIN,
23
+ * so a console on `app.example.com` shares nothing with a storefront on `example.com` no
24
+ * matter what the key is called. Which is why {@link queryKey} exists.
25
+ */
26
+ storageKey: string;
27
+ /**
28
+ * The query parameter that hands a language ACROSS an origin, e.g. `"lang"`.
29
+ *
30
+ * When set it is detected FIRST, ahead of storage and the browser — it is the only one of the
31
+ * three that somebody chose on purpose and just now. A reader who picked Português on the
32
+ * storefront and followed a link into the app means it more than their laptop's locale does.
33
+ * It is cached on arrival, so it decides once and no URL inside the app has to carry it.
34
+ *
35
+ * Only one donor had this. The other has the same split-origin layout and the same problem.
36
+ */
37
+ queryKey?: string;
38
+ }
39
+ export declare function i18nInitOptions({ fallbackLng, supportedLngs, storageKey, queryKey, }: I18nInitOptions): {
40
+ fallbackLng: string;
41
+ supportedLngs: string[];
42
+ load: "currentOnly";
43
+ interpolation: {
44
+ escapeValue: boolean;
45
+ };
46
+ detection: {
47
+ lookupLocalStorage: string;
48
+ lookupQuerystring?: string | undefined;
49
+ order: string[];
50
+ caches: string[];
51
+ };
52
+ react: {
53
+ useSuspense: boolean;
54
+ };
55
+ };
56
+ /**
57
+ * Substitute `{{brand}}`-style placeholders through a bundled resource tree, once, before
58
+ * i18next ever sees it.
59
+ *
60
+ * Done here rather than through interpolation because i18next v26 has no global interpolation
61
+ * defaults, and these strings are often resolved by dynamic key — so a wrapping `t()` would not
62
+ * reach them. The point is that a rename or a domain move is ONE edit in the brand constants
63
+ * instead of a sweep across every locale file.
64
+ *
65
+ * Returns a fresh tree; the input is untouched. The placeholder names come from `vars`, so
66
+ * adding one needs no change here — two donors hardcoded their own list in the regex and both
67
+ * had to remember to update it.
68
+ */
69
+ export declare function applyBrandVars<T>(resources: T, vars: Record<string, string>): T;
70
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,eAAe;IAC9B,6CAA6C;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,aAAa,EACb,UAAU,EACV,QAAQ,GACT,EAAE,eAAe;;;;;;;;;;;;;;;;EAqBjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAmB/E"}
package/dist/i18n.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The i18next bootstrap two donors wrote nearly comment-for-comment.
3
+ *
4
+ * Each browser surface spreads these into its own init with its own bundled resources:
5
+ *
6
+ * ```ts
7
+ * i18n.use(LanguageDetector).use(initReactI18next).init({ resources, ...i18nInitOptions({…}) })
8
+ * ```
9
+ *
10
+ * Keeping the options here is what stops a console, a storefront and a docs site in the same
11
+ * product from drifting apart on the details below — each of which is a bug somebody already
12
+ * shipped.
13
+ */
14
+ export function i18nInitOptions({ fallbackLng, supportedLngs, storageKey, queryKey, }) {
15
+ return {
16
+ fallbackLng,
17
+ supportedLngs: [...supportedLngs],
18
+ load: "currentOnly",
19
+ interpolation: { escapeValue: false }, // React already escapes.
20
+ detection: {
21
+ order: queryKey
22
+ ? ["querystring", "localStorage", "navigator"]
23
+ : ["localStorage", "navigator"],
24
+ caches: ["localStorage"],
25
+ ...(queryKey ? { lookupQuerystring: queryKey } : {}),
26
+ lookupLocalStorage: storageKey,
27
+ },
28
+ react: { useSuspense: false },
29
+ // `nonExplicitSupportedLngs` is omitted deliberately, and both donors say so in the same
30
+ // words. Enabling it validates the BASE subtag against `supportedLngs`, which rejects a
31
+ // region-coded "pt-BR" (its base "pt" is not in the list) and silently falls the whole app
32
+ // back to English. Detected sub-locales like "en-US" already resolve to their base through
33
+ // i18next's ordinary fallback hierarchy, so the flag buys nothing and breaks pt-BR.
34
+ };
35
+ }
36
+ /**
37
+ * Substitute `{{brand}}`-style placeholders through a bundled resource tree, once, before
38
+ * i18next ever sees it.
39
+ *
40
+ * Done here rather than through interpolation because i18next v26 has no global interpolation
41
+ * defaults, and these strings are often resolved by dynamic key — so a wrapping `t()` would not
42
+ * reach them. The point is that a rename or a domain move is ONE edit in the brand constants
43
+ * instead of a sweep across every locale file.
44
+ *
45
+ * Returns a fresh tree; the input is untouched. The placeholder names come from `vars`, so
46
+ * adding one needs no change here — two donors hardcoded their own list in the regex and both
47
+ * had to remember to update it.
48
+ */
49
+ export function applyBrandVars(resources, vars) {
50
+ const names = Object.keys(vars);
51
+ if (names.length === 0)
52
+ return resources;
53
+ const pattern = new RegExp(`\\{\\{(${names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\}\\}`, "g");
54
+ const fill = (s) => s.replace(pattern, (_, name) => vars[name] ?? "");
55
+ const walk = (value) => {
56
+ if (typeof value === "string")
57
+ return fill(value);
58
+ if (Array.isArray(value))
59
+ return value.map(walk);
60
+ if (value && typeof value === "object")
61
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, walk(v)]));
62
+ return value;
63
+ };
64
+ return walk(resources);
65
+ }
66
+ //# sourceMappingURL=i18n.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA4BH,MAAM,UAAU,eAAe,CAAC,EAC9B,WAAW,EACX,aAAa,EACb,UAAU,EACV,QAAQ,GACQ;IAChB,OAAO;QACL,WAAW;QACX,aAAa,EAAE,CAAC,GAAG,aAAa,CAAC;QACjC,IAAI,EAAE,aAAsB;QAC5B,aAAa,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,yBAAyB;QAChE,SAAS,EAAE;YACT,KAAK,EAAE,QAAQ;gBACb,CAAC,CAAC,CAAC,aAAa,EAAE,cAAc,EAAE,WAAW,CAAC;gBAC9C,CAAC,CAAC,CAAC,cAAc,EAAE,WAAW,CAAC;YACjC,MAAM,EAAE,CAAC,cAAc,CAAC;YACxB,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACpD,kBAAkB,EAAE,UAAU;SAC/B;QACD,KAAK,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE;QAC7B,yFAAyF;QACzF,wFAAwF;QACxF,2FAA2F;QAC3F,2FAA2F;QAC3F,oFAAoF;KACrF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAI,SAAY,EAAE,IAA4B;IAC1E,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,MAAM,CACxB,UAAU,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EACvF,GAAG,CACJ,CAAC;IAEF,MAAM,IAAI,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAE9F,MAAM,IAAI,GAAG,CAAC,KAAc,EAAW,EAAE;QACvC,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;QAClD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;YACpC,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACjF,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IAEF,OAAO,IAAI,CAAC,SAAS,CAAM,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,12 @@
1
+ export { cn } from "./cn.ts";
2
+ export { ApiError, isAbortError } from "./api-error.ts";
3
+ export { createApiClient, type ApiClient, type ApiClientOptions, type RefreshResult, type RequestOptions, type SessionAdapter, } from "./api-client.ts";
4
+ export { createSseParser, readSseStream, type SseFrame } from "./sse.ts";
5
+ export { queryDefaults, shouldRetry, type QueryDefaultsOptions } from "./query.ts";
6
+ export { ErrorBoundary, type ErrorBoundaryFallbackProps, type ErrorBoundaryProps, } from "./error-boundary.tsx";
7
+ export { createErrorDescriber, humanizeWait, retryAfterSecs, type DescribedError, type ErrorContext, type ErrorDescriberOptions, type Translate, } from "./describe-error.ts";
8
+ export { installPreloadErrorHandler, isChunkLoadError, isPreloadHintFailure, reloadOnceForChunkError, } from "./chunk-reload.ts";
9
+ export { hydrateOrMount, PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./hydrate.ts";
10
+ export { applyBrandVars, i18nInitOptions, type I18nInitOptions } from "./i18n.ts";
11
+ export type { EmptyStateProps, ErrorStateProps } from "./states.ts";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEnF,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEnF,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAElF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ // @gusnips/react — the headless runtime under a Vite + React SPA.
2
+ //
3
+ // No styling, no brand, no Node built-ins.
4
+ //
5
+ // Three things live behind their own subpath, and the rule picking them is invariant 15 stated
6
+ // as a test rather than a judgement: **a peer marked `optional` must not be reachable from this
7
+ // barrel.** An optional peer the barrel imports anyway is not optional — it is a required peer
8
+ // with the error moved from install time to the adopter's first build, which is the worse of the
9
+ // two places to find out.
10
+ //
11
+ // @gusnips/react/ui the Base UI wrappers → @base-ui/react
12
+ // @gusnips/react/store createAuthStore → zustand
13
+ // @gusnips/react/guards the route guards → react-router-dom
14
+ //
15
+ // So what is left here imports `react`, `react-dom` and `@gusnips/http` and nothing else, and an
16
+ // app on TanStack Router or on Redux takes the fetch client without installing a router or a
17
+ // store it will never call. `@tanstack/react-query`, `i18next` and `react-i18next` stay optional
18
+ // AND stay here, because `query.ts`, `i18n.ts` and `states.ts` import only their TYPES — which
19
+ // erase, so the built barrel does not reference them at runtime. Check that claim against
20
+ // `dist/`, never against this file: a type-only re-export looks identical in source.
21
+ export { cn } from "./cn.js";
22
+ export { ApiError, isAbortError } from "./api-error.js";
23
+ export { createApiClient, } from "./api-client.js";
24
+ export { createSseParser, readSseStream } from "./sse.js";
25
+ export { queryDefaults, shouldRetry } from "./query.js";
26
+ export { ErrorBoundary, } from "./error-boundary.js";
27
+ export { createErrorDescriber, humanizeWait, retryAfterSecs, } from "./describe-error.js";
28
+ export { installPreloadErrorHandler, isChunkLoadError, isPreloadHintFailure, reloadOnceForChunkError, } from "./chunk-reload.js";
29
+ export { hydrateOrMount, PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./hydrate.js";
30
+ export { applyBrandVars, i18nInitOptions } from "./i18n.js";
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,oEAAoE;AACpE,6DAA6D;AAC7D,sEAAsE;AACtE,EAAE;AACF,iGAAiG;AACjG,6FAA6F;AAC7F,iGAAiG;AACjG,+FAA+F;AAC/F,0FAA0F;AAC1F,qFAAqF;AAErF,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,WAAW,EAA6B,MAAM,YAAY,CAAC;AAEnF,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,cAAc,GAKf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEnF,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The two constants the build and the browser both need — and **nothing else in this file**.
3
+ *
4
+ * It is separate from `hydrate.ts` for one reason: that module imports `react-dom/client`, and
5
+ * `@gusnips/vite` needs these constants to WRITE the attribute at build time. If the constants
6
+ * lived beside the hydration helper, a build script asking for a string would pull the browser
7
+ * renderer into a Node process, and every `entry-server.tsx` that touched this package would do
8
+ * the same. Zero imports here is the whole point; keep it that way.
9
+ *
10
+ * The contract itself was invented three times independently, with the same name and the same
11
+ * string value each time. See `hydrate.ts` for why it exists at all.
12
+ */
13
+ /**
14
+ * The attribute a prerendered file names its own route in — written by the build, read by the
15
+ * browser entry.
16
+ *
17
+ * One constant because the writer and the reader are in different builds, and a typo between
18
+ * them would show up only as a silent full re-render: a page that works, and a bug nobody sees.
19
+ */
20
+ export declare const PRERENDERED_ROUTE_ATTR = "data-prerendered-route";
21
+ /**
22
+ * What a 404 shell writes instead of a route.
23
+ *
24
+ * It is the pattern that actually matched — the catch-all — and it can never equal an address,
25
+ * which is the property that matters: every route a static host answers from the shell mounts
26
+ * fresh rather than hydrating the not-found page over itself. A real 404 pays one redundant
27
+ * client render for that, and keeps the markup a crawler reads.
28
+ */
29
+ export declare const SHELL_ROUTE = "*";
30
+ //# sourceMappingURL=prerender-contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prerender-contract.d.ts","sourceRoot":"","sources":["../src/prerender-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAE/D;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,MAAM,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The two constants the build and the browser both need — and **nothing else in this file**.
3
+ *
4
+ * It is separate from `hydrate.ts` for one reason: that module imports `react-dom/client`, and
5
+ * `@gusnips/vite` needs these constants to WRITE the attribute at build time. If the constants
6
+ * lived beside the hydration helper, a build script asking for a string would pull the browser
7
+ * renderer into a Node process, and every `entry-server.tsx` that touched this package would do
8
+ * the same. Zero imports here is the whole point; keep it that way.
9
+ *
10
+ * The contract itself was invented three times independently, with the same name and the same
11
+ * string value each time. See `hydrate.ts` for why it exists at all.
12
+ */
13
+ /**
14
+ * The attribute a prerendered file names its own route in — written by the build, read by the
15
+ * browser entry.
16
+ *
17
+ * One constant because the writer and the reader are in different builds, and a typo between
18
+ * them would show up only as a silent full re-render: a page that works, and a bug nobody sees.
19
+ */
20
+ export const PRERENDERED_ROUTE_ATTR = "data-prerendered-route";
21
+ /**
22
+ * What a 404 shell writes instead of a route.
23
+ *
24
+ * It is the pattern that actually matched — the catch-all — and it can never equal an address,
25
+ * which is the property that matters: every route a static host answers from the shell mounts
26
+ * fresh rather than hydrating the not-found page over itself. A real 404 pays one redundant
27
+ * client render for that, and keeps the markup a crawler reads.
28
+ */
29
+ export const SHELL_ROUTE = "*";
30
+ //# sourceMappingURL=prerender-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prerender-contract.js","sourceRoot":"","sources":["../src/prerender-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,wBAAwB,CAAC;AAE/D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { DefaultOptions } from "@tanstack/react-query";
2
+ export interface QueryDefaultsOptions {
3
+ /**
4
+ * Error codes that mean "this limit does not clear by waiting" — a spent monthly quota, a
5
+ * balance that needs topping up. They usually arrive as 402 or 429; the status alone cannot
6
+ * tell them apart from a burst limit, which is why the caller names them.
7
+ */
8
+ durableLimitCodes?: readonly string[];
9
+ /** How many times to retry a transient failure. Two donors used 2, one used 1. */
10
+ maxRetries?: number;
11
+ staleTime?: number;
12
+ gcTime?: number;
13
+ /**
14
+ * Left to the caller on purpose. A dashboard someone leaves open wants it on; a console
15
+ * whose data changes only when the operator changes it wants it off, and one donor turned it
16
+ * off deliberately. There is no right answer to inherit.
17
+ */
18
+ refetchOnWindowFocus?: boolean;
19
+ }
20
+ /**
21
+ * Should this failure be retried?
22
+ *
23
+ * Exported on its own because an app with its own QueryClient config still wants this rule, and
24
+ * because it is the part worth testing.
25
+ */
26
+ export declare function shouldRetry(error: unknown, durableLimitCodes?: readonly string[]): boolean;
27
+ export declare function queryDefaults({ durableLimitCodes, maxRetries, staleTime, gcTime, refetchOnWindowFocus, }?: QueryDefaultsOptions): DefaultOptions;
28
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAuB5D,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,iBAAiB,GAAE,SAAS,MAAM,EAAO,GAAG,OAAO,CAQ9F;AAED,wBAAgB,aAAa,CAAC,EAC5B,iBAAsB,EACtB,UAAc,EACd,SAAyB,EACzB,MAAuB,EACvB,oBAA4B,GAC7B,GAAE,oBAAyB,GAAG,cAAc,CAc5C"}
package/dist/query.js ADDED
@@ -0,0 +1,51 @@
1
+ import { ApiError } from "./api-error.js";
2
+ /**
3
+ * react-query defaults — the merge of three repos that each got part of this right.
4
+ *
5
+ * The default is three blind retries, which is wrong for an API that answers refusals
6
+ * precisely: a 402, a 403 or a 451 says exactly the same thing three times, and a 429 we hammer
7
+ * is a 429 we deserve. All three donors knew that much. What each knew alone:
8
+ *
9
+ * - one allowed **408** through, the only 4xx that a second attempt genuinely fixes;
10
+ * - one refused to retry a 429 whose CODE says the limit is durable — a monthly quota or a
11
+ * spent balance clears by buying, not by waiting, so retrying burns another request against
12
+ * the limiter and cannot succeed;
13
+ * - one set **`refetchOnReconnect`**, which none of the others had, and which is the single
14
+ * most useful refetch there is: coming back from a tunnel is exactly when the screen is stale.
15
+ *
16
+ * This is invariant 8.
17
+ */
18
+ /** 4xx statuses a second attempt can fix. Everything else in the 4xx family is an answer. */
19
+ const RETRYABLE_CLIENT_STATUS = new Set([408, 429]);
20
+ /**
21
+ * Should this failure be retried?
22
+ *
23
+ * Exported on its own because an app with its own QueryClient config still wants this rule, and
24
+ * because it is the part worth testing.
25
+ */
26
+ export function shouldRetry(error, durableLimitCodes = []) {
27
+ // No response at all — offline, DNS, a dropped connection. The request never landed, so
28
+ // nothing about it is an answer, and a second attempt is exactly right.
29
+ if (!(error instanceof ApiError))
30
+ return true;
31
+ if (error.code !== undefined && durableLimitCodes.includes(error.code))
32
+ return false;
33
+ if (error.status >= 500)
34
+ return true;
35
+ return RETRYABLE_CLIENT_STATUS.has(error.status);
36
+ }
37
+ export function queryDefaults({ durableLimitCodes = [], maxRetries = 2, staleTime = 2 * 60 * 1000, gcTime = 10 * 60 * 1000, refetchOnWindowFocus = false, } = {}) {
38
+ return {
39
+ queries: {
40
+ staleTime,
41
+ gcTime,
42
+ retry: (failureCount, error) => failureCount < maxRetries && shouldRetry(error, durableLimitCodes),
43
+ refetchOnWindowFocus,
44
+ refetchOnReconnect: true,
45
+ },
46
+ // A mutation is not idempotent by default. Retrying one that already reached the server is
47
+ // how a customer gets charged twice; all three donors agreed on this without discussion.
48
+ mutations: { retry: false },
49
+ };
50
+ }
51
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;;;;;;;;;;;GAeG;AAEH,6FAA6F;AAC7F,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAqBpD;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc,EAAE,oBAAuC,EAAE;IACnF,wFAAwF;IACxF,wEAAwE;IACxE,IAAI,CAAC,CAAC,KAAK,YAAY,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9C,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,iBAAiB,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACrF,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,uBAAuB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAC5B,iBAAiB,GAAG,EAAE,EACtB,UAAU,GAAG,CAAC,EACd,SAAS,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,EACzB,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EACvB,oBAAoB,GAAG,KAAK,MACJ,EAAE;IAC1B,OAAO;QACL,OAAO,EAAE;YACP,SAAS;YACT,MAAM;YACN,KAAK,EAAE,CAAC,YAAY,EAAE,KAAK,EAAE,EAAE,CAC7B,YAAY,GAAG,UAAU,IAAI,WAAW,CAAC,KAAK,EAAE,iBAAiB,CAAC;YACpE,oBAAoB;YACpB,kBAAkB,EAAE,IAAI;SACzB;QACD,2FAA2F;QAC3F,yFAAyF;QACzF,SAAS,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;KAC5B,CAAC;AACJ,CAAC"}
package/dist/sse.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Server-sent events: the framing and the transport, kept apart.
3
+ *
4
+ * Three donors had three readers and each had half the answer. One was spec-correct about the
5
+ * wire but welded to `TextDecoderStream`, so the React Native app — whose fetch has no body
6
+ * reader — could not use a line of it. One was a platform-free `feed(text)` that three
7
+ * transports shared, and it silently dropped data. One was a whole second transport with its
8
+ * own fetch and its own 401 handling, bypassing the app's only API client.
9
+ *
10
+ * So: {@link createSseParser} is the state machine and knows about no platform at all;
11
+ * {@link readSseStream} is fifteen lines of `ReadableStream` on top of it. A caller on a
12
+ * platform without body readers keeps its own fifteen lines and shares the machine underneath.
13
+ * (providerkit reached this exact split from the other direction and for the same reason.)
14
+ *
15
+ * And streams ride the ordinary API client — `client.request(path, {headers: {Accept:
16
+ * "text/event-stream"}, timeoutMs: null})`. Not `EventSource`, which cannot carry an
17
+ * Authorization header, and not a second fetch, which would need its own copy of the refresh
18
+ * logic and would drift from it.
19
+ */
20
+ /** One frame off the wire. `data` is raw text; see {@link createSseParser} on why. */
21
+ export interface SseFrame {
22
+ /** The `event:` field, or `"message"` — which is what the spec says an unnamed frame is. */
23
+ event: string;
24
+ /** Every `data:` line in the frame, joined with newlines. Never parsed here. */
25
+ data: string;
26
+ /** The `id:` field, if the server sent one. Feed it back as `Last-Event-ID` to resume. */
27
+ id: string | undefined;
28
+ }
29
+ /**
30
+ * An incremental parser. Feed it text in whatever sizes the transport delivers.
31
+ *
32
+ * Frames split across reads are held until they complete, so a payload cut mid-line is never
33
+ * lost or truncated — which is the failure this shape exists to prevent, and the one that only
34
+ * shows up under a slow network.
35
+ *
36
+ * Three rules that are easy to get wrong, and one donor got each of them wrong:
37
+ *
38
+ * - **`data:` repeats.** A frame may carry several `data:` lines and they are JOINED with
39
+ * newlines. One donor assigned instead of appending, so a multi-line payload arrived as its
40
+ * last line only — no error, no warning, just a truncated message.
41
+ * - **Strip ONE leading space after the colon**, not all whitespace. `.trim()` is the obvious
42
+ * thing and it corrupts any payload whose own content begins or ends with a space.
43
+ * - **A frame with no `data:` is not dispatched.** That is what lets a server send bare
44
+ * `event: heartbeat` keepalives, or `:` comment lines, without waking a consumer.
45
+ *
46
+ * `data` is handed back as a raw string and deliberately NOT JSON-parsed. One donor parsed here
47
+ * and its three consumers wanted three different things — one wraps non-JSON, one uses a
48
+ * tolerant parser, one parses directly. Parsing in the parser picks a policy for all of them.
49
+ */
50
+ export declare function createSseParser(onFrame: (frame: SseFrame) => void): {
51
+ feed: (text: string) => void;
52
+ };
53
+ /**
54
+ * Read a `Response` body as SSE. Resolves when the server closes the stream.
55
+ *
56
+ * Pass the response from the ordinary API client, so the stream inherits the token, the
57
+ * single-flight refresh, and a 402 or a 404 arriving as an `ApiError` the page can explain
58
+ * rather than an `onerror` event with nothing in it.
59
+ *
60
+ * Cancels the reader on the way out however it leaves — a stream abandoned without cancelling
61
+ * holds the connection open until the server gives up on it.
62
+ */
63
+ export declare function readSseStream(response: Response, onFrame: (frame: SseFrame) => void): Promise<void>;
64
+ //# sourceMappingURL=sse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,sFAAsF;AACtF,MAAM,WAAW,QAAQ;IACvB,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG;IACnE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B,CAgBA;AAqBD;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,GACjC,OAAO,CAAC,IAAI,CAAC,CAaf"}
package/dist/sse.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Server-sent events: the framing and the transport, kept apart.
3
+ *
4
+ * Three donors had three readers and each had half the answer. One was spec-correct about the
5
+ * wire but welded to `TextDecoderStream`, so the React Native app — whose fetch has no body
6
+ * reader — could not use a line of it. One was a platform-free `feed(text)` that three
7
+ * transports shared, and it silently dropped data. One was a whole second transport with its
8
+ * own fetch and its own 401 handling, bypassing the app's only API client.
9
+ *
10
+ * So: {@link createSseParser} is the state machine and knows about no platform at all;
11
+ * {@link readSseStream} is fifteen lines of `ReadableStream` on top of it. A caller on a
12
+ * platform without body readers keeps its own fifteen lines and shares the machine underneath.
13
+ * (providerkit reached this exact split from the other direction and for the same reason.)
14
+ *
15
+ * And streams ride the ordinary API client — `client.request(path, {headers: {Accept:
16
+ * "text/event-stream"}, timeoutMs: null})`. Not `EventSource`, which cannot carry an
17
+ * Authorization header, and not a second fetch, which would need its own copy of the refresh
18
+ * logic and would drift from it.
19
+ */
20
+ /**
21
+ * An incremental parser. Feed it text in whatever sizes the transport delivers.
22
+ *
23
+ * Frames split across reads are held until they complete, so a payload cut mid-line is never
24
+ * lost or truncated — which is the failure this shape exists to prevent, and the one that only
25
+ * shows up under a slow network.
26
+ *
27
+ * Three rules that are easy to get wrong, and one donor got each of them wrong:
28
+ *
29
+ * - **`data:` repeats.** A frame may carry several `data:` lines and they are JOINED with
30
+ * newlines. One donor assigned instead of appending, so a multi-line payload arrived as its
31
+ * last line only — no error, no warning, just a truncated message.
32
+ * - **Strip ONE leading space after the colon**, not all whitespace. `.trim()` is the obvious
33
+ * thing and it corrupts any payload whose own content begins or ends with a space.
34
+ * - **A frame with no `data:` is not dispatched.** That is what lets a server send bare
35
+ * `event: heartbeat` keepalives, or `:` comment lines, without waking a consumer.
36
+ *
37
+ * `data` is handed back as a raw string and deliberately NOT JSON-parsed. One donor parsed here
38
+ * and its three consumers wanted three different things — one wraps non-JSON, one uses a
39
+ * tolerant parser, one parses directly. Parsing in the parser picks a policy for all of them.
40
+ */
41
+ export function createSseParser(onFrame) {
42
+ let buffer = "";
43
+ return {
44
+ feed(text) {
45
+ // A server may send CRLF; the frame boundary is a blank line either way.
46
+ buffer += text.replace(/\r\n/g, "\n");
47
+ let end = buffer.indexOf("\n\n");
48
+ while (end !== -1) {
49
+ const frame = parseFrame(buffer.slice(0, end));
50
+ buffer = buffer.slice(end + 2);
51
+ if (frame)
52
+ onFrame(frame);
53
+ end = buffer.indexOf("\n\n");
54
+ }
55
+ },
56
+ };
57
+ }
58
+ /** The SSE wire format: `field: value` lines, `data` repeatable, a leading `:` is a comment. */
59
+ function parseFrame(raw) {
60
+ let event = "message";
61
+ let id;
62
+ const data = [];
63
+ for (const line of raw.split("\n")) {
64
+ if (line === "" || line.startsWith(":"))
65
+ continue;
66
+ const colon = line.indexOf(":");
67
+ const field = colon === -1 ? line : line.slice(0, colon);
68
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
69
+ if (field === "event")
70
+ event = value;
71
+ else if (field === "id")
72
+ id = value;
73
+ else if (field === "data")
74
+ data.push(value);
75
+ }
76
+ return data.length === 0 ? null : { event, data: data.join("\n"), id };
77
+ }
78
+ /**
79
+ * Read a `Response` body as SSE. Resolves when the server closes the stream.
80
+ *
81
+ * Pass the response from the ordinary API client, so the stream inherits the token, the
82
+ * single-flight refresh, and a 402 or a 404 arriving as an `ApiError` the page can explain
83
+ * rather than an `onerror` event with nothing in it.
84
+ *
85
+ * Cancels the reader on the way out however it leaves — a stream abandoned without cancelling
86
+ * holds the connection open until the server gives up on it.
87
+ */
88
+ export async function readSseStream(response, onFrame) {
89
+ if (!response.body)
90
+ return;
91
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
92
+ const parser = createSseParser(onFrame);
93
+ try {
94
+ for (;;) {
95
+ const { value, done } = await reader.read();
96
+ if (done)
97
+ break;
98
+ parser.feed(value);
99
+ }
100
+ }
101
+ finally {
102
+ await reader.cancel().catch(() => { });
103
+ }
104
+ }
105
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"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"}