feat: adds reactive Map class to svelte/reactivity

pull/10803/head
Dominic Gannaway 2 years ago
parent ffb27f667a
commit 010a78dbce

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: adds reactive Map class to svelte/reactivity

@ -1,2 +1,3 @@
export { ReactiveDate as Date } from './date.js'; export { ReactiveDate as Date } from './date.js';
export { ReactiveSet as Set } from './set.js'; export { ReactiveSet as Set } from './set.js';
export { ReactiveMap as Map } from './map.js';

@ -0,0 +1,185 @@
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 { make_iterable } 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) {
for (var [key, v] of value) {
this.set(key, v);
}
}
}
#increment_version() {
set(this.#version, this.#version.v + 1);
}
/** @param {K} key */
has(key) {
var source = this.#sources.get(key);
if (source === undefined) {
// We should always track the version in case
// the Set ever gets this value in the future.
get(this.#version);
return false;
}
get(source);
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 source = this.#sources.get(key);
if (source === 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(source);
}
/**
* @param {K} key
* @param {V} value
* */
set(key, value) {
var sources = this.#sources;
var source_value = sources.get(key);
if (source_value === undefined) {
sources.set(key, source(value));
set(this.#size, sources.size);
this.#increment_version();
} else {
set(source_value, value);
}
return super.set(key, value);
}
/** @param {K} key */
delete(key) {
var sources = this.#sources;
var source = sources.get(key);
if (source !== undefined) {
sources.delete(key);
set(this.#size, sources.size);
set(source, /** @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 source of sources.values()) {
set(source, /** @type {V} */ (UNINITIALIZED));
}
this.#increment_version();
}
sources.clear();
super.clear();
}
keys() {
get(this.#version);
var iterator = this.#sources.keys();
return make_iterable(
/** @type {IterableIterator<K>} */ ({
next() {
for (var value of iterator) {
return { value, done: false };
}
return { done: true };
}
})
);
}
values() {
get(this.#version);
var iterator = this.#sources.values();
return make_iterable(
/** @type {IterableIterator<V>} */ ({
next() {
for (var source of iterator) {
return { value: get(source), done: false };
}
return { done: true };
}
})
);
}
entries() {
get(this.#version);
var iterator = this.#sources.entries();
return make_iterable(
/** @type {IterableIterator<[K, V]>} */ ({
next() {
for (var [key, source] of iterator) {
return { value: [key, get(source)], done: false };
}
return { done: true };
}
})
);
}
[Symbol.iterator]() {
return this.entries();
}
get size() {
return get(this.#size);
}
}

@ -0,0 +1,125 @@
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();
});

@ -1,6 +1,7 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js'; import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js'; import { get } from '../internal/client/runtime.js';
import { make_iterable } from './utils.js';
var read = [ var read = [
'difference', 'difference',
@ -13,22 +14,6 @@ var read = [
'union' 'union'
]; ];
/**
* @template T
* @param {IterableIterator<T>} iterator
*/
function make_iterable(iterator) {
iterator[Symbol.iterator] = get_self;
return iterator;
}
/**
* @this {any}
*/
function get_self() {
return this;
}
var inited = false; var inited = false;
/** /**

@ -0,0 +1,15 @@
/**
* @template T
* @param {IterableIterator<T>} iterator
*/
export function make_iterable(iterator) {
iterator[Symbol.iterator] = get_self;
return iterator;
}
/**
* @this {any}
*/
function get_self() {
return this;
}

@ -0,0 +1,50 @@
import { flushSync } from '../../../../src/main/main-client';
import { test } from '../../test';
export default test({
html: `<button>add</button><button>delete</button><button>clear</button>`,
test({ assert, target }) {
const [btn, btn2, btn3] = target.querySelectorAll('button');
flushSync(() => {
btn?.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>add</button><button>delete</button><button>clear</button><div>1:1</div>`
);
flushSync(() => {
btn?.click();
});
flushSync(() => {
btn?.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>add</button><button>delete</button><button>clear</button><div>1:1</div><div>2:2</div><div>3:3</div>`
);
flushSync(() => {
btn2?.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>add</button><button>delete</button><button>clear</button><div>1:1</div><div>2:2</div>`
);
flushSync(() => {
btn3?.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>add</button><button>delete</button><button>clear</button>`
);
}
});

@ -0,0 +1,21 @@
<script>
import {Map} from 'svelte/reactivity';
let state = new Map();
</script>
<button onclick={() => {
state.set(state.size + 1, state.size + 1);
}}>add</button>
<button onclick={() => {
state.delete(state.size);
}}>delete</button>
<button onclick={() => {
state.clear();
}}>clear</button>
{#each state as [key, value]}
<div>{key}:{value}</div>
{/each}
Loading…
Cancel
Save