Skip to content

Commit

Permalink
change: behavior of promises to hold on to the promise until it resol…
Browse files Browse the repository at this point in the history
…ves, and set status on it
  • Loading branch information
jmeistrich committed Aug 9, 2023
1 parent eecb7ea commit 03730ce
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 62 deletions.
12 changes: 11 additions & 1 deletion src/ObservableObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
symbolToPrimitive,
} from './globals';
import { isArray, isBoolean, isChildNodeValue, isEmpty, isFunction, isObject, isPrimitive, isPromise } from './is';
import type { ChildNodeValue, NodeValue } from './observableInterfaces';
import type { ChildNodeValue, NodeValue, PromiseInfo } from './observableInterfaces';
import { onChange } from './onChange';
import { updateTracking } from './tracking';

Expand Down Expand Up @@ -711,3 +711,13 @@ function updateNodesAndNotify(

endBatch();
}

export function extractPromise(node: NodeValue, value: Promise<any>) {
(value as PromiseInfo).status = 'pending';
value.catch((error) => {
set(node, { error, status: 'rejected' } as PromiseInfo);
});
value.then((value) => {
set(node, value);
});
}
2 changes: 1 addition & 1 deletion src/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,5 @@ export function computed<T, T2 = T>(
};
}

return obs as ObservableComputed<T>;
return obs as any;
}
29 changes: 0 additions & 29 deletions src/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ export const optimized = Symbol('optimized');
export const extraPrimitiveActivators = new Map<string | symbol, boolean>();
export const extraPrimitiveProps = new Map<string | symbol, any>();

export const __devExtractFunctionsAndComputedsNodes =
process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' ? new Set() : undefined;

export function checkActivate(node: NodeValue) {
const root = node.root;
root.activate?.();
Expand Down Expand Up @@ -181,29 +178,3 @@ export function extractFunction(
node.root.computedChildrenNeedingActivation.push(computedChildNode);
}
}

export function extractFunctionsAndComputeds(obj: Record<string, any>, node: NodeValue) {
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
if (__devExtractFunctionsAndComputedsNodes!.has(obj)) {
console.error(
'[legend-state] Circular reference detected in object. You may want to use opaqueObject to stop traversing child nodes.',
obj,
);
return false;
}
__devExtractFunctionsAndComputedsNodes!.add(obj);
}
for (const k in obj) {
const v = obj[k];
if (typeof v === 'function') {
extractFunction(node, k, v);
} else if (typeof v == 'object' && v !== null && v !== undefined) {
const childNode = getNode(v);
if (childNode?.isComputed) {
extractFunction(node, k, v, childNode);
} else if (!v[symbolOpaque]) {
extractFunctionsAndComputeds(obj[k], getChildNode(node, k));
}
}
}
}
69 changes: 51 additions & 18 deletions src/observable.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,57 @@
import { getProxy } from './ObservableObject';
import { extractPromise, getProxy } from './ObservableObject';
import { ObservablePrimitiveClass } from './ObservablePrimitive';
import { __devExtractFunctionsAndComputedsNodes, extractFunctionsAndComputeds } from './globals';
import { extractFunction, getChildNode, getNode, symbolOpaque } from './globals';
import { isActualPrimitive, isPromise } from './is';
import type {
NodeValue,
Observable,
ObservableObjectOrArray,
ObservablePrimitive,
ObservableRoot,
PromiseInfo,
} from './observableInterfaces';
import { NodeValue } from './observableInterfaces';

function createObservable<T>(value?: T | Promise<T>, makePrimitive?: true): ObservablePrimitive<T>;
export const __devExtractFunctionsAndComputedsNodes =
process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' ? new Set() : undefined;

export function extractFunctionsAndComputeds(node: NodeValue, obj: Record<string, any>) {
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
if (__devExtractFunctionsAndComputedsNodes!.has(obj)) {
console.error(
'[legend-state] Circular reference detected in object. You may want to use opaqueObject to stop traversing child nodes.',
obj,
);
return false;
}
__devExtractFunctionsAndComputedsNodes!.add(obj);
}
for (const k in obj) {
const v = obj[k];
if (isPromise(v)) {
extractPromise(getChildNode(node, k), v);
} else if (typeof v === 'function') {
extractFunction(node, k, v);
} else if (typeof v == 'object' && v !== null && v !== undefined) {
const childNode = getNode(v);
if (childNode?.isComputed) {
extractFunction(node, k, v, childNode);
} else if (!v[symbolOpaque]) {
extractFunctionsAndComputeds(getChildNode(node, k), obj[k]);
}
}
}
}

function createObservable<T>(value?: Promise<T>, makePrimitive?: true): ObservablePrimitive<T & PromiseInfo>;
function createObservable<T>(value?: T, makePrimitive?: true): ObservablePrimitive<T>;
function createObservable<T>(
value?: T | Promise<T>,
value?: Promise<T>,
makePrimitive?: boolean,
): ObservablePrimitive<T> | ObservableObjectOrArray<T> {
): ObservablePrimitive<T & PromiseInfo> | ObservableObjectOrArray<T & PromiseInfo>;
function createObservable<T>(value?: T, makePrimitive?: boolean): ObservablePrimitive<T> | ObservableObjectOrArray<T> {
const valueIsPromise = isPromise<T>(value);
const root: ObservableRoot = {
_: valueIsPromise ? undefined : value,
_: value,
};

const node: NodeValue = {
Expand All @@ -31,28 +65,27 @@ function createObservable<T>(
: (getProxy(node) as ObservableObjectOrArray<T>);

if (valueIsPromise) {
value.catch((error) => {
obs.set({ error } as any);
});
value.then((value) => {
obs.set(value);
});
extractPromise(node, value);
} else if (!prim) {
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
__devExtractFunctionsAndComputedsNodes!.clear();
}
if (value) {
extractFunctionsAndComputeds(value, node);
extractFunctionsAndComputeds(node, value);
}
}

return obs;
}

export function observable<T>(value?: T | Promise<T>): Observable<T> {
return createObservable(value) as Observable<T>;
export function observable<T>(value?: Promise<T>): Observable<T & PromiseInfo>;
export function observable<T>(value?: T): Observable<T>;
export function observable<T>(value?: T | Promise<T>): Observable<T & PromiseInfo> {
return createObservable(value) as Observable<T & PromiseInfo>;
}

export function observablePrimitive<T>(value?: T | Promise<T>): ObservablePrimitive<T> {
return createObservable<T>(value, /*makePrimitive*/ true);
export function observablePrimitive<T>(value?: Promise<T>): ObservablePrimitive<T & PromiseInfo>;
export function observablePrimitive<T>(value?: T): ObservablePrimitive<T>;
export function observablePrimitive<T>(value?: T | Promise<T>): ObservablePrimitive<T & PromiseInfo> {
return createObservable(value, /*makePrimitive*/ true) as ObservablePrimitive<T & PromiseInfo>;
}
8 changes: 7 additions & 1 deletion src/observableInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ type NonPrimitiveKeys<T> = Pick<T, { [K in keyof T]-?: T[K] extends Primitive ?

type Recurse<T, K extends keyof T, TRecurse> = T[K] extends ObservableReadable
? T[K]
: T[K] extends Function | Promise<any>
: T[K] extends Promise<infer t>
? Observable<t & PromiseInfo>
: T[K] extends Function
? T[K]
: T[K] extends ObservableProxy<infer t>
? ObservableProxy<t>
Expand Down Expand Up @@ -438,3 +440,7 @@ export type ObservableProxy<T extends Record<string, any>> = {
} & {
[symbolGetNode]: NodeValue;
};
export type PromiseInfo = {
error?: any;
status?: 'pending' | 'rejected';
};
2 changes: 1 addition & 1 deletion src/react/reactInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ export type FCReactive<P, P2> = P &
>;

export interface UseSelectorOptions {
suspend?: boolean;
suspense?: boolean;
}
13 changes: 7 additions & 6 deletions src/react/useSelector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { computeSelector, isPromise, Selector, tracking, trackSelector } from '@legendapp/state';
import { useRef } from 'react';
import React, { useRef } from 'react';
import { UseSelectorOptions } from 'src/react/reactInterfaces';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

Expand Down Expand Up @@ -73,12 +73,13 @@ export function useSelector<T>(selector: Selector<T>, options?: UseSelectorOptio
useSyncExternalStore(subscribe, getVersion, getVersion);

// Suspense support
// Note: We may want to change the throw to React.use when React updates their guidances on Suspense.
if (options?.suspend) {
if (options?.suspense) {
if (isPromise(value)) {
throw value;
} else if (value?.error) {
throw value.error;
if (React.use) {
React.use(value);
} else {
throw value;
}
}
}

Expand Down
17 changes: 12 additions & 5 deletions tests/tests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2124,14 +2124,20 @@ describe('Promise values', () => {
test('Promise child', async () => {
const promise = Promise.resolve(10);
const obs = observable({ promise });
await promise;
expect(obs.promise).resolves.toEqual(10);
expect(obs.promise.status.get()).toEqual('pending');
await promise;
expect(obs.promise.get()).toEqual(10);
});
test('Promise object becomes value', async () => {
const promise = Promise.resolve({ value: 10 });
const promise = Promise.resolve({ child: 10 });
const obs = observable(promise);

expect(obs.get().status).toEqual('pending');
expect(obs.status.get()).toEqual('pending');
await promise;
expect(obs.value.get()).toEqual(10);
expect(obs.get()).toEqual({ child: 10 });
expect(obs.child.get()).toEqual(10);
});
test('Promise primitive becomes value', async () => {
const promise = Promise.resolve(10);
Expand Down Expand Up @@ -2267,11 +2273,12 @@ describe('Observable with promise', () => {
resolver = resolve;
});
const obs = observable(promise);
expect(obs.get()).toEqual(undefined);
expect(obs.get().status).toEqual('pending');
if (resolver) {
resolver(10);
}
await promiseTimeout(0);
expect(obs.get().status).toEqual(undefined);
expect(obs.get()).toEqual(10);
});
test('when with promise observable', async () => {
Expand All @@ -2281,7 +2288,7 @@ describe('Observable with promise', () => {
});
const obs = observable(promise);

expect(obs.get()).toEqual(undefined);
expect(obs.get().status).toEqual('pending');

const fn = jest.fn();
when(() => obs.get() === 10, fn);
Expand Down

0 comments on commit 03730ce

Please sign in to comment.