mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
testing generic deploy
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> }>()
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "notify.rec.djdevin.net",
|
||||
"pattern": "notify.rec.example.com",
|
||||
"custom_domain": true
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user