-
Notifications
You must be signed in to change notification settings - Fork 404
Build the session runtime from platform ports instead of DurableObjectState (COL-83) #1715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; | ||
| import type { SqlDatabase } from "../db/sql-database"; | ||
| import type { Logger } from "../logger"; | ||
| import { createDurableObjectSessionPlatform } from "./session-platform"; | ||
|
|
||
| /** Stand-in for the Workers runtime's request/response pair. */ | ||
| class FakeRequestResponsePair { | ||
| constructor( | ||
| readonly request: string, | ||
| readonly response: string | ||
| ) {} | ||
| } | ||
|
|
||
| function createFakeState() { | ||
| const storage = { | ||
| sql: { exec: vi.fn() }, | ||
| transactionSync: vi.fn(<T>(closure: () => T): T => closure()), | ||
| getAlarm: vi.fn(async () => null), | ||
| setAlarm: vi.fn(async () => {}), | ||
| deleteAlarm: vi.fn(async () => {}), | ||
| }; | ||
| const calls = { | ||
| id: { toString: () => "do-id" }, | ||
| storage, | ||
| acceptWebSocket: vi.fn(), | ||
| getTags: vi.fn(() => ["sandbox", "sid:sb-1"]), | ||
| getWebSockets: vi.fn(() => []), | ||
| setWebSocketAutoResponse: vi.fn(), | ||
| waitUntil: vi.fn(), | ||
| }; | ||
| return { state: calls as unknown as DurableObjectState, storage, calls }; | ||
| } | ||
|
|
||
| describe("createDurableObjectSessionPlatform", () => { | ||
| beforeEach(() => { | ||
| vi.stubGlobal("WebSocketRequestResponsePair", FakeRequestResponsePair); | ||
| }); | ||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it("exposes the object's id, SQL, transaction, alarm store, and db", () => { | ||
| const { state, storage } = createFakeState(); | ||
| const db = {} as SqlDatabase; | ||
|
|
||
| const platform = createDurableObjectSessionPlatform(state, db); | ||
|
|
||
| expect(platform.id).toBe("do-id"); | ||
| expect(platform.sql).toBe(storage.sql); | ||
| expect(platform.db).toBe(db); | ||
| expect(platform.alarmStore).toBe(storage); | ||
| expect(platform.transactionSync(() => 42)).toBe(42); | ||
| expect(storage.transactionSync).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it("delegates socket acceptance, tags, and enumeration, passing the tag filter through", () => { | ||
| const { state, calls } = createFakeState(); | ||
| const ws = {} as WebSocket; | ||
|
|
||
| const platform = createDurableObjectSessionPlatform(state, null); | ||
| platform.sockets.accept(ws, ["sandbox", "sid:sb-1"]); | ||
| platform.sockets.all(); | ||
| platform.sockets.all("sandbox"); | ||
|
|
||
| expect(calls.acceptWebSocket).toHaveBeenCalledWith(ws, ["sandbox", "sid:sb-1"]); | ||
| expect(platform.sockets.tags(ws)).toEqual(["sandbox", "sid:sb-1"]); | ||
| expect(calls.getTags).toHaveBeenCalledWith(ws); | ||
| expect(calls.getWebSockets.mock.calls).toEqual([[undefined], ["sandbox"]]); | ||
| }); | ||
|
|
||
| it("installs the auto-response as a request/response pair", () => { | ||
| const { state, calls } = createFakeState(); | ||
|
|
||
| const platform = createDurableObjectSessionPlatform(state, null); | ||
| platform.sockets.setAutoResponse('{"type":"ping"}', '{"type":"pong"}'); | ||
|
|
||
| expect(calls.setWebSocketAutoResponse).toHaveBeenCalledTimes(1); | ||
| const pair = calls.setWebSocketAutoResponse.mock.calls[0][0] as FakeRequestResponsePair; | ||
| expect(pair).toBeInstanceOf(FakeRequestResponsePair); | ||
| expect(pair.request).toBe('{"type":"ping"}'); | ||
| expect(pair.response).toBe('{"type":"pong"}'); | ||
| }); | ||
|
|
||
| it("builds background tasks over the object's event lifetime that report to the given logger", async () => { | ||
| const { state, calls } = createFakeState(); | ||
| const logger = { error: vi.fn() } as unknown as Logger; | ||
|
|
||
| const platform = createDurableObjectSessionPlatform(state, null); | ||
| platform.createBackgroundTasks(logger).submit(() => Promise.reject(new Error("boom")), { | ||
| name: "session.task", | ||
| }); | ||
|
|
||
| expect(calls.waitUntil).toHaveBeenCalledTimes(1); | ||
| await calls.waitUntil.mock.calls[0][0]; | ||
| expect(logger.error).toHaveBeenCalledWith( | ||
| "background_task.failed", | ||
| expect.objectContaining({ task_name: "session.task" }) | ||
| ); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import type { SqlDatabase } from "../db/sql-database"; | ||
| import type { SessionPlatform } from "../session/platform"; | ||
| import { createCloudflareBackgroundTasks } from "./background-tasks"; | ||
|
|
||
| /** | ||
| * A Durable Object's storage, hibernatable sockets, alarm, and event lifetime | ||
| * as the session platform. | ||
| */ | ||
| export function createDurableObjectSessionPlatform( | ||
| ctx: DurableObjectState, | ||
| db: SqlDatabase | null | ||
| ): SessionPlatform { | ||
| return { | ||
| id: ctx.id.toString(), | ||
| sql: ctx.storage.sql, | ||
| transactionSync: <T>(closure: () => T): T => ctx.storage.transactionSync(closure), | ||
| db, | ||
| alarmStore: ctx.storage, | ||
| sockets: { | ||
| accept: (ws, tags) => ctx.acceptWebSocket(ws, tags), | ||
| tags: (ws) => ctx.getTags(ws), | ||
| all: (tag) => ctx.getWebSockets(tag), | ||
| // Hibernation-level auto-response: matched by the runtime without | ||
| // waking the object. | ||
| setAutoResponse: (request, response) => | ||
| ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(request, response)), | ||
| }, | ||
| createBackgroundTasks: (log) => createCloudflareBackgroundTasks(ctx, log), | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /** | ||
| * The platform surface one session runtime is built over: what a host must | ||
| * supply for `createSessionRuntime` to assemble the collaborator graph. The | ||
| * Cloudflare adapter is `createDurableObjectSessionPlatform` | ||
| * (cloudflare/session-platform.ts); a Node host supplies the same record from | ||
| * its own storage, sockets, and process facilities. | ||
| */ | ||
|
|
||
| import type { Logger } from "../logger"; | ||
| import type { BackgroundTasks } from "../platform-ports"; | ||
| import type { SqlDatabase } from "../db/sql-database"; | ||
| import type { AlarmScheduleStore } from "./alarm/scheduler"; | ||
| import type { SqlStorage, TransactionSync } from "./sql-storage"; | ||
|
|
||
| /** Host socket operations the session's connection registry is built over. */ | ||
| export interface SocketPlatform { | ||
| /** Adopt `ws` into the runtime, tagged so its identity survives a restart. */ | ||
| accept(ws: WebSocket, tags: string[]): void; | ||
| /** The tags `ws` was accepted with. */ | ||
| tags(ws: WebSocket): string[]; | ||
| /** Every accepted socket, or only those carrying `tag`. */ | ||
| all(tag?: string): WebSocket[]; | ||
| /** | ||
| * Answer `request` frames with `response` at the platform level, without | ||
| * waking the runtime. | ||
| */ | ||
| setAutoResponse(request: string, response: string): void; | ||
| } | ||
|
|
||
| export interface SessionPlatform { | ||
| /** | ||
| * The host's identity for this runtime. It stands in for the session id | ||
| * until `init` writes the session row. | ||
| */ | ||
| id: string; | ||
| /** The session's own SQLite store. */ | ||
| sql: SqlStorage; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [deep review] This splits one atomic storage capability into independently constructible
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in a06e9b9 for the transactional half: |
||
| /** Run `closure` atomically against `sql`. */ | ||
| transactionSync: TransactionSync; | ||
| /** The global store, or null when the deployment has none bound. */ | ||
| db: SqlDatabase | null; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [deep review]
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in a06e9b9 at the boundary and in the composition root: |
||
| /** The runtime's single scheduled wake-up. */ | ||
| alarmStore: AlarmScheduleStore; | ||
| sockets: SocketPlatform; | ||
| /** | ||
| * Build the deferred-work port for this runtime. Takes the session-scoped | ||
| * logger so failures of background work are attributed to the session. | ||
| */ | ||
| createBackgroundTasks(log: Logger): BackgroundTasks; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.