@tinyfx/runtime 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -28
- package/dist/__tests__/http-errors.test.d.ts +1 -0
- package/dist/__tests__/http-errors.test.js +65 -0
- package/dist/__tests__/router-merged.test.d.ts +1 -0
- package/dist/__tests__/router-merged.test.js +33 -0
- package/dist/__tests__/signals.test.d.ts +1 -0
- package/dist/__tests__/signals.test.js +93 -0
- package/dist/http/data.d.ts +33 -41
- package/dist/http/data.js +15 -59
- package/dist/http/http.js +15 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/init.js +7 -3
- package/dist/router/index.d.ts +21 -23
- package/dist/router/index.js +12 -36
- package/dist/router/lifecycle.d.ts +2 -2
- package/dist/router/lifecycle.js +2 -2
- package/dist/router/params.d.ts +3 -14
- package/dist/router/params.js +4 -32
- package/dist/signals.d.ts +0 -20
- package/dist/signals.js +14 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# @tinyfx/runtime
|
|
2
2
|
|
|
3
|
-
The TinyFX browser runtime: signals, DOM helpers, typed HTTP,
|
|
3
|
+
The TinyFX browser runtime: signals, DOM helpers, typed HTTP, router lifecycle utilities, and dynamic registration APIs.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- Signals (`signal`, `effect`)
|
|
8
8
|
- DOM binding helpers (`bindText`, `bindAttr`, `bindClass`)
|
|
9
|
-
- Typed HTTP client (`createHttp`)
|
|
10
|
-
- Router helpers (`
|
|
9
|
+
- Typed HTTP client (`createHttp`) with discriminated error types
|
|
10
|
+
- Router helpers (`getParam`, `getParams`, `navigate`, `goBack`)
|
|
11
11
|
- Lifecycle hooks (`onMount`, `onDestroy`)
|
|
12
12
|
- Lightweight `TinyFxContext` (`params`, `navigate`)
|
|
13
|
+
- Dynamic component registration (`registerComponent`, `getComponentFactory`, `mountComponents`)
|
|
14
|
+
- Dynamic page registration (`registerPage`, `getPageModule`, `runPageInit`)
|
|
15
|
+
- Mount state tracking (`markMounted`, `isMounted`)
|
|
16
|
+
- Path matching (`matchPath`, `splitPath`, `RouteDef`)
|
|
17
|
+
- Unified initialization (`init`)
|
|
13
18
|
|
|
14
19
|
## Installation
|
|
15
20
|
|
|
@@ -33,7 +38,7 @@ effect(() => {
|
|
|
33
38
|
count.set(count() + 1);
|
|
34
39
|
```
|
|
35
40
|
|
|
36
|
-
### DOM
|
|
41
|
+
### DOM Bindings
|
|
37
42
|
|
|
38
43
|
```ts
|
|
39
44
|
import { signal, bindText, bindClass, bindAttr } from "@tinyfx/runtime";
|
|
@@ -46,12 +51,12 @@ const btn = document.querySelector("button");
|
|
|
46
51
|
|
|
47
52
|
if (el && btn) {
|
|
48
53
|
bindText(el, () => `Count: ${count()}`);
|
|
49
|
-
bindClass(btn, "active", isActive);
|
|
54
|
+
bindClass(btn, "active", () => isActive());
|
|
50
55
|
bindAttr(btn, "disabled", () => count() > 10);
|
|
51
56
|
}
|
|
52
57
|
```
|
|
53
58
|
|
|
54
|
-
### HTTP
|
|
59
|
+
### HTTP Client
|
|
55
60
|
|
|
56
61
|
```ts
|
|
57
62
|
import { createHttp } from "@tinyfx/runtime";
|
|
@@ -65,20 +70,16 @@ const users = await http.get<{ id: number; name: string }[]>("/users");
|
|
|
65
70
|
```ts
|
|
66
71
|
import type { TinyFxContext } from "@tinyfx/runtime";
|
|
67
72
|
|
|
68
|
-
export function init(
|
|
73
|
+
export function init(_el: HTMLElement, ctx: TinyFxContext) {
|
|
69
74
|
console.log(ctx.params);
|
|
70
75
|
ctx.navigate("/about");
|
|
71
76
|
}
|
|
72
77
|
```
|
|
73
78
|
|
|
74
|
-
### Router +
|
|
79
|
+
### Router + Lifecycle Hooks
|
|
75
80
|
|
|
76
81
|
```ts
|
|
77
|
-
import {
|
|
78
|
-
|
|
79
|
-
initRouter({
|
|
80
|
-
"/": { page: "index", path: "/" },
|
|
81
|
-
});
|
|
82
|
+
import { onMount, onDestroy } from "@tinyfx/runtime";
|
|
82
83
|
|
|
83
84
|
onMount(() => {
|
|
84
85
|
console.log("mounted");
|
|
@@ -89,21 +90,66 @@ onDestroy(() => {
|
|
|
89
90
|
});
|
|
90
91
|
```
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
93
|
+
### Component Registration
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { registerComponent } from "@tinyfx/runtime";
|
|
97
|
+
|
|
98
|
+
registerComponent("MyButton", (el, ctx) => {
|
|
99
|
+
el.addEventListener("click", () => console.log("clicked"));
|
|
100
|
+
return { el };
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Page Registration
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { registerPage } from "@tinyfx/runtime";
|
|
108
|
+
|
|
109
|
+
registerPage("/", {
|
|
110
|
+
init(_el, _ctx) {
|
|
111
|
+
console.log("home page loaded");
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Initialization
|
|
117
|
+
|
|
118
|
+
The compiler-generated `tinyfx.gen.ts` calls `init()` automatically. You typically do not need to call it yourself.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { init } from "@tinyfx/runtime";
|
|
122
|
+
|
|
123
|
+
init({ routes, setupDirectives });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## API Overview
|
|
127
|
+
|
|
128
|
+
- `signal<T>(value: T)` — creates a reactive signal
|
|
129
|
+
- `effect(fn)` — registers a reactive effect
|
|
130
|
+
- `bindText(el, source)` — binds text content reactively
|
|
131
|
+
- `bindAttr(el, attr, source)` — binds an attribute reactively
|
|
132
|
+
- `bindClass(el, className, source)` — binds a class toggle reactively
|
|
133
|
+
- `createHttp(config?)` — creates a typed HTTP client
|
|
134
|
+
- `getParam(name)` — gets a single route parameter
|
|
135
|
+
- `getParams()` — gets all route parameters
|
|
136
|
+
- `navigate(path)` — triggers full browser navigation
|
|
137
|
+
- `goBack()` — goes back in browser history
|
|
138
|
+
- `onMount(fn)` — registers a mount callback
|
|
139
|
+
- `onDestroy(fn)` — registers a destroy callback
|
|
140
|
+
- `registerComponent(name, factory)` — registers a dynamic component
|
|
141
|
+
- `getComponentFactory(name)` — retrieves a registered component factory
|
|
142
|
+
- `mountComponents(ctx, root?)` — mounts all components in a root element
|
|
143
|
+
- `registerPage(route, module)` — registers a page module
|
|
144
|
+
- `getPageModule(route)` — retrieves a registered page module
|
|
145
|
+
- `runPageInit(route, ctx)` — runs page init if registered
|
|
146
|
+
- `markMounted(el)` — marks an element as mounted (returns true on first call)
|
|
147
|
+
- `isMounted(el)` — checks if an element has been mounted
|
|
148
|
+
- `matchPath(def, segments)` — matches a RouteDef against path segments
|
|
149
|
+
- `splitPath(pathname)` — splits a pathname into segments
|
|
150
|
+
- `init(config)` — unified bootstrap entry point
|
|
151
|
+
- `TinyFxContext` — type for page/component context
|
|
152
|
+
- `RouteMap`, `RouteMeta`, `RouteDef` — routing types
|
|
107
153
|
|
|
108
154
|
## License
|
|
109
155
|
|
|
@@ -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
|
+
});
|
package/dist/http/data.d.ts
CHANGED
|
@@ -1,56 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
*
|
|
@@ -117,4 +108,5 @@ export interface RequestOptions {
|
|
|
117
108
|
timeout?: number;
|
|
118
109
|
signal?: AbortSignal;
|
|
119
110
|
params?: Record<string, string | number | boolean>;
|
|
111
|
+
json?: boolean;
|
|
120
112
|
}
|
package/dist/http/data.js
CHANGED
|
@@ -1,62 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 {
|
|
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`.
|
|
@@ -25,22 +25,26 @@ export function createHttp(config = {}) {
|
|
|
25
25
|
const requestInterceptors = (_f = config.requestInterceptors) !== null && _f !== void 0 ? _f : [];
|
|
26
26
|
const responseInterceptors = (_g = config.responseInterceptors) !== null && _g !== void 0 ? _g : [];
|
|
27
27
|
async function request(method, url, body, options = {}) {
|
|
28
|
-
var _a, _b;
|
|
28
|
+
var _a, _b, _c;
|
|
29
29
|
const fullUrl = buildUrl(url, base, options.params);
|
|
30
30
|
const timeoutMs = (_a = options.timeout) !== null && _a !== void 0 ? _a : defaultTimeout;
|
|
31
|
+
const useJson = (_b = options.json) !== null && _b !== void 0 ? _b : true;
|
|
31
32
|
let attempts = 0;
|
|
32
33
|
const maxAttempts = maxRetries + 1;
|
|
33
34
|
while (attempts < maxAttempts) {
|
|
34
35
|
attempts++;
|
|
35
36
|
try {
|
|
36
37
|
const headers = Object.assign(Object.assign({}, defaultHeaders), options.headers);
|
|
37
|
-
if (body !== undefined && !headers["Content-Type"]) {
|
|
38
|
+
if (body !== undefined && useJson && !headers["Content-Type"]) {
|
|
38
39
|
headers["Content-Type"] = "application/json";
|
|
39
40
|
}
|
|
41
|
+
const serializedBody = body !== undefined
|
|
42
|
+
? (useJson ? JSON.stringify(body) : body)
|
|
43
|
+
: undefined;
|
|
40
44
|
let fetchOptions = {
|
|
41
45
|
method,
|
|
42
46
|
headers,
|
|
43
|
-
body:
|
|
47
|
+
body: serializedBody,
|
|
44
48
|
signal: options.signal,
|
|
45
49
|
};
|
|
46
50
|
let requestUrl = fullUrl;
|
|
@@ -70,10 +74,10 @@ export function createHttp(config = {}) {
|
|
|
70
74
|
catch (err) {
|
|
71
75
|
clearTimeout(timeoutId);
|
|
72
76
|
if (err instanceof Error && err.name === "AbortError") {
|
|
73
|
-
if ((
|
|
77
|
+
if ((_c = options.signal) === null || _c === void 0 ? void 0 : _c.aborted) {
|
|
74
78
|
throw err; // User cancelled
|
|
75
79
|
}
|
|
76
|
-
throw
|
|
80
|
+
throw timeoutError(requestUrl, timeoutMs);
|
|
77
81
|
}
|
|
78
82
|
throw err;
|
|
79
83
|
}
|
|
@@ -85,7 +89,7 @@ export function createHttp(config = {}) {
|
|
|
85
89
|
res = await interceptor(res);
|
|
86
90
|
}
|
|
87
91
|
if (!res.ok) {
|
|
88
|
-
throw
|
|
92
|
+
throw httpError(res.status, res.statusText, requestUrl, method, res);
|
|
89
93
|
}
|
|
90
94
|
// Parse response based on content type
|
|
91
95
|
const contentType = res.headers.get("content-type") || "";
|
|
@@ -98,7 +102,7 @@ export function createHttp(config = {}) {
|
|
|
98
102
|
return JSON.parse(text);
|
|
99
103
|
}
|
|
100
104
|
catch (err) {
|
|
101
|
-
throw
|
|
105
|
+
throw parseError(`Failed to parse JSON response: ${err instanceof Error ? err.message : "Unknown error"}`, requestUrl);
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
if (contentType.includes("text/")) {
|
|
@@ -109,14 +113,14 @@ export function createHttp(config = {}) {
|
|
|
109
113
|
const text = await res.text();
|
|
110
114
|
return (text || undefined);
|
|
111
115
|
}
|
|
112
|
-
catch (
|
|
116
|
+
catch (_d) {
|
|
113
117
|
return undefined;
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
catch (err) {
|
|
117
121
|
// Retry logic for network errors or 5xx errors
|
|
118
|
-
const
|
|
119
|
-
|
|
122
|
+
const is5xx = isHttpError(err) && err.status >= 500 && err.status < 600;
|
|
123
|
+
const shouldRetry = attempts < maxAttempts && (!isHttpError(err) || is5xx);
|
|
120
124
|
if (shouldRetry) {
|
|
121
125
|
await sleep(retryDelay * attempts);
|
|
122
126
|
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 {
|
|
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/
|
|
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 {
|
|
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/
|
|
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;
|
|
@@ -24,12 +25,15 @@ export function init(config) {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
if (!matchedPath) {
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
if (__DEV__) {
|
|
29
|
+
console.warn(`[tinyfx] No route matched for pathname: "${pathname}". ` +
|
|
30
|
+
"Check that a page file exists for this URL in src/pages/.");
|
|
31
|
+
}
|
|
29
32
|
return null;
|
|
30
33
|
}
|
|
31
34
|
initLifecycle();
|
|
32
35
|
highlightActiveLinks(pathname);
|
|
36
|
+
setParams(params);
|
|
33
37
|
const ctx = { params, navigate };
|
|
34
38
|
const instances = mountComponents(ctx);
|
|
35
39
|
if (config.setupDirectives) {
|
package/dist/router/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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";
|
package/dist/router/index.js
CHANGED
|
@@ -1,39 +1,15 @@
|
|
|
1
|
-
// @tinyfx/runtime — router
|
|
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
|
-
*
|
|
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
|
|
27
|
-
|
|
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 `
|
|
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 `
|
|
42
|
+
* Called by `init()` after matching the current route.
|
|
43
43
|
*
|
|
44
44
|
* @returns Nothing
|
|
45
45
|
*
|
package/dist/router/lifecycle.js
CHANGED
|
@@ -39,7 +39,7 @@ export function onDestroy(fn) {
|
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Trigger all registered `onMount` callbacks.
|
|
42
|
-
* Called by `
|
|
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 `
|
|
60
|
+
* Called by `init()` after matching the current route.
|
|
61
61
|
*
|
|
62
62
|
* @returns Nothing
|
|
63
63
|
*
|
package/dist/router/params.d.ts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Set resolved route params (called internally by init()).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
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
|
*
|
package/dist/router/params.js
CHANGED
|
@@ -2,40 +2,12 @@
|
|
|
2
2
|
/** Module-level store for the current page's resolved params. */
|
|
3
3
|
let currentParams = {};
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Set resolved route params (called internally by init()).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
50
|
-
const
|
|
51
|
-
fn
|
|
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
|
/**
|