@modularizer/plat-client 0.4.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/args/coerce.d.ts +54 -0
- package/dist/args/coerce.d.ts.map +1 -0
- package/dist/args/coerce.js +236 -0
- package/dist/args/coerce.js.map +1 -0
- package/dist/args/index.d.ts +2 -0
- package/dist/args/index.d.ts.map +1 -0
- package/dist/args/index.js +2 -0
- package/dist/args/index.js.map +1 -0
- package/dist/args/scalars.d.ts +87 -0
- package/dist/args/scalars.d.ts.map +1 -0
- package/dist/args/scalars.js +22 -0
- package/dist/args/scalars.js.map +1 -0
- package/dist/args/validate.d.ts +23 -0
- package/dist/args/validate.d.ts.map +1 -0
- package/dist/args/validate.js +185 -0
- package/dist/args/validate.js.map +1 -0
- package/dist/args/z2.d.ts +27 -0
- package/dist/args/z2.d.ts.map +1 -0
- package/dist/args/z2.js +24 -0
- package/dist/args/z2.js.map +1 -0
- package/dist/client/css-transport-plugin.d.ts +19 -0
- package/dist/client/css-transport-plugin.d.ts.map +1 -0
- package/dist/client/css-transport-plugin.js +78 -0
- package/dist/client/css-transport-plugin.js.map +1 -0
- package/dist/client/file-transport-plugin.d.ts +28 -0
- package/dist/client/file-transport-plugin.d.ts.map +1 -0
- package/dist/client/file-transport-plugin.js +80 -0
- package/dist/client/file-transport-plugin.js.map +1 -0
- package/dist/client/http-transport-plugin.d.ts +27 -0
- package/dist/client/http-transport-plugin.d.ts.map +1 -0
- package/dist/client/http-transport-plugin.js +48 -0
- package/dist/client/http-transport-plugin.js.map +1 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +7 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/openapi-client.d.ts +334 -0
- package/dist/client/openapi-client.d.ts.map +1 -0
- package/dist/client/openapi-client.js +910 -0
- package/dist/client/openapi-client.js.map +1 -0
- package/dist/client/proxy.d.ts +5 -0
- package/dist/client/proxy.d.ts.map +1 -0
- package/dist/client/proxy.js +353 -0
- package/dist/client/proxy.js.map +1 -0
- package/dist/client/request-builder.d.ts +5 -0
- package/dist/client/request-builder.d.ts.map +1 -0
- package/dist/client/request-builder.js +88 -0
- package/dist/client/request-builder.js.map +1 -0
- package/dist/client/rpc-transport-plugin.d.ts +17 -0
- package/dist/client/rpc-transport-plugin.d.ts.map +1 -0
- package/dist/client/rpc-transport-plugin.js +69 -0
- package/dist/client/rpc-transport-plugin.js.map +1 -0
- package/dist/client/tools.d.ts +69 -0
- package/dist/client/tools.d.ts.map +1 -0
- package/dist/client/tools.js +122 -0
- package/dist/client/tools.js.map +1 -0
- package/dist/client/transport-plugin.d.ts +62 -0
- package/dist/client/transport-plugin.d.ts.map +1 -0
- package/dist/client/transport-plugin.js +40 -0
- package/dist/client/transport-plugin.js.map +1 -0
- package/dist/client-entry.d.ts +25 -0
- package/dist/client-entry.d.ts.map +1 -0
- package/dist/client-entry.js +25 -0
- package/dist/client-entry.js.map +1 -0
- package/dist/client-server-entry.d.ts +13 -0
- package/dist/client-server-entry.d.ts.map +1 -0
- package/dist/client-server-entry.js +13 -0
- package/dist/client-server-entry.js.map +1 -0
- package/dist/client-side-python/runtime.d.ts +102 -0
- package/dist/client-side-python/runtime.d.ts.map +1 -0
- package/dist/client-side-python/runtime.js +595 -0
- package/dist/client-side-python/runtime.js.map +1 -0
- package/dist/client-side-server/bootstrap.d.ts +3 -0
- package/dist/client-side-server/bootstrap.d.ts.map +1 -0
- package/dist/client-side-server/bootstrap.js +20 -0
- package/dist/client-side-server/bootstrap.js.map +1 -0
- package/dist/client-side-server/channel.d.ts +17 -0
- package/dist/client-side-server/channel.d.ts.map +1 -0
- package/dist/client-side-server/channel.js +38 -0
- package/dist/client-side-server/channel.js.map +1 -0
- package/dist/client-side-server/identity.d.ts +116 -0
- package/dist/client-side-server/identity.d.ts.map +1 -0
- package/dist/client-side-server/identity.js +358 -0
- package/dist/client-side-server/identity.js.map +1 -0
- package/dist/client-side-server/mqtt-webrtc.d.ts +77 -0
- package/dist/client-side-server/mqtt-webrtc.d.ts.map +1 -0
- package/dist/client-side-server/mqtt-webrtc.js +575 -0
- package/dist/client-side-server/mqtt-webrtc.js.map +1 -0
- package/dist/client-side-server/protocol.d.ts +49 -0
- package/dist/client-side-server/protocol.d.ts.map +1 -0
- package/dist/client-side-server/protocol.js +13 -0
- package/dist/client-side-server/protocol.js.map +1 -0
- package/dist/client-side-server/runtime.d.ts +57 -0
- package/dist/client-side-server/runtime.d.ts.map +1 -0
- package/dist/client-side-server/runtime.js +188 -0
- package/dist/client-side-server/runtime.js.map +1 -0
- package/dist/client-side-server/server.d.ts +75 -0
- package/dist/client-side-server/server.d.ts.map +1 -0
- package/dist/client-side-server/server.js +380 -0
- package/dist/client-side-server/server.js.map +1 -0
- package/dist/client-side-server/signaling.d.ts +10 -0
- package/dist/client-side-server/signaling.d.ts.map +1 -0
- package/dist/client-side-server/signaling.js +19 -0
- package/dist/client-side-server/signaling.js.map +1 -0
- package/dist/client-side-server/source-analysis.d.ts +29 -0
- package/dist/client-side-server/source-analysis.d.ts.map +1 -0
- package/dist/client-side-server/source-analysis.js +395 -0
- package/dist/client-side-server/source-analysis.js.map +1 -0
- package/dist/generated/python-browser-sources.d.ts +2 -0
- package/dist/generated/python-browser-sources.d.ts.map +1 -0
- package/dist/generated/python-browser-sources.js +13 -0
- package/dist/generated/python-browser-sources.js.map +1 -0
- package/dist/logging.d.ts +9 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +64 -0
- package/dist/logging.js.map +1 -0
- package/dist/python-browser-entry.d.ts +2 -0
- package/dist/python-browser-entry.d.ts.map +1 -0
- package/dist/python-browser-entry.js +2 -0
- package/dist/python-browser-entry.js.map +1 -0
- package/dist/rpc.d.ts +39 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +2 -0
- package/dist/rpc.js.map +1 -0
- package/dist/server/authority-server.d.ts +27 -0
- package/dist/server/authority-server.d.ts.map +1 -0
- package/dist/server/authority-server.js +97 -0
- package/dist/server/authority-server.js.map +1 -0
- package/dist/server/cache/index.d.ts +2 -0
- package/dist/server/cache/index.d.ts.map +1 -0
- package/dist/server/cache/index.js +2 -0
- package/dist/server/cache/index.js.map +1 -0
- package/dist/server/cache/utils.d.ts +30 -0
- package/dist/server/cache/utils.d.ts.map +1 -0
- package/dist/server/cache/utils.js +116 -0
- package/dist/server/cache/utils.js.map +1 -0
- package/dist/server/core.d.ts +43 -0
- package/dist/server/core.d.ts.map +1 -0
- package/dist/server/core.js +215 -0
- package/dist/server/core.js.map +1 -0
- package/dist/server/operation-registry.d.ts +9 -0
- package/dist/server/operation-registry.d.ts.map +1 -0
- package/dist/server/operation-registry.js +16 -0
- package/dist/server/operation-registry.js.map +1 -0
- package/dist/server/param-aliases.d.ts +40 -0
- package/dist/server/param-aliases.d.ts.map +1 -0
- package/dist/server/param-aliases.js +112 -0
- package/dist/server/param-aliases.js.map +1 -0
- package/dist/server/rate-limit/index.d.ts +2 -0
- package/dist/server/rate-limit/index.d.ts.map +1 -0
- package/dist/server/rate-limit/index.js +2 -0
- package/dist/server/rate-limit/index.js.map +1 -0
- package/dist/server/rate-limit/utils.d.ts +27 -0
- package/dist/server/rate-limit/utils.d.ts.map +1 -0
- package/dist/server/rate-limit/utils.js +126 -0
- package/dist/server/rate-limit/utils.js.map +1 -0
- package/dist/server/routing.d.ts +39 -0
- package/dist/server/routing.d.ts.map +1 -0
- package/dist/server/routing.js +70 -0
- package/dist/server/routing.js.map +1 -0
- package/dist/server/token-limit/index.d.ts +2 -0
- package/dist/server/token-limit/index.d.ts.map +1 -0
- package/dist/server/token-limit/index.js +2 -0
- package/dist/server/token-limit/index.js.map +1 -0
- package/dist/server/token-limit/utils.d.ts +44 -0
- package/dist/server/token-limit/utils.d.ts.map +1 -0
- package/dist/server/token-limit/utils.js +260 -0
- package/dist/server/token-limit/utils.js.map +1 -0
- package/dist/server/tools.d.ts +33 -0
- package/dist/server/tools.d.ts.map +1 -0
- package/dist/server/tools.js +160 -0
- package/dist/server/tools.js.map +1 -0
- package/dist/server/transports.d.ts +25 -0
- package/dist/server/transports.d.ts.map +1 -0
- package/dist/server/transports.js +2 -0
- package/dist/server/transports.js.map +1 -0
- package/dist/shared/tools.d.ts +24 -0
- package/dist/shared/tools.d.ts.map +1 -0
- package/dist/shared/tools.js +86 -0
- package/dist/shared/tools.js.map +1 -0
- package/dist/spec/decorators.d.ts +41 -0
- package/dist/spec/decorators.d.ts.map +1 -0
- package/dist/spec/decorators.js +93 -0
- package/dist/spec/decorators.js.map +1 -0
- package/dist/spec/index.d.ts +3 -0
- package/dist/spec/index.d.ts.map +1 -0
- package/dist/spec/index.js +3 -0
- package/dist/spec/index.js.map +1 -0
- package/dist/spec/metadata.d.ts +5 -0
- package/dist/spec/metadata.d.ts.map +1 -0
- package/dist/spec/metadata.js +37 -0
- package/dist/spec/metadata.js.map +1 -0
- package/dist/types/client-route.d.ts +7 -0
- package/dist/types/client-route.d.ts.map +1 -0
- package/dist/types/client-route.js +2 -0
- package/dist/types/client-route.js.map +1 -0
- package/dist/types/client.d.ts +81 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/client.js +14 -0
- package/dist/types/client.js.map +1 -0
- package/dist/types/endpoints.d.ts +76 -0
- package/dist/types/endpoints.d.ts.map +1 -0
- package/dist/types/endpoints.js +2 -0
- package/dist/types/endpoints.js.map +1 -0
- package/dist/types/errors.d.ts +86 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +153 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/http.d.ts +80 -0
- package/dist/types/http.d.ts.map +1 -0
- package/dist/types/http.js +61 -0
- package/dist/types/http.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/openapi.d.ts +220 -0
- package/dist/types/openapi.d.ts.map +1 -0
- package/dist/types/openapi.js +11 -0
- package/dist/types/openapi.js.map +1 -0
- package/dist/types/opts.d.ts +46 -0
- package/dist/types/opts.d.ts.map +1 -0
- package/dist/types/opts.js +2 -0
- package/dist/types/opts.js.map +1 -0
- package/dist/types/plugins.d.ts +93 -0
- package/dist/types/plugins.d.ts.map +1 -0
- package/dist/types/plugins.js +5 -0
- package/dist/types/plugins.js.map +1 -0
- package/dist/types/tools.d.ts +52 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +2 -0
- package/dist/types/tools.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
import { ClientError, ServerError } from '../types/errors.js';
|
|
2
|
+
import { ResponseFormats, RequestFormats } from '../types/client.js';
|
|
3
|
+
import { HttpMethods, ProxyProps, ParamLocations, ContentTypes } from '../types/http.js';
|
|
4
|
+
import { DEFAULT_RPC_PATH, } from '../rpc.js';
|
|
5
|
+
import { extractToolsFromOpenAPI } from './tools.js';
|
|
6
|
+
import { createHttpTransportPlugin } from './http-transport-plugin.js';
|
|
7
|
+
import { createRpcTransportPlugin } from './rpc-transport-plugin.js';
|
|
8
|
+
import { createFileTransportPlugin } from './file-transport-plugin.js';
|
|
9
|
+
import { executeClientTransportPlugin } from './transport-plugin.js';
|
|
10
|
+
import { createClientSideServerMQTTWebRTCTransportPlugin } from '../client-side-server/mqtt-webrtc.js';
|
|
11
|
+
class OpenAPIClientImpl {
|
|
12
|
+
openAPISpec;
|
|
13
|
+
baseUrl;
|
|
14
|
+
headers;
|
|
15
|
+
fetchInit;
|
|
16
|
+
timeoutMs;
|
|
17
|
+
retryConfig;
|
|
18
|
+
transportMode;
|
|
19
|
+
rpcPath;
|
|
20
|
+
callsPath;
|
|
21
|
+
hooks;
|
|
22
|
+
transportPlugins;
|
|
23
|
+
openapi;
|
|
24
|
+
cachedTools;
|
|
25
|
+
rpcSocket;
|
|
26
|
+
rpcSocketPromise;
|
|
27
|
+
nodeFileRuntimePromise;
|
|
28
|
+
rpcPending = new Map();
|
|
29
|
+
rpcCounter = 0;
|
|
30
|
+
/** Typed accessor for spec paths, handling the optional field. */
|
|
31
|
+
get _paths() {
|
|
32
|
+
return (this.openapi.paths ?? {});
|
|
33
|
+
}
|
|
34
|
+
_opIndex;
|
|
35
|
+
_segTree;
|
|
36
|
+
_rootProxy;
|
|
37
|
+
constructor(openAPISpec, options) {
|
|
38
|
+
this.openAPISpec = openAPISpec;
|
|
39
|
+
this.baseUrl = options.baseUrl;
|
|
40
|
+
this.headers = this.normalizeHeaders(options.headers);
|
|
41
|
+
this.fetchInit = options.fetchInit;
|
|
42
|
+
this.timeoutMs = options.timeoutMs ?? 30000;
|
|
43
|
+
this.retryConfig = {
|
|
44
|
+
maxAttempts: options.retry?.maxAttempts ?? 3,
|
|
45
|
+
delayMs: options.retry?.delayMs ?? 1000,
|
|
46
|
+
backoffMultiplier: options.retry?.backoffMultiplier ?? 2,
|
|
47
|
+
};
|
|
48
|
+
this.transportMode = this.resolveTransportMode(options.transport);
|
|
49
|
+
this.rpcPath = options.rpcPath ?? DEFAULT_RPC_PATH;
|
|
50
|
+
this.callsPath = options.callsPath ?? '/platCall';
|
|
51
|
+
this.hooks = options.hooks;
|
|
52
|
+
const defaultTransportPlugins = this.baseUrl.startsWith('css://')
|
|
53
|
+
? [createClientSideServerMQTTWebRTCTransportPlugin()]
|
|
54
|
+
: [];
|
|
55
|
+
this.transportPlugins = [
|
|
56
|
+
...(options.transportPlugins ?? []),
|
|
57
|
+
...defaultTransportPlugins,
|
|
58
|
+
createHttpTransportPlugin(this.createBuiltInTransportRuntime()),
|
|
59
|
+
createRpcTransportPlugin(this.createBuiltInTransportRuntime()),
|
|
60
|
+
createFileTransportPlugin(this.createBuiltInTransportRuntime()),
|
|
61
|
+
];
|
|
62
|
+
this.openapi = openAPISpec;
|
|
63
|
+
// Return a Proxy that enables dot-notation route access:
|
|
64
|
+
// client.listProducts({ limit: 10 })
|
|
65
|
+
// client.products.listProducts({ limit: 10 })
|
|
66
|
+
// client.listProducts.get({ limit: 10 })
|
|
67
|
+
// client.routes → ['listProducts', 'products', ...]
|
|
68
|
+
// client.children → { listProducts: proxy, products: proxy, ... }
|
|
69
|
+
const rootProxy = new Proxy(this, {
|
|
70
|
+
get: (target, prop, receiver) => {
|
|
71
|
+
if (typeof prop === 'symbol' || Reflect.has(target, prop)) {
|
|
72
|
+
return Reflect.get(target, prop, receiver);
|
|
73
|
+
}
|
|
74
|
+
const p = String(prop);
|
|
75
|
+
if (p === ProxyProps.ROOT)
|
|
76
|
+
return rootProxy;
|
|
77
|
+
if (p === ProxyProps.CLIENT)
|
|
78
|
+
return target;
|
|
79
|
+
if (p === ProxyProps.ROUTES)
|
|
80
|
+
return target._rootRouteNames();
|
|
81
|
+
if (p === ProxyProps.CHILDREN)
|
|
82
|
+
return target._rootChildren();
|
|
83
|
+
return target._resolveRoute(p);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
this._rootProxy = rootProxy;
|
|
87
|
+
return rootProxy;
|
|
88
|
+
}
|
|
89
|
+
normalizeHeaders(headers) {
|
|
90
|
+
if (!headers)
|
|
91
|
+
return {};
|
|
92
|
+
if (headers instanceof Headers) {
|
|
93
|
+
const result = {};
|
|
94
|
+
headers.forEach((value, key) => {
|
|
95
|
+
result[key] = value;
|
|
96
|
+
});
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(headers)) {
|
|
100
|
+
return Object.fromEntries(headers);
|
|
101
|
+
}
|
|
102
|
+
return headers;
|
|
103
|
+
}
|
|
104
|
+
/** Coerce all header values to strings for the fetch API. */
|
|
105
|
+
stringifyHeaders(headers) {
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
108
|
+
if (v !== undefined)
|
|
109
|
+
out[k] = String(v);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
async buildHeaders() {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
resolveTransportMode(mode) {
|
|
117
|
+
if (mode && mode !== 'auto')
|
|
118
|
+
return mode;
|
|
119
|
+
if (/^file:\/\//i.test(this.baseUrl))
|
|
120
|
+
return 'file';
|
|
121
|
+
if (/^css:\/\//i.test(this.baseUrl))
|
|
122
|
+
return 'css';
|
|
123
|
+
return /^wss?:\/\//i.test(this.baseUrl) ? 'rpc' : 'http';
|
|
124
|
+
}
|
|
125
|
+
resolveTransportPlugin() {
|
|
126
|
+
return this.transportPlugins.find((plugin) => plugin.canHandle({ baseUrl: this.baseUrl, transportMode: this.transportMode }));
|
|
127
|
+
}
|
|
128
|
+
createBuiltInTransportRuntime() {
|
|
129
|
+
return {
|
|
130
|
+
baseUrl: this.baseUrl,
|
|
131
|
+
callsPath: this.callsPath,
|
|
132
|
+
delay: (ms) => this.delay(ms),
|
|
133
|
+
nextRequestId: (prefix) => `${prefix}-${this.nextRpcId()}`,
|
|
134
|
+
stringifyHeaders: (headers) => this.stringifyHeaders(headers),
|
|
135
|
+
parseJson: (text) => this.tryParseJson(text),
|
|
136
|
+
resolveRpcUrl: () => this.resolveRpcUrl(),
|
|
137
|
+
ensureRpcSocket: () => this.ensureRpcSocket(),
|
|
138
|
+
sendRpcCancel: (id) => this.sendRpcCancel(id),
|
|
139
|
+
createDeferredHandle: (id, options) => this.createDeferredHandle(id, options),
|
|
140
|
+
fetchHttp: async (request) => {
|
|
141
|
+
return await Promise.race([
|
|
142
|
+
fetch(request.url, {
|
|
143
|
+
...(request.fetchInit ?? this.fetchInit),
|
|
144
|
+
method: request.method,
|
|
145
|
+
headers: this.stringifyHeaders(request.headers),
|
|
146
|
+
body: request.body ?? undefined,
|
|
147
|
+
signal: request.signal,
|
|
148
|
+
}),
|
|
149
|
+
this.createTimeoutPromise(request.timeoutMs),
|
|
150
|
+
]);
|
|
151
|
+
},
|
|
152
|
+
parseResponse: (response, format) => this._parseResponse(response, format),
|
|
153
|
+
detectResponseFormat: (response, specContentTypes) => this._detectResponseFormat(response, specContentTypes),
|
|
154
|
+
fetchInit: this.fetchInit,
|
|
155
|
+
timeoutMs: this.timeoutMs,
|
|
156
|
+
fileQueue: {
|
|
157
|
+
resolvePaths: () => this.resolveFileQueuePaths(),
|
|
158
|
+
pollIntervalMs: 100,
|
|
159
|
+
mkdir: async (path) => {
|
|
160
|
+
const runtime = await this.getNodeFileRuntime();
|
|
161
|
+
await runtime.mkdir(path);
|
|
162
|
+
},
|
|
163
|
+
write: async (path, content) => {
|
|
164
|
+
const runtime = await this.getNodeFileRuntime();
|
|
165
|
+
await runtime.writeFile(path, content);
|
|
166
|
+
},
|
|
167
|
+
read: async (path) => {
|
|
168
|
+
const runtime = await this.getNodeFileRuntime();
|
|
169
|
+
return await runtime.readFile(path);
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get tool definitions for AI integrations (Claude, OpenAI, etc)
|
|
176
|
+
* Tools are extracted from the OpenAPI spec and cached
|
|
177
|
+
*/
|
|
178
|
+
get tools() {
|
|
179
|
+
if (!this.cachedTools) {
|
|
180
|
+
this.cachedTools = extractToolsFromOpenAPI(this.openAPISpec);
|
|
181
|
+
}
|
|
182
|
+
return this.cachedTools;
|
|
183
|
+
}
|
|
184
|
+
async get(path, params, options) {
|
|
185
|
+
return this.call(HttpMethods.GET, path, params, options);
|
|
186
|
+
}
|
|
187
|
+
async post(path, params, options) {
|
|
188
|
+
return this.call(HttpMethods.POST, path, params, options);
|
|
189
|
+
}
|
|
190
|
+
async put(path, params, options) {
|
|
191
|
+
return this.call(HttpMethods.PUT, path, params, options);
|
|
192
|
+
}
|
|
193
|
+
async patch(path, params, options) {
|
|
194
|
+
return this.call(HttpMethods.PATCH, path, params, options);
|
|
195
|
+
}
|
|
196
|
+
async delete(path, params, options) {
|
|
197
|
+
return this.call(HttpMethods.DELETE, path, params, options);
|
|
198
|
+
}
|
|
199
|
+
async call(method, path, params, options) {
|
|
200
|
+
// Find the operation in the OpenAPI spec by method and path
|
|
201
|
+
const operation = this.findOperationByPath(method, path);
|
|
202
|
+
if (!operation) {
|
|
203
|
+
throw new Error(`Operation ${method} ${path} not found in OpenAPI spec`);
|
|
204
|
+
}
|
|
205
|
+
const { pathParams, queryParams, headerParams, requestBody, requestContentTypes, responseContentTypes } = operation;
|
|
206
|
+
// Cast params to a plain record for runtime property access
|
|
207
|
+
const p = params;
|
|
208
|
+
// Replace path parameters
|
|
209
|
+
let url = path;
|
|
210
|
+
pathParams.forEach((param) => {
|
|
211
|
+
const value = p[param];
|
|
212
|
+
if (!value) {
|
|
213
|
+
throw new Error(`Missing required path parameter: ${param}`);
|
|
214
|
+
}
|
|
215
|
+
url = url.replace(`{${param}}`, String(value));
|
|
216
|
+
});
|
|
217
|
+
// Add query parameters
|
|
218
|
+
const queryString = new URLSearchParams();
|
|
219
|
+
queryParams.forEach((param) => {
|
|
220
|
+
if (param in p && p[param] !== undefined) {
|
|
221
|
+
queryString.append(param, String(p[param]));
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
if (queryString.toString()) {
|
|
225
|
+
url += `?${queryString.toString()}`;
|
|
226
|
+
}
|
|
227
|
+
const fullUrl = `${this.baseUrl}${url}`;
|
|
228
|
+
// Prepare request
|
|
229
|
+
let headers = {
|
|
230
|
+
'Content-Type': ContentTypes.JSON,
|
|
231
|
+
...this.headers,
|
|
232
|
+
...(await this.buildHeaders()),
|
|
233
|
+
};
|
|
234
|
+
if (options?.headers) {
|
|
235
|
+
const optionHeaders = this.normalizeHeaders(options.headers);
|
|
236
|
+
Object.assign(headers, optionHeaders);
|
|
237
|
+
}
|
|
238
|
+
// Set header params from the params object (declared via `in: 'header'` in the spec)
|
|
239
|
+
for (const name of headerParams) {
|
|
240
|
+
if (name in p && p[name] !== undefined) {
|
|
241
|
+
headers[name] = String(p[name]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Call buildHeaders hook
|
|
245
|
+
const requestContext = { method, path: url, url: fullUrl, headers };
|
|
246
|
+
if (this.hooks?.buildHeaders) {
|
|
247
|
+
headers = await this.hooks.buildHeaders(headers, requestContext);
|
|
248
|
+
}
|
|
249
|
+
if (options?.execution === 'deferred') {
|
|
250
|
+
headers['X-PLAT-Execution'] = 'deferred';
|
|
251
|
+
}
|
|
252
|
+
// Resolve timeout and retry config with per-call overrides
|
|
253
|
+
const timeoutMs = options?.timeoutMs ?? this.timeoutMs;
|
|
254
|
+
// Handle retry disabled case
|
|
255
|
+
const retryDisabled = options?.retry === false;
|
|
256
|
+
const optionsRetry = options?.retry && typeof options.retry === 'object' ? options.retry : null;
|
|
257
|
+
const retryConfig = {
|
|
258
|
+
maxAttempts: retryDisabled
|
|
259
|
+
? 1
|
|
260
|
+
: optionsRetry?.maxAttempts ?? this.retryConfig.maxAttempts,
|
|
261
|
+
delayMs: retryDisabled
|
|
262
|
+
? 0
|
|
263
|
+
: optionsRetry?.retryDelayMs && typeof optionsRetry.retryDelayMs === 'number'
|
|
264
|
+
? optionsRetry.retryDelayMs
|
|
265
|
+
: this.retryConfig.delayMs,
|
|
266
|
+
};
|
|
267
|
+
// Serialize request body based on format
|
|
268
|
+
let body;
|
|
269
|
+
if (options?.body) {
|
|
270
|
+
// Raw body passthrough
|
|
271
|
+
body = options.body;
|
|
272
|
+
}
|
|
273
|
+
else if (requestBody && p._body) {
|
|
274
|
+
const payload = p._body;
|
|
275
|
+
const reqFormat = options?.requestFormat
|
|
276
|
+
?? this._detectRequestFormat(payload, requestContentTypes);
|
|
277
|
+
if (reqFormat === RequestFormats.FORM) {
|
|
278
|
+
const form = new URLSearchParams();
|
|
279
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
280
|
+
if (v !== undefined)
|
|
281
|
+
form.append(k, String(v));
|
|
282
|
+
}
|
|
283
|
+
body = form.toString();
|
|
284
|
+
headers['Content-Type'] = ContentTypes.FORM;
|
|
285
|
+
}
|
|
286
|
+
else if (reqFormat === RequestFormats.MULTIPART) {
|
|
287
|
+
const form = new FormData();
|
|
288
|
+
for (const [k, v] of Object.entries(payload)) {
|
|
289
|
+
if (v instanceof Blob || typeof v === 'string') {
|
|
290
|
+
form.append(k, v);
|
|
291
|
+
}
|
|
292
|
+
else if (v !== undefined) {
|
|
293
|
+
form.append(k, String(v));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
body = form;
|
|
297
|
+
// Let the runtime set Content-Type with boundary
|
|
298
|
+
delete headers['Content-Type'];
|
|
299
|
+
}
|
|
300
|
+
else if (reqFormat === RequestFormats.RAW) {
|
|
301
|
+
body = payload;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
body = JSON.stringify(payload);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const customTransport = this.resolveTransportPlugin();
|
|
308
|
+
if (customTransport) {
|
|
309
|
+
return await executeClientTransportPlugin(customTransport, {
|
|
310
|
+
id: customTransport.name === 'file' ? `file-${this.nextRpcId()}` : this.nextRpcId(),
|
|
311
|
+
baseUrl: this.baseUrl,
|
|
312
|
+
transportMode: this.transportMode,
|
|
313
|
+
method,
|
|
314
|
+
path: url,
|
|
315
|
+
url: fullUrl,
|
|
316
|
+
operationId: operation.operationId,
|
|
317
|
+
params,
|
|
318
|
+
headers,
|
|
319
|
+
body,
|
|
320
|
+
timeoutMs,
|
|
321
|
+
execution: options?.execution,
|
|
322
|
+
requestContext,
|
|
323
|
+
signal: options?.signal,
|
|
324
|
+
options,
|
|
325
|
+
onEvent: options?.onRpcEvent,
|
|
326
|
+
responseFormat: options?.responseFormat,
|
|
327
|
+
responseContentTypes,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
// Make request with retries
|
|
331
|
+
let lastError = null;
|
|
332
|
+
for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt++) {
|
|
333
|
+
try {
|
|
334
|
+
// Call pre-request hook
|
|
335
|
+
if (this.hooks?.onPreRequest) {
|
|
336
|
+
await this.hooks.onPreRequest(requestContext);
|
|
337
|
+
}
|
|
338
|
+
const response = await Promise.race([
|
|
339
|
+
fetch(fullUrl, {
|
|
340
|
+
...this.fetchInit,
|
|
341
|
+
method,
|
|
342
|
+
headers: this.stringifyHeaders(headers),
|
|
343
|
+
body: body ?? undefined,
|
|
344
|
+
signal: options?.signal,
|
|
345
|
+
}),
|
|
346
|
+
this.createTimeoutPromise(timeoutMs),
|
|
347
|
+
]);
|
|
348
|
+
// Call post-request hook
|
|
349
|
+
if (this.hooks?.onPostRequest) {
|
|
350
|
+
await this.hooks.onPostRequest(requestContext, response);
|
|
351
|
+
}
|
|
352
|
+
if (response.ok) {
|
|
353
|
+
if (options?.execution === 'deferred' && response.status === 202) {
|
|
354
|
+
const payload = await response.json();
|
|
355
|
+
return this.createDeferredHandle(payload.id, options);
|
|
356
|
+
}
|
|
357
|
+
return this._parseResponse(response, options?.responseFormat
|
|
358
|
+
?? this._detectResponseFormat(response, responseContentTypes));
|
|
359
|
+
}
|
|
360
|
+
const bodyText = await response.text();
|
|
361
|
+
const bodyJson = this.tryParseJson(bodyText);
|
|
362
|
+
const error = response.status >= 400 && response.status < 500
|
|
363
|
+
? new ClientError({
|
|
364
|
+
url: fullUrl,
|
|
365
|
+
method,
|
|
366
|
+
status: response.status,
|
|
367
|
+
statusText: response.statusText,
|
|
368
|
+
headers: response.headers,
|
|
369
|
+
bodyText,
|
|
370
|
+
bodyJson,
|
|
371
|
+
})
|
|
372
|
+
: new ServerError({
|
|
373
|
+
url: fullUrl,
|
|
374
|
+
method,
|
|
375
|
+
status: response.status,
|
|
376
|
+
statusText: response.statusText,
|
|
377
|
+
headers: response.headers,
|
|
378
|
+
bodyText,
|
|
379
|
+
bodyJson,
|
|
380
|
+
});
|
|
381
|
+
const retryContext = {
|
|
382
|
+
attempt,
|
|
383
|
+
maxAttempts: retryConfig.maxAttempts,
|
|
384
|
+
status: response.status,
|
|
385
|
+
error,
|
|
386
|
+
};
|
|
387
|
+
// Call onError hook
|
|
388
|
+
if (this.hooks?.onError) {
|
|
389
|
+
await this.hooks.onError(error, retryContext);
|
|
390
|
+
}
|
|
391
|
+
// Determine if we should retry using hook or default logic
|
|
392
|
+
const shouldRetryRequest = this.hooks?.shouldRetry?.(response.status, retryContext) ??
|
|
393
|
+
response.status >= 500;
|
|
394
|
+
if (!shouldRetryRequest || attempt === retryConfig.maxAttempts) {
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
lastError = error;
|
|
398
|
+
// Delay before retry with exponential backoff
|
|
399
|
+
await this.delay(retryConfig.delayMs * Math.pow(2, attempt - 1));
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
if (error instanceof ClientError || error instanceof ServerError) {
|
|
403
|
+
const retryContext = {
|
|
404
|
+
attempt,
|
|
405
|
+
maxAttempts: retryConfig.maxAttempts,
|
|
406
|
+
error,
|
|
407
|
+
};
|
|
408
|
+
// Call onError hook for client/server errors
|
|
409
|
+
if (this.hooks?.onError) {
|
|
410
|
+
await this.hooks.onError(error, retryContext);
|
|
411
|
+
}
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
lastError = error;
|
|
415
|
+
if (attempt < retryConfig.maxAttempts) {
|
|
416
|
+
await this.delay(retryConfig.delayMs * Math.pow(2, attempt - 1));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
throw lastError || new Error('Request failed after retries');
|
|
421
|
+
}
|
|
422
|
+
findOperationByPath(method, path) {
|
|
423
|
+
const pathItem = this._paths[path];
|
|
424
|
+
if (!pathItem) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
const op = pathItem[method.toLowerCase()];
|
|
428
|
+
if (!op) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const pathParams = this.extractPathParams(path);
|
|
432
|
+
const queryParams = this.extractQueryParams(op);
|
|
433
|
+
const headerParams = this.extractHeaderParams(op);
|
|
434
|
+
const reqBody = op.requestBody;
|
|
435
|
+
const requestBody = !!reqBody;
|
|
436
|
+
const reqBodyContent = reqBody?.content;
|
|
437
|
+
const requestContentTypes = reqBodyContent ? Object.keys(reqBodyContent) : [];
|
|
438
|
+
// Collect response content types from the success response (200 or 201)
|
|
439
|
+
const responses = op.responses;
|
|
440
|
+
const successResponse = responses?.['200'] ?? responses?.['201'];
|
|
441
|
+
const respContent = successResponse?.content;
|
|
442
|
+
const responseContentTypes = respContent ? Object.keys(respContent) : [];
|
|
443
|
+
return {
|
|
444
|
+
operationId: typeof op.operationId === 'string' ? op.operationId : undefined,
|
|
445
|
+
pathParams,
|
|
446
|
+
queryParams,
|
|
447
|
+
headerParams,
|
|
448
|
+
requestBody,
|
|
449
|
+
requestContentTypes,
|
|
450
|
+
responseContentTypes,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
nextRpcId() {
|
|
454
|
+
this.rpcCounter += 1;
|
|
455
|
+
return `rpc-${this.rpcCounter}`;
|
|
456
|
+
}
|
|
457
|
+
resolveRpcUrl() {
|
|
458
|
+
const url = new URL(this.baseUrl);
|
|
459
|
+
if (!/^wss?:$/i.test(url.protocol)) {
|
|
460
|
+
throw new Error(`RPC transport requires ws:// or wss:// baseUrl, got ${this.baseUrl}`);
|
|
461
|
+
}
|
|
462
|
+
if (!url.pathname || url.pathname === '/' || url.pathname === '') {
|
|
463
|
+
url.pathname = this.rpcPath;
|
|
464
|
+
}
|
|
465
|
+
return url.toString();
|
|
466
|
+
}
|
|
467
|
+
async sendRpcCancel(id) {
|
|
468
|
+
try {
|
|
469
|
+
const socket = await this.ensureRpcSocket();
|
|
470
|
+
socket.send(JSON.stringify({
|
|
471
|
+
jsonrpc: '2.0',
|
|
472
|
+
id,
|
|
473
|
+
method: 'CANCEL',
|
|
474
|
+
path: '',
|
|
475
|
+
cancel: true,
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// Best effort; cancellation should still reject locally even if the socket is unavailable.
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async ensureRpcSocket() {
|
|
483
|
+
if (this.rpcSocket && this.rpcSocket.readyState === WebSocket.OPEN) {
|
|
484
|
+
return this.rpcSocket;
|
|
485
|
+
}
|
|
486
|
+
if (this.rpcSocketPromise)
|
|
487
|
+
return this.rpcSocketPromise;
|
|
488
|
+
this.rpcSocketPromise = new Promise((resolve, reject) => {
|
|
489
|
+
const socket = new WebSocket(this.resolveRpcUrl());
|
|
490
|
+
socket.addEventListener('open', () => {
|
|
491
|
+
this.rpcSocket = socket;
|
|
492
|
+
resolve(socket);
|
|
493
|
+
}, { once: true });
|
|
494
|
+
socket.addEventListener('message', (event) => {
|
|
495
|
+
const payload = this.tryParseJson(String(event.data));
|
|
496
|
+
if (!payload || typeof payload !== 'object' || !('id' in payload))
|
|
497
|
+
return;
|
|
498
|
+
const pending = this.rpcPending.get(String(payload.id));
|
|
499
|
+
if (!pending)
|
|
500
|
+
return;
|
|
501
|
+
if ('event' in payload && typeof payload.event === 'string') {
|
|
502
|
+
void pending.onEvent?.({
|
|
503
|
+
id: String(payload.id),
|
|
504
|
+
event: payload.event,
|
|
505
|
+
data: payload.data,
|
|
506
|
+
});
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this.rpcPending.delete(String(payload.id));
|
|
510
|
+
pending.resolve(payload);
|
|
511
|
+
});
|
|
512
|
+
socket.addEventListener('close', () => {
|
|
513
|
+
this.rpcSocket = undefined;
|
|
514
|
+
this.rpcSocketPromise = undefined;
|
|
515
|
+
for (const [id, pending] of Array.from(this.rpcPending.entries())) {
|
|
516
|
+
this.rpcPending.delete(id);
|
|
517
|
+
pending.reject(new Error('RPC socket closed'));
|
|
518
|
+
}
|
|
519
|
+
}, { once: true });
|
|
520
|
+
socket.addEventListener('error', () => {
|
|
521
|
+
reject(new Error(`Failed to connect to RPC socket at ${this.resolveRpcUrl()}`));
|
|
522
|
+
}, { once: true });
|
|
523
|
+
});
|
|
524
|
+
return this.rpcSocketPromise;
|
|
525
|
+
}
|
|
526
|
+
async getNodeFileRuntime() {
|
|
527
|
+
if (!this.nodeFileRuntimePromise) {
|
|
528
|
+
const dynamicImport = new Function('m', 'return import(m)');
|
|
529
|
+
this.nodeFileRuntimePromise = Promise.all([
|
|
530
|
+
dynamicImport('node:fs/promises'),
|
|
531
|
+
dynamicImport('node:path'),
|
|
532
|
+
]).then(([fs, path]) => ({
|
|
533
|
+
mkdir: async (targetPath) => { await fs.mkdir(targetPath, { recursive: true }); },
|
|
534
|
+
writeFile: async (targetPath, content) => { await fs.writeFile(targetPath, content); },
|
|
535
|
+
readFile: async (targetPath) => await fs.readFile(targetPath, 'utf8'),
|
|
536
|
+
join: (...parts) => path.join(...parts),
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
return this.nodeFileRuntimePromise;
|
|
540
|
+
}
|
|
541
|
+
async resolveFileQueuePaths() {
|
|
542
|
+
const url = new URL(this.baseUrl);
|
|
543
|
+
if (url.protocol !== 'file:') {
|
|
544
|
+
throw new Error(`File transport requires file:// baseUrl, got ${this.baseUrl}`);
|
|
545
|
+
}
|
|
546
|
+
const root = decodeURIComponent(url.pathname);
|
|
547
|
+
const { join } = await this.getNodeFileRuntime();
|
|
548
|
+
return {
|
|
549
|
+
inbox: join(root, 'inbox'),
|
|
550
|
+
outbox: join(root, 'outbox'),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
createDeferredHandle(id, options) {
|
|
554
|
+
return {
|
|
555
|
+
id,
|
|
556
|
+
status: async () => {
|
|
557
|
+
return await this.fetchDeferredJson(`${this.callsPath}Status?id=${encodeURIComponent(id)}`, options);
|
|
558
|
+
},
|
|
559
|
+
events: async (args) => {
|
|
560
|
+
const search = new URLSearchParams();
|
|
561
|
+
search.set('id', id);
|
|
562
|
+
if (args?.since)
|
|
563
|
+
search.set('since', String(args.since));
|
|
564
|
+
if (args?.event)
|
|
565
|
+
search.set('event', args.event);
|
|
566
|
+
const payload = await this.fetchDeferredJson(`${this.callsPath}Events?${search.toString()}`, options);
|
|
567
|
+
return payload.events;
|
|
568
|
+
},
|
|
569
|
+
logs: async (since) => {
|
|
570
|
+
const search = new URLSearchParams();
|
|
571
|
+
search.set('id', id);
|
|
572
|
+
if (since)
|
|
573
|
+
search.set('since', String(since));
|
|
574
|
+
search.set('event', 'log');
|
|
575
|
+
const payload = await this.fetchDeferredJson(`${this.callsPath}Events?${search.toString()}`, options);
|
|
576
|
+
return payload.events;
|
|
577
|
+
},
|
|
578
|
+
result: async () => {
|
|
579
|
+
const payload = await this.fetchDeferredJson(`${this.callsPath}Result?id=${encodeURIComponent(id)}`, options);
|
|
580
|
+
if (payload.status === 'completed') {
|
|
581
|
+
return payload.result;
|
|
582
|
+
}
|
|
583
|
+
if (payload.status === 'failed') {
|
|
584
|
+
throw new Error(payload.error?.message || 'Deferred call failed');
|
|
585
|
+
}
|
|
586
|
+
if (payload.status === 'cancelled') {
|
|
587
|
+
throw new DOMException('Deferred call was cancelled', 'AbortError');
|
|
588
|
+
}
|
|
589
|
+
throw new Error(`Deferred call ${id} is still ${payload.status}`);
|
|
590
|
+
},
|
|
591
|
+
wait: async (args) => {
|
|
592
|
+
const pollIntervalMs = args?.pollIntervalMs ?? options?.pollIntervalMs ?? 1000;
|
|
593
|
+
while (true) {
|
|
594
|
+
if (args?.signal?.aborted) {
|
|
595
|
+
throw new DOMException('Deferred wait aborted', 'AbortError');
|
|
596
|
+
}
|
|
597
|
+
const snapshot = await this.fetchDeferredJson(`${this.callsPath}Result?id=${encodeURIComponent(id)}`, options);
|
|
598
|
+
if (snapshot.status === 'completed') {
|
|
599
|
+
return snapshot.result;
|
|
600
|
+
}
|
|
601
|
+
if (snapshot.status === 'failed') {
|
|
602
|
+
throw new Error(snapshot.error?.message || 'Deferred call failed');
|
|
603
|
+
}
|
|
604
|
+
if (snapshot.status === 'cancelled') {
|
|
605
|
+
throw new DOMException('Deferred call was cancelled', 'AbortError');
|
|
606
|
+
}
|
|
607
|
+
await this.delay(pollIntervalMs);
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
cancel: async () => {
|
|
611
|
+
const payload = await this.fetchDeferredJson(`${this.callsPath}Cancel`, options, 'POST', { id });
|
|
612
|
+
return payload.cancelled;
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
async fetchDeferredJson(path, options, method = 'GET', bodyPayload) {
|
|
617
|
+
const headers = {
|
|
618
|
+
...this.stringifyHeaders({
|
|
619
|
+
...this.headers,
|
|
620
|
+
...(await this.buildHeaders()),
|
|
621
|
+
}),
|
|
622
|
+
...(options?.headers ? this.stringifyHeaders(this.normalizeHeaders(options.headers)) : {}),
|
|
623
|
+
};
|
|
624
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
625
|
+
...this.fetchInit,
|
|
626
|
+
method,
|
|
627
|
+
headers: bodyPayload === undefined ? headers : { ...headers, 'Content-Type': 'application/json' },
|
|
628
|
+
body: bodyPayload === undefined ? undefined : JSON.stringify(bodyPayload),
|
|
629
|
+
signal: options?.signal,
|
|
630
|
+
});
|
|
631
|
+
const payload = await response.json();
|
|
632
|
+
if (!response.ok) {
|
|
633
|
+
throw new Error((payload && typeof payload === 'object' && 'error' in payload && typeof payload.error === 'string')
|
|
634
|
+
? payload.error
|
|
635
|
+
: `Deferred call request failed with ${response.status}`);
|
|
636
|
+
}
|
|
637
|
+
return payload;
|
|
638
|
+
}
|
|
639
|
+
extractPathParams(path) {
|
|
640
|
+
const matches = path.match(/{(\w+)}/g) || [];
|
|
641
|
+
return matches.map((m) => m.slice(1, -1));
|
|
642
|
+
}
|
|
643
|
+
extractQueryParams(operation) {
|
|
644
|
+
const params = (operation.parameters ?? []);
|
|
645
|
+
return params
|
|
646
|
+
.filter((p) => p.in === ParamLocations.QUERY)
|
|
647
|
+
.map((p) => p.name);
|
|
648
|
+
}
|
|
649
|
+
extractHeaderParams(operation) {
|
|
650
|
+
const params = (operation.parameters ?? []);
|
|
651
|
+
return params
|
|
652
|
+
.filter((p) => p.in === ParamLocations.HEADER)
|
|
653
|
+
.map((p) => p.name);
|
|
654
|
+
}
|
|
655
|
+
async _parseResponse(response, format) {
|
|
656
|
+
switch (format) {
|
|
657
|
+
case ResponseFormats.RAW: return response;
|
|
658
|
+
case ResponseFormats.TEXT: return await response.text();
|
|
659
|
+
case ResponseFormats.BLOB: return await response.blob();
|
|
660
|
+
case ResponseFormats.ARRAY_BUFFER: return await response.arrayBuffer();
|
|
661
|
+
default: return await response.json();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
createTimeoutPromise(timeoutMs) {
|
|
665
|
+
return new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeoutMs));
|
|
666
|
+
}
|
|
667
|
+
delay(ms) {
|
|
668
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
669
|
+
}
|
|
670
|
+
tryParseJson(text) {
|
|
671
|
+
try {
|
|
672
|
+
return JSON.parse(text);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
return text;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Detect request format from body values and OpenAPI spec content types.
|
|
680
|
+
* Priority: Blob/File in values → spec declares multipart → spec declares form → json
|
|
681
|
+
*/
|
|
682
|
+
_detectRequestFormat(payload, specContentTypes) {
|
|
683
|
+
// If any value is a Blob or File, it must be multipart
|
|
684
|
+
for (const v of Object.values(payload)) {
|
|
685
|
+
if (v instanceof Blob)
|
|
686
|
+
return RequestFormats.MULTIPART;
|
|
687
|
+
}
|
|
688
|
+
// Check what the OpenAPI spec declares
|
|
689
|
+
for (const ct of specContentTypes) {
|
|
690
|
+
if (ct.includes('multipart'))
|
|
691
|
+
return RequestFormats.MULTIPART;
|
|
692
|
+
if (ct.includes('x-www-form-urlencoded'))
|
|
693
|
+
return RequestFormats.FORM;
|
|
694
|
+
}
|
|
695
|
+
return RequestFormats.JSON;
|
|
696
|
+
}
|
|
697
|
+
/** Detect the best response format from the OpenAPI spec and response headers. */
|
|
698
|
+
_detectResponseFormat(response, specContentTypes) {
|
|
699
|
+
// Check spec-declared content types first (known at build time)
|
|
700
|
+
if (specContentTypes.length > 0) {
|
|
701
|
+
const detected = this._contentTypeToFormat(specContentTypes[0]);
|
|
702
|
+
if (detected)
|
|
703
|
+
return detected;
|
|
704
|
+
}
|
|
705
|
+
// Fall back to the actual response Content-Type header
|
|
706
|
+
const ct = response.headers.get('content-type');
|
|
707
|
+
if (ct) {
|
|
708
|
+
const detected = this._contentTypeToFormat(ct);
|
|
709
|
+
if (detected)
|
|
710
|
+
return detected;
|
|
711
|
+
}
|
|
712
|
+
return ResponseFormats.JSON;
|
|
713
|
+
}
|
|
714
|
+
/** Map a MIME content type string to a ResponseFormat. */
|
|
715
|
+
_contentTypeToFormat(ct) {
|
|
716
|
+
if (ct.includes('json'))
|
|
717
|
+
return ResponseFormats.JSON;
|
|
718
|
+
if (ct.startsWith('text/'))
|
|
719
|
+
return ResponseFormats.TEXT;
|
|
720
|
+
if (ct.includes('image/') || ct.includes('audio/') || ct.includes('video/') || ct.includes('octet-stream')) {
|
|
721
|
+
return ResponseFormats.BLOB;
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
// ── Route proxy ────────────────────────────────────────────
|
|
726
|
+
_ensureIndexes() {
|
|
727
|
+
if (this._opIndex)
|
|
728
|
+
return;
|
|
729
|
+
this._opIndex = new Map();
|
|
730
|
+
this._segTree = { methods: new Map(), children: new Map() };
|
|
731
|
+
for (const [urlPath, pathItem] of Object.entries(this._paths)) {
|
|
732
|
+
const segments = urlPath.split('/').filter(Boolean);
|
|
733
|
+
// Walk/create segment tree nodes
|
|
734
|
+
let node = this._segTree;
|
|
735
|
+
for (const seg of segments) {
|
|
736
|
+
if (!node.children.has(seg)) {
|
|
737
|
+
node.children.set(seg, { methods: new Map(), children: new Map() });
|
|
738
|
+
}
|
|
739
|
+
node = node.children.get(seg);
|
|
740
|
+
}
|
|
741
|
+
// Register each HTTP method at this path
|
|
742
|
+
for (const [httpMethod, op] of Object.entries(pathItem)) {
|
|
743
|
+
const method = httpMethod.toUpperCase();
|
|
744
|
+
const operation = op;
|
|
745
|
+
const routeOp = {
|
|
746
|
+
method,
|
|
747
|
+
path: urlPath,
|
|
748
|
+
operation,
|
|
749
|
+
};
|
|
750
|
+
node.methods.set(method, routeOp);
|
|
751
|
+
const opId = operation.operationId;
|
|
752
|
+
if (opId) {
|
|
753
|
+
if (!this._opIndex.has(opId))
|
|
754
|
+
this._opIndex.set(opId, []);
|
|
755
|
+
this._opIndex.get(opId).push(routeOp);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/** All unique route names accessible from the root (operationIds + top-level segments). */
|
|
761
|
+
_rootRouteNames() {
|
|
762
|
+
this._ensureIndexes();
|
|
763
|
+
const names = new Set();
|
|
764
|
+
this._opIndex.forEach((_, name) => names.add(name));
|
|
765
|
+
this._segTree.children.forEach((_, name) => names.add(name));
|
|
766
|
+
return Array.from(names);
|
|
767
|
+
}
|
|
768
|
+
/** Object mapping each root route name → its route proxy. */
|
|
769
|
+
_rootChildren() {
|
|
770
|
+
const out = {};
|
|
771
|
+
for (const name of this._rootRouteNames()) {
|
|
772
|
+
out[name] = this._resolveRoute(name);
|
|
773
|
+
}
|
|
774
|
+
return out;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Resolve a property name to a callable route proxy.
|
|
778
|
+
* Checks operationId first, then path segment children.
|
|
779
|
+
*/
|
|
780
|
+
_resolveRoute(name) {
|
|
781
|
+
this._ensureIndexes();
|
|
782
|
+
const ops = this._opIndex.get(name) ?? [];
|
|
783
|
+
const segChild = this._segTree.children.get(name);
|
|
784
|
+
if (ops.length === 0 && !segChild)
|
|
785
|
+
return undefined;
|
|
786
|
+
return this._createCallableNode(ops, segChild ?? null);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Create a callable Proxy node for a route.
|
|
790
|
+
*
|
|
791
|
+
* The node is a function that can be called directly (if exactly one
|
|
792
|
+
* HTTP method is registered) and also supports:
|
|
793
|
+
* .get(params) .post(params) etc. — explicit HTTP method
|
|
794
|
+
* .child — nested path segment navigation
|
|
795
|
+
*/
|
|
796
|
+
_createCallableNode(ops, segNode) {
|
|
797
|
+
// Merge methods from operationId matches and segment node
|
|
798
|
+
const methods = new Map();
|
|
799
|
+
for (const op of ops)
|
|
800
|
+
methods.set(op.method, op);
|
|
801
|
+
if (segNode) {
|
|
802
|
+
segNode.methods.forEach((routeOp, method) => {
|
|
803
|
+
if (!methods.has(method))
|
|
804
|
+
methods.set(method, routeOp);
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
// Build the .spec object — raw OpenAPI operation(s) with path/method added
|
|
808
|
+
const specObj = this._buildSpec(methods);
|
|
809
|
+
const client = this;
|
|
810
|
+
// Direct-call function: works when exactly one HTTP method
|
|
811
|
+
const fn = function (params, options) {
|
|
812
|
+
if (methods.size === 0) {
|
|
813
|
+
throw new Error('No HTTP methods at this route — use a child segment');
|
|
814
|
+
}
|
|
815
|
+
if (methods.size > 1) {
|
|
816
|
+
const available = Array.from(methods.keys()).join(', ');
|
|
817
|
+
throw new Error(`Ambiguous: multiple methods (${available}). Use .get(), .post(), etc.`);
|
|
818
|
+
}
|
|
819
|
+
const [, routeOp] = Array.from(methods.entries())[0];
|
|
820
|
+
return client._callRoute(routeOp, params ?? {}, options);
|
|
821
|
+
};
|
|
822
|
+
return new Proxy(fn, {
|
|
823
|
+
get: (_target, prop) => {
|
|
824
|
+
if (typeof prop === 'symbol')
|
|
825
|
+
return Reflect.get(fn, prop);
|
|
826
|
+
const p = String(prop);
|
|
827
|
+
if (p === ProxyProps.THEN)
|
|
828
|
+
return undefined;
|
|
829
|
+
if (p === ProxyProps.ROOT)
|
|
830
|
+
return client._rootProxy;
|
|
831
|
+
if (p === ProxyProps.CLIENT)
|
|
832
|
+
return client;
|
|
833
|
+
if (p === ProxyProps.SPEC)
|
|
834
|
+
return specObj;
|
|
835
|
+
if (p === ProxyProps.ROUTES) {
|
|
836
|
+
return segNode ? Array.from(segNode.children.keys()) : [];
|
|
837
|
+
}
|
|
838
|
+
if (p === ProxyProps.CHILDREN) {
|
|
839
|
+
const out = {};
|
|
840
|
+
if (segNode) {
|
|
841
|
+
segNode.children.forEach((child, name) => {
|
|
842
|
+
out[name] = client._createCallableNode([], child);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
return out;
|
|
846
|
+
}
|
|
847
|
+
// HTTP method accessor: .get(), .post(), .put(), .patch(), .delete()
|
|
848
|
+
const upper = p.toUpperCase();
|
|
849
|
+
if (upper in HttpMethods) {
|
|
850
|
+
const httpMethod = upper;
|
|
851
|
+
const routeOp = methods.get(httpMethod);
|
|
852
|
+
if (routeOp) {
|
|
853
|
+
return (params, options) => client._callRoute(routeOp, params ?? {}, options);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
// Child segment navigation
|
|
857
|
+
if (segNode) {
|
|
858
|
+
const child = segNode.children.get(p);
|
|
859
|
+
if (child)
|
|
860
|
+
return client._createCallableNode([], child);
|
|
861
|
+
}
|
|
862
|
+
// Fall through to native function properties (bind, call, apply, etc.)
|
|
863
|
+
return Reflect.get(fn, prop);
|
|
864
|
+
},
|
|
865
|
+
apply: (_target, _thisArg, args) => {
|
|
866
|
+
return fn(args[0], args[1]);
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Build a .spec object from the route's registered methods.
|
|
872
|
+
*
|
|
873
|
+
* Uses standard OpenAPI field names:
|
|
874
|
+
* operationId, summary, description, parameters,
|
|
875
|
+
* requestBody, responses
|
|
876
|
+
*
|
|
877
|
+
* Single-method routes return the operation directly.
|
|
878
|
+
* Multi-method routes return { GET: {...}, POST: {...} }.
|
|
879
|
+
*/
|
|
880
|
+
_callRoute(routeOp, params, options) {
|
|
881
|
+
return this.call(routeOp.method, routeOp.path, params, options);
|
|
882
|
+
}
|
|
883
|
+
_buildSpec(methods) {
|
|
884
|
+
const buildOne = (httpMethod, routeOp) => {
|
|
885
|
+
const op = routeOp.operation;
|
|
886
|
+
return {
|
|
887
|
+
method: httpMethod,
|
|
888
|
+
path: routeOp.path,
|
|
889
|
+
operationId: op.operationId,
|
|
890
|
+
summary: op.summary,
|
|
891
|
+
description: op.description,
|
|
892
|
+
tags: op.tags,
|
|
893
|
+
parameters: op.parameters,
|
|
894
|
+
requestBody: op.requestBody,
|
|
895
|
+
responses: op.responses,
|
|
896
|
+
};
|
|
897
|
+
};
|
|
898
|
+
if (methods.size === 1) {
|
|
899
|
+
const [method, routeOp] = Array.from(methods.entries())[0];
|
|
900
|
+
return buildOne(method, routeOp);
|
|
901
|
+
}
|
|
902
|
+
const spec = {};
|
|
903
|
+
methods.forEach((routeOp, method) => {
|
|
904
|
+
spec[method] = buildOne(method, routeOp);
|
|
905
|
+
});
|
|
906
|
+
return spec;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
export const OpenAPIClient = OpenAPIClientImpl;
|
|
910
|
+
//# sourceMappingURL=openapi-client.js.map
|