Skip to content

Commit a5d24cb

Browse files
committed
refactor(deno-runtime): extract main handler loop from main.ts
squash into previous PR
1 parent 38d07be commit a5d24cb

4 files changed

Lines changed: 142 additions & 124 deletions

File tree

packages/apps/deno-runtime/handlers/app/construct.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { IParseAppPackageResult } from '@rocket.chat/apps/dist/server/compiler/IParseAppPackageResult';
22

33
import { AppObjectRegistry } from '../../AppObjectRegistry';
4-
import { require } from '../../lib/require';
54
import { sanitizeDeprecatedUsage } from '../../lib/sanitizeDeprecatedUsage';
65
import { AppAccessorsInstance } from '../../lib/accessors/mod';
76
import { RequestContext } from '../../lib/requestContext';
@@ -52,23 +51,23 @@ function buildRequire(): (module: string) => unknown {
5251
const normalized = module.replace('node:', '');
5352

5453
if (ALLOWED_NATIVE_MODULES.includes(normalized)) {
55-
return require(`node:${normalized}`);
54+
return sandboxRequire(`node:${normalized}`);
5655
}
5756

5857
if (ALLOWED_EXTERNAL_MODULES.includes(module)) {
59-
return require(`npm:${module}`);
58+
return sandboxRequire(`npm:${module}`);
6059
}
6160

6261
if (module.startsWith('@rocket.chat/apps-engine')) {
6362
// Our `require` function knows how to handle these
64-
return require(module);
63+
return sandboxRequire(module);
6564
}
6665

6766
throw new Error(`Module ${module} is not allowed`);
6867
};
6968
}
7069

71-
function wrapAppCode(code: string): (require: SandboxRequire) => unknown) => Promise<Record<string, unknown>> {
70+
function wrapAppCode(code: string): (require: SandboxRequire) => Promise<Record<string, unknown>> {
7271
const globals = sandboxGlobals;
7372
// The common globals are bound by name; any platform-specific extras are
7473
// spread in by name from the injected `sandboxGlobals`, so the shell

packages/apps/deno-runtime/lib/require.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ export const require = (mod: string) => {
77
// However, the import maps are configured to look at the source folder for typescript files, but during
88
// runtime those files are not available
99
if (mod.startsWith('@rocket.chat/apps-engine')) {
10-
// Only remove "src/" substring when it comes after "apps-engine/"
11-
mod = import.meta.resolve(mod).replace('file://', '').replace('apps-engine/src/', 'apps-engine/');
10+
mod = import.meta.resolve(mod).replace('file://', '');
1211
}
1312

1413
return _require(mod);

packages/apps/deno-runtime/main.ts

Lines changed: 12 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1+
import { Buffer } from 'node:buffer';
12
import { Socket } from 'node:net';
23
import process from 'node:process';
34

4-
import { JsonRpcError, type SuccessObject } from 'jsonrpc-lite';
5-
6-
import * as Messenger from './lib/messenger';
5+
// Deno consumes the base as TypeScript source through the import map: ESM
6+
// imports honor the map (`@rocket.chat/apps/` → `../`), so bare specifiers and
7+
// `@rocket.chat/apps/*` resolve to allowed paths. Importing the compiled `dist`
8+
// instead would run the base as CommonJS, whose `require()` bypasses the import
9+
// map and falls back to node_modules — outside the subprocess read allowlist.
710
import { stdoutTransport } from './lib/transports/stdoutTransport';
8-
import { decoder } from './lib/codec';
9-
import { Logger } from './lib/logger';
10-
import slashcommandHandler from './handlers/slashcommand-handler';
11-
import videoConferenceHandler from './handlers/videoconference-handler';
12-
import apiHandler from './handlers/api-handler';
11+
import { setTransport } from './lib/messenger';
12+
1313
import { setSandboxGlobals, setSandboxRequire } from './handlers/app/construct';
14-
import handleApp from './handlers/app/handler';
15-
import handleScheduler from './handlers/scheduler-handler';
1614
import registerErrorListeners from './error-handlers';
17-
import { sendMetrics } from './lib/metricsCollector';
18-
import outboundMessageHandler from './handlers/outboundcomms-handler';
19-
import { RequestContext } from './lib/requestContext';
15+
import { require } from './lib/require';
16+
import { startMainLoop } from './mainLoop';
2017

2118
if (!process.argv.includes('--subprocess')) {
2219
process.stderr.write(`
@@ -26,108 +23,6 @@ if (!process.argv.includes('--subprocess')) {
2623
process.exit(1001);
2724
}
2825

29-
type Handlers = {
30-
app: typeof handleApp;
31-
api: typeof apiHandler;
32-
slashcommand: typeof slashcommandHandler;
33-
videoconference: typeof videoConferenceHandler;
34-
outboundCommunication: typeof outboundMessageHandler;
35-
scheduler: typeof handleScheduler;
36-
ping: (request: RequestContext) => 'pong';
37-
};
38-
39-
const COMMAND_PING = '_zPING';
40-
41-
async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promise<void> {
42-
const methodHandlers: Handlers = {
43-
app: handleApp,
44-
api: apiHandler,
45-
slashcommand: slashcommandHandler,
46-
videoconference: videoConferenceHandler,
47-
outboundCommunication: outboundMessageHandler,
48-
scheduler: handleScheduler,
49-
ping: (_request) => 'pong',
50-
};
51-
52-
// We're not handling notifications at the moment
53-
if (type === 'notification') {
54-
return Messenger.sendInvalidRequestError();
55-
}
56-
57-
const { id, method } = payload;
58-
59-
const logger = new Logger(method);
60-
61-
const context: RequestContext = Object.assign(payload, {
62-
context: { logger },
63-
});
64-
65-
const [methodPrefix] = method.split(':') as [keyof Handlers];
66-
const handler = methodHandlers[methodPrefix];
67-
68-
if (!handler) {
69-
return Messenger.errorResponse(
70-
{
71-
error: { message: 'Method not found', code: -32601 },
72-
id,
73-
},
74-
context,
75-
);
76-
}
77-
78-
const result = await handler(context);
79-
80-
if (result instanceof JsonRpcError) {
81-
return Messenger.errorResponse({ id, error: result }, context);
82-
}
83-
84-
return Messenger.successResponse({ id, result }, context);
85-
}
86-
87-
function handleResponse(response: Messenger.JsonRpcResponse): void {
88-
let payload: { error: Error } | { detail: SuccessObject };
89-
90-
if (Messenger.isErrorResponse(response.payload)) {
91-
payload = { error: new Error(response.payload.error.message) };
92-
} else {
93-
payload = { detail: response.payload };
94-
}
95-
96-
Messenger.RPCResponseObserver.emit(`response:${response.payload.id}`, payload);
97-
}
98-
99-
async function main() {
100-
Messenger.sendNotification({ method: 'ready' });
101-
102-
for await (const message of decoder.decodeStream(process.stdin)) {
103-
try {
104-
// Process PING command first as it is not JSON RPC
105-
if (message === COMMAND_PING) {
106-
void Messenger.pongResponse();
107-
void sendMetrics();
108-
continue;
109-
}
110-
111-
const JSONRPCMessage = Messenger.parseMessage(message as Record<string, unknown>);
112-
113-
if (Messenger.isRequest(JSONRPCMessage)) {
114-
void requestRouter(JSONRPCMessage);
115-
continue;
116-
}
117-
118-
if (Messenger.isResponse(JSONRPCMessage)) {
119-
handleResponse(JSONRPCMessage);
120-
}
121-
} catch (error) {
122-
if (Messenger.isErrorResponse(error)) {
123-
await Messenger.errorResponse(error);
124-
} else {
125-
await Messenger.sendParseError();
126-
}
127-
}
128-
}
129-
}
130-
13126
function prepareEnvironment() {
13227
// Deno does not behave equally to Node when it comes to piping content to a socket
13328
// So we intervene here
@@ -142,7 +37,7 @@ function prepareEnvironment() {
14237
}
14338

14439
// This runtime communicates with the Apps-Engine host through stdout
145-
Messenger.setTransport(stdoutTransport);
40+
setTransport(stdoutTransport);
14641

14742
// Deno's sandbox `require` is a createRequire shim that resolves compiled
14843
// apps-engine paths; the eval shell additionally needs a `Buffer` and a
@@ -155,4 +50,4 @@ registerErrorListeners();
15550
// Process-global side effect; doing it once at startup is cleaner than inside construct
15651
prepareEnvironment();
15752

158-
main();
53+
startMainLoop();
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import process from 'node:process';
2+
3+
import { JsonRpcError, type SuccessObject } from 'jsonrpc-lite';
4+
5+
import apiHandler from './handlers/api-handler';
6+
import handleApp from './handlers/app/handler';
7+
import outboundMessageHandler from './handlers/outboundcomms-handler';
8+
import handleScheduler from './handlers/scheduler-handler';
9+
import slashcommandHandler from './handlers/slashcommand-handler';
10+
import videoConferenceHandler from './handlers/videoconference-handler';
11+
import { decoder } from './lib/codec';
12+
import { Logger } from './lib/logger';
13+
import * as Messenger from './lib/messenger';
14+
import { sendMetrics } from './lib/metricsCollector';
15+
import type { RequestContext } from './lib/requestContext';
16+
17+
type Handlers = {
18+
app: typeof handleApp;
19+
api: typeof apiHandler;
20+
slashcommand: typeof slashcommandHandler;
21+
videoconference: typeof videoConferenceHandler;
22+
outboundCommunication: typeof outboundMessageHandler;
23+
scheduler: typeof handleScheduler;
24+
ping: (request: RequestContext) => 'pong';
25+
};
26+
27+
const COMMAND_PING = '_zPING';
28+
29+
async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promise<void> {
30+
const methodHandlers: Handlers = {
31+
app: handleApp,
32+
api: apiHandler,
33+
slashcommand: slashcommandHandler,
34+
videoconference: videoConferenceHandler,
35+
outboundCommunication: outboundMessageHandler,
36+
scheduler: handleScheduler,
37+
ping: (_request) => 'pong',
38+
};
39+
40+
// We're not handling notifications at the moment
41+
if (type === 'notification') {
42+
return Messenger.sendInvalidRequestError();
43+
}
44+
45+
const { id, method } = payload;
46+
47+
const logger = new Logger(method);
48+
49+
const context: RequestContext = Object.assign(payload, {
50+
context: { logger },
51+
});
52+
53+
const [methodPrefix] = method.split(':') as [keyof Handlers];
54+
const handler = methodHandlers[methodPrefix];
55+
56+
if (!handler) {
57+
return Messenger.errorResponse(
58+
{
59+
error: { message: 'Method not found', code: -32601 },
60+
id,
61+
},
62+
context,
63+
);
64+
}
65+
66+
const result = await handler(context);
67+
68+
if (result instanceof JsonRpcError) {
69+
return Messenger.errorResponse({ id, error: result }, context);
70+
}
71+
72+
return Messenger.successResponse({ id, result }, context);
73+
}
74+
75+
function handleResponse(response: Messenger.JsonRpcResponse): void {
76+
let payload: { error: Error } | { detail: SuccessObject };
77+
78+
if (Messenger.isErrorResponse(response.payload)) {
79+
payload = { error: new Error(response.payload.error.message) };
80+
} else {
81+
payload = { detail: response.payload };
82+
}
83+
84+
Messenger.RPCResponseObserver.emit(`response:${response.payload.id}`, payload);
85+
}
86+
87+
/**
88+
* The platform-agnostic message loop shared by every runtime.
89+
*
90+
* Adapters are expected to wire up their platform seams — transport, sandbox
91+
* `require`/globals, error listeners — during bootstrap and only then invoke
92+
* this loop. It reads messages from `process.stdin` (a `node:` API available on
93+
* every supported platform) and dispatches them to the shared handlers.
94+
*/
95+
export async function startMainLoop(): Promise<void> {
96+
Messenger.sendNotification({ method: 'ready', params: [] });
97+
98+
for await (const message of decoder.decodeStream(process.stdin)) {
99+
try {
100+
// Process PING command first as it is not JSON RPC
101+
if (message === COMMAND_PING) {
102+
void Messenger.pongResponse();
103+
void sendMetrics();
104+
continue;
105+
}
106+
107+
const JSONRPCMessage = Messenger.parseMessage(message as Record<string, unknown>);
108+
109+
if (Messenger.isRequest(JSONRPCMessage)) {
110+
void requestRouter(JSONRPCMessage);
111+
continue;
112+
}
113+
114+
if (Messenger.isResponse(JSONRPCMessage)) {
115+
handleResponse(JSONRPCMessage);
116+
}
117+
} catch (error) {
118+
if (Messenger.isErrorResponse(error)) {
119+
await Messenger.errorResponse(error);
120+
} else {
121+
await Messenger.sendParseError();
122+
}
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)