From 9f0656f3a02aba5be78ae1040d9ee5837ef648c1 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 9 Apr 2025 05:26:17 +0700 Subject: [PATCH] feat(lib/utils/proxy): add `destructure` helper, fix issues with `unproxify` --- lib/utils/src/proxy.ts | 113 +++++++++++++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 16 deletions(-) diff --git a/lib/utils/src/proxy.ts b/lib/utils/src/proxy.ts index 5d16d8f..b645a7a 100644 --- a/lib/utils/src/proxy.ts +++ b/lib/utils/src/proxy.ts @@ -17,7 +17,7 @@ export function getProxyTarget(obj: object) { return _targets.get(obj) } -// Heavily inspired by Wintry's lazy utils, but more optimized and stripped down. +// Heavily inspired by Wintry's lazy utils, but more optimized and stripped down, with a few fixes. // https://github.com/pylixonly/wintry/blob/main/src/utils/lazy.ts const _proxifyMetadatas = new WeakMap unknown, cacheable: boolean, cache?: unknown]>() @@ -26,25 +26,20 @@ const _proxifyHandler = { ...Object.fromEntries( Object.getOwnPropertyNames(Reflect).map(k => [ k, - (hint: object, ...args: any[]) => { + (hint: object, ...args: any[]) => // @ts-expect-error - const fn = Reflect[k] - const meta = _proxifyMetadatas.get(hint)! - if (meta[1]) return fn(meta[2] ?? (meta[2] = meta[0]()), ...args) - return fn(meta[0](), ...args) - }, + Reflect[k](unproxifyFromHint(hint), ...args), ]), ), // Workaround to fix functions that the correct `this` get: (hint, p, recv) => { - const meta = _proxifyMetadatas.get(hint)! - const metaTarget = meta[1] ? (meta[2] ?? (meta[2] = meta[0]())) : meta[0]() - const val = Reflect.get(metaTarget!, p, recv) + const target = unproxifyFromHint(hint) + const val = Reflect.get(target!, p, recv) if (val && val.__proto__ === Function.prototype) return new Proxy(val, { // If thisArg happens to be a proxified value, we will use the target object instead - apply: (fn, thisArg, args) => Reflect.apply(fn, thisArg === recv ? metaTarget : thisArg, args), + apply: (fn, thisArg, args) => Reflect.apply(fn, thisArg === recv ? target : thisArg, args), }) return val @@ -52,7 +47,7 @@ const _proxifyHandler = { // Workaround to fix: // TypeError: getOwnPropertyDescriptor trap result is not configurable but target property '...' is configurable or non-existent getOwnPropertyDescriptor: (hint, p) => { - const d = Reflect.getOwnPropertyDescriptor(hint, p) + const d = Reflect.getOwnPropertyDescriptor(unproxifyFromHint(hint)!, p) if (d && !Reflect.getOwnPropertyDescriptor(hint, p)) Object.defineProperty(hint, p, d) return d }, @@ -76,8 +71,24 @@ export type ProxifyOptions = { * @param signal The signal to use to get the value. * @param options The options to use for the proxified value. * @returns A proxified value that will be updated when the signal is updated. + * + * @example Without cache + * ```ts + * const proxified = proxify(() => ({ value: Math.random() }), { hint: 'object' }) + * console.log(proxified) // { value: 0.123 } + * console.log(proxified.value) // 0.456 + * console.log(proxified) // { value: 0.789 } + * ``` + * + * @example With cache + * ```ts + * const proxified = proxify(() => ({ value: Math.random() }), { hint: 'object', cache: true }) + * console.log(proxified) // { value: 0.123 } + * console.log(proxified.value) // 0.123 + * console.log(proxified) // { value: 0.123 } + * ``` */ -export function proxify(signal: () => T, options?: ProxifyOptions): T | undefined { +export function proxify(signal: () => T, options?: ProxifyOptions): T { // biome-ignore lint/complexity/useArrowFunction: We need a function with a constructor const hint = (options?.hint === 'object' ? {} : function () {}) as any _proxifyMetadatas.set(hint, [signal, options?.cache ?? false]) @@ -86,12 +97,82 @@ export function proxify(signal: () => T, options?: ProxifyOptions): T | undef /** * Get the value of a proxified value at the current moment. + * + * @see {@link proxify} for more documentation. + * * @param proxified The proxified value. * @returns The unproxified value. + * + * @throws {TypeError} If the value is not a proxified value. + * + * @example Without cache + * ```ts + * const proxified = proxify(() => ({ value: Math.random() }), { hint: 'object' }) + * const x = unproxify(proxified) + * console.log(x) // { value: 0.123 } + * console.log(x.value) // 0.123 + * console.log(proxified) // { value: 0.456 } + * ``` + * + * @example With cache + * ```ts + * const proxified = proxify(() => ({ value: Math.random() }), { hint: 'object', cache: true }) + * const x = unproxify(proxified) + * console.log(x) // { value: 0.123 } + * console.log(x.value) // 0.123 + * console.log(proxified) // { value: 0.123 } + * ``` */ -export function unproxify(proxified: T): T | undefined { - const meta = _proxifyMetadatas.get(getProxyTarget(proxified)!) - if (!meta) throw new TypeError(`${typeof proxified} is not a proxified value`) +export function unproxify(proxified: T): T { + const hint = getProxyTarget(proxified) + if (!hint) throw new TypeError(`${typeof proxified} is not a proxified value`) + return unproxifyFromHint(hint) +} + +function unproxifyFromHint(hint: object) { + const meta = _proxifyMetadatas.get(hint)! if (meta[1]) return meta[2] ?? ((meta[2] = meta[0]()) as any) return meta[0]() as any } + +/** + * Destructure a proxified value. + * @param proxified The proxified value. + * @param options The options to use for the destructured value. + * + * @see {@link proxify} for more documentation. + * + * @throws {TypeError} If the value is not a proxifiable value (primitives). + * + * @example + * ```ts + * // cache is not turned on, so each access will call the signal again + * const { x, y } = destructure( + * proxify(() => ({ x: Math.random(), y: [Math.random()], z: null })), + * { hint: 'object' } + * ) + * + * // Non-nullish primitives can be proxified, but you'll need to access their values with valueOf() + * x.valueOf() === x.valueOf() // false + * + * y // [0.123] + * y // [0.456] + * + * z // TypeError: Cannot destructure and proxify null (reading 'z') + * ``` + */ +export function destructure(proxified: T, options?: ProxifyOptions): T { + return new Proxy({} as T, { + get: (_, p) => + proxify(() => { + // @ts-expect-error + const v = unproxify(proxified)[p] + + if (v == null) throw new TypeError(`Cannot destructure and proxify ${v} (reading '${String(p)}')`) + // Forcefully proxify primitives + if (typeof v !== 'object' && typeof v !== 'function') return Object(v) + + return v + }, options), + }) +}