@fudanda/client 0.0.0-stage → 2.20261024.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +47 -3
- package/dist/goa-stream.d.ts +12 -0
- package/dist/goa-stream.d.ts.map +1 -0
- package/dist/goa-stream.js +117 -0
- package/dist/goa-stream.js.map +1 -0
- package/dist/goa.d.ts +81 -0
- package/dist/goa.d.ts.map +1 -0
- package/dist/goa.js +391 -0
- package/dist/goa.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -4
- package/src/goa-stream.ts +80 -0
- package/src/goa.ts +343 -0
- package/src/index.ts +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 iwangbowen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @fudanda/client
|
|
2
|
+
|
|
3
|
+
原生 Goa TypeScript 客户端,运行时只依赖 `@fudanda/shared`。支持浏览器 Cookie/CSRF、脚本 API Key、严格请求/响应校验、multipart、上传进度、下载和 SSE,不依赖 React、UI 或查询缓存。
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { GoaClient } from '@fudanda/client';
|
|
7
|
+
import { authMe } from '@fudanda/shared/api/identity';
|
|
8
|
+
import { positionsList } from '@fudanda/shared/api/positions';
|
|
9
|
+
|
|
10
|
+
const client = new GoaClient();
|
|
11
|
+
const session = await client.call(authMe);
|
|
12
|
+
client.setCSRFToken(session.csrfToken);
|
|
13
|
+
const page = await client.call(positionsList, {
|
|
14
|
+
query: { page: 1, pageSize: 10, keyword: '工程' },
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Goa DSL 生成路径、方法、参数、Result、错误及协议描述。`call` 直接返回原生 Result,204 返回 `undefined`;`callWithResponse` 另外返回状态和响应头。旧 `Client`、`callRaw` 和响应包装接口已退出公共包。
|
|
19
|
+
|
|
20
|
+
## 会话与错误
|
|
21
|
+
|
|
22
|
+
浏览器默认同源并发送 HttpOnly Cookie;刷新页面后读取 `authMe` 恢复 CSRF。登录成功后调用 `setCSRFToken`,退出或身份改变时调用 `clearSession`。`subscribeUnauthorized` 和 `onUnauthorized` 供宿主更新登录状态。受保护请求的 401 清除当前会话,503 不清除;迟到的旧会话结果以 `AbortError` 结束,不能覆盖新会话。
|
|
23
|
+
|
|
24
|
+
`GoaError` 提供 `status`、`reason`、`errorId`、`requestId`、`temporary`、`timeout` 和 `fault`;网络与取消错误保留原语义。请求的 `signal` 支持取消。PATCH 省略字段保持原值,清空使用设计允许的 `clearFields`,集合 PUT 允许 `[]`。客户端不自动重放写入。
|
|
25
|
+
|
|
26
|
+
`baseURL` 只能是 HTTP origin;Node 原生 fetch 不维护 Cookie,Cookie 模式须注入维护 Cookie 的 `transport`。`new GoaClient({ baseURL, apiKey })` 使用受限 Bearer Key,默认不携带 Cookie,不保存 Key 到浏览器存储。服务端继续校验实时账号权限、Key scope 和数据范围。
|
|
27
|
+
|
|
28
|
+
## 文件与 SSE
|
|
29
|
+
|
|
30
|
+
`upload(operation, input, { signal, onProgress })` 接收生成的 multipart 输入;普通上传使用 fetch,需要进度时使用 XHR。二者使用最新 CSRF,浏览器自行生成 multipart boundary,宿主不能覆盖认证头。Blob、Blob 数组或 FormData 仍按生成操作校验。
|
|
31
|
+
|
|
32
|
+
`download(operation, input, options)` 返回未缓冲的 `Response`,调用者须消费或取消 body。宿主负责保存文件、Toast 和界面状态。
|
|
33
|
+
|
|
34
|
+
`stream(operation, input, { onEvent, onConnection, onError, signal })` 返回 `{ close, done }`。事件以生成 schema 校验,支持 UTF-8 分片、多行 data、Last-Event-ID 和有界重连;权限或会话失效终止观察。卸载时调用 `close()` 并等待 `done`,不会取消服务端业务运行。缓存重查由宿主负责。
|
|
35
|
+
|
|
36
|
+
独立业务模块直接将自身 Goa 生成的描述符传给同一个客户端。TypeScript 生成器公开为 `create-arcbase/goa-contracts`,不能自行维护第二份请求 schema。
|
|
37
|
+
|
|
38
|
+
## 验证
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm run check:types -w @fudanda/client
|
|
42
|
+
npm run test:client
|
|
43
|
+
npm run lint -w @fudanda/client
|
|
44
|
+
npm run check:boundaries
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
两套管理台通过 `@fudanda/react-core` 共享会话、Query、缓存失效和文件适配。完整协议与权限说明见 [集成扩展](../../docs/guide/go-integrations.md)。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SSEFrame {
|
|
2
|
+
event: string;
|
|
3
|
+
data?: string;
|
|
4
|
+
id?: string;
|
|
5
|
+
retry?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class SSEProtocolError extends Error {
|
|
8
|
+
}
|
|
9
|
+
/** Incremental SSE framing. Bound each frame before retaining its text. */
|
|
10
|
+
export declare function readSSE(body: ReadableStream<Uint8Array>, signal: AbortSignal): AsyncGenerator<SSEFrame>;
|
|
11
|
+
export declare function waitForReconnect(milliseconds: number, signal: AbortSignal): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=goa-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"goa-stream.d.ts","sourceRoot":"","sources":["../src/goa-stream.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;AAEvF,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAE9C,2EAA2E;AAC3E,wBAAuB,OAAO,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,cAAc,CAAC,QAAQ,CAAC,CAiE9G;AAED,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAOzF"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
export class SSEProtocolError extends Error {
|
|
2
|
+
}
|
|
3
|
+
/** Incremental SSE framing. Bound each frame before retaining its text. */
|
|
4
|
+
export async function* readSSE(body, signal) {
|
|
5
|
+
const reader = body.getReader();
|
|
6
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
let line = '', event = '', data = [], id, retry;
|
|
9
|
+
let frameBytes = 0, afterCR = false, finished = false, cancelled = false;
|
|
10
|
+
const cancel = async () => {
|
|
11
|
+
if (finished || cancelled)
|
|
12
|
+
return;
|
|
13
|
+
cancelled = true;
|
|
14
|
+
try {
|
|
15
|
+
await reader.cancel();
|
|
16
|
+
}
|
|
17
|
+
catch { /* The original read failure is reported by the caller. */ }
|
|
18
|
+
};
|
|
19
|
+
const abort = () => { void cancel(); };
|
|
20
|
+
const append = (fragment) => {
|
|
21
|
+
frameBytes += encoder.encode(fragment).byteLength;
|
|
22
|
+
if (frameBytes > 512 * 1024)
|
|
23
|
+
throw new SSEProtocolError('SSE frame exceeds 512 KiB');
|
|
24
|
+
line += fragment;
|
|
25
|
+
};
|
|
26
|
+
const consume = () => {
|
|
27
|
+
const current = line;
|
|
28
|
+
line = '';
|
|
29
|
+
if (!current) {
|
|
30
|
+
const frame = data.length || id !== undefined || retry !== undefined ? { event: event || 'message', data: data.length ? data.join('\n') : undefined, id, retry } : undefined;
|
|
31
|
+
event = '';
|
|
32
|
+
data = [];
|
|
33
|
+
id = undefined;
|
|
34
|
+
retry = undefined;
|
|
35
|
+
frameBytes = 0;
|
|
36
|
+
return frame;
|
|
37
|
+
}
|
|
38
|
+
if (current.startsWith(':')) {
|
|
39
|
+
frameBytes -= encoder.encode(current).byteLength;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const colon = current.indexOf(':');
|
|
43
|
+
const field = colon === -1 ? current : current.slice(0, colon);
|
|
44
|
+
let value = colon === -1 ? '' : current.slice(colon + 1);
|
|
45
|
+
if (value.startsWith(' '))
|
|
46
|
+
value = value.slice(1);
|
|
47
|
+
if (field === 'event')
|
|
48
|
+
event = value;
|
|
49
|
+
else if (field === 'data')
|
|
50
|
+
data.push(value);
|
|
51
|
+
else if (field === 'id' && !value.includes('\0'))
|
|
52
|
+
id = value;
|
|
53
|
+
else if (field === 'retry' && /^\d+$/.test(value) && Number.isSafeInteger(Number(value)))
|
|
54
|
+
retry = Number(value);
|
|
55
|
+
};
|
|
56
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
57
|
+
try {
|
|
58
|
+
signal.throwIfAborted();
|
|
59
|
+
while (true) {
|
|
60
|
+
const part = await reader.read();
|
|
61
|
+
signal.throwIfAborted();
|
|
62
|
+
if (part.done) {
|
|
63
|
+
finished = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
let text;
|
|
67
|
+
try {
|
|
68
|
+
text = decoder.decode(part.value, { stream: true });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new SSEProtocolError('SSE contains invalid UTF-8');
|
|
72
|
+
}
|
|
73
|
+
let start = 0;
|
|
74
|
+
for (let index = 0; index < text.length; index++) {
|
|
75
|
+
const char = text[index];
|
|
76
|
+
if (afterCR) {
|
|
77
|
+
afterCR = false;
|
|
78
|
+
if (char === '\n') {
|
|
79
|
+
start = index + 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (char !== '\r' && char !== '\n')
|
|
84
|
+
continue;
|
|
85
|
+
append(text.slice(start, index));
|
|
86
|
+
const frame = consume();
|
|
87
|
+
if (frame)
|
|
88
|
+
yield frame;
|
|
89
|
+
afterCR = char === '\r';
|
|
90
|
+
start = index + 1;
|
|
91
|
+
}
|
|
92
|
+
append(text.slice(start));
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
decoder.decode();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new SSEProtocolError('SSE contains incomplete UTF-8');
|
|
99
|
+
}
|
|
100
|
+
// The SSE protocol discards a final event without its empty-line delimiter.
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
signal.removeEventListener('abort', abort);
|
|
104
|
+
await cancel();
|
|
105
|
+
reader.releaseLock();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function waitForReconnect(milliseconds, signal) {
|
|
109
|
+
return new Promise(resolve => {
|
|
110
|
+
const finish = () => { clearTimeout(timer); signal.removeEventListener('abort', finish); resolve(); };
|
|
111
|
+
const timer = setTimeout(finish, milliseconds);
|
|
112
|
+
signal.addEventListener('abort', finish, { once: true });
|
|
113
|
+
if (signal.aborted)
|
|
114
|
+
finish();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=goa-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"goa-stream.js","sourceRoot":"","sources":["../src/goa-stream.ts"],"names":[],"mappings":"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"}
|
package/dist/goa.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { GoaInputOf, GoaOperation, GoaOutputOf } from '@fudanda/shared/core';
|
|
2
|
+
export interface GoaClientOptions {
|
|
3
|
+
baseURL?: string;
|
|
4
|
+
credentials?: RequestCredentials;
|
|
5
|
+
transport?: typeof fetch;
|
|
6
|
+
xhrFactory?: () => XMLHttpRequest;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
csrfToken?: string;
|
|
9
|
+
onUnauthorized?: () => void;
|
|
10
|
+
}
|
|
11
|
+
export interface GoaRequestOptions {
|
|
12
|
+
signal?: AbortSignal | null;
|
|
13
|
+
headers?: HeadersInit;
|
|
14
|
+
}
|
|
15
|
+
export interface GoaUploadOptions extends GoaRequestOptions {
|
|
16
|
+
onProgress?: (percent: number) => void;
|
|
17
|
+
}
|
|
18
|
+
export type GoaConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'closed';
|
|
19
|
+
export interface GoaStreamOptions<Data> extends GoaRequestOptions {
|
|
20
|
+
onEvent: (event: Data) => void;
|
|
21
|
+
onConnection?: (state: GoaConnectionState) => void;
|
|
22
|
+
onError?: (error: Error) => void;
|
|
23
|
+
reconnectDelay?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface GoaSubscription {
|
|
26
|
+
close(): void;
|
|
27
|
+
done: Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare class GoaError extends Error {
|
|
30
|
+
readonly status: number;
|
|
31
|
+
readonly reason: string;
|
|
32
|
+
readonly name = "GoaError";
|
|
33
|
+
errorId?: string;
|
|
34
|
+
requestId?: string;
|
|
35
|
+
temporary: boolean;
|
|
36
|
+
timeout: boolean;
|
|
37
|
+
fault: boolean;
|
|
38
|
+
details?: unknown;
|
|
39
|
+
constructor(status: number, reason: string, message: string);
|
|
40
|
+
}
|
|
41
|
+
type Arguments<Operation extends GoaOperation> = undefined extends GoaInputOf<Operation> ? [input?: GoaInputOf<Operation>, options?: GoaRequestOptions] : [input: GoaInputOf<Operation>, options?: GoaRequestOptions];
|
|
42
|
+
type UploadArguments<Operation extends GoaOperation> = [input: GoaInputOf<Operation>, options?: GoaUploadOptions];
|
|
43
|
+
export interface GoaResponse<Data> {
|
|
44
|
+
data: Data;
|
|
45
|
+
status: number;
|
|
46
|
+
headers: Headers;
|
|
47
|
+
}
|
|
48
|
+
/** Native Goa HTTP client. Generated schemas own request and response shapes. */
|
|
49
|
+
export declare class GoaClient {
|
|
50
|
+
private readonly options;
|
|
51
|
+
private csrfToken;
|
|
52
|
+
private sessionRevision;
|
|
53
|
+
private readonly responseSessions;
|
|
54
|
+
private readonly unauthorizedListeners;
|
|
55
|
+
constructor(options?: GoaClientOptions);
|
|
56
|
+
setCSRFToken(token: string): void;
|
|
57
|
+
clearSession(): void;
|
|
58
|
+
sessionHeaders(): Record<string, string>;
|
|
59
|
+
subscribeUnauthorized(listener: () => void): () => void;
|
|
60
|
+
expireSession(): void;
|
|
61
|
+
call<Operation extends GoaOperation>(operation: Operation, ...args: Arguments<Operation>): Promise<GoaOutputOf<Operation>>;
|
|
62
|
+
callWithResponse<Operation extends GoaOperation>(operation: Operation, ...args: Arguments<Operation>): Promise<GoaResponse<GoaOutputOf<Operation>>>;
|
|
63
|
+
upload<Operation extends GoaOperation & {
|
|
64
|
+
readonly kind: 'multipart';
|
|
65
|
+
}>(operation: Operation, ...args: UploadArguments<Operation>): Promise<GoaOutputOf<Operation>>;
|
|
66
|
+
/** Observe a generated GET stream; closing it never cancels domain execution. */
|
|
67
|
+
stream<Operation extends GoaOperation & {
|
|
68
|
+
readonly kind: 'sse';
|
|
69
|
+
}>(operation: Operation, input: GoaInputOf<Operation>, options: GoaStreamOptions<GoaOutputOf<Operation>>): GoaSubscription;
|
|
70
|
+
/** The caller consumes or cancels the returned body; the transport never buffers files. */
|
|
71
|
+
download<Operation extends GoaOperation & {
|
|
72
|
+
readonly kind: 'file' | 'csv';
|
|
73
|
+
}>(operation: Operation, ...args: Arguments<Operation>): Promise<Response>;
|
|
74
|
+
private execute;
|
|
75
|
+
private requireCurrentSession;
|
|
76
|
+
private fetch;
|
|
77
|
+
private sendUpload;
|
|
78
|
+
private error;
|
|
79
|
+
}
|
|
80
|
+
export {};
|
|
81
|
+
//# sourceMappingURL=goa.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"goa.d.ts","sourceRoot":"","sources":["../src/goa.ts"],"names":[],"mappings":"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"}
|
package/dist/goa.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { readSSE, SSEProtocolError, waitForReconnect } from './goa-stream.js';
|
|
2
|
+
export class GoaError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
reason;
|
|
5
|
+
name = 'GoaError';
|
|
6
|
+
errorId;
|
|
7
|
+
requestId;
|
|
8
|
+
temporary = false;
|
|
9
|
+
timeout = false;
|
|
10
|
+
fault = false;
|
|
11
|
+
details;
|
|
12
|
+
constructor(status, reason, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.reason = reason;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function pathFor(operation, input) {
|
|
19
|
+
if (!operation.path.startsWith('/api/v1/') || /[?#\\\s]/.test(operation.path) || operation.path.split('/').some(part => part === '.' || part === '..'))
|
|
20
|
+
throw new Error('Invalid generated API path');
|
|
21
|
+
const path = operation.path.replace(/\{(\*?\w+)\}/g, (_match, name) => {
|
|
22
|
+
const wildcard = name.startsWith('*');
|
|
23
|
+
const value = input.params?.[wildcard ? name.slice(1) : name];
|
|
24
|
+
if (value === undefined || value === null)
|
|
25
|
+
throw new Error(`Missing path parameter: ${name}`);
|
|
26
|
+
if (!wildcard)
|
|
27
|
+
return encodeURIComponent(String(value));
|
|
28
|
+
const parts = String(value).split('/');
|
|
29
|
+
if (parts.some(part => part === '.' || part === '..'))
|
|
30
|
+
throw new Error('Invalid wildcard path');
|
|
31
|
+
return parts.map(encodeURIComponent).join('/');
|
|
32
|
+
});
|
|
33
|
+
const query = new URLSearchParams();
|
|
34
|
+
for (const [key, value] of Object.entries(input.query ?? {})) {
|
|
35
|
+
if (value === undefined || value === null)
|
|
36
|
+
continue;
|
|
37
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
38
|
+
query.append(key, String(item));
|
|
39
|
+
}
|
|
40
|
+
return path + (query.size ? '?' + query.toString() : '');
|
|
41
|
+
}
|
|
42
|
+
/** Native Goa HTTP client. Generated schemas own request and response shapes. */
|
|
43
|
+
export class GoaClient {
|
|
44
|
+
options;
|
|
45
|
+
csrfToken;
|
|
46
|
+
sessionRevision = 0;
|
|
47
|
+
responseSessions = new WeakMap();
|
|
48
|
+
unauthorizedListeners = new Set();
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.options = options;
|
|
51
|
+
const baseURL = options.baseURL ?? '';
|
|
52
|
+
if (baseURL && (!/^https?:\/\//.test(baseURL) || new URL(baseURL).origin !== baseURL.replace(/\/$/, '')))
|
|
53
|
+
throw new Error('baseURL must be an HTTP origin');
|
|
54
|
+
if (options.apiKey && !/^arc_[a-f0-9]{64}$/.test(options.apiKey))
|
|
55
|
+
throw new Error('Invalid ArcBase API Key');
|
|
56
|
+
this.csrfToken = options.csrfToken ?? '';
|
|
57
|
+
}
|
|
58
|
+
setCSRFToken(token) { if (token !== this.csrfToken) {
|
|
59
|
+
this.csrfToken = token;
|
|
60
|
+
this.sessionRevision++;
|
|
61
|
+
} }
|
|
62
|
+
clearSession() { this.csrfToken = ''; this.sessionRevision++; }
|
|
63
|
+
sessionHeaders() { return this.csrfToken ? { 'X-CSRF-Token': this.csrfToken } : {}; }
|
|
64
|
+
subscribeUnauthorized(listener) {
|
|
65
|
+
this.unauthorizedListeners.add(listener);
|
|
66
|
+
return () => { this.unauthorizedListeners.delete(listener); };
|
|
67
|
+
}
|
|
68
|
+
expireSession() {
|
|
69
|
+
this.clearSession();
|
|
70
|
+
for (const listener of this.unauthorizedListeners)
|
|
71
|
+
listener();
|
|
72
|
+
this.options.onUnauthorized?.();
|
|
73
|
+
}
|
|
74
|
+
call(operation, ...args) {
|
|
75
|
+
return this.execute(operation, args[0], args[1]).then(response => response.data);
|
|
76
|
+
}
|
|
77
|
+
callWithResponse(operation, ...args) {
|
|
78
|
+
return this.execute(operation, args[0], args[1]);
|
|
79
|
+
}
|
|
80
|
+
upload(operation, ...args) {
|
|
81
|
+
return this.execute(operation, args[0], args[1], 'multipart').then(response => response.data);
|
|
82
|
+
}
|
|
83
|
+
/** Observe a generated GET stream; closing it never cancels domain execution. */
|
|
84
|
+
stream(operation, input, options) {
|
|
85
|
+
if (operation.kind !== 'sse' || operation.method !== 'GET')
|
|
86
|
+
throw new Error('Expected a GET SSE operation');
|
|
87
|
+
const values = operation.input.parse(input);
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const streamRevision = this.sessionRevision;
|
|
90
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
91
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
92
|
+
if (options.signal?.aborted)
|
|
93
|
+
abort();
|
|
94
|
+
const close = () => controller.abort();
|
|
95
|
+
const done = (async () => {
|
|
96
|
+
const initialHeaders = new Headers();
|
|
97
|
+
for (const [key, value] of Object.entries(values.headers ?? {}))
|
|
98
|
+
if (value !== undefined && value !== null)
|
|
99
|
+
initialHeaders.set(key, String(value));
|
|
100
|
+
let cursor = initialHeaders.get('Last-Event-ID');
|
|
101
|
+
let delay = Math.min(15000, Math.max(250, options.reconnectDelay ?? 1000));
|
|
102
|
+
let attempts = 0;
|
|
103
|
+
try {
|
|
104
|
+
while (!controller.signal.aborted) {
|
|
105
|
+
if (operation.access.kind !== 'public' && this.sessionRevision !== streamRevision)
|
|
106
|
+
return;
|
|
107
|
+
options.onConnection?.(attempts ? 'reconnecting' : 'connecting');
|
|
108
|
+
try {
|
|
109
|
+
const resumed = { ...values, headers: { ...values.headers } };
|
|
110
|
+
if (operation.sse?.requestIdField && cursor !== null)
|
|
111
|
+
resumed.headers['Last-Event-ID'] = cursor;
|
|
112
|
+
if (!operation.input.safeParse(resumed).success)
|
|
113
|
+
throw new GoaError(400, 'invalid_response', '服务端续传标识不符合契约');
|
|
114
|
+
const response = await this.fetch(operation, resumed, { ...options, signal: controller.signal });
|
|
115
|
+
if (!response.ok)
|
|
116
|
+
throw await this.error(operation, response);
|
|
117
|
+
await this.requireCurrentSession(operation, response);
|
|
118
|
+
const schema = operation.responses[response.status];
|
|
119
|
+
if (!schema || !response.body || response.headers.get('Content-Type')?.split(';')[0].trim().toLowerCase() !== 'text/event-stream') {
|
|
120
|
+
await response.body?.cancel();
|
|
121
|
+
throw new GoaError(response.status, 'invalid_response', '服务端流式响应不符合契约');
|
|
122
|
+
}
|
|
123
|
+
options.onConnection?.('connected');
|
|
124
|
+
attempts = 0;
|
|
125
|
+
for await (const frame of readSSE(response.body, controller.signal)) {
|
|
126
|
+
if (operation.access.kind !== 'public' && this.sessionRevision !== streamRevision)
|
|
127
|
+
return;
|
|
128
|
+
if (frame.id !== undefined)
|
|
129
|
+
cursor = frame.id;
|
|
130
|
+
if (frame.retry !== undefined)
|
|
131
|
+
delay = Math.min(15000, Math.max(250, frame.retry));
|
|
132
|
+
if (frame.data === undefined)
|
|
133
|
+
continue;
|
|
134
|
+
if (frame.event === 'session-expired') {
|
|
135
|
+
if (this.responseSessions.get(response) === this.sessionRevision)
|
|
136
|
+
this.expireSession();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (frame.event === 'denied')
|
|
140
|
+
throw new GoaError(403, 'stream_denied', '订阅权限已失效');
|
|
141
|
+
let raw;
|
|
142
|
+
try {
|
|
143
|
+
raw = JSON.parse(frame.data);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
throw new GoaError(response.status, 'invalid_response', '服务端流式事件格式错误');
|
|
147
|
+
}
|
|
148
|
+
const wire = operation.sse;
|
|
149
|
+
if (wire?.dataField)
|
|
150
|
+
raw = { [wire.dataField]: raw };
|
|
151
|
+
if (wire?.idField || wire?.eventField || wire?.retryField) {
|
|
152
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
153
|
+
throw new GoaError(response.status, 'invalid_response', '服务端流式事件格式错误');
|
|
154
|
+
const mapped = { ...raw };
|
|
155
|
+
if (wire.idField)
|
|
156
|
+
mapped[wire.idField] = cursor ?? '';
|
|
157
|
+
if (wire.eventField)
|
|
158
|
+
mapped[wire.eventField] = frame.event;
|
|
159
|
+
if (wire.retryField && frame.retry !== undefined)
|
|
160
|
+
mapped[wire.retryField] = frame.retry;
|
|
161
|
+
raw = mapped;
|
|
162
|
+
}
|
|
163
|
+
const parsed = schema.safeParse(raw);
|
|
164
|
+
if (!parsed.success)
|
|
165
|
+
throw new GoaError(response.status, 'invalid_response', '服务端流式事件不符合契约');
|
|
166
|
+
options.onEvent(parsed.data);
|
|
167
|
+
if (frame.event === 'done')
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (controller.signal.aborted || error instanceof DOMException && error.name === 'AbortError')
|
|
173
|
+
return;
|
|
174
|
+
options.onError?.(error instanceof Error ? error : new Error('流式连接中断'));
|
|
175
|
+
if (error instanceof SSEProtocolError || error instanceof GoaError && (error.reason === 'invalid_response' || error.status < 500 && error.status !== 429))
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
attempts++;
|
|
179
|
+
await waitForReconnect(Math.min(15000, delay * 2 ** Math.min(attempts - 1, 4)), controller.signal);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
options.signal?.removeEventListener('abort', abort);
|
|
184
|
+
options.onConnection?.('closed');
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
return { close, done };
|
|
188
|
+
}
|
|
189
|
+
/** The caller consumes or cancels the returned body; the transport never buffers files. */
|
|
190
|
+
async download(operation, ...args) {
|
|
191
|
+
if (operation.kind !== 'file' && operation.kind !== 'csv')
|
|
192
|
+
throw new Error(`Operation ${operation.id} is not a download`);
|
|
193
|
+
const values = operation.input.parse(args[0]);
|
|
194
|
+
const response = await this.fetch(operation, values, args[1] ?? {});
|
|
195
|
+
if (!response.ok)
|
|
196
|
+
throw await this.error(operation, response);
|
|
197
|
+
await this.requireCurrentSession(operation, response);
|
|
198
|
+
const declared = operation.mediaTypes?.[response.status] ?? [];
|
|
199
|
+
const contentType = response.headers.get('Content-Type')?.split(';')[0].trim().toLowerCase();
|
|
200
|
+
const matchesMediaType = declared.some(mediaType => {
|
|
201
|
+
const expected = mediaType.split(';')[0].trim().toLowerCase();
|
|
202
|
+
if (!contentType || !/^[^\s/;]+\/[^\s/;]+$/.test(contentType))
|
|
203
|
+
return false;
|
|
204
|
+
return expected === contentType || expected === '*/*' || expected.endsWith('/*') && contentType.startsWith(expected.slice(0, -1));
|
|
205
|
+
});
|
|
206
|
+
if (!operation.responses[response.status] || !matchesMediaType) {
|
|
207
|
+
await response.body?.cancel();
|
|
208
|
+
throw new GoaError(response.status, 'invalid_response', '文件响应不符合契约');
|
|
209
|
+
}
|
|
210
|
+
return response;
|
|
211
|
+
}
|
|
212
|
+
async execute(operation, input, options = {}, channel = 'json') {
|
|
213
|
+
if (operation.kind !== channel)
|
|
214
|
+
throw new Error(`Operation ${operation.id} requires the ${operation.kind} channel`);
|
|
215
|
+
const values = operation.input.parse(input);
|
|
216
|
+
const response = await this.fetch(operation, values, options);
|
|
217
|
+
if (!response.ok)
|
|
218
|
+
throw await this.error(operation, response);
|
|
219
|
+
await this.requireCurrentSession(operation, response);
|
|
220
|
+
const schema = operation.responses[response.status];
|
|
221
|
+
if (!schema)
|
|
222
|
+
throw new GoaError(response.status, 'unexpected_status', '服务端返回了未声明的状态');
|
|
223
|
+
let raw;
|
|
224
|
+
if (response.status !== 204) {
|
|
225
|
+
try {
|
|
226
|
+
raw = await response.json();
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
throw new GoaError(response.status, 'invalid_response', '服务端响应格式错误');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const parsed = schema.safeParse(raw);
|
|
233
|
+
if (!parsed.success)
|
|
234
|
+
throw new GoaError(response.status, 'invalid_response', '服务端响应不符合契约');
|
|
235
|
+
await this.requireCurrentSession(operation, response);
|
|
236
|
+
return { data: parsed.data, status: response.status, headers: response.headers };
|
|
237
|
+
}
|
|
238
|
+
async requireCurrentSession(operation, response) {
|
|
239
|
+
if (operation.access.kind === 'public' || this.options.apiKey || this.responseSessions.get(response) === this.sessionRevision)
|
|
240
|
+
return;
|
|
241
|
+
if (!response.bodyUsed)
|
|
242
|
+
await response.body?.cancel();
|
|
243
|
+
throw new DOMException('会话已变更,已丢弃旧请求结果', 'AbortError');
|
|
244
|
+
}
|
|
245
|
+
async fetch(operation, input, options) {
|
|
246
|
+
options.signal?.throwIfAborted();
|
|
247
|
+
const sessionRevision = this.sessionRevision;
|
|
248
|
+
const headers = new Headers(options.headers);
|
|
249
|
+
for (const [key, value] of Object.entries(input.headers ?? {}))
|
|
250
|
+
if (value !== undefined && value !== null)
|
|
251
|
+
headers.set(key, String(value));
|
|
252
|
+
headers.delete('Authorization');
|
|
253
|
+
headers.delete('Cookie');
|
|
254
|
+
headers.delete('X-CSRF-Token');
|
|
255
|
+
headers.set('Accept', operation.kind === 'file' || operation.kind === 'csv'
|
|
256
|
+
? [...new Set(Object.values(operation.mediaTypes ?? {}).flat())].join(', ')
|
|
257
|
+
: operation.kind === 'sse' ? 'text/event-stream' : 'application/json');
|
|
258
|
+
if (this.options.apiKey) {
|
|
259
|
+
if (!operation.apiKey.allowed)
|
|
260
|
+
throw new GoaError(403, 'key_route_denied', '此接口需要浏览器会话');
|
|
261
|
+
headers.set('Authorization', 'Bearer ' + this.options.apiKey);
|
|
262
|
+
}
|
|
263
|
+
else if (!['GET', 'HEAD', 'OPTIONS'].includes(operation.method) && operation.access.kind !== 'public') {
|
|
264
|
+
if (!this.csrfToken)
|
|
265
|
+
throw new GoaError(401, 'session_missing', '登录状态尚未恢复');
|
|
266
|
+
headers.set('X-CSRF-Token', this.csrfToken);
|
|
267
|
+
}
|
|
268
|
+
let body;
|
|
269
|
+
if (input.body !== undefined) {
|
|
270
|
+
if (operation.kind === 'multipart') {
|
|
271
|
+
headers.delete('Content-Type');
|
|
272
|
+
const form = new FormData();
|
|
273
|
+
for (const [name, value] of Object.entries(input.body)) {
|
|
274
|
+
if (value === undefined)
|
|
275
|
+
continue;
|
|
276
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
277
|
+
if (item instanceof Blob)
|
|
278
|
+
form.append(name, item);
|
|
279
|
+
else if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean')
|
|
280
|
+
form.append(name, String(item));
|
|
281
|
+
else
|
|
282
|
+
throw new Error(`Unsupported multipart field: ${name}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
body = form;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
headers.set('Content-Type', 'application/json');
|
|
289
|
+
body = JSON.stringify(input.body);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const url = (this.options.baseURL ?? '').replace(/\/$/, '') + pathFor(operation, input);
|
|
293
|
+
const init = { method: operation.method, credentials: this.options.credentials ?? (this.options.apiKey ? 'omit' : 'include'), headers, body, signal: options.signal, cache: 'no-store' };
|
|
294
|
+
const response = operation.kind === 'multipart' && options.onProgress
|
|
295
|
+
? await this.sendUpload(url, init, options.onProgress)
|
|
296
|
+
: await (this.options.transport ?? globalThis.fetch)(url, init);
|
|
297
|
+
this.responseSessions.set(response, sessionRevision);
|
|
298
|
+
if (options.signal?.aborted) {
|
|
299
|
+
await response.body?.cancel();
|
|
300
|
+
options.signal.throwIfAborted();
|
|
301
|
+
}
|
|
302
|
+
return response;
|
|
303
|
+
}
|
|
304
|
+
sendUpload(url, init, progress) {
|
|
305
|
+
const xhr = this.options.xhrFactory?.() ?? new XMLHttpRequest();
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
xhr.open(init.method, url);
|
|
308
|
+
const target = new URL(url, globalThis.location?.href ?? 'http://localhost');
|
|
309
|
+
xhr.withCredentials = init.credentials === 'include' || init.credentials === 'same-origin' && target.origin === globalThis.location?.origin;
|
|
310
|
+
new Headers(init.headers).forEach((value, name) => xhr.setRequestHeader(name, value));
|
|
311
|
+
const cleanup = () => {
|
|
312
|
+
init.signal?.removeEventListener('abort', abort);
|
|
313
|
+
xhr.upload.removeEventListener('progress', report);
|
|
314
|
+
xhr.removeEventListener('load', load);
|
|
315
|
+
xhr.removeEventListener('error', failed);
|
|
316
|
+
xhr.removeEventListener('abort', aborted);
|
|
317
|
+
xhr.removeEventListener('timeout', timeout);
|
|
318
|
+
};
|
|
319
|
+
const report = (event) => { if (event.lengthComputable && event.total > 0)
|
|
320
|
+
progress(Math.min(100, Math.round(event.loaded / event.total * 100))); };
|
|
321
|
+
const aborted = () => { cleanup(); reject(init.signal?.reason ?? new DOMException('已取消', 'AbortError')); };
|
|
322
|
+
const abort = () => { cleanup(); xhr.abort(); aborted(); };
|
|
323
|
+
const failed = () => { cleanup(); reject(new GoaError(0, 'network_error', '上传失败,请检查网络连接')); };
|
|
324
|
+
const timeout = () => { cleanup(); reject(new GoaError(0, 'timeout', '上传超时')); };
|
|
325
|
+
const load = () => {
|
|
326
|
+
cleanup();
|
|
327
|
+
const headers = new Headers();
|
|
328
|
+
for (const name of ['Content-Type', 'X-Request-Id', 'Retry-After']) {
|
|
329
|
+
const value = xhr.getResponseHeader(name);
|
|
330
|
+
if (value)
|
|
331
|
+
headers.set(name, value);
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
resolve(new Response(xhr.status === 204 ? null : xhr.responseText, { status: xhr.status, headers }));
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
reject(error);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
init.signal?.addEventListener('abort', abort, { once: true });
|
|
341
|
+
xhr.upload.addEventListener('progress', report);
|
|
342
|
+
xhr.addEventListener('load', load);
|
|
343
|
+
xhr.addEventListener('error', failed);
|
|
344
|
+
xhr.addEventListener('abort', aborted);
|
|
345
|
+
xhr.addEventListener('timeout', timeout);
|
|
346
|
+
if (init.signal?.aborted) {
|
|
347
|
+
abort();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
xhr.send(init.body);
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
failed();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
async error(operation, response) {
|
|
359
|
+
if (response.status === 401 && operation.access.kind !== 'public' && this.responseSessions.get(response) === this.sessionRevision)
|
|
360
|
+
this.expireSession();
|
|
361
|
+
let value;
|
|
362
|
+
try {
|
|
363
|
+
value = await response.json();
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return new GoaError(response.status, 'invalid_response', '服务端错误响应格式错误');
|
|
367
|
+
}
|
|
368
|
+
const schema = operation.errors[response.status];
|
|
369
|
+
if (schema) {
|
|
370
|
+
const parsed = schema.safeParse(value);
|
|
371
|
+
if (!parsed.success)
|
|
372
|
+
return new GoaError(response.status, 'invalid_response', '服务端错误响应不符合契约');
|
|
373
|
+
value = parsed.data;
|
|
374
|
+
}
|
|
375
|
+
if (!value || typeof value !== 'object' || !('name' in value) || typeof value.name !== 'string' || !('message' in value) || typeof value.message !== 'string')
|
|
376
|
+
return new GoaError(response.status, 'invalid_response', '服务端错误响应格式错误');
|
|
377
|
+
const error = new GoaError(response.status, value.name, value.message);
|
|
378
|
+
error.requestId = response.headers.get('X-Request-Id') ?? undefined;
|
|
379
|
+
if ('id' in value && typeof value.id === 'string')
|
|
380
|
+
error.errorId = value.id;
|
|
381
|
+
if ('temporary' in value && typeof value.temporary === 'boolean')
|
|
382
|
+
error.temporary = value.temporary;
|
|
383
|
+
if ('timeout' in value && typeof value.timeout === 'boolean')
|
|
384
|
+
error.timeout = value.timeout;
|
|
385
|
+
if ('fault' in value && typeof value.fault === 'boolean')
|
|
386
|
+
error.fault = value.fault;
|
|
387
|
+
error.details = value;
|
|
388
|
+
return error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=goa.js.map
|
package/dist/goa.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"goa.js","sourceRoot":"","sources":["../src/goa.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,OAAO,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fudanda/client",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.20261024.1-beta.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"arcbase-source": "./src/index.ts",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"development": "./src/index.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "node ../../scripts/clean-package.mjs && tsc -p tsconfig.json && tsc-alias --resolve-full-paths",
|
|
19
|
+
"check:types": "tsc -p tsconfig.json --noEmit",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"lint": "eslint src --max-warnings 0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@fudanda/shared": "2.20261024.1-beta.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@eslint/js": "^10.0.1",
|
|
28
|
+
"eslint": "^10.12.0",
|
|
29
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
30
|
+
"typescript-eslint": "^8.71.1",
|
|
31
|
+
"vitest": "^5.0.3",
|
|
32
|
+
"tsc-alias": "^1.9.7"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src",
|
|
37
|
+
"README.md",
|
|
38
|
+
"!src/**/*.test.ts",
|
|
39
|
+
"!src/**/*.test.tsx",
|
|
40
|
+
"!dist/.tsbuildinfo",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/fudanda/arcbase.git",
|
|
47
|
+
"directory": "packages/client"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"registry": "https://registry.npmjs.org/",
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export interface SSEFrame { event: string; data?: string; id?: string; retry?: number }
|
|
2
|
+
|
|
3
|
+
export class SSEProtocolError extends Error {}
|
|
4
|
+
|
|
5
|
+
/** Incremental SSE framing. Bound each frame before retaining its text. */
|
|
6
|
+
export async function* readSSE(body: ReadableStream<Uint8Array>, signal: AbortSignal): AsyncGenerator<SSEFrame> {
|
|
7
|
+
const reader = body.getReader();
|
|
8
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
let line = '', event = '', data: string[] = [], id: string | undefined, retry: number | undefined;
|
|
11
|
+
let frameBytes = 0, afterCR = false, finished = false, cancelled = false;
|
|
12
|
+
const cancel = async () => {
|
|
13
|
+
if (finished || cancelled) return;
|
|
14
|
+
cancelled = true;
|
|
15
|
+
try { await reader.cancel(); } catch { /* The original read failure is reported by the caller. */ }
|
|
16
|
+
};
|
|
17
|
+
const abort = () => { void cancel(); };
|
|
18
|
+
const append = (fragment: string) => {
|
|
19
|
+
frameBytes += encoder.encode(fragment).byteLength;
|
|
20
|
+
if (frameBytes > 512 * 1024) throw new SSEProtocolError('SSE frame exceeds 512 KiB');
|
|
21
|
+
line += fragment;
|
|
22
|
+
};
|
|
23
|
+
const consume = (): SSEFrame | undefined => {
|
|
24
|
+
const current = line;
|
|
25
|
+
line = '';
|
|
26
|
+
if (!current) {
|
|
27
|
+
const frame = data.length || id !== undefined || retry !== undefined ? { event: event || 'message', data: data.length ? data.join('\n') : undefined, id, retry } : undefined;
|
|
28
|
+
event = ''; data = []; id = undefined; retry = undefined; frameBytes = 0;
|
|
29
|
+
return frame;
|
|
30
|
+
}
|
|
31
|
+
if (current.startsWith(':')) { frameBytes -= encoder.encode(current).byteLength; return; }
|
|
32
|
+
const colon = current.indexOf(':');
|
|
33
|
+
const field = colon === -1 ? current : current.slice(0, colon);
|
|
34
|
+
let value = colon === -1 ? '' : current.slice(colon + 1);
|
|
35
|
+
if (value.startsWith(' ')) value = value.slice(1);
|
|
36
|
+
if (field === 'event') event = value;
|
|
37
|
+
else if (field === 'data') data.push(value);
|
|
38
|
+
else if (field === 'id' && !value.includes('\0')) id = value;
|
|
39
|
+
else if (field === 'retry' && /^\d+$/.test(value) && Number.isSafeInteger(Number(value))) retry = Number(value);
|
|
40
|
+
};
|
|
41
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
42
|
+
try {
|
|
43
|
+
signal.throwIfAborted();
|
|
44
|
+
while (true) {
|
|
45
|
+
const part = await reader.read();
|
|
46
|
+
signal.throwIfAborted();
|
|
47
|
+
if (part.done) { finished = true; break; }
|
|
48
|
+
let text: string;
|
|
49
|
+
try { text = decoder.decode(part.value, { stream: true }); }
|
|
50
|
+
catch { throw new SSEProtocolError('SSE contains invalid UTF-8'); }
|
|
51
|
+
let start = 0;
|
|
52
|
+
for (let index = 0; index < text.length; index++) {
|
|
53
|
+
const char = text[index];
|
|
54
|
+
if (afterCR) { afterCR = false; if (char === '\n') { start = index + 1; continue; } }
|
|
55
|
+
if (char !== '\r' && char !== '\n') continue;
|
|
56
|
+
append(text.slice(start, index));
|
|
57
|
+
const frame = consume();
|
|
58
|
+
if (frame) yield frame;
|
|
59
|
+
afterCR = char === '\r';
|
|
60
|
+
start = index + 1;
|
|
61
|
+
}
|
|
62
|
+
append(text.slice(start));
|
|
63
|
+
}
|
|
64
|
+
try { decoder.decode(); } catch { throw new SSEProtocolError('SSE contains incomplete UTF-8'); }
|
|
65
|
+
// The SSE protocol discards a final event without its empty-line delimiter.
|
|
66
|
+
} finally {
|
|
67
|
+
signal.removeEventListener('abort', abort);
|
|
68
|
+
await cancel();
|
|
69
|
+
reader.releaseLock();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function waitForReconnect(milliseconds: number, signal: AbortSignal): Promise<void> {
|
|
74
|
+
return new Promise(resolve => {
|
|
75
|
+
const finish = () => { clearTimeout(timer); signal.removeEventListener('abort', finish); resolve(); };
|
|
76
|
+
const timer = setTimeout(finish, milliseconds);
|
|
77
|
+
signal.addEventListener('abort', finish, { once: true });
|
|
78
|
+
if (signal.aborted) finish();
|
|
79
|
+
});
|
|
80
|
+
}
|
package/src/goa.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import type { GoaInputOf, GoaOperation, GoaOutputOf } from '@fudanda/shared/core';
|
|
2
|
+
import { readSSE, SSEProtocolError, waitForReconnect } from './goa-stream';
|
|
3
|
+
|
|
4
|
+
export interface GoaClientOptions {
|
|
5
|
+
baseURL?: string;
|
|
6
|
+
credentials?: RequestCredentials;
|
|
7
|
+
transport?: typeof fetch;
|
|
8
|
+
xhrFactory?: () => XMLHttpRequest;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
csrfToken?: string;
|
|
11
|
+
onUnauthorized?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GoaRequestOptions {
|
|
15
|
+
signal?: AbortSignal | null;
|
|
16
|
+
headers?: HeadersInit;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GoaUploadOptions extends GoaRequestOptions {
|
|
20
|
+
onProgress?: (percent: number) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type GoaConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'closed';
|
|
24
|
+
export interface GoaStreamOptions<Data> extends GoaRequestOptions {
|
|
25
|
+
onEvent: (event: Data) => void;
|
|
26
|
+
onConnection?: (state: GoaConnectionState) => void;
|
|
27
|
+
onError?: (error: Error) => void;
|
|
28
|
+
reconnectDelay?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface GoaSubscription { close(): void; done: Promise<void> }
|
|
31
|
+
|
|
32
|
+
export class GoaError extends Error {
|
|
33
|
+
readonly name = 'GoaError';
|
|
34
|
+
errorId?: string;
|
|
35
|
+
requestId?: string;
|
|
36
|
+
temporary = false;
|
|
37
|
+
timeout = false;
|
|
38
|
+
fault = false;
|
|
39
|
+
details?: unknown;
|
|
40
|
+
|
|
41
|
+
constructor(readonly status: number, readonly reason: string, message: string) { super(message); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Arguments<Operation extends GoaOperation> = undefined extends GoaInputOf<Operation>
|
|
45
|
+
? [input?: GoaInputOf<Operation>, options?: GoaRequestOptions]
|
|
46
|
+
: [input: GoaInputOf<Operation>, options?: GoaRequestOptions];
|
|
47
|
+
type UploadArguments<Operation extends GoaOperation> = [input: GoaInputOf<Operation>, options?: GoaUploadOptions];
|
|
48
|
+
|
|
49
|
+
type Values = { params?: Record<string, unknown>; query?: Record<string, unknown>; headers?: Record<string, unknown>; body?: unknown };
|
|
50
|
+
|
|
51
|
+
export interface GoaResponse<Data> { data: Data; status: number; headers: Headers }
|
|
52
|
+
|
|
53
|
+
function pathFor(operation: GoaOperation, input: Values): string {
|
|
54
|
+
if (!operation.path.startsWith('/api/v1/') || /[?#\\\s]/.test(operation.path) || operation.path.split('/').some(part => part === '.' || part === '..')) throw new Error('Invalid generated API path');
|
|
55
|
+
const path = operation.path.replace(/\{(\*?\w+)\}/g, (_match, name: string) => {
|
|
56
|
+
const wildcard = name.startsWith('*');
|
|
57
|
+
const value = input.params?.[wildcard ? name.slice(1) : name];
|
|
58
|
+
if (value === undefined || value === null) throw new Error(`Missing path parameter: ${name}`);
|
|
59
|
+
if (!wildcard) return encodeURIComponent(String(value));
|
|
60
|
+
const parts = String(value).split('/');
|
|
61
|
+
if (parts.some(part => part === '.' || part === '..')) throw new Error('Invalid wildcard path');
|
|
62
|
+
return parts.map(encodeURIComponent).join('/');
|
|
63
|
+
});
|
|
64
|
+
const query = new URLSearchParams();
|
|
65
|
+
for (const [key, value] of Object.entries(input.query ?? {})) {
|
|
66
|
+
if (value === undefined || value === null) continue;
|
|
67
|
+
for (const item of Array.isArray(value) ? value : [value]) query.append(key, String(item));
|
|
68
|
+
}
|
|
69
|
+
return path + (query.size ? '?' + query.toString() : '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Native Goa HTTP client. Generated schemas own request and response shapes. */
|
|
73
|
+
export class GoaClient {
|
|
74
|
+
private csrfToken: string;
|
|
75
|
+
private sessionRevision = 0;
|
|
76
|
+
private readonly responseSessions = new WeakMap<Response, number>();
|
|
77
|
+
private readonly unauthorizedListeners = new Set<() => void>();
|
|
78
|
+
|
|
79
|
+
constructor(private readonly options: GoaClientOptions = {}) {
|
|
80
|
+
const baseURL = options.baseURL ?? '';
|
|
81
|
+
if (baseURL && (!/^https?:\/\//.test(baseURL) || new URL(baseURL).origin !== baseURL.replace(/\/$/, ''))) throw new Error('baseURL must be an HTTP origin');
|
|
82
|
+
if (options.apiKey && !/^arc_[a-f0-9]{64}$/.test(options.apiKey)) throw new Error('Invalid ArcBase API Key');
|
|
83
|
+
this.csrfToken = options.csrfToken ?? '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setCSRFToken(token: string): void { if (token !== this.csrfToken) { this.csrfToken = token; this.sessionRevision++; } }
|
|
87
|
+
clearSession(): void { this.csrfToken = ''; this.sessionRevision++; }
|
|
88
|
+
sessionHeaders(): Record<string,string> { return this.csrfToken ? { 'X-CSRF-Token': this.csrfToken } : {}; }
|
|
89
|
+
subscribeUnauthorized(listener: () => void): () => void {
|
|
90
|
+
this.unauthorizedListeners.add(listener);
|
|
91
|
+
return () => { this.unauthorizedListeners.delete(listener); };
|
|
92
|
+
}
|
|
93
|
+
expireSession(): void {
|
|
94
|
+
this.clearSession();
|
|
95
|
+
for (const listener of this.unauthorizedListeners) listener();
|
|
96
|
+
this.options.onUnauthorized?.();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
call<Operation extends GoaOperation>(operation: Operation, ...args: Arguments<Operation>): Promise<GoaOutputOf<Operation>> {
|
|
100
|
+
return this.execute(operation, args[0], args[1]).then(response => response.data);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
callWithResponse<Operation extends GoaOperation>(operation: Operation, ...args: Arguments<Operation>): Promise<GoaResponse<GoaOutputOf<Operation>>> {
|
|
104
|
+
return this.execute(operation, args[0], args[1]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
upload<Operation extends GoaOperation & { readonly kind: 'multipart' }>(operation: Operation, ...args: UploadArguments<Operation>): Promise<GoaOutputOf<Operation>> {
|
|
108
|
+
return this.execute(operation, args[0], args[1], 'multipart').then(response => response.data);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Observe a generated GET stream; closing it never cancels domain execution. */
|
|
112
|
+
stream<Operation extends GoaOperation & { readonly kind: 'sse' }>(operation: Operation, input: GoaInputOf<Operation>, options: GoaStreamOptions<GoaOutputOf<Operation>>): GoaSubscription {
|
|
113
|
+
if (operation.kind !== 'sse' || operation.method !== 'GET') throw new Error('Expected a GET SSE operation');
|
|
114
|
+
const values = operation.input.parse(input) as Values;
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
const streamRevision = this.sessionRevision;
|
|
117
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
118
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
119
|
+
if (options.signal?.aborted) abort();
|
|
120
|
+
const close = () => controller.abort();
|
|
121
|
+
const done = (async () => {
|
|
122
|
+
const initialHeaders = new Headers();
|
|
123
|
+
for (const [key, value] of Object.entries(values.headers ?? {})) if (value !== undefined && value !== null) initialHeaders.set(key, String(value));
|
|
124
|
+
let cursor = initialHeaders.get('Last-Event-ID');
|
|
125
|
+
let delay = Math.min(15000, Math.max(250, options.reconnectDelay ?? 1000));
|
|
126
|
+
let attempts = 0;
|
|
127
|
+
try {
|
|
128
|
+
while (!controller.signal.aborted) {
|
|
129
|
+
if (operation.access.kind !== 'public' && this.sessionRevision !== streamRevision) return;
|
|
130
|
+
options.onConnection?.(attempts ? 'reconnecting' : 'connecting');
|
|
131
|
+
try {
|
|
132
|
+
const resumed = { ...values, headers: { ...values.headers } };
|
|
133
|
+
if (operation.sse?.requestIdField && cursor !== null) resumed.headers['Last-Event-ID'] = cursor;
|
|
134
|
+
if (!operation.input.safeParse(resumed).success) throw new GoaError(400, 'invalid_response', '服务端续传标识不符合契约');
|
|
135
|
+
const response = await this.fetch(operation, resumed, { ...options, signal: controller.signal });
|
|
136
|
+
if (!response.ok) throw await this.error(operation, response);
|
|
137
|
+
await this.requireCurrentSession(operation, response);
|
|
138
|
+
const schema = operation.responses[response.status];
|
|
139
|
+
if (!schema || !response.body || response.headers.get('Content-Type')?.split(';')[0].trim().toLowerCase() !== 'text/event-stream') {
|
|
140
|
+
await response.body?.cancel();
|
|
141
|
+
throw new GoaError(response.status, 'invalid_response', '服务端流式响应不符合契约');
|
|
142
|
+
}
|
|
143
|
+
options.onConnection?.('connected');
|
|
144
|
+
attempts = 0;
|
|
145
|
+
for await (const frame of readSSE(response.body, controller.signal)) {
|
|
146
|
+
if (operation.access.kind !== 'public' && this.sessionRevision !== streamRevision) return;
|
|
147
|
+
if (frame.id !== undefined) cursor = frame.id;
|
|
148
|
+
if (frame.retry !== undefined) delay = Math.min(15000, Math.max(250, frame.retry));
|
|
149
|
+
if (frame.data === undefined) continue;
|
|
150
|
+
if (frame.event === 'session-expired') {
|
|
151
|
+
if (this.responseSessions.get(response) === this.sessionRevision) this.expireSession();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (frame.event === 'denied') throw new GoaError(403, 'stream_denied', '订阅权限已失效');
|
|
155
|
+
let raw: unknown;
|
|
156
|
+
try { raw = JSON.parse(frame.data); }
|
|
157
|
+
catch { throw new GoaError(response.status, 'invalid_response', '服务端流式事件格式错误'); }
|
|
158
|
+
const wire = operation.sse;
|
|
159
|
+
if (wire?.dataField) raw = { [wire.dataField]: raw };
|
|
160
|
+
if (wire?.idField || wire?.eventField || wire?.retryField) {
|
|
161
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new GoaError(response.status, 'invalid_response', '服务端流式事件格式错误');
|
|
162
|
+
const mapped: Record<string, unknown> = { ...raw };
|
|
163
|
+
if (wire.idField) mapped[wire.idField] = cursor ?? '';
|
|
164
|
+
if (wire.eventField) mapped[wire.eventField] = frame.event;
|
|
165
|
+
if (wire.retryField && frame.retry !== undefined) mapped[wire.retryField] = frame.retry;
|
|
166
|
+
raw = mapped;
|
|
167
|
+
}
|
|
168
|
+
const parsed = schema.safeParse(raw);
|
|
169
|
+
if (!parsed.success) throw new GoaError(response.status, 'invalid_response', '服务端流式事件不符合契约');
|
|
170
|
+
options.onEvent(parsed.data as GoaOutputOf<Operation>);
|
|
171
|
+
if (frame.event === 'done') return;
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (controller.signal.aborted || error instanceof DOMException && error.name === 'AbortError') return;
|
|
175
|
+
options.onError?.(error instanceof Error ? error : new Error('流式连接中断'));
|
|
176
|
+
if (error instanceof SSEProtocolError || error instanceof GoaError && (error.reason === 'invalid_response' || error.status < 500 && error.status !== 429)) return;
|
|
177
|
+
}
|
|
178
|
+
attempts++;
|
|
179
|
+
await waitForReconnect(Math.min(15000, delay * 2 ** Math.min(attempts - 1, 4)), controller.signal);
|
|
180
|
+
}
|
|
181
|
+
} finally {
|
|
182
|
+
options.signal?.removeEventListener('abort', abort);
|
|
183
|
+
options.onConnection?.('closed');
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
return { close, done };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The caller consumes or cancels the returned body; the transport never buffers files. */
|
|
190
|
+
async download<Operation extends GoaOperation & { readonly kind: 'file' | 'csv' }>(operation: Operation, ...args: Arguments<Operation>): Promise<Response> {
|
|
191
|
+
if (operation.kind !== 'file' && operation.kind !== 'csv') throw new Error(`Operation ${operation.id} is not a download`);
|
|
192
|
+
const values = operation.input.parse(args[0]) as Values;
|
|
193
|
+
const response = await this.fetch(operation, values, args[1] ?? {});
|
|
194
|
+
if (!response.ok) throw await this.error(operation, response);
|
|
195
|
+
await this.requireCurrentSession(operation, response);
|
|
196
|
+
const declared = operation.mediaTypes?.[response.status] ?? [];
|
|
197
|
+
const contentType = response.headers.get('Content-Type')?.split(';')[0].trim().toLowerCase();
|
|
198
|
+
const matchesMediaType = declared.some(mediaType => {
|
|
199
|
+
const expected = mediaType.split(';')[0].trim().toLowerCase();
|
|
200
|
+
if (!contentType || !/^[^\s/;]+\/[^\s/;]+$/.test(contentType)) return false;
|
|
201
|
+
return expected === contentType || expected === '*/*' || expected.endsWith('/*') && contentType.startsWith(expected.slice(0, -1));
|
|
202
|
+
});
|
|
203
|
+
if (!operation.responses[response.status] || !matchesMediaType) {
|
|
204
|
+
await response.body?.cancel();
|
|
205
|
+
throw new GoaError(response.status, 'invalid_response', '文件响应不符合契约');
|
|
206
|
+
}
|
|
207
|
+
return response;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async execute<Operation extends GoaOperation>(operation: Operation, input: unknown, options: GoaUploadOptions = {}, channel: 'json' | 'multipart' = 'json'): Promise<GoaResponse<GoaOutputOf<Operation>>> {
|
|
211
|
+
if (operation.kind !== channel) throw new Error(`Operation ${operation.id} requires the ${operation.kind} channel`);
|
|
212
|
+
const values = operation.input.parse(input) as Values;
|
|
213
|
+
const response = await this.fetch(operation, values, options);
|
|
214
|
+
if (!response.ok) throw await this.error(operation, response);
|
|
215
|
+
await this.requireCurrentSession(operation, response);
|
|
216
|
+
const schema = operation.responses[response.status];
|
|
217
|
+
if (!schema) throw new GoaError(response.status, 'unexpected_status', '服务端返回了未声明的状态');
|
|
218
|
+
let raw: unknown;
|
|
219
|
+
if (response.status !== 204) {
|
|
220
|
+
try { raw = await response.json(); } catch { throw new GoaError(response.status, 'invalid_response', '服务端响应格式错误'); }
|
|
221
|
+
}
|
|
222
|
+
const parsed = schema.safeParse(raw);
|
|
223
|
+
if (!parsed.success) throw new GoaError(response.status, 'invalid_response', '服务端响应不符合契约');
|
|
224
|
+
await this.requireCurrentSession(operation, response);
|
|
225
|
+
return { data: parsed.data as GoaOutputOf<Operation>, status: response.status, headers: response.headers };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private async requireCurrentSession(operation: GoaOperation, response: Response): Promise<void> {
|
|
229
|
+
if (operation.access.kind === 'public' || this.options.apiKey || this.responseSessions.get(response) === this.sessionRevision) return;
|
|
230
|
+
if (!response.bodyUsed) await response.body?.cancel();
|
|
231
|
+
throw new DOMException('会话已变更,已丢弃旧请求结果', 'AbortError');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private async fetch(operation: GoaOperation, input: Values, options: GoaUploadOptions): Promise<Response> {
|
|
235
|
+
options.signal?.throwIfAborted();
|
|
236
|
+
const sessionRevision = this.sessionRevision;
|
|
237
|
+
const headers = new Headers(options.headers);
|
|
238
|
+
for (const [key, value] of Object.entries(input.headers ?? {})) if (value !== undefined && value !== null) headers.set(key, String(value));
|
|
239
|
+
headers.delete('Authorization');
|
|
240
|
+
headers.delete('Cookie');
|
|
241
|
+
headers.delete('X-CSRF-Token');
|
|
242
|
+
headers.set('Accept', operation.kind === 'file' || operation.kind === 'csv'
|
|
243
|
+
? [...new Set(Object.values(operation.mediaTypes ?? {}).flat())].join(', ')
|
|
244
|
+
: operation.kind === 'sse' ? 'text/event-stream' : 'application/json');
|
|
245
|
+
if (this.options.apiKey) {
|
|
246
|
+
if (!operation.apiKey.allowed) throw new GoaError(403, 'key_route_denied', '此接口需要浏览器会话');
|
|
247
|
+
headers.set('Authorization', 'Bearer ' + this.options.apiKey);
|
|
248
|
+
} else if (!['GET', 'HEAD', 'OPTIONS'].includes(operation.method) && operation.access.kind !== 'public') {
|
|
249
|
+
if (!this.csrfToken) throw new GoaError(401, 'session_missing', '登录状态尚未恢复');
|
|
250
|
+
headers.set('X-CSRF-Token', this.csrfToken);
|
|
251
|
+
}
|
|
252
|
+
let body: BodyInit | undefined;
|
|
253
|
+
if (input.body !== undefined) {
|
|
254
|
+
if (operation.kind === 'multipart') {
|
|
255
|
+
headers.delete('Content-Type');
|
|
256
|
+
const form = new FormData();
|
|
257
|
+
for (const [name, value] of Object.entries(input.body as Record<string, unknown>)) {
|
|
258
|
+
if (value === undefined) continue;
|
|
259
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
260
|
+
if (item instanceof Blob) form.append(name, item);
|
|
261
|
+
else if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') form.append(name, String(item));
|
|
262
|
+
else throw new Error(`Unsupported multipart field: ${name}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
body = form;
|
|
266
|
+
}
|
|
267
|
+
else { headers.set('Content-Type', 'application/json'); body = JSON.stringify(input.body); }
|
|
268
|
+
}
|
|
269
|
+
const url = (this.options.baseURL ?? '').replace(/\/$/, '') + pathFor(operation, input);
|
|
270
|
+
const init: RequestInit = { method: operation.method, credentials: this.options.credentials ?? (this.options.apiKey ? 'omit' : 'include'), headers, body, signal: options.signal, cache: 'no-store' };
|
|
271
|
+
const response = operation.kind === 'multipart' && options.onProgress
|
|
272
|
+
? await this.sendUpload(url, init, options.onProgress)
|
|
273
|
+
: await (this.options.transport ?? globalThis.fetch)(url, init);
|
|
274
|
+
this.responseSessions.set(response, sessionRevision);
|
|
275
|
+
if (options.signal?.aborted) {
|
|
276
|
+
await response.body?.cancel();
|
|
277
|
+
options.signal.throwIfAborted();
|
|
278
|
+
}
|
|
279
|
+
return response;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private sendUpload(url: string, init: RequestInit, progress: (percent: number) => void): Promise<Response> {
|
|
283
|
+
const xhr = this.options.xhrFactory?.() ?? new XMLHttpRequest();
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
xhr.open(init.method!, url);
|
|
286
|
+
const target = new URL(url, globalThis.location?.href ?? 'http://localhost');
|
|
287
|
+
xhr.withCredentials = init.credentials === 'include' || init.credentials === 'same-origin' && target.origin === globalThis.location?.origin;
|
|
288
|
+
new Headers(init.headers).forEach((value, name) => xhr.setRequestHeader(name, value));
|
|
289
|
+
const cleanup = () => {
|
|
290
|
+
init.signal?.removeEventListener('abort', abort);
|
|
291
|
+
xhr.upload.removeEventListener('progress', report);
|
|
292
|
+
xhr.removeEventListener('load', load);
|
|
293
|
+
xhr.removeEventListener('error', failed);
|
|
294
|
+
xhr.removeEventListener('abort', aborted);
|
|
295
|
+
xhr.removeEventListener('timeout', timeout);
|
|
296
|
+
};
|
|
297
|
+
const report = (event: ProgressEvent) => { if (event.lengthComputable && event.total > 0) progress(Math.min(100, Math.round(event.loaded / event.total * 100))); };
|
|
298
|
+
const aborted = () => { cleanup(); reject(init.signal?.reason ?? new DOMException('已取消', 'AbortError')); };
|
|
299
|
+
const abort = () => { cleanup(); xhr.abort(); aborted(); };
|
|
300
|
+
const failed = () => { cleanup(); reject(new GoaError(0, 'network_error', '上传失败,请检查网络连接')); };
|
|
301
|
+
const timeout = () => { cleanup(); reject(new GoaError(0, 'timeout', '上传超时')); };
|
|
302
|
+
const load = () => {
|
|
303
|
+
cleanup();
|
|
304
|
+
const headers = new Headers();
|
|
305
|
+
for (const name of ['Content-Type', 'X-Request-Id', 'Retry-After']) {
|
|
306
|
+
const value = xhr.getResponseHeader(name);
|
|
307
|
+
if (value) headers.set(name, value);
|
|
308
|
+
}
|
|
309
|
+
try { resolve(new Response(xhr.status === 204 ? null : xhr.responseText, { status: xhr.status, headers })); }
|
|
310
|
+
catch (error) { reject(error); }
|
|
311
|
+
};
|
|
312
|
+
init.signal?.addEventListener('abort', abort, { once: true });
|
|
313
|
+
xhr.upload.addEventListener('progress', report);
|
|
314
|
+
xhr.addEventListener('load', load);
|
|
315
|
+
xhr.addEventListener('error', failed);
|
|
316
|
+
xhr.addEventListener('abort', aborted);
|
|
317
|
+
xhr.addEventListener('timeout', timeout);
|
|
318
|
+
if (init.signal?.aborted) { abort(); return; }
|
|
319
|
+
try { xhr.send(init.body as FormData); } catch { failed(); }
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async error(operation: GoaOperation, response: Response): Promise<GoaError> {
|
|
324
|
+
if (response.status === 401 && operation.access.kind !== 'public' && this.responseSessions.get(response) === this.sessionRevision) this.expireSession();
|
|
325
|
+
let value: unknown;
|
|
326
|
+
try { value = await response.json(); } catch { return new GoaError(response.status, 'invalid_response', '服务端错误响应格式错误'); }
|
|
327
|
+
const schema = operation.errors[response.status];
|
|
328
|
+
if (schema) {
|
|
329
|
+
const parsed = schema.safeParse(value);
|
|
330
|
+
if (!parsed.success) return new GoaError(response.status, 'invalid_response', '服务端错误响应不符合契约');
|
|
331
|
+
value = parsed.data;
|
|
332
|
+
}
|
|
333
|
+
if (!value || typeof value !== 'object' || !('name' in value) || typeof value.name !== 'string' || !('message' in value) || typeof value.message !== 'string') return new GoaError(response.status, 'invalid_response', '服务端错误响应格式错误');
|
|
334
|
+
const error = new GoaError(response.status, value.name, value.message);
|
|
335
|
+
error.requestId = response.headers.get('X-Request-Id') ?? undefined;
|
|
336
|
+
if ('id' in value && typeof value.id === 'string') error.errorId = value.id;
|
|
337
|
+
if ('temporary' in value && typeof value.temporary === 'boolean') error.temporary = value.temporary;
|
|
338
|
+
if ('timeout' in value && typeof value.timeout === 'boolean') error.timeout = value.timeout;
|
|
339
|
+
if ('fault' in value && typeof value.fault === 'boolean') error.fault = value.fault;
|
|
340
|
+
error.details = value;
|
|
341
|
+
return error;
|
|
342
|
+
}
|
|
343
|
+
}
|
package/src/index.ts
ADDED