UNPKG

@player-ui/player

Version:

269 lines (223 loc) 7.37 kB
import { SyncHook } from "tapable-ts"; import type { BindingLike, BindingFactory } from "../binding"; import { BindingInstance, isBinding } from "../binding"; import { NOOP_MODEL } from "./noop-model"; export const ROOT_BINDING = new BindingInstance([]); export type BatchSetTransaction = [BindingInstance, any][]; export type Updates = Array<{ /** The updated binding */ binding: BindingInstance; /** The old value */ oldValue: any; /** The new value */ newValue: any; /** Force the Update to be included even if no data changed */ force?: boolean; }>; /** Options to use when getting or setting data */ export interface DataModelOptions { /** * The data (either to set or get) should represent a formatted value * For setting data, the data will be de-formatted before continuing in the pipeline * For getting data, the data will be formatted before returning */ formatted?: boolean; /** * By default, fetching data will ignore any invalid data. * You can choose to grab the queued invalid data if you'd like * This is usually the case for user-inputs */ includeInvalid?: boolean; /** * A flag to set to ignore any default value in the schema, and just use the raw value */ ignoreDefaultValue?: boolean; /** * A flag to indicate that this update should happen silently */ silent?: boolean; /** Other context associated with this request */ context?: { /** The data model to use when getting other data from the context of this request */ model: DataModelWithParser; }; } export interface DataModelWithParser<Options = DataModelOptions> { get(binding: BindingLike, options?: Options): any; set(transaction: [BindingLike, any][], options?: Options): Updates; delete(binding: BindingLike, options?: Options): void; } export interface DataModelImpl<Options = DataModelOptions> { get(binding: BindingInstance, options?: Options): any; set(transaction: BatchSetTransaction, options?: Options): Updates; delete(binding: BindingInstance, options?: Options): void; } export interface DataModelMiddleware { /** The name of the middleware */ name?: string; set( transaction: BatchSetTransaction, options?: DataModelOptions, next?: DataModelImpl, ): Updates; get( binding: BindingInstance, options?: DataModelOptions, next?: DataModelImpl, ): any; delete?( binding: BindingInstance, options?: DataModelOptions, next?: DataModelImpl, ): void; reset?(): void; } /** Wrap the inputs of the DataModel with calls to parse raw binding inputs */ export function withParser<Options = unknown>( model: DataModelImpl<Options>, parseBinding: BindingFactory, ): DataModelWithParser<Options> { /** Parse something into a binding if it requires it */ function maybeParse( binding: BindingLike, readOnly: boolean, ): BindingInstance { const parsed = isBinding(binding) ? binding : parseBinding(binding, { get: model.get, set: model.set, readOnly, }); if (!parsed) { throw new Error("Unable to parse binding"); } return parsed; } return { get(binding, options?: Options) { return model.get(maybeParse(binding, true), options); }, set(transaction, options?: Options) { return model.set( transaction.map(([key, val]) => [maybeParse(key, false), val]), options, ); }, delete(binding, options?: Options) { return model.delete(maybeParse(binding, false), options); }, }; } /** Wrap a middleware instance in a DataModel compliant API */ export function toModel( middleware: DataModelMiddleware, defaultOptions?: DataModelOptions, next?: DataModelImpl, ): DataModelImpl { if (!next) { return middleware as DataModelImpl; } return { get: (binding: BindingInstance, options?: DataModelOptions) => { const resolvedOptions = options ?? defaultOptions; if (middleware.get) { return middleware.get(binding, resolvedOptions, next); } return next?.get(binding, resolvedOptions); }, set: (transaction: BatchSetTransaction, options?: DataModelOptions) => { const resolvedOptions = options ?? defaultOptions; if (middleware.set) { return middleware.set(transaction, resolvedOptions, next); } return next?.set(transaction, resolvedOptions); }, delete: (binding: BindingInstance, options?: DataModelOptions) => { const resolvedOptions = options ?? defaultOptions; if (middleware.delete) { return middleware.delete(binding, resolvedOptions, next); } return next?.delete(binding, resolvedOptions); }, }; } export type DataPipeline = Array<DataModelMiddleware | DataModelImpl>; /** * Given a set of steps in a pipeline, create the effective data-model */ export function constructModelForPipeline( pipeline: DataPipeline, ): DataModelImpl { if (pipeline.length === 0) { return NOOP_MODEL; } if (pipeline.length === 1) { return toModel(pipeline[0]); } /** Default and propagate the options into the nested calls */ function createModelWithOptions(options?: DataModelOptions) { const model: DataModelImpl = pipeline.reduce<DataModelImpl | undefined>( (nextModel, middleware) => toModel(middleware, options, nextModel), undefined, ) ?? NOOP_MODEL; return model; } return { get: (binding: BindingInstance, options?: DataModelOptions) => { return createModelWithOptions(options)?.get(binding, options); }, set: (transaction, options) => { return createModelWithOptions(options)?.set(transaction, options); }, delete: (binding, options) => { return createModelWithOptions(options)?.delete(binding, options); }, }; } /** A DataModel that manages middleware data handlers */ export class PipelinedDataModel implements DataModelImpl { private pipeline: DataPipeline; private effectiveDataModel: DataModelImpl; public readonly hooks = { onSet: new SyncHook<[BatchSetTransaction]>(), }; constructor(pipeline: DataPipeline = []) { this.pipeline = pipeline; this.effectiveDataModel = constructModelForPipeline(this.pipeline); } public setMiddleware(handlers: DataPipeline) { this.pipeline = handlers; this.effectiveDataModel = constructModelForPipeline(handlers); } public addMiddleware(handler: DataModelMiddleware) { this.pipeline = [...this.pipeline, handler]; this.effectiveDataModel = constructModelForPipeline(this.pipeline); } public reset(model = {}) { this.pipeline.forEach((middleware) => { if ("reset" in middleware) { middleware.reset?.(); } }); this.set([[ROOT_BINDING, model]]); } public set( transaction: BatchSetTransaction, options?: DataModelOptions, ): Updates { const appliedTransaction = this.effectiveDataModel.set( transaction, options, ); this.hooks.onSet.call(transaction); return appliedTransaction; } public get(binding: BindingInstance, options?: DataModelOptions): any { return this.effectiveDataModel.get(binding, options); } public delete(binding: BindingInstance, options?: DataModelOptions): void { return this.effectiveDataModel.delete(binding, options); } }