castle-web-sdk 0.4.34 → 0.4.35
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 +131 -0
- package/dist/agent.d.ts +6 -0
- package/dist/agent.js +70 -0
- package/dist/castle.d.ts +4 -0
- package/dist/castle.js +2 -0
- package/dist/commands.d.ts +106 -0
- package/dist/commands.js +21 -0
- package/dist/decks.d.ts +7 -0
- package/dist/decks.js +31 -0
- package/dist/runtime.d.ts +4 -2
- package/dist/runtime.js +51 -40
- package/dist/server/storage.d.ts +44 -0
- package/dist/server/storage.js +54 -0
- package/dist/transport.d.ts +5 -1
- package/dist/transport.js +106 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,9 @@ import { setup, initCard, Store, Leaderboard } from "castle-web-sdk";
|
|
|
24
24
|
- [Pass](#pass)
|
|
25
25
|
- [Snapshot](#snapshot)
|
|
26
26
|
- [Portal](#portal)
|
|
27
|
+
- [Decks](#decks)
|
|
27
28
|
- [Haptics](#haptics)
|
|
29
|
+
- [Agent](#agent)
|
|
28
30
|
- [Lifecycle](#lifecycle)
|
|
29
31
|
- [Multiplayer](#multiplayer)
|
|
30
32
|
- [Setup](#setup)
|
|
@@ -616,6 +618,38 @@ if (info.status === "ok") {
|
|
|
616
618
|
|
|
617
619
|
Answers are kept per deck for the session, so asking again is cheap.
|
|
618
620
|
|
|
621
|
+
## Decks
|
|
622
|
+
|
|
623
|
+
`Decks` lists decks the player can point a portal at: their own published
|
|
624
|
+
decks, and the decks they saved. A deck can offer these as a picker rather
|
|
625
|
+
than asking for a pasted link.
|
|
626
|
+
Decks is limited to Castle admins for now; for every other player both calls
|
|
627
|
+
reject with a `PERMISSION_DENIED` error.
|
|
628
|
+
|
|
629
|
+
### `Decks.mine(options?): Promise<DecksListResult>`
|
|
630
|
+
|
|
631
|
+
The player's own decks, newest first, leaving out private ones.
|
|
632
|
+
|
|
633
|
+
### `Decks.saved(options?): Promise<DecksListResult>`
|
|
634
|
+
|
|
635
|
+
The decks the player bookmarked, then the decks in their playlists, each
|
|
636
|
+
once.
|
|
637
|
+
|
|
638
|
+
Both take `{ limit }`, from 1 through 50 (default 20), and both leave out
|
|
639
|
+
decks whose creator turned portals off. The result is
|
|
640
|
+
`{ status: 'ok', decks }`, where each deck is
|
|
641
|
+
`{ deckId, title, creator: { userId, username } | null, coverUrl }` — the
|
|
642
|
+
same shape as `Portal.info`. `coverUrl` is a `data:` url, since a deck may
|
|
643
|
+
not load remote images; it is null when the deck has no cover. A player
|
|
644
|
+
who is not signed in gets an empty list.
|
|
645
|
+
|
|
646
|
+
```js
|
|
647
|
+
const { decks } = await Decks.mine({ limit: 12 });
|
|
648
|
+
for (const deck of decks) {
|
|
649
|
+
addPickerTile(deck.title, deck.coverUrl, () => placePortal(deck.deckId));
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
619
653
|
## Haptics
|
|
620
654
|
|
|
621
655
|
`Haptics` plays a short device vibration ("buzz") for tactile feedback —
|
|
@@ -651,6 +685,103 @@ Haptics respect the player's settings — if a player has muted haptics,
|
|
|
651
685
|
`play` does nothing and resolves `'unavailable'`. Rapid repeated calls may
|
|
652
686
|
be coalesced, so it's safe to call on frequent events.
|
|
653
687
|
|
|
688
|
+
## Agent
|
|
689
|
+
|
|
690
|
+
`Agent` asks a language model for one chat completion. The host sends it
|
|
691
|
+
through Castle's oracle on the player's own Castle account, so the deck
|
|
692
|
+
holds no key. Agent.chat is limited to Castle admins for now; every other
|
|
693
|
+
player gets `'refused'`.
|
|
694
|
+
|
|
695
|
+
### `Agent.chat(params): Promise<AgentChatResult>`
|
|
696
|
+
|
|
697
|
+
`params`:
|
|
698
|
+
|
|
699
|
+
- `messages` — the conversation, in the OpenAI chat format
|
|
700
|
+
(`{ role, content }`, plus `tool_calls` on an assistant message and
|
|
701
|
+
`tool_call_id` on a `'tool'` message).
|
|
702
|
+
- `tools` — optional function tools, in the OpenAI function-tool schema.
|
|
703
|
+
- `toolChoice` — optional: `'auto'` (the default), `'none'`, `'required'`,
|
|
704
|
+
or `{ type: 'function', function: { name } }` to force one tool.
|
|
705
|
+
- `model` — optional OpenRouter model id. Defaults to
|
|
706
|
+
`'meta/muse-spark-1.3'`.
|
|
707
|
+
- `maxTokens` — optional cap on the reply's length.
|
|
708
|
+
- `reasoning` — optional `{ effort: 'minimal' | 'low' | 'medium' | 'high' }`,
|
|
709
|
+
how hard the model thinks before it answers. Lower effort answers sooner.
|
|
710
|
+
Left out, the model uses its own default. Muse can't turn reasoning off;
|
|
711
|
+
`'minimal'` is its least.
|
|
712
|
+
- `onDelta` — optional callback for the reply as it streams in. It gets
|
|
713
|
+
`{ type: 'text', text }` and `{ type: 'reasoning', text }` pieces, and
|
|
714
|
+
`{ type: 'toolCall', toolCall }` once for each tool call, when its
|
|
715
|
+
arguments are complete. Pieces come in the order the model wrote them.
|
|
716
|
+
- `signal` — optional `AbortSignal`. Aborting it stops the model and
|
|
717
|
+
resolves the call with `'cancelled'` at once.
|
|
718
|
+
|
|
719
|
+
The promise resolves with the whole message when the model finishes. A deck
|
|
720
|
+
runs any tool calls itself, appends the message and one `'tool'` message per
|
|
721
|
+
call, and calls Agent.chat again, until a reply has no tool calls:
|
|
722
|
+
|
|
723
|
+
```js
|
|
724
|
+
import { Agent } from "castle-web-sdk";
|
|
725
|
+
|
|
726
|
+
const tools = [
|
|
727
|
+
{
|
|
728
|
+
type: "function",
|
|
729
|
+
function: {
|
|
730
|
+
name: "roll_dice",
|
|
731
|
+
description: "Roll six-sided dice.",
|
|
732
|
+
parameters: {
|
|
733
|
+
type: "object",
|
|
734
|
+
properties: { count: { type: "integer" } },
|
|
735
|
+
required: ["count"],
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
];
|
|
740
|
+
const messages = [{ role: "user", content: "Roll two dice and add them." }];
|
|
741
|
+
const stop = new AbortController();
|
|
742
|
+
|
|
743
|
+
for (let round = 0; round < 8; round++) {
|
|
744
|
+
const result = await Agent.chat({
|
|
745
|
+
messages,
|
|
746
|
+
tools,
|
|
747
|
+
signal: stop.signal,
|
|
748
|
+
onDelta: (delta) => {
|
|
749
|
+
if (delta.type === "text") {
|
|
750
|
+
showText(delta.text);
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
if (result.status !== "ok") {
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
messages.push(result.message);
|
|
758
|
+
const calls = result.message.tool_calls ?? [];
|
|
759
|
+
if (calls.length === 0) {
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
for (const call of calls) {
|
|
763
|
+
const { count } = JSON.parse(call.function.arguments);
|
|
764
|
+
const faces = Array.from({ length: count }, () => 1 + Math.floor(Math.random() * 6));
|
|
765
|
+
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(faces) });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
`AgentChatResult` has a `status`:
|
|
771
|
+
|
|
772
|
+
- `'ok'` — `message` is the assistant's reply: `{ role, content,
|
|
773
|
+
tool_calls? }`, with `content` null when the reply is only tool calls.
|
|
774
|
+
`reasoning` is the model's reasoning summary when it gave one, for
|
|
775
|
+
display; it is not part of `message`. `finishReason` is `'stop'`,
|
|
776
|
+
`'tool_calls'` or `'length'`. `usage` carries `prompt_tokens`,
|
|
777
|
+
`completion_tokens` and `total_tokens`.
|
|
778
|
+
- `'refused'` — the player is not a Castle admin. The model was not called.
|
|
779
|
+
- `'unavailable'` — this host has no route to the oracle.
|
|
780
|
+
- `'cancelled'` — the call's `signal` was aborted.
|
|
781
|
+
- `'error'` — the oracle or the model failed; `reason` says how.
|
|
782
|
+
|
|
783
|
+
Every status but `'ok'` carries a `reason`.
|
|
784
|
+
|
|
654
785
|
## Lifecycle
|
|
655
786
|
|
|
656
787
|
`Lifecycle` tells the host when the deck has painted its first frame, so
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentChatParams, AgentChatResult } from "./commands";
|
|
2
|
+
export type { AgentChatDelta, AgentChatParams, AgentChatResult, AgentMessage, AgentReasoning, AgentTool, AgentToolCall, AgentToolChoice, AgentUsage, } from "./commands";
|
|
3
|
+
export interface CastleAgentApi {
|
|
4
|
+
chat(params: AgentChatParams): Promise<AgentChatResult>;
|
|
5
|
+
}
|
|
6
|
+
export declare const Agent: CastleAgentApi;
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Agent — one chat completion from a language model, run by the host through
|
|
2
|
+
// Castle's oracle with the player's own Castle account. Messages and tools
|
|
3
|
+
// follow the OpenAI chat completions format. The reply streams: `onDelta` gets
|
|
4
|
+
// text and reasoning as they arrive and each tool call once its arguments are
|
|
5
|
+
// complete, and the promise resolves with the whole message. A deck runs any
|
|
6
|
+
// tool calls itself and calls Agent.chat again with the results.
|
|
7
|
+
//
|
|
8
|
+
// Agent.chat is limited to Castle admins for now: every other player gets
|
|
9
|
+
// `refused`. A host with no route to the oracle answers `unavailable`.
|
|
10
|
+
import { CastleError } from "./errors";
|
|
11
|
+
import { hostRequest } from "./transport";
|
|
12
|
+
async function chat(params) {
|
|
13
|
+
validate(params);
|
|
14
|
+
const onDelta = params.onDelta;
|
|
15
|
+
try {
|
|
16
|
+
return await hostRequest("agent.chat", {
|
|
17
|
+
messages: params.messages,
|
|
18
|
+
tools: params.tools,
|
|
19
|
+
toolChoice: params.toolChoice,
|
|
20
|
+
model: params.model,
|
|
21
|
+
maxTokens: params.maxTokens,
|
|
22
|
+
reasoning: params.reasoning,
|
|
23
|
+
}, {
|
|
24
|
+
onStream: onDelta ? (event) => deliverDelta(onDelta, event) : undefined,
|
|
25
|
+
signal: params.signal,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error instanceof CastleError && error.code === "CANCELLED") {
|
|
30
|
+
return { status: "cancelled", reason: "Agent.chat was cancelled." };
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
|
|
36
|
+
function validate(params) {
|
|
37
|
+
if (!params || typeof params !== "object") {
|
|
38
|
+
invalid("Agent.chat requires { messages }.");
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(params.messages) || params.messages.length === 0) {
|
|
41
|
+
invalid("Agent.chat messages must be a non-empty array.");
|
|
42
|
+
}
|
|
43
|
+
if (params.tools !== undefined && !Array.isArray(params.tools)) {
|
|
44
|
+
invalid("Agent.chat tools must be an array.");
|
|
45
|
+
}
|
|
46
|
+
if (params.model !== undefined && (typeof params.model !== "string" || params.model === "")) {
|
|
47
|
+
invalid("Agent.chat model must be a model id.");
|
|
48
|
+
}
|
|
49
|
+
if (params.maxTokens !== undefined &&
|
|
50
|
+
(!Number.isSafeInteger(params.maxTokens) || params.maxTokens < 1)) {
|
|
51
|
+
invalid("Agent.chat maxTokens must be a positive integer.");
|
|
52
|
+
}
|
|
53
|
+
if (params.reasoning !== undefined && !REASONING_EFFORTS.includes(params.reasoning?.effort)) {
|
|
54
|
+
invalid("Agent.chat reasoning must be { effort: 'minimal' | 'low' | 'medium' | 'high' }.");
|
|
55
|
+
}
|
|
56
|
+
if (params.onDelta !== undefined && typeof params.onDelta !== "function") {
|
|
57
|
+
invalid("Agent.chat onDelta must be a function.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Keepalives only restart the wait in transport.ts; the deck never sees them.
|
|
61
|
+
function deliverDelta(onDelta, event) {
|
|
62
|
+
const type = event?.type;
|
|
63
|
+
if (type === "text" || type === "reasoning" || type === "toolCall") {
|
|
64
|
+
onDelta(event);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function invalid(message) {
|
|
68
|
+
throw new CastleError({ code: "INVALID_ARGUMENT", message, operation: "Agent.chat" });
|
|
69
|
+
}
|
|
70
|
+
export const Agent = Object.freeze({ chat });
|
package/dist/castle.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { isEdit } from "./context";
|
|
2
|
+
export { Agent } from "./agent";
|
|
3
|
+
export type { AgentChatDelta, AgentChatParams, AgentChatResult, AgentMessage, AgentReasoning, AgentTool, AgentToolCall, AgentToolChoice, AgentUsage, CastleAgentApi, } from "./agent";
|
|
2
4
|
export { Characters } from "./characters";
|
|
3
5
|
export type { CastleCharactersApi, CharacterHandle, CharacterHistoryOptions, CharacterSendInput, } from "./characters";
|
|
6
|
+
export { Decks } from "./decks";
|
|
7
|
+
export type { CastleDecksApi, DeckSummary, DecksListParams, DecksListResult } from "./decks";
|
|
4
8
|
export { CastleError } from "./errors";
|
|
5
9
|
export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
|
|
6
10
|
export { Haptics } from "./haptics";
|
package/dist/castle.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Castle Web SDK
|
|
2
2
|
export { isEdit } from "./context";
|
|
3
|
+
export { Agent } from "./agent";
|
|
3
4
|
export { Characters } from "./characters";
|
|
5
|
+
export { Decks } from "./decks";
|
|
4
6
|
export { CastleError } from "./errors";
|
|
5
7
|
export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
|
|
6
8
|
export { Haptics } from "./haptics";
|
package/dist/commands.d.ts
CHANGED
|
@@ -142,7 +142,97 @@ export interface CharacterHistory {
|
|
|
142
142
|
operations: CharacterOperation[];
|
|
143
143
|
cursor: string | null;
|
|
144
144
|
}
|
|
145
|
+
export interface AgentToolCall {
|
|
146
|
+
id: string;
|
|
147
|
+
type: "function";
|
|
148
|
+
function: {
|
|
149
|
+
name: string;
|
|
150
|
+
arguments: string;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export interface AgentMessage {
|
|
154
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
155
|
+
content: string | Json[] | null;
|
|
156
|
+
tool_calls?: AgentToolCall[];
|
|
157
|
+
tool_call_id?: string;
|
|
158
|
+
name?: string;
|
|
159
|
+
}
|
|
160
|
+
export interface AgentTool {
|
|
161
|
+
type: "function";
|
|
162
|
+
function: {
|
|
163
|
+
name: string;
|
|
164
|
+
description?: string;
|
|
165
|
+
parameters?: Json;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export type AgentToolChoice = "auto" | "none" | "required" | {
|
|
169
|
+
type: "function";
|
|
170
|
+
function: {
|
|
171
|
+
name: string;
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
export type AgentChatDelta = {
|
|
175
|
+
type: "text";
|
|
176
|
+
text: string;
|
|
177
|
+
} | {
|
|
178
|
+
type: "reasoning";
|
|
179
|
+
text: string;
|
|
180
|
+
} | {
|
|
181
|
+
type: "toolCall";
|
|
182
|
+
toolCall: AgentToolCall;
|
|
183
|
+
};
|
|
184
|
+
export type AgentChatStreamEvent = AgentChatDelta | {
|
|
185
|
+
type: "keepalive";
|
|
186
|
+
};
|
|
187
|
+
export interface AgentReasoning {
|
|
188
|
+
effort: "minimal" | "low" | "medium" | "high";
|
|
189
|
+
}
|
|
190
|
+
export interface AgentChatParams {
|
|
191
|
+
messages: AgentMessage[];
|
|
192
|
+
tools?: AgentTool[];
|
|
193
|
+
toolChoice?: AgentToolChoice;
|
|
194
|
+
model?: string;
|
|
195
|
+
maxTokens?: number;
|
|
196
|
+
reasoning?: AgentReasoning;
|
|
197
|
+
onDelta?: (delta: AgentChatDelta) => void;
|
|
198
|
+
signal?: AbortSignal;
|
|
199
|
+
}
|
|
200
|
+
export type AgentChatRequest = Omit<AgentChatParams, "onDelta" | "signal">;
|
|
201
|
+
export interface AgentUsage {
|
|
202
|
+
prompt_tokens: number;
|
|
203
|
+
completion_tokens: number;
|
|
204
|
+
total_tokens: number;
|
|
205
|
+
}
|
|
206
|
+
export type AgentChatResult = {
|
|
207
|
+
status: "ok";
|
|
208
|
+
message: AgentMessage;
|
|
209
|
+
reasoning?: string;
|
|
210
|
+
finishReason: string | null;
|
|
211
|
+
usage?: AgentUsage;
|
|
212
|
+
} | {
|
|
213
|
+
status: "unavailable" | "refused" | "error" | "cancelled";
|
|
214
|
+
reason: string;
|
|
215
|
+
};
|
|
216
|
+
export interface DeckSummary {
|
|
217
|
+
deckId: string;
|
|
218
|
+
title: string;
|
|
219
|
+
creator: {
|
|
220
|
+
userId: string;
|
|
221
|
+
username: string;
|
|
222
|
+
} | null;
|
|
223
|
+
coverUrl: string | null;
|
|
224
|
+
}
|
|
225
|
+
export interface DecksListParams {
|
|
226
|
+
limit?: number | null;
|
|
227
|
+
}
|
|
228
|
+
export interface DecksListResult {
|
|
229
|
+
status: "ok";
|
|
230
|
+
decks: DeckSummary[];
|
|
231
|
+
}
|
|
145
232
|
export interface CommandParams {
|
|
233
|
+
"agent.chat": AgentChatRequest;
|
|
234
|
+
"decks.mine": DecksListParams;
|
|
235
|
+
"decks.saved": DecksListParams;
|
|
146
236
|
"deckStorage.load": Record<string, never>;
|
|
147
237
|
"deckStorage.update": {
|
|
148
238
|
updates: StorageUpdate[];
|
|
@@ -279,6 +369,9 @@ export interface CommandParams {
|
|
|
279
369
|
"lifecycle.restoreState": Record<string, never>;
|
|
280
370
|
}
|
|
281
371
|
export interface CommandResult {
|
|
372
|
+
"agent.chat": AgentChatResult;
|
|
373
|
+
"decks.mine": DecksListResult;
|
|
374
|
+
"decks.saved": DecksListResult;
|
|
282
375
|
"deckStorage.load": {
|
|
283
376
|
blob: StorageBlob;
|
|
284
377
|
};
|
|
@@ -365,6 +458,8 @@ export interface UserPayload {
|
|
|
365
458
|
color: string | null;
|
|
366
459
|
}
|
|
367
460
|
export type CommandName = keyof CommandParams;
|
|
461
|
+
export declare const COMMAND_TIMEOUTS_MS: Partial<Record<CommandName, number>>;
|
|
462
|
+
export declare const STREAM_IDLE_TIMEOUT_MS = 60000;
|
|
368
463
|
export interface SerializedCommandError {
|
|
369
464
|
code: string;
|
|
370
465
|
message: string;
|
|
@@ -385,6 +480,17 @@ export interface CommandResponseEnvelope {
|
|
|
385
480
|
error?: SerializedCommandError;
|
|
386
481
|
}
|
|
387
482
|
export declare function isResponseEnvelope(value: unknown): value is CommandResponseEnvelope;
|
|
483
|
+
export interface CommandStreamEnvelope {
|
|
484
|
+
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
485
|
+
requestId: string;
|
|
486
|
+
stream: unknown;
|
|
487
|
+
}
|
|
488
|
+
export declare function isStreamEnvelope(value: unknown): value is CommandStreamEnvelope;
|
|
489
|
+
export interface CommandCancelEnvelope {
|
|
490
|
+
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
491
|
+
requestId: string;
|
|
492
|
+
cancel: true;
|
|
493
|
+
}
|
|
388
494
|
export type LifecycleEvent = "ready" | "unloadInterest";
|
|
389
495
|
export interface LifecycleEnvelope {
|
|
390
496
|
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
package/dist/commands.js
CHANGED
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
// Marker + protocol version. Doubles as a discriminator so host messages can't
|
|
7
7
|
// be confused with embed.js `castlexyz:` strings or RN console messages.
|
|
8
8
|
export const CASTLE_SDK_PROTOCOL = 1;
|
|
9
|
+
// How long the deck waits for a reply to these commands, in place of the
|
|
10
|
+
// data-command timeout. A deck list fetches every cover on the list. A host
|
|
11
|
+
// from before streaming reads a whole model reply before answering agent.chat.
|
|
12
|
+
export const COMMAND_TIMEOUTS_MS = {
|
|
13
|
+
"agent.chat": 180_000,
|
|
14
|
+
"decks.mine": 30_000,
|
|
15
|
+
"decks.saved": 30_000,
|
|
16
|
+
};
|
|
17
|
+
// Once a host streams a command, each stream envelope restarts the wait at this
|
|
18
|
+
// length instead. The oracle sends a heartbeat every 15 seconds, and the host
|
|
19
|
+
// passes each one on.
|
|
20
|
+
export const STREAM_IDLE_TIMEOUT_MS = 60_000;
|
|
9
21
|
export function isResponseEnvelope(value) {
|
|
10
22
|
if (typeof value !== "object" || value === null)
|
|
11
23
|
return false;
|
|
@@ -14,6 +26,15 @@ export function isResponseEnvelope(value) {
|
|
|
14
26
|
typeof record.requestId === "string" &&
|
|
15
27
|
typeof record.ok === "boolean");
|
|
16
28
|
}
|
|
29
|
+
export function isStreamEnvelope(value) {
|
|
30
|
+
if (typeof value !== "object" || value === null) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const record = value;
|
|
34
|
+
return (record.castleSdk === CASTLE_SDK_PROTOCOL &&
|
|
35
|
+
typeof record.requestId === "string" &&
|
|
36
|
+
"stream" in record);
|
|
37
|
+
}
|
|
17
38
|
export function unloadRequestId(value) {
|
|
18
39
|
if (typeof value !== "object" || value === null)
|
|
19
40
|
return null;
|
package/dist/decks.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DecksListParams, DecksListResult } from "./commands";
|
|
2
|
+
export type { DeckSummary, DecksListParams, DecksListResult } from "./commands";
|
|
3
|
+
export interface CastleDecksApi {
|
|
4
|
+
mine(options?: DecksListParams): Promise<DecksListResult>;
|
|
5
|
+
saved(options?: DecksListParams): Promise<DecksListResult>;
|
|
6
|
+
}
|
|
7
|
+
export declare const Decks: CastleDecksApi;
|
package/dist/decks.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Decks — the player's own published decks and the decks they saved, for
|
|
2
|
+
// picking where a portal leads. Each entry has the same shape as Portal.info:
|
|
3
|
+
// deckId, title, creator and a cover as a data: url.
|
|
4
|
+
//
|
|
5
|
+
// Both lists leave out decks a portal cannot reach: a private deck, or one
|
|
6
|
+
// whose creator turned portals off.
|
|
7
|
+
//
|
|
8
|
+
// Decks is limited to Castle admins for now; other players get a
|
|
9
|
+
// PERMISSION_DENIED error.
|
|
10
|
+
import { CastleError } from "./errors";
|
|
11
|
+
import { hostRequest } from "./transport";
|
|
12
|
+
function mine(options = {}) {
|
|
13
|
+
return hostRequest("decks.mine", { limit: listLimit(options.limit, "Decks.mine") });
|
|
14
|
+
}
|
|
15
|
+
function saved(options = {}) {
|
|
16
|
+
return hostRequest("decks.saved", { limit: listLimit(options.limit, "Decks.saved") });
|
|
17
|
+
}
|
|
18
|
+
function listLimit(value, operation) {
|
|
19
|
+
if (value === undefined || value === null) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (Number.isSafeInteger(value) && value >= 1 && value <= 50) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
throw new CastleError({
|
|
26
|
+
code: "INVALID_ARGUMENT",
|
|
27
|
+
message: `${operation} limit must be an integer from 1 through 50.`,
|
|
28
|
+
operation,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export const Decks = Object.freeze({ mine, saved });
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CommandName, type CommandParams, type CommandResponseEnvelope } from "./commands";
|
|
1
|
+
import { type CommandName, type CommandParams, type CommandResponseEnvelope, type CommandStreamEnvelope } from "./commands";
|
|
2
2
|
import { type TunedParam } from "./paramsRegistry";
|
|
3
3
|
export declare const CARD_RATIO: number;
|
|
4
4
|
interface LocalResponse {
|
|
@@ -43,7 +43,9 @@ export declare function renameFile(from: string, to: string): Promise<void>;
|
|
|
43
43
|
export declare function deleteFile(path: string): Promise<void>;
|
|
44
44
|
export declare function openFile(path: string): void;
|
|
45
45
|
export declare function initCard(): HTMLDivElement;
|
|
46
|
-
export declare function sendLocalCommand<C extends CommandName>(command: C, params: CommandParams[C]): Promise<
|
|
46
|
+
export declare function sendLocalCommand<C extends CommandName>(requestId: string, command: C, params: CommandParams[C], listener: (message: CommandResponseEnvelope | CommandStreamEnvelope) => void): Promise<void>;
|
|
47
|
+
export declare function cancelLocalCommand(requestId: string): void;
|
|
48
|
+
export declare function releaseLocalCommand(requestId: string): void;
|
|
47
49
|
/**
|
|
48
50
|
* Notified `true` on every fresh dev-server socket and `false` when one is lost.
|
|
49
51
|
* What lets a queued write know the moment retrying is worth it again.
|
package/dist/runtime.js
CHANGED
|
@@ -13,10 +13,9 @@ const pendingRequests = new Map();
|
|
|
13
13
|
// Local-dev command channel: the `castle-web serve` dev server is the host, so
|
|
14
14
|
// SDK commands ride the same websocket runtime.ts already uses for
|
|
15
15
|
// logs/screenshots/restart. Correlated by requestId, separate from the
|
|
16
|
-
// screenshot/write_file request map above.
|
|
17
|
-
const COMMAND_TIMEOUT_MS = 15000;
|
|
16
|
+
// screenshot/write_file request map above. transport.ts owns the timeouts.
|
|
18
17
|
const SOCKET_WAIT_TIMEOUT_MS = 10000;
|
|
19
|
-
const
|
|
18
|
+
const localCommandListeners = new Map();
|
|
20
19
|
const origLog = console.log;
|
|
21
20
|
const origWarn = console.warn;
|
|
22
21
|
const origError = console.error;
|
|
@@ -324,38 +323,42 @@ function sendLocalRequest(msg) {
|
|
|
324
323
|
ws.send(JSON.stringify(request));
|
|
325
324
|
});
|
|
326
325
|
}
|
|
327
|
-
// Send an SDK command to the dev server
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
});
|
|
358
|
-
}
|
|
326
|
+
// Send an SDK command to the dev server. Its stream envelopes and its response
|
|
327
|
+
// go to `listener` as they arrive, and the listener is dropped after the
|
|
328
|
+
// response. transport.ts interprets them, so error reconstruction stays uniform
|
|
329
|
+
// across all three channels there. Waits for the socket to open so a command
|
|
330
|
+
// issued during startup isn't dropped; rejects if it never opens.
|
|
331
|
+
export async function sendLocalCommand(requestId, command, params, listener) {
|
|
332
|
+
localCommandListeners.set(requestId, listener);
|
|
333
|
+
try {
|
|
334
|
+
const socket = await waitForSocket();
|
|
335
|
+
// Cancelled or timed out while the socket was opening.
|
|
336
|
+
if (!localCommandListeners.has(requestId)) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
socket.send(JSON.stringify({
|
|
340
|
+
type: "castle_command",
|
|
341
|
+
castleSdk: CASTLE_SDK_PROTOCOL,
|
|
342
|
+
requestId,
|
|
343
|
+
command,
|
|
344
|
+
params,
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
localCommandListeners.delete(requestId);
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Ask the dev server to stop a streaming command, and stop listening for it.
|
|
353
|
+
export function cancelLocalCommand(requestId) {
|
|
354
|
+
localCommandListeners.delete(requestId);
|
|
355
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
356
|
+
ws.send(JSON.stringify({ type: "castle_command_cancel", castleSdk: CASTLE_SDK_PROTOCOL, requestId }));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Stop listening for a command the deck has given up on.
|
|
360
|
+
export function releaseLocalCommand(requestId) {
|
|
361
|
+
localCommandListeners.delete(requestId);
|
|
359
362
|
}
|
|
360
363
|
function waitForSocket() {
|
|
361
364
|
if (ws && ws.readyState === WebSocket.OPEN)
|
|
@@ -374,10 +377,15 @@ function waitForSocket() {
|
|
|
374
377
|
}, 100);
|
|
375
378
|
});
|
|
376
379
|
}
|
|
377
|
-
function
|
|
378
|
-
const
|
|
379
|
-
if (
|
|
380
|
-
|
|
380
|
+
function deliverLocalCommand(msg) {
|
|
381
|
+
const listener = localCommandListeners.get(msg.requestId);
|
|
382
|
+
if (!listener) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (!("stream" in msg)) {
|
|
386
|
+
localCommandListeners.delete(msg.requestId);
|
|
387
|
+
}
|
|
388
|
+
listener(msg);
|
|
381
389
|
}
|
|
382
390
|
function resolveLocalRequest(msg) {
|
|
383
391
|
if (!msg.requestId)
|
|
@@ -919,7 +927,10 @@ function handleLocalMessage(msg) {
|
|
|
919
927
|
resolveLocalRequest(msg);
|
|
920
928
|
}
|
|
921
929
|
else if (msg.type === "castle_command_response") {
|
|
922
|
-
|
|
930
|
+
deliverLocalCommand(msg);
|
|
931
|
+
}
|
|
932
|
+
else if (msg.type === "castle_command_stream") {
|
|
933
|
+
deliverLocalCommand(msg);
|
|
923
934
|
}
|
|
924
935
|
}
|
|
925
936
|
// Files-changed listeners, registered by consumers (the kit engine, editors).
|
package/dist/server/storage.d.ts
CHANGED
|
@@ -106,6 +106,48 @@ export interface EvaluateCharacterInformationInput {
|
|
|
106
106
|
operationId: string;
|
|
107
107
|
maxDeliveries?: number;
|
|
108
108
|
}
|
|
109
|
+
export type CharacterStoryOpportunity = {
|
|
110
|
+
type: "approach" | "room_entry" | "world_action";
|
|
111
|
+
id: string;
|
|
112
|
+
};
|
|
113
|
+
export interface EvaluateCharacterBeatsInput {
|
|
114
|
+
operationId: string;
|
|
115
|
+
opportunityId: string;
|
|
116
|
+
opportunity: CharacterStoryOpportunity;
|
|
117
|
+
presentCharacterIds: string[];
|
|
118
|
+
}
|
|
119
|
+
export interface CharacterBeatSelection {
|
|
120
|
+
deliveryId: string;
|
|
121
|
+
beatId: string;
|
|
122
|
+
characterId: string;
|
|
123
|
+
openingText: string;
|
|
124
|
+
}
|
|
125
|
+
export interface CharacterBeatEvaluationResult {
|
|
126
|
+
operationId: string;
|
|
127
|
+
opportunityId: string;
|
|
128
|
+
opportunityCount: number;
|
|
129
|
+
gameTime: number | null;
|
|
130
|
+
selection: CharacterBeatSelection | null;
|
|
131
|
+
}
|
|
132
|
+
export type CharacterBeatEvaluationOperation = Omit<CharacterOperation, "result"> & {
|
|
133
|
+
result: CharacterBeatEvaluationResult | null;
|
|
134
|
+
};
|
|
135
|
+
export interface CompleteScheduledCharacterBeatInput {
|
|
136
|
+
operationId: string;
|
|
137
|
+
deliveryId: string;
|
|
138
|
+
outcomeId: string;
|
|
139
|
+
}
|
|
140
|
+
export interface CharacterScheduledBeatCompletionResult {
|
|
141
|
+
completionId: string;
|
|
142
|
+
deliveryId: string;
|
|
143
|
+
beatId: string;
|
|
144
|
+
characterId: string;
|
|
145
|
+
outcomeId: string;
|
|
146
|
+
completedAt: string;
|
|
147
|
+
}
|
|
148
|
+
export type CharacterScheduledBeatCompletionOperation = Omit<CharacterOperation, "result"> & {
|
|
149
|
+
result: CharacterScheduledBeatCompletionResult | null;
|
|
150
|
+
};
|
|
109
151
|
export interface CharacterInformationEvaluationResult {
|
|
110
152
|
generation: number;
|
|
111
153
|
evaluatedAt: string;
|
|
@@ -153,6 +195,8 @@ export interface ServerPlaythroughCharacters {
|
|
|
153
195
|
deliverKnowledge(input: DeliverCharacterKnowledgeInput): Promise<CharacterOperation>;
|
|
154
196
|
completeBeat(input: CompleteCharacterBeatInput): Promise<CharacterOperation>;
|
|
155
197
|
evaluateInformation(input: EvaluateCharacterInformationInput): Promise<CharacterInformationEvaluationOperation>;
|
|
198
|
+
evaluateBeats(input: EvaluateCharacterBeatsInput): Promise<CharacterBeatEvaluationOperation>;
|
|
199
|
+
completeScheduledBeat(input: CompleteScheduledCharacterBeatInput): Promise<CharacterScheduledBeatCompletionOperation>;
|
|
156
200
|
status(operationId: string): Promise<CharacterOperation>;
|
|
157
201
|
history(options?: ServerCharacterHistoryOptions): Promise<CharacterHistory>;
|
|
158
202
|
}
|
package/dist/server/storage.js
CHANGED
|
@@ -159,6 +159,25 @@ function createPlaythrough(call, subject, generation) {
|
|
|
159
159
|
...(maxDeliveries === undefined ? {} : { maxDeliveries }),
|
|
160
160
|
});
|
|
161
161
|
},
|
|
162
|
+
async evaluateBeats(input) {
|
|
163
|
+
return call("characterEvaluateBeats", {
|
|
164
|
+
subject,
|
|
165
|
+
generation,
|
|
166
|
+
operationId: assertOperationId(input?.operationId),
|
|
167
|
+
opportunityId: assertOpportunityId(input?.opportunityId),
|
|
168
|
+
opportunity: assertStoryOpportunity(input?.opportunity),
|
|
169
|
+
presentCharacterIds: assertPresentCharacterIds(input?.presentCharacterIds),
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
async completeScheduledBeat(input) {
|
|
173
|
+
return call("characterCompleteScheduledBeat", {
|
|
174
|
+
subject,
|
|
175
|
+
generation,
|
|
176
|
+
operationId: assertOperationId(input?.operationId),
|
|
177
|
+
deliveryId: assertOpportunityId(input?.deliveryId, "deliveryId"),
|
|
178
|
+
outcomeId: assertNarrativeId(input?.outcomeId, "outcomeId"),
|
|
179
|
+
});
|
|
180
|
+
},
|
|
162
181
|
async status(operationId) {
|
|
163
182
|
return call("characterStatus", {
|
|
164
183
|
subject,
|
|
@@ -343,6 +362,41 @@ function assertOperationId(value) {
|
|
|
343
362
|
}
|
|
344
363
|
return value;
|
|
345
364
|
}
|
|
365
|
+
function assertOpportunityId(value, field = "opportunityId") {
|
|
366
|
+
if (typeof value !== "string" ||
|
|
367
|
+
value.length === 0 ||
|
|
368
|
+
value.length > MAX_OPERATION_ID_CHARACTERS ||
|
|
369
|
+
value.includes("\0") ||
|
|
370
|
+
hasUnpairedSurrogate(value) ||
|
|
371
|
+
utf8Length(value) > MAX_OPERATION_ID_BYTES) {
|
|
372
|
+
throw characterArgumentError(`${field} must be valid text of at most ${MAX_OPERATION_ID_CHARACTERS} characters and ${MAX_OPERATION_ID_BYTES} bytes.`);
|
|
373
|
+
}
|
|
374
|
+
return value;
|
|
375
|
+
}
|
|
376
|
+
function assertStoryOpportunity(value) {
|
|
377
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
378
|
+
throw characterArgumentError("opportunity must contain a type and id.");
|
|
379
|
+
}
|
|
380
|
+
const opportunity = value;
|
|
381
|
+
if (Object.keys(opportunity).some((key) => key !== "type" && key !== "id") ||
|
|
382
|
+
!["approach", "room_entry", "world_action"].includes(String(opportunity.type))) {
|
|
383
|
+
throw characterArgumentError("opportunity type is invalid.");
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
type: opportunity.type,
|
|
387
|
+
id: assertNarrativeId(opportunity.id, "opportunity.id"),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function assertPresentCharacterIds(value) {
|
|
391
|
+
if (!Array.isArray(value) || value.length > 16) {
|
|
392
|
+
throw characterArgumentError("presentCharacterIds must contain at most 16 character ids.");
|
|
393
|
+
}
|
|
394
|
+
const ids = value.map((item) => assertNarrativeId(item, "presentCharacterIds"));
|
|
395
|
+
if (new Set(ids).size !== ids.length) {
|
|
396
|
+
throw characterArgumentError("presentCharacterIds must not contain duplicates.");
|
|
397
|
+
}
|
|
398
|
+
return ids.sort();
|
|
399
|
+
}
|
|
346
400
|
function assertNarrativeId(value, field) {
|
|
347
401
|
if (typeof value !== "string" ||
|
|
348
402
|
value.length === 0 ||
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type CommandName, type CommandParams, type CommandResult, type LifecycleEvent } from "./commands";
|
|
2
2
|
type PostChannel = "mobile" | "web";
|
|
3
|
+
export interface HostStreamOptions {
|
|
4
|
+
onStream?: (event: unknown) => void;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}
|
|
3
7
|
interface ReactNativeWebViewBridge {
|
|
4
8
|
postMessage: (message: string) => void;
|
|
5
9
|
}
|
|
@@ -11,7 +15,7 @@ declare global {
|
|
|
11
15
|
};
|
|
12
16
|
}
|
|
13
17
|
}
|
|
14
|
-
export declare function hostRequest<C extends CommandName>(command: C, params: CommandParams[C]): Promise<CommandResult[C]>;
|
|
18
|
+
export declare function hostRequest<C extends CommandName>(command: C, params: CommandParams[C], stream?: HostStreamOptions): Promise<CommandResult[C]>;
|
|
15
19
|
export declare function getCommandChannel(): PostChannel | "local";
|
|
16
20
|
export declare function hostNotify(event: LifecycleEvent): void;
|
|
17
21
|
export declare function postUnloadDone(unloadId: string, state?: string): void;
|
package/dist/transport.js
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
// - web → window.parent.postMessage; responses via 'message' events
|
|
8
8
|
// - local → the castle-web serve dev server, over runtime.ts's websocket
|
|
9
9
|
// Error reconstruction is uniform here so callers always get a CastleError.
|
|
10
|
-
|
|
10
|
+
//
|
|
11
|
+
// A streaming command (agent.chat) gets stream envelopes on its requestId before
|
|
12
|
+
// its response. Each one goes to the caller's onStream and restarts the wait,
|
|
13
|
+
// and the caller's AbortSignal sends the host a cancel envelope.
|
|
14
|
+
import { CASTLE_SDK_PROTOCOL, COMMAND_TIMEOUTS_MS, STREAM_IDLE_TIMEOUT_MS, isResponseEnvelope, isStreamEnvelope, unloadRequestId, } from "./commands";
|
|
11
15
|
import { getCastleEmbed } from "./context";
|
|
12
16
|
import { CastleError } from "./errors";
|
|
13
|
-
import { sendLocalCommand } from "./runtime";
|
|
17
|
+
import { cancelLocalCommand, releaseLocalCommand, sendLocalCommand } from "./runtime";
|
|
14
18
|
const REQUEST_TIMEOUT_MS = 15000;
|
|
15
19
|
// Interactive platform commands hold the screen while the player interacts with
|
|
16
20
|
// a host-native sheet (e.g. a pass purchase), so the flat data-command timeout
|
|
@@ -20,16 +24,16 @@ const INTERACTIVE_COMMANDS = new Set([
|
|
|
20
24
|
"snapshot.offer",
|
|
21
25
|
"portal.open",
|
|
22
26
|
]);
|
|
27
|
+
// Request ids carry a per-page prefix so that a host serving several decks at
|
|
28
|
+
// once can key a cancel by requestId alone.
|
|
29
|
+
const requestIdPrefix = `csdk_${Math.random().toString(36).slice(2, 10)}_`;
|
|
23
30
|
let nextRequestId = 1;
|
|
24
31
|
const pending = new Map();
|
|
25
32
|
let listenersInstalled = false;
|
|
26
33
|
let unloadHandler = null;
|
|
27
|
-
export async function hostRequest(command, params) {
|
|
34
|
+
export async function hostRequest(command, params, stream = {}) {
|
|
28
35
|
try {
|
|
29
|
-
const
|
|
30
|
-
const env = channel === "local"
|
|
31
|
-
? await sendLocalCommand(command, params)
|
|
32
|
-
: await postCommand(channel, command, params);
|
|
36
|
+
const env = await sendCommand(resolveChannel(), command, params, stream);
|
|
33
37
|
return interpretResponse(command, env);
|
|
34
38
|
}
|
|
35
39
|
catch (error) {
|
|
@@ -103,20 +107,84 @@ function resolveChannel() {
|
|
|
103
107
|
// local dev server (top-level page served by `castle-web serve`).
|
|
104
108
|
return window.parent && window.parent !== window ? "web" : "local";
|
|
105
109
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
const requestId = `csdk_${nextRequestId++}`;
|
|
110
|
+
function sendCommand(channel, command, params, stream) {
|
|
111
|
+
const requestId = `${requestIdPrefix}${nextRequestId++}`;
|
|
109
112
|
return new Promise((resolve, reject) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
if (stream.signal?.aborted) {
|
|
114
|
+
reject(cancelledError(command));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const entry = { command, channel, resolve, reject, onStream: stream.onStream };
|
|
118
|
+
pending.set(requestId, entry);
|
|
119
|
+
restartTimeout(requestId, entry, COMMAND_TIMEOUTS_MS[command] ?? REQUEST_TIMEOUT_MS);
|
|
120
|
+
watchAbort(requestId, entry, stream.signal);
|
|
121
|
+
if (channel === "local") {
|
|
122
|
+
sendLocalCommand(requestId, command, params, receive).catch((error) => {
|
|
123
|
+
forget(requestId);
|
|
124
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
installHostListener();
|
|
117
129
|
sendEnvelope(channel, { castleSdk: CASTLE_SDK_PROTOCOL, requestId, command, params });
|
|
118
130
|
});
|
|
119
131
|
}
|
|
132
|
+
// Interactive commands have no timeout; they resolve only when the host replies.
|
|
133
|
+
function restartTimeout(requestId, entry, ms) {
|
|
134
|
+
if (entry.timeout) {
|
|
135
|
+
clearTimeout(entry.timeout);
|
|
136
|
+
}
|
|
137
|
+
if (INTERACTIVE_COMMANDS.has(entry.command)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
entry.timeout = setTimeout(() => {
|
|
141
|
+
forget(requestId);
|
|
142
|
+
entry.reject(new Error(`Castle host did not respond to ${entry.command}.`));
|
|
143
|
+
}, ms);
|
|
144
|
+
}
|
|
145
|
+
function watchAbort(requestId, entry, signal) {
|
|
146
|
+
if (!signal) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const onAbort = () => {
|
|
150
|
+
forget(requestId);
|
|
151
|
+
if (entry.channel === "local") {
|
|
152
|
+
cancelLocalCommand(requestId);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const envelope = {
|
|
156
|
+
castleSdk: CASTLE_SDK_PROTOCOL,
|
|
157
|
+
requestId,
|
|
158
|
+
cancel: true,
|
|
159
|
+
};
|
|
160
|
+
sendEnvelope(entry.channel, envelope);
|
|
161
|
+
}
|
|
162
|
+
entry.reject(cancelledError(entry.command));
|
|
163
|
+
};
|
|
164
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
165
|
+
entry.stopWatchingAbort = () => signal.removeEventListener("abort", onAbort);
|
|
166
|
+
}
|
|
167
|
+
function forget(requestId) {
|
|
168
|
+
const entry = pending.get(requestId);
|
|
169
|
+
if (!entry) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
pending.delete(requestId);
|
|
173
|
+
if (entry.timeout) {
|
|
174
|
+
clearTimeout(entry.timeout);
|
|
175
|
+
}
|
|
176
|
+
entry.stopWatchingAbort?.();
|
|
177
|
+
if (entry.channel === "local") {
|
|
178
|
+
releaseLocalCommand(requestId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function cancelledError(command) {
|
|
182
|
+
return new CastleError({
|
|
183
|
+
code: "CANCELLED",
|
|
184
|
+
message: `${command} was cancelled.`,
|
|
185
|
+
operation: command,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
120
188
|
function sendEnvelope(channel, envelope) {
|
|
121
189
|
const json = JSON.stringify(envelope);
|
|
122
190
|
if (channel === "mobile") {
|
|
@@ -146,21 +214,36 @@ function receive(message) {
|
|
|
146
214
|
settle(message);
|
|
147
215
|
return;
|
|
148
216
|
}
|
|
217
|
+
if (isStreamEnvelope(message)) {
|
|
218
|
+
deliverStream(message.requestId, message.stream);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
149
221
|
const unloadId = unloadRequestId(message);
|
|
150
222
|
if (unloadId !== null)
|
|
151
223
|
unloadHandler?.(unloadId);
|
|
152
224
|
}
|
|
153
225
|
function settle(message) {
|
|
154
|
-
if (!isResponseEnvelope(message))
|
|
155
|
-
return;
|
|
156
226
|
const entry = pending.get(message.requestId);
|
|
157
|
-
if (!entry)
|
|
227
|
+
if (!entry) {
|
|
158
228
|
return;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
pending.delete(message.requestId);
|
|
229
|
+
}
|
|
230
|
+
forget(message.requestId);
|
|
162
231
|
entry.resolve(message);
|
|
163
232
|
}
|
|
233
|
+
// A throwing onStream is logged, and the stream goes on.
|
|
234
|
+
function deliverStream(requestId, event) {
|
|
235
|
+
const entry = pending.get(requestId);
|
|
236
|
+
if (!entry) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
restartTimeout(requestId, entry, STREAM_IDLE_TIMEOUT_MS);
|
|
240
|
+
try {
|
|
241
|
+
entry.onStream?.(event);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
console.error(`Castle ${entry.command} stream handler threw:`, error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
164
247
|
function interpretResponse(command, env) {
|
|
165
248
|
if (env.ok)
|
|
166
249
|
return env.data;
|