Skip to content

Commit

Permalink
feat: add map map
Browse files Browse the repository at this point in the history
  • Loading branch information
acrazing committed Nov 15, 2024
1 parent f1c363e commit fa4eb5e
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/amos-boxes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
88 changes: 88 additions & 0 deletions packages/amos-boxes/src/mapMapBox.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* @since 2024-11-14 23:00:43
* @author junbao <[email protected]>
*/

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<number, number>(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,
]);
});
});
62 changes: 62 additions & 0 deletions packages/amos-boxes/src/mapMapBox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* @since 2024-11-14 23:00:43
* @author junbao <[email protected]>
*/

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<LM extends MapMap<any, any>>
extends MapBox<LM>,
ShapeBox<
LM,
| 'setItem'
| 'setAll'
| 'setItemIn'
| 'setAllIn'
| 'mergeItemIn'
| 'mergeAllIn'
| 'updateItemIn'
| 'updateAllIn'
| 'deleteItemIn'
| 'deleteAllIn'
| 'clearIn'
| 'resetIn',
'hasItemIn' | 'getItemIn' | 'sizeIn',
MapMap<any, any>
> {}

export const MapMapBox = MapBox.extends<MapMapBox<any>>({
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<KO, KI, V>(
key: string,
outerKey: KO & ID,
innerKey: KI & ID,
defaultValue: V,
): MapMapBox<MapMap<IDOf<KO>, Map<IDOf<KI>, V>>> {
return new MapMapBox(
key,
once(() => new MapMap(new Map(defaultValue))),
);
}
3 changes: 2 additions & 1 deletion packages/amos-shapes/src/Map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -20,6 +20,7 @@ describe('Map', () => {
// @ts-expect-error
m1.mergeAll({ 3: [3, 1] as const });
});

it('should not update', () => {
expect([
m1,
Expand Down
7 changes: 5 additions & 2 deletions packages/amos-shapes/src/Map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,17 @@ export class Map<K extends ID, V> implements JSONSerializable<Record<K, V>> {
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, V>)[k as K] !== this.getItem(k as K)) {
if (
!this.hasItem(k as K) ||
(items as PartialRecord<K, V>)[k as K] !== this.getItem(k as K)
) {
dirty ||= true;
up[k as K] = (items as PartialRecord<K, V>)[k as K];
}
Expand Down
54 changes: 54 additions & 0 deletions packages/amos-shapes/src/MapMap.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* @since 2024-11-14 22:38:52
* @author junbao <[email protected]>
*/

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<number, Map<string, number>>(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<number, Map<TodoStatus, string>>(new Map(''));
// @ts-expect-error
expect(bar.getItem(0).getItem(0) === '10').toBeFalsy();

class EMap<T> extends Map<number, T> {
fine() {
return this.size() > 1;
}
}

expect(
new EMap(TodoStatus.created)
.setAll([
[1, 1],
[2, 2],
])
.size(),
).toBe(2);

const l1 = new MapMap<number, EMap<TodoStatus>>(new EMap(TodoStatus.created));
expect(l1.getItem(0).fine()).toBe(false);
expect(
l1
.setAllIn(1, [
[1, 1],
[2, 2],
])
.getItem(1)
.fine(),
).toBe(true);
});
});
91 changes: 91 additions & 0 deletions packages/amos-shapes/src/MapMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* @since 2024-11-14 22:38:52
* @author junbao <[email protected]>
*/

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<K extends ID, M extends Map<any, any>>
extends MapDelegateOperations<
K,
M,
| 'setItem'
| 'setAll'
| 'mergeItem'
| 'mergeAll'
| 'updateItem'
| 'updateAll'
| 'deleteItem'
| 'deleteAll'
| 'clear'
| 'reset',
'hasItem' | 'getItem' | 'size' | 'entries' | 'keys' | 'values',
Map<any, any>
> {}

export class MapMap<K extends ID, M extends Map<any, any>> extends Map<K, M> {
constructor(defaultValue: M) {
super(defaultValue);
}

override setItem(
key: K,
value: M | PartialDictionary<MapKey<M>, MapValue<M>> | readonly MapEntry<M>[],
): this {
return super.setItem(
key,
value instanceof Map
? value
: this.defaultValue.reset(Array.isArray(value) ? Object.fromEntries(value) : value),
);
}

override setAll(
items:
| PartialDictionary<K, M | PartialDictionary<MapKey<M>, MapValue<M>> | readonly MapEntry<M>[]>
| ArraySource<
Entry<K, M | PartialDictionary<MapKey<M>, MapValue<M>> | readonly MapEntry<M>[]>
>,
): 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',
});
1 change: 1 addition & 0 deletions packages/amos-shapes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
export * from './List';
export * from './ListMap';
export * from './Map';
export * from './MapMap';
export * from './Record';
export * from './RecordMap';
3 changes: 3 additions & 0 deletions packages/amos/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
ListBox,
ListMapBox,
MapBox,
MapMapBox,
NumberBox,
ObjectBox,
RecordBox,
Expand All @@ -18,6 +19,7 @@ export {
listBox,
listMapBox,
mapBox,
mapMapBox,
numberBox,
objectBox,
recordBox,
Expand Down Expand Up @@ -105,6 +107,7 @@ export {
MapDelegateOperations,
MapEntry,
MapKey,
MapMap,
MapValue,
PartialProps,
PartialRequiredProps,
Expand Down

0 comments on commit fa4eb5e

Please sign in to comment.