@allisall/sdk 0.1.0 → 0.2.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.
Files changed (60) hide show
  1. package/README.md +142 -4
  2. package/dist/client.d.ts +81 -37
  3. package/dist/client.js +136 -73
  4. package/dist/generated/openapi.d.ts +7724 -0
  5. package/dist/generated/openapi.js +4 -0
  6. package/dist/generated/operations.d.ts +37 -0
  7. package/dist/generated/operations.js +976 -0
  8. package/dist/index.d.ts +7 -2
  9. package/dist/index.js +1 -0
  10. package/dist/ingest.d.ts +1 -1
  11. package/dist/ingest.js +2 -2
  12. package/dist/money.d.ts +57 -0
  13. package/dist/money.js +284 -0
  14. package/dist/multipart.d.ts +27 -0
  15. package/dist/multipart.js +48 -0
  16. package/dist/options.d.ts +50 -0
  17. package/dist/options.js +73 -0
  18. package/dist/pagination.d.ts +55 -0
  19. package/dist/pagination.js +85 -0
  20. package/dist/request.d.ts +117 -0
  21. package/dist/request.js +363 -0
  22. package/dist/resources/agents.d.ts +51 -0
  23. package/dist/resources/agents.js +65 -0
  24. package/dist/resources/analytics.d.ts +42 -0
  25. package/dist/resources/analytics.js +68 -0
  26. package/dist/resources/api-keys.d.ts +54 -0
  27. package/dist/resources/api-keys.js +63 -0
  28. package/dist/resources/calculations.d.ts +78 -0
  29. package/dist/resources/calculations.js +111 -0
  30. package/dist/resources/clients.d.ts +65 -0
  31. package/dist/resources/clients.js +85 -0
  32. package/dist/resources/cost.d.ts +69 -0
  33. package/dist/resources/cost.js +89 -0
  34. package/dist/resources/dashboard.d.ts +24 -0
  35. package/dist/resources/dashboard.js +28 -0
  36. package/dist/resources/events.d.ts +47 -0
  37. package/dist/resources/events.js +69 -0
  38. package/dist/resources/health.d.ts +14 -0
  39. package/dist/resources/health.js +17 -0
  40. package/dist/resources/imports.d.ts +33 -0
  41. package/dist/resources/imports.js +37 -0
  42. package/dist/resources/outcome-types.d.ts +25 -0
  43. package/dist/resources/outcome-types.js +33 -0
  44. package/dist/resources/reports.d.ts +77 -0
  45. package/dist/resources/reports.js +103 -0
  46. package/dist/resources/value-models.d.ts +92 -0
  47. package/dist/resources/value-models.js +126 -0
  48. package/dist/runtime.d.ts +2 -0
  49. package/dist/runtime.js +2 -1
  50. package/dist/share.d.ts +52 -0
  51. package/dist/share.js +60 -0
  52. package/dist/transport.d.ts +54 -5
  53. package/dist/transport.js +81 -32
  54. package/dist/types.d.ts +135 -2
  55. package/dist/types.js +12 -3
  56. package/dist/unicode.d.ts +2 -0
  57. package/dist/unicode.js +11 -0
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/package.json +4 -3
@@ -0,0 +1,92 @@
1
+ import type { MoneyInput } from "../money.js";
2
+ import type { IdempotentRequestOptions, RequestOptions } from "../options.js";
3
+ import type { PagePromise } from "../pagination.js";
4
+ import type { TransportConfig } from "../transport.js";
5
+ import type { ListValueModelsQuery, ListValueTemplatesQuery, ValueLineItemsSet, ValueModel, ValueModelCreate, ValueModelList, ValueModelPatch, ValueModelSummary, ValueTemplate, ValueTemplateList } from "../types.js";
6
+ /** The `valueModels` operations. */
7
+ export declare class ValueModels {
8
+ #private;
9
+ constructor(config: TransportConfig);
10
+ /**
11
+ * Lists value models. Await the result for one page; iterate it with
12
+ * `for await` for every value model of every page.
13
+ *
14
+ * Needs the `read` scope. Operation `listValueModels`.
15
+ */
16
+ list(query?: ListValueModelsQuery, options?: RequestOptions): PagePromise<ValueModelSummary, ValueModelList>;
17
+ /**
18
+ * Reads a value model by its own id: any model in the workspace,
19
+ * archived ones included.
20
+ *
21
+ * Needs the `read` scope. Operation `getValueModel`.
22
+ *
23
+ * @param id - the value model's id, not its client's.
24
+ */
25
+ get(id: string, options?: RequestOptions): Promise<ValueModel>;
26
+ /**
27
+ * Reads a client's active value model.
28
+ *
29
+ * Needs the `read` scope. Operation `getClientValueModel`.
30
+ *
31
+ * @param clientId - the client's id.
32
+ */
33
+ getForClient(clientId: string, options?: RequestOptions): Promise<ValueModel>;
34
+ /**
35
+ * Creates a client's value model.
36
+ *
37
+ * Every attempt of the call sends one `Idempotency-Key`, so a retry
38
+ * cannot create a second value model.
39
+ *
40
+ * Needs the `write` scope. Operation `createValueModel`.
41
+ *
42
+ * @param clientId - the client's id.
43
+ */
44
+ createForClient(clientId: string, body: ValueModelCreate, options?: IdempotentRequestOptions): Promise<ValueModel>;
45
+ /**
46
+ * Updates a client's active value model with a merge patch: a member
47
+ * left out keeps its value, `null` clears it, and any other value sets
48
+ * it.
49
+ *
50
+ * Never retried.
51
+ *
52
+ * Needs the `write` scope. Operation `updateValueModel`.
53
+ *
54
+ * @param clientId - the client's id.
55
+ */
56
+ updateForClient(clientId: string, patch: ValueModelPatch, options?: RequestOptions): Promise<ValueModel>;
57
+ /**
58
+ * Deletes a client's active value model.
59
+ *
60
+ * A retry after a lost response can answer `404` for a delete that
61
+ * succeeded.
62
+ *
63
+ * Needs the `write` scope. Operation `deleteValueModel`.
64
+ *
65
+ * @param clientId - the client's id.
66
+ */
67
+ deleteForClient(clientId: string, options?: RequestOptions): Promise<void>;
68
+ /**
69
+ * Replaces the line items of a client's active value model, as a whole
70
+ * set. It is guarded by `version`: a stale version is refused rather
71
+ * than overwriting a co-editor's change.
72
+ *
73
+ * Each `unit_value` takes a `Decimal` or a `number`.
74
+ *
75
+ * Never retried, because the `version` guard refuses a repeat of a write
76
+ * that already landed as a stale version. On a `ConnectionError` the
77
+ * write may have landed: read the value model again for its current
78
+ * `version` before trying again.
79
+ *
80
+ * Needs the `write` scope. Operation `setValueModelLineItems`.
81
+ *
82
+ * @param clientId - the client's id.
83
+ */
84
+ setLineItemsForClient(clientId: string, body: MoneyInput<ValueLineItemsSet>, options?: RequestOptions): Promise<ValueModel>;
85
+ /**
86
+ * Lists value templates. Await the result for one page; iterate it with
87
+ * `for await` for every template of every page.
88
+ *
89
+ * Needs the `read` scope. Operation `listValueTemplates`.
90
+ */
91
+ listTemplates(query?: ListValueTemplatesQuery, options?: RequestOptions): PagePromise<ValueTemplate, ValueTemplateList>;
92
+ }
@@ -0,0 +1,126 @@
1
+ // The `valueModels` operations: value models, read by their own id or
2
+ // through the client they belong to, and value templates.
3
+ import { call, callPage } from "../request.js";
4
+ /** The `valueModels` operations. */
5
+ export class ValueModels {
6
+ #config;
7
+ constructor(config) {
8
+ this.#config = config;
9
+ }
10
+ /**
11
+ * Lists value models. Await the result for one page; iterate it with
12
+ * `for await` for every value model of every page.
13
+ *
14
+ * Needs the `read` scope. Operation `listValueModels`.
15
+ */
16
+ list(query, options) {
17
+ return callPage(this.#config, "listValueModels", { query, options });
18
+ }
19
+ /**
20
+ * Reads a value model by its own id: any model in the workspace,
21
+ * archived ones included.
22
+ *
23
+ * Needs the `read` scope. Operation `getValueModel`.
24
+ *
25
+ * @param id - the value model's id, not its client's.
26
+ */
27
+ get(id, options) {
28
+ return call(this.#config, "getValueModel", { path: { id }, options });
29
+ }
30
+ /**
31
+ * Reads a client's active value model.
32
+ *
33
+ * Needs the `read` scope. Operation `getClientValueModel`.
34
+ *
35
+ * @param clientId - the client's id.
36
+ */
37
+ getForClient(clientId, options) {
38
+ return call(this.#config, "getClientValueModel", {
39
+ path: { id: clientId },
40
+ options,
41
+ });
42
+ }
43
+ /**
44
+ * Creates a client's value model.
45
+ *
46
+ * Every attempt of the call sends one `Idempotency-Key`, so a retry
47
+ * cannot create a second value model.
48
+ *
49
+ * Needs the `write` scope. Operation `createValueModel`.
50
+ *
51
+ * @param clientId - the client's id.
52
+ */
53
+ createForClient(clientId, body, options) {
54
+ return call(this.#config, "createValueModel", {
55
+ path: { id: clientId },
56
+ body,
57
+ options,
58
+ });
59
+ }
60
+ /**
61
+ * Updates a client's active value model with a merge patch: a member
62
+ * left out keeps its value, `null` clears it, and any other value sets
63
+ * it.
64
+ *
65
+ * Never retried.
66
+ *
67
+ * Needs the `write` scope. Operation `updateValueModel`.
68
+ *
69
+ * @param clientId - the client's id.
70
+ */
71
+ updateForClient(clientId, patch, options) {
72
+ return call(this.#config, "updateValueModel", {
73
+ path: { id: clientId },
74
+ body: patch,
75
+ options,
76
+ });
77
+ }
78
+ /**
79
+ * Deletes a client's active value model.
80
+ *
81
+ * A retry after a lost response can answer `404` for a delete that
82
+ * succeeded.
83
+ *
84
+ * Needs the `write` scope. Operation `deleteValueModel`.
85
+ *
86
+ * @param clientId - the client's id.
87
+ */
88
+ deleteForClient(clientId, options) {
89
+ return call(this.#config, "deleteValueModel", {
90
+ path: { id: clientId },
91
+ options,
92
+ });
93
+ }
94
+ /**
95
+ * Replaces the line items of a client's active value model, as a whole
96
+ * set. It is guarded by `version`: a stale version is refused rather
97
+ * than overwriting a co-editor's change.
98
+ *
99
+ * Each `unit_value` takes a `Decimal` or a `number`.
100
+ *
101
+ * Never retried, because the `version` guard refuses a repeat of a write
102
+ * that already landed as a stale version. On a `ConnectionError` the
103
+ * write may have landed: read the value model again for its current
104
+ * `version` before trying again.
105
+ *
106
+ * Needs the `write` scope. Operation `setValueModelLineItems`.
107
+ *
108
+ * @param clientId - the client's id.
109
+ */
110
+ setLineItemsForClient(clientId, body, options) {
111
+ return call(this.#config, "setValueModelLineItems", {
112
+ path: { id: clientId },
113
+ body,
114
+ options,
115
+ });
116
+ }
117
+ /**
118
+ * Lists value templates. Await the result for one page; iterate it with
119
+ * `for await` for every template of every page.
120
+ *
121
+ * Needs the `read` scope. Operation `listValueTemplates`.
122
+ */
123
+ listTemplates(query, options) {
124
+ return callPage(this.#config, "listValueTemplates", { query, options });
125
+ }
126
+ }
package/dist/runtime.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /** Reads a global by name, so a runtime without it is never touched. */
2
+ export declare function globalValue(name: string): unknown;
1
3
  /**
2
4
  * Reports whether the SDK runs in a browser: a window, where `document`
3
5
  * is defined, or a Web Worker, where `globalThis` is an instance of
package/dist/runtime.js CHANGED
@@ -3,7 +3,8 @@
3
3
  // `globalThis` by name, so a runtime without them, such as a browser
4
4
  // without `process`, is never touched.
5
5
  import { VERSION } from "./version.js";
6
- function globalValue(name) {
6
+ /** Reads a global by name, so a runtime without it is never touched. */
7
+ export function globalValue(name) {
7
8
  const value = Reflect.get(globalThis, name);
8
9
  return value;
9
10
  }
@@ -0,0 +1,52 @@
1
+ import { type FetchLike } from "./transport.js";
2
+ import type { PublicReport } from "./types.js";
3
+ /** The options of `getSharedReport`. Every one is optional. */
4
+ export interface SharedReportOptions {
5
+ /**
6
+ * The API's base URL, absolute, `http` or `https`. Defaults to
7
+ * `AIA_BASE_URL` from the environment, then `https://api.allisai.ai`.
8
+ * In a browser it may also be a path on the page's own origin, starting
9
+ * with a single `/`.
10
+ */
11
+ baseURL?: string | undefined;
12
+ /** The timeout of each attempt, in milliseconds. Defaults to 60,000. */
13
+ timeout?: number | undefined;
14
+ /** How many times a failure worth repeating is retried. Defaults to 2; 0 turns retries off. */
15
+ maxRetries?: number | undefined;
16
+ /** The `fetch` to send the request with. Defaults to the platform's own. */
17
+ fetch?: FetchLike | undefined;
18
+ /** Cancels the call, including any wait between retries. */
19
+ signal?: AbortSignal | undefined;
20
+ /**
21
+ * Extra headers to send. They may not name `Authorization`,
22
+ * `Content-Type`, `Idempotency-Key` or `X-Request-Id`, which the SDK sets.
23
+ */
24
+ headers?: Readonly<Record<string, string>> | undefined;
25
+ }
26
+ /**
27
+ * Reads a shared report with its share token, as the report's recipient
28
+ * sees it. Needs no API key and no session, and never reads `AIA_API_KEY`.
29
+ *
30
+ * A share token is a bearer capability: anyone holding it can read the
31
+ * report. Handle it like a secret, and keep it out of URLs and logs. The
32
+ * SDK sends it only in the `Authorization` header, as
33
+ * `Authorization: Report <token>`, and keeps it for this call alone.
34
+ *
35
+ * Every failure, a missing, unknown or revoked token included, is the same
36
+ * `404`, as an `ApiError`. Each read stamps the report's view times, and a
37
+ * retry after a lost response stamps them again.
38
+ *
39
+ * Operation `getSharedReport`.
40
+ *
41
+ * @param shareToken - the report's share token.
42
+ * @throws {AllisallError} when the share token is empty or holds a
43
+ * character other than printable ASCII, or an option is one a client would
44
+ * refuse, before anything is sent.
45
+ * @throws {TypeError} when `options.headers` is not a plain object of
46
+ * header names and string values, or names a header the SDK sets, before
47
+ * anything is sent.
48
+ * @throws {ApiError} for an error response, such as the `404` for a token
49
+ * that reads no report.
50
+ * @throws {ConnectionError} when no response arrived, after any retries.
51
+ */
52
+ export declare function getSharedReport(shareToken: string, options?: SharedReportOptions): Promise<PublicReport>;
package/dist/share.js ADDED
@@ -0,0 +1,60 @@
1
+ // Reading a shared report with its share token. It is a function, not a
2
+ // client method: its caller, such as a server rendering the report's page,
3
+ // holds no API key and no session, which a client requires.
4
+ import { transportSettings } from "./client.js";
5
+ import { AllisallError } from "./errors.js";
6
+ import { request } from "./request.js";
7
+ import { isBrowser } from "./runtime.js";
8
+ import { isPrintableCredential, } from "./transport.js";
9
+ /** Trims a share token and checks it can travel in a header. */
10
+ function checkedShareToken(shareToken) {
11
+ const token = typeof shareToken === "string" ? shareToken.trim() : "";
12
+ if (token === "") {
13
+ throw new AllisallError("allisall: the share token is empty");
14
+ }
15
+ if (!isPrintableCredential(token)) {
16
+ throw new AllisallError("allisall: the share token holds a character other than printable ASCII, which cannot be sent");
17
+ }
18
+ return token;
19
+ }
20
+ /**
21
+ * Reads a shared report with its share token, as the report's recipient
22
+ * sees it. Needs no API key and no session, and never reads `AIA_API_KEY`.
23
+ *
24
+ * A share token is a bearer capability: anyone holding it can read the
25
+ * report. Handle it like a secret, and keep it out of URLs and logs. The
26
+ * SDK sends it only in the `Authorization` header, as
27
+ * `Authorization: Report <token>`, and keeps it for this call alone.
28
+ *
29
+ * Every failure, a missing, unknown or revoked token included, is the same
30
+ * `404`, as an `ApiError`. Each read stamps the report's view times, and a
31
+ * retry after a lost response stamps them again.
32
+ *
33
+ * Operation `getSharedReport`.
34
+ *
35
+ * @param shareToken - the report's share token.
36
+ * @throws {AllisallError} when the share token is empty or holds a
37
+ * character other than printable ASCII, or an option is one a client would
38
+ * refuse, before anything is sent.
39
+ * @throws {TypeError} when `options.headers` is not a plain object of
40
+ * header names and string values, or names a header the SDK sets, before
41
+ * anything is sent.
42
+ * @throws {ApiError} for an error response, such as the `404` for a token
43
+ * that reads no report.
44
+ * @throws {ConnectionError} when no response arrived, after any retries.
45
+ */
46
+ export async function getSharedReport(shareToken, options = {}) {
47
+ const token = checkedShareToken(shareToken);
48
+ const config = {
49
+ ...transportSettings(options, isBrowser()),
50
+ // The share token is the call's only credential, and it is not an API
51
+ // key, so no browser opt-in applies. No client sends default headers.
52
+ credential: undefined,
53
+ refuseApiKeys: false,
54
+ defaultHeaders: {},
55
+ };
56
+ return request(config, "getSharedReport", {
57
+ shareToken: token,
58
+ options: { signal: options.signal, headers: options.headers },
59
+ });
60
+ }
@@ -14,7 +14,11 @@ export type Credential = {
14
14
  /** Everything an attempt needs, fixed when the client is constructed. */
15
15
  export interface TransportConfig {
16
16
  readonly baseURL: URL;
17
- readonly credential: Credential;
17
+ /**
18
+ * The client's credential. Absent for a shared report, whose call carries
19
+ * its own share token and sends no client credential.
20
+ */
21
+ readonly credential: Credential | undefined;
18
22
  readonly timeout: number;
19
23
  readonly maxRetries: number;
20
24
  readonly fetch: FetchLike | undefined;
@@ -22,6 +26,8 @@ export interface TransportConfig {
22
26
  readonly refuseApiKeys: boolean;
23
27
  /** The `User-Agent` to send; `undefined` in a browser. */
24
28
  readonly userAgent: string | undefined;
29
+ /** Headers sent on every request, already checked. */
30
+ readonly defaultHeaders: Readonly<Record<string, string>>;
25
31
  }
26
32
  /** The message of the error refusing an API key in a browser. */
27
33
  export declare const BROWSER_KEY_MESSAGE: string;
@@ -46,8 +52,51 @@ export declare function isPrintableCredential(credential: string): boolean;
46
52
  */
47
53
  export declare function readProblem(contentType: string | null, text: string): Problem | undefined;
48
54
  /**
49
- * Posts a JSON body, retrying a failure worth repeating. The body is the
50
- * same bytes on every attempt. Each attempt reads a fresh token and sends
51
- * a fresh `X-Request-Id`.
55
+ * How a successful body is read. `undefined` means the body was not the
56
+ * expected shape, which is an `ApiError` carrying the status.
52
57
  */
53
- export declare function post<T>(config: TransportConfig, path: string, body: Uint8Array, read: (body: unknown) => T | undefined, signal: AbortSignal | undefined): Promise<T>;
58
+ export type ReadBody<T> = (text: string) => {
59
+ readonly value: T;
60
+ } | undefined;
61
+ /**
62
+ * The `Authorization` a request sends: the client's credential as a bearer,
63
+ * none, or a share token under the `Report` scheme.
64
+ */
65
+ export type Authorization = "bearer" | "none" | {
66
+ readonly report: string;
67
+ };
68
+ /** One request, fixed before the first attempt and sent unchanged on each. */
69
+ export interface Outgoing {
70
+ readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
71
+ readonly url: string;
72
+ readonly authorization: Authorization;
73
+ /** The body: bytes, or a form whose boundary `fetch` writes. */
74
+ readonly body: Uint8Array | FormData | undefined;
75
+ /** The body's `Content-Type`; `undefined` for no body, or a form. */
76
+ readonly contentType: string | undefined;
77
+ /** The `Idempotency-Key` sent on every attempt, if any. */
78
+ readonly idempotencyKey: string | undefined;
79
+ }
80
+ /** What one call may do, from its operation and its options. */
81
+ export interface CallPolicy {
82
+ readonly timeout: number;
83
+ readonly maxRetries: number;
84
+ /** The call's headers, already checked. */
85
+ readonly headers: Readonly<Record<string, string>>;
86
+ readonly signal: AbortSignal | undefined;
87
+ /** Whether a retry cannot apply the request twice, so it may be retried at all. */
88
+ readonly eligible: boolean;
89
+ /** Whether a `409` `idempotency_key_in_use` is retried: the request carries a reused key. */
90
+ readonly retryKeyInUse: boolean;
91
+ }
92
+ /**
93
+ * Sends a request, retrying a failure worth repeating when the request is
94
+ * eligible. The body is the same on every attempt. Each attempt reads a
95
+ * fresh token and sends a fresh `X-Request-Id`.
96
+ */
97
+ export declare function send<T>(config: TransportConfig, out: Outgoing, read: ReadBody<T>, policy: CallPolicy): Promise<T>;
98
+ /**
99
+ * Posts a JSON body that a retry cannot apply twice, such as an ingest
100
+ * batch, retrying a failure worth repeating.
101
+ */
102
+ export declare function post<T>(config: TransportConfig, path: string, body: Uint8Array, read: (body: unknown) => T | undefined, policy: Omit<CallPolicy, "eligible" | "retryKeyInUse">): Promise<T>;
package/dist/transport.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // One HTTP attempt (headers, timeout, reading the response) and the loop
2
- // that repeats it under the retry rules in retry.ts.
2
+ // that repeats it under the retry rules in retry.ts, for any method and
3
+ // any body.
3
4
  import { AllisallError, ApiError, ConnectionError } from "./errors.js";
5
+ import { mergeHeaders } from "./options.js";
4
6
  import { backoffMs, isRetryableStatus, MAX_RETRY_AFTER_MS, parseRetryAfter, sleep, } from "./retry.js";
5
7
  import { uuidv4 } from "./uuid.js";
6
8
  /** The message of the error refusing an API key in a browser. */
@@ -33,6 +35,9 @@ export function isPrintableCredential(credential) {
33
35
  }
34
36
  async function bearerToken(config) {
35
37
  const { credential } = config;
38
+ if (credential === undefined) {
39
+ throw new AllisallError("allisall: this call has no client credential");
40
+ }
36
41
  if (credential.kind === "apiKey")
37
42
  return credential.apiKey;
38
43
  const raw = await credential.provider();
@@ -121,33 +126,41 @@ function headerOrUndefined(headers, name) {
121
126
  return headers.get(name) ?? undefined;
122
127
  }
123
128
  /**
124
- * Sends one attempt. Resolves with the parsed `2xx` body; throws an
129
+ * Sends one attempt. Resolves with the read `2xx` body; throws an
125
130
  * `ApiError` for any other status or an unreadable `2xx` body, a
126
131
  * `ConnectionError` when no response arrived, or the caller's abort reason.
127
132
  */
128
- async function attempt(config, url, body, read, token, requestId, signal) {
129
- const headers = {
130
- Authorization: `Bearer ${token}`,
131
- "Content-Type": "application/json",
132
- "X-Request-Id": requestId,
133
- };
133
+ async function attempt(config, out, read, authorization, requestId, policy) {
134
+ const headers = {};
135
+ if (authorization !== undefined)
136
+ headers.Authorization = authorization;
137
+ if (out.contentType !== undefined)
138
+ headers["Content-Type"] = out.contentType;
139
+ headers["X-Request-Id"] = requestId;
134
140
  if (config.userAgent !== undefined)
135
141
  headers["User-Agent"] = config.userAgent;
136
- const timeout = AbortSignal.timeout(config.timeout);
142
+ if (out.idempotencyKey !== undefined) {
143
+ headers["Idempotency-Key"] = out.idempotencyKey;
144
+ }
145
+ mergeHeaders(headers, config.defaultHeaders);
146
+ mergeHeaders(headers, policy.headers);
147
+ const { signal } = policy;
148
+ const timeout = AbortSignal.timeout(policy.timeout);
137
149
  const combined = signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
138
150
  const doFetch = config.fetch ?? globalThis.fetch;
151
+ const init = {
152
+ method: out.method,
153
+ headers,
154
+ ...(out.body === undefined ? {} : { body: out.body }),
155
+ signal: combined,
156
+ // A redirect is never followed, so Authorization never travels to
157
+ // another location.
158
+ redirect: "manual",
159
+ };
139
160
  let response;
140
161
  let text;
141
162
  try {
142
- response = await doFetch(url, {
143
- method: "POST",
144
- headers,
145
- body,
146
- signal: combined,
147
- // A redirect is never followed, so Authorization never travels to
148
- // another location.
149
- redirect: "manual",
150
- });
163
+ response = await doFetch(out.url, init);
151
164
  text = await response.text();
152
165
  }
153
166
  catch (err) {
@@ -167,10 +180,9 @@ async function attempt(config, url, body, read, token, requestId, signal) {
167
180
  });
168
181
  }
169
182
  if (status >= 200 && status < 300) {
170
- const parsed = parseJSON(text);
171
- const value = parsed === undefined ? undefined : read(parsed);
172
- if (value !== undefined)
173
- return value;
183
+ const result = read(text);
184
+ if (result !== undefined)
185
+ return result.value;
174
186
  throw new ApiError({
175
187
  status,
176
188
  headers: response.headers,
@@ -186,24 +198,43 @@ async function attempt(config, url, body, read, token, requestId, signal) {
186
198
  headers: response.headers,
187
199
  });
188
200
  }
201
+ function worthRepeating(err, policy) {
202
+ if (err instanceof ConnectionError)
203
+ return true;
204
+ if (isRetryableStatus(err.status))
205
+ return true;
206
+ // The first request with this key is still running; a later attempt can
207
+ // collect its outcome.
208
+ return (policy.retryKeyInUse &&
209
+ err.status === 409 &&
210
+ err.code === "idempotency_key_in_use");
211
+ }
212
+ /** The `Authorization` value of one attempt, reading a fresh bearer token. */
213
+ async function authorizationOf(config, out) {
214
+ const { authorization } = out;
215
+ if (authorization === "none")
216
+ return undefined;
217
+ if (authorization === "bearer")
218
+ return `Bearer ${await bearerToken(config)}`;
219
+ return `Report ${authorization.report}`;
220
+ }
189
221
  /**
190
- * Posts a JSON body, retrying a failure worth repeating. The body is the
191
- * same bytes on every attempt. Each attempt reads a fresh token and sends
192
- * a fresh `X-Request-Id`.
222
+ * Sends a request, retrying a failure worth repeating when the request is
223
+ * eligible. The body is the same on every attempt. Each attempt reads a
224
+ * fresh token and sends a fresh `X-Request-Id`.
193
225
  */
194
- export async function post(config, path, body, read, signal) {
195
- const url = joinURL(config.baseURL, path);
226
+ export async function send(config, out, read, policy) {
227
+ const maxRetries = policy.eligible ? policy.maxRetries : 0;
196
228
  for (let retry = 0;; retry++) {
197
229
  // An error from the token provider ends the call unwrapped and unretried.
198
- const token = await bearerToken(config);
230
+ const authorization = await authorizationOf(config, out);
199
231
  try {
200
- return await attempt(config, url, body, read, token, uuidv4(), signal);
232
+ return await attempt(config, out, read, authorization, uuidv4(), policy);
201
233
  }
202
234
  catch (err) {
203
235
  if (!(err instanceof ApiError || err instanceof ConnectionError))
204
236
  throw err;
205
- const retryable = err instanceof ConnectionError || isRetryableStatus(err.status);
206
- if (!retryable || retry >= config.maxRetries)
237
+ if (!worthRepeating(err, policy) || retry >= maxRetries)
207
238
  throw err;
208
239
  let wait;
209
240
  if (err instanceof ApiError && err.retryAfter !== undefined) {
@@ -214,7 +245,25 @@ export async function post(config, path, body, read, signal) {
214
245
  else {
215
246
  wait = backoffMs(retry + 1);
216
247
  }
217
- await sleep(wait, signal);
248
+ await sleep(wait, policy.signal);
218
249
  }
219
250
  }
220
251
  }
252
+ /**
253
+ * Posts a JSON body that a retry cannot apply twice, such as an ingest
254
+ * batch, retrying a failure worth repeating.
255
+ */
256
+ export function post(config, path, body, read, policy) {
257
+ return send(config, {
258
+ method: "POST",
259
+ url: joinURL(config.baseURL, path),
260
+ authorization: "bearer",
261
+ body,
262
+ contentType: "application/json",
263
+ idempotencyKey: undefined,
264
+ }, (text) => {
265
+ const parsed = parseJSON(text);
266
+ const value = parsed === undefined ? undefined : read(parsed);
267
+ return value === undefined ? undefined : { value };
268
+ }, { ...policy, eligible: true, retryKeyInUse: false });
269
+ }