Skip to content

Commit

Permalink
fix(signal): fix signal auto computed issue with storage (#132)
Browse files Browse the repository at this point in the history
  • Loading branch information
unadlib authored Apr 30, 2024
1 parent c1e1989 commit 6180e77
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/reactant-module/src/constants/reduxKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const subscriptionsKey: unique symbol = Symbol('subscriptions');
export const unsubscriptionsKey: unique symbol = Symbol('unsubscriptions');
export const stateKey: unique symbol = Symbol('state');
export const defaultStateKey: unique symbol = Symbol('defaultState');
export const signalMapKey: unique symbol = Symbol('signalMap');
export const enablePatchesKey: unique symbol = Symbol('enablePatches');
export const enableAutoComputedKey: unique symbol =
Symbol('enableAutoComputed');
Expand Down
11 changes: 10 additions & 1 deletion packages/reactant-module/src/core/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
dynamicModulesKey,
strictKey,
enableAutoComputedKey,
signalMapKey,
} from '../constants';
import { getStagedState } from '../decorators';
import type {
Expand Down Expand Up @@ -176,7 +177,14 @@ export function createStore<T = any>({
const signalMap: Record<string, Signal> = {};
const isEmptyObject = Object.keys(service[stateKey]!).length === 0;
if (!isEmptyObject) {
const descriptors: Record<string, PropertyDescriptor> = {};
const descriptors: Record<string, PropertyDescriptor> = {
[signalMapKey]: {
enumerable: false,
configurable: false,
writable: false,
value: signalMap,
},
};
for (const key in service[stateKey]) {
const descriptor = Object.getOwnPropertyDescriptor(service, key);
// eslint-disable-next-line no-continue
Expand All @@ -196,6 +204,7 @@ export function createStore<T = any>({
signalMap[key] &&
!isEqual(signalMap[key].value, current)
) {
// Manual update signal value when the state is changed outside the common reducer.
signalMap[key].value = current;
}
return current;
Expand Down
3 changes: 3 additions & 0 deletions packages/reactant-module/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ import {
enableInspectorKey,
strictKey,
enableAutoComputedKey,
signalMapKey,
} from './constants';
import { PluginModule } from './core';
import type { Signal } from './core/signal';

export type Patch = IPatch<true>;

Expand Down Expand Up @@ -84,6 +86,7 @@ export interface Service<T extends Record<string, any> = Record<string, any>> {
readonly [stateKey]?: T;
readonly [defaultStateKey]?: T;
readonly [storeKey]?: ReduxStore;
readonly [signalMapKey]?: Record<string, Signal<unknown>>;
readonly [loaderKey]?: Loader;
readonly [enablePatchesKey]?: boolean;
readonly [enableAutoComputedKey]?: boolean;
Expand Down
224 changes: 224 additions & 0 deletions packages/reactant-share/test/storage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
StorageOptions,
IStorageOptions,
mockPairTransports,
computed,
watch,
} from '..';
import { MemoryStorage } from './MemoryStorage';

Expand Down Expand Up @@ -479,4 +481,226 @@ describe('base', () => {
)
);
});

test('base SPA mode with storage and enable auto-computed', async () => {
@injectable({
name: 'counter1',
})
class Counter1 {
constructor(private storage: Storage) {
this.storage.setStorage(this, {
whitelist: ['count1'],
});
}

@state
count1 = 0;

@action
increase() {
this.count1 += 1;
}

@computed
get doubleCount() {
return this.count1 * 2;
}
}

const storage = new MemoryStorage({
'persist:root': '{"_persist":"{\\"version\\":-1,\\"rehydrated\\":true}"}',
'persist:counter1':
'{"count1":"1","_persist":"{\\"version\\":-1,\\"rehydrated\\":true}"}',
});

onClientFn = jest.fn();
subscribeOnClientFn = jest.fn();
onServerFn = jest.fn();
subscribeOnServerFn = jest.fn();

const app = await createSharedApp({
modules: [
Storage,
Counter1,
{
provide: StorageOptions,
useValue: {
storage,
blacklist: [],
} as IStorageOptions,
},
],
main: AppView,
render,
share: {
name: 'counter',
type: 'Base',
},
devOptions: {
autoComputed: true,
},
});
expect(onClientFn.mock.calls.length).toBe(0);
expect(subscribeOnClientFn.mock.calls.length).toBe(0);
expect(onServerFn.mock.calls.length).toBe(0);
expect(subscribeOnServerFn.mock.calls.length).toBe(0);
await app.bootstrap(serverContainer);

await new Promise((resolve) => setTimeout(resolve));

expect(onClientFn.mock.calls.length).toBe(0);
expect(subscribeOnClientFn.mock.calls.length).toBe(0);
expect(onServerFn.mock.calls.length).toBe(0);
expect(subscribeOnServerFn.mock.calls.length).toBe(0);
expect(serverContainer.querySelector('#count')?.textContent).toBe('0');

act(() => {
serverContainer
.querySelector('#increase')!
.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});

await new Promise((resolve) => setTimeout(resolve));

expect(onClientFn.mock.calls.length).toBe(0);
expect(subscribeOnClientFn.mock.calls.length).toBe(0);
expect(onServerFn.mock.calls.length).toBe(0);
expect(subscribeOnServerFn.mock.calls.length).toBe(0);

expect(serverContainer.querySelector('#count')?.textContent).toBe('1');

act(() => {
serverContainer
.querySelector('#increase')!
.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});

await new Promise((resolve) => setTimeout(resolve));

expect(onClientFn.mock.calls.length).toBe(0);
expect(subscribeOnClientFn.mock.calls.length).toBe(0);
expect(onServerFn.mock.calls.length).toBe(0);
expect(subscribeOnServerFn.mock.calls.length).toBe(0);

expect(serverContainer.querySelector('#count')?.textContent).toBe('2');

await new Promise((resolve) => setTimeout(resolve));

expect(app.container.get(Counter1).doubleCount).toBe(2);
});

test('base SPA mode with storage and enable auto-computed', async () => {
@injectable({
name: 'counter1',
})
class Counter1 {
constructor(private storage: Storage) {
this.storage.setStorage(this, {
whitelist: ['count1'],
});
watch(
this,
() => this.count1,
() => {
expect(this.doubleCount).toBe(2);
expect(this.count1).toBe(1);
}
);
}

@state
count1 = 0;

@action
increase() {
this.count1 += 1;
}

@computed
get doubleCount() {
return this.count1 * 2;
}
}

const storage = new MemoryStorage({
'persist:root': '{"_persist":"{\\"version\\":-1,\\"rehydrated\\":true}"}',
'persist:counter1':
'{"count1":"1","_persist":"{\\"version\\":-1,\\"rehydrated\\":true}"}',
});

onClientFn = jest.fn();
subscribeOnClientFn = jest.fn();
onServerFn = jest.fn();
subscribeOnServerFn = jest.fn();

const app = await createSharedApp({
modules: [
Storage,
Counter1,
{
provide: StorageOptions,
useValue: {
storage,
blacklist: [],
} as IStorageOptions,
},
],
main: AppView,
render,
share: {
name: 'counter',
type: 'Base',
},
devOptions: {
autoComputed: true,
},
});
expect(onClientFn.mock.calls.length).toBe(0);
expect(subscribeOnClientFn.mock.calls.length).toBe(0);
expect(onServerFn.mock.calls.length).toBe(0);
expect(subscribeOnServerFn.mock.calls.length).toBe(0);
await app.bootstrap(serverContainer);

await new Promise((resolve) => setTimeout(resolve));

expect(onClientFn.mock.calls.length).toBe(0);
expect(subscribeOnClientFn.mock.calls.length).toBe(0);
expect(onServerFn.mock.calls.length).toBe(0);
expect(subscribeOnServerFn.mock.calls.length).toBe(0);
expect(serverContainer.querySelector('#count')?.textContent).toBe('0');

act(() => {
serverContainer
.querySelector('#increase')!
.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});

await new Promise((resolve) => setTimeout(resolve));

expect(onClientFn.mock.calls.length).toBe(0);
expect(subscribeOnClientFn.mock.calls.length).toBe(0);
expect(onServerFn.mock.calls.length).toBe(0);
expect(subscribeOnServerFn.mock.calls.length).toBe(0);

expect(serverContainer.querySelector('#count')?.textContent).toBe('1');

act(() => {
serverContainer
.querySelector('#increase')!
.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});

await new Promise((resolve) => setTimeout(resolve));

expect(onClientFn.mock.calls.length).toBe(0);
expect(subscribeOnClientFn.mock.calls.length).toBe(0);
expect(onServerFn.mock.calls.length).toBe(0);
expect(subscribeOnServerFn.mock.calls.length).toBe(0);

expect(serverContainer.querySelector('#count')?.textContent).toBe('2');

await new Promise((resolve) => setTimeout(resolve));

expect(app.container.get(Counter1).doubleCount).toBe(2);
});
});
23 changes: 23 additions & 0 deletions packages/reactant-storage/src/storage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import {
identifierKey,
nameKey,
storeKey,
getRef,
watch,
signalMapKey,
enableAutoComputedKey,
} from 'reactant-module';
import type { Reducer, ReducersMapObject, Store } from 'redux';
import {
Expand Down Expand Up @@ -158,6 +162,25 @@ class ReactantStorage extends PluginModule {
}
const persistConfig = this.persistConfig[key];
if (persistConfig) {
if ((this as Service)[enableAutoComputedKey]) {
const target = getRef(this)!.modules![key];
const ref = getRef(target);
const stopWatching = watch(
this,
() => ref!.state!._persist?.rehydrated,
(rehydrated) => {
if (rehydrated) {
stopWatching();
const persistStateKeys = persistConfig.whitelist ?? [];
persistStateKeys.forEach((persistStateKey) => {
// need to update the target signal value to the latest hydrated state
target[signalMapKey][persistStateKey].value =
ref.state![persistStateKey];
});
}
}
);
}
const reducer = persistReducer(persistConfig, reducers[key]);
Object.assign(reducers, {
[key]: reducer,
Expand Down

0 comments on commit 6180e77

Please sign in to comment.