README
@qiwi/deep-proxy
Deep proxy implementation for TypeScript
Install
npm i @qiwi/deep-proxy
yarn add @qiwi/deep-proxy
Key features
- Single proxy handler with rich context instead of trap map
- Proxy self-reference in handler context (meaningful for methods binding)
- JS, TS and Flow support
- Directive shortcuts
- Proxies caching
Usage
import {DeepProxy} from '@qiwi/deep-proxy'
const target = {foo: 'bar', a: {b: 'c'}}
const proxy = new DeepProxy(target, ({trapName, value, key, DEFAULT, PROXY}: THandlerContext) => {
if (trapName === 'set') {
throw new TypeError('target is immutable')
}
if (trapName === 'get') {
if (typeof value === 'object' && value !== null) {
return PROXY
}
if (key === 'd') {
return 'baz'
}
return 'qux'
}
return DEFAULT
})
proxy.foo // qux
proxy.a.b // qux
proxy.bar // qux
proxy.d // baz
proxy.a = 'a' // TypeError
FP adepts may use createDeepProxy
factory instead of DeepProxy
class and get some magic.
import {createDeepProxy} from '@qiwi/deep-proxy'
// Regular usage case
const handler = ({DEFAULT}) => DEFAULT
const proxy1 = createDeepProxy(target, handler)
// Passing defaults through this context
const customProxyFactory = createDeepProxy.bind({handler})
const proxy2 = customProxyFactory(target)
All the traps follow to the single handler, so you're able to build various complex conditions in one place. This approach might be useful if you need some kind of "rich" model, but you don't want to complicate DTO.
if (path.length > 10) {
if (prop === 'foo' || ['get', 'set', 'ownKeys'].includes(trapName)) {
return DEFAULT
}
throw new Error('Bla-bla')
}
if (trapName === 'set' && typeof value !== 'number') {
throw new TypeError('only `number` type is allowed')
}
// and so on
return DEFAULT
Another example. Imagine a client which uses an unstable channel with 10% of loss.
const client = createClient({...opts})
await client.foo(...args) // 90% of success
const clientWithRetry = new DeepProxy(client, ({value, trapName}: THandlerContext) => {
if (trapName === 'get') {
if (typeof value === 'function') {
return retryify(value, 2)
}
if (typeof value === 'object' && value !== null) {
return PROXY
}
}
return DEFAULT
})
await clientWithRetry.foo(...args) // 99% of success
Metrics, debugging, throttling — all becomes better with deep proxy.
Directives
Directive | Description |
---|---|
DEFAULT |
Returns standard flow control. The current operation (get, set, ownKeys, etc) will be performed as without proxy. |
PROXY |
Returns a proxy of nested object with parent's proxy handler. |
A bit more sugar on top: by default PROXY
directive uses value
from context, but you can pass your own.
const proxy = new DeepProxy({foo: {bar: 'baz'}}, ({value, trapName}) => {
if (trapName === 'get' && typeof value === 'object' && value !== null) {
return PROXY({baz: 'qux'})
}
return DEFAULT
})
proxy.foo.baz // 'qux'
THandlerContext
type THandlerContext<T extends TTarget> = {
target: T // proxy target object/function
trapName: TTrapName // proxy handler trap: get, set, ownKeys and so on
traps: TTraps // proxy handler map reference
root: TTarget // root level proxy's target
args: any[] // trap method arguments as is
path: string[] // path to current proxy from root
key?: keyof T // prop key if defined in trap args
value: any // current field value by key
newValue?: any // new assigned value (#set())
handler: TProxyHandler // handler reference
PROXY: symbol // directives
DEFAULT: symbol
proxy: TTarget // proxy reference
}
Caching
createDeepProxy
factory returns the stored proxy reference when all the arguments matched one of the previous calls:
target
refs are strictly equalroot
refs equal toopath
values match
Note, this is not a regular memoization, but a loosely coupled WeakMap
, so unused proxies can be cleaned up by GC.
export type TProxyCache = {
// root object refers to some targets objects,
// that refer to map, that binds nested paths with their traps
traps: WeakMap<TTarget, WeakMap<TTarget, Map<string, TTraps>>>
// And these traps refer to proxies
proxies: WeakMap<TTraps, TProxy<TTarget>>
}
Note
Proxies are slow. Very slow. Use them wisely with care.
Alternatives & Refs
- tc39.es/proxy-object
- stackoverflow/how-to-create-a-deep-proxy
- samvv/js-proxy-deep
- lukigarazus/deep-proxy
- cronvel/nested-proxies
- CharlesStover/deep-proxy-polyfill