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() { }
}