feat: allow $inspect reactivity map, set, date (#11164)

* feat: allow $inspect reactivity map, set, date

* feat: inspect map without adding new data source

* feat: add inspect

* feat: define inspect on dev mode only

* fix: lint error
pull/11223/head
Tan Li Hau 3 months ago committed by GitHub
parent 70b47de124
commit 43d13e92a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: allow inspect reactivity map, set, date

@ -16,3 +16,4 @@ export const EFFECT_RAN = 1 << 13;
export const EFFECT_TRANSPARENT = 1 << 14;
export const STATE_SYMBOL = Symbol('$state');
export const INSPECT_SYMBOL = Symbol('$inspect');

@ -27,7 +27,8 @@ import {
BRANCH_EFFECT,
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT
ROOT_EFFECT,
INSPECT_SYMBOL
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js';
@ -1117,6 +1118,24 @@ export function pop(component) {
return component || /** @type {T} */ ({});
}
/**
*
* This is called from the inspect.
* Deeply traverse every item in the array with `deep_read` to register for inspect callback
* If the item implements INSPECT_SYMBOL, will use that instead
* @param {Array<any>} value
* @returns {void}
*/
function deep_read_inpect(value) {
for (const item of value) {
if (item && typeof item[INSPECT_SYMBOL] === 'function') {
item[INSPECT_SYMBOL]();
} else {
deep_read(item);
}
}
}
/**
* Possibly traverse an object and read all its properties so that they're all reactive in case this is `$state`.
* Does only check first level of an object for performance reasons (heuristic should be good for 99% of all cases).
@ -1247,7 +1266,7 @@ export function inspect(get_value, inspect = console.log) {
inspect_fn = fn;
const value = get_value();
deep_read(value);
deep_read_inpect(value);
inspect_fn = null;
const signals = inspect_captured_signals.slice();

@ -1,3 +1,5 @@
import { DEV } from 'esm-env';
import { INSPECT_SYMBOL } from '../internal/client/constants.js';
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
@ -88,6 +90,13 @@ export class ReactiveDate extends Date {
return v;
};
}
if (DEV) {
// @ts-ignore
proto[INSPECT_SYMBOL] = function () {
get(this.#raw_time);
};
}
}
}

@ -3,6 +3,9 @@ import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
import { UNINITIALIZED } from '../constants.js';
import { map } from './utils.js';
import { INSPECT_SYMBOL } from '../internal/client/constants.js';
var inited = false;
/**
* @template K
@ -20,6 +23,22 @@ export class ReactiveMap extends Map {
constructor(value) {
super();
if (DEV) {
if (!inited) {
inited = true;
// @ts-ignore
ReactiveMap.prototype[INSPECT_SYMBOL] = function () {
// changes could either introduced by
// - modifying the value, or
// - add / remove entries to the map
for (const [, source] of this.#sources) {
get(source);
}
get(this.#size);
};
}
}
// If the value is invalid then the native exception will fire here
if (DEV) new Map(value);

@ -2,6 +2,7 @@ import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
import { map } from './utils.js';
import { INSPECT_SYMBOL } from '../internal/client/constants.js';
var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
@ -65,6 +66,13 @@ export class ReactiveSet extends Set {
return new ReactiveSet(set);
};
}
if (DEV) {
// @ts-ignore
proto[INSPECT_SYMBOL] = function () {
get(this.#version);
};
}
}
#increment_version() {

@ -1,7 +1,11 @@
import { DEV } from 'esm-env';
import { INSPECT_SYMBOL } from '../internal/client/constants.js';
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
const REPLACE = Symbol();
var inited_url = false;
var inited_search_params = false;
export class ReactiveURL extends URL {
#protocol = source(super.protocol);
@ -21,6 +25,14 @@ export class ReactiveURL extends URL {
url = new URL(url, base);
super(url);
this.#searchParams[REPLACE](url.searchParams);
if (DEV && !inited_url) {
inited_url = true;
// @ts-ignore
ReactiveURL.prototype[INSPECT_SYMBOL] = function () {
this.href;
};
}
}
get hash() {
@ -159,6 +171,17 @@ export class ReactiveURLSearchParams extends URLSearchParams {
set(this.#version, this.#version.v + 1);
}
constructor() {
super();
if (DEV && !inited_search_params) {
inited_search_params = true;
// @ts-ignore
ReactiveURLSearchParams.prototype[INSPECT_SYMBOL] = function () {
get(this.#version);
};
}
}
/**
* @param {URLSearchParams} params
*/

@ -0,0 +1,81 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
import { log } from './log';
export default test({
compileOptions: {
dev: true
},
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const [in1, in2] = target.querySelectorAll('input');
const [b1, b2, b3] = target.querySelectorAll('button');
assert.deepEqual(log, [
{ label: 'map', type: 'init', values: [] },
{ label: 'set', type: 'init', values: [] },
{ label: 'date', type: 'init', values: 1712966400000 }
]);
log.length = 0;
flushSync(() => b1.click()); // map.set('key', 'value')
in1.value = 'name';
in2.value = 'Svelte';
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
flushSync(() => b1.click()); // map.set('name', 'Svelte')
in2.value = 'World';
in2.dispatchEvent(new window.Event('input', { bubbles: true }));
flushSync(() => b1.click()); // map.set('name', 'World')
flushSync(() => b1.click()); // map.set('name', 'World')
assert.deepEqual(log, [
{ label: 'map', type: 'update', values: [['key', 'value']] },
{
label: 'map',
type: 'update',
values: [
['key', 'value'],
['name', 'Svelte']
]
},
{
label: 'map',
type: 'update',
values: [
['key', 'value'],
['name', 'World']
]
}
]);
log.length = 0;
flushSync(() => b2.click()); // set.add('name');
in1.value = 'Svelte';
in1.dispatchEvent(new window.Event('input', { bubbles: true }));
flushSync(() => b2.click()); // set.add('Svelte');
flushSync(() => b2.click()); // set.add('Svelte');
assert.deepEqual(log, [
{ label: 'set', type: 'update', values: ['name'] },
{ label: 'set', type: 'update', values: ['name', 'Svelte'] }
]);
log.length = 0;
flushSync(() => b3.click()); // date.minutes++
flushSync(() => b3.click()); // date.minutes++
flushSync(() => b3.click()); // date.minutes++
assert.deepEqual(log, [
{ label: 'date', type: 'update', values: 1712966460000 },
{ label: 'date', type: 'update', values: 1712966520000 },
{ label: 'date', type: 'update', values: 1712966580000 }
]);
}
});

@ -0,0 +1,2 @@
/** @type {any[]} */
export const log = [];

@ -0,0 +1,27 @@
<script>
import { Map, Set, Date } from 'svelte/reactivity';
import { log } from './log';
const map = new Map();
const set = new Set();
const date = new Date('2024-04-13 00:00:00+0000');
let key = $state('key');
let value = $state('value');
$inspect(map).with((type, map) => {
log.push({ label: 'map', type, values: [...map] });
});
$inspect(set).with((type, set) => {
log.push({ label: 'set', type, values: [...set] });
});
$inspect(date).with((type, date) => {
log.push({ label: 'date', type, values: date.getTime() });
});
</script>
<input bind:value={key} />
<input bind:value={value} />
<button on:click={() => map.set(key, value)}>map</button>
<button on:click={() => set.add(key)}>set</button>
<button on:click={() => date.setMinutes(date.getMinutes() + 1)}>date</button>

@ -2021,6 +2021,7 @@ declare module 'svelte/reactivity' {
#private;
}
class ReactiveURLSearchParams extends URLSearchParams {
constructor();
[REPLACE](params: URLSearchParams): void;
#private;

Loading…
Cancel
Save