@chaos-overlords/worker 0.9.2 → 0.9.4
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/package.json +8 -8
- package/src/MatchHub.ts +29 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaos-overlords/worker",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "Cloudflare Workers runtime facade: the shared Hono app over D1, with one Durable Object per match for SSE fan-out and turn-deadline alarms.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chaos-overlords",
|
|
@@ -47,17 +47,17 @@
|
|
|
47
47
|
"provenance": true
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@chaos-overlords/bug-reports": "0.9.
|
|
51
|
-
"@chaos-overlords/contracts": "0.9.
|
|
52
|
-
"@chaos-overlords/kernel": "0.9.
|
|
53
|
-
"@chaos-overlords/server": "0.9.
|
|
54
|
-
"@chaos-overlords/storage": "0.9.
|
|
50
|
+
"@chaos-overlords/bug-reports": "0.9.4",
|
|
51
|
+
"@chaos-overlords/contracts": "0.9.4",
|
|
52
|
+
"@chaos-overlords/kernel": "0.9.4",
|
|
53
|
+
"@chaos-overlords/server": "0.9.4",
|
|
54
|
+
"@chaos-overlords/storage": "0.9.4",
|
|
55
55
|
"drizzle-orm": "^0.45.2",
|
|
56
56
|
"hono": "^4.13.8"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@chaos-overlords/client": "0.9.
|
|
60
|
-
"@chaos-overlords/conformance": "0.9.
|
|
59
|
+
"@chaos-overlords/client": "0.9.4",
|
|
60
|
+
"@chaos-overlords/conformance": "0.9.4",
|
|
61
61
|
"@cloudflare/vitest-pool-workers": "^0.22.0",
|
|
62
62
|
"@cloudflare/workers-types": "^5.20260920.1",
|
|
63
63
|
"typescript": "^7.0.2",
|
package/src/MatchHub.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
EARLY_DEADLINE_RETRY_MS,
|
|
2
3
|
isDomainError,
|
|
3
4
|
type MultiplayerStorage,
|
|
4
5
|
type PersistedEvent,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
DEFAULT_SERVER_CONFIG,
|
|
9
10
|
isActiveMember,
|
|
10
11
|
LocalEventHub,
|
|
12
|
+
logStreamClosed,
|
|
11
13
|
} from '@chaos-overlords/server'
|
|
12
14
|
import { createSqliteStorage, sqliteSchema } from '@chaos-overlords/storage/sqlite'
|
|
13
15
|
import type { DurableObjectState } from '@cloudflare/workers-types'
|
|
@@ -18,10 +20,33 @@ import { buildKernel, HUB_PATHS, workerLogger } from './kernel'
|
|
|
18
20
|
interface PendingDeadline {
|
|
19
21
|
matchId: string
|
|
20
22
|
turn: number
|
|
23
|
+
/** When the alarm was set for. Absent on keys written before it was recorded. */
|
|
24
|
+
dueAtMs?: number
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
const DEADLINE_KEY = 'deadline'
|
|
24
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Where a deadline the kernel re-arms from inside an alarm may be set for.
|
|
31
|
+
*
|
|
32
|
+
* An alarm that the kernel finds early is re-armed for the same turn, floored against the object's
|
|
33
|
+
* `Date.now()`. That floor means nothing to the alarm scheduler when the object's clock lags it:
|
|
34
|
+
* the scheduler has already passed the time the alarm fired at, so a deadline at or before that
|
|
35
|
+
* time fires again at once, and again, building a kernel and reading D1 each time until the
|
|
36
|
+
* object's clock catches up. Having fired proves the scheduler's clock reached `fired.dueAtMs`, so
|
|
37
|
+
* the retry goes at least the kernel's retry interval past that, which bounds the rate on the
|
|
38
|
+
* scheduler's own clock. Any other turn's deadline (the successor a seal opened) is set as asked.
|
|
39
|
+
*/
|
|
40
|
+
function retryFloor(
|
|
41
|
+
fired: PendingDeadline,
|
|
42
|
+
next: { matchId: string; turn: number },
|
|
43
|
+
dueAtMs: number,
|
|
44
|
+
): number {
|
|
45
|
+
const sameTurn = next.matchId === fired.matchId && next.turn === fired.turn
|
|
46
|
+
if (!sameTurn || fired.dueAtMs === undefined) return dueAtMs
|
|
47
|
+
return Math.max(dueAtMs, fired.dueAtMs + EARLY_DEADLINE_RETRY_MS)
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
/**
|
|
26
51
|
* One instance per match. It holds the open event streams of that match (so a notification from
|
|
27
52
|
* any Worker isolate reaches every subscriber) and the alarm for the open turn's deadline. The
|
|
@@ -47,6 +72,7 @@ export class MatchHub {
|
|
|
47
72
|
workerLogger.info('answered an already abandoned event stream', { matchId, playerId }),
|
|
48
73
|
revalidate: (matchId, playerId) =>
|
|
49
74
|
isActiveMember(this.repositories.players, matchId, playerId),
|
|
75
|
+
closed: logStreamClosed(workerLogger),
|
|
50
76
|
},
|
|
51
77
|
)
|
|
52
78
|
}
|
|
@@ -102,7 +128,8 @@ export class MatchHub {
|
|
|
102
128
|
notifier: { notify: async (event) => this.hub.notify(event) },
|
|
103
129
|
streams: this.hub,
|
|
104
130
|
scheduler: {
|
|
105
|
-
schedule: async (input) =>
|
|
131
|
+
schedule: async (input) =>
|
|
132
|
+
this.arm(input.matchId, input.turn, retryFloor(pending, input, input.dueAt.getTime())),
|
|
106
133
|
},
|
|
107
134
|
})
|
|
108
135
|
const sealed = await kernel.turns.trySeal(pending.matchId, pending.turn, 'deadline')
|
|
@@ -112,7 +139,7 @@ export class MatchHub {
|
|
|
112
139
|
|
|
113
140
|
/** Record which turn the alarm is for, then set the alarm. One deadline is pending at a time. */
|
|
114
141
|
private async arm(matchId: string, turn: number, dueAtMs: number): Promise<void> {
|
|
115
|
-
await this.state.storage.put<PendingDeadline>(DEADLINE_KEY, { matchId, turn })
|
|
142
|
+
await this.state.storage.put<PendingDeadline>(DEADLINE_KEY, { matchId, turn, dueAtMs })
|
|
116
143
|
await this.state.storage.setAlarm(dueAtMs)
|
|
117
144
|
}
|
|
118
145
|
|