Skip to content
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

Create mocks to call accessors from Deno #668

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deno-runtime/deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"imports": {
"@rocket.chat/apps-engine/": "./../src/",
"acorn": "npm:[email protected]",
"acorn-walk": "npm:[email protected]",
"astring": "npm:[email protected]"
Expand Down
64 changes: 57 additions & 7 deletions deno-runtime/lib/accessors/mod.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,61 @@
import { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors.ts';
import { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite.ts';
import { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead.ts';

export function proxify(namespace: string) {
return new Proxy({}, {
get(target: unknown, prop: string): unknown {
return (...args: unknown[]) => {
return {};
};
}
})
return new Proxy(
{},
{
get(target: unknown, prop: string): unknown {
return (...args: unknown[]) => {
return {};
};
},
},
);
}

export class AppAccessors {
private environmentReader?: IEnvironmentRead;
private defaultAppAccessors?: IAppAccessors;
private environmentWriter?: IEnvironmentWrite;

constructor(private readonly appId: string) {}

public getEnvironmentReader() {
if (!this.environmentReader) {
this.environmentReader = {
getSettings: this.getSettingsReader(),
getServerSettings: this.getServerSettingsReader(),
getEnvironmentVariables: this.getEnvironmentVariablesReader(),
}
}

return this.environmentReader;
}

public getEnvironmentWriter() {
if (!this.environmentWriter) {
this.environmentWriter = {
getSettings: this.getSettingsUpdater(),
getServerSettings: this.getServerSettingsUpdater(),
}
}

return this.environmentWriter;
}

public getDefaultAppAccessors() {
if (!this.defaultAppAccessors) {
this.defaultAppAccessors = {
environmentReader: this.getEnvironmentReader(),
environmentWriter: this.getEnvironmentWriter(),
reader: this.getReader(),
http: this.getHttp(),
providedApiEndpoints: this.getProvidedApiEndpoints(),
}
}

return this.defaultAppAccessors;
}
}
95 changes: 90 additions & 5 deletions deno-runtime/lib/messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<void> {
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<void> {
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<ErrorResponse, 'jsonrpc'>): Promise<void> {
export async function errorResponse({ error: { message, code = -32000, data }, id }: ErrorResponseDescriptor): Promise<void> {
const rpc: ErrorResponse = {
jsonrpc: '2.0-rc',
id,
Expand All @@ -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<void> {
export async function successResponse({ id, result }: SuccessResponseDescriptor): Promise<void> {
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<Request> {
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);
});
}
60 changes: 44 additions & 16 deletions deno-runtime/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,36 @@ async function handlInitializeApp({ id, source }: { id: string; source: string }
return app;
}

async function handleRequest({ method, params, id }: Messenger.Request): Promise<void> {
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<void> {
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);

Expand All @@ -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);
}
}
}
Expand Down
30 changes: 29 additions & 1 deletion src/server/runtime/AppsEngineDenoRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,7 +78,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter {
this.sendRequest({ method: 'construct', params: [this.appId, this.appSource] });
}

private async sendRequest(message: Record<string, unknown>): Promise<unknown> {
public async sendRequest(message: Record<string, unknown>): Promise<unknown> {
const id = String(Math.random()).substring(2);

this.deno.stdin.write(JSON.stringify({ id, ...message }));
Expand Down Expand Up @@ -160,9 +163,24 @@ export class DenoRuntimeSubprocessController extends EventEmitter {
}
}

type ExecRequestContext = {
method: string;
params: Record<string, unknown>;
namespace?: string; // Use a namespace notation in the `method` property for this
};

export class AppsEngineDenoRuntime {
private readonly subprocesses: Record<string, DenoRuntimeSubprocessController> = {};

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<void> {
if (appId in this.subprocesses && !options.force) {
throw new Error('App already has an associated runtime');
Expand All @@ -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);
}
}
Loading