@ragable/sdk 0.6.14 → 0.6.16
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 +112 -5
- package/dist/index.d.ts +112 -5
- package/dist/index.js +249 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +249 -38
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2196,6 +2196,51 @@ var RagableBrowserAuthClient = class {
|
|
|
2196
2196
|
return this.auth.getSession();
|
|
2197
2197
|
}
|
|
2198
2198
|
};
|
|
2199
|
+
var BrowserCollectionApi = class {
|
|
2200
|
+
constructor(database, name, databaseInstanceId) {
|
|
2201
|
+
this.database = database;
|
|
2202
|
+
this.name = name;
|
|
2203
|
+
this.databaseInstanceId = databaseInstanceId;
|
|
2204
|
+
__publicField(this, "find", (whereOrParams = {}) => {
|
|
2205
|
+
const hasQueryKeys = ["where", "filters", "limit", "offset", "orderBy", "orderDirection"].some(
|
|
2206
|
+
(key) => Object.prototype.hasOwnProperty.call(whereOrParams, key)
|
|
2207
|
+
);
|
|
2208
|
+
const body = hasQueryKeys ? whereOrParams : { where: whereOrParams };
|
|
2209
|
+
return asPostgrestResponse(
|
|
2210
|
+
() => this.database._requestCollection(
|
|
2211
|
+
"POST",
|
|
2212
|
+
`/${encodeURIComponent(this.name)}/find`,
|
|
2213
|
+
body,
|
|
2214
|
+
this.databaseInstanceId
|
|
2215
|
+
)
|
|
2216
|
+
);
|
|
2217
|
+
});
|
|
2218
|
+
__publicField(this, "insert", (data) => asPostgrestResponse(
|
|
2219
|
+
() => this.database._requestCollection(
|
|
2220
|
+
"POST",
|
|
2221
|
+
`/${encodeURIComponent(this.name)}/records`,
|
|
2222
|
+
{ data },
|
|
2223
|
+
this.databaseInstanceId
|
|
2224
|
+
)
|
|
2225
|
+
));
|
|
2226
|
+
__publicField(this, "update", (where, patch, options) => asPostgrestResponse(
|
|
2227
|
+
() => this.database._requestCollection(
|
|
2228
|
+
"PATCH",
|
|
2229
|
+
`/${encodeURIComponent(this.name)}/records`,
|
|
2230
|
+
{ where, patch, ...options?.limit ? { limit: options.limit } : {} },
|
|
2231
|
+
this.databaseInstanceId
|
|
2232
|
+
)
|
|
2233
|
+
));
|
|
2234
|
+
__publicField(this, "delete", (where, options) => asPostgrestResponse(
|
|
2235
|
+
() => this.database._requestCollection(
|
|
2236
|
+
"DELETE",
|
|
2237
|
+
`/${encodeURIComponent(this.name)}/records`,
|
|
2238
|
+
{ where, ...options?.limit ? { limit: options.limit } : {} },
|
|
2239
|
+
this.databaseInstanceId
|
|
2240
|
+
)
|
|
2241
|
+
));
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
2199
2244
|
var RagableBrowserDatabaseClient = class {
|
|
2200
2245
|
constructor(options, ragableAuth = null) {
|
|
2201
2246
|
this.options = options;
|
|
@@ -2255,6 +2300,25 @@ var RagableBrowserDatabaseClient = class {
|
|
|
2255
2300
|
};
|
|
2256
2301
|
return new PostgrestTableApi(pgFetch, id, table);
|
|
2257
2302
|
});
|
|
2303
|
+
__publicField(this, "collection", (name, databaseInstanceId) => {
|
|
2304
|
+
return new BrowserCollectionApi(this, name, databaseInstanceId);
|
|
2305
|
+
});
|
|
2306
|
+
__publicField(this, "defineCollection", (name, schema, databaseInstanceId) => asPostgrestResponse(
|
|
2307
|
+
() => this._requestCollection(
|
|
2308
|
+
"POST",
|
|
2309
|
+
"/",
|
|
2310
|
+
{ name, ...schema ? { schema } : {} },
|
|
2311
|
+
databaseInstanceId
|
|
2312
|
+
)
|
|
2313
|
+
));
|
|
2314
|
+
__publicField(this, "listCollections", (databaseInstanceId) => asPostgrestResponse(
|
|
2315
|
+
() => this._requestCollection(
|
|
2316
|
+
"GET",
|
|
2317
|
+
"/",
|
|
2318
|
+
void 0,
|
|
2319
|
+
databaseInstanceId
|
|
2320
|
+
)
|
|
2321
|
+
));
|
|
2258
2322
|
__publicField(this, "query", async (params) => {
|
|
2259
2323
|
return asPostgrestResponse(async () => {
|
|
2260
2324
|
const gid = requireAuthGroupId(this.options);
|
|
@@ -2298,6 +2362,20 @@ var RagableBrowserDatabaseClient = class {
|
|
|
2298
2362
|
* Postgres `LISTEN` / `NOTIFY` realtime via server-proxied SSE.
|
|
2299
2363
|
* Channels must be lowercase identifiers: `[a-z_][a-z0-9_]*` (max 63 chars).
|
|
2300
2364
|
*/
|
|
2365
|
+
/**
|
|
2366
|
+
* Postgres `LISTEN` / `NOTIFY` realtime via server-proxied SSE.
|
|
2367
|
+
*
|
|
2368
|
+
* Returns a `BrowserRealtimeSubscription` with:
|
|
2369
|
+
* - `unsubscribe()` — permanently close the subscription (stops reconnects).
|
|
2370
|
+
* - `status` — current connection state: `"connecting"` | `"connected"` | `"reconnecting"` | `"disconnected"`.
|
|
2371
|
+
*
|
|
2372
|
+
* The subscription automatically reconnects with exponential backoff when the
|
|
2373
|
+
* stream drops. Server heartbeats (every 15 s) are monitored — if none arrive
|
|
2374
|
+
* within `heartbeatTimeoutMs` (default 45 s), the connection is treated as dead
|
|
2375
|
+
* and a reconnect is triggered. Auth errors (401/403/404) are non-retryable.
|
|
2376
|
+
*
|
|
2377
|
+
* Channel names must be lowercase identifiers: `[a-z_][a-z0-9_]*` (max 63 chars).
|
|
2378
|
+
*/
|
|
2301
2379
|
__publicField(this, "realtime", {
|
|
2302
2380
|
subscribe: (params) => subscribeBrowserRealtime(
|
|
2303
2381
|
this.options,
|
|
@@ -2315,6 +2393,36 @@ var RagableBrowserDatabaseClient = class {
|
|
|
2315
2393
|
toUrl(path) {
|
|
2316
2394
|
return `${normalizeBrowserApiBase()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
2317
2395
|
}
|
|
2396
|
+
async _requestCollection(method, path, body, databaseInstanceId) {
|
|
2397
|
+
const gid = requireAuthGroupId(this.options);
|
|
2398
|
+
const token = await resolveDatabaseAuthBearer(this.options, this.ragableAuth);
|
|
2399
|
+
const id = databaseInstanceId?.trim() || this.options.databaseInstanceId?.trim();
|
|
2400
|
+
if (!id) {
|
|
2401
|
+
throw new RagableError(
|
|
2402
|
+
"db.collection() requires databaseInstanceId in client options or as an argument",
|
|
2403
|
+
400,
|
|
2404
|
+
{ code: "SDK_MISSING_DATABASE_INSTANCE_ID" }
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
2407
|
+
const headers = this.baseHeaders();
|
|
2408
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
2409
|
+
headers.set("X-Database-Instance-Id", id);
|
|
2410
|
+
if (body !== void 0) headers.set("Content-Type", "application/json");
|
|
2411
|
+
const response = await this.fetchImpl(
|
|
2412
|
+
this.toUrl(`/auth-groups/${gid}/data/collections${path}`),
|
|
2413
|
+
{
|
|
2414
|
+
method,
|
|
2415
|
+
headers,
|
|
2416
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
2417
|
+
}
|
|
2418
|
+
);
|
|
2419
|
+
const payload = await parseMaybeJsonBody(response);
|
|
2420
|
+
if (!response.ok) {
|
|
2421
|
+
const message = extractErrorMessage(payload, response.statusText);
|
|
2422
|
+
throw new RagableError(message, response.status, payload);
|
|
2423
|
+
}
|
|
2424
|
+
return payload;
|
|
2425
|
+
}
|
|
2318
2426
|
baseHeaders() {
|
|
2319
2427
|
return new Headers(this.options.headers);
|
|
2320
2428
|
}
|
|
@@ -2327,9 +2435,13 @@ function followAbortSignal(parent, child) {
|
|
|
2327
2435
|
}
|
|
2328
2436
|
parent.addEventListener("abort", () => child.abort(), { once: true });
|
|
2329
2437
|
}
|
|
2438
|
+
function backoffDelay(attempt, baseMs, maxMs) {
|
|
2439
|
+
const exp = Math.min(baseMs * 2 ** (attempt - 1), maxMs);
|
|
2440
|
+
const jitter = exp * (0.5 + Math.random() * 0.5);
|
|
2441
|
+
return Math.round(jitter);
|
|
2442
|
+
}
|
|
2330
2443
|
async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params) {
|
|
2331
2444
|
const gid = requireAuthGroupId(options);
|
|
2332
|
-
const token = await resolveDatabaseAuthBearer(options, ragableAuth);
|
|
2333
2445
|
const databaseInstanceId = params.databaseInstanceId?.trim() || options.databaseInstanceId?.trim();
|
|
2334
2446
|
if (!databaseInstanceId) {
|
|
2335
2447
|
throw new RagableError(
|
|
@@ -2345,41 +2457,75 @@ async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params)
|
|
|
2345
2457
|
{ code: "SDK_REALTIME_CHANNELS_REQUIRED" }
|
|
2346
2458
|
);
|
|
2347
2459
|
}
|
|
2348
|
-
const
|
|
2349
|
-
|
|
2350
|
-
const
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2460
|
+
const maxAttempts = params.maxReconnectAttempts ?? Infinity;
|
|
2461
|
+
const baseDelay = params.reconnectBaseDelayMs ?? 1e3;
|
|
2462
|
+
const maxDelay = params.reconnectMaxDelayMs ?? 3e4;
|
|
2463
|
+
const heartbeatTimeout = params.heartbeatTimeoutMs ?? 45e3;
|
|
2464
|
+
const lifecycleAc = new AbortController();
|
|
2465
|
+
followAbortSignal(params.signal, lifecycleAc);
|
|
2466
|
+
let currentStatus = "connecting";
|
|
2467
|
+
const setStatus = (s) => {
|
|
2468
|
+
if (s === currentStatus) return;
|
|
2469
|
+
currentStatus = s;
|
|
2470
|
+
params.onStatusChange?.(s);
|
|
2471
|
+
};
|
|
2472
|
+
const subscription = {
|
|
2473
|
+
unsubscribe: () => lifecycleAc.abort(),
|
|
2474
|
+
get status() {
|
|
2475
|
+
return currentStatus;
|
|
2363
2476
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
const
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2477
|
+
};
|
|
2478
|
+
setStatus("connecting");
|
|
2479
|
+
async function connectOnce(signal) {
|
|
2480
|
+
const token = await resolveDatabaseAuthBearer(options, ragableAuth);
|
|
2481
|
+
const headers = new Headers(options.headers);
|
|
2482
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
2483
|
+
headers.set("Content-Type", "application/json");
|
|
2484
|
+
const response = await fetchImpl(
|
|
2485
|
+
`${normalizeBrowserApiBase()}/auth-groups/${gid}/data/realtime/stream`,
|
|
2486
|
+
{
|
|
2487
|
+
method: "POST",
|
|
2488
|
+
headers,
|
|
2489
|
+
body: JSON.stringify({
|
|
2490
|
+
databaseInstanceId,
|
|
2491
|
+
channels: params.channels
|
|
2492
|
+
}),
|
|
2493
|
+
signal
|
|
2494
|
+
}
|
|
2376
2495
|
);
|
|
2377
|
-
|
|
2378
|
-
|
|
2496
|
+
if (!response.ok) {
|
|
2497
|
+
const payload = await parseMaybeJsonBody(response);
|
|
2498
|
+
const message = extractErrorMessage(payload, response.statusText);
|
|
2499
|
+
throw new RagableError(message, response.status, payload);
|
|
2500
|
+
}
|
|
2501
|
+
const streamBody = response.body;
|
|
2502
|
+
if (!streamBody) {
|
|
2503
|
+
throw new RagableError("Realtime stream has no body", 502, {
|
|
2504
|
+
code: "SDK_REALTIME_NO_BODY"
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
let heartbeatTimer = null;
|
|
2508
|
+
const resetHeartbeatTimer = () => {
|
|
2509
|
+
if (heartbeatTimer) clearTimeout(heartbeatTimer);
|
|
2510
|
+
heartbeatTimer = setTimeout(() => {
|
|
2511
|
+
streamReader?.cancel().catch(() => void 0);
|
|
2512
|
+
}, heartbeatTimeout);
|
|
2513
|
+
};
|
|
2514
|
+
let streamReader = null;
|
|
2379
2515
|
try {
|
|
2380
|
-
|
|
2516
|
+
streamReader = streamBody.getReader();
|
|
2517
|
+
const decoder = new TextDecoder();
|
|
2518
|
+
let buffer = "";
|
|
2519
|
+
resetHeartbeatTimer();
|
|
2520
|
+
const processEvent = (evt) => {
|
|
2521
|
+
if (evt.type === "realtime:heartbeat") {
|
|
2522
|
+
resetHeartbeatTimer();
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
resetHeartbeatTimer();
|
|
2381
2526
|
if (evt.type === "realtime:ready") {
|
|
2382
2527
|
const ch = evt.channels;
|
|
2528
|
+
setStatus("connected");
|
|
2383
2529
|
params.onReady?.(
|
|
2384
2530
|
Array.isArray(ch) ? ch.map((c) => String(c)) : []
|
|
2385
2531
|
);
|
|
@@ -2392,15 +2538,78 @@ async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params)
|
|
|
2392
2538
|
} else if (evt.type === "realtime:error") {
|
|
2393
2539
|
params.onError?.(String(evt.message ?? "Realtime error"));
|
|
2394
2540
|
}
|
|
2541
|
+
};
|
|
2542
|
+
while (true) {
|
|
2543
|
+
const { done, value } = await streamReader.read();
|
|
2544
|
+
if (done) break;
|
|
2545
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2546
|
+
let boundary = buffer.indexOf("\n\n");
|
|
2547
|
+
while (boundary !== -1) {
|
|
2548
|
+
const block = buffer.slice(0, boundary);
|
|
2549
|
+
buffer = buffer.slice(boundary + 2);
|
|
2550
|
+
for (const line of block.split("\n")) {
|
|
2551
|
+
const dataPrefix = "data: ";
|
|
2552
|
+
if (!line.startsWith(dataPrefix)) continue;
|
|
2553
|
+
const json = line.slice(dataPrefix.length).trim();
|
|
2554
|
+
if (!json || json === "[DONE]") continue;
|
|
2555
|
+
try {
|
|
2556
|
+
processEvent(JSON.parse(json));
|
|
2557
|
+
} catch {
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
boundary = buffer.indexOf("\n\n");
|
|
2561
|
+
}
|
|
2395
2562
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2563
|
+
return "stream_ended";
|
|
2564
|
+
} finally {
|
|
2565
|
+
if (heartbeatTimer) clearTimeout(heartbeatTimer);
|
|
2566
|
+
streamReader?.releaseLock();
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
void (async () => {
|
|
2570
|
+
let attempt = 0;
|
|
2571
|
+
while (!lifecycleAc.signal.aborted) {
|
|
2572
|
+
const iterAc = new AbortController();
|
|
2573
|
+
followAbortSignal(lifecycleAc.signal, iterAc);
|
|
2574
|
+
try {
|
|
2575
|
+
const result = await connectOnce(iterAc.signal);
|
|
2576
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2577
|
+
if (result === "stream_ended") {
|
|
2578
|
+
attempt++;
|
|
2579
|
+
}
|
|
2580
|
+
} catch (e) {
|
|
2581
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2582
|
+
if (e.name === "AbortError") break;
|
|
2583
|
+
const status = e.status;
|
|
2584
|
+
if (status === 400 || status === 401 || status === 403 || status === 404) {
|
|
2585
|
+
params.onError?.(e.message);
|
|
2586
|
+
break;
|
|
2587
|
+
}
|
|
2588
|
+
attempt++;
|
|
2589
|
+
}
|
|
2590
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2591
|
+
if (attempt > maxAttempts) {
|
|
2592
|
+
params.onError?.(`Realtime: gave up after ${maxAttempts} reconnect attempts`);
|
|
2593
|
+
break;
|
|
2594
|
+
}
|
|
2595
|
+
const delay = backoffDelay(attempt, baseDelay, maxDelay);
|
|
2596
|
+
setStatus("reconnecting");
|
|
2597
|
+
params.onDisconnect?.({ attempt, retryInMs: delay });
|
|
2598
|
+
await new Promise((r) => {
|
|
2599
|
+
const timer = setTimeout(r, delay);
|
|
2600
|
+
const onAbort = () => {
|
|
2601
|
+
clearTimeout(timer);
|
|
2602
|
+
r();
|
|
2603
|
+
};
|
|
2604
|
+
lifecycleAc.signal.addEventListener("abort", onAbort, { once: true });
|
|
2605
|
+
});
|
|
2606
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2607
|
+
setStatus("connecting");
|
|
2608
|
+
params.onReconnect?.({ attempt });
|
|
2399
2609
|
}
|
|
2610
|
+
setStatus("disconnected");
|
|
2400
2611
|
})();
|
|
2401
|
-
return
|
|
2402
|
-
unsubscribe: () => ac.abort()
|
|
2403
|
-
};
|
|
2612
|
+
return subscription;
|
|
2404
2613
|
}
|
|
2405
2614
|
var RagableBrowserAgentsClient = class {
|
|
2406
2615
|
constructor(options) {
|
|
@@ -2446,6 +2655,7 @@ var RagableBrowser = class {
|
|
|
2446
2655
|
__publicField(this, "agents");
|
|
2447
2656
|
__publicField(this, "auth");
|
|
2448
2657
|
__publicField(this, "database");
|
|
2658
|
+
__publicField(this, "db");
|
|
2449
2659
|
__publicField(this, "transport");
|
|
2450
2660
|
__publicField(this, "_ragableAuth");
|
|
2451
2661
|
/** Delegates to `database.from()`. Kept for back-compat — prefer `database.from()`. */
|
|
@@ -2485,6 +2695,7 @@ var RagableBrowser = class {
|
|
|
2485
2695
|
this._ragableAuth
|
|
2486
2696
|
);
|
|
2487
2697
|
this.database._setTransport(this.transport);
|
|
2698
|
+
this.db = this.database;
|
|
2488
2699
|
}
|
|
2489
2700
|
destroy() {
|
|
2490
2701
|
this._ragableAuth?.destroy();
|
|
@@ -2558,7 +2769,7 @@ function createClient(options) {
|
|
|
2558
2769
|
if (isServerClientOptions(options)) {
|
|
2559
2770
|
if (typeof options === "object" && options !== null && "organizationId" in options && typeof options.organizationId === "string") {
|
|
2560
2771
|
console.warn(
|
|
2561
|
-
"[@ragable/sdk] createClient: `apiKey` is set, so the server client is returned. It has no `database` or `auth` \u2014 only `agents` and `shift`. For `database.from()` / `auth.*`, use the browser client without `apiKey` (e.g. createClient({ organizationId, authGroupId, databaseInstanceId, ... }))."
|
|
2772
|
+
"[@ragable/sdk] createClient: `apiKey` is set, so the server client is returned. It has no `database` or `auth` \u2014 only `agents` and `shift`. For `db.collection()` / `database.from()` / `auth.*`, use the browser client without `apiKey` (e.g. createClient({ organizationId, authGroupId, databaseInstanceId, ... }))."
|
|
2562
2773
|
);
|
|
2563
2774
|
}
|
|
2564
2775
|
return new Ragable(options);
|