@dcc-bs/communication.bs.js 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/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # communication.bs.js
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.2.9. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
package/biome.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
3
+ "vcs": {
4
+ "enabled": false,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": false
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false
10
+ },
11
+ "formatter": {
12
+ "enabled": true,
13
+ "indentStyle": "space",
14
+ "indentWidth": 4
15
+ },
16
+ "linter": {
17
+ "enabled": true,
18
+ "rules": {
19
+ "recommended": true
20
+ }
21
+ },
22
+ "javascript": {
23
+ "formatter": {
24
+ "quoteStyle": "double"
25
+ }
26
+ },
27
+ "assist": {
28
+ "enabled": true,
29
+ "actions": {
30
+ "source": {
31
+ "organizeImports": "on"
32
+ }
33
+ }
34
+ }
35
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@dcc-bs/communication.bs.js",
3
+ "module": "index.ts",
4
+ "type": "module",
5
+ "version": "0.0.1",
6
+ "scripts": {
7
+ "check": "biome check --write",
8
+ "build": "vite build",
9
+ "test": "vitest"
10
+ },
11
+ "devDependencies": {
12
+ "@biomejs/biome": "2.2.4",
13
+ "@types/bun": "latest",
14
+ "vitest": "^4.0.13"
15
+ },
16
+ "peerDependencies": {
17
+ "typescript": "^5"
18
+ },
19
+ "dependencies": {
20
+ "vite": "^7.1.6",
21
+ "vue": "^3.5.25",
22
+ "zod": "^4.1.9"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ }
27
+ }
@@ -0,0 +1,208 @@
1
+ import type { ZodType, z } from "zod";
2
+ import { ApiError } from "./models/api_error";
3
+ import { ApiErrorResponse } from "./models/api_error_response.js";
4
+ import type {
5
+ ApiFetchOptions,
6
+ ApiFetchOptionsWithSchema,
7
+ } from "./models/api_fetch_options.js";
8
+
9
+ export type ErrorId = "unexpected_error" | "fetch_failed" | string;
10
+ export type ApiResponse<T> = T | ApiError;
11
+
12
+ export function isApiError(response: unknown): response is ApiError {
13
+ return (
14
+ typeof response === "object" &&
15
+ response !== null &&
16
+ "$type" in response &&
17
+ response.$type === "ApiError"
18
+ );
19
+ }
20
+
21
+ async function extractApiError(response: Response): Promise<ApiError> {
22
+ try {
23
+ const data = ApiErrorResponse.parse(await response.json());
24
+
25
+ return new ApiError(
26
+ data.errorId,
27
+ response.status,
28
+ data.debugMessage ?? JSON.stringify(data),
29
+ );
30
+ } catch (_) {
31
+ return new ApiError("unexpected_error", response.status);
32
+ }
33
+ }
34
+
35
+ async function extractApiErrorFormError(error: unknown) {
36
+ if (error instanceof ApiError) {
37
+ return error;
38
+ }
39
+
40
+ if (
41
+ error &&
42
+ typeof error === "object" &&
43
+ "name" in error &&
44
+ error.name === "AbortError"
45
+ ) {
46
+ return new ApiError("request_aborted", 499);
47
+ }
48
+
49
+ if (
50
+ error &&
51
+ typeof error === "object" &&
52
+ "cause" in error &&
53
+ error.cause === "aborted"
54
+ ) {
55
+ return new ApiError("request_aborted", 499);
56
+ }
57
+
58
+ if (typeof error === "string" && error === "aborted") {
59
+ return new ApiError("request_aborted", 499);
60
+ }
61
+
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ return new ApiError("fetch_failed", 500, message);
64
+ }
65
+
66
+ async function _fetch(url: string, options: ApiFetchOptions) {
67
+ const isFormData = options.body instanceof FormData;
68
+
69
+ // Define headers with an index signature so properties are optional
70
+ const headers: Record<string, string> = {
71
+ "Content-Type": "application/json",
72
+ ...(options.headers as Record<string, string>),
73
+ };
74
+
75
+ // Remove Content-Type if body is FormData
76
+ if (isFormData) {
77
+ delete headers["Content-Type"];
78
+ }
79
+
80
+ return await fetch(url, {
81
+ ...options,
82
+ body: isFormData
83
+ ? (options.body as FormData)
84
+ : JSON.stringify(options.body),
85
+ headers: headers,
86
+ });
87
+ }
88
+
89
+ // overloads
90
+ export async function apiFetch<T extends ZodType>(
91
+ url: string,
92
+ options: ApiFetchOptionsWithSchema<T>,
93
+ ): Promise<ApiResponse<z.infer<T>>>;
94
+ export async function apiFetch<T extends object>(
95
+ url: string,
96
+ options?: ApiFetchOptions,
97
+ ): Promise<ApiResponse<T>>;
98
+
99
+ // implementation
100
+ export async function apiFetch(
101
+ url: string,
102
+ options?: ApiFetchOptions | ApiFetchOptionsWithSchema<ZodType>,
103
+ ): Promise<ApiResponse<unknown>> {
104
+ try {
105
+ const response = await _fetch(url, options ?? {});
106
+
107
+ if (response.ok) {
108
+ const data = await response.json();
109
+ if (options && "schema" in options) {
110
+ const schema = options.schema as ZodType;
111
+
112
+ const parsed = schema.safeParse(data);
113
+
114
+ if (!parsed.success) {
115
+ return new ApiError(
116
+ "schema_validation_failed",
117
+ 500,
118
+ `Response validation failed: ${parsed.error.message}`,
119
+ );
120
+ }
121
+
122
+ return parsed.data;
123
+ }
124
+
125
+ return data;
126
+ }
127
+
128
+ return extractApiError(response);
129
+ } catch (error) {
130
+ return extractApiErrorFormError(error);
131
+ }
132
+ }
133
+
134
+ export async function apiStreamFetch(
135
+ url: string,
136
+ options?: ApiFetchOptions,
137
+ ): Promise<ApiResponse<ReadableStream<Uint8Array>>> {
138
+ try {
139
+ const response = await _fetch(url, options ?? {});
140
+
141
+ if (response.ok) {
142
+ return response.body as ReadableStream<Uint8Array>;
143
+ }
144
+
145
+ return extractApiError(response);
146
+ } catch (error) {
147
+ return extractApiErrorFormError(error);
148
+ }
149
+ }
150
+
151
+ export async function* apiFetchTextMany(
152
+ url: string,
153
+ options?: ApiFetchOptions,
154
+ ): AsyncGenerator<string, void, void> {
155
+ try {
156
+ const response = await _fetch(url, options ?? {});
157
+
158
+ if (response.ok) {
159
+ const reader = response.body?.getReader();
160
+ const decoder = new TextDecoder();
161
+
162
+ if (!reader) {
163
+ throw new ApiError(
164
+ "unexpected_error",
165
+ 500,
166
+ "Response body is null",
167
+ );
168
+ }
169
+
170
+ while (true) {
171
+ const { done, value } = await reader.read();
172
+ if (done) break;
173
+ const chunk = decoder.decode(value, {
174
+ stream: true,
175
+ });
176
+ yield chunk;
177
+ }
178
+ } else {
179
+ throw extractApiError(response);
180
+ }
181
+ } catch (error) {
182
+ throw extractApiErrorFormError(error);
183
+ }
184
+ }
185
+
186
+ export async function* apiFetchMany<T extends ZodType>(
187
+ url: string,
188
+ options: ApiFetchOptionsWithSchema<T>,
189
+ ): AsyncGenerator<z.infer<T>, void, void> {
190
+ try {
191
+ for await (const chunk of apiFetchTextMany(url, options)) {
192
+ const json = JSON.parse(chunk);
193
+ const parsed = options.schema.safeParse(json);
194
+
195
+ if (!parsed.success) {
196
+ throw new ApiError(
197
+ "schema_validation_failed",
198
+ 500,
199
+ `Chunk validation failed: ${parsed.error.message}`,
200
+ );
201
+ }
202
+
203
+ yield parsed.data;
204
+ }
205
+ } catch (error) {
206
+ throw extractApiErrorFormError(error);
207
+ }
208
+ }
@@ -0,0 +1,43 @@
1
+ import { type Ref, ref } from "vue";
2
+ import { apiFetch, isApiError } from "../apiFetch";
3
+ import type { ApiErrorResponse } from "../models/api_error_response";
4
+ import type { ApiFetchOptions } from "../models/api_fetch_options";
5
+
6
+ export type UseApiFetchOutput<T> = {
7
+ data: Ref<T | undefined>;
8
+ error: Ref<ApiErrorResponse | undefined>;
9
+ pending: Ref<boolean>;
10
+ };
11
+
12
+ export function useApiFetch<T extends object>(
13
+ url: string,
14
+ options?: ApiFetchOptions,
15
+ ) {
16
+ const data = ref<T>();
17
+ const error = ref<ApiErrorResponse>();
18
+ const pending = ref(true);
19
+
20
+ apiFetch<T>(url, options)
21
+ .then((response) => {
22
+ if (isApiError(response)) {
23
+ error.value = response;
24
+ } else {
25
+ data.value = response;
26
+ }
27
+ })
28
+ .catch((err) => {
29
+ error.value = {
30
+ errorId: "fetch_failed",
31
+ debugMessage: String(err),
32
+ };
33
+ })
34
+ .finally(() => {
35
+ pending.value = false;
36
+ });
37
+
38
+ return {
39
+ data: data,
40
+ error: error,
41
+ pending,
42
+ };
43
+ }
@@ -0,0 +1,39 @@
1
+ import { ref } from "vue";
2
+ import type { ZodType, z } from "zod";
3
+ import { apiFetch, isApiError } from "../apiFetch";
4
+ import type { ApiErrorResponse } from "../models/api_error_response";
5
+ import type { ApiFetchOptionsWithSchema } from "../models/api_fetch_options";
6
+ import type { UseApiFetchOutput } from "./apiFetch.composable";
7
+
8
+ export function useApiFetchWithSchema<T extends ZodType>(
9
+ url: string,
10
+ options: ApiFetchOptionsWithSchema<T>,
11
+ ): UseApiFetchOutput<z.infer<T>> {
12
+ const data = ref<z.infer<T>>();
13
+ const error = ref<ApiErrorResponse>();
14
+ const pending = ref(true);
15
+
16
+ apiFetch(url, options)
17
+ .then((response) => {
18
+ if (isApiError(response)) {
19
+ error.value = response;
20
+ } else {
21
+ data.value = response;
22
+ }
23
+ })
24
+ .catch((err) => {
25
+ error.value = {
26
+ errorId: "fetch_failed",
27
+ debugMessage: String(err),
28
+ };
29
+ })
30
+ .finally(() => {
31
+ pending.value = false;
32
+ });
33
+
34
+ return {
35
+ data,
36
+ error,
37
+ pending,
38
+ };
39
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export {
2
+ apiFetch,
3
+ apiFetchMany,
4
+ apiFetchTextMany,
5
+ apiStreamFetch,
6
+ isApiError,
7
+ } from "./apiFetch";
8
+ export { ApiError } from "./models/api_error";
9
+ export type { ApiErrorResponse } from "./models/api_error_response";
10
+ export type {
11
+ ApiFetchOptions,
12
+ ApiFetchOptionsWithSchema,
13
+ } from "./models/api_fetch_options";
@@ -0,0 +1,15 @@
1
+ import type { ErrorId } from "../apiFetch";
2
+
3
+ export class ApiError extends Error {
4
+ $type = "ApiError" as const;
5
+ errorId: ErrorId;
6
+ debugMessage?: string;
7
+ status: number;
8
+
9
+ constructor(errorId: ErrorId, status: number, debugMessage?: string) {
10
+ super(`API Error: ${errorId} (status: ${status})`);
11
+ this.errorId = errorId;
12
+ this.status = status;
13
+ this.debugMessage = debugMessage;
14
+ }
15
+ }
@@ -0,0 +1,8 @@
1
+ import zod from "zod";
2
+
3
+ export const ApiErrorResponse = zod.object({
4
+ errorId: zod.string().optional().default("unexpected_error"),
5
+ debugMessage: zod.string().optional(),
6
+ });
7
+
8
+ export type ApiErrorResponse = zod.infer<typeof ApiErrorResponse>;
@@ -0,0 +1,9 @@
1
+ import type { ZodType } from "zod";
2
+
3
+ export type ApiFetchOptions = Omit<RequestInit, "body"> & {
4
+ body?: object | FormData;
5
+ };
6
+
7
+ export type ApiFetchOptionsWithSchema<T extends ZodType> = ApiFetchOptions & {
8
+ schema: T;
9
+ };
@@ -0,0 +1,148 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { z } from "zod";
3
+ import { apiFetch } from "../src";
4
+
5
+ type User = {
6
+ id: string;
7
+ name: string;
8
+ };
9
+
10
+ const UserSchema = z.object({
11
+ id: z.string(),
12
+ name: z.string(),
13
+ });
14
+
15
+ describe("apiFetch", () => {
16
+ test("success fetch", async () => {
17
+ const dummyBody: User = { id: "1", name: "Alice" };
18
+
19
+ vi.stubGlobal(
20
+ "fetch",
21
+ vi.fn().mockResolvedValue({
22
+ ok: true,
23
+ status: 200,
24
+ json: () => Promise.resolve(dummyBody),
25
+ } as Response),
26
+ );
27
+
28
+ const response = await apiFetch<User>("https://api.example.com/user/1");
29
+
30
+ expect(response).toEqual(dummyBody);
31
+ });
32
+
33
+ test("error fetch", async () => {
34
+ const dummyError = {
35
+ errorId: "not_found",
36
+ debugMessage: "User not found",
37
+ };
38
+
39
+ vi.stubGlobal(
40
+ "fetch",
41
+ vi.fn().mockResolvedValue({
42
+ ok: false,
43
+ status: 404,
44
+ json: () => Promise.resolve(dummyError),
45
+ } as Response),
46
+ );
47
+
48
+ const response = await apiFetch<User>(
49
+ "https://api.example.com/user/999",
50
+ );
51
+
52
+ expect(response).toHaveProperty("errorId", "not_found");
53
+ expect(response).toHaveProperty("debugMessage", "User not found");
54
+ });
55
+
56
+ test("network error fetch", async () => {
57
+ vi.stubGlobal(
58
+ "fetch",
59
+ vi.fn().mockRejectedValue(new Error("Network error")),
60
+ );
61
+
62
+ const response = await apiFetch<User>("https://api.example.com/user/1");
63
+
64
+ expect(response).toHaveProperty("errorId", "fetch_failed");
65
+ expect(response).toHaveProperty("debugMessage", "Network error");
66
+ });
67
+ });
68
+
69
+ describe("apiFetch with schema", () => {
70
+ test("success fetch", async () => {
71
+ const dummyBody = { id: "1", name: "Alice" };
72
+
73
+ vi.stubGlobal(
74
+ "fetch",
75
+ vi.fn().mockResolvedValue({
76
+ ok: true,
77
+ status: 200,
78
+ json: () => Promise.resolve(dummyBody),
79
+ } as Response),
80
+ );
81
+
82
+ const response = await apiFetch("https://api.example.com/user/1", {
83
+ schema: UserSchema,
84
+ });
85
+
86
+ expect(response).toEqual(dummyBody);
87
+ });
88
+
89
+ test("invalid data", async () => {
90
+ const invalidBody = { id: 1, fullName: "Alice" };
91
+
92
+ vi.stubGlobal(
93
+ "fetch",
94
+ vi.fn().mockResolvedValue({
95
+ ok: true,
96
+ status: 200,
97
+ json: () => Promise.resolve(invalidBody),
98
+ } as Response),
99
+ );
100
+
101
+ const response = await apiFetch("https://api.example.com/user/1", {
102
+ schema: UserSchema,
103
+ });
104
+
105
+ expect(response).toHaveProperty("errorId", "schema_validation_failed");
106
+ expect(response).toHaveProperty(
107
+ "debugMessage",
108
+ expect.stringContaining("Invalid input"),
109
+ );
110
+ });
111
+
112
+ test("error fetch", async () => {
113
+ const dummyError = {
114
+ errorId: "not_found",
115
+ debugMessage: "User not found",
116
+ };
117
+
118
+ vi.stubGlobal(
119
+ "fetch",
120
+ vi.fn().mockResolvedValue({
121
+ ok: false,
122
+ status: 404,
123
+ json: () => Promise.resolve(dummyError),
124
+ } as Response),
125
+ );
126
+
127
+ const response = await apiFetch("https://api.example.com/user/999", {
128
+ schema: UserSchema,
129
+ });
130
+
131
+ expect(response).toHaveProperty("errorId", "not_found");
132
+ expect(response).toHaveProperty("debugMessage", "User not found");
133
+ });
134
+
135
+ test("network error fetch", async () => {
136
+ vi.stubGlobal(
137
+ "fetch",
138
+ vi.fn().mockRejectedValue(new Error("Network error")),
139
+ );
140
+
141
+ const response = await apiFetch("https://api.example.com/user/1", {
142
+ schema: UserSchema,
143
+ });
144
+
145
+ expect(response).toHaveProperty("errorId", "fetch_failed");
146
+ expect(response).toHaveProperty("debugMessage", "Network error");
147
+ });
148
+ });
@@ -0,0 +1,222 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { z } from "zod";
3
+ import { apiFetchMany, apiFetchTextMany, isApiError } from "../src";
4
+
5
+ const MessageSchema = z.object({
6
+ id: z.number(),
7
+ text: z.string(),
8
+ });
9
+
10
+ describe("apiFetchTextMany", () => {
11
+ test("success text many fetch", async () => {
12
+ const chunks = ["Hello", " ", "World"];
13
+
14
+ const mockStream = new ReadableStream({
15
+ start(controller) {
16
+ for (const chunk of chunks) {
17
+ controller.enqueue(new TextEncoder().encode(chunk));
18
+ }
19
+ controller.close();
20
+ },
21
+ });
22
+
23
+ vi.stubGlobal(
24
+ "fetch",
25
+ vi.fn().mockResolvedValue({
26
+ ok: true,
27
+ status: 200,
28
+ body: mockStream,
29
+ } as Response),
30
+ );
31
+
32
+ const result: string[] = [];
33
+ for await (const chunk of apiFetchTextMany(
34
+ "https://api.example.com/stream",
35
+ )) {
36
+ if (isApiError(chunk)) {
37
+ throw new Error("Should not return error");
38
+ }
39
+ result.push(chunk);
40
+ }
41
+
42
+ expect(result).toEqual(chunks);
43
+ });
44
+
45
+ test("error text many fetch", async () => {
46
+ const dummyError = {
47
+ errorId: "not_found",
48
+ debugMessage: "Stream not found",
49
+ };
50
+
51
+ vi.stubGlobal(
52
+ "fetch",
53
+ vi.fn().mockResolvedValue({
54
+ ok: false,
55
+ status: 404,
56
+ json: () => Promise.resolve(dummyError),
57
+ } as Response),
58
+ );
59
+
60
+ const generator = apiFetchTextMany(
61
+ "https://api.example.com/stream/999",
62
+ );
63
+
64
+ await expect(async () => {
65
+ for await (const _chunk of generator) {
66
+ // Should throw error
67
+ }
68
+ }).rejects.toThrow();
69
+ });
70
+
71
+ test("network error text many fetch", async () => {
72
+ vi.stubGlobal(
73
+ "fetch",
74
+ vi.fn().mockRejectedValue(new Error("Network error")),
75
+ );
76
+
77
+ const generator = apiFetchTextMany("https://api.example.com/stream");
78
+
79
+ await expect(async () => {
80
+ for await (const _chunk of generator) {
81
+ // Should throw error
82
+ }
83
+ }).rejects.toThrow();
84
+ });
85
+
86
+ test("null body error", async () => {
87
+ vi.stubGlobal(
88
+ "fetch",
89
+ vi.fn().mockResolvedValue({
90
+ ok: true,
91
+ status: 200,
92
+ body: null,
93
+ } as Response),
94
+ );
95
+
96
+ const generator = apiFetchTextMany("https://api.example.com/stream");
97
+
98
+ await expect(async () => {
99
+ for await (const _chunk of generator) {
100
+ // Should throw error for null body
101
+ }
102
+ }).rejects.toThrow();
103
+ });
104
+ });
105
+
106
+ describe("apiFetchMany", () => {
107
+ test("success fetch many", async () => {
108
+ const messages = [
109
+ { id: 1, text: "First" },
110
+ { id: 2, text: "Second" },
111
+ { id: 3, text: "Third" },
112
+ ];
113
+
114
+ const mockStream = new ReadableStream({
115
+ start(controller) {
116
+ for (const message of messages) {
117
+ controller.enqueue(
118
+ new TextEncoder().encode(
119
+ `${JSON.stringify(message)}\n`,
120
+ ),
121
+ );
122
+ }
123
+ controller.close();
124
+ },
125
+ });
126
+
127
+ vi.stubGlobal(
128
+ "fetch",
129
+ vi.fn().mockResolvedValue({
130
+ ok: true,
131
+ status: 200,
132
+ body: mockStream,
133
+ } as Response),
134
+ );
135
+
136
+ const result = [];
137
+ const generator = apiFetchMany("https://api.example.com/stream", {
138
+ schema: MessageSchema,
139
+ });
140
+
141
+ for await (const chunk of generator) {
142
+ result.push(chunk);
143
+ }
144
+
145
+ expect(result).toEqual(messages);
146
+ });
147
+
148
+ test("error fetch many", async () => {
149
+ const dummyError = {
150
+ errorId: "not_found",
151
+ debugMessage: "Stream not found",
152
+ };
153
+
154
+ vi.stubGlobal(
155
+ "fetch",
156
+ vi.fn().mockResolvedValue({
157
+ ok: false,
158
+ status: 404,
159
+ json: () => Promise.resolve(dummyError),
160
+ } as Response),
161
+ );
162
+
163
+ const generator = apiFetchMany("https://api.example.com/stream/999", {
164
+ schema: MessageSchema,
165
+ });
166
+
167
+ await expect(async () => {
168
+ for await (const _chunk of generator) {
169
+ // Should throw error
170
+ }
171
+ }).rejects.toThrow();
172
+ });
173
+
174
+ test("network error fetch many", async () => {
175
+ vi.stubGlobal(
176
+ "fetch",
177
+ vi.fn().mockRejectedValue(new Error("Network error")),
178
+ );
179
+
180
+ const generator = apiFetchMany("https://api.example.com/stream", {
181
+ schema: MessageSchema,
182
+ });
183
+
184
+ await expect(async () => {
185
+ for await (const _chunk of generator) {
186
+ // Should throw error
187
+ }
188
+ }).rejects.toThrow();
189
+ });
190
+
191
+ test("invalid schema data", async () => {
192
+ const invalidData = [{ id: "not-a-number", text: "First" }];
193
+
194
+ const mockStream = new ReadableStream({
195
+ start(controller) {
196
+ controller.enqueue(
197
+ new TextEncoder().encode(JSON.stringify(invalidData[0])),
198
+ );
199
+ controller.close();
200
+ },
201
+ });
202
+
203
+ vi.stubGlobal(
204
+ "fetch",
205
+ vi.fn().mockResolvedValue({
206
+ ok: true,
207
+ status: 200,
208
+ body: mockStream,
209
+ } as Response),
210
+ );
211
+
212
+ const generator = apiFetchMany("https://api.example.com/stream", {
213
+ schema: MessageSchema,
214
+ });
215
+
216
+ await expect(async () => {
217
+ for await (const _chunk of generator) {
218
+ // Should throw validation error
219
+ }
220
+ }).rejects.toThrow();
221
+ });
222
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { apiStreamFetch, isApiError } from "../src";
3
+
4
+ describe("apiStreamFetch", () => {
5
+ test("success stream fetch", async () => {
6
+ const mockStream = new ReadableStream({
7
+ start(controller) {
8
+ controller.enqueue(new Uint8Array([1, 2, 3]));
9
+ controller.close();
10
+ },
11
+ });
12
+
13
+ vi.stubGlobal(
14
+ "fetch",
15
+ vi.fn().mockResolvedValue({
16
+ ok: true,
17
+ status: 200,
18
+ body: mockStream,
19
+ } as Response),
20
+ );
21
+
22
+ const response = await apiStreamFetch("https://api.example.com/stream");
23
+
24
+ expect(isApiError(response)).toBe(false);
25
+ expect(response).toBeInstanceOf(ReadableStream);
26
+
27
+ // Verify we can read from the stream
28
+ const reader = (response as ReadableStream<Uint8Array>).getReader();
29
+ const { value, done } = await reader.read();
30
+ expect(value).toEqual(new Uint8Array([1, 2, 3]));
31
+ expect(done).toBe(false);
32
+ });
33
+
34
+ test("error stream fetch", async () => {
35
+ const dummyError = {
36
+ errorId: "not_found",
37
+ debugMessage: "Stream not found",
38
+ };
39
+
40
+ vi.stubGlobal(
41
+ "fetch",
42
+ vi.fn().mockResolvedValue({
43
+ ok: false,
44
+ status: 404,
45
+ json: () => Promise.resolve(dummyError),
46
+ } as Response),
47
+ );
48
+
49
+ const response = await apiStreamFetch(
50
+ "https://api.example.com/stream/999",
51
+ );
52
+
53
+ expect(isApiError(response)).toBe(true);
54
+ expect(response).toHaveProperty("errorId", "not_found");
55
+ expect(response).toHaveProperty("debugMessage", "Stream not found");
56
+ });
57
+
58
+ test("network error stream fetch", async () => {
59
+ vi.stubGlobal(
60
+ "fetch",
61
+ vi.fn().mockRejectedValue(new Error("Network error")),
62
+ );
63
+
64
+ const response = await apiStreamFetch("https://api.example.com/stream");
65
+
66
+ expect(isApiError(response)).toBe(true);
67
+ expect(response).toHaveProperty("errorId", "fetch_failed");
68
+ expect(response).toHaveProperty("debugMessage", "Network error");
69
+ });
70
+ });
@@ -0,0 +1,14 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { ApiError, isApiError } from "../src";
3
+
4
+ describe("isApiError", () => {
5
+ test("identifies ApiError instances correctly", () => {
6
+ const apiError = new ApiError("not_found", 404, "User not found");
7
+ const nonApiError = new Error("Some other error");
8
+ const someString = "Just a string";
9
+
10
+ expect(isApiError(apiError)).toBe(true);
11
+ expect(isApiError(nonApiError)).toBe(false);
12
+ expect(isApiError(someString)).toBe(false);
13
+ });
14
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "ESNext",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+
23
+ // Some stricter flags (disabled by default)
24
+ "noUnusedLocals": false,
25
+ "noUnusedParameters": false,
26
+ "noPropertyAccessFromIndexSignature": false
27
+ }
28
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { defineConfig } from "vite";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
7
+ export default defineConfig({
8
+ build: {
9
+ lib: {
10
+ entry: resolve(__dirname, "src/index.ts"),
11
+ name: "@dcc-bs/commmunication.bs.js",
12
+ formats: ["es", "umd"], // Specify browser-compatible formats
13
+ },
14
+ // Include CSS in the build
15
+ cssCodeSplit: false,
16
+ // Copy CSS files to dist
17
+ copyPublicDir: false,
18
+ target: "esnext", // Target modern browsers
19
+ minify: true,
20
+ },
21
+ define: {
22
+ // Ensure browser environment
23
+ "process.env.NODE_ENV": JSON.stringify("production"),
24
+ },
25
+ });