@roll-agent/relay-client 0.0.0 → 0.1.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/README.md +103 -6
- package/dist/client.d.ts +80 -0
- package/dist/client.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/schemas.d.ts +1088 -0
- package/dist/schemas.js +1 -0
- package/dist/testing.d.ts +10 -0
- package/dist/testing.js +1 -0
- package/dist/transport.d.ts +31 -0
- package/dist/transport.js +1 -0
- package/package.json +38 -3
package/README.md
CHANGED
|
@@ -1,8 +1,105 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@roll-agent/relay-client`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Browser-safe client for connecting a Web application to a user's enrolled Roll Workspace through
|
|
4
|
+
the official Cloud Relay. It owns Control 1.0 and Relay Wire 1.1 framing, request correlation,
|
|
5
|
+
reconnection, ACK/gap recovery, and the Chat/Interaction reducer.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
The package does not contain React components, account management, a Relay admin client, or a raw
|
|
8
|
+
frame API.
|
|
9
|
+
|
|
10
|
+
The separate `roll-cloud-relay` repository must implement the Browser Session and WSS contracts
|
|
11
|
+
before this path can be used in production; its current implementation has not yet been updated to
|
|
12
|
+
these contracts.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @roll-agent/relay-client
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Your application backend must exchange its authenticated user for a short-lived, single-use Relay
|
|
21
|
+
Browser session. Relay credentials must never be returned to Browser code.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createRelayClient } from "@roll-agent/relay-client";
|
|
25
|
+
|
|
26
|
+
const client = createRelayClient({
|
|
27
|
+
getSession: async ({ signal }) => {
|
|
28
|
+
const response = await fetch("/api/roll/session", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
credentials: "include",
|
|
31
|
+
signal,
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Session request failed: ${response.status}`);
|
|
35
|
+
}
|
|
36
|
+
return response.json(); // Validated as { connectUrl: "wss://...", expiresAt: ISO timestamp }.
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await client.connect();
|
|
41
|
+
const thread = await client.createThread({ title: "Support task" });
|
|
42
|
+
|
|
43
|
+
const unsubscribe = thread.subscribe((view) => {
|
|
44
|
+
renderChat(view.snapshot, view.liveAssistantMessages, view.interactions);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await thread.send("Check this workspace and summarize the project.");
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`workspaceId`, Relay request IDs, Runtime mutation request IDs, Turn IDs, Wire frames, ACKs, and
|
|
51
|
+
session refresh are internal. A normal Web application only renders `RelayThreadView` and invokes
|
|
52
|
+
the typed methods.
|
|
53
|
+
|
|
54
|
+
## Public API
|
|
55
|
+
|
|
56
|
+
- `client.connect()` / `client.close()`
|
|
57
|
+
- `client.getConnectionState()` / `client.subscribeConnection(listener)`
|
|
58
|
+
- `client.listThreads()` / `client.createThread()` / `client.openThread(threadId)`
|
|
59
|
+
- `thread.getSnapshot()` / `thread.subscribe(listener)`
|
|
60
|
+
- `thread.send(text)` / `thread.cancel(turnId)`
|
|
61
|
+
- `thread.respond(interactionId, candidate)` / `thread.refresh()`
|
|
62
|
+
|
|
63
|
+
All requests support an optional `AbortSignal` and timeout. If a mutation was sent but its response
|
|
64
|
+
cannot be observed, the client rejects with a typed `RelayClientError` whose code is
|
|
65
|
+
`OUTCOME_UNKNOWN`. The application should refresh the Thread snapshot instead of automatically
|
|
66
|
+
creating a new mutation.
|
|
67
|
+
|
|
68
|
+
`createThread()` also protects this boundary: once `thread.create` succeeds, a failure to hydrate its
|
|
69
|
+
first snapshot does not reject the already-completed creation. It returns a Thread whose view is in
|
|
70
|
+
the `error` state; call `thread.refresh()` to converge without creating a second Thread.
|
|
71
|
+
|
|
72
|
+
## Recovery semantics
|
|
73
|
+
|
|
74
|
+
The client applies event and Interaction frames only in contiguous `relaySequence` order and emits
|
|
75
|
+
Wire 1.1 `runtime.ack` for the highest sequence already applied. A Relay gap, or a locally observed
|
|
76
|
+
sequence hole, triggers `thread.snapshot` recovery before the recovery boundary is acknowledged.
|
|
77
|
+
|
|
78
|
+
An in-flight mutation remains in memory across a same-page reconnect and is sent again with the
|
|
79
|
+
same Relay request ID and Runtime mutation request ID. Cloud Relay and Companion are responsible for
|
|
80
|
+
idempotent replay. A full page refresh intentionally loses this process-local replay state: the old
|
|
81
|
+
Promise no longer exists, the new client never guesses or replays that mutation, and the application
|
|
82
|
+
must reopen the Thread and converge from its snapshot. `OUTCOME_UNKNOWN` applies while the original
|
|
83
|
+
client instance is still alive but can no longer confirm a sent mutation's result.
|
|
84
|
+
|
|
85
|
+
Wire 1.1 snapshots do not contain a respondable `interactionId` for pending Approval or User Input.
|
|
86
|
+
The client never invents one from `pendingApprovals`. On a fresh Browser session, Cloud Relay must
|
|
87
|
+
re-deliver outstanding `interaction.request` frames; until that happens the snapshot is state-only
|
|
88
|
+
and cannot be remotely answered.
|
|
89
|
+
|
|
90
|
+
## Interaction lifecycle
|
|
91
|
+
|
|
92
|
+
`view.interactions` is a discriminated union with `pending`, `responding`, `resolved`, and
|
|
93
|
+
`cancelled` states. Pass the `interactionId` from a pending request back to `thread.respond()`. The
|
|
94
|
+
candidate is validated against the original Approval or User Input projection before it is sent.
|
|
95
|
+
|
|
96
|
+
Runtime policy remains authoritative: Runtime `deny` creates no remotely answerable Interaction,
|
|
97
|
+
while Runtime `confirm` produces an authenticated Web Interaction. A Host denial is exposed as the
|
|
98
|
+
stable remote code `REMOTE_REQUEST_DENIED`; local policy details are not copied into the Browser
|
|
99
|
+
error.
|
|
100
|
+
|
|
101
|
+
## Testing
|
|
102
|
+
|
|
103
|
+
`@roll-agent/relay-client/testing` exports `createRelayClientForTesting()` and the minimal WebSocket
|
|
104
|
+
and scheduler interfaces. Inject a deterministic transport without adding Node APIs to production
|
|
105
|
+
Browser code.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { relayRequestMethodSchemasV11, type RelayInteractionRequestV11, type RelayRequestParamsForVersion, type RelayRequestResultForVersion } from "@roll-agent/relay-protocol";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import { type RelayClientErrorDetails, type RelayConnectionState, type RelayThreadView } from "./schemas.ts";
|
|
4
|
+
import { type RelayClientRuntimeDependencies } from "./transport.ts";
|
|
5
|
+
type ThreadListWireInput = z.input<(typeof relayRequestMethodSchemasV11)["thread.list"]["params"]>;
|
|
6
|
+
type ThreadCreateWireInput = z.input<(typeof relayRequestMethodSchemasV11)["thread.create"]["params"]>;
|
|
7
|
+
type ThreadListWireResult = RelayRequestResultForVersion<"1.1", "thread.list">;
|
|
8
|
+
type ThreadOpenWireResult = RelayRequestResultForVersion<"1.1", "thread.open">;
|
|
9
|
+
type ThreadSnapshotWireResult = RelayRequestResultForVersion<"1.1", "thread.snapshot">;
|
|
10
|
+
type TurnStartWireResult = RelayRequestResultForVersion<"1.1", "turn.start">;
|
|
11
|
+
type TurnCancelWireResult = RelayRequestResultForVersion<"1.1", "turn.cancel">;
|
|
12
|
+
type InteractionCandidateWireResult = RelayRequestResultForVersion<"1.1", "interaction.candidate">;
|
|
13
|
+
export type RelayThreadId = ThreadOpenWireResult["thread"]["id"];
|
|
14
|
+
export type RelayTurnId = RelayRequestParamsForVersion<"1.1", "turn.cancel">["turnId"];
|
|
15
|
+
export type RelayInteractionId = RelayInteractionRequestV11["interactionId"];
|
|
16
|
+
export type RelayThreadSummary = ThreadListWireResult["items"][number];
|
|
17
|
+
export type RelayThreadListInput = Omit<ThreadListWireInput, "limit"> & {
|
|
18
|
+
readonly limit?: ThreadListWireInput["limit"];
|
|
19
|
+
};
|
|
20
|
+
export type RelayThreadCreateInput = Omit<ThreadCreateWireInput, "requestId">;
|
|
21
|
+
export type RelayThreadListResult = ThreadListWireResult;
|
|
22
|
+
export type RelayThreadSnapshot = ThreadSnapshotWireResult;
|
|
23
|
+
export type RelayTurnStartResult = TurnStartWireResult;
|
|
24
|
+
export type RelayTurnCancelResult = TurnCancelWireResult;
|
|
25
|
+
export type RelayInteractionCandidateResult = InteractionCandidateWireResult;
|
|
26
|
+
export interface RelaySessionProviderInput {
|
|
27
|
+
readonly signal: AbortSignal;
|
|
28
|
+
}
|
|
29
|
+
export type RelaySessionProvider = (input: RelaySessionProviderInput) => Promise<unknown>;
|
|
30
|
+
export interface RelayRequestOptions {
|
|
31
|
+
readonly signal?: AbortSignal;
|
|
32
|
+
readonly timeoutMs?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface CreateRelayClientOptions {
|
|
35
|
+
readonly getSession: RelaySessionProvider;
|
|
36
|
+
readonly requestTimeoutMs?: number;
|
|
37
|
+
}
|
|
38
|
+
export type RelayConnectionListener = (state: RelayConnectionState) => void;
|
|
39
|
+
export type RelayThreadListener = (view: RelayThreadView) => void;
|
|
40
|
+
export type RelayUnsubscribe = () => void;
|
|
41
|
+
export interface RelayThread {
|
|
42
|
+
readonly id: RelayThreadId;
|
|
43
|
+
getSnapshot(): RelayThreadView;
|
|
44
|
+
subscribe(listener: RelayThreadListener): RelayUnsubscribe;
|
|
45
|
+
send(text: string, options?: RelayRequestOptions): Promise<RelayTurnStartResult>;
|
|
46
|
+
cancel(turnId: RelayTurnId, options?: RelayRequestOptions): Promise<RelayTurnCancelResult>;
|
|
47
|
+
respond(interactionId: RelayInteractionId, candidate: unknown, options?: RelayRequestOptions): Promise<RelayInteractionCandidateResult>;
|
|
48
|
+
refresh(options?: RelayRequestOptions): Promise<RelayThreadSnapshot>;
|
|
49
|
+
}
|
|
50
|
+
export interface RelayClient {
|
|
51
|
+
connect(options?: RelayRequestOptions): Promise<void>;
|
|
52
|
+
close(): void;
|
|
53
|
+
getConnectionState(): RelayConnectionState;
|
|
54
|
+
subscribeConnection(listener: RelayConnectionListener): RelayUnsubscribe;
|
|
55
|
+
listThreads(input?: RelayThreadListInput, options?: RelayRequestOptions): Promise<RelayThreadListResult>;
|
|
56
|
+
createThread(input?: RelayThreadCreateInput, options?: RelayRequestOptions): Promise<RelayThread>;
|
|
57
|
+
openThread(threadId: RelayThreadId, options?: RelayRequestOptions): Promise<RelayThread>;
|
|
58
|
+
}
|
|
59
|
+
export declare class RelayClientError extends Error {
|
|
60
|
+
readonly details: RelayClientErrorDetails;
|
|
61
|
+
constructor(details: RelayClientErrorDetails);
|
|
62
|
+
}
|
|
63
|
+
export declare class RelayClientImpl implements RelayClient {
|
|
64
|
+
#private;
|
|
65
|
+
constructor(options: CreateRelayClientOptions, runtime: RelayClientRuntimeDependencies);
|
|
66
|
+
connect(options?: RelayRequestOptions): Promise<void>;
|
|
67
|
+
close(): void;
|
|
68
|
+
getConnectionState(): RelayConnectionState;
|
|
69
|
+
subscribeConnection(listener: RelayConnectionListener): RelayUnsubscribe;
|
|
70
|
+
listThreads(input?: RelayThreadListInput, options?: RelayRequestOptions): Promise<RelayThreadListResult>;
|
|
71
|
+
createThread(input?: RelayThreadCreateInput, options?: RelayRequestOptions): Promise<RelayThread>;
|
|
72
|
+
openThread(threadId: RelayThreadId, options?: RelayRequestOptions): Promise<RelayThread>;
|
|
73
|
+
startTurn(threadId: RelayThreadId, text: string, options?: RelayRequestOptions): Promise<RelayTurnStartResult>;
|
|
74
|
+
cancelTurn(threadId: RelayThreadId, turnId: RelayTurnId, options?: RelayRequestOptions): Promise<RelayTurnCancelResult>;
|
|
75
|
+
respondToInteraction(request: RelayInteractionRequestV11, candidate: unknown, options?: RelayRequestOptions): Promise<RelayInteractionCandidateResult>;
|
|
76
|
+
getThreadSnapshot(threadId: RelayThreadId, options?: RelayRequestOptions): Promise<RelayThreadSnapshot>;
|
|
77
|
+
}
|
|
78
|
+
export declare function createRelayClientWithRuntime(options: CreateRelayClientOptions, runtime: RelayClientRuntimeDependencies): RelayClient;
|
|
79
|
+
export declare function createRelayClient(options: CreateRelayClientOptions): RelayClient;
|
|
80
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{RELAY_ERROR_CODES_V11 as e,RELAY_MESSAGE_TYPES_V11 as t,RELAY_REQUEST_METHODS_V11 as s,RELAY_REQUEST_METHOD_DISPOSITIONS_V11 as n,parseRelayInteractionCandidateForRequestV11 as i,parseRelayRequestParamsForVersion as r,parseRelayRequestResultForVersion as o,relayAckSchemaV11 as a,relayMessageSchemaV11 as c,relayRequestIdSchema as l,relayRequestMethodSchemasV11 as d,relayRuntimeRequestSchemaV11 as h}from"@roll-agent/relay-protocol";import{parseRelayBrowserFirstControlFrame as u,relayBrowserControlMessageSchema as p,relaySessionDescriptorSchema as y}from"@roll-agent/relay-protocol/control";import{z as m}from"zod/v4";import{RELAY_CLIENT_TRANSPORT_ERROR_CODES as f,getPendingInteractionRequest as g,relayClientErrorDetailsSchema as v,relayConnectionStateSchema as w,relayThreadViewSchema as S}from"./schemas.js";import{browserRelayRuntimeDependencies as R}from"./transport.js";export class RelayClientError extends Error{details;constructor(e){super(e.message),this.name="RelayClientError",this.details=v.parse(e)}}const b=3e4,q=1,I=-1,k={normal:1e3,invalidFrame:4002,sessionFatal:4008,sessionRetryable:4012},C=new Set(["completed","cancelled","failed"]);function T(e,t,s){return new RelayClientError({kind:"transport",code:e,message:t,retryable:s})}function E(e){return e instanceof RelayClientError?e:T(f.transportError,e instanceof Error?e.message:"Relay transport failed",!0)}function M(e,t){try{e(t)}catch(e){if("function"==typeof globalThis.reportError)try{globalThis.reportError(e)}catch{}}}function P(e){if("string"!=typeof e)throw T(f.protocolError,"Relay sent a non-text frame",!1);try{return JSON.parse(e)}catch{throw T(f.protocolError,"Relay sent invalid JSON",!1)}}function A(e){try{return u(e)}catch{throw T(f.protocolError,"Relay first frame must be Control 1.0 session.ready for Wire 1.1",!1)}}function B(e){const t=e.activeTurn;return void 0===t?{status:"idle"}:"cancelling"===t.status?{status:"cancelling",turnId:t.id}:{status:"running",turnId:t.id}}function x(e,t){const s=e.findIndex(e=>e.streamId===t.streamId);return-1===s?[...e,t]:e.map((e,n)=>n===s?t:e)}function O(e,t){return{status:e,interactionId:t.interactionId,threadId:t.threadId,turnId:t.turnId,method:t.method}}class RelayThreadImpl{id;#e;#t=new Set;#s;constructor(e,t){this.#e=e,this.id=t,this.#s=S.parse({status:"loading",threadId:t,snapshot:null,liveAssistantMessages:[],interactions:[],turn:{status:"idle"}})}getSnapshot(){return this.#s}subscribe(e){return this.#t.add(e),M(e,this.#s),()=>{this.#t.delete(e)}}async send(e,t){const s=await this.#e.startTurn(this.id,e,t);return this.#n({status:"running",turnId:s.turnId}),s}async cancel(e,t){const s=await this.#e.cancelTurn(this.id,e,t);return s.cancelling&&this.#n({status:"cancelling",turnId:e}),s}async respond(e,t,s){const n=this.#s.interactions.find(t=>{const s=g(t);return s?.interactionId===e}),i=void 0===n?void 0:g(n);if(void 0===i)throw T(f.connectionUnavailable,"Interaction is no longer pending in this Browser session",!1);this.#i(e,"responding");try{return await this.#e.respondToInteraction(i,t,s)}catch(t){throw this.#i(e,"pending"),t}}async refresh(e){try{const t=await this.#e.getThreadSnapshot(this.id,e);return this.applySnapshot(t),t}catch(e){throw this.applyError(E(e).details),e}}applySnapshot(e){this.#r({status:"ready",threadId:this.id,snapshot:e,liveAssistantMessages:[],interactions:this.#s.interactions,turn:B(e)})}applyError(e){this.#r({status:"error",threadId:this.id,snapshot:this.#s.snapshot,liveAssistantMessages:this.#s.liveAssistantMessages,interactions:this.#s.interactions,turn:this.#s.turn,error:e})}applyRuntimeEvent(e){const t=e.event,s=t.event,n=t.turnId;let i=this.#s.liveAssistantMessages,r=this.#s.turn;if("message.started"===s.type)i=x(i,{status:"streaming",streamId:s.streamId,text:""});else if("message.delta"===s.type){const e=i.find(e=>e.streamId===s.streamId);i=x(i,{status:"streaming",streamId:s.streamId,text:`${e?.text??""}${s.delta}`})}else"message.completed"===s.type?i=x(i,{status:"completed",streamId:s.streamId,text:s.text}):"turn.started"===s.type&&void 0!==n?r={status:"running",turnId:n}:"turn.completed"===s.type&&void 0!==n?r={status:"completed",turnId:n}:"turn.cancelled"===s.type&&void 0!==n?r={status:"cancelled",turnId:n}:"turn.failed"===s.type&&void 0!==n&&(r={status:"failed",turnId:n,stage:s.stage});this.#o({liveAssistantMessages:i,turn:r}),"turn.completed"!==s.type&&"turn.cancelled"!==s.type&&"turn.failed"!==s.type||this.refresh().catch(()=>{})}applyInteractionRequest(e){const t=this.#s.interactions.findIndex(t=>{const s=g(t);return s?.interactionId===e.interactionId}),s={status:"pending",request:e},n=-1===t?[...this.#s.interactions,s]:this.#s.interactions.map((e,n)=>n===t?s:e);this.#o({interactions:n})}applyInteractionTerminal(e){const s=O(e.type===t.interactionResolved?"resolved":"cancelled",e),n=this.#s.interactions.findIndex(t=>{const s=g(t);return s?.interactionId===e.interactionId}),i=-1===n?[...this.#s.interactions,s]:this.#s.interactions.map((e,t)=>t===n?s:e);this.#o({interactions:i})}#n(e){const t=this.#s.turn;"idle"!==t.status&&t.turnId===e.turnId&&C.has(t.status)||this.#o({turn:e})}#i(e,t){const s=this.#s.interactions.map(s=>{const n=g(s);return n?.interactionId===e?{status:t,request:n}:s});this.#o({interactions:s})}#o(e){const t={threadId:this.id,snapshot:this.#s.snapshot,liveAssistantMessages:e.liveAssistantMessages??this.#s.liveAssistantMessages,interactions:e.interactions??this.#s.interactions,turn:e.turn??this.#s.turn};this.#r(null===this.#s.snapshot?{status:"loading",...t,snapshot:null}:{status:"ready",...t,snapshot:this.#s.snapshot})}#r(e){this.#s=S.parse(e);for(const e of this.#t)M(e,this.#s)}}export class RelayClientImpl{#a;#c;#l=new Set;#d=new Map;#h=new Map;#u=new Map;#p={status:"idle"};#y;#m;#f;#g;#v;#w=0;#S=!1;#R=!1;#b=I;#q=!1;#I=0;#k=!1;#C;constructor(e,t){if(this.#a=e,this.#c=t,this.#C=e.requestTimeoutMs??b,!Number.isFinite(this.#C)||this.#C<=0)throw new TypeError("requestTimeoutMs must be a positive finite number")}async connect(e){if("connected"===this.#p.status)return;if(this.#S)throw T(f.closed,"Relay client is closed",!1);this.#T();const t=this.#f;return void 0!==t?t:(this.#R=!1,this.#w=0,this.#E({status:"connecting",attempt:1}),this.#M(e?.signal,e?.timeoutMs).catch(e=>{const t=E(e);throw this.#S||this.#R||this.#E({status:"closed",reason:"session"===t.details.kind?"session":"transport"}),t}))}close(){this.#S||(this.#S=!0,this.#R=!0,this.#g?.abort(),this.#T(),this.#P("client closed"),this.#A(!0),this.#E({status:"closed",reason:"client"}))}getConnectionState(){return this.#p}subscribeConnection(e){return this.#l.add(e),M(e,this.#p),()=>{this.#l.delete(e)}}async listThreads(e={},t){return this.#B(s.threadList,e,t)}async createThread(e={},t){const n=await this.#B(s.threadCreate,{...e,requestId:this.#c.createUuid()},t),i=this.#x(n.thread.id);return await i.refresh(t).catch(()=>{}),i}async openThread(e,t){const n=await this.#B(s.threadOpen,{threadId:e},t),i=this.#x(e);return i.applySnapshot(n),i}async startTurn(e,t,n){return this.#B(s.turnStart,{requestId:this.#c.createUuid(),threadId:e,turnId:this.#c.createUuid(),input:{text:t}},n)}async cancelTurn(e,t,n){return this.#B(s.turnCancel,{requestId:this.#c.createUuid(),threadId:e,turnId:t},n)}async respondToInteraction(e,t,n){const r=i(e,t);return this.#B(s.interactionCandidate,r,n)}async getThreadSnapshot(e,t){return this.#B(s.threadSnapshot,{threadId:e,limit:100},t)}async#O(e,t){const s=t??this.#C;if(!Number.isFinite(s)||s<=0)throw new TypeError("timeoutMs must be a positive finite number");const n=new AbortController;this.#g=n;let i=!1;const r=()=>{n.abort()};e?.addEventListener("abort",r,{once:!0}),!0===e?.aborted&&n.abort();const o=this.#c.scheduler.setTimer(()=>{i=!0,n.abort()},s),a=new Promise((e,t)=>{const s=()=>{t(T(i?f.requestTimeout:f.aborted,i?"Relay session connection timed out":"Relay session request was aborted",i)),this.#y?.close(k.normal,"session attempt cancelled")};n.signal.aborted?s():n.signal.addEventListener("abort",s,{once:!0})});try{await Promise.race([this.#W(n.signal),a])}finally{this.#c.scheduler.clearTimer(o),e?.removeEventListener("abort",r),this.#g===n&&(this.#g=void 0)}}async#W(e){try{let t;try{t=await this.#a.getSession({signal:e})}catch(t){if(e.aborted)throw T(f.aborted,"Relay session request was aborted",!1);throw E(t)}if(e.aborted)throw T(f.aborted,"Relay session request was aborted",!1);const s=y.safeParse(t);if(!s.success)throw T(f.invalidSession,"Session endpoint returned an invalid Relay session",!1);if(Date.parse(s.data.expiresAt)<=this.#c.scheduler.now())throw T(f.invalidSession,"Session endpoint returned an expired Relay session",!1);this.#P("superseded relay session");const n=this.#c.createWebSocket(s.data.connectUrl);this.#y=n,await this.#F(n)}catch(e){throw E(e)}}#F(e){return new Promise((t,s)=>{const n={ready:!1,settled:!1},i=e=>{n.settled||(n.settled=!0,s(e))};e.setHandlers({onOpen:()=>{},onMessage:s=>{if(!n.ready||this.#y===e)try{const e=P(s);if(!n.ready){const s=A(e);if(void 0!==this.#m&&this.#m!==s.workspaceId)throw T(f.protocolError,"Relay session changed the bound Workspace",!1);return this.#m=s.workspaceId,n.ready=!0,n.settled=!0,this.#w=0,this.#k=!0,this.#E({status:"connected",workspaceStatus:s.workspaceStatus}),this.#j(),"online"===s.workspaceStatus&&this.#L(),void t()}this.#U(e)}catch(t){const s=E(t);n.ready?this.#z(s):i(s),e.close(k.invalidFrame,"invalid relay frame")}},onClose:()=>{const t=this.#y===e;t&&(this.#y=void 0),n.ready?t&&this.#N():i(T(f.transportError,"Relay connection closed before session.ready",!0))},onError:()=>{n.ready||i(T(f.transportError,"Relay WebSocket failed before session.ready",!0))}})})}#U(e){const t=p.safeParse(e);if(t.success)return void this.#G(t.data);const s=c.safeParse(e);if(!s.success)throw T(f.protocolError,"Relay sent an invalid Control 1.0 or Wire 1.1 frame",!1);this.#H(s.data)}#G(e){if("session.ready"===e.type)throw T(f.protocolError,"session.ready may only be the first frame",!1);if("workspace.status"===e.type){this.#$(e.workspaceId);const t=this.#p,s="connected"===t.status&&"offline"===t.workspaceStatus&&"online"===e.status;return this.#E({status:"connected",workspaceStatus:e.status}),s&&(this.#k=!0,this.#D()),void("online"===e.status&&this.#L())}const t=new RelayClientError({kind:"session",code:e.code,message:`Relay session failed: ${e.code}`,retryable:e.retryable});if(e.retryable)this.#y?.close(k.sessionRetryable,"retryable relay session error");else{this.#R=!0,this.#A(!0);for(const e of this.#d.values())e.applyError(t.details);this.#E({status:"closed",reason:"session"}),this.#y?.close(k.sessionFatal,"non-retryable relay session error")}}#H(e){if(e.type!==t.runtimeResponse){if(e.type===t.runtimeGap)return this.#$(e.workspaceId),this.#J(e.throughRelaySequence),void this.#Q(e.throughRelaySequence);if(e.type!==t.runtimeEvent&&e.type!==t.interactionRequest&&e.type!==t.interactionResolved&&e.type!==t.interactionCancelled)throw T(f.protocolError,`Relay sent disallowed Browser inbound frame: ${e.type}`,!1);this.#V({relaySequence:e.relaySequence,message:e})}else this.#K(e)}#K(t){this.#$(t.workspaceId);const s=this.#h.get(t.requestId);if(void 0!==s){if(void 0!==t.error){const n=t.error.code===e.remoteRequestDenied?"Remote request denied":t.error.message;return void this.#X(s,new RelayClientError({kind:"remote",code:t.error.code,message:n,retryable:t.error.retryable}))}if(void 0!==t.result)try{const e=o("1.1",s.method,t.result);this.#X(s,void 0,e)}catch{this.#X(s,T(f.protocolError,`Relay returned an invalid ${s.method} result`,!1))}else this.#X(s,T(f.protocolError,"runtime.response contains neither result nor error",!1))}}#V(e){if(this.#$(e.message.workspaceId),this.#J(e.relaySequence),e.relaySequence<=this.#b)this.#j();else if(this.#q)this.#u.set(e.relaySequence,e);else{if(e.relaySequence!==this.#b+1)return this.#u.set(e.relaySequence,e),void this.#Q(e.relaySequence-1);this.#Y(e.message),this.#b=e.relaySequence,this.#j()}}#Y(e){const s=e.type===t.runtimeEvent?e.event.threadId:e.threadId,n=this.#x(s);e.type===t.runtimeEvent?n.applyRuntimeEvent(e):e.type===t.interactionRequest?n.applyInteractionRequest(e):n.applyInteractionTerminal(e)}#Q(e){if(e<=this.#b)return void this.#j();if(this.#q)return;this.#q=!0;const t=e,s=this.#I,n=[...this.#d.values()].map(async e=>e.refresh());Promise.all(n).then(()=>{if(this.#I===s){this.#b=t;for(const e of[...this.#u.keys()])e<=t&&this.#u.delete(e);this.#q=!1,this.#j(),this.#Z()}}).catch(()=>{this.#I===s&&(this.#q=!1,this.#y?.close(k.sessionRetryable,"snapshot recovery failed"))})}#J(e){this.#k&&(this.#k=!1,e>this.#b||(this.#I+=1,this.#b=I,this.#u.clear(),this.#q=!1,this.#D()))}#D(){for(const e of this.#d.values())e.refresh().catch(()=>{})}#Z(){for(;!this.#q;){const e=this.#b+1,t=this.#u.get(e);if(void 0===t){const t=[...this.#u.keys()].sort((e,t)=>e-t)[0];return void(void 0!==t&&t>e&&this.#Q(t-1))}this.#u.delete(e),this.#Y(t.message),this.#b=e,this.#j()}}#j(){const e=this.#m,s=this.#y;if(void 0===e||1!==s?.readyState)return;const n=a.parse({type:t.runtimeAck,workspaceId:e,throughRelaySequence:this.#b});s.send(JSON.stringify(n))}#N(){this.#S||this.#R||(this.#_(),this.#ee())}#ee(){if(void 0!==this.#v||this.#S||this.#R)return;this.#w+=1;const e=this.#c.reconnectDelayMs(this.#w),t=new Date(this.#c.scheduler.now()+e).toISOString();this.#E({status:"reconnecting",attempt:this.#w,retryAt:t}),this.#v=this.#c.scheduler.setTimer(()=>{this.#v=void 0,this.#S||this.#R||void 0!==this.#f||"connected"===this.#p.status||this.#M().catch(e=>{if(this.#S)return;if(!E(e).details.retryable)return this.#R=!0,this.#A(!0),void this.#E({status:"closed",reason:"session"});this.#ee()})},e)}#M(e,t){const s=this.#O(e,t).finally(()=>{this.#f===s&&(this.#f=void 0)});return this.#f=s,s}#P(e){const t=this.#y;void 0!==t&&(this.#y=void 0,t.close(k.normal,e))}#T(){void 0!==this.#v&&(this.#c.scheduler.clearTimer(this.#v),this.#v=void 0)}#L(){for(const e of this.#h.values())e.mutation&&this.#te(e)}#B(e,s,i){if("connected"!==this.#p.status||"online"!==this.#p.workspaceStatus||void 0===this.#m){const e="connected"===this.#p.status&&"offline"===this.#p.workspaceStatus;return Promise.reject(T(e?f.workspaceOffline:f.connectionUnavailable,e?"The local Workspace is offline":"Relay is not connected",!0))}if(!0===i?.signal?.aborted)return Promise.reject(T(f.aborted,"Relay request was aborted",!1));const o=r("1.1",e,s),a=l.parse(this.#c.createUuid()),c=h.parse({type:t.runtimeRequest,requestId:a,workspaceId:this.#m,method:e,params:o}),d=i?.timeoutMs??this.#C;if(!Number.isFinite(d)||d<=0)return Promise.reject(new TypeError("timeoutMs must be a positive finite number"));const u="mutation"===n[e];return new Promise((t,s)=>{const n=i?.signal?()=>{const e=this.#h.get(a);if(void 0===e)return;const t=e.mutation&&e.sent;this.#X(e,T(t?f.outcomeUnknown:f.aborted,t?"Mutation outcome is unknown after Browser cancellation":"Relay request was aborted",!1))}:void 0,r={requestId:a,method:e,frame:JSON.stringify(c),mutation:u,sent:!1,resolve:e=>{t(e)},reject:s,signal:i?.signal,abortListener:n,timeoutHandle:void 0};r.timeoutHandle=this.#c.scheduler.setTimer(()=>{const e=this.#h.get(a);if(void 0===e)return;const t=e.mutation&&e.sent;this.#X(e,T(t?f.outcomeUnknown:f.requestTimeout,t?"Mutation outcome is unknown after Relay timeout":"Relay request timed out",!t))},d),i?.signal?.addEventListener("abort",n??(()=>{}),{once:!0}),this.#h.set(a,r),this.#te(r)})}#te(e){const t=this.#y;1===t?.readyState&&"connected"===this.#p.status&&"online"===this.#p.workspaceStatus&&(t.send(e.frame),e.sent=!0)}#X(e,t,s){this.#h.delete(e.requestId)&&(void 0!==e.timeoutHandle&&this.#c.scheduler.clearTimer(e.timeoutHandle),void 0!==e.signal&&void 0!==e.abortListener&&e.signal.removeEventListener("abort",e.abortListener),void 0!==t?e.reject(t):e.resolve(s))}#_(){for(const e of[...this.#h.values()])e.mutation||this.#X(e,T(f.transportError,"Relay disconnected before the query completed",!0))}#A(e){for(const t of[...this.#h.values()]){const s=e&&t.mutation&&t.sent;this.#X(t,T(s?f.outcomeUnknown:f.closed,s?"Mutation outcome is unknown after Relay closed":"Relay client is closed",!1))}}#$(e){if(this.#m!==e)throw T(f.protocolError,"Relay frame does not match the bound Workspace",!1)}#x(e){const t=this.#d.get(e);if(void 0!==t)return t;const s=new RelayThreadImpl(this,e);return this.#d.set(e,s),s}#E(e){this.#p=w.parse(e);for(const e of this.#l)M(e,this.#p)}#z(e){this.#R=!0,this.#A(!0);for(const t of this.#d.values())t.applyError(e.details);this.#E({status:"closed",reason:"protocol"})}}export function createRelayClientWithRuntime(e,t){return new RelayClientImpl(e,t)}export function createRelayClient(e){return createRelayClientWithRuntime(e,R)}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { RelayClientError, createRelayClient, type CreateRelayClientOptions, type RelayClient, type RelayConnectionListener, type RelayInteractionCandidateResult, type RelayInteractionId, type RelayRequestOptions, type RelaySessionProvider, type RelaySessionProviderInput, type RelayThread, type RelayThreadCreateInput, type RelayThreadId, type RelayThreadListInput, type RelayThreadListResult, type RelayThreadListener, type RelayThreadSnapshot, type RelayThreadSummary, type RelayTurnCancelResult, type RelayTurnId, type RelayTurnStartResult, type RelayUnsubscribe, } from "./client.ts";
|
|
2
|
+
export { RELAY_CLIENT_TRANSPORT_ERROR_CODES, RELAY_CONNECTION_STATUSES, getPendingInteractionRequest, relayClientErrorDetailsSchema, relayClientTransportErrorCodeSchema, relayConnectionStateSchema, relayInteractionStateSchema, relayLiveAssistantMessageSchema, relayThreadViewSchema, relayTurnStateSchema, type RelayClientErrorDetails, type RelayClientTransportErrorCode, type RelayConnectionState, type RelayInteractionState, type RelayLiveAssistantMessage, type RelayThreadView, type RelayTurnState, } from "./schemas.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{RelayClientError,createRelayClient}from"./client.js";export{RELAY_CLIENT_TRANSPORT_ERROR_CODES,RELAY_CONNECTION_STATUSES,getPendingInteractionRequest,relayClientErrorDetailsSchema,relayClientTransportErrorCodeSchema,relayConnectionStateSchema,relayInteractionStateSchema,relayLiveAssistantMessageSchema,relayThreadViewSchema,relayTurnStateSchema}from"./schemas.js";
|