From fa4eb5e7e3dace5cc820707274e1737509394095 Mon Sep 17 00:00:00 2001 From: junbao Date: Thu, 14 Nov 2024 23:29:40 -0800 Subject: [PATCH] feat: add map map --- packages/amos-boxes/src/index.ts | 1 + packages/amos-boxes/src/mapMapBox.spec.ts | 88 ++++++++++++++++++++++ packages/amos-boxes/src/mapMapBox.ts | 62 +++++++++++++++ packages/amos-shapes/src/Map.spec.ts | 3 +- packages/amos-shapes/src/Map.ts | 7 +- packages/amos-shapes/src/MapMap.spec.ts | 54 ++++++++++++++ packages/amos-shapes/src/MapMap.ts | 91 +++++++++++++++++++++++ packages/amos-shapes/src/index.ts | 1 + packages/amos/src/index.ts | 3 + 9 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 packages/amos-boxes/src/mapMapBox.spec.ts create mode 100644 packages/amos-boxes/src/mapMapBox.ts create mode 100644 packages/amos-shapes/src/MapMap.spec.ts create mode 100644 packages/amos-shapes/src/MapMap.ts diff --git a/packages/amos-boxes/src/index.ts b/packages/amos-boxes/src/index.ts index c5526d5..d0f1758 100644 --- a/packages/amos-boxes/src/index.ts +++ b/packages/amos-boxes/src/index.ts @@ -8,6 +8,7 @@ export * from './boolBox'; export * from './listBox'; export * from './listMapBox'; export * from './mapBox'; +export * from './mapMapBox'; export * from './numberBox'; export * from './objectBox'; export * from './recordBox'; diff --git a/packages/amos-boxes/src/mapMapBox.spec.ts b/packages/amos-boxes/src/mapMapBox.spec.ts new file mode 100644 index 0000000..e85d6fb --- /dev/null +++ b/packages/amos-boxes/src/mapMapBox.spec.ts @@ -0,0 +1,88 @@ +/* + * @since 2024-11-14 23:00:43 + * @author junbao + */ + +import { createStore } from 'amos-core'; +import { Map } from 'amos-shapes'; +import { runMutations } from 'amos-testing'; +import { toJS } from 'amos-utils'; +import { mapMapBox } from './mapMapBox'; + +const accountDirtyPostMapBox = mapMapBox('unit.posts.accountDirtyMap', 0, 0, 0); + +describe('MapMapBox', () => { + it('should create mutations', () => { + expect( + runMutations( + accountDirtyPostMapBox.getInitialState().setAll({ + 1: [ + [1, 2], + [3, 4], + ], + 2: { 2: 3, 4: 5 }, + }), + [ + accountDirtyPostMapBox.setItem(1, [[2, 3]]), + accountDirtyPostMapBox.setItem(1, new Map(0).setAll([[2, 3]])), + accountDirtyPostMapBox.setAll({ 0: [], 1: [[2, 3]] }), + accountDirtyPostMapBox.setAll([ + [0, [[1, 2]]], + [1, { 2: 3 }], + ]), + accountDirtyPostMapBox.setItemIn(1, 2, 3), + accountDirtyPostMapBox.deleteItemIn(1, 3), + accountDirtyPostMapBox.mergeItemIn(1, 2, 3), + accountDirtyPostMapBox.updateItemIn(1, 2, (v) => v + 1), + accountDirtyPostMapBox.clearIn(1), + accountDirtyPostMapBox.resetIn(1, { 2: 3 }), + ], + ).map((v) => toJS(v)), + ).toEqual([ + { 1: { 2: 3 }, 2: { 2: 3, 4: 5 } }, + { 1: { 2: 3 }, 2: { 2: 3, 4: 5 } }, + { 0: {}, 1: { 2: 3 }, 2: { 2: 3, 4: 5 } }, + { 0: { 1: 2 }, 1: { 2: 3 }, 2: { 2: 3, 4: 5 } }, + { 1: { 1: 2, 2: 3, 3: 4 }, 2: { 2: 3, 4: 5 } }, + { 1: { 1: 2 }, 2: { 2: 3, 4: 5 } }, + { 1: { 1: 2, 2: 3, 3: 4 }, 2: { 2: 3, 4: 5 } }, + { 1: { 1: 2, 2: 1, 3: 4 }, 2: { 2: 3, 4: 5 } }, + { 1: {}, 2: { 2: 3, 4: 5 } }, + { 1: { 2: 3 }, 2: { 2: 3, 4: 5 } }, + ]); + }); + it('should create selectors', () => { + const store = createStore(); + store.dispatch( + accountDirtyPostMapBox.setAll({ + 1: [ + [1, 2], + [3, 4], + ], + 2: { 2: 3, 4: 5 }, + }), + ); + expect( + store.select([ + accountDirtyPostMapBox.getItem(1), + accountDirtyPostMapBox.getItemIn(1, 1), + accountDirtyPostMapBox.hasItem(0), + accountDirtyPostMapBox.hasItemIn(1, 2), + accountDirtyPostMapBox.hasItemIn(1, 3), + accountDirtyPostMapBox.sizeIn(2), + accountDirtyPostMapBox.size(), + ]), + ).toEqual([ + new Map(0).setAll([ + [1, 2], + [3, 4], + ]), + 2, + false, + false, + true, + 2, + 2, + ]); + }); +}); diff --git a/packages/amos-boxes/src/mapMapBox.ts b/packages/amos-boxes/src/mapMapBox.ts new file mode 100644 index 0000000..7ad54a0 --- /dev/null +++ b/packages/amos-boxes/src/mapMapBox.ts @@ -0,0 +1,62 @@ +/* + * @since 2024-11-14 23:00:43 + * @author junbao + */ + +import { ShapeBox } from 'amos-core'; +import { Map, MapMap } from 'amos-shapes'; +import { type ID, IDOf, once } from 'amos-utils'; +import { MapBox } from './mapBox'; + +export interface MapMapBox> + extends MapBox, + ShapeBox< + LM, + | 'setItem' + | 'setAll' + | 'setItemIn' + | 'setAllIn' + | 'mergeItemIn' + | 'mergeAllIn' + | 'updateItemIn' + | 'updateAllIn' + | 'deleteItemIn' + | 'deleteAllIn' + | 'clearIn' + | 'resetIn', + 'hasItemIn' | 'getItemIn' | 'sizeIn', + MapMap + > {} + +export const MapMapBox = MapBox.extends>({ + name: 'MapMap', + mutations: { + setItemIn: null, + setAllIn: null, + mergeItemIn: null, + mergeAllIn: null, + updateItemIn: null, + updateAllIn: null, + deleteItemIn: null, + deleteAllIn: null, + clearIn: null, + resetIn: null, + }, + selectors: { + getItemIn: null, + hasItemIn: null, + sizeIn: null, + }, +}); + +export function mapMapBox( + key: string, + outerKey: KO & ID, + innerKey: KI & ID, + defaultValue: V, +): MapMapBox, Map, V>>> { + return new MapMapBox( + key, + once(() => new MapMap(new Map(defaultValue))), + ); +} diff --git a/packages/amos-shapes/src/Map.spec.ts b/packages/amos-shapes/src/Map.spec.ts index 0ba8f3f..63d11f7 100644 --- a/packages/amos-shapes/src/Map.spec.ts +++ b/packages/amos-shapes/src/Map.spec.ts @@ -4,7 +4,7 @@ */ import { checkType, expectCalledWith } from 'amos-testing'; -import { takeFirst, isIterable, isIterableIterator } from 'amos-utils'; +import { isIterable, isIterableIterator, takeFirst } from 'amos-utils'; import { Map } from './Map'; describe('Map', () => { @@ -20,6 +20,7 @@ describe('Map', () => { // @ts-expect-error m1.mergeAll({ 3: [3, 1] as const }); }); + it('should not update', () => { expect([ m1, diff --git a/packages/amos-shapes/src/Map.ts b/packages/amos-shapes/src/Map.ts index 9782ec0..bbb7adc 100644 --- a/packages/amos-shapes/src/Map.ts +++ b/packages/amos-shapes/src/Map.ts @@ -69,14 +69,17 @@ export class Map implements JSONSerializable> { let dirty = false; if (Array.isArray(items) || isIterable(items)) { for (const [k, v] of items) { - if (v !== this.getItem(k)) { + if (!this.hasItem(k) || v !== this.getItem(k)) { dirty ||= true; up[k as K] = v; } } } else { for (const k in items) { - if ((items as PartialRecord)[k as K] !== this.getItem(k as K)) { + if ( + !this.hasItem(k as K) || + (items as PartialRecord)[k as K] !== this.getItem(k as K) + ) { dirty ||= true; up[k as K] = (items as PartialRecord)[k as K]; } diff --git a/packages/amos-shapes/src/MapMap.spec.ts b/packages/amos-shapes/src/MapMap.spec.ts new file mode 100644 index 0000000..56498a9 --- /dev/null +++ b/packages/amos-shapes/src/MapMap.spec.ts @@ -0,0 +1,54 @@ +/* + * @since 2024-11-14 22:38:52 + * @author junbao + */ + +import { TodoStatus } from 'amos-testing'; +import { Map } from './Map'; +import { MapMap } from './MapMap'; + +describe('MapMap', () => { + it('should create MapMap', () => { + // default list map + const foo = new MapMap>(new Map(0)); + // @ts-expect-error + foo.getItem(''); + foo.setItemIn(1, '', 1); + foo.setItem(0, { '': 1 }); + foo.getItem(0); + foo.getItem(0).getItem('').toExponential(); + foo.getItemIn(1, ''); + foo.hasItemIn(1, ''); + // with value type limit + const bar = new MapMap>(new Map('')); + // @ts-expect-error + expect(bar.getItem(0).getItem(0) === '10').toBeFalsy(); + + class EMap extends Map { + fine() { + return this.size() > 1; + } + } + + expect( + new EMap(TodoStatus.created) + .setAll([ + [1, 1], + [2, 2], + ]) + .size(), + ).toBe(2); + + const l1 = new MapMap>(new EMap(TodoStatus.created)); + expect(l1.getItem(0).fine()).toBe(false); + expect( + l1 + .setAllIn(1, [ + [1, 1], + [2, 2], + ]) + .getItem(1) + .fine(), + ).toBe(true); + }); +}); diff --git a/packages/amos-shapes/src/MapMap.ts b/packages/amos-shapes/src/MapMap.ts new file mode 100644 index 0000000..4a7790f --- /dev/null +++ b/packages/amos-shapes/src/MapMap.ts @@ -0,0 +1,91 @@ +/* + * @since 2024-11-14 22:38:52 + * @author junbao + */ + +import { type ArraySource, Entry, ID, isIterable, type PartialDictionary } from 'amos-utils'; +import { + implementMapDelegations, + Map, + MapDelegateOperations, + type MapEntry, + type MapKey, + type MapValue, +} from './Map'; + +export interface MapMap> + extends MapDelegateOperations< + K, + M, + | 'setItem' + | 'setAll' + | 'mergeItem' + | 'mergeAll' + | 'updateItem' + | 'updateAll' + | 'deleteItem' + | 'deleteAll' + | 'clear' + | 'reset', + 'hasItem' | 'getItem' | 'size' | 'entries' | 'keys' | 'values', + Map + > {} + +export class MapMap> extends Map { + constructor(defaultValue: M) { + super(defaultValue); + } + + override setItem( + key: K, + value: M | PartialDictionary, MapValue> | readonly MapEntry[], + ): this { + return super.setItem( + key, + value instanceof Map + ? value + : this.defaultValue.reset(Array.isArray(value) ? Object.fromEntries(value) : value), + ); + } + + override setAll( + items: + | PartialDictionary, MapValue> | readonly MapEntry[]> + | ArraySource< + Entry, MapValue> | readonly MapEntry[]> + >, + ): this { + const data = Array.isArray(items) + ? items + : isIterable(items) + ? Array.from(items) + : Object.entries(items); + data.forEach((d) => { + if (Array.isArray(d[1])) { + d[1] = this.defaultValue.reset(Object.fromEntries(d[1])); + } else if (!(d[1] instanceof Map)) { + d[1] = this.defaultValue.reset(d[1]); + } + }); + return super.setAll(data); + } +} + +implementMapDelegations(MapMap, { + setItem: 'set', + setAll: 'set', + mergeItem: 'set', + mergeAll: 'set', + updateItem: 'set', + updateAll: 'set', + deleteItem: 'set', + deleteAll: 'set', + clear: 'set', + reset: 'set', + getItem: 'get', + hasItem: 'get', + size: 'get', + entries: 'get', + keys: 'get', + values: 'get', +}); diff --git a/packages/amos-shapes/src/index.ts b/packages/amos-shapes/src/index.ts index 4545046..a4cbe9f 100644 --- a/packages/amos-shapes/src/index.ts +++ b/packages/amos-shapes/src/index.ts @@ -6,5 +6,6 @@ export * from './List'; export * from './ListMap'; export * from './Map'; +export * from './MapMap'; export * from './Record'; export * from './RecordMap'; diff --git a/packages/amos/src/index.ts b/packages/amos/src/index.ts index 113e4eb..b018600 100644 --- a/packages/amos/src/index.ts +++ b/packages/amos/src/index.ts @@ -9,6 +9,7 @@ export { ListBox, ListMapBox, MapBox, + MapMapBox, NumberBox, ObjectBox, RecordBox, @@ -18,6 +19,7 @@ export { listBox, listMapBox, mapBox, + mapMapBox, numberBox, objectBox, recordBox, @@ -105,6 +107,7 @@ export { MapDelegateOperations, MapEntry, MapKey, + MapMap, MapValue, PartialProps, PartialRequiredProps,