Skip to content

Commit

Permalink
feat: adds reactive Map class to svelte/reactivity (#10803)
Browse files Browse the repository at this point in the history
* 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 <rich.harris@vercel.com>
Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
  • Loading branch information
3 people authored Mar 14, 2024
1 parent c35f0c1 commit fe3b3b4
Show file tree
Hide file tree
Showing 10 changed files with 466 additions and 68 deletions.
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

0 comments on commit fe3b3b4

Please sign in to comment.