@blueshed/railroad 0.3.0 → 0.3.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.
Files changed (3) hide show
  1. package/jsx.ts +10 -0
  2. package/package.json +1 -1
  3. package/routes.ts +25 -5
package/jsx.ts CHANGED
@@ -327,6 +327,16 @@ export function list<T>(
327
327
  // Existing keyed item — push new value into its signal
328
328
  entry.item!.set(arr[i]!);
329
329
  entry.index!.set(i);
330
+ } else {
331
+ // Index-based — dispose old, recreate with new item
332
+ const oldNode = entry.node;
333
+ entry.dispose();
334
+ pushDisposeScope();
335
+ const node = (keyFnOrRender as (item: T, index: number) => Node)(arr[i]!, i);
336
+ const dispose = popDisposeScope();
337
+ entry = { node, dispose };
338
+ entries.set(key, entry);
339
+ if (oldNode.parentNode) oldNode.parentNode.replaceChild(node, oldNode);
330
340
  }
331
341
 
332
342
  // Move or insert into correct position
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueshed/railroad",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Signals, JSX, and routes — a micro UI framework for Bun",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/routes.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * navigate(path) — set location.hash programmatically
8
8
  * matchRoute(pattern, path) — pure pattern matcher, returns params or null
9
9
  *
10
- * Handlers receive (params, params$) and return a Node.
10
+ * Handlers receive (params, params$) and return a Node (sync or async).
11
11
  * params — plain object for destructuring: ({ id }) => ...
12
12
  * params$ — Signal that updates when params change within the same pattern
13
13
  *
@@ -16,6 +16,7 @@
16
16
  * routes(app, {
17
17
  * "/": () => <Home />,
18
18
  * "/site/:id": ({ id }, params$) => <SiteDetail id={id} params$={params$} />,
19
+ * "/status": async () => { const s = await api.get(); return <Status data={s} />; },
19
20
  * });
20
21
  */
21
22
 
@@ -81,7 +82,7 @@ export function navigate(path: string): void {
81
82
  type RouteHandler = (
82
83
  params: Record<string, string>,
83
84
  params$: Signal<Record<string, string>>,
84
- ) => Node;
85
+ ) => Node | Promise<Node>;
85
86
 
86
87
  export function routes(
87
88
  target: HTMLElement,
@@ -91,8 +92,15 @@ export function routes(
91
92
  let activePattern: string | null = null;
92
93
  let activeParams: Signal<Record<string, string>> | null = null;
93
94
  let activeDispose: Dispose | null = null;
95
+ let runId = 0;
96
+ let asyncPending = false;
94
97
 
95
98
  function teardown() {
99
+ runId++;
100
+ if (asyncPending) {
101
+ popDisposeScope()();
102
+ asyncPending = false;
103
+ }
96
104
  if (activeDispose) activeDispose();
97
105
  activeDispose = null;
98
106
  activePattern = null;
@@ -101,11 +109,23 @@ export function routes(
101
109
  }
102
110
 
103
111
  function run(handler: RouteHandler, params: Record<string, string>) {
112
+ const myRunId = ++runId;
104
113
  activeParams = signal(params);
105
114
  pushDisposeScope();
106
- const node = handler(params, activeParams);
107
- activeDispose = popDisposeScope();
108
- target.appendChild(node);
115
+ const result = handler(params, activeParams);
116
+
117
+ if (result instanceof Promise) {
118
+ asyncPending = true;
119
+ result.then((node) => {
120
+ if (myRunId !== runId) return; // navigated away during await
121
+ asyncPending = false;
122
+ activeDispose = popDisposeScope();
123
+ target.appendChild(node);
124
+ });
125
+ } else {
126
+ activeDispose = popDisposeScope();
127
+ target.appendChild(result);
128
+ }
109
129
  }
110
130
 
111
131
  const disposeEffect = effect(() => {