From ea8be9e74f4f5bf3e1900de20f649367997dc8d9 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Mon, 7 Aug 2023 17:07:33 -0400
Subject: [PATCH 1/5] trackedFunction is now usable in other resources
---
ember-resources/src/util/cell.ts | 1 +
ember-resources/src/util/function.ts | 83 +++++++++++++-
.../tests/utils/function/rendering-test.gts | 108 +++++++++++++++++-
3 files changed, 187 insertions(+), 5 deletions(-)
diff --git a/ember-resources/src/util/cell.ts b/ember-resources/src/util/cell.ts
index b3fc09269..e36f209e6 100644
--- a/ember-resources/src/util/cell.ts
+++ b/ember-resources/src/util/cell.ts
@@ -163,3 +163,4 @@ class CellManager {
const cellEvaluator = new CellManager();
setHelperManager(() => cellEvaluator, Cell.prototype);
+setHelperManager(() => cellEvaluator, ReadonlyCell.prototype);
diff --git a/ember-resources/src/util/function.ts b/ember-resources/src/util/function.ts
index 70b9681d3..4947fc189 100644
--- a/ember-resources/src/util/function.ts
+++ b/ember-resources/src/util/function.ts
@@ -1,10 +1,61 @@
import { tracked } from '@glimmer/tracking';
+import { assert } from '@ember/debug';
import { associateDestroyableChild, destroy, isDestroyed, isDestroying } from '@ember/destroyable';
import { TrackedAsyncData } from 'ember-async-data';
import { resource } from '../core/function-based';
+/**
+ *
+ *
+ * This is not a core part of ember-resources, but is an example utility to demonstrate a concept when authoring your own resources. However, this utility is still under the broader library's SemVer policy.
+ *
+ * A consuming app will not pay for the bytes of this utility unless imported.
+ *
+ *
+ *
+ * _An example utility that uses resource_
+ *
+ * Any tracked data accessed in a tracked function _before_ an `await`
+ * will "entangle" with the function -- we can call these accessed tracked
+ * properties, the "tracked prelude". If any properties within the tracked
+ * payload change, the function will re-run.
+ *
+ * ```js
+ * import Component from '@glimmer/component';
+ * import { tracked } from '@glimmer/tracking';
+ * import { resourceFactory, use } from 'ember-resources';
+ * import { trackedFunction } from 'ember-resources/util/function';
+ *
+ * const Request = resourceFactory((idFn) => {
+ * return trackedFunction(this, async () => {
+ * let id = idFn();
+ * let response = await fetch(`https://swapi.dev/api/people/${id}`);
+ * let data = await response.json();
+ *
+ * return data; // { name: 'Luke Skywalker', ... }
+ * });
+ * });
+ *
+ * class Demo extends Component {
+ * @tracked id = 1;
+ *
+ * updateId = (event) => this.id = event.target.value;
+ *
+ * request = use(this, Request(() => this.id));
+ *
+ * // Renders "Luke Skywalker"
+ *
+ * {{this.request.current.value.name}}
+ *
+ *
+ *
+ * }
+ * ```
+ */
+export function trackedFunction(fn: () => Return): State;
+
/**
*
*
@@ -56,7 +107,37 @@ import { resource } from '../core/function-based';
* @param {Object} context destroyable parent, e.g.: component instance aka "this"
* @param {Function} fn the function to run with the return value available on .value
*/
-export function trackedFunction(context: object, fn: () => Return) {
+export function trackedFunction(context: object, fn: () => Return): State;
+
+export function trackedFunction(
+ ...args: Parameters> | Parameters>
+): State {
+ if (args.length === 1) {
+ return classUsable(...args);
+ }
+
+ if (args.length === 2) {
+ return directTrackedFunction(...args);
+ }
+
+ assert('Unknown arity: trackedFunction must be called with 1 or 2 arguments');
+}
+
+function classUsable(fn: () => Return) {
+ const state = new State(fn);
+
+ let destroyable = resource>(() => {
+ state.retry();
+
+ return state;
+ });
+
+ associateDestroyableChild(destroyable, state);
+
+ return destroyable;
+}
+
+function directTrackedFunction(context: object, fn: () => Return) {
const state = new State(fn);
let destroyable = resource>(context, () => {
diff --git a/test-app/tests/utils/function/rendering-test.gts b/test-app/tests/utils/function/rendering-test.gts
index 9b759b191..23b725250 100644
--- a/test-app/tests/utils/function/rendering-test.gts
+++ b/test-app/tests/utils/function/rendering-test.gts
@@ -5,10 +5,12 @@ import { click, render, settled } from '@ember/test-helpers';
import { on } from '@ember/modifier';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
+import { setOwner } from '@ember/application';
+import { use, resource, resourceFactory } from 'ember-resources';
import { trackedFunction } from 'ember-resources/util/function';
-const timeout = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+const timeout = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
module('Utils | trackedFunction | rendering', function (hooks) {
setupRenderingTest(hooks);
@@ -24,7 +26,7 @@ module('Utils | trackedFunction | rendering', function (hooks) {
{{this.data.value}}
-
+
}
@@ -55,7 +57,7 @@ module('Utils | trackedFunction | rendering', function (hooks) {
{{this.data.value}}
-
+
}
@@ -89,7 +91,7 @@ module('Utils | trackedFunction | rendering', function (hooks) {
{{this.data.value}}
-
+
}
@@ -106,4 +108,102 @@ module('Utils | trackedFunction | rendering', function (hooks) {
assert.dom('out').hasText('4');
});
+
+ test('can be "use"d in a class', async function (assert) {
+ const Doubler = resourceFactory((numFn) =>
+ trackedFunction(async () => {
+ let num = numFn();
+
+ return num * 2;
+ })
+ );
+
+ class TestComponent extends Component {
+ @tracked multiplier = 1;
+
+ increment = () => this.multiplier++;
+
+ data = use(
+ this,
+ Doubler(() => this.multiplier)
+ );
+
+
+ {{this.data.current.value}}
+
+
+ }
+
+ await render();
+
+ assert.dom('out').hasText('2');
+
+ await click('button');
+
+ assert.dom('out').hasText('4');
+ });
+
+ test('can be composed with the resource use', async function (assert) {
+ type NumberThunk = () => number;
+
+ const Sqrt = resourceFactory((numFn: NumberThunk) =>
+ trackedFunction(async () => {
+ let num = numFn();
+
+ return Math.sqrt(num);
+ })
+ );
+
+ const Squared = resourceFactory((numFn: NumberThunk) =>
+ trackedFunction(async () => {
+ let num = numFn();
+
+ return Math.pow(num, 2);
+ })
+ );
+
+ const Hypotenuse = resourceFactory((aFn: NumberThunk, bFn: NumberThunk) => {
+ return resource(({ use }) => {
+ const aSquared = use(Squared(aFn));
+ const bSquared = use(Squared(bFn));
+ const c = use(
+ Sqrt(() => {
+ return (aSquared.current.value ?? 0) + (bSquared.current.value ?? 0);
+ })
+ );
+
+ return () => c.current.value;
+ });
+ });
+
+ class State {
+ @tracked a = 3;
+ @tracked b = 4;
+
+ c = use(
+ this,
+ Hypotenuse(
+ () => this.a,
+ () => this.b
+ )
+ );
+ }
+
+ let state = new State();
+
+ setOwner(state, this.owner);
+
+ await render({{state.c}});
+
+ assert.dom('out').hasText('5');
+
+ state.a = 7;
+ await settled();
+
+ assert.dom('out').containsText('8.06');
+
+ state.b = 10;
+ await settled();
+ assert.dom('out').containsText('12.206');
+ });
});
From 1a964f16ca6e528c337ad2788d4c870aa699ef32 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Mon, 7 Aug 2023 18:44:20 -0400
Subject: [PATCH 2/5] Add changeset and another test
---
.changeset/wet-cobras-happen.md | 133 ++++++++++++++++++
.../tests/utils/function/rendering-test.gts | 25 +++-
2 files changed, 156 insertions(+), 2 deletions(-)
create mode 100644 .changeset/wet-cobras-happen.md
diff --git a/.changeset/wet-cobras-happen.md b/.changeset/wet-cobras-happen.md
new file mode 100644
index 000000000..b472480e8
--- /dev/null
+++ b/.changeset/wet-cobras-happen.md
@@ -0,0 +1,133 @@
+---
+"ember-resources": minor
+---
+
+`trackedFunction` can now be composed just like regular resources.
+
+The function body still auto-tracks and will update within consuming resource appropriately.
+
+Example
+
+```ts
+const Person = resourceFactory((maybeIdFn) => {
+ return resource(({ use }) => {
+ let request = use(
+ trackedFunction(async () => {
+ let id = typeof maybeIdFn === "function" ? maybeIdFn() : maybeIdFn;
+ let response = await fetch(`https://github.com/gitapi/users/${id}`);
+ return response.json();
+ })
+ );
+
+ // `use` returns a ReadonlyCell where `.current`
+ // is the State of trackedFunction.
+ return () => request.current;
+ });
+});
+```
+
+Usage examples:
+
+```gjs
+
+ {{#let (Person 1) as |request|}}
+ {{#if request.isLoading}}
+ ... loading ...
+ {{/if}}
+
+ {{#if request.value}}
+
+ {{/if}}
+ {{/let}}
+
+```
+
+
+
+An async doubler
+
+```ts
+const Doubled = resourceFactory((num: number) => {
+ return resource(({ use }) => {
+ let doubler = use(trackedFunction(async () => num * 2));
+
+ // Since current is the "State" of `trackedFunction`,
+ // accessing .value on it means that the overall value of
+ // `Doubled` is the eventual return value of the `trackedFunction`
+ return () => doubler.current.value;
+ });
+});
+
+// Actual code from a test
+class State {
+ @tracked num = 2;
+}
+
+let state = new State();
+
+setOwner(state, this.owner);
+
+await render({{Doubled state.num}});
+```
+
+
+
+Example with arguments
+
+Imagine you want to compute the hypotenuse of a triangle,
+but all calculations are asynchronous (maybe the measurements exist on external APIs or something).
+
+```ts
+// Actual code from a test
+type NumberThunk = () => number;
+
+const Sqrt = resourceFactory((numFn: NumberThunk) =>
+ trackedFunction(async () => {
+ let num = numFn();
+
+ return Math.sqrt(num);
+ })
+);
+
+const Squared = resourceFactory((numFn: NumberThunk) =>
+ trackedFunction(async () => {
+ let num = numFn();
+
+ return Math.pow(num, 2);
+ })
+);
+
+const Hypotenuse = resourceFactory((aFn: NumberThunk, bFn: NumberThunk) => {
+ return resource(({ use }) => {
+ const aSquared = use(Squared(aFn));
+ const bSquared = use(Squared(bFn));
+ const c = use(
+ Sqrt(() => {
+ return (aSquared.current.value ?? 0) + (bSquared.current.value ?? 0);
+ })
+ );
+
+ // We use the function return because we want this property chain
+ // to be what's lazily evaluated -- in this example, since
+ // we want to return the hypotenuse, we don't (atm)
+ // care about loading / error state, etc.
+ // In real apps, you might care about loading state though!
+ return () => c.current.value;
+
+ // In situations where you care about forwarding other states,
+ // you could do this
+ return {
+ get value() {
+ return c.current.value;
+ },
+ get isLoading() {
+ return (
+ a.current.isLoading || b.current.isLoading || c.current.isLoading
+ );
+ },
+ };
+ });
+});
+```
+
+
diff --git a/test-app/tests/utils/function/rendering-test.gts b/test-app/tests/utils/function/rendering-test.gts
index 23b725250..ef11d6a3a 100644
--- a/test-app/tests/utils/function/rendering-test.gts
+++ b/test-app/tests/utils/function/rendering-test.gts
@@ -143,9 +143,30 @@ module('Utils | trackedFunction | rendering', function (hooks) {
assert.dom('out').hasText('4');
});
- test('can be composed with the resource use', async function (assert) {
- type NumberThunk = () => number;
+ type NumberThunk = () => number;
+ test('can be composed directly within a resource', async function (assert) {
+ const Doubled = resourceFactory((num: number) => {
+ return resource(({ use }) => {
+ let state = use(trackedFunction(() => num * 2));
+
+ return () => state.current.value;
+ });
+ });
+ class State {
+ @tracked num = 2;
+ }
+
+ let state = new State();
+
+ setOwner(state, this.owner);
+
+ await render({{Doubled state.num}});
+
+ assert.dom('out').hasText('4');
+ });
+
+ test('can be composed with the resource use', async function (assert) {
const Sqrt = resourceFactory((numFn: NumberThunk) =>
trackedFunction(async () => {
let num = numFn();
From 071323e6255905c90107c0f2588402f8d29aa6be Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Mon, 7 Aug 2023 19:05:13 -0400
Subject: [PATCH 3/5] Refactor
---
ember-resources/package.json | 2 +-
ember-resources/src/{util => core}/cell.ts | 4 ++--
ember-resources/src/core/function-based/manager.ts | 2 +-
ember-resources/src/core/use.ts | 2 +-
ember-resources/src/index.ts | 2 +-
5 files changed, 6 insertions(+), 6 deletions(-)
rename ember-resources/src/{util => core}/cell.ts (96%)
diff --git a/ember-resources/package.json b/ember-resources/package.json
index 830b64785..5999c8673 100644
--- a/ember-resources/package.json
+++ b/ember-resources/package.json
@@ -16,7 +16,7 @@
"./service": "./dist/service.js",
"./modifier": "./dist/modifier/index.js",
"./util": "./dist/util/index.js",
- "./util/cell": "./dist/util/cell.js",
+ "./util/cell": "./dist/core/cell.js",
"./util/keep-latest": "./dist/util/keep-latest.js",
"./util/fps": "./dist/util/fps.js",
"./util/map": "./dist/util/map.js",
diff --git a/ember-resources/src/util/cell.ts b/ember-resources/src/core/cell.ts
similarity index 96%
rename from ember-resources/src/util/cell.ts
rename to ember-resources/src/core/cell.ts
index e36f209e6..18990cf6f 100644
--- a/ember-resources/src/util/cell.ts
+++ b/ember-resources/src/core/cell.ts
@@ -142,9 +142,9 @@ export function cell(initialValue?: Value): Cell {
// @ts-ignore
import { capabilities as helperCapabilities, setHelperManager } from '@ember/helper';
-import { CURRENT } from '../core/function-based/types';
+import { CURRENT } from './function-based/types';
-import type { GlintRenderable, Reactive } from '../core/function-based/types';
+import type { GlintRenderable, Reactive } from './function-based/types';
class CellManager {
capabilities = helperCapabilities('3.23', {
diff --git a/ember-resources/src/core/function-based/manager.ts b/ember-resources/src/core/function-based/manager.ts
index 80681d533..9c7252c1b 100644
--- a/ember-resources/src/core/function-based/manager.ts
+++ b/ember-resources/src/core/function-based/manager.ts
@@ -8,7 +8,7 @@ import { invokeHelper } from '@ember/helper';
import { capabilities as helperCapabilities } from '@ember/helper';
import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros';
-import { ReadonlyCell } from '../../util/cell';
+import { ReadonlyCell } from '../cell';
import { CURRENT, INTERNAL } from './types';
import type {
diff --git a/ember-resources/src/core/use.ts b/ember-resources/src/core/use.ts
index 397e6929b..6cb77498f 100644
--- a/ember-resources/src/core/use.ts
+++ b/ember-resources/src/core/use.ts
@@ -8,7 +8,7 @@ import { associateDestroyableChild } from '@ember/destroyable';
// @ts-ignore
import { invokeHelper } from '@ember/helper';
-import { ReadonlyCell } from '../util/cell';
+import { ReadonlyCell } from './cell';
import { INTERNAL } from './function-based/types';
import { normalizeThunk } from './utils';
diff --git a/ember-resources/src/index.ts b/ember-resources/src/index.ts
index ef90a965e..a256f187d 100644
--- a/ember-resources/src/index.ts
+++ b/ember-resources/src/index.ts
@@ -4,7 +4,7 @@ export { resource, resourceFactory } from './core/function-based';
export { use } from './core/use';
// Public API -- Utilities
-export { cell } from './util/cell';
+export { cell } from './core/cell';
// Public Type Utilities
export type { ResourceAPI } from './core/function-based';
From 7299e6562bcafecbdab67a75fab6761d5038dbb7 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Mon, 7 Aug 2023 20:22:26 -0400
Subject: [PATCH 4/5] Update ci
---
.github/workflows/ci.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index db42042c9..b770ea356 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -101,6 +101,7 @@ jobs:
with:
no-lockfile: true
- uses: ./.github/actions/download-built-package
+ - run: pnpm i -f
- run: pnpm --filter test-app test:ember
From 0958ae1660604292d93845ca703df63e5850fb62 Mon Sep 17 00:00:00 2001
From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com>
Date: Mon, 7 Aug 2023 20:31:13 -0400
Subject: [PATCH 5/5] Revert "Refactor"
This reverts commit 071323e6255905c90107c0f2588402f8d29aa6be.
---
ember-resources/package.json | 2 +-
ember-resources/src/core/function-based/manager.ts | 2 +-
ember-resources/src/core/use.ts | 2 +-
ember-resources/src/index.ts | 2 +-
ember-resources/src/{core => util}/cell.ts | 4 ++--
5 files changed, 6 insertions(+), 6 deletions(-)
rename ember-resources/src/{core => util}/cell.ts (96%)
diff --git a/ember-resources/package.json b/ember-resources/package.json
index 5999c8673..830b64785 100644
--- a/ember-resources/package.json
+++ b/ember-resources/package.json
@@ -16,7 +16,7 @@
"./service": "./dist/service.js",
"./modifier": "./dist/modifier/index.js",
"./util": "./dist/util/index.js",
- "./util/cell": "./dist/core/cell.js",
+ "./util/cell": "./dist/util/cell.js",
"./util/keep-latest": "./dist/util/keep-latest.js",
"./util/fps": "./dist/util/fps.js",
"./util/map": "./dist/util/map.js",
diff --git a/ember-resources/src/core/function-based/manager.ts b/ember-resources/src/core/function-based/manager.ts
index 9c7252c1b..80681d533 100644
--- a/ember-resources/src/core/function-based/manager.ts
+++ b/ember-resources/src/core/function-based/manager.ts
@@ -8,7 +8,7 @@ import { invokeHelper } from '@ember/helper';
import { capabilities as helperCapabilities } from '@ember/helper';
import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros';
-import { ReadonlyCell } from '../cell';
+import { ReadonlyCell } from '../../util/cell';
import { CURRENT, INTERNAL } from './types';
import type {
diff --git a/ember-resources/src/core/use.ts b/ember-resources/src/core/use.ts
index 6cb77498f..397e6929b 100644
--- a/ember-resources/src/core/use.ts
+++ b/ember-resources/src/core/use.ts
@@ -8,7 +8,7 @@ import { associateDestroyableChild } from '@ember/destroyable';
// @ts-ignore
import { invokeHelper } from '@ember/helper';
-import { ReadonlyCell } from './cell';
+import { ReadonlyCell } from '../util/cell';
import { INTERNAL } from './function-based/types';
import { normalizeThunk } from './utils';
diff --git a/ember-resources/src/index.ts b/ember-resources/src/index.ts
index a256f187d..ef90a965e 100644
--- a/ember-resources/src/index.ts
+++ b/ember-resources/src/index.ts
@@ -4,7 +4,7 @@ export { resource, resourceFactory } from './core/function-based';
export { use } from './core/use';
// Public API -- Utilities
-export { cell } from './core/cell';
+export { cell } from './util/cell';
// Public Type Utilities
export type { ResourceAPI } from './core/function-based';
diff --git a/ember-resources/src/core/cell.ts b/ember-resources/src/util/cell.ts
similarity index 96%
rename from ember-resources/src/core/cell.ts
rename to ember-resources/src/util/cell.ts
index 18990cf6f..e36f209e6 100644
--- a/ember-resources/src/core/cell.ts
+++ b/ember-resources/src/util/cell.ts
@@ -142,9 +142,9 @@ export function cell(initialValue?: Value): Cell {
// @ts-ignore
import { capabilities as helperCapabilities, setHelperManager } from '@ember/helper';
-import { CURRENT } from './function-based/types';
+import { CURRENT } from '../core/function-based/types';
-import type { GlintRenderable, Reactive } from './function-based/types';
+import type { GlintRenderable, Reactive } from '../core/function-based/types';
class CellManager {
capabilities = helperCapabilities('3.23', {