wardens 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -2
- package/README.md +8 -4
- package/dist/wardens.es.js +18 -18
- package/dist/wardens.umd.js +1 -1
- package/package.json +13 -4
- package/src/__tests__/allocation.test.ts +13 -13
- package/src/__tests__/resource.test.ts +11 -10
- package/src/allocation.ts +16 -13
- package/src/index.ts +2 -1
- package/src/resource.ts +19 -22
- package/src/state.ts +2 -2
- package/src/types.ts +8 -0
package/CHANGELOG.md
CHANGED
@@ -6,7 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
-
## [0.
|
9
|
+
## [0.2.0] - 2022-05-24
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
|
13
|
+
- `mount(...)` and `allocate(...)` no longer require a config argument if the resource doesn't explicitly define one.
|
14
|
+
|
15
|
+
### Added
|
16
|
+
|
17
|
+
- `enter(...)` now supports variable arguments.
|
18
|
+
|
19
|
+
### Changed
|
20
|
+
|
21
|
+
- The second generic parameter of `Resource` was a config parameter, but now it's an argument tuple.
|
22
|
+
- The `ExternalControls` utility type was renamed to `Controls`.
|
23
|
+
|
24
|
+
## [0.1.0] - 2022-05-22
|
10
25
|
|
11
26
|
### Added
|
12
27
|
|
@@ -14,5 +29,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
14
29
|
- `mount`/`unmount` hooks to provision resources
|
15
30
|
- `allocate`/`deallocate` for creating hierarchies of resources
|
16
31
|
|
17
|
-
[Unreleased]: https://github.com/PsychoLlama/wardens/compare/v0.
|
32
|
+
[Unreleased]: https://github.com/PsychoLlama/wardens/compare/v0.2.0...HEAD
|
33
|
+
[0.2.0]: https://github.com/PsychoLlama/wardens/compare/v0.1.0...v0.2.0
|
18
34
|
[0.1.0]: https://github.com/PsychoLlama/wardens/releases/tag/v0.1.0
|
package/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
A tiny framework for managing resources
|
1
|
+
<div align="center">
|
2
|
+
<h1>Wardens</h1>
|
3
|
+
<p>A tiny framework for managing resources.</p>
|
4
|
+
<img alt="Build status" src="https://img.shields.io/github/workflow/status/PsychoLlama/wardens/Test/main" />
|
5
|
+
<img alt="TypeScript" src="https://img.shields.io/npm/types/wardens" />
|
6
|
+
<img alt="npm version" src="https://img.shields.io/npm/v/wardens" />
|
7
|
+
</div>
|
4
8
|
|
5
9
|
## Overview
|
6
10
|
|
@@ -30,7 +34,7 @@ class Worker extends Resource<Thread> {
|
|
30
34
|
Now define a pool that creates and manages workers:
|
31
35
|
|
32
36
|
```typescript
|
33
|
-
class WorkerPool extends Resource<Controls, Config> {
|
37
|
+
class WorkerPool extends Resource<Controls, [Config]> {
|
34
38
|
threads!: Array<Thread> = [];
|
35
39
|
|
36
40
|
async enter({ poolSize }: Config) {
|
package/dist/wardens.es.js
CHANGED
@@ -17,25 +17,25 @@ const ownership = /* @__PURE__ */ new WeakMap();
|
|
17
17
|
function wrapWithProxy(value) {
|
18
18
|
return Proxy.revocable(value, {});
|
19
19
|
}
|
20
|
-
async function mount(
|
21
|
-
const resource = new
|
22
|
-
await resource.enter(
|
23
|
-
const
|
24
|
-
const { proxy, revoke } = wrapWithProxy(
|
20
|
+
async function mount(Entity, ...args) {
|
21
|
+
const resource = new Entity();
|
22
|
+
await resource.enter(...args);
|
23
|
+
const controls = resource.exports();
|
24
|
+
const { proxy, revoke } = wrapWithProxy(controls);
|
25
25
|
resources.set(proxy, {
|
26
26
|
resource,
|
27
27
|
revoke
|
28
28
|
});
|
29
29
|
return proxy;
|
30
30
|
}
|
31
|
-
async function unmount(
|
32
|
-
const entry = resources.get(
|
31
|
+
async function unmount(controls) {
|
32
|
+
const entry = resources.get(controls);
|
33
33
|
if (entry) {
|
34
|
-
resources.delete(
|
34
|
+
resources.delete(controls);
|
35
35
|
entry.revoke();
|
36
36
|
const children = ownership.get(entry.resource);
|
37
37
|
ownership.delete(entry.resource);
|
38
|
-
const recursiveUnmounts = children.map((
|
38
|
+
const recursiveUnmounts = children.map((controls2) => unmount(controls2));
|
39
39
|
const results = await Promise.allSettled(recursiveUnmounts);
|
40
40
|
await entry.resource.leave();
|
41
41
|
results.forEach((result) => {
|
@@ -51,23 +51,23 @@ class Resource {
|
|
51
51
|
__privateAdd(this, _children, []);
|
52
52
|
ownership.set(this, __privateGet(this, _children));
|
53
53
|
}
|
54
|
-
async enter(
|
54
|
+
async enter(..._args) {
|
55
55
|
return;
|
56
56
|
}
|
57
57
|
async leave() {
|
58
58
|
return;
|
59
59
|
}
|
60
|
-
async allocate(Child,
|
61
|
-
const
|
62
|
-
__privateGet(this, _resources).add(
|
63
|
-
__privateGet(this, _children).push(
|
64
|
-
return
|
60
|
+
async allocate(Child, ...args) {
|
61
|
+
const controls = await mount(Child, ...args);
|
62
|
+
__privateGet(this, _resources).add(controls);
|
63
|
+
__privateGet(this, _children).push(controls);
|
64
|
+
return controls;
|
65
65
|
}
|
66
|
-
async deallocate(
|
67
|
-
if (!__privateGet(this, _resources).has(
|
66
|
+
async deallocate(resource) {
|
67
|
+
if (!__privateGet(this, _resources).has(resource)) {
|
68
68
|
throw new Error("You do not own this resource.");
|
69
69
|
}
|
70
|
-
return unmount(
|
70
|
+
return unmount(resource);
|
71
71
|
}
|
72
72
|
}
|
73
73
|
_resources = new WeakMap();
|
package/dist/wardens.umd.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
var b=(e,t,c)=>{if(!t.has(e))throw TypeError("Cannot "+c)};var f=(e,t,c)=>(b(e,t,"read from private field"),c?c.call(e):t.get(e)),h=(e,t,c)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,c)};(function(e,t){typeof exports=="object"&&typeof module!="undefined"?t(exports):typeof define=="function"&&define.amd?define(["exports"],t):(e=typeof globalThis!="undefined"?globalThis:e||self,t(e.wardens={}))})(this,function(e){var a,d;"use strict";const t=new WeakMap,c=new WeakMap;function
|
1
|
+
var b=(e,t,c)=>{if(!t.has(e))throw TypeError("Cannot "+c)};var f=(e,t,c)=>(b(e,t,"read from private field"),c?c.call(e):t.get(e)),h=(e,t,c)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,c)};(function(e,t){typeof exports=="object"&&typeof module!="undefined"?t(exports):typeof define=="function"&&define.amd?define(["exports"],t):(e=typeof globalThis!="undefined"?globalThis:e||self,t(e.wardens={}))})(this,function(e){var a,d;"use strict";const t=new WeakMap,c=new WeakMap;function p(i){return Proxy.revocable(i,{})}async function w(i,...n){const o=new i;await o.enter(...n);const s=o.exports(),{proxy:r,revoke:u}=p(s);return t.set(r,{resource:o,revoke:u}),r}async function l(i){const n=t.get(i);if(n){t.delete(i),n.revoke();const o=c.get(n.resource);c.delete(n.resource);const s=o.map(u=>l(u)),r=await Promise.allSettled(s);await n.resource.leave(),r.forEach(u=>{if(u.status==="rejected")throw u.reason})}}class y{constructor(){h(this,a,new WeakSet);h(this,d,[]);c.set(this,f(this,d))}async enter(...n){}async leave(){}async allocate(n,...o){const s=await w(n,...o);return f(this,a).add(s),f(this,d).push(s),s}async deallocate(n){if(!f(this,a).has(n))throw new Error("You do not own this resource.");return l(n)}}a=new WeakMap,d=new WeakMap;function m(i){const n=new WeakMap;return new Proxy(i,{get(o,s){const r=Reflect.get(o,s,o);if(typeof r=="function"){if(n.has(r)===!1){const u=r.bind(o);Object.defineProperties(u,Object.getOwnPropertyDescriptors(r)),n.set(r,u)}return n.get(r)}return r},set(o,s,r){return Reflect.set(o,s,r,o)}})}e.Resource=y,e.bindContext=m,e.mount=w,e.unmount=l,Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "wardens",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.2.0",
|
4
4
|
"description": "A framework for resource management",
|
5
5
|
"main": "./dist/wardens.umd.js",
|
6
6
|
"module": "./dist/wardens.es.js",
|
@@ -11,6 +11,15 @@
|
|
11
11
|
"dist",
|
12
12
|
"src"
|
13
13
|
],
|
14
|
+
"keywords": [
|
15
|
+
"resource",
|
16
|
+
"management",
|
17
|
+
"manager",
|
18
|
+
"server",
|
19
|
+
"lifetimes",
|
20
|
+
"hierarchy",
|
21
|
+
"framework"
|
22
|
+
],
|
14
23
|
"scripts": {
|
15
24
|
"prepare": "tsc && vite build",
|
16
25
|
"test": "./bin/run-tests",
|
@@ -74,14 +83,14 @@
|
|
74
83
|
},
|
75
84
|
"devDependencies": {
|
76
85
|
"@types/jest": "27.5.1",
|
77
|
-
"@typescript-eslint/eslint-plugin": "5.
|
78
|
-
"@typescript-eslint/parser": "5.
|
86
|
+
"@typescript-eslint/eslint-plugin": "5.26.0",
|
87
|
+
"@typescript-eslint/parser": "5.26.0",
|
79
88
|
"eslint": "8.16.0",
|
80
89
|
"husky": "8.0.1",
|
81
90
|
"jest": "28.1.0",
|
82
91
|
"lint-staged": "12.4.1",
|
83
92
|
"prettier": "2.6.2",
|
84
|
-
"ts-jest": "28.0.
|
93
|
+
"ts-jest": "28.0.3",
|
85
94
|
"typescript": "4.6.4",
|
86
95
|
"vite": "2.9.9",
|
87
96
|
"vite-dts": "1.0.4"
|
@@ -4,16 +4,16 @@ import { mount, unmount } from '../allocation';
|
|
4
4
|
describe('allocation', () => {
|
5
5
|
describe('mount', () => {
|
6
6
|
it('allocates the resource', async () => {
|
7
|
-
const
|
7
|
+
const config = { test: 'init-args' };
|
8
8
|
const enter = jest.fn();
|
9
9
|
|
10
|
-
class Test extends Resource<{ test: boolean }, { test: string }> {
|
10
|
+
class Test extends Resource<{ test: boolean }, [{ test: string }]> {
|
11
11
|
exports = () => ({ test: true });
|
12
12
|
enter = enter;
|
13
13
|
}
|
14
14
|
|
15
|
-
await expect(mount(Test,
|
16
|
-
expect(enter).toHaveBeenCalledWith(
|
15
|
+
await expect(mount(Test, config)).resolves.toEqual({ test: true });
|
16
|
+
expect(enter).toHaveBeenCalledWith(config);
|
17
17
|
});
|
18
18
|
});
|
19
19
|
|
@@ -26,10 +26,10 @@ describe('allocation', () => {
|
|
26
26
|
leave = leave;
|
27
27
|
}
|
28
28
|
|
29
|
-
const
|
29
|
+
const test = await mount(Test);
|
30
30
|
|
31
31
|
expect(leave).not.toHaveBeenCalled();
|
32
|
-
await expect(unmount(
|
32
|
+
await expect(unmount(test)).resolves.not.toThrow();
|
33
33
|
expect(leave).toHaveBeenCalled();
|
34
34
|
});
|
35
35
|
|
@@ -38,10 +38,10 @@ describe('allocation', () => {
|
|
38
38
|
exports = () => [];
|
39
39
|
}
|
40
40
|
|
41
|
-
const
|
42
|
-
await unmount(
|
41
|
+
const test = await mount(Test);
|
42
|
+
await unmount(test);
|
43
43
|
|
44
|
-
await expect(unmount(
|
44
|
+
await expect(unmount(test)).resolves.not.toThrow();
|
45
45
|
});
|
46
46
|
|
47
47
|
it('automatically unmounts all children', async () => {
|
@@ -54,11 +54,11 @@ describe('allocation', () => {
|
|
54
54
|
class Parent extends Resource<string[]> {
|
55
55
|
exports = () => [];
|
56
56
|
async enter() {
|
57
|
-
await this.allocate(Child
|
57
|
+
await this.allocate(Child);
|
58
58
|
}
|
59
59
|
}
|
60
60
|
|
61
|
-
const parent = await mount(Parent
|
61
|
+
const parent = await mount(Parent);
|
62
62
|
await unmount(parent);
|
63
63
|
|
64
64
|
expect(leave).toHaveBeenCalled();
|
@@ -76,11 +76,11 @@ describe('allocation', () => {
|
|
76
76
|
class Parent extends Resource<string[]> {
|
77
77
|
exports = () => [];
|
78
78
|
async enter() {
|
79
|
-
await this.allocate(Child
|
79
|
+
await this.allocate(Child);
|
80
80
|
}
|
81
81
|
}
|
82
82
|
|
83
|
-
const parent = await mount(Parent
|
83
|
+
const parent = await mount(Parent);
|
84
84
|
await expect(unmount(parent)).rejects.toThrow(error);
|
85
85
|
});
|
86
86
|
});
|
@@ -1,5 +1,6 @@
|
|
1
|
-
import Resource
|
1
|
+
import Resource from '../resource';
|
2
2
|
import { mount } from '../allocation';
|
3
|
+
import { type Controls } from '../types';
|
3
4
|
|
4
5
|
describe('Resource', () => {
|
5
6
|
class Test extends Resource<Record<string, never>> {
|
@@ -18,16 +19,16 @@ describe('Resource', () => {
|
|
18
19
|
exports = () => ({ child: true });
|
19
20
|
}
|
20
21
|
|
21
|
-
class Parent extends Resource<
|
22
|
+
class Parent extends Resource<Controls<Child>> {
|
22
23
|
exports = () => this.child;
|
23
|
-
child!:
|
24
|
+
child!: Controls<Child>;
|
24
25
|
|
25
26
|
async enter() {
|
26
|
-
this.child = await this.allocate(Child
|
27
|
+
this.child = await this.allocate(Child);
|
27
28
|
}
|
28
29
|
}
|
29
30
|
|
30
|
-
await expect(mount(Parent
|
31
|
+
await expect(mount(Parent)).resolves.toEqual({ child: true });
|
31
32
|
});
|
32
33
|
|
33
34
|
it('can deallocate child resources on demand', async () => {
|
@@ -40,20 +41,20 @@ describe('Resource', () => {
|
|
40
41
|
|
41
42
|
class Parent extends Resource<{ parent: boolean }> {
|
42
43
|
exports = () => ({ parent: true });
|
43
|
-
child!:
|
44
|
+
child!: Controls<Child>;
|
44
45
|
|
45
46
|
async enter() {
|
46
|
-
const child = await this.allocate(Child
|
47
|
+
const child = await this.allocate(Child);
|
47
48
|
await this.deallocate(child);
|
48
49
|
}
|
49
50
|
}
|
50
51
|
|
51
|
-
await expect(mount(Parent
|
52
|
+
await expect(mount(Parent)).resolves.toEqual({ parent: true });
|
52
53
|
expect(leave).toHaveBeenCalled();
|
53
54
|
});
|
54
55
|
|
55
56
|
it('fails to destroy resources owned by someone else', async () => {
|
56
|
-
const test = await mount(Test
|
57
|
+
const test = await mount(Test);
|
57
58
|
|
58
59
|
class Sneaky extends Resource<string[]> {
|
59
60
|
exports = () => [];
|
@@ -62,6 +63,6 @@ describe('Resource', () => {
|
|
62
63
|
}
|
63
64
|
}
|
64
65
|
|
65
|
-
await expect(mount(Sneaky
|
66
|
+
await expect(mount(Sneaky)).rejects.toThrow(/do not own/i);
|
66
67
|
});
|
67
68
|
});
|
package/src/allocation.ts
CHANGED
@@ -3,15 +3,18 @@ import { resources, ownership } from './state';
|
|
3
3
|
import wrap from './proxy';
|
4
4
|
|
5
5
|
/** Provision a resource and return its external API. */
|
6
|
-
export async function mount<
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
const
|
14
|
-
|
6
|
+
export async function mount<
|
7
|
+
Controls extends object,
|
8
|
+
Args extends Array<unknown>,
|
9
|
+
>(
|
10
|
+
Entity: new () => Resource<Controls, Args>,
|
11
|
+
...args: Args
|
12
|
+
): Promise<Controls> {
|
13
|
+
const resource = new Entity();
|
14
|
+
await resource.enter(...args);
|
15
|
+
|
16
|
+
const controls = resource.exports();
|
17
|
+
const { proxy, revoke } = wrap(controls);
|
15
18
|
|
16
19
|
resources.set(proxy, {
|
17
20
|
resource,
|
@@ -27,12 +30,12 @@ export async function mount<Api extends object, InitArgs>(
|
|
27
30
|
*
|
28
31
|
* @todo Add type marker to catch cases where the wrong object is unmounted.
|
29
32
|
*/
|
30
|
-
export async function unmount(
|
31
|
-
const entry = resources.get(
|
33
|
+
export async function unmount(controls: object) {
|
34
|
+
const entry = resources.get(controls);
|
32
35
|
|
33
36
|
if (entry) {
|
34
37
|
// Instantly delete to prevent race conditions.
|
35
|
-
resources.delete(
|
38
|
+
resources.delete(controls);
|
36
39
|
|
37
40
|
// Free all references.
|
38
41
|
entry.revoke();
|
@@ -41,7 +44,7 @@ export async function unmount(api: object) {
|
|
41
44
|
ownership.delete(entry.resource);
|
42
45
|
|
43
46
|
// Recursively close out the children first...
|
44
|
-
const recursiveUnmounts = children.map((
|
47
|
+
const recursiveUnmounts = children.map((controls) => unmount(controls));
|
45
48
|
const results = await Promise.allSettled(recursiveUnmounts);
|
46
49
|
|
47
50
|
// Then close the parent.
|
package/src/index.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
-
export { default as Resource
|
1
|
+
export { default as Resource } from './resource';
|
2
2
|
export { default as bindContext } from './bind-context';
|
3
3
|
export { mount, unmount } from './allocation';
|
4
|
+
export type { Controls } from './types';
|
package/src/resource.ts
CHANGED
@@ -7,8 +7,8 @@ import { ownership } from './state';
|
|
7
7
|
* first tears down the children.
|
8
8
|
*/
|
9
9
|
export default abstract class Resource<
|
10
|
-
|
11
|
-
|
10
|
+
Controls extends object,
|
11
|
+
Args extends Array<unknown> = [],
|
12
12
|
> {
|
13
13
|
#resources = new WeakSet<object>();
|
14
14
|
#children: Array<object> = [];
|
@@ -19,7 +19,7 @@ export default abstract class Resource<
|
|
19
19
|
|
20
20
|
/** A hook that gets called when the resource is created. */
|
21
21
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
22
|
-
async enter(
|
22
|
+
async enter(..._args: Args) {
|
23
23
|
return;
|
24
24
|
}
|
25
25
|
|
@@ -29,35 +29,32 @@ export default abstract class Resource<
|
|
29
29
|
}
|
30
30
|
|
31
31
|
/** Provision an owned resource and make sure it doesn't outlive us. */
|
32
|
-
async allocate<
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
32
|
+
async allocate<
|
33
|
+
ChildControls extends object,
|
34
|
+
ChildArgs extends Array<unknown> = [],
|
35
|
+
>(
|
36
|
+
Child: new () => Resource<ChildControls, ChildArgs>,
|
37
|
+
...args: ChildArgs
|
38
|
+
): Promise<ChildControls> {
|
39
|
+
const controls = await mount(Child, ...args);
|
40
|
+
this.#resources.add(controls);
|
41
|
+
this.#children.push(controls);
|
42
|
+
|
43
|
+
return controls;
|
41
44
|
}
|
42
45
|
|
43
46
|
/**
|
44
47
|
* Tear down a resource. Happens automatically when resource owners are
|
45
48
|
* deallocated.
|
46
49
|
*/
|
47
|
-
async deallocate(
|
48
|
-
if (!this.#resources.has(
|
50
|
+
async deallocate(resource: object) {
|
51
|
+
if (!this.#resources.has(resource)) {
|
49
52
|
throw new Error('You do not own this resource.');
|
50
53
|
}
|
51
54
|
|
52
|
-
return unmount(
|
55
|
+
return unmount(resource);
|
53
56
|
}
|
54
57
|
|
55
58
|
/** Returns an external API to the parent resource. */
|
56
|
-
abstract exports():
|
59
|
+
abstract exports(): Controls;
|
57
60
|
}
|
58
|
-
|
59
|
-
/** The `exports` type for a resource. */
|
60
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
61
|
-
export type ExternalControls<ArbitraryResource extends Resource<any, any>> =
|
62
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
63
|
-
ArbitraryResource extends Resource<infer Api, any> ? Api : never;
|
package/src/state.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import type Resource from './resource';
|
2
2
|
|
3
3
|
interface RevokableResource {
|
4
|
-
resource: Resource<object, unknown
|
4
|
+
resource: Resource<object, Array<unknown>>;
|
5
5
|
|
6
6
|
/**
|
7
7
|
* Destroys outer references to the API and frees the object for garbage
|
@@ -15,6 +15,6 @@ export const resources = new WeakMap<object, RevokableResource>();
|
|
15
15
|
|
16
16
|
/** Maps a resource to all the other resources it provisioned. */
|
17
17
|
export const ownership = new WeakMap<
|
18
|
-
Resource<object, unknown
|
18
|
+
Resource<object, Array<unknown>>,
|
19
19
|
Array<object>
|
20
20
|
>();
|
package/src/types.ts
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
import type Resource from './resource';
|
2
|
+
|
3
|
+
/** The `exports` type for a resource. */
|
4
|
+
export type Controls<
|
5
|
+
ArbitraryResource extends Resource<object, Array<unknown>>,
|
6
|
+
> = ArbitraryResource extends Resource<infer Controls, Array<unknown>>
|
7
|
+
? Controls
|
8
|
+
: never;
|