Skip to content

Commit

Permalink
refactor(collections): add batch set and delete
Browse files Browse the repository at this point in the history
  • Loading branch information
crimx committed Jan 21, 2024
1 parent dbeb461 commit e65e6ea
Show file tree
Hide file tree
Showing 6 changed files with 375 additions and 23 deletions.
83 changes: 69 additions & 14 deletions src/collections/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import type { ReadonlyVal } from "../typings";
* ```
*/
export class ReactiveList<TValue> {
public constructor(arrayLike?: ArrayLike<TValue>) {
public constructor(items?: Iterable<TValue> | null) {
const [$, set$] = readonlyVal<ReadonlyArray<TValue>>(
arrayLike ? Array.from(arrayLike) : [],
items ? [...items] : [],
{ equal: false }
);
this.$ = $;
Expand Down Expand Up @@ -168,16 +168,76 @@ export class ReactiveList<TValue> {
}

/**
* Sets new element to the list at specific index in place of the existing element.
* @param index The zero-based location in the list at which to insert element.
* Same as `Array.prototype.splice`.
* Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements.
* @param start The zero-based location in the array from which to start removing elements.
* A negative index will be ignored.
* @param item Element to insert into the list.
* @param deleteCount The number of elements to remove.
* @returns An array containing the elements that were deleted.
*/
public set(index: number, item: TValue): void {
if (index >= 0) {
public splice(start: number, deleteCount?: number): TValue[];
/**
* Same as `Array.prototype.splice`.
* Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements.
* @param start The zero-based location in the array from which to start removing elements.
* A negative index will be ignored.
* @param deleteCount The number of elements to remove.
* @param items Elements to insert into the array in place of the deleted elements.
* @returns An array containing the elements that were deleted.
*/
public splice(
start: number,
deleteCount: number,
...items: TValue[]
): TValue[];
public splice(
start: number,
deleteCount?: number,
...rest: TValue[]
): TValue[] {
const result = (this.array as TValue[]).splice(
start as number,
deleteCount as number,
...(rest as TValue[])
);
if (result.length > 0 || rest.length > 0) {
this.#notify();
}
return result;
}

/**
* Sets new item to the list at specific index in place of the existing item.
* @param index The zero-based location in the list at which to insert item.
* A negative index will be ignored.
* @param item Item to set to the list.
* @returns this
*/
public set(index: number, item: TValue): this {
if (index >= 0 && this.array[index] !== item) {
(this.array as TValue[])[index] = item;
this.#notify();
}
return this;
}

/**
* Sets new items to the list at specific index in place of the existing items.
* @param entries An iterable object that contains key-value pairs.
* @returns this
*/
public batchSet(entries: Iterable<readonly [number, TValue]>): this {
let isDirty = false;
for (const [index, item] of entries) {
if (index >= 0 && this.array[index] !== item) {
isDirty = true;
(this.array as TValue[])[index] = item;
}
}
if (isDirty) {
this.#notify();
}
return this;
}

/**
Expand All @@ -188,8 +248,7 @@ export class ReactiveList<TValue> {
*/
public insert(index: number, ...items: TValue[]): void {
if (index >= 0 && items.length > 0) {
(this.array as TValue[]).splice(index, 0, ...items);
this.#notify();
this.splice(index, 0, ...items);
}
}

Expand All @@ -202,10 +261,7 @@ export class ReactiveList<TValue> {
*/
public delete(index: number, count = 1): void {
if (index >= 0 && count >= 1) {
const result = (this.array as TValue[]).splice(index, count);
if (result.length > 0) {
this.#notify();
}
this.splice(index, count);
}
}

Expand Down Expand Up @@ -300,7 +356,6 @@ export class ReactiveList<TValue> {
*/
export type ReadonlyReactiveList<TValue> = Omit<
ReactiveList<TValue>,
| "$"
| "length"
| "push"
| "pop"
Expand Down
33 changes: 31 additions & 2 deletions src/collections/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type { ReadonlyVal } from "../typings";
* ```
*/
export class ReactiveMap<TKey, TValue> extends Map<TKey, TValue> {
public constructor(entries?: readonly (readonly [TKey, TValue])[] | null) {
public constructor(entries?: Iterable<readonly [TKey, TValue]> | null) {
super();

const [$, set$] = readonlyVal(this, { equal: false });
Expand Down Expand Up @@ -53,6 +53,20 @@ export class ReactiveMap<TKey, TValue> extends Map<TKey, TValue> {
return deleted;
}

/**
* Delete multiple entries from the Map.
*/
public batchDelete(keys: Iterable<TKey>): boolean {
let deleted = false;
for (const key of keys) {
deleted = super.delete(key) || deleted;
}
if (deleted) {
this.#notify();
}
return deleted;
}

public override clear(): void {
if (this.size > 0) {
super.clear();
Expand All @@ -69,6 +83,21 @@ export class ReactiveMap<TKey, TValue> extends Map<TKey, TValue> {
return this;
}

/**
* Set multiple entries in the Map.
*/
public batchSet(entries: Iterable<readonly [TKey, TValue]>): this {
let isDirty = false;
for (const [key, value] of entries) {
isDirty = isDirty || !this.has(key) || this.get(key) !== value;
super.set(key, value);
}
if (isDirty) {
this.#notify();
}
return this;
}

/**
* Replace all entries in the Map.
*
Expand Down Expand Up @@ -102,7 +131,7 @@ export class ReactiveMap<TKey, TValue> extends Map<TKey, TValue> {
*/
export type ReadonlyReactiveMap<TKey, TValue> = Omit<
ReactiveMap<TKey, TValue>,
"$" | "delete" | "clear" | "set" | "replace"
"$" | "delete" | "clear" | "set" | "batchSet" | "replace"
> & {
readonly $: ReadonlyVal<ReadonlyReactiveMap<TKey, TValue>>;
};
40 changes: 33 additions & 7 deletions src/collections/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,15 @@ import type { ReadonlyVal } from "../typings";
* ```
*/
export class ReactiveSet<TValue> extends Set<TValue> {
public constructor(entries?: readonly TValue[] | null) {
public constructor(values?: Iterable<TValue> | null) {
super();

const [$, set$] = readonlyVal(this, { equal: false });
this.$ = $;
this.#notify = () => set$(this);

if (entries) {
for (const value of entries) {
this.add(value);
}
if (values) {
this.batchAdd(values);
}
}

Expand All @@ -42,8 +40,22 @@ export class ReactiveSet<TValue> extends Set<TValue> {

#notify: () => void;

public override delete(key: TValue): boolean {
const deleted = super.delete(key);
public override delete(value: TValue): boolean {
const deleted = super.delete(value);
if (deleted) {
this.#notify();
}
return deleted;
}

/**
* Delete multiple values from the Set.
*/
public batchDelete(values: Iterable<TValue>): boolean {
let deleted = false;
for (const value of values) {
deleted = super.delete(value) || deleted;
}
if (deleted) {
this.#notify();
}
Expand All @@ -66,6 +78,20 @@ export class ReactiveSet<TValue> extends Set<TValue> {
return this;
}

/**
* Add multiple values to the Set.
*/
public batchAdd(values: Iterable<TValue>): this {
const prevSize = this.size;
for (const value of values) {
super.add(value);
}
if (prevSize !== this.size) {
this.#notify();
}
return this;
}

public dispose(): void {
this.$.dispose();
}
Expand Down
110 changes: 110 additions & 0 deletions test/collections/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,116 @@ describe("ReactiveList", () => {
});
});

describe("splice", () => {
it("should remove the specified elements from the list", () => {
const list = new ReactiveList([1, 2, 3]);
list.splice(1, 2);
expect(list.array).toEqual([1]);
});

it("should remove the specified elements from the list and insert new elements", () => {
const list = new ReactiveList([1, 2, 3]);
list.splice(1, 2, 4, 5);
expect(list.array).toEqual([1, 4, 5]);
});

it("should count backward on negative index", () => {
const list = new ReactiveList([1, 2, 3]);
list.splice(-1, 2);
expect(list.array).toEqual([1, 2]);
});

it("should notify on splice", () => {
const list = new ReactiveList(["a", "b", "c", "d", "e"]);
const mockNotify = jest.fn();
const dispose = list.$.reaction(mockNotify, true);

list.splice(1, 2);
expect(mockNotify).toHaveBeenCalledTimes(1);
expect(mockNotify).lastCalledWith(list.array);

list.splice(0, 0, "x", "y");
expect(mockNotify).toHaveBeenCalledTimes(2);
expect(mockNotify).lastCalledWith(list.array);

list.splice(2, 1, "z");
expect(mockNotify).toHaveBeenCalledTimes(3);
expect(mockNotify).lastCalledWith(list.array);

dispose();
});

it("should not notify on splice if the list is empty.", () => {
const list = new ReactiveList();
const mockNotify = jest.fn();
const dispose = list.$.reaction(mockNotify, true);

list.splice(0, 0);
expect(mockNotify).not.toHaveBeenCalled();

list.splice(0, 0, "x", "y");
expect(mockNotify).lastCalledWith(["x", "y"]);

dispose();
});
});

describe("batchSet", () => {
it("should set the elements at the specified indices", () => {
const list = new ReactiveList([1, 2, 3]);
list.batchSet([
[0, 4],
[2, 5],
]);
expect(list.array).toEqual([4, 2, 5]);
});

it("should ignore negative index", () => {
const list = new ReactiveList([1, 2, 3]);
list.batchSet([
[-1, 4],
[-2, 5],
]);
expect(list.array).toEqual([1, 2, 3]);
});

it("should notify on batchSet", () => {
const list = new ReactiveList(["a", "b", "c"]);
const mockNotify = jest.fn();
const dispose = list.$.reaction(mockNotify, true);

list.batchSet([
[1, "x"],
[2, "y"],
]);
expect(mockNotify).toHaveBeenCalledTimes(1);
expect(mockNotify).lastCalledWith(list.array);

list.batchSet([
[0, "z"],
[1, "w"],
]);
expect(mockNotify).toHaveBeenCalledTimes(2);
expect(mockNotify).lastCalledWith(list.array);

dispose();
});

it("should not notify on batchSet for negative index", () => {
const list = new ReactiveList(["a", "b", "c"]);
const mockNotify = jest.fn();
const dispose = list.$.reaction(mockNotify, true);

list.batchSet([
[-2, "x"],
[-1, "y"],
]);
expect(mockNotify).not.toHaveBeenCalled();

dispose();
});
});

describe("insert", () => {
it("should insert an element at the specified index", () => {
const list = new ReactiveList([1, 2, 3]);
Expand Down
Loading

0 comments on commit e65e6ea

Please sign in to comment.