Skip to content

Commit ae1b028

Browse files
authored
feat: independent producerWrapper & runc support in utils package (#117)
* support runc function and split producerWrapper * externalize runc function from core to utils package
1 parent 27eac6b commit ae1b028

File tree

13 files changed

+402
-126
lines changed

13 files changed

+402
-126
lines changed

packages/core/src/AsyncState.ts

Lines changed: 140 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
4949
private locks?: number;
5050
isEmitting?: boolean;
5151

52+
readonly producer: ProducerFunction<T, E, R>;
53+
5254

5355
//endregion
5456

@@ -81,7 +83,6 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
8183
this.abort = this.abort.bind(this);
8284
this.getState = this.getState.bind(this);
8385
this.setState = this.setState.bind(this);
84-
this.producer = this.producer.bind(this);
8586
this.subscribe = this.subscribe.bind(this);
8687
this.getPayload = this.getPayload.bind(this);
8788
this.mergePayload = this.mergePayload.bind(this);
@@ -96,6 +97,17 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
9697
this.invalidateCache = this.invalidateCache.bind(this);
9798
this.replaceProducer = this.replaceProducer.bind(this);
9899

100+
let instance = this;
101+
this.producer = producerWrapper.bind(null, {
102+
setProducerType: (type: ProducerType) => instance.producerType = type,
103+
setState: instance.setState,
104+
getState: instance.getState,
105+
instance: instance,
106+
setSuspender: (suspender: Promise<T>) => instance.suspender = suspender,
107+
replaceState: instance.replaceState.bind(instance),
108+
getProducer: () => instance.originalProducer,
109+
});
110+
99111
this._source = makeSource(this);
100112

101113
if (__DEV__) {
@@ -120,120 +132,7 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
120132
Object.assign(this.config, partialConfig);
121133
}
122134

123-
producer(
124-
props: ProducerProps<T, E, R>,
125-
indicators: RunIndicators,
126-
callbacks?: ProducerCallbacks<T, E, R>,
127-
): AbortFn {
128-
let instance = this;
129-
const currentProducer = this.originalProducer;
130-
if (!isFunction(currentProducer)) {
131-
indicators.fulfilled = true;
132-
instance.producerType = ProducerType.notProvided;
133-
instance.setState(props.args[0], props.args[1]);
134-
if (callbacks) {
135-
switch (instance.state.status) {
136-
case Status.success: {
137-
callbacks.onSuccess?.(instance.state);
138-
break;
139-
}
140-
case Status.aborted: {
141-
callbacks.onAborted?.(instance.state);
142-
break;
143-
}
144-
case Status.error: {
145-
callbacks.onError?.(instance.state);
146-
break;
147-
}
148-
}
149-
}
150-
return;
151-
}
152-
// the running promise is used to pass the status to pending and as suspender in react18+
153-
let runningPromise;
154-
// the execution value is the return of the initial producer function
155-
let executionValue;
156-
// it is important to clone to capture properties and save only serializable stuff
157-
const savedProps = cloneProducerProps(props);
158-
159-
try {
160-
executionValue = currentProducer!(props);
161-
if (indicators.aborted) {
162-
return;
163-
}
164-
} catch (e) {
165-
if (indicators.aborted) {
166-
return;
167-
}
168-
if (__DEV__) devtools.emitRunSync(instance, savedProps);
169-
indicators.fulfilled = true;
170-
let errorState = StateBuilder.error<T, E>(e, savedProps);
171-
instance.replaceState(errorState);
172-
callbacks?.onError?.(errorState);
173-
return;
174-
}
175135

176-
if (isGenerator(executionValue)) {
177-
instance.producerType = ProducerType.generator;
178-
if (__DEV__) devtools.emitRunGenerator(instance, savedProps);
179-
// generatorResult is either {done, value} or a promise
180-
let generatorResult;
181-
try {
182-
generatorResult = wrapStartedGenerator(executionValue, props, indicators);
183-
} catch (e) {
184-
indicators.fulfilled = true;
185-
let errorState = StateBuilder.error<T, E>(e, savedProps);
186-
instance.replaceState(errorState);
187-
callbacks?.onError?.(errorState);
188-
return;
189-
}
190-
if (generatorResult.done) {
191-
indicators.fulfilled = true;
192-
let successState = StateBuilder.success(generatorResult.value, savedProps);
193-
instance.replaceState(successState);
194-
callbacks?.onSuccess?.(successState);
195-
return;
196-
} else {
197-
runningPromise = generatorResult;
198-
instance.suspender = runningPromise;
199-
instance.replaceState(StateBuilder.pending(savedProps));
200-
}
201-
} else if (isPromise(executionValue)) {
202-
instance.producerType = ProducerType.promise;
203-
if (__DEV__) devtools.emitRunPromise(instance, savedProps);
204-
runningPromise = executionValue;
205-
instance.suspender = runningPromise;
206-
instance.replaceState(StateBuilder.pending(savedProps));
207-
} else { // final value
208-
if (__DEV__) devtools.emitRunSync(instance, savedProps);
209-
indicators.fulfilled = true;
210-
instance.producerType = ProducerType.sync;
211-
let successState = StateBuilder.success(executionValue, savedProps);
212-
instance.replaceState(successState);
213-
callbacks?.onSuccess?.(successState);
214-
return;
215-
}
216-
217-
runningPromise
218-
.then(stateData => {
219-
let aborted = indicators.aborted;
220-
if (!aborted) {
221-
indicators.fulfilled = true;
222-
let successState = StateBuilder.success(stateData, savedProps);
223-
instance.replaceState(successState);
224-
callbacks?.onSuccess?.(successState);
225-
}
226-
})
227-
.catch(stateError => {
228-
let aborted = indicators.aborted;
229-
if (!aborted) {
230-
indicators.fulfilled = true;
231-
let errorState = StateBuilder.error<T, E>(stateError, savedProps);
232-
instance.replaceState(errorState);
233-
callbacks?.onError?.(errorState);
234-
}
235-
});
236-
};
237136

238137
getPayload(): Record<string, any> {
239138
if (!this.payload) {
@@ -714,7 +613,133 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
714613

715614
//region AsyncState methods helpers
716615

717-
function cloneProducerProps<T, E, R>(props: ProducerProps<T, E, R>): ProducerSavedProps<T> {
616+
export type ProducerWrapperInput<T, E, R> = {
617+
setProducerType(type: ProducerType): void,
618+
setState: StateUpdater<T, E, R>,
619+
getState(): State<T, E, R>,
620+
instance?: StateInterface<T, E, R>,
621+
setSuspender(p: Promise<T>): void,
622+
replaceState(newState: State<T, E, R>, notify?: boolean),
623+
getProducer(): Producer<T, E, R> | undefined | null,
624+
}
625+
export function producerWrapper<T, E = any, R = any>(
626+
input: ProducerWrapperInput<T, E, R>,
627+
props: ProducerProps<T, E, R>,
628+
indicators: RunIndicators,
629+
callbacks?: ProducerCallbacks<T, E, R>,
630+
): AbortFn {
631+
const currentProducer = input.getProducer();
632+
if (!isFunction(currentProducer)) {
633+
indicators.fulfilled = true;
634+
input.setProducerType(ProducerType.notProvided);
635+
input.setState(props.args[0], props.args[1]);
636+
637+
if (callbacks) {
638+
let currentState = input.getState();
639+
switch (currentState.status) {
640+
case Status.success: {
641+
callbacks.onSuccess?.(currentState);
642+
break;
643+
}
644+
case Status.aborted: {
645+
callbacks.onAborted?.(currentState);
646+
break;
647+
}
648+
case Status.error: {
649+
callbacks.onError?.(currentState);
650+
break;
651+
}
652+
}
653+
}
654+
return;
655+
}
656+
// the running promise is used to pass the status to pending and as suspender in react18+
657+
let runningPromise;
658+
// the execution value is the return of the initial producer function
659+
let executionValue;
660+
// it is important to clone to capture properties and save only serializable stuff
661+
const savedProps = cloneProducerProps(props);
662+
663+
try {
664+
executionValue = currentProducer!(props);
665+
if (indicators.aborted) {
666+
return;
667+
}
668+
} catch (e) {
669+
if (indicators.aborted) {
670+
return;
671+
}
672+
if (__DEV__ && input.instance) devtools.emitRunSync(input.instance, savedProps);
673+
indicators.fulfilled = true;
674+
let errorState = StateBuilder.error<T, E>(e, savedProps);
675+
input.replaceState(errorState);
676+
callbacks?.onError?.(errorState);
677+
return;
678+
}
679+
680+
if (isGenerator(executionValue)) {
681+
input.setProducerType(ProducerType.generator);
682+
if (__DEV__ && input.instance) devtools.emitRunGenerator(input.instance, savedProps);
683+
// generatorResult is either {done, value} or a promise
684+
let generatorResult;
685+
try {
686+
generatorResult = wrapStartedGenerator(executionValue, props, indicators);
687+
} catch (e) {
688+
indicators.fulfilled = true;
689+
let errorState = StateBuilder.error<T, E>(e, savedProps);
690+
input.replaceState(errorState);
691+
callbacks?.onError?.(errorState);
692+
return;
693+
}
694+
if (generatorResult.done) {
695+
indicators.fulfilled = true;
696+
let successState = StateBuilder.success(generatorResult.value, savedProps);
697+
input.replaceState(successState);
698+
callbacks?.onSuccess?.(successState);
699+
return;
700+
} else {
701+
runningPromise = generatorResult;
702+
input.setSuspender(runningPromise);
703+
input.replaceState(StateBuilder.pending(savedProps));
704+
}
705+
} else if (isPromise(executionValue)) {
706+
input.setProducerType(ProducerType.promise);
707+
if (__DEV__ && input.instance) devtools.emitRunPromise(input.instance, savedProps);
708+
runningPromise = executionValue;
709+
input.setSuspender(runningPromise);
710+
input.replaceState(StateBuilder.pending(savedProps));
711+
} else { // final value
712+
if (__DEV__ && input.instance) devtools.emitRunSync(input.instance, savedProps);
713+
indicators.fulfilled = true;
714+
input.setProducerType(ProducerType.sync);
715+
let successState = StateBuilder.success(executionValue, savedProps);
716+
input.replaceState(successState);
717+
callbacks?.onSuccess?.(successState);
718+
return;
719+
}
720+
721+
runningPromise
722+
.then(stateData => {
723+
let aborted = indicators.aborted;
724+
if (!aborted) {
725+
indicators.fulfilled = true;
726+
let successState = StateBuilder.success(stateData, savedProps);
727+
input.replaceState(successState);
728+
callbacks?.onSuccess?.(successState);
729+
}
730+
})
731+
.catch(stateError => {
732+
let aborted = indicators.aborted;
733+
if (!aborted) {
734+
indicators.fulfilled = true;
735+
let errorState = StateBuilder.error<T, E>(stateError, savedProps);
736+
input.replaceState(errorState);
737+
callbacks?.onError?.(errorState);
738+
}
739+
});
740+
}
741+
742+
export function cloneProducerProps<T, E, R>(props: ProducerProps<T, E, R>): ProducerSavedProps<T> {
718743
const output: ProducerSavedProps<T> = {
719744
lastSuccess: shallowClone(props.lastSuccess),
720745
payload: props.payload,

packages/core/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export {
1010
RunEffect,
1111
standaloneProducerEffectsCreator,
1212
readSource,
13+
producerWrapper,
14+
cloneProducerProps,
1315
} from "./AsyncState";
1416

1517
export type {
@@ -36,8 +38,10 @@ export type {
3638
ProducerFunction,
3739
OnCacheLoadProps,
3840
ProducerRunInput,
41+
ProducerCallbacks,
3942
ProducerRunConfig,
4043
ProducerSavedProps,
44+
ProducerWrapperInput,
4145
StateFunctionUpdater,
4246
AsyncStateKeyOrSource,
4347
StateBuilderInterface,
@@ -69,5 +73,6 @@ export type {
6973

7074
export {
7175
DevtoolsEvent, DevtoolsRequest, DevtoolsJournalEvent
72-
} from "./devtools/index"
76+
} from "./devtools/index";
77+
7378
export {default as devtools} from "./devtools/Devtools"

packages/core/src/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ export function defaultHash(args?: any[], payload?: {[id: string]: any} | null):
1515
return JSON.stringify({args, payload});
1616
}
1717

18+
//region useAsyncState value construction
19+
export function noop(): void {
20+
// that's a noop fn
21+
}
22+
1823
export function didNotExpire<T, E, R>(cachedState: CachedState<T, E, R>) {
1924
const {addedAt, deadline} = cachedState;
2025

packages/docs/docs/api/99-concurrent-mode.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,11 @@ This can be useful if you want to pass the read function to a child component
3939
to suspend only itself.
4040

4141
## Tearing
42-
The library doesn't include `useRef` at all, and schedules a render everytime
43-
the state changes, so it is immune to all tearing problems.
44-
45-
Just don't make it tear with own custom selectors.
42+
The library performs an optimistic lock, so everytime a subscribed component
43+
renders, it will check the current version in the instance, if different, it
44+
will schedule an update.
4645

4746
## Transitions
4847
If your producer isn't used with a `runEffect`, then the transition to the
4948
`pending` state is scheduled immediately in a sync way. So you may benefit
50-
from `useTransition` ans `startTransition` APIs.
49+
from `useTransition` and `startTransition` APIs.

packages/react-async-states-utils/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"private": false,
33
"license": "MIT",
4-
"version": "1.0.0-rc-2",
4+
"version": "1.0.0-rc-3",
55
"author": "incepter",
66
"sideEffects": false,
77
"main": "dist/umd/index",
@@ -20,7 +20,7 @@
2020
"dev": "pnpm clean:dist && rollup -c rollup/rollup.config.dev.js -w"
2121
},
2222
"peerDependencies": {
23-
"react-async-states": ">=1.0.0-rc-11",
23+
"react-async-states": "^1.0.0-rc-18",
2424
"react": "^16.8.0 || ^17.0.2 || ^18.0.0"
2525
},
2626
"devDependencies": {

packages/react-async-states-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ export type {
1616
StateBoundaryRenderProp
1717
} from "./StateBoundary";
1818

19+
export {runc} from "./runc";
1920
export {addBooleanStatus} from "./selectors";
2021
export type {StateWithBooleanStatus} from "./selectors";

0 commit comments

Comments
 (0)