Skip to content
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

feat: better $state.snapshot #11233

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/svelte/src/internal/client/constants.js
Expand Up @@ -16,3 +16,4 @@ export const EFFECT_RAN = 1 << 13;
export const EFFECT_TRANSPARENT = 1 << 14;

export const STATE_SYMBOL = Symbol('$state');
export const STATE_SNAPSHOT_SYMBOL = Symbol('$state.snapshot');
48 changes: 5 additions & 43 deletions packages/svelte/src/internal/client/dev/inspect.js
@@ -1,7 +1,6 @@
import { snapshot } from '../proxy.js';
import { snapshot } from '../reactivity/snapshot.js';
import { render_effect } from '../reactivity/effects.js';
import { current_effect, deep_read } from '../runtime.js';
import { array_prototype, get_prototype_of, object_prototype } from '../utils.js';

/** @type {Function | null} */
export let inspect_fn = null;
Expand Down Expand Up @@ -32,12 +31,15 @@ export function inspect(get_value, inspector = console.log) {
// calling `inspector` directly inside the effect, so that
// we get useful stack traces
var fn = () => {
const value = deep_snapshot(get_value());
const value = snapshot(get_value(), true);
inspector(initial ? 'init' : 'update', ...value);
};

render_effect(() => {
inspect_fn = fn;
// TODO ideally we'd use `snapshot` here instead of `deep_read`, and pass
// the result to `fn` on the initial run, but it doesn't work because
// of some weird (and possibly buggy?) behaviour around `batch_inspect`
deep_read(get_value());
inspect_fn = null;

Expand All @@ -56,43 +58,3 @@ export function inspect(get_value, inspector = console.log) {
};
});
}

/**
* Like `snapshot`, but recursively traverses into normal arrays/objects to find potential states in them.
* @param {any} value
* @param {Map<any, any>} visited
* @returns {any}
*/
function deep_snapshot(value, visited = new Map()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
const unstated = snapshot(value);

if (unstated !== value) {
visited.set(value, unstated);
return unstated;
}

const prototype = get_prototype_of(value);

// Only deeply snapshot plain objects and arrays
if (prototype === object_prototype || prototype === array_prototype) {
let contains_unstated = false;
/** @type {any} */
const nested_unstated = Array.isArray(value) ? [] : {};

for (let key in value) {
const result = deep_snapshot(value[key], visited);
nested_unstated[key] = result;
if (result !== value[key]) {
contains_unstated = true;
}
}

visited.set(value, contains_unstated ? nested_unstated : value);
} else {
visited.set(value, value);
}
}

return visited.get(value) ?? value;
}
3 changes: 2 additions & 1 deletion packages/svelte/src/internal/client/index.js
Expand Up @@ -88,6 +88,7 @@ export {
update_pre_prop,
update_prop
} from './reactivity/props.js';
export { snapshot } from './reactivity/snapshot.js';
export {
invalidate_store,
mutate_store,
Expand Down Expand Up @@ -128,7 +129,7 @@ export {
validate_store
} from './validate.js';
export { raf } from './timing.js';
export { proxy, snapshot } from './proxy.js';
export { proxy } from './proxy.js';
export { create_custom_element } from './dom/elements/custom-element.js';
export {
child,
Expand Down
58 changes: 0 additions & 58 deletions packages/svelte/src/internal/client/proxy.js
Expand Up @@ -10,7 +10,6 @@ import {
array_prototype,
define_property,
get_descriptor,
get_descriptors,
get_prototype_of,
is_array,
is_frozen,
Expand Down Expand Up @@ -87,63 +86,6 @@ export function proxy(value, immutable = true, parent = null) {
return value;
}

/**
* @template {import('#client').ProxyStateObject} T
* @param {T} value
* @param {Map<T, Record<string | symbol, any>>} already_unwrapped
* @returns {Record<string | symbol, any>}
*/
function unwrap(value, already_unwrapped) {
if (typeof value === 'object' && value != null && STATE_SYMBOL in value) {
const unwrapped = already_unwrapped.get(value);
if (unwrapped !== undefined) {
return unwrapped;
}

if (is_array(value)) {
/** @type {Record<string | symbol, any>} */
const array = [];
already_unwrapped.set(value, array);
for (const element of value) {
array.push(unwrap(element, already_unwrapped));
}
return array;
} else {
/** @type {Record<string | symbol, any>} */
const obj = {};
const keys = Reflect.ownKeys(value);
const descriptors = get_descriptors(value);
already_unwrapped.set(value, obj);

for (const key of keys) {
if (key === STATE_SYMBOL) continue;
if (descriptors[key].get) {
define_property(obj, key, descriptors[key]);
} else {
/** @type {T} */
const property = value[key];
obj[key] = unwrap(property, already_unwrapped);
}
}

return obj;
}
}

return value;
}

/**
* @template T
* @param {T} value
* @returns {T}
*/
export function snapshot(value) {
return /** @type {T} */ (
unwrap(/** @type {import('#client').ProxyStateObject} */ (value), new Map())
);
}

/**
* @param {import('#client').Source<number>} signal
* @param {1 | -1} [d]
Expand Down
57 changes: 57 additions & 0 deletions packages/svelte/src/internal/client/reactivity/snapshot.js
@@ -0,0 +1,57 @@
import { STATE_SNAPSHOT_SYMBOL, STATE_SYMBOL } from '../constants.js';
import { array_prototype, get_prototype_of, is_array, object_prototype } from '../utils.js';

/**
* @template {any} T
* @param {T} value
* @param {boolean} deep
* @param {Map<any, any>} values
* @returns {T}
*/
export function snapshot(value, deep = false, values = new Map()) {
if (typeof value !== 'object' || value === null) {
return value;
}

var unwrapped = /** @type {T} */ (values.get(value));
if (unwrapped !== undefined) {
return unwrapped;
}

if (STATE_SNAPSHOT_SYMBOL in value) {
// @ts-expect-error
return value[STATE_SNAPSHOT_SYMBOL](deep);
}

var proto = get_prototype_of(value);

if (
(proto === object_prototype || proto === array_prototype) &&
(deep || STATE_SYMBOL in value)
) {
if (is_array(value)) {
var length = value.length;
var array = Array(length);

values.set(value, array);

for (var i = 0; i < length; i += 1) {
array[i] = snapshot(value[i], deep, values);
}

return /** @type {T} */ (array);
}

/** @type {Record<string | symbol, any>} */
var obj = {};
values.set(value, obj);

for (var [k, v] of Object.entries(value)) {
obj[k] = snapshot(v, deep, values);
}

return /** @type {T} */ (obj);
}

return value;
}
17 changes: 12 additions & 5 deletions packages/svelte/src/internal/client/runtime.js
@@ -1,6 +1,6 @@
import { DEV } from 'esm-env';
import { get_descriptors, get_prototype_of, is_frozen, object_freeze } from './utils.js';
import { snapshot } from './proxy.js';
import { snapshot } from './reactivity/snapshot.js';
import { destroy_effect, effect, execute_effect_teardown } from './reactivity/effects.js';
import {
EFFECT,
Expand All @@ -15,7 +15,8 @@ import {
BRANCH_EFFECT,
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT
ROOT_EFFECT,
STATE_SNAPSHOT_SYMBOL
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js';
Expand Down Expand Up @@ -125,9 +126,8 @@ export function is_runes() {
*/
export function batch_inspect(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
/**
* @this {any}
*/

/** @this {any} */
return function () {
const previously_batching_effect = is_batching_effect;
is_batching_effect = true;
Expand Down Expand Up @@ -1138,13 +1138,20 @@ export function deep_read(value, visited = new Set()) {
!visited.has(value)
) {
visited.add(value);

if (STATE_SNAPSHOT_SYMBOL in value) {
value[STATE_SNAPSHOT_SYMBOL](true);
return;
}

for (let key in value) {
try {
deep_read(value[key], visited);
} catch (e) {
// continue
}
}

const proto = get_prototype_of(value);
if (
proto !== Object.prototype &&
Expand Down
5 changes: 5 additions & 0 deletions packages/svelte/src/reactivity/date.js
@@ -1,3 +1,4 @@
import { STATE_SNAPSHOT_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 @@ -99,4 +100,8 @@ export class ReactiveDate extends Date {
super(...values);
this.#init();
}

[STATE_SNAPSHOT_SYMBOL]() {
return new Date(get(this.#raw_time));
}
}
13 changes: 13 additions & 0 deletions packages/svelte/src/reactivity/map.js
Expand Up @@ -3,6 +3,8 @@ 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 { STATE_SNAPSHOT_SYMBOL } from '../internal/client/constants.js';
import { snapshot } from '../internal/client/reactivity/snapshot.js';

/**
* @template K
Expand Down Expand Up @@ -155,4 +157,15 @@ export class ReactiveMap extends Map {
get size() {
return get(this.#size);
}

/** @param {boolean} deep */
[STATE_SNAPSHOT_SYMBOL](deep) {
return new Map(
map(
this.#sources.entries(),
([key, source]) => /** @type {[K, V]} */ ([key, snapshot(get(source), deep)]),
'Map Iterator'
)
);
}
}
7 changes: 7 additions & 0 deletions packages/svelte/src/reactivity/set.js
Expand Up @@ -2,6 +2,8 @@ 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 { STATE_SNAPSHOT_SYMBOL } from '../internal/client/constants.js';
import { snapshot } from '../internal/client/reactivity/snapshot.js';

var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
Expand Down Expand Up @@ -149,4 +151,9 @@ export class ReactiveSet extends Set {
get size() {
return get(this.#size);
}

/** @param {boolean} deep */
[STATE_SNAPSHOT_SYMBOL](deep) {
return new Set(map(this.keys(), (key) => snapshot(key, deep), 'Set Iterator'));
}
}
5 changes: 5 additions & 0 deletions packages/svelte/src/reactivity/url.js
@@ -1,3 +1,4 @@
import { STATE_SNAPSHOT_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 @@ -150,6 +151,10 @@ export class ReactiveURL extends URL {
toJSON() {
return this.href;
}

[STATE_SNAPSHOT_SYMBOL]() {
return new URL(this.href);
}
}

export class ReactiveURLSearchParams extends URLSearchParams {
Expand Down
Expand Up @@ -25,13 +25,6 @@ export default test({
console.log = original_log;
},
async test({ assert, target }) {
const button = target.querySelector('button');

flushSync(() => {
button?.click();
});

assert.htmlEqual(target.innerHTML, `<button>update</button>\n1`);
assert.deepEqual(log, [
'init',
{
Expand All @@ -40,7 +33,19 @@ export default test({
list: []
},
derived: []
},
}
]);

log.length = 0;

const button = target.querySelector('button');

flushSync(() => {
button?.click();
});

assert.htmlEqual(target.innerHTML, `<button>update</button>\n1`);
assert.deepEqual(log, [
'update',
{
data: {
Expand Down