wardens 0.5.1 → 0.6.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +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';
|