@klbsjpolp/realtime-core 0.1.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/dist/code.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare const ROOM_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTVWXYZ";
2
+ export declare const ROOM_CODE_LENGTH = 3;
3
+ export declare const normalizeRoomCode: (value: string) => string;
4
+ export declare const isValidRoomCode: (value: string) => boolean;
5
+ //# sourceMappingURL=code.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../src/code.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,2BAA2B,CAAC;AAC3D,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,MAKnB,CAAC;AAEhC,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,OAI/C,CAAC"}
package/dist/code.js ADDED
@@ -0,0 +1,11 @@
1
+ export const ROOM_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTVWXYZ';
2
+ export const ROOM_CODE_LENGTH = 3;
3
+ export const normalizeRoomCode = (value) => value
4
+ .trim()
5
+ .toUpperCase()
6
+ .replace(/[\s-]+/g, '')
7
+ .slice(0, ROOM_CODE_LENGTH);
8
+ export const isValidRoomCode = (value) => {
9
+ const normalized = normalizeRoomCode(value);
10
+ return normalized.length === ROOM_CODE_LENGTH && [...normalized].every((char) => ROOM_CODE_ALPHABET.includes(char));
11
+ };
@@ -0,0 +1,23 @@
1
+ export interface RoomSession {
2
+ expiresAt: string;
3
+ hostSeatIndex: number;
4
+ roomCode: string;
5
+ seatCapacity: number;
6
+ seatIndex: number;
7
+ seatToken: string;
8
+ wsUrl: string;
9
+ }
10
+ export interface CreateRoomRequest {
11
+ playerName?: string;
12
+ /** Which game this room hosts. The server treats it as an opaque routing key. */
13
+ gameId?: string;
14
+ /** Opaque per-game configuration. The server stores it without interpreting it. */
15
+ gameConfig?: unknown;
16
+ }
17
+ export interface JoinRoomRequest {
18
+ playerName?: string;
19
+ roomCode: string;
20
+ }
21
+ export type CreateRoomResponse = RoomSession;
22
+ export type JoinRoomResponse = RoomSession;
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,kBAAkB,GAAG,WAAW,CAAC;AAC7C,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ export * from './code.js';
2
+ export * from './version.js';
3
+ export * from './playerName.js';
4
+ export * from './shuffle.js';
5
+ export * from './http/index.js';
6
+ export * from './room/index.js';
7
+ export * from './protocol/index.js';
8
+ export * from './schemas/http.js';
9
+ export * from './schemas/websocket.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from './code.js';
2
+ export * from './version.js';
3
+ export * from './playerName.js';
4
+ export * from './shuffle.js';
5
+ export * from './http/index.js';
6
+ export * from './room/index.js';
7
+ export * from './protocol/index.js';
8
+ export * from './schemas/http.js';
9
+ export * from './schemas/websocket.js';
@@ -0,0 +1,5 @@
1
+ export declare const MAX_PLAYER_NAME_LENGTH = 10;
2
+ export declare const getDefaultPlayerName: (seatIndex: number) => string;
3
+ export declare const normalizePlayerName: (value: string | null | undefined) => string | undefined;
4
+ export declare const resolvePlayerName: (value: string | null | undefined, seatIndex: number) => string;
5
+ //# sourceMappingURL=playerName.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playerName.d.ts","sourceRoot":"","sources":["../src/playerName.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,eAAO,MAAM,oBAAoB,GAAI,WAAW,MAAM,KAAG,MAAmC,CAAC;AAE7F,eAAO,MAAM,mBAAmB,GAAI,OAAO,MAAM,GAAG,IAAI,GAAG,SAAS,KAAG,MAAM,GAAG,SAQ/E,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,WAAW,MAAM,KAAG,MACzB,CAAC"}
@@ -0,0 +1,10 @@
1
+ export const MAX_PLAYER_NAME_LENGTH = 10;
2
+ export const getDefaultPlayerName = (seatIndex) => `Joueur ${seatIndex + 1}`;
3
+ export const normalizePlayerName = (value) => {
4
+ if (typeof value !== 'string') {
5
+ return undefined;
6
+ }
7
+ const trimmedValue = value.trim().slice(0, MAX_PLAYER_NAME_LENGTH);
8
+ return trimmedValue.length > 0 ? trimmedValue : undefined;
9
+ };
10
+ export const resolvePlayerName = (value, seatIndex) => normalizePlayerName(value) ?? getDefaultPlayerName(seatIndex);
@@ -0,0 +1,48 @@
1
+ import type { RelayKind } from '../schemas/websocket.js';
2
+ import type { RoomStatus, RoomSummary } from '../room/index.js';
3
+ /** An opaque message forwarded from another seat. `payload` is never inspected. */
4
+ export interface RelayedServerMessage {
5
+ type: 'relayed';
6
+ fromSeat: number;
7
+ kind: RelayKind;
8
+ payload: unknown;
9
+ }
10
+ /** The abstract turn pointer changed. */
11
+ export interface TurnServerMessage {
12
+ type: 'turn';
13
+ currentSeatIndex: number;
14
+ }
15
+ /**
16
+ * The host pressed start. The server has shuffled the seating and set the
17
+ * first turn; the host now builds the real game state from this.
18
+ */
19
+ export interface GameStartedServerMessage {
20
+ type: 'gameStarted';
21
+ activeSeatIndices: number[];
22
+ currentSeatIndex: number;
23
+ gameConfig?: unknown;
24
+ }
25
+ /**
26
+ * Sent to the host seat on reconnect: the opaque full-state blob it last pushed,
27
+ * or `null` if none was stored yet.
28
+ */
29
+ export interface SnapshotRestoreServerMessage {
30
+ type: 'snapshotRestore';
31
+ payload: unknown;
32
+ }
33
+ export interface PresenceServerMessage {
34
+ type: 'presence';
35
+ room: RoomSummary;
36
+ }
37
+ export interface RoomClosedServerMessage {
38
+ type: 'roomClosed';
39
+ roomCode: string;
40
+ status: RoomStatus;
41
+ }
42
+ export interface ActionRejectedServerMessage {
43
+ type: 'actionRejected';
44
+ code: 'forbidden' | 'invalid_action' | 'invalid_state' | 'not_authenticated' | 'not_your_turn';
45
+ reason: string;
46
+ }
47
+ export type ServerMessage = RelayedServerMessage | TurnServerMessage | GameStartedServerMessage | SnapshotRestoreServerMessage | PresenceServerMessage | RoomClosedServerMessage | ActionRejectedServerMessage;
48
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/protocol/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAMhE,mFAAmF;AACnF,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,yCAAyC;AACzC,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,aAAa,CAAC;IACpB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,iBAAiB,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,YAAY,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,WAAW,GAAG,gBAAgB,GAAG,eAAe,GAAG,mBAAmB,GAAG,eAAe,CAAC;IAC/F,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,aAAa,GACrB,oBAAoB,GACpB,iBAAiB,GACjB,wBAAwB,GACxB,4BAA4B,GAC5B,qBAAqB,GACrB,uBAAuB,GACvB,2BAA2B,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ export type RoomStatus = 'WAITING' | 'ACTIVE' | 'FINISHED';
2
+ export type LobbyReadyState = 'never-ready' | 'ready' | 'unready';
3
+ export interface LobbySeatInfo {
4
+ seatIndex: number;
5
+ readyState: LobbyReadyState;
6
+ displayName: string | null;
7
+ }
8
+ export interface DisconnectedSeatInfo {
9
+ seatIndex: number;
10
+ disconnectedAt: string;
11
+ }
12
+ export interface RoomSummary {
13
+ connectedSeats: number[];
14
+ /** Whose turn it is, as an abstract seat index. `null` while WAITING/FINISHED. */
15
+ currentSeatIndex: number | null;
16
+ disconnectedSeats: DisconnectedSeatInfo[];
17
+ expiresAt: string;
18
+ hostSeatIndex: number;
19
+ lobbySeats: LobbySeatInfo[];
20
+ roomCode: string;
21
+ seatCapacity: number;
22
+ status: RoomStatus;
23
+ version: number;
24
+ }
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/room/index.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE3D,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,OAAO,GAAG,SAAS,CAAC;AAElE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,eAAe,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,iBAAiB,EAAE,oBAAoB,EAAE,CAAC;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB"}
@@ -0,0 +1,4 @@
1
+ // Game-agnostic room, lobby and presence shapes. Nothing here knows about a
2
+ // specific game — only seats, lobby readiness, connectivity and the abstract
3
+ // turn pointer (currentSeatIndex).
4
+ export {};
@@ -0,0 +1,23 @@
1
+ import { z } from 'zod';
2
+ export declare const roomCodeSchema: z.ZodString;
3
+ export declare const playerNameSchema: z.ZodString;
4
+ export declare const gameIdSchema: z.ZodString;
5
+ export declare const createRoomRequestSchema: z.ZodObject<{
6
+ playerName: z.ZodOptional<z.ZodString>;
7
+ gameId: z.ZodOptional<z.ZodString>;
8
+ gameConfig: z.ZodOptional<z.ZodUnknown>;
9
+ }, z.core.$strip>;
10
+ export declare const joinRoomRequestSchema: z.ZodObject<{
11
+ playerName: z.ZodOptional<z.ZodString>;
12
+ roomCode: z.ZodString;
13
+ }, z.core.$strip>;
14
+ export declare const roomSessionSchema: z.ZodObject<{
15
+ expiresAt: z.ZodString;
16
+ hostSeatIndex: z.ZodNumber;
17
+ roomCode: z.ZodString;
18
+ seatCapacity: z.ZodNumber;
19
+ seatIndex: z.ZodNumber;
20
+ seatToken: z.ZodString;
21
+ wsUrl: z.ZodString;
22
+ }, z.core.$strip>;
23
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/schemas/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,cAAc,aAAiD,CAAC;AAC7E,eAAO,MAAM,gBAAgB,aAAgD,CAAC;AAC9E,eAAO,MAAM,YAAY,aAAmC,CAAC;AAE7D,eAAO,MAAM,uBAAuB;;;;iBAKlC,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;iBAGhC,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;iBAQ5B,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ import { ROOM_CODE_LENGTH } from '../code.js';
3
+ import { MAX_PLAYER_NAME_LENGTH } from '../playerName.js';
4
+ export const roomCodeSchema = z.string().trim().min(1).max(ROOM_CODE_LENGTH);
5
+ export const playerNameSchema = z.string().trim().max(MAX_PLAYER_NAME_LENGTH);
6
+ export const gameIdSchema = z.string().trim().min(1).max(64);
7
+ export const createRoomRequestSchema = z.object({
8
+ playerName: playerNameSchema.optional(),
9
+ gameId: gameIdSchema.optional(),
10
+ // Opaque to the server; the host client validates it against the game.
11
+ gameConfig: z.unknown().optional(),
12
+ });
13
+ export const joinRoomRequestSchema = z.object({
14
+ playerName: playerNameSchema.optional(),
15
+ roomCode: roomCodeSchema,
16
+ });
17
+ export const roomSessionSchema = z.object({
18
+ expiresAt: z.string().datetime(),
19
+ hostSeatIndex: z.number().int().min(0).max(3),
20
+ roomCode: z.string().length(ROOM_CODE_LENGTH),
21
+ seatCapacity: z.number().int().min(2).max(4),
22
+ seatIndex: z.number().int().min(0).max(3),
23
+ seatToken: z.string().min(1),
24
+ wsUrl: z.string().url(),
25
+ });
@@ -0,0 +1,99 @@
1
+ import { z } from 'zod';
2
+ export declare const relayKindSchema: z.ZodEnum<{
3
+ move: "move";
4
+ event: "event";
5
+ view: "view";
6
+ }>;
7
+ export type RelayKind = z.infer<typeof relayKindSchema>;
8
+ export declare const relayClientMessageSchema: z.ZodObject<{
9
+ type: z.ZodLiteral<"relay">;
10
+ kind: z.ZodEnum<{
11
+ move: "move";
12
+ event: "event";
13
+ view: "view";
14
+ }>;
15
+ payload: z.ZodUnknown;
16
+ toSeats: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
17
+ }, z.core.$strip>;
18
+ export declare const setTurnClientMessageSchema: z.ZodObject<{
19
+ type: z.ZodLiteral<"setTurn">;
20
+ currentSeatIndex: z.ZodNumber;
21
+ }, z.core.$strip>;
22
+ export declare const snapshotClientMessageSchema: z.ZodObject<{
23
+ type: z.ZodLiteral<"snapshot">;
24
+ payload: z.ZodUnknown;
25
+ }, z.core.$strip>;
26
+ export declare const endGameClientMessageSchema: z.ZodObject<{
27
+ type: z.ZodLiteral<"endGame">;
28
+ winnerSeatIndex: z.ZodNullable<z.ZodNumber>;
29
+ }, z.core.$strip>;
30
+ export declare const authClientMessageSchema: z.ZodObject<{
31
+ type: z.ZodLiteral<"auth">;
32
+ protocolVersion: z.ZodOptional<z.ZodNumber>;
33
+ roomCode: z.ZodString;
34
+ seatIndex: z.ZodNumber;
35
+ seatToken: z.ZodString;
36
+ }, z.core.$strip>;
37
+ export declare const startGameClientMessageSchema: z.ZodObject<{
38
+ type: z.ZodLiteral<"startGame">;
39
+ clientVersion: z.ZodOptional<z.ZodNumber>;
40
+ }, z.core.$strip>;
41
+ export declare const pingClientMessageSchema: z.ZodObject<{
42
+ type: z.ZodLiteral<"ping">;
43
+ }, z.core.$strip>;
44
+ export declare const setReadyClientMessageSchema: z.ZodObject<{
45
+ type: z.ZodLiteral<"setReady">;
46
+ playerName: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strip>;
48
+ export declare const setUnreadyClientMessageSchema: z.ZodObject<{
49
+ type: z.ZodLiteral<"setUnready">;
50
+ }, z.core.$strip>;
51
+ export declare const kickSeatClientMessageSchema: z.ZodObject<{
52
+ type: z.ZodLiteral<"kickSeat">;
53
+ targetSeatIndex: z.ZodNumber;
54
+ }, z.core.$strip>;
55
+ export declare const leaveLobbyClientMessageSchema: z.ZodObject<{
56
+ type: z.ZodLiteral<"leaveLobby">;
57
+ }, z.core.$strip>;
58
+ export declare const clientMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
59
+ type: z.ZodLiteral<"auth">;
60
+ protocolVersion: z.ZodOptional<z.ZodNumber>;
61
+ roomCode: z.ZodString;
62
+ seatIndex: z.ZodNumber;
63
+ seatToken: z.ZodString;
64
+ }, z.core.$strip>, z.ZodObject<{
65
+ type: z.ZodLiteral<"relay">;
66
+ kind: z.ZodEnum<{
67
+ move: "move";
68
+ event: "event";
69
+ view: "view";
70
+ }>;
71
+ payload: z.ZodUnknown;
72
+ toSeats: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
73
+ }, z.core.$strip>, z.ZodObject<{
74
+ type: z.ZodLiteral<"setTurn">;
75
+ currentSeatIndex: z.ZodNumber;
76
+ }, z.core.$strip>, z.ZodObject<{
77
+ type: z.ZodLiteral<"snapshot">;
78
+ payload: z.ZodUnknown;
79
+ }, z.core.$strip>, z.ZodObject<{
80
+ type: z.ZodLiteral<"endGame">;
81
+ winnerSeatIndex: z.ZodNullable<z.ZodNumber>;
82
+ }, z.core.$strip>, z.ZodObject<{
83
+ type: z.ZodLiteral<"startGame">;
84
+ clientVersion: z.ZodOptional<z.ZodNumber>;
85
+ }, z.core.$strip>, z.ZodObject<{
86
+ type: z.ZodLiteral<"ping">;
87
+ }, z.core.$strip>, z.ZodObject<{
88
+ type: z.ZodLiteral<"setReady">;
89
+ playerName: z.ZodOptional<z.ZodString>;
90
+ }, z.core.$strip>, z.ZodObject<{
91
+ type: z.ZodLiteral<"setUnready">;
92
+ }, z.core.$strip>, z.ZodObject<{
93
+ type: z.ZodLiteral<"kickSeat">;
94
+ targetSeatIndex: z.ZodNumber;
95
+ }, z.core.$strip>, z.ZodObject<{
96
+ type: z.ZodLiteral<"leaveLobby">;
97
+ }, z.core.$strip>], "type">;
98
+ export type ClientMessage = z.infer<typeof clientMessageSchema>;
99
+ //# sourceMappingURL=websocket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/schemas/websocket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAaxB,eAAO,MAAM,eAAe;;;;EAAoC,CAAC;AAEjE,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,eAAO,MAAM,wBAAwB;;;;;;;;;iBAMnC,CAAC;AAKH,eAAO,MAAM,0BAA0B;;;iBAGrC,CAAC;AAEH,eAAO,MAAM,2BAA2B;;;iBAItC,CAAC;AAEH,eAAO,MAAM,0BAA0B;;;iBAGrC,CAAC;AAKH,eAAO,MAAM,uBAAuB;;;;;;iBAMlC,CAAC;AAEH,eAAO,MAAM,4BAA4B;;;iBAGvC,CAAC;AAEH,eAAO,MAAM,uBAAuB;;iBAElC,CAAC;AAEH,eAAO,MAAM,2BAA2B;;;iBAGtC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;iBAExC,CAAC;AAEH,eAAO,MAAM,2BAA2B;;;iBAGtC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;iBAExC,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAY9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
@@ -0,0 +1,78 @@
1
+ import { z } from 'zod';
2
+ import { MAX_PLAYER_NAME_LENGTH } from '../playerName.js';
3
+ const MAX_SEAT_INDEX = 3;
4
+ // ---------------------------------------------------------------------------
5
+ // Relay envelope
6
+ // ---------------------------------------------------------------------------
7
+ // The server never inspects `payload`. It only enforces:
8
+ // - `move`: may only be sent by the seat whose turn it is (currentSeatIndex)
9
+ // - `view`: host-only (the authoritative client pushing a redacted view)
10
+ // - `event`: any authenticated seat (chat, resync requests, …)
11
+ export const relayKindSchema = z.enum(['move', 'event', 'view']);
12
+ export const relayClientMessageSchema = z.object({
13
+ type: z.literal('relay'),
14
+ kind: relayKindSchema,
15
+ payload: z.unknown(),
16
+ // Target seats. Omitted = every other authenticated seat in the room.
17
+ toSeats: z.array(z.number().int().min(0).max(MAX_SEAT_INDEX)).optional(),
18
+ });
19
+ // ---------------------------------------------------------------------------
20
+ // Host-only control messages
21
+ // ---------------------------------------------------------------------------
22
+ export const setTurnClientMessageSchema = z.object({
23
+ type: z.literal('setTurn'),
24
+ currentSeatIndex: z.number().int().min(0).max(MAX_SEAT_INDEX),
25
+ });
26
+ export const snapshotClientMessageSchema = z.object({
27
+ type: z.literal('snapshot'),
28
+ // Opaque full-state blob the server stores for host reconnection only.
29
+ payload: z.unknown(),
30
+ });
31
+ export const endGameClientMessageSchema = z.object({
32
+ type: z.literal('endGame'),
33
+ winnerSeatIndex: z.number().int().min(0).max(MAX_SEAT_INDEX).nullable(),
34
+ });
35
+ // ---------------------------------------------------------------------------
36
+ // Lobby + session messages (game-agnostic)
37
+ // ---------------------------------------------------------------------------
38
+ export const authClientMessageSchema = z.object({
39
+ type: z.literal('auth'),
40
+ protocolVersion: z.number().int().min(0).optional(),
41
+ roomCode: z.string().min(1),
42
+ seatIndex: z.number().int().min(0).max(MAX_SEAT_INDEX),
43
+ seatToken: z.string().min(1),
44
+ });
45
+ export const startGameClientMessageSchema = z.object({
46
+ type: z.literal('startGame'),
47
+ clientVersion: z.number().int().min(0).optional(),
48
+ });
49
+ export const pingClientMessageSchema = z.object({
50
+ type: z.literal('ping'),
51
+ });
52
+ export const setReadyClientMessageSchema = z.object({
53
+ type: z.literal('setReady'),
54
+ playerName: z.string().trim().max(MAX_PLAYER_NAME_LENGTH).optional(),
55
+ });
56
+ export const setUnreadyClientMessageSchema = z.object({
57
+ type: z.literal('setUnready'),
58
+ });
59
+ export const kickSeatClientMessageSchema = z.object({
60
+ type: z.literal('kickSeat'),
61
+ targetSeatIndex: z.number().int().min(0).max(MAX_SEAT_INDEX),
62
+ });
63
+ export const leaveLobbyClientMessageSchema = z.object({
64
+ type: z.literal('leaveLobby'),
65
+ });
66
+ export const clientMessageSchema = z.discriminatedUnion('type', [
67
+ authClientMessageSchema,
68
+ relayClientMessageSchema,
69
+ setTurnClientMessageSchema,
70
+ snapshotClientMessageSchema,
71
+ endGameClientMessageSchema,
72
+ startGameClientMessageSchema,
73
+ pingClientMessageSchema,
74
+ setReadyClientMessageSchema,
75
+ setUnreadyClientMessageSchema,
76
+ kickSeatClientMessageSchema,
77
+ leaveLobbyClientMessageSchema,
78
+ ]);
@@ -0,0 +1,3 @@
1
+ export declare const shuffle: <T>(array: readonly T[]) => T[];
2
+ export declare const shuffleInPlace: <T>(array: T[]) => void;
3
+ //# sourceMappingURL=shuffle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shuffle.d.ts","sourceRoot":"","sources":["../src/shuffle.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,OAAO,SAAS,CAAC,EAAE,KAAG,CAAC,EAIjD,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,CAAC,EAAE,OAAO,CAAC,EAAE,KAAG,IAK9C,CAAC"}
@@ -0,0 +1,11 @@
1
+ export const shuffle = (array) => {
2
+ const result = [...array];
3
+ shuffleInPlace(result);
4
+ return result;
5
+ };
6
+ export const shuffleInPlace = (array) => {
7
+ for (let i = array.length - 1; i > 0; i--) {
8
+ const j = Math.floor(Math.random() * (i + 1));
9
+ [array[i], array[j]] = [array[j], array[i]];
10
+ }
11
+ };
@@ -0,0 +1,5 @@
1
+ export declare const PROTOCOL_VERSION = 2;
2
+ export declare const MIN_SUPPORTED_PROTOCOL_VERSION = 2;
3
+ export declare const ASSUMED_LEGACY_PROTOCOL_VERSION = 1;
4
+ export declare const isProtocolVersionSupported: (clientVersion: number | undefined) => boolean;
5
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAIhD,eAAO,MAAM,+BAA+B,IAAI,CAAC;AAEjD,eAAO,MAAM,0BAA0B,GAAI,eAAe,MAAM,GAAG,SAAS,KAAG,OACO,CAAC"}
@@ -0,0 +1,16 @@
1
+ // Bumped when the wire protocol changes in a backwards-incompatible way (new
2
+ // required message types, renamed fields, changed semantics). The server reads
3
+ // the client's reported version during auth and rejects mismatched clients
4
+ // with HTTP 426. Keep MIN_SUPPORTED_PROTOCOL_VERSION in sync with the oldest
5
+ // client build the server still accepts; pair bumps with
6
+ // PWA_MINIMUM_SUPPORTED_VERSION.
7
+ //
8
+ // v2 introduces the host-authoritative relay protocol (relay/setTurn/snapshot/
9
+ // endGame messages) and removes the server-authoritative `action`/`snapshot`
10
+ // flow. It is intentionally incompatible with v1 clients.
11
+ export const PROTOCOL_VERSION = 2;
12
+ export const MIN_SUPPORTED_PROTOCOL_VERSION = 2;
13
+ // Clients that omit `protocolVersion` predate the field entirely (v1). Since the
14
+ // relay protocol is a hard break, absent-field clients are rejected.
15
+ export const ASSUMED_LEGACY_PROTOCOL_VERSION = 1;
16
+ export const isProtocolVersionSupported = (clientVersion) => (clientVersion ?? ASSUMED_LEGACY_PROTOCOL_VERSION) >= MIN_SUPPORTED_PROTOCOL_VERSION;
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@klbsjpolp/realtime-core",
3
+ "version": "0.1.0",
4
+ "description": "Game-agnostic realtime relay protocol and room model shared by the realtime-api server and game clients.",
5
+ "license": "GPL-3.0-only",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/klbsjpolp/realtime-infra.git",
9
+ "directory": "packages/realtime-core"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "dependencies": {
27
+ "zod": "^4.4.3"
28
+ },
29
+ "devDependencies": {
30
+ "@vitest/coverage-v8": "^4.1.7",
31
+ "vite": "^8.0.14",
32
+ "vitest": "^4.1.7"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.json",
36
+ "lint": "eslint src tests --ext ts --report-unused-disable-directives --max-warnings 0",
37
+ "test": "vitest run",
38
+ "test:coverage": "vitest run --coverage --coverage.reporter=lcovonly --coverage.reporter=text-summary",
39
+ "typecheck": "tsc --noEmit -p tsconfig.json"
40
+ }
41
+ }