wardens 0.5.1 → 0.6.0-rc.0
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/dist/wardens.cjs +1 -1
- package/dist/wardens.js +103 -74
- package/package.json +4 -4
- package/src/__tests__/resource-api.test.ts +186 -0
- package/src/__tests__/{allocation.test.ts → root-lifecycle.test.ts} +14 -14
- package/src/__tests__/root-lifecycles.test.ts +236 -0
- package/src/__tests__/roots.test.ts +236 -0
- package/src/__tests__/{types.test-d.ts → utility-types.test-d.ts} +2 -2
- package/src/{state.ts → global-weakrefs.ts} +11 -11
- package/src/index.ts +4 -3
- package/src/inherited-context.ts +32 -0
- package/src/{resource-context.ts → resource-controls.ts} +32 -8
- package/src/{allocation.ts → resource-lifecycle.ts} +11 -9
- package/src/root-lifecycle.ts +22 -0
- package/src/{types.ts → utility-types.ts} +3 -3
- package/src/__tests__/resource-context.test.ts +0 -75
- /package/src/{proxy.ts → wrap-with-proxy.ts} +0 -0
@@ -0,0 +1,236 @@
|
|
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
|
+
});
|
@@ -0,0 +1,236 @@
|
|
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
|
+
});
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { create,
|
1
|
+
import { create, ResourceControls, 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: ResourceControls, value: { count: number }) {
|
21
21
|
return {
|
22
22
|
value,
|
23
23
|
};
|
@@ -1,4 +1,14 @@
|
|
1
|
-
import type { Resource } from './types';
|
1
|
+
import type { Resource } from './utility-types';
|
2
|
+
|
3
|
+
/** Maps an external API back to the resource that created it. */
|
4
|
+
export const resources = new WeakMap<object, RevokableResource>();
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Identifies if an object is a Wardens resource. Carries no metadata and
|
8
|
+
* objects are never deleted. This is separate from `resources` to avoid
|
9
|
+
* retaining associated objects in memory.
|
10
|
+
*/
|
11
|
+
export const constructed = new WeakSet<object>();
|
2
12
|
|
3
13
|
export interface RevokableResource {
|
4
14
|
resource: Resource<object>;
|
@@ -18,13 +28,3 @@ export interface RevokableResource {
|
|
18
28
|
*/
|
19
29
|
curfew: { enforced: boolean };
|
20
30
|
}
|
21
|
-
|
22
|
-
/** Maps an external API back to the resource that created it. */
|
23
|
-
export const resources = new WeakMap<object, RevokableResource>();
|
24
|
-
|
25
|
-
/**
|
26
|
-
* Identifies if an object is a Wardens resource. Carries no metadata and
|
27
|
-
* objects are never deleted. This is separate from `resources` to avoid
|
28
|
-
* retaining associated objects in memory.
|
29
|
-
*/
|
30
|
-
export const constructed = new WeakSet<object>();
|
package/src/index.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
export { default as bindContext } from './bind-context';
|
2
|
-
export { create, destroy } from './
|
3
|
-
export
|
4
|
-
export type {
|
2
|
+
export { create, destroy } from './root-lifecycle';
|
3
|
+
export { createContext } from './inherited-context';
|
4
|
+
export type { Resource, ResourceHandle } from './utility-types';
|
5
|
+
export type { default as ResourceControls } from './resource-controls';
|
@@ -0,0 +1,32 @@
|
|
1
|
+
/**
|
2
|
+
* A prototype chain that matches the resource hierarchy. This is used to
|
3
|
+
* pass state down the tree without plumbing through every function.
|
4
|
+
*/
|
5
|
+
export type InheritedContext = Record<symbol, unknown>;
|
6
|
+
|
7
|
+
/** An opaque handle used to get/set values in a prototype chain. */
|
8
|
+
export class ContextHandle<Value> {
|
9
|
+
static getId(handle: ContextHandle<unknown>): symbol {
|
10
|
+
return handle.#id;
|
11
|
+
}
|
12
|
+
|
13
|
+
static getDefaultValue<Value>(handle: ContextHandle<Value>): Value {
|
14
|
+
return handle.#getDefaultContext();
|
15
|
+
}
|
16
|
+
|
17
|
+
#id = Symbol('Context ID');
|
18
|
+
#getDefaultContext: () => Value;
|
19
|
+
|
20
|
+
constructor(getDefaultContext: () => Value) {
|
21
|
+
this.#getDefaultContext = getDefaultContext;
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Create a context object that is used to get and set context values in the
|
27
|
+
* resource hierarchy.
|
28
|
+
*/
|
29
|
+
export const createContext = <Value>(
|
30
|
+
/** Returns a default value if the context could not be found. */
|
31
|
+
getDefaultContext: () => Value,
|
32
|
+
): ContextHandle<Value> => new ContextHandle(getDefaultContext);
|
@@ -1,23 +1,27 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
3
|
-
import {
|
1
|
+
import { createWithContext, destroy } from './resource-lifecycle';
|
2
|
+
import { ContextHandle, InheritedContext } from './inherited-context';
|
3
|
+
import { RevokableResource } from './global-weakrefs';
|
4
|
+
import { ResourceFactory, ParametrizedResourceFactory } from './utility-types';
|
4
5
|
|
5
6
|
/**
|
6
7
|
* An instance of this class is passed to resources as they're being
|
7
8
|
* provisioned. It allows them to provision other resources while keeping
|
8
9
|
* track of ownership and lifetimes.
|
9
10
|
*/
|
10
|
-
export default class
|
11
|
+
export default class ResourceControls {
|
11
12
|
#destroyed = new WeakSet<object>();
|
12
13
|
#resources: Set<object>;
|
13
|
-
#
|
14
|
+
#curfew: RevokableResource['curfew'];
|
15
|
+
#state: InheritedContext;
|
14
16
|
|
15
17
|
constructor(
|
18
|
+
state: InheritedContext,
|
16
19
|
ownedResources: Set<object>,
|
17
20
|
freeze: RevokableResource['curfew'],
|
18
21
|
) {
|
22
|
+
this.#state = state;
|
19
23
|
this.#resources = ownedResources;
|
20
|
-
this.#
|
24
|
+
this.#curfew = freeze;
|
21
25
|
}
|
22
26
|
|
23
27
|
/** Provision an owned resource and make sure it doesn't outlive us. */
|
@@ -27,11 +31,12 @@ export default class ResourceContext {
|
|
27
31
|
| ResourceFactory<Controls>,
|
28
32
|
...args: Args
|
29
33
|
): Promise<Controls> => {
|
30
|
-
if (this.#
|
34
|
+
if (this.#curfew.enforced) {
|
31
35
|
throw new Error('Cannot create new resources after teardown.');
|
32
36
|
}
|
33
37
|
|
34
|
-
const
|
38
|
+
const context = Object.create(this.#state);
|
39
|
+
const controls = await createWithContext(context, factory, ...args);
|
35
40
|
this.#resources.add(controls);
|
36
41
|
|
37
42
|
return controls;
|
@@ -54,4 +59,23 @@ export default class ResourceContext {
|
|
54
59
|
this.#destroyed.add(resource);
|
55
60
|
await destroy(resource);
|
56
61
|
};
|
62
|
+
|
63
|
+
/** Store a value in context. Anything down the chain can read it. */
|
64
|
+
public setContext = <Value>(
|
65
|
+
context: ContextHandle<Value>,
|
66
|
+
value: Value,
|
67
|
+
): void => {
|
68
|
+
this.#state[ContextHandle.getId(context)] = value;
|
69
|
+
};
|
70
|
+
|
71
|
+
/** Retrieve a value from context, or a default if it is unset. */
|
72
|
+
public getContext = <Value>(context: ContextHandle<Value>): Value => {
|
73
|
+
const id = ContextHandle.getId(context);
|
74
|
+
|
75
|
+
if (id in this.#state) {
|
76
|
+
return this.#state[id] as Value;
|
77
|
+
}
|
78
|
+
|
79
|
+
return ContextHandle.getDefaultValue(context);
|
80
|
+
};
|
57
81
|
}
|
@@ -1,25 +1,27 @@
|
|
1
|
-
import { resources, constructed } from './
|
2
|
-
import wrap from './proxy';
|
3
|
-
import
|
4
|
-
import {
|
1
|
+
import { resources, constructed } from './global-weakrefs';
|
2
|
+
import wrap from './wrap-with-proxy';
|
3
|
+
import ResourceControls from './resource-controls';
|
4
|
+
import type { InheritedContext } from './inherited-context';
|
5
|
+
import { ResourceFactory, ParametrizedResourceFactory } from './utility-types';
|
5
6
|
|
6
7
|
/** Provision a resource and return its external API. */
|
7
|
-
export const
|
8
|
+
export const createWithContext = async <
|
8
9
|
Controls extends object,
|
9
10
|
Args extends Array<unknown>,
|
10
11
|
>(
|
11
|
-
|
12
|
+
state: InheritedContext,
|
13
|
+
factory:
|
12
14
|
| ParametrizedResourceFactory<Controls, Args>
|
13
15
|
| ResourceFactory<Controls>,
|
14
16
|
...args: Args
|
15
17
|
): Promise<Controls> => {
|
16
18
|
const curfew = { enforced: false };
|
17
19
|
const children: Set<object> = new Set();
|
18
|
-
const context = new
|
19
|
-
let resource: Awaited<ReturnType<typeof
|
20
|
+
const context = new ResourceControls(state, children, curfew);
|
21
|
+
let resource: Awaited<ReturnType<typeof factory>>;
|
20
22
|
|
21
23
|
try {
|
22
|
-
resource = await
|
24
|
+
resource = await factory(context, ...args);
|
23
25
|
} catch (error) {
|
24
26
|
// Resource could not be created. Clean up the intermediate resources.
|
25
27
|
const orphans = Array.from(children).reverse();
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { createWithContext } from './resource-lifecycle';
|
2
|
+
import { ParametrizedResourceFactory } from './utility-types';
|
3
|
+
import { ResourceFactory } from './utility-types';
|
4
|
+
|
5
|
+
/** Provision a resource and return its external API. */
|
6
|
+
export const create = async <
|
7
|
+
Controls extends object,
|
8
|
+
Args extends Array<unknown>,
|
9
|
+
>(
|
10
|
+
/** An async function that resolves to a resource. */
|
11
|
+
factory:
|
12
|
+
| ParametrizedResourceFactory<Controls, Args>
|
13
|
+
| ResourceFactory<Controls>,
|
14
|
+
...args: Args
|
15
|
+
): Promise<Controls> => {
|
16
|
+
const rootContext = Object.create(null);
|
17
|
+
return createWithContext(rootContext, factory, ...args);
|
18
|
+
};
|
19
|
+
|
20
|
+
// Destroying a root resource is the same process as destroying a child
|
21
|
+
// resource. No need to change the implementation.
|
22
|
+
export { destroy } from './resource-lifecycle';
|