diff --git a/src/ObservableObject.ts b/src/ObservableObject.ts index b81d535ce..f377ede92 100644 --- a/src/ObservableObject.ts +++ b/src/ObservableObject.ts @@ -1,3 +1,4 @@ +import { createObservable } from './createObservable'; import { beginBatch, endBatch, notify } from './batching'; import { checkActivate, @@ -25,7 +26,13 @@ import { isPrimitive, isPromise, } from './is'; -import type { ChildNodeValue, NodeValue, PromiseInfo, TrackingType } from './observableInterfaces'; +import type { + ChildNodeValue, + NodeValue, + ObservableObject, + ObservableState, + TrackingType, +} from './observableInterfaces'; import { onChange } from './onChange'; import { updateTracking } from './tracking'; @@ -310,12 +317,12 @@ function updateNodes(parent: NodeValue, obj: Record | Array | und return retValue ?? false; } -export function getProxy(node: NodeValue, p?: string) { +export function getProxy(node: NodeValue, p?: string): ObservableObject { // Get the child node if p prop if (p !== undefined) node = getChildNode(node, p); // Create a proxy if not already cached and return it - return node.proxy || (node.proxy = new Proxy(node, proxyHandler)); + return (node.proxy || (node.proxy = new Proxy(node, proxyHandler))) as ObservableObject; } const proxyHandler: ProxyHandler = { @@ -445,6 +452,10 @@ const proxyHandler: ProxyHandler = { return fnOrComputed; } + if (p === 'state' && node.state) { + return node.state; + } + // Return an observable proxy to the property return getProxy(node, p); }, @@ -719,13 +730,22 @@ function updateNodesAndNotify( } export function extractPromise(node: NodeValue, value: Promise) { - (value as PromiseInfo).status = 'pending'; + if (!node.state) { + node.state = createObservable( + { + isLoaded: false, + }, + false, + getProxy, + ) as ObservableObject; + } value .then((value) => { set(node, value); + node.state!.isLoaded.set(true); }) .catch((error) => { - set(node, { error, status: 'rejected' } as PromiseInfo); + node.state!.error.set(error); }); } diff --git a/src/observableInterfaces.ts b/src/observableInterfaces.ts index b72ac1f34..27a947776 100644 --- a/src/observableInterfaces.ts +++ b/src/observableInterfaces.ts @@ -103,7 +103,7 @@ type NonPrimitiveKeys = Pick = T[K] extends ObservableReadable ? T[K] : T[K] extends Promise - ? Observable + ? Observable : T[K] extends Function ? T[K] : T[K] extends ObservableProxyTwoWay @@ -292,13 +292,17 @@ export interface ObservablePersistRemoteFunctions { params: ObservablePersistRemoteSetParams, ): Promise; } - -export interface ObservablePersistState { - isLoadedLocal: boolean; +export interface ObservableState { isLoaded: boolean; + error?: Error; +} +export interface WithState { + state?: ObservableState; +} +export interface ObservablePersistState extends ObservableState { + isLoadedLocal: boolean; isEnabledLocal: boolean; isEnabledRemote: boolean; - error?: Error; dateModified?: number; clearLocal: () => Promise; sync: () => Promise; @@ -440,6 +444,7 @@ interface BaseNodeValue { parentOther?: NodeValue; functions?: Map>; lazy?: boolean; + state?: Observable; } export interface RootNodeValue extends BaseNodeValue { @@ -489,7 +494,3 @@ export type ObservableProxyTwoWay, T2> = { } & ObservableBaseFns & { [symbolGetNode]: NodeValue; }; -export type PromiseInfo = { - error?: any; - status?: 'pending' | 'rejected'; -}; diff --git a/tests/tests.test.ts b/tests/tests.test.ts index c3f039bb1..92bda56b4 100644 --- a/tests/tests.test.ts +++ b/tests/tests.test.ts @@ -2137,43 +2137,44 @@ describe('Promise values', () => { const promise = Promise.resolve(10); const obs = observable({ promise }); expect(obs.promise).resolves.toEqual(10); - expect(obs.promise.status.get()).toEqual('pending'); + expect(obs.promise.state.isLoaded.get()).toEqual(false); await promise; expect(obs.promise.get()).toEqual(10); + expect(obs.promise.state.isLoaded.get()).toEqual(true); }); test('Promise child works when set later', async () => { const obs = observable({ promise: undefined as unknown as Promise }); let resolver: (value: number) => void; const promise = new Promise((resolve) => (resolver = resolve)); - expect(obs.promise.status.get()).toEqual(undefined); obs.promise.set(promise); - expect(obs.promise.status.get()).toEqual('pending'); + expect(obs.promise.state.isLoaded.get()).toEqual(false); // @ts-expect-error Fake error resolver(10); await promise; expect(obs.promise.get()).toEqual(10); + expect(obs.promise.state.isLoaded.get()).toEqual(true); }); test('Promise child works when assigned later', async () => { const obs = observable({ promise: undefined as unknown as Promise }); let resolver: (value: number) => void; const promise = new Promise((resolve) => (resolver = resolve)); - expect(obs.promise.status.get()).toEqual(undefined); obs.assign({ promise }); - expect(obs.promise.status.get()).toEqual('pending'); + expect(obs.promise.state.isLoaded.get()).toEqual(false); // @ts-expect-error Fake error resolver(10); await promise; expect(obs.promise.get()).toEqual(10); + expect(obs.promise.state.isLoaded.get()).toEqual(true); }); test('Promise object becomes value', async () => { const promise = Promise.resolve({ child: 10 }); const obs = observable(promise); - expect(obs.get().status).toEqual('pending'); - expect(obs.status.get()).toEqual('pending'); + expect(obs.state.isLoaded.get()).toEqual(false); await promise; expect(obs.get()).toEqual({ child: 10 }); expect(obs.child.get()).toEqual(10); + expect(obs.state.isLoaded.get()).toEqual(true); }); test('Promise primitive becomes value', async () => { const promise = Promise.resolve(10); @@ -2197,14 +2198,16 @@ describe('Promise values', () => { }); test('Promise value in set is error if it rejects', async () => { const promise = Promise.reject('test'); - const obs = observable<{ promise: number | undefined }>({ promise: undefined }); + const obs = observable<{ + promise: Promise; + }>({ promise: undefined as any }); obs.promise.set(promise); try { await promise; } catch { await promiseTimeout(0); } - expect(obs.promise.get()).toEqual({ error: 'test', status: 'rejected' }); + expect(obs.promise.state.error.get()).toEqual('test'); }); test('when callback works with promises', async () => { let resolver: (value: number) => void; @@ -2238,7 +2241,7 @@ describe('Promise values', () => { await promise; // Still pending because it was not activated expect(obs.promise).resolves.toEqual(10); - expect(obs.promise.status.get()).toEqual('pending'); + expect(obs.promise.state.isLoaded.get()).toEqual(false); // This get activates it but it takes a frame for it to equal the value expect(obs.promise.get()).not.toEqual(10); @@ -2511,12 +2514,12 @@ describe('Observable with promise', () => { resolver = resolve; }); const obs = observable(promise); - expect(obs.get().status).toEqual('pending'); + expect(obs.state.isLoaded.get()).toEqual(false); if (resolver) { resolver(10); } await promiseTimeout(0); - expect(obs.get().status).toEqual(undefined); + expect(obs.state.isLoaded.get()).toEqual(true); expect(obs.get()).toEqual(10); }); test('when with promise observable', async () => { @@ -2526,7 +2529,7 @@ describe('Observable with promise', () => { }); const obs = observable(promise); - expect(obs.get().status).toEqual('pending'); + expect(obs.state.isLoaded.get()).toEqual(false); const fn = jest.fn(); when(() => obs.get() === 10, fn); @@ -2538,6 +2541,7 @@ describe('Observable with promise', () => { await promiseTimeout(1000); expect(fn).toHaveBeenCalled(); + expect(obs.state.isLoaded.get()).toEqual(true); }); test('recursive batches prevented', async () => { let isInInnerBatch = false;