forked from aws/jsii
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathobjects.ts
More file actions
342 lines (295 loc) · 9.71 KB
/
Copy pathobjects.ts
File metadata and controls
342 lines (295 loc) · 9.71 KB
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
import * as spec from '@jsii/spec';
import assert from 'assert';
import { inspect } from 'util';
import * as api from './api';
import { JsiiFault } from './kernel';
import { EMPTY_OBJECT_FQN } from './serialization';
/**
* Symbol under which we store the { type -> objid } map on object instances
*/
const OBJID_SYMBOL = Symbol.for('$__jsii__objid__$');
/**
* Symbol under which we store the interfaces implemented by instances
*/
const IFACES_SYMBOL = Symbol.for('$__jsii__interfaces__$');
/**
* Symbol under which jsii runtime type information is stored.
*/
const JSII_RTTI_SYMBOL = Symbol.for('jsii.rtti');
/**
* Cache for resolved associations between constructors and FQNs.
*/
const RESOLVED_TYPE_FQN = new WeakMap<object, spec.FQN>();
/**
* Get the JSII fqn for an object (if available)
*
* This will return something if the object was constructed from a JSII-enabled
* class/constructor, or if a literal object was annotated with type
* information.
*
* @param obj the object for which a jsii FQN is requested.
* @param isVisibleType a function that determines if a type is visible.
*/
export function jsiiTypeFqn(
obj: any,
isVisibleType: (fqn: spec.FQN) => boolean,
): spec.FQN | undefined {
const ctor = obj.constructor;
// We've already resolved for this type, return the cached value.
if (RESOLVED_TYPE_FQN.has(ctor)) {
return RESOLVED_TYPE_FQN.get(ctor)!;
}
let curr = ctor;
while (curr[JSII_RTTI_SYMBOL]?.fqn) {
if (isVisibleType(curr[JSII_RTTI_SYMBOL].fqn)) {
const fqn = curr[JSII_RTTI_SYMBOL].fqn;
tagJsiiConstructor(curr, fqn);
tagJsiiConstructor(ctor, fqn);
return fqn;
}
// Walk up the prototype chain...
curr = Object.getPrototypeOf(curr);
}
return undefined;
}
/**
* If this object was previously serialized under a given reference, return the same reference
*
* This is to retain object identity across invocations.
*/
export function objectReference(obj: unknown): api.ObjRef | undefined {
// If this object as already returned
if ((obj as any)[OBJID_SYMBOL]) {
return {
[api.TOKEN_REF]: (obj as ManagedObject)[OBJID_SYMBOL],
[api.TOKEN_INTERFACES]: (obj as ManagedObject)[IFACES_SYMBOL],
};
}
return undefined;
}
type ManagedObject = {
[OBJID_SYMBOL]: string;
[IFACES_SYMBOL]?: string[];
};
function tagObject(obj: unknown, objid: string, interfaces?: string[]) {
const privateField: Omit<PropertyDescriptor, 'value' | 'get' | 'set'> = {
// Make sure the field does not show in `JSON.stringify` outputs, and is not
// copied by splat expressions (`{...obj}`), as this would be problematic.
// See https://github.com/aws/aws-cdk/issues/17876 for an example of the
// consequences this could have.
enumerable: false,
// Probably not necessary, but allow the property to be re-configured (it
// would be good to make this `false` in the future, but might cause weird
// bugs, so not doing it now...)
configurable: true,
writable: true,
};
// Log a warning in case we are re-tagging this value, so we can hopefully
// discover about the bugs we'd have if we did not make it configurable nor
// writable.
if (Object.prototype.hasOwnProperty.call(obj, OBJID_SYMBOL)) {
console.error(
`[jsii/kernel] WARNING: object ${inspect(obj, {
depth: 2,
breakLength: Infinity,
})} was already tagged as ${(obj as any)[OBJID_SYMBOL]}!`,
);
}
Object.defineProperty(obj, OBJID_SYMBOL, { ...privateField, value: objid });
Object.defineProperty(obj, IFACES_SYMBOL, {
...privateField,
value: interfaces,
});
}
/**
* Set the JSII FQN for classes produced by a given constructor
*/
export function tagJsiiConstructor(constructor: any, fqn: spec.FQN) {
const existing = RESOLVED_TYPE_FQN.get(constructor);
if (existing != null) {
return assert.strictEqual(
existing,
fqn,
`Unable to register ${constructor.name} as ${fqn}: it is already registerd with FQN ${existing}`,
);
}
// Mark this constructor as exported from a jsii module, so we know we
// should be considering it's FQN as a valid exported type.
RESOLVED_TYPE_FQN.set(constructor, fqn);
}
/**
* Table of JSII objects
*
* There can be multiple references to the same object, each under a different requested
* type.
*/
export class ObjectTable {
readonly #resolveType: (fqn: spec.FQN) => spec.Type;
readonly #objects = new Map<string, RegisteredObject>();
#nextid = 10000;
public constructor(resolveType: (fqn: spec.FQN) => spec.Type) {
this.#resolveType = resolveType;
}
/**
* Register the given object with the given type
*
* Return the existing registration if available.
*/
public registerObject(
obj: unknown,
fqn: spec.FQN,
interfaces?: spec.FQN[],
): api.ObjRef {
if (fqn === undefined) {
throw new JsiiFault('FQN cannot be undefined');
}
const existingRef = objectReference(obj);
if (existingRef) {
if (interfaces) {
const allIfaces = new Set(interfaces);
for (const iface of existingRef[api.TOKEN_INTERFACES] ?? []) {
allIfaces.add(iface);
}
// Note - obj[INTERFACES_SYMBOL] should already have been declared as a
// private property by a previous call to tagObject at this stage.
if (!Object.prototype.hasOwnProperty.call(obj, IFACES_SYMBOL)) {
console.error(
`[jsii/kernel] WARNING: referenced object ${
existingRef[api.TOKEN_REF]
} does not have the ${String(IFACES_SYMBOL)} property!`,
);
}
this.#objects.get(existingRef[api.TOKEN_REF])!.interfaces =
(obj as any)[IFACES_SYMBOL] =
existingRef[api.TOKEN_INTERFACES] =
interfaces =
this.#removeRedundant(Array.from(allIfaces), fqn);
}
return existingRef;
}
interfaces = this.#removeRedundant(interfaces, fqn);
const objid = this.#makeId(fqn);
this.#objects.set(objid, { instance: obj, fqn, interfaces });
tagObject(obj, objid, interfaces);
return { [api.TOKEN_REF]: objid, [api.TOKEN_INTERFACES]: interfaces };
}
/**
* Find the object and registered type for the given ObjRef
*/
public findObject(objref: api.ObjRef): RegisteredObject {
if (typeof objref !== 'object' || !(api.TOKEN_REF in objref)) {
throw new JsiiFault(
`Malformed object reference: ${JSON.stringify(objref)}`,
);
}
const objid = objref[api.TOKEN_REF];
const obj = this.#objects.get(objid);
if (!obj) {
throw new JsiiFault(`Object ${objid} not found`);
}
// If there are "additional" interfaces declared on the objref, merge them
// into the returned object. This is used to support client-side forced
// down-casting (a.k.a: unsafe casting). We do NOT register the extra
// interfaces here so that if the client provided an interface that is
// actually not implemented, we aren't "poisoning" our state with that
// incorrect information.
const additionalInterfaces = objref[api.TOKEN_INTERFACES];
if (additionalInterfaces != null && additionalInterfaces.length > 0) {
return {
...obj,
interfaces: [
...(obj.interfaces ?? []),
// We append at the end so "registered" interface information has
// precedence over client-declared ones.
...additionalInterfaces,
],
};
}
return obj;
}
/**
* Delete the registration with the given objref
*/
public deleteObject({ [api.TOKEN_REF]: objid }: api.ObjRef) {
if (!this.#objects.delete(objid)) {
throw new JsiiFault(`Object ${objid} not found`);
}
}
public get count(): number {
return this.#objects.size;
}
#makeId(fqn: spec.FQN) {
return `${fqn}@${this.#nextid++}`;
}
#removeRedundant(
interfaces: spec.FQN[] | undefined,
fqn: spec.FQN,
): spec.FQN[] | undefined {
if (!interfaces || interfaces.length === 0) {
return undefined;
}
const result = new Set(interfaces);
const builtIn = new InterfaceCollection(this.#resolveType);
if (fqn !== EMPTY_OBJECT_FQN) {
builtIn.addFromClass(fqn);
}
interfaces.forEach(builtIn.addFromInterface.bind(builtIn));
for (const iface of builtIn) {
result.delete(iface);
}
return result.size > 0 ? Array.from(result).sort() : undefined;
}
}
export interface RegisteredObject {
instance: any;
fqn: spec.FQN;
interfaces?: spec.FQN[];
}
class InterfaceCollection implements Iterable<string> {
readonly #resolveType: (fqn: spec.FQN) => spec.Type;
readonly #interfaces = new Set<spec.FQN>();
public constructor(resolveType: (fqn: spec.FQN) => spec.Type) {
this.#resolveType = resolveType;
}
public addFromClass(fqn: spec.FQN): void {
const ti = this.#resolveType(fqn);
if (!spec.isClassType(ti)) {
throw new JsiiFault(
`Expected a class, but received ${spec.describeTypeReference(ti)}`,
);
}
if (ti.base) {
this.addFromClass(ti.base);
}
if (ti.interfaces) {
for (const iface of ti.interfaces) {
if (this.#interfaces.has(iface)) {
continue;
}
this.#interfaces.add(iface);
this.addFromInterface(iface);
}
}
}
public addFromInterface(fqn: spec.FQN): void {
const ti = this.#resolveType(fqn);
if (!spec.isInterfaceType(ti)) {
throw new JsiiFault(
`Expected an interface, but received ${spec.describeTypeReference(ti)}`,
);
}
if (!ti.interfaces) {
return;
}
for (const iface of ti.interfaces) {
if (this.#interfaces.has(iface)) {
continue;
}
this.#interfaces.add(iface);
this.addFromInterface(iface);
}
}
public [Symbol.iterator]() {
return this.#interfaces[Symbol.iterator]();
}
}