UNPKG

tinybase

Version:

A reactive data store and sync engine.

486 lines (479 loc) 17.4 kB
/** * The synchronizers module of the TinyBase project lets you synchronize * MergeableStore data to and from other MergeableStore instances. * @see Synchronization guide * @packageDocumentation * @module synchronizers * @since v5.0.0 */ import type {Id, IdOrNull} from '../common/index.d.ts'; import type {MergeableStore} from '../mergeable-store/index.d.ts'; import type {Persister, Persists} from '../persisters/index.d.ts'; import type {Content} from '../store/index.d.ts'; /** * The Message enum is used to indicate the type of the message being passed * between Synchronizer instances. * * These message comprise the basic synchronization protocol for merging * MergeableStore instances across multiple systems. * * The enum is generally intended to be used internally within TinyBase itself * and opaque to applications that use synchronization. * @category Synchronization * @since v5.0.0 */ export const enum Message { /** * A message that is a response to a previous request. * @category Enum * @since v5.0.0 */ Response = 0, /** * A message that is a request to get ContentHashes from another * MergeableStore. * @category Enum * @since v5.0.0 */ GetContentHashes = 1, /** * A message that contains ContentHashes. * @category Enum * @since v5.0.0 */ ContentHashes = 2, /** * A message that contains a ContentDiff. * @category Enum * @since v5.0.0 */ ContentDiff = 3, /** * A message that is a request to get a TableDiff from another MergeableStore. * @category Enum * @since v5.0.0 */ GetTableDiff = 4, /** * A message that is a request to get a RowDiff from another MergeableStore. * @category Enum * @since v5.0.0 */ GetRowDiff = 5, /** * A message that is a request to get a CellDiff from another MergeableStore. * @category Enum * @since v5.0.0 */ GetCellDiff = 6, /** * A message that is a request to get a ValueDiff from another MergeableStore. * @category Enum * @since v5.0.0 */ GetValueDiff = 7, } /** * The Send type describes a function that knows how to dispatch a message as * part of the synchronization protocol. * @param toClientId The optional Id of the other client (in other words, the * other system) to which the message should be sent. If omitted, this is to be * a broadcast. * @param requestId The optional Id of the message, which should be awaited in * the response (if requested) to constitute a matched request/response * transaction. * @param message A number that indicates the type of the message, according to * the Message enum. * @param body A message-specific payload. * @category Synchronization * @since v5.0.0 */ export type Send = ( toClientId: IdOrNull, requestId: IdOrNull, message: Message, body: any, ) => void; /** * The Receive type describes a function that knows how to handle the arrival of * a message as part of the synchronization protocol. * * When a message arrives (most likely from another system), the function will * be called with parameters that indicate where the message came from, and its * meaning and content. * @param fromClientId The Id of the other client (in other words, the other * system) that sent the message. * @param requestId The optional Id of the message, which should be returned in * the response (if requested) to constitute a matched request/response * transaction. * @param message A number that indicates the type of the message, according to * the Message enum. * @param body A message-specific payload. * @category Synchronization * @since v5.0.0 */ export type Receive = ( fromClientId: Id, requestId: IdOrNull, message: Message, body: any, ) => void; /** * The SynchronizerStats type describes the number of times a Synchronizer * object has sent or received data. * * A SynchronizerStats object is returned from the getSynchronizerStats method. * @category Development * @since v5.0.0 */ export type SynchronizerStats = { /** * The number of times messages have been sent. * @category Stat * @since v5.0.0 */ sends: number; /** * The number of times messages has been received. * @category Stat * @since v5.0.0 */ receives: number; }; /** * A Synchronizer object lets you synchronize MergeableStore data with another * TinyBase client or system. * * This is useful for sharing data between users, or between devices of a single * user. This is especially valuable when there is the possibility that there * has been a lack of immediate connectivity between clients and the * synchronization requires some negotiation to orchestrate merging the * MergeableStore objects together. * * Creating a Synchronizer depends on the choice of underlying medium over which * the synchronization will take place. Options include the createWsSynchronizer * function (for a Synchronizer that will sync over web-sockets), and the * createLocalSynchronizer function (for a Synchronizer that will sync two * MergeableStore objects in memory on one system). The createCustomSynchronizer * function can also be used to easily create a fully customized way to send and * receive the messages of the synchronization protocol. * * Note that, as an interface, it is an extension to the Persister interface, * since they share underlying implementations. Think of a Synchronizer as * 'persisting' your MergeableStore to another client (and vice-versa). * @example * This example creates two empty MergeableStore objects, creates a * LocalSynchronizer for each, and starts synchronizing them. A change in one * becomes evident in the other. * * ```js * import {createMergeableStore} from 'tinybase'; * import {createLocalSynchronizer} from 'tinybase/synchronizers/synchronizer-local'; * * const store1 = createMergeableStore(); * const store2 = createMergeableStore(); * * const synchronizer1 = createLocalSynchronizer(store1); * const synchronizer2 = createLocalSynchronizer(store2); * * await synchronizer1.startSync(); * await synchronizer2.startSync(); * * store1.setTables({pets: {fido: {species: 'dog'}}}); * // ... * console.log(store2.getTables()); * // -> {pets: {fido: {species: 'dog'}}} * * store2.setRow('pets', 'felix', {species: 'cat'}); * // ... * console.log(store1.getTables()); * // -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}} * * synchronizer1.destroy(); * synchronizer2.destroy(); * ``` * @category Synchronizer * @since v5.0.0 */ export interface Synchronizer extends Persister<Persists.MergeableStoreOnly> { /** * The startSync method is used to start the process of synchronization * between this instance and another matching Synchronizer. * * The underlying implementation of a Synchronizer is shared with the * Persister framework, and so this startSync method is equivalent to starting * both auto-loading (listening to sync messages from other active * Synchronizer instances) and auto-saving (sending sync messages to it). * * This method is asynchronous so you should you `await` calls to this method * or handle the return type natively as a Promise. * @param initialContent An optional Content object used when no content is * available from another other peer Synchronizer instances. * @returns A Promise containing a reference to the Synchronizer object. * @example * This example creates two empty MergeableStore objects, creates a * LocalSynchronizer for each, and starts synchronizing them. A change in one * becomes evident in the other. * * ```js * import {createMergeableStore} from 'tinybase'; * import {createLocalSynchronizer} from 'tinybase/synchronizers/synchronizer-local'; * * const store1 = createMergeableStore(); * const store2 = createMergeableStore(); * * const synchronizer1 = createLocalSynchronizer(store1); * const synchronizer2 = createLocalSynchronizer(store2); * * await synchronizer1.startSync(); * await synchronizer2.startSync(); * * store1.setTables({pets: {fido: {species: 'dog'}}}); * // ... * console.log(store2.getTables()); * // -> {pets: {fido: {species: 'dog'}}} * * store2.setRow('pets', 'felix', {species: 'cat'}); * // ... * console.log(store1.getTables()); * // -> {pets: {fido: {species: 'dog'}, felix: {species: 'cat'}}} * * synchronizer1.destroy(); * synchronizer2.destroy(); * ``` * @example * This example creates two empty MergeableStore objects, creates a * LocalSynchronizer for each, and starts synchronizing them with default * content. The default content from the first Synchronizer's startSync method * ends up populated in both MergeableStore instances: by the time the second * started, the first was available to synchronize with and its default was * not needed. * * ```js * import {createMergeableStore} from 'tinybase'; * import {createLocalSynchronizer} from 'tinybase/synchronizers/synchronizer-local'; * * const store1 = createMergeableStore(); * const store2 = createMergeableStore(); * * const synchronizer1 = createLocalSynchronizer(store1); * const synchronizer2 = createLocalSynchronizer(store2); * * await synchronizer1.startSync([{pets: {fido: {species: 'dog'}}}, {}]); * await synchronizer2.startSync([{pets: {felix: {species: 'cat'}}}, {}]); * * // ... * * console.log(store1.getTables()); * // -> {pets: {fido: {species: 'dog'}}} * console.log(store2.getTables()); * // -> {pets: {fido: {species: 'dog'}}} * * synchronizer1.destroy(); * synchronizer2.destroy(); * ``` * @category Synchronization * @since v5.0.0 */ startSync(initialContent?: Content): Promise<this>; /** * The stopSync method is used to stop the process of synchronization between * this instance and another matching Synchronizer. * * The underlying implementation of a Synchronizer is shared with the * Persister framework, and so this startSync method is equivalent to stopping * both auto-loading (listening to sync messages from other active * Synchronizer instances) and auto-saving (sending sync messages to them). * @returns A reference to the Synchronizer object. * @example * This example creates two empty MergeableStore objects, creates a * LocalSynchronizer for each, and starts - then stops - synchronizing them. * Subsequent changes are not merged. * * ```js * import {createMergeableStore} from 'tinybase'; * import {createLocalSynchronizer} from 'tinybase/synchronizers/synchronizer-local'; * * const store1 = createMergeableStore(); * const synchronizer1 = createLocalSynchronizer(store1); * await synchronizer1.startSync(); * * const store2 = createMergeableStore(); * const synchronizer2 = createLocalSynchronizer(store2); * await synchronizer2.startSync(); * * store1.setTables({pets: {fido: {species: 'dog'}}}); * // ... * console.log(store1.getTables()); * // -> {pets: {fido: {species: 'dog'}}} * console.log(store2.getTables()); * // -> {pets: {fido: {species: 'dog'}}} * * synchronizer1.stopSync(); * synchronizer2.stopSync(); * * store1.setCell('pets', 'fido', 'color', 'brown'); * // ... * console.log(store1.getTables()); * // -> {pets: {fido: {species: 'dog', color: 'brown'}}} * console.log(store2.getTables()); * // -> {pets: {fido: {species: 'dog'}}} * * synchronizer1.destroy(); * synchronizer2.destroy(); * ``` * @category Synchronization * @since v5.0.0 */ stopSync(): this; /** * The getSynchronizerStats method provides a set of statistics about the * Synchronizer, and is used for debugging purposes. * * The SynchronizerStats object contains a count of the number of times the * Synchronizer has sent and received messages. * * The method is intended to be used during development to ensure your * synchronization layer is acting as expected, for example. * @returns A SynchronizerStats object containing Synchronizer send and * receive statistics. * @example * This example gets the send and receive statistics of two active * Synchronizer instances. * * ```js * import {createMergeableStore} from 'tinybase'; * import {createLocalSynchronizer} from 'tinybase/synchronizers/synchronizer-local'; * * const store1 = createMergeableStore(); * const store2 = createMergeableStore(); * * const synchronizer1 = createLocalSynchronizer(store1); * const synchronizer2 = createLocalSynchronizer(store2); * * await synchronizer1.startSync(); * await synchronizer2.startSync(); * * store1.setTables({pets: {fido: {species: 'dog'}}}); * // ... * store2.setRow('pets', 'felix', {species: 'cat'}); * // ... * * console.log(synchronizer1.getSynchronizerStats()); * // -> {receives: 4, sends: 5} * console.log(synchronizer2.getSynchronizerStats()); * // -> {receives: 5, sends: 4} * * synchronizer1.destroy(); * synchronizer2.destroy(); * ``` * @category Synchronization * @since v5.0.0 */ getSynchronizerStats(): SynchronizerStats; } /** * The createCustomSynchronizer function creates a Synchronizer object that can * persist one MergeableStore to another. * * As well as providing a reference to the MergeableStore to synchronize, you * must provide parameters which identify how to send and receive changes to and * from this MergeableStore and its peers. This is entirely dependent upon the * medium of communication used. * * You must also provide a callback for when the Synchronizer is destroyed * (which is a good place to clean up resources and stop communication * listeners), and indicate how long the Synchronizer will wait for responses to * message requests before timing out. * * A final set of optional handlers can be provided to help debug sends, * receives, and errors respectively. * @param store The MergeableStore to synchronize. * @param send A Send function for sending a message. * @param registerReceive A callback (called once when the Synchronizer is * created) that is passed a Receive function that you need to ensure will * receive messages addressed or broadcast to this client. * @param destroy A function called when destroying the Synchronizer which can * be used to clean up underlying resources. * @param requestTimeoutSeconds An number of seconds before a request sent from * this Synchronizer to another peer times out. * @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. * @returns A reference to the new Synchronizer object. * @example * This example creates a function for creating custom Synchronizer objects via * a very naive pair of message buses (which are first-in, first-out). Each * Synchronizer can write to the other's bus, and they each poll to read from * their own. The example then uses these Synchronizer instances to sync two * MergeableStore objects together * * ```js * import {createMergeableStore, getUniqueId} from 'tinybase'; * import {createCustomSynchronizer} from 'tinybase/synchronizers'; * * const bus1 = []; * const bus2 = []; * * const createBusSynchronizer = (store, localBus, remoteBus) => { * let timer; * const clientId = getUniqueId(); * return createCustomSynchronizer( * store, * (toClientId, requestId, message, body) => { * // send * remoteBus.push([clientId, requestId, message, body]); * }, * (receive) => { * // registerReceive * timer = setInterval(() => { * if (localBus.length > 0) { * receive(...localBus.shift()); * } * }, 1); * }, * () => clearInterval(timer), // destroy * 1, * ); * }; * * const store1 = createMergeableStore(); * const store2 = createMergeableStore(); * * const synchronizer1 = createBusSynchronizer(store1, bus1, bus2); * const synchronizer2 = createBusSynchronizer(store2, bus2, bus1); * * 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'}}} * * synchronizer1.destroy(); * synchronizer2.destroy(); * ``` * @category Creation * @since v5.0.0 */ export function createCustomSynchronizer( store: MergeableStore, send: Send, registerReceive: (receive: Receive) => void, destroy: () => void, requestTimeoutSeconds: number, onSend?: Send, onReceive?: Receive, onIgnoredError?: (error: any) => void, ): Synchronizer;