Skip to content

Commit 29c1ea7

Browse files
committed
fix: repeat must not mutate its result object in place (#785)
1 parent 4af32ec commit 29c1ea7

File tree

2 files changed

+89
-44
lines changed

2 files changed

+89
-44
lines changed

packages/range/src/repeat.ts

Lines changed: 51 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,60 +23,68 @@ export function repeat<T>(
2323
mapFn: (i: number) => T,
2424
options: { fallback?: Accessor<T> } = {},
2525
): Accessor<T[]> {
26-
let disposers: (() => void)[] = [],
27-
items: T[] = [],
28-
prevLen = 0;
29-
30-
onCleanup(() => disposers.forEach(f => f()));
26+
let prev: readonly T[] = [];
27+
let prevLen = 0;
28+
const disposers: (() => void)[] = [];
29+
onCleanup(() => {
30+
for (let index = 0; index < disposers.length; index++) {
31+
disposers[index]!();
32+
}
33+
});
3134

32-
const mapLength = (len: number): T[] => {
33-
if (len === 0) {
34-
disposers.forEach(f => f());
35+
// Truncate toward zero and force positive
36+
const memoLen = createMemo(() => Math.max(times() | 0, 0));
3537

36-
if (options.fallback)
37-
return createRoot(dispose => {
38-
disposers = [dispose];
39-
return (items = [options.fallback!()]);
40-
});
38+
return function mapLength(): T[] {
39+
const len = memoLen();
40+
if (len === prevLen) return prev as T[];
4141

42-
disposers = [];
43-
return (items = []);
42+
// Dispose of fallback or unnecessarry elements
43+
if (prevLen === 0) disposers[0]?.();
44+
else {
45+
for (let index = len; index < disposers.length; index++) {
46+
disposers[index]!();
47+
}
4448
}
4549

46-
if (prevLen === 0) {
47-
// after fallback case:
48-
if (disposers[0]) disposers[0]();
49-
for (let i = 0; i < len; i++) items[i] = createRoot(mapper.bind(void 0, i));
50-
return items;
51-
}
50+
// The following prefers to use `prev.slice` to
51+
// preserve any array element kind optimizations
52+
// the runtime has made.
5253

53-
{
54-
const diff = prevLen - len;
55-
if (diff > 0) {
56-
for (let i = prevLen - 1; i >= len; i--) disposers[i]!();
57-
items.splice(len, diff);
58-
disposers.splice(len, diff);
59-
return items;
54+
if (len === 0) {
55+
const fallback = options.fallback;
56+
if (fallback) {
57+
// Show fallback if available
58+
const next = prev.slice(0, 1);
59+
next[0] = createRoot(dispose => {
60+
disposers[0] = dispose;
61+
return fallback();
62+
});
63+
64+
disposers.length = 1;
65+
prevLen = 0;
66+
return (prev = next);
67+
} else {
68+
// Show empty array, otherwise
69+
disposers.length = 0;
70+
prevLen = 0;
71+
return (prev = prev.slice(0, 0));
6072
}
6173
}
6274

63-
for (let i = prevLen; i < len; i++) items[i] = createRoot(mapper.bind(void 0, i));
64-
return items;
65-
};
75+
const next = prev.slice(0, len);
6676

67-
const mapper = (index: number, dispose: () => void): T => {
68-
disposers[index] = dispose;
69-
return mapFn(index);
70-
};
77+
// Create new elements as needed
78+
for (let index = prevLen; index < len; index++) {
79+
next[index] = createRoot(dispose => {
80+
disposers[index] = dispose;
81+
return mapFn(index);
82+
});
83+
}
7184

72-
const memoLen = createMemo(() => Math.floor(Math.max(times(), 0)));
73-
return () => {
74-
const len = memoLen();
75-
return untrack(() => {
76-
const newItems = mapLength(len);
77-
prevLen = len;
78-
return newItems;
79-
});
85+
disposers.length = len;
86+
prevLen = len;
87+
return (prev = next);
8088
};
8189
}
8290

packages/range/test/repeat.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect, describe, it } from "vitest";
22
import { createComputed, createRoot, createSignal, onCleanup } from "solid-js";
3-
import { repeat } from "../src/index.js";
3+
import { Repeat, repeat } from "../src/index.js";
44

55
describe("repeat", () => {
66
it("maps only added items", () =>
@@ -83,3 +83,40 @@ describe("repeat", () => {
8383
expect(mapped(), "mapped after dispose").toEqual(["fb"]);
8484
}));
8585
});
86+
87+
describe("<Repeat/>", () => {
88+
it("notifies observers on length change", () => {
89+
const [length, setLength] = createSignal(3);
90+
91+
const [dispose, accessor] = createRoot(dispose => {
92+
const accessor = Repeat({
93+
get times() {
94+
return length();
95+
},
96+
fallback: () => 0,
97+
children: () => 1,
98+
}) as never as () => {};
99+
return [dispose, accessor];
100+
});
101+
102+
let notifications = 0;
103+
createComputed(() => {
104+
accessor();
105+
notifications++;
106+
});
107+
108+
expect(notifications).toEqual(1);
109+
setLength(4);
110+
expect(notifications).toEqual(2);
111+
setLength(0);
112+
expect(notifications).toEqual(3);
113+
setLength(2);
114+
expect(notifications).toEqual(4);
115+
setLength(1);
116+
expect(notifications).toEqual(5);
117+
setLength(1.5);
118+
expect(notifications).toEqual(5);
119+
120+
dispose();
121+
});
122+
});

0 commit comments

Comments
 (0)