Bring in packages/montar
This commit is contained in:
parent
fe4509a2ae
commit
67f7cf8e1b
19 changed files with 800 additions and 24 deletions
|
|
@ -17,5 +17,12 @@ module.exports = {
|
|||
],
|
||||
"no-extra-semi": "off",
|
||||
"@typescript-eslint/no-extra-semi": "error",
|
||||
"@typescript-eslint/ban-ts-comment": [
|
||||
"error",
|
||||
{
|
||||
"ts-nocheck": "allow-with-description",
|
||||
"ts-expect-error": "allow-with-description",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
15
packages/montar/.editorconfig
Normal file
15
packages/montar/.editorconfig
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 80
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = 0
|
||||
trim_trailing_whitespace = false
|
||||
18
packages/montar/.eslintrc.js
Normal file
18
packages/montar/.eslintrc.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
require("eslint-config-link/patch/modern-module-resolution");
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint-config-link/profile/node",
|
||||
"eslint-config-link/profile/typescript"
|
||||
],
|
||||
parserOptions: { tsconfigRootDir: __dirname, project: "./tsconfig.spec.json" },
|
||||
rules: {
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
2
packages/montar/.prettierignore
Normal file
2
packages/montar/.prettierignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# package.json is formatted by package managers, so we ignore it here
|
||||
package.json
|
||||
32
packages/montar/CHANGELOG.md
Normal file
32
packages/montar/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [0.1.6](https://gitlab.com/digiresilience/link/montar/compare/0.1.5...0.1.6) (2021-10-11)
|
||||
|
||||
### [0.1.5](https://gitlab.com/digiresilience/link/montar/compare/0.1.4...0.1.5) (2021-10-08)
|
||||
|
||||
### [0.1.4](https://gitlab.com/digiresilience/link/montar/compare/0.1.3...0.1.4) (2021-10-08)
|
||||
|
||||
### [0.1.3](https://gitlab.com/digiresilience/link/montar/compare/0.1.2...0.1.3) (2021-10-08)
|
||||
|
||||
### [0.1.2](https://gitlab.com/digiresilience/link/montar/compare/0.1.1...0.1.2) (2020-11-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add startOnly function to start only certain states ([b776b66](https://gitlab.com/digiresilience/link/montar/commit/b776b6640efe52f934b8afecc64f5e8c2b1be0f3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* only stop states that were started ([178bbd2](https://gitlab.com/digiresilience/link/montar/commit/178bbd2296671ed480bcc8d22c5f72736f6335b8))
|
||||
|
||||
### [0.1.1](https://gitlab.com/digiresilience/link/montar/compare/0.1.0...0.1.1) (2020-11-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Use debug module for optional debugging ([0cb617c](https://gitlab.com/digiresilience/link/montar/commit/0cb617cf4eac9178cd56af8834bc429f656beba5))
|
||||
|
||||
## 0.1.0 (2020-11-10)
|
||||
37
packages/montar/Makefile
Normal file
37
packages/montar/Makefile
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
test: build
|
||||
mkdir -p coverage
|
||||
yarn test
|
||||
|
||||
.PHONY: build
|
||||
build: node_modules/
|
||||
yarn build
|
||||
|
||||
lint:
|
||||
yarn test:lint
|
||||
|
||||
fmt:
|
||||
yarn fix:prettier
|
||||
yarn fix:lint
|
||||
|
||||
doc:
|
||||
yarn doc
|
||||
|
||||
publish: test
|
||||
npm publish
|
||||
|
||||
node_modules/: .npmrc
|
||||
test -d node_modules || yarn
|
||||
|
||||
.npmrc:
|
||||
ifdef CI_JOB_TOKEN
|
||||
echo '@guardianproject-ops:registry=https://gitlab.com/api/v4/packages/npm/' > .npmrc
|
||||
echo '//gitlab.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}' >> .npmrc
|
||||
echo '//gitlab.com/api/v4/projects/:_authToken=${CI_JOB_TOKEN}' >> .npmrc
|
||||
echo '//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}' >> .npmrc
|
||||
endif
|
||||
|
||||
clean:
|
||||
rm -rf build tmp coverage yarn-error.log
|
||||
|
||||
distclean: clean
|
||||
rm -rf node_modules
|
||||
138
packages/montar/README.md
Normal file
138
packages/montar/README.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# montar
|
||||
|
||||
manage state in typescript. inspired by [tolitius/mount](https://github.com/tolitius/mount) from clojure.
|
||||
|
||||
What's this all about? Watch this [video from Stuart Sierra](https://www.youtube.com/watch?v=13cmHf_kt-Q) to learn more about the background of component, mount, and montar.
|
||||
|
||||
## Install
|
||||
|
||||
```console
|
||||
$ yarn add -D @digiresilience/montar
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
import { defState } from "@digiresilience/montar"
|
||||
```
|
||||
|
||||
### Creating State
|
||||
|
||||
```
|
||||
// db.ts
|
||||
const db = defState("db", {
|
||||
start: createDbConnection
|
||||
})
|
||||
|
||||
export default db
|
||||
```
|
||||
|
||||
where the createDbConnection function creates a connection (for example to a database) and is defined elsewhere.
|
||||
|
||||
### Starting state
|
||||
|
||||
```
|
||||
import { start } from "@digiresilience/montar"
|
||||
|
||||
const bootYourApp = async () => {
|
||||
// .. prepare for app boot
|
||||
// ..
|
||||
|
||||
// boot!
|
||||
return start()
|
||||
}
|
||||
```
|
||||
|
||||
### Using State
|
||||
|
||||
But wait, there is more.. this state is a top level being, which means it can be simply imported by other namespaces:
|
||||
|
||||
For example let's say an app needs a connection above. No problem:
|
||||
|
||||
```
|
||||
// server.ts
|
||||
import db from "./db"
|
||||
|
||||
|
||||
async function doStuff() {
|
||||
return db.executeSql("foo")
|
||||
}
|
||||
```
|
||||
|
||||
Note that before `start()` is called, `db` will be uninitialized and therefore unusable. You must call `start()` before accessing any states.
|
||||
|
||||
### Starting/Stopping State
|
||||
|
||||
`montar` has start and stop functions that will walk all the states created with `defState` and start / stop them accordingly: i.e. will call their start and stop defined functions.
|
||||
|
||||
When testing you might be interested in starting only certain states. You can do this with
|
||||
|
||||
- `startOnly(string[])` - only start the given states. WARNING: dependencies are not auto started, so you must ensure all dependencies are passed.
|
||||
- `startWithout(string[])` - start all states _except_ those passed
|
||||
|
||||
### Start and Stop Order
|
||||
|
||||
Since dependencies are "injected" by requiring on the namespace level, montar trusts the typescript compiler and node.js runtime to maintain the start and stop order for all the defStates.
|
||||
|
||||
The "start" order is then recorded and replayed on each subsequent start
|
||||
|
||||
The "stop" order is simply the reverse of the start order.
|
||||
|
||||
### Troubleshooting / Debugging
|
||||
|
||||
Set the `DEBUG=montar` environment variable for runtime debug logs.
|
||||
|
||||
## Differences to mount
|
||||
|
||||
montar was created to bring management of top-level application state under
|
||||
control. It steals greedily from
|
||||
[tolitius/mount](https://github.com/tolitius/mount) a well-known library in the
|
||||
clojure and clojurescript community.
|
||||
|
||||
The problem solved is: how to load different sub-systems of your application while respecting dependency order? Also, how do achieve this without complicated dependency injection containers that make the code harder to read.
|
||||
|
||||
With montar once you define your state, then every other module can import and
|
||||
use your state without special syntax or wrappers.
|
||||
|
||||
montar differs from mount in these ways:
|
||||
|
||||
- **start/stop are async** - the start and stop functions are async
|
||||
- **no var rebinding** - instead we use the little-known [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
|
||||
- this comes with limitations: only functions, arrays, objects can be defined as state. scalars do not work.
|
||||
- **not concerned with reloading** - in clj/cljs you have the repl and great hot-code reloading (thanks to immutable and persistent data structures). we do not have this in javascript.
|
||||
|
||||
## Credits
|
||||
|
||||
Copyright © 2020-present [Center for Digital Resilience][cdr]
|
||||
|
||||
### Contributors
|
||||
|
||||
| [![Abel Luck][abelxluck_avatar]][abelxluck_homepage]<br/>[Abel Luck][abelxluck_homepage] |
|
||||
| ---------------------------------------------------------------------------------------- |
|
||||
|
||||
|
||||
[abelxluck_homepage]: https://gitlab.com/abelxluck
|
||||
[abelxluck_avatar]: https://secure.gravatar.com/avatar/0f605397e0ead93a68e1be26dc26481a?s=100&d=identicon
|
||||
|
||||
### License
|
||||
|
||||
[](https://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
[cdrtech]: https://digiresilience.org/tech/
|
||||
[cdr]: https://digiresilience.org
|
||||
5
packages/montar/babel.config.json
Normal file
5
packages/montar/babel.config.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"presets": [
|
||||
"babel-preset-link"
|
||||
]
|
||||
}
|
||||
4
packages/montar/jest.config.json
Normal file
4
packages/montar/jest.config.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"preset": "jest-config-link"
|
||||
}
|
||||
|
||||
33
packages/montar/package.json
Normal file
33
packages/montar/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "@digiresilience/montar",
|
||||
"version": "0.1.6",
|
||||
"description": "manage typescript state",
|
||||
"main": "build/main/index.js",
|
||||
"typings": "build/main/index.d.ts",
|
||||
"module": "build/module/index.js",
|
||||
"author": "Abel Luck <abel@guardianproject.info>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"fix:lint": "eslint src --ext .ts --fix",
|
||||
"fmt": "prettier \"src/**/*.ts\" --write",
|
||||
"test": "DEBUG=montar jest --config jest.config.json",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"lint-fmt": "prettier \"src/**/*.ts\" --list-different",
|
||||
"doc": "typedoc src/ --exclude '**/*.test.ts' --exclude '**/*.spec.ts' --name $npm_package_name --readme README.md --target es2019 --mode file --out build/docs",
|
||||
"watch:build": "tsc -p tsconfig.json -w"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsconfig-link": "*",
|
||||
"eslint-config-link": "*",
|
||||
"jest-config-link": "*",
|
||||
"babel-preset-link": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "^4.3.2"
|
||||
}
|
||||
}
|
||||
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);
|
||||
});
|
||||
});
|
||||
12
packages/montar/tsconfig.json
Normal file
12
packages/montar/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "tsconfig-link",
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"outDir": "build/main",
|
||||
"rootDir": "src",
|
||||
"baseUrl": "./",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules/**"]
|
||||
}
|
||||
4
packages/montar/tsconfig.spec.json
Normal file
4
packages/montar/tsconfig.spec.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"exclude": ["node_modules"],
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue