wardens 0.2.0 → 0.3.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 +15 -1
- package/README.md +8 -8
- package/dist/wardens.es.js +18 -14
- package/dist/wardens.umd.js +1 -1
- package/package.json +8 -8
- package/src/__tests__/allocation.test.ts +31 -29
- package/src/__tests__/resource.test.ts +12 -19
- package/src/allocation.ts +24 -6
- package/src/index.ts +1 -1
- package/src/resource.ts +11 -22
- package/src/state.ts +2 -5
- package/src/types.ts +16 -5
package/CHANGELOG.md
CHANGED
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [0.3.0] - 2022-06-04
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
|
13
|
+
- Prevent use of `allocate(...)`/`deallocate(...)` outside a resource subclass.
|
14
|
+
- Renamed `enter()` and `leave()` to `create()` and `destroy()`.
|
15
|
+
- Renamed `mount()` and `unmount()` to `create()` and `destroy()`.
|
16
|
+
|
17
|
+
### Removed
|
18
|
+
|
19
|
+
- Second type parameter to `Resource` is gone. Arguments to `enter(...)` are now inferred.
|
20
|
+
- No more default implementations for `enter(...)`/`leave(...)` on resources.
|
21
|
+
|
9
22
|
## [0.2.0] - 2022-05-24
|
10
23
|
|
11
24
|
### Fixed
|
@@ -29,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
29
42
|
- `mount`/`unmount` hooks to provision resources
|
30
43
|
- `allocate`/`deallocate` for creating hierarchies of resources
|
31
44
|
|
32
|
-
[Unreleased]: https://github.com/PsychoLlama/wardens/compare/v0.
|
45
|
+
[Unreleased]: https://github.com/PsychoLlama/wardens/compare/v0.3.0...HEAD
|
46
|
+
[0.3.0]: https://github.com/PsychoLlama/wardens/compare/v0.2.0...v0.3.0
|
33
47
|
[0.2.0]: https://github.com/PsychoLlama/wardens/compare/v0.1.0...v0.2.0
|
34
48
|
[0.1.0]: https://github.com/PsychoLlama/wardens/releases/tag/v0.1.0
|
package/README.md
CHANGED
@@ -17,12 +17,12 @@ class Worker extends Resource<Thread> {
|
|
17
17
|
thread!: Thread;
|
18
18
|
|
19
19
|
// Called when the resource is created
|
20
|
-
async
|
20
|
+
async create() {
|
21
21
|
this.thread = await spawn()
|
22
22
|
}
|
23
23
|
|
24
24
|
// Called when the resource is destroyed
|
25
|
-
async
|
25
|
+
async destroy() {
|
26
26
|
this.thread.close()
|
27
27
|
}
|
28
28
|
|
@@ -34,11 +34,11 @@ class Worker extends Resource<Thread> {
|
|
34
34
|
Now define a pool that creates and manages workers:
|
35
35
|
|
36
36
|
```typescript
|
37
|
-
class WorkerPool extends Resource<Controls
|
37
|
+
class WorkerPool extends Resource<Controls> {
|
38
38
|
threads!: Array<Thread> = [];
|
39
39
|
|
40
|
-
async
|
41
|
-
const promises = Array(poolSize
|
40
|
+
async create({ poolSize }: Config) {
|
41
|
+
const promises = Array.from({ length: poolSize }, () => {
|
42
42
|
return this.allocate(Worker)
|
43
43
|
})
|
44
44
|
|
@@ -53,10 +53,10 @@ class WorkerPool extends Resource<Controls, [Config]> {
|
|
53
53
|
}
|
54
54
|
```
|
55
55
|
|
56
|
-
Finally,
|
56
|
+
Finally, create the pool:
|
57
57
|
|
58
58
|
```typescript
|
59
|
-
const pool = await
|
59
|
+
const pool = await create(WorkerPool, {
|
60
60
|
poolSize: cpus().length,
|
61
61
|
})
|
62
62
|
|
@@ -68,7 +68,7 @@ pool.doSomethingElse()
|
|
68
68
|
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
69
|
|
70
70
|
```typescript
|
71
|
-
await
|
71
|
+
await destroy(pool)
|
72
72
|
|
73
73
|
// [info] closing worker
|
74
74
|
// [info] closing worker
|
package/dist/wardens.es.js
CHANGED
@@ -17,9 +17,11 @@ const ownership = /* @__PURE__ */ new WeakMap();
|
|
17
17
|
function wrapWithProxy(value) {
|
18
18
|
return Proxy.revocable(value, {});
|
19
19
|
}
|
20
|
-
async function
|
20
|
+
async function create(Entity, ...args) {
|
21
21
|
const resource = new Entity();
|
22
|
-
|
22
|
+
if (mountable(resource)) {
|
23
|
+
await resource.create(...args);
|
24
|
+
}
|
23
25
|
const controls = resource.exports();
|
24
26
|
const { proxy, revoke } = wrapWithProxy(controls);
|
25
27
|
resources.set(proxy, {
|
@@ -28,16 +30,18 @@ async function mount(Entity, ...args) {
|
|
28
30
|
});
|
29
31
|
return proxy;
|
30
32
|
}
|
31
|
-
async function
|
33
|
+
async function destroy(controls) {
|
32
34
|
const entry = resources.get(controls);
|
33
35
|
if (entry) {
|
34
36
|
resources.delete(controls);
|
35
37
|
entry.revoke();
|
36
38
|
const children = ownership.get(entry.resource);
|
37
39
|
ownership.delete(entry.resource);
|
38
|
-
const recursiveUnmounts = children.map((controls2) =>
|
40
|
+
const recursiveUnmounts = children.map((controls2) => destroy(controls2));
|
39
41
|
const results = await Promise.allSettled(recursiveUnmounts);
|
40
|
-
|
42
|
+
if (unmountable(entry.resource)) {
|
43
|
+
await entry.resource.destroy();
|
44
|
+
}
|
41
45
|
results.forEach((result) => {
|
42
46
|
if (result.status === "rejected") {
|
43
47
|
throw result.reason;
|
@@ -45,20 +49,20 @@ async function unmount(controls) {
|
|
45
49
|
});
|
46
50
|
}
|
47
51
|
}
|
52
|
+
function mountable(resource) {
|
53
|
+
return "create" in resource;
|
54
|
+
}
|
55
|
+
function unmountable(resource) {
|
56
|
+
return "destroy" in resource;
|
57
|
+
}
|
48
58
|
class Resource {
|
49
59
|
constructor() {
|
50
60
|
__privateAdd(this, _resources, /* @__PURE__ */ new WeakSet());
|
51
61
|
__privateAdd(this, _children, []);
|
52
62
|
ownership.set(this, __privateGet(this, _children));
|
53
63
|
}
|
54
|
-
async enter(..._args) {
|
55
|
-
return;
|
56
|
-
}
|
57
|
-
async leave() {
|
58
|
-
return;
|
59
|
-
}
|
60
64
|
async allocate(Child, ...args) {
|
61
|
-
const controls = await
|
65
|
+
const controls = await create(Child, ...args);
|
62
66
|
__privateGet(this, _resources).add(controls);
|
63
67
|
__privateGet(this, _children).push(controls);
|
64
68
|
return controls;
|
@@ -67,7 +71,7 @@ class Resource {
|
|
67
71
|
if (!__privateGet(this, _resources).has(resource)) {
|
68
72
|
throw new Error("You do not own this resource.");
|
69
73
|
}
|
70
|
-
return
|
74
|
+
return destroy(resource);
|
71
75
|
}
|
72
76
|
}
|
73
77
|
_resources = new WeakMap();
|
@@ -92,4 +96,4 @@ function bindContext(value) {
|
|
92
96
|
}
|
93
97
|
});
|
94
98
|
}
|
95
|
-
export { Resource, bindContext,
|
99
|
+
export { Resource, bindContext, create, destroy };
|
package/dist/wardens.umd.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
var
|
1
|
+
var v=(e,t,i)=>{if(!t.has(e))throw TypeError("Cannot "+i)};var f=(e,t,i)=>(v(e,t,"read from private field"),i?i.call(e):t.get(e)),h=(e,t,i)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,i)};(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,i=new WeakMap;function y(s){return Proxy.revocable(s,{})}async function w(s,...n){const o=new s;p(o)&&await o.create(...n);const c=o.exports(),{proxy:r,revoke:u}=y(c);return t.set(r,{resource:o,revoke:u}),r}async function l(s){const n=t.get(s);if(n){t.delete(s),n.revoke();const o=i.get(n.resource);i.delete(n.resource);const c=o.map(u=>l(u)),r=await Promise.allSettled(c);b(n.resource)&&await n.resource.destroy(),r.forEach(u=>{if(u.status==="rejected")throw u.reason})}}function p(s){return"create"in s}function b(s){return"destroy"in s}class m{constructor(){h(this,a,new WeakSet);h(this,d,[]);i.set(this,f(this,d))}async allocate(n,...o){const c=await w(n,...o);return f(this,a).add(c),f(this,d).push(c),c}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 g(s){const n=new WeakMap;return new Proxy(s,{get(o,c){const r=Reflect.get(o,c,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,c,r){return Reflect.set(o,c,r,o)}})}e.Resource=m,e.bindContext=g,e.create=w,e.destroy=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.3.0",
|
4
4
|
"description": "A framework for resource management",
|
5
5
|
"main": "./dist/wardens.umd.js",
|
6
6
|
"module": "./dist/wardens.es.js",
|
@@ -82,16 +82,16 @@
|
|
82
82
|
"preset": "ts-jest"
|
83
83
|
},
|
84
84
|
"devDependencies": {
|
85
|
-
"@types/jest": "
|
86
|
-
"@typescript-eslint/eslint-plugin": "5.
|
87
|
-
"@typescript-eslint/parser": "5.
|
88
|
-
"eslint": "8.
|
85
|
+
"@types/jest": "28.1.0",
|
86
|
+
"@typescript-eslint/eslint-plugin": "5.27.0",
|
87
|
+
"@typescript-eslint/parser": "5.27.0",
|
88
|
+
"eslint": "8.17.0",
|
89
89
|
"husky": "8.0.1",
|
90
90
|
"jest": "28.1.0",
|
91
|
-
"lint-staged": "
|
91
|
+
"lint-staged": "13.0.0",
|
92
92
|
"prettier": "2.6.2",
|
93
|
-
"ts-jest": "28.0.
|
94
|
-
"typescript": "4.
|
93
|
+
"ts-jest": "28.0.4",
|
94
|
+
"typescript": "4.7.3",
|
95
95
|
"vite": "2.9.9",
|
96
96
|
"vite-dts": "1.0.4"
|
97
97
|
}
|
@@ -1,36 +1,38 @@
|
|
1
1
|
import Resource from '../resource';
|
2
|
-
import {
|
2
|
+
import { create, destroy } from '../allocation';
|
3
3
|
|
4
4
|
describe('allocation', () => {
|
5
|
-
describe('
|
5
|
+
describe('create', () => {
|
6
6
|
it('allocates the resource', async () => {
|
7
7
|
const config = { test: 'init-args' };
|
8
|
-
const
|
8
|
+
const spy = jest.fn((config: { test: string }) => {
|
9
|
+
config;
|
10
|
+
});
|
9
11
|
|
10
|
-
class Test extends Resource<{ test: boolean }
|
12
|
+
class Test extends Resource<{ test: boolean }> {
|
11
13
|
exports = () => ({ test: true });
|
12
|
-
|
14
|
+
create = spy;
|
13
15
|
}
|
14
16
|
|
15
|
-
await expect(
|
16
|
-
expect(
|
17
|
+
await expect(create(Test, config)).resolves.toEqual({ test: true });
|
18
|
+
expect(spy).toHaveBeenCalledWith(config);
|
17
19
|
});
|
18
20
|
});
|
19
21
|
|
20
|
-
describe('
|
22
|
+
describe('destroy', () => {
|
21
23
|
it('deallocates the resource', async () => {
|
22
|
-
const
|
24
|
+
const spy = jest.fn<void, []>();
|
23
25
|
|
24
26
|
class Test extends Resource<Array<string>> {
|
25
27
|
exports = () => [];
|
26
|
-
|
28
|
+
destroy = spy;
|
27
29
|
}
|
28
30
|
|
29
|
-
const test = await
|
31
|
+
const test = await create(Test);
|
30
32
|
|
31
|
-
expect(
|
32
|
-
await expect(
|
33
|
-
expect(
|
33
|
+
expect(spy).not.toHaveBeenCalled();
|
34
|
+
await expect(destroy(test)).resolves.not.toThrow();
|
35
|
+
expect(spy).toHaveBeenCalled();
|
34
36
|
});
|
35
37
|
|
36
38
|
it('survives if the resource is already deallocated', async () => {
|
@@ -38,50 +40,50 @@ describe('allocation', () => {
|
|
38
40
|
exports = () => [];
|
39
41
|
}
|
40
42
|
|
41
|
-
const test = await
|
42
|
-
await
|
43
|
+
const test = await create(Test);
|
44
|
+
await destroy(test);
|
43
45
|
|
44
|
-
await expect(
|
46
|
+
await expect(destroy(test)).resolves.not.toThrow();
|
45
47
|
});
|
46
48
|
|
47
49
|
it('automatically unmounts all children', async () => {
|
48
|
-
const
|
50
|
+
const spy = jest.fn();
|
49
51
|
class Child extends Resource<number[]> {
|
50
52
|
exports = () => [];
|
51
|
-
|
53
|
+
destroy = spy;
|
52
54
|
}
|
53
55
|
|
54
56
|
class Parent extends Resource<string[]> {
|
55
57
|
exports = () => [];
|
56
|
-
async
|
58
|
+
async create() {
|
57
59
|
await this.allocate(Child);
|
58
60
|
}
|
59
61
|
}
|
60
62
|
|
61
|
-
const parent = await
|
62
|
-
await
|
63
|
+
const parent = await create(Parent);
|
64
|
+
await destroy(parent);
|
63
65
|
|
64
|
-
expect(
|
66
|
+
expect(spy).toHaveBeenCalled();
|
65
67
|
});
|
66
68
|
|
67
|
-
it('throws an error if any of the children fail to
|
68
|
-
const error = new Error('Testing
|
69
|
+
it('throws an error if any of the children fail to close', async () => {
|
70
|
+
const error = new Error('Testing resource destruction errors');
|
69
71
|
class Child extends Resource<number[]> {
|
70
72
|
exports = () => [];
|
71
|
-
async
|
73
|
+
async destroy() {
|
72
74
|
throw error;
|
73
75
|
}
|
74
76
|
}
|
75
77
|
|
76
78
|
class Parent extends Resource<string[]> {
|
77
79
|
exports = () => [];
|
78
|
-
async
|
80
|
+
async create() {
|
79
81
|
await this.allocate(Child);
|
80
82
|
}
|
81
83
|
}
|
82
84
|
|
83
|
-
const parent = await
|
84
|
-
await expect(
|
85
|
+
const parent = await create(Parent);
|
86
|
+
await expect(destroy(parent)).rejects.toThrow(error);
|
85
87
|
});
|
86
88
|
});
|
87
89
|
});
|
@@ -1,19 +1,12 @@
|
|
1
1
|
import Resource from '../resource';
|
2
|
-
import {
|
3
|
-
import {
|
2
|
+
import { create } from '../allocation';
|
3
|
+
import { Controls } from '../types';
|
4
4
|
|
5
5
|
describe('Resource', () => {
|
6
6
|
class Test extends Resource<Record<string, never>> {
|
7
7
|
exports = () => ({});
|
8
8
|
}
|
9
9
|
|
10
|
-
it('implements default enter/leave methods', async () => {
|
11
|
-
const test = new Test();
|
12
|
-
|
13
|
-
await expect(test.enter()).resolves.not.toThrow();
|
14
|
-
await expect(test.leave()).resolves.not.toThrow();
|
15
|
-
});
|
16
|
-
|
17
10
|
it('can spawn children of its own', async () => {
|
18
11
|
class Child extends Resource<{ child: boolean }> {
|
19
12
|
exports = () => ({ child: true });
|
@@ -23,46 +16,46 @@ describe('Resource', () => {
|
|
23
16
|
exports = () => this.child;
|
24
17
|
child!: Controls<Child>;
|
25
18
|
|
26
|
-
async
|
19
|
+
async create() {
|
27
20
|
this.child = await this.allocate(Child);
|
28
21
|
}
|
29
22
|
}
|
30
23
|
|
31
|
-
await expect(
|
24
|
+
await expect(create(Parent)).resolves.toEqual({ child: true });
|
32
25
|
});
|
33
26
|
|
34
27
|
it('can deallocate child resources on demand', async () => {
|
35
|
-
const
|
28
|
+
const spy = jest.fn();
|
36
29
|
|
37
30
|
class Child extends Resource<{ child: boolean }> {
|
38
31
|
exports = () => ({ child: true });
|
39
|
-
|
32
|
+
destroy = spy;
|
40
33
|
}
|
41
34
|
|
42
35
|
class Parent extends Resource<{ parent: boolean }> {
|
43
36
|
exports = () => ({ parent: true });
|
44
37
|
child!: Controls<Child>;
|
45
38
|
|
46
|
-
async
|
39
|
+
async create() {
|
47
40
|
const child = await this.allocate(Child);
|
48
41
|
await this.deallocate(child);
|
49
42
|
}
|
50
43
|
}
|
51
44
|
|
52
|
-
await expect(
|
53
|
-
expect(
|
45
|
+
await expect(create(Parent)).resolves.toEqual({ parent: true });
|
46
|
+
expect(spy).toHaveBeenCalled();
|
54
47
|
});
|
55
48
|
|
56
49
|
it('fails to destroy resources owned by someone else', async () => {
|
57
|
-
const test = await
|
50
|
+
const test = await create(Test);
|
58
51
|
|
59
52
|
class Sneaky extends Resource<string[]> {
|
60
53
|
exports = () => [];
|
61
|
-
async
|
54
|
+
async create() {
|
62
55
|
await this.deallocate(test);
|
63
56
|
}
|
64
57
|
}
|
65
58
|
|
66
|
-
await expect(
|
59
|
+
await expect(create(Sneaky)).rejects.toThrow(/do not own/i);
|
67
60
|
});
|
68
61
|
});
|
package/src/allocation.ts
CHANGED
@@ -1,17 +1,21 @@
|
|
1
1
|
import type Resource from './resource';
|
2
2
|
import { resources, ownership } from './state';
|
3
3
|
import wrap from './proxy';
|
4
|
+
import { MountableResource, UnmountableResource } from './types';
|
4
5
|
|
5
6
|
/** Provision a resource and return its external API. */
|
6
|
-
export async function
|
7
|
+
export async function create<
|
7
8
|
Controls extends object,
|
8
9
|
Args extends Array<unknown>,
|
9
10
|
>(
|
10
|
-
Entity: new () =>
|
11
|
+
Entity: new () => MountableResource<Controls, Args> | Resource<Controls>,
|
11
12
|
...args: Args
|
12
13
|
): Promise<Controls> {
|
13
14
|
const resource = new Entity();
|
14
|
-
|
15
|
+
|
16
|
+
if (mountable(resource)) {
|
17
|
+
await resource.create(...args);
|
18
|
+
}
|
15
19
|
|
16
20
|
const controls = resource.exports();
|
17
21
|
const { proxy, revoke } = wrap(controls);
|
@@ -30,7 +34,7 @@ export async function mount<
|
|
30
34
|
*
|
31
35
|
* @todo Add type marker to catch cases where the wrong object is unmounted.
|
32
36
|
*/
|
33
|
-
export async function
|
37
|
+
export async function destroy(controls: object) {
|
34
38
|
const entry = resources.get(controls);
|
35
39
|
|
36
40
|
if (entry) {
|
@@ -44,11 +48,13 @@ export async function unmount(controls: object) {
|
|
44
48
|
ownership.delete(entry.resource);
|
45
49
|
|
46
50
|
// Recursively close out the children first...
|
47
|
-
const recursiveUnmounts = children.map((controls) =>
|
51
|
+
const recursiveUnmounts = children.map((controls) => destroy(controls));
|
48
52
|
const results = await Promise.allSettled(recursiveUnmounts);
|
49
53
|
|
50
54
|
// Then close the parent.
|
51
|
-
|
55
|
+
if (unmountable(entry.resource)) {
|
56
|
+
await entry.resource.destroy();
|
57
|
+
}
|
52
58
|
|
53
59
|
// Fail loudly if any of the children couldn't be deallocated.
|
54
60
|
results.forEach((result) => {
|
@@ -58,3 +64,15 @@ export async function unmount(controls: object) {
|
|
58
64
|
});
|
59
65
|
}
|
60
66
|
}
|
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/index.ts
CHANGED
package/src/resource.ts
CHANGED
@@ -1,15 +1,13 @@
|
|
1
|
-
import {
|
1
|
+
import { create, destroy } from './allocation';
|
2
2
|
import { ownership } from './state';
|
3
|
+
import { MountableResource } from './types';
|
3
4
|
|
4
5
|
/**
|
5
6
|
* Represents an arbitrary stateful resource that is asynchronously provisioned
|
6
7
|
* and destroyed. Resources can own other resources, and destroying a parent
|
7
8
|
* first tears down the children.
|
8
9
|
*/
|
9
|
-
export default abstract class Resource<
|
10
|
-
Controls extends object,
|
11
|
-
Args extends Array<unknown> = [],
|
12
|
-
> {
|
10
|
+
export default abstract class Resource<Controls extends object> {
|
13
11
|
#resources = new WeakSet<object>();
|
14
12
|
#children: Array<object> = [];
|
15
13
|
|
@@ -17,26 +15,17 @@ export default abstract class Resource<
|
|
17
15
|
ownership.set(this, this.#children);
|
18
16
|
}
|
19
17
|
|
20
|
-
/** A hook that gets called when the resource is created. */
|
21
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
22
|
-
async enter(..._args: Args) {
|
23
|
-
return;
|
24
|
-
}
|
25
|
-
|
26
|
-
/** A hook that gets called when the resource is destroyed. */
|
27
|
-
async leave() {
|
28
|
-
return;
|
29
|
-
}
|
30
|
-
|
31
18
|
/** Provision an owned resource and make sure it doesn't outlive us. */
|
32
|
-
async allocate<
|
19
|
+
protected async allocate<
|
33
20
|
ChildControls extends object,
|
34
|
-
ChildArgs extends Array<unknown
|
21
|
+
ChildArgs extends Array<unknown>,
|
35
22
|
>(
|
36
|
-
Child: new () =>
|
23
|
+
Child: new () =>
|
24
|
+
| MountableResource<ChildControls, ChildArgs>
|
25
|
+
| Resource<ChildControls>,
|
37
26
|
...args: ChildArgs
|
38
27
|
): Promise<ChildControls> {
|
39
|
-
const controls = await
|
28
|
+
const controls: ChildControls = await create(Child, ...args);
|
40
29
|
this.#resources.add(controls);
|
41
30
|
this.#children.push(controls);
|
42
31
|
|
@@ -47,12 +36,12 @@ export default abstract class Resource<
|
|
47
36
|
* Tear down a resource. Happens automatically when resource owners are
|
48
37
|
* deallocated.
|
49
38
|
*/
|
50
|
-
async deallocate(resource: object) {
|
39
|
+
protected async deallocate(resource: object) {
|
51
40
|
if (!this.#resources.has(resource)) {
|
52
41
|
throw new Error('You do not own this resource.');
|
53
42
|
}
|
54
43
|
|
55
|
-
return
|
44
|
+
return destroy(resource);
|
56
45
|
}
|
57
46
|
|
58
47
|
/** Returns an external API to the parent resource. */
|
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
|
4
|
+
resource: Resource<object>;
|
5
5
|
|
6
6
|
/**
|
7
7
|
* Destroys outer references to the API and frees the object for garbage
|
@@ -14,7 +14,4 @@ interface RevokableResource {
|
|
14
14
|
export const resources = new WeakMap<object, RevokableResource>();
|
15
15
|
|
16
16
|
/** Maps a resource to all the other resources it provisioned. */
|
17
|
-
export const ownership = new WeakMap<
|
18
|
-
Resource<object, Array<unknown>>,
|
19
|
-
Array<object>
|
20
|
-
>();
|
17
|
+
export const ownership = new WeakMap<Resource<object>, Array<object>>();
|
package/src/types.ts
CHANGED
@@ -1,8 +1,19 @@
|
|
1
1
|
import type Resource from './resource';
|
2
2
|
|
3
3
|
/** The `exports` type for a resource. */
|
4
|
-
export type Controls<
|
5
|
-
ArbitraryResource extends Resource<
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
export type Controls<ArbitraryResource extends Resource<object>> =
|
5
|
+
ArbitraryResource extends Resource<infer Controls> ? Controls : never;
|
6
|
+
|
7
|
+
export interface MountableResource<
|
8
|
+
Controls extends object,
|
9
|
+
InitArgs extends Array<unknown>,
|
10
|
+
> extends Resource<Controls> {
|
11
|
+
/** A hook that gets called when the resource is created. */
|
12
|
+
create(...args: InitArgs): Promise<void>;
|
13
|
+
}
|
14
|
+
|
15
|
+
export interface UnmountableResource<Controls extends object>
|
16
|
+
extends Resource<Controls> {
|
17
|
+
/** A hook that gets called when the resource is destroyed. */
|
18
|
+
destroy(): Promise<void>;
|
19
|
+
}
|