wardens 0.3.0 → 0.4.1
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 +21 -1
- package/README.md +27 -34
- package/dist/wardens.mjs +87 -0
- package/dist/wardens.umd.js +1 -1
- package/package.json +14 -14
- package/src/__tests__/allocation.test.ts +27 -41
- package/src/__tests__/resource-context.test.ts +63 -0
- package/src/allocation.ts +13 -27
- package/src/bind-context.ts +2 -3
- package/src/index.ts +2 -2
- package/src/resource-context.ts +44 -0
- package/src/state.ts +4 -4
- package/src/types.ts +19 -12
- package/dist/wardens.es.d.ts +0 -1
- package/dist/wardens.es.js +0 -99
- package/dist/wardens.umd.d.ts +0 -1
- package/src/__tests__/resource.test.ts +0 -61
- package/src/resource.ts +0 -49
package/CHANGELOG.md
CHANGED
@@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [0.4.1] - 2023-01-14
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
|
13
|
+
- Newer versions of TypeScript complained about signatures in `bindContext(...)`.
|
14
|
+
|
15
|
+
## [0.4.0] - 2022-06-19
|
16
|
+
|
17
|
+
### Added
|
18
|
+
|
19
|
+
- Support for provisioning resources through async functions instead of `Resource` subclasses. This offers better type safety around null conditions.
|
20
|
+
- A new `Resource` utility type is exported. The new functional API expects you to return this interface.
|
21
|
+
|
22
|
+
### Removed
|
23
|
+
|
24
|
+
- The `Resource` abstract class was removed. Use async functions instead.
|
25
|
+
- The `Controls<...>` utility type was removed. Import the type you need from the module instead.
|
26
|
+
|
9
27
|
## [0.3.0] - 2022-06-04
|
10
28
|
|
11
29
|
### Changed
|
@@ -42,7 +60,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
42
60
|
- `mount`/`unmount` hooks to provision resources
|
43
61
|
- `allocate`/`deallocate` for creating hierarchies of resources
|
44
62
|
|
45
|
-
[Unreleased]: https://github.com/PsychoLlama/wardens/compare/v0.
|
63
|
+
[Unreleased]: https://github.com/PsychoLlama/wardens/compare/v0.4.1...HEAD
|
64
|
+
[0.4.1]: https://github.com/PsychoLlama/wardens/compare/v0.4.0...v0.4.1
|
65
|
+
[0.4.0]: https://github.com/PsychoLlama/wardens/compare/v0.3.0...v0.4.0
|
46
66
|
[0.3.0]: https://github.com/PsychoLlama/wardens/compare/v0.2.0...v0.3.0
|
47
67
|
[0.2.0]: https://github.com/PsychoLlama/wardens/compare/v0.1.0...v0.2.0
|
48
68
|
[0.1.0]: https://github.com/PsychoLlama/wardens/releases/tag/v0.1.0
|
package/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
<div align="center">
|
2
2
|
<h1>Wardens</h1>
|
3
3
|
<p>A tiny framework for managing resources.</p>
|
4
|
-
<img alt="Build status" src="https://img.shields.io/github/workflow/status/PsychoLlama/wardens/
|
4
|
+
<img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/PsychoLlama/wardens/test.yml?branch=main" />
|
5
5
|
<img alt="TypeScript" src="https://img.shields.io/npm/types/wardens" />
|
6
6
|
<img alt="npm version" src="https://img.shields.io/npm/v/wardens" />
|
7
7
|
</div>
|
@@ -13,43 +13,36 @@ This library is designed for applications that dynamically provision and dealloc
|
|
13
13
|
Here's an example: let's say you've got a thread pool, one per CPU. Each thread gets a `Resource`, a small wrapper that hooks into setup and teardown controls.
|
14
14
|
|
15
15
|
```typescript
|
16
|
-
|
17
|
-
thread
|
16
|
+
async function Worker() {
|
17
|
+
const thread = await spawn();
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
}
|
19
|
+
return {
|
20
|
+
// The value returned after initialization completes
|
21
|
+
value: thread,
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
}
|
28
|
-
|
29
|
-
// The value returned after initialization completes
|
30
|
-
exports = () => this.thread
|
23
|
+
// Called when the resource is destroyed
|
24
|
+
destroy: () => thread.close(),
|
25
|
+
};
|
31
26
|
}
|
32
27
|
```
|
33
28
|
|
34
29
|
Now define a pool that creates and manages workers:
|
35
30
|
|
36
31
|
```typescript
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
doSomethingElse() {},
|
52
|
-
})
|
32
|
+
async function WorkerPool(
|
33
|
+
{ create }: ResourceContext,
|
34
|
+
config: { poolSize: number },
|
35
|
+
) {
|
36
|
+
const promises = Array(config.poolSize).fill(Worker).map(create);
|
37
|
+
const threads = await Promise.all(promises);
|
38
|
+
|
39
|
+
return {
|
40
|
+
// ... External API goes here ...
|
41
|
+
value: {
|
42
|
+
doSomeWork() {},
|
43
|
+
doSomethingElse() {},
|
44
|
+
},
|
45
|
+
};
|
53
46
|
}
|
54
47
|
```
|
55
48
|
|
@@ -58,17 +51,17 @@ Finally, create the pool:
|
|
58
51
|
```typescript
|
59
52
|
const pool = await create(WorkerPool, {
|
60
53
|
poolSize: cpus().length,
|
61
|
-
})
|
54
|
+
});
|
62
55
|
|
63
56
|
// Provisioned and ready to go!
|
64
|
-
pool.doSomeWork()
|
65
|
-
pool.doSomethingElse()
|
57
|
+
pool.doSomeWork();
|
58
|
+
pool.doSomethingElse();
|
66
59
|
```
|
67
60
|
|
68
61
|
The magic of this framework is that resources never outlive their owners. If you tear down the pool, it will deallocate everything beneath it first:
|
69
62
|
|
70
63
|
```typescript
|
71
|
-
await destroy(pool)
|
64
|
+
await destroy(pool);
|
72
65
|
|
73
66
|
// [info] closing worker
|
74
67
|
// [info] closing worker
|
package/dist/wardens.mjs
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
var p = Object.defineProperty;
|
2
|
+
var x = (n, e, t) => e in n ? p(n, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : n[e] = t;
|
3
|
+
var a = (n, e, t) => (x(n, typeof e != "symbol" ? e + "" : e, t), t), f = (n, e, t) => {
|
4
|
+
if (!e.has(n))
|
5
|
+
throw TypeError("Cannot " + t);
|
6
|
+
};
|
7
|
+
var i = (n, e, t) => (f(n, e, "read from private field"), t ? t.call(n) : e.get(n)), y = (n, e, t) => {
|
8
|
+
if (e.has(n))
|
9
|
+
throw TypeError("Cannot add the same private member more than once");
|
10
|
+
e instanceof WeakSet ? e.add(n) : e.set(n, t);
|
11
|
+
}, l = (n, e, t, o) => (f(n, e, "write to private field"), o ? o.call(n, t) : e.set(n, t), t);
|
12
|
+
function g(n) {
|
13
|
+
const e = /* @__PURE__ */ new WeakMap();
|
14
|
+
return new Proxy(n, {
|
15
|
+
get(t, o) {
|
16
|
+
const r = Reflect.get(t, o, t);
|
17
|
+
if (typeof r == "function") {
|
18
|
+
if (e.has(r) === !1) {
|
19
|
+
const c = r.bind(t);
|
20
|
+
Object.defineProperties(
|
21
|
+
c,
|
22
|
+
Object.getOwnPropertyDescriptors(r)
|
23
|
+
), e.set(r, c);
|
24
|
+
}
|
25
|
+
return e.get(r);
|
26
|
+
}
|
27
|
+
return r;
|
28
|
+
},
|
29
|
+
set(t, o, r) {
|
30
|
+
return Reflect.set(t, o, r, t);
|
31
|
+
}
|
32
|
+
});
|
33
|
+
}
|
34
|
+
const u = /* @__PURE__ */ new WeakMap();
|
35
|
+
function m(n) {
|
36
|
+
return Proxy.revocable(n, {});
|
37
|
+
}
|
38
|
+
var s;
|
39
|
+
class v {
|
40
|
+
constructor(e) {
|
41
|
+
y(this, s, void 0);
|
42
|
+
/** Provision an owned resource and make sure it doesn't outlive us. */
|
43
|
+
a(this, "create", async (e, ...t) => {
|
44
|
+
const o = await P(e, ...t);
|
45
|
+
return i(this, s).add(o), o;
|
46
|
+
});
|
47
|
+
/**
|
48
|
+
* Tear down a resource. Happens automatically when resource owners are
|
49
|
+
* deallocated.
|
50
|
+
*/
|
51
|
+
a(this, "destroy", async (e) => {
|
52
|
+
if (!i(this, s).has(e))
|
53
|
+
throw new Error("You do not own this resource.");
|
54
|
+
try {
|
55
|
+
await w(e);
|
56
|
+
} finally {
|
57
|
+
i(this, s).delete(e);
|
58
|
+
}
|
59
|
+
});
|
60
|
+
l(this, s, e);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
s = new WeakMap();
|
64
|
+
async function P(n, ...e) {
|
65
|
+
const t = /* @__PURE__ */ new Set(), o = new v(t), r = await n(o, ...e), c = r.value, { proxy: d, revoke: h } = m(c);
|
66
|
+
return u.set(d, {
|
67
|
+
resource: r,
|
68
|
+
children: t,
|
69
|
+
revoke: h
|
70
|
+
}), d;
|
71
|
+
}
|
72
|
+
async function w(n) {
|
73
|
+
const e = u.get(n);
|
74
|
+
if (e) {
|
75
|
+
u.delete(n), e.revoke();
|
76
|
+
const t = Array.from(e.children).map(w), o = await Promise.allSettled(t);
|
77
|
+
e.resource.destroy && await e.resource.destroy(), o.forEach((r) => {
|
78
|
+
if (r.status === "rejected")
|
79
|
+
throw r.reason;
|
80
|
+
});
|
81
|
+
}
|
82
|
+
}
|
83
|
+
export {
|
84
|
+
g as bindContext,
|
85
|
+
P as create,
|
86
|
+
w as destroy
|
87
|
+
};
|
package/dist/wardens.umd.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
var
|
1
|
+
var P=Object.defineProperty;var g=(e,t,n)=>t in e?P(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var l=(e,t,n)=>(g(e,typeof t!="symbol"?t+"":t,n),n),p=(e,t,n)=>{if(!t.has(e))throw TypeError("Cannot "+n)};var f=(e,t,n)=>(p(e,t,"read from private field"),n?n.call(e):t.get(e)),b=(e,t,n)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,n)},m=(e,t,n,d)=>(p(e,t,"write to private field"),d?d.call(e,n):t.set(e,n),n);(function(e,t){typeof exports=="object"&&typeof module<"u"?t(exports):typeof define=="function"&&define.amd?define(["exports"],t):(e=typeof globalThis<"u"?globalThis:e||self,t(e.wardens={}))})(this,function(e){var a;"use strict";function t(c){const o=new WeakMap;return new Proxy(c,{get(s,i){const r=Reflect.get(s,i,s);if(typeof r=="function"){if(o.has(r)===!1){const u=r.bind(s);Object.defineProperties(u,Object.getOwnPropertyDescriptors(r)),o.set(r,u)}return o.get(r)}return r},set(s,i,r){return Reflect.set(s,i,r,s)}})}const n=new WeakMap;function d(c){return Proxy.revocable(c,{})}class x{constructor(o){b(this,a,void 0);l(this,"create",async(o,...s)=>{const i=await h(o,...s);return f(this,a).add(i),i});l(this,"destroy",async o=>{if(!f(this,a).has(o))throw new Error("You do not own this resource.");try{await y(o)}finally{f(this,a).delete(o)}});m(this,a,o)}}a=new WeakMap;async function h(c,...o){const s=new Set,i=new x(s),r=await c(i,...o),u=r.value,{proxy:w,revoke:v}=d(u);return n.set(w,{resource:r,children:s,revoke:v}),w}async function y(c){const o=n.get(c);if(o){n.delete(c),o.revoke();const s=Array.from(o.children).map(y),i=await Promise.allSettled(s);o.resource.destroy&&await o.resource.destroy(),i.forEach(r=>{if(r.status==="rejected")throw r.reason})}}e.bindContext=t,e.create=h,e.destroy=y,Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})});
|
package/package.json
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
{
|
2
2
|
"name": "wardens",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.4.1",
|
4
4
|
"description": "A framework for resource management",
|
5
5
|
"main": "./dist/wardens.umd.js",
|
6
6
|
"module": "./dist/wardens.es.js",
|
7
|
+
"types": "./src/index.ts",
|
7
8
|
"repository": "git@github.com:PsychoLlama/wardens.git",
|
8
9
|
"author": "Jesse Gibson <JesseTheGibson@gmail.com>",
|
9
10
|
"license": "MIT",
|
@@ -31,7 +32,7 @@
|
|
31
32
|
"exports": {
|
32
33
|
".": {
|
33
34
|
"require": "./dist/wardens.umd.js",
|
34
|
-
"import": "./dist/wardens.
|
35
|
+
"import": "./dist/wardens.mjs"
|
35
36
|
}
|
36
37
|
},
|
37
38
|
"husky": {
|
@@ -82,17 +83,16 @@
|
|
82
83
|
"preset": "ts-jest"
|
83
84
|
},
|
84
85
|
"devDependencies": {
|
85
|
-
"@types/jest": "
|
86
|
-
"@typescript-eslint/eslint-plugin": "5.
|
87
|
-
"@typescript-eslint/parser": "5.
|
88
|
-
"eslint": "8.
|
89
|
-
"husky": "8.0.
|
90
|
-
"jest": "
|
91
|
-
"lint-staged": "13.
|
92
|
-
"prettier": "2.
|
93
|
-
"ts-jest": "
|
94
|
-
"typescript": "4.
|
95
|
-
"vite": "
|
96
|
-
"vite-dts": "1.0.4"
|
86
|
+
"@types/jest": "29.2.5",
|
87
|
+
"@typescript-eslint/eslint-plugin": "5.48.1",
|
88
|
+
"@typescript-eslint/parser": "5.48.1",
|
89
|
+
"eslint": "8.31.0",
|
90
|
+
"husky": "8.0.3",
|
91
|
+
"jest": "29.3.1",
|
92
|
+
"lint-staged": "13.1.0",
|
93
|
+
"prettier": "2.8.3",
|
94
|
+
"ts-jest": "29.0.5",
|
95
|
+
"typescript": "4.9.4",
|
96
|
+
"vite": "4.0.4"
|
97
97
|
}
|
98
98
|
}
|
@@ -1,32 +1,28 @@
|
|
1
|
-
import
|
1
|
+
import type ResourceContext from '../resource-context';
|
2
2
|
import { create, destroy } from '../allocation';
|
3
3
|
|
4
4
|
describe('allocation', () => {
|
5
5
|
describe('create', () => {
|
6
6
|
it('allocates the resource', async () => {
|
7
7
|
const config = { test: 'init-args' };
|
8
|
-
const
|
9
|
-
config
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
await expect(create(Test, config)).resolves.toEqual({ test: true });
|
18
|
-
expect(spy).toHaveBeenCalledWith(config);
|
8
|
+
const Test = jest.fn(
|
9
|
+
async (_resource: ResourceContext, config: { test: string }) => ({
|
10
|
+
value: config,
|
11
|
+
}),
|
12
|
+
);
|
13
|
+
|
14
|
+
await expect(create(Test, config)).resolves.toEqual(config);
|
15
|
+
expect(Test).toHaveBeenCalledWith(expect.anything(), config);
|
19
16
|
});
|
20
17
|
});
|
21
18
|
|
22
19
|
describe('destroy', () => {
|
23
20
|
it('deallocates the resource', async () => {
|
24
21
|
const spy = jest.fn<void, []>();
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
}
|
22
|
+
const Test = async () => ({
|
23
|
+
value: {},
|
24
|
+
destroy: spy,
|
25
|
+
});
|
30
26
|
|
31
27
|
const test = await create(Test);
|
32
28
|
|
@@ -36,9 +32,7 @@ describe('allocation', () => {
|
|
36
32
|
});
|
37
33
|
|
38
34
|
it('survives if the resource is already deallocated', async () => {
|
39
|
-
|
40
|
-
exports = () => [];
|
41
|
-
}
|
35
|
+
const Test = async () => ({ value: [] });
|
42
36
|
|
43
37
|
const test = await create(Test);
|
44
38
|
await destroy(test);
|
@@ -48,16 +42,10 @@ describe('allocation', () => {
|
|
48
42
|
|
49
43
|
it('automatically unmounts all children', async () => {
|
50
44
|
const spy = jest.fn();
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
class Parent extends Resource<string[]> {
|
57
|
-
exports = () => [];
|
58
|
-
async create() {
|
59
|
-
await this.allocate(Child);
|
60
|
-
}
|
45
|
+
const Child = async () => ({ value: [], destroy: spy });
|
46
|
+
async function Parent(resource: ResourceContext) {
|
47
|
+
await resource.create(Child);
|
48
|
+
return { value: [] };
|
61
49
|
}
|
62
50
|
|
63
51
|
const parent = await create(Parent);
|
@@ -68,19 +56,17 @@ describe('allocation', () => {
|
|
68
56
|
|
69
57
|
it('throws an error if any of the children fail to close', async () => {
|
70
58
|
const error = new Error('Testing resource destruction errors');
|
71
|
-
|
72
|
-
|
73
|
-
|
59
|
+
const Child = async () => ({
|
60
|
+
value: [],
|
61
|
+
destroy() {
|
74
62
|
throw error;
|
75
|
-
}
|
76
|
-
}
|
63
|
+
},
|
64
|
+
});
|
77
65
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
}
|
83
|
-
}
|
66
|
+
const Parent = async (resource: ResourceContext) => {
|
67
|
+
await resource.create(Child);
|
68
|
+
return { value: [] };
|
69
|
+
};
|
84
70
|
|
85
71
|
const parent = await create(Parent);
|
86
72
|
await expect(destroy(parent)).rejects.toThrow(error);
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import ResourceContext from '../resource-context';
|
2
|
+
import { create } from '../allocation';
|
3
|
+
|
4
|
+
describe('ResourceContext', () => {
|
5
|
+
async function Test() {
|
6
|
+
return { value: {} };
|
7
|
+
}
|
8
|
+
|
9
|
+
it('can spawn children of its own', async () => {
|
10
|
+
const Child = async () => ({
|
11
|
+
value: { child: true },
|
12
|
+
});
|
13
|
+
|
14
|
+
const Parent = async (resource: ResourceContext) => {
|
15
|
+
return { value: await resource.create(Child) };
|
16
|
+
};
|
17
|
+
|
18
|
+
await expect(create(Parent)).resolves.toEqual({ child: true });
|
19
|
+
});
|
20
|
+
|
21
|
+
it('can deallocate child resources on demand', async () => {
|
22
|
+
const spy = jest.fn();
|
23
|
+
|
24
|
+
const Child = async () => ({
|
25
|
+
value: { child: true },
|
26
|
+
destroy: spy,
|
27
|
+
});
|
28
|
+
|
29
|
+
const Parent = async (resource: ResourceContext) => {
|
30
|
+
const child = await resource.create(Child);
|
31
|
+
await resource.destroy(child);
|
32
|
+
|
33
|
+
return {
|
34
|
+
value: { parent: true },
|
35
|
+
};
|
36
|
+
};
|
37
|
+
|
38
|
+
await expect(create(Parent)).resolves.toEqual({ parent: true });
|
39
|
+
expect(spy).toHaveBeenCalled();
|
40
|
+
});
|
41
|
+
|
42
|
+
it('fails to destroy resources owned by someone else', async () => {
|
43
|
+
const test = await create(Test);
|
44
|
+
|
45
|
+
const Sneaky = async (resource: ResourceContext) => {
|
46
|
+
await resource.destroy(test);
|
47
|
+
return { value: {} };
|
48
|
+
};
|
49
|
+
|
50
|
+
await expect(create(Sneaky)).rejects.toThrow(/do not own/i);
|
51
|
+
});
|
52
|
+
|
53
|
+
it('binds create/destroy handlers to the class instance', async () => {
|
54
|
+
async function Allocator({ create, destroy }: ResourceContext) {
|
55
|
+
const test = await create(Test);
|
56
|
+
await destroy(test);
|
57
|
+
|
58
|
+
return { value: [] };
|
59
|
+
}
|
60
|
+
|
61
|
+
await expect(create(Allocator)).resolves.not.toThrow();
|
62
|
+
});
|
63
|
+
});
|
package/src/allocation.ts
CHANGED
@@ -1,27 +1,28 @@
|
|
1
|
-
import
|
2
|
-
import { resources, ownership } from './state';
|
1
|
+
import { resources } from './state';
|
3
2
|
import wrap from './proxy';
|
4
|
-
import
|
3
|
+
import ResourceContext from './resource-context';
|
4
|
+
import { ResourceFactory, ParametrizedResourceFactory } from './types';
|
5
5
|
|
6
6
|
/** Provision a resource and return its external API. */
|
7
7
|
export async function create<
|
8
8
|
Controls extends object,
|
9
9
|
Args extends Array<unknown>,
|
10
10
|
>(
|
11
|
-
|
11
|
+
provision:
|
12
|
+
| ParametrizedResourceFactory<Controls, Args>
|
13
|
+
| ResourceFactory<Controls>,
|
12
14
|
...args: Args
|
13
15
|
): Promise<Controls> {
|
14
|
-
const
|
16
|
+
const children: Set<object> = new Set();
|
17
|
+
const context = new ResourceContext(children);
|
18
|
+
const resource = await provision(context, ...args);
|
15
19
|
|
16
|
-
|
17
|
-
await resource.create(...args);
|
18
|
-
}
|
19
|
-
|
20
|
-
const controls = resource.exports();
|
20
|
+
const controls = resource.value;
|
21
21
|
const { proxy, revoke } = wrap(controls);
|
22
22
|
|
23
23
|
resources.set(proxy, {
|
24
24
|
resource,
|
25
|
+
children,
|
25
26
|
revoke,
|
26
27
|
});
|
27
28
|
|
@@ -44,15 +45,12 @@ export async function destroy(controls: object) {
|
|
44
45
|
// Free all references.
|
45
46
|
entry.revoke();
|
46
47
|
|
47
|
-
const children = ownership.get(entry.resource)!;
|
48
|
-
ownership.delete(entry.resource);
|
49
|
-
|
50
48
|
// Recursively close out the children first...
|
51
|
-
const recursiveUnmounts = children.map(
|
49
|
+
const recursiveUnmounts = Array.from(entry.children).map(destroy);
|
52
50
|
const results = await Promise.allSettled(recursiveUnmounts);
|
53
51
|
|
54
52
|
// Then close the parent.
|
55
|
-
if (
|
53
|
+
if (entry.resource.destroy) {
|
56
54
|
await entry.resource.destroy();
|
57
55
|
}
|
58
56
|
|
@@ -64,15 +62,3 @@ export async function destroy(controls: object) {
|
|
64
62
|
});
|
65
63
|
}
|
66
64
|
}
|
67
|
-
|
68
|
-
function mountable(
|
69
|
-
resource: MountableResource<object, Array<unknown>> | Resource<object>,
|
70
|
-
): resource is MountableResource<object, Array<unknown>> {
|
71
|
-
return 'create' in resource;
|
72
|
-
}
|
73
|
-
|
74
|
-
function unmountable(
|
75
|
-
resource: UnmountableResource<object> | Resource<object>,
|
76
|
-
): resource is UnmountableResource<object> {
|
77
|
-
return 'destroy' in resource;
|
78
|
-
}
|
package/src/bind-context.ts
CHANGED
@@ -15,7 +15,7 @@
|
|
15
15
|
*
|
16
16
|
*/
|
17
17
|
export default function bindContext<T extends object>(value: T) {
|
18
|
-
const methodBindings = new WeakMap<
|
18
|
+
const methodBindings = new WeakMap<object, AnyMethod>();
|
19
19
|
|
20
20
|
return new Proxy(value, {
|
21
21
|
get(target, property) {
|
@@ -48,5 +48,4 @@ export default function bindContext<T extends object>(value: T) {
|
|
48
48
|
});
|
49
49
|
}
|
50
50
|
|
51
|
-
|
52
|
-
type Fn = (...args: any) => any;
|
51
|
+
type AnyMethod = (...args: Array<unknown>) => unknown;
|
package/src/index.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
export { default as Resource } from './resource';
|
2
1
|
export { default as bindContext } from './bind-context';
|
3
2
|
export { create, destroy } from './allocation';
|
4
|
-
export type {
|
3
|
+
export type { Resource } from './types';
|
4
|
+
export type { default as ResourceContext } from './resource-context';
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import { create, destroy } from './allocation';
|
2
|
+
import { ResourceFactory, ParametrizedResourceFactory } from './types';
|
3
|
+
|
4
|
+
/**
|
5
|
+
* An instance of this class is passed to resources as they're being
|
6
|
+
* provisioned. It allows them to provision other resources while keeping
|
7
|
+
* track of ownership and lifetimes.
|
8
|
+
*/
|
9
|
+
export default class ResourceContext {
|
10
|
+
#resources: Set<object>;
|
11
|
+
|
12
|
+
constructor(ownedResources: Set<object>) {
|
13
|
+
this.#resources = ownedResources;
|
14
|
+
}
|
15
|
+
|
16
|
+
/** Provision an owned resource and make sure it doesn't outlive us. */
|
17
|
+
public create = async <Controls extends object, Args extends Array<unknown>>(
|
18
|
+
factory:
|
19
|
+
| ParametrizedResourceFactory<Controls, Args>
|
20
|
+
| ResourceFactory<Controls>,
|
21
|
+
...args: Args
|
22
|
+
): Promise<Controls> => {
|
23
|
+
const controls = await create(factory, ...args);
|
24
|
+
this.#resources.add(controls);
|
25
|
+
|
26
|
+
return controls;
|
27
|
+
};
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Tear down a resource. Happens automatically when resource owners are
|
31
|
+
* deallocated.
|
32
|
+
*/
|
33
|
+
public destroy = async (resource: object) => {
|
34
|
+
if (!this.#resources.has(resource)) {
|
35
|
+
throw new Error('You do not own this resource.');
|
36
|
+
}
|
37
|
+
|
38
|
+
try {
|
39
|
+
await destroy(resource);
|
40
|
+
} finally {
|
41
|
+
this.#resources.delete(resource);
|
42
|
+
}
|
43
|
+
};
|
44
|
+
}
|
package/src/state.ts
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
-
import type Resource from './
|
1
|
+
import type { Resource } from './types';
|
2
2
|
|
3
3
|
interface RevokableResource {
|
4
4
|
resource: Resource<object>;
|
5
5
|
|
6
|
+
/** All the resources this resource personally allocated. */
|
7
|
+
children: Set<object>;
|
8
|
+
|
6
9
|
/**
|
7
10
|
* Destroys outer references to the API and frees the object for garbage
|
8
11
|
* collection.
|
@@ -12,6 +15,3 @@ interface RevokableResource {
|
|
12
15
|
|
13
16
|
/** Maps an external API back to the resource that created it. */
|
14
17
|
export const resources = new WeakMap<object, RevokableResource>();
|
15
|
-
|
16
|
-
/** Maps a resource to all the other resources it provisioned. */
|
17
|
-
export const ownership = new WeakMap<Resource<object>, Array<object>>();
|
package/src/types.ts
CHANGED
@@ -1,19 +1,26 @@
|
|
1
|
-
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
|
+
import type ResourceContext from './resource-context';
|
2
3
|
|
3
|
-
/**
|
4
|
-
|
5
|
-
|
4
|
+
/**
|
5
|
+
* Represents an arbitrary stateful resource that is asynchronously provisioned
|
6
|
+
* and destroyed. Resources can own other resources, and destroying a parent
|
7
|
+
* first tears down the children.
|
8
|
+
*/
|
9
|
+
export interface ResourceFactory<Controls extends object> {
|
10
|
+
(resource: ResourceContext): Promise<Resource<Controls>>;
|
11
|
+
}
|
6
12
|
|
7
|
-
export interface
|
13
|
+
export interface ParametrizedResourceFactory<
|
8
14
|
Controls extends object,
|
9
|
-
|
10
|
-
>
|
11
|
-
|
12
|
-
create(...args: InitArgs): Promise<void>;
|
15
|
+
Args extends Array<unknown>,
|
16
|
+
> {
|
17
|
+
(resource: ResourceContext, ...args: Args): Promise<Resource<Controls>>;
|
13
18
|
}
|
14
19
|
|
15
|
-
export interface
|
16
|
-
|
20
|
+
export interface Resource<Value extends object> {
|
21
|
+
/** The resource value returned by `create(...)`. */
|
22
|
+
value: Value;
|
23
|
+
|
17
24
|
/** A hook that gets called when the resource is destroyed. */
|
18
|
-
destroy(): Promise<
|
25
|
+
destroy?(): Promise<unknown> | unknown;
|
19
26
|
}
|
package/dist/wardens.es.d.ts
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
export * from "../src/index"
|
package/dist/wardens.es.js
DELETED
@@ -1,99 +0,0 @@
|
|
1
|
-
var __accessCheck = (obj, member, msg) => {
|
2
|
-
if (!member.has(obj))
|
3
|
-
throw TypeError("Cannot " + msg);
|
4
|
-
};
|
5
|
-
var __privateGet = (obj, member, getter) => {
|
6
|
-
__accessCheck(obj, member, "read from private field");
|
7
|
-
return getter ? getter.call(obj) : member.get(obj);
|
8
|
-
};
|
9
|
-
var __privateAdd = (obj, member, value) => {
|
10
|
-
if (member.has(obj))
|
11
|
-
throw TypeError("Cannot add the same private member more than once");
|
12
|
-
member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
13
|
-
};
|
14
|
-
var _resources, _children;
|
15
|
-
const resources = /* @__PURE__ */ new WeakMap();
|
16
|
-
const ownership = /* @__PURE__ */ new WeakMap();
|
17
|
-
function wrapWithProxy(value) {
|
18
|
-
return Proxy.revocable(value, {});
|
19
|
-
}
|
20
|
-
async function create(Entity, ...args) {
|
21
|
-
const resource = new Entity();
|
22
|
-
if (mountable(resource)) {
|
23
|
-
await resource.create(...args);
|
24
|
-
}
|
25
|
-
const controls = resource.exports();
|
26
|
-
const { proxy, revoke } = wrapWithProxy(controls);
|
27
|
-
resources.set(proxy, {
|
28
|
-
resource,
|
29
|
-
revoke
|
30
|
-
});
|
31
|
-
return proxy;
|
32
|
-
}
|
33
|
-
async function destroy(controls) {
|
34
|
-
const entry = resources.get(controls);
|
35
|
-
if (entry) {
|
36
|
-
resources.delete(controls);
|
37
|
-
entry.revoke();
|
38
|
-
const children = ownership.get(entry.resource);
|
39
|
-
ownership.delete(entry.resource);
|
40
|
-
const recursiveUnmounts = children.map((controls2) => destroy(controls2));
|
41
|
-
const results = await Promise.allSettled(recursiveUnmounts);
|
42
|
-
if (unmountable(entry.resource)) {
|
43
|
-
await entry.resource.destroy();
|
44
|
-
}
|
45
|
-
results.forEach((result) => {
|
46
|
-
if (result.status === "rejected") {
|
47
|
-
throw result.reason;
|
48
|
-
}
|
49
|
-
});
|
50
|
-
}
|
51
|
-
}
|
52
|
-
function mountable(resource) {
|
53
|
-
return "create" in resource;
|
54
|
-
}
|
55
|
-
function unmountable(resource) {
|
56
|
-
return "destroy" in resource;
|
57
|
-
}
|
58
|
-
class Resource {
|
59
|
-
constructor() {
|
60
|
-
__privateAdd(this, _resources, /* @__PURE__ */ new WeakSet());
|
61
|
-
__privateAdd(this, _children, []);
|
62
|
-
ownership.set(this, __privateGet(this, _children));
|
63
|
-
}
|
64
|
-
async allocate(Child, ...args) {
|
65
|
-
const controls = await create(Child, ...args);
|
66
|
-
__privateGet(this, _resources).add(controls);
|
67
|
-
__privateGet(this, _children).push(controls);
|
68
|
-
return controls;
|
69
|
-
}
|
70
|
-
async deallocate(resource) {
|
71
|
-
if (!__privateGet(this, _resources).has(resource)) {
|
72
|
-
throw new Error("You do not own this resource.");
|
73
|
-
}
|
74
|
-
return destroy(resource);
|
75
|
-
}
|
76
|
-
}
|
77
|
-
_resources = new WeakMap();
|
78
|
-
_children = new WeakMap();
|
79
|
-
function bindContext(value) {
|
80
|
-
const methodBindings = /* @__PURE__ */ new WeakMap();
|
81
|
-
return new Proxy(value, {
|
82
|
-
get(target, property) {
|
83
|
-
const value2 = Reflect.get(target, property, target);
|
84
|
-
if (typeof value2 === "function") {
|
85
|
-
if (methodBindings.has(value2) === false) {
|
86
|
-
const methodBinding = value2.bind(target);
|
87
|
-
Object.defineProperties(methodBinding, Object.getOwnPropertyDescriptors(value2));
|
88
|
-
methodBindings.set(value2, methodBinding);
|
89
|
-
}
|
90
|
-
return methodBindings.get(value2);
|
91
|
-
}
|
92
|
-
return value2;
|
93
|
-
},
|
94
|
-
set(target, property, newValue) {
|
95
|
-
return Reflect.set(target, property, newValue, target);
|
96
|
-
}
|
97
|
-
});
|
98
|
-
}
|
99
|
-
export { Resource, bindContext, create, destroy };
|
package/dist/wardens.umd.d.ts
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
export * from "../src/index"
|
@@ -1,61 +0,0 @@
|
|
1
|
-
import Resource from '../resource';
|
2
|
-
import { create } from '../allocation';
|
3
|
-
import { Controls } from '../types';
|
4
|
-
|
5
|
-
describe('Resource', () => {
|
6
|
-
class Test extends Resource<Record<string, never>> {
|
7
|
-
exports = () => ({});
|
8
|
-
}
|
9
|
-
|
10
|
-
it('can spawn children of its own', async () => {
|
11
|
-
class Child extends Resource<{ child: boolean }> {
|
12
|
-
exports = () => ({ child: true });
|
13
|
-
}
|
14
|
-
|
15
|
-
class Parent extends Resource<Controls<Child>> {
|
16
|
-
exports = () => this.child;
|
17
|
-
child!: Controls<Child>;
|
18
|
-
|
19
|
-
async create() {
|
20
|
-
this.child = await this.allocate(Child);
|
21
|
-
}
|
22
|
-
}
|
23
|
-
|
24
|
-
await expect(create(Parent)).resolves.toEqual({ child: true });
|
25
|
-
});
|
26
|
-
|
27
|
-
it('can deallocate child resources on demand', async () => {
|
28
|
-
const spy = jest.fn();
|
29
|
-
|
30
|
-
class Child extends Resource<{ child: boolean }> {
|
31
|
-
exports = () => ({ child: true });
|
32
|
-
destroy = spy;
|
33
|
-
}
|
34
|
-
|
35
|
-
class Parent extends Resource<{ parent: boolean }> {
|
36
|
-
exports = () => ({ parent: true });
|
37
|
-
child!: Controls<Child>;
|
38
|
-
|
39
|
-
async create() {
|
40
|
-
const child = await this.allocate(Child);
|
41
|
-
await this.deallocate(child);
|
42
|
-
}
|
43
|
-
}
|
44
|
-
|
45
|
-
await expect(create(Parent)).resolves.toEqual({ parent: true });
|
46
|
-
expect(spy).toHaveBeenCalled();
|
47
|
-
});
|
48
|
-
|
49
|
-
it('fails to destroy resources owned by someone else', async () => {
|
50
|
-
const test = await create(Test);
|
51
|
-
|
52
|
-
class Sneaky extends Resource<string[]> {
|
53
|
-
exports = () => [];
|
54
|
-
async create() {
|
55
|
-
await this.deallocate(test);
|
56
|
-
}
|
57
|
-
}
|
58
|
-
|
59
|
-
await expect(create(Sneaky)).rejects.toThrow(/do not own/i);
|
60
|
-
});
|
61
|
-
});
|
package/src/resource.ts
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
import { create, destroy } from './allocation';
|
2
|
-
import { ownership } from './state';
|
3
|
-
import { MountableResource } from './types';
|
4
|
-
|
5
|
-
/**
|
6
|
-
* Represents an arbitrary stateful resource that is asynchronously provisioned
|
7
|
-
* and destroyed. Resources can own other resources, and destroying a parent
|
8
|
-
* first tears down the children.
|
9
|
-
*/
|
10
|
-
export default abstract class Resource<Controls extends object> {
|
11
|
-
#resources = new WeakSet<object>();
|
12
|
-
#children: Array<object> = [];
|
13
|
-
|
14
|
-
constructor() {
|
15
|
-
ownership.set(this, this.#children);
|
16
|
-
}
|
17
|
-
|
18
|
-
/** Provision an owned resource and make sure it doesn't outlive us. */
|
19
|
-
protected async allocate<
|
20
|
-
ChildControls extends object,
|
21
|
-
ChildArgs extends Array<unknown>,
|
22
|
-
>(
|
23
|
-
Child: new () =>
|
24
|
-
| MountableResource<ChildControls, ChildArgs>
|
25
|
-
| Resource<ChildControls>,
|
26
|
-
...args: ChildArgs
|
27
|
-
): Promise<ChildControls> {
|
28
|
-
const controls: ChildControls = await create(Child, ...args);
|
29
|
-
this.#resources.add(controls);
|
30
|
-
this.#children.push(controls);
|
31
|
-
|
32
|
-
return controls;
|
33
|
-
}
|
34
|
-
|
35
|
-
/**
|
36
|
-
* Tear down a resource. Happens automatically when resource owners are
|
37
|
-
* deallocated.
|
38
|
-
*/
|
39
|
-
protected async deallocate(resource: object) {
|
40
|
-
if (!this.#resources.has(resource)) {
|
41
|
-
throw new Error('You do not own this resource.');
|
42
|
-
}
|
43
|
-
|
44
|
-
return destroy(resource);
|
45
|
-
}
|
46
|
-
|
47
|
-
/** Returns an external API to the parent resource. */
|
48
|
-
abstract exports(): Controls;
|
49
|
-
}
|