UNPKG

@multisynq/client

Version:

Real-time multiplayer framework for web applications

1,082 lines (1,004 loc) 46.7 kB
declare module "@multisynq/client" { export type ClassId = string; export interface Class<T> extends Function { new (...args: any[]): T; } export type InstanceSerializer<T, IS> = { cls: Class<T>; write: (value: T) => IS; read: (state: IS) => T; } export type StaticSerializer<S> = { writeStatic: () => S; readStatic: (state: S) => void; } export type InstAndStaticSerializer<T, IS, S> = { cls: Class<T>; write: (value: T) => IS; read: (state: IS) => T; writeStatic: () => S; readStatic: (state: S) => void; } export type Serializer = InstanceSerializer<any, any> | StaticSerializer<any> | InstAndStaticSerializer<any, any, any>; export type SubscriptionHandler<T> = ((e: T) => void) | string; export abstract class PubSubParticipant<SubOptions> { publish<T>(scope: string, event: string, data?: T): void; subscribe<T>( scope: string, event: string | { event: string } | ({ event: string } & SubOptions), handler: SubscriptionHandler<T>, ): void; unsubscribe<T>( scope: string, event: string, handler?: SubscriptionHandler<T>, ): void; unsubscribeAll(): void; } export type FutureHandler<T extends any[]> = ((...args: T) => void) | string; export type QFuncEnv = Record<string, any>; export type EventType = { scope: string; event: string; source: "model" | "view"; } /** * Models are synchronized objects in Multisynq. * * They are automatically kept in sync for each user in the same [session]{@link Session.join}. * Models receive input by [subscribing]{@link Model#subscribe} to events published in a {@link View}. * Their output is handled by views subscribing to events [published]{@link Model#publish} by a model. * Models advance time by sending messages into their [future]{@link Model#future}. * * ## Instance Creation and Initialization * * ### Do __NOT__ create a {@link Model} instance using `new` and<br>do __NOT__ override the `constructor`! * * To __create__ a new instance, use [create()]{@link Model.create}, for example: * ``` * this.foo = FooModel.create({answer: 123}); * ``` * To __initialize__ an instance, override [init()]{@link Model#init}, for example: * ``` * class FooModel extends Multisynq.Model { * init(options={}) { * this.answer = options.answer || 42; * } * } * ``` * The **reason** for this is that Models are only initialized by calling `init()` * the first time the object comes into existence in the session. * After that, when joining a session, the models are deserialized from the snapshot, which * restores all properties automatically without calling `init()`. A constructor would * be called all the time, not just when starting a session. * * @hideconstructor * @public */ export class Model extends PubSubParticipant<{}> { id: string; /** * __Create an instance of a Model subclass.__ * * The instance will be registered for automatical snapshotting, and is assigned an [id]{@link Model#id}. * * Then it will call the user-defined [init()]{@link Model#init} method to initialize the instance, * passing the {@link options}. * * **Note:** When your model instance is no longer needed, you must [destroy]{@link Model#destroy} it. * Otherwise it will be kept in the snapshot forever. * * **Warning**: never create a Model instance using `new`, or override its constructor. See [above]{@link Model}. * * Example: * ``` * this.foo = FooModel.create({answer: 123}); * ``` * * @public * @param options - option object to be passed to [init()]{@link Model#init}. * There are no system-defined options as of now, you're free to define your own. */ static create<T extends typeof Model>(this: T, options?: any): InstanceType<T>; /** * __Registers this model subclass with Multisynq__ * * It is necessary to register all Model subclasses so the serializer can recreate their instances from a snapshot. * Also, the [session id]{@link Session.join} is derived by hashing the source code of all registered classes. * * **Important**: for the hashing to work reliably across browsers, be sure to specify `charset="utf-8"` for your `<html>` or all `<script>` tags. * * Example * ``` * class MyModel extends Multisynq.Model { * ... * } * MyModel.register("MyModel") * ``` * * @param classId Id for this model class. Must be unique. If you use the same class name in two files, use e.g. `"file1/MyModel"` and `"file2/MyModel"`. * @public */ static register(classId:ClassId): void; /** Static version of [wellKnownModel()]{@link Model#wellKnownModel} for currently executing model. * * This can be used to emulate static accessors, e.g. for lazy initialization. * * __WARNING!__ Do not store the result in a static variable. * Like any global state, that can lead to divergence. * * Will throw an error if called from outside model code. * * Example: * ``` * static get Default() { * let default = this.wellKnownModel("DefaultModel"); * if (!default) { * console.log("Creating default") * default = MyModel.create(); * default.beWellKnownAs("DefaultModel"); * } * return default; * } * ``` */ static wellKnownModel<M extends Model>(name: string): Model | undefined; /** * __Static declaration of how to serialize non-model classes.__ * * The Multisynq snapshot mechanism only knows about {@link Model} subclasses. * If you want to store instances of non-model classes in your model, override this method. * * `types()` needs to return an Object that maps _names_ to _class descriptions_: * - the name can be any string, it just has to be unique within your app * - the class description can either be just the class itself (if the serializer should * snapshot all its fields, see first example below), or an object with `write()` and `read()` methods to * convert instances from and to their serializable form (see second example below). * - the serialized form answered by `write()` can be almost anything. E.g. if it answers an Array of objects * then the serializer will be called for each of those objects. Conversely, these objects will be deserialized * before passing the Array to `read()`. * * The types only need to be declared once, even if several different Model subclasses are using them. * * __NOTE:__ This is currently the only way to customize serialization (for example to keep snapshots fast and small). * The serialization of Model subclasses themselves can not be customized. * * Example: To use the default serializer just declare the class:</caption> * ``` * class MyModel extends Multisynq.Model { * static types() { * return { * "SomeUniqueName": MyNonModelClass, * "THREE.Vector3": THREE.Vector3, // serialized as '{"x":...,"y":...,"z":...}' * "THREE.Quaternion": THREE.Quaternion, * }; * } * } * ``` * * Example: To define your own serializer, declare read and write functions: * ``` * class MyModel extends Multisynq.Model { * static types() { * return { * "THREE.Vector3": { * cls: THREE.Vector3, * write: v => [v.x, v.y, v.z], // serialized as '[...,...,...]' which is shorter than the default above * read: a => new THREE.Vector3(a[0], a[1], a[2]), * }, * "THREE.Color": { * cls: THREE.Color, * write: color => '#' + color.getHexString(), * read: state => new THREE.Color(state), * }, * }; * } * } * ``` * @public */ static types(): Record<ClassId, Class<any> | Serializer>; /** Find classes inside an external library * * This recursivley traverses a dummy object and gathers all object classes found. * Returns a mapping that can be returned from a Model's static `types()` method. * * This can be used to gather all internal class types of a third party library * that otherwise would not be accessible to the serializer * * Example: If `Foo` is a class from a third party library * that internally create a `Bar` instance, * this would find both classes * ``` * class Bar {} * class Foo { bar = new Bar(); } * static types() { * const sample = new Foo(); * return this.gatherClassTypes(sample, "MyLib"); * // returns { "MyLib.Foo": Foo, "MyLib.Bar": Bar } * } * ``` * @param {Object} dummyObject - an instance of a class from the library * @param {String} prefix - a prefix to add to the class names * @since 2.0 */ static gatherClassTypes<T extends Object>(dummyObject: T, prefix: string): Record<ClassId, Class<any>>; /** * This is called by [create()]{@link Model.create} to initialize a model instance. * * In your Model subclass this is the place to [subscribe]{@link Model#subscribe} to events, * or start a [future]{@link Model#future} message chain. * * **Note:** When your model instance is no longer needed, you must [destroy]{@link Model#destroy} it. * * @param options - there are no system-defined options, you're free to define your own * @public */ init(_options?: any, persistentData?: any): void; /** * Unsubscribes all [subscriptions]{@link Model#subscribe} this model has, * unschedules all [future]{@link Model#future} messages, * and removes it from future snapshots. * * Example: * ``` * removeChild(child) { * const index = this.children.indexOf(child); * this.children.splice(index, 1); * child.destroy(); * } * ``` * @public */ destroy(): void; /** * **Publish an event to a scope.** * * Events are the main form of communication between models and views in Multisynq. * Both models and views can publish events, and subscribe to each other's events. * Model-to-model and view-to-view subscriptions are possible, too. * * See [Model.subscribe]{@link Model#subscribe}() for a discussion of **scopes** and **event names**. * Refer to [View.subscribe]{@link View#subscribe}() for invoking event handlers *asynchronously* or *immediately*. * * Optionally, you can pass some **data** along with the event. * For events published by a model, this can be any arbitrary value or object. * See View's [publish]{@link View#publish} method for restrictions in passing data from a view to a model. * * If you subscribe inside the model to an event that is published by the model, * the handler will be called immediately, before the publish method returns. * If you want to have it handled asynchronously, you can use a future message: * `this.future(0).publish("scope", "event", data)`. * * Note that there is no way of testing whether subscriptions exist or not (because models can exist independent of views). * Publishing an event that has no subscriptions is about as cheap as that test would be, so feel free to always publish, * there is very little overhead. * * Example: * ``` * this.publish("something", "changed"); * this.publish(this.id, "moved", this.pos); * ``` * @param {String} scope see [subscribe]{@link Model#subscribe}() * @param {String} event see [subscribe]{@link Model#subscribe}() * @param {*=} data can be any value or object * @public */ publish<T>(scope: string, event: string, data?: T): void; /** * **Register an event handler for an event published to a scope.** * * Both `scope` and `event` can be arbitrary strings. * Typically, the scope would select the object (or groups of objects) to respond to the event, * and the event name would select which operation to perform. * * A commonly used scope is `this.id` (in a model) and `model.id` (in a view) to establish * a communication channel between a model and its corresponding view. * * You can use any literal string as a global scope, or use [`this.sessionId`]{@link Model#sessionId} for a * session-global scope (if your application supports multipe sessions at the same time). * The predefined events [`"view-join"`]{@link event:view-join} and [`"view-exit"`]{@link event:view-exit} * use this session scope. * * The handler must be a method of `this`, e.g. `subscribe("scope", "event", this.methodName)` will schedule the * invocation of `this["methodName"](data)` whenever `publish("scope", "event", data)` is executed. * * If `data` was passed to the [publish]{@link Model#publish} call, it will be passed as an argument to the handler method. * You can have at most one argument. To pass multiple values, pass an Object or Array containing those values. * Note that views can only pass serializable data to models, because those events are routed via a reflector server * (see [View.publish]{@link View#publish}). * * Example: * ``` * this.subscribe("something", "changed", this.update); * this.subscribe(this.id, "moved", this.handleMove); * ``` * Example: * ``` * class MyModel extends Multisynq.Model { * init() { * this.subscribe(this.id, "moved", this.handleMove); * } * handleMove({x,y}) { * this.x = x; * this.y = y; * } * } * class MyView extends Multisynq.View { * constructor(model) { * this.modelId = model.id; * } * onpointermove(evt) { * const x = evt.x; * const y = evt.y; * this.publish(this.modelId, "moved", {x,y}); * } * } * ``` * @param {String} scope - the event scope (to distinguish between events of the same name used by different objects) * @param {String} event - the event name (user-defined or system-defined) * @param {Function|String} handler - the event handler (must be a method of `this`, or the method name as string) * @return {this} * @public */ subscribe<T>( scope: string, event: string, handler: SubscriptionHandler<T>, ): void; /** * Unsubscribes this model's handler for the given event in the given scope. * @param {String} scope see [subscribe]{@link Model#subscribe} * @param {String} event see [subscribe]{@link Model#subscribe} * @param {Function=} handler - the event handler (if not given, all handlers for the event are removed) * @public */ unsubscribe<T>( scope: string, event: string, handler?: SubscriptionHandler<T>, ): void; /** * Unsubscribes all of this model's handlers for any event in any scope. * @public */ unsubscribeAll(): void; /** * Scope, event, and source of the currently executing subscription handler. * * The `source' is either `"model"` or `"view"`. * * ``` * // this.subscribe("*", "*", this.logEvents) * logEvents(data: any) { * const {scope, event, source} = this.activeSubscription!; * console.log(`${this.now()} Event in model from ${source} ${scope}:${event} with`, data); * } * ``` * @returns {Object} `{scope, event, source}` or `undefined` if not in a subscription handler. * @since 2.0 * @public */ get activeSubscription(): EventType | undefined; /** * **Schedule a message for future execution** * * Use a future message to automatically advance time in a model, * for example for animations. * The execution will be scheduled `tOffset` milliseconds into the future. * It will run at precisely `this.now() + tOffset`. * * Use the form `this.future(100).methodName(args)` to schedule the execution * of `this.methodName(args)` at time `this.now() + tOffset`. * * **Hint**: This would be an unusual use of `future()`, but the `tOffset` given may be `0`, * in which case the execution will happen asynchronously before advancing time. * This is the only way for asynchronous execution in the model since you must not * use Promises or async functions in model code (because a snapshot may happen at any time * and it would not capture those executions). * * **Note:** the recommended form given above is equivalent to `this.future(100, "methodName", arg1, arg2)` * but makes it more clear that "methodName" is not just a string but the name of a method of this object. * Also, this will survive minification. * Technically, it answers a [Proxy]{@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy} * that captures the name and arguments of `.methodName(args)` for later execution. * * See this [tutorial]{@tutorial 1_1_hello_world} for a complete example. * * Example: single invocation with two arguments * ``` * this.future(3000).say("hello", "world"); * ``` * Example: repeated invocation with no arguments * ``` * tick() { * this.n++; * this.publish(this.id, "count", {time: this.now(), count: this.n)}); * this.future(100).tick(); * } * ``` * @param {Number} tOffset - time offset in milliseconds, must be >= 0 * @returns {this} * @public */ future<T extends any[]>(tOffset?:number, method?: FutureHandler<T>, ...args: T): this; /** * **Cancel a previously scheduled future message** * * This unschedules the invocation of a message that was scheduled with [future]{@link Model#future}. * It is okay to call this method even if the message was already executed or if it was never scheduled. * * **Note:** as with [future]{@link Model#future}, the recommended form is to pass the method itself, * but you can also pass the name of the method as a string. * * @example * this.future(3000).say("hello", "world"); * ... * this.cancelFuture(this.say); * @param {Function} method - the method (must be a method of `this`) * @returns {Boolean} true if the message was found and canceled, false otherwise * @since 1.1.0-16 * @public */ cancelFuture<T extends any[]>(method: FutureHandler<T>): boolean; /** **Generate a synchronized pseudo-random number** * * This returns a floating-point, pseudo-random number in the range 0–1 (inclusive of 0, but not 1) * with approximately uniform distribution over that range * (just like [Math.random](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random)). * * Since the model computation is synchronized for every user on their device, the sequence of random numbers * generated must also be exactly the same for everyone. This method provides access to such a random number generator. */ random(): number; /** **The model's current time** * * Every [event handler]{@link Model#subscribe} and [future message]{@link Model#future} is run at a precisely defined moment * in virtual model time, and time stands still while this execution is happening. That means if you were to access this.now() * in a loop, it would never answer a different value. * * The unit of now is milliseconds (1/1000 second) but the value can be fractional, it is a floating-point value. */ now(): number; /** Make this model globally accessible under the given name. It can be retrieved from any other model in the same session * using [wellKnownModel()]{@link Model#wellKnownModel}. * * Hint: Another way to make a model well-known is to pass a name as second argument to [Model.create()]{@link Model#create}. * * Example: * ``` * class FooManager extends Multisynq.Model { * init() { * this.beWellKnownAs("UberFoo"); * } * } * class Underlings extends Multisynq.Model { * reportToManager(something) { * this.wellKnownModel("UberFoo").report(something); * } * } * ```*/ beWellKnownAs(name: string): void; /** Access a model that was registered previously using beWellKnownAs(). * * Note: The instance of your root Model class is automatically made well-known as `"modelRoot"` * and passed to the [constructor]{@link View#constructor} of your root View during [Session.join]{@link Session.join}. * * Example: * ``` * const topModel = this.wellKnownModel("modelRoot"); * ``` */ wellKnownModel<M extends Model>(name: string): Model | undefined; /** Look up a model in the current session given its `id`. * * Example: * ``` * const otherModel = this.getModel(otherId); * ``` */ getModel<M extends Model>(id: string): M | undefined; /** This methods checks if it is being called from a model, and throws an Error otherwise. * * Use this to protect some model code against accidentally being called from a view. * * Example: * ``` * get foo() { return this._foo; } * set foo(value) { this.modelOnly(); this._foo = value; } * ```*/ modelOnly(errorMessage?: string): boolean; /** * **Create a serializable function that can be stored in the model.** * * Plain functions can not be serialized because they may contain closures that can * not be introspected by the snapshot mechanism. This method creates a serializable * "QFunc" from a regular function. It can be stored in the model and called like * the original function. * * The function can only access global references (like classes), *all local * references must be passed in the `env` object*. They are captured * as constants at the time the QFunc is created. Since they are constants, * re-assignments will throw an error. * * In a fat-arrow function, `this` is bound to the model that called `createQFunc`, * even in a different lexical scope. It is okay to call a model's `createQFunc` from * anywhere, e.g. from a view. QFuncs can be passed from view to model as arguments * in `publish()` (provided their environment is serializable). * * **Warning:** Minification can change the names of local variables and functions, * but the env will still use the unminified names. You need to disable * minification for source code that creates QFuncs with env. Alternatively, you can * pass the function's source code as a string, which will not be minified. * * Behind the scenes, the function is stored as a string and compiled when needed. * The env needs to be constant because the serializer would not able to capture * the values if they were allowed to change. * * @example * const template = { greeting: "Hi there," }; * this.greet = this.createQFunc({template}, (name) => console.log(template.greeting, name)); * this.greet(this, "friend"); // logs "Hi there, friend" * template.greeting = "Bye now,"; * this.greet(this, "friend"); // logs "Bye now, friend" * * @param env - an object with references used by the function * @param func - the function to be wrapped, or a string with the function's source code * @returns a serializable function bound to the given environment * @public * @since 2.0 */ createQFunc<T extends Function>(env: QFuncEnv, func: T|string): T; createQFunc<T extends Function>(func: T|string): T; persistSession(func: () => any): void; /** **Identifies the shared session of all users** * * (as opposed to the [viewId]{@link View#viewId} which identifies the non-shared views of each user). * * The session id is used as "global" scope for events like [`"view-join"`]{@link event:view-join}. * * See {@link Session.join} for how the session id is generated. * * If your app has several sessions at the same time, each session id will be different. * * Example * ``` * this.subscribe(this.sessionId, "view-join", this.addUser); * ```*/ sessionId: string; /** **The number of users currently in this session.** * * All users in a session share the same Model (meaning all model objects) but each user has a different View * (meaning all the non-model state). This is the number of views currently sharing this model. * It increases by 1 for every [`"view-join"`]{@link event:view-join} * and decreases by 1 for every [`"view-exit"`]{@link event:view-exit} event. * * Example * ``` * this.subscribe(this.sessionId, "view-join", this.showUsers); * this.subscribe(this.sessionId, "view-exit", this.showUsers); * showUsers() { this.publish(this.sessionId, "view-count", this.viewCount); } * ```*/ viewCount: number; /** make module exports accessible via any subclass */ static Multisynq: Multisynq; } export type ViewLocation = { country: string; region: string; city?: { name: string; lat: number; lng: number; } } /** payload of view-join and view-exit if viewData was passed in Session.join */ export type ViewInfo<T> = { viewId: string; // set by reflector viewData: T; // passed in Session.join location?: ViewLocation; // set by reflector if enabled in Session.join }; export type ViewSubOptions = { handling?: "queued" | "oncePerFrame" | "immediate"; }; export class View extends PubSubParticipant<ViewSubOptions> { /** * A View instance is created in {@link Session.join}, and the root model is passed into its constructor. * * This inherited constructor does not use the model in any way. * Your constructor should recreate the view state to exactly match what is in the model. * It should also [subscribe]{@link View#subscribe} to any changes published by the model. * Typically, a view would also subscribe to the browser's or framework's input events, * and in response [publish]{@link View#publish} events for the model to consume. * * The constructor will, however, register the view and assign it an [id]{@link View#id}. * * **Note:** When your view instance is no longer needed, you must [detach]{@link View#detach} it. * Otherwise it will be kept in memory forever. * * @param {Model} model - the view's model * @public */ constructor(model: Model); /** * **Unsubscribes all [subscriptions]{@link View#subscribe} this model has, * and removes it from the list of views** * * This needs to be called when a view is no longer needed to prevent memory leaks. * * Example: * ``` * removeChild(child) { * const index = this.children.indexOf(child); * this.children.splice(index, 1); * child.detach(); * } * ``` * @public */ detach(): void; /** * **Publish an event to a scope.** * * Events are the main form of communication between models and views in Multisynq. * Both models and views can publish events, and subscribe to each other's events. * Model-to-model and view-to-view subscriptions are possible, too. * * See [Model.subscribe]{@link Model#subscribe} for a discussion of **scopes** and **event names**. * * Optionally, you can pass some **data** along with the event. * For events published by a view and received by a model, * the data needs to be serializable, because it will be sent via the reflector to all users. * For view-to-view events it can be any value or object. * * Note that there is no way of testing whether subscriptions exist or not (because models can exist independent of views). * Publishing an event that has no subscriptions is about as cheap as that test would be, so feel free to always publish, * there is very little overhead. * * Example: * ``` * this.publish("input", "keypressed", {key: 'A'}); * this.publish(this.model.id, "move-to", this.pos); * ``` * @param {String} scope see [subscribe]{@link Model#subscribe}() * @param {String} event see [subscribe]{@link Model#subscribe}() * @param {*=} data can be any value or object (for view-to-model, must be serializable) * @public */ publish<T>(scope: string, event: string, data?: T): void; /** * **Register an event handler for an event published to a scope.** * * Both `scope` and `event` can be arbitrary strings. * Typically, the scope would select the object (or groups of objects) to respond to the event, * and the event name would select which operation to perform. * * A commonly used scope is `this.id` (in a model) and `model.id` (in a view) to establish * a communication channel between a model and its corresponding view. * * Unlike in a model's [subscribe]{@link Model#subscribe} method, you can specify when the event should be handled: * - **Queued:** The handler will be called on the next run of the [main loop]{@link Session.join}, * the same number of times this event was published. * This is useful if you need each piece of data that was passed in each [publish]{@link Model#publish} call. * * An example would be log entries generated in the model that the view is supposed to print. * Even if more than one log event is published in one render frame, the view needs to receive each one. * * **`{ event: "name", handling: "queued" }` is the default. Simply specify `"name"` instead.** * * - **Once Per Frame:** The handler will be called only _once_ during the next run of the [main loop]{@link Session.join}. * If [publish]{@link Model#publish} was called multiple times, the handler will only be invoked once, * passing the data of only the last `publish` call. * * For example, a view typically would only be interested in the current position of a model to render it. * Since rendering only happens once per frame, it should subscribe using the `oncePerFrame` option. * The event typically would be published only once per frame anyways, however, * while the model is catching up when joining a session, this would be fired rapidly. * * **`{ event: "name", handling: "oncePerFrame" }` is the most efficient option, you should use it whenever possible.** * * - **Immediate:** The handler will be invoked _synchronously_ during the [publish]{@link Model#publish} call. * This will tie the view code very closely to the model simulation, which in general is undesirable. * However, if the view needs to know the exact state of the model at the time the event was published, * before execution in the model proceeds, then this is the facility to allow this without having to copy model state. * * Pass `{event: "name", handling: "immediate"}` to enforce this behavior. * * The `handler` can be any callback function. * Unlike a model's [handler]{@link Model#subscribe} which must be a method of that model, * a view's handler can be any function, including fat-arrow functions declared in-line. * Passing a method like in the model is allowed too, it will be bound to `this` in the subscribe call. * * Example: * ``` * this.subscribe("something", "changed", this.update); * this.subscribe(this.id, {event: "moved", handling: "oncePerFrame"}, pos => this.sceneObject.setPosition(pos.x, pos.y, pos.z)); * ``` * @tutorial 1_4_view_smoothing * @param {String} scope - the event scope (to distinguish between events of the same name used by different objects) * @param {String|Object} eventSpec - the event name (user-defined or system-defined), or an event handling spec object * @param {String} eventSpec.event - the event name (user-defined or system-defined) * @param {String} eventSpec.handling - `"queued"` (default), `"oncePerFrame"`, or `"immediate"` * @param {Function} handler - the event handler (can be any function) * @return {this} * @public */ subscribe( scope: string, eventSpec: | string | { event: string; handling: "queued" | "oncePerFrame" | "immediate" }, callback: (e: any) => void, ): void; /** * Unsubscribes this view's handler for the given event in the given scope. * @param {String} scope see [subscribe]{@link View#subscribe} * @param {String} event see [subscribe]{@link View#subscribe} * @param {Function=} handler - the event handler (if not given, all handlers for the event are removed) * @public */ unsubscribe<T>( scope: string, event: string, handler?: SubscriptionHandler<T>, ): void; /** * Unsubscribes all of this views's handlers for any event in any scope. * @public */ unsubscribeAll(): void; /** * Scope, event, and source of the currently executing subscription handler. * * The `source' is either `"model"` or `"view"`. *` * ``` * // this.subscribe("*", "*", this.logEvents) * logEvents(data: any) { * const {scope, event, source} = this.activeSubscription; * console.log(`Event in view from ${source} ${scope}:${event} with`, data); * } * ``` * @returns {Object} `{scope, event, source}` or `undefined` if not in a subscription handler. * @since 2.0 * @public */ get activeSubscription(): EventType | undefined; /** * The ID of the view. * @public */ viewId: string; /** **Schedule a message for future execution** * This method is here for symmetry with [Model.future]{@link Model#future}. * * It simply schedules the execution using `window.setTimeout`. * The only advantage to using this over setTimeout() is consistent style. */ future(tOffset: number): this; /** **Answers `Math.random()`** * * This method is here purely for symmetry with [Model.random]{@link Model#random}. */ random(): number; /** **The model's current time** * * This is the time of how far the model has been simulated. * Normally this corresponds roughly to real-world time, since the reflector is generating * time stamps based on real-world time. * * If there is [backlog]{@link View#externalNow} however (e.g while a newly joined user is catching up), * this time will advance much faster than real time. * * The unit is milliseconds (1/1000 second) but the value can be fractional, it is a floating-point value. * * Returns: the model's time in milliseconds since the first user created the session. */ now(): number; /** **The latest timestamp received from reflector** * * Timestamps are received asynchronously from the reflector at the specified tick rate. * [Model time]{@View#now} however only advances synchronously on every iteration of the [main loop]{@link Session.join}. * Usually `now == externalNow`, but if the model has not caught up yet, then `now < externalNow`. * * We call the difference "backlog". If the backlog is too large, Multisynq will put an overlay on the scene, * and remove it once the model simulation has caught up. The `"synced"` event is sent when that happens. * * The `externalNow` value is rarely used by apps but may be useful if you need to synchronize views to real-time. * * Example: * ``` * const backlog = this.externalNow() - this.now(); * ``` */ externalNow(): number; /** * **The model time extrapolated beyond latest timestamp received from reflector** * * Timestamps are received asynchronously from the reflector at the specified tick rate. * In-between ticks or messages, neither [now()]{@link View#now} nor [externalNow()]{@link View#externalNow} advances. * `extrapolatedNow` is `externalNow` plus the local time elapsed since that timestamp was received, * so it always advances. * * `extrapolatedNow()` will always be >= `now()` and `externalNow()`. * However, it is only guaranteed to be monotonous in-between time stamps received from the reflector * (there is no "smoothing" to reconcile local time with reflector time). */ extrapolatedNow(): number; /** Called on the root view from [main loop]{@link Session.join} once per frame. Default implementation does nothing. * * Override to add your own view-side input polling, rendering, etc. * * If you want this to be called for other views than the root view, * you will have to call those methods from the root view's `update()`. * * The time received is related to the local real-world time. If you need to access the model's time, use [this.now()]{@link View#now}. */ update(time: number): void; /** Access a model that was registered previously using beWellKnownAs(). * * Note: The instance of your root Model class is automatically made well-known as `"modelRoot"` * and passed to the [constructor]{@link View#constructor} of your root View during [Session.join]{@link Session.join}. * * Example: * ``` * const topModel = this.wellKnownModel("modelRoot"); * ``` */ wellKnownModel<M extends Model>(name: string): Model | undefined; /** Access the session object. * * Note: The view instance may be taken down and reconstructed during the lifetime of a session. the `view` property of the session may differ from `this`, when you store the view instance in our data structure outside of Multisynq and access it sometime later. * @public */ get session(): MultisynqSession<View>; /** make module exports accessible via any subclass */ static Multisynq: Multisynq; } export type MultisynqSession<V extends View> = { id: string; view: V; step: (time: number) => void; leave: () => Promise<void>; data: { fetch: (handle: DataHandle) => Promise<ArrayBuffer>; store: ( data: ArrayBuffer, options?: { shareable?: boolean; keep?: boolean }, ) => Promise<DataHandle>; toId: (handle: DataHandle) => string; fromId: (id: string) => DataHandle; }; }; export type MultisynqModelOptions = object; export type MultisynqViewOptions = object; export type MultisynqDebugOption = | "session" | "messages" | "sends" | "snapshot" | "data" | "hashing" | "subscribe" | "publish" | "events" | "classes" | "ticks" | "write" | "offline"; type ClassOf<M> = new (...args: any[]) => M; export type MultisynqSessionParameters<M extends Model, V extends View, T> = { apiKey?: string; appId: string; name?: string | Promise<string>; password?: string | Promise<string>; model: ClassOf<M>; view?: ClassOf<V>; options?: MultisynqModelOptions; viewOptions?: MultisynqViewOptions; viewData?: T; location?: boolean; step?: "auto" | "manual"; tps?: number | string; autoSleep?: number | boolean; rejoinLimit?: number; eventRateLimit?: number; reflector?: string; files?: string; box?: string; debug?: MultisynqDebugOption | Array<MultisynqDebugOption>; }; export namespace Session { function join<M extends Model, V extends View, T>( parameters: MultisynqSessionParameters<M, V, T>, ): Promise<MultisynqSession<V>>; } export var Constants: Record<string, any>; export const VERSION: string; interface IApp { sessionURL: string; root: HTMLElement | null; sync: boolean; messages: boolean; badge: boolean; stats: boolean; qrcode: boolean; makeWidgetDock(options?: { debug?: boolean; iframe?: boolean; badge?: boolean; qrcode?: boolean; stats?: boolean; alwaysPinned?: boolean; fixedSize?: boolean; }): void; makeSessionWidgets(sessionId: string): void; makeQRCanvas(options?: { text?: string; width?: number; height?: number; colorDark?: string; colorLight?: string; correctLevel?: "L" | "M" | "Q" | "H"; }): HTMLCanvasElement | null; clearSessionMoniker(): void; showSyncWait(bool: boolean): void; messageFunction( msg: string, options?: { duration?: number; gravity?: "bottom" | "top"; position?: "right" | "left" | "center" | "bottom"; backgroundColor?: string; stopOnFocus?: boolean; }, ): void; showMessage(msg: string, options?: Record<string, unknown>): void; isMultisynqHost(hostname: string): boolean; referrerURL(): string; autoSession: (options?: { key?: string; force?: boolean; default?: string; keyless?: boolean; }) => Promise<string>; autoPassword: (options?: { key?: string; default?: string; force?: boolean; scrub?: boolean; keyless?: boolean; }) => Promise<string>; randomSession: (len?: number) => string; randomPassword: (len?: number) => string; } /** * The App API is under construction. * * @public */ export var App:IApp; interface DataHandle { store( sessionId: string, data: string | ArrayBuffer, keep?: boolean, ): Promise<DataHandle>; fetch(sessionid: string, handle: DataHandle): string | ArrayBuffer; hash(data: unknown, output?: string): string; } /** * The Data API is under construction. * * @public */ export var Data: DataHandle; type Multisynq = { Model: typeof Model; View: typeof View; Session: typeof Session; Data: DataHandle; Constants: typeof Constants; App: IApp; }; }