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

synchronously recompute value after state change #220

Merged
merged 1 commit into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 105 additions & 42 deletions packages/core/src/computed/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
State,
StateConfigInterface,
StateIngestConfigInterface,
StateObserver,
} from '../state';
import { Observer } from '../runtime';
import { ComputedTracker } from './computed.tracker';
Expand All @@ -15,8 +16,19 @@ export class Computed<
> extends State<ComputedValueType> {
public config: ComputedConfigInterface;

// Caches if the compute function is async
private computeFunctionIsAsync!: boolean;

// Function to compute the Computed Class value
public computeFunction: ComputeFunctionType<ComputedValueType>;
private _computeFunction!: ComputeFunctionType<ComputedValueType>;
public get computeFunction() : ComputeFunctionType<ComputedValueType> {
return this._computeFunction;
}
public set computeFunction(v : ComputeFunctionType<ComputedValueType>) {
this._computeFunction = v;
this.computeFunctionIsAsync = isAsyncFunction(v);
}

// All dependencies the Computed Class depends on (including hardCoded and automatically detected dependencies)
public deps: Set<Observer> = new Set();
// Only hardCoded dependencies the Computed Class depends on
Expand Down Expand Up @@ -60,12 +72,13 @@ export class Computed<
dependents: config.dependents,
}
);
this.computeFunction = computeFunction;

config = defineConfig(config, {
computedDeps: [],
autodetect: !isAsyncFunction(computeFunction),
autodetect: !this.computeFunctionIsAsync,
});
this.agileInstance = () => agileInstance;
this.computeFunction = computeFunction;
this.config = {
autodetect: config.autodetect as any,
};
Expand All @@ -86,6 +99,64 @@ export class Computed<
this.recompute({ autodetect: config.autodetect, overwrite: true });
}

/**
* synchronously computes the value
*
* @param config ComputeConfigInterface
* @returns
*/
private computeSync(config: ComputeConfigInterface = {}): ComputedValueType {
config = defineConfig(config, {
autodetect: this.config.autodetect,
});

// Start auto tracking of Observers on which the computeFunction might depend
if (config.autodetect) ComputedTracker.track();

const computeFunction = this.computeFunction as SyncComputeFunctionType<ComputedValueType>;
const computedValue = computeFunction();

// Handle auto tracked Observers
if (config.autodetect) {
const foundDeps = ComputedTracker.getTrackedObservers();

// Clean up old dependencies
this.deps.forEach((observer) => {
if (
!foundDeps.includes(observer) &&
!this.hardCodedDeps.includes(observer)
) {
this.deps.delete(observer);
observer.removeDependent(this.observers['value']);
}
});

// Make this Observer depend on the newly found dep Observers
foundDeps.forEach((observer) => {
if (!this.deps.has(observer)) {
this.deps.add(observer);
observer.addDependent(this.observers['value']);
}
});
}

return computedValue;
}

/**
* asynchronously computes the value
*
* @param config ComputeConfigInterface
* @returns
*/
private async computeAsync(config: ComputeConfigInterface = {}): Promise<ComputedValueType> {
config = defineConfig(config, {
autodetect: this.config.autodetect,
});

return this.computeFunction();
}

/**
* Forces a recomputation of the cached value with the compute function.
*
Expand All @@ -98,12 +169,31 @@ export class Computed<
config = defineConfig(config, {
autodetect: false,
});
this.compute({ autodetect: config.autodetect }).then((result) => {
this.observers['value'].ingestValue(result, config);
});

this.computeAndIngest(this.observers['value'], config, { autodetect: config.autodetect });

return this;
}

/**
* Recomputes value and ingests it into the observer
*
* @public
* @param observer - StateObserver<ComputedValueType> to ingest value into
* @param ingestConfig - Configuration object
*/
public computeAndIngest(observer: StateObserver<ComputedValueType>, ingestConfig: StateIngestConfigInterface, computeConfig: ComputeConfigInterface = {}) {
if (this.computeFunctionIsAsync) {
this.computeAsync(computeConfig).then((result) => {
observer.ingestValue(result, ingestConfig);
});
}
else {
const result = this.computeSync(computeConfig);
observer.ingestValue(result, ingestConfig);
}
}

/**
* Assigns a new function to the Computed Class for computing its value.
*
Expand Down Expand Up @@ -165,46 +255,19 @@ export class Computed<
public async compute(
config: ComputeConfigInterface = {}
): Promise<ComputedValueType> {
config = defineConfig(config, {
autodetect: this.config.autodetect,
});

// Start auto tracking of Observers on which the computeFunction might depend
if (config.autodetect) ComputedTracker.track();

const computedValue = this.computeFunction();

// Handle auto tracked Observers
if (config.autodetect) {
const foundDeps = ComputedTracker.getTrackedObservers();

// Clean up old dependencies
this.deps.forEach((observer) => {
if (
!foundDeps.includes(observer) &&
!this.hardCodedDeps.includes(observer)
) {
this.deps.delete(observer);
observer.removeDependent(this.observers['value']);
}
});

// Make this Observer depend on the newly found dep Observers
foundDeps.forEach((observer) => {
if (!this.deps.has(observer)) {
this.deps.add(observer);
observer.addDependent(this.observers['value']);
}
});
if (this.computeFunctionIsAsync) {
return this.computeAsync(config);
}
else {
return this.computeSync(config);
}

return computedValue;
}
}

export type ComputeFunctionType<ComputedValueType = any> = () =>
| ComputedValueType
| Promise<ComputedValueType>;
export type SyncComputeFunctionType<ComputedValueType = any> = () => ComputedValueType;
export type AsyncComputeFunctionType<ComputedValueType = any> = () => Promise<ComputedValueType>;

export type ComputeFunctionType<ComputedValueType = any> = SyncComputeFunctionType<ComputedValueType> | AsyncComputeFunctionType<ComputedValueType>;

export interface CreateComputedConfigInterface<ComputedValueType = any>
extends StateConfigInterface {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/state/state.observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from './state.runtime.job';
import { SideEffectInterface, State } from './state';
import { logCodeManager } from '../logCodeManager';
import { Computed } from '../computed';

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

if (state.isComputed) {
state.compute().then((result) => {
this.ingestValue(result, config);
});
const computedState = state as Computed;
computedState.computeAndIngest(this, config);
} else {
this.ingestValue(state.nextStateValue, config);
}
Expand Down
37 changes: 29 additions & 8 deletions packages/core/tests/unit/computed/computed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
Observer,
State,
ComputedTracker,
createComputed,
createState,
} from '../../../src';
import * as Utils from '../../../src/utils';
import { LogMock } from '../../helper/logMock';
Expand Down Expand Up @@ -110,10 +112,10 @@ describe('Computed Tests', () => {
expect(computed._key).toBe('coolComputed');
expect(computed.isSet).toBeFalsy();
expect(computed.isPlaceholder).toBeFalsy();
expect(computed.initialStateValue).toBe('initialValue');
expect(computed._value).toBe('initialValue');
expect(computed.previousStateValue).toBe('initialValue');
expect(computed.nextStateValue).toBe('initialValue');
expect(computed.initialStateValue).toBe('computedValue'); // must be "computedValue" since the value was computed in the constructor with overwrite=true
expect(computed._value).toBe('computedValue');
expect(computed.previousStateValue).toBe('computedValue');
expect(computed.nextStateValue).toBe('computedValue');
expect(computed.observers['value']).toBeInstanceOf(StateObserver);
expect(Array.from(computed.observers['value'].dependents)).toStrictEqual([
dummyObserver1,
Expand Down Expand Up @@ -155,6 +157,25 @@ describe('Computed Tests', () => {
expect(computed.sideEffects).toStrictEqual({});
});

it('should synchronously update the computed value', () => {
const counter = createState(1, {
agileInstance: dummyAgile
});
const timesTwo = createComputed(() => {
return counter.value * 2;
}, {
agileInstance: dummyAgile
});

expect(counter.value).toBe(1);
expect(timesTwo.value).toBe(2);

counter.set(3);

expect(counter.value).toBe(3);
expect(timesTwo.value).toBe(6);
});

describe('Computed Function Tests', () => {
let computed: Computed;
const dummyComputeFunction = jest.fn(() => 'computedValue');
Expand All @@ -169,11 +190,11 @@ describe('Computed Tests', () => {
});

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

computed.recompute();

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

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

computed.recompute({
autodetect: true,
Expand All @@ -197,7 +218,7 @@ describe('Computed Tests', () => {
key: 'jeff',
});

expect(computed.compute).toHaveBeenCalledWith({ autodetect: true });
expect((computed as any).computeSync).toHaveBeenCalledWith({ autodetect: true });
await waitForExpect(() => {
expect(computed.observers['value'].ingestValue).toHaveBeenCalledWith(
'jeff',
Expand Down
6 changes: 3 additions & 3 deletions packages/core/tests/unit/state/state.observer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,13 @@ describe('StateObserver Tests', () => {
"should call 'ingestValue' with computed value " +
'if Observer belongs to a Computed State (default config)',
async () => {
dummyComputed.compute = jest.fn(() =>
Promise.resolve('computedValue')
(dummyComputed as any).computeSync = jest.fn(() =>
'computedValue'
);

computedObserver.ingest();

expect(dummyComputed.compute).toHaveBeenCalled();
expect((dummyComputed as any).computeSync).toHaveBeenCalled();
await waitForExpect(() => {
expect(computedObserver.ingestValue).toHaveBeenCalledWith(
'computedValue',
Expand Down