wardens 0.6.0-rc.0 → 0.6.0-rc.2
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +2 -1
- package/README.md +1 -1
- package/dist/wardens.cjs +1 -1
- package/dist/wardens.js +105 -98
- package/package.json +6 -6
- package/src/__tests__/{resource-api.test.ts → resource-scope.test.ts} +28 -16
- 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} +3 -2
- 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,8 @@ 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.
|
16
17
|
|
17
18
|
## [0.5.1] - 2023-08-12
|
18
19
|
|
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,154 +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
|
-
|
82
|
-
n(this, i)[x.getId(e)] = t;
|
83
|
-
});
|
81
|
+
h(this, "setContext", (t, r) => (n(this, i)[x.getId(t)] = r, r));
|
84
82
|
/** Retrieve a value from context, or a default if it is unset. */
|
85
|
-
|
86
|
-
const
|
87
|
-
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);
|
88
86
|
});
|
89
|
-
|
87
|
+
f(this, i, t), f(this, l, r), f(this, p, o);
|
90
88
|
}
|
91
89
|
}
|
92
|
-
y = new WeakMap(),
|
93
|
-
const
|
94
|
-
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);
|
95
93
|
let c;
|
96
94
|
try {
|
97
|
-
c = await
|
98
|
-
} catch (
|
99
|
-
const
|
100
|
-
|
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))
|
101
99
|
)).filter(
|
102
|
-
(
|
100
|
+
(u) => u.status === "rejected"
|
103
101
|
);
|
104
|
-
throw v.length ?
|
105
|
-
v.map((
|
106
|
-
{ cause:
|
107
|
-
) :
|
102
|
+
throw v.length ? P(
|
103
|
+
v.map((u) => u.reason),
|
104
|
+
{ cause: S }
|
105
|
+
) : S;
|
108
106
|
}
|
109
|
-
const
|
110
|
-
return
|
111
|
-
curfew:
|
107
|
+
const W = c.value, { proxy: C, revoke: I } = A(W);
|
108
|
+
return k.add(C), g.set(C, {
|
109
|
+
curfew: o,
|
112
110
|
resource: c,
|
113
|
-
children:
|
114
|
-
revoke:
|
115
|
-
}),
|
116
|
-
},
|
117
|
-
if (!
|
111
|
+
children: s,
|
112
|
+
revoke: I
|
113
|
+
}), C;
|
114
|
+
}, b = async (e) => {
|
115
|
+
if (!k.has(e))
|
118
116
|
throw new Error("Cannot destroy object. It is not a resource.");
|
119
|
-
const
|
120
|
-
if (
|
121
|
-
|
122
|
-
let
|
117
|
+
const t = g.get(e);
|
118
|
+
if (t) {
|
119
|
+
g.delete(e), t.revoke();
|
120
|
+
let r = {
|
123
121
|
status: "fulfilled",
|
124
122
|
value: void 0
|
125
123
|
};
|
126
|
-
if (
|
124
|
+
if (t.resource.destroy)
|
127
125
|
try {
|
128
|
-
await
|
126
|
+
await t.resource.destroy();
|
129
127
|
} catch (c) {
|
130
|
-
|
128
|
+
r = { status: "rejected", reason: c };
|
131
129
|
}
|
132
|
-
|
133
|
-
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(
|
134
132
|
(c) => c.status === "rejected"
|
135
133
|
);
|
136
134
|
if (a.length)
|
137
|
-
throw
|
135
|
+
throw P(a.map((c) => c.reason));
|
138
136
|
}
|
139
|
-
},
|
140
|
-
class
|
141
|
-
constructor(
|
137
|
+
}, P = (e, t) => e.length === 1 ? e[0] : new V(e, t);
|
138
|
+
class V extends Error {
|
139
|
+
constructor(t, r) {
|
142
140
|
super(
|
143
141
|
"Some resources could not be destroyed. See the `failures` property for details.",
|
144
|
-
|
145
|
-
), this.failures =
|
142
|
+
r
|
143
|
+
), this.failures = t;
|
146
144
|
}
|
147
145
|
}
|
148
|
-
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
|
+
};
|
149
156
|
export {
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
157
|
+
U as bindContext,
|
158
|
+
q as create,
|
159
|
+
Y as createContext,
|
160
|
+
z as destroy
|
154
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.2",
|
4
4
|
"description": "A framework for resource management",
|
5
5
|
"type": "module",
|
6
6
|
"main": "./dist/wardens.cjs",
|
@@ -79,13 +79,13 @@
|
|
79
79
|
}
|
80
80
|
},
|
81
81
|
"devDependencies": {
|
82
|
-
"@typescript-eslint/eslint-plugin": "6.4.
|
83
|
-
"@typescript-eslint/parser": "6.4.
|
84
|
-
"eslint": "8.
|
82
|
+
"@typescript-eslint/eslint-plugin": "6.4.1",
|
83
|
+
"@typescript-eslint/parser": "6.4.1",
|
84
|
+
"eslint": "8.48.0",
|
85
85
|
"husky": "8.0.3",
|
86
|
-
"lint-staged": "14.0.
|
86
|
+
"lint-staged": "14.0.1",
|
87
87
|
"prettier": "2.8.8",
|
88
|
-
"typescript": "5.
|
88
|
+
"typescript": "5.2.2",
|
89
89
|
"vite": "4.4.9",
|
90
90
|
"vitest": "^0.34.0"
|
91
91
|
}
|
@@ -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');
|
@@ -92,14 +92,26 @@ describe('ResourceControls', () => {
|
|
92
92
|
await expect(create(Test)).resolves.toEqual([]);
|
93
93
|
});
|
94
94
|
|
95
|
+
it('returns the new context value', async () => {
|
96
|
+
const SharedValue = createContext(() => 'none');
|
97
|
+
|
98
|
+
const Test = async (resource: ResourceScope) => {
|
99
|
+
return {
|
100
|
+
value: { value: resource.setContext(SharedValue, 'returned') },
|
101
|
+
};
|
102
|
+
};
|
103
|
+
|
104
|
+
await expect(create(Test)).resolves.toEqual({ value: 'returned' });
|
105
|
+
});
|
106
|
+
|
95
107
|
it('passes context to child resources', async () => {
|
96
108
|
const SharedValue = createContext<null | string>(() => null);
|
97
109
|
|
98
|
-
const Child = async (ctx:
|
110
|
+
const Child = async (ctx: ResourceScope) => ({
|
99
111
|
value: { content: ctx.getContext(SharedValue) },
|
100
112
|
});
|
101
113
|
|
102
|
-
const Parent = async (resource:
|
114
|
+
const Parent = async (resource: ResourceScope) => {
|
103
115
|
resource.setContext(SharedValue, 'inherited');
|
104
116
|
return { value: await resource.create(Child) };
|
105
117
|
};
|
@@ -109,7 +121,7 @@ describe('ResourceControls', () => {
|
|
109
121
|
|
110
122
|
it('can override context without affecting the parent', async () => {
|
111
123
|
const Message = createContext<null | string>(() => null);
|
112
|
-
const Child = async (resource:
|
124
|
+
const Child = async (resource: ResourceScope) => {
|
113
125
|
// This should *NOT* affect the parent context.
|
114
126
|
resource.setContext(Message, 'child context');
|
115
127
|
|
@@ -118,7 +130,7 @@ describe('ResourceControls', () => {
|
|
118
130
|
};
|
119
131
|
};
|
120
132
|
|
121
|
-
const Parent = async (resource:
|
133
|
+
const Parent = async (resource: ResourceScope) => {
|
122
134
|
resource.setContext(Message, 'parent context');
|
123
135
|
const child = await resource.create(Child);
|
124
136
|
|
@@ -140,7 +152,7 @@ describe('ResourceControls', () => {
|
|
140
152
|
|
141
153
|
it('allows two siblings to have different context values', async () => {
|
142
154
|
const Message = createContext<null | string>(() => null);
|
143
|
-
const Child = async (resource:
|
155
|
+
const Child = async (resource: ResourceScope, msg: string) => {
|
144
156
|
resource.setContext(Message, msg);
|
145
157
|
|
146
158
|
return {
|
@@ -148,7 +160,7 @@ describe('ResourceControls', () => {
|
|
148
160
|
};
|
149
161
|
};
|
150
162
|
|
151
|
-
const Parent = async (resource:
|
163
|
+
const Parent = async (resource: ResourceScope) => {
|
152
164
|
resource.setContext(Message, 'parent context');
|
153
165
|
|
154
166
|
return {
|
@@ -166,11 +178,11 @@ describe('ResourceControls', () => {
|
|
166
178
|
|
167
179
|
it('provides a live view of the current value, not just a snapshot', async () => {
|
168
180
|
const Message = createContext(() => 'default');
|
169
|
-
const Child = async (resource:
|
181
|
+
const Child = async (resource: ResourceScope) => ({
|
170
182
|
value: { getMessage: () => resource.getContext(Message) },
|
171
183
|
});
|
172
184
|
|
173
|
-
const Parent = async (resource:
|
185
|
+
const Parent = async (resource: ResourceScope) => ({
|
174
186
|
value: {
|
175
187
|
setMessage: (msg: string) => resource.setContext(Message, msg),
|
176
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'];
|
@@ -64,8 +64,9 @@ export default class ResourceControls {
|
|
64
64
|
public setContext = <Value>(
|
65
65
|
context: ContextHandle<Value>,
|
66
66
|
value: Value,
|
67
|
-
):
|
67
|
+
): Value => {
|
68
68
|
this.#state[ContextHandle.getId(context)] = value;
|
69
|
+
return value;
|
69
70
|
};
|
70
71
|
|
71
72
|
/** Retrieve a value from context, or a default if it is unset. */
|
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
|
-
});
|