@orakl/protocol 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/LICENSE +662 -0
- package/dist/badges.d.ts +19 -0
- package/dist/badges.js +1 -0
- package/dist/emotes.d.ts +5 -0
- package/dist/emotes.js +62 -0
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- package/dist/game-answer-record.d.ts +31 -0
- package/dist/game-answer-record.js +1 -0
- package/dist/game.d.ts +83 -0
- package/dist/game.js +1 -0
- package/dist/game.settings.d.ts +91 -0
- package/dist/game.settings.js +61 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +13 -0
- package/dist/pages/app.schema.d.ts +544 -0
- package/dist/pages/app.schema.js +336 -0
- package/dist/pages/auth.schema.d.ts +25 -0
- package/dist/pages/auth.schema.js +23 -0
- package/dist/pages/common.schema.d.ts +27 -0
- package/dist/pages/common.schema.js +28 -0
- package/dist/pages/history.schema.d.ts +273 -0
- package/dist/pages/history.schema.js +210 -0
- package/dist/pages/quiz.schema.d.ts +150 -0
- package/dist/pages/quiz.schema.js +132 -0
- package/dist/player-safe-question.d.ts +13 -0
- package/dist/player-safe-question.js +1 -0
- package/dist/question.d.ts +6 -0
- package/dist/question.js +12 -0
- package/dist/question.schema.d.ts +494 -0
- package/dist/question.schema.js +132 -0
- package/dist/scoring.schema.d.ts +7 -0
- package/dist/scoring.schema.js +8 -0
- package/dist/solo-ws.d.ts +202 -0
- package/dist/solo-ws.js +4 -0
- package/dist/streak-flourish.d.ts +6 -0
- package/dist/streak-flourish.js +1 -0
- package/dist/wire/leaderboard.d.ts +6 -0
- package/dist/wire/leaderboard.js +1 -0
- package/dist/wire/question-bank.d.ts +23 -0
- package/dist/wire/question-bank.js +1 -0
- package/dist/wire/question-flags.d.ts +40 -0
- package/dist/wire/question-flags.js +1 -0
- package/dist/ws-game.schema.d.ts +75 -0
- package/dist/ws-game.schema.js +131 -0
- package/dist/ws-solo.schema.d.ts +31 -0
- package/dist/ws-solo.schema.js +57 -0
- package/dist/ws.d.ts +281 -0
- package/dist/ws.js +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export declare const SoloClientMessageSchema: Schema.Union<[Schema.Struct<{
|
|
3
|
+
type: Schema.Literal<["auth"]>;
|
|
4
|
+
token: Schema.filter<typeof Schema.NonEmptyString>;
|
|
5
|
+
}>, Schema.Struct<{
|
|
6
|
+
type: Schema.Literal<["solo:start"]>;
|
|
7
|
+
categoryIds: Schema.filter<Schema.Array$<typeof Schema.NonEmptyString>>;
|
|
8
|
+
timerDuration: Schema.filter<Schema.filter<Schema.filter<typeof Schema.Number>>>;
|
|
9
|
+
questionCount: Schema.filter<Schema.filter<Schema.filter<typeof Schema.Number>>>;
|
|
10
|
+
mode: Schema.Literal<["normal", "endless"]>;
|
|
11
|
+
difficulty: Schema.Literal<["all", "easy", "medium", "hard"]>;
|
|
12
|
+
device: Schema.optional<Schema.Literal<["mobile", "tablet", "desktop"]>>;
|
|
13
|
+
}>, Schema.Struct<{
|
|
14
|
+
type: Schema.Literal<["solo:answer"]>;
|
|
15
|
+
answerId: Schema.filter<typeof Schema.NonEmptyString>;
|
|
16
|
+
nonce: Schema.optional<Schema.filter<typeof Schema.NonEmptyString>>;
|
|
17
|
+
}>, Schema.Struct<{
|
|
18
|
+
type: Schema.Literal<["solo:answer-change"]>;
|
|
19
|
+
}>, Schema.Struct<{
|
|
20
|
+
type: Schema.Literal<["solo:next"]>;
|
|
21
|
+
}>, Schema.Struct<{
|
|
22
|
+
type: Schema.Literal<["solo:tab-blur"]>;
|
|
23
|
+
}>, Schema.Struct<{
|
|
24
|
+
type: Schema.Literal<["solo:tab-focus"]>;
|
|
25
|
+
}>, Schema.Struct<{
|
|
26
|
+
type: Schema.Literal<["solo:pong"]>;
|
|
27
|
+
}>, Schema.Struct<{
|
|
28
|
+
type: Schema.Literal<["solo:resume"]>;
|
|
29
|
+
sessionId: Schema.filter<typeof Schema.NonEmptyString>;
|
|
30
|
+
resumeToken: Schema.optional<Schema.filter<typeof Schema.NonEmptyString>>;
|
|
31
|
+
}>]>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* Frame-level validation for the solo-mode WebSocket (issue #734 hardening).
|
|
4
|
+
* One schema per `SoloClientMessage` union member in `./solo-ws.ts` —
|
|
5
|
+
* mirrors the hand-written types (no `Schema.Type` derivation feeds back into
|
|
6
|
+
* `SoloClientMessage`, see `solo-connection-handler.ts`). Keep in sync with
|
|
7
|
+
* `solo-ws.ts` whenever a frame is added/changed.
|
|
8
|
+
*/
|
|
9
|
+
// Tokens ride the first frame; generous cap is a DoS guard only.
|
|
10
|
+
const TokenSchema = Schema.NonEmptyString.pipe(Schema.maxLength(4096));
|
|
11
|
+
// DoS guard: ids that round-trip through session state.
|
|
12
|
+
const BoundedIdSchema = Schema.NonEmptyString.pipe(Schema.maxLength(200));
|
|
13
|
+
const SoloAuthMessageSchema = Schema.Struct({
|
|
14
|
+
type: Schema.Literal("auth"),
|
|
15
|
+
token: TokenSchema,
|
|
16
|
+
});
|
|
17
|
+
const SoloStartMessageSchema = Schema.Struct({
|
|
18
|
+
type: Schema.Literal("solo:start"),
|
|
19
|
+
categoryIds: Schema.Array(Schema.NonEmptyString).pipe(Schema.maxItems(50)),
|
|
20
|
+
// Plain `number` on the hand-written type (clamped to [5,120] downstream) —
|
|
21
|
+
// bounded generously here against absurd/non-finite values.
|
|
22
|
+
timerDuration: Schema.Number.pipe(Schema.finite(), Schema.greaterThan(0), Schema.lessThanOrEqualTo(600)),
|
|
23
|
+
// 0 is a valid sentinel for endless mode (draw count is uncapped downstream).
|
|
24
|
+
questionCount: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(0), Schema.lessThanOrEqualTo(500)),
|
|
25
|
+
mode: Schema.Literal("normal", "endless"),
|
|
26
|
+
difficulty: Schema.Literal("all", "easy", "medium", "hard"),
|
|
27
|
+
device: Schema.optional(Schema.Literal("mobile", "tablet", "desktop")),
|
|
28
|
+
});
|
|
29
|
+
const SoloAnswerMessageSchema = Schema.Struct({
|
|
30
|
+
type: Schema.Literal("solo:answer"),
|
|
31
|
+
answerId: BoundedIdSchema,
|
|
32
|
+
nonce: Schema.optional(BoundedIdSchema),
|
|
33
|
+
});
|
|
34
|
+
const SoloAnswerChangeMessageSchema = Schema.Struct({
|
|
35
|
+
type: Schema.Literal("solo:answer-change"),
|
|
36
|
+
});
|
|
37
|
+
const SoloNextMessageSchema = Schema.Struct({
|
|
38
|
+
type: Schema.Literal("solo:next"),
|
|
39
|
+
});
|
|
40
|
+
const SoloTabBlurMessageSchema = Schema.Struct({
|
|
41
|
+
type: Schema.Literal("solo:tab-blur"),
|
|
42
|
+
});
|
|
43
|
+
const SoloTabFocusMessageSchema = Schema.Struct({
|
|
44
|
+
type: Schema.Literal("solo:tab-focus"),
|
|
45
|
+
});
|
|
46
|
+
const SoloPongMessageSchema = Schema.Struct({
|
|
47
|
+
type: Schema.Literal("solo:pong"),
|
|
48
|
+
});
|
|
49
|
+
const SoloResumeMessageSchema = Schema.Struct({
|
|
50
|
+
type: Schema.Literal("solo:resume"),
|
|
51
|
+
sessionId: BoundedIdSchema,
|
|
52
|
+
resumeToken: Schema.optional(BoundedIdSchema),
|
|
53
|
+
});
|
|
54
|
+
// Discriminant: Effect Schema detects the shared `type: Schema.Literal(...)`
|
|
55
|
+
// field and short-circuits matching — every member needs a unique `type`.
|
|
56
|
+
export const SoloClientMessageSchema = Schema.Union(SoloAuthMessageSchema, SoloStartMessageSchema, SoloAnswerMessageSchema, SoloAnswerChangeMessageSchema, SoloNextMessageSchema, SoloTabBlurMessageSchema, SoloTabFocusMessageSchema, SoloPongMessageSchema, SoloResumeMessageSchema);
|
|
57
|
+
const _assertNoSoloClientMessageDrift = null;
|
package/dist/ws.d.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type { BadgeAward, BadgeRunParams } from "./badges.ts";
|
|
2
|
+
import type { ErrorCode } from "./error-codes.ts";
|
|
3
|
+
import type { AdvanceMode, Difficulty, DifficultyFilter, ScoringMode, TimerDuration } from "./game.settings.ts";
|
|
4
|
+
import type { Player, PlayerRole, QuizQuestion } from "./game.ts";
|
|
5
|
+
import type { GameAnswerRecord } from "./game-answer-record.ts";
|
|
6
|
+
import type { MediaType, QuestionType } from "./question.schema.ts";
|
|
7
|
+
import type { StreakFlourish } from "./streak-flourish.ts";
|
|
8
|
+
export type ClientMessage = WsAuthMessage | PlayerAnswerMessage | PlayerEmoteMessage | PlayerRoleMessage | PlayerLeaveMessage | CuratorStartMessage | CuratorAdvanceMessage | CuratorStartRoleSelectionMessage | CuratorIntermissionMessage | CuratorPlayerActionMessage | CuratorStopMessage | CuratorResetMessage | WsPongMessage;
|
|
9
|
+
/** First frame for native/display (cross-origin, no cookie): authenticates the
|
|
10
|
+
* socket with the bearer game token (ADR 0014). Web rides the cookie instead. */
|
|
11
|
+
export interface WsAuthMessage {
|
|
12
|
+
type: "auth";
|
|
13
|
+
token: string;
|
|
14
|
+
}
|
|
15
|
+
export interface PlayerAnswerMessage {
|
|
16
|
+
type: "player:answer";
|
|
17
|
+
answerId: string;
|
|
18
|
+
/** @deprecated identity is bound at the socket upgrade; ignored if sent. */
|
|
19
|
+
playerId?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Player reacts after answering; positioned at the long-press/right-click point. */
|
|
22
|
+
export interface PlayerEmoteMessage {
|
|
23
|
+
type: "player:emote";
|
|
24
|
+
emoteId: string;
|
|
25
|
+
offsetX: number;
|
|
26
|
+
offsetY: number;
|
|
27
|
+
}
|
|
28
|
+
/** Player picks a role during role selection. */
|
|
29
|
+
export interface PlayerRoleMessage {
|
|
30
|
+
type: "player:role";
|
|
31
|
+
role: PlayerRole;
|
|
32
|
+
}
|
|
33
|
+
/** Player leaves the lobby/game voluntarily. */
|
|
34
|
+
export interface PlayerLeaveMessage {
|
|
35
|
+
type: "player:leave";
|
|
36
|
+
}
|
|
37
|
+
/** Curator starts the game. Questions are drawn server-side from categories,
|
|
38
|
+
* or taken from a curator-side import (preloadedQuestions). */
|
|
39
|
+
export interface CuratorStartMessage {
|
|
40
|
+
type: "curator:start";
|
|
41
|
+
categoryIds?: string[];
|
|
42
|
+
timerDuration?: TimerDuration;
|
|
43
|
+
questionsPerRound?: number;
|
|
44
|
+
difficulty?: DifficultyFilter;
|
|
45
|
+
advanceMode?: AdvanceMode;
|
|
46
|
+
scoringMode?: ScoringMode;
|
|
47
|
+
preloadedQuestions?: QuizQuestion[];
|
|
48
|
+
}
|
|
49
|
+
export interface CuratorAdvanceMessage {
|
|
50
|
+
type: "curator:advance";
|
|
51
|
+
}
|
|
52
|
+
/** Curator stops the game early — remaining questions drop, the open
|
|
53
|
+
* question miss-fills, and the game finishes on the answers given. */
|
|
54
|
+
export interface CuratorStopMessage {
|
|
55
|
+
type: "curator:stop";
|
|
56
|
+
}
|
|
57
|
+
/** Curator sends everyone back to setup — membership drops, lobby and code
|
|
58
|
+
* stay alive, nothing is saved. */
|
|
59
|
+
export interface CuratorResetMessage {
|
|
60
|
+
type: "curator:reset";
|
|
61
|
+
}
|
|
62
|
+
/** Curator opens the end-of-game role-selection window. */
|
|
63
|
+
export interface CuratorStartRoleSelectionMessage {
|
|
64
|
+
type: "curator:start-role-selection";
|
|
65
|
+
}
|
|
66
|
+
export interface CuratorIntermissionMessage {
|
|
67
|
+
type: "curator:intermission";
|
|
68
|
+
action: "start" | "end";
|
|
69
|
+
}
|
|
70
|
+
export interface CuratorPlayerActionMessage {
|
|
71
|
+
type: "curator:player";
|
|
72
|
+
playerId: string;
|
|
73
|
+
action: "approve" | "reject" | "remove" | "block";
|
|
74
|
+
}
|
|
75
|
+
/** Keepalive reply to a server game:ping (survives Cloudflare's ~100s idle WS timeout). */
|
|
76
|
+
export interface WsPongMessage {
|
|
77
|
+
type: "pong";
|
|
78
|
+
}
|
|
79
|
+
export type ServerMessage = JoinAcceptedMessage | JoinRejectedMessage | LobbyUpdateMessage | LobbyEndedMessage | LobbyResetMessage | PlayerNicknameChangedMessage | GameStartMessage | GameQuestionMessage | GameTickMessage | PlayerAnsweredMessage | GameRoundResultMessage | GameFinalScoresMessage | GameResultsReadyMessage | RoleSelectionStartMessage | RoleSelectionUpdateMessage | RoleSelectionLockedMessage | PlayerRemovedMessage | GameIntermissionMessage | GameResumeMessage | PlayerEmoteBroadcastMessage | GameErrorMessage | GamePingMessage;
|
|
80
|
+
export interface JoinAcceptedMessage {
|
|
81
|
+
type: "join:accepted";
|
|
82
|
+
playerId: string;
|
|
83
|
+
quizName: string;
|
|
84
|
+
}
|
|
85
|
+
export interface JoinRejectedMessage {
|
|
86
|
+
type: "join:rejected";
|
|
87
|
+
reason: string;
|
|
88
|
+
}
|
|
89
|
+
export interface LobbyUpdateMessage {
|
|
90
|
+
type: "lobby:update";
|
|
91
|
+
players: readonly Player[];
|
|
92
|
+
gameStarted?: boolean;
|
|
93
|
+
accessMode?: "open" | "invite-only";
|
|
94
|
+
maxPlayers?: number | null;
|
|
95
|
+
/** Curator's live-edited quiz title. */
|
|
96
|
+
quizName?: string;
|
|
97
|
+
/** Curator's live-edited quiz description. */
|
|
98
|
+
description?: string;
|
|
99
|
+
/** Curator's live-edited category list. */
|
|
100
|
+
categories?: readonly string[];
|
|
101
|
+
/** Curator's live-edited difficulty filter. */
|
|
102
|
+
difficulty?: DifficultyFilter;
|
|
103
|
+
}
|
|
104
|
+
export interface LobbyEndedMessage {
|
|
105
|
+
type: "lobby:ended";
|
|
106
|
+
reason?: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The game was cleared so the curator can host the next quiz ("New quiz" or
|
|
110
|
+
* "Reset to setup"): every player keeps their seat with score 0 and returns to
|
|
111
|
+
* the lobby waiting room (phase → lobby); the curator's client goes to the
|
|
112
|
+
* create page to edit the quiz.
|
|
113
|
+
*/
|
|
114
|
+
export interface LobbyResetMessage {
|
|
115
|
+
type: "lobby:reset";
|
|
116
|
+
}
|
|
117
|
+
export interface PlayerNicknameChangedMessage {
|
|
118
|
+
type: "player:nickname_changed";
|
|
119
|
+
playerId: string;
|
|
120
|
+
nickname: string;
|
|
121
|
+
}
|
|
122
|
+
export interface GameStartMessage {
|
|
123
|
+
type: "game:start";
|
|
124
|
+
totalQuestions: number;
|
|
125
|
+
timerDuration: TimerDuration;
|
|
126
|
+
advanceMode: AdvanceMode;
|
|
127
|
+
}
|
|
128
|
+
/** Question sent to players — isCorrect is omitted to prevent cheating */
|
|
129
|
+
export interface PlayerQuestionAnswer {
|
|
130
|
+
id: string;
|
|
131
|
+
text: string;
|
|
132
|
+
}
|
|
133
|
+
export interface GameQuestionMessage {
|
|
134
|
+
type: "game:question";
|
|
135
|
+
/** The live `questions` table id — stable across the shuffle instance that
|
|
136
|
+
* built this round's `answers[]` ids (question-flagging, ADR 0020). Never
|
|
137
|
+
* compare answer ids across questions/games; this is the one stable
|
|
138
|
+
* identifier for "which question is this". */
|
|
139
|
+
questionId: string;
|
|
140
|
+
questionIndex: number;
|
|
141
|
+
totalQuestions: number;
|
|
142
|
+
text: string;
|
|
143
|
+
categoryId: string;
|
|
144
|
+
difficulty?: Difficulty;
|
|
145
|
+
questionType?: QuestionType;
|
|
146
|
+
answers: PlayerQuestionAnswer[];
|
|
147
|
+
matchItems?: {
|
|
148
|
+
left: string[];
|
|
149
|
+
right: string[];
|
|
150
|
+
};
|
|
151
|
+
mediaUrl?: string;
|
|
152
|
+
mediaType?: MediaType;
|
|
153
|
+
timeRemaining: number;
|
|
154
|
+
serverTs: number;
|
|
155
|
+
/** This recipient's own already-submitted answer, if any — set only on a
|
|
156
|
+
* snapshot replay so a mid-question (re)connect restores the locked/answered
|
|
157
|
+
* UI instead of re-opening the question. Omitted on the live broadcast. */
|
|
158
|
+
yourAnswerId?: string;
|
|
159
|
+
/** Current answered tally, set only on a snapshot replay so the live
|
|
160
|
+
* "N of M answered" progress survives a mid-question reconnect (otherwise it
|
|
161
|
+
* resets to 0 until the next player:answered frame). Omitted on broadcast. */
|
|
162
|
+
answeredCount?: number;
|
|
163
|
+
answeredTotal?: number;
|
|
164
|
+
}
|
|
165
|
+
export interface GameTickMessage {
|
|
166
|
+
type: "game:tick";
|
|
167
|
+
timeRemaining: number;
|
|
168
|
+
serverTs: number;
|
|
169
|
+
/** Live answered tally piggybacked on the tick so players get "N of M
|
|
170
|
+
* answered" progress without a per-answer broadcast (that stays curator/
|
|
171
|
+
* display-only). Coarse to the 1s tick — fine for a progress cue. */
|
|
172
|
+
answeredCount?: number;
|
|
173
|
+
answeredTotal?: number;
|
|
174
|
+
}
|
|
175
|
+
export interface PlayerAnsweredMessage {
|
|
176
|
+
type: "player:answered";
|
|
177
|
+
playerId: string;
|
|
178
|
+
answeredCount: number;
|
|
179
|
+
totalCount: number;
|
|
180
|
+
}
|
|
181
|
+
export interface GameRoundResultMessage {
|
|
182
|
+
type: "game:round-result";
|
|
183
|
+
correctAnswerId: string;
|
|
184
|
+
players: Player[];
|
|
185
|
+
playerAnswers: Record<string, string>;
|
|
186
|
+
/** The curator's explanation of the answer, withheld until now (map #912,
|
|
187
|
+
* decision 2). Omitted when the question has none. */
|
|
188
|
+
postAnswerNote?: string;
|
|
189
|
+
/** The recipient's own consecutive-correct streak and flourish — set per
|
|
190
|
+
* connection by the transport, never broadcast, so each player sees only
|
|
191
|
+
* their own streak (mirrors solo). */
|
|
192
|
+
yourStreak?: {
|
|
193
|
+
streak: number;
|
|
194
|
+
flourish: StreakFlourish | null;
|
|
195
|
+
};
|
|
196
|
+
/** Seconds left on the between-question countdown, carried on the
|
|
197
|
+
* reconnect replay only, so the ring resumes where the server's advance
|
|
198
|
+
* timer stands rather than at the full delay. */
|
|
199
|
+
countdownRemaining?: number;
|
|
200
|
+
}
|
|
201
|
+
export interface GameFinalScoresMessage {
|
|
202
|
+
type: "game:final-scores";
|
|
203
|
+
/** The roster as it stood at game end, roles included. Later role changes
|
|
204
|
+
* arrive on `lobby:update` and never rewrite this list. */
|
|
205
|
+
players: Player[];
|
|
206
|
+
/** The recipient's own badges — set per-connection by the transport, never
|
|
207
|
+
* broadcast, so each player sees only their honours (ADR 0018). */
|
|
208
|
+
yourBadges?: BadgeAward[];
|
|
209
|
+
/** Run params the client derives badge rarity from (rarity is never stored). */
|
|
210
|
+
badgeRun?: BadgeRunParams;
|
|
211
|
+
/** The recipient's own per-question answer log — set per-connection by the
|
|
212
|
+
* transport, never broadcast, so the results page has its breakdown before
|
|
213
|
+
* `game_results` is persisted (the persist call is fire-and-forget). */
|
|
214
|
+
yourBreakdown?: GameAnswerRecord[];
|
|
215
|
+
/** Present only when the curator stopped the game early. `served` is the
|
|
216
|
+
* completed-question count (a round still open at the stop is dropped);
|
|
217
|
+
* `planned` is the quiz length, which feeds history and badge floors. */
|
|
218
|
+
stopped?: {
|
|
219
|
+
served: number;
|
|
220
|
+
planned: number;
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Broadcast once the game's `game_results` row is persisted (the persist call
|
|
225
|
+
* from `game:final-scores` is fire-and-forget, so the row may not exist yet
|
|
226
|
+
* when a results page loads on arrival). Tells a results page that loaded
|
|
227
|
+
* early to reload its row-dependent state (canFlag, alreadyFlaggedIds).
|
|
228
|
+
*/
|
|
229
|
+
export interface GameResultsReadyMessage {
|
|
230
|
+
type: "game:results-ready";
|
|
231
|
+
}
|
|
232
|
+
export interface RoleSelectionStartMessage {
|
|
233
|
+
type: "role:selection-start";
|
|
234
|
+
players: Player[];
|
|
235
|
+
timeRemaining: number;
|
|
236
|
+
}
|
|
237
|
+
export interface RoleSelectionUpdateMessage {
|
|
238
|
+
type: "role:selection-update";
|
|
239
|
+
players: Player[];
|
|
240
|
+
timeRemaining: number;
|
|
241
|
+
}
|
|
242
|
+
export interface RoleSelectionLockedMessage {
|
|
243
|
+
type: "role:selection-locked";
|
|
244
|
+
players: Player[];
|
|
245
|
+
}
|
|
246
|
+
export interface PlayerRemovedMessage {
|
|
247
|
+
type: "player:removed";
|
|
248
|
+
playerId: string;
|
|
249
|
+
}
|
|
250
|
+
export interface GameIntermissionMessage {
|
|
251
|
+
type: "game:intermission";
|
|
252
|
+
frozenTimeRemaining: number;
|
|
253
|
+
}
|
|
254
|
+
export interface GameResumeMessage {
|
|
255
|
+
type: "game:resume";
|
|
256
|
+
timeRemaining: number;
|
|
257
|
+
}
|
|
258
|
+
/** Server broadcast of a player's emote to every connected client. */
|
|
259
|
+
export interface PlayerEmoteBroadcastMessage {
|
|
260
|
+
type: "player:emote";
|
|
261
|
+
playerId: string;
|
|
262
|
+
emoteId: string;
|
|
263
|
+
offsetX: number;
|
|
264
|
+
offsetY: number;
|
|
265
|
+
}
|
|
266
|
+
/** Server keepalive; client replies with {type:"pong"}. */
|
|
267
|
+
export interface GamePingMessage {
|
|
268
|
+
type: "game:ping";
|
|
269
|
+
}
|
|
270
|
+
/** Failure feedback for the acting client — sent only to the socket whose
|
|
271
|
+
* action failed (a start that drew zero questions, an answer from an
|
|
272
|
+
* observer, a malformed frame), never broadcast. */
|
|
273
|
+
export interface GameErrorMessage {
|
|
274
|
+
type: "game:error";
|
|
275
|
+
/** Machine code for the UI to map to copy. Store `ErrorCode`s plus two
|
|
276
|
+
* transport-only codes: a frame that failed decoding, or an unexpected
|
|
277
|
+
* server-side throw. */
|
|
278
|
+
code: ErrorCode | "invalid_message" | "internal";
|
|
279
|
+
/** Human-readable fallback when the UI has no copy for `code`. */
|
|
280
|
+
message: string;
|
|
281
|
+
}
|
package/dist/ws.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orakl/protocol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Orakl wire contract: WebSocket frames and API response schemas.",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/robcsaszar/orakl.git",
|
|
9
|
+
"directory": "packages/protocol"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"effect": "^3.22.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"effect": "^3.22.1",
|
|
24
|
+
"typescript": "^6.0.3",
|
|
25
|
+
"vitest": "^5.0.0"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
}
|
|
39
|
+
}
|