ai-props 2.1.3 → 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.
- package/.dev.vars +2 -0
- package/CHANGELOG.md +11 -0
- package/README.md +2 -0
- package/package.json +39 -13
- package/src/ai.ts +12 -31
- package/src/cascade.ts +795 -0
- package/src/client.ts +440 -0
- package/src/durable-cascade.ts +743 -0
- package/src/event-bridge.ts +478 -0
- package/src/generate.ts +14 -12
- package/src/hoc.ts +15 -19
- package/src/hono-jsx.ts +675 -0
- package/src/index.ts +30 -0
- package/src/mdx-types.ts +169 -0
- package/src/mdx-utils.ts +437 -0
- package/src/mdx.ts +1008 -0
- package/src/rpc.ts +614 -0
- package/src/streaming.ts +618 -0
- package/src/validate.ts +15 -29
- package/src/worker.ts +547 -0
- package/test/cascade.test.ts +338 -0
- package/test/durable-cascade.test.ts +319 -0
- package/test/event-bridge.test.ts +351 -0
- package/test/generate.test.ts +6 -16
- package/test/mdx.test.ts +817 -0
- package/test/worker/capnweb-rpc.test.ts +1084 -0
- package/test/worker/full-flow.integration.test.ts +1463 -0
- package/test/worker/hono-jsx.test.ts +1258 -0
- package/test/worker/mdx-parsing.test.ts +1148 -0
- package/test/worker/setup.ts +56 -0
- package/test/worker.test.ts +595 -0
- package/tsconfig.json +2 -1
- package/vitest.config.js +6 -0
- package/vitest.config.ts +15 -1
- package/vitest.workers.config.ts +58 -0
- package/wrangler.jsonc +27 -0
- package/.turbo/turbo-build.log +0 -4
- package/LICENSE +0 -21
- package/dist/ai.d.ts +0 -125
- package/dist/ai.d.ts.map +0 -1
- package/dist/ai.js +0 -199
- package/dist/ai.js.map +0 -1
- package/dist/cache.d.ts +0 -66
- package/dist/cache.d.ts.map +0 -1
- package/dist/cache.js +0 -183
- package/dist/cache.js.map +0 -1
- package/dist/generate.d.ts +0 -69
- package/dist/generate.d.ts.map +0 -1
- package/dist/generate.js +0 -221
- package/dist/generate.js.map +0 -1
- package/dist/hoc.d.ts +0 -164
- package/dist/hoc.d.ts.map +0 -1
- package/dist/hoc.js +0 -236
- package/dist/hoc.js.map +0 -1
- package/dist/index.d.ts +0 -15
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -21
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -152
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -7
- package/dist/types.js.map +0 -1
- package/dist/validate.d.ts +0 -58
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js +0 -253
- package/dist/validate.js.map +0 -1
- package/src/ai.js +0 -198
- package/src/cache.js +0 -182
- package/src/generate.js +0 -220
- package/src/hoc.js +0 -235
- package/src/index.js +0 -20
- package/src/types.js +0 -6
- 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
|
+
}
|