@ragable/sdk 0.6.14 → 0.6.15
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/dist/index.d.mts +65 -5
- package/dist/index.d.ts +65 -5
- package/dist/index.js +152 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +152 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2298,6 +2298,20 @@ var RagableBrowserDatabaseClient = class {
|
|
|
2298
2298
|
* Postgres `LISTEN` / `NOTIFY` realtime via server-proxied SSE.
|
|
2299
2299
|
* Channels must be lowercase identifiers: `[a-z_][a-z0-9_]*` (max 63 chars).
|
|
2300
2300
|
*/
|
|
2301
|
+
/**
|
|
2302
|
+
* Postgres `LISTEN` / `NOTIFY` realtime via server-proxied SSE.
|
|
2303
|
+
*
|
|
2304
|
+
* Returns a `BrowserRealtimeSubscription` with:
|
|
2305
|
+
* - `unsubscribe()` — permanently close the subscription (stops reconnects).
|
|
2306
|
+
* - `status` — current connection state: `"connecting"` | `"connected"` | `"reconnecting"` | `"disconnected"`.
|
|
2307
|
+
*
|
|
2308
|
+
* The subscription automatically reconnects with exponential backoff when the
|
|
2309
|
+
* stream drops. Server heartbeats (every 15 s) are monitored — if none arrive
|
|
2310
|
+
* within `heartbeatTimeoutMs` (default 45 s), the connection is treated as dead
|
|
2311
|
+
* and a reconnect is triggered. Auth errors (401/403/404) are non-retryable.
|
|
2312
|
+
*
|
|
2313
|
+
* Channel names must be lowercase identifiers: `[a-z_][a-z0-9_]*` (max 63 chars).
|
|
2314
|
+
*/
|
|
2301
2315
|
__publicField(this, "realtime", {
|
|
2302
2316
|
subscribe: (params) => subscribeBrowserRealtime(
|
|
2303
2317
|
this.options,
|
|
@@ -2327,9 +2341,13 @@ function followAbortSignal(parent, child) {
|
|
|
2327
2341
|
}
|
|
2328
2342
|
parent.addEventListener("abort", () => child.abort(), { once: true });
|
|
2329
2343
|
}
|
|
2344
|
+
function backoffDelay(attempt, baseMs, maxMs) {
|
|
2345
|
+
const exp = Math.min(baseMs * 2 ** (attempt - 1), maxMs);
|
|
2346
|
+
const jitter = exp * (0.5 + Math.random() * 0.5);
|
|
2347
|
+
return Math.round(jitter);
|
|
2348
|
+
}
|
|
2330
2349
|
async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params) {
|
|
2331
2350
|
const gid = requireAuthGroupId(options);
|
|
2332
|
-
const token = await resolveDatabaseAuthBearer(options, ragableAuth);
|
|
2333
2351
|
const databaseInstanceId = params.databaseInstanceId?.trim() || options.databaseInstanceId?.trim();
|
|
2334
2352
|
if (!databaseInstanceId) {
|
|
2335
2353
|
throw new RagableError(
|
|
@@ -2345,41 +2363,75 @@ async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params)
|
|
|
2345
2363
|
{ code: "SDK_REALTIME_CHANNELS_REQUIRED" }
|
|
2346
2364
|
);
|
|
2347
2365
|
}
|
|
2348
|
-
const
|
|
2349
|
-
|
|
2350
|
-
const
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2366
|
+
const maxAttempts = params.maxReconnectAttempts ?? Infinity;
|
|
2367
|
+
const baseDelay = params.reconnectBaseDelayMs ?? 1e3;
|
|
2368
|
+
const maxDelay = params.reconnectMaxDelayMs ?? 3e4;
|
|
2369
|
+
const heartbeatTimeout = params.heartbeatTimeoutMs ?? 45e3;
|
|
2370
|
+
const lifecycleAc = new AbortController();
|
|
2371
|
+
followAbortSignal(params.signal, lifecycleAc);
|
|
2372
|
+
let currentStatus = "connecting";
|
|
2373
|
+
const setStatus = (s) => {
|
|
2374
|
+
if (s === currentStatus) return;
|
|
2375
|
+
currentStatus = s;
|
|
2376
|
+
params.onStatusChange?.(s);
|
|
2377
|
+
};
|
|
2378
|
+
const subscription = {
|
|
2379
|
+
unsubscribe: () => lifecycleAc.abort(),
|
|
2380
|
+
get status() {
|
|
2381
|
+
return currentStatus;
|
|
2363
2382
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
const
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2383
|
+
};
|
|
2384
|
+
setStatus("connecting");
|
|
2385
|
+
async function connectOnce(signal) {
|
|
2386
|
+
const token = await resolveDatabaseAuthBearer(options, ragableAuth);
|
|
2387
|
+
const headers = new Headers(options.headers);
|
|
2388
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
2389
|
+
headers.set("Content-Type", "application/json");
|
|
2390
|
+
const response = await fetchImpl(
|
|
2391
|
+
`${normalizeBrowserApiBase()}/auth-groups/${gid}/data/realtime/stream`,
|
|
2392
|
+
{
|
|
2393
|
+
method: "POST",
|
|
2394
|
+
headers,
|
|
2395
|
+
body: JSON.stringify({
|
|
2396
|
+
databaseInstanceId,
|
|
2397
|
+
channels: params.channels
|
|
2398
|
+
}),
|
|
2399
|
+
signal
|
|
2400
|
+
}
|
|
2376
2401
|
);
|
|
2377
|
-
|
|
2378
|
-
|
|
2402
|
+
if (!response.ok) {
|
|
2403
|
+
const payload = await parseMaybeJsonBody(response);
|
|
2404
|
+
const message = extractErrorMessage(payload, response.statusText);
|
|
2405
|
+
throw new RagableError(message, response.status, payload);
|
|
2406
|
+
}
|
|
2407
|
+
const streamBody = response.body;
|
|
2408
|
+
if (!streamBody) {
|
|
2409
|
+
throw new RagableError("Realtime stream has no body", 502, {
|
|
2410
|
+
code: "SDK_REALTIME_NO_BODY"
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
let heartbeatTimer = null;
|
|
2414
|
+
const resetHeartbeatTimer = () => {
|
|
2415
|
+
if (heartbeatTimer) clearTimeout(heartbeatTimer);
|
|
2416
|
+
heartbeatTimer = setTimeout(() => {
|
|
2417
|
+
streamReader?.cancel().catch(() => void 0);
|
|
2418
|
+
}, heartbeatTimeout);
|
|
2419
|
+
};
|
|
2420
|
+
let streamReader = null;
|
|
2379
2421
|
try {
|
|
2380
|
-
|
|
2422
|
+
streamReader = streamBody.getReader();
|
|
2423
|
+
const decoder = new TextDecoder();
|
|
2424
|
+
let buffer = "";
|
|
2425
|
+
resetHeartbeatTimer();
|
|
2426
|
+
const processEvent = (evt) => {
|
|
2427
|
+
if (evt.type === "realtime:heartbeat") {
|
|
2428
|
+
resetHeartbeatTimer();
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
resetHeartbeatTimer();
|
|
2381
2432
|
if (evt.type === "realtime:ready") {
|
|
2382
2433
|
const ch = evt.channels;
|
|
2434
|
+
setStatus("connected");
|
|
2383
2435
|
params.onReady?.(
|
|
2384
2436
|
Array.isArray(ch) ? ch.map((c) => String(c)) : []
|
|
2385
2437
|
);
|
|
@@ -2392,15 +2444,78 @@ async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params)
|
|
|
2392
2444
|
} else if (evt.type === "realtime:error") {
|
|
2393
2445
|
params.onError?.(String(evt.message ?? "Realtime error"));
|
|
2394
2446
|
}
|
|
2447
|
+
};
|
|
2448
|
+
while (true) {
|
|
2449
|
+
const { done, value } = await streamReader.read();
|
|
2450
|
+
if (done) break;
|
|
2451
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2452
|
+
let boundary = buffer.indexOf("\n\n");
|
|
2453
|
+
while (boundary !== -1) {
|
|
2454
|
+
const block = buffer.slice(0, boundary);
|
|
2455
|
+
buffer = buffer.slice(boundary + 2);
|
|
2456
|
+
for (const line of block.split("\n")) {
|
|
2457
|
+
const dataPrefix = "data: ";
|
|
2458
|
+
if (!line.startsWith(dataPrefix)) continue;
|
|
2459
|
+
const json = line.slice(dataPrefix.length).trim();
|
|
2460
|
+
if (!json || json === "[DONE]") continue;
|
|
2461
|
+
try {
|
|
2462
|
+
processEvent(JSON.parse(json));
|
|
2463
|
+
} catch {
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
boundary = buffer.indexOf("\n\n");
|
|
2467
|
+
}
|
|
2395
2468
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2469
|
+
return "stream_ended";
|
|
2470
|
+
} finally {
|
|
2471
|
+
if (heartbeatTimer) clearTimeout(heartbeatTimer);
|
|
2472
|
+
streamReader?.releaseLock();
|
|
2399
2473
|
}
|
|
2474
|
+
}
|
|
2475
|
+
void (async () => {
|
|
2476
|
+
let attempt = 0;
|
|
2477
|
+
while (!lifecycleAc.signal.aborted) {
|
|
2478
|
+
const iterAc = new AbortController();
|
|
2479
|
+
followAbortSignal(lifecycleAc.signal, iterAc);
|
|
2480
|
+
try {
|
|
2481
|
+
const result = await connectOnce(iterAc.signal);
|
|
2482
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2483
|
+
if (result === "stream_ended") {
|
|
2484
|
+
attempt++;
|
|
2485
|
+
}
|
|
2486
|
+
} catch (e) {
|
|
2487
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2488
|
+
if (e.name === "AbortError") break;
|
|
2489
|
+
const status = e.status;
|
|
2490
|
+
if (status === 400 || status === 401 || status === 403 || status === 404) {
|
|
2491
|
+
params.onError?.(e.message);
|
|
2492
|
+
break;
|
|
2493
|
+
}
|
|
2494
|
+
attempt++;
|
|
2495
|
+
}
|
|
2496
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2497
|
+
if (attempt > maxAttempts) {
|
|
2498
|
+
params.onError?.(`Realtime: gave up after ${maxAttempts} reconnect attempts`);
|
|
2499
|
+
break;
|
|
2500
|
+
}
|
|
2501
|
+
const delay = backoffDelay(attempt, baseDelay, maxDelay);
|
|
2502
|
+
setStatus("reconnecting");
|
|
2503
|
+
params.onDisconnect?.({ attempt, retryInMs: delay });
|
|
2504
|
+
await new Promise((r) => {
|
|
2505
|
+
const timer = setTimeout(r, delay);
|
|
2506
|
+
const onAbort = () => {
|
|
2507
|
+
clearTimeout(timer);
|
|
2508
|
+
r();
|
|
2509
|
+
};
|
|
2510
|
+
lifecycleAc.signal.addEventListener("abort", onAbort, { once: true });
|
|
2511
|
+
});
|
|
2512
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2513
|
+
setStatus("connecting");
|
|
2514
|
+
params.onReconnect?.({ attempt });
|
|
2515
|
+
}
|
|
2516
|
+
setStatus("disconnected");
|
|
2400
2517
|
})();
|
|
2401
|
-
return
|
|
2402
|
-
unsubscribe: () => ac.abort()
|
|
2403
|
-
};
|
|
2518
|
+
return subscription;
|
|
2404
2519
|
}
|
|
2405
2520
|
var RagableBrowserAgentsClient = class {
|
|
2406
2521
|
constructor(options) {
|