Bring in packages/montar

This commit is contained in:
Abel Luck 2023-03-13 10:04:22 +00:00
parent fe4509a2ae
commit 67f7cf8e1b
19 changed files with 800 additions and 24 deletions

View file

@ -0,0 +1,73 @@
import { defState, start, stop, isStarted } from ".";
const _inc41 = (): number => 41 + 1;
const _inc = (n: number): number => n + 1;
const obj = defState("obj", {
start: async () => ({
value: 42,
}),
});
const mutableObj = defState("mutableObj", {
start: async () => ({
value: 41,
}),
});
type FortyOneAdder = () => number;
type Incrementer = (n: number) => number;
const inc41 = defState<FortyOneAdder>("inc41", {
isFunction: true,
start: async () => _inc41,
});
const inc = defState<Incrementer>("inc", {
isFunction: true,
start: async () => _inc,
});
describe("defstate", () => {
beforeEach(async () => {
await start();
});
afterEach(async () => {
await stop();
});
test("obj", async () => {
expect(obj.value).toBe(42);
});
test("mutable obj", async () => {
expect(mutableObj.value).toBe(41);
mutableObj.value++;
expect(mutableObj.value).toBe(42);
});
test("inc41", async () => {
expect(inc41()).toBe(42);
expect(isStarted("inc41")).toBe(true);
});
test("inc", async () => {
expect(inc(41)).toBe(42);
});
});
describe("errors", () => {
test("doesn't exist", () => {
expect(() => isStarted("not-real")).toThrow();
});
test("invalid type", () => {
defState("invalid", { start: () => 42 as any });
expect(start()).rejects.toThrow();
});
test("multiple defs", () => {
defState("foo", { async start() {} });
expect(() => {
defState("foo", { async start() {} });
}).toThrow();
});
});

View file

@ -0,0 +1,136 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-await-in-loop */
import Debug from "debug";
import { mutableProxyFactory } from "./proxy";
const debug = Debug("montar");
export type Start<T> = () => Promise<T>;
interface StateMeta<T> {
name: string;
start: Start<T>;
stop(): Promise<void>;
isFunction: boolean;
setHandler<T extends object>(handler: ProxyHandler<T>): void;
setTarget<T>(target: T): void;
}
interface DefState<T> {
start: Start<T>;
stop?(): Promise<void>;
isFunction?: boolean;
}
interface StateMetaMap<T> {
[name: string]: StateMeta<T>;
}
interface StateMap {
[name: string]: any;
}
const defaultStop = async () => {};
const states: StateMap = {};
const statesMeta: StateMetaMap<any> = {};
const statesOrder: string[] = [];
function getState(name: string) {
if (!statesMeta.hasOwnProperty(name)) {
throw new Error(`State ${name} not started.`);
}
return states[name];
}
function setState(name: string, state: any) {
states[name] = state;
}
function getStateMeta(name: string) {
return statesMeta[name];
}
function setStateMeta(name: string, meta: StateMeta<any>) {
statesMeta[name] = meta;
}
export function isStarted(name: string): boolean {
return getState(name) !== undefined;
}
const canary = {
error:
"I am the bare proxy. You shouldn't see me. If you see me, then a montar state was not started.",
};
export function defState<T>(name: string, meta: DefState<T>): any {
if (statesMeta.hasOwnProperty(name)) {
throw new Error(`Already registered ${name}`);
}
const { start, stop = defaultStop, isFunction = false } = meta;
let initialTarget: any = canary;
if (isFunction) initialTarget = () => canary.error;
const { proxy, setTarget, setHandler } = mutableProxyFactory(initialTarget);
setStateMeta(name, {
name,
start,
stop,
setTarget,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
setHandler,
isFunction,
});
statesOrder.push(name);
debug(`defined: ${name}`);
return proxy;
}
async function _start(toStart?: string[]): Promise<void> {
for (const name of toStart) {
debug(` >> starting.. ${name}`);
const meta = getStateMeta(name);
const state = await meta.start();
const stateType = typeof state;
if (!["object", "function"].includes(stateType)) {
throw new Error(
`error ${name}'s start() returned a non-object, non-function type`
);
}
meta.setTarget(state);
setState(name, state);
}
}
export async function start(): Promise<void> {
return _start(statesOrder);
}
export async function startWithout(excluded: string[]): Promise<void> {
const toStart = statesOrder.filter((name) => !excluded.includes(name));
return _start(toStart);
}
export async function startOnly(included: string[]): Promise<void> {
const toStart = statesOrder.filter((name) => included.includes(name));
debug(" startOnly ", included);
return _start(toStart);
}
export async function stop(): Promise<void> {
for (let i = statesOrder.length - 1; i >= 0; i--) {
const name = statesOrder[i];
if (states[name] === undefined) continue;
const meta = statesMeta[name];
await meta.stop();
delete states[name];
debug(`<< stopping.. ${name}`);
}
}

View file

@ -0,0 +1,91 @@
// @ts-nocheck PITA for this file.. revisit later.
import { mutableProxyFactory, PProxyHandler } from "./proxy";
describe("mutable proxy types", () => {
test("function", async () => {
const { proxy, setTarget } = mutableProxyFactory(() => 42);
expect(proxy()).toBe(42);
setTarget(() => 43);
expect(proxy()).toBe(43);
});
test("object", async () => {
const { proxy, setTarget } = mutableProxyFactory({ value: 42 });
expect(proxy.value).toBe(42);
setTarget({ value: 43 });
expect(proxy.value).toBe(43);
});
test("array", async () => {
const { proxy, setTarget } = mutableProxyFactory([42]);
expect(proxy[0]).toBe(42);
setTarget([43]);
expect(proxy[0]).toBe(43);
});
test("object to function", async () => {
const { proxy, setTarget } = mutableProxyFactory({ value: 42 });
expect(proxy.value).toBe(42);
setTarget(() => 43);
expect(() => proxy()).toThrow();
});
test("scalar", async () => {
expect(() => {
mutableProxyFactory(42);
}).toThrow();
});
test("boolean", async () => {
expect(() => {
mutableProxyFactory(false);
}).toThrow();
});
test("null", async () => {
expect(() => {
mutableProxyFactory(null); // eslint-disable-line unicorn/no-null
}).toThrow();
});
test("undefined", async () => {
expect(() => {
mutableProxyFactory();
}).toThrow();
});
test("setHandler", async () => {
const { proxy, setHandler } = mutableProxyFactory({ value: 42 });
setHandler(new PProxyHandler());
expect(proxy.value).toBe(42);
});
test("setTarget", async () => {
const { proxy, setTarget } = mutableProxyFactory({ value: 42 });
setTarget([43]);
expect(proxy[0]).toBe(43);
});
test("setHandler simple", async () => {
const { proxy, setHandler } = mutableProxyFactory({ value: 41 });
expect(proxy.value).toBe(41);
setHandler({
get: () => 42,
});
expect(proxy.value).toBe(42);
});
test("getHandler", async () => {
const { getHandler, setHandler, proxy } = mutableProxyFactory({
value: 42,
});
const handler = new PProxyHandler();
setHandler(handler);
expect(getHandler()).toBe(handler);
expect(proxy.value).toBe(42);
});
test("getTarget", async () => {
const { getTarget } = mutableProxyFactory({ value: 42 });
expect(getTarget().value).toBe(42);
});
});

View file

@ -0,0 +1,130 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
/* eslint-disable no-new,no-useless-call */
// mutableProxyFactory from https://stackoverflow.com/a/54460544
// (C) Alex Hall https://stackoverflow.com/users/2482744/alex-hall
// License CC BY-SA 3.0
/* eslint-disable @typescript-eslint/ban-types */
export class PProxyHandler<T extends object> implements ProxyHandler<T> {
getPrototypeOf?(target: T): object | null {
return Reflect.getPrototypeOf(target);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setPrototypeOf?(target: T, v: any): boolean {
return Reflect.setPrototypeOf(target, v);
}
isExtensible?(target: T): boolean {
return Reflect.isExtensible(target);
}
preventExtensions?(target: T): boolean {
return Reflect.preventExtensions(target);
}
getOwnPropertyDescriptor?(
target: T,
p: PropertyKey
): PropertyDescriptor | undefined {
return Reflect.getOwnPropertyDescriptor(target, p);
}
has?(target: T, p: PropertyKey): boolean {
return Reflect.has(target, p);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get?(target: T, p: PropertyKey, receiver: any): any {
return Reflect.get(target, p, receiver);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
set?(target: T, p: PropertyKey, value: any, receiver: any): boolean {
return Reflect.set(target, p, value, receiver);
}
deleteProperty?(target: T, p: PropertyKey): boolean {
return Reflect.deleteProperty(target, p);
}
defineProperty?(
target: T,
p: PropertyKey,
attributes: PropertyDescriptor
): boolean {
return Reflect.defineProperty(target, p, attributes);
}
enumerate?(target: T): PropertyKey[] {
return Reflect.ownKeys(target);
}
ownKeys?(target: T): PropertyKey[] {
return Reflect.ownKeys(target);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
apply?(target: T, thisArg: any, argArray?: any): any {
return Reflect.apply(target as Function, thisArg, argArray);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
construct?(target: T, argArray: any, newTarget?: any): object {
return Reflect.construct(target as Function, argArray, newTarget);
}
}
interface MutableProxy<T extends object> {
setTarget(target: T): void;
setHandler(handler: PProxyHandler<T>): void;
getTarget(): T;
getHandler(): ProxyHandler<T>;
proxy: T;
}
export function mutableProxyFactory<T extends object>(
mutableTarget: T,
mutableHandler?: ProxyHandler<T>
): MutableProxy<T> {
if (!mutableHandler) mutableHandler = new PProxyHandler();
return {
setTarget(target: T): void {
new Proxy(target, {}); // test target validity
mutableTarget = target;
},
setHandler(handler: PProxyHandler<T>): void {
new Proxy({}, handler); // test handler validity
Object.keys(handler).forEach((key) => {
const value = handler[key];
if (Reflect[key] && typeof value !== "function") {
throw new Error(`Trap "${key}: ${value}" is not a function`);
}
});
mutableHandler = handler;
},
getTarget(): T {
return mutableTarget;
},
getHandler(): PProxyHandler<T> {
return mutableHandler;
},
proxy: new Proxy(
mutableTarget,
new Proxy(
{},
{
// Dynamically forward all the traps to the associated methods on the mutable handler
get(target, property) {
return (_target, ...args) =>
mutableHandler[property].apply(mutableHandler, [
mutableTarget,
...args,
]);
},
}
)
),
};
}

View file

@ -0,0 +1,37 @@
import { defState, stop, startOnly, startWithout } from ".";
const startOnlyState = defState("startOnlyState", {
start: async () => ({
value: 42,
}),
});
const startedState = defState("startState", {
start: async () => ({
value: 42,
}),
});
const neverStartedState = defState("neverStartedState", {
start: async () => ({
value: 42,
}),
});
describe("starting", () => {
afterEach(async () => stop());
test("startOnly", async () => {
await startOnly(["startOnlyState"]);
expect(startOnlyState.value).toBe(42);
expect(startedState.value).toBe(undefined);
expect(neverStartedState.value).toBe(undefined);
});
test("startWithout", async () => {
await startWithout(["neverStartedState"]);
expect(startOnlyState.value).toBe(42);
expect(startedState.value).toBe(42);
expect(neverStartedState.value).toBe(undefined);
});
});