diff --git a/deno-runtime/lib/messenger.ts b/deno-runtime/lib/messenger.ts index 9ebf3c2d1..98e50028e 100644 --- a/deno-runtime/lib/messenger.ts +++ b/deno-runtime/lib/messenger.ts @@ -2,12 +2,24 @@ export type JSONRPC_Message = { jsonrpc: '2.0-rc'; }; -export type SuccessResponse = JSONRPC_Message & { +export type RequestDescriptor = { + method: string; + params: any[]; +}; + +export type Request = JSONRPC_Message & + RequestDescriptor & { + id: string; + }; + +export type SuccessResponseDescriptor = { id: string; result: any; }; -export type ErrorResponse = JSONRPC_Message & { +export type SuccessResponse = JSONRPC_Message & SuccessResponseDescriptor; + +export type ErrorResponseDescriptor = { error: { code: number; message: string; @@ -16,11 +28,56 @@ export type ErrorResponse = JSONRPC_Message & { id: string | null; }; -export type JSONRPC_Response = SuccessResponse | ErrorResponse; +export type ErrorResponse = JSONRPC_Message & ErrorResponseDescriptor; + +export type Response = SuccessResponse | ErrorResponse; + +export function isJSONRPCMessage(message: object): message is JSONRPC_Message { + return 'jsonrpc' in message && message['jsonrpc'] === '2.0-rc'; +} + +export function isRequest(message: object): message is Request { + return isJSONRPCMessage(message) && 'method' in message && 'params' in message && 'id' in message; +} + +export function isResponse(message: object): message is Response { + return isJSONRPCMessage(message) && ('result' in message || 'error' in message); +} + +export function isErrorResponse(response: Response): response is ErrorResponse { + return 'error' in response; +} + +export function isSuccessResponse(response: Response): response is SuccessResponse { + return 'result' in response; +} const encoder = new TextEncoder(); +export const RPCResponseObserver = new EventTarget(); + +export async function serverParseError(): Promise { + const rpc: ErrorResponse = { + jsonrpc: '2.0-rc', + id: null, + error: { message: 'Parse error', code: -32700 }, + }; + + const encoded = encoder.encode(JSON.stringify(rpc)); + await Deno.stdout.write(encoded); +} + +export async function serverMethodNotFound(id: string): Promise { + const rpc: ErrorResponse = { + jsonrpc: '2.0-rc', + id, + error: { message: 'Method not found', code: -32601 }, + }; + + const encoded = encoder.encode(JSON.stringify(rpc)); + await Deno.stdout.write(encoded); +} -export async function errorResponse({ error: { message, code = -32000, data }, id }: Omit): Promise { +export async function errorResponse({ error: { message, code = -32000, data }, id }: ErrorResponseDescriptor): Promise { const rpc: ErrorResponse = { jsonrpc: '2.0-rc', id, @@ -31,12 +88,40 @@ export async function errorResponse({ error: { message, code = -32000, data }, i Deno.stdout.write(encoded); } -export async function successResponse(id: string, ...result: unknown[]): Promise { +export async function successResponse({ id, result }: SuccessResponseDescriptor): Promise { const rpc: SuccessResponse = { jsonrpc: '2.0-rc', id, result, }; + const encoded = encoder.encode(JSON.stringify(rpc)); await Deno.stdout.write(encoded); } + +export async function sendRequest(requestDescriptor: RequestDescriptor): Promise { + const request: Request = { + jsonrpc: '2.0-rc', + id: Math.random().toString(36).slice(2), + ...requestDescriptor, + }; + + const encoded = encoder.encode(JSON.stringify(request)); + await Deno.stdout.write(encoded); + + return new Promise((resolve, reject) => { + const handler = (event: Event) => { + if (event instanceof ErrorEvent) { + reject(event.error); + } + + if (event instanceof CustomEvent) { + resolve(event.detail); + } + + RPCResponseObserver.removeEventListener(`response:${request.id}`, handler); + }; + + RPCResponseObserver.addEventListener(`response:${request.id}`, handler); + }); +} diff --git a/deno-runtime/main.ts b/deno-runtime/main.ts index d1035d135..9e50220a8 100644 --- a/deno-runtime/main.ts +++ b/deno-runtime/main.ts @@ -95,6 +95,36 @@ async function handlInitializeApp({ id, source }: { id: string; source: string } return app; } +async function handleRequest({ method, params, id }: Messenger.Request): Promise { + switch (method) { + case 'construct': { + const [appId, source] = params; + app = await handlInitializeApp({ id: appId, source }) + Messenger.successResponse(id, { result: "hooray!" }); + break; + } + default: { + Messenger.errorResponse({ + error: { message: 'Method not found', code: -32601 }, + id, + }); + break; + } + } +} + +async function handleResponse(response: Messenger.Response): Promise { + let event: Event; + + if (Messenger.isErrorResponse(response)) { + event = new ErrorEvent(`response:${response.id}`, { error: response.error }); + } else { + event = new CustomEvent(`response:${response.id}`, { detail: response.result }); + } + + Messenger.RPCResponseObserver.dispatchEvent(event); +} + async function main() { setTimeout(() => notifyEngine({ method: 'ready' }), 1_780); @@ -103,22 +133,20 @@ async function main() { for await (const chunk of Deno.stdin.readable) { const message = decoder.decode(chunk); - const { method, params, id } = JSON.parse(message); - - switch (method) { - case 'construct': { - const [appId, source] = params; - app = await handlInitializeApp({ id: appId, source }) - Messenger.successResponse(id, { result: "hooray!" }); - break; - } - default: { - Messenger.errorResponse({ - error: { message: 'Method not found', code: -32601 }, - id, - }); - break; - } + let JSONRPCMessage + + try { + JSONRPCMessage = JSON.parse(message); + } catch (_) { + return Messenger.serverParseError(); + } + + if (Messenger.isRequest(JSONRPCMessage)) { + await handleRequest(JSONRPCMessage); + } + + if (Messenger.isResponse(JSONRPCMessage)) { + await handleResponse(JSONRPCMessage); } } } diff --git a/src/server/runtime/AppsEngineDenoRuntime.ts b/src/server/runtime/AppsEngineDenoRuntime.ts index 13056646f..743e6d77e 100644 --- a/src/server/runtime/AppsEngineDenoRuntime.ts +++ b/src/server/runtime/AppsEngineDenoRuntime.ts @@ -2,6 +2,9 @@ import * as child_process from 'child_process'; import * as path from 'path'; import { EventEmitter } from 'stream'; +import type { AppAccessorManager, AppApiManager } from '../managers'; +import type { AppManager } from '../AppManager'; + export type AppRuntimeParams = { appId: string; appSource: string; @@ -75,7 +78,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter { this.sendRequest({ method: 'construct', params: [this.appId, this.appSource] }); } - private async sendRequest(message: Record): Promise { + public async sendRequest(message: Record): Promise { const id = String(Math.random()).substring(2); this.deno.stdin.write(JSON.stringify({ id, ...message })); @@ -160,9 +163,24 @@ export class DenoRuntimeSubprocessController extends EventEmitter { } } +type ExecRequestContext = { + method: string; + params: Record; + namespace?: string; // Use a namespace notation in the `method` property for this +}; + export class AppsEngineDenoRuntime { private readonly subprocesses: Record = {}; + private readonly accessorManager: AppAccessorManager; + + private readonly apiManager: AppApiManager; + + constructor(manager: AppManager) { + this.accessorManager = manager.getAccessorManager(); + this.apiManager = manager.getApiManager(); + } + public async startRuntimeForApp({ appId, appSource }: AppRuntimeParams, options = { force: false }): Promise { if (appId in this.subprocesses && !options.force) { throw new Error('App already has an associated runtime'); @@ -172,4 +190,14 @@ export class AppsEngineDenoRuntime { await this.subprocesses[appId].setupApp(); } + + public async runInSandbox(appId: string, execRequest: ExecRequestContext) { + const subprocess = this.subprocesses[appId]; + + if (!subprocess) { + throw new Error('App does not have an associated runtime'); + } + + return subprocess.sendRequest(execRequest); + } }