@bounded-sh/core 0.0.2 → 0.0.4
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 +2 -2
- package/dist/client/live.d.ts +44 -4
- package/dist/client/operations.d.ts +7 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +155 -28
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +152 -29
- package/dist/index.mjs.map +1 -1
- package/dist/time.d.ts +59 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4909,6 +4909,13 @@ async function runExpressionMany(many) {
|
|
|
4909
4909
|
throw error;
|
|
4910
4910
|
}
|
|
4911
4911
|
}
|
|
4912
|
+
/**
|
|
4913
|
+
* Write a document at `path`. Sugar for a one-element {@link setMany}.
|
|
4914
|
+
*
|
|
4915
|
+
* **Delete:** pass `null` as the document to delete it — `set(path, null)` is
|
|
4916
|
+
* the delete (there is no separate `del`/`remove`). It is routed through the
|
|
4917
|
+
* collection's policy `delete` rule and broadcasts a delete to subscribers.
|
|
4918
|
+
*/
|
|
4912
4919
|
async function set(path, document, options) {
|
|
4913
4920
|
const result = await setMany([{ path, document }], options);
|
|
4914
4921
|
// Clear cache entries that might be affected by this update
|
|
@@ -6587,6 +6594,73 @@ function serverTimestamp() {
|
|
|
6587
6594
|
return { operation: 'time', value: 'now' };
|
|
6588
6595
|
}
|
|
6589
6596
|
|
|
6597
|
+
/**
|
|
6598
|
+
* Time units in Bounded — read this once and never get a 1000× timestamp bug.
|
|
6599
|
+
*
|
|
6600
|
+
* **Bounded's policy/proof layer is Unix SECONDS.** `@time.now` in a rule,
|
|
6601
|
+
* `rollingSum` `windowSeconds`, `scheduledAt`, and any timestamp *field your
|
|
6602
|
+
* policy compares against `@time.now`* are all **seconds**. (This is also what
|
|
6603
|
+
* the chain uses — Solana's on-chain clock is `unix_timestamp` in seconds — so a
|
|
6604
|
+
* single seconds unit works for both onchain and offchain rules.)
|
|
6605
|
+
*
|
|
6606
|
+
* **JavaScript is MILLISECONDS.** `Date.now()`, `new Date().getTime()`, and the
|
|
6607
|
+
* auto-stamped system fields `_createdAt` / `_updatedAt` are all **ms**.
|
|
6608
|
+
*
|
|
6609
|
+
* Comparing across the two (e.g. `@time.now - myField` where `myField` was set
|
|
6610
|
+
* from `Date.now()`) is 1000× off, so a freshness / TTL check silently treats
|
|
6611
|
+
* every row as ancient (or far-future) and drops it — which reads as "realtime
|
|
6612
|
+
* isn't delivering" when the data is actually fine.
|
|
6613
|
+
*
|
|
6614
|
+
* **The rules:**
|
|
6615
|
+
* - To **write** a timestamp a policy will read, prefer **`serverTimestamp()`**
|
|
6616
|
+
* (from this package) — the *server* stamps it in seconds, so it matches
|
|
6617
|
+
* `@time.now` AND can't be forged by the client (use it for TTLs, rate windows,
|
|
6618
|
+
* anti-cheat). Use `now()` only when you need the value in client code before
|
|
6619
|
+
* the write.
|
|
6620
|
+
* - To **compare** timestamps in client/render code, use `now()` (seconds), not
|
|
6621
|
+
* `Date.now()` (ms), and `toSeconds()` to convert the ms system fields
|
|
6622
|
+
* (`_createdAt`/`_updatedAt`) or any `Date.now()` value first.
|
|
6623
|
+
*/
|
|
6624
|
+
/**
|
|
6625
|
+
* Current time as a **Unix timestamp in seconds** — the unit Bounded policy rules
|
|
6626
|
+
* use (`@time.now`). Use this (not `Date.now()`) when you need a timestamp in
|
|
6627
|
+
* client code (e.g. a freshness check). For a value you *store* and a policy
|
|
6628
|
+
* reads, prefer the server-authoritative {@link serverTimestamp} instead.
|
|
6629
|
+
*
|
|
6630
|
+
* ```ts
|
|
6631
|
+
* // stale if >15s old — seconds vs seconds ✓
|
|
6632
|
+
* if (now() - doc.lastSeenSeconds > 15) renderStale();
|
|
6633
|
+
* ```
|
|
6634
|
+
*/
|
|
6635
|
+
function now() {
|
|
6636
|
+
return Math.floor(Date.now() / 1000);
|
|
6637
|
+
}
|
|
6638
|
+
/**
|
|
6639
|
+
* Convert a JavaScript millisecond timestamp to Bounded's **seconds**. Accepts a
|
|
6640
|
+
* `Date`, or an ms number such as `Date.now()` or a doc's `_createdAt` /
|
|
6641
|
+
* `_updatedAt` system field.
|
|
6642
|
+
*
|
|
6643
|
+
* ```ts
|
|
6644
|
+
* // doc is >15s old (compare the ms system field in seconds):
|
|
6645
|
+
* if (now() - toSeconds(doc._updatedAt) > 15) renderStale();
|
|
6646
|
+
* ```
|
|
6647
|
+
*/
|
|
6648
|
+
function toSeconds(msOrDate) {
|
|
6649
|
+
const ms = msOrDate instanceof Date ? msOrDate.getTime() : msOrDate;
|
|
6650
|
+
return Math.floor(ms / 1000);
|
|
6651
|
+
}
|
|
6652
|
+
/**
|
|
6653
|
+
* Convert a Bounded **seconds** timestamp back to JavaScript **milliseconds** —
|
|
6654
|
+
* e.g. to build a `Date` or do client-side date math/formatting.
|
|
6655
|
+
*
|
|
6656
|
+
* ```ts
|
|
6657
|
+
* new Date(toMillis(doc.createdAtSeconds)).toLocaleString();
|
|
6658
|
+
* ```
|
|
6659
|
+
*/
|
|
6660
|
+
function toMillis(seconds) {
|
|
6661
|
+
return seconds * 1000;
|
|
6662
|
+
}
|
|
6663
|
+
|
|
6590
6664
|
// ---------------------------------------------------------------------------
|
|
6591
6665
|
// realtime-store.ts — Client-side state manager for realtime apps.
|
|
6592
6666
|
//
|
|
@@ -7490,6 +7564,7 @@ const functions = { invoke };
|
|
|
7490
7564
|
// collection. This file is the consumer surface around that runtime:
|
|
7491
7565
|
//
|
|
7492
7566
|
// bounded.live.intent(roomPath, intent) -> POST {realtime}/live/intent
|
|
7567
|
+
// bounded.live.status(roomPath) -> GET {realtime}/live/status
|
|
7493
7568
|
//
|
|
7494
7569
|
// It mirrors `functions.invoke` exactly: it attaches the caller's session token
|
|
7495
7570
|
// AUTOMATICALLY (the same token the SDK uses for data reads/writes), and throws
|
|
@@ -7502,18 +7577,14 @@ const functions = { invoke };
|
|
|
7502
7577
|
// Addressing: clients address rooms BY PATH (the `path` field in the body). The
|
|
7503
7578
|
// worker derives the roomId from `body.path` and sets X-Room-Id internally;
|
|
7504
7579
|
// clients NEVER set X-Room-Id. The caller's address is taken server-side from
|
|
7505
|
-
// the Authorization header (auth.userAddress) — it is NOT sent in the
|
|
7580
|
+
// the Authorization header (auth.userId/userAddress) — it is NOT sent in the
|
|
7581
|
+
// body.
|
|
7506
7582
|
//
|
|
7507
7583
|
// Subscribing to your view: a per-player view doc lives at
|
|
7508
|
-
// `<roomPath>/view/<
|
|
7509
|
-
// `rooms/$roomId/view/$
|
|
7510
|
-
//
|
|
7511
|
-
//
|
|
7512
|
-
// `subscribeView()` below is thin sugar over that (path construction + address
|
|
7513
|
-
// defaulting); it adds zero new transport.
|
|
7514
|
-
//
|
|
7515
|
-
// Status polling (GET /live/status?path=<sessionCollection>/<roomId>) is out of
|
|
7516
|
-
// scope for this piece — see follow-up.
|
|
7584
|
+
// `<roomPath>/view/<myUserId>` (the policy declares
|
|
7585
|
+
// `rooms/$roomId/view/$userId` ephemeral with `read: $userId == @user.id`).
|
|
7586
|
+
// Wallet-address keyed view paths remain supported through opts.address for
|
|
7587
|
+
// older policies, but new live rooms should key views by @user.id.
|
|
7517
7588
|
// ---------------------------------------------------------------------------
|
|
7518
7589
|
class LiveIntentError extends Error {
|
|
7519
7590
|
constructor(message, statusCode, details) {
|
|
@@ -7594,7 +7665,7 @@ async function intent(roomPath, intent, opts = {}) {
|
|
|
7594
7665
|
res = await fetch(`${base}/live/intent`, {
|
|
7595
7666
|
method: 'POST',
|
|
7596
7667
|
headers,
|
|
7597
|
-
body: JSON.stringify({ path:
|
|
7668
|
+
body: JSON.stringify({ path: normalizedRoomPath, intent }),
|
|
7598
7669
|
signal: controller.signal,
|
|
7599
7670
|
});
|
|
7600
7671
|
}
|
|
@@ -7622,43 +7693,95 @@ async function intent(roomPath, intent, opts = {}) {
|
|
|
7622
7693
|
}
|
|
7623
7694
|
return (body !== null && body !== void 0 ? body : { ok: true });
|
|
7624
7695
|
}
|
|
7696
|
+
/**
|
|
7697
|
+
* Fetch the runtime status for a live room.
|
|
7698
|
+
*
|
|
7699
|
+
* const s = await bounded.live.status('rooms/abc');
|
|
7700
|
+
* console.log(s.running, s.stopReason, s.generation, s.etag);
|
|
7701
|
+
*
|
|
7702
|
+
* This is diagnostic/ops surface. It reports whether the room facet exists,
|
|
7703
|
+
* whether it is currently ticking, why it last stopped, which module etag is
|
|
7704
|
+
* loaded, and the current generation used after terminal restarts.
|
|
7705
|
+
*/
|
|
7706
|
+
async function status(roomPath, opts = {}) {
|
|
7707
|
+
var _a, _b, _c, _d, _e;
|
|
7708
|
+
if (!roomPath || typeof roomPath !== 'string') {
|
|
7709
|
+
throw new LiveIntentError('A room path is required');
|
|
7710
|
+
}
|
|
7711
|
+
const normalizedRoomPath = roomPath.replace(/\/$/, '');
|
|
7712
|
+
const config = await getConfig();
|
|
7713
|
+
const base = realtimeHttpBase(config.wsApiUrl);
|
|
7714
|
+
const headers = Object.assign({ 'X-App-Id': config.appId, 'X-Public-App-Id': config.appId }, ((_a = opts.headers) !== null && _a !== void 0 ? _a : {}));
|
|
7715
|
+
const controller = new AbortController();
|
|
7716
|
+
const timeoutMs = (_b = opts.timeoutMs) !== null && _b !== void 0 ? _b : 15000;
|
|
7717
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
7718
|
+
let res;
|
|
7719
|
+
try {
|
|
7720
|
+
res = await fetch(`${base}/live/status?path=${encodeURIComponent(normalizedRoomPath)}`, {
|
|
7721
|
+
method: 'GET',
|
|
7722
|
+
headers,
|
|
7723
|
+
signal: controller.signal,
|
|
7724
|
+
});
|
|
7725
|
+
}
|
|
7726
|
+
catch (err) {
|
|
7727
|
+
clearTimeout(timer);
|
|
7728
|
+
if ((err === null || err === void 0 ? void 0 : err.name) === 'AbortError') {
|
|
7729
|
+
throw new LiveIntentError(`Live status for "${normalizedRoomPath}" timed out after ${timeoutMs}ms`);
|
|
7730
|
+
}
|
|
7731
|
+
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)}`);
|
|
7732
|
+
}
|
|
7733
|
+
clearTimeout(timer);
|
|
7734
|
+
let body = null;
|
|
7735
|
+
const text = await res.text();
|
|
7736
|
+
if (text) {
|
|
7737
|
+
try {
|
|
7738
|
+
body = JSON.parse(text);
|
|
7739
|
+
}
|
|
7740
|
+
catch (_f) {
|
|
7741
|
+
body = { raw: text };
|
|
7742
|
+
}
|
|
7743
|
+
}
|
|
7744
|
+
if (!res.ok) {
|
|
7745
|
+
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}`;
|
|
7746
|
+
throw new LiveIntentError(message, res.status, body);
|
|
7747
|
+
}
|
|
7748
|
+
return body;
|
|
7749
|
+
}
|
|
7625
7750
|
/**
|
|
7626
7751
|
* Subscribe to YOUR per-player view of a room. Thin sugar over:
|
|
7627
7752
|
*
|
|
7628
|
-
* subscribe('<roomPath>/view/<
|
|
7753
|
+
* subscribe('<roomPath>/view/<myUserId>', { onData, onError })
|
|
7629
7754
|
*
|
|
7630
|
-
* The
|
|
7631
|
-
*
|
|
7755
|
+
* The view id defaults to the logged-in user's @user.id (from the session token
|
|
7756
|
+
* claims); pass `opts.userId` to override. `opts.address` is kept as a legacy
|
|
7757
|
+
* alias for older wallet-address keyed policies. Returns the unsubscribe
|
|
7632
7758
|
* function (a Promise<() => Promise<void>>, same as `subscribe`).
|
|
7633
7759
|
*
|
|
7634
7760
|
* Note: this is a browser-first helper (the WS subscription manager is
|
|
7635
7761
|
* browser-oriented). Server consumers should use `live.intent`.
|
|
7636
7762
|
*/
|
|
7637
7763
|
async function subscribeView(roomPath, opts) {
|
|
7638
|
-
var _a;
|
|
7764
|
+
var _a, _b, _c;
|
|
7639
7765
|
if (!roomPath || typeof roomPath !== 'string') {
|
|
7640
7766
|
throw new LiveIntentError('A room path is required');
|
|
7641
7767
|
}
|
|
7642
7768
|
if (!opts || typeof opts.onData !== 'function') {
|
|
7643
7769
|
throw new LiveIntentError('subscribeView requires an onData callback');
|
|
7644
7770
|
}
|
|
7645
|
-
let
|
|
7646
|
-
if (!
|
|
7771
|
+
let viewUserId = (_a = opts.userId) !== null && _a !== void 0 ? _a : opts.address;
|
|
7772
|
+
if (!viewUserId) {
|
|
7647
7773
|
const config = await getConfig();
|
|
7648
7774
|
const info = await getUserInfo(config.isServer);
|
|
7649
|
-
// getUserInfo returns the RAW idToken payload
|
|
7650
|
-
//
|
|
7651
|
-
//
|
|
7652
|
-
|
|
7653
|
-
// identity it already authenticated. Resolve it from the actual claim so a
|
|
7654
|
-
// logged-in caller never needs to pass its own address.
|
|
7655
|
-
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'];
|
|
7775
|
+
// getUserInfo returns the RAW idToken payload. The universal live view key
|
|
7776
|
+
// is @user.id (`custom:userId`); wallet-address keyed views are still
|
|
7777
|
+
// resolved as a compatibility fallback for older policies.
|
|
7778
|
+
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;
|
|
7656
7779
|
}
|
|
7657
|
-
if (!
|
|
7658
|
-
throw new LiveIntentError('Could not resolve a player
|
|
7780
|
+
if (!viewUserId || typeof viewUserId !== 'string') {
|
|
7781
|
+
throw new LiveIntentError('Could not resolve a player view id for subscribeView; pass opts.userId or log in first');
|
|
7659
7782
|
}
|
|
7660
7783
|
const normalizedRoomPath = roomPath.replace(/\/$/, '');
|
|
7661
|
-
const viewPath = `${normalizedRoomPath}/view/${
|
|
7784
|
+
const viewPath = `${normalizedRoomPath}/view/${viewUserId}`;
|
|
7662
7785
|
// This is a LIVE/room subscription by construction (the caller hands us the
|
|
7663
7786
|
// room path), so we route its connection to the per-room DO where the live
|
|
7664
7787
|
// view fan-out runs. App code passes NO routing — the room path is the helper's
|
|
@@ -7669,7 +7792,7 @@ async function subscribeView(roomPath, opts) {
|
|
|
7669
7792
|
return subscribeV2(viewPath, { onData: opts.onData, onError: opts.onError }, normalizedRoomPath);
|
|
7670
7793
|
}
|
|
7671
7794
|
/** The `bounded.live` namespace surface. */
|
|
7672
|
-
const live = { intent, subscribeView };
|
|
7795
|
+
const live = { intent, status, subscribeView };
|
|
7673
7796
|
|
|
7674
7797
|
// ---------------------------------------------------------------------------
|
|
7675
7798
|
// live-effects.ts -- AUTHOR-facing types + helpers for writing a Bounded live
|
|
@@ -7722,5 +7845,5 @@ function defineLiveModule(mod) {
|
|
|
7722
7845
|
return mod;
|
|
7723
7846
|
}
|
|
7724
7847
|
|
|
7725
|
-
export { EFFECT_INTENT_ADDRESS, FunctionInvokeError, InsufficientBalanceError, LiveIntentError, ReactNativeSessionManager, RealtimeStore, ServerSessionManager, WebSessionManager, aggregate, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithSignature, defineLiveModule, deriveUserIdentityFromIdToken, functions, genAuthNonce, genSolanaMessage, get, getActiveSessionManager, getCachedData, getConfig, getFiles, getIdToken, getMany, getRealtimeStore, getWebhookKeysUrl, hasActiveConnection, increment, init, invoke as invokeFunction, isEffectResult, live, intent as liveIntent, queryAggregate, reconnectWithNewAuth, refreshSession, resetRealtimeStore, revokeSession, runExpression, runExpressionMany, runQuery, runQueryMany, search, serverTimestamp, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe, subscribeView as subscribeLiveView, withEffects, wsDelete, wsGet, wsGetMany, wsQuery, wsSet };
|
|
7848
|
+
export { EFFECT_INTENT_ADDRESS, FunctionInvokeError, InsufficientBalanceError, LiveIntentError, ReactNativeSessionManager, RealtimeStore, ServerSessionManager, WebSessionManager, aggregate, buildSetDocumentsTransaction, clearCache, closeAllSubscriptions, convertRemainingAccounts, count, createSessionWithSignature, defineLiveModule, deriveUserIdentityFromIdToken, functions, genAuthNonce, genSolanaMessage, get, getActiveSessionManager, getCachedData, getConfig, getFiles, getIdToken, getMany, getRealtimeStore, getWebhookKeysUrl, hasActiveConnection, increment, init, invoke as invokeFunction, isEffectResult, live, intent as liveIntent, status as liveStatus, now, queryAggregate, reconnectWithNewAuth, refreshSession, resetRealtimeStore, revokeSession, runExpression, runExpressionMany, runQuery, runQueryMany, search, serverTimestamp, set, setFile, setMany, signAndSubmitTransaction, signMessage, signSessionCreateMessage, signTransaction, subscribe, subscribeView as subscribeLiveView, toMillis, toSeconds, withEffects, wsDelete, wsGet, wsGetMany, wsQuery, wsSet };
|
|
7726
7849
|
//# sourceMappingURL=index.mjs.map
|