Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions ember-async-data/src/tracked-async-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,25 @@ class _TrackedAsyncData<T> {
#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<T>) {
constructor(data: T | Promise<T> | (() => T | Promise<T>)) {
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;
Expand Down Expand Up @@ -298,7 +310,7 @@ interface Rejected<T> extends _TrackedAsyncData<T> {
*/
type TrackedAsyncData<T> = Pending<T> | Resolved<T> | Rejected<T>;
const TrackedAsyncData = _TrackedAsyncData as new <T>(
data: T | Promise<T>,
data: T | Promise<T> | (() => T | Promise<T>),
) => TrackedAsyncData<T>;
export default TrackedAsyncData;

Expand All @@ -318,3 +330,7 @@ function isPromiseLike(data: unknown): data is PromiseLike<unknown> {
typeof data.then === 'function'
);
}

function isFunction<T>(data: unknown): data is () => T | Promise<T> {
return typeof data === 'function';
}
15 changes: 14 additions & 1 deletion ember-async-data/type-tests/tracked-async-data-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { expectTypeOf } from 'expect-type';
declare function unreachable(x: never): never;

declare class PublicAPI<T> {
constructor(data: T | Promise<T>);
constructor(data: T | Promise<T> | (() => T | Promise<T>));
get state(): 'PENDING' | 'RESOLVED' | 'REJECTED';
get value(): T | null;
get error(): unknown;
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
43 changes: 43 additions & 0 deletions test-app/tests/unit/tracked-async-data-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
},
);
});