@bounded-sh/core 0.0.2 → 0.0.3

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/README.md CHANGED
@@ -63,7 +63,7 @@ function getConfig(): Promise<ClientConfig>;
63
63
  function get(path: string): Promise<any>;
64
64
  function getMany(paths: string[], options?: { bypassCache?: boolean }): Promise<GetManyResult[]>;
65
65
  function set(path: string, data: any, options?: SetOptions): Promise<any>;
66
- function setMany(paths: { [key: string]: any }, options?: SetOptions): Promise<any>;
66
+ function setMany(many: { path: string; document: any }[], options?: SetOptions): Promise<any>;
67
67
  function setFile(path: string, file: File, metadata?: any): Promise<any>;
68
68
  function getFiles(path: string): Promise<any>;
69
69
  function runQuery(queryString: string, variables?: any): Promise<any>;
@@ -75,4 +75,4 @@ function subscribe(path: string, options?: SubscriptionOptions): Promise<() => v
75
75
 
76
76
  ## Contributing
77
77
 
78
- Please see the main repository for contribution guidelines.
78
+ Please see the main repository for contribution guidelines.
@@ -12,6 +12,31 @@ export type LiveIntentOptions = {
12
12
  */
13
13
  fireAndForget?: boolean;
14
14
  };
15
+ export type LiveStatusOptions = {
16
+ /** Per-call timeout (ms) for the HTTP request to the realtime worker. */
17
+ timeoutMs?: number;
18
+ /** Extra headers (advanced/testing). */
19
+ headers?: Record<string, string>;
20
+ };
21
+ export type LiveStopReason = 'idle' | 'lifetime' | 'error' | 'manual' | 'evicted' | string;
22
+ export type LiveStatus = {
23
+ available: boolean;
24
+ started: boolean;
25
+ module?: string;
26
+ etag?: string;
27
+ running?: boolean;
28
+ tick?: number;
29
+ lastErr?: string | null;
30
+ stopReason?: LiveStopReason | null;
31
+ lastTickAt?: number | null;
32
+ nextTickAt?: number | null;
33
+ nextAlarmAt?: number | null;
34
+ generation?: number;
35
+ connections?: number;
36
+ startedAtMs?: number | null;
37
+ stoppedAtMs?: number | null;
38
+ reason?: string;
39
+ };
15
40
  export declare class LiveIntentError extends Error {
16
41
  statusCode?: number | undefined;
17
42
  details?: any | undefined;
@@ -33,8 +58,21 @@ export declare class LiveIntentError extends Error {
33
58
  export declare function intent(roomPath: string, intent: unknown, opts?: LiveIntentOptions): Promise<{
34
59
  ok: true;
35
60
  }>;
61
+ /**
62
+ * Fetch the runtime status for a live room.
63
+ *
64
+ * const s = await bounded.live.status('rooms/abc');
65
+ * console.log(s.running, s.stopReason, s.generation, s.etag);
66
+ *
67
+ * This is diagnostic/ops surface. It reports whether the room facet exists,
68
+ * whether it is currently ticking, why it last stopped, which module etag is
69
+ * loaded, and the current generation used after terminal restarts.
70
+ */
71
+ export declare function status(roomPath: string, opts?: LiveStatusOptions): Promise<LiveStatus>;
36
72
  export type SubscribeViewOptions = {
37
- /** Address whose view to read. Defaults to the logged-in user's address. */
73
+ /** User id whose view to read. Defaults to the logged-in user's @user.id. */
74
+ userId?: string;
75
+ /** Legacy alias for wallet-address keyed view docs. Prefer userId for new rooms. */
38
76
  address?: string;
39
77
  /** Called with the latest per-player view document. */
40
78
  onData: (view: any) => void;
@@ -44,10 +82,11 @@ export type SubscribeViewOptions = {
44
82
  /**
45
83
  * Subscribe to YOUR per-player view of a room. Thin sugar over:
46
84
  *
47
- * subscribe('<roomPath>/view/<myAddress>', { onData, onError })
85
+ * subscribe('<roomPath>/view/<myUserId>', { onData, onError })
48
86
  *
49
- * The address defaults to the logged-in user's address (from the session
50
- * token's claims); pass `opts.address` to override. Returns the unsubscribe
87
+ * The view id defaults to the logged-in user's @user.id (from the session token
88
+ * claims); pass `opts.userId` to override. `opts.address` is kept as a legacy
89
+ * alias for older wallet-address keyed policies. Returns the unsubscribe
51
90
  * function (a Promise<() => Promise<void>>, same as `subscribe`).
52
91
  *
53
92
  * Note: this is a browser-first helper (the WS subscription manager is
@@ -57,5 +96,6 @@ export declare function subscribeView(roomPath: string, opts: SubscribeViewOptio
57
96
  /** The `bounded.live` namespace surface. */
58
97
  export declare const live: {
59
98
  intent: typeof intent;
99
+ status: typeof status;
60
100
  subscribeView: typeof subscribeView;
61
101
  };
@@ -236,6 +236,13 @@ export declare function runExpressionMany(many: {
236
236
  returnType?: 'Bool' | 'String' | 'Int' | 'UInt';
237
237
  _overrides?: RequestOverrides;
238
238
  }[]): Promise<RunExpressionResult[]>;
239
+ /**
240
+ * Write a document at `path`. Sugar for a one-element {@link setMany}.
241
+ *
242
+ * **Delete:** pass `null` as the document to delete it — `set(path, null)` is
243
+ * the delete (there is no separate `del`/`remove`). It is routed through the
244
+ * collection's policy `delete` rule and broadcasts a delete to subscribers.
245
+ */
239
246
  export declare function set(path: string, document: any, options?: SetOptions): Promise<any>;
240
247
  export declare function setMany(many: {
241
248
  path: string;
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ export { RealtimeStore, getRealtimeStore, resetRealtimeStore } from './client/re
23
23
  export type { StorageTier, SubscriptionStatus, SubscriptionState, SubscribeOptions, DeltaChange } from './client/realtime-store';
24
24
  export { functions, invoke as invokeFunction, FunctionInvokeError } from './client/functions';
25
25
  export type { InvokeOptions } from './client/functions';
26
- export { live, intent as liveIntent, subscribeView as subscribeLiveView, LiveIntentError } from './client/live';
27
- export type { LiveIntentOptions, SubscribeViewOptions } from './client/live';
26
+ export { live, intent as liveIntent, status as liveStatus, subscribeView as subscribeLiveView, LiveIntentError } from './client/live';
27
+ export type { LiveIntentOptions, LiveStatus, LiveStatusOptions, SubscribeViewOptions } from './client/live';
28
28
  export { withEffects, isEffectResult, defineLiveModule, EFFECT_INTENT_ADDRESS } from './client/live-effects';
29
29
  export type { Effect, EffectKind, EffectResult, LiveIntent, LiveTickResult, LiveModule } from './client/live-effects';
package/dist/index.js CHANGED
@@ -4929,6 +4929,13 @@ async function runExpressionMany(many) {
4929
4929
  throw error;
4930
4930
  }
4931
4931
  }
4932
+ /**
4933
+ * Write a document at `path`. Sugar for a one-element {@link setMany}.
4934
+ *
4935
+ * **Delete:** pass `null` as the document to delete it — `set(path, null)` is
4936
+ * the delete (there is no separate `del`/`remove`). It is routed through the
4937
+ * collection's policy `delete` rule and broadcasts a delete to subscribers.
4938
+ */
4932
4939
  async function set(path, document, options) {
4933
4940
  const result = await setMany([{ path, document }], options);
4934
4941
  // Clear cache entries that might be affected by this update
@@ -7510,6 +7517,7 @@ const functions = { invoke };
7510
7517
  // collection. This file is the consumer surface around that runtime:
7511
7518
  //
7512
7519
  // bounded.live.intent(roomPath, intent) -> POST {realtime}/live/intent
7520
+ // bounded.live.status(roomPath) -> GET {realtime}/live/status
7513
7521
  //
7514
7522
  // It mirrors `functions.invoke` exactly: it attaches the caller's session token
7515
7523
  // AUTOMATICALLY (the same token the SDK uses for data reads/writes), and throws
@@ -7522,18 +7530,14 @@ const functions = { invoke };
7522
7530
  // Addressing: clients address rooms BY PATH (the `path` field in the body). The
7523
7531
  // worker derives the roomId from `body.path` and sets X-Room-Id internally;
7524
7532
  // clients NEVER set X-Room-Id. The caller's address is taken server-side from
7525
- // the Authorization header (auth.userAddress) — it is NOT sent in the body.
7533
+ // the Authorization header (auth.userId/userAddress) — it is NOT sent in the
7534
+ // body.
7526
7535
  //
7527
7536
  // Subscribing to your view: a per-player view doc lives at
7528
- // `<roomPath>/view/<myAddress>` (the policy declares
7529
- // `rooms/$roomId/view/$addr` ephemeral with `read: $addr == @user.address`).
7530
- // That path has an EVEN segment count, so it is an ordinary document path —
7531
- // plain `subscribe('<roomPath>/view/<myAddress>', { onData })` already works.
7532
- // `subscribeView()` below is thin sugar over that (path construction + address
7533
- // defaulting); it adds zero new transport.
7534
- //
7535
- // Status polling (GET /live/status?path=<sessionCollection>/<roomId>) is out of
7536
- // scope for this piece — see follow-up.
7537
+ // `<roomPath>/view/<myUserId>` (the policy declares
7538
+ // `rooms/$roomId/view/$userId` ephemeral with `read: $userId == @user.id`).
7539
+ // Wallet-address keyed view paths remain supported through opts.address for
7540
+ // older policies, but new live rooms should key views by @user.id.
7537
7541
  // ---------------------------------------------------------------------------
7538
7542
  class LiveIntentError extends Error {
7539
7543
  constructor(message, statusCode, details) {
@@ -7614,7 +7618,7 @@ async function intent(roomPath, intent, opts = {}) {
7614
7618
  res = await fetch(`${base}/live/intent`, {
7615
7619
  method: 'POST',
7616
7620
  headers,
7617
- body: JSON.stringify({ path: roomPath, intent }),
7621
+ body: JSON.stringify({ path: normalizedRoomPath, intent }),
7618
7622
  signal: controller.signal,
7619
7623
  });
7620
7624
  }
@@ -7642,43 +7646,95 @@ async function intent(roomPath, intent, opts = {}) {
7642
7646
  }
7643
7647
  return (body !== null && body !== void 0 ? body : { ok: true });
7644
7648
  }
7649
+ /**
7650
+ * Fetch the runtime status for a live room.
7651
+ *
7652
+ * const s = await bounded.live.status('rooms/abc');
7653
+ * console.log(s.running, s.stopReason, s.generation, s.etag);
7654
+ *
7655
+ * This is diagnostic/ops surface. It reports whether the room facet exists,
7656
+ * whether it is currently ticking, why it last stopped, which module etag is
7657
+ * loaded, and the current generation used after terminal restarts.
7658
+ */
7659
+ async function status(roomPath, opts = {}) {
7660
+ var _a, _b, _c, _d, _e;
7661
+ if (!roomPath || typeof roomPath !== 'string') {
7662
+ throw new LiveIntentError('A room path is required');
7663
+ }
7664
+ const normalizedRoomPath = roomPath.replace(/\/$/, '');
7665
+ const config = await getConfig();
7666
+ const base = realtimeHttpBase(config.wsApiUrl);
7667
+ const headers = Object.assign({ 'X-App-Id': config.appId, 'X-Public-App-Id': config.appId }, ((_a = opts.headers) !== null && _a !== void 0 ? _a : {}));
7668
+ const controller = new AbortController();
7669
+ const timeoutMs = (_b = opts.timeoutMs) !== null && _b !== void 0 ? _b : 15000;
7670
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
7671
+ let res;
7672
+ try {
7673
+ res = await fetch(`${base}/live/status?path=${encodeURIComponent(normalizedRoomPath)}`, {
7674
+ method: 'GET',
7675
+ headers,
7676
+ signal: controller.signal,
7677
+ });
7678
+ }
7679
+ catch (err) {
7680
+ clearTimeout(timer);
7681
+ if ((err === null || err === void 0 ? void 0 : err.name) === 'AbortError') {
7682
+ throw new LiveIntentError(`Live status for "${normalizedRoomPath}" timed out after ${timeoutMs}ms`);
7683
+ }
7684
+ throw new LiveIntentError(`Failed to reach the realtime worker: ${(_c = err === null || err === void 0 ? void 0 : err.message) !== null && _c !== void 0 ? _c : String(err)}`);
7685
+ }
7686
+ clearTimeout(timer);
7687
+ let body = null;
7688
+ const text = await res.text();
7689
+ if (text) {
7690
+ try {
7691
+ body = JSON.parse(text);
7692
+ }
7693
+ catch (_f) {
7694
+ body = { raw: text };
7695
+ }
7696
+ }
7697
+ if (!res.ok) {
7698
+ const message = (_e = (_d = body === null || body === void 0 ? void 0 : body.error) !== null && _d !== void 0 ? _d : body === null || body === void 0 ? void 0 : body.message) !== null && _e !== void 0 ? _e : `Live status failed with HTTP ${res.status}`;
7699
+ throw new LiveIntentError(message, res.status, body);
7700
+ }
7701
+ return body;
7702
+ }
7645
7703
  /**
7646
7704
  * Subscribe to YOUR per-player view of a room. Thin sugar over:
7647
7705
  *
7648
- * subscribe('<roomPath>/view/<myAddress>', { onData, onError })
7706
+ * subscribe('<roomPath>/view/<myUserId>', { onData, onError })
7649
7707
  *
7650
- * The address defaults to the logged-in user's address (from the session
7651
- * token's claims); pass `opts.address` to override. Returns the unsubscribe
7708
+ * The view id defaults to the logged-in user's @user.id (from the session token
7709
+ * claims); pass `opts.userId` to override. `opts.address` is kept as a legacy
7710
+ * alias for older wallet-address keyed policies. Returns the unsubscribe
7652
7711
  * function (a Promise<() => Promise<void>>, same as `subscribe`).
7653
7712
  *
7654
7713
  * Note: this is a browser-first helper (the WS subscription manager is
7655
7714
  * browser-oriented). Server consumers should use `live.intent`.
7656
7715
  */
7657
7716
  async function subscribeView(roomPath, opts) {
7658
- var _a;
7717
+ var _a, _b, _c;
7659
7718
  if (!roomPath || typeof roomPath !== 'string') {
7660
7719
  throw new LiveIntentError('A room path is required');
7661
7720
  }
7662
7721
  if (!opts || typeof opts.onData !== 'function') {
7663
7722
  throw new LiveIntentError('subscribeView requires an onData callback');
7664
7723
  }
7665
- let address = opts.address;
7666
- if (!address) {
7724
+ let viewUserId = (_a = opts.userId) !== null && _a !== void 0 ? _a : opts.address;
7725
+ if (!viewUserId) {
7667
7726
  const config = await getConfig();
7668
7727
  const info = await getUserInfo(config.isServer);
7669
- // getUserInfo returns the RAW idToken payload the wallet address lives in
7670
- // the `custom:walletAddress` claim, NOT a flat `address` field. Reading only
7671
- // `.address` meant auto-resolution NEVER worked, forcing every caller (browser
7672
- // AND server) to pass opts.address the client having to hand the backend an
7673
- // identity it already authenticated. Resolve it from the actual claim so a
7674
- // logged-in caller never needs to pass its own address.
7675
- address = (_a = info === null || info === void 0 ? void 0 : info.address) !== null && _a !== void 0 ? _a : info === null || info === void 0 ? void 0 : info['custom:walletAddress'];
7728
+ // getUserInfo returns the RAW idToken payload. The universal live view key
7729
+ // is @user.id (`custom:userId`); wallet-address keyed views are still
7730
+ // resolved as a compatibility fallback for older policies.
7731
+ viewUserId = (_c = (_b = info === null || info === void 0 ? void 0 : info['custom:userId']) !== null && _b !== void 0 ? _b : info === null || info === void 0 ? void 0 : info['custom:walletAddress']) !== null && _c !== void 0 ? _c : info === null || info === void 0 ? void 0 : info.address;
7676
7732
  }
7677
- if (!address || typeof address !== 'string') {
7678
- throw new LiveIntentError('Could not resolve a player address for subscribeView; pass opts.address or log in first');
7733
+ if (!viewUserId || typeof viewUserId !== 'string') {
7734
+ throw new LiveIntentError('Could not resolve a player view id for subscribeView; pass opts.userId or log in first');
7679
7735
  }
7680
7736
  const normalizedRoomPath = roomPath.replace(/\/$/, '');
7681
- const viewPath = `${normalizedRoomPath}/view/${address}`;
7737
+ const viewPath = `${normalizedRoomPath}/view/${viewUserId}`;
7682
7738
  // This is a LIVE/room subscription by construction (the caller hands us the
7683
7739
  // room path), so we route its connection to the per-room DO where the live
7684
7740
  // view fan-out runs. App code passes NO routing — the room path is the helper's
@@ -7689,7 +7745,7 @@ async function subscribeView(roomPath, opts) {
7689
7745
  return subscribeV2(viewPath, { onData: opts.onData, onError: opts.onError }, normalizedRoomPath);
7690
7746
  }
7691
7747
  /** The `bounded.live` namespace surface. */
7692
- const live = { intent, subscribeView };
7748
+ const live = { intent, status, subscribeView };
7693
7749
 
7694
7750
  // ---------------------------------------------------------------------------
7695
7751
  // live-effects.ts -- AUTHOR-facing types + helpers for writing a Bounded live
@@ -7778,6 +7834,7 @@ exports.invokeFunction = invoke;
7778
7834
  exports.isEffectResult = isEffectResult;
7779
7835
  exports.live = live;
7780
7836
  exports.liveIntent = intent;
7837
+ exports.liveStatus = status;
7781
7838
  exports.queryAggregate = queryAggregate;
7782
7839
  exports.reconnectWithNewAuth = reconnectWithNewAuth;
7783
7840
  exports.refreshSession = refreshSession;