@coderegtech/jsonify-ws 1.5.3

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/index.mjs ADDED
@@ -0,0 +1,411 @@
1
+ // src/useJsonifyWs.ts
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { io } from "socket.io-client";
4
+ function resolveUrl(optionsUrl) {
5
+ if (optionsUrl) return optionsUrl;
6
+ if (typeof import.meta !== "undefined" && import.meta.env?.WSL_URL) {
7
+ return import.meta.env.WSL_URL;
8
+ }
9
+ if (typeof process !== "undefined" && process.env?.WSL_URL) {
10
+ return process.env.WSL_URL;
11
+ }
12
+ return "http://localhost:4000";
13
+ }
14
+ function useJsonifyWs(options = {}) {
15
+ const {
16
+ url: optionsUrl,
17
+ autoConnect = false,
18
+ initialData = {},
19
+ reconnectionDelay = 3e3,
20
+ onSync,
21
+ onStatusChange,
22
+ onError
23
+ } = options;
24
+ const resolvedUrl = resolveUrl(optionsUrl);
25
+ const [data, setDataState] = useState(initialData);
26
+ const [status, setStatus] = useState("disconnected");
27
+ const socketRef = useRef(null);
28
+ const isRemoteUpdate = useRef(false);
29
+ const dataRef = useRef(data);
30
+ dataRef.current = data;
31
+ const updateStatus = useCallback(
32
+ (s) => {
33
+ setStatus(s);
34
+ onStatusChange?.(s);
35
+ },
36
+ [onStatusChange]
37
+ );
38
+ const cleanup = useCallback(() => {
39
+ if (socketRef.current) {
40
+ socketRef.current.disconnect();
41
+ socketRef.current = null;
42
+ }
43
+ updateStatus("disconnected");
44
+ }, [updateStatus]);
45
+ const connect = useCallback(
46
+ (overrideUrl) => {
47
+ cleanup();
48
+ const targetUrl = overrideUrl || resolvedUrl;
49
+ updateStatus("connecting");
50
+ const socket = io(targetUrl, {
51
+ reconnection: true,
52
+ reconnectionDelay,
53
+ reconnectionAttempts: Infinity
54
+ });
55
+ socketRef.current = socket;
56
+ socket.on("connect", () => {
57
+ updateStatus("connected");
58
+ socket.emit("update", dataRef.current);
59
+ });
60
+ socket.on("sync", (syncData) => {
61
+ isRemoteUpdate.current = true;
62
+ setDataState(syncData);
63
+ onSync?.(syncData);
64
+ });
65
+ socket.on("disconnect", () => {
66
+ updateStatus("disconnected");
67
+ });
68
+ socket.on("connect_error", (err) => {
69
+ onError?.(err);
70
+ });
71
+ },
72
+ [cleanup, resolvedUrl, reconnectionDelay, onSync, onStatusChange, onError]
73
+ );
74
+ const disconnect = useCallback(() => {
75
+ cleanup();
76
+ }, [cleanup]);
77
+ const setData = useCallback((newData) => {
78
+ setDataState(newData);
79
+ }, []);
80
+ useEffect(() => {
81
+ if (isRemoteUpdate.current) {
82
+ isRemoteUpdate.current = false;
83
+ return;
84
+ }
85
+ if (socketRef.current?.connected) {
86
+ socketRef.current.emit("update", data);
87
+ }
88
+ }, [data]);
89
+ useEffect(() => {
90
+ if (autoConnect) {
91
+ connect();
92
+ }
93
+ return () => {
94
+ cleanup();
95
+ };
96
+ }, []);
97
+ return {
98
+ data,
99
+ setData,
100
+ status,
101
+ connect,
102
+ disconnect,
103
+ url: resolvedUrl
104
+ };
105
+ }
106
+
107
+ // src/useJsonify.ts
108
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
109
+ import { io as io2 } from "socket.io-client";
110
+
111
+ // src/data-copy.ts
112
+ function getByPath(obj, path) {
113
+ const keys = path.split(".");
114
+ let current = obj;
115
+ for (const key of keys) {
116
+ if (current === null || current === void 0 || typeof current !== "object" || Array.isArray(current)) {
117
+ return void 0;
118
+ }
119
+ current = current[key];
120
+ }
121
+ return current;
122
+ }
123
+ function setByPath(obj, path, value) {
124
+ const keys = path.split(".");
125
+ const result = { ...obj };
126
+ if (keys.length === 1) {
127
+ result[keys[0]] = value;
128
+ return result;
129
+ }
130
+ let current = result;
131
+ for (let i = 0; i < keys.length - 1; i++) {
132
+ const key = keys[i];
133
+ const next = current[key];
134
+ if (next && typeof next === "object" && !Array.isArray(next)) {
135
+ current[key] = { ...next };
136
+ current = current[key];
137
+ } else {
138
+ current[key] = {};
139
+ current = current[key];
140
+ }
141
+ }
142
+ current[keys[keys.length - 1]] = value;
143
+ return result;
144
+ }
145
+ function scanDataCopyElements(doc) {
146
+ const elements = doc.querySelectorAll("[data-copy]");
147
+ return Array.from(elements).map((el) => ({
148
+ element: el,
149
+ path: el.getAttribute("data-copy") || "",
150
+ originalValue: el.textContent || ""
151
+ }));
152
+ }
153
+ function enableEditMode(elements) {
154
+ for (const { element } of elements) {
155
+ element.contentEditable = "true";
156
+ element.style.outline = "2px dashed hsl(var(--primary, 210 100% 50%))";
157
+ element.style.outlineOffset = "2px";
158
+ element.style.cursor = "text";
159
+ element.style.borderRadius = "2px";
160
+ element.style.transition = "outline-color 0.2s";
161
+ element.setAttribute("data-copy-active", "true");
162
+ }
163
+ }
164
+ function disableEditMode(elements) {
165
+ for (const { element } of elements) {
166
+ element.contentEditable = "false";
167
+ element.style.outline = "";
168
+ element.style.outlineOffset = "";
169
+ element.style.cursor = "";
170
+ element.style.borderRadius = "";
171
+ element.style.transition = "";
172
+ element.removeAttribute("data-copy-active");
173
+ }
174
+ }
175
+ function syncElementsFromData(elements, data) {
176
+ for (const item of elements) {
177
+ const val = getByPath(data, item.path);
178
+ if (val !== void 0 && typeof val === "string") {
179
+ if (item.element.textContent !== val) {
180
+ item.element.textContent = val;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ function injectEditToggle(doc, onToggle) {
186
+ const btn = doc.createElement("button");
187
+ btn.id = "jsonify-edit-toggle";
188
+ btn.textContent = "\u270F\uFE0F Edit Mode";
189
+ btn.style.cssText = `
190
+ position: fixed;
191
+ bottom: 20px;
192
+ right: 20px;
193
+ z-index: 99999;
194
+ padding: 8px 16px;
195
+ border: none;
196
+ border-radius: 8px;
197
+ background: hsl(210, 100%, 50%);
198
+ color: white;
199
+ font-size: 13px;
200
+ font-weight: 600;
201
+ cursor: pointer;
202
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
203
+ transition: all 0.2s;
204
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
205
+ `;
206
+ let active = false;
207
+ btn.addEventListener("click", () => {
208
+ active = !active;
209
+ btn.textContent = active ? "\u2705 Editing" : "\u270F\uFE0F Edit Mode";
210
+ btn.style.background = active ? "hsl(140, 60%, 45%)" : "hsl(210, 100%, 50%)";
211
+ onToggle(active);
212
+ });
213
+ doc.body.appendChild(btn);
214
+ return () => {
215
+ btn.remove();
216
+ };
217
+ }
218
+
219
+ // src/useJsonify.ts
220
+ function resolveUrl2(optionsUrl) {
221
+ if (optionsUrl) return optionsUrl;
222
+ if (typeof import.meta !== "undefined" && import.meta.env?.VITE_WSL_URL) {
223
+ return import.meta.env.VITE_WSL_URL;
224
+ }
225
+ if (typeof import.meta !== "undefined" && import.meta.env?.WSL_URL) {
226
+ return import.meta.env.WSL_URL;
227
+ }
228
+ if (typeof process !== "undefined" && process.env?.REACT_APP_WSL_URL) {
229
+ return process.env.REACT_APP_WSL_URL;
230
+ }
231
+ if (typeof process !== "undefined" && process.env?.WSL_URL) {
232
+ return process.env.WSL_URL;
233
+ }
234
+ return "http://localhost:4000";
235
+ }
236
+ function useJsonify(options = {}) {
237
+ const {
238
+ url: optionsUrl,
239
+ autoConnect = false,
240
+ initialData = {},
241
+ reconnectionDelay = 3e3,
242
+ onSync,
243
+ onStatusChange,
244
+ onError,
245
+ targetDocument,
246
+ injectToggle = true
247
+ } = options;
248
+ const resolvedUrl = resolveUrl2(optionsUrl);
249
+ const [data, setDataState] = useState2(initialData);
250
+ const [status, setStatus] = useState2("disconnected");
251
+ const [editMode, setEditModeState] = useState2(false);
252
+ const [elements, setElements] = useState2([]);
253
+ const socketRef = useRef2(null);
254
+ const isRemoteUpdate = useRef2(false);
255
+ const dataRef = useRef2(data);
256
+ const editModeRef = useRef2(editMode);
257
+ const elementsRef = useRef2(elements);
258
+ const cleanupToggleRef = useRef2(null);
259
+ dataRef.current = data;
260
+ editModeRef.current = editMode;
261
+ elementsRef.current = elements;
262
+ const updateStatus = useCallback2(
263
+ (s) => {
264
+ setStatus(s);
265
+ onStatusChange?.(s);
266
+ },
267
+ [onStatusChange]
268
+ );
269
+ const cleanup = useCallback2(() => {
270
+ if (socketRef.current) {
271
+ socketRef.current.disconnect();
272
+ socketRef.current = null;
273
+ }
274
+ updateStatus("disconnected");
275
+ }, [updateStatus]);
276
+ const connect = useCallback2(
277
+ (overrideUrl) => {
278
+ cleanup();
279
+ const targetUrl = overrideUrl || resolvedUrl;
280
+ updateStatus("connecting");
281
+ const socket = io2(targetUrl, {
282
+ reconnection: true,
283
+ reconnectionDelay,
284
+ reconnectionAttempts: Infinity
285
+ });
286
+ socketRef.current = socket;
287
+ socket.on("connect", () => {
288
+ updateStatus("connected");
289
+ socket.emit("update", dataRef.current);
290
+ });
291
+ socket.on("sync", (syncData) => {
292
+ isRemoteUpdate.current = true;
293
+ setDataState(syncData);
294
+ onSync?.(syncData);
295
+ if (editModeRef.current) {
296
+ syncElementsFromData(elementsRef.current, syncData);
297
+ }
298
+ });
299
+ socket.on("disconnect", () => {
300
+ updateStatus("disconnected");
301
+ });
302
+ socket.on("connect_error", (err) => {
303
+ onError?.(err);
304
+ });
305
+ },
306
+ [cleanup, resolvedUrl, reconnectionDelay, onSync, onStatusChange, onError]
307
+ );
308
+ const disconnect = useCallback2(() => {
309
+ cleanup();
310
+ }, [cleanup]);
311
+ const setData = useCallback2((newData) => {
312
+ setDataState(newData);
313
+ }, []);
314
+ const setPath = useCallback2((path, value) => {
315
+ setDataState((prev) => setByPath(prev, path, value));
316
+ }, []);
317
+ useEffect2(() => {
318
+ if (isRemoteUpdate.current) {
319
+ isRemoteUpdate.current = false;
320
+ return;
321
+ }
322
+ if (socketRef.current?.connected) {
323
+ socketRef.current.emit("update", data);
324
+ }
325
+ }, [data]);
326
+ useEffect2(() => {
327
+ if (autoConnect) {
328
+ connect();
329
+ }
330
+ return () => {
331
+ cleanup();
332
+ };
333
+ }, []);
334
+ const rescan = useCallback2(() => {
335
+ const doc = targetDocument || (typeof document !== "undefined" ? document : null);
336
+ if (!doc) return;
337
+ const found = scanDataCopyElements(doc);
338
+ setElements(found);
339
+ return found;
340
+ }, [targetDocument]);
341
+ const setEditMode = useCallback2(
342
+ (active) => {
343
+ setEditModeState(active);
344
+ const currentElements = elementsRef.current.length > 0 ? elementsRef.current : rescan() || [];
345
+ if (active) {
346
+ enableEditMode(currentElements);
347
+ syncElementsFromData(currentElements, dataRef.current);
348
+ for (const item of currentElements) {
349
+ const handler = () => {
350
+ const newVal = item.element.textContent || "";
351
+ setDataState((prev) => setByPath(prev, item.path, newVal));
352
+ };
353
+ item.element.addEventListener("input", handler);
354
+ item.element.__jsonifyHandler = handler;
355
+ }
356
+ } else {
357
+ for (const item of currentElements) {
358
+ if (item.element.__jsonifyHandler) {
359
+ item.element.removeEventListener("input", item.element.__jsonifyHandler);
360
+ delete item.element.__jsonifyHandler;
361
+ }
362
+ }
363
+ disableEditMode(currentElements);
364
+ }
365
+ },
366
+ [rescan]
367
+ );
368
+ const toggleEditMode = useCallback2(() => {
369
+ setEditMode(!editModeRef.current);
370
+ }, [setEditMode]);
371
+ useEffect2(() => {
372
+ if (!injectToggle) return;
373
+ const doc = targetDocument || (typeof document !== "undefined" ? document : null);
374
+ if (!doc) return;
375
+ cleanupToggleRef.current = injectEditToggle(doc, (active) => {
376
+ setEditMode(active);
377
+ });
378
+ return () => {
379
+ cleanupToggleRef.current?.();
380
+ };
381
+ }, [injectToggle, targetDocument, setEditMode]);
382
+ useEffect2(() => {
383
+ rescan();
384
+ }, [rescan]);
385
+ return {
386
+ data,
387
+ setData,
388
+ setPath,
389
+ status,
390
+ connect,
391
+ disconnect,
392
+ url: resolvedUrl,
393
+ editMode,
394
+ toggleEditMode,
395
+ setEditMode,
396
+ elements,
397
+ rescan
398
+ };
399
+ }
400
+ export {
401
+ disableEditMode,
402
+ enableEditMode,
403
+ getByPath,
404
+ injectEditToggle,
405
+ scanDataCopyElements,
406
+ setByPath,
407
+ syncElementsFromData,
408
+ useJsonify,
409
+ useJsonifyWs
410
+ };
411
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useJsonifyWs.ts","../src/useJsonify.ts","../src/data-copy.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\r\nimport { io, Socket } from \"socket.io-client\";\r\nimport type { JsonifyWsOptions, JsonifyWsReturn, JsonValue, WsStatus } from \"./types\";\r\n\r\n/**\r\n * Get the WebSocket URL from options or environment.\r\n * Priority: options.url > WSL_URL env > fallback\r\n */\r\nfunction resolveUrl(optionsUrl?: string): string {\r\n if (optionsUrl) return optionsUrl;\r\n\r\n // Support Vite env\r\n if (typeof import.meta !== \"undefined\" && (import.meta as any).env?.WSL_URL) {\r\n return (import.meta as any).env.WSL_URL;\r\n }\r\n\r\n // Support process.env (Node / CRA / Next.js)\r\n if (typeof process !== \"undefined\" && process.env?.WSL_URL) {\r\n return process.env.WSL_URL;\r\n }\r\n\r\n return \"http://localhost:4000\";\r\n}\r\n\r\n/**\r\n * React hook for real-time JSON synchronization over WebSocket.\r\n *\r\n * @example\r\n * ```tsx\r\n * import { useJsonifyWs } from \"jsonify-ws\";\r\n *\r\n * function App() {\r\n * const { data, setData, status, connect, disconnect } = useJsonifyWs({\r\n * autoConnect: true,\r\n * initialData: { hello: \"world\" },\r\n * });\r\n *\r\n * return <pre>{JSON.stringify(data, null, 2)}</pre>;\r\n * }\r\n * ```\r\n */\r\nexport function useJsonifyWs(options: JsonifyWsOptions = {}): JsonifyWsReturn {\r\n const {\r\n url: optionsUrl,\r\n autoConnect = false,\r\n initialData = {},\r\n reconnectionDelay = 3000,\r\n onSync,\r\n onStatusChange,\r\n onError,\r\n } = options;\r\n\r\n const resolvedUrl = resolveUrl(optionsUrl);\r\n const [data, setDataState] = useState<JsonValue>(initialData);\r\n const [status, setStatus] = useState<WsStatus>(\"disconnected\");\r\n const socketRef = useRef<Socket | null>(null);\r\n const isRemoteUpdate = useRef(false);\r\n const dataRef = useRef(data);\r\n\r\n // Keep dataRef in sync\r\n dataRef.current = data;\r\n\r\n // Notify status changes\r\n const updateStatus = useCallback(\r\n (s: WsStatus) => {\r\n setStatus(s);\r\n onStatusChange?.(s);\r\n },\r\n [onStatusChange],\r\n );\r\n\r\n const cleanup = useCallback(() => {\r\n if (socketRef.current) {\r\n socketRef.current.disconnect();\r\n socketRef.current = null;\r\n }\r\n updateStatus(\"disconnected\");\r\n }, [updateStatus]);\r\n\r\n const connect = useCallback(\r\n (overrideUrl?: string) => {\r\n cleanup();\r\n const targetUrl = overrideUrl || resolvedUrl;\r\n updateStatus(\"connecting\");\r\n\r\n const socket = io(targetUrl, {\r\n reconnection: true,\r\n reconnectionDelay,\r\n reconnectionAttempts: Infinity,\r\n });\r\n socketRef.current = socket;\r\n\r\n socket.on(\"connect\", () => {\r\n updateStatus(\"connected\");\r\n socket.emit(\"update\", dataRef.current);\r\n });\r\n\r\n socket.on(\"sync\", (syncData: JsonValue) => {\r\n isRemoteUpdate.current = true;\r\n setDataState(syncData);\r\n onSync?.(syncData);\r\n });\r\n\r\n socket.on(\"disconnect\", () => {\r\n updateStatus(\"disconnected\");\r\n });\r\n\r\n socket.on(\"connect_error\", (err) => {\r\n onError?.(err);\r\n });\r\n },\r\n [cleanup, resolvedUrl, reconnectionDelay, onSync, onStatusChange, onError],\r\n );\r\n\r\n const disconnect = useCallback(() => {\r\n cleanup();\r\n }, [cleanup]);\r\n\r\n // Public setData — updates local + broadcasts\r\n const setData = useCallback((newData: JsonValue) => {\r\n setDataState(newData);\r\n }, []);\r\n\r\n // Broadcast local changes\r\n useEffect(() => {\r\n if (isRemoteUpdate.current) {\r\n isRemoteUpdate.current = false;\r\n return;\r\n }\r\n if (socketRef.current?.connected) {\r\n socketRef.current.emit(\"update\", data);\r\n }\r\n }, [data]);\r\n\r\n // Auto-connect\r\n useEffect(() => {\r\n if (autoConnect) {\r\n connect();\r\n }\r\n return () => {\r\n cleanup();\r\n };\r\n }, []);\r\n\r\n return {\r\n data,\r\n setData,\r\n status,\r\n connect,\r\n disconnect,\r\n url: resolvedUrl,\r\n };\r\n}\r\n","import { useCallback, useEffect, useRef, useState } from \"react\";\r\nimport { io, Socket } from \"socket.io-client\";\r\nimport type { JsonValue, WsStatus } from \"./types\";\r\nimport {\r\n scanDataCopyElements,\r\n enableEditMode,\r\n disableEditMode,\r\n syncElementsFromData,\r\n setByPath,\r\n injectEditToggle,\r\n type DataCopyElement,\r\n} from \"./data-copy\";\r\n\r\nexport interface UseJsonifyOptions {\r\n /**\r\n * WebSocket server URL. Defaults to WSL_URL env variable.\r\n * Falls back to \"http://localhost:4000\" if not set.\r\n */\r\n url?: string;\r\n\r\n /**\r\n * Auto-connect on mount. Default: false\r\n */\r\n autoConnect?: boolean;\r\n\r\n /**\r\n * Initial JSON data. Default: {}\r\n */\r\n initialData?: Record<string, JsonValue>;\r\n\r\n /**\r\n * Reconnection delay in ms. Default: 3000\r\n */\r\n reconnectionDelay?: number;\r\n\r\n /**\r\n * Callback fired when remote data is received.\r\n */\r\n onSync?: (data: Record<string, JsonValue>) => void;\r\n\r\n /**\r\n * Callback fired when connection status changes.\r\n */\r\n onStatusChange?: (status: WsStatus) => void;\r\n\r\n /**\r\n * Callback fired on connection error.\r\n */\r\n onError?: (error: Error) => void;\r\n\r\n /**\r\n * Target document for data-copy scanning.\r\n * Defaults to window.document.\r\n * Pass an iframe's contentDocument to scan an iframe.\r\n */\r\n targetDocument?: Document;\r\n\r\n /**\r\n * Auto-inject the floating edit toggle button. Default: true\r\n */\r\n injectToggle?: boolean;\r\n}\r\n\r\nexport interface UseJsonifyReturn {\r\n /** Current JSON data state */\r\n data: Record<string, JsonValue>;\r\n /** Update the JSON data (will broadcast to peers) */\r\n setData: (data: Record<string, JsonValue>) => void;\r\n /** Update a single path in the data */\r\n setPath: (path: string, value: JsonValue) => void;\r\n /** Current connection status */\r\n status: WsStatus;\r\n /** Connect to the WebSocket server */\r\n connect: (url?: string) => void;\r\n /** Disconnect from the WebSocket server */\r\n disconnect: () => void;\r\n /** The resolved WebSocket URL */\r\n url: string;\r\n /** Whether edit mode is active */\r\n editMode: boolean;\r\n /** Toggle edit mode on/off */\r\n toggleEditMode: () => void;\r\n /** Set edit mode explicitly */\r\n setEditMode: (active: boolean) => void;\r\n /** Scanned data-copy elements */\r\n elements: DataCopyElement[];\r\n /** Re-scan the document for data-copy elements */\r\n rescan: () => void;\r\n}\r\n\r\nfunction resolveUrl(optionsUrl?: string): string {\r\n if (optionsUrl) return optionsUrl;\r\n\r\n // Support Vite env\r\n if (typeof import.meta !== \"undefined\" && (import.meta as any).env?.VITE_WSL_URL) {\r\n return (import.meta as any).env.VITE_WSL_URL;\r\n }\r\n if (typeof import.meta !== \"undefined\" && (import.meta as any).env?.WSL_URL) {\r\n return (import.meta as any).env.WSL_URL;\r\n }\r\n\r\n // Support process.env (Node / CRA / Next.js)\r\n if (typeof process !== \"undefined\" && process.env?.REACT_APP_WSL_URL) {\r\n return process.env.REACT_APP_WSL_URL;\r\n }\r\n if (typeof process !== \"undefined\" && process.env?.WSL_URL) {\r\n return process.env.WSL_URL;\r\n }\r\n\r\n return \"http://localhost:4000\";\r\n}\r\n\r\n/**\r\n * React hook for real-time JSON synchronization with data-copy attribute support.\r\n *\r\n * @example\r\n * ```tsx\r\n * import { useJsonify } from \"jsonify-ws\";\r\n *\r\n * function App() {\r\n * const j = useJsonify({\r\n * autoConnect: true,\r\n * initialData: { home: { title: \"Hello\" } },\r\n * });\r\n *\r\n * return (\r\n * <div>\r\n * <p>Status: {j.status}</p>\r\n * <button onClick={j.toggleEditMode}>\r\n * {j.editMode ? \"Stop Editing\" : \"Edit Mode\"}\r\n * </button>\r\n * <h1 data-copy=\"home.title\">{j.data.home?.title}</h1>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useJsonify(options: UseJsonifyOptions = {}): UseJsonifyReturn {\r\n const {\r\n url: optionsUrl,\r\n autoConnect = false,\r\n initialData = {},\r\n reconnectionDelay = 3000,\r\n onSync,\r\n onStatusChange,\r\n onError,\r\n targetDocument,\r\n injectToggle = true,\r\n } = options;\r\n\r\n const resolvedUrl = resolveUrl(optionsUrl);\r\n const [data, setDataState] = useState<Record<string, JsonValue>>(initialData);\r\n const [status, setStatus] = useState<WsStatus>(\"disconnected\");\r\n const [editMode, setEditModeState] = useState(false);\r\n const [elements, setElements] = useState<DataCopyElement[]>([]);\r\n\r\n const socketRef = useRef<Socket | null>(null);\r\n const isRemoteUpdate = useRef(false);\r\n const dataRef = useRef(data);\r\n const editModeRef = useRef(editMode);\r\n const elementsRef = useRef(elements);\r\n const cleanupToggleRef = useRef<(() => void) | null>(null);\r\n\r\n dataRef.current = data;\r\n editModeRef.current = editMode;\r\n elementsRef.current = elements;\r\n\r\n const updateStatus = useCallback(\r\n (s: WsStatus) => {\r\n setStatus(s);\r\n onStatusChange?.(s);\r\n },\r\n [onStatusChange],\r\n );\r\n\r\n const cleanup = useCallback(() => {\r\n if (socketRef.current) {\r\n socketRef.current.disconnect();\r\n socketRef.current = null;\r\n }\r\n updateStatus(\"disconnected\");\r\n }, [updateStatus]);\r\n\r\n const connect = useCallback(\r\n (overrideUrl?: string) => {\r\n cleanup();\r\n const targetUrl = overrideUrl || resolvedUrl;\r\n updateStatus(\"connecting\");\r\n\r\n const socket = io(targetUrl, {\r\n reconnection: true,\r\n reconnectionDelay,\r\n reconnectionAttempts: Infinity,\r\n });\r\n socketRef.current = socket;\r\n\r\n socket.on(\"connect\", () => {\r\n updateStatus(\"connected\");\r\n socket.emit(\"update\", dataRef.current);\r\n });\r\n\r\n socket.on(\"sync\", (syncData: Record<string, JsonValue>) => {\r\n isRemoteUpdate.current = true;\r\n setDataState(syncData);\r\n onSync?.(syncData);\r\n // Sync data-copy elements with new data\r\n if (editModeRef.current) {\r\n syncElementsFromData(elementsRef.current, syncData);\r\n }\r\n });\r\n\r\n socket.on(\"disconnect\", () => {\r\n updateStatus(\"disconnected\");\r\n });\r\n\r\n socket.on(\"connect_error\", (err) => {\r\n onError?.(err);\r\n });\r\n },\r\n [cleanup, resolvedUrl, reconnectionDelay, onSync, onStatusChange, onError],\r\n );\r\n\r\n const disconnect = useCallback(() => {\r\n cleanup();\r\n }, [cleanup]);\r\n\r\n const setData = useCallback((newData: Record<string, JsonValue>) => {\r\n setDataState(newData);\r\n }, []);\r\n\r\n const setPath = useCallback((path: string, value: JsonValue) => {\r\n setDataState((prev) => setByPath(prev, path, value));\r\n }, []);\r\n\r\n // Broadcast local changes\r\n useEffect(() => {\r\n if (isRemoteUpdate.current) {\r\n isRemoteUpdate.current = false;\r\n return;\r\n }\r\n if (socketRef.current?.connected) {\r\n socketRef.current.emit(\"update\", data);\r\n }\r\n }, [data]);\r\n\r\n // Auto-connect\r\n useEffect(() => {\r\n if (autoConnect) {\r\n connect();\r\n }\r\n return () => {\r\n cleanup();\r\n };\r\n }, []);\r\n\r\n // Scan for data-copy elements\r\n const rescan = useCallback(() => {\r\n const doc = targetDocument || (typeof document !== \"undefined\" ? document : null);\r\n if (!doc) return;\r\n const found = scanDataCopyElements(doc);\r\n setElements(found);\r\n return found;\r\n }, [targetDocument]);\r\n\r\n // Handle edit mode toggle\r\n const setEditMode = useCallback(\r\n (active: boolean) => {\r\n setEditModeState(active);\r\n const currentElements = elementsRef.current.length > 0 ? elementsRef.current : (rescan() || []);\r\n\r\n if (active) {\r\n enableEditMode(currentElements);\r\n syncElementsFromData(currentElements, dataRef.current);\r\n\r\n // Attach input listeners\r\n for (const item of currentElements) {\r\n const handler = () => {\r\n const newVal = item.element.textContent || \"\";\r\n setDataState((prev) => setByPath(prev, item.path, newVal));\r\n };\r\n item.element.addEventListener(\"input\", handler);\r\n (item.element as any).__jsonifyHandler = handler;\r\n }\r\n } else {\r\n // Remove input listeners\r\n for (const item of currentElements) {\r\n if ((item.element as any).__jsonifyHandler) {\r\n item.element.removeEventListener(\"input\", (item.element as any).__jsonifyHandler);\r\n delete (item.element as any).__jsonifyHandler;\r\n }\r\n }\r\n disableEditMode(currentElements);\r\n }\r\n },\r\n [rescan],\r\n );\r\n\r\n const toggleEditMode = useCallback(() => {\r\n setEditMode(!editModeRef.current);\r\n }, [setEditMode]);\r\n\r\n // Inject floating toggle button\r\n useEffect(() => {\r\n if (!injectToggle) return;\r\n const doc = targetDocument || (typeof document !== \"undefined\" ? document : null);\r\n if (!doc) return;\r\n\r\n cleanupToggleRef.current = injectEditToggle(doc, (active) => {\r\n setEditMode(active);\r\n });\r\n\r\n return () => {\r\n cleanupToggleRef.current?.();\r\n };\r\n }, [injectToggle, targetDocument, setEditMode]);\r\n\r\n // Initial scan\r\n useEffect(() => {\r\n rescan();\r\n }, [rescan]);\r\n\r\n return {\r\n data,\r\n setData,\r\n setPath,\r\n status,\r\n connect,\r\n disconnect,\r\n url: resolvedUrl,\r\n editMode,\r\n toggleEditMode,\r\n setEditMode,\r\n elements,\r\n rescan,\r\n };\r\n}\r\n","/**\r\n * data-copy attribute utilities.\r\n *\r\n * Scans a document (or iframe) for elements with `data-copy=\"path.to.key\"`\r\n * and enables contentEditable on them, syncing edits back to the JSON state.\r\n */\r\n\r\nimport type { JsonValue } from \"./types\";\r\n\r\nexport interface DataCopyElement {\r\n element: HTMLElement;\r\n path: string;\r\n originalValue: string;\r\n}\r\n\r\n/**\r\n * Get a value from a nested object by dot-path.\r\n * e.g. getByPath({ home: { title: \"Hi\" } }, \"home.title\") => \"Hi\"\r\n */\r\nexport function getByPath(obj: Record<string, JsonValue>, path: string): JsonValue | undefined {\r\n const keys = path.split(\".\");\r\n let current: JsonValue = obj;\r\n for (const key of keys) {\r\n if (current === null || current === undefined || typeof current !== \"object\" || Array.isArray(current)) {\r\n return undefined;\r\n }\r\n current = (current as Record<string, JsonValue>)[key];\r\n }\r\n return current;\r\n}\r\n\r\n/**\r\n * Set a value in a nested object by dot-path (immutable — returns new object).\r\n */\r\nexport function setByPath(obj: Record<string, JsonValue>, path: string, value: JsonValue): Record<string, JsonValue> {\r\n const keys = path.split(\".\");\r\n const result = { ...obj };\r\n\r\n if (keys.length === 1) {\r\n result[keys[0]] = value;\r\n return result;\r\n }\r\n\r\n let current: Record<string, JsonValue> = result;\r\n for (let i = 0; i < keys.length - 1; i++) {\r\n const key = keys[i];\r\n const next = current[key];\r\n if (next && typeof next === \"object\" && !Array.isArray(next)) {\r\n current[key] = { ...(next as Record<string, JsonValue>) };\r\n current = current[key] as Record<string, JsonValue>;\r\n } else {\r\n current[key] = {};\r\n current = current[key] as Record<string, JsonValue>;\r\n }\r\n }\r\n current[keys[keys.length - 1]] = value;\r\n return result;\r\n}\r\n\r\n/**\r\n * Scan a document for elements with `data-copy` attribute.\r\n */\r\nexport function scanDataCopyElements(doc: Document): DataCopyElement[] {\r\n const elements = doc.querySelectorAll<HTMLElement>(\"[data-copy]\");\r\n return Array.from(elements).map((el) => ({\r\n element: el,\r\n path: el.getAttribute(\"data-copy\") || \"\",\r\n originalValue: el.textContent || \"\",\r\n }));\r\n}\r\n\r\n/**\r\n * Apply contentEditable styling to data-copy elements.\r\n */\r\nexport function enableEditMode(elements: DataCopyElement[]): void {\r\n for (const { element } of elements) {\r\n element.contentEditable = \"true\";\r\n element.style.outline = \"2px dashed hsl(var(--primary, 210 100% 50%))\";\r\n element.style.outlineOffset = \"2px\";\r\n element.style.cursor = \"text\";\r\n element.style.borderRadius = \"2px\";\r\n element.style.transition = \"outline-color 0.2s\";\r\n element.setAttribute(\"data-copy-active\", \"true\");\r\n }\r\n}\r\n\r\n/**\r\n * Remove contentEditable styling from data-copy elements.\r\n */\r\nexport function disableEditMode(elements: DataCopyElement[]): void {\r\n for (const { element } of elements) {\r\n element.contentEditable = \"false\";\r\n element.style.outline = \"\";\r\n element.style.outlineOffset = \"\";\r\n element.style.cursor = \"\";\r\n element.style.borderRadius = \"\";\r\n element.style.transition = \"\";\r\n element.removeAttribute(\"data-copy-active\");\r\n }\r\n}\r\n\r\n/**\r\n * Update element text content from JSON data.\r\n */\r\nexport function syncElementsFromData(\r\n elements: DataCopyElement[],\r\n data: Record<string, JsonValue>,\r\n): void {\r\n for (const item of elements) {\r\n const val = getByPath(data, item.path);\r\n if (val !== undefined && typeof val === \"string\") {\r\n if (item.element.textContent !== val) {\r\n item.element.textContent = val;\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Inject a floating \"Edit Mode\" toggle button into the page.\r\n * Returns a cleanup function.\r\n */\r\nexport function injectEditToggle(\r\n doc: Document,\r\n onToggle: (active: boolean) => void,\r\n): () => void {\r\n const btn = doc.createElement(\"button\");\r\n btn.id = \"jsonify-edit-toggle\";\r\n btn.textContent = \"✏️ Edit Mode\";\r\n btn.style.cssText = `\r\n position: fixed;\r\n bottom: 20px;\r\n right: 20px;\r\n z-index: 99999;\r\n padding: 8px 16px;\r\n border: none;\r\n border-radius: 8px;\r\n background: hsl(210, 100%, 50%);\r\n color: white;\r\n font-size: 13px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n box-shadow: 0 4px 12px rgba(0,0,0,0.15);\r\n transition: all 0.2s;\r\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\r\n `;\r\n\r\n let active = false;\r\n btn.addEventListener(\"click\", () => {\r\n active = !active;\r\n btn.textContent = active ? \"✅ Editing\" : \"✏️ Edit Mode\";\r\n btn.style.background = active ? \"hsl(140, 60%, 45%)\" : \"hsl(210, 100%, 50%)\";\r\n onToggle(active);\r\n });\r\n\r\n doc.body.appendChild(btn);\r\n\r\n return () => {\r\n btn.remove();\r\n };\r\n}\r\n"],"mappings":"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","names":["useCallback","useEffect","useRef","useState","io","resolveUrl","useState","useRef","useCallback","io","useEffect"]}
@@ -0,0 +1,12 @@
1
+ import * as socket_io from 'socket.io';
2
+ import { Server } from 'socket.io';
3
+
4
+ /**
5
+ * Create and start a jsonify-ws sync server.
6
+ *
7
+ * @param port - Port number (default: WSL_URL port or 4000)
8
+ * @param corsOrigin - CORS origin (default: "*")
9
+ */
10
+ declare function createJsonifyServer(port?: number, corsOrigin?: string | string[]): Server<socket_io.DefaultEventsMap, socket_io.DefaultEventsMap, socket_io.DefaultEventsMap, any>;
11
+
12
+ export { createJsonifyServer };
@@ -0,0 +1,12 @@
1
+ import * as socket_io from 'socket.io';
2
+ import { Server } from 'socket.io';
3
+
4
+ /**
5
+ * Create and start a jsonify-ws sync server.
6
+ *
7
+ * @param port - Port number (default: WSL_URL port or 4000)
8
+ * @param corsOrigin - CORS origin (default: "*")
9
+ */
10
+ declare function createJsonifyServer(port?: number, corsOrigin?: string | string[]): Server<socket_io.DefaultEventsMap, socket_io.DefaultEventsMap, socket_io.DefaultEventsMap, any>;
11
+
12
+ export { createJsonifyServer };
package/dist/server.js ADDED
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ createJsonifyServer: () => createJsonifyServer
24
+ });
25
+ module.exports = __toCommonJS(server_exports);
26
+ var import_socket = require("socket.io");
27
+ function createJsonifyServer(port = Number(process.env.WS_PORT) || 4e3, corsOrigin = "*") {
28
+ const io = new import_socket.Server(port, {
29
+ cors: {
30
+ origin: corsOrigin,
31
+ methods: ["GET", "POST"]
32
+ }
33
+ });
34
+ let latestData = null;
35
+ io.on("connection", (socket) => {
36
+ console.log(`[jsonify-ws] Client connected (${io.engine.clientsCount} total)`);
37
+ if (latestData !== null) {
38
+ socket.emit("sync", latestData);
39
+ }
40
+ socket.on("update", (data) => {
41
+ latestData = data;
42
+ socket.broadcast.emit("sync", latestData);
43
+ });
44
+ socket.on("disconnect", () => {
45
+ console.log(`[jsonify-ws] Client disconnected (${io.engine.clientsCount} total)`);
46
+ });
47
+ socket.on("error", (err) => {
48
+ console.error("[jsonify-ws] Socket error:", err);
49
+ });
50
+ });
51
+ console.log(`[jsonify-ws] Server running on http://localhost:${port}`);
52
+ return io;
53
+ }
54
+ if (typeof require !== "undefined" && require.main === module) {
55
+ createJsonifyServer();
56
+ }
57
+ var isMainModule = typeof process !== "undefined" && process.argv[1] && (process.argv[1].endsWith("/server.ts") || process.argv[1].endsWith("/server.js") || process.argv[1].endsWith("/server.mjs"));
58
+ if (isMainModule) {
59
+ createJsonifyServer();
60
+ }
61
+ // Annotate the CommonJS export names for ESM import in node:
62
+ 0 && (module.exports = {
63
+ createJsonifyServer
64
+ });
65
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { Server } from \"socket.io\";\r\n\r\ntype JsonValue =\r\n | string\r\n | number\r\n | boolean\r\n | null\r\n | JsonValue[]\r\n | { [key: string]: JsonValue };\r\n\r\n/**\r\n * Create and start a jsonify-ws sync server.\r\n *\r\n * @param port - Port number (default: WSL_URL port or 4000)\r\n * @param corsOrigin - CORS origin (default: \"*\")\r\n */\r\nexport function createJsonifyServer(\r\n port: number = Number(process.env.WS_PORT) || 4000,\r\n corsOrigin: string | string[] = \"*\",\r\n) {\r\n const io = new Server(port, {\r\n cors: {\r\n origin: corsOrigin,\r\n methods: [\"GET\", \"POST\"],\r\n },\r\n });\r\n\r\n let latestData: JsonValue | null = null;\r\n\r\n io.on(\"connection\", (socket) => {\r\n console.log(`[jsonify-ws] Client connected (${io.engine.clientsCount} total)`);\r\n\r\n if (latestData !== null) {\r\n socket.emit(\"sync\", latestData);\r\n }\r\n\r\n socket.on(\"update\", (data: JsonValue) => {\r\n latestData = data;\r\n socket.broadcast.emit(\"sync\", latestData);\r\n });\r\n\r\n socket.on(\"disconnect\", () => {\r\n console.log(`[jsonify-ws] Client disconnected (${io.engine.clientsCount} total)`);\r\n });\r\n\r\n socket.on(\"error\", (err) => {\r\n console.error(\"[jsonify-ws] Socket error:\", err);\r\n });\r\n });\r\n\r\n console.log(`[jsonify-ws] Server running on http://localhost:${port}`);\r\n return io;\r\n}\r\n\r\n// Auto-start when run directly\r\nif (typeof require !== \"undefined\" && require.main === module) {\r\n createJsonifyServer();\r\n}\r\n\r\n// Also auto-start for ESM direct execution\r\nconst isMainModule =\r\n typeof process !== \"undefined\" &&\r\n process.argv[1] &&\r\n (process.argv[1].endsWith(\"/server.ts\") ||\r\n process.argv[1].endsWith(\"/server.js\") ||\r\n process.argv[1].endsWith(\"/server.mjs\"));\r\n\r\nif (isMainModule) {\r\n createJsonifyServer();\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAuB;AAgBhB,SAAS,oBACd,OAAe,OAAO,QAAQ,IAAI,OAAO,KAAK,KAC9C,aAAgC,KAChC;AACA,QAAM,KAAK,IAAI,qBAAO,MAAM;AAAA,IAC1B,MAAM;AAAA,MACJ,QAAQ;AAAA,MACR,SAAS,CAAC,OAAO,MAAM;AAAA,IACzB;AAAA,EACF,CAAC;AAED,MAAI,aAA+B;AAEnC,KAAG,GAAG,cAAc,CAAC,WAAW;AAC9B,YAAQ,IAAI,kCAAkC,GAAG,OAAO,YAAY,SAAS;AAE7E,QAAI,eAAe,MAAM;AACvB,aAAO,KAAK,QAAQ,UAAU;AAAA,IAChC;AAEA,WAAO,GAAG,UAAU,CAAC,SAAoB;AACvC,mBAAa;AACb,aAAO,UAAU,KAAK,QAAQ,UAAU;AAAA,IAC1C,CAAC;AAED,WAAO,GAAG,cAAc,MAAM;AAC5B,cAAQ,IAAI,qCAAqC,GAAG,OAAO,YAAY,SAAS;AAAA,IAClF,CAAC;AAED,WAAO,GAAG,SAAS,CAAC,QAAQ;AAC1B,cAAQ,MAAM,8BAA8B,GAAG;AAAA,IACjD,CAAC;AAAA,EACH,CAAC;AAED,UAAQ,IAAI,mDAAmD,IAAI,EAAE;AACrE,SAAO;AACT;AAGA,IAAI,OAAO,YAAY,eAAe,QAAQ,SAAS,QAAQ;AAC7D,sBAAoB;AACtB;AAGA,IAAM,eACJ,OAAO,YAAY,eACnB,QAAQ,KAAK,CAAC,MACb,QAAQ,KAAK,CAAC,EAAE,SAAS,YAAY,KACpC,QAAQ,KAAK,CAAC,EAAE,SAAS,YAAY,KACrC,QAAQ,KAAK,CAAC,EAAE,SAAS,aAAa;AAE1C,IAAI,cAAc;AAChB,sBAAoB;AACtB;","names":[]}
@@ -0,0 +1,47 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/server.ts
9
+ import { Server } from "socket.io";
10
+ function createJsonifyServer(port = Number(process.env.WS_PORT) || 4e3, corsOrigin = "*") {
11
+ const io = new Server(port, {
12
+ cors: {
13
+ origin: corsOrigin,
14
+ methods: ["GET", "POST"]
15
+ }
16
+ });
17
+ let latestData = null;
18
+ io.on("connection", (socket) => {
19
+ console.log(`[jsonify-ws] Client connected (${io.engine.clientsCount} total)`);
20
+ if (latestData !== null) {
21
+ socket.emit("sync", latestData);
22
+ }
23
+ socket.on("update", (data) => {
24
+ latestData = data;
25
+ socket.broadcast.emit("sync", latestData);
26
+ });
27
+ socket.on("disconnect", () => {
28
+ console.log(`[jsonify-ws] Client disconnected (${io.engine.clientsCount} total)`);
29
+ });
30
+ socket.on("error", (err) => {
31
+ console.error("[jsonify-ws] Socket error:", err);
32
+ });
33
+ });
34
+ console.log(`[jsonify-ws] Server running on http://localhost:${port}`);
35
+ return io;
36
+ }
37
+ if (typeof __require !== "undefined" && __require.main === module) {
38
+ createJsonifyServer();
39
+ }
40
+ var isMainModule = typeof process !== "undefined" && process.argv[1] && (process.argv[1].endsWith("/server.ts") || process.argv[1].endsWith("/server.js") || process.argv[1].endsWith("/server.mjs"));
41
+ if (isMainModule) {
42
+ createJsonifyServer();
43
+ }
44
+ export {
45
+ createJsonifyServer
46
+ };
47
+ //# sourceMappingURL=server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import { Server } from \"socket.io\";\r\n\r\ntype JsonValue =\r\n | string\r\n | number\r\n | boolean\r\n | null\r\n | JsonValue[]\r\n | { [key: string]: JsonValue };\r\n\r\n/**\r\n * Create and start a jsonify-ws sync server.\r\n *\r\n * @param port - Port number (default: WSL_URL port or 4000)\r\n * @param corsOrigin - CORS origin (default: \"*\")\r\n */\r\nexport function createJsonifyServer(\r\n port: number = Number(process.env.WS_PORT) || 4000,\r\n corsOrigin: string | string[] = \"*\",\r\n) {\r\n const io = new Server(port, {\r\n cors: {\r\n origin: corsOrigin,\r\n methods: [\"GET\", \"POST\"],\r\n },\r\n });\r\n\r\n let latestData: JsonValue | null = null;\r\n\r\n io.on(\"connection\", (socket) => {\r\n console.log(`[jsonify-ws] Client connected (${io.engine.clientsCount} total)`);\r\n\r\n if (latestData !== null) {\r\n socket.emit(\"sync\", latestData);\r\n }\r\n\r\n socket.on(\"update\", (data: JsonValue) => {\r\n latestData = data;\r\n socket.broadcast.emit(\"sync\", latestData);\r\n });\r\n\r\n socket.on(\"disconnect\", () => {\r\n console.log(`[jsonify-ws] Client disconnected (${io.engine.clientsCount} total)`);\r\n });\r\n\r\n socket.on(\"error\", (err) => {\r\n console.error(\"[jsonify-ws] Socket error:\", err);\r\n });\r\n });\r\n\r\n console.log(`[jsonify-ws] Server running on http://localhost:${port}`);\r\n return io;\r\n}\r\n\r\n// Auto-start when run directly\r\nif (typeof require !== \"undefined\" && require.main === module) {\r\n createJsonifyServer();\r\n}\r\n\r\n// Also auto-start for ESM direct execution\r\nconst isMainModule =\r\n typeof process !== \"undefined\" &&\r\n process.argv[1] &&\r\n (process.argv[1].endsWith(\"/server.ts\") ||\r\n process.argv[1].endsWith(\"/server.js\") ||\r\n process.argv[1].endsWith(\"/server.mjs\"));\r\n\r\nif (isMainModule) {\r\n createJsonifyServer();\r\n}\r\n"],"mappings":";;;;;;;;AAAA,SAAS,cAAc;AAgBhB,SAAS,oBACd,OAAe,OAAO,QAAQ,IAAI,OAAO,KAAK,KAC9C,aAAgC,KAChC;AACA,QAAM,KAAK,IAAI,OAAO,MAAM;AAAA,IAC1B,MAAM;AAAA,MACJ,QAAQ;AAAA,MACR,SAAS,CAAC,OAAO,MAAM;AAAA,IACzB;AAAA,EACF,CAAC;AAED,MAAI,aAA+B;AAEnC,KAAG,GAAG,cAAc,CAAC,WAAW;AAC9B,YAAQ,IAAI,kCAAkC,GAAG,OAAO,YAAY,SAAS;AAE7E,QAAI,eAAe,MAAM;AACvB,aAAO,KAAK,QAAQ,UAAU;AAAA,IAChC;AAEA,WAAO,GAAG,UAAU,CAAC,SAAoB;AACvC,mBAAa;AACb,aAAO,UAAU,KAAK,QAAQ,UAAU;AAAA,IAC1C,CAAC;AAED,WAAO,GAAG,cAAc,MAAM;AAC5B,cAAQ,IAAI,qCAAqC,GAAG,OAAO,YAAY,SAAS;AAAA,IAClF,CAAC;AAED,WAAO,GAAG,SAAS,CAAC,QAAQ;AAC1B,cAAQ,MAAM,8BAA8B,GAAG;AAAA,IACjD,CAAC;AAAA,EACH,CAAC;AAED,UAAQ,IAAI,mDAAmD,IAAI,EAAE;AACrE,SAAO;AACT;AAGA,IAAI,OAAO,cAAY,eAAe,UAAQ,SAAS,QAAQ;AAC7D,sBAAoB;AACtB;AAGA,IAAM,eACJ,OAAO,YAAY,eACnB,QAAQ,KAAK,CAAC,MACb,QAAQ,KAAK,CAAC,EAAE,SAAS,YAAY,KACpC,QAAQ,KAAK,CAAC,EAAE,SAAS,YAAY,KACrC,QAAQ,KAAK,CAAC,EAAE,SAAS,aAAa;AAE1C,IAAI,cAAc;AAChB,sBAAoB;AACtB;","names":[]}