diff --git a/.gitignore b/.gitignore
index 2882fb7..81093de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,5 @@ dist/**
.next/**
docker/zammad/auto_install/**
.npmrc
+coverage/
+build/
diff --git a/package-lock.json b/package-lock.json
index 539c1d5..617eb15 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3369,18 +3369,8 @@
}
},
"node_modules/@digiresilience/montar": {
- "version": "0.1.6",
- "resolved": "https://gitlab.com/api/v4/projects/22248969/packages/npm/@digiresilience/montar/-/@digiresilience/montar-0.1.6.tgz",
- "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=="
+ "resolved": "packages/montar",
+ "link": true
},
"node_modules/@digiresilience/node-signald": {
"version": "0.0.3",
@@ -25757,6 +25747,22 @@
"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": {
"version": "0.1.4",
"extraneous": true,
@@ -27612,19 +27618,13 @@
}
},
"@digiresilience/montar": {
- "version": "0.1.6",
- "resolved": "https://gitlab.com/api/v4/projects/22248969/packages/npm/@digiresilience/montar/-/@digiresilience/montar-0.1.6.tgz",
- "integrity": "sha1-PSVHC+/DCyaCnbYJHi3xH4d5e1o=",
+ "version": "file:packages/montar",
"requires": {
- "@types/node": "14.14.6",
- "debug": "^4.3.2"
- },
- "dependencies": {
- "@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=="
- }
+ "babel-preset-link": "*",
+ "debug": "^4.3.2",
+ "eslint-config-link": "*",
+ "jest-config-link": "*",
+ "tsconfig-link": "*"
}
},
"@digiresilience/node-signald": {
diff --git a/packages/eslint-config-link/profile/typescript.js b/packages/eslint-config-link/profile/typescript.js
index c9d18d2..a5a7175 100644
--- a/packages/eslint-config-link/profile/typescript.js
+++ b/packages/eslint-config-link/profile/typescript.js
@@ -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",
+ },
+ ],
},
};
diff --git a/packages/montar/.editorconfig b/packages/montar/.editorconfig
new file mode 100644
index 0000000..63187fe
--- /dev/null
+++ b/packages/montar/.editorconfig
@@ -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
diff --git a/packages/montar/.eslintrc.js b/packages/montar/.eslintrc.js
new file mode 100644
index 0000000..ec3aad3
--- /dev/null
+++ b/packages/montar/.eslintrc.js
@@ -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: "^_",
+ },
+ ],
+ },
+};
diff --git a/packages/montar/.prettierignore b/packages/montar/.prettierignore
new file mode 100644
index 0000000..0e80a3c
--- /dev/null
+++ b/packages/montar/.prettierignore
@@ -0,0 +1,2 @@
+# package.json is formatted by package managers, so we ignore it here
+package.json
\ No newline at end of file
diff --git a/packages/montar/CHANGELOG.md b/packages/montar/CHANGELOG.md
new file mode 100644
index 0000000..b40cbd0
--- /dev/null
+++ b/packages/montar/CHANGELOG.md
@@ -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)
diff --git a/packages/montar/Makefile b/packages/montar/Makefile
new file mode 100644
index 0000000..a3c5c33
--- /dev/null
+++ b/packages/montar/Makefile
@@ -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
diff --git a/packages/montar/README.md b/packages/montar/README.md
new file mode 100644
index 0000000..23a9749
--- /dev/null
+++ b/packages/montar/README.md
@@ -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]
[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 .
+
+[cdrtech]: https://digiresilience.org/tech/
+[cdr]: https://digiresilience.org
diff --git a/packages/montar/babel.config.json b/packages/montar/babel.config.json
new file mode 100644
index 0000000..708ea0e
--- /dev/null
+++ b/packages/montar/babel.config.json
@@ -0,0 +1,5 @@
+{
+ "presets": [
+ "babel-preset-link"
+ ]
+}
diff --git a/packages/montar/jest.config.json b/packages/montar/jest.config.json
new file mode 100644
index 0000000..294020f
--- /dev/null
+++ b/packages/montar/jest.config.json
@@ -0,0 +1,4 @@
+{
+ "preset": "jest-config-link"
+}
+
diff --git a/packages/montar/package.json b/packages/montar/package.json
new file mode 100644
index 0000000..69f24c8
--- /dev/null
+++ b/packages/montar/package.json
@@ -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 ",
+ "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"
+ }
+}
diff --git a/packages/montar/src/index.spec.ts b/packages/montar/src/index.spec.ts
new file mode 100644
index 0000000..422cc3e
--- /dev/null
+++ b/packages/montar/src/index.spec.ts
@@ -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("inc41", {
+ isFunction: true,
+ start: async () => _inc41,
+});
+const inc = defState("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();
+ });
+});
diff --git a/packages/montar/src/index.ts b/packages/montar/src/index.ts
new file mode 100644
index 0000000..d556e6a
--- /dev/null
+++ b/packages/montar/src/index.ts
@@ -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 = () => Promise;
+
+interface StateMeta {
+ name: string;
+ start: Start;
+ stop(): Promise;
+ isFunction: boolean;
+ setHandler(handler: ProxyHandler): void;
+ setTarget(target: T): void;
+}
+
+interface DefState {
+ start: Start;
+ stop?(): Promise;
+ isFunction?: boolean;
+}
+interface StateMetaMap {
+ [name: string]: StateMeta;
+}
+
+interface StateMap {
+ [name: string]: any;
+}
+
+const defaultStop = async () => {};
+const states: StateMap = {};
+const statesMeta: StateMetaMap = {};
+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) {
+ 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(name: string, meta: DefState): 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 {
+ 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 {
+ return _start(statesOrder);
+}
+
+export async function startWithout(excluded: string[]): Promise {
+ const toStart = statesOrder.filter((name) => !excluded.includes(name));
+ return _start(toStart);
+}
+
+export async function startOnly(included: string[]): Promise {
+ const toStart = statesOrder.filter((name) => included.includes(name));
+ debug(" startOnly ", included);
+ return _start(toStart);
+}
+
+export async function stop(): Promise {
+ 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}`);
+ }
+}
diff --git a/packages/montar/src/proxy.spec.ts b/packages/montar/src/proxy.spec.ts
new file mode 100644
index 0000000..c7d7531
--- /dev/null
+++ b/packages/montar/src/proxy.spec.ts
@@ -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);
+ });
+});
diff --git a/packages/montar/src/proxy.ts b/packages/montar/src/proxy.ts
new file mode 100644
index 0000000..de365a6
--- /dev/null
+++ b/packages/montar/src/proxy.ts
@@ -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 implements ProxyHandler {
+ 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 {
+ setTarget(target: T): void;
+ setHandler(handler: PProxyHandler): void;
+ getTarget(): T;
+ getHandler(): ProxyHandler;
+ proxy: T;
+}
+
+export function mutableProxyFactory(
+ mutableTarget: T,
+ mutableHandler?: ProxyHandler
+): MutableProxy {
+ if (!mutableHandler) mutableHandler = new PProxyHandler();
+ return {
+ setTarget(target: T): void {
+ new Proxy(target, {}); // test target validity
+ mutableTarget = target;
+ },
+ setHandler(handler: PProxyHandler): 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 {
+ 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,
+ ]);
+ },
+ }
+ )
+ ),
+ };
+}
diff --git a/packages/montar/src/starting.spec.ts b/packages/montar/src/starting.spec.ts
new file mode 100644
index 0000000..02733b5
--- /dev/null
+++ b/packages/montar/src/starting.spec.ts
@@ -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);
+ });
+});
diff --git a/packages/montar/tsconfig.json b/packages/montar/tsconfig.json
new file mode 100644
index 0000000..77496bb
--- /dev/null
+++ b/packages/montar/tsconfig.json
@@ -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/**"]
+}
diff --git a/packages/montar/tsconfig.spec.json b/packages/montar/tsconfig.spec.json
new file mode 100644
index 0000000..9674ab1
--- /dev/null
+++ b/packages/montar/tsconfig.spec.json
@@ -0,0 +1,4 @@
+{
+ "exclude": ["node_modules"],
+ "extends": "./tsconfig.json"
+}