Skip to content

Commit 2e3b876

Browse files
committed
Implement experiment for: emberjs/rfcs#905
1 parent e7b15d7 commit 2e3b876

File tree

4 files changed

+259
-0
lines changed

4 files changed

+259
-0
lines changed

.changeset/hip-fishes-agree.md

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
"ember-resources": minor
3+
---
4+
5+
Add link() and @link, importable from `ember-resources/link`.
6+
7+
NOTE: for existing users of `ember-resources`, this addition has no impact on your bundle.
8+
9+
<details><summary>Example property usage</summary>
10+
11+
```js
12+
import { link } from 'ember-resources/link';
13+
14+
class MyClass { ... }
15+
16+
export default class Demo extends Component {
17+
// This usage does now allow passing args to `MyClass`
18+
@link(MyClass) myInstance;
19+
}
20+
```
21+
22+
</details>
23+
24+
<details><summary>Example inline usage</summary>
25+
26+
```js
27+
import Component from '@glimmer/component';
28+
import { cached } from '@glimmer/tracking';
29+
import { link } from 'ember-resources/link';
30+
31+
export default class Demo extends Component {
32+
// To pass args to `MyClass`, you must use this form
33+
// NOTE though, that `instance` is linked to the `Demo`s lifecycle.
34+
// So if @foo is changing frequently, memory pressure will increase rapidly
35+
// until the `Demo` instance is destroyed.
36+
//
37+
// Resources are a better fit for this use case, as they won't add to memory pressure.
38+
@cached
39+
get myFunction() {
40+
let instance = new MyClass(this.args.foo);
41+
42+
return link(instance, this);
43+
}
44+
}
45+
```
46+
47+
</details>
48+
49+
50+
This abstracts away the following boilerplate:
51+
```js
52+
import { getOwner, setOwner } from '@ember/owner';
53+
import { associateDestroyableChild } from '@ember/destroyable';
54+
55+
class MyClass { /* ... */ }
56+
57+
export default class Demo extends Component {
58+
@cached
59+
get myInstance() {
60+
let instance = new MyClass();
61+
62+
associateDestroyableChild(this, instance);
63+
64+
let owner = getOwner(this);
65+
66+
if (owner) {
67+
setOwner(instance, owner);
68+
}
69+
70+
return instance;
71+
}
72+
}
73+
```
74+

ember-resources/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"./core": "./dist/core/index.js",
1313
"./core/class-based": "./dist/core/class-based/index.js",
1414
"./core/function-based": "./dist/core/function-based/index.js",
15+
"./link": "./dist/link.js",
1516
"./service": "./dist/service.js",
1617
"./util": "./dist/util/index.js",
1718
"./util/cell": "./dist/util/cell.js",
@@ -33,6 +34,9 @@
3334
"core": [
3435
"dist/core/index.d.ts"
3536
],
37+
"link": [
38+
"dist/link.d.ts"
39+
],
3640
"service": [
3741
"dist/service.d.ts"
3842
],

ember-resources/src/link.ts

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { getOwner, setOwner } from '@ember/application';
2+
import { assert } from '@ember/debug';
3+
import { associateDestroyableChild } from '@ember/destroyable';
4+
5+
import type { Stage1DecoratorDescriptor } from '[core-types]';
6+
7+
/**
8+
* A util to abstract away the boilerplate of linking of "things" with an owner
9+
* and making them destroyable.
10+
*
11+
* ```js
12+
* import Component from '@glimmer/component';
13+
* import { cached } from '@glimmer/tracking';
14+
* import { link } from 'ember-resources/link';
15+
*
16+
* export default class Demo extends Component {
17+
* @cached
18+
* get myFunction() {
19+
* let instance = new MyClass(this.args.foo);
20+
*
21+
* return link(instance, this);
22+
* }
23+
* }
24+
* ```
25+
*
26+
* NOTE: If args change, as in this example, memory pressure will increase,
27+
* as the linked instance will be held on to until the host object is destroyed.
28+
*/
29+
export function link<Child extends object>(child: Child, parent: object): Child;
30+
31+
/**
32+
* A util to abstract away the boilerplate of linking of "things" with an owner
33+
* and making them destroyable.
34+
*
35+
*
36+
* Example as a class property:
37+
* ```js
38+
* import { link } from 'ember-resources/link';
39+
*
40+
* class MyClass { ... }
41+
*
42+
* export default class Demo extends Component {
43+
* @link(MyClass) myInstance;
44+
* }
45+
* ```
46+
*
47+
* Example inline usage:
48+
* ```js
49+
* import Component from '@glimmer/component';
50+
* import { cached } from '@glimmer/tracking';
51+
* import { link } from 'ember-resources/link';
52+
*
53+
* export default class Demo extends Component {
54+
* @cached
55+
* get myFunction() {
56+
* let instance = new MyClass(this.args.foo);
57+
*
58+
* return link(instance, this);
59+
* }
60+
* }
61+
* ```
62+
*/
63+
export function link(prototype: object, key: string, descriptor: Stage1DecoratorDescriptor): void;
64+
65+
export function link(...args: [object, string, Stage1DecoratorDescriptor] | [object, object]) {
66+
if (args.length === 3) {
67+
return linkDecorator(...args);
68+
}
69+
70+
return directLink(...args);
71+
}
72+
73+
function directLink(child: object, parent: object) {
74+
associateDestroyableChild(parent, child);
75+
76+
let owner = getOwner(parent);
77+
78+
if (owner) {
79+
setOwner(child, owner);
80+
}
81+
82+
return child;
83+
}
84+
85+
function linkDecorator(
86+
_prototype: object,
87+
key: string,
88+
descriptor?: Stage1DecoratorDescriptor
89+
): void {
90+
if (!descriptor) return;
91+
92+
assert(`@link can only be used with string-keys`, typeof key === 'string');
93+
94+
let { initializer } = descriptor;
95+
96+
assert(
97+
`@link may only be used on initialized properties. For example, ` +
98+
`\`@link foo = new MyClass();\``,
99+
initializer
100+
);
101+
102+
let caches = new WeakMap<object, any>();
103+
104+
// https://github.com/pzuraq/ember-could-get-used-to-this/blob/master/addon/index.js
105+
return {
106+
get(this: object) {
107+
let child = caches.get(this);
108+
109+
if (!child) {
110+
child = initializer.call(this);
111+
112+
associateDestroyableChild(this, child);
113+
114+
let owner = getOwner(this);
115+
116+
if (owner) {
117+
setOwner(child, owner);
118+
}
119+
120+
caches.set(this, child);
121+
assert(`Failed to create cache for internal resource configuration object`, child);
122+
}
123+
124+
return child;
125+
},
126+
} as unknown as void /* Thanks TS. */;
127+
}

test-app/tests/link-test.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { setOwner } from '@ember/owner';
2+
import Service, { service } from '@ember/service';
3+
import { module, test } from 'qunit';
4+
import { setupTest } from 'ember-qunit';
5+
6+
import { link } from 'ember-resources/link';
7+
8+
module('@link', function (hooks) {
9+
setupTest(hooks);
10+
11+
class FooService extends Service {
12+
foo = 2;
13+
}
14+
15+
test('it works', async function (assert) {
16+
this.owner.register('service:foo', FooService);
17+
18+
class Demo {
19+
@service declare foo: FooService;
20+
}
21+
22+
class TestDemo {
23+
@link(Demo) declare demo: Demo;
24+
}
25+
26+
let testDemo = new TestDemo();
27+
28+
setOwner(testDemo, this.owner);
29+
30+
assert.strictEqual(testDemo.demo.foo.foo, 2);
31+
});
32+
});
33+
34+
module('link', function (hooks) {
35+
setupTest(hooks);
36+
37+
class FooService extends Service {
38+
foo = 2;
39+
}
40+
41+
test('it works', async function (assert) {
42+
this.owner.register('service:foo', FooService);
43+
44+
class Demo {
45+
@service declare foo: FooService;
46+
}
47+
48+
let demo = new Demo();
49+
50+
link(demo, this);
51+
52+
assert.strictEqual(demo.foo.foo, 2);
53+
});
54+
});

0 commit comments

Comments
 (0)