UNPKG

enso

Version:

Maximalist state & form management library for React

520 lines (451 loc) 16 kB
"use strict"; exports.coreChangesBits = exports.childChangesShift = exports.childChangesMask = exports.childChange = exports.changesBits = exports.change = exports.atomChangesMask = exports.atomChange = exports.ChangesEvent = void 0; exports.isolateAtomChanges = isolateAtomChanges; exports.isolateChildChanges = isolateChildChanges; exports.isolateSubtreeChanges = isolateSubtreeChanges; exports.maskedChanges = maskedChanges; exports.metaAtomChangesMask = void 0; exports.metaChanges = metaChanges; exports.metaSubtreeChangesMask = exports.metaChildChangesMask = exports.metaChangesMask = exports.metaChangesBits = void 0; exports.shapeChanges = shapeChanges; exports.shiftChildChanges = shiftChildChanges; exports.structuralAtomChangesMask = void 0; exports.structuralChanges = structuralChanges; exports.subtreeChangesShift = exports.subtreeChangesMask = exports.subtreeChange = exports.structuralSubtreeChangesMask = exports.structuralChildChangesMask = exports.structuralChangesMask = exports.structuralChangesBits = void 0; /** * @module change * * State changes module. It defines event types, changes bit maps and helpers * that help to identify the changes. */ //#region Events /** * Changes event class. It extends the native `Event` class and adds * the `changes` property to it. */ class ChangesEvent extends Event { /** Current batch of changes. It collects all changes that happened during * the batch and allows to dispatch them as a single event for each target. * @internal */ static #batch; /** * Batches changes events and dispatches them as a single event for each * target after callstack is empty. * * @param target - Event target to dispatch the changes to. * @param changes - Changes to dispatch. */ static batch(target, changes) { const currentContext = {}; for (const ctx of this.#context) { Object.assign(currentContext, ctx); } // Create a new batch if it doesn't exist. if (!this.#batch) this.#batch = new Map(); const [batchedChanges, batchedContext] = this.#batch.get(target) || [0n, {}]; let newChanges = batchedChanges | changes; // Cancel out opposite changes // valid/invalid if (changes & atomChange.valid && batchedChanges & atomChange.invalid) newChanges &= ~atomChange.invalid; if (changes & atomChange.invalid && batchedChanges & atomChange.valid) newChanges &= ~atomChange.valid; // attach/detach if (changes & atomChange.attach && batchedChanges & atomChange.detach) newChanges &= ~atomChange.detach; if (changes & atomChange.detach && batchedChanges & atomChange.attach) newChanges &= ~atomChange.attach; const newContext = Object.assign({}, batchedContext, currentContext); this.#batch.set(target, [newChanges, newContext]); queueMicrotask(() => { // Check if the batch was consumed if (!this.#batch) return; // Clear the batch so when new events are chain-batched, they will // postpone until the next microtask. const batch = this.#batch; this.#batch = undefined; // Dispatch currently batched events batch.forEach(([changes, context], target) => target.dispatchEvent(new ChangesEvent(changes, context))); }); } /** Stack of contexts. * @internal */ static #context = []; /** * Provides context for the changes events. Nesting contexts will result * in the context being merged. * * @param context - Context to assign. */ static context(context, callback) { this.#context.push(context); const result = callback(); // TODO: Add tests if (result instanceof Promise) { // @ts-ignore TODO: return result.finally(() => { this.#context.pop(); }); } this.#context.pop(); // @ts-ignore TODO: return result; } /** Changes bitmask. */ /** Context record. */ context = {}; /** * Creates a new changes event. * * @param changes - Changes that happened. */ constructor(changes, context) { super("change"); // TODO: Join context building with the batched context so it's computed // in the same place. // TODO: Add tests if (!context) { context = {}; for (const ctx of ChangesEvent.#context) { Object.assign(context, ctx); } } Object.assign(this.context, context); this.changes = changes; } } //#endregion //#region Changes //#region Bitmask /** * Atom change type. It aliases the `bigint` type to represent the changes * type. */ // TODO: Consider making it opaque. exports.ChangesEvent = ChangesEvent; /** * Structural changes bits. It defines the allocate size for the structural * changes. It allows to access the structural changes bits range. */ const structuralChangesBits = exports.structuralChangesBits = 8n; /** * Meta changes bits. It defines the allocate size for the meta changes. It * allows to access the meta changes bits range which goes after the structural * changes. */ const metaChangesBits = exports.metaChangesBits = 8n; /** * Allocated change bits. */ const changesBits = exports.changesBits = structuralChangesBits + metaChangesBits; /** * Allocated core changes bits. It defines the allocated bits for the core * changes. It allows extensions to allocate additional bits for their own * changes after the core changes. */ const coreChangesBits = exports.coreChangesBits = changesBits * 3n; /** * Atom changes mask. It allows to isolate the atom changes bits. */ const atomChangesMask = exports.atomChangesMask = 2n ** changesBits - 1n; /** * Structural atom changes mask. It allows to isolate the structural atom * changes bits. */ const structuralAtomChangesMask = exports.structuralAtomChangesMask = 2n ** structuralChangesBits - 1n; /** * Meta atom changes mask. It allows to isolate the meta atom changes bits. */ const metaAtomChangesMask = exports.metaAtomChangesMask = 2n ** metaChangesBits - 1n << structuralChangesBits; /** * Bit index used to generate the next bit in the sequence. * * @private */ let bitIdx = 0n; /** * Atom changes map. * * It represents the changes for the atom itself. */ const atomChange = exports.atomChange = { //#region Structural changes // Changes that affect the inner value of the atom. /** Atom type changed. It indicates that atom type(array/object/number/etc.) * is now different. */ type: takeBit(), /** Atom value changed. It applies only to primitive atom type as * object/array atoms value is a reference. */ value: takeBit(), /** Atom attached (inserted) to object/array. Before it gets created * the atom might be in the detached state. */ attach: takeBit(), /** Atom detached from object/array. Rather than removing, the atom will * be in detached state and won't receive any updates until it gets attached * again. */ detach: takeBit(), /** Shape of object/array changed. It means one of the following: a child * got attached, detached or moved. */ shape: takeBit(), //#endregion // Structural changes padding ...reserveBit(), ...reserveBit(), ...reserveBit(), //#region Meta changes // Changes that affect the meta state of the atom. /** Atom id changed. It happens when the atom watched by a hook is replaced * with a new atom. */ id: takeBit(), /** Atom key changed. It indicates that the atom got moved. It doesn't * apply to the atom getting detached and attached. */ key: takeBit(), /** Current atom value committed as initial. */ // TODO: Utilize this flag commit: takeBit(), /** Atom value reset to initial. */ // TODO: Utilize this flag reset: takeBit(), /** Atom become invalid. */ invalid: takeBit(), /** Atom become valid. */ valid: takeBit(), /** Errors change. A new error was inserted or removed. */ errors: takeBit(), /** Atom lost focus. */ blur: takeBit() //#endregion }; /** * Atom change map. It maps the human-readable atom change names to * the corresponding bit mask. */ // NOTE: Without using this type to define `childChange` and `subtreeChange`, // the LSP actions such as "rename" will not work. /** * Child changes shift. */ const childChangesShift = exports.childChangesShift = changesBits; /** * Child changes bits mask. It allows to isolate the child changes bits. */ const childChangesMask = exports.childChangesMask = 2n ** (changesBits + childChangesShift) - 1n & ~atomChangesMask; /** * Structural child changes mask. It allows to isolate the structural child * changes bits. */ const structuralChildChangesMask = exports.structuralChildChangesMask = structuralAtomChangesMask << childChangesShift; /** * Meta child changes mask. It allows to isolate the meta child changes bits. */ const metaChildChangesMask = exports.metaChildChangesMask = metaAtomChangesMask << childChangesShift; /** * Child changes map. * * It represents the changes for the immediate children of the atom. */ const childChange = exports.childChange = { ...atomChange }; // Shift child atom changes to its dedicated category bits range. shiftCategoryBits(childChange, childChangesShift); /** * Subtree changes shift. */ const subtreeChangesShift = exports.subtreeChangesShift = changesBits * 2n; /** * Subtree changes bits mask. It allows to isolate the subtree changes bits. */ const subtreeChangesMask = exports.subtreeChangesMask = 2n ** (changesBits + subtreeChangesShift) - 1n & ~(atomChangesMask | childChangesMask); /** * Structural subtree changes mask. It allows to isolate the structural subtree * changes bits. */ const structuralSubtreeChangesMask = exports.structuralSubtreeChangesMask = structuralAtomChangesMask << subtreeChangesShift; /** * Meta subtree changes mask. It allows to isolate the meta subtree changes * bits. */ const metaSubtreeChangesMask = exports.metaSubtreeChangesMask = metaAtomChangesMask << subtreeChangesShift; /** * Subtree changes map. * * It represents the changes for the deeply nested children of the atom. */ const subtreeChange = exports.subtreeChange = { ...atomChange }; // Shift child atom changes to its dedicated category bits range. shiftCategoryBits(subtreeChange, subtreeChangesShift); /** * Structural changes mask. It allows to isolate the structural changes bits on * all levels. */ const structuralChangesMask = exports.structuralChangesMask = structuralAtomChangesMask | structuralChildChangesMask | structuralSubtreeChangesMask; /** * Meta changes mask. It allows to isolate the meta changes bits on all levels. */ const metaChangesMask = exports.metaChangesMask = metaAtomChangesMask | metaChildChangesMask | metaSubtreeChangesMask; /** * Atom changes map. Each bit indicates a certain type of change in the atom. * * The changes are represented as a bit mask, which allows to combine multiple * change types into a single value. BigInt is used to represent the changes * as bitwise operations on numbers convert their operands to 32-bit integers * and limit the available bits range. * * All changes are divided into three main categories: * * - **Atom changes** that affect the atom itself. * - **Child changes** that affect the immediate children of the atom. * - **Subtree changes** that affect the deeply nested children of the atom. * * We allocate first 48 bits (16*3) for the category changes. * * Each category bit further divided into subcategories (8 bits each): * * - **Structural changes** that affect the inner value of the atom. * - **Meta changes** that affect the meta state of the atom. * * Here is the visual representation of the changes map: * * Subtree Child Atom * v v v * 0b000000000000000000000000000000000000000000000000n * ^ ^ ^ ^ ^ ^ * Meta Struct. Meta Struct. Meta Struct. * * The structural atom changes range is exclusive meaning that only one of * the flags can be set at a time. This allows to simplify the logic and * make it easier to understand. Initially that was not the case and * `detach` and `attach` flags were always set with `type` change, but this * logic warrants always setting `value` flag as well. The possible combinations * made it harder to understand and test the logic, so it was decided to make * the structural changes exclusive. * * The meta atom changes as well as any child and subtree changes are not * exclusive and can be combined, as there might be multiple children having * conflicting changes i.e. `detach` and `attach` at the same time. */ const change = exports.change = { /** Atom changes that affect the atom itself. */ atom: atomChange, /** Child changes that affect the immediate children of the atom. */ child: childChange, /** Subtree changes that affect the deeply nested children of the atom. */ subtree: subtreeChange }; //#endregion //#region Manipulations /** * Shifts atom changes to child changes. The subtree changes get merged with * existing subtree changes. * * @param changes - Changes to shift. */ function shiftChildChanges(changes) { const subtreeChanges = isolateSubtreeChanges(changes); return (changes & ~subtreeChanges) << childChangesShift | subtreeChanges; } /** * Isolates the atom changes from the changes map. * * @param changes - Changes to isolate the atom changes from. * @returns Isolated atom changes. */ function isolateAtomChanges(changes) { return changes & atomChangesMask; } /** * Isolates the child changes from the changes map. * * @param changes - Changes to isolate the child changes from. * @returns Isolated child changes. */ function isolateChildChanges(changes) { return changes & childChangesMask; } /** * Isolates the atom changes from the changes map. * * @param changes - Changes to isolate the subtree changes from. * @returns Isolated subtree changes. */ function isolateSubtreeChanges(changes) { return changes & subtreeChangesMask; } //#endregion //#region Checks /** * Checks if child changes affect atom shape and returns shape change if so. * * @param changes - Changes to check. * @returns Shape change if child changes affect atom shape or `0n` otherwise. */ function shapeChanges(changes) { return maskedChanges(changes, change.child.attach | change.child.detach | change.child.key) && atomChange.shape; } /** * Isolates structural changes. * * @param changes - Changes to check. * @returns Isolates structural changes if found or `0n` otherwise. */ function structuralChanges(changes) { return changes & structuralChangesMask; } /** * Isolates meta changes. * * @param changes - Changes to check * @returns Isolated meta changes if found or `0n` otherwise. */ function metaChanges(changes) { return changes & metaChangesMask; } /** * Checks if changes contain any of changes in the given mask. * * @param changes - Changes to check. * @param mask - Changes mask to check against. * @returns Detected changes if found or `0n` otherwise. */ function maskedChanges(changes, mask) { return changes & mask; } //#endregion //#endregion //#region Private /** * Shifts the changes map bits to the left by 16 bits, so it placed on * the dedicated category bits range. * * @param changes - Changes map to shift. * * @private */ function shiftCategoryBits(changes, shift) { for (const key in atomChange) { changes[key] <<= shift; } } /** * Takes the next bit in the sequence and increments the global `bitIdx`. * * @returns The next bit in the sequence. */ function takeBit() { return 2n ** bitIdx++; } /** * Generates object to assign to the changes map, it uses the global `baseBits` * variable to generate the next reserved change and increments it. * * @returns Object to assign to the changes map. * * @private */ function reserveBit() { return { /** Reserved for future use. * @deprecated */ [`reserved${bitIdx + 1n}`]: takeBit() }; } //#endregion