@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.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 ac = new AbortController();
2423
- followAbortSignal(params.signal, ac);
2424
- const headers = new Headers(options.headers);
2425
- headers.set("Authorization", `Bearer ${token}`);
2426
- headers.set("Content-Type", "application/json");
2427
- const response = await fetchImpl(
2428
- `${normalizeBrowserApiBase()}/auth-groups/${gid}/data/realtime/stream`,
2429
- {
2430
- method: "POST",
2431
- headers,
2432
- body: JSON.stringify({
2433
- databaseInstanceId,
2434
- channels: params.channels
2435
- }),
2436
- signal: ac.signal
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
- const payload = await parseMaybeJsonBody(response);
2440
- if (!response.ok) {
2441
- const message = extractErrorMessage(payload, response.statusText);
2442
- throw new RagableError(message, response.status, payload);
2443
- }
2444
- const streamBody = response.body;
2445
- if (!streamBody) {
2446
- throw new RagableError(
2447
- "Realtime stream has no body",
2448
- 502,
2449
- { code: "SDK_REALTIME_NO_BODY" }
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
- void (async () => {
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
- for await (const evt of readSseStream(streamBody)) {
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
- } catch (e) {
2471
- if (e.name === "AbortError") return;
2472
- params.onError?.(e.message);
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);