@script-bridge/server
Version:
two-way communication system between ScriptAPI and backend server using http request
204 lines (203 loc) • 8.37 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScriptBridgeServer = void 0;
const http = __importStar(require("node:http"));
const node_events_1 = require("node:events");
const express_1 = __importDefault(require("express"));
const protocol_1 = require("@script-bridge/protocol");
const session_1 = require("./session");
const client_action_1 = require("./client-action");
const errors_1 = require("./errors");
const handlers_1 = require("./handlers");
class ScriptBridgeServer extends node_events_1.EventEmitter {
static PROTOCOL_VERSION = protocol_1.PROTOCOL_VERSION;
sessions = new Map();
port;
requestIntervalTicks = 8;
timeoutThresholdMultiplier = 20;
server;
app;
actionHandlers = new Map();
constructor(options) {
super();
this.app = (0, express_1.default)();
this.server = http.createServer(this.app);
this.app.use(express_1.default.json());
this.app.use(express_1.default.urlencoded({ extended: true }));
this.port = options.port;
if (options.requestIntervalTicks !== undefined)
this.requestIntervalTicks = options.requestIntervalTicks;
if (options.timeoutThresholdMultiplier !== undefined)
this.timeoutThresholdMultiplier = options.timeoutThresholdMultiplier;
this.app.get('/new', (_, res) => {
const session = new session_1.Session(this);
res.json({
type: protocol_1.PayloadType.Response,
data: {
sessionId: session.id,
requestIntervalTicks: this.requestIntervalTicks,
},
});
});
this.app.get('/query', (req, res) => {
const sessionId = req.headers['session-id'];
const session = sessionId ? this.sessions.get(sessionId) : undefined;
if (!session) {
res.json({
type: protocol_1.PayloadType.Response,
error: true,
message: 'Session is invalid',
errorReason: protocol_1.ResponseErrorReason.InvalidSession,
});
return;
}
const requests = session.getQueue();
res.json(requests);
for (const request of requests)
this.emit('requestSend', request, session);
this.emit('queryReceive', session);
});
this.app.post('/query', (req, res) => {
const body = req.body;
const session = this.sessions.get(body.sessionId);
if (!session) {
res.json({
type: protocol_1.PayloadType.Response,
error: true,
errorReason: protocol_1.ResponseErrorReason.InvalidSession,
message: 'Invalid session',
});
return;
}
if (body.type === protocol_1.PayloadType.Request) {
this.emit('requestReceive', body, session);
this.handleRequest(session, body, (data) => {
res.json(data);
this.emit('responseSend', data, session);
});
}
else if (body.type === protocol_1.PayloadType.Response) {
this.handleResponse(session, body);
this.emit('responseReceive', body, session);
res.sendStatus(200);
}
else {
res.json({
type: protocol_1.PayloadType.Response,
error: true,
errorReason: protocol_1.ResponseErrorReason.InvalidPayload,
message: 'Invalid payload type',
});
}
});
(0, handlers_1.registerHandlers)(this);
}
async start() {
return new Promise(resolve => {
this.server.listen(this.port, () => {
this.emit('serverOpen');
resolve();
});
});
}
async stop() {
await Promise.all([...this.sessions.values()].map((session) => session.disconnect()));
this.server.close();
this.emit('serverClose');
}
broadcast(channelId, data, timeout = 10_000) {
if (typeof channelId !== 'string')
throw new TypeError('channelId must be a string');
if (!channelId.includes(':'))
throw new errors_1.NamespaceRequiredError(channelId);
return Promise.all([...this.sessions.values()].map((session) => session.send(channelId, data, timeout)));
}
registerHandler(channelId, handler) {
if (typeof channelId !== 'string')
throw new TypeError('channelId must be a string');
if (!channelId.includes(':'))
throw new errors_1.NamespaceRequiredError(channelId);
if (this.actionHandlers.has(channelId)) {
process.emitWarning(`[ScriptBridge::registerHandler] Overwriting existing handler for channel: ${channelId}`);
}
this.actionHandlers.set(channelId, handler);
}
async handleRequest(session, request, respond) {
const handler = this.actionHandlers.get(request.channelId);
if (!handler) {
this.emit('error', new errors_1.UnhandledRequestError(request.channelId));
respond({
type: protocol_1.PayloadType.Response,
error: true,
errorReason: protocol_1.ResponseErrorReason.UnhandledRequest,
message: `No handler found for channel: ${request.channelId}`,
});
return;
}
let isResponded = false;
try {
const action = new client_action_1.ClientAction(request.data, (data) => {
if (isResponded)
return;
respond({ error: false, data, type: protocol_1.PayloadType.Response });
isResponded = true;
}, session);
await handler(action);
}
catch (error) {
this.emit('error', error);
if (!isResponded)
respond({
type: protocol_1.PayloadType.Response,
error: true,
errorReason: protocol_1.ResponseErrorReason.InternalError,
message: 'An error occurred while handling the request\n' + error.message,
});
}
}
handleResponse(session, response) {
const awaitingResponse = session._awaitingResponses.get(response.requestId);
if (!awaitingResponse) {
this.emit('error', new Error(`Received response for unknown request: ${response.requestId}`));
return;
}
awaitingResponse(response);
}
}
exports.ScriptBridgeServer = ScriptBridgeServer;