@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.4
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/AUTHORING.md +14 -0
- package/CHANGELOG.md +4 -0
- package/bin/apifuse-check.ts +18 -58
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-smoke.ts +22 -81
- package/bin/apifuse-pack-types.ts +266 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +23 -61
- package/bin/apifuse-submit-check.ts +26 -21
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-xml.ts +1 -1
- package/dist/auth-turn/index.d.ts +1 -1
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +1 -1
- package/dist/ceremonies/index.js +8 -18
- package/dist/config/loader.d.ts +2 -2
- package/dist/config/loader.js +19 -42
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +8 -1
- package/dist/define.js +57 -115
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -3
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +43 -42
- package/dist/index.js +38 -37
- package/dist/lint.js +8 -15
- package/dist/provider.d.ts +11 -11
- package/dist/provider.js +9 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +1 -1
- package/dist/runtime/auth-flow.js +3 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +12 -34
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +2 -2
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +5 -5
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +5 -15
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +3 -5
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +1 -1
- package/dist/runtime/proxy-telemetry.js +16 -48
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +1 -1
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +3 -3
- package/dist/runtime/stealth.d.ts +2 -2
- package/dist/runtime/stealth.js +6 -6
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +3 -3
- package/dist/server/self-test.js +3 -3
- package/dist/server/serve.d.ts +3 -3
- package/dist/server/serve.js +36 -49
- package/dist/server/types.js +2 -7
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +1 -1
- package/dist/testing/run.js +7 -14
- package/dist/types.d.ts +57 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +6 -4
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +24 -78
- package/src/config/loader.ts +58 -176
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +174 -495
- package/src/dev.ts +4 -9
- package/src/errors.ts +4 -11
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +44 -42
- package/src/lint.ts +89 -159
- package/src/provider.ts +12 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +5 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +55 -129
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +9 -19
- package/src/runtime/http.ts +7 -7
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +19 -58
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +17 -38
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +23 -72
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +4 -13
- package/src/runtime/state.ts +30 -109
- package/src/runtime/stealth.ts +8 -8
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +7 -7
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +5 -5
- package/src/server/serve.ts +87 -242
- package/src/server/types.ts +6 -19
- package/src/stealth/profiles.ts +10 -26
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +2 -2
- package/src/testing/run.ts +19 -67
- package/src/types.ts +58 -0
- package/src/user-input.ts +118 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// Guards the published artifact against the nodenext type-resolution regression
|
|
4
|
+
// shipped in 2.2.0-beta.1/beta.2: emitted dist/*.d.ts (and dist/*.js) carried
|
|
5
|
+
// EXTENSIONLESS relative imports, so external provider repos compiling with
|
|
6
|
+
// moduleResolution nodenext could not resolve them. With skipLibCheck the
|
|
7
|
+
// errors were masked and types silently degraded — re-exported ProviderError
|
|
8
|
+
// lost its inherited Error members (name/message) and re-exported zod stopped
|
|
9
|
+
// inferring .refine() callback params (~17 false type errors in
|
|
10
|
+
// apifuse-provider-tabelog, see its PR #16).
|
|
11
|
+
//
|
|
12
|
+
// Three layers, all against the packed tarball (what consumers actually get):
|
|
13
|
+
// 1. @arethetypeswrong/cli — resolution/module-kind audit of every exports
|
|
14
|
+
// subpath, including internal resolution of relative specifiers in d.ts.
|
|
15
|
+
// 2. A fixture consumer compiled with real tsc under moduleResolution
|
|
16
|
+
// nodenext and skipLibCheck:false, asserting ProviderError keeps
|
|
17
|
+
// inherited Error members and zod re-exports keep .refine() inference.
|
|
18
|
+
// 3. A Node (not bun) ESM runtime import — node does not tolerate
|
|
19
|
+
// extensionless relative specifiers, bun does.
|
|
20
|
+
//
|
|
21
|
+
// A deliberate negative control proves the fixture compiler actually fails on
|
|
22
|
+
// type errors, so this check cannot rot into a false-positive green.
|
|
23
|
+
|
|
24
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
25
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { join, resolve } from "node:path";
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
|
|
30
|
+
const PACK_RESULT_SCHEMA = z.array(
|
|
31
|
+
z.object({
|
|
32
|
+
filename: z.string(),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const KEEP_TEMP = process.env.APIFUSE__PACK_TYPES__KEEP_TEMP === "1";
|
|
37
|
+
const sdkRoot = process.cwd();
|
|
38
|
+
|
|
39
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "apifuse-provider-sdk-pack-types-"));
|
|
40
|
+
const packDir = join(tempRoot, "pack");
|
|
41
|
+
const consumerDir = join(tempRoot, "consumer");
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
mkdirSync(packDir, { recursive: true });
|
|
45
|
+
mkdirSync(consumerDir, { recursive: true });
|
|
46
|
+
|
|
47
|
+
const packed = packSdk(packDir);
|
|
48
|
+
const tarballPath = resolve(packDir, packed.filename);
|
|
49
|
+
|
|
50
|
+
runAreTheTypesWrong(tarballPath);
|
|
51
|
+
setUpFixtureConsumer(consumerDir, tarballPath);
|
|
52
|
+
run("bun", ["install"], consumerDir);
|
|
53
|
+
compileFixtureConsumer(consumerDir);
|
|
54
|
+
assertNegativeControlFails(consumerDir);
|
|
55
|
+
runNodeRuntimeImport(consumerDir);
|
|
56
|
+
|
|
57
|
+
console.log(`Packed artifact types OK under nodenext: ${tarballPath}`);
|
|
58
|
+
} finally {
|
|
59
|
+
if (KEEP_TEMP) {
|
|
60
|
+
console.log(`Keeping pack-types temp directory: ${tempRoot}`);
|
|
61
|
+
} else {
|
|
62
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runAreTheTypesWrong(tarballPath: string): void {
|
|
67
|
+
// esm-only profile: this package publishes ESM only, so CJS-consumer
|
|
68
|
+
// resolution failures are expected and out of scope for this guard.
|
|
69
|
+
run(
|
|
70
|
+
"bun",
|
|
71
|
+
[join(sdkRoot, "node_modules", ".bin", "attw"), tarballPath, "--profile", "esm-only"],
|
|
72
|
+
sdkRoot,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function setUpFixtureConsumer(consumerDir: string, tarballPath: string): void {
|
|
77
|
+
writeFileSync(
|
|
78
|
+
join(consumerDir, "package.json"),
|
|
79
|
+
`${JSON.stringify(
|
|
80
|
+
{
|
|
81
|
+
private: true,
|
|
82
|
+
type: "module",
|
|
83
|
+
dependencies: {
|
|
84
|
+
"@apifuse/provider-sdk": `file:${tarballPath}`,
|
|
85
|
+
},
|
|
86
|
+
devDependencies: {
|
|
87
|
+
// Pinned to TS 5.x independently of the repo's own TypeScript 7
|
|
88
|
+
// toolchain: this must reflect what external provider polyrepos
|
|
89
|
+
// (e.g. apifuse-provider-tabelog) run in their CI.
|
|
90
|
+
typescript: "^5.9.3",
|
|
91
|
+
// skipLibCheck:false type-checks transitive deps too (ioredis
|
|
92
|
+
// references Buffer), so the fixture needs node types like any
|
|
93
|
+
// real provider repo.
|
|
94
|
+
"@types/node": "^25.9.3",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
null,
|
|
98
|
+
2,
|
|
99
|
+
)}\n`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
writeFileSync(
|
|
103
|
+
join(consumerDir, "tsconfig.json"),
|
|
104
|
+
`${JSON.stringify(
|
|
105
|
+
{
|
|
106
|
+
compilerOptions: {
|
|
107
|
+
target: "ES2022",
|
|
108
|
+
module: "nodenext",
|
|
109
|
+
moduleResolution: "nodenext",
|
|
110
|
+
strict: true,
|
|
111
|
+
// skipLibCheck must stay off: it is exactly what masked the
|
|
112
|
+
// extensionless-d.ts regression downstream.
|
|
113
|
+
skipLibCheck: false,
|
|
114
|
+
noEmit: true,
|
|
115
|
+
},
|
|
116
|
+
include: ["consumer.ts"],
|
|
117
|
+
},
|
|
118
|
+
null,
|
|
119
|
+
2,
|
|
120
|
+
)}\n`,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
writeFileSync(
|
|
124
|
+
join(consumerDir, "consumer.ts"),
|
|
125
|
+
[
|
|
126
|
+
'import { ProviderError, SessionExpiredError, z } from "@apifuse/provider-sdk";',
|
|
127
|
+
'import { defineCredentialsAuth } from "@apifuse/provider-sdk/provider";',
|
|
128
|
+
'import { extractProviderContract } from "@apifuse/provider-sdk/contract";',
|
|
129
|
+
'import { AUTH_TURN_SCHEMA } from "@apifuse/provider-sdk/auth-turn";',
|
|
130
|
+
'import { serve } from "@apifuse/provider-sdk/server";',
|
|
131
|
+
'import { runStandardTests } from "@apifuse/provider-sdk/testing";',
|
|
132
|
+
"",
|
|
133
|
+
"// ProviderError must keep its inherited Error members under nodenext.",
|
|
134
|
+
"// When dist d.ts imports fail to resolve, the class type degrades and",
|
|
135
|
+
"// name/message disappear (the tabelog false-error mode).",
|
|
136
|
+
'const providerError = new ProviderError("boom");',
|
|
137
|
+
"const inheritedName: string = providerError.name;",
|
|
138
|
+
"const inheritedMessage: string = providerError.message;",
|
|
139
|
+
"const inheritedStack: string | undefined = providerError.stack;",
|
|
140
|
+
"const isError: Error = providerError;",
|
|
141
|
+
"const sessionExpired: ProviderError = new SessionExpiredError();",
|
|
142
|
+
"",
|
|
143
|
+
"// Re-exported zod must keep .refine() callback parameter inference.",
|
|
144
|
+
"const refined = z.object({ shopId: z.string() }).refine((value) => value.shopId.length > 0);",
|
|
145
|
+
'const refinedString = z.string().refine((value) => value.startsWith("tabelog:"));',
|
|
146
|
+
"",
|
|
147
|
+
"export const witnesses = {",
|
|
148
|
+
" inheritedName,",
|
|
149
|
+
" inheritedMessage,",
|
|
150
|
+
" inheritedStack,",
|
|
151
|
+
" isError,",
|
|
152
|
+
" sessionExpired,",
|
|
153
|
+
" refined,",
|
|
154
|
+
" refinedString,",
|
|
155
|
+
" defineCredentialsAuth,",
|
|
156
|
+
" extractProviderContract,",
|
|
157
|
+
" AUTH_TURN_SCHEMA,",
|
|
158
|
+
" serve,",
|
|
159
|
+
" runStandardTests,",
|
|
160
|
+
"};",
|
|
161
|
+
"",
|
|
162
|
+
].join("\n"),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
writeFileSync(
|
|
166
|
+
join(consumerDir, "negative-control.ts"),
|
|
167
|
+
[
|
|
168
|
+
'import { ProviderError } from "@apifuse/provider-sdk";',
|
|
169
|
+
"",
|
|
170
|
+
"// Intentionally wrong: name is a string. If this file compiles, the",
|
|
171
|
+
"// fixture consumer is not actually type-checking and the guard is void.",
|
|
172
|
+
'export const mustNotCompile: number = new ProviderError("boom").name;',
|
|
173
|
+
"",
|
|
174
|
+
].join("\n"),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function compileFixtureConsumer(consumerDir: string): void {
|
|
179
|
+
run(
|
|
180
|
+
"bun",
|
|
181
|
+
[join(consumerDir, "node_modules", ".bin", "tsc"), "-p", "tsconfig.json"],
|
|
182
|
+
consumerDir,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function assertNegativeControlFails(consumerDir: string): void {
|
|
187
|
+
const result = spawnSync(
|
|
188
|
+
"bun",
|
|
189
|
+
[
|
|
190
|
+
join(consumerDir, "node_modules", ".bin", "tsc"),
|
|
191
|
+
"--target",
|
|
192
|
+
"ES2022",
|
|
193
|
+
"--module",
|
|
194
|
+
"nodenext",
|
|
195
|
+
"--moduleResolution",
|
|
196
|
+
"nodenext",
|
|
197
|
+
"--strict",
|
|
198
|
+
"--noEmit",
|
|
199
|
+
"negative-control.ts",
|
|
200
|
+
],
|
|
201
|
+
{ cwd: consumerDir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
202
|
+
);
|
|
203
|
+
if (result.status === 0) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
"Negative control compiled cleanly: the fixture consumer is not detecting type errors, so the nodenext guard proves nothing.",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (!`${result.stdout}\n${result.stderr}`.includes("TS2322")) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Negative control failed for an unexpected reason (wanted TS2322):\n${result.stdout}\n${result.stderr}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function runNodeRuntimeImport(consumerDir: string): void {
|
|
216
|
+
// Node, not bun: bun resolves extensionless relative specifiers in
|
|
217
|
+
// published dist/*.js, node correctly refuses them (ERR_MODULE_NOT_FOUND).
|
|
218
|
+
run(
|
|
219
|
+
"node",
|
|
220
|
+
[
|
|
221
|
+
"--input-type=module",
|
|
222
|
+
"-e",
|
|
223
|
+
[
|
|
224
|
+
'const sdk = await import("@apifuse/provider-sdk");',
|
|
225
|
+
'const error = new sdk.ProviderError("boom");',
|
|
226
|
+
'if (error.name !== "ProviderError" || error.message !== "boom" || !(error instanceof Error)) {',
|
|
227
|
+
' throw new Error("ProviderError runtime shape is wrong: " + JSON.stringify({ name: error.name, message: error.message }));',
|
|
228
|
+
"}",
|
|
229
|
+
'console.log("node ESM runtime import OK");',
|
|
230
|
+
].join("\n"),
|
|
231
|
+
],
|
|
232
|
+
consumerDir,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function packSdk(destination: string): { filename: string } {
|
|
237
|
+
const raw = execFileSync("npm", ["pack", "--json", "--pack-destination", destination], {
|
|
238
|
+
cwd: sdkRoot,
|
|
239
|
+
encoding: "utf8",
|
|
240
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
241
|
+
});
|
|
242
|
+
const parsed = PACK_RESULT_SCHEMA.parse(JSON.parse(raw));
|
|
243
|
+
const first = parsed[0];
|
|
244
|
+
if (!first) {
|
|
245
|
+
throw new Error("npm pack --json returned no package metadata.");
|
|
246
|
+
}
|
|
247
|
+
return first;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function run(command: string, args: string[], cwd: string): void {
|
|
251
|
+
const result = spawnSync(command, args, {
|
|
252
|
+
cwd,
|
|
253
|
+
env: process.env,
|
|
254
|
+
stdio: "inherit",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (result.error) {
|
|
258
|
+
throw result.error;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (result.status !== 0) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Command failed (${[command, ...args].join(" ")}) in ${cwd} with exit code ${result.status}`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
package/bin/apifuse-perf.ts
CHANGED
|
@@ -24,19 +24,12 @@ import {
|
|
|
24
24
|
type StealthClient,
|
|
25
25
|
type StealthResponse,
|
|
26
26
|
wrapWithInstrumentation,
|
|
27
|
-
} from "../src";
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from "../src/
|
|
33
|
-
import { createMemoryProviderRuntimeState } from "../src/runtime/state";
|
|
34
|
-
import {
|
|
35
|
-
createTraceContext,
|
|
36
|
-
resolveTraceContextOptions,
|
|
37
|
-
} from "../src/runtime/trace";
|
|
38
|
-
import { renderWaterfall } from "../src/runtime/waterfall";
|
|
39
|
-
import type { BrowserClient } from "../src/types";
|
|
27
|
+
} from "../src/index.js";
|
|
28
|
+
import { computeStats, groupSpansByName, type PerfStats } from "../src/runtime/perf.js";
|
|
29
|
+
import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
|
|
30
|
+
import { createTraceContext, resolveTraceContextOptions } from "../src/runtime/trace.js";
|
|
31
|
+
import { renderWaterfall } from "../src/runtime/waterfall.js";
|
|
32
|
+
import type { BrowserClient } from "../src/types.js";
|
|
40
33
|
|
|
41
34
|
type CliArgs = {
|
|
42
35
|
compareProxy: boolean;
|
|
@@ -112,11 +105,7 @@ export async function main() {
|
|
|
112
105
|
const inputSchema = getOperationSchema(provider, operation, "input");
|
|
113
106
|
const outputSchema = getOperationSchema(provider, operation, "output");
|
|
114
107
|
const fixtureReplay = await loadFixtureReplay(providerDirectory);
|
|
115
|
-
const inputTemplate = resolveInputTemplate(
|
|
116
|
-
provider,
|
|
117
|
-
inputSchema,
|
|
118
|
-
args.params,
|
|
119
|
-
);
|
|
108
|
+
const inputTemplate = resolveInputTemplate(provider, inputSchema, args.params);
|
|
120
109
|
|
|
121
110
|
const directSuite = await runProfileSuite({
|
|
122
111
|
args,
|
|
@@ -168,9 +157,7 @@ export async function main() {
|
|
|
168
157
|
label: `${providerId}/${args.operation}`,
|
|
169
158
|
proxyEnabled: false,
|
|
170
159
|
stats: directSuite.stats,
|
|
171
|
-
mode: directSuite.runs.some((run) => run.mode === "fixture")
|
|
172
|
-
? "fixture"
|
|
173
|
-
: "live",
|
|
160
|
+
mode: directSuite.runs.some((run) => run.mode === "fixture") ? "fixture" : "live",
|
|
174
161
|
});
|
|
175
162
|
console.log(`Flamegraph: ${flamePath}`);
|
|
176
163
|
}
|
|
@@ -294,27 +281,18 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
294
281
|
}
|
|
295
282
|
|
|
296
283
|
if (arg.startsWith("--warmup=")) {
|
|
297
|
-
warmup = parseNonNegativeInteger(
|
|
298
|
-
arg.slice("--warmup=".length),
|
|
299
|
-
"--warmup",
|
|
300
|
-
);
|
|
284
|
+
warmup = parseNonNegativeInteger(arg.slice("--warmup=".length), "--warmup");
|
|
301
285
|
continue;
|
|
302
286
|
}
|
|
303
287
|
|
|
304
288
|
if (arg === "--concurrency" || arg === "-c") {
|
|
305
|
-
concurrency = parsePositiveInteger(
|
|
306
|
-
requireArgValue(argv, index, arg),
|
|
307
|
-
arg,
|
|
308
|
-
);
|
|
289
|
+
concurrency = parsePositiveInteger(requireArgValue(argv, index, arg), arg);
|
|
309
290
|
index += 1;
|
|
310
291
|
continue;
|
|
311
292
|
}
|
|
312
293
|
|
|
313
294
|
if (arg.startsWith("--concurrency=")) {
|
|
314
|
-
concurrency = parsePositiveInteger(
|
|
315
|
-
arg.slice("--concurrency=".length),
|
|
316
|
-
"--concurrency",
|
|
317
|
-
);
|
|
295
|
+
concurrency = parsePositiveInteger(arg.slice("--concurrency=".length), "--concurrency");
|
|
318
296
|
continue;
|
|
319
297
|
}
|
|
320
298
|
|
|
@@ -354,11 +332,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
354
332
|
};
|
|
355
333
|
}
|
|
356
334
|
|
|
357
|
-
function requireArgValue(
|
|
358
|
-
argv: string[],
|
|
359
|
-
index: number,
|
|
360
|
-
option: string,
|
|
361
|
-
): string {
|
|
335
|
+
function requireArgValue(argv: string[], index: number, option: string): string {
|
|
362
336
|
const value = argv[index + 1];
|
|
363
337
|
if (!value) {
|
|
364
338
|
throw new Error(`Missing value for ${option}`);
|
|
@@ -396,9 +370,7 @@ function resolveProviderEntry(providerDirectory: string): string {
|
|
|
396
370
|
throw new Error(`Provider entry not found in ${providerDirectory}`);
|
|
397
371
|
}
|
|
398
372
|
|
|
399
|
-
async function loadProvider(
|
|
400
|
-
providerEntry: string,
|
|
401
|
-
): Promise<ProviderDefinition> {
|
|
373
|
+
async function loadProvider(providerEntry: string): Promise<ProviderDefinition> {
|
|
402
374
|
const mod = (await import(pathToFileURL(providerEntry).href)) as {
|
|
403
375
|
default?: ProviderDefinition;
|
|
404
376
|
};
|
|
@@ -469,9 +441,7 @@ function resolveInputTemplate(
|
|
|
469
441
|
}
|
|
470
442
|
}
|
|
471
443
|
|
|
472
|
-
async function loadFixtureReplay(
|
|
473
|
-
providerDirectory: string,
|
|
474
|
-
): Promise<FixtureReplay | null> {
|
|
444
|
+
async function loadFixtureReplay(providerDirectory: string): Promise<FixtureReplay | null> {
|
|
475
445
|
const fixturePath = resolve(providerDirectory, "__fixtures__", "raw.json");
|
|
476
446
|
if (!existsSync(fixturePath)) {
|
|
477
447
|
return null;
|
|
@@ -521,15 +491,8 @@ async function runProfileSuite(options: {
|
|
|
521
491
|
}
|
|
522
492
|
|
|
523
493
|
const runs: RunResult[] = [];
|
|
524
|
-
for (
|
|
525
|
-
|
|
526
|
-
index < options.args.runs;
|
|
527
|
-
index += options.args.concurrency
|
|
528
|
-
) {
|
|
529
|
-
const batchSize = Math.min(
|
|
530
|
-
options.args.concurrency,
|
|
531
|
-
options.args.runs - index,
|
|
532
|
-
);
|
|
494
|
+
for (let index = 0; index < options.args.runs; index += options.args.concurrency) {
|
|
495
|
+
const batchSize = Math.min(options.args.concurrency, options.args.runs - index);
|
|
533
496
|
const batch = Array.from({ length: batchSize }, () =>
|
|
534
497
|
profileRun({
|
|
535
498
|
config: options.config,
|
|
@@ -552,8 +515,7 @@ async function runProfileSuite(options: {
|
|
|
552
515
|
const breakdown = [...groupedSpans.entries()]
|
|
553
516
|
.filter(([name]) => name !== rootSpanName)
|
|
554
517
|
.map(([name, durations]) => {
|
|
555
|
-
const avgMs =
|
|
556
|
-
durations.reduce((sum, value) => sum + value, 0) / durations.length;
|
|
518
|
+
const avgMs = durations.reduce((sum, value) => sum + value, 0) / durations.length;
|
|
557
519
|
const percent = stats.avg > 0 ? (avgMs / stats.avg) * 100 : 0;
|
|
558
520
|
return { avgMs, name, percent };
|
|
559
521
|
})
|
|
@@ -611,9 +573,7 @@ async function executeProfileRun(options: {
|
|
|
611
573
|
proxyEnabled: boolean;
|
|
612
574
|
}): Promise<RunResult> {
|
|
613
575
|
const _operation = getOperation(options.provider, options.operationName);
|
|
614
|
-
const traceContext = createTraceContext(
|
|
615
|
-
resolveTraceContextOptions(options.config.trace),
|
|
616
|
-
);
|
|
576
|
+
const traceContext = createTraceContext(resolveTraceContextOptions(options.config.trace));
|
|
617
577
|
const baseContext = createBaseContext({
|
|
618
578
|
config: options.config,
|
|
619
579
|
provider: options.provider,
|
|
@@ -638,9 +598,7 @@ async function executeProfileRun(options: {
|
|
|
638
598
|
normalizedInput,
|
|
639
599
|
);
|
|
640
600
|
|
|
641
|
-
return ctx.trace.span("transformResponse", async () =>
|
|
642
|
-
options.outputSchema.parse(result),
|
|
643
|
-
);
|
|
601
|
+
return ctx.trace.span("transformResponse", async () => options.outputSchema.parse(result));
|
|
644
602
|
});
|
|
645
603
|
const durationMs = performance.now() - startedAt;
|
|
646
604
|
const spans = ctx.trace.getSpans();
|
|
@@ -790,36 +748,24 @@ function createBrowserStub(): BrowserClient {
|
|
|
790
748
|
engine: "playwright-stealth",
|
|
791
749
|
async close() {},
|
|
792
750
|
async newPage() {
|
|
793
|
-
throw new ProviderError(
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
797
|
-
},
|
|
798
|
-
);
|
|
751
|
+
throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
|
|
752
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
753
|
+
});
|
|
799
754
|
},
|
|
800
755
|
async rawPage() {
|
|
801
|
-
throw new ProviderError(
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
805
|
-
},
|
|
806
|
-
);
|
|
756
|
+
throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
|
|
757
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
758
|
+
});
|
|
807
759
|
},
|
|
808
760
|
async withIsolatedContext() {
|
|
809
|
-
throw new ProviderError(
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
813
|
-
},
|
|
814
|
-
);
|
|
761
|
+
throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
|
|
762
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
763
|
+
});
|
|
815
764
|
},
|
|
816
765
|
async solveChallenge() {
|
|
817
|
-
throw new ProviderError(
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
821
|
-
},
|
|
822
|
-
);
|
|
766
|
+
throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
|
|
767
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
768
|
+
});
|
|
823
769
|
},
|
|
824
770
|
};
|
|
825
771
|
}
|
|
@@ -833,9 +779,7 @@ function buildInsights(
|
|
|
833
779
|
const allSpans = runs.flatMap((run) => run.spans);
|
|
834
780
|
const stealthSpans = allSpans.filter((span) => span.name === "stealth.fetch");
|
|
835
781
|
const dnsSpans = allSpans.filter((span) => span.name === "dns");
|
|
836
|
-
const transform = breakdown.find(
|
|
837
|
-
(entry) => entry.name === "transformResponse",
|
|
838
|
-
);
|
|
782
|
+
const transform = breakdown.find((entry) => entry.name === "transformResponse");
|
|
839
783
|
const responseSizes = allSpans
|
|
840
784
|
.map((span) => span.attributes.response_size)
|
|
841
785
|
.filter((value): value is number => typeof value === "number");
|
|
@@ -844,9 +788,7 @@ function buildInsights(
|
|
|
844
788
|
.filter((value): value is boolean => typeof value === "boolean");
|
|
845
789
|
|
|
846
790
|
if (reuseFlags.length > 0) {
|
|
847
|
-
const reusePercent = Math.round(
|
|
848
|
-
(reuseFlags.filter(Boolean).length / reuseFlags.length) * 100,
|
|
849
|
-
);
|
|
791
|
+
const reusePercent = Math.round((reuseFlags.filter(Boolean).length / reuseFlags.length) * 100);
|
|
850
792
|
insights.push(
|
|
851
793
|
reusePercent >= 80
|
|
852
794
|
? `✓ Stealth connection reuse: ${reusePercent}% (good)`
|
|
@@ -863,24 +805,16 @@ function buildInsights(
|
|
|
863
805
|
}
|
|
864
806
|
|
|
865
807
|
if (dnsSpans.length > 0) {
|
|
866
|
-
const dnsAvg =
|
|
867
|
-
dnsSpans.reduce((sum, span) => sum + span.duration_ms, 0) /
|
|
868
|
-
dnsSpans.length;
|
|
808
|
+
const dnsAvg = dnsSpans.reduce((sum, span) => sum + span.duration_ms, 0) / dnsSpans.length;
|
|
869
809
|
if (dnsAvg >= 5) {
|
|
870
|
-
insights.push(
|
|
871
|
-
`⚠ DNS resolution: ${formatDuration(dnsAvg)} avg — consider DNS caching`,
|
|
872
|
-
);
|
|
810
|
+
insights.push(`⚠ DNS resolution: ${formatDuration(dnsAvg)} avg — consider DNS caching`);
|
|
873
811
|
}
|
|
874
812
|
}
|
|
875
813
|
|
|
876
814
|
if (responseSizes.length > 0) {
|
|
877
|
-
const avgSize =
|
|
878
|
-
responseSizes.reduce((sum, value) => sum + value, 0) /
|
|
879
|
-
responseSizes.length;
|
|
815
|
+
const avgSize = responseSizes.reduce((sum, value) => sum + value, 0) / responseSizes.length;
|
|
880
816
|
if (avgSize >= 4096) {
|
|
881
|
-
insights.push(
|
|
882
|
-
`⚠ Response size: ${formatBytes(avgSize)} avg — check gzip (Accept-Encoding)`,
|
|
883
|
-
);
|
|
817
|
+
insights.push(`⚠ Response size: ${formatBytes(avgSize)} avg — check gzip (Accept-Encoding)`);
|
|
884
818
|
}
|
|
885
819
|
}
|
|
886
820
|
|
|
@@ -925,13 +859,10 @@ function renderReport(options: {
|
|
|
925
859
|
|
|
926
860
|
if (options.proxySuite) {
|
|
927
861
|
const delta = options.proxySuite.stats.p50 - options.suite.stats.p50;
|
|
928
|
-
const deltaPercent =
|
|
929
|
-
options.suite.stats.p50 > 0 ? (delta / options.suite.stats.p50) * 100 : 0;
|
|
862
|
+
const deltaPercent = options.suite.stats.p50 > 0 ? (delta / options.suite.stats.p50) * 100 : 0;
|
|
930
863
|
lines.push("│");
|
|
931
864
|
lines.push("│ Proxy Comparison (--compare-proxy):");
|
|
932
|
-
lines.push(
|
|
933
|
-
`│ proxy: off → p50: ${formatDuration(options.suite.stats.p50)}`,
|
|
934
|
-
);
|
|
865
|
+
lines.push(`│ proxy: off → p50: ${formatDuration(options.suite.stats.p50)}`);
|
|
935
866
|
lines.push(
|
|
936
867
|
`│ proxy: on → p50: ${formatDuration(options.proxySuite.stats.p50)} (${delta >= 0 ? "+" : ""}${Math.round(deltaPercent)}%)`,
|
|
937
868
|
);
|
|
@@ -979,9 +910,7 @@ function formatBytes(value: number): string {
|
|
|
979
910
|
}
|
|
980
911
|
|
|
981
912
|
function selectRepresentativeRun(runs: RunResult[]): RunResult {
|
|
982
|
-
const sorted = [...runs].sort(
|
|
983
|
-
(left, right) => left.durationMs - right.durationMs,
|
|
984
|
-
);
|
|
913
|
+
const sorted = [...runs].sort((left, right) => left.durationMs - right.durationMs);
|
|
985
914
|
const middle = sorted[Math.floor(sorted.length / 2)];
|
|
986
915
|
if (middle) {
|
|
987
916
|
return middle;
|
|
@@ -1065,17 +994,10 @@ function buildFlamegraphSvg(
|
|
|
1065
994
|
|
|
1066
995
|
const rects = flattened
|
|
1067
996
|
.map((node) => {
|
|
1068
|
-
const x =
|
|
1069
|
-
|
|
1070
|
-
((node.span.startedAt - root.startedAt) / totalDuration) * (width - 24);
|
|
1071
|
-
const rectWidth = Math.max(
|
|
1072
|
-
1,
|
|
1073
|
-
(node.span.duration_ms / totalDuration) * (width - 24),
|
|
1074
|
-
);
|
|
997
|
+
const x = 12 + ((node.span.startedAt - root.startedAt) / totalDuration) * (width - 24);
|
|
998
|
+
const rectWidth = Math.max(1, (node.span.duration_ms / totalDuration) * (width - 24));
|
|
1075
999
|
const y = chartTop + node.depth * rowHeight;
|
|
1076
|
-
const label = escapeXml(
|
|
1077
|
-
`${node.span.name} (${formatDuration(node.span.duration_ms)})`,
|
|
1078
|
-
);
|
|
1000
|
+
const label = escapeXml(`${node.span.name} (${formatDuration(node.span.duration_ms)})`);
|
|
1079
1001
|
const title = escapeXml(
|
|
1080
1002
|
`${node.span.name}\n${formatDuration(node.span.duration_ms)}\nstatus: ${node.span.status}`,
|
|
1081
1003
|
);
|
|
@@ -1094,9 +1016,7 @@ function buildFlamegraphSvg(
|
|
|
1094
1016
|
}
|
|
1095
1017
|
|
|
1096
1018
|
function buildFlameTree(spans: Span[]): FlameNode[] {
|
|
1097
|
-
const sorted = [...spans].sort(
|
|
1098
|
-
(left, right) => left.startedAt - right.startedAt,
|
|
1099
|
-
);
|
|
1019
|
+
const sorted = [...spans].sort((left, right) => left.startedAt - right.startedAt);
|
|
1100
1020
|
const nodeMap = new Map<string, FlameNode>();
|
|
1101
1021
|
|
|
1102
1022
|
for (const span of sorted) {
|
|
@@ -1139,9 +1059,7 @@ function findRootSpan(spans: Span[]): Span {
|
|
|
1139
1059
|
return root;
|
|
1140
1060
|
}
|
|
1141
1061
|
|
|
1142
|
-
const sorted = [...spans].sort(
|
|
1143
|
-
(left, right) => left.startedAt - right.startedAt,
|
|
1144
|
-
);
|
|
1062
|
+
const sorted = [...spans].sort((left, right) => left.startedAt - right.startedAt);
|
|
1145
1063
|
const first = sorted[0];
|
|
1146
1064
|
if (!first) {
|
|
1147
1065
|
throw new Error("Expected at least one span");
|