diff --git a/.changeset/sour-pears-tickle.md b/.changeset/sour-pears-tickle.md
new file mode 100644
index 000000000..6afcefa4d
--- /dev/null
+++ b/.changeset/sour-pears-tickle.md
@@ -0,0 +1,23 @@
+---
+"ember-resources": patch
+---
+
+Resolves: https://github.com/NullVoxPopuli/ember-resources/issues/958
+
+`use`d Resources can now be immediately returned from other resources.
+
+```js
+const Clock = resource(({ use }) => {
+ return use(Instant({ intervalMs: 1000 });
+});
+
+const Stopwatch = resource(({ use }) => {
+ return use(Instant({ intervalMs: 0 });
+});
+
+
+
+
+ MS since Epoch: {{Stopwatch}}
+
+```
diff --git a/ember-resources/src/core/class-based/manager.ts b/ember-resources/src/core/class-based/manager.ts
index 1f37d2907..69784a07e 100644
--- a/ember-resources/src/core/class-based/manager.ts
+++ b/ember-resources/src/core/class-based/manager.ts
@@ -9,20 +9,6 @@ import { Resource } from './resource';
import type { ArgsWrapper } from '[core-types]';
import type Owner from '@ember/owner';
-/**
- *
- */
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
-// export interface Resource extends InstanceType<
-// HelperLike<{
-// Args: {
-// Named: NonNullable;
-// Positional: NonNullable
-// };
-// // Return: number
-// }>
-// > {}
-
class ResourceManager {
capabilities = helperCapabilities('3.23', {
hasValue: true,
diff --git a/ember-resources/src/core/function-based/manager.ts b/ember-resources/src/core/function-based/manager.ts
index 03851b565..80681d533 100644
--- a/ember-resources/src/core/function-based/manager.ts
+++ b/ember-resources/src/core/function-based/manager.ts
@@ -8,8 +8,8 @@ import { invokeHelper } from '@ember/helper';
import { capabilities as helperCapabilities } from '@ember/helper';
import { dependencySatisfies, importSync, macroCondition } from '@embroider/macros';
-import { Cell } from '../../util/cell';
-import { INTERNAL } from './types';
+import { ReadonlyCell } from '../../util/cell';
+import { CURRENT, INTERNAL } from './types';
import type {
Cache,
@@ -22,15 +22,18 @@ import type { ResourceAPI } from './types';
import type Owner from '@ember/owner';
let getOwner: (context: unknown) => Owner | undefined;
+let setOwner: (context: unknown, owner: Owner) => void;
if (macroCondition(dependencySatisfies('ember-source', '>=4.12.0'))) {
// In no version of ember where `@ember/owner` tried to be imported did it exist
// if (macroCondition(false)) {
// Using 'any' here because importSync can't lookup types correctly
getOwner = (importSync('@ember/owner') as any).getOwner;
+ setOwner = (importSync('@ember/owner') as any).setOwner;
} else {
// Using 'any' here because importSync can't lookup types correctly
getOwner = (importSync('@ember/application') as any).getOwner;
+ setOwner = (importSync('@ember/application') as any).setOwner;
}
/**
@@ -93,19 +96,17 @@ class FunctionResourceManager {
destroy(previousCache);
}
- let cache = invokeHelper(owner, usable);
+ let nestedCache = invokeHelper(cache, usable);
- associateDestroyableChild(currentFn, cache as object);
+ associateDestroyableChild(currentFn, nestedCache as object);
- usableCache.set(usable, cache);
+ usableCache.set(usable, nestedCache);
- return {
- get current() {
- let cache = usableCache.get(usable);
+ return new ReadonlyCell(() => {
+ let cache = usableCache.get(usable);
- return getValue(cache);
- },
- };
+ return getValue(cache);
+ });
};
let maybeValue = currentFn({
@@ -121,6 +122,8 @@ class FunctionResourceManager {
return maybeValue;
});
+ setOwner(cache, owner);
+
return { fn: thisFn, cache };
}
@@ -131,8 +134,8 @@ class FunctionResourceManager {
return maybeValue();
}
- if (maybeValue instanceof Cell) {
- return maybeValue.current;
+ if (isReactive(maybeValue)) {
+ return maybeValue[CURRENT];
}
return maybeValue;
@@ -143,4 +146,8 @@ class FunctionResourceManager {
}
}
+function isReactive(maybe: unknown): maybe is Reactive {
+ return typeof maybe === 'object' && maybe !== null && CURRENT in maybe;
+}
+
export const ResourceManagerFactory = (owner: Owner) => new FunctionResourceManager(owner);
diff --git a/ember-resources/src/core/function-based/types.ts b/ember-resources/src/core/function-based/types.ts
index 8a2cc0ee5..5ee72b371 100644
--- a/ember-resources/src/core/function-based/types.ts
+++ b/ember-resources/src/core/function-based/types.ts
@@ -10,9 +10,28 @@ export interface InternalFunctionResourceConfig {
[INTERNAL]: true;
}
+export const CURRENT = Symbol('ember-resources::CURRENT');
+
+export interface GlintRenderable {
+ /**
+ * Cells aren't inherently understood by Glint,
+ * so to work around that, we'll hook in to the fact that
+ * ContentValue (the type expected for all renderables),
+ * defines an interface with this signature.
+ *
+ * (SafeString)
+ *
+ * There *has* been interest in the community to formally support
+ * toString and toHTML APIs across all objects. An RFC needs to be
+ * written so that we can gather feedback / potential problems.
+ */
+ toHTML(): string;
+}
+
// Will need to be a class for .current flattening / auto-rendering
-export interface Reactive {
+export interface Reactive extends GlintRenderable {
current: Value;
+ [CURRENT]: Value;
[Invoke]?: Value;
}
diff --git a/ember-resources/src/core/use.ts b/ember-resources/src/core/use.ts
index 59dda376d..397e6929b 100644
--- a/ember-resources/src/core/use.ts
+++ b/ember-resources/src/core/use.ts
@@ -8,6 +8,7 @@ import { associateDestroyableChild } from '@ember/destroyable';
// @ts-ignore
import { invokeHelper } from '@ember/helper';
+import { ReadonlyCell } from '../util/cell';
import { INTERNAL } from './function-based/types';
import { normalizeThunk } from './utils';
@@ -120,19 +121,17 @@ function classContextLink(
): Reactive {
let cache: ReturnType;
- return {
- get current() {
- if (!cache) {
- cache = invokeHelper(context, definition);
+ return new ReadonlyCell(() => {
+ if (!cache) {
+ cache = invokeHelper(context, definition);
- associateDestroyableChild(context, cache);
- }
+ associateDestroyableChild(context, cache);
+ }
- let value = getValue(cache);
+ let value = getValue(cache);
- return getCurrentValue(value);
- },
- };
+ return getCurrentValue(value);
+ });
}
function argumentToDecorator(definition: Value | (() => Value)): PropertyDecorator {
diff --git a/ember-resources/src/util/cell.ts b/ember-resources/src/util/cell.ts
index 12596c238..b3fc09269 100644
--- a/ember-resources/src/util/cell.ts
+++ b/ember-resources/src/util/cell.ts
@@ -1,25 +1,35 @@
import { tracked } from '@glimmer/tracking';
import { assert } from '@ember/debug';
-interface GlintRenderable {
- /**
- * Cells aren't inherently understood by Glint,
- * so to work around that, we'll hook in to the fact that
- * ContentValue (the type expected for all renderables),
- * defines an interface with this signature.
- *
- * (SafeString)
- *
- * There *has* been interest in the community to formally support
- * toString and toHTML APIs across all objects. An RFC needs to be
- * written so that we can gather feedback / potential problems.
- */
- toHTML(): string;
+export class ReadonlyCell implements Reactive {
+ #getter: () => Value;
+
+ constructor(getter: () => Value) {
+ this.#getter = getter;
+ }
+
+ toHTML(): string {
+ assert(
+ 'Not a valid API. Please access either .current or .read() if the value of this Cell is needed'
+ );
+ }
+
+ get [CURRENT](): Value {
+ return this.current;
+ }
+
+ get current(): Value {
+ return this.#getter();
+ }
}
-export class Cell implements GlintRenderable {
+export class Cell implements Reactive {
@tracked declare current: Value;
+ get [CURRENT](): Value {
+ return this.current;
+ }
+
toHTML(): string {
assert(
'Not a valid API. Please access either .current or .read() if the value of this Cell is needed'
@@ -132,6 +142,10 @@ export function cell(initialValue?: Value): Cell {
// @ts-ignore
import { capabilities as helperCapabilities, setHelperManager } from '@ember/helper';
+import { CURRENT } from '../core/function-based/types';
+
+import type { GlintRenderable, Reactive } from '../core/function-based/types';
+
class CellManager {
capabilities = helperCapabilities('3.23', {
hasValue: true,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 42d347501..76b3f2450 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1352,7 +1352,7 @@ packages:
debug: 4.3.4(supports-color@8.1.1)
lodash.debounce: 4.0.8
resolve: 1.22.2
- semver: 6.3.0
+ semver: 6.3.1
transitivePeerDependencies:
- supports-color
@@ -3226,13 +3226,6 @@ packages:
regenerator-runtime: 0.13.11
dev: true
- /@babel/runtime@7.22.5:
- resolution: {integrity: sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==}
- engines: {node: '>=6.9.0'}
- dependencies:
- regenerator-runtime: 0.13.11
- dev: true
-
/@babel/runtime@7.22.6:
resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==}
engines: {node: '>=6.9.0'}
@@ -3325,7 +3318,7 @@ packages:
/@changesets/apply-release-plan@6.1.3:
resolution: {integrity: sha512-ECDNeoc3nfeAe1jqJb5aFQX7CqzQhD2klXRez2JDb/aVpGUbX673HgKrnrgJRuQR/9f2TtLoYIzrGB9qwD77mg==}
dependencies:
- '@babel/runtime': 7.22.5
+ '@babel/runtime': 7.22.6
'@changesets/config': 2.3.0
'@changesets/get-version-range-type': 0.3.2
'@changesets/git': 2.0.0
@@ -3337,18 +3330,18 @@ packages:
outdent: 0.5.0
prettier: 2.8.4
resolve-from: 5.0.0
- semver: 5.7.1
+ semver: 5.7.2
dev: true
/@changesets/assemble-release-plan@5.2.3(patch_hash=oi6v6io33uxvojef6ezqzay4oy):
resolution: {integrity: sha512-g7EVZCmnWz3zMBAdrcKhid4hkHT+Ft1n0mLussFMcB1dE2zCuwcvGoy9ec3yOgPGF4hoMtgHaMIk3T3TBdvU9g==}
dependencies:
- '@babel/runtime': 7.22.5
+ '@babel/runtime': 7.22.6
'@changesets/errors': 0.1.4
'@changesets/get-dependents-graph': 1.3.5
'@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3
- semver: 5.7.1
+ semver: 5.7.2
dev: true
patched: true
@@ -3432,7 +3425,7 @@ packages:
'@manypkg/get-packages': 1.1.3
chalk: 2.4.2
fs-extra: 7.0.1
- semver: 5.7.1
+ semver: 5.7.2
dev: true
/@changesets/get-github-info@0.5.2:
@@ -3447,7 +3440,7 @@ packages:
/@changesets/get-release-plan@3.0.16:
resolution: {integrity: sha512-OpP9QILpBp1bY2YNIKFzwigKh7Qe9KizRsZomzLe6pK8IUo8onkAAVUD8+JRKSr8R7d4+JRuQrfSSNlEwKyPYg==}
dependencies:
- '@babel/runtime': 7.22.5
+ '@babel/runtime': 7.22.6
'@changesets/assemble-release-plan': 5.2.3(patch_hash=oi6v6io33uxvojef6ezqzay4oy)
'@changesets/config': 2.3.0
'@changesets/pre': 1.0.14
@@ -3463,7 +3456,7 @@ packages:
/@changesets/git@2.0.0:
resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==}
dependencies:
- '@babel/runtime': 7.22.5
+ '@babel/runtime': 7.22.6
'@changesets/errors': 0.1.4
'@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3
@@ -3488,7 +3481,7 @@ packages:
/@changesets/pre@1.0.14:
resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==}
dependencies:
- '@babel/runtime': 7.22.5
+ '@babel/runtime': 7.22.6
'@changesets/errors': 0.1.4
'@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3
@@ -3498,7 +3491,7 @@ packages:
/@changesets/read@0.5.9:
resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==}
dependencies:
- '@babel/runtime': 7.22.5
+ '@babel/runtime': 7.22.6
'@changesets/git': 2.0.0
'@changesets/logger': 0.0.5
'@changesets/parse': 0.3.16
@@ -3519,7 +3512,7 @@ packages:
/@changesets/write@0.2.3:
resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==}
dependencies:
- '@babel/runtime': 7.22.5
+ '@babel/runtime': 7.22.6
'@changesets/types': 5.2.1
fs-extra: 7.0.1
human-id: 1.0.2
@@ -4657,7 +4650,7 @@ packages:
/@manypkg/find-root@1.1.0:
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
dependencies:
- '@babel/runtime': 7.22.5
+ '@babel/runtime': 7.22.6
'@types/node': 12.20.55
find-up: 4.1.0
fs-extra: 8.1.0
@@ -4666,7 +4659,7 @@ packages:
/@manypkg/get-packages@1.1.3:
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
dependencies:
- '@babel/runtime': 7.22.5
+ '@babel/runtime': 7.22.6
'@changesets/types': 4.1.0
'@manypkg/find-root': 1.1.0
fs-extra: 8.1.0
@@ -11048,7 +11041,7 @@ packages:
eslint-import-resolver-webpack:
optional: true
dependencies:
- '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@5.0.3)
+ '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@4.8.2)
debug: 3.2.7
eslint: 8.35.0
eslint-import-resolver-node: 0.3.7
@@ -11097,7 +11090,7 @@ packages:
optional: true
dependencies:
'@babel/core': 7.22.9(supports-color@8.1.1)
- '@babel/eslint-parser': 7.19.1(@babel/core@7.22.9)(eslint@8.35.0)
+ '@babel/eslint-parser': 7.19.1(@babel/core@7.21.4)(eslint@8.35.0)
'@babel/plugin-proposal-decorators': 7.22.7(@babel/core@7.22.9)
'@ember-data/rfc395-data': 0.0.4
ember-rfc176-data: 0.3.18
@@ -11162,7 +11155,7 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
- '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@5.0.3)
+ '@typescript-eslint/parser': 5.54.0(eslint@8.35.0)(typescript@4.8.2)
array-includes: 3.1.6
array.prototype.flat: 1.3.1
array.prototype.flatmap: 1.3.1
@@ -14471,7 +14464,7 @@ packages:
dependencies:
hosted-git-info: 2.8.9
resolve: 1.22.2
- semver: 5.7.1
+ semver: 5.7.2
validate-npm-package-license: 3.0.4
dev: true
diff --git a/test-app/ember-cli-build.js b/test-app/ember-cli-build.js
index 5711df926..bbaf03989 100644
--- a/test-app/ember-cli-build.js
+++ b/test-app/ember-cli-build.js
@@ -29,9 +29,9 @@ module.exports = function (defaults) {
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.
- const { maybeEmbroider } = require('@embroider/test-setup');
+ const { Webpack } = require('@embroider/webpack');
- return maybeEmbroider(app, {
+ return require('@embroider/compat').compatBuild(app, Webpack, {
packageRules: [
{
package: 'test-app',
@@ -44,5 +44,8 @@ module.exports = function (defaults) {
},
},
],
+ packagerOptions: {
+ webpackConfig: { devtool: 'source-map' },
+ },
});
};
diff --git a/test-app/package.json b/test-app/package.json
index d99886a96..5d6196662 100644
--- a/test-app/package.json
+++ b/test-app/package.json
@@ -20,7 +20,7 @@
"lint:prettier:fix": "prettier --write .",
"lint:hbs:fix": "ember-template-lint . --fix --no-error-on-unmatched-pattern",
"lint:js:fix": "eslint . --fix",
- "start": "concurrently 'ember serve' 'pnpm _syncPnpm --watch' --names 'tests serve,tests sync deps'",
+ "start": "concurrently 'ember serve -p 4204' 'pnpm _syncPnpm --watch' --names 'tests serve,tests sync deps'",
"test": "ember test",
"test:ember": "ember test",
"_syncPnpm": "DEBUG=true pnpm sync-dependencies-meta-injected"
diff --git a/test-app/tests/core/function-resource/composition-test.gts b/test-app/tests/core/function-resource/composition-test.gts
index 1a9647516..82c5854ec 100644
--- a/test-app/tests/core/function-resource/composition-test.gts
+++ b/test-app/tests/core/function-resource/composition-test.gts
@@ -1,4 +1,4 @@
-import { render, rerender, clearRender } from '@ember/test-helpers';
+import { render, rerender, clearRender, settled } from '@ember/test-helpers';
import { tracked } from '@glimmer/tracking';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
@@ -53,6 +53,52 @@ module('Core | (function) resource | use | rendering', function (hooks) {
assert.notEqual(first, second);
});
+ test('it works with directly returning the resource', async function (assert) {
+ let controlledCount = cell(0);
+
+ const Count = resource(() => {
+ return controlledCount;
+ });
+
+ const AlsoCount = resource(({ use }) => {
+ return use(Count);
+ });
+
+ await render({{AlsoCount}});
+
+ assert.dom().hasText('0');
+
+ controlledCount.current++;
+ await settled();
+
+ assert.dom().hasText('1');
+ });
+
+ test('it deeply works with directly returning the resource', async function (assert) {
+ let controlledCount = cell(0);
+
+ const Count = resource(() => {
+ return controlledCount;
+ });
+
+ const AlsoCount = resource(({ use }) => {
+ return use(Count);
+ });
+
+ const DeeplyCount = resource(({ use }) => {
+ return use(AlsoCount);
+ });
+
+ await render({{DeeplyCount}});
+
+ assert.dom().hasText('0');
+
+ controlledCount.current++;
+ await settled();
+
+ assert.dom().hasText('1');
+ });
+
test('it works with the blueprint/factory', async function (assert) {
let nowDate = Date.now();
let format = (time: Reactive) => formatter.format(time.current);