Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add definitions for WeakRef and FinalizationRegistry #38232

Merged
merged 5 commits into from
Oct 6, 2020
Merged

Add definitions for WeakRef and FinalizationRegistry #38232

merged 5 commits into from
Oct 6, 2020

Conversation

ikokostya
Copy link
Contributor

@msftclas
Copy link

msftclas commented Apr 28, 2020

CLA assistant check
All CLA requirements met.

@orta
Copy link
Contributor

orta commented May 12, 2020

Great, this looks good to me:

  • the proposal is at stage 3, so it's good to be merged
  • the JSDoc annotations match the reference (nice work there)
  • It doesn't look like anything is missing

The file naming fits too, thanks 👍

@orta
Copy link
Contributor

orta commented May 12, 2020

I'll give Nathan a chance to look over it, but I'm good to merge

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

Questions about the type of target and unregisterToken:

  1. Maybe they should be unknown or {} instead.
  2. The proposal uses 'value' and 'object' fairly consistently, but doesn't define them. Does the ecmascript spec do so?

* @param callback If given, the registry uses the given callback instead of the one it was
* created with.
*/
cleanupSome(callback?: (heldValue: any) => void): void;
Copy link
Member

Choose a reason for hiding this comment

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

it's weird that heldValue: any but target: object and unregisterToken: object. I know the proposal uses the terms 'value' and 'object' but I don't think they exactly match the type system's definitions. In particular, object means "non-primitive". Can you provide null, undefined, "foo", false or 3 for target or unregisterToken?

Copy link
Contributor Author

@ikokostya ikokostya May 12, 2020

Choose a reason for hiding this comment

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

  1. target must be an object, i.e. non-primitive type.
    Specification: https://tc39.es/proposal-weakrefs/#sec-weak-ref-target

    1. If Type(target) is not Object, throw a TypeError exception.

    V8 implementation behavior:

    $ node --harmony-weak-refs
    Welcome to Node.js v14.0.0.
    Type ".help" for more information.
    > new WeakRef(null)
    Uncaught TypeError: WeakRef: target must be an object
        at new WeakRef (<anonymous>)
    > new WeakRef(undefined)
    Uncaught TypeError: WeakRef: target must be an object
        at new WeakRef (<anonymous>)
    > new WeakRef(1)
    Uncaught TypeError: WeakRef: target must be an object
        at new WeakRef (<anonymous>)
    > new WeakRef("str")
    Uncaught TypeError: WeakRef: target must be an object
        at new WeakRef (<anonymous>)
    
  2. unregisterToken must be an object (non-primitive type), or undefined.

    Specification: https://tc39.es/proposal-weakrefs/#sec-finalization-registry.prototype.register

    1. If Type(unregisterToken) is not Object,
      a. If unregisterToken is not undefined, throw a TypeError exception.
      b. Set unregisterToken to empty.

    V8 implementation behavior (please note that v8 in Node.js still uses old specification name FinalizationGroup instead of FinalizationRegistry):

    $ node --harmony-weak-refs
    > var fg = new FinalizationGroup(() => {})
    undefined
    > fg.register({}, null, null)
    Uncaught TypeError: unregisterToken ('null') must be an object
        at FinalizationGroup.register (<anonymous>)
    > fg.register({}, null, undefined)
    undefined
    > fg.register({}, null, 1)
    Uncaught TypeError: unregisterToken ('1') must be an object
        at FinalizationGroup.register (<anonymous>)
    > fg.register({}, null, 'a')
    Uncaught TypeError: unregisterToken ('a') must be an object
        at FinalizationGroup.register (<anonymous>)
    > fg.register({}, null, false)
    Uncaught TypeError: unregisterToken ('false') must be an object
        at FinalizationGroup.register (<anonymous>)
    
  3. heldValue can have any type.

    Specification: https://tc39.es/proposal-weakrefs/#sec-finalization-registry.prototype.register
    V8 implementation behavior:

    $ node --harmony-weak-refs
    > var fg = new FinalizationGroup(() => {})
    undefined
    > fg.register({}, null)
    undefined
    > fg.register({}, false)
    undefined
    > fg.register({}, 1)
    undefined
    > fg.register({}, {})
    undefined
    

Choose a reason for hiding this comment

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

I'd suggest omitting cleanupSome from .d.ts, or otherwise somehow mark it as dispreferred/not tab-complete it. It's "normative optional" in the specification, it's currently under some debate, and it's not going to be part of what V8/Chrome ships initially. See whatwg/html#5446 for context.

Copy link
Contributor Author

@ikokostya ikokostya May 13, 2020

Choose a reason for hiding this comment

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

If it's "normative optional" in the specification, it can be marked as optional in .d.ts. Anyway this declaration is available only under --lib esnext compiler flag, which means that it isn't stable. If in final stage of the proposal this method will be removed, corresponding TypeScript declaration should reflect this change before move to stable es2020 lib.

Copy link
Member

Choose a reason for hiding this comment

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

If Chrome's not going to ship with it initially, let's make this optional to start so that people have to prove that it's available before using it. (Only when strict: true of course.)

Suggested change
cleanupSome(callback?: (heldValue: any) => void): void;
cleanupSome?(callback?: (heldValue: any) => void): void;

@ikokostya ikokostya requested a review from sandersn May 12, 2020 16:12
@ikokostya
Copy link
Contributor Author

Actually this behavior is the same as in WeakMap, where key must be an object:

interface WeakMap<K extends object, V> {

@sandersn sandersn added the For Backlog Bug PRs that fix a backlog bug label May 20, 2020
* @param callback If given, the registry uses the given callback instead of the one it was
* created with.
*/
cleanupSome(callback?: (heldValue: any) => void): void;
Copy link
Member

Choose a reason for hiding this comment

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

If Chrome's not going to ship with it initially, let's make this optional to start so that people have to prove that it's available before using it. (Only when strict: true of course.)

Suggested change
cleanupSome(callback?: (heldValue: any) => void): void;
cleanupSome?(callback?: (heldValue: any) => void): void;

@Jamesernator
Copy link

Jamesernator commented Jul 22, 2020

Is there any reason FinalizationRegistry isn't generic? I've used it a bit on the older version in Node (with --harmony-weakrefs) and I generally found the typechecking was helpful when I defined the types.

@littledan
Copy link

cleanupSome has been moved into a separate Stage 2 proposal, so I recommend omitting it from the typings here.

@willstott101
Copy link

Is there any reason FinalizationRegistry isn't generic? I've used it a bit on the older version in Node (with --harmony-weakrefs) and I generally found the typechecking was helpful when I defined the types.

What would you include in the Generic? target, heldValue, unregisterToken or multiple?

  • I suppose target for typedocs so you know what it's intended to be holding references to, though I don't think this would help much with correctness.
  • heldValue to be type-safe between your callback and register calls.
  • unregisterToken to be type-safe for all register and unregister calls.

I see a lot more value in the latter two, but in many cases you might only use one or the other. It doesn't seem obvious to me what the Generic implementation would be.

@Jamesernator
Copy link

What would you include in the Generic? target, heldValue, unregisterToken or multiple?

I've generally defined it with all 3 as:

declare global {
     class FinalizationRegistry<
         TValue extends object=object,
         THeldValue=any, 
         TUnregisterToken extends object=never
     > {
        constructor(
           finalizationCallback: (heldValue: THeldValue) => void,
        );
        register(
            object: TValue,
            heldValue: THeldValue,
            unregisterToken?: TUnregisterToken,
        ): void;
        unregister(unregisterToken: TUnregisterToken): void;
    }
}

I do agree you'd often only use some, it's a bit of a shame there aren't named generics. There are hacky ways around it (like this) but this isn't an existing convention in the TS standard lib.

type FinalizationOptions = {
    Value: object,
    HeldValue: any,
    UnregisterToken: object,
}

type Get<T extends object, K, Else> = K extends keyof T ? T[K] : Else;

declare class FinalizationRegistry<T extends Partial<FinalizationOptions>> {
    constructor(
        finalizationCallback: (heldValue: Get<T, 'HeldValue', any>) => void,
    );
    register(
        value: Get<T, 'Value', object>,
        heldValue: Get<T, 'HeldValue', any>,
        unregisterToken?: Get<T, 'UnregisterToken', never>,
    ): void;
    unregister(
        unregisterToken: Get<T, 'UnregisterToken', never>,
    ): void;
}

const f = new FinalizationRegistry<{ HeldValue: number }>((i /* number */) => {
    console.log(i);
});

f.register({} /* object */, 3 /* number */);

deref(): T | undefined;
}

interface WeakRefConstructor {
Copy link

Choose a reason for hiding this comment

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

Hi, any chance to expedite this change?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it got lost in my email, but I think it's good to go after I re-run the github CI.

@sandersn sandersn merged commit bd1d8e5 into microsoft:master Oct 6, 2020
sandersn added a commit that referenced this pull request Oct 7, 2020
sandersn added a commit that referenced this pull request Oct 7, 2020
@KilianKilmister
Copy link

KilianKilmister commented Oct 8, 2020

Funny coincidence, i just started using weak refs/Finalisation registry in a project yesterday and was little annoyed that it wasn't typed (the typescript nightly i had installed was a few days old). Perfect timing.

Two notes on FinalisationRegistry:
In FinalizationRegistry.prototype.register it looks like heldValue is optional (tested in node-v14.13.0). While i didn't find a anything specific calling it optional in the proposal, on MDN at least it is stated that undefined is an acceptable value for heldValue

As for Generics, this is what i used:

  • T extends object => so it can be integrated nicely with a collection
  • V = void => this way a type can be declared in the constructor argument and TypeScript knows that the second
    parameter of FinalizationRegistry.prototype.register is only needed if it has a non-default type (if V defaults to
    undefined, TypeScript will still want that second argument as a literal and throw a compiler error otherwise:
    finalisationRegistry.register(someObject, undefined) )
  • Token extends object = object => defaulting to object since it is optional and needs a default since it's following a
    another defaulting generic
declare class FinalizationRegistry<T extends object, V = void, Token extends object = object> {
  constructor (callback: (heldValue: V) => void)
  register (target: T, heldValue: V, token?: Token): void
  unregister (token: Token): void
}

usage:

const registry = new FinalizationRegistry<Record<string, string>>(() => console.log('some cleanup optimisation'))
// => `FinalizationRegistry<Record<string, string>, void, object>`
const registry = new FinalizationRegistry<readonly string[], number, readonly string[]>((heldItem: number) => console.log(`Array of ${heldItem} hasn't been unregistered`'))
const arr = [1,2,3,4] as const
registry.register(arr, arr.length, arr)
class Collection<T extends object, Held = void> {
  #registry: FinalizationRegistry<T, Held, this>
  constructor (callback: (heldValue: Held) => void, iterable?: Iterable<[T, Held]>) {
    this.#registry = new (FinalizationRegistry)(callback)
    for (const [item, held] of iterable ?? []) this.#registry.register(item, held, this)
  }

  register (item: T, heldValue: Held) { this.#registry.register(item, heldValue, this) }
  reset () { this.#registry.unregister(this) }
}

How @Jamesernator did the Token extends object = never is interesting, tho. migth me a good choice too, but for the other two i think this way is the best.

* Creates a WeakRef instance for the given target object.
* @param target The target object for the WeakRef instance.
*/
new<T extends object>(target?: T): WeakRef<T>;
Copy link
Contributor

@ExE-Boss ExE-Boss Jan 10, 2021

Choose a reason for hiding this comment

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

It’s invalid to call WeakRef with undefined:

Suggested change
new<T extends object>(target?: T): WeakRef<T>;
new<T extends object>(target: T): WeakRef<T>;

I’m fixing this in #42274.

Copy link
Contributor

@ExE-Boss ExE-Boss left a comment

Choose a reason for hiding this comment

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

FinalizationRegistry should be generic.


I’m fixing this in #42274.


declare var WeakRef: WeakRefConstructor;

interface FinalizationRegistry {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
interface FinalizationRegistry {
interface FinalizationRegistry<T> {

* object. If provided (and not undefined), this must be an object. If not provided, the target
* cannot be unregistered.
*/
register(target: object, heldValue: any, unregisterToken?: object): void;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
register(target: object, heldValue: any, unregisterToken?: object): void;
register(target: object, heldValue: T, unregisterToken?: object): void;

}

interface FinalizationRegistryConstructor {
readonly prototype: FinalizationRegistry;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
readonly prototype: FinalizationRegistry;
readonly prototype: FinalizationRegistry<any>;

* Creates a finalization registry with an associated cleanup callback
* @param cleanupCallback The callback to call after an object in the registry has been reclaimed.
*/
new(cleanupCallback: (heldValue: any) => void): FinalizationRegistry;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
new(cleanupCallback: (heldValue: any) => void): FinalizationRegistry;
new<T>(cleanupCallback: (heldValue: T) => void): FinalizationRegistry<T>;

This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Suport for WeakRef / FinalizationGroup
10 participants