diff --git a/ember-async-data/src/tracked-async-data.ts b/ember-async-data/src/tracked-async-data.ts index af7e58ff..f7b6f9b2 100644 --- a/ember-async-data/src/tracked-async-data.ts +++ b/ember-async-data/src/tracked-async-data.ts @@ -32,13 +32,25 @@ class _TrackedAsyncData { #token: unknown; /** - @param promise The promise to load. + @param promise The promise to load or function to call to load the promise. */ - constructor(data: T | Promise) { + constructor(data: T | Promise | (() => T | Promise)) { if (this.constructor !== _TrackedAsyncData) { throw new Error('tracked-async-data cannot be subclassed'); } + // If the data is a function, we call it to get the actual data. + if (isFunction(data)) { + try { + data = data(); + } catch (error) { + // If the function throws an error instead of returning a promise, we conclude the state as rejected. + // This is not called if the returned promise rejects, as that is handled later. + this.#state.data = ['REJECTED', error]; + return; + } + } + if (!isPromiseLike(data)) { this.#state.data = ['RESOLVED', data]; return; @@ -298,7 +310,7 @@ interface Rejected extends _TrackedAsyncData { */ type TrackedAsyncData = Pending | Resolved | Rejected; const TrackedAsyncData = _TrackedAsyncData as new ( - data: T | Promise, + data: T | Promise | (() => T | Promise), ) => TrackedAsyncData; export default TrackedAsyncData; @@ -318,3 +330,7 @@ function isPromiseLike(data: unknown): data is PromiseLike { typeof data.then === 'function' ); } + +function isFunction(data: unknown): data is () => T | Promise { + return typeof data === 'function'; +} diff --git a/ember-async-data/type-tests/tracked-async-data-test.ts b/ember-async-data/type-tests/tracked-async-data-test.ts index 3e8fc11e..57601ac3 100644 --- a/ember-async-data/type-tests/tracked-async-data-test.ts +++ b/ember-async-data/type-tests/tracked-async-data-test.ts @@ -6,7 +6,7 @@ import { expectTypeOf } from 'expect-type'; declare function unreachable(x: never): never; declare class PublicAPI { - constructor(data: T | Promise); + constructor(data: T | Promise | (() => T | Promise)); get state(): 'PENDING' | 'RESOLVED' | 'REJECTED'; get value(): T | null; get error(): unknown; @@ -31,6 +31,19 @@ expectTypeOf(TrackedAsyncData).toBeConstructibleWith(Promise.resolve()); expectTypeOf(TrackedAsyncData).toBeConstructibleWith(Promise.resolve(12)); expectTypeOf(TrackedAsyncData).toBeConstructibleWith(Promise.reject()); expectTypeOf(TrackedAsyncData).toBeConstructibleWith(Promise.reject('gah')); +expectTypeOf(TrackedAsyncData).toBeConstructibleWith(() => 12); +expectTypeOf(TrackedAsyncData).toBeConstructibleWith(() => 'hello'); +expectTypeOf(TrackedAsyncData).toBeConstructibleWith(() => true); +expectTypeOf(TrackedAsyncData).toBeConstructibleWith(() => null); +expectTypeOf(TrackedAsyncData).toBeConstructibleWith(() => undefined); +expectTypeOf(TrackedAsyncData).toBeConstructibleWith(() => ({ cool: 'story' })); +expectTypeOf(TrackedAsyncData).toBeConstructibleWith(() => ['neat']); +expectTypeOf(TrackedAsyncData).toBeConstructibleWith(() => Promise.resolve()); +expectTypeOf(TrackedAsyncData).toBeConstructibleWith(() => Promise.resolve(12)); +expectTypeOf(TrackedAsyncData).toBeConstructibleWith(() => Promise.reject()); +expectTypeOf(TrackedAsyncData).toBeConstructibleWith(() => + Promise.reject('gah'), +); // We use `toMatchTypeOf` here to confirm the union type which makes up // `TrackedAsyncData` is structurally compatible with the desired public diff --git a/package.json b/package.json index 7aa9c009..2c991068 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-async-data", - "version": "1.0.3", + "version": "2.0.0", "private": true, "repository": "https://github.com/chriskrycho/ember-async-data", "license": "MIT", diff --git a/test-app/tests/unit/tracked-async-data-test.ts b/test-app/tests/unit/tracked-async-data-test.ts index 9f301e28..87b82d71 100644 --- a/test-app/tests/unit/tracked-async-data-test.ts +++ b/test-app/tests/unit/tracked-async-data-test.ts @@ -201,4 +201,47 @@ module('Unit | TrackedAsyncData', function () { assert.strictEqual(result.state, 'REJECTED'); }); + + module( + 'it calls input and uses return value when input is a function', + function () { + test('function returning a value', async function (assert) { + const result = new TrackedAsyncData(() => 'hello'); + await settled(); + + assert.strictEqual(result.state, 'RESOLVED'); + assert.strictEqual(result.value, 'hello'); + }); + + test('function returning a promise', async function (assert) { + const deferred = defer(); + const result = new TrackedAsyncData(() => deferred.promise); + + deferred.resolve('hello'); + await settled(); + + assert.strictEqual(result.state, 'RESOLVED'); + assert.strictEqual(result.value, 'hello'); + }); + + test('function throwing an error', async function (assert) { + const result = new TrackedAsyncData(() => { + throw new Error('foobar'); + }); + + assert.strictEqual(result.state, 'REJECTED'); + assert.strictEqual((result.error as Error).message, 'foobar'); + }); + + test('function returning a rejected promise', async function (assert) { + const result = new TrackedAsyncData(() => + Promise.reject(new Error('foobar')), + ); + await settled(); + + assert.strictEqual(result.state, 'REJECTED'); + assert.strictEqual((result.error as Error).message, 'foobar'); + }); + }, + ); });