@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaos-overlords/worker",
3
- "version": "0.8.2",
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.8.2",
51
- "@chaos-overlords/contracts": "0.8.2",
52
- "@chaos-overlords/kernel": "0.8.2",
53
- "@chaos-overlords/server": "0.8.2",
54
- "@chaos-overlords/storage": "0.8.2",
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.8.2",
60
- "@chaos-overlords/conformance": "0.8.2",
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 { isDomainError, type PersistedEvent } from '@chaos-overlords/kernel'
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
- const storage = createSqliteStorage(drizzle(env.DB, { schema: sqliteSchema }))
29
- this.hub = new LocalEventHub(storage.events, DEFAULT_SERVER_CONFIG.sseHeartbeatMs)
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.state.storage.put<PendingDeadline>(DEADLINE_KEY, {
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
- storage.matches.get(fired.matchId),
114
- storage.turns.get(fired.matchId, fired.turn),
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: days(env.RETENTION_DAYS, DEFAULT_RETENTION_DAYS) * DAY_MS,
130
+ maxAgeMs: wholeNumber(env.RETENTION_DAYS, DEFAULT_RETENTION_DAYS) * DAY_MS,
129
131
  abandonedLiveMaxAgeMs:
130
- days(env.ABANDONED_RETENTION_DAYS, DEFAULT_ABANDONED_RETENTION_DAYS) * DAY_MS,
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
- days(env.ABANDONED_RETENTION_DAYS, DEFAULT_ABANDONED_RETENTION_DAYS) * 2 * DAY_MS,
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
- number(
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
- number(env.BUG_REPORT_RETENTION_DAYS, DEFAULT_BUG_REPORT_RETENTION.maxAgeMs / DAY_MS) *
172
+ wholeNumber(env.BUG_REPORT_RETENTION_DAYS, DEFAULT_BUG_REPORT_RETENTION.maxAgeMs / DAY_MS) *
175
173
  DAY_MS,
176
174
  },
177
175
  ...(blobs ? { blobs } : {}),