From c52f6a198d6f677fd6b46a4e1ae122a431de78cc Mon Sep 17 00:00:00 2001 From: Arnoud de Vries <6420061+arnoud-dv@users.noreply.github.com> Date: Tue, 26 Dec 2023 23:44:54 +0100 Subject: [PATCH] refactor(angular-query): generic signal proxy --- .../src/__tests__/signal-proxy.test.ts | 28 ++++++++++ .../src/create-base-query.ts | 4 +- .../src/query-proxy.ts | 51 ------------------- .../src/signal-proxy.ts | 48 +++++++++++++++++ 4 files changed, 78 insertions(+), 53 deletions(-) create mode 100644 packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts delete mode 100644 packages/angular-query-experimental/src/query-proxy.ts create mode 100644 packages/angular-query-experimental/src/signal-proxy.ts diff --git a/packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts b/packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts new file mode 100644 index 0000000000..affa8b6953 --- /dev/null +++ b/packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts @@ -0,0 +1,28 @@ +import { signal } from '@angular/core' +import { isReactive } from '@angular/core/primitives/signals' +import { describe } from 'vitest' +import { signalProxy } from '../signal-proxy' + +describe('signalProxy', () => { + const inputSignal = signal({ fn: () => 'bar', baz: 'qux' }) + const proxy = signalProxy(inputSignal) + + it('should have computed fields', () => { + expect(proxy.baz()).toEqual('qux') + expect(isReactive(proxy.baz)).toBe(true) + }) + + it('should pass through functions as-is', () => { + expect(proxy.fn()).toEqual('bar') + expect(isReactive(proxy.fn)).toBe(false) + }) + + it('supports "in" operator', () => { + expect('baz' in proxy).toBe(true) + expect('foo' in proxy).toBe(false) + }) + + it('supports "Object.keys"', () => { + expect(Object.keys(proxy)).toEqual(['fn', 'baz']) + }) +}) diff --git a/packages/angular-query-experimental/src/create-base-query.ts b/packages/angular-query-experimental/src/create-base-query.ts index fa739a494f..b12c6f745c 100644 --- a/packages/angular-query-experimental/src/create-base-query.ts +++ b/packages/angular-query-experimental/src/create-base-query.ts @@ -6,7 +6,7 @@ import { inject, signal, } from '@angular/core' -import { createResultStateSignalProxy } from './query-proxy' +import { signalProxy } from './signal-proxy' import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core' import type { CreateBaseQueryOptions, CreateBaseQueryResult } from './types' @@ -81,5 +81,5 @@ export function createBaseQuery< const unsubscribe = observer.subscribe(resultSignal.set) destroyRef.onDestroy(unsubscribe) - return createResultStateSignalProxy(resultSignal) + return signalProxy(resultSignal) } diff --git a/packages/angular-query-experimental/src/query-proxy.ts b/packages/angular-query-experimental/src/query-proxy.ts deleted file mode 100644 index b2b2070d7a..0000000000 --- a/packages/angular-query-experimental/src/query-proxy.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { type Signal, computed, untracked } from '@angular/core' -import type { DefaultError, QueryObserverResult } from '@tanstack/query-core' -import type { CreateBaseQueryResult } from './types' - -export function createResultStateSignalProxy< - TData = unknown, - TError = DefaultError, - State = QueryObserverResult, ->(resultState: Signal) { - const internalState = {} as CreateBaseQueryResult - - return new Proxy(internalState, { - get( - target: CreateBaseQueryResult, - prop: Key | string | symbol, - ) { - // first check if we have it in our internal state and return it - const computedField = target[prop as Key] - - // TODO: check if this if statement is necessary - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (computedField) return computedField - - // then, check if it's a function on the resultState and return it - const targetField = untracked(resultState)[prop as Key] - if (typeof targetField === 'function') return targetField - - // finally, create a computed field, store it and return it - // @ts-ignore - return (target[prop] = computed(() => resultState()[prop as Key])) - }, - has( - target: CreateBaseQueryResult, - prop: K | string | symbol, - ) { - return !!target[prop as K] - }, - ownKeys(target) { - return [...Reflect.ownKeys(target)] - }, - getOwnPropertyDescriptor() { - return { - enumerable: true, - configurable: true, - } - }, - set(): boolean { - return true - }, - }) -} diff --git a/packages/angular-query-experimental/src/signal-proxy.ts b/packages/angular-query-experimental/src/signal-proxy.ts new file mode 100644 index 0000000000..8b3cd884dd --- /dev/null +++ b/packages/angular-query-experimental/src/signal-proxy.ts @@ -0,0 +1,48 @@ +import { computed, untracked } from '@angular/core' +import type { Signal } from '@angular/core' + +type MapToSignals = { + [K in keyof T]: T[K] extends Function ? T[K] : Signal +} + +/** + * Exposes fields of an object passed via an Angular `Signal` as `Computed` signals. + * + * Functions on the object are passed through as-is. + * + * @param inputSignal - `Signal` that must return an object. + * + */ +export function signalProxy>( + inputSignal: Signal, +) { + const internalState = {} as MapToSignals + + return new Proxy>(internalState, { + get(target, prop) { + // first check if we have it in our internal state and return it + const computedField = target[prop] + if (computedField) return computedField + + // then, check if it's a function on the resultState and return it + const targetField = untracked(inputSignal)[prop] + if (typeof targetField === 'function') return targetField + + // finally, create a computed field, store it and return it + // @ts-ignore + return (target[prop] = computed(() => inputSignal()[prop])) + }, + has(_, prop) { + return !!untracked(inputSignal)[prop] + }, + ownKeys() { + return Reflect.ownKeys(untracked(inputSignal)) + }, + getOwnPropertyDescriptor() { + return { + enumerable: true, + configurable: true, + } + }, + }) +}