castle-web-sdk 0.4.44 → 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.
@@ -1,6 +1,7 @@
1
1
  import type { Json } from "../types";
2
2
  import type { CharacterHistory, CharacterOperation } from "../commands";
3
3
  import type { PlatformHandle } from "./platformHandle";
4
+ import { type SaveApi, type SaveBucketApi } from "../save";
4
5
  export interface ServerStorageListOptions {
5
6
  prefix?: string;
6
7
  limit?: number;
@@ -15,7 +16,7 @@ export interface ServerStorageListPage<T extends Json = Json> {
15
16
  /** Null means there is nothing left. A short page alone does not mean that. */
16
17
  cursor: string | null;
17
18
  }
18
- export interface ServerStorageScope {
19
+ export interface ServerStorageScope extends SaveBucketApi {
19
20
  get<T extends Json = Json>(keys: string[]): Promise<Record<string, T>>;
20
21
  set(values: Record<string, Json>): Promise<void>;
21
22
  remove(keys: string[]): Promise<void>;
@@ -95,6 +96,59 @@ export interface DeliverCharacterKnowledgeInput {
95
96
  claimIds: string[];
96
97
  source: CharacterKnowledgeSource;
97
98
  }
99
+ /**
100
+ * A character acting on its own after a command woke it (autonomous character
101
+ * actions, bundles with `characterActions`). `declined` covers choosing
102
+ * nothing, too little confidence for its temperament, and an action no longer
103
+ * eligible when it committed; `failed` is a decision-model error.
104
+ */
105
+ export type CharacterActionNotice = {
106
+ characterId: string;
107
+ /** `id` is the event occurrence, routine, or promise id. */
108
+ trigger: {
109
+ type: "witnessed" | "routine" | "promise_due";
110
+ id: string;
111
+ };
112
+ outcome: "acted" | "declined" | "failed";
113
+ /** When acted. */
114
+ actionId?: string;
115
+ /** When acted with a parameterized action's variant. */
116
+ variantId?: string;
117
+ /** The action's world event, when it has one. */
118
+ eventOccurrenceId?: string;
119
+ /** The decision model's probability for its choice. */
120
+ probability?: number;
121
+ };
122
+ export type CharacterGameStateResult = {
123
+ revision: number;
124
+ state: {
125
+ [key: string]: string | number | boolean;
126
+ };
127
+ promiseOutcomes: Array<{
128
+ id: string;
129
+ status: "fulfilled" | "broken";
130
+ due: number;
131
+ resolvedAtGameTime: number;
132
+ }>;
133
+ /** At most 8, omitted when no character acted, declined, or failed. */
134
+ characterActions?: CharacterActionNotice[];
135
+ };
136
+ export type CharacterGameStateOperation = Omit<CharacterOperation, "result"> & {
137
+ result: CharacterGameStateResult | null;
138
+ };
139
+ export type CharacterKnowledgeDeliveryResult = {
140
+ deliveryId: string;
141
+ recipientCharacterId: string;
142
+ eventOccurrenceId: string;
143
+ claimIds: string[];
144
+ source: CharacterKnowledgeSource;
145
+ deliveredAt: string;
146
+ /** At most 8, omitted when no character acted, declined, or failed. */
147
+ characterActions?: CharacterActionNotice[];
148
+ };
149
+ export type CharacterKnowledgeDeliveryOperation = Omit<CharacterOperation, "result"> & {
150
+ result: CharacterKnowledgeDeliveryResult | null;
151
+ };
98
152
  export interface CompleteCharacterBeatInput {
99
153
  operationId: string;
100
154
  characterId: string;
@@ -189,10 +243,10 @@ export interface ServerPlaythroughCharacters {
189
243
  /** Queues a funded turn whose admitted listeners are fixed at enqueue time. */
190
244
  send(input: SendCharacterInput): Promise<CharacterOperation>;
191
245
  recordEvent(input: RecordCharacterEventInput): Promise<CharacterOperation>;
192
- setGameState(input: SetCharacterGameStateInput): Promise<CharacterOperation>;
246
+ setGameState(input: SetCharacterGameStateInput): Promise<CharacterGameStateOperation>;
193
247
  shareClaim(input: ShareCharacterClaimInput): Promise<CharacterOperation>;
194
248
  inspect(): Promise<Json>;
195
- deliverKnowledge(input: DeliverCharacterKnowledgeInput): Promise<CharacterOperation>;
249
+ deliverKnowledge(input: DeliverCharacterKnowledgeInput): Promise<CharacterKnowledgeDeliveryOperation>;
196
250
  completeBeat(input: CompleteCharacterBeatInput): Promise<CharacterOperation>;
197
251
  evaluateInformation(input: EvaluateCharacterInformationInput): Promise<CharacterInformationEvaluationOperation>;
198
252
  evaluateBeats(input: EvaluateCharacterBeatsInput): Promise<CharacterBeatEvaluationOperation>;
@@ -204,7 +258,7 @@ export interface ServerCharacterHistoryOptions {
204
258
  limit?: number;
205
259
  cursor?: string | null;
206
260
  }
207
- export interface ServerStorage {
261
+ export interface ServerStorage extends SaveApi {
208
262
  /** Shared by everyone playing the deck, and owned by this server. */
209
263
  readonly deck: ServerStorageScope;
210
264
  /** One player's public data. Readable by anyone; writable by them or by this server. */
@@ -1,5 +1,6 @@
1
1
  import { chunked } from "../chunk";
2
2
  import { decodeStorageValue, encodeStorageValue, storageError, } from "../storageJson";
3
+ import { createSaveApi, createSaveBucket, preflightSaveRequest, SaveError, } from "../save";
3
4
  // Deck storage for an author's session server.
4
5
  //
5
6
  // The same store the deck's clients use, reached over the platform channel instead of
@@ -56,8 +57,26 @@ export function createServerStorage(platformHandle) {
56
57
  const result = await platformHandle.storage(op, JSON.stringify(args));
57
58
  return JSON.parse(result);
58
59
  };
60
+ // The host forwards these args to the platform's storage route unchanged,
61
+ // which takes a save's request as `input` and a lookup as `operationId`.
62
+ const saveApi = createSaveApi(async (verb, input) => {
63
+ if (typeof platformHandle.storage !== "function" ||
64
+ platformHandle.storageEnabled === false) {
65
+ throw new SaveError({
66
+ code: "CAULDRON_STORAGE_UNAVAILABLE",
67
+ message: "Storage is not available to this session.",
68
+ operationId: input.operationId,
69
+ retryable: false,
70
+ committed: false,
71
+ });
72
+ }
73
+ const args = verb === "operation" ? input : { input };
74
+ preflightSaveRequest(verb, args, MAX_REQUEST_BYTES);
75
+ return call(verb, args);
76
+ }, true);
59
77
  return {
60
- deck: createScope(call, "DECK", null),
78
+ ...saveApi,
79
+ deck: createScope(call, "DECK", null, saveApi),
61
80
  user(userId) {
62
81
  // A bad id poisons the scope rather than throwing here, so the failure arrives
63
82
  // the same way every other failure does -- as a rejection the author can await.
@@ -65,12 +84,12 @@ export function createServerStorage(platformHandle) {
65
84
  // resolved by the platform to the deck's creator, quietly writing the wrong
66
85
  // player's bucket, so nothing is sent either.
67
86
  if (typeof userId !== "string" || userId.length === 0) {
68
- return createScope(rejectMissingSubject, "USER", null);
87
+ return createScope(rejectMissingSubject, "USER", null, saveApi);
69
88
  }
70
89
  if (utf8Length(userId) > MAX_SUBJECT_BYTES) {
71
- return createScope(rejectOversizeSubject, "USER", null);
90
+ return createScope(rejectOversizeSubject, "USER", userId, saveApi);
72
91
  }
73
- return createScope(call, "USER", userId);
92
+ return createScope(call, "USER", userId, saveApi);
74
93
  },
75
94
  board(name) {
76
95
  return createBoard(call, name);
@@ -224,8 +243,9 @@ function createPlaythrough(call, subject, generation) {
224
243
  },
225
244
  });
226
245
  }
227
- function createScope(call, scope, subject) {
246
+ function createScope(call, scope, subject, saveApi) {
228
247
  return {
248
+ ...createSaveBucket(saveApi, scope === "DECK" ? "deck" : "user", subject ?? undefined),
229
249
  async get(keys) {
230
250
  // A bare array, not a wrapper: the platform hands back exactly what the
231
251
  // storage verb returns, and `get` returns its entries directly.
@@ -1,10 +1,18 @@
1
1
  import type { MultiplayerSessionMode, PlatformHandle, PlayerIdentity } from "./platformHandle";
2
2
  import type { Json } from "../types";
3
+ import type { MultiplayerWorldCharacterCommand, MultiplayerWorldCharacterCommandResult, MultiplayerWorldCharacterCommandResultValue } from "../multiplayer";
3
4
  import { type ServerStorage } from "./storage";
4
- export type { MultiplayerSessionMode, PlatformHandle, PlatformSessionConfig, PlatformWorldCheckpoint, PlatformWorldSnapshot, PlayerIdentity, } from "./platformHandle";
5
- export type { CharacterKnowledgeSource, CompleteCharacterBeatInput, DeliverCharacterKnowledgeInput, RecordCharacterEventInput, SetCharacterGameStateInput, ShareCharacterClaimInput, ServerStorage, ServerStorageBoard, ServerStorageBoardScore, ServerStorageListEntry, ServerStorageListOptions, ServerStorageListPage, ServerPlaythrough, ServerPlaythroughCharacters, ServerCharacterHistoryOptions, ServerStorageScope, } from "./storage";
5
+ import { type MultiplayerHubArrival, type MultiplayerServerHub } from "./hub";
6
+ export type { MultiplayerSessionMode, PlatformHandle, PlatformSessionConfig, PlatformWorldCheckpoint, PlatformWorldSnapshot, PlatformWorldTimeConfig, PlayerIdentity, } from "./platformHandle";
7
+ export { createSaveCoordinator } from "../saveCoordinator";
8
+ export type { CoordinatedChange, CoordinatedSave, SaveCoordinator, SaveStatus, } from "../saveCoordinator";
9
+ export { SaveError } from "../save";
10
+ 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";
11
+ export type { MultiplayerHubArrival, MultiplayerHubBoard, MultiplayerHubOutcome, MultiplayerHubPadUpdateInput, MultiplayerHubPadUpdateResult, MultiplayerHubPlayerResults, MultiplayerHubRecord, MultiplayerHubReportReceipt, MultiplayerHubResultInput, MultiplayerHubResultRow, MultiplayerHubResultShape, MultiplayerHubResultsRead, MultiplayerHubResultsReadInput, MultiplayerHubWearable, MultiplayerHubWearReceipt, MultiplayerServerHub, } from "./hub";
12
+ export type { CharacterActionNotice, CharacterGameStateOperation, CharacterGameStateResult, CharacterKnowledgeDeliveryOperation, CharacterKnowledgeDeliveryResult, CharacterKnowledgeSource, CompleteCharacterBeatInput, DeliverCharacterKnowledgeInput, RecordCharacterEventInput, SetCharacterGameStateInput, ShareCharacterClaimInput, ServerStorage, ServerStorageBoard, ServerStorageBoardScore, ServerStorageListEntry, ServerStorageListOptions, ServerStorageListPage, ServerPlaythrough, ServerPlaythroughCharacters, ServerCharacterHistoryOptions, ServerStorageScope, } from "./storage";
6
13
  export interface MultiplayerServerPlayer extends PlayerIdentity {
7
14
  playerId: string;
15
+ readonly participantId: string;
8
16
  }
9
17
  export interface MultiplayerBroadcastOptions {
10
18
  except?: string;
@@ -14,6 +22,12 @@ export interface MultiplayerServerSession {
14
22
  readonly sessionId: string;
15
23
  readonly deckId: string;
16
24
  readonly mode: MultiplayerSessionMode;
25
+ /**
26
+ * The place the host started this session for (a hub door's destination, or
27
+ * the hub place ghost pinned the session to), or null when the host stamped
28
+ * none. Never set from a client.
29
+ */
30
+ readonly place: string | null;
17
31
  /**
18
32
  * Deck storage that outlives the session. Nothing else here does: a session ends
19
33
  * when its last player leaves and its memory goes with it.
@@ -21,6 +35,27 @@ export interface MultiplayerServerSession {
21
35
  readonly storage: ServerStorage;
22
36
  /** A durable checkpoint for an opted-in persistent destination. */
23
37
  readonly world: MultiplayerServerWorld | null;
38
+ /**
39
+ * The shared characters this session can address: `session.world.characters`
40
+ * in a shared-character world (the same object), or a hub place's characters
41
+ * on one of its layers, where `session.world` stays null. Null everywhere else.
42
+ */
43
+ readonly characters: MultiplayerServerWorldCharacters | null;
44
+ /**
45
+ * The hub place this session runs (castle.json `hub`, as ghost pinned it):
46
+ * results, pads and arrivals. Null for a deck without a hub block and on hosts
47
+ * without hub support.
48
+ */
49
+ readonly hub: MultiplayerServerHub | null;
50
+ /**
51
+ * The variant a connected player has in the A/B experiment `name`, or null
52
+ * when they are in no such experiment (or have left). Castle's server
53
+ * resolved it and the host delivered it with the join, so it is the same arm
54
+ * the player's page reads and nothing a client sent.
55
+ */
56
+ experiment(player: MultiplayerServerPlayer | string, name: string): string | null;
57
+ /** Every experiment a connected player is in, `{ name: variant }`. */
58
+ experiments(player: MultiplayerServerPlayer | string): Record<string, string>;
24
59
  /** Sends bytes as a binary message and all other payloads as JSON. */
25
60
  send(playerId: string, data: unknown): void;
26
61
  /** Broadcasts bytes as a binary message and all other payloads as JSON. */
@@ -52,8 +87,106 @@ export interface MultiplayerServerWorld {
52
87
  read(): Promise<MultiplayerWorldSnapshot>;
53
88
  /** Commits exactly against expectedRevision; conflicts never partially write. */
54
89
  commit(input: MultiplayerWorldCommitInput): Promise<MultiplayerWorldCommitReceipt>;
90
+ catchUp(options: MultiplayerWorldCatchUpOptions): Promise<MultiplayerWorldCatchUpResult>;
91
+ readonly journal: MultiplayerServerWorldJournal;
92
+ readonly participants: MultiplayerServerWorldParticipants;
93
+ readonly notifications: MultiplayerServerWorldNotifications;
55
94
  readonly characters: MultiplayerServerWorldCharacters;
56
95
  }
96
+ export interface MultiplayerWorldCatchUpOptions {
97
+ key?: string;
98
+ maxElapsedMs?: number;
99
+ apply(input: {
100
+ elapsedMs: number;
101
+ now: number;
102
+ checkpoint: MultiplayerWorldCheckpoint | null;
103
+ }): MultiplayerWorldCheckpoint | Promise<MultiplayerWorldCheckpoint>;
104
+ }
105
+ export interface MultiplayerWorldCatchUpResult {
106
+ applied: boolean;
107
+ elapsedMs: number;
108
+ revision: string;
109
+ }
110
+ export interface MultiplayerWorldJournalAppendInput {
111
+ operationId: string;
112
+ entries: readonly MultiplayerWorldJournalAppendEntry[];
113
+ }
114
+ export interface MultiplayerWorldJournalAppendEntry {
115
+ kind: string;
116
+ disclosure: "world" | "participants";
117
+ participantIds?: readonly string[];
118
+ data?: Json;
119
+ }
120
+ export interface MultiplayerWorldJournalAppendReceipt {
121
+ operationId: string;
122
+ replayed: boolean;
123
+ firstSeq: string;
124
+ lastSeq: string;
125
+ }
126
+ export interface MultiplayerWorldJournalReadOptions {
127
+ afterSeq?: string;
128
+ limit?: number;
129
+ participantId?: string;
130
+ }
131
+ export interface MultiplayerWorldJournalEntry {
132
+ seq: string;
133
+ kind: string;
134
+ disclosure: "world" | "participants";
135
+ participantIds: readonly string[];
136
+ data: Json;
137
+ createdAt: string;
138
+ }
139
+ export interface MultiplayerWorldJournalReadResult {
140
+ entries: readonly MultiplayerWorldJournalEntry[];
141
+ nextSeq: string;
142
+ oldestRetainedSeq: string | null;
143
+ }
144
+ export interface MultiplayerServerWorldJournal {
145
+ append(input: MultiplayerWorldJournalAppendInput): Promise<MultiplayerWorldJournalAppendReceipt>;
146
+ read(options?: MultiplayerWorldJournalReadOptions): Promise<MultiplayerWorldJournalReadResult>;
147
+ }
148
+ export interface MultiplayerWorldParticipantsReadOptions {
149
+ participantIds?: readonly string[];
150
+ limit?: number;
151
+ }
152
+ export interface MultiplayerWorldParticipant {
153
+ participantId: string;
154
+ firstSeenAt: string;
155
+ lastSeenAt: string;
156
+ lastLeftAt: string | null;
157
+ visitCount: number;
158
+ }
159
+ export interface MultiplayerWorldParticipantsReadResult {
160
+ participants: readonly MultiplayerWorldParticipant[];
161
+ }
162
+ export interface MultiplayerServerWorldParticipants {
163
+ read(options?: MultiplayerWorldParticipantsReadOptions): Promise<MultiplayerWorldParticipantsReadResult>;
164
+ }
165
+ export interface MultiplayerWorldNotificationNotifyInput {
166
+ key: string;
167
+ participantIds: readonly string[];
168
+ templateId: string;
169
+ slots?: Readonly<Record<string, string>>;
170
+ at?: string;
171
+ expiresAt?: string;
172
+ }
173
+ export interface MultiplayerWorldNotificationNotifyResult {
174
+ key: string;
175
+ accepted: number;
176
+ skipped: number;
177
+ limited: number;
178
+ }
179
+ export interface MultiplayerWorldNotificationCancelInput {
180
+ key: string;
181
+ participantIds?: readonly string[];
182
+ }
183
+ export interface MultiplayerWorldNotificationCancelResult {
184
+ cancelled: number;
185
+ }
186
+ export interface MultiplayerServerWorldNotifications {
187
+ notify(input: MultiplayerWorldNotificationNotifyInput): Promise<MultiplayerWorldNotificationNotifyResult>;
188
+ cancel(input: MultiplayerWorldNotificationCancelInput): Promise<MultiplayerWorldNotificationCancelResult>;
189
+ }
57
190
  export interface MultiplayerServerWorldCharacters {
58
191
  create(input: MultiplayerWorldRuntimeCharacterCreateInput): Promise<MultiplayerWorldRuntimeCharacterCreateReceipt>;
59
192
  list(): Promise<MultiplayerWorldCharacterList>;
@@ -97,14 +230,21 @@ export interface MultiplayerWorldRuntimeCharacterCreateReceipt {
97
230
  export interface MultiplayerWorldCharacterList {
98
231
  characters: readonly MultiplayerWorldCharacterSummary[];
99
232
  }
100
- export interface MultiplayerWorldCharacterCommandRequest {
233
+ interface MultiplayerWorldCharacterCommandRequestFor<Command extends MultiplayerWorldCharacterCommand> {
234
+ /** Command discriminator mirrored at the request level for TypeScript narrowing. */
235
+ readonly type: Command["type"];
101
236
  readonly requestId: string;
102
237
  readonly operationId: string;
103
238
  readonly speaker: MultiplayerServerPlayer;
104
- readonly command: Readonly<Record<string, unknown>>;
105
- apply(): Promise<unknown>;
239
+ readonly command: Readonly<Command>;
240
+ apply(): Promise<MultiplayerWorldCharacterCommandResult<MultiplayerWorldCharacterCommandResultValue<Command>>>;
106
241
  reject(code?: string): void;
107
242
  }
243
+ export type MultiplayerWorldCharacterCommandRequest = {
244
+ [Type in MultiplayerWorldCharacterCommand["type"]]: MultiplayerWorldCharacterCommandRequestFor<Extract<MultiplayerWorldCharacterCommand, {
245
+ type: Type;
246
+ }>>;
247
+ }[MultiplayerWorldCharacterCommand["type"]];
108
248
  export interface MultiplayerWorldCharacterReadRequest {
109
249
  readonly requestId: string;
110
250
  readonly speaker: MultiplayerServerPlayer;
@@ -136,7 +276,8 @@ export interface MultiplayerWorldCharacterTurn {
136
276
  type CallbackResult = unknown;
137
277
  export interface MultiplayerServerCallbacks {
138
278
  onStart?: (session: MultiplayerServerSession) => CallbackResult;
139
- onPlayerJoin?: (session: MultiplayerServerSession, player: MultiplayerServerPlayer) => CallbackResult;
279
+ /** `arrival` is set when ghost placed the player through a hub door. */
280
+ onPlayerJoin?: (session: MultiplayerServerSession, player: MultiplayerServerPlayer, arrival: MultiplayerHubArrival | null) => CallbackResult;
140
281
  onPlayerLeave?: (session: MultiplayerServerSession, player: MultiplayerServerPlayer) => CallbackResult;
141
282
  /** Receives binary messages as Uint8Array values and JSON messages otherwise. */
142
283
  onMessage?: (session: MultiplayerServerSession, player: MultiplayerServerPlayer, data: unknown) => CallbackResult;