stitchkit 0.78.0 → 0.80.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/dist/agent-runtime-browser.js +53 -581
- package/dist/agent-runtime-coding-tools.js +6 -5
- package/dist/agent-runtime-harness.js +21 -94
- package/dist/agent-runtime-openrouter.js +1 -1
- package/dist/agent-runtime.js +68 -64
- package/dist/application/kernel.d.ts.map +1 -1
- package/dist/application/schemas.d.ts +2 -0
- package/dist/application/schemas.d.ts.map +1 -1
- package/dist/application/watch-hub.d.ts.map +1 -1
- package/dist/application-diagnostic-journal.js +14 -96
- package/dist/application-opentelemetry.js +6 -92
- package/dist/application-schemas.js +2 -1
- package/dist/application.js +158 -1802
- package/dist/cli.js +13 -11
- package/dist/contract/index.js +7 -4
- package/dist/index-2qzskm8h.js +63 -0
- package/dist/{index-93xp5tgg.js → index-2rwwmr7v.js} +3 -3
- package/dist/{index-kw12fsbc.js → index-3gye9wzb.js} +7 -3
- package/dist/{index-1ckavx4h.js → index-3z73fh2c.js} +1 -49
- package/dist/index-4fpa9dy2.js +51 -0
- package/dist/{index-cwp38wxn.js → index-5mtc969c.js} +2 -2
- package/dist/index-5s3zajp8.js +192 -0
- package/dist/index-6s7n2v50.js +583 -0
- package/dist/{index-qaajjfp6.js → index-758zx2ha.js} +15 -94
- package/dist/{index-qyrqwr4c.js → index-77fekveh.js} +4 -1
- package/dist/{index-k45qqrdh.js → index-7ag81v2f.js} +13 -166
- package/dist/{index-n1g2xm6a.js → index-7gnkny4z.js} +124 -1
- package/dist/index-7zbps32p.js +89 -0
- package/dist/index-85vfqd7m.js +135 -0
- package/dist/{index-4ntp63ps.js → index-9s6z8vgf.js} +18 -142
- package/dist/index-a916km3r.js +45 -0
- package/dist/index-b9aefrqd.js +20 -0
- package/dist/index-dre2ywck.js +48 -0
- package/dist/{index-s5tn4kdv.js → index-dwf1f2ap.js} +3 -3
- package/dist/{index-wwst0td5.js → index-dx9xehwt.js} +1 -1
- package/dist/{index-5r0ak8fr.js → index-dxesg10d.js} +4 -4
- package/dist/{index-aczggrty.js → index-dxjjv399.js} +2 -2
- package/dist/{index-j6mfaw26.js → index-egaabxkz.js} +3 -3
- package/dist/index-evcnmsc3.js +866 -0
- package/dist/{index-efm026pw.js → index-fxz0v7vg.js} +86 -86
- package/dist/{index-56fm73re.js → index-hb5pf3s4.js} +1 -1
- package/dist/{index-n7apa6sy.js → index-k9ay6v6d.js} +24 -275
- package/dist/index-kzfs85xp.js +9 -0
- package/dist/{index-vbf2p6me.js → index-m9kzax52.js} +91 -89
- package/dist/{index-22by16v6.js → index-mgtxfe6b.js} +1 -1
- package/dist/index-p7hgdyhb.js +22 -0
- package/dist/index-pq95qkbf.js +0 -0
- package/dist/index-t0z5p5w6.js +318 -0
- package/dist/{index-31k1ns1s.js → index-vwp69871.js} +13 -11
- package/dist/index-wc4x00bn.js +103 -0
- package/dist/{index-aewy3x5j.js → index-xcetq6j3.js} +10 -8
- package/dist/{index-7z69kyrh.js → index-y01f4223.js} +24 -40
- package/dist/{index-dmfn9m4x.js → index-ywd0y6m1.js} +20 -341
- package/dist/index.js +46 -407
- package/dist/live.js +6 -3
- package/dist/node.js +17 -14
- package/dist/observability/index.js +8 -7
- package/dist/primitives.js +4 -2
- package/dist/react.js +1 -1
- package/dist/remote.js +7 -3
- package/dist/server/index.js +32 -45
- package/dist/telegram.js +1 -1
- package/dist/testing.js +90 -1576
- package/dist/tool-invoker.js +11 -9
- package/dist/tools/async-operation-contract.d.ts +93 -0
- package/dist/tools/async-operation-contract.d.ts.map +1 -0
- package/dist/tools/async-operation.d.ts +2 -47
- package/dist/tools/async-operation.d.ts.map +1 -1
- package/dist/tools/view-file-contract.d.ts +98 -0
- package/dist/tools/view-file-contract.d.ts.map +1 -0
- package/dist/tools/view-file.d.ts +4 -83
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools-contract.d.ts +29 -0
- package/dist/tools-contract.d.ts.map +1 -0
- package/dist/tools-contract.js +24 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +61 -140
- package/llms-full.txt +86 -3
- package/package.json +9 -6
- package/dist/index-anyqnqcv.js +0 -79
- package/dist/index-ksp6e2ye.js +0 -24
- package/dist/index-vj3vvpaa.js +0 -307
- package/dist/index-wfq8zerm.js +0 -92
package/dist/testing.js
CHANGED
|
@@ -1,670 +1,47 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
createApplication
|
|
3
|
+
} from "./index-evcnmsc3.js";
|
|
4
|
+
import"./index-2k4yrqkc.js";
|
|
5
|
+
import"./index-3gye9wzb.js";
|
|
6
|
+
import"./index-3z73fh2c.js";
|
|
6
7
|
import {
|
|
7
8
|
AgentMessageSchema,
|
|
8
9
|
AgentRunSchema
|
|
9
10
|
} from "./index-x1th9s8c.js";
|
|
10
11
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
parseSSE,
|
|
17
|
-
readBoundedUtf8Lines
|
|
18
|
-
} from "./index-56fm73re.js";
|
|
12
|
+
RealtimeRejectDirectionSchema,
|
|
13
|
+
RealtimeRejectFaultSchema,
|
|
14
|
+
RealtimeRejectPhaseSchema,
|
|
15
|
+
RealtimeRejectReasonSchema
|
|
16
|
+
} from "./index-p7hgdyhb.js";
|
|
19
17
|
import {
|
|
20
18
|
RealtimeRequestDisconnectedError,
|
|
21
19
|
RealtimeRequestInvalidAcknowledgementError,
|
|
22
|
-
RealtimeRequestTimeoutError
|
|
23
|
-
|
|
24
|
-
} from "./index-1ckavx4h.js";
|
|
20
|
+
RealtimeRequestTimeoutError
|
|
21
|
+
} from "./index-2qzskm8h.js";
|
|
25
22
|
import {
|
|
26
23
|
mcpProjectionCandidate,
|
|
27
24
|
prepareProjectedMcpTools,
|
|
28
25
|
projectToolSurface
|
|
29
|
-
} from "./index-
|
|
26
|
+
} from "./index-mgtxfe6b.js";
|
|
30
27
|
import {
|
|
31
28
|
toJsonSchema
|
|
32
29
|
} from "./index-cby4ar3v.js";
|
|
33
|
-
import"./index-efm026pw.js";
|
|
34
|
-
import {
|
|
35
|
-
ContractStreamFrameSchema,
|
|
36
|
-
DEFAULT_CONTRACT_STREAM_FRAME_BYTES,
|
|
37
|
-
joinRoutePath,
|
|
38
|
-
parseTrailingWildcard
|
|
39
|
-
} from "./index-vj3vvpaa.js";
|
|
40
30
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
31
|
+
createClient,
|
|
32
|
+
createClients
|
|
33
|
+
} from "./index-7ag81v2f.js";
|
|
34
|
+
import"./index-hb5pf3s4.js";
|
|
35
|
+
import"./index-fxz0v7vg.js";
|
|
45
36
|
import {
|
|
46
|
-
|
|
47
|
-
} from "./index-
|
|
37
|
+
joinRoutePath
|
|
38
|
+
} from "./index-y01f4223.js";
|
|
48
39
|
import"./index-6k1937bx.js";
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
super(cause === "caller" ? "Request was aborted" : "Request timed out");
|
|
55
|
-
this.cause = cause;
|
|
56
|
-
this.name = "RequestCancellationError";
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function createRequestCancellation(caller, timeoutMs) {
|
|
60
|
-
if (!caller && timeoutMs === undefined) {
|
|
61
|
-
return { signal: undefined, run: (operation) => operation() };
|
|
62
|
-
}
|
|
63
|
-
const timeoutController = timeoutMs === undefined ? undefined : new AbortController;
|
|
64
|
-
const signal = caller && timeoutController ? AbortSignal.any([caller, timeoutController.signal]) : caller ?? timeoutController?.signal;
|
|
65
|
-
let cause;
|
|
66
|
-
let timer;
|
|
67
|
-
const abortFromCaller = () => {
|
|
68
|
-
if (cause)
|
|
69
|
-
return;
|
|
70
|
-
cause = "caller";
|
|
71
|
-
};
|
|
72
|
-
if (caller?.aborted)
|
|
73
|
-
abortFromCaller();
|
|
74
|
-
else
|
|
75
|
-
caller?.addEventListener("abort", abortFromCaller, { once: true });
|
|
76
|
-
if (timeoutMs !== undefined && timeoutController) {
|
|
77
|
-
timer = setTimeout(() => {
|
|
78
|
-
if (cause)
|
|
79
|
-
return;
|
|
80
|
-
cause = "timeout";
|
|
81
|
-
timeoutController.abort(new DOMException("Request timed out", "TimeoutError"));
|
|
82
|
-
}, timeoutMs);
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
signal,
|
|
86
|
-
async run(operation) {
|
|
87
|
-
try {
|
|
88
|
-
if (cause === "caller")
|
|
89
|
-
throw cancellationError(cause);
|
|
90
|
-
return await operation(signal);
|
|
91
|
-
} catch (error) {
|
|
92
|
-
if (cause)
|
|
93
|
-
throw cancellationError(cause);
|
|
94
|
-
throw error;
|
|
95
|
-
} finally {
|
|
96
|
-
if (timer !== undefined)
|
|
97
|
-
clearTimeout(timer);
|
|
98
|
-
caller?.removeEventListener("abort", abortFromCaller);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
function cancellationError(cause) {
|
|
104
|
-
return new RequestCancellationError(cause);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// src/browser/http.ts
|
|
108
|
-
import ky, {
|
|
109
|
-
isHTTPError,
|
|
110
|
-
isNetworkError,
|
|
111
|
-
isTimeoutError
|
|
112
|
-
} from "ky";
|
|
113
|
-
|
|
114
|
-
// src/browser/request-id.ts
|
|
115
|
-
var REQUEST_ID_HEADER = "x-request-id";
|
|
116
|
-
function responseTraceId(response) {
|
|
117
|
-
return response?.headers.get(REQUEST_ID_HEADER) ?? undefined;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// src/browser/http.ts
|
|
121
|
-
var API_ERROR_BRAND = Symbol.for("stitchkit.ApiError");
|
|
122
|
-
function messageForCode(code, message) {
|
|
123
|
-
return message !== undefined && message.length > 0 ? message : `${code} (no message supplied)`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
class ApiError extends Error {
|
|
127
|
-
code;
|
|
128
|
-
status;
|
|
129
|
-
details;
|
|
130
|
-
hint;
|
|
131
|
-
traceId;
|
|
132
|
-
constructor(code, status = 0, details, message, hint, traceId, options) {
|
|
133
|
-
super(messageForCode(code, message), options);
|
|
134
|
-
this.code = code;
|
|
135
|
-
this.status = status;
|
|
136
|
-
this.details = details;
|
|
137
|
-
this.hint = hint;
|
|
138
|
-
this.traceId = traceId;
|
|
139
|
-
this.name = "ApiError";
|
|
140
|
-
Object.defineProperty(this, API_ERROR_BRAND, { value: true });
|
|
141
|
-
}
|
|
142
|
-
static is(error) {
|
|
143
|
-
return typeof error === "object" && error !== null && API_ERROR_BRAND in error;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function refuseLocally(path, message) {
|
|
147
|
-
return new ApiError("VALIDATION_ERROR", 0, { issues: [{ path, code: "invalid_type", message }] }, message);
|
|
148
|
-
}
|
|
149
|
-
function parseApiErrorBody(body) {
|
|
150
|
-
if (!isRecord(body) || !isRecord(body.error))
|
|
151
|
-
return null;
|
|
152
|
-
const error = body.error;
|
|
153
|
-
if (typeof error.code !== "string")
|
|
154
|
-
return null;
|
|
155
|
-
return {
|
|
156
|
-
code: error.code,
|
|
157
|
-
message: typeof error.message === "string" ? error.message : undefined,
|
|
158
|
-
details: error.details,
|
|
159
|
-
hint: typeof error.hint === "string" ? error.hint : undefined
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// src/browser/client-multipart.ts
|
|
164
|
-
function isFileDescriptor(value) {
|
|
165
|
-
return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
|
|
166
|
-
}
|
|
167
|
-
function isMultipartFile(value) {
|
|
168
|
-
return value instanceof Blob || isFileDescriptor(value);
|
|
169
|
-
}
|
|
170
|
-
function appendMultipartFile(form, field, file) {
|
|
171
|
-
const sink = form;
|
|
172
|
-
sink.append(field, file);
|
|
173
|
-
}
|
|
174
|
-
function appendFormFields(formData, values, skipKeys) {
|
|
175
|
-
for (const [key, value] of Object.entries(values)) {
|
|
176
|
-
if (skipKeys.has(key) || value === undefined || value === null)
|
|
177
|
-
continue;
|
|
178
|
-
formData.append(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
function buildMultipartForm(descriptor, values) {
|
|
182
|
-
const formData = new FormData;
|
|
183
|
-
const fileFields = new Set(Object.keys(descriptor.files));
|
|
184
|
-
for (const [field, policy] of Object.entries(descriptor.files)) {
|
|
185
|
-
const value = values[field];
|
|
186
|
-
if (value === undefined) {
|
|
187
|
-
if (policy.required !== false) {
|
|
188
|
-
throw refuseLocally(field, `Missing multipart file field: ${field}`);
|
|
189
|
-
}
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (policy.multiple === true) {
|
|
193
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
194
|
-
throw refuseLocally(field, `Multipart file field "${field}" must be a non-empty array`);
|
|
195
|
-
}
|
|
196
|
-
for (const file of value) {
|
|
197
|
-
if (!isMultipartFile(file)) {
|
|
198
|
-
throw refuseLocally(field, `Invalid multipart file field: ${field}`);
|
|
199
|
-
}
|
|
200
|
-
appendMultipartFile(formData, field, file);
|
|
201
|
-
}
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (!isMultipartFile(value)) {
|
|
205
|
-
throw refuseLocally(field, `Invalid multipart file field: ${field}`);
|
|
206
|
-
}
|
|
207
|
-
appendMultipartFile(formData, field, value);
|
|
208
|
-
}
|
|
209
|
-
appendFormFields(formData, values, fileFields);
|
|
210
|
-
return formData;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// src/browser/client-url.ts
|
|
214
|
-
function isParamArray(value) {
|
|
215
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string" || typeof item === "number");
|
|
216
|
-
}
|
|
217
|
-
function collectQueryParams(args, endpoint) {
|
|
218
|
-
const params = {};
|
|
219
|
-
let hasParams = false;
|
|
220
|
-
for (const [key, value] of Object.entries(args)) {
|
|
221
|
-
if (value === undefined || value === null)
|
|
222
|
-
continue;
|
|
223
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
|
|
224
|
-
params[key] = value;
|
|
225
|
-
hasParams = true;
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
const what = Array.isArray(value) ? "an array with non-primitive items" : typeof value === "object" ? "a nested object" : `a ${typeof value}`;
|
|
229
|
-
throw refuseLocally(key, `${endpoint.method} ${endpoint.path}: input field "${key}" is ${what} — it cannot ` + "travel as a query parameter. GET / DELETE input must be flat (string / number / " + "boolean, or an array of string / number); flatten the field or move the " + "operation to a body verb (POST).");
|
|
230
|
-
}
|
|
231
|
-
return hasParams ? params : undefined;
|
|
232
|
-
}
|
|
233
|
-
function hasStringKeys(args, keys) {
|
|
234
|
-
for (const key of keys) {
|
|
235
|
-
if (typeof args[key] !== "string")
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
return true;
|
|
239
|
-
}
|
|
240
|
-
function resolvePathPrefix(config, args) {
|
|
241
|
-
if (!config?.pathPrefix)
|
|
242
|
-
return "";
|
|
243
|
-
if (typeof config.pathPrefix === "string")
|
|
244
|
-
return config.pathPrefix;
|
|
245
|
-
const keys = config.stripPrefixKeys ?? [];
|
|
246
|
-
if (!hasStringKeys(args, keys)) {
|
|
247
|
-
const missing = keys.find((key) => typeof args[key] !== "string");
|
|
248
|
-
throw refuseLocally(String(missing), `Missing path prefix key: ${missing}`);
|
|
249
|
-
}
|
|
250
|
-
return config.pathPrefix(args);
|
|
251
|
-
}
|
|
252
|
-
function extractParamNames(path) {
|
|
253
|
-
const matches = path.match(/:(\w+)/g);
|
|
254
|
-
const names = matches ? matches.map((match) => match.slice(1)) : [];
|
|
255
|
-
const wildcard = parseTrailingWildcard(path);
|
|
256
|
-
if (wildcard)
|
|
257
|
-
names.push(wildcard.name);
|
|
258
|
-
return names;
|
|
259
|
-
}
|
|
260
|
-
function fillPathParams(path, args) {
|
|
261
|
-
const wildcard = parseTrailingWildcard(path);
|
|
262
|
-
let filled = path.replace(/:(\w+)/g, (_, key) => {
|
|
263
|
-
const value = args[key];
|
|
264
|
-
if (value === undefined || value === null) {
|
|
265
|
-
throw refuseLocally(String(key), `Missing path param: ${key}`);
|
|
266
|
-
}
|
|
267
|
-
return encodeURIComponent(String(value));
|
|
268
|
-
});
|
|
269
|
-
if (!wildcard)
|
|
270
|
-
return filled;
|
|
271
|
-
const wildcardValue = args[wildcard.name];
|
|
272
|
-
if (wildcardValue === undefined || wildcardValue === null) {
|
|
273
|
-
throw refuseLocally(wildcard.name, `Missing path param: ${wildcard.name}`);
|
|
274
|
-
}
|
|
275
|
-
const remainder = String(wildcardValue).split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
276
|
-
filled = `${filled.slice(0, -(wildcard.name.length + 1))}${remainder}`;
|
|
277
|
-
return filled;
|
|
278
|
-
}
|
|
279
|
-
function stripConsumedArgs(args, path, scopeKeys) {
|
|
280
|
-
const consumed = new Set(scopeKeys);
|
|
281
|
-
for (const name of extractParamNames(path))
|
|
282
|
-
consumed.add(name);
|
|
283
|
-
const remaining = {};
|
|
284
|
-
for (const [key, value] of Object.entries(args)) {
|
|
285
|
-
if (!consumed.has(key) && value !== undefined)
|
|
286
|
-
remaining[key] = value;
|
|
287
|
-
}
|
|
288
|
-
return remaining;
|
|
289
|
-
}
|
|
290
|
-
function appendQuery(relativeUrl, params) {
|
|
291
|
-
if (!params)
|
|
292
|
-
return relativeUrl;
|
|
293
|
-
const search = new URLSearchParams;
|
|
294
|
-
for (const [key, value] of Object.entries(params)) {
|
|
295
|
-
if (Array.isArray(value)) {
|
|
296
|
-
for (const item of value)
|
|
297
|
-
search.append(key, String(item));
|
|
298
|
-
} else {
|
|
299
|
-
search.set(key, String(value));
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return search.size > 0 ? `${relativeUrl}?${search}` : relativeUrl;
|
|
303
|
-
}
|
|
304
|
-
function planClientRequest(endpoint, contractPrefix, args, config) {
|
|
305
|
-
let pathPrefix = resolvePathPrefix(config, args);
|
|
306
|
-
if (pathPrefix && !pathPrefix.endsWith("/"))
|
|
307
|
-
pathPrefix += "/";
|
|
308
|
-
if (pathPrefix.startsWith("/"))
|
|
309
|
-
pathPrefix = pathPrefix.slice(1);
|
|
310
|
-
const endpointPath = endpoint.path === "/" ? "" : endpoint.path;
|
|
311
|
-
let relativeUrl = fillPathParams(`${pathPrefix}${contractPrefix}${endpointPath}`, args);
|
|
312
|
-
if (relativeUrl.endsWith("/"))
|
|
313
|
-
relativeUrl = relativeUrl.slice(0, -1);
|
|
314
|
-
const remainingArgs = stripConsumedArgs(args, endpoint.path, config?.stripPrefixKeys ?? []);
|
|
315
|
-
if (inputIsQuery(endpoint.method)) {
|
|
316
|
-
relativeUrl = appendQuery(relativeUrl, collectQueryParams(remainingArgs, endpoint));
|
|
317
|
-
}
|
|
318
|
-
return { relativeUrl, remainingArgs };
|
|
319
|
-
}
|
|
320
|
-
function joinClientBaseUrl(baseUrl, relativeUrl) {
|
|
321
|
-
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
322
|
-
const path = relativeUrl.startsWith("/") ? relativeUrl : `/${relativeUrl}`;
|
|
323
|
-
return `${base}${path}`;
|
|
324
|
-
}
|
|
325
|
-
// src/browser/stream.ts
|
|
326
|
-
function parseFailure(raw, error, onParseError) {
|
|
327
|
-
const failure = error instanceof Error ? error : new Error(String(error));
|
|
328
|
-
if (!onParseError)
|
|
329
|
-
throw failure;
|
|
330
|
-
onParseError(raw, failure);
|
|
331
|
-
}
|
|
332
|
-
async function* parseNDJSON(response, options) {
|
|
333
|
-
try {
|
|
334
|
-
for await (const rawLine of readBoundedUtf8Lines(response, options?.maxLineBytes ?? DEFAULT_STREAM_LINE_BYTES, options?.finalLine === "require-newline")) {
|
|
335
|
-
const line = (rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine).trim();
|
|
336
|
-
if (line === "")
|
|
337
|
-
continue;
|
|
338
|
-
try {
|
|
339
|
-
yield JSON.parse(line);
|
|
340
|
-
} catch (error) {
|
|
341
|
-
parseFailure(line, error, options?.onParseError);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
} catch (error) {
|
|
345
|
-
parseFailure("", error, options?.onParseError);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// src/browser/contract-stream.ts
|
|
350
|
-
function sourceFor(response, descriptor) {
|
|
351
|
-
const options = {
|
|
352
|
-
maxLineBytes: descriptor.maxFrameBytes ?? DEFAULT_CONTRACT_STREAM_FRAME_BYTES,
|
|
353
|
-
finalLine: descriptor.finalLine
|
|
354
|
-
};
|
|
355
|
-
return descriptor.format === "sse" ? parseSSE(response, options) : parseNDJSON(response, options);
|
|
356
|
-
}
|
|
357
|
-
async function* readContractStream(response, descriptor, abort) {
|
|
358
|
-
let ended = false;
|
|
359
|
-
let terminalSeen = descriptor.terminal === undefined;
|
|
360
|
-
const source = sourceFor(response, descriptor);
|
|
361
|
-
try {
|
|
362
|
-
for (;; ) {
|
|
363
|
-
const next = await source.next();
|
|
364
|
-
if (next.done)
|
|
365
|
-
break;
|
|
366
|
-
let candidate = next.value;
|
|
367
|
-
if (descriptor.framing !== "item") {
|
|
368
|
-
const parsedFrame = ContractStreamFrameSchema.safeParse(candidate);
|
|
369
|
-
if (!parsedFrame.success) {
|
|
370
|
-
throw new ApiError("STREAM_PROTOCOL_ERROR", 0, undefined, "Stream frame did not match the protocol envelope");
|
|
371
|
-
}
|
|
372
|
-
const frame = parsedFrame.data;
|
|
373
|
-
if (frame.type === "error") {
|
|
374
|
-
throw new ApiError(frame.error.code, 0, frame.error.details, frame.error.message, frame.error.hint);
|
|
375
|
-
}
|
|
376
|
-
if (frame.type === "end") {
|
|
377
|
-
if (!terminalSeen) {
|
|
378
|
-
throw new ApiError("STREAM_TERMINAL_MISSING", 0, undefined, "Stream completed before its declared terminal item");
|
|
379
|
-
}
|
|
380
|
-
ended = true;
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
candidate = frame.data;
|
|
384
|
-
}
|
|
385
|
-
const parsedItem = descriptor.item.safeParse(candidate);
|
|
386
|
-
if (!parsedItem.success) {
|
|
387
|
-
throw new ApiError("STREAM_ITEM_INVALID", 0, undefined, "Stream item did not match its contract");
|
|
388
|
-
}
|
|
389
|
-
const item = parsedItem.data;
|
|
390
|
-
const terminal = descriptor.terminal?.safeParse(item).success ?? false;
|
|
391
|
-
if (terminal)
|
|
392
|
-
terminalSeen = true;
|
|
393
|
-
if (terminal && descriptor.completion === "terminal") {
|
|
394
|
-
abort();
|
|
395
|
-
await source.return(undefined);
|
|
396
|
-
ended = true;
|
|
397
|
-
}
|
|
398
|
-
yield item;
|
|
399
|
-
if (ended)
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
if (descriptor.framing === "item") {
|
|
403
|
-
if (terminalSeen)
|
|
404
|
-
return;
|
|
405
|
-
throw new ApiError("STREAM_TERMINAL_MISSING", 0, undefined, "Stream completed before its declared terminal item");
|
|
406
|
-
}
|
|
407
|
-
if (!ended) {
|
|
408
|
-
throw new ApiError("STREAM_TRUNCATED", 0, undefined, "Stream ended without its protocol terminal frame");
|
|
409
|
-
}
|
|
410
|
-
} finally {
|
|
411
|
-
abort();
|
|
412
|
-
await source.return(undefined);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
class OwnedContractStream {
|
|
417
|
-
#inner;
|
|
418
|
-
#abort;
|
|
419
|
-
constructor(inner, abort) {
|
|
420
|
-
this.#inner = inner;
|
|
421
|
-
this.#abort = abort;
|
|
422
|
-
}
|
|
423
|
-
[Symbol.asyncIterator]() {
|
|
424
|
-
return this;
|
|
425
|
-
}
|
|
426
|
-
next() {
|
|
427
|
-
return this.#inner.next();
|
|
428
|
-
}
|
|
429
|
-
return() {
|
|
430
|
-
this.#abort();
|
|
431
|
-
return this.#inner.return(undefined);
|
|
432
|
-
}
|
|
433
|
-
throw(error) {
|
|
434
|
-
this.#abort();
|
|
435
|
-
return this.#inner.throw(error);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
function parseContractStream(response, descriptor, abort) {
|
|
439
|
-
return new OwnedContractStream(readContractStream(response, descriptor, abort), abort);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// src/browser/client.ts
|
|
443
|
-
function withTimeout(options, endpoint) {
|
|
444
|
-
const responseType = endpoint.rawResponse || "stream" in endpoint ? "response" : endpoint.output ? undefined : "void";
|
|
445
|
-
if (endpoint.timeout === undefined && responseType === undefined)
|
|
446
|
-
return options;
|
|
447
|
-
return {
|
|
448
|
-
...options,
|
|
449
|
-
...endpoint.timeout !== undefined && { timeout: endpoint.timeout },
|
|
450
|
-
...responseType && { responseType }
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
function withOutput(endpoint, result, abortStream) {
|
|
454
|
-
if (endpoint.rawResponse)
|
|
455
|
-
return result;
|
|
456
|
-
if ("stream" in endpoint && endpoint.stream) {
|
|
457
|
-
return result.then((value) => {
|
|
458
|
-
if (!(value instanceof Response)) {
|
|
459
|
-
abortStream?.();
|
|
460
|
-
throw new Error("Streaming endpoint did not return a Response");
|
|
461
|
-
}
|
|
462
|
-
return parseContractStream(value, endpoint.stream, abortStream ?? (() => {
|
|
463
|
-
return;
|
|
464
|
-
}));
|
|
465
|
-
}, (error) => {
|
|
466
|
-
abortStream?.();
|
|
467
|
-
throw error;
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
const schema = endpoint.output;
|
|
471
|
-
return result.then((value) => {
|
|
472
|
-
if (!schema) {
|
|
473
|
-
if (value === undefined || value === null)
|
|
474
|
-
return;
|
|
475
|
-
throw new Error("Server returned data for an endpoint with no output contract");
|
|
476
|
-
}
|
|
477
|
-
if (value === undefined) {
|
|
478
|
-
throw new Error("Server returned no body for an endpoint with an output contract");
|
|
479
|
-
}
|
|
480
|
-
return schema.parse(value);
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
function createClient(contract, configOrClient, contractConfig) {
|
|
484
|
-
const client = {};
|
|
485
|
-
const makeExecutor = isHttpAdapter(configOrClient) ? (endpoint) => createHttpExecutor(endpoint, contract.meta.prefix, configOrClient, contractConfig) : (endpoint) => createFetchExecutor(endpoint, contract.meta.prefix, configOrClient, contractConfig);
|
|
486
|
-
for (const [key, endpoint] of typedEntries(contract.endpoints)) {
|
|
487
|
-
if (endpoint.expose && !endpoint.expose.includes("HTTP"))
|
|
488
|
-
continue;
|
|
489
|
-
setClientMethod(client, key, createEndpointMethod(endpoint, makeExecutor(endpoint), contractConfig));
|
|
490
|
-
}
|
|
491
|
-
return client;
|
|
492
|
-
}
|
|
493
|
-
function createClients(contracts, configOrClient, contractConfig) {
|
|
494
|
-
return mapObject(contracts, (_key, contract) => createClient(contract, configOrClient, contractConfig));
|
|
495
|
-
}
|
|
496
|
-
function isHttpAdapter(value) {
|
|
497
|
-
return typeof value === "object" && "get" in value && typeof value.get === "function";
|
|
498
|
-
}
|
|
499
|
-
function setClientMethod(target, key, method) {
|
|
500
|
-
target[key] = method;
|
|
501
|
-
}
|
|
502
|
-
function createEndpointMethod(endpoint, execute, contractConfig) {
|
|
503
|
-
const hasScopedArguments = (contractConfig?.stripPrefixKeys?.length ?? 0) > 0;
|
|
504
|
-
if (endpointHasArguments(endpoint) || hasScopedArguments) {
|
|
505
|
-
const method2 = (requestArgs) => settle(execute, readClientRequestArgs(requestArgs), undefined);
|
|
506
|
-
return Object.assign(method2, {
|
|
507
|
-
withOptions: (...args) => {
|
|
508
|
-
refuseExtraWithOptionsArguments(endpoint, args.length, 2);
|
|
509
|
-
return settle(execute, readClientRequestArgs(args[0]), readClientRequestOptions(args[1]));
|
|
510
|
-
}
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
const method = () => settle(execute, {}, undefined);
|
|
514
|
-
return Object.assign(method, {
|
|
515
|
-
withOptions: (...args) => {
|
|
516
|
-
refuseExtraWithOptionsArguments(endpoint, args.length, 1);
|
|
517
|
-
return settle(execute, {}, readClientRequestOptions(args[0]));
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
function settle(execute, requestArgs, options) {
|
|
522
|
-
try {
|
|
523
|
-
return execute(requestArgs, options);
|
|
524
|
-
} catch (error) {
|
|
525
|
-
return Promise.reject(error);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
function refuseExtraWithOptionsArguments(endpoint, received, expected) {
|
|
529
|
-
if (received <= expected)
|
|
530
|
-
return;
|
|
531
|
-
const shape = expected === 1 ? "withOptions(options)" : "withOptions(args, options)";
|
|
532
|
-
throw new TypeError(`${endpoint.method} ${endpoint.path}: this endpoint declares ${expected === 1 ? "no input" : "an input"}, so its method is ${shape} — it received ${received} arguments. ` + "An extra argument here is dropped, and a request options object in the dropped position " + "sends the request without them: an abort signal placed there never reaches the server.");
|
|
533
|
-
}
|
|
534
|
-
function createHttpExecutor(endpoint, prefix, client, config) {
|
|
535
|
-
const httpMethod = endpoint.method.toLowerCase();
|
|
536
|
-
return (requestArgs, options) => {
|
|
537
|
-
const plan = planClientRequest(endpoint, prefix, requestArgs, config);
|
|
538
|
-
const streamAbort = "stream" in endpoint ? new AbortController : undefined;
|
|
539
|
-
const requestOptions = streamAbort ? {
|
|
540
|
-
...options,
|
|
541
|
-
signal: options?.signal ? AbortSignal.any([options.signal, streamAbort.signal]) : streamAbort.signal
|
|
542
|
-
} : options;
|
|
543
|
-
const finishStream = streamAbort ? () => streamAbort.abort() : undefined;
|
|
544
|
-
if (endpoint.multipart) {
|
|
545
|
-
if (httpMethod === "get" || httpMethod === "head" || httpMethod === "delete") {
|
|
546
|
-
throw new Error(`Multipart endpoint ${endpoint.method} ${endpoint.path} must be POST / PUT / PATCH`);
|
|
547
|
-
}
|
|
548
|
-
const formData = buildMultipartForm(endpoint.multipart, plan.remainingArgs);
|
|
549
|
-
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, formData, withTimeout(requestOptions, endpoint)), finishStream);
|
|
550
|
-
}
|
|
551
|
-
if (httpMethod === "get" || httpMethod === "head") {
|
|
552
|
-
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, withTimeout(requestOptions, endpoint)), finishStream);
|
|
553
|
-
}
|
|
554
|
-
if (httpMethod === "delete") {
|
|
555
|
-
return withOutput(endpoint, client.delete(plan.relativeUrl, withTimeout(requestOptions, endpoint)), finishStream);
|
|
556
|
-
}
|
|
557
|
-
return withOutput(endpoint, client[httpMethod](plan.relativeUrl, Object.keys(plan.remainingArgs).length > 0 ? plan.remainingArgs : undefined, withTimeout(requestOptions, endpoint)), finishStream);
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
function createFetchExecutor(endpoint, prefix, config, contractConfig) {
|
|
561
|
-
const executeFetch = config.fetch ?? globalThis.fetch;
|
|
562
|
-
return async (requestArgs, options) => {
|
|
563
|
-
const plan = planClientRequest(endpoint, prefix, requestArgs, contractConfig);
|
|
564
|
-
const url = joinClientBaseUrl(config.baseUrl, plan.relativeUrl);
|
|
565
|
-
const headers = {
|
|
566
|
-
Accept: "application/json",
|
|
567
|
-
...typeof config.headers === "function" ? config.headers() : config.headers
|
|
568
|
-
};
|
|
569
|
-
const streamAbort = "stream" in endpoint ? new AbortController : undefined;
|
|
570
|
-
const requestSignal = streamAbort ? options?.signal ? AbortSignal.any([options.signal, streamAbort.signal]) : streamAbort.signal : options?.signal;
|
|
571
|
-
const openTimeoutMs = endpoint.timeout ?? config.timeout ?? 30000;
|
|
572
|
-
const cancellation = streamAbort ? {
|
|
573
|
-
signal: requestSignal,
|
|
574
|
-
async run(operation) {
|
|
575
|
-
let timedOut = false;
|
|
576
|
-
const timer = setTimeout(() => {
|
|
577
|
-
timedOut = true;
|
|
578
|
-
streamAbort.abort(new DOMException("Request timed out", "TimeoutError"));
|
|
579
|
-
}, openTimeoutMs);
|
|
580
|
-
try {
|
|
581
|
-
if (options?.signal?.aborted)
|
|
582
|
-
throw new RequestCancellationError("caller");
|
|
583
|
-
return await operation(requestSignal);
|
|
584
|
-
} catch (error) {
|
|
585
|
-
if (timedOut)
|
|
586
|
-
throw new RequestCancellationError("timeout");
|
|
587
|
-
if (options?.signal?.aborted)
|
|
588
|
-
throw new RequestCancellationError("caller");
|
|
589
|
-
throw error;
|
|
590
|
-
} finally {
|
|
591
|
-
clearTimeout(timer);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
} : createRequestCancellation(requestSignal, openTimeoutMs);
|
|
595
|
-
const hasBody = endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && Object.keys(plan.remainingArgs).length > 0;
|
|
596
|
-
if (hasBody)
|
|
597
|
-
headers["Content-Type"] = "application/json";
|
|
598
|
-
try {
|
|
599
|
-
return await cancellation.run(async (signal) => {
|
|
600
|
-
const body = endpoint.multipart ? buildMultipartForm(endpoint.multipart, plan.remainingArgs) : hasBody ? JSON.stringify(plan.remainingArgs) : undefined;
|
|
601
|
-
const res = await executeFetch(url, {
|
|
602
|
-
method: endpoint.method,
|
|
603
|
-
headers,
|
|
604
|
-
credentials: config.credentials,
|
|
605
|
-
signal,
|
|
606
|
-
...body !== undefined && { body }
|
|
607
|
-
});
|
|
608
|
-
if (!res.ok) {
|
|
609
|
-
await throwForErrorResponse(res, config, { error: res.statusText });
|
|
610
|
-
}
|
|
611
|
-
if (endpoint.rawResponse)
|
|
612
|
-
return res;
|
|
613
|
-
if ("stream" in endpoint && endpoint.stream) {
|
|
614
|
-
return parseContractStream(res, endpoint.stream, () => streamAbort?.abort());
|
|
615
|
-
}
|
|
616
|
-
if (!endpoint.output) {
|
|
617
|
-
const text = await res.text();
|
|
618
|
-
if (text.length > 0) {
|
|
619
|
-
throw new Error("Server returned data for an endpoint with no output contract");
|
|
620
|
-
}
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
return endpoint.output.parse(await res.json());
|
|
624
|
-
});
|
|
625
|
-
} catch (error) {
|
|
626
|
-
if (error instanceof RequestCancellationError) {
|
|
627
|
-
throw new ApiError(error.cause === "caller" ? "REQUEST_ABORTED" : "REQUEST_TIMEOUT", 0, undefined, error.message);
|
|
628
|
-
}
|
|
629
|
-
if (ApiError.is(error))
|
|
630
|
-
throw error;
|
|
631
|
-
const message = error instanceof Error ? error.message : undefined;
|
|
632
|
-
throw new ApiError("UNKNOWN_ERROR", 0, message ? { message } : undefined, message, undefined, undefined, { cause: error });
|
|
633
|
-
}
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
function endpointHasArguments(endpoint) {
|
|
637
|
-
return Boolean(endpoint.params || endpoint.input || endpoint.multipart);
|
|
638
|
-
}
|
|
639
|
-
function readClientRequestArgs(requestArgs) {
|
|
640
|
-
if (requestArgs !== undefined && !isRecord(requestArgs)) {
|
|
641
|
-
throw new TypeError("Endpoint arguments must be an object");
|
|
642
|
-
}
|
|
643
|
-
return requestArgs ?? {};
|
|
644
|
-
}
|
|
645
|
-
function readClientRequestOptions(value) {
|
|
646
|
-
if (!isRecord(value))
|
|
647
|
-
throw new TypeError("Client request options must be an object");
|
|
648
|
-
const signal = value.signal;
|
|
649
|
-
if (signal === undefined)
|
|
650
|
-
return {};
|
|
651
|
-
if (!isAbortSignal(signal)) {
|
|
652
|
-
throw new TypeError("Client request signal must be an AbortSignal");
|
|
653
|
-
}
|
|
654
|
-
return { signal };
|
|
655
|
-
}
|
|
656
|
-
function isAbortSignal(value) {
|
|
657
|
-
return typeof value === "object" && value !== null && "aborted" in value && typeof value.aborted === "boolean" && "addEventListener" in value && typeof value.addEventListener === "function" && "removeEventListener" in value && typeof value.removeEventListener === "function";
|
|
658
|
-
}
|
|
659
|
-
async function throwForErrorResponse(res, config, fallbackBody) {
|
|
660
|
-
const body = await res.json().catch(() => fallbackBody);
|
|
661
|
-
config.onError?.(res.status, body);
|
|
662
|
-
const parsed = parseApiErrorBody(body);
|
|
663
|
-
if (parsed) {
|
|
664
|
-
throw new ApiError(parsed.code, res.status, parsed.details, parsed.message, parsed.hint, responseTraceId(res));
|
|
665
|
-
}
|
|
666
|
-
throw new ApiError("HTTP_ERROR", res.status, { body }, undefined, undefined, responseTraceId(res));
|
|
667
|
-
}
|
|
40
|
+
import"./index-0w9abg87.js";
|
|
41
|
+
import"./index-kzfs85xp.js";
|
|
42
|
+
import {
|
|
43
|
+
isRecord
|
|
44
|
+
} from "./index-77fekveh.js";
|
|
668
45
|
|
|
669
46
|
// src/agent-runtime/testing.ts
|
|
670
47
|
function createAgentRaceBarrier(timeoutMs = 5000) {
|
|
@@ -1652,851 +1029,6 @@ function observeManagedResource(resource, required, trace) {
|
|
|
1652
1029
|
};
|
|
1653
1030
|
}
|
|
1654
1031
|
|
|
1655
|
-
// src/application/kernel.ts
|
|
1656
|
-
import { z as z2 } from "zod";
|
|
1657
|
-
|
|
1658
|
-
// src/application/graph.ts
|
|
1659
|
-
function resolveResourceGraph(resources) {
|
|
1660
|
-
const entries = resources.map((resource, declarationIndex) => ({
|
|
1661
|
-
resource,
|
|
1662
|
-
id: ApplicationIdSchema.parse(resource.id),
|
|
1663
|
-
dependsOn: [...resource.dependsOn ?? []].map((dependency) => ApplicationIdSchema.parse(managedResourceDependencyId(dependency))),
|
|
1664
|
-
required: resource.required ?? true,
|
|
1665
|
-
declarationIndex
|
|
1666
|
-
}));
|
|
1667
|
-
const byId = new Map;
|
|
1668
|
-
for (const entry of entries) {
|
|
1669
|
-
if (byId.has(entry.id)) {
|
|
1670
|
-
throw new Error(`[stitchkit] createApplication: duplicate resource id "${entry.id}"`);
|
|
1671
|
-
}
|
|
1672
|
-
byId.set(entry.id, entry);
|
|
1673
|
-
}
|
|
1674
|
-
for (const entry of entries) {
|
|
1675
|
-
for (const dependencyId of entry.dependsOn) {
|
|
1676
|
-
const dependency = byId.get(dependencyId);
|
|
1677
|
-
if (!dependency) {
|
|
1678
|
-
throw new Error(`[stitchkit] createApplication: resource "${entry.id}" depends on missing resource "${dependencyId}"`);
|
|
1679
|
-
}
|
|
1680
|
-
if (entry.required && !dependency.required) {
|
|
1681
|
-
throw new Error(`[stitchkit] createApplication: required resource "${entry.id}" cannot depend on optional resource "${dependencyId}"`);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
const pending = new Map(entries.map((entry) => [entry.id, entry]));
|
|
1686
|
-
const resolved = new Set;
|
|
1687
|
-
const ordered = [];
|
|
1688
|
-
while (pending.size > 0) {
|
|
1689
|
-
const ready = [...pending.values()].filter((entry) => entry.dependsOn.every((id) => resolved.has(id))).sort((left, right) => left.declarationIndex - right.declarationIndex);
|
|
1690
|
-
if (ready.length === 0) {
|
|
1691
|
-
throw new Error(`[stitchkit] createApplication: resource dependency cycle: ${[...pending.keys()].join(", ")}`);
|
|
1692
|
-
}
|
|
1693
|
-
for (const entry of ready) {
|
|
1694
|
-
pending.delete(entry.id);
|
|
1695
|
-
resolved.add(entry.id);
|
|
1696
|
-
ordered.push(entry);
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
return ordered;
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
// src/application/kernel.ts
|
|
1703
|
-
class ResourceCompletionBeforeReadyError extends Error {
|
|
1704
|
-
constructor(resourceId, cause) {
|
|
1705
|
-
super(`[stitchkit] resource "${resourceId}" completed before reaching readiness`, {
|
|
1706
|
-
...cause !== undefined && { cause }
|
|
1707
|
-
});
|
|
1708
|
-
this.name = "ResourceCompletionBeforeReadyError";
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
class ApplicationStartupInterruptedError extends Error {
|
|
1713
|
-
constructor() {
|
|
1714
|
-
super("[stitchkit] application startup interrupted by shutdown");
|
|
1715
|
-
this.name = "ApplicationStartupInterruptedError";
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
var ApplicationShutdownOptionsSchema = ShutdownOptionsSchema.pick({
|
|
1719
|
-
gracePeriodMs: true,
|
|
1720
|
-
forceTimeoutMs: true,
|
|
1721
|
-
signal: true
|
|
1722
|
-
});
|
|
1723
|
-
var ApplicationShutdownBudgetSchema = ShutdownOptionsSchema.pick({
|
|
1724
|
-
gracePeriodMs: true,
|
|
1725
|
-
forceTimeoutMs: true
|
|
1726
|
-
});
|
|
1727
|
-
var ApplicationRestartInputSchema = z2.object({
|
|
1728
|
-
resourceId: z2.string().min(1),
|
|
1729
|
-
gracePeriodMs: z2.number().int().nonnegative().optional(),
|
|
1730
|
-
forceTimeoutMs: z2.number().int().nonnegative().optional()
|
|
1731
|
-
}).strict().readonly();
|
|
1732
|
-
var ApplicationRestartOutcomeSchema = z2.enum(["restarted", "failed", "refused"]);
|
|
1733
|
-
var ApplicationRestartResultSchema = z2.object({
|
|
1734
|
-
resourceId: z2.string(),
|
|
1735
|
-
affected: z2.array(z2.string()).readonly(),
|
|
1736
|
-
outcome: ApplicationRestartOutcomeSchema,
|
|
1737
|
-
reason: z2.string().optional(),
|
|
1738
|
-
durationMs: z2.number().int().nonnegative()
|
|
1739
|
-
}).strict().readonly();
|
|
1740
|
-
|
|
1741
|
-
class ApplicationAdmissionError extends AppError {
|
|
1742
|
-
constructor() {
|
|
1743
|
-
super("APPLICATION_NOT_ACCEPTING", "Application is not accepting new operations", 503);
|
|
1744
|
-
this.name = "ApplicationAdmissionError";
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
function isStartResult(value) {
|
|
1748
|
-
return typeof value === "object" && value !== null;
|
|
1749
|
-
}
|
|
1750
|
-
function waitForAbort(signal) {
|
|
1751
|
-
if (signal.aborted)
|
|
1752
|
-
return Promise.resolve();
|
|
1753
|
-
return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
1754
|
-
}
|
|
1755
|
-
async function untilDeadline(work, signal) {
|
|
1756
|
-
const result = await Promise.race([
|
|
1757
|
-
work.then((value) => ({ settled: true, value }), (error) => ({ settled: true, error })),
|
|
1758
|
-
waitForAbort(signal).then(() => ({ settled: false }))
|
|
1759
|
-
]);
|
|
1760
|
-
return result;
|
|
1761
|
-
}
|
|
1762
|
-
function createApplication(config) {
|
|
1763
|
-
const id = ApplicationIdSchema.parse(config.id);
|
|
1764
|
-
const ordered = resolveResourceGraph(config.resources ?? []);
|
|
1765
|
-
const shutdownBudget = ApplicationShutdownBudgetSchema.parse(config.shutdown ?? {});
|
|
1766
|
-
const reverse = [...ordered].reverse();
|
|
1767
|
-
const reportFailure = (resourceId, phase, error) => {
|
|
1768
|
-
if (!config.onResourceFailure)
|
|
1769
|
-
return;
|
|
1770
|
-
try {
|
|
1771
|
-
Promise.resolve(config.onResourceFailure({ resourceId, phase, error })).catch(() => {
|
|
1772
|
-
return;
|
|
1773
|
-
});
|
|
1774
|
-
} catch {}
|
|
1775
|
-
};
|
|
1776
|
-
const published = new Map;
|
|
1777
|
-
const records = new Map;
|
|
1778
|
-
for (const entry of ordered) {
|
|
1779
|
-
records.set(entry.id, {
|
|
1780
|
-
entry,
|
|
1781
|
-
state: "registered",
|
|
1782
|
-
health: "unknown",
|
|
1783
|
-
healthReported: false,
|
|
1784
|
-
everHealthy: false,
|
|
1785
|
-
attempted: false,
|
|
1786
|
-
activated: false,
|
|
1787
|
-
closeInvoked: false,
|
|
1788
|
-
closed: false,
|
|
1789
|
-
failures: []
|
|
1790
|
-
});
|
|
1791
|
-
}
|
|
1792
|
-
const epoch = crypto.randomUUID();
|
|
1793
|
-
const listeners = new Set;
|
|
1794
|
-
const lifetimeAbort = new AbortController;
|
|
1795
|
-
const startupAbort = new AbortController;
|
|
1796
|
-
let lifecycle = "created";
|
|
1797
|
-
let revision = 0;
|
|
1798
|
-
let changedAt = new Date().toISOString();
|
|
1799
|
-
let accepting = false;
|
|
1800
|
-
let accepted = 0;
|
|
1801
|
-
let completed = 0;
|
|
1802
|
-
let pending = 0;
|
|
1803
|
-
let startPromise;
|
|
1804
|
-
let shutdownPromise;
|
|
1805
|
-
let shutdownRequested = false;
|
|
1806
|
-
let activationComplete = false;
|
|
1807
|
-
const pendingWaiters = new Set;
|
|
1808
|
-
const aggregateHealth = () => {
|
|
1809
|
-
if (lifecycle === "created" || lifecycle === "starting")
|
|
1810
|
-
return "unknown";
|
|
1811
|
-
let optionalUnhealthy = false;
|
|
1812
|
-
for (const record of records.values()) {
|
|
1813
|
-
if (record.entry.required && (record.state !== "ready" || record.health !== "healthy")) {
|
|
1814
|
-
return "unhealthy";
|
|
1815
|
-
}
|
|
1816
|
-
if (!record.entry.required && (record.state !== "ready" || record.health !== "healthy")) {
|
|
1817
|
-
optionalUnhealthy = true;
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
return optionalUnhealthy ? "degraded" : "healthy";
|
|
1821
|
-
};
|
|
1822
|
-
const isReady = () => lifecycle === "ready" && [...records.values()].every((record) => !record.entry.required || record.state === "ready" && record.health === "healthy");
|
|
1823
|
-
const snapshot = () => ApplicationSnapshotSchema.parse({
|
|
1824
|
-
id,
|
|
1825
|
-
epoch,
|
|
1826
|
-
revision,
|
|
1827
|
-
lifecycle,
|
|
1828
|
-
health: aggregateHealth(),
|
|
1829
|
-
ready: isReady(),
|
|
1830
|
-
capturedAt: new Date().toISOString(),
|
|
1831
|
-
changedAt,
|
|
1832
|
-
admission: { accepting, accepted, completed, pending },
|
|
1833
|
-
resources: ordered.map((entry) => {
|
|
1834
|
-
const record = records.get(entry.id);
|
|
1835
|
-
if (!record)
|
|
1836
|
-
throw new Error("Managed resource record disappeared");
|
|
1837
|
-
return {
|
|
1838
|
-
id: entry.id,
|
|
1839
|
-
required: entry.required,
|
|
1840
|
-
dependsOn: entry.dependsOn,
|
|
1841
|
-
state: record.state,
|
|
1842
|
-
health: record.health,
|
|
1843
|
-
ready: record.state === "ready" && record.health === "healthy"
|
|
1844
|
-
};
|
|
1845
|
-
})
|
|
1846
|
-
});
|
|
1847
|
-
const publish = () => {
|
|
1848
|
-
revision += 1;
|
|
1849
|
-
changedAt = new Date().toISOString();
|
|
1850
|
-
const value = snapshot();
|
|
1851
|
-
for (const listener of listeners) {
|
|
1852
|
-
try {
|
|
1853
|
-
listener(value);
|
|
1854
|
-
} catch {}
|
|
1855
|
-
}
|
|
1856
|
-
if (config.onSnapshot) {
|
|
1857
|
-
Promise.resolve().then(() => config.onSnapshot?.(value)).catch(() => {});
|
|
1858
|
-
}
|
|
1859
|
-
};
|
|
1860
|
-
const contextFor = (record, options = {}) => ({
|
|
1861
|
-
applicationId: id,
|
|
1862
|
-
signal: options.signal ?? lifetimeAbort.signal,
|
|
1863
|
-
...options.deadlineAt !== undefined && { deadlineAt: options.deadlineAt },
|
|
1864
|
-
...options.forceDeadlineAt !== undefined && {
|
|
1865
|
-
forceDeadlineAt: options.forceDeadlineAt
|
|
1866
|
-
},
|
|
1867
|
-
now: () => performance.now(),
|
|
1868
|
-
use(resource) {
|
|
1869
|
-
const dependencyId = resource?.id;
|
|
1870
|
-
if (typeof dependencyId !== "string" || dependencyId.length === 0) {
|
|
1871
|
-
throw new Error(`[stitchkit] resource "${record.entry.id}": use() takes a managed resource, and this one has no id`);
|
|
1872
|
-
}
|
|
1873
|
-
if (!record.entry.dependsOn.includes(dependencyId)) {
|
|
1874
|
-
throw new Error(`[stitchkit] resource "${record.entry.id}" used "${dependencyId}" without declaring it in dependsOn`);
|
|
1875
|
-
}
|
|
1876
|
-
if (!published.has(dependencyId)) {
|
|
1877
|
-
throw new Error(`[stitchkit] resource "${record.entry.id}" used "${dependencyId}", which published no value from start()`);
|
|
1878
|
-
}
|
|
1879
|
-
return published.get(dependencyId);
|
|
1880
|
-
},
|
|
1881
|
-
reportHealth(health) {
|
|
1882
|
-
if (record.state === "stopped")
|
|
1883
|
-
return;
|
|
1884
|
-
record.health = health;
|
|
1885
|
-
record.healthReported = true;
|
|
1886
|
-
if (health === "healthy")
|
|
1887
|
-
record.everHealthy = true;
|
|
1888
|
-
accepting = activationComplete && !shutdownRequested && isReady();
|
|
1889
|
-
publish();
|
|
1890
|
-
}
|
|
1891
|
-
});
|
|
1892
|
-
const markLateCompletion = (record, failure) => {
|
|
1893
|
-
if (shutdownRequested || record.state === "stopping" || record.state === "stopped")
|
|
1894
|
-
return;
|
|
1895
|
-
if (failure) {
|
|
1896
|
-
record.failures.push("completion");
|
|
1897
|
-
reportFailure(record.entry.id, "completion", failure.error);
|
|
1898
|
-
}
|
|
1899
|
-
record.state = "failed";
|
|
1900
|
-
record.health = "unhealthy";
|
|
1901
|
-
accepting = activationComplete && isReady();
|
|
1902
|
-
publish();
|
|
1903
|
-
};
|
|
1904
|
-
const closeAttempted = async () => {
|
|
1905
|
-
const errors = [];
|
|
1906
|
-
const rollbackStartedAt = performance.now();
|
|
1907
|
-
const rollbackDeadlineAt = rollbackStartedAt + shutdownBudget.gracePeriodMs;
|
|
1908
|
-
const rollbackForceDeadlineAt = rollbackDeadlineAt + shutdownBudget.forceTimeoutMs;
|
|
1909
|
-
const rollbackAbort = new AbortController;
|
|
1910
|
-
const rollbackTimer = setTimeout(() => rollbackAbort.abort(), Math.max(0, rollbackForceDeadlineAt - performance.now()));
|
|
1911
|
-
try {
|
|
1912
|
-
await closeEachAttempted(rollbackAbort.signal, errors, {
|
|
1913
|
-
deadlineAt: rollbackDeadlineAt,
|
|
1914
|
-
forceDeadlineAt: rollbackForceDeadlineAt
|
|
1915
|
-
});
|
|
1916
|
-
} finally {
|
|
1917
|
-
clearTimeout(rollbackTimer);
|
|
1918
|
-
}
|
|
1919
|
-
return errors;
|
|
1920
|
-
};
|
|
1921
|
-
const closeEachAttempted = async (bound, errors, deadlines) => {
|
|
1922
|
-
for (const entry of reverse) {
|
|
1923
|
-
const record = records.get(entry.id);
|
|
1924
|
-
if (!record?.attempted || record.closed)
|
|
1925
|
-
continue;
|
|
1926
|
-
record.state = "stopping";
|
|
1927
|
-
try {
|
|
1928
|
-
record.closeInvoked = true;
|
|
1929
|
-
const settled = await untilDeadline(Promise.resolve(entry.resource.close?.(contextFor(record, { signal: startupAbort.signal, ...deadlines }))), bound);
|
|
1930
|
-
if (!settled.settled) {
|
|
1931
|
-
record.failures.push("close");
|
|
1932
|
-
record.state = "failed";
|
|
1933
|
-
const timedOut = new Error(`[stitchkit] resource "${entry.id}" did not finish closing during rollback`);
|
|
1934
|
-
reportFailure(entry.id, "close", timedOut);
|
|
1935
|
-
errors.push(timedOut);
|
|
1936
|
-
publish();
|
|
1937
|
-
break;
|
|
1938
|
-
}
|
|
1939
|
-
if (settled.error !== undefined)
|
|
1940
|
-
throw settled.error;
|
|
1941
|
-
record.closed = true;
|
|
1942
|
-
record.state = "stopped";
|
|
1943
|
-
} catch (error) {
|
|
1944
|
-
record.failures.push("close");
|
|
1945
|
-
record.state = "failed";
|
|
1946
|
-
reportFailure(entry.id, "close", error);
|
|
1947
|
-
errors.push(error);
|
|
1948
|
-
}
|
|
1949
|
-
publish();
|
|
1950
|
-
}
|
|
1951
|
-
};
|
|
1952
|
-
const startEach = async (entries, signal) => {
|
|
1953
|
-
for (const entry of entries) {
|
|
1954
|
-
if (shutdownRequested || signal.aborted) {
|
|
1955
|
-
throw new ApplicationStartupInterruptedError;
|
|
1956
|
-
}
|
|
1957
|
-
const record = records.get(entry.id);
|
|
1958
|
-
if (!record)
|
|
1959
|
-
throw new Error("Managed resource record disappeared");
|
|
1960
|
-
const dependencyFailed = entry.dependsOn.some((dependencyId) => records.get(dependencyId)?.state !== "ready");
|
|
1961
|
-
if (dependencyFailed) {
|
|
1962
|
-
record.state = "failed";
|
|
1963
|
-
record.health = "unhealthy";
|
|
1964
|
-
record.failures.push("start");
|
|
1965
|
-
publish();
|
|
1966
|
-
if (entry.required) {
|
|
1967
|
-
throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable dependency`);
|
|
1968
|
-
}
|
|
1969
|
-
continue;
|
|
1970
|
-
}
|
|
1971
|
-
record.attempted = true;
|
|
1972
|
-
record.state = "starting";
|
|
1973
|
-
publish();
|
|
1974
|
-
try {
|
|
1975
|
-
const started = await entry.resource.start(contextFor(record, { signal }));
|
|
1976
|
-
if (shutdownRequested || signal.aborted) {
|
|
1977
|
-
throw new ApplicationStartupInterruptedError;
|
|
1978
|
-
}
|
|
1979
|
-
if (isStartResult(started)) {
|
|
1980
|
-
record.runtime = started;
|
|
1981
|
-
if (started.value !== undefined)
|
|
1982
|
-
published.set(entry.id, started.value);
|
|
1983
|
-
let resourceReady = started.ready === undefined;
|
|
1984
|
-
let completionSettled = false;
|
|
1985
|
-
let completionFailure;
|
|
1986
|
-
const completion = started.completion?.then(() => {
|
|
1987
|
-
completionSettled = true;
|
|
1988
|
-
if (resourceReady)
|
|
1989
|
-
markLateCompletion(record);
|
|
1990
|
-
}, (error) => {
|
|
1991
|
-
completionSettled = true;
|
|
1992
|
-
completionFailure = error;
|
|
1993
|
-
if (resourceReady)
|
|
1994
|
-
markLateCompletion(record, { error });
|
|
1995
|
-
});
|
|
1996
|
-
if (started.ready && completion) {
|
|
1997
|
-
const readiness = started.ready.then(() => "ready");
|
|
1998
|
-
const completionBeforeReady = completion.then(() => "completion");
|
|
1999
|
-
const first = await Promise.race([readiness, completionBeforeReady]);
|
|
2000
|
-
if (first === "completion") {
|
|
2001
|
-
throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
|
|
2002
|
-
}
|
|
2003
|
-
resourceReady = true;
|
|
2004
|
-
if (completionSettled) {
|
|
2005
|
-
throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
|
|
2006
|
-
}
|
|
2007
|
-
} else if (started.ready) {
|
|
2008
|
-
await started.ready;
|
|
2009
|
-
resourceReady = true;
|
|
2010
|
-
} else if (completion) {}
|
|
2011
|
-
}
|
|
2012
|
-
if (shutdownRequested || signal.aborted) {
|
|
2013
|
-
throw new ApplicationStartupInterruptedError;
|
|
2014
|
-
}
|
|
2015
|
-
record.state = "ready";
|
|
2016
|
-
if (!record.healthReported) {
|
|
2017
|
-
record.health = "healthy";
|
|
2018
|
-
record.everHealthy = true;
|
|
2019
|
-
}
|
|
2020
|
-
publish();
|
|
2021
|
-
} catch (error) {
|
|
2022
|
-
const interrupted = shutdownRequested || signal.aborted;
|
|
2023
|
-
if (!(error instanceof ApplicationStartupInterruptedError)) {
|
|
2024
|
-
record.failures.push(error instanceof ResourceCompletionBeforeReadyError ? "completion" : record.runtime?.ready ? "ready" : "start");
|
|
2025
|
-
record.state = "failed";
|
|
2026
|
-
record.health = "unhealthy";
|
|
2027
|
-
reportFailure(entry.id, record.failures[record.failures.length - 1] ?? "start", error);
|
|
2028
|
-
publish();
|
|
2029
|
-
}
|
|
2030
|
-
if (interrupted || entry.required)
|
|
2031
|
-
throw error;
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
};
|
|
2035
|
-
const activateEach = async (entries, signal) => {
|
|
2036
|
-
for (const entry of entries) {
|
|
2037
|
-
if (shutdownRequested || signal.aborted) {
|
|
2038
|
-
throw new ApplicationStartupInterruptedError;
|
|
2039
|
-
}
|
|
2040
|
-
const record = records.get(entry.id);
|
|
2041
|
-
if (record?.state !== "ready")
|
|
2042
|
-
continue;
|
|
2043
|
-
const dependencyUnavailable = entry.dependsOn.some((dependencyId) => {
|
|
2044
|
-
const dependency = records.get(dependencyId);
|
|
2045
|
-
return dependency?.state !== "ready" || !dependency.activated;
|
|
2046
|
-
});
|
|
2047
|
-
if (dependencyUnavailable) {
|
|
2048
|
-
record.failures.push("start");
|
|
2049
|
-
record.state = "failed";
|
|
2050
|
-
record.health = "unhealthy";
|
|
2051
|
-
publish();
|
|
2052
|
-
if (entry.required) {
|
|
2053
|
-
throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable activation dependency`);
|
|
2054
|
-
}
|
|
2055
|
-
continue;
|
|
2056
|
-
}
|
|
2057
|
-
try {
|
|
2058
|
-
await entry.resource.activate?.(contextFor(record));
|
|
2059
|
-
if (shutdownRequested || signal.aborted) {
|
|
2060
|
-
throw new ApplicationStartupInterruptedError;
|
|
2061
|
-
}
|
|
2062
|
-
record.activated = true;
|
|
2063
|
-
if (entry.required && (record.state !== "ready" || record.health !== "healthy")) {
|
|
2064
|
-
const observed = `${record.state}/${record.health}`;
|
|
2065
|
-
throw new Error(record.everHealthy ? `[stitchkit] required resource "${entry.id}" lost readiness during activation (${observed})` : `[stitchkit] required resource "${entry.id}" is not healthy (${observed}). A required resource must be healthy for the application to be ready; a resource that is expected to start degraded belongs behind \`required: false\`.`);
|
|
2066
|
-
}
|
|
2067
|
-
} catch (error) {
|
|
2068
|
-
const interrupted = shutdownRequested || signal.aborted;
|
|
2069
|
-
if (!(error instanceof ApplicationStartupInterruptedError)) {
|
|
2070
|
-
record.failures.push("start");
|
|
2071
|
-
record.state = "failed";
|
|
2072
|
-
record.health = "unhealthy";
|
|
2073
|
-
reportFailure(entry.id, "start", error);
|
|
2074
|
-
publish();
|
|
2075
|
-
}
|
|
2076
|
-
if (interrupted || entry.required)
|
|
2077
|
-
throw error;
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
};
|
|
2081
|
-
const subtreeOf = (resourceId) => {
|
|
2082
|
-
const affected = new Set([resourceId]);
|
|
2083
|
-
for (const entry of ordered) {
|
|
2084
|
-
if (entry.dependsOn.some((dependencyId) => affected.has(dependencyId))) {
|
|
2085
|
-
affected.add(entry.id);
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
return ordered.filter((entry) => affected.has(entry.id));
|
|
2089
|
-
};
|
|
2090
|
-
const closeOne = async (entry, record, signal, deadlines) => {
|
|
2091
|
-
const context = () => contextFor(record, { ...deadlines, signal });
|
|
2092
|
-
if (record.attempted && !record.closed) {
|
|
2093
|
-
const phase = async (work, what) => {
|
|
2094
|
-
const settled = await untilDeadline(Promise.resolve(work), signal);
|
|
2095
|
-
if (!settled.settled) {
|
|
2096
|
-
throw new Error(`[stitchkit] resource "${entry.id}" did not ${what} within the restart budget`);
|
|
2097
|
-
}
|
|
2098
|
-
if (settled.error !== undefined)
|
|
2099
|
-
throw settled.error;
|
|
2100
|
-
};
|
|
2101
|
-
await phase(entry.resource.stopAdmission?.(context()), "stop admitting");
|
|
2102
|
-
await phase(entry.resource.drain?.(context()), "drain");
|
|
2103
|
-
record.closeInvoked = true;
|
|
2104
|
-
await phase(entry.resource.close?.(context()), "close");
|
|
2105
|
-
}
|
|
2106
|
-
record.closed = false;
|
|
2107
|
-
record.closeInvoked = false;
|
|
2108
|
-
record.attempted = false;
|
|
2109
|
-
record.activated = false;
|
|
2110
|
-
record.state = "registered";
|
|
2111
|
-
record.health = "unknown";
|
|
2112
|
-
record.healthReported = false;
|
|
2113
|
-
record.runtime = undefined;
|
|
2114
|
-
published.delete(entry.id);
|
|
2115
|
-
};
|
|
2116
|
-
let restarting = Promise.resolve();
|
|
2117
|
-
const runRestart = async (input) => {
|
|
2118
|
-
const startedAt = Date.now();
|
|
2119
|
-
const parsed = ApplicationRestartInputSchema.parse(input);
|
|
2120
|
-
const affected = subtreeOf(parsed.resourceId);
|
|
2121
|
-
const affectedIds = affected.map((entry) => entry.id);
|
|
2122
|
-
const refuse = (reason) => ({
|
|
2123
|
-
resourceId: parsed.resourceId,
|
|
2124
|
-
affected: affectedIds,
|
|
2125
|
-
outcome: "refused",
|
|
2126
|
-
reason,
|
|
2127
|
-
durationMs: Date.now() - startedAt
|
|
2128
|
-
});
|
|
2129
|
-
if (!records.has(parsed.resourceId)) {
|
|
2130
|
-
return refuse(`no resource is registered as "${parsed.resourceId}"`);
|
|
2131
|
-
}
|
|
2132
|
-
if (shutdownRequested) {
|
|
2133
|
-
return refuse("the application is shutting down");
|
|
2134
|
-
}
|
|
2135
|
-
if (lifecycle !== "ready") {
|
|
2136
|
-
return refuse(`the application is ${lifecycle}, not ready`);
|
|
2137
|
-
}
|
|
2138
|
-
const budget = {
|
|
2139
|
-
gracePeriodMs: parsed.gracePeriodMs ?? shutdownBudget.gracePeriodMs,
|
|
2140
|
-
forceTimeoutMs: parsed.forceTimeoutMs ?? shutdownBudget.forceTimeoutMs
|
|
2141
|
-
};
|
|
2142
|
-
const closeDeadlineAt = performance.now() + budget.gracePeriodMs;
|
|
2143
|
-
const deadlines = {
|
|
2144
|
-
deadlineAt: closeDeadlineAt,
|
|
2145
|
-
forceDeadlineAt: closeDeadlineAt + budget.forceTimeoutMs
|
|
2146
|
-
};
|
|
2147
|
-
const restartAbort = new AbortController;
|
|
2148
|
-
const closeTimer = setTimeout(() => restartAbort.abort(), Math.max(0, deadlines.forceDeadlineAt - performance.now()));
|
|
2149
|
-
const stillFailed = () => affectedIds.filter((id2) => records.get(id2)?.state === "failed");
|
|
2150
|
-
try {
|
|
2151
|
-
for (const entry of [...affected].reverse()) {
|
|
2152
|
-
const record = records.get(entry.id);
|
|
2153
|
-
if (record)
|
|
2154
|
-
await closeOne(entry, record, restartAbort.signal, deadlines);
|
|
2155
|
-
}
|
|
2156
|
-
publish();
|
|
2157
|
-
await startEach(affected, restartAbort.signal);
|
|
2158
|
-
await activateEach(affected, restartAbort.signal);
|
|
2159
|
-
publish();
|
|
2160
|
-
const failed = stillFailed();
|
|
2161
|
-
if (failed.length > 0) {
|
|
2162
|
-
return {
|
|
2163
|
-
resourceId: parsed.resourceId,
|
|
2164
|
-
affected: affectedIds,
|
|
2165
|
-
outcome: "failed",
|
|
2166
|
-
reason: `did not come back: ${failed.join(", ")}`,
|
|
2167
|
-
durationMs: Date.now() - startedAt
|
|
2168
|
-
};
|
|
2169
|
-
}
|
|
2170
|
-
return {
|
|
2171
|
-
resourceId: parsed.resourceId,
|
|
2172
|
-
affected: affectedIds,
|
|
2173
|
-
outcome: "restarted",
|
|
2174
|
-
durationMs: Date.now() - startedAt
|
|
2175
|
-
};
|
|
2176
|
-
} catch (error) {
|
|
2177
|
-
restartAbort.abort();
|
|
2178
|
-
publish();
|
|
2179
|
-
return {
|
|
2180
|
-
resourceId: parsed.resourceId,
|
|
2181
|
-
affected: affectedIds,
|
|
2182
|
-
outcome: "failed",
|
|
2183
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
2184
|
-
durationMs: Date.now() - startedAt
|
|
2185
|
-
};
|
|
2186
|
-
} finally {
|
|
2187
|
-
clearTimeout(closeTimer);
|
|
2188
|
-
}
|
|
2189
|
-
};
|
|
2190
|
-
const runStart = async () => {
|
|
2191
|
-
lifecycle = "starting";
|
|
2192
|
-
publish();
|
|
2193
|
-
let startFailure;
|
|
2194
|
-
try {
|
|
2195
|
-
await startEach(ordered, startupAbort.signal);
|
|
2196
|
-
lifecycle = "ready";
|
|
2197
|
-
publish();
|
|
2198
|
-
await activateEach(ordered, startupAbort.signal);
|
|
2199
|
-
if (shutdownRequested) {
|
|
2200
|
-
throw new ApplicationStartupInterruptedError;
|
|
2201
|
-
}
|
|
2202
|
-
if (!isReady()) {
|
|
2203
|
-
const blocking = [...records.values()].filter((record) => record.entry.required && (record.state !== "ready" || record.health !== "healthy")).map((record) => `${record.entry.id} (${record.state}/${record.health})`);
|
|
2204
|
-
if (blocking.length === 0)
|
|
2205
|
-
blocking.push("(none identified)");
|
|
2206
|
-
const chosen = [...records.values()].some((record) => record.entry.required && record.health !== "healthy" && !record.everHealthy);
|
|
2207
|
-
throw new Error(`[stitchkit] the application is not ready after startup — required ${blocking.length === 1 ? "resource" : "resources"} ${blocking.join(", ")}. A required resource must be healthy for the application to be ready; ${chosen ? "a resource that is expected to start degraded belongs behind `required: false`." : "this one was healthy and stopped being so — read `onResourceFailure` for the cause."}`);
|
|
2208
|
-
}
|
|
2209
|
-
activationComplete = true;
|
|
2210
|
-
accepting = isReady();
|
|
2211
|
-
publish();
|
|
2212
|
-
return snapshot();
|
|
2213
|
-
} catch (error) {
|
|
2214
|
-
startFailure = error;
|
|
2215
|
-
}
|
|
2216
|
-
if (!shutdownRequested) {
|
|
2217
|
-
const rollbackErrors = await closeAttempted();
|
|
2218
|
-
lifecycle = "failed";
|
|
2219
|
-
accepting = false;
|
|
2220
|
-
publish();
|
|
2221
|
-
if (rollbackErrors.length > 0) {
|
|
2222
|
-
throw new AggregateError([startFailure, ...rollbackErrors], "[stitchkit] application startup and rollback failed", { cause: startFailure });
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
throw startFailure;
|
|
2226
|
-
};
|
|
2227
|
-
const start = () => {
|
|
2228
|
-
if (startPromise)
|
|
2229
|
-
return startPromise;
|
|
2230
|
-
if (lifecycle !== "created") {
|
|
2231
|
-
return Promise.reject(new Error(`[stitchkit] application cannot start from lifecycle "${lifecycle}"`));
|
|
2232
|
-
}
|
|
2233
|
-
startPromise = runStart();
|
|
2234
|
-
startPromise.catch(() => {
|
|
2235
|
-
return;
|
|
2236
|
-
});
|
|
2237
|
-
return startPromise;
|
|
2238
|
-
};
|
|
2239
|
-
const acquire = () => {
|
|
2240
|
-
if (!accepting || !isReady())
|
|
2241
|
-
return null;
|
|
2242
|
-
accepted += 1;
|
|
2243
|
-
pending += 1;
|
|
2244
|
-
publish();
|
|
2245
|
-
let released = false;
|
|
2246
|
-
return {
|
|
2247
|
-
get released() {
|
|
2248
|
-
return released;
|
|
2249
|
-
},
|
|
2250
|
-
release() {
|
|
2251
|
-
if (released)
|
|
2252
|
-
return;
|
|
2253
|
-
released = true;
|
|
2254
|
-
pending -= 1;
|
|
2255
|
-
completed += 1;
|
|
2256
|
-
if (pending === 0) {
|
|
2257
|
-
for (const waiter of pendingWaiters)
|
|
2258
|
-
waiter();
|
|
2259
|
-
pendingWaiters.clear();
|
|
2260
|
-
}
|
|
2261
|
-
publish();
|
|
2262
|
-
}
|
|
2263
|
-
};
|
|
2264
|
-
};
|
|
2265
|
-
const run = async (work) => {
|
|
2266
|
-
const lease = acquire();
|
|
2267
|
-
if (!lease)
|
|
2268
|
-
throw new ApplicationAdmissionError;
|
|
2269
|
-
try {
|
|
2270
|
-
return await work();
|
|
2271
|
-
} finally {
|
|
2272
|
-
lease.release();
|
|
2273
|
-
}
|
|
2274
|
-
};
|
|
2275
|
-
const waitForPending = () => {
|
|
2276
|
-
if (pending === 0)
|
|
2277
|
-
return Promise.resolve();
|
|
2278
|
-
return new Promise((resolve) => pendingWaiters.add(resolve));
|
|
2279
|
-
};
|
|
2280
|
-
const shutdown = (options) => {
|
|
2281
|
-
if (shutdownPromise)
|
|
2282
|
-
return shutdownPromise;
|
|
2283
|
-
const requested = options ?? {};
|
|
2284
|
-
const parsed = ApplicationShutdownOptionsSchema.parse({
|
|
2285
|
-
gracePeriodMs: requested.gracePeriodMs ?? shutdownBudget.gracePeriodMs,
|
|
2286
|
-
forceTimeoutMs: requested.forceTimeoutMs ?? shutdownBudget.forceTimeoutMs,
|
|
2287
|
-
...requested.signal !== undefined && { signal: requested.signal }
|
|
2288
|
-
});
|
|
2289
|
-
const startedAt = performance.now();
|
|
2290
|
-
const graceDeadlineAt = startedAt + parsed.gracePeriodMs;
|
|
2291
|
-
const forceDeadlineAt = graceDeadlineAt + parsed.forceTimeoutMs;
|
|
2292
|
-
shutdownRequested = true;
|
|
2293
|
-
accepting = false;
|
|
2294
|
-
startupAbort.abort();
|
|
2295
|
-
lifecycle = lifecycle === "created" ? "stopping" : "draining";
|
|
2296
|
-
publish();
|
|
2297
|
-
shutdownPromise = (async () => {
|
|
2298
|
-
const gracefulAbort = new AbortController;
|
|
2299
|
-
let forcedReason;
|
|
2300
|
-
let gracefulFailed = [...records.values()].some((record) => record.attempted && record.closeInvoked && !record.closed);
|
|
2301
|
-
const force = (reason) => {
|
|
2302
|
-
if (forcedReason)
|
|
2303
|
-
return;
|
|
2304
|
-
forcedReason = reason;
|
|
2305
|
-
gracefulAbort.abort();
|
|
2306
|
-
lifetimeAbort.abort();
|
|
2307
|
-
};
|
|
2308
|
-
const graceTimer = setTimeout(() => force("deadline"), Math.max(0, graceDeadlineAt - performance.now()));
|
|
2309
|
-
const onExternalAbort = () => force("signal");
|
|
2310
|
-
parsed.signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
2311
|
-
if (parsed.signal?.aborted)
|
|
2312
|
-
force("signal");
|
|
2313
|
-
if (startPromise) {
|
|
2314
|
-
await untilDeadline(startPromise.catch(() => {
|
|
2315
|
-
return;
|
|
2316
|
-
}), gracefulAbort.signal);
|
|
2317
|
-
}
|
|
2318
|
-
const gracefulContext = (record) => contextFor(record, {
|
|
2319
|
-
signal: gracefulAbort.signal,
|
|
2320
|
-
deadlineAt: graceDeadlineAt,
|
|
2321
|
-
forceDeadlineAt
|
|
2322
|
-
});
|
|
2323
|
-
for (const entry of reverse) {
|
|
2324
|
-
if (gracefulAbort.signal.aborted)
|
|
2325
|
-
break;
|
|
2326
|
-
const record = records.get(entry.id);
|
|
2327
|
-
if (!record?.attempted || record.closed)
|
|
2328
|
-
continue;
|
|
2329
|
-
try {
|
|
2330
|
-
const result = await untilDeadline(Promise.resolve(entry.resource.stopAdmission?.(gracefulContext(record))), gracefulAbort.signal);
|
|
2331
|
-
if (!result.settled)
|
|
2332
|
-
break;
|
|
2333
|
-
if (result.error !== undefined)
|
|
2334
|
-
throw result.error;
|
|
2335
|
-
} catch (error) {
|
|
2336
|
-
record.failures.push("admission");
|
|
2337
|
-
reportFailure(entry.id, "admission", error);
|
|
2338
|
-
gracefulFailed = true;
|
|
2339
|
-
lifetimeAbort.abort();
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
if (!gracefulAbort.signal.aborted && !gracefulFailed) {
|
|
2343
|
-
const result = await untilDeadline(waitForPending(), gracefulAbort.signal);
|
|
2344
|
-
if (!result.settled)
|
|
2345
|
-
force("deadline");
|
|
2346
|
-
}
|
|
2347
|
-
for (const entry of reverse) {
|
|
2348
|
-
if (gracefulAbort.signal.aborted || gracefulFailed)
|
|
2349
|
-
break;
|
|
2350
|
-
const record = records.get(entry.id);
|
|
2351
|
-
if (!record?.attempted || record.closed)
|
|
2352
|
-
continue;
|
|
2353
|
-
record.state = "stopping";
|
|
2354
|
-
publish();
|
|
2355
|
-
try {
|
|
2356
|
-
const drained = await untilDeadline(Promise.resolve(entry.resource.drain?.(gracefulContext(record))), gracefulAbort.signal);
|
|
2357
|
-
if (!drained.settled) {
|
|
2358
|
-
force("deadline");
|
|
2359
|
-
break;
|
|
2360
|
-
}
|
|
2361
|
-
if (drained.error !== undefined)
|
|
2362
|
-
throw drained.error;
|
|
2363
|
-
} catch (error) {
|
|
2364
|
-
record.failures.push("drain");
|
|
2365
|
-
reportFailure(entry.id, "drain", error);
|
|
2366
|
-
gracefulFailed = true;
|
|
2367
|
-
lifetimeAbort.abort();
|
|
2368
|
-
break;
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
lifecycle = "stopping";
|
|
2372
|
-
publish();
|
|
2373
|
-
for (const entry of reverse) {
|
|
2374
|
-
if (gracefulAbort.signal.aborted || gracefulFailed)
|
|
2375
|
-
break;
|
|
2376
|
-
const record = records.get(entry.id);
|
|
2377
|
-
if (!record?.attempted || record.closed || record.closeInvoked)
|
|
2378
|
-
continue;
|
|
2379
|
-
try {
|
|
2380
|
-
record.closeInvoked = true;
|
|
2381
|
-
const closed = await untilDeadline(Promise.resolve(entry.resource.close?.(gracefulContext(record))), gracefulAbort.signal);
|
|
2382
|
-
if (!closed.settled) {
|
|
2383
|
-
force("deadline");
|
|
2384
|
-
break;
|
|
2385
|
-
}
|
|
2386
|
-
if (closed.error !== undefined)
|
|
2387
|
-
throw closed.error;
|
|
2388
|
-
record.closed = true;
|
|
2389
|
-
record.state = "stopped";
|
|
2390
|
-
publish();
|
|
2391
|
-
} catch (error) {
|
|
2392
|
-
record.failures.push("close");
|
|
2393
|
-
reportFailure(entry.id, "close", error);
|
|
2394
|
-
record.state = "failed";
|
|
2395
|
-
gracefulFailed = true;
|
|
2396
|
-
lifetimeAbort.abort();
|
|
2397
|
-
publish();
|
|
2398
|
-
break;
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
clearTimeout(graceTimer);
|
|
2402
|
-
parsed.signal?.removeEventListener("abort", onExternalAbort);
|
|
2403
|
-
const mustForce = forcedReason !== undefined || gracefulFailed;
|
|
2404
|
-
const pendingOperationsAtForce = mustForce ? pending : 0;
|
|
2405
|
-
if (mustForce) {
|
|
2406
|
-
lifetimeAbort.abort();
|
|
2407
|
-
const forceAbort = new AbortController;
|
|
2408
|
-
const forceTimer = setTimeout(() => forceAbort.abort(), Math.max(0, forceDeadlineAt - performance.now()));
|
|
2409
|
-
await Promise.all(reverse.map(async (entry) => {
|
|
2410
|
-
const record = records.get(entry.id);
|
|
2411
|
-
if (!record?.attempted || record.closed)
|
|
2412
|
-
return;
|
|
2413
|
-
try {
|
|
2414
|
-
let cleanup;
|
|
2415
|
-
if (entry.resource.force) {
|
|
2416
|
-
cleanup = Promise.resolve(entry.resource.force(contextFor(record, {
|
|
2417
|
-
signal: forceAbort.signal,
|
|
2418
|
-
deadlineAt: graceDeadlineAt,
|
|
2419
|
-
forceDeadlineAt
|
|
2420
|
-
})));
|
|
2421
|
-
} else if (entry.resource.close && !record.closeInvoked) {
|
|
2422
|
-
record.closeInvoked = true;
|
|
2423
|
-
cleanup = Promise.resolve(entry.resource.close(contextFor(record, {
|
|
2424
|
-
signal: forceAbort.signal,
|
|
2425
|
-
deadlineAt: graceDeadlineAt,
|
|
2426
|
-
forceDeadlineAt
|
|
2427
|
-
})));
|
|
2428
|
-
} else if (entry.resource.close) {
|
|
2429
|
-
record.failures.push("force");
|
|
2430
|
-
reportFailure(entry.id, "force", new Error(`[stitchkit] resource "${entry.id}" was already closing and did not settle before the force deadline`));
|
|
2431
|
-
return;
|
|
2432
|
-
}
|
|
2433
|
-
const forced = await untilDeadline(cleanup ?? Promise.resolve(), forceAbort.signal);
|
|
2434
|
-
if (!forced.settled || forced.error !== undefined) {
|
|
2435
|
-
record.failures.push("force");
|
|
2436
|
-
reportFailure(entry.id, "force", forced.settled ? forced.error : new Error("[stitchkit] forced cleanup did not settle in time"));
|
|
2437
|
-
return;
|
|
2438
|
-
}
|
|
2439
|
-
record.closed = true;
|
|
2440
|
-
record.state = "stopped";
|
|
2441
|
-
} catch (error) {
|
|
2442
|
-
record.failures.push("force");
|
|
2443
|
-
reportFailure(entry.id, "force", error);
|
|
2444
|
-
}
|
|
2445
|
-
}));
|
|
2446
|
-
clearTimeout(forceTimer);
|
|
2447
|
-
}
|
|
2448
|
-
const cleanupComplete = [...records.values()].every((record) => !record.attempted || record.closed);
|
|
2449
|
-
lifecycle = cleanupComplete ? "stopped" : "failed";
|
|
2450
|
-
publish();
|
|
2451
|
-
return ApplicationShutdownResultSchema.parse({
|
|
2452
|
-
outcome: mustForce ? "forced" : "clean",
|
|
2453
|
-
...forcedReason && { reason: forcedReason },
|
|
2454
|
-
cleanupComplete,
|
|
2455
|
-
acceptedOperations: accepted,
|
|
2456
|
-
completedOperations: completed,
|
|
2457
|
-
pendingOperations: pending,
|
|
2458
|
-
pendingOperationsAtForce,
|
|
2459
|
-
resources: ordered.map((entry) => {
|
|
2460
|
-
const record = records.get(entry.id);
|
|
2461
|
-
if (!record)
|
|
2462
|
-
throw new Error("Managed resource record disappeared");
|
|
2463
|
-
return {
|
|
2464
|
-
id: entry.id,
|
|
2465
|
-
state: !record.attempted ? "not-started" : record.closed ? "closed" : "force-failed",
|
|
2466
|
-
failures: record.failures
|
|
2467
|
-
};
|
|
2468
|
-
}),
|
|
2469
|
-
durationMs: performance.now() - startedAt
|
|
2470
|
-
});
|
|
2471
|
-
})();
|
|
2472
|
-
shutdownPromise.catch(() => {
|
|
2473
|
-
return;
|
|
2474
|
-
});
|
|
2475
|
-
return shutdownPromise;
|
|
2476
|
-
};
|
|
2477
|
-
return {
|
|
2478
|
-
id,
|
|
2479
|
-
admission: { acquire, run },
|
|
2480
|
-
start,
|
|
2481
|
-
getSnapshot: snapshot,
|
|
2482
|
-
subscribe(listener) {
|
|
2483
|
-
listeners.add(listener);
|
|
2484
|
-
listener(snapshot());
|
|
2485
|
-
return () => listeners.delete(listener);
|
|
2486
|
-
},
|
|
2487
|
-
shutdown,
|
|
2488
|
-
restart(input) {
|
|
2489
|
-
const queued = restarting.then(() => runRestart(input));
|
|
2490
|
-
restarting = queued.then(() => {
|
|
2491
|
-
return;
|
|
2492
|
-
}, () => {
|
|
2493
|
-
return;
|
|
2494
|
-
});
|
|
2495
|
-
return queued;
|
|
2496
|
-
}
|
|
2497
|
-
};
|
|
2498
|
-
}
|
|
2499
|
-
|
|
2500
1032
|
// src/testing/managed-resource-conformance-scenarios.ts
|
|
2501
1033
|
var MANAGED_RESOURCE_CONFORMANCE_SCENARIOS = ManagedResourceConformanceScenarioSchema.array().parse([
|
|
2502
1034
|
{ id: "clean-shutdown", required: true },
|
|
@@ -2864,90 +1396,72 @@ async function runManagedResourceConformance(config) {
|
|
|
2864
1396
|
}
|
|
2865
1397
|
}
|
|
2866
1398
|
// src/testing/surface-conformance.ts
|
|
2867
|
-
import { z as z5 } from "zod";
|
|
2868
|
-
|
|
2869
|
-
// src/realtime/contract.ts
|
|
2870
1399
|
import { z as z3 } from "zod";
|
|
2871
|
-
var RealtimeRejectDirectionSchema = z3.enum([
|
|
2872
|
-
"client-inbound",
|
|
2873
|
-
"client-outbound",
|
|
2874
|
-
"server-inbound",
|
|
2875
|
-
"server-outbound"
|
|
2876
|
-
]);
|
|
2877
|
-
var RealtimeRejectPhaseSchema = z3.enum(["arguments", "acknowledgement"]);
|
|
2878
|
-
var RealtimeRejectReasonSchema = z3.enum([
|
|
2879
|
-
"unknown-event",
|
|
2880
|
-
"invalid-arguments",
|
|
2881
|
-
"invalid-acknowledgement-value",
|
|
2882
|
-
"missing-acknowledgement",
|
|
2883
|
-
"rejected-by-peer"
|
|
2884
|
-
]);
|
|
2885
|
-
var RealtimeRejectFaultSchema = z3.enum(["peer", "local"]);
|
|
2886
1400
|
|
|
2887
1401
|
// src/testing/surface-manifest.ts
|
|
2888
1402
|
import { createHash } from "node:crypto";
|
|
2889
|
-
import { z as
|
|
2890
|
-
var HttpMethodSchema =
|
|
2891
|
-
var ToolTransportSchema =
|
|
2892
|
-
var SurfaceTransportSchema =
|
|
2893
|
-
var SurfaceSchemaDigestsSchema =
|
|
2894
|
-
params:
|
|
2895
|
-
input:
|
|
2896
|
-
output:
|
|
2897
|
-
multipart:
|
|
1403
|
+
import { z as z2 } from "zod";
|
|
1404
|
+
var HttpMethodSchema = z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
|
|
1405
|
+
var ToolTransportSchema = z2.enum(["MCP", "AGENT", "CLI"]);
|
|
1406
|
+
var SurfaceTransportSchema = z2.enum(["HTTP", "MCP", "AGENT", "CLI", "REALTIME"]);
|
|
1407
|
+
var SurfaceSchemaDigestsSchema = z2.object({
|
|
1408
|
+
params: z2.string().nullable(),
|
|
1409
|
+
input: z2.string().nullable(),
|
|
1410
|
+
output: z2.string().nullable(),
|
|
1411
|
+
multipart: z2.string().nullable()
|
|
2898
1412
|
});
|
|
2899
|
-
var SurfaceManifestOperationSchema =
|
|
2900
|
-
kind:
|
|
2901
|
-
service:
|
|
2902
|
-
action:
|
|
1413
|
+
var SurfaceManifestOperationSchema = z2.object({
|
|
1414
|
+
kind: z2.enum(["contract", "runtime"]),
|
|
1415
|
+
service: z2.string(),
|
|
1416
|
+
action: z2.string(),
|
|
2903
1417
|
method: HttpMethodSchema,
|
|
2904
|
-
scope:
|
|
2905
|
-
description:
|
|
1418
|
+
scope: z2.string().nullable(),
|
|
1419
|
+
description: z2.string(),
|
|
2906
1420
|
schemas: SurfaceSchemaDigestsSchema,
|
|
2907
|
-
http:
|
|
1421
|
+
http: z2.array(z2.object({ method: HttpMethodSchema, path: z2.string() }))
|
|
2908
1422
|
});
|
|
2909
|
-
var SurfaceManifestToolSchema =
|
|
2910
|
-
kind:
|
|
2911
|
-
service:
|
|
2912
|
-
action:
|
|
2913
|
-
name:
|
|
2914
|
-
input:
|
|
1423
|
+
var SurfaceManifestToolSchema = z2.object({
|
|
1424
|
+
kind: z2.enum(["contract", "runtime"]),
|
|
1425
|
+
service: z2.string(),
|
|
1426
|
+
action: z2.string(),
|
|
1427
|
+
name: z2.string(),
|
|
1428
|
+
input: z2.string()
|
|
2915
1429
|
});
|
|
2916
|
-
var SurfaceManifestToolSurfaceSchema =
|
|
1430
|
+
var SurfaceManifestToolSurfaceSchema = z2.object({
|
|
2917
1431
|
transport: ToolTransportSchema,
|
|
2918
|
-
surface:
|
|
2919
|
-
tools:
|
|
1432
|
+
surface: z2.string().nullable(),
|
|
1433
|
+
tools: z2.array(SurfaceManifestToolSchema)
|
|
2920
1434
|
});
|
|
2921
|
-
var SurfaceManifestCliCommandSchema =
|
|
2922
|
-
name:
|
|
2923
|
-
description:
|
|
2924
|
-
input:
|
|
2925
|
-
output:
|
|
1435
|
+
var SurfaceManifestCliCommandSchema = z2.object({
|
|
1436
|
+
name: z2.string(),
|
|
1437
|
+
description: z2.string(),
|
|
1438
|
+
input: z2.string(),
|
|
1439
|
+
output: z2.string().nullable()
|
|
2926
1440
|
});
|
|
2927
|
-
var SurfaceManifestExtensionSchema =
|
|
2928
|
-
name:
|
|
1441
|
+
var SurfaceManifestExtensionSchema = z2.object({
|
|
1442
|
+
name: z2.string(),
|
|
2929
1443
|
transport: SurfaceTransportSchema
|
|
2930
1444
|
});
|
|
2931
|
-
var SurfaceRealtimeSchemaPairSchema =
|
|
2932
|
-
input:
|
|
2933
|
-
output:
|
|
1445
|
+
var SurfaceRealtimeSchemaPairSchema = z2.object({
|
|
1446
|
+
input: z2.string(),
|
|
1447
|
+
output: z2.string()
|
|
2934
1448
|
});
|
|
2935
|
-
var SurfaceManifestRealtimeEventSchema =
|
|
2936
|
-
contract:
|
|
2937
|
-
direction:
|
|
2938
|
-
event:
|
|
1449
|
+
var SurfaceManifestRealtimeEventSchema = z2.object({
|
|
1450
|
+
contract: z2.string(),
|
|
1451
|
+
direction: z2.enum(["serverToClient", "clientToServer"]),
|
|
1452
|
+
event: z2.string(),
|
|
2939
1453
|
args: SurfaceRealtimeSchemaPairSchema,
|
|
2940
1454
|
acknowledgement: SurfaceRealtimeSchemaPairSchema.nullable()
|
|
2941
1455
|
});
|
|
2942
|
-
var SurfaceManifestSchema =
|
|
2943
|
-
manifestVersion:
|
|
2944
|
-
digestVersion:
|
|
2945
|
-
operations:
|
|
2946
|
-
toolSurfaces:
|
|
2947
|
-
realtimeContracts:
|
|
2948
|
-
realtime:
|
|
2949
|
-
cliOnly:
|
|
2950
|
-
extensions:
|
|
1456
|
+
var SurfaceManifestSchema = z2.object({
|
|
1457
|
+
manifestVersion: z2.literal(2),
|
|
1458
|
+
digestVersion: z2.literal(1),
|
|
1459
|
+
operations: z2.array(SurfaceManifestOperationSchema),
|
|
1460
|
+
toolSurfaces: z2.array(SurfaceManifestToolSurfaceSchema),
|
|
1461
|
+
realtimeContracts: z2.array(z2.string()),
|
|
1462
|
+
realtime: z2.array(SurfaceManifestRealtimeEventSchema),
|
|
1463
|
+
cliOnly: z2.array(SurfaceManifestCliCommandSchema),
|
|
1464
|
+
extensions: z2.array(SurfaceManifestExtensionSchema)
|
|
2951
1465
|
});
|
|
2952
1466
|
function compareCodepoints(left, right) {
|
|
2953
1467
|
if (left < right)
|
|
@@ -3228,37 +1742,37 @@ actual: ${actual}`);
|
|
|
3228
1742
|
}
|
|
3229
1743
|
|
|
3230
1744
|
// src/testing/surface-conformance.ts
|
|
3231
|
-
var RealtimeRejectionObservationSchema =
|
|
1745
|
+
var RealtimeRejectionObservationSchema = z3.object({
|
|
3232
1746
|
direction: RealtimeRejectDirectionSchema,
|
|
3233
1747
|
phase: RealtimeRejectPhaseSchema,
|
|
3234
1748
|
reason: RealtimeRejectReasonSchema,
|
|
3235
1749
|
fault: RealtimeRejectFaultSchema
|
|
3236
1750
|
});
|
|
3237
1751
|
var ObservationFields = {
|
|
3238
|
-
code:
|
|
3239
|
-
data:
|
|
3240
|
-
diagnostic:
|
|
3241
|
-
handlerCalls:
|
|
1752
|
+
code: z3.string().optional(),
|
|
1753
|
+
data: z3.unknown().optional(),
|
|
1754
|
+
diagnostic: z3.string().optional(),
|
|
1755
|
+
handlerCalls: z3.number().int().nonnegative().optional()
|
|
3242
1756
|
};
|
|
3243
|
-
var RealtimeDisconnectObservationSchema =
|
|
3244
|
-
phase:
|
|
1757
|
+
var RealtimeDisconnectObservationSchema = z3.object({
|
|
1758
|
+
phase: z3.enum(["before-invoke", "in-flight"])
|
|
3245
1759
|
});
|
|
3246
|
-
var TransportObservationSchema =
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
outcome:
|
|
1760
|
+
var TransportObservationSchema = z3.discriminatedUnion("outcome", [
|
|
1761
|
+
z3.object({ outcome: z3.literal("success"), ...ObservationFields }),
|
|
1762
|
+
z3.object({ outcome: z3.literal("validation_error"), ...ObservationFields }),
|
|
1763
|
+
z3.object({ outcome: z3.literal("domain_error"), ...ObservationFields }),
|
|
1764
|
+
z3.object({ outcome: z3.literal("aborted"), ...ObservationFields }),
|
|
1765
|
+
z3.object({
|
|
1766
|
+
outcome: z3.literal("realtime_rejected"),
|
|
3253
1767
|
...ObservationFields,
|
|
3254
1768
|
rejection: RealtimeRejectionObservationSchema
|
|
3255
1769
|
}),
|
|
3256
|
-
|
|
3257
|
-
outcome:
|
|
1770
|
+
z3.object({
|
|
1771
|
+
outcome: z3.literal("disconnected"),
|
|
3258
1772
|
...ObservationFields,
|
|
3259
1773
|
disconnect: RealtimeDisconnectObservationSchema
|
|
3260
1774
|
}),
|
|
3261
|
-
|
|
1775
|
+
z3.object({ outcome: z3.literal("timeout"), ...ObservationFields })
|
|
3262
1776
|
]);
|
|
3263
1777
|
function scenarioOutcome(scenario) {
|
|
3264
1778
|
if (scenario === "event" || scenario === "acknowledgement")
|