Bring in packages/montar
This commit is contained in:
parent
fe4509a2ae
commit
67f7cf8e1b
19 changed files with 800 additions and 24 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -6,3 +6,5 @@ dist/**
|
||||||
.next/**
|
.next/**
|
||||||
docker/zammad/auto_install/**
|
docker/zammad/auto_install/**
|
||||||
.npmrc
|
.npmrc
|
||||||
|
coverage/
|
||||||
|
build/
|
||||||
|
|
|
||||||
48
package-lock.json
generated
48
package-lock.json
generated
|
|
@ -3369,18 +3369,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@digiresilience/montar": {
|
"node_modules/@digiresilience/montar": {
|
||||||
"version": "0.1.6",
|
"resolved": "packages/montar",
|
||||||
"resolved": "https://gitlab.com/api/v4/projects/22248969/packages/npm/@digiresilience/montar/-/@digiresilience/montar-0.1.6.tgz",
|
"link": true
|
||||||
"integrity": "sha1-PSVHC+/DCyaCnbYJHi3xH4d5e1o=",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "14.14.6",
|
|
||||||
"debug": "^4.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@digiresilience/montar/node_modules/@types/node": {
|
|
||||||
"version": "14.14.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz",
|
|
||||||
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw=="
|
|
||||||
},
|
},
|
||||||
"node_modules/@digiresilience/node-signald": {
|
"node_modules/@digiresilience/node-signald": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
|
|
@ -25757,6 +25747,22 @@
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/montar": {
|
||||||
|
"version": "0.1.6",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-preset-link": "*",
|
||||||
|
"eslint-config-link": "*",
|
||||||
|
"jest-config-link": "*",
|
||||||
|
"tsconfig-link": "*"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/tsconfig": {
|
"packages/tsconfig": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"extraneous": true,
|
"extraneous": true,
|
||||||
|
|
@ -27612,19 +27618,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@digiresilience/montar": {
|
"@digiresilience/montar": {
|
||||||
"version": "0.1.6",
|
"version": "file:packages/montar",
|
||||||
"resolved": "https://gitlab.com/api/v4/projects/22248969/packages/npm/@digiresilience/montar/-/@digiresilience/montar-0.1.6.tgz",
|
|
||||||
"integrity": "sha1-PSVHC+/DCyaCnbYJHi3xH4d5e1o=",
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "14.14.6",
|
"babel-preset-link": "*",
|
||||||
"debug": "^4.3.2"
|
"debug": "^4.3.2",
|
||||||
},
|
"eslint-config-link": "*",
|
||||||
"dependencies": {
|
"jest-config-link": "*",
|
||||||
"@types/node": {
|
"tsconfig-link": "*"
|
||||||
"version": "14.14.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz",
|
|
||||||
"integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@digiresilience/node-signald": {
|
"@digiresilience/node-signald": {
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,12 @@ module.exports = {
|
||||||
],
|
],
|
||||||
"no-extra-semi": "off",
|
"no-extra-semi": "off",
|
||||||
"@typescript-eslint/no-extra-semi": "error",
|
"@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