UNPKG

@triplit/client

Version:
173 lines 6.5 kB
import { WebSocketsUnavailableError } from '../errors.js'; const DEFAULT_PAYLOAD_SIZE_LIMIT = (1024 * 1024) / 2; function webSocketsAreAvailable() { return typeof WebSocket !== 'undefined'; } export class WebSocketTransport { options; ws = undefined; constructor(options = {}) { this.options = options; this.options.messagePayloadSizeLimit = // allow 0 to disable the limit this.options.messagePayloadSizeLimit == undefined ? DEFAULT_PAYLOAD_SIZE_LIMIT : this.options.messagePayloadSizeLimit; } get isOpen() { return !!this.ws && this.ws.readyState === this.ws.OPEN; } get connectionStatus() { return this.ws ? friendlyReadyState(this.ws) : 'UNINITIALIZED'; } onOpen(callback) { if (this.ws) this.ws.onopen = callback; } sendMessage(message) { // For now, skip sending messages if we're not connected. I dont think we need a queue yet. if (!this.ws) return false; if (!this.isOpen) { // console.log('skipping', type, payload); return false; } // Perform chunking if the message is too large const serializedMessage = JSON.stringify(message); const bytes = getPayloadSize(serializedMessage); if (this.options.messagePayloadSizeLimit && bytes > this.options.messagePayloadSizeLimit) { const chunks = chunkMessage(serializedMessage, Math.ceil(bytes / this.options.messagePayloadSizeLimit)); const messageid = (Math.random() + 1).toString(36).substring(7); for (let i = 0; i < chunks.length; i++) { this.ws.send(JSON.stringify({ type: 'CHUNK', payload: { data: chunks[i], total: chunks.length, index: i, id: messageid, }, })); } return true; } this.ws.send(JSON.stringify(message)); return true; } connect(params) { // Close any existing connection this.close(); // Setup connection URL const { token, schema, syncSchema, server } = params; const wsOptions = new URLSearchParams(); if (schema) { wsOptions.set('schema', schema.toString()); } wsOptions.set('sync-schema', String(!!syncSchema)); wsOptions.set('token', token); const secure = server.startsWith('https://'); const domain = server.slice(secure ? 8 : 7); // remove protocol const wsUri = `${secure ? 'wss' : 'ws'}://${domain}?${wsOptions.toString()}`; if (!webSocketsAreAvailable()) { throw new WebSocketsUnavailableError(); } // Create a new WebSocket connection and set up event listeners this.ws = new WebSocket(wsUri); } // TODO: feels a bit awkward that these have to be set up after connect() onMessage(callback) { if (this.ws) this.ws.onmessage = callback; } onError(callback) { if (this.ws) this.ws.onerror = callback; } close(reason) { // Assuming normal close for now (1000), possibly map reasons to codes later if (!this.ws) return; // If socket is open, close if (this.ws.readyState === this.ws.OPEN) { return this.ws.close(1000, JSON.stringify(reason)); } // If socket is connecting, close once open if (this.ws.readyState === this.ws.CONNECTING) { return this.ws.addEventListener('open', () => { this.ws?.close(1000, JSON.stringify(reason)); }, { once: true }); } } onClose(callback) { if (this.ws) this.ws.onclose = callback; } onConnectionChange(callback) { if (this.ws) this.ws.onconnectionchange = callback; } } function getPayloadSize(payload) { var sizeInBytes = 0; for (let i = 0; i < payload.length; i++) { const code = payload.charCodeAt(i); sizeInBytes += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4; } return sizeInBytes; } function chunkMessage(message, numChunks) { let chunks = []; const chunkSize = Math.ceil(message.length / numChunks); for (let i = 0; i < message.length; i += chunkSize) { chunks.push(message.slice(i, i + chunkSize)); } return chunks; } function friendlyReadyState(conn) { switch (conn.readyState) { case conn.CONNECTING: return 'CONNECTING'; case conn.OPEN: return 'OPEN'; case conn.CLOSING: return 'CLOSING'; // I'm not sure 'CLOSING' will ever be a state we see with connection change events case conn.CLOSED: // Default to closed... this shouldnt happen and probably indicates something is wrong default: return 'CLOSED'; } } if (typeof globalThis !== 'undefined' && globalThis.WebSocket) { var WebSocketProxy = new Proxy(globalThis.WebSocket, { construct: function (target, args) { //@ts-expect-error const instance = new target(...args); function dispatchConnectionChangeEvent() { instance.dispatchEvent(new Event('connectionchange')); if (instance.onconnectionchange && typeof instance.onconnectionchange === 'function') { instance.onconnectionchange(friendlyReadyState(instance)); } } // Handle connecting state after constructor setTimeout(() => { dispatchConnectionChangeEvent(); }, 0); const openHandler = () => { dispatchConnectionChangeEvent(); }; const closeHandler = () => { dispatchConnectionChangeEvent(); instance.removeEventListener('open', openHandler); instance.removeEventListener('close', closeHandler); }; instance.addEventListener('open', openHandler); instance.addEventListener('close', closeHandler); return instance; }, }); // Replace native/global WebSocket with the proxy globalThis.WebSocket = WebSocketProxy; } //# sourceMappingURL=websocket-transport.js.map