diff --git a/README.md b/README.md index f46b21f..c0a0dc6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ =========== [![.github/workflows/build.yml](https://github.com/eight04/store/actions/workflows/build.yml/badge.svg)](https://github.com/eight04/store/actions/workflows/build.yml) -[![codecov](https://codecov.io/gh/eight04/store/branch/master/graph/badge.svg)](https://codecov.io/gh/eight04/store) +[![Coverage Status](https://coveralls.io/repos/github/eight04/store/badge.svg?branch=main)](https://coveralls.io/github/eight04/store?branch=main) A reactive store library designed to work with large collection. Inspired by svelte/store. diff --git a/index.mts b/index.mts index 783b396..75c54d4 100644 --- a/index.mts +++ b/index.mts @@ -624,3 +624,69 @@ export function sort( } } + +/** + * Reindex a collection into a new set + */ +export function reindex, Index>( + $c: C, + indexFn: (item: ItemFromCollection) => Index +): SetStore[]]> { + const $s = new SetStore[]]>(t => t[0]); + const keyToIndexCache = new Map, Index>(); + onChange({ + added: [...$c.get()], + updated: [], + removed: [], + ts: $c.ts + }); + $c.on("change", onChange); + $s.addCleanup(() => $c.off("change", onChange)); + return $s; + + function onChange({added, updated, removed, ts}: CollectionDelta>) { + const modified = new Map[]>(); + + for (const item of [...updated, ...removed]) { + // FIXME: updated items will be move to the last. is that intended? + const key = $c.key(item); + const index = keyToIndexCache.get(key)!; + if (!modified.has(index)) { + modified.set(index, $s.map.get(index)![1]); + } + modified.set(index, modified.get(index)!.filter(i => key !== $c.key(i))) + keyToIndexCache.delete(key); + } + + for (const item of [...updated, ...added]) { + const index = indexFn(item); + const key = $c.key(item); + if (!modified.has(index)) { + modified.set(index, $s.map.get(index)?.[1] || []); + } + modified.set(index, [...modified.get(index)!, item]); + keyToIndexCache.set(key, index); + } + + const toAdd = []; + const toUpdate = []; + const toRemove = []; + + for (const [index, value] of modified.entries()) { + if (!$s.map.has(index)) { + toAdd.push([index, value] as const); + } else if (value.length) { + toUpdate.push([index, value] as const); + } else { + // FIXME: value is always an empty array here. which should be set to removed items + toRemove.push([index, value] as const); + } + } + + $s.set({ + added: toAdd, + updated: toUpdate, + removed: toRemove + }, ts); + } +} diff --git a/test/reindex.mts b/test/reindex.mts new file mode 100644 index 0000000..3227095 --- /dev/null +++ b/test/reindex.mts @@ -0,0 +1,66 @@ +/* eslint-env mocha */ +import * as assert from "node:assert"; +import {ArrayStore, reindex} from "../index.mjs"; + +class Item { + id: number; + value: number; + constructor(id: any, value: any) { + this.id = id; + this.value = value; + } +} + +function cmp(a: Item, b: Item) { + return a.value - b.value; +} + +describe("reindex", () => { + it("basic", () => { + const $s = new ArrayStore(i => i.id, cmp); + const added = [new Item(1, 2), new Item(2, 1)]; + $s.set({ added }); + const $r = reindex($s, i => i.value); + assert.deepStrictEqual($r.get(), new Set([ + [1, [ new Item(2, 1)]], + [2, [ new Item(1, 2)]] + ])); + + $s.set({ removed: [new Item(1, 2)] }); + assert.deepStrictEqual($r.get(), new Set([ + [1, [ new Item(2, 1)]], + ])); + + $s.set({ updated: [new Item(2, 3)] }); + assert.deepStrictEqual($r.get(), new Set([ + [3, [ new Item(2, 3)]], + ])); + }); + + it("duplicated index", () => { + const $s = new ArrayStore(i => i.id, cmp); + const $r = reindex($s, i => i.value); + const added = [new Item(1, 2), new Item(2, 1), new Item(3, 1)]; + $s.set({ added }); + assert.deepStrictEqual($r.get(), new Set([ + [1, [ new Item(2, 1), new Item(3, 1) ]], + [2, [ new Item(1, 2)]] + ])); + }); + + it("index doesn't match when removed", () => { + const $s = new ArrayStore(i => i.id, cmp); + const added = [new Item(1, 2), new Item(2, 1)]; + $s.set({ added }); + const $r = reindex($s, i => i.value); + assert.deepStrictEqual($r.get(), new Set([ + [1, [ new Item(2, 1)]], + [2, [ new Item(1, 2)]] + ])); + + $s.set({ removed: [new Item(1, 3)] }); + assert.deepStrictEqual($r.get(), new Set([ + [1, [ new Item(2, 1)]], + ])); + }) +});