diff --git a/docs/api/utils/derive.mdx b/docs/api/utils/derive.mdx index b4c7fb68..c5ff7ce6 100644 --- a/docs/api/utils/derive.mdx +++ b/docs/api/utils/derive.mdx @@ -7,12 +7,18 @@ description: 'create a new proxy derived from others' # derive +## Installation + +```bash +npm install derive-valtio +``` + #### create a new proxy derived from others You can subscribe to some proxies using `get` to create snapshots used to compute new values. ```js -import { derive } from 'valtio/utils' +import { derive } from 'derive-valtio' // create a base proxy const state = proxy({ @@ -43,7 +49,7 @@ In some cases you may want to unsubscribe after deriving a proxy. To do so, use and you can attach new derived properties. ```js -import { derive, underive } from 'valtio/utils' +import { derive, underive } from 'derive-valtio' const state = proxy({ count: 1, }) diff --git a/package.json b/package.json index da3e94c5..08b86afa 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "homepage": "https://github.com/pmndrs/valtio", "dependencies": { "proxy-compare": "2.5.1", + "derive-valtio": "0.1.0", "use-sync-external-store": "1.2.0" }, "devDependencies": { diff --git a/src/vanilla/utils.ts b/src/vanilla/utils.ts index 7040a64b..69285a3a 100644 --- a/src/vanilla/utils.ts +++ b/src/vanilla/utils.ts @@ -1,11 +1,7 @@ export { subscribeKey } from './utils/subscribeKey.ts' export { watch } from './utils/watch.ts' export { devtools } from './utils/devtools.ts' -export { - derive, - underive, - unstable_deriveSubscriptions, -} from './utils/derive.ts' +export { derive, underive, unstable_deriveSubscriptions } from 'derive-valtio' export { addComputed_DEPRECATED as addComputed } from './utils/addComputed.ts' export { proxyWithComputed_DEPRECATED as proxyWithComputed } from './utils/proxyWithComputed.ts' export { proxyWithHistory } from './utils/proxyWithHistory.ts' diff --git a/src/vanilla/utils/addComputed.ts b/src/vanilla/utils/addComputed.ts index 484a9457..bb1da935 100644 --- a/src/vanilla/utils/addComputed.ts +++ b/src/vanilla/utils/addComputed.ts @@ -1,4 +1,4 @@ -import { derive } from './derive.ts' +import { derive } from 'derive-valtio' /** * addComputed (DEPRECATED) diff --git a/src/vanilla/utils/derive.ts b/src/vanilla/utils/derive.ts deleted file mode 100644 index c9af01ae..00000000 --- a/src/vanilla/utils/derive.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { getVersion, proxy, subscribe } from '../../vanilla.ts' - -type DeriveGet = (proxyObject: T) => T - -type Subscription = { - s: object // "s"ourceObject - d: object // "d"erivedObject - k: string // derived "k"ey - c: () => void // "c"allback - n: boolean // "n"otifyInSync - i: string[] // "i"goringKeys - p?: Promise // "p"romise -} - -type SourceObjectEntry = [ - subscriptions: Set, - unsubscribe: () => void, - pendingCount: number, - pendingCallbacks: Set<() => void>, -] - -type DerivedObjectEntry = [subscriptions: Set] - -const sourceObjectMap = new WeakMap() -const derivedObjectMap = new WeakMap() - -const markPending = (sourceObject: object, callback?: () => void) => { - const sourceObjectEntry = sourceObjectMap.get(sourceObject) - if (sourceObjectEntry) { - sourceObjectEntry[0].forEach((subscription) => { - const { d: derivedObject } = subscription - if (sourceObject !== derivedObject) { - markPending(derivedObject) - } - }) - ++sourceObjectEntry[2] // pendingCount - if (callback) { - sourceObjectEntry[3].add(callback) // pendingCallbacks - } - } -} - -// has side effect (even though used in Array.map) -const checkPending = (sourceObject: object, callback: () => void) => { - const sourceObjectEntry = sourceObjectMap.get(sourceObject) - if (sourceObjectEntry?.[2]) { - sourceObjectEntry[3].add(callback) // pendingCallbacks - return true - } - return false -} - -const unmarkPending = (sourceObject: object) => { - const sourceObjectEntry = sourceObjectMap.get(sourceObject) - if (sourceObjectEntry) { - --sourceObjectEntry[2] // pendingCount - if (!sourceObjectEntry[2]) { - sourceObjectEntry[3].forEach((callback) => callback()) - sourceObjectEntry[3].clear() // pendingCallbacks - } - sourceObjectEntry[0].forEach((subscription) => { - const { d: derivedObject } = subscription - if (sourceObject !== derivedObject) { - unmarkPending(derivedObject) - } - }) - } -} - -const addSubscription = (subscription: Subscription) => { - const { s: sourceObject, d: derivedObject } = subscription - let derivedObjectEntry = derivedObjectMap.get(derivedObject) - if (!derivedObjectEntry) { - derivedObjectEntry = [new Set()] - derivedObjectMap.set(subscription.d, derivedObjectEntry) - } - derivedObjectEntry[0].add(subscription) - let sourceObjectEntry = sourceObjectMap.get(sourceObject) - if (!sourceObjectEntry) { - const subscriptions = new Set() - const unsubscribe = subscribe( - sourceObject, - (ops) => { - subscriptions.forEach((subscription) => { - const { - d: derivedObject, - c: callback, - n: notifyInSync, - i: ignoreKeys, - } = subscription - if ( - sourceObject === derivedObject && - ops.every( - (op) => - op[1].length === 1 && ignoreKeys.includes(op[1][0] as string) - ) - ) { - // only setting derived properties - return - } - if (subscription.p) { - // already scheduled - return - } - markPending(sourceObject, callback) - if (notifyInSync) { - unmarkPending(sourceObject) - } else { - subscription.p = Promise.resolve().then(() => { - delete subscription.p // promise - unmarkPending(sourceObject) - }) - } - }) - }, - true - ) - sourceObjectEntry = [subscriptions, unsubscribe, 0, new Set()] - sourceObjectMap.set(sourceObject, sourceObjectEntry) - } - sourceObjectEntry[0].add(subscription) -} - -const removeSubscription = (subscription: Subscription) => { - const { s: sourceObject, d: derivedObject } = subscription - const derivedObjectEntry = derivedObjectMap.get(derivedObject) - derivedObjectEntry?.[0].delete(subscription) - if (derivedObjectEntry?.[0].size === 0) { - derivedObjectMap.delete(derivedObject) - } - const sourceObjectEntry = sourceObjectMap.get(sourceObject) - if (sourceObjectEntry) { - const [subscriptions, unsubscribe] = sourceObjectEntry - subscriptions.delete(subscription) - if (!subscriptions.size) { - unsubscribe() - sourceObjectMap.delete(sourceObject) - } - } -} - -const listSubscriptions = (derivedObject: object) => { - const derivedObjectEntry = derivedObjectMap.get(derivedObject) - if (derivedObjectEntry) { - return Array.from(derivedObjectEntry[0]) // NOTE do we need to copy? - } - return [] -} - -// NOTE This is experimentally exported. -// The availability is not guaranteed, and it will be renamed, -// changed or removed without any notice in future versions. -// It's not expected to use this in production. -export const unstable_deriveSubscriptions = { - add: addSubscription, - remove: removeSubscription, - list: listSubscriptions, -} - -/** - * derive - * - * This creates derived properties and attaches them - * to a new proxy object or an existing proxy object. - * - * @example - * import { proxy } from 'valtio' - * import { derive } from 'valtio/utils' - * - * const state = proxy({ - * count: 1, - * }) - * - * const derivedState = derive({ - * doubled: (get) => get(state).count * 2, - * }) - * - * derive({ - * tripled: (get) => get(state).count * 3, - * }, { - * proxy: state, - * }) - */ -export function derive( - derivedFns: { - [K in keyof U]: (get: DeriveGet) => U[K] - }, - options?: { - proxy?: T - sync?: boolean - } -) { - const proxyObject = (options?.proxy || proxy({})) as U - const notifyInSync = !!options?.sync - const derivedKeys = Object.keys(derivedFns) - derivedKeys.forEach((key) => { - if (Object.getOwnPropertyDescriptor(proxyObject, key)) { - throw new Error('object property already defined') - } - const fn = derivedFns[key as keyof U] - type DependencyEntry = { - v: number // "v"ersion - s?: Subscription // "s"ubscription - } - let lastDependencies: Map | null = null - const evaluate = () => { - if (lastDependencies) { - if ( - Array.from(lastDependencies) - .map(([p]) => checkPending(p, evaluate)) - .some((isPending) => isPending) - ) { - // some dependencies are pending - return - } - if ( - Array.from(lastDependencies).every( - ([p, entry]) => getVersion(p) === entry.v - ) - ) { - // no dependencies are changed - return - } - } - const dependencies = new Map() - const get =

(p: P) => { - dependencies.set(p, { v: getVersion(p) as number }) - return p - } - const value = fn(get) - const subscribeToDependencies = () => { - dependencies.forEach((entry, p) => { - const lastSubscription = lastDependencies?.get(p)?.s - if (lastSubscription) { - entry.s = lastSubscription - } else { - const subscription: Subscription = { - s: p, // sourceObject - d: proxyObject, // derivedObject - k: key, // derived key - c: evaluate, // callback - n: notifyInSync, - i: derivedKeys, // ignoringKeys - } - addSubscription(subscription) - entry.s = subscription - } - }) - lastDependencies?.forEach((entry, p) => { - if (!dependencies.has(p) && entry.s) { - removeSubscription(entry.s) - } - }) - lastDependencies = dependencies - } - if ((value as unknown) instanceof Promise) { - ;(value as Promise).finally(subscribeToDependencies) - } else { - subscribeToDependencies() - } - proxyObject[key as keyof U] = value - } - evaluate() - }) - return proxyObject as T & U -} - -/** - * underive - * - * This stops derived properties to evaluate. - * It will stop all (or specified by `keys` option) subscriptions. - * If you specify `delete` option, it will delete the properties - * and you can attach new derived properties. - * - * @example - * import { proxy } from 'valtio' - * import { derive, underive } from 'valtio/utils' - * - * const state = proxy({ - * count: 1, - * }) - * - * const derivedState = derive({ - * doubled: (get) => get(state).count * 2, - * }) - * - * underive(derivedState) - */ -export function underive( - proxyObject: T & U, - options?: { - delete?: boolean - keys?: (keyof U)[] - } -) { - const keysToDelete = options?.delete ? new Set() : null - listSubscriptions(proxyObject).forEach((subscription) => { - const { k: key } = subscription - if (!options?.keys || options.keys.includes(key as keyof U)) { - removeSubscription(subscription) - if (keysToDelete) { - keysToDelete.add(key as keyof U) - } - } - }) - if (keysToDelete) { - keysToDelete.forEach((key) => { - delete proxyObject[key] - }) - } -} diff --git a/vitest.config.ts b/vitest.config.ts index e9b3ec82..33c35fa4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,11 +1,12 @@ +import path from 'path' // eslint-disable-next-line import/extensions import { defineConfig } from 'vitest/config' export default defineConfig({ resolve: { alias: [ - { find: /^valtio$/, replacement: './src/index.ts' }, - { find: /^valtio(.*)$/, replacement: './src/$1.ts' }, + { find: /^valtio$/, replacement: path.resolve('./src/index.ts') }, + { find: /^valtio(.*)$/, replacement: path.resolve('./src/$1.ts') }, ], }, test: { diff --git a/yarn.lock b/yarn.lock index 9b7758de..20b605c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2465,6 +2465,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +derive-valtio@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/derive-valtio/-/derive-valtio-0.1.0.tgz#4b9fb393dfefccfef15fcbbddd745dd22d5d63d7" + integrity sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A== + diff-sequences@^29.4.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921"