castle-web-sdk 0.4.35 → 0.4.37
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 +70 -7
- package/dist/castle.d.ts +3 -2
- package/dist/commands.d.ts +93 -0
- package/dist/commands.js +25 -0
- package/dist/multiplayer.d.ts +16 -1
- package/dist/multiplayer.js +166 -11
- package/dist/multiplayerVoice.d.ts +91 -0
- package/dist/multiplayerVoice.js +424 -0
- package/dist/server/platformHandle.d.ts +13 -0
- package/dist/server/wrapper.d.ts +30 -1
- package/dist/server/wrapper.js +92 -0
- package/dist/transport.d.ts +2 -1
- package/dist/transport.js +16 -1
- package/package.json +1 -1
package/dist/server/wrapper.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isBinaryPayload, toUint8Array, } from "../multiplayerProtocol";
|
|
2
|
+
import { encodeStorageValue } from "../storageJson";
|
|
2
3
|
import { createServerStorage } from "./storage";
|
|
3
4
|
// The publish bundler binds the author's default callbacks export once and
|
|
4
5
|
// exports the returned one-argument function as `__castleBootSession`.
|
|
@@ -68,6 +69,7 @@ function createAuthorSession(platformHandle, players) {
|
|
|
68
69
|
deckId: platformHandle.config.deckId,
|
|
69
70
|
mode: platformHandle.config.mode,
|
|
70
71
|
storage: createServerStorage(platformHandle),
|
|
72
|
+
world: createServerWorld(platformHandle),
|
|
71
73
|
send(playerId, data) {
|
|
72
74
|
if (isBinaryPayload(data)) {
|
|
73
75
|
requireSendBinary(platformHandle);
|
|
@@ -106,6 +108,96 @@ function createAuthorSession(platformHandle, players) {
|
|
|
106
108
|
},
|
|
107
109
|
};
|
|
108
110
|
}
|
|
111
|
+
const WORLD_OPERATION_ID_MAX_BYTES = 128;
|
|
112
|
+
const WORLD_OPERATION_ID_MAX_CHARACTERS = 120;
|
|
113
|
+
const WORLD_CHECKPOINT_VERSION_MAX = 2_147_483_647;
|
|
114
|
+
function createServerWorld(platformHandle) {
|
|
115
|
+
if (!platformHandle.config.world)
|
|
116
|
+
return null;
|
|
117
|
+
if (typeof platformHandle.worldCheckpointRead !== "function" ||
|
|
118
|
+
typeof platformHandle.worldCheckpointCommit !== "function") {
|
|
119
|
+
throw worldError("The persistent-world checkpoint broker is unavailable.", "read");
|
|
120
|
+
}
|
|
121
|
+
const initial = parseWorldSnapshot(platformHandle.config.world, "read");
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
snapshot: initial,
|
|
124
|
+
async read() {
|
|
125
|
+
const result = await platformHandle.worldCheckpointRead();
|
|
126
|
+
return parseWorldSnapshot(parsePlatformJson(result, "read"), "read");
|
|
127
|
+
},
|
|
128
|
+
async commit(input) {
|
|
129
|
+
const expectedRevision = assertWorldRevision(input?.expectedRevision);
|
|
130
|
+
const operationId = assertWorldOperationId(input?.operationId);
|
|
131
|
+
const checkpoint = assertWorldCheckpoint(input?.checkpoint);
|
|
132
|
+
const raw = await platformHandle.worldCheckpointCommit(JSON.stringify({
|
|
133
|
+
expectedRevision,
|
|
134
|
+
operationId,
|
|
135
|
+
checkpoint,
|
|
136
|
+
}));
|
|
137
|
+
const result = parsePlatformJson(raw, "commit");
|
|
138
|
+
if (typeof result.replayed !== "boolean") {
|
|
139
|
+
throw worldError("The platform returned an invalid commit receipt.", "commit");
|
|
140
|
+
}
|
|
141
|
+
return Object.freeze({
|
|
142
|
+
revision: assertWorldRevision(result.revision),
|
|
143
|
+
replayed: result.replayed,
|
|
144
|
+
operationId,
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function parseWorldSnapshot(value, operation) {
|
|
150
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
151
|
+
throw worldError("The platform returned an invalid world checkpoint.", "read");
|
|
152
|
+
}
|
|
153
|
+
const raw = value;
|
|
154
|
+
return Object.freeze({
|
|
155
|
+
revision: assertWorldRevision(raw.revision),
|
|
156
|
+
checkpoint: raw.checkpoint === null
|
|
157
|
+
? null
|
|
158
|
+
: assertWorldCheckpoint(raw.checkpoint, operation),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function assertWorldCheckpoint(value, operation = "commit") {
|
|
162
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
163
|
+
throw worldError("A world checkpoint requires version and value.", operation);
|
|
164
|
+
}
|
|
165
|
+
const raw = value;
|
|
166
|
+
if (!Number.isSafeInteger(raw.version) ||
|
|
167
|
+
raw.version < 0 ||
|
|
168
|
+
raw.version > WORLD_CHECKPOINT_VERSION_MAX) {
|
|
169
|
+
throw worldError(`A world checkpoint version must be an integer from 0 through ${WORLD_CHECKPOINT_VERSION_MAX}.`, operation);
|
|
170
|
+
}
|
|
171
|
+
const text = encodeStorageValue(raw.value, `session.world.${operation}`);
|
|
172
|
+
const normalized = JSON.parse(text);
|
|
173
|
+
return Object.freeze({ version: raw.version, value: normalized });
|
|
174
|
+
}
|
|
175
|
+
function assertWorldRevision(value) {
|
|
176
|
+
if (typeof value === "string" && /^(?:0|[1-9][0-9]*)$/.test(value))
|
|
177
|
+
return value;
|
|
178
|
+
throw worldError("A world checkpoint revision must be a canonical decimal string.", "commit");
|
|
179
|
+
}
|
|
180
|
+
function assertWorldOperationId(value) {
|
|
181
|
+
if (typeof value !== "string" ||
|
|
182
|
+
value.length === 0 ||
|
|
183
|
+
value.length > WORLD_OPERATION_ID_MAX_CHARACTERS ||
|
|
184
|
+
value.includes("\0") ||
|
|
185
|
+
new TextEncoder().encode(value).byteLength > WORLD_OPERATION_ID_MAX_BYTES) {
|
|
186
|
+
throw worldError(`A world checkpoint operation id must be at most ${WORLD_OPERATION_ID_MAX_CHARACTERS} characters and ${WORLD_OPERATION_ID_MAX_BYTES} bytes.`, "commit");
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function parsePlatformJson(value, operation) {
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(value);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
throw worldError("The platform returned an invalid world checkpoint response.", operation);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function worldError(message, operation) {
|
|
199
|
+
return new TypeError(`session.world.${operation}: ${message}`);
|
|
200
|
+
}
|
|
109
201
|
function requireSendBinary(platformHandle) {
|
|
110
202
|
if (typeof platformHandle.sendBinary !== "function")
|
|
111
203
|
unsupportedBinaryRuntime();
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CommandName, type CommandParams, type CommandResult, type LifecycleEvent } from "./commands";
|
|
1
|
+
import { type CommandName, type CommandParams, type CommandResult, type LifecycleEvent, type VoiceEvent } from "./commands";
|
|
2
2
|
type PostChannel = "mobile" | "web";
|
|
3
3
|
export interface HostStreamOptions {
|
|
4
4
|
onStream?: (event: unknown) => void;
|
|
@@ -20,5 +20,6 @@ export declare function getCommandChannel(): PostChannel | "local";
|
|
|
20
20
|
export declare function hostNotify(event: LifecycleEvent): void;
|
|
21
21
|
export declare function postUnloadDone(unloadId: string, state?: string): void;
|
|
22
22
|
export declare function setUnloadHandler(handler: (unloadId: string) => void): void;
|
|
23
|
+
export declare function setVoiceEventHandler(handler: ((event: VoiceEvent) => void) | null): void;
|
|
23
24
|
export declare function installHostListener(): void;
|
|
24
25
|
export {};
|
package/dist/transport.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// A streaming command (agent.chat) gets stream envelopes on its requestId before
|
|
12
12
|
// its response. Each one goes to the caller's onStream and restarts the wait,
|
|
13
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";
|
|
14
|
+
import { CASTLE_SDK_PROTOCOL, COMMAND_TIMEOUTS_MS, STREAM_IDLE_TIMEOUT_MS, isResponseEnvelope, isStreamEnvelope, unloadRequestId, voiceEventFrom, } from "./commands";
|
|
15
15
|
import { getCastleEmbed } from "./context";
|
|
16
16
|
import { CastleError } from "./errors";
|
|
17
17
|
import { cancelLocalCommand, releaseLocalCommand, sendLocalCommand } from "./runtime";
|
|
@@ -23,6 +23,9 @@ const INTERACTIVE_COMMANDS = new Set([
|
|
|
23
23
|
"pass.offer",
|
|
24
24
|
"snapshot.offer",
|
|
25
25
|
"portal.open",
|
|
26
|
+
"multiplayer.joinDestination",
|
|
27
|
+
// The host's voice prompt and the browser's microphone permission.
|
|
28
|
+
"voice.requestJoin",
|
|
26
29
|
]);
|
|
27
30
|
// Request ids carry a per-page prefix so that a host serving several decks at
|
|
28
31
|
// once can key a cancel by requestId alone.
|
|
@@ -31,6 +34,7 @@ let nextRequestId = 1;
|
|
|
31
34
|
const pending = new Map();
|
|
32
35
|
let listenersInstalled = false;
|
|
33
36
|
let unloadHandler = null;
|
|
37
|
+
let voiceEventHandler = null;
|
|
34
38
|
export async function hostRequest(command, params, stream = {}) {
|
|
35
39
|
try {
|
|
36
40
|
const env = await sendCommand(resolveChannel(), command, params, stream);
|
|
@@ -93,6 +97,12 @@ export function postUnloadDone(unloadId, state) {
|
|
|
93
97
|
export function setUnloadHandler(handler) {
|
|
94
98
|
unloadHandler = handler;
|
|
95
99
|
}
|
|
100
|
+
// Registered by multiplayerVoice.ts, the same way, for host voice events.
|
|
101
|
+
export function setVoiceEventHandler(handler) {
|
|
102
|
+
voiceEventHandler = handler;
|
|
103
|
+
if (handler)
|
|
104
|
+
installHostListener();
|
|
105
|
+
}
|
|
96
106
|
function resolveChannel() {
|
|
97
107
|
if (typeof window === "undefined")
|
|
98
108
|
return "local";
|
|
@@ -218,6 +228,11 @@ function receive(message) {
|
|
|
218
228
|
deliverStream(message.requestId, message.stream);
|
|
219
229
|
return;
|
|
220
230
|
}
|
|
231
|
+
const voiceEvent = voiceEventFrom(message);
|
|
232
|
+
if (voiceEvent) {
|
|
233
|
+
voiceEventHandler?.(voiceEvent);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
221
236
|
const unloadId = unloadRequestId(message);
|
|
222
237
|
if (unloadId !== null)
|
|
223
238
|
unloadHandler?.(unloadId);
|