Skip to content

Commit 56f92fc

Browse files
committed
feat: ensure data utils work well with legacy relationship proxies
1 parent 0518476 commit 56f92fc

File tree

4 files changed

+161
-4
lines changed

4 files changed

+161
-4
lines changed

packages/ember/src/-private/promise-state.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,24 @@ export class PromiseState<T = unknown, E = unknown> {
4444
}
4545
}
4646

47+
const LegacyPromiseProxy = Symbol.for('LegacyPromiseProxy');
48+
type LegacyAwaitable<T, E> = { promise: Promise<T> | Awaitable<T, E>; [LegacyPromiseProxy]: true };
49+
50+
function isLegacyAwaitable<T, E>(promise: object): promise is LegacyAwaitable<T, E> {
51+
return LegacyPromiseProxy in promise && 'promise' in promise && promise[LegacyPromiseProxy] === true;
52+
}
53+
54+
function getPromise<T, E>(promise: Promise<T> | Awaitable<T, E> | LegacyAwaitable<T, E>): Promise<T> | Awaitable<T, E> {
55+
return isLegacyAwaitable(promise) ? promise.promise : promise;
56+
}
57+
4758
export function getPromiseState<T = unknown, E = unknown>(promise: Promise<T> | Awaitable<T, E>): PromiseState<T, E> {
48-
let state = PromiseCache.get(promise) as PromiseState<T, E> | undefined;
59+
const _promise = getPromise(promise);
60+
let state = PromiseCache.get(_promise) as PromiseState<T, E> | undefined;
4961

5062
if (!state) {
51-
state = new PromiseState(promise);
52-
PromiseCache.set(promise, state);
63+
state = new PromiseState(_promise);
64+
PromiseCache.set(_promise, state);
5365
}
5466

5567
return state;

packages/model/src/-private/promise-belongs-to.ts

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface BelongsToProxyCreateArgs<T = unknown> {
2323
_belongsToState: BelongsToProxyMeta<T>;
2424
}
2525

26+
export const LegacyPromiseProxy = Symbol.for('LegacyPromiseProxy');
27+
2628
interface PromiseObjectType<T> extends PromiseProxyMixin<T | null>, ObjectProxy<T> {
2729
// eslint-disable-next-line @typescript-eslint/no-misused-new
2830
new <PT>(...args: unknown[]): PromiseObjectType<PT>;
@@ -80,6 +82,8 @@ class PromiseBelongsTo<T = unknown> extends Extended<T> {
8082
await legacySupport.reloadBelongsTo(key, options);
8183
return this;
8284
}
85+
86+
[LegacyPromiseProxy] = true as const;
8387
}
8488

8589
export { PromiseBelongsTo };

packages/model/src/-private/promise-many-array.ts

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { compat } from '@ember-data/tracking';
66
import { defineSignal } from '@ember-data/tracking/-private';
77

88
import type ManyArray from './many-array';
9+
import { LegacyPromiseProxy } from './promise-belongs-to';
910

1011
export interface HasManyProxyCreateArgs<T = unknown> {
1112
promise: Promise<ManyArray<T>>;
@@ -195,6 +196,8 @@ export default class PromiseManyArray<T = unknown> {
195196
static create<T>({ promise, content }: HasManyProxyCreateArgs<T>): PromiseManyArray<T> {
196197
return new this(promise, content);
197198
}
199+
200+
[LegacyPromiseProxy] = true as const;
198201
}
199202
defineSignal(PromiseManyArray.prototype, 'content', null);
200203
defineSignal(PromiseManyArray.prototype, 'isPending', false);

tests/warp-drive__ember/tests/integration/get-promise-state-test.gts

+139-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,34 @@
11
import { rerender, settled } from '@ember/test-helpers';
22

3-
import { createDeferred, setPromiseResult } from '@ember-data/request';
3+
import { createDeferred, setPromiseResult, type Awaitable } from '@ember-data/request';
44
import type { RenderingTestContext } from '@warp-drive/diagnostic/ember';
55
import { module, setupRenderingTest, test } from '@warp-drive/diagnostic/ember';
66
import { getPromiseState } from '@warp-drive/ember';
77

88
type PromiseState<T, E> = ReturnType<typeof getPromiseState<T, E>>;
9+
const SecretSymbol = Symbol.for('LegacyPromiseProxy');
10+
11+
class PromiseProxy<T, E> {
12+
[SecretSymbol]: true;
13+
promise: Awaitable<T, E>;
14+
15+
constructor(promise: Awaitable<T, E>) {
16+
this[SecretSymbol] = true;
17+
this.promise = promise;
18+
}
19+
20+
then(onFulfilled: (value: T) => T, onRejected: (error: E) => void): Promise<T> {
21+
return this.promise.then(onFulfilled, onRejected) as Promise<T>;
22+
}
23+
24+
catch(onRejected: (error: E) => void): Promise<unknown> {
25+
return this.promise.catch(onRejected) as Promise<unknown>;
26+
}
27+
28+
finally(onFinally: () => void): Promise<unknown> {
29+
return this.promise.finally(onFinally) as Promise<unknown>;
30+
}
31+
}
932

1033
module('Integration | get-promise-state', function (hooks) {
1134
setupRenderingTest(hooks);
@@ -196,4 +219,119 @@ module('Integration | get-promise-state', function (hooks) {
196219
assert.equal(counter, 1);
197220
assert.equal(this.element.textContent?.trim(), 'Our Error\n Count:\n 1');
198221
});
222+
223+
test('it renders only once when the promise already has a result cached', async function (this: RenderingTestContext, assert) {
224+
const promise = Promise.resolve().then(() => 'Our Data');
225+
226+
const result = await promise;
227+
setPromiseResult(promise, { result, isError: false });
228+
229+
let state: PromiseState<string, Error>;
230+
function _getPromiseState<T>(p: Promise<T>): PromiseState<T, Error> {
231+
state = getPromiseState(p) as PromiseState<string, Error>;
232+
return state as PromiseState<T, Error>;
233+
}
234+
let counter = 0;
235+
function countFor(_result: unknown) {
236+
return ++counter;
237+
}
238+
239+
await this.render(
240+
<template>
241+
{{#let (_getPromiseState promise) as |state|}}
242+
{{state.result}}<br />Count:
243+
{{countFor state.result}}
244+
{{/let}}
245+
</template>
246+
);
247+
248+
assert.equal(this.element.textContent?.trim(), 'Our DataCount:\n 1');
249+
await settled();
250+
251+
assert.equal(this.element.textContent?.trim(), 'Our DataCount:\n 1');
252+
});
253+
254+
test('it unwraps promise-proxies that utilize the secret symbol for error states', async function (this: RenderingTestContext, assert) {
255+
const _promise = Promise.resolve().then(() => {
256+
throw new Error('Our Error');
257+
});
258+
const promise = new PromiseProxy(_promise);
259+
260+
try {
261+
getPromiseState(promise);
262+
await promise;
263+
} catch {
264+
// do nothing
265+
}
266+
267+
let state: PromiseState<string, Error>;
268+
function _getPromiseState<T>(p: Promise<T>): PromiseState<T, Error> {
269+
state = getPromiseState(p) as PromiseState<string, Error>;
270+
return state as PromiseState<T, Error>;
271+
}
272+
let counter = 0;
273+
function countFor(_result: unknown, _error: unknown) {
274+
return ++counter;
275+
}
276+
277+
await this.render(
278+
<template>
279+
{{#let (_getPromiseState promise) as |state|}}
280+
{{#if state.isPending}}
281+
Pending
282+
{{else if state.isError}}
283+
{{state.error.message}}
284+
{{else if state.isSuccess}}
285+
Invalid Success Reached
286+
{{/if}}
287+
<br />Count:
288+
{{countFor state.result state.error}}{{/let}}
289+
</template>
290+
);
291+
292+
assert.equal(state!.result, null);
293+
assert.true(state!.error instanceof Error);
294+
assert.equal((state!.error as Error | undefined)?.message, 'Our Error');
295+
assert.equal(counter, 1);
296+
assert.equal(this.element.textContent?.trim(), 'Our Error\n Count:\n 1');
297+
await rerender();
298+
assert.equal(state!.result, null);
299+
assert.true(state!.error instanceof Error);
300+
assert.equal((state!.error as Error | undefined)?.message, 'Our Error');
301+
assert.equal(counter, 1);
302+
assert.equal(this.element.textContent?.trim(), 'Our Error\n Count:\n 1');
303+
assert.strictEqual(state, getPromiseState(_promise));
304+
});
305+
306+
test('it unwraps promise-proxies that utilize the secret symbol for success states', async function (this: RenderingTestContext, assert) {
307+
const _promise = Promise.resolve().then(() => 'Our Data');
308+
const promise = new PromiseProxy(_promise);
309+
getPromiseState(promise);
310+
await promise;
311+
312+
let state: PromiseState<string, Error>;
313+
function _getPromiseState<T>(p: Promise<T>): PromiseState<T, Error> {
314+
state = getPromiseState(p) as PromiseState<string, Error>;
315+
return state as PromiseState<T, Error>;
316+
}
317+
let counter = 0;
318+
function countFor(_result: unknown) {
319+
return ++counter;
320+
}
321+
322+
await this.render(
323+
<template>
324+
{{#let (_getPromiseState promise) as |state|}}
325+
{{state.result}}<br />Count:
326+
{{countFor state.result}}
327+
{{/let}}
328+
</template>
329+
);
330+
331+
assert.equal(this.element.textContent?.trim(), 'Our DataCount:\n 1');
332+
await settled();
333+
334+
assert.equal(this.element.textContent?.trim(), 'Our DataCount:\n 1');
335+
assert.strictEqual(state, getPromiseState(_promise));
336+
});
199337
});

0 commit comments

Comments
 (0)