Skip to content

Add support for async disposable - #1073

Merged
cowboyd merged 1 commit into
thefrontside:v4from
joshamaju:native-disposable-resource
Feb 19, 2026
Merged

Add support for async disposable#1073
cowboyd merged 1 commit into
thefrontside:v4from
joshamaju:native-disposable-resource

Conversation

@joshamaju

@joshamajujoshamaju commented Jan 5, 2026

Copy link
Copy Markdown
Contributor

Motivation

Cleanup effection task at application/effection boundary to avoid hanging operations.

resolves#1023

JavaScript now has a native scope based mechanism for cleanup called "Explicit Resource Management"

Approach

This integrates this spec in two ways:

  1. Every Task implements the AsyncDisposable api directly.
  2. When calling createScope() the scope returned also implements AsyncDisposable interface

There is a bit of hackery that we needed to do with createScope() so that it can support both:

await using scope=createScope();

and

let[scope,destroy]=createScope();

@pkg-pr-new

pkg-pr-newBot commented Jan 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/thefrontside/effection@1073

commit: 8a0c134

@joshamajujoshamaju changed the title feat: Implement Symbol.asyncDispose on tasks to enable `await using…Add support for async disposableJan 5, 2026
Comment threadlib/types.ts Outdated
Comment threadlib/types.ts
Comment threadlib/task.ts Outdated
Comment threadtest/run.test.ts Outdated
Comment threadtest/run.test.ts
@joshamaju

Copy link
Copy Markdown
ContributorAuthor

How do we move this forward?

cowboyd
cowboyd previously requested changes Jan 9, 2026

@cowboydcowboyd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, let's push this over the finish line.

  1. Formatting changes: don't worry about it, I'll fix them up.
  2. Question about the types
  3. We should also add asyncDispose to Scope (along with a test)

Comment threadlib/types.ts Outdated
@joshamaju

Copy link
Copy Markdown
ContributorAuthor

Please review the scope feature

Comment threadlib/types.ts
@cowboyd

Copy link
Copy Markdown
Member

@joshamaju I removed the formatting changes, and moved the scope implementation into ScopeInternal.

The only remaining issue is documentation. I'm not sure how much is required for asyncDispose, and perhaps not at all.

Comment threadlib/scope-internal.ts Outdated
@cowboyd
cowboyd self-requested a review January 15, 2026 16:09
@cowboyd
cowboyd dismissed their stale reviewJanuary 15, 2026 16:10

Issues from initial review have been addressed

@taras

Copy link
Copy Markdown
Member

We should add it to resource page to say that they're supported out of the box with this version.

@joshamaju

Copy link
Copy Markdown
ContributorAuthor

I've added a section to the upgrade guide documentation

@cowboyd

Copy link
Copy Markdown
Member

@taras I'm not sure the resources page isn't really the right place because resources are implicitly cleaned up. Only tasks and scopes implement AsyncDisposable, but not sure where the place to explain that is. The section on typescript maybe? Rosetta Stone?

@cowboyd

Copy link
Copy Markdown
Member

@joshamaju What do you think about making the Array returned by createScope() actually implement the AsyncDisposable. This would allow us to say:

using [scope]=createScope();

@joshamaju

Copy link
Copy Markdown
ContributorAuthor

Is that even possible? I don't think it's possible

@joshamaju

joshamaju commented Feb 3, 2026

Copy link
Copy Markdown
ContributorAuthor

So I just checked. Can only be used like this using scope = createScope();, and cannot be async, only Symbol.dispose.

@cowboyd

Copy link
Copy Markdown
Member

Yeah, it's not possible to use destructing assignment with explicitly managed resources.... another reason I don't like them. I had an AI tell me it was though, and so I got my hopes up. 🤣🤣🤣

I just think it will be awkward to use without being able to do it with a one-liner. Maybe we need a new method?

using scope=disposableScope();
using scope=createDisposableScope();

something like those or another alternative? ☝🏻

@joshamaju

Copy link
Copy Markdown
ContributorAuthor

Maybe createScope should return a object instead of a tuple?

@cowboyd

Copy link
Copy Markdown
Member

I don't think that would work either, would it? We could make the object Iterable for backwards compatibility if it did.

@joshamaju

Copy link
Copy Markdown
ContributorAuthor

Yep, it works

@joshamaju

Copy link
Copy Markdown
ContributorAuthor

Should it be a separate function i.e createDisposableScope?

@cowboyd

Copy link
Copy Markdown
Member

So we could define a scope property on the tuple?

@joshamaju

Copy link
Copy Markdown
ContributorAuthor

How do you mean?

@cowboyd

cowboyd commented Feb 4, 2026

Copy link
Copy Markdown
Member
exportfunctioncreateScope(parent: Scope=global,): [Scope,()=>Future<void>]{let[scope,destroy]=createScopeInternal(parent);lettuple=[scope,()=>parent.run(destroy)];letdisposable=Object.defineProperty(scope,Symbol.asyncDispose,{value: ()=>parent.run(destroy)});Object.defineProperty(tuple,'scope',{value: disposable});}

This would then let us say:

using { scope }=createScope();

@cowboyd

cowboyd commented Feb 4, 2026

Copy link
Copy Markdown
Member

Yeah, it's like I thought. Binding expressions are not supported inside using declarations microsoft/TypeScript#55527

It's yet another example of how explicit resource management was railroaded through without concern over how much incongruence with existing patterns it would introduce.

@cowboyd

Copy link
Copy Markdown
Member

I think we can still do it, but it means we really have to abuse the runtime and the type system to make it work. I think what we can do is define Symbol.iteratorandSymbol.asyncDispose on the actual returned scope so that it can be used as a tuple and as an explicitly managed resource:

// 🤣 🤣 declarefunctioncreateScope(parent?: Scope): Scope&AsyncDisposable&[Scope,()=>Promise<void>];

@cowboyd

Copy link
Copy Markdown
Member

This is horrendous, but it works:

exportfunctioncreateScope(parent: Scope=global,): Scope&AsyncDisposable&[Scope,()=>Future<void>]{let[scope,destroy]=createScopeInternal(parent);letdispose=()=>parent.run(destroy);lettuple=[scope,dispose];Object.defineProperty(scope,Symbol.iterator,{enumerable: false,value: tuple[Symbol.iterator].bind(tuple),});Object.defineProperty(scope,Symbol.asyncDispose,{enumerable: false,value: dispose,})returnscopeasunknownasScope&AsyncDisposable&[Scope,()=>Future<void>];}

@joshamaju

Copy link
Copy Markdown
ContributorAuthor

Is the goal that a user can use it in one of the following ways?

await using scope=createScope();

or like this

const[scope]=createScope();

@cowboyd

Copy link
Copy Markdown
Member

@joshamaju yes, that's the idea, although the second way is a no-no. You should always capture the destroy() function if you use it the second way.

It is a bit disgusting, but it let's you consume it either way.

@joshamaju

Copy link
Copy Markdown
ContributorAuthor

@cowboyd I've added the update.

I'm soo going to abuse tuple[Symbol.iterator].bind(tuple) in my own code 😅.

Comment threadlib/scope.ts Outdated
Comment threadlib/scope.ts Outdated
@joshamajujoshamaju reopened this Feb 9, 2026
@joshamaju

Copy link
Copy Markdown
ContributorAuthor

I've applied the changes as discussed

@cowboyd
cowboydforce-pushed the native-disposable-resource branch from efb2591 to 9ffb088CompareFebruary 10, 2026 20:53
@cowboyd

Copy link
Copy Markdown
Member

@joshamaju I rebased on v4, squashed all the commits, fixed the formatting and added documentation to the repo as well as the PR. Anything else we should cover?

@tarastaras mentioned this pull request Feb 12, 2026
JavaScript now has a native scope based mechanism for cleanup called
"Explicit Resource Management"
This integrates this spec in two ways:
1. Every Task implements the AsyncDisposable api directly.
2. When calling `createScope()` the scope returned also implements
AsyncDisposable interface.
There is a bit of hackery that we needed to do with `createScope()` so
that it can support both:
```ts
await using scope = createScope();
```
and
```ts
let [scope, destroy] = createScope();
```
@cowboyd
cowboydforce-pushed the native-disposable-resource branch from 9ffb088 to 8a0c134CompareFebruary 13, 2026 16:07
@joshamaju

Copy link
Copy Markdown
ContributorAuthor

I should push updates with the using function

@cowboyd
cowboydforce-pushed the native-disposable-resource branch from f08cf53 to 8a0c134CompareFebruary 18, 2026 19:58

@cowboydcowboyd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to go! 🚀

@cowboyd
cowboyd merged commit 58b81b4 into thefrontside:v4Feb 19, 2026
19 checks passed
@tarastaras added this to the v4.1 milestone Feb 26, 2026
@cowboydcowboyd mentioned this pull request Jul 8, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make Task implement explicitly managed resource API

3 participants

@joshamaju@cowboyd@taras