Skip to content

feat: adds reactive Map class to svelte/reactivity #10803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fuzzy-bags-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

feat: add reactive Map class to svelte/reactivity
1 change: 0 additions & 1 deletion packages/svelte/src/internal/client/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export function proxy(value, immutable = true, owners) {

const prototype = get_prototype_of(value);

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

Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/reactivity/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { ReactiveDate as Date } from './date.js';
export { ReactiveSet as Set } from './set.js';
export { ReactiveMap as Map } from './map.js';
157 changes: 157 additions & 0 deletions packages/svelte/src/reactivity/map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
import { UNINITIALIZED } from '../internal/client/constants.js';
import { map } from './utils.js';

/**
* @template K
* @template V
*/
export class ReactiveMap extends Map {
/** @type {Map<K, import('#client').Source<V>>} */
#sources = new Map();
#version = source(0);
#size = source(0);

/**
* @param {Iterable<readonly [K, V]> | null | undefined} [value]
*/
constructor(value) {
super();

// If the value is invalid then the native exception will fire here
if (DEV) new Map(value);

if (value) {
var sources = this.#sources;

for (var [key, v] of value) {
sources.set(key, source(v));
super.set(key, v);
}

this.#size.v = sources.size;
}
}

#increment_version() {
set(this.#version, this.#version.v + 1);
}

/** @param {K} key */
has(key) {
var s = this.#sources.get(key);

if (s === undefined) {
// We should always track the version in case
// the Set ever gets this value in the future.
get(this.#version);

return false;
}

get(s);
return true;
}

/**
* @param {(value: V, key: K, map: Map<K, V>) => void} callbackfn
* @param {any} [this_arg]
*/
forEach(callbackfn, this_arg) {
get(this.#version);

return super.forEach(callbackfn, this_arg);
}

/** @param {K} key */
get(key) {
var s = this.#sources.get(key);

if (s === undefined) {
// We should always track the version in case
// the Set ever gets this value in the future.
get(this.#version);

return undefined;
}

return get(s);
}

/**
* @param {K} key
* @param {V} value
* */
set(key, value) {
var sources = this.#sources;
var s = sources.get(key);

if (s === undefined) {
sources.set(key, source(value));
set(this.#size, sources.size);
this.#increment_version();
} else {
set(s, value);
}

return super.set(key, value);
}

/** @param {K} key */
delete(key) {
var sources = this.#sources;
var s = sources.get(key);

if (s !== undefined) {
sources.delete(key);
set(this.#size, sources.size);
set(s, /** @type {V} */ (UNINITIALIZED));
this.#increment_version();
}

return super.delete(key);
}

clear() {
var sources = this.#sources;

if (sources.size !== 0) {
set(this.#size, 0);
for (var s of sources.values()) {
set(s, /** @type {V} */ (UNINITIALIZED));
}
this.#increment_version();
}

sources.clear();
super.clear();
}

keys() {
get(this.#version);
return this.#sources.keys();
}

values() {
get(this.#version);
return map(this.#sources.values(), get);
}

entries() {
get(this.#version);
return map(
this.#sources.entries(),
([key, source]) => /** @type {[K, V]} */ ([key, get(source)])
);
}

[Symbol.iterator]() {
return this.entries();
}

get size() {
return get(this.#size);
}
}
151 changes: 151 additions & 0 deletions packages/svelte/src/reactivity/map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../main/main-client.js';
import { ReactiveMap } from './map.js';
import { assert, test } from 'vitest';

test('map.values()', () => {
const map = new ReactiveMap([
[1, 1],
[2, 2],
[3, 3],
[4, 4],
[5, 5]
]);

const log: any = [];

const cleanup = user_root_effect(() => {
pre_effect(() => {
log.push(map.size);
});

pre_effect(() => {
log.push(map.has(3));
});

pre_effect(() => {
log.push(Array.from(map.values()));
});
});

flushSync(() => {
map.delete(3);
});

flushSync(() => {
map.clear();
});

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

cleanup();
});

test('map.get(...)', () => {
const map = new ReactiveMap([
[1, 1],
[2, 2],
[3, 3]
]);

const log: any = [];

const cleanup = user_root_effect(() => {
pre_effect(() => {
log.push('get 1', map.get(1));
});

pre_effect(() => {
log.push('get 2', map.get(2));
});

pre_effect(() => {
log.push('get 3', map.get(3));
});
});

flushSync(() => {
map.delete(2);
});

flushSync(() => {
map.set(2, 2);
});

assert.deepEqual(log, ['get 1', 1, 'get 2', 2, 'get 3', 3, 'get 2', undefined, 'get 2', 2]);

cleanup();
});

test('map.has(...)', () => {
const map = new ReactiveMap([
[1, 1],
[2, 2],
[3, 3]
]);

const log: any = [];

const cleanup = user_root_effect(() => {
pre_effect(() => {
log.push('has 1', map.has(1));
});

pre_effect(() => {
log.push('has 2', map.has(2));
});

pre_effect(() => {
log.push('has 3', map.has(3));
});
});

flushSync(() => {
map.delete(2);
});

flushSync(() => {
map.set(2, 2);
});

assert.deepEqual(log, [
'has 1',
true,
'has 2',
true,
'has 3',
true,
'has 2',
false,
'has 2',
true
]);

cleanup();
});

test('map handling of undefined values', () => {
const map = new ReactiveMap();

const log: any = [];

const cleanup = user_root_effect(() => {
map.set(1, undefined);

pre_effect(() => {
log.push(map.get(1));
});

flushSync(() => {
map.delete(1);
});

flushSync(() => {
map.set(1, 1);
});
});

assert.deepEqual(log, [undefined, undefined, 1]);

cleanup();
});
Loading