qiwi/deep-proxy

View on GitHub
src/main/ts/proxy.ts

Summary

Maintainability
A
3 hrs
Test Coverage
A
93%
// eslint-disable-next-line
import type {
  TTarget,
  TProxy,
  TProxyFactory,
  TTrapName,
  TTraps,
  THandlerContext,
  TProxyHandler,
  TTrapContext,
  TProxyFactoryThis,
  DeepProxyConstructor,
} from './interface.ts'

import { addToCache, getFromCache } from './cache.ts'

export const DEFAULT = Symbol('default')

const trapNames = Object.keys(
  Object.getOwnPropertyDescriptors(Reflect),
) as Array<TTrapName>

const trapsWithKey = new Set([
  'get',
  'has',
  'set',
  'defineProperty',
  'deleteProperty',
  'getOwnPropertyDescriptor',
])

const parseParameters = <T extends TTarget>(trapName: TTrapName, parameters: [T, ...any[]]): {
  target: T,
  name: keyof T,
  val: any,
  receiver: any,
  args: any[],
  descriptor: PropertyDescriptor,
  thisValue: any,
  prototype: any,

} => {
  // https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Proxy
  let target, name, val, receiver, args, descriptor, thisValue, prototype

  // prettier-ignore
  switch (trapName) {
    case 'get':
      [target, name, receiver] = parameters
      break
    case 'set':
      [target, name, val, receiver] = parameters
      break
    case 'deleteProperty':
    case 'defineProperty':
      [target, descriptor] = parameters
      break
    case 'has':
    case 'getOwnPropertyDescriptor':
      [target, name] = parameters
      break
    case 'apply':
      [target, thisValue, args] = parameters
      break
     case 'construct':
      [target, args] = parameters
      break
    case 'setPrototypeOf':
      [target, prototype] = parameters
       break
    case 'preventExtensions':
    case 'isExtensible':
    case 'ownKeys':
    case 'getPrototypeOf':
    default:
      [target] = parameters
  }
  return {
    target,
    name,
    receiver,
    val,
    args,
    descriptor,
    thisValue,
    prototype
  }
}

const createHandlerContext = <T extends TTarget>(
  trapContext: TTrapContext,
  parameters: [T, ...any[]],
): THandlerContext<T> => {
  const { trapName, handler, traps, root, path } = trapContext
  const {target, name, val, receiver, args, descriptor, thisValue, prototype} = parseParameters(trapName, parameters)
  const key = trapsWithKey.has(trapName) ? name : undefined
  const newValue = trapName === 'set' ? val : undefined

  // prettier-ignore
  return {
    parameters,
    target,
    name,
    val,
    args,
    descriptor,
    receiver,
    thisValue,
    prototype,

    trapName,
    traps,
    path,
    handler,
    key,
    newValue,
    root,
    get proxy() { return getFromCache(root, target, path) as TProxy<T>  },
    get value() { return key && target[key] },
    DEFAULT,
    PROXY: createDeepProxy.bind({ root, handler, path: [...path, key as string] }),
  }
}

const trap = function <T extends TTarget>(
  this: TTrapContext,
  ...parameters: [T, ...any[]]
) {
  const { trapName, handler } = this
  const handlerContext = createHandlerContext(this, parameters)
  const { PROXY, DEFAULT, value } = handlerContext
  const result = handler(handlerContext)

  if (result === PROXY) {
    return PROXY(value)
  }

  if (result === DEFAULT) {
    // eslint-disable-next-line
    // @ts-ignore
    return Reflect[trapName](...parameters)
  }

  return result
}

const createTraps = (handler: TProxyHandler, root: TTarget, path: string[]) =>
  trapNames.reduce<TTraps>((traps, trapName) => {
    traps[trapName] = trap.bind({
      trapName,
      handler,
      traps,
      root,
      path,
    })
    return traps
  }, {})

const checkTarget = (target: any): void => {
  if (
    target === null ||
    (typeof target !== 'object' && typeof target !== 'function')
  ) {
    throw new TypeError(
      'Deep proxy could be applied to objects and functions only',
    )
  }
}

export const defaultProxyHandler: TProxyHandler = ({ DEFAULT }) => DEFAULT

export const createDeepProxy: TProxyFactory = function <T extends TTarget>(
  this: TProxyFactoryThis | void,
  target: T,
  handler?: TProxyHandler,
  path?: string[],
  root?: TTarget,
): T {
  checkTarget(target)

  const _this: TProxyFactoryThis = { ...this }
  const _handler = handler || _this.handler || defaultProxyHandler
  const _path = path || _this.path || []
  const _root = _this.root || root || target
  const _proxy = getFromCache(_root, target, _path)

  if (_proxy) {
    return _proxy as T
  }

  const traps = createTraps(_handler, _root, _path)
  const proxy = new Proxy<T>(target, traps)

  addToCache(_root, target, _path, traps, proxy)

  return proxy
}

export const DeepProxy = class<T extends TTarget> {
  constructor(
    target: T,
    handler?: TProxyHandler,
    path?: string[],
    root?: TTarget,
  ) {
    return createDeepProxy(target, handler, path, root)
  }
} as DeepProxyConstructor