Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tiny-poems-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

feat: allow inspect reactivity map, set, date
1 change: 1 addition & 0 deletions packages/svelte/src/internal/client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export const IS_ELSEIF = 1 << 13;
export const EFFECT_RAN = 1 << 14;

export const STATE_SYMBOL = Symbol('$state');
export const INSPECT_SYMBOL = Symbol('$inspect');
23 changes: 21 additions & 2 deletions packages/svelte/src/internal/client/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,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';
Expand Down Expand Up @@ -1096,6 +1097,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).
Expand Down Expand Up @@ -1226,7 +1245,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();
Expand Down
9 changes: 9 additions & 0 deletions packages/svelte/src/reactivity/date.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -88,6 +90,13 @@ export class ReactiveDate extends Date {
return v;
};
}

if (DEV) {
// @ts-ignore
proto[INSPECT_SYMBOL] = function () {
get(this.#raw_time);
};
}
}
}

Expand Down
19 changes: 19 additions & 0 deletions packages/svelte/src/reactivity/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand Down
8 changes: 8 additions & 0 deletions packages/svelte/src/reactivity/set.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -66,6 +67,13 @@ export class ReactiveSet extends Set {
return new ReactiveSet(set);
};
}

if (DEV) {
// @ts-ignore
proto[INSPECT_SYMBOL] = function () {
get(this.#version);
};
}
}

#increment_version() {
Expand Down
23 changes: 23 additions & 0 deletions packages/svelte/src/reactivity/url.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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() {
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }
]);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** @type {any[]} */
export const log = [];
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2013,6 +2013,7 @@ declare module 'svelte/reactivity' {
#private;
}
class ReactiveURLSearchParams extends URLSearchParams {
constructor();

[REPLACE](params: URLSearchParams): void;
#private;
Expand Down
Loading