@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.js
CHANGED
|
@@ -2270,6 +2270,51 @@ var RagableBrowserAuthClient = class {
|
|
|
2270
2270
|
return this.auth.getSession();
|
|
2271
2271
|
}
|
|
2272
2272
|
};
|
|
2273
|
+
var BrowserCollectionApi = class {
|
|
2274
|
+
constructor(database, name, databaseInstanceId) {
|
|
2275
|
+
this.database = database;
|
|
2276
|
+
this.name = name;
|
|
2277
|
+
this.databaseInstanceId = databaseInstanceId;
|
|
2278
|
+
__publicField(this, "find", (whereOrParams = {}) => {
|
|
2279
|
+
const hasQueryKeys = ["where", "filters", "limit", "offset", "orderBy", "orderDirection"].some(
|
|
2280
|
+
(key) => Object.prototype.hasOwnProperty.call(whereOrParams, key)
|
|
2281
|
+
);
|
|
2282
|
+
const body = hasQueryKeys ? whereOrParams : { where: whereOrParams };
|
|
2283
|
+
return asPostgrestResponse(
|
|
2284
|
+
() => this.database._requestCollection(
|
|
2285
|
+
"POST",
|
|
2286
|
+
`/${encodeURIComponent(this.name)}/find`,
|
|
2287
|
+
body,
|
|
2288
|
+
this.databaseInstanceId
|
|
2289
|
+
)
|
|
2290
|
+
);
|
|
2291
|
+
});
|
|
2292
|
+
__publicField(this, "insert", (data) => asPostgrestResponse(
|
|
2293
|
+
() => this.database._requestCollection(
|
|
2294
|
+
"POST",
|
|
2295
|
+
`/${encodeURIComponent(this.name)}/records`,
|
|
2296
|
+
{ data },
|
|
2297
|
+
this.databaseInstanceId
|
|
2298
|
+
)
|
|
2299
|
+
));
|
|
2300
|
+
__publicField(this, "update", (where, patch, options) => asPostgrestResponse(
|
|
2301
|
+
() => this.database._requestCollection(
|
|
2302
|
+
"PATCH",
|
|
2303
|
+
`/${encodeURIComponent(this.name)}/records`,
|
|
2304
|
+
{ where, patch, ...options?.limit ? { limit: options.limit } : {} },
|
|
2305
|
+
this.databaseInstanceId
|
|
2306
|
+
)
|
|
2307
|
+
));
|
|
2308
|
+
__publicField(this, "delete", (where, options) => asPostgrestResponse(
|
|
2309
|
+
() => this.database._requestCollection(
|
|
2310
|
+
"DELETE",
|
|
2311
|
+
`/${encodeURIComponent(this.name)}/records`,
|
|
2312
|
+
{ where, ...options?.limit ? { limit: options.limit } : {} },
|
|
2313
|
+
this.databaseInstanceId
|
|
2314
|
+
)
|
|
2315
|
+
));
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2273
2318
|
var RagableBrowserDatabaseClient = class {
|
|
2274
2319
|
constructor(options, ragableAuth = null) {
|
|
2275
2320
|
this.options = options;
|
|
@@ -2329,6 +2374,25 @@ var RagableBrowserDatabaseClient = class {
|
|
|
2329
2374
|
};
|
|
2330
2375
|
return new PostgrestTableApi(pgFetch, id, table);
|
|
2331
2376
|
});
|
|
2377
|
+
__publicField(this, "collection", (name, databaseInstanceId) => {
|
|
2378
|
+
return new BrowserCollectionApi(this, name, databaseInstanceId);
|
|
2379
|
+
});
|
|
2380
|
+
__publicField(this, "defineCollection", (name, schema, databaseInstanceId) => asPostgrestResponse(
|
|
2381
|
+
() => this._requestCollection(
|
|
2382
|
+
"POST",
|
|
2383
|
+
"/",
|
|
2384
|
+
{ name, ...schema ? { schema } : {} },
|
|
2385
|
+
databaseInstanceId
|
|
2386
|
+
)
|
|
2387
|
+
));
|
|
2388
|
+
__publicField(this, "listCollections", (databaseInstanceId) => asPostgrestResponse(
|
|
2389
|
+
() => this._requestCollection(
|
|
2390
|
+
"GET",
|
|
2391
|
+
"/",
|
|
2392
|
+
void 0,
|
|
2393
|
+
databaseInstanceId
|
|
2394
|
+
)
|
|
2395
|
+
));
|
|
2332
2396
|
__publicField(this, "query", async (params) => {
|
|
2333
2397
|
return asPostgrestResponse(async () => {
|
|
2334
2398
|
const gid = requireAuthGroupId(this.options);
|
|
@@ -2372,6 +2436,20 @@ var RagableBrowserDatabaseClient = class {
|
|
|
2372
2436
|
* Postgres `LISTEN` / `NOTIFY` realtime via server-proxied SSE.
|
|
2373
2437
|
* Channels must be lowercase identifiers: `[a-z_][a-z0-9_]*` (max 63 chars).
|
|
2374
2438
|
*/
|
|
2439
|
+
/**
|
|
2440
|
+
* Postgres `LISTEN` / `NOTIFY` realtime via server-proxied SSE.
|
|
2441
|
+
*
|
|
2442
|
+
* Returns a `BrowserRealtimeSubscription` with:
|
|
2443
|
+
* - `unsubscribe()` — permanently close the subscription (stops reconnects).
|
|
2444
|
+
* - `status` — current connection state: `"connecting"` | `"connected"` | `"reconnecting"` | `"disconnected"`.
|
|
2445
|
+
*
|
|
2446
|
+
* The subscription automatically reconnects with exponential backoff when the
|
|
2447
|
+
* stream drops. Server heartbeats (every 15 s) are monitored — if none arrive
|
|
2448
|
+
* within `heartbeatTimeoutMs` (default 45 s), the connection is treated as dead
|
|
2449
|
+
* and a reconnect is triggered. Auth errors (401/403/404) are non-retryable.
|
|
2450
|
+
*
|
|
2451
|
+
* Channel names must be lowercase identifiers: `[a-z_][a-z0-9_]*` (max 63 chars).
|
|
2452
|
+
*/
|
|
2375
2453
|
__publicField(this, "realtime", {
|
|
2376
2454
|
subscribe: (params) => subscribeBrowserRealtime(
|
|
2377
2455
|
this.options,
|
|
@@ -2389,6 +2467,36 @@ var RagableBrowserDatabaseClient = class {
|
|
|
2389
2467
|
toUrl(path) {
|
|
2390
2468
|
return `${normalizeBrowserApiBase()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
2391
2469
|
}
|
|
2470
|
+
async _requestCollection(method, path, body, databaseInstanceId) {
|
|
2471
|
+
const gid = requireAuthGroupId(this.options);
|
|
2472
|
+
const token = await resolveDatabaseAuthBearer(this.options, this.ragableAuth);
|
|
2473
|
+
const id = databaseInstanceId?.trim() || this.options.databaseInstanceId?.trim();
|
|
2474
|
+
if (!id) {
|
|
2475
|
+
throw new RagableError(
|
|
2476
|
+
"db.collection() requires databaseInstanceId in client options or as an argument",
|
|
2477
|
+
400,
|
|
2478
|
+
{ code: "SDK_MISSING_DATABASE_INSTANCE_ID" }
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
const headers = this.baseHeaders();
|
|
2482
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
2483
|
+
headers.set("X-Database-Instance-Id", id);
|
|
2484
|
+
if (body !== void 0) headers.set("Content-Type", "application/json");
|
|
2485
|
+
const response = await this.fetchImpl(
|
|
2486
|
+
this.toUrl(`/auth-groups/${gid}/data/collections${path}`),
|
|
2487
|
+
{
|
|
2488
|
+
method,
|
|
2489
|
+
headers,
|
|
2490
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
2491
|
+
}
|
|
2492
|
+
);
|
|
2493
|
+
const payload = await parseMaybeJsonBody(response);
|
|
2494
|
+
if (!response.ok) {
|
|
2495
|
+
const message = extractErrorMessage(payload, response.statusText);
|
|
2496
|
+
throw new RagableError(message, response.status, payload);
|
|
2497
|
+
}
|
|
2498
|
+
return payload;
|
|
2499
|
+
}
|
|
2392
2500
|
baseHeaders() {
|
|
2393
2501
|
return new Headers(this.options.headers);
|
|
2394
2502
|
}
|
|
@@ -2401,9 +2509,13 @@ function followAbortSignal(parent, child) {
|
|
|
2401
2509
|
}
|
|
2402
2510
|
parent.addEventListener("abort", () => child.abort(), { once: true });
|
|
2403
2511
|
}
|
|
2512
|
+
function backoffDelay(attempt, baseMs, maxMs) {
|
|
2513
|
+
const exp = Math.min(baseMs * 2 ** (attempt - 1), maxMs);
|
|
2514
|
+
const jitter = exp * (0.5 + Math.random() * 0.5);
|
|
2515
|
+
return Math.round(jitter);
|
|
2516
|
+
}
|
|
2404
2517
|
async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params) {
|
|
2405
2518
|
const gid = requireAuthGroupId(options);
|
|
2406
|
-
const token = await resolveDatabaseAuthBearer(options, ragableAuth);
|
|
2407
2519
|
const databaseInstanceId = params.databaseInstanceId?.trim() || options.databaseInstanceId?.trim();
|
|
2408
2520
|
if (!databaseInstanceId) {
|
|
2409
2521
|
throw new RagableError(
|
|
@@ -2419,41 +2531,75 @@ async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params)
|
|
|
2419
2531
|
{ code: "SDK_REALTIME_CHANNELS_REQUIRED" }
|
|
2420
2532
|
);
|
|
2421
2533
|
}
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2424
|
-
const
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2534
|
+
const maxAttempts = params.maxReconnectAttempts ?? Infinity;
|
|
2535
|
+
const baseDelay = params.reconnectBaseDelayMs ?? 1e3;
|
|
2536
|
+
const maxDelay = params.reconnectMaxDelayMs ?? 3e4;
|
|
2537
|
+
const heartbeatTimeout = params.heartbeatTimeoutMs ?? 45e3;
|
|
2538
|
+
const lifecycleAc = new AbortController();
|
|
2539
|
+
followAbortSignal(params.signal, lifecycleAc);
|
|
2540
|
+
let currentStatus = "connecting";
|
|
2541
|
+
const setStatus = (s) => {
|
|
2542
|
+
if (s === currentStatus) return;
|
|
2543
|
+
currentStatus = s;
|
|
2544
|
+
params.onStatusChange?.(s);
|
|
2545
|
+
};
|
|
2546
|
+
const subscription = {
|
|
2547
|
+
unsubscribe: () => lifecycleAc.abort(),
|
|
2548
|
+
get status() {
|
|
2549
|
+
return currentStatus;
|
|
2437
2550
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
const
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2551
|
+
};
|
|
2552
|
+
setStatus("connecting");
|
|
2553
|
+
async function connectOnce(signal) {
|
|
2554
|
+
const token = await resolveDatabaseAuthBearer(options, ragableAuth);
|
|
2555
|
+
const headers = new Headers(options.headers);
|
|
2556
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
2557
|
+
headers.set("Content-Type", "application/json");
|
|
2558
|
+
const response = await fetchImpl(
|
|
2559
|
+
`${normalizeBrowserApiBase()}/auth-groups/${gid}/data/realtime/stream`,
|
|
2560
|
+
{
|
|
2561
|
+
method: "POST",
|
|
2562
|
+
headers,
|
|
2563
|
+
body: JSON.stringify({
|
|
2564
|
+
databaseInstanceId,
|
|
2565
|
+
channels: params.channels
|
|
2566
|
+
}),
|
|
2567
|
+
signal
|
|
2568
|
+
}
|
|
2450
2569
|
);
|
|
2451
|
-
|
|
2452
|
-
|
|
2570
|
+
if (!response.ok) {
|
|
2571
|
+
const payload = await parseMaybeJsonBody(response);
|
|
2572
|
+
const message = extractErrorMessage(payload, response.statusText);
|
|
2573
|
+
throw new RagableError(message, response.status, payload);
|
|
2574
|
+
}
|
|
2575
|
+
const streamBody = response.body;
|
|
2576
|
+
if (!streamBody) {
|
|
2577
|
+
throw new RagableError("Realtime stream has no body", 502, {
|
|
2578
|
+
code: "SDK_REALTIME_NO_BODY"
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
let heartbeatTimer = null;
|
|
2582
|
+
const resetHeartbeatTimer = () => {
|
|
2583
|
+
if (heartbeatTimer) clearTimeout(heartbeatTimer);
|
|
2584
|
+
heartbeatTimer = setTimeout(() => {
|
|
2585
|
+
streamReader?.cancel().catch(() => void 0);
|
|
2586
|
+
}, heartbeatTimeout);
|
|
2587
|
+
};
|
|
2588
|
+
let streamReader = null;
|
|
2453
2589
|
try {
|
|
2454
|
-
|
|
2590
|
+
streamReader = streamBody.getReader();
|
|
2591
|
+
const decoder = new TextDecoder();
|
|
2592
|
+
let buffer = "";
|
|
2593
|
+
resetHeartbeatTimer();
|
|
2594
|
+
const processEvent = (evt) => {
|
|
2595
|
+
if (evt.type === "realtime:heartbeat") {
|
|
2596
|
+
resetHeartbeatTimer();
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
resetHeartbeatTimer();
|
|
2455
2600
|
if (evt.type === "realtime:ready") {
|
|
2456
2601
|
const ch = evt.channels;
|
|
2602
|
+
setStatus("connected");
|
|
2457
2603
|
params.onReady?.(
|
|
2458
2604
|
Array.isArray(ch) ? ch.map((c) => String(c)) : []
|
|
2459
2605
|
);
|
|
@@ -2466,15 +2612,78 @@ async function subscribeBrowserRealtime(options, ragableAuth, fetchImpl, params)
|
|
|
2466
2612
|
} else if (evt.type === "realtime:error") {
|
|
2467
2613
|
params.onError?.(String(evt.message ?? "Realtime error"));
|
|
2468
2614
|
}
|
|
2615
|
+
};
|
|
2616
|
+
while (true) {
|
|
2617
|
+
const { done, value } = await streamReader.read();
|
|
2618
|
+
if (done) break;
|
|
2619
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2620
|
+
let boundary = buffer.indexOf("\n\n");
|
|
2621
|
+
while (boundary !== -1) {
|
|
2622
|
+
const block = buffer.slice(0, boundary);
|
|
2623
|
+
buffer = buffer.slice(boundary + 2);
|
|
2624
|
+
for (const line of block.split("\n")) {
|
|
2625
|
+
const dataPrefix = "data: ";
|
|
2626
|
+
if (!line.startsWith(dataPrefix)) continue;
|
|
2627
|
+
const json = line.slice(dataPrefix.length).trim();
|
|
2628
|
+
if (!json || json === "[DONE]") continue;
|
|
2629
|
+
try {
|
|
2630
|
+
processEvent(JSON.parse(json));
|
|
2631
|
+
} catch {
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
boundary = buffer.indexOf("\n\n");
|
|
2635
|
+
}
|
|
2469
2636
|
}
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2637
|
+
return "stream_ended";
|
|
2638
|
+
} finally {
|
|
2639
|
+
if (heartbeatTimer) clearTimeout(heartbeatTimer);
|
|
2640
|
+
streamReader?.releaseLock();
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
void (async () => {
|
|
2644
|
+
let attempt = 0;
|
|
2645
|
+
while (!lifecycleAc.signal.aborted) {
|
|
2646
|
+
const iterAc = new AbortController();
|
|
2647
|
+
followAbortSignal(lifecycleAc.signal, iterAc);
|
|
2648
|
+
try {
|
|
2649
|
+
const result = await connectOnce(iterAc.signal);
|
|
2650
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2651
|
+
if (result === "stream_ended") {
|
|
2652
|
+
attempt++;
|
|
2653
|
+
}
|
|
2654
|
+
} catch (e) {
|
|
2655
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2656
|
+
if (e.name === "AbortError") break;
|
|
2657
|
+
const status = e.status;
|
|
2658
|
+
if (status === 400 || status === 401 || status === 403 || status === 404) {
|
|
2659
|
+
params.onError?.(e.message);
|
|
2660
|
+
break;
|
|
2661
|
+
}
|
|
2662
|
+
attempt++;
|
|
2663
|
+
}
|
|
2664
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2665
|
+
if (attempt > maxAttempts) {
|
|
2666
|
+
params.onError?.(`Realtime: gave up after ${maxAttempts} reconnect attempts`);
|
|
2667
|
+
break;
|
|
2668
|
+
}
|
|
2669
|
+
const delay = backoffDelay(attempt, baseDelay, maxDelay);
|
|
2670
|
+
setStatus("reconnecting");
|
|
2671
|
+
params.onDisconnect?.({ attempt, retryInMs: delay });
|
|
2672
|
+
await new Promise((r) => {
|
|
2673
|
+
const timer = setTimeout(r, delay);
|
|
2674
|
+
const onAbort = () => {
|
|
2675
|
+
clearTimeout(timer);
|
|
2676
|
+
r();
|
|
2677
|
+
};
|
|
2678
|
+
lifecycleAc.signal.addEventListener("abort", onAbort, { once: true });
|
|
2679
|
+
});
|
|
2680
|
+
if (lifecycleAc.signal.aborted) break;
|
|
2681
|
+
setStatus("connecting");
|
|
2682
|
+
params.onReconnect?.({ attempt });
|
|
2473
2683
|
}
|
|
2684
|
+
setStatus("disconnected");
|
|
2474
2685
|
})();
|
|
2475
|
-
return
|
|
2476
|
-
unsubscribe: () => ac.abort()
|
|
2477
|
-
};
|
|
2686
|
+
return subscription;
|
|
2478
2687
|
}
|
|
2479
2688
|
var RagableBrowserAgentsClient = class {
|
|
2480
2689
|
constructor(options) {
|
|
@@ -2520,6 +2729,7 @@ var RagableBrowser = class {
|
|
|
2520
2729
|
__publicField(this, "agents");
|
|
2521
2730
|
__publicField(this, "auth");
|
|
2522
2731
|
__publicField(this, "database");
|
|
2732
|
+
__publicField(this, "db");
|
|
2523
2733
|
__publicField(this, "transport");
|
|
2524
2734
|
__publicField(this, "_ragableAuth");
|
|
2525
2735
|
/** Delegates to `database.from()`. Kept for back-compat — prefer `database.from()`. */
|
|
@@ -2559,6 +2769,7 @@ var RagableBrowser = class {
|
|
|
2559
2769
|
this._ragableAuth
|
|
2560
2770
|
);
|
|
2561
2771
|
this.database._setTransport(this.transport);
|
|
2772
|
+
this.db = this.database;
|
|
2562
2773
|
}
|
|
2563
2774
|
destroy() {
|
|
2564
2775
|
this._ragableAuth?.destroy();
|
|
@@ -2632,7 +2843,7 @@ function createClient(options) {
|
|
|
2632
2843
|
if (isServerClientOptions(options)) {
|
|
2633
2844
|
if (typeof options === "object" && options !== null && "organizationId" in options && typeof options.organizationId === "string") {
|
|
2634
2845
|
console.warn(
|
|
2635
|
-
"[@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, ... }))."
|
|
2846
|
+
"[@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, ... }))."
|
|
2636
2847
|
);
|
|
2637
2848
|
}
|
|
2638
2849
|
return new Ragable(options);
|