@resq-sw/http 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # @resq-sw/http
2
+
3
+ > Effect-based HTTP client with retry, timeout, schema validation, and security middleware.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @resq-sw/http effect
9
+ ```
10
+
11
+ Peer dependency: `effect`.
12
+
13
+ ## Quick Start
14
+
15
+ ```ts
16
+ import { get, post } from "@resq-sw/http";
17
+ import { Effect } from "effect";
18
+ import { HttpClient } from "effect/unstable/http";
19
+
20
+ const program = Effect.gen(function* () {
21
+ const users = yield* get<User[]>("/api/users");
22
+ const created = yield* post<User>("/api/users", { name: "Alice" });
23
+ return { users, created };
24
+ });
25
+
26
+ // Run with the default HTTP client
27
+ Effect.runPromise(program.pipe(Effect.provide(HttpClient.layer)));
28
+ ```
29
+
30
+ ## API Reference
31
+
32
+ ### `fetcher(url, method?, options?, params?, body?)`
33
+
34
+ Core HTTP function. All convenience methods delegate to this.
35
+
36
+ | Parameter | Type | Default | Description |
37
+ |-----------|------|---------|-------------|
38
+ | `url` | `string` | required | URL path or absolute URL |
39
+ | `method` | `HttpMethod` | `"GET"` | HTTP method |
40
+ | `options` | `FetcherOptions<T>` | `{}` | Request options |
41
+ | `params` | `QueryParams` | -- | Query parameters |
42
+ | `body` | `RequestBody` | -- | Request body (POST/PUT/PATCH only) |
43
+
44
+ Returns `Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>`.
45
+
46
+ URL resolution: absolute URLs are used as-is; relative paths are prefixed with `VITE_SITE_URL`, `SITE_URL`, or `http://localhost:5173`.
47
+
48
+ ### FetcherOptions
49
+
50
+ | Option | Type | Default | Description |
51
+ |--------|------|---------|-------------|
52
+ | `retries` | `number` | `0` | Retry count on failure |
53
+ | `retryDelay` | `number` | `1000` | Base delay between retries (exponential backoff) |
54
+ | `timeout` | `number` | `10000` | Request timeout in ms |
55
+ | `headers` | `Record<string, string>` | `{}` | Additional request headers |
56
+ | `schema` | `Schema.Schema<T>` | -- | Effect Schema for response validation |
57
+ | `onError` | `(error: unknown) => void` | -- | Error callback |
58
+ | `signal` | `AbortSignal` | -- | Abort signal |
59
+ | `bodyType` | `"json" \| "text" \| "form"` | `"json"` | Request body encoding |
60
+
61
+ ### Retry Behavior
62
+
63
+ - Uses exponential backoff starting from `retryDelay`.
64
+ - **Always retries**: 429 (rate limit), 5xx, network errors, timeouts.
65
+ - **Never retries**: validation errors, 4xx (except 429).
66
+
67
+ ### Convenience Methods
68
+
69
+ All return `Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>`.
70
+
71
+ | Function | Signature |
72
+ |----------|-----------|
73
+ | `get` | `(url, options?, params?) => Effect` |
74
+ | `post` | `(url, body?, options?, params?) => Effect` |
75
+ | `put` | `(url, body?, options?, params?) => Effect` |
76
+ | `patch` | `(url, body?, options?, params?) => Effect` |
77
+ | `del` | `(url, options?, params?) => Effect` |
78
+ | `options` | `(url, options?, params?) => Effect` |
79
+ | `head` | `(url, options?, params?) => Effect` |
80
+
81
+ All methods support schema overloads for compile-time type safety:
82
+
83
+ ```ts
84
+ import { Schema } from "effect";
85
+
86
+ const UserSchema = Schema.Struct({ id: Schema.Number, name: Schema.String });
87
+
88
+ const user = get("/api/users/1", { schema: UserSchema });
89
+ // Type: Effect<{ id: number; name: string }, ...>
90
+ ```
91
+
92
+ ### Schema Helpers
93
+
94
+ #### `createPaginatedSchema(itemSchema)`
95
+
96
+ Creates a schema for paginated API responses.
97
+
98
+ ```ts
99
+ const PagedUsers = createPaginatedSchema(UserSchema);
100
+ // { data: User[], pagination: { page, pageSize, total, totalPages } }
101
+ ```
102
+
103
+ #### `createApiResponseSchema(dataSchema)`
104
+
105
+ Creates a schema for standard API responses.
106
+
107
+ ```ts
108
+ const ApiUser = createApiResponseSchema(UserSchema);
109
+ // { success: boolean, data: User, message?: string, errors?: string[] }
110
+ ```
111
+
112
+ ### Error Types
113
+
114
+ #### `FetcherError`
115
+
116
+ Thrown on network errors, timeouts, and non-2xx responses.
117
+
118
+ | Property | Type | Description |
119
+ |----------|------|-------------|
120
+ | `message` | `string` | Error description |
121
+ | `url` | `string` | Request URL |
122
+ | `status` | `number?` | HTTP status code |
123
+ | `responseData` | `unknown?` | Response body if available |
124
+ | `attempt` | `number?` | Retry attempt number |
125
+
126
+ #### `FetcherValidationError`
127
+
128
+ Thrown when response data fails schema validation.
129
+
130
+ | Property | Type | Description |
131
+ |----------|------|-------------|
132
+ | `message` | `string` | Error description |
133
+ | `url` | `string` | Request URL |
134
+ | `problems` | `string` | Schema validation errors |
135
+ | `responseData` | `unknown` | Raw response data |
136
+ | `attempt` | `number?` | Retry attempt number |
137
+
138
+ ### Security Utilities
139
+
140
+ #### `shouldRedirectToHttps(protocol, url, headers, nodeEnv?): string | null`
141
+
142
+ Checks if a request should be redirected to HTTPS. Handles proxy headers (`x-forwarded-proto`, `x-forwarded-ssl`). Returns the HTTPS URL or `null`.
143
+
144
+ - Skipped in `development` and `test` environments.
145
+
146
+ ```ts
147
+ const redirect = shouldRedirectToHttps("http", req.url, req.headers);
148
+ if (redirect) return Response.redirect(redirect, 301);
149
+ ```
150
+
151
+ #### `getRequestId(existingId?): string`
152
+
153
+ Returns the existing request ID or generates a new UUID.
154
+
155
+ ```ts
156
+ const reqId = getRequestId(headers["x-request-id"]);
157
+ ```
158
+
159
+ ## License
160
+
161
+ Apache-2.0
@@ -0,0 +1,134 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { HttpClient } from "effect/unstable/http";
3
+
4
+ //#region src/fetcher.d.ts
5
+ /**
6
+ * A Schema with DecodingServices constrained to `never`, allowing synchronous decoding.
7
+ * All standard built-in schemas (e.g. `Schema.Struct(...)`) satisfy this constraint.
8
+ */
9
+ type SyncSchema<T> = Schema.Schema<T> & {
10
+ readonly DecodingServices: never;
11
+ };
12
+ /**
13
+ * Configuration options for the fetcher utility.
14
+ */
15
+ interface FetcherOptions<T = unknown> {
16
+ /** Number of times to retry the request on failure */
17
+ retries?: number;
18
+ /** Delay in milliseconds between retries */
19
+ retryDelay?: number;
20
+ /** Optional callback invoked on error */
21
+ onError?: (error: unknown) => void;
22
+ /** Timeout in milliseconds for the request */
23
+ timeout?: number;
24
+ /** Additional headers to include in the request */
25
+ headers?: Record<string, string>;
26
+ /** Effect/Schema for runtime validation of the response */
27
+ schema?: SyncSchema<T>;
28
+ /** Abortsignal */
29
+ signal?: AbortSignal;
30
+ /** Body type - defaults to 'json', use 'text' for raw data, 'form' for FormData */
31
+ bodyType?: 'json' | 'text' | 'form';
32
+ }
33
+ /**
34
+ * Represents all supported HTTP methods for the fetcher utility.
35
+ */
36
+ type HttpMethod = Schema.Schema.Type<typeof HttpMethod>;
37
+ /**
38
+ * Represents a type-safe map of query parameters.
39
+ * Each value can be a string, number, boolean, null, undefined, or an array of those types.
40
+ */
41
+ type QueryParams = Schema.Schema.Type<typeof QueryParams>;
42
+ /**
43
+ * Represents a type-safe request body for HTTP methods that support a body.
44
+ * Can be an object, array, string, number, boolean, or null.
45
+ */
46
+ type RequestBody = Schema.Schema.Type<typeof RequestBody>;
47
+ /**
48
+ * Represents HTTP headers as key-value string pairs.
49
+ */
50
+ type Headers = Schema.Schema.Type<typeof Headers>;
51
+ declare const HttpMethod: any;
52
+ declare const QueryParams: Schema.Record$<Schema.Schema.All, Schema.Schema.All>;
53
+ declare const RequestBody: typeof Schema.Any;
54
+ declare const Headers: Schema.Record$<Schema.Schema.All, Schema.Schema.All>;
55
+ /**
56
+ * Custom error class for validation-specific errors.
57
+ */
58
+ declare class FetcherValidationError extends Error {
59
+ readonly url: string;
60
+ readonly problems: string;
61
+ readonly responseData: unknown;
62
+ readonly attempt?: number | undefined;
63
+ constructor(message: string, url: string, problems: string, responseData: unknown, attempt?: number | undefined);
64
+ [Symbol.toStringTag]: string;
65
+ toString(): string;
66
+ getProblemsString(): string;
67
+ }
68
+ /**
69
+ * Custom error class for fetcher-specific errors.
70
+ */
71
+ declare class FetcherError extends Error {
72
+ readonly url: string;
73
+ readonly status?: number | undefined;
74
+ readonly responseData?: unknown | undefined;
75
+ readonly attempt?: number | undefined;
76
+ constructor(message: string, url: string, status?: number | undefined, responseData?: unknown | undefined, attempt?: number | undefined);
77
+ [Symbol.toStringTag]: string;
78
+ toString(): string;
79
+ }
80
+ declare function fetcher<T = unknown>(input: string, method?: 'GET', options?: FetcherOptions<T>, params?: QueryParams): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
81
+ declare function fetcher<S extends SyncSchema<Schema.Schema.Type<S>>>(input: string, method: 'GET', options: FetcherOptions<Schema.Schema.Type<S>> & {
82
+ schema: S;
83
+ }, params?: QueryParams): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
84
+ declare function fetcher<T = unknown>(input: string, method: 'POST' | 'PUT' | 'PATCH', options?: FetcherOptions<T>, params?: QueryParams, body?: RequestBody): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
85
+ declare function fetcher<S extends SyncSchema<Schema.Schema.Type<S>>>(input: string, method: 'POST' | 'PUT' | 'PATCH', options: FetcherOptions<Schema.Schema.Type<S>> & {
86
+ schema: S;
87
+ }, params?: QueryParams, body?: RequestBody): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
88
+ declare function fetcher<T = unknown>(input: string, method: 'DELETE' | 'OPTIONS' | 'HEAD', options?: FetcherOptions<T>, params?: QueryParams): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
89
+ declare function get<T = unknown>(url: string, options?: FetcherOptions<T>, params?: QueryParams): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
90
+ declare function get<A>(url: string, options: FetcherOptions<A> & {
91
+ schema: Schema.Schema<A>;
92
+ }, params?: QueryParams): Effect.Effect<A, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
93
+ declare function post<T = unknown>(url: string, body?: RequestBody, options?: FetcherOptions<T>, params?: QueryParams): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
94
+ declare function post<S extends SyncSchema<Schema.Schema.Type<S>>>(url: string, body: RequestBody, options: FetcherOptions<Schema.Schema.Type<S>> & {
95
+ schema: S;
96
+ }, params?: QueryParams): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
97
+ declare function put<T = unknown>(url: string, body?: RequestBody, options?: FetcherOptions<T>, params?: QueryParams): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
98
+ declare function put<S extends SyncSchema<Schema.Schema.Type<S>>>(url: string, body: RequestBody, options: FetcherOptions<Schema.Schema.Type<S>> & {
99
+ schema: S;
100
+ }, params?: QueryParams): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
101
+ declare function patch<T = unknown>(url: string, body?: RequestBody, options?: FetcherOptions<T>, params?: QueryParams): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
102
+ declare function patch<S extends SyncSchema<Schema.Schema.Type<S>>>(url: string, body: RequestBody, options: FetcherOptions<Schema.Schema.Type<S>> & {
103
+ schema: S;
104
+ }, params?: QueryParams): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
105
+ declare function del<T = unknown>(url: string, options?: FetcherOptions<T>, params?: QueryParams): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
106
+ declare function del<S extends SyncSchema<Schema.Schema.Type<S>>>(url: string, options: FetcherOptions<Schema.Schema.Type<S>> & {
107
+ schema: S;
108
+ }, params?: QueryParams): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
109
+ declare function options<T = unknown>(url: string, options?: FetcherOptions<T>, params?: QueryParams): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
110
+ declare function options<S extends SyncSchema<Schema.Schema.Type<S>>>(url: string, options: FetcherOptions<Schema.Schema.Type<S>> & {
111
+ schema: S;
112
+ }, params?: QueryParams): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
113
+ declare function head<T = unknown>(url: string, options?: FetcherOptions<T>, params?: QueryParams): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
114
+ declare function head<S extends SyncSchema<Schema.Schema.Type<S>>>(url: string, options: FetcherOptions<Schema.Schema.Type<S>> & {
115
+ schema: S;
116
+ }, params?: QueryParams): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;
117
+ declare const createPaginatedSchema: <T>(itemSchema: Schema.Schema<T>) => Schema.Struct<{
118
+ data: Schema.Array$<Schema.Schema<T, T, never>>;
119
+ pagination: Schema.Struct<{
120
+ page: typeof Schema.Number;
121
+ pageSize: typeof Schema.Number;
122
+ total: typeof Schema.Number;
123
+ totalPages: typeof Schema.Number;
124
+ }>;
125
+ }>;
126
+ declare const createApiResponseSchema: <T>(dataSchema: Schema.Schema<T>) => Schema.Struct<{
127
+ success: typeof Schema.Boolean;
128
+ data: Schema.Schema<T, T, never>;
129
+ message: Schema.optional<typeof Schema.String>;
130
+ errors: Schema.optional<Schema.Array$<typeof Schema.String>>;
131
+ }>;
132
+ //#endregion
133
+ export { FetcherError, FetcherOptions, FetcherValidationError, Headers, HttpMethod, QueryParams, RequestBody, createApiResponseSchema, createPaginatedSchema, del, fetcher, get, head, options, patch, post, put };
134
+ //# sourceMappingURL=fetcher.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetcher.d.mts","names":[],"sources":["../src/fetcher.ts"],"mappings":"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"}
@@ -0,0 +1,264 @@
1
+ import { Cause, Duration, Effect, Exit, Schedule, Schema, pipe } from "effect";
2
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http";
3
+ //#region src/fetcher.ts
4
+ /**
5
+ * Copyright 2026 ResQ
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ const EMPTY = "";
20
+ const safeStringify = (error) => {
21
+ if (error instanceof Error) return error.message;
22
+ try {
23
+ return JSON.stringify(error);
24
+ } catch {
25
+ return String(error);
26
+ }
27
+ };
28
+ /**
29
+ * Get the base URL for API requests.
30
+ */
31
+ const getBaseURL = () => {
32
+ if (globalThis.window !== void 0) return "";
33
+ if (process.env["VITE_SITE_URL"]) return process.env["VITE_SITE_URL"].replace(/\/+$/, "");
34
+ if (process.env["SITE_URL"]) return process.env["SITE_URL"].replace(/\/+$/, "");
35
+ return "http://localhost:5173";
36
+ };
37
+ Schema.Literals([
38
+ "GET",
39
+ "POST",
40
+ "PUT",
41
+ "PATCH",
42
+ "DELETE",
43
+ "OPTIONS",
44
+ "HEAD"
45
+ ]);
46
+ Schema.Record(Schema.String, Schema.Union([
47
+ Schema.String,
48
+ Schema.Number,
49
+ Schema.Boolean,
50
+ Schema.Undefined,
51
+ Schema.Null,
52
+ Schema.Array(Schema.Union([
53
+ Schema.String,
54
+ Schema.Number,
55
+ Schema.Boolean
56
+ ]))
57
+ ]));
58
+ Schema.Any;
59
+ Schema.Record(Schema.String, Schema.String);
60
+ /**
61
+ * Custom error class for validation-specific errors.
62
+ */
63
+ var FetcherValidationError = class FetcherValidationError extends Error {
64
+ constructor(message, url, problems, responseData, attempt) {
65
+ super(message);
66
+ this.url = url;
67
+ this.problems = problems;
68
+ this.responseData = responseData;
69
+ this.attempt = attempt;
70
+ this.name = "FetcherValidationError";
71
+ Object.setPrototypeOf(this, FetcherValidationError.prototype);
72
+ }
73
+ [Symbol.toStringTag] = "FetcherValidationError";
74
+ toString() {
75
+ const attemptStr = this.attempt ? `, Attempt: ${this.attempt}` : "";
76
+ return `FetcherValidationError: ${this.message} (URL: ${this.url}${attemptStr})`;
77
+ }
78
+ getProblemsString() {
79
+ return this.problems;
80
+ }
81
+ };
82
+ /**
83
+ * Custom error class for fetcher-specific errors.
84
+ */
85
+ var FetcherError = class FetcherError extends Error {
86
+ constructor(message, url, status, responseData, attempt) {
87
+ super(message);
88
+ this.url = url;
89
+ this.status = status;
90
+ this.responseData = responseData;
91
+ this.attempt = attempt;
92
+ this.name = "FetcherError";
93
+ Object.setPrototypeOf(this, FetcherError.prototype);
94
+ }
95
+ [Symbol.toStringTag] = "FetcherError";
96
+ toString() {
97
+ const statusStr = this.status ? `, Status: ${this.status}` : "";
98
+ const attemptStr = this.attempt ? `, Attempt: ${this.attempt}` : "";
99
+ return `FetcherError: ${this.message} (URL: ${this.url}${statusStr}${attemptStr})`;
100
+ }
101
+ };
102
+ /**
103
+ * Builds a query string from the provided query parameters.
104
+ */
105
+ const buildQueryString = (params) => {
106
+ if (!params) return EMPTY;
107
+ const urlParams = new URLSearchParams();
108
+ Object.entries(params).forEach(([key, value]) => {
109
+ if (value == null) return;
110
+ if (Array.isArray(value)) value.filter((item) => item != null).forEach((item) => {
111
+ urlParams.append(key, String(item));
112
+ });
113
+ else urlParams.append(key, String(value));
114
+ });
115
+ return urlParams.toString();
116
+ };
117
+ /**
118
+ * Builds a type-safe HttpClientRequest for the given method and URL.
119
+ */
120
+ const buildRequest = (method, url) => {
121
+ switch (method) {
122
+ case "GET": return HttpClientRequest.get(url);
123
+ case "POST": return HttpClientRequest.post(url);
124
+ case "PUT": return HttpClientRequest.put(url);
125
+ case "PATCH": return HttpClientRequest.patch(url);
126
+ case "DELETE": return HttpClientRequest.delete(url);
127
+ case "OPTIONS": return HttpClientRequest.options(url);
128
+ case "HEAD": return HttpClientRequest.head(url);
129
+ }
130
+ };
131
+ /**
132
+ * Validates response data using the provided Effect schema.
133
+ */
134
+ const validateResponse = (data, attempt, url, schema, onError) => {
135
+ if (!schema) return Effect.succeed(data);
136
+ const result = Schema.decodeUnknownExit(schema)(data);
137
+ if (Exit.isFailure(result)) {
138
+ const schemaError = Cause.squash(result.cause);
139
+ const validationError = new FetcherValidationError("Response validation failed", url, schemaError instanceof Error ? schemaError.message : String(schemaError), data, attempt);
140
+ if (onError) onError(validationError);
141
+ return Effect.fail(validationError);
142
+ }
143
+ return Effect.succeed(result.value);
144
+ };
145
+ /**
146
+ * Wraps an Effect with a timeout, converting timeout errors to FetcherError.
147
+ */
148
+ const withTimeout = (eff, timeout, url, attempt) => Effect.timeoutOrElse(eff, {
149
+ duration: Duration.millis(timeout),
150
+ orElse: () => Effect.fail(new FetcherError("Request timed out", url, void 0, void 0, attempt))
151
+ });
152
+ /**
153
+ * Type guard: narrows unknown to FetcherError.
154
+ */
155
+ const isFetcherError = (error) => error instanceof FetcherError;
156
+ /**
157
+ * Type guard: narrows unknown to FetcherValidationError.
158
+ */
159
+ const isFetcherValidationError = (error) => error instanceof FetcherValidationError;
160
+ /**
161
+ * Creates a retry schedule with exponential backoff and error filtering.
162
+ */
163
+ const createRetrySchedule = (retries, retryDelay) => pipe(Schedule.exponential(Duration.millis(retryDelay)), Schedule.both(Schedule.recurs(retries)), Schedule.while((metadata) => {
164
+ const error = metadata.input;
165
+ if (isFetcherValidationError(error)) return Effect.succeed(false);
166
+ if (isFetcherError(error) && error.status) {
167
+ if (error.status === 429) return Effect.succeed(true);
168
+ if (error.status >= 400 && error.status < 500) return Effect.succeed(false);
169
+ }
170
+ return Effect.succeed(true);
171
+ }));
172
+ /**
173
+ * Parses the response, handling HTTP errors and JSON parsing.
174
+ */
175
+ const parseResponse = (response, url, attempt) => {
176
+ if (response.status < 200 || response.status >= 300) return pipe(response.json, Effect.catch(() => Effect.succeed(void 0)), Effect.flatMap((errorData) => pipe(response.text, Effect.catch(() => Effect.succeed("Request failed")), Effect.flatMap((errorText) => {
177
+ const errorMessage = response.status === 429 ? `Rate limit exceeded (429). Please slow down requests to ${url}` : `HTTP ${response.status}: ${errorText}`;
178
+ return Effect.fail(new FetcherError(errorMessage, url, response.status, errorData, attempt));
179
+ }))));
180
+ return pipe(response.json, Effect.catch((error) => pipe(response.text, Effect.flatMap((text) => {
181
+ const errorMessage = `Failed to parse JSON response. Status: ${response.status}, Content-Type: ${response.headers["Content-Type"] || "unknown"}, Body: ${text.slice(0, 200)}${text.length > 200 ? "..." : ""}`;
182
+ return Effect.fail(new FetcherError(errorMessage, url, response.status, {
183
+ originalError: error,
184
+ responseText: text
185
+ }, attempt));
186
+ }), Effect.catch(() => Effect.fail(new FetcherError(`Failed to parse response: ${error.message}`, url, response.status, void 0, attempt))))));
187
+ };
188
+ function fetcher(input, method = "GET", options = {}, params, body) {
189
+ const { retries = 0, retryDelay = 1e3, onError, timeout = 1e4, headers = {}, schema, bodyType = "json" } = options;
190
+ const queryString = buildQueryString(params);
191
+ let url;
192
+ if (input.startsWith("http")) url = queryString ? `${input}?${queryString}` : input;
193
+ else {
194
+ const baseURL = getBaseURL();
195
+ const fullPath = baseURL ? `${baseURL}${input}` : input;
196
+ url = queryString ? `${fullPath}?${queryString}` : fullPath;
197
+ }
198
+ return Effect.gen(function* () {
199
+ const client = yield* HttpClient.HttpClient;
200
+ let attempt = 0;
201
+ let req = buildRequest(method, url);
202
+ if (body != null && (method === "POST" || method === "PUT" || method === "PATCH")) if (bodyType === "form" || body instanceof FormData) req = HttpClientRequest.bodyFormData(body)(req);
203
+ else if (bodyType === "text") {
204
+ const textBody = typeof body === "object" ? JSON.stringify(body) : String(body);
205
+ req = HttpClientRequest.bodyText(textBody)(req);
206
+ } else req = yield* pipe(HttpClientRequest.bodyJson(body)(req), Effect.mapError((error) => new FetcherError(`Failed to serialize request body: ${safeStringify(error)}`, url, void 0, void 0, attempt)));
207
+ req = HttpClientRequest.setHeaders(headers)(req);
208
+ const retrySchedule = createRetrySchedule(retries, retryDelay);
209
+ return yield* pipe(Effect.gen(function* () {
210
+ attempt++;
211
+ return yield* validateResponse(yield* parseResponse(yield* pipe(client.execute(req), (eff) => withTimeout(eff, timeout, url, attempt), Effect.mapError((error) => {
212
+ if (error instanceof FetcherError) return error;
213
+ return new FetcherError(error instanceof Error ? error.message : String(error), url, void 0, void 0, attempt);
214
+ })), url, attempt), attempt, url, schema, onError);
215
+ }), Effect.retry(retrySchedule), Effect.catch((error) => {
216
+ if (onError) onError(error);
217
+ return Effect.fail(error);
218
+ }));
219
+ });
220
+ }
221
+ function get(url, options, params) {
222
+ return fetcher(url, "GET", options, params);
223
+ }
224
+ function post(url, body, options, params) {
225
+ return fetcher(url, "POST", options, params, body);
226
+ }
227
+ function put(url, body, options, params) {
228
+ return fetcher(url, "PUT", options, params, body);
229
+ }
230
+ function patch(url, body, options, params) {
231
+ return fetcher(url, "PATCH", options, params, body);
232
+ }
233
+ function del(url, options, params) {
234
+ return fetcher(url, "DELETE", options, params);
235
+ }
236
+ function options(url, options, params) {
237
+ return fetcher(url, "OPTIONS", options, params);
238
+ }
239
+ function head(url, options, params) {
240
+ return fetcher(url, "HEAD", options, params);
241
+ }
242
+ const createPaginatedSchema = (itemSchema) => {
243
+ return Schema.Struct({
244
+ data: Schema.Array(itemSchema),
245
+ pagination: Schema.Struct({
246
+ page: Schema.Number,
247
+ pageSize: Schema.Number,
248
+ total: Schema.Number,
249
+ totalPages: Schema.Number
250
+ })
251
+ });
252
+ };
253
+ const createApiResponseSchema = (dataSchema) => {
254
+ return Schema.Struct({
255
+ success: Schema.Boolean,
256
+ data: dataSchema,
257
+ message: Schema.optional(Schema.String),
258
+ errors: Schema.optional(Schema.Array(Schema.String))
259
+ });
260
+ };
261
+ //#endregion
262
+ export { FetcherError, FetcherValidationError, createApiResponseSchema, createPaginatedSchema, del, fetcher, get, head, options, patch, post, put };
263
+
264
+ //# sourceMappingURL=fetcher.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetcher.mjs","names":[],"sources":["../src/fetcher.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Cause, Duration, Effect, Exit, pipe, Schedule, Schema } from 'effect';\nimport { HttpClient, HttpClientError, HttpClientRequest, type HttpClientResponse } from 'effect/unstable/http';\n\n/**\n * A Schema with DecodingServices constrained to `never`, allowing synchronous decoding.\n * All standard built-in schemas (e.g. `Schema.Struct(...)`) satisfy this constraint.\n */\ntype SyncSchema<T> = Schema.Schema<T> & { readonly DecodingServices: never };\n\n/**\n * Configuration options for the fetcher utility.\n */\nexport interface FetcherOptions<T = unknown> {\n /** Number of times to retry the request on failure */\n retries?: number;\n /** Delay in milliseconds between retries */\n retryDelay?: number;\n /** Optional callback invoked on error */\n onError?: (error: unknown) => void;\n /** Timeout in milliseconds for the request */\n timeout?: number;\n /** Additional headers to include in the request */\n headers?: Record<string, string>;\n /** Effect/Schema for runtime validation of the response */\n schema?: SyncSchema<T>;\n /** Abortsignal */\n signal?: AbortSignal;\n /** Body type - defaults to 'json', use 'text' for raw data, 'form' for FormData */\n bodyType?: 'json' | 'text' | 'form';\n}\n\n/**\n * Represents all supported HTTP methods for the fetcher utility.\n */\nexport type HttpMethod = Schema.Schema.Type<typeof HttpMethod>;\n/**\n * Represents a type-safe map of query parameters.\n * Each value can be a string, number, boolean, null, undefined, or an array of those types.\n */\nexport type QueryParams = Schema.Schema.Type<typeof QueryParams>;\n/**\n * Represents a type-safe request body for HTTP methods that support a body.\n * Can be an object, array, string, number, boolean, or null.\n */\nexport type RequestBody = Schema.Schema.Type<typeof RequestBody>;\n/**\n * Represents HTTP headers as key-value string pairs.\n */\nexport type Headers = Schema.Schema.Type<typeof Headers>;\n\nconst EMPTY = '';\n\nconst safeStringify = (error: unknown): string => {\n if (error instanceof Error) {\n return error.message;\n }\n try {\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n};\n\n/**\n * Get the base URL for API requests.\n */\nconst getBaseURL = (): string => {\n if (globalThis.window !== undefined) {\n return '';\n }\n if (process.env['VITE_SITE_URL']) {\n return process.env['VITE_SITE_URL'].replace(/\\/+$/, '');\n }\n if (process.env['SITE_URL']) {\n return process.env['SITE_URL'].replace(/\\/+$/, '');\n }\n return 'http://localhost:5173';\n};\n\n// HTTP Method type definition\nconst HttpMethod = Schema.Literals(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']);\n// Query parameters type definition\nconst QueryParams = Schema.Record(\n Schema.String,\n Schema.Union([\n Schema.String,\n Schema.Number,\n Schema.Boolean,\n Schema.Undefined,\n Schema.Null,\n Schema.Array(Schema.Union([Schema.String, Schema.Number, Schema.Boolean])),\n ]),\n);\n// Request body type definition\nconst RequestBody = Schema.Any;\n\n// Headers type definition\nconst Headers = Schema.Record(Schema.String, Schema.String);\n\n/**\n * Custom error class for validation-specific errors.\n */\nexport class FetcherValidationError extends Error {\n constructor(\n message: string,\n public readonly url: string,\n public readonly problems: string,\n public readonly responseData: unknown,\n public readonly attempt?: number,\n ) {\n super(message);\n this.name = 'FetcherValidationError';\n Object.setPrototypeOf(this, FetcherValidationError.prototype);\n }\n\n [Symbol.toStringTag] = 'FetcherValidationError';\n\n override toString(): string {\n const attemptStr = this.attempt ? `, Attempt: ${this.attempt}` : '';\n return `FetcherValidationError: ${this.message} (URL: ${this.url}${attemptStr})`;\n }\n\n getProblemsString(): string {\n return this.problems;\n }\n}\n\n/**\n * Custom error class for fetcher-specific errors.\n */\nexport class FetcherError extends Error {\n constructor(\n message: string,\n public readonly url: string,\n public readonly status?: number,\n public readonly responseData?: unknown,\n public readonly attempt?: number,\n ) {\n super(message);\n this.name = 'FetcherError';\n Object.setPrototypeOf(this, FetcherError.prototype);\n }\n\n [Symbol.toStringTag] = 'FetcherError';\n\n override toString(): string {\n const statusStr = this.status ? `, Status: ${this.status}` : '';\n const attemptStr = this.attempt ? `, Attempt: ${this.attempt}` : '';\n return `FetcherError: ${this.message} (URL: ${this.url}${statusStr}${attemptStr})`;\n }\n}\n\n/**\n * Builds a query string from the provided query parameters.\n */\nconst buildQueryString = (params?: QueryParams): string => {\n if (!params) return EMPTY;\n const urlParams = new URLSearchParams();\n\n Object.entries(params).forEach(([key, value]) => {\n if (value == null) return;\n\n if (Array.isArray(value)) {\n value\n .filter((item): item is string | number | boolean => item != null)\n .forEach((item) => {\n urlParams.append(key, String(item));\n });\n } else {\n urlParams.append(key, String(value));\n }\n });\n\n return urlParams.toString();\n};\n\n/**\n * Builds a type-safe HttpClientRequest for the given method and URL.\n */\nconst buildRequest = (method: HttpMethod, url: string): HttpClientRequest.HttpClientRequest => {\n switch (method) {\n case 'GET':\n return HttpClientRequest.get(url);\n case 'POST':\n return HttpClientRequest.post(url);\n case 'PUT':\n return HttpClientRequest.put(url);\n case 'PATCH':\n return HttpClientRequest.patch(url);\n case 'DELETE':\n return HttpClientRequest.delete(url);\n case 'OPTIONS':\n return HttpClientRequest.options(url);\n case 'HEAD':\n return HttpClientRequest.head(url);\n }\n};\n\n/**\n * Validates response data using the provided Effect schema.\n */\nconst validateResponse = <T>(\n data: unknown,\n attempt: number,\n url: string,\n schema?: SyncSchema<T>,\n onError?: (error: unknown) => void,\n): Effect.Effect<T, FetcherValidationError, never> => {\n if (!schema) {\n return Effect.succeed(data as T);\n }\n\n const result = Schema.decodeUnknownExit(schema as any)(data);\n\n if (Exit.isFailure(result)) {\n const schemaError = Cause.squash(result.cause);\n const problems = schemaError instanceof Error ? schemaError.message : String(schemaError);\n const validationError = new FetcherValidationError(\n 'Response validation failed',\n url,\n problems,\n data,\n attempt,\n );\n\n if (onError) onError(validationError);\n return Effect.fail(validationError);\n }\n\n return Effect.succeed(result.value as T);\n};\n\n/**\n * Wraps an Effect with a timeout, converting timeout errors to FetcherError.\n */\nconst withTimeout = <A, E, R>(\n eff: Effect.Effect<A, E, R>,\n timeout: number,\n url: string,\n attempt: number,\n): Effect.Effect<A, FetcherError | E, R> =>\n Effect.timeoutOrElse(eff, {\n duration: Duration.millis(timeout),\n orElse: () => Effect.fail(new FetcherError('Request timed out', url, undefined, undefined, attempt)),\n });\n\n/**\n * Type guard: narrows unknown to FetcherError.\n */\nconst isFetcherError = (error: unknown): error is FetcherError => error instanceof FetcherError;\n\n/**\n * Type guard: narrows unknown to FetcherValidationError.\n */\nconst isFetcherValidationError = (error: unknown): error is FetcherValidationError =>\n error instanceof FetcherValidationError;\n\n/**\n * Creates a retry schedule with exponential backoff and error filtering.\n */\nconst createRetrySchedule = (retries: number, retryDelay: number) =>\n pipe(\n Schedule.exponential(Duration.millis(retryDelay)),\n Schedule.both(Schedule.recurs(retries)),\n Schedule.while((metadata) => {\n const error: unknown = metadata.input;\n // Don't retry validation errors or client errors (except 429)\n if (isFetcherValidationError(error)) return Effect.succeed(false);\n if (isFetcherError(error) && error.status) {\n if (error.status === 429) return Effect.succeed(true); // Always retry 429 rate limits\n if (error.status >= 400 && error.status < 500) return Effect.succeed(false); // Don't retry other 4xx\n }\n return Effect.succeed(true);\n }),\n );\n\n/**\n * Parses the response, handling HTTP errors and JSON parsing.\n */\nconst parseResponse = (\n response: HttpClientResponse.HttpClientResponse,\n url: string,\n attempt: number,\n): Effect.Effect<unknown, FetcherError, never> => {\n // Check for HTTP errors (non-2xx status codes)\n if (response.status < 200 || response.status >= 300) {\n return pipe(\n response.json,\n Effect.catch(() => Effect.succeed(undefined as unknown)),\n Effect.flatMap((errorData) =>\n pipe(\n response.text,\n Effect.catch(() => Effect.succeed('Request failed')),\n Effect.flatMap((errorText) => {\n const errorMessage =\n response.status === 429\n ? `Rate limit exceeded (429). Please slow down requests to ${url}`\n : `HTTP ${response.status}: ${errorText}`;\n return Effect.fail(\n new FetcherError(errorMessage, url, response.status, errorData, attempt),\n );\n }),\n ),\n ),\n );\n }\n\n // Parse response data as JSON, with fallback to text and detailed error reporting\n return pipe(\n response.json,\n Effect.catch((error: HttpClientError.HttpClientError) =>\n pipe(\n response.text,\n Effect.flatMap((text: string) => {\n const errorMessage = `Failed to parse JSON response. Status: ${response.status}, Content-Type: ${response.headers['Content-Type'] || 'unknown'}, Body: ${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`;\n return Effect.fail(\n new FetcherError(\n errorMessage,\n url,\n response.status,\n { originalError: error, responseText: text },\n attempt,\n ),\n );\n }),\n Effect.catch(() =>\n Effect.fail(\n new FetcherError(\n `Failed to parse response: ${error.message}`,\n url,\n response.status,\n undefined,\n attempt,\n ),\n ),\n ),\n ),\n ),\n );\n};\n\n// --- Overloaded function signatures for type safety with effect/Schema ---\n\nexport function fetcher<T = unknown>(\n input: string,\n method?: 'GET',\n options?: FetcherOptions<T>,\n params?: QueryParams,\n): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function fetcher<S extends SyncSchema<Schema.Schema.Type<S>>>(\n input: string,\n method: 'GET',\n options: FetcherOptions<Schema.Schema.Type<S>> & { schema: S },\n params?: QueryParams,\n): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function fetcher<T = unknown>(\n input: string,\n method: 'POST' | 'PUT' | 'PATCH',\n options?: FetcherOptions<T>,\n params?: QueryParams,\n body?: RequestBody,\n): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function fetcher<S extends SyncSchema<Schema.Schema.Type<S>>>(\n input: string,\n method: 'POST' | 'PUT' | 'PATCH',\n options: FetcherOptions<Schema.Schema.Type<S>> & { schema: S },\n params?: QueryParams,\n body?: RequestBody,\n): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function fetcher<T = unknown>(\n input: string,\n method: 'DELETE' | 'OPTIONS' | 'HEAD',\n options?: FetcherOptions<T>,\n params?: QueryParams,\n): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function fetcher<T = unknown>(\n input: string,\n method: HttpMethod = 'GET',\n options: FetcherOptions<T> = {},\n params?: QueryParams,\n body?: RequestBody,\n): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient> {\n const {\n retries = 0,\n retryDelay = 1_000,\n onError,\n timeout = 10_000,\n headers = {},\n schema,\n bodyType = 'json',\n } = options;\n\n const queryString = buildQueryString(params);\n\n let url: string;\n if (input.startsWith('http')) {\n url = queryString ? `${input}?${queryString}` : input;\n } else {\n const baseURL = getBaseURL();\n const fullPath = baseURL ? `${baseURL}${input}` : input;\n url = queryString ? `${fullPath}?${queryString}` : fullPath;\n }\n\n return Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient;\n let attempt = 0;\n\n let req = buildRequest(method, url);\n\n if (body != null && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {\n if (bodyType === 'form' || body instanceof FormData) {\n req = HttpClientRequest.bodyFormData(body as FormData)(req);\n } else if (bodyType === 'text') {\n const textBody =\n typeof body === 'object' ? JSON.stringify(body) : String(body);\n req = HttpClientRequest.bodyText(textBody)(req);\n } else {\n req = yield* pipe(\n HttpClientRequest.bodyJson(body)(req),\n Effect.mapError(\n (error: unknown) =>\n new FetcherError(\n `Failed to serialize request body: ${safeStringify(error)}`,\n url,\n undefined,\n undefined,\n attempt,\n ),\n ),\n );\n }\n }\n\n req = HttpClientRequest.setHeaders(headers)(req);\n\n const retrySchedule = createRetrySchedule(retries, retryDelay);\n\n const executeRequest: Effect.Effect<T, FetcherError | FetcherValidationError, never> =\n Effect.gen(function* () {\n attempt++;\n\n const response = yield* pipe(\n client.execute(req),\n (eff) => withTimeout(eff, timeout, url, attempt),\n Effect.mapError((error): FetcherError => {\n if (error instanceof FetcherError) return error;\n\n return new FetcherError(\n error instanceof Error ? error.message : String(error),\n url,\n undefined,\n undefined,\n attempt,\n );\n }),\n );\n\n const rawData: unknown = yield* parseResponse(response, url, attempt);\n\n const validatedData: T = yield* validateResponse(rawData, attempt, url, schema, onError);\n\n return validatedData;\n });\n\n return yield* pipe(\n executeRequest,\n Effect.retry(retrySchedule),\n Effect.catch(\n (error: FetcherError | FetcherValidationError): Effect.Effect<never, FetcherError | FetcherValidationError> => {\n if (onError) onError(error);\n return Effect.fail(error);\n },\n ),\n );\n });\n}\n\nexport function get<T = unknown>(\n url: string,\n options?: FetcherOptions<T>,\n params?: QueryParams,\n): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function get<A>(\n url: string,\n options: FetcherOptions<A> & { schema: Schema.Schema<A> },\n params?: QueryParams,\n): Effect.Effect<A, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function get<T = unknown>(url: string, options?: FetcherOptions<T>, params?: QueryParams) {\n return fetcher<T>(url, 'GET', options, params);\n}\n\nexport function post<T = unknown>(\n url: string,\n body?: RequestBody,\n options?: FetcherOptions<T>,\n params?: QueryParams,\n): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function post<S extends SyncSchema<Schema.Schema.Type<S>>>(\n url: string,\n body: RequestBody,\n options: FetcherOptions<Schema.Schema.Type<S>> & { schema: S },\n params?: QueryParams,\n): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function post<T = unknown>(\n url: string,\n body?: RequestBody,\n options?: FetcherOptions<T>,\n params?: QueryParams,\n) {\n return fetcher<T>(url, 'POST', options, params, body);\n}\n\nexport function put<T = unknown>(\n url: string,\n body?: RequestBody,\n options?: FetcherOptions<T>,\n params?: QueryParams,\n): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function put<S extends SyncSchema<Schema.Schema.Type<S>>>(\n url: string,\n body: RequestBody,\n options: FetcherOptions<Schema.Schema.Type<S>> & { schema: S },\n params?: QueryParams,\n): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function put<T = unknown>(\n url: string,\n body?: RequestBody,\n options?: FetcherOptions<T>,\n params?: QueryParams,\n) {\n return fetcher<T>(url, 'PUT', options, params, body);\n}\n\nexport function patch<T = unknown>(\n url: string,\n body?: RequestBody,\n options?: FetcherOptions<T>,\n params?: QueryParams,\n): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function patch<S extends SyncSchema<Schema.Schema.Type<S>>>(\n url: string,\n body: RequestBody,\n options: FetcherOptions<Schema.Schema.Type<S>> & { schema: S },\n params?: QueryParams,\n): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function patch<T = unknown>(\n url: string,\n body?: RequestBody,\n options?: FetcherOptions<T>,\n params?: QueryParams,\n) {\n return fetcher<T>(url, 'PATCH', options, params, body);\n}\n\nexport function del<T = unknown>(\n url: string,\n options?: FetcherOptions<T>,\n params?: QueryParams,\n): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function del<S extends SyncSchema<Schema.Schema.Type<S>>>(\n url: string,\n options: FetcherOptions<Schema.Schema.Type<S>> & { schema: S },\n params?: QueryParams,\n): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function del<T = unknown>(url: string, options?: FetcherOptions<T>, params?: QueryParams) {\n return fetcher<T>(url, 'DELETE', options, params);\n}\n\nexport function options<T = unknown>(\n url: string,\n options?: FetcherOptions<T>,\n params?: QueryParams,\n): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function options<S extends SyncSchema<Schema.Schema.Type<S>>>(\n url: string,\n options: FetcherOptions<Schema.Schema.Type<S>> & { schema: S },\n params?: QueryParams,\n): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function options<T = unknown>(\n url: string,\n options?: FetcherOptions<T>,\n params?: QueryParams,\n) {\n return fetcher<T>(url, 'OPTIONS', options, params);\n}\n\nexport function head<T = unknown>(\n url: string,\n options?: FetcherOptions<T>,\n params?: QueryParams,\n): Effect.Effect<T, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function head<S extends SyncSchema<Schema.Schema.Type<S>>>(\n url: string,\n options: FetcherOptions<Schema.Schema.Type<S>> & { schema: S },\n params?: QueryParams,\n): Effect.Effect<Schema.Schema.Type<S>, FetcherError | FetcherValidationError, HttpClient.HttpClient>;\n\nexport function head<T = unknown>(url: string, options?: FetcherOptions<T>, params?: QueryParams) {\n return fetcher<T>(url, 'HEAD', options, params);\n}\n\nexport const createPaginatedSchema = <T>(itemSchema: Schema.Schema<T>) => {\n return Schema.Struct({\n data: Schema.Array(itemSchema),\n pagination: Schema.Struct({\n page: Schema.Number,\n pageSize: Schema.Number,\n total: Schema.Number,\n totalPages: Schema.Number,\n }),\n });\n};\n\nexport const createApiResponseSchema = <T>(dataSchema: Schema.Schema<T>) => {\n return Schema.Struct({\n success: Schema.Boolean,\n data: dataSchema,\n message: Schema.optional(Schema.String),\n errors: Schema.optional(Schema.Array(Schema.String)),\n });\n};\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { FetcherError, FetcherOptions, FetcherValidationError, Headers, HttpMethod, QueryParams, RequestBody, createApiResponseSchema, createPaginatedSchema, del, fetcher, get, head, options, patch, post, put } from "./fetcher.mjs";
2
+ import { getRequestId, shouldRedirectToHttps } from "./security.mjs";
3
+ export { FetcherError, FetcherOptions, FetcherValidationError, Headers, HttpMethod, QueryParams, RequestBody, createApiResponseSchema, createPaginatedSchema, del, fetcher, get, getRequestId, head, options, patch, post, put, shouldRedirectToHttps };
package/lib/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { FetcherError, FetcherValidationError, createApiResponseSchema, createPaginatedSchema, del, fetcher, get, head, options, patch, post, put } from "./fetcher.mjs";
2
+ import { getRequestId, shouldRedirectToHttps } from "./security.mjs";
3
+ export { FetcherError, FetcherValidationError, createApiResponseSchema, createPaginatedSchema, del, fetcher, get, getRequestId, head, options, patch, post, put, shouldRedirectToHttps };
@@ -0,0 +1,34 @@
1
+ //#region src/security.d.ts
2
+ /**
3
+ * Copyright 2026 ResQ
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * @file Security Middleware Utilities
19
+ * @module @resq-sw/http/security
20
+ * @author ResQ
21
+ * @description Framework-agnostic security middleware logic.
22
+ * @compliance NIST 800-53 SC-8 (Transmission Confidentiality), SC-23 (Session Authenticity)
23
+ */
24
+ /**
25
+ * Check if a request should be redirected to HTTPS
26
+ */
27
+ declare function shouldRedirectToHttps(protocol: string, url: string, headers: Record<string, string | undefined>, nodeEnv?: string): string | null;
28
+ /**
29
+ * Generate or retrieve a request ID
30
+ */
31
+ declare function getRequestId(existingId?: string): string;
32
+ //#endregion
33
+ export { getRequestId, shouldRedirectToHttps };
34
+ //# sourceMappingURL=security.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.d.mts","names":[],"sources":["../src/security.ts"],"mappings":";;AA2BA;;;;;;;;;;;AAiCA;;;;;;;;;;;;;iBAjCgB,qBAAA,CACd,QAAA,UACA,GAAA,UACA,OAAA,EAAS,MAAA,8BACT,OAAA;;;;iBA6Bc,YAAA,CAAa,UAAA"}
@@ -0,0 +1,47 @@
1
+ //#region src/security.ts
2
+ /**
3
+ * Copyright 2026 ResQ
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * @file Security Middleware Utilities
19
+ * @module @resq-sw/http/security
20
+ * @author ResQ
21
+ * @description Framework-agnostic security middleware logic.
22
+ * @compliance NIST 800-53 SC-8 (Transmission Confidentiality), SC-23 (Session Authenticity)
23
+ */
24
+ /**
25
+ * Check if a request should be redirected to HTTPS
26
+ */
27
+ function shouldRedirectToHttps(protocol, url, headers, nodeEnv = process.env["NODE_ENV"] || "development") {
28
+ if (nodeEnv === "development" || nodeEnv === "test") return null;
29
+ const forwardedProto = headers["x-forwarded-proto"];
30
+ const forwardedSsl = headers["x-forwarded-ssl"];
31
+ if (!(forwardedProto === "https" || forwardedSsl === "on" || protocol === "https" || url.startsWith("https://"))) {
32
+ const httpsUrl = new URL(url);
33
+ httpsUrl.protocol = "https:";
34
+ return httpsUrl.toString();
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Generate or retrieve a request ID
40
+ */
41
+ function getRequestId(existingId) {
42
+ return existingId || crypto.randomUUID();
43
+ }
44
+ //#endregion
45
+ export { getRequestId, shouldRedirectToHttps };
46
+
47
+ //# sourceMappingURL=security.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.mjs","names":[],"sources":["../src/security.ts"],"sourcesContent":["/**\n * Copyright 2026 ResQ\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @file Security Middleware Utilities\n * @module @resq-sw/http/security\n * @author ResQ\n * @description Framework-agnostic security middleware logic.\n * @compliance NIST 800-53 SC-8 (Transmission Confidentiality), SC-23 (Session Authenticity)\n */\n\n/**\n * Check if a request should be redirected to HTTPS\n */\nexport function shouldRedirectToHttps(\n protocol: string,\n url: string,\n headers: Record<string, string | undefined>,\n nodeEnv: string = process.env['NODE_ENV'] || 'development'\n): string | null {\n // Skip in development/test environments\n if (nodeEnv === 'development' || nodeEnv === 'test') {\n return null;\n }\n\n // Check for HTTPS via various headers (handles proxies/load balancers)\n const forwardedProto = headers['x-forwarded-proto'];\n const forwardedSsl = headers['x-forwarded-ssl'];\n \n const isSecure =\n forwardedProto === 'https' ||\n forwardedSsl === 'on' ||\n protocol === 'https' ||\n url.startsWith('https://');\n\n if (!isSecure) {\n const httpsUrl = new URL(url);\n httpsUrl.protocol = 'https:';\n return httpsUrl.toString();\n }\n\n return null;\n}\n\n/**\n * Generate or retrieve a request ID\n */\nexport function getRequestId(existingId?: string): string {\n return existingId || crypto.randomUUID();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,sBACd,UACA,KACA,SACA,UAAkB,QAAQ,IAAI,eAAe,eAC9B;AAEf,KAAI,YAAY,iBAAiB,YAAY,OAC3C,QAAO;CAIT,MAAM,iBAAiB,QAAQ;CAC/B,MAAM,eAAe,QAAQ;AAQ7B,KAAI,EALF,mBAAmB,WACnB,iBAAiB,QACjB,aAAa,WACb,IAAI,WAAW,WAAW,GAEb;EACb,MAAM,WAAW,IAAI,IAAI,IAAI;AAC7B,WAAS,WAAW;AACpB,SAAO,SAAS,UAAU;;AAG5B,QAAO;;;;;AAMT,SAAgB,aAAa,YAA6B;AACxD,QAAO,cAAc,OAAO,YAAY"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@resq-sw/http",
3
+ "version": "0.1.0",
4
+ "description": "Effect-based HTTP client with retry, timeout, and schema validation",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./lib/index.d.mts",
10
+ "import": "./lib/index.mjs",
11
+ "default": "./lib/index.mjs"
12
+ },
13
+ "./security": {
14
+ "types": "./lib/security.d.mts",
15
+ "import": "./lib/security.mjs",
16
+ "default": "./lib/security.mjs"
17
+ }
18
+ },
19
+ "main": "lib/index.mjs",
20
+ "types": "lib/index.d.mts",
21
+ "files": ["lib", "README.md"],
22
+ "scripts": {
23
+ "build": "tsdown",
24
+ "test": "vitest run"
25
+ },
26
+ "peerDependencies": {
27
+ "effect": ">=3.0.0",
28
+ "@effect/platform": ">=0.60.0",
29
+ "@effect/platform-bun": ">=0.40.0"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "@effect/platform-bun": {
33
+ "optional": true
34
+ }
35
+ },
36
+ "devDependencies": {
37
+ "@effect/platform-bun": "4.0.0-beta.43",
38
+ "effect": "4.0.0-beta.43",
39
+ "tsdown": "^0.21.7",
40
+ "typescript": "5.9.3",
41
+ "vitest": "3.2.4"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "provenance": true,
46
+ "registry": "https://registry.npmjs.org/"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/resq-software/npm.git",
51
+ "directory": "packages/http"
52
+ },
53
+ "keywords": ["http", "fetch", "effect", "retry", "schema-validation"],
54
+ "engines": {
55
+ "node": ">=20.19.0"
56
+ }
57
+ }