@expandai/sdk 0.17.2 → 0.18.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/Service.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { HttpClient, HttpClientRequest, HttpClientResponse } from '@effect/platform'
2
2
  import { NodeHttpClient } from '@effect/platform-node'
3
- import { Context, Duration, Effect, flow, Layer, Schedule, Schema } from 'effect'
3
+ import { Context, Duration, Effect, flow, Layer, Option, Random, Schedule, Schema } from 'effect'
4
4
  import { ExpandSdkError } from './Error.js'
5
5
  import {
6
6
  type FetchObjectModeResultWithJson,
@@ -31,29 +31,28 @@ export class ExpandServiceOptions extends Schema.Class<ExpandServiceOptions>('Ex
31
31
  }),
32
32
  /**
33
33
  * Total wall-clock budget in milliseconds for the operation, including retries and backoff.
34
- * Each attempt gets `timeoutMs / (maxRetries + 1)`. Defaults to 60_000.
34
+ * A successful attempt may use the remaining operation budget. Defaults to 60_000.
35
35
  */
36
36
  timeoutMs: Schema.optional(Schema.Number).annotations({
37
37
  description:
38
38
  'Total wall-clock budget in milliseconds for the operation, including retries and backoff. ' +
39
- 'Each attempt gets `timeoutMs / (maxRetries + 1)`. Defaults to 60_000.',
39
+ 'A successful attempt may use the remaining operation budget. Defaults to 60_000.',
40
40
  }),
41
41
  /**
42
42
  * Number of retry attempts for retryable failures (timeouts, connection errors, 408/409/429/5xx).
43
- * Defaults to 2. Note: `batched()` defaults to 0 regardless of this setting; pass `{ maxRetries: N }`
44
- * per-call to opt in.
43
+ * Defaults to 2.
45
44
  */
46
45
  maxRetries: Schema.optional(Schema.Number).annotations({
47
46
  description:
48
47
  'Number of retry attempts for retryable failures (timeouts, connection errors, 408/409/429/5xx). ' +
49
- 'Defaults to 2. Note: `batched()` defaults to 0 regardless of this setting; pass `{ maxRetries: N }` per-call to opt in.',
48
+ 'Defaults to 2.',
50
49
  }),
51
50
  },
52
51
  { description: 'Options for constructing an `ExpandService` layer or `ExpandClient` instance.' },
53
52
  ) {}
54
53
 
55
54
  /**
56
- * Per-request overrides accepted by `ExpandService.fetch`/`batched`/`getBatched` (and, transitively,
55
+ * Per-request overrides accepted by `ExpandService.fetch`/`batched`/`getBatched`/`cancelBatched` (and, transitively,
57
56
  * by `ExpandClient`'s methods).
58
57
  */
59
58
  export class RequestOptions extends Schema.Class<RequestOptions>('RequestOptions')(
@@ -82,8 +81,12 @@ export type FetchSearchParams = typeof Generated.FetchFetchSearchRequest.Encoded
82
81
  export type FetchSearchResponse = FetchSnapshotSearchResultWithJson
83
82
  export type BatchedParams = typeof Generated.FetchBatchedRequest.Encoded
84
83
  export type BatchedResponse = typeof Generated.BatchedRun.Type
84
+ export interface BatchedRequestOptions extends RequestOptions {
85
+ readonly idempotencyKey?: string
86
+ }
85
87
  export type GetBatchedParams = typeof Generated.FetchGetBatchedParams.Encoded
86
88
  export type GetBatchedResponse = typeof Generated.BatchedStatus.Type
89
+ export type CancelBatchedResponse = typeof Generated.BatchedCancellation.Type
87
90
 
88
91
  class ServiceConfig extends Schema.Class<ServiceConfig>('ExpandServiceConfig')({
89
92
  apiKey: Schema.NonEmptyString,
@@ -175,20 +178,14 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
175
178
  ) {
176
179
  const totalTimeoutMs = requestOptions.timeoutMs ?? config.timeoutMs
177
180
  const maxRetries = requestOptions.maxRetries ?? config.maxRetries
178
- // `timeoutMs` is the wall-clock budget for the whole operation. Split it across attempts so the
179
- // sum of per-attempt budgets equals `timeoutMs`; the outer Effect.timeout enforces the hard cap
180
- // including retry backoff. Pipe ordering matters: per-attempt timeout must wrap the operation
181
- // (so retry can catch its TimeoutException), and total timeout must wrap retry (so it caps
182
- // backoff between attempts). Floor with a 1ms minimum so degenerate inputs don't produce a
183
- // zero-millisecond budget that fires before any work happens.
184
- const perAttemptTimeoutMs = Math.max(1, Math.floor(totalTimeoutMs / (maxRetries + 1)))
181
+ // `timeoutMs` is one wall-clock budget for the operation, including retries and backoff. A slow
182
+ // valid capture may use most of that budget; only failures that arrive before it expires can retry.
185
183
  const retrySchedule = Schedule.exponential(Duration.millis(250)).pipe(
186
184
  Schedule.jittered,
187
185
  Schedule.compose(Schedule.recurs(maxRetries)),
188
186
  Schedule.whileInput((error: unknown) => isRetryable(error, totalTimeoutMs)),
189
187
  )
190
188
  return yield* operation.pipe(
191
- Effect.timeout(Duration.millis(perAttemptTimeoutMs)),
192
189
  Effect.retry(retrySchedule),
193
190
  Effect.timeout(Duration.millis(totalTimeoutMs)),
194
191
  Effect.mapError((error) => toExpandError(error, totalTimeoutMs)),
@@ -226,15 +223,26 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
226
223
 
227
224
  const batched = Effect.fn('ExpandService.batched')(function* (
228
225
  params: BatchedParams,
229
- requestOptions: RequestOptions = {},
226
+ requestOptions: BatchedRequestOptions = {},
230
227
  ) {
231
- // Batched POST kicks off server-side work and the API has no idempotency-key support; a retry on
232
- // a request that succeeded server-side but failed client-side (timeout, dropped connection)
233
- // would create a duplicate batched run. Default to no retries; callers can opt in explicitly.
234
- // Destructure-with-default also handles `{ maxRetries: undefined }` correctly — falls back to 0
235
- // rather than the service-level default.
236
- const { maxRetries = 0, ...rest } = requestOptions
237
- return yield* withRetries(generated.fetchBatched(params), { maxRetries, ...rest })
228
+ const { idempotencyKey: providedIdempotencyKey, ...retryOptions } = requestOptions
229
+ const idempotencyKey = yield* Option.fromNullable(providedIdempotencyKey).pipe(
230
+ Option.match({
231
+ onSome: Effect.succeed,
232
+ onNone: () =>
233
+ Effect.all([Random.nextInt, Random.nextInt, Random.nextInt]).pipe(
234
+ Effect.map(
235
+ ([first, second, third]) =>
236
+ `sdk-${Math.abs(first).toString(36)}-${Math.abs(second).toString(36)}-${Math.abs(third).toString(36)}`,
237
+ ),
238
+ ),
239
+ }),
240
+ )
241
+ const generatedRequest: Parameters<typeof generated.fetchBatched>[0] = {
242
+ params: { 'x-idempotency-key': idempotencyKey },
243
+ payload: params,
244
+ }
245
+ return yield* withRetries(generated.fetchBatched(generatedRequest), retryOptions)
238
246
  })
239
247
 
240
248
  const getBatched = Effect.fn('ExpandService.getBatched')(function* (
@@ -247,7 +255,14 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
247
255
  return yield* withRetries(generated.fetchGetBatched(id, params), { timeoutMs, maxRetries })
248
256
  })
249
257
 
250
- return { fetch, fetchJson, fetchSearch, batched, getBatched } as const
258
+ const cancelBatched = Effect.fn('ExpandService.cancelBatched')(function* (
259
+ id: string,
260
+ requestOptions: RequestOptions = {},
261
+ ) {
262
+ return yield* withRetries(generated.fetchCancelBatched(id), requestOptions)
263
+ })
264
+
265
+ return { fetch, fetchJson, fetchSearch, batched, getBatched, cancelBatched } as const
251
266
  }),
252
267
  }) {
253
268
  /**
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export type {
2
2
  BatchedParams,
3
+ BatchedRequestOptions,
3
4
  BatchedResponse,
5
+ CancelBatchedResponse,
4
6
  ExpandClientOptions,
5
7
  FetchJsonParams,
6
8
  FetchJsonRequestOptions,