@elmohq/sdk 0.0.0-stage → 0.1.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/LICENSE.md +21 -0
- package/README.md +325 -2
- package/dist/calls/index.d.mts +288 -0
- package/dist/calls/index.d.mts.map +1 -0
- package/dist/calls/index.mjs +390 -0
- package/dist/calls/index.mjs.map +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1029 -0
- package/dist/index.mjs.map +1 -0
- package/dist/request-options-DWYI_Ldj.d.mts +1550 -0
- package/dist/request-options-DWYI_Ldj.d.mts.map +1 -0
- package/dist/tools-7Hs5J2xb.d.mts +1945 -0
- package/dist/tools-7Hs5J2xb.d.mts.map +1 -0
- package/dist/tools-SwP0sK8H.mjs +2458 -0
- package/dist/tools-SwP0sK8H.mjs.map +1 -0
- package/dist/types/index.d.mts +2 -0
- package/dist/types/index.mjs +1 -0
- package/package.json +67 -4
- package/src/calls/brands-analytics-get.ts +33 -0
- package/src/calls/brands-citations-domains-list.ts +36 -0
- package/src/calls/brands-citations-urls-list.ts +32 -0
- package/src/calls/brands-create.ts +19 -0
- package/src/calls/brands-get.ts +19 -0
- package/src/calls/brands-list.ts +18 -0
- package/src/calls/brands-opportunities-get.ts +42 -0
- package/src/calls/brands-prompt-performance-list.ts +39 -0
- package/src/calls/brands-query-fanout-get.ts +30 -0
- package/src/calls/brands-tags-list.ts +28 -0
- package/src/calls/brands-update.ts +20 -0
- package/src/calls/competitors-create.ts +23 -0
- package/src/calls/competitors-delete.ts +19 -0
- package/src/calls/competitors-get.ts +19 -0
- package/src/calls/competitors-list.ts +23 -0
- package/src/calls/competitors-update.ts +24 -0
- package/src/calls/index.ts +33 -0
- package/src/calls/me-get.ts +18 -0
- package/src/calls/models-list.ts +20 -0
- package/src/calls/organizations-billing-get.ts +37 -0
- package/src/calls/organizations-get.ts +24 -0
- package/src/calls/organizations-list.ts +27 -0
- package/src/calls/prompts-create.ts +27 -0
- package/src/calls/prompts-delete.ts +28 -0
- package/src/calls/prompts-get.ts +23 -0
- package/src/calls/prompts-list.ts +28 -0
- package/src/calls/prompts-runs-get.ts +27 -0
- package/src/calls/prompts-runs-list.ts +30 -0
- package/src/calls/prompts-snapshot-get.ts +28 -0
- package/src/calls/prompts-update.ts +29 -0
- package/src/calls/reports-create.ts +31 -0
- package/src/calls/reports-get.ts +27 -0
- package/src/calls/reports-list.ts +28 -0
- package/src/calls/tools-analyze.ts +26 -0
- package/src/client.ts +662 -0
- package/src/index.ts +21 -0
- package/src/internal/binding/rest/binding.ts +127 -0
- package/src/internal/binding/rest/errors.ts +107 -0
- package/src/internal/binding/rest/path.ts +9 -0
- package/src/internal/binding/rest/url.ts +97 -0
- package/src/internal/codec/json.ts +66 -0
- package/src/internal/codec/registry.ts +55 -0
- package/src/internal/core/backoff.ts +170 -0
- package/src/internal/core/body.ts +54 -0
- package/src/internal/core/browser.ts +11 -0
- package/src/internal/core/call-promise.ts +145 -0
- package/src/internal/core/callable.ts +11 -0
- package/src/internal/core/client.ts +89 -0
- package/src/internal/core/config.ts +84 -0
- package/src/internal/core/credentials.ts +40 -0
- package/src/internal/core/dispatch.ts +227 -0
- package/src/internal/core/env.ts +68 -0
- package/src/internal/core/errors.ts +431 -0
- package/src/internal/core/features.ts +60 -0
- package/src/internal/core/lazy-promise.ts +39 -0
- package/src/internal/core/metadata.ts +79 -0
- package/src/internal/core/params.ts +98 -0
- package/src/internal/core/protocol.ts +17 -0
- package/src/internal/core/read-reply.ts +7 -0
- package/src/internal/core/text.ts +7 -0
- package/src/internal/core/types.ts +556 -0
- package/src/internal/feature/auth.ts +201 -0
- package/src/internal/feature/interceptors.ts +36 -0
- package/src/internal/feature/logger.ts +222 -0
- package/src/internal/feature/retry.ts +165 -0
- package/src/internal/feature/rules.ts +54 -0
- package/src/internal/feature/timeout.ts +146 -0
- package/src/internal/feature/validate.ts +74 -0
- package/src/internal/page/page.ts +402 -0
- package/src/internal/transport/fetch.ts +82 -0
- package/src/resources/analytics.ts +363 -0
- package/src/resources/brands.ts +285 -0
- package/src/resources/competitors.ts +212 -0
- package/src/resources/identity.ts +48 -0
- package/src/resources/models.ts +32 -0
- package/src/resources/opportunities.ts +79 -0
- package/src/resources/organizations.ts +181 -0
- package/src/resources/prompts.ts +292 -0
- package/src/resources/reports.ts +211 -0
- package/src/resources/runs.ts +123 -0
- package/src/resources/shared/auth.ts +13 -0
- package/src/resources/shared/request-options.ts +26 -0
- package/src/resources/shared/resource.ts +23 -0
- package/src/resources/shared/rule.ts +25 -0
- package/src/resources/snapshots.ts +54 -0
- package/src/resources/tags.ts +46 -0
- package/src/resources/tools.ts +68 -0
- package/src/types/analytics.ts +497 -0
- package/src/types/brands.ts +228 -0
- package/src/types/competitors.ts +196 -0
- package/src/types/identity.ts +55 -0
- package/src/types/index.ts +163 -0
- package/src/types/models.ts +40 -0
- package/src/types/opportunities.ts +84 -0
- package/src/types/organizations.ts +157 -0
- package/src/types/prompts.ts +231 -0
- package/src/types/reports.ts +266 -0
- package/src/types/runs.ts +131 -0
- package/src/types/shared/brand-id-path.ts +2 -0
- package/src/types/shared/conflict-error.ts +7 -0
- package/src/types/shared/error.ts +29 -0
- package/src/types/shared/limit.ts +8 -0
- package/src/types/shared/mention-entry.ts +21 -0
- package/src/types/shared/model-filter.ts +15 -0
- package/src/types/shared/not-found-error.ts +4 -0
- package/src/types/shared/page.ts +7 -0
- package/src/types/shared/pagination.ts +7 -0
- package/src/types/shared/validation-error.ts +4 -0
- package/src/types/snapshots.ts +88 -0
- package/src/types/tags.ts +47 -0
- package/src/types/tools.ts +78 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { retryAfterDelay } from './backoff';
|
|
2
|
+
import type { Interaction } from './types';
|
|
3
|
+
import type { Error as ErrorSchema } from '../../types/shared/error';
|
|
4
|
+
|
|
5
|
+
/** The base of every error this SDK throws, including one from a call that never arrived. */
|
|
6
|
+
export class ElmoError extends Error {
|
|
7
|
+
/** The class's name. Compare it where `instanceof` fails, such as across two copies of this SDK. */
|
|
8
|
+
override name: string = 'ElmoError';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function refusedInput(held: string, rule: string, got: bigint | number) {
|
|
12
|
+
return new ElmoError(`${held} ${rule}, and got ${got}, so the request was not sent.`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Thrown before the request goes out, when no credential satisfied the call. */
|
|
16
|
+
export class MissingCredentialError extends ElmoError {
|
|
17
|
+
override readonly name = 'MissingCredentialError';
|
|
18
|
+
/** Credential options that would have satisfied the call. */
|
|
19
|
+
readonly schemes: ReadonlyArray<string>;
|
|
20
|
+
|
|
21
|
+
constructor(message: string, schemes: ReadonlyArray<string> = []) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.schemes = schemes;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function unsendable(held: string): string {
|
|
28
|
+
return `${held} holds a line break or another character a header cannot carry, so the request was not sent.`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Thrown when the caller called the request off before the API answered.
|
|
33
|
+
*
|
|
34
|
+
* Not a {@link TransportError}, so code that retries or reports a failed
|
|
35
|
+
* connection leaves a call the caller cancelled alone.
|
|
36
|
+
*/
|
|
37
|
+
export class AbortError extends ElmoError {
|
|
38
|
+
constructor(cause?: unknown) {
|
|
39
|
+
super('The request was called off before the API answered.', { cause });
|
|
40
|
+
this.name = 'AbortError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Which way a {@link TransportError} failed. `connect` and `other` have no class of their own. */
|
|
45
|
+
export type TransportErrorKind = 'connect' | 'other' | 'timeout';
|
|
46
|
+
|
|
47
|
+
const TRANSPORT_MESSAGES: Readonly<Record<TransportErrorKind, string>> = {
|
|
48
|
+
connect: 'The request never reached the API.',
|
|
49
|
+
other: 'The connection failed before the whole reply arrived.',
|
|
50
|
+
timeout: 'The API did not answer in time.',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A request that got no whole reply, as its transport states it.
|
|
55
|
+
*
|
|
56
|
+
* A transport rejects with one, or with a {@link TransportError} built from
|
|
57
|
+
* one. Any other rejection is read by its name and code.
|
|
58
|
+
*/
|
|
59
|
+
export interface TransportFailure {
|
|
60
|
+
/** `true` where the request was called off, not lost. */
|
|
61
|
+
aborted: boolean;
|
|
62
|
+
/** What the library underneath threw. */
|
|
63
|
+
cause?: unknown;
|
|
64
|
+
/**
|
|
65
|
+
* Whether the API can have acted on the request. After `'no'`, the call is
|
|
66
|
+
* safe to send again, whatever its method.
|
|
67
|
+
*/
|
|
68
|
+
reachedServer: 'maybe' | 'no' | 'yes';
|
|
69
|
+
/** How far the request got. */
|
|
70
|
+
stage: 'connect' | 'receive' | 'resolve' | 'send' | 'tls';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Thrown when no whole answer arrived, so there is no reply to go on. */
|
|
74
|
+
export class TransportError extends ElmoError {
|
|
75
|
+
readonly kind: TransportErrorKind;
|
|
76
|
+
/**
|
|
77
|
+
* Whether the API can have acted on the request. After `'no'`, the call is
|
|
78
|
+
* safe to send again, whatever its method.
|
|
79
|
+
*/
|
|
80
|
+
readonly reachedServer: TransportFailure['reachedServer'];
|
|
81
|
+
/** How far the request got. */
|
|
82
|
+
readonly stage: TransportFailure['stage'];
|
|
83
|
+
|
|
84
|
+
constructor(failure: TransportErrorKind | TransportFailure, cause?: unknown, message?: string) {
|
|
85
|
+
const unreached = failure === 'connect';
|
|
86
|
+
const stated: Omit<TransportFailure, 'aborted'> =
|
|
87
|
+
typeof failure === 'string'
|
|
88
|
+
? {
|
|
89
|
+
cause,
|
|
90
|
+
reachedServer: unreached ? 'no' : 'maybe',
|
|
91
|
+
stage: unreached ? 'connect' : 'receive',
|
|
92
|
+
}
|
|
93
|
+
: failure;
|
|
94
|
+
const kind =
|
|
95
|
+
typeof failure === 'string' ? failure : failure.reachedServer === 'no' ? 'connect' : 'other';
|
|
96
|
+
super(message ?? TRANSPORT_MESSAGES[kind], stated);
|
|
97
|
+
this.kind = kind;
|
|
98
|
+
this.name = 'TransportError';
|
|
99
|
+
this.reachedServer = stated.reachedServer;
|
|
100
|
+
this.stage = stated.stage;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function calledOffWith(reason: unknown): AbortError | TransportError {
|
|
105
|
+
return reason instanceof AbortError || reason instanceof TransportError
|
|
106
|
+
? reason
|
|
107
|
+
: new AbortError(reason);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const TIMEOUT_CODES: ReadonlyArray<string> = ['ECONNABORTED', 'ETIMEDOUT'];
|
|
111
|
+
|
|
112
|
+
function codeOf(error: unknown): string | undefined {
|
|
113
|
+
let current: unknown = error;
|
|
114
|
+
for (let depth = 0; current instanceof Error && depth < 5; depth++) {
|
|
115
|
+
const { code } = current as { code?: unknown };
|
|
116
|
+
if (typeof code === 'string') return code;
|
|
117
|
+
current = current.cause;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const UNREACHED: Readonly<Record<string, TransportFailure['stage']>> = {
|
|
123
|
+
EAI_AGAIN: 'resolve',
|
|
124
|
+
ECONNREFUSED: 'connect',
|
|
125
|
+
ENOTFOUND: 'resolve',
|
|
126
|
+
UND_ERR_CONNECT_TIMEOUT: 'connect',
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export function failureOf(error: unknown): TransportFailure {
|
|
130
|
+
const stated = (error ?? {}) as TransportFailure;
|
|
131
|
+
if (stated.reachedServer && stated.stage) return stated;
|
|
132
|
+
const stage = UNREACHED[codeOf(error) ?? ''];
|
|
133
|
+
return {
|
|
134
|
+
aborted: error instanceof Error && error.name === 'AbortError',
|
|
135
|
+
cause: error,
|
|
136
|
+
reachedServer: stage ? 'no' : 'maybe',
|
|
137
|
+
stage: stage ?? 'receive',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function classifyError(error: unknown): TransportErrorKind | 'abort' {
|
|
142
|
+
if (error instanceof TransportError) return error.kind;
|
|
143
|
+
const failure = failureOf(error);
|
|
144
|
+
if (failure.aborted) return 'abort';
|
|
145
|
+
if (error instanceof Error && error.name === 'TimeoutError') return 'timeout';
|
|
146
|
+
if (failure.reachedServer === 'no') return 'connect';
|
|
147
|
+
return TIMEOUT_CODES.includes(codeOf(error) ?? '') ? 'timeout' : 'other';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const USERINFO_RE = /^([a-z][a-z\d+.-]*:\/\/)[^/?#@]*@/i;
|
|
151
|
+
|
|
152
|
+
export function safeAddress(url: string): string {
|
|
153
|
+
return (url.split('?')[0] ?? '').replace(USERINFO_RE, '$1');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Thrown when a call waited on the API for as long as it allowed, from sending
|
|
158
|
+
* the request to the last byte of the reply.
|
|
159
|
+
*
|
|
160
|
+
* A reply with status 408 is an {@link APIError}, not one of these.
|
|
161
|
+
*/
|
|
162
|
+
export class TimeoutError extends TransportError {
|
|
163
|
+
/** Milliseconds the call was allowed to wait on the API. */
|
|
164
|
+
readonly timeoutMs: number | undefined;
|
|
165
|
+
|
|
166
|
+
constructor(timeout?: number, cause?: unknown, target?: string) {
|
|
167
|
+
super(
|
|
168
|
+
'timeout',
|
|
169
|
+
cause,
|
|
170
|
+
timeout === undefined
|
|
171
|
+
? TRANSPORT_MESSAGES.timeout
|
|
172
|
+
: `${target ?? 'The request'} timed out after ${timeout} ms waiting on the API. Pass a larger \`timeout\` with the call, or \`timeout: false\` to wait as long as it takes.`,
|
|
173
|
+
);
|
|
174
|
+
this.name = 'TimeoutError';
|
|
175
|
+
this.timeoutMs = timeout;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The body of a failure, as the API description declares it. */
|
|
180
|
+
export type FailureBody = ErrorSchema;
|
|
181
|
+
|
|
182
|
+
const CODE_KEYS: ReadonlyArray<string> = [
|
|
183
|
+
'code',
|
|
184
|
+
'error_code',
|
|
185
|
+
'errorCode',
|
|
186
|
+
'error_type',
|
|
187
|
+
'errorType',
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
function statedCode(bag: Record<string, unknown>): string | number | undefined {
|
|
191
|
+
for (const key of CODE_KEYS) {
|
|
192
|
+
const value = bag[key];
|
|
193
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
194
|
+
if (typeof value === 'number') return value;
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function describeCode(error: unknown): string | number | undefined {
|
|
200
|
+
if (!error || typeof error !== 'object') return undefined;
|
|
201
|
+
|
|
202
|
+
const body = error as Record<string, unknown>;
|
|
203
|
+
const direct = statedCode(body);
|
|
204
|
+
if (direct !== undefined) return direct;
|
|
205
|
+
|
|
206
|
+
for (const value of Object.values(body)) {
|
|
207
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
208
|
+
const nested = statedCode(value as Record<string, unknown>);
|
|
209
|
+
if (nested !== undefined) return nested;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const MAX_MESSAGE_BODY = 200;
|
|
216
|
+
|
|
217
|
+
function oneLine(text: string): string {
|
|
218
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function shorten(text: string): string {
|
|
222
|
+
const line = oneLine(text);
|
|
223
|
+
if (line.length <= MAX_MESSAGE_BODY) return line;
|
|
224
|
+
const cut = line.slice(0, MAX_MESSAGE_BODY);
|
|
225
|
+
const space = cut.lastIndexOf(' ');
|
|
226
|
+
const kept = space > MAX_MESSAGE_BODY / 2 ? cut.slice(0, space) : cut;
|
|
227
|
+
return `${kept.trimEnd()}…`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const MESSAGE_KEYS: ReadonlyArray<string> = [
|
|
231
|
+
'message',
|
|
232
|
+
'error_message',
|
|
233
|
+
'errorMessage',
|
|
234
|
+
'error',
|
|
235
|
+
'detail',
|
|
236
|
+
'title',
|
|
237
|
+
'description',
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
function statedMessage(bag: Record<string, unknown>): string | undefined {
|
|
241
|
+
for (const key of MESSAGE_KEYS) {
|
|
242
|
+
const value = bag[key];
|
|
243
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function describeStatedFailure(error: unknown): string | undefined {
|
|
249
|
+
if (typeof error === 'string') return shorten(error.trim()) || undefined;
|
|
250
|
+
if (!error || typeof error !== 'object') return undefined;
|
|
251
|
+
|
|
252
|
+
const body = error as Record<string, unknown>;
|
|
253
|
+
const direct = statedMessage(body);
|
|
254
|
+
if (direct) return shorten(direct);
|
|
255
|
+
|
|
256
|
+
for (const value of Object.values(body)) {
|
|
257
|
+
if (value && typeof value === 'object') {
|
|
258
|
+
const nested = statedMessage(value as Record<string, unknown>);
|
|
259
|
+
if (nested) return shorten(nested);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const json = JSON.stringify(error);
|
|
264
|
+
return json && json !== '{}' && json !== '[]' ? shorten(json) : undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function isStructured(body: unknown): boolean {
|
|
268
|
+
return typeof body === 'object' && body !== null && !(body instanceof Blob);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const REQUEST_ID_FALLBACKS: ReadonlyArray<string> = ['x-correlation-id', 'cf-ray'];
|
|
272
|
+
|
|
273
|
+
const REQUEST_ID_NAME = /(?:^|-)request-?id$/;
|
|
274
|
+
|
|
275
|
+
export function requestIdOf(response: Response): string | undefined {
|
|
276
|
+
let named: string | undefined;
|
|
277
|
+
response.headers.forEach((value, name) => {
|
|
278
|
+
if (!named && value && REQUEST_ID_NAME.test(name)) named = value;
|
|
279
|
+
});
|
|
280
|
+
if (named) return named;
|
|
281
|
+
for (const header of REQUEST_ID_FALLBACKS) {
|
|
282
|
+
const value = response.headers.get(header);
|
|
283
|
+
if (value) return value;
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** A failure the API answered with, so there is a status and a reply to read. */
|
|
289
|
+
export class APIError<TBody = FailureBody> extends ElmoError {
|
|
290
|
+
/**
|
|
291
|
+
* The decoded failure body. `undefined` where the reply carried none, or
|
|
292
|
+
* carried text such as a proxy's page, which is in {@link APIError.text}.
|
|
293
|
+
*/
|
|
294
|
+
readonly body: TBody | undefined;
|
|
295
|
+
/** The API's own error code, read from the failure body. `undefined` where it carried none. */
|
|
296
|
+
readonly code: string | number | undefined;
|
|
297
|
+
readonly headers: Headers;
|
|
298
|
+
/** The request id to quote when reporting this failure. */
|
|
299
|
+
readonly requestId: string | undefined;
|
|
300
|
+
/**
|
|
301
|
+
* The reply that carried this failure. Its body is already read, so take it
|
|
302
|
+
* from {@link APIError.body} or {@link APIError.text}.
|
|
303
|
+
*/
|
|
304
|
+
readonly response: Response;
|
|
305
|
+
/** Milliseconds the API asked to wait before trying again. */
|
|
306
|
+
readonly retryAfterMs: number | undefined;
|
|
307
|
+
readonly status: number;
|
|
308
|
+
/**
|
|
309
|
+
* The failure body where the API sent text rather than JSON, such as a
|
|
310
|
+
* proxy's page. The message holds only its start.
|
|
311
|
+
*/
|
|
312
|
+
readonly text: string | undefined;
|
|
313
|
+
|
|
314
|
+
constructor(status: number, body: unknown, response: Response, note?: string) {
|
|
315
|
+
const reason = response.statusText ? `${status} ${response.statusText}` : `${status}`;
|
|
316
|
+
const requestId = requestIdOf(response);
|
|
317
|
+
const url = response.url ? ` for "${safeAddress(response.url)}"` : '';
|
|
318
|
+
const where = requestId ? `${url} (request id "${requestId}")` : url;
|
|
319
|
+
const said = describeStatedFailure(body);
|
|
320
|
+
const tail = note
|
|
321
|
+
? `. ${note}${said ? ` The API said: ${said}` : ''}`
|
|
322
|
+
: said
|
|
323
|
+
? `: ${said}`
|
|
324
|
+
: '.';
|
|
325
|
+
super(`The API answered ${reason}${where}${tail}`);
|
|
326
|
+
this.body = isStructured(body) ? (body as TBody) : undefined;
|
|
327
|
+
this.code = describeCode(body);
|
|
328
|
+
this.headers = response.headers;
|
|
329
|
+
this.name = 'APIError';
|
|
330
|
+
this.requestId = requestId;
|
|
331
|
+
this.response = response;
|
|
332
|
+
this.retryAfterMs = retryAfterDelay(response);
|
|
333
|
+
this.status = status;
|
|
334
|
+
this.text = typeof body === 'string' ? body : undefined;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export class UnsupportedInteractionError extends ElmoError {
|
|
339
|
+
readonly interaction: Interaction;
|
|
340
|
+
readonly transport: string;
|
|
341
|
+
|
|
342
|
+
constructor(transport: string, interaction: Interaction) {
|
|
343
|
+
super(`Transport "${transport}" cannot send this call. It has no "${interaction}" method.`);
|
|
344
|
+
this.interaction = interaction;
|
|
345
|
+
this.name = 'UnsupportedInteractionError';
|
|
346
|
+
this.transport = transport;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function unansweredError(error: unknown): ElmoError {
|
|
351
|
+
if (error instanceof ElmoError) return error;
|
|
352
|
+
const kind = classifyError(error);
|
|
353
|
+
if (kind === 'abort') return new AbortError(error);
|
|
354
|
+
if (kind === 'timeout') return new TimeoutError(undefined, error);
|
|
355
|
+
return new TransportError(failureOf(error));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function unsendableHeader(name: string) {
|
|
359
|
+
return new ElmoError(unsendable(`Header \`${name}\``));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function unsendableAddress(address: string, problem: string, fix: string) {
|
|
363
|
+
return new ElmoError(`"${address}" ${problem}, so the request was not sent. ${fix}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** A reply from the API that this SDK could not read, whatever its status. */
|
|
367
|
+
export class DecodeError extends ElmoError {
|
|
368
|
+
/** Where the value sat, or absent where the whole reply failed. */
|
|
369
|
+
readonly at: string | undefined;
|
|
370
|
+
/** The request id to quote when reporting this failure. */
|
|
371
|
+
readonly requestId: string | undefined;
|
|
372
|
+
/** The reply. Its body is already read. */
|
|
373
|
+
readonly response: Response | undefined;
|
|
374
|
+
/** The reply's status. `undefined` where there is no {@link DecodeError.response}. */
|
|
375
|
+
readonly status: number | undefined;
|
|
376
|
+
/** What the API sent. The message holds only a short form of it. */
|
|
377
|
+
readonly value: unknown;
|
|
378
|
+
|
|
379
|
+
constructor(
|
|
380
|
+
message: string,
|
|
381
|
+
options: {
|
|
382
|
+
at?: string | undefined;
|
|
383
|
+
cause?: unknown;
|
|
384
|
+
response?: Response | undefined;
|
|
385
|
+
value?: unknown;
|
|
386
|
+
} = {},
|
|
387
|
+
) {
|
|
388
|
+
const requestId = options.response && requestIdOf(options.response);
|
|
389
|
+
super(requestId ? `${message} The request id is "${requestId}".` : message, {
|
|
390
|
+
cause: options.cause,
|
|
391
|
+
});
|
|
392
|
+
this.at = options.at;
|
|
393
|
+
this.name = 'DecodeError';
|
|
394
|
+
this.requestId = requestId;
|
|
395
|
+
this.response = options.response;
|
|
396
|
+
this.status = options.response?.status;
|
|
397
|
+
this.value = options.value;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function emptyPathParameter(name: string, url: string) {
|
|
402
|
+
return new ElmoError(
|
|
403
|
+
`Path parameter \`${name}\` is empty. \`${url}\` cannot be sent without it.`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function sent(value: unknown): string {
|
|
408
|
+
if (value === undefined) return 'nothing';
|
|
409
|
+
if (typeof value === 'string') return `"${shorten(value)}"`;
|
|
410
|
+
if (value === null || typeof value !== 'object') return shorten(String(value));
|
|
411
|
+
try {
|
|
412
|
+
return shorten(JSON.stringify(value) ?? String(value));
|
|
413
|
+
} catch {
|
|
414
|
+
return shorten(String(value));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function unreadableValue(
|
|
419
|
+
at: string | undefined,
|
|
420
|
+
value: unknown,
|
|
421
|
+
cause?: unknown,
|
|
422
|
+
response?: Response,
|
|
423
|
+
): DecodeError {
|
|
424
|
+
const where = at === undefined ? 'the reply' : `\`${at}\` from the reply`;
|
|
425
|
+
return new DecodeError(`Could not read ${where}: the API sent ${sent(value)}.`, {
|
|
426
|
+
at,
|
|
427
|
+
cause,
|
|
428
|
+
response,
|
|
429
|
+
value,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CallableDescriptor,
|
|
3
|
+
Feature,
|
|
4
|
+
FeatureContext,
|
|
5
|
+
PreparedRequest,
|
|
6
|
+
ResolvedOptions,
|
|
7
|
+
Result,
|
|
8
|
+
Send,
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
export class FeatureRunner {
|
|
12
|
+
private readonly features: ReadonlyArray<Feature>;
|
|
13
|
+
|
|
14
|
+
constructor(features: ReadonlyArray<Feature> = []) {
|
|
15
|
+
this.features = features;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async options(
|
|
19
|
+
options: ResolvedOptions,
|
|
20
|
+
callable: CallableDescriptor,
|
|
21
|
+
ctx: FeatureContext,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
for (const feature of this.features) await feature.onOptions?.(options, callable, ctx);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async prepare(request: PreparedRequest, ctx: FeatureContext): Promise<void> {
|
|
27
|
+
for (const feature of this.features) await feature.onPrepare?.(request, ctx);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async request(request: PreparedRequest, ctx: FeatureContext): Promise<void> {
|
|
31
|
+
for (const feature of this.features) await feature.onRequest?.(request, ctx);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
send(core: Send, ctx: FeatureContext, hook: 'onOpen' | 'onSend' = 'onSend'): Send {
|
|
35
|
+
let next = core;
|
|
36
|
+
for (let i = this.features.length - 1; i >= 0; i--) {
|
|
37
|
+
const feature = this.features[i]!;
|
|
38
|
+
if (!feature[hook]) continue;
|
|
39
|
+
const downstream = next;
|
|
40
|
+
next = (request) => feature[hook]!(request, downstream, ctx);
|
|
41
|
+
}
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async result(result: Result, request: PreparedRequest, ctx: FeatureContext): Promise<Result> {
|
|
46
|
+
let current = result;
|
|
47
|
+
for (let i = this.features.length - 1; i >= 0; i--) {
|
|
48
|
+
current = (await this.features[i]!.onResult?.(current, request, ctx)) ?? current;
|
|
49
|
+
}
|
|
50
|
+
return current;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async error(error: unknown, request: PreparedRequest, ctx: FeatureContext): Promise<unknown> {
|
|
54
|
+
let current = error;
|
|
55
|
+
for (let i = this.features.length - 1; i >= 0; i--) {
|
|
56
|
+
current = (await this.features[i]!.onError?.(current, request, ctx)) ?? current;
|
|
57
|
+
}
|
|
58
|
+
return current;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export abstract class LazyPromise<T> extends Promise<T> {
|
|
2
|
+
private answer: Promise<T> | undefined;
|
|
3
|
+
|
|
4
|
+
constructor() {
|
|
5
|
+
super(() => {});
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
protected abstract settle(): Promise<T>;
|
|
9
|
+
|
|
10
|
+
private answered(): Promise<T> {
|
|
11
|
+
return (this.answer ??= this.settle());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override then<TResolved = T, TCaught = never>(
|
|
15
|
+
onFulfilled?: ((value: T) => PromiseLike<TResolved> | TResolved) | null,
|
|
16
|
+
onRejected?: ((reason: unknown) => PromiseLike<TCaught> | TCaught) | null,
|
|
17
|
+
): Promise<TCaught | TResolved> {
|
|
18
|
+
return this.answered().then(onFulfilled, onRejected);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override catch<TCaught = never>(
|
|
22
|
+
onRejected?: ((reason: unknown) => PromiseLike<TCaught> | TCaught) | null,
|
|
23
|
+
): Promise<T | TCaught> {
|
|
24
|
+
return this.answered().catch(onRejected);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override finally(onFinally?: (() => void) | null): Promise<T> {
|
|
28
|
+
return this.answered().finally(onFinally);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static override get [Symbol.species](): PromiseConstructor {
|
|
32
|
+
return Promise;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function unattended<T>(promise: Promise<T>): Promise<T> {
|
|
37
|
+
promise.catch(() => {});
|
|
38
|
+
return promise;
|
|
39
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { scalarText, toText } from './text';
|
|
2
|
+
import type { Metadata, MetadataInput, MetadataValue } from './types';
|
|
3
|
+
|
|
4
|
+
function entriesOf(source: MetadataInput): Iterable<readonly [string, unknown]> {
|
|
5
|
+
if (typeof (source as Iterable<unknown>)[Symbol.iterator] === 'function') {
|
|
6
|
+
return source as Iterable<readonly [string, string]>;
|
|
7
|
+
}
|
|
8
|
+
return Object.entries(source);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function metadataText(value: unknown): string {
|
|
12
|
+
if (Array.isArray(value)) return value.map(toText).join(',');
|
|
13
|
+
if (typeof value !== 'object' || value === null || scalarText(value) !== undefined) {
|
|
14
|
+
return toText(value);
|
|
15
|
+
}
|
|
16
|
+
const pairs: Array<string> = [];
|
|
17
|
+
for (const [key, item] of Object.entries(value)) {
|
|
18
|
+
if (item !== undefined && item !== null) pairs.push(key, toText(item));
|
|
19
|
+
}
|
|
20
|
+
return pairs.join(',');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function mergeMetadata(...sources: ReadonlyArray<MetadataInput | undefined>): Metadata {
|
|
24
|
+
const merged: Record<string, MetadataValue> = {};
|
|
25
|
+
for (const source of sources) {
|
|
26
|
+
if (!source) continue;
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
for (const [key, value] of entriesOf(source)) {
|
|
29
|
+
const name = key.toLowerCase();
|
|
30
|
+
if (value === undefined) continue;
|
|
31
|
+
if (value === null) {
|
|
32
|
+
delete merged[name];
|
|
33
|
+
seen.add(name);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const text = metadataText(value);
|
|
37
|
+
const held = seen.has(name) ? merged[name] : undefined;
|
|
38
|
+
seen.add(name);
|
|
39
|
+
if (held === undefined) merged[name] = text;
|
|
40
|
+
else merged[name] = Array.isArray(held) ? [...held, text] : [held, text];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return merged;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const FIELD_VALUE = /^[\t\x20-\x7e\x80-\xff]*$/;
|
|
47
|
+
|
|
48
|
+
export function isFieldValue(value: string): boolean {
|
|
49
|
+
return FIELD_VALUE.test(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function metadataPairs(meta: Metadata): Array<[string, string]> {
|
|
53
|
+
const pairs: Array<[string, string]> = [];
|
|
54
|
+
for (const [name, value] of Object.entries(meta)) {
|
|
55
|
+
if (value === undefined) continue;
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
for (const item of value) pairs.push([name, item]);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
pairs.push([name, value]);
|
|
61
|
+
}
|
|
62
|
+
return pairs;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function unsendableName(meta: Metadata): string | undefined {
|
|
66
|
+
for (const [name, value] of metadataPairs(meta)) if (!isFieldValue(value)) return name;
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function metadataHeaders(meta: Metadata): Headers {
|
|
71
|
+
const headers = new Headers();
|
|
72
|
+
for (const [name, value] of metadataPairs(meta)) headers.append(name, value);
|
|
73
|
+
return headers;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function metadataValue(value: MetadataValue): string | undefined {
|
|
77
|
+
if (value === undefined) return undefined;
|
|
78
|
+
return Array.isArray(value) ? value.join(', ') : value;
|
|
79
|
+
}
|