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 +1 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/react/dom.d.ts +0 -141
- package/react/index.js +58 -31
- package/wasm/index.d.ts +19 -3
- package/wasm/index.js +186 -10
- package/wasm/init.js +186 -10
package/README.md
CHANGED
package/index.js
CHANGED
package/package.json
CHANGED
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
|
|
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
|
|
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
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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({
|
|
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
|
-
/**
|
|
22
|
-
|
|
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
|
-
#
|
|
204
|
-
return this.#
|
|
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
|
-
|
|
235
|
+
fetchAsync(urlPtr, urlLen, methodPtr, methodLen, headersPtr, headersLen, bodyPtr, bodyLen, timeoutMs, fetchId) {
|
|
216
236
|
const url = readString(urlPtr, urlLen);
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
bridgeRef.current?.
|
|
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
|
-
#
|
|
204
|
-
return this.#
|
|
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
|
-
|
|
235
|
+
fetchAsync(urlPtr, urlLen, methodPtr, methodLen, headersPtr, headersLen, bodyPtr, bodyLen, timeoutMs, fetchId) {
|
|
216
236
|
const url = readString(urlPtr, urlLen);
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
bridgeRef.current?.
|
|
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
|
};
|