@mindexec/cli 0.2.499 → 0.2.501
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/package.json +1 -1
- package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
- package/scripts/remote-registry-follower-smoke.mjs +4 -8
- package/wwwroot/_content/MindExecution.Shared/css/native-hybrid.css +98 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-native-render-mirror.js +27 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-native-render-webview2.js +1275 -0
- package/wwwroot/_framework/{MindExecution.Core.s6sxjy1v7h.dll → MindExecution.Core.tzma6emhy9.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Kernel.yta9wud6wh.dll → MindExecution.Kernel.8uq80sy1fj.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Admin.7aytj09ejo.dll → MindExecution.Plugins.Admin.9wa3rcqkgd.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Business.wzj1pi81by.dll → MindExecution.Plugins.Business.9dppflwwaw.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Concept.o6eyyttldg.dll → MindExecution.Plugins.Concept.vzlljp6zpk.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Directory.rkqhevehjj.dll → MindExecution.Plugins.Directory.vygyxe6pw5.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.qfmuwz1kj6.dll → MindExecution.Plugins.PlanMaster.i8uxoahd9m.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.YouTube.p6hp1w5nfq.dll → MindExecution.Plugins.YouTube.z0ibm6ffx5.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Shared.b4arhy9bqr.dll → MindExecution.Shared.cdr8vpcn5m.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Web.hlrui65yuu.dll → MindExecution.Web.8l14n1gl76.dll} +0 -0
- package/wwwroot/_framework/blazor.boot.json +21 -21
- package/wwwroot/index.html +3 -1
- package/wwwroot/service-worker-assets.js +32 -24
- package/wwwroot/service-worker.js +1 -1
|
@@ -0,0 +1,1275 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const BUILD_ID = '20260831-native-render-webview2-v003';
|
|
5
|
+
const WIRE_PREFIX = 'mindexec.native-render.v1';
|
|
6
|
+
const PROTOCOL_MAGIC = 0x31524e4d;
|
|
7
|
+
const PROTOCOL_VERSION = 1;
|
|
8
|
+
const HEADER_BYTES = 12;
|
|
9
|
+
const MAX_BINARY_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
const MAX_BOARD_ID_BYTES = 256;
|
|
11
|
+
const MAX_BACKEND_BYTES = 64;
|
|
12
|
+
const MAX_REASON_BYTES = 512;
|
|
13
|
+
const MAX_NODE_COUNT = 250000;
|
|
14
|
+
const MAX_EDGE_COUNT = 500000;
|
|
15
|
+
const MAX_EDGE_POINT_COUNT = 2000000;
|
|
16
|
+
const MAX_PENDING_REQUESTS = 8;
|
|
17
|
+
const RESPONSE_TIMEOUT_MS = 5000;
|
|
18
|
+
const MAX_UINT32 = 0xffffffff;
|
|
19
|
+
const MAX_UINT64 = 0xffffffffffffffffn;
|
|
20
|
+
const REQUEST_ID_PATTERN = /^[1-9][0-9]{0,15}$/;
|
|
21
|
+
const SESSION_TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,64}$/;
|
|
22
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
23
|
+
|
|
24
|
+
const REQUEST_TYPE = Object.freeze({
|
|
25
|
+
snapshot: 1,
|
|
26
|
+
camera: 2,
|
|
27
|
+
detach: 3
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const RESPONSE_TYPE = Object.freeze({
|
|
31
|
+
ready: 0x8001,
|
|
32
|
+
accepted: 0x8002,
|
|
33
|
+
rejected: 0x8003,
|
|
34
|
+
shutdown: 0x8004
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const webview = window?.chrome?.webview;
|
|
38
|
+
if (!webview ||
|
|
39
|
+
typeof webview.postMessage !== 'function' ||
|
|
40
|
+
typeof webview.addEventListener !== 'function') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let hybridCompareEnabled = false;
|
|
45
|
+
try {
|
|
46
|
+
hybridCompareEnabled = new window.URLSearchParams(window.location?.search || '')
|
|
47
|
+
.get('nativeDesktop') === 'hybridCompare';
|
|
48
|
+
} catch {
|
|
49
|
+
hybridCompareEnabled = false;
|
|
50
|
+
}
|
|
51
|
+
if (!hybridCompareEnabled ||
|
|
52
|
+
typeof window.TextEncoder !== 'function' ||
|
|
53
|
+
typeof window.TextDecoder !== 'function' ||
|
|
54
|
+
typeof window.atob !== 'function' ||
|
|
55
|
+
typeof window.btoa !== 'function' ||
|
|
56
|
+
typeof window.setTimeout !== 'function' ||
|
|
57
|
+
typeof window.clearTimeout !== 'function') {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const textEncoder = new window.TextEncoder();
|
|
62
|
+
const textDecoder = new window.TextDecoder('utf-8', { fatal: true });
|
|
63
|
+
const statusListeners = new Set();
|
|
64
|
+
const pending = new Map();
|
|
65
|
+
const root = window.document?.documentElement || null;
|
|
66
|
+
const visualViewportTarget = window.visualViewport || null;
|
|
67
|
+
|
|
68
|
+
let nextRequestId = 1;
|
|
69
|
+
let sessionToken = '';
|
|
70
|
+
let statusChecked = false;
|
|
71
|
+
let disposed = false;
|
|
72
|
+
let hasAcceptedSnapshot = false;
|
|
73
|
+
let acceptedSnapshotOwner = null;
|
|
74
|
+
let viewportEpoch = 0;
|
|
75
|
+
let viewportAcknowledgementRequired = false;
|
|
76
|
+
let requestedCompareLayer = 'web';
|
|
77
|
+
let effectiveCompareLayer = 'web';
|
|
78
|
+
const status = {
|
|
79
|
+
mode: 'mirror',
|
|
80
|
+
ready: false,
|
|
81
|
+
backend: '',
|
|
82
|
+
engineGeneration: 0,
|
|
83
|
+
acceptedFrames: 0,
|
|
84
|
+
rejectedFrames: 0,
|
|
85
|
+
lastError: ''
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function createProtocolError(code) {
|
|
89
|
+
const error = new Error(code);
|
|
90
|
+
error.code = code;
|
|
91
|
+
return error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toFiniteNumber(value, name) {
|
|
95
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
96
|
+
throw createProtocolError(`native-renderer-invalid-${name}`);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function toUint32(value, name) {
|
|
102
|
+
if (typeof value !== 'number' ||
|
|
103
|
+
!Number.isInteger(value) ||
|
|
104
|
+
value < 0 ||
|
|
105
|
+
value > MAX_UINT32) {
|
|
106
|
+
throw createProtocolError(`native-renderer-invalid-${name}`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function toUint64(value, name) {
|
|
112
|
+
let result;
|
|
113
|
+
if (typeof value === 'bigint') {
|
|
114
|
+
result = value;
|
|
115
|
+
} else if (typeof value === 'number') {
|
|
116
|
+
if (!Number.isInteger(value)) {
|
|
117
|
+
throw createProtocolError(`native-renderer-invalid-${name}`);
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isSafeInteger(value)) {
|
|
120
|
+
throw createProtocolError(`native-renderer-unsafe-${name}`);
|
|
121
|
+
}
|
|
122
|
+
result = BigInt(value);
|
|
123
|
+
} else {
|
|
124
|
+
throw createProtocolError(`native-renderer-invalid-${name}`);
|
|
125
|
+
}
|
|
126
|
+
if (result < 0n || result > MAX_UINT64) {
|
|
127
|
+
throw createProtocolError(`native-renderer-invalid-${name}`);
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function encodeUtf8U16(value, name, { allowEmpty = false, maxBytes = 0xffff } = {}) {
|
|
133
|
+
if (typeof value !== 'string' || value.includes('\0')) {
|
|
134
|
+
throw createProtocolError(`native-renderer-invalid-${name}`);
|
|
135
|
+
}
|
|
136
|
+
const encoded = textEncoder.encode(value);
|
|
137
|
+
let roundTrip;
|
|
138
|
+
try {
|
|
139
|
+
roundTrip = textDecoder.decode(encoded);
|
|
140
|
+
} catch {
|
|
141
|
+
throw createProtocolError(`native-renderer-invalid-${name}`);
|
|
142
|
+
}
|
|
143
|
+
if (roundTrip !== value || (!allowEmpty && encoded.length === 0) || encoded.length > maxBytes) {
|
|
144
|
+
throw createProtocolError(`native-renderer-invalid-${name}`);
|
|
145
|
+
}
|
|
146
|
+
return encoded;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeOwner(owner) {
|
|
150
|
+
if (!owner || typeof owner !== 'object' || Array.isArray(owner)) {
|
|
151
|
+
throw createProtocolError('native-renderer-invalid-owner');
|
|
152
|
+
}
|
|
153
|
+
const normalized = {
|
|
154
|
+
boardId: owner.boardId,
|
|
155
|
+
boardIdBytes: encodeUtf8U16(owner.boardId, 'board-id', { maxBytes: MAX_BOARD_ID_BYTES }),
|
|
156
|
+
engineGeneration: toUint64(owner.engineGeneration, 'engine-generation'),
|
|
157
|
+
boardEpoch: toUint64(owner.boardEpoch, 'board-epoch'),
|
|
158
|
+
boardRevision: toUint64(owner.boardRevision, 'board-revision'),
|
|
159
|
+
requestSequence: toUint64(owner.requestSequence, 'request-sequence')
|
|
160
|
+
};
|
|
161
|
+
if (normalized.engineGeneration === 0n ||
|
|
162
|
+
normalized.boardEpoch === 0n ||
|
|
163
|
+
normalized.boardRevision === 0n ||
|
|
164
|
+
normalized.requestSequence === 0n) {
|
|
165
|
+
throw createProtocolError('native-renderer-owner-token-zero');
|
|
166
|
+
}
|
|
167
|
+
return normalized;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function ownersEqual(left, right) {
|
|
171
|
+
return left?.boardId === right?.boardId &&
|
|
172
|
+
left?.engineGeneration === right?.engineGeneration &&
|
|
173
|
+
left?.boardEpoch === right?.boardEpoch &&
|
|
174
|
+
left?.boardRevision === right?.boardRevision &&
|
|
175
|
+
left?.requestSequence === right?.requestSequence;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeCamera(camera) {
|
|
179
|
+
if (!camera || typeof camera !== 'object' || Array.isArray(camera)) {
|
|
180
|
+
throw createProtocolError('native-renderer-invalid-camera');
|
|
181
|
+
}
|
|
182
|
+
const normalized = {
|
|
183
|
+
x: toFiniteNumber(camera.x, 'camera-x'),
|
|
184
|
+
y: toFiniteNumber(camera.y, 'camera-y'),
|
|
185
|
+
z: toFiniteNumber(camera.z, 'camera-z'),
|
|
186
|
+
near: toFiniteNumber(camera.near, 'camera-near'),
|
|
187
|
+
far: toFiniteNumber(camera.far, 'camera-far'),
|
|
188
|
+
fov: toFiniteNumber(camera.fov, 'camera-fov'),
|
|
189
|
+
viewportW: toFiniteNumber(camera.viewportW, 'camera-viewport-w'),
|
|
190
|
+
viewportH: toFiniteNumber(camera.viewportH, 'camera-viewport-h'),
|
|
191
|
+
zoom: toFiniteNumber(camera.zoom, 'camera-zoom')
|
|
192
|
+
};
|
|
193
|
+
if (normalized.near <= 0 ||
|
|
194
|
+
normalized.far <= normalized.near ||
|
|
195
|
+
normalized.fov <= 0 ||
|
|
196
|
+
normalized.fov >= 180 ||
|
|
197
|
+
normalized.viewportW <= 0 ||
|
|
198
|
+
normalized.viewportH <= 0 ||
|
|
199
|
+
normalized.zoom <= 0) {
|
|
200
|
+
throw createProtocolError('native-renderer-invalid-camera-range');
|
|
201
|
+
}
|
|
202
|
+
return normalized;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function currentRawPhysicalViewport() {
|
|
206
|
+
const visualViewport = window.visualViewport;
|
|
207
|
+
const visualWidth = Number(visualViewport?.width);
|
|
208
|
+
const visualHeight = Number(visualViewport?.height);
|
|
209
|
+
const innerWidth = Number(window.innerWidth);
|
|
210
|
+
const innerHeight = Number(window.innerHeight);
|
|
211
|
+
const cssWidth = Number.isFinite(visualWidth) && visualWidth > 0
|
|
212
|
+
? visualWidth
|
|
213
|
+
: innerWidth;
|
|
214
|
+
const cssHeight = Number.isFinite(visualHeight) && visualHeight > 0
|
|
215
|
+
? visualHeight
|
|
216
|
+
: innerHeight;
|
|
217
|
+
const dpr = Number(window.devicePixelRatio);
|
|
218
|
+
if (!Number.isFinite(cssWidth) || cssWidth <= 0 ||
|
|
219
|
+
!Number.isFinite(cssHeight) || cssHeight <= 0 ||
|
|
220
|
+
!Number.isFinite(dpr) || dpr <= 0) {
|
|
221
|
+
throw createProtocolError('native-renderer-invalid-physical-viewport');
|
|
222
|
+
}
|
|
223
|
+
const viewportW = Math.max(1, Math.round(cssWidth * dpr));
|
|
224
|
+
const viewportH = Math.max(1, Math.round(cssHeight * dpr));
|
|
225
|
+
if (!Number.isSafeInteger(viewportW) || !Number.isSafeInteger(viewportH)) {
|
|
226
|
+
throw createProtocolError('native-renderer-invalid-physical-viewport');
|
|
227
|
+
}
|
|
228
|
+
return { viewportW, viewportH };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function normalizeHybridCamera(camera) {
|
|
232
|
+
return {
|
|
233
|
+
...normalizeCamera(camera),
|
|
234
|
+
...currentRawPhysicalViewport()
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeNodes(nodes) {
|
|
239
|
+
if (!Array.isArray(nodes)) {
|
|
240
|
+
throw createProtocolError('native-renderer-invalid-nodes');
|
|
241
|
+
}
|
|
242
|
+
if (nodes.length > MAX_NODE_COUNT) {
|
|
243
|
+
throw createProtocolError('native-renderer-too-many-nodes');
|
|
244
|
+
}
|
|
245
|
+
const handles = new Set();
|
|
246
|
+
const normalized = [];
|
|
247
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
248
|
+
const node = nodes[index];
|
|
249
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
250
|
+
throw createProtocolError(`native-renderer-invalid-node-${index}`);
|
|
251
|
+
}
|
|
252
|
+
const handle = toUint32(node.handle, `node-handle-${index}`);
|
|
253
|
+
if (handle === 0 || handles.has(handle)) {
|
|
254
|
+
throw createProtocolError(`native-renderer-duplicate-node-handle-${index}`);
|
|
255
|
+
}
|
|
256
|
+
handles.add(handle);
|
|
257
|
+
const entry = {
|
|
258
|
+
handle,
|
|
259
|
+
x: toFiniteNumber(node.x, `node-x-${index}`),
|
|
260
|
+
y: toFiniteNumber(node.y, `node-y-${index}`),
|
|
261
|
+
z: toFiniteNumber(node.z, `node-z-${index}`),
|
|
262
|
+
width: toFiniteNumber(node.width, `node-width-${index}`),
|
|
263
|
+
height: toFiniteNumber(node.height, `node-height-${index}`),
|
|
264
|
+
fillRgba: toUint32(node.fillRgba, `node-fill-${index}`)
|
|
265
|
+
};
|
|
266
|
+
if (entry.width <= 0 || entry.height <= 0) {
|
|
267
|
+
throw createProtocolError(`native-renderer-invalid-node-size-${index}`);
|
|
268
|
+
}
|
|
269
|
+
normalized.push(entry);
|
|
270
|
+
}
|
|
271
|
+
return { nodes: normalized, handles };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function normalizeEdges(edges, nodeHandles) {
|
|
275
|
+
if (!Array.isArray(edges)) {
|
|
276
|
+
throw createProtocolError('native-renderer-invalid-edges');
|
|
277
|
+
}
|
|
278
|
+
if (edges.length > MAX_EDGE_COUNT) {
|
|
279
|
+
throw createProtocolError('native-renderer-too-many-edges');
|
|
280
|
+
}
|
|
281
|
+
const handles = new Set();
|
|
282
|
+
const normalized = [];
|
|
283
|
+
let totalPointCount = 0;
|
|
284
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
285
|
+
const edge = edges[index];
|
|
286
|
+
if (!edge || typeof edge !== 'object' || Array.isArray(edge) || !Array.isArray(edge.points)) {
|
|
287
|
+
throw createProtocolError(`native-renderer-invalid-edge-${index}`);
|
|
288
|
+
}
|
|
289
|
+
const handle = toUint32(edge.handle, `edge-handle-${index}`);
|
|
290
|
+
if (handle === 0 || handles.has(handle)) {
|
|
291
|
+
throw createProtocolError(`native-renderer-duplicate-edge-handle-${index}`);
|
|
292
|
+
}
|
|
293
|
+
handles.add(handle);
|
|
294
|
+
if (edge.points.length < 2) {
|
|
295
|
+
throw createProtocolError(`native-renderer-invalid-edge-points-${index}`);
|
|
296
|
+
}
|
|
297
|
+
totalPointCount += edge.points.length;
|
|
298
|
+
if (!Number.isSafeInteger(totalPointCount) || totalPointCount > MAX_EDGE_POINT_COUNT) {
|
|
299
|
+
throw createProtocolError('native-renderer-too-many-edge-points');
|
|
300
|
+
}
|
|
301
|
+
const points = [];
|
|
302
|
+
for (let pointIndex = 0; pointIndex < edge.points.length; pointIndex += 1) {
|
|
303
|
+
const point = edge.points[pointIndex];
|
|
304
|
+
if (!point || typeof point !== 'object' || Array.isArray(point)) {
|
|
305
|
+
throw createProtocolError(`native-renderer-invalid-edge-point-${index}-${pointIndex}`);
|
|
306
|
+
}
|
|
307
|
+
points.push({
|
|
308
|
+
x: toFiniteNumber(point.x, `edge-point-x-${index}-${pointIndex}`),
|
|
309
|
+
y: toFiniteNumber(point.y, `edge-point-y-${index}-${pointIndex}`)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const entry = {
|
|
313
|
+
handle,
|
|
314
|
+
sourceHandle: toUint32(edge.sourceHandle, `edge-source-${index}`),
|
|
315
|
+
targetHandle: toUint32(edge.targetHandle, `edge-target-${index}`),
|
|
316
|
+
z: toFiniteNumber(edge.z, `edge-z-${index}`),
|
|
317
|
+
width: toFiniteNumber(edge.width, `edge-width-${index}`),
|
|
318
|
+
colorRgba: toUint32(edge.colorRgba, `edge-color-${index}`),
|
|
319
|
+
points
|
|
320
|
+
};
|
|
321
|
+
if (entry.sourceHandle === 0 ||
|
|
322
|
+
entry.targetHandle === 0 ||
|
|
323
|
+
!nodeHandles.has(entry.sourceHandle) ||
|
|
324
|
+
!nodeHandles.has(entry.targetHandle) ||
|
|
325
|
+
entry.width <= 0) {
|
|
326
|
+
throw createProtocolError(`native-renderer-invalid-edge-range-${index}`);
|
|
327
|
+
}
|
|
328
|
+
normalized.push(entry);
|
|
329
|
+
}
|
|
330
|
+
return { edges: normalized, totalPointCount };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
class BinaryWriter {
|
|
334
|
+
constructor(length) {
|
|
335
|
+
this.bytes = new Uint8Array(length);
|
|
336
|
+
this.view = new DataView(this.bytes.buffer);
|
|
337
|
+
this.offset = 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
uint16(value) {
|
|
341
|
+
this.view.setUint16(this.offset, value, true);
|
|
342
|
+
this.offset += 2;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
uint32(value) {
|
|
346
|
+
this.view.setUint32(this.offset, value, true);
|
|
347
|
+
this.offset += 4;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
uint64(value) {
|
|
351
|
+
this.view.setBigUint64(this.offset, value, true);
|
|
352
|
+
this.offset += 8;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
float64(value) {
|
|
356
|
+
this.view.setFloat64(this.offset, value, true);
|
|
357
|
+
this.offset += 8;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
raw(bytes) {
|
|
361
|
+
this.bytes.set(bytes, this.offset);
|
|
362
|
+
this.offset += bytes.length;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
finish(code) {
|
|
366
|
+
if (this.offset !== this.bytes.length) {
|
|
367
|
+
throw createProtocolError(code);
|
|
368
|
+
}
|
|
369
|
+
return this.bytes;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function ownerPayloadBytes(owner) {
|
|
374
|
+
return 2 + owner.boardIdBytes.length + 32;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function writeOwner(writer, owner) {
|
|
378
|
+
writer.uint16(owner.boardIdBytes.length);
|
|
379
|
+
writer.raw(owner.boardIdBytes);
|
|
380
|
+
writer.uint64(owner.engineGeneration);
|
|
381
|
+
writer.uint64(owner.boardEpoch);
|
|
382
|
+
writer.uint64(owner.boardRevision);
|
|
383
|
+
writer.uint64(owner.requestSequence);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function writeCamera(writer, camera) {
|
|
387
|
+
for (const value of [
|
|
388
|
+
camera.x,
|
|
389
|
+
camera.y,
|
|
390
|
+
camera.z,
|
|
391
|
+
camera.near,
|
|
392
|
+
camera.far,
|
|
393
|
+
camera.fov,
|
|
394
|
+
camera.viewportW,
|
|
395
|
+
camera.viewportH,
|
|
396
|
+
camera.zoom
|
|
397
|
+
]) {
|
|
398
|
+
writer.float64(value);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function framePayload(requestType, payload) {
|
|
403
|
+
const frameLength = HEADER_BYTES + payload.length;
|
|
404
|
+
if (frameLength > MAX_BINARY_BYTES) {
|
|
405
|
+
throw createProtocolError('native-renderer-payload-too-large');
|
|
406
|
+
}
|
|
407
|
+
const writer = new BinaryWriter(frameLength);
|
|
408
|
+
writer.uint32(PROTOCOL_MAGIC);
|
|
409
|
+
writer.uint16(PROTOCOL_VERSION);
|
|
410
|
+
writer.uint16(requestType);
|
|
411
|
+
writer.uint32(payload.length);
|
|
412
|
+
writer.raw(payload);
|
|
413
|
+
return writer.finish('native-renderer-frame-size-mismatch');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function encodeSnapshotMessage(scene, owner) {
|
|
417
|
+
if (!scene || typeof scene !== 'object' || Array.isArray(scene)) {
|
|
418
|
+
throw createProtocolError('native-renderer-invalid-scene');
|
|
419
|
+
}
|
|
420
|
+
const camera = normalizeHybridCamera(scene.camera);
|
|
421
|
+
if (!Array.isArray(scene.nodes) || !Array.isArray(scene.edges)) {
|
|
422
|
+
throw createProtocolError(!Array.isArray(scene.nodes)
|
|
423
|
+
? 'native-renderer-invalid-nodes'
|
|
424
|
+
: 'native-renderer-invalid-edges');
|
|
425
|
+
}
|
|
426
|
+
const fixedLength = ownerPayloadBytes(owner) + 72 + 12 +
|
|
427
|
+
scene.nodes.length * 48 + scene.edges.length * 36;
|
|
428
|
+
if (!Number.isSafeInteger(fixedLength) || HEADER_BYTES + fixedLength > MAX_BINARY_BYTES) {
|
|
429
|
+
throw createProtocolError('native-renderer-payload-too-large');
|
|
430
|
+
}
|
|
431
|
+
const normalizedNodes = normalizeNodes(scene.nodes);
|
|
432
|
+
const normalizedEdges = normalizeEdges(scene.edges, normalizedNodes.handles);
|
|
433
|
+
const payloadLength = fixedLength + normalizedEdges.totalPointCount * 16;
|
|
434
|
+
if (!Number.isSafeInteger(payloadLength) || HEADER_BYTES + payloadLength > MAX_BINARY_BYTES) {
|
|
435
|
+
throw createProtocolError('native-renderer-payload-too-large');
|
|
436
|
+
}
|
|
437
|
+
const writer = new BinaryWriter(payloadLength);
|
|
438
|
+
writeOwner(writer, owner);
|
|
439
|
+
writeCamera(writer, camera);
|
|
440
|
+
writer.uint32(normalizedNodes.nodes.length);
|
|
441
|
+
writer.uint32(normalizedEdges.edges.length);
|
|
442
|
+
writer.uint32(normalizedEdges.totalPointCount);
|
|
443
|
+
|
|
444
|
+
for (const node of normalizedNodes.nodes) {
|
|
445
|
+
writer.uint32(node.handle);
|
|
446
|
+
writer.float64(node.x);
|
|
447
|
+
writer.float64(node.y);
|
|
448
|
+
writer.float64(node.z);
|
|
449
|
+
writer.float64(node.width);
|
|
450
|
+
writer.float64(node.height);
|
|
451
|
+
writer.uint32(node.fillRgba);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (const edge of normalizedEdges.edges) {
|
|
455
|
+
writer.uint32(edge.handle);
|
|
456
|
+
writer.uint32(edge.sourceHandle);
|
|
457
|
+
writer.uint32(edge.targetHandle);
|
|
458
|
+
writer.float64(edge.z);
|
|
459
|
+
writer.float64(edge.width);
|
|
460
|
+
writer.uint32(edge.colorRgba);
|
|
461
|
+
writer.uint32(edge.points.length);
|
|
462
|
+
for (const point of edge.points) {
|
|
463
|
+
writer.float64(point.x);
|
|
464
|
+
writer.float64(point.y);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return framePayload(
|
|
469
|
+
REQUEST_TYPE.snapshot,
|
|
470
|
+
writer.finish('native-renderer-snapshot-size-mismatch')
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function encodeCameraMessage(frame, owner) {
|
|
475
|
+
if (!frame || typeof frame !== 'object' || Array.isArray(frame)) {
|
|
476
|
+
throw createProtocolError('native-renderer-invalid-camera-frame');
|
|
477
|
+
}
|
|
478
|
+
const camera = normalizeHybridCamera(frame.camera);
|
|
479
|
+
const writer = new BinaryWriter(ownerPayloadBytes(owner) + 72);
|
|
480
|
+
writeOwner(writer, owner);
|
|
481
|
+
writeCamera(writer, camera);
|
|
482
|
+
return framePayload(
|
|
483
|
+
REQUEST_TYPE.camera,
|
|
484
|
+
writer.finish('native-renderer-camera-size-mismatch')
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function encodeDetachMessage(owner) {
|
|
489
|
+
const writer = new BinaryWriter(ownerPayloadBytes(owner));
|
|
490
|
+
writeOwner(writer, owner);
|
|
491
|
+
return framePayload(
|
|
492
|
+
REQUEST_TYPE.detach,
|
|
493
|
+
writer.finish('native-renderer-detach-size-mismatch')
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
class PayloadReader {
|
|
498
|
+
constructor(bytes) {
|
|
499
|
+
this.bytes = bytes;
|
|
500
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
501
|
+
this.offset = 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
require(count) {
|
|
505
|
+
if (!Number.isSafeInteger(count) || count < 0 || count > this.bytes.length - this.offset) {
|
|
506
|
+
throw createProtocolError('native-renderer-response-truncated');
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
uint8() {
|
|
511
|
+
this.require(1);
|
|
512
|
+
const value = this.view.getUint8(this.offset);
|
|
513
|
+
this.offset += 1;
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
uint16() {
|
|
518
|
+
this.require(2);
|
|
519
|
+
const value = this.view.getUint16(this.offset, true);
|
|
520
|
+
this.offset += 2;
|
|
521
|
+
return value;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
uint64() {
|
|
525
|
+
this.require(8);
|
|
526
|
+
const value = this.view.getBigUint64(this.offset, true);
|
|
527
|
+
this.offset += 8;
|
|
528
|
+
return value;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
stringU16(name, { allowEmpty = true, maxBytes = 0xffff } = {}) {
|
|
532
|
+
const length = this.uint16();
|
|
533
|
+
if (length > maxBytes) {
|
|
534
|
+
throw createProtocolError(`native-renderer-response-invalid-${name}`);
|
|
535
|
+
}
|
|
536
|
+
this.require(length);
|
|
537
|
+
const bytes = this.bytes.subarray(this.offset, this.offset + length);
|
|
538
|
+
this.offset += length;
|
|
539
|
+
let value;
|
|
540
|
+
try {
|
|
541
|
+
value = textDecoder.decode(bytes);
|
|
542
|
+
} catch {
|
|
543
|
+
throw createProtocolError(`native-renderer-response-invalid-${name}`);
|
|
544
|
+
}
|
|
545
|
+
if ((!allowEmpty && value.length === 0) || value.includes('\0')) {
|
|
546
|
+
throw createProtocolError(`native-renderer-response-invalid-${name}`);
|
|
547
|
+
}
|
|
548
|
+
return value;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
owner() {
|
|
552
|
+
const owner = {
|
|
553
|
+
boardId: this.stringU16('board-id', { allowEmpty: false, maxBytes: MAX_BOARD_ID_BYTES }),
|
|
554
|
+
engineGeneration: this.uint64(),
|
|
555
|
+
boardEpoch: this.uint64(),
|
|
556
|
+
boardRevision: this.uint64(),
|
|
557
|
+
requestSequence: this.uint64()
|
|
558
|
+
};
|
|
559
|
+
if (owner.engineGeneration === 0n ||
|
|
560
|
+
owner.boardEpoch === 0n ||
|
|
561
|
+
owner.boardRevision === 0n ||
|
|
562
|
+
owner.requestSequence === 0n) {
|
|
563
|
+
throw createProtocolError('native-renderer-response-owner-token-zero');
|
|
564
|
+
}
|
|
565
|
+
return owner;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
finish() {
|
|
569
|
+
if (this.offset !== this.bytes.length) {
|
|
570
|
+
throw createProtocolError('native-renderer-response-trailing-bytes');
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function decodeResponseFrame(bytes) {
|
|
576
|
+
if (!(bytes instanceof Uint8Array) || bytes.length < HEADER_BYTES) {
|
|
577
|
+
throw createProtocolError('native-renderer-response-truncated');
|
|
578
|
+
}
|
|
579
|
+
if (bytes.length > MAX_BINARY_BYTES) {
|
|
580
|
+
throw createProtocolError('native-renderer-response-too-large');
|
|
581
|
+
}
|
|
582
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
583
|
+
const magic = view.getUint32(0, true);
|
|
584
|
+
const version = view.getUint16(4, true);
|
|
585
|
+
const responseType = view.getUint16(6, true);
|
|
586
|
+
const payloadLength = view.getUint32(8, true);
|
|
587
|
+
if (magic !== PROTOCOL_MAGIC || version !== PROTOCOL_VERSION) {
|
|
588
|
+
throw createProtocolError('native-renderer-response-header-invalid');
|
|
589
|
+
}
|
|
590
|
+
if (HEADER_BYTES + payloadLength !== bytes.length) {
|
|
591
|
+
throw createProtocolError('native-renderer-response-size-mismatch');
|
|
592
|
+
}
|
|
593
|
+
const reader = new PayloadReader(bytes.subarray(HEADER_BYTES));
|
|
594
|
+
let response;
|
|
595
|
+
switch (responseType) {
|
|
596
|
+
case RESPONSE_TYPE.ready:
|
|
597
|
+
response = {
|
|
598
|
+
type: 'ready',
|
|
599
|
+
backend: reader.stringU16('backend', { allowEmpty: false, maxBytes: MAX_BACKEND_BYTES })
|
|
600
|
+
};
|
|
601
|
+
break;
|
|
602
|
+
case RESPONSE_TYPE.accepted:
|
|
603
|
+
response = {
|
|
604
|
+
type: 'accepted',
|
|
605
|
+
requestType: reader.uint16(),
|
|
606
|
+
owner: reader.owner()
|
|
607
|
+
};
|
|
608
|
+
break;
|
|
609
|
+
case RESPONSE_TYPE.rejected: {
|
|
610
|
+
const requestType = reader.uint16();
|
|
611
|
+
const hasOwner = reader.uint8();
|
|
612
|
+
if (hasOwner !== 0 && hasOwner !== 1) {
|
|
613
|
+
throw createProtocolError('native-renderer-response-invalid-owner-flag');
|
|
614
|
+
}
|
|
615
|
+
response = {
|
|
616
|
+
type: 'rejected',
|
|
617
|
+
requestType,
|
|
618
|
+
owner: hasOwner === 1 ? reader.owner() : null,
|
|
619
|
+
reason: reader.stringU16('reason', { allowEmpty: false, maxBytes: MAX_REASON_BYTES })
|
|
620
|
+
};
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
case RESPONSE_TYPE.shutdown:
|
|
624
|
+
response = { type: 'shutdown' };
|
|
625
|
+
break;
|
|
626
|
+
default:
|
|
627
|
+
throw createProtocolError('native-renderer-response-type-unknown');
|
|
628
|
+
}
|
|
629
|
+
reader.finish();
|
|
630
|
+
return response;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function bytesToBase64(bytes) {
|
|
634
|
+
const chunkBytes = 0x6000;
|
|
635
|
+
let encoded = '';
|
|
636
|
+
for (let offset = 0; offset < bytes.length; offset += chunkBytes) {
|
|
637
|
+
const chunk = bytes.subarray(offset, Math.min(bytes.length, offset + chunkBytes));
|
|
638
|
+
let binary = '';
|
|
639
|
+
for (let index = 0; index < chunk.length; index += 1) {
|
|
640
|
+
binary += String.fromCharCode(chunk[index]);
|
|
641
|
+
}
|
|
642
|
+
encoded += window.btoa(binary);
|
|
643
|
+
}
|
|
644
|
+
return encoded;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function base64ToBytes(value, maxBytes, errorCode) {
|
|
648
|
+
if (typeof value !== 'string' ||
|
|
649
|
+
value.length === 0 ||
|
|
650
|
+
value.length % 4 !== 0 ||
|
|
651
|
+
!BASE64_PATTERN.test(value)) {
|
|
652
|
+
throw createProtocolError(errorCode);
|
|
653
|
+
}
|
|
654
|
+
const padding = value.endsWith('==') ? 2 : (value.endsWith('=') ? 1 : 0);
|
|
655
|
+
const decodedLength = value.length / 4 * 3 - padding;
|
|
656
|
+
if (!Number.isSafeInteger(decodedLength) || decodedLength <= 0 || decodedLength > maxBytes) {
|
|
657
|
+
throw createProtocolError(decodedLength > maxBytes
|
|
658
|
+
? 'native-renderer-response-too-large'
|
|
659
|
+
: errorCode);
|
|
660
|
+
}
|
|
661
|
+
let binary;
|
|
662
|
+
try {
|
|
663
|
+
binary = window.atob(value);
|
|
664
|
+
} catch {
|
|
665
|
+
throw createProtocolError(errorCode);
|
|
666
|
+
}
|
|
667
|
+
if (binary.length !== decodedLength) {
|
|
668
|
+
throw createProtocolError(errorCode);
|
|
669
|
+
}
|
|
670
|
+
const bytes = new Uint8Array(decodedLength);
|
|
671
|
+
for (let index = 0; index < decodedLength; index += 1) {
|
|
672
|
+
bytes[index] = binary.charCodeAt(index);
|
|
673
|
+
}
|
|
674
|
+
if (bytesToBase64(bytes) !== value) {
|
|
675
|
+
throw createProtocolError(errorCode);
|
|
676
|
+
}
|
|
677
|
+
return bytes;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function decodeErrorReason(value) {
|
|
681
|
+
const bytes = base64ToBytes(value, MAX_REASON_BYTES, 'native-renderer-error-reason-invalid');
|
|
682
|
+
let reason;
|
|
683
|
+
try {
|
|
684
|
+
reason = textDecoder.decode(bytes);
|
|
685
|
+
} catch {
|
|
686
|
+
throw createProtocolError('native-renderer-error-reason-invalid');
|
|
687
|
+
}
|
|
688
|
+
if (!reason || reason.includes('\0')) {
|
|
689
|
+
throw createProtocolError('native-renderer-error-reason-invalid');
|
|
690
|
+
}
|
|
691
|
+
return reason;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function parseCanonicalPositiveDecimal(value, name) {
|
|
695
|
+
if (typeof value !== 'string' || !REQUEST_ID_PATTERN.test(value)) {
|
|
696
|
+
throw createProtocolError(`native-renderer-invalid-${name}`);
|
|
697
|
+
}
|
|
698
|
+
const number = Number(value);
|
|
699
|
+
if (!Number.isSafeInteger(number) || number <= 0 || String(number) !== value) {
|
|
700
|
+
throw createProtocolError(`native-renderer-invalid-${name}`);
|
|
701
|
+
}
|
|
702
|
+
return number;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function allocateRequestId() {
|
|
706
|
+
if (!Number.isSafeInteger(nextRequestId) ||
|
|
707
|
+
nextRequestId <= 0 ||
|
|
708
|
+
nextRequestId > Number.MAX_SAFE_INTEGER) {
|
|
709
|
+
throw createProtocolError('native-renderer-request-id-exhausted');
|
|
710
|
+
}
|
|
711
|
+
const requestId = String(nextRequestId);
|
|
712
|
+
nextRequestId += 1;
|
|
713
|
+
return requestId;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function currentStatus(extra = null) {
|
|
717
|
+
const snapshot = {
|
|
718
|
+
...status,
|
|
719
|
+
desktopStage: 'hybrid-compare',
|
|
720
|
+
requestedCompareLayer,
|
|
721
|
+
compareLayer: effectiveCompareLayer
|
|
722
|
+
};
|
|
723
|
+
return extra ? { ...snapshot, ...extra } : snapshot;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function notifyStatus() {
|
|
727
|
+
const snapshot = currentStatus();
|
|
728
|
+
for (const callback of [...statusListeners]) {
|
|
729
|
+
try {
|
|
730
|
+
callback(snapshot);
|
|
731
|
+
} catch {
|
|
732
|
+
// A consumer callback must not break host message correlation.
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function writeCompareLayer(value) {
|
|
738
|
+
effectiveCompareLayer = value;
|
|
739
|
+
if (root?.dataset) {
|
|
740
|
+
root.dataset.mindexecNativeCompareLayer = value;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function applyRequestedCompareLayer() {
|
|
745
|
+
writeCompareLayer(status.ready && hasAcceptedSnapshot &&
|
|
746
|
+
!viewportAcknowledgementRequired
|
|
747
|
+
? requestedCompareLayer
|
|
748
|
+
: 'web');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function requireWebFallback() {
|
|
752
|
+
hasAcceptedSnapshot = false;
|
|
753
|
+
acceptedSnapshotOwner = null;
|
|
754
|
+
viewportAcknowledgementRequired = false;
|
|
755
|
+
writeCompareLayer('web');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function sameSceneOwner(left, right) {
|
|
759
|
+
return left?.boardId === right?.boardId &&
|
|
760
|
+
left?.engineGeneration === right?.engineGeneration &&
|
|
761
|
+
left?.boardEpoch === right?.boardEpoch &&
|
|
762
|
+
left?.boardRevision === right?.boardRevision;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function retainedSceneOwner(owner) {
|
|
766
|
+
if (!owner) return null;
|
|
767
|
+
return {
|
|
768
|
+
boardId: owner.boardId,
|
|
769
|
+
engineGeneration: owner.engineGeneration.toString(),
|
|
770
|
+
boardEpoch: owner.boardEpoch.toString(),
|
|
771
|
+
boardRevision: owner.boardRevision.toString()
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function requireCameraAcknowledgement() {
|
|
776
|
+
if (!hasAcceptedSnapshot) {
|
|
777
|
+
requireWebFallback();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
viewportAcknowledgementRequired = true;
|
|
781
|
+
writeCompareLayer('web');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function handleViewportResize() {
|
|
785
|
+
if (disposed) return;
|
|
786
|
+
viewportEpoch = viewportEpoch >= Number.MAX_SAFE_INTEGER
|
|
787
|
+
? 1
|
|
788
|
+
: viewportEpoch + 1;
|
|
789
|
+
viewportAcknowledgementRequired = true;
|
|
790
|
+
writeCompareLayer('web');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function setCompareLayer(value) {
|
|
794
|
+
if (value !== 'web' && value !== 'overlay' && value !== 'native') {
|
|
795
|
+
throw createProtocolError('native-renderer-invalid-compare-layer');
|
|
796
|
+
}
|
|
797
|
+
requestedCompareLayer = value;
|
|
798
|
+
applyRequestedCompareLayer();
|
|
799
|
+
return effectiveCompareLayer;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function countsAsFrame(requestType) {
|
|
803
|
+
return requestType === REQUEST_TYPE.snapshot || requestType === REQUEST_TYPE.camera;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function takePending(requestId) {
|
|
807
|
+
const entry = pending.get(requestId);
|
|
808
|
+
if (!entry) return null;
|
|
809
|
+
pending.delete(requestId);
|
|
810
|
+
window.clearTimeout(entry.timer);
|
|
811
|
+
return entry;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function markFrameFailure(requestType, reason, { notify = true } = {}) {
|
|
815
|
+
if (countsAsFrame(requestType)) {
|
|
816
|
+
status.rejectedFrames += 1;
|
|
817
|
+
}
|
|
818
|
+
status.lastError = String(reason || 'native-renderer-rejected').slice(0, MAX_REASON_BYTES);
|
|
819
|
+
if (requestType === REQUEST_TYPE.camera) {
|
|
820
|
+
requireCameraAcknowledgement();
|
|
821
|
+
} else {
|
|
822
|
+
requireWebFallback();
|
|
823
|
+
}
|
|
824
|
+
if (notify) notifyStatus();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function settleFrameFailure(entry, reason, options = {}) {
|
|
828
|
+
const attachmentRetained = entry.requestType === REQUEST_TYPE.camera &&
|
|
829
|
+
hasAcceptedSnapshot &&
|
|
830
|
+
sameSceneOwner(acceptedSnapshotOwner, entry.owner);
|
|
831
|
+
markFrameFailure(entry.requestType, reason, options);
|
|
832
|
+
entry.resolve(currentStatus({
|
|
833
|
+
accepted: false,
|
|
834
|
+
reason,
|
|
835
|
+
attachmentRetained,
|
|
836
|
+
cameraRetryAllowed: attachmentRetained,
|
|
837
|
+
retainedSceneOwner: attachmentRetained
|
|
838
|
+
? retainedSceneOwner(acceptedSnapshotOwner)
|
|
839
|
+
: null
|
|
840
|
+
}));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function invalidateStatus(reason, entry = null) {
|
|
844
|
+
statusChecked = true;
|
|
845
|
+
status.ready = false;
|
|
846
|
+
status.backend = '';
|
|
847
|
+
status.lastError = String(reason || 'native-renderer-not-ready').slice(0, MAX_REASON_BYTES);
|
|
848
|
+
sessionToken = '';
|
|
849
|
+
requireWebFallback();
|
|
850
|
+
|
|
851
|
+
for (const [requestId, candidate] of [...pending]) {
|
|
852
|
+
if (candidate === entry || candidate.kind !== 'frame') continue;
|
|
853
|
+
takePending(requestId);
|
|
854
|
+
settleFrameFailure(candidate, status.lastError, { notify: false });
|
|
855
|
+
}
|
|
856
|
+
notifyStatus();
|
|
857
|
+
entry?.resolve(currentStatus());
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function rejectBeforeSend(requestType, reason) {
|
|
861
|
+
markFrameFailure(requestType, reason);
|
|
862
|
+
return currentStatus({ accepted: false, reason });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function onRequestTimeout(requestId) {
|
|
866
|
+
const entry = takePending(requestId);
|
|
867
|
+
if (!entry) return;
|
|
868
|
+
const reason = 'native-renderer-response-timeout';
|
|
869
|
+
if (entry.kind === 'status') {
|
|
870
|
+
invalidateStatus(reason, entry);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
settleFrameFailure(entry, reason);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function registerPending(requestId, entry) {
|
|
877
|
+
const timer = window.setTimeout(() => onRequestTimeout(requestId), RESPONSE_TIMEOUT_MS);
|
|
878
|
+
timer?.unref?.();
|
|
879
|
+
pending.set(requestId, { ...entry, timer });
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function postString(message, requestId) {
|
|
883
|
+
try {
|
|
884
|
+
webview.postMessage(message);
|
|
885
|
+
return true;
|
|
886
|
+
} catch (error) {
|
|
887
|
+
const entry = takePending(requestId);
|
|
888
|
+
if (!entry) return false;
|
|
889
|
+
const reason = `native-renderer-webmessage-post-failed:${String(error?.message || error)}`
|
|
890
|
+
.slice(0, MAX_REASON_BYTES);
|
|
891
|
+
if (entry.kind === 'status') {
|
|
892
|
+
invalidateStatus(reason, entry);
|
|
893
|
+
} else {
|
|
894
|
+
settleFrameFailure(entry, reason);
|
|
895
|
+
}
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function getStatus() {
|
|
901
|
+
if (disposed) {
|
|
902
|
+
return Promise.resolve(currentStatus({
|
|
903
|
+
ready: false,
|
|
904
|
+
lastError: 'native-renderer-bridge-disposed'
|
|
905
|
+
}));
|
|
906
|
+
}
|
|
907
|
+
if (pending.size >= MAX_PENDING_REQUESTS) {
|
|
908
|
+
return Promise.resolve(currentStatus({ lastError: 'native-renderer-pending-limit' }));
|
|
909
|
+
}
|
|
910
|
+
let requestId;
|
|
911
|
+
try {
|
|
912
|
+
requestId = allocateRequestId();
|
|
913
|
+
} catch (error) {
|
|
914
|
+
return Promise.resolve(currentStatus({ ready: false, lastError: error.message }));
|
|
915
|
+
}
|
|
916
|
+
return new Promise((resolve) => {
|
|
917
|
+
registerPending(requestId, { kind: 'status', resolve });
|
|
918
|
+
postString(`${WIRE_PREFIX}|${requestId}|status`, requestId);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function hasDuplicatePending(requestType, owner) {
|
|
923
|
+
for (const entry of pending.values()) {
|
|
924
|
+
if (entry.kind === 'frame' &&
|
|
925
|
+
entry.requestType === requestType &&
|
|
926
|
+
ownersEqual(entry.owner, owner)) {
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async function sendOwned(requestType, ownerValue, encode) {
|
|
934
|
+
if (disposed) {
|
|
935
|
+
return rejectBeforeSend(requestType, 'native-renderer-bridge-disposed');
|
|
936
|
+
}
|
|
937
|
+
if (!statusChecked || !sessionToken) {
|
|
938
|
+
return rejectBeforeSend(requestType, 'native-renderer-status-required');
|
|
939
|
+
}
|
|
940
|
+
if (!status.ready) {
|
|
941
|
+
return rejectBeforeSend(requestType, status.lastError || 'native-renderer-not-ready');
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
let owner;
|
|
945
|
+
try {
|
|
946
|
+
owner = normalizeOwner(ownerValue);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
return rejectBeforeSend(requestType, error?.message || String(error));
|
|
949
|
+
}
|
|
950
|
+
if (owner.engineGeneration !== BigInt(status.engineGeneration)) {
|
|
951
|
+
return rejectBeforeSend(requestType, 'native-renderer-engine-generation-mismatch');
|
|
952
|
+
}
|
|
953
|
+
if (requestType === REQUEST_TYPE.detach) {
|
|
954
|
+
// Board A must disappear before board B can begin attaching. Waiting
|
|
955
|
+
// for the detach acknowledgement could expose the retired native shell.
|
|
956
|
+
requireWebFallback();
|
|
957
|
+
}
|
|
958
|
+
if (pending.size >= MAX_PENDING_REQUESTS) {
|
|
959
|
+
return rejectBeforeSend(requestType, 'native-renderer-pending-limit');
|
|
960
|
+
}
|
|
961
|
+
if (hasDuplicatePending(requestType, owner)) {
|
|
962
|
+
return rejectBeforeSend(requestType, 'native-renderer-request-duplicate');
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const requestViewportEpoch = viewportEpoch;
|
|
966
|
+
let frame;
|
|
967
|
+
try {
|
|
968
|
+
frame = encode(owner);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
return rejectBeforeSend(requestType, error?.message || String(error));
|
|
971
|
+
}
|
|
972
|
+
if (!(frame instanceof Uint8Array) || frame.length > MAX_BINARY_BYTES) {
|
|
973
|
+
return rejectBeforeSend(requestType, 'native-renderer-payload-too-large');
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
let requestId;
|
|
977
|
+
try {
|
|
978
|
+
requestId = allocateRequestId();
|
|
979
|
+
} catch (error) {
|
|
980
|
+
return rejectBeforeSend(requestType, error?.message || String(error));
|
|
981
|
+
}
|
|
982
|
+
const token = sessionToken;
|
|
983
|
+
return new Promise((resolve) => {
|
|
984
|
+
registerPending(requestId, {
|
|
985
|
+
kind: 'frame',
|
|
986
|
+
requestType,
|
|
987
|
+
owner,
|
|
988
|
+
sessionToken: token,
|
|
989
|
+
viewportEpoch: requestViewportEpoch,
|
|
990
|
+
resolve
|
|
991
|
+
});
|
|
992
|
+
postString(
|
|
993
|
+
`${WIRE_PREFIX}|${requestId}|frame|${token}|${bytesToBase64(frame)}`,
|
|
994
|
+
requestId
|
|
995
|
+
);
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function sendSnapshot(scene) {
|
|
1000
|
+
return sendOwned(
|
|
1001
|
+
REQUEST_TYPE.snapshot,
|
|
1002
|
+
scene?.owner,
|
|
1003
|
+
(owner) => encodeSnapshotMessage(scene, owner)
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function sendCamera(frame) {
|
|
1008
|
+
return sendOwned(
|
|
1009
|
+
REQUEST_TYPE.camera,
|
|
1010
|
+
frame?.owner,
|
|
1011
|
+
(owner) => encodeCameraMessage(frame, owner)
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function detach(ownerValue) {
|
|
1016
|
+
return sendOwned(
|
|
1017
|
+
REQUEST_TYPE.detach,
|
|
1018
|
+
ownerValue,
|
|
1019
|
+
(owner) => encodeDetachMessage(owner)
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function onStatus(callback) {
|
|
1024
|
+
if (typeof callback !== 'function' || disposed) {
|
|
1025
|
+
return () => undefined;
|
|
1026
|
+
}
|
|
1027
|
+
statusListeners.add(callback);
|
|
1028
|
+
return () => statusListeners.delete(callback);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function settleMalformed(entry, error) {
|
|
1032
|
+
const reason = String(error?.message || error || 'native-renderer-response-malformed')
|
|
1033
|
+
.slice(0, MAX_REASON_BYTES);
|
|
1034
|
+
if (entry.kind === 'status') {
|
|
1035
|
+
invalidateStatus(reason, entry);
|
|
1036
|
+
} else {
|
|
1037
|
+
settleFrameFailure(entry, reason);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function retireOldSessionFrames(newToken) {
|
|
1042
|
+
for (const [requestId, entry] of [...pending]) {
|
|
1043
|
+
if (entry.kind !== 'frame' || entry.sessionToken === newToken) continue;
|
|
1044
|
+
takePending(requestId);
|
|
1045
|
+
settleFrameFailure(entry, 'native-renderer-session-changed', { notify: false });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function handleStatusResponse(fields, requestId, entry) {
|
|
1050
|
+
if (fields.length !== 6 || entry.kind !== 'status') {
|
|
1051
|
+
throw createProtocolError('native-renderer-status-envelope-invalid');
|
|
1052
|
+
}
|
|
1053
|
+
const generation = parseCanonicalPositiveDecimal(fields[3], 'engine-generation');
|
|
1054
|
+
const nextToken = fields[4];
|
|
1055
|
+
if (!SESSION_TOKEN_PATTERN.test(nextToken)) {
|
|
1056
|
+
throw createProtocolError('native-renderer-session-token-invalid');
|
|
1057
|
+
}
|
|
1058
|
+
const response = decodeResponseFrame(base64ToBytes(
|
|
1059
|
+
fields[5],
|
|
1060
|
+
MAX_BINARY_BYTES,
|
|
1061
|
+
'native-renderer-status-base64-invalid'
|
|
1062
|
+
));
|
|
1063
|
+
if (response.type !== 'ready') {
|
|
1064
|
+
throw createProtocolError('native-renderer-status-response-type-invalid');
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
takePending(requestId);
|
|
1068
|
+
const generationChanged = status.engineGeneration !== generation;
|
|
1069
|
+
const sessionChanged = sessionToken !== nextToken;
|
|
1070
|
+
if (generationChanged) {
|
|
1071
|
+
status.acceptedFrames = 0;
|
|
1072
|
+
status.rejectedFrames = 0;
|
|
1073
|
+
}
|
|
1074
|
+
if (generationChanged || sessionChanged) {
|
|
1075
|
+
requireWebFallback();
|
|
1076
|
+
retireOldSessionFrames(nextToken);
|
|
1077
|
+
}
|
|
1078
|
+
statusChecked = true;
|
|
1079
|
+
sessionToken = nextToken;
|
|
1080
|
+
status.ready = true;
|
|
1081
|
+
status.backend = response.backend;
|
|
1082
|
+
status.engineGeneration = generation;
|
|
1083
|
+
status.lastError = '';
|
|
1084
|
+
applyRequestedCompareLayer();
|
|
1085
|
+
notifyStatus();
|
|
1086
|
+
entry.resolve(currentStatus());
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function handleFrameResponse(fields, requestId, entry) {
|
|
1090
|
+
if (fields.length !== 5 || entry.kind !== 'frame') {
|
|
1091
|
+
throw createProtocolError('native-renderer-response-envelope-invalid');
|
|
1092
|
+
}
|
|
1093
|
+
const responseToken = fields[3];
|
|
1094
|
+
if (!SESSION_TOKEN_PATTERN.test(responseToken)) {
|
|
1095
|
+
throw createProtocolError('native-renderer-session-token-invalid');
|
|
1096
|
+
}
|
|
1097
|
+
if (responseToken !== sessionToken || responseToken !== entry.sessionToken) {
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
const response = decodeResponseFrame(base64ToBytes(
|
|
1101
|
+
fields[4],
|
|
1102
|
+
MAX_BINARY_BYTES,
|
|
1103
|
+
'native-renderer-response-base64-invalid'
|
|
1104
|
+
));
|
|
1105
|
+
if ((response.type !== 'accepted' && response.type !== 'rejected') ||
|
|
1106
|
+
response.requestType !== entry.requestType) {
|
|
1107
|
+
throw createProtocolError('native-renderer-response-request-type-mismatch');
|
|
1108
|
+
}
|
|
1109
|
+
if (!response.owner || !ownersEqual(response.owner, entry.owner)) {
|
|
1110
|
+
throw createProtocolError('native-renderer-response-owner-mismatch');
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
takePending(requestId);
|
|
1114
|
+
if (response.type === 'accepted') {
|
|
1115
|
+
if (countsAsFrame(entry.requestType)) {
|
|
1116
|
+
status.acceptedFrames += 1;
|
|
1117
|
+
}
|
|
1118
|
+
status.lastError = '';
|
|
1119
|
+
if (entry.requestType === REQUEST_TYPE.snapshot) {
|
|
1120
|
+
hasAcceptedSnapshot = true;
|
|
1121
|
+
acceptedSnapshotOwner = entry.owner;
|
|
1122
|
+
if (entry.viewportEpoch === viewportEpoch) {
|
|
1123
|
+
viewportAcknowledgementRequired = false;
|
|
1124
|
+
}
|
|
1125
|
+
applyRequestedCompareLayer();
|
|
1126
|
+
} else if (entry.requestType === REQUEST_TYPE.camera &&
|
|
1127
|
+
hasAcceptedSnapshot && entry.viewportEpoch === viewportEpoch) {
|
|
1128
|
+
viewportAcknowledgementRequired = false;
|
|
1129
|
+
applyRequestedCompareLayer();
|
|
1130
|
+
} else if (entry.requestType === REQUEST_TYPE.detach && !hasAcceptedSnapshot) {
|
|
1131
|
+
// A newer board snapshot may have completed while this older
|
|
1132
|
+
// detach acknowledgement was in flight. Never hide that newer surface.
|
|
1133
|
+
requireWebFallback();
|
|
1134
|
+
}
|
|
1135
|
+
notifyStatus();
|
|
1136
|
+
entry.resolve(currentStatus({ accepted: true }));
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
settleFrameFailure(entry, response.reason || 'native-renderer-rejected');
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function handleErrorResponse(fields, requestId, entry) {
|
|
1143
|
+
if (fields.length !== 5) {
|
|
1144
|
+
throw createProtocolError('native-renderer-error-envelope-invalid');
|
|
1145
|
+
}
|
|
1146
|
+
const responseToken = fields[3];
|
|
1147
|
+
if (responseToken !== '-' && !SESSION_TOKEN_PATTERN.test(responseToken)) {
|
|
1148
|
+
throw createProtocolError('native-renderer-session-token-invalid');
|
|
1149
|
+
}
|
|
1150
|
+
if (responseToken !== '-' &&
|
|
1151
|
+
(responseToken !== sessionToken ||
|
|
1152
|
+
(entry.kind === 'frame' && responseToken !== entry.sessionToken))) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const reason = decodeErrorReason(fields[4]);
|
|
1156
|
+
takePending(requestId);
|
|
1157
|
+
if (entry.kind === 'status') {
|
|
1158
|
+
invalidateStatus(reason, entry);
|
|
1159
|
+
} else {
|
|
1160
|
+
settleFrameFailure(entry, reason);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function handleWebMessage(event) {
|
|
1165
|
+
if (disposed || typeof event?.data !== 'string') return;
|
|
1166
|
+
const fields = event.data.split('|');
|
|
1167
|
+
if (fields[0] !== WIRE_PREFIX || fields.length < 3) return;
|
|
1168
|
+
|
|
1169
|
+
let requestId;
|
|
1170
|
+
try {
|
|
1171
|
+
parseCanonicalPositiveDecimal(fields[1], 'request-id');
|
|
1172
|
+
requestId = fields[1];
|
|
1173
|
+
} catch {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
const entry = pending.get(requestId);
|
|
1177
|
+
if (!entry) return;
|
|
1178
|
+
|
|
1179
|
+
try {
|
|
1180
|
+
if (fields[2] === 'status') {
|
|
1181
|
+
handleStatusResponse(fields, requestId, entry);
|
|
1182
|
+
} else if (fields[2] === 'response') {
|
|
1183
|
+
handleFrameResponse(fields, requestId, entry);
|
|
1184
|
+
} else if (fields[2] === 'error') {
|
|
1185
|
+
handleErrorResponse(fields, requestId, entry);
|
|
1186
|
+
} else {
|
|
1187
|
+
throw createProtocolError('native-renderer-response-envelope-invalid');
|
|
1188
|
+
}
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
const current = takePending(requestId);
|
|
1191
|
+
if (current) settleMalformed(current, error);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function disposeBridge() {
|
|
1196
|
+
if (disposed) return;
|
|
1197
|
+
disposed = true;
|
|
1198
|
+
statusChecked = false;
|
|
1199
|
+
sessionToken = '';
|
|
1200
|
+
status.ready = false;
|
|
1201
|
+
status.backend = '';
|
|
1202
|
+
status.lastError = 'native-renderer-bridge-disposed';
|
|
1203
|
+
requireWebFallback();
|
|
1204
|
+
try {
|
|
1205
|
+
webview.removeEventListener?.('message', handleWebMessage);
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1208
|
+
try {
|
|
1209
|
+
window.removeEventListener?.('pagehide', disposeBridge);
|
|
1210
|
+
} catch {
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
window.removeEventListener?.('resize', handleViewportResize);
|
|
1214
|
+
} catch {
|
|
1215
|
+
}
|
|
1216
|
+
try {
|
|
1217
|
+
visualViewportTarget?.removeEventListener?.('resize', handleViewportResize);
|
|
1218
|
+
} catch {
|
|
1219
|
+
}
|
|
1220
|
+
for (const [requestId, entry] of [...pending]) {
|
|
1221
|
+
takePending(requestId);
|
|
1222
|
+
if (entry.kind === 'status') {
|
|
1223
|
+
entry.resolve(currentStatus());
|
|
1224
|
+
} else {
|
|
1225
|
+
if (countsAsFrame(entry.requestType)) status.rejectedFrames += 1;
|
|
1226
|
+
entry.resolve(currentStatus({
|
|
1227
|
+
accepted: false,
|
|
1228
|
+
reason: 'native-renderer-bridge-disposed'
|
|
1229
|
+
}));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
notifyStatus();
|
|
1233
|
+
statusListeners.clear();
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const api = Object.freeze({
|
|
1237
|
+
BUILD_ID,
|
|
1238
|
+
getStatus,
|
|
1239
|
+
onStatus,
|
|
1240
|
+
sendSnapshot,
|
|
1241
|
+
sendCamera,
|
|
1242
|
+
detach,
|
|
1243
|
+
setCompareLayer,
|
|
1244
|
+
dispose: disposeBridge
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
const existingDesktop = window.mindExecDesktop;
|
|
1248
|
+
let installed = false;
|
|
1249
|
+
if (existingDesktop && (typeof existingDesktop === 'object' || typeof existingDesktop === 'function')) {
|
|
1250
|
+
try {
|
|
1251
|
+
existingDesktop.nativeRender = api;
|
|
1252
|
+
installed = existingDesktop.nativeRender === api;
|
|
1253
|
+
} catch {
|
|
1254
|
+
installed = false;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (!installed) {
|
|
1258
|
+
try {
|
|
1259
|
+
window.mindExecDesktop = Object.assign({}, existingDesktop || {}, { nativeRender: api });
|
|
1260
|
+
installed = window.mindExecDesktop?.nativeRender === api;
|
|
1261
|
+
} catch {
|
|
1262
|
+
installed = false;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (!installed) return;
|
|
1266
|
+
|
|
1267
|
+
if (root?.dataset) {
|
|
1268
|
+
root.dataset.mindexecNativeDesktopStage = 'hybrid-compare';
|
|
1269
|
+
}
|
|
1270
|
+
writeCompareLayer('web');
|
|
1271
|
+
webview.addEventListener('message', handleWebMessage);
|
|
1272
|
+
window.addEventListener?.('pagehide', disposeBridge, { once: true });
|
|
1273
|
+
window.addEventListener?.('resize', handleViewportResize);
|
|
1274
|
+
visualViewportTarget?.addEventListener?.('resize', handleViewportResize);
|
|
1275
|
+
})();
|