wardens 0.6.0-rc.1 → 0.6.0-rc.3
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 +3 -1
- package/README.md +1 -1
- package/dist/wardens.cjs +1 -1
- package/dist/wardens.js +105 -96
- package/package.json +7 -6
- package/src/__tests__/{resource-controls.test.ts → resource-scope.test.ts} +17 -17
- package/src/__tests__/root-lifecycle.test.ts +26 -13
- package/src/__tests__/roots.test.ts +13 -13
- package/src/__tests__/utility-types.test-d.ts +2 -2
- package/src/global-weakrefs.ts +6 -0
- package/src/index.ts +2 -2
- package/src/resource-lifecycle.ts +3 -3
- package/src/{resource-controls.ts → resource-scope.ts} +1 -1
- package/src/root-lifecycle.ts +20 -6
- package/src/utility-types.ts +3 -3
- package/src/__tests__/root-lifecycles.test.ts +0 -236
package/CHANGELOG.md
CHANGED
@@ -12,7 +12,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
12
12
|
|
13
13
|
### Changed
|
14
14
|
|
15
|
-
- Renamed public type `ResourceContext` to `
|
15
|
+
- Renamed public type `ResourceContext` to `ResourceScope`.
|
16
|
+
- `destroy(...)` is no longer allowed to destroy child resources, only roots.
|
17
|
+
- Added tree-shaking metadata. This is useful for bundle analysis tools.
|
16
18
|
|
17
19
|
## [0.5.1] - 2023-08-12
|
18
20
|
|
package/README.md
CHANGED
@@ -30,7 +30,7 @@ Now define a pool that creates and manages workers:
|
|
30
30
|
|
31
31
|
```typescript
|
32
32
|
async function WorkerPool(
|
33
|
-
{ create }:
|
33
|
+
{ create }: ResourceScope,
|
34
34
|
config: { poolSize: number },
|
35
35
|
) {
|
36
36
|
const promises = Array(config.poolSize).fill(Worker).map(create);
|
package/dist/wardens.cjs
CHANGED
@@ -1 +1 @@
|
|
1
|
-
"use strict";var
|
1
|
+
"use strict";var R=Object.defineProperty;var B=(e,t,r)=>t in e?R(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var f=(e,t,r)=>(B(e,typeof t!="symbol"?t+"":t,r),r),j=(e,t,r)=>{if(!t.has(e))throw TypeError("Cannot "+r)};var s=(e,t,r)=>(j(e,t,"read from private field"),r?r.call(e):t.get(e)),d=(e,t,r)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,r)},h=(e,t,r,o)=>(j(e,t,"write to private field"),o?o.call(e,r):t.set(e,r),r);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function M(e){const t=new WeakMap;return new Proxy(e,{get(r,o){const n=Reflect.get(r,o,r);if(typeof n=="function"){if(t.has(n)===!1){const a=n.bind(r);Object.defineProperties(a,Object.getOwnPropertyDescriptors(n)),t.set(n,a)}return t.get(n)}return n},set(r,o,n){return Reflect.set(r,o,n,r)}})}const g=new WeakMap,k=new WeakSet,E=new WeakSet;function A(e){return Proxy.revocable(e,{})}var C,w;class x{constructor(t){d(this,C,Symbol("Context ID"));d(this,w,void 0);h(this,w,t)}static getId(t){return s(t,C)}static getDefaultValue(t){var r;return s(r=t,w).call(r)}}C=new WeakMap,w=new WeakMap;const T=e=>new x(e);var y,l,p,i;class V{constructor(t,r,o){d(this,y,new WeakSet);d(this,l,void 0);d(this,p,void 0);d(this,i,void 0);f(this,"create",async(t,...r)=>{if(s(this,p).enforced)throw new Error("Cannot create new resources after teardown.");const o=Object.create(s(this,i)),n=await P(o,t,...r);return s(this,l).add(n),n});f(this,"destroy",async t=>{if(s(this,y).has(t))throw new Error("Resource already destroyed.");if(!s(this,l).has(t))throw new Error("You do not own this resource.");s(this,l).delete(t),s(this,y).add(t),await m(t)});f(this,"setContext",(t,r)=>(s(this,i)[x.getId(t)]=r,r));f(this,"getContext",t=>{const r=x.getId(t);return r in s(this,i)?s(this,i)[r]:x.getDefaultValue(t)});h(this,i,t),h(this,l,r),h(this,p,o)}}y=new WeakMap,l=new WeakMap,p=new WeakMap,i=new WeakMap;const P=async(e,t,...r)=>{const o={enforced:!1},n=new Set,a=new V(e,n,o);let c;try{c=await t(a,...r)}catch(S){const O=Array.from(n).reverse(),v=(await Promise.allSettled(O.map(u=>a.destroy(u)))).filter(u=>u.status==="rejected");throw v.length?D(v.map(u=>u.reason),{cause:S}):S}const W=c.value,{proxy:b,revoke:I}=A(W);return k.add(b),g.set(b,{curfew:o,resource:c,children:n,revoke:I}),b},m=async e=>{if(!k.has(e))throw new Error("Cannot destroy object. It is not a resource.");const t=g.get(e);if(t){g.delete(e),t.revoke();let r={status:"fulfilled",value:void 0};if(t.resource.destroy)try{await t.resource.destroy()}catch(c){r={status:"rejected",reason:c}}t.curfew.enforced=!0;const o=Array.from(t.children).reverse().map(m),n=await Promise.allSettled(o),a=[r].concat(n).filter(c=>c.status==="rejected");if(a.length)throw D(a.map(c=>c.reason))}},D=(e,t)=>e.length===1?e[0]:new H(e,t);class H extends Error{constructor(t,r){super("Some resources could not be destroyed. See the `failures` property for details.",r),this.failures=t}}const U=async(e,...t)=>{const o=await P(Object.create(null),e,...t);return E.add(o),o},Y=async e=>{if(g.has(e)&&!E.has(e))throw new Error("Cannot destroy child resource. It is owned by another scope.");return m(e)};exports.bindContext=M;exports.create=U;exports.createContext=T;exports.destroy=Y;
|
package/dist/wardens.js
CHANGED
@@ -1,152 +1,161 @@
|
|
1
1
|
var O = Object.defineProperty;
|
2
|
-
var
|
3
|
-
var
|
4
|
-
if (!
|
5
|
-
throw TypeError("Cannot " +
|
2
|
+
var B = (e, t, r) => t in e ? O(e, t, { enumerable: !0, configurable: !0, writable: !0, value: r }) : e[t] = r;
|
3
|
+
var h = (e, t, r) => (B(e, typeof t != "symbol" ? t + "" : t, r), r), j = (e, t, r) => {
|
4
|
+
if (!t.has(e))
|
5
|
+
throw TypeError("Cannot " + r);
|
6
6
|
};
|
7
|
-
var n = (
|
8
|
-
if (
|
7
|
+
var n = (e, t, r) => (j(e, t, "read from private field"), r ? r.call(e) : t.get(e)), d = (e, t, r) => {
|
8
|
+
if (t.has(e))
|
9
9
|
throw TypeError("Cannot add the same private member more than once");
|
10
|
-
|
11
|
-
},
|
12
|
-
function
|
13
|
-
const
|
14
|
-
return new Proxy(
|
15
|
-
get(
|
16
|
-
const
|
17
|
-
if (typeof
|
18
|
-
if (
|
19
|
-
const a =
|
10
|
+
t instanceof WeakSet ? t.add(e) : t.set(e, r);
|
11
|
+
}, f = (e, t, r, o) => (j(e, t, "write to private field"), o ? o.call(e, r) : t.set(e, r), r);
|
12
|
+
function U(e) {
|
13
|
+
const t = /* @__PURE__ */ new WeakMap();
|
14
|
+
return new Proxy(e, {
|
15
|
+
get(r, o) {
|
16
|
+
const s = Reflect.get(r, o, r);
|
17
|
+
if (typeof s == "function") {
|
18
|
+
if (t.has(s) === !1) {
|
19
|
+
const a = s.bind(r);
|
20
20
|
Object.defineProperties(
|
21
21
|
a,
|
22
|
-
Object.getOwnPropertyDescriptors(
|
23
|
-
),
|
22
|
+
Object.getOwnPropertyDescriptors(s)
|
23
|
+
), t.set(s, a);
|
24
24
|
}
|
25
|
-
return
|
25
|
+
return t.get(s);
|
26
26
|
}
|
27
|
-
return
|
27
|
+
return s;
|
28
28
|
},
|
29
|
-
set(
|
30
|
-
return Reflect.set(
|
29
|
+
set(r, o, s) {
|
30
|
+
return Reflect.set(r, o, s, r);
|
31
31
|
}
|
32
32
|
});
|
33
33
|
}
|
34
|
-
const
|
35
|
-
function
|
36
|
-
return Proxy.revocable(
|
34
|
+
const g = /* @__PURE__ */ new WeakMap(), k = /* @__PURE__ */ new WeakSet(), E = /* @__PURE__ */ new WeakSet();
|
35
|
+
function A(e) {
|
36
|
+
return Proxy.revocable(e, {});
|
37
37
|
}
|
38
|
-
var
|
38
|
+
var m, w;
|
39
39
|
class x {
|
40
|
-
constructor(
|
41
|
-
|
42
|
-
|
43
|
-
|
40
|
+
constructor(t) {
|
41
|
+
d(this, m, Symbol("Context ID"));
|
42
|
+
d(this, w, void 0);
|
43
|
+
f(this, w, t);
|
44
44
|
}
|
45
|
-
static getId(
|
46
|
-
return n(
|
45
|
+
static getId(t) {
|
46
|
+
return n(t, m);
|
47
47
|
}
|
48
|
-
static getDefaultValue(
|
49
|
-
var
|
50
|
-
return n(
|
48
|
+
static getDefaultValue(t) {
|
49
|
+
var r;
|
50
|
+
return n(r = t, w).call(r);
|
51
51
|
}
|
52
52
|
}
|
53
|
-
|
54
|
-
const
|
55
|
-
var y,
|
56
|
-
class
|
57
|
-
constructor(
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
53
|
+
m = new WeakMap(), w = new WeakMap();
|
54
|
+
const Y = (e) => new x(e);
|
55
|
+
var y, l, p, i;
|
56
|
+
class M {
|
57
|
+
constructor(t, r, o) {
|
58
|
+
d(this, y, /* @__PURE__ */ new WeakSet());
|
59
|
+
d(this, l, void 0);
|
60
|
+
d(this, p, void 0);
|
61
|
+
d(this, i, void 0);
|
62
62
|
/** Provision an owned resource and make sure it doesn't outlive us. */
|
63
|
-
|
63
|
+
h(this, "create", async (t, ...r) => {
|
64
64
|
if (n(this, p).enforced)
|
65
65
|
throw new Error("Cannot create new resources after teardown.");
|
66
|
-
const
|
67
|
-
return n(this,
|
66
|
+
const o = Object.create(n(this, i)), s = await D(o, t, ...r);
|
67
|
+
return n(this, l).add(s), s;
|
68
68
|
});
|
69
69
|
/**
|
70
70
|
* Tear down a resource. Happens automatically when resource owners are
|
71
71
|
* deallocated.
|
72
72
|
*/
|
73
|
-
|
74
|
-
if (n(this, y).has(
|
73
|
+
h(this, "destroy", async (t) => {
|
74
|
+
if (n(this, y).has(t))
|
75
75
|
throw new Error("Resource already destroyed.");
|
76
|
-
if (!n(this,
|
76
|
+
if (!n(this, l).has(t))
|
77
77
|
throw new Error("You do not own this resource.");
|
78
|
-
n(this,
|
78
|
+
n(this, l).delete(t), n(this, y).add(t), await b(t);
|
79
79
|
});
|
80
80
|
/** Store a value in context. Anything down the chain can read it. */
|
81
|
-
|
81
|
+
h(this, "setContext", (t, r) => (n(this, i)[x.getId(t)] = r, r));
|
82
82
|
/** Retrieve a value from context, or a default if it is unset. */
|
83
|
-
|
84
|
-
const
|
85
|
-
return
|
83
|
+
h(this, "getContext", (t) => {
|
84
|
+
const r = x.getId(t);
|
85
|
+
return r in n(this, i) ? n(this, i)[r] : x.getDefaultValue(t);
|
86
86
|
});
|
87
|
-
|
87
|
+
f(this, i, t), f(this, l, r), f(this, p, o);
|
88
88
|
}
|
89
89
|
}
|
90
|
-
y = new WeakMap(),
|
91
|
-
const
|
92
|
-
const
|
90
|
+
y = new WeakMap(), l = new WeakMap(), p = new WeakMap(), i = new WeakMap();
|
91
|
+
const D = async (e, t, ...r) => {
|
92
|
+
const o = { enforced: !1 }, s = /* @__PURE__ */ new Set(), a = new M(e, s, o);
|
93
93
|
let c;
|
94
94
|
try {
|
95
|
-
c = await
|
96
|
-
} catch (
|
97
|
-
const
|
98
|
-
|
95
|
+
c = await t(a, ...r);
|
96
|
+
} catch (S) {
|
97
|
+
const R = Array.from(s).reverse(), v = (await Promise.allSettled(
|
98
|
+
R.map((u) => a.destroy(u))
|
99
99
|
)).filter(
|
100
|
-
(
|
100
|
+
(u) => u.status === "rejected"
|
101
101
|
);
|
102
|
-
throw v.length ?
|
103
|
-
v.map((
|
104
|
-
{ cause:
|
105
|
-
) :
|
102
|
+
throw v.length ? P(
|
103
|
+
v.map((u) => u.reason),
|
104
|
+
{ cause: S }
|
105
|
+
) : S;
|
106
106
|
}
|
107
|
-
const
|
108
|
-
return
|
109
|
-
curfew:
|
107
|
+
const W = c.value, { proxy: C, revoke: I } = A(W);
|
108
|
+
return k.add(C), g.set(C, {
|
109
|
+
curfew: o,
|
110
110
|
resource: c,
|
111
|
-
children:
|
112
|
-
revoke:
|
113
|
-
}),
|
114
|
-
},
|
115
|
-
if (!
|
111
|
+
children: s,
|
112
|
+
revoke: I
|
113
|
+
}), C;
|
114
|
+
}, b = async (e) => {
|
115
|
+
if (!k.has(e))
|
116
116
|
throw new Error("Cannot destroy object. It is not a resource.");
|
117
|
-
const
|
118
|
-
if (
|
119
|
-
|
120
|
-
let
|
117
|
+
const t = g.get(e);
|
118
|
+
if (t) {
|
119
|
+
g.delete(e), t.revoke();
|
120
|
+
let r = {
|
121
121
|
status: "fulfilled",
|
122
122
|
value: void 0
|
123
123
|
};
|
124
|
-
if (
|
124
|
+
if (t.resource.destroy)
|
125
125
|
try {
|
126
|
-
await
|
126
|
+
await t.resource.destroy();
|
127
127
|
} catch (c) {
|
128
|
-
|
128
|
+
r = { status: "rejected", reason: c };
|
129
129
|
}
|
130
|
-
|
131
|
-
const
|
130
|
+
t.curfew.enforced = !0;
|
131
|
+
const o = Array.from(t.children).reverse().map(b), s = await Promise.allSettled(o), a = [r].concat(s).filter(
|
132
132
|
(c) => c.status === "rejected"
|
133
133
|
);
|
134
134
|
if (a.length)
|
135
|
-
throw
|
135
|
+
throw P(a.map((c) => c.reason));
|
136
136
|
}
|
137
|
-
},
|
138
|
-
class
|
139
|
-
constructor(
|
137
|
+
}, P = (e, t) => e.length === 1 ? e[0] : new V(e, t);
|
138
|
+
class V extends Error {
|
139
|
+
constructor(t, r) {
|
140
140
|
super(
|
141
141
|
"Some resources could not be destroyed. See the `failures` property for details.",
|
142
|
-
|
143
|
-
), this.failures =
|
142
|
+
r
|
143
|
+
), this.failures = t;
|
144
144
|
}
|
145
145
|
}
|
146
|
-
const
|
146
|
+
const q = async (e, ...t) => {
|
147
|
+
const o = await D(/* @__PURE__ */ Object.create(null), e, ...t);
|
148
|
+
return E.add(o), o;
|
149
|
+
}, z = async (e) => {
|
150
|
+
if (g.has(e) && !E.has(e))
|
151
|
+
throw new Error(
|
152
|
+
"Cannot destroy child resource. It is owned by another scope."
|
153
|
+
);
|
154
|
+
return b(e);
|
155
|
+
};
|
147
156
|
export {
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
157
|
+
U as bindContext,
|
158
|
+
q as create,
|
159
|
+
Y as createContext,
|
160
|
+
z as destroy
|
152
161
|
};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "wardens",
|
3
|
-
"version": "0.6.0-rc.
|
3
|
+
"version": "0.6.0-rc.3",
|
4
4
|
"description": "A framework for resource management",
|
5
5
|
"type": "module",
|
6
6
|
"main": "./dist/wardens.cjs",
|
@@ -9,6 +9,7 @@
|
|
9
9
|
"repository": "git@github.com:PsychoLlama/wardens.git",
|
10
10
|
"author": "Jesse Gibson <JesseTheGibson@gmail.com>",
|
11
11
|
"license": "MIT",
|
12
|
+
"sideEffects": false,
|
12
13
|
"exports": {
|
13
14
|
".": {
|
14
15
|
"require": "./dist/wardens.cjs",
|
@@ -79,13 +80,13 @@
|
|
79
80
|
}
|
80
81
|
},
|
81
82
|
"devDependencies": {
|
82
|
-
"@typescript-eslint/eslint-plugin": "6.4.
|
83
|
-
"@typescript-eslint/parser": "6.4.
|
84
|
-
"eslint": "8.
|
83
|
+
"@typescript-eslint/eslint-plugin": "6.4.1",
|
84
|
+
"@typescript-eslint/parser": "6.4.1",
|
85
|
+
"eslint": "8.48.0",
|
85
86
|
"husky": "8.0.3",
|
86
|
-
"lint-staged": "14.0.
|
87
|
+
"lint-staged": "14.0.1",
|
87
88
|
"prettier": "2.8.8",
|
88
|
-
"typescript": "5.
|
89
|
+
"typescript": "5.2.2",
|
89
90
|
"vite": "4.4.9",
|
90
91
|
"vitest": "^0.34.0"
|
91
92
|
}
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import
|
1
|
+
import ResourceScope from '../resource-scope';
|
2
2
|
import { create } from '../';
|
3
3
|
import { createContext } from '../inherited-context';
|
4
4
|
|
5
|
-
describe('
|
5
|
+
describe('ResourceScope', () => {
|
6
6
|
async function Test() {
|
7
7
|
return { value: {} };
|
8
8
|
}
|
@@ -12,7 +12,7 @@ describe('ResourceControls', () => {
|
|
12
12
|
value: { child: true },
|
13
13
|
});
|
14
14
|
|
15
|
-
const Parent = async (resource:
|
15
|
+
const Parent = async (resource: ResourceScope) => {
|
16
16
|
return { value: await resource.create(Child) };
|
17
17
|
};
|
18
18
|
|
@@ -27,7 +27,7 @@ describe('ResourceControls', () => {
|
|
27
27
|
destroy: spy,
|
28
28
|
});
|
29
29
|
|
30
|
-
const Parent = async (resource:
|
30
|
+
const Parent = async (resource: ResourceScope) => {
|
31
31
|
const child = await resource.create(Child);
|
32
32
|
await resource.destroy(child);
|
33
33
|
|
@@ -43,7 +43,7 @@ describe('ResourceControls', () => {
|
|
43
43
|
it('fails to destroy resources owned by someone else', async () => {
|
44
44
|
const test = await create(Test);
|
45
45
|
|
46
|
-
const Sneaky = async (resource:
|
46
|
+
const Sneaky = async (resource: ResourceScope) => {
|
47
47
|
await resource.destroy(test);
|
48
48
|
return { value: {} };
|
49
49
|
};
|
@@ -52,7 +52,7 @@ describe('ResourceControls', () => {
|
|
52
52
|
});
|
53
53
|
|
54
54
|
it('binds create/destroy handlers to the class instance', async () => {
|
55
|
-
async function Allocator({ create, destroy }:
|
55
|
+
async function Allocator({ create, destroy }: ResourceScope) {
|
56
56
|
const test = await create(Test);
|
57
57
|
await destroy(test);
|
58
58
|
|
@@ -63,7 +63,7 @@ describe('ResourceControls', () => {
|
|
63
63
|
});
|
64
64
|
|
65
65
|
it('indicates if a resource was already destroyed', async () => {
|
66
|
-
async function Allocator(resource:
|
66
|
+
async function Allocator(resource: ResourceScope) {
|
67
67
|
const test = await resource.create(Test);
|
68
68
|
await resource.destroy(test);
|
69
69
|
await resource.destroy(test);
|
@@ -77,7 +77,7 @@ describe('ResourceControls', () => {
|
|
77
77
|
it('can set and retrieve context', async () => {
|
78
78
|
const SharedValue = createContext(() => 'none');
|
79
79
|
|
80
|
-
const Test = async (resource:
|
80
|
+
const Test = async (resource: ResourceScope) => {
|
81
81
|
expect(resource.getContext(SharedValue)).toBe('none');
|
82
82
|
|
83
83
|
resource.setContext(SharedValue, 'saved');
|
@@ -95,7 +95,7 @@ describe('ResourceControls', () => {
|
|
95
95
|
it('returns the new context value', async () => {
|
96
96
|
const SharedValue = createContext(() => 'none');
|
97
97
|
|
98
|
-
const Test = async (resource:
|
98
|
+
const Test = async (resource: ResourceScope) => {
|
99
99
|
return {
|
100
100
|
value: { value: resource.setContext(SharedValue, 'returned') },
|
101
101
|
};
|
@@ -107,11 +107,11 @@ describe('ResourceControls', () => {
|
|
107
107
|
it('passes context to child resources', async () => {
|
108
108
|
const SharedValue = createContext<null | string>(() => null);
|
109
109
|
|
110
|
-
const Child = async (ctx:
|
110
|
+
const Child = async (ctx: ResourceScope) => ({
|
111
111
|
value: { content: ctx.getContext(SharedValue) },
|
112
112
|
});
|
113
113
|
|
114
|
-
const Parent = async (resource:
|
114
|
+
const Parent = async (resource: ResourceScope) => {
|
115
115
|
resource.setContext(SharedValue, 'inherited');
|
116
116
|
return { value: await resource.create(Child) };
|
117
117
|
};
|
@@ -121,7 +121,7 @@ describe('ResourceControls', () => {
|
|
121
121
|
|
122
122
|
it('can override context without affecting the parent', async () => {
|
123
123
|
const Message = createContext<null | string>(() => null);
|
124
|
-
const Child = async (resource:
|
124
|
+
const Child = async (resource: ResourceScope) => {
|
125
125
|
// This should *NOT* affect the parent context.
|
126
126
|
resource.setContext(Message, 'child context');
|
127
127
|
|
@@ -130,7 +130,7 @@ describe('ResourceControls', () => {
|
|
130
130
|
};
|
131
131
|
};
|
132
132
|
|
133
|
-
const Parent = async (resource:
|
133
|
+
const Parent = async (resource: ResourceScope) => {
|
134
134
|
resource.setContext(Message, 'parent context');
|
135
135
|
const child = await resource.create(Child);
|
136
136
|
|
@@ -152,7 +152,7 @@ describe('ResourceControls', () => {
|
|
152
152
|
|
153
153
|
it('allows two siblings to have different context values', async () => {
|
154
154
|
const Message = createContext<null | string>(() => null);
|
155
|
-
const Child = async (resource:
|
155
|
+
const Child = async (resource: ResourceScope, msg: string) => {
|
156
156
|
resource.setContext(Message, msg);
|
157
157
|
|
158
158
|
return {
|
@@ -160,7 +160,7 @@ describe('ResourceControls', () => {
|
|
160
160
|
};
|
161
161
|
};
|
162
162
|
|
163
|
-
const Parent = async (resource:
|
163
|
+
const Parent = async (resource: ResourceScope) => {
|
164
164
|
resource.setContext(Message, 'parent context');
|
165
165
|
|
166
166
|
return {
|
@@ -178,11 +178,11 @@ describe('ResourceControls', () => {
|
|
178
178
|
|
179
179
|
it('provides a live view of the current value, not just a snapshot', async () => {
|
180
180
|
const Message = createContext(() => 'default');
|
181
|
-
const Child = async (resource:
|
181
|
+
const Child = async (resource: ResourceScope) => ({
|
182
182
|
value: { getMessage: () => resource.getContext(Message) },
|
183
183
|
});
|
184
184
|
|
185
|
-
const Parent = async (resource:
|
185
|
+
const Parent = async (resource: ResourceScope) => ({
|
186
186
|
value: {
|
187
187
|
setMessage: (msg: string) => resource.setContext(Message, msg),
|
188
188
|
child: await resource.create(Child),
|
@@ -1,5 +1,5 @@
|
|
1
|
-
import type
|
2
|
-
import { create, destroy } from '../
|
1
|
+
import type ResourceScope from '../resource-scope';
|
2
|
+
import { create, destroy } from '../';
|
3
3
|
import bindContext from '../bind-context';
|
4
4
|
|
5
5
|
describe('roots', () => {
|
@@ -7,7 +7,7 @@ describe('roots', () => {
|
|
7
7
|
it('allocates the resource', async () => {
|
8
8
|
const config = { test: 'init-args' };
|
9
9
|
const Test = vi.fn(
|
10
|
-
async (_resource:
|
10
|
+
async (_resource: ResourceScope, config: { test: string }) => ({
|
11
11
|
value: config,
|
12
12
|
}),
|
13
13
|
);
|
@@ -22,7 +22,7 @@ describe('roots', () => {
|
|
22
22
|
const First = async () => ({ value: [], destroy: () => spy('1st') });
|
23
23
|
const Second = async () => ({ value: [], destroy: () => spy('2nd') });
|
24
24
|
|
25
|
-
const Parent = async (resource:
|
25
|
+
const Parent = async (resource: ResourceScope) => {
|
26
26
|
await resource.create(First);
|
27
27
|
await resource.create(Second);
|
28
28
|
throw new Error('Testing resource initialization errors');
|
@@ -42,7 +42,7 @@ describe('roots', () => {
|
|
42
42
|
},
|
43
43
|
});
|
44
44
|
|
45
|
-
const Parent = async (resource:
|
45
|
+
const Parent = async (resource: ResourceScope) => {
|
46
46
|
await resource.create(Child);
|
47
47
|
await resource.create(Child);
|
48
48
|
throw parentError;
|
@@ -89,7 +89,7 @@ describe('roots', () => {
|
|
89
89
|
it('automatically unmounts all children', async () => {
|
90
90
|
const spy = vi.fn();
|
91
91
|
const Child = async () => ({ value: [], destroy: spy });
|
92
|
-
async function Parent(resource:
|
92
|
+
async function Parent(resource: ResourceScope) {
|
93
93
|
await resource.create(Child);
|
94
94
|
return { value: [] };
|
95
95
|
}
|
@@ -109,7 +109,7 @@ describe('roots', () => {
|
|
109
109
|
},
|
110
110
|
});
|
111
111
|
|
112
|
-
const Parent = async (resource:
|
112
|
+
const Parent = async (resource: ResourceScope) => {
|
113
113
|
await resource.create(Child);
|
114
114
|
return { value: [] };
|
115
115
|
};
|
@@ -123,7 +123,7 @@ describe('roots', () => {
|
|
123
123
|
const First = async () => ({ value: [], destroy: () => spy('1st') });
|
124
124
|
const Second = async () => ({ value: [], destroy: () => spy('2nd') });
|
125
125
|
|
126
|
-
const Parent = async (resource:
|
126
|
+
const Parent = async (resource: ResourceScope) => {
|
127
127
|
await resource.create(First);
|
128
128
|
await resource.create(Second);
|
129
129
|
return { value: [] };
|
@@ -144,7 +144,7 @@ describe('roots', () => {
|
|
144
144
|
},
|
145
145
|
});
|
146
146
|
|
147
|
-
const Parent = async (resource:
|
147
|
+
const Parent = async (resource: ResourceScope) => {
|
148
148
|
await resource.create(Child);
|
149
149
|
await resource.create(Child);
|
150
150
|
return { value: [] };
|
@@ -158,7 +158,7 @@ describe('roots', () => {
|
|
158
158
|
|
159
159
|
it('ensures child resources outlive their consumers', async () => {
|
160
160
|
const Child = async () => ({ value: [1] });
|
161
|
-
const Parent = async (resource:
|
161
|
+
const Parent = async (resource: ResourceScope) => {
|
162
162
|
const child = await resource.create(Child);
|
163
163
|
return {
|
164
164
|
value: [],
|
@@ -176,7 +176,7 @@ describe('roots', () => {
|
|
176
176
|
it('destroys child resources even if the parent fails to close', async () => {
|
177
177
|
const spy = vi.fn();
|
178
178
|
const Child = async () => ({ value: [], destroy: spy });
|
179
|
-
const Parent = async (resource:
|
179
|
+
const Parent = async (resource: ResourceScope) => {
|
180
180
|
await resource.create(Child);
|
181
181
|
return {
|
182
182
|
value: [],
|
@@ -202,7 +202,7 @@ describe('roots', () => {
|
|
202
202
|
},
|
203
203
|
});
|
204
204
|
|
205
|
-
const Parent = async (resource:
|
205
|
+
const Parent = async (resource: ResourceScope) => {
|
206
206
|
await resource.create(Child);
|
207
207
|
return {
|
208
208
|
value: [],
|
@@ -220,7 +220,7 @@ describe('roots', () => {
|
|
220
220
|
|
221
221
|
it('guards against creating new resources after teardown', async () => {
|
222
222
|
const Child = async () => ({ value: [] });
|
223
|
-
const Parent = async (resource:
|
223
|
+
const Parent = async (resource: ResourceScope) => ({
|
224
224
|
value: bindContext(resource),
|
225
225
|
});
|
226
226
|
|
@@ -232,5 +232,18 @@ describe('roots', () => {
|
|
232
232
|
/cannot create.*after teardown/i,
|
233
233
|
);
|
234
234
|
});
|
235
|
+
|
236
|
+
it('refuses to destroy non-root resources', async () => {
|
237
|
+
const Child = async () => ({ value: [] });
|
238
|
+
const Parent = async ({ create }: ResourceScope) => ({
|
239
|
+
value: { child: await create(Child) },
|
240
|
+
});
|
241
|
+
|
242
|
+
const { child } = await create(Parent);
|
243
|
+
|
244
|
+
await expect(destroy(child)).rejects.toThrow(
|
245
|
+
/cannot destroy child resource/i,
|
246
|
+
);
|
247
|
+
});
|
235
248
|
});
|
236
249
|
});
|
@@ -1,5 +1,5 @@
|
|
1
|
-
import type
|
2
|
-
import { create, destroy } from '../
|
1
|
+
import type ResourceScope from '../resource-scope';
|
2
|
+
import { create, destroy } from '../';
|
3
3
|
import bindContext from '../bind-context';
|
4
4
|
|
5
5
|
describe('roots', () => {
|
@@ -7,7 +7,7 @@ describe('roots', () => {
|
|
7
7
|
it('allocates the resource', async () => {
|
8
8
|
const config = { test: 'init-args' };
|
9
9
|
const Test = vi.fn(
|
10
|
-
async (_resource:
|
10
|
+
async (_resource: ResourceScope, config: { test: string }) => ({
|
11
11
|
value: config,
|
12
12
|
}),
|
13
13
|
);
|
@@ -22,7 +22,7 @@ describe('roots', () => {
|
|
22
22
|
const First = async () => ({ value: [], destroy: () => spy('1st') });
|
23
23
|
const Second = async () => ({ value: [], destroy: () => spy('2nd') });
|
24
24
|
|
25
|
-
const Parent = async (resource:
|
25
|
+
const Parent = async (resource: ResourceScope) => {
|
26
26
|
await resource.create(First);
|
27
27
|
await resource.create(Second);
|
28
28
|
throw new Error('Testing resource initialization errors');
|
@@ -42,7 +42,7 @@ describe('roots', () => {
|
|
42
42
|
},
|
43
43
|
});
|
44
44
|
|
45
|
-
const Parent = async (resource:
|
45
|
+
const Parent = async (resource: ResourceScope) => {
|
46
46
|
await resource.create(Child);
|
47
47
|
await resource.create(Child);
|
48
48
|
throw parentError;
|
@@ -89,7 +89,7 @@ describe('roots', () => {
|
|
89
89
|
it('automatically unmounts all children', async () => {
|
90
90
|
const spy = vi.fn();
|
91
91
|
const Child = async () => ({ value: [], destroy: spy });
|
92
|
-
async function Parent(resource:
|
92
|
+
async function Parent(resource: ResourceScope) {
|
93
93
|
await resource.create(Child);
|
94
94
|
return { value: [] };
|
95
95
|
}
|
@@ -109,7 +109,7 @@ describe('roots', () => {
|
|
109
109
|
},
|
110
110
|
});
|
111
111
|
|
112
|
-
const Parent = async (resource:
|
112
|
+
const Parent = async (resource: ResourceScope) => {
|
113
113
|
await resource.create(Child);
|
114
114
|
return { value: [] };
|
115
115
|
};
|
@@ -123,7 +123,7 @@ describe('roots', () => {
|
|
123
123
|
const First = async () => ({ value: [], destroy: () => spy('1st') });
|
124
124
|
const Second = async () => ({ value: [], destroy: () => spy('2nd') });
|
125
125
|
|
126
|
-
const Parent = async (resource:
|
126
|
+
const Parent = async (resource: ResourceScope) => {
|
127
127
|
await resource.create(First);
|
128
128
|
await resource.create(Second);
|
129
129
|
return { value: [] };
|
@@ -144,7 +144,7 @@ describe('roots', () => {
|
|
144
144
|
},
|
145
145
|
});
|
146
146
|
|
147
|
-
const Parent = async (resource:
|
147
|
+
const Parent = async (resource: ResourceScope) => {
|
148
148
|
await resource.create(Child);
|
149
149
|
await resource.create(Child);
|
150
150
|
return { value: [] };
|
@@ -158,7 +158,7 @@ describe('roots', () => {
|
|
158
158
|
|
159
159
|
it('ensures child resources outlive their consumers', async () => {
|
160
160
|
const Child = async () => ({ value: [1] });
|
161
|
-
const Parent = async (resource:
|
161
|
+
const Parent = async (resource: ResourceScope) => {
|
162
162
|
const child = await resource.create(Child);
|
163
163
|
return {
|
164
164
|
value: [],
|
@@ -176,7 +176,7 @@ describe('roots', () => {
|
|
176
176
|
it('destroys child resources even if the parent fails to close', async () => {
|
177
177
|
const spy = vi.fn();
|
178
178
|
const Child = async () => ({ value: [], destroy: spy });
|
179
|
-
const Parent = async (resource:
|
179
|
+
const Parent = async (resource: ResourceScope) => {
|
180
180
|
await resource.create(Child);
|
181
181
|
return {
|
182
182
|
value: [],
|
@@ -202,7 +202,7 @@ describe('roots', () => {
|
|
202
202
|
},
|
203
203
|
});
|
204
204
|
|
205
|
-
const Parent = async (resource:
|
205
|
+
const Parent = async (resource: ResourceScope) => {
|
206
206
|
await resource.create(Child);
|
207
207
|
return {
|
208
208
|
value: [],
|
@@ -220,7 +220,7 @@ describe('roots', () => {
|
|
220
220
|
|
221
221
|
it('guards against creating new resources after teardown', async () => {
|
222
222
|
const Child = async () => ({ value: [] });
|
223
|
-
const Parent = async (resource:
|
223
|
+
const Parent = async (resource: ResourceScope) => ({
|
224
224
|
value: bindContext(resource),
|
225
225
|
});
|
226
226
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { create,
|
1
|
+
import { create, ResourceScope, ResourceHandle } from '../';
|
2
2
|
|
3
3
|
describe('Utility types', () => {
|
4
4
|
describe('ResourceHandle', () => {
|
@@ -17,7 +17,7 @@ describe('Utility types', () => {
|
|
17
17
|
});
|
18
18
|
|
19
19
|
it('infers the type when the value comes from a parameter', async () => {
|
20
|
-
async function Test(_ctx:
|
20
|
+
async function Test(_ctx: ResourceScope, value: { count: number }) {
|
21
21
|
return {
|
22
22
|
value,
|
23
23
|
};
|
package/src/global-weakrefs.ts
CHANGED
@@ -10,6 +10,12 @@ export const resources = new WeakMap<object, RevokableResource>();
|
|
10
10
|
*/
|
11
11
|
export const constructed = new WeakSet<object>();
|
12
12
|
|
13
|
+
/**
|
14
|
+
* Indicates if an object is a root resource. This is used to prevent
|
15
|
+
* child resources from being destroyed by the root `destroy(...)` function.
|
16
|
+
*/
|
17
|
+
export const roots = new WeakSet<object>();
|
18
|
+
|
13
19
|
export interface RevokableResource {
|
14
20
|
resource: Resource<object>;
|
15
21
|
|
package/src/index.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
export { default as bindContext } from './bind-context';
|
2
|
-
export { create, destroy } from './root-lifecycle';
|
2
|
+
export { createRoot as create, destroyRoot as destroy } from './root-lifecycle';
|
3
3
|
export { createContext } from './inherited-context';
|
4
4
|
export type { Resource, ResourceHandle } from './utility-types';
|
5
|
-
export type { default as
|
5
|
+
export type { default as ResourceScope } from './resource-scope';
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { resources, constructed } from './global-weakrefs';
|
2
2
|
import wrap from './wrap-with-proxy';
|
3
|
-
import
|
3
|
+
import ResourceScope from './resource-scope';
|
4
4
|
import type { InheritedContext } from './inherited-context';
|
5
5
|
import { ResourceFactory, ParametrizedResourceFactory } from './utility-types';
|
6
6
|
|
@@ -17,7 +17,7 @@ export const createWithContext = async <
|
|
17
17
|
): Promise<Controls> => {
|
18
18
|
const curfew = { enforced: false };
|
19
19
|
const children: Set<object> = new Set();
|
20
|
-
const context = new
|
20
|
+
const context = new ResourceScope(state, children, curfew);
|
21
21
|
let resource: Awaited<ReturnType<typeof factory>>;
|
22
22
|
|
23
23
|
try {
|
@@ -60,7 +60,7 @@ export const createWithContext = async <
|
|
60
60
|
|
61
61
|
/**
|
62
62
|
* Tear down the resource and all its children, permanently destroying the
|
63
|
-
* reference.
|
63
|
+
* reference. This works on both root resources and child resources.
|
64
64
|
*/
|
65
65
|
export const destroy = async (handle: object) => {
|
66
66
|
if (!constructed.has(handle)) {
|
@@ -8,7 +8,7 @@ import { ResourceFactory, ParametrizedResourceFactory } from './utility-types';
|
|
8
8
|
* provisioned. It allows them to provision other resources while keeping
|
9
9
|
* track of ownership and lifetimes.
|
10
10
|
*/
|
11
|
-
export default class
|
11
|
+
export default class ResourceScope {
|
12
12
|
#destroyed = new WeakSet<object>();
|
13
13
|
#resources: Set<object>;
|
14
14
|
#curfew: RevokableResource['curfew'];
|
package/src/root-lifecycle.ts
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
-
import { createWithContext } from './resource-lifecycle';
|
1
|
+
import { createWithContext, destroy } from './resource-lifecycle';
|
2
2
|
import { ParametrizedResourceFactory } from './utility-types';
|
3
3
|
import { ResourceFactory } from './utility-types';
|
4
|
+
import { resources, roots } from './global-weakrefs';
|
4
5
|
|
5
6
|
/** Provision a resource and return its external API. */
|
6
|
-
export const
|
7
|
+
export const createRoot = async <
|
7
8
|
Controls extends object,
|
8
9
|
Args extends Array<unknown>,
|
9
10
|
>(
|
@@ -14,9 +15,22 @@ export const create = async <
|
|
14
15
|
...args: Args
|
15
16
|
): Promise<Controls> => {
|
16
17
|
const rootContext = Object.create(null);
|
17
|
-
|
18
|
+
const root = await createWithContext(rootContext, factory, ...args);
|
19
|
+
roots.add(root);
|
20
|
+
|
21
|
+
return root;
|
18
22
|
};
|
19
23
|
|
20
|
-
|
21
|
-
|
22
|
-
|
24
|
+
/**
|
25
|
+
* Tear down the resource and all its children, permanently destroying the
|
26
|
+
* reference. This cannot be used to destroy child resources, only roots.
|
27
|
+
*/
|
28
|
+
export const destroyRoot = async (resource: object): Promise<void> => {
|
29
|
+
if (resources.has(resource) && !roots.has(resource)) {
|
30
|
+
throw new Error(
|
31
|
+
'Cannot destroy child resource. It is owned by another scope.',
|
32
|
+
);
|
33
|
+
}
|
34
|
+
|
35
|
+
return destroy(resource);
|
36
|
+
};
|
package/src/utility-types.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2
|
-
import type
|
2
|
+
import type ResourceScope from './resource-scope';
|
3
3
|
|
4
4
|
/**
|
5
5
|
* Represents an arbitrary stateful resource that is asynchronously provisioned
|
@@ -7,14 +7,14 @@ import type ResourceControls from './resource-controls';
|
|
7
7
|
* first tears down the children.
|
8
8
|
*/
|
9
9
|
export interface ResourceFactory<Value extends object> {
|
10
|
-
(resource:
|
10
|
+
(resource: ResourceScope): Promise<Resource<Value>>;
|
11
11
|
}
|
12
12
|
|
13
13
|
export interface ParametrizedResourceFactory<
|
14
14
|
Value extends object,
|
15
15
|
Args extends Array<unknown>,
|
16
16
|
> {
|
17
|
-
(resource:
|
17
|
+
(resource: ResourceScope, ...args: Args): Promise<Resource<Value>>;
|
18
18
|
}
|
19
19
|
|
20
20
|
export interface Resource<Value extends object> {
|
@@ -1,236 +0,0 @@
|
|
1
|
-
import type ResourceControls from '../resource-controls';
|
2
|
-
import { create, destroy } from '../root-lifecycle';
|
3
|
-
import bindContext from '../bind-context';
|
4
|
-
|
5
|
-
describe('roots', () => {
|
6
|
-
describe('create', () => {
|
7
|
-
it('allocates the resource', async () => {
|
8
|
-
const config = { test: 'init-args' };
|
9
|
-
const Test = vi.fn(
|
10
|
-
async (_resource: ResourceControls, config: { test: string }) => ({
|
11
|
-
value: config,
|
12
|
-
}),
|
13
|
-
);
|
14
|
-
|
15
|
-
await expect(create(Test, config)).resolves.toEqual(config);
|
16
|
-
expect(Test).toHaveBeenCalledWith(expect.anything(), config);
|
17
|
-
});
|
18
|
-
|
19
|
-
describe('after initialization failure', () => {
|
20
|
-
it('destroys child resources in reverse order', async () => {
|
21
|
-
const spy = vi.fn();
|
22
|
-
const First = async () => ({ value: [], destroy: () => spy('1st') });
|
23
|
-
const Second = async () => ({ value: [], destroy: () => spy('2nd') });
|
24
|
-
|
25
|
-
const Parent = async (resource: ResourceControls) => {
|
26
|
-
await resource.create(First);
|
27
|
-
await resource.create(Second);
|
28
|
-
throw new Error('Testing resource initialization errors');
|
29
|
-
};
|
30
|
-
|
31
|
-
await expect(create(Parent)).rejects.toThrow();
|
32
|
-
expect(spy.mock.calls).toEqual([['2nd'], ['1st']]);
|
33
|
-
});
|
34
|
-
|
35
|
-
it('continues even if the children cannot be destroyed', async () => {
|
36
|
-
const parentError = new Error('Testing parent resource aborts');
|
37
|
-
const childError = new Error('Testing child resource aborts');
|
38
|
-
const Child = async () => ({
|
39
|
-
value: [],
|
40
|
-
destroy() {
|
41
|
-
throw childError;
|
42
|
-
},
|
43
|
-
});
|
44
|
-
|
45
|
-
const Parent = async (resource: ResourceControls) => {
|
46
|
-
await resource.create(Child);
|
47
|
-
await resource.create(Child);
|
48
|
-
throw parentError;
|
49
|
-
};
|
50
|
-
|
51
|
-
await expect(create(Parent)).rejects.toMatchObject({
|
52
|
-
cause: parentError,
|
53
|
-
failures: [childError, childError],
|
54
|
-
});
|
55
|
-
});
|
56
|
-
});
|
57
|
-
});
|
58
|
-
|
59
|
-
describe('destroy', () => {
|
60
|
-
it('deallocates the resource', async () => {
|
61
|
-
const spy = vi.fn<[], void>();
|
62
|
-
const Test = async () => ({
|
63
|
-
value: {},
|
64
|
-
destroy: spy,
|
65
|
-
});
|
66
|
-
|
67
|
-
const test = await create(Test);
|
68
|
-
|
69
|
-
expect(spy).not.toHaveBeenCalled();
|
70
|
-
await expect(destroy(test)).resolves.not.toThrow();
|
71
|
-
expect(spy).toHaveBeenCalled();
|
72
|
-
});
|
73
|
-
|
74
|
-
it('throws an error if the object is not a resource', async () => {
|
75
|
-
await expect(destroy({})).rejects.toThrow(
|
76
|
-
'Cannot destroy object. It is not a resource.',
|
77
|
-
);
|
78
|
-
});
|
79
|
-
|
80
|
-
it('survives if the resource is already deallocated', async () => {
|
81
|
-
const Test = async () => ({ value: [] });
|
82
|
-
|
83
|
-
const test = await create(Test);
|
84
|
-
await destroy(test);
|
85
|
-
|
86
|
-
await expect(destroy(test)).resolves.not.toThrow();
|
87
|
-
});
|
88
|
-
|
89
|
-
it('automatically unmounts all children', async () => {
|
90
|
-
const spy = vi.fn();
|
91
|
-
const Child = async () => ({ value: [], destroy: spy });
|
92
|
-
async function Parent(resource: ResourceControls) {
|
93
|
-
await resource.create(Child);
|
94
|
-
return { value: [] };
|
95
|
-
}
|
96
|
-
|
97
|
-
const parent = await create(Parent);
|
98
|
-
await destroy(parent);
|
99
|
-
|
100
|
-
expect(spy).toHaveBeenCalled();
|
101
|
-
});
|
102
|
-
|
103
|
-
it('throws an error if any of the children fail to close', async () => {
|
104
|
-
const error = new Error('Testing resource destruction errors');
|
105
|
-
const Child = async () => ({
|
106
|
-
value: [],
|
107
|
-
destroy() {
|
108
|
-
throw error;
|
109
|
-
},
|
110
|
-
});
|
111
|
-
|
112
|
-
const Parent = async (resource: ResourceControls) => {
|
113
|
-
await resource.create(Child);
|
114
|
-
return { value: [] };
|
115
|
-
};
|
116
|
-
|
117
|
-
const parent = await create(Parent);
|
118
|
-
await expect(destroy(parent)).rejects.toThrow(error);
|
119
|
-
});
|
120
|
-
|
121
|
-
it('destroys child resources in reverse order', async () => {
|
122
|
-
const spy = vi.fn();
|
123
|
-
const First = async () => ({ value: [], destroy: () => spy('1st') });
|
124
|
-
const Second = async () => ({ value: [], destroy: () => spy('2nd') });
|
125
|
-
|
126
|
-
const Parent = async (resource: ResourceControls) => {
|
127
|
-
await resource.create(First);
|
128
|
-
await resource.create(Second);
|
129
|
-
return { value: [] };
|
130
|
-
};
|
131
|
-
|
132
|
-
const parent = await create(Parent);
|
133
|
-
await destroy(parent);
|
134
|
-
|
135
|
-
expect(spy.mock.calls).toEqual([['2nd'], ['1st']]);
|
136
|
-
});
|
137
|
-
|
138
|
-
it('reports if multiple resources could not be destroyed', async () => {
|
139
|
-
const childError = new Error('Testing child resource aborts');
|
140
|
-
const Child = async () => ({
|
141
|
-
value: [],
|
142
|
-
destroy() {
|
143
|
-
throw childError;
|
144
|
-
},
|
145
|
-
});
|
146
|
-
|
147
|
-
const Parent = async (resource: ResourceControls) => {
|
148
|
-
await resource.create(Child);
|
149
|
-
await resource.create(Child);
|
150
|
-
return { value: [] };
|
151
|
-
};
|
152
|
-
|
153
|
-
const parent = await create(Parent);
|
154
|
-
await expect(destroy(parent)).rejects.toMatchObject({
|
155
|
-
failures: [childError, childError],
|
156
|
-
});
|
157
|
-
});
|
158
|
-
|
159
|
-
it('ensures child resources outlive their consumers', async () => {
|
160
|
-
const Child = async () => ({ value: [1] });
|
161
|
-
const Parent = async (resource: ResourceControls) => {
|
162
|
-
const child = await resource.create(Child);
|
163
|
-
return {
|
164
|
-
value: [],
|
165
|
-
destroy() {
|
166
|
-
// The `child` resource must still be usable here.
|
167
|
-
expect(child).toHaveLength(1);
|
168
|
-
},
|
169
|
-
};
|
170
|
-
};
|
171
|
-
|
172
|
-
const parent = await create(Parent);
|
173
|
-
await destroy(parent);
|
174
|
-
});
|
175
|
-
|
176
|
-
it('destroys child resources even if the parent fails to close', async () => {
|
177
|
-
const spy = vi.fn();
|
178
|
-
const Child = async () => ({ value: [], destroy: spy });
|
179
|
-
const Parent = async (resource: ResourceControls) => {
|
180
|
-
await resource.create(Child);
|
181
|
-
return {
|
182
|
-
value: [],
|
183
|
-
destroy() {
|
184
|
-
throw new Error('Testing parent teardown errors');
|
185
|
-
},
|
186
|
-
};
|
187
|
-
};
|
188
|
-
|
189
|
-
const parent = await create(Parent);
|
190
|
-
await expect(destroy(parent)).rejects.toThrow();
|
191
|
-
expect(spy).toHaveBeenCalled();
|
192
|
-
});
|
193
|
-
|
194
|
-
it('aggregates errors if the child resource fails to close', async () => {
|
195
|
-
const parentError = new Error('Testing parent resource aborts');
|
196
|
-
const childError = new Error('Testing child resource aborts');
|
197
|
-
|
198
|
-
const Child = async () => ({
|
199
|
-
value: [],
|
200
|
-
destroy() {
|
201
|
-
throw childError;
|
202
|
-
},
|
203
|
-
});
|
204
|
-
|
205
|
-
const Parent = async (resource: ResourceControls) => {
|
206
|
-
await resource.create(Child);
|
207
|
-
return {
|
208
|
-
value: [],
|
209
|
-
destroy() {
|
210
|
-
throw parentError;
|
211
|
-
},
|
212
|
-
};
|
213
|
-
};
|
214
|
-
|
215
|
-
const parent = await create(Parent);
|
216
|
-
await expect(destroy(parent)).rejects.toMatchObject({
|
217
|
-
failures: [parentError, childError],
|
218
|
-
});
|
219
|
-
});
|
220
|
-
|
221
|
-
it('guards against creating new resources after teardown', async () => {
|
222
|
-
const Child = async () => ({ value: [] });
|
223
|
-
const Parent = async (resource: ResourceControls) => ({
|
224
|
-
value: bindContext(resource),
|
225
|
-
});
|
226
|
-
|
227
|
-
const parent = await create(Parent);
|
228
|
-
const { create: resourceCreate } = parent;
|
229
|
-
await destroy(parent);
|
230
|
-
|
231
|
-
await expect(resourceCreate(Child)).rejects.toThrow(
|
232
|
-
/cannot create.*after teardown/i,
|
233
|
-
);
|
234
|
-
});
|
235
|
-
});
|
236
|
-
});
|