From 436b4ff3bb2a1d688c949ac8f6055a58124ad1ae Mon Sep 17 00:00:00 2001 From: Daishi Kato Date: Sat, 15 Oct 2022 09:52:05 +0900 Subject: [PATCH] fix(vanilla, react): use experimental_use and some refactors (#545) * feat: use experimental_use * fix and refactor defaultUse * wip: disable experimental_use * Revert "wip: disable experimental_use" This reverts commit 10b8b6b6d3b1eb50a2ea14d1be664b2352b91ffd. * small refactor * simplify types * fallback use outside render * revert to react@latest * Revert "revert to react@latest" This reverts commit 5eaa3099b5f5379b98dfd86d021eaa657622c9c8. * Revert "fallback use outside render" This reverts commit 35ca1ea066f7a654c4ad681431485535fe912a3e. * use use recursively * wip: avoid affectedToPathList * use custom affectedToPathList * revert to react@latest * refactor * further refactor * update types/react and remove ts-ignore * refactor with promise convension and better typing with awaited * refactor a bit --- package.json | 4 +- src/react.ts | 50 ++++++++++++-- src/vanilla.ts | 184 +++++++++++++++++++++++++++++-------------------- 3 files changed, 156 insertions(+), 82 deletions(-) diff --git a/package.json b/package.json index b20bfe23..edbe2f41 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "./index.js", "types": "./index.d.ts", "typesVersions": { - "<4.0": { + "<4.5": { "esm/*": [ "ts3.4/*" ], @@ -73,7 +73,7 @@ "test:coverage:watch": "jest --watch", "copy": "shx cp -r dist/src/* dist/esm && shx mv dist/src/* dist && shx rm -rf dist/{src,tests} && downlevel-dts dist dist/ts3.4 && shx cp package.json readme.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.prettier=undefined; this.jest=undefined;\"", "patch-macro-vite": "shx cp dist/esm/macro/vite.d.ts dist/macro/ && shx mkdir dist/ts3.4/macro && shx cp dist/ts3.4/esm/macro/vite.d.ts dist/ts3.4/macro/", - "patch-ts3.4": "shx sed -i 's/^declare type Snapshot =/declare type Snapshot = T extends AnyFunction ? T : T extends AsRef ? T : T extends Promise ? Snapshot2 : { readonly [K in keyof T]: Snapshot2 }; type Snapshot2 = T extends AnyFunction ? T : T extends AsRef ? T : T extends Promise ? V : { readonly [K in keyof T]: T[K] };declare type _Snapshot =/' 'dist/ts3.4/**/*.d.ts'" + "patch-ts3.4": "node -e \"require('shelljs').find('dist/ts3.4/**/*.d.ts').forEach(f=>require('fs').appendFileSync(f,'declare type Awaited = T extends Promise ? V : T;'))\"; shx sed -i 's/^declare type Snapshot =/declare type Snapshot = T extends AnyFunction ? T : T extends AsRef ? T : T extends Promise ? Awaited : { readonly [K in keyof T]: Snapshot2 }; type Snapshot2 = T extends AnyFunction ? T : T extends AsRef ? T : T extends Promise ? Awaited : { readonly [K in keyof T]: T[K] };declare type _Snapshot =/' 'dist/ts3.4/**/*.d.ts'" }, "engines": { "node": ">=12.7.0" diff --git a/src/react.ts b/src/react.ts index 7cdc48e2..d146120c 100644 --- a/src/react.ts +++ b/src/react.ts @@ -1,7 +1,17 @@ -import { useCallback, useDebugValue, useEffect, useMemo, useRef } from 'react' +/// + +import { + experimental_use as use, + useCallback, + useDebugValue, + useEffect, + useMemo, + useRef, +} from 'react' import { - affectedToPathList, + // affectedToPathList, createProxy as createProxyToCompare, + getUntracked, isChanged, } from 'proxy-compare' // import { useSyncExternalStore } from 'use-sync-external-store/shim' @@ -14,6 +24,38 @@ import type { INTERNAL_Snapshot as Snapshot } from './vanilla' const { useSyncExternalStore } = useSyncExternalStoreExports +// customized version of affectedToPathList +// we need to avoid invoking getters +const affectedToPathList = ( + obj: unknown, + affected: WeakMap +) => { + const list: (string | symbol)[][] = [] + const seen = new WeakSet() + const walk = (x: unknown, path?: (string | symbol)[]) => { + if (seen.has(x as object)) { + // for object with cycles + return + } + let used: Set | undefined + if (typeof x === 'object' && x !== null) { + seen.add(x) + used = affected.get(getUntracked(x) || x) as any + } + if (used) { + used.forEach((key) => { + if ('value' in (Object.getOwnPropertyDescriptor(x, key) || {})) { + walk((x as any)[key], path ? [...path, key] : [key]) + } + }) + } else if (path) { + list.push(path) + } + } + walk(obj) + return list +} + const useAffectedDebugValue = ( state: object, affected: WeakMap @@ -119,7 +161,7 @@ export function useSnapshot( [proxyObject, notifyInSync] ), () => { - const nextSnapshot = snapshot(proxyObject) + const nextSnapshot = snapshot(proxyObject, use) try { if ( !inRender && @@ -140,7 +182,7 @@ export function useSnapshot( } return nextSnapshot }, - () => snapshot(proxyObject) + () => snapshot(proxyObject, use) ) inRender = false const currAffected = new WeakMap() diff --git a/src/vanilla.ts b/src/vanilla.ts index f6898805..a348addb 100644 --- a/src/vanilla.ts +++ b/src/vanilla.ts @@ -21,8 +21,8 @@ type Snapshot = T extends AnyFunction ? T : T extends AsRef ? T - : T extends Promise - ? Snapshot + : T extends Promise + ? Awaited : { readonly [K in keyof T]: Snapshot } @@ -33,11 +33,26 @@ type Snapshot = T extends AnyFunction */ export type INTERNAL_Snapshot = Snapshot +type HandlePromise =

>(promise: P) => Awaited

+ +type CreateSnapshot = ( + target: T, + receiver: object, + version: number, + handlePromise?: HandlePromise +) => T + +type ProxyState = readonly [ + target: object, + receiver: object, + version: number, + createSnapshot: CreateSnapshot, + listeners: Set +] + // shared state +const PROXY_STATE = Symbol() const refSet = new WeakSet() -const VERSION = __DEV__ ? Symbol('VERSION') : Symbol() -const LISTENERS = __DEV__ ? Symbol('LISTENERS') : Symbol() -const SNAPSHOT = __DEV__ ? Symbol('SNAPSHOT') : Symbol() const buildProxyFunction = ( objectIs = Object.is, @@ -58,51 +73,58 @@ const buildProxyFunction = ( !(x instanceof RegExp) && !(x instanceof ArrayBuffer), - PROMISE_RESULT = __DEV__ ? Symbol('PROMISE_RESULT') : Symbol(), - PROMISE_ERROR = __DEV__ ? Symbol('PROMISE_ERROR') : Symbol(), + defaultHandlePromise =

>( + promise: P & { + status?: 'pending' | 'fulfilled' | 'rejected' + value?: Awaited

+ reason?: unknown + } + ) => { + switch (promise.status) { + case 'fulfilled': + return promise.value as Awaited

+ case 'rejected': + throw promise.reason + default: + throw promise + } + }, - snapshotCache = new WeakMap(), + snapCache = new WeakMap(), - createSnapshot = ( - version: number, + createSnapshot: CreateSnapshot = ( target: T, - receiver: any + receiver: object, + version: number, + handlePromise: HandlePromise = defaultHandlePromise ): T => { - const cache = snapshotCache.get(receiver) + const cache = snapCache.get(receiver) if (cache?.[0] === version) { return cache[1] as T } - const snapshot: any = Array.isArray(target) + const snap: any = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target)) - markToTrack(snapshot, true) // mark to track - snapshotCache.set(receiver, [version, snapshot]) + markToTrack(snap, true) // mark to track + snapCache.set(receiver, [version, snap]) Reflect.ownKeys(target).forEach((key) => { const value = Reflect.get(target, key, receiver) if (refSet.has(value)) { markToTrack(value, false) // mark not to track - snapshot[key] = value + snap[key] = value } else if (value instanceof Promise) { - if (PROMISE_RESULT in value) { - snapshot[key] = (value as any)[PROMISE_RESULT] - } else { - const errorOrPromise = (value as any)[PROMISE_ERROR] || value - Object.defineProperty(snapshot, key, { - get() { - if (PROMISE_RESULT in value) { - return (value as any)[PROMISE_RESULT] - } - throw errorOrPromise - }, - }) - } - } else if (value?.[LISTENERS]) { - snapshot[key] = value[SNAPSHOT] + Object.defineProperty(snap, key, { + get() { + return handlePromise(value) + }, + }) + } else if (value?.[PROXY_STATE]) { + snap[key] = snapshot(value, handlePromise) } else { - snapshot[key] = value + snap[key] = value } }) - return Object.freeze(snapshot) + return Object.freeze(snap) }, proxyCache = new WeakMap(), @@ -147,23 +169,26 @@ const buildProxyFunction = ( ? [] : Object.create(Object.getPrototypeOf(initialObject)) const handler: ProxyHandler = { - get(target: T, prop: string | symbol, receiver: any) { - if (prop === VERSION) { - return version - } - if (prop === LISTENERS) { - return listeners - } - if (prop === SNAPSHOT) { - return createSnapshot(version, target, receiver) + get(target: T, prop: string | symbol, receiver: object) { + if (prop === PROXY_STATE) { + const state: ProxyState = [ + target, + receiver, + version, + createSnapshot, + listeners, + ] + return state } return Reflect.get(target, prop, receiver) }, deleteProperty(target: T, prop: string | symbol) { const prevValue = Reflect.get(target, prop) - const childListeners = prevValue?.[LISTENERS] + const childListeners = ( + (prevValue as any)?.[PROXY_STATE] as ProxyState | undefined + )?.[4] if (childListeners) { - childListeners.delete(popPropListener(prop)) + childListeners.delete(popPropListener(prop) as Listener) } const deleted = Reflect.deleteProperty(target, prop) if (deleted) { @@ -171,41 +196,45 @@ const buildProxyFunction = ( } return deleted }, - set(target: T, prop: string | symbol, value: any, receiver: any) { + set(target: T, prop: string | symbol, value: any, receiver: object) { const hasPrevValue = Reflect.has(target, prop) const prevValue = Reflect.get(target, prop, receiver) if (hasPrevValue && objectIs(prevValue, value)) { return true } - const childListeners = prevValue?.[LISTENERS] + const childListeners = ( + (prevValue as any)?.[PROXY_STATE] as ProxyState | undefined + )?.[4] if (childListeners) { - childListeners.delete(popPropListener(prop)) + childListeners.delete(popPropListener(prop) as Listener) } if (isObject(value)) { value = getUntracked(value) || value } - let nextValue: any + let nextValue = value if (Object.getOwnPropertyDescriptor(target, prop)?.set) { - nextValue = value + // do nothing } else if (value instanceof Promise) { - nextValue = value + value .then((v) => { - nextValue[PROMISE_RESULT] = v + value.status = 'fulfilled' + value.value = v notifyUpdate(['resolve', [prop], v]) - return v }) .catch((e) => { - nextValue[PROMISE_ERROR] = e + value.status = 'rejected' + value.reason = e notifyUpdate(['reject', [prop], e]) }) - } else if (value?.[LISTENERS]) { - nextValue = value - nextValue[LISTENERS].add(getPropListener(prop)) - } else if (canProxy(value)) { - nextValue = proxy(value) - nextValue[LISTENERS].add(getPropListener(prop)) } else { - nextValue = value + if (!value?.[PROXY_STATE] && canProxy(value)) { + nextValue = proxy(value) + } + if (nextValue?.[PROXY_STATE]) { + ;(nextValue[PROXY_STATE] as ProxyState)[4].add( + getPropListener(prop) + ) + } } Reflect.set(target, prop, nextValue, receiver) notifyUpdate(['set', [prop], value, prevValue]) @@ -232,17 +261,14 @@ const buildProxyFunction = ( // public functions proxyFunction, // shared state + PROXY_STATE, refSet, - VERSION, - LISTENERS, - SNAPSHOT, // internal things objectIs, newProxy, canProxy, - PROMISE_RESULT, - PROMISE_ERROR, - snapshotCache, + defaultHandlePromise, + snapCache, createSnapshot, proxyCache, versionHolder, @@ -255,7 +281,8 @@ export function proxy(initialObject: T = {} as T): T { } export function getVersion(proxyObject: unknown): number | undefined { - return isObject(proxyObject) ? (proxyObject as any)[VERSION] : undefined + const state = (proxyObject as any)?.[PROXY_STATE] as ProxyState | undefined + return state?.[2] } export function subscribe( @@ -263,11 +290,12 @@ export function subscribe( callback: (ops: Op[]) => void, notifyInSync?: boolean ) { - if (__DEV__ && !(proxyObject as any)?.[LISTENERS]) { + if (__DEV__ && !(proxyObject as any)?.[PROXY_STATE]) { console.warn('Please use proxy object') } let promise: Promise | undefined const ops: Op[] = [] + const listeners = ((proxyObject as any)[PROXY_STATE] as ProxyState)[4] const listener: Listener = (op) => { ops.push(op) if (notifyInSync) { @@ -277,23 +305,27 @@ export function subscribe( if (!promise) { promise = Promise.resolve().then(() => { promise = undefined - if ((proxyObject as any)[LISTENERS].has(listener)) { + if (listeners.has(listener)) { callback(ops.splice(0)) } }) } } - ;(proxyObject as any)[LISTENERS].add(listener) - return () => { - ;(proxyObject as any)[LISTENERS].delete(listener) - } + listeners.add(listener) + return () => listeners.delete(listener) } -export function snapshot(proxyObject: T): Snapshot { - if (__DEV__ && !(proxyObject as any)?.[SNAPSHOT]) { +export function snapshot( + proxyObject: T, + handlePromise?: HandlePromise +): Snapshot { + if (__DEV__ && !(proxyObject as any)?.[PROXY_STATE]) { console.warn('Please use proxy object') } - return (proxyObject as any)[SNAPSHOT] + const [target, receiver, version, createSnapshot] = (proxyObject as any)[ + PROXY_STATE + ] as ProxyState + return createSnapshot(target, receiver, version, handlePromise) as Snapshot } export function ref(obj: T): T & AsRef {