Skip to content

Commit d13a60f

Browse files
committed
feat(collections): add replace
1 parent d76e301 commit d13a60f

File tree

7 files changed

+437
-0
lines changed

7 files changed

+437
-0
lines changed

src/collections/reactiveArray.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,22 @@ export class OwnedReactiveArray<V> extends Array<V> implements ReadableProvider<
167167
return this;
168168
}
169169

170+
/**
171+
* Replaces the contents of the array with the provided items.
172+
* @param items The new items to replace the contents of the array with.
173+
* @returns The array itself.
174+
*/
175+
public replace(items: Iterable<V>): this {
176+
const isBatchTop = batchStart();
177+
let i = 0;
178+
for (const item of items) {
179+
this.set(i++, item);
180+
}
181+
this.setLength(i);
182+
isBatchTop && batchFlush();
183+
return this;
184+
}
185+
170186
public dispose(): void {
171187
if (this._disposed_) return;
172188
if (process.env.NODE_ENV !== "production") {

src/collections/reactiveMap.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { batchFlush, batchStart, type BatchTask, batchTasks } from "../batch";
2+
import { context } from "../context";
23
import { type EventObject, on, type RemoveListener, send, size } from "../event";
34
import { type ReadableProvider, type OwnedWritable, type Readable } from "../interface";
45
import { writable } from "../readable";
@@ -201,6 +202,28 @@ export class OwnedReactiveMap<K, V> extends Map<K, V> implements ReadableProvide
201202
}
202203
}
203204

205+
/**
206+
* Replace the contents of the map with the given entries.
207+
* @param entries - The new entries to replace the map with.
208+
* @returns The map itself.
209+
*/
210+
public replace(entries: Iterable<readonly [K, V]>): this {
211+
const isBatchTop = batchStart();
212+
const newKeys: Set<K> = (context.replaceMarkers_ ??= new Set());
213+
for (const [key, value] of entries) {
214+
newKeys.add(key);
215+
this.set(key, value);
216+
}
217+
for (const key of this.keys()) {
218+
if (!newKeys.has(key)) {
219+
this.delete(key);
220+
}
221+
}
222+
newKeys.clear();
223+
isBatchTop && batchFlush();
224+
return this;
225+
}
226+
204227
/** @internal */
205228
private _disposed_?: Error | true;
206229

src/collections/reactiveSet.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { batchFlush, batchStart, type BatchTask, batchTasks } from "../batch";
2+
import { context } from "../context";
23
import { type EventObject, on, type RemoveListener, send, size } from "../event";
34
import { type ReadableProvider, type OwnedWritable, type Readable } from "../interface";
45
import { writable } from "../readable";
@@ -186,6 +187,28 @@ export class OwnedReactiveSet<V> extends Set<V> implements ReadableProvider<Read
186187
}
187188
}
188189

190+
/**
191+
* Replace the contents of the set with the given values.
192+
* @param values - The new values to replace the set with.
193+
* @returns The set itself.
194+
*/
195+
public replace(values: Iterable<V>): this {
196+
const isBatchTop = batchStart();
197+
const newValues: Set<V> = (context.replaceMarkers_ ??= new Set());
198+
for (const value of values) {
199+
newValues.add(value);
200+
this.add(value);
201+
}
202+
for (const value of this) {
203+
if (!newValues.has(value)) {
204+
this.delete(value);
205+
}
206+
}
207+
newValues.clear();
208+
isBatchTop && batchFlush();
209+
return this;
210+
}
211+
189212
/** @internal */
190213
private _disposed_?: Error | true;
191214

src/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BRAND } from "./utils";
44
export interface Context {
55
batching_: boolean;
66
readonly batchTask_: Set<BatchTask>;
7+
replaceMarkers_?: Set<any>;
78
}
89

910
declare const globalThis: {

test/collections/reactiveArray.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,103 @@ describe("ReactiveArray", () => {
650650
});
651651
});
652652

653+
describe("replace", () => {
654+
it("should replace the array with the specified elements", () => {
655+
const arr = reactiveArray([1, 2, 3]);
656+
const spy = vi.fn();
657+
arr.$.reaction(spy);
658+
659+
expect(spy).toHaveBeenCalledTimes(0);
660+
661+
arr.replace([4, 5, 6]);
662+
663+
expect(spy).toHaveBeenCalledTimes(1);
664+
665+
expect(arr).toEqual([4, 5, 6]);
666+
});
667+
668+
it("should replace the array with less elements", () => {
669+
const arr = reactiveArray([1, 2, 3]);
670+
const spy = vi.fn();
671+
arr.$.reaction(spy);
672+
673+
expect(spy).toHaveBeenCalledTimes(0);
674+
675+
arr.replace([1]);
676+
677+
expect(spy).toHaveBeenCalledTimes(1);
678+
679+
expect(arr).toEqual([1]);
680+
});
681+
682+
it("should replace the array with different order", () => {
683+
const arr = reactiveArray([1, 2, 3]);
684+
const spy = vi.fn();
685+
arr.$.reaction(spy);
686+
687+
expect(spy).toHaveBeenCalledTimes(0);
688+
689+
arr.replace([3, 2, 1]);
690+
691+
expect(spy).toHaveBeenCalledTimes(1);
692+
693+
expect(arr).toEqual([3, 2, 1]);
694+
});
695+
696+
it("should replace an empty array with the specified elements", () => {
697+
const arr = reactiveArray<number>([]);
698+
699+
const spy = vi.fn();
700+
arr.$.reaction(spy);
701+
702+
expect(spy).toHaveBeenCalledTimes(0);
703+
704+
arr.replace([1, 2, 3]);
705+
706+
expect(spy).toHaveBeenCalledTimes(1);
707+
708+
expect(arr).toEqual([1, 2, 3]);
709+
});
710+
711+
it("should notify on replace", () => {
712+
const arr = reactiveArray(["a", "b", "c"]);
713+
const mockNotify = vi.fn();
714+
const dispose = arr.$.reaction(mockNotify);
715+
716+
arr.replace(["x", "y", "z"]);
717+
expect(mockNotify).toHaveBeenCalledTimes(1);
718+
expect(mockNotify).lastCalledWith(arr);
719+
720+
dispose();
721+
});
722+
723+
it("should not notify if not changed", () => {
724+
const arr = reactiveArray([1]);
725+
const mockNotify = vi.fn();
726+
const dispose = arr.$.reaction(mockNotify);
727+
728+
arr.replace([2, 3]);
729+
expect(mockNotify).toHaveBeenCalledTimes(1);
730+
expect(mockNotify).lastCalledWith(arr);
731+
732+
dispose();
733+
});
734+
735+
it("should notify if some keys are removed", () => {
736+
const arr = reactiveArray([1, 2, 3]);
737+
const mockNotify = vi.fn();
738+
const dispose = arr.$.reaction(mockNotify);
739+
740+
expect(arr).toEqual([1, 2, 3]);
741+
742+
arr.replace([1, 2]);
743+
expect(mockNotify).toHaveBeenCalledTimes(1);
744+
expect(arr).toEqual([1, 2]);
745+
746+
dispose();
747+
});
748+
});
749+
653750
describe.each(["test", "production"])("dispose [%s]", NODE_ENV => {
654751
const originalEnv = process.env.NODE_ENV;
655752

0 commit comments

Comments
 (0)