ziex 0.1.0-dev.526 → 0.1.0-dev.562

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 CHANGED
@@ -122,6 +122,7 @@ const zx = @import("zx");
122
122
  - [x] Page
123
123
  - [ ] Assets
124
124
  - [x] API Route
125
+ - [x] Websocket Route
125
126
  - [ ] Plugin (_Alpha_)
126
127
  - [x] Builtin TailwindCSS and Esbuild
127
128
  - [x] Command based plugin system
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  var zx = {
3
3
  name: "zx",
4
- version: "0.1.0-dev.526",
4
+ version: "0.1.0-dev.562",
5
5
  description: "ZX is a framework for building web applications with Zig.",
6
6
  repository: "https://github.com/nurulhudaapon/zx",
7
7
  fingerprint: 14616285862371232000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ziex",
3
- "version": "0.1.0-dev.526",
3
+ "version": "0.1.0-dev.562",
4
4
  "description": "ZX is a framework for building web applications with Zig.",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/react/dom.d.ts CHANGED
@@ -1,162 +1,21 @@
1
1
  import type { ComponentMetadata } from "./types";
2
- /**
3
- * Result of preparing a component for hydration.
4
- *
5
- * Contains all the necessary data to render a React component into its server-rendered container.
6
- */
7
2
  export type PreparedComponent = {
8
- /**
9
- * The HTML element where the component should be rendered.
10
- *
11
- * This is a container element created between the comment markers. The server-rendered
12
- * content is moved into this container, and React will hydrate it with the interactive component.
13
- *
14
- * @example
15
- * ```tsx
16
- * // Server-rendered HTML with comment markers:
17
- * // <!--$zx-abc123-0 CounterComponent {"max_count":10}-->
18
- * // <button>0</button>
19
- * // <!--/$zx-abc123-0-->
20
- *
21
- * const { domNode } = await prepareComponent(component);
22
- * createRoot(domNode).render(<Component {...props} />);
23
- * ```
24
- */
25
3
  domNode: HTMLElement;
26
- /**
27
- * Component props parsed from the comment marker.
28
- *
29
- * Props are extracted from the start comment marker content. The comment format is:
30
- * `<!--$id name props-->` where props is JSON-encoded.
31
- *
32
- * @example
33
- * ```tsx
34
- * // Server-rendered HTML:
35
- * // <!--$zx-abc123-0 CounterComponent {"max_count":10,"label":"Counter"}-->
36
- * // <button>0</button>
37
- * // <!--/$zx-abc123-0-->
38
- *
39
- * const { props } = await prepareComponent(component);
40
- * // props = { max_count: 10, label: "Counter" }
41
- * ```
42
- */
43
4
  props: Record<string, any> & {
44
- /**
45
- * React's special prop for setting inner HTML directly.
46
- *
47
- * May be used when the component has server-rendered children that should
48
- * be preserved during hydration.
49
- */
50
5
  dangerouslySetInnerHTML?: {
51
6
  __html: string;
52
7
  };
53
8
  };
54
- /**
55
- * The loaded React component function ready to render.
56
- *
57
- * This is the default export from the component module, lazy-loaded via the component's
58
- * import function. The component is ready to be rendered with React's `createRoot().render()`.
59
- *
60
- * @example
61
- * ```tsx
62
- * const { Component, props, domNode } = await prepareComponent(component);
63
- *
64
- * // Component is the default export from the component file:
65
- * // export default function CounterComponent({ max_count }: { max_count: number }) {
66
- * // return <div>Count: {max_count}</div>;
67
- * // }
68
- *
69
- * createRoot(domNode).render(<Component {...props} />);
70
- * ```
71
- */
72
9
  Component: (props: any) => React.ReactElement;
73
10
  };
74
- /**
75
- * Prepares a client-side component for hydration by locating its comment markers, extracting
76
- * props from the marker content, and lazy-loading the component module.
77
- *
78
- * This function bridges server-rendered HTML (from ZX's Zig transpiler) and client-side React
79
- * components. It searches for comment markers in the format `<!--$id name props-->...<!--/$id-->`
80
- * and extracts the component data from the marker content.
81
- *
82
- * @param component - The component metadata containing ID, import function, and other metadata
83
- * needed to locate and load the component
84
- *
85
- * @returns A Promise that resolves to a `PreparedComponent` object containing the DOM node,
86
- * parsed props, and the loaded React component function
87
- *
88
- * @throws {Error} If the component's comment markers cannot be found in the DOM. This typically
89
- * happens if the component ID doesn't match any marker, the script runs before
90
- * the HTML is loaded, or there's a mismatch between server and client metadata
91
- *
92
- * @example
93
- * ```tsx
94
- * // Basic usage with React:
95
- * import { createRoot } from "react-dom/client";
96
- * import { prepareComponent } from "ziex";
97
- * import { components } from "@ziex/components";
98
- *
99
- * for (const component of components) {
100
- * prepareComponent(component).then(({ domNode, Component, props }) => {
101
- * createRoot(domNode).render(<Component {...props} />);
102
- * }).catch(console.error);
103
- * }
104
- * ```
105
- *
106
- * @example
107
- * ```tsx
108
- * // Server-rendered HTML with comment markers:
109
- * // <!--$zx-abc123-0 CounterComponent {"max_count":10}-->
110
- * // <button>0</button>
111
- * // <!--/$zx-abc123-0-->
112
- * ```
113
- */
114
11
  export declare function prepareComponent(component: ComponentMetadata): Promise<PreparedComponent>;
115
12
  export declare function filterComponents(components: ComponentMetadata[]): ComponentMetadata[];
116
- /**
117
- * Discovered component from DOM traversal.
118
- * Contains all metadata needed to hydrate the component.
119
- */
120
13
  export type DiscoveredComponent = {
121
14
  id: string;
122
15
  name: string;
123
16
  props: Record<string, any>;
124
17
  container: HTMLElement;
125
18
  };
126
- /**
127
- * Finds all React component markers in the DOM and returns their metadata.
128
- *
129
- * This is a DOM-first approach that:
130
- * 1. Walks the DOM once to find all `<!--$id-->` markers
131
- * 2. Reads metadata from companion `<script data-zx="id">` elements
132
- * 3. Creates containers for React to render into
133
- *
134
- * @returns Array of discovered components with their containers and props
135
- */
136
19
  export declare function discoverComponents(): DiscoveredComponent[];
137
- /**
138
- * Component registry mapping component names to their import functions.
139
- */
140
20
  export type ComponentRegistry = Record<string, () => Promise<(props: any) => React.ReactElement>>;
141
- /**
142
- * Hydrates all React components found in the DOM.
143
- *
144
- * This is the simplest way to hydrate React islands - it automatically:
145
- * 1. Discovers all component markers in the DOM
146
- * 2. Looks up components by name in the registry
147
- * 3. Renders each component into its container
148
- *
149
- * @param registry - Map of component names to import functions
150
- * @param render - Function to render a component (e.g., `(el, Component, props) => createRoot(el).render(<Component {...props} />)`)
151
- *
152
- * @example
153
- * ```ts
154
- * import { hydrateAll } from "ziex/react";
155
- *
156
- * hydrateAll({
157
- * CounterComponent: () => import("./Counter"),
158
- * ToggleComponent: () => import("./Toggle"),
159
- * });
160
- * ```
161
- */
162
21
  export declare function hydrateAll(registry: ComponentRegistry, render: (container: HTMLElement, Component: (props: any) => React.ReactElement, props: Record<string, any>) => void): Promise<void>;
package/react/index.js CHANGED
@@ -1,15 +1,28 @@
1
1
  // src/react/dom.ts
2
2
  function findCommentMarker(id) {
3
- const startMarker = `$${id}`;
3
+ const startPrefix = `$${id} `;
4
4
  const endMarker = `/$${id}`;
5
5
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT, null);
6
6
  let startComment = null;
7
7
  let endComment = null;
8
+ let name = "";
9
+ let props = {};
8
10
  let node;
9
11
  while (node = walker.nextNode()) {
10
12
  const text = node.textContent?.trim() || "";
11
- if (text === startMarker) {
13
+ if (text.startsWith(startPrefix)) {
12
14
  startComment = node;
15
+ const content = text.slice(startPrefix.length);
16
+ const jsonStart = content.indexOf("{");
17
+ if (jsonStart !== -1) {
18
+ name = content.slice(0, jsonStart).trim();
19
+ const jsonStr = content.slice(jsonStart);
20
+ try {
21
+ props = JSON.parse(jsonStr);
22
+ } catch {}
23
+ } else {
24
+ name = content.trim();
25
+ }
13
26
  }
14
27
  if (text === endMarker) {
15
28
  endComment = node;
@@ -17,20 +30,10 @@ function findCommentMarker(id) {
17
30
  }
18
31
  }
19
32
  if (startComment && endComment) {
20
- return { startComment, endComment };
33
+ return { startComment, endComment, name, props };
21
34
  }
22
35
  return null;
23
36
  }
24
- function getComponentMetadata(id) {
25
- const script = document.querySelector(`script[data-zx="${id}"]`);
26
- if (script?.textContent) {
27
- try {
28
- const data = JSON.parse(script.textContent);
29
- return { name: data.name || "", props: data.props || {} };
30
- } catch {}
31
- }
32
- return { name: "", props: {} };
33
- }
34
37
  function createContainerBetweenMarkers(startComment, endComment) {
35
38
  const container = document.createElement("div");
36
39
  container.style.display = "contents";
@@ -48,8 +51,7 @@ async function prepareComponent(component) {
48
51
  if (!marker) {
49
52
  throw new Error(`Comment marker for ${component.id} not found`, { cause: component });
50
53
  }
51
- const metadata = getComponentMetadata(component.id);
52
- const props = metadata.props;
54
+ const props = marker.props;
53
55
  const domNode = createContainerBetweenMarkers(marker.startComment, marker.endComment);
54
56
  const Component = await component.import();
55
57
  return { domNode, props, Component };
@@ -60,25 +62,50 @@ function filterComponents(components) {
60
62
  }
61
63
  function discoverComponents() {
62
64
  const components = [];
63
- const scripts = Array.from(document.querySelectorAll("script[data-zx]"));
64
- for (const script of scripts) {
65
- const id = script.getAttribute("data-zx");
66
- if (!id)
67
- continue;
68
- let name = "";
69
- let props = {};
70
- try {
71
- const data = JSON.parse(script.textContent || "{}");
72
- name = data.name || "";
73
- props = data.props || {};
74
- } catch {
75
- continue;
65
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT, null);
66
+ const markers = [];
67
+ let node;
68
+ while (node = walker.nextNode()) {
69
+ const text = node.textContent?.trim() || "";
70
+ if (text.startsWith("$") && !text.startsWith("/$")) {
71
+ const spaceIdx = text.indexOf(" ");
72
+ if (spaceIdx !== -1) {
73
+ const id = text.slice(1, spaceIdx);
74
+ const content = text.slice(spaceIdx + 1);
75
+ if (content.startsWith("[")) {
76
+ continue;
77
+ }
78
+ const jsonStart = content.indexOf("{");
79
+ let name = "";
80
+ let props = {};
81
+ if (jsonStart !== -1) {
82
+ name = content.slice(0, jsonStart).trim();
83
+ try {
84
+ props = JSON.parse(content.slice(jsonStart));
85
+ } catch {}
86
+ } else {
87
+ name = content.trim();
88
+ }
89
+ markers.push({ id, name, props, startComment: node, endComment: null });
90
+ }
91
+ } else if (text.startsWith("/$")) {
92
+ const id = text.slice(2);
93
+ const marker = markers.find((m) => m.id === id && !m.endComment);
94
+ if (marker) {
95
+ marker.endComment = node;
96
+ }
76
97
  }
77
- const marker = findCommentMarker(id);
78
- if (!marker)
98
+ }
99
+ for (const marker of markers) {
100
+ if (!marker.endComment)
79
101
  continue;
80
102
  const container = createContainerBetweenMarkers(marker.startComment, marker.endComment);
81
- components.push({ id, name, props, container });
103
+ components.push({
104
+ id: marker.id,
105
+ name: marker.name,
106
+ props: marker.props,
107
+ container
108
+ });
82
109
  }
83
110
  return components;
84
111
  }
package/wasm/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ZigJS } from "../../../../vendor/jsz/js/src";
2
2
  /**
3
3
  * ZX Client Bridge - Unified JS↔WASM communication layer
4
- * Handles events, fetch, timers, and other async callbacks using jsz
4
+ * Handles events, fetch, WebSocket, timers, and other async callbacks using jsz
5
5
  */
6
6
  export declare const CallbackType: {
7
7
  readonly Event: 0;
@@ -9,6 +9,10 @@ export declare const CallbackType: {
9
9
  readonly FetchError: 2;
10
10
  readonly Timeout: 3;
11
11
  readonly Interval: 4;
12
+ readonly WebSocketOpen: 5;
13
+ readonly WebSocketMessage: 6;
14
+ readonly WebSocketError: 7;
15
+ readonly WebSocketClose: 8;
12
16
  };
13
17
  export type CallbackTypeValue = typeof CallbackType[keyof typeof CallbackType];
14
18
  export declare const jsz: ZigJS;
@@ -18,14 +22,26 @@ export declare function storeValueGetRef(val: any): bigint;
18
22
  export declare class ZxBridge {
19
23
  #private;
20
24
  constructor(exports: WebAssembly.Exports);
21
- /** Fetch a URL and callback with the response */
22
- fetch(urlPtr: number, urlLen: number, callbackId: bigint): void;
25
+ /**
26
+ * Async fetch with full options support.
27
+ * Calls __zx_fetch_complete when done.
28
+ */
29
+ fetchAsync(urlPtr: number, urlLen: number, methodPtr: number, methodLen: number, headersPtr: number, headersLen: number, bodyPtr: number, bodyLen: number, timeoutMs: number, fetchId: bigint): void;
23
30
  /** Set a timeout and callback when it fires */
24
31
  setTimeout(callbackId: bigint, delayMs: number): void;
25
32
  /** Set an interval and callback each time it fires */
26
33
  setInterval(callbackId: bigint, intervalMs: number): void;
27
34
  /** Clear an interval */
28
35
  clearInterval(callbackId: bigint): void;
36
+ /**
37
+ * Create and connect a WebSocket.
38
+ * Calls __zx_ws_onopen, __zx_ws_onmessage, __zx_ws_onerror, __zx_ws_onclose.
39
+ */
40
+ wsConnect(wsId: bigint, urlPtr: number, urlLen: number, protocolsPtr: number, protocolsLen: number): void;
41
+ /** Send data over WebSocket */
42
+ wsSend(wsId: bigint, dataPtr: number, dataLen: number, isBinary: number): void;
43
+ /** Close WebSocket connection */
44
+ wsClose(wsId: bigint, code: number, reasonPtr: number, reasonLen: number): void;
29
45
  /** Handle a DOM event (called by event delegation) */
30
46
  eventbridge(velementId: bigint, eventTypeId: number, event: Event): void;
31
47
  /** Create the import object for WASM instantiation */
package/wasm/index.js CHANGED
@@ -173,7 +173,11 @@ var CallbackType = {
173
173
  FetchSuccess: 1,
174
174
  FetchError: 2,
175
175
  Timeout: 3,
176
- Interval: 4
176
+ Interval: 4,
177
+ WebSocketOpen: 5,
178
+ WebSocketMessage: 6,
179
+ WebSocketError: 7,
180
+ WebSocketClose: 8
177
181
  };
178
182
  var jsz = new ZigJS;
179
183
  var tempRefBuffer = new ArrayBuffer(8);
@@ -189,19 +193,35 @@ function readString(ptr, len) {
189
193
  const memory = new Uint8Array(jsz.memory.buffer);
190
194
  return new TextDecoder().decode(memory.slice(ptr, ptr + len));
191
195
  }
196
+ function writeBytes(ptr, data) {
197
+ const memory = new Uint8Array(jsz.memory.buffer);
198
+ memory.set(data, ptr);
199
+ }
192
200
 
193
201
  class ZxBridge {
194
202
  #exports;
195
- #nextCallbackId = BigInt(1);
196
203
  #intervals = new Map;
204
+ #websockets = new Map;
197
205
  constructor(exports) {
198
206
  this.#exports = exports;
199
207
  }
200
208
  get #handler() {
201
209
  return this.#exports.__zx_cb;
202
210
  }
203
- #getNextId() {
204
- return this.#nextCallbackId++;
211
+ get #fetchCompleteHandler() {
212
+ return this.#exports.__zx_fetch_complete;
213
+ }
214
+ get #wsOnOpenHandler() {
215
+ return this.#exports.__zx_ws_onopen;
216
+ }
217
+ get #wsOnMessageHandler() {
218
+ return this.#exports.__zx_ws_onmessage;
219
+ }
220
+ get #wsOnErrorHandler() {
221
+ return this.#exports.__zx_ws_onerror;
222
+ }
223
+ get #wsOnCloseHandler() {
224
+ return this.#exports.__zx_ws_onclose;
205
225
  }
206
226
  #invoke(type, id, data) {
207
227
  const handler = this.#handler;
@@ -212,14 +232,62 @@ class ZxBridge {
212
232
  const dataRef = storeValueGetRef(data);
213
233
  handler(type, id, dataRef);
214
234
  }
215
- fetch(urlPtr, urlLen, callbackId) {
235
+ fetchAsync(urlPtr, urlLen, methodPtr, methodLen, headersPtr, headersLen, bodyPtr, bodyLen, timeoutMs, fetchId) {
216
236
  const url = readString(urlPtr, urlLen);
217
- fetch(url).then((response) => response.text()).then((text) => {
218
- this.#invoke(CallbackType.FetchSuccess, callbackId, text);
237
+ const method = methodLen > 0 ? readString(methodPtr, methodLen) : "GET";
238
+ const headersJson = headersLen > 0 ? readString(headersPtr, headersLen) : "{}";
239
+ const body = bodyLen > 0 ? readString(bodyPtr, bodyLen) : undefined;
240
+ let headers = {};
241
+ try {
242
+ headers = JSON.parse(headersJson);
243
+ } catch {
244
+ for (const line of headersJson.split(`
245
+ `)) {
246
+ const colonIdx = line.indexOf(":");
247
+ if (colonIdx > 0) {
248
+ headers[line.slice(0, colonIdx)] = line.slice(colonIdx + 1);
249
+ }
250
+ }
251
+ }
252
+ const controller = new AbortController;
253
+ const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
254
+ const fetchOptions = {
255
+ method,
256
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
257
+ body: method !== "GET" && method !== "HEAD" ? body : undefined,
258
+ signal: controller.signal
259
+ };
260
+ fetch(url, fetchOptions).then(async (response) => {
261
+ if (timeout)
262
+ clearTimeout(timeout);
263
+ const text = await response.text();
264
+ this.#notifyFetchComplete(fetchId, response.status, text, false);
219
265
  }).catch((error) => {
220
- this.#invoke(CallbackType.FetchError, callbackId, error.message ?? "Fetch failed");
266
+ if (timeout)
267
+ clearTimeout(timeout);
268
+ const isAbort = error.name === "AbortError";
269
+ const errorMsg = isAbort ? "Request timeout" : error.message ?? "Fetch failed";
270
+ this.#notifyFetchComplete(fetchId, 0, errorMsg, true);
221
271
  });
222
272
  }
273
+ #notifyFetchComplete(fetchId, statusCode, body, isError) {
274
+ const handler = this.#fetchCompleteHandler;
275
+ if (!handler) {
276
+ console.warn("__zx_fetch_complete not exported from WASM");
277
+ return;
278
+ }
279
+ const encoded = new TextEncoder().encode(body);
280
+ const allocFn = this.#exports.__zx_alloc;
281
+ let ptr = 0;
282
+ if (allocFn) {
283
+ ptr = allocFn(encoded.length);
284
+ } else {
285
+ const heapBase = this.#exports.__heap_base?.value ?? 65536;
286
+ ptr = heapBase + Number(fetchId % BigInt(256)) * 65536;
287
+ }
288
+ writeBytes(ptr, encoded);
289
+ handler(fetchId, statusCode, ptr, encoded.length, isError ? 1 : 0);
290
+ }
223
291
  setTimeout(callbackId, delayMs) {
224
292
  setTimeout(() => {
225
293
  this.#invoke(CallbackType.Timeout, callbackId, null);
@@ -238,6 +306,105 @@ class ZxBridge {
238
306
  this.#intervals.delete(callbackId);
239
307
  }
240
308
  }
309
+ wsConnect(wsId, urlPtr, urlLen, protocolsPtr, protocolsLen) {
310
+ const url = readString(urlPtr, urlLen);
311
+ const protocolsStr = protocolsLen > 0 ? readString(protocolsPtr, protocolsLen) : "";
312
+ const protocols = protocolsStr ? protocolsStr.split(",").map((p) => p.trim()).filter(Boolean) : undefined;
313
+ try {
314
+ const ws = protocols && protocols.length > 0 ? new WebSocket(url, protocols) : new WebSocket(url);
315
+ ws.binaryType = "arraybuffer";
316
+ ws.onopen = () => {
317
+ const handler = this.#wsOnOpenHandler;
318
+ if (!handler)
319
+ return;
320
+ const protocol = ws.protocol || "";
321
+ const { ptr, len } = this.#writeStringToWasm(protocol);
322
+ handler(wsId, ptr, len);
323
+ };
324
+ ws.onmessage = (event) => {
325
+ const handler = this.#wsOnMessageHandler;
326
+ if (!handler)
327
+ return;
328
+ const isBinary = event.data instanceof ArrayBuffer;
329
+ let data;
330
+ if (isBinary) {
331
+ data = new Uint8Array(event.data);
332
+ } else {
333
+ data = new TextEncoder().encode(event.data);
334
+ }
335
+ const { ptr, len } = this.#writeBytesToWasm(data);
336
+ handler(wsId, ptr, len, isBinary ? 1 : 0);
337
+ };
338
+ ws.onerror = (event) => {
339
+ const handler = this.#wsOnErrorHandler;
340
+ if (!handler)
341
+ return;
342
+ const msg = "WebSocket error";
343
+ const { ptr, len } = this.#writeStringToWasm(msg);
344
+ handler(wsId, ptr, len);
345
+ };
346
+ ws.onclose = (event) => {
347
+ const handler = this.#wsOnCloseHandler;
348
+ if (!handler)
349
+ return;
350
+ const reason = event.reason || "";
351
+ const { ptr, len } = this.#writeStringToWasm(reason);
352
+ handler(wsId, event.code, ptr, len, event.wasClean ? 1 : 0);
353
+ this.#websockets.delete(wsId);
354
+ };
355
+ this.#websockets.set(wsId, ws);
356
+ } catch (error) {
357
+ const handler = this.#wsOnErrorHandler;
358
+ if (handler) {
359
+ const msg = error instanceof Error ? error.message : "WebSocket connection failed";
360
+ const { ptr, len } = this.#writeStringToWasm(msg);
361
+ handler(wsId, ptr, len);
362
+ }
363
+ }
364
+ }
365
+ wsSend(wsId, dataPtr, dataLen, isBinary) {
366
+ const ws = this.#websockets.get(wsId);
367
+ if (!ws || ws.readyState !== WebSocket.OPEN)
368
+ return;
369
+ const memory = new Uint8Array(jsz.memory.buffer);
370
+ const data = memory.slice(dataPtr, dataPtr + dataLen);
371
+ if (isBinary) {
372
+ ws.send(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
373
+ } else {
374
+ ws.send(new TextDecoder().decode(data));
375
+ }
376
+ }
377
+ wsClose(wsId, code, reasonPtr, reasonLen) {
378
+ const ws = this.#websockets.get(wsId);
379
+ if (!ws)
380
+ return;
381
+ const reason = reasonLen > 0 ? readString(reasonPtr, reasonLen) : undefined;
382
+ try {
383
+ if (reason) {
384
+ ws.close(code, reason);
385
+ } else {
386
+ ws.close(code);
387
+ }
388
+ } catch {
389
+ ws.close();
390
+ }
391
+ }
392
+ #writeStringToWasm(str) {
393
+ const encoded = new TextEncoder().encode(str);
394
+ return this.#writeBytesToWasm(encoded);
395
+ }
396
+ #writeBytesToWasm(data) {
397
+ const allocFn = this.#exports.__zx_alloc;
398
+ let ptr = 0;
399
+ if (allocFn) {
400
+ ptr = allocFn(data.length);
401
+ } else {
402
+ const heapBase = this.#exports.__heap_base?.value ?? 65536;
403
+ ptr = heapBase + Date.now() % 256 * 4096;
404
+ }
405
+ writeBytes(ptr, data);
406
+ return { ptr, len: data.length };
407
+ }
241
408
  eventbridge(velementId, eventTypeId, event) {
242
409
  const eventRef = storeValueGetRef(event);
243
410
  const eventbridge = this.#exports.__zx_eventbridge;
@@ -248,8 +415,8 @@ class ZxBridge {
248
415
  return {
249
416
  ...jsz.importObject(),
250
417
  __zx: {
251
- _fetch: (urlPtr, urlLen, callbackId) => {
252
- bridgeRef.current?.fetch(urlPtr, urlLen, callbackId);
418
+ _fetchAsync: (urlPtr, urlLen, methodPtr, methodLen, headersPtr, headersLen, bodyPtr, bodyLen, timeoutMs, fetchId) => {
419
+ bridgeRef.current?.fetchAsync(urlPtr, urlLen, methodPtr, methodLen, headersPtr, headersLen, bodyPtr, bodyLen, timeoutMs, fetchId);
253
420
  },
254
421
  _setTimeout: (callbackId, delayMs) => {
255
422
  bridgeRef.current?.setTimeout(callbackId, delayMs);
@@ -259,6 +426,15 @@ class ZxBridge {
259
426
  },
260
427
  _clearInterval: (callbackId) => {
261
428
  bridgeRef.current?.clearInterval(callbackId);
429
+ },
430
+ _wsConnect: (wsId, urlPtr, urlLen, protocolsPtr, protocolsLen) => {
431
+ bridgeRef.current?.wsConnect(wsId, urlPtr, urlLen, protocolsPtr, protocolsLen);
432
+ },
433
+ _wsSend: (wsId, dataPtr, dataLen, isBinary) => {
434
+ bridgeRef.current?.wsSend(wsId, dataPtr, dataLen, isBinary);
435
+ },
436
+ _wsClose: (wsId, code, reasonPtr, reasonLen) => {
437
+ bridgeRef.current?.wsClose(wsId, code, reasonPtr, reasonLen);
262
438
  }
263
439
  }
264
440
  };
package/wasm/init.js CHANGED
@@ -173,7 +173,11 @@ var CallbackType = {
173
173
  FetchSuccess: 1,
174
174
  FetchError: 2,
175
175
  Timeout: 3,
176
- Interval: 4
176
+ Interval: 4,
177
+ WebSocketOpen: 5,
178
+ WebSocketMessage: 6,
179
+ WebSocketError: 7,
180
+ WebSocketClose: 8
177
181
  };
178
182
  var jsz = new ZigJS;
179
183
  var tempRefBuffer = new ArrayBuffer(8);
@@ -189,19 +193,35 @@ function readString(ptr, len) {
189
193
  const memory = new Uint8Array(jsz.memory.buffer);
190
194
  return new TextDecoder().decode(memory.slice(ptr, ptr + len));
191
195
  }
196
+ function writeBytes(ptr, data) {
197
+ const memory = new Uint8Array(jsz.memory.buffer);
198
+ memory.set(data, ptr);
199
+ }
192
200
 
193
201
  class ZxBridge {
194
202
  #exports;
195
- #nextCallbackId = BigInt(1);
196
203
  #intervals = new Map;
204
+ #websockets = new Map;
197
205
  constructor(exports) {
198
206
  this.#exports = exports;
199
207
  }
200
208
  get #handler() {
201
209
  return this.#exports.__zx_cb;
202
210
  }
203
- #getNextId() {
204
- return this.#nextCallbackId++;
211
+ get #fetchCompleteHandler() {
212
+ return this.#exports.__zx_fetch_complete;
213
+ }
214
+ get #wsOnOpenHandler() {
215
+ return this.#exports.__zx_ws_onopen;
216
+ }
217
+ get #wsOnMessageHandler() {
218
+ return this.#exports.__zx_ws_onmessage;
219
+ }
220
+ get #wsOnErrorHandler() {
221
+ return this.#exports.__zx_ws_onerror;
222
+ }
223
+ get #wsOnCloseHandler() {
224
+ return this.#exports.__zx_ws_onclose;
205
225
  }
206
226
  #invoke(type, id, data) {
207
227
  const handler = this.#handler;
@@ -212,14 +232,62 @@ class ZxBridge {
212
232
  const dataRef = storeValueGetRef(data);
213
233
  handler(type, id, dataRef);
214
234
  }
215
- fetch(urlPtr, urlLen, callbackId) {
235
+ fetchAsync(urlPtr, urlLen, methodPtr, methodLen, headersPtr, headersLen, bodyPtr, bodyLen, timeoutMs, fetchId) {
216
236
  const url = readString(urlPtr, urlLen);
217
- fetch(url).then((response) => response.text()).then((text) => {
218
- this.#invoke(CallbackType.FetchSuccess, callbackId, text);
237
+ const method = methodLen > 0 ? readString(methodPtr, methodLen) : "GET";
238
+ const headersJson = headersLen > 0 ? readString(headersPtr, headersLen) : "{}";
239
+ const body = bodyLen > 0 ? readString(bodyPtr, bodyLen) : undefined;
240
+ let headers = {};
241
+ try {
242
+ headers = JSON.parse(headersJson);
243
+ } catch {
244
+ for (const line of headersJson.split(`
245
+ `)) {
246
+ const colonIdx = line.indexOf(":");
247
+ if (colonIdx > 0) {
248
+ headers[line.slice(0, colonIdx)] = line.slice(colonIdx + 1);
249
+ }
250
+ }
251
+ }
252
+ const controller = new AbortController;
253
+ const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
254
+ const fetchOptions = {
255
+ method,
256
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
257
+ body: method !== "GET" && method !== "HEAD" ? body : undefined,
258
+ signal: controller.signal
259
+ };
260
+ fetch(url, fetchOptions).then(async (response) => {
261
+ if (timeout)
262
+ clearTimeout(timeout);
263
+ const text = await response.text();
264
+ this.#notifyFetchComplete(fetchId, response.status, text, false);
219
265
  }).catch((error) => {
220
- this.#invoke(CallbackType.FetchError, callbackId, error.message ?? "Fetch failed");
266
+ if (timeout)
267
+ clearTimeout(timeout);
268
+ const isAbort = error.name === "AbortError";
269
+ const errorMsg = isAbort ? "Request timeout" : error.message ?? "Fetch failed";
270
+ this.#notifyFetchComplete(fetchId, 0, errorMsg, true);
221
271
  });
222
272
  }
273
+ #notifyFetchComplete(fetchId, statusCode, body, isError) {
274
+ const handler = this.#fetchCompleteHandler;
275
+ if (!handler) {
276
+ console.warn("__zx_fetch_complete not exported from WASM");
277
+ return;
278
+ }
279
+ const encoded = new TextEncoder().encode(body);
280
+ const allocFn = this.#exports.__zx_alloc;
281
+ let ptr = 0;
282
+ if (allocFn) {
283
+ ptr = allocFn(encoded.length);
284
+ } else {
285
+ const heapBase = this.#exports.__heap_base?.value ?? 65536;
286
+ ptr = heapBase + Number(fetchId % BigInt(256)) * 65536;
287
+ }
288
+ writeBytes(ptr, encoded);
289
+ handler(fetchId, statusCode, ptr, encoded.length, isError ? 1 : 0);
290
+ }
223
291
  setTimeout(callbackId, delayMs) {
224
292
  setTimeout(() => {
225
293
  this.#invoke(CallbackType.Timeout, callbackId, null);
@@ -238,6 +306,105 @@ class ZxBridge {
238
306
  this.#intervals.delete(callbackId);
239
307
  }
240
308
  }
309
+ wsConnect(wsId, urlPtr, urlLen, protocolsPtr, protocolsLen) {
310
+ const url = readString(urlPtr, urlLen);
311
+ const protocolsStr = protocolsLen > 0 ? readString(protocolsPtr, protocolsLen) : "";
312
+ const protocols = protocolsStr ? protocolsStr.split(",").map((p) => p.trim()).filter(Boolean) : undefined;
313
+ try {
314
+ const ws = protocols && protocols.length > 0 ? new WebSocket(url, protocols) : new WebSocket(url);
315
+ ws.binaryType = "arraybuffer";
316
+ ws.onopen = () => {
317
+ const handler = this.#wsOnOpenHandler;
318
+ if (!handler)
319
+ return;
320
+ const protocol = ws.protocol || "";
321
+ const { ptr, len } = this.#writeStringToWasm(protocol);
322
+ handler(wsId, ptr, len);
323
+ };
324
+ ws.onmessage = (event) => {
325
+ const handler = this.#wsOnMessageHandler;
326
+ if (!handler)
327
+ return;
328
+ const isBinary = event.data instanceof ArrayBuffer;
329
+ let data;
330
+ if (isBinary) {
331
+ data = new Uint8Array(event.data);
332
+ } else {
333
+ data = new TextEncoder().encode(event.data);
334
+ }
335
+ const { ptr, len } = this.#writeBytesToWasm(data);
336
+ handler(wsId, ptr, len, isBinary ? 1 : 0);
337
+ };
338
+ ws.onerror = (event) => {
339
+ const handler = this.#wsOnErrorHandler;
340
+ if (!handler)
341
+ return;
342
+ const msg = "WebSocket error";
343
+ const { ptr, len } = this.#writeStringToWasm(msg);
344
+ handler(wsId, ptr, len);
345
+ };
346
+ ws.onclose = (event) => {
347
+ const handler = this.#wsOnCloseHandler;
348
+ if (!handler)
349
+ return;
350
+ const reason = event.reason || "";
351
+ const { ptr, len } = this.#writeStringToWasm(reason);
352
+ handler(wsId, event.code, ptr, len, event.wasClean ? 1 : 0);
353
+ this.#websockets.delete(wsId);
354
+ };
355
+ this.#websockets.set(wsId, ws);
356
+ } catch (error) {
357
+ const handler = this.#wsOnErrorHandler;
358
+ if (handler) {
359
+ const msg = error instanceof Error ? error.message : "WebSocket connection failed";
360
+ const { ptr, len } = this.#writeStringToWasm(msg);
361
+ handler(wsId, ptr, len);
362
+ }
363
+ }
364
+ }
365
+ wsSend(wsId, dataPtr, dataLen, isBinary) {
366
+ const ws = this.#websockets.get(wsId);
367
+ if (!ws || ws.readyState !== WebSocket.OPEN)
368
+ return;
369
+ const memory = new Uint8Array(jsz.memory.buffer);
370
+ const data = memory.slice(dataPtr, dataPtr + dataLen);
371
+ if (isBinary) {
372
+ ws.send(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
373
+ } else {
374
+ ws.send(new TextDecoder().decode(data));
375
+ }
376
+ }
377
+ wsClose(wsId, code, reasonPtr, reasonLen) {
378
+ const ws = this.#websockets.get(wsId);
379
+ if (!ws)
380
+ return;
381
+ const reason = reasonLen > 0 ? readString(reasonPtr, reasonLen) : undefined;
382
+ try {
383
+ if (reason) {
384
+ ws.close(code, reason);
385
+ } else {
386
+ ws.close(code);
387
+ }
388
+ } catch {
389
+ ws.close();
390
+ }
391
+ }
392
+ #writeStringToWasm(str) {
393
+ const encoded = new TextEncoder().encode(str);
394
+ return this.#writeBytesToWasm(encoded);
395
+ }
396
+ #writeBytesToWasm(data) {
397
+ const allocFn = this.#exports.__zx_alloc;
398
+ let ptr = 0;
399
+ if (allocFn) {
400
+ ptr = allocFn(data.length);
401
+ } else {
402
+ const heapBase = this.#exports.__heap_base?.value ?? 65536;
403
+ ptr = heapBase + Date.now() % 256 * 4096;
404
+ }
405
+ writeBytes(ptr, data);
406
+ return { ptr, len: data.length };
407
+ }
241
408
  eventbridge(velementId, eventTypeId, event) {
242
409
  const eventRef = storeValueGetRef(event);
243
410
  const eventbridge = this.#exports.__zx_eventbridge;
@@ -248,8 +415,8 @@ class ZxBridge {
248
415
  return {
249
416
  ...jsz.importObject(),
250
417
  __zx: {
251
- _fetch: (urlPtr, urlLen, callbackId) => {
252
- bridgeRef.current?.fetch(urlPtr, urlLen, callbackId);
418
+ _fetchAsync: (urlPtr, urlLen, methodPtr, methodLen, headersPtr, headersLen, bodyPtr, bodyLen, timeoutMs, fetchId) => {
419
+ bridgeRef.current?.fetchAsync(urlPtr, urlLen, methodPtr, methodLen, headersPtr, headersLen, bodyPtr, bodyLen, timeoutMs, fetchId);
253
420
  },
254
421
  _setTimeout: (callbackId, delayMs) => {
255
422
  bridgeRef.current?.setTimeout(callbackId, delayMs);
@@ -259,6 +426,15 @@ class ZxBridge {
259
426
  },
260
427
  _clearInterval: (callbackId) => {
261
428
  bridgeRef.current?.clearInterval(callbackId);
429
+ },
430
+ _wsConnect: (wsId, urlPtr, urlLen, protocolsPtr, protocolsLen) => {
431
+ bridgeRef.current?.wsConnect(wsId, urlPtr, urlLen, protocolsPtr, protocolsLen);
432
+ },
433
+ _wsSend: (wsId, dataPtr, dataLen, isBinary) => {
434
+ bridgeRef.current?.wsSend(wsId, dataPtr, dataLen, isBinary);
435
+ },
436
+ _wsClose: (wsId, code, reasonPtr, reasonLen) => {
437
+ bridgeRef.current?.wsClose(wsId, code, reasonPtr, reasonLen);
262
438
  }
263
439
  }
264
440
  };