Skip to content

Commit fe3b3b4

Browse files
trueadmRich-Harris
andauthored
feat: adds reactive Map class to svelte/reactivity (#10803)
* feat: adds reactive Map class to svelte/reactivity * add docs * add docs * add test case * types * make reactive set better * address feedback * fix typo * more efficient initialisation * this is incorrect, it would fail if given a map for example * increase consistency (with e.g. proxy.js) * tidy up * Revert "more efficient initialisation" This reverts commit 29d4a80. * efficient initialization, without bugs this time * convention * delete make_iterable * update changeset * efficient initialization * avoid generator functions * Update sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md --------- Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent c35f0c1 commit fe3b3b4

File tree

10 files changed

+466
-68
lines changed

10 files changed

+466
-68
lines changed

.changeset/fuzzy-bags-camp.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: add reactive Map class to svelte/reactivity

packages/svelte/src/internal/client/proxy.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export function proxy(value, immutable = true, owners) {
4848

4949
const prototype = get_prototype_of(value);
5050

51-
// TODO handle Map and Set as well
5251
if (prototype === object_prototype || prototype === array_prototype) {
5352
const proxy = new Proxy(value, state_proxy_handler);
5453

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { ReactiveDate as Date } from './date.js';
22
export { ReactiveSet as Set } from './set.js';
3+
export { ReactiveMap as Map } from './map.js';

packages/svelte/src/reactivity/map.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { DEV } from 'esm-env';
2+
import { source, set } from '../internal/client/reactivity/sources.js';
3+
import { get } from '../internal/client/runtime.js';
4+
import { UNINITIALIZED } from '../internal/client/constants.js';
5+
import { map } from './utils.js';
6+
7+
/**
8+
* @template K
9+
* @template V
10+
*/
11+
export class ReactiveMap extends Map {
12+
/** @type {Map<K, import('#client').Source<V>>} */
13+
#sources = new Map();
14+
#version = source(0);
15+
#size = source(0);
16+
17+
/**
18+
* @param {Iterable<readonly [K, V]> | null | undefined} [value]
19+
*/
20+
constructor(value) {
21+
super();
22+
23+
// If the value is invalid then the native exception will fire here
24+
if (DEV) new Map(value);
25+
26+
if (value) {
27+
var sources = this.#sources;
28+
29+
for (var [key, v] of value) {
30+
sources.set(key, source(v));
31+
super.set(key, v);
32+
}
33+
34+
this.#size.v = sources.size;
35+
}
36+
}
37+
38+
#increment_version() {
39+
set(this.#version, this.#version.v + 1);
40+
}
41+
42+
/** @param {K} key */
43+
has(key) {
44+
var s = this.#sources.get(key);
45+
46+
if (s === undefined) {
47+
// We should always track the version in case
48+
// the Set ever gets this value in the future.
49+
get(this.#version);
50+
51+
return false;
52+
}
53+
54+
get(s);
55+
return true;
56+
}
57+
58+
/**
59+
* @param {(value: V, key: K, map: Map<K, V>) => void} callbackfn
60+
* @param {any} [this_arg]
61+
*/
62+
forEach(callbackfn, this_arg) {
63+
get(this.#version);
64+
65+
return super.forEach(callbackfn, this_arg);
66+
}
67+
68+
/** @param {K} key */
69+
get(key) {
70+
var s = this.#sources.get(key);
71+
72+
if (s === undefined) {
73+
// We should always track the version in case
74+
// the Set ever gets this value in the future.
75+
get(this.#version);
76+
77+
return undefined;
78+
}
79+
80+
return get(s);
81+
}
82+
83+
/**
84+
* @param {K} key
85+
* @param {V} value
86+
* */
87+
set(key, value) {
88+
var sources = this.#sources;
89+
var s = sources.get(key);
90+
91+
if (s === undefined) {
92+
sources.set(key, source(value));
93+
set(this.#size, sources.size);
94+
this.#increment_version();
95+
} else {
96+
set(s, value);
97+
}
98+
99+
return super.set(key, value);
100+
}
101+
102+
/** @param {K} key */
103+
delete(key) {
104+
var sources = this.#sources;
105+
var s = sources.get(key);
106+
107+
if (s !== undefined) {
108+
sources.delete(key);
109+
set(this.#size, sources.size);
110+
set(s, /** @type {V} */ (UNINITIALIZED));
111+
this.#increment_version();
112+
}
113+
114+
return super.delete(key);
115+
}
116+
117+
clear() {
118+
var sources = this.#sources;
119+
120+
if (sources.size !== 0) {
121+
set(this.#size, 0);
122+
for (var s of sources.values()) {
123+
set(s, /** @type {V} */ (UNINITIALIZED));
124+
}
125+
this.#increment_version();
126+
}
127+
128+
sources.clear();
129+
super.clear();
130+
}
131+
132+
keys() {
133+
get(this.#version);
134+
return this.#sources.keys();
135+
}
136+
137+
values() {
138+
get(this.#version);
139+
return map(this.#sources.values(), get);
140+
}
141+
142+
entries() {
143+
get(this.#version);
144+
return map(
145+
this.#sources.entries(),
146+
([key, source]) => /** @type {[K, V]} */ ([key, get(source)])
147+
);
148+
}
149+
150+
[Symbol.iterator]() {
151+
return this.entries();
152+
}
153+
154+
get size() {
155+
return get(this.#size);
156+
}
157+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js';
2+
import { flushSync } from '../main/main-client.js';
3+
import { ReactiveMap } from './map.js';
4+
import { assert, test } from 'vitest';
5+
6+
test('map.values()', () => {
7+
const map = new ReactiveMap([
8+
[1, 1],
9+
[2, 2],
10+
[3, 3],
11+
[4, 4],
12+
[5, 5]
13+
]);
14+
15+
const log: any = [];
16+
17+
const cleanup = user_root_effect(() => {
18+
pre_effect(() => {
19+
log.push(map.size);
20+
});
21+
22+
pre_effect(() => {
23+
log.push(map.has(3));
24+
});
25+
26+
pre_effect(() => {
27+
log.push(Array.from(map.values()));
28+
});
29+
});
30+
31+
flushSync(() => {
32+
map.delete(3);
33+
});
34+
35+
flushSync(() => {
36+
map.clear();
37+
});
38+
39+
assert.deepEqual(log, [5, true, [1, 2, 3, 4, 5], 4, false, [1, 2, 4, 5], 0, [], false]); // TODO update when we fix effect ordering bug
40+
41+
cleanup();
42+
});
43+
44+
test('map.get(...)', () => {
45+
const map = new ReactiveMap([
46+
[1, 1],
47+
[2, 2],
48+
[3, 3]
49+
]);
50+
51+
const log: any = [];
52+
53+
const cleanup = user_root_effect(() => {
54+
pre_effect(() => {
55+
log.push('get 1', map.get(1));
56+
});
57+
58+
pre_effect(() => {
59+
log.push('get 2', map.get(2));
60+
});
61+
62+
pre_effect(() => {
63+
log.push('get 3', map.get(3));
64+
});
65+
});
66+
67+
flushSync(() => {
68+
map.delete(2);
69+
});
70+
71+
flushSync(() => {
72+
map.set(2, 2);
73+
});
74+
75+
assert.deepEqual(log, ['get 1', 1, 'get 2', 2, 'get 3', 3, 'get 2', undefined, 'get 2', 2]);
76+
77+
cleanup();
78+
});
79+
80+
test('map.has(...)', () => {
81+
const map = new ReactiveMap([
82+
[1, 1],
83+
[2, 2],
84+
[3, 3]
85+
]);
86+
87+
const log: any = [];
88+
89+
const cleanup = user_root_effect(() => {
90+
pre_effect(() => {
91+
log.push('has 1', map.has(1));
92+
});
93+
94+
pre_effect(() => {
95+
log.push('has 2', map.has(2));
96+
});
97+
98+
pre_effect(() => {
99+
log.push('has 3', map.has(3));
100+
});
101+
});
102+
103+
flushSync(() => {
104+
map.delete(2);
105+
});
106+
107+
flushSync(() => {
108+
map.set(2, 2);
109+
});
110+
111+
assert.deepEqual(log, [
112+
'has 1',
113+
true,
114+
'has 2',
115+
true,
116+
'has 3',
117+
true,
118+
'has 2',
119+
false,
120+
'has 2',
121+
true
122+
]);
123+
124+
cleanup();
125+
});
126+
127+
test('map handling of undefined values', () => {
128+
const map = new ReactiveMap();
129+
130+
const log: any = [];
131+
132+
const cleanup = user_root_effect(() => {
133+
map.set(1, undefined);
134+
135+
pre_effect(() => {
136+
log.push(map.get(1));
137+
});
138+
139+
flushSync(() => {
140+
map.delete(1);
141+
});
142+
143+
flushSync(() => {
144+
map.set(1, 1);
145+
});
146+
});
147+
148+
assert.deepEqual(log, [undefined, undefined, 1]);
149+
150+
cleanup();
151+
});

0 commit comments

Comments
 (0)