/** * The synchronizer-ws module of the TinyBase project lets you synchronize * MergeableStore data to and from other MergeableStore instances via WebSockets * facilitated by a server. * @see Synchronization guide * @see Todo App v6 (collaboration) demo * @packageDocumentation * @module synchronizer-ws-client * @since v5.0.0 */ import type {WebSocket as WsWebSocket} from 'ws'; import type {Id} from '../../common/index.d.ts'; import type {MergeableStore} from '../../mergeable-store/index.d.ts'; import type {Receive, Send, Synchronizer} from '../index.d.ts'; /** * The WebSocketTypes type represents the valid types of WebSocket that can be * used with the WsSynchronizer. * * This includes the browser-native WebSocket type, as well as the WebSocket * type from the well-known `ws` package (such that the Synchronizer can be used * in a server environment). * @category Creation * @since v5.0.0 */ export type WebSocketTypes = WebSocket | WsWebSocket; /** * The WsSynchronizer interface represents a Synchronizer that lets you * synchronize MergeableStore data to and from other MergeableStore instances * via WebSockets facilitated by a server. * * You should use the createWsSynchronizer function to create a WsSynchronizer * object. * * It is a minor extension to the Synchronizer interface and simply provides an * extra getWebSocket method for accessing a reference to the WebSocket being * used. * @category Synchronizer * @since v5.0.0 */ export interface WsSynchronizer< WebSocketType extends WebSocketTypes, > extends Synchronizer { /** * The getWebSocket method returns a reference to the WebSocket being used for * synchronization. * @returns The WebSocket reference. * @example * This example creates a server and WsSynchronizer object for a newly-created * MergeableStore and then gets the WebSocket reference back out again. * * ```js * import {createMergeableStore} from 'tinybase'; * import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client'; * import {createWsServer} from 'tinybase/synchronizers/synchronizer-ws-server'; * import {WebSocket, WebSocketServer} from 'ws'; * * const server = createWsServer(new WebSocketServer({port: 8046})); * * const store = createMergeableStore(); * const webSocket = new WebSocket('ws://localhost:8046'); * const synchronizer = await createWsSynchronizer(store, webSocket); * * console.log(synchronizer.getWebSocket() == webSocket); * // -> true * * await synchronizer.destroy(); * await server.destroy(); * ``` * @category Getter * @since v5.0.0 */ getWebSocket(): WebSocketType; } /** * The createWsSynchronizer function creates a WsSynchronizer object that can * synchronize MergeableStore data to and from other MergeableStore instances * via WebSockets facilitated by a WsServer. * * As well as providing a reference to the MergeableStore to persist, you must * provide a configured WebSocket to send synchronization messages over. The * path in the WebSocket URL is used by a WsServer as the path that the client * joins. The MergeableStore Id does not select the server path. * * The WsSynchronizer owns the WebSocket and closes it when it is destroyed. * Instead of the raw browser implementation of WebSocket, you may prefer to * use the [Reconnecting * WebSocket](https://github.com/pladaria/reconnecting-websocket) wrapper so * that if a client goes offline, it can easily re-establish a connection when * it comes back online. Its API is compatible with this Synchronizer. * * This method is asynchronous because it will await the websocket's * connection to the server. You will need to `await` a call to this function * or handle the return type natively as a Promise. * @param store The MergeableStore to synchronize. * @param webSocket The WebSocket to send synchronization messages over. * @param requestTimeoutSeconds An optional time in seconds that the * Synchronizer will wait for responses to request messages, defaulting to 1. * @param onSend An optional handler for the messages that this Synchronizer * sends. This is suitable for debugging synchronization issues in a * development environment, since v5.1. * @param onReceive An optional handler for the messages that this * Synchronizer receives. This is suitable for debugging synchronization * issues in a development environment, since v5.1. * @param onIgnoredError An optional handler for the errors that the * Synchronizer would otherwise ignore when trying to synchronize data. This * is suitable for debugging synchronization issues in a development * environment. * @param fragmentSize An optional target maximum UTF-8 byte size for each * WebSocket message fragment. Unicode code points are never split and can * exceed this size. TinyBase sends at most 1,000 fragments for one payload, * increasing the target when needed. When set, larger synchronization * payloads are split into fragments and reassembled by the receiving * WsSynchronizer, since v9.0. * @returns A reference to the new WsSynchronizer object. * @example * This example creates two WsSynchronizer objects to synchronize one * MergeableStore to another via a server. * * ```js * import {createMergeableStore} from 'tinybase'; * import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client'; * import {createWsServer} from 'tinybase/synchronizers/synchronizer-ws-server'; * import {WebSocket, WebSocketServer} from 'ws'; * * const server = createWsServer(new WebSocketServer({port: 8047})); * * const store1 = createMergeableStore(); * const store2 = createMergeableStore(); * * const synchronizer1 = await createWsSynchronizer( * store1, * new WebSocket('ws://localhost:8047/petShop'), * ); * const synchronizer2 = await createWsSynchronizer( * store2, * new WebSocket('ws://localhost:8047/petShop'), * ); * * await synchronizer1.startSync(); * await synchronizer2.startSync(); * * store1.setTables({pets: {fido: {species: 'dog'}}}); * store2.setTables({pets: {felix: {species: 'cat'}}}); * * // ... * console.log(store1.getTables()); * // -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}} * console.log(store2.getTables()); * // -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}} * * await synchronizer1.destroy(); * await synchronizer2.destroy(); * await server.destroy(); * ``` * @category Creation * @essential Synchronizing stores * @since v5.0.0 */ export function createWsSynchronizer( store: MergeableStore, webSocket: WebSocketType, requestTimeoutSeconds?: number, onSend?: Send, onReceive?: Receive, onIgnoredError?: (error: any) => void, fragmentSize?: number, ): Promise>; /** * This overload creates a WsSynchronizer that shares one WebSocket with * other WsSynchronizer instances, each synchronizing a different * MergeableStore. * * Construct the WebSocket with the `tinybase` subprotocol and provide a * channel Id as the third argument. The channel Id is appended to the * WebSocket URL path to form the server path; it is not taken from the * MergeableStore Id. It can contain at most 1,024 UTF-8 bytes, and one shared * WebSocket can have at most 100 channels. Creating a 101st channel is * rejected without closing the existing shared WebSocket. * * Destroying the WsSynchronizer unsubscribes its channel. The shared * WebSocket is closed only when the last WsSynchronizer using it is * destroyed. Reconnecting WebSocket implementations will renegotiate and * resubscribe all channels, then flush queued traffic. * * Sharing a WebSocket requires a WsServer or WsServerSimple from v9.3 or * later. It is not supported by WsServerDurableObject. * @param store The MergeableStore to synchronize. * @param webSocket The shared WebSocket to send synchronization messages * over, constructed with the `tinybase` subprotocol and shared by at most 100 * channels. * @param channelId The channel Id to append to the WebSocket URL path, with * at most 1,024 UTF-8 bytes. * @param requestTimeoutSeconds An optional time in seconds that the * Synchronizer will wait for responses to request messages, defaulting to 1. * @param onSend An optional handler for the messages that this Synchronizer * sends. This is suitable for debugging synchronization issues in a * development environment. * @param onReceive An optional handler for the messages that this * Synchronizer receives. This is suitable for debugging synchronization * issues in a development environment. * @param onIgnoredError An optional handler for errors that the Synchronizer * would otherwise ignore when trying to synchronize data. * @param fragmentSize An optional target maximum UTF-8 byte size for each * WebSocket message fragment. Unicode code points are never split and can * exceed this size. TinyBase sends at most 1,000 fragments for one payload, * increasing the target when needed. * @returns A reference to the new WsSynchronizer object. * @example * This example creates two WsSynchronizer objects for different * MergeableStore instances that share one WebSocket. * * ```js * import {createMergeableStore} from 'tinybase'; * import {createWsSynchronizer} from 'tinybase/synchronizers/synchronizer-ws-client'; * import {createWsServer} from 'tinybase/synchronizers/synchronizer-ws-server'; * import {WebSocket, WebSocketServer} from 'ws'; * * const server = createWsServer(new WebSocketServer({port: 8048})); * const webSocket = new WebSocket('ws://localhost:8048/petShop', 'tinybase'); * const petsSynchronizer = await createWsSynchronizer( * createMergeableStore(), * webSocket, * 'pets', * ); * const employeesSynchronizer = await createWsSynchronizer( * createMergeableStore(), * webSocket, * 'employees', * ); * * console.log(petsSynchronizer.getWebSocket() == webSocket); * // -> true * console.log(employeesSynchronizer.getWebSocket() == webSocket); * // -> true * * await petsSynchronizer.destroy(); * console.log(webSocket.readyState == WebSocket.OPEN); * // -> true * * await employeesSynchronizer.destroy(); * await server.destroy(); * ``` * @category Creation * @since v9.3.0 */ export function createWsSynchronizer( store: MergeableStore, webSocket: WebSocketType, channelId: Id, requestTimeoutSeconds?: number, onSend?: Send, onReceive?: Receive, onIgnoredError?: (error: any) => void, fragmentSize?: number, ): Promise>;