Skip to content
This repository was archived by the owner on Oct 16, 2024. It is now read-only.

Commit 9acb5b4

Browse files
authored
Merge pull request #220 from leopf/master
synchronously recompute value after state change
2 parents f563d0c + 963a7f2 commit 9acb5b4

File tree

4 files changed

+140
-56
lines changed

4 files changed

+140
-56
lines changed

packages/core/src/computed/computed.ts

Lines changed: 105 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
State,
66
StateConfigInterface,
77
StateIngestConfigInterface,
8+
StateObserver,
89
} from '../state';
910
import { Observer } from '../runtime';
1011
import { ComputedTracker } from './computed.tracker';
@@ -15,8 +16,19 @@ export class Computed<
1516
> extends State<ComputedValueType> {
1617
public config: ComputedConfigInterface;
1718

19+
// Caches if the compute function is async
20+
private computeFunctionIsAsync!: boolean;
21+
1822
// Function to compute the Computed Class value
19-
public computeFunction: ComputeFunctionType<ComputedValueType>;
23+
private _computeFunction!: ComputeFunctionType<ComputedValueType>;
24+
public get computeFunction() : ComputeFunctionType<ComputedValueType> {
25+
return this._computeFunction;
26+
}
27+
public set computeFunction(v : ComputeFunctionType<ComputedValueType>) {
28+
this._computeFunction = v;
29+
this.computeFunctionIsAsync = isAsyncFunction(v);
30+
}
31+
2032
// All dependencies the Computed Class depends on (including hardCoded and automatically detected dependencies)
2133
public deps: Set<Observer> = new Set();
2234
// Only hardCoded dependencies the Computed Class depends on
@@ -60,12 +72,13 @@ export class Computed<
6072
dependents: config.dependents,
6173
}
6274
);
75+
this.computeFunction = computeFunction;
76+
6377
config = defineConfig(config, {
6478
computedDeps: [],
65-
autodetect: !isAsyncFunction(computeFunction),
79+
autodetect: !this.computeFunctionIsAsync,
6680
});
6781
this.agileInstance = () => agileInstance;
68-
this.computeFunction = computeFunction;
6982
this.config = {
7083
autodetect: config.autodetect as any,
7184
};
@@ -86,6 +99,64 @@ export class Computed<
8699
this.recompute({ autodetect: config.autodetect, overwrite: true });
87100
}
88101

102+
/**
103+
* synchronously computes the value
104+
*
105+
* @param config ComputeConfigInterface
106+
* @returns
107+
*/
108+
private computeSync(config: ComputeConfigInterface = {}): ComputedValueType {
109+
config = defineConfig(config, {
110+
autodetect: this.config.autodetect,
111+
});
112+
113+
// Start auto tracking of Observers on which the computeFunction might depend
114+
if (config.autodetect) ComputedTracker.track();
115+
116+
const computeFunction = this.computeFunction as SyncComputeFunctionType<ComputedValueType>;
117+
const computedValue = computeFunction();
118+
119+
// Handle auto tracked Observers
120+
if (config.autodetect) {
121+
const foundDeps = ComputedTracker.getTrackedObservers();
122+
123+
// Clean up old dependencies
124+
this.deps.forEach((observer) => {
125+
if (
126+
!foundDeps.includes(observer) &&
127+
!this.hardCodedDeps.includes(observer)
128+
) {
129+
this.deps.delete(observer);
130+
observer.removeDependent(this.observers['value']);
131+
}
132+
});
133+
134+
// Make this Observer depend on the newly found dep Observers
135+
foundDeps.forEach((observer) => {
136+
if (!this.deps.has(observer)) {
137+
this.deps.add(observer);
138+
observer.addDependent(this.observers['value']);
139+
}
140+
});
141+
}
142+
143+
return computedValue;
144+
}
145+
146+
/**
147+
* asynchronously computes the value
148+
*
149+
* @param config ComputeConfigInterface
150+
* @returns
151+
*/
152+
private async computeAsync(config: ComputeConfigInterface = {}): Promise<ComputedValueType> {
153+
config = defineConfig(config, {
154+
autodetect: this.config.autodetect,
155+
});
156+
157+
return this.computeFunction();
158+
}
159+
89160
/**
90161
* Forces a recomputation of the cached value with the compute function.
91162
*
@@ -98,12 +169,31 @@ export class Computed<
98169
config = defineConfig(config, {
99170
autodetect: false,
100171
});
101-
this.compute({ autodetect: config.autodetect }).then((result) => {
102-
this.observers['value'].ingestValue(result, config);
103-
});
172+
173+
this.computeAndIngest(this.observers['value'], config, { autodetect: config.autodetect });
174+
104175
return this;
105176
}
106177

178+
/**
179+
* Recomputes value and ingests it into the observer
180+
*
181+
* @public
182+
* @param observer - StateObserver<ComputedValueType> to ingest value into
183+
* @param ingestConfig - Configuration object
184+
*/
185+
public computeAndIngest(observer: StateObserver<ComputedValueType>, ingestConfig: StateIngestConfigInterface, computeConfig: ComputeConfigInterface = {}) {
186+
if (this.computeFunctionIsAsync) {
187+
this.computeAsync(computeConfig).then((result) => {
188+
observer.ingestValue(result, ingestConfig);
189+
});
190+
}
191+
else {
192+
const result = this.computeSync(computeConfig);
193+
observer.ingestValue(result, ingestConfig);
194+
}
195+
}
196+
107197
/**
108198
* Assigns a new function to the Computed Class for computing its value.
109199
*
@@ -165,46 +255,19 @@ export class Computed<
165255
public async compute(
166256
config: ComputeConfigInterface = {}
167257
): Promise<ComputedValueType> {
168-
config = defineConfig(config, {
169-
autodetect: this.config.autodetect,
170-
});
171-
172-
// Start auto tracking of Observers on which the computeFunction might depend
173-
if (config.autodetect) ComputedTracker.track();
174-
175-
const computedValue = this.computeFunction();
176-
177-
// Handle auto tracked Observers
178-
if (config.autodetect) {
179-
const foundDeps = ComputedTracker.getTrackedObservers();
180-
181-
// Clean up old dependencies
182-
this.deps.forEach((observer) => {
183-
if (
184-
!foundDeps.includes(observer) &&
185-
!this.hardCodedDeps.includes(observer)
186-
) {
187-
this.deps.delete(observer);
188-
observer.removeDependent(this.observers['value']);
189-
}
190-
});
191-
192-
// Make this Observer depend on the newly found dep Observers
193-
foundDeps.forEach((observer) => {
194-
if (!this.deps.has(observer)) {
195-
this.deps.add(observer);
196-
observer.addDependent(this.observers['value']);
197-
}
198-
});
258+
if (this.computeFunctionIsAsync) {
259+
return this.computeAsync(config);
260+
}
261+
else {
262+
return this.computeSync(config);
199263
}
200-
201-
return computedValue;
202264
}
203265
}
204266

205-
export type ComputeFunctionType<ComputedValueType = any> = () =>
206-
| ComputedValueType
207-
| Promise<ComputedValueType>;
267+
export type SyncComputeFunctionType<ComputedValueType = any> = () => ComputedValueType;
268+
export type AsyncComputeFunctionType<ComputedValueType = any> = () => Promise<ComputedValueType>;
269+
270+
export type ComputeFunctionType<ComputedValueType = any> = SyncComputeFunctionType<ComputedValueType> | AsyncComputeFunctionType<ComputedValueType>;
208271

209272
export interface CreateComputedConfigInterface<ComputedValueType = any>
210273
extends StateConfigInterface {

packages/core/src/state/state.observer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from './state.runtime.job';
2020
import { SideEffectInterface, State } from './state';
2121
import { logCodeManager } from '../logCodeManager';
22+
import { Computed } from '../computed';
2223

2324
export class StateObserver<ValueType = any> extends Observer<ValueType> {
2425
// State the Observer belongs to
@@ -65,9 +66,8 @@ export class StateObserver<ValueType = any> extends Observer<ValueType> {
6566
const state = this.state() as any;
6667

6768
if (state.isComputed) {
68-
state.compute().then((result) => {
69-
this.ingestValue(result, config);
70-
});
69+
const computedState = state as Computed;
70+
computedState.computeAndIngest(this, config);
7171
} else {
7272
this.ingestValue(state.nextStateValue, config);
7373
}

packages/core/tests/unit/computed/computed.test.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
Observer,
66
State,
77
ComputedTracker,
8+
createComputed,
9+
createState,
810
} from '../../../src';
911
import * as Utils from '../../../src/utils';
1012
import { LogMock } from '../../helper/logMock';
@@ -110,10 +112,10 @@ describe('Computed Tests', () => {
110112
expect(computed._key).toBe('coolComputed');
111113
expect(computed.isSet).toBeFalsy();
112114
expect(computed.isPlaceholder).toBeFalsy();
113-
expect(computed.initialStateValue).toBe('initialValue');
114-
expect(computed._value).toBe('initialValue');
115-
expect(computed.previousStateValue).toBe('initialValue');
116-
expect(computed.nextStateValue).toBe('initialValue');
115+
expect(computed.initialStateValue).toBe('computedValue'); // must be "computedValue" since the value was computed in the constructor with overwrite=true
116+
expect(computed._value).toBe('computedValue');
117+
expect(computed.previousStateValue).toBe('computedValue');
118+
expect(computed.nextStateValue).toBe('computedValue');
117119
expect(computed.observers['value']).toBeInstanceOf(StateObserver);
118120
expect(Array.from(computed.observers['value'].dependents)).toStrictEqual([
119121
dummyObserver1,
@@ -155,6 +157,25 @@ describe('Computed Tests', () => {
155157
expect(computed.sideEffects).toStrictEqual({});
156158
});
157159

160+
it('should synchronously update the computed value', () => {
161+
const counter = createState(1, {
162+
agileInstance: dummyAgile
163+
});
164+
const timesTwo = createComputed(() => {
165+
return counter.value * 2;
166+
}, {
167+
agileInstance: dummyAgile
168+
});
169+
170+
expect(counter.value).toBe(1);
171+
expect(timesTwo.value).toBe(2);
172+
173+
counter.set(3);
174+
175+
expect(counter.value).toBe(3);
176+
expect(timesTwo.value).toBe(6);
177+
});
178+
158179
describe('Computed Function Tests', () => {
159180
let computed: Computed;
160181
const dummyComputeFunction = jest.fn(() => 'computedValue');
@@ -169,11 +190,11 @@ describe('Computed Tests', () => {
169190
});
170191

171192
it('should ingest Computed Class into the Runtime (default config)', async () => {
172-
computed.compute = jest.fn(() => Promise.resolve('jeff'));
193+
(computed as any).computeSync = jest.fn(() => 'jeff');
173194

174195
computed.recompute();
175196

176-
expect(computed.compute).toHaveBeenCalledWith({ autodetect: false });
197+
expect((computed as any).computeSync).toHaveBeenCalledWith({ autodetect: false });
177198
await waitForExpect(() => {
178199
expect(computed.observers['value'].ingestValue).toHaveBeenCalledWith(
179200
'jeff',
@@ -185,7 +206,7 @@ describe('Computed Tests', () => {
185206
});
186207

187208
it('should ingest Computed Class into the Runtime (specific config)', async () => {
188-
computed.compute = jest.fn(() => Promise.resolve('jeff'));
209+
(computed as any).computeSync = jest.fn(() => 'jeff');
189210

190211
computed.recompute({
191212
autodetect: true,
@@ -197,7 +218,7 @@ describe('Computed Tests', () => {
197218
key: 'jeff',
198219
});
199220

200-
expect(computed.compute).toHaveBeenCalledWith({ autodetect: true });
221+
expect((computed as any).computeSync).toHaveBeenCalledWith({ autodetect: true });
201222
await waitForExpect(() => {
202223
expect(computed.observers['value'].ingestValue).toHaveBeenCalledWith(
203224
'jeff',

packages/core/tests/unit/state/state.observer.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,13 @@ describe('StateObserver Tests', () => {
143143
"should call 'ingestValue' with computed value " +
144144
'if Observer belongs to a Computed State (default config)',
145145
async () => {
146-
dummyComputed.compute = jest.fn(() =>
147-
Promise.resolve('computedValue')
146+
(dummyComputed as any).computeSync = jest.fn(() =>
147+
'computedValue'
148148
);
149149

150150
computedObserver.ingest();
151151

152-
expect(dummyComputed.compute).toHaveBeenCalled();
152+
expect((dummyComputed as any).computeSync).toHaveBeenCalled();
153153
await waitForExpect(() => {
154154
expect(computedObserver.ingestValue).toHaveBeenCalledWith(
155155
'computedValue',

0 commit comments

Comments
 (0)