wardens 0.1.0 → 0.2.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 CHANGED
@@ -6,7 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [0.1.0]
9
+ ## [0.2.0] - 2022-05-24
10
+
11
+ ### Fixed
12
+
13
+ - `mount(...)` and `allocate(...)` no longer require a config argument if the resource doesn't explicitly define one.
14
+
15
+ ### Added
16
+
17
+ - `enter(...)` now supports variable arguments.
18
+
19
+ ### Changed
20
+
21
+ - The second generic parameter of `Resource` was a config parameter, but now it's an argument tuple.
22
+ - The `ExternalControls` utility type was renamed to `Controls`.
23
+
24
+ ## [0.1.0] - 2022-05-22
10
25
 
11
26
  ### Added
12
27
 
@@ -14,5 +29,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
14
29
  - `mount`/`unmount` hooks to provision resources
15
30
  - `allocate`/`deallocate` for creating hierarchies of resources
16
31
 
17
- [Unreleased]: https://github.com/PsychoLlama/wardens/compare/v0.1.0...HEAD
32
+ [Unreleased]: https://github.com/PsychoLlama/wardens/compare/v0.2.0...HEAD
33
+ [0.2.0]: https://github.com/PsychoLlama/wardens/compare/v0.1.0...v0.2.0
18
34
  [0.1.0]: https://github.com/PsychoLlama/wardens/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
- # Wardens
2
-
3
- A tiny framework for managing resources.
1
+ <div align="center">
2
+ <h1>Wardens</h1>
3
+ <p>A tiny framework for managing resources.</p>
4
+ <img alt="Build status" src="https://img.shields.io/github/workflow/status/PsychoLlama/wardens/Test/main" />
5
+ <img alt="TypeScript" src="https://img.shields.io/npm/types/wardens" />
6
+ <img alt="npm version" src="https://img.shields.io/npm/v/wardens" />
7
+ </div>
4
8
 
5
9
  ## Overview
6
10
 
@@ -30,7 +34,7 @@ class Worker extends Resource<Thread> {
30
34
  Now define a pool that creates and manages workers:
31
35
 
32
36
  ```typescript
33
- class WorkerPool extends Resource<Controls, Config> {
37
+ class WorkerPool extends Resource<Controls, [Config]> {
34
38
  threads!: Array<Thread> = [];
35
39
 
36
40
  async enter({ poolSize }: Config) {
@@ -17,25 +17,25 @@ const ownership = /* @__PURE__ */ new WeakMap();
17
17
  function wrapWithProxy(value) {
18
18
  return Proxy.revocable(value, {});
19
19
  }
20
- async function mount(Subtype, params) {
21
- const resource = new Subtype();
22
- await resource.enter(params);
23
- const api = resource.exports();
24
- const { proxy, revoke } = wrapWithProxy(api);
20
+ async function mount(Entity, ...args) {
21
+ const resource = new Entity();
22
+ await resource.enter(...args);
23
+ const controls = resource.exports();
24
+ const { proxy, revoke } = wrapWithProxy(controls);
25
25
  resources.set(proxy, {
26
26
  resource,
27
27
  revoke
28
28
  });
29
29
  return proxy;
30
30
  }
31
- async function unmount(api) {
32
- const entry = resources.get(api);
31
+ async function unmount(controls) {
32
+ const entry = resources.get(controls);
33
33
  if (entry) {
34
- resources.delete(api);
34
+ resources.delete(controls);
35
35
  entry.revoke();
36
36
  const children = ownership.get(entry.resource);
37
37
  ownership.delete(entry.resource);
38
- const recursiveUnmounts = children.map((api2) => unmount(api2));
38
+ const recursiveUnmounts = children.map((controls2) => unmount(controls2));
39
39
  const results = await Promise.allSettled(recursiveUnmounts);
40
40
  await entry.resource.leave();
41
41
  results.forEach((result) => {
@@ -51,23 +51,23 @@ class Resource {
51
51
  __privateAdd(this, _children, []);
52
52
  ownership.set(this, __privateGet(this, _children));
53
53
  }
54
- async enter(_params) {
54
+ async enter(..._args) {
55
55
  return;
56
56
  }
57
57
  async leave() {
58
58
  return;
59
59
  }
60
- async allocate(Child, params) {
61
- const api = await mount(Child, params);
62
- __privateGet(this, _resources).add(api);
63
- __privateGet(this, _children).push(api);
64
- return api;
60
+ async allocate(Child, ...args) {
61
+ const controls = await mount(Child, ...args);
62
+ __privateGet(this, _resources).add(controls);
63
+ __privateGet(this, _children).push(controls);
64
+ return controls;
65
65
  }
66
- async deallocate(api) {
67
- if (!__privateGet(this, _resources).has(api)) {
66
+ async deallocate(resource) {
67
+ if (!__privateGet(this, _resources).has(resource)) {
68
68
  throw new Error("You do not own this resource.");
69
69
  }
70
- return unmount(api);
70
+ return unmount(resource);
71
71
  }
72
72
  }
73
73
  _resources = new WeakMap();
@@ -1 +1 @@
1
- var b=(e,t,c)=>{if(!t.has(e))throw TypeError("Cannot "+c)};var f=(e,t,c)=>(b(e,t,"read from private field"),c?c.call(e):t.get(e)),h=(e,t,c)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,c)};(function(e,t){typeof exports=="object"&&typeof module!="undefined"?t(exports):typeof define=="function"&&define.amd?define(["exports"],t):(e=typeof globalThis!="undefined"?globalThis:e||self,t(e.wardens={}))})(this,function(e){var a,d;"use strict";const t=new WeakMap,c=new WeakMap;function w(i){return Proxy.revocable(i,{})}async function p(i,n){const o=new i;await o.enter(n);const s=o.exports(),{proxy:r,revoke:u}=w(s);return t.set(r,{resource:o,revoke:u}),r}async function l(i){const n=t.get(i);if(n){t.delete(i),n.revoke();const o=c.get(n.resource);c.delete(n.resource);const s=o.map(u=>l(u)),r=await Promise.allSettled(s);await n.resource.leave(),r.forEach(u=>{if(u.status==="rejected")throw u.reason})}}class y{constructor(){h(this,a,new WeakSet);h(this,d,[]);c.set(this,f(this,d))}async enter(n){}async leave(){}async allocate(n,o){const s=await p(n,o);return f(this,a).add(s),f(this,d).push(s),s}async deallocate(n){if(!f(this,a).has(n))throw new Error("You do not own this resource.");return l(n)}}a=new WeakMap,d=new WeakMap;function m(i){const n=new WeakMap;return new Proxy(i,{get(o,s){const r=Reflect.get(o,s,o);if(typeof r=="function"){if(n.has(r)===!1){const u=r.bind(o);Object.defineProperties(u,Object.getOwnPropertyDescriptors(r)),n.set(r,u)}return n.get(r)}return r},set(o,s,r){return Reflect.set(o,s,r,o)}})}e.Resource=y,e.bindContext=m,e.mount=p,e.unmount=l,Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
1
+ var b=(e,t,c)=>{if(!t.has(e))throw TypeError("Cannot "+c)};var f=(e,t,c)=>(b(e,t,"read from private field"),c?c.call(e):t.get(e)),h=(e,t,c)=>{if(t.has(e))throw TypeError("Cannot add the same private member more than once");t instanceof WeakSet?t.add(e):t.set(e,c)};(function(e,t){typeof exports=="object"&&typeof module!="undefined"?t(exports):typeof define=="function"&&define.amd?define(["exports"],t):(e=typeof globalThis!="undefined"?globalThis:e||self,t(e.wardens={}))})(this,function(e){var a,d;"use strict";const t=new WeakMap,c=new WeakMap;function p(i){return Proxy.revocable(i,{})}async function w(i,...n){const o=new i;await o.enter(...n);const s=o.exports(),{proxy:r,revoke:u}=p(s);return t.set(r,{resource:o,revoke:u}),r}async function l(i){const n=t.get(i);if(n){t.delete(i),n.revoke();const o=c.get(n.resource);c.delete(n.resource);const s=o.map(u=>l(u)),r=await Promise.allSettled(s);await n.resource.leave(),r.forEach(u=>{if(u.status==="rejected")throw u.reason})}}class y{constructor(){h(this,a,new WeakSet);h(this,d,[]);c.set(this,f(this,d))}async enter(...n){}async leave(){}async allocate(n,...o){const s=await w(n,...o);return f(this,a).add(s),f(this,d).push(s),s}async deallocate(n){if(!f(this,a).has(n))throw new Error("You do not own this resource.");return l(n)}}a=new WeakMap,d=new WeakMap;function m(i){const n=new WeakMap;return new Proxy(i,{get(o,s){const r=Reflect.get(o,s,o);if(typeof r=="function"){if(n.has(r)===!1){const u=r.bind(o);Object.defineProperties(u,Object.getOwnPropertyDescriptors(r)),n.set(r,u)}return n.get(r)}return r},set(o,s,r){return Reflect.set(o,s,r,o)}})}e.Resource=y,e.bindContext=m,e.mount=w,e.unmount=l,Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wardens",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A framework for resource management",
5
5
  "main": "./dist/wardens.umd.js",
6
6
  "module": "./dist/wardens.es.js",
@@ -11,6 +11,15 @@
11
11
  "dist",
12
12
  "src"
13
13
  ],
14
+ "keywords": [
15
+ "resource",
16
+ "management",
17
+ "manager",
18
+ "server",
19
+ "lifetimes",
20
+ "hierarchy",
21
+ "framework"
22
+ ],
14
23
  "scripts": {
15
24
  "prepare": "tsc && vite build",
16
25
  "test": "./bin/run-tests",
@@ -74,14 +83,14 @@
74
83
  },
75
84
  "devDependencies": {
76
85
  "@types/jest": "27.5.1",
77
- "@typescript-eslint/eslint-plugin": "5.25.0",
78
- "@typescript-eslint/parser": "5.25.0",
86
+ "@typescript-eslint/eslint-plugin": "5.26.0",
87
+ "@typescript-eslint/parser": "5.26.0",
79
88
  "eslint": "8.16.0",
80
89
  "husky": "8.0.1",
81
90
  "jest": "28.1.0",
82
91
  "lint-staged": "12.4.1",
83
92
  "prettier": "2.6.2",
84
- "ts-jest": "28.0.2",
93
+ "ts-jest": "28.0.3",
85
94
  "typescript": "4.6.4",
86
95
  "vite": "2.9.9",
87
96
  "vite-dts": "1.0.4"
@@ -4,16 +4,16 @@ import { mount, unmount } from '../allocation';
4
4
  describe('allocation', () => {
5
5
  describe('mount', () => {
6
6
  it('allocates the resource', async () => {
7
- const params = { test: 'init-args' };
7
+ const config = { test: 'init-args' };
8
8
  const enter = jest.fn();
9
9
 
10
- class Test extends Resource<{ test: boolean }, { test: string }> {
10
+ class Test extends Resource<{ test: boolean }, [{ test: string }]> {
11
11
  exports = () => ({ test: true });
12
12
  enter = enter;
13
13
  }
14
14
 
15
- await expect(mount(Test, params)).resolves.toEqual({ test: true });
16
- expect(enter).toHaveBeenCalledWith(params);
15
+ await expect(mount(Test, config)).resolves.toEqual({ test: true });
16
+ expect(enter).toHaveBeenCalledWith(config);
17
17
  });
18
18
  });
19
19
 
@@ -26,10 +26,10 @@ describe('allocation', () => {
26
26
  leave = leave;
27
27
  }
28
28
 
29
- const api = await mount(Test, null);
29
+ const test = await mount(Test);
30
30
 
31
31
  expect(leave).not.toHaveBeenCalled();
32
- await expect(unmount(api)).resolves.not.toThrow();
32
+ await expect(unmount(test)).resolves.not.toThrow();
33
33
  expect(leave).toHaveBeenCalled();
34
34
  });
35
35
 
@@ -38,10 +38,10 @@ describe('allocation', () => {
38
38
  exports = () => [];
39
39
  }
40
40
 
41
- const api = await mount(Test, null);
42
- await unmount(api);
41
+ const test = await mount(Test);
42
+ await unmount(test);
43
43
 
44
- await expect(unmount(api)).resolves.not.toThrow();
44
+ await expect(unmount(test)).resolves.not.toThrow();
45
45
  });
46
46
 
47
47
  it('automatically unmounts all children', async () => {
@@ -54,11 +54,11 @@ describe('allocation', () => {
54
54
  class Parent extends Resource<string[]> {
55
55
  exports = () => [];
56
56
  async enter() {
57
- await this.allocate(Child, null);
57
+ await this.allocate(Child);
58
58
  }
59
59
  }
60
60
 
61
- const parent = await mount(Parent, null);
61
+ const parent = await mount(Parent);
62
62
  await unmount(parent);
63
63
 
64
64
  expect(leave).toHaveBeenCalled();
@@ -76,11 +76,11 @@ describe('allocation', () => {
76
76
  class Parent extends Resource<string[]> {
77
77
  exports = () => [];
78
78
  async enter() {
79
- await this.allocate(Child, null);
79
+ await this.allocate(Child);
80
80
  }
81
81
  }
82
82
 
83
- const parent = await mount(Parent, null);
83
+ const parent = await mount(Parent);
84
84
  await expect(unmount(parent)).rejects.toThrow(error);
85
85
  });
86
86
  });
@@ -1,5 +1,6 @@
1
- import Resource, { type ExternalControls } from '../resource';
1
+ import Resource from '../resource';
2
2
  import { mount } from '../allocation';
3
+ import { type Controls } from '../types';
3
4
 
4
5
  describe('Resource', () => {
5
6
  class Test extends Resource<Record<string, never>> {
@@ -18,16 +19,16 @@ describe('Resource', () => {
18
19
  exports = () => ({ child: true });
19
20
  }
20
21
 
21
- class Parent extends Resource<ExternalControls<Child>> {
22
+ class Parent extends Resource<Controls<Child>> {
22
23
  exports = () => this.child;
23
- child!: ExternalControls<Child>;
24
+ child!: Controls<Child>;
24
25
 
25
26
  async enter() {
26
- this.child = await this.allocate(Child, null);
27
+ this.child = await this.allocate(Child);
27
28
  }
28
29
  }
29
30
 
30
- await expect(mount(Parent, null)).resolves.toEqual({ child: true });
31
+ await expect(mount(Parent)).resolves.toEqual({ child: true });
31
32
  });
32
33
 
33
34
  it('can deallocate child resources on demand', async () => {
@@ -40,20 +41,20 @@ describe('Resource', () => {
40
41
 
41
42
  class Parent extends Resource<{ parent: boolean }> {
42
43
  exports = () => ({ parent: true });
43
- child!: ExternalControls<Child>;
44
+ child!: Controls<Child>;
44
45
 
45
46
  async enter() {
46
- const child = await this.allocate(Child, null);
47
+ const child = await this.allocate(Child);
47
48
  await this.deallocate(child);
48
49
  }
49
50
  }
50
51
 
51
- await expect(mount(Parent, null)).resolves.toEqual({ parent: true });
52
+ await expect(mount(Parent)).resolves.toEqual({ parent: true });
52
53
  expect(leave).toHaveBeenCalled();
53
54
  });
54
55
 
55
56
  it('fails to destroy resources owned by someone else', async () => {
56
- const test = await mount(Test, null);
57
+ const test = await mount(Test);
57
58
 
58
59
  class Sneaky extends Resource<string[]> {
59
60
  exports = () => [];
@@ -62,6 +63,6 @@ describe('Resource', () => {
62
63
  }
63
64
  }
64
65
 
65
- await expect(mount(Sneaky, null)).rejects.toThrow(/do not own/i);
66
+ await expect(mount(Sneaky)).rejects.toThrow(/do not own/i);
66
67
  });
67
68
  });
package/src/allocation.ts CHANGED
@@ -3,15 +3,18 @@ import { resources, ownership } from './state';
3
3
  import wrap from './proxy';
4
4
 
5
5
  /** Provision a resource and return its external API. */
6
- export async function mount<Api extends object, InitArgs>(
7
- Subtype: new () => Resource<Api, InitArgs>,
8
- params: InitArgs,
9
- ): Promise<Api> {
10
- const resource = new Subtype();
11
- await resource.enter(params);
12
-
13
- const api = resource.exports();
14
- const { proxy, revoke } = wrap(api);
6
+ export async function mount<
7
+ Controls extends object,
8
+ Args extends Array<unknown>,
9
+ >(
10
+ Entity: new () => Resource<Controls, Args>,
11
+ ...args: Args
12
+ ): Promise<Controls> {
13
+ const resource = new Entity();
14
+ await resource.enter(...args);
15
+
16
+ const controls = resource.exports();
17
+ const { proxy, revoke } = wrap(controls);
15
18
 
16
19
  resources.set(proxy, {
17
20
  resource,
@@ -27,12 +30,12 @@ export async function mount<Api extends object, InitArgs>(
27
30
  *
28
31
  * @todo Add type marker to catch cases where the wrong object is unmounted.
29
32
  */
30
- export async function unmount(api: object) {
31
- const entry = resources.get(api);
33
+ export async function unmount(controls: object) {
34
+ const entry = resources.get(controls);
32
35
 
33
36
  if (entry) {
34
37
  // Instantly delete to prevent race conditions.
35
- resources.delete(api);
38
+ resources.delete(controls);
36
39
 
37
40
  // Free all references.
38
41
  entry.revoke();
@@ -41,7 +44,7 @@ export async function unmount(api: object) {
41
44
  ownership.delete(entry.resource);
42
45
 
43
46
  // Recursively close out the children first...
44
- const recursiveUnmounts = children.map((api) => unmount(api));
47
+ const recursiveUnmounts = children.map((controls) => unmount(controls));
45
48
  const results = await Promise.allSettled(recursiveUnmounts);
46
49
 
47
50
  // Then close the parent.
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { default as Resource, type ExternalControls } from './resource';
1
+ export { default as Resource } from './resource';
2
2
  export { default as bindContext } from './bind-context';
3
3
  export { mount, unmount } from './allocation';
4
+ export type { Controls } from './types';
package/src/resource.ts CHANGED
@@ -7,8 +7,8 @@ import { ownership } from './state';
7
7
  * first tears down the children.
8
8
  */
9
9
  export default abstract class Resource<
10
- ExternalApi extends object,
11
- InitArgs = void,
10
+ Controls extends object,
11
+ Args extends Array<unknown> = [],
12
12
  > {
13
13
  #resources = new WeakSet<object>();
14
14
  #children: Array<object> = [];
@@ -19,7 +19,7 @@ export default abstract class Resource<
19
19
 
20
20
  /** A hook that gets called when the resource is created. */
21
21
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
22
- async enter(_params: InitArgs) {
22
+ async enter(..._args: Args) {
23
23
  return;
24
24
  }
25
25
 
@@ -29,35 +29,32 @@ export default abstract class Resource<
29
29
  }
30
30
 
31
31
  /** Provision an owned resource and make sure it doesn't outlive us. */
32
- async allocate<Api extends object, Params>(
33
- Child: new () => Resource<Api, Params>,
34
- params: Params,
35
- ): Promise<Api> {
36
- const api = await mount(Child, params);
37
- this.#resources.add(api);
38
- this.#children.push(api);
39
-
40
- return api;
32
+ async allocate<
33
+ ChildControls extends object,
34
+ ChildArgs extends Array<unknown> = [],
35
+ >(
36
+ Child: new () => Resource<ChildControls, ChildArgs>,
37
+ ...args: ChildArgs
38
+ ): Promise<ChildControls> {
39
+ const controls = await mount(Child, ...args);
40
+ this.#resources.add(controls);
41
+ this.#children.push(controls);
42
+
43
+ return controls;
41
44
  }
42
45
 
43
46
  /**
44
47
  * Tear down a resource. Happens automatically when resource owners are
45
48
  * deallocated.
46
49
  */
47
- async deallocate(api: object) {
48
- if (!this.#resources.has(api)) {
50
+ async deallocate(resource: object) {
51
+ if (!this.#resources.has(resource)) {
49
52
  throw new Error('You do not own this resource.');
50
53
  }
51
54
 
52
- return unmount(api);
55
+ return unmount(resource);
53
56
  }
54
57
 
55
58
  /** Returns an external API to the parent resource. */
56
- abstract exports(): ExternalApi;
59
+ abstract exports(): Controls;
57
60
  }
58
-
59
- /** The `exports` type for a resource. */
60
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
- export type ExternalControls<ArbitraryResource extends Resource<any, any>> =
62
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
- ArbitraryResource extends Resource<infer Api, any> ? Api : never;
package/src/state.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type Resource from './resource';
2
2
 
3
3
  interface RevokableResource {
4
- resource: Resource<object, unknown>;
4
+ resource: Resource<object, Array<unknown>>;
5
5
 
6
6
  /**
7
7
  * Destroys outer references to the API and frees the object for garbage
@@ -15,6 +15,6 @@ export const resources = new WeakMap<object, RevokableResource>();
15
15
 
16
16
  /** Maps a resource to all the other resources it provisioned. */
17
17
  export const ownership = new WeakMap<
18
- Resource<object, unknown>,
18
+ Resource<object, Array<unknown>>,
19
19
  Array<object>
20
20
  >();
package/src/types.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type Resource from './resource';
2
+
3
+ /** The `exports` type for a resource. */
4
+ export type Controls<
5
+ ArbitraryResource extends Resource<object, Array<unknown>>,
6
+ > = ArbitraryResource extends Resource<infer Controls, Array<unknown>>
7
+ ? Controls
8
+ : never;