From d589a16d87a40a7576cbd733a28b2734bf293670 Mon Sep 17 00:00:00 2001 From: Krasilnikov Roman Date: Fri, 7 Jun 2024 13:19:43 +0300 Subject: [PATCH 1/5] WIP: RPC --- src/lib/guards.ts | 3 +++ src/lib/patcher.ts | 7 +++++ src/lib/rpc.ts | 39 ++++++++++++++++++++++++++++ src/lib/testing/js/js-test-runner.ts | 6 ++--- 4 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 src/lib/guards.ts create mode 100644 src/lib/patcher.ts create mode 100644 src/lib/rpc.ts diff --git a/src/lib/guards.ts b/src/lib/guards.ts new file mode 100644 index 0000000..43788a8 --- /dev/null +++ b/src/lib/guards.ts @@ -0,0 +1,3 @@ +export function neverError(value: never, message: string) { + return new Error(`${message}: ${value}`); +} diff --git a/src/lib/patcher.ts b/src/lib/patcher.ts new file mode 100644 index 0000000..12d1ef3 --- /dev/null +++ b/src/lib/patcher.ts @@ -0,0 +1,7 @@ +export function patch(obj: T, key: K, value: T[K]) { + const originalValue = obj[key]; + obj[key] = value; + return () => { + obj[key] = originalValue; + }; +} diff --git a/src/lib/rpc.ts b/src/lib/rpc.ts new file mode 100644 index 0000000..4f8d206 --- /dev/null +++ b/src/lib/rpc.ts @@ -0,0 +1,39 @@ +import { neverError } from './guards'; + +export enum MessageType { + Request = "request", +} + +export interface AbstractMessage { + type: T; +} + +export interface RequestMessage + extends AbstractMessage { + id: number; + payload: T; +} + +export type Message = RequestMessage; + +export interface HandlerCall { + handler: H; + args: I; +} + +export type HandlerCalls any>> = { + [K in keyof H & string]: HandlerCall>; +}[keyof H & string]; + +export function defineServer any>>( + handlers: H +) { + function handleMessage({ data }: MessageEvent>>) { + switch (data.type) { + case MessageType.Request: + return + default: + throw neverError(data.type, `Unknown message type`); + } + } +} diff --git a/src/lib/testing/js/js-test-runner.ts b/src/lib/testing/js/js-test-runner.ts index 273a823..3da6bf7 100644 --- a/src/lib/testing/js/js-test-runner.ts +++ b/src/lib/testing/js/js-test-runner.ts @@ -1,4 +1,5 @@ import { redirect, type Logger } from "@/lib/logger"; +import { patch } from "@/lib/patcher"; import type { TestRunner } from "../model"; @@ -19,13 +20,12 @@ export abstract class JsTestRunner implements TestRunner { async run(input: I): Promise { const transformedCode = this.transformCode(this.code); - const originalConsole = globalThis.console; - globalThis.console = this.patchedConsole + const recover = patch(globalThis, "console", this.patchedConsole); try { const m = await import(/* @vite-ignore */ transformedCode); return this.executeTest(m, input); } finally { - globalThis.console = originalConsole; + recover(); } } From ac2b837026e687c0263569f10c72f368e0fc0a2e Mon Sep 17 00:00:00 2001 From: Krasilnikov Roman Date: Mon, 10 Jun 2024 01:26:18 +0300 Subject: [PATCH 2/5] WIP: Connection --- src/lib/actor/actor.ts | 140 +++++++++++++++++++++++++++++ src/lib/actor/connection.ts | 4 + src/lib/actor/index.ts | 2 + src/lib/actor/worker-connection.ts | 30 +++++++ src/lib/context.ts | 41 +++++++++ src/lib/result.ts | 27 ++++++ src/lib/rpc.ts | 39 -------- src/lib/type.ts | 3 + 8 files changed, 247 insertions(+), 39 deletions(-) create mode 100644 src/lib/actor/actor.ts create mode 100644 src/lib/actor/connection.ts create mode 100644 src/lib/actor/index.ts create mode 100644 src/lib/actor/worker-connection.ts create mode 100644 src/lib/context.ts create mode 100644 src/lib/result.ts delete mode 100644 src/lib/rpc.ts create mode 100644 src/lib/type.ts diff --git a/src/lib/actor/actor.ts b/src/lib/actor/actor.ts new file mode 100644 index 0000000..1a1d84d --- /dev/null +++ b/src/lib/actor/actor.ts @@ -0,0 +1,140 @@ +import type { AnyKey, Brand } from "@/lib/type"; +import { err, ok, type Result } from "@/lib/result"; +import { neverError } from "@/lib/guards"; + +import type { Connection } from "./connection"; + +export type ActorId = Brand<"ActorId">; + +export enum MessageType { + Event = "event", + Request = "request", + Response = "response", +} + +export interface AbstractMessage { + type: T; +} + +export interface EventMessage + extends AbstractMessage { + event: T; + payload: P; +} + +export type RequestId = Brand<"RequestId">; + +export interface RequestMessage + extends AbstractMessage { + id: RequestId; + request: T; + payload: P; +} + +export interface ResponseMessage + extends AbstractMessage { + id: RequestId; + result: R; +} + +export enum EventType { + Error = "error", + Cancel = "cancel", +} + +export interface AbstractEventMessage + extends AbstractMessage { + event: T; +} + +export interface ErrorEventMessage + extends AbstractEventMessage { + error: E; +} + +export interface CancelRequestEventMessage + extends AbstractEventMessage { + id: RequestId; +} + +export type Handlers = Record any>; + +type IncomingMessage = { + [K in keyof H]: + | RequestMessage[0]> + | EventMessage[0]>; +}[keyof H]; + +type OutgoingMessage = + | ResponseMessage, E>> + | ErrorEventMessage; + +export interface ActorConfig { + connection: Connection, OutgoingMessage>; + handlers: H; + caseError: (error: any) => E; +} + +export class Actor { + constructor(private readonly config: ActorConfig) {} + + start() { + const { connection } = this.config; + const handleMessage = this.handleMessage.bind(this); + connection.onMessage(handleMessage); + return () => { + connection.onMessage(handleMessage); + }; + } + + private async handleMessage(data: IncomingMessage) { + switch (data.type) { + case MessageType.Request: { + const { connection } = this.config; + const result = await this.handleRequest(data); + connection.send(result); + return; + } + case MessageType.Event: { + await this.handleEvent(data); + return; + } + default: + throw neverError(data, "Unknown message type"); + } + } + + private async handleEvent( + event: EventMessage[0]> + ) { + try { + await this.config.handlers[event.event](event.payload); + } catch (error) { + this.config.connection.send({ + type: MessageType.Event, + event: EventType.Error, + error: this.config.caseError(error), + }); + } + } + + private async handleRequest( + request: RequestMessage[0]> + ): Promise, E>>> { + const { handlers, caseError } = this.config; + try { + const result = await handlers[request.request](request.payload); + return { + type: MessageType.Response, + id: request.id, + result: ok(result), + }; + } catch (error) { + return { + type: MessageType.Response, + id: request.id, + result: err(caseError(error)), + }; + } + } +} diff --git a/src/lib/actor/connection.ts b/src/lib/actor/connection.ts new file mode 100644 index 0000000..3f56a1f --- /dev/null +++ b/src/lib/actor/connection.ts @@ -0,0 +1,4 @@ +export interface Connection { + onMessage: (handler: (message: Incoming) => void) => () => void; + send: (message: Outgoing) => void; +} diff --git a/src/lib/actor/index.ts b/src/lib/actor/index.ts new file mode 100644 index 0000000..bbfe941 --- /dev/null +++ b/src/lib/actor/index.ts @@ -0,0 +1,2 @@ +export * from './actor' +export * from './worker-connection' diff --git a/src/lib/actor/worker-connection.ts b/src/lib/actor/worker-connection.ts new file mode 100644 index 0000000..588d873 --- /dev/null +++ b/src/lib/actor/worker-connection.ts @@ -0,0 +1,30 @@ +import type { Connection } from "./connection"; + +export class WorkerConnection implements Connection { + private handlers = new Set<(message: I) => void>(); + + constructor(private readonly worker: Worker) {} + + start() { + const onMsg = (e: MessageEvent) => { + for (const handler of this.handlers) { + handler(e.data); + } + }; + this.worker.addEventListener("message", onMsg); + return () => { + this.worker.removeEventListener("message", onMsg); + }; + } + + send(message: O) { + this.worker.postMessage(message); + } + + onMessage(handler: (message: I) => void) { + this.handlers.add(handler); + return () => { + this.handlers.delete(handler); + }; + } +} diff --git a/src/lib/context.ts b/src/lib/context.ts new file mode 100644 index 0000000..3d9390b --- /dev/null +++ b/src/lib/context.ts @@ -0,0 +1,41 @@ +export interface Context { + signal: AbortSignal; + canceled: boolean; + cancel(): void; + onCancel(cb: () => void): () => void; +} + +export function root(): Context { + const ctrl = new AbortController(); + return { + get canceled() { + return ctrl.signal.aborted; + }, + get signal() { + return ctrl.signal; + }, + onCancel(cb) { + ctrl.signal.addEventListener("abort", cb); + return () => ctrl.signal.removeEventListener("abort", cb); + }, + cancel() { + ctrl.abort(); + }, + }; +} + +export function withCancel(ctx: Context): Context { + if (ctx.canceled) { + return ctx; + } + const leaf = root(); + const cancel = () => { + ctx.signal.removeEventListener("abort", cancel); + leaf.cancel(); + }; + ctx.signal.addEventListener("abort", cancel); + return { + ...leaf, + cancel, + }; +} diff --git a/src/lib/result.ts b/src/lib/result.ts new file mode 100644 index 0000000..8746f64 --- /dev/null +++ b/src/lib/result.ts @@ -0,0 +1,27 @@ +export interface Ok { + ok: true + value: T +} + +export interface Err { + ok: false + error: E +} + +export type Result = Ok | Err + +export function isOk(result: Result): result is Ok { + return result.ok +} + +export function isErr(result: Result): result is Err { + return !result.ok +} + +export function ok(value: T): Ok { + return { ok: true, value } +} + +export function err(error: E): Err { + return { ok: false, error } +} diff --git a/src/lib/rpc.ts b/src/lib/rpc.ts deleted file mode 100644 index 4f8d206..0000000 --- a/src/lib/rpc.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { neverError } from './guards'; - -export enum MessageType { - Request = "request", -} - -export interface AbstractMessage { - type: T; -} - -export interface RequestMessage - extends AbstractMessage { - id: number; - payload: T; -} - -export type Message = RequestMessage; - -export interface HandlerCall { - handler: H; - args: I; -} - -export type HandlerCalls any>> = { - [K in keyof H & string]: HandlerCall>; -}[keyof H & string]; - -export function defineServer any>>( - handlers: H -) { - function handleMessage({ data }: MessageEvent>>) { - switch (data.type) { - case MessageType.Request: - return - default: - throw neverError(data.type, `Unknown message type`); - } - } -} diff --git a/src/lib/type.ts b/src/lib/type.ts new file mode 100644 index 0000000..d2b797a --- /dev/null +++ b/src/lib/type.ts @@ -0,0 +1,3 @@ +export type Brand = Base & { __brand: Name } + +export type AnyKey = keyof any; From 31f8bd8d05ff25aaff1ade6350009a7120009abf Mon Sep 17 00:00:00 2001 From: Krasilnikov Roman Date: Mon, 10 Jun 2024 03:09:38 +0300 Subject: [PATCH 3/5] WIP: Testing actor --- src/adapters/testing-actor.ts | 72 +++++++++++++++++++++++ src/lib/actor/actor.ts | 93 ++++++------------------------ src/lib/actor/connection.ts | 4 -- src/lib/actor/model.ts | 53 +++++++++++++++++ src/lib/actor/remote.ts | 33 +++++++++++ src/lib/actor/worker-connection.ts | 2 +- 6 files changed, 177 insertions(+), 80 deletions(-) create mode 100644 src/adapters/testing-actor.ts delete mode 100644 src/lib/actor/connection.ts create mode 100644 src/lib/actor/model.ts create mode 100644 src/lib/actor/remote.ts diff --git a/src/adapters/testing-actor.ts b/src/adapters/testing-actor.ts new file mode 100644 index 0000000..6242fa6 --- /dev/null +++ b/src/adapters/testing-actor.ts @@ -0,0 +1,72 @@ +import { Actor } from "@/lib/actor"; +import { + MessageType, + type Connection, + type EventMessage, + type IncomingMessage, + type OutgoingMessage, +} from "@/lib/actor/model"; +import type { TestData, TestRunner, TestRunnerFactory } from "@/lib/testing"; + +interface Handlers { + [key: string]: any; + init: (code: string) => Promise; + run: (data: TestData) => Promise; +} + +export interface WriteEventMessage extends EventMessage<"write", string> {} +export interface WritelnEventMessage extends EventMessage<"writeln", string> {} + +type LogEventMessage = WriteEventMessage | WritelnEventMessage; + +export class TestRunnerActor extends Actor, string> { + private runner: TestRunner | null = null; + + constructor( + connection: Connection< + IncomingMessage>, + OutgoingMessage, string> | LogEventMessage + >, + factory: TestRunnerFactory + ) { + super({ + connection, + handlers: { + init: async (code) => { + this.runner = await factory({ + code, + out: { + write(text) { + connection.send({ + type: MessageType.Event, + event: "write", + payload: text, + }); + }, + writeln(text) { + connection.send({ + type: MessageType.Event, + event: "writeln", + payload: text, + }); + }, + }, + }); + }, + run: (data) => { + if (!this.runner) { + const err = new Error("Test runner not initialized"); + connection.send({ + type: MessageType.Event, + event: "error", + payload: err.message, + }); + throw err; + } + return this.runner.run(data.input); + }, + }, + caseError: String, + }); + } +} diff --git a/src/lib/actor/actor.ts b/src/lib/actor/actor.ts index 1a1d84d..7b28d98 100644 --- a/src/lib/actor/actor.ts +++ b/src/lib/actor/actor.ts @@ -1,73 +1,16 @@ -import type { AnyKey, Brand } from "@/lib/type"; import { err, ok, type Result } from "@/lib/result"; import { neverError } from "@/lib/guards"; -import type { Connection } from "./connection"; - -export type ActorId = Brand<"ActorId">; - -export enum MessageType { - Event = "event", - Request = "request", - Response = "response", -} - -export interface AbstractMessage { - type: T; -} - -export interface EventMessage - extends AbstractMessage { - event: T; - payload: P; -} - -export type RequestId = Brand<"RequestId">; - -export interface RequestMessage - extends AbstractMessage { - id: RequestId; - request: T; - payload: P; -} - -export interface ResponseMessage - extends AbstractMessage { - id: RequestId; - result: R; -} - -export enum EventType { - Error = "error", - Cancel = "cancel", -} - -export interface AbstractEventMessage - extends AbstractMessage { - event: T; -} - -export interface ErrorEventMessage - extends AbstractEventMessage { - error: E; -} - -export interface CancelRequestEventMessage - extends AbstractEventMessage { - id: RequestId; -} - -export type Handlers = Record any>; - -type IncomingMessage = { - [K in keyof H]: - | RequestMessage[0]> - | EventMessage[0]>; -}[keyof H]; - -type OutgoingMessage = - | ResponseMessage, E>> - | ErrorEventMessage; +import { + MessageType, + type Connection, + type EventMessage, + type Handlers, + type IncomingMessage, + type OutgoingMessage, + type RequestMessage, + type ResponseMessage, +} from "./model"; export interface ActorConfig { connection: Connection, OutgoingMessage>; @@ -76,7 +19,7 @@ export interface ActorConfig { } export class Actor { - constructor(private readonly config: ActorConfig) {} + constructor(protected readonly config: ActorConfig) {} start() { const { connection } = this.config; @@ -87,20 +30,20 @@ export class Actor { }; } - private async handleMessage(data: IncomingMessage) { - switch (data.type) { + private async handleMessage(msg: IncomingMessage) { + switch (msg.type) { case MessageType.Request: { const { connection } = this.config; - const result = await this.handleRequest(data); + const result = await this.handleRequest(msg); connection.send(result); return; } case MessageType.Event: { - await this.handleEvent(data); + await this.handleEvent(msg); return; } default: - throw neverError(data, "Unknown message type"); + throw neverError(msg, "Unknown message type"); } } @@ -112,8 +55,8 @@ export class Actor { } catch (error) { this.config.connection.send({ type: MessageType.Event, - event: EventType.Error, - error: this.config.caseError(error), + event: "error", + payload: this.config.caseError(error), }); } } diff --git a/src/lib/actor/connection.ts b/src/lib/actor/connection.ts deleted file mode 100644 index 3f56a1f..0000000 --- a/src/lib/actor/connection.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface Connection { - onMessage: (handler: (message: Incoming) => void) => () => void; - send: (message: Outgoing) => void; -} diff --git a/src/lib/actor/model.ts b/src/lib/actor/model.ts new file mode 100644 index 0000000..d2a6216 --- /dev/null +++ b/src/lib/actor/model.ts @@ -0,0 +1,53 @@ +import type { AnyKey, Brand } from "@/lib/type"; +import type { Result } from '@/lib/result'; + +export enum MessageType { + Event = "event", + Request = "request", + Response = "response", +} + +export interface AbstractMessage { + type: T; +} + +export interface EventMessage + extends AbstractMessage { + event: T; + payload: P; +} + +export type RequestId = Brand<"RequestId", number>; + +export interface RequestMessage + extends AbstractMessage { + id: RequestId; + request: T; + payload: P; +} + +export interface ResponseMessage + extends AbstractMessage { + id: RequestId; + result: R; +} + +export interface Connection { + onMessage: (handler: (message: Incoming) => void) => () => void; + send: (message: Outgoing) => void; +} + +export type Handlers = Record any>; + +export type IncomingMessage = { + [K in keyof H]: + | RequestMessage[0]> + | EventMessage[0]>; +}[keyof H]; + +export interface ErrorEventMessage extends EventMessage<"error", E> {} + +export type OutgoingMessage = + | ResponseMessage, E>> + | ErrorEventMessage; + diff --git a/src/lib/actor/remote.ts b/src/lib/actor/remote.ts new file mode 100644 index 0000000..b2bbbc0 --- /dev/null +++ b/src/lib/actor/remote.ts @@ -0,0 +1,33 @@ +import { + MessageType, + type Connection, + type Handlers, + type IncomingMessage, + type RequestId, +} from "./model"; + +export function createRemote( + connection: Connection> +) { + let lastId = 0; + // const + return new Proxy( + {}, + { + get(_, prop) { + const request = prop as keyof H; + return (arg: Parameters[0]) => { + const id = lastId++ as RequestId; + connection.send({ + id: id, + request, + type: MessageType.Request, + payload: arg, + }); + }; + }, + } + ) as { + [K in keyof H]: (arg: Parameters[0]) => Promise>; + }; +} diff --git a/src/lib/actor/worker-connection.ts b/src/lib/actor/worker-connection.ts index 588d873..335f315 100644 --- a/src/lib/actor/worker-connection.ts +++ b/src/lib/actor/worker-connection.ts @@ -1,4 +1,4 @@ -import type { Connection } from "./connection"; +import type { Connection } from "./model"; export class WorkerConnection implements Connection { private handlers = new Set<(message: I) => void>(); From d48eb4c99c0e5f6cc68cd6f053768eefcf315aec Mon Sep 17 00:00:00 2001 From: Krasilnikov Roman Date: Mon, 10 Jun 2024 12:40:08 +0300 Subject: [PATCH 4/5] Bump deps --- bun.lockb | Bin 290668 -> 292895 bytes package.json | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lockb b/bun.lockb index 36ba9e201160fb6c4caabb76d4cb1abf599dbe7b..183f533c9f35be5b007c8b59b9f86438b3bbbac6 100755 GIT binary patch delta 10646 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 TT1LEXHK8MJW`6#_>Z|_&8I9)u delta 11226 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 beUZFnc2@l!Rx$rFxw=dmJ&j^tl`{St1Aw5F diff --git a/package.json b/package.json index 119712d..fad4ec2 100644 --- a/package.json +++ b/package.json @@ -17,17 +17,17 @@ "@php-wasm/web": "^0.7.20", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", - "astro": "^4.10.0", + "astro": "^4.10.1", "astro-icon": "^1.1.0", "fast-deep-equal": "^3.1.3", "monaco-editor": "^0.49.0", "monaco-vim": "^0.4.1", - "pyodide": "^0.26.0", + "pyodide": "^0.26.1", "tailwindcss": "^3.4.4", "typescript": "^5.4.5" }, "devDependencies": { - "@iconify-json/lucide": "^1.1.190", + "@iconify-json/lucide": "^1.1.191", "@iconify/svelte": "^4.0.2", "@tailwindcss/typography": "^0.5.13", "@types/color": "^3.0.6", From a2fda7cfe8a12f174718ab323f07dc51d42925e6 Mon Sep 17 00:00:00 2001 From: Krasilnikov Roman Date: Wed, 12 Jun 2024 13:11:42 +0300 Subject: [PATCH 5/5] Move tests execution to the workers --- src/adapters/testing-actor.ts | 147 ++++++++++++------ .../design-patterns/factory/editor.svelte | 43 ++--- .../design-patterns/factory/js/index.ts | 3 + .../factory/js/test-runners.ts | 29 ---- .../design-patterns/factory/js/worker.ts | 22 +++ .../design-patterns/factory/php/index.ts | 3 + .../php/{test-runners.ts => worker.ts} | 7 +- .../design-patterns/factory/python/index.ts | 3 + .../python/{test-runners.ts => worker.ts} | 9 +- .../factory/{js => ts}/code.ts | 0 .../design-patterns/factory/ts/index.ts | 3 + .../design-patterns/factory/ts/worker.ts | 22 +++ src/lib/actor/actor.ts | 28 ++-- src/lib/actor/index.ts | 2 + src/lib/actor/model.ts | 3 +- src/lib/actor/remote.ts | 73 ++++++++- src/lib/actor/worker-connection.ts | 10 +- 17 files changed, 282 insertions(+), 125 deletions(-) create mode 100644 src/content/design-patterns/factory/js/index.ts delete mode 100644 src/content/design-patterns/factory/js/test-runners.ts create mode 100644 src/content/design-patterns/factory/js/worker.ts create mode 100644 src/content/design-patterns/factory/php/index.ts rename src/content/design-patterns/factory/php/{test-runners.ts => worker.ts} (82%) create mode 100644 src/content/design-patterns/factory/python/index.ts rename src/content/design-patterns/factory/python/{test-runners.ts => worker.ts} (57%) rename src/content/design-patterns/factory/{js => ts}/code.ts (100%) create mode 100644 src/content/design-patterns/factory/ts/index.ts create mode 100644 src/content/design-patterns/factory/ts/worker.ts diff --git a/src/adapters/testing-actor.ts b/src/adapters/testing-actor.ts index 6242fa6..c187968 100644 --- a/src/adapters/testing-actor.ts +++ b/src/adapters/testing-actor.ts @@ -1,72 +1,123 @@ -import { Actor } from "@/lib/actor"; import { + Actor, MessageType, + WorkerConnection, + startRemote, type Connection, type EventMessage, type IncomingMessage, type OutgoingMessage, -} from "@/lib/actor/model"; -import type { TestData, TestRunner, TestRunnerFactory } from "@/lib/testing"; +} from "@/lib/actor"; +import { createLogger } from "@/lib/logger"; +import type { TestRunner, TestRunnerFactory } from "@/lib/testing"; interface Handlers { [key: string]: any; init: (code: string) => Promise; - run: (data: TestData) => Promise; + run: (data: I) => Promise; } -export interface WriteEventMessage extends EventMessage<"write", string> {} -export interface WritelnEventMessage extends EventMessage<"writeln", string> {} +type Incoming = IncomingMessage>; -type LogEventMessage = WriteEventMessage | WritelnEventMessage; +interface WriteEventMessage extends EventMessage<"write", string> {} +interface WritelnEventMessage extends EventMessage<"writeln", string> {} -export class TestRunnerActor extends Actor, string> { +type TestingActorEvent = WriteEventMessage | WritelnEventMessage; + +type Outgoing = OutgoingMessage, string> | TestingActorEvent; + +class TestRunnerActor extends Actor, string> { private runner: TestRunner | null = null; constructor( - connection: Connection< - IncomingMessage>, - OutgoingMessage, string> | LogEventMessage - >, + connection: Connection, Outgoing>, factory: TestRunnerFactory ) { - super({ - connection, - handlers: { - init: async (code) => { - this.runner = await factory({ - code, - out: { - write(text) { - connection.send({ - type: MessageType.Event, - event: "write", - payload: text, - }); - }, - writeln(text) { - connection.send({ - type: MessageType.Event, - event: "writeln", - payload: text, - }); - }, + const handlers: Handlers = { + init: async (code) => { + this.runner = await factory({ + code, + out: { + write(text) { + connection.send({ + type: MessageType.Event, + event: "write", + payload: text, + }); + }, + writeln(text) { + connection.send({ + type: MessageType.Event, + event: "writeln", + payload: text, + }); }, + }, + }); + }, + run: (input) => { + if (!this.runner) { + const err = new Error("Test runner not initialized"); + connection.send({ + type: MessageType.Event, + event: "error", + payload: err.message, }); - }, - run: (data) => { - if (!this.runner) { - const err = new Error("Test runner not initialized"); - connection.send({ - type: MessageType.Event, - event: "error", - payload: err.message, - }); - throw err; - } - return this.runner.run(data.input); - }, + throw err; + } + return this.runner.run(input); }, - caseError: String, - }); + }; + super(connection, handlers, String); } } + +export function startTestRunnerActor(factory: TestRunnerFactory) { + const connection = new WorkerConnection, Outgoing>( + self as unknown as Worker + ); + const actor = new TestRunnerActor(connection, factory); + const stopConnection = connection.start(); + const stopActor = actor.start(); + return () => { + stopActor(); + stopConnection(); + }; +} + +interface WorkerConstructor { + new (): Worker; +} + +export function makeRemoteTestRunnerFactory( + Worker: WorkerConstructor +): TestRunnerFactory { + return async ({ code, out }) => { + const worker = new Worker(); + const connection = new WorkerConnection, Incoming>( + worker + ); + const stopConnection = connection.start(); + const log = createLogger(out); + const remote = startRemote, string, TestingActorEvent>( + log, + connection, + { + error: (err) => log.error(err), + write: (text) => out.write(text), + writeln: (text) => out.writeln(text), + } + ); + await remote.init(code); + return { + async run(input) { + return remote.run(input); + }, + [Symbol.dispose]() { + remote[Symbol.dispose](); + stopConnection(); + worker.terminate(); + }, + }; + }; +} diff --git a/src/content/design-patterns/factory/editor.svelte b/src/content/design-patterns/factory/editor.svelte index d4cc810..79b62de 100644 --- a/src/content/design-patterns/factory/editor.svelte +++ b/src/content/design-patterns/factory/editor.svelte @@ -1,31 +1,32 @@ { - async executeTest(m: TestingModule, input: Input): Promise { - return m.payment(input.paymentSystem, input.base, input.amount); - } -} - -export const jsTestRunnerFactory = async ({ code, out }: TestRunnerConfig) => - new SimpleJsTestRunner(createLogger(out), code); - -class SimpleTsTestRunner extends TsTestRunner { - async executeTest(m: TestingModule, input: Input): Promise { - return m.payment(input.paymentSystem, input.base, input.amount); - } -} - -export const tsTestRunnerFactory = async ({ code, out }: TestRunnerConfig) => - new SimpleTsTestRunner(createLogger(out), code); diff --git a/src/content/design-patterns/factory/js/worker.ts b/src/content/design-patterns/factory/js/worker.ts new file mode 100644 index 0000000..a877eb3 --- /dev/null +++ b/src/content/design-patterns/factory/js/worker.ts @@ -0,0 +1,22 @@ +import { createLogger } from "@/lib/logger"; +import type { TestRunnerConfig } from "@/lib/testing"; +import { JsTestRunner } from "@/lib/testing/js"; +import { startTestRunnerActor } from "@/adapters/testing-actor"; + +import { type Input, type Output } from "../tests-data"; +import type { PaymentSystemType } from "../reference"; + +interface TestingModule { + payment(type: PaymentSystemType, base: number, amount: number): number; +} + +class SimpleJsTestRunner extends JsTestRunner { + async executeTest(m: TestingModule, input: Input): Promise { + return m.payment(input.paymentSystem, input.base, input.amount); + } +} + +startTestRunnerActor( + async ({ code, out }: TestRunnerConfig) => + new SimpleJsTestRunner(createLogger(out), code) +); diff --git a/src/content/design-patterns/factory/php/index.ts b/src/content/design-patterns/factory/php/index.ts new file mode 100644 index 0000000..729afca --- /dev/null +++ b/src/content/design-patterns/factory/php/index.ts @@ -0,0 +1,3 @@ +export { default as phpCode } from './code.php?raw' + +export { default as PhpWorker } from './worker?worker' diff --git a/src/content/design-patterns/factory/php/test-runners.ts b/src/content/design-patterns/factory/php/worker.ts similarity index 82% rename from src/content/design-patterns/factory/php/test-runners.ts rename to src/content/design-patterns/factory/php/worker.ts index f0365b6..c6ca3f5 100644 --- a/src/content/design-patterns/factory/php/test-runners.ts +++ b/src/content/design-patterns/factory/php/worker.ts @@ -4,6 +4,7 @@ import { PHPTestRunner, phpRuntimeFactory, } from "@/lib/testing/php"; +import { startTestRunnerActor } from "@/adapters/testing-actor"; import { type Input, type Output } from "../tests-data"; import { PaymentSystemType } from "../reference"; @@ -27,5 +28,7 @@ class SimpleTestRunner extends PHPTestRunner { } } -export const testRunnerFactory = async ({ code, out }: TestRunnerConfig) => - new SimpleTestRunner(out, new FailSafePHP(phpRuntimeFactory), code); +startTestRunnerActor( + async ({ code, out }: TestRunnerConfig) => + new SimpleTestRunner(out, new FailSafePHP(phpRuntimeFactory), code) +); diff --git a/src/content/design-patterns/factory/python/index.ts b/src/content/design-patterns/factory/python/index.ts new file mode 100644 index 0000000..40cbc3e --- /dev/null +++ b/src/content/design-patterns/factory/python/index.ts @@ -0,0 +1,3 @@ +export { default as pyCode } from "./code.py?raw" + +export { default as PyWorker } from "./worker?worker" diff --git a/src/content/design-patterns/factory/python/test-runners.ts b/src/content/design-patterns/factory/python/worker.ts similarity index 57% rename from src/content/design-patterns/factory/python/test-runners.ts rename to src/content/design-patterns/factory/python/worker.ts index 2760d5e..6c2f146 100644 --- a/src/content/design-patterns/factory/python/test-runners.ts +++ b/src/content/design-patterns/factory/python/worker.ts @@ -1,5 +1,6 @@ -import type { TestRunnerConfig } from '@/lib/testing'; +import type { TestRunnerConfig } from "@/lib/testing"; import { PyTestRunner, pyRuntimeFactory } from "@/lib/testing/python"; +import { startTestRunnerActor } from "@/adapters/testing-actor"; import { type Input, type Output } from "../tests-data"; @@ -9,5 +10,7 @@ class SimpleTestRunner extends PyTestRunner { } } -export const testRunnerFactory = async ({ code, out }: TestRunnerConfig) => - new SimpleTestRunner(await pyRuntimeFactory(out), code); +startTestRunnerActor( + async ({ code, out }: TestRunnerConfig) => + new SimpleTestRunner(await pyRuntimeFactory(out), code) +); diff --git a/src/content/design-patterns/factory/js/code.ts b/src/content/design-patterns/factory/ts/code.ts similarity index 100% rename from src/content/design-patterns/factory/js/code.ts rename to src/content/design-patterns/factory/ts/code.ts diff --git a/src/content/design-patterns/factory/ts/index.ts b/src/content/design-patterns/factory/ts/index.ts new file mode 100644 index 0000000..ea37eb2 --- /dev/null +++ b/src/content/design-patterns/factory/ts/index.ts @@ -0,0 +1,3 @@ +export { default as tsCode } from './code.ts?raw' + +export { default as TsWorker } from './worker?worker' diff --git a/src/content/design-patterns/factory/ts/worker.ts b/src/content/design-patterns/factory/ts/worker.ts new file mode 100644 index 0000000..78fddb8 --- /dev/null +++ b/src/content/design-patterns/factory/ts/worker.ts @@ -0,0 +1,22 @@ +import { createLogger } from "@/lib/logger"; +import type { TestRunnerConfig } from "@/lib/testing"; +import { TsTestRunner } from "@/lib/testing/js"; +import { startTestRunnerActor } from "@/adapters/testing-actor"; + +import { type Input, type Output } from "../tests-data"; +import type { PaymentSystemType } from "../reference"; + +interface TestingModule { + payment(type: PaymentSystemType, base: number, amount: number): number; +} + +class SimpleTsTestRunner extends TsTestRunner { + async executeTest(m: TestingModule, input: Input): Promise { + return m.payment(input.paymentSystem, input.base, input.amount); + } +} + +startTestRunnerActor( + async ({ code, out }: TestRunnerConfig) => + new SimpleTsTestRunner(createLogger(out), code) +); diff --git a/src/lib/actor/actor.ts b/src/lib/actor/actor.ts index 7b28d98..e893d4b 100644 --- a/src/lib/actor/actor.ts +++ b/src/lib/actor/actor.ts @@ -19,23 +19,28 @@ export interface ActorConfig { } export class Actor { - constructor(protected readonly config: ActorConfig) {} + constructor( + protected readonly connection: Connection< + IncomingMessage, + OutgoingMessage + >, + protected readonly handlers: H, + protected readonly caseError: (error: any) => E + ) {} start() { - const { connection } = this.config; const handleMessage = this.handleMessage.bind(this); - connection.onMessage(handleMessage); + this.connection.onMessage(handleMessage); return () => { - connection.onMessage(handleMessage); + this.connection.onMessage(handleMessage); }; } private async handleMessage(msg: IncomingMessage) { switch (msg.type) { case MessageType.Request: { - const { connection } = this.config; const result = await this.handleRequest(msg); - connection.send(result); + this.connection.send(result); return; } case MessageType.Event: { @@ -51,12 +56,12 @@ export class Actor { event: EventMessage[0]> ) { try { - await this.config.handlers[event.event](event.payload); + await this.handlers[event.event](event.payload); } catch (error) { - this.config.connection.send({ + this.connection.send({ type: MessageType.Event, event: "error", - payload: this.config.caseError(error), + payload: this.caseError(error), }); } } @@ -64,9 +69,8 @@ export class Actor { private async handleRequest( request: RequestMessage[0]> ): Promise, E>>> { - const { handlers, caseError } = this.config; try { - const result = await handlers[request.request](request.payload); + const result = await this.handlers[request.request](request.payload); return { type: MessageType.Response, id: request.id, @@ -76,7 +80,7 @@ export class Actor { return { type: MessageType.Response, id: request.id, - result: err(caseError(error)), + result: err(this.caseError(error)), }; } } diff --git a/src/lib/actor/index.ts b/src/lib/actor/index.ts index bbfe941..032b80c 100644 --- a/src/lib/actor/index.ts +++ b/src/lib/actor/index.ts @@ -1,2 +1,4 @@ +export * from './model' export * from './actor' export * from './worker-connection' +export * from './remote' diff --git a/src/lib/actor/model.ts b/src/lib/actor/model.ts index d2a6216..35601e2 100644 --- a/src/lib/actor/model.ts +++ b/src/lib/actor/model.ts @@ -1,5 +1,5 @@ import type { AnyKey, Brand } from "@/lib/type"; -import type { Result } from '@/lib/result'; +import type { Result } from "@/lib/result"; export enum MessageType { Event = "event", @@ -50,4 +50,3 @@ export interface ErrorEventMessage extends EventMessage<"error", E> {} export type OutgoingMessage = | ResponseMessage, E>> | ErrorEventMessage; - diff --git a/src/lib/actor/remote.ts b/src/lib/actor/remote.ts index b2bbbc0..a8580ab 100644 --- a/src/lib/actor/remote.ts +++ b/src/lib/actor/remote.ts @@ -1,33 +1,98 @@ +import type { Logger } from "@/lib/logger"; +import { neverError } from "@/lib/guards"; +import { isOk } from "@/lib/result"; + import { MessageType, type Connection, + type ErrorEventMessage, + type EventMessage, type Handlers, type IncomingMessage, + type OutgoingMessage, type RequestId, } from "./model"; -export function createRemote( - connection: Connection> +interface DeferredPromise { + resolve: (value: T) => void; + reject: (error: E) => void; +} + +export function startRemote< + H extends Handlers, + E, + Event extends EventMessage +>( + log: Logger, + connection: Connection | Event, IncomingMessage>, + eventHandlers: { + [K in Event["event"]]: (e: Extract>['payload']) => void; + } & { + [K in ErrorEventMessage["event"]]: (e: ErrorEventMessage['payload']) => void; + } ) { let lastId = 0; - // const + const promises = new Map< + RequestId, + DeferredPromise, E> + >(); + const unsubscribe = connection.onMessage((msg) => { + switch (msg.type) { + case MessageType.Response: { + const { id, result } = msg; + const deferred = promises.get(id); + if (!deferred) { + log.error(`Received response for unknown request ${id}`); + return; + } + promises.delete(id); + if (isOk(result)) { + deferred.resolve(result.value); + } else { + deferred.reject(result.error); + } + return; + } + case MessageType.Event: { + // @ts-expect-error ts problem + const handler = eventHandlers[msg.event]; + if (!handler) { + log.error(`Received unknown event ${msg.event}`); + return; + } + handler(msg.payload); + return; + } + default: + throw neverError(msg, "Unknown message type"); + } + }); return new Proxy( {}, { get(_, prop) { + if (prop === Symbol.dispose) { + return unsubscribe; + } const request = prop as keyof H; return (arg: Parameters[0]) => { const id = lastId++ as RequestId; + const promise = new Promise>( + (resolve, reject) => { + promises.set(id, { resolve, reject }); + } + ); connection.send({ id: id, request, type: MessageType.Request, payload: arg, }); + return promise; }; }, } ) as { [K in keyof H]: (arg: Parameters[0]) => Promise>; - }; + } & Disposable; } diff --git a/src/lib/actor/worker-connection.ts b/src/lib/actor/worker-connection.ts index 335f315..49ee872 100644 --- a/src/lib/actor/worker-connection.ts +++ b/src/lib/actor/worker-connection.ts @@ -1,7 +1,9 @@ import type { Connection } from "./model"; -export class WorkerConnection implements Connection { - private handlers = new Set<(message: I) => void>(); +export class WorkerConnection + implements Connection +{ + private handlers = new Set<(message: Incoming) => void>(); constructor(private readonly worker: Worker) {} @@ -17,11 +19,11 @@ export class WorkerConnection implements Connection { }; } - send(message: O) { + send(message: Outgoing) { this.worker.postMessage(message); } - onMessage(handler: (message: I) => void) { + onMessage(handler: (message: Incoming) => void) { this.handlers.add(handler); return () => { this.handlers.delete(handler);