diff --git a/package.json b/package.json index 85bd513..771e66a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@amos/root", "private": true, - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "license": "MIT", "workspaces": [ "packages/*", diff --git a/packages/amos-babel/package.json b/packages/amos-babel/package.json index 2c86f89..be17092 100644 --- a/packages/amos-babel/package.json +++ b/packages/amos-babel/package.json @@ -1,6 +1,6 @@ { "name": "amos-babel", - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "private": true, "files": [], "peerDependencies": { diff --git a/packages/amos-boxes/package.json b/packages/amos-boxes/package.json index 10572c2..e892261 100644 --- a/packages/amos-boxes/package.json +++ b/packages/amos-boxes/package.json @@ -1,6 +1,6 @@ { "name": "amos-boxes", - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "private": true, "files": [] } diff --git a/packages/amos-boxes/src/mapBox.ts b/packages/amos-boxes/src/mapBox.ts index c09e663..5375aa4 100644 --- a/packages/amos-boxes/src/mapBox.ts +++ b/packages/amos-boxes/src/mapBox.ts @@ -49,6 +49,7 @@ export const MapBox = Box.extends({ options: { table: { toRows: (state: Map) => state.toJSON(), + hasRow: (state: Map, key) => state.hasItem(key), getRow: (state: Map, key) => state.getItem(key), hydrate: (state: Map, rows) => state.setAll(state.fromJS(rows).toJSON()), }, diff --git a/packages/amos-core/package.json b/packages/amos-core/package.json index eb2bc2d..3b8c120 100644 --- a/packages/amos-core/package.json +++ b/packages/amos-core/package.json @@ -1,6 +1,6 @@ { "name": "amos-core", - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "private": true, "files": [] } diff --git a/packages/amos-core/src/box.ts b/packages/amos-core/src/box.ts index a591396..0493b4d 100644 --- a/packages/amos-core/src/box.ts +++ b/packages/amos-core/src/box.ts @@ -74,7 +74,12 @@ export interface TableOptions { /** * Get a row from current state */ - getRow: (state: S, rowId: ID) => boolean; + hasRow: (state: S, rowId: ID) => boolean; + + /** + * Get a row from current state + */ + getRow: (state: S, rowId: ID) => any; /** * Merge the persisted state to current state. diff --git a/packages/amos-io/package.json b/packages/amos-io/package.json index 6b2fffd..1c7c117 100644 --- a/packages/amos-io/package.json +++ b/packages/amos-io/package.json @@ -1,6 +1,6 @@ { "name": "amos-io", - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "private": true, "files": [] } diff --git a/packages/amos-persist/package.json b/packages/amos-persist/package.json index 9c6696d..5f6ce09 100644 --- a/packages/amos-persist/package.json +++ b/packages/amos-persist/package.json @@ -1,6 +1,6 @@ { "name": "amos-persist", - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "private": true, "files": [] } diff --git a/packages/amos-persist/src/enhancer.spec.ts b/packages/amos-persist/src/enhancer.spec.ts index 86c8128..0e9e1b0 100644 --- a/packages/amos-persist/src/enhancer.spec.ts +++ b/packages/amos-persist/src/enhancer.spec.ts @@ -8,6 +8,7 @@ import { countBox, darkModeBox, dispatch, + expectCalled, expectCalledWith, Jerry, Morty, @@ -144,4 +145,31 @@ describe('withPersist', () => { await store.dispatch(hydrate([])); expect(store.select(userMapBox.getIn(Rick.id, 'firstName'))).toBe('F1'); }); + + it('should not loop infinite', async () => { + const storage = new MemoryStorage(); + await storage.setMulti([ + [toKey(countBox), 1, countBox.getInitialState()], + [toKey(userMapBox, Rick.id), 1, Rick], + ]); + const store = createStore(void 0, withPersist({ storage, includes: () => true })); + store.subscribe(() => { + store.select(countBox); + store.select(userMapBox.getItem(Rick.id)); + store.select(userMapBox.getItem(Morty.id)); + store.select(userMapBox.getItem(10000)); + }); + const getMulti = jest.fn(); + append(storage, 'getMulti', getMulti); + store.select(countBox); + store.select(userMapBox.getItem(Rick.id)); + store.select(userMapBox.getItem(Morty.id)); + store.select(userMapBox.getItem(10000)); + store.select(userMapBox.getItem(10001)); + await store.dispatch(hydrate([])); + expectCalled(getMulti, 1); + await Promise.resolve(); + await store.dispatch(hydrate([])); + expectCalled(getMulti, 0); + }); }); diff --git a/packages/amos-persist/src/enhancer.ts b/packages/amos-persist/src/enhancer.ts index 5bd7684..1d919eb 100644 --- a/packages/amos-persist/src/enhancer.ts +++ b/packages/amos-persist/src/enhancer.ts @@ -7,9 +7,9 @@ import { Box, type Mutation, type Selectable, type Selector, StoreEnhancer } fro import { append, isAmosObject, once, override, PartialRequired, StackObserver } from 'amos-utils'; import { createHydrate } from './hydrate'; import { createPersist } from './persist'; -import { persistBox, type PersistState } from './state'; +import { persistBox } from './state'; import { PersistOptions, type PersistRowKey } from './types'; -import { shouldPersist } from './utils'; +import { shouldPersist, toKey } from './utils'; export function withPersist(options: PartialRequired): StoreEnhancer { return (next) => (_options) => { @@ -22,29 +22,35 @@ export function withPersist(options: PartialRequired) const hydrate = createHydrate(store, finalOptions); const persist = createPersist(store, finalOptions); - + const hydrated = new Set(); + const persisted = new Map(); + const initial = new Map(); const selecting = new StackObserver(); - const initial = new Map(); - - const state: PersistState = { - ...finalOptions, - init: once(async () => options.storage.init?.()), - snapshot: new Map(), - select: selecting.observe((s: any) => store.select(s)), - getInitial: (box) => { - if (initial.has(box.key)) { - return initial.get(box.key); - } - state.select(box); - return state.getInitial(box); - }, - hydrate, - persist, + const init = once(async () => options.storage.init?.()); + const select = selecting.observe((s: any) => store.select(s)); + const getInitial = (box: Box) => { + if (!initial.has(box.key)) { + select(box); + } + return initial.get(box.key)!; }; - append(store, 'onInit', () => store.dispatch(persistBox.setState(state))); + append(store, 'onInit', () => + store.dispatch( + persistBox.setState({ + ...finalOptions, + init, + select, + getInitial, + hydrated, + persisted, + hydrate, + persist, + }), + ), + ); append(store, 'onMount', (box, initialState, preloadedState) => { - initial.set(box.key, initialState); + initial.set(box.key, [initialState, preloadedState]); }); let dispatchingMutation = 0; @@ -80,7 +86,10 @@ export function withPersist(options: PartialRequired) // hydrate rows for multi-row boxes if ( loadRows && - r === loadRows[0].table!.getRow(state.getInitial(loadRows[0]), loadRows[1]) && + (getInitial(loadRows[0])[1] === void 0 || + !loadRows[0].table!.hasRow(getInitial(loadRows[0])[0], loadRows[1])) && + !hydrated.has(toKey(loadRows[0])) && + !hydrated.has(toKey(loadRows[0], loadRows[1])) && shouldPersist(finalOptions, loadRows[0]) ) { hydrate(loadRows); @@ -91,7 +100,8 @@ export function withPersist(options: PartialRequired) isBox && !selectable.table && !selectingRows && - r === state.getInitial(selectable) && + getInitial(selectable)[1] === void 0 && + !hydrated.has(toKey(selectable)) && shouldPersist(finalOptions, selectable) ) { hydrate(selectable); diff --git a/packages/amos-persist/src/hydrate.ts b/packages/amos-persist/src/hydrate.ts index 7bb3f7b..4210d77 100644 --- a/packages/amos-persist/src/hydrate.ts +++ b/packages/amos-persist/src/hydrate.ts @@ -29,9 +29,16 @@ export const createHydrate = (store: Store, finalOptions: PersistOptions) => { const addRow = (box: Box, rowId?: ID) => { if (rowId === void 0) { + if (state.hydrated.has(toKey(box))) { + return; + } + state.hydrated.add(toKey(box)); targets.set(box.key, box.table ? null : void 0); + } else if (state.hydrated.has(toKey(box)) || state.hydrated.has(toKey(box, rowId))) { + return; } else { must(box.table, `${box.key} is not a multi-row box`); + state.hydrated.add(toKey(box, rowId)); if (!targets.has(box.key)) { targets.set(box.key, new Set()); } @@ -65,6 +72,10 @@ export const createHydrate = (store: Store, finalOptions: PersistOptions) => { } }); + if (prefixBoxes.length === 0 && exactKeys.length === 0) { + return; + } + const [prefixes, exacts] = await Promise.all([ Promise.all(prefixBoxes.map((key) => state.storage.getPrefix(toKey(key, null)))), exactKeys.length ? state.storage.getMulti(exactKeys.map(([k, i]) => toKey(k, i))) : [], @@ -85,10 +96,10 @@ export const createHydrate = (store: Store, finalOptions: PersistOptions) => { prefixes.push(tablePrefixMap[key]); } tablePrefixMap[key].push([toKey(key, rowId), ...value]); - } else if (state.select(box) === state.getInitial(box)) { + } else if (state.select(box) === state.getInitial(box)[0]) { const js = migrate(box, value[0], '', value[1]); if (js !== void 0) { - exactEntries.push(box.setState(fromJS(state.getInitial(box), js))); + exactEntries.push(box.setState(fromJS(state.getInitial(box)[0], js))); } } }); @@ -104,7 +115,7 @@ export const createHydrate = (store: Store, finalOptions: PersistOptions) => { if (id === void 0) { continue; } - if (box.table!.getRow(curr, id) !== box.table!.getRow(state.getInitial(box), id)) { + if (box.table!.getRow(curr, id) !== box.table!.getRow(state.getInitial(box)[0], id)) { continue; } d = migrate(box, v, id, d); diff --git a/packages/amos-persist/src/persist.ts b/packages/amos-persist/src/persist.ts index 0db525f..ac3285a 100644 --- a/packages/amos-persist/src/persist.ts +++ b/packages/amos-persist/src/persist.ts @@ -24,8 +24,8 @@ export const createPersist = (store: Store, finalOptions: PersistOptions) => { continue; } const curr = snapshot[k]; - const prev = state.snapshot.has(k) ? state.snapshot.get(k) : state.getInitial(box); - state.snapshot.set(k, curr); + const prev = state.persisted.has(k) ? state.persisted.get(k) : state.getInitial(box)[0]; + state.persisted.set(k, curr); if (curr === prev) { continue; } diff --git a/packages/amos-persist/src/state.ts b/packages/amos-persist/src/state.ts index 29f19ef..296e4ee 100644 --- a/packages/amos-persist/src/state.ts +++ b/packages/amos-persist/src/state.ts @@ -11,8 +11,9 @@ import { toKey } from './utils'; export interface PersistState extends PersistOptions { init: () => Promise; select: Select; - getInitial: (box: Box) => any; - snapshot: Map; + getInitial: (box: Box) => [initial: any, preloaded: any]; + hydrated: Set; + persisted: Map; hydrate: NextTicker, void>; persist: NextTicker; } diff --git a/packages/amos-react/package.json b/packages/amos-react/package.json index f1c1aa7..d4fb62e 100644 --- a/packages/amos-react/package.json +++ b/packages/amos-react/package.json @@ -1,6 +1,6 @@ { "name": "amos-react", - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "private": true, "files": [], "peerDependencies": { diff --git a/packages/amos-shapes/package.json b/packages/amos-shapes/package.json index 462d061..76d4b1b 100644 --- a/packages/amos-shapes/package.json +++ b/packages/amos-shapes/package.json @@ -1,6 +1,6 @@ { "name": "amos-shapes", - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "private": true, "files": [] } diff --git a/packages/amos-testing/package.json b/packages/amos-testing/package.json index cd39d19..e291c78 100644 --- a/packages/amos-testing/package.json +++ b/packages/amos-testing/package.json @@ -1,6 +1,6 @@ { "name": "amos-testing", - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "private": true, "files": [] } diff --git a/packages/amos-typescript/package.json b/packages/amos-typescript/package.json index c23a758..6be3c97 100644 --- a/packages/amos-typescript/package.json +++ b/packages/amos-typescript/package.json @@ -1,6 +1,6 @@ { "name": "amos-typescript", - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "private": true, "files": [], "peerDependencies": { diff --git a/packages/amos-utils/package.json b/packages/amos-utils/package.json index 7925bf2..9bedb07 100644 --- a/packages/amos-utils/package.json +++ b/packages/amos-utils/package.json @@ -1,6 +1,6 @@ { "name": "amos-utils", - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "private": true, "files": [] } diff --git a/packages/amos/package.json b/packages/amos/package.json index 1e24851..0ae9896 100644 --- a/packages/amos/package.json +++ b/packages/amos/package.json @@ -1,6 +1,6 @@ { "name": "amos", - "version": "0.3.0-beta.33", + "version": "0.3.0-beta.34", "description": "An out-of-the-box state management library designed for your large-scale projects.", "keywords": [ "amos",