@virtualstate/hooks-extended
Version:
147 lines (122 loc) • 4.31 kB
text/typescript
import { Hook as Basic, HookPair } from "@virtualstate/hooks";
import { createFragment, createNode, isSourceReference, VNode } from "@virtualstate/fringe";
import { Mutation, MutationIs, MutationToken } from "./mutation";
import { Reference, ReferenceIs, ReferenceToken } from "./reference";
import { Isolated, IsolatedToken } from "./isolated";
export type Target = MutationToken | ReferenceToken | IsolatedToken;
export function isTarget(node: VNode): node is Target {
return Mutation.is(node) || Reference.is(node) || Isolated.is(node);
}
export interface HookedTarget<T = Target> {
target: T;
depth: number;
}
export interface HookOptions {
targets?: HookedTarget[];
depth?: number;
mutate?: boolean;
}
export type HookExtendedOptions = HookOptions;
export const HookExtended = Hook;
export function Hook(options: HookOptions, child: VNode): VNode {
return createNode(Basic, { ...options, hook: hook.bind(undefined, options) }, child);
async function hook(options: HookOptions, node: VNode): Promise<HookPair> {
const { targets = [], depth = 0 } = options;
if (isTarget(node)) {
return [
createFragment({}, node.children),
hook.bind(undefined, {
...options,
depth: depth + 1,
targets: targets.concat([{
target: node,
depth
}])
})
];
} else {
return [
await run(node, targets),
hook.bind(undefined, {
...options,
depth: depth + 1
})
];
}
}
}
async function run<V extends VNode = VNode>(node: V, targets: HookedTarget[] = []): Promise<VNode> {
if (!targets.length) {
return node;
}
const isolated = targets.filter(isIsolatedTarget);
const isolatedDepth = isolated.reduce(
(depth, descriptor) => Math.min(depth, descriptor.depth),
Number.POSITIVE_INFINITY
);
function isAllowedDepth(descriptor: HookedTarget) {
if (isolatedDepth === Number.POSITIVE_INFINITY) {
return true;
}
return descriptor.depth >= isolatedDepth;
}
const mutators = targets
.filter(isAllowedDepth)
.filter(isMutationTarget);
const references = targets
.filter(isAllowedDepth)
.filter(isReferenceTarget);
// Reference before we mutate
await reference(node, references);
const mutated = await mutate(node, mutators);
// If _any_ change is made then we will re-invoke our references
// This is a very important point that we may invoke reference fragments multiple times with the same vnode
//
// The implementation will need to track whether the reference was found before
//
// Mutators should only be returning a new node instance if they have a change!
if (mutated !== node) {
await reference(mutated, references);
}
return mutated;
function reference<V extends VNode>(node: V, targets: HookedTarget<ReferenceToken>[]) {
// Invoke all at once
return Promise.all(
targets
.map(descriptor => descriptor.target)
.filter(target => isExpected(node, target.options))
.map(target => target.options.on(node))
);
}
async function mutate(node: VNode, targets: HookedTarget<MutationToken>[]): Promise<VNode> {
if (!targets.length) {
return node;
}
const currentMutators = targets.slice();
const nextMutator = currentMutators.shift();
if (isExpected(node, nextMutator.target.options)) {
const nextValue = await nextMutator.target.options.mutate(node);
return mutate(nextValue, currentMutators);
} else {
return mutate(node, currentMutators);
}
}
function isExpected(node: VNode, { is: isValue }: { is?: ReferenceIs | MutationIs }): boolean {
if (typeof isValue === "function") {
return isValue(node);
}
if (isSourceReference(isValue)) {
return node.reference === isValue;
}
return true;
}
}
function isIsolatedTarget(hooked: HookedTarget<unknown>): hooked is HookedTarget<IsolatedToken> {
return Isolated.is(hooked.target);
}
function isReferenceTarget(hooked: HookedTarget<unknown>): hooked is HookedTarget<ReferenceToken> {
return Reference.is(hooked.target);
}
function isMutationTarget(hooked: HookedTarget<unknown>): hooked is HookedTarget<MutationToken> {
return Mutation.is(hooked.target);
}