chore: cleanup proxy files (#10268)

- merge `readonly.js` into `proxy.js` and get rid of sub folder
- extract types into `d.ts` file and properly document the properties
- type tweaks
pull/10274/head
Simon H 10 months ago committed by GitHub
parent 14bf4b4b2c
commit 40c2956381
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -14,7 +14,7 @@ import {
set_current_hydration_fragment set_current_hydration_fragment
} from './hydration.js'; } from './hydration.js';
import { clear_text_content, map_get, map_set } from './operations.js'; import { clear_text_content, map_get, map_set } from './operations.js';
import { STATE_SYMBOL } from './proxy/proxy.js'; import { STATE_SYMBOL } from './proxy.js';
import { insert, remove } from './reconciler.js'; import { insert, remove } from './reconciler.js';
import { empty } from './render.js'; import { empty } from './render.js';
import { import {

@ -27,11 +27,11 @@ export function clear_loops() {
/** /**
* Creates a new task that runs on each raf frame * Creates a new task that runs on each raf frame
* until it returns a falsy value or is aborted * until it returns a falsy value or is aborted
* @param {import('./private.js').TaskCallback} callback * @param {import('./types.js').TaskCallback} callback
* @returns {import('./private.js').Task} * @returns {import('./types.js').Task}
*/ */
export function loop(callback) { export function loop(callback) {
/** @type {import('./private.js').TaskEntry} */ /** @type {import('./types.js').TaskEntry} */
let task; let task;
if (tasks.size === 0) raf.tick(run_tasks); if (tasks.size === 0) raf.tick(run_tasks);
return { return {

@ -1,8 +0,0 @@
export type TaskCallback = (now: number) => boolean | void;
export type TaskEntry = { c: TaskCallback; f: () => void };
export interface Task {
abort(): void;
promise: Promise<void>;
}

@ -9,7 +9,7 @@ import {
UNINITIALIZED, UNINITIALIZED,
mutable_source, mutable_source,
batch_inspect batch_inspect
} from '../runtime.js'; } from './runtime.js';
import { import {
array_prototype, array_prototype,
define_property, define_property,
@ -20,36 +20,40 @@ import {
is_frozen, is_frozen,
object_keys, object_keys,
object_prototype object_prototype
} from '../utils.js'; } from './utils.js';
/** @typedef {{ s: Map<string | symbol, import('../types.js').SourceSignal<any>>; v: import('../types.js').SourceSignal<number>; a: boolean, i: boolean, p: StateObject }} Metadata */
/** @typedef {Record<string | symbol, any> & { [STATE_SYMBOL]: Metadata }} StateObject */
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
export const READONLY_SYMBOL = Symbol('readonly'); export const READONLY_SYMBOL = Symbol('readonly');
/** /**
* @template {StateObject} T * @template T
* @param {T} value * @param {T} value
* @param {boolean} [immutable] * @param {boolean} [immutable]
* @returns {T} * @returns {import('./types.js').ProxyStateObject<T> | T}
*/ */
export function proxy(value, immutable = true) { export function proxy(value, immutable = true) {
if (typeof value === 'object' && value != null && !is_frozen(value)) { if (typeof value === 'object' && value != null && !is_frozen(value)) {
if (STATE_SYMBOL in value) { if (STATE_SYMBOL in value) {
return /** @type {T} */ (value[STATE_SYMBOL].p); return /** @type {import('./types.js').ProxyMetadata<T>} */ (value[STATE_SYMBOL]).p;
} }
const prototype = get_prototype_of(value); const prototype = get_prototype_of(value);
// TODO handle Map and Set as well // TODO handle Map and Set as well
if (prototype === object_prototype || prototype === array_prototype) { if (prototype === object_prototype || prototype === array_prototype) {
const proxy = new Proxy(value, handler); const proxy = new Proxy(
value,
/** @type {ProxyHandler<import('./types.js').ProxyStateObject<T>>} */ (state_proxy_handler)
);
define_property(value, STATE_SYMBOL, { define_property(value, STATE_SYMBOL, {
value: init(value, proxy, immutable), value: init(
/** @type {import('./types.js').ProxyStateObject<T>} */ (value),
/** @type {import('./types.js').ProxyStateObject<T>} */ (proxy),
immutable
),
writable: false writable: false
}); });
// @ts-expect-error not sure how to fix this
return proxy; return proxy;
} }
} }
@ -58,7 +62,7 @@ export function proxy(value, immutable = true) {
} }
/** /**
* @template {StateObject} T * @template {import('./types.js').ProxyStateObject} T
* @param {T} value * @param {T} value
* @param {Map<T, Record<string | symbol, any>>} already_unwrapped * @param {Map<T, Record<string | symbol, any>>} already_unwrapped
* @returns {Record<string | symbol, any>} * @returns {Record<string | symbol, any>}
@ -104,14 +108,14 @@ function unwrap(value, already_unwrapped = new Map()) {
* @returns {T} * @returns {T}
*/ */
export function unstate(value) { export function unstate(value) {
return /** @type {T} */ (unwrap(/** @type {StateObject} */ (value))); return /** @type {T} */ (unwrap(/** @type {import('./types.js').ProxyStateObject} */ (value)));
} }
/** /**
* @param {StateObject} value * @param {import('./types.js').ProxyStateObject} value
* @param {StateObject} proxy * @param {import('./types.js').ProxyStateObject} proxy
* @param {boolean} immutable * @param {boolean} immutable
* @returns {Metadata} * @returns {import('./types.js').ProxyMetadata}
*/ */
function init(value, proxy, immutable) { function init(value, proxy, immutable) {
return { return {
@ -123,8 +127,8 @@ function init(value, proxy, immutable) {
}; };
} }
/** @type {ProxyHandler<StateObject>} */ /** @type {ProxyHandler<import('./types.js').ProxyStateObject>} */
const handler = { const state_proxy_handler = {
defineProperty(target, prop, descriptor) { defineProperty(target, prop, descriptor) {
if (descriptor.value) { if (descriptor.value) {
const metadata = target[STATE_SYMBOL]; const metadata = target[STATE_SYMBOL];
@ -290,9 +294,67 @@ export function observe(object) {
} }
if (DEV) { if (DEV) {
handler.setPrototypeOf = () => { state_proxy_handler.setPrototypeOf = () => {
throw new Error('Cannot set prototype of $state object'); throw new Error('Cannot set prototype of $state object');
}; };
} }
export { readonly } from './readonly.js'; /**
* Expects a value that was wrapped with `proxy` and makes it readonly.
*
* @template {Record<string | symbol, any>} T
* @template {import('./types.js').ProxyReadonlyObject<T> | T} U
* @param {U} value
* @returns {Proxy<U> | U}
*/
export function readonly(value) {
const proxy = value && value[READONLY_SYMBOL];
if (proxy) return proxy;
if (
typeof value === 'object' &&
value != null &&
!is_frozen(value) &&
STATE_SYMBOL in value && // TODO handle Map and Set as well
!(READONLY_SYMBOL in value)
) {
const proxy = new Proxy(
value,
/** @type {ProxyHandler<import('./types.js').ProxyReadonlyObject<U>>} */ (
readonly_proxy_handler
)
);
define_property(value, READONLY_SYMBOL, { value: proxy, writable: false });
return proxy;
}
return value;
}
/**
* @param {any} _
* @param {string} prop
* @returns {never}
*/
const readonly_error = (_, prop) => {
throw new Error(
`Non-bound props cannot be mutated — to make the \`${prop}\` settable, ensure the object it is used within is bound as a prop \`bind:<prop>={...}\`. Fallback values can never be mutated.`
);
};
/** @type {ProxyHandler<import('./types.js').ProxyReadonlyObject>} */
const readonly_proxy_handler = {
defineProperty: readonly_error,
deleteProperty: readonly_error,
set: readonly_error,
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (!(prop in target)) {
return readonly(value);
}
return value;
}
};

@ -1,62 +0,0 @@
import { define_property, is_frozen } from '../utils.js';
import { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js';
/**
* @template {Record<string | symbol, any>} T
* @typedef {T & { [READONLY_SYMBOL]: Proxy<T> }} StateObject
*/
/**
* Expects a value that was wrapped with `proxy` and makes it readonly.
*
* @template {Record<string | symbol, any>} T
* @template {StateObject<T>} U
* @param {U} value
* @returns {Proxy<U> | U}
*/
export function readonly(value) {
const proxy = value && value[READONLY_SYMBOL];
if (proxy) return proxy;
if (
typeof value === 'object' &&
value != null &&
!is_frozen(value) &&
STATE_SYMBOL in value && // TODO handle Map and Set as well
!(READONLY_SYMBOL in value)
) {
const proxy = new Proxy(value, handler);
define_property(value, READONLY_SYMBOL, { value: proxy, writable: false });
return proxy;
}
return value;
}
/**
* @param {any} _
* @param {string} prop
* @returns {never}
*/
const readonly_error = (_, prop) => {
throw new Error(
`Non-bound props cannot be mutated — to make the \`${prop}\` settable, ensure the object it is used within is bound as a prop \`bind:<prop>={...}\`. Fallback values can never be mutated.`
);
};
/** @type {ProxyHandler<StateObject<any>>} */
const handler = {
defineProperty: readonly_error,
deleteProperty: readonly_error,
set: readonly_error,
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (!(prop in target)) {
return readonly(value);
}
return value;
}
};

@ -57,7 +57,7 @@ import {
} from './utils.js'; } from './utils.js';
import { is_promise } from '../common.js'; import { is_promise } from '../common.js';
import { bind_transition, trigger_transitions } from './transitions.js'; import { bind_transition, trigger_transitions } from './transitions.js';
import { proxy } from './proxy/proxy.js'; import { proxy } from './proxy.js';
/** @type {Set<string>} */ /** @type {Set<string>} */
const all_registerd_events = new Set(); const all_registerd_events = new Set();

@ -17,8 +17,7 @@ import {
PROPS_IS_RUNES, PROPS_IS_RUNES,
PROPS_IS_UPDATED PROPS_IS_UPDATED
} from '../../constants.js'; } from '../../constants.js';
import { readonly } from './proxy/readonly.js'; import { READONLY_SYMBOL, STATE_SYMBOL, proxy, readonly, unstate } from './proxy.js';
import { READONLY_SYMBOL, STATE_SYMBOL, proxy, unstate } from './proxy/proxy.js';
import { EACH_BLOCK, IF_BLOCK } from './block.js'; import { EACH_BLOCK, IF_BLOCK } from './block.js';
export const SOURCE = 1; export const SOURCE = 1;
@ -127,7 +126,7 @@ function is_runes(context) {
} }
/** /**
* @param {import("./proxy/proxy.js").StateObject} target * @param {import('./types.js').ProxyStateObject} target
* @param {string | symbol} prop * @param {string | symbol} prop
* @param {any} receiver * @param {any} receiver
*/ */
@ -2120,9 +2119,9 @@ if (DEV) {
/** /**
* Expects a value that was wrapped with `freeze` and makes it frozen. * Expects a value that was wrapped with `freeze` and makes it frozen.
* @template {import('./proxy/proxy.js').StateObject} T * @template T
* @param {T} value * @param {T} value
* @returns {Readonly<Record<string | symbol, any>>} * @returns {Readonly<T>}
*/ */
export function freeze(value) { export function freeze(value) {
if (typeof value === 'object' && value != null && !is_frozen(value)) { if (typeof value === 'object' && value != null && !is_frozen(value)) {

@ -10,6 +10,7 @@ import {
DYNAMIC_ELEMENT_BLOCK, DYNAMIC_ELEMENT_BLOCK,
SNIPPET_BLOCK SNIPPET_BLOCK
} from './block.js'; } from './block.js';
import type { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js';
import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT, LAZY_PROPERTY } from './runtime.js'; import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT, LAZY_PROPERTY } from './runtime.js';
// Put all internal types in this file. Once we convert to JSDoc, we can make this a d.ts file // Put all internal types in this file. Once we convert to JSDoc, we can make this a d.ts file
@ -388,3 +389,33 @@ export type Raf = {
tick: (callback: (time: DOMHighResTimeStamp) => void) => any; tick: (callback: (time: DOMHighResTimeStamp) => void) => any;
now: () => number; now: () => number;
}; };
export interface Task {
abort(): void;
promise: Promise<void>;
}
export type TaskCallback = (now: number) => boolean | void;
export type TaskEntry = { c: TaskCallback; f: () => void };
export interface ProxyMetadata<T = Record<string | symbol, any>> {
/** A map of signals associated to the properties that are reactive */
s: Map<string | symbol, SourceSignal<any>>;
/** A version counter, used within the proxy to signal changes in places where there's no other way to signal an update */
v: SourceSignal<number>;
/** `true` if the proxified object is an array */
a: boolean;
/** Immutable: Whether to use a source or mutable source under the hood */
i: boolean;
/** The associated proxy */
p: ProxyStateObject<T> | ProxyReadonlyObject<T>;
}
export type ProxyStateObject<T = Record<string | symbol, any>> = T & {
[STATE_SYMBOL]: ProxyMetadata;
};
export type ProxyReadonlyObject<T = Record<string | symbol, any>> = ProxyStateObject<T> & {
[READONLY_SYMBOL]: ProxyMetadata;
};

@ -45,7 +45,7 @@ export * from './client/each.js';
export * from './client/render.js'; export * from './client/render.js';
export * from './client/validate.js'; export * from './client/validate.js';
export { raf } from './client/timing.js'; export { raf } from './client/timing.js';
export { proxy, readonly, unstate } from './client/proxy/proxy.js'; export { proxy, readonly, unstate } from './client/proxy.js';
export { create_custom_element } from './client/custom-element.js'; export { create_custom_element } from './client/custom-element.js';

@ -61,7 +61,7 @@ export function spring(value, opts = {}) {
const { stiffness = 0.15, damping = 0.8, precision = 0.01 } = opts; const { stiffness = 0.15, damping = 0.8, precision = 0.01 } = opts;
/** @type {number} */ /** @type {number} */
let last_time; let last_time;
/** @type {import('../internal/client/private').Task | null} */ /** @type {import('../internal/client/types').Task | null} */
let task; let task;
/** @type {object} */ /** @type {object} */
let current_token; let current_token;
@ -120,7 +120,7 @@ export function spring(value, opts = {}) {
}); });
} }
return new Promise((fulfil) => { return new Promise((fulfil) => {
/** @type {import('../internal/client/private').Task} */ (task).promise.then(() => { /** @type {import('../internal/client/types').Task} */ (task).promise.then(() => {
if (token === current_token) fulfil(); if (token === current_token) fulfil();
}); });
}); });

@ -81,7 +81,7 @@ function get_interpolator(a, b) {
*/ */
export function tweened(value, defaults = {}) { export function tweened(value, defaults = {}) {
const store = writable(value); const store = writable(value);
/** @type {import('../internal/client/private').Task} */ /** @type {import('../internal/client/types').Task} */
let task; let task;
let target_value = value; let target_value = value;
/** /**
@ -95,7 +95,7 @@ export function tweened(value, defaults = {}) {
} }
target_value = new_value; target_value = new_value;
/** @type {import('../internal/client/private').Task | null} */ /** @type {import('../internal/client/types').Task | null} */
let previous_task = task; let previous_task = task;
let started = false; let started = false;

Loading…
Cancel
Save