@tinyfx/runtime 0.1.0 → 0.1.2

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/dist/di.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /** A token used to register and retrieve a service from the container. */
2
+ export type Token<T> = (new (...args: any[]) => T) | symbol;
3
+ /** Create a unique symbol token with a debug description. */
4
+ export declare function createToken<T>(description: string): Token<T>;
5
+ /** Context passed to every component's init() function. */
6
+ export interface TinyFxContext {
7
+ container: Container;
8
+ }
9
+ /**
10
+ * Lightweight DI container.
11
+ *
12
+ * Register services at bootstrap time with `provide()`,
13
+ * then retrieve them inside components via `inject()`.
14
+ */
15
+ export declare class Container {
16
+ private readonly services;
17
+ /** Register a service instance under the given token. */
18
+ provide<T>(token: Token<T>, instance: T): void;
19
+ /** Retrieve a service instance. Throws if not registered. */
20
+ inject<T>(token: Token<T>): T;
21
+ /** Check whether a token has been provided. */
22
+ has<T>(token: Token<T>): boolean;
23
+ }
package/dist/di.js ADDED
@@ -0,0 +1,32 @@
1
+ // @tinyfx/runtime — Dependency Injection container
2
+ /** Create a unique symbol token with a debug description. */
3
+ export function createToken(description) {
4
+ return Symbol(description);
5
+ }
6
+ /**
7
+ * Lightweight DI container.
8
+ *
9
+ * Register services at bootstrap time with `provide()`,
10
+ * then retrieve them inside components via `inject()`.
11
+ */
12
+ export class Container {
13
+ constructor() {
14
+ this.services = new Map();
15
+ }
16
+ /** Register a service instance under the given token. */
17
+ provide(token, instance) {
18
+ this.services.set(token, instance);
19
+ }
20
+ /** Retrieve a service instance. Throws if not registered. */
21
+ inject(token) {
22
+ var _a;
23
+ if (!this.services.has(token)) {
24
+ throw new Error(`TinyFX: No provider for ${typeof token === "symbol" ? token.toString() : (_a = token.name) !== null && _a !== void 0 ? _a : token}`);
25
+ }
26
+ return this.services.get(token);
27
+ }
28
+ /** Check whether a token has been provided. */
29
+ has(token) {
30
+ return this.services.has(token);
31
+ }
32
+ }
@@ -0,0 +1,44 @@
1
+ export declare class HttpError extends Error {
2
+ readonly status: number;
3
+ readonly statusText: string;
4
+ readonly url: string;
5
+ readonly method: string;
6
+ readonly response?: Response | undefined;
7
+ constructor(status: number, statusText: string, url: string, method: string, response?: Response | undefined);
8
+ }
9
+ export declare class HttpTimeoutError extends Error {
10
+ readonly url: string;
11
+ readonly timeout: number;
12
+ constructor(url: string, timeout: number);
13
+ }
14
+ export declare class HttpParseError extends Error {
15
+ readonly url: string;
16
+ constructor(message: string, url: string);
17
+ }
18
+ export interface RequestInterceptor {
19
+ (url: string, options: RequestInit): Promise<{
20
+ url: string;
21
+ options: RequestInit;
22
+ }> | {
23
+ url: string;
24
+ options: RequestInit;
25
+ };
26
+ }
27
+ export interface ResponseInterceptor {
28
+ (response: Response): Promise<Response> | Response;
29
+ }
30
+ export interface HttpConfig {
31
+ baseUrl?: string;
32
+ headers?: Record<string, string>;
33
+ timeout?: number;
34
+ retries?: number;
35
+ retryDelay?: number;
36
+ requestInterceptors?: RequestInterceptor[];
37
+ responseInterceptors?: ResponseInterceptor[];
38
+ }
39
+ export interface RequestOptions {
40
+ headers?: Record<string, string>;
41
+ timeout?: number;
42
+ signal?: AbortSignal;
43
+ params?: Record<string, string | number | boolean>;
44
+ }
@@ -0,0 +1,26 @@
1
+ export class HttpError extends Error {
2
+ constructor(status, statusText, url, method, response) {
3
+ super(`HTTP ${status} ${statusText}: ${method} ${url}`);
4
+ this.status = status;
5
+ this.statusText = statusText;
6
+ this.url = url;
7
+ this.method = method;
8
+ this.response = response;
9
+ this.name = "HttpError";
10
+ }
11
+ }
12
+ export class HttpTimeoutError extends Error {
13
+ constructor(url, timeout) {
14
+ super(`Request timeout after ${timeout}ms: ${url}`);
15
+ this.url = url;
16
+ this.timeout = timeout;
17
+ this.name = "HttpTimeoutError";
18
+ }
19
+ }
20
+ export class HttpParseError extends Error {
21
+ constructor(message, url) {
22
+ super(message);
23
+ this.url = url;
24
+ this.name = "HttpParseError";
25
+ }
26
+ }
@@ -0,0 +1,2 @@
1
+ declare function buildUrl(url: string, base: string, params?: Record<string, string | number | boolean>): string;
2
+ declare function sleep(ms: number): Promise<void>;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ function buildUrl(url, base, params) {
3
+ const fullUrl = base + url;
4
+ if (!params || Object.keys(params).length === 0)
5
+ return fullUrl;
6
+ const urlObj = new URL(fullUrl, typeof window !== 'undefined' ? window.location.href : 'http://localhost');
7
+ Object.entries(params).forEach(([key, value]) => {
8
+ urlObj.searchParams.append(key, String(value));
9
+ });
10
+ return urlObj.toString().replace(urlObj.origin, '');
11
+ }
12
+ async function sleep(ms) {
13
+ return new Promise(resolve => setTimeout(resolve, ms));
14
+ }
@@ -0,0 +1,9 @@
1
+ import { HttpConfig, RequestOptions } from "./data";
2
+ export declare function createHttp(config?: HttpConfig): {
3
+ get: <T>(url: string, options?: RequestOptions) => Promise<T>;
4
+ post: <T>(url: string, body?: unknown, options?: RequestOptions) => Promise<T>;
5
+ put: <T>(url: string, body?: unknown, options?: RequestOptions) => Promise<T>;
6
+ patch: <T>(url: string, body?: unknown, options?: RequestOptions) => Promise<T>;
7
+ del: <T>(url: string, options?: RequestOptions) => Promise<T>;
8
+ delete: <T>(url: string, options?: RequestOptions) => Promise<T>;
9
+ };
@@ -0,0 +1,123 @@
1
+ // @tinyfx/runtime — HTTP client
2
+ // Thin, typed wrapper over fetch.
3
+ import { HttpError, HttpParseError, HttpTimeoutError } from "./data";
4
+ export function createHttp(config = {}) {
5
+ var _a, _b, _c, _d, _e, _f, _g;
6
+ const base = (_a = config.baseUrl) !== null && _a !== void 0 ? _a : "";
7
+ const defaultHeaders = (_b = config.headers) !== null && _b !== void 0 ? _b : {};
8
+ const defaultTimeout = (_c = config.timeout) !== null && _c !== void 0 ? _c : 30000;
9
+ const maxRetries = (_d = config.retries) !== null && _d !== void 0 ? _d : 0;
10
+ const retryDelay = (_e = config.retryDelay) !== null && _e !== void 0 ? _e : 1000;
11
+ const requestInterceptors = (_f = config.requestInterceptors) !== null && _f !== void 0 ? _f : [];
12
+ const responseInterceptors = (_g = config.responseInterceptors) !== null && _g !== void 0 ? _g : [];
13
+ async function request(method, url, body, options = {}) {
14
+ var _a, _b;
15
+ const fullUrl = buildUrl(url, base, options.params);
16
+ const timeoutMs = (_a = options.timeout) !== null && _a !== void 0 ? _a : defaultTimeout;
17
+ let attempts = 0;
18
+ const maxAttempts = maxRetries + 1;
19
+ while (attempts < maxAttempts) {
20
+ attempts++;
21
+ try {
22
+ const headers = Object.assign(Object.assign({}, defaultHeaders), options.headers);
23
+ if (body !== undefined && !headers["Content-Type"]) {
24
+ headers["Content-Type"] = "application/json";
25
+ }
26
+ let fetchOptions = {
27
+ method,
28
+ headers,
29
+ body: body !== undefined ? JSON.stringify(body) : undefined,
30
+ signal: options.signal,
31
+ };
32
+ let requestUrl = fullUrl;
33
+ // Apply request interceptors
34
+ for (const interceptor of requestInterceptors) {
35
+ const result = await interceptor(requestUrl, fetchOptions);
36
+ requestUrl = result.url;
37
+ fetchOptions = result.options;
38
+ }
39
+ // Create timeout controller
40
+ const timeoutController = new AbortController();
41
+ const timeoutId = setTimeout(() => timeoutController.abort(), timeoutMs);
42
+ // Combine signals if user provided one
43
+ let combinedSignal = timeoutController.signal;
44
+ if (options.signal) {
45
+ const combinedController = new AbortController();
46
+ const abortHandler = () => combinedController.abort();
47
+ options.signal.addEventListener("abort", abortHandler, { once: true });
48
+ timeoutController.signal.addEventListener("abort", abortHandler, { once: true });
49
+ combinedSignal = combinedController.signal;
50
+ }
51
+ fetchOptions.signal = combinedSignal;
52
+ let res;
53
+ try {
54
+ res = await fetch(base + requestUrl, fetchOptions);
55
+ }
56
+ catch (err) {
57
+ clearTimeout(timeoutId);
58
+ if (err instanceof Error && err.name === "AbortError") {
59
+ if ((_b = options.signal) === null || _b === void 0 ? void 0 : _b.aborted) {
60
+ throw err; // User cancelled
61
+ }
62
+ throw new HttpTimeoutError(requestUrl, timeoutMs);
63
+ }
64
+ throw err;
65
+ }
66
+ finally {
67
+ clearTimeout(timeoutId);
68
+ }
69
+ // Apply response interceptors
70
+ for (const interceptor of responseInterceptors) {
71
+ res = await interceptor(res);
72
+ }
73
+ if (!res.ok) {
74
+ throw new HttpError(res.status, res.statusText, requestUrl, method, res);
75
+ }
76
+ // Parse response based on content type
77
+ const contentType = res.headers.get("content-type") || "";
78
+ if (contentType.includes("application/json")) {
79
+ try {
80
+ const text = await res.text();
81
+ if (!text || text.trim().length === 0) {
82
+ return undefined;
83
+ }
84
+ return JSON.parse(text);
85
+ }
86
+ catch (err) {
87
+ throw new HttpParseError(`Failed to parse JSON response: ${err instanceof Error ? err.message : "Unknown error"}`, requestUrl);
88
+ }
89
+ }
90
+ if (contentType.includes("text/")) {
91
+ return (await res.text());
92
+ }
93
+ // For other content types, try to read as text
94
+ try {
95
+ const text = await res.text();
96
+ return (text || undefined);
97
+ }
98
+ catch (_c) {
99
+ return undefined;
100
+ }
101
+ }
102
+ catch (err) {
103
+ // Retry logic for network errors or 5xx errors
104
+ const shouldRetry = attempts < maxAttempts &&
105
+ (!(err instanceof HttpError) || (err.status >= 500 && err.status < 600));
106
+ if (shouldRetry) {
107
+ await sleep(retryDelay * attempts);
108
+ continue;
109
+ }
110
+ throw err;
111
+ }
112
+ }
113
+ throw new Error("Max retries exceeded");
114
+ }
115
+ return {
116
+ get: (url, options) => request("GET", url, undefined, options),
117
+ post: (url, body, options) => request("POST", url, body, options),
118
+ put: (url, body, options) => request("PUT", url, body, options),
119
+ patch: (url, body, options) => request("PATCH", url, body, options),
120
+ del: (url, options) => request("DELETE", url, undefined, options),
121
+ delete: (url, options) => request("DELETE", url, undefined, options),
122
+ };
123
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { signal, effect, Signal } from "./signals";
2
2
  export { bindText, bindAttr, bindClass } from "./dom";
3
- export { createHttp } from "./http";
4
- export type { HttpConfig } from "./http";
3
+ export { createHttp } from "./http/http";
4
+ export type { HttpConfig } from "./http/data";
5
+ export { Container, createToken } from "./di";
6
+ export type { Token, TinyFxContext } from "./di";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // @tinyfx/runtime — barrel export
2
2
  export { signal, effect, Signal } from "./signals";
3
3
  export { bindText, bindAttr, bindClass } from "./dom";
4
- export { createHttp } from "./http";
4
+ export { createHttp } from "./http/http";
5
+ export { Container, createToken } from "./di";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyfx/runtime",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Minimal frontend runtime — signals, DOM helpers, typed HTTP, DTO mapping",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,7 +9,8 @@
9
9
  ".": "./dist/index.js",
10
10
  "./signals": "./dist/signals.js",
11
11
  "./dom": "./dist/dom.js",
12
- "./http": "./dist/http.js"
12
+ "./http": "./dist/http.js",
13
+ "./di": "./dist/di.js"
13
14
  },
14
15
  "files": [
15
16
  "dist"