@chaos-overlords/worker 0.7.1 → 0.8.1
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 +27 -1
- package/src/env.ts +1 -0
- package/src/index.ts +10 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaos-overlords/worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
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.8.1",
|
|
51
|
+
"@chaos-overlords/contracts": "0.8.1",
|
|
52
|
+
"@chaos-overlords/kernel": "0.8.1",
|
|
53
|
+
"@chaos-overlords/server": "0.8.1",
|
|
54
|
+
"@chaos-overlords/storage": "0.8.1",
|
|
55
55
|
"drizzle-orm": "^0.45.2",
|
|
56
56
|
"hono": "^4.13.7"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@chaos-overlords/client": "0.
|
|
60
|
-
"@chaos-overlords/conformance": "0.
|
|
59
|
+
"@chaos-overlords/client": "0.8.1",
|
|
60
|
+
"@chaos-overlords/conformance": "0.8.1",
|
|
61
61
|
"@cloudflare/vitest-pool-workers": "^0.22.0",
|
|
62
62
|
"@cloudflare/workers-types": "^5.20260910.1",
|
|
63
63
|
"typescript": "^5.9.3",
|
package/src/MatchHub.ts
CHANGED
|
@@ -57,7 +57,10 @@ export class MatchHub {
|
|
|
57
57
|
// reach. A bare status crosses back instead and the Worker rethrows it as the domain
|
|
58
58
|
// error, so the envelope still has exactly one producer.
|
|
59
59
|
if (!isDomainError(error)) throw error
|
|
60
|
-
return new Response(null, {
|
|
60
|
+
return new Response(null, {
|
|
61
|
+
status: 429,
|
|
62
|
+
headers: { 'X-Stream-Refusal': String(error.details?.scope ?? 'match') },
|
|
63
|
+
})
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
case HUB_PATHS.disconnect: {
|
|
@@ -89,5 +92,28 @@ export class MatchHub {
|
|
|
89
92
|
})
|
|
90
93
|
const sealed = await kernel.turns.trySeal(pending.matchId, pending.turn, 'deadline')
|
|
91
94
|
workerLogger.info('deadline alarm handled', { ...pending, sealed })
|
|
95
|
+
await this.forgetSpentDeadline(pending)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Drop the pending deadline once nothing is left to fire for. A seal on readiness, a finished
|
|
100
|
+
* match and a match retention deleted all leave the alarm armed for a turn that is no longer
|
|
101
|
+
* open; each such alarm builds a kernel and reads the match for nothing, and the key kept every
|
|
102
|
+
* timed match's object in storage forever. A seal that just ran has already replaced the key
|
|
103
|
+
* with the next turn's deadline, which is why the key is re-read rather than deleted outright.
|
|
104
|
+
*/
|
|
105
|
+
private async forgetSpentDeadline(fired: PendingDeadline): Promise<void> {
|
|
106
|
+
const current = await this.state.storage.get<PendingDeadline>(DEADLINE_KEY)
|
|
107
|
+
if (!current || current.matchId !== fired.matchId || current.turn !== fired.turn) return
|
|
108
|
+
const storage = createSqliteStorage(drizzle(this.env.DB, { schema: sqliteSchema }))
|
|
109
|
+
const [match, turn] = await Promise.all([
|
|
110
|
+
storage.matches.get(fired.matchId),
|
|
111
|
+
storage.turns.get(fired.matchId, fired.turn),
|
|
112
|
+
])
|
|
113
|
+
const stillDue =
|
|
114
|
+
match?.status === 'running' && turn?.status === 'open' && turn.deadlineAt !== null
|
|
115
|
+
if (stillDue) return
|
|
116
|
+
await this.state.storage.delete(DEADLINE_KEY)
|
|
117
|
+
await this.state.storage.deleteAlarm()
|
|
92
118
|
}
|
|
93
119
|
}
|
package/src/env.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface Env {
|
|
|
21
21
|
* `BUG_REPORT_LIMITS.inlineStateBytes` are kept.
|
|
22
22
|
*/
|
|
23
23
|
BUG_BLOBS?: R2Bucket
|
|
24
|
+
/** `"false"` stops serving `GET /api/v1/matches`; anything else (unset included) serves it. */
|
|
24
25
|
PUBLIC_LISTING?: string
|
|
25
26
|
RATE_LIMIT_PER_MINUTE?: string
|
|
26
27
|
MEMBER_RATE_LIMIT_PER_MINUTE?: string
|
package/src/index.ts
CHANGED
|
@@ -65,9 +65,13 @@ export function buildContainer(env: Env): ServerContainer {
|
|
|
65
65
|
// The object refuses an over-cap stream with a bare 429; turning it back into the domain
|
|
66
66
|
// error here is what gets the caller the same envelope every other refusal has.
|
|
67
67
|
if (response.status === 429) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const scope = response.headers.get('X-Stream-Refusal') === 'process' ? 'process' : 'match'
|
|
69
|
+
throw new RateLimitedError(
|
|
70
|
+
scope === 'process'
|
|
71
|
+
? 'This server is holding as many event streams as it can'
|
|
72
|
+
: 'This match is holding as many event streams as it can',
|
|
73
|
+
{ reason: 'too_many_streams', scope },
|
|
74
|
+
)
|
|
71
75
|
}
|
|
72
76
|
return new Response(response.body as ReadableStream<Uint8Array> | null, {
|
|
73
77
|
status: response.status,
|
|
@@ -84,7 +88,9 @@ export function buildContainer(env: Env): ServerContainer {
|
|
|
84
88
|
DEFAULT_RATE_LIMITS.bugReportPerMinute,
|
|
85
89
|
),
|
|
86
90
|
},
|
|
87
|
-
|
|
91
|
+
// Listing is on unless a deployment turns it off: an unset var means the Browse screen works,
|
|
92
|
+
// rather than every client being told the server lists nothing.
|
|
93
|
+
config: { ...DEFAULT_SERVER_CONFIG, publicListing: env.PUBLIC_LISTING !== 'false' },
|
|
88
94
|
// `CF-Connecting-IP` is authoritative here and only here: Cloudflare sets it on every request
|
|
89
95
|
// that reaches a Worker and a client cannot forge it through the edge. Off Cloudflare it is a
|
|
90
96
|
// header anyone can write, which is why the default resolver ignores it unless told otherwise.
|