Skip to content

Commit 9f03431

Browse files
authored
Add StateHook.onChange and some cleaning(#132)
* Fix runc work with keepPending and move all callbacks to one the state update occurs * rename * Rename originalProducer to _producer * Some cleaning of AsyncState constructor * Add internal hook.onChange * Finalize hooksReturn.onChange support * Fix config.events.change undefined in statehook * Fix gitignore * Refactor
1 parent 0bbc37d commit 9f03431

File tree

11 files changed

+244
-193
lines changed

11 files changed

+244
-193
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ __tests__/async-state
1717

1818
.scannerwork
1919
memlab
20+
useQuery.ts

packages/core/src/AsyncState.ts

Lines changed: 69 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ import {
1414
import devtools from "./devtools/Devtools";
1515
import {hideStateInstanceInNewObject} from "./hide-object";
1616
import {
17+
AbortedState,
1718
AbortFn,
1819
AsyncStateSubscribeProps,
1920
CachedState,
20-
CreateSourceObject,
21+
CreateSourceObject, ErrorState,
2122
ForkConfig,
2223
HydrationData,
2324
InitialState,
@@ -30,7 +31,7 @@ import {
3031
InstanceEventHandlerType,
3132
InstanceEvents,
3233
InstanceEventType,
33-
LibraryPoolsContext,
34+
LibraryPoolsContext, PendingState,
3435
PendingTimeout,
3536
PendingUpdate,
3637
PoolInterface,
@@ -80,7 +81,7 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
8081
queue?: UpdateQueue<T, E, R>;
8182
flushing?: boolean;
8283

83-
originalProducer: Producer<T, E, R> | undefined;
84+
_producer: Producer<T, E, R> | undefined;
8485

8586
//
8687
payload?: Record<string, any>;
@@ -113,40 +114,49 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
113114
poolName?: string,
114115
) {
115116

116-
let executionContext = requestContext(config?.context);
117-
let {poolInUse, getOrCreatePool} = executionContext;
117+
let ctx = config && config.context;
118+
let {poolInUse: poolToUse, getOrCreatePool} = requestContext(ctx);
118119

119-
let poolToUse: PoolInterface = poolInUse;
120120
if (poolName) {
121121
poolToUse = getOrCreatePool(poolName);
122122
}
123123

124-
let maybeInstance = poolToUse.instances.get(key);
124+
let maybeInstance = poolToUse.instances
125+
.get(key) as AsyncState<T, E, R> | undefined;
126+
125127
if (maybeInstance) {
126128
if (__DEV__) {
127129
warnAboutAlreadyExistingSourceWithSameKey(key);
128130
}
131+
maybeInstance.replaceProducer(producer || undefined);
132+
maybeInstance.patchConfig(config);
133+
return maybeInstance;
134+
}
129135

130-
let instance = maybeInstance as AsyncState<T, E, R>;
131-
instance.replaceProducer(producer || undefined);
132-
instance.patchConfig(config);
133-
return instance;
136+
this.bindMethods();
137+
if (__DEV__) {
138+
this.journal = [];
134139
}
140+
poolToUse.set(key, this);
135141

136142
this.key = key;
137143
this.pool = poolToUse;
138144
this.uniqueId = nextUniqueId();
145+
this._source = makeSource(this);
139146
this.config = shallowClone(config);
140-
this.originalProducer = producer ?? undefined;
141-
142-
if (__DEV__) {
143-
this.journal = [];
144-
}
147+
this._producer = producer ?? undefined;
145148

146149
loadCache(this);
150+
let instance = this;
151+
this.producer = producerWrapper.bind(null, {
152+
instance: this,
153+
setState: this.setState,
154+
getProducer: () => instance._producer,
155+
replaceState: this.replaceState.bind(this),
156+
setSuspender: (suspender: Promise<T>) => instance.suspender = suspender,
157+
});
147158

148159
let maybeHydratedState = attemptHydratedState<T, E, R>(this.pool.name, this.key);
149-
150160
if (maybeHydratedState) {
151161
this.state = maybeHydratedState.state;
152162
this.payload = maybeHydratedState.payload;
@@ -168,26 +178,9 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
168178
this.lastSuccess = this.state;
169179
}
170180

171-
172-
this.bindMethods();
173-
174-
let instance = this;
175-
this.producer = producerWrapper.bind(null, {
176-
setState: instance.setState,
177-
getState: instance.getState,
178-
instance: instance,
179-
setSuspender: (suspender: Promise<T>) => instance.suspender = suspender,
180-
replaceState: instance.replaceState.bind(instance),
181-
getProducer: () => instance.originalProducer,
182-
});
183-
184-
this._source = makeSource(this);
185-
186181
if (__DEV__) {
187182
devtools.emitCreation(this);
188183
}
189-
190-
poolToUse.set(key, this);
191184
}
192185

193186
private bindMethods() {
@@ -312,7 +305,8 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
312305

313306
replaceState(
314307
newState: State<T, E, R>,
315-
notify: boolean = true
308+
notify: boolean = true,
309+
callbacks?: ProducerCallbacks<T, E, R>
316310
): void {
317311
let {config} = this;
318312
let isPending = newState.status === pending;
@@ -322,7 +316,7 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
322316
}
323317

324318
if (this.queue) {
325-
enqueueUpdate(this, newState);
319+
enqueueUpdate(this, newState, callbacks);
326320
return;
327321
}
328322

@@ -331,7 +325,7 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
331325
this.state.status === pending &&
332326
!this.flushing
333327
) {
334-
enqueueUpdate(this, newState);
328+
enqueueUpdate(this, newState, callbacks);
335329
return;
336330
}
337331

@@ -356,6 +350,7 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
356350
if (__DEV__) devtools.startUpdate(this);
357351
this.state = newState;
358352
this.version += 1;
353+
invokeChangeCallbacks(newState, callbacks);
359354
invokeInstanceEvents(this, "change");
360355
if (__DEV__) devtools.emitUpdate(this);
361356

@@ -420,15 +415,12 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
420415
return cleanup;
421416
}
422417

423-
setState(
424-
newValue: T | StateFunctionUpdater<T, E, R>,
425-
status = success,
426-
): void {
418+
setState(newValue: T | StateFunctionUpdater<T, E, R>, status: Status = success, callbacks?: ProducerCallbacks<T, E, R>): void {
427419
if (!StateBuilder[status]) {
428420
throw new Error(`Unknown status ('${status}')`);
429421
}
430422
if (this.queue) {
431-
enqueueSetState(this, newValue, status);
423+
enqueueSetState(this, newValue, status, callbacks);
432424
return;
433425
}
434426
this.willUpdate = true;
@@ -450,7 +442,7 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
450442
if (__DEV__) devtools.emitReplaceState(this, savedProps);
451443
// @ts-ignore
452444
let newState = StateBuilder[status](effectiveValue, savedProps) as State<T, E, R>;
453-
this.replaceState(newState);
445+
this.replaceState(newState, true, callbacks);
454446
this.willUpdate = false;
455447
}
456448

@@ -464,7 +456,7 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
464456
}
465457

466458
replaceProducer(newProducer: Producer<T, E, R> | undefined) {
467-
this.originalProducer = newProducer;
459+
this._producer = newProducer;
468460
}
469461

470462
invalidateCache(cacheKey?: string) {
@@ -601,7 +593,7 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
601593
if (cachedState) {
602594
if (didNotExpire(cachedState)) {
603595
if (cachedState.state !== this.state) {
604-
this.replaceState(cachedState.state);
596+
this.replaceState(cachedState.state, true, runProps);
605597
}
606598
if (__DEV__) devtools.emitRunConsumedFromCache(this, payload, execArgs);
607599
return;
@@ -692,7 +684,7 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
692684
key = `${this.key}-fork-${this.forksIndex + 1}`;
693685
}
694686

695-
const clone = new AsyncState(key, this.originalProducer, this.config, this.pool.simpleName);
687+
const clone = new AsyncState(key, this._producer, this.config, this.pool.simpleName);
696688

697689
// if something fail, no need to increment
698690
this.forksIndex += 1;
@@ -730,6 +722,25 @@ export class AsyncState<T, E, R> implements StateInterface<T, E, R> {
730722

731723
//region AsyncState methods helpers
732724

725+
function invokeChangeCallbacks<T, E, R>(
726+
state: State<T, E, R>,
727+
callbacks: ProducerCallbacks<T, E, R> | undefined
728+
) {
729+
if (!callbacks) {
730+
return;
731+
}
732+
let {onError, onAborted, onSuccess} = callbacks;
733+
if (onSuccess && state.status === success) {
734+
onSuccess(state);
735+
}
736+
if (onAborted && state.status === aborted) {
737+
onAborted(state);
738+
}
739+
if (onError && state.status === error) {
740+
onError(state);
741+
}
742+
}
743+
733744
export function shallowClone(
734745
source1,
735746
source2?
@@ -738,7 +749,9 @@ export function shallowClone(
738749
}
739750

740751
export function attemptHydratedState<T, E, R>(
741-
poolName: string, key: string): HydrationData<T, E, R> | null {
752+
poolName: string,
753+
key: string
754+
): HydrationData<T, E, R> | null {
742755
// do not attempt hydration outside server
743756
if (isServer) {
744757
return null;
@@ -871,7 +884,7 @@ function constructPropsObject<T, E, R>(
871884
return;
872885
}
873886
instance.isEmitting = true;
874-
instance.setState(updater, status);
887+
instance.setState(updater, status, callbacks);
875888
instance.isEmitting = false;
876889
}
877890

@@ -891,8 +904,7 @@ function constructPropsObject<T, E, R>(
891904
// these state updates are only with aborted status
892905
if (!instance.willUpdate) {
893906
let abortedState = StateBuilder.aborted<T, E, R>(reason, cloneProducerProps(props));
894-
instance.replaceState(abortedState);
895-
callbacks?.onAborted?.(abortedState);
907+
instance.replaceState(abortedState, true, callbacks);
896908
}
897909
}
898910

@@ -1307,8 +1319,10 @@ function getQueueTail<T, E, R>(instance: StateInterface<T, E, R>): UpdateQueue<T
13071319
export function enqueueUpdate<T, E, R>(
13081320
instance: StateInterface<T, E, R>,
13091321
newState: State<T, E, R>,
1322+
callbacks?: ProducerCallbacks<T, E, R>
13101323
) {
13111324
let update: UpdateQueue<T, E, R> = {
1325+
callbacks,
13121326
data: newState,
13131327
kind: 0,
13141328
next: null,
@@ -1330,8 +1344,10 @@ export function enqueueSetState<T, E, R>(
13301344
instance: StateInterface<T, E, R>,
13311345
newValue: T | StateFunctionUpdater<T, E, R>,
13321346
status = success,
1347+
callbacks?: ProducerCallbacks<T, E, R>,
13331348
) {
13341349
let update: UpdateQueue<T, E, R> = {
1350+
callbacks,
13351351
kind: 1,
13361352
data: {data: newValue, status},
13371353
next: null,
@@ -1384,18 +1400,18 @@ function flushUpdateQueue<T, E, R>(
13841400

13851401
instance.flushing = true;
13861402
while (current !== null) {
1387-
let {data: {status}} = current;
1403+
let {data: {status}, callbacks} = current;
13881404
let canBailoutPendingStatus = status === pending && current.next !== null;
13891405

13901406
if (canBailoutPendingStatus) {
13911407
current = current.next;
13921408
} else {
13931409
if (current.kind === 0) {
1394-
instance.replaceState(current.data);
1410+
instance.replaceState(current.data, undefined, callbacks);
13951411
}
13961412
if (current.kind === 1) {
13971413
let {data: {data, status}} = current;
1398-
instance.setState(data, status);
1414+
instance.setState(data, status, callbacks);
13991415
}
14001416
current = current.next;
14011417
}

packages/core/src/__tests__/async-state/AsyncState.fork.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('AsyncState - fork', () => {
2929
expect(forkedAsyncState.forksIndex).toBe(undefined);
3030
expect(forkedAsyncState.config).toEqual(myAsyncState.config);
3131
expect(forkedAsyncState.lastSuccess).toEqual(myAsyncState.lastSuccess);
32-
expect(forkedAsyncState.originalProducer).toBe(myAsyncState.originalProducer);
32+
expect(forkedAsyncState._producer).toBe(myAsyncState._producer);
3333

3434

3535
expect(forkedAsyncState.key).not.toBe(myAsyncState.key);

packages/core/src/__tests__/async-state/AsyncState.keeppending.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,29 @@ describe('AsyncState - keepPending', () => {
158158
expect(instance.state.status).toBe(Status.pending);
159159
expect(abortedSpy).toHaveBeenCalledTimes(1);
160160
});
161+
it('should work normally with run callbacks', async () => {
162+
// given
163+
let key = "keep-7";
164+
let onSuccess = jest.fn();
165+
let producer = timeout<number>(50, 0);
166+
let config: ProducerConfig<number> = {
167+
keepPendingForMs: 100,
168+
initialValue: 1,
169+
};
170+
171+
// when
172+
let instance = new AsyncState(key, producer, config);
173+
174+
// then
175+
instance.runc({
176+
onSuccess,
177+
});
178+
await jest.advanceTimersByTime(60);
179+
180+
expect(instance.state.status).toBe(Status.pending);
181+
expect(onSuccess).not.toHaveBeenCalled();
182+
await jest.advanceTimersByTime(40);
183+
expect(instance.state.status).toBe(Status.success);
184+
expect(onSuccess).toHaveBeenCalled();
185+
});
161186
});

0 commit comments

Comments
 (0)