@chaos-overlords/kernel 0.9.1 → 0.9.2
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 +7 -2
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js +6 -0
- package/dist/domain/entities.js.map +1 -1
- package/dist/logic/turn-logic.d.ts +9 -1
- package/dist/logic/turn-logic.d.ts.map +1 -1
- package/dist/logic/turn-logic.js +14 -1
- package/dist/logic/turn-logic.js.map +1 -1
- package/dist/ports/runtime.d.ts +2 -0
- package/dist/ports/runtime.d.ts.map +1 -1
- package/dist/ports/storage.d.ts +25 -8
- package/dist/ports/storage.d.ts.map +1 -1
- package/dist/services/LobbyService.d.ts +18 -0
- package/dist/services/LobbyService.d.ts.map +1 -1
- package/dist/services/LobbyService.js +71 -23
- package/dist/services/LobbyService.js.map +1 -1
- package/dist/services/RateLimiter.d.ts +8 -0
- package/dist/services/RateLimiter.d.ts.map +1 -1
- package/dist/services/RateLimiter.js +32 -8
- package/dist/services/RateLimiter.js.map +1 -1
- package/dist/services/RetentionService.d.ts +50 -10
- package/dist/services/RetentionService.d.ts.map +1 -1
- package/dist/services/RetentionService.js +36 -11
- package/dist/services/RetentionService.js.map +1 -1
- package/dist/services/TurnService.d.ts +5 -0
- package/dist/services/TurnService.d.ts.map +1 -1
- package/dist/services/TurnService.js +43 -11
- package/dist/services/TurnService.js.map +1 -1
- package/dist/testing/InMemoryStorage.d.ts.map +1 -1
- package/dist/testing/InMemoryStorage.js +25 -1
- package/dist/testing/InMemoryStorage.js.map +1 -1
- package/package.json +2 -2
|
@@ -23,8 +23,11 @@ export interface Match {
|
|
|
23
23
|
/** Seats taken in the lobby; capacity is enforced on this counter atomically. */
|
|
24
24
|
seatCount: number;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* The next position in the match's join sequence: advanced by every lobby seat claimed and every
|
|
27
|
+
* late-join position taken, including claims whose join then failed, so it is an upper bound on
|
|
28
|
+
* the players ever seated rather than a count of them. It never decreases, so the value handed to
|
|
29
|
+
* each player as `joinOrder` is a stable total order even after someone leaves and a newcomer
|
|
30
|
+
* takes the seat; gaps in that order carry no meaning.
|
|
28
31
|
*/
|
|
29
32
|
joinCounter: number;
|
|
30
33
|
createdAt: Date;
|
|
@@ -156,6 +159,8 @@ export interface TakeoverVote {
|
|
|
156
159
|
export type PersistedEvent = MatchEvent;
|
|
157
160
|
export declare const ACTIVE_PLAYER: PlayerStatus;
|
|
158
161
|
export declare function activePlayers(players: readonly Player[]): Player[];
|
|
162
|
+
/** A human seat nobody is playing: the ones an absence vote can hand to the computer. */
|
|
163
|
+
export declare const ABSENT_HUMAN_STATUSES: ReadonlyArray<PlayerStatus>;
|
|
159
164
|
/** A seat a human still holds: present, or absent and waited for while a takeover is voted on. */
|
|
160
165
|
export declare function isHumanParticipant(player: Pick<Player, 'status'>): boolean;
|
|
161
166
|
/** Human-controlled seats, including an absent player the lobby elected to keep waiting for. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../src/domain/entities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,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,oEAAoE;IACpE,eAAe,EAAE,MAAM,CAAA;IACvB;;;;;;;OAOG;IACH,cAAc,EAAE,MAAM,CAAA;IACtB,MAAM,EAAE,WAAW,CAAA;IACnB,QAAQ,EAAE,aAAa,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAA;IACnB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAA;IACjB
|
|
1
|
+
{"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../src/domain/entities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,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,oEAAoE;IACpE,eAAe,EAAE,MAAM,CAAA;IACvB;;;;;;;OAOG;IACH,cAAc,EAAE,MAAM,CAAA;IACtB,MAAM,EAAE,WAAW,CAAA;IACnB,QAAQ,EAAE,aAAa,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAA;IACnB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;CAChB;AAED,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,EAAE,YAAY,CAAA;IACpB,QAAQ,EAAE,IAAI,CAAA;CACf;AAED,+EAA+E;AAC/E,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,IAAI;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,UAAU,CAAA;IAClB,QAAQ,EAAE,IAAI,CAAA;IACd,UAAU,EAAE,IAAI,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAA;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B;;;;OAIG;IACH,WAAW,EAAE,SAAS,UAAU,EAAE,GAAG,IAAI,CAAA;IACzC;;;;;;;OAOG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;;;OAOG;IACH,UAAU,EAAE,IAAI,GAAG,IAAI,CAAA;CACxB;AAED,kGAAkG;AAClG,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,IAAI,GAAG,IAAI,CAAA;CACzB;AAED,8FAA8F;AAC9F,MAAM,MAAM,YAAY,GAAG,IAAI,CAC7B,UAAU,EACV,SAAS,GAAG,MAAM,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,CACzD,CAAA;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;IACjB,UAAU,EAAE,IAAI,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAA;IACvB,uEAAuE;IACvE,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,UAAU,EAAE,IAAI,CAAA;IAChB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAA;CACb;AAED,uCAAuC;AACvC,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;AAEpD;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAe,SAAQ,YAAY;IAClD,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,6FAA6F;AAC7F,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,MAAM,CAAA;AAElD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,MAAM,EAAE,IAAI,CAAA;CACb;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,CAAA;AAEvC,eAAO,MAAM,aAAa,EAAE,YAAuB,CAAA;AAEnD,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAElE;AAED,yFAAyF;AACzF,eAAO,MAAM,qBAAqB,EAAE,aAAa,CAAC,YAAY,CAI7D,CAAA;AAED,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,OAAO,CAE1E;AAED,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAEtE;AAED,4FAA4F;AAC5F,wBAAgB,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,OAAO,CAElE"}
|
package/dist/domain/entities.js
CHANGED
|
@@ -2,6 +2,12 @@ export const ACTIVE_PLAYER = 'active';
|
|
|
2
2
|
export function activePlayers(players) {
|
|
3
3
|
return players.filter((player) => player.status === ACTIVE_PLAYER);
|
|
4
4
|
}
|
|
5
|
+
/** A human seat nobody is playing: the ones an absence vote can hand to the computer. */
|
|
6
|
+
export const ABSENT_HUMAN_STATUSES = [
|
|
7
|
+
'takeoverPending',
|
|
8
|
+
'left',
|
|
9
|
+
'kicked',
|
|
10
|
+
];
|
|
5
11
|
/** A seat a human still holds: present, or absent and waited for while a takeover is voted on. */
|
|
6
12
|
export function isHumanParticipant(player) {
|
|
7
13
|
return player.status === 'active' || player.status === 'takeoverPending';
|
|
@@ -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":"AAyLA,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,yFAAyF;AACzF,MAAM,CAAC,MAAM,qBAAqB,GAAgC;IAChE,iBAAiB;IACjB,MAAM;IACN,QAAQ;CACT,CAAA;AAED,kGAAkG;AAClG,MAAM,UAAU,kBAAkB,CAAC,MAA8B;IAC/D,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,iBAAiB,CAAA;AAC1E,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,iBAAiB,CAAC,OAA0B;IAC1D,OAAO,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAA;AAC3C,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,YAAY,CAAC,KAA4B;IACvD,OAAO,KAAK,CAAC,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,UAAU,CAAA;AAClE,CAAC"}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { type Player, type TurnOrders, type TurnReport } from '../domain/entities';
|
|
1
|
+
import { type Player, type Turn, 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
3
|
export declare function allActiveReady(players: readonly Player[], orders: ReadonlyArray<Pick<TurnOrders, 'playerId' | 'ready'>>): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Whether the turn was sealed by its clock running out rather than by everyone being ready. Read
|
|
6
|
+
* from the stored row alone, so a sweep finishing an interrupted seal reaches the same answer as the
|
|
7
|
+
* call that sealed it: a deadline seal is refused until the deadline has passed, so its `sealedAt`
|
|
8
|
+
* is never earlier. A turn without a deadline (timer off, or paused by an absence vote) can only
|
|
9
|
+
* seal on readiness.
|
|
10
|
+
*/
|
|
11
|
+
export declare function sealedByDeadline(turn: Pick<Turn, 'deadlineAt' | 'sealedAt'>): boolean;
|
|
4
12
|
export type Consensus = {
|
|
5
13
|
kind: 'pending';
|
|
6
14
|
} | {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-logic.d.ts","sourceRoot":"","sources":["../../src/logic/turn-logic.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,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,
|
|
1
|
+
{"version":3,"file":"turn-logic.d.ts","sourceRoot":"","sources":["../../src/logic/turn-logic.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,IAAI,EACT,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,CAMT;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,GAAG,UAAU,CAAC,GAAG,OAAO,CAGrF;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,CAQ3C;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAElF"}
|
package/dist/logic/turn-logic.js
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import { activePlayers, humanParticipants, } from '../domain/entities';
|
|
2
2
|
/** Every human seat still being waited on has marked ready. An empty roster is never "ready". */
|
|
3
3
|
export function allActiveReady(players, orders) {
|
|
4
|
-
const
|
|
4
|
+
const asked = new Set(orders.map((row) => row.playerId));
|
|
5
|
+
const active = humanParticipants(players).filter((player) => asked.has(player.id));
|
|
5
6
|
if (active.length === 0)
|
|
6
7
|
return false;
|
|
7
8
|
const readyIds = new Set(orders.filter((row) => row.ready).map((row) => row.playerId));
|
|
8
9
|
return active.every((player) => readyIds.has(player.id));
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Whether the turn was sealed by its clock running out rather than by everyone being ready. Read
|
|
13
|
+
* from the stored row alone, so a sweep finishing an interrupted seal reaches the same answer as the
|
|
14
|
+
* call that sealed it: a deadline seal is refused until the deadline has passed, so its `sealedAt`
|
|
15
|
+
* is never earlier. A turn without a deadline (timer off, or paused by an absence vote) can only
|
|
16
|
+
* seal on readiness.
|
|
17
|
+
*/
|
|
18
|
+
export function sealedByDeadline(turn) {
|
|
19
|
+
if (turn.deadlineAt === null || turn.sealedAt === null)
|
|
20
|
+
return false;
|
|
21
|
+
return turn.sealedAt.getTime() >= turn.deadlineAt.getTime();
|
|
22
|
+
}
|
|
10
23
|
/**
|
|
11
24
|
* The state hashes that the most active players reported, tied if more than one.
|
|
12
25
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-logic.js","sourceRoot":"","sources":["../../src/logic/turn-logic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,iBAAiB,
|
|
1
|
+
{"version":3,"file":"turn-logic.js","sourceRoot":"","sources":["../../src/logic/turn-logic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,iBAAiB,GAKlB,MAAM,oBAAoB,CAAA;AAE3B,iGAAiG;AACjG,MAAM,UAAU,cAAc,CAC5B,OAA0B,EAC1B,MAA6D;IAE7D,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;IACxD,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;IAClF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACrC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;IACtF,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;AAC1D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAA2C;IAC1E,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IACpE,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAA;AAC7D,CAAC;AAYD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAA0B,EAC1B,OAA8B;IAE9B,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IACnE,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC;YAAE,SAAQ;QAC1C,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACvE,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;IAC5C,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACzB,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC;SACrC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC;SAC/B,IAAI,EAAE,CAAA;AACX,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAA0B,EAC1B,OAA8B,EAC9B,iBAAgC;IAEhC,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAA;IACzC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IACnD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IAC5E,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;IACrE,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,SAAS,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IACpF,MAAM,OAAO,GAAG,aAA6B,CAAA;IAE7C,IAAI,iBAAiB,KAAK,IAAI,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,KAAK,iBAAiB,CAAC,EAAE,CAAC;YACtE,OAAO;gBACL,IAAI,EAAE,WAAW;gBACjB,SAAS,EAAE,iBAAiB;gBAC5B,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC;aACrD,CAAA;QACH,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IAC5B,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IACxB,IAAI,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7E,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC;SACrD,CAAA;IACH,CAAC;IACD,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;QAC9F,oBAAoB,EAAE,uBAAuB,CAAC,OAAO,EAAE,OAAO,CAAC;KAChE,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,OAA0B,EAC1B,YAAoB;IAEpB,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACnD,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE;YAAE,OAAO,CAAC,CAAA;QAC3B,IAAI,CAAC,CAAC,EAAE,KAAK,YAAY;YAAE,OAAO,CAAC,CAAC,CAAA;QACpC,IAAI,CAAC,CAAC,EAAE,KAAK,YAAY;YAAE,OAAO,CAAC,CAAA;QACnC,OAAO,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;IACF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;AACvE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAc,EAAE,gBAAwB;IACnE,OAAO,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,gBAAgB,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAC7F,CAAC"}
|
package/dist/ports/runtime.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/ports/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,MAAM,WAAW,KAAK;IACpB,GAAG,IAAI,IAAI,CAAA;CACZ;AAED,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACzD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAC3D;AAED,eAAO,MAAM,UAAU,EAAE,MAKxB,CAAA;AAED,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7C;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/E;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,KAAK,EAAE;QACV,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;QAChB,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,EAAE,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/ports/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,MAAM,WAAW,KAAK;IACpB,GAAG,IAAI,IAAI,CAAA;CACZ;AAED,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACzD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAC3D;AAED,eAAO,MAAM,UAAU,EAAE,MAKxB,CAAA;AAED,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7C;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/E;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,KAAK,EAAE;QACV,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;QAChB,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,EAAE,WAAW,CAAA;QACnB,mFAAmF;QACnF,KAAK,CAAC,EAAE,OAAO,CAAA;KAChB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CACtB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnE"}
|
package/dist/ports/storage.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type { Match, MatchSeat, OrderSummary, PersistedEvent, Player, PublicLobb
|
|
|
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`
|
|
6
|
-
* is such a compare-and-swap; `claimSeat`/`releaseSeat`
|
|
6
|
+
* is such a compare-and-swap; `claimSeat`/`releaseSeat` and `claimLateJoinOrder` are atomic
|
|
7
|
+
* counters; `submitOrders` is
|
|
7
8
|
* conditional on the turn still being open; `append` allocates its own sequence number.
|
|
8
9
|
*
|
|
9
10
|
* Writes that a unique constraint can refuse answer `false` instead of throwing, so the services
|
|
@@ -27,6 +28,15 @@ export interface MatchRepository {
|
|
|
27
28
|
* position in the match's monotonic join sequence, or null when no seat was available.
|
|
28
29
|
*/
|
|
29
30
|
claimSeat(matchId: string): Promise<number | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Atomically take the next position in the match's join sequence for a late joiner, while the
|
|
33
|
+
* match is running; null when it is not running or no longer exists. It advances the same
|
|
34
|
+
* `joinCounter` `claimSeat` does, in one conditional UPDATE — a separate sequence, or a read
|
|
35
|
+
* followed by a write, would hand two joiners the same `joinOrder` — and it never returns a
|
|
36
|
+
* position a stored player of the match already holds. It does not test capacity: `createLate`
|
|
37
|
+
* does, and a position taken for a join that `createLate` then refuses stays unused.
|
|
38
|
+
*/
|
|
39
|
+
claimLateJoinOrder(matchId: string): Promise<number | null>;
|
|
30
40
|
releaseSeat(matchId: string): Promise<void>;
|
|
31
41
|
/** Host-only lobby configuration; false after the match starts or below the occupied seat count. */
|
|
32
42
|
updateSettings(matchId: string, settings: MatchSettings, updatedAt: Date): Promise<boolean>;
|
|
@@ -52,8 +62,8 @@ export interface MatchRepository {
|
|
|
52
62
|
* being `active` only through `leave`, `kick` or a missed deadline, and the game's default is an
|
|
53
63
|
* untimed match, so two friends whose clients both died mid-match leave two `active` rows that
|
|
54
64
|
* nothing ever clears: with the roster test alone their match is immortal. The longer window drops
|
|
55
|
-
* the test, because `updated_at` is refreshed by every turn open
|
|
56
|
-
* months of silence on
|
|
65
|
+
* the test, because `updated_at` is refreshed by every turn open, every status change and every
|
|
66
|
+
* join or rejoin, and months of silence on all of them is not something a live match does.
|
|
57
67
|
*/
|
|
58
68
|
deleteAbandonedLive(before: Date, limit: number, requireEmptyRoster: boolean): Promise<number>;
|
|
59
69
|
/**
|
|
@@ -66,7 +76,7 @@ export interface MatchRepository {
|
|
|
66
76
|
* on every report and every snapshot upload, so a match that nothing touched since the previous
|
|
67
77
|
* pass has no new evidence and its verdict cannot have changed; without the bound a public
|
|
68
78
|
* server's parked desyncs — the documented way a match ends when a host never uploads — were
|
|
69
|
-
* re-judged every fifteen seconds for the
|
|
79
|
+
* re-judged every fifteen seconds for the weeks retention keeps them. It also ends the
|
|
70
80
|
* starvation of a page ordered by an `updatedAt` that never moves: pass null to sweep everything,
|
|
71
81
|
* which is what a freshly started process does once to pick up whatever it missed.
|
|
72
82
|
*/
|
|
@@ -79,6 +89,8 @@ export interface MatchRepository {
|
|
|
79
89
|
hostPlayerId?: string;
|
|
80
90
|
updatedAt: Date;
|
|
81
91
|
}): Promise<boolean>;
|
|
92
|
+
/** Advance the current turn in one conditional write; a late sweep may never move it backwards. */
|
|
93
|
+
advanceCurrentTurn(matchId: string, number: number, updatedAt: Date): Promise<boolean>;
|
|
82
94
|
}
|
|
83
95
|
export interface PlayerRepository {
|
|
84
96
|
/**
|
|
@@ -132,8 +144,10 @@ export interface PlayerRepository {
|
|
|
132
144
|
}
|
|
133
145
|
export interface TurnRepository {
|
|
134
146
|
/**
|
|
135
|
-
* Insert the turn and one empty orders row per player
|
|
136
|
-
*
|
|
147
|
+
* Insert the turn and one empty orders row per player that has none yet. Rows are only added while
|
|
148
|
+
* the stored turn is still `open`: a top-up that races a seal must not land in the sealed turn.
|
|
149
|
+
* False when the turn already exists, which is how the seal repair can re-run the open step
|
|
150
|
+
* without publishing a second `turn.opened`.
|
|
137
151
|
*/
|
|
138
152
|
open(turn: Turn, playerIds: readonly string[]): Promise<boolean>;
|
|
139
153
|
get(matchId: string, number: number): Promise<Turn | null>;
|
|
@@ -227,7 +241,7 @@ export interface TurnRepository {
|
|
|
227
241
|
* which is why a missing turn counts as stalled rather than being skipped.
|
|
228
242
|
*
|
|
229
243
|
* `touchedSince` bounds the join to matches something happened to recently. A seal in flight is
|
|
230
|
-
* seconds old, but abandoned matches are deliberately kept `running` for
|
|
244
|
+
* seconds old, but abandoned matches are deliberately kept `running` for weeks, so the
|
|
231
245
|
* unbounded join walked thousands of rows every fifteen seconds to find nothing. The caller runs
|
|
232
246
|
* the bounded scan every tick and the unbounded one (null) on a much longer period, which is what
|
|
233
247
|
* still finds a seal interrupted while the process was down.
|
|
@@ -285,7 +299,10 @@ export interface CastVoteInput {
|
|
|
285
299
|
* are one indexed read instead of a scan of every event the match ever logged.
|
|
286
300
|
*/
|
|
287
301
|
export interface TakeoverRepository {
|
|
288
|
-
/**
|
|
302
|
+
/**
|
|
303
|
+
* Opens a prompt only while the seat is a human absence (`ABSENT_HUMAN_STATUSES`) of that match;
|
|
304
|
+
* false when one is already open or the seat is active, computer controlled or not in the match.
|
|
305
|
+
*/
|
|
289
306
|
openPrompt(matchId: string, playerId: string, turn: number, openedAt: Date): Promise<boolean>;
|
|
290
307
|
/** Closes the prompt and discards its votes. A no-op when none is open. */
|
|
291
308
|
closePrompt(matchId: string, playerId: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/ports/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACb,WAAW,EACX,UAAU,EACX,MAAM,4BAA4B,CAAA;AACnC,OAAO,KAAK,EACV,KAAK,EACL,SAAS,EACT,YAAY,EACZ,cAAc,EACd,MAAM,EACN,cAAc,EACd,UAAU,EACV,QAAQ,EACR,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,IAAI,EACJ,UAAU,EACV,UAAU,EACX,MAAM,oBAAoB,CAAA;AAE3B
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/ports/storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -37,8 +37,12 @@ export declare class LobbyService {
|
|
|
37
37
|
createMatch(request: CreateMatchRequest): Promise<MembershipView>;
|
|
38
38
|
join(request: JoinMatchRequest, caller?: string): Promise<MembershipView>;
|
|
39
39
|
joinRunning(request: JoinRunningMatchRequest, caller?: string): Promise<MembershipView>;
|
|
40
|
+
/** Why the late-join claim found no running match: it stopped running, or it is gone. */
|
|
41
|
+
private lateJoinClaimRefused;
|
|
40
42
|
leave(principal: Principal): Promise<void>;
|
|
41
43
|
rejoin(principal: Principal): Promise<void>;
|
|
44
|
+
/** A seat claim can cross a seal, so use the turn current after the seat was committed. */
|
|
45
|
+
private topUpCurrentTurn;
|
|
42
46
|
updateSettings(principal: Principal, settings: MatchSettings): Promise<void>;
|
|
43
47
|
kick(principal: Principal, targetPlayerId: string): Promise<void>;
|
|
44
48
|
/** The player a host or a voter named, refused unless it is a member of the caller's match. */
|
|
@@ -61,12 +65,26 @@ export declare class LobbyService {
|
|
|
61
65
|
private retallyOpenPrompts;
|
|
62
66
|
start(principal: Principal): Promise<void>;
|
|
63
67
|
private remove;
|
|
68
|
+
/**
|
|
69
|
+
* Restart a running or desynced match's retention age: somebody has just come (back) to it.
|
|
70
|
+
*
|
|
71
|
+
* The abandoned and silent windows both run from `updatedAt`, and neither a join nor a rejoin
|
|
72
|
+
* otherwise moves it — a player returning to an untimed match whose partner is still away opens
|
|
73
|
+
* no turn and changes no status — so a match somebody was demonstrably playing could be collected
|
|
74
|
+
* on the age it had before they came back. A no-op on any other status.
|
|
75
|
+
*/
|
|
76
|
+
private refreshRetention;
|
|
64
77
|
/**
|
|
65
78
|
* Move the host role of a started match and announce it. `at` is the caller's to supply: `remove`
|
|
66
79
|
* stamps the handoff with the time it read on entry, and the other callers read the clock as
|
|
67
80
|
* they call.
|
|
68
81
|
*/
|
|
69
82
|
private handHostTo;
|
|
83
|
+
/**
|
|
84
|
+
* Hand the host role of a started match to its first active player, if it has one. Answers
|
|
85
|
+
* whether anybody was there to take it.
|
|
86
|
+
*/
|
|
87
|
+
private handHostToFirstActive;
|
|
70
88
|
/**
|
|
71
89
|
* Hang up a revoked membership's streams. Best effort by design: the token is already gone, so a
|
|
72
90
|
* fan-out that cannot be reached costs one stale stream until it drops, never the revoke itself.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LobbyService.d.ts","sourceRoot":"","sources":["../../src/services/LobbyService.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EAGvB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAE5B,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"LobbyService.d.ts","sourceRoot":"","sources":["../../src/services/LobbyService.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EAGvB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAE5B,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAA;AAoBnC,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;AAItD,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,eAAe,CAAA;AAwC5D,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,MAAM,CAAA;CACrB;AAED,0EAA0E;AAC1E,qBAAa,YAAY;IAOrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IARxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAa;IAC9C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAa;IAE9C,YACmB,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,cAAc,EACzB,KAAK,EAAE,WAAW,EACnC,OAAO,GAAE,mBAAwB,EAYlC;IAED;;;;;;;;;;;;;;OAcG;YACW,mBAAmB;IAsBjC,OAAO,CAAC,uBAAuB;IAOzB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CA4CtE;IAEK,IAAI,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAkD9E;IAEK,WAAW,CAAC,OAAO,EAAE,uBAAuB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CA4E5F;IAED,yFAAyF;YAC3E,oBAAoB;IAM5B,KAAK,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;IAEK,MAAM,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAiDhD;IAED,2FAA2F;YAC7E,gBAAgB;IAKxB,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBjF;IAEK,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtE;IAED,+FAA+F;YACjF,aAAa;IAQ3B;;;OAGG;IACG,cAAc,CAClB,SAAS,EAAE,SAAS,EACpB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAqCf;IAED;;;;;;;OAOG;YACW,iBAAiB;IAgC/B,8EAA8E;YAChE,kBAAkB;IAM1B,KAAK,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC/C;YAEa,MAAM;IA+HpB;;;;;;;OAOG;YACW,gBAAgB;IAM9B;;;;OAIG;YACW,UAAU;IAWxB;;;OAGG;YACW,qBAAqB;IAOnC;;;OAGG;YACW,MAAM;IAYpB;;;;;;;;;;;;OAYG;YACW,mBAAmB;IAIjC,OAAO,CAAC,gBAAgB;IASxB;;;;OAIG;YACW,YAAY;YAaZ,OAAO;IAcrB;;;;OAIG;YACW,uBAAuB;YAUvB,UAAU;CAQzB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { foldName, GAME_BOUNDS, LIMITS, } from '@chaos-overlords/contracts';
|
|
2
|
-
import { activePlayers, humanParticipants, isInProgress, } from '../domain/entities';
|
|
2
|
+
import { ABSENT_HUMAN_STATUSES, activePlayers, humanParticipants, isInProgress, } from '../domain/entities';
|
|
3
3
|
import { ConflictError, ForbiddenError, NotFoundError, RateLimitedError, UnauthorizedError, } from '../domain/errors';
|
|
4
4
|
import { generateJoinCode, generateSeed, generateToken, hashToken } from '../logic/crypto';
|
|
5
5
|
import { hashPassword, verifyPassword } from '../logic/password';
|
|
@@ -18,8 +18,6 @@ const HOST_JOIN_ORDER = 0;
|
|
|
18
18
|
const DEFAULT_PORTRAIT_ID = 0;
|
|
19
19
|
/** Host statuses that mean the seat is genuinely empty and the role may move. */
|
|
20
20
|
const VACANT_HOST_STATUSES = ['left', 'kicked', 'computer'];
|
|
21
|
-
/** A human seat nobody is playing: the ones an absence vote can hand to the computer. */
|
|
22
|
-
const ABSENT_HUMAN_STATUSES = ['takeoverPending', 'left', 'kicked'];
|
|
23
21
|
/**
|
|
24
22
|
* Passwords one caller will have verified against one match in a window.
|
|
25
23
|
*
|
|
@@ -254,11 +252,17 @@ export class LobbyService {
|
|
|
254
252
|
this.assertNameIsFree(existing, request.displayName);
|
|
255
253
|
const token = generateToken();
|
|
256
254
|
const seatKey = (await hashToken(`${match.id}:${request.slot}`)).slice(0, 32);
|
|
255
|
+
// The position is taken before the insert because the port has no transactions to take both
|
|
256
|
+
// in one; a join `createLate` then refuses leaves a gap in the sequence, which only has to be
|
|
257
|
+
// unique and increasing. The checks above refuse the requests that are doomed from the start.
|
|
258
|
+
const joinOrder = await this.deps.storage.matches.claimLateJoinOrder(match.id);
|
|
259
|
+
if (joinOrder === null)
|
|
260
|
+
throw await this.lateJoinClaimRefused(match.id);
|
|
257
261
|
const player = {
|
|
258
262
|
id: `late-${seatKey}`,
|
|
259
263
|
matchId: match.id,
|
|
260
264
|
slot: request.slot,
|
|
261
|
-
joinOrder
|
|
265
|
+
joinOrder,
|
|
262
266
|
displayName: request.displayName,
|
|
263
267
|
portraitId: request.portraitId ?? DEFAULT_PORTRAIT_ID,
|
|
264
268
|
tokenHash: await hashToken(token),
|
|
@@ -270,15 +274,20 @@ export class LobbyService {
|
|
|
270
274
|
reason: 'seat_reserved',
|
|
271
275
|
});
|
|
272
276
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
await this.deps.storage.turns.open(openTurn, [player.id]);
|
|
277
|
+
await this.refreshRetention(match.id);
|
|
278
|
+
await this.topUpCurrentTurn(match.id, player.id);
|
|
276
279
|
await this.publisher.publish(match.id, {
|
|
277
280
|
type: 'match.latePlayerJoined',
|
|
278
281
|
payload: { playerId: player.id, slot: player.slot },
|
|
279
282
|
});
|
|
280
283
|
return this.membership(match, player, token);
|
|
281
284
|
}
|
|
285
|
+
/** Why the late-join claim found no running match: it stopped running, or it is gone. */
|
|
286
|
+
async lateJoinClaimRefused(matchId) {
|
|
287
|
+
return (await this.deps.storage.matches.get(matchId))
|
|
288
|
+
? new ConflictError('The match is not running', { reason: 'match_not_running' })
|
|
289
|
+
: new NotFoundError('No match with that id or join code', { reason: 'unknown_match' });
|
|
290
|
+
}
|
|
282
291
|
async leave(principal) {
|
|
283
292
|
await this.remove(principal.match, principal.player, 'left');
|
|
284
293
|
}
|
|
@@ -288,17 +297,18 @@ export class LobbyService {
|
|
|
288
297
|
if (player.status === 'kicked') {
|
|
289
298
|
throw new ForbiddenError('A kicked player cannot rejoin', { reason: 'kicked' });
|
|
290
299
|
}
|
|
291
|
-
|
|
300
|
+
await this.refreshRetention(match.id);
|
|
301
|
+
if (player.status === 'active') {
|
|
302
|
+
// Nothing to reclaim, but a seat whose row an interrupted turn open never wrote is repaired
|
|
303
|
+
// here too, so the returning client is waited for instead of silently sealed past.
|
|
304
|
+
await this.topUpCurrentTurn(match.id, player.id);
|
|
292
305
|
return;
|
|
306
|
+
}
|
|
293
307
|
const replacedComputer = player.status === 'computer';
|
|
294
308
|
if (!(await this.deps.storage.players.transitionStatus(player.id, ['left', 'takeoverPending', 'computer'], 'active'))) {
|
|
295
309
|
throw new ConflictError('The player seat could not be reclaimed', { reason: 'rejoin_race' });
|
|
296
310
|
}
|
|
297
|
-
|
|
298
|
-
if (openTurn?.status === 'open') {
|
|
299
|
-
// `open` is idempotent and tops up missing participant rows even when the turn already exists.
|
|
300
|
-
await this.deps.storage.turns.open(openTurn, [player.id]);
|
|
301
|
-
}
|
|
311
|
+
await this.topUpCurrentTurn(match.id, player.id);
|
|
302
312
|
await this.deps.storage.takeovers.closePrompt(match.id, player.id);
|
|
303
313
|
await this.publisher.publish(match.id, {
|
|
304
314
|
type: 'match.playerReturned',
|
|
@@ -318,14 +328,20 @@ export class LobbyService {
|
|
|
318
328
|
// Only a host who is gone is replaced. `takeoverPending` is set on a player who is still
|
|
319
329
|
// connected and merely missed one timed deadline, so treating it as absence would hand the role
|
|
320
330
|
// to any former member who called `rejoin` at that moment — and the new host can kick the old
|
|
321
|
-
// one, which revokes their token for good. A pending host is still present; the takeover vote
|
|
322
|
-
// the path that decides otherwise.
|
|
331
|
+
// one, which revokes their token for good. A pending host is still present; the takeover vote,
|
|
332
|
+
// or the pending host's own `leave`, is the path that decides otherwise.
|
|
323
333
|
if (currentHost === undefined || VACANT_HOST_STATUSES.includes(currentHost.status)) {
|
|
324
334
|
await this.handHostTo(match.id, player.id, this.deps.clock.now());
|
|
325
335
|
}
|
|
326
336
|
await this.turns.reevaluate(match.id);
|
|
327
337
|
await this.turns.resumeAfterTakeoverVotes(match.id);
|
|
328
338
|
}
|
|
339
|
+
/** A seat claim can cross a seal, so use the turn current after the seat was committed. */
|
|
340
|
+
async topUpCurrentTurn(matchId, playerId) {
|
|
341
|
+
const match = await this.deps.storage.matches.get(matchId);
|
|
342
|
+
if (match)
|
|
343
|
+
await this.turns.topUpSeat(matchId, match.currentTurn, playerId);
|
|
344
|
+
}
|
|
329
345
|
async updateSettings(principal, settings) {
|
|
330
346
|
requireHost(principal);
|
|
331
347
|
if (principal.match.status !== 'lobby') {
|
|
@@ -427,9 +443,7 @@ export class LobbyService {
|
|
|
427
443
|
payload: { playerId: targetPlayerId },
|
|
428
444
|
});
|
|
429
445
|
if (targetPlayerId === match.hostPlayerId) {
|
|
430
|
-
|
|
431
|
-
if (successor)
|
|
432
|
-
await this.handHostTo(match.id, successor.id, this.deps.clock.now());
|
|
446
|
+
await this.handHostToFirstActive(match.id, this.deps.clock.now());
|
|
433
447
|
}
|
|
434
448
|
await this.turns.reevaluate(match.id);
|
|
435
449
|
await this.turns.resumeAfterTakeoverVotes(match.id);
|
|
@@ -521,9 +535,9 @@ export class LobbyService {
|
|
|
521
535
|
if (!(await this.deps.storage.players.delete(target.id)))
|
|
522
536
|
return;
|
|
523
537
|
await this.deps.storage.matches.releaseSeat(match.id);
|
|
524
|
-
// A kicked member's token is dead with the row, but a stream they already hold
|
|
525
|
-
//
|
|
526
|
-
// every seal, desync and roster change
|
|
538
|
+
// A kicked member's token is dead with the row, but a stream they already hold only re-checks
|
|
539
|
+
// its membership on the hub's catch-up heartbeat: without this it goes on delivering
|
|
540
|
+
// `match.started` with the seed and every seal, desync and roster change until then.
|
|
527
541
|
if (reason === 'kicked')
|
|
528
542
|
await this.hangUp(match.id, target.id);
|
|
529
543
|
await this.publisher.publish(match.id, {
|
|
@@ -543,8 +557,8 @@ export class LobbyService {
|
|
|
543
557
|
// controlled, but the person behind it must still lose the token and the streams either way.
|
|
544
558
|
// Membership is the only thing the token ever proved, so it stops working here: a kicked player
|
|
545
559
|
// keeps neither the event stream nor the sealed order sets of the turns that follow. The revoke
|
|
546
|
-
// closes the next request and the hang-up closes the streams already open, which
|
|
547
|
-
//
|
|
560
|
+
// closes the next request and the hang-up closes the streams already open, which otherwise
|
|
561
|
+
// outlive the membership until the hub's periodic membership check catches up with them.
|
|
548
562
|
if (reason === 'kicked') {
|
|
549
563
|
await this.deps.storage.players.revokeToken(target.id);
|
|
550
564
|
await this.hangUp(match.id, target.id);
|
|
@@ -566,7 +580,17 @@ export class LobbyService {
|
|
|
566
580
|
// the turn waiting on a seat that could never answer, with the clock paused by its own prompt
|
|
567
581
|
// and everyone present already ready. A `left` seat really is idle, and re-running the verdict
|
|
568
582
|
// for it costs one query.
|
|
583
|
+
//
|
|
584
|
+
// A `takeoverPending` host who leaves is gone, not merely late, so the role moves on the way it
|
|
585
|
+
// does for an active host; otherwise it stayed with a departed seat until a takeover vote or a
|
|
586
|
+
// rejoin happened to move it. With nobody present to take it, the role stays put and the first
|
|
587
|
+
// former member to rejoin becomes host, as below.
|
|
569
588
|
if (!claimed || !wasActive) {
|
|
589
|
+
if (claimed &&
|
|
590
|
+
target.id === match.hostPlayerId &&
|
|
591
|
+
!(await this.handHostToFirstActive(match.id, now))) {
|
|
592
|
+
await this.turns.pauseAbandonedMatch(match.id);
|
|
593
|
+
}
|
|
570
594
|
await this.turns.reevaluate(match.id);
|
|
571
595
|
await this.retallyOpenPrompts(match);
|
|
572
596
|
return;
|
|
@@ -592,6 +616,19 @@ export class LobbyService {
|
|
|
592
616
|
await this.turns.reevaluate(match.id);
|
|
593
617
|
await this.retallyOpenPrompts(match);
|
|
594
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* Restart a running or desynced match's retention age: somebody has just come (back) to it.
|
|
621
|
+
*
|
|
622
|
+
* The abandoned and silent windows both run from `updatedAt`, and neither a join nor a rejoin
|
|
623
|
+
* otherwise moves it — a player returning to an untimed match whose partner is still away opens
|
|
624
|
+
* no turn and changes no status — so a match somebody was demonstrably playing could be collected
|
|
625
|
+
* on the age it had before they came back. A no-op on any other status.
|
|
626
|
+
*/
|
|
627
|
+
async refreshRetention(matchId) {
|
|
628
|
+
await this.deps.storage.matches.transition(matchId, ['running', 'desynced'], {
|
|
629
|
+
updatedAt: this.deps.clock.now(),
|
|
630
|
+
});
|
|
631
|
+
}
|
|
595
632
|
/**
|
|
596
633
|
* Move the host role of a started match and announce it. `at` is the caller's to supply: `remove`
|
|
597
634
|
* stamps the handoff with the time it read on entry, and the other callers read the clock as
|
|
@@ -607,6 +644,17 @@ export class LobbyService {
|
|
|
607
644
|
payload: { hostPlayerId: playerId },
|
|
608
645
|
});
|
|
609
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Hand the host role of a started match to its first active player, if it has one. Answers
|
|
649
|
+
* whether anybody was there to take it.
|
|
650
|
+
*/
|
|
651
|
+
async handHostToFirstActive(matchId, at) {
|
|
652
|
+
const successor = activePlayers(await this.deps.storage.players.listByMatch(matchId))[0];
|
|
653
|
+
if (!successor)
|
|
654
|
+
return false;
|
|
655
|
+
await this.handHostTo(matchId, successor.id, at);
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
610
658
|
/**
|
|
611
659
|
* Hang up a revoked membership's streams. Best effort by design: the token is already gone, so a
|
|
612
660
|
* fan-out that cannot be reached costs one stale stream until it drops, never the revoke itself.
|