@chaos-overlords/kernel 0.9.8 → 0.9.10
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/logic/turn-logic.d.ts +10 -0
- package/dist/logic/turn-logic.d.ts.map +1 -1
- package/dist/logic/turn-logic.js +20 -0
- package/dist/logic/turn-logic.js.map +1 -1
- package/dist/ports/index.d.ts +1 -0
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +1 -0
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/rateLimits.d.ts +52 -0
- package/dist/ports/rateLimits.d.ts.map +1 -0
- package/dist/ports/rateLimits.js +2 -0
- package/dist/ports/rateLimits.js.map +1 -0
- package/dist/services/LobbyService.d.ts +15 -1
- package/dist/services/LobbyService.d.ts.map +1 -1
- package/dist/services/LobbyService.js +48 -7
- package/dist/services/LobbyService.js.map +1 -1
- package/dist/services/RateLimiter.d.ts +97 -21
- package/dist/services/RateLimiter.d.ts.map +1 -1
- package/dist/services/RateLimiter.js +161 -17
- package/dist/services/RateLimiter.js.map +1 -1
- package/dist/services/SnapshotService.d.ts +6 -3
- package/dist/services/SnapshotService.d.ts.map +1 -1
- package/dist/services/SnapshotService.js +12 -7
- package/dist/services/SnapshotService.js.map +1 -1
- package/dist/services/TurnService.d.ts +27 -2
- package/dist/services/TurnService.d.ts.map +1 -1
- package/dist/services/TurnService.js +70 -13
- package/dist/services/TurnService.js.map +1 -1
- package/dist/services/deps.d.ts +7 -0
- package/dist/services/deps.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -10,12 +10,164 @@
|
|
|
10
10
|
* key's remaining budget, which is the safe direction to fail on a memory bound.
|
|
11
11
|
*/
|
|
12
12
|
const DEFAULT_MAX_KEYS = 50_000;
|
|
13
|
+
/** Limiters that count in this process: every call a single process takes, and nothing more. */
|
|
14
|
+
export function memoryRateLimiters(clock) {
|
|
15
|
+
return (_name, options) => new MemoryRateLimiter(clock, options);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Limiters that count in a {@link RateLimitStore} every instance of the deployment shares.
|
|
19
|
+
*
|
|
20
|
+
* A store that fails (the database is unreachable, a Durable Object is overloaded) lets the call
|
|
21
|
+
* through and logs, at most once a minute. The limiters are abuse controls in front of a game, and
|
|
22
|
+
* refusing every player because the counter is down would be the outage the abuse was going to
|
|
23
|
+
* cause; when the failure is shared with the match storage, the request fails there on its own.
|
|
24
|
+
*/
|
|
25
|
+
export function sharedRateLimiters(store, clock, logger) {
|
|
26
|
+
const report = throttledWarning(clock, logger);
|
|
27
|
+
return (name, options) => new SharedRateLimiter(store, clock, {
|
|
28
|
+
name,
|
|
29
|
+
policy: { limit: options.limit, windowMs: options.windowMs },
|
|
30
|
+
report,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* What a fixed window answers a call at `now`: the window to keep, and whether the call was
|
|
35
|
+
* counted. A window that has rolled is replaced by one opening at `now`. Every store applies this,
|
|
36
|
+
* so the budgets mean the same thing in a Postgres row, a Durable Object and a test.
|
|
37
|
+
*/
|
|
38
|
+
export function consumeWindow(current, policy, now) {
|
|
39
|
+
if (current === undefined || current.resetAt <= now) {
|
|
40
|
+
return { allowed: true, count: 1, windowStart: now, resetAt: now + policy.windowMs };
|
|
41
|
+
}
|
|
42
|
+
const allowed = current.count < policy.limit;
|
|
43
|
+
return {
|
|
44
|
+
allowed,
|
|
45
|
+
count: allowed ? current.count + 1 : current.count,
|
|
46
|
+
windowStart: current.windowStart,
|
|
47
|
+
resetAt: current.resetAt,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Whole seconds until a window that rolls at `resetAt` has rolled; at least one. */
|
|
51
|
+
function secondsUntil(resetAt, now) {
|
|
52
|
+
return Math.max(1, Math.ceil((resetAt - now) / 1000));
|
|
53
|
+
}
|
|
13
54
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
55
|
+
* A limiter whose windows live in a {@link RateLimitStore}.
|
|
56
|
+
*
|
|
57
|
+
* It keeps the in-process limiter's contract on top of the store's three operations, so a budget
|
|
58
|
+
* answers the same on every runtime. It measures on the wall clock: the windows are compared across
|
|
59
|
+
* instances, and the wall clock is the only one they have in common.
|
|
17
60
|
*/
|
|
18
|
-
export class
|
|
61
|
+
export class SharedRateLimiter {
|
|
62
|
+
store;
|
|
63
|
+
clock;
|
|
64
|
+
name;
|
|
65
|
+
policy;
|
|
66
|
+
report;
|
|
67
|
+
constructor(store, clock, options) {
|
|
68
|
+
this.store = store;
|
|
69
|
+
this.clock = clock;
|
|
70
|
+
this.name = options.name;
|
|
71
|
+
this.policy = options.policy;
|
|
72
|
+
this.report = options.report;
|
|
73
|
+
}
|
|
74
|
+
async take(key) {
|
|
75
|
+
const now = this.clock.now().getTime();
|
|
76
|
+
const window = await this.guard('consume', () => this.store.consume(this.key(key), this.policy, now));
|
|
77
|
+
if (window === undefined || window.allowed)
|
|
78
|
+
return null;
|
|
79
|
+
return secondsUntil(window.resetAt, now);
|
|
80
|
+
}
|
|
81
|
+
async reserve(key) {
|
|
82
|
+
const stored = this.key(key);
|
|
83
|
+
const now = this.clock.now().getTime();
|
|
84
|
+
const window = await this.guard('consume', () => this.store.consume(stored, this.policy, now));
|
|
85
|
+
// The store failed and the call goes through; there is nothing to give back.
|
|
86
|
+
if (window === undefined)
|
|
87
|
+
return async () => { };
|
|
88
|
+
if (!window.allowed)
|
|
89
|
+
return null;
|
|
90
|
+
let released = false;
|
|
91
|
+
return async () => {
|
|
92
|
+
if (released)
|
|
93
|
+
return;
|
|
94
|
+
released = true;
|
|
95
|
+
await this.guard('refund', () => this.store.refund(stored, window.windowStart));
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async peek(key) {
|
|
99
|
+
const now = this.clock.now().getTime();
|
|
100
|
+
const live = await this.guard('inspect', () => this.store.inspect(this.key(key), now));
|
|
101
|
+
if (!live || live.count < this.policy.limit)
|
|
102
|
+
return null;
|
|
103
|
+
return secondsUntil(live.resetAt, now);
|
|
104
|
+
}
|
|
105
|
+
async spent(key) {
|
|
106
|
+
const now = this.clock.now().getTime();
|
|
107
|
+
const live = await this.guard('inspect', () => this.store.inspect(this.key(key), now));
|
|
108
|
+
return Math.min(live?.count ?? 0, this.policy.limit);
|
|
109
|
+
}
|
|
110
|
+
key(key) {
|
|
111
|
+
return `${this.name}|${key}`;
|
|
112
|
+
}
|
|
113
|
+
async guard(operation, run) {
|
|
114
|
+
try {
|
|
115
|
+
return await run();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
this.report(operation, error);
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** At most one warning a minute about a failing store, however many calls meet the failure. */
|
|
124
|
+
function throttledWarning(clock, logger) {
|
|
125
|
+
let lastWarned = Number.NEGATIVE_INFINITY;
|
|
126
|
+
return (operation, error) => {
|
|
127
|
+
const now = clock.now().getTime();
|
|
128
|
+
if (now - lastWarned < 60_000)
|
|
129
|
+
return;
|
|
130
|
+
lastWarned = now;
|
|
131
|
+
logger.warn('rate limit store failed; letting the call through', {
|
|
132
|
+
operation,
|
|
133
|
+
error: String(error),
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A {@link RateLimitStore} in this process: the reference the shared stores are tested against,
|
|
139
|
+
* and the store for a test that needs one without a database. It never forgets a key, so it is no
|
|
140
|
+
* substitute for {@link MemoryRateLimiter} in a server.
|
|
141
|
+
*/
|
|
142
|
+
export class MemoryRateLimitStore {
|
|
143
|
+
windows = new Map();
|
|
144
|
+
async consume(key, policy, now) {
|
|
145
|
+
const window = consumeWindow(this.windows.get(key), policy, now);
|
|
146
|
+
this.windows.set(key, {
|
|
147
|
+
windowStart: window.windowStart,
|
|
148
|
+
resetAt: window.resetAt,
|
|
149
|
+
count: window.count,
|
|
150
|
+
});
|
|
151
|
+
return window;
|
|
152
|
+
}
|
|
153
|
+
async inspect(key, now) {
|
|
154
|
+
const window = this.windows.get(key);
|
|
155
|
+
if (!window || window.resetAt <= now)
|
|
156
|
+
return null;
|
|
157
|
+
return { count: window.count, resetAt: window.resetAt };
|
|
158
|
+
}
|
|
159
|
+
async refund(key, windowStart) {
|
|
160
|
+
const window = this.windows.get(key);
|
|
161
|
+
if (window && window.windowStart === windowStart && window.count > 0)
|
|
162
|
+
window.count -= 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Fixed-window counter per key, in memory. It counts what one process sees, which is every call a
|
|
167
|
+
* single self-hosted server takes; a deployment of more than one instance uses
|
|
168
|
+
* {@link sharedRateLimiters} instead.
|
|
169
|
+
*/
|
|
170
|
+
export class MemoryRateLimiter {
|
|
19
171
|
clock;
|
|
20
172
|
options;
|
|
21
173
|
windows = new Map();
|
|
@@ -28,8 +180,7 @@ export class RateLimiter {
|
|
|
28
180
|
this.options = options;
|
|
29
181
|
this.maxKeys = options.maxKeys ?? DEFAULT_MAX_KEYS;
|
|
30
182
|
}
|
|
31
|
-
|
|
32
|
-
take(key) {
|
|
183
|
+
async take(key) {
|
|
33
184
|
const now = this.tick();
|
|
34
185
|
const entry = this.open(key, now);
|
|
35
186
|
if (entry.count >= this.options.limit)
|
|
@@ -37,18 +188,13 @@ export class RateLimiter {
|
|
|
37
188
|
entry.count += 1;
|
|
38
189
|
return null;
|
|
39
190
|
}
|
|
40
|
-
|
|
41
|
-
* Reserves one unit for work that may be rejected after validation. The returned release function
|
|
42
|
-
* refunds exactly this reservation, once, while its window is still live. A rolled or evicted
|
|
43
|
-
* window is never decremented on behalf of an older request.
|
|
44
|
-
*/
|
|
45
|
-
reserve(key) {
|
|
191
|
+
async reserve(key) {
|
|
46
192
|
const reserved = this.open(key, this.tick());
|
|
47
193
|
if (reserved.count >= this.options.limit)
|
|
48
194
|
return null;
|
|
49
195
|
reserved.count += 1;
|
|
50
196
|
let released = false;
|
|
51
|
-
return () => {
|
|
197
|
+
return async () => {
|
|
52
198
|
if (released)
|
|
53
199
|
return;
|
|
54
200
|
released = true;
|
|
@@ -56,16 +202,14 @@ export class RateLimiter {
|
|
|
56
202
|
reserved.count -= 1;
|
|
57
203
|
};
|
|
58
204
|
}
|
|
59
|
-
|
|
60
|
-
peek(key) {
|
|
205
|
+
async peek(key) {
|
|
61
206
|
const now = this.tick();
|
|
62
207
|
const entry = this.live(key, now);
|
|
63
208
|
if (!entry || entry.count < this.options.limit)
|
|
64
209
|
return null;
|
|
65
210
|
return this.retryAfter(entry, now);
|
|
66
211
|
}
|
|
67
|
-
|
|
68
|
-
spent(key) {
|
|
212
|
+
async spent(key) {
|
|
69
213
|
return this.live(key, this.tick())?.count ?? 0;
|
|
70
214
|
}
|
|
71
215
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RateLimiter.js","sourceRoot":"","sources":["../../src/services/RateLimiter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RateLimiter.js","sourceRoot":"","sources":["../../src/services/RateLimiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -74,9 +74,12 @@ export declare class SnapshotService {
|
|
|
74
74
|
* the one case a host-only rule could not serve at all: a host that is itself the odd one out can
|
|
75
75
|
* never name a majority hash, so no repair existed and the match stayed paused until retention
|
|
76
76
|
* collected it. A TIE — most of all the 1-1 split of a two-player match — is different: there is
|
|
77
|
-
* no majority, nothing to count, and no third party to ask, so
|
|
78
|
-
*
|
|
79
|
-
* whoever uploaded first.
|
|
77
|
+
* no majority, nothing to count, and no third party to ask, so exactly one player breaks it and
|
|
78
|
+
* nobody else may. Letting either side of a tie impose its state would hand a two-player match to
|
|
79
|
+
* whoever uploaded first. That player is {@link tieBreaker}'s: the host when the host holds one
|
|
80
|
+
* of the tied hashes, which is every tie of four players or fewer, and otherwise the
|
|
81
|
+
* lowest-numbered seat that does. It is judged on the reports and the roster as they are now, the
|
|
82
|
+
* same inputs the announcement that named the player was computed from.
|
|
80
83
|
*/
|
|
81
84
|
private requireRepairAuthority;
|
|
82
85
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,YAAY,EAEjB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC3B,MAAM,4BAA4B,CAAA;AAKnC,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,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAQhD;;;;GAIG;AACH,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IAHxB,YACmB,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,cAAc,EACzB,KAAK,EAAE,WAAW,EACjC;IAEE,MAAM,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6ChF;IAED;;;;;;;;;;;;;;;;OAgBG;YACW,eAAe;IAkB7B;;;;;;;;;OASG;YACW,KAAK;IAYnB,OAAO,CAAC,UAAU;IAkBlB;;;OAGG;YACW,kBAAkB;IAehC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,mBAAmB;IAO3B
|
|
1
|
+
{"version":3,"file":"SnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,YAAY,EAEjB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC3B,MAAM,4BAA4B,CAAA;AAKnC,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,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAQhD;;;;GAIG;AACH,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IAHxB,YACmB,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,cAAc,EACzB,KAAK,EAAE,WAAW,EACjC;IAEE,MAAM,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6ChF;IAED;;;;;;;;;;;;;;;;OAgBG;YACW,eAAe;IAkB7B;;;;;;;;;OASG;YACW,KAAK;IAYnB,OAAO,CAAC,UAAU;IAkBlB;;;OAGG;YACW,kBAAkB;IAehC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,mBAAmB;IAO3B;;;;;;;;;;;;;;;;;;;OAmBG;YACW,sBAAsB;IAyBpC;;;;;;OAMG;YACW,iBAAiB;IASzB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAInD;IAEK,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAI9D;CACF;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,CAAC,EAC3C,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,SAAS,aAAa,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,YAAY,EAC1B,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,GACpD,YAAY,CASd"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { gameSettingsSchema, } from '@chaos-overlords/contracts';
|
|
2
2
|
import { safeParse } from 'valibot';
|
|
3
3
|
import { ConflictError, ForbiddenError, NotFoundError } from '../domain/errors';
|
|
4
|
-
import { authoritativeCandidates } from '../logic/turn-logic';
|
|
4
|
+
import { authoritativeCandidates, tieBreaker } from '../logic/turn-logic';
|
|
5
5
|
import { requireInProgress, requireParticipant, requireTurn } from './guards';
|
|
6
6
|
/**
|
|
7
7
|
* Snapshots kept per live match. Enough to cover a desync being repaired while an earlier one is
|
|
@@ -171,9 +171,12 @@ export class SnapshotService {
|
|
|
171
171
|
* the one case a host-only rule could not serve at all: a host that is itself the odd one out can
|
|
172
172
|
* never name a majority hash, so no repair existed and the match stayed paused until retention
|
|
173
173
|
* collected it. A TIE — most of all the 1-1 split of a two-player match — is different: there is
|
|
174
|
-
* no majority, nothing to count, and no third party to ask, so
|
|
175
|
-
*
|
|
176
|
-
* whoever uploaded first.
|
|
174
|
+
* no majority, nothing to count, and no third party to ask, so exactly one player breaks it and
|
|
175
|
+
* nobody else may. Letting either side of a tie impose its state would hand a two-player match to
|
|
176
|
+
* whoever uploaded first. That player is {@link tieBreaker}'s: the host when the host holds one
|
|
177
|
+
* of the tied hashes, which is every tie of four players or fewer, and otherwise the
|
|
178
|
+
* lowest-numbered seat that does. It is judged on the reports and the roster as they are now, the
|
|
179
|
+
* same inputs the announcement that named the player was computed from.
|
|
177
180
|
*/
|
|
178
181
|
async requireRepairAuthority(match, player, turn, stateHash) {
|
|
179
182
|
const [players, reports] = await Promise.all([
|
|
@@ -184,10 +187,12 @@ export class SnapshotService {
|
|
|
184
187
|
if (!candidates.includes(stateHash)) {
|
|
185
188
|
throw new ConflictError('A recovery snapshot must match a state hash the players reported most often', { reason: 'uncorroborated_state_hash', candidateStateHashes: candidates });
|
|
186
189
|
}
|
|
187
|
-
if (candidates.length === 1
|
|
190
|
+
if (candidates.length === 1)
|
|
188
191
|
return;
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
if (player.id === tieBreaker(players, reports, candidates, match.hostPlayerId))
|
|
193
|
+
return;
|
|
194
|
+
throw new ForbiddenError('Only the designated player may break a tie between reported states', {
|
|
195
|
+
reason: 'not_tie_breaker',
|
|
191
196
|
candidateStateHashes: candidates,
|
|
192
197
|
});
|
|
193
198
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SnapshotService.js","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,kBAAkB,GAGnB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAEnC,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"SnapshotService.js","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"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"}
|
|
@@ -180,13 +180,13 @@ export declare class TurnService {
|
|
|
180
180
|
/** Tell clients the open turn's restarted clock, then arm the timer that seals it. */
|
|
181
181
|
private announceDeadline;
|
|
182
182
|
/** Re-run the verdict of every unconfirmed turn and the auto-seal of the open one. */
|
|
183
|
-
reevaluate(matchId: string): Promise<void>;
|
|
183
|
+
reevaluate(matchId: string, options?: SettleOptions): Promise<void>;
|
|
184
184
|
/**
|
|
185
185
|
* Decide turn `number` from the reports on file. Idempotent and race-safe through CAS. Returns
|
|
186
186
|
* whether this call finished anything a confirmation cut short (see `finishConfirmation`), which
|
|
187
187
|
* is what the sweep reports as a repair.
|
|
188
188
|
*/
|
|
189
|
-
settle(matchId: string, number: number): Promise<boolean>;
|
|
189
|
+
settle(matchId: string, number: number, options?: SettleOptions): Promise<boolean>;
|
|
190
190
|
/**
|
|
191
191
|
* Tell clients a match is paused, unless the log's last word on its status already says so.
|
|
192
192
|
*
|
|
@@ -197,6 +197,22 @@ export declare class TurnService {
|
|
|
197
197
|
* since the lift saw no pause to announce and so said nothing.
|
|
198
198
|
*/
|
|
199
199
|
private announcePause;
|
|
200
|
+
/**
|
|
201
|
+
* Announce a desync verdict again when it differs from what clients were last told about the
|
|
202
|
+
* turn. A verdict can return to an earlier one (a seat that left and rejoined restores the tie
|
|
203
|
+
* its departure broke), and a key naming only the content would find that earlier announcement
|
|
204
|
+
* in the log and drop the new one, leaving every client acting on the departure's verdict while
|
|
205
|
+
* the server enforces the tie: the designated player never learned it was named, and everybody
|
|
206
|
+
* else was refused with `not_tie_breaker`. So the verdict is compared with the latest
|
|
207
|
+
* announcement and, like a status change, keyed by the event it follows.
|
|
208
|
+
*
|
|
209
|
+
* When the latest announcement is of another turn (two turns desynced at once), the turn's own
|
|
210
|
+
* last word is not at hand. A loud call then announces anyway, keyed by that event, because a
|
|
211
|
+
* repeat of an unchanged verdict costs a duplicate clients already tolerate and a dropped change
|
|
212
|
+
* costs the match. A quiet call (the sweep) publishes only a change it can see, so a paused
|
|
213
|
+
* match costs one indexed read per desynced turn and no writes while nothing changes.
|
|
214
|
+
*/
|
|
215
|
+
private reannounceDesync;
|
|
200
216
|
/**
|
|
201
217
|
* Log `match.statusChanged` for a pause or its lift when it is owed: a pause when the last status
|
|
202
218
|
* announced is anything but a pause, a lift only when it is the pause (a match never paused, or
|
|
@@ -228,4 +244,13 @@ export declare class TurnService {
|
|
|
228
244
|
private liftUnannounced;
|
|
229
245
|
private restartClockAfterPause;
|
|
230
246
|
}
|
|
247
|
+
export interface SettleOptions {
|
|
248
|
+
/**
|
|
249
|
+
* Whether a desync whose announcement is already out is announced again whenever its verdict may
|
|
250
|
+
* have changed. On by default; the sweep turns it off, since it re-runs verdicts on a timer
|
|
251
|
+
* rather than because anything happened, and then only a change visible against the turn's own
|
|
252
|
+
* latest announcement is published.
|
|
253
|
+
*/
|
|
254
|
+
reannounce?: boolean;
|
|
255
|
+
}
|
|
231
256
|
//# sourceMappingURL=TurnService.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TurnService.d.ts","sourceRoot":"","sources":["../../src/services/TurnService.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"TurnService.d.ts","sourceRoot":"","sources":["../../src/services/TurnService.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { foreignOps, } from '@chaos-overlords/contracts';
|
|
|
2
2
|
import { activePlayers, isInProgress, } from '../domain/entities';
|
|
3
3
|
import { ConflictError, ValidationError } from '../domain/errors';
|
|
4
4
|
import { hashOrderDocument, hashOrderSet } from '../logic/crypto';
|
|
5
|
-
import { allAwaitedReady, assignSlots, awaitedSeats, evaluateConsensus, sealedByDeadline, turnDeadline, } from '../logic/turn-logic';
|
|
5
|
+
import { allAwaitedReady, assignSlots, awaitedSeats, evaluateConsensus, tieBreaker, sealedByDeadline, turnDeadline, } from '../logic/turn-logic';
|
|
6
6
|
import { requireInProgress, requireParticipant, requireTurn } from './guards';
|
|
7
7
|
import { matchStartedEvent } from './MatchQueryService';
|
|
8
8
|
import { publishSeatSummaries } from './SnapshotService';
|
|
@@ -543,7 +543,7 @@ export class TurnService {
|
|
|
543
543
|
// finished match still `running`. `settle` finishes the follow-ups of a confirmed turn whose
|
|
544
544
|
// `settledAt` is still null, and nothing else ever looks at a confirmed turn again.
|
|
545
545
|
for (const { matchId, number } of await this.deps.storage.turns.listUnannouncedVerdicts(limit)) {
|
|
546
|
-
const finished = await this.guard(matchId, number, () => this.settle(matchId, number));
|
|
546
|
+
const finished = await this.guard(matchId, number, () => this.settle(matchId, number, QUIET_SETTLE));
|
|
547
547
|
if (finished) {
|
|
548
548
|
this.deps.logger.warn('finished an interrupted verdict', { matchId, turn: number });
|
|
549
549
|
repaired += 1;
|
|
@@ -553,7 +553,7 @@ export class TurnService {
|
|
|
553
553
|
// unsettled, which no other path revisits.
|
|
554
554
|
for (const matchId of await this.deps.storage.matches.listDesynced(limit, touchedSince)) {
|
|
555
555
|
await this.guard(matchId, 0, async () => {
|
|
556
|
-
await this.reevaluate(matchId);
|
|
556
|
+
await this.reevaluate(matchId, QUIET_SETTLE);
|
|
557
557
|
return false;
|
|
558
558
|
});
|
|
559
559
|
}
|
|
@@ -739,13 +739,13 @@ export class TurnService {
|
|
|
739
739
|
await this.deps.scheduler.schedule({ matchId, turn, dueAt: deadlineAt });
|
|
740
740
|
}
|
|
741
741
|
/** Re-run the verdict of every unconfirmed turn and the auto-seal of the open one. */
|
|
742
|
-
async reevaluate(matchId) {
|
|
742
|
+
async reevaluate(matchId, options = {}) {
|
|
743
743
|
const match = await this.deps.storage.matches.get(matchId);
|
|
744
744
|
if (!match || !isInProgress(match))
|
|
745
745
|
return;
|
|
746
746
|
const unsettled = await this.deps.storage.turns.listUnsettled(matchId);
|
|
747
747
|
for (const turn of unsettled) {
|
|
748
|
-
await this.settle(matchId, turn.number);
|
|
748
|
+
await this.settle(matchId, turn.number, options);
|
|
749
749
|
}
|
|
750
750
|
// A match left `desynced` with nothing unsettled is a verdict whose consequences were cut short
|
|
751
751
|
// after its compare-and-swap. Nothing else reaches `resumeAfterDesync`, so without this the
|
|
@@ -760,7 +760,7 @@ export class TurnService {
|
|
|
760
760
|
* whether this call finished anything a confirmation cut short (see `finishConfirmation`), which
|
|
761
761
|
* is what the sweep reports as a repair.
|
|
762
762
|
*/
|
|
763
|
-
async settle(matchId, number) {
|
|
763
|
+
async settle(matchId, number, options = {}) {
|
|
764
764
|
const [match, turn] = await Promise.all([
|
|
765
765
|
this.deps.storage.matches.get(matchId),
|
|
766
766
|
this.deps.storage.turns.get(matchId, number),
|
|
@@ -831,17 +831,27 @@ export class TurnService {
|
|
|
831
831
|
// null one means the announcement is still owed. It used to be claimed first and published
|
|
832
832
|
// after, and a publish that threw in between lost the announcement for good. The event is
|
|
833
833
|
// keyed, so racing verdicts and repeats log it once.
|
|
834
|
+
const payload = {
|
|
835
|
+
turn: number,
|
|
836
|
+
reports: verdict.reports,
|
|
837
|
+
candidateStateHashes: verdict.candidateStateHashes,
|
|
838
|
+
tieBreakerPlayerId: tieBreaker(players, reports, verdict.candidateStateHashes, match.hostPlayerId),
|
|
839
|
+
};
|
|
834
840
|
if (turn.desyncedAt === null) {
|
|
835
|
-
await this.publisher.publishOnce(matchId, desyncAnnouncementKey(
|
|
841
|
+
await this.publisher.publishOnce(matchId, desyncAnnouncementKey(payload), {
|
|
836
842
|
type: 'turn.desynced',
|
|
837
|
-
payload
|
|
838
|
-
turn: number,
|
|
839
|
-
reports: verdict.reports,
|
|
840
|
-
candidateStateHashes: verdict.candidateStateHashes,
|
|
841
|
-
},
|
|
843
|
+
payload,
|
|
842
844
|
});
|
|
843
845
|
await this.deps.storage.turns.claimDesyncAnnouncement(matchId, number, now);
|
|
844
846
|
}
|
|
847
|
+
else {
|
|
848
|
+
// The verdict was re-run because something changed: a client re-reported after checking
|
|
849
|
+
// its own state, or the roster moved. Either can change who holds the most-reported hash
|
|
850
|
+
// and who breaks a tie, and a client acts on the announcement it last saw, so a changed
|
|
851
|
+
// verdict is announced again. The sweep runs this too, quietly, as the retry for a
|
|
852
|
+
// re-announcement whose publish threw after the change it follows was already committed.
|
|
853
|
+
await this.reannounceDesync(matchId, payload, options.reannounce !== false);
|
|
854
|
+
}
|
|
845
855
|
// Not behind the receipt above: a turn stamped by an older build that died before pausing
|
|
846
856
|
// the match is paused only now, and its pause is still owed to clients.
|
|
847
857
|
await this.announcePause(matchId);
|
|
@@ -866,6 +876,46 @@ export class TurnService {
|
|
|
866
876
|
await this.announceStatus(matchId, 'running');
|
|
867
877
|
}
|
|
868
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* Announce a desync verdict again when it differs from what clients were last told about the
|
|
881
|
+
* turn. A verdict can return to an earlier one (a seat that left and rejoined restores the tie
|
|
882
|
+
* its departure broke), and a key naming only the content would find that earlier announcement
|
|
883
|
+
* in the log and drop the new one, leaving every client acting on the departure's verdict while
|
|
884
|
+
* the server enforces the tie: the designated player never learned it was named, and everybody
|
|
885
|
+
* else was refused with `not_tie_breaker`. So the verdict is compared with the latest
|
|
886
|
+
* announcement and, like a status change, keyed by the event it follows.
|
|
887
|
+
*
|
|
888
|
+
* When the latest announcement is of another turn (two turns desynced at once), the turn's own
|
|
889
|
+
* last word is not at hand. A loud call then announces anyway, keyed by that event, because a
|
|
890
|
+
* repeat of an unchanged verdict costs a duplicate clients already tolerate and a dropped change
|
|
891
|
+
* costs the match. A quiet call (the sweep) publishes only a change it can see, so a paused
|
|
892
|
+
* match costs one indexed read per desynced turn and no writes while nothing changes.
|
|
893
|
+
*/
|
|
894
|
+
async reannounceDesync(matchId, payload, loud) {
|
|
895
|
+
const last = await this.deps.storage.events.latestOfType(matchId, 'turn.desynced');
|
|
896
|
+
const lastPayload = last?.type === 'turn.desynced' ? last.payload : null;
|
|
897
|
+
if (!last || !lastPayload) {
|
|
898
|
+
if (loud)
|
|
899
|
+
await this.publisher.publishOnce(matchId, desyncAnnouncementKey(payload), {
|
|
900
|
+
type: 'turn.desynced',
|
|
901
|
+
payload,
|
|
902
|
+
});
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (lastPayload.turn === payload.turn) {
|
|
906
|
+
if ((lastPayload.tieBreakerPlayerId ?? null) === payload.tieBreakerPlayerId &&
|
|
907
|
+
lastPayload.candidateStateHashes.join(',') === payload.candidateStateHashes.join(',')) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
else if (!loud) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
await this.publisher.publishOnce(matchId, `${desyncAnnouncementKey(payload)}:after:${last.seq}`, {
|
|
915
|
+
type: 'turn.desynced',
|
|
916
|
+
payload,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
869
919
|
/**
|
|
870
920
|
* Log `match.statusChanged` for a pause or its lift when it is owed: a pause when the last status
|
|
871
921
|
* announced is anything but a pause, a lift only when it is the pause (a match never paused, or
|
|
@@ -988,7 +1038,14 @@ export class TurnService {
|
|
|
988
1038
|
*/
|
|
989
1039
|
const sealAnnouncementKey = (turn) => `turn.sealed:${turn}`;
|
|
990
1040
|
const confirmationKey = (turn) => `turn.confirmed:${turn}`;
|
|
991
|
-
|
|
1041
|
+
/**
|
|
1042
|
+
* The dedupe key of a desync announcement: the turn and everything a client acts on, so a verdict
|
|
1043
|
+
* announced again with the same content is logged once. The reports themselves are left out,
|
|
1044
|
+
* because a report that changes without changing the candidates or the tie-breaker asks nothing
|
|
1045
|
+
* new of anybody.
|
|
1046
|
+
*/
|
|
1047
|
+
const desyncAnnouncementKey = (payload) => `turn.desynced:${payload.turn}:${payload.candidateStateHashes.join(',')}:${payload.tieBreakerPlayerId ?? ''}`;
|
|
1048
|
+
const QUIET_SETTLE = { reannounce: false };
|
|
992
1049
|
/**
|
|
993
1050
|
* A pause or its lift, named by the status event it follows: the same change announced after the
|
|
994
1051
|
* same last word is the same fact, and the next pause of the match follows a different event.
|