-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
createCompositeProxy.ts
136 lines (117 loc) · 4.2 KB
/
createCompositeProxy.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import { type InspectOptions, inspect } from 'util'
import { defaultPropertyDescriptor } from '../model/utils/defaultProxyHandlers'
export interface CompositeProxyLayer<KeyType extends string | symbol = string | symbol> {
/**
* Returns a list of keys, defined by a layer
*/
getKeys(): KeyType[]
/**
* Returns a value for a property for a given key (one of the keys, returned
* from `getKeys`)
* @param key
*/
getPropertyValue(key: KeyType): unknown
/**
* Gets a descriptor for given property. If not implemented or undefined is returned, { enumerable: true, writeable: true, configurable: true} is defaulted
* is used
* @param key
*/
getPropertyDescriptor?(key: KeyType): PropertyDescriptor | undefined
/**
* Allows to override results for hasOwnProperty/in operator. If not implemented, returns true
* @param key
*/
has?(key: KeyType): boolean
}
const customInspect = Symbol.for('nodejs.util.inspect.custom')
/**
* Creates a proxy from a set of layers.
* Each layer is a building for a proxy (potentially, reusable) that
* can add or override property on top of the target.
* When multiple layers define the same property, last one wins
*
* @param target
* @param layers
* @returns
*/
export function createCompositeProxy<T extends object>(target: T, layers: CompositeProxyLayer[]): T {
const keysToLayerMap = mapKeysToLayers(layers)
const overwrittenKeys = new Set<string | symbol>()
const proxy = new Proxy(target, {
get(target, prop) {
// explicit overwrites of a property have highest priority
if (overwrittenKeys.has(prop)) {
return target[prop]
}
// next, we see if property is defined in one of the layers
const layer = keysToLayerMap.get(prop)
if (layer) {
return layer.getPropertyValue(prop)
}
// finally, we read a prop from target
return target[prop]
},
has(target, prop) {
if (overwrittenKeys.has(prop)) {
return true
}
const layer = keysToLayerMap.get(prop)
if (layer) {
return layer.has?.(prop) ?? true
}
return Reflect.has(target, prop)
},
ownKeys(target) {
const targetKeys = getExistingKeys(Reflect.ownKeys(target), keysToLayerMap)
const layerKeys = getExistingKeys(Array.from(keysToLayerMap.keys()), keysToLayerMap)
return [...new Set([...targetKeys, ...layerKeys, ...overwrittenKeys])]
},
set(target, prop, value) {
const layer = keysToLayerMap.get(prop)
if (layer?.getPropertyDescriptor?.(prop)?.writable === false) {
return false
}
overwrittenKeys.add(prop)
return Reflect.set(target, prop, value)
},
getOwnPropertyDescriptor(target, prop) {
const layer = keysToLayerMap.get(prop)
if (layer && layer.getPropertyDescriptor) {
return {
...defaultPropertyDescriptor,
...layer.getPropertyDescriptor(prop),
}
}
return defaultPropertyDescriptor
},
defineProperty(target, property, attributes) {
overwrittenKeys.add(property)
return Reflect.defineProperty(target, property, attributes)
},
})
proxy[customInspect] = function (depth: number, options: InspectOptions, defaultInspect: typeof inspect = inspect) {
// Default node.js console.log and util.inspect deliberately avoid triggering any proxy traps and log
// original target. This is not we want for our usecases: we want console.log to output the result as if
// the properties actually existed on the target. Using spread operator forces us to produce correct object
const toLog = { ...this }
delete toLog[customInspect]
return defaultInspect(toLog, options)
}
return proxy
}
function mapKeysToLayers(layers: CompositeProxyLayer[]) {
const keysToLayerMap = new Map<string | symbol, CompositeProxyLayer>()
for (const layer of layers) {
const keys = layer.getKeys()
for (const key of keys) {
keysToLayerMap.set(key, layer)
}
}
return keysToLayerMap
}
function getExistingKeys(keys: Array<string | symbol>, keysToLayerMap: Map<string | symbol, CompositeProxyLayer>) {
return keys.filter((key) => {
const layer = keysToLayerMap.get(key)
return layer?.has?.(key) ?? true
})
}