Repository navigation
Design Meeting Notes, 6/21/2023 #54735
Description
Activity
- addedDesign NotesNotes from our design meetingsNotes from our design meetings
on Jun 21, 2023 CJS does
--moduleResolution node16 --module esnextSurely this was supposed to be
--module commonjs…No other language with a disposable concept (destructors, Drop, IDisposable, ...) has more than a single method.
If we’re classifying destructors as a kind of disposal method, then technically C# has either two or three depending on how you count 😉 (destructor +
IDisposable.Dispose, the latter of which has two overloads)--and the way they interact is not super intuitive either FWIR.No other language with a disposable concept (destructors, Drop, IDisposable, ...) has more than a single method.
If we’re classifying destructors as a kind of disposal method, then technically C# has either two or three depending on how you count 😉 (destructor +
IDisposable.Dispose, the latter of which has two overloads)--and the way they interact is not super intuitive either FWIR.C# has
Dispose()for explicit resource management, and finalizers for implicit resource management (e.g., garbage collection). JavaScript will haveSymbol.disposefor explicit resource management, andFinalizationRegistryfor implicit resource management. A combination of explicit and implicit management is useful for unmanaged handles/file descriptors to ensure they are still closed if the handle wrapper is GC'd but not disposed, and you can do the same thing in both languages:// C# class MySafeNativeHandle : IDisposable { private IntPtr unsafeHandle; public SafeNativeHandle(IntPtr unsafeHandle) { this.unsafeHandle = unsafeHandle; } // implicit resource management ~SafeNativeHandle() { this.DisposeCommon(); } // explicit resource management public Dispose() { GC.SuppressFinalize(this); this.DisposeCommon(); } private DisposeCommon() { if (this.unsafeHandle !== IntPtr.Zero) { ReleaseHandle(this.unsafeHandle); this.unsafeHandle = IntPtr.Zero; } } }
// ts class MySafeNativeHandle implements Disposable { #unregisterToken = {}; #holder: { handle: number; }; constructor(unsafeNativeHandle: number) { this.#holder = { value: unsafeHandle; }; // register for finalization MySafeNativeHandle.#finalizer.register(this, this.#holder, this.#unregisterToken); } // implicit resource management static #finalizer = new FinalizationRegistry<{ handle: number }>(holder => { MySafeNativeHandle.#disposeCommon(holder); }); // explicit resource management [Symbol.dispose]() { MySafeNativeHandle.#finalizer.unregister(this.#unregisterToken); MySafeNativeHandle.#disposeCommon(this.#holder); } static #disposeCommon(holder: { handle: number }) { if (holder.handle !== 0) { releaseHandle(holder.handle); holder.handle = 0; } } }
I seem to recall that C# has a
Dispose()overload that takes adisposingboolean parameter (which is really confusing, if I'm callingDisposethen obviously I'm disposing it!)That is for cases when you want to support subclassing. The
Dispose(bool disposing)method is essentially theDisposeCommon()method above:// C# class MySubclassableSafeNativeHandle : IDisposable { private IntPtr unsafeHandle; public MySubclassableSafeNativeHandle(IntPtr unsafeHandle) { this.unsafeHandle = unsafeHandle; } // implicit resource management ~MySubclassableSafeNativeHandle () { this.Dispose(false); } // explicit resource management public Dispose() { GC.SuppressFinalize(this); this.Dispose(true); } protected virtual Dispose(bool disposing) { if (this.unsafeHandle !== IntPtr.Zero) { ReleaseHandle(this.unsafeHandle); this.unsafeHandle = IntPtr.Zero; } } }
Subclasses can override
Dispose(bool disposing)and use thedisposingparameter to avoid resurrecting other held object references when it is invoked from a finalizer and not fromDispose(). JavaScript doesn't need this because aFinalizationRegistrycan't cause resurrection because the callback is only triggered after an object has already been GC'd and is completely unreachable.interface FinalizationRegistry<T> { register(target: object, heldValue: T, unregisterToken?: object): void; }
The
targetargument is monitored for GC, but is not held (as that would prevent GC). TheheldValueargument is held (preventing GC) so that it can be passed to the finalizer callback, but is itself not monitored. Passingthisas the "held value" would prevent GC so the finalizer callback would never execute. Similarly, passing a closure that holds ontothisas the "held value" would also prevent GC.- added 2 commits that reference this issue
on Jun 28, 2023
Restricting Mixing
moduleandmoduleResolutionwhen one isnode*#54567
moduleandmoduleResolution.moduleResolutionis somewhat straightforward; notmodule.moduleResolutionis strictly path lookups,moduledecides emit, and is driven by a localpackage.json, etc.--moduleResolution node16 --module esnext--moduleResolution node16 --module esnext--moduleResolution bundler--module esnext --moduleResolution node16?modulehas to benode16as well has no downsides.@tsconfigpackages use an invalid mix!--module node16 --moduleResolution node!!!!!package.jsonsupported multiple values for thetypesfield depending on path/directory. - but by the time Node runtimes broadly support this, it will be a bit.@tsconfigNaming of
Diposable#54505 (comment)
Disposablevs.DisposableLike.Iteratorfor iterator methods, and felt like there was some agreement in committee about placement being off; but ultimately JS can't really support extension methods, and needed an instance and constructor with a prototype so that these Iterator objects can have methods called on them.Disposablefeels... different? It's a one-and-done concept. There's no chaining.DisposableStack.Drop,IDisposable, ...) has more than a single method.