@ironflow/browser 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -13
- package/dist/client.d.ts +94 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +191 -122
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/offline.d.ts +166 -0
- package/dist/offline.d.ts.map +1 -0
- package/dist/offline.js +306 -0
- package/dist/offline.js.map +1 -0
- package/dist/queue/db.d.ts +123 -0
- package/dist/queue/db.d.ts.map +1 -0
- package/dist/queue/db.js +304 -0
- package/dist/queue/db.js.map +1 -0
- package/dist/queue/ids.d.ts +22 -0
- package/dist/queue/ids.d.ts.map +1 -0
- package/dist/queue/ids.js +45 -0
- package/dist/queue/ids.js.map +1 -0
- package/dist/queue/lock.d.ts +42 -0
- package/dist/queue/lock.d.ts.map +1 -0
- package/dist/queue/lock.js +65 -0
- package/dist/queue/lock.js.map +1 -0
- package/dist/queue/queue.d.ts +281 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/queue.js +894 -0
- package/dist/queue/queue.js.map +1 -0
- package/dist/queue/types.d.ts +145 -0
- package/dist/queue/types.d.ts.map +1 -0
- package/dist/queue/types.js +6 -0
- package/dist/queue/types.js.map +1 -0
- package/package.json +5 -3
package/dist/client.js
CHANGED
|
@@ -12,6 +12,28 @@ import { filterWaitStreamFrames } from "./projection-stream.js";
|
|
|
12
12
|
import { BrowserKVClient } from "./kv.js";
|
|
13
13
|
import { BrowserConfigClient } from "./config-client.js";
|
|
14
14
|
import { createAgentsNamespace } from "./agents/index.js";
|
|
15
|
+
/**
|
|
16
|
+
* Key for the offline queue's replay entry point.
|
|
17
|
+
*
|
|
18
|
+
* Not exported from the package root, so it is unreachable outside this module
|
|
19
|
+
* pair — the closest thing to package-private TypeScript offers.
|
|
20
|
+
*/
|
|
21
|
+
export const REPLAY_QUEUED_WRITE = Symbol("ironflow.replayQueuedWrite");
|
|
22
|
+
/**
|
|
23
|
+
* Whether an HTTP status is worth trying again.
|
|
24
|
+
*
|
|
25
|
+
* 5xx is the obvious half. 429 is the half that is easy to miss: a rate limit
|
|
26
|
+
* is a "later", not a "never", and a caller that treats it as permanent gives
|
|
27
|
+
* up at exactly the moment backing off would have worked.
|
|
28
|
+
*
|
|
29
|
+
* This matters most for the offline write queue (ADR 0052), whose drain after
|
|
30
|
+
* a long offline stretch is a burst of writes — the single most likely way to
|
|
31
|
+
* earn a 429. Classifying it as permanent would send those writes to the dead
|
|
32
|
+
* letter store precisely when the queue was doing its job.
|
|
33
|
+
*/
|
|
34
|
+
function isRetryableStatus(status) {
|
|
35
|
+
return status >= 500 || status === 429;
|
|
36
|
+
}
|
|
15
37
|
/**
|
|
16
38
|
* Ironflow browser client singleton
|
|
17
39
|
*/
|
|
@@ -89,6 +111,34 @@ class IronflowClient {
|
|
|
89
111
|
transport: this.config.transport,
|
|
90
112
|
});
|
|
91
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Replace the credentials without tearing anything down.
|
|
116
|
+
*
|
|
117
|
+
* `configure()` cannot be used for this: it calls `cleanup()`, which destroys
|
|
118
|
+
* the transport, every subscription, and — for an offline client — the
|
|
119
|
+
* drainer mid-flight. Refreshing an expired token must not cost you all of
|
|
120
|
+
* that.
|
|
121
|
+
*
|
|
122
|
+
* Every request path reads `config.auth` at send time (`request()`,
|
|
123
|
+
* `streamRequest()`, `restRequest()` all build their `Authorization` header
|
|
124
|
+
* per call), so the next request picks this up with no further work. An
|
|
125
|
+
* already-open subscription keeps the credentials it connected with until it
|
|
126
|
+
* reconnects.
|
|
127
|
+
*/
|
|
128
|
+
setAuth(auth) {
|
|
129
|
+
this.ensureConfigured();
|
|
130
|
+
this.config.auth = auth;
|
|
131
|
+
this.logger.debug("Auth updated");
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The configured logger.
|
|
135
|
+
*
|
|
136
|
+
* @internal Lets sibling modules (the offline queue) honour the app's logging
|
|
137
|
+
* configuration instead of writing straight to the console.
|
|
138
|
+
*/
|
|
139
|
+
getLogger() {
|
|
140
|
+
return this.logger;
|
|
141
|
+
}
|
|
92
142
|
/**
|
|
93
143
|
* Check if the client is configured
|
|
94
144
|
*/
|
|
@@ -301,14 +351,6 @@ class IronflowClient {
|
|
|
301
351
|
const response = await this.request(RunResponseSchema, "POST", "/ironflow.v1.IronflowService/CancelRun", { id: runId, reason });
|
|
302
352
|
return this.mapRunResponse(response);
|
|
303
353
|
}
|
|
304
|
-
/**
|
|
305
|
-
* Retry a failed run
|
|
306
|
-
*/
|
|
307
|
-
async retryRun(runId, fromStep) {
|
|
308
|
-
this.ensureConfigured();
|
|
309
|
-
const response = await this.request(RunResponseSchema, "POST", "/ironflow.v1.IronflowService/RetryRun", { id: runId, fromStep });
|
|
310
|
-
return this.mapRunResponse(response);
|
|
311
|
-
}
|
|
312
354
|
/**
|
|
313
355
|
* Patch a step's output (hot patching)
|
|
314
356
|
*/
|
|
@@ -2009,28 +2051,112 @@ class IronflowClient {
|
|
|
2009
2051
|
};
|
|
2010
2052
|
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
2011
2053
|
}
|
|
2012
|
-
|
|
2054
|
+
/**
|
|
2055
|
+
* Shared HTTP shell for every REST/Connect call the client makes.
|
|
2056
|
+
*
|
|
2057
|
+
* `send` owns the transport concerns; `handle` owns how one response becomes
|
|
2058
|
+
* a `T`. Splitting there is what lets three callers with very different
|
|
2059
|
+
* response handling share one implementation:
|
|
2060
|
+
*
|
|
2061
|
+
* caller ──> send(method, path, body, handle)
|
|
2062
|
+
* │
|
|
2063
|
+
* ├─ headers: Content-Type (only when a body is sent),
|
|
2064
|
+
* │ X-Ironflow-Environment,
|
|
2065
|
+
* │ Authorization (apiKey wins over token)
|
|
2066
|
+
* ├─ fetch ──> handle(response) ──> T
|
|
2067
|
+
* │ └─ may throw IronflowError ──┐
|
|
2068
|
+
* ├─ catch: AbortError -> TIMEOUT │ rethrown
|
|
2069
|
+
* │ anything else -> REQUEST_FAILED │ as-is
|
|
2070
|
+
* └─ finally: clearTimeout <───────────────────┘
|
|
2071
|
+
*
|
|
2072
|
+
* Two details are load-bearing and must not be "tidied":
|
|
2073
|
+
*
|
|
2074
|
+
* 1. The abort timer is cleared only *after* `handle` returns, so a slow
|
|
2075
|
+
* parse of a large body can still trip the timeout. That matches the
|
|
2076
|
+
* behaviour of the three helpers this replaces.
|
|
2077
|
+
* 2. `handle` runs inside the try, so errors it throws pass through the
|
|
2078
|
+
* catch below. Every typed error in this SDK (`ValidationError`,
|
|
2079
|
+
* `UnauthenticatedError`, `EnterpriseRequiredError`, `UnauthorizedError`)
|
|
2080
|
+
* extends `IronflowError`, so the single `instanceof` guard covers all of
|
|
2081
|
+
* them and they are rethrown untouched.
|
|
2082
|
+
*/
|
|
2083
|
+
async send(method, path, body, handle) {
|
|
2013
2084
|
const url = `${this.config.serverUrl}${path}`;
|
|
2014
2085
|
const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
|
|
2015
2086
|
const controller = new AbortController();
|
|
2016
2087
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
2017
2088
|
try {
|
|
2018
2089
|
const headers = {
|
|
2019
|
-
"Content-Type": "application/json",
|
|
2020
2090
|
[HEADERS.ENVIRONMENT]: this.config.environment,
|
|
2021
2091
|
};
|
|
2092
|
+
if (body !== undefined) {
|
|
2093
|
+
headers["Content-Type"] = "application/json";
|
|
2094
|
+
}
|
|
2022
2095
|
if (this.config.auth?.apiKey) {
|
|
2023
2096
|
headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
|
|
2024
2097
|
}
|
|
2025
2098
|
else if (this.config.auth?.token) {
|
|
2026
2099
|
headers["Authorization"] = `Bearer ${this.config.auth.token}`;
|
|
2027
2100
|
}
|
|
2028
|
-
const
|
|
2101
|
+
const fetchOptions = {
|
|
2029
2102
|
method,
|
|
2030
2103
|
headers,
|
|
2031
|
-
body: JSON.stringify(body),
|
|
2032
2104
|
signal: controller.signal,
|
|
2105
|
+
};
|
|
2106
|
+
if (body !== undefined) {
|
|
2107
|
+
fetchOptions.body = JSON.stringify(body);
|
|
2108
|
+
}
|
|
2109
|
+
const response = await fetch(url, fetchOptions);
|
|
2110
|
+
try {
|
|
2111
|
+
return await handle(response);
|
|
2112
|
+
}
|
|
2113
|
+
catch (error) {
|
|
2114
|
+
if (error instanceof IronflowError)
|
|
2115
|
+
throw error;
|
|
2116
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
2117
|
+
throw error;
|
|
2118
|
+
// The request itself succeeded; the BODY could not be understood.
|
|
2119
|
+
// Retrying cannot fix a server that returns unparseable bytes, and
|
|
2120
|
+
// marking it retryable would stall the offline queue at this write
|
|
2121
|
+
// indefinitely (it blocks on the head of a strict FIFO).
|
|
2122
|
+
throw new IronflowError(`${method} ${path} returned an unparseable response: ${error instanceof Error ? error.message : String(error)}`, {
|
|
2123
|
+
code: "RESPONSE_UNPARSEABLE",
|
|
2124
|
+
status: response.status,
|
|
2125
|
+
retryable: false,
|
|
2126
|
+
cause: error instanceof Error ? error : undefined,
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
catch (error) {
|
|
2131
|
+
if (error instanceof IronflowError) {
|
|
2132
|
+
throw error;
|
|
2133
|
+
}
|
|
2134
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
2135
|
+
throw new IronflowError(`Request timeout after ${timeout}ms for ${method} ${path}`, {
|
|
2136
|
+
code: "TIMEOUT",
|
|
2137
|
+
retryable: true,
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
throw new IronflowError(error instanceof Error
|
|
2141
|
+
? `${method} ${path} failed: ${error.message}`
|
|
2142
|
+
: `${method} ${path} failed`, {
|
|
2143
|
+
code: "REQUEST_FAILED",
|
|
2144
|
+
retryable: true,
|
|
2145
|
+
cause: error instanceof Error ? error : undefined,
|
|
2033
2146
|
});
|
|
2147
|
+
}
|
|
2148
|
+
finally {
|
|
2149
|
+
clearTimeout(timeoutId);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Connect-style request whose response is validated against a zod schema.
|
|
2154
|
+
*
|
|
2155
|
+
* Reads the body exactly once and reuses the text for both the error path
|
|
2156
|
+
* and the success parse.
|
|
2157
|
+
*/
|
|
2158
|
+
async request(schema, method, path, body) {
|
|
2159
|
+
return this.send(method, path, body, async (response) => {
|
|
2034
2160
|
const responseBody = await response.text();
|
|
2035
2161
|
if (!response.ok) {
|
|
2036
2162
|
const errorResult = ErrorResponseSchema.safeParse(safeJsonParse(responseBody));
|
|
@@ -2039,7 +2165,8 @@ class IronflowClient {
|
|
|
2039
2165
|
: { message: responseBody };
|
|
2040
2166
|
throw new IronflowError(errorData.message ?? `Request failed: ${response.status}`, {
|
|
2041
2167
|
code: errorData.code ?? `HTTP_${response.status}`,
|
|
2042
|
-
|
|
2168
|
+
status: response.status,
|
|
2169
|
+
retryable: isRetryableStatus(response.status),
|
|
2043
2170
|
});
|
|
2044
2171
|
}
|
|
2045
2172
|
const parsed = safeJsonParse(responseBody);
|
|
@@ -2054,78 +2181,61 @@ class IronflowClient {
|
|
|
2054
2181
|
throw new ValidationError(`Invalid response from server: ${issues}`);
|
|
2055
2182
|
}
|
|
2056
2183
|
return result.data;
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Replay a write that the offline queue stored earlier (ADR 0052).
|
|
2188
|
+
*
|
|
2189
|
+
* Keyed by a module-private symbol rather than a name. An `@internal` JSDoc
|
|
2190
|
+
* tag does nothing without `stripInternal`, which this repo does not set, so
|
|
2191
|
+
* a named method would ship in the published .d.ts — and this one is an
|
|
2192
|
+
* arbitrary-path authenticated POST, which is not something to hand out.
|
|
2193
|
+
* `index.ts` does not re-export the symbol, so no consumer can reach it.
|
|
2194
|
+
*
|
|
2195
|
+
* Takes the serialised body the queue persisted and routes it through the
|
|
2196
|
+
* ordinary request path, so a replayed write gets exactly the same headers,
|
|
2197
|
+
* timeout and retry classification as a live one — including 429 being
|
|
2198
|
+
* retryable, which the drainer depends on.
|
|
2199
|
+
*/
|
|
2200
|
+
async [REPLAY_QUEUED_WRITE](path, body) {
|
|
2201
|
+
this.ensureConfigured();
|
|
2202
|
+
// Parsed here rather than inline in the call, and as a NON-retryable coded
|
|
2203
|
+
// error: a bare SyntaxError is not an IronflowError, and the drainer treats
|
|
2204
|
+
// anything else as a network fault worth retrying. Since the queue is strict
|
|
2205
|
+
// FIFO, an unreadable record at the head would then block every write behind
|
|
2206
|
+
// it until retention expired. Dead-lettering quarantines just the one.
|
|
2207
|
+
let parsed;
|
|
2208
|
+
try {
|
|
2209
|
+
parsed = JSON.parse(body);
|
|
2057
2210
|
}
|
|
2058
2211
|
catch (error) {
|
|
2059
|
-
|
|
2060
|
-
throw error;
|
|
2061
|
-
}
|
|
2062
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
2063
|
-
throw new IronflowError(`Request timeout after ${timeout}ms for ${method} ${path}`, {
|
|
2064
|
-
code: "TIMEOUT",
|
|
2065
|
-
retryable: true,
|
|
2066
|
-
});
|
|
2067
|
-
}
|
|
2068
|
-
throw new IronflowError(error instanceof Error
|
|
2069
|
-
? `${method} ${path} failed: ${error.message}`
|
|
2070
|
-
: `${method} ${path} failed`, {
|
|
2071
|
-
code: "REQUEST_FAILED",
|
|
2072
|
-
retryable: true,
|
|
2073
|
-
cause: error instanceof Error ? error : undefined,
|
|
2074
|
-
});
|
|
2075
|
-
}
|
|
2076
|
-
finally {
|
|
2077
|
-
clearTimeout(timeoutId);
|
|
2212
|
+
throw new IronflowError(`Queued write body is not valid JSON and can never be sent: ${error instanceof Error ? error.message : String(error)}`, { code: "QUEUE_BODY_CORRUPT", retryable: false });
|
|
2078
2213
|
}
|
|
2214
|
+
const response = await this.streamRequest(path, parsed);
|
|
2215
|
+
return {
|
|
2216
|
+
eventId: response.eventId,
|
|
2217
|
+
entityVersion: response.entityVersion === undefined
|
|
2218
|
+
? undefined
|
|
2219
|
+
: Number(response.entityVersion),
|
|
2220
|
+
};
|
|
2079
2221
|
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Connect-style POST returning the raw decoded JSON, no schema validation.
|
|
2224
|
+
*
|
|
2225
|
+
* Only reads the body on the error path.
|
|
2226
|
+
*/
|
|
2080
2227
|
async streamRequest(path, body) {
|
|
2081
|
-
|
|
2082
|
-
const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
|
|
2083
|
-
const controller = new AbortController();
|
|
2084
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
2085
|
-
try {
|
|
2086
|
-
const headers = {
|
|
2087
|
-
"Content-Type": "application/json",
|
|
2088
|
-
[HEADERS.ENVIRONMENT]: this.config.environment,
|
|
2089
|
-
};
|
|
2090
|
-
if (this.config.auth?.apiKey) {
|
|
2091
|
-
headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
|
|
2092
|
-
}
|
|
2093
|
-
else if (this.config.auth?.token) {
|
|
2094
|
-
headers["Authorization"] = `Bearer ${this.config.auth.token}`;
|
|
2095
|
-
}
|
|
2096
|
-
const response = await fetch(url, {
|
|
2097
|
-
method: "POST",
|
|
2098
|
-
headers,
|
|
2099
|
-
body: JSON.stringify(body),
|
|
2100
|
-
signal: controller.signal,
|
|
2101
|
-
});
|
|
2228
|
+
return this.send("POST", path, body, async (response) => {
|
|
2102
2229
|
if (!response.ok) {
|
|
2103
2230
|
const errorBody = safeJsonParse(await response.text());
|
|
2104
2231
|
throw new IronflowError(errorBody?.message ?? `Request failed: ${response.status}`, {
|
|
2105
2232
|
code: errorBody?.code ?? `HTTP_${response.status}`,
|
|
2106
|
-
|
|
2233
|
+
status: response.status,
|
|
2234
|
+
retryable: isRetryableStatus(response.status),
|
|
2107
2235
|
});
|
|
2108
2236
|
}
|
|
2109
2237
|
return response.json();
|
|
2110
|
-
}
|
|
2111
|
-
catch (error) {
|
|
2112
|
-
if (error instanceof IronflowError) {
|
|
2113
|
-
throw error;
|
|
2114
|
-
}
|
|
2115
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
2116
|
-
throw new IronflowError(`Request timeout after ${timeout}ms for POST ${path}`, { code: "TIMEOUT", retryable: true });
|
|
2117
|
-
}
|
|
2118
|
-
throw new IronflowError(error instanceof Error
|
|
2119
|
-
? `POST ${path} failed: ${error.message}`
|
|
2120
|
-
: `POST ${path} failed`, {
|
|
2121
|
-
code: "REQUEST_FAILED",
|
|
2122
|
-
retryable: true,
|
|
2123
|
-
cause: error instanceof Error ? error : undefined,
|
|
2124
|
-
});
|
|
2125
|
-
}
|
|
2126
|
-
finally {
|
|
2127
|
-
clearTimeout(timeoutId);
|
|
2128
|
-
}
|
|
2238
|
+
});
|
|
2129
2239
|
}
|
|
2130
2240
|
/**
|
|
2131
2241
|
* REST request helper for auth management endpoints.
|
|
@@ -2134,34 +2244,13 @@ class IronflowClient {
|
|
|
2134
2244
|
* - 401 → UnauthenticatedError
|
|
2135
2245
|
* - 402 → EnterpriseRequiredError
|
|
2136
2246
|
* - 403 → UnauthorizedError
|
|
2247
|
+
*
|
|
2248
|
+
* Note the error message template differs from the two Connect helpers
|
|
2249
|
+
* above ("Request failed with status N" vs "Request failed: N"); both are
|
|
2250
|
+
* pinned by tests, so pick deliberately if you ever unify them.
|
|
2137
2251
|
*/
|
|
2138
2252
|
async restRequest(method, path, body) {
|
|
2139
|
-
|
|
2140
|
-
const timeout = this.config.timeout ?? DEFAULT_TIMEOUTS.CLIENT;
|
|
2141
|
-
const controller = new AbortController();
|
|
2142
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
2143
|
-
try {
|
|
2144
|
-
const headers = {
|
|
2145
|
-
[HEADERS.ENVIRONMENT]: this.config.environment,
|
|
2146
|
-
};
|
|
2147
|
-
if (body !== undefined) {
|
|
2148
|
-
headers["Content-Type"] = "application/json";
|
|
2149
|
-
}
|
|
2150
|
-
if (this.config.auth?.apiKey) {
|
|
2151
|
-
headers["Authorization"] = `Bearer ${this.config.auth.apiKey}`;
|
|
2152
|
-
}
|
|
2153
|
-
else if (this.config.auth?.token) {
|
|
2154
|
-
headers["Authorization"] = `Bearer ${this.config.auth.token}`;
|
|
2155
|
-
}
|
|
2156
|
-
const fetchOptions = {
|
|
2157
|
-
method,
|
|
2158
|
-
headers,
|
|
2159
|
-
signal: controller.signal,
|
|
2160
|
-
};
|
|
2161
|
-
if (body !== undefined) {
|
|
2162
|
-
fetchOptions.body = JSON.stringify(body);
|
|
2163
|
-
}
|
|
2164
|
-
const response = await fetch(url, fetchOptions);
|
|
2253
|
+
return this.send(method, path, body, async (response) => {
|
|
2165
2254
|
// Handle 204 No Content
|
|
2166
2255
|
if (response.status === 204) {
|
|
2167
2256
|
return undefined;
|
|
@@ -2188,33 +2277,13 @@ class IronflowClient {
|
|
|
2188
2277
|
default:
|
|
2189
2278
|
throw new IronflowError(errorMessage, {
|
|
2190
2279
|
code: `HTTP_${response.status}`,
|
|
2191
|
-
|
|
2280
|
+
status: response.status,
|
|
2281
|
+
retryable: isRetryableStatus(response.status),
|
|
2192
2282
|
});
|
|
2193
2283
|
}
|
|
2194
2284
|
}
|
|
2195
2285
|
return response.json();
|
|
2196
|
-
}
|
|
2197
|
-
catch (error) {
|
|
2198
|
-
if (error instanceof IronflowError ||
|
|
2199
|
-
error instanceof UnauthenticatedError ||
|
|
2200
|
-
error instanceof EnterpriseRequiredError ||
|
|
2201
|
-
error instanceof UnauthorizedError) {
|
|
2202
|
-
throw error;
|
|
2203
|
-
}
|
|
2204
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
2205
|
-
throw new IronflowError(`Request timeout after ${timeout}ms for ${method} ${path}`, { code: "TIMEOUT", retryable: true });
|
|
2206
|
-
}
|
|
2207
|
-
throw new IronflowError(error instanceof Error
|
|
2208
|
-
? `${method} ${path} failed: ${error.message}`
|
|
2209
|
-
: `${method} ${path} failed`, {
|
|
2210
|
-
code: "REQUEST_FAILED",
|
|
2211
|
-
retryable: true,
|
|
2212
|
-
cause: error instanceof Error ? error : undefined,
|
|
2213
|
-
});
|
|
2214
|
-
}
|
|
2215
|
-
finally {
|
|
2216
|
-
clearTimeout(timeoutId);
|
|
2217
|
-
}
|
|
2286
|
+
});
|
|
2218
2287
|
}
|
|
2219
2288
|
mapRunResponse(response) {
|
|
2220
2289
|
// ConnectRPC returns proto enum strings like "RUN_STATUS_COMPLETED" — normalize to "completed"
|