import * as Pixi from "pixi.js"; import { batchifySetState } from "../../lib/util/batchify"; import { UpdaterFn, updaterGenerator2 } from "../../lib/util/updaterGenerator"; type Props = { args?: { markForceUpdate?: (self: LifecycleHandlerBase<any, any>) => void, [k: string]: any }, [k: string]: any }; type State = {}; type ChildInstructions< ChildInstanceType, ChildPropsType extends Props, ParentPropsType extends Props, ParentStateType extends State > = { childClass: new (props: ChildPropsType) => ChildInstanceType; instance?: ChildInstanceType; propsFactory: ( parentProps: ParentPropsType, parentState: ParentStateType ) => ChildPropsType; }; class ChildrenArray<P extends Props, S extends State> { private _values: ChildInstructions<LifecycleHandlerBase<any, any>, any, P, S>[] = []; public add<CIT extends LifecycleHandlerBase<any, any>, CPT>(c: ChildInstructions<CIT, CPT, P, S>) { if (this._values.indexOf(c) === -1 || (c.instance && this.contains(c.instance))) { // do nohting - its already in here } this._values.push(c); } public remove<CIT extends LifecycleHandlerBase<any, any>>(c: CIT): ChildInstructions<CIT, any, P, S> | undefined { const removed = this._values.splice(this._values.findIndex(it => it.instance === c), 1); if (removed.length === 0) { return undefined; } else { return removed[0] as ChildInstructions<CIT, any, P, S>; } } public contains<CIT extends LifecycleHandlerBase<any, any>>(c: CIT): boolean { return (this._values.findIndex(it => it.instance === c) > -1); } public get<CIT extends LifecycleHandlerBase<CPT, any>, CPT>(c: CIT): ChildInstructions<CIT, CPT, P, S> | undefined { return this._values.find((it) => it.instance === c) as (ChildInstructions<CIT, CPT, P, S> | undefined); } public clone(): ChildrenArray<P, S> { let cloned = new ChildrenArray<P, S>(); cloned._values = [...this._values]; return cloned; } public forEach(callbackfn: ( value: ChildInstructions<LifecycleHandlerBase<any, any>, any, P, S>, index: number, array: ChildInstructions<LifecycleHandlerBase<any, any>, any, P, S>[], ) => void) { this._values.forEach(callbackfn); } } // export interface LifecycleHandlerBase<P extends Props, S extends State> { // // useful for interface merging?? https://stackoverflow.com/questions/44153378/typescript-abstract-optional-method // } /** * LifecycleHandlerConstructor <- this should take the usual props, and will return new proxy, new base component(props), the handler object which has the construct() property and that function in it */ // export function LifecycleHandlerConstructor<T>(props: // class and interface merging??? https://stackoverflow.com/questions/44153378/typescript-abstract-optional-method export abstract class LifecycleHandlerBase<P extends Props, S extends State> { // public, only to interface with non lifecycleHandler classes that we have yet to refactor public abstract container: Pixi.Container; // public, only to allow useState function below to set this.state public abstract state: S; protected _staleProps: P; // NOTE(bowei): need it for args for now; maybe we can extract out args? private _children: ChildrenArray<P, S> = new ChildrenArray(); private _childrenToConstruct: ChildrenArray<P, S> = new ChildrenArray(); private _childrenToDestruct: ChildrenArray<P, S> = new ChildrenArray(); private _forceUpdates: ChildrenArray<P, S> = new ChildrenArray(); // private _self!: LifecycleHandlerBase<P, S>; constructor(props: P) { this._staleProps = props; } protected addChild<CIT extends LifecycleHandlerBase<CPT, any>, CPT>( c: ChildInstructions<CIT, CPT, P, S> ) { this._children.add(c); // make sure children are updated this._childrenToConstruct.add(c); // if not already constructed/added to pixi hierarchy, queue it up } protected registerChild<CIT extends LifecycleHandlerBase<CPT, any>, CPT>( c: ChildInstructions<CIT, CPT, P, S> ) { // only add children to updateable, not constructed this._children.add(c); } protected removeChild<CIT extends LifecycleHandlerBase<any, any>>(c: CIT) { let childInfo = this._children.remove(c); // make sure children are no longer updated // NOTE(bowei): do we need to call willUnount on the children here?? childInfo && this._childrenToDestruct.add(childInfo); // queue it for destruction next update tick } private _didConstruct(props: P) { // this._self = this; this._childrenToConstruct.forEach((child) => { if (!child.instance) { child.instance = new child.childClass( child.propsFactory(props, this.state) ); } // NOTE(bowei): we are assuming the derived class did NOT manually add child to pixi hierarchy, even if // they constructed the instance themselves (in order to e.g. hold a reference); we do that here this.container.addChild(child.instance.container); }); this.renderSelf(props); this.didMount?.(); } /** callback passed to child - since child is not a pure component, it needs to inform us of updates if otherwise we wouldnt update */ protected markForceUpdate = (childInstance: any) => { this._staleProps.args?.markForceUpdate?.(this); // mark us for update in OUR parent const childInfo = this._children.get(childInstance); if (childInfo) { this._forceUpdates.add(childInfo); } else { throw new Error(`Error, child ${childInstance} not found in ${this}`); } } // cannot be attached to an instance due to typescript // if satic, cannot be called "useState" or else react linter complains protected useState<S, T extends { state: S }>(self: T, initialState: S) { const setState: UpdaterFn<S> = (valueOrCallback) => { if (typeof valueOrCallback === "function") { self.state = (valueOrCallback as (s: S) => S)(self.state); } else { self.state = valueOrCallback; } }; const [batchedSetState, fireBatch] = batchifySetState(setState); const stateUpdaters = updaterGenerator2<S>(initialState, batchedSetState); return { state: initialState, setState, fireStateUpdaters: fireBatch, stateUpdaters, }; } // shim while we migrate public update(nextProps: P) { this._update(nextProps); } // NOTE(bowei): this is public because the root of component hierarchy needs to be bootstrapped from pixi react bridge public _update(nextProps: P) { // nextProps is guaranteed to be referentially a distinct object (might be shallow copy though) const staleState = { ...this.state }; this.fireStateUpdaters?.(); this.updateSelf?.(nextProps); if (this.shouldUpdate && !this.shouldUpdate(this._staleProps, staleState, nextProps, this.state)) { // we think we don't need to update; however, we still need to // update the chidlren that asked us to forcefully update them let forceUpdates = this._forceUpdates.clone(); this._forceUpdates = new ChildrenArray<P, S>(); forceUpdates.forEach(childInfo => { let { instance, propsFactory } = childInfo; instance?._update(propsFactory(nextProps, this.state)); // why are we even calling props factory here?? theres no point... we should just tell the child to use their own stale props, like this: // instance._forceUpdate(); // note that children can add themselves into forceupdate next tick as well, if they need to ensure they're continuously in there instance && this.didForceUpdateChild?.(instance); }) // no need to do anything else -- stale props has not changed this.didForceUpdate?.(); return; } this.updateChildren?.(nextProps); this._updateChildren(nextProps); // implementation should call children._update in here this.renderSelf(nextProps); this._staleProps = nextProps; new Promise((resolve) => resolve(this.didUpdate?.())); } protected updateChildren?(nextProps: P): void // destroy, update, create in that order, so that there's no extra update right before destroy or after create private _updateChildren(nextProps: P) { this._childrenToDestruct.forEach((child) => { if (child.instance) { // should always be true child.instance.willUnmount?.() this.container.removeChild(child.instance.container); } }); this._childrenToDestruct = new ChildrenArray(); this._children.forEach(({ instance, propsFactory }) => { instance?._update(propsFactory(nextProps, this.state)); }); this._childrenToConstruct.forEach((child) => { // here we expect the child instances to be empty, but they could be already constructed, if the derived class needs to keep a reference to it if (!child.instance) { child.instance = new child.childClass( child.propsFactory(nextProps, this.state) ); } this.container.addChild(child.instance.container); }); this._childrenToConstruct = new ChildrenArray(); } protected fireStateUpdaters?(): void; protected didMount?(): void; protected updateSelf?(nextProps: P): void; protected shouldUpdate?( staleProps: P, staleState: S, nextProps: P, state: S ): boolean; protected abstract renderSelf(nextProps: P): void; protected didUpdate?(): void; protected didForceUpdate?(): void; public willUnmount(): void {} // TODO(bowei): revert this to protected nullable; however it's needed for shim for now protected didForceUpdateChild?(child: LifecycleHandlerBase<any, any>): void; public toString(): string { return "lifecyclehandler object"; } } export type LifecycleHandlerType<P, S> = LifecycleHandlerBase<P, S>; export const LifecycleHandler = new Proxy(LifecycleHandlerBase, { construct: (target, args, newTarget) => { const instance = Reflect.construct(target, args, newTarget); instance._didConstruct(...args); return instance; }, }); export function engageLifecycle<T extends object>(derived: T): T { return new Proxy<T>(derived, { construct: (target, args) => { const instance = new (target as any)(args[0]); instance._didConstruct(args[0]); return instance; }, }); } /** * First render: * constructor * renderChildren? * renderSelf * didMount * * Subsequent updates: * * fireStateUpdaters * updateSelf * shouldUpdate(props,state)? * updateChildren * children._update * renderSelf * didUpdate * staleProps = props * */ type ReferenceProps = { updaters: "stuff"; args: { s: "other stuff" }; }; type ReferenceState = { lalalala: "hahahah"; }; export class Reference extends LifecycleHandler<ReferenceProps, ReferenceState> { public container: Pixi.Container public state: ReferenceState constructor(props: ReferenceProps) { super(props); this.container = new Pixi.Container(); this.state = { lalalala: "hahahah", }; } updateSelf(nextProps: ReferenceProps) { } renderSelf(nextProps: ReferenceProps) { } didMount() { } didUpdate() { } shouldUpdate(): boolean { return true; } fireStateUpdaters() { } willUnmount() { } }