UNPKG

lib0

Version:

isomorphic utility functions

324 lines (297 loc) • 11.6 kB
import * as s from '../../schema.js' import * as error from '../../error.js' import * as delta from '../delta.js' import * as fingerprintTrait from '../../trait/fingerprint.js' /** * @template {import('../delta.js').DeltaConf} [A={}] * @template {import('../delta.js').DeltaConf} [B={}] */ class TransformResult { /** * @param {import('../delta.js').DeltaBuilder<A>?} a * @param {import('../delta.js').DeltaBuilder<B>?} b */ constructor (a, b) { /** * @type {import('../delta.js').DeltaBuilder<A>?} */ this.a = a /** * @type {import('../delta.js').DeltaBuilder<B>?} */ this.b = b } isEmpty () { return this.a == null && this.b == null } clear () { this.a = null this.b = null } /** * @param {import('../delta.js').DeltaBuilder<A>?} a */ applyA (a) { if (a !== null) { if (this.a == null) { this.a = a // adopt: `a` is donated to this result (callers must not reuse it) } else { this.a.apply(a, { move: true }) // ...so the merge may consume `a` rather than freeze-copy it } } return this } /** * @param {import('../delta.js').DeltaBuilder<B>?} b */ applyB (b) { if (b !== null) { if (this.b == null) { this.b = b // adopt: `b` is donated to this result (callers must not reuse it) } else { this.b.apply(b, { move: true }) // ...so the merge may consume `b` rather than freeze-copy it } } return this } reverse () { return new TransformResult(this.b, this.a) } } /** * @typedef {TransformResult<any,any>} TransformResultAny */ /** * @template {import('../delta.js').DeltaConf} A * @template {import('../delta.js').DeltaConf} B * @param {s.Schema<import('../delta.js').Delta<A>>} $a * @param {s.Schema<import('../delta.js').Delta<B>>} $b * @return {s.Schema<TransformResult<A,B>>} */ export const $tresult = ($a, $b) => /** @type {any} */ (s.$instanceOf(TransformResult, tr => (tr.a === null || $a.check(tr.a)) && (tr.b === null || $b.check(tr.b)))) /** * Build a {@link TransformResult}. Exposed so transformers in `./` can construct their results. * * @template {import('../delta.js').DeltaBuilderAny} [DeltaA=import('../delta.js').DeltaBuilderAny] * @template {import('../delta.js').DeltaBuilderAny} [DeltaB=import('../delta.js').DeltaBuilderAny] * @param {DeltaA?} a * @param {DeltaB?} b */ export const createTransformResult = (a, b) => new TransformResult(a, b) /** * A stateful, schema-bound mapper between changes of side A and side B, materialized from a * {@link Template} via `init()`. * * ## Deltas are manipulated in place * * `apply`/`applyA`/`applyB` **mutate their input deltas directly** — they rebase nodes and splice op * lists in place, and some transformers `apply(other, { move: true })` the input into an accumulator — * rather than editing only through the copy-on-write `apply`/`rebase` surface. This is a deliberate * exception to the general rule (a delta is normally edited only via `apply`/`rebase`, which re-clone * frozen content on first touch); it keeps the transform allocation-free, but it means a transformer is * an *owning* consumer of the delta it is given. **Callers must pass a privately-owned, fully-mutable * delta** — never a shared or `done` (frozen) one. A caller holding a shared read delta must * {@link import('../delta.js').cloneDeep deep-clone} it first (this is exactly what a * {@link import('../rdt.js').Binding} does before routing a change through its transformer). * * @template {import('../delta.js').DeltaConf} A * @template {import('../delta.js').DeltaConf} B */ export class Transformer { /** * @param {s.Schema<import('../delta.js').Delta<A>>} $in input delta schema * @param {s.Schema<import('../delta.js').Delta<B>>} $out output delta schema (computed by the factory) */ constructor ($in, $out) { /** * @type {s.Schema<import('../delta.js').Delta<A>>} */ this.$in = $in /** * @type {s.Schema<import('../delta.js').Delta<B>>} */ this.$out = $out } /** * @param {TransformResult<A, B>} tin * @return {TransformResult<A,B>} */ apply (tin) { const ta = tin.a const tb = tin.b const ares = ta != null ? this.applyA(ta) : createTransformResult(null, null) // transform tb if necessary if (tb != null) { if (ares.b != null) { tb.rebase(ares.b, false) } const bres = this.applyB(tb) // a fresh, single-use result -> its sides may be consumed (moved) if (ares.a) { ares.a.apply(bres.a, { move: true }) } else { ares.a = bres.a } if (ares.b) { ares.b.apply(bres.b, { move: true }) } else { ares.b = bres.b } } return ares } /** * @param {import('../delta.js').DeltaBuilder<A>} t * @return {TransformResult<A,B>} */ applyA (t) { return this.apply(createTransformResult(t, null)) } /** * @param {import('../delta.js').DeltaBuilder<B>} t * @return {TransformResult<A,B>} */ applyB (t) { return this.apply(createTransformResult(null, t)) } } /** * Recognize a {@link Transformer} by its `$type` tag. Every value is either a `Transformer` or a * {@link Template}, never both, so the two roles can each own a single prototype `$type`. * * @type {s.Schema<Transformer<any,any>>} */ export const $transformer = /** @type {s.Schema<Transformer<any,any>>} */ (Transformer.prototype.$type = s.$type('d:transformer', Transformer)) /** * Like {@link $transformer}, but carries the side-A / side-B delta types for typechecking; the two * schema params do not constrain the runtime check. * * @template {import('../delta.js').DeltaConf} A * @template {import('../delta.js').DeltaConf} B * @param {s.Schema<import('../delta.js').Delta<A>>|A} _a * @param {s.Schema<import('../delta.js').Delta<B>>|A} _b * @return {s.Schema<Transformer<A,B>>} */ /* @__NO_SIDE_EFFECTS__ */ export const transformerWith = (_a, _b) => /** @type {s.Schema<Transformer<A,B>>} */ ($transformer) /** * A function from an input delta schema to a {@link Template} — the schema-deferred form of a * transformer factory. Used for `pipe` stages, recursive composition, and `bind`. * * @template {import('../delta.js').DeltaConf} In * @template {import('../delta.js').DeltaConf} Out * @typedef {($in: s.Schema<import('../delta.js').Delta<In>>) => Template<In, Out>} TemplateFactory */ /** * Force a factory's computed output conf to display fully-resolved (flatten `Omit<…> & {…}` / bare * alias artifacts) while staying a valid `DeltaConf` for use as a `Transformer`/`Delta` type * argument — conf-aware and depth-limited (nested `Delta<…>` stay intact). Implemented as an * overwrite-with-nothing, reusing {@link import('../delta.js').DeltaConfOverwrite}'s * prettify+coerce recipe. Apply to every factory's `OUT` typedef whose result is not already * produced by `DeltaConfOverwrite`. * * @template {import('../delta.js').DeltaConf} Conf * @typedef {import('../delta.js').DeltaConfOverwrite<Conf, {}>} ResolveOut */ /** * The reusable, schema-bound form of a transformer: built by a factory (`attr($d, …)`), it holds the * input/output delta schemas (`$in`/`$out`) and materializes a fresh stateful {@link Transformer} via * the parameterless `init()`. Concrete templates `extend Template`. * * Templates are {@link fingerprintTrait.Fingerprintable}: this is what lets them be embedded as values * (holes) in a `delta.create(...)` projection spec without collapsing the spec's type. * * @template {import('../delta.js').DeltaConf} [IN=any] * @template {import('../delta.js').DeltaConf} [OUT=any] */ export class Template { /** * @param {s.Schema<import('../delta.js').Delta<IN>>} $in input delta schema * @param {s.Schema<import('../delta.js').Delta<OUT>>} $out output delta schema (computed by the factory) */ constructor ($in, $out) { /** * @type {s.Schema<import('../delta.js').Delta<IN>>} */ this.$in = $in /** * @type {s.Schema<import('../delta.js').Delta<OUT>>} */ this.$out = $out } /** * Stable, mangle-safe identifier folded into this template's fingerprint. Subclasses with * configuration override it so that e.g. `attr('a')` and `attr('b')` fingerprint differently * (do NOT use `constructor.name` — class names do not survive mangling). * * @return {string} */ get name () { return 'lib0:template' } /** * @return {string} */ [fingerprintTrait.FingerprintTraitSymbol] () { return this.name } /** * Instantiate a fresh stateful {@link Transformer} bound to this template's schema. * * @return {Transformer<IN, OUT>} */ /* c8 ignore next 3 */ init () { return error.methodUnimplemented() } } /** * Bind an input schema into the `$d => Template` lambda pattern (the shape used by {@link pipe} * stages, {@link import('./children.js').children} handlers, and `bind`) at the top level: calls * `fn` with `$d` and returns its {@link Template}. Lets you write `transform($d, $d => pipe($d, …))` * — keeping every template construction in the same lambda style — without a separate `const $d` * statement. The lambda's `$d` parameter is typed as the input delta schema, and the concrete * template subtype `fn` returns is preserved in the result. * * @template {import('../delta.js').DeltaConf} IN * @template {Template<IN, any>} T * @param {s.Schema<import('../delta.js').Delta<IN>>} $d * @param {($d: s.Schema<import('../delta.js').Delta<IN>>) => T} fn * @return {T} */ export const transform = ($d, fn) => fn($d) /** * Rebuild `$in`'s delta schema with a replaced attrs-shape map (keeping name/children/text). Used by * the attr-renaming/filtering factories to compute a concrete output schema (`$out`). Falls back to * `$deltaAny` when `$in` is loose (not a concrete `$delta(...)`). * * @param {s.Schema<import('../delta.js').DeltaAny>} $in * @param {{[k:string|number]: s.Schema<any>}} newAttrsShape * @return {s.Schema<import('../delta.js').DeltaAny>} */ export const withAttrs = ($in, newAttrsShape) => delta.$$delta.check($in) ? new delta.$Delta($in.shape.$name, s.$object(newAttrsShape), $in.shape.$children, $in.shape.hasText, false, $in.shape.$formats) : delta.$deltaAny /** * Rebuild `$in`'s delta schema with a replaced node `name` (keeping attrs/children/text). Falls back * to `$deltaAny` when `$in` is loose. * * @param {s.Schema<import('../delta.js').DeltaAny>} $in * @param {string} name * @return {s.Schema<import('../delta.js').DeltaAny>} */ export const withName = ($in, name) => delta.$$delta.check($in) ? new delta.$Delta(s.$(name), $in.shape.$attrs, $in.shape.$children, $in.shape.hasText, false, $in.shape.$formats) : delta.$deltaAny /** * The `{ attrKey: Schema }` map of `$in`'s attrs, or `null` when `$in` is loose / has no object attrs * schema. Lets attr-renaming/filtering factories compute a concrete `$out`. * * @param {s.Schema<import('../delta.js').DeltaAny>} $in * @return {{[k:string|number]: s.Schema<any>}?} */ export const attrsShapeOf = ($in) => delta.$$delta.check($in) && s.$$object.check($in.shape.$attrs) ? /** @type {{[k:string|number]: s.Schema<any>}} */ ($in.shape.$attrs.shape) : null /** * Recognize a {@link Template} by its `$type` tag. Used to detect transformer "holes" embedded in a * `project` spec. * * @type {s.Schema<Template>} */ export const $template = /** @type {s.Schema<Template>} */ (Template.prototype.$type = s.$type('d:template', Template))