@tinyfx/runtime 0.1.9 → 0.2.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { httpError, timeoutError, parseError } from "../http/data";
3
+ describe("httpError", () => {
4
+ it("creates http error with all fields", () => {
5
+ const err = httpError(404, "Not Found", "/api/test", "GET");
6
+ expect(err.kind).toBe("http");
7
+ if (err.kind === "http") {
8
+ expect(err.status).toBe(404);
9
+ expect(err.statusText).toBe("Not Found");
10
+ expect(err.url).toBe("/api/test");
11
+ expect(err.method).toBe("GET");
12
+ }
13
+ });
14
+ it("includes optional response", () => {
15
+ const response = new Response();
16
+ const err = httpError(500, "Server Error", "/api", "POST", response);
17
+ if (err.kind === "http") {
18
+ expect(err.response).toBe(response);
19
+ }
20
+ });
21
+ });
22
+ describe("timeoutError", () => {
23
+ it("creates timeout error with url and timeout", () => {
24
+ const err = timeoutError("/api/slow", 5000);
25
+ expect(err.kind).toBe("timeout");
26
+ if (err.kind === "timeout") {
27
+ expect(err.url).toBe("/api/slow");
28
+ expect(err.timeout).toBe(5000);
29
+ }
30
+ });
31
+ });
32
+ describe("parseError", () => {
33
+ it("creates parse error with message and url", () => {
34
+ const err = parseError("Unexpected token", "/api/json");
35
+ expect(err.kind).toBe("parse");
36
+ if (err.kind === "parse") {
37
+ expect(err.message).toBe("Unexpected token");
38
+ expect(err.url).toBe("/api/json");
39
+ }
40
+ });
41
+ });
42
+ describe("error discrimination", () => {
43
+ it("allows discriminating by kind", () => {
44
+ const errors = [
45
+ httpError(400, "Bad Request", "/a", "GET"),
46
+ timeoutError("/b", 1000),
47
+ parseError("Bad JSON", "/c"),
48
+ ];
49
+ const httpErr = errors.find((e) => e.kind === "http");
50
+ expect(httpErr === null || httpErr === void 0 ? void 0 : httpErr.kind).toBe("http");
51
+ if ((httpErr === null || httpErr === void 0 ? void 0 : httpErr.kind) === "http") {
52
+ expect(httpErr.status).toBe(400);
53
+ }
54
+ const timeoutErr = errors.find((e) => e.kind === "timeout");
55
+ expect(timeoutErr === null || timeoutErr === void 0 ? void 0 : timeoutErr.kind).toBe("timeout");
56
+ if ((timeoutErr === null || timeoutErr === void 0 ? void 0 : timeoutErr.kind) === "timeout") {
57
+ expect(timeoutErr.timeout).toBe(1000);
58
+ }
59
+ const parseErr = errors.find((e) => e.kind === "parse");
60
+ expect(parseErr === null || parseErr === void 0 ? void 0 : parseErr.kind).toBe("parse");
61
+ if ((parseErr === null || parseErr === void 0 ? void 0 : parseErr.kind) === "parse") {
62
+ expect(parseErr.message).toBe("Bad JSON");
63
+ }
64
+ });
65
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { navigate, goBack } from "../router/index";
3
+ describe("navigate", () => {
4
+ it("is a function that accepts a path string", () => {
5
+ expect(typeof navigate).toBe("function");
6
+ expect(() => navigate("/test/path")).not.toThrow();
7
+ });
8
+ });
9
+ describe("goBack", () => {
10
+ it("calls history.go(-1)", () => {
11
+ const spy = vi.spyOn(window.history, "go");
12
+ goBack();
13
+ expect(spy).toHaveBeenCalledWith(-1);
14
+ spy.mockRestore();
15
+ });
16
+ });
17
+ describe("RouteMeta type", () => {
18
+ it("accepts valid route metadata", () => {
19
+ const meta = { page: "blog-post", path: "/blog/:slug" };
20
+ expect(meta.page).toBe("blog-post");
21
+ expect(meta.path).toBe("/blog/:slug");
22
+ });
23
+ });
24
+ describe("RouteMap type", () => {
25
+ it("accepts route map object", () => {
26
+ const routes = {
27
+ "/": { page: "index", path: "/" },
28
+ "/blog/:slug": { page: "blog-post", path: "/blog/:slug" },
29
+ };
30
+ expect(Object.keys(routes)).toHaveLength(2);
31
+ expect(routes["/blog/:slug"].page).toBe("blog-post");
32
+ });
33
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { signal, effect } from "../signals";
3
+ describe("signal (inlined)", () => {
4
+ it("returns current value when called", () => {
5
+ const count = signal(0);
6
+ expect(count()).toBe(0);
7
+ });
8
+ it("updates value via set()", () => {
9
+ const count = signal(0);
10
+ count.set(5);
11
+ expect(count()).toBe(5);
12
+ });
13
+ it("does not notify subscribers when value is the same", () => {
14
+ const count = signal(0);
15
+ let runs = 0;
16
+ effect(() => {
17
+ count();
18
+ runs++;
19
+ });
20
+ expect(runs).toBe(1);
21
+ count.set(0);
22
+ expect(runs).toBe(1);
23
+ });
24
+ it("notifies subscribers on change", () => {
25
+ const count = signal(0);
26
+ let value = 0;
27
+ effect(() => {
28
+ value = count();
29
+ });
30
+ expect(value).toBe(0);
31
+ count.set(10);
32
+ expect(value).toBe(10);
33
+ });
34
+ it("supports multiple subscribers", () => {
35
+ const count = signal(0);
36
+ let a = 0;
37
+ let b = 0;
38
+ effect(() => { a = count(); });
39
+ effect(() => { b = count() * 2; });
40
+ count.set(3);
41
+ expect(a).toBe(3);
42
+ expect(b).toBe(6);
43
+ });
44
+ it("works with objects", () => {
45
+ const user = signal({ name: "Alice" });
46
+ expect(user().name).toBe("Alice");
47
+ user.set({ name: "Bob" });
48
+ expect(user().name).toBe("Bob");
49
+ });
50
+ it("does not re-run effect when unrelated signal changes", () => {
51
+ const a = signal(0);
52
+ const b = signal(0);
53
+ let runs = 0;
54
+ effect(() => {
55
+ a();
56
+ runs++;
57
+ });
58
+ expect(runs).toBe(1);
59
+ b.set(1);
60
+ expect(runs).toBe(1);
61
+ });
62
+ });
63
+ describe("effect", () => {
64
+ it("runs immediately on creation", () => {
65
+ let ran = false;
66
+ effect(() => { ran = true; });
67
+ expect(ran).toBe(true);
68
+ });
69
+ it("re-runs when tracked signal changes", () => {
70
+ const count = signal(0);
71
+ let runs = 0;
72
+ effect(() => {
73
+ count();
74
+ runs++;
75
+ });
76
+ expect(runs).toBe(1);
77
+ count.set(1);
78
+ expect(runs).toBe(2);
79
+ });
80
+ it("supports nested signal reads", () => {
81
+ const a = signal(1);
82
+ const b = signal(2);
83
+ let sum = 0;
84
+ effect(() => {
85
+ sum = a() + b();
86
+ });
87
+ expect(sum).toBe(3);
88
+ a.set(10);
89
+ expect(sum).toBe(12);
90
+ b.set(5);
91
+ expect(sum).toBe(15);
92
+ });
93
+ });
@@ -1,56 +1,47 @@
1
1
  /**
2
- * Error thrown for non-success HTTP responses.
2
+ * Discriminated union type for HTTP errors.
3
3
  *
4
4
  * @example
5
5
  * try {
6
6
  * await http.get("/posts");
7
7
  * } catch (error) {
8
- * if (error instanceof HttpError) {
8
+ * if (error.kind === "http") {
9
9
  * console.error(error.status, error.url);
10
- * }
11
- * }
12
- */
13
- export declare class HttpError extends Error {
14
- readonly status: number;
15
- readonly statusText: string;
16
- readonly url: string;
17
- readonly method: string;
18
- readonly response?: Response | undefined;
19
- constructor(status: number, statusText: string, url: string, method: string, response?: Response | undefined);
20
- }
21
- /**
22
- * Error thrown when a request times out.
23
- *
24
- * @example
25
- * try {
26
- * await http.get("/slow", { timeout: 100 });
27
- * } catch (error) {
28
- * if (error instanceof HttpTimeoutError) {
10
+ * } else if (error.kind === "timeout") {
29
11
  * console.error(error.timeout);
30
- * }
31
- * }
32
- */
33
- export declare class HttpTimeoutError extends Error {
34
- readonly url: string;
35
- readonly timeout: number;
36
- constructor(url: string, timeout: number);
37
- }
38
- /**
39
- * Error thrown when a JSON response cannot be parsed.
40
- *
41
- * @example
42
- * try {
43
- * await http.get("/broken-json");
44
- * } catch (error) {
45
- * if (error instanceof HttpParseError) {
12
+ * } else if (error.kind === "parse") {
46
13
  * console.error(error.url);
47
14
  * }
48
15
  * }
49
16
  */
50
- export declare class HttpParseError extends Error {
51
- readonly url: string;
52
- constructor(message: string, url: string);
53
- }
17
+ export type HttpError = {
18
+ kind: "http";
19
+ status: number;
20
+ statusText: string;
21
+ url: string;
22
+ method: string;
23
+ response?: Response;
24
+ } | {
25
+ kind: "timeout";
26
+ url: string;
27
+ timeout: number;
28
+ } | {
29
+ kind: "parse";
30
+ message: string;
31
+ url: string;
32
+ };
33
+ export declare function httpError(status: number, statusText: string, url: string, method: string, response?: Response): HttpError;
34
+ export declare function timeoutError(url: string, timeout: number): HttpError;
35
+ export declare function parseError(message: string, url: string): HttpError;
36
+ export declare function isHttpError(err: unknown): err is Extract<HttpError, {
37
+ kind: "http";
38
+ }>;
39
+ export declare function isTimeoutError(err: unknown): err is Extract<HttpError, {
40
+ kind: "timeout";
41
+ }>;
42
+ export declare function isParseError(err: unknown): err is Extract<HttpError, {
43
+ kind: "parse";
44
+ }>;
54
45
  /**
55
46
  * Hook that can rewrite an outgoing request before `fetch` runs.
56
47
  *
package/dist/http/data.js CHANGED
@@ -1,62 +1,18 @@
1
- /**
2
- * Error thrown for non-success HTTP responses.
3
- *
4
- * @example
5
- * try {
6
- * await http.get("/posts");
7
- * } catch (error) {
8
- * if (error instanceof HttpError) {
9
- * console.error(error.status, error.url);
10
- * }
11
- * }
12
- */
13
- export class HttpError extends Error {
14
- constructor(status, statusText, url, method, response) {
15
- super(`HTTP ${status} ${statusText}: ${method} ${url}`);
16
- this.status = status;
17
- this.statusText = statusText;
18
- this.url = url;
19
- this.method = method;
20
- this.response = response;
21
- this.name = "HttpError";
22
- }
1
+ export function httpError(status, statusText, url, method, response) {
2
+ return { kind: "http", status, statusText, url, method, response };
23
3
  }
24
- /**
25
- * Error thrown when a request times out.
26
- *
27
- * @example
28
- * try {
29
- * await http.get("/slow", { timeout: 100 });
30
- * } catch (error) {
31
- * if (error instanceof HttpTimeoutError) {
32
- * console.error(error.timeout);
33
- * }
34
- * }
35
- */
36
- export class HttpTimeoutError extends Error {
37
- constructor(url, timeout) {
38
- super(`Request timeout after ${timeout}ms: ${url}`);
39
- this.url = url;
40
- this.timeout = timeout;
41
- this.name = "HttpTimeoutError";
42
- }
4
+ export function timeoutError(url, timeout) {
5
+ return { kind: "timeout", url, timeout };
43
6
  }
44
- /**
45
- * Error thrown when a JSON response cannot be parsed.
46
- *
47
- * @example
48
- * try {
49
- * await http.get("/broken-json");
50
- * } catch (error) {
51
- * if (error instanceof HttpParseError) {
52
- * console.error(error.url);
53
- * }
54
- * }
55
- */
56
- export class HttpParseError extends Error {
57
- constructor(message, url) {
58
- super(message);
59
- this.url = url;
60
- this.name = "HttpParseError";
61
- }
7
+ export function parseError(message, url) {
8
+ return { kind: "parse", message, url };
9
+ }
10
+ export function isHttpError(err) {
11
+ return typeof err === "object" && err !== null && "kind" in err && err.kind === "http";
12
+ }
13
+ export function isTimeoutError(err) {
14
+ return typeof err === "object" && err !== null && "kind" in err && err.kind === "timeout";
15
+ }
16
+ export function isParseError(err) {
17
+ return typeof err === "object" && err !== null && "kind" in err && err.kind === "parse";
62
18
  }
package/dist/http/http.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @tinyfx/runtime — HTTP client
2
2
  // Thin, typed wrapper over fetch.
3
- import { HttpError, HttpParseError, HttpTimeoutError } from "./data";
3
+ import { httpError, timeoutError, parseError, isHttpError } from "./data";
4
4
  import { buildUrl, sleep } from "./helper";
5
5
  /**
6
6
  * Creates a typed HTTP client around `fetch`.
@@ -73,7 +73,7 @@ export function createHttp(config = {}) {
73
73
  if ((_b = options.signal) === null || _b === void 0 ? void 0 : _b.aborted) {
74
74
  throw err; // User cancelled
75
75
  }
76
- throw new HttpTimeoutError(requestUrl, timeoutMs);
76
+ throw timeoutError(requestUrl, timeoutMs);
77
77
  }
78
78
  throw err;
79
79
  }
@@ -85,7 +85,7 @@ export function createHttp(config = {}) {
85
85
  res = await interceptor(res);
86
86
  }
87
87
  if (!res.ok) {
88
- throw new HttpError(res.status, res.statusText, requestUrl, method, res);
88
+ throw httpError(res.status, res.statusText, requestUrl, method, res);
89
89
  }
90
90
  // Parse response based on content type
91
91
  const contentType = res.headers.get("content-type") || "";
@@ -98,7 +98,7 @@ export function createHttp(config = {}) {
98
98
  return JSON.parse(text);
99
99
  }
100
100
  catch (err) {
101
- throw new HttpParseError(`Failed to parse JSON response: ${err instanceof Error ? err.message : "Unknown error"}`, requestUrl);
101
+ throw parseError(`Failed to parse JSON response: ${err instanceof Error ? err.message : "Unknown error"}`, requestUrl);
102
102
  }
103
103
  }
104
104
  if (contentType.includes("text/")) {
@@ -115,8 +115,8 @@ export function createHttp(config = {}) {
115
115
  }
116
116
  catch (err) {
117
117
  // Retry logic for network errors or 5xx errors
118
- const shouldRetry = attempts < maxAttempts &&
119
- (!(err instanceof HttpError) || (err.status >= 500 && err.status < 600));
118
+ const is5xx = isHttpError(err) && err.status >= 500 && err.status < 600;
119
+ const shouldRetry = attempts < maxAttempts && (!isHttpError(err) || is5xx);
120
120
  if (shouldRetry) {
121
121
  await sleep(retryDelay * attempts);
122
122
  continue;
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export { matchPath, splitPath } from "./router/path-matcher";
9
9
  export type { RouteDef } from "./router/path-matcher";
10
10
  export { init } from "./init";
11
11
  export type { InitConfig } from "./init";
12
- export { initRouter, getParam, navigate, goBack } from "./router/index";
12
+ export { getParam, getParams, navigate, goBack } from "./router/index";
13
13
  export { onMount, onDestroy } from "./router/lifecycle";
14
14
  export type { TinyFxContext } from "./context";
15
- export type { RouteMap, RouteMeta } from "./router/types";
15
+ export type { RouteMap, RouteMeta } from "./router/index";
package/dist/index.js CHANGED
@@ -10,5 +10,5 @@ export { markMounted, isMounted } from "./mount-state";
10
10
  export { matchPath, splitPath } from "./router/path-matcher";
11
11
  export { init } from "./init";
12
12
  // Keep existing router exports
13
- export { initRouter, getParam, navigate, goBack } from "./router/index";
13
+ export { getParam, getParams, navigate, goBack } from "./router/index";
14
14
  export { onMount, onDestroy } from "./router/lifecycle";
package/dist/init.js CHANGED
@@ -3,7 +3,8 @@
3
3
  import { matchPath, splitPath } from "./router/path-matcher";
4
4
  import { initLifecycle } from "./router/lifecycle";
5
5
  import { highlightActiveLinks } from "./router/active-links";
6
- import { navigate } from "./router/navigate";
6
+ import { navigate } from "./router/index";
7
+ import { setParams } from "./router/params";
7
8
  import { mountComponents } from "./registry";
8
9
  import { runPageInit } from "./page-registry";
9
10
  let initialized = false;
@@ -30,6 +31,7 @@ export function init(config) {
30
31
  }
31
32
  initLifecycle();
32
33
  highlightActiveLinks(pathname);
34
+ setParams(params);
33
35
  const ctx = { params, navigate };
34
36
  const instances = mountComponents(ctx);
35
37
  if (config.setupDirectives) {
@@ -1,25 +1,23 @@
1
- import type { RouteMap } from "./types";
2
- import { navigate, goBack } from "./navigate";
3
- import { getParam } from "./params";
4
- export type { RouteMap };
5
- export { navigate, goBack, getParam };
6
1
  /**
7
- * Initialise the TinyFX router for the current page.
8
- *
9
- * Called automatically from the compiler-generated `tinyfx.gen.ts` on every
10
- * page load. Matches `window.location.pathname` against the compiled route
11
- * map, resolves URL params, starts lifecycle hooks, and highlights active
12
- * navigation links.
13
- *
14
- * @param routes - The route map emitted by `tinyfx build`.
15
- * @returns Nothing
16
- *
17
- * @example
18
- * // tinyfx.gen.ts (auto-generated — do not edit)
19
- * initRouter({
20
- * "/": { page: "home", path: "/" },
21
- * "/blog": { page: "blog", path: "/blog" },
22
- * "/blog/:slug":{ page: "blog-post", path: "/blog/:slug" },
23
- * });
2
+ * Metadata for a single route produced by the compiler.
24
3
  */
25
- export declare function initRouter(routes: RouteMap): void;
4
+ export interface RouteMeta {
5
+ /** The page name, derived from the source filename. */
6
+ page: string;
7
+ /** The declared URL pattern, e.g. "/blog/:slug". */
8
+ path: string;
9
+ }
10
+ /**
11
+ * The full route map emitted by `tinyfx build` into `tinyfx.gen.ts`.
12
+ */
13
+ export type RouteMap = Record<string, RouteMeta>;
14
+ /**
15
+ * Navigate to the given path.
16
+ * This is a full browser navigation — not a DOM swap.
17
+ */
18
+ export declare function navigate(path: string): void;
19
+ /**
20
+ * Go back one step in the browser history.
21
+ */
22
+ export declare function goBack(): void;
23
+ export { getParam, getParams } from "./params";
@@ -1,39 +1,15 @@
1
- // @tinyfx/runtime — router entry point
2
- import { navigate, goBack } from "./navigate";
3
- import { getParam, resolveParams } from "./params";
4
- import { initLifecycle } from "./lifecycle";
5
- import { highlightActiveLinks } from "./active-links";
6
- export { navigate, goBack, getParam };
1
+ // @tinyfx/runtime — router utilities
7
2
  /**
8
- * Initialise the TinyFX router for the current page.
9
- *
10
- * Called automatically from the compiler-generated `tinyfx.gen.ts` on every
11
- * page load. Matches `window.location.pathname` against the compiled route
12
- * map, resolves URL params, starts lifecycle hooks, and highlights active
13
- * navigation links.
14
- *
15
- * @param routes - The route map emitted by `tinyfx build`.
16
- * @returns Nothing
17
- *
18
- * @example
19
- * // tinyfx.gen.ts (auto-generated — do not edit)
20
- * initRouter({
21
- * "/": { page: "home", path: "/" },
22
- * "/blog": { page: "blog", path: "/blog" },
23
- * "/blog/:slug":{ page: "blog-post", path: "/blog/:slug" },
24
- * });
3
+ * Navigate to the given path.
4
+ * This is a full browser navigation — not a DOM swap.
25
5
  */
26
- export function initRouter(routes) {
27
- const pathname = window.location.pathname;
28
- for (const pattern of Object.keys(routes)) {
29
- if (resolveParams(pattern, pathname)) {
30
- // Match found
31
- initLifecycle();
32
- highlightActiveLinks(pathname);
33
- return;
34
- }
35
- }
36
- // No route matched — log a warning and do nothing else.
37
- console.warn(`[tinyfx] No route matched for pathname: "${pathname}". ` +
38
- "Check that a page file exists for this URL in src/pages/.");
6
+ export function navigate(path) {
7
+ window.location.href = path;
39
8
  }
9
+ /**
10
+ * Go back one step in the browser history.
11
+ */
12
+ export function goBack() {
13
+ window.history.go(-1);
14
+ }
15
+ export { getParam, getParams } from "./params";
@@ -29,7 +29,7 @@ export declare function onMount(fn: LifecycleCallback): void;
29
29
  export declare function onDestroy(fn: LifecycleCallback): void;
30
30
  /**
31
31
  * Trigger all registered `onMount` callbacks.
32
- * Called by `initRouter` after matching the current route.
32
+ * Called by `init()` after matching the current route.
33
33
  *
34
34
  * @returns Nothing
35
35
  *
@@ -39,7 +39,7 @@ export declare function onDestroy(fn: LifecycleCallback): void;
39
39
  export declare function flushMount(): void;
40
40
  /**
41
41
  * Wire up registered `onDestroy` callbacks to the `pagehide` event.
42
- * Called by `initRouter` after matching the current route.
42
+ * Called by `init()` after matching the current route.
43
43
  *
44
44
  * @returns Nothing
45
45
  *
@@ -39,7 +39,7 @@ export function onDestroy(fn) {
39
39
  }
40
40
  /**
41
41
  * Trigger all registered `onMount` callbacks.
42
- * Called by `initRouter` after matching the current route.
42
+ * Called by `init()` after matching the current route.
43
43
  *
44
44
  * @returns Nothing
45
45
  *
@@ -57,7 +57,7 @@ export function flushMount() {
57
57
  }
58
58
  /**
59
59
  * Wire up registered `onDestroy` callbacks to the `pagehide` event.
60
- * Called by `initRouter` after matching the current route.
60
+ * Called by `init()` after matching the current route.
61
61
  *
62
62
  * @returns Nothing
63
63
  *
@@ -1,20 +1,9 @@
1
1
  /**
2
- * Match `pattern` (e.g. "/blog/:slug") against `pathname` (e.g. "/blog/hello").
2
+ * Set resolved route params (called internally by init()).
3
3
  *
4
- * Returns true if they match, and stores any `:param` captures in the
5
- * module-level store so `getParam()` can retrieve them.
6
- *
7
- * Static segments must match exactly; `:param` segments match any non-empty
8
- * path segment.
9
- *
10
- * @param pattern - The route pattern produced by the compiler
11
- * @param pathname - The current browser pathname
12
- * @returns `true` when the pathname matches the pattern, otherwise `false`
13
- *
14
- * @example
15
- * resolveParams("/blog/:slug", "/blog/hello-world");
4
+ * @param params - The resolved URL parameters
16
5
  */
17
- export declare function resolveParams(pattern: string, pathname: string): boolean;
6
+ export declare function setParams(params: Record<string, string>): void;
18
7
  /**
19
8
  * Retrieve a URL parameter resolved for the current page.
20
9
  *
@@ -2,40 +2,12 @@
2
2
  /** Module-level store for the current page's resolved params. */
3
3
  let currentParams = {};
4
4
  /**
5
- * Match `pattern` (e.g. "/blog/:slug") against `pathname` (e.g. "/blog/hello").
5
+ * Set resolved route params (called internally by init()).
6
6
  *
7
- * Returns true if they match, and stores any `:param` captures in the
8
- * module-level store so `getParam()` can retrieve them.
9
- *
10
- * Static segments must match exactly; `:param` segments match any non-empty
11
- * path segment.
12
- *
13
- * @param pattern - The route pattern produced by the compiler
14
- * @param pathname - The current browser pathname
15
- * @returns `true` when the pathname matches the pattern, otherwise `false`
16
- *
17
- * @example
18
- * resolveParams("/blog/:slug", "/blog/hello-world");
7
+ * @param params - The resolved URL parameters
19
8
  */
20
- export function resolveParams(pattern, pathname) {
21
- const patternParts = pattern.split("/").filter(Boolean);
22
- const pathParts = pathname.split("/").filter(Boolean);
23
- if (patternParts.length !== pathParts.length)
24
- return false;
25
- const captured = {};
26
- for (let i = 0; i < patternParts.length; i++) {
27
- const seg = patternParts[i];
28
- if (seg.startsWith(":")) {
29
- // Dynamic segment — capture the value
30
- captured[seg.slice(1)] = decodeURIComponent(pathParts[i]);
31
- }
32
- else if (seg !== pathParts[i]) {
33
- // Static segment mismatch
34
- return false;
35
- }
36
- }
37
- currentParams = captured;
38
- return true;
9
+ export function setParams(params) {
10
+ currentParams = params;
39
11
  }
40
12
  /**
41
13
  * Retrieve a URL parameter resolved for the current page.
package/dist/signals.d.ts CHANGED
@@ -1,23 +1,3 @@
1
- /**
2
- * Reactive value container used by TinyFX signals.
3
- *
4
- * Reading the value through {@link Signal.get} during an active effect records
5
- * that effect as a subscriber. Writing through {@link Signal.set} re-runs the
6
- * affected subscribers.
7
- *
8
- * @example
9
- * const count = new Signal(0);
10
- * console.log(count.get());
11
- * count.set(1);
12
- * console.log(count.get());
13
- */
14
- export declare class Signal<T> {
15
- private value;
16
- private subs;
17
- constructor(v: T);
18
- get(): T;
19
- set(v: T): void;
20
- }
21
1
  /**
22
2
  * Creates a reactive signal function.
23
3
  *
package/dist/signals.js CHANGED
@@ -1,38 +1,6 @@
1
1
  // @tinyfx/runtime — signals
2
2
  // Minimal, explicit reactivity — no proxies, no magic.
3
3
  const effectStack = [];
4
- /**
5
- * Reactive value container used by TinyFX signals.
6
- *
7
- * Reading the value through {@link Signal.get} during an active effect records
8
- * that effect as a subscriber. Writing through {@link Signal.set} re-runs the
9
- * affected subscribers.
10
- *
11
- * @example
12
- * const count = new Signal(0);
13
- * console.log(count.get());
14
- * count.set(1);
15
- * console.log(count.get());
16
- */
17
- export class Signal {
18
- constructor(v) {
19
- this.subs = new Set();
20
- this.value = v;
21
- }
22
- get() {
23
- const running = effectStack[effectStack.length - 1];
24
- if (running) {
25
- this.subs.add(running);
26
- }
27
- return this.value;
28
- }
29
- set(v) {
30
- if (Object.is(v, this.value))
31
- return;
32
- this.value = v;
33
- this.subs.forEach((fn) => fn());
34
- }
35
- }
36
4
  /**
37
5
  * Creates a reactive signal function.
38
6
  *
@@ -46,9 +14,20 @@ export class Signal {
46
14
  * count.set(1);
47
15
  */
48
16
  export function signal(v) {
49
- const s = new Signal(v);
50
- const fn = () => s.get();
51
- fn.set = (v) => s.set(v);
17
+ let value = v;
18
+ const subs = new Set();
19
+ const fn = () => {
20
+ const running = effectStack[effectStack.length - 1];
21
+ if (running)
22
+ subs.add(running);
23
+ return value;
24
+ };
25
+ fn.set = (next) => {
26
+ if (Object.is(next, value))
27
+ return;
28
+ value = next;
29
+ subs.forEach((s) => s());
30
+ };
52
31
  return fn;
53
32
  }
54
33
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyfx/runtime",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "Minimal frontend runtime — signals, DOM helpers, typed HTTP, DTO mapping",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",