@fairmint/canton-node-sdk
Version:
Canton Node SDK
117 lines • 4.82 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebSocketClient = void 0;
const ws_1 = __importDefault(require("ws"));
const WebSocketErrorUtils_1 = require("./WebSocketErrorUtils");
/**
* Minimal WebSocket helper that:
*
* - Upgrades http(s) URL to ws(s)
* - Auth via Authorization header (standard approach)
* - Uses 'daml-ledger-api' subprotocol
* - Sends an initial request message
* - Dispatches parsed messages to user handlers
*/
class WebSocketClient {
constructor(client) {
this.client = client;
}
async connect(path, requestMessage, handlers) {
const baseUrl = this.client.getApiUrl();
const wsUrl = this.buildWsUrl(baseUrl, path);
const token = await this.client.authenticate();
// Use standard WebSocket auth pattern:
// - JWT in Authorization header
// - Application protocol in subprotocol (daml.ws.auth)
const protocols = ['daml.ws.auth'];
const socket = new ws_1.default(wsUrl, protocols, {
handshakeTimeout: 30000,
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
});
const logger = this.client.getLogger();
const log = async (event, payload) => {
if (logger) {
await logger.logRequestResponse(wsUrl, { event, requestMessage }, payload);
}
};
await log('connect', { headers: token ? { Authorization: '[REDACTED]' } : undefined, protocols });
socket.on('open', () => {
void (async () => {
try {
socket.send(JSON.stringify(requestMessage));
await log('send', requestMessage);
if (handlers.onOpen)
handlers.onOpen();
}
catch (err) {
await log('send_error', err instanceof Error ? { message: err.message } : String(err));
if (handlers.onError)
handlers.onError(err);
socket.close();
}
})();
});
socket.on('message', (rawData) => {
// Convert RawData to string safely
let dataString;
if (Buffer.isBuffer(rawData)) {
dataString = rawData.toString('utf8');
}
else if (Array.isArray(rawData)) {
dataString = Buffer.concat(rawData).toString('utf8');
}
else {
dataString = new TextDecoder().decode(rawData);
}
void (async () => {
try {
const parsed = WebSocketErrorUtils_1.WebSocketErrorUtils.safeJsonParse(dataString, 'WebSocket message');
await log('message', parsed);
handlers.onMessage(parsed);
}
catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
await log('parse_error', { raw: dataString, error: errorMessage });
// Close connection on JSON parse failure to prevent inconsistent state
socket.close(1003, 'Invalid JSON received');
if (handlers.onError)
handlers.onError(err);
}
})();
});
socket.on('error', (err) => {
void (async () => {
await log('socket_error', { message: err.message });
if (handlers.onError)
handlers.onError(err);
})();
});
socket.on('close', (code, reason) => {
void (async () => {
await log('close', { code, reason: reason.toString() });
if (handlers.onClose)
handlers.onClose(code, reason.toString());
})();
});
return {
close: () => {
// Remove all event listeners to prevent memory leaks
socket.removeAllListeners();
socket.close();
},
isConnected: () => socket.readyState === ws_1.default.OPEN,
getConnectionState: () => socket.readyState,
};
}
buildWsUrl(base, path) {
const hasProtocol = base.startsWith('http://') || base.startsWith('https://');
const normalizedBase = hasProtocol ? base : `https://${base}`;
const wsBase = normalizedBase.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:');
return `${wsBase}${path}`;
}
}
exports.WebSocketClient = WebSocketClient;
//# sourceMappingURL=WebSocketClient.js.map