tempest-react-sdk 0.33.2 → 0.35.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/README.md +5 -3
- package/dist/components/AIChat/AIChat.cjs +2 -0
- package/dist/components/AIChat/AIChat.cjs.map +1 -0
- package/dist/components/AIChat/AIChat.js +141 -0
- package/dist/components/AIChat/AIChat.js.map +1 -0
- package/dist/components/AIChat/AIChat.module.cjs +2 -0
- package/dist/components/AIChat/AIChat.module.cjs.map +1 -0
- package/dist/components/AIChat/AIChat.module.js +58 -0
- package/dist/components/AIChat/AIChat.module.js.map +1 -0
- package/dist/components/AIChat/AIChatComposer.cjs +2 -0
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -0
- package/dist/components/AIChat/AIChatComposer.js +87 -0
- package/dist/components/AIChat/AIChatComposer.js.map +1 -0
- package/dist/components/AIChat/AIChatTurn.cjs +3 -0
- package/dist/components/AIChat/AIChatTurn.cjs.map +1 -0
- package/dist/components/AIChat/AIChatTurn.js +217 -0
- package/dist/components/AIChat/AIChatTurn.js.map +1 -0
- package/dist/components/AIChat/ai-chat-turns.cjs +2 -0
- package/dist/components/AIChat/ai-chat-turns.cjs.map +1 -0
- package/dist/components/AIChat/ai-chat-turns.js +90 -0
- package/dist/components/AIChat/ai-chat-turns.js.map +1 -0
- package/dist/perf/cache-size.cjs +2 -0
- package/dist/perf/cache-size.cjs.map +1 -0
- package/dist/perf/cache-size.js +16 -0
- package/dist/perf/cache-size.js.map +1 -0
- package/dist/perf/device.cjs +2 -0
- package/dist/perf/device.cjs.map +1 -0
- package/dist/perf/device.js +22 -0
- package/dist/perf/device.js.map +1 -0
- package/dist/perf/format.cjs +2 -0
- package/dist/perf/format.cjs.map +1 -0
- package/dist/perf/format.js +8 -0
- package/dist/perf/format.js.map +1 -0
- package/dist/perf/profiler.cjs +2 -0
- package/dist/perf/profiler.cjs.map +1 -0
- package/dist/perf/profiler.js +46 -0
- package/dist/perf/profiler.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +626 -9
- package/dist/tempest-react-sdk.js +122 -114
- package/dist/vision/core/timing.cjs +2 -0
- package/dist/vision/core/timing.cjs.map +1 -0
- package/dist/vision/core/timing.js +24 -0
- package/dist/vision/core/timing.js.map +1 -0
- package/dist/vision/index.cjs +1 -1
- package/dist/vision/index.cjs.map +1 -1
- package/dist/vision/index.js +16 -15
- package/dist/vision/index.js.map +1 -1
- package/dist/vision/postprocess/detection.cjs +1 -1
- package/dist/vision/postprocess/detection.cjs.map +1 -1
- package/dist/vision/postprocess/detection.js +2 -2
- package/dist/vision/postprocess/detection.js.map +1 -1
- package/dist/vision/postprocess/segmentation.cjs +1 -1
- package/dist/vision/postprocess/segmentation.cjs.map +1 -1
- package/dist/vision/postprocess/segmentation.js +1 -1
- package/dist/vision/postprocess/segmentation.js.map +1 -1
- package/dist/vision/results.cjs +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js +18 -13
- package/dist/vision/results.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js +46 -39
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detector.cjs +1 -1
- package/dist/vision/tasks/detector.cjs.map +1 -1
- package/dist/vision/tasks/detector.js +40 -33
- package/dist/vision/tasks/detector.js.map +1 -1
- package/dist/vision/tasks/segmenter.cjs +1 -1
- package/dist/vision/tasks/segmenter.cjs.map +1 -1
- package/dist/vision/tasks/segmenter.js +35 -28
- package/dist/vision/tasks/segmenter.js.map +1 -1
- package/dist/vision.cjs +1 -1
- package/dist/vision.d.ts +68 -10
- package/dist/vision.js +19 -18
- package/dist/ws/create-web-socket.cjs +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js +54 -37
- package/dist/ws/create-web-socket.js.map +1 -1
- package/dist/ws/use-web-socket.cjs +1 -1
- package/dist/ws/use-web-socket.cjs.map +1 -1
- package/dist/ws/use-web-socket.js +49 -18
- package/dist/ws/use-web-socket.js.map +1 -1
- package/package.json +1 -1
- package/template/src/lib/api.ts +27 -5
- package/template/src/stores/auth.ts +14 -0
|
@@ -7,60 +7,77 @@ function e(e) {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
function t(t, n = {}) {
|
|
10
|
-
let { protocols: r, maxRetries: i = 10, initialBackoff: a = 1e3, maxBackoff: o = 3e4, pingInterval: s = 0, pingPayload: c = JSON.stringify({ type: "ping" }),
|
|
11
|
-
function
|
|
12
|
-
|
|
10
|
+
let { protocols: r, maxRetries: i = 10, initialBackoff: a = 1e3, maxBackoff: o = 3e4, pingInterval: s = 0, pingPayload: c = JSON.stringify({ type: "ping" }), respondToPing: l = !0, pongPayload: u = JSON.stringify({ type: "pong" }), queueWhileClosed: d = !1, maxQueuedMessages: f = 100, parser: p = e, onOpen: m, onMessage: h, onClose: g, onError: _, onStatusChange: v } = n, y = null, b = null, x = null, S = 0, C = "idle", w = !1, T = [];
|
|
11
|
+
function E(e) {
|
|
12
|
+
return typeof e == "object" && !!e && e.type === "ping";
|
|
13
|
+
}
|
|
14
|
+
function D(e) {
|
|
15
|
+
for (; T.length > 0 && e.readyState === WebSocket.OPEN;) e.send(T.shift());
|
|
13
16
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
17
|
+
function O(e) {
|
|
18
|
+
C !== e && (C = e, v?.(e));
|
|
16
19
|
}
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
function k() {
|
|
21
|
+
x &&= (clearInterval(x), null);
|
|
22
|
+
}
|
|
23
|
+
function A() {
|
|
24
|
+
!s || s <= 0 || (k(), x = setInterval(() => {
|
|
25
|
+
y?.readyState === WebSocket.OPEN && y.send(c);
|
|
20
26
|
}, s));
|
|
21
27
|
}
|
|
22
|
-
function
|
|
23
|
-
if (
|
|
24
|
-
if (
|
|
25
|
-
|
|
28
|
+
function j() {
|
|
29
|
+
if (w) return;
|
|
30
|
+
if (S >= i) {
|
|
31
|
+
O("error");
|
|
26
32
|
return;
|
|
27
33
|
}
|
|
28
|
-
let e = Math.min(a * 2 **
|
|
29
|
-
|
|
34
|
+
let e = Math.min(a * 2 ** S, o);
|
|
35
|
+
S += 1, b = setTimeout(M, e);
|
|
30
36
|
}
|
|
31
|
-
function
|
|
32
|
-
if (
|
|
33
|
-
|
|
37
|
+
function M() {
|
|
38
|
+
if (w) return;
|
|
39
|
+
if (y) {
|
|
40
|
+
let e = y;
|
|
41
|
+
e.onmessage = null, e.onclose = null, e.onerror = null, e.readyState !== WebSocket.CONNECTING && (e.onopen = null), F(e);
|
|
42
|
+
}
|
|
43
|
+
O("connecting");
|
|
34
44
|
let e = new WebSocket(t, r);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}, e.onmessage = (
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
data:
|
|
41
|
-
raw:
|
|
45
|
+
y = e, e.onopen = (t) => {
|
|
46
|
+
S = 0, O("open"), A(), D(e), m?.(t);
|
|
47
|
+
}, e.onmessage = (t) => {
|
|
48
|
+
let n = typeof t.data == "string" ? t.data : "", r = p(n);
|
|
49
|
+
l && E(r) && e.readyState === WebSocket.OPEN && e.send(u), h?.({
|
|
50
|
+
data: r,
|
|
51
|
+
raw: t
|
|
42
52
|
});
|
|
43
53
|
}, e.onerror = (e) => {
|
|
44
|
-
|
|
54
|
+
_?.(e);
|
|
45
55
|
}, e.onclose = (e) => {
|
|
46
|
-
|
|
56
|
+
k(), g?.(e), y = null, O("closed"), !w && !e.wasClean && j();
|
|
47
57
|
};
|
|
48
58
|
}
|
|
49
|
-
function
|
|
50
|
-
return
|
|
59
|
+
function N(e) {
|
|
60
|
+
return y?.readyState === WebSocket.OPEN ? (y.send(e), !0) : !d || w ? !1 : (T.length >= f && T.shift(), T.push(e), !0);
|
|
61
|
+
}
|
|
62
|
+
function P(e, t) {
|
|
63
|
+
w = !0, b &&= (clearTimeout(b), null), k(), S = 0, T.length = 0, y &&= (O("closing"), F(y, e, t), null), O("closed");
|
|
51
64
|
}
|
|
52
|
-
function
|
|
53
|
-
|
|
65
|
+
function F(e, t, n) {
|
|
66
|
+
if (e.readyState === WebSocket.CONNECTING) {
|
|
67
|
+
e.onopen = () => e.close(t, n), e.onmessage = null, e.onerror = null, e.onclose = null;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
e.close(t, n);
|
|
54
71
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
72
|
+
function I() {
|
|
73
|
+
b &&= (clearTimeout(b), null), S = 0, w = !1, M();
|
|
57
74
|
}
|
|
58
|
-
return
|
|
59
|
-
send:
|
|
60
|
-
close:
|
|
61
|
-
reconnect:
|
|
75
|
+
return M(), {
|
|
76
|
+
send: N,
|
|
77
|
+
close: P,
|
|
78
|
+
reconnect: I,
|
|
62
79
|
get status() {
|
|
63
|
-
return
|
|
80
|
+
return C;
|
|
64
81
|
}
|
|
65
82
|
};
|
|
66
83
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-web-socket.js","names":[],"sources":["../../src/ws/create-web-socket.ts"],"sourcesContent":["export 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 * Ping interval (ms). When set, the client sends `pingPayload` periodically\n * to keep the socket alive. Default: 0 (disabled).\n */\n pingInterval?: number;\n /** Payload sent on each ping. Default: `JSON.stringify({ type: \"ping\" })`. */\n pingPayload?: string | Blob | BufferSource;\n /** Parse incoming frames. Default: JSON with raw-string fallback. */\n parser?: (raw: string) => T;\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\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 /** Current connection status. */\n readonly status: WebSocketStatus;\n}\n\nfunction defaultParser<T>(raw: string): T {\n try {\n return JSON.parse(raw) as T;\n } catch {\n return raw as unknown as T;\n }\n}\n\n/**\n * Open a WebSocket with automatic exponential-backoff reconnect, optional\n * heartbeat pings, and typed JSON parsing.\n *\n * @param url - Full ws:// or wss:// URL.\n * @param options - Connection configuration and callbacks.\n * @returns Controller exposing `send`, `close`, `reconnect`, and `status`.\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 pingInterval = 0,\n pingPayload = JSON.stringify({ type: \"ping\" }),\n parser = defaultParser<T>,\n onOpen,\n onMessage,\n onClose,\n onError,\n onStatusChange,\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 retries = 0;\n let status: WebSocketStatus = \"idle\";\n let closed = false;\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 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 function scheduleReconnect(): void {\n if (closed) return;\n if (retries >= maxRetries) {\n setStatus(\"error\");\n return;\n }\n const delay = Math.min(initialBackoff * 2 ** retries, maxBackoff);\n retries += 1;\n retryTimer = setTimeout(connect, delay);\n }\n\n function connect(): void {\n if (closed) return;\n if (socket) {\n socket.onopen = null;\n socket.onmessage = null;\n socket.onclose = null;\n socket.onerror = null;\n socket.close();\n }\n setStatus(\"connecting\");\n\n const ws = new WebSocket(url, protocols);\n socket = ws;\n\n ws.onopen = (event) => {\n retries = 0;\n setStatus(\"open\");\n startPing();\n onOpen?.(event);\n };\n\n ws.onmessage = (event) => {\n const raw = typeof event.data === \"string\" ? event.data : \"\";\n onMessage?.({\n data: parser(raw),\n raw: event,\n });\n };\n\n ws.onerror = (event) => {\n onError?.(event);\n };\n\n ws.onclose = (event) => {\n clearPing();\n onClose?.(event);\n socket = null;\n setStatus(\"closed\");\n if (!closed && !event.wasClean) {\n scheduleReconnect();\n }\n };\n }\n\n function send(payload: string | Blob | BufferSource): boolean {\n if (socket?.readyState !== WebSocket.OPEN) return false;\n socket.send(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 retries = 0;\n if (socket) {\n setStatus(\"closing\");\n socket.close(code, reason);\n socket = null;\n }\n setStatus(\"closed\");\n }\n\n function reconnect(): void {\n if (retryTimer) {\n clearTimeout(retryTimer);\n retryTimer = null;\n }\n retries = 0;\n closed = false;\n connect();\n }\n\n connect();\n\n return {\n send,\n close,\n reconnect,\n get status() {\n return status;\n },\n };\n}\n"],"mappings":";AA6CA,SAAS,EAAiB,GAAgB;CACtC,IAAI;EACA,OAAO,KAAK,MAAM,CAAG;CACzB,QAAQ;EACJ,OAAO;CACX;AACJ;AAUA,SAAgB,EACZ,GACA,IAAqC,CAAC,GACnB;CACnB,IAAM,EACF,cACA,gBAAa,IACb,oBAAiB,KACjB,gBAAa,KACb,kBAAe,GACf,iBAAc,KAAK,UAAU,EAAE,MAAM,OAAO,CAAC,GAC7C,YAAS,GACT,WACA,cACA,YACA,YACA,sBACA,GAEA,IAA2B,MAC3B,IAAmD,MACnD,IAAmD,MACnD,IAAU,GACV,IAA0B,QAC1B,IAAS;CAEb,SAAS,EAAU,GAA6B;EACxC,MAAW,MACf,IAAS,GACT,IAAiB,CAAI;CACzB;CAEA,SAAS,IAAkB;EACvB,AAEI,OADA,cAAc,CAAS,GACX;CAEpB;CAEA,SAAS,IAAkB;EACnB,CAAC,KAAgB,KAAgB,MACrC,EAAU,GACV,IAAY,kBAAkB;GAC1B,AAAI,GAAQ,eAAe,UAAU,QACjC,EAAO,KAAK,CAAW;EAE/B,GAAG,CAAY;CACnB;CAEA,SAAS,IAA0B;EAC/B,IAAI,GAAQ;EACZ,IAAI,KAAW,GAAY;GACvB,EAAU,OAAO;GACjB;EACJ;EACA,IAAM,IAAQ,KAAK,IAAI,IAAiB,KAAK,GAAS,CAAU;EAEhE,AADA,KAAW,GACX,IAAa,WAAW,GAAS,CAAK;CAC1C;CAEA,SAAS,IAAgB;EACrB,IAAI,GAAQ;EAQZ,AAPI,MACA,EAAO,SAAS,MAChB,EAAO,YAAY,MACnB,EAAO,UAAU,MACjB,EAAO,UAAU,MACjB,EAAO,MAAM,IAEjB,EAAU,YAAY;EAEtB,IAAM,IAAK,IAAI,UAAU,GAAK,CAAS;EAsBvC,AArBA,IAAS,GAET,EAAG,UAAU,MAAU;GAInB,AAHA,IAAU,GACV,EAAU,MAAM,GAChB,EAAU,GACV,IAAS,CAAK;EAClB,GAEA,EAAG,aAAa,MAAU;GACtB,IAAM,IAAM,OAAO,EAAM,QAAS,WAAW,EAAM,OAAO;GAC1D,IAAY;IACR,MAAM,EAAO,CAAG;IAChB,KAAK;GACT,CAAC;EACL,GAEA,EAAG,WAAW,MAAU;GACpB,IAAU,CAAK;EACnB,GAEA,EAAG,WAAW,MAAU;GAKpB,AAJA,EAAU,GACV,IAAU,CAAK,GACf,IAAS,MACT,EAAU,QAAQ,GACd,CAAC,KAAU,CAAC,EAAM,YAClB,EAAkB;EAE1B;CACJ;CAEA,SAAS,EAAK,GAAgD;EAG1D,OAFI,GAAQ,eAAe,UAAU,QACrC,EAAO,KAAK,CAAO,GACZ,MAF2C;CAGtD;CAEA,SAAS,EAAM,GAAe,GAAuB;EAajD,AAZA,IAAS,IACT,AAEI,OADA,aAAa,CAAU,GACV,OAEjB,EAAU,GACV,IAAU,GACV,AAGI,OAFA,EAAU,SAAS,GACnB,EAAO,MAAM,GAAM,CAAM,GAChB,OAEb,EAAU,QAAQ;CACtB;CAEA,SAAS,IAAkB;EAOvB,AANA,AAEI,OADA,aAAa,CAAU,GACV,OAEjB,IAAU,GACV,IAAS,IACT,EAAQ;CACZ;CAIA,OAFA,EAAQ,GAED;EACH;EACA;EACA;EACA,IAAI,SAAS;GACT,OAAO;EACX;CACJ;AACJ"}
|
|
1
|
+
{"version":3,"file":"create-web-socket.js","names":[],"sources":["../../src/ws/create-web-socket.ts"],"sourcesContent":["export 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 * 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 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\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 /** Current connection status. */\n readonly status: WebSocketStatus;\n}\n\nfunction defaultParser<T>(raw: string): T {\n try {\n return JSON.parse(raw) as T;\n } catch {\n return raw as unknown as T;\n }\n}\n\n/**\n * Open a WebSocket with automatic exponential-backoff reconnect, optional\n * heartbeat pings, and typed JSON parsing.\n *\n * @param url - Full ws:// or wss:// URL.\n * @param options - Connection configuration and callbacks.\n * @returns Controller exposing `send`, `close`, `reconnect`, and `status`.\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 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 = defaultParser<T>,\n onOpen,\n onMessage,\n onClose,\n onError,\n onStatusChange,\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 retries = 0;\n let status: WebSocketStatus = \"idle\";\n let closed = false;\n const outbox: Array<string | Blob | BufferSource> = [];\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 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 function scheduleReconnect(): void {\n if (closed) return;\n if (retries >= maxRetries) {\n setStatus(\"error\");\n return;\n }\n const delay = Math.min(initialBackoff * 2 ** retries, maxBackoff);\n retries += 1;\n retryTimer = setTimeout(connect, delay);\n }\n\n function connect(): void {\n if (closed) return;\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 ws.onopen = (event) => {\n retries = 0;\n setStatus(\"open\");\n startPing();\n flushOutbox(ws);\n onOpen?.(event);\n };\n\n ws.onmessage = (event) => {\n const raw = typeof event.data === \"string\" ? event.data : \"\";\n const data = parser(raw);\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 ws.onclose = (event) => {\n clearPing();\n onClose?.(event);\n socket = null;\n setStatus(\"closed\");\n if (!closed && !event.wasClean) {\n scheduleReconnect();\n }\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 retries = 0;\n outbox.length = 0;\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 retries = 0;\n closed = false;\n connect();\n }\n\n connect();\n\n return {\n send,\n close,\n reconnect,\n get status() {\n return status;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./create-web-socket.cjs");let t=require("react");function n(
|
|
1
|
+
const e=require("./create-web-socket.cjs");let t=require("react");function n(e){try{return JSON.parse(e)}catch{return e}}function r(r,i={}){let{enabled:a=!0,protocols:o,maxRetries:s,initialBackoff:c,maxBackoff:l,pingInterval:u,respondToPing:d,queueWhileClosed:f,maxQueuedMessages:p}=i,[m,h]=(0,t.useState)(`idle`),[g,_]=(0,t.useState)(null),v=(0,t.useRef)(null),y=(0,t.useRef)(i);return(0,t.useEffect)(()=>{y.current=i}),(0,t.useEffect)(()=>{if(!a||!r){h(`idle`);return}let t=e.createWebSocket(r,{protocols:y.current.protocols,maxRetries:s,initialBackoff:c,maxBackoff:l,pingInterval:u,pingPayload:y.current.pingPayload,respondToPing:d,pongPayload:y.current.pongPayload,queueWhileClosed:f,maxQueuedMessages:p,parser:e=>(y.current.parser??n)(e),onStatusChange:h,onOpen:e=>y.current.onOpen?.(e),onClose:e=>y.current.onClose?.(e),onError:e=>y.current.onError?.(e),onMessage:e=>{_(e),y.current.onMessage?.(e)}});return v.current=t,()=>{t.close(),v.current=null}},[r,a,Array.isArray(o)?o.join(`,`):o??``,s,c,l,u,d,f,p]),{status:m,lastMessage:g,send:(0,t.useCallback)(e=>v.current?.send(e)??!1,[]),reconnect:(0,t.useCallback)(()=>{v.current?.reconnect()},[])}}exports.useWebSocket=r;
|
|
2
2
|
//# sourceMappingURL=use-web-socket.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-web-socket.cjs","names":[],"sources":["../../src/ws/use-web-socket.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n createWebSocket,\n type CreateWebSocketOptions,\n type WebSocketController,\n type WebSocketMessage,\n type WebSocketStatus,\n} from \"./create-web-socket\";\n\nexport interface UseWebSocketOptions<T> extends Omit<CreateWebSocketOptions<T>, \"onStatusChange\"> {\n /** When false, the socket is not opened. Default: true. */\n enabled?: boolean;\n}\n\nexport interface UseWebSocketResult<T> {\n status: WebSocketStatus;\n
|
|
1
|
+
{"version":3,"file":"use-web-socket.cjs","names":[],"sources":["../../src/ws/use-web-socket.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n createWebSocket,\n type CreateWebSocketOptions,\n type WebSocketController,\n type WebSocketMessage,\n type WebSocketStatus,\n} from \"./create-web-socket\";\n\nexport interface UseWebSocketOptions<T> extends Omit<CreateWebSocketOptions<T>, \"onStatusChange\"> {\n /** When false, the socket is not opened. Default: true. */\n enabled?: boolean;\n}\n\nexport interface UseWebSocketResult<T> {\n status: WebSocketStatus;\n /**\n * Last decoded frame received.\n *\n * A snapshot, not a stream: two frames arriving in the same tick collapse\n * into a single render and only the later one is ever visible. One server\n * action often emits several frames in a row, so anything that must see\n * every message has to use `onMessage`, which fires once per frame. Read\n * `lastMessage` for \"what is the current state\" rendering only.\n */\n lastMessage: WebSocketMessage<T> | null;\n /** Send a payload through the active connection. Returns false when not open. */\n send: (payload: string | Blob | BufferSource) => boolean;\n /** Force a reconnect, resetting the retry counter. */\n reconnect: () => void;\n}\n\n/** Mirror of `createWebSocket`'s default parser: JSON, raw string on failure. */\nfunction defaultParse<T>(raw: string): T {\n try {\n return JSON.parse(raw) as T;\n } catch {\n return raw as unknown as T;\n }\n}\n\n/**\n * React hook around {@link createWebSocket}. Manages the connection lifecycle\n * for the host component and tears it down on unmount.\n *\n * Every callback is read through a ref, so `onOpen` / `onMessage` / `onClose` /\n * `onError` always run the latest closure — an inline arrow function is fine\n * and never reopens the socket. Connection-shaping options (`protocols`,\n * `maxRetries`, `initialBackoff`, `maxBackoff`, `pingInterval`,\n * `queueWhileClosed`) are baked into the connection, so changing one reopens\n * it with the new value rather than being silently ignored.\n *\n * @param url - Full ws:// or wss:// URL.\n * @param options - Connection configuration and callbacks.\n * @returns Status, last frame, and the `send` / `reconnect` controls.\n */\nexport function useWebSocket<T = unknown>(\n url: string,\n options: UseWebSocketOptions<T> = {},\n): UseWebSocketResult<T> {\n const {\n enabled = true,\n protocols,\n maxRetries,\n initialBackoff,\n maxBackoff,\n pingInterval,\n respondToPing,\n queueWhileClosed,\n maxQueuedMessages,\n } = options;\n const [status, setStatus] = useState<WebSocketStatus>(\"idle\");\n const [lastMessage, setLastMessage] = useState<WebSocketMessage<T> | null>(null);\n const controllerRef = useRef<WebSocketController | null>(null);\n\n const optionsRef = useRef(options);\n useEffect(() => {\n optionsRef.current = options;\n });\n\n const protocolsKey = Array.isArray(protocols) ? protocols.join(\",\") : (protocols ?? \"\");\n\n useEffect(() => {\n if (!enabled || !url) {\n setStatus(\"idle\");\n return;\n }\n\n const controller = createWebSocket<T>(url, {\n protocols: optionsRef.current.protocols,\n maxRetries,\n initialBackoff,\n maxBackoff,\n pingInterval,\n pingPayload: optionsRef.current.pingPayload,\n respondToPing,\n pongPayload: optionsRef.current.pongPayload,\n queueWhileClosed,\n maxQueuedMessages,\n parser: (raw) => (optionsRef.current.parser ?? defaultParse<T>)(raw),\n onStatusChange: setStatus,\n onOpen: (event) => optionsRef.current.onOpen?.(event),\n onClose: (event) => optionsRef.current.onClose?.(event),\n onError: (event) => optionsRef.current.onError?.(event),\n onMessage: (message) => {\n setLastMessage(message);\n optionsRef.current.onMessage?.(message);\n },\n });\n controllerRef.current = controller;\n\n return () => {\n controller.close();\n controllerRef.current = null;\n };\n }, [\n url,\n enabled,\n protocolsKey,\n maxRetries,\n initialBackoff,\n maxBackoff,\n pingInterval,\n respondToPing,\n queueWhileClosed,\n maxQueuedMessages,\n ]);\n\n const send = useCallback((payload: string | Blob | BufferSource): boolean => {\n return controllerRef.current?.send(payload) ?? false;\n }, []);\n\n const reconnect = useCallback((): void => {\n controllerRef.current?.reconnect();\n }, []);\n\n return { status, lastMessage, send, reconnect };\n}\n"],"mappings":"kEAiCA,SAAS,EAAgB,EAAgB,CACrC,GAAI,CACA,OAAO,KAAK,MAAM,CAAG,CACzB,MAAQ,CACJ,OAAO,CACX,CACJ,CAiBA,SAAgB,EACZ,EACA,EAAkC,CAAC,EACd,CACrB,GAAM,CACF,UAAU,GACV,YACA,aACA,iBACA,aACA,eACA,gBACA,mBACA,qBACA,EACE,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAAuC,MAAM,EACtD,CAAC,EAAa,IAAA,EAAA,EAAA,SAAA,CAAuD,IAAI,EACzE,GAAA,EAAA,EAAA,OAAA,CAAmD,IAAI,EAEvD,GAAA,EAAA,EAAA,OAAA,CAAoB,CAAO,EA6DjC,OA5DA,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAW,QAAU,CACzB,CAAC,GAID,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,CAAC,GAAW,CAAC,EAAK,CAClB,EAAU,MAAM,EAChB,MACJ,CAEA,IAAM,EAAa,EAAA,gBAAmB,EAAK,CACvC,UAAW,EAAW,QAAQ,UAC9B,aACA,iBACA,aACA,eACA,YAAa,EAAW,QAAQ,YAChC,gBACA,YAAa,EAAW,QAAQ,YAChC,mBACA,oBACA,OAAS,IAAS,EAAW,QAAQ,QAAU,EAAA,CAAiB,CAAG,EACnE,eAAgB,EAChB,OAAS,GAAU,EAAW,QAAQ,SAAS,CAAK,EACpD,QAAU,GAAU,EAAW,QAAQ,UAAU,CAAK,EACtD,QAAU,GAAU,EAAW,QAAQ,UAAU,CAAK,EACtD,UAAY,GAAY,CACpB,EAAe,CAAO,EACtB,EAAW,QAAQ,YAAY,CAAO,CAC1C,CACJ,CAAC,EAGD,MAFA,GAAc,QAAU,MAEX,CACT,EAAW,MAAM,EACjB,EAAc,QAAU,IAC5B,CACJ,EAAG,CACC,EACA,EArCiB,MAAM,QAAQ,CAAS,EAAI,EAAU,KAAK,GAAG,EAAK,GAAa,GAuChF,EACA,EACA,EACA,EACA,EACA,EACA,CACJ,CAAC,EAUM,CAAE,SAAQ,cAAa,MAAA,EAAA,EAAA,YAAA,CARJ,GACf,EAAc,SAAS,KAAK,CAAO,GAAK,GAChD,CAAC,CAM0B,EAAM,WAAA,EAAA,EAAA,YAAA,KAJM,CACtC,EAAc,SAAS,UAAU,CACrC,EAAG,CAAC,CAEgC,CAAU,CAClD"}
|
|
@@ -1,35 +1,66 @@
|
|
|
1
1
|
import { createWebSocket as e } from "./create-web-socket.js";
|
|
2
2
|
import { useCallback as t, useEffect as n, useRef as r, useState as i } from "react";
|
|
3
3
|
//#region src/ws/use-web-socket.ts
|
|
4
|
-
function a(
|
|
5
|
-
|
|
4
|
+
function a(e) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(e);
|
|
7
|
+
} catch {
|
|
8
|
+
return e;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function o(o, s = {}) {
|
|
12
|
+
let { enabled: c = !0, protocols: l, maxRetries: u, initialBackoff: d, maxBackoff: f, pingInterval: p, respondToPing: m, queueWhileClosed: h, maxQueuedMessages: g } = s, [_, v] = i("idle"), [y, b] = i(null), x = r(null), S = r(s);
|
|
6
13
|
return n(() => {
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
if (!
|
|
10
|
-
|
|
14
|
+
S.current = s;
|
|
15
|
+
}), n(() => {
|
|
16
|
+
if (!c || !o) {
|
|
17
|
+
v("idle");
|
|
11
18
|
return;
|
|
12
19
|
}
|
|
13
|
-
let t = e(
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
let t = e(o, {
|
|
21
|
+
protocols: S.current.protocols,
|
|
22
|
+
maxRetries: u,
|
|
23
|
+
initialBackoff: d,
|
|
24
|
+
maxBackoff: f,
|
|
25
|
+
pingInterval: p,
|
|
26
|
+
pingPayload: S.current.pingPayload,
|
|
27
|
+
respondToPing: m,
|
|
28
|
+
pongPayload: S.current.pongPayload,
|
|
29
|
+
queueWhileClosed: h,
|
|
30
|
+
maxQueuedMessages: g,
|
|
31
|
+
parser: (e) => (S.current.parser ?? a)(e),
|
|
32
|
+
onStatusChange: v,
|
|
33
|
+
onOpen: (e) => S.current.onOpen?.(e),
|
|
34
|
+
onClose: (e) => S.current.onClose?.(e),
|
|
35
|
+
onError: (e) => S.current.onError?.(e),
|
|
16
36
|
onMessage: (e) => {
|
|
17
|
-
|
|
37
|
+
b(e), S.current.onMessage?.(e);
|
|
18
38
|
}
|
|
19
39
|
});
|
|
20
|
-
return
|
|
21
|
-
t.close(),
|
|
40
|
+
return x.current = t, () => {
|
|
41
|
+
t.close(), x.current = null;
|
|
22
42
|
};
|
|
23
|
-
}, [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
}, [
|
|
44
|
+
o,
|
|
45
|
+
c,
|
|
46
|
+
Array.isArray(l) ? l.join(",") : l ?? "",
|
|
47
|
+
u,
|
|
48
|
+
d,
|
|
49
|
+
f,
|
|
50
|
+
p,
|
|
51
|
+
m,
|
|
52
|
+
h,
|
|
53
|
+
g
|
|
54
|
+
]), {
|
|
55
|
+
status: _,
|
|
56
|
+
lastMessage: y,
|
|
57
|
+
send: t((e) => x.current?.send(e) ?? !1, []),
|
|
27
58
|
reconnect: t(() => {
|
|
28
|
-
|
|
59
|
+
x.current?.reconnect();
|
|
29
60
|
}, [])
|
|
30
61
|
};
|
|
31
62
|
}
|
|
32
63
|
//#endregion
|
|
33
|
-
export {
|
|
64
|
+
export { o as useWebSocket };
|
|
34
65
|
|
|
35
66
|
//# sourceMappingURL=use-web-socket.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-web-socket.js","names":[],"sources":["../../src/ws/use-web-socket.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n createWebSocket,\n type CreateWebSocketOptions,\n type WebSocketController,\n type WebSocketMessage,\n type WebSocketStatus,\n} from \"./create-web-socket\";\n\nexport interface UseWebSocketOptions<T> extends Omit<CreateWebSocketOptions<T>, \"onStatusChange\"> {\n /** When false, the socket is not opened. Default: true. */\n enabled?: boolean;\n}\n\nexport interface UseWebSocketResult<T> {\n status: WebSocketStatus;\n
|
|
1
|
+
{"version":3,"file":"use-web-socket.js","names":[],"sources":["../../src/ws/use-web-socket.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n createWebSocket,\n type CreateWebSocketOptions,\n type WebSocketController,\n type WebSocketMessage,\n type WebSocketStatus,\n} from \"./create-web-socket\";\n\nexport interface UseWebSocketOptions<T> extends Omit<CreateWebSocketOptions<T>, \"onStatusChange\"> {\n /** When false, the socket is not opened. Default: true. */\n enabled?: boolean;\n}\n\nexport interface UseWebSocketResult<T> {\n status: WebSocketStatus;\n /**\n * Last decoded frame received.\n *\n * A snapshot, not a stream: two frames arriving in the same tick collapse\n * into a single render and only the later one is ever visible. One server\n * action often emits several frames in a row, so anything that must see\n * every message has to use `onMessage`, which fires once per frame. Read\n * `lastMessage` for \"what is the current state\" rendering only.\n */\n lastMessage: WebSocketMessage<T> | null;\n /** Send a payload through the active connection. Returns false when not open. */\n send: (payload: string | Blob | BufferSource) => boolean;\n /** Force a reconnect, resetting the retry counter. */\n reconnect: () => void;\n}\n\n/** Mirror of `createWebSocket`'s default parser: JSON, raw string on failure. */\nfunction defaultParse<T>(raw: string): T {\n try {\n return JSON.parse(raw) as T;\n } catch {\n return raw as unknown as T;\n }\n}\n\n/**\n * React hook around {@link createWebSocket}. Manages the connection lifecycle\n * for the host component and tears it down on unmount.\n *\n * Every callback is read through a ref, so `onOpen` / `onMessage` / `onClose` /\n * `onError` always run the latest closure — an inline arrow function is fine\n * and never reopens the socket. Connection-shaping options (`protocols`,\n * `maxRetries`, `initialBackoff`, `maxBackoff`, `pingInterval`,\n * `queueWhileClosed`) are baked into the connection, so changing one reopens\n * it with the new value rather than being silently ignored.\n *\n * @param url - Full ws:// or wss:// URL.\n * @param options - Connection configuration and callbacks.\n * @returns Status, last frame, and the `send` / `reconnect` controls.\n */\nexport function useWebSocket<T = unknown>(\n url: string,\n options: UseWebSocketOptions<T> = {},\n): UseWebSocketResult<T> {\n const {\n enabled = true,\n protocols,\n maxRetries,\n initialBackoff,\n maxBackoff,\n pingInterval,\n respondToPing,\n queueWhileClosed,\n maxQueuedMessages,\n } = options;\n const [status, setStatus] = useState<WebSocketStatus>(\"idle\");\n const [lastMessage, setLastMessage] = useState<WebSocketMessage<T> | null>(null);\n const controllerRef = useRef<WebSocketController | null>(null);\n\n const optionsRef = useRef(options);\n useEffect(() => {\n optionsRef.current = options;\n });\n\n const protocolsKey = Array.isArray(protocols) ? protocols.join(\",\") : (protocols ?? \"\");\n\n useEffect(() => {\n if (!enabled || !url) {\n setStatus(\"idle\");\n return;\n }\n\n const controller = createWebSocket<T>(url, {\n protocols: optionsRef.current.protocols,\n maxRetries,\n initialBackoff,\n maxBackoff,\n pingInterval,\n pingPayload: optionsRef.current.pingPayload,\n respondToPing,\n pongPayload: optionsRef.current.pongPayload,\n queueWhileClosed,\n maxQueuedMessages,\n parser: (raw) => (optionsRef.current.parser ?? defaultParse<T>)(raw),\n onStatusChange: setStatus,\n onOpen: (event) => optionsRef.current.onOpen?.(event),\n onClose: (event) => optionsRef.current.onClose?.(event),\n onError: (event) => optionsRef.current.onError?.(event),\n onMessage: (message) => {\n setLastMessage(message);\n optionsRef.current.onMessage?.(message);\n },\n });\n controllerRef.current = controller;\n\n return () => {\n controller.close();\n controllerRef.current = null;\n };\n }, [\n url,\n enabled,\n protocolsKey,\n maxRetries,\n initialBackoff,\n maxBackoff,\n pingInterval,\n respondToPing,\n queueWhileClosed,\n maxQueuedMessages,\n ]);\n\n const send = useCallback((payload: string | Blob | BufferSource): boolean => {\n return controllerRef.current?.send(payload) ?? false;\n }, []);\n\n const reconnect = useCallback((): void => {\n controllerRef.current?.reconnect();\n }, []);\n\n return { status, lastMessage, send, reconnect };\n}\n"],"mappings":";;;AAiCA,SAAS,EAAgB,GAAgB;CACrC,IAAI;EACA,OAAO,KAAK,MAAM,CAAG;CACzB,QAAQ;EACJ,OAAO;CACX;AACJ;AAiBA,SAAgB,EACZ,GACA,IAAkC,CAAC,GACd;CACrB,IAAM,EACF,aAAU,IACV,cACA,eACA,mBACA,eACA,iBACA,kBACA,qBACA,yBACA,GACE,CAAC,GAAQ,KAAa,EAA0B,MAAM,GACtD,CAAC,GAAa,KAAkB,EAAqC,IAAI,GACzE,IAAgB,EAAmC,IAAI,GAEvD,IAAa,EAAO,CAAO;CA6DjC,OA5DA,QAAgB;EACZ,EAAW,UAAU;CACzB,CAAC,GAID,QAAgB;EACZ,IAAI,CAAC,KAAW,CAAC,GAAK;GAClB,EAAU,MAAM;GAChB;EACJ;EAEA,IAAM,IAAa,EAAmB,GAAK;GACvC,WAAW,EAAW,QAAQ;GAC9B;GACA;GACA;GACA;GACA,aAAa,EAAW,QAAQ;GAChC;GACA,aAAa,EAAW,QAAQ;GAChC;GACA;GACA,SAAS,OAAS,EAAW,QAAQ,UAAU,EAAA,CAAiB,CAAG;GACnE,gBAAgB;GAChB,SAAS,MAAU,EAAW,QAAQ,SAAS,CAAK;GACpD,UAAU,MAAU,EAAW,QAAQ,UAAU,CAAK;GACtD,UAAU,MAAU,EAAW,QAAQ,UAAU,CAAK;GACtD,YAAY,MAAY;IAEpB,AADA,EAAe,CAAO,GACtB,EAAW,QAAQ,YAAY,CAAO;GAC1C;EACJ,CAAC;EAGD,OAFA,EAAc,UAAU,SAEX;GAET,AADA,EAAW,MAAM,GACjB,EAAc,UAAU;EAC5B;CACJ,GAAG;EACC;EACA;EArCiB,MAAM,QAAQ,CAAS,IAAI,EAAU,KAAK,GAAG,IAAK,KAAa;EAuChF;EACA;EACA;EACA;EACA;EACA;EACA;CACJ,CAAC,GAUM;EAAE;EAAQ;EAAa,MARjB,GAAa,MACf,EAAc,SAAS,KAAK,CAAO,KAAK,IAChD,CAAC,CAM0B;EAAM,WAJlB,QAAwB;GACtC,EAAc,SAAS,UAAU;EACrC,GAAG,CAAC,CAEgC;CAAU;AAClD"}
|
package/package.json
CHANGED
package/template/src/lib/api.ts
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
1
|
import { createApiClient, createQueryKeys } from "tempest-react-sdk";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** Reads the current bearer token, or null when there is no session. */
|
|
4
|
+
export type TokenProvider = () => string | null;
|
|
5
|
+
|
|
6
|
+
let readToken: TokenProvider = () => null;
|
|
7
|
+
let handleUnauthorized: () => void = () => {};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tell the client where the session lives.
|
|
11
|
+
*
|
|
12
|
+
* The client ships knowing nothing about auth so a project without accounts
|
|
13
|
+
* can delete `@/stores/auth` and touch nothing here: with no call to this
|
|
14
|
+
* function every request goes out unauthenticated and a 401 is just an error.
|
|
15
|
+
* `@/stores/auth` calls it at module load to wire the store in.
|
|
16
|
+
*
|
|
17
|
+
* @param options - Where to read the token and what to do on a 401.
|
|
18
|
+
*/
|
|
19
|
+
export function configureApiAuth(options: {
|
|
20
|
+
getToken: TokenProvider;
|
|
21
|
+
onUnauthorized?: () => void;
|
|
22
|
+
}): void {
|
|
23
|
+
readToken = options.getToken;
|
|
24
|
+
handleUnauthorized = options.onUnauthorized ?? (() => {});
|
|
25
|
+
}
|
|
4
26
|
|
|
5
27
|
/**
|
|
6
|
-
* Typed HTTP client
|
|
7
|
-
*
|
|
28
|
+
* Typed HTTP client pointed at `VITE_API_URL`, reading the bearer token
|
|
29
|
+
* through whatever `configureApiAuth` registered.
|
|
8
30
|
*/
|
|
9
31
|
export const api = createApiClient({
|
|
10
32
|
baseURL: import.meta.env.VITE_API_URL ?? "http://127.0.0.1:8000",
|
|
11
|
-
getToken: () =>
|
|
12
|
-
onUnauthorized: () =>
|
|
33
|
+
getToken: () => readToken(),
|
|
34
|
+
onUnauthorized: () => handleUnauthorized(),
|
|
13
35
|
});
|
|
14
36
|
|
|
15
37
|
/** Stable, namespaced query keys for TanStack Query. */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createAuthStore, createSelectors } from "tempest-react-sdk";
|
|
2
2
|
|
|
3
|
+
import { configureApiAuth } from "@/lib/api";
|
|
4
|
+
|
|
3
5
|
/** The user shape your backend returns. Adjust to match your API. */
|
|
4
6
|
export interface User {
|
|
5
7
|
id: string;
|
|
@@ -13,3 +15,15 @@ export interface User {
|
|
|
13
15
|
* subscribe to a single slice.
|
|
14
16
|
*/
|
|
15
17
|
export const useAuth = createSelectors(createAuthStore<User>({ name: "app-auth" }));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hand the store to the HTTP client.
|
|
21
|
+
*
|
|
22
|
+
* The dependency points this way on purpose: `@/lib/api` knows nothing about
|
|
23
|
+
* auth, so a project without accounts deletes this file and the client keeps
|
|
24
|
+
* working unauthenticated.
|
|
25
|
+
*/
|
|
26
|
+
configureApiAuth({
|
|
27
|
+
getToken: () => useAuth.getState().token,
|
|
28
|
+
onUnauthorized: () => useAuth.getState().logout(),
|
|
29
|
+
});
|