@chaos-overlords/kernel 0.8.2 → 0.9.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 +4 -0
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js +9 -1
- package/dist/domain/entities.js.map +1 -1
- package/dist/logic/turn-logic.d.ts.map +1 -1
- package/dist/logic/turn-logic.js +2 -4
- package/dist/logic/turn-logic.js.map +1 -1
- package/dist/services/LobbyService.d.ts +8 -0
- package/dist/services/LobbyService.d.ts.map +1 -1
- package/dist/services/LobbyService.js +49 -73
- package/dist/services/LobbyService.js.map +1 -1
- package/dist/services/MatchQueryService.d.ts +4 -2
- package/dist/services/MatchQueryService.d.ts.map +1 -1
- package/dist/services/MatchQueryService.js +14 -5
- package/dist/services/MatchQueryService.js.map +1 -1
- package/dist/services/RateLimiter.d.ts +4 -0
- package/dist/services/RateLimiter.d.ts.map +1 -1
- package/dist/services/RateLimiter.js +17 -13
- package/dist/services/RateLimiter.js.map +1 -1
- package/dist/services/RetentionService.d.ts +2 -0
- package/dist/services/RetentionService.d.ts.map +1 -1
- package/dist/services/RetentionService.js +14 -34
- package/dist/services/RetentionService.js.map +1 -1
- package/dist/services/SnapshotService.d.ts +9 -0
- package/dist/services/SnapshotService.d.ts.map +1 -1
- package/dist/services/SnapshotService.js +19 -23
- package/dist/services/SnapshotService.js.map +1 -1
- package/dist/services/TurnService.d.ts +7 -9
- package/dist/services/TurnService.d.ts.map +1 -1
- package/dist/services/TurnService.js +33 -47
- package/dist/services/TurnService.js.map +1 -1
- package/dist/services/guards.d.ts +9 -0
- package/dist/services/guards.d.ts.map +1 -0
- package/dist/services/guards.js +22 -0
- package/dist/services/guards.js.map +1 -0
- package/package.json +2 -2
|
@@ -26,36 +26,40 @@ export class RateLimiter {
|
|
|
26
26
|
/** Returns the retry delay in seconds when over the limit, or null when the call is allowed. */
|
|
27
27
|
take(key) {
|
|
28
28
|
const now = this.clock.now().getTime();
|
|
29
|
-
const entry = this.
|
|
30
|
-
if (!entry
|
|
29
|
+
const entry = this.live(key, now);
|
|
30
|
+
if (!entry) {
|
|
31
31
|
this.windows.set(key, { windowStart: now, count: 1 });
|
|
32
32
|
this.prune(now);
|
|
33
33
|
this.evictOldest();
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
|
-
if (entry.count >= this.options.limit)
|
|
37
|
-
return
|
|
38
|
-
}
|
|
36
|
+
if (entry.count >= this.options.limit)
|
|
37
|
+
return this.retryAfter(entry, now);
|
|
39
38
|
entry.count += 1;
|
|
40
39
|
return null;
|
|
41
40
|
}
|
|
42
41
|
/** What `take` would answer right now, spending nothing. */
|
|
43
42
|
peek(key) {
|
|
44
43
|
const now = this.clock.now().getTime();
|
|
45
|
-
const entry = this.
|
|
46
|
-
if (!entry ||
|
|
47
|
-
return null;
|
|
48
|
-
if (entry.count < this.options.limit)
|
|
44
|
+
const entry = this.live(key, now);
|
|
45
|
+
if (!entry || entry.count < this.options.limit)
|
|
49
46
|
return null;
|
|
50
|
-
return
|
|
47
|
+
return this.retryAfter(entry, now);
|
|
51
48
|
}
|
|
52
49
|
/** How much of `key`'s budget the current window has already spent; 0 once it has rolled. */
|
|
53
50
|
spent(key) {
|
|
54
|
-
|
|
51
|
+
return this.live(key, this.clock.now().getTime())?.count ?? 0;
|
|
52
|
+
}
|
|
53
|
+
/** The window `key` is spending from, or undefined once it has rolled or was never opened. */
|
|
54
|
+
live(key, now) {
|
|
55
55
|
const entry = this.windows.get(key);
|
|
56
56
|
if (!entry || now - entry.windowStart >= this.options.windowMs)
|
|
57
|
-
return
|
|
58
|
-
return entry
|
|
57
|
+
return undefined;
|
|
58
|
+
return entry;
|
|
59
|
+
}
|
|
60
|
+
/** Whole seconds until `entry`'s window rolls, rounded up. */
|
|
61
|
+
retryAfter(entry, now) {
|
|
62
|
+
return Math.ceil((entry.windowStart + this.options.windowMs - now) / 1000);
|
|
59
63
|
}
|
|
60
64
|
/**
|
|
61
65
|
* Drop windows that have expired, at most once per window period.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RateLimiter.js","sourceRoot":"","sources":["../../src/services/RateLimiter.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,QAAQ,GAAG,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"RateLimiter.js","sourceRoot":"","sources":["../../src/services/RateLimiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -46,5 +46,7 @@ export declare class RetentionService {
|
|
|
46
46
|
private collectAbandonedLive;
|
|
47
47
|
/** The same delete without the roster test, on a window long enough to stand in for it. */
|
|
48
48
|
private collectSilentLive;
|
|
49
|
+
/** One retention window: a window of 0 is switched off and reads neither the clock nor storage. */
|
|
50
|
+
private collectWindow;
|
|
49
51
|
}
|
|
50
52
|
//# sourceMappingURL=RetentionService.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RetentionService.d.ts","sourceRoot":"","sources":["../../src/services/RetentionService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAKxC,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;;;OAQG;IACH,qBAAqB,EAAE,MAAM,CAAA;IAC7B;;;;;;;;;OASG;IACH,kBAAkB,EAAE,MAAM,CAAA;IAC1B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAA;CAClB;AAID,eAAO,MAAM,iBAAiB,EAAE,eAK/B,CAAA;AAED;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFzB,YACmB,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC,EACtD,MAAM,GAAE,eAAmC,EAC1D;IAEE,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAM/B;
|
|
1
|
+
{"version":3,"file":"RetentionService.d.ts","sourceRoot":"","sources":["../../src/services/RetentionService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAKxC,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;;;OAQG;IACH,qBAAqB,EAAE,MAAM,CAAA;IAC7B;;;;;;;;;OASG;IACH,kBAAkB,EAAE,MAAM,CAAA;IAC1B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAA;CAClB;AAID,eAAO,MAAM,iBAAiB,EAAE,eAK/B,CAAA;AAED;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFzB,YACmB,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC,EACtD,MAAM,GAAE,eAAmC,EAC1D;IAEE,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAM/B;IAED,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,oBAAoB;IAS5B,2FAA2F;IAC3F,OAAO,CAAC,iBAAiB;IASzB,mGAAmG;YACrF,aAAa;CAW5B"}
|
|
@@ -29,44 +29,24 @@ export class RetentionService {
|
|
|
29
29
|
(await this.collectAbandonedLive()) +
|
|
30
30
|
(await this.collectSilentLive()));
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return 0;
|
|
35
|
-
const before = new Date(this.deps.clock.now().getTime() - this.policy.maxAgeMs);
|
|
36
|
-
const deleted = await this.deps.storage.matches.deleteInactive(COLLECTABLE, before, this.policy.batchSize);
|
|
37
|
-
if (deleted > 0) {
|
|
38
|
-
this.deps.logger.info('retention deleted inactive matches', {
|
|
39
|
-
deleted,
|
|
40
|
-
before: before.toISOString(),
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
return deleted;
|
|
32
|
+
collectTerminated() {
|
|
33
|
+
return this.collectWindow(this.policy.maxAgeMs, 'retention deleted inactive matches', (before) => this.deps.storage.matches.deleteInactive(COLLECTABLE, before, this.policy.batchSize));
|
|
44
34
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return 0;
|
|
48
|
-
const before = new Date(this.deps.clock.now().getTime() - this.policy.abandonedLiveMaxAgeMs);
|
|
49
|
-
const deleted = await this.deps.storage.matches.deleteAbandonedLive(before, this.policy.batchSize, true);
|
|
50
|
-
if (deleted > 0) {
|
|
51
|
-
this.deps.logger.info('retention deleted long-abandoned live matches', {
|
|
52
|
-
deleted,
|
|
53
|
-
before: before.toISOString(),
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return deleted;
|
|
35
|
+
collectAbandonedLive() {
|
|
36
|
+
return this.collectWindow(this.policy.abandonedLiveMaxAgeMs, 'retention deleted long-abandoned live matches', (before) => this.deps.storage.matches.deleteAbandonedLive(before, this.policy.batchSize, true));
|
|
57
37
|
}
|
|
58
38
|
/** The same delete without the roster test, on a window long enough to stand in for it. */
|
|
59
|
-
|
|
60
|
-
|
|
39
|
+
collectSilentLive() {
|
|
40
|
+
return this.collectWindow(this.policy.silentLiveMaxAgeMs, 'retention deleted silent live matches', (before) => this.deps.storage.matches.deleteAbandonedLive(before, this.policy.batchSize, false));
|
|
41
|
+
}
|
|
42
|
+
/** One retention window: a window of 0 is switched off and reads neither the clock nor storage. */
|
|
43
|
+
async collectWindow(maxAgeMs, message, run) {
|
|
44
|
+
if (maxAgeMs <= 0)
|
|
61
45
|
return 0;
|
|
62
|
-
const before = new Date(this.deps.clock.now().getTime() -
|
|
63
|
-
const deleted = await
|
|
64
|
-
if (deleted > 0)
|
|
65
|
-
this.deps.logger.info(
|
|
66
|
-
deleted,
|
|
67
|
-
before: before.toISOString(),
|
|
68
|
-
});
|
|
69
|
-
}
|
|
46
|
+
const before = new Date(this.deps.clock.now().getTime() - maxAgeMs);
|
|
47
|
+
const deleted = await run(before);
|
|
48
|
+
if (deleted > 0)
|
|
49
|
+
this.deps.logger.info(message, { deleted, before: before.toISOString() });
|
|
70
50
|
return deleted;
|
|
71
51
|
}
|
|
72
52
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RetentionService.js","sourceRoot":"","sources":["../../src/services/RetentionService.ts"],"names":[],"mappings":"AAGA,qGAAqG;AACrG,MAAM,WAAW,GAA2B,CAAC,UAAU,EAAE,WAAW,EAAE,OAAO,CAAC,CAAA;AA8B9E,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAElC,MAAM,CAAC,MAAM,iBAAiB,GAAoB;IAChD,QAAQ,EAAE,EAAE,GAAG,MAAM;IACrB,qBAAqB,EAAE,EAAE,GAAG,MAAM;IAClC,kBAAkB,EAAE,GAAG,GAAG,MAAM;IAChC,SAAS,EAAE,EAAE;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,gBAAgB;IAER,IAAI;IACJ,MAAM;IAFzB,YACmB,IAAsD,EACtD,MAAM,GAAoB,iBAAiB;oBAD3C,IAAI;sBACJ,MAAM;IACtB,CAAC;IAEJ,KAAK,CAAC,OAAO;QACX,OAAO,CACL,CAAC,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAChC,CAAC,MAAM,IAAI,CAAC,oBAAoB,EAAE,CAAC;YACnC,CAAC,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC,CACjC,CAAA;IACH,CAAC;IAEO,
|
|
1
|
+
{"version":3,"file":"RetentionService.js","sourceRoot":"","sources":["../../src/services/RetentionService.ts"],"names":[],"mappings":"AAGA,qGAAqG;AACrG,MAAM,WAAW,GAA2B,CAAC,UAAU,EAAE,WAAW,EAAE,OAAO,CAAC,CAAA;AA8B9E,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAElC,MAAM,CAAC,MAAM,iBAAiB,GAAoB;IAChD,QAAQ,EAAE,EAAE,GAAG,MAAM;IACrB,qBAAqB,EAAE,EAAE,GAAG,MAAM;IAClC,kBAAkB,EAAE,GAAG,GAAG,MAAM;IAChC,SAAS,EAAE,EAAE;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,gBAAgB;IAER,IAAI;IACJ,MAAM;IAFzB,YACmB,IAAsD,EACtD,MAAM,GAAoB,iBAAiB;oBAD3C,IAAI;sBACJ,MAAM;IACtB,CAAC;IAEJ,KAAK,CAAC,OAAO;QACX,OAAO,CACL,CAAC,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAChC,CAAC,MAAM,IAAI,CAAC,oBAAoB,EAAE,CAAC;YACnC,CAAC,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC,CACjC,CAAA;IACH,CAAC;IAEO,iBAAiB;QACvB,OAAO,IAAI,CAAC,aAAa,CACvB,IAAI,CAAC,MAAM,CAAC,QAAQ,EACpB,oCAAoC,EACpC,CAAC,MAAM,EAAE,EAAE,CACT,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CACvF,CAAA;IACH,CAAC;IAEO,oBAAoB;QAC1B,OAAO,IAAI,CAAC,aAAa,CACvB,IAAI,CAAC,MAAM,CAAC,qBAAqB,EACjC,+CAA+C,EAC/C,CAAC,MAAM,EAAE,EAAE,CACT,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CACrF,CAAA;IACH,CAAC;IAED,2FAA2F;IACnF,iBAAiB;QACvB,OAAO,IAAI,CAAC,aAAa,CACvB,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAC9B,uCAAuC,EACvC,CAAC,MAAM,EAAE,EAAE,CACT,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CACtF,CAAA;IACH,CAAC;IAED,mGAAmG;IAC3F,KAAK,CAAC,aAAa,CACzB,QAAgB,EAChB,OAAe,EACf,GAAsC;QAEtC,IAAI,QAAQ,IAAI,CAAC;YAAE,OAAO,CAAC,CAAA;QAC3B,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAA;QACnE,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,CAAA;QACjC,IAAI,OAAO,GAAG,CAAC;YAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;QAC1F,OAAO,OAAO,CAAA;IAChB,CAAC;CACF"}
|
|
@@ -32,6 +32,15 @@ export declare class SnapshotService {
|
|
|
32
32
|
* reconnects next, and `getLatest` is what finds it.
|
|
33
33
|
*/
|
|
34
34
|
private storeCheckpoint;
|
|
35
|
+
/**
|
|
36
|
+
* Write an accepted snapshot: the row, the seat summaries it carries, then the prune.
|
|
37
|
+
*
|
|
38
|
+
* The merge is judged before anything is written: a refusal after the row landed would leave a
|
|
39
|
+
* stored snapshot the caller was told was rejected, with no `snapshot.available` and no verdict.
|
|
40
|
+
* Re-running the settings schema over the merge is what holds the blob to its cap, which is why
|
|
41
|
+
* every upload path comes through here before its write.
|
|
42
|
+
*/
|
|
43
|
+
private store;
|
|
35
44
|
private snapshotOf;
|
|
36
45
|
/**
|
|
37
46
|
* Late-join hints are optional metadata; the snapshot the players are waiting on is not. A
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,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;
|
|
1
|
+
{"version":3,"file":"SnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,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;;;;;;;OAOG;YACW,KAAK;IAWnB,OAAO,CAAC,UAAU;IAkBlB;;;OAGG;YACW,oBAAoB;IAYlC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,mBAAmB;IAO3B;;;;;;;;;;;;;;;;OAgBG;YACW,sBAAsB;IAwBpC;;;;;;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;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,YAAY,EAC1B,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,GACpD,YAAY,CASd"}
|
|
@@ -2,6 +2,7 @@ import { gameSettingsSchema, } from '@chaos-overlords/contracts';
|
|
|
2
2
|
import { safeParse } from 'valibot';
|
|
3
3
|
import { ConflictError, ForbiddenError, NotFoundError } from '../domain/errors';
|
|
4
4
|
import { authoritativeCandidates } from '../logic/turn-logic';
|
|
5
|
+
import { requireInProgress, requireParticipant, requireTurn } from './guards';
|
|
5
6
|
/**
|
|
6
7
|
* Snapshots kept per live match. Enough to cover a desync being repaired while an earlier one is
|
|
7
8
|
* still being fetched by a straggler, and far fewer than a long match would otherwise accumulate.
|
|
@@ -31,12 +32,8 @@ export class SnapshotService {
|
|
|
31
32
|
reason: 'host_only',
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
if (!isBootstrap && match.status !== 'running' && match.status !== 'desynced') {
|
|
38
|
-
throw new ConflictError('The match is not in progress', { reason: 'match_not_running' });
|
|
39
|
-
}
|
|
35
|
+
requireParticipant(player);
|
|
36
|
+
requireInProgress(match);
|
|
40
37
|
if (request.turn > match.currentTurn) {
|
|
41
38
|
throw new ConflictError('Cannot snapshot a turn that has not opened', {
|
|
42
39
|
reason: 'future_turn',
|
|
@@ -49,26 +46,17 @@ export class SnapshotService {
|
|
|
49
46
|
}
|
|
50
47
|
if (isBootstrap) {
|
|
51
48
|
// Nothing has been reported yet, so there is no consensus a bootstrap could contradict.
|
|
52
|
-
await this.
|
|
53
|
-
await this.publishSeatSummaries(match.id, mergeSeatSummaries(match.settings.gameSettings, request.seatSummaries));
|
|
54
|
-
await this.pruneOldSnapshots(match.id);
|
|
49
|
+
await this.store(match, player.id, request);
|
|
55
50
|
return;
|
|
56
51
|
}
|
|
57
|
-
const turn = await this.deps.storage.turns
|
|
58
|
-
if (!turn)
|
|
59
|
-
throw new NotFoundError('No such turn', { reason: 'unknown_turn' });
|
|
52
|
+
const turn = await requireTurn(this.deps.storage.turns, match.id, request.turn);
|
|
60
53
|
if (turn.status === 'confirmed') {
|
|
61
54
|
await this.storeCheckpoint(match, player, turn, request);
|
|
62
55
|
return;
|
|
63
56
|
}
|
|
64
57
|
this.requireDesyncedTurn(turn);
|
|
65
58
|
await this.requireRepairAuthority(match, player, request.turn, request.stateHash);
|
|
66
|
-
|
|
67
|
-
// snapshot the caller was told was rejected, with no `snapshot.available` and no verdict.
|
|
68
|
-
const gameSettings = mergeSeatSummaries(match.settings.gameSettings, request.seatSummaries);
|
|
69
|
-
await this.deps.storage.snapshots.put(await this.snapshotOf(match, player.id, request));
|
|
70
|
-
await this.publishSeatSummaries(match.id, gameSettings);
|
|
71
|
-
await this.pruneOldSnapshots(match.id);
|
|
59
|
+
await this.store(match, player.id, request);
|
|
72
60
|
await this.publisher.publish(match.id, {
|
|
73
61
|
type: 'snapshot.available',
|
|
74
62
|
payload: {
|
|
@@ -107,15 +95,23 @@ export class SnapshotService {
|
|
|
107
95
|
candidateStateHashes: turn.stateHash === null ? [] : [turn.stateHash],
|
|
108
96
|
});
|
|
109
97
|
}
|
|
98
|
+
await this.store(match, player.id, request);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Write an accepted snapshot: the row, the seat summaries it carries, then the prune.
|
|
102
|
+
*
|
|
103
|
+
* The merge is judged before anything is written: a refusal after the row landed would leave a
|
|
104
|
+
* stored snapshot the caller was told was rejected, with no `snapshot.available` and no verdict.
|
|
105
|
+
* Re-running the settings schema over the merge is what holds the blob to its cap, which is why
|
|
106
|
+
* every upload path comes through here before its write.
|
|
107
|
+
*/
|
|
108
|
+
async store(match, uploadedByPlayerId, request) {
|
|
110
109
|
const gameSettings = mergeSeatSummaries(match.settings.gameSettings, request.seatSummaries);
|
|
111
|
-
await this.deps.storage.snapshots.put(
|
|
110
|
+
await this.deps.storage.snapshots.put(this.snapshotOf(match, uploadedByPlayerId, request));
|
|
112
111
|
await this.publishSeatSummaries(match.id, gameSettings);
|
|
113
112
|
await this.pruneOldSnapshots(match.id);
|
|
114
113
|
}
|
|
115
|
-
|
|
116
|
-
// Re-running the settings schema over the merge is what holds the blob to its cap; it throws
|
|
117
|
-
// before anything is stored, which is why it is called on both paths before the write.
|
|
118
|
-
mergeSeatSummaries(match.settings.gameSettings, request.seatSummaries);
|
|
114
|
+
snapshotOf(match, uploadedByPlayerId, request) {
|
|
119
115
|
return {
|
|
120
116
|
matchId: match.id,
|
|
121
117
|
turn: request.turn,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SnapshotService.js","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,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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"}
|
|
@@ -81,17 +81,13 @@ export declare class TurnService {
|
|
|
81
81
|
* seats it finds absent, or by a vote on a seat that went quiet before anyone was present to ask,
|
|
82
82
|
* is exactly as blocking as one raised by a kick, and each of those used to leave the countdown
|
|
83
83
|
* running. `openTurn` applies the same rule to a turn opening while a prompt is already up.
|
|
84
|
-
*/
|
|
85
|
-
openTakeoverPrompt(matchId: string, playerId: string, turn: number): Promise<boolean>;
|
|
86
|
-
/**
|
|
87
|
-
* Pause the open turn while players decide what to do with an absent human seat.
|
|
88
84
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
85
|
+
* The pause is a no-op when there is no open turn to stop (during a seal, where the turn the
|
|
86
|
+
* absence was noticed on is already sealed and its successor opens paused instead) and when the
|
|
87
|
+
* clock is stopped already, so a second absent seat neither double-publishes nor disturbs the
|
|
88
|
+
* first pause.
|
|
93
89
|
*/
|
|
94
|
-
|
|
90
|
+
openTakeoverPrompt(matchId: string, playerId: string, turn: number): Promise<boolean>;
|
|
95
91
|
/**
|
|
96
92
|
* Stop the clock of a match whose last active player has just left.
|
|
97
93
|
*
|
|
@@ -102,6 +98,8 @@ export declare class TurnService {
|
|
|
102
98
|
private clearOpenDeadline;
|
|
103
99
|
/** Restart a full turn clock after the final outstanding absence vote closes. */
|
|
104
100
|
resumeAfterTakeoverVotes(matchId: string): Promise<void>;
|
|
101
|
+
/** Tell clients the open turn's restarted clock, then arm the timer that seals it. */
|
|
102
|
+
private announceDeadline;
|
|
105
103
|
/** Re-run the verdict of every unconfirmed turn and the auto-seal of the open one. */
|
|
106
104
|
reevaluate(matchId: string): Promise<void>;
|
|
107
105
|
/** Decide turn `number` from the reports on file. Idempotent and race-safe through CAS. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TurnService.d.ts","sourceRoot":"","sources":["../../src/services/TurnService.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,
|
|
1
|
+
{"version":3,"file":"TurnService.d.ts","sourceRoot":"","sources":["../../src/services/TurnService.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAIL,KAAK,KAAK,EAEX,MAAM,oBAAoB,CAAA;AAI3B,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;AAKtD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,UAAU,CAAA;AAE9C,yFAAyF;AACzF,eAAO,MAAM,UAAU,IAAI,CAAA;AAmB3B;;;;;;;;;;GAUG;AACH,qBAAa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAL5B,qGAAqG;IACrG,OAAO,CAAC,mBAAmB,CAAkB;IAE7C,YACmB,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,cAAc,EACxC;IAEE,YAAY,CAChB,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CAkD5B;IAED,yFAAyF;IACnF,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BrF;IAED;;;;;OAKG;YACW,YAAY;IA+D1B;;;;;;OAMG;YACW,sBAAsB;IAepC;;;;;;;OAOG;YACW,QAAQ;IAWtB;;;;OAIG;IACG,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAuD7D;IAED;;;;OAIG;IACG,KAAK,CAAC,KAAK,SAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CA8CtE;YAEa,KAAK;IAiBb,MAAM,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoD5F;IAED,6FAA6F;YAC/E,oBAAoB;IAWlC;;;;;;;;;;;;;;;;;OAiBG;IACG,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAc1F;IAED;;;;;OAKG;IACG,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAExD;YAEa,iBAAiB;IAa/B,iFAAiF;IAC3E,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB7D;IAED,sFAAsF;YACxE,gBAAgB;IAQ9B,sFAAsF;IAChF,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAc/C;IAED,2FAA2F;IACrF,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiG3D;IAED;;;;OAIG;YACW,iBAAiB;CA+BhC"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { foreignOps, } from '@chaos-overlords/contracts';
|
|
2
|
-
import { activePlayers, humanParticipants } from '../domain/entities';
|
|
3
|
-
import { ConflictError,
|
|
2
|
+
import { activePlayers, humanParticipants, isInProgress, } from '../domain/entities';
|
|
3
|
+
import { ConflictError, ValidationError } from '../domain/errors';
|
|
4
4
|
import { hashOrderDocument, hashOrderSet } from '../logic/crypto';
|
|
5
5
|
import { allActiveReady, assignSlots, evaluateConsensus, turnDeadline } from '../logic/turn-logic';
|
|
6
|
-
import {
|
|
6
|
+
import { requireInProgress, requireParticipant, requireTurn } from './guards';
|
|
7
|
+
import { matchStartedEvent } from './MatchQueryService';
|
|
7
8
|
import { mergeSeatSummaries } from './SnapshotService';
|
|
8
9
|
/** Turns are numbered from 1; 0 is the lobby's `currentTurn`, before any turn exists. */
|
|
9
10
|
export const FIRST_TURN = 1;
|
|
@@ -45,20 +46,23 @@ export class TurnService {
|
|
|
45
46
|
async submitOrders(principal, number, request) {
|
|
46
47
|
const { match, player } = principal;
|
|
47
48
|
requireRunning(match);
|
|
48
|
-
|
|
49
|
-
throw new ForbiddenError('You are no longer part of this match', { reason: 'not_active' });
|
|
50
|
-
}
|
|
49
|
+
requireParticipant(player);
|
|
51
50
|
assertOwnOps(request, player.slot);
|
|
52
51
|
const ordersHash = await hashOrderDocument(request.orders);
|
|
52
|
+
const own = {
|
|
53
|
+
turn: number,
|
|
54
|
+
orders: request.orders,
|
|
55
|
+
ready: request.ready,
|
|
56
|
+
ordersHash,
|
|
57
|
+
};
|
|
53
58
|
if (number !== match.currentTurn) {
|
|
54
59
|
// The document may have been committed and sealed while its success response was lost. The
|
|
55
60
|
// endpoint is a replacement, not an append, so an identical retry is an acknowledgement of
|
|
56
61
|
// that durable row even after currentTurn advances. A different document remains a stale
|
|
57
62
|
// write and is refused.
|
|
58
63
|
const persisted = await this.deps.storage.turns.getOrderSummary(match.id, number, player.id);
|
|
59
|
-
if (persisted?.ordersHash === ordersHash && persisted.ready === request.ready)
|
|
60
|
-
return
|
|
61
|
-
}
|
|
64
|
+
if (persisted?.ordersHash === ordersHash && persisted.ready === request.ready)
|
|
65
|
+
return own;
|
|
62
66
|
throw new ConflictError(`Turn ${match.currentTurn} is the open turn`, {
|
|
63
67
|
reason: 'not_current_turn',
|
|
64
68
|
currentTurn: match.currentTurn,
|
|
@@ -90,7 +94,7 @@ export class TurnService {
|
|
|
90
94
|
}
|
|
91
95
|
if (request.ready)
|
|
92
96
|
await this.trySeal(match.id, number, 'ready');
|
|
93
|
-
return
|
|
97
|
+
return own;
|
|
94
98
|
}
|
|
95
99
|
/** Seal `number` if its trigger condition holds. Returns whether THIS call sealed it. */
|
|
96
100
|
async trySeal(matchId, number, trigger) {
|
|
@@ -214,13 +218,7 @@ export class TurnService {
|
|
|
214
218
|
return;
|
|
215
219
|
const seated = await this.deps.storage.players.listByMatch(match.id);
|
|
216
220
|
this.deps.logger.warn('finished an interrupted start', { matchId: match.id });
|
|
217
|
-
await this.publisher.publish(match.id,
|
|
218
|
-
type: 'match.started',
|
|
219
|
-
payload: {
|
|
220
|
-
seed: match.seed,
|
|
221
|
-
players: activePlayers(seated).map((player) => toPlayerView(player, match.hostPlayerId)),
|
|
222
|
-
},
|
|
223
|
-
});
|
|
221
|
+
await this.publisher.publish(match.id, matchStartedEvent(match.seed, seated, match.hostPlayerId));
|
|
224
222
|
}
|
|
225
223
|
/**
|
|
226
224
|
* Whether the log carries an event of `type`.
|
|
@@ -367,15 +365,9 @@ export class TurnService {
|
|
|
367
365
|
}
|
|
368
366
|
async report(principal, number, request) {
|
|
369
367
|
const { match, player } = principal;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (player.status !== 'active' && player.status !== 'takeoverPending') {
|
|
374
|
-
throw new ForbiddenError('You are no longer part of this match', { reason: 'not_active' });
|
|
375
|
-
}
|
|
376
|
-
const turn = await this.deps.storage.turns.get(match.id, number);
|
|
377
|
-
if (!turn)
|
|
378
|
-
throw new NotFoundError('No such turn', { reason: 'unknown_turn' });
|
|
368
|
+
requireInProgress(match);
|
|
369
|
+
requireParticipant(player);
|
|
370
|
+
const turn = await requireTurn(this.deps.storage.turns, match.id, number);
|
|
379
371
|
if (turn.status === 'open') {
|
|
380
372
|
throw new ConflictError('The turn has not been sealed yet', { reason: 'turn_open' });
|
|
381
373
|
}
|
|
@@ -442,6 +434,11 @@ export class TurnService {
|
|
|
442
434
|
* seats it finds absent, or by a vote on a seat that went quiet before anyone was present to ask,
|
|
443
435
|
* is exactly as blocking as one raised by a kick, and each of those used to leave the countdown
|
|
444
436
|
* running. `openTurn` applies the same rule to a turn opening while a prompt is already up.
|
|
437
|
+
*
|
|
438
|
+
* The pause is a no-op when there is no open turn to stop (during a seal, where the turn the
|
|
439
|
+
* absence was noticed on is already sealed and its successor opens paused instead) and when the
|
|
440
|
+
* clock is stopped already, so a second absent seat neither double-publishes nor disturbs the
|
|
441
|
+
* first pause.
|
|
445
442
|
*/
|
|
446
443
|
async openTakeoverPrompt(matchId, playerId, turn) {
|
|
447
444
|
const opened = await this.deps.storage.takeovers.openPrompt(matchId, playerId, turn, this.deps.clock.now());
|
|
@@ -451,19 +448,8 @@ export class TurnService {
|
|
|
451
448
|
type: 'match.takeoverVoteRequested',
|
|
452
449
|
payload: { playerId, turn },
|
|
453
450
|
});
|
|
454
|
-
await this.pauseForTakeoverVote(matchId);
|
|
455
|
-
return true;
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Pause the open turn while players decide what to do with an absent human seat.
|
|
459
|
-
*
|
|
460
|
-
* Only `openTakeoverPrompt` calls this, so every prompt pauses and none has to remember to. It is
|
|
461
|
-
* a no-op when there is no open turn to stop — during a seal, where the turn the absence was
|
|
462
|
-
* noticed on is already sealed and its successor opens paused instead — and when the clock is
|
|
463
|
-
* stopped already, so a second absent seat neither double-publishes nor disturbs the first pause.
|
|
464
|
-
*/
|
|
465
|
-
async pauseForTakeoverVote(matchId) {
|
|
466
451
|
await this.clearOpenDeadline(matchId);
|
|
452
|
+
return true;
|
|
467
453
|
}
|
|
468
454
|
/**
|
|
469
455
|
* Stop the clock of a match whose last active player has just left.
|
|
@@ -476,7 +462,7 @@ export class TurnService {
|
|
|
476
462
|
}
|
|
477
463
|
async clearOpenDeadline(matchId) {
|
|
478
464
|
const match = await this.deps.storage.matches.get(matchId);
|
|
479
|
-
if (!match || (match
|
|
465
|
+
if (!match || !isInProgress(match))
|
|
480
466
|
return;
|
|
481
467
|
const turn = await this.deps.storage.turns.get(matchId, match.currentTurn);
|
|
482
468
|
if (turn?.status !== 'open' || turn.deadlineAt === null)
|
|
@@ -511,16 +497,20 @@ export class TurnService {
|
|
|
511
497
|
await this.clearOpenDeadline(matchId);
|
|
512
498
|
return;
|
|
513
499
|
}
|
|
500
|
+
await this.announceDeadline(matchId, match.currentTurn, deadlineAt);
|
|
501
|
+
}
|
|
502
|
+
/** Tell clients the open turn's restarted clock, then arm the timer that seals it. */
|
|
503
|
+
async announceDeadline(matchId, turn, deadlineAt) {
|
|
514
504
|
await this.publisher.publish(matchId, {
|
|
515
505
|
type: 'turn.deadlineExtended',
|
|
516
|
-
payload: { turn
|
|
506
|
+
payload: { turn, deadlineAt: deadlineAt.toISOString() },
|
|
517
507
|
});
|
|
518
|
-
await this.deps.scheduler.schedule({ matchId, turn
|
|
508
|
+
await this.deps.scheduler.schedule({ matchId, turn, dueAt: deadlineAt });
|
|
519
509
|
}
|
|
520
510
|
/** Re-run the verdict of every unconfirmed turn and the auto-seal of the open one. */
|
|
521
511
|
async reevaluate(matchId) {
|
|
522
512
|
const match = await this.deps.storage.matches.get(matchId);
|
|
523
|
-
if (!match || (match
|
|
513
|
+
if (!match || !isInProgress(match))
|
|
524
514
|
return;
|
|
525
515
|
const unsettled = await this.deps.storage.turns.listUnsettled(matchId);
|
|
526
516
|
for (const turn of unsettled) {
|
|
@@ -662,11 +652,7 @@ export class TurnService {
|
|
|
662
652
|
if (!(await this.deps.storage.turns.rescheduleDeadline(matchId, match.currentTurn, deadlineAt))) {
|
|
663
653
|
return;
|
|
664
654
|
}
|
|
665
|
-
await this.
|
|
666
|
-
type: 'turn.deadlineExtended',
|
|
667
|
-
payload: { turn: match.currentTurn, deadlineAt: deadlineAt.toISOString() },
|
|
668
|
-
});
|
|
669
|
-
await this.deps.scheduler.schedule({ matchId, turn: match.currentTurn, dueAt: deadlineAt });
|
|
655
|
+
await this.announceDeadline(matchId, match.currentTurn, deadlineAt);
|
|
670
656
|
}
|
|
671
657
|
}
|
|
672
658
|
/**
|