@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.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 ac = new AbortController();
2349
- followAbortSignal(params.signal, ac);
2350
- const headers = new Headers(options.headers);
2351
- headers.set("Authorization", `Bearer ${token}`);
2352
- headers.set("Content-Type", "application/json");
2353
- const response = await fetchImpl(
2354
- `${normalizeBrowserApiBase()}/auth-groups/${gid}/data/realtime/stream`,
2355
- {
2356
- method: "POST",
2357
- headers,
2358
- body: JSON.stringify({
2359
- databaseInstanceId,
2360
- channels: params.channels
2361
- }),
2362
- signal: ac.signal
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
- const payload = await parseMaybeJsonBody(response);
2366
- if (!response.ok) {
2367
- const message = extractErrorMessage(payload, response.statusText);
2368
- throw new RagableError(message, response.status, payload);
2369
- }
2370
- const streamBody = response.body;
2371
- if (!streamBody) {
2372
- throw new RagableError(
2373
- "Realtime stream has no body",
2374
- 502,
2375
- { code: "SDK_REALTIME_NO_BODY" }
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
- void (async () => {
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
- for await (const evt of readSseStream(streamBody)) {
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
- } catch (e) {
2397
- if (e.name === "AbortError") return;
2398
- params.onError?.(e.message);
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);