domqueue-host 0.1.0-beta.1
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 +95 -0
- package/dist/backend.d.ts +10 -0
- package/dist/backend.js +23 -0
- package/dist/core.d.ts +64 -0
- package/dist/core.js +601 -0
- package/dist/host.d.ts +15 -0
- package/dist/host.js +81 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/protocol.d.ts +50 -0
- package/dist/protocol.js +50 -0
- package/dist/wasm/domqueue_example_app.wasm +0 -0
- package/dist/wasm/domqueue_todomvc_app.wasm +0 -0
- package/dist/worker.d.ts +12 -0
- package/dist/worker.js +46 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# SikioDQ
|
|
2
|
+
|
|
3
|
+
    
|
|
4
|
+
|
|
5
|
+
A command-buffered Wasm -> DOM runtime (DOMQueue / DQ). Wasm writes UI commands into linear memory; a tiny JS host applies them in one tight loop per frame.
|
|
6
|
+
|
|
7
|
+
## Performance
|
|
8
|
+
|
|
9
|
+
The perf harness compares naive DOM text updates vs the DOMQueue command buffer path.
|
|
10
|
+
|
|
11
|
+
| Scenario | Naive DOM | DOMQueue cmd buffer | Speedup |
|
|
12
|
+
|---------|-----------|---------------------|---------|
|
|
13
|
+
| NODES=200, ITER=100 | TBD | TBD | TBD |
|
|
14
|
+
|
|
15
|
+
Run the harness:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm --prefix packages/domqueue-host run build
|
|
19
|
+
npm run serve
|
|
20
|
+
# open http://127.0.0.1:4173/examples/perf/index.html
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Tune parameters in `examples/perf/bench-config.js`.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- Command buffer protocol (u32 opcodes) with a versioned shared header (v0.2)
|
|
28
|
+
- String and atom interning to avoid per-frame UTF-8 decode
|
|
29
|
+
- Event ring with coalescing for high-rate events
|
|
30
|
+
- List reordering using LIS (`LIST_BEGIN/ITEM/END`)
|
|
31
|
+
- Blueprints + slot commits for fast text updates
|
|
32
|
+
- Worker + shared memory mode (CAP_WORKER_SHARED)
|
|
33
|
+
- Tools for inspecting buffers and traces (`dq-dump`, `dq-trace-viewer`)
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm i @domqueue/host
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { createHost } from "@domqueue/host";
|
|
45
|
+
|
|
46
|
+
const mount = document.getElementById("app");
|
|
47
|
+
const host = await createHost({ wasmUrl: "/app.wasm", mount });
|
|
48
|
+
host.start();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Your Wasm module must export:
|
|
52
|
+
|
|
53
|
+
- `dq_get_shared_ptr() -> u32`
|
|
54
|
+
- `dq_frame(now_ms: f64)`
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
- Counter: `examples/counter/index.html`
|
|
59
|
+
- TodoMVC: `examples/todomvc/index.html`
|
|
60
|
+
- Perf: `examples/perf/index.html`
|
|
61
|
+
|
|
62
|
+
Serve examples:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm --prefix packages/domqueue-host run build
|
|
66
|
+
npm run serve
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Protocol
|
|
70
|
+
|
|
71
|
+
See the protocol spec in `packages/domqueue-protocol/SPEC.md`.
|
|
72
|
+
|
|
73
|
+
## Project Layout
|
|
74
|
+
|
|
75
|
+
- `packages/domqueue-host` - JS host runtime (`createHost`, `startWorker`, DOM backend)
|
|
76
|
+
- `packages/domqueue-rust` - Rust ABI + command writer + example Wasm apps
|
|
77
|
+
- `examples` - Counter, TodoMVC, perf harness
|
|
78
|
+
- `tools/dq-dump` - Command buffer decoder
|
|
79
|
+
- `tools/dq-trace-viewer` - Trace viewer HTML
|
|
80
|
+
|
|
81
|
+
## Worker Mode
|
|
82
|
+
|
|
83
|
+
Worker mode requires shared memory and cross-origin isolation. See `packages/domqueue-host/WORKER.md`.
|
|
84
|
+
|
|
85
|
+
## Building From Source
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
rustup target add wasm32-unknown-unknown
|
|
89
|
+
npm --prefix packages/domqueue-host run build
|
|
90
|
+
npm --prefix packages/domqueue-host run build:wasm
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
This project is licensed under AGPLv3 (`AGPL-3.0-only`)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type DomBackend<TNode> = {
|
|
2
|
+
createElem: (tag: string) => TNode;
|
|
3
|
+
createText: (text: string) => TNode;
|
|
4
|
+
setText: (node: TNode, text: string) => void;
|
|
5
|
+
setAttr: (node: TNode, name: string, value: string) => void;
|
|
6
|
+
append: (parent: TNode, child: TNode) => void;
|
|
7
|
+
insertBefore: (parent: TNode, child: TNode, before: TNode | null) => void;
|
|
8
|
+
remove: (node: TNode) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare const createDomBackend: (doc: Document) => DomBackend<Node>;
|
package/dist/backend.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const createDomBackend = (doc) => ({
|
|
2
|
+
createElem: (tag) => doc.createElement(tag),
|
|
3
|
+
createText: (text) => doc.createTextNode(text),
|
|
4
|
+
setText: (node, text) => {
|
|
5
|
+
node.textContent = text;
|
|
6
|
+
},
|
|
7
|
+
setAttr: (node, name, value) => {
|
|
8
|
+
if (node instanceof Element) {
|
|
9
|
+
node.setAttribute(name, value);
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
append: (parent, child) => {
|
|
13
|
+
parent.appendChild(child);
|
|
14
|
+
},
|
|
15
|
+
insertBefore: (parent, child, before) => {
|
|
16
|
+
parent.insertBefore(child, before);
|
|
17
|
+
},
|
|
18
|
+
remove: (node) => {
|
|
19
|
+
if (node.parentNode) {
|
|
20
|
+
node.parentNode.removeChild(node);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { DomBackend } from "./backend.js";
|
|
2
|
+
type BlueprintNode = {
|
|
3
|
+
handle: number;
|
|
4
|
+
kind: number;
|
|
5
|
+
parent: number;
|
|
6
|
+
tagOrSlot: number;
|
|
7
|
+
};
|
|
8
|
+
type Blueprint<TNode> = {
|
|
9
|
+
nodes: BlueprintNode[];
|
|
10
|
+
slotIds: number[];
|
|
11
|
+
};
|
|
12
|
+
type PendingEvent = {
|
|
13
|
+
typeId: number;
|
|
14
|
+
targetHandle: number;
|
|
15
|
+
time: number;
|
|
16
|
+
data0: number;
|
|
17
|
+
data1: number;
|
|
18
|
+
data2: number;
|
|
19
|
+
};
|
|
20
|
+
export type Core<TNode> = {
|
|
21
|
+
memory: WebAssembly.Memory;
|
|
22
|
+
sharedPtr: number;
|
|
23
|
+
sharedIndex: number;
|
|
24
|
+
backend: DomBackend<TNode>;
|
|
25
|
+
buffer: ArrayBuffer;
|
|
26
|
+
u8: Uint8Array;
|
|
27
|
+
u32: Uint32Array;
|
|
28
|
+
i32: Int32Array;
|
|
29
|
+
isShared: boolean;
|
|
30
|
+
caps: number;
|
|
31
|
+
nodes: Array<TNode | null>;
|
|
32
|
+
gens: number[];
|
|
33
|
+
decoder: TextDecoder;
|
|
34
|
+
atomTable: string[];
|
|
35
|
+
stringTable: string[];
|
|
36
|
+
eventBindings: Set<string>;
|
|
37
|
+
pendingEvents: Map<string, PendingEvent>;
|
|
38
|
+
listParentHandle: number;
|
|
39
|
+
listParent: TNode | null;
|
|
40
|
+
listItems: Array<{
|
|
41
|
+
key: number;
|
|
42
|
+
node: TNode;
|
|
43
|
+
}>;
|
|
44
|
+
listStates: Map<number, number[]>;
|
|
45
|
+
blueprints: Map<number, Blueprint<TNode>>;
|
|
46
|
+
slotNodes: Map<number, TNode>;
|
|
47
|
+
};
|
|
48
|
+
export type CoreInit<TNode> = {
|
|
49
|
+
memory: WebAssembly.Memory;
|
|
50
|
+
sharedPtr: number;
|
|
51
|
+
backend: DomBackend<TNode>;
|
|
52
|
+
mountHandle: number;
|
|
53
|
+
mountNode: TNode;
|
|
54
|
+
};
|
|
55
|
+
export declare const flushPendingEvents: <TNode>(core: Core<TNode>) => void;
|
|
56
|
+
export declare const createCore: <TNode>(init: CoreInit<TNode>) => Core<TNode>;
|
|
57
|
+
export declare const refreshViewsIfNeeded: <TNode>(core: Core<TNode>) => void;
|
|
58
|
+
export declare const validateHeader: <TNode>(core: Core<TNode>) => void;
|
|
59
|
+
export declare const applyCmdBuffer: <TNode>(core: Core<TNode>) => void;
|
|
60
|
+
export declare const decodeHandleParts: (handle: number) => {
|
|
61
|
+
index: number;
|
|
62
|
+
gen: number;
|
|
63
|
+
};
|
|
64
|
+
export {};
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { CAP_EVENTS, DQ_MAGIC, DQ_VERSION, EVENT_RECORD_WORDS, HDR_CAPS, HDR_CMD_ACTIVE, HDR_CMD_CAP_WORDS, HDR_CMD_LEN_WORDS, HDR_CMD_LEN_WORDS_B, HDR_CMD_PTR, HDR_CMD_PTR_B, HDR_EV_CAP_WORDS, HDR_EV_HEAD, HDR_EV_PTR, HDR_EV_TAIL, HDR_MAGIC, HDR_SYNC_STATE, HDR_VERSION, HANDLE_GEN_SHIFT, HANDLE_INDEX_MASK, OP_APPEND, OP_BLUEPRINT_DEFINE, OP_BLUEPRINT_INSTANTIATE, OP_CREATE_ELEM, OP_CREATE_ELEM_ATOM, OP_CREATE_TEXT, OP_EVENT_ON, OP_INSERT_BEFORE, OP_INTERN_ATOM, OP_INTERN_STRING, OP_LIST_BEGIN, OP_LIST_END, OP_LIST_ITEM, OP_REMOVE, OP_SET_ATTR, OP_SET_ATTR_ATOM, OP_SET_TEXT, OP_SET_TEXT_ID, OP_SLOT_COMMIT, CAP_WORKER_SHARED, handleGen, handleIndex } from "./protocol.js";
|
|
2
|
+
const isSharedBuffer = (buffer) => typeof SharedArrayBuffer !== "undefined" && buffer instanceof SharedArrayBuffer;
|
|
3
|
+
const ensureSlot = (core, index, gen) => {
|
|
4
|
+
while (core.nodes.length <= index) {
|
|
5
|
+
core.nodes.push(null);
|
|
6
|
+
core.gens.push(0);
|
|
7
|
+
}
|
|
8
|
+
if (core.gens[index] === 0) {
|
|
9
|
+
core.gens[index] = gen;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const setHandle = (core, handle, node) => {
|
|
13
|
+
const index = handleIndex(handle);
|
|
14
|
+
const gen = handleGen(handle);
|
|
15
|
+
if (index === 0) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
ensureSlot(core, index, gen);
|
|
19
|
+
core.nodes[index] = node;
|
|
20
|
+
core.gens[index] = gen;
|
|
21
|
+
};
|
|
22
|
+
const getNode = (core, handle) => {
|
|
23
|
+
if (handle === 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const index = handleIndex(handle);
|
|
27
|
+
const gen = handleGen(handle);
|
|
28
|
+
if (index >= core.nodes.length) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (core.gens[index] !== gen) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return core.nodes[index];
|
|
35
|
+
};
|
|
36
|
+
const dropHandle = (core, handle) => {
|
|
37
|
+
if (handle === 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const index = handleIndex(handle);
|
|
41
|
+
const gen = handleGen(handle);
|
|
42
|
+
if (index >= core.nodes.length) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (core.gens[index] !== gen) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
core.nodes[index] = null;
|
|
49
|
+
const nextGen = (gen + 1) & 0xff;
|
|
50
|
+
core.gens[index] = nextGen === 0 ? 1 : nextGen;
|
|
51
|
+
};
|
|
52
|
+
const readString = (core, ptr, len) => {
|
|
53
|
+
if (len === 0) {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
const end = ptr + len;
|
|
57
|
+
if (end > core.u8.length) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
return core.decoder.decode(core.u8.subarray(ptr, end));
|
|
61
|
+
};
|
|
62
|
+
const ensureStringSlot = (table, id) => {
|
|
63
|
+
while (table.length <= id) {
|
|
64
|
+
table.push("");
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const internToTable = (core, table, id, ptr, len) => {
|
|
68
|
+
ensureStringSlot(table, id);
|
|
69
|
+
table[id] = readString(core, ptr, len);
|
|
70
|
+
};
|
|
71
|
+
const lookupString = (table, id) => {
|
|
72
|
+
if (id < 0 || id >= table.length) {
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
return table[id] ?? "";
|
|
76
|
+
};
|
|
77
|
+
const loadIndex = (core, offset) => {
|
|
78
|
+
const idx = core.sharedIndex + offset;
|
|
79
|
+
if (core.isShared) {
|
|
80
|
+
return Atomics.load(core.i32, idx) >>> 0;
|
|
81
|
+
}
|
|
82
|
+
return core.u32[idx];
|
|
83
|
+
};
|
|
84
|
+
const storeIndex = (core, offset, value) => {
|
|
85
|
+
const idx = core.sharedIndex + offset;
|
|
86
|
+
if (core.isShared) {
|
|
87
|
+
Atomics.store(core.i32, idx, value);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
core.u32[idx] = value;
|
|
91
|
+
};
|
|
92
|
+
const pushEvent = (core, ev) => {
|
|
93
|
+
const evPtr = core.u32[core.sharedIndex + HDR_EV_PTR];
|
|
94
|
+
const evCapWords = core.u32[core.sharedIndex + HDR_EV_CAP_WORDS];
|
|
95
|
+
if (evPtr === 0 || evCapWords === 0) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const head = loadIndex(core, HDR_EV_HEAD);
|
|
99
|
+
const tail = loadIndex(core, HDR_EV_TAIL);
|
|
100
|
+
const next = head + EVENT_RECORD_WORDS >= evCapWords ? 0 : head + EVENT_RECORD_WORDS;
|
|
101
|
+
if (next === tail) {
|
|
102
|
+
const drop = tail + EVENT_RECORD_WORDS >= evCapWords ? 0 : tail + EVENT_RECORD_WORDS;
|
|
103
|
+
storeIndex(core, HDR_EV_TAIL, drop);
|
|
104
|
+
}
|
|
105
|
+
const base = (evPtr >>> 2) + head;
|
|
106
|
+
core.u32[base] = ev.typeId;
|
|
107
|
+
core.u32[base + 1] = ev.targetHandle;
|
|
108
|
+
core.u32[base + 2] = ev.time >>> 0;
|
|
109
|
+
core.u32[base + 3] = ev.data0 >>> 0;
|
|
110
|
+
core.u32[base + 4] = ev.data1 >>> 0;
|
|
111
|
+
core.u32[base + 5] = ev.data2 >>> 0;
|
|
112
|
+
storeIndex(core, HDR_EV_HEAD, next);
|
|
113
|
+
};
|
|
114
|
+
const COALESCE_EVENTS = new Set(["pointermove", "mousemove", "scroll", "wheel"]);
|
|
115
|
+
const enqueueEvent = (core, ev, eventName) => {
|
|
116
|
+
if (COALESCE_EVENTS.has(eventName)) {
|
|
117
|
+
const key = `${eventName}:${ev.targetHandle}`;
|
|
118
|
+
core.pendingEvents.set(key, ev);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
pushEvent(core, ev);
|
|
122
|
+
};
|
|
123
|
+
export const flushPendingEvents = (core) => {
|
|
124
|
+
if (core.pendingEvents.size === 0) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
for (const ev of core.pendingEvents.values()) {
|
|
128
|
+
pushEvent(core, ev);
|
|
129
|
+
}
|
|
130
|
+
core.pendingEvents.clear();
|
|
131
|
+
};
|
|
132
|
+
const eventPayload = (event) => {
|
|
133
|
+
if (event instanceof MouseEvent) {
|
|
134
|
+
return {
|
|
135
|
+
data0: event.clientX >>> 0,
|
|
136
|
+
data1: event.clientY >>> 0,
|
|
137
|
+
data2: event.buttons >>> 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (event instanceof KeyboardEvent) {
|
|
141
|
+
const mod = (event.shiftKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.altKey ? 4 : 0) | (event.metaKey ? 8 : 0);
|
|
142
|
+
return {
|
|
143
|
+
data0: event.keyCode >>> 0,
|
|
144
|
+
data1: mod >>> 0,
|
|
145
|
+
data2: 0
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return { data0: 0, data1: 0, data2: 0 };
|
|
149
|
+
};
|
|
150
|
+
export const createCore = (init) => {
|
|
151
|
+
const buffer = init.memory.buffer;
|
|
152
|
+
const u8 = new Uint8Array(buffer);
|
|
153
|
+
const u32 = new Uint32Array(buffer);
|
|
154
|
+
const i32 = new Int32Array(buffer);
|
|
155
|
+
const sharedIndex = init.sharedPtr >>> 2;
|
|
156
|
+
const core = {
|
|
157
|
+
memory: init.memory,
|
|
158
|
+
sharedPtr: init.sharedPtr,
|
|
159
|
+
sharedIndex,
|
|
160
|
+
backend: init.backend,
|
|
161
|
+
buffer,
|
|
162
|
+
u8,
|
|
163
|
+
u32,
|
|
164
|
+
i32,
|
|
165
|
+
isShared: isSharedBuffer(buffer),
|
|
166
|
+
caps: 0,
|
|
167
|
+
nodes: [],
|
|
168
|
+
gens: [],
|
|
169
|
+
decoder: new TextDecoder(),
|
|
170
|
+
atomTable: [],
|
|
171
|
+
stringTable: [],
|
|
172
|
+
eventBindings: new Set(),
|
|
173
|
+
pendingEvents: new Map(),
|
|
174
|
+
listParentHandle: 0,
|
|
175
|
+
listParent: null,
|
|
176
|
+
listItems: [],
|
|
177
|
+
listStates: new Map(),
|
|
178
|
+
blueprints: new Map(),
|
|
179
|
+
slotNodes: new Map()
|
|
180
|
+
};
|
|
181
|
+
setHandle(core, init.mountHandle, init.mountNode);
|
|
182
|
+
return core;
|
|
183
|
+
};
|
|
184
|
+
export const refreshViewsIfNeeded = (core) => {
|
|
185
|
+
if (core.memory.buffer === core.buffer) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
core.buffer = core.memory.buffer;
|
|
189
|
+
core.u8 = new Uint8Array(core.buffer);
|
|
190
|
+
core.u32 = new Uint32Array(core.buffer);
|
|
191
|
+
core.i32 = new Int32Array(core.buffer);
|
|
192
|
+
core.isShared = isSharedBuffer(core.buffer);
|
|
193
|
+
};
|
|
194
|
+
export const validateHeader = (core) => {
|
|
195
|
+
const magic = core.u32[core.sharedIndex + HDR_MAGIC];
|
|
196
|
+
const version = core.u32[core.sharedIndex + HDR_VERSION];
|
|
197
|
+
if (magic !== DQ_MAGIC) {
|
|
198
|
+
throw new Error("DQ magic mismatch");
|
|
199
|
+
}
|
|
200
|
+
if (version !== DQ_VERSION) {
|
|
201
|
+
throw new Error("DQ version mismatch");
|
|
202
|
+
}
|
|
203
|
+
core.caps = core.u32[core.sharedIndex + HDR_CAPS];
|
|
204
|
+
};
|
|
205
|
+
const computeLis = (seq) => {
|
|
206
|
+
const n = seq.length;
|
|
207
|
+
const prev = new Array(n).fill(-1);
|
|
208
|
+
const tails = [];
|
|
209
|
+
const tailsIndex = [];
|
|
210
|
+
let i = 0;
|
|
211
|
+
while (i < n) {
|
|
212
|
+
const v = seq[i];
|
|
213
|
+
if (v >= 0) {
|
|
214
|
+
let lo = 0;
|
|
215
|
+
let hi = tails.length;
|
|
216
|
+
while (lo < hi) {
|
|
217
|
+
const mid = (lo + hi) >> 1;
|
|
218
|
+
if (tails[mid] < v) {
|
|
219
|
+
lo = mid + 1;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
hi = mid;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (lo > 0) {
|
|
226
|
+
prev[i] = tailsIndex[lo - 1];
|
|
227
|
+
}
|
|
228
|
+
if (lo === tails.length) {
|
|
229
|
+
tails.push(v);
|
|
230
|
+
tailsIndex.push(i);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
tails[lo] = v;
|
|
234
|
+
tailsIndex[lo] = i;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
i += 1;
|
|
238
|
+
}
|
|
239
|
+
const keep = new Array(n).fill(false);
|
|
240
|
+
if (tailsIndex.length === 0) {
|
|
241
|
+
return keep;
|
|
242
|
+
}
|
|
243
|
+
let k = tailsIndex[tailsIndex.length - 1];
|
|
244
|
+
while (k >= 0) {
|
|
245
|
+
keep[k] = true;
|
|
246
|
+
k = prev[k];
|
|
247
|
+
}
|
|
248
|
+
return keep;
|
|
249
|
+
};
|
|
250
|
+
const applyList = (core) => {
|
|
251
|
+
const parent = core.listParent;
|
|
252
|
+
const parentHandle = core.listParentHandle;
|
|
253
|
+
if (!parent || parentHandle === 0) {
|
|
254
|
+
core.listItems = [];
|
|
255
|
+
core.listParent = null;
|
|
256
|
+
core.listParentHandle = 0;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const newKeys = core.listItems.map((item) => item.key);
|
|
260
|
+
const prevKeys = core.listStates.get(parentHandle) ?? [];
|
|
261
|
+
const indexMap = new Map();
|
|
262
|
+
let p = 0;
|
|
263
|
+
while (p < prevKeys.length) {
|
|
264
|
+
indexMap.set(prevKeys[p], p);
|
|
265
|
+
p += 1;
|
|
266
|
+
}
|
|
267
|
+
const seq = newKeys.map((key) => {
|
|
268
|
+
const idx = indexMap.get(key);
|
|
269
|
+
return idx === undefined ? -1 : idx;
|
|
270
|
+
});
|
|
271
|
+
const keep = computeLis(seq);
|
|
272
|
+
let anchor = null;
|
|
273
|
+
let i = core.listItems.length - 1;
|
|
274
|
+
while (i >= 0) {
|
|
275
|
+
const item = core.listItems[i];
|
|
276
|
+
if (keep[i]) {
|
|
277
|
+
anchor = item.node;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
core.backend.insertBefore(parent, item.node, anchor);
|
|
281
|
+
anchor = item.node;
|
|
282
|
+
}
|
|
283
|
+
i -= 1;
|
|
284
|
+
}
|
|
285
|
+
core.listStates.set(parentHandle, newKeys);
|
|
286
|
+
core.listItems = [];
|
|
287
|
+
core.listParent = null;
|
|
288
|
+
core.listParentHandle = 0;
|
|
289
|
+
};
|
|
290
|
+
const parseBlueprint = (core, ptr, lenWords) => {
|
|
291
|
+
const base = ptr >>> 2;
|
|
292
|
+
if (base + lenWords > core.u32.length || lenWords < 2) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const version = core.u32[base];
|
|
296
|
+
const count = core.u32[base + 1];
|
|
297
|
+
if (version !== 1) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const nodes = [];
|
|
301
|
+
const slotIds = [];
|
|
302
|
+
let offset = base + 2;
|
|
303
|
+
let i = 0;
|
|
304
|
+
while (i < count) {
|
|
305
|
+
if (offset + 4 > base + lenWords) {
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
const handle = core.u32[offset];
|
|
309
|
+
const kind = core.u32[offset + 1];
|
|
310
|
+
const parent = core.u32[offset + 2];
|
|
311
|
+
const tagOrSlot = core.u32[offset + 3];
|
|
312
|
+
nodes.push({ handle, kind, parent, tagOrSlot });
|
|
313
|
+
if (kind === 2) {
|
|
314
|
+
slotIds.push(tagOrSlot);
|
|
315
|
+
}
|
|
316
|
+
offset += 4;
|
|
317
|
+
i += 1;
|
|
318
|
+
}
|
|
319
|
+
return { nodes, slotIds };
|
|
320
|
+
};
|
|
321
|
+
const instantiateBlueprint = (core, blueprint) => {
|
|
322
|
+
for (const node of blueprint.nodes) {
|
|
323
|
+
if (node.kind === 1) {
|
|
324
|
+
const tag = lookupString(core.atomTable, node.tagOrSlot);
|
|
325
|
+
const elem = core.backend.createElem(tag);
|
|
326
|
+
setHandle(core, node.handle, elem);
|
|
327
|
+
const parent = node.parent === 0 ? null : getNode(core, node.parent);
|
|
328
|
+
if (parent) {
|
|
329
|
+
core.backend.append(parent, elem);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else if (node.kind === 2) {
|
|
333
|
+
const text = core.backend.createText("");
|
|
334
|
+
setHandle(core, node.handle, text);
|
|
335
|
+
core.slotNodes.set(node.tagOrSlot, text);
|
|
336
|
+
const parent = node.parent === 0 ? null : getNode(core, node.parent);
|
|
337
|
+
if (parent) {
|
|
338
|
+
core.backend.append(parent, text);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const applySlotCommit = (core, valuesPtr, valuesLenWords, dirtyPtr, dirtyLenWords) => {
|
|
344
|
+
const valuesIndex = valuesPtr >>> 2;
|
|
345
|
+
const dirtyIndex = dirtyPtr >>> 2;
|
|
346
|
+
if (valuesIndex + valuesLenWords > core.u32.length) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (dirtyIndex + dirtyLenWords > core.u32.length) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
let wordIndex = 0;
|
|
353
|
+
while (wordIndex < dirtyLenWords) {
|
|
354
|
+
const bits = core.u32[dirtyIndex + wordIndex];
|
|
355
|
+
if (bits !== 0) {
|
|
356
|
+
let bit = 0;
|
|
357
|
+
while (bit < 32) {
|
|
358
|
+
if ((bits & (1 << bit)) !== 0) {
|
|
359
|
+
const slotId = wordIndex * 32 + bit;
|
|
360
|
+
if (slotId < valuesLenWords) {
|
|
361
|
+
const valueId = core.u32[valuesIndex + slotId];
|
|
362
|
+
const node = core.slotNodes.get(slotId);
|
|
363
|
+
if (node) {
|
|
364
|
+
const text = lookupString(core.stringTable, valueId);
|
|
365
|
+
core.backend.setText(node, text);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
bit += 1;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
wordIndex += 1;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
export const applyCmdBuffer = (core) => {
|
|
376
|
+
if ((core.caps & CAP_WORKER_SHARED) !== 0) {
|
|
377
|
+
const state = core.u32[core.sharedIndex + HDR_SYNC_STATE];
|
|
378
|
+
if (state !== 2) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const active = core.u32[core.sharedIndex + HDR_CMD_ACTIVE];
|
|
383
|
+
const cmdPtr = active === 1 ? core.u32[core.sharedIndex + HDR_CMD_PTR_B] : core.u32[core.sharedIndex + HDR_CMD_PTR];
|
|
384
|
+
const cmdLenWords = active === 1 ? core.u32[core.sharedIndex + HDR_CMD_LEN_WORDS_B] : core.u32[core.sharedIndex + HDR_CMD_LEN_WORDS];
|
|
385
|
+
const cmdCapWords = core.u32[core.sharedIndex + HDR_CMD_CAP_WORDS];
|
|
386
|
+
if (cmdPtr === 0 || cmdLenWords === 0) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (cmdLenWords > cmdCapWords) {
|
|
390
|
+
if (active === 1) {
|
|
391
|
+
core.u32[core.sharedIndex + HDR_CMD_LEN_WORDS_B] = 0;
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
core.u32[core.sharedIndex + HDR_CMD_LEN_WORDS] = 0;
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const baseIndex = cmdPtr >>> 2;
|
|
399
|
+
let i = 0;
|
|
400
|
+
while (i < cmdLenWords) {
|
|
401
|
+
const header = core.u32[baseIndex + i];
|
|
402
|
+
const opcode = header & 0xff;
|
|
403
|
+
const len = header >>> 16;
|
|
404
|
+
const next = i + 1 + len;
|
|
405
|
+
if (next > cmdLenWords) {
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
const args = baseIndex + i + 1;
|
|
409
|
+
if (opcode === OP_CREATE_ELEM) {
|
|
410
|
+
const handle = core.u32[args];
|
|
411
|
+
const tagPtr = core.u32[args + 1];
|
|
412
|
+
const tagLen = core.u32[args + 2];
|
|
413
|
+
const tag = readString(core, tagPtr, tagLen);
|
|
414
|
+
const node = core.backend.createElem(tag);
|
|
415
|
+
setHandle(core, handle, node);
|
|
416
|
+
}
|
|
417
|
+
else if (opcode === OP_CREATE_TEXT) {
|
|
418
|
+
const handle = core.u32[args];
|
|
419
|
+
const textPtr = core.u32[args + 1];
|
|
420
|
+
const textLen = core.u32[args + 2];
|
|
421
|
+
const text = readString(core, textPtr, textLen);
|
|
422
|
+
const node = core.backend.createText(text);
|
|
423
|
+
setHandle(core, handle, node);
|
|
424
|
+
}
|
|
425
|
+
else if (opcode === OP_SET_TEXT) {
|
|
426
|
+
const handle = core.u32[args];
|
|
427
|
+
const textPtr = core.u32[args + 1];
|
|
428
|
+
const textLen = core.u32[args + 2];
|
|
429
|
+
const node = getNode(core, handle);
|
|
430
|
+
if (node) {
|
|
431
|
+
const text = readString(core, textPtr, textLen);
|
|
432
|
+
core.backend.setText(node, text);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else if (opcode === OP_SET_ATTR) {
|
|
436
|
+
const handle = core.u32[args];
|
|
437
|
+
const namePtr = core.u32[args + 1];
|
|
438
|
+
const nameLen = core.u32[args + 2];
|
|
439
|
+
const valuePtr = core.u32[args + 3];
|
|
440
|
+
const valueLen = core.u32[args + 4];
|
|
441
|
+
const node = getNode(core, handle);
|
|
442
|
+
if (node) {
|
|
443
|
+
const name = readString(core, namePtr, nameLen);
|
|
444
|
+
const value = readString(core, valuePtr, valueLen);
|
|
445
|
+
core.backend.setAttr(node, name, value);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else if (opcode === OP_APPEND) {
|
|
449
|
+
const parentHandle = core.u32[args];
|
|
450
|
+
const childHandle = core.u32[args + 1];
|
|
451
|
+
const parent = getNode(core, parentHandle);
|
|
452
|
+
const child = getNode(core, childHandle);
|
|
453
|
+
if (parent && child) {
|
|
454
|
+
core.backend.append(parent, child);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (opcode === OP_INSERT_BEFORE) {
|
|
458
|
+
const parentHandle = core.u32[args];
|
|
459
|
+
const childHandle = core.u32[args + 1];
|
|
460
|
+
const beforeHandle = core.u32[args + 2];
|
|
461
|
+
const parent = getNode(core, parentHandle);
|
|
462
|
+
const child = getNode(core, childHandle);
|
|
463
|
+
const before = beforeHandle === 0 ? null : getNode(core, beforeHandle);
|
|
464
|
+
if (parent && child) {
|
|
465
|
+
core.backend.insertBefore(parent, child, before);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (opcode === OP_REMOVE) {
|
|
469
|
+
const handle = core.u32[args];
|
|
470
|
+
const node = getNode(core, handle);
|
|
471
|
+
if (node) {
|
|
472
|
+
core.backend.remove(node);
|
|
473
|
+
}
|
|
474
|
+
dropHandle(core, handle);
|
|
475
|
+
}
|
|
476
|
+
else if (opcode === OP_INTERN_STRING) {
|
|
477
|
+
const id = core.u32[args];
|
|
478
|
+
const ptr = core.u32[args + 1];
|
|
479
|
+
const lenStr = core.u32[args + 2];
|
|
480
|
+
internToTable(core, core.stringTable, id, ptr, lenStr);
|
|
481
|
+
}
|
|
482
|
+
else if (opcode === OP_INTERN_ATOM) {
|
|
483
|
+
const id = core.u32[args];
|
|
484
|
+
const ptr = core.u32[args + 1];
|
|
485
|
+
const lenStr = core.u32[args + 2];
|
|
486
|
+
internToTable(core, core.atomTable, id, ptr, lenStr);
|
|
487
|
+
}
|
|
488
|
+
else if (opcode === OP_CREATE_ELEM_ATOM) {
|
|
489
|
+
const handle = core.u32[args];
|
|
490
|
+
const tagId = core.u32[args + 1];
|
|
491
|
+
const tag = lookupString(core.atomTable, tagId);
|
|
492
|
+
const node = core.backend.createElem(tag);
|
|
493
|
+
setHandle(core, handle, node);
|
|
494
|
+
}
|
|
495
|
+
else if (opcode === OP_SET_ATTR_ATOM) {
|
|
496
|
+
const handle = core.u32[args];
|
|
497
|
+
const nameId = core.u32[args + 1];
|
|
498
|
+
const valueId = core.u32[args + 2];
|
|
499
|
+
const node = getNode(core, handle);
|
|
500
|
+
if (node) {
|
|
501
|
+
const name = lookupString(core.atomTable, nameId);
|
|
502
|
+
const value = lookupString(core.stringTable, valueId);
|
|
503
|
+
core.backend.setAttr(node, name, value);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
else if (opcode === OP_SET_TEXT_ID) {
|
|
507
|
+
const handle = core.u32[args];
|
|
508
|
+
const textId = core.u32[args + 1];
|
|
509
|
+
const node = getNode(core, handle);
|
|
510
|
+
if (node) {
|
|
511
|
+
const text = lookupString(core.stringTable, textId);
|
|
512
|
+
core.backend.setText(node, text);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
else if (opcode === OP_EVENT_ON) {
|
|
516
|
+
const handle = core.u32[args];
|
|
517
|
+
const eventId = core.u32[args + 1];
|
|
518
|
+
const flags = core.u32[args + 2];
|
|
519
|
+
const node = getNode(core, handle);
|
|
520
|
+
if (node && (core.caps & CAP_EVENTS) !== 0) {
|
|
521
|
+
const eventName = lookupString(core.atomTable, eventId);
|
|
522
|
+
if (!eventName) {
|
|
523
|
+
i = next;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const key = `${handle}:${eventId}`;
|
|
527
|
+
if (!core.eventBindings.has(key)) {
|
|
528
|
+
core.eventBindings.add(key);
|
|
529
|
+
const listener = (event) => {
|
|
530
|
+
const { data0, data1, data2 } = eventPayload(event);
|
|
531
|
+
const ev = {
|
|
532
|
+
typeId: eventId,
|
|
533
|
+
targetHandle: handle,
|
|
534
|
+
time: Math.floor(event.timeStamp || 0),
|
|
535
|
+
data0,
|
|
536
|
+
data1,
|
|
537
|
+
data2
|
|
538
|
+
};
|
|
539
|
+
enqueueEvent(core, ev, eventName);
|
|
540
|
+
};
|
|
541
|
+
node.addEventListener(eventName, listener, { passive: (flags & 1) !== 0 });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else if (opcode === OP_LIST_BEGIN) {
|
|
546
|
+
const parentHandle = core.u32[args];
|
|
547
|
+
const parent = getNode(core, parentHandle);
|
|
548
|
+
core.listParentHandle = parentHandle;
|
|
549
|
+
core.listParent = parent;
|
|
550
|
+
core.listItems = [];
|
|
551
|
+
}
|
|
552
|
+
else if (opcode === OP_LIST_ITEM) {
|
|
553
|
+
const key = core.u32[args];
|
|
554
|
+
const childHandle = core.u32[args + 1];
|
|
555
|
+
const child = getNode(core, childHandle);
|
|
556
|
+
if (child) {
|
|
557
|
+
core.listItems.push({ key, node: child });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
else if (opcode === OP_LIST_END) {
|
|
561
|
+
applyList(core);
|
|
562
|
+
}
|
|
563
|
+
else if (opcode === OP_BLUEPRINT_DEFINE) {
|
|
564
|
+
const id = core.u32[args];
|
|
565
|
+
const ptr = core.u32[args + 1];
|
|
566
|
+
const lenWords = core.u32[args + 2];
|
|
567
|
+
const blueprint = parseBlueprint(core, ptr, lenWords);
|
|
568
|
+
if (blueprint) {
|
|
569
|
+
core.blueprints.set(id, blueprint);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else if (opcode === OP_BLUEPRINT_INSTANTIATE) {
|
|
573
|
+
const id = core.u32[args];
|
|
574
|
+
const blueprint = core.blueprints.get(id);
|
|
575
|
+
if (blueprint) {
|
|
576
|
+
instantiateBlueprint(core, blueprint);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else if (opcode === OP_SLOT_COMMIT) {
|
|
580
|
+
const valuesPtr = core.u32[args];
|
|
581
|
+
const valuesLen = core.u32[args + 1];
|
|
582
|
+
const dirtyPtr = core.u32[args + 2];
|
|
583
|
+
const dirtyLen = core.u32[args + 3];
|
|
584
|
+
applySlotCommit(core, valuesPtr, valuesLen, dirtyPtr, dirtyLen);
|
|
585
|
+
}
|
|
586
|
+
i = next;
|
|
587
|
+
}
|
|
588
|
+
if (active === 1) {
|
|
589
|
+
core.u32[core.sharedIndex + HDR_CMD_LEN_WORDS_B] = 0;
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
core.u32[core.sharedIndex + HDR_CMD_LEN_WORDS] = 0;
|
|
593
|
+
}
|
|
594
|
+
if ((core.caps & CAP_WORKER_SHARED) !== 0) {
|
|
595
|
+
core.u32[core.sharedIndex + HDR_SYNC_STATE] = 0;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
export const decodeHandleParts = (handle) => ({
|
|
599
|
+
index: handle & HANDLE_INDEX_MASK,
|
|
600
|
+
gen: handle >>> HANDLE_GEN_SHIFT
|
|
601
|
+
});
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type DomBackend } from "./backend.js";
|
|
2
|
+
export type HostOptions = {
|
|
3
|
+
wasmUrl: string;
|
|
4
|
+
mount: Node;
|
|
5
|
+
backend?: DomBackend<Node>;
|
|
6
|
+
importObject?: WebAssembly.Imports;
|
|
7
|
+
rootHandle?: number;
|
|
8
|
+
};
|
|
9
|
+
export type Host = {
|
|
10
|
+
start: () => void;
|
|
11
|
+
stop: () => void;
|
|
12
|
+
step: (now?: number) => void;
|
|
13
|
+
instance: WebAssembly.Instance;
|
|
14
|
+
};
|
|
15
|
+
export declare const createHost: (options: HostOptions) => Promise<Host>;
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createDomBackend } from "./backend.js";
|
|
2
|
+
import { applyCmdBuffer, createCore, flushPendingEvents, refreshViewsIfNeeded, validateHeader } from "./core.js";
|
|
3
|
+
import { HDR_ROOT_HANDLE, makeHandle } from "./protocol.js";
|
|
4
|
+
const getNow = () => {
|
|
5
|
+
const perf = globalThis.performance;
|
|
6
|
+
if (perf && typeof perf.now === "function") {
|
|
7
|
+
return perf.now();
|
|
8
|
+
}
|
|
9
|
+
return Date.now();
|
|
10
|
+
};
|
|
11
|
+
const scheduleFrame = (cb) => {
|
|
12
|
+
if (typeof requestAnimationFrame === "function") {
|
|
13
|
+
return requestAnimationFrame(cb);
|
|
14
|
+
}
|
|
15
|
+
return setTimeout(() => cb(getNow()), 16);
|
|
16
|
+
};
|
|
17
|
+
const cancelFrame = (id) => {
|
|
18
|
+
if (typeof cancelAnimationFrame === "function") {
|
|
19
|
+
cancelAnimationFrame(id);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
clearTimeout(id);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
export const createHost = async (options) => {
|
|
26
|
+
const backend = options.backend ?? (typeof document !== "undefined" ? createDomBackend(document) : undefined);
|
|
27
|
+
if (!backend) {
|
|
28
|
+
throw new Error("Dom backend required");
|
|
29
|
+
}
|
|
30
|
+
const response = await fetch(options.wasmUrl);
|
|
31
|
+
const result = await WebAssembly.instantiateStreaming(response, options.importObject ?? {});
|
|
32
|
+
const instance = result.instance;
|
|
33
|
+
const exports = instance.exports;
|
|
34
|
+
if (!exports.memory || typeof exports.dq_get_shared_ptr !== "function" || typeof exports.dq_frame !== "function") {
|
|
35
|
+
throw new Error("Missing DQ exports");
|
|
36
|
+
}
|
|
37
|
+
const sharedPtr = exports.dq_get_shared_ptr();
|
|
38
|
+
const rootHandle = options.rootHandle ?? makeHandle(1, 1);
|
|
39
|
+
const core = createCore({
|
|
40
|
+
memory: exports.memory,
|
|
41
|
+
sharedPtr,
|
|
42
|
+
backend,
|
|
43
|
+
mountHandle: rootHandle,
|
|
44
|
+
mountNode: options.mount
|
|
45
|
+
});
|
|
46
|
+
refreshViewsIfNeeded(core);
|
|
47
|
+
validateHeader(core);
|
|
48
|
+
core.u32[core.sharedIndex + HDR_ROOT_HANDLE] = rootHandle;
|
|
49
|
+
const step = (now) => {
|
|
50
|
+
const t = now ?? getNow();
|
|
51
|
+
refreshViewsIfNeeded(core);
|
|
52
|
+
flushPendingEvents(core);
|
|
53
|
+
exports.dq_frame(t);
|
|
54
|
+
refreshViewsIfNeeded(core);
|
|
55
|
+
applyCmdBuffer(core);
|
|
56
|
+
};
|
|
57
|
+
let running = false;
|
|
58
|
+
let frameId = 0;
|
|
59
|
+
const loop = (t) => {
|
|
60
|
+
if (!running) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
step(t);
|
|
64
|
+
frameId = scheduleFrame(loop);
|
|
65
|
+
};
|
|
66
|
+
const start = () => {
|
|
67
|
+
if (running) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
running = true;
|
|
71
|
+
frameId = scheduleFrame(loop);
|
|
72
|
+
};
|
|
73
|
+
const stop = () => {
|
|
74
|
+
if (!running) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
running = false;
|
|
78
|
+
cancelFrame(frameId);
|
|
79
|
+
};
|
|
80
|
+
return { start, stop, step, instance };
|
|
81
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createHost } from "./host.js";
|
|
2
|
+
export { startWorker } from "./worker.js";
|
|
3
|
+
export { createDomBackend } from "./backend.js";
|
|
4
|
+
export { applyCmdBuffer, createCore, flushPendingEvents, refreshViewsIfNeeded, validateHeader } from "./core.js";
|
|
5
|
+
export * from "./protocol.js";
|
|
6
|
+
export type { DomBackend } from "./backend.js";
|
|
7
|
+
export declare const wasmUrls: {
|
|
8
|
+
counter: string;
|
|
9
|
+
todomvc: string;
|
|
10
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createHost } from "./host.js";
|
|
2
|
+
export { startWorker } from "./worker.js";
|
|
3
|
+
export { createDomBackend } from "./backend.js";
|
|
4
|
+
export { applyCmdBuffer, createCore, flushPendingEvents, refreshViewsIfNeeded, validateHeader } from "./core.js";
|
|
5
|
+
export * from "./protocol.js";
|
|
6
|
+
export const wasmUrls = {
|
|
7
|
+
counter: new URL("./wasm/domqueue_example_app.wasm", import.meta.url).toString(),
|
|
8
|
+
todomvc: new URL("./wasm/domqueue_todomvc_app.wasm", import.meta.url).toString()
|
|
9
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const DQ_MAGIC = 1146171441;
|
|
2
|
+
export declare const DQ_VERSION = 2;
|
|
3
|
+
export declare const HDR_MAGIC = 0;
|
|
4
|
+
export declare const HDR_VERSION = 1;
|
|
5
|
+
export declare const HDR_CAPS = 2;
|
|
6
|
+
export declare const HDR_CMD_PTR = 3;
|
|
7
|
+
export declare const HDR_CMD_CAP_WORDS = 4;
|
|
8
|
+
export declare const HDR_CMD_LEN_WORDS = 5;
|
|
9
|
+
export declare const HDR_HEAP_BASE_PTR = 6;
|
|
10
|
+
export declare const HDR_ROOT_HANDLE = 7;
|
|
11
|
+
export declare const HDR_EV_PTR = 8;
|
|
12
|
+
export declare const HDR_EV_CAP_WORDS = 9;
|
|
13
|
+
export declare const HDR_EV_HEAD = 10;
|
|
14
|
+
export declare const HDR_EV_TAIL = 11;
|
|
15
|
+
export declare const HDR_CMD_PTR_B = 12;
|
|
16
|
+
export declare const HDR_CMD_LEN_WORDS_B = 13;
|
|
17
|
+
export declare const HDR_CMD_ACTIVE = 14;
|
|
18
|
+
export declare const HDR_SYNC_STATE = 15;
|
|
19
|
+
export declare const HDR_FRAME_ID = 16;
|
|
20
|
+
export declare const HDR_WORDS = 17;
|
|
21
|
+
export declare const CAP_INTERN = 1;
|
|
22
|
+
export declare const CAP_EVENTS = 2;
|
|
23
|
+
export declare const CAP_LIST = 4;
|
|
24
|
+
export declare const CAP_BLUEPRINT = 8;
|
|
25
|
+
export declare const CAP_WORKER_SHARED = 16;
|
|
26
|
+
export declare const HANDLE_GEN_SHIFT = 24;
|
|
27
|
+
export declare const HANDLE_INDEX_MASK = 16777215;
|
|
28
|
+
export declare const EVENT_RECORD_WORDS = 6;
|
|
29
|
+
export declare const makeHandle: (index: number, gen: number) => number;
|
|
30
|
+
export declare const handleIndex: (handle: number) => number;
|
|
31
|
+
export declare const handleGen: (handle: number) => number;
|
|
32
|
+
export declare const OP_CREATE_ELEM = 1;
|
|
33
|
+
export declare const OP_CREATE_TEXT = 2;
|
|
34
|
+
export declare const OP_SET_TEXT = 3;
|
|
35
|
+
export declare const OP_SET_ATTR = 4;
|
|
36
|
+
export declare const OP_APPEND = 5;
|
|
37
|
+
export declare const OP_INSERT_BEFORE = 6;
|
|
38
|
+
export declare const OP_REMOVE = 7;
|
|
39
|
+
export declare const OP_INTERN_STRING = 8;
|
|
40
|
+
export declare const OP_INTERN_ATOM = 9;
|
|
41
|
+
export declare const OP_CREATE_ELEM_ATOM = 10;
|
|
42
|
+
export declare const OP_SET_ATTR_ATOM = 11;
|
|
43
|
+
export declare const OP_SET_TEXT_ID = 12;
|
|
44
|
+
export declare const OP_EVENT_ON = 13;
|
|
45
|
+
export declare const OP_LIST_BEGIN = 14;
|
|
46
|
+
export declare const OP_LIST_ITEM = 15;
|
|
47
|
+
export declare const OP_LIST_END = 16;
|
|
48
|
+
export declare const OP_BLUEPRINT_DEFINE = 17;
|
|
49
|
+
export declare const OP_BLUEPRINT_INSTANTIATE = 18;
|
|
50
|
+
export declare const OP_SLOT_COMMIT = 19;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const DQ_MAGIC = 0x44513031;
|
|
2
|
+
export const DQ_VERSION = 2;
|
|
3
|
+
export const HDR_MAGIC = 0;
|
|
4
|
+
export const HDR_VERSION = 1;
|
|
5
|
+
export const HDR_CAPS = 2;
|
|
6
|
+
export const HDR_CMD_PTR = 3;
|
|
7
|
+
export const HDR_CMD_CAP_WORDS = 4;
|
|
8
|
+
export const HDR_CMD_LEN_WORDS = 5;
|
|
9
|
+
export const HDR_HEAP_BASE_PTR = 6;
|
|
10
|
+
export const HDR_ROOT_HANDLE = 7;
|
|
11
|
+
export const HDR_EV_PTR = 8;
|
|
12
|
+
export const HDR_EV_CAP_WORDS = 9;
|
|
13
|
+
export const HDR_EV_HEAD = 10;
|
|
14
|
+
export const HDR_EV_TAIL = 11;
|
|
15
|
+
export const HDR_CMD_PTR_B = 12;
|
|
16
|
+
export const HDR_CMD_LEN_WORDS_B = 13;
|
|
17
|
+
export const HDR_CMD_ACTIVE = 14;
|
|
18
|
+
export const HDR_SYNC_STATE = 15;
|
|
19
|
+
export const HDR_FRAME_ID = 16;
|
|
20
|
+
export const HDR_WORDS = 17;
|
|
21
|
+
export const CAP_INTERN = 1;
|
|
22
|
+
export const CAP_EVENTS = 2;
|
|
23
|
+
export const CAP_LIST = 4;
|
|
24
|
+
export const CAP_BLUEPRINT = 8;
|
|
25
|
+
export const CAP_WORKER_SHARED = 16;
|
|
26
|
+
export const HANDLE_GEN_SHIFT = 24;
|
|
27
|
+
export const HANDLE_INDEX_MASK = 0x00ffffff;
|
|
28
|
+
export const EVENT_RECORD_WORDS = 6;
|
|
29
|
+
export const makeHandle = (index, gen) => (gen << HANDLE_GEN_SHIFT) | index;
|
|
30
|
+
export const handleIndex = (handle) => handle & HANDLE_INDEX_MASK;
|
|
31
|
+
export const handleGen = (handle) => handle >>> HANDLE_GEN_SHIFT;
|
|
32
|
+
export const OP_CREATE_ELEM = 1;
|
|
33
|
+
export const OP_CREATE_TEXT = 2;
|
|
34
|
+
export const OP_SET_TEXT = 3;
|
|
35
|
+
export const OP_SET_ATTR = 4;
|
|
36
|
+
export const OP_APPEND = 5;
|
|
37
|
+
export const OP_INSERT_BEFORE = 6;
|
|
38
|
+
export const OP_REMOVE = 7;
|
|
39
|
+
export const OP_INTERN_STRING = 8;
|
|
40
|
+
export const OP_INTERN_ATOM = 9;
|
|
41
|
+
export const OP_CREATE_ELEM_ATOM = 10;
|
|
42
|
+
export const OP_SET_ATTR_ATOM = 11;
|
|
43
|
+
export const OP_SET_TEXT_ID = 12;
|
|
44
|
+
export const OP_EVENT_ON = 13;
|
|
45
|
+
export const OP_LIST_BEGIN = 14;
|
|
46
|
+
export const OP_LIST_ITEM = 15;
|
|
47
|
+
export const OP_LIST_END = 16;
|
|
48
|
+
export const OP_BLUEPRINT_DEFINE = 17;
|
|
49
|
+
export const OP_BLUEPRINT_INSTANTIATE = 18;
|
|
50
|
+
export const OP_SLOT_COMMIT = 19;
|
|
Binary file
|
|
Binary file
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type WorkerOptions = {
|
|
2
|
+
wasmUrl: string;
|
|
3
|
+
importObject?: WebAssembly.Imports;
|
|
4
|
+
tickMs?: number;
|
|
5
|
+
onReady?: (sharedPtr: number, instance: WebAssembly.Instance) => void;
|
|
6
|
+
};
|
|
7
|
+
export type WorkerRuntime = {
|
|
8
|
+
stop: () => void;
|
|
9
|
+
instance: WebAssembly.Instance;
|
|
10
|
+
sharedPtr: number;
|
|
11
|
+
};
|
|
12
|
+
export declare const startWorker: (options: WorkerOptions) => Promise<WorkerRuntime>;
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { HDR_SYNC_STATE } from "./protocol.js";
|
|
2
|
+
const nowMs = () => {
|
|
3
|
+
const perf = globalThis.performance;
|
|
4
|
+
if (perf && typeof perf.now === "function") {
|
|
5
|
+
return perf.now();
|
|
6
|
+
}
|
|
7
|
+
return Date.now();
|
|
8
|
+
};
|
|
9
|
+
export const startWorker = async (options) => {
|
|
10
|
+
const response = await fetch(options.wasmUrl);
|
|
11
|
+
const result = await WebAssembly.instantiateStreaming(response, options.importObject ?? {});
|
|
12
|
+
const instance = result.instance;
|
|
13
|
+
const exports = instance.exports;
|
|
14
|
+
if (!exports.memory || typeof exports.dq_get_shared_ptr !== "function" || typeof exports.dq_frame !== "function") {
|
|
15
|
+
throw new Error("Missing DQ exports");
|
|
16
|
+
}
|
|
17
|
+
const sharedPtr = exports.dq_get_shared_ptr();
|
|
18
|
+
const tickMs = options.tickMs ?? 16;
|
|
19
|
+
options.onReady?.(sharedPtr, instance);
|
|
20
|
+
let buffer = exports.memory.buffer;
|
|
21
|
+
let u32 = new Uint32Array(buffer);
|
|
22
|
+
const headerIndex = sharedPtr >>> 2;
|
|
23
|
+
const refresh = () => {
|
|
24
|
+
if (exports.memory.buffer !== buffer) {
|
|
25
|
+
buffer = exports.memory.buffer;
|
|
26
|
+
u32 = new Uint32Array(buffer);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
let running = true;
|
|
30
|
+
const loop = () => {
|
|
31
|
+
if (!running) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
refresh();
|
|
35
|
+
u32[headerIndex + HDR_SYNC_STATE] = 1;
|
|
36
|
+
exports.dq_frame(nowMs());
|
|
37
|
+
refresh();
|
|
38
|
+
u32[headerIndex + HDR_SYNC_STATE] = 2;
|
|
39
|
+
setTimeout(loop, tickMs);
|
|
40
|
+
};
|
|
41
|
+
loop();
|
|
42
|
+
const stop = () => {
|
|
43
|
+
running = false;
|
|
44
|
+
};
|
|
45
|
+
return { stop, instance, sharedPtr };
|
|
46
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "domqueue-host",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"license": "AGPL-3.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"WORKER.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"build:wasm": "node scripts/build-wasm.mjs",
|
|
25
|
+
"prepublishOnly": "npm run build && npm run build:wasm",
|
|
26
|
+
"test": "node --test"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.9.2"
|
|
30
|
+
}
|
|
31
|
+
}
|