@fragno-dev/core 0.0.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/.turbo/turbo-build.log +61 -0
- package/.turbo/turbo-types$colon$check.log +2 -0
- package/dist/api/api.d.ts +2 -0
- package/dist/api/api.js +3 -0
- package/dist/api-CBDGZiLC.d.ts +278 -0
- package/dist/api-CBDGZiLC.d.ts.map +1 -0
- package/dist/api-DgHfYjq2.js +54 -0
- package/dist/api-DgHfYjq2.js.map +1 -0
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +6 -0
- package/dist/client/client.svelte.d.ts +33 -0
- package/dist/client/client.svelte.d.ts.map +1 -0
- package/dist/client/client.svelte.js +123 -0
- package/dist/client/client.svelte.js.map +1 -0
- package/dist/client/react.d.ts +58 -0
- package/dist/client/react.d.ts.map +1 -0
- package/dist/client/react.js +80 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/vanilla.d.ts +61 -0
- package/dist/client/vanilla.d.ts.map +1 -0
- package/dist/client/vanilla.js +136 -0
- package/dist/client/vanilla.js.map +1 -0
- package/dist/client/vue.d.ts +39 -0
- package/dist/client/vue.d.ts.map +1 -0
- package/dist/client/vue.js +108 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client-DWjxKDnE.js +703 -0
- package/dist/client-DWjxKDnE.js.map +1 -0
- package/dist/client-XFdAy-IQ.d.ts +287 -0
- package/dist/client-XFdAy-IQ.d.ts.map +1 -0
- package/dist/integrations/astro.d.ts +18 -0
- package/dist/integrations/astro.d.ts.map +1 -0
- package/dist/integrations/astro.js +16 -0
- package/dist/integrations/astro.js.map +1 -0
- package/dist/integrations/next-js.d.ts +15 -0
- package/dist/integrations/next-js.d.ts.map +1 -0
- package/dist/integrations/next-js.js +17 -0
- package/dist/integrations/next-js.js.map +1 -0
- package/dist/integrations/react-ssr.d.ts +19 -0
- package/dist/integrations/react-ssr.d.ts.map +1 -0
- package/dist/integrations/react-ssr.js +38 -0
- package/dist/integrations/react-ssr.js.map +1 -0
- package/dist/integrations/svelte-kit.d.ts +21 -0
- package/dist/integrations/svelte-kit.d.ts.map +1 -0
- package/dist/integrations/svelte-kit.js +18 -0
- package/dist/integrations/svelte-kit.js.map +1 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +177 -0
- package/dist/mod.js.map +1 -0
- package/dist/route-Bp6eByhz.js +331 -0
- package/dist/route-Bp6eByhz.js.map +1 -0
- package/dist/ssr-tJHqcNSw.js +48 -0
- package/dist/ssr-tJHqcNSw.js.map +1 -0
- package/package.json +127 -0
- package/src/api/api.test.ts +140 -0
- package/src/api/api.ts +106 -0
- package/src/api/error.ts +47 -0
- package/src/api/fragment.test.ts +509 -0
- package/src/api/fragment.ts +277 -0
- package/src/api/internal/path-runtime.test.ts +121 -0
- package/src/api/internal/path-type.test.ts +602 -0
- package/src/api/internal/path.ts +322 -0
- package/src/api/internal/response-stream.ts +118 -0
- package/src/api/internal/route.test.ts +56 -0
- package/src/api/internal/route.ts +9 -0
- package/src/api/request-input-context.test.ts +437 -0
- package/src/api/request-input-context.ts +201 -0
- package/src/api/request-middleware.test.ts +544 -0
- package/src/api/request-middleware.ts +126 -0
- package/src/api/request-output-context.test.ts +626 -0
- package/src/api/request-output-context.ts +175 -0
- package/src/api/route.test.ts +176 -0
- package/src/api/route.ts +152 -0
- package/src/client/client-builder.test.ts +264 -0
- package/src/client/client-error.test.ts +15 -0
- package/src/client/client-error.ts +141 -0
- package/src/client/client-types.test.ts +493 -0
- package/src/client/client.ssr.test.ts +173 -0
- package/src/client/client.svelte.test.ts +837 -0
- package/src/client/client.svelte.ts +278 -0
- package/src/client/client.test.ts +1690 -0
- package/src/client/client.ts +1035 -0
- package/src/client/component.test.svelte +21 -0
- package/src/client/internal/ndjson-streaming.test.ts +457 -0
- package/src/client/internal/ndjson-streaming.ts +248 -0
- package/src/client/react.test.ts +947 -0
- package/src/client/react.ts +241 -0
- package/src/client/vanilla.test.ts +867 -0
- package/src/client/vanilla.ts +265 -0
- package/src/client/vue.test.ts +754 -0
- package/src/client/vue.ts +242 -0
- package/src/http/http-status.ts +60 -0
- package/src/integrations/astro.ts +17 -0
- package/src/integrations/next-js.ts +31 -0
- package/src/integrations/react-ssr.ts +40 -0
- package/src/integrations/svelte-kit.ts +41 -0
- package/src/mod.ts +20 -0
- package/src/util/async.test.ts +85 -0
- package/src/util/async.ts +96 -0
- package/src/util/content-type.test.ts +136 -0
- package/src/util/content-type.ts +84 -0
- package/src/util/nanostores.test.ts +28 -0
- package/src/util/nanostores.ts +65 -0
- package/src/util/ssr.ts +75 -0
- package/src/util/types-util.ts +16 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +21 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import { test, expect, describe, vi } from "vitest";
|
|
2
|
+
import { RequestOutputContext } from "./request-output-context";
|
|
3
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
4
|
+
import { ResponseStream } from "./internal/response-stream";
|
|
5
|
+
|
|
6
|
+
// Mock schema implementations for testing
|
|
7
|
+
const createMockSchema = (shouldPass: boolean, returnValue?: unknown): StandardSchemaV1 => ({
|
|
8
|
+
"~standard": {
|
|
9
|
+
version: 1,
|
|
10
|
+
vendor: "test",
|
|
11
|
+
validate: async (value: unknown) => {
|
|
12
|
+
if (shouldPass) {
|
|
13
|
+
return { value: returnValue ?? value };
|
|
14
|
+
} else {
|
|
15
|
+
return {
|
|
16
|
+
issues: [
|
|
17
|
+
{
|
|
18
|
+
kind: "validation",
|
|
19
|
+
type: "string",
|
|
20
|
+
input: value,
|
|
21
|
+
expected: "string",
|
|
22
|
+
received: typeof value,
|
|
23
|
+
message: "Expected string",
|
|
24
|
+
path: [],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const mockStringSchema = createMockSchema(true, "validated-string");
|
|
34
|
+
|
|
35
|
+
describe("RequestOutputContext", () => {
|
|
36
|
+
describe("Constructor", () => {
|
|
37
|
+
test("Should create instance without schema", () => {
|
|
38
|
+
const ctx = new RequestOutputContext();
|
|
39
|
+
expect(ctx).toBeInstanceOf(RequestOutputContext);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("Should create instance with schema", () => {
|
|
43
|
+
const ctx = new RequestOutputContext(mockStringSchema);
|
|
44
|
+
expect(ctx).toBeInstanceOf(RequestOutputContext);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("empty() method", () => {
|
|
49
|
+
test("Should return empty response with default 201 status", async () => {
|
|
50
|
+
const ctx = new RequestOutputContext();
|
|
51
|
+
const response = ctx.empty();
|
|
52
|
+
|
|
53
|
+
expect(response).toBeInstanceOf(Response);
|
|
54
|
+
expect(response.status).toBe(201);
|
|
55
|
+
|
|
56
|
+
const body = await response.json();
|
|
57
|
+
expect(body).toBe(null);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("Should return empty response with custom status number", async () => {
|
|
61
|
+
const ctx = new RequestOutputContext();
|
|
62
|
+
const response = ctx.empty(204);
|
|
63
|
+
|
|
64
|
+
expect(response.status).toBe(204);
|
|
65
|
+
|
|
66
|
+
const body = await response.json();
|
|
67
|
+
expect(body).toBe(null);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("Should return empty response with custom headers via second parameter", async () => {
|
|
71
|
+
const ctx = new RequestOutputContext();
|
|
72
|
+
const headers = { "X-Custom": "test-value" };
|
|
73
|
+
const response = ctx.empty(undefined, headers);
|
|
74
|
+
|
|
75
|
+
expect(response.status).toBe(201);
|
|
76
|
+
expect(response.headers.get("X-Custom")).toBe("test-value");
|
|
77
|
+
|
|
78
|
+
const body = await response.json();
|
|
79
|
+
expect(body).toBe(null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("Should return empty response with status and headers via second parameter", async () => {
|
|
83
|
+
const ctx = new RequestOutputContext();
|
|
84
|
+
const headers = { "X-Custom": "test-value" };
|
|
85
|
+
const response = ctx.empty(204, headers);
|
|
86
|
+
|
|
87
|
+
expect(response.status).toBe(204);
|
|
88
|
+
expect(response.headers.get("X-Custom")).toBe("test-value");
|
|
89
|
+
|
|
90
|
+
const body = await response.json();
|
|
91
|
+
expect(body).toBe(null);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("Should return empty response with ResponseInit object", async () => {
|
|
95
|
+
const ctx = new RequestOutputContext();
|
|
96
|
+
const init = {
|
|
97
|
+
status: 204 as const,
|
|
98
|
+
headers: { "X-Custom": "test-value" },
|
|
99
|
+
statusText: "No Content",
|
|
100
|
+
};
|
|
101
|
+
const response = ctx.empty(init);
|
|
102
|
+
|
|
103
|
+
expect(response.status).toBe(204);
|
|
104
|
+
expect(response.headers.get("X-Custom")).toBe("test-value");
|
|
105
|
+
|
|
106
|
+
const body = await response.json();
|
|
107
|
+
expect(body).toBe(null);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("Should handle multiple headers in ResponseInit", async () => {
|
|
111
|
+
const ctx = new RequestOutputContext();
|
|
112
|
+
const init = {
|
|
113
|
+
status: 204 as const,
|
|
114
|
+
headers: {
|
|
115
|
+
"X-Custom": "test-value",
|
|
116
|
+
"X-Another": "another-value",
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const response = ctx.empty(init);
|
|
121
|
+
|
|
122
|
+
expect(response.status).toBe(204);
|
|
123
|
+
expect(response.headers.get("X-Custom")).toBe("test-value");
|
|
124
|
+
expect(response.headers.get("X-Another")).toBe("another-value");
|
|
125
|
+
expect(response.headers.get("Content-Type")).toBe("application/json");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("json() method", () => {
|
|
130
|
+
test("Should return JSON response with default 200 status", async () => {
|
|
131
|
+
const ctx = new RequestOutputContext();
|
|
132
|
+
const data = { message: "test" };
|
|
133
|
+
const response = ctx.json(data);
|
|
134
|
+
|
|
135
|
+
expect(response).toBeInstanceOf(Response);
|
|
136
|
+
expect(response.status).toBe(200);
|
|
137
|
+
|
|
138
|
+
const body = await response.json();
|
|
139
|
+
expect(body).toEqual(data);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("Should return JSON response with custom status number", async () => {
|
|
143
|
+
const ctx = new RequestOutputContext();
|
|
144
|
+
const data = { message: "created" };
|
|
145
|
+
const response = ctx.json(data, 201);
|
|
146
|
+
|
|
147
|
+
expect(response.status).toBe(201);
|
|
148
|
+
|
|
149
|
+
const body = await response.json();
|
|
150
|
+
expect(body).toEqual(data);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("Should have JSON content type by default", async () => {
|
|
154
|
+
const ctx = new RequestOutputContext();
|
|
155
|
+
const data = { message: "test" };
|
|
156
|
+
const response = ctx.json(data);
|
|
157
|
+
expect(response.headers.get("content-type")).toBe("application/json");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("Should return JSON response with custom headers via third parameter", async () => {
|
|
161
|
+
const ctx = new RequestOutputContext();
|
|
162
|
+
const data = { message: "test" };
|
|
163
|
+
const headers = { "X-Custom": "test-value" };
|
|
164
|
+
const response = ctx.json(data, undefined, headers);
|
|
165
|
+
|
|
166
|
+
expect(response.status).toBe(200);
|
|
167
|
+
expect(response.headers.get("X-Custom")).toBe("test-value");
|
|
168
|
+
|
|
169
|
+
const body = await response.json();
|
|
170
|
+
expect(body).toEqual(data);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("Should return JSON response with status and headers via third parameter", async () => {
|
|
174
|
+
const ctx = new RequestOutputContext();
|
|
175
|
+
const data = { message: "test" };
|
|
176
|
+
const headers = { "X-Custom": "test-value" };
|
|
177
|
+
const response = ctx.json(data, 201, headers);
|
|
178
|
+
|
|
179
|
+
expect(response.status).toBe(201);
|
|
180
|
+
expect(response.headers.get("X-Custom")).toBe("test-value");
|
|
181
|
+
|
|
182
|
+
const body = await response.json();
|
|
183
|
+
expect(body).toEqual(data);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("Should return JSON response with ResponseInit object", async () => {
|
|
187
|
+
const ctx = new RequestOutputContext();
|
|
188
|
+
const data = { message: "test" };
|
|
189
|
+
const init = {
|
|
190
|
+
status: 201 as const,
|
|
191
|
+
headers: { "X-Custom": "test-value" },
|
|
192
|
+
statusText: "Created",
|
|
193
|
+
};
|
|
194
|
+
const response = ctx.json(data, init);
|
|
195
|
+
|
|
196
|
+
expect(response.status).toBe(201);
|
|
197
|
+
expect(response.headers.get("X-Custom")).toBe("test-value");
|
|
198
|
+
|
|
199
|
+
const body = await response.json();
|
|
200
|
+
expect(body).toEqual(data);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("Should merge headers when both ResponseInit and headers parameter are provided", async () => {
|
|
204
|
+
const ctx = new RequestOutputContext();
|
|
205
|
+
const data = { message: "test" };
|
|
206
|
+
const init = {
|
|
207
|
+
status: 201 as const,
|
|
208
|
+
headers: { "X-Init": "init-value" },
|
|
209
|
+
};
|
|
210
|
+
const headers = { "X-Param": "param-value" };
|
|
211
|
+
const response = ctx.json(data, init, headers);
|
|
212
|
+
|
|
213
|
+
expect(response.status).toBe(201);
|
|
214
|
+
expect(response.headers.get("X-Init")).toBe("init-value");
|
|
215
|
+
expect(response.headers.get("X-Param")).toBe("param-value");
|
|
216
|
+
|
|
217
|
+
const body = await response.json();
|
|
218
|
+
expect(body).toEqual(data);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("Should override headers when same key exists in both ResponseInit and headers parameter", async () => {
|
|
222
|
+
const ctx = new RequestOutputContext();
|
|
223
|
+
const data = { message: "test" };
|
|
224
|
+
const init = {
|
|
225
|
+
status: 201 as const,
|
|
226
|
+
headers: { "X-Custom": "init-value" },
|
|
227
|
+
};
|
|
228
|
+
const headers = { "X-Custom": "param-value" };
|
|
229
|
+
const response = ctx.json(data, init, headers);
|
|
230
|
+
|
|
231
|
+
expect(response.status).toBe(201);
|
|
232
|
+
// Headers parameter should override ResponseInit headers
|
|
233
|
+
expect(response.headers.get("X-Custom")).toBe("param-value");
|
|
234
|
+
|
|
235
|
+
const body = await response.json();
|
|
236
|
+
expect(body).toEqual(data);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("Should handle null data", async () => {
|
|
240
|
+
const ctx = new RequestOutputContext();
|
|
241
|
+
const response = ctx.json(null);
|
|
242
|
+
|
|
243
|
+
expect(response.status).toBe(200);
|
|
244
|
+
|
|
245
|
+
const body = await response.json();
|
|
246
|
+
expect(body).toBe(null);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("Should handle array data", async () => {
|
|
250
|
+
const ctx = new RequestOutputContext();
|
|
251
|
+
const data = [1, 2, 3];
|
|
252
|
+
const response = ctx.json(data);
|
|
253
|
+
|
|
254
|
+
expect(response.status).toBe(200);
|
|
255
|
+
|
|
256
|
+
const body = await response.json();
|
|
257
|
+
expect(body).toEqual(data);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("Should handle primitive data", async () => {
|
|
261
|
+
const ctx = new RequestOutputContext();
|
|
262
|
+
const response = ctx.json("test string");
|
|
263
|
+
|
|
264
|
+
expect(response.status).toBe(200);
|
|
265
|
+
|
|
266
|
+
const body = await response.json();
|
|
267
|
+
expect(body).toBe("test string");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("Should work with typed schema", async () => {
|
|
271
|
+
const ctx = new RequestOutputContext(mockStringSchema);
|
|
272
|
+
const data = "test string";
|
|
273
|
+
const response = ctx.json(data);
|
|
274
|
+
|
|
275
|
+
expect(response.status).toBe(200);
|
|
276
|
+
|
|
277
|
+
const body = await response.json();
|
|
278
|
+
expect(body).toBe(data);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("stream() method", () => {
|
|
283
|
+
test("Should return streaming response", () => {
|
|
284
|
+
const ctx = new RequestOutputContext();
|
|
285
|
+
const response = ctx.jsonStream(() => {});
|
|
286
|
+
|
|
287
|
+
expect(response).toBeInstanceOf(Response);
|
|
288
|
+
expect(response.body).toBeInstanceOf(ReadableStream);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("Should have chunked transfer encoding by default", async () => {
|
|
292
|
+
const ctx = new RequestOutputContext();
|
|
293
|
+
const response = ctx.jsonStream(() => {});
|
|
294
|
+
expect(response.headers.get("transfer-encoding")).toBe("chunked");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("Should handle callback that writes data", async () => {
|
|
298
|
+
const ctx = new RequestOutputContext();
|
|
299
|
+
const testData = "Hello, World!";
|
|
300
|
+
|
|
301
|
+
const response = ctx.jsonStream(async (stream) => {
|
|
302
|
+
await stream.writeRaw(testData);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const reader = response.body!.getReader();
|
|
306
|
+
const { value } = await reader.read();
|
|
307
|
+
const decoder = new TextDecoder();
|
|
308
|
+
const result = decoder.decode(value);
|
|
309
|
+
|
|
310
|
+
expect(result).toBe(testData);
|
|
311
|
+
|
|
312
|
+
reader.releaseLock();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("Should handle callback that writes multiple chunks", async () => {
|
|
316
|
+
const ctx = new RequestOutputContext();
|
|
317
|
+
|
|
318
|
+
const response = ctx.jsonStream(async (stream) => {
|
|
319
|
+
await stream.writeRaw("Hello, ");
|
|
320
|
+
await stream.writeRaw("World!");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const reader = response.body!.getReader();
|
|
324
|
+
const chunks: string[] = [];
|
|
325
|
+
const decoder = new TextDecoder();
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
while (true) {
|
|
329
|
+
const { done, value } = await reader.read();
|
|
330
|
+
if (done) break;
|
|
331
|
+
chunks.push(decoder.decode(value));
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
// Stream might be closed
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
expect(chunks.join("")).toBe("Hello, World!");
|
|
338
|
+
|
|
339
|
+
reader.releaseLock();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("Should handle callback that uses writeln", async () => {
|
|
343
|
+
const ctx = new RequestOutputContext();
|
|
344
|
+
|
|
345
|
+
const response = ctx.jsonStream(async (stream) => {
|
|
346
|
+
await stream.writeRaw("Line 1\n");
|
|
347
|
+
await stream.writeRaw("Line 2\n");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const reader = response.body!.getReader();
|
|
351
|
+
const chunks: string[] = [];
|
|
352
|
+
const decoder = new TextDecoder();
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
while (true) {
|
|
356
|
+
const { done, value } = await reader.read();
|
|
357
|
+
if (done) break;
|
|
358
|
+
chunks.push(decoder.decode(value));
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
// Stream might be closed
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
expect(chunks.join("")).toBe("Line 1\nLine 2\n");
|
|
365
|
+
|
|
366
|
+
reader.releaseLock();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("Should handle errors in callback", async () => {
|
|
370
|
+
const ctx = new RequestOutputContext();
|
|
371
|
+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
372
|
+
|
|
373
|
+
const _response = ctx.jsonStream(() => {
|
|
374
|
+
throw new Error("Test error");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Wait for async execution
|
|
378
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
379
|
+
|
|
380
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(new Error("Test error"));
|
|
381
|
+
|
|
382
|
+
consoleErrorSpy.mockRestore();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("Should call onError handler when provided", async () => {
|
|
386
|
+
const ctx = new RequestOutputContext();
|
|
387
|
+
const onErrorMock = vi.fn();
|
|
388
|
+
const testError = new Error("Test error");
|
|
389
|
+
|
|
390
|
+
ctx.jsonStream(
|
|
391
|
+
() => {
|
|
392
|
+
throw testError;
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
onError: onErrorMock,
|
|
396
|
+
},
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// Wait for async execution
|
|
400
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
401
|
+
|
|
402
|
+
expect(onErrorMock).toHaveBeenCalledTimes(1);
|
|
403
|
+
expect(onErrorMock).toHaveBeenCalledWith(testError, expect.any(ResponseStream));
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("Should handle undefined error (canceled stream)", async () => {
|
|
407
|
+
const ctx = new RequestOutputContext();
|
|
408
|
+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
409
|
+
|
|
410
|
+
ctx.jsonStream(() => {
|
|
411
|
+
throw undefined;
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Wait for async execution
|
|
415
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
416
|
+
|
|
417
|
+
// Should not call console.error for undefined
|
|
418
|
+
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
|
419
|
+
|
|
420
|
+
consoleErrorSpy.mockRestore();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("Should handle non-Error exceptions without onError handler", async () => {
|
|
424
|
+
const ctx = new RequestOutputContext();
|
|
425
|
+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
426
|
+
|
|
427
|
+
ctx.jsonStream(() => {
|
|
428
|
+
throw "String error";
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Wait for async execution
|
|
432
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
433
|
+
|
|
434
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith("String error");
|
|
435
|
+
|
|
436
|
+
consoleErrorSpy.mockRestore();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("Should handle async callback", async () => {
|
|
440
|
+
const ctx = new RequestOutputContext();
|
|
441
|
+
const testData = "Async data";
|
|
442
|
+
|
|
443
|
+
const response = ctx.jsonStream(async (stream) => {
|
|
444
|
+
await stream.sleep(1);
|
|
445
|
+
await stream.writeRaw(testData);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const reader = response.body!.getReader();
|
|
449
|
+
const { value } = await reader.read();
|
|
450
|
+
const decoder = new TextDecoder();
|
|
451
|
+
const result = decoder.decode(value);
|
|
452
|
+
|
|
453
|
+
expect(result).toBe(testData);
|
|
454
|
+
|
|
455
|
+
reader.releaseLock();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("Should handle stream abort", async () => {
|
|
459
|
+
const ctx = new RequestOutputContext();
|
|
460
|
+
let streamRef: ResponseStream<unknown> | undefined;
|
|
461
|
+
|
|
462
|
+
const _response = ctx.jsonStream((stream) => {
|
|
463
|
+
streamRef = stream;
|
|
464
|
+
stream.onAbort(() => {
|
|
465
|
+
// Abort handler
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Wait for setup
|
|
470
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
471
|
+
|
|
472
|
+
expect(streamRef!.aborted).toBe(false);
|
|
473
|
+
|
|
474
|
+
streamRef!.abort();
|
|
475
|
+
|
|
476
|
+
expect(streamRef!.aborted).toBe(true);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("Should close stream after callback execution", async () => {
|
|
480
|
+
const ctx = new RequestOutputContext();
|
|
481
|
+
let streamRef: ResponseStream<unknown> | undefined;
|
|
482
|
+
|
|
483
|
+
ctx.jsonStream((stream) => {
|
|
484
|
+
streamRef = stream;
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Wait for execution and cleanup
|
|
488
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
489
|
+
|
|
490
|
+
expect(streamRef!.closed).toBe(true);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("Should handle Uint8Array data", async () => {
|
|
494
|
+
const ctx = new RequestOutputContext();
|
|
495
|
+
const testData = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
|
|
496
|
+
|
|
497
|
+
const response = ctx.jsonStream(async (stream) => {
|
|
498
|
+
await stream.writeRaw(testData);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const reader = response.body!.getReader();
|
|
502
|
+
const { value } = await reader.read();
|
|
503
|
+
|
|
504
|
+
expect(value).toEqual(testData);
|
|
505
|
+
|
|
506
|
+
reader.releaseLock();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("Should be able to override the content type", async () => {
|
|
510
|
+
const ctx = new RequestOutputContext();
|
|
511
|
+
const response = ctx.jsonStream(() => {}, {
|
|
512
|
+
headers: { "content-type": "application/octet-stream" },
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
expect(response.headers.get("content-type")).toBe("application/octet-stream");
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe("Type inference with schema", () => {
|
|
520
|
+
test("Should work with typed schema for json method", async () => {
|
|
521
|
+
// This test mainly checks that TypeScript compilation works correctly
|
|
522
|
+
const ctx = new RequestOutputContext(mockStringSchema);
|
|
523
|
+
|
|
524
|
+
// With schema, the json method should expect the inferred type
|
|
525
|
+
const response = ctx.json("test string");
|
|
526
|
+
|
|
527
|
+
expect(response.status).toBe(200);
|
|
528
|
+
|
|
529
|
+
const body = await response.json();
|
|
530
|
+
expect(body).toBe("test string");
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("Should work without schema (unknown type)", async () => {
|
|
534
|
+
const ctx = new RequestOutputContext();
|
|
535
|
+
|
|
536
|
+
// Without schema, json method accepts any type
|
|
537
|
+
const response1 = ctx.json({ any: "object" });
|
|
538
|
+
const response2 = ctx.json("string");
|
|
539
|
+
const response3 = ctx.json(123);
|
|
540
|
+
const response4 = ctx.json([1, 2, 3]);
|
|
541
|
+
|
|
542
|
+
expect(response1.status).toBe(200);
|
|
543
|
+
expect(response2.status).toBe(200);
|
|
544
|
+
expect(response3.status).toBe(200);
|
|
545
|
+
expect(response4.status).toBe(200);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
describe("Edge cases", () => {
|
|
550
|
+
test("Should handle empty object in json", async () => {
|
|
551
|
+
const ctx = new RequestOutputContext();
|
|
552
|
+
const response = ctx.json({});
|
|
553
|
+
|
|
554
|
+
expect(response.status).toBe(200);
|
|
555
|
+
|
|
556
|
+
const body = await response.json();
|
|
557
|
+
expect(body).toEqual({});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("Should handle complex nested object in json", async () => {
|
|
561
|
+
const ctx = new RequestOutputContext();
|
|
562
|
+
const data = {
|
|
563
|
+
user: {
|
|
564
|
+
id: 1,
|
|
565
|
+
name: "John",
|
|
566
|
+
preferences: {
|
|
567
|
+
theme: "dark",
|
|
568
|
+
notifications: true,
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
items: [1, 2, 3],
|
|
572
|
+
};
|
|
573
|
+
const response = ctx.json(data);
|
|
574
|
+
|
|
575
|
+
expect(response.status).toBe(200);
|
|
576
|
+
|
|
577
|
+
const body = await response.json();
|
|
578
|
+
expect(body).toEqual(data);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("Should handle Headers object in ResponseInit", async () => {
|
|
582
|
+
const ctx = new RequestOutputContext();
|
|
583
|
+
const headers = new Headers();
|
|
584
|
+
headers.set("X-Custom", "test-value");
|
|
585
|
+
headers.set("Content-Type", "application/json");
|
|
586
|
+
|
|
587
|
+
const init = {
|
|
588
|
+
status: 201 as const,
|
|
589
|
+
headers,
|
|
590
|
+
};
|
|
591
|
+
const response = ctx.json({ message: "test" }, init);
|
|
592
|
+
|
|
593
|
+
expect(response.status).toBe(201);
|
|
594
|
+
expect(response.headers.get("X-Custom")).toBe("test-value");
|
|
595
|
+
expect(response.headers.get("Content-Type")).toBe("application/json");
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("Should handle Headers object merging", async () => {
|
|
599
|
+
const ctx = new RequestOutputContext();
|
|
600
|
+
const initHeaders = new Headers();
|
|
601
|
+
initHeaders.set("X-Init", "init-value");
|
|
602
|
+
|
|
603
|
+
const init = {
|
|
604
|
+
status: 201 as const,
|
|
605
|
+
headers: initHeaders,
|
|
606
|
+
};
|
|
607
|
+
const paramHeaders = { "X-Param": "param-value" };
|
|
608
|
+
const response = ctx.json({ message: "test" }, init, paramHeaders);
|
|
609
|
+
|
|
610
|
+
expect(response.status).toBe(201);
|
|
611
|
+
expect(response.headers.get("X-Init")).toBe("init-value");
|
|
612
|
+
expect(response.headers.get("X-Param")).toBe("param-value");
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("Should handle stream with immediate close", async () => {
|
|
616
|
+
const ctx = new RequestOutputContext();
|
|
617
|
+
|
|
618
|
+
const response = ctx.jsonStream((stream) => {
|
|
619
|
+
stream.close();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
expect(response).toBeInstanceOf(Response);
|
|
623
|
+
expect(response.body).toBeInstanceOf(ReadableStream);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
});
|