├── .gitignore ├── .vscode └── settings.json ├── scripts └── build_npm.ts ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── mod.ts └── mod.test.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /npm 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": false 5 | } 6 | -------------------------------------------------------------------------------- /scripts/build_npm.ts: -------------------------------------------------------------------------------- 1 | import { build, emptyDir } from "https://deno.land/x/dnt@0.32.1/mod.ts"; 2 | 3 | await emptyDir("./npm"); 4 | 5 | await build({ 6 | entryPoints: ["./mod.ts"], 7 | outDir: "./npm", 8 | shims: { 9 | deno: "dev", 10 | }, 11 | package: { 12 | name: "using-statement", 13 | version: Deno.args[0], 14 | description: `"using statement" in JavaScript and TypeScript.`, 15 | repository: { 16 | "type": "git", 17 | "url": "git+https://github.com/dsherret/using-statement.git", 18 | }, 19 | keywords: [ 20 | "using", 21 | "statement", 22 | ], 23 | author: "David Sherret", 24 | license: "MIT", 25 | bugs: { 26 | url: "https://github.com/dsherret/using-statement/issues", 27 | }, 28 | homepage: "https://github.com/dsherret/using-statement#readme", 29 | }, 30 | }); 31 | 32 | // post build steps 33 | Deno.copyFileSync("LICENSE", "npm/LICENSE"); 34 | Deno.copyFileSync("README.md", "npm/README.md"); 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | test-library: 5 | name: CI 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: denoland/setup-deno@v1 11 | with: 12 | deno-version: v1.x 13 | - name: Lint 14 | run: deno lint 15 | - name: Format 16 | run: deno fmt --check 17 | - name: Run tests 18 | run: deno test 19 | 20 | - name: Get tag version 21 | if: startsWith(github.ref, 'refs/tags/') 22 | id: get_tag_version 23 | run: echo ::set-output name=TAG_VERSION::${GITHUB_REF/refs\/tags\//} 24 | - uses: actions/setup-node@v2 25 | with: 26 | node-version: '16.x' 27 | registry-url: 'https://registry.npmjs.org' 28 | - name: npm build and test 29 | run: deno run -A ./scripts/build_npm.ts ${{steps.get_tag_version.outputs.TAG_VERSION}} 30 | - name: npm publish 31 | if: startsWith(github.ref, 'refs/tags/') 32 | env: 33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | run: cd npm && npm publish 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2022 David Sherret 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # using_statement 2 | 3 | [![npm version](https://badge.fury.io/js/using-statement.svg)](https://badge.fury.io/js/using-statement) 4 | [![CI](https://github.com/dsherret/using-statement/workflows/CI/badge.svg)](https://github.com/dsherret/using_statement/actions?query=workflow%3ACI) 5 | [![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/using_statement/mod.ts) 6 | 7 | Function call that acts like a 8 | [using statement](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement). 9 | 10 | With Deno: 11 | 12 | ```ts 13 | import { using } from "https://deno.land/x/using_statement/mod.ts"; 14 | ``` 15 | 16 | Or with Node: 17 | 18 | ``` 19 | npm install --save using-statement 20 | ``` 21 | 22 | ## Example 23 | 24 | Before: 25 | 26 | ```ts 27 | const camera = new Camera(); 28 | try { 29 | outputPicture(camera.takePictureSync()); 30 | } finally { 31 | camera.dispose(); 32 | } 33 | ``` 34 | 35 | After: 36 | 37 | ```ts 38 | import { using } from "https://deno.land/x/using_statement/mod.ts"; 39 | 40 | using(new Camera(), (camera) => { 41 | outputPicture(camera.takePictureSync()); 42 | }); 43 | ``` 44 | 45 | ## Features 46 | 47 | - Supports synchronous, asynchronous, and generator functions. 48 | - Handles exceptions to ensure the resource is properly disposed. 49 | - Accepts objects with a `dispose()`, `close()`, or `unsubscribe()` method. 50 | - Allows asynchronously disposing when using a synchronous or asynchronous 51 | function. 52 | 53 | ## Examples 54 | 55 | Setup: 56 | 57 | ```ts 58 | // Camera.ts 59 | export class Camera { 60 | takePictureSync() { 61 | // ...etc... 62 | return pictureData; 63 | } 64 | 65 | async takePicture() { 66 | // ...etc... 67 | return pictureData; 68 | } 69 | 70 | dispose() { 71 | // clean up the resource this class is holding 72 | } 73 | } 74 | ``` 75 | 76 | Synchronous example: 77 | 78 | ```ts 79 | import { using } from "https://deno.land/x/using_statement/mod.ts"; 80 | import { Camera } from "./Camera.ts"; 81 | 82 | using(new Camera(), (camera) => { 83 | const picture = camera.takePictureSync(); 84 | outputPicture(picture); // some function that outputs the picture 85 | }); 86 | 87 | // camera is disposed here 88 | ``` 89 | 90 | Asynchronous example: 91 | 92 | ```ts 93 | import { using } from "https://deno.land/x/using_statement/mod.ts"; 94 | import { Camera } from "./Camera.ts"; 95 | 96 | await using(new Camera(), async (camera) => { 97 | const picture = await camera.takePicture(); 98 | outputPicture(picture); 99 | }); 100 | 101 | // camera is disposed here 102 | ``` 103 | 104 | Generator function example: 105 | 106 | ```ts 107 | import { using } from "https://deno.land/x/using_statement/mod.ts"; 108 | import { Camera } from "./Camera.ts"; 109 | 110 | const picturesIterator = using(new Camera(), function* (camera) { 111 | for (let i = 0; i < 10; i++) { 112 | yield camera.takePictureSync(); 113 | } 114 | }); 115 | 116 | // camera is not disposed yet... 117 | 118 | for (const picture of picturesIterator) { 119 | outputPicture(picture); 120 | } 121 | 122 | // camera is now disposed 123 | ``` 124 | 125 | ### Inspiration 126 | 127 | - C#'s 128 | [using statement](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement). 129 | - My old gist [here](https://gist.github.com/dsherret/cf5d6bec3d0f791cef00). 130 | - [ECMAScript using statement proposal](https://github.com/tc39/proposal-using-statement). 131 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export interface Disposable { 2 | dispose(): void; 3 | } 4 | 5 | export interface AsyncDisposable { 6 | dispose(): Promise; 7 | } 8 | 9 | type UsingObject = Disposable | { close(): void } | { unsubscribe(): void }; 10 | type AsyncUsingObject = AsyncDisposable | { close(): Promise } | { 11 | unsubscribe(): Promise; 12 | }; 13 | 14 | export function using< 15 | TDisposable extends UsingObject | AsyncUsingObject, 16 | TResult = void, 17 | >( 18 | resource: TDisposable, 19 | func: (resource: TDisposable) => Promise, 20 | ): Promise; 21 | export function using( 22 | resource: TDisposable, 23 | func: (resource: TDisposable) => IterableIterator, 24 | ): IterableIterator; 25 | export function using( 26 | resource: TDisposable, 27 | func: (resource: TDisposable) => TResult, 28 | ): Promise; 29 | export function using( 30 | resource: TDisposable, 31 | func: (resource: TDisposable) => TResult, 32 | ): TResult; 33 | export function using< 34 | TDisposable extends UsingObject | AsyncUsingObject, 35 | TIteratorItem, 36 | TResult, 37 | >( 38 | resource: TDisposable, 39 | func: ( 40 | resource: TDisposable, 41 | ) => TResult | Promise | IterableIterator, 42 | ): TResult | Promise | IterableIterator { 43 | let shouldDispose = true; 44 | let result: 45 | | TResult 46 | | Promise 47 | | IterableIterator 48 | | undefined = undefined; 49 | try { 50 | result = func(resource); 51 | 52 | // dispose it asynchronously if it returns a promise 53 | if (isPromise(result)) { 54 | const capturedResult = result; 55 | shouldDispose = false; 56 | return result.finally(() => dispose(resource)).then(() => capturedResult); 57 | } else if (isIterator(result)) { 58 | shouldDispose = false; 59 | const originalNext = result.next!; 60 | result.next = function () { 61 | let shouldDispose = false; 62 | try { 63 | const args = Array.from(arguments); 64 | // deno-lint-ignore no-explicit-any 65 | const iterationResult = originalNext.apply(this, args as any); 66 | if (iterationResult.done) { 67 | shouldDispose = true; 68 | } 69 | return iterationResult; 70 | } catch (err) { 71 | shouldDispose = true; 72 | throw err; 73 | } finally { 74 | if (shouldDispose) { 75 | dispose(resource); 76 | } 77 | } 78 | }; 79 | } 80 | } finally { 81 | if (shouldDispose) { 82 | const disposeResult = dispose(resource); 83 | if (isPromise(disposeResult)) { 84 | const finalPromise = result == null 85 | ? undefined 86 | : Promise.resolve(result as TResult); 87 | if (finalPromise == null) { 88 | result = disposeResult; 89 | } else { 90 | result = disposeResult.then(() => finalPromise!); 91 | } 92 | } 93 | } 94 | } 95 | 96 | return result!; 97 | } 98 | 99 | const funcNames = ["dispose", "close", "unsubscribe"]; 100 | function dispose(obj: UsingObject | undefined): void | Promise { 101 | if (obj == null) { 102 | return; 103 | } 104 | 105 | for (const funcName of funcNames) { 106 | // deno-lint-ignore no-explicit-any 107 | if (typeof (obj as any)[funcName] === "function") { 108 | // deno-lint-ignore no-explicit-any 109 | return (obj as any)[funcName](); 110 | } 111 | } 112 | 113 | throw new Error("Object provided to using did not have a dispose method."); 114 | } 115 | 116 | function isPromise(obj: unknown): obj is Promise { 117 | return obj != null && 118 | // deno-lint-ignore no-explicit-any 119 | typeof (obj as any).then === "function" && 120 | // deno-lint-ignore no-explicit-any 121 | typeof (obj as any).finally === "function"; 122 | } 123 | 124 | function isIterator(obj: unknown): obj is Iterator { 125 | return obj != null && 126 | // deno-lint-ignore no-explicit-any 127 | typeof (obj as any).next === "function"; 128 | } 129 | -------------------------------------------------------------------------------- /mod.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertThrows, 4 | } from "https://deno.land/std@0.121.0/testing/asserts.ts"; 5 | import { 6 | assert, 7 | IsExact, 8 | } from "https://deno.land/x/conditional_type_checks@1.0.5/mod.ts"; 9 | import { using } from "./mod.ts"; 10 | 11 | class Disposable { 12 | isDisposed = false; 13 | 14 | dispose() { 15 | this.isDisposed = true; 16 | } 17 | } 18 | 19 | class AsyncDisposable { 20 | isDisposed = false; 21 | 22 | dispose() { 23 | return new Promise((resolve) => { 24 | setTimeout(() => { 25 | this.isDisposed = true; 26 | resolve(); 27 | }, 0); 28 | }); 29 | } 30 | } 31 | 32 | Deno.test("using - sync - should dispose the resource", () => { 33 | const disposable = new Disposable(); 34 | using(disposable, () => { 35 | assertEquals(disposable.isDisposed, false); 36 | }); 37 | 38 | assertEquals(disposable.isDisposed, true); 39 | }); 40 | 41 | Deno.test("using - sync - should dispose the resource when an exception occurs inside the function", () => { 42 | const disposable = new Disposable(); 43 | try { 44 | using(disposable, () => { 45 | throw new Error(); 46 | }); 47 | } catch { 48 | // do nothing 49 | } 50 | 51 | assertEquals(disposable.isDisposed, true); 52 | }); 53 | 54 | Deno.test("using - sync - should return the returned value", () => { 55 | const disposable = new Disposable(); 56 | const result = using(disposable, () => { 57 | return 5; 58 | }); 59 | 60 | assertEquals(result, 5); 61 | }); 62 | 63 | Deno.test("using - sync - should dispose the resource asynchronously", async () => { 64 | const disposable = new AsyncDisposable(); 65 | const result = using(disposable, () => { 66 | assertEquals(disposable.isDisposed, false); 67 | }); 68 | 69 | assertEquals(disposable.isDisposed, false); 70 | await result; 71 | assertEquals(disposable.isDisposed, true); 72 | }); 73 | 74 | Deno.test("using - sync - should dispose the resource asynchronously and return the result in the promise", async () => { 75 | const disposable = new AsyncDisposable(); 76 | const promise = using(disposable, () => { 77 | return 5; 78 | }); 79 | 80 | assert>>(true); 81 | const result = await promise; 82 | assertEquals(result, 5); 83 | }); 84 | 85 | const methodNames = ["dispose", "close", "unsubscribe"]; 86 | for (const methodName of methodNames) { 87 | Deno.test(`should dispose using #${methodName}()`, () => { 88 | let isDisposed = false; 89 | const obj = { 90 | [methodName]() { 91 | isDisposed = true; 92 | }, 93 | }; 94 | // deno-lint-ignore no-explicit-any 95 | using(obj as any, () => { 96 | assertEquals(isDisposed, false); 97 | }); 98 | 99 | assertEquals(isDisposed, true); 100 | }); 101 | } 102 | 103 | Deno.test("using - sync - should throw when providing an object that's not supported", () => { 104 | // deno-lint-ignore no-explicit-any 105 | assertThrows(() => using({} as any, () => {})); 106 | }); 107 | 108 | Deno.test("using - sync - should not error if providing undefined", () => { 109 | let entered = false; 110 | 111 | // deno-lint-ignore no-explicit-any 112 | using(undefined as any, () => { 113 | entered = true; 114 | }); 115 | 116 | assertEquals(entered, true); 117 | }); 118 | 119 | Deno.test("using - sync - should not error if providing null", () => { 120 | let entered = false; 121 | 122 | // deno-lint-ignore no-explicit-any 123 | using(undefined as any, () => { 124 | entered = true; 125 | }); 126 | 127 | assertEquals(entered, true); 128 | }); 129 | 130 | Deno.test("using - async - should dispose the resource", async () => { 131 | const disposable = new Disposable(); 132 | const result = using(disposable, () => { 133 | assertEquals(disposable.isDisposed, false); 134 | return new Promise((resolve) => { 135 | assertEquals(disposable.isDisposed, false); 136 | resolve(); 137 | }); 138 | }); 139 | 140 | assertEquals(disposable.isDisposed, false); 141 | await result; 142 | assertEquals(disposable.isDisposed, true); 143 | }); 144 | 145 | Deno.test("using - async - should handle disposing when the promise is rejected", async () => { 146 | const disposable = new Disposable(); 147 | try { 148 | await using(disposable, () => { 149 | return Promise.reject(new Error()); 150 | }); 151 | } catch { 152 | // do nothing; 153 | } 154 | 155 | assertEquals(disposable.isDisposed, true); 156 | }); 157 | 158 | Deno.test("using - async - should get the returned value", async () => { 159 | const disposable = new Disposable(); 160 | const promise = using(disposable, () => { 161 | return new Promise((resolve) => { 162 | assertEquals(disposable.isDisposed, false); 163 | resolve(5); 164 | }); 165 | }); 166 | 167 | assert>>(true); 168 | const result = await promise; 169 | assertEquals(result, 5); 170 | }); 171 | 172 | Deno.test("using - async - should dispose the resource asynchronously", async () => { 173 | const disposable = new AsyncDisposable(); 174 | const result = using(disposable, () => { 175 | return new Promise((resolve) => resolve()); 176 | }); 177 | 178 | assertEquals(disposable.isDisposed, false); 179 | await result; 180 | assertEquals(disposable.isDisposed, true); 181 | }); 182 | 183 | Deno.test("using - async - should get the returned value when disposing asynchronously", async () => { 184 | const disposable = new AsyncDisposable(); 185 | const promise = using(disposable, () => { 186 | return new Promise((resolve) => { 187 | assertEquals(disposable.isDisposed, false); 188 | resolve(5); 189 | }); 190 | }); 191 | 192 | assert>>(true); 193 | const result = await promise; 194 | assertEquals(result, 5); 195 | }); 196 | 197 | Deno.test("iterator - should handle disposing after done with an iterator", () => { 198 | const disposable = new Disposable(); 199 | const result = using(disposable, function* () { 200 | yield 0; 201 | assertEquals(disposable.isDisposed, false); 202 | yield 1; 203 | assertEquals(disposable.isDisposed, false); 204 | }); 205 | 206 | let value = 0; 207 | for (const item of result) { 208 | assertEquals(disposable.isDisposed, false); 209 | assertEquals(item, value); 210 | value++; 211 | } 212 | 213 | assertEquals(disposable.isDisposed, true); 214 | assertEquals(value, 2); 215 | }); 216 | 217 | Deno.test("iterator - should handle disposing when an exception is thrown", () => { 218 | const disposable = new Disposable(); 219 | const result = using(disposable, function* () { 220 | yield 0; 221 | throw new Error(); 222 | }); 223 | 224 | let value = 0; 225 | try { 226 | for (const item of result) { 227 | assertEquals(item, value); 228 | value++; 229 | } 230 | } catch { 231 | // ignore 232 | } 233 | 234 | assertEquals(disposable.isDisposed, true); 235 | assertEquals(value, 1); 236 | }); 237 | --------------------------------------------------------------------------------