@littlebearapps/platform-consumer-sdk 1.0.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/README.md +306 -0
- package/package.json +53 -0
- package/src/ai-gateway.ts +305 -0
- package/src/constants.ts +147 -0
- package/src/costs.ts +590 -0
- package/src/do-heartbeat.ts +249 -0
- package/src/dynamic-patterns.ts +273 -0
- package/src/errors.ts +285 -0
- package/src/features.ts +149 -0
- package/src/heartbeat.ts +27 -0
- package/src/index.ts +950 -0
- package/src/logging.ts +543 -0
- package/src/middleware.ts +447 -0
- package/src/patterns.ts +156 -0
- package/src/proxy.ts +732 -0
- package/src/retry.ts +19 -0
- package/src/service-client.ts +291 -0
- package/src/telemetry.ts +342 -0
- package/src/timeout.ts +212 -0
- package/src/tracing.ts +403 -0
- package/src/types.ts +465 -0
package/src/proxy.ts
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Platform SDK Proxy
|
|
5
|
+
*
|
|
6
|
+
* Proxy handlers for automatic metric collection on Cloudflare bindings.
|
|
7
|
+
* Supports D1, KV, AI, and Vectorize with lazy/JIT circuit breaker checks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { MetricsAccumulator } from './types';
|
|
11
|
+
import { getTelemetryContext } from './telemetry';
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// TYPE GUARDS
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Type guard to check if a value is a D1 database.
|
|
19
|
+
*/
|
|
20
|
+
export function isD1Database(value: unknown): value is D1Database {
|
|
21
|
+
return (
|
|
22
|
+
value !== null &&
|
|
23
|
+
typeof value === 'object' &&
|
|
24
|
+
'prepare' in value &&
|
|
25
|
+
'batch' in value &&
|
|
26
|
+
'exec' in value
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Type guard to check if a value is a KV namespace.
|
|
32
|
+
*/
|
|
33
|
+
export function isKVNamespace(value: unknown): value is KVNamespace {
|
|
34
|
+
return (
|
|
35
|
+
value !== null &&
|
|
36
|
+
typeof value === 'object' &&
|
|
37
|
+
'get' in value &&
|
|
38
|
+
'put' in value &&
|
|
39
|
+
'delete' in value &&
|
|
40
|
+
'list' in value &&
|
|
41
|
+
!('prepare' in value) && // Distinguish from D1
|
|
42
|
+
!('head' in value) // Distinguish from R2
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Type guard to check if a value is a Workers AI binding.
|
|
48
|
+
* AI bindings have a 'run' method.
|
|
49
|
+
*/
|
|
50
|
+
export function isAIBinding(value: unknown): value is Ai {
|
|
51
|
+
return (
|
|
52
|
+
value !== null &&
|
|
53
|
+
typeof value === 'object' &&
|
|
54
|
+
'run' in value &&
|
|
55
|
+
typeof (value as { run: unknown }).run === 'function'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Type guard to check if a value is a Vectorize index.
|
|
61
|
+
* Vectorize has query, insert, upsert, deleteByIds methods.
|
|
62
|
+
*/
|
|
63
|
+
export function isVectorizeIndex(value: unknown): value is VectorizeIndex {
|
|
64
|
+
return (
|
|
65
|
+
value !== null &&
|
|
66
|
+
typeof value === 'object' &&
|
|
67
|
+
'query' in value &&
|
|
68
|
+
'insert' in value &&
|
|
69
|
+
'upsert' in value &&
|
|
70
|
+
'deleteByIds' in value
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Type guard to check if a value is a Queue.
|
|
76
|
+
*/
|
|
77
|
+
export function isQueue(value: unknown): value is Queue {
|
|
78
|
+
return value !== null && typeof value === 'object' && 'send' in value && 'sendBatch' in value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Type guard to check if a value is a Durable Object namespace.
|
|
83
|
+
*/
|
|
84
|
+
export function isDurableObjectNamespace(value: unknown): value is DurableObjectNamespace {
|
|
85
|
+
return (
|
|
86
|
+
value !== null &&
|
|
87
|
+
typeof value === 'object' &&
|
|
88
|
+
'get' in value &&
|
|
89
|
+
'idFromName' in value &&
|
|
90
|
+
'idFromString' in value
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Type guard to check if a value is an R2 bucket.
|
|
96
|
+
*/
|
|
97
|
+
export function isR2Bucket(value: unknown): value is R2Bucket {
|
|
98
|
+
return (
|
|
99
|
+
value !== null &&
|
|
100
|
+
typeof value === 'object' &&
|
|
101
|
+
'put' in value &&
|
|
102
|
+
'get' in value &&
|
|
103
|
+
'head' in value &&
|
|
104
|
+
'list' in value &&
|
|
105
|
+
'delete' in value &&
|
|
106
|
+
'createMultipartUpload' in value
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Type guard to check if a value is a Workflow binding.
|
|
112
|
+
* Workflow bindings have get, create, and createBatch methods.
|
|
113
|
+
*/
|
|
114
|
+
export function isWorkflow(value: unknown): value is Workflow {
|
|
115
|
+
return (
|
|
116
|
+
value !== null &&
|
|
117
|
+
typeof value === 'object' &&
|
|
118
|
+
'get' in value &&
|
|
119
|
+
'create' in value &&
|
|
120
|
+
'createBatch' in value &&
|
|
121
|
+
!('idFromName' in value) // Distinguish from DurableObjectNamespace
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// D1 PROXY
|
|
127
|
+
// =============================================================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create a proxied D1 database that tracks reads and writes.
|
|
131
|
+
*/
|
|
132
|
+
export function createD1Proxy(db: D1Database, metrics: MetricsAccumulator): D1Database {
|
|
133
|
+
return new Proxy(db, {
|
|
134
|
+
get(target, prop) {
|
|
135
|
+
if (prop === 'prepare') {
|
|
136
|
+
return (query: string) => {
|
|
137
|
+
const stmt = target.prepare(query);
|
|
138
|
+
return createD1StatementProxy(stmt, metrics);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (prop === 'batch') {
|
|
143
|
+
return async (statements: D1PreparedStatement[]) => {
|
|
144
|
+
const results = await target.batch(statements);
|
|
145
|
+
// Count all batch operations from meta
|
|
146
|
+
for (const result of results) {
|
|
147
|
+
if (result.meta) {
|
|
148
|
+
metrics.d1Writes += result.meta.changes ?? 0;
|
|
149
|
+
metrics.d1RowsWritten += result.meta.changes ?? 0;
|
|
150
|
+
metrics.d1Reads += result.meta.rows_read ?? 0;
|
|
151
|
+
metrics.d1RowsRead += result.meta.rows_read ?? 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return results;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (prop === 'dump') {
|
|
159
|
+
return async () => {
|
|
160
|
+
metrics.d1Reads += 1;
|
|
161
|
+
return target.dump();
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return Reflect.get(target, prop);
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a proxied D1 prepared statement that tracks statement runs.
|
|
172
|
+
* Handles bind() chaining correctly.
|
|
173
|
+
*/
|
|
174
|
+
function createD1StatementProxy(
|
|
175
|
+
stmt: D1PreparedStatement,
|
|
176
|
+
metrics: MetricsAccumulator
|
|
177
|
+
): D1PreparedStatement {
|
|
178
|
+
return new Proxy(stmt, {
|
|
179
|
+
get(target, prop) {
|
|
180
|
+
// Handle bind() - returns a new statement that also needs proxying
|
|
181
|
+
if (prop === 'bind') {
|
|
182
|
+
return (...values: unknown[]) => {
|
|
183
|
+
const boundStmt = target.bind(...values);
|
|
184
|
+
return createD1StatementProxy(boundStmt, metrics);
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Handle statement runs
|
|
189
|
+
if (prop === 'run' || prop === 'all' || prop === 'first' || prop === 'raw') {
|
|
190
|
+
return async (...args: unknown[]) => {
|
|
191
|
+
const method = target[prop as keyof D1PreparedStatement] as (
|
|
192
|
+
...args: unknown[]
|
|
193
|
+
) => Promise<D1Result<unknown>>;
|
|
194
|
+
const result = await method.apply(target, args);
|
|
195
|
+
|
|
196
|
+
// Track based on result meta
|
|
197
|
+
if (result && typeof result === 'object' && 'meta' in result) {
|
|
198
|
+
const meta = (result as D1Result<unknown>).meta;
|
|
199
|
+
if (meta) {
|
|
200
|
+
metrics.d1Writes += meta.changes ?? 0;
|
|
201
|
+
metrics.d1RowsWritten += meta.changes ?? 0;
|
|
202
|
+
metrics.d1Reads += meta.rows_read ?? 0;
|
|
203
|
+
metrics.d1RowsRead += meta.rows_read ?? 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return result;
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return Reflect.get(target, prop);
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// =============================================================================
|
|
217
|
+
// KV PROXY
|
|
218
|
+
// =============================================================================
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Create a proxied KV namespace that tracks reads, writes, deletes, and lists.
|
|
222
|
+
*/
|
|
223
|
+
export function createKVProxy(kv: KVNamespace, metrics: MetricsAccumulator): KVNamespace {
|
|
224
|
+
return new Proxy(kv, {
|
|
225
|
+
get(target, prop) {
|
|
226
|
+
if (prop === 'get') {
|
|
227
|
+
return async (...args: Parameters<KVNamespace['get']>) => {
|
|
228
|
+
metrics.kvReads += 1;
|
|
229
|
+
return target.get(...args);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (prop === 'getWithMetadata') {
|
|
234
|
+
return async (...args: Parameters<KVNamespace['getWithMetadata']>) => {
|
|
235
|
+
metrics.kvReads += 1;
|
|
236
|
+
return target.getWithMetadata(...args);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (prop === 'put') {
|
|
241
|
+
return async (...args: Parameters<KVNamespace['put']>) => {
|
|
242
|
+
metrics.kvWrites += 1;
|
|
243
|
+
return target.put(...args);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (prop === 'delete') {
|
|
248
|
+
return async (...args: Parameters<KVNamespace['delete']>) => {
|
|
249
|
+
metrics.kvDeletes += 1;
|
|
250
|
+
return target.delete(...args);
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (prop === 'list') {
|
|
255
|
+
return async (...args: Parameters<KVNamespace['list']>) => {
|
|
256
|
+
metrics.kvLists += 1;
|
|
257
|
+
return target.list(...args);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return Reflect.get(target, prop);
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// =============================================================================
|
|
267
|
+
// AI PROXY
|
|
268
|
+
// =============================================================================
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Create a proxied AI binding that tracks run() calls.
|
|
272
|
+
* Tracks both total request count and per-model breakdown.
|
|
273
|
+
*/
|
|
274
|
+
export function createAIProxy(ai: Ai, metrics: MetricsAccumulator): Ai {
|
|
275
|
+
return new Proxy(ai, {
|
|
276
|
+
get(target, prop) {
|
|
277
|
+
if (prop === 'run') {
|
|
278
|
+
return async (
|
|
279
|
+
model: string | { name: string; [key: string]: unknown },
|
|
280
|
+
inputs: unknown,
|
|
281
|
+
options?: unknown
|
|
282
|
+
) => {
|
|
283
|
+
// Extract model name from string or object
|
|
284
|
+
const modelName = typeof model === 'string' ? model : model.name;
|
|
285
|
+
|
|
286
|
+
// Track total AI requests
|
|
287
|
+
metrics.aiRequests += 1;
|
|
288
|
+
|
|
289
|
+
// Track per-model count
|
|
290
|
+
const currentCount = metrics.aiModelCounts.get(modelName) ?? 0;
|
|
291
|
+
metrics.aiModelCounts.set(modelName, currentCount + 1);
|
|
292
|
+
|
|
293
|
+
// Call the actual AI.run method
|
|
294
|
+
const result = await target.run(
|
|
295
|
+
model as Parameters<Ai['run']>[0],
|
|
296
|
+
inputs as Parameters<Ai['run']>[1],
|
|
297
|
+
options as Parameters<Ai['run']>[2]
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Note: We can't easily track neurons here as they're returned
|
|
301
|
+
// in usage metadata that varies by model. The consumer can
|
|
302
|
+
// derive this from the model + input size.
|
|
303
|
+
|
|
304
|
+
return result;
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return Reflect.get(target, prop);
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// =============================================================================
|
|
314
|
+
// VECTORIZE PROXY
|
|
315
|
+
// =============================================================================
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Create a proxied Vectorize index that tracks queries, inserts, and deletes.
|
|
319
|
+
*/
|
|
320
|
+
export function createVectorizeProxy(
|
|
321
|
+
index: VectorizeIndex,
|
|
322
|
+
metrics: MetricsAccumulator
|
|
323
|
+
): VectorizeIndex {
|
|
324
|
+
return new Proxy(index, {
|
|
325
|
+
get(target, prop) {
|
|
326
|
+
if (prop === 'query') {
|
|
327
|
+
return async (...args: Parameters<VectorizeIndex['query']>) => {
|
|
328
|
+
metrics.vectorizeQueries += 1;
|
|
329
|
+
return target.query(...args);
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (prop === 'insert') {
|
|
334
|
+
return async (vectors: VectorizeVector[]) => {
|
|
335
|
+
metrics.vectorizeInserts += vectors.length;
|
|
336
|
+
return target.insert(vectors);
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (prop === 'upsert') {
|
|
341
|
+
return async (vectors: VectorizeVector[]) => {
|
|
342
|
+
metrics.vectorizeInserts += vectors.length;
|
|
343
|
+
return target.upsert(vectors);
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (prop === 'deleteByIds') {
|
|
348
|
+
return async (ids: string[]) => {
|
|
349
|
+
// vectorizeDeletes removed - Analytics Engine 20 double limit
|
|
350
|
+
// Deletes still work, just not tracked in telemetry
|
|
351
|
+
return target.deleteByIds(ids);
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (prop === 'getByIds') {
|
|
356
|
+
return async (...args: Parameters<VectorizeIndex['getByIds']>) => {
|
|
357
|
+
metrics.vectorizeQueries += 1;
|
|
358
|
+
return target.getByIds(...args);
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (prop === 'describe') {
|
|
363
|
+
return async () => {
|
|
364
|
+
// describe() is a read operation but doesn't query vectors
|
|
365
|
+
return target.describe();
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return Reflect.get(target, prop);
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// =============================================================================
|
|
375
|
+
// R2 PROXY
|
|
376
|
+
// =============================================================================
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Create a proxied R2MultipartUpload that tracks uploadPart/complete/abort.
|
|
380
|
+
*/
|
|
381
|
+
function createR2MultipartUploadProxy(
|
|
382
|
+
upload: R2MultipartUpload,
|
|
383
|
+
metrics: MetricsAccumulator
|
|
384
|
+
): R2MultipartUpload {
|
|
385
|
+
return new Proxy(upload, {
|
|
386
|
+
get(target, prop) {
|
|
387
|
+
if (prop === 'uploadPart') {
|
|
388
|
+
return async (
|
|
389
|
+
partNumber: number,
|
|
390
|
+
value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob
|
|
391
|
+
) => {
|
|
392
|
+
metrics.r2ClassA += 1;
|
|
393
|
+
return target.uploadPart(partNumber, value);
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (prop === 'complete') {
|
|
398
|
+
return async (uploadedParts: R2UploadedPart[]) => {
|
|
399
|
+
metrics.r2ClassA += 1;
|
|
400
|
+
return target.complete(uploadedParts);
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (prop === 'abort') {
|
|
405
|
+
return async () => {
|
|
406
|
+
metrics.r2ClassA += 1;
|
|
407
|
+
return target.abort();
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return Reflect.get(target, prop);
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Create a proxied R2 bucket that tracks Class A and Class B operations.
|
|
418
|
+
* Class A: list, put, delete, createMultipartUpload, resumeMultipartUpload
|
|
419
|
+
* Class B: head, get
|
|
420
|
+
*
|
|
421
|
+
* IMPORTANT: get() does NOT consume the stream body - just logs the call.
|
|
422
|
+
*/
|
|
423
|
+
export function createR2Proxy(bucket: R2Bucket, metrics: MetricsAccumulator): R2Bucket {
|
|
424
|
+
return new Proxy(bucket, {
|
|
425
|
+
get(target, prop) {
|
|
426
|
+
// Class B: head
|
|
427
|
+
if (prop === 'head') {
|
|
428
|
+
return async (key: string) => {
|
|
429
|
+
metrics.r2ClassB += 1;
|
|
430
|
+
return target.head(key);
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Class B: get (don't consume stream)
|
|
435
|
+
if (prop === 'get') {
|
|
436
|
+
return async (key: string, options?: R2GetOptions) => {
|
|
437
|
+
metrics.r2ClassB += 1;
|
|
438
|
+
return target.get(key, options);
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Class A: put
|
|
443
|
+
if (prop === 'put') {
|
|
444
|
+
return async (
|
|
445
|
+
key: string,
|
|
446
|
+
value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob,
|
|
447
|
+
options?: R2PutOptions
|
|
448
|
+
) => {
|
|
449
|
+
metrics.r2ClassA += 1;
|
|
450
|
+
return target.put(key, value, options);
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Class A: delete
|
|
455
|
+
if (prop === 'delete') {
|
|
456
|
+
return async (keys: string | string[]) => {
|
|
457
|
+
metrics.r2ClassA += 1;
|
|
458
|
+
return target.delete(keys);
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Class A: list
|
|
463
|
+
if (prop === 'list') {
|
|
464
|
+
return async (options?: R2ListOptions) => {
|
|
465
|
+
metrics.r2ClassA += 1;
|
|
466
|
+
return target.list(options);
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Class A: createMultipartUpload
|
|
471
|
+
if (prop === 'createMultipartUpload') {
|
|
472
|
+
return async (key: string, options?: R2MultipartOptions) => {
|
|
473
|
+
metrics.r2ClassA += 1;
|
|
474
|
+
const upload = await target.createMultipartUpload(key, options);
|
|
475
|
+
return createR2MultipartUploadProxy(upload, metrics);
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Class A: resumeMultipartUpload (sync)
|
|
480
|
+
if (prop === 'resumeMultipartUpload') {
|
|
481
|
+
return (key: string, uploadId: string) => {
|
|
482
|
+
metrics.r2ClassA += 1;
|
|
483
|
+
const upload = target.resumeMultipartUpload(key, uploadId);
|
|
484
|
+
return createR2MultipartUploadProxy(upload, metrics);
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return Reflect.get(target, prop);
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// =============================================================================
|
|
494
|
+
// QUEUE PROXY
|
|
495
|
+
// =============================================================================
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Create a proxied Queue that tracks message sends.
|
|
499
|
+
* send() = 1 message, sendBatch() = N messages.
|
|
500
|
+
*/
|
|
501
|
+
export function createQueueProxy<T = unknown>(
|
|
502
|
+
queue: Queue<T>,
|
|
503
|
+
metrics: MetricsAccumulator
|
|
504
|
+
): Queue<T> {
|
|
505
|
+
return new Proxy(queue, {
|
|
506
|
+
get(target, prop) {
|
|
507
|
+
if (prop === 'send') {
|
|
508
|
+
return async (message: T, options?: QueueSendOptions) => {
|
|
509
|
+
metrics.queueMessages += 1;
|
|
510
|
+
return target.send(message, options);
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (prop === 'sendBatch') {
|
|
515
|
+
return async (
|
|
516
|
+
messages: Iterable<MessageSendRequest<T>>,
|
|
517
|
+
options?: QueueSendBatchOptions
|
|
518
|
+
) => {
|
|
519
|
+
const messageArray = Array.isArray(messages) ? messages : Array.from(messages);
|
|
520
|
+
metrics.queueMessages += messageArray.length;
|
|
521
|
+
return target.sendBatch(messageArray, options);
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return Reflect.get(target, prop);
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// =============================================================================
|
|
531
|
+
// WORKFLOW PROXY
|
|
532
|
+
// =============================================================================
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Create a proxied Workflow that tracks invocations.
|
|
536
|
+
* create() = 1 invocation, createBatch() = N invocations.
|
|
537
|
+
* get() is read-only and not tracked.
|
|
538
|
+
*/
|
|
539
|
+
export function createWorkflowProxy<PARAMS = unknown>(
|
|
540
|
+
workflow: Workflow<PARAMS>,
|
|
541
|
+
metrics: MetricsAccumulator
|
|
542
|
+
): Workflow<PARAMS> {
|
|
543
|
+
return new Proxy(workflow, {
|
|
544
|
+
get(target, prop) {
|
|
545
|
+
if (prop === 'create') {
|
|
546
|
+
return async (options?: WorkflowInstanceCreateOptions<PARAMS>) => {
|
|
547
|
+
metrics.workflowInvocations += 1;
|
|
548
|
+
return target.create(options);
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (prop === 'createBatch') {
|
|
553
|
+
return async (batch: WorkflowInstanceCreateOptions<PARAMS>[]) => {
|
|
554
|
+
metrics.workflowInvocations += batch.length;
|
|
555
|
+
return target.createBatch(batch);
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// get() is read-only, pass through
|
|
560
|
+
return Reflect.get(target, prop);
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// =============================================================================
|
|
566
|
+
// DURABLE OBJECTS PROXY
|
|
567
|
+
// =============================================================================
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Create a proxied Durable Object stub that tracks fetch() calls.
|
|
571
|
+
* This is the second level (returned by namespace.get()).
|
|
572
|
+
*/
|
|
573
|
+
function createDOStubProxy(
|
|
574
|
+
stub: DurableObjectStub,
|
|
575
|
+
metrics: MetricsAccumulator
|
|
576
|
+
): DurableObjectStub {
|
|
577
|
+
return new Proxy(stub, {
|
|
578
|
+
get(target, prop) {
|
|
579
|
+
if (prop === 'fetch') {
|
|
580
|
+
return async (...args: Parameters<DurableObjectStub['fetch']>) => {
|
|
581
|
+
const startTime = performance.now();
|
|
582
|
+
metrics.doRequests += 1;
|
|
583
|
+
try {
|
|
584
|
+
return await target.fetch(...args);
|
|
585
|
+
} finally {
|
|
586
|
+
const latencyMs = performance.now() - startTime;
|
|
587
|
+
metrics.doLatencyMs.push(latencyMs);
|
|
588
|
+
metrics.doTotalLatencyMs += latencyMs;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Pass through: id, name
|
|
594
|
+
return Reflect.get(target, prop);
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Create a proxied Durable Object namespace.
|
|
601
|
+
* get(id) returns a wrapped stub that tracks fetch() calls.
|
|
602
|
+
* ID creation methods (idFromName, idFromString, newUniqueId) pass through.
|
|
603
|
+
*/
|
|
604
|
+
export function createDOProxy(
|
|
605
|
+
ns: DurableObjectNamespace,
|
|
606
|
+
metrics: MetricsAccumulator
|
|
607
|
+
): DurableObjectNamespace {
|
|
608
|
+
return new Proxy(ns, {
|
|
609
|
+
get(target, prop) {
|
|
610
|
+
if (prop === 'get') {
|
|
611
|
+
return (id: DurableObjectId) => {
|
|
612
|
+
const stub = target.get(id);
|
|
613
|
+
return createDOStubProxy(stub, metrics);
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Pass through ID methods
|
|
618
|
+
return Reflect.get(target, prop);
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// =============================================================================
|
|
624
|
+
// ENVIRONMENT PROXY
|
|
625
|
+
// =============================================================================
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Reserved binding names that should NOT be proxied.
|
|
629
|
+
* These are used by the SDK itself for control/telemetry.
|
|
630
|
+
*/
|
|
631
|
+
const RESERVED_BINDINGS = new Set(['PLATFORM_CACHE', 'PLATFORM_TELEMETRY']);
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Create a proxied environment that wraps all known binding types.
|
|
635
|
+
* Bindings are wrapped lazily on first access.
|
|
636
|
+
*
|
|
637
|
+
* @param env - Original environment object
|
|
638
|
+
* @param metrics - Metrics accumulator to update
|
|
639
|
+
* @returns Proxied environment with tracked bindings
|
|
640
|
+
*/
|
|
641
|
+
export function createEnvProxy<T extends object>(env: T, metrics: MetricsAccumulator): T {
|
|
642
|
+
// Cache for wrapped bindings to avoid re-wrapping on each access
|
|
643
|
+
const wrappedBindings = new Map<string | symbol, unknown>();
|
|
644
|
+
|
|
645
|
+
return new Proxy(env, {
|
|
646
|
+
get(target, prop) {
|
|
647
|
+
// Return cached wrapped binding if available
|
|
648
|
+
if (wrappedBindings.has(prop)) {
|
|
649
|
+
return wrappedBindings.get(prop);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const value = Reflect.get(target, prop);
|
|
653
|
+
|
|
654
|
+
// Skip wrapping for reserved bindings
|
|
655
|
+
if (typeof prop === 'string' && RESERVED_BINDINGS.has(prop)) {
|
|
656
|
+
return value;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Wrap D1 databases
|
|
660
|
+
if (isD1Database(value)) {
|
|
661
|
+
const wrapped = createD1Proxy(value, metrics);
|
|
662
|
+
wrappedBindings.set(prop, wrapped);
|
|
663
|
+
return wrapped;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Wrap KV namespaces
|
|
667
|
+
if (isKVNamespace(value)) {
|
|
668
|
+
const wrapped = createKVProxy(value, metrics);
|
|
669
|
+
wrappedBindings.set(prop, wrapped);
|
|
670
|
+
return wrapped;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Wrap AI bindings
|
|
674
|
+
if (isAIBinding(value)) {
|
|
675
|
+
const wrapped = createAIProxy(value, metrics);
|
|
676
|
+
wrappedBindings.set(prop, wrapped);
|
|
677
|
+
return wrapped;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Wrap Vectorize indexes
|
|
681
|
+
if (isVectorizeIndex(value)) {
|
|
682
|
+
const wrapped = createVectorizeProxy(value, metrics);
|
|
683
|
+
wrappedBindings.set(prop, wrapped);
|
|
684
|
+
return wrapped;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Wrap R2 buckets
|
|
688
|
+
if (isR2Bucket(value)) {
|
|
689
|
+
const wrapped = createR2Proxy(value, metrics);
|
|
690
|
+
wrappedBindings.set(prop, wrapped);
|
|
691
|
+
return wrapped;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Wrap Queues (skip PLATFORM_TELEMETRY - handled by RESERVED_BINDINGS)
|
|
695
|
+
if (isQueue(value)) {
|
|
696
|
+
const wrapped = createQueueProxy(value, metrics);
|
|
697
|
+
wrappedBindings.set(prop, wrapped);
|
|
698
|
+
return wrapped;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Wrap Workflows
|
|
702
|
+
if (isWorkflow(value)) {
|
|
703
|
+
const wrapped = createWorkflowProxy(value, metrics);
|
|
704
|
+
wrappedBindings.set(prop, wrapped);
|
|
705
|
+
return wrapped;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Wrap Durable Object namespaces
|
|
709
|
+
if (isDurableObjectNamespace(value)) {
|
|
710
|
+
const wrapped = createDOProxy(value, metrics);
|
|
711
|
+
wrappedBindings.set(prop, wrapped);
|
|
712
|
+
return wrapped;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Return unwrapped value for other types
|
|
716
|
+
return value;
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// =============================================================================
|
|
722
|
+
// UTILITY FUNCTIONS
|
|
723
|
+
// =============================================================================
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Get the metrics accumulator for a proxied environment.
|
|
727
|
+
* Returns undefined if the environment is not being tracked.
|
|
728
|
+
*/
|
|
729
|
+
export function getMetrics(env: object): MetricsAccumulator | undefined {
|
|
730
|
+
const context = getTelemetryContext(env);
|
|
731
|
+
return context?.metrics;
|
|
732
|
+
}
|