supabase-edge-function-continuous-stream 1.0.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 +54 -0
- package/dist/chunk-GP2SEFBH.js +462 -0
- package/dist/index.cjs +507 -0
- package/dist/index.d.cts +43 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +28 -0
- package/dist/react-BgnmZ7M3.d.cts +75 -0
- package/dist/react-BgnmZ7M3.d.ts +75 -0
- package/dist/react.cjs +485 -0
- package/dist/react.d.cts +1 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.js +6 -0
- package/package.json +53 -0
- package/react.js +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
type EdgeFunctionRawMessage = Record<string, unknown> & {
|
|
2
|
+
type: string;
|
|
3
|
+
};
|
|
4
|
+
interface EdgeFunctionMessageContext<TResponse extends Record<string, unknown>> {
|
|
5
|
+
resolve: (value: TResponse) => void;
|
|
6
|
+
reject: (reason: Error) => void;
|
|
7
|
+
closeSocket: () => void;
|
|
8
|
+
clearOverallTimeout: () => void;
|
|
9
|
+
isResolved: () => boolean;
|
|
10
|
+
}
|
|
11
|
+
type EdgeStreamConfig = {
|
|
12
|
+
functionPath: string;
|
|
13
|
+
};
|
|
14
|
+
/** Callbacks a caller can wire up for side-effects during streaming. */
|
|
15
|
+
interface StandardAiCallbacks {
|
|
16
|
+
onServerAction?: (type: string, data: unknown) => void;
|
|
17
|
+
toUserMessage?: (error: unknown) => string | null;
|
|
18
|
+
}
|
|
19
|
+
type WarmupOptions = {
|
|
20
|
+
onServerAction?: (type: string, data: unknown) => void;
|
|
21
|
+
};
|
|
22
|
+
type StartStreamOptions<TResponse extends Record<string, unknown>> = {
|
|
23
|
+
/** Optional: Use a custom message handler. If omitted, it uses the Standard AI Protocol handler. */
|
|
24
|
+
onMessage?: (message: EdgeFunctionRawMessage, ctx: EdgeFunctionMessageContext<TResponse>) => void;
|
|
25
|
+
/** Optional: Defaults to seed the response object if not sent by the server. */
|
|
26
|
+
defaults?: Partial<TResponse>;
|
|
27
|
+
/** Optional: Listener for server actions (status, complete, etc.) */
|
|
28
|
+
onServerAction?: (type: string, data: unknown) => void;
|
|
29
|
+
/** Optional: Tags to invalidate on completion. */
|
|
30
|
+
invalidateTags?: {
|
|
31
|
+
tags: Array<string | {
|
|
32
|
+
type: string;
|
|
33
|
+
id?: string | number;
|
|
34
|
+
}>;
|
|
35
|
+
condition?: (response?: TResponse) => boolean;
|
|
36
|
+
};
|
|
37
|
+
/** Optional: Edge worker expiry from server checkpoint metadata */
|
|
38
|
+
getWorkerExpiresAt?: () => number | null;
|
|
39
|
+
};
|
|
40
|
+
/** Mutable ref bag used by connectEdgeSocket without React */
|
|
41
|
+
type Ref<T> = {
|
|
42
|
+
current: T;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Client-side edge worker timing configuration */
|
|
46
|
+
type EdgeWorkerLimits = {
|
|
47
|
+
edgeWorkerTtlMs: number;
|
|
48
|
+
edgeRotateThresholdMs: number;
|
|
49
|
+
overallTimeoutMs: number;
|
|
50
|
+
};
|
|
51
|
+
/** Default limits aligned with short-lived edge workers (~2.5 min TTL) */
|
|
52
|
+
declare const DEFAULT_EDGE_WORKER_LIMITS: EdgeWorkerLimits;
|
|
53
|
+
|
|
54
|
+
type EdgeStreamCoreDeps = {
|
|
55
|
+
getAccessToken: () => Promise<string>;
|
|
56
|
+
getSupabaseUrl: () => string;
|
|
57
|
+
workerLimits: EdgeWorkerLimits;
|
|
58
|
+
toUserMessage?: (error: unknown) => string | null;
|
|
59
|
+
invalidateTags?: (tags: Array<string | {
|
|
60
|
+
type: string;
|
|
61
|
+
id?: string | number;
|
|
62
|
+
}>) => void;
|
|
63
|
+
};
|
|
64
|
+
/** Factory for a WebSocket streaming hook wired to a host app */
|
|
65
|
+
declare function createUseEdgeStream(deps: EdgeStreamCoreDeps): <TPayload, TResponse extends Record<string, unknown>>(config: EdgeStreamConfig) => {
|
|
66
|
+
warmup: (payload: TPayload, options?: WarmupOptions) => Promise<void>;
|
|
67
|
+
send: (payload: TPayload, options: StartStreamOptions<TResponse>) => Promise<TResponse>;
|
|
68
|
+
abort: () => void;
|
|
69
|
+
isLoading: boolean;
|
|
70
|
+
error: Error | null;
|
|
71
|
+
data: TResponse | undefined;
|
|
72
|
+
isConnected: boolean;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export { DEFAULT_EDGE_WORKER_LIMITS as D, type EdgeFunctionRawMessage as E, type Ref as R, type StandardAiCallbacks as S, type WarmupOptions as W, type EdgeFunctionMessageContext as a, type EdgeStreamConfig as b, type EdgeStreamCoreDeps as c, type EdgeWorkerLimits as d, type StartStreamOptions as e, createUseEdgeStream as f };
|
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react.ts
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
createUseEdgeStream: () => createUseEdgeStream
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(react_exports);
|
|
26
|
+
|
|
27
|
+
// src/createUseEdgeStream.ts
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
|
|
30
|
+
// src/constants.ts
|
|
31
|
+
var MAX_RETRIES = 200;
|
|
32
|
+
var INITIAL_RETRY_DELAY_MS = 1e3;
|
|
33
|
+
var CONNECTION_TIMEOUT_MS = 1e4;
|
|
34
|
+
|
|
35
|
+
// src/connection.ts
|
|
36
|
+
async function connectEdgeSocket(deps) {
|
|
37
|
+
const {
|
|
38
|
+
functionPath,
|
|
39
|
+
getAccessToken,
|
|
40
|
+
getSupabaseUrl,
|
|
41
|
+
wsRef,
|
|
42
|
+
isExplicitDisconnectRef,
|
|
43
|
+
isConnectingRef,
|
|
44
|
+
retryCountRef,
|
|
45
|
+
socketOpenedAtRef,
|
|
46
|
+
isWarmupReadyRef,
|
|
47
|
+
warmupWaitersRef,
|
|
48
|
+
passiveOnServerActionRef,
|
|
49
|
+
activeRequestRef,
|
|
50
|
+
lastWarmupPayloadRef,
|
|
51
|
+
setIsConnected,
|
|
52
|
+
closeSocket,
|
|
53
|
+
sendWarmupPayload,
|
|
54
|
+
settleWarmupWaiters
|
|
55
|
+
} = deps;
|
|
56
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) return Promise.resolve();
|
|
57
|
+
if (isConnectingRef.current) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const checkInterval = setInterval(() => {
|
|
60
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
61
|
+
clearInterval(checkInterval);
|
|
62
|
+
resolve();
|
|
63
|
+
} else if (wsRef.current?.readyState === WebSocket.CLOSED) {
|
|
64
|
+
clearInterval(checkInterval);
|
|
65
|
+
reject(new Error("Connection closed during check"));
|
|
66
|
+
}
|
|
67
|
+
}, 100);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
isConnectingRef.current = true;
|
|
71
|
+
isExplicitDisconnectRef.current = false;
|
|
72
|
+
try {
|
|
73
|
+
const accessToken = await getAccessToken();
|
|
74
|
+
const supabaseUrl = getSupabaseUrl();
|
|
75
|
+
if (!supabaseUrl) throw new Error("Missing Supabase URL");
|
|
76
|
+
const wsUrl = supabaseUrl.replace(
|
|
77
|
+
/^https?:\/\//,
|
|
78
|
+
(m) => m === "https://" ? "wss://" : "ws://"
|
|
79
|
+
);
|
|
80
|
+
const functionUrl = `${wsUrl}/functions/v1/${functionPath}`;
|
|
81
|
+
return await new Promise((resolveConnection, rejectConnection) => {
|
|
82
|
+
let connectionTimeout = null;
|
|
83
|
+
if (wsRef.current) {
|
|
84
|
+
try {
|
|
85
|
+
wsRef.current.onopen = null;
|
|
86
|
+
wsRef.current.onmessage = null;
|
|
87
|
+
wsRef.current.onerror = null;
|
|
88
|
+
wsRef.current.onclose = null;
|
|
89
|
+
if (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING) {
|
|
90
|
+
wsRef.current.close();
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const urlWithToken = new URL(functionUrl);
|
|
96
|
+
urlWithToken.searchParams.set("jwt", accessToken);
|
|
97
|
+
wsRef.current = new WebSocket(urlWithToken.toString());
|
|
98
|
+
if (retryCountRef.current === 0) {
|
|
99
|
+
connectionTimeout = setTimeout(() => {
|
|
100
|
+
if (wsRef.current && wsRef.current.readyState !== WebSocket.OPEN) {
|
|
101
|
+
isExplicitDisconnectRef.current = false;
|
|
102
|
+
closeSocket();
|
|
103
|
+
}
|
|
104
|
+
}, CONNECTION_TIMEOUT_MS);
|
|
105
|
+
}
|
|
106
|
+
wsRef.current.onopen = () => {
|
|
107
|
+
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
108
|
+
retryCountRef.current = 0;
|
|
109
|
+
isConnectingRef.current = false;
|
|
110
|
+
setIsConnected(true);
|
|
111
|
+
socketOpenedAtRef.current = Date.now();
|
|
112
|
+
if (lastWarmupPayloadRef.current) {
|
|
113
|
+
sendWarmupPayload();
|
|
114
|
+
}
|
|
115
|
+
resolveConnection();
|
|
116
|
+
};
|
|
117
|
+
wsRef.current.onerror = () => {
|
|
118
|
+
};
|
|
119
|
+
const retryConnect = () => {
|
|
120
|
+
connectEdgeSocket(deps).then(resolveConnection).catch(rejectConnection);
|
|
121
|
+
};
|
|
122
|
+
wsRef.current.onclose = () => {
|
|
123
|
+
if (connectionTimeout) clearTimeout(connectionTimeout);
|
|
124
|
+
isConnectingRef.current = false;
|
|
125
|
+
setIsConnected(false);
|
|
126
|
+
isWarmupReadyRef.current = false;
|
|
127
|
+
const willRetry = !isExplicitDisconnectRef.current && retryCountRef.current < MAX_RETRIES;
|
|
128
|
+
if (!willRetry) {
|
|
129
|
+
settleWarmupWaiters(new Error("WebSocket closed"));
|
|
130
|
+
}
|
|
131
|
+
if (isExplicitDisconnectRef.current) {
|
|
132
|
+
rejectConnection(
|
|
133
|
+
new Error("WebSocket closed by server or aborted")
|
|
134
|
+
);
|
|
135
|
+
} else if (retryCountRef.current < MAX_RETRIES) {
|
|
136
|
+
retryCountRef.current++;
|
|
137
|
+
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCountRef.current - 1);
|
|
138
|
+
setTimeout(retryConnect, delay);
|
|
139
|
+
} else {
|
|
140
|
+
rejectConnection(
|
|
141
|
+
new Error(
|
|
142
|
+
`WebSocket connection failed after ${retryCountRef.current} retries`
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
wsRef.current.onmessage = (event) => {
|
|
148
|
+
try {
|
|
149
|
+
const message = JSON.parse(event.data);
|
|
150
|
+
if (message.type === "status" && message.data === "ready") {
|
|
151
|
+
isWarmupReadyRef.current = true;
|
|
152
|
+
settleWarmupWaiters();
|
|
153
|
+
}
|
|
154
|
+
if (message.type === "error" && warmupWaitersRef.current.length > 0 && !isWarmupReadyRef.current) {
|
|
155
|
+
isWarmupReadyRef.current = false;
|
|
156
|
+
settleWarmupWaiters(
|
|
157
|
+
new Error(String(message.data ?? "Warmup failed"))
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const passive = passiveOnServerActionRef.current;
|
|
161
|
+
if (passive && !activeRequestRef.current) {
|
|
162
|
+
const isWarmupControl = message.type === "status" && (message.data === "ready" || message.data === "context");
|
|
163
|
+
if (!isWarmupControl) {
|
|
164
|
+
passive(message.type, message.data);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!activeRequestRef.current) return;
|
|
168
|
+
const { ctx, handler } = activeRequestRef.current;
|
|
169
|
+
handler(message, ctx);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (!activeRequestRef.current) return;
|
|
172
|
+
const ctx = activeRequestRef.current.ctx;
|
|
173
|
+
ctx.reject(
|
|
174
|
+
new Error(
|
|
175
|
+
`Failed to parse WebSocket message: ${error instanceof Error ? error.message : String(error)}`
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
isConnectingRef.current = false;
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/handler.ts
|
|
188
|
+
function createStandardAiMessageHandler(defaults = {}, callbacks) {
|
|
189
|
+
const state = {};
|
|
190
|
+
const buildResponse = (overrides) => ({
|
|
191
|
+
...defaults,
|
|
192
|
+
...state,
|
|
193
|
+
...overrides
|
|
194
|
+
});
|
|
195
|
+
return (message, ctx) => {
|
|
196
|
+
if (message.type === "response_text" && typeof message.data === "string") {
|
|
197
|
+
state.reply = message.data;
|
|
198
|
+
}
|
|
199
|
+
if (typeof message.data === "object" && message.data !== null && message.type !== "error") {
|
|
200
|
+
Object.assign(state, message.data);
|
|
201
|
+
}
|
|
202
|
+
if (message.type === "error") {
|
|
203
|
+
const raw = message.data ?? "Server error";
|
|
204
|
+
const userMsg = callbacks?.toUserMessage?.(raw);
|
|
205
|
+
if (userMsg) callbacks?.onServerAction?.("error", userMsg);
|
|
206
|
+
ctx.reject(new Error(raw));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (message.type === "complete") {
|
|
210
|
+
ctx.clearOverallTimeout();
|
|
211
|
+
callbacks?.onServerAction?.("complete", message.data);
|
|
212
|
+
if (!ctx.isResolved()) {
|
|
213
|
+
ctx.resolve(buildResponse(message.data));
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
callbacks?.onServerAction?.(message.type, message.data);
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/createUseEdgeStream.ts
|
|
222
|
+
function createUseEdgeStream(deps) {
|
|
223
|
+
const {
|
|
224
|
+
getAccessToken,
|
|
225
|
+
getSupabaseUrl,
|
|
226
|
+
workerLimits,
|
|
227
|
+
toUserMessage,
|
|
228
|
+
invalidateTags
|
|
229
|
+
} = deps;
|
|
230
|
+
const { edgeWorkerTtlMs, edgeRotateThresholdMs, overallTimeoutMs } = workerLimits;
|
|
231
|
+
return function useEdgeStream(config) {
|
|
232
|
+
const [isLoading, setIsLoading] = (0, import_react.useState)(false);
|
|
233
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
234
|
+
const [data, setData] = (0, import_react.useState)(void 0);
|
|
235
|
+
const wsRef = (0, import_react.useRef)(null);
|
|
236
|
+
const isExplicitDisconnectRef = (0, import_react.useRef)(false);
|
|
237
|
+
const [isConnected, setIsConnected] = (0, import_react.useState)(false);
|
|
238
|
+
const isConnectingRef = (0, import_react.useRef)(false);
|
|
239
|
+
const lastWarmupPayloadRef = (0, import_react.useRef)(null);
|
|
240
|
+
const isWarmupReadyRef = (0, import_react.useRef)(false);
|
|
241
|
+
const warmupWaitersRef = (0, import_react.useRef)([]);
|
|
242
|
+
const retryCountRef = (0, import_react.useRef)(0);
|
|
243
|
+
const passiveOnServerActionRef = (0, import_react.useRef)(null);
|
|
244
|
+
const socketOpenedAtRef = (0, import_react.useRef)(null);
|
|
245
|
+
const settleWarmupWaiters = (0, import_react.useCallback)((warmupError) => {
|
|
246
|
+
const waiters = warmupWaitersRef.current;
|
|
247
|
+
warmupWaitersRef.current = [];
|
|
248
|
+
if (warmupError) {
|
|
249
|
+
waiters.forEach((w) => w.reject(warmupError));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
waiters.forEach((w) => w.resolve());
|
|
253
|
+
}, []);
|
|
254
|
+
const waitForWarmupReady = (0, import_react.useCallback)(() => {
|
|
255
|
+
if (isWarmupReadyRef.current) return Promise.resolve();
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
257
|
+
warmupWaitersRef.current.push({ resolve, reject });
|
|
258
|
+
});
|
|
259
|
+
}, []);
|
|
260
|
+
const sendWarmupPayload = (0, import_react.useCallback)(() => {
|
|
261
|
+
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!lastWarmupPayloadRef.current) return;
|
|
265
|
+
isWarmupReadyRef.current = false;
|
|
266
|
+
wsRef.current.send(
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
type: "client_warmup",
|
|
269
|
+
data: lastWarmupPayloadRef.current
|
|
270
|
+
})
|
|
271
|
+
);
|
|
272
|
+
}, []);
|
|
273
|
+
const activeRequestRef = (0, import_react.useRef)(null);
|
|
274
|
+
(0, import_react.useEffect)(() => {
|
|
275
|
+
return () => {
|
|
276
|
+
isExplicitDisconnectRef.current = true;
|
|
277
|
+
if (wsRef.current) {
|
|
278
|
+
try {
|
|
279
|
+
wsRef.current.close();
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}, []);
|
|
285
|
+
const closeSocket = (0, import_react.useCallback)(() => {
|
|
286
|
+
if (!wsRef.current) return;
|
|
287
|
+
try {
|
|
288
|
+
wsRef.current.close();
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
socketOpenedAtRef.current = null;
|
|
292
|
+
}, []);
|
|
293
|
+
const abort = (0, import_react.useCallback)(() => {
|
|
294
|
+
isExplicitDisconnectRef.current = true;
|
|
295
|
+
closeSocket();
|
|
296
|
+
setIsLoading(false);
|
|
297
|
+
if (activeRequestRef.current) {
|
|
298
|
+
activeRequestRef.current.reject(new Error("Request aborted"));
|
|
299
|
+
if (activeRequestRef.current.overallTimeout) {
|
|
300
|
+
clearTimeout(activeRequestRef.current.overallTimeout);
|
|
301
|
+
}
|
|
302
|
+
activeRequestRef.current = null;
|
|
303
|
+
}
|
|
304
|
+
}, [closeSocket]);
|
|
305
|
+
const connectWebSocket = (0, import_react.useCallback)(
|
|
306
|
+
() => connectEdgeSocket({
|
|
307
|
+
functionPath: config.functionPath,
|
|
308
|
+
getAccessToken,
|
|
309
|
+
getSupabaseUrl,
|
|
310
|
+
wsRef,
|
|
311
|
+
isExplicitDisconnectRef,
|
|
312
|
+
isConnectingRef,
|
|
313
|
+
retryCountRef,
|
|
314
|
+
socketOpenedAtRef,
|
|
315
|
+
isWarmupReadyRef,
|
|
316
|
+
warmupWaitersRef,
|
|
317
|
+
passiveOnServerActionRef,
|
|
318
|
+
activeRequestRef,
|
|
319
|
+
lastWarmupPayloadRef,
|
|
320
|
+
setIsConnected,
|
|
321
|
+
closeSocket,
|
|
322
|
+
sendWarmupPayload,
|
|
323
|
+
settleWarmupWaiters
|
|
324
|
+
}),
|
|
325
|
+
[
|
|
326
|
+
config.functionPath,
|
|
327
|
+
closeSocket,
|
|
328
|
+
sendWarmupPayload,
|
|
329
|
+
settleWarmupWaiters
|
|
330
|
+
]
|
|
331
|
+
);
|
|
332
|
+
const rotateConnectionIfNeeded = (0, import_react.useCallback)(
|
|
333
|
+
async (getExpiresAt) => {
|
|
334
|
+
const fallbackExpires = socketOpenedAtRef.current ? socketOpenedAtRef.current + edgeWorkerTtlMs : null;
|
|
335
|
+
const expiresAt = getExpiresAt?.() ?? fallbackExpires;
|
|
336
|
+
if (!expiresAt) return;
|
|
337
|
+
if (expiresAt - Date.now() >= edgeRotateThresholdMs) return;
|
|
338
|
+
isExplicitDisconnectRef.current = false;
|
|
339
|
+
closeSocket();
|
|
340
|
+
isWarmupReadyRef.current = false;
|
|
341
|
+
await connectWebSocket();
|
|
342
|
+
},
|
|
343
|
+
[closeSocket, connectWebSocket, edgeRotateThresholdMs, edgeWorkerTtlMs]
|
|
344
|
+
);
|
|
345
|
+
const warmup = (0, import_react.useCallback)(
|
|
346
|
+
async (payload, options) => {
|
|
347
|
+
if (options?.onServerAction) {
|
|
348
|
+
passiveOnServerActionRef.current = options.onServerAction;
|
|
349
|
+
}
|
|
350
|
+
lastWarmupPayloadRef.current = payload;
|
|
351
|
+
isWarmupReadyRef.current = false;
|
|
352
|
+
const wasOpen = wsRef.current?.readyState === WebSocket.OPEN;
|
|
353
|
+
await connectWebSocket();
|
|
354
|
+
if (!isWarmupReadyRef.current) {
|
|
355
|
+
if (wasOpen) sendWarmupPayload();
|
|
356
|
+
await waitForWarmupReady();
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
[connectWebSocket, sendWarmupPayload, waitForWarmupReady]
|
|
360
|
+
);
|
|
361
|
+
const send = (0, import_react.useCallback)(
|
|
362
|
+
async (payload, options) => {
|
|
363
|
+
if (!isLoading) {
|
|
364
|
+
await rotateConnectionIfNeeded(options.getWorkerExpiresAt);
|
|
365
|
+
}
|
|
366
|
+
setIsLoading(true);
|
|
367
|
+
setError(null);
|
|
368
|
+
setData(void 0);
|
|
369
|
+
isExplicitDisconnectRef.current = false;
|
|
370
|
+
let resolved = false;
|
|
371
|
+
const ctx = {
|
|
372
|
+
resolve: (value) => {
|
|
373
|
+
resolved = true;
|
|
374
|
+
setData(value);
|
|
375
|
+
setIsLoading(false);
|
|
376
|
+
if (options.invalidateTags && invalidateTags) {
|
|
377
|
+
const shouldInvalidate = options.invalidateTags.condition ? options.invalidateTags.condition(value) : true;
|
|
378
|
+
if (shouldInvalidate) {
|
|
379
|
+
invalidateTags(options.invalidateTags.tags);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (activeRequestRef.current?.overallTimeout) {
|
|
383
|
+
clearTimeout(activeRequestRef.current.overallTimeout);
|
|
384
|
+
}
|
|
385
|
+
if (activeRequestRef.current?.resolve) {
|
|
386
|
+
activeRequestRef.current.resolve(value);
|
|
387
|
+
}
|
|
388
|
+
activeRequestRef.current = null;
|
|
389
|
+
},
|
|
390
|
+
reject: (err) => {
|
|
391
|
+
resolved = true;
|
|
392
|
+
setError(err);
|
|
393
|
+
setIsLoading(false);
|
|
394
|
+
if (activeRequestRef.current?.overallTimeout) {
|
|
395
|
+
clearTimeout(activeRequestRef.current.overallTimeout);
|
|
396
|
+
}
|
|
397
|
+
if (activeRequestRef.current?.reject) {
|
|
398
|
+
activeRequestRef.current.reject(err);
|
|
399
|
+
}
|
|
400
|
+
activeRequestRef.current = null;
|
|
401
|
+
},
|
|
402
|
+
closeSocket,
|
|
403
|
+
clearOverallTimeout: () => {
|
|
404
|
+
if (activeRequestRef.current?.overallTimeout) {
|
|
405
|
+
clearTimeout(activeRequestRef.current.overallTimeout);
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
isResolved: () => resolved
|
|
409
|
+
};
|
|
410
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
411
|
+
const overallTimeout = setTimeout(() => {
|
|
412
|
+
if (!resolved) {
|
|
413
|
+
const err = new Error("WebSocket request timeout");
|
|
414
|
+
setError(err);
|
|
415
|
+
setIsLoading(false);
|
|
416
|
+
rejectPromise(err);
|
|
417
|
+
activeRequestRef.current = null;
|
|
418
|
+
}
|
|
419
|
+
}, overallTimeoutMs);
|
|
420
|
+
const handler = options.onMessage || createStandardAiMessageHandler(options.defaults || {}, {
|
|
421
|
+
onServerAction: options.onServerAction,
|
|
422
|
+
toUserMessage
|
|
423
|
+
});
|
|
424
|
+
activeRequestRef.current = {
|
|
425
|
+
payload,
|
|
426
|
+
options,
|
|
427
|
+
ctx,
|
|
428
|
+
resolve: resolvePromise,
|
|
429
|
+
reject: rejectPromise,
|
|
430
|
+
overallTimeout,
|
|
431
|
+
handler
|
|
432
|
+
};
|
|
433
|
+
connectWebSocket().then(async () => {
|
|
434
|
+
if (!activeRequestRef.current || activeRequestRef.current.sent) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (!lastWarmupPayloadRef.current) {
|
|
438
|
+
throw new Error("Warmup required");
|
|
439
|
+
}
|
|
440
|
+
if (!isWarmupReadyRef.current) {
|
|
441
|
+
sendWarmupPayload();
|
|
442
|
+
await waitForWarmupReady();
|
|
443
|
+
}
|
|
444
|
+
if (wsRef.current?.readyState === WebSocket.OPEN && activeRequestRef.current && !activeRequestRef.current.sent) {
|
|
445
|
+
activeRequestRef.current.sent = true;
|
|
446
|
+
wsRef.current.send(
|
|
447
|
+
JSON.stringify({ type: "client_message", data: payload })
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}).catch((err) => {
|
|
451
|
+
clearTimeout(overallTimeout);
|
|
452
|
+
setError(err);
|
|
453
|
+
setIsLoading(false);
|
|
454
|
+
rejectPromise(err);
|
|
455
|
+
activeRequestRef.current = null;
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
},
|
|
459
|
+
[
|
|
460
|
+
connectWebSocket,
|
|
461
|
+
closeSocket,
|
|
462
|
+
sendWarmupPayload,
|
|
463
|
+
waitForWarmupReady,
|
|
464
|
+
rotateConnectionIfNeeded,
|
|
465
|
+
isLoading,
|
|
466
|
+
overallTimeoutMs,
|
|
467
|
+
toUserMessage,
|
|
468
|
+
invalidateTags
|
|
469
|
+
]
|
|
470
|
+
);
|
|
471
|
+
return {
|
|
472
|
+
warmup,
|
|
473
|
+
send,
|
|
474
|
+
abort,
|
|
475
|
+
isLoading,
|
|
476
|
+
error,
|
|
477
|
+
data,
|
|
478
|
+
isConnected
|
|
479
|
+
};
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
483
|
+
0 && (module.exports = {
|
|
484
|
+
createUseEdgeStream
|
|
485
|
+
});
|
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { a as EdgeFunctionMessageContext, E as EdgeFunctionRawMessage, b as EdgeStreamConfig, c as EdgeStreamCoreDeps, S as StandardAiCallbacks, e as StartStreamOptions, W as WarmupOptions, f as createUseEdgeStream } from './react-BgnmZ7M3.cjs';
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { a as EdgeFunctionMessageContext, E as EdgeFunctionRawMessage, b as EdgeStreamConfig, c as EdgeStreamCoreDeps, S as StandardAiCallbacks, e as StartStreamOptions, W as WarmupOptions, f as createUseEdgeStream } from './react-BgnmZ7M3.js';
|
package/dist/react.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "supabase-edge-function-continuous-stream",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Continuous WebSocket streaming for Supabase edge functions",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./react": {
|
|
14
|
+
"types": "./dist/react.d.ts",
|
|
15
|
+
"import": "./react.js",
|
|
16
|
+
"require": "./dist/react.cjs",
|
|
17
|
+
"default": "./react.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"react.js",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"prepublishOnly": "npm run build",
|
|
29
|
+
"test": "vitest run"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"react": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/react": "^19",
|
|
41
|
+
"react": "^19",
|
|
42
|
+
"tsup": "^8.5.0",
|
|
43
|
+
"typescript": "^5.9.0",
|
|
44
|
+
"vitest": "^3.2.4"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"supabase",
|
|
48
|
+
"edge-functions",
|
|
49
|
+
"websocket",
|
|
50
|
+
"streaming",
|
|
51
|
+
"react"
|
|
52
|
+
]
|
|
53
|
+
}
|
package/react.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./dist/react.js";
|