Skip to content

Thoughts on this more ergonomic way to wire up the owner + destroyable association? #905

Open
@NullVoxPopuli

Description

@NullVoxPopuli

Problem

there is a lot of boilerplate for creating vanilla classes that are properly wired up to the container.
For example:

import { cached } from '@glimmer/tracking';
import { getOwner, setOwner } from '@ember/owner';
import { associateDestroyableChild } from '@ember/destroyable';

class MyClass { /* ... */ }

export default class Demo extends Component {
  @cached 
  get myInstance() {
    let instance = new MyClass();
    
    associateDestroyableChild(this, instance);
    
    let owner = getOwner(this);

    if (owner) {
      setOwner(instance, owner);
    }

    return instance;
  }
}

Solution

this would be a new feature to ember in my ideal world, and not in a user-land package (even though it could live there)

Usage:

import { link } from '@ember/owner'; // proposed;

class MyClass { /* ... */ }

export default class Demo extends Component {
  @link myInstance = new MyClass();

  // alternatively, suggestion by Runspired
  @link(MyClass) declare myInstance: MyClass;
}
  • One import instead of 3
  • Two lines instead of 13
  • This is a common task -- and something that's easy to mess up as the order of params swaps between associateDestroyableChild and setOwner

Implementation:

function link(_prototype: object, key: string, descriptor?: Descriptor): void {
  if (!descriptor) return;

  assert(`@link can only be used with string-keys`, typeof key === 'string');


  let { initializer } = descriptor;

  assert(
    `@link may only be used on initialized properties. For example, ` +
      `\`@link foo = new MyClass();\``,
    initializer
  );


  let caches = new WeakMap<object, any>();

  // https://github.com/pzuraq/ember-could-get-used-to-this/blob/master/addon/index.js
  return {
    get(this: object) {
      let child = caches.get(this);

      if (!child) {
        child = initializer.call(this);

        associateDestroyableChild(this, child);

        let owner = getOwner(this);

        if (owner) {
          setOwner(child, owner);
        }

        caches.set(this, child);
        assert(`Failed to create cache for internal resource configuration object`, child);
      }

      return child;
    },
  } as unknown as void /* Thanks TS. */;
}

Disclaimer: I've given up with "properly typing" Stage 1 decorators, and instead assert my way in to the shapes I need.

Thoughts?


Some may wonder how you'd reactively manage args passed to MyClass, and the answer there is a Resource.
Reason being is that you can't use this pattern:

@cached 
get myInstance() {
  let instance = new MyClass(this.args.foo);
  
  link(this, instance); // overloaded
  
  return instance;
}

because you only get constructions of MyClass, and no destructions or updates when the args change.
This may be perfectly fine for some cases, but Resources are specifically for managing this destruction-or-update scenario where as the @cached getter is constructions only, until the parent class is destroyed (then all relevant destructors still remaining in memory would be called)
Sure, you could manage an "update" pattern via a local un-tracked variable such as:

_myInstance;

@cached 
get myInstance() {
  if (this._myInstance) {
     this._myInstance.update(this.args.foo);
     return this._myInstance;
  } 
  let instance = new MyClass(this.args.foo);
  
  link(this, instance); // overloaded
  
  this._myInstance = instance;
  return instance;
}

But wouldn't it feel much better to do this instead:

@use myInstance = MyClass.from(() => [this.args.foo]);
// or
myInstance = MyClass.from(this, () => [this.args.foo]);

ember-resources supports both of these APIs (with and without the decorator), and both are 100% TS safe / make sense to consumers. I need more feedback before one could be decided on -- and no matter what happens, there will be codemods <3

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions