unfazed 0.0.1 → 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 +21 -0
- package/README.md +300 -1
- package/dist/client.d.ts +134 -0
- package/dist/client.js +266 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +93 -0
- package/dist/errors.js +137 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated/gql.d.ts +132 -0
- package/dist/generated/gql.js +41 -0
- package/dist/generated/gql.js.map +1 -0
- package/dist/generated/graphql.d.ts +774 -0
- package/dist/generated/graphql.js +607 -0
- package/dist/generated/graphql.js.map +1 -0
- package/dist/generated/index.d.ts +1 -0
- package/dist/generated/index.js +2 -0
- package/dist/generated/index.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/operations.d.ts +136 -0
- package/dist/operations.js +461 -0
- package/dist/operations.js.map +1 -0
- package/dist/unfazed.d.ts +445 -0
- package/dist/unfazed.js +510 -0
- package/dist/unfazed.js.map +1 -0
- package/dist/wait.d.ts +57 -0
- package/dist/wait.js +59 -0
- package/dist/wait.js.map +1 -0
- package/package.json +50 -6
- package/schema/SOURCE.json +9 -0
- package/schema/schema.graphqls +1303 -0
- package/index.js +0 -2
package/dist/client.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { UnfazedAuthError, UnfazedConfigError, UnfazedGraphQLError, UnfazedHTTPError, UnfazedTimeoutError, } from "./errors.js";
|
|
2
|
+
/** SDK version, sent as the User-Agent. Keep in sync with package.json. */
|
|
3
|
+
export const VERSION = "0.1.0";
|
|
4
|
+
/**
|
|
5
|
+
* Default GraphQL endpoint.
|
|
6
|
+
*
|
|
7
|
+
* Note this is the FULL endpoint, not an origin: the platform's own documented
|
|
8
|
+
* convention (`UNFZ_API_URL="https://unfazed.dev/query"`, and cmd/showcase-deploy
|
|
9
|
+
* in the unfazed repo) treats the /query URL as the configurable unit, so the
|
|
10
|
+
* SDK matches it rather than inventing a base-URL convention.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_ENDPOINT = "https://unfazed.dev/query";
|
|
13
|
+
/** Platform origin, used to derive the endpoint when only `baseUrl` is given. */
|
|
14
|
+
export const DEFAULT_BASE_URL = "https://unfazed.dev";
|
|
15
|
+
/** Path of the GraphQL endpoint under an origin. */
|
|
16
|
+
export const GRAPHQL_PATH = "/query";
|
|
17
|
+
/**
|
|
18
|
+
* Environment variable holding the bearer token.
|
|
19
|
+
*
|
|
20
|
+
* This is the platform's documented name (docs/agent-deployments.md,
|
|
21
|
+
* internal/agentdocs/reference.md). It accepts either an operator API token or
|
|
22
|
+
* an agent access token from POST /agents/register — /query takes both as
|
|
23
|
+
* plain bearers.
|
|
24
|
+
*/
|
|
25
|
+
export const TOKEN_ENV_VAR = "UNFZ_API_TOKEN";
|
|
26
|
+
/** Environment variable holding the full GraphQL endpoint URL. */
|
|
27
|
+
export const ENDPOINT_ENV_VAR = "UNFZ_API_URL";
|
|
28
|
+
/**
|
|
29
|
+
* Layer 1 — a typed, dependency-free GraphQL transport over `fetch`.
|
|
30
|
+
*
|
|
31
|
+
* This is the SDK's entire networking surface. It understands auth, timeouts
|
|
32
|
+
* and the GraphQL error envelope, and nothing about deployments. The workflow
|
|
33
|
+
* layer ({@link import("./unfazed.js").Unfazed}) is built strictly on top of
|
|
34
|
+
* it, so you can always drop down here for any operation the workflow layer
|
|
35
|
+
* does not wrap — the schema has ~660 fields and the workflow layer
|
|
36
|
+
* deliberately covers only the agent-facing ones.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* const client = new UnfazedClient(); // reads UNFZ_API_TOKEN
|
|
40
|
+
* const data = await client.query<{ whoami: { name: string } }, never>(
|
|
41
|
+
* `query { whoami { name } }`,
|
|
42
|
+
* );
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export class UnfazedClient {
|
|
46
|
+
/** Resolved GraphQL endpoint URL. */
|
|
47
|
+
endpoint;
|
|
48
|
+
#token;
|
|
49
|
+
#headers;
|
|
50
|
+
#timeoutMs;
|
|
51
|
+
#fetch;
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
const token = options.token ?? readEnv(TOKEN_ENV_VAR);
|
|
54
|
+
if (!token) {
|
|
55
|
+
throw new UnfazedAuthError(`unfazed: no API token. Pass { token } or set ${TOKEN_ENV_VAR}. ` +
|
|
56
|
+
`Get one at ${DEFAULT_BASE_URL}/auth.md`);
|
|
57
|
+
}
|
|
58
|
+
this.#token = token.trim();
|
|
59
|
+
this.endpoint = resolveEndpoint(options);
|
|
60
|
+
this.#headers = { ...(options.headers ?? {}) };
|
|
61
|
+
this.#timeoutMs = options.timeoutMs ?? 60_000;
|
|
62
|
+
const injected = options.fetch;
|
|
63
|
+
if (injected) {
|
|
64
|
+
this.#fetch = injected;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const globalFetch = globalThis.fetch;
|
|
68
|
+
if (typeof globalFetch !== "function") {
|
|
69
|
+
throw new UnfazedConfigError("unfazed: no global fetch. Use Bun, Node >= 20, or pass { fetch } to the client.");
|
|
70
|
+
}
|
|
71
|
+
this.#fetch = globalFetch.bind(globalThis);
|
|
72
|
+
}
|
|
73
|
+
const ua = options.userAgent ?? `unfazed-sdk/${VERSION}`;
|
|
74
|
+
if (ua)
|
|
75
|
+
this.#headers["User-Agent"] = ua;
|
|
76
|
+
}
|
|
77
|
+
/** Runs a query document. Identical wire behaviour to {@link mutate}. */
|
|
78
|
+
query(document, variables, options) {
|
|
79
|
+
return this.request(document, variables, options);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Runs a mutation document. Distinct from {@link query} for call-site
|
|
83
|
+
* readability only — GraphQL over HTTP POST makes no distinction.
|
|
84
|
+
*/
|
|
85
|
+
mutate(document, variables, options) {
|
|
86
|
+
return this.request(document, variables, options);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The single code path behind {@link query} and {@link mutate}.
|
|
90
|
+
*
|
|
91
|
+
* Throws {@link UnfazedHTTPError} on non-2xx, {@link UnfazedGraphQLError}
|
|
92
|
+
* when the body carries `errors`, {@link UnfazedTimeoutError} on timeout or
|
|
93
|
+
* abort. Resolves to the `data` object on success.
|
|
94
|
+
*/
|
|
95
|
+
async request(document, variables, options = {}) {
|
|
96
|
+
const timeoutMs = options.timeoutMs ?? this.#timeoutMs;
|
|
97
|
+
const { signal, cleanup, didTimeout } = makeSignal(timeoutMs, options.signal);
|
|
98
|
+
// `document` may be a TypedDocumentString (a String object). JSON.stringify
|
|
99
|
+
// would serialise that fine, but String(...) keeps the wire body a plain
|
|
100
|
+
// primitive regardless of how the caller obtained the document.
|
|
101
|
+
const body = JSON.stringify({
|
|
102
|
+
query: String(document),
|
|
103
|
+
...(variables === undefined ? {} : { variables }),
|
|
104
|
+
...(options.operationName ? { operationName: options.operationName } : {}),
|
|
105
|
+
});
|
|
106
|
+
// The deadline covers BOTH phases — headers AND body. A proxy that answers
|
|
107
|
+
// with headers and then stalls the body is a real failure mode, and
|
|
108
|
+
// stopping the clock at the end of `fetch()` would leave `response.text()`
|
|
109
|
+
// hanging forever. `untilAborted` additionally races the signal itself, so
|
|
110
|
+
// an injected transport that ignores `signal` still cannot outlive the
|
|
111
|
+
// timeout.
|
|
112
|
+
let response;
|
|
113
|
+
let text;
|
|
114
|
+
try {
|
|
115
|
+
response = await untilAborted(this.#fetch(this.endpoint, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
...this.#headers,
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
Accept: "application/json",
|
|
121
|
+
Authorization: `Bearer ${this.#token}`,
|
|
122
|
+
},
|
|
123
|
+
body,
|
|
124
|
+
...(signal ? { signal } : {}),
|
|
125
|
+
}), signal);
|
|
126
|
+
text = await untilAborted(response.text(), signal);
|
|
127
|
+
}
|
|
128
|
+
catch (cause) {
|
|
129
|
+
if (didTimeout()) {
|
|
130
|
+
throw new UnfazedTimeoutError(`unfazed: request timed out after ${timeoutMs}ms`, { cause });
|
|
131
|
+
}
|
|
132
|
+
if (isAbort(cause)) {
|
|
133
|
+
throw new UnfazedTimeoutError("unfazed: request aborted", { cause });
|
|
134
|
+
}
|
|
135
|
+
throw cause;
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
cleanup();
|
|
139
|
+
}
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new UnfazedHTTPError(response.status, text, this.endpoint);
|
|
142
|
+
}
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = JSON.parse(text);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
throw new UnfazedHTTPError(response.status, text, this.endpoint);
|
|
149
|
+
}
|
|
150
|
+
// A GraphQL 200 can still be a failure, and can be a PARTIAL failure: the
|
|
151
|
+
// batched writeFiles document resolves each aliased field independently.
|
|
152
|
+
// Attaching `data` to the error is what lets a caller see which writes
|
|
153
|
+
// landed before the failing one.
|
|
154
|
+
if (parsed.errors?.length) {
|
|
155
|
+
throw new UnfazedGraphQLError(parsed.errors, parsed.data);
|
|
156
|
+
}
|
|
157
|
+
if (parsed.data === undefined || parsed.data === null) {
|
|
158
|
+
throw new UnfazedGraphQLError([{ message: "response contained neither data nor errors" }], undefined);
|
|
159
|
+
}
|
|
160
|
+
return parsed.data;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function resolveEndpoint(options) {
|
|
164
|
+
if (options.endpoint)
|
|
165
|
+
return options.endpoint;
|
|
166
|
+
if (options.baseUrl)
|
|
167
|
+
return trimSlash(options.baseUrl) + GRAPHQL_PATH;
|
|
168
|
+
const env = readEnv(ENDPOINT_ENV_VAR);
|
|
169
|
+
// UNFZ_API_URL is documented as the full /query URL and is used verbatim.
|
|
170
|
+
// Tolerate a bare origin anyway, since that is the obvious mistake and
|
|
171
|
+
// guessing right costs nothing.
|
|
172
|
+
if (env) {
|
|
173
|
+
const trimmed = trimSlash(env);
|
|
174
|
+
return /\/query$|\/graphql$/.test(trimmed) ? trimmed : trimmed + GRAPHQL_PATH;
|
|
175
|
+
}
|
|
176
|
+
return DEFAULT_ENDPOINT;
|
|
177
|
+
}
|
|
178
|
+
function readEnv(name) {
|
|
179
|
+
// Guarded: the SDK must also work in a browser or worker, where there is no
|
|
180
|
+
// `process` at all.
|
|
181
|
+
const env = globalThis.process
|
|
182
|
+
?.env;
|
|
183
|
+
return env?.[name] || undefined;
|
|
184
|
+
}
|
|
185
|
+
function trimSlash(u) {
|
|
186
|
+
return u.replace(/\/+$/, "");
|
|
187
|
+
}
|
|
188
|
+
function isAbort(e) {
|
|
189
|
+
return typeof e === "object" && e !== null && e.name === "AbortError";
|
|
190
|
+
}
|
|
191
|
+
function abortError() {
|
|
192
|
+
const e = new Error("The operation was aborted.");
|
|
193
|
+
e.name = "AbortError";
|
|
194
|
+
return e;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Settles with `promise`, or rejects the moment `signal` aborts.
|
|
198
|
+
*
|
|
199
|
+
* A global `fetch` honours the signal for headers and body alike, but an
|
|
200
|
+
* injected {@link FetchLike} — a proxy, an instrumented transport, a test
|
|
201
|
+
* double — need not. Racing the signal makes the timeout an upper bound on the
|
|
202
|
+
* whole request regardless of what the transport does with it. The loser's
|
|
203
|
+
* rejection is swallowed by the already-settled promise rather than surfacing
|
|
204
|
+
* as an unhandled rejection.
|
|
205
|
+
*/
|
|
206
|
+
function untilAborted(promise, signal) {
|
|
207
|
+
if (!signal)
|
|
208
|
+
return promise;
|
|
209
|
+
if (signal.aborted)
|
|
210
|
+
return Promise.reject(abortError());
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
const onAbort = () => reject(abortError());
|
|
213
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
214
|
+
const done = () => signal.removeEventListener("abort", onAbort);
|
|
215
|
+
promise.then((value) => {
|
|
216
|
+
done();
|
|
217
|
+
resolve(value);
|
|
218
|
+
}, (error) => {
|
|
219
|
+
done();
|
|
220
|
+
reject(error);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Combines the request timeout with any caller-supplied signal.
|
|
226
|
+
*
|
|
227
|
+
* `didTimeout` is tracked separately from the abort so a caller's own
|
|
228
|
+
* cancellation is not mislabelled as a timeout in the thrown message.
|
|
229
|
+
*/
|
|
230
|
+
function makeSignal(timeoutMs, external) {
|
|
231
|
+
if (!timeoutMs && !external)
|
|
232
|
+
return { cleanup: () => { }, didTimeout: () => false };
|
|
233
|
+
if (typeof AbortController !== "function") {
|
|
234
|
+
return {
|
|
235
|
+
...(external ? { signal: external } : {}),
|
|
236
|
+
cleanup: () => { },
|
|
237
|
+
didTimeout: () => false,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const controller = new AbortController();
|
|
241
|
+
let timedOut = false;
|
|
242
|
+
let timer;
|
|
243
|
+
if (timeoutMs > 0) {
|
|
244
|
+
timer = setTimeout(() => {
|
|
245
|
+
timedOut = true;
|
|
246
|
+
controller.abort();
|
|
247
|
+
}, timeoutMs);
|
|
248
|
+
}
|
|
249
|
+
const onExternalAbort = () => controller.abort();
|
|
250
|
+
if (external) {
|
|
251
|
+
if (external.aborted)
|
|
252
|
+
controller.abort();
|
|
253
|
+
else
|
|
254
|
+
external.addEventListener("abort", onExternalAbort, { once: true });
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
signal: controller.signal,
|
|
258
|
+
cleanup: () => {
|
|
259
|
+
if (timer !== undefined)
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
external?.removeEventListener("abort", onExternalAbort);
|
|
262
|
+
},
|
|
263
|
+
didTimeout: () => timedOut,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n UnfazedAuthError,\n UnfazedConfigError,\n UnfazedGraphQLError,\n UnfazedHTTPError,\n UnfazedTimeoutError,\n type GraphQLResponseError,\n} from \"./errors.js\";\n\n/** SDK version, sent as the User-Agent. Keep in sync with package.json. */\nexport const VERSION = \"0.1.0\";\n\n/**\n * Default GraphQL endpoint.\n *\n * Note this is the FULL endpoint, not an origin: the platform's own documented\n * convention (`UNFZ_API_URL=\"https://unfazed.dev/query\"`, and cmd/showcase-deploy\n * in the unfazed repo) treats the /query URL as the configurable unit, so the\n * SDK matches it rather than inventing a base-URL convention.\n */\nexport const DEFAULT_ENDPOINT = \"https://unfazed.dev/query\";\n\n/** Platform origin, used to derive the endpoint when only `baseUrl` is given. */\nexport const DEFAULT_BASE_URL = \"https://unfazed.dev\";\n\n/** Path of the GraphQL endpoint under an origin. */\nexport const GRAPHQL_PATH = \"/query\";\n\n/**\n * Environment variable holding the bearer token.\n *\n * This is the platform's documented name (docs/agent-deployments.md,\n * internal/agentdocs/reference.md). It accepts either an operator API token or\n * an agent access token from POST /agents/register — /query takes both as\n * plain bearers.\n */\nexport const TOKEN_ENV_VAR = \"UNFZ_API_TOKEN\";\n\n/** Environment variable holding the full GraphQL endpoint URL. */\nexport const ENDPOINT_ENV_VAR = \"UNFZ_API_URL\";\n\n/** Minimal structural type of the `fetch` the transport needs. */\nexport type FetchLike = (\n input: string,\n init: {\n method: string;\n headers: Record<string, string>;\n body: string;\n signal?: AbortSignal;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n text(): Promise<string>;\n}>;\n\nexport interface ClientOptions {\n /**\n * Bearer token. Falls back to `UNFZ_API_TOKEN`. Get one from\n * https://unfazed.dev/auth.md — the same token the MCP server uses.\n */\n token?: string;\n /**\n * Full GraphQL endpoint URL. Wins over `baseUrl`. Falls back to\n * `UNFZ_API_URL`, then {@link DEFAULT_ENDPOINT}.\n */\n endpoint?: string;\n /**\n * Platform origin; `/query` is appended. Convenience for self-hosted or\n * staging control planes where only the host differs.\n */\n baseUrl?: string;\n /** Extra headers merged into every request. Authorization always wins. */\n headers?: Record<string, string>;\n /**\n * Per-request timeout in ms. Default 60_000. Pass 0 to disable.\n *\n * Bounds the WHOLE request — connect, headers and reading the response body —\n * so a proxy that answers with headers and then stalls still fails on time.\n */\n timeoutMs?: number;\n /** Injection point for tests and instrumented/proxied transports. */\n fetch?: FetchLike;\n /** Overrides the default `unfazed-sdk/<version>` User-Agent. */\n userAgent?: string;\n}\n\n/**\n * A GraphQL document.\n *\n * Structural rather than `string & {...}` on purpose: graphql-codegen emits\n * `TypedDocumentString`, a String SUBCLASS, which is not assignable to the\n * primitive `string`. Requiring only `toString()` plus the optional phantom\n * `__apiType` lets three things through with full inference:\n *\n * - generated `TypedDocumentString` documents (types inferred),\n * - documents assembled at runtime, like the batched writeFiles mutation,\n * - plain template-literal strings a caller wrote by hand.\n */\nexport interface GraphQLDocument<TResult, TVariables> {\n /** Phantom type carrier. Never called; present only to bind the generics. */\n __apiType?: (variables: TVariables) => TResult;\n toString(): string;\n}\n\n/** Result type carried by a typed document. */\nexport type ResultOf<T> = T extends GraphQLDocument<infer R, infer _V> ? R : unknown;\n\n/** Variables type carried by a typed document. */\nexport type VariablesOf<T> = T extends GraphQLDocument<infer _R, infer V> ? V : never;\n\n/** Per-request knobs. */\nexport interface RequestOptions {\n signal?: AbortSignal;\n timeoutMs?: number;\n operationName?: string;\n}\n\n/**\n * Layer 1 — a typed, dependency-free GraphQL transport over `fetch`.\n *\n * This is the SDK's entire networking surface. It understands auth, timeouts\n * and the GraphQL error envelope, and nothing about deployments. The workflow\n * layer ({@link import(\"./unfazed.js\").Unfazed}) is built strictly on top of\n * it, so you can always drop down here for any operation the workflow layer\n * does not wrap — the schema has ~660 fields and the workflow layer\n * deliberately covers only the agent-facing ones.\n *\n * ```ts\n * const client = new UnfazedClient(); // reads UNFZ_API_TOKEN\n * const data = await client.query<{ whoami: { name: string } }, never>(\n * `query { whoami { name } }`,\n * );\n * ```\n */\nexport class UnfazedClient {\n /** Resolved GraphQL endpoint URL. */\n readonly endpoint: string;\n\n readonly #token: string;\n readonly #headers: Record<string, string>;\n readonly #timeoutMs: number;\n readonly #fetch: FetchLike;\n\n constructor(options: ClientOptions = {}) {\n const token = options.token ?? readEnv(TOKEN_ENV_VAR);\n if (!token) {\n throw new UnfazedAuthError(\n `unfazed: no API token. Pass { token } or set ${TOKEN_ENV_VAR}. ` +\n `Get one at ${DEFAULT_BASE_URL}/auth.md`,\n );\n }\n this.#token = token.trim();\n\n this.endpoint = resolveEndpoint(options);\n\n this.#headers = { ...(options.headers ?? {}) };\n this.#timeoutMs = options.timeoutMs ?? 60_000;\n\n const injected = options.fetch;\n if (injected) {\n this.#fetch = injected;\n } else {\n const globalFetch = (globalThis as { fetch?: unknown }).fetch;\n if (typeof globalFetch !== \"function\") {\n throw new UnfazedConfigError(\n \"unfazed: no global fetch. Use Bun, Node >= 20, or pass { fetch } to the client.\",\n );\n }\n this.#fetch = globalFetch.bind(globalThis) as FetchLike;\n }\n\n const ua = options.userAgent ?? `unfazed-sdk/${VERSION}`;\n if (ua) this.#headers[\"User-Agent\"] = ua;\n }\n\n /** Runs a query document. Identical wire behaviour to {@link mutate}. */\n query<TResult, TVariables>(\n document: GraphQLDocument<TResult, TVariables>,\n variables?: TVariables,\n options?: RequestOptions,\n ): Promise<TResult> {\n return this.request(document, variables, options);\n }\n\n /**\n * Runs a mutation document. Distinct from {@link query} for call-site\n * readability only — GraphQL over HTTP POST makes no distinction.\n */\n mutate<TResult, TVariables>(\n document: GraphQLDocument<TResult, TVariables>,\n variables?: TVariables,\n options?: RequestOptions,\n ): Promise<TResult> {\n return this.request(document, variables, options);\n }\n\n /**\n * The single code path behind {@link query} and {@link mutate}.\n *\n * Throws {@link UnfazedHTTPError} on non-2xx, {@link UnfazedGraphQLError}\n * when the body carries `errors`, {@link UnfazedTimeoutError} on timeout or\n * abort. Resolves to the `data` object on success.\n */\n async request<TResult = unknown>(\n document: string | { toString(): string },\n variables?: unknown,\n options: RequestOptions = {},\n ): Promise<TResult> {\n const timeoutMs = options.timeoutMs ?? this.#timeoutMs;\n const { signal, cleanup, didTimeout } = makeSignal(timeoutMs, options.signal);\n\n // `document` may be a TypedDocumentString (a String object). JSON.stringify\n // would serialise that fine, but String(...) keeps the wire body a plain\n // primitive regardless of how the caller obtained the document.\n const body = JSON.stringify({\n query: String(document),\n ...(variables === undefined ? {} : { variables }),\n ...(options.operationName ? { operationName: options.operationName } : {}),\n });\n\n // The deadline covers BOTH phases — headers AND body. A proxy that answers\n // with headers and then stalls the body is a real failure mode, and\n // stopping the clock at the end of `fetch()` would leave `response.text()`\n // hanging forever. `untilAborted` additionally races the signal itself, so\n // an injected transport that ignores `signal` still cannot outlive the\n // timeout.\n let response: Awaited<ReturnType<FetchLike>>;\n let text: string;\n try {\n response = await untilAborted(\n this.#fetch(this.endpoint, {\n method: \"POST\",\n headers: {\n ...this.#headers,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n Authorization: `Bearer ${this.#token}`,\n },\n body,\n ...(signal ? { signal } : {}),\n }),\n signal,\n );\n text = await untilAborted(response.text(), signal);\n } catch (cause) {\n if (didTimeout()) {\n throw new UnfazedTimeoutError(`unfazed: request timed out after ${timeoutMs}ms`, { cause });\n }\n if (isAbort(cause)) {\n throw new UnfazedTimeoutError(\"unfazed: request aborted\", { cause });\n }\n throw cause;\n } finally {\n cleanup();\n }\n\n if (!response.ok) {\n throw new UnfazedHTTPError(response.status, text, this.endpoint);\n }\n\n let parsed: { data?: TResult; errors?: GraphQLResponseError[] };\n try {\n parsed = JSON.parse(text) as typeof parsed;\n } catch {\n throw new UnfazedHTTPError(response.status, text, this.endpoint);\n }\n\n // A GraphQL 200 can still be a failure, and can be a PARTIAL failure: the\n // batched writeFiles document resolves each aliased field independently.\n // Attaching `data` to the error is what lets a caller see which writes\n // landed before the failing one.\n if (parsed.errors?.length) {\n throw new UnfazedGraphQLError(parsed.errors, parsed.data);\n }\n if (parsed.data === undefined || parsed.data === null) {\n throw new UnfazedGraphQLError(\n [{ message: \"response contained neither data nor errors\" }],\n undefined,\n );\n }\n return parsed.data;\n }\n}\n\nfunction resolveEndpoint(options: ClientOptions): string {\n if (options.endpoint) return options.endpoint;\n if (options.baseUrl) return trimSlash(options.baseUrl) + GRAPHQL_PATH;\n const env = readEnv(ENDPOINT_ENV_VAR);\n // UNFZ_API_URL is documented as the full /query URL and is used verbatim.\n // Tolerate a bare origin anyway, since that is the obvious mistake and\n // guessing right costs nothing.\n if (env) {\n const trimmed = trimSlash(env);\n return /\\/query$|\\/graphql$/.test(trimmed) ? trimmed : trimmed + GRAPHQL_PATH;\n }\n return DEFAULT_ENDPOINT;\n}\n\nfunction readEnv(name: string): string | undefined {\n // Guarded: the SDK must also work in a browser or worker, where there is no\n // `process` at all.\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n ?.env;\n return env?.[name] || undefined;\n}\n\nfunction trimSlash(u: string): string {\n return u.replace(/\\/+$/, \"\");\n}\n\nfunction isAbort(e: unknown): boolean {\n return typeof e === \"object\" && e !== null && (e as { name?: string }).name === \"AbortError\";\n}\n\nfunction abortError(): Error {\n const e = new Error(\"The operation was aborted.\");\n e.name = \"AbortError\";\n return e;\n}\n\n/**\n * Settles with `promise`, or rejects the moment `signal` aborts.\n *\n * A global `fetch` honours the signal for headers and body alike, but an\n * injected {@link FetchLike} — a proxy, an instrumented transport, a test\n * double — need not. Racing the signal makes the timeout an upper bound on the\n * whole request regardless of what the transport does with it. The loser's\n * rejection is swallowed by the already-settled promise rather than surfacing\n * as an unhandled rejection.\n */\nfunction untilAborted<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {\n if (!signal) return promise;\n if (signal.aborted) return Promise.reject(abortError());\n return new Promise<T>((resolve, reject) => {\n const onAbort = () => reject(abortError());\n signal.addEventListener(\"abort\", onAbort, { once: true });\n const done = () => signal.removeEventListener(\"abort\", onAbort);\n promise.then(\n (value) => {\n done();\n resolve(value);\n },\n (error: unknown) => {\n done();\n reject(error as Error);\n },\n );\n });\n}\n\n/**\n * Combines the request timeout with any caller-supplied signal.\n *\n * `didTimeout` is tracked separately from the abort so a caller's own\n * cancellation is not mislabelled as a timeout in the thrown message.\n */\nfunction makeSignal(\n timeoutMs: number,\n external?: AbortSignal,\n): { signal?: AbortSignal; cleanup: () => void; didTimeout: () => boolean } {\n if (!timeoutMs && !external) return { cleanup: () => {}, didTimeout: () => false };\n if (typeof AbortController !== \"function\") {\n return {\n ...(external ? { signal: external } : {}),\n cleanup: () => {},\n didTimeout: () => false,\n };\n }\n\n const controller = new AbortController();\n let timedOut = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n if (timeoutMs > 0) {\n timer = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, timeoutMs);\n }\n\n const onExternalAbort = () => controller.abort();\n if (external) {\n if (external.aborted) controller.abort();\n else external.addEventListener(\"abort\", onExternalAbort, { once: true });\n }\n\n return {\n signal: controller.signal,\n cleanup: () => {\n if (timer !== undefined) clearTimeout(timer);\n external?.removeEventListener(\"abort\", onExternalAbort);\n },\n didTimeout: () => timedOut,\n };\n}\n"]}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* Everything the SDK throws is an {@link UnfazedError}, so a code-mode script
|
|
5
|
+
* can `catch (e) { if (e instanceof UnfazedError) ... }` and still
|
|
6
|
+
* discriminate on the subclass to decide whether to retry.
|
|
7
|
+
*/
|
|
8
|
+
/** A single entry from a GraphQL response's `errors` array. */
|
|
9
|
+
export interface GraphQLResponseError {
|
|
10
|
+
message: string;
|
|
11
|
+
path?: (string | number)[];
|
|
12
|
+
extensions?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
/** Base class for everything this SDK throws. */
|
|
15
|
+
export declare class UnfazedError extends Error {
|
|
16
|
+
constructor(message: string, options?: {
|
|
17
|
+
cause?: unknown;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** No token was passed and no token environment variable was set. */
|
|
21
|
+
export declare class UnfazedAuthError extends UnfazedError {
|
|
22
|
+
}
|
|
23
|
+
/** The runtime has no global `fetch` and none was injected. */
|
|
24
|
+
export declare class UnfazedConfigError extends UnfazedError {
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The endpoint answered with a non-2xx status. `status` is the HTTP status and
|
|
28
|
+
* `body` the response text, which is where the platform reports non-GraphQL
|
|
29
|
+
* failures such as an expired or wrong-scope bearer token.
|
|
30
|
+
*/
|
|
31
|
+
export declare class UnfazedHTTPError extends UnfazedError {
|
|
32
|
+
readonly status: number;
|
|
33
|
+
readonly body: string;
|
|
34
|
+
constructor(status: number, body: string, url: string);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The endpoint answered 200 with a GraphQL `errors` array.
|
|
38
|
+
*
|
|
39
|
+
* Partial `data` is preserved deliberately: the batched writeFiles document
|
|
40
|
+
* resolves each aliased field independently, so one path can fail while the
|
|
41
|
+
* others commit, and the caller needs to see which landed.
|
|
42
|
+
*/
|
|
43
|
+
export declare class UnfazedGraphQLError extends UnfazedError {
|
|
44
|
+
readonly errors: GraphQLResponseError[];
|
|
45
|
+
readonly data: unknown;
|
|
46
|
+
constructor(errors: GraphQLResponseError[], data: unknown);
|
|
47
|
+
}
|
|
48
|
+
/** A request exceeded `timeoutMs`, or the caller's AbortSignal fired. */
|
|
49
|
+
export declare class UnfazedTimeoutError extends UnfazedError {
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Waiting on a deploy ended badly.
|
|
53
|
+
*
|
|
54
|
+
* `reason` separates the two outcomes because they need different handling:
|
|
55
|
+
* FAILED means the build finished and is broken (read `deployment.error` and
|
|
56
|
+
* fix the source — retrying will not help), TIMEOUT means it is still BUILDING
|
|
57
|
+
* and polling longer may still succeed.
|
|
58
|
+
*/
|
|
59
|
+
export declare class UnfazedDeployError extends UnfazedError {
|
|
60
|
+
readonly reason: "FAILED" | "TIMEOUT";
|
|
61
|
+
readonly deploymentId: string;
|
|
62
|
+
/** Last deployment snapshot observed before giving up. */
|
|
63
|
+
readonly deployment: unknown;
|
|
64
|
+
constructor(reason: "FAILED" | "TIMEOUT", deploymentId: string, deployment: unknown, message: string);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A rollback's compare-and-swap guard was rejected: the alias moved between
|
|
68
|
+
* reading the current release and swapping it.
|
|
69
|
+
*
|
|
70
|
+
* Recovery is always "re-list releases and rebuild the guard" — never "reuse
|
|
71
|
+
* the old tuple", because the server checks the holder id, epoch AND opaque
|
|
72
|
+
* revision to close same-id ABA. `Unfazed.rollback` does that re-list for you
|
|
73
|
+
* once by default.
|
|
74
|
+
*/
|
|
75
|
+
export declare class UnfazedConflictError extends UnfazedError {
|
|
76
|
+
readonly errors: GraphQLResponseError[];
|
|
77
|
+
constructor(message: string, errors: GraphQLResponseError[]);
|
|
78
|
+
}
|
|
79
|
+
/** The requested alias, deployment or release does not exist (or was pruned). */
|
|
80
|
+
export declare class UnfazedNotFoundError extends UnfazedError {
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Heuristic: does this GraphQL error set look like a rejected compare-and-swap?
|
|
84
|
+
*
|
|
85
|
+
* The server carries no machine-readable code in `extensions` — its error
|
|
86
|
+
* presenter returns gqlgen's default untouched — so this has to match message
|
|
87
|
+
* text. It is used ONLY to decide whether to re-list and retry once: a false
|
|
88
|
+
* negative costs a surfaced error the caller can retry by hand, a false
|
|
89
|
+
* positive costs an extra read and a misleading error class. Neither can
|
|
90
|
+
* promote the wrong release, because the retry rebuilds the guard from a fresh
|
|
91
|
+
* read and the server re-validates it.
|
|
92
|
+
*/
|
|
93
|
+
export declare function looksLikeCASConflict(errors: GraphQLResponseError[]): boolean;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* Everything the SDK throws is an {@link UnfazedError}, so a code-mode script
|
|
5
|
+
* can `catch (e) { if (e instanceof UnfazedError) ... }` and still
|
|
6
|
+
* discriminate on the subclass to decide whether to retry.
|
|
7
|
+
*/
|
|
8
|
+
/** Base class for everything this SDK throws. */
|
|
9
|
+
export class UnfazedError extends Error {
|
|
10
|
+
constructor(message, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = new.target.name;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** No token was passed and no token environment variable was set. */
|
|
16
|
+
export class UnfazedAuthError extends UnfazedError {
|
|
17
|
+
}
|
|
18
|
+
/** The runtime has no global `fetch` and none was injected. */
|
|
19
|
+
export class UnfazedConfigError extends UnfazedError {
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The endpoint answered with a non-2xx status. `status` is the HTTP status and
|
|
23
|
+
* `body` the response text, which is where the platform reports non-GraphQL
|
|
24
|
+
* failures such as an expired or wrong-scope bearer token.
|
|
25
|
+
*/
|
|
26
|
+
export class UnfazedHTTPError extends UnfazedError {
|
|
27
|
+
status;
|
|
28
|
+
body;
|
|
29
|
+
constructor(status, body, url) {
|
|
30
|
+
super(`unfazed: HTTP ${status} from ${url}${body ? `: ${truncate(body)}` : ""}`);
|
|
31
|
+
this.status = status;
|
|
32
|
+
this.body = body;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The endpoint answered 200 with a GraphQL `errors` array.
|
|
37
|
+
*
|
|
38
|
+
* Partial `data` is preserved deliberately: the batched writeFiles document
|
|
39
|
+
* resolves each aliased field independently, so one path can fail while the
|
|
40
|
+
* others commit, and the caller needs to see which landed.
|
|
41
|
+
*/
|
|
42
|
+
export class UnfazedGraphQLError extends UnfazedError {
|
|
43
|
+
errors;
|
|
44
|
+
data;
|
|
45
|
+
constructor(errors, data) {
|
|
46
|
+
const summary = errors.map((e) => e.message).join("; ") || "unknown GraphQL error";
|
|
47
|
+
super(`unfazed: ${summary}`);
|
|
48
|
+
this.errors = errors;
|
|
49
|
+
this.data = data;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** A request exceeded `timeoutMs`, or the caller's AbortSignal fired. */
|
|
53
|
+
export class UnfazedTimeoutError extends UnfazedError {
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Waiting on a deploy ended badly.
|
|
57
|
+
*
|
|
58
|
+
* `reason` separates the two outcomes because they need different handling:
|
|
59
|
+
* FAILED means the build finished and is broken (read `deployment.error` and
|
|
60
|
+
* fix the source — retrying will not help), TIMEOUT means it is still BUILDING
|
|
61
|
+
* and polling longer may still succeed.
|
|
62
|
+
*/
|
|
63
|
+
export class UnfazedDeployError extends UnfazedError {
|
|
64
|
+
reason;
|
|
65
|
+
deploymentId;
|
|
66
|
+
/** Last deployment snapshot observed before giving up. */
|
|
67
|
+
deployment;
|
|
68
|
+
constructor(reason, deploymentId, deployment, message) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.reason = reason;
|
|
71
|
+
this.deploymentId = deploymentId;
|
|
72
|
+
this.deployment = deployment;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A rollback's compare-and-swap guard was rejected: the alias moved between
|
|
77
|
+
* reading the current release and swapping it.
|
|
78
|
+
*
|
|
79
|
+
* Recovery is always "re-list releases and rebuild the guard" — never "reuse
|
|
80
|
+
* the old tuple", because the server checks the holder id, epoch AND opaque
|
|
81
|
+
* revision to close same-id ABA. `Unfazed.rollback` does that re-list for you
|
|
82
|
+
* once by default.
|
|
83
|
+
*/
|
|
84
|
+
export class UnfazedConflictError extends UnfazedError {
|
|
85
|
+
errors;
|
|
86
|
+
constructor(message, errors) {
|
|
87
|
+
super(message);
|
|
88
|
+
this.errors = errors;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** The requested alias, deployment or release does not exist (or was pruned). */
|
|
92
|
+
export class UnfazedNotFoundError extends UnfazedError {
|
|
93
|
+
}
|
|
94
|
+
function truncate(s, max = 500) {
|
|
95
|
+
return s.length <= max ? s : `${s.slice(0, max)}…`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Message shapes that mean "the alias moved under you".
|
|
99
|
+
*
|
|
100
|
+
* The first two are the platform's ACTUAL strings. A rejected rollback guard
|
|
101
|
+
* returns `store.ErrAliasMoved` verbatim — every guard point in the rollback
|
|
102
|
+
* and adopt paths returns that one sentinel unwrapped — and the staged-service
|
|
103
|
+
* rollback wraps the same guard with its own prefix. The rest are defensive:
|
|
104
|
+
* the guard ARGUMENTS named back at you, or the alias-revision concept in any
|
|
105
|
+
* spelling, or `alias` sitting in the same clause as a conflict verb, so a
|
|
106
|
+
* rewording on the server side does not silently disable the retry.
|
|
107
|
+
*
|
|
108
|
+
* Deliberately NOT matched: a bare "conflict", "changed" or "stale" anywhere in
|
|
109
|
+
* any message. Those fire on errors that have nothing to do with the alias
|
|
110
|
+
* compare-and-swap, and a false positive is not free — it spends a pointless
|
|
111
|
+
* re-list AND rewraps an unrelated failure as {@link UnfazedConflictError},
|
|
112
|
+
* whose message would then blame concurrent deploys for, say, a permission
|
|
113
|
+
* problem.
|
|
114
|
+
*/
|
|
115
|
+
const CAS_PATTERNS = [
|
|
116
|
+
/alias was repointed by another change/i,
|
|
117
|
+
/became stale before alias change/i,
|
|
118
|
+
/expected[\s_-]*current/i,
|
|
119
|
+
/alias[\s_-]*revision/i,
|
|
120
|
+
/\balias\b[^.;]{0,80}\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\b/i,
|
|
121
|
+
/\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\b[^.;]{0,80}\balias\b/i,
|
|
122
|
+
];
|
|
123
|
+
/**
|
|
124
|
+
* Heuristic: does this GraphQL error set look like a rejected compare-and-swap?
|
|
125
|
+
*
|
|
126
|
+
* The server carries no machine-readable code in `extensions` — its error
|
|
127
|
+
* presenter returns gqlgen's default untouched — so this has to match message
|
|
128
|
+
* text. It is used ONLY to decide whether to re-list and retry once: a false
|
|
129
|
+
* negative costs a surfaced error the caller can retry by hand, a false
|
|
130
|
+
* positive costs an extra read and a misleading error class. Neither can
|
|
131
|
+
* promote the wrong release, because the retry rebuilds the guard from a fresh
|
|
132
|
+
* read and the server re-validates it.
|
|
133
|
+
*/
|
|
134
|
+
export function looksLikeCASConflict(errors) {
|
|
135
|
+
return errors.some((e) => CAS_PATTERNS.some((pattern) => pattern.test(e.message)));
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Error taxonomy.\n *\n * Everything the SDK throws is an {@link UnfazedError}, so a code-mode script\n * can `catch (e) { if (e instanceof UnfazedError) ... }` and still\n * discriminate on the subclass to decide whether to retry.\n */\n\n/** A single entry from a GraphQL response's `errors` array. */\nexport interface GraphQLResponseError {\n message: string;\n path?: (string | number)[];\n extensions?: Record<string, unknown>;\n}\n\n/** Base class for everything this SDK throws. */\nexport class UnfazedError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = new.target.name;\n }\n}\n\n/** No token was passed and no token environment variable was set. */\nexport class UnfazedAuthError extends UnfazedError {}\n\n/** The runtime has no global `fetch` and none was injected. */\nexport class UnfazedConfigError extends UnfazedError {}\n\n/**\n * The endpoint answered with a non-2xx status. `status` is the HTTP status and\n * `body` the response text, which is where the platform reports non-GraphQL\n * failures such as an expired or wrong-scope bearer token.\n */\nexport class UnfazedHTTPError extends UnfazedError {\n readonly status: number;\n readonly body: string;\n\n constructor(status: number, body: string, url: string) {\n super(`unfazed: HTTP ${status} from ${url}${body ? `: ${truncate(body)}` : \"\"}`);\n this.status = status;\n this.body = body;\n }\n}\n\n/**\n * The endpoint answered 200 with a GraphQL `errors` array.\n *\n * Partial `data` is preserved deliberately: the batched writeFiles document\n * resolves each aliased field independently, so one path can fail while the\n * others commit, and the caller needs to see which landed.\n */\nexport class UnfazedGraphQLError extends UnfazedError {\n readonly errors: GraphQLResponseError[];\n readonly data: unknown;\n\n constructor(errors: GraphQLResponseError[], data: unknown) {\n const summary = errors.map((e) => e.message).join(\"; \") || \"unknown GraphQL error\";\n super(`unfazed: ${summary}`);\n this.errors = errors;\n this.data = data;\n }\n}\n\n/** A request exceeded `timeoutMs`, or the caller's AbortSignal fired. */\nexport class UnfazedTimeoutError extends UnfazedError {}\n\n/**\n * Waiting on a deploy ended badly.\n *\n * `reason` separates the two outcomes because they need different handling:\n * FAILED means the build finished and is broken (read `deployment.error` and\n * fix the source — retrying will not help), TIMEOUT means it is still BUILDING\n * and polling longer may still succeed.\n */\nexport class UnfazedDeployError extends UnfazedError {\n readonly reason: \"FAILED\" | \"TIMEOUT\";\n readonly deploymentId: string;\n /** Last deployment snapshot observed before giving up. */\n readonly deployment: unknown;\n\n constructor(\n reason: \"FAILED\" | \"TIMEOUT\",\n deploymentId: string,\n deployment: unknown,\n message: string,\n ) {\n super(message);\n this.reason = reason;\n this.deploymentId = deploymentId;\n this.deployment = deployment;\n }\n}\n\n/**\n * A rollback's compare-and-swap guard was rejected: the alias moved between\n * reading the current release and swapping it.\n *\n * Recovery is always \"re-list releases and rebuild the guard\" — never \"reuse\n * the old tuple\", because the server checks the holder id, epoch AND opaque\n * revision to close same-id ABA. `Unfazed.rollback` does that re-list for you\n * once by default.\n */\nexport class UnfazedConflictError extends UnfazedError {\n readonly errors: GraphQLResponseError[];\n\n constructor(message: string, errors: GraphQLResponseError[]) {\n super(message);\n this.errors = errors;\n }\n}\n\n/** The requested alias, deployment or release does not exist (or was pruned). */\nexport class UnfazedNotFoundError extends UnfazedError {}\n\nfunction truncate(s: string, max = 500): string {\n return s.length <= max ? s : `${s.slice(0, max)}…`;\n}\n\n/**\n * Message shapes that mean \"the alias moved under you\".\n *\n * The first two are the platform's ACTUAL strings. A rejected rollback guard\n * returns `store.ErrAliasMoved` verbatim — every guard point in the rollback\n * and adopt paths returns that one sentinel unwrapped — and the staged-service\n * rollback wraps the same guard with its own prefix. The rest are defensive:\n * the guard ARGUMENTS named back at you, or the alias-revision concept in any\n * spelling, or `alias` sitting in the same clause as a conflict verb, so a\n * rewording on the server side does not silently disable the retry.\n *\n * Deliberately NOT matched: a bare \"conflict\", \"changed\" or \"stale\" anywhere in\n * any message. Those fire on errors that have nothing to do with the alias\n * compare-and-swap, and a false positive is not free — it spends a pointless\n * re-list AND rewraps an unrelated failure as {@link UnfazedConflictError},\n * whose message would then blame concurrent deploys for, say, a permission\n * problem.\n */\nconst CAS_PATTERNS: RegExp[] = [\n /alias was repointed by another change/i,\n /became stale before alias change/i,\n /expected[\\s_-]*current/i,\n /alias[\\s_-]*revision/i,\n /\\balias\\b[^.;]{0,80}\\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\\b/i,\n /\\b(?:moved|repointed|changed|conflicts?|stale|mismatch)\\b[^.;]{0,80}\\balias\\b/i,\n];\n\n/**\n * Heuristic: does this GraphQL error set look like a rejected compare-and-swap?\n *\n * The server carries no machine-readable code in `extensions` — its error\n * presenter returns gqlgen's default untouched — so this has to match message\n * text. It is used ONLY to decide whether to re-list and retry once: a false\n * negative costs a surfaced error the caller can retry by hand, a false\n * positive costs an extra read and a misleading error class. Neither can\n * promote the wrong release, because the retry rebuilds the guard from a fresh\n * read and the server re-validates it.\n */\nexport function looksLikeCASConflict(errors: GraphQLResponseError[]): boolean {\n return errors.some((e) => CAS_PATTERNS.some((pattern) => pattern.test(e.message)));\n}\n"]}
|