@chaos-overlords/kernel 0.7.0 → 0.8.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/domain/entities.d.ts +33 -2
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js.map +1 -1
- package/dist/logic/turn-logic.d.ts +10 -3
- package/dist/logic/turn-logic.d.ts.map +1 -1
- package/dist/logic/turn-logic.js +12 -4
- package/dist/logic/turn-logic.js.map +1 -1
- package/dist/ports/storage.d.ts +33 -1
- package/dist/ports/storage.d.ts.map +1 -1
- package/dist/services/EventPublisher.d.ts +2 -1
- package/dist/services/EventPublisher.d.ts.map +1 -1
- package/dist/services/EventPublisher.js +2 -1
- package/dist/services/EventPublisher.js.map +1 -1
- package/dist/services/LobbyService.d.ts +0 -1
- package/dist/services/LobbyService.d.ts.map +1 -1
- package/dist/services/LobbyService.js +43 -41
- package/dist/services/LobbyService.js.map +1 -1
- package/dist/services/MatchQueryService.d.ts.map +1 -1
- package/dist/services/MatchQueryService.js +4 -2
- package/dist/services/MatchQueryService.js.map +1 -1
- package/dist/services/SnapshotService.d.ts +5 -0
- package/dist/services/SnapshotService.d.ts.map +1 -1
- package/dist/services/SnapshotService.js +18 -1
- package/dist/services/SnapshotService.js.map +1 -1
- package/dist/services/TurnService.d.ts +21 -0
- package/dist/services/TurnService.d.ts.map +1 -1
- package/dist/services/TurnService.js +127 -14
- package/dist/services/TurnService.js.map +1 -1
- package/dist/testing/InMemoryStorage.d.ts +4 -1
- package/dist/testing/InMemoryStorage.d.ts.map +1 -1
- package/dist/testing/InMemoryStorage.js +69 -0
- package/dist/testing/InMemoryStorage.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { MatchEvent, MatchSettings, MatchStatus, OrderDocument, PlayerStatus, TurnStatus } from '@chaos-overlords/contracts';
|
|
2
2
|
export interface Match {
|
|
3
3
|
id: string;
|
|
4
|
-
/** Wire protocol
|
|
4
|
+
/** Wire protocol spoken by the client that created this session. */
|
|
5
5
|
protocolVersion: number;
|
|
6
|
+
/**
|
|
7
|
+
* Shape of the session as it is stored here.
|
|
8
|
+
*
|
|
9
|
+
* Separate from `protocolVersion` because the two answer different questions: the protocol says
|
|
10
|
+
* whether a client and this server can talk, and this says whether a client can pick the match
|
|
11
|
+
* up and keep playing it. A client that speaks a newer protocol resumes a match created under an
|
|
12
|
+
* older one as long as this is the version it plays.
|
|
13
|
+
*/
|
|
14
|
+
sessionVersion: number;
|
|
6
15
|
status: MatchStatus;
|
|
7
16
|
settings: MatchSettings;
|
|
8
17
|
hostPlayerId: string;
|
|
@@ -29,6 +38,14 @@ export interface Player {
|
|
|
29
38
|
/** Position in the match's monotonic join sequence; the host is always 0. */
|
|
30
39
|
joinOrder: number;
|
|
31
40
|
displayName: string;
|
|
41
|
+
/**
|
|
42
|
+
* The overlord face this player chose when they created or joined the match.
|
|
43
|
+
*
|
|
44
|
+
* Set once and never changed: every client builds its city from the roster, and the setup a city
|
|
45
|
+
* is generated from is part of the state hash the turn verdict is taken over, so a face that
|
|
46
|
+
* moved after the match started would read as a desync on any client that bootstrapped before it.
|
|
47
|
+
*/
|
|
48
|
+
portraitId: number;
|
|
32
49
|
/**
|
|
33
50
|
* SHA-256 of the player's bearer token, or null once the membership is revoked (kicked
|
|
34
51
|
* from a running match). A null hash matches no token, so revocation needs no extra check.
|
|
@@ -76,6 +93,8 @@ export interface TurnOrders {
|
|
|
76
93
|
ready: boolean;
|
|
77
94
|
submittedAt: Date | null;
|
|
78
95
|
}
|
|
96
|
+
/** `TurnOrders` without the document: what a seal decision and a match view actually read. */
|
|
97
|
+
export type OrderSummary = Pick<TurnOrders, 'matchId' | 'turn' | 'playerId' | 'ordersHash' | 'ready'>;
|
|
79
98
|
export interface TurnReport {
|
|
80
99
|
matchId: string;
|
|
81
100
|
turn: number;
|
|
@@ -88,14 +107,26 @@ export interface Snapshot {
|
|
|
88
107
|
matchId: string;
|
|
89
108
|
turn: number;
|
|
90
109
|
formatVersion: number;
|
|
91
|
-
/** Wire protocol
|
|
110
|
+
/** Wire protocol spoken by the client that serialized this snapshot. */
|
|
92
111
|
protocolVersion: number;
|
|
112
|
+
/** Session shape these bytes belong to; see `Match.sessionVersion`. */
|
|
113
|
+
sessionVersion: number;
|
|
93
114
|
stateHash: string;
|
|
94
115
|
uploadedByPlayerId: string;
|
|
95
116
|
uploadedAt: Date;
|
|
96
117
|
/** Base64 of the client's native snapshot bytes; the server never decodes it. */
|
|
97
118
|
body: string;
|
|
98
119
|
}
|
|
120
|
+
/** A snapshot row without its body. */
|
|
121
|
+
export type SnapshotSummary = Omit<Snapshot, 'body'>;
|
|
122
|
+
export type TakeoverDecision = 'computer' | 'wait';
|
|
123
|
+
export interface TakeoverVote {
|
|
124
|
+
matchId: string;
|
|
125
|
+
targetPlayerId: string;
|
|
126
|
+
voterPlayerId: string;
|
|
127
|
+
decision: TakeoverDecision;
|
|
128
|
+
castAt: Date;
|
|
129
|
+
}
|
|
99
130
|
export type PersistedEvent = MatchEvent;
|
|
100
131
|
export declare const ACTIVE_PLAYER: PlayerStatus;
|
|
101
132
|
export declare function activePlayers(players: readonly Player[]): Player[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../src/domain/entities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EACb,WAAW,EACX,aAAa,EACb,YAAY,EACZ,UAAU,EACX,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,
|
|
1
|
+
{"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../src/domain/entities.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entities.js","sourceRoot":"","sources":["../../src/domain/entities.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"entities.js","sourceRoot":"","sources":["../../src/domain/entities.ts"],"names":[],"mappings":"AAyJA,MAAM,CAAC,MAAM,aAAa,GAAiB,QAAQ,CAAA;AAEnD,MAAM,UAAU,aAAa,CAAC,OAA0B;IACtD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,aAAa,CAAC,CAAA;AACpE,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,iBAAiB,CAAC,OAA0B;IAC1D,OAAO,OAAO,CAAC,MAAM,CACnB,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,iBAAiB,CAC9E,CAAA;AACH,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Player, type TurnOrders, type TurnReport } from '../domain/entities';
|
|
2
2
|
/** Every human seat still being waited on has marked ready. An empty roster is never "ready". */
|
|
3
|
-
export declare function allActiveReady(players: readonly Player[], orders:
|
|
3
|
+
export declare function allActiveReady(players: readonly Player[], orders: ReadonlyArray<Pick<TurnOrders, 'playerId' | 'ready'>>): boolean;
|
|
4
4
|
export type Consensus = {
|
|
5
5
|
kind: 'pending';
|
|
6
6
|
} | {
|
|
@@ -31,12 +31,19 @@ export type Consensus = {
|
|
|
31
31
|
*/
|
|
32
32
|
export declare function authoritativeCandidates(players: readonly Player[], reports: readonly TurnReport[]): string[];
|
|
33
33
|
/**
|
|
34
|
-
* Compare the post-turn state hashes the
|
|
34
|
+
* Compare the post-turn state hashes the human seats reported.
|
|
35
35
|
*
|
|
36
36
|
* Without an authoritative hash: once everyone has reported, unanimous agreement confirms the
|
|
37
37
|
* turn and any disagreement flags a desync. With one (the host uploaded a snapshot for this
|
|
38
38
|
* turn), a report is counted only when it matches it, so stragglers reloading the snapshot can
|
|
39
39
|
* converge without tripping a second desync.
|
|
40
|
+
*
|
|
41
|
+
* A seat that merely missed one timed deadline (`takeoverPending`) is still a human seat whose
|
|
42
|
+
* client applies every sealed turn, so its report is waited for like any other: confirming without
|
|
43
|
+
* it would refuse its later, possibly disagreeing, report as `turn_confirmed` and leave a genuine
|
|
44
|
+
* divergence undetected. The wait costs nothing the match was not already paying — an open
|
|
45
|
+
* absence vote pauses the turn clock — and the vote that makes the seat computer controlled
|
|
46
|
+
* re-runs the verdict without it.
|
|
40
47
|
*/
|
|
41
48
|
export declare function evaluateConsensus(players: readonly Player[], reports: readonly TurnReport[], authoritativeHash: string | null): Consensus;
|
|
42
49
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-logic.d.ts","sourceRoot":"","sources":["../../src/logic/turn-logic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"turn-logic.d.ts","sourceRoot":"","sources":["../../src/logic/turn-logic.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,UAAU,EACf,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAA;AAE3B,iGAAiG;AACjG,wBAAgB,cAAc,CAC5B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,MAAM,EAAE,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC,GAC5D,OAAO,CAKT;AAED,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAC3D;IACE,IAAI,EAAE,UAAU,CAAA;IAChB,OAAO,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACvD,gFAAgF;IAChF,oBAAoB,EAAE,MAAM,EAAE,CAAA;CAC/B,CAAA;AAEL;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,SAAS,UAAU,EAAE,GAC7B,MAAM,EAAE,CAaV;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAC/B,SAAS,CAgCX;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,YAAY,EAAE,MAAM,GACnB,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAU3C;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAElF"}
|
package/dist/logic/turn-logic.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { humanParticipants, } from '../domain/entities';
|
|
1
2
|
/** Every human seat still being waited on has marked ready. An empty roster is never "ready". */
|
|
2
3
|
export function allActiveReady(players, orders) {
|
|
3
|
-
const active = players
|
|
4
|
+
const active = humanParticipants(players);
|
|
4
5
|
if (active.length === 0)
|
|
5
6
|
return false;
|
|
6
7
|
const readyIds = new Set(orders.filter((row) => row.ready).map((row) => row.playerId));
|
|
@@ -20,7 +21,7 @@ export function allActiveReady(players, orders) {
|
|
|
20
21
|
* majority's state is right even when the host's own client happens to be the correct one.
|
|
21
22
|
*/
|
|
22
23
|
export function authoritativeCandidates(players, reports) {
|
|
23
|
-
const active = new Set(players
|
|
24
|
+
const active = new Set(humanParticipants(players).map((p) => p.id));
|
|
24
25
|
const counts = new Map();
|
|
25
26
|
for (const report of reports) {
|
|
26
27
|
if (!active.has(report.playerId))
|
|
@@ -36,15 +37,22 @@ export function authoritativeCandidates(players, reports) {
|
|
|
36
37
|
.sort();
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
|
-
* Compare the post-turn state hashes the
|
|
40
|
+
* Compare the post-turn state hashes the human seats reported.
|
|
40
41
|
*
|
|
41
42
|
* Without an authoritative hash: once everyone has reported, unanimous agreement confirms the
|
|
42
43
|
* turn and any disagreement flags a desync. With one (the host uploaded a snapshot for this
|
|
43
44
|
* turn), a report is counted only when it matches it, so stragglers reloading the snapshot can
|
|
44
45
|
* converge without tripping a second desync.
|
|
46
|
+
*
|
|
47
|
+
* A seat that merely missed one timed deadline (`takeoverPending`) is still a human seat whose
|
|
48
|
+
* client applies every sealed turn, so its report is waited for like any other: confirming without
|
|
49
|
+
* it would refuse its later, possibly disagreeing, report as `turn_confirmed` and leave a genuine
|
|
50
|
+
* divergence undetected. The wait costs nothing the match was not already paying — an open
|
|
51
|
+
* absence vote pauses the turn clock — and the vote that makes the seat computer controlled
|
|
52
|
+
* re-runs the verdict without it.
|
|
45
53
|
*/
|
|
46
54
|
export function evaluateConsensus(players, reports, authoritativeHash) {
|
|
47
|
-
const active = players
|
|
55
|
+
const active = humanParticipants(players);
|
|
48
56
|
if (active.length === 0)
|
|
49
57
|
return { kind: 'pending' };
|
|
50
58
|
const byPlayer = new Map(reports.map((report) => [report.playerId, report]));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-logic.js","sourceRoot":"","sources":["../../src/logic/turn-logic.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"turn-logic.js","sourceRoot":"","sources":["../../src/logic/turn-logic.ts"],"names":[],"mappings":"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"}
|
package/dist/ports/storage.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LobbyListing, MatchEventBody, MatchSettings, MatchStatus, TurnStatus } from '@chaos-overlords/contracts';
|
|
2
|
-
import type { Match, PersistedEvent, Player, SealedSlot, Snapshot, Turn, TurnOrders, TurnReport } from '../domain/entities';
|
|
2
|
+
import type { Match, OrderSummary, PersistedEvent, Player, SealedSlot, Snapshot, SnapshotSummary, TakeoverDecision, TakeoverVote, Turn, TurnOrders, TurnReport } from '../domain/entities';
|
|
3
3
|
/**
|
|
4
4
|
* Persistence ports. There are NO transactions: D1 has none, so every invariant that two writers
|
|
5
5
|
* could race on is a single conditional statement whose row count says who won. Each `transition`
|
|
@@ -100,6 +100,12 @@ export interface TurnRepository {
|
|
|
100
100
|
}): Promise<boolean>;
|
|
101
101
|
getOrders(matchId: string, number: number, playerId: string): Promise<TurnOrders | null>;
|
|
102
102
|
listOrders(matchId: string, number: number): Promise<TurnOrders[]>;
|
|
103
|
+
/**
|
|
104
|
+
* The readiness and digest of every row of a turn, WITHOUT the documents. The seal decision,
|
|
105
|
+
* the digest of the sealed set and the match view need only these; loading a quarter-megabyte
|
|
106
|
+
* document per player to read a flag is what this projection avoids.
|
|
107
|
+
*/
|
|
108
|
+
listOrderSummaries(matchId: string, number: number): Promise<OrderSummary[]>;
|
|
103
109
|
transition(matchId: string, number: number, from: readonly TurnStatus[], patch: {
|
|
104
110
|
status: TurnStatus;
|
|
105
111
|
sealedAt?: Date;
|
|
@@ -128,6 +134,8 @@ export interface SnapshotRepository {
|
|
|
128
134
|
put(snapshot: Snapshot): Promise<void>;
|
|
129
135
|
get(matchId: string, turn: number): Promise<Snapshot | null>;
|
|
130
136
|
getLatest(matchId: string): Promise<Snapshot | null>;
|
|
137
|
+
/** The newest snapshot's metadata without its body, which is a megabyte a caller checking for existence never reads. */
|
|
138
|
+
getLatestSummary(matchId: string): Promise<SnapshotSummary | null>;
|
|
131
139
|
/**
|
|
132
140
|
* Keep only the `keep` newest turns' snapshots of a match, dropping the rest. Returns how many
|
|
133
141
|
* went. Retention collects whole terminated matches; this bounds what a single LIVE match holds,
|
|
@@ -150,10 +158,34 @@ export interface EventRepository {
|
|
|
150
158
|
/** Highest sequence number persisted for the match, or 0 when the log is empty. */
|
|
151
159
|
lastSeq(matchId: string): Promise<number>;
|
|
152
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Absence prompts and their votes, as durable state rather than a replay of the event log.
|
|
163
|
+
*
|
|
164
|
+
* A prompt is open from the moment a human seat goes quiet (a departure, a kick, a wholly missed
|
|
165
|
+
* timed turn) until the seat returns or is voted to the computer. The open-turn clock is paused
|
|
166
|
+
* while a match has any, and every vote is judged against the prompts on file, so both questions
|
|
167
|
+
* are one indexed read instead of a scan of every event the match ever logged.
|
|
168
|
+
*/
|
|
169
|
+
export interface TakeoverRepository {
|
|
170
|
+
/** Opens a prompt for the seat; false when one is already open, so a caller can announce only the first. */
|
|
171
|
+
openPrompt(matchId: string, playerId: string, turn: number, openedAt: Date): Promise<boolean>;
|
|
172
|
+
/** Closes the prompt and discards its votes. A no-op when none is open. */
|
|
173
|
+
closePrompt(matchId: string, playerId: string): Promise<void>;
|
|
174
|
+
hasOpenPrompts(matchId: string): Promise<boolean>;
|
|
175
|
+
/** Player ids with an open prompt, in a stable order. */
|
|
176
|
+
listOpenPrompts(matchId: string): Promise<string[]>;
|
|
177
|
+
/**
|
|
178
|
+
* Records or replaces one voter's choice on an open prompt, in ONE statement conditional on the
|
|
179
|
+
* prompt being open. False when it is not, so a vote can never outlive the prompt it answers.
|
|
180
|
+
*/
|
|
181
|
+
castVote(matchId: string, targetPlayerId: string, voterPlayerId: string, decision: TakeoverDecision, castAt: Date): Promise<boolean>;
|
|
182
|
+
listVotes(matchId: string, targetPlayerId: string): Promise<TakeoverVote[]>;
|
|
183
|
+
}
|
|
153
184
|
export interface MultiplayerStorage {
|
|
154
185
|
matches: MatchRepository;
|
|
155
186
|
players: PlayerRepository;
|
|
156
187
|
turns: TurnRepository;
|
|
188
|
+
takeovers: TakeoverRepository;
|
|
157
189
|
snapshots: SnapshotRepository;
|
|
158
190
|
events: EventRepository;
|
|
159
191
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/ports/storage.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/ports/storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,7 +10,8 @@ import type { KernelDeps } from './deps';
|
|
|
10
10
|
* "write the row" would publish a hole that a monotonic cursor then skips forever.
|
|
11
11
|
*
|
|
12
12
|
* Fan-out is only a wake-up hint. It happens after the event is durable and its failure is logged,
|
|
13
|
-
* never propagated: the
|
|
13
|
+
* never propagated: the stream's heartbeat catch-up (or its next ordinary drain) delivers the
|
|
14
|
+
* event anyway.
|
|
14
15
|
*/
|
|
15
16
|
export declare class EventPublisher {
|
|
16
17
|
private readonly deps;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventPublisher.d.ts","sourceRoot":"","sources":["../../src/services/EventPublisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAExC
|
|
1
|
+
{"version":3,"file":"EventPublisher.d.ts","sourceRoot":"","sources":["../../src/services/EventPublisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAExC;;;;;;;;;;;GAWG;AACH,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC;IAGhF,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;CAkB9E"}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* "write the row" would publish a hole that a monotonic cursor then skips forever.
|
|
8
8
|
*
|
|
9
9
|
* Fan-out is only a wake-up hint. It happens after the event is durable and its failure is logged,
|
|
10
|
-
* never propagated: the
|
|
10
|
+
* never propagated: the stream's heartbeat catch-up (or its next ordinary drain) delivers the
|
|
11
|
+
* event anyway.
|
|
11
12
|
*/
|
|
12
13
|
export class EventPublisher {
|
|
13
14
|
deps;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventPublisher.js","sourceRoot":"","sources":["../../src/services/EventPublisher.ts"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"EventPublisher.js","sourceRoot":"","sources":["../../src/services/EventPublisher.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,cAAc;IAEN;IADnB,YACmB,IAAmE;QAAnE,SAAI,GAAJ,IAAI,CAA+D;IACnF,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,OAAe,EAAE,IAAoB;QACjD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;YAClD,GAAG,IAAI;YACP,OAAO;YACP,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;SAC/C,CAAC,CAAA;QACF,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,6FAA6F;YAC7F,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE;gBACjD,OAAO;gBACP,GAAG,EAAE,KAAK,CAAC,GAAG;gBACd,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;aACrB,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;CACF"}
|
|
@@ -49,7 +49,6 @@ export declare class LobbyService {
|
|
|
49
49
|
*/
|
|
50
50
|
private refuseDuplicateName;
|
|
51
51
|
private assertNameIsFree;
|
|
52
|
-
private currentTakeoverVotes;
|
|
53
52
|
/**
|
|
54
53
|
* Undo a join that could not be completed. The token was never returned, so the row is
|
|
55
54
|
* unreachable: leaving it would hold a seat and keep `allActiveReady` waiting forever on a player
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LobbyService.d.ts","sourceRoot":"","sources":["../../src/services/LobbyService.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAE5B,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAA;AAMnC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAEtD,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"LobbyService.d.ts","sourceRoot":"","sources":["../../src/services/LobbyService.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAE5B,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAA;AAMnC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAEtD,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,eAAe,CAAA;AAgB5D,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,MAAM,CAAA;CACrB;AAED,0EAA0E;AAC1E,qBAAa,YAAY;IAKrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IANxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;gBAGjB,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,cAAc,EACzB,KAAK,EAAE,WAAW,EACnC,OAAO,GAAE,mBAAwB;IAM7B,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC;IA8CjE,IAAI,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC;IA2DxD,WAAW,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,cAAc,CAAC;IAqEtE,KAAK,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1C,MAAM,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA0D3C,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB5E,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAevE;;;OAGG;IACG,cAAc,CAClB,SAAS,EAAE,SAAS,EACpB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC;IAuFV,KAAK,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;YAoClC,MAAM;IA6DpB;;;OAGG;YACW,MAAM;IAYpB;;;;;;;;;;;;OAYG;YACW,mBAAmB;IAIjC,OAAO,CAAC,gBAAgB;IASxB;;;;OAIG;YACW,YAAY;YAaZ,OAAO;IAcrB;;;;OAIG;YACW,uBAAuB;YAUvB,UAAU;CAQzB"}
|
|
@@ -12,8 +12,12 @@ const MIN_PLAYERS_TO_START = LIMITS.minPlayers;
|
|
|
12
12
|
const JOIN_CODE_ATTEMPTS = 5;
|
|
13
13
|
/** The host always holds the first position in the join sequence. */
|
|
14
14
|
const HOST_JOIN_ORDER = 0;
|
|
15
|
+
/** The face a client that predates portrait selection is seated with. */
|
|
16
|
+
const DEFAULT_PORTRAIT_ID = 0;
|
|
15
17
|
/** Host statuses that mean the seat is genuinely empty and the role may move. */
|
|
16
18
|
const VACANT_HOST_STATUSES = ['left', 'kicked', 'computer'];
|
|
19
|
+
/** A human seat nobody is playing: the ones an absence vote can hand to the computer. */
|
|
20
|
+
const ABSENT_HUMAN_STATUSES = ['takeoverPending', 'left', 'kicked'];
|
|
17
21
|
/** Match creation, joining, leaving, kicking and the start transition. */
|
|
18
22
|
export class LobbyService {
|
|
19
23
|
deps;
|
|
@@ -37,6 +41,7 @@ export class LobbyService {
|
|
|
37
41
|
const match = await this.createWithFreshJoinCode({
|
|
38
42
|
id: matchId,
|
|
39
43
|
protocolVersion: request.protocolVersion ?? 1,
|
|
44
|
+
sessionVersion: request.sessionVersion ?? 1,
|
|
40
45
|
status: 'lobby',
|
|
41
46
|
settings: request.settings,
|
|
42
47
|
hostPlayerId: hostId,
|
|
@@ -55,6 +60,7 @@ export class LobbyService {
|
|
|
55
60
|
slot: -1,
|
|
56
61
|
joinOrder: HOST_JOIN_ORDER,
|
|
57
62
|
displayName: request.hostDisplayName,
|
|
63
|
+
portraitId: request.hostPortraitId ?? DEFAULT_PORTRAIT_ID,
|
|
58
64
|
tokenHash: await hashToken(token),
|
|
59
65
|
status: 'active',
|
|
60
66
|
joinedAt: now,
|
|
@@ -102,6 +108,7 @@ export class LobbyService {
|
|
|
102
108
|
slot: -1,
|
|
103
109
|
joinOrder,
|
|
104
110
|
displayName: request.displayName,
|
|
111
|
+
portraitId: request.portraitId ?? DEFAULT_PORTRAIT_ID,
|
|
105
112
|
tokenHash: await hashToken(token),
|
|
106
113
|
status: 'active',
|
|
107
114
|
joinedAt: this.deps.clock.now(),
|
|
@@ -148,7 +155,7 @@ export class LobbyService {
|
|
|
148
155
|
reason: 'late_join_disabled',
|
|
149
156
|
});
|
|
150
157
|
}
|
|
151
|
-
if (!(await this.deps.storage.snapshots.
|
|
158
|
+
if (!(await this.deps.storage.snapshots.getLatestSummary(match.id))) {
|
|
152
159
|
throw new ConflictError('Late join is available after the bootstrap snapshot is uploaded', {
|
|
153
160
|
reason: 'late_join_not_ready',
|
|
154
161
|
});
|
|
@@ -178,6 +185,7 @@ export class LobbyService {
|
|
|
178
185
|
slot: request.slot,
|
|
179
186
|
joinOrder: match.joinCounter,
|
|
180
187
|
displayName: request.displayName,
|
|
188
|
+
portraitId: request.portraitId ?? DEFAULT_PORTRAIT_ID,
|
|
181
189
|
tokenHash: await hashToken(token),
|
|
182
190
|
status: 'active',
|
|
183
191
|
joinedAt: this.deps.clock.now(),
|
|
@@ -218,11 +226,21 @@ export class LobbyService {
|
|
|
218
226
|
// `open` is idempotent and tops up missing participant rows even when the turn already exists.
|
|
219
227
|
await this.deps.storage.turns.open(openTurn, [player.id]);
|
|
220
228
|
}
|
|
229
|
+
await this.deps.storage.takeovers.closePrompt(match.id, player.id);
|
|
221
230
|
await this.publisher.publish(match.id, {
|
|
222
231
|
type: 'match.playerReturned',
|
|
223
232
|
payload: { playerId: player.id, replacedComputer },
|
|
224
233
|
});
|
|
225
234
|
const players = await this.deps.storage.players.listByMatch(match.id);
|
|
235
|
+
// A seat that went quiet while nobody was present to ask was never put to a vote (see
|
|
236
|
+
// `remove`), and one whose vote closed with the last present player leaving has no prompt
|
|
237
|
+
// either. The returning player is present now, so every absent human seat is put to them; a
|
|
238
|
+
// prompt that is already open is left exactly as it is.
|
|
239
|
+
for (const absent of players) {
|
|
240
|
+
if (absent.id === player.id || !ABSENT_HUMAN_STATUSES.includes(absent.status))
|
|
241
|
+
continue;
|
|
242
|
+
await this.turns.openTakeoverPrompt(match.id, absent.id, match.currentTurn);
|
|
243
|
+
}
|
|
226
244
|
const currentHost = players.find((candidate) => candidate.id === match.hostPlayerId);
|
|
227
245
|
// Only a host who is gone is replaced. `takeoverPending` is set on a player who is still
|
|
228
246
|
// connected and merely missed one timed deadline, so treating it as absence would hand the role
|
|
@@ -240,6 +258,7 @@ export class LobbyService {
|
|
|
240
258
|
});
|
|
241
259
|
}
|
|
242
260
|
await this.turns.reevaluate(match.id);
|
|
261
|
+
await this.turns.resumeAfterTakeoverVotes(match.id);
|
|
243
262
|
}
|
|
244
263
|
async updateSettings(principal, settings) {
|
|
245
264
|
requireHost(principal);
|
|
@@ -283,24 +302,40 @@ export class LobbyService {
|
|
|
283
302
|
if (!target || target.matchId !== match.id) {
|
|
284
303
|
throw new NotFoundError('No such player in this match', { reason: 'unknown_player' });
|
|
285
304
|
}
|
|
286
|
-
if (!
|
|
305
|
+
if (!ABSENT_HUMAN_STATUSES.includes(target.status)) {
|
|
287
306
|
throw new ConflictError('That player is not awaiting a takeover vote', {
|
|
288
307
|
reason: 'takeover_not_pending',
|
|
289
308
|
});
|
|
290
309
|
}
|
|
310
|
+
const now = this.deps.clock.now();
|
|
311
|
+
if (!(await this.deps.storage.takeovers.castVote(match.id, target.id, player.id, request.decision, now))) {
|
|
312
|
+
// The seat is absent but nobody asked about it yet: it went quiet before this table existed,
|
|
313
|
+
// or while nobody was present to ask. The vote itself opens the question, so the seat can
|
|
314
|
+
// still be decided rather than left idle for the rest of the match.
|
|
315
|
+
await this.turns.openTakeoverPrompt(match.id, target.id, match.currentTurn);
|
|
316
|
+
if (!(await this.deps.storage.takeovers.castVote(match.id, target.id, player.id, request.decision, now))) {
|
|
317
|
+
throw new ConflictError('That player is not awaiting a takeover vote', {
|
|
318
|
+
reason: 'takeover_not_pending',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
291
322
|
await this.publisher.publish(match.id, {
|
|
292
323
|
type: 'match.takeoverVoteCast',
|
|
293
324
|
payload: { playerId: target.id, voterPlayerId: player.id, decision: request.decision },
|
|
294
325
|
});
|
|
295
326
|
if (request.decision === 'wait')
|
|
296
327
|
return;
|
|
297
|
-
const votes = await this.
|
|
328
|
+
const votes = new Map((await this.deps.storage.takeovers.listVotes(match.id, target.id)).map((vote) => [
|
|
329
|
+
vote.voterPlayerId,
|
|
330
|
+
vote.decision,
|
|
331
|
+
]));
|
|
298
332
|
const voters = activePlayers(await this.deps.storage.players.listByMatch(match.id));
|
|
299
333
|
if (voters.length === 0 || voters.some((voter) => votes.get(voter.id) !== 'computer'))
|
|
300
334
|
return;
|
|
301
335
|
if (!(await this.deps.storage.players.transitionStatus(target.id, [target.status], 'computer'))) {
|
|
302
336
|
return;
|
|
303
337
|
}
|
|
338
|
+
await this.deps.storage.takeovers.closePrompt(match.id, target.id);
|
|
304
339
|
await this.publisher.publish(match.id, {
|
|
305
340
|
type: 'match.playerTakenOver',
|
|
306
341
|
payload: { playerId: target.id },
|
|
@@ -319,6 +354,7 @@ export class LobbyService {
|
|
|
319
354
|
}
|
|
320
355
|
}
|
|
321
356
|
await this.turns.reevaluate(match.id);
|
|
357
|
+
await this.turns.resumeAfterTakeoverVotes(match.id);
|
|
322
358
|
}
|
|
323
359
|
async start(principal) {
|
|
324
360
|
const { match } = principal;
|
|
@@ -394,18 +430,16 @@ export class LobbyService {
|
|
|
394
430
|
});
|
|
395
431
|
// A seat that was already absent changes no tally and holds no host role: whatever vote or
|
|
396
432
|
// succession its absence called for ran when it went quiet, and it is not a seat any turn is
|
|
397
|
-
// waiting on now.
|
|
398
|
-
if (!wasActive)
|
|
433
|
+
// waiting on now. Nor does a match that is over have anything left to vote on.
|
|
434
|
+
if (!wasActive || (match.status !== 'running' && match.status !== 'desynced'))
|
|
399
435
|
return;
|
|
400
436
|
const remaining = activePlayers(await this.deps.storage.players.listByMatch(match.id));
|
|
401
437
|
if (remaining.length === 0) {
|
|
402
438
|
// Keep the durable match available. The first former member to rejoin becomes host.
|
|
403
439
|
return;
|
|
404
440
|
}
|
|
405
|
-
await this.
|
|
406
|
-
|
|
407
|
-
payload: { playerId: target.id, turn: match.currentTurn },
|
|
408
|
-
});
|
|
441
|
+
await this.turns.openTakeoverPrompt(match.id, target.id, match.currentTurn);
|
|
442
|
+
await this.turns.pauseForTakeoverVote(match.id);
|
|
409
443
|
if (target.id === match.hostPlayerId) {
|
|
410
444
|
const successor = remaining[0];
|
|
411
445
|
await this.deps.storage.matches.transition(match.id, ['running', 'desynced'], {
|
|
@@ -460,38 +494,6 @@ export class LobbyService {
|
|
|
460
494
|
});
|
|
461
495
|
}
|
|
462
496
|
}
|
|
463
|
-
async currentTakeoverVotes(matchId, targetPlayerId) {
|
|
464
|
-
const votes = new Map();
|
|
465
|
-
let requested = false;
|
|
466
|
-
let after = 0;
|
|
467
|
-
for (;;) {
|
|
468
|
-
const page = await this.deps.storage.events.listAfter(matchId, after, 200);
|
|
469
|
-
if (page.length === 0)
|
|
470
|
-
return votes;
|
|
471
|
-
for (const event of page) {
|
|
472
|
-
after = event.seq;
|
|
473
|
-
if (event.type === 'match.takeoverVoteRequested' &&
|
|
474
|
-
event.payload.playerId === targetPlayerId) {
|
|
475
|
-
requested = true;
|
|
476
|
-
votes.clear();
|
|
477
|
-
}
|
|
478
|
-
else if ((event.type === 'match.takeoverVoteCancelled' ||
|
|
479
|
-
event.type === 'match.playerTakenOver' ||
|
|
480
|
-
event.type === 'match.playerReturned') &&
|
|
481
|
-
event.payload.playerId === targetPlayerId) {
|
|
482
|
-
requested = false;
|
|
483
|
-
votes.clear();
|
|
484
|
-
}
|
|
485
|
-
else if (requested &&
|
|
486
|
-
event.type === 'match.takeoverVoteCast' &&
|
|
487
|
-
event.payload.playerId === targetPlayerId) {
|
|
488
|
-
votes.set(event.payload.voterPlayerId, event.payload.decision);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
if (page.length < 200)
|
|
492
|
-
return votes;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
497
|
/**
|
|
496
498
|
* Undo a join that could not be completed. The token was never returned, so the row is
|
|
497
499
|
* unreachable: leaving it would hold a seat and keep `allActiveReady` waiting forever on a player
|