@chaos-overlords/worker 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/package.json +8 -8
- package/src/MatchHub.ts +18 -18
- package/src/kernel.ts +11 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaos-overlords/worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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.
|
|
51
|
-
"@chaos-overlords/contracts": "0.
|
|
52
|
-
"@chaos-overlords/kernel": "0.
|
|
53
|
-
"@chaos-overlords/server": "0.
|
|
54
|
-
"@chaos-overlords/storage": "0.
|
|
50
|
+
"@chaos-overlords/bug-reports": "0.9.0",
|
|
51
|
+
"@chaos-overlords/contracts": "0.9.0",
|
|
52
|
+
"@chaos-overlords/kernel": "0.9.0",
|
|
53
|
+
"@chaos-overlords/server": "0.9.0",
|
|
54
|
+
"@chaos-overlords/storage": "0.9.0",
|
|
55
55
|
"drizzle-orm": "^0.45.2",
|
|
56
56
|
"hono": "^4.13.8"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@chaos-overlords/client": "0.
|
|
60
|
-
"@chaos-overlords/conformance": "0.
|
|
59
|
+
"@chaos-overlords/client": "0.9.0",
|
|
60
|
+
"@chaos-overlords/conformance": "0.9.0",
|
|
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,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isDomainError,
|
|
3
|
+
type MultiplayerStorage,
|
|
4
|
+
type PersistedEvent,
|
|
5
|
+
} from '@chaos-overlords/kernel'
|
|
2
6
|
import { DEFAULT_SERVER_CONFIG, LocalEventHub } from '@chaos-overlords/server'
|
|
3
7
|
import { createSqliteStorage, sqliteSchema } from '@chaos-overlords/storage/sqlite'
|
|
4
8
|
import type { DurableObjectState } from '@cloudflare/workers-types'
|
|
@@ -20,13 +24,14 @@ const DEADLINE_KEY = 'deadline'
|
|
|
20
24
|
*/
|
|
21
25
|
export class MatchHub {
|
|
22
26
|
private readonly hub: LocalEventHub
|
|
27
|
+
private readonly repositories: MultiplayerStorage
|
|
23
28
|
|
|
24
29
|
constructor(
|
|
25
30
|
private readonly state: DurableObjectState,
|
|
26
31
|
private readonly env: Env,
|
|
27
32
|
) {
|
|
28
|
-
|
|
29
|
-
this.hub = new LocalEventHub(
|
|
33
|
+
this.repositories = createSqliteStorage(drizzle(env.DB, { schema: sqliteSchema }))
|
|
34
|
+
this.hub = new LocalEventHub(this.repositories.events, DEFAULT_SERVER_CONFIG.sseHeartbeatMs)
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
async fetch(request: Request): Promise<Response> {
|
|
@@ -42,11 +47,7 @@ export class MatchHub {
|
|
|
42
47
|
}
|
|
43
48
|
case HUB_PATHS.schedule: {
|
|
44
49
|
const body = (await request.json()) as PendingDeadline & { dueAt: string }
|
|
45
|
-
await this.
|
|
46
|
-
matchId: body.matchId,
|
|
47
|
-
turn: body.turn,
|
|
48
|
-
})
|
|
49
|
-
await this.state.storage.setAlarm(new Date(body.dueAt).getTime())
|
|
50
|
+
await this.arm(body.matchId, body.turn, new Date(body.dueAt).getTime())
|
|
50
51
|
return new Response(null, { status: 204 })
|
|
51
52
|
}
|
|
52
53
|
case HUB_PATHS.subscribe: {
|
|
@@ -84,13 +85,7 @@ export class MatchHub {
|
|
|
84
85
|
notifier: { notify: async (event) => this.hub.notify(event) },
|
|
85
86
|
streams: this.hub,
|
|
86
87
|
scheduler: {
|
|
87
|
-
schedule: async (input) =>
|
|
88
|
-
await this.state.storage.put<PendingDeadline>(DEADLINE_KEY, {
|
|
89
|
-
matchId: input.matchId,
|
|
90
|
-
turn: input.turn,
|
|
91
|
-
})
|
|
92
|
-
await this.state.storage.setAlarm(input.dueAt.getTime())
|
|
93
|
-
},
|
|
88
|
+
schedule: async (input) => this.arm(input.matchId, input.turn, input.dueAt.getTime()),
|
|
94
89
|
},
|
|
95
90
|
})
|
|
96
91
|
const sealed = await kernel.turns.trySeal(pending.matchId, pending.turn, 'deadline')
|
|
@@ -98,6 +93,12 @@ export class MatchHub {
|
|
|
98
93
|
await this.forgetSpentDeadline(pending)
|
|
99
94
|
}
|
|
100
95
|
|
|
96
|
+
/** Record which turn the alarm is for, then set the alarm. One deadline is pending at a time. */
|
|
97
|
+
private async arm(matchId: string, turn: number, dueAtMs: number): Promise<void> {
|
|
98
|
+
await this.state.storage.put<PendingDeadline>(DEADLINE_KEY, { matchId, turn })
|
|
99
|
+
await this.state.storage.setAlarm(dueAtMs)
|
|
100
|
+
}
|
|
101
|
+
|
|
101
102
|
/**
|
|
102
103
|
* Drop the pending deadline once nothing is left to fire for. A seal on readiness, a finished
|
|
103
104
|
* match and a match retention deleted all leave the alarm armed for a turn that is no longer
|
|
@@ -108,10 +109,9 @@ export class MatchHub {
|
|
|
108
109
|
private async forgetSpentDeadline(fired: PendingDeadline): Promise<void> {
|
|
109
110
|
const current = await this.state.storage.get<PendingDeadline>(DEADLINE_KEY)
|
|
110
111
|
if (!current || current.matchId !== fired.matchId || current.turn !== fired.turn) return
|
|
111
|
-
const storage = createSqliteStorage(drizzle(this.env.DB, { schema: sqliteSchema }))
|
|
112
112
|
const [match, turn] = await Promise.all([
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
this.repositories.matches.get(fired.matchId),
|
|
114
|
+
this.repositories.turns.get(fired.matchId, fired.turn),
|
|
115
115
|
])
|
|
116
116
|
const stillDue =
|
|
117
117
|
match?.status === 'running' && turn?.status === 'open' && turn.deadlineAt !== null
|
package/src/kernel.ts
CHANGED
|
@@ -30,6 +30,12 @@ const DAY_MS = 24 * 60 * 60 * 1000
|
|
|
30
30
|
const DEFAULT_RETENTION_DAYS = 30
|
|
31
31
|
const DEFAULT_ABANDONED_RETENTION_DAYS = 90
|
|
32
32
|
|
|
33
|
+
/** A non-negative whole number from an environment variable; anything else reads as `fallback`. */
|
|
34
|
+
function wholeNumber(raw: string | undefined, fallback: number): number {
|
|
35
|
+
const value = Number(raw ?? fallback)
|
|
36
|
+
return Number.isInteger(value) && value >= 0 ? value : fallback
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
export const HUB_PATHS = {
|
|
34
40
|
notify: '/notify',
|
|
35
41
|
schedule: '/schedule',
|
|
@@ -110,10 +116,6 @@ export function buildKernel(
|
|
|
110
116
|
body: { ...input, dueAt: input.dueAt.toISOString() },
|
|
111
117
|
}),
|
|
112
118
|
}
|
|
113
|
-
const days = (raw: string | undefined, fallback: number): number => {
|
|
114
|
-
const value = Number(raw ?? fallback)
|
|
115
|
-
return Number.isInteger(value) && value >= 0 ? value : fallback
|
|
116
|
-
}
|
|
117
119
|
return createKernel(
|
|
118
120
|
{
|
|
119
121
|
storage,
|
|
@@ -125,13 +127,13 @@ export function buildKernel(
|
|
|
125
127
|
},
|
|
126
128
|
{
|
|
127
129
|
retention: {
|
|
128
|
-
maxAgeMs:
|
|
130
|
+
maxAgeMs: wholeNumber(env.RETENTION_DAYS, DEFAULT_RETENTION_DAYS) * DAY_MS,
|
|
129
131
|
abandonedLiveMaxAgeMs:
|
|
130
|
-
|
|
132
|
+
wholeNumber(env.ABANDONED_RETENTION_DAYS, DEFAULT_ABANDONED_RETENTION_DAYS) * DAY_MS,
|
|
131
133
|
// Twice the abandoned window, and without its roster test, which never collects an untimed
|
|
132
134
|
// match whose players' clients died without a `leave`.
|
|
133
135
|
silentLiveMaxAgeMs:
|
|
134
|
-
|
|
136
|
+
wholeNumber(env.ABANDONED_RETENTION_DAYS, DEFAULT_ABANDONED_RETENTION_DAYS) * 2 * DAY_MS,
|
|
135
137
|
batchSize: 50,
|
|
136
138
|
},
|
|
137
139
|
},
|
|
@@ -153,10 +155,6 @@ export function buildBugReports(env: Env): BugReportService | undefined {
|
|
|
153
155
|
if (!env.BUG_DB) return undefined
|
|
154
156
|
const repository = createBugReportRepository(drizzle(env.BUG_DB, { schema: bugReportSchema }))
|
|
155
157
|
const blobs = env.BUG_BLOBS ? createR2BlobStore(env.BUG_BLOBS) : undefined
|
|
156
|
-
const number = (raw: string | undefined, fallback: number): number => {
|
|
157
|
-
const value = Number(raw ?? fallback)
|
|
158
|
-
return Number.isInteger(value) && value >= 0 ? value : fallback
|
|
159
|
-
}
|
|
160
158
|
return createBugReportService({
|
|
161
159
|
repository,
|
|
162
160
|
clock: { now: () => new Date() },
|
|
@@ -164,14 +162,14 @@ export function buildBugReports(env: Env): BugReportService | undefined {
|
|
|
164
162
|
retention: {
|
|
165
163
|
...DEFAULT_BUG_REPORT_RETENTION,
|
|
166
164
|
dailyStateBytes:
|
|
167
|
-
|
|
165
|
+
wholeNumber(
|
|
168
166
|
env.BUG_REPORT_DAILY_STATE_MB,
|
|
169
167
|
DEFAULT_BUG_REPORT_RETENTION.dailyStateBytes / (1024 * 1024),
|
|
170
168
|
) *
|
|
171
169
|
1024 *
|
|
172
170
|
1024,
|
|
173
171
|
maxAgeMs:
|
|
174
|
-
|
|
172
|
+
wholeNumber(env.BUG_REPORT_RETENTION_DAYS, DEFAULT_BUG_REPORT_RETENTION.maxAgeMs / DAY_MS) *
|
|
175
173
|
DAY_MS,
|
|
176
174
|
},
|
|
177
175
|
...(blobs ? { blobs } : {}),
|