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 #670

Merged
merged 6 commits into from
Nov 9, 2023
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
wip
d-gubert committed Nov 2, 2023
commit 37e71bb67d86a53858d63a8ddc325a939ab39d63
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:acorn@8.10.0",
"acorn-walk": "npm:acorn-walk@8.2.0",
"astring": "npm:astring@1.8.6"
115 changes: 88 additions & 27 deletions deno-runtime/lib/accessors/mod.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,122 @@
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';
import { IConfigurationModify } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationModify.ts';
import { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead.ts';
import { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend.ts';

export function proxify(namespace: string) {
import * as Messenger from '../messenger.ts';

export function proxify<T>(namespace: string): T {
return new Proxy(
{},
{
get(target: unknown, prop: string): unknown {
return (...args: unknown[]) => {
return {};
};
},
get:
(_target: unknown, prop: string) =>
(...params: unknown[]) =>
Messenger.sendRequest({
method: `accessor:${namespace}.${prop}`,
params,
}),
},
);
) as T;
}

export class AppAccessors {
private environmentReader?: IEnvironmentRead;
private defaultAppAccessors?: IAppAccessors;
private environmentWriter?: IEnvironmentWrite;
private configModifier?: IConfigurationModify;
private configExtender?: IConfigurationExtend;
private reader?: IRead;

constructor(private readonly appId: string) {}
public getEnvironmentRead(namespacePrefix = ''): IEnvironmentRead {
// Not worth it to "cache" this one because of the prefix
return {
getSettings: () => proxify(namespacePrefix + 'environmentRead.getSettings'),
getServerSettings: () => proxify(namespacePrefix + 'environmentRead.getServerSettings'),
getEnvironmentVariables: () => proxify(namespacePrefix + 'environmentRead.getEnvironmentVariables'),
};
}

public getEnvironmentReader() {
if (!this.environmentReader) {
this.environmentReader = {
getSettings: this.getSettingsReader(),
getServerSettings: this.getServerSettingsReader(),
getEnvironmentVariables: this.getEnvironmentVariablesReader(),
}
public getEnvironmentWrite() {
if (!this.environmentWriter) {
this.environmentWriter = {
getSettings: () => proxify('environmentWrite.getSettings'),
getServerSettings: () => proxify('environmentWrite.getServerSettings'),
};
}

return this.environmentReader;
return this.environmentWriter;
}

public getEnvironmentWriter() {
if (!this.environmentWriter) {
this.environmentWriter = {
getSettings: this.getSettingsUpdater(),
getServerSettings: this.getServerSettingsUpdater(),
public getConfigurationModify() {
if (!this.configModifier) {
this.configModifier = {
scheduler: proxify('configurationModify.scheduler'),
slashCommands: proxify('configurationModify.slashCommands'),
serverSettings: proxify('configurationModify.serverSettings'),
};
}

return this.configModifier;
}

public getConifgurationExtend() {
if (!this.configExtender) {
this.configExtender = {
ui: proxify('configurationExtend.ui'),
api: proxify('configurationExtend.api'),
http: proxify('configurationExtend.http'),
settings: proxify('configurationExtend.settings'),
scheduler: proxify('configurationExtend.scheduler'),
slashCommands: proxify('configurationExtend.slashCommands'),
externalComponents: proxify('configurationExtend.externalComponents'),
videoConfProviders: proxify('configurationExtend.videoConfProviders'),
}
}

return this.environmentWriter;
return this.configExtender;
}

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

return this.defaultAppAccessors;
}

public getReader() {
if (!this.reader) {
this.reader = {
getEnvironmentReader: () => this.getEnvironmentRead('reader.'),
getMessageReader: () => proxify('reader.getMessageReader'),
getPersistenceReader: () => proxify('reader.getPersistenceReader'),
getRoomReader: () => proxify('reader.getRoomReader'),
getUserReader: () => proxify('reader.getUserReader'),
getNotifier: () => proxify('reader.getNotifier'),
getLivechatReader: () => proxify('reader.getLivechatReader'),
getUploadReader: () => proxify('reader.getUploadReader'),
getCloudWorkspaceReader: () => proxify('reader.getCloudWorkspaceReader'),
getVideoConferenceReader: () => proxify('reader.getVideoConferenceReader'),
getOAuthAppsReader: () => proxify('reader.getOAuthAppsReader'),
getThreadReader: () => proxify('reader.getThreadReader'),
getRoleReader: () => proxify('reader.getRoleReader'),
};
}

return this.reader;
}

public getHttp() {
return proxify('http');
}
}

export const AppAccessorsInstance = new AppAccessors();
18 changes: 15 additions & 3 deletions deno-runtime/lib/messenger.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ export type JSONRPC_Message = {

export type RequestDescriptor = {
method: string;
params: any[];
params: unknown[];
};

export type Request = JSONRPC_Message &
@@ -14,7 +14,7 @@ export type Request = JSONRPC_Message &

export type SuccessResponseDescriptor = {
id: string;
result: any;
result: unknown;
};

export type SuccessResponse = JSONRPC_Message & SuccessResponseDescriptor;
@@ -32,6 +32,8 @@ export type ErrorResponse = JSONRPC_Message & ErrorResponseDescriptor;

export type Response = SuccessResponse | ErrorResponse;

export type NotificationDescriptor = RequestDescriptor;

export function isJSONRPCMessage(message: object): message is JSONRPC_Message {
return 'jsonrpc' in message && message['jsonrpc'] === '2.0-rc';
}
@@ -99,7 +101,7 @@ export async function successResponse({ id, result }: SuccessResponseDescriptor)
await Deno.stdout.write(encoded);
}

export async function sendRequest(requestDescriptor: RequestDescriptor): Promise<Request> {
export async function sendRequest(requestDescriptor: RequestDescriptor): Promise<SuccessResponse> {
const request: Request = {
jsonrpc: '2.0-rc',
id: Math.random().toString(36).slice(2),
@@ -125,3 +127,13 @@ export async function sendRequest(requestDescriptor: RequestDescriptor): Promise
RPCResponseObserver.addEventListener(`response:${request.id}`, handler);
});
}

export function sendNotification(notification: NotificationDescriptor) {
const request = {
jsonrpc: '2.0-rc',
...notification,
}

const encoded = encoder.encode(JSON.stringify(request));
Deno.stdout.write(encoded);
}
18 changes: 9 additions & 9 deletions deno-runtime/main.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ if (!Deno.args.includes('--subprocess')) {

import { createRequire } from 'node:module';
import { sanitizeDeprecatedUsage } from "./lib/sanitizeDeprecatedUsage.ts";
import { proxify } from "./lib/accessors/mod.ts";
import { AppAccessorsInstance, proxify } from "./lib/accessors/mod.ts";
import * as Messenger from "./lib/messenger.ts";

const require = createRequire(import.meta.url);
@@ -66,7 +66,7 @@ async function handlInitializeApp({ id, source }: { id: string; source: string }
const exports = await wrapAppCode(source)(require);
// This is the same naive logic we've been using in the App Compiler
const appClass = Object.values(exports)[0] as typeof App;
const app = new appClass({ author: {} }, proxify('logger'), proxify('AppAccessors'));
const app = new appClass({ author: {} }, proxify('logger'), AppAccessorsInstance.getDefaultAppAccessors());

if (typeof app.getName !== 'function') {
throw new Error('App must contain a getName function');
@@ -98,9 +98,9 @@ async function handlInitializeApp({ id, source }: { id: string; source: string }
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!" });
const [appId, source] = params as [string, string];
const app = await handlInitializeApp({ id: appId, source })
Messenger.successResponse({ id, result: 'ok'});
break;
}
default: {
@@ -113,7 +113,7 @@ async function handleRequest({ method, params, id }: Messenger.Request): Promise
}
}

async function handleResponse(response: Messenger.Response): Promise<void> {
function handleResponse(response: Messenger.Response) {
let event: Event;

if (Messenger.isErrorResponse(response)) {
@@ -126,14 +126,14 @@ async function handleResponse(response: Messenger.Response): Promise<void> {
}

async function main() {
setTimeout(() => notifyEngine({ method: 'ready' }), 1_780);
setTimeout(() => Messenger.sendNotification({ method: 'ready', params: null }), 1_780);

const decoder = new TextDecoder();
let app: typeof App;

for await (const chunk of Deno.stdin.readable) {
const message = decoder.decode(chunk);
let JSONRPCMessage
let JSONRPCMessage;

try {
JSONRPCMessage = JSON.parse(message);
@@ -146,7 +146,7 @@ async function main() {
}

if (Messenger.isResponse(JSONRPCMessage)) {
await handleResponse(JSONRPCMessage);
handleResponse(JSONRPCMessage);
}
}
}
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -95,6 +95,7 @@
"cryptiles": "^4.1.3",
"deno-bin": "^1.36.2",
"jose": "^4.11.1",
"jsonrpc-lite": "^2.2.0",
"lodash.clonedeep": "^4.5.0",
"semver": "^5.7.1",
"stack-trace": "0.0.10",
14 changes: 13 additions & 1 deletion src/server/runtime/AppsEngineDenoRuntime.ts
Original file line number Diff line number Diff line change
@@ -29,6 +29,11 @@ export function getDenoWrapperPath(): string {
}
}

type ControllerDeps = {
accessors: AppAccessorManager;
api: AppApiManager;
};

export class DenoRuntimeSubprocessController extends EventEmitter {
private readonly deno: child_process.ChildProcess;

@@ -38,8 +43,12 @@ export class DenoRuntimeSubprocessController extends EventEmitter {

private state: 'uninitialized' | 'ready' | 'invalid' | 'unknown';

private readonly accessors: AppAccessorManager;

private readonly api: AppApiManager;

// We need to keep the appSource around in case the Deno process needs to be restarted
constructor(private readonly appId: string, private readonly appSource: string) {
constructor(private readonly appId: string, private readonly appSource: string, deps: ControllerDeps) {
super();

this.state = 'uninitialized';
@@ -55,6 +64,9 @@ export class DenoRuntimeSubprocessController extends EventEmitter {
} catch {
this.state = 'invalid';
}

this.accessors = deps.accessors;
this.api = deps.api;
}

emit(eventName: string | symbol, ...args: any[]): boolean {