@ggui-ai/iframe-runtime 0.27.0 → 0.29.0
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/credential-controller.d.ts +25 -4
- package/dist/credential-controller.js +45 -15
- package/dist/events-polling.js +3 -2
- package/dist/iframe-runtime.js +125 -126
- package/dist/iframe-runtime.meta.json +1 -1
- package/dist/ladders.d.ts +1 -3
- package/dist/ladders.js +23 -13
- package/dist/react-renderer.d.ts +3 -2
- package/dist/react-renderer.js +7 -2
- package/dist/render-item.d.ts +45 -4
- package/dist/render-item.js +5 -44
- package/dist/runtime.d.ts +23 -0
- package/dist/runtime.js +18 -7
- package/dist/shims/components.js +1 -1
- package/dist/shims/compositions.js +1 -1
- package/dist/shims/design.js +1 -1
- package/dist/shims/primitives.js +1 -1
- package/dist/shims/templates.js +1 -1
- package/dist/zod-jitless.js +1 -1
- package/package.json +10 -10
|
@@ -36,11 +36,21 @@ export interface CredentialControllerOptions {
|
|
|
36
36
|
readonly sleep?: (ms: number) => Promise<void>;
|
|
37
37
|
/** Test seam: defaults to `Math.random`. */
|
|
38
38
|
readonly random?: () => number;
|
|
39
|
+
/** Test seam: the view's clock, in ms; defaults to `Date.now`. */
|
|
40
|
+
readonly now?: () => number;
|
|
39
41
|
}
|
|
40
42
|
export interface CredentialController {
|
|
41
43
|
current(): HeldCredential;
|
|
42
44
|
/** The credential was accepted on a token rung (a WS or SSE ack, or a polling `ok`). */
|
|
43
45
|
markAccepted(credential: HeldCredential): void;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a bridge pull should report `credential` expired now (ggui#1734):
|
|
48
|
+
* it is the current credential, its `expiresAt` has passed by the view's
|
|
49
|
+
* clock, and {@link BRIDGE_REFRESH_INTERVAL_MS} has passed since the last
|
|
50
|
+
* refresh attempt on any rung (or there was none). A credential with no
|
|
51
|
+
* `expiresAt` is never due.
|
|
52
|
+
*/
|
|
53
|
+
dueOnBridge(credential: HeldCredential): boolean;
|
|
44
54
|
/**
|
|
45
55
|
* `credential` is the one the reporting ladder was built with. `retry`
|
|
46
56
|
* re-sends the SAME request after a relay error, inside the one budgeted
|
|
@@ -60,12 +70,23 @@ export interface RefreshRetry {
|
|
|
60
70
|
export declare const REFRESH_JITTER_MAX_MS = 5000;
|
|
61
71
|
/**
|
|
62
72
|
* The boot refresh of a live-only slice is the view's only way to a live
|
|
63
|
-
* channel, so a
|
|
64
|
-
*
|
|
65
|
-
*
|
|
73
|
+
* channel, and so is a bridge-rung refresh (the ladder has left every token
|
|
74
|
+
* rung), so a relay error on either (the host's relay failed, or the
|
|
75
|
+
* server's store read threw) is re-sent this many times inside the one
|
|
76
|
+
* attempt — before the boot reports `EXPIRED_BOOTSTRAP`, or before the bridge
|
|
77
|
+
* waits out its next interval — each after a uniform random wait between
|
|
66
78
|
* these bounds.
|
|
67
79
|
*/
|
|
68
|
-
export declare const
|
|
80
|
+
export declare const REFRESH_RELAY_ERROR_RETRIES = 2;
|
|
69
81
|
export declare const REFRESH_RETRY_DELAY_MIN_MS = 1000;
|
|
70
82
|
export declare const REFRESH_RETRY_DELAY_MAX_MS = 3000;
|
|
83
|
+
/**
|
|
84
|
+
* The bridge rung's attempt clock (ggui#1734): from the bridge, a view asks
|
|
85
|
+
* for a refresh at most once per this span, counted from its last attempt on
|
|
86
|
+
* any rung and whatever that attempt answered. It is the server's default
|
|
87
|
+
* credential lifetime, the one number both readers share; a server that
|
|
88
|
+
* mints longer-lived credentials slows the heartbeat through `expiresAt`, a
|
|
89
|
+
* shorter-lived one does not speed it past this floor.
|
|
90
|
+
*/
|
|
91
|
+
export declare const BRIDGE_REFRESH_INTERVAL_MS: number;
|
|
71
92
|
export declare function createCredentialController(opts: CredentialControllerOptions): CredentialController;
|
|
@@ -10,20 +10,30 @@
|
|
|
10
10
|
* the credential and the budget; the runtime owns the ladders and tells it
|
|
11
11
|
* when a credential expired or was accepted.
|
|
12
12
|
*
|
|
13
|
-
* The budget is one predicate:
|
|
14
|
-
* - a
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* A credential is marked spent when its refresh is REQUESTED, so
|
|
19
|
-
* that fire together
|
|
20
|
-
*
|
|
13
|
+
* The budget is one predicate per rung class:
|
|
14
|
+
* - On a token rung (a WS refusal, the polling 410, the boot): a credential
|
|
15
|
+
* that came back from a refresh and has NOT been accepted on a token rung
|
|
16
|
+
* is never refreshed again, whether it was refused or left the token
|
|
17
|
+
* rungs (the loop guard); any other expired credential gets exactly one
|
|
18
|
+
* refresh. A credential is marked spent when its refresh is REQUESTED, so
|
|
19
|
+
* triggers that fire together produce one request.
|
|
20
|
+
* - On the bridge rung (ggui#1734): no rung carries the credential there, so
|
|
21
|
+
* only the view's own clock can say it expired, and a relaying host's pull
|
|
22
|
+
* circuit half-opens only on a refresh the session grants. So the view
|
|
23
|
+
* asks again from the bridge whenever the current credential's `expiresAt`
|
|
24
|
+
* has passed and at least {@link BRIDGE_REFRESH_INTERVAL_MS} has passed
|
|
25
|
+
* since its last attempt on ANY rung — whatever that attempt answered,
|
|
26
|
+
* because the attempt that fails is the one inside the outage that opened
|
|
27
|
+
* the circuit. The interval is the storm bound, and it is the floor that
|
|
28
|
+
* keeps a view whose clock runs ahead of the server's from reading every
|
|
29
|
+
* fresh credential as expired.
|
|
21
30
|
*
|
|
22
31
|
* Expiry is always reported by the caller, from the slice's `expiresAt` or
|
|
23
32
|
* from a refusal signal, never derived from a token's issue time: a
|
|
24
33
|
* chained token can legitimately live less than a full TTL.
|
|
25
34
|
*/
|
|
26
35
|
import { withWsToken } from "@ggui-ai/protocol/integrations/mcp-apps";
|
|
36
|
+
import { DEFAULT_WS_TOKEN_TTL_SEC } from "@ggui-ai/protocol/transport/websocket";
|
|
27
37
|
import { unwrapCallToolResult } from "./call-tool-unwrap.js";
|
|
28
38
|
import { domainErrorCodeOf, isErrorToolResult, toolResultText } from "./tool-result-error.js";
|
|
29
39
|
/**
|
|
@@ -34,23 +44,41 @@ import { domainErrorCodeOf, isErrorToolResult, toolResultText } from "./tool-res
|
|
|
34
44
|
export const REFRESH_JITTER_MAX_MS = 5_000;
|
|
35
45
|
/**
|
|
36
46
|
* The boot refresh of a live-only slice is the view's only way to a live
|
|
37
|
-
* channel, so a
|
|
38
|
-
*
|
|
39
|
-
*
|
|
47
|
+
* channel, and so is a bridge-rung refresh (the ladder has left every token
|
|
48
|
+
* rung), so a relay error on either (the host's relay failed, or the
|
|
49
|
+
* server's store read threw) is re-sent this many times inside the one
|
|
50
|
+
* attempt — before the boot reports `EXPIRED_BOOTSTRAP`, or before the bridge
|
|
51
|
+
* waits out its next interval — each after a uniform random wait between
|
|
40
52
|
* these bounds.
|
|
41
53
|
*/
|
|
42
|
-
export const
|
|
54
|
+
export const REFRESH_RELAY_ERROR_RETRIES = 2;
|
|
43
55
|
export const REFRESH_RETRY_DELAY_MIN_MS = 1_000;
|
|
44
56
|
export const REFRESH_RETRY_DELAY_MAX_MS = 3_000;
|
|
57
|
+
/**
|
|
58
|
+
* The bridge rung's attempt clock (ggui#1734): from the bridge, a view asks
|
|
59
|
+
* for a refresh at most once per this span, counted from its last attempt on
|
|
60
|
+
* any rung and whatever that attempt answered. It is the server's default
|
|
61
|
+
* credential lifetime, the one number both readers share; a server that
|
|
62
|
+
* mints longer-lived credentials slows the heartbeat through `expiresAt`, a
|
|
63
|
+
* shorter-lived one does not speed it past this floor.
|
|
64
|
+
*/
|
|
65
|
+
export const BRIDGE_REFRESH_INTERVAL_MS = DEFAULT_WS_TOKEN_TTL_SEC * 1_000;
|
|
45
66
|
const REFRESH_TOOL = "ggui_runtime_refresh_ws_token";
|
|
46
67
|
export function createCredentialController(opts) {
|
|
47
68
|
const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
48
69
|
const random = opts.random ?? Math.random;
|
|
70
|
+
const now = opts.now ?? Date.now;
|
|
49
71
|
let current = opts.initial;
|
|
50
72
|
const spent = new Set();
|
|
51
73
|
const accepted = new Set();
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
/** The view's clock at the last refresh request, on any rung. */
|
|
75
|
+
let lastAttemptAt;
|
|
76
|
+
const expired = (credential) => credential.expiresAt !== undefined && Date.parse(credential.expiresAt) <= now();
|
|
77
|
+
const intervalPassed = () => lastAttemptAt === undefined || now() - lastAttemptAt >= BRIDGE_REFRESH_INTERVAL_MS;
|
|
78
|
+
const mayRefresh = (credential, source) => source === "bridge"
|
|
79
|
+
? expired(credential) && intervalPassed()
|
|
80
|
+
: !spent.has(credential.wsToken) &&
|
|
81
|
+
!(credential.origin === "refreshed" && !accepted.has(credential.wsToken));
|
|
54
82
|
const requestRefresh = async (credential) => {
|
|
55
83
|
let result;
|
|
56
84
|
try {
|
|
@@ -66,12 +94,14 @@ export function createCredentialController(opts) {
|
|
|
66
94
|
markAccepted(credential) {
|
|
67
95
|
accepted.add(credential.wsToken);
|
|
68
96
|
},
|
|
97
|
+
dueOnBridge: (credential) => credential === current && mayRefresh(credential, "bridge"),
|
|
69
98
|
async onExpired(credential, source, retry) {
|
|
70
99
|
if (credential !== current)
|
|
71
100
|
return { kind: "skipped", reason: "stale-ladder" };
|
|
72
|
-
if (!mayRefresh(credential))
|
|
101
|
+
if (!mayRefresh(credential, source))
|
|
73
102
|
return { kind: "skipped", reason: "budget" };
|
|
74
103
|
spent.add(credential.wsToken);
|
|
104
|
+
lastAttemptAt = now();
|
|
75
105
|
if (source !== "boot")
|
|
76
106
|
await sleep(Math.floor(random() * REFRESH_JITTER_MAX_MS));
|
|
77
107
|
let outcome = await requestRefresh(credential);
|
package/dist/events-polling.js
CHANGED
|
@@ -231,8 +231,9 @@ export function buildBridgePolling(opts) {
|
|
|
231
231
|
// holds the card is presumed quiet; drop to sparse un-held
|
|
232
232
|
// pulls every `idleIntervalMs` so a dormant card doesn't pin
|
|
233
233
|
// the host's relay open forever.
|
|
234
|
-
// - A FAILED pull (the
|
|
235
|
-
//
|
|
234
|
+
// - A FAILED pull (the view's own request timeout — the MCP SDK's
|
|
235
|
+
// 60 s default on `callServerTool`, answered as `-32001` — or a
|
|
236
|
+
// transport error) is not a live session either (ggui#1029):
|
|
236
237
|
// it counts toward demotion exactly like an empty hold, and
|
|
237
238
|
// repeated failures back off geometrically from `intervalMs`
|
|
238
239
|
// up to `idleIntervalMs` — a relay whose call timeout is shorter
|