@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/src/types.ts ADDED
@@ -0,0 +1,465 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ /**
4
+ * Platform SDK Types
5
+ *
6
+ * Shared type definitions for the Platform SDK.
7
+ */
8
+
9
+ // =============================================================================
10
+ // FEATURE IDENTIFICATION
11
+ // =============================================================================
12
+
13
+ /**
14
+ * Feature identifier string in format 'project:category:feature'
15
+ * Example: 'scout:ocr:process', 'brand-copilot:scanner:github'
16
+ */
17
+ export type FeatureId = string;
18
+
19
+ /**
20
+ * Parsed feature configuration from a FeatureId.
21
+ */
22
+ export interface FeatureConfig {
23
+ project: string;
24
+ category: string;
25
+ feature: string;
26
+ }
27
+
28
+ // =============================================================================
29
+ // METRICS
30
+ // =============================================================================
31
+
32
+ /**
33
+ * Resource metrics tracked per feature invocation.
34
+ * All fields are optional - only tracked resources are included.
35
+ */
36
+ export interface FeatureMetrics {
37
+ // D1 Database
38
+ d1Writes?: number;
39
+ d1Reads?: number;
40
+ d1RowsRead?: number;
41
+ d1RowsWritten?: number;
42
+
43
+ // KV Namespace
44
+ kvReads?: number;
45
+ kvWrites?: number;
46
+ kvDeletes?: number;
47
+ kvLists?: number;
48
+
49
+ // Workers AI
50
+ aiRequests?: number;
51
+ aiNeurons?: number;
52
+ aiModelBreakdown?: Record<string, number>; // model name → invocation count
53
+
54
+ // Vectorize (vectorizeDeletes removed - Analytics Engine 20 double limit)
55
+ vectorizeQueries?: number;
56
+ vectorizeInserts?: number;
57
+
58
+ // Durable Objects
59
+ doRequests?: number;
60
+ doGbSeconds?: number;
61
+ doAvgLatencyMs?: number;
62
+ doMaxLatencyMs?: number;
63
+ doP99LatencyMs?: number;
64
+
65
+ // R2
66
+ r2ClassA?: number;
67
+ r2ClassB?: number;
68
+
69
+ // Queue
70
+ queueMessages?: number;
71
+
72
+ // Workflow
73
+ workflowInvocations?: number;
74
+
75
+ // General
76
+ requests?: number;
77
+ cpuMs?: number;
78
+ }
79
+
80
+ /**
81
+ * Error category for classification in telemetry.
82
+ * Used for alerting priority and deduplication.
83
+ */
84
+ export type ErrorCategory =
85
+ | 'VALIDATION'
86
+ | 'NETWORK'
87
+ | 'CIRCUIT_BREAKER'
88
+ | 'INTERNAL'
89
+ | 'AUTH'
90
+ | 'RATE_LIMIT'
91
+ | 'D1_ERROR'
92
+ | 'KV_ERROR'
93
+ | 'QUEUE_ERROR'
94
+ | 'EXTERNAL_API'
95
+ | 'TIMEOUT';
96
+
97
+ /**
98
+ * Internal metrics accumulator used during request processing.
99
+ * Mutable version of FeatureMetrics for tracking.
100
+ */
101
+ export interface MetricsAccumulator {
102
+ d1Writes: number;
103
+ d1Reads: number;
104
+ d1RowsRead: number;
105
+ d1RowsWritten: number;
106
+ kvReads: number;
107
+ kvWrites: number;
108
+ kvDeletes: number;
109
+ kvLists: number;
110
+ aiRequests: number;
111
+ aiNeurons: number;
112
+ aiModelCounts: Map<string, number>; // mutable map for model name accumulation
113
+ vectorizeQueries: number;
114
+ vectorizeInserts: number;
115
+ // R2
116
+ r2ClassA: number;
117
+ r2ClassB: number;
118
+ // Queue
119
+ queueMessages: number;
120
+ // Durable Objects
121
+ doRequests: number;
122
+ doLatencyMs: number[]; // Array for percentile calculation
123
+ doTotalLatencyMs: number; // Sum for average
124
+ // Workflow
125
+ workflowInvocations: number;
126
+ // Error tracking
127
+ errorCount: number;
128
+ lastErrorCategory: ErrorCategory | null;
129
+ errorCodes: string[];
130
+ }
131
+
132
+ /**
133
+ * Create a fresh metrics accumulator with all values at zero.
134
+ */
135
+ export function createMetricsAccumulator(): MetricsAccumulator {
136
+ return {
137
+ d1Writes: 0,
138
+ d1Reads: 0,
139
+ d1RowsRead: 0,
140
+ d1RowsWritten: 0,
141
+ kvReads: 0,
142
+ kvWrites: 0,
143
+ kvDeletes: 0,
144
+ kvLists: 0,
145
+ aiRequests: 0,
146
+ aiNeurons: 0,
147
+ aiModelCounts: new Map<string, number>(),
148
+ vectorizeQueries: 0,
149
+ vectorizeInserts: 0,
150
+ // R2
151
+ r2ClassA: 0,
152
+ r2ClassB: 0,
153
+ // Queue
154
+ queueMessages: 0,
155
+ // Durable Objects
156
+ doRequests: 0,
157
+ doLatencyMs: [],
158
+ doTotalLatencyMs: 0,
159
+ // Workflow
160
+ workflowInvocations: 0,
161
+ // Error tracking
162
+ errorCount: 0,
163
+ lastErrorCategory: null,
164
+ errorCodes: [],
165
+ };
166
+ }
167
+
168
+ // =============================================================================
169
+ // TELEMETRY
170
+ // =============================================================================
171
+
172
+ /**
173
+ * Queue message format for telemetry.
174
+ * Sent to PLATFORM_TELEMETRY queue for processing by consumer.
175
+ */
176
+ export interface TelemetryMessage {
177
+ /** Feature key in format 'project:category:feature' */
178
+ feature_key: string;
179
+ /** Project name */
180
+ project: string;
181
+ /** Category within project */
182
+ category: string;
183
+ /** Specific feature */
184
+ feature: string;
185
+ /** Usage metrics for this invocation */
186
+ metrics: FeatureMetrics;
187
+ /** Unix timestamp in milliseconds */
188
+ timestamp: number;
189
+ /** True for health check probes (skips budget check, records in D1) */
190
+ is_heartbeat?: boolean;
191
+ /** Error count for this invocation */
192
+ error_count?: number;
193
+ /** Most recent error category */
194
+ error_category?: ErrorCategory;
195
+ /** Error codes encountered */
196
+ error_codes?: string[];
197
+ /** Correlation ID for request tracing */
198
+ correlation_id?: string;
199
+ /** Request wall-clock duration in milliseconds */
200
+ request_duration_ms?: number;
201
+ /** W3C Trace ID for distributed tracing */
202
+ trace_id?: string;
203
+ /** W3C Span ID for distributed tracing */
204
+ span_id?: string;
205
+ /** External API cost in USD (e.g., OpenAI, Apify) */
206
+ external_cost_usd?: number;
207
+ }
208
+
209
+ // =============================================================================
210
+ // CIRCUIT BREAKER
211
+ // =============================================================================
212
+
213
+ /**
214
+ * Circuit breaker status values.
215
+ */
216
+ export type CircuitStatus = 'GO' | 'STOP';
217
+
218
+ /**
219
+ * Circuit breaker check result.
220
+ */
221
+ export interface CircuitBreakerResult {
222
+ status: CircuitStatus;
223
+ reason?: string;
224
+ level: 'feature' | 'project' | 'global';
225
+ }
226
+
227
+ /**
228
+ * Error thrown when a circuit breaker is open (STOP).
229
+ * Workers should catch this and return 503 Service Unavailable.
230
+ */
231
+ export class CircuitBreakerError extends Error {
232
+ public readonly featureId: FeatureId;
233
+ public readonly level: 'feature' | 'project' | 'global';
234
+ public readonly reason?: string;
235
+
236
+ constructor(featureId: FeatureId, level: 'feature' | 'project' | 'global', reason?: string) {
237
+ const message = reason
238
+ ? `Circuit breaker STOP for ${featureId} at ${level} level: ${reason}`
239
+ : `Circuit breaker STOP for ${featureId} at ${level} level`;
240
+ super(message);
241
+ this.name = 'CircuitBreakerError';
242
+ this.featureId = featureId;
243
+ this.level = level;
244
+ this.reason = reason;
245
+
246
+ // Maintain proper stack trace in V8 (optional - not available in all environments)
247
+ const ErrorWithCapture = Error as typeof Error & {
248
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
249
+ captureStackTrace?: (target: object, constructor?: Function) => void;
250
+ };
251
+ if (ErrorWithCapture.captureStackTrace) {
252
+ ErrorWithCapture.captureStackTrace(this, CircuitBreakerError);
253
+ }
254
+ }
255
+ }
256
+
257
+ // =============================================================================
258
+ // SDK OPTIONS
259
+ // =============================================================================
260
+
261
+ /**
262
+ * Options for withFeatureBudget wrapper.
263
+ */
264
+ export interface SDKOptions {
265
+ /**
266
+ * ExecutionContext for waitUntil support.
267
+ * Required for proper telemetry flushing.
268
+ */
269
+ ctx?: ExecutionContext;
270
+
271
+ /**
272
+ * Whether to check circuit breaker before processing.
273
+ * Default: true
274
+ */
275
+ checkCircuitBreaker?: boolean;
276
+
277
+ /**
278
+ * Whether to report telemetry after processing.
279
+ * Default: true
280
+ */
281
+ reportTelemetry?: boolean;
282
+
283
+ /**
284
+ * Custom KV namespace for circuit breaker state.
285
+ * Default: env.PLATFORM_CACHE
286
+ */
287
+ cacheKv?: KVNamespace;
288
+
289
+ /**
290
+ * Custom queue for telemetry.
291
+ * Default: env.PLATFORM_TELEMETRY
292
+ */
293
+ telemetryQueue?: Queue<TelemetryMessage>;
294
+
295
+ /**
296
+ * Correlation ID for request tracing.
297
+ * If not provided, one will be generated automatically.
298
+ */
299
+ correlationId?: string;
300
+
301
+ /**
302
+ * External API cost in USD (e.g., OpenAI, Apify).
303
+ * Added to auto-calculated CF resource costs.
304
+ * @default 0
305
+ */
306
+ externalCostUsd?: number;
307
+ }
308
+
309
+ // =============================================================================
310
+ // CRON/QUEUE HELPER OPTIONS
311
+ // =============================================================================
312
+
313
+ /**
314
+ * Options for withCronBudget wrapper.
315
+ * Extends SDKOptions with cron-specific fields.
316
+ *
317
+ * @example
318
+ * ```typescript
319
+ * withCronBudget(env, 'platform:cron:cleanup', {
320
+ * ctx,
321
+ * cronExpression: '0 0 * * *',
322
+ * });
323
+ * ```
324
+ */
325
+ export interface CronBudgetOptions {
326
+ /**
327
+ * ExecutionContext - REQUIRED for cron handlers.
328
+ * Used for waitUntil support and proper telemetry flushing.
329
+ */
330
+ ctx: ExecutionContext;
331
+
332
+ /**
333
+ * Cron expression for this scheduled task.
334
+ * Used to generate deterministic correlation IDs for tracing.
335
+ * @example '0 0 * * *' (daily at midnight)
336
+ * @example '0 * * * *' (hourly)
337
+ */
338
+ cronExpression?: string;
339
+
340
+ /**
341
+ * External API cost in USD (e.g., OpenAI, Apify).
342
+ * Added to auto-calculated CF resource costs.
343
+ * @default 0
344
+ */
345
+ externalCostUsd?: number;
346
+ }
347
+
348
+ /**
349
+ * Options for withQueueBudget wrapper.
350
+ * Extends SDKOptions with queue-specific fields.
351
+ *
352
+ * @example
353
+ * ```typescript
354
+ * withQueueBudget(env, 'platform:queue:process', {
355
+ * message: message.body,
356
+ * batchSize: batch.messages.length,
357
+ * });
358
+ * ```
359
+ */
360
+ export interface QueueBudgetOptions<M = unknown> {
361
+ /**
362
+ * Queue message body for correlation ID extraction.
363
+ * If the message has a `correlation_id` field, it will be propagated.
364
+ */
365
+ message?: M;
366
+
367
+ /**
368
+ * Number of messages in the current batch.
369
+ * Logged for observability but not used for billing.
370
+ */
371
+ batchSize?: number;
372
+
373
+ /**
374
+ * Queue name for correlation ID generation.
375
+ * @example 'platform-telemetry'
376
+ */
377
+ queueName?: string;
378
+
379
+ /**
380
+ * External API cost in USD (e.g., OpenAI, Apify).
381
+ * Added to auto-calculated CF resource costs.
382
+ * @default 0
383
+ */
384
+ externalCostUsd?: number;
385
+ }
386
+
387
+ // =============================================================================
388
+ // ENVIRONMENT BINDINGS
389
+ // =============================================================================
390
+
391
+ /**
392
+ * Required environment bindings for Platform SDK.
393
+ */
394
+ export interface PlatformBindings {
395
+ /** KV namespace for circuit breaker state and config */
396
+ PLATFORM_CACHE: KVNamespace;
397
+ /** Queue for telemetry messages */
398
+ PLATFORM_TELEMETRY: Queue<TelemetryMessage>;
399
+ }
400
+
401
+ /**
402
+ * Type helper for environments that include Platform bindings.
403
+ */
404
+ export type WithPlatformBindings<T> = T & PlatformBindings;
405
+
406
+ // =============================================================================
407
+ // HEALTH CHECK
408
+ // =============================================================================
409
+
410
+ /**
411
+ * Result of a control plane health check (KV connectivity).
412
+ */
413
+ export interface ControlPlaneHealth {
414
+ /** Whether KV is accessible */
415
+ healthy: boolean;
416
+ /** Circuit breaker status if KV is accessible */
417
+ status: CircuitStatus | 'UNKNOWN';
418
+ /** Error message if unhealthy */
419
+ error?: string;
420
+ }
421
+
422
+ /**
423
+ * Result of a data plane health check (queue delivery).
424
+ */
425
+ export interface DataPlaneHealth {
426
+ /** Whether the queue accepted the heartbeat message */
427
+ healthy: boolean;
428
+ /** True if the queue send succeeded */
429
+ queueSent: boolean;
430
+ /** Error message if unhealthy */
431
+ error?: string;
432
+ }
433
+
434
+ /**
435
+ * Combined health check result for a feature.
436
+ */
437
+ export interface HealthResult {
438
+ /** Overall health (both planes must be healthy) */
439
+ healthy: boolean;
440
+ /** Control plane status (KV connectivity) */
441
+ controlPlane: ControlPlaneHealth;
442
+ /** Data plane status (queue delivery) */
443
+ dataPlane: DataPlaneHealth;
444
+ /** Project name from featureId */
445
+ project: string;
446
+ /** Feature key in format 'project:category:feature' */
447
+ feature: string;
448
+ /** Unix timestamp when check was performed */
449
+ timestamp: number;
450
+ }
451
+
452
+ /**
453
+ * Extended environment type returned by withFeatureBudget().
454
+ * Includes health() method for dual-plane connectivity checks
455
+ * and fetch() for tracked HTTP calls (auto-detects AI Gateway URLs).
456
+ */
457
+ export type TrackedEnv<T> = T & {
458
+ health(): Promise<HealthResult>;
459
+ /**
460
+ * Tracked fetch that auto-detects AI Gateway URLs.
461
+ * AI Gateway calls are automatically tracked with provider/model extraction.
462
+ * Non-AI Gateway calls pass through unchanged.
463
+ */
464
+ fetch: typeof fetch;
465
+ };