@vex-chat/libvex 6.6.1 → 6.6.3
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/README.md +1 -1
- package/dist/Client.d.ts +3 -14
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +88 -105
- package/dist/Client.js.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/codecs.d.ts +3 -6
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +6 -9
- package/dist/codecs.js.map +1 -1
- package/dist/http.d.ts +83 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +368 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/src/Client.ts +127 -139
- package/src/__tests__/harness/shared-suite.ts +33 -3
- package/src/__tests__/http.test.ts +98 -0
- package/src/codec.ts +1 -1
- package/src/codecs.ts +6 -9
- package/src/http.ts +563 -0
- package/src/index.ts +6 -0
- package/dist/storage/node/http-agents.d.ts +0 -20
- package/dist/storage/node/http-agents.d.ts.map +0 -1
- package/dist/storage/node/http-agents.js +0 -26
- package/dist/storage/node/http-agents.js.map +0 -1
- package/src/storage/node/http-agents.ts +0 -39
|
@@ -32,8 +32,7 @@ import type { Storage } from "../../Storage.js";
|
|
|
32
32
|
|
|
33
33
|
import { getCryptoProfile, setCryptoProfile } from "@vex-chat/crypto";
|
|
34
34
|
|
|
35
|
-
import {
|
|
36
|
-
|
|
35
|
+
import { isHttpError } from "../../http.js";
|
|
37
36
|
import { Client } from "../../index.js";
|
|
38
37
|
|
|
39
38
|
import { testFile, testImage } from "./fixtures.js";
|
|
@@ -394,6 +393,37 @@ export function platformSuite(
|
|
|
394
393
|
expect(new Uint8Array(fetched!.data)).toEqual(testFile);
|
|
395
394
|
});
|
|
396
395
|
|
|
396
|
+
test("file upload + download via JSON fallback path", async () => {
|
|
397
|
+
const globalWithFormData = globalThis as typeof globalThis & {
|
|
398
|
+
FormData?: unknown;
|
|
399
|
+
};
|
|
400
|
+
const originalFormData = globalWithFormData.FormData;
|
|
401
|
+
Object.defineProperty(globalThis, "FormData", {
|
|
402
|
+
configurable: true,
|
|
403
|
+
enumerable: true,
|
|
404
|
+
value: undefined,
|
|
405
|
+
writable: true,
|
|
406
|
+
});
|
|
407
|
+
try {
|
|
408
|
+
const [details, key] = await client.files.create(testFile);
|
|
409
|
+
expect(details.fileID).toBeTruthy();
|
|
410
|
+
|
|
411
|
+
const fetched = await client.files.retrieve(
|
|
412
|
+
details.fileID,
|
|
413
|
+
key,
|
|
414
|
+
);
|
|
415
|
+
expect(fetched).toBeTruthy();
|
|
416
|
+
expect(new Uint8Array(fetched!.data)).toEqual(testFile);
|
|
417
|
+
} finally {
|
|
418
|
+
Object.defineProperty(globalThis, "FormData", {
|
|
419
|
+
configurable: true,
|
|
420
|
+
enumerable: true,
|
|
421
|
+
value: originalFormData,
|
|
422
|
+
writable: true,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
397
427
|
test("emoji upload", async () => {
|
|
398
428
|
const server = await client.servers.create("Emoji Test Server");
|
|
399
429
|
const emoji = await client.emoji.create(
|
|
@@ -685,7 +715,7 @@ async function withTransientRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
685
715
|
} catch (e) {
|
|
686
716
|
last = e;
|
|
687
717
|
const transient =
|
|
688
|
-
|
|
718
|
+
isHttpError(e) &&
|
|
689
719
|
(e.response?.status === 502 || e.response?.status === 503);
|
|
690
720
|
if (transient && i < attempts - 1) {
|
|
691
721
|
await new Promise((r) => setTimeout(r, 400 * (i + 1)));
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-2026 Vex Heavy Industries LLC
|
|
3
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
|
+
* Commercial licenses available at vex.wtf
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
import { createFetchHttpClient, isHttpError } from "../http.js";
|
|
10
|
+
|
|
11
|
+
const originalFetch = globalThis.fetch;
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = originalFetch;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("FetchHttpClient", () => {
|
|
18
|
+
it("preserves status metadata for empty JSON error responses", async () => {
|
|
19
|
+
globalThis.fetch = () =>
|
|
20
|
+
Promise.resolve(
|
|
21
|
+
new Response(null, {
|
|
22
|
+
status: 401,
|
|
23
|
+
statusText: "Unauthorized",
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const client = createFetchHttpClient();
|
|
28
|
+
const err = await captureError(() =>
|
|
29
|
+
client.post(
|
|
30
|
+
"https://example.test/device/id/notifications",
|
|
31
|
+
{},
|
|
32
|
+
{ responseType: "json" },
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(isHttpError(err)).toBe(true);
|
|
37
|
+
if (!isHttpError(err)) {
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
expect(err.response?.status).toBe(401);
|
|
41
|
+
expect(err.response?.statusText).toBe("Unauthorized");
|
|
42
|
+
expect(err.response?.data).toBeNull();
|
|
43
|
+
expect(err.config.method).toBe("POST");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("preserves status metadata for non-JSON error responses", async () => {
|
|
47
|
+
globalThis.fetch = () =>
|
|
48
|
+
Promise.resolve(
|
|
49
|
+
new Response("plain failure", {
|
|
50
|
+
headers: { "Content-Type": "text/plain" },
|
|
51
|
+
status: 400,
|
|
52
|
+
statusText: "Bad Request",
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const client = createFetchHttpClient();
|
|
57
|
+
const err = await captureError(() =>
|
|
58
|
+
client.get("https://example.test/status", {
|
|
59
|
+
responseType: "json",
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(isHttpError(err)).toBe(true);
|
|
64
|
+
if (!isHttpError(err)) {
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
expect(err.response?.status).toBe(400);
|
|
68
|
+
expect(err.response?.data).toBe("plain failure");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("emits a final upload progress event for FormData payloads", async () => {
|
|
72
|
+
globalThis.fetch = () =>
|
|
73
|
+
Promise.resolve(new Response(new ArrayBuffer(0), { status: 200 }));
|
|
74
|
+
|
|
75
|
+
const client = createFetchHttpClient();
|
|
76
|
+
const events: { loaded: number; total?: number }[] = [];
|
|
77
|
+
const payload = new FormData();
|
|
78
|
+
payload.set("file", new Blob([new Uint8Array([1, 2, 3])]));
|
|
79
|
+
payload.set("name", "ok");
|
|
80
|
+
|
|
81
|
+
await client.post("https://example.test/file", payload, {
|
|
82
|
+
onUploadProgress: (event) => {
|
|
83
|
+
events.push(event);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(events).toEqual([{ loaded: 5, total: 5 }]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
async function captureError(fn: () => Promise<unknown>): Promise<unknown> {
|
|
92
|
+
try {
|
|
93
|
+
await fn();
|
|
94
|
+
} catch (err: unknown) {
|
|
95
|
+
return err;
|
|
96
|
+
}
|
|
97
|
+
throw new Error("Expected function to throw");
|
|
98
|
+
}
|
package/src/codec.ts
CHANGED
|
@@ -59,7 +59,7 @@ function msgpackDecode(data: Uint8Array): unknown {
|
|
|
59
59
|
/**
|
|
60
60
|
* Encode a value to msgpack. Returns a fresh Uint8Array copy
|
|
61
61
|
* (not a subarray of the internal pool buffer) to avoid browser
|
|
62
|
-
*
|
|
62
|
+
* transport corruption when a runtime reuses pooled buffers.
|
|
63
63
|
*/
|
|
64
64
|
function msgpackEncode(value: unknown): Uint8Array {
|
|
65
65
|
const packed = _packr.encode(value);
|
package/src/codecs.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Pre-built codec instances for every HTTP response type.
|
|
9
9
|
*
|
|
10
10
|
* Usage: import { UserCodec } from "./codecs.js";
|
|
11
|
-
* const data =
|
|
11
|
+
* const data = decodeHttpResponse(UserCodec, res.data);
|
|
12
12
|
*
|
|
13
13
|
* decode() returns typed data without runtime validation (SDK trusts server).
|
|
14
14
|
* For trust boundary validation, use codec.decodeSafe() directly.
|
|
@@ -190,18 +190,15 @@ export const PasskeyAuthFinishResponseCodec = createCodec(
|
|
|
190
190
|
}),
|
|
191
191
|
);
|
|
192
192
|
|
|
193
|
-
// ── Helper: decode
|
|
193
|
+
// ── Helper: decode binary HTTP response buffer ──────────────────────────────
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
* Decode
|
|
196
|
+
* Decode a binary HTTP response with a typed codec.
|
|
197
197
|
* Uses decodeSafe (Zod-validated) so schema mismatches surface immediately.
|
|
198
198
|
*/
|
|
199
|
-
export function
|
|
199
|
+
export function decodeHttpResponse<T>(
|
|
200
200
|
codec: { decodeSafe: (data: Uint8Array) => T },
|
|
201
|
-
/**
|
|
202
|
-
* Accepts `unknown` because axios types its `responseType: 'arraybuffer'`
|
|
203
|
-
* responses as `any`. At runtime this is always an `ArrayBuffer`.
|
|
204
|
-
*/
|
|
201
|
+
/** Accepts `unknown` so callers can pass transport response data directly. */
|
|
205
202
|
data: unknown,
|
|
206
203
|
): T {
|
|
207
204
|
if (data instanceof Uint8Array) {
|
|
@@ -210,5 +207,5 @@ export function decodeAxios<T>(
|
|
|
210
207
|
if (data instanceof ArrayBuffer) {
|
|
211
208
|
return codec.decodeSafe(new Uint8Array(data));
|
|
212
209
|
}
|
|
213
|
-
throw new Error("Expected Uint8Array or ArrayBuffer from
|
|
210
|
+
throw new Error("Expected Uint8Array or ArrayBuffer from HTTP response");
|
|
214
211
|
}
|