mirror of https://github.com/sveltejs/svelte
feat: add reactive Set class to svelte/reactivity (#10781)
* feat: add reactive Set class to svelte/reactivity * add some type safety * simplify, read entries lazily * failing unit test * fix deletions * minor tweaks * work around effect ordering bug * simplify, make entries lazy * small tweak * use var, minor tweaks --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>pull/10770/head
parent
eb40417c36
commit
3f7fcf9aec
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"svelte": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: add reactive Set class to svelte/reactivity
|
@ -0,0 +1,101 @@
|
|||||||
|
import { source, set } from '../internal/client/reactivity/sources.js';
|
||||||
|
import { get } from '../internal/client/runtime.js';
|
||||||
|
|
||||||
|
/** @type {Array<keyof Date>} */
|
||||||
|
const read = [
|
||||||
|
'getDate',
|
||||||
|
'getDay',
|
||||||
|
'getFullYear',
|
||||||
|
'getHours',
|
||||||
|
'getMilliseconds',
|
||||||
|
'getMinutes',
|
||||||
|
'getMonth',
|
||||||
|
'getSeconds',
|
||||||
|
'getTime',
|
||||||
|
'getTimezoneOffset',
|
||||||
|
'getUTCDate',
|
||||||
|
'getUTCDay',
|
||||||
|
'getUTCFullYear',
|
||||||
|
'getUTCHours',
|
||||||
|
'getUTCMilliseconds',
|
||||||
|
'getUTCMinutes',
|
||||||
|
'getUTCMonth',
|
||||||
|
'getUTCSeconds',
|
||||||
|
// @ts-expect-error this is deprecated
|
||||||
|
'getYear',
|
||||||
|
'toDateString',
|
||||||
|
'toISOString',
|
||||||
|
'toJSON',
|
||||||
|
'toLocaleDateString',
|
||||||
|
'toLocaleString',
|
||||||
|
'toLocaleTimeString',
|
||||||
|
'toString',
|
||||||
|
'toTimeString',
|
||||||
|
'toUTCString'
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @type {Array<keyof Date>} */
|
||||||
|
const write = [
|
||||||
|
'setDate',
|
||||||
|
'setFullYear',
|
||||||
|
'setHours',
|
||||||
|
'setMilliseconds',
|
||||||
|
'setMinutes',
|
||||||
|
'setMonth',
|
||||||
|
'setSeconds',
|
||||||
|
'setTime',
|
||||||
|
'setUTCDate',
|
||||||
|
'setUTCFullYear',
|
||||||
|
'setUTCHours',
|
||||||
|
'setUTCMilliseconds',
|
||||||
|
'setUTCMinutes',
|
||||||
|
'setUTCMonth',
|
||||||
|
'setUTCSeconds',
|
||||||
|
// @ts-expect-error this is deprecated
|
||||||
|
'setYear'
|
||||||
|
];
|
||||||
|
|
||||||
|
export class ReactiveDate extends Date {
|
||||||
|
#raw_time = source(super.getTime());
|
||||||
|
static #inited = false;
|
||||||
|
|
||||||
|
// We init as part of the first instance so that we can treeshake this class
|
||||||
|
#init() {
|
||||||
|
if (!ReactiveDate.#inited) {
|
||||||
|
ReactiveDate.#inited = true;
|
||||||
|
const proto = ReactiveDate.prototype;
|
||||||
|
const date_proto = Date.prototype;
|
||||||
|
|
||||||
|
for (const method of read) {
|
||||||
|
// @ts-ignore
|
||||||
|
proto[method] = function () {
|
||||||
|
get(this.#raw_time);
|
||||||
|
// @ts-ignore
|
||||||
|
return date_proto[method].call(this);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const method of write) {
|
||||||
|
// @ts-ignore
|
||||||
|
proto[method] = function (/** @type {any} */ ...args) {
|
||||||
|
// @ts-ignore
|
||||||
|
const v = date_proto[method].apply(this, args);
|
||||||
|
const time = date_proto.getTime.call(this);
|
||||||
|
if (time !== this.#raw_time.v) {
|
||||||
|
set(this.#raw_time, time);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any[]} values
|
||||||
|
*/
|
||||||
|
constructor(...values) {
|
||||||
|
// @ts-ignore
|
||||||
|
super(...values);
|
||||||
|
this.#init();
|
||||||
|
}
|
||||||
|
}
|
@ -1,103 +1,2 @@
|
|||||||
import { source, set } from '../internal/client/reactivity/sources.js';
|
export { ReactiveDate as Date } from './date.js';
|
||||||
import { get } from '../internal/client/runtime.js';
|
export { ReactiveSet as Set } from './set.js';
|
||||||
|
|
||||||
/** @type {Array<keyof Date>} */
|
|
||||||
const read = [
|
|
||||||
'getDate',
|
|
||||||
'getDay',
|
|
||||||
'getFullYear',
|
|
||||||
'getHours',
|
|
||||||
'getMilliseconds',
|
|
||||||
'getMinutes',
|
|
||||||
'getMonth',
|
|
||||||
'getSeconds',
|
|
||||||
'getTime',
|
|
||||||
'getTimezoneOffset',
|
|
||||||
'getUTCDate',
|
|
||||||
'getUTCDay',
|
|
||||||
'getUTCFullYear',
|
|
||||||
'getUTCHours',
|
|
||||||
'getUTCMilliseconds',
|
|
||||||
'getUTCMinutes',
|
|
||||||
'getUTCMonth',
|
|
||||||
'getUTCSeconds',
|
|
||||||
// @ts-expect-error this is deprecated
|
|
||||||
'getYear',
|
|
||||||
'toDateString',
|
|
||||||
'toISOString',
|
|
||||||
'toJSON',
|
|
||||||
'toLocaleDateString',
|
|
||||||
'toLocaleString',
|
|
||||||
'toLocaleTimeString',
|
|
||||||
'toString',
|
|
||||||
'toTimeString',
|
|
||||||
'toUTCString'
|
|
||||||
];
|
|
||||||
|
|
||||||
/** @type {Array<keyof Date>} */
|
|
||||||
const write = [
|
|
||||||
'setDate',
|
|
||||||
'setFullYear',
|
|
||||||
'setHours',
|
|
||||||
'setMilliseconds',
|
|
||||||
'setMinutes',
|
|
||||||
'setMonth',
|
|
||||||
'setSeconds',
|
|
||||||
'setTime',
|
|
||||||
'setUTCDate',
|
|
||||||
'setUTCFullYear',
|
|
||||||
'setUTCHours',
|
|
||||||
'setUTCMilliseconds',
|
|
||||||
'setUTCMinutes',
|
|
||||||
'setUTCMonth',
|
|
||||||
'setUTCSeconds',
|
|
||||||
// @ts-expect-error this is deprecated
|
|
||||||
'setYear'
|
|
||||||
];
|
|
||||||
|
|
||||||
class ReactiveDate extends Date {
|
|
||||||
#raw_time = source(super.getTime());
|
|
||||||
static #inited = false;
|
|
||||||
|
|
||||||
// We init as part of the first instance so that we can treeshake this class
|
|
||||||
#init() {
|
|
||||||
if (!ReactiveDate.#inited) {
|
|
||||||
ReactiveDate.#inited = true;
|
|
||||||
const proto = ReactiveDate.prototype;
|
|
||||||
const date_proto = Date.prototype;
|
|
||||||
|
|
||||||
for (const method of read) {
|
|
||||||
// @ts-ignore
|
|
||||||
proto[method] = function () {
|
|
||||||
get(this.#raw_time);
|
|
||||||
// @ts-ignore
|
|
||||||
return date_proto[method].call(this);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const method of write) {
|
|
||||||
// @ts-ignore
|
|
||||||
proto[method] = function (/** @type {any} */ ...args) {
|
|
||||||
// @ts-ignore
|
|
||||||
const v = date_proto[method].apply(this, args);
|
|
||||||
const time = date_proto.getTime.call(this);
|
|
||||||
if (time !== this.#raw_time.v) {
|
|
||||||
set(this.#raw_time, time);
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any[]} values
|
|
||||||
*/
|
|
||||||
constructor(...values) {
|
|
||||||
// @ts-ignore
|
|
||||||
super(...values);
|
|
||||||
this.#init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { ReactiveDate as Date };
|
|
||||||
|
@ -0,0 +1,182 @@
|
|||||||
|
import { DEV } from 'esm-env';
|
||||||
|
import { source, set } from '../internal/client/reactivity/sources.js';
|
||||||
|
import { get } from '../internal/client/runtime.js';
|
||||||
|
|
||||||
|
var read = [
|
||||||
|
'difference',
|
||||||
|
'forEach',
|
||||||
|
'intersection',
|
||||||
|
'isDisjointFrom',
|
||||||
|
'isSubsetOf',
|
||||||
|
'isSupersetOf',
|
||||||
|
'symmetricDifference',
|
||||||
|
'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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
*/
|
||||||
|
export class ReactiveSet extends Set {
|
||||||
|
/** @type {Map<T, import('#client').Source<boolean>>} */
|
||||||
|
#sources = new Map();
|
||||||
|
#version = source(0);
|
||||||
|
#size = source(0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Iterable<T> | null | undefined} value
|
||||||
|
*/
|
||||||
|
constructor(value) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// If the value is invalid then the native exception will fire here
|
||||||
|
if (DEV) new Set(value);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
for (var element of value) {
|
||||||
|
this.add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inited) this.#init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We init as part of the first instance so that we can treeshake this class
|
||||||
|
#init() {
|
||||||
|
inited = true;
|
||||||
|
|
||||||
|
var proto = ReactiveSet.prototype;
|
||||||
|
var set_proto = Set.prototype;
|
||||||
|
|
||||||
|
for (var method of read) {
|
||||||
|
// @ts-ignore
|
||||||
|
proto[method] = function (...v) {
|
||||||
|
get(this.#version);
|
||||||
|
// @ts-ignore
|
||||||
|
return set_proto[method].apply(this, v);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#increment_version() {
|
||||||
|
set(this.#version, this.#version.v + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {T} value */
|
||||||
|
has(value) {
|
||||||
|
var source = this.#sources.get(value);
|
||||||
|
|
||||||
|
if (source === undefined) {
|
||||||
|
get(this.#version);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return get(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {T} value */
|
||||||
|
add(value) {
|
||||||
|
var sources = this.#sources;
|
||||||
|
|
||||||
|
if (!sources.has(value)) {
|
||||||
|
sources.set(value, source(true));
|
||||||
|
set(this.#size, sources.size);
|
||||||
|
this.#increment_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {T} value */
|
||||||
|
delete(value) {
|
||||||
|
var sources = this.#sources;
|
||||||
|
var source = sources.get(value);
|
||||||
|
|
||||||
|
if (source !== undefined) {
|
||||||
|
sources.delete(value);
|
||||||
|
set(this.#size, sources.size);
|
||||||
|
set(source, false);
|
||||||
|
this.#increment_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.delete(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
var sources = this.#sources;
|
||||||
|
|
||||||
|
if (sources.size !== 0) {
|
||||||
|
set(this.#size, 0);
|
||||||
|
for (var source of sources.values()) {
|
||||||
|
set(source, false);
|
||||||
|
}
|
||||||
|
this.#increment_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
sources.clear();
|
||||||
|
super.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
return this.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
values() {
|
||||||
|
get(this.#version);
|
||||||
|
|
||||||
|
var iterator = this.#sources.keys();
|
||||||
|
|
||||||
|
return make_iterable(
|
||||||
|
/** @type {IterableIterator<T>} */ ({
|
||||||
|
next() {
|
||||||
|
for (var value of iterator) {
|
||||||
|
return { value, done: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { done: true };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries() {
|
||||||
|
var iterator = this.values();
|
||||||
|
|
||||||
|
return make_iterable(
|
||||||
|
/** @type {IterableIterator<[T, T]>} */ ({
|
||||||
|
next() {
|
||||||
|
for (var value of iterator) {
|
||||||
|
return { value: [value, value], done: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { done: true };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return get(this.#size);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js';
|
||||||
|
import { flushSync } from '../main/main-client.js';
|
||||||
|
import { ReactiveSet } from './set.js';
|
||||||
|
import { assert, test } from 'vitest';
|
||||||
|
|
||||||
|
test('set.values()', () => {
|
||||||
|
const set = new ReactiveSet([1, 2, 3, 4, 5]);
|
||||||
|
|
||||||
|
const log: any = [];
|
||||||
|
|
||||||
|
const cleanup = user_root_effect(() => {
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push(set.size);
|
||||||
|
});
|
||||||
|
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push(set.has(3));
|
||||||
|
});
|
||||||
|
|
||||||
|
pre_effect(() => {
|
||||||
|
log.push(Array.from(set));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
set.delete(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
set.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO looks like another effect ordering bug — sequence should be <size, has, values>,
|
||||||
|
// but values is reversed at end
|
||||||
|
assert.deepEqual(log, [5, true, [1, 2, 3, 4, 5], 4, false, [1, 2, 4, 5], 0, [], false]);
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
});
|
@ -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</div>`
|
||||||
|
);
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn?.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn?.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`<button>add</button><button>delete</button><button>clear</button><div>1</div><div>2</div><div>3</div>`
|
||||||
|
);
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn2?.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`<button>add</button><button>delete</button><button>clear</button><div>1</div><div>2</div>`
|
||||||
|
);
|
||||||
|
|
||||||
|
flushSync(() => {
|
||||||
|
btn3?.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`<button>add</button><button>delete</button><button>clear</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
import {Set} from 'svelte/reactivity';
|
||||||
|
|
||||||
|
let state = new Set();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button onclick={() => {
|
||||||
|
state.add(state.size + 1);
|
||||||
|
}}>add</button>
|
||||||
|
|
||||||
|
<button onclick={() => {
|
||||||
|
state.delete(state.size);
|
||||||
|
}}>delete</button>
|
||||||
|
|
||||||
|
<button onclick={() => {
|
||||||
|
state.clear();
|
||||||
|
}}>clear</button>
|
||||||
|
|
||||||
|
{#each state as item}
|
||||||
|
<div>{item}</div>
|
||||||
|
{/each}
|
Loading…
Reference in new issue