// Copyright 2026 The Lynx Authors. All rights reserved. // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. import type { NodesRef, SelectorQuery } from '@lynx-js/types'; export type RefCleanup = (() => void) | void; export type RefCallback = (ref: T | null) => RefCleanup; export interface OrdinaryRefBinding { cleanup?: RefCleanup; } export interface RefObject { current: T | null; } export type OrdinaryRef = RefCallback | RefObject; type FunctionPropertyNames = { [K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never; }[keyof T]; export type ForwardableNodesRefMethod = Exclude, 'exec'>; export type RefProxyForwardedMethods = { [K in ForwardableNodesRefMethod]: (...args: Parameters) => TProxy; }; type RefTask = (nodesRef: NodesRef) => SelectorQuery; export function assertValidRef(value: unknown): OrdinaryRef { if ( typeof value === 'function' || (typeof value === 'object' && value !== null && 'current' in value) ) { return value as OrdinaryRef; } throw new Error( `Elements' "ref" property should be a function, or an object created ` + `by createRef(), but got [${typeof value}] instead`, ); } export function normalizeRefValue(value: unknown): OrdinaryRef | null | undefined { if (value === null || value === undefined) { return value; } return assertValidRef(value); } export function applyOrdinaryRef( ref: OrdinaryRef, value: T | null, binding: OrdinaryRefBinding, ): void { try { if (typeof ref === 'function') { const cleanup = binding.cleanup; const hasCleanup = typeof cleanup === 'function'; binding.cleanup = undefined; if (hasCleanup) { cleanup(); } if (!hasCleanup || value !== null) { const nextCleanup = ref(value); if (typeof nextCleanup === 'function') { binding.cleanup = nextCleanup; } } } else { ref.current = value; } } catch (error) { lynx.reportError(error as Error); } } // Keeps the Snapshot/ET ordinary ref ordering shared without owning backend // timing: each backend decides when to queue/flush and how to build the proxy. export class OrdinaryRefEffectQueue { // A host instance survives hydration's numeric ID remapping. Each ref slot // owns its cleanup independently, even when callbacks are shared. private readonly bindings = new WeakMap>(); private readonly refsToClear: Array<[ref: OrdinaryRef, binding: OrdinaryRefBinding]> = []; private readonly refsToApply: Array<[ref: OrdinaryRef, token: TToken, binding: OrdinaryRefBinding]> = []; queue( oldRef: OrdinaryRef | null | undefined, newRef: OrdinaryRef | null | undefined, owner: object, slot: number, token: TToken, ): void { if (oldRef === newRef) { return; } let slots = this.bindings.get(owner); if (!slots) { this.bindings.set(owner, slots = new Map()); } let binding = slots.get(slot); if (!binding) { slots.set(slot, binding = {}); } if (oldRef) { this.refsToClear.push([oldRef, binding]); } if (newRef) { this.refsToApply.push([newRef, token, binding]); } } flush( createValue: (token: TToken) => TProxy, ): void { // Ref callbacks can synchronously trigger more work; detach this batch from // the queue before invoking user code so later effects stay in the next batch. const refsToClearNow = this.refsToClear.splice(0); const refsToApplyNow = this.refsToApply.splice(0); for (const [ref, binding] of refsToClearNow) { applyOrdinaryRef(ref, null, binding); } for (const [ref, token, binding] of refsToApplyNow) { applyOrdinaryRef(ref, createValue(token), binding); } } clear(): void { this.refsToClear.length = 0; this.refsToApply.length = 0; } hasPending(): boolean { return this.refsToClear.length > 0 || this.refsToApply.length > 0; } } export abstract class SelectorRefProxy> { private task: RefTask | undefined; protected createProxy(): TProxy { return new Proxy(this, { get: (target, prop, receiver) => { if ( typeof prop === 'symbol' || prop === 'then' || prop in target || typeof prop !== 'string' ) { return Reflect.get(target, prop, receiver); } return (...args: Parameters) => { return target.createProxyTarget().setTask(prop as ForwardableNodesRefMethod, args); }; }, }) as unknown as TProxy; } protected abstract createProxyTarget(): TProxy; protected abstract runOrDelay(task: () => void): void; abstract get selector(): string; private setTask( method: K, args: Parameters, ): TProxy { this.task = (nodesRef) => { const nodesRefMethod = nodesRef[method] as (...params: Parameters) => SelectorQuery; return nodesRefMethod.apply(nodesRef, args); }; return this as unknown as TProxy; } exec(): void { this.runOrDelay(() => { this.task!(lynx.createSelectorQuery().select(this.selector)).exec(); }); } }