@expandai/sdk 0.17.0 → 0.17.1
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/LICENSE +2 -1
- package/README.md +83 -307
- package/build/dts/Client.d.ts +61 -0
- package/build/dts/Client.d.ts.map +1 -0
- package/build/dts/Error.d.ts +90 -0
- package/build/dts/Error.d.ts.map +1 -0
- package/build/dts/FetchJson.d.ts +364 -0
- package/build/dts/FetchJson.d.ts.map +1 -0
- package/build/dts/Generated.d.ts +1863 -0
- package/build/dts/Generated.d.ts.map +1 -0
- package/build/dts/HttpClientHelpers.d.ts +5 -0
- package/build/dts/HttpClientHelpers.d.ts.map +1 -0
- package/build/dts/Internal.d.ts +49 -0
- package/build/dts/Internal.d.ts.map +1 -0
- package/build/dts/Playground.d.ts +24 -0
- package/build/dts/Playground.d.ts.map +1 -0
- package/build/dts/Service.d.ts +277 -0
- package/build/dts/Service.d.ts.map +1 -0
- package/build/dts/index.d.ts +6 -0
- package/build/dts/index.d.ts.map +1 -0
- package/build/esm/Client.js +87 -0
- package/build/esm/Client.js.map +1 -0
- package/build/esm/Error.js +91 -0
- package/build/esm/Error.js.map +1 -0
- package/build/esm/FetchJson.js +70 -0
- package/build/esm/FetchJson.js.map +1 -0
- package/build/esm/Generated.js +646 -0
- package/build/esm/Generated.js.map +1 -0
- package/build/esm/HttpClientHelpers.js +14 -0
- package/build/esm/HttpClientHelpers.js.map +1 -0
- package/build/esm/Internal.js +95 -0
- package/build/esm/Internal.js.map +1 -0
- package/build/esm/Playground.js +55 -0
- package/build/esm/Playground.js.map +1 -0
- package/build/esm/Service.js +174 -0
- package/build/esm/Service.js.map +1 -0
- package/build/esm/index.js +4 -0
- package/build/esm/index.js.map +1 -0
- package/package.json +53 -136
- package/src/Client.ts +128 -0
- package/src/Error.ts +105 -0
- package/src/FetchJson.ts +109 -0
- package/src/Generated.ts +949 -0
- package/src/HttpClientHelpers.ts +24 -0
- package/src/Internal.ts +246 -0
- package/src/Playground.ts +76 -0
- package/src/Service.ts +264 -0
- package/src/index.ts +27 -21
- package/CHANGELOG.md +0 -238
- package/api-promise.d.mts +0 -2
- package/api-promise.d.mts.map +0 -1
- package/api-promise.d.ts +0 -2
- package/api-promise.d.ts.map +0 -1
- package/api-promise.js +0 -6
- package/api-promise.js.map +0 -1
- package/api-promise.mjs +0 -2
- package/api-promise.mjs.map +0 -1
- package/client.d.mts +0 -184
- package/client.d.mts.map +0 -1
- package/client.d.ts +0 -184
- package/client.d.ts.map +0 -1
- package/client.js +0 -476
- package/client.js.map +0 -1
- package/client.mjs +0 -472
- package/client.mjs.map +0 -1
- package/core/api-promise.d.mts +0 -46
- package/core/api-promise.d.mts.map +0 -1
- package/core/api-promise.d.ts +0 -46
- package/core/api-promise.d.ts.map +0 -1
- package/core/api-promise.js +0 -74
- package/core/api-promise.js.map +0 -1
- package/core/api-promise.mjs +0 -70
- package/core/api-promise.mjs.map +0 -1
- package/core/error.d.mts +0 -46
- package/core/error.d.mts.map +0 -1
- package/core/error.d.ts +0 -46
- package/core/error.d.ts.map +0 -1
- package/core/error.js +0 -113
- package/core/error.js.map +0 -1
- package/core/error.mjs +0 -97
- package/core/error.mjs.map +0 -1
- package/core/resource.d.mts +0 -6
- package/core/resource.d.mts.map +0 -1
- package/core/resource.d.ts +0 -6
- package/core/resource.d.ts.map +0 -1
- package/core/resource.js +0 -11
- package/core/resource.js.map +0 -1
- package/core/resource.mjs +0 -7
- package/core/resource.mjs.map +0 -1
- package/core/uploads.d.mts +0 -3
- package/core/uploads.d.mts.map +0 -1
- package/core/uploads.d.ts +0 -3
- package/core/uploads.d.ts.map +0 -1
- package/core/uploads.js +0 -6
- package/core/uploads.js.map +0 -1
- package/core/uploads.mjs +0 -2
- package/core/uploads.mjs.map +0 -1
- package/error.d.mts +0 -2
- package/error.d.mts.map +0 -1
- package/error.d.ts +0 -2
- package/error.d.ts.map +0 -1
- package/error.js +0 -6
- package/error.js.map +0 -1
- package/error.mjs +0 -2
- package/error.mjs.map +0 -1
- package/index.d.mts +0 -6
- package/index.d.mts.map +0 -1
- package/index.d.ts +0 -6
- package/index.d.ts.map +0 -1
- package/index.js +0 -30
- package/index.js.map +0 -1
- package/index.mjs +0 -7
- package/index.mjs.map +0 -1
- package/internal/builtin-types.d.mts +0 -73
- package/internal/builtin-types.d.mts.map +0 -1
- package/internal/builtin-types.d.ts +0 -73
- package/internal/builtin-types.d.ts.map +0 -1
- package/internal/builtin-types.js +0 -4
- package/internal/builtin-types.js.map +0 -1
- package/internal/builtin-types.mjs +0 -3
- package/internal/builtin-types.mjs.map +0 -1
- package/internal/detect-platform.d.mts +0 -15
- package/internal/detect-platform.d.mts.map +0 -1
- package/internal/detect-platform.d.ts +0 -15
- package/internal/detect-platform.d.ts.map +0 -1
- package/internal/detect-platform.js +0 -162
- package/internal/detect-platform.js.map +0 -1
- package/internal/detect-platform.mjs +0 -157
- package/internal/detect-platform.mjs.map +0 -1
- package/internal/errors.d.mts +0 -3
- package/internal/errors.d.mts.map +0 -1
- package/internal/errors.d.ts +0 -3
- package/internal/errors.d.ts.map +0 -1
- package/internal/errors.js +0 -41
- package/internal/errors.js.map +0 -1
- package/internal/errors.mjs +0 -36
- package/internal/errors.mjs.map +0 -1
- package/internal/headers.d.mts +0 -20
- package/internal/headers.d.mts.map +0 -1
- package/internal/headers.d.ts +0 -20
- package/internal/headers.d.ts.map +0 -1
- package/internal/headers.js +0 -79
- package/internal/headers.js.map +0 -1
- package/internal/headers.mjs +0 -74
- package/internal/headers.mjs.map +0 -1
- package/internal/parse.d.mts +0 -12
- package/internal/parse.d.mts.map +0 -1
- package/internal/parse.d.ts +0 -12
- package/internal/parse.d.ts.map +0 -1
- package/internal/parse.js +0 -40
- package/internal/parse.js.map +0 -1
- package/internal/parse.mjs +0 -37
- package/internal/parse.mjs.map +0 -1
- package/internal/request-options.d.mts +0 -75
- package/internal/request-options.d.mts.map +0 -1
- package/internal/request-options.d.ts +0 -75
- package/internal/request-options.d.ts.map +0 -1
- package/internal/request-options.js +0 -14
- package/internal/request-options.js.map +0 -1
- package/internal/request-options.mjs +0 -10
- package/internal/request-options.mjs.map +0 -1
- package/internal/shim-types.d.mts +0 -17
- package/internal/shim-types.d.mts.map +0 -1
- package/internal/shim-types.d.ts +0 -17
- package/internal/shim-types.d.ts.map +0 -1
- package/internal/shim-types.js +0 -4
- package/internal/shim-types.js.map +0 -1
- package/internal/shim-types.mjs +0 -3
- package/internal/shim-types.mjs.map +0 -1
- package/internal/shims.d.mts +0 -20
- package/internal/shims.d.mts.map +0 -1
- package/internal/shims.d.ts +0 -20
- package/internal/shims.d.ts.map +0 -1
- package/internal/shims.js +0 -92
- package/internal/shims.js.map +0 -1
- package/internal/shims.mjs +0 -85
- package/internal/shims.mjs.map +0 -1
- package/internal/to-file.d.mts +0 -45
- package/internal/to-file.d.mts.map +0 -1
- package/internal/to-file.d.ts +0 -45
- package/internal/to-file.d.ts.map +0 -1
- package/internal/to-file.js +0 -91
- package/internal/to-file.js.map +0 -1
- package/internal/to-file.mjs +0 -88
- package/internal/to-file.mjs.map +0 -1
- package/internal/tslib.js +0 -81
- package/internal/tslib.mjs +0 -17
- package/internal/types.d.mts +0 -69
- package/internal/types.d.mts.map +0 -1
- package/internal/types.d.ts +0 -69
- package/internal/types.d.ts.map +0 -1
- package/internal/types.js +0 -4
- package/internal/types.js.map +0 -1
- package/internal/types.mjs +0 -3
- package/internal/types.mjs.map +0 -1
- package/internal/uploads.d.mts +0 -42
- package/internal/uploads.d.mts.map +0 -1
- package/internal/uploads.d.ts +0 -42
- package/internal/uploads.d.ts.map +0 -1
- package/internal/uploads.js +0 -141
- package/internal/uploads.js.map +0 -1
- package/internal/uploads.mjs +0 -131
- package/internal/uploads.mjs.map +0 -1
- package/internal/utils/base64.d.mts +0 -3
- package/internal/utils/base64.d.mts.map +0 -1
- package/internal/utils/base64.d.ts +0 -3
- package/internal/utils/base64.d.ts.map +0 -1
- package/internal/utils/base64.js +0 -38
- package/internal/utils/base64.js.map +0 -1
- package/internal/utils/base64.mjs +0 -33
- package/internal/utils/base64.mjs.map +0 -1
- package/internal/utils/bytes.d.mts +0 -4
- package/internal/utils/bytes.d.mts.map +0 -1
- package/internal/utils/bytes.d.ts +0 -4
- package/internal/utils/bytes.d.ts.map +0 -1
- package/internal/utils/bytes.js +0 -31
- package/internal/utils/bytes.js.map +0 -1
- package/internal/utils/bytes.mjs +0 -26
- package/internal/utils/bytes.mjs.map +0 -1
- package/internal/utils/env.d.mts +0 -9
- package/internal/utils/env.d.mts.map +0 -1
- package/internal/utils/env.d.ts +0 -9
- package/internal/utils/env.d.ts.map +0 -1
- package/internal/utils/env.js +0 -22
- package/internal/utils/env.js.map +0 -1
- package/internal/utils/env.mjs +0 -18
- package/internal/utils/env.mjs.map +0 -1
- package/internal/utils/log.d.mts +0 -37
- package/internal/utils/log.d.mts.map +0 -1
- package/internal/utils/log.d.ts +0 -37
- package/internal/utils/log.d.ts.map +0 -1
- package/internal/utils/log.js +0 -86
- package/internal/utils/log.js.map +0 -1
- package/internal/utils/log.mjs +0 -80
- package/internal/utils/log.mjs.map +0 -1
- package/internal/utils/path.d.mts +0 -15
- package/internal/utils/path.d.mts.map +0 -1
- package/internal/utils/path.d.ts +0 -15
- package/internal/utils/path.d.ts.map +0 -1
- package/internal/utils/path.js +0 -79
- package/internal/utils/path.js.map +0 -1
- package/internal/utils/path.mjs +0 -74
- package/internal/utils/path.mjs.map +0 -1
- package/internal/utils/sleep.d.mts +0 -2
- package/internal/utils/sleep.d.mts.map +0 -1
- package/internal/utils/sleep.d.ts +0 -2
- package/internal/utils/sleep.d.ts.map +0 -1
- package/internal/utils/sleep.js +0 -7
- package/internal/utils/sleep.js.map +0 -1
- package/internal/utils/sleep.mjs +0 -3
- package/internal/utils/sleep.mjs.map +0 -1
- package/internal/utils/uuid.d.mts +0 -5
- package/internal/utils/uuid.d.mts.map +0 -1
- package/internal/utils/uuid.d.ts +0 -5
- package/internal/utils/uuid.d.ts.map +0 -1
- package/internal/utils/uuid.js +0 -19
- package/internal/utils/uuid.js.map +0 -1
- package/internal/utils/uuid.mjs +0 -15
- package/internal/utils/uuid.mjs.map +0 -1
- package/internal/utils/values.d.mts +0 -18
- package/internal/utils/values.d.mts.map +0 -1
- package/internal/utils/values.d.ts +0 -18
- package/internal/utils/values.d.ts.map +0 -1
- package/internal/utils/values.js +0 -112
- package/internal/utils/values.js.map +0 -1
- package/internal/utils/values.mjs +0 -94
- package/internal/utils/values.mjs.map +0 -1
- package/internal/utils.d.mts +0 -7
- package/internal/utils.d.mts.map +0 -1
- package/internal/utils.d.ts +0 -7
- package/internal/utils.d.ts.map +0 -1
- package/internal/utils.js +0 -11
- package/internal/utils.js.map +0 -1
- package/internal/utils.mjs +0 -8
- package/internal/utils.mjs.map +0 -1
- package/resource.d.mts +0 -2
- package/resource.d.mts.map +0 -1
- package/resource.d.ts +0 -2
- package/resource.d.ts.map +0 -1
- package/resource.js +0 -6
- package/resource.js.map +0 -1
- package/resource.mjs +0 -2
- package/resource.mjs.map +0 -1
- package/resources/index.d.mts +0 -2
- package/resources/index.d.mts.map +0 -1
- package/resources/index.d.ts +0 -2
- package/resources/index.d.ts.map +0 -1
- package/resources/index.js +0 -4
- package/resources/index.js.map +0 -1
- package/resources/index.mjs +0 -3
- package/resources/index.mjs.map +0 -1
- package/resources/top-level.d.mts +0 -469
- package/resources/top-level.d.mts.map +0 -1
- package/resources/top-level.d.ts +0 -469
- package/resources/top-level.d.ts.map +0 -1
- package/resources/top-level.js +0 -4
- package/resources/top-level.js.map +0 -1
- package/resources/top-level.mjs +0 -3
- package/resources/top-level.mjs.map +0 -1
- package/resources.d.mts +0 -2
- package/resources.d.mts.map +0 -1
- package/resources.d.ts +0 -2
- package/resources.d.ts.map +0 -1
- package/resources.js +0 -5
- package/resources.js.map +0 -1
- package/resources.mjs +0 -2
- package/resources.mjs.map +0 -1
- package/src/api-promise.ts +0 -2
- package/src/client.ts +0 -744
- package/src/core/README.md +0 -3
- package/src/core/api-promise.ts +0 -92
- package/src/core/error.ts +0 -130
- package/src/core/resource.ts +0 -11
- package/src/core/uploads.ts +0 -2
- package/src/error.ts +0 -2
- package/src/internal/README.md +0 -3
- package/src/internal/builtin-types.ts +0 -93
- package/src/internal/detect-platform.ts +0 -196
- package/src/internal/errors.ts +0 -33
- package/src/internal/headers.ts +0 -97
- package/src/internal/parse.ts +0 -56
- package/src/internal/request-options.ts +0 -91
- package/src/internal/shim-types.ts +0 -26
- package/src/internal/shims.ts +0 -107
- package/src/internal/to-file.ts +0 -154
- package/src/internal/types.ts +0 -95
- package/src/internal/uploads.ts +0 -187
- package/src/internal/utils/base64.ts +0 -40
- package/src/internal/utils/bytes.ts +0 -32
- package/src/internal/utils/env.ts +0 -18
- package/src/internal/utils/log.ts +0 -127
- package/src/internal/utils/path.ts +0 -88
- package/src/internal/utils/sleep.ts +0 -3
- package/src/internal/utils/uuid.ts +0 -17
- package/src/internal/utils/values.ts +0 -105
- package/src/internal/utils.ts +0 -8
- package/src/lib/.keep +0 -4
- package/src/resource.ts +0 -2
- package/src/resources/index.ts +0 -3
- package/src/resources/top-level.ts +0 -552
- package/src/resources.ts +0 -1
- package/src/tsconfig.json +0 -11
- package/src/uploads.ts +0 -2
- package/src/version.ts +0 -1
- package/uploads.d.mts +0 -2
- package/uploads.d.mts.map +0 -1
- package/uploads.d.ts +0 -2
- package/uploads.d.ts.map +0 -1
- package/uploads.js +0 -6
- package/uploads.js.map +0 -1
- package/uploads.mjs +0 -2
- package/uploads.mjs.map +0 -1
- package/version.d.mts +0 -2
- package/version.d.mts.map +0 -1
- package/version.d.ts +0 -2
- package/version.d.ts.map +0 -1
- package/version.js +0 -5
- package/version.js.map +0 -1
- package/version.mjs +0 -2
- package/version.mjs.map +0 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HttpClientError, HttpClientRequest, HttpClientResponse } from '@effect/platform'
|
|
2
|
+
import { Effect, Schema } from 'effect'
|
|
3
|
+
|
|
4
|
+
export function unexpectedStatus(
|
|
5
|
+
request: HttpClientRequest.HttpClientRequest,
|
|
6
|
+
response: HttpClientResponse.HttpClientResponse,
|
|
7
|
+
) {
|
|
8
|
+
return Effect.flatMap(
|
|
9
|
+
Effect.orElseSucceed(response.text, () => 'Unexpected status code'),
|
|
10
|
+
(description) =>
|
|
11
|
+
Effect.fail(
|
|
12
|
+
new HttpClientError.ResponseError({
|
|
13
|
+
request,
|
|
14
|
+
response,
|
|
15
|
+
reason: 'StatusCode',
|
|
16
|
+
description,
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function decodeError<A, I, R>(response: HttpClientResponse.HttpClientResponse, schema: Schema.Schema<A, I, R>) {
|
|
23
|
+
return Effect.flatMap(HttpClientResponse.schemaBodyJson(schema)(response), Effect.fail)
|
|
24
|
+
}
|
package/src/Internal.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { HttpClientError } from '@effect/platform'
|
|
2
|
+
import { Cause, Effect, HashSet, Layer, Match, Option, ParseResult } from 'effect'
|
|
3
|
+
import {
|
|
4
|
+
ExpandApiError,
|
|
5
|
+
ExpandClientError,
|
|
6
|
+
ExpandConnectionError,
|
|
7
|
+
type ExpandError,
|
|
8
|
+
ExpandSdkError,
|
|
9
|
+
ExpandTimeoutError,
|
|
10
|
+
} from './Error.js'
|
|
11
|
+
import * as Generated from './Generated.js'
|
|
12
|
+
import {
|
|
13
|
+
type BatchedParams,
|
|
14
|
+
type BatchedResponse,
|
|
15
|
+
ExpandService,
|
|
16
|
+
type ExpandServiceOptions,
|
|
17
|
+
type FetchJsonParams,
|
|
18
|
+
type FetchJsonResponse,
|
|
19
|
+
type FetchParams,
|
|
20
|
+
type FetchResponse,
|
|
21
|
+
type FetchSearchParams,
|
|
22
|
+
type FetchSearchResponse,
|
|
23
|
+
type GetBatchedParams,
|
|
24
|
+
type GetBatchedResponse,
|
|
25
|
+
type RequestOptions,
|
|
26
|
+
} from './Service.js'
|
|
27
|
+
|
|
28
|
+
const RETRYABLE_STATUS_CODES = HashSet.make(408, 409, 429, 500, 502, 503, 504)
|
|
29
|
+
|
|
30
|
+
interface TaggedApiError {
|
|
31
|
+
readonly _tag: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasApiErrorTag(error: unknown, tag: string): error is TaggedApiError {
|
|
35
|
+
return typeof error === 'object' && error !== null && '_tag' in error && error._tag === tag
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function apiError(status: number, error: unknown): ExpandApiError {
|
|
39
|
+
const message =
|
|
40
|
+
typeof error === 'object' && error !== null && '_tag' in error && typeof error._tag === 'string'
|
|
41
|
+
? error._tag
|
|
42
|
+
: `HTTP ${status}`
|
|
43
|
+
return new ExpandApiError({ message, status, body: error, cause: error })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Lift a raw failure (HTTP error, parse error, codegen-typed error class, or unrelated throwable)
|
|
48
|
+
* into the typed `ExpandError` union surfaced by the SDK. Used by `ExpandService` to construct
|
|
49
|
+
* its public error channel.
|
|
50
|
+
*/
|
|
51
|
+
export function toExpandError(error: unknown, timeoutMs: number): ExpandError {
|
|
52
|
+
return Match.value(error).pipe(
|
|
53
|
+
Match.when(
|
|
54
|
+
(error: unknown) => error instanceof Cause.TimeoutException,
|
|
55
|
+
(error) => new ExpandTimeoutError({ message: `Request timed out after ${timeoutMs}ms`, timeoutMs, cause: error }),
|
|
56
|
+
),
|
|
57
|
+
Match.when(
|
|
58
|
+
(error: unknown) => error instanceof HttpClientError.RequestError,
|
|
59
|
+
(error) => new ExpandConnectionError({ message: error.message, cause: error }),
|
|
60
|
+
),
|
|
61
|
+
Match.when(
|
|
62
|
+
(error: unknown) => error instanceof HttpClientError.ResponseError,
|
|
63
|
+
(error) =>
|
|
64
|
+
new ExpandApiError({
|
|
65
|
+
message: error.message,
|
|
66
|
+
status: error.response.status,
|
|
67
|
+
body: error.description,
|
|
68
|
+
cause: error,
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
Match.when(
|
|
72
|
+
(error: unknown) => error instanceof Generated.AuthFailed,
|
|
73
|
+
(error) => new ExpandApiError({ message: 'AuthFailed', status: 401, body: error, cause: error }),
|
|
74
|
+
),
|
|
75
|
+
Match.when(
|
|
76
|
+
(error: unknown) => hasApiErrorTag(error, 'AuthFailed'),
|
|
77
|
+
(error) => apiError(401, error),
|
|
78
|
+
),
|
|
79
|
+
Match.when(
|
|
80
|
+
(error: unknown) => hasApiErrorTag(error, 'AccessBlocked'),
|
|
81
|
+
(error) => apiError(403, error),
|
|
82
|
+
),
|
|
83
|
+
Match.when(
|
|
84
|
+
(error: unknown) => error instanceof Generated.PayloadTooLarge,
|
|
85
|
+
(error) => new ExpandApiError({ message: 'PayloadTooLarge', status: 413, body: error, cause: error }),
|
|
86
|
+
),
|
|
87
|
+
Match.when(
|
|
88
|
+
(error: unknown) => hasApiErrorTag(error, 'PayloadTooLarge'),
|
|
89
|
+
(error) => apiError(413, error),
|
|
90
|
+
),
|
|
91
|
+
Match.when(
|
|
92
|
+
(error: unknown) => error instanceof Generated.TooManyRequests,
|
|
93
|
+
(error) => new ExpandApiError({ message: 'TooManyRequests', status: 429, body: error, cause: error }),
|
|
94
|
+
),
|
|
95
|
+
Match.when(
|
|
96
|
+
(error: unknown) => hasApiErrorTag(error, 'TooManyRequests'),
|
|
97
|
+
(error) => apiError(429, error),
|
|
98
|
+
),
|
|
99
|
+
Match.when(
|
|
100
|
+
(error: unknown) => error instanceof Generated.InternalError,
|
|
101
|
+
(error) => new ExpandApiError({ message: 'InternalError', status: 500, body: error, cause: error }),
|
|
102
|
+
),
|
|
103
|
+
Match.when(
|
|
104
|
+
(error: unknown) => hasApiErrorTag(error, 'InternalError'),
|
|
105
|
+
(error) => apiError(500, error),
|
|
106
|
+
),
|
|
107
|
+
Match.when(
|
|
108
|
+
(error: unknown) => error instanceof Generated.ServiceUnavailable,
|
|
109
|
+
(error) => new ExpandApiError({ message: 'ServiceUnavailable', status: 503, body: error, cause: error }),
|
|
110
|
+
),
|
|
111
|
+
Match.when(
|
|
112
|
+
(error: unknown) => hasApiErrorTag(error, 'ServiceUnavailable'),
|
|
113
|
+
(error) => apiError(503, error),
|
|
114
|
+
),
|
|
115
|
+
Match.when(
|
|
116
|
+
(error: unknown) => error instanceof Generated.HttpApiDecodeError,
|
|
117
|
+
(error) => new ExpandApiError({ message: 'HttpApiDecodeError', status: 400, body: error, cause: error }),
|
|
118
|
+
),
|
|
119
|
+
Match.when(
|
|
120
|
+
(error: unknown) => hasApiErrorTag(error, 'HttpApiDecodeError') || hasApiErrorTag(error, 'BadRequest'),
|
|
121
|
+
(error) => apiError(400, error),
|
|
122
|
+
),
|
|
123
|
+
Match.when(
|
|
124
|
+
(error: unknown) => hasApiErrorTag(error, 'FetchSnapshotSearchNotFound'),
|
|
125
|
+
(error) => apiError(404, error),
|
|
126
|
+
),
|
|
127
|
+
// A response we couldn't decode (e.g. a 5xx whose body is empty or doesn't match the declared
|
|
128
|
+
// error schema) arrives as a bare ParseError with no status. Surface a clear SDK error instead
|
|
129
|
+
// of dumping the raw schema-mismatch tree as the message.
|
|
130
|
+
Match.when(
|
|
131
|
+
(error: unknown) => ParseResult.isParseError(error),
|
|
132
|
+
(error) => new ExpandSdkError({ message: 'Failed to decode the API response', cause: error }),
|
|
133
|
+
),
|
|
134
|
+
Match.orElse(
|
|
135
|
+
(error) => new ExpandSdkError({ message: error instanceof Error ? error.message : String(error), cause: error }),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Decide whether a failure should trigger a retry. Timeouts and connection errors are always
|
|
142
|
+
* retryable; API errors are retryable only for the standard transient status codes (408/409/429/5xx).
|
|
143
|
+
*/
|
|
144
|
+
export function isRetryable(error: unknown, timeoutMs: number): boolean {
|
|
145
|
+
// An undecodable response (a transient 5xx with an empty/garbled body, a proxy error page, etc.)
|
|
146
|
+
// surfaces as a bare ParseError with no HTTP status, so the status-based classification below can't
|
|
147
|
+
// see the 5xx and the request would fail un-retried. Treat decode failures as retryable — the
|
|
148
|
+
// operations that retry (fetch/getBatched) are idempotent and safe to repeat.
|
|
149
|
+
if (ParseResult.isParseError(error)) {
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
const expandError = toExpandError(error, timeoutMs)
|
|
153
|
+
return Match.value(expandError).pipe(
|
|
154
|
+
Match.when(
|
|
155
|
+
(error: unknown) => error instanceof ExpandTimeoutError,
|
|
156
|
+
() => true,
|
|
157
|
+
),
|
|
158
|
+
Match.when(
|
|
159
|
+
(error: unknown) => error instanceof ExpandConnectionError,
|
|
160
|
+
() => true,
|
|
161
|
+
),
|
|
162
|
+
Match.when(
|
|
163
|
+
(error: unknown) => error instanceof ExpandApiError,
|
|
164
|
+
(error) => HashSet.has(RETRYABLE_STATUS_CODES, error.status),
|
|
165
|
+
),
|
|
166
|
+
Match.orElse(() => false),
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Effect-side bridge between {@link ExpandService} and the Promise-based {@link ExpandClient}.
|
|
172
|
+
*
|
|
173
|
+
* Exposes the same fetch/batched/getBatched methods as `ExpandService` but with the typed `ExpandError`
|
|
174
|
+
* channel mapped to the `ExpandClientError` hierarchy that async/await consumers expect. Not exported
|
|
175
|
+
* from the package — Effect consumers depend on `ExpandService` directly and receive the typed
|
|
176
|
+
* `ExpandError` unions as-is.
|
|
177
|
+
*/
|
|
178
|
+
export class InternalClient extends Effect.Service<InternalClient>()('@expandai/sdk/InternalClient', {
|
|
179
|
+
effect: Effect.gen(function* () {
|
|
180
|
+
const service = yield* ExpandService
|
|
181
|
+
|
|
182
|
+
const fetch = (
|
|
183
|
+
params: FetchParams,
|
|
184
|
+
options: RequestOptions = {},
|
|
185
|
+
): Effect.Effect<FetchResponse, ExpandClientError> =>
|
|
186
|
+
service.fetch(params, options).pipe(Effect.mapError((error) => error.toPublicError()))
|
|
187
|
+
|
|
188
|
+
const fetchJson = (
|
|
189
|
+
params: FetchJsonParams,
|
|
190
|
+
options: RequestOptions = {},
|
|
191
|
+
): Effect.Effect<FetchJsonResponse, ExpandClientError> =>
|
|
192
|
+
service.fetchJson(params, options).pipe(Effect.mapError((error) => error.toPublicError()))
|
|
193
|
+
|
|
194
|
+
const fetchSearch = (
|
|
195
|
+
params: FetchSearchParams,
|
|
196
|
+
options: RequestOptions = {},
|
|
197
|
+
): Effect.Effect<FetchSearchResponse, ExpandClientError> =>
|
|
198
|
+
service.fetchSearch(params, options).pipe(Effect.mapError((error) => error.toPublicError()))
|
|
199
|
+
|
|
200
|
+
const batched = (
|
|
201
|
+
params: BatchedParams,
|
|
202
|
+
options: RequestOptions = {},
|
|
203
|
+
): Effect.Effect<BatchedResponse, ExpandClientError> =>
|
|
204
|
+
service.batched(params, options).pipe(Effect.mapError((error) => error.toPublicError()))
|
|
205
|
+
|
|
206
|
+
const getBatched = (
|
|
207
|
+
id: string,
|
|
208
|
+
options: GetBatchedParams & RequestOptions = {},
|
|
209
|
+
): Effect.Effect<GetBatchedResponse, ExpandClientError> =>
|
|
210
|
+
service.getBatched(id, options).pipe(Effect.mapError((error) => error.toPublicError()))
|
|
211
|
+
|
|
212
|
+
return { fetch, fetchJson, fetchSearch, batched, getBatched } as const
|
|
213
|
+
}),
|
|
214
|
+
}) {
|
|
215
|
+
/**
|
|
216
|
+
* Build a Layer providing `InternalClient` (and its `ExpandService` dependency) from the given
|
|
217
|
+
* options. The layer can fail at acquisition with `ExpandSdkError` (propagated from
|
|
218
|
+
* `ExpandService.layer`'s validation); callers running this for a Promise wrapper should pipe
|
|
219
|
+
* through `translateCause`, which converts via `toPublicError` to the public `ExpandClientError`.
|
|
220
|
+
*/
|
|
221
|
+
static layer(options: ExpandServiceOptions = {}): Layer.Layer<InternalClient, ExpandSdkError> {
|
|
222
|
+
return this.Default.pipe(Layer.provide(ExpandService.layer(options)))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Flatten an Effect failure cause into the value to throw from a Promise wrapper. Typed errors
|
|
227
|
+
* (`ExpandClientError`, `ExpandSdkError`) pass through `toPublicError` if available; non-failure
|
|
228
|
+
* causes (defects, interruption) get wrapped in a generic `ExpandClientError`.
|
|
229
|
+
*/
|
|
230
|
+
static translateCause(cause: Cause.Cause<unknown>): unknown {
|
|
231
|
+
const failure = Cause.failureOption(cause).pipe(Option.orElse(() => Cause.dieOption(cause)))
|
|
232
|
+
return Option.match(failure, {
|
|
233
|
+
onSome: (error) => (InternalClient.hasPublicErrorTranslator(error) ? error.toPublicError() : error),
|
|
234
|
+
onNone: () => new ExpandClientError(Cause.isInterrupted(cause) ? 'Request was interrupted' : Cause.pretty(cause)),
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private static hasPublicErrorTranslator(error: unknown): error is { toPublicError: () => ExpandClientError } {
|
|
239
|
+
return (
|
|
240
|
+
typeof error === 'object' &&
|
|
241
|
+
error !== null &&
|
|
242
|
+
'toPublicError' in error &&
|
|
243
|
+
typeof error.toPublicError === 'function'
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Public playground (expand.land) URL builders for citing fetch snapshots.
|
|
2
|
+
//
|
|
3
|
+
// Canonical public snapshot URLs (all public, no auth, no TTL):
|
|
4
|
+
//
|
|
5
|
+
// /s/<snapshotId> — whole snapshot
|
|
6
|
+
// /s/<snapshotId>/playground#focus=<id> — highlight a markdown/appendix block by node id
|
|
7
|
+
// /s/<snapshotId>?id=<evidenceId> — open extracted State JSON evidence
|
|
8
|
+
//
|
|
9
|
+
// A citation maps a search snippet (`source` + `location`) to the view that audits it.
|
|
10
|
+
|
|
11
|
+
import { Match } from 'effect'
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_PLAYGROUND_HOST = 'https://expand.land'
|
|
14
|
+
|
|
15
|
+
export type CitationSource = 'markdown' | 'appendix' | 'statejson'
|
|
16
|
+
|
|
17
|
+
/** Snippet location subset needed to build a deep link. Tolerates `null` and `undefined`. */
|
|
18
|
+
export interface CitationLocation {
|
|
19
|
+
readonly nodeIds?: ReadonlyArray<number> | null
|
|
20
|
+
readonly stateJsonSourceId?: number | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeHost(host: string): string {
|
|
24
|
+
return host.replace(/\/+$/, '')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Whole-snapshot URL: `${host}/s/${snapshotId}`. */
|
|
28
|
+
export function playgroundBase(snapshotId: string, host: string = DEFAULT_PLAYGROUND_HOST): string {
|
|
29
|
+
return `${normalizeHost(host)}/s/${encodeURIComponent(snapshotId)}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Origin of a server-provided `meta.playground` URL, for reuse as the host (staging-safe). */
|
|
33
|
+
export function playgroundOrigin(metaPlayground: string): string {
|
|
34
|
+
return new URL(metaPlayground).origin
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Host to build citation links against: the origin of a server-provided `meta.playground`
|
|
39
|
+
* (authoritative, staging-safe) when present and parseable, else the caller's fallback.
|
|
40
|
+
*/
|
|
41
|
+
export function resolvePlaygroundHost(metaPlayground: string | null | undefined, fallback: string): string {
|
|
42
|
+
if (metaPlayground == null) {
|
|
43
|
+
return fallback
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return playgroundOrigin(metaPlayground)
|
|
47
|
+
} catch {
|
|
48
|
+
// meta.playground should always be a valid URL; guard defensively against a malformed value.
|
|
49
|
+
return fallback
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Deep link auditing a single snippet:
|
|
55
|
+
* - `statejson` with a source id → `?id=<stateJsonSourceId>` (opens the JSON evidence object).
|
|
56
|
+
* - `markdown`/`appendix` with a node id → `/playground#focus=<nodeIds[0]>` (highlights the block).
|
|
57
|
+
* - otherwise → the whole-snapshot base URL.
|
|
58
|
+
*/
|
|
59
|
+
export function citationUrl(
|
|
60
|
+
snapshotId: string,
|
|
61
|
+
source: CitationSource,
|
|
62
|
+
location: CitationLocation | null | undefined,
|
|
63
|
+
host: string = DEFAULT_PLAYGROUND_HOST,
|
|
64
|
+
): string {
|
|
65
|
+
const base = playgroundBase(snapshotId, host)
|
|
66
|
+
return Match.value(source).pipe(
|
|
67
|
+
Match.when('statejson', () => {
|
|
68
|
+
const sourceId = location?.stateJsonSourceId
|
|
69
|
+
return sourceId != null ? `${base}?id=${sourceId}` : base
|
|
70
|
+
}),
|
|
71
|
+
Match.orElse(() => {
|
|
72
|
+
const nodeId = location?.nodeIds?.[0]
|
|
73
|
+
return nodeId != null ? `${base}/playground#focus=${nodeId}` : base
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
76
|
+
}
|
package/src/Service.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { HttpClient, HttpClientRequest, HttpClientResponse } from '@effect/platform'
|
|
2
|
+
import { NodeHttpClient } from '@effect/platform-node'
|
|
3
|
+
import { Context, Duration, Effect, flow, Layer, Schedule, Schema } from 'effect'
|
|
4
|
+
import { ExpandSdkError } from './Error.js'
|
|
5
|
+
import {
|
|
6
|
+
type FetchObjectModeResultWithJson,
|
|
7
|
+
type FetchSnapshotSearchResultWithJson,
|
|
8
|
+
fetchObjectModeJson,
|
|
9
|
+
fetchSnapshotSearch,
|
|
10
|
+
} from './FetchJson.js'
|
|
11
|
+
import * as Generated from './Generated.js'
|
|
12
|
+
import { decodeError, unexpectedStatus } from './HttpClientHelpers.js'
|
|
13
|
+
import { isRetryable, toExpandError } from './Internal.js'
|
|
14
|
+
|
|
15
|
+
const DEFAULT_BASE_URL = 'https://api.expand.ai'
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 60_000
|
|
17
|
+
const DEFAULT_MAX_RETRIES = 2
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options accepted by `ExpandService.layer`. All fields are optional; defaults match `ExpandClient`.
|
|
21
|
+
*/
|
|
22
|
+
export class ExpandServiceOptions extends Schema.Class<ExpandServiceOptions>('ExpandServiceOptions')(
|
|
23
|
+
{
|
|
24
|
+
/** API key. Falls back to the `EXPAND_API_KEY` environment variable. */
|
|
25
|
+
apiKey: Schema.optional(Schema.String).annotations({
|
|
26
|
+
description: 'API key. Falls back to the `EXPAND_API_KEY` environment variable.',
|
|
27
|
+
}),
|
|
28
|
+
/** API base URL. Defaults to `https://api.expand.ai`. */
|
|
29
|
+
baseUrl: Schema.optional(Schema.String).annotations({
|
|
30
|
+
description: 'API base URL. Defaults to `https://api.expand.ai`.',
|
|
31
|
+
}),
|
|
32
|
+
/**
|
|
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.
|
|
35
|
+
*/
|
|
36
|
+
timeoutMs: Schema.optional(Schema.Number).annotations({
|
|
37
|
+
description:
|
|
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.',
|
|
40
|
+
}),
|
|
41
|
+
/**
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
maxRetries: Schema.optional(Schema.Number).annotations({
|
|
47
|
+
description:
|
|
48
|
+
'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.',
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
{ description: 'Options for constructing an `ExpandService` layer or `ExpandClient` instance.' },
|
|
53
|
+
) {}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Per-request overrides accepted by `ExpandService.fetch`/`batched`/`getBatched` (and, transitively,
|
|
57
|
+
* by `ExpandClient`'s methods).
|
|
58
|
+
*/
|
|
59
|
+
export class RequestOptions extends Schema.Class<RequestOptions>('RequestOptions')(
|
|
60
|
+
{
|
|
61
|
+
/** Override the configured `timeoutMs` for this request. Total wall-clock budget across all attempts. */
|
|
62
|
+
timeoutMs: Schema.optional(Schema.Number).annotations({
|
|
63
|
+
description: 'Override the configured `timeoutMs` for this request. Total wall-clock budget across all attempts.',
|
|
64
|
+
}),
|
|
65
|
+
/** Override the configured `maxRetries` for this request. */
|
|
66
|
+
maxRetries: Schema.optional(Schema.Number).annotations({
|
|
67
|
+
description: 'Override the configured `maxRetries` for this request.',
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
{ description: 'Per-request overrides for an `ExpandService` operation.' },
|
|
71
|
+
) {}
|
|
72
|
+
|
|
73
|
+
export type FetchParams = typeof Generated.FetchFetchRequest.Encoded
|
|
74
|
+
export type FetchResponse = string
|
|
75
|
+
export type FetchRequestOptions = RequestOptions & typeof Generated.FetchFetchParams.Encoded
|
|
76
|
+
export type FetchJsonParams = typeof Generated.FetchFetchJsonRequest.Encoded
|
|
77
|
+
// `*WithJson` rather than the generated result: the codegen drops the statejson snippet `json`, so
|
|
78
|
+
// the service decodes the body with the recovering schemas (see `FetchJson.ts`).
|
|
79
|
+
export type FetchJsonResponse = FetchObjectModeResultWithJson
|
|
80
|
+
export type FetchJsonRequestOptions = RequestOptions & typeof Generated.FetchFetchJsonParams.Encoded
|
|
81
|
+
export type FetchSearchParams = typeof Generated.FetchFetchSearchRequest.Encoded
|
|
82
|
+
export type FetchSearchResponse = FetchSnapshotSearchResultWithJson
|
|
83
|
+
export type BatchedParams = typeof Generated.FetchBatchedRequest.Encoded
|
|
84
|
+
export type BatchedResponse = typeof Generated.BatchedRun.Type
|
|
85
|
+
export type GetBatchedParams = typeof Generated.FetchGetBatchedParams.Encoded
|
|
86
|
+
export type GetBatchedResponse = typeof Generated.BatchedStatus.Type
|
|
87
|
+
|
|
88
|
+
class ServiceConfig extends Schema.Class<ServiceConfig>('ExpandServiceConfig')({
|
|
89
|
+
apiKey: Schema.NonEmptyString,
|
|
90
|
+
baseUrl: Schema.NonEmptyString,
|
|
91
|
+
timeoutMs: Schema.Number,
|
|
92
|
+
maxRetries: Schema.Number,
|
|
93
|
+
}) {}
|
|
94
|
+
|
|
95
|
+
class ServiceConfigTag extends Context.Tag('@expandai/sdk/ExpandServiceConfig')<
|
|
96
|
+
ServiceConfigTag,
|
|
97
|
+
typeof ServiceConfig.Type
|
|
98
|
+
>() {}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate user-supplied options into a fully-defaulted `ServiceConfig`. Reads
|
|
102
|
+
* `EXPAND_API_KEY` from `process.env` when `options.apiKey` is missing. Used by
|
|
103
|
+
* `ExpandService.layer` (Effect-typed Layer error channel) and by `ExpandClient`
|
|
104
|
+
* (eagerly via `Effect.runSync` to preserve sync-throw constructor semantics).
|
|
105
|
+
*/
|
|
106
|
+
export const buildServiceConfig = Effect.fn('buildServiceConfig')(function* (options: ExpandServiceOptions) {
|
|
107
|
+
const apiKey = options.apiKey ?? process.env.EXPAND_API_KEY
|
|
108
|
+
if (!apiKey) {
|
|
109
|
+
return yield* new ExpandSdkError({ message: 'Missing Expand API key. Pass apiKey or set EXPAND_API_KEY.' })
|
|
110
|
+
}
|
|
111
|
+
return yield* Schema.decode(ServiceConfig)({
|
|
112
|
+
apiKey,
|
|
113
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
114
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
115
|
+
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
116
|
+
}).pipe(Effect.mapError((error) => new ExpandSdkError({ message: 'Invalid Expand client options', cause: error })))
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Effect-native client for the Expand API. Mirrors the {@link ExpandClient} Promise interface —
|
|
121
|
+
* methods accept the same `RequestOptions` per-call overrides and return Effects with the typed
|
|
122
|
+
* `ExpandError` channel (`ExpandApiError | ExpandConnectionError | ExpandTimeoutError | ExpandSdkError`).
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* const program = Effect.gen(function* () {
|
|
127
|
+
* const expand = yield* ExpandService
|
|
128
|
+
* return yield* expand.fetch({ url: 'https://example.com' })
|
|
129
|
+
* }).pipe(Effect.provide(ExpandService.layer({ apiKey: 'sk_...' })))
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export class ExpandService extends Effect.Service<ExpandService>()('@expandai/sdk/ExpandService', {
|
|
133
|
+
dependencies: [NodeHttpClient.layer],
|
|
134
|
+
effect: Effect.gen(function* () {
|
|
135
|
+
const config = yield* ServiceConfigTag
|
|
136
|
+
const httpClient = yield* HttpClient.HttpClient
|
|
137
|
+
const authedClient = httpClient.pipe(
|
|
138
|
+
HttpClient.mapRequest(
|
|
139
|
+
flow(
|
|
140
|
+
HttpClientRequest.prependUrl(config.baseUrl),
|
|
141
|
+
HttpClientRequest.setHeader('x-expand-api-key', config.apiKey),
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
const generated = Generated.make(authedClient)
|
|
146
|
+
const fetchMarkdown = (params: FetchParams, queryParams: typeof Generated.FetchFetchParams.Encoded = {}) =>
|
|
147
|
+
HttpClientRequest.make('POST')('/v1/fetch').pipe(
|
|
148
|
+
// Coalesce null -> undefined: the generated `include` schema is nullable, but `setUrlParams`
|
|
149
|
+
// throws on a null value. `undefined` is correctly dropped (no query param emitted).
|
|
150
|
+
HttpClientRequest.setUrlParams({ include: queryParams.include ?? undefined }),
|
|
151
|
+
(req) => Effect.orDie(HttpClientRequest.bodyJson(req, params)),
|
|
152
|
+
Effect.flatMap((request) =>
|
|
153
|
+
Effect.flatMap(
|
|
154
|
+
authedClient.execute(request),
|
|
155
|
+
HttpClientResponse.matchStatus({
|
|
156
|
+
'200': (response) => response.text,
|
|
157
|
+
'400': (response) => decodeError(response, Generated.FetchFetch400),
|
|
158
|
+
'401': (response) => decodeError(response, Generated.AuthFailed),
|
|
159
|
+
'403': (response) => decodeError(response, Generated.FetchFetch403),
|
|
160
|
+
'413': (response) => decodeError(response, Generated.PayloadTooLarge),
|
|
161
|
+
'429': (response) => decodeError(response, Generated.TooManyRequests),
|
|
162
|
+
'500': (response) => decodeError(response, Generated.InternalError),
|
|
163
|
+
'503': (response) => decodeError(response, Generated.ServiceUnavailable),
|
|
164
|
+
orElse: (response) => unexpectedStatus(request, response),
|
|
165
|
+
}),
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
// Generic over the operation's success/error/requirement channels so callers (fetch/batched/getBatched)
|
|
171
|
+
// retain their full Effect typing through the wrapper.
|
|
172
|
+
const withRetries = Effect.fn('ExpandService.withRetries')(function* <A, E, R>(
|
|
173
|
+
operation: Effect.Effect<A, E, R>,
|
|
174
|
+
requestOptions: RequestOptions = {},
|
|
175
|
+
) {
|
|
176
|
+
const totalTimeoutMs = requestOptions.timeoutMs ?? config.timeoutMs
|
|
177
|
+
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)))
|
|
185
|
+
const retrySchedule = Schedule.exponential(Duration.millis(250)).pipe(
|
|
186
|
+
Schedule.jittered,
|
|
187
|
+
Schedule.compose(Schedule.recurs(maxRetries)),
|
|
188
|
+
Schedule.whileInput((error: unknown) => isRetryable(error, totalTimeoutMs)),
|
|
189
|
+
)
|
|
190
|
+
return yield* operation.pipe(
|
|
191
|
+
Effect.timeout(Duration.millis(perAttemptTimeoutMs)),
|
|
192
|
+
Effect.retry(retrySchedule),
|
|
193
|
+
Effect.timeout(Duration.millis(totalTimeoutMs)),
|
|
194
|
+
Effect.mapError((error) => toExpandError(error, totalTimeoutMs)),
|
|
195
|
+
)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const fetch = Effect.fn('ExpandService.fetch')(function* (
|
|
199
|
+
params: FetchParams,
|
|
200
|
+
requestOptions: FetchRequestOptions = {},
|
|
201
|
+
) {
|
|
202
|
+
const { include, ...retryOptions } = requestOptions
|
|
203
|
+
return yield* withRetries(fetchMarkdown(params, { include }), retryOptions)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const fetchJson = Effect.fn('ExpandService.fetchJson')(function* (
|
|
207
|
+
params: FetchJsonParams,
|
|
208
|
+
requestOptions: FetchJsonRequestOptions = {},
|
|
209
|
+
) {
|
|
210
|
+
const { include, ...retryOptions } = requestOptions
|
|
211
|
+
// `fetchObjectModeJson` mirrors the generated call but preserves the statejson snippet `json`.
|
|
212
|
+
// Coalesce null -> undefined before `setUrlParams`, which throws on null.
|
|
213
|
+
return yield* withRetries(
|
|
214
|
+
fetchObjectModeJson(authedClient, { params: { include: include ?? undefined }, payload: params }),
|
|
215
|
+
retryOptions,
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const fetchSearch = Effect.fn('ExpandService.fetchSearch')(function* (
|
|
220
|
+
params: FetchSearchParams,
|
|
221
|
+
requestOptions: RequestOptions = {},
|
|
222
|
+
) {
|
|
223
|
+
// `fetchSnapshotSearch` mirrors the generated call but preserves the statejson snippet `json`.
|
|
224
|
+
return yield* withRetries(fetchSnapshotSearch(authedClient, params), requestOptions)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const batched = Effect.fn('ExpandService.batched')(function* (
|
|
228
|
+
params: BatchedParams,
|
|
229
|
+
requestOptions: RequestOptions = {},
|
|
230
|
+
) {
|
|
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 })
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const getBatched = Effect.fn('ExpandService.getBatched')(function* (
|
|
241
|
+
id: string,
|
|
242
|
+
options: GetBatchedParams & RequestOptions = {},
|
|
243
|
+
) {
|
|
244
|
+
// Pull RequestOptions out, pass everything else through as query params. Drift-resilient: any
|
|
245
|
+
// new GetBatchedParams field added by codegen flows through `params` automatically.
|
|
246
|
+
const { timeoutMs, maxRetries, ...params } = options
|
|
247
|
+
return yield* withRetries(generated.fetchGetBatched(id, params), { timeoutMs, maxRetries })
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
return { fetch, fetchJson, fetchSearch, batched, getBatched } as const
|
|
251
|
+
}),
|
|
252
|
+
}) {
|
|
253
|
+
/**
|
|
254
|
+
* Build a Layer providing `ExpandService` with the given options. Validation (missing apiKey,
|
|
255
|
+
* Schema-rejected config) surfaces via the Layer's error channel as `ExpandSdkError`.
|
|
256
|
+
*/
|
|
257
|
+
static layer(options: ExpandServiceOptions = {}) {
|
|
258
|
+
return Layer.unwrapEffect(
|
|
259
|
+
buildServiceConfig(options).pipe(
|
|
260
|
+
Effect.map((config) => ExpandService.Default.pipe(Layer.provide(Layer.succeed(ServiceConfigTag, config)))),
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
export type {
|
|
2
|
+
BatchedParams,
|
|
3
|
+
BatchedResponse,
|
|
4
|
+
ExpandClientOptions,
|
|
5
|
+
FetchJsonParams,
|
|
6
|
+
FetchJsonRequestOptions,
|
|
7
|
+
FetchJsonResponse,
|
|
8
|
+
FetchParams,
|
|
9
|
+
FetchRequestOptions,
|
|
10
|
+
FetchResponse,
|
|
11
|
+
FetchSearchParams,
|
|
12
|
+
FetchSearchResponse,
|
|
13
|
+
GetBatchedParams,
|
|
14
|
+
GetBatchedResponse,
|
|
15
|
+
} from './Client.js'
|
|
16
|
+
export { ExpandClient } from './Client.js'
|
|
17
|
+
export type { ExpandError } from './Error.js'
|
|
8
18
|
export {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
InternalServerError,
|
|
20
|
-
PermissionDeniedError,
|
|
21
|
-
UnprocessableEntityError,
|
|
22
|
-
} from './core/error';
|
|
19
|
+
ExpandApiError,
|
|
20
|
+
ExpandClientApiError,
|
|
21
|
+
ExpandClientConnectionError,
|
|
22
|
+
ExpandClientError,
|
|
23
|
+
ExpandClientTimeoutError,
|
|
24
|
+
ExpandConnectionError,
|
|
25
|
+
ExpandSdkError,
|
|
26
|
+
ExpandTimeoutError,
|
|
27
|
+
} from './Error.js'
|
|
28
|
+
export { ExpandService, ExpandServiceOptions, RequestOptions } from './Service.js'
|