@anweather/game-plugin-ssdk 1.17.0 → 1.19.0

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,10 +1,25 @@
1
- /** `save:{id}`, `player:{id}` or `world:{uuid}` -- see the model's doc. */
2
- export type BlobOwnerKey = `save:${string}` | `player:${string}` | `world:${string}`;
1
+ /** `save:{id}`, `player:{id}`, `world:{uuid}` or `space:{spaceId}` -- see
2
+ * the model's doc. A `space:` owner can only be written once the Space
3
+ * exists (and is this game type's); check current membership first, see
4
+ * spaceClient.ts. */
5
+ export type BlobOwnerKey = `save:${string}` | `player:${string}` | `world:${string}` | `space:${string}`;
3
6
  export declare const blobOwner: {
4
7
  save: (saveFileId: string) => BlobOwnerKey;
5
8
  player: (participantId: string) => BlobOwnerKey;
6
9
  world: (worldId: string) => BlobOwnerKey;
10
+ space: (spaceId: string) => BlobOwnerKey;
7
11
  };
12
+ /** One owner holding blobs, as ListBlobOwners reports it. */
13
+ export interface BlobOwnerSummary {
14
+ ownerKey: string;
15
+ blobCount: number;
16
+ totalBytes: number;
17
+ }
18
+ export interface BlobOwnerPage {
19
+ owners: BlobOwnerSummary[];
20
+ /** Pass as `after` for the next page; absent on the last one. */
21
+ nextToken?: string;
22
+ }
8
23
  export interface BlobClientOptions {
9
24
  /** The platform's base URL, e.g. http://platform:8080. */
10
25
  platformUrl: string;
@@ -70,8 +85,18 @@ export interface BlobClient {
70
85
  totalBytes: number;
71
86
  quotaBytes: number;
72
87
  }>;
88
+ /** Owners of this game type holding blobs, by key prefix ("world:",
89
+ * "space", or "" for all), sorted by key, one page (default 500, at
90
+ * most 1000) at a time. See allBlobOwners for every page. */
91
+ listOwners(prefix?: string, page?: {
92
+ after?: string;
93
+ limit?: number;
94
+ }): Promise<BlobOwnerPage>;
73
95
  }
74
96
  export declare function createBlobClient(options: BlobClientOptions): BlobClient;
97
+ /** Every owner with this prefix, following nextToken page by page -- e.g.
98
+ * a backfill's `for await (const o of allBlobOwners(blobs, "world:"))`. */
99
+ export declare function allBlobOwners(client: BlobClient, prefix?: string, pageSize?: number): AsyncGenerator<BlobOwnerSummary>;
75
100
  /** Read `key`, apply `change` to its value (undefined when absent), and
76
101
  * write the result conditional on the version read -- re-reading and
77
102
  * re-applying on a VersionConflict, up to `maxAttempts` times. `change`
@@ -8,8 +8,8 @@ export type { RegistrationOptions, RegistrationLoop } from "./registration";
8
8
  export { runConformance, assertConformance } from "./conformance";
9
9
  export type { ConformanceOptions, ConformanceReport } from "./conformance";
10
10
  export { PLUGIN_PROTOCOL_VERSION, runtimeVersion } from "./version";
11
- export { createBlobClient, casUpdate, blobOwner, BlobStoreError, isVersionConflict } from "./blobClient";
12
- export type { BlobClient, BlobClientOptions, BlobOwnerKey, Blob, BlobItem, BlobInfo, BlobWrite, BlobErrorType, } from "./blobClient";
11
+ export { createBlobClient, casUpdate, blobOwner, allBlobOwners, BlobStoreError, isVersionConflict } from "./blobClient";
12
+ export type { BlobClient, BlobClientOptions, BlobOwnerKey, Blob, BlobItem, BlobInfo, BlobWrite, BlobErrorType, BlobOwnerSummary, BlobOwnerPage, } from "./blobClient";
13
13
  export { attachLive } from "./live/server";
14
14
  export type { AttachLiveOptions, LiveServer } from "./live/server";
15
15
  export type { LiveOptions, Room, RoomHandlers, Conn, RoomCloseReason, RoomTimer, SendOptions, BroadcastOptions, KickOptions, } from "./live/rooms";
@@ -21,3 +21,5 @@ export { runLiveConformance, assertLiveConformance } from "./live/conformance";
21
21
  export type { LiveConformanceOptions, LiveConformanceReport } from "./live/conformance";
22
22
  export { LIVE_PROTOCOL_VERSION, LIVE_PATH, LinkCloseCode, MAX_LINK_FRAME_BYTES } from "./live/protocol";
23
23
  export type { RoomScope, SpaceMember, PresenceState, Role } from "./live/protocol";
24
+ export { createSpaceClient, createMembershipGuard, isAlreadyASpace, SpaceApiError, NotASpaceMemberError, FakeSpaceClient, MAX_MEMBERSHIP_AGE_MS, } from "./spaceClient";
25
+ export type { SpaceClient, SpaceClientOptions, PluginSpace, PluginSpaceMember, SpaceImport, SpaceImportMember, SpaceImportResult, SpaceErrorType, SpaceCounts, SpaceRole, SpaceMemberStatus, MembershipGuard, MembershipGuardOptions, } from "./spaceClient";
@@ -0,0 +1,159 @@
1
+ export type SpaceRole = "OWNER" | "MEMBER";
2
+ export type SpaceMemberStatus = "ACTIVE" | "LEFT" | "REMOVED";
3
+ /** A member as GetSpace lists one: ACTIVE only, so status is always
4
+ * "ACTIVE". Whoever left or was removed is simply absent. */
5
+ export interface PluginSpaceMember {
6
+ playerId: string;
7
+ /** The platform's display name; absent if the Player is gone. */
8
+ displayName?: string;
9
+ role: SpaceRole;
10
+ status: "ACTIVE";
11
+ }
12
+ /** A Space as `config.space` carries it, plus `archived` and `revision`. */
13
+ export interface PluginSpace<S = Record<string, unknown>> {
14
+ id: string;
15
+ name: string;
16
+ ownerId: string;
17
+ settings: S;
18
+ /** ACTIVE members only, owner first, then by first join. */
19
+ members: PluginSpaceMember[];
20
+ /** Closed by the owner: no new joins or Games; running Games carry on. */
21
+ archived: boolean;
22
+ /** Goes up with every change to the Space. */
23
+ revision: number;
24
+ }
25
+ export interface SpaceImportMember {
26
+ playerId: string;
27
+ role: SpaceRole;
28
+ /** A member who left and later rejoined is ACTIVE. */
29
+ status: SpaceMemberStatus;
30
+ /** ISO-8601: first join. */
31
+ joinedAt: string;
32
+ /** ISO-8601: latest status change. */
33
+ statusAt: string;
34
+ }
35
+ /** What ImportSpace takes: everyone who ever joined, owner included, with
36
+ * exactly one OWNER who is ownerId and ACTIVE. The game type is always
37
+ * this client's. */
38
+ export interface SpaceImport {
39
+ name: string;
40
+ ownerId: string;
41
+ /** "K7QM-4XPA" in any case/spacing; null or absent: closed to joins. */
42
+ inviteCode?: string | null;
43
+ /** A JSON object; absent: {}. */
44
+ settings?: Record<string, unknown>;
45
+ members: SpaceImportMember[];
46
+ }
47
+ export interface SpaceImportResult {
48
+ /** False: an identical import had already landed; nothing was written. */
49
+ created: boolean;
50
+ space: PluginSpace;
51
+ }
52
+ export type SpaceErrorType = "SpaceNotFound" | "InvalidSpaceImport" | "SpaceImportDisabled" | "SpaceAlreadyExists" | "InviteCodeTaken" | "PluginUnauthorized";
53
+ /** A refusal from PluginSpaces. `details` is the error body (for
54
+ * InvalidSpaceImport, `unknownPlayerIds` when that was the reason). Any
55
+ * other failure (network, timeout, a 5xx) is a plain Error. */
56
+ export declare class SpaceApiError extends Error {
57
+ readonly type: SpaceErrorType;
58
+ readonly status: number;
59
+ readonly details: Record<string, unknown>;
60
+ constructor(type: SpaceErrorType, status: number, details: Record<string, unknown>);
61
+ }
62
+ /** A 409 SpaceAlreadyExists on import: it already is a Space, and the
63
+ * platform's copy wins (§3.5.6). Use its membership; don't retry. */
64
+ export declare function isAlreadyASpace(e: unknown): e is SpaceApiError;
65
+ export interface SpaceClient {
66
+ /** null when there is no Space with this id of this game type. */
67
+ get(spaceId: string): Promise<PluginSpace | null>;
68
+ /** Create-only; idempotent for an identical body. */
69
+ import(spaceId: string, body: SpaceImport): Promise<SpaceImportResult>;
70
+ /** This game type's Spaces, invite codes and ACTIVE members, counted:
71
+ * what a backfill reads back to verify its real run against its dry run. */
72
+ counts(): Promise<SpaceCounts>;
73
+ }
74
+ export interface SpaceCounts {
75
+ spaces: number;
76
+ inviteCodes: number;
77
+ activeMembers: number;
78
+ }
79
+ export interface SpaceClientOptions {
80
+ /** The platform's base URL, e.g. http://platform:8080. */
81
+ platformUrl: string;
82
+ gameType: string;
83
+ /** The token this plugin registered with; sent as X-Plugin-Auth. */
84
+ authToken: string | undefined;
85
+ /** Per-request timeout. Default 5000. */
86
+ timeoutMs?: number;
87
+ fetchImpl?: typeof fetch;
88
+ }
89
+ export declare function createSpaceClient(options: SpaceClientOptions): SpaceClient;
90
+ /** The platform's ceiling on how stale a membership check may be. */
91
+ export declare const MAX_MEMBERSHIP_AGE_MS = 15000;
92
+ /** The writer is not (or no longer) an ACTIVE member of the Space, or
93
+ * there is no such Space. Refuse the write. */
94
+ export declare class NotASpaceMemberError extends Error {
95
+ readonly spaceId: string;
96
+ readonly playerId: string;
97
+ constructor(spaceId: string, playerId: string);
98
+ }
99
+ export interface MembershipGuardOptions {
100
+ /** How old a cached read may be. Default and ceiling: 15000 (§3.5.4). */
101
+ maxAgeMs?: number;
102
+ /** Clock, for tests. */
103
+ now?: () => number;
104
+ }
105
+ export interface MembershipGuard {
106
+ /** The Space, read at most maxAgeMs ago; null when there is none. */
107
+ space(spaceId: string): Promise<PluginSpace | null>;
108
+ /** Whether playerId is an ACTIVE member as of a read at most maxAgeMs old. */
109
+ isActiveMember(spaceId: string, playerId: string): Promise<boolean>;
110
+ /** Call before every durable write to the Space's shared state: returns
111
+ * the Space, or throws NotASpaceMemberError. A failed read throws too
112
+ * (fail closed), and is not cached. */
113
+ assertActiveMember(spaceId: string, playerId: string): Promise<PluginSpace>;
114
+ /** Drop the cached read for one Space, or for all. */
115
+ invalidate(spaceId?: string): void;
116
+ }
117
+ /** The plugin obligation of LIVE_CHANNELS_DESIGN.md §3.5.4 as one call:
118
+ * a cached Space read (never older than 15 s, concurrent misses share one
119
+ * request; a missing Space and a failed read are never cached) and the
120
+ * ACTIVE-member check on top of it. */
121
+ export declare function createMembershipGuard(client: Pick<SpaceClient, "get">, options?: MembershipGuardOptions): MembershipGuard;
122
+ /** An in-memory SpaceClient for a plugin's own tests: the platform's
123
+ * rules for import (create-only, idempotent for the same body, 409 for a
124
+ * different one or a taken code, one ACTIVE OWNER who is ownerId, known
125
+ * players only when `players` is given) and an ACTIVE-only read. Change
126
+ * membership with `setStatus` the way a player would on the platform. */
127
+ export declare class FakeSpaceClient implements SpaceClient {
128
+ private readonly options;
129
+ /** What the platform holds, every status included. */
130
+ readonly spaces: Map<string, {
131
+ space: Omit<PluginSpace, "members">;
132
+ inviteCode: string | null;
133
+ members: Map<string, SpaceImportMember>;
134
+ imported?: string;
135
+ }>;
136
+ /** Every call, as "get <id>" / "import <id>". */
137
+ readonly calls: string[];
138
+ /** Display names by playerId, used by reads. */
139
+ readonly names: Map<string, string>;
140
+ importEnabled: boolean;
141
+ constructor(options?: {
142
+ players?: Iterable<string>;
143
+ });
144
+ /** A Space as if created on the platform. */
145
+ add(space: {
146
+ id: string;
147
+ name?: string;
148
+ ownerId: string;
149
+ settings?: Record<string, unknown>;
150
+ inviteCode?: string | null;
151
+ }): void;
152
+ /** A join, leave or removal, as the platform's player routes make them. */
153
+ setStatus(spaceId: string, playerId: string, status: SpaceMemberStatus): void;
154
+ archive(spaceId: string): void;
155
+ get(spaceId: string): Promise<PluginSpace | null>;
156
+ import(spaceId: string, body: SpaceImport): Promise<SpaceImportResult>;
157
+ counts(): Promise<SpaceCounts>;
158
+ private view;
159
+ }
@@ -201,6 +201,64 @@ export declare namespace GameStatusOutput {
201
201
  */
202
202
  const validate: (obj: GameStatusOutput, path?: string) => __ValidationFailure[];
203
203
  }
204
+ /**
205
+ * @public
206
+ * @enum
207
+ */
208
+ export declare const LiveMode: {
209
+ readonly HOSTED: "HOSTED";
210
+ readonly RELAY: "RELAY";
211
+ };
212
+ /**
213
+ * @public
214
+ */
215
+ export type LiveMode = typeof LiveMode[keyof typeof LiveMode];
216
+ /**
217
+ * @public
218
+ * @enum
219
+ */
220
+ export declare const LiveScope: {
221
+ readonly GAME: "GAME";
222
+ readonly SPACE: "SPACE";
223
+ };
224
+ /**
225
+ * @public
226
+ */
227
+ export type LiveScope = typeof LiveScope[keyof typeof LiveScope];
228
+ /**
229
+ * A game type's live channels (LIVE_CHANNELS_DESIGN.md §4.1).
230
+ * @public
231
+ */
232
+ export interface LiveCapability {
233
+ mode: LiveMode | undefined;
234
+ scope?: LiveScope | undefined;
235
+ spectators?: boolean | undefined;
236
+ maxSignalsPerSecond?: number | undefined;
237
+ maxSignalBytes?: number | undefined;
238
+ }
239
+ export declare namespace LiveCapability {
240
+ /**
241
+ * @internal
242
+ */
243
+ const validate: (obj: LiveCapability, path?: string) => __ValidationFailure[];
244
+ }
245
+ /**
246
+ * A game type's Spaces: shared contexts above individual Games, whose
247
+ * membership the platform owns (LIVE_CHANNELS_DESIGN.md §3.5).
248
+ * @public
249
+ */
250
+ export interface SpaceCapability {
251
+ maxMembers: number | undefined;
252
+ label?: string | undefined;
253
+ settingsSchema?: __DocumentType | undefined;
254
+ gameConfig?: __DocumentType | undefined;
255
+ }
256
+ export declare namespace SpaceCapability {
257
+ /**
258
+ * @internal
259
+ */
260
+ const validate: (obj: SpaceCapability, path?: string) => __ValidationFailure[];
261
+ }
204
262
  /**
205
263
  * @public
206
264
  */
@@ -220,6 +278,17 @@ export interface GameTypeMetadata {
220
278
  hasSaveFiles?: boolean | undefined;
221
279
  hasSaveSummary?: boolean | undefined;
222
280
  hasSaveFileEditor?: boolean | undefined;
281
+ /**
282
+ * A game type's Spaces: shared contexts above individual Games, whose
283
+ * membership the platform owns (LIVE_CHANNELS_DESIGN.md §3.5).
284
+ * @public
285
+ */
286
+ spaces?: SpaceCapability | undefined;
287
+ /**
288
+ * A game type's live channels (LIVE_CHANNELS_DESIGN.md §4.1).
289
+ * @public
290
+ */
291
+ live?: LiveCapability | undefined;
223
292
  }
224
293
  export declare namespace GameTypeMetadata {
225
294
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@anweather/game-plugin-ssdk",
3
3
  "description": "Generated GamePlugin RPC types/server (plugin-protocol/model/game-plugin.smithy) plus a hand-written Express bridge and default-handler wrapper (./runtime) -- see plugin-protocol/CLAUDE.md's \"Publishing for cross-repo plugins\".",
4
- "version": "1.17.0",
4
+ "version": "1.19.0",
5
5
  "scripts": {
6
6
  "build": "concurrently 'npm:build:cjs' 'npm:build:es' 'npm:build:types'",
7
7
  "build:cjs": "tsc -p tsconfig.cjs.json",