svelte
Version:
Cybernetically enhanced web apps
214 lines (180 loc) • 5 kB
JavaScript
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
import { source, set, state, increment } from '../internal/client/reactivity/sources.js';
import { label, tag } from '../internal/client/dev/tracing.js';
import { get, update_version } from '../internal/client/runtime.js';
var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
var inited = false;
/**
* A reactive version of the built-in [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) object.
* Reading contents of the set (by iterating, or by reading `set.size` or calling `set.has(...)` as in the [example](https://svelte.dev/playground/53438b51194b4882bcc18cddf9f96f15) below) in an [effect](https://svelte.dev/docs/svelte/$effect) or [derived](https://svelte.dev/docs/svelte/$derived)
* will cause it to be re-evaluated as necessary when the set is updated.
*
* Note that values in a reactive set are _not_ made [deeply reactive](https://svelte.dev/docs/svelte/$state#Deep-state).
*
* ```svelte
* <script>
* import { SvelteSet } from 'svelte/reactivity';
* let monkeys = new SvelteSet();
*
* function toggle(monkey) {
* if (monkeys.has(monkey)) {
* monkeys.delete(monkey);
* } else {
* monkeys.add(monkey);
* }
* }
* </script>
*
* {#each ['🙈', '🙉', '🙊'] as monkey}
* <button onclick={() => toggle(monkey)}>{monkey}</button>
* {/each}
*
* <button onclick={() => monkeys.clear()}>clear</button>
*
* {#if monkeys.has('🙈')}<p>see no evil</p>{/if}
* {#if monkeys.has('🙉')}<p>hear no evil</p>{/if}
* {#if monkeys.has('🙊')}<p>speak no evil</p>{/if}
* ```
*
* @template T
* @extends {Set<T>}
*/
export class SvelteSet extends Set {
/** @type {Map<T, Source<boolean>>} */
#sources = new Map();
#version = state(0);
#size = state(0);
#update_version = update_version || -1;
/**
* @param {Iterable<T> | null | undefined} [value]
*/
constructor(value) {
super();
if (DEV) {
// If the value is invalid then the native exception will fire here
value = new Set(value);
tag(this.#version, 'SvelteSet version');
tag(this.#size, 'SvelteSet.size');
}
if (value) {
for (var element of value) {
super.add(element);
}
this.#size.v = super.size;
}
if (!inited) this.#init();
}
/**
* If the source is being created inside the same reaction as the SvelteSet instance,
* we use `state` so that it will not be a dependency of the reaction. Otherwise we
* use `source` so it will be.
*
* @template T
* @param {T} value
* @returns {Source<T>}
*/
#source(value) {
return update_version === this.#update_version ? state(value) : source(value);
}
// We init as part of the first instance so that we can treeshake this class
#init() {
inited = true;
var proto = SvelteSet.prototype;
var set_proto = Set.prototype;
for (const method of read_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
// @ts-ignore
return set_proto[method].apply(this, v);
};
}
for (const method of set_like_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
// @ts-ignore
var set = /** @type {Set<T>} */ (set_proto[method].apply(this, v));
return new SvelteSet(set);
};
}
}
/** @param {T} value */
has(value) {
var has = super.has(value);
var sources = this.#sources;
var s = sources.get(value);
if (s === undefined) {
if (!has) {
// If the value doesn't exist, track the version in case it's added later
// but don't create sources willy-nilly to track all possible values
get(this.#version);
return false;
}
s = this.#source(true);
if (DEV) {
tag(s, `SvelteSet has(${label(value)})`);
}
sources.set(value, s);
}
get(s);
return has;
}
/** @param {T} value */
add(value) {
if (!super.has(value)) {
super.add(value);
set(this.#size, super.size);
increment(this.#version);
}
return this;
}
/** @param {T} value */
delete(value) {
var deleted = super.delete(value);
var sources = this.#sources;
var s = sources.get(value);
if (s !== undefined) {
sources.delete(value);
set(s, false);
}
if (deleted) {
set(this.#size, super.size);
increment(this.#version);
}
return deleted;
}
clear() {
if (super.size === 0) {
return;
}
// Clear first, so we get nice console.log outputs with $inspect
super.clear();
var sources = this.#sources;
for (var s of sources.values()) {
set(s, false);
}
sources.clear();
set(this.#size, 0);
increment(this.#version);
}
keys() {
return this.values();
}
values() {
get(this.#version);
return super.values();
}
entries() {
get(this.#version);
return super.entries();
}
[Symbol.iterator]() {
return this.keys();
}
get size() {
return get(this.#size);
}
}