castle-web-sdk 0.4.45 → 0.4.46
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 +449 -53
- package/dist/agent.js +4 -4
- package/dist/avatar.d.ts +12 -0
- package/dist/avatar.js +23 -0
- package/dist/castle.d.ts +10 -1
- package/dist/castle.js +5 -0
- package/dist/commands.d.ts +223 -0
- package/dist/commands.js +16 -0
- package/dist/context.d.ts +6 -0
- package/dist/experiments.d.ts +15 -0
- package/dist/experiments.js +83 -0
- package/dist/hub.d.ts +85 -0
- package/dist/hub.js +107 -0
- package/dist/multiplayer.d.ts +86 -6
- package/dist/multiplayer.js +209 -8
- package/dist/save.d.ts +197 -0
- package/dist/save.js +525 -0
- package/dist/saveCoordinator.d.ts +39 -0
- package/dist/saveCoordinator.js +252 -0
- package/dist/server/hub.d.ts +169 -0
- package/dist/server/hub.js +125 -0
- package/dist/server/platformHandle.d.ts +49 -2
- package/dist/server/storage.d.ts +58 -4
- package/dist/server/storage.js +25 -5
- package/dist/server/wrapper.d.ts +89 -6
- package/dist/server/wrapper.js +395 -30
- package/dist/store.d.ts +3 -2
- package/dist/store.js +5 -0
- package/dist/transport.d.ts +2 -1
- package/dist/transport.js +13 -1
- package/package.json +1 -1
package/dist/castle.d.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
export { isEdit } from "./context";
|
|
2
2
|
export { Agent } from "./agent";
|
|
3
3
|
export type { AgentChatDelta, AgentChatParams, AgentChatResult, AgentMessage, AgentReasoning, AgentTool, AgentToolCall, AgentToolChoice, AgentUsage, CastleAgentApi, } from "./agent";
|
|
4
|
+
export { Avatar } from "./avatar";
|
|
5
|
+
export type { AvatarResult, CastleAvatarApi } from "./avatar";
|
|
4
6
|
export { Characters } from "./characters";
|
|
5
7
|
export type { CastleCharactersApi, CharacterHandle, CharacterHistoryOptions, CharacterSendInput, } from "./characters";
|
|
6
8
|
export { Decks } from "./decks";
|
|
7
9
|
export type { CastleDecksApi, DeckSummary, DecksListParams, DecksListResult } from "./decks";
|
|
8
10
|
export { CastleError } from "./errors";
|
|
11
|
+
export { experiment, experiments, experimentsReady } from "./experiments";
|
|
9
12
|
export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
|
|
10
13
|
export { Haptics } from "./haptics";
|
|
11
14
|
export type { CastleHapticsApi, HapticsResult, HapticsStatus, HapticStyle } from "./haptics";
|
|
15
|
+
export { Hub } from "./hub";
|
|
16
|
+
export type { CastleHubApi, HubBackResult, HubCancelResult, HubCancelStatus, HubChatMessage, HubChatResult, HubDoor, HubDoorsResult, HubFriend, HubFriendsResult, HubGoParams, HubGoResult, HubHandoffResult, HubHandoffStatus, HubJumpResult, HubPlacementKind, HubReportResult, HubSayResult, } from "./hub";
|
|
12
17
|
export { Leaderboard } from "./leaderboard";
|
|
13
18
|
export type { LeaderboardData, LeaderboardEntry, LeaderboardOptions, LeaderboardScope, LeaderboardShowOptions, LeaderboardShowResult, LeaderboardShowStatus, LeaderboardSort, } from "./leaderboard";
|
|
14
19
|
export { Lifecycle } from "./lifecycle";
|
|
15
20
|
export type { CastleLifecycleApi } from "./lifecycle";
|
|
16
21
|
export { Multiplayer } from "./multiplayer";
|
|
17
|
-
export type { CastleMultiplayerApi, MultiplayerConnection, MultiplayerConnectionState, MultiplayerDestinationJoinResult, MultiplayerDestinationOptions, MultiplayerJoinHint, MultiplayerJoinOptions, MultiplayerRoster, MultiplayerStateMetadata, } from "./multiplayer";
|
|
22
|
+
export type { CastleMultiplayerApi, MultiplayerConnection, MultiplayerConnectionState, MultiplayerCompleteScheduledBeatCommand, MultiplayerCompleteScheduledBeatResult, MultiplayerDestinationJoinResult, MultiplayerDestinationOptions, MultiplayerEvaluateBeatsCommand, MultiplayerEvaluateBeatsResult, MultiplayerJoinHint, MultiplayerJoinOptions, MultiplayerRoster, MultiplayerScheduledBeatReason, MultiplayerScheduledBeatSelection, MultiplayerStateMetadata, MultiplayerStoryOpportunity, MultiplayerWorldCharacterCommand, MultiplayerWorldCharacterCommandResult, MultiplayerWorldCharacterCommandResultValue, } from "./multiplayer";
|
|
18
23
|
export type { MultiplayerVoice, VoiceJoinStatus, VoiceListener, VoiceMixEntry, VoiceSpatialOptions, VoiceState, VoiceVector, } from "./multiplayerVoice";
|
|
19
24
|
export type { MultiplayerDestinationAudience, MultiplayerSoloReason } from "./commands";
|
|
20
25
|
export { MAX_CLIENT_FRAME_BYTES, MAX_PLAYERS_CAP } from "./multiplayerProtocol";
|
|
@@ -41,3 +46,7 @@ export type { CastleClockZone, CastleDateParts, CastleTimeApi } from "./time";
|
|
|
41
46
|
export type { Json } from "./types";
|
|
42
47
|
export { User } from "./user";
|
|
43
48
|
export type { CastleUser, CastleUserApi } from "./user";
|
|
49
|
+
export { createSaveCoordinator } from "./saveCoordinator";
|
|
50
|
+
export type { CoordinatedChange, CoordinatedSave, SaveCoordinator, SaveStatus, } from "./saveCoordinator";
|
|
51
|
+
export { SaveError } from "./save";
|
|
52
|
+
export type { SaveApi, SaveBucketApi, SaveBucketRead, SaveChange, SaveCheck, SaveClaimed, SaveClaimInput, SaveErrorEnvelope, SaveInitial, SaveInitializeInput, SaveOpened, SaveOperation, SaveReadTarget, SaveReceipt, SaveRevision, SaveRows, SaveScope, SaveSelection, SaveSnapshot, SaveTarget, SaveTransactionInput, SaveWriter, } from "./save";
|
package/dist/castle.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// Castle Web SDK
|
|
2
2
|
export { isEdit } from "./context";
|
|
3
3
|
export { Agent } from "./agent";
|
|
4
|
+
export { Avatar } from "./avatar";
|
|
4
5
|
export { Characters } from "./characters";
|
|
5
6
|
export { Decks } from "./decks";
|
|
6
7
|
export { CastleError } from "./errors";
|
|
8
|
+
export { experiment, experiments, experimentsReady } from "./experiments";
|
|
7
9
|
export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
|
|
8
10
|
export { Haptics } from "./haptics";
|
|
11
|
+
export { Hub } from "./hub";
|
|
9
12
|
export { Leaderboard } from "./leaderboard";
|
|
10
13
|
export { Lifecycle } from "./lifecycle";
|
|
11
14
|
export { Multiplayer } from "./multiplayer";
|
|
@@ -21,3 +24,5 @@ export { SharedStorage, Storage } from "./storage";
|
|
|
21
24
|
export { Store } from "./store";
|
|
22
25
|
export { Time } from "./time";
|
|
23
26
|
export { User } from "./user";
|
|
27
|
+
export { createSaveCoordinator } from "./saveCoordinator";
|
|
28
|
+
export { SaveError } from "./save";
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SaveClaimed, SaveClaimInput, SaveInitializeInput, SaveOpened, SaveOperation, SaveReadTarget, SaveReceipt, SaveSnapshot, SaveTransactionInput } from "./save";
|
|
1
2
|
import type { Json } from "./types";
|
|
2
3
|
export declare const CASTLE_SDK_PROTOCOL = 1;
|
|
3
4
|
export type StorageBlob = Record<string, string>;
|
|
@@ -83,6 +84,174 @@ export type PortalInfoResult = {
|
|
|
83
84
|
export interface PortalPrefetchResult {
|
|
84
85
|
status: PortalPrefetchStatus;
|
|
85
86
|
}
|
|
87
|
+
export interface HubGoParams {
|
|
88
|
+
doorId: string;
|
|
89
|
+
}
|
|
90
|
+
export type HubPlacementKind = "layer" | "pad" | "exact";
|
|
91
|
+
export type HubHandoffStatus = "ok" | "cancelled" | "full" | "unavailable" | "not_found" | "forbidden" | "party_required";
|
|
92
|
+
/** Session acquisition, not a completed scene swap or socket join. */
|
|
93
|
+
export type HubHandoffResult = {
|
|
94
|
+
status: "ok";
|
|
95
|
+
/** Destination session WebSocket URL, as in multiplayer.getSession. */
|
|
96
|
+
url: string;
|
|
97
|
+
/** Single-use join nonce, valid for 60 s. */
|
|
98
|
+
nonce: string;
|
|
99
|
+
sessionId: string;
|
|
100
|
+
/** The destination session's pinned server bundle (sha256). */
|
|
101
|
+
bundleHash: string;
|
|
102
|
+
/** Immutable URL of the content pack `bundleHash` names; null when the
|
|
103
|
+
* destination has none. */
|
|
104
|
+
contentUrl: string | null;
|
|
105
|
+
kind?: undefined;
|
|
106
|
+
} | {
|
|
107
|
+
/**
|
|
108
|
+
* A cabinet: the door leads to a classic Castle deck, which the host plays
|
|
109
|
+
* itself. There is no session to join; the trip is already committed, and
|
|
110
|
+
* `Hub.back()` is the way out.
|
|
111
|
+
*/
|
|
112
|
+
status: "ok";
|
|
113
|
+
kind: "classic";
|
|
114
|
+
url: null;
|
|
115
|
+
nonce: null;
|
|
116
|
+
sessionId: null;
|
|
117
|
+
bundleHash: string;
|
|
118
|
+
contentUrl: null;
|
|
119
|
+
} | {
|
|
120
|
+
status: Exclude<HubHandoffStatus, "ok">;
|
|
121
|
+
url: null;
|
|
122
|
+
nonce: null;
|
|
123
|
+
sessionId: null;
|
|
124
|
+
bundleHash: null;
|
|
125
|
+
contentUrl: null;
|
|
126
|
+
};
|
|
127
|
+
export type HubGoResult = HubHandoffResult;
|
|
128
|
+
export type HubBackResult = HubHandoffResult;
|
|
129
|
+
export type HubJumpResult = HubHandoffResult;
|
|
130
|
+
export interface HubJumpParams {
|
|
131
|
+
userId: string;
|
|
132
|
+
}
|
|
133
|
+
/** A mutual friend ghost shows in a layer of the current place right now. */
|
|
134
|
+
export interface HubFriend {
|
|
135
|
+
userId: string;
|
|
136
|
+
username: string | null;
|
|
137
|
+
/** In the layer the player is in already: nothing to jump to. */
|
|
138
|
+
here: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Set when the friend is not in this place but inside the place behind one
|
|
141
|
+
* of its doors: that door's id. Null for a friend in a layer of this place.
|
|
142
|
+
*/
|
|
143
|
+
door: string | null;
|
|
144
|
+
}
|
|
145
|
+
/** Hub.report: another player, by user id, and an optional short reason. */
|
|
146
|
+
export interface HubReportParams {
|
|
147
|
+
userId: string;
|
|
148
|
+
reason?: string;
|
|
149
|
+
}
|
|
150
|
+
/** `unavailable` on hosts without hub support or outside a hub session. */
|
|
151
|
+
export interface HubReportResult {
|
|
152
|
+
status: "ok" | "unavailable";
|
|
153
|
+
}
|
|
154
|
+
export type HubFriendsResult = {
|
|
155
|
+
status: "ok";
|
|
156
|
+
friends: HubFriend[];
|
|
157
|
+
} | {
|
|
158
|
+
status: "unavailable";
|
|
159
|
+
friends: null;
|
|
160
|
+
};
|
|
161
|
+
/** Hub.chat: only the messages after a cursor an earlier answer returned. */
|
|
162
|
+
export interface HubChatParams {
|
|
163
|
+
after?: string;
|
|
164
|
+
}
|
|
165
|
+
/** One chat message ghost lets this player read. */
|
|
166
|
+
export interface HubChatMessage {
|
|
167
|
+
id: string;
|
|
168
|
+
fromUserId: string;
|
|
169
|
+
fromUsername: string | null;
|
|
170
|
+
/** At most 200 characters. */
|
|
171
|
+
text: string;
|
|
172
|
+
/** When ghost took it, as an ISO 8601 time. */
|
|
173
|
+
at: string;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* `canChat` is whether ghost lets this player chat at all; `peers` are the
|
|
177
|
+
* user ids in this hub session who would be sent what they say. `unavailable`
|
|
178
|
+
* on hosts without hub support or outside a hub session.
|
|
179
|
+
*/
|
|
180
|
+
export type HubChatResult = {
|
|
181
|
+
status: "ok";
|
|
182
|
+
canChat: boolean;
|
|
183
|
+
peers: string[];
|
|
184
|
+
messages: HubChatMessage[];
|
|
185
|
+
/** Pass to the next `Hub.chat` to read only what is newer. */
|
|
186
|
+
cursor: string | null;
|
|
187
|
+
} | {
|
|
188
|
+
status: "unavailable";
|
|
189
|
+
canChat: false;
|
|
190
|
+
peers: null;
|
|
191
|
+
messages: null;
|
|
192
|
+
cursor: null;
|
|
193
|
+
};
|
|
194
|
+
/** Hub.say: free text, at most 200 characters once trimmed. */
|
|
195
|
+
export interface HubSayParams {
|
|
196
|
+
text: string;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* `sent` carries the message's id; `none` when nobody in the session may read
|
|
200
|
+
* it, `refused` when ghost will not take it, `limited` when the player is
|
|
201
|
+
* saying too much too fast. `unavailable` on hosts without hub support or
|
|
202
|
+
* outside a hub session.
|
|
203
|
+
*/
|
|
204
|
+
export interface HubSayResult {
|
|
205
|
+
status: "sent" | "none" | "refused" | "limited" | "unavailable";
|
|
206
|
+
id: string | null;
|
|
207
|
+
}
|
|
208
|
+
/** Hub.reportChat: a message this player was returned, by its id. */
|
|
209
|
+
export interface HubReportChatParams {
|
|
210
|
+
messageId: string;
|
|
211
|
+
}
|
|
212
|
+
/** Public door row; destination identity and return tickets stay host-owned. */
|
|
213
|
+
export interface HubDoor {
|
|
214
|
+
doorId: string;
|
|
215
|
+
launcherKind: string;
|
|
216
|
+
placementKind: HubPlacementKind;
|
|
217
|
+
/** The row's placement data: `{}`, or `{ key }` for an exact door. */
|
|
218
|
+
placement: Json;
|
|
219
|
+
padSize: number | null;
|
|
220
|
+
enabled: boolean;
|
|
221
|
+
featured: boolean;
|
|
222
|
+
/** Ghost-owned presence, not a count maintained by the place's engine. */
|
|
223
|
+
status: {
|
|
224
|
+
playersInside: number;
|
|
225
|
+
friendsInside: number;
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
export type HubDoorsResult = {
|
|
229
|
+
status: "ok";
|
|
230
|
+
doors: HubDoor[];
|
|
231
|
+
} | {
|
|
232
|
+
status: "unavailable";
|
|
233
|
+
doors: null;
|
|
234
|
+
};
|
|
235
|
+
export interface HubCommitParams {
|
|
236
|
+
/** The acquisition's nonce: unique to it, unlike its session. */
|
|
237
|
+
nonce: string;
|
|
238
|
+
}
|
|
239
|
+
export type HubCommitStatus = "committed" | "expired" | "superseded" | "cancelled" | "none" | "unavailable";
|
|
240
|
+
export interface HubCommitResult {
|
|
241
|
+
status: HubCommitStatus;
|
|
242
|
+
}
|
|
243
|
+
export type HubCancelStatus = "cancelled" | "expired" | "none" | "unavailable";
|
|
244
|
+
export interface HubCancelResult {
|
|
245
|
+
status: HubCancelStatus;
|
|
246
|
+
}
|
|
247
|
+
export type HubEvent = {
|
|
248
|
+
type: "back";
|
|
249
|
+
};
|
|
250
|
+
export interface HubEventEnvelope {
|
|
251
|
+
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
252
|
+
hub: HubEvent;
|
|
253
|
+
}
|
|
254
|
+
export declare function hubEventFrom(value: unknown): HubEvent | null;
|
|
86
255
|
export type HapticStyle = "light" | "medium" | "heavy" | "selection" | "success" | "warning" | "error";
|
|
87
256
|
export type HapticsStatus = "triggered" | "unavailable";
|
|
88
257
|
export interface HapticsResult {
|
|
@@ -331,10 +500,15 @@ export interface CommandParams {
|
|
|
331
500
|
"user.get": {
|
|
332
501
|
userId: string;
|
|
333
502
|
};
|
|
503
|
+
"experiments.get": Record<string, never>;
|
|
334
504
|
"time.getServerTime": Record<string, never>;
|
|
335
505
|
"pass.has": {
|
|
336
506
|
passId: string;
|
|
337
507
|
};
|
|
508
|
+
"avatar.get": Record<string, never>;
|
|
509
|
+
"avatar.set": {
|
|
510
|
+
avatar: number;
|
|
511
|
+
};
|
|
338
512
|
"pass.offer": {
|
|
339
513
|
passId: string;
|
|
340
514
|
};
|
|
@@ -352,6 +526,19 @@ export interface CommandParams {
|
|
|
352
526
|
"portal.info": {
|
|
353
527
|
targetDeckId: string;
|
|
354
528
|
};
|
|
529
|
+
"hub.go": HubGoParams;
|
|
530
|
+
"hub.back": Record<string, never>;
|
|
531
|
+
"hub.doors": Record<string, never>;
|
|
532
|
+
"hub.friends": Record<string, never>;
|
|
533
|
+
"hub.jump": HubJumpParams;
|
|
534
|
+
"hub.report": HubReportParams;
|
|
535
|
+
"hub.chat": HubChatParams;
|
|
536
|
+
"hub.say": HubSayParams;
|
|
537
|
+
"hub.reportChat": HubReportChatParams;
|
|
538
|
+
"hub.commit": HubCommitParams;
|
|
539
|
+
"hub.cancel": {
|
|
540
|
+
nonce?: string;
|
|
541
|
+
};
|
|
355
542
|
"haptics.play": {
|
|
356
543
|
style: HapticStyle;
|
|
357
544
|
};
|
|
@@ -398,6 +585,15 @@ export interface CommandParams {
|
|
|
398
585
|
characterId: string;
|
|
399
586
|
operationId: string;
|
|
400
587
|
};
|
|
588
|
+
"cauldronStorage.snapshot": {
|
|
589
|
+
targets: SaveReadTarget[];
|
|
590
|
+
};
|
|
591
|
+
"cauldronStorage.transaction": SaveTransactionInput;
|
|
592
|
+
"cauldronStorage.claim": SaveClaimInput;
|
|
593
|
+
"cauldronStorage.initialize": SaveInitializeInput;
|
|
594
|
+
"cauldronStorage.operation": {
|
|
595
|
+
operationId: string;
|
|
596
|
+
};
|
|
401
597
|
"cauldronStorage.get": {
|
|
402
598
|
scope: StoreScope;
|
|
403
599
|
subject?: string | null;
|
|
@@ -482,6 +678,9 @@ export interface CommandResult {
|
|
|
482
678
|
"user.get": {
|
|
483
679
|
user: UserPayload | null;
|
|
484
680
|
};
|
|
681
|
+
"experiments.get": {
|
|
682
|
+
variants: Record<string, string>;
|
|
683
|
+
};
|
|
485
684
|
"time.getServerTime": {
|
|
486
685
|
timestamp: number;
|
|
487
686
|
timezoneOffset: number;
|
|
@@ -490,11 +689,24 @@ export interface CommandResult {
|
|
|
490
689
|
"pass.has": {
|
|
491
690
|
hasPass: boolean;
|
|
492
691
|
};
|
|
692
|
+
"avatar.get": AvatarResult;
|
|
693
|
+
"avatar.set": AvatarResult;
|
|
493
694
|
"pass.offer": PassOfferResult;
|
|
494
695
|
"snapshot.offer": SnapshotOfferResult;
|
|
495
696
|
"portal.open": PortalOpenResult;
|
|
496
697
|
"portal.prefetch": PortalPrefetchResult;
|
|
497
698
|
"portal.info": PortalInfoResult;
|
|
699
|
+
"hub.go": HubGoResult;
|
|
700
|
+
"hub.back": HubBackResult;
|
|
701
|
+
"hub.doors": HubDoorsResult;
|
|
702
|
+
"hub.friends": HubFriendsResult;
|
|
703
|
+
"hub.jump": HubJumpResult;
|
|
704
|
+
"hub.report": HubReportResult;
|
|
705
|
+
"hub.chat": HubChatResult;
|
|
706
|
+
"hub.say": HubSayResult;
|
|
707
|
+
"hub.reportChat": HubReportResult;
|
|
708
|
+
"hub.commit": HubCommitResult;
|
|
709
|
+
"hub.cancel": HubCancelResult;
|
|
498
710
|
"haptics.play": HapticsResult;
|
|
499
711
|
"multiplayer.getSession": MultiplayerGetSessionResult;
|
|
500
712
|
"multiplayer.getJoinHint": MultiplayerJoinHint | null;
|
|
@@ -519,6 +731,11 @@ export interface CommandResult {
|
|
|
519
731
|
"characters.status": CharacterOperation;
|
|
520
732
|
"characters.history": CharacterHistory;
|
|
521
733
|
"characters.reset": CharacterAdmission;
|
|
734
|
+
"cauldronStorage.snapshot": SaveSnapshot;
|
|
735
|
+
"cauldronStorage.transaction": SaveReceipt;
|
|
736
|
+
"cauldronStorage.claim": SaveClaimed;
|
|
737
|
+
"cauldronStorage.initialize": SaveOpened;
|
|
738
|
+
"cauldronStorage.operation": SaveOperation;
|
|
522
739
|
"cauldronStorage.get": {
|
|
523
740
|
entries: StoreEntry[];
|
|
524
741
|
};
|
|
@@ -557,6 +774,12 @@ export interface UserPayload {
|
|
|
557
774
|
frameUrl: string | null;
|
|
558
775
|
color: string | null;
|
|
559
776
|
}
|
|
777
|
+
export interface AvatarResult {
|
|
778
|
+
status: "ok" | "unavailable";
|
|
779
|
+
avatar: number | null;
|
|
780
|
+
/** False while the player wears the default ghost gives every account. */
|
|
781
|
+
chosen: boolean;
|
|
782
|
+
}
|
|
560
783
|
export type CommandName = keyof CommandParams;
|
|
561
784
|
export declare const COMMAND_TIMEOUTS_MS: Partial<Record<CommandName, number>>;
|
|
562
785
|
export declare const STREAM_IDLE_TIMEOUT_MS = 60000;
|
package/dist/commands.js
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
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
|
+
export function hubEventFrom(value) {
|
|
10
|
+
if (typeof value !== "object" || value === null)
|
|
11
|
+
return null;
|
|
12
|
+
const record = value;
|
|
13
|
+
if (record.castleSdk !== CASTLE_SDK_PROTOCOL)
|
|
14
|
+
return null;
|
|
15
|
+
const event = record.hub;
|
|
16
|
+
if (typeof event !== "object" || event === null)
|
|
17
|
+
return null;
|
|
18
|
+
return event.type === "back" ? { type: "back" } : null;
|
|
19
|
+
}
|
|
9
20
|
// How long the deck waits for a reply to these commands, in place of the
|
|
10
21
|
// data-command timeout. A deck list fetches every cover on the list. A host
|
|
11
22
|
// from before streaming reads a whole model reply before answering agent.chat.
|
|
@@ -13,6 +24,11 @@ export const COMMAND_TIMEOUTS_MS = {
|
|
|
13
24
|
"agent.chat": 180_000,
|
|
14
25
|
"decks.mine": 30_000,
|
|
15
26
|
"decks.saved": 30_000,
|
|
27
|
+
// A door used right after arriving can wait out ghost's roster lag (up to
|
|
28
|
+
// 15 s) before the host gives up on it.
|
|
29
|
+
"hub.go": 30_000,
|
|
30
|
+
"hub.back": 30_000,
|
|
31
|
+
"hub.jump": 30_000,
|
|
16
32
|
};
|
|
17
33
|
// Once a host streams a command, each stream envelope restarts the wait at this
|
|
18
34
|
// length instead. The deck-agent service sends a heartbeat every 15 seconds, and the host
|
package/dist/context.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface CastleEmbed {
|
|
|
7
7
|
*/
|
|
8
8
|
feed?: boolean;
|
|
9
9
|
host?: CastleHost;
|
|
10
|
+
/**
|
|
11
|
+
* The player's A/B arms on this deck, `{ experimentName: variant }`, when the
|
|
12
|
+
* host hands them over with the page. A host that does not answers the
|
|
13
|
+
* `experiments.get` command instead (experiments.ts).
|
|
14
|
+
*/
|
|
15
|
+
experiments?: Record<string, string>;
|
|
10
16
|
}
|
|
11
17
|
declare global {
|
|
12
18
|
interface Window {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The variant this player has in `name`, or `null` when they are in no such
|
|
3
|
+
* experiment. Synchronous; see `experimentsReady` for the one case where a read
|
|
4
|
+
* can come before the answer.
|
|
5
|
+
*/
|
|
6
|
+
export declare function experiment(name: string): string | null;
|
|
7
|
+
/** Every experiment this player is in on this deck, `{ name: variant }`. */
|
|
8
|
+
export declare function experiments(): Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* Resolves, with the same map `experiments()` returns, once the host has
|
|
11
|
+
* answered. Await it once before the first read. On a host that hands the
|
|
12
|
+
* answer over with the page it is already resolved. Never rejects: a host that
|
|
13
|
+
* cannot answer means no experiments.
|
|
14
|
+
*/
|
|
15
|
+
export declare function experimentsReady(): Promise<Record<string, string>>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// A/B experiments. Castle's server gives each player one variant of every
|
|
2
|
+
// experiment this deck is in (castle.json `experiments`, plus any Castle runs
|
|
3
|
+
// across decks and lists this deck in), the same one on every load, and resolves
|
|
4
|
+
// them once per deck load. The deck only ever reads the answer.
|
|
5
|
+
//
|
|
6
|
+
// The reads are synchronous. A host that builds the deck's document puts the
|
|
7
|
+
// answer in `CastleEmbed.experiments`, and it is there from the first line the
|
|
8
|
+
// deck runs. Any other host answers one `experiments.get` command instead, so a
|
|
9
|
+
// deck awaits `experimentsReady()` once at boot and reads synchronously after.
|
|
10
|
+
//
|
|
11
|
+
// `null` is a normal answer: no such experiment, one that has ended, a player
|
|
12
|
+
// who is not in it, or a host that predates experiments. Always handle it.
|
|
13
|
+
import { getCastleEmbed } from "./context";
|
|
14
|
+
import { hostRequest } from "./transport";
|
|
15
|
+
const NONE = Object.freeze({});
|
|
16
|
+
let variants = null;
|
|
17
|
+
let loading = null;
|
|
18
|
+
let warnedEarlyRead = false;
|
|
19
|
+
/**
|
|
20
|
+
* The variant this player has in `name`, or `null` when they are in no such
|
|
21
|
+
* experiment. Synchronous; see `experimentsReady` for the one case where a read
|
|
22
|
+
* can come before the answer.
|
|
23
|
+
*/
|
|
24
|
+
export function experiment(name) {
|
|
25
|
+
const known = current();
|
|
26
|
+
return Object.hasOwn(known, name) ? known[name] : null;
|
|
27
|
+
}
|
|
28
|
+
/** Every experiment this player is in on this deck, `{ name: variant }`. */
|
|
29
|
+
export function experiments() {
|
|
30
|
+
return { ...current() };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolves, with the same map `experiments()` returns, once the host has
|
|
34
|
+
* answered. Await it once before the first read. On a host that hands the
|
|
35
|
+
* answer over with the page it is already resolved. Never rejects: a host that
|
|
36
|
+
* cannot answer means no experiments.
|
|
37
|
+
*/
|
|
38
|
+
export function experimentsReady() {
|
|
39
|
+
return load().then((known) => ({ ...known }));
|
|
40
|
+
}
|
|
41
|
+
function current() {
|
|
42
|
+
if (variants)
|
|
43
|
+
return variants;
|
|
44
|
+
const embedded = fromEmbed();
|
|
45
|
+
if (embedded) {
|
|
46
|
+
variants = embedded;
|
|
47
|
+
return variants;
|
|
48
|
+
}
|
|
49
|
+
if (!loading && !warnedEarlyRead) {
|
|
50
|
+
warnedEarlyRead = true;
|
|
51
|
+
console.warn("Castle experiment() was read before the host answered, so it returned null. " +
|
|
52
|
+
"Call `await experimentsReady()` once at boot, before the first read.");
|
|
53
|
+
}
|
|
54
|
+
// Asked for now, so a later read has the answer even without the await.
|
|
55
|
+
void load();
|
|
56
|
+
return NONE;
|
|
57
|
+
}
|
|
58
|
+
function load() {
|
|
59
|
+
if (variants)
|
|
60
|
+
return Promise.resolve(variants);
|
|
61
|
+
const embedded = fromEmbed();
|
|
62
|
+
if (embedded) {
|
|
63
|
+
variants = embedded;
|
|
64
|
+
return Promise.resolve(variants);
|
|
65
|
+
}
|
|
66
|
+
loading ??= hostRequest("experiments.get", {})
|
|
67
|
+
.then((result) => clean(result?.variants) ?? NONE)
|
|
68
|
+
// A host from before experiments does not know the command.
|
|
69
|
+
.catch(() => NONE)
|
|
70
|
+
.then((known) => {
|
|
71
|
+
variants = known;
|
|
72
|
+
return known;
|
|
73
|
+
});
|
|
74
|
+
return loading;
|
|
75
|
+
}
|
|
76
|
+
function fromEmbed() {
|
|
77
|
+
return clean(getCastleEmbed()?.experiments);
|
|
78
|
+
}
|
|
79
|
+
function clean(value) {
|
|
80
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
81
|
+
return null;
|
|
82
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string")));
|
|
83
|
+
}
|
package/dist/hub.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { HubBackResult, HubCancelResult, HubChatResult, HubDoorsResult, HubFriendsResult, HubGoResult, HubJumpResult, HubReportResult, HubSayResult } from "./commands";
|
|
2
|
+
export type { HubBackResult, HubCancelResult, HubCancelStatus, HubChatMessage, HubChatResult, HubDoor, HubDoorsResult, HubFriend, HubFriendsResult, HubGoParams, HubGoResult, HubHandoffResult, HubHandoffStatus, HubJumpResult, HubPlacementKind, HubReportResult, HubSayResult, } from "./commands";
|
|
3
|
+
/**
|
|
4
|
+
* Doors between hub places. Every hub-kit deck is a place; ghost's door table
|
|
5
|
+
* is the allowlist, and the host supplies real authentication and the current
|
|
6
|
+
* place, session and party. A deck names only a door, never a destination
|
|
7
|
+
* deck, session or party.
|
|
8
|
+
*
|
|
9
|
+
* Going through a door is acquire, then join: `go` (or `back`) acquires the
|
|
10
|
+
* destination and changes nothing else; the deck loads the destination's
|
|
11
|
+
* content behind its door animation and joins with
|
|
12
|
+
* `Multiplayer.join({ mode: "hub", handoff })`. When that socket says hello the
|
|
13
|
+
* SDK tells the host, which commits the trip: presence moves and the return
|
|
14
|
+
* ticket is written. A failure before then leaves the player where they were.
|
|
15
|
+
*/
|
|
16
|
+
export interface CastleHubApi {
|
|
17
|
+
/**
|
|
18
|
+
* Acquire the destination behind a door of the current place. A door used
|
|
19
|
+
* within seconds of arriving may take up to 15 s while ghost catches up with
|
|
20
|
+
* the arrival. `unavailable` on hosts without hub support.
|
|
21
|
+
*/
|
|
22
|
+
go(doorId: string): Promise<HubGoResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Acquire the way back through the door this player came in by: the layer
|
|
25
|
+
* left behind if it has room, else the party's, else the usual pick. The deck
|
|
26
|
+
* cannot choose the return place. `not_found` when there is no way back.
|
|
27
|
+
*/
|
|
28
|
+
back(): Promise<HubBackResult>;
|
|
29
|
+
/** The current place's door rows and who is inside, read at boot. */
|
|
30
|
+
doors(): Promise<HubDoorsResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Mutual friends in a layer of the current place right now (at most 20);
|
|
33
|
+
* `here` is true for those in the player's own layer. Ghost shows only the
|
|
34
|
+
* friends whose now-playing privacy lets this player see them. `unavailable`
|
|
35
|
+
* on hosts without hub support.
|
|
36
|
+
*/
|
|
37
|
+
friends(): Promise<HubFriendsResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Acquire the layer of this place a friend is in. The destination is this
|
|
40
|
+
* same place in another session; join it exactly like a door's
|
|
41
|
+
* (`Multiplayer.join({ mode: "hub", handoff })`), which commits it. The way
|
|
42
|
+
* back is unchanged. `not_found` when the friend is not in another layer.
|
|
43
|
+
*/
|
|
44
|
+
jump(userId: string): Promise<HubJumpResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Report another player in this hub session to Castle's moderators, with an
|
|
47
|
+
* optional short reason. Hub chrome only: the host fills in the place and
|
|
48
|
+
* session. `unavailable` on hosts without hub support.
|
|
49
|
+
*/
|
|
50
|
+
report(userId: string, reason?: string): Promise<HubReportResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Read this hub session's chat: the messages this player may read, whether
|
|
53
|
+
* they may chat at all, and the user ids who would be sent what they say.
|
|
54
|
+
* Who may chat is ghost's rule (players who follow each other, both over
|
|
55
|
+
* 13), never the deck's. Poll it, passing the last answer's `cursor` as
|
|
56
|
+
* `after` for only what is newer. `unavailable` on hosts without hub support
|
|
57
|
+
* or outside a hub session.
|
|
58
|
+
*/
|
|
59
|
+
chat(after?: string): Promise<HubChatResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Say something in this hub session's chat (at most 200 characters). The
|
|
62
|
+
* deck cannot choose who is sent it: ghost sends it to the players here who
|
|
63
|
+
* may chat with this one. `none` when there is nobody, `refused` when ghost
|
|
64
|
+
* will not take it, `limited` when said too fast.
|
|
65
|
+
*/
|
|
66
|
+
say(text: string): Promise<HubSayResult>;
|
|
67
|
+
/**
|
|
68
|
+
* Report a chat message this player was returned to Castle's moderators, by
|
|
69
|
+
* its id. Hub chrome only: the host fills in the place and session.
|
|
70
|
+
* `unavailable` on hosts without hub support or outside a hub session.
|
|
71
|
+
*/
|
|
72
|
+
reportChat(messageId: string): Promise<HubReportResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Abandon an acquisition the deck will not join (its content failed to load,
|
|
75
|
+
* the player walked away). Optional: an unjoined one expires on its own.
|
|
76
|
+
*/
|
|
77
|
+
cancel(): Promise<HubCancelResult>;
|
|
78
|
+
/**
|
|
79
|
+
* The player pressed the host's Back control, which a game cannot hide.
|
|
80
|
+
* Answer it with `back()` and the door animation; a host that hears nothing
|
|
81
|
+
* takes the player home itself. Returns an unsubscribe function.
|
|
82
|
+
*/
|
|
83
|
+
onBack(listener: () => void): () => void;
|
|
84
|
+
}
|
|
85
|
+
export declare const Hub: CastleHubApi;
|
package/dist/hub.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { CastleError } from "./errors";
|
|
2
|
+
import { hostRequest, setHubEventHandler } from "./transport";
|
|
3
|
+
export const Hub = {
|
|
4
|
+
go,
|
|
5
|
+
back,
|
|
6
|
+
doors,
|
|
7
|
+
friends,
|
|
8
|
+
jump,
|
|
9
|
+
report,
|
|
10
|
+
chat,
|
|
11
|
+
say,
|
|
12
|
+
reportChat,
|
|
13
|
+
cancel,
|
|
14
|
+
onBack,
|
|
15
|
+
};
|
|
16
|
+
async function go(doorId) {
|
|
17
|
+
if (typeof doorId !== "string" || doorId.length === 0) {
|
|
18
|
+
throw new CastleError({
|
|
19
|
+
code: "INVALID_ARGUMENT",
|
|
20
|
+
message: "Hub.go requires a doorId.",
|
|
21
|
+
operation: "Hub.go",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return hostRequest("hub.go", { doorId });
|
|
25
|
+
}
|
|
26
|
+
async function back() {
|
|
27
|
+
return hostRequest("hub.back", {});
|
|
28
|
+
}
|
|
29
|
+
async function doors() {
|
|
30
|
+
return hostRequest("hub.doors", {});
|
|
31
|
+
}
|
|
32
|
+
async function friends() {
|
|
33
|
+
return hostRequest("hub.friends", {});
|
|
34
|
+
}
|
|
35
|
+
async function jump(userId) {
|
|
36
|
+
if (typeof userId !== "string" || userId.length === 0) {
|
|
37
|
+
throw new CastleError({
|
|
38
|
+
code: "INVALID_ARGUMENT",
|
|
39
|
+
message: "Hub.jump requires a userId.",
|
|
40
|
+
operation: "Hub.jump",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return hostRequest("hub.jump", { userId });
|
|
44
|
+
}
|
|
45
|
+
async function report(userId, reason) {
|
|
46
|
+
if (typeof userId !== "string" || userId.length === 0) {
|
|
47
|
+
throw new CastleError({
|
|
48
|
+
code: "INVALID_ARGUMENT",
|
|
49
|
+
message: "Hub.report requires a userId.",
|
|
50
|
+
operation: "Hub.report",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return hostRequest("hub.report", typeof reason === "string" && reason.length > 0 ? { userId, reason } : { userId });
|
|
54
|
+
}
|
|
55
|
+
async function chat(after) {
|
|
56
|
+
return hostRequest("hub.chat", typeof after === "string" && after.length > 0 ? { after } : {});
|
|
57
|
+
}
|
|
58
|
+
async function say(text) {
|
|
59
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
60
|
+
throw new CastleError({
|
|
61
|
+
code: "INVALID_ARGUMENT",
|
|
62
|
+
message: "Hub.say requires a text.",
|
|
63
|
+
operation: "Hub.say",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return hostRequest("hub.say", { text });
|
|
67
|
+
}
|
|
68
|
+
async function reportChat(messageId) {
|
|
69
|
+
if (typeof messageId !== "string" || messageId.length === 0) {
|
|
70
|
+
throw new CastleError({
|
|
71
|
+
code: "INVALID_ARGUMENT",
|
|
72
|
+
message: "Hub.reportChat requires a messageId.",
|
|
73
|
+
operation: "Hub.reportChat",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return hostRequest("hub.reportChat", { messageId });
|
|
77
|
+
}
|
|
78
|
+
async function cancel() {
|
|
79
|
+
return hostRequest("hub.cancel", {});
|
|
80
|
+
}
|
|
81
|
+
const backListeners = new Set();
|
|
82
|
+
function onBack(listener) {
|
|
83
|
+
if (typeof listener !== "function") {
|
|
84
|
+
throw new CastleError({
|
|
85
|
+
code: "INVALID_ARGUMENT",
|
|
86
|
+
message: "Hub.onBack requires a function.",
|
|
87
|
+
operation: "Hub.onBack",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
backListeners.add(listener);
|
|
91
|
+
setHubEventHandler(dispatchHubEvent);
|
|
92
|
+
return () => {
|
|
93
|
+
backListeners.delete(listener);
|
|
94
|
+
if (backListeners.size === 0)
|
|
95
|
+
setHubEventHandler(null);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function dispatchHubEvent() {
|
|
99
|
+
for (const listener of [...backListeners]) {
|
|
100
|
+
try {
|
|
101
|
+
listener();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error("Castle Hub.onBack listener threw:", error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|