@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.
- package/LICENSE +21 -0
- package/README.md +180 -0
- package/dist/api-client.d.ts +139 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +133 -0
- package/dist/api-client.js.map +1 -0
- package/dist/api-error.d.ts +45 -0
- package/dist/api-error.d.ts.map +1 -0
- package/dist/api-error.js +52 -0
- package/dist/api-error.js.map +1 -0
- package/dist/auth-store.d.ts +49 -0
- package/dist/auth-store.d.ts.map +1 -0
- package/dist/auth-store.js +27 -0
- package/dist/auth-store.js.map +1 -0
- package/dist/chunk-reload.d.ts +61 -0
- package/dist/chunk-reload.d.ts.map +1 -0
- package/dist/chunk-reload.js +92 -0
- package/dist/chunk-reload.js.map +1 -0
- package/dist/cn.d.ts +13 -0
- package/dist/cn.d.ts.map +1 -0
- package/dist/cn.js +16 -0
- package/dist/cn.js.map +1 -0
- package/dist/describe-error.d.ts +74 -0
- package/dist/describe-error.d.ts.map +1 -0
- package/dist/describe-error.js +70 -0
- package/dist/describe-error.js.map +1 -0
- package/dist/error-boundary.d.ts +61 -0
- package/dist/error-boundary.d.ts.map +1 -0
- package/dist/error-boundary.js +32 -0
- package/dist/error-boundary.js.map +1 -0
- package/dist/guards.d.ts +65 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +52 -0
- package/dist/guards.js.map +1 -0
- package/dist/hydrate.d.ts +30 -0
- package/dist/hydrate.d.ts.map +1 -0
- package/dist/hydrate.js +25 -0
- package/dist/hydrate.js.map +1 -0
- package/dist/i18n.d.ts +70 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +66 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/prerender-contract.d.ts +30 -0
- package/dist/prerender-contract.d.ts.map +1 -0
- package/dist/prerender-contract.js +30 -0
- package/dist/prerender-contract.js.map +1 -0
- package/dist/query.d.ts +28 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +51 -0
- package/dist/query.js.map +1 -0
- package/dist/sse.d.ts +64 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +105 -0
- package/dist/sse.js.map +1 -0
- package/dist/states.d.ts +69 -0
- package/dist/states.d.ts.map +1 -0
- package/dist/states.js +2 -0
- package/dist/states.js.map +1 -0
- package/dist/ui/class-name.d.ts +16 -0
- package/dist/ui/class-name.d.ts.map +1 -0
- package/dist/ui/class-name.js +13 -0
- package/dist/ui/class-name.js.map +1 -0
- package/dist/ui/combobox.d.ts +81 -0
- package/dist/ui/combobox.d.ts.map +1 -0
- package/dist/ui/combobox.js +70 -0
- package/dist/ui/combobox.js.map +1 -0
- package/dist/ui/dialog.d.ts +39 -0
- package/dist/ui/dialog.d.ts.map +1 -0
- package/dist/ui/dialog.js +32 -0
- package/dist/ui/dialog.js.map +1 -0
- package/dist/ui/drawer.d.ts +63 -0
- package/dist/ui/drawer.d.ts.map +1 -0
- package/dist/ui/drawer.js +55 -0
- package/dist/ui/drawer.js.map +1 -0
- package/dist/ui/glyphs.d.ts +4 -0
- package/dist/ui/glyphs.d.ts.map +1 -0
- package/dist/ui/glyphs.js +20 -0
- package/dist/ui/glyphs.js.map +1 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +48 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/input.d.ts +45 -0
- package/dist/ui/input.d.ts.map +1 -0
- package/dist/ui/input.js +108 -0
- package/dist/ui/input.js.map +1 -0
- package/dist/ui/menu.d.ts +70 -0
- package/dist/ui/menu.d.ts.map +1 -0
- package/dist/ui/menu.js +85 -0
- package/dist/ui/menu.js.map +1 -0
- package/dist/ui/select.d.ts +69 -0
- package/dist/ui/select.d.ts.map +1 -0
- package/dist/ui/select.js +48 -0
- package/dist/ui/select.js.map +1 -0
- package/dist/ui/tabs.d.ts +52 -0
- package/dist/ui/tabs.d.ts.map +1 -0
- package/dist/ui/tabs.js +62 -0
- package/dist/ui/tabs.js.map +1 -0
- package/package.json +124 -0
- package/src/api-client.test.ts +256 -0
- package/src/api-client.ts +305 -0
- package/src/api-error.ts +60 -0
- package/src/auth-store.ts +59 -0
- package/src/chunk-reload.test.ts +46 -0
- package/src/chunk-reload.ts +94 -0
- package/src/cn.ts +16 -0
- package/src/describe-error.test.ts +102 -0
- package/src/describe-error.ts +140 -0
- package/src/error-boundary.tsx +88 -0
- package/src/guards.tsx +99 -0
- package/src/hydrate.ts +36 -0
- package/src/i18n.test.ts +68 -0
- package/src/i18n.ts +101 -0
- package/src/index.ts +63 -0
- package/src/prerender-contract.ts +31 -0
- package/src/query.test.ts +54 -0
- package/src/query.ts +79 -0
- package/src/sse.test.ts +50 -0
- package/src/sse.ts +117 -0
- package/src/states.ts +71 -0
- package/src/ui/class-name.ts +23 -0
- package/src/ui/combobox.test.ts +32 -0
- package/src/ui/combobox.tsx +242 -0
- package/src/ui/dialog.tsx +87 -0
- package/src/ui/drawer.tsx +161 -0
- package/src/ui/focus.test.ts +82 -0
- package/src/ui/glyphs.tsx +35 -0
- package/src/ui/index.ts +145 -0
- package/src/ui/input.test.ts +22 -0
- package/src/ui/input.tsx +181 -0
- package/src/ui/menu.test.ts +73 -0
- package/src/ui/menu.tsx +186 -0
- package/src/ui/select.tsx +191 -0
- 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"}
|
package/dist/hydrate.js
ADDED
|
@@ -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
|
package/dist/i18n.js.map
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/query.d.ts
ADDED
|
@@ -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
|
package/dist/sse.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"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"}
|