@matthewp/beepboop
Version:
A framework built on Finite State Machines
267 lines (242 loc) • 9.1 kB
TypeScript
import { Component } from 'preact';
import { type GetSelectors as GetTemplateSelectors } from 'ts-types-html-parser';
import type { StandardSchemaV1 } from '@standard-schema/spec';
declare namespace util {
type ArrayElement<ArrayType extends readonly unknown[]> =
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
type AllKeys<T> = T extends any ? keyof T : never;
type identity<T> = T;
type flatten<T extends object> = identity<{ [k in keyof T]: T[k] }>;
type extendShape<A, B> = flatten<Omit<A, keyof B> & B>;
const brandSymbol: unique symbol;
type brand<T, N> = T & {
[brandSymbol]: N;
}
}
type RawShape = {
model: StandardSchemaV1,
props?: StandardSchemaV1 | undefined;
selectors: {
[s1 in string]: {};
};
states: {
[s2 in string]: {
// events
events: {
[e in string]: string[];
};
immediates: string[];
};
};
};
// Getters
type GetSelectors<R extends RawShape> = keyof R['selectors'] extends string ? keyof R['selectors'] : never;
type GetStates<R extends RawShape> = keyof R['states'] extends string ? keyof R['states'] : never;
type GetEvents<R extends RawShape, S extends GetStates<R>> = keyof R['states'][S]['events'] extends string ? keyof R['states'][S]['events'] : never;
type GetAllEvents<R extends RawShape> = util.AllKeys<R['states'][GetStates<R>]['events']> | 'props'
type GetImmediates<R extends RawShape, S extends GetStates<R>> =
R['states'][S]['immediates'] extends undefined ? [] : R['states'][S]['immediates'];
type GetTransitions<R extends RawShape, S extends GetStates<R>, E extends GetEvents<R, S>> = R['states'][S]['events'][E];
type GetModelKeys<R extends RawShape> = NestedPaths<StandardSchemaV1.InferOutput<R['model']>>;
type GetModelKeyType<R extends RawShape, K extends GetModelKeys<R>> = GetNestedType<StandardSchemaV1.InferOutput<R['model']>, K>;
// Props types
type GetPropsType<R extends RawShape> = R['props'] extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<R['props']> : any;
// Debug nested path types step by step
type NestedPaths<T> = T extends Record<string, any>
? {
[K in keyof T & string]: T[K] extends Record<string, any>
? T[K] extends readonly any[]
? K // Arrays are terminal paths
: K | `${K}.${NestedPaths<T[K]>}`
: K;
}[keyof T & string]
: never;
type GetNestedType<T, P extends string> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? GetNestedType<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never;
// Setters
type AddSelector<R extends RawShape, S extends string> = util.extendShape<R, {
selectors: util.extendShape<R['selectors'], {
[s in S]: {};
}>
}>;
type AddState<R extends RawShape, S extends string, B = {}, I = undefined> = util.extendShape<R, {
states: util.extendShape<R['states'], {
[s in S]: {
events: B;
immediates: I;
};
}>
}>;
type AddModel<R extends RawShape, S extends StandardSchemaV1> = util.extendShape<R, { model: S }>;
type AddProps<R extends RawShape, S extends StandardSchemaV1> = util.extendShape<R, { props: S }>;
type AddEvent<R extends RawShape, S extends GetStates<R>, E extends string, D extends readonly string[] = []> = AddState<
R,
S,
util.extendShape<
R['states'][S]['events'],
{
[e in E]: D;
}
>,
GetImmediates<R, S>
>;
type AddTransition<R extends RawShape, S extends GetStates<R>, E extends GetEvents<R, S>, D extends string> =
AddEvent<R, S, E, [...GetTransitions<R, S, E>, D]>;
type AddImmediate<R extends RawShape, S extends GetStates<R>, D extends string> = AddState<R, S, GetEvents<R, S>, [...GetImmediates<R, S>, D]>;
type AddAlwaysEvent<R extends RawShape, E extends string> = R & {
states: {
[K in keyof R['states']]: {
events: R['states'][K]['events'] & { [e in E]: [K] };
immediates: R['states'][K]['immediates'];
};
};
};
// Extras
declare const GUARD_BRAND: unique symbol;
type GuardType<R extends RawShape> = util.brand<{
[GUARD_BRAND]: R;
fn: (model: R['model']) => boolean;
}, 'guard'>;
declare const REDUCE_BRAND: unique symbol;
type ReduceType<R extends RawShape> = util.brand<{
[REDUCE_BRAND]: R; // necessary for weird reasons
fn: (model: StandardSchemaV1.InferOutput<R['model']>) => StandardSchemaV1.InferOutput<R['model']>;
}, 'reduce'>;
type ExtraType<R extends RawShape> = GuardType<R> | ReduceType<R>;
type SendFunction<R extends RawShape> = (event: GetAllEvents<R> | { type: GetAllEvents<R>;[key: string]: any }) => void;
// Standard Schema is now used for all model definitions
// Event - conditional type for props events
type MachineEvent<R extends RawShape, E extends GetAllEvents<R> = GetAllEvents<R>, T = any> = {
type: E;
data: E extends 'props' ? GetPropsType<R> : T;
domEvent: Event;
model: {
[k in GetModelKeys<R>]: GetModelKeyType<R, k>
};
state: GetStates<R>;
root: Component;
send(type: string, data?: any): void;
send({ type: any }): void;
sendEvent: (type: string, domEvent: Event) => void;
};
declare const me: MachineEvent<any>;
// Actor
type Actor<R extends RawShape = any> = {
mount(rootSelector: string | HTMLElement | Document): void;
interpret(): Actor<R>;
send(eventType: GetAllEvents<R>, data?: any): void;
send(event: { type: GetAllEvents<R>; [key: string]: any }): void;
};
type BuilderType<R extends RawShape> = {
// UI
selectors<const S extends readonly string[]>(
sel: S
): BuilderType<AddSelector<R, util.ArrayElement<S>>>;
template<S extends string>(tmpl: S): BuilderType<AddSelector<R, GetTemplateSelectors<S> & {}>>;
on<S extends GetSelectors<R> = GetSelectors<R>, E extends GetAllEvents<R> = GetAllEvents<R>>(
sel: S,
domEvent: string,
machineEvent: E,
): BuilderType<R>;
text<S extends GetSelectors<R> = GetSelectors<R>, K extends GetModelKeys<R> = GetModelKeys<R>>(
sel: S,
modelProp: K
): BuilderType<R>;
attr<S extends GetSelectors<R> = GetSelectors<R>, K extends GetModelKeys<R> = GetModelKeys<R>>(
sel: S,
attrName: string,
modelProp: K
): BuilderType<R>;
class<S extends GetSelectors<R> = GetSelectors<R>, K extends GetModelKeys<R> = GetModelKeys<R>>(
sel: S,
className: string,
modelProp: K
): BuilderType<R>;
prop<S extends GetSelectors<R> = GetSelectors<R>, K extends GetModelKeys<R> = GetModelKeys<R>>(
sel: S,
propName: string,
modelProp: K
): BuilderType<R>;
effect<K extends GetModelKeys<R>>(
key: K,
fn: (event: MachineEvent<R>) => void
): BuilderType<R>;
effect(
fn: (event: MachineEvent<R>) => void | (() => void)
): BuilderType<R>;
spawn<S extends GetSelectors<R> = GetSelectors<R>, K extends GetModelKeys<R> = GetModelKeys<R>>(
sel: S,
key: K,
actor: Actor<any>
): BuilderType<R>;
view(
fn: (props: {
model: { [k in GetModelKeys<R>]: GetModelKeyType<R, k> };
send: SendFunction<R>;
deliver: (name: GetAllEvents<R>) => (event: Event) => void;
}) => any
): BuilderType<R>;
view<M extends RawShape>(machine: M & { viewFn: any }): Component<GetPropsType<M>>;
// Data model
model<S extends StandardSchemaV1>(schema: S): BuilderType<AddModel<R, S>>;
props<S extends StandardSchemaV1>(schema: S): BuilderType<AddProps<R, S>>;
// FSM
states<const S extends readonly string[]>(
states: S
): BuilderType<AddState<R, util.ArrayElement<S>>>;
events<S extends GetStates<R>, const E extends readonly string[]>(
state: S,
events: E
): BuilderType<AddEvent<R, S, util.ArrayElement<E>>>;
transition<S extends GetStates<R>, E extends GetEvents<R, S> = GetEvents<R, S>, D extends GetStates<R> = GetStates<R>>(
state: S,
event: E,
dest: D,
...extras: ExtraType<R>[]
): BuilderType<AddTransition<R, S, E, D>>;
immediate<S extends GetStates<R>, D extends GetStates<R>>(
state: S,
dest: D,
...extras: ExtraType<R>[]
): BuilderType<AddImmediate<R, S, D>>;
always<E extends string>(
event: E,
...extras: ExtraType<R>[]
): BuilderType<AddAlwaysEvent<R, E>>;
init(...extras: ExtraType<R>[]): BuilderType<R>;
invoke<S extends GetStates<R>>(
state: S,
fn: (event: MachineEvent<R>) => Promise<any>
): BuilderType<R>;
// Helpers
guard<RR extends R>(
fn: (event: MachineEvent<RR>) => boolean
): GuardType<RR>;
reduce<RR extends R>(
fn: (event: MachineEvent<RR>) => StandardSchemaV1.InferOutput<RR['model']>
): ReduceType<RR>;
assign<RR extends R, K extends GetModelKeys<RR> = GetModelKeys<RR>>(
key: K,
fn: (event: MachineEvent<RR>) => GetModelKeyType<RR, K>
): ReduceType<RR>;
action<RR extends R>(
fn: (event: MachineEvent<RR>) => void | (() => void)
): any;
actor<BR extends RawShape>(builder: BuilderType<BR>): Actor<BR>;
actor(): Actor<R>;
}
type Builder = BuilderType<{ states: {}, selectors: {}, model: StandardSchemaV1 }>;
declare const bb: Builder;
interface BeepBoopComponent extends Component<{ actor: Actor<any> }> {
draw(): void;
}
export {
bb,
BeepBoopComponent as Component,
BeepBoopComponent as View,
};