DRY out snapshot code

better-snapshot
Rich Harris 9 months ago
parent 307f15d5f7
commit 56ecdbaeac

@ -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;
@ -32,7 +31,7 @@ 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());
inspector(initial ? 'init' : 'update', ...value);
};
@ -56,43 +55,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;
}

@ -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,
@ -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,

@ -10,7 +10,6 @@ import {
array_prototype,
define_property,
get_descriptor,
get_descriptors,
get_prototype_of,
is_array,
is_frozen,
@ -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]

@ -0,0 +1,46 @@
import { STATE_SYMBOL } from '../constants.js';
import { is_array } from '../utils.js';
/**
* @template {any} T
* @param {T} value
* @param {Map<any, any>} values
* @returns {T}
*/
export function snapshot(value, values = new Map()) {
if (typeof value !== 'object' || value === null) {
return value;
}
if (STATE_SYMBOL in value) {
var unwrapped = /** @type {T} */ (values.get(value));
if (unwrapped !== undefined) {
return unwrapped;
}
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], 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, values);
}
return /** @type {T} */ (obj);
}
return value;
}

@ -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,

@ -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',
{
@ -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: {

@ -1,40 +1,34 @@
import { test } from '../../test';
/**
* @type {any[]}
*/
/** @type {any[]} */
let log;
/**
* @type {typeof console.log}}
*/
let original_log;
let original_log = console.log;
export default test({
compileOptions: {
dev: true
},
before_test() {
log = [];
original_log = console.log;
console.log = (...v) => {
log.push(...v);
};
},
after_test() {
console.log = original_log;
},
async test({ assert, target }) {
assert.deepEqual(log, ['init', { x: { count: 0 } }, [{ count: 0 }]]);
log.length = 0;
const [b1] = target.querySelectorAll('button');
b1.click();
await Promise.resolve();
assert.deepEqual(log, [
'init',
{ x: { count: 0 } },
[{ count: 0 }],
'update',
{ x: { count: 1 } },
[{ count: 1 }]
]);
assert.deepEqual(log, ['update', { x: { count: 1 } }, [{ count: 1 }]]);
}
});

@ -1,5 +1,5 @@
<script>
let x = $state({count: 0});
let x = $state({ count: 0 });
$inspect({x}, [x]);
</script>

@ -1,33 +1,34 @@
import { test } from '../../test';
/**
* @type {any[]}
*/
/** @type {any[]} */
let log;
/**
* @type {typeof console.log}}
*/
let original_log;
let original_log = console.log;
export default test({
compileOptions: {
dev: true
},
before_test() {
log = [];
original_log = console.log;
console.log = (...v) => {
log.push(...v);
};
},
after_test() {
console.log = original_log;
},
async test({ assert, target }) {
assert.deepEqual(log, ['init', {}, 'init', []]);
log.length = 0;
const [btn] = target.querySelectorAll('button');
btn.click();
await Promise.resolve();
assert.deepEqual(log, ['init', {}, 'init', [], 'update', { x: 'hello' }, 'update', ['hello']]);
assert.deepEqual(log, ['update', { x: 'hello' }, 'update', ['hello']]);
}
});

Loading…
Cancel
Save