@joystyk/rooms 0.0.0 → 0.2.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.
- package/CHANGELOG.md +38 -0
- package/README.md +147 -2
- package/dist/canonical.d.ts +10 -0
- package/dist/canonical.js +30 -0
- package/dist/canonical.js.map +1 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.js +1879 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/names.d.ts +19 -0
- package/dist/names.js +28 -0
- package/dist/names.js.map +1 -0
- package/dist/node/host.d.ts +67 -0
- package/dist/node/host.js +323 -0
- package/dist/node/host.js.map +1 -0
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.js +7 -0
- package/dist/node/jobs.d.ts +48 -0
- package/dist/node/jobs.js +176 -0
- package/dist/node/jobs.js.map +1 -0
- package/dist/node/registry.d.ts +32 -0
- package/dist/node/registry.js +43 -0
- package/dist/node/registry.js.map +1 -0
- package/dist/node/server.d.ts +59 -0
- package/dist/node/server.js +577 -0
- package/dist/node/server.js.map +1 -0
- package/dist/node/store.d.ts +53 -0
- package/dist/node/store.js +245 -0
- package/dist/node/store.js.map +1 -0
- package/dist/node/support.d.ts +67 -0
- package/dist/node/support.js +126 -0
- package/dist/node/support.js.map +1 -0
- package/dist/policy.d.ts +15 -0
- package/dist/policy.js +41 -0
- package/dist/policy.js.map +1 -0
- package/dist/protocol/index.d.ts +43 -0
- package/dist/protocol/index.js +48 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/schemas.d.ts +792 -0
- package/dist/protocol/schemas.js +332 -0
- package/dist/protocol/schemas.js.map +1 -0
- package/dist/protocol/types.d.ts +279 -0
- package/dist/protocol/types.js +1 -0
- package/dist/record.d.ts +173 -0
- package/dist/record.js +181 -0
- package/dist/record.js.map +1 -0
- package/dist/types.d.ts +374 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -11
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"support.js","names":[],"sources":["../../src/node/support.ts"],"sourcesContent":["// Small Node-side helpers: structured logs, the clock, secrets and client addresses.\nimport { createHash, randomBytes, randomInt } from 'node:crypto';\nimport { BlockList, isIP } from 'node:net';\nimport type { IncomingMessage } from 'node:http';\nimport type { Json } from '@joystyk/contracts';\nimport { ROOM_ALPHABET, ROOM_CODE_LENGTH } from '../protocol/index.ts';\n\n// ── Logs ────────────────────────────────────────────────────────────────────────────────\n\nexport type LogLevel = 'info' | 'warn' | 'error';\n\n/**\n * One JSON object per line (section 9). Callers never pass tokens, token hashes, display\n * names, IP addresses or game state: every visitor may be a child (D-26).\n */\nexport interface Logger {\n write(level: LogLevel, event: string, fields?: { readonly [k: string]: Json }): void;\n}\n\nexport const createLogger = (\n sink: (line: string) => void = (line) => process.stdout.write(`${line}\\n`),\n now: () => number = Date.now,\n): Logger => ({\n write: (level, event, fields = {}) =>\n sink(JSON.stringify({ ts: new Date(now()).toISOString(), level, event, ...fields })),\n});\n\n/** A logger that keeps lines in memory, for tests. */\nexport const memoryLogger = (): Logger & {\n readonly lines: { level: LogLevel; event: string; fields: { readonly [k: string]: Json } }[];\n} => {\n const lines: { level: LogLevel; event: string; fields: { readonly [k: string]: Json } }[] = [];\n return { lines, write: (level, event, fields = {}) => void lines.push({ level, event, fields }) };\n};\n\n// ── Clock ───────────────────────────────────────────────────────────────────────────────\n\nexport interface Clock {\n now(): number;\n /** Calls `fire` at `at` (epoch ms). Firing early, late or twice is harmless to the core. */\n setTimer(at: number, fire: () => void): { cancel(): void };\n}\n\n/** Node's timers cap a delay at about 24.8 days; longer waits re-arm. */\nconst MAX_DELAY = 2 ** 31 - 1;\n\nexport const systemClock: Clock = {\n now: () => Date.now(),\n setTimer: (at, fire) => {\n let handle: ReturnType<typeof setTimeout>;\n const arm = (): void => {\n const delay = Math.max(0, at - Date.now());\n handle = setTimeout(delay > MAX_DELAY ? arm : fire, Math.min(delay, MAX_DELAY));\n handle.unref?.();\n };\n arm();\n return { cancel: () => clearTimeout(handle) };\n },\n};\n\n// ── Secrets ─────────────────────────────────────────────────────────────────────────────\n\n/** A seat token: 24 random bytes, base64url (32 characters). Never logged or stored. */\nexport const newSeatToken = (): string => randomBytes(24).toString('base64url');\n\n/** The SHA-256 hex digest the room stores in place of a token. */\nexport const hashToken = (token: string): string => createHash('sha256').update(token).digest('hex');\n\n/** A fresh invite code from the room alphabet. */\nexport const newRoomCode = (): string =>\n Array.from({ length: ROOM_CODE_LENGTH }, () => ROOM_ALPHABET[randomInt(ROOM_ALPHABET.length)]).join('');\n\n/** `count` uniformly random 32-bit words from the CSPRNG, for seeding a match's rng. */\nexport const randomWords = (count: number): number[] => {\n const bytes = randomBytes(count * 4);\n return Array.from({ length: count }, (_, i) => bytes.readUInt32LE(i * 4));\n};\n\n// ── Client addresses ────────────────────────────────────────────────────────────────────\n\nconst normalize = (address: string): string => address.replace(/^::ffff:(?=\\d+\\.\\d+\\.\\d+\\.\\d+$)/, '');\n\n/** Parses `TRUSTED_PROXY`: one address or a CIDR range. Throws on anything else. */\nexport const parseTrustedProxy = (value: string): BlockList => {\n const list = new BlockList();\n const [address = '', prefix] = value.split('/');\n const family = isIP(address);\n if (family === 0) throw new Error('TRUSTED_PROXY must be an IP address or a CIDR range such as 172.18.0.0/16.');\n const type = family === 4 ? 'ipv4' : 'ipv6';\n if (prefix === undefined) {\n list.addAddress(address, type);\n } else {\n const bits = Number(prefix);\n if (!Number.isInteger(bits) || bits < 0 || bits > (family === 4 ? 32 : 128))\n throw new Error('TRUSTED_PROXY has an invalid prefix length.');\n list.addSubnet(address, bits, type);\n }\n return list;\n};\n\n/**\n * The visitor's address. Only a trusted proxy (Traefik) may report it, through the configured\n * header or else the last `X-Forwarded-For` entry; anyone else is taken at their socket's word.\n */\nexport const clientAddress = (request: IncomingMessage, trusted: BlockList | null, header: string | null): string => {\n const peer = normalize(request.socket.remoteAddress ?? 'unknown');\n const family = isIP(peer);\n if (trusted === null || family === 0 || !trusted.check(peer, family === 4 ? 'ipv4' : 'ipv6')) return peer;\n const raw =\n header === null\n ? String(request.headers['x-forwarded-for'] ?? '')\n .split(',')\n .at(-1)\n : request.headers[header.toLowerCase()];\n const reported = normalize(String(Array.isArray(raw) ? raw.at(-1) : (raw ?? '')).trim());\n return isIP(reported) === 0 ? peer : reported;\n};\n\n// ── Rate limits ─────────────────────────────────────────────────────────────────────────\n\n/** Counts events per key in a sliding window, in memory only (section 8). */\nexport class WindowCounter {\n private readonly hits = new Map<string, number[]>();\n private readonly windowMs: number;\n private readonly max: number;\n\n constructor(windowMs: number, max: number) {\n this.windowMs = windowMs;\n this.max = max;\n }\n\n /** True if `key` is still under the limit; records the hit when `record` is set. */\n allow(key: string, now: number, record = true): boolean {\n const recent = (this.hits.get(key) ?? []).filter((t) => now - t < this.windowMs);\n const ok = recent.length < this.max;\n if (ok && record) recent.push(now);\n this.hits.set(key, recent);\n return ok;\n }\n\n hit(key: string, now: number): void {\n const recent = (this.hits.get(key) ?? []).filter((t) => now - t < this.windowMs);\n recent.push(now);\n this.hits.set(key, recent);\n }\n\n prune(now: number): void {\n for (const [key, times] of this.hits) if (!times.some((t) => now - t < this.windowMs)) this.hits.delete(key);\n }\n}\n\n/** A token bucket: `rate` per second, holding at most `burst`. */\nexport class TokenBucket {\n private tokens: number;\n private last: number;\n private readonly rate: number;\n private readonly burst: number;\n\n constructor(rate: number, burst: number, now: number) {\n this.rate = rate;\n this.burst = burst;\n this.tokens = burst;\n this.last = now;\n }\n\n take(now: number): boolean {\n this.tokens = Math.min(this.burst, this.tokens + ((now - this.last) * this.rate) / 1000);\n this.last = now;\n if (this.tokens < 1) return false;\n this.tokens -= 1;\n return true;\n }\n}\n"],"mappings":"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"}
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GameManifest } from '@joystyk/contracts';
|
|
2
|
+
import type { RoomPolicy } from './types.js';
|
|
3
|
+
export declare const DEFAULT_GRACE_MS = 30000;
|
|
4
|
+
export declare const DEFAULT_MAX_SPECTATORS = 8;
|
|
5
|
+
export declare const DEFAULT_RETAIN_MS: number;
|
|
6
|
+
/** No room outlives this, so hashed assets (kept 30 days) always outlive their rooms. */
|
|
7
|
+
export declare const MAX_ROOM_AGE_MS: number;
|
|
8
|
+
/**
|
|
9
|
+
* The policy a game's rooms start with. Seat counts come from `players`, the clock from
|
|
10
|
+
* `clock.default`, the rest from `rooms` with platform defaults. Without an AI, an absent
|
|
11
|
+
* player's seat waits rather than going to the AI.
|
|
12
|
+
*/
|
|
13
|
+
export declare const policyFromManifest: (manifest: GameManifest) => RoomPolicy;
|
|
14
|
+
/** Why a policy can't run, or null when it can. */
|
|
15
|
+
export declare const policyProblem: (policy: RoomPolicy) => string | null;
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/policy.ts
|
|
2
|
+
var DEFAULT_GRACE_MS = 3e4;
|
|
3
|
+
var DEFAULT_MAX_SPECTATORS = 8;
|
|
4
|
+
var DEFAULT_RETAIN_MS = 864e5;
|
|
5
|
+
/** No room outlives this, so hashed assets (kept 30 days) always outlive their rooms. */
|
|
6
|
+
var MAX_ROOM_AGE_MS = 6048e5;
|
|
7
|
+
/**
|
|
8
|
+
* The policy a game's rooms start with. Seat counts come from `players`, the clock from
|
|
9
|
+
* `clock.default`, the rest from `rooms` with platform defaults. Without an AI, an absent
|
|
10
|
+
* player's seat waits rather than going to the AI.
|
|
11
|
+
*/
|
|
12
|
+
var policyFromManifest = (manifest) => {
|
|
13
|
+
const rooms = manifest.rooms ?? {};
|
|
14
|
+
return {
|
|
15
|
+
seats: {
|
|
16
|
+
min: manifest.players.min,
|
|
17
|
+
max: manifest.players.max
|
|
18
|
+
},
|
|
19
|
+
start: rooms.start ?? "host",
|
|
20
|
+
lateJoin: rooms.lateJoin ?? "spectate",
|
|
21
|
+
absent: rooms.absent ?? (manifest.ai === null ? "wait" : "ai"),
|
|
22
|
+
graceMs: rooms.graceMs ?? 3e4,
|
|
23
|
+
decisionMs: manifest.clock === null || manifest.clock.mode === "bank" ? null : manifest.clock.default * 1e3,
|
|
24
|
+
bank: manifest.clock?.mode === "bank" ? manifest.clock.default : null,
|
|
25
|
+
maxSpectators: rooms.maxSpectators ?? 8,
|
|
26
|
+
retainMs: DEFAULT_RETAIN_MS
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
/** Why a policy can't run, or null when it can. */
|
|
30
|
+
var policyProblem = (policy) => {
|
|
31
|
+
const { min, max } = policy.seats;
|
|
32
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || max < min || max > 64) return "Seats must satisfy 1 ≤ min ≤ max ≤ 64.";
|
|
33
|
+
if (policy.graceMs < 0 || policy.maxSpectators < 0 || policy.retainMs <= 0) return "Durations and caps must not be negative.";
|
|
34
|
+
if (policy.decisionMs !== null && policy.decisionMs <= 0) return "The decision clock must be positive or off.";
|
|
35
|
+
if (policy.bank !== void 0 && policy.bank !== null && (policy.decisionMs !== null || policy.bank.initialMs <= 0 || policy.bank.incrementMs < 0)) return "A time bank needs a positive start, no negative increment, and no per-move clock.";
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
export { DEFAULT_GRACE_MS, DEFAULT_MAX_SPECTATORS, DEFAULT_RETAIN_MS, MAX_ROOM_AGE_MS, policyFromManifest, policyProblem };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","names":[],"sources":["../src/policy.ts"],"sourcesContent":["// A room's policy from the game's manifest (game-contract.md, \"Manifest and mounting\").\nimport type { GameManifest } from '@joystyk/contracts';\nimport type { RoomPolicy } from './types.ts';\n\nexport const DEFAULT_GRACE_MS = 30_000;\nexport const DEFAULT_MAX_SPECTATORS = 8;\nexport const DEFAULT_RETAIN_MS = 24 * 60 * 60 * 1000;\n/** No room outlives this, so hashed assets (kept 30 days) always outlive their rooms. */\nexport const MAX_ROOM_AGE_MS = 7 * 24 * 60 * 60 * 1000;\n\n/**\n * The policy a game's rooms start with. Seat counts come from `players`, the clock from\n * `clock.default`, the rest from `rooms` with platform defaults. Without an AI, an absent\n * player's seat waits rather than going to the AI.\n */\nexport const policyFromManifest = (manifest: GameManifest): RoomPolicy => {\n const rooms = manifest.rooms ?? {};\n return {\n seats: { min: manifest.players.min, max: manifest.players.max },\n start: rooms.start ?? 'host',\n lateJoin: rooms.lateJoin ?? 'spectate',\n absent: rooms.absent ?? (manifest.ai === null ? 'wait' : 'ai'),\n graceMs: rooms.graceMs ?? DEFAULT_GRACE_MS,\n decisionMs: manifest.clock === null || manifest.clock.mode === 'bank' ? null : manifest.clock.default * 1000,\n bank: manifest.clock?.mode === 'bank' ? manifest.clock.default : null,\n maxSpectators: rooms.maxSpectators ?? DEFAULT_MAX_SPECTATORS,\n retainMs: DEFAULT_RETAIN_MS,\n };\n};\n\n/** Why a policy can't run, or null when it can. */\nexport const policyProblem = (policy: RoomPolicy): string | null => {\n const { min, max } = policy.seats;\n if (!Number.isInteger(min) || !Number.isInteger(max) || min < 1 || max < min || max > 64)\n return 'Seats must satisfy 1 ≤ min ≤ max ≤ 64.';\n if (policy.graceMs < 0 || policy.maxSpectators < 0 || policy.retainMs <= 0)\n return 'Durations and caps must not be negative.';\n if (policy.decisionMs !== null && policy.decisionMs <= 0) return 'The decision clock must be positive or off.';\n if (\n policy.bank !== undefined &&\n policy.bank !== null &&\n (policy.decisionMs !== null || policy.bank.initialMs <= 0 || policy.bank.incrementMs < 0)\n ) {\n return 'A time bank needs a positive start, no negative increment, and no per-move clock.';\n }\n return null;\n};\n"],"mappings":";AAIA,IAAa,mBAAmB;AAChC,IAAa,yBAAyB;AACtC,IAAa,oBAAoB;;AAEjC,IAAa,kBAAkB;;;;;;AAO/B,IAAa,sBAAsB,aAAuC;CACxE,MAAM,QAAQ,SAAS,SAAS,CAAC;CACjC,OAAO;EACL,OAAO;GAAE,KAAK,SAAS,QAAQ;GAAK,KAAK,SAAS,QAAQ;EAAI;EAC9D,OAAO,MAAM,SAAS;EACtB,UAAU,MAAM,YAAY;EAC5B,QAAQ,MAAM,WAAW,SAAS,OAAO,OAAO,SAAS;EACzD,SAAS,MAAM,WAAA;EACf,YAAY,SAAS,UAAU,QAAQ,SAAS,MAAM,SAAS,SAAS,OAAO,SAAS,MAAM,UAAU;EACxG,MAAM,SAAS,OAAO,SAAS,SAAS,SAAS,MAAM,UAAU;EACjE,eAAe,MAAM,iBAAA;EACrB,UAAU;CACZ;AACF;;AAGA,IAAa,iBAAiB,WAAsC;CAClE,MAAM,EAAE,KAAK,QAAQ,OAAO;CAC5B,IAAI,CAAC,OAAO,UAAU,GAAG,KAAK,CAAC,OAAO,UAAU,GAAG,KAAK,MAAM,KAAK,MAAM,OAAO,MAAM,IACpF,OAAO;CACT,IAAI,OAAO,UAAU,KAAK,OAAO,gBAAgB,KAAK,OAAO,YAAY,GACvE,OAAO;CACT,IAAI,OAAO,eAAe,QAAQ,OAAO,cAAc,GAAG,OAAO;CACjE,IACE,OAAO,SAAS,KAAA,KAChB,OAAO,SAAS,SACf,OAAO,eAAe,QAAQ,OAAO,KAAK,aAAa,KAAK,OAAO,KAAK,cAAc,IAEvF,OAAO;CAET,OAAO;AACT"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ServerMessage } from './types.js';
|
|
2
|
+
export * from './schemas.js';
|
|
3
|
+
export type * from './types.js';
|
|
4
|
+
/** WebSocket close codes (section 5.2). */
|
|
5
|
+
export declare const CloseCode: {
|
|
6
|
+
readonly normal: 1000;
|
|
7
|
+
/** Server restarting; reconnect after `retryAfterMs`. */
|
|
8
|
+
readonly restarting: 1012;
|
|
9
|
+
/** Too slow (outgoing buffer full) or the server is busy; reconnect with backoff. */
|
|
10
|
+
readonly tryAgainLater: 1013;
|
|
11
|
+
/** A newer tab took the seat; stop and offer "Use this tab instead". */
|
|
12
|
+
readonly replaced: 4001;
|
|
13
|
+
/** The player left the room; stop. */
|
|
14
|
+
readonly leftRoom: 4002;
|
|
15
|
+
/** No such room, or it closed; stop. */
|
|
16
|
+
readonly noSuchRoom: 4004;
|
|
17
|
+
/** A rate or policy limit; reconnect after a longer backoff. */
|
|
18
|
+
readonly policy: 4008;
|
|
19
|
+
};
|
|
20
|
+
/** Rejection codes the platform itself uses; engines add `game:<GameError.code>`. */
|
|
21
|
+
export declare const RejectionCode: {
|
|
22
|
+
readonly stale: "stale";
|
|
23
|
+
readonly badRevision: "bad-revision";
|
|
24
|
+
readonly actionIdReused: "action-id-reused";
|
|
25
|
+
readonly spectator: "spectator";
|
|
26
|
+
readonly notHost: "not-host";
|
|
27
|
+
readonly wrongPhase: "wrong-phase";
|
|
28
|
+
readonly storageUnavailable: "storage-unavailable";
|
|
29
|
+
readonly engineError: "engine-error";
|
|
30
|
+
readonly badCommand: "bad-command";
|
|
31
|
+
readonly notAllowed: "not-allowed";
|
|
32
|
+
};
|
|
33
|
+
export type RejectionCode = (typeof RejectionCode)[keyof typeof RejectionCode];
|
|
34
|
+
/** The rejection code for an engine's `GameError`. */
|
|
35
|
+
export declare const gameRejection: (code: string) => string;
|
|
36
|
+
/** True when the server speaks this protocol version. */
|
|
37
|
+
export declare const isSupportedProtocol: (version: number) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* A server message as a connection that joined with `version` must receive it. There is one
|
|
40
|
+
* version today, so this only stamps `v`; when version 2 exists, the down-conversion for
|
|
41
|
+
* version-1 clients lives here and nowhere else.
|
|
42
|
+
*/
|
|
43
|
+
export declare const encodeFor: (message: ServerMessage, version: number) => ServerMessage;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ActMessageSchema, ActionId, BankSettingSchema, BuildId, ClientMessageSchema, ClosingMessageSchema, ControlMessageSchema, CreateRoomRequestSchema, CreateRoomResponseSchema, DecisionMs, DisplayName, GameId, GamePinSchema, HostedGamesSchema, JoinMessageSchema, JsonValue, NoticeCodeSchema, NoticeMessageSchema, OfferKindSchema, PROTOCOL_VERSION, PresenceMessageSchema, ROOM_ALPHABET, ROOM_CODE_LENGTH, ReceiptMessageSchema, RejectionCodeSchema, ReplacedMessageSchema, ResyncMessageSchema, Revision, RoomCode, RoomControlSchema, RoomInfoSchema, RoomOfferSchema, RoomPhaseSchema, SUPPORTED_PROTOCOLS, SeatIdSchema, SeatToken, ServerMessageSchema, SnapshotMessageSchema, UpdateMessageSchema, WelcomeMessageSchema } from "./schemas.js";
|
|
2
|
+
//#region src/protocol/index.ts
|
|
3
|
+
/** WebSocket close codes (section 5.2). */
|
|
4
|
+
var CloseCode = {
|
|
5
|
+
normal: 1e3,
|
|
6
|
+
/** Server restarting; reconnect after `retryAfterMs`. */
|
|
7
|
+
restarting: 1012,
|
|
8
|
+
/** Too slow (outgoing buffer full) or the server is busy; reconnect with backoff. */
|
|
9
|
+
tryAgainLater: 1013,
|
|
10
|
+
/** A newer tab took the seat; stop and offer "Use this tab instead". */
|
|
11
|
+
replaced: 4001,
|
|
12
|
+
/** The player left the room; stop. */
|
|
13
|
+
leftRoom: 4002,
|
|
14
|
+
/** No such room, or it closed; stop. */
|
|
15
|
+
noSuchRoom: 4004,
|
|
16
|
+
/** A rate or policy limit; reconnect after a longer backoff. */
|
|
17
|
+
policy: 4008
|
|
18
|
+
};
|
|
19
|
+
/** Rejection codes the platform itself uses; engines add `game:<GameError.code>`. */
|
|
20
|
+
var RejectionCode = {
|
|
21
|
+
stale: "stale",
|
|
22
|
+
badRevision: "bad-revision",
|
|
23
|
+
actionIdReused: "action-id-reused",
|
|
24
|
+
spectator: "spectator",
|
|
25
|
+
notHost: "not-host",
|
|
26
|
+
wrongPhase: "wrong-phase",
|
|
27
|
+
storageUnavailable: "storage-unavailable",
|
|
28
|
+
engineError: "engine-error",
|
|
29
|
+
badCommand: "bad-command",
|
|
30
|
+
notAllowed: "not-allowed"
|
|
31
|
+
};
|
|
32
|
+
/** The rejection code for an engine's `GameError`. */
|
|
33
|
+
var gameRejection = (code) => `game:${code}`;
|
|
34
|
+
/** True when the server speaks this protocol version. */
|
|
35
|
+
var isSupportedProtocol = (version) => SUPPORTED_PROTOCOLS.includes(version);
|
|
36
|
+
/**
|
|
37
|
+
* A server message as a connection that joined with `version` must receive it. There is one
|
|
38
|
+
* version today, so this only stamps `v`; when version 2 exists, the down-conversion for
|
|
39
|
+
* version-1 clients lives here and nowhere else.
|
|
40
|
+
*/
|
|
41
|
+
var encodeFor = (message, version) => message.v === version ? message : {
|
|
42
|
+
...message,
|
|
43
|
+
v: version
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
export { ActMessageSchema, ActionId, BankSettingSchema, BuildId, ClientMessageSchema, CloseCode, ClosingMessageSchema, ControlMessageSchema, CreateRoomRequestSchema, CreateRoomResponseSchema, DecisionMs, DisplayName, GameId, GamePinSchema, HostedGamesSchema, JoinMessageSchema, JsonValue, NoticeCodeSchema, NoticeMessageSchema, OfferKindSchema, PROTOCOL_VERSION, PresenceMessageSchema, ROOM_ALPHABET, ROOM_CODE_LENGTH, ReceiptMessageSchema, RejectionCode, RejectionCodeSchema, ReplacedMessageSchema, ResyncMessageSchema, Revision, RoomCode, RoomControlSchema, RoomInfoSchema, RoomOfferSchema, RoomPhaseSchema, SUPPORTED_PROTOCOLS, SeatIdSchema, SeatToken, ServerMessageSchema, SnapshotMessageSchema, UpdateMessageSchema, WelcomeMessageSchema, encodeFor, gameRejection, isSupportedProtocol };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/protocol/index.ts"],"sourcesContent":["// @joystyk/rooms/protocol: the versioned wire protocol (room-server.md section 5). Pure: it\n// imports only @joystyk/contracts and zod/mini, so the browser's remote session client and\n// the P2P host can import it without pulling in the room core or the Node adapter.\nimport { SUPPORTED_PROTOCOLS } from './schemas.ts';\nimport type { ServerMessage } from './types.ts';\n\nexport * from './schemas.ts';\nexport type * from './types.ts';\n\n/** WebSocket close codes (section 5.2). */\nexport const CloseCode = {\n normal: 1000,\n /** Server restarting; reconnect after `retryAfterMs`. */\n restarting: 1012,\n /** Too slow (outgoing buffer full) or the server is busy; reconnect with backoff. */\n tryAgainLater: 1013,\n /** A newer tab took the seat; stop and offer \"Use this tab instead\". */\n replaced: 4001,\n /** The player left the room; stop. */\n leftRoom: 4002,\n /** No such room, or it closed; stop. */\n noSuchRoom: 4004,\n /** A rate or policy limit; reconnect after a longer backoff. */\n policy: 4008,\n} as const;\n\n/** Rejection codes the platform itself uses; engines add `game:<GameError.code>`. */\nexport const RejectionCode = {\n stale: 'stale',\n badRevision: 'bad-revision',\n actionIdReused: 'action-id-reused',\n spectator: 'spectator',\n notHost: 'not-host',\n wrongPhase: 'wrong-phase',\n storageUnavailable: 'storage-unavailable',\n engineError: 'engine-error',\n badCommand: 'bad-command',\n notAllowed: 'not-allowed',\n} as const;\n\nexport type RejectionCode = (typeof RejectionCode)[keyof typeof RejectionCode];\n\n/** The rejection code for an engine's `GameError`. */\nexport const gameRejection = (code: string): string => `game:${code}`;\n\n/** True when the server speaks this protocol version. */\nexport const isSupportedProtocol = (version: number): boolean => SUPPORTED_PROTOCOLS.includes(version);\n\n/**\n * A server message as a connection that joined with `version` must receive it. There is one\n * version today, so this only stamps `v`; when version 2 exists, the down-conversion for\n * version-1 clients lives here and nowhere else.\n */\nexport const encodeFor = (message: ServerMessage, version: number): ServerMessage =>\n message.v === version ? message : { ...message, v: version };\n"],"mappings":";;;AAUA,IAAa,YAAY;CACvB,QAAQ;;CAER,YAAY;;CAEZ,eAAe;;CAEf,UAAU;;CAEV,UAAU;;CAEV,YAAY;;CAEZ,QAAQ;AACV;;AAGA,IAAa,gBAAgB;CAC3B,OAAO;CACP,aAAa;CACb,gBAAgB;CAChB,WAAW;CACX,SAAS;CACT,YAAY;CACZ,oBAAoB;CACpB,aAAa;CACb,YAAY;CACZ,YAAY;AACd;;AAKA,IAAa,iBAAiB,SAAyB,QAAQ;;AAG/D,IAAa,uBAAuB,YAA6B,oBAAoB,SAAS,OAAO;;;;;;AAOrG,IAAa,aAAa,SAAwB,YAChD,QAAQ,MAAM,UAAU,UAAU;CAAE,GAAG;CAAS,GAAG;AAAQ"}
|