okengine 0.21.1 → 0.22.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/AGENTS.md +4 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +10 -0
- package/package.json +22 -20
- package/site/content/docs/ai/skills.mdx +1 -1
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.mdx +1 -1
- package/site/content/docs/elements/channel/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- package/site/content/docs/reference/configuration.mdx +2 -2
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +28 -15
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/index.ts +9 -1
- package/src/cli/load-config.images.test.ts +10 -10
- package/src/cli/load-config.ts +1 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +225 -0
- package/src/client/transport.ts +132 -19
- package/src/client/types.ts +29 -3
- package/src/compiler/effects-infer.ts +726 -16
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +113 -12
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/ui-next/dist/assets/{access-page-Bgt9bq2r.js → access-page-Ze6ckjdd.js} +2 -2
- package/src/console/ui-next/dist/assets/agent-disclosure-Bkohc_8X.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-rL0lHxat.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-Deo4PP18.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-CoJ6amHf.js} +1 -1
- package/src/console/ui-next/dist/assets/{copy-inline-button-CAYD18cr.js → copy-inline-button-CWG5_ZOh.js} +1 -1
- package/src/console/ui-next/dist/assets/{detail-header-DVWNjWjg.js → detail-header-b_ZzIsmU.js} +1 -1
- package/src/console/ui-next/dist/assets/{dropdown-menu-4h2LOVXM.js → dropdown-menu-nwXEO1Ac.js} +1 -1
- package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +1 -0
- package/src/console/ui-next/dist/assets/{explorer-empty-CJs5A-wm.js → explorer-empty-C8sSCqYT.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BMHs-IzK.js → flows-page-D2E7BtKK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-CPYBzDh0.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DTPevKmM.js} +1 -1
- package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +4 -0
- package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-BM27lXld.js} +3 -3
- package/src/console/ui-next/dist/assets/{request-meta-DMbnAe3f.js → request-meta-BjFT7DNl.js} +1 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +41 -0
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-Htk8Cm9t.js → trace-detail-sheet-Cmitq3da.js} +2 -2
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CFMPWX4f.js → tree-expand-toggle-CbDIB-7x.js} +2 -2
- package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-CT4-gajj.js → use-vault-list-Cs45Czkt.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-CBYl0LW_.js → vault-page-BOMr0go9.js} +2 -2
- package/src/console/ui-next/dist/index.html +2 -2
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +9 -0
- package/src/docker/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/journal-postgres.ts +168 -3
- package/src/drivers/meilisearch.ts +2 -0
- package/src/drivers/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +4 -0
- package/src/i18n/catalogs/en.ts +4 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/app.ts +191 -18
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +19 -1
- package/src/kernel/builtin-errors.ts +8 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- package/src/kernel/errors-compiler.ts +20 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.ts +5 -1
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +56 -0
- package/src/manifest/types.ts +8 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +25 -0
- package/src/release/http-graph.ts +90 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +105 -14
- package/src/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
package/src/client/transport.ts
CHANGED
|
@@ -20,11 +20,18 @@ import {
|
|
|
20
20
|
toQuery,
|
|
21
21
|
} from "./wire.ts";
|
|
22
22
|
|
|
23
|
-
/** Per-call transport options (binary decode, abort). */
|
|
23
|
+
/** Per-call transport options (binary decode, abort, retry opt-in). */
|
|
24
24
|
export interface TransportCallOptions {
|
|
25
25
|
readonly headers?: ClientHeaders;
|
|
26
26
|
readonly response?: "json" | "blob" | "arrayBuffer";
|
|
27
27
|
readonly signal?: AbortSignal;
|
|
28
|
+
/**
|
|
29
|
+
* Repeat this call under the client `retry` policy even when it sends
|
|
30
|
+
* no idempotency key.
|
|
31
|
+
*/
|
|
32
|
+
readonly retry?: boolean;
|
|
33
|
+
/** Stable key, or `false` to send none. */
|
|
34
|
+
readonly idempotencyKey?: string | false;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
/** Internal transport handle. */
|
|
@@ -63,10 +70,25 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
63
70
|
let delay = delay0;
|
|
64
71
|
const callClientOpts: ClientOptions =
|
|
65
72
|
callOpts.signal !== undefined ? { ...opts, signal: callOpts.signal } : opts;
|
|
73
|
+
const method = (opts.routes?.[key.replace("/", ".")]?.method ?? "POST").toUpperCase();
|
|
74
|
+
const idempotencyKey = resolveIdempotencyKey(method, callOpts.idempotencyKey);
|
|
75
|
+
const allowRetry =
|
|
76
|
+
callOpts.retry === true ||
|
|
77
|
+
method === "GET" ||
|
|
78
|
+
method === "QUERY" ||
|
|
79
|
+
idempotencyKey !== undefined;
|
|
66
80
|
|
|
67
81
|
for (;;) {
|
|
68
82
|
try {
|
|
69
|
-
const res = await once(
|
|
83
|
+
const res = await once(
|
|
84
|
+
base,
|
|
85
|
+
key,
|
|
86
|
+
input,
|
|
87
|
+
callClientOpts,
|
|
88
|
+
fetchFn,
|
|
89
|
+
callOpts.headers,
|
|
90
|
+
idempotencyKey,
|
|
91
|
+
);
|
|
70
92
|
if (
|
|
71
93
|
res.status === 401 &&
|
|
72
94
|
opts.auth &&
|
|
@@ -78,9 +100,22 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
78
100
|
await opts.auth.refresh();
|
|
79
101
|
continue;
|
|
80
102
|
}
|
|
103
|
+
if (res.status === 409 && allowRetry && attempt < retries) {
|
|
104
|
+
const seconds = Number(res.headers.get("retry-after"));
|
|
105
|
+
const wait = seconds > 0 ? seconds * 1000 : delay;
|
|
106
|
+
const structured = await decodeIfEnvelope(res);
|
|
107
|
+
if (structured?.error?.code === "IdempotencyInProgress") {
|
|
108
|
+
await sleep(wait);
|
|
109
|
+
delay *= backoff;
|
|
110
|
+
attempt += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (structured) return stampReplay(structured, res);
|
|
114
|
+
return transportEnvelope("HTTP 409", 409);
|
|
115
|
+
}
|
|
81
116
|
if (res.status >= 500) {
|
|
82
117
|
const structured = await decodeIfEnvelope(res);
|
|
83
|
-
if (structured) return structured;
|
|
118
|
+
if (structured) return stampReplay(structured, res);
|
|
84
119
|
throw new Error(`HTTP ${res.status}`);
|
|
85
120
|
}
|
|
86
121
|
if (callOpts.response === "blob" || callOpts.response === "arrayBuffer") {
|
|
@@ -89,7 +124,7 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
|
|
|
89
124
|
return decode(res);
|
|
90
125
|
} catch (err) {
|
|
91
126
|
const transient = isTransient(err);
|
|
92
|
-
if (!transient || attempt >= retries) {
|
|
127
|
+
if (!transient || !allowRetry || attempt >= retries) {
|
|
93
128
|
return transportEnvelope(err instanceof Error ? err.message : String(err));
|
|
94
129
|
}
|
|
95
130
|
await sleep(delay);
|
|
@@ -127,10 +162,20 @@ function normalizeCallOpts(
|
|
|
127
162
|
if (Array.isArray(headersOrOpts)) return { headers: headersOrOpts };
|
|
128
163
|
if (
|
|
129
164
|
typeof headersOrOpts === "object" &&
|
|
130
|
-
("response" in headersOrOpts ||
|
|
165
|
+
("response" in headersOrOpts ||
|
|
166
|
+
"signal" in headersOrOpts ||
|
|
167
|
+
"headers" in headersOrOpts ||
|
|
168
|
+
"retry" in headersOrOpts ||
|
|
169
|
+
"idempotencyKey" in headersOrOpts)
|
|
131
170
|
) {
|
|
132
171
|
const o = headersOrOpts as TransportCallOptions;
|
|
133
|
-
if (
|
|
172
|
+
if (
|
|
173
|
+
o.response !== undefined ||
|
|
174
|
+
o.signal !== undefined ||
|
|
175
|
+
o.headers !== undefined ||
|
|
176
|
+
o.retry !== undefined ||
|
|
177
|
+
o.idempotencyKey !== undefined
|
|
178
|
+
) {
|
|
134
179
|
return o;
|
|
135
180
|
}
|
|
136
181
|
}
|
|
@@ -138,7 +183,9 @@ function normalizeCallOpts(
|
|
|
138
183
|
}
|
|
139
184
|
|
|
140
185
|
/**
|
|
141
|
-
* Single HTTP attempt. Throws on network / abort / 5xx
|
|
186
|
+
* Single HTTP attempt. Throws on network / abort / non-envelope 5xx.
|
|
187
|
+
* A client timeout is rethrown as a plain `Error` so the retry loop can
|
|
188
|
+
* repeat it. A caller `AbortSignal` stays an `AbortError` and is not retried.
|
|
142
189
|
*
|
|
143
190
|
* @param base - Origin
|
|
144
191
|
* @param key - `unit/flow`
|
|
@@ -154,6 +201,7 @@ async function once(
|
|
|
154
201
|
opts: ClientOptions,
|
|
155
202
|
fetchFn: ClientFetch,
|
|
156
203
|
callHeaders?: ClientHeaders,
|
|
204
|
+
idempotencyKey?: string,
|
|
157
205
|
): Promise<Response> {
|
|
158
206
|
const route = opts.routes?.[key.replace("/", ".")];
|
|
159
207
|
const { url, method, body } = route
|
|
@@ -163,6 +211,7 @@ async function once(
|
|
|
163
211
|
const headers = new Headers();
|
|
164
212
|
applyHeaderBag(headers, await resolveHeaders(opts));
|
|
165
213
|
applyHeaderBag(headers, callHeaders);
|
|
214
|
+
if (idempotencyKey !== undefined) headers.set("Idempotency-Key", idempotencyKey);
|
|
166
215
|
if (body !== undefined && !headers.has("content-type") && typeof body === "string") {
|
|
167
216
|
headers.set("content-type", "application/json");
|
|
168
217
|
}
|
|
@@ -172,13 +221,20 @@ async function once(
|
|
|
172
221
|
const signal =
|
|
173
222
|
opts.signal && timeout ? AbortSignal.any([opts.signal, timeout]) : (opts.signal ?? timeout);
|
|
174
223
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
224
|
+
try {
|
|
225
|
+
return await fetchFn(url, {
|
|
226
|
+
method,
|
|
227
|
+
headers,
|
|
228
|
+
body: body as RequestInit["body"],
|
|
229
|
+
signal,
|
|
230
|
+
...(opts.credentials !== undefined ? { credentials: opts.credentials } : {}),
|
|
231
|
+
});
|
|
232
|
+
} catch (err) {
|
|
233
|
+
if (isAbortError(err) && timeout?.aborted === true && opts.signal?.aborted !== true) {
|
|
234
|
+
throw new Error(err instanceof Error ? err.message : "timeout");
|
|
235
|
+
}
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
182
238
|
}
|
|
183
239
|
|
|
184
240
|
/**
|
|
@@ -253,12 +309,12 @@ function isRawBody(input: unknown): input is ClientBodyInit {
|
|
|
253
309
|
|
|
254
310
|
async function decode(res: Response): Promise<ClientEnvelope> {
|
|
255
311
|
if (res.status === 204) {
|
|
256
|
-
return { data: undefined, error: null };
|
|
312
|
+
return stampReplay({ data: undefined, error: null }, res);
|
|
257
313
|
}
|
|
258
314
|
|
|
259
315
|
const text = await res.text();
|
|
260
316
|
if (!text) {
|
|
261
|
-
if (res.ok) return { data: undefined, error: null };
|
|
317
|
+
if (res.ok) return stampReplay({ data: undefined, error: null }, res);
|
|
262
318
|
return transportEnvelope(`HTTP ${res.status}`, res.status);
|
|
263
319
|
}
|
|
264
320
|
|
|
@@ -270,11 +326,11 @@ async function decode(res: Response): Promise<ClientEnvelope> {
|
|
|
270
326
|
}
|
|
271
327
|
|
|
272
328
|
if (json !== null && typeof json === "object" && "data" in json && "error" in json) {
|
|
273
|
-
return json as ClientEnvelope;
|
|
329
|
+
return stampReplay(json as ClientEnvelope, res);
|
|
274
330
|
}
|
|
275
331
|
|
|
276
332
|
if (res.ok) {
|
|
277
|
-
return { data: json, error: null };
|
|
333
|
+
return stampReplay({ data: json, error: null }, res);
|
|
278
334
|
}
|
|
279
335
|
|
|
280
336
|
return transportEnvelope(`HTTP ${res.status}`, res.status);
|
|
@@ -287,7 +343,64 @@ async function decodeBinary(res: Response, mode: "blob" | "arrayBuffer"): Promis
|
|
|
287
343
|
return transportEnvelope(`HTTP ${res.status}`, res.status);
|
|
288
344
|
}
|
|
289
345
|
const data = mode === "blob" ? await res.blob() : await res.arrayBuffer();
|
|
290
|
-
return { data, error: null };
|
|
346
|
+
return stampReplay({ data, error: null }, res);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Default `okid()` alphabet, inlined.
|
|
351
|
+
*
|
|
352
|
+
* Same 64-character Base64URL order as {@link OKID_ALPHABET}. Importing
|
|
353
|
+
* `okid` would pull `lazyRequire` and every alphabet constant into the client.
|
|
354
|
+
*/
|
|
355
|
+
export const IDEMPOTENCY_KEY_ALPHABET =
|
|
356
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 21-char key from `crypto.getRandomValues`.
|
|
360
|
+
*
|
|
361
|
+
* `getRandomValues` works on plain HTTP. When `crypto` is missing entirely,
|
|
362
|
+
* returns `undefined` so the call sends no key and runs once.
|
|
363
|
+
*/
|
|
364
|
+
export function mintIdempotencyKey(): string | undefined {
|
|
365
|
+
const webCrypto = globalThis.crypto;
|
|
366
|
+
if (webCrypto?.getRandomValues === undefined) return undefined;
|
|
367
|
+
const bytes = new Uint8Array(21);
|
|
368
|
+
webCrypto.getRandomValues(bytes);
|
|
369
|
+
let out = "";
|
|
370
|
+
for (let i = 0; i < 21; i++) out += IDEMPOTENCY_KEY_ALPHABET[bytes[i]! & 63]!;
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Key for this logical call, or `undefined` when none is sent.
|
|
376
|
+
*
|
|
377
|
+
* @param method - HTTP method, already uppercased
|
|
378
|
+
* @param opt - Per-call override
|
|
379
|
+
*/
|
|
380
|
+
function resolveIdempotencyKey(
|
|
381
|
+
method: string,
|
|
382
|
+
opt: string | false | undefined,
|
|
383
|
+
): string | undefined {
|
|
384
|
+
if (opt === false) return undefined;
|
|
385
|
+
if (typeof opt === "string") return opt;
|
|
386
|
+
if (method === "GET") return undefined;
|
|
387
|
+
return mintIdempotencyKey();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Copy `Idempotent-Replayed: true` onto result meta.
|
|
392
|
+
*
|
|
393
|
+
* @param envelope - Decoded envelope
|
|
394
|
+
* @param res - HTTP response
|
|
395
|
+
*/
|
|
396
|
+
function stampReplay(envelope: ClientEnvelope, res: Response): ClientEnvelope {
|
|
397
|
+
if (res.headers.get("idempotent-replayed") !== "true") return envelope;
|
|
398
|
+
const prev = envelope.meta;
|
|
399
|
+
return { ...envelope, meta: { ...prev, idempotentReplayed: true } };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function isAbortError(err: unknown): boolean {
|
|
403
|
+
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
291
404
|
}
|
|
292
405
|
|
|
293
406
|
function isTransient(err: unknown): boolean {
|
package/src/client/types.ts
CHANGED
|
@@ -146,6 +146,8 @@ export type ClientListMeta = {
|
|
|
146
146
|
readonly prev?: ClientPagerLink | null;
|
|
147
147
|
readonly total?: number;
|
|
148
148
|
readonly offset?: number;
|
|
149
|
+
/** Set when the server replayed a completed idempotent call. */
|
|
150
|
+
readonly idempotentReplayed?: true;
|
|
149
151
|
};
|
|
150
152
|
|
|
151
153
|
/**
|
|
@@ -163,7 +165,11 @@ export type ClientEnvelope<
|
|
|
163
165
|
readonly error: null;
|
|
164
166
|
readonly meta?: ClientListMeta;
|
|
165
167
|
}
|
|
166
|
-
| {
|
|
168
|
+
| {
|
|
169
|
+
readonly data: null;
|
|
170
|
+
readonly error: ClientError<E> | TransportError;
|
|
171
|
+
readonly meta?: ClientListMeta;
|
|
172
|
+
};
|
|
167
173
|
|
|
168
174
|
/**
|
|
169
175
|
* Client call result — envelope plus always-callable `next` / `prev`.
|
|
@@ -251,7 +257,15 @@ export interface ClientOptions {
|
|
|
251
257
|
readonly headers?: ClientHeaders | (() => ClientHeaders | Promise<ClientHeaders>);
|
|
252
258
|
/** Abort the request after this many milliseconds. */
|
|
253
259
|
readonly timeout?: number;
|
|
254
|
-
/**
|
|
260
|
+
/**
|
|
261
|
+
* Retry transient failures (network errors, client timeouts, and
|
|
262
|
+
* non-envelope 5xx).
|
|
263
|
+
*
|
|
264
|
+
* A call retries when it is `GET` or `QUERY`, or when it carries an
|
|
265
|
+
* idempotency key. Keys are sent automatically on every non-GET call.
|
|
266
|
+
* `idempotencyKey: false` runs once unless that call passes `{ retry: true }`.
|
|
267
|
+
* `409 IdempotencyInProgress` waits for `Retry-After` and counts as an attempt.
|
|
268
|
+
*/
|
|
255
269
|
readonly retry?: {
|
|
256
270
|
/** Extra attempts after the first (default 0). */
|
|
257
271
|
readonly retries?: number;
|
|
@@ -296,11 +310,22 @@ export interface ClientOptions {
|
|
|
296
310
|
export type ClientThenableIterable<T> = PromiseLike<T> & AsyncIterable<T>;
|
|
297
311
|
|
|
298
312
|
/**
|
|
299
|
-
* Per-call options on a Flow invoke (binary decode
|
|
313
|
+
* Per-call options on a Flow invoke (binary decode, abort, retry opt-in).
|
|
300
314
|
*/
|
|
301
315
|
export interface ClientCallOpts {
|
|
302
316
|
readonly response?: "json" | "blob" | "arrayBuffer";
|
|
303
317
|
readonly signal?: AbortSignal;
|
|
318
|
+
/**
|
|
319
|
+
* Repeat this call under the client `retry` policy even when it sends
|
|
320
|
+
* no idempotency key.
|
|
321
|
+
*/
|
|
322
|
+
readonly retry?: boolean;
|
|
323
|
+
/**
|
|
324
|
+
* Stable `Idempotency-Key` for this logical call. A string is sent as-is
|
|
325
|
+
* (one key per form render dedupes double submits). `false` sends no key.
|
|
326
|
+
* Omitted on a non-GET call generates one key and reuses it across retries.
|
|
327
|
+
*/
|
|
328
|
+
readonly idempotencyKey?: string | false;
|
|
304
329
|
}
|
|
305
330
|
|
|
306
331
|
/**
|
|
@@ -503,6 +528,7 @@ export interface ClientDescriptor {
|
|
|
503
528
|
readonly stream?: true;
|
|
504
529
|
readonly matchKey?: readonly string[];
|
|
505
530
|
readonly gates?: readonly string[];
|
|
531
|
+
readonly idempotency?: { readonly mode: "off" | "auto" | "required"; readonly ttl: string };
|
|
506
532
|
};
|
|
507
533
|
};
|
|
508
534
|
};
|