@blueshed/railroad 0.4.0 → 0.5.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.
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: railroad
3
- description: "Railroad — micro reactive UI framework for Bun. Use when writing JSX components with signals, routes, when(), list(), or importing @blueshed/railroad."
3
+ description: "Railroad — micro reactive UI framework for Bun. Use when writing JSX components with signals, routes, when(), list(), delta-doc, or importing @blueshed/railroad."
4
4
  ---
5
5
 
6
6
  Micro reactive UI framework for Bun. ~900 lines, zero dependencies, real DOM.
7
7
 
8
8
  **Read the source files for full API detail** — each has a JSDoc header:
9
- `signals.ts` · `jsx.ts` · `routes.ts` · `shared.ts` · `logger.ts`
9
+ `signals.ts` · `jsx.ts` · `routes.ts` · `shared.ts` · `logger.ts` · `delta.ts` · `delta-server.ts` · `delta-client.ts`
10
10
 
11
11
  ## Setup
12
12
 
@@ -52,6 +52,45 @@ function SitesLayout() {
52
52
  }
53
53
  ```
54
54
 
55
+ ## Delta-doc
56
+
57
+ Real-time JSON document sync over WebSocket. Server persists to files, client gets reactive signals.
58
+
59
+ **Read source for full API:** `delta.ts` · `delta-server.ts` · `delta-client.ts`
60
+
61
+ ```ts
62
+ // Server — register docs and methods
63
+ import { createWs, registerDoc, registerMethod } from "@blueshed/railroad/delta-server";
64
+
65
+ const ws = createWs();
66
+ await registerDoc<Message>(ws, "message", { file: "./data/message.json", empty: { message: "" } });
67
+ registerMethod(ws, "status", () => ({ bun: Bun.version }));
68
+
69
+ const server = Bun.serve({ routes: { "/ws": ws.upgrade }, websocket: ws.websocket });
70
+ ws.setServer(server);
71
+ ```
72
+
73
+ ```tsx
74
+ // Client — open docs as reactive signals
75
+ import { provide } from "@blueshed/railroad";
76
+ import { connectWs, WS, openDoc, call } from "@blueshed/railroad/delta-client";
77
+
78
+ provide(WS, connectWs("/ws"));
79
+
80
+ const message = openDoc<Message>("message");
81
+ effect(() => console.log(message.data.get()));
82
+ message.send([{ op: "replace", path: "/message", value: "hello" }]);
83
+
84
+ const status = await call<Status>("status");
85
+ ```
86
+
87
+ **Delta ops** use JSON Pointer paths:
88
+ - `{ op: "replace", path: "/field", value: "new" }` — set
89
+ - `{ op: "add", path: "/items/-", value: item }` — append to array
90
+ - `{ op: "remove", path: "/items/0" }` — delete by index
91
+
92
+ Multiple ops in one `send()` are atomic.
93
+
55
94
  ## Anti-Patterns
56
95
 
57
96
  1. **No React.** No useState, useEffect, hooks, lifecycle methods, or react imports.
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Delta Client — WebSocket client + reactive document sync for the browser.
3
+ *
4
+ * Provides the client half of the delta-doc system:
5
+ * - connectWs() — reconnecting WebSocket with request/response and notifications
6
+ * - openDoc() — open a persisted document as a reactive signal
7
+ * - call() — invoke a stateless RPC method
8
+ *
9
+ * Usage:
10
+ * import { connectWs, openDoc, call, WS } from "@blueshed/railroad/delta-client";
11
+ * import { provide } from "@blueshed/railroad/shared";
12
+ *
13
+ * provide(WS, connectWs("/ws"));
14
+ *
15
+ * const message = openDoc<Message>("message");
16
+ * effect(() => console.log(message.data.get()));
17
+ * message.send([{ op: "replace", path: "/message", value: "hello" }]);
18
+ *
19
+ * const status = await call<Status>("status");
20
+ */
21
+ import { signal, createLogger } from "./index";
22
+ import { key, inject } from "./shared";
23
+ import { applyOps, type DeltaOp } from "./delta";
24
+
25
+ export type { DeltaOp } from "./delta";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Reconnecting WebSocket
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function reconnectingWebSocket(url: string): WebSocket {
32
+ let ws!: WebSocket;
33
+ const proxy = new EventTarget();
34
+ let backoff = 500;
35
+
36
+ function connect() {
37
+ ws = new WebSocket(url);
38
+ ws.addEventListener("open", () => {
39
+ backoff = 500;
40
+ proxy.dispatchEvent(new Event("open"));
41
+ });
42
+ ws.addEventListener("message", (e: MessageEvent) => {
43
+ proxy.dispatchEvent(new MessageEvent("message", { data: e.data }));
44
+ });
45
+ ws.addEventListener("close", () => {
46
+ proxy.dispatchEvent(new Event("close"));
47
+ setTimeout(connect, (backoff = Math.min(backoff * 2, 30_000)));
48
+ });
49
+ ws.addEventListener("error", () => {
50
+ proxy.dispatchEvent(new Event("error"));
51
+ });
52
+ }
53
+
54
+ (proxy as any).send = (data: string) => {
55
+ if (ws.readyState === WebSocket.OPEN) ws.send(data);
56
+ };
57
+
58
+ Object.defineProperty(proxy, "readyState", {
59
+ get: () => ws.readyState,
60
+ });
61
+
62
+ connect();
63
+ return proxy as unknown as WebSocket;
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // WebSocket client
68
+ // ---------------------------------------------------------------------------
69
+
70
+ export type NotifyHandler = (msg: any) => void;
71
+
72
+ export interface WsClient {
73
+ connected: ReturnType<typeof signal<boolean>>;
74
+ send(msg: any): Promise<any>;
75
+ on(event: string, handler: NotifyHandler): () => void;
76
+ }
77
+
78
+ export const WS = key<WsClient>("ws");
79
+
80
+ /** Connect to a delta-server WebSocket endpoint. */
81
+ export function connectWs(
82
+ wsPath: string = "/ws",
83
+ opts?: { clientId?: string },
84
+ ): WsClient {
85
+ const log = createLogger("[ws]");
86
+ const proto = location.protocol === "https:" ? "wss:" : "ws:";
87
+ const query = opts?.clientId ? `?clientId=${opts.clientId}` : "";
88
+ const connected = signal(false);
89
+ const ws = reconnectingWebSocket(
90
+ `${proto}//${location.host}${wsPath}${query}`,
91
+ );
92
+ const pending = new Map<
93
+ number,
94
+ { resolve: (v: any) => void; reject: (e: any) => void }
95
+ >();
96
+ const listeners = new Map<string, Set<NotifyHandler>>();
97
+ let nextId = 1;
98
+ let readyResolve: () => void;
99
+ let ready = new Promise<void>((r) => {
100
+ readyResolve = r;
101
+ });
102
+
103
+ ws.addEventListener("open", () => {
104
+ log.info("connected");
105
+ connected.set(true);
106
+ readyResolve();
107
+ listeners.get("open")?.forEach((fn) => fn({}));
108
+ });
109
+
110
+ ws.addEventListener("close", () => {
111
+ log.info("disconnected");
112
+ connected.set(false);
113
+ ready = new Promise<void>((r) => {
114
+ readyResolve = r;
115
+ });
116
+ listeners.get("close")?.forEach((fn) => fn({}));
117
+ });
118
+
119
+ ws.addEventListener(
120
+ "message",
121
+ ((ev: MessageEvent) => {
122
+ const msg = JSON.parse(ev.data);
123
+ if (msg.id != null && pending.has(msg.id)) {
124
+ const { resolve, reject } = pending.get(msg.id)!;
125
+ pending.delete(msg.id);
126
+ if (msg.error) {
127
+ log.error(`#${msg.id} error: ${msg.error.message}`);
128
+ reject(msg.error);
129
+ } else {
130
+ log.debug(`#${msg.id} ack`);
131
+ resolve(msg.result);
132
+ }
133
+ } else {
134
+ log.debug(`notify ${JSON.stringify(msg).slice(0, 80)}`);
135
+ listeners.get("message")?.forEach((fn) => fn(msg));
136
+ }
137
+ }) as EventListener,
138
+ );
139
+
140
+ return {
141
+ connected,
142
+ async send(msg: any): Promise<any> {
143
+ await ready;
144
+ return new Promise((resolve, reject) => {
145
+ const id = nextId++;
146
+ pending.set(id, { resolve, reject });
147
+ log.debug(`#${id} ${msg.action} ${msg.doc ?? msg.method ?? ""}`);
148
+ ws.send(JSON.stringify({ ...msg, id }));
149
+ });
150
+ },
151
+ on(event: string, handler: NotifyHandler): () => void {
152
+ if (!listeners.has(event)) listeners.set(event, new Set());
153
+ listeners.get(event)!.add(handler);
154
+ return () => listeners.get(event)!.delete(handler);
155
+ },
156
+ };
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Document — reactive signal backed by a server-side delta-doc
161
+ // ---------------------------------------------------------------------------
162
+
163
+ export interface Doc<T> {
164
+ data: ReturnType<typeof signal<T | null>>;
165
+ dataVersion: ReturnType<typeof signal<number>>;
166
+ ready: Promise<void>;
167
+ send(ops: DeltaOp[]): Promise<any>;
168
+ }
169
+
170
+ const log = createLogger("[doc]");
171
+ const openDocs = new Map<
172
+ string,
173
+ { data: ReturnType<typeof signal<any>>; dataVersion: ReturnType<typeof signal<number>> }
174
+ >();
175
+
176
+ /** Lazily resolve the WS client — deferred so openDoc can be called at module level. */
177
+ let _ws: WsClient | null = null;
178
+ function ws(): WsClient {
179
+ if (!_ws) {
180
+ _ws = inject(WS);
181
+
182
+ _ws.on("open", () => {
183
+ for (const [name, entry] of openDocs) {
184
+ _ws!.send({ action: "open", doc: name }).then((state) => {
185
+ entry.data.set(state);
186
+ entry.dataVersion.set(entry.dataVersion.peek() + 1);
187
+ });
188
+ }
189
+ });
190
+
191
+ _ws.on("message", (msg) => {
192
+ if (msg.doc && msg.ops) {
193
+ const entry = openDocs.get(msg.doc);
194
+ if (entry) {
195
+ const current = entry.data.peek();
196
+ if (current) {
197
+ const updated = structuredClone(current);
198
+ applyOps(updated, msg.ops);
199
+ entry.data.set(updated);
200
+ entry.dataVersion.set(entry.dataVersion.peek() + 1);
201
+ }
202
+ }
203
+ }
204
+ });
205
+ }
206
+ return _ws;
207
+ }
208
+
209
+ /** Open a persisted doc as a reactive signal. Safe to call at module level. */
210
+ export function openDoc<T>(name: string): Doc<T> {
211
+ const data = signal<T | null>(null);
212
+ const dataVersion = signal(0);
213
+ openDocs.set(name, { data, dataVersion });
214
+
215
+ let readyResolve: () => void;
216
+ const ready = new Promise<void>((r) => {
217
+ readyResolve = r;
218
+ });
219
+
220
+ queueMicrotask(() => {
221
+ try {
222
+ ws()
223
+ .send({ action: "open", doc: name })
224
+ .then((state) => {
225
+ data.set(state as T);
226
+ readyResolve();
227
+ });
228
+ } catch (err: any) {
229
+ log.error(`openDoc("${name}"): ${err.message}`);
230
+ }
231
+ });
232
+
233
+ return {
234
+ data,
235
+ dataVersion,
236
+ ready,
237
+ send(ops: DeltaOp[]) {
238
+ return ws().send({ action: "delta", doc: name, ops });
239
+ },
240
+ };
241
+ }
242
+
243
+ /** Call a stateless RPC method. */
244
+ export function call<T>(method: string, params?: any): Promise<T> {
245
+ return ws().send({ action: "call", method, params });
246
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Delta Server — WebSocket protocol layer + document/method registration for Bun.
3
+ *
4
+ * Provides the server half of the delta-doc system:
5
+ * - createWs() — shared WebSocket infrastructure (action routing, pub/sub, upgrade)
6
+ * - registerDoc() — persist a typed JSON document, sync via delta ops
7
+ * - registerMethod() — expose a stateless RPC handler
8
+ *
9
+ * Usage:
10
+ * import { createWs, registerDoc, registerMethod } from "@blueshed/railroad/delta-server";
11
+ *
12
+ * const ws = createWs();
13
+ * await registerDoc<Message>(ws, "message", { file: "./message.json", empty: { message: "" } });
14
+ * registerMethod(ws, "status", () => ({ bun: Bun.version }));
15
+ *
16
+ * const server = Bun.serve({ routes: { "/ws": ws.upgrade }, websocket: ws.websocket });
17
+ * ws.setServer(server);
18
+ */
19
+ import { createLogger } from "./logger";
20
+ import { applyOps, type DeltaOp } from "./delta";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Types
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export type ActionHandler = (
27
+ msg: any,
28
+ ws: any,
29
+ respond: (result: any) => void,
30
+ ) => any | Promise<any>;
31
+
32
+ export interface WsServer {
33
+ on(action: string, handler: ActionHandler): void;
34
+ publish(channel: string, data: any): void;
35
+ sendTo(clientId: string, data: any): void;
36
+ setServer(s: any): void;
37
+ upgrade: (req: Request, server: any) => Response | undefined;
38
+ websocket: {
39
+ idleTimeout: number;
40
+ sendPings: boolean;
41
+ publishToSelf: boolean;
42
+ open(ws: any): void;
43
+ message(ws: any, raw: any): void;
44
+ close(ws: any): void;
45
+ };
46
+ }
47
+
48
+ export interface DocHandle<T> {
49
+ getDoc(): T;
50
+ setDoc(d: T): void;
51
+ persist(): Promise<void>;
52
+ applyAndBroadcast(ops: DeltaOp[]): void;
53
+ }
54
+
55
+ export interface DocOptions<T> {
56
+ file: string;
57
+ empty: T;
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // WebSocket server
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /** Create a shared WebSocket server with action routing and Bun pub/sub. */
65
+ export function createWs(): WsServer {
66
+ const log = createLogger("[ws]");
67
+ const actions = new Map<string, ActionHandler[]>();
68
+ const clients = new Map<string, any>();
69
+ let serverRef: any;
70
+
71
+ return {
72
+ on(action: string, handler: ActionHandler) {
73
+ if (!actions.has(action)) actions.set(action, []);
74
+ actions.get(action)!.push(handler);
75
+ },
76
+
77
+ publish(channel: string, data: any) {
78
+ serverRef?.publish(channel, JSON.stringify(data));
79
+ },
80
+
81
+ sendTo(clientId: string, data: any) {
82
+ const ws = clients.get(clientId);
83
+ if (ws?.readyState === 1) ws.send(JSON.stringify(data));
84
+ },
85
+
86
+ setServer(s: any) {
87
+ serverRef = s;
88
+ },
89
+
90
+ upgrade: (req: Request, server: any) => {
91
+ const url = new URL(req.url, "http://localhost");
92
+ const clientId = url.searchParams.get("clientId") ?? crypto.randomUUID();
93
+ if (server.upgrade(req, { data: { clientId } })) return;
94
+ return new Response("WebSocket upgrade failed", { status: 400 });
95
+ },
96
+
97
+ websocket: {
98
+ idleTimeout: 60,
99
+ sendPings: true,
100
+ publishToSelf: true,
101
+ open(ws: any) {
102
+ const clientId = ws.data?.clientId;
103
+ if (clientId) clients.set(clientId, ws);
104
+ for (const ch of ws.data?.channels ?? []) ws.subscribe(ch);
105
+ log.debug(`open id=${clientId ?? "?"}`);
106
+ },
107
+ async message(ws: any, raw: any) {
108
+ const msg = JSON.parse(String(raw));
109
+ const { id, action } = msg;
110
+
111
+ if (!action) {
112
+ for (const handler of actions.get("_raw") ?? []) {
113
+ await handler(msg, ws, () => {});
114
+ }
115
+ return;
116
+ }
117
+
118
+ try {
119
+ const handlers = actions.get(action);
120
+ if (!handlers?.length) {
121
+ if (id)
122
+ ws.send(
123
+ JSON.stringify({
124
+ id,
125
+ error: { code: -1, message: `Unknown action: ${action}` },
126
+ }),
127
+ );
128
+ return;
129
+ }
130
+ let responded = false;
131
+ const respond = (response: any) => {
132
+ if (!responded && id) {
133
+ responded = true;
134
+ ws.send(JSON.stringify({ id, ...response }));
135
+ }
136
+ };
137
+ for (const handler of handlers) {
138
+ await handler(msg, ws, respond);
139
+ if (responded) break;
140
+ }
141
+ if (!responded && id) {
142
+ ws.send(
143
+ JSON.stringify({
144
+ id,
145
+ error: { code: -1, message: `No handler matched: ${action}` },
146
+ }),
147
+ );
148
+ }
149
+ } catch (err: any) {
150
+ log.error(`error: ${err.message}`);
151
+ if (id)
152
+ ws.send(
153
+ JSON.stringify({ id, error: { code: -1, message: err.message } }),
154
+ );
155
+ }
156
+ },
157
+ close(ws: any) {
158
+ const clientId = ws.data?.clientId;
159
+ if (clientId) clients.delete(clientId);
160
+ log.debug(`close id=${clientId ?? "?"}`);
161
+ },
162
+ },
163
+ };
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Document registration
168
+ // ---------------------------------------------------------------------------
169
+
170
+ /** Register a persisted JSON document with the WebSocket server. */
171
+ export async function registerDoc<T>(
172
+ ws: Pick<WsServer, "on" | "publish">,
173
+ name: string,
174
+ opts: DocOptions<T>,
175
+ ): Promise<DocHandle<T>> {
176
+ const log = createLogger(`[${name}]`);
177
+ const dataFile = Bun.file(opts.file);
178
+ let doc: T = (await dataFile.exists())
179
+ ? { ...structuredClone(opts.empty), ...((await dataFile.json()) as T) }
180
+ : structuredClone(opts.empty);
181
+
182
+ log.info(`loaded from ${opts.file}`);
183
+
184
+ async function persist() {
185
+ await Bun.write(dataFile, JSON.stringify(doc, null, 2));
186
+ }
187
+
188
+ function applyAndBroadcast(ops: DeltaOp[]) {
189
+ applyOps(doc, ops);
190
+ log.info(`delta [${ops.map((o) => `${o.op} ${o.path}`).join(", ")}]`);
191
+ ws.publish(name, { doc: name, ops });
192
+ persist();
193
+ }
194
+
195
+ ws.on("open", (msg, client, respond) => {
196
+ if (msg.doc !== name) return;
197
+ client.subscribe(name);
198
+ respond({ result: doc });
199
+ log.debug("opened");
200
+ });
201
+
202
+ ws.on("delta", (msg, _client, respond) => {
203
+ if (msg.doc !== name) return;
204
+ applyAndBroadcast(msg.ops);
205
+ respond({ result: { ack: true } });
206
+ });
207
+
208
+ ws.on("close", (msg, client, respond) => {
209
+ if (msg.doc !== name) return;
210
+ client.unsubscribe(name);
211
+ respond({ result: { ack: true } });
212
+ log.debug("closed");
213
+ });
214
+
215
+ return { getDoc: () => doc, setDoc: (d: T) => { doc = d; }, persist, applyAndBroadcast };
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Method registration
220
+ // ---------------------------------------------------------------------------
221
+
222
+ /** Register a stateless RPC method with the WebSocket server. */
223
+ export function registerMethod(
224
+ ws: Pick<WsServer, "on">,
225
+ name: string,
226
+ handler: (params: any, client: any) => any | Promise<any>,
227
+ ) {
228
+ ws.on("call", async (msg, client, respond) => {
229
+ if (msg.method !== name) return;
230
+ const log = createLogger(`[${name}]`);
231
+ log.debug("called");
232
+ respond({ result: await handler(msg.params, client) });
233
+ });
234
+ }
package/delta.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Delta — shared types and operations for JSON document patching.
3
+ *
4
+ * Used by both delta-server (apply + persist) and delta-client (apply + render).
5
+ * No dependencies — safe to import anywhere.
6
+ *
7
+ * Delta ops use JSON Pointer paths (`/`-separated, numeric for array index, `-` for append):
8
+ * { op: "replace", path: "/field", value: "new" } — set a value at path
9
+ * { op: "add", path: "/items/-", value: item } — append to array
10
+ * { op: "remove", path: "/items/0" } — delete by index
11
+ *
12
+ * Multiple ops applied via applyOps() are atomic in memory.
13
+ */
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export type DeltaOp =
20
+ | { op: "replace"; path: string; value: unknown }
21
+ | { op: "add"; path: string; value: unknown }
22
+ | { op: "remove"; path: string };
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Apply
26
+ // ---------------------------------------------------------------------------
27
+
28
+ function parsePath(path: string): (string | number)[] {
29
+ return path
30
+ .split("/")
31
+ .filter(Boolean)
32
+ .map((s) => (/^\d+$/.test(s) ? Number(s) : s));
33
+ }
34
+
35
+ function walk(
36
+ obj: any,
37
+ segments: (string | number)[],
38
+ ): { parent: any; key: string | number } {
39
+ let current = obj;
40
+ for (let i = 0; i < segments.length - 1; i++) {
41
+ current = current[segments[i]!];
42
+ if (current == null)
43
+ throw new Error(`Path not found at segment ${segments[i]}`);
44
+ }
45
+ return { parent: current, key: segments[segments.length - 1]! };
46
+ }
47
+
48
+ /** Apply delta ops to a document in place. */
49
+ export function applyOps(doc: any, ops: DeltaOp[]): void {
50
+ for (const op of ops) {
51
+ const segments = parsePath(op.path);
52
+ const { parent, key } = walk(doc, segments);
53
+ switch (op.op) {
54
+ case "replace":
55
+ case "add":
56
+ if (Array.isArray(parent) && key === "-") parent.push(op.value);
57
+ else parent[key] = op.value;
58
+ break;
59
+ case "remove":
60
+ if (Array.isArray(parent) && typeof key === "number")
61
+ parent.splice(key, 1);
62
+ else delete parent[key];
63
+ break;
64
+ }
65
+ }
66
+ }
package/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Railroad — Signals, JSX, and Routes
1
+ // Railroad — Signals, JSX, Routes, and Delta-doc
2
2
 
3
3
  export { Signal, signal, computed, effect, batch } from "./signals";
4
4
  export type { Dispose } from "./signals";
@@ -15,3 +15,6 @@ export type { Key } from "./shared";
15
15
 
16
16
  export { createLogger, setLogLevel, getLogLevel, loggedRequest } from "./logger";
17
17
  export type { LogLevel } from "./logger";
18
+
19
+ export { applyOps } from "./delta";
20
+ export type { DeltaOp } from "./delta";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blueshed/railroad",
3
- "version": "0.4.0",
4
- "description": "Signals, JSX, and routes — a micro UI framework for Bun",
3
+ "version": "0.5.0",
4
+ "description": "Signals, JSX, routes, and delta-doc — a micro UI framework for Bun",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "types": "index.ts",
@@ -11,6 +11,9 @@
11
11
  "./jsx": "./jsx.ts",
12
12
  "./routes": "./routes.ts",
13
13
  "./shared": "./shared.ts",
14
+ "./delta": "./delta.ts",
15
+ "./delta-server": "./delta-server.ts",
16
+ "./delta-client": "./delta-client.ts",
14
17
  "./jsx-runtime": "./jsx-runtime.ts",
15
18
  "./jsx-dev-runtime": "./jsx-dev-runtime.ts"
16
19
  },
@@ -36,5 +39,5 @@
36
39
  "type": "git",
37
40
  "url": "https://github.com/blueshed/railroad"
38
41
  },
39
- "keywords": ["signals", "jsx", "router", "bun", "ui", "framework"]
42
+ "keywords": ["signals", "jsx", "router", "delta-doc", "bun", "ui", "framework"]
40
43
  }