ai-props 2.1.1 → 2.3.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.
Files changed (72) hide show
  1. package/.dev.vars +2 -0
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +131 -118
  4. package/package.json +30 -4
  5. package/src/ai.ts +12 -31
  6. package/src/cascade.ts +795 -0
  7. package/src/client.ts +440 -0
  8. package/src/durable-cascade.ts +743 -0
  9. package/src/event-bridge.ts +478 -0
  10. package/src/generate.ts +14 -12
  11. package/src/hoc.ts +15 -19
  12. package/src/hono-jsx.ts +675 -0
  13. package/src/index.ts +30 -0
  14. package/src/mdx-types.ts +169 -0
  15. package/src/mdx-utils.ts +437 -0
  16. package/src/mdx.ts +1008 -0
  17. package/src/rpc.ts +614 -0
  18. package/src/streaming.ts +618 -0
  19. package/src/validate.ts +15 -29
  20. package/src/worker.ts +547 -0
  21. package/test/cascade.test.ts +338 -0
  22. package/test/durable-cascade.test.ts +319 -0
  23. package/test/event-bridge.test.ts +351 -0
  24. package/test/generate.test.ts +6 -16
  25. package/test/mdx.test.ts +817 -0
  26. package/test/worker/capnweb-rpc.test.ts +1084 -0
  27. package/test/worker/full-flow.integration.test.ts +1463 -0
  28. package/test/worker/hono-jsx.test.ts +1258 -0
  29. package/test/worker/mdx-parsing.test.ts +1148 -0
  30. package/test/worker/setup.ts +56 -0
  31. package/test/worker.test.ts +595 -0
  32. package/tsconfig.json +2 -1
  33. package/vitest.config.js +6 -0
  34. package/vitest.config.ts +15 -1
  35. package/vitest.workers.config.ts +58 -0
  36. package/wrangler.jsonc +27 -0
  37. package/.turbo/turbo-build.log +0 -5
  38. package/dist/ai.d.ts +0 -125
  39. package/dist/ai.d.ts.map +0 -1
  40. package/dist/ai.js +0 -199
  41. package/dist/ai.js.map +0 -1
  42. package/dist/cache.d.ts +0 -66
  43. package/dist/cache.d.ts.map +0 -1
  44. package/dist/cache.js +0 -183
  45. package/dist/cache.js.map +0 -1
  46. package/dist/generate.d.ts +0 -69
  47. package/dist/generate.d.ts.map +0 -1
  48. package/dist/generate.js +0 -221
  49. package/dist/generate.js.map +0 -1
  50. package/dist/hoc.d.ts +0 -164
  51. package/dist/hoc.d.ts.map +0 -1
  52. package/dist/hoc.js +0 -236
  53. package/dist/hoc.js.map +0 -1
  54. package/dist/index.d.ts +0 -15
  55. package/dist/index.d.ts.map +0 -1
  56. package/dist/index.js +0 -21
  57. package/dist/index.js.map +0 -1
  58. package/dist/types.d.ts +0 -152
  59. package/dist/types.d.ts.map +0 -1
  60. package/dist/types.js +0 -7
  61. package/dist/types.js.map +0 -1
  62. package/dist/validate.d.ts +0 -58
  63. package/dist/validate.d.ts.map +0 -1
  64. package/dist/validate.js +0 -253
  65. package/dist/validate.js.map +0 -1
  66. package/src/ai.js +0 -198
  67. package/src/cache.js +0 -182
  68. package/src/generate.js +0 -220
  69. package/src/hoc.js +0 -235
  70. package/src/index.js +0 -20
  71. package/src/types.js +0 -6
  72. package/src/validate.js +0 -252
package/src/rpc.ts ADDED
@@ -0,0 +1,614 @@
1
+ /**
2
+ * RPC Export - Cloudflare Workers RPC utilities for AI Props
3
+ *
4
+ * This module provides RPC-specific functionality for consuming the AI Props
5
+ * service via Cloudflare Workers Service Bindings. It exports the service
6
+ * classes, types, and utilities for building RPC clients.
7
+ *
8
+ * ## RPC Pattern Overview
9
+ *
10
+ * Cloudflare Workers RPC allows direct method invocation between workers
11
+ * using Service Bindings. The AI Props service exposes methods through:
12
+ *
13
+ * 1. **WorkerEntrypoint** (`PropsService`) - The main entry point that workers
14
+ * bind to. Provides `getService()` to get an RPC-callable service instance.
15
+ *
16
+ * 2. **RpcTarget** (`PropsServiceCore`) - The actual service implementation
17
+ * with all callable methods. Returned by `getService()`.
18
+ *
19
+ * ## Usage Patterns
20
+ *
21
+ * ### Basic Service Binding
22
+ * ```typescript
23
+ * // wrangler.jsonc
24
+ * {
25
+ * "services": [
26
+ * { "binding": "AI_PROPS", "service": "ai-props" }
27
+ * ]
28
+ * }
29
+ *
30
+ * // worker.ts
31
+ * import type { PropsService } from 'ai-props/rpc'
32
+ *
33
+ * interface Env {
34
+ * AI_PROPS: Service<PropsService>
35
+ * }
36
+ *
37
+ * export default {
38
+ * async fetch(request: Request, env: Env) {
39
+ * const service = env.AI_PROPS.getService()
40
+ * const result = await service.generate({
41
+ * schema: { title: 'Page title' }
42
+ * })
43
+ * return Response.json(result)
44
+ * }
45
+ * }
46
+ * ```
47
+ *
48
+ * ### With Typed Client Factory
49
+ * ```typescript
50
+ * import { createPropsClient, type PropsClientOptions } from 'ai-props/rpc'
51
+ *
52
+ * const client = createPropsClient({
53
+ * service: env.AI_PROPS,
54
+ * timeout: 30000,
55
+ * retry: { attempts: 3, backoff: 'exponential' }
56
+ * })
57
+ *
58
+ * const result = await client.generate({
59
+ * schema: { title: 'Page title' }
60
+ * })
61
+ * ```
62
+ *
63
+ * ### Error Handling
64
+ * ```typescript
65
+ * import { PropsRPCError, isPropsRPCError } from 'ai-props/rpc'
66
+ *
67
+ * try {
68
+ * const result = await service.generate(options)
69
+ * } catch (error) {
70
+ * if (isPropsRPCError(error)) {
71
+ * console.error(`RPC Error: ${error.code} - ${error.message}`)
72
+ * }
73
+ * }
74
+ * ```
75
+ *
76
+ * @packageDocumentation
77
+ */
78
+
79
+ // Re-export service classes from worker
80
+ export { PropsService, PropsServiceCore, PropsWorker } from './worker.js'
81
+
82
+ // Re-export types needed for RPC usage
83
+ export type {
84
+ PropSchema,
85
+ GeneratePropsOptions,
86
+ GeneratePropsResult,
87
+ AIPropsConfig,
88
+ PropsCache,
89
+ PropsCacheEntry,
90
+ ValidationResult,
91
+ ValidationError,
92
+ } from './types.js'
93
+
94
+ // Re-export Env type
95
+ export type { Env } from './worker.js'
96
+
97
+ // ==================== RPC Error Types ====================
98
+
99
+ /**
100
+ * Error codes for RPC operations
101
+ */
102
+ export enum PropsRPCErrorCode {
103
+ /** Method not found on service */
104
+ METHOD_NOT_FOUND = 'METHOD_NOT_FOUND',
105
+ /** Invalid arguments passed to method */
106
+ INVALID_ARGUMENTS = 'INVALID_ARGUMENTS',
107
+ /** Service connection failed */
108
+ CONNECTION_FAILED = 'CONNECTION_FAILED',
109
+ /** Request timed out */
110
+ TIMEOUT = 'TIMEOUT',
111
+ /** Service returned an error */
112
+ SERVICE_ERROR = 'SERVICE_ERROR',
113
+ /** Network error during RPC call */
114
+ NETWORK_ERROR = 'NETWORK_ERROR',
115
+ /** Unknown error */
116
+ UNKNOWN = 'UNKNOWN',
117
+ }
118
+
119
+ /**
120
+ * Structured error for RPC failures
121
+ *
122
+ * Provides detailed error information for debugging RPC issues.
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * try {
127
+ * await service.generate(options)
128
+ * } catch (error) {
129
+ * if (error instanceof PropsRPCError) {
130
+ * console.error(`[${error.code}] ${error.message}`)
131
+ * if (error.cause) console.error('Caused by:', error.cause)
132
+ * }
133
+ * }
134
+ * ```
135
+ */
136
+ export class PropsRPCError extends Error {
137
+ /** Error code for programmatic handling */
138
+ readonly code: PropsRPCErrorCode
139
+ /** Original error that caused this RPC error */
140
+ readonly originalCause: Error | undefined
141
+ /** Method that was being called */
142
+ readonly method: string | undefined
143
+ /** Timestamp when error occurred */
144
+ readonly timestamp: number
145
+
146
+ constructor(
147
+ message: string,
148
+ code: PropsRPCErrorCode = PropsRPCErrorCode.UNKNOWN,
149
+ options?: { cause?: Error; method?: string }
150
+ ) {
151
+ super(message, options?.cause ? { cause: options.cause } : undefined)
152
+ this.name = 'PropsRPCError'
153
+ this.code = code
154
+ this.originalCause = options?.cause
155
+ this.method = options?.method
156
+ this.timestamp = Date.now()
157
+ }
158
+
159
+ /**
160
+ * Convert to JSON for logging/serialization
161
+ */
162
+ toJSON(): Record<string, unknown> {
163
+ return {
164
+ name: this.name,
165
+ message: this.message,
166
+ code: this.code,
167
+ method: this.method,
168
+ timestamp: this.timestamp,
169
+ cause: this.originalCause?.message,
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Type guard to check if an error is a PropsRPCError
176
+ */
177
+ export function isPropsRPCError(error: unknown): error is PropsRPCError {
178
+ return error instanceof PropsRPCError
179
+ }
180
+
181
+ // ==================== Retry Configuration ====================
182
+
183
+ /**
184
+ * Retry backoff strategy
185
+ */
186
+ export type RetryBackoff = 'linear' | 'exponential' | 'fixed'
187
+
188
+ /**
189
+ * Configuration for retry behavior
190
+ */
191
+ export interface RetryConfig {
192
+ /** Number of retry attempts (default: 3) */
193
+ attempts?: number
194
+ /** Backoff strategy (default: 'exponential') */
195
+ backoff?: RetryBackoff
196
+ /** Initial delay in milliseconds (default: 1000) */
197
+ initialDelay?: number
198
+ /** Maximum delay in milliseconds (default: 30000) */
199
+ maxDelay?: number
200
+ /** Multiplier for exponential backoff (default: 2) */
201
+ multiplier?: number
202
+ /** Error codes to retry on (default: CONNECTION_FAILED, TIMEOUT, NETWORK_ERROR) */
203
+ retryOn?: PropsRPCErrorCode[]
204
+ }
205
+
206
+ /**
207
+ * Default retry configuration
208
+ */
209
+ export const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {
210
+ attempts: 3,
211
+ backoff: 'exponential',
212
+ initialDelay: 1000,
213
+ maxDelay: 30000,
214
+ multiplier: 2,
215
+ retryOn: [
216
+ PropsRPCErrorCode.CONNECTION_FAILED,
217
+ PropsRPCErrorCode.TIMEOUT,
218
+ PropsRPCErrorCode.NETWORK_ERROR,
219
+ ],
220
+ }
221
+
222
+ /**
223
+ * Calculate delay for retry attempt based on backoff strategy
224
+ */
225
+ export function calculateRetryDelay(attempt: number, config: Required<RetryConfig>): number {
226
+ let delay: number
227
+
228
+ switch (config.backoff) {
229
+ case 'fixed':
230
+ delay = config.initialDelay
231
+ break
232
+ case 'linear':
233
+ delay = config.initialDelay * attempt
234
+ break
235
+ case 'exponential':
236
+ default:
237
+ delay = config.initialDelay * Math.pow(config.multiplier, attempt - 1)
238
+ break
239
+ }
240
+
241
+ return Math.min(delay, config.maxDelay)
242
+ }
243
+
244
+ /**
245
+ * Sleep for a specified number of milliseconds
246
+ */
247
+ function sleep(ms: number): Promise<void> {
248
+ return new Promise((resolve) => setTimeout(resolve, ms))
249
+ }
250
+
251
+ /**
252
+ * Execute a function with retry logic
253
+ *
254
+ * Wraps an async function with configurable retry behavior including
255
+ * exponential backoff, maximum attempts, and error code filtering.
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * const result = await withRetry(
260
+ * () => service.generate(options),
261
+ * { attempts: 3, backoff: 'exponential' }
262
+ * )
263
+ * ```
264
+ */
265
+ export async function withRetry<T>(fn: () => Promise<T>, config?: RetryConfig): Promise<T> {
266
+ const fullConfig: Required<RetryConfig> = {
267
+ ...DEFAULT_RETRY_CONFIG,
268
+ ...config,
269
+ }
270
+
271
+ let lastError: Error | undefined
272
+ let attempt = 0
273
+
274
+ while (attempt < fullConfig.attempts) {
275
+ attempt++
276
+
277
+ try {
278
+ return await fn()
279
+ } catch (error) {
280
+ lastError = error instanceof Error ? error : new Error(String(error))
281
+
282
+ // Check if we should retry this error
283
+ const shouldRetry =
284
+ attempt < fullConfig.attempts &&
285
+ (isPropsRPCError(error) ? fullConfig.retryOn.includes(error.code) : true)
286
+
287
+ if (!shouldRetry) {
288
+ throw error
289
+ }
290
+
291
+ // Calculate and wait for delay
292
+ const delay = calculateRetryDelay(attempt, fullConfig)
293
+ await sleep(delay)
294
+ }
295
+ }
296
+
297
+ throw lastError
298
+ }
299
+
300
+ // ==================== Client Factory ====================
301
+
302
+ /**
303
+ * Options for creating a Props RPC client
304
+ */
305
+ export interface PropsClientOptions<Env = unknown> {
306
+ /**
307
+ * The service binding from the worker environment
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * const client = createPropsClient({ service: env.AI_PROPS })
312
+ * ```
313
+ */
314
+ service: {
315
+ getService(): PropsServiceCoreInterface
316
+ }
317
+ /**
318
+ * Request timeout in milliseconds (default: 30000)
319
+ */
320
+ timeout?: number
321
+ /**
322
+ * Retry configuration
323
+ */
324
+ retry?: RetryConfig
325
+ /**
326
+ * Called before each RPC call
327
+ */
328
+ onRequest?: (method: string, args: unknown[]) => void
329
+ /**
330
+ * Called after each RPC call
331
+ */
332
+ onResponse?: (method: string, result: unknown, duration: number) => void
333
+ /**
334
+ * Called when an RPC call fails
335
+ */
336
+ onError?: (method: string, error: Error) => void
337
+ }
338
+
339
+ /**
340
+ * Interface matching PropsServiceCore methods
341
+ * Used for typing the client without importing the class
342
+ */
343
+ export interface PropsServiceCoreInterface {
344
+ generate<T = Record<string, unknown>>(
345
+ options: GeneratePropsOptions
346
+ ): Promise<GeneratePropsResult<T>>
347
+ getSync<T = Record<string, unknown>>(schema: PropSchema, context?: Record<string, unknown>): T
348
+ prefetch(requests: GeneratePropsOptions[]): Promise<void>
349
+ generateMany<T = Record<string, unknown>>(
350
+ requests: GeneratePropsOptions[]
351
+ ): Promise<GeneratePropsResult<T>[]>
352
+ mergeWithGenerated<T extends Record<string, unknown>>(
353
+ schema: PropSchema,
354
+ partialProps: Partial<T>,
355
+ options?: Omit<GeneratePropsOptions, 'schema' | 'context'>
356
+ ): Promise<T>
357
+ configure(config: Partial<AIPropsConfig>): void
358
+ getConfig(): AIPropsConfig
359
+ resetConfig(): void
360
+ getCached<T>(key: string): PropsCacheEntry<T> | undefined
361
+ setCached<T>(key: string, props: T): void
362
+ deleteCached(key: string): boolean
363
+ clearCache(): void
364
+ getCacheSize(): number
365
+ createCacheKey(schema: PropSchema, context?: Record<string, unknown>): string
366
+ configureCache(ttl: number): void
367
+ validate(props: Record<string, unknown>, schema: PropSchema): ValidationResult
368
+ hasRequired(props: Record<string, unknown>, required: string[]): boolean
369
+ getMissing(props: Record<string, unknown>, schema: PropSchema): string[]
370
+ isComplete(props: Record<string, unknown>, schema: PropSchema): boolean
371
+ sanitize<T extends Record<string, unknown>>(props: T, schema: PropSchema): Partial<T>
372
+ mergeDefaults<T extends Record<string, unknown>>(
373
+ props: Partial<T>,
374
+ defaults: Partial<T>,
375
+ schema: PropSchema
376
+ ): Partial<T>
377
+ }
378
+
379
+ // Import types for re-export
380
+ import type {
381
+ PropSchema,
382
+ GeneratePropsOptions,
383
+ GeneratePropsResult,
384
+ AIPropsConfig,
385
+ PropsCacheEntry,
386
+ ValidationResult,
387
+ } from './types.js'
388
+
389
+ /**
390
+ * Props client instance with wrapped methods
391
+ */
392
+ export interface PropsClientInstance extends PropsServiceCoreInterface {
393
+ /**
394
+ * Get the underlying service instance
395
+ */
396
+ getUnderlyingService(): PropsServiceCoreInterface
397
+ }
398
+
399
+ /**
400
+ * Create a typed Props RPC client with optional retry and monitoring
401
+ *
402
+ * Wraps the service binding with timeout handling, retry logic, and
403
+ * lifecycle hooks for monitoring.
404
+ *
405
+ * @example
406
+ * ```typescript
407
+ * // Basic usage
408
+ * const client = createPropsClient({ service: env.AI_PROPS })
409
+ * const result = await client.generate({ schema: { title: 'Page title' } })
410
+ *
411
+ * // With retry and monitoring
412
+ * const client = createPropsClient({
413
+ * service: env.AI_PROPS,
414
+ * timeout: 30000,
415
+ * retry: { attempts: 3, backoff: 'exponential' },
416
+ * onRequest: (method, args) => console.log(`Calling ${method}`),
417
+ * onResponse: (method, result, duration) =>
418
+ * console.log(`${method} completed in ${duration}ms`),
419
+ * onError: (method, error) => console.error(`${method} failed:`, error)
420
+ * })
421
+ * ```
422
+ */
423
+ export function createPropsClient(options: PropsClientOptions): PropsClientInstance {
424
+ const { service, timeout = 30000, retry, onRequest, onResponse, onError } = options
425
+
426
+ // Get the underlying service instance
427
+ const serviceInstance = service.getService()
428
+
429
+ /**
430
+ * Wrap a method with timeout, retry, and hooks
431
+ */
432
+ function wrapMethod<TArgs extends unknown[], TResult>(
433
+ methodName: string,
434
+ method: (...args: TArgs) => Promise<TResult>
435
+ ): (...args: TArgs) => Promise<TResult> {
436
+ return async (...args: TArgs): Promise<TResult> => {
437
+ const startTime = Date.now()
438
+
439
+ // Call onRequest hook
440
+ onRequest?.(methodName, args)
441
+
442
+ const executeCall = async (): Promise<TResult> => {
443
+ // Create timeout promise
444
+ const timeoutPromise = new Promise<never>((_, reject) => {
445
+ setTimeout(() => {
446
+ reject(
447
+ new PropsRPCError(`Request timed out after ${timeout}ms`, PropsRPCErrorCode.TIMEOUT, {
448
+ method: methodName,
449
+ })
450
+ )
451
+ }, timeout)
452
+ })
453
+
454
+ // Race between call and timeout
455
+ try {
456
+ return await Promise.race([method.apply(serviceInstance, args), timeoutPromise])
457
+ } catch (error) {
458
+ // Convert to PropsRPCError if not already
459
+ if (!isPropsRPCError(error)) {
460
+ const message = error instanceof Error ? error.message : String(error)
461
+ const errorCause = error instanceof Error ? error : undefined
462
+ throw new PropsRPCError(
463
+ message,
464
+ PropsRPCErrorCode.SERVICE_ERROR,
465
+ errorCause ? { cause: errorCause, method: methodName } : { method: methodName }
466
+ )
467
+ }
468
+ throw error
469
+ }
470
+ }
471
+
472
+ try {
473
+ // Execute with optional retry
474
+ const result = retry ? await withRetry(executeCall, retry) : await executeCall()
475
+
476
+ // Call onResponse hook
477
+ const duration = Date.now() - startTime
478
+ onResponse?.(methodName, result, duration)
479
+
480
+ return result
481
+ } catch (error) {
482
+ // Call onError hook
483
+ onError?.(methodName, error instanceof Error ? error : new Error(String(error)))
484
+ throw error
485
+ }
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Wrap a synchronous method (just adds hooks, no timeout/retry)
491
+ */
492
+ function wrapSyncMethod<TArgs extends unknown[], TResult>(
493
+ methodName: string,
494
+ method: (...args: TArgs) => TResult
495
+ ): (...args: TArgs) => TResult {
496
+ return (...args: TArgs): TResult => {
497
+ const startTime = Date.now()
498
+ onRequest?.(methodName, args)
499
+
500
+ try {
501
+ const result = method.apply(serviceInstance, args)
502
+ const duration = Date.now() - startTime
503
+ onResponse?.(methodName, result, duration)
504
+ return result
505
+ } catch (error) {
506
+ onError?.(methodName, error instanceof Error ? error : new Error(String(error)))
507
+ throw error
508
+ }
509
+ }
510
+ }
511
+
512
+ // Create wrapped client with type assertions to preserve generics
513
+ const client: PropsClientInstance = {
514
+ // Async methods with timeout/retry
515
+ generate: wrapMethod(
516
+ 'generate',
517
+ serviceInstance.generate.bind(serviceInstance)
518
+ ) as PropsClientInstance['generate'],
519
+ prefetch: wrapMethod('prefetch', serviceInstance.prefetch.bind(serviceInstance)),
520
+ generateMany: wrapMethod(
521
+ 'generateMany',
522
+ serviceInstance.generateMany.bind(serviceInstance)
523
+ ) as PropsClientInstance['generateMany'],
524
+ mergeWithGenerated: wrapMethod(
525
+ 'mergeWithGenerated',
526
+ serviceInstance.mergeWithGenerated.bind(serviceInstance)
527
+ ) as PropsClientInstance['mergeWithGenerated'],
528
+
529
+ // Sync methods (just hooks)
530
+ getSync: wrapSyncMethod(
531
+ 'getSync',
532
+ serviceInstance.getSync.bind(serviceInstance)
533
+ ) as PropsClientInstance['getSync'],
534
+ configure: wrapSyncMethod('configure', serviceInstance.configure.bind(serviceInstance)),
535
+ getConfig: wrapSyncMethod('getConfig', serviceInstance.getConfig.bind(serviceInstance)),
536
+ resetConfig: wrapSyncMethod('resetConfig', serviceInstance.resetConfig.bind(serviceInstance)),
537
+ getCached: wrapSyncMethod(
538
+ 'getCached',
539
+ serviceInstance.getCached.bind(serviceInstance)
540
+ ) as PropsClientInstance['getCached'],
541
+ setCached: wrapSyncMethod(
542
+ 'setCached',
543
+ serviceInstance.setCached.bind(serviceInstance)
544
+ ) as PropsClientInstance['setCached'],
545
+ deleteCached: wrapSyncMethod(
546
+ 'deleteCached',
547
+ serviceInstance.deleteCached.bind(serviceInstance)
548
+ ),
549
+ clearCache: wrapSyncMethod('clearCache', serviceInstance.clearCache.bind(serviceInstance)),
550
+ getCacheSize: wrapSyncMethod(
551
+ 'getCacheSize',
552
+ serviceInstance.getCacheSize.bind(serviceInstance)
553
+ ),
554
+ createCacheKey: wrapSyncMethod(
555
+ 'createCacheKey',
556
+ serviceInstance.createCacheKey.bind(serviceInstance)
557
+ ),
558
+ configureCache: wrapSyncMethod(
559
+ 'configureCache',
560
+ serviceInstance.configureCache.bind(serviceInstance)
561
+ ),
562
+ validate: wrapSyncMethod('validate', serviceInstance.validate.bind(serviceInstance)),
563
+ hasRequired: wrapSyncMethod('hasRequired', serviceInstance.hasRequired.bind(serviceInstance)),
564
+ getMissing: wrapSyncMethod('getMissing', serviceInstance.getMissing.bind(serviceInstance)),
565
+ isComplete: wrapSyncMethod('isComplete', serviceInstance.isComplete.bind(serviceInstance)),
566
+ sanitize: wrapSyncMethod(
567
+ 'sanitize',
568
+ serviceInstance.sanitize.bind(serviceInstance)
569
+ ) as PropsClientInstance['sanitize'],
570
+ mergeDefaults: wrapSyncMethod(
571
+ 'mergeDefaults',
572
+ serviceInstance.mergeDefaults.bind(serviceInstance)
573
+ ) as PropsClientInstance['mergeDefaults'],
574
+
575
+ // Utility method
576
+ getUnderlyingService: () => serviceInstance,
577
+ }
578
+
579
+ return client
580
+ }
581
+
582
+ // ==================== Utility Types ====================
583
+
584
+ /**
585
+ * Extract the service type from an environment binding
586
+ *
587
+ * @example
588
+ * ```typescript
589
+ * interface Env {
590
+ * AI_PROPS: Service<PropsService>
591
+ * }
592
+ *
593
+ * type ServiceType = ExtractService<Env['AI_PROPS']>
594
+ * // ServiceType = PropsService
595
+ * ```
596
+ */
597
+ export type ExtractService<T> = T extends { getService(): infer S } ? S : never
598
+
599
+ /**
600
+ * Type for a Service Binding to PropsService
601
+ *
602
+ * Use this in your Env interface for proper typing.
603
+ *
604
+ * @example
605
+ * ```typescript
606
+ * interface Env {
607
+ * AI_PROPS: PropsServiceBinding
608
+ * }
609
+ * ```
610
+ */
611
+ export interface PropsServiceBinding {
612
+ getService(): PropsServiceCoreInterface
613
+ fetch(request: Request): Promise<Response>
614
+ }