UNPKG

@fakehost/signalr

Version:

A Fake Signalr Service for faking/mocking signalr hub services for testing, prototyping, and demoing

460 lines (455 loc) 15 kB
// src/FakeSignalrHub.ts import { Subject } from "@microsoft/signalr"; import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; // src/ClientState.ts var ClientState = class { constructor(connection) { this.connection = connection; this.state = {}; // streams service -> client this.subscriptions = /* @__PURE__ */ new Map(); // streams client -> service this.subjects = /* @__PURE__ */ new Map(); } unsubscribe(id) { const subscription = this.subscriptions.get(id); if (!subscription) return; if ("unsubscribe" in subscription) { subscription.unsubscribe(); } else { subscription.dispose(); } } cleanup(id) { const subject = this.subjects.get(id); if (subject) { subject.complete(); this.subjects.delete(id); } const subscription = this.subscriptions.get(id); if (subscription) { this.unsubscribe(id); this.subscriptions.delete(id); } } dispose() { Array.from(this.subscriptions.keys()).forEach((id) => this.unsubscribe(id)); this.subjects.forEach((subject) => subject.complete()); } setState(key, value) { this.state[key] = value; } }; // src/messageTypes.ts var isHandshakeMessage = (message) => { return message.protocol !== void 0; }; // src/messagePack/BinaryMessageFormat.ts var BinaryMessageFormat = class { // The length prefix of binary messages is encoded as VarInt. Read the comment in // the BinaryMessageParser.TryParseMessage for details. static write(output) { let size = output.byteLength || output.length; const lenBuffer = []; do { let sizePart = size & 127; size = size >> 7; if (size > 0) { sizePart |= 128; } lenBuffer.push(sizePart); } while (size > 0); size = output.byteLength || output.length; const buffer = new Uint8Array(lenBuffer.length + size); buffer.set(lenBuffer, 0); buffer.set(output, lenBuffer.length); return buffer.buffer; } static parse(input) { const result = []; const uint8Array = new Uint8Array(input); const maxLengthPrefixSize = 5; const numBitsToShift = [0, 7, 14, 21, 28]; for (let offset = 0; offset < input.byteLength; ) { let numBytes = 0; let size = 0; let byteRead; do { byteRead = uint8Array[offset + numBytes]; size = size | (byteRead & 127) << numBitsToShift[numBytes]; numBytes++; } while (numBytes < Math.min(maxLengthPrefixSize, input.byteLength - offset) && (byteRead & 128) !== 0); if ((byteRead & 128) !== 0 && numBytes < maxLengthPrefixSize) { throw new Error("Cannot read message size."); } if (numBytes === maxLengthPrefixSize && byteRead > 7) { throw new Error("Messages bigger than 2GB are not supported."); } if (uint8Array.byteLength >= offset + numBytes + size) { result.push( uint8Array.slice ? uint8Array.slice(offset + numBytes, offset + numBytes + size) : uint8Array.subarray(offset + numBytes, offset + numBytes + size) ); } else { throw new Error("Incomplete message."); } offset = offset + numBytes + size; } return result; } }; // src/messagePack/parse.ts import { Decoder } from "@msgpack/msgpack"; var parse = (input) => { const buffer = Buffer.from(input); const arrayBuffer = buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength ); const messages = BinaryMessageFormat.parse(arrayBuffer); const hubMessages = new Array(); for (const message of messages) { const parsedMessage = parseMessage(message); if (parsedMessage) { hubMessages.push(parsedMessage); } } return hubMessages; }; var parseMessage = (input) => { if (input.length === 0) { throw new Error("Invalid payload."); } const decoder = new Decoder(); const properties = decoder.decode(input); if (properties.length === 0 || !(properties instanceof Array)) { throw new Error("Invalid payload."); } const messageType = properties[0]; switch (messageType) { case 1 /* Invocation */: return { type: 1 /* Invocation */, arguments: properties[4], invocationId: properties[2], streamIds: properties[5], target: properties[3] }; case 2 /* StreamItem */: return { type: 2 /* StreamItem */, invocationId: properties[2], item: properties[3] }; case 3 /* Completion */: { const resultKind = properties[3]; return { type: 3 /* Completion */, error: resultKind === 1 ? properties[4] : void 0, invocationId: properties[2], result: resultKind === 1 ? void 0 : properties[4] }; } case 6 /* Ping */: return { type: 6 /* Ping */ }; case 7 /* Close */: return { type: 7 /* Close */, allowReconnect: properties.length >= 3 ? properties[2] : void 0, error: properties[1] }; case 5 /* CancelInvocation */: return { type: 5 /* CancelInvocation */, invocationId: properties[2] }; case 4 /* StreamInvocation */: return { type: 4 /* StreamInvocation */, invocationId: properties[2], target: properties[3], arguments: properties[4] }; default: console.log("Unknown message type '" + messageType + "' ignored."); return null; } }; // src/FakeSignalrHub.ts var protocol = new MessagePackHubProtocol(); var TERMINATING_CHAR = String.fromCharCode(30); var FakeSignalrHub = class { constructor(path, receivers = {}, format) { this.path = path; this.receivers = receivers; this.format = format; // active client connections to this hub this.clients = /* @__PURE__ */ new Map(); // methods from Hub. Not typed due to casing of methods (camelCase in ts vs PascalCase in C#) this.handlers = /* @__PURE__ */ new Map(); this.messageProtocol = /* @__PURE__ */ new Map(); this.connectionEvents = /* @__PURE__ */ new Map(); } disconnect(options) { var _a; (_a = this.host) == null ? void 0 : _a.disconnect({ path: this.path, ...options }); } setHost(host) { this.host = host; this.host.on("connection", this.onConnection.bind(this)); this.host.on("disconnection", this.onDisconnection.bind(this)); this.host.on("message", (e) => { if (e.connection.url.pathname !== this.path) return; if (!this.messageProtocol.has(e.connection.id)) { this.handleHandshake(e.connection, e.message); return; } const messages = this.deserialize(e.connection, e.message); messages.forEach((message) => this.onMessage.bind(this)(e.connection, message)); }); } /** * There can be differences in casing between the client typescript and the server handler methods in C#. * This method formats the target to match the casing of the server. * @param s * @returns */ formatTarget(s) { if (this.format === "capitalize" && typeof s === "string") { return capitalize(s); } else if (typeof this.format === "function") { return this.format(s); } else { return s; } } onConnection({ connection }) { if (connection.url.pathname !== this.path) return; this.clients.set(connection.id, new ClientState(connection)); } onDisconnection({ connection }) { var _a; if (connection.url.pathname !== this.path) return; (_a = this.clients.get(connection.id)) == null ? void 0 : _a.dispose(); this.clients.delete(connection.id); const handlers = this.connectionEvents.get(`${connection.id}.disconnect`); handlers == null ? void 0 : handlers.forEach((handler) => handler()); this.connectionEvents.delete(`${connection.id}.disconnect`); } handleHandshake(connection, message) { const [parsed] = message.toString().split(TERMINATING_CHAR).filter(Boolean).map((m) => JSON.parse(m)); if (!isHandshakeMessage(parsed)) { console.error("Expected initial handshake message, but none was received."); connection.close({ code: 1002, reason: "No handshake supplied" }); return; } this.messageProtocol.set(connection.id, parsed.protocol); connection.write(JSON.stringify({ type: 0 }) + TERMINATING_CHAR); } serialize(connection, message) { switch (this.messageProtocol.get(connection.id)) { case "json": return JSON.stringify(message) + TERMINATING_CHAR; case "messagepack": return protocol.writeMessage(message); default: throw new Error("Unknown connection mode"); } } deserialize(connection, message) { switch (this.messageProtocol.get(connection.id)) { case "json": { return message.toString().split(TERMINATING_CHAR).filter(Boolean).map((m) => JSON.parse(m)); } case "messagepack": { return parse(message); } default: throw new Error("Unknown connection mode"); } } async onMessage(connection, message) { var _a, _b; const connectionId = connection.id; const client = this.clients.get(connectionId); if (!client) return; switch (message.type) { case 1 /* Invocation */: { const handler = this.handlers.get(message.target); if (message.streamIds) { message.streamIds.forEach(async (streamId) => { const subject = new Subject(); client.subjects.set(streamId, subject); await (handler == null ? void 0 : handler.apply(this.getSignalrInstance(connectionId), [subject])); }); return; } try { const result = await (handler == null ? void 0 : handler.apply( this.getSignalrInstance(connectionId), (_a = message.arguments) != null ? _a : [] )); return connection.write( this.serialize(connection, { type: 3 /* Completion */, invocationId: message.invocationId, result }) ); } catch (error) { return connection.write( this.serialize(connection, { type: 3 /* Completion */, invocationId: message.invocationId, error: stringifyError(error) }) ); } } case 4 /* StreamInvocation */: { const handler = this.handlers.get(message.target); const result = await (handler == null ? void 0 : handler.apply( this.getSignalrInstance(connectionId), (_b = message.arguments) != null ? _b : [] )); const subscription = result.subscribe({ next: (value) => { connection.write( this.serialize(connection, { type: 2 /* StreamItem */, invocationId: message.invocationId, item: value }) ); }, error: (error) => { connection.write( this.serialize(connection, { type: 3 /* Completion */, invocationId: message.invocationId, error: stringifyError(error) }) ); }, complete: () => { connection.write( this.serialize(connection, { type: 3 /* Completion */, invocationId: message.invocationId }) ); } }); client.subscriptions.set(message.invocationId, subscription); return; } case 2 /* StreamItem */: { const subject = client.subjects.get(message.invocationId); return subject == null ? void 0 : subject.next(message.item); } case 5 /* CancelInvocation */: { client.cleanup(message.invocationId); connection.write( this.serialize(connection, { type: 3 /* Completion */, invocationId: message.invocationId }) ); return; } case 3 /* Completion */: { client.cleanup(message.invocationId); return; } case 6 /* Ping */: return connection.write(this.serialize(connection, { type: 6 /* Ping */ })); default: console.warn("Not handled", message); } } getSignalrInstance(currentConnectionId) { const client = this.clients.get(currentConnectionId); if (!client) { throw new Error("Excepted a client but there was none"); } const createClientSender = (predicate) => { const result = Object.keys(this.receivers).reduce((acc, target) => { acc[target] = (...args) => { Array.from(this.clients.entries()).filter(([connId]) => predicate(connId)).forEach(([, client2]) => { client2.connection.write( this.serialize(client2.connection, { type: 1 /* Invocation */, target: this.formatTarget(target), arguments: args }) ); }); }; return acc; }, {}); return result; }; const signalrThis = { Connection: { get id() { return client.connection.id; }, setState: (key, value) => { client.setState(key, value); }, getState: (key) => { return client.state[key]; }, addEventHandler: (eventName, handler) => { const handlers = this.connectionEvents.get(`${currentConnectionId}.${eventName}`) || /* @__PURE__ */ new Set(); handlers.add(handler); this.connectionEvents.set(`${currentConnectionId}.${eventName}`, handlers); }, removeEventHandler: (eventName, handler) => { const handlers = this.connectionEvents.get(`${currentConnectionId}.${eventName}`) || /* @__PURE__ */ new Set(); handlers.delete(handler); this.connectionEvents.set(`${currentConnectionId}.${eventName}`, handlers); } }, Clients: { get All() { return createClientSender(() => true); }, get Others() { return createClientSender((id) => id !== client.connection.id); }, get Caller() { return createClientSender((id) => id === client.connection.id); }, Client: (clientId) => { return createClientSender((id) => id === clientId); } } }; return signalrThis; } get thisInstance() { throw new Error("Not callable. Used only for type inference."); } register(target, handler) { this.handlers.set(this.formatTarget(target), handler); } }; var capitalize = (key) => key.slice(0, 1).toUpperCase() + key.slice(1); var stringifyError = (e) => { if (typeof e === "string") return e; if (e instanceof Error) return e.message; return JSON.stringify(e); }; export { FakeSignalrHub }; //# sourceMappingURL=index.js.map