UNPKG

@multisynq/react

Version:

React bindings for Multisynq

126 lines (106 loc) 4.94 kB
import { Model, SubscriptionHandler, FutureHandler } from '@multisynq/client' // TODO: import from @multisynq/client export interface ViewInfo<T> { viewId: string viewData?: T } export class ReactModel<T = unknown> extends Model { __reactEvents: { scope: string; event: string }[] = [] __views: Map<string, ViewInfo<T>> | null = null init(options: any) { const { trackViews, ...opts } = options super.init(opts) this.__reactEvents = [] // We don't want to track the joined views in every model. // the trackViews option enables this behavior if (trackViews === true) { this.__views = new Map() } // Subscribe directly via super since we don't need to publish react-updated super.subscribe(this.sessionId, 'view-join', this.__viewJoin) super.subscribe(this.sessionId, 'view-exit', this.__viewExit) } // The view-join event provides either a `viewId` string, or if viewData was // specified, a ViewInfo<T> object private __viewJoin(viewIdOrInfo: string | ViewInfo<T>) { const viewInfo: ViewInfo<T> = typeof viewIdOrInfo !== 'string' ? viewIdOrInfo : { viewId: viewIdOrInfo } const viewId = viewInfo.viewId if (this.__views) { this.__views.set(viewId, viewInfo) this.publish(this.sessionId, 'views-updated') } this.handleViewJoin(viewId, viewInfo) } // The view-exit event provides either a `viewId` string, or if viewData was // specified, a ViewInfo<T> object private __viewExit(viewIdOrInfo: string | ViewInfo<T>) { const viewId = typeof viewIdOrInfo === 'string' ? viewIdOrInfo : viewIdOrInfo.viewId const viewInfo = this.__views?.get(viewId) ?? { viewId } if (this.__views) { this.__views.delete(viewId) this.publish(this.sessionId, 'views-updated') } this.handleViewExit(viewId, viewInfo) } // Override these methods to add custom handling handleViewJoin(viewId: string, viewInfo: ViewInfo<T>) {} // eslint-disable-line @typescript-eslint/no-unused-vars handleViewExit(viewId: string, viewInfo: ViewInfo<T>) {} // eslint-disable-line @typescript-eslint/no-unused-vars // Public function to assert that users do not subscribe directly to view-join/exit events subscribe<T>(scope: string, event: string, handler: SubscriptionHandler<T>): void { if (event === 'view-join' || event === 'view-exit') { throw new Error( `In @multisynq/react you cannot directly subscribe to ${event}.\n` + `Override ${event === 'view-join' ? 'handleViewJoin' : 'handleViewExit'} instead\n` ) } this.__subscribe(scope, event, handler) } // This function is the one that performs the subscription logic, without performing any checks. // It's separate from the one above since we need to directly subscribe to view-join/exit events private __subscribe<T>(scope: string, event: string, handler: SubscriptionHandler<T>): void { this.__reactEvents.push({ scope, event }) // normalize handler into string or QFunc if (typeof handler === 'function') { const model = this as any; // if the handler is a method of the model, use the method name if (model[handler.name] === handler) handler = handler.name // otherwise, we assume that the handler is a QFunc } // we call the original handler, and then publish a react-updated event // We pass as a javascript string to survive minification const reactHandler = this.createQFunc<(e: any) => void>({handler}, `(data) => { if (typeof handler === 'function') handler(data) else this[handler](data) this.publish(this.sessionId, 'react-updated') }`) super.subscribe(scope, event, reactHandler) } // Function that helps ReactModel publish a react-updated event // every time a future message is executed __future_wrapper(methodName: keyof this, ...args: any[]) { const value = this[methodName] if (typeof value === 'function') { value.apply(this, args) this.publish(this.sessionId, 'react-updated') } else { console.error(`${methodName as string} is not a function of ${this}`) } } future<T extends any[]>(tOffset: number, method?: FutureHandler<T>, ...args: T): this { // non-proxy case: we schedule the call to a __future_wrapper function // that will call method, and then publish a react-updated event if (method !== undefined) { return super.future(tOffset, '__future_wrapper', method, ...args) } // We want to allow for the `this.future(tOffset).methodName(...args) syntax // To do so, we return a Proxy that invokes this function // with all the arguments for the non-proxy case return new Proxy(this, { // eslint-disable-next-line @typescript-eslint/no-unused-vars get(target, prop, _) { return (...args: any[]) => target.future(tOffset, prop as string, ...args) }, }) } } ReactModel.register('ReactModel')