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.
@@ -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();
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-sdk",
3
- "version": "0.4.35",
3
+ "version": "0.4.37",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/castle-xyz/castle-experimental-web.git"