testing generic deploy

This commit is contained in:
Devin Zuczek
2026-06-29 21:52:50 -04:00
parent 3affabd280
commit 69b89e2dc4
79 changed files with 399 additions and 307 deletions
+6 -9
View File
@@ -1,13 +1,11 @@
# notify
Notifications Worker served at `notify.rec.djdevin.net`. Ported from the C#
`NotifyController` / `NotificationsHub` / `NotificationService`, which host a
SignalR hub at `/hub/v1`.
Notifications Worker served on the `notify` subdomain. Hosts a SignalR hub at
`/hub/v1`.
The hub is implemented as a **Durable Object** (`NotificationsHub`) speaking the
SignalR JSON Hub Protocol over a hibernatable WebSocket. A single global DO
instance plays the role of the C# static dictionaries (one shared process across
all connections).
instance holds the shared hub state (one process across all connections).
## Endpoints
@@ -27,8 +25,7 @@ all connections).
1. Client `POST /hub/v1/negotiate`, then opens a WebSocket to `/hub/v1?id=<token>`.
2. Handshake: client sends `{"protocol":"json","version":1}␞`, server replies
`{}␞` (`␞` = record separator `0x1e`).
3. Server immediately sends the `OnConnect` invocation (mirrors the C#
`OnConnectedAsync`).
3. Server immediately sends the `OnConnect` invocation on connect.
4. Client→server invocations:
- `SubscribeToPlayers({ playerIds })` — replaces this connection's
subscriptions and flushes any queued notifications for those players.
@@ -42,11 +39,11 @@ all connections).
Held in the DO's SQLite so it survives hibernation:
- `subscriptions(connectionId, playerId)` — serves both the connection→players
and player→connections lookups from the C#.
and player→connections lookups.
- `pending(id, playerId, payload)` — per-player queue delivered once the player
subscribes.
A connection's rows are removed on `webSocketClose` (the C# `OnDisconnected`).
A connection's rows are removed on `webSocketClose`.
## TODO before production
+6 -7
View File
@@ -3,9 +3,8 @@ import { DurableObject } from 'cloudflare:workers'
import type { Env } from './context'
/**
* Durable Object hosting the SignalR notifications hub, ported from the C#
* `NotificationsHub` + `NotificationService`. A single global instance plays the
* role of the C# static dictionaries (one process, shared across connections).
* Durable Object hosting the SignalR notifications hub. A single global instance
* holds the shared hub state (one process, shared across connections).
*
* It speaks the SignalR JSON Hub Protocol over a hibernatable WebSocket:
* 1. negotiate happens in the worker; the client then opens a WS to `/hub/v1`.
@@ -15,7 +14,7 @@ import type { Env } from './context'
*
* Connection/subscription state lives in SQLite so it survives hibernation:
* - `subscriptions(connectionId, playerId)` — both the connection→players and
* (queried the other way) the player→connections maps from the C#.
* (queried the other way) the player→connections maps.
* - `pending(id, playerId, payload)` — the per-player queue delivered once a
* player is subscribed.
*/
@@ -136,7 +135,7 @@ export class NotificationsHub extends DurableObject<Env> {
state.handshakeDone = true
ws.serializeAttachment(state)
// C# OnConnectedAsync sends "OnConnect" to the caller after connecting.
// Send "OnConnect" to the caller after connecting.
ws.send(this.invocation('OnConnect', []))
}
@@ -275,8 +274,8 @@ export class NotificationsHub extends DurableObject<Env> {
// ---- Helpers -------------------------------------------------------------
/**
* Build the `Notification` argument: a JSON string `{ Id, Msg }`, matching the
* C# `SendToConnection` (null values are dropped from `Msg`).
* Build the `Notification` argument: a JSON string `{ Id, Msg }`
* (null values are dropped from `Msg`).
*/
private buildNotificationPayload(
notificationType: number,
+5 -6
View File
@@ -8,15 +8,14 @@ import { NotificationsHub } from './notifications-hub'
import type { App } from './context'
/**
* Ported from the C# `NotifyController`, which maps a SignalR hub at `/hub/v1`
* (see `NotificationsHub` / `NotificationService`). The hub itself — WebSocket
* transport, the SignalR JSON Hub Protocol, and the shared connection state —
* Maps a SignalR hub at `/hub/v1`. The hub itself — WebSocket transport, the
* SignalR JSON Hub Protocol, and the shared connection state —
* lives in the `NotificationsHub` Durable Object; this worker handles the
* SignalR negotiate handshake, forwards the WebSocket upgrade to the DO, and
* exposes internal send/broadcast endpoints for other workers.
*/
/** The hub state is global in the C# (static dictionaries) → one DO instance. */
/** The hub state is global → one DO instance. */
const HUB_INSTANCE = 'global'
const app = new Hono<App>()
@@ -57,8 +56,8 @@ const app = new Hono<App>()
})
// ---- Internal service-to-service send/broadcast --------------------------
// Lets other workers push notifications, the way the C# controllers called
// the shared NotificationService. TODO: protect these before production.
// Lets other workers push notifications through the shared hub.
// TODO: protect these before production.
.post('/internal/notify', async (c) => {
const body = await c.req
.json<{ playerId?: number; notificationType?: number; data?: Record<string, unknown> }>()
+2 -2
View File
@@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest'
import '../../notify.app'
const ORIGIN = 'https://notify.rec.djdevin.net'
const ORIGIN = 'https://example.com'
const RS = '\u001e'
interface HubRecord {
@@ -58,7 +58,7 @@ async function connect(
})
}
// Handshake, then the C# OnConnect callback.
// Handshake, then the OnConnect callback.
ws.send(`{"protocol":"json","version":1}${RS}`)
await waitFor((r) => r.type === 1 && r.target === 'OnConnect')
+1 -1
View File
@@ -6,7 +6,7 @@
"compatibility_flags": ["nodejs_compat"],
"routes": [
{
"pattern": "notify.rec.djdevin.net",
"pattern": "notify.rec.example.com",
"custom_domain": true
}
],