Bring in packages/montar
This commit is contained in:
parent
fe4509a2ae
commit
67f7cf8e1b
19 changed files with 800 additions and 24 deletions
73
packages/montar/src/index.spec.ts
Normal file
73
packages/montar/src/index.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
136
packages/montar/src/index.ts
Normal file
136
packages/montar/src/index.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
91
packages/montar/src/proxy.spec.ts
Normal file
91
packages/montar/src/proxy.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
130
packages/montar/src/proxy.ts
Normal file
130
packages/montar/src/proxy.ts
Normal 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,
|
||||
]);
|
||||
},
|
||||
}
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
37
packages/montar/src/starting.spec.ts
Normal file
37
packages/montar/src/starting.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue