@expandai/sdk 0.19.0 → 0.20.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 +130 -4
- package/build/dts/AppendixMarkers.d.ts +30 -0
- package/build/dts/AppendixMarkers.d.ts.map +1 -0
- package/build/dts/Client.d.ts +41 -6
- package/build/dts/Client.d.ts.map +1 -1
- package/build/dts/Crawls.d.ts +568 -0
- package/build/dts/Crawls.d.ts.map +1 -0
- package/build/dts/EnvProxyHttpClient.d.ts +28 -0
- package/build/dts/EnvProxyHttpClient.d.ts.map +1 -0
- package/build/dts/FetchJson.d.ts +444 -37
- package/build/dts/FetchJson.d.ts.map +1 -1
- package/build/dts/Generated.d.ts +1545 -788
- package/build/dts/Generated.d.ts.map +1 -1
- package/build/dts/HttpClientHelpers.d.ts +13 -0
- package/build/dts/HttpClientHelpers.d.ts.map +1 -1
- package/build/dts/Internal.d.ts +6 -4
- package/build/dts/Internal.d.ts.map +1 -1
- package/build/dts/ParseErrorBody.d.ts +26 -0
- package/build/dts/ParseErrorBody.d.ts.map +1 -0
- package/build/dts/Playground.d.ts +20 -0
- package/build/dts/Playground.d.ts.map +1 -1
- package/build/dts/RetryAfter.d.ts +40 -0
- package/build/dts/RetryAfter.d.ts.map +1 -0
- package/build/dts/Service.d.ts +94 -45
- package/build/dts/Service.d.ts.map +1 -1
- package/build/dts/StreamPage.d.ts +10 -0
- package/build/dts/StreamPage.d.ts.map +1 -0
- package/build/dts/index.d.ts +9 -2
- package/build/dts/index.d.ts.map +1 -1
- package/build/esm/AppendixMarkers.js +307 -0
- package/build/esm/AppendixMarkers.js.map +1 -0
- package/build/esm/Client.js +96 -15
- package/build/esm/Client.js.map +1 -1
- package/build/esm/Crawls.js +786 -0
- package/build/esm/Crawls.js.map +1 -0
- package/build/esm/EnvProxyHttpClient.js +55 -0
- package/build/esm/EnvProxyHttpClient.js.map +1 -0
- package/build/esm/FetchJson.js +48 -22
- package/build/esm/FetchJson.js.map +1 -1
- package/build/esm/Generated.js +540 -112
- package/build/esm/Generated.js.map +1 -1
- package/build/esm/HttpClientHelpers.js +36 -1
- package/build/esm/HttpClientHelpers.js.map +1 -1
- package/build/esm/Internal.js +140 -33
- package/build/esm/Internal.js.map +1 -1
- package/build/esm/ParseErrorBody.js +41 -0
- package/build/esm/ParseErrorBody.js.map +1 -0
- package/build/esm/Playground.js +38 -0
- package/build/esm/Playground.js.map +1 -1
- package/build/esm/RetryAfter.js +104 -0
- package/build/esm/RetryAfter.js.map +1 -0
- package/build/esm/Service.js +96 -17
- package/build/esm/Service.js.map +1 -1
- package/build/esm/StreamPage.js +46 -0
- package/build/esm/StreamPage.js.map +1 -0
- package/build/esm/index.js +5 -1
- package/build/esm/index.js.map +1 -1
- package/package.json +8 -2
- package/src/AppendixMarkers.ts +370 -0
- package/src/Client.ts +131 -16
- package/src/Crawls.ts +1387 -0
- package/src/EnvProxyHttpClient.ts +71 -0
- package/src/FetchJson.ts +63 -25
- package/src/Generated.ts +738 -162
- package/src/HttpClientHelpers.ts +48 -1
- package/src/Internal.ts +163 -105
- package/src/ParseErrorBody.ts +42 -0
- package/src/Playground.ts +48 -0
- package/src/RetryAfter.ts +138 -0
- package/src/Service.ts +208 -25
- package/src/StreamPage.ts +47 -0
- package/src/index.ts +29 -3
package/src/Service.ts
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { HttpClient, HttpClientRequest, HttpClientResponse } from '@effect/platform'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { Cause, Context, Duration, Effect, flow, Layer, MutableRef, Option, Random, Schedule, Schema } from 'effect'
|
|
3
|
+
import type {
|
|
4
|
+
CrawlChangePolicy,
|
|
5
|
+
CrawlCreateOptions,
|
|
6
|
+
CrawlListResponse,
|
|
7
|
+
CrawlRequest,
|
|
8
|
+
CrawlRunOptions,
|
|
9
|
+
CrawlStatus,
|
|
10
|
+
CrawlSyncPolicy,
|
|
11
|
+
} from './Crawls.js'
|
|
12
|
+
import * as EnvProxyHttpClient from './EnvProxyHttpClient.js'
|
|
13
|
+
import { ExpandApiError, type ExpandError, ExpandSdkError } from './Error.js'
|
|
5
14
|
import {
|
|
6
|
-
type
|
|
7
|
-
type
|
|
15
|
+
type FetchObjectModeJsonResponse,
|
|
16
|
+
type FetchSnapshotHighlightsResultWithJson,
|
|
8
17
|
fetchObjectModeJson,
|
|
9
|
-
|
|
18
|
+
fetchSnapshotHighlights,
|
|
10
19
|
} from './FetchJson.js'
|
|
11
20
|
import * as Generated from './Generated.js'
|
|
12
21
|
import { decodeError, unexpectedStatus } from './HttpClientHelpers.js'
|
|
13
22
|
import { isRetryable, toExpandError } from './Internal.js'
|
|
23
|
+
import { nextRetryDelay, recordRetryAfter } from './RetryAfter.js'
|
|
14
24
|
|
|
15
25
|
const DEFAULT_BASE_URL = 'https://api.expand.ai'
|
|
16
26
|
const DEFAULT_TIMEOUT_MS = 60_000
|
|
@@ -74,11 +84,24 @@ export type FetchResponse = string
|
|
|
74
84
|
export type FetchRequestOptions = RequestOptions & typeof Generated.FetchFetchParams.Encoded
|
|
75
85
|
export type FetchJsonParams = typeof Generated.FetchFetchJsonRequest.Encoded
|
|
76
86
|
// `*WithJson` rather than the generated result: the codegen drops the statejson snippet `json`, so
|
|
77
|
-
// the service decodes the body with the recovering schemas (see `FetchJson.ts`).
|
|
78
|
-
|
|
87
|
+
// the service decodes the body with the recovering schemas (see `FetchJson.ts`). A union because an
|
|
88
|
+
// origin that never answered has no content to describe — narrow on `_tag`.
|
|
89
|
+
export type FetchJsonResponse = FetchObjectModeJsonResponse
|
|
79
90
|
export type FetchJsonRequestOptions = RequestOptions & typeof Generated.FetchFetchJsonParams.Encoded
|
|
80
|
-
export
|
|
81
|
-
|
|
91
|
+
export interface SnapshotHighlightsParams extends Schema.Schema.Encoded<typeof Generated.SnapshotHighlightsInput> {
|
|
92
|
+
readonly snapshotId: string
|
|
93
|
+
}
|
|
94
|
+
export interface SnapshotCatalogParams {
|
|
95
|
+
readonly snapshotId: string
|
|
96
|
+
readonly highlights?: undefined
|
|
97
|
+
}
|
|
98
|
+
export interface SnapshotGetParams {
|
|
99
|
+
readonly snapshotId: string
|
|
100
|
+
readonly highlights?: SnapshotHighlightsParams['highlights']
|
|
101
|
+
readonly include?: SnapshotHighlightsParams['include']
|
|
102
|
+
}
|
|
103
|
+
export type SnapshotCatalog = typeof Generated.SnapshotCatalog.Type
|
|
104
|
+
export type SnapshotHighlightsResponse = FetchSnapshotHighlightsResultWithJson
|
|
82
105
|
export type BatchedParams = typeof Generated.FetchBatchedRequest.Encoded
|
|
83
106
|
export type BatchedResponse = typeof Generated.BatchedRun.Type
|
|
84
107
|
export interface BatchedRequestOptions extends RequestOptions {
|
|
@@ -126,6 +149,12 @@ export const buildServiceConfig = Effect.fn('buildServiceConfig')(function* (opt
|
|
|
126
149
|
if (!apiKey) {
|
|
127
150
|
return yield* new ExpandSdkError({ message: 'Missing Expand API key. Pass apiKey or set EXPAND_API_KEY.' })
|
|
128
151
|
}
|
|
152
|
+
// Checked here, not only in the HttpClient layer, so `new ExpandClient()` keeps its
|
|
153
|
+
// documented sync-throw contract for bad configuration instead of rejecting later.
|
|
154
|
+
const invalidProxy = EnvProxyHttpClient.invalidProxyEnvMessage()
|
|
155
|
+
if (Option.isSome(invalidProxy)) {
|
|
156
|
+
return yield* new ExpandSdkError({ message: invalidProxy.value })
|
|
157
|
+
}
|
|
129
158
|
return yield* Schema.decode(ServiceConfig)({
|
|
130
159
|
apiKey,
|
|
131
160
|
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
@@ -148,7 +177,7 @@ export const buildServiceConfig = Effect.fn('buildServiceConfig')(function* (opt
|
|
|
148
177
|
* ```
|
|
149
178
|
*/
|
|
150
179
|
export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sdk/ExpandService', {
|
|
151
|
-
dependencies: [
|
|
180
|
+
dependencies: [EnvProxyHttpClient.layer],
|
|
152
181
|
effect: Effect.gen(function* () {
|
|
153
182
|
const config = yield* ServiceConfigTag
|
|
154
183
|
const httpClient = yield* HttpClient.HttpClient
|
|
@@ -159,6 +188,9 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
|
|
|
159
188
|
HttpClientRequest.setHeader('x-expand-api-key', config.apiKey),
|
|
160
189
|
),
|
|
161
190
|
),
|
|
191
|
+
// Wraps every path, generated and hand-written alike, so `withRetries` can honour a
|
|
192
|
+
// `Retry-After` no matter which call produced the failure.
|
|
193
|
+
recordRetryAfter,
|
|
162
194
|
)
|
|
163
195
|
const generated = Generated.make(authedClient)
|
|
164
196
|
const fetchMarkdown = (params: FetchParams, queryParams: typeof Generated.FetchFetchParams.Encoded = {}) =>
|
|
@@ -175,11 +207,15 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
|
|
|
175
207
|
'400': (response) => decodeError(response, Generated.FetchFetch400),
|
|
176
208
|
'401': (response) => decodeError(response, Generated.AuthFailed),
|
|
177
209
|
'403': (response) => decodeError(response, Generated.FetchFetch403),
|
|
178
|
-
'
|
|
179
|
-
'
|
|
210
|
+
'404': (response) => decodeError(response, Generated.FetchOriginNotFound),
|
|
211
|
+
'410': (response) => decodeError(response, Generated.FetchOriginGone),
|
|
212
|
+
'413': (response) => decodeError(response, Generated.FetchFetch413),
|
|
213
|
+
'415': (response) => decodeError(response, Generated.FetchUnsupportedContentType),
|
|
214
|
+
'422': (response) => decodeError(response, Generated.FetchVideoUnavailable),
|
|
215
|
+
'429': (response) => decodeError(response, Generated.FetchFetch429),
|
|
180
216
|
'500': (response) => decodeError(response, Generated.InternalError),
|
|
181
|
-
'502': (response) => decodeError(response, Generated.
|
|
182
|
-
'503': (response) => decodeError(response, Generated.
|
|
217
|
+
'502': (response) => decodeError(response, Generated.FetchFetch502),
|
|
218
|
+
'503': (response) => decodeError(response, Generated.FetchFetch503),
|
|
183
219
|
'504': (response) => decodeError(response, Generated.FetchCaptureTimeout),
|
|
184
220
|
'529': (response) => decodeError(response, Generated.FetchCapacityTimeout),
|
|
185
221
|
orElse: (response) => unexpectedStatus(request, response),
|
|
@@ -196,16 +232,38 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
|
|
|
196
232
|
) {
|
|
197
233
|
const totalTimeoutMs = requestOptions.timeoutMs ?? config.timeoutMs
|
|
198
234
|
const maxRetries = requestOptions.maxRetries ?? config.maxRetries
|
|
235
|
+
// The failure the schedule last chose to wait on, while the fiber is inside that wait.
|
|
236
|
+
// If the wall-clock budget expires mid-wait, the caller is handed this refusal rather
|
|
237
|
+
// than a timeout: the server said "not before X", the caller said "no later than Y",
|
|
238
|
+
// and a timeout would hide the 429/503 the caller branches on. Cleared before every
|
|
239
|
+
// attempt, so a budget that expires during a live attempt still reports as a timeout.
|
|
240
|
+
const pendingRefusal = MutableRef.make<Option.Option<unknown>>(Option.none())
|
|
199
241
|
// `timeoutMs` is one wall-clock budget for the operation, including retries and backoff. A slow
|
|
200
242
|
// valid capture may use most of that budget; only failures that arrive before it expires can retry.
|
|
201
243
|
const retrySchedule = Schedule.exponential(Duration.millis(250)).pipe(
|
|
202
244
|
Schedule.jittered,
|
|
203
245
|
Schedule.compose(Schedule.recurs(maxRetries)),
|
|
204
246
|
Schedule.whileInput((error: unknown) => isRetryable(error, totalTimeoutMs)),
|
|
247
|
+
// `passthrough` republishes the failure as the schedule's output so the delay function
|
|
248
|
+
// can see which error it is about to wait on.
|
|
249
|
+
Schedule.passthrough,
|
|
250
|
+
// Floors the delay at the server's requested wait (re-jittered) and caps it at
|
|
251
|
+
// `MAX_RETRY_AFTER`; the whole policy lives with its inputs in `RetryAfter.ts`.
|
|
252
|
+
Schedule.modifyDelayEffect((error: unknown, backoff) =>
|
|
253
|
+
Effect.zipLeft(
|
|
254
|
+
nextRetryDelay(error, backoff),
|
|
255
|
+
Effect.sync(() => MutableRef.set(pendingRefusal, Option.some(error))),
|
|
256
|
+
),
|
|
257
|
+
),
|
|
205
258
|
)
|
|
206
|
-
return yield*
|
|
259
|
+
return yield* Effect.sync(() => MutableRef.set(pendingRefusal, Option.none())).pipe(
|
|
260
|
+
Effect.zipRight(operation),
|
|
207
261
|
Effect.retry(retrySchedule),
|
|
208
|
-
Effect.
|
|
262
|
+
Effect.timeoutFail({
|
|
263
|
+
duration: Duration.millis(totalTimeoutMs),
|
|
264
|
+
onTimeout: (): unknown =>
|
|
265
|
+
Option.getOrElse(MutableRef.get(pendingRefusal), () => new Cause.TimeoutException()),
|
|
266
|
+
}),
|
|
209
267
|
Effect.mapError((error) => toExpandError(error, totalTimeoutMs)),
|
|
210
268
|
)
|
|
211
269
|
})
|
|
@@ -231,13 +289,39 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
|
|
|
231
289
|
)
|
|
232
290
|
})
|
|
233
291
|
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
292
|
+
const publicClient = httpClient.pipe(
|
|
293
|
+
HttpClient.mapRequest(HttpClientRequest.prependUrl(config.baseUrl)),
|
|
294
|
+
recordRetryAfter,
|
|
295
|
+
)
|
|
296
|
+
function getSnapshot(
|
|
297
|
+
params: SnapshotHighlightsParams,
|
|
298
|
+
options?: RequestOptions,
|
|
299
|
+
): Effect.Effect<SnapshotHighlightsResponse, ExpandError>
|
|
300
|
+
function getSnapshot(
|
|
301
|
+
params: SnapshotCatalogParams,
|
|
302
|
+
options?: RequestOptions,
|
|
303
|
+
): Effect.Effect<SnapshotCatalog, ExpandError>
|
|
304
|
+
function getSnapshot(
|
|
305
|
+
params: SnapshotGetParams,
|
|
306
|
+
options?: RequestOptions,
|
|
307
|
+
): Effect.Effect<SnapshotCatalog | SnapshotHighlightsResponse, ExpandError>
|
|
308
|
+
function getSnapshot(
|
|
309
|
+
params: SnapshotGetParams,
|
|
310
|
+
options: RequestOptions = {},
|
|
311
|
+
): Effect.Effect<SnapshotCatalog | SnapshotHighlightsResponse, ExpandError> {
|
|
312
|
+
return params.highlights === undefined
|
|
313
|
+
? withRetries(
|
|
314
|
+
Generated.make(publicClient).publicSnapshotsGetCatalog(encodeURIComponent(params.snapshotId), {}),
|
|
315
|
+
options,
|
|
316
|
+
)
|
|
317
|
+
: withRetries(
|
|
318
|
+
fetchSnapshotHighlights(authedClient, params.snapshotId, {
|
|
319
|
+
highlights: params.highlights,
|
|
320
|
+
include: params.include,
|
|
321
|
+
}),
|
|
322
|
+
options,
|
|
323
|
+
)
|
|
324
|
+
}
|
|
241
325
|
|
|
242
326
|
const batched = Effect.fn('ExpandService.batched')(function* (
|
|
243
327
|
params: BatchedParams,
|
|
@@ -301,7 +385,106 @@ export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sd
|
|
|
301
385
|
return yield* withRetries(generated.fetchCancelBatched(id), requestOptions)
|
|
302
386
|
})
|
|
303
387
|
|
|
304
|
-
|
|
388
|
+
// Crawl control routes intentionally do not use the fetch-only generator. They still
|
|
389
|
+
// share this service's authenticated client, wall-clock budget, and retry policy.
|
|
390
|
+
const crawlRequest = Effect.fn('ExpandService.crawlRequest')(function* <A>(
|
|
391
|
+
path: string,
|
|
392
|
+
method: 'GET' | 'POST' | 'PATCH',
|
|
393
|
+
options: RequestOptions = {},
|
|
394
|
+
body?: unknown,
|
|
395
|
+
idempotencyKey?: string,
|
|
396
|
+
) {
|
|
397
|
+
const request = HttpClientRequest.make(method)(path)
|
|
398
|
+
const keyed =
|
|
399
|
+
idempotencyKey === undefined
|
|
400
|
+
? request
|
|
401
|
+
: HttpClientRequest.setHeader(request, 'x-idempotency-key', idempotencyKey)
|
|
402
|
+
const encoded =
|
|
403
|
+
body === undefined
|
|
404
|
+
? keyed
|
|
405
|
+
: yield* HttpClientRequest.bodyJson(keyed, body).pipe(
|
|
406
|
+
Effect.mapError((cause) => new ExpandSdkError({ message: 'Invalid Crawl request body.', cause })),
|
|
407
|
+
)
|
|
408
|
+
return yield* withRetries(
|
|
409
|
+
authedClient.execute(encoded).pipe(
|
|
410
|
+
Effect.flatMap((response) =>
|
|
411
|
+
(response.status >= 200 && response.status < 300
|
|
412
|
+
? response.json
|
|
413
|
+
: response.json.pipe(Effect.catchAll(() => Effect.void))
|
|
414
|
+
).pipe(
|
|
415
|
+
Effect.flatMap((value) => {
|
|
416
|
+
if (response.status >= 200 && response.status < 300) {
|
|
417
|
+
// The hand-written interfaces mirror the versioned Crawl HTTP contract.
|
|
418
|
+
return Effect.succeed(value as unknown as A)
|
|
419
|
+
}
|
|
420
|
+
const error = typeof value === 'object' && value !== null && 'error' in value ? value.error : undefined
|
|
421
|
+
const message =
|
|
422
|
+
typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string'
|
|
423
|
+
? error.message
|
|
424
|
+
: `HTTP ${response.status}`
|
|
425
|
+
return new ExpandApiError({ message, status: response.status, body: value })
|
|
426
|
+
}),
|
|
427
|
+
),
|
|
428
|
+
),
|
|
429
|
+
),
|
|
430
|
+
options,
|
|
431
|
+
)
|
|
432
|
+
})
|
|
433
|
+
// Mirrors the Promise client: a durable caller key is required before any request is issued,
|
|
434
|
+
// or an unretryable failure could not be replayed by key.
|
|
435
|
+
// Takes `string | undefined`: TypeScript does not stop a JavaScript caller, and
|
|
436
|
+
// reading `.length` off undefined threw where the contract promises a typed failure.
|
|
437
|
+
const requireCrawlIdempotencyKey = (
|
|
438
|
+
value: string | undefined,
|
|
439
|
+
operation: string,
|
|
440
|
+
): Effect.Effect<string, ExpandSdkError> =>
|
|
441
|
+
value !== undefined && value.length >= 1 && value.length <= 255
|
|
442
|
+
? Effect.succeed(value)
|
|
443
|
+
: new ExpandSdkError({ message: `${operation} requires an idempotencyKey between 1 and 255 characters.` })
|
|
444
|
+
function crawlById(id: string) {
|
|
445
|
+
const path = `/v1/crawls/${encodeURIComponent(id)}`
|
|
446
|
+
return {
|
|
447
|
+
id,
|
|
448
|
+
status: (options: RequestOptions = {}) => crawlRequest<CrawlStatus>(path, 'GET', options),
|
|
449
|
+
cancel: (options: RequestOptions = {}) => crawlRequest<CrawlStatus>(`${path}/cancel`, 'POST', options),
|
|
450
|
+
update: (
|
|
451
|
+
patch: {
|
|
452
|
+
readonly sync?: CrawlSyncPolicy | null
|
|
453
|
+
readonly changePolicy?: CrawlChangePolicy
|
|
454
|
+
readonly maxPages?: number
|
|
455
|
+
},
|
|
456
|
+
options: RequestOptions = {},
|
|
457
|
+
) => crawlRequest<CrawlStatus>(path, 'PATCH', options, patch),
|
|
458
|
+
run: (options: CrawlRunOptions & RequestOptions) =>
|
|
459
|
+
requireCrawlIdempotencyKey(options.idempotencyKey, 'Crawl.run').pipe(
|
|
460
|
+
Effect.flatMap((idempotencyKey) =>
|
|
461
|
+
crawlRequest<CrawlStatus>(
|
|
462
|
+
`${path}/runs${options.replayOnly ? '/replay' : ''}`,
|
|
463
|
+
'POST',
|
|
464
|
+
options,
|
|
465
|
+
undefined,
|
|
466
|
+
idempotencyKey,
|
|
467
|
+
),
|
|
468
|
+
),
|
|
469
|
+
),
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const crawls = Object.assign(crawlById, {
|
|
473
|
+
list: (options: { readonly limit?: number } & RequestOptions = {}) =>
|
|
474
|
+
crawlRequest<CrawlListResponse>(
|
|
475
|
+
options.limit === undefined ? '/v1/crawls' : `/v1/crawls?limit=${encodeURIComponent(options.limit)}`,
|
|
476
|
+
'GET',
|
|
477
|
+
options,
|
|
478
|
+
),
|
|
479
|
+
create: (request: CrawlRequest, options: CrawlCreateOptions & RequestOptions) =>
|
|
480
|
+
requireCrawlIdempotencyKey(options.idempotencyKey, 'Crawls.create').pipe(
|
|
481
|
+
Effect.flatMap((idempotencyKey) =>
|
|
482
|
+
crawlRequest<readonly { readonly id: string }[]>('/v1/crawls', 'POST', options, request, idempotencyKey),
|
|
483
|
+
),
|
|
484
|
+
Effect.map((created) => created.map(({ id }) => crawlById(id))),
|
|
485
|
+
),
|
|
486
|
+
})
|
|
487
|
+
return { fetch, fetchJson, snapshots: { get: getSnapshot }, batched, getBatched, cancelBatched, crawls } as const
|
|
305
488
|
}),
|
|
306
489
|
}) {
|
|
307
490
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BackoffDefaults, type JsonBatch, type StreamResponse, stream } from '@durable-streams/client'
|
|
2
|
+
|
|
3
|
+
/** live:false reads one bounded server response, never the complete retained history. */
|
|
4
|
+
export async function readStreamPage<T>(response: StreamResponse<T>): Promise<JsonBatch<T>> {
|
|
5
|
+
if (response.live !== false) throw new Error('A bounded stream page requires live:false.')
|
|
6
|
+
try {
|
|
7
|
+
const items = await response.json<T>()
|
|
8
|
+
return {
|
|
9
|
+
items,
|
|
10
|
+
offset: response.offset,
|
|
11
|
+
upToDate: response.upToDate,
|
|
12
|
+
streamClosed: response.streamClosed,
|
|
13
|
+
cursor: response.cursor,
|
|
14
|
+
}
|
|
15
|
+
} finally {
|
|
16
|
+
response.cancel()
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Decode bounded responses with the official protocol client while preserving the caller's auth transport. */
|
|
21
|
+
export class DurableStreamReader {
|
|
22
|
+
private readonly cursors = new Map<string, string>()
|
|
23
|
+
|
|
24
|
+
cursor(key: string): string | undefined {
|
|
25
|
+
return this.cursors.get(key)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async read<T>(key: string, response: Response, offset: string): Promise<JsonBatch<T>> {
|
|
29
|
+
const page = await readStreamPage(
|
|
30
|
+
await stream<T>({
|
|
31
|
+
url: response.url || 'https://stream.invalid/',
|
|
32
|
+
offset,
|
|
33
|
+
live: false,
|
|
34
|
+
json: true,
|
|
35
|
+
fetch: async () => response,
|
|
36
|
+
backoffOptions: { ...BackoffDefaults, maxRetries: 0 },
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
this.cursors.delete(key)
|
|
40
|
+
if (!page.streamClosed && page.cursor !== undefined) this.cursors.set(key, page.cursor)
|
|
41
|
+
if (this.cursors.size > 128) {
|
|
42
|
+
const oldest = this.cursors.keys().next().value
|
|
43
|
+
if (oldest !== undefined) this.cursors.delete(oldest)
|
|
44
|
+
}
|
|
45
|
+
return page
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
export type { AppendixMarker } from './AppendixMarkers.js'
|
|
2
|
+
export {
|
|
3
|
+
APPENDIX_MARKER_PREFIX,
|
|
4
|
+
AppendixMarkerLevel,
|
|
5
|
+
isAppendixMarkerLine,
|
|
6
|
+
parseAppendixMarkerLine,
|
|
7
|
+
parseAppendixMarkers,
|
|
8
|
+
stripAppendixMarkers,
|
|
9
|
+
withoutFencedCode,
|
|
10
|
+
} from './AppendixMarkers.js'
|
|
1
11
|
export type {
|
|
2
12
|
BatchedParams,
|
|
3
13
|
BatchedRequestOptions,
|
|
@@ -10,12 +20,16 @@ export type {
|
|
|
10
20
|
FetchParams,
|
|
11
21
|
FetchRequestOptions,
|
|
12
22
|
FetchResponse,
|
|
13
|
-
FetchSearchParams,
|
|
14
|
-
FetchSearchResponse,
|
|
15
23
|
GetBatchedParams,
|
|
16
24
|
GetBatchedResponse,
|
|
25
|
+
SnapshotCatalog,
|
|
26
|
+
SnapshotCatalogParams,
|
|
27
|
+
SnapshotGetParams,
|
|
28
|
+
SnapshotHighlightsParams,
|
|
29
|
+
SnapshotHighlightsResponse,
|
|
17
30
|
} from './Client.js'
|
|
18
|
-
export { ExpandClient } from './Client.js'
|
|
31
|
+
export { Expand, ExpandClient } from './Client.js'
|
|
32
|
+
export * from './Crawls.js'
|
|
19
33
|
export type { ExpandError } from './Error.js'
|
|
20
34
|
export {
|
|
21
35
|
ExpandApiError,
|
|
@@ -27,4 +41,16 @@ export {
|
|
|
27
41
|
ExpandSdkError,
|
|
28
42
|
ExpandTimeoutError,
|
|
29
43
|
} from './Error.js'
|
|
44
|
+
export type { FetchOriginErrorResponse } from './FetchJson.js'
|
|
45
|
+
export { isFetchOriginError } from './FetchJson.js'
|
|
46
|
+
export type { SnapshotHandle } from './Playground.js'
|
|
47
|
+
export {
|
|
48
|
+
citationUrl,
|
|
49
|
+
DEFAULT_PLAYGROUND_HOST,
|
|
50
|
+
parseSnapshotHandle,
|
|
51
|
+
playgroundAppendixUrl,
|
|
52
|
+
playgroundBase,
|
|
53
|
+
playgroundContentUrl,
|
|
54
|
+
resolvePlaygroundHost,
|
|
55
|
+
} from './Playground.js'
|
|
30
56
|
export { ExpandService, ExpandServiceOptions, RequestOptions } from './Service.js'
|