autotel-cloudflare 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actors.js +1 -1
- package/dist/bindings.d.ts +113 -1
- package/dist/bindings.js +2 -2
- package/dist/chunk-4UG2QCPQ.js +1060 -0
- package/dist/chunk-4UG2QCPQ.js.map +1 -0
- package/dist/{chunk-5NL62W4L.js → chunk-O4IYKWPJ.js} +8 -3
- package/dist/chunk-O4IYKWPJ.js.map +1 -0
- package/dist/{chunk-ADWSZ5GY.js → chunk-ZJPX4N7S.js} +3 -3
- package/dist/{chunk-ADWSZ5GY.js.map → chunk-ZJPX4N7S.js.map} +1 -1
- package/dist/handlers.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +34 -6
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/bindings/ai.test.ts +156 -0
- package/src/bindings/ai.ts +71 -0
- package/src/bindings/analytics-engine.test.ts +160 -0
- package/src/bindings/analytics-engine.ts +78 -0
- package/src/bindings/bindings-detection.test.ts +235 -0
- package/src/bindings/bindings.ts +98 -47
- package/src/bindings/browser-rendering.test.ts +144 -0
- package/src/bindings/browser-rendering.ts +70 -0
- package/src/bindings/common.ts +9 -0
- package/src/bindings/hyperdrive.test.ts +154 -0
- package/src/bindings/hyperdrive.ts +74 -0
- package/src/bindings/images.test.ts +229 -0
- package/src/bindings/images.ts +182 -0
- package/src/bindings/index.ts +8 -0
- package/src/bindings/queue-producer.test.ts +192 -0
- package/src/bindings/queue-producer.ts +105 -0
- package/src/bindings/rate-limiter.test.ts +124 -0
- package/src/bindings/rate-limiter.ts +69 -0
- package/src/bindings/vectorize.test.ts +340 -0
- package/src/bindings/vectorize.ts +86 -0
- package/src/index.ts +8 -0
- package/src/wrappers/cf-attributes.test.ts +275 -0
- package/src/wrappers/instrument.ts +38 -0
- package/dist/chunk-5NL62W4L.js.map +0 -1
- package/dist/chunk-UPQE3J4I.js +0 -520
- package/dist/chunk-UPQE3J4I.js.map +0 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { instrument } from './instrument';
|
|
3
|
+
import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
|
|
4
|
+
|
|
5
|
+
describe('CF Attributes extraction via instrument()', () => {
|
|
6
|
+
let mockTracer: any;
|
|
7
|
+
let mockSpan: any;
|
|
8
|
+
let getTracerSpy: any;
|
|
9
|
+
let capturedSpanOptions: any;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockSpan = {
|
|
13
|
+
spanContext: () => ({
|
|
14
|
+
traceId: 'test-trace-id',
|
|
15
|
+
spanId: 'test-span-id',
|
|
16
|
+
traceFlags: 1,
|
|
17
|
+
}),
|
|
18
|
+
setAttribute: vi.fn(),
|
|
19
|
+
setAttributes: vi.fn(),
|
|
20
|
+
setStatus: vi.fn(),
|
|
21
|
+
recordException: vi.fn(),
|
|
22
|
+
end: vi.fn(),
|
|
23
|
+
isRecording: () => true,
|
|
24
|
+
updateName: vi.fn(),
|
|
25
|
+
addEvent: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
capturedSpanOptions = null;
|
|
29
|
+
|
|
30
|
+
mockTracer = {
|
|
31
|
+
startActiveSpan: vi.fn((...args: any[]) => {
|
|
32
|
+
// startActiveSpan can be called with (name, options, context, fn) or (name, options, fn)
|
|
33
|
+
// The fetch instrumentation calls it with 4 args: (name, options, parentContext, fn)
|
|
34
|
+
const fn = args.at(-1);
|
|
35
|
+
if (args.length >= 2) {
|
|
36
|
+
capturedSpanOptions = args[1];
|
|
37
|
+
}
|
|
38
|
+
return fn(mockSpan);
|
|
39
|
+
}),
|
|
40
|
+
setHeadSampler: vi.fn(),
|
|
41
|
+
forceFlush: vi.fn(async () => {}),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
getTracerSpy.mockRestore();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function createMockCtx() {
|
|
52
|
+
return {
|
|
53
|
+
waitUntil: vi.fn(),
|
|
54
|
+
passThroughOnException: vi.fn(),
|
|
55
|
+
} as any;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
it('should extract CF attributes when request has .cf object', async () => {
|
|
59
|
+
const handler = {
|
|
60
|
+
async fetch(request: Request) {
|
|
61
|
+
return new Response('OK', { status: 200 });
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const instrumented = instrument(handler, {
|
|
66
|
+
service: { name: 'test-worker' },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const request = new Request('http://example.com/test');
|
|
70
|
+
// Attach .cf properties to the request (Cloudflare Workers runtime does this)
|
|
71
|
+
Object.defineProperty(request, 'cf', {
|
|
72
|
+
value: {
|
|
73
|
+
colo: 'SJC',
|
|
74
|
+
country: 'US',
|
|
75
|
+
city: 'San Jose',
|
|
76
|
+
region: 'California',
|
|
77
|
+
continent: 'NA',
|
|
78
|
+
timezone: 'America/Los_Angeles',
|
|
79
|
+
latitude: '37.3382',
|
|
80
|
+
longitude: '-121.8863',
|
|
81
|
+
asn: 13_335,
|
|
82
|
+
asOrganization: 'Cloudflare Inc',
|
|
83
|
+
httpProtocol: 'HTTP/2',
|
|
84
|
+
tlsVersion: 'TLSv1.3',
|
|
85
|
+
clientTcpRtt: 5,
|
|
86
|
+
},
|
|
87
|
+
writable: false,
|
|
88
|
+
enumerable: true,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const env = {} as any;
|
|
92
|
+
const ctx = createMockCtx();
|
|
93
|
+
|
|
94
|
+
await instrumented.fetch!(request, env, ctx);
|
|
95
|
+
|
|
96
|
+
expect(capturedSpanOptions).toBeDefined();
|
|
97
|
+
const attrs = capturedSpanOptions.attributes;
|
|
98
|
+
|
|
99
|
+
expect(attrs['cloudflare.colo']).toBe('SJC');
|
|
100
|
+
expect(attrs['cloudflare.country']).toBe('US');
|
|
101
|
+
expect(attrs['cloudflare.city']).toBe('San Jose');
|
|
102
|
+
expect(attrs['cloudflare.region']).toBe('California');
|
|
103
|
+
expect(attrs['cloudflare.continent']).toBe('NA');
|
|
104
|
+
expect(attrs['cloudflare.timezone']).toBe('America/Los_Angeles');
|
|
105
|
+
expect(attrs['cloudflare.latitude']).toBe('37.3382');
|
|
106
|
+
expect(attrs['cloudflare.longitude']).toBe('-121.8863');
|
|
107
|
+
expect(attrs['cloudflare.asn']).toBe(13_335);
|
|
108
|
+
expect(attrs['cloudflare.as_organization']).toBe('Cloudflare Inc');
|
|
109
|
+
expect(attrs['cloudflare.http_protocol']).toBe('HTTP/2');
|
|
110
|
+
expect(attrs['cloudflare.tls_version']).toBe('TLSv1.3');
|
|
111
|
+
expect(attrs['cloudflare.client_tcp_rtt']).toBe(5);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should extract CF ray_id from cf-ray header', async () => {
|
|
115
|
+
const handler = {
|
|
116
|
+
async fetch(request: Request) {
|
|
117
|
+
return new Response('OK', { status: 200 });
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const instrumented = instrument(handler, {
|
|
122
|
+
service: { name: 'test-worker' },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const request = new Request('http://example.com/test', {
|
|
126
|
+
headers: {
|
|
127
|
+
'cf-ray': '8a1b2c3d4e5f6-SJC',
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
Object.defineProperty(request, 'cf', {
|
|
131
|
+
value: { colo: 'SJC' },
|
|
132
|
+
writable: false,
|
|
133
|
+
enumerable: true,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const env = {} as any;
|
|
137
|
+
const ctx = createMockCtx();
|
|
138
|
+
|
|
139
|
+
await instrumented.fetch!(request, env, ctx);
|
|
140
|
+
|
|
141
|
+
expect(capturedSpanOptions).toBeDefined();
|
|
142
|
+
const attrs = capturedSpanOptions.attributes;
|
|
143
|
+
expect(attrs['cloudflare.ray_id']).toBe('8a1b2c3d4e5f6-SJC');
|
|
144
|
+
expect(attrs['cloudflare.colo']).toBe('SJC');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should not include CF attributes when request.cf is undefined', async () => {
|
|
148
|
+
const handler = {
|
|
149
|
+
async fetch(request: Request) {
|
|
150
|
+
return new Response('OK', { status: 200 });
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const instrumented = instrument(handler, {
|
|
155
|
+
service: { name: 'test-worker' },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Standard request without .cf (local dev / Miniflare scenario)
|
|
159
|
+
const request = new Request('http://example.com/test');
|
|
160
|
+
const env = {} as any;
|
|
161
|
+
const ctx = createMockCtx();
|
|
162
|
+
|
|
163
|
+
await instrumented.fetch!(request, env, ctx);
|
|
164
|
+
|
|
165
|
+
expect(capturedSpanOptions).toBeDefined();
|
|
166
|
+
const attrs = capturedSpanOptions.attributes;
|
|
167
|
+
|
|
168
|
+
// Standard HTTP attributes should still be present
|
|
169
|
+
expect(attrs['http.request.method']).toBe('GET');
|
|
170
|
+
expect(attrs['url.full']).toBe('http://example.com/test');
|
|
171
|
+
|
|
172
|
+
// No cloudflare.* attributes should be present
|
|
173
|
+
const cfKeys = Object.keys(attrs).filter(k => k.startsWith('cloudflare.'));
|
|
174
|
+
expect(cfKeys).toHaveLength(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should correctly map all CF fields to cloudflare.* attribute names', async () => {
|
|
178
|
+
const handler = {
|
|
179
|
+
async fetch(request: Request) {
|
|
180
|
+
return new Response('OK', { status: 200 });
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const instrumented = instrument(handler, {
|
|
185
|
+
service: { name: 'test-worker' },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const request = new Request('http://example.com/test', {
|
|
189
|
+
headers: { 'cf-ray': 'abc123-LAX' },
|
|
190
|
+
});
|
|
191
|
+
Object.defineProperty(request, 'cf', {
|
|
192
|
+
value: {
|
|
193
|
+
colo: 'LAX',
|
|
194
|
+
country: 'US',
|
|
195
|
+
city: 'Los Angeles',
|
|
196
|
+
region: 'California',
|
|
197
|
+
continent: 'NA',
|
|
198
|
+
timezone: 'America/Los_Angeles',
|
|
199
|
+
latitude: '34.0522',
|
|
200
|
+
longitude: '-118.2437',
|
|
201
|
+
asn: 13_335,
|
|
202
|
+
asOrganization: 'Cloudflare Inc',
|
|
203
|
+
httpProtocol: 'HTTP/3',
|
|
204
|
+
tlsVersion: 'TLSv1.3',
|
|
205
|
+
clientTcpRtt: 10,
|
|
206
|
+
},
|
|
207
|
+
writable: false,
|
|
208
|
+
enumerable: true,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const env = {} as any;
|
|
212
|
+
const ctx = createMockCtx();
|
|
213
|
+
|
|
214
|
+
await instrumented.fetch!(request, env, ctx);
|
|
215
|
+
|
|
216
|
+
const attrs = capturedSpanOptions.attributes;
|
|
217
|
+
|
|
218
|
+
// Verify the exact mapping from CF property names to attribute names
|
|
219
|
+
const expectedMappings: Record<string, [string, any]> = {
|
|
220
|
+
'cloudflare.colo': ['colo', 'LAX'],
|
|
221
|
+
'cloudflare.ray_id': ['cf-ray header', 'abc123-LAX'],
|
|
222
|
+
'cloudflare.country': ['country', 'US'],
|
|
223
|
+
'cloudflare.city': ['city', 'Los Angeles'],
|
|
224
|
+
'cloudflare.region': ['region', 'California'],
|
|
225
|
+
'cloudflare.continent': ['continent', 'NA'],
|
|
226
|
+
'cloudflare.timezone': ['timezone', 'America/Los_Angeles'],
|
|
227
|
+
'cloudflare.latitude': ['latitude', '34.0522'],
|
|
228
|
+
'cloudflare.longitude': ['longitude', '-118.2437'],
|
|
229
|
+
'cloudflare.asn': ['asn', 13_335],
|
|
230
|
+
'cloudflare.as_organization': ['asOrganization', 'Cloudflare Inc'],
|
|
231
|
+
'cloudflare.http_protocol': ['httpProtocol', 'HTTP/3'],
|
|
232
|
+
'cloudflare.tls_version': ['tlsVersion', 'TLSv1.3'],
|
|
233
|
+
'cloudflare.client_tcp_rtt': ['clientTcpRtt', 10],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
for (const [attrKey, [, expectedValue]] of Object.entries(expectedMappings)) {
|
|
237
|
+
expect(attrs[attrKey]).toBe(expectedValue);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('preserves valid falsy numeric CF attributes (0 values)', async () => {
|
|
242
|
+
const handler = {
|
|
243
|
+
async fetch(_request: Request) {
|
|
244
|
+
return new Response('OK', { status: 200 });
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const instrumented = instrument(handler, {
|
|
249
|
+
service: { name: 'test-worker' },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const request = new Request('http://example.com/test');
|
|
253
|
+
Object.defineProperty(request, 'cf', {
|
|
254
|
+
value: {
|
|
255
|
+
latitude: 0,
|
|
256
|
+
longitude: 0,
|
|
257
|
+
asn: 0,
|
|
258
|
+
clientTcpRtt: 0,
|
|
259
|
+
},
|
|
260
|
+
writable: false,
|
|
261
|
+
enumerable: true,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const env = {} as any;
|
|
265
|
+
const ctx = createMockCtx();
|
|
266
|
+
|
|
267
|
+
await instrumented.fetch!(request, env, ctx);
|
|
268
|
+
|
|
269
|
+
const attrs = capturedSpanOptions.attributes;
|
|
270
|
+
expect(attrs['cloudflare.latitude']).toBe(0);
|
|
271
|
+
expect(attrs['cloudflare.longitude']).toBe(0);
|
|
272
|
+
expect(attrs['cloudflare.asn']).toBe(0);
|
|
273
|
+
expect(attrs['cloudflare.client_tcp_rtt']).toBe(0);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -75,6 +75,39 @@ type EmailHandler = (
|
|
|
75
75
|
/**
|
|
76
76
|
* Create fetch handler instrumentation with config support for postProcess
|
|
77
77
|
*/
|
|
78
|
+
/**
|
|
79
|
+
* Extract Cloudflare-specific attributes from a request
|
|
80
|
+
*/
|
|
81
|
+
function extractCfAttributes(request: Request): Record<string, string | number | boolean> {
|
|
82
|
+
const cf = (request as any).cf;
|
|
83
|
+
if (!cf) return {};
|
|
84
|
+
|
|
85
|
+
const attrs: Record<string, string | number | boolean> = {};
|
|
86
|
+
const set = (key: string, value: unknown) => {
|
|
87
|
+
if (value !== undefined && value !== null) {
|
|
88
|
+
attrs[key] = value as string | number | boolean;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
set('cloudflare.colo', cf.colo);
|
|
93
|
+
const ray = request.headers.get('cf-ray');
|
|
94
|
+
if (ray) attrs['cloudflare.ray_id'] = ray;
|
|
95
|
+
set('cloudflare.country', cf.country);
|
|
96
|
+
set('cloudflare.city', cf.city);
|
|
97
|
+
set('cloudflare.region', cf.region);
|
|
98
|
+
set('cloudflare.continent', cf.continent);
|
|
99
|
+
set('cloudflare.timezone', cf.timezone);
|
|
100
|
+
set('cloudflare.latitude', cf.latitude);
|
|
101
|
+
set('cloudflare.longitude', cf.longitude);
|
|
102
|
+
set('cloudflare.asn', cf.asn);
|
|
103
|
+
set('cloudflare.as_organization', cf.asOrganization);
|
|
104
|
+
set('cloudflare.http_protocol', cf.httpProtocol);
|
|
105
|
+
set('cloudflare.tls_version', cf.tlsVersion);
|
|
106
|
+
set('cloudflare.client_tcp_rtt', cf.clientTcpRtt);
|
|
107
|
+
|
|
108
|
+
return attrs;
|
|
109
|
+
}
|
|
110
|
+
|
|
78
111
|
function createFetchInstrumentation(
|
|
79
112
|
config: ResolvedEdgeConfig,
|
|
80
113
|
): HandlerInstrumentation<Request, Response> {
|
|
@@ -82,6 +115,10 @@ function createFetchInstrumentation(
|
|
|
82
115
|
getInitialSpanInfo: (request: Request): InitialSpanInfo => {
|
|
83
116
|
const url = new URL(request.url);
|
|
84
117
|
|
|
118
|
+
const cfAttrs = (config as any).extractCfAttributes === false
|
|
119
|
+
? {}
|
|
120
|
+
: extractCfAttributes(request);
|
|
121
|
+
|
|
85
122
|
return {
|
|
86
123
|
name: `${request.method} ${url.pathname}`,
|
|
87
124
|
options: {
|
|
@@ -89,6 +126,7 @@ function createFetchInstrumentation(
|
|
|
89
126
|
attributes: {
|
|
90
127
|
'http.request.method': request.method,
|
|
91
128
|
'url.full': request.url,
|
|
129
|
+
...cfAttrs,
|
|
92
130
|
},
|
|
93
131
|
},
|
|
94
132
|
context: propagation.extract(api_context.active(), request.headers),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bindings/common.ts"],"names":[],"mappings":";AAOO,IAAM,iBAAN,MAAqB;AAAA,EAClB,WAA+B,EAAC;AAAA,EAExC,MAAM,OAAA,EAAiC;AACrC,IAAA,IAAA,CAAK,QAAA,CAAS,KAAK,OAAO,CAAA;AAAA,EAC5B;AAAA,EAEA,MAAM,IAAA,GAAsB;AAC1B,IAAA,MAAM,OAAA,CAAQ,UAAA,CAAW,IAAA,CAAK,QAAQ,CAAA;AAAA,EACxC;AACF,CAAA;AAKO,SAAS,sBAAsB,GAAA,EAGpC;AACA,EAAA,MAAM,OAAA,GAAU,IAAI,cAAA,EAAe;AAEnC,EAAA,MAAM,OAAA,GAAU,IAAI,KAAA,CAAM,GAAA,EAAK;AAAA,IAC7B,GAAA,CAAI,QAAQ,IAAA,EAAM;AAChB,MAAA,IAAI,SAAS,WAAA,EAAa;AACxB,QAAA,OAAO,CAAC,OAAA,KAA8B;AACpC,UAAA,OAAA,CAAQ,MAAM,OAAO,CAAA;AACrB,UAAA,OAAO,MAAA,CAAO,UAAU,OAAO,CAAA;AAAA,QACjC,CAAA;AAAA,MACF;AACA,MAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,MAAA,EAAQ,IAAI,CAAA;AAAA,IACjC;AAAA,GACD,CAAA;AAED,EAAA,OAAO,EAAE,GAAA,EAAK,OAAA,EAAS,OAAA,EAAQ;AACjC;AAKA,IAAM,YAAA,0BAAsB,QAAQ,CAAA;AAI7B,SAAS,UAAa,IAAA,EAA6B;AACxD,EAAA,OAAO,IAAA,IAAQ,CAAC,CAAE,IAAA,CAAoB,YAAY,CAAA;AACpD;AAEO,SAAS,OAAyB,IAAA,EAAY;AACnD,EAAA,IAAI,IAAA,IAAQ,SAAA,CAAU,IAAI,CAAA,EAAG;AAC3B,IAAA,OAAO,KAAK,YAAY,CAAA;AAAA,EAC1B,CAAA,MAAO;AACL,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAEO,SAAS,IAAA,CACd,MACA,OAAA,EACY;AACZ,EAAA,MAAM,KAAA,GAAQ,IAAI,KAAA,CAAM,IAAA,EAAM,OAAO,CAAA;AACrC,EAAA,MAAA,CAAO,cAAA,CAAe,OAAO,YAAA,EAAc;AAAA,IACzC,KAAA,EAAO,IAAA;AAAA,IACP,QAAA,EAAU,KAAA;AAAA,IACV,UAAA,EAAY,KAAA;AAAA,IACZ,YAAA,EAAc;AAAA,GACf,CAAA;AACD,EAAA,OAAO,KAAA;AACT","file":"chunk-5NL62W4L.js","sourcesContent":["/**\n * Common instrumentation utilities\n */\n\n/**\n * Promise tracker for waitUntil\n */\nexport class PromiseTracker {\n private promises: Promise<unknown>[] = [];\n\n track(promise: Promise<unknown>): void {\n this.promises.push(promise);\n }\n\n async wait(): Promise<void> {\n await Promise.allSettled(this.promises);\n }\n}\n\n/**\n * Proxy ExecutionContext to track waitUntil promises\n */\nexport function proxyExecutionContext(ctx: ExecutionContext): {\n ctx: ExecutionContext;\n tracker: PromiseTracker;\n} {\n const tracker = new PromiseTracker();\n\n const proxied = new Proxy(ctx, {\n get(target, prop) {\n if (prop === 'waitUntil') {\n return (promise: Promise<unknown>) => {\n tracker.track(promise);\n return target.waitUntil(promise);\n };\n }\n return Reflect.get(target, prop);\n },\n });\n\n return { ctx: proxied, tracker };\n}\n\n/**\n * Helper to wrap/unwrap proxied objects\n */\nconst unwrapSymbol = Symbol('unwrap');\n\ntype Wrapped<T> = { [unwrapSymbol]: T } & T;\n\nexport function isWrapped<T>(item: T): item is Wrapped<T> {\n return item && !!(item as Wrapped<T>)[unwrapSymbol];\n}\n\nexport function unwrap<T extends object>(item: T): T {\n if (item && isWrapped(item)) {\n return item[unwrapSymbol];\n } else {\n return item;\n }\n}\n\nexport function wrap<T extends object>(\n item: T,\n handler: ProxyHandler<T>,\n): Wrapped<T> {\n const proxy = new Proxy(item, handler) as Wrapped<T>;\n Object.defineProperty(proxy, unwrapSymbol, {\n value: item,\n writable: false,\n enumerable: false,\n configurable: false,\n });\n return proxy;\n}\n"]}
|