@floegence/redevplugin-ui 3.2.0 → 3.3.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/dist/openapi.gen.d.ts +17 -98
- package/dist/platform.d.ts +6 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/surface.d.ts +37 -143
- package/dist/surface.js +325 -2554
- package/dist/testing.d.ts +20 -34
- package/dist/testing.js +46 -12
- package/dist/trusted-parent.d.ts +2 -2
- package/dist/trusted-parent.js +1 -1
- package/package.json +4 -12
- package/dist/jsx-dev-runtime.d.ts +0 -2
- package/dist/jsx-dev-runtime.js +0 -1
- package/dist/jsx-runtime.d.ts +0 -27
- package/dist/jsx-runtime.js +0 -84
- package/dist/opaque-surface-policy.gen.d.ts +0 -47
- package/dist/opaque-surface-policy.gen.js +0 -233
- package/dist/surface-integrity.gen.d.ts +0 -1
- package/dist/surface-integrity.gen.js +0 -2
- package/dist/ui-patch-validator.d.ts +0 -67
- package/dist/ui-patch-validator.js +0 -128
- package/dist/ui-reconciler.d.ts +0 -10
- package/dist/ui-reconciler.js +0 -283
package/dist/surface.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2
2
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
3
|
-
import { surfaceIntegrityScript } from "./surface-integrity.gen.js";
|
|
4
3
|
import { PluginBridgeError, PluginPlatformRequestError, PluginTransportError, pluginBridgeErrorCodes, } from "./errors.js";
|
|
5
4
|
import { pluginBridgeCapabilityEffect, } from "./bridge-capability.js";
|
|
6
|
-
import { opaqueSurfaceAllowedTags, opaqueSurfaceCanvasContextTypes, opaqueSurfaceGlobalAttributes, opaqueSurfaceRenderLimits, opaqueSurfaceSafeInputTypes, opaqueSurfaceTagAttributes, } from "./opaque-surface-policy.gen.js";
|
|
7
5
|
import { defaultFetch, hasAllowedKeys, hasExactKeys, hasRequiredKeys, isRecord, readMutationPlatformResponse, readPlatformResponse, } from "./http.js";
|
|
8
6
|
import { defaultPluginSurfaceScope, registerPluginSurface, } from "./surface-scope.js";
|
|
9
|
-
|
|
10
|
-
export const opaqueSurfaceDocumentSchemaVersion = "redevplugin.opaque_surface_document.v3";
|
|
7
|
+
export const webSurfaceDocumentSchemaVersion = "redevplugin.web_surface.v1";
|
|
11
8
|
export const pluginRiskPlanSchemaVersion = "redevplugin.capability.risk_plan.v1";
|
|
12
9
|
export const pluginSurfaceContextSchemaVersion = "redevplugin.surface_context.v1";
|
|
13
10
|
export const pluginSurfaceFileExportMaxBytes = 16 * 1024 * 1024;
|
|
14
|
-
export const pluginSurfaceFileExportActionWindowMs = 30_000;
|
|
15
11
|
export const pluginSurfaceFileExportMediaTypes = [
|
|
16
12
|
"image/png",
|
|
17
13
|
"image/jpeg",
|
|
@@ -19,13 +15,13 @@ export const pluginSurfaceFileExportMediaTypes = [
|
|
|
19
15
|
"image/svg+xml",
|
|
20
16
|
"text/plain",
|
|
21
17
|
];
|
|
22
|
-
const
|
|
18
|
+
const nativeBridgeClock = {
|
|
19
|
+
schedule(callback, delayMs) { const timer = setTimeout(callback, delayMs); return () => clearTimeout(timer); },
|
|
20
|
+
};
|
|
21
|
+
const webPluginBridgeGlobalKey = "__redevpluginWebBridge";
|
|
23
22
|
const maxPendingPluginBridgeRequests = 256;
|
|
24
|
-
const maxPluginBridgeMessageBytes =
|
|
23
|
+
const maxPluginBridgeMessageBytes = 1024 * 1024;
|
|
25
24
|
const maxPluginJSONStructuralNodes = 32 * 1024;
|
|
26
|
-
const maxOpaqueSurfaceLazyAssets = 128;
|
|
27
|
-
const maxOpaqueSurfaceLazyBytes = 32 * 1024 * 1024;
|
|
28
|
-
const maxConcurrentAssetReads = 4;
|
|
29
25
|
const maxSurfaceQuiesceMs = 1500;
|
|
30
26
|
const maxSurfaceInteractionEventsPerSecond = 120;
|
|
31
27
|
const pluginBridgeErrorCodeSet = new Set(pluginBridgeErrorCodes);
|
|
@@ -36,12 +32,12 @@ export class PluginBridgeClient {
|
|
|
36
32
|
surfaceHandle;
|
|
37
33
|
timeoutMs;
|
|
38
34
|
#nextID = 1;
|
|
35
|
+
#clock;
|
|
39
36
|
#port;
|
|
40
37
|
#pending = new Map();
|
|
41
|
-
#actionHandlers = new Map();
|
|
42
|
-
#canvasInputHandlers = new Map();
|
|
43
|
-
#keyboardInputHandlers = new Set();
|
|
44
38
|
#lifecycleHandlers = new Set();
|
|
39
|
+
#beforeCloseHandlers = new Set();
|
|
40
|
+
#beforeClosePending = false;
|
|
45
41
|
#contextHandlers = new Set();
|
|
46
42
|
#context;
|
|
47
43
|
#ready = false;
|
|
@@ -50,20 +46,16 @@ export class PluginBridgeClient {
|
|
|
50
46
|
#rejectReady;
|
|
51
47
|
#lifecycleState = "active";
|
|
52
48
|
#handlingLifecycle = false;
|
|
53
|
-
#renderRevision = 0;
|
|
54
|
-
#committedTree;
|
|
55
|
-
#pendingRender;
|
|
56
|
-
#renderLoop;
|
|
57
|
-
#controlEditRevisions = new Map();
|
|
58
49
|
#onMessage = (event) => {
|
|
59
50
|
void this.#handleMessage(event);
|
|
60
51
|
};
|
|
61
52
|
constructor(options = {}) {
|
|
62
53
|
const claimed = options.port
|
|
63
54
|
? { port: options.port, surfaceHandle: normalizeSurfaceHandle(options.surfaceHandle) }
|
|
64
|
-
:
|
|
55
|
+
: claimWebPluginBridge();
|
|
65
56
|
this.surfaceHandle = claimed.surfaceHandle;
|
|
66
57
|
this.timeoutMs = normalizeTimeout(options.timeoutMs);
|
|
58
|
+
this.#clock = options.clock ?? nativeBridgeClock;
|
|
67
59
|
this.#port = claimed.port;
|
|
68
60
|
this.#readyPromise = new Promise((resolve, reject) => {
|
|
69
61
|
this.#resolveReady = resolve;
|
|
@@ -72,6 +64,7 @@ export class PluginBridgeClient {
|
|
|
72
64
|
void this.#readyPromise.catch(() => undefined);
|
|
73
65
|
this.#port.addEventListener("message", this.#onMessage);
|
|
74
66
|
this.#port.start();
|
|
67
|
+
this.#port.postMessage({ type: "redevplugin.bridge.client_ready" });
|
|
75
68
|
}
|
|
76
69
|
ready() {
|
|
77
70
|
this.#assertActive();
|
|
@@ -165,91 +158,6 @@ export class PluginBridgeClient {
|
|
|
165
158
|
}
|
|
166
159
|
return () => this.#contextHandlers.delete(handler);
|
|
167
160
|
}
|
|
168
|
-
render(tree) {
|
|
169
|
-
this.#assertActive();
|
|
170
|
-
let validated;
|
|
171
|
-
try {
|
|
172
|
-
validated = validatePluginUITree(tree);
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
throw new PluginBridgeError("PLUGIN_UI_PROTOCOL_VIOLATION", error instanceof Error ? error.message : "Plugin UI tree is invalid");
|
|
176
|
-
}
|
|
177
|
-
return new Promise((resolve, reject) => {
|
|
178
|
-
const waiters = this.#pendingRender?.waiters ?? [];
|
|
179
|
-
waiters.push({ resolve, reject });
|
|
180
|
-
this.#pendingRender = { tree: validated, waiters };
|
|
181
|
-
this.#startRenderLoop();
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
openCanvas(canvasId) {
|
|
185
|
-
this.#assertActive();
|
|
186
|
-
if (!validUIIdentifier(canvasId)) {
|
|
187
|
-
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin canvas identifier is invalid");
|
|
188
|
-
}
|
|
189
|
-
const id = this.#requestID("canvas");
|
|
190
|
-
return this.#request(id, {
|
|
191
|
-
type: "redevplugin.ui.canvas.open",
|
|
192
|
-
id,
|
|
193
|
-
canvas_id: canvasId,
|
|
194
|
-
}, { kind: "canvas", identifier: canvasId });
|
|
195
|
-
}
|
|
196
|
-
updateCanvasAccessibility(canvasId, state) {
|
|
197
|
-
this.#assertActive();
|
|
198
|
-
if (!validUIIdentifier(canvasId) || !validCanvasAccessibilityState(state)) {
|
|
199
|
-
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin canvas accessibility state is invalid");
|
|
200
|
-
}
|
|
201
|
-
const id = this.#requestID("canvas");
|
|
202
|
-
return this.#request(id, {
|
|
203
|
-
type: "redevplugin.ui.canvas.accessibility",
|
|
204
|
-
id,
|
|
205
|
-
canvas_id: canvasId,
|
|
206
|
-
label: state.label,
|
|
207
|
-
description: state.description,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
setKeyboardBindings(bindings) {
|
|
211
|
-
this.#assertActive();
|
|
212
|
-
if (!validKeyboardBindings(bindings)) {
|
|
213
|
-
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin surface keyboard bindings are invalid");
|
|
214
|
-
}
|
|
215
|
-
const id = this.#requestID("keyboard");
|
|
216
|
-
return this.#request(id, {
|
|
217
|
-
type: "redevplugin.ui.keyboard.bindings",
|
|
218
|
-
id,
|
|
219
|
-
bindings: bindings.map((binding) => ({
|
|
220
|
-
binding_id: binding.id,
|
|
221
|
-
event: binding.event,
|
|
222
|
-
code: binding.code,
|
|
223
|
-
repeat: binding.repeat,
|
|
224
|
-
alt_key: binding.altKey,
|
|
225
|
-
ctrl_key: binding.ctrlKey,
|
|
226
|
-
meta_key: binding.metaKey,
|
|
227
|
-
shift_key: binding.shiftKey,
|
|
228
|
-
target_key: binding.targetKey,
|
|
229
|
-
target_kind: binding.targetKind,
|
|
230
|
-
})),
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
onKeyboardInput(handler) {
|
|
234
|
-
this.#assertActive();
|
|
235
|
-
if (typeof handler !== "function") {
|
|
236
|
-
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin surface keyboard input subscription is invalid");
|
|
237
|
-
}
|
|
238
|
-
this.#keyboardInputHandlers.add(handler);
|
|
239
|
-
return () => this.#keyboardInputHandlers.delete(handler);
|
|
240
|
-
}
|
|
241
|
-
loadImageAsset(assetId) {
|
|
242
|
-
this.#assertActive();
|
|
243
|
-
if (!validUIIdentifier(assetId)) {
|
|
244
|
-
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin image asset identifier is invalid");
|
|
245
|
-
}
|
|
246
|
-
const id = this.#requestID("asset");
|
|
247
|
-
return this.#request(id, {
|
|
248
|
-
type: "redevplugin.ui.asset.image.open",
|
|
249
|
-
id,
|
|
250
|
-
asset_id: assetId,
|
|
251
|
-
}, { kind: "asset", identifier: assetId });
|
|
252
|
-
}
|
|
253
161
|
exportFile(request, options = {}) {
|
|
254
162
|
this.#assertActive();
|
|
255
163
|
if (!validFileExportName(request?.fileName) || !validFileExportMediaType(request?.mediaType) ||
|
|
@@ -267,33 +175,14 @@ export class PluginBridgeClient {
|
|
|
267
175
|
content,
|
|
268
176
|
}, { mutation: true, signal: options.signal }, [content]);
|
|
269
177
|
}
|
|
270
|
-
|
|
178
|
+
/** Return false when saving failed or the application still has unsaved work. */
|
|
179
|
+
onBeforeClose(handler) {
|
|
271
180
|
this.#assertActive();
|
|
272
|
-
if (
|
|
273
|
-
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "
|
|
181
|
+
if (typeof handler !== "function" || this.#beforeCloseHandlers.size >= 32) {
|
|
182
|
+
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Application close handler is invalid or exceeds the limit");
|
|
274
183
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.#actionHandlers.set(action, handlers);
|
|
278
|
-
return () => {
|
|
279
|
-
handlers.delete(handler);
|
|
280
|
-
if (handlers.size === 0)
|
|
281
|
-
this.#actionHandlers.delete(action);
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
onCanvasInput(canvasId, handler) {
|
|
285
|
-
this.#assertActive();
|
|
286
|
-
if (!validUIIdentifier(canvasId) || typeof handler !== "function") {
|
|
287
|
-
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin canvas input subscription is invalid");
|
|
288
|
-
}
|
|
289
|
-
const handlers = this.#canvasInputHandlers.get(canvasId) ?? new Set();
|
|
290
|
-
handlers.add(handler);
|
|
291
|
-
this.#canvasInputHandlers.set(canvasId, handlers);
|
|
292
|
-
return () => {
|
|
293
|
-
handlers.delete(handler);
|
|
294
|
-
if (handlers.size === 0)
|
|
295
|
-
this.#canvasInputHandlers.delete(canvasId);
|
|
296
|
-
};
|
|
184
|
+
this.#beforeCloseHandlers.add(handler);
|
|
185
|
+
return () => this.#beforeCloseHandlers.delete(handler);
|
|
297
186
|
}
|
|
298
187
|
onLifecycle(handler) {
|
|
299
188
|
this.#assertActive();
|
|
@@ -311,90 +200,17 @@ export class PluginBridgeClient {
|
|
|
311
200
|
this.#rejectReady(disposedError);
|
|
312
201
|
this.#port.removeEventListener("message", this.#onMessage);
|
|
313
202
|
for (const [id, pending] of this.#pending) {
|
|
314
|
-
|
|
203
|
+
pending.cancelTimer();
|
|
315
204
|
pending.abortCleanup?.();
|
|
316
205
|
pending.reject(new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", `Plugin bridge request ${id} was disposed`));
|
|
317
206
|
}
|
|
318
207
|
this.#pending.clear();
|
|
319
|
-
this.#actionHandlers.clear();
|
|
320
|
-
this.#canvasInputHandlers.clear();
|
|
321
|
-
this.#keyboardInputHandlers.clear();
|
|
322
208
|
this.#lifecycleHandlers.clear();
|
|
323
209
|
this.#contextHandlers.clear();
|
|
210
|
+
this.#beforeCloseHandlers.clear();
|
|
324
211
|
this.#context = undefined;
|
|
325
|
-
for (const waiter of this.#pendingRender?.waiters ?? [])
|
|
326
|
-
waiter.reject(disposedError);
|
|
327
|
-
this.#pendingRender = undefined;
|
|
328
|
-
this.#controlEditRevisions.clear();
|
|
329
212
|
this.#port.close();
|
|
330
213
|
}
|
|
331
|
-
async #drainRenders() {
|
|
332
|
-
while (this.#pendingRender && this.#acceptsPluginWork()) {
|
|
333
|
-
const pending = this.#pendingRender;
|
|
334
|
-
this.#pendingRender = undefined;
|
|
335
|
-
const id = this.#requestID("render");
|
|
336
|
-
const nextRevision = this.#renderRevision + 1;
|
|
337
|
-
try {
|
|
338
|
-
if (!this.#committedTree) {
|
|
339
|
-
await this.#request(id, {
|
|
340
|
-
type: "redevplugin.ui.mount",
|
|
341
|
-
id,
|
|
342
|
-
revision: 1,
|
|
343
|
-
tree: pending.tree,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
const operations = reconcileValidatedPluginUITrees(this.#committedTree, pending.tree, {
|
|
348
|
-
controlEditRevisions: this.#controlEditRevisions,
|
|
349
|
-
});
|
|
350
|
-
if (operations.length > 0) {
|
|
351
|
-
await this.#request(id, {
|
|
352
|
-
type: "redevplugin.ui.patch",
|
|
353
|
-
id,
|
|
354
|
-
base_revision: this.#renderRevision,
|
|
355
|
-
revision: nextRevision,
|
|
356
|
-
operations,
|
|
357
|
-
});
|
|
358
|
-
this.#renderRevision = nextRevision;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
this.#committedTree = pending.tree;
|
|
362
|
-
if (this.#renderRevision === 0)
|
|
363
|
-
this.#renderRevision = 1;
|
|
364
|
-
for (const waiter of pending.waiters)
|
|
365
|
-
waiter.resolve();
|
|
366
|
-
}
|
|
367
|
-
catch (error) {
|
|
368
|
-
const bridgeError = error instanceof PluginUIReconcileError
|
|
369
|
-
? new PluginBridgeError("PLUGIN_UI_PROTOCOL_VIOLATION", error.message)
|
|
370
|
-
: error;
|
|
371
|
-
for (const waiter of pending.waiters)
|
|
372
|
-
waiter.reject(bridgeError);
|
|
373
|
-
const queued = this.#pendingRender;
|
|
374
|
-
for (const waiter of queued?.waiters ?? [])
|
|
375
|
-
waiter.reject(bridgeError);
|
|
376
|
-
this.#pendingRender = undefined;
|
|
377
|
-
throw bridgeError;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
if (this.#pendingRender) {
|
|
381
|
-
const error = new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", "Plugin UI cannot render after quiescing starts");
|
|
382
|
-
for (const waiter of this.#pendingRender.waiters)
|
|
383
|
-
waiter.reject(error);
|
|
384
|
-
this.#pendingRender = undefined;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
#startRenderLoop() {
|
|
388
|
-
if (this.#renderLoop)
|
|
389
|
-
return;
|
|
390
|
-
this.#renderLoop = this.#drainRenders()
|
|
391
|
-
.catch(() => undefined)
|
|
392
|
-
.finally(() => {
|
|
393
|
-
this.#renderLoop = undefined;
|
|
394
|
-
if (this.#pendingRender)
|
|
395
|
-
this.#startRenderLoop();
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
214
|
#request(id, message, options = {}) {
|
|
399
215
|
let normalizedMessage;
|
|
400
216
|
try {
|
|
@@ -416,8 +232,7 @@ export class PluginBridgeClient {
|
|
|
416
232
|
return Promise.reject(requestCancelledError(options, false));
|
|
417
233
|
let posted = false;
|
|
418
234
|
const result = new Promise((resolve, reject) => {
|
|
419
|
-
|
|
420
|
-
const timer = options.kind === "canvas" ? undefined : setTimeout(() => {
|
|
235
|
+
const cancelTimer = this.#clock.schedule(() => {
|
|
421
236
|
const pending = this.#takePending(id);
|
|
422
237
|
if (!pending)
|
|
423
238
|
return;
|
|
@@ -446,7 +261,7 @@ export class PluginBridgeClient {
|
|
|
446
261
|
this.#pending.set(id, {
|
|
447
262
|
resolve: (value) => resolve(value),
|
|
448
263
|
reject,
|
|
449
|
-
|
|
264
|
+
cancelTimer,
|
|
450
265
|
abortCleanup: options.signal ? () => options.signal?.removeEventListener("abort", onAbort) : undefined,
|
|
451
266
|
kind: options.kind ?? "json",
|
|
452
267
|
identifier: options.identifier,
|
|
@@ -474,7 +289,7 @@ export class PluginBridgeClient {
|
|
|
474
289
|
if (!pending)
|
|
475
290
|
return undefined;
|
|
476
291
|
this.#pending.delete(id);
|
|
477
|
-
|
|
292
|
+
pending.cancelTimer();
|
|
478
293
|
pending.abortCleanup?.();
|
|
479
294
|
return pending;
|
|
480
295
|
}
|
|
@@ -482,34 +297,6 @@ export class PluginBridgeClient {
|
|
|
482
297
|
if (this.#lifecycleState === "disposed")
|
|
483
298
|
return;
|
|
484
299
|
const data = event.data;
|
|
485
|
-
if (isCanvasReadyCandidate(data)) {
|
|
486
|
-
const pending = this.#takePending(data.id);
|
|
487
|
-
if (!pending)
|
|
488
|
-
return;
|
|
489
|
-
if (pending.kind !== "canvas" || pending.identifier !== data.canvas_id || !isCanvasReadyMessage(data)) {
|
|
490
|
-
pending.reject(new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", `Plugin canvas response ${data.id} is invalid`));
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
pending.resolve({
|
|
494
|
-
canvas: data.canvas,
|
|
495
|
-
canvasId: data.canvas_id,
|
|
496
|
-
cssWidth: data.css_width,
|
|
497
|
-
cssHeight: data.css_height,
|
|
498
|
-
devicePixelRatio: data.device_pixel_ratio,
|
|
499
|
-
});
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
if (isImageReadyCandidate(data)) {
|
|
503
|
-
const pending = this.#takePending(data.id);
|
|
504
|
-
if (!pending)
|
|
505
|
-
return;
|
|
506
|
-
if (pending.kind !== "asset" || pending.identifier !== data.asset_id || !isImageReadyMessage(data)) {
|
|
507
|
-
pending.reject(new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", `Plugin image response ${data.id} is invalid`));
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
pending.resolve(data.image);
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
300
|
if (!messageWithinLimit(data))
|
|
514
301
|
return;
|
|
515
302
|
if (isBridgeResponseCandidate(data)) {
|
|
@@ -529,6 +316,19 @@ export class PluginBridgeClient {
|
|
|
529
316
|
pending.reject(new PluginBridgeError(data.error_code, data.error, undefined, data.error_details, data.mutation_outcome));
|
|
530
317
|
return;
|
|
531
318
|
}
|
|
319
|
+
if (hasExactKeys(data, ["type", "close_id"]) && data.type === "redevplugin.bridge.close_request" && validOpaqueHandle(data.close_id, "close")) {
|
|
320
|
+
if (this.#beforeClosePending)
|
|
321
|
+
return;
|
|
322
|
+
this.#beforeClosePending = true;
|
|
323
|
+
const results = await Promise.allSettled(Array.from(this.#beforeCloseHandlers, handler => withTimeout(Promise.resolve().then(handler), Math.min(this.timeoutMs, maxSurfaceQuiesceMs), "Application close handler timed out")));
|
|
324
|
+
this.#beforeClosePending = false;
|
|
325
|
+
if (this.#lifecycleState === "active")
|
|
326
|
+
this.#port.postMessage({
|
|
327
|
+
type: "redevplugin.bridge.close_response", close_id: data.close_id,
|
|
328
|
+
allow: results.every(result => result.status === "fulfilled" && result.value === true),
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
532
332
|
if (isPluginBridgeContextMessage(data)) {
|
|
533
333
|
const context = normalizePluginSurfaceContext(data.context);
|
|
534
334
|
if (this.#context && context.revision <= this.#context.revision)
|
|
@@ -572,24 +372,6 @@ export class PluginBridgeClient {
|
|
|
572
372
|
this.dispose();
|
|
573
373
|
return;
|
|
574
374
|
}
|
|
575
|
-
if (isActionMessage(data)) {
|
|
576
|
-
this.#controlEditRevisions.set(data.target_key, data.edit_revision);
|
|
577
|
-
const action = publicActionEvent(data);
|
|
578
|
-
for (const handler of this.#actionHandlers.get(data.action) ?? [])
|
|
579
|
-
handler(action);
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
const canvasInput = publicCanvasInputMessage(data);
|
|
583
|
-
if (canvasInput) {
|
|
584
|
-
for (const handler of this.#canvasInputHandlers.get(canvasInput.canvasId) ?? [])
|
|
585
|
-
handler(canvasInput.event);
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
const keyboardInput = publicKeyboardInputMessage(data);
|
|
589
|
-
if (keyboardInput) {
|
|
590
|
-
for (const handler of this.#keyboardInputHandlers)
|
|
591
|
-
handler(keyboardInput);
|
|
592
|
-
}
|
|
593
375
|
}
|
|
594
376
|
#requestID(prefix) {
|
|
595
377
|
return `${prefix}_${this.#nextID++}`;
|
|
@@ -773,1899 +555,33 @@ async function revokeSurfaceBootstrap(transport, bootstrap, timeoutMs, keepalive
|
|
|
773
555
|
clearTimeout(timer);
|
|
774
556
|
}
|
|
775
557
|
}
|
|
776
|
-
export function createOpaquePluginBootstrapHTML(options = {}) {
|
|
777
|
-
const scriptNonce = options.scriptNonce ?? randomOpaqueNonce();
|
|
778
|
-
if (!/^[A-Za-z0-9_-]{8,128}$/.test(scriptNonce)) {
|
|
779
|
-
throw new Error("scriptNonce must contain 8-128 URL-safe characters");
|
|
780
|
-
}
|
|
781
|
-
const csp = [
|
|
782
|
-
"default-src 'none'",
|
|
783
|
-
`script-src 'nonce-${scriptNonce}'`,
|
|
784
|
-
`style-src 'nonce-${scriptNonce}'`,
|
|
785
|
-
"img-src data: blob:",
|
|
786
|
-
"font-src data: blob:",
|
|
787
|
-
"media-src data: blob:",
|
|
788
|
-
"connect-src 'none'",
|
|
789
|
-
"frame-src 'none'",
|
|
790
|
-
"worker-src blob:",
|
|
791
|
-
"child-src blob:",
|
|
792
|
-
"form-action 'none'",
|
|
793
|
-
"base-uri 'none'",
|
|
794
|
-
"object-src 'none'",
|
|
795
|
-
"manifest-src 'none'",
|
|
796
|
-
].join("; ");
|
|
797
|
-
const bootstrapScript = `(() => {
|
|
798
|
-
"use strict";
|
|
799
|
-
${surfaceIntegrityScript}
|
|
800
|
-
const documentSchema = ${JSON.stringify(opaqueSurfaceDocumentSchemaVersion)};
|
|
801
|
-
const workerGlobalKey = ${JSON.stringify(opaquePluginBridgeGlobalKey)};
|
|
802
|
-
const scriptNonce = ${JSON.stringify(scriptNonce)};
|
|
803
|
-
const maxMessageBytes = ${opaqueSurfaceRenderLimits.max_message_bytes};
|
|
804
|
-
const maxInFlightRequests = ${opaqueSurfaceRenderLimits.max_in_flight_requests};
|
|
805
|
-
const maxRendersPerSecond = ${opaqueSurfaceRenderLimits.max_renders_per_second};
|
|
806
|
-
const maxRenderDepth = ${opaqueSurfaceRenderLimits.max_render_depth};
|
|
807
|
-
const maxRenderNodes = ${opaqueSurfaceRenderLimits.max_render_nodes};
|
|
808
|
-
const maxPatchOperations = ${opaqueSurfaceRenderLimits.max_patch_operations};
|
|
809
|
-
const maxAttributesPerElement = ${opaqueSurfaceRenderLimits.max_attributes_per_element};
|
|
810
|
-
const maxTextLength = ${opaqueSurfaceRenderLimits.max_text_length};
|
|
811
|
-
const maxAttributeValueLength = ${opaqueSurfaceRenderLimits.max_attribute_value_length};
|
|
812
|
-
const maxFormFields = ${opaqueSurfaceRenderLimits.max_form_fields};
|
|
813
|
-
const maxKeyboardBindings = ${opaqueSurfaceRenderLimits.max_keyboard_bindings};
|
|
814
|
-
const maxCanvasCount = ${opaqueSurfaceRenderLimits.max_canvas_count};
|
|
815
|
-
const maxCanvasDimension = ${opaqueSurfaceRenderLimits.max_canvas_dimension};
|
|
816
|
-
const maxCanvasTotalPixels = ${opaqueSurfaceRenderLimits.max_canvas_total_pixels};
|
|
817
|
-
const maxCanvasPointerEventsPerSecond = ${opaqueSurfaceRenderLimits.max_canvas_pointer_events_per_second};
|
|
818
|
-
const maxImageCount = ${opaqueSurfaceRenderLimits.max_image_count};
|
|
819
|
-
const maxImageDimension = ${opaqueSurfaceRenderLimits.max_image_dimension};
|
|
820
|
-
const maxImageTotalPixels = ${opaqueSurfaceRenderLimits.max_image_total_pixels};
|
|
821
|
-
const maxFileExportBytes = ${pluginSurfaceFileExportMaxBytes};
|
|
822
|
-
const fileExportActionWindowMs = ${pluginSurfaceFileExportActionWindowMs};
|
|
823
|
-
const fileExportMediaTypes = new Set(${JSON.stringify(pluginSurfaceFileExportMediaTypes)});
|
|
824
|
-
const workerHeartbeatIntervalMs = ${opaqueSurfaceRenderLimits.worker_heartbeat_interval_ms};
|
|
825
|
-
const workerHeartbeatTimeoutMs = ${opaqueSurfaceRenderLimits.worker_heartbeat_timeout_ms};
|
|
826
|
-
const rendererCommitFallbackMs = 100;
|
|
827
|
-
const maxCriticalDocumentBytes = ${8 * 1024 * 1024};
|
|
828
|
-
const maxPrivateAssetBase64Length = ${Math.ceil((32 * 1024 * 1024) / 3) * 4};
|
|
829
|
-
const maxLazyAssetCount = ${maxOpaqueSurfaceLazyAssets};
|
|
830
|
-
const maxLazyAssetBytes = ${maxOpaqueSurfaceLazyBytes};
|
|
831
|
-
const maxConcurrentAssetReads = ${maxConcurrentAssetReads};
|
|
832
|
-
const allowedTags = new Set(${JSON.stringify(opaqueSurfaceAllowedTags)});
|
|
833
|
-
const globalAttributes = new Set(${JSON.stringify(opaqueSurfaceGlobalAttributes)});
|
|
834
|
-
const tagAttributes = Object.freeze(Object.fromEntries(
|
|
835
|
-
Object.entries(${JSON.stringify(opaqueSurfaceTagAttributes)}).map(([tag, attributes]) => [tag, new Set(attributes)])
|
|
836
|
-
));
|
|
837
|
-
const safeInputTypes = new Set(${JSON.stringify(opaqueSurfaceSafeInputTypes)});
|
|
838
|
-
const exactKeys = (value, keys) => {
|
|
839
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
840
|
-
const actual = Object.keys(value).sort();
|
|
841
|
-
const expected = [...keys].sort();
|
|
842
|
-
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
843
|
-
};
|
|
844
|
-
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
845
|
-
const canonicalJSON = (value, depth = 0, state = { nodes: 0 }, seen = new Set()) => {
|
|
846
|
-
state.nodes += 1;
|
|
847
|
-
if (state.nodes > ${maxPluginJSONStructuralNodes} || depth > 64) return false;
|
|
848
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
849
|
-
if (typeof value === "number") return Number.isFinite(value);
|
|
850
|
-
if (typeof value !== "object" || seen.has(value)) return false;
|
|
851
|
-
seen.add(value);
|
|
852
|
-
if (Array.isArray(value)) {
|
|
853
|
-
const valid = value.every((item) => canonicalJSON(item, depth + 1, state, seen));
|
|
854
|
-
seen.delete(value);
|
|
855
|
-
return valid;
|
|
856
|
-
}
|
|
857
|
-
const prototype = Object.getPrototypeOf(value);
|
|
858
|
-
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
859
|
-
const keys = Reflect.ownKeys(value);
|
|
860
|
-
if (keys.some((key) => typeof key !== "string")) return false;
|
|
861
|
-
for (const key of keys) {
|
|
862
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
863
|
-
if (!descriptor || !descriptor.enumerable || !("value" in descriptor) || !canonicalJSON(descriptor.value, depth + 1, state, seen)) return false;
|
|
864
|
-
}
|
|
865
|
-
seen.delete(value);
|
|
866
|
-
return true;
|
|
867
|
-
};
|
|
868
|
-
const withinLimit = (value) => {
|
|
869
|
-
try { return canonicalJSON(value) && new TextEncoder().encode(JSON.stringify(value)).byteLength <= maxMessageBytes; }
|
|
870
|
-
catch { return false; }
|
|
871
|
-
};
|
|
872
|
-
const validIdentifier = (value) => typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value);
|
|
873
|
-
const validResourceIdentifier = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value);
|
|
874
|
-
const validOpaqueHandle = (value, prefix) => typeof value === "string" && value.startsWith(prefix + "_") && /^[A-Za-z0-9_-]{8,160}$/.test(value);
|
|
875
|
-
const validDigest = (value) => typeof value === "string" && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
876
|
-
const validPath = (value) => typeof value === "string" && value.length > 0 && value.length <= 512 && !value.startsWith("/") && !value.includes("\\\\") && !value.split("/").some((part) => !part || part === "." || part === "..");
|
|
877
|
-
const validFileExportName = (value) => {
|
|
878
|
-
if (typeof value !== "string" || value.length === 0 || value.length > 128 || value.trim() !== value || value === "." || value === ".." || new TextEncoder().encode(value).byteLength > 255) return false;
|
|
879
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
880
|
-
const code = value.charCodeAt(index);
|
|
881
|
-
if (code < 32 || code === 47 || code === 92 || code === 127) return false;
|
|
882
|
-
}
|
|
883
|
-
return true;
|
|
884
|
-
};
|
|
885
|
-
const validWorkerFileExport = (value) => exactKeys(value, ["type", "id", "file_name", "media_type", "content"]) &&
|
|
886
|
-
value.type === "redevplugin.ui.file.export" && typeof value.id === "string" && validFileExportName(value.file_name) &&
|
|
887
|
-
fileExportMediaTypes.has(value.media_type) && value.content instanceof ArrayBuffer &&
|
|
888
|
-
value.content.byteLength > 0 && value.content.byteLength <= maxFileExportBytes;
|
|
889
|
-
const validAttribute = (tag, name, value) => {
|
|
890
|
-
const lower = name.toLowerCase();
|
|
891
|
-
if (lower.startsWith("on") || lower === "style" || lower === "src" || lower === "srcset" || lower === "href" || lower === "srcdoc" || lower === "action" || lower === "formaction") return false;
|
|
892
|
-
if (!globalAttributes.has(lower) && !lower.startsWith("aria-") && !(tagAttributes[tag] && tagAttributes[tag].has(lower))) return false;
|
|
893
|
-
if (!["string", "number", "boolean"].includes(typeof value)) return false;
|
|
894
|
-
if (lower === "data-redevplugin-action" && !validIdentifier(String(value))) return false;
|
|
895
|
-
if (lower === "data-redevplugin-escape-action" && !validIdentifier(String(value))) return false;
|
|
896
|
-
if (lower === "data-redevplugin-asset-binding" && !validOpaqueHandle(String(value), "asset")) return false;
|
|
897
|
-
if (lower === "data-redevplugin-asset-attr" && !["src", "poster"].includes(String(value))) return false;
|
|
898
|
-
if (lower === "data-redevplugin-canvas" && (tag !== "canvas" || !validResourceIdentifier(String(value)))) return false;
|
|
899
|
-
if (lower === "maxlength" && (!/^(0|[1-9][0-9]*)$/.test(String(value)) || !Number.isSafeInteger(Number(value)))) return false;
|
|
900
|
-
if (tag === "canvas" && (lower === "width" || lower === "height") && (!/^[1-9][0-9]{0,4}$/.test(String(value)) || Number(value) > maxCanvasDimension)) return false;
|
|
901
|
-
if (tag === "input" && lower === "type" && !safeInputTypes.has(String(value).trim().toLowerCase() || "text")) return false;
|
|
902
|
-
return String(value).length <= maxAttributeValueLength;
|
|
903
|
-
};
|
|
904
|
-
const validateDOMTree = (root) => {
|
|
905
|
-
let nodes = 0;
|
|
906
|
-
const walk = (node, depth) => {
|
|
907
|
-
nodes += 1;
|
|
908
|
-
if (nodes > maxRenderNodes || depth > maxRenderDepth) return false;
|
|
909
|
-
if (node.nodeType === Node.TEXT_NODE) return (node.nodeValue || "").length <= maxTextLength;
|
|
910
|
-
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
911
|
-
const tag = node.tagName.toLowerCase();
|
|
912
|
-
if (!allowedTags.has(tag)) return false;
|
|
913
|
-
for (const attr of node.attributes) {
|
|
914
|
-
if (!validAttribute(tag, attr.name, attr.value)) return false;
|
|
915
|
-
}
|
|
916
|
-
for (const child of node.childNodes) if (!walk(child, depth + 1)) return false;
|
|
917
|
-
return true;
|
|
918
|
-
};
|
|
919
|
-
for (const child of root.childNodes) if (!walk(child, 1)) return false;
|
|
920
|
-
return true;
|
|
921
|
-
};
|
|
922
|
-
const validStyle = (value) => exactKeys(value, ["path", "sha256", "content"]) && validPath(value.path) && validDigest(value.sha256) && typeof value.content === "string" && value.content.length <= 2097152;
|
|
923
|
-
const validWorker = (value) => exactKeys(value, ["path", "sha256", "type", "content"]) && validPath(value.path) && validDigest(value.sha256) && value.type === "classic" && typeof value.content === "string" && value.content.length <= 4194304;
|
|
924
|
-
const validAsset = (value) => exactKeys(value, ["binding_id", "logical_ids", "path", "sha256", "size", "content_type"]) && validOpaqueHandle(value.binding_id, "asset") && Array.isArray(value.logical_ids) && value.logical_ids.length > 0 && value.logical_ids.length <= 16 && value.logical_ids.every(validResourceIdentifier) && new Set(value.logical_ids).size === value.logical_ids.length && validPath(value.path) && validDigest(value.sha256) && Number.isSafeInteger(value.size) && value.size >= 0 && value.size <= maxLazyAssetBytes && typeof value.content_type === "string" && value.content_type.length > 0 && value.content_type.length <= 256;
|
|
925
|
-
const validDocument = (value) => isRecord(value) && Object.keys(value).every((key) => ["schema_version", "entry_path", "entry_sha256", "title", "language", "direction", "body_html", "styles", "worker", "assets", "critical_bytes"].includes(key)) && value.schema_version === documentSchema && validPath(value.entry_path) && validDigest(value.entry_sha256) && (value.title === undefined || (typeof value.title === "string" && value.title.length <= 256)) && (value.language === undefined || (typeof value.language === "string" && value.language.length <= 64)) && (value.direction === undefined || ["ltr", "rtl", "auto"].includes(value.direction)) && typeof value.body_html === "string" && value.body_html.length <= 4194304 && Array.isArray(value.styles) && value.styles.every(validStyle) && validWorker(value.worker) && Array.isArray(value.assets) && value.assets.length <= maxLazyAssetCount && value.assets.every(validAsset) && new Set(value.assets.map((asset) => asset.binding_id)).size === value.assets.length && new Set(value.assets.map((asset) => asset.path)).size === value.assets.length && new Set(value.assets.flatMap((asset) => asset.logical_ids)).size === value.assets.reduce((total, asset) => total + asset.logical_ids.length, 0) && value.assets.reduce((total, asset) => total + asset.size, 0) <= maxLazyAssetBytes && Number.isSafeInteger(value.critical_bytes) && value.critical_bytes >= 0 && value.critical_bytes <= maxCriticalDocumentBytes;
|
|
926
|
-
const contextColorKeys = ["canvas", "surface", "surface_elevated", "text", "text_muted", "border", "accent", "accent_text", "success", "warning", "danger", "focus"];
|
|
927
|
-
const validContextColor = (value) => typeof value === "string" && /^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/.test(value);
|
|
928
|
-
const validSurfaceContext = (value) => {
|
|
929
|
-
if (!isRecord(value) || !exactKeys(value, ["schema_version", "revision", "appearance", "locale"]) || value.schema_version !== "redevplugin.surface_context.v1" || !Number.isSafeInteger(value.revision) || value.revision < 1) return false;
|
|
930
|
-
if (!isRecord(value.appearance) || !exactKeys(value.appearance, ["color_scheme", "colors"]) || !["light", "dark"].includes(value.appearance.color_scheme) || !isRecord(value.appearance.colors) || !exactKeys(value.appearance.colors, contextColorKeys) || contextColorKeys.some((key) => !validContextColor(value.appearance.colors[key]))) return false;
|
|
931
|
-
if (!isRecord(value.locale) || !exactKeys(value.locale, ["language_tag", "direction"]) || typeof value.locale.language_tag !== "string" || value.locale.language_tag.length < 2 || value.locale.language_tag.length > 64 || !["ltr", "rtl"].includes(value.locale.direction)) return false;
|
|
932
|
-
try { if (typeof Intl !== "undefined" && Intl.getCanonicalLocales(value.locale.language_tag).length !== 1) return false; } catch { return false; }
|
|
933
|
-
return true;
|
|
934
|
-
};
|
|
935
|
-
let accepted = false;
|
|
936
|
-
let initialized = false;
|
|
937
|
-
let parentPort;
|
|
938
|
-
let worker;
|
|
939
|
-
let workerControlPort;
|
|
940
|
-
let workerPort;
|
|
941
|
-
let frameGenerationID = "";
|
|
942
|
-
let surfaceHandle = "";
|
|
943
|
-
let currentDocument;
|
|
944
|
-
let currentContext;
|
|
945
|
-
let workerReady = false;
|
|
946
|
-
let workerHeartbeatSequence = 0;
|
|
947
|
-
let workerHeartbeatPendingID;
|
|
948
|
-
let workerHeartbeatTimer;
|
|
949
|
-
let workerHeartbeatTimeout;
|
|
950
|
-
let pendingQuiesceID;
|
|
951
|
-
let pendingFileExportAction;
|
|
952
|
-
let runtimeDisposed = false;
|
|
953
|
-
const pendingWorkerRequests = new Set();
|
|
954
|
-
const pendingRendererCommits = new Set();
|
|
955
|
-
const requestSequence = { rpc: 0, execution: 0, render: 0, canvas: 0, asset: 0, keyboard: 0, file: 0 };
|
|
956
|
-
let executionQuerySurfaceTokens = 8;
|
|
957
|
-
let executionQuerySurfaceRefillAt = performance.now();
|
|
958
|
-
const executionQueryStateIdleMS = 120000;
|
|
959
|
-
let executionQueryNextPruneAt = performance.now() + 30000;
|
|
960
|
-
const maxExecutionQueryStates = 1024;
|
|
961
|
-
const executionQueryStates = new Map();
|
|
962
|
-
const executionQueryRequests = new Map();
|
|
963
|
-
let renderWindowStartedAt = 0;
|
|
964
|
-
let renderCount = 0;
|
|
965
|
-
let uiRevision = 0;
|
|
966
|
-
let uiGraph = new Map();
|
|
967
|
-
let uiNodes = new Map();
|
|
968
|
-
const controlEdits = new Map();
|
|
969
|
-
let keyboardBindings = [];
|
|
970
|
-
let lastAutofocusIdentity = "";
|
|
971
|
-
const pendingAssets = new Map();
|
|
972
|
-
const queuedAssets = [];
|
|
973
|
-
let activeAssetReads = 0;
|
|
974
|
-
let assetSequence = 0;
|
|
975
|
-
const blobURLs = new Set();
|
|
976
|
-
const assetByLogicalID = new Map();
|
|
977
|
-
const verifiedAssetBytes = new Map();
|
|
978
|
-
const imageAssetDimensions = new Map();
|
|
979
|
-
const imageAssetTypes = new Map();
|
|
980
|
-
const pendingImageRequests = new Map();
|
|
981
|
-
const activeImageDecodes = new Set();
|
|
982
|
-
const imageReservations = new Map();
|
|
983
|
-
let retainedImageCount = 0;
|
|
984
|
-
let retainedImagePixels = 0;
|
|
985
|
-
const canvasRuntimes = new Map();
|
|
986
|
-
|
|
987
|
-
const keyboardTarget = (target) => {
|
|
988
|
-
const origin = target instanceof Element ? target : document.activeElement instanceof Element ? document.activeElement : null;
|
|
989
|
-
const keyed = origin?.closest("[data-redevplugin-key]");
|
|
990
|
-
const key = keyed?.getAttribute("data-redevplugin-key");
|
|
991
|
-
const targetKey = validResourceIdentifier(key) ? key : null;
|
|
992
|
-
const control = origin?.closest("canvas, input, textarea, select, button");
|
|
993
|
-
if (!control) return { targetKey, targetKind: "surface" };
|
|
994
|
-
const tag = control.tagName.toLowerCase();
|
|
995
|
-
if (tag === "canvas") return { targetKey, targetKind: "canvas" };
|
|
996
|
-
if (tag === "textarea") return { targetKey, targetKind: "editable" };
|
|
997
|
-
if (tag === "input" && ["text", "search", "number"].includes((control.getAttribute("type") || "text").toLowerCase())) {
|
|
998
|
-
return { targetKey, targetKind: "editable" };
|
|
999
|
-
}
|
|
1000
|
-
return { targetKey, targetKind: "control" };
|
|
1001
|
-
};
|
|
1002
|
-
const handleSurfaceKeyboard = (event) => {
|
|
1003
|
-
if (!initialized || runtimeDisposed || pendingQuiesceID || !workerPort || !["keydown", "keyup"].includes(event.type)) return;
|
|
1004
|
-
const target = keyboardTarget(event.target);
|
|
1005
|
-
const edit = target.targetKey === null ? undefined : controlEdits.get(target.targetKey);
|
|
1006
|
-
const isComposing = Boolean(event.isComposing || edit?.isComposing);
|
|
1007
|
-
const binding = isComposing ? undefined : keyboardBindings.find((candidate) =>
|
|
1008
|
-
candidate.event === event.type && candidate.code === String(event.code || "") &&
|
|
1009
|
-
candidate.repeat === Boolean(event.repeat) && candidate.alt_key === Boolean(event.altKey) &&
|
|
1010
|
-
candidate.ctrl_key === Boolean(event.ctrlKey) && candidate.meta_key === Boolean(event.metaKey) &&
|
|
1011
|
-
candidate.shift_key === Boolean(event.shiftKey) && candidate.target_key === target.targetKey &&
|
|
1012
|
-
candidate.target_kind === target.targetKind
|
|
1013
|
-
);
|
|
1014
|
-
if (binding) event.preventDefault();
|
|
1015
|
-
sendWorker({
|
|
1016
|
-
type: "redevplugin.ui.keyboard.input",
|
|
1017
|
-
event: {
|
|
1018
|
-
event: event.type,
|
|
1019
|
-
code: String(event.code || "").slice(0, 64),
|
|
1020
|
-
key: String(event.key || "").slice(0, 64),
|
|
1021
|
-
repeat: Boolean(event.repeat),
|
|
1022
|
-
alt_key: Boolean(event.altKey),
|
|
1023
|
-
ctrl_key: Boolean(event.ctrlKey),
|
|
1024
|
-
meta_key: Boolean(event.metaKey),
|
|
1025
|
-
shift_key: Boolean(event.shiftKey),
|
|
1026
|
-
is_composing: isComposing,
|
|
1027
|
-
target_key: target.targetKey,
|
|
1028
|
-
target_kind: target.targetKind,
|
|
1029
|
-
default_prevented: Boolean(event.defaultPrevented),
|
|
1030
|
-
binding_id: binding?.binding_id || null,
|
|
1031
|
-
},
|
|
1032
|
-
});
|
|
1033
|
-
};
|
|
1034
|
-
for (const eventType of ["keydown", "keyup"]) document.addEventListener(eventType, handleSurfaceKeyboard, true);
|
|
1035
|
-
|
|
1036
|
-
const sendParent = (message) => {
|
|
1037
|
-
if (parentPort && withinLimit(message)) parentPort.postMessage(message);
|
|
1038
|
-
};
|
|
1039
|
-
const sendParentTransfer = (message, transfer) => {
|
|
1040
|
-
if (parentPort) parentPort.postMessage(message, transfer);
|
|
1041
|
-
};
|
|
1042
|
-
let interactionSequence = 0;
|
|
1043
|
-
const interactionTargetKey = (target) => {
|
|
1044
|
-
const element = target instanceof Element ? target.closest("[data-redevplugin-key]") : null;
|
|
1045
|
-
const key = element?.getAttribute("data-redevplugin-key");
|
|
1046
|
-
return validResourceIdentifier(key) ? key : null;
|
|
1047
|
-
};
|
|
1048
|
-
const ownsLocalScroll = (event) => {
|
|
1049
|
-
const path = typeof event.composedPath === "function" ? event.composedPath() : [event.target];
|
|
1050
|
-
for (const candidate of path) {
|
|
1051
|
-
if (!(candidate instanceof Element)) continue;
|
|
1052
|
-
if (candidate === document.scrollingElement &&
|
|
1053
|
-
(candidate.scrollHeight > candidate.clientHeight || candidate.scrollWidth > candidate.clientWidth)) return true;
|
|
1054
|
-
const style = getComputedStyle(candidate);
|
|
1055
|
-
const vertical = /(auto|scroll|overlay)/.test(style.overflowY) && candidate.scrollHeight > candidate.clientHeight;
|
|
1056
|
-
const horizontal = /(auto|scroll|overlay)/.test(style.overflowX) && candidate.scrollWidth > candidate.clientWidth;
|
|
1057
|
-
if (vertical || horizontal) return true;
|
|
1058
|
-
}
|
|
1059
|
-
return false;
|
|
1060
|
-
};
|
|
1061
|
-
const sendInteraction = (kind, target, options = {}) => {
|
|
1062
|
-
if (!initialized || pendingQuiesceID) return 0;
|
|
1063
|
-
interactionSequence += 1;
|
|
1064
|
-
sendParent({
|
|
1065
|
-
type: "redevplugin.surface.interaction",
|
|
1066
|
-
frame_generation_id: frameGenerationID,
|
|
1067
|
-
surface_handle: surfaceHandle,
|
|
1068
|
-
sequence: interactionSequence,
|
|
1069
|
-
kind,
|
|
1070
|
-
target_key: interactionTargetKey(target),
|
|
1071
|
-
action: options.action || null,
|
|
1072
|
-
local_scroll: options.localScroll === true,
|
|
1073
|
-
selection_active: options.selectionActive === true,
|
|
1074
|
-
});
|
|
1075
|
-
return interactionSequence;
|
|
1076
|
-
};
|
|
1077
|
-
document.addEventListener("pointerdown", (event) => sendInteraction("activation", event.target), true);
|
|
1078
|
-
document.addEventListener("focusin", (event) => sendInteraction("focus", event.target), true);
|
|
1079
|
-
const ownsCanvasWheel = (target) => {
|
|
1080
|
-
const element = target instanceof Element ? target.closest("canvas[data-redevplugin-canvas]") : null;
|
|
1081
|
-
const canvasID = element?.getAttribute("data-redevplugin-canvas");
|
|
1082
|
-
return validResourceIdentifier(canvasID) && canvasRuntimes.has(canvasID);
|
|
1083
|
-
};
|
|
1084
|
-
document.addEventListener("wheel", (event) => sendInteraction("wheel", event.target, { localScroll: ownsCanvasWheel(event.target) || ownsLocalScroll(event) }), { capture: true, passive: true });
|
|
1085
|
-
document.addEventListener("selectionchange", () => {
|
|
1086
|
-
const selection = document.getSelection();
|
|
1087
|
-
const target = selection?.anchorNode instanceof Element ? selection.anchorNode : selection?.anchorNode?.parentElement;
|
|
1088
|
-
sendInteraction("text-selection", target, { selectionActive: Boolean(selection && !selection.isCollapsed) });
|
|
1089
|
-
});
|
|
1090
|
-
const sendWorker = (message) => {
|
|
1091
|
-
if (workerPort && withinLimit(message)) workerPort.postMessage(message);
|
|
1092
|
-
};
|
|
1093
|
-
const sendWorkerTransfer = (message, transfer) => {
|
|
1094
|
-
if (workerPort) workerPort.postMessage(message, transfer);
|
|
1095
|
-
};
|
|
1096
|
-
const fail = (message) => {
|
|
1097
|
-
document.documentElement.lang = "en";
|
|
1098
|
-
document.body.replaceChildren();
|
|
1099
|
-
const error = document.createElement("pre");
|
|
1100
|
-
error.setAttribute("role", "alert");
|
|
1101
|
-
error.textContent = "Plugin surface failed to initialize.";
|
|
1102
|
-
document.body.append(error);
|
|
1103
|
-
sendParent({ type: "redevplugin.surface.error", error: String(message).slice(0, 512) });
|
|
1104
|
-
disposeRuntime();
|
|
1105
|
-
};
|
|
1106
|
-
const cancelPendingRendererCommits = () => {
|
|
1107
|
-
for (const cancel of [...pendingRendererCommits]) cancel();
|
|
1108
|
-
};
|
|
1109
|
-
const disposeRuntime = () => {
|
|
1110
|
-
if (runtimeDisposed) return;
|
|
1111
|
-
runtimeDisposed = true;
|
|
1112
|
-
cancelPendingRendererCommits();
|
|
1113
|
-
if (workerHeartbeatTimer) clearTimeout(workerHeartbeatTimer);
|
|
1114
|
-
if (workerHeartbeatTimeout) clearTimeout(workerHeartbeatTimeout);
|
|
1115
|
-
workerHeartbeatTimer = undefined;
|
|
1116
|
-
workerHeartbeatTimeout = undefined;
|
|
1117
|
-
workerHeartbeatPendingID = undefined;
|
|
1118
|
-
workerReady = false;
|
|
1119
|
-
keyboardBindings = [];
|
|
1120
|
-
for (const eventType of ["keydown", "keyup"]) document.removeEventListener(eventType, handleSurfaceKeyboard, true);
|
|
1121
|
-
for (const runtime of canvasRuntimes.values()) runtime.dispose();
|
|
1122
|
-
canvasRuntimes.clear();
|
|
1123
|
-
if (workerControlPort) {
|
|
1124
|
-
workerControlPort.close();
|
|
1125
|
-
workerControlPort = undefined;
|
|
1126
|
-
}
|
|
1127
|
-
if (workerPort) {
|
|
1128
|
-
try { workerPort.postMessage({ type: "redevplugin.bridge.lifecycle", event: { type: "dispose" } }); } catch {}
|
|
1129
|
-
workerPort.close();
|
|
1130
|
-
workerPort = undefined;
|
|
1131
|
-
}
|
|
1132
|
-
if (worker) {
|
|
1133
|
-
worker.terminate();
|
|
1134
|
-
worker = undefined;
|
|
1135
|
-
}
|
|
1136
|
-
for (const url of blobURLs) URL.revokeObjectURL(url);
|
|
1137
|
-
blobURLs.clear();
|
|
1138
|
-
pendingWorkerRequests.clear();
|
|
1139
|
-
executionQueryStates.clear();
|
|
1140
|
-
executionQueryRequests.clear();
|
|
1141
|
-
pendingAssets.clear();
|
|
1142
|
-
queuedAssets.length = 0;
|
|
1143
|
-
activeAssetReads = 0;
|
|
1144
|
-
pendingImageRequests.clear();
|
|
1145
|
-
activeImageDecodes.clear();
|
|
1146
|
-
imageReservations.clear();
|
|
1147
|
-
imageAssetDimensions.clear();
|
|
1148
|
-
imageAssetTypes.clear();
|
|
1149
|
-
retainedImageCount = 0;
|
|
1150
|
-
retainedImagePixels = 0;
|
|
1151
|
-
verifiedAssetBytes.clear();
|
|
1152
|
-
assetByLogicalID.clear();
|
|
1153
|
-
};
|
|
1154
|
-
const terminateQuiescedWorker = () => {
|
|
1155
|
-
cancelPendingRendererCommits();
|
|
1156
|
-
for (const runtime of canvasRuntimes.values()) runtime.dispose();
|
|
1157
|
-
canvasRuntimes.clear();
|
|
1158
|
-
if (workerHeartbeatTimer) clearTimeout(workerHeartbeatTimer);
|
|
1159
|
-
if (workerHeartbeatTimeout) clearTimeout(workerHeartbeatTimeout);
|
|
1160
|
-
workerHeartbeatTimer = undefined;
|
|
1161
|
-
workerHeartbeatTimeout = undefined;
|
|
1162
|
-
workerHeartbeatPendingID = undefined;
|
|
1163
|
-
workerReady = false;
|
|
1164
|
-
if (workerControlPort) {
|
|
1165
|
-
workerControlPort.close();
|
|
1166
|
-
workerControlPort = undefined;
|
|
1167
|
-
}
|
|
1168
|
-
if (workerPort) {
|
|
1169
|
-
workerPort.close();
|
|
1170
|
-
workerPort = undefined;
|
|
1171
|
-
}
|
|
1172
|
-
if (worker) {
|
|
1173
|
-
worker.terminate();
|
|
1174
|
-
worker = undefined;
|
|
1175
|
-
}
|
|
1176
|
-
pendingWorkerRequests.clear();
|
|
1177
|
-
};
|
|
1178
|
-
const validateCanvasIdentifiers = (root) => {
|
|
1179
|
-
const identifiers = new Set();
|
|
1180
|
-
let totalPixels = 0;
|
|
1181
|
-
for (const canvas of root.querySelectorAll("canvas[data-redevplugin-canvas]")) {
|
|
1182
|
-
const identifier = canvas.getAttribute("data-redevplugin-canvas");
|
|
1183
|
-
if (!validResourceIdentifier(identifier) || identifiers.has(identifier)) throw new Error("plugin canvas identifiers are invalid or ambiguous");
|
|
1184
|
-
identifiers.add(identifier);
|
|
1185
|
-
if (identifiers.size > maxCanvasCount) throw new Error("plugin document exceeds the canvas count limit");
|
|
1186
|
-
const width = Number(canvas.getAttribute("width") || 300);
|
|
1187
|
-
const height = Number(canvas.getAttribute("height") || 150);
|
|
1188
|
-
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0 || width > maxCanvasDimension || height > maxCanvasDimension) {
|
|
1189
|
-
throw new Error("plugin canvas dimensions are invalid");
|
|
1190
|
-
}
|
|
1191
|
-
totalPixels += width * height;
|
|
1192
|
-
if (totalPixels > maxCanvasTotalPixels) throw new Error("plugin document exceeds the canvas pixel budget");
|
|
1193
|
-
}
|
|
1194
|
-
};
|
|
1195
|
-
const applyStaticDocument = (surfaceDocument) => {
|
|
1196
|
-
for (const asset of surfaceDocument.assets) {
|
|
1197
|
-
for (const logicalID of asset.logical_ids) assetByLogicalID.set(logicalID, asset);
|
|
1198
|
-
}
|
|
1199
|
-
document.title = typeof surfaceDocument.title === "string" ? surfaceDocument.title.slice(0, 256) : "Plugin";
|
|
1200
|
-
document.documentElement.lang = typeof surfaceDocument.language === "string" ? surfaceDocument.language.slice(0, 64) : "en";
|
|
1201
|
-
if (["ltr", "rtl", "auto"].includes(surfaceDocument.direction)) document.documentElement.dir = surfaceDocument.direction;
|
|
1202
|
-
const template = document.createElement("template");
|
|
1203
|
-
template.innerHTML = surfaceDocument.body_html;
|
|
1204
|
-
if (!validateDOMTree(template.content)) throw new Error("static plugin document failed renderer validation");
|
|
1205
|
-
validateCanvasIdentifiers(template.content);
|
|
1206
|
-
document.body.replaceChildren(template.content.cloneNode(true));
|
|
1207
|
-
for (const styleAsset of surfaceDocument.styles) {
|
|
1208
|
-
const style = document.createElement("style");
|
|
1209
|
-
style.setAttribute("nonce", scriptNonce);
|
|
1210
|
-
style.dataset.redevpluginAsset = styleAsset.path;
|
|
1211
|
-
style.textContent = styleAsset.content;
|
|
1212
|
-
document.head.append(style);
|
|
1213
|
-
}
|
|
1214
|
-
};
|
|
1215
|
-
const applySurfaceContext = (context) => {
|
|
1216
|
-
document.documentElement.lang = context.locale.language_tag;
|
|
1217
|
-
document.documentElement.dir = context.locale.direction;
|
|
1218
|
-
document.documentElement.style.colorScheme = context.appearance.color_scheme;
|
|
1219
|
-
for (const [key, value] of Object.entries(context.appearance.colors)) document.documentElement.style.setProperty("--redevplugin-color-" + key.replaceAll("_", "-"), value);
|
|
1220
|
-
};
|
|
1221
|
-
const applyWorkerAttribute = (element, tag, name, attributeValue) => {
|
|
1222
|
-
const lower = name.toLowerCase();
|
|
1223
|
-
if (!validAttribute(tag, name, attributeValue)) throw new Error("plugin render attribute is not allowed");
|
|
1224
|
-
if (lower === "value" && "value" in element) element.value = String(attributeValue);
|
|
1225
|
-
if (lower === "checked" && "checked" in element) element.checked = attributeValue === true;
|
|
1226
|
-
if (lower === "autofocus") {
|
|
1227
|
-
if (attributeValue !== false) element.setAttribute("data-redevplugin-renderer-autofocus", "");
|
|
1228
|
-
else element.removeAttribute("data-redevplugin-renderer-autofocus");
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
if (typeof attributeValue === "boolean") {
|
|
1232
|
-
const serialized = lower.startsWith("aria-") ? String(attributeValue) : "";
|
|
1233
|
-
if (attributeValue || serialized) element.setAttribute(name, serialized);
|
|
1234
|
-
else element.removeAttribute(name);
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
element.setAttribute(name, String(attributeValue));
|
|
1238
|
-
};
|
|
1239
|
-
const buildWorkerSubtree = (value, state, depth, parentKey, graph = new Map(), nodes = new Map()) => {
|
|
1240
|
-
state.nodes += 1;
|
|
1241
|
-
if (state.nodes > maxRenderNodes || depth > maxRenderDepth || !isRecord(value) || typeof value.key !== "string" ||
|
|
1242
|
-
!validResourceIdentifier(value.key) || graph.has(value.key)) {
|
|
1243
|
-
throw new Error("plugin render tree exceeds limits or contains an invalid key");
|
|
1244
|
-
}
|
|
1245
|
-
if (value.type === "text") {
|
|
1246
|
-
if (!exactKeys(value, ["type", "key", "text"]) || typeof value.text !== "string" || value.text.length > maxTextLength) {
|
|
1247
|
-
throw new Error("plugin render text node is invalid");
|
|
1248
|
-
}
|
|
1249
|
-
const node = document.createTextNode(value.text);
|
|
1250
|
-
graph.set(value.key, {
|
|
1251
|
-
key: value.key,
|
|
1252
|
-
type: "text",
|
|
1253
|
-
text: value.text,
|
|
1254
|
-
parentKey,
|
|
1255
|
-
prevKey: null,
|
|
1256
|
-
nextKey: null,
|
|
1257
|
-
firstChildKey: null,
|
|
1258
|
-
lastChildKey: null,
|
|
1259
|
-
height: 1,
|
|
1260
|
-
heightDirty: false,
|
|
1261
|
-
});
|
|
1262
|
-
nodes.set(value.key, node);
|
|
1263
|
-
return { node, graph, nodes };
|
|
1264
|
-
}
|
|
1265
|
-
if (!Object.keys(value).every((key) => ["type", "key", "tag", "attributes", "children"].includes(key)) ||
|
|
1266
|
-
value.type !== "element" || typeof value.tag !== "string") {
|
|
1267
|
-
throw new Error("plugin render node is invalid");
|
|
1268
|
-
}
|
|
1269
|
-
const tag = value.tag.toLowerCase();
|
|
1270
|
-
if (!allowedTags.has(tag)) throw new Error("plugin render tag is not allowed");
|
|
1271
|
-
const attributes = {};
|
|
1272
|
-
if (value.attributes !== undefined) {
|
|
1273
|
-
if (!isRecord(value.attributes) || Object.keys(value.attributes).length > maxAttributesPerElement) throw new Error("plugin render attributes are invalid");
|
|
1274
|
-
for (const [name, attributeValue] of Object.entries(value.attributes)) {
|
|
1275
|
-
if (!validAttribute(tag, name, attributeValue)) throw new Error("plugin render attribute is not allowed");
|
|
1276
|
-
attributes[name] = attributeValue;
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
if (value.children !== undefined && !Array.isArray(value.children)) throw new Error("plugin render children are invalid");
|
|
1280
|
-
const element = document.createElement(tag);
|
|
1281
|
-
element.setAttribute("data-redevplugin-key", value.key);
|
|
1282
|
-
for (const [name, attributeValue] of Object.entries(attributes)) applyWorkerAttribute(element, tag, name, attributeValue);
|
|
1283
|
-
const record = {
|
|
1284
|
-
key: value.key,
|
|
1285
|
-
type: "element",
|
|
1286
|
-
tag,
|
|
1287
|
-
attributes,
|
|
1288
|
-
parentKey,
|
|
1289
|
-
prevKey: null,
|
|
1290
|
-
nextKey: null,
|
|
1291
|
-
firstChildKey: null,
|
|
1292
|
-
lastChildKey: null,
|
|
1293
|
-
height: 1,
|
|
1294
|
-
heightDirty: false,
|
|
1295
|
-
};
|
|
1296
|
-
graph.set(value.key, record);
|
|
1297
|
-
nodes.set(value.key, element);
|
|
1298
|
-
let previousKey = null;
|
|
1299
|
-
for (const child of value.children || []) {
|
|
1300
|
-
const built = buildWorkerSubtree(child, state, depth + 1, value.key, graph, nodes);
|
|
1301
|
-
const childRecord = graph.get(child.key);
|
|
1302
|
-
childRecord.prevKey = previousKey;
|
|
1303
|
-
if (previousKey) graph.get(previousKey).nextKey = child.key;
|
|
1304
|
-
else record.firstChildKey = child.key;
|
|
1305
|
-
record.lastChildKey = child.key;
|
|
1306
|
-
record.height = Math.max(record.height, childRecord.height + 1);
|
|
1307
|
-
previousKey = child.key;
|
|
1308
|
-
element.append(built.node);
|
|
1309
|
-
}
|
|
1310
|
-
return { node: element, graph, nodes };
|
|
1311
|
-
};
|
|
1312
|
-
const renderElementIdentity = (element) => ({
|
|
1313
|
-
tag: element.tagName,
|
|
1314
|
-
id: element.getAttribute("id") || "",
|
|
1315
|
-
name: element.getAttribute("name") || "",
|
|
1316
|
-
action: element.getAttribute("data-redevplugin-action") || "",
|
|
1317
|
-
escapeAction: element.getAttribute("data-redevplugin-escape-action") || "",
|
|
1318
|
-
ariaLabel: element.getAttribute("aria-label") || "",
|
|
1319
|
-
value: element.tagName === "BUTTON" || element.tagName === "OPTION" ? element.getAttribute("value") || "" : "",
|
|
1320
|
-
});
|
|
1321
|
-
const sameRenderElementIdentity = (left, right) => left.tag === right.tag && left.id === right.id && left.name === right.name && left.action === right.action && left.escapeAction === right.escapeAction && left.ariaLabel === right.ariaLabel && left.value === right.value;
|
|
1322
|
-
const captureRenderState = () => {
|
|
1323
|
-
const active = document.activeElement instanceof Element && document.body.contains(document.activeElement) ? document.activeElement : undefined;
|
|
1324
|
-
let selection;
|
|
1325
|
-
if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) {
|
|
1326
|
-
selection = { start: active.selectionStart, end: active.selectionEnd, direction: active.selectionDirection };
|
|
1327
|
-
}
|
|
1328
|
-
return {
|
|
1329
|
-
activeIdentity: active ? renderElementIdentity(active) : undefined,
|
|
1330
|
-
selection,
|
|
1331
|
-
activeScrollTop: active instanceof HTMLElement ? active.scrollTop : 0,
|
|
1332
|
-
activeScrollLeft: active instanceof HTMLElement ? active.scrollLeft : 0,
|
|
1333
|
-
documentScrollTop: document.documentElement.scrollTop,
|
|
1334
|
-
documentScrollLeft: document.documentElement.scrollLeft,
|
|
1335
|
-
bodyScrollTop: document.body.scrollTop,
|
|
1336
|
-
bodyScrollLeft: document.body.scrollLeft,
|
|
1337
|
-
};
|
|
1338
|
-
};
|
|
1339
|
-
const restoreRenderState = (snapshot) => {
|
|
1340
|
-
const autofocus = document.body.querySelector("[data-redevplugin-renderer-autofocus]");
|
|
1341
|
-
const autofocusIdentity = autofocus instanceof Element ? JSON.stringify(renderElementIdentity(autofocus)) : "";
|
|
1342
|
-
if (autofocus instanceof Element) autofocus.removeAttribute("data-redevplugin-renderer-autofocus");
|
|
1343
|
-
let target;
|
|
1344
|
-
if (autofocus instanceof HTMLElement && !autofocus.hasAttribute("disabled") && autofocusIdentity !== lastAutofocusIdentity) {
|
|
1345
|
-
target = autofocus;
|
|
1346
|
-
lastAutofocusIdentity = autofocusIdentity;
|
|
1347
|
-
} else {
|
|
1348
|
-
if (!autofocusIdentity) lastAutofocusIdentity = "";
|
|
1349
|
-
if (snapshot.activeIdentity) {
|
|
1350
|
-
for (const candidate of document.body.querySelectorAll(snapshot.activeIdentity.tag.toLowerCase())) {
|
|
1351
|
-
if (sameRenderElementIdentity(renderElementIdentity(candidate), snapshot.activeIdentity)) {
|
|
1352
|
-
target = candidate;
|
|
1353
|
-
break;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
if (target instanceof HTMLElement && !target.hasAttribute("disabled")) {
|
|
1359
|
-
try { target.focus({ preventScroll: true }); } catch { target.focus(); }
|
|
1360
|
-
target.scrollTop = snapshot.activeScrollTop;
|
|
1361
|
-
target.scrollLeft = snapshot.activeScrollLeft;
|
|
1362
|
-
if (snapshot.selection && (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
|
|
1363
|
-
const length = target.value.length;
|
|
1364
|
-
const start = Math.min(snapshot.selection.start ?? length, length);
|
|
1365
|
-
const end = Math.min(snapshot.selection.end ?? start, length);
|
|
1366
|
-
try { target.setSelectionRange(start, end, snapshot.selection.direction || "none"); } catch {}
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
document.documentElement.scrollTop = snapshot.documentScrollTop;
|
|
1370
|
-
document.documentElement.scrollLeft = snapshot.documentScrollLeft;
|
|
1371
|
-
document.body.scrollTop = snapshot.bodyScrollTop;
|
|
1372
|
-
document.body.scrollLeft = snapshot.bodyScrollLeft;
|
|
1373
|
-
};
|
|
1374
|
-
const transferredCanvasKey = (key) => {
|
|
1375
|
-
const element = uiNodes.get(key);
|
|
1376
|
-
if (!(element instanceof HTMLCanvasElement)) return false;
|
|
1377
|
-
for (const runtime of canvasRuntimes.values()) if (runtime.canvas === element) return true;
|
|
1378
|
-
return false;
|
|
1379
|
-
};
|
|
1380
|
-
const editableControlAttribute = (tag, name) => {
|
|
1381
|
-
const normalized = name.toLowerCase();
|
|
1382
|
-
return (normalized === "value" && ["input", "textarea", "select", "option"].includes(tag)) ||
|
|
1383
|
-
(normalized === "checked" && tag === "input");
|
|
1384
|
-
};
|
|
1385
|
-
const createGraphOverlay = (base) => {
|
|
1386
|
-
const changed = new Map();
|
|
1387
|
-
const deleted = new Set();
|
|
1388
|
-
let count = base.size;
|
|
1389
|
-
const get = (key) => changed.has(key) ? changed.get(key) : deleted.has(key) ? undefined : base.get(key);
|
|
1390
|
-
const edit = (key) => {
|
|
1391
|
-
const current = get(key);
|
|
1392
|
-
if (!current) throw new Error("plugin UI patch target does not exist");
|
|
1393
|
-
if (changed.has(key)) return current;
|
|
1394
|
-
const copy = { ...current, ...(current.attributes ? { attributes: { ...current.attributes } } : {}) };
|
|
1395
|
-
changed.set(key, copy);
|
|
1396
|
-
return copy;
|
|
1397
|
-
};
|
|
1398
|
-
const add = (record) => {
|
|
1399
|
-
if (get(record.key)) throw new Error("plugin UI insertion duplicates a key");
|
|
1400
|
-
changed.set(record.key, { ...record, ...(record.attributes ? { attributes: { ...record.attributes } } : {}) });
|
|
1401
|
-
count += 1;
|
|
1402
|
-
};
|
|
1403
|
-
const remove = (key) => {
|
|
1404
|
-
if (!get(key)) throw new Error("plugin UI removal target does not exist");
|
|
1405
|
-
changed.delete(key);
|
|
1406
|
-
if (base.has(key)) deleted.add(key);
|
|
1407
|
-
count -= 1;
|
|
1408
|
-
};
|
|
1409
|
-
const commit = () => {
|
|
1410
|
-
for (const key of deleted) base.delete(key);
|
|
1411
|
-
for (const [key, record] of changed) base.set(key, record);
|
|
1412
|
-
};
|
|
1413
|
-
return { get, edit, add, remove, commit, count: () => count };
|
|
1414
|
-
};
|
|
1415
|
-
const graphDepth = (graph, key) => {
|
|
1416
|
-
let depth = 0;
|
|
1417
|
-
let cursor = graph.get(key);
|
|
1418
|
-
const seen = new Set();
|
|
1419
|
-
while (cursor) {
|
|
1420
|
-
if (seen.has(cursor.key)) throw new Error("plugin UI graph contains a cycle");
|
|
1421
|
-
seen.add(cursor.key);
|
|
1422
|
-
depth += 1;
|
|
1423
|
-
cursor = cursor.parentKey ? graph.get(cursor.parentKey) : undefined;
|
|
1424
|
-
}
|
|
1425
|
-
return depth;
|
|
1426
|
-
};
|
|
1427
|
-
const graphSubtreeKeys = (graph, rootKey) => {
|
|
1428
|
-
const keys = [];
|
|
1429
|
-
const stack = [rootKey];
|
|
1430
|
-
while (stack.length > 0) {
|
|
1431
|
-
const key = stack.pop();
|
|
1432
|
-
const record = key && graph.get(key);
|
|
1433
|
-
if (!record) throw new Error("plugin UI graph is inconsistent");
|
|
1434
|
-
keys.push(key);
|
|
1435
|
-
const children = [];
|
|
1436
|
-
let childKey = record.firstChildKey;
|
|
1437
|
-
while (childKey) {
|
|
1438
|
-
const child = graph.get(childKey);
|
|
1439
|
-
if (!child || child.parentKey !== key) throw new Error("plugin UI child graph is inconsistent");
|
|
1440
|
-
children.push(childKey);
|
|
1441
|
-
childKey = child.nextKey;
|
|
1442
|
-
}
|
|
1443
|
-
stack.push(...children.reverse());
|
|
1444
|
-
}
|
|
1445
|
-
return keys;
|
|
1446
|
-
};
|
|
1447
|
-
const graphSubtreeHeight = (graph, rootKey) => {
|
|
1448
|
-
const record = graph.get(rootKey);
|
|
1449
|
-
if (!record) throw new Error("plugin UI graph is inconsistent");
|
|
1450
|
-
if (!record.heightDirty) return record.height;
|
|
1451
|
-
let height = 1;
|
|
1452
|
-
let childKey = record.firstChildKey;
|
|
1453
|
-
while (childKey) {
|
|
1454
|
-
const child = graph.get(childKey);
|
|
1455
|
-
if (!child || child.parentKey !== rootKey) throw new Error("plugin UI child graph is inconsistent");
|
|
1456
|
-
height = Math.max(height, graphSubtreeHeight(graph, childKey) + 1);
|
|
1457
|
-
childKey = child.nextKey;
|
|
1458
|
-
}
|
|
1459
|
-
const edited = graph.edit(rootKey);
|
|
1460
|
-
edited.height = height;
|
|
1461
|
-
edited.heightDirty = false;
|
|
1462
|
-
return height;
|
|
1463
|
-
};
|
|
1464
|
-
const graphParentChainContains = (graph, rootKey, targetKey) => {
|
|
1465
|
-
let cursor = graph.get(rootKey);
|
|
1466
|
-
const seen = new Set();
|
|
1467
|
-
while (cursor) {
|
|
1468
|
-
if (cursor.key === targetKey) return true;
|
|
1469
|
-
if (seen.has(cursor.key)) throw new Error("plugin UI graph contains a cycle");
|
|
1470
|
-
seen.add(cursor.key);
|
|
1471
|
-
cursor = cursor.parentKey ? graph.get(cursor.parentKey) : undefined;
|
|
1472
|
-
}
|
|
1473
|
-
return false;
|
|
1474
|
-
};
|
|
1475
|
-
const graphSubtreeHasTransferredCanvas = (graph, rootKey) => {
|
|
1476
|
-
for (const runtime of canvasRuntimes.values()) {
|
|
1477
|
-
const key = runtime.canvas.getAttribute("data-redevplugin-key");
|
|
1478
|
-
if (!key) continue;
|
|
1479
|
-
let cursor = graph.get(key);
|
|
1480
|
-
const seen = new Set();
|
|
1481
|
-
while (cursor) {
|
|
1482
|
-
if (cursor.key === rootKey) return true;
|
|
1483
|
-
if (seen.has(cursor.key)) throw new Error("plugin UI graph contains a cycle");
|
|
1484
|
-
seen.add(cursor.key);
|
|
1485
|
-
cursor = cursor.parentKey ? graph.get(cursor.parentKey) : undefined;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
return false;
|
|
1489
|
-
};
|
|
1490
|
-
const markGraphHeightDirty = (graph, key) => {
|
|
1491
|
-
let cursorKey = key;
|
|
1492
|
-
const seen = new Set();
|
|
1493
|
-
while (cursorKey) {
|
|
1494
|
-
if (seen.has(cursorKey)) throw new Error("plugin UI graph contains a cycle");
|
|
1495
|
-
seen.add(cursorKey);
|
|
1496
|
-
const record = graph.edit(cursorKey);
|
|
1497
|
-
record.heightDirty = true;
|
|
1498
|
-
cursorKey = record.parentKey;
|
|
1499
|
-
}
|
|
1500
|
-
};
|
|
1501
|
-
const detachGraphNode = (graph, key) => {
|
|
1502
|
-
const record = graph.edit(key);
|
|
1503
|
-
if (!record.parentKey) throw new Error("plugin UI root cannot be detached");
|
|
1504
|
-
const parentKey = record.parentKey;
|
|
1505
|
-
const parent = graph.edit(parentKey);
|
|
1506
|
-
if (record.prevKey) graph.edit(record.prevKey).nextKey = record.nextKey;
|
|
1507
|
-
else parent.firstChildKey = record.nextKey;
|
|
1508
|
-
if (record.nextKey) graph.edit(record.nextKey).prevKey = record.prevKey;
|
|
1509
|
-
else parent.lastChildKey = record.prevKey;
|
|
1510
|
-
record.parentKey = undefined;
|
|
1511
|
-
record.prevKey = null;
|
|
1512
|
-
record.nextKey = null;
|
|
1513
|
-
markGraphHeightDirty(graph, parentKey);
|
|
1514
|
-
};
|
|
1515
|
-
const attachGraphNode = (graph, key, parentKey, beforeKey) => {
|
|
1516
|
-
const record = graph.edit(key);
|
|
1517
|
-
const parent = graph.edit(parentKey);
|
|
1518
|
-
if (parent.type !== "element" || record.parentKey) throw new Error("plugin UI insertion parent is invalid");
|
|
1519
|
-
let previousKey = parent.lastChildKey;
|
|
1520
|
-
if (beforeKey !== null) {
|
|
1521
|
-
if (beforeKey === key) throw new Error("plugin UI anchor cannot target the moved node");
|
|
1522
|
-
const before = graph.get(beforeKey);
|
|
1523
|
-
if (!before || before.parentKey !== parentKey) throw new Error("plugin UI anchor is invalid");
|
|
1524
|
-
previousKey = before.prevKey;
|
|
1525
|
-
graph.edit(beforeKey).prevKey = key;
|
|
1526
|
-
} else {
|
|
1527
|
-
parent.lastChildKey = key;
|
|
1528
|
-
}
|
|
1529
|
-
if (previousKey) graph.edit(previousKey).nextKey = key;
|
|
1530
|
-
else parent.firstChildKey = key;
|
|
1531
|
-
record.parentKey = parentKey;
|
|
1532
|
-
record.prevKey = previousKey;
|
|
1533
|
-
record.nextKey = beforeKey;
|
|
1534
|
-
if (beforeKey === null) parent.lastChildKey = key;
|
|
1535
|
-
markGraphHeightDirty(graph, parentKey);
|
|
1536
|
-
};
|
|
1537
|
-
const validatePatch = (operations) => {
|
|
1538
|
-
if (!Array.isArray(operations) || operations.length > maxPatchOperations) throw new Error("plugin UI patch exceeds limits");
|
|
1539
|
-
const graph = createGraphOverlay(uiGraph);
|
|
1540
|
-
const plan = [];
|
|
1541
|
-
for (const operation of operations) {
|
|
1542
|
-
if (!isRecord(operation) || typeof operation.type !== "string") throw new Error("plugin UI patch operation is invalid");
|
|
1543
|
-
if (operation.type === "set_text") {
|
|
1544
|
-
if (!exactKeys(operation, ["type", "target_key", "text"]) || !validResourceIdentifier(operation.target_key) ||
|
|
1545
|
-
typeof operation.text !== "string" || operation.text.length > maxTextLength) throw new Error("plugin UI text patch is invalid");
|
|
1546
|
-
const target = graph.edit(operation.target_key);
|
|
1547
|
-
if (target.type !== "text") throw new Error("plugin UI text patch target is invalid");
|
|
1548
|
-
target.text = operation.text;
|
|
1549
|
-
plan.push(operation);
|
|
1550
|
-
} else if (operation.type === "patch_attributes") {
|
|
1551
|
-
if (!exactKeys(operation, ["type", "target_key", "set", "remove"]) || !validResourceIdentifier(operation.target_key) ||
|
|
1552
|
-
!isRecord(operation.set) || Object.keys(operation.set).length > maxAttributesPerElement || !Array.isArray(operation.remove) ||
|
|
1553
|
-
operation.remove.length > maxAttributesPerElement || new Set(operation.remove).size !== operation.remove.length ||
|
|
1554
|
-
operation.remove.some((name) => typeof name !== "string")) throw new Error("plugin UI attribute patch is invalid");
|
|
1555
|
-
const target = graph.edit(operation.target_key);
|
|
1556
|
-
if (target.type !== "element" || transferredCanvasKey(target.key) || Object.keys(operation.set).some((name) => editableControlAttribute(target.tag, name)) ||
|
|
1557
|
-
operation.remove.some((name) => editableControlAttribute(target.tag, name))) throw new Error("plugin UI attribute patch target is invalid");
|
|
1558
|
-
target.attributes ||= {};
|
|
1559
|
-
for (const [name, value] of Object.entries(operation.set)) {
|
|
1560
|
-
if (!validAttribute(target.tag, name, value)) throw new Error("plugin UI attribute patch is not allowed");
|
|
1561
|
-
target.attributes[name] = value;
|
|
1562
|
-
}
|
|
1563
|
-
for (const name of operation.remove) delete target.attributes[name];
|
|
1564
|
-
plan.push(operation);
|
|
1565
|
-
} else if (operation.type === "patch_control") {
|
|
1566
|
-
if (!Object.keys(operation).every((key) => ["type", "target_key", "edit_revision", "value", "checked"].includes(key)) ||
|
|
1567
|
-
!validResourceIdentifier(operation.target_key) || !Number.isSafeInteger(operation.edit_revision) || operation.edit_revision < 0 ||
|
|
1568
|
-
(operation.value === undefined && operation.checked === undefined) ||
|
|
1569
|
-
(operation.value !== undefined && operation.value !== null && typeof operation.value !== "string") ||
|
|
1570
|
-
(operation.checked !== undefined && operation.checked !== null && typeof operation.checked !== "boolean")) throw new Error("plugin UI control patch is invalid");
|
|
1571
|
-
const target = graph.edit(operation.target_key);
|
|
1572
|
-
if (target.type !== "element" || !["input", "textarea", "select", "option"].includes(target.tag)) throw new Error("plugin UI control patch target is invalid");
|
|
1573
|
-
target.attributes ||= {};
|
|
1574
|
-
if (operation.value !== undefined) operation.value === null ? delete target.attributes.value : target.attributes.value = operation.value;
|
|
1575
|
-
if (operation.checked !== undefined) operation.checked === null ? delete target.attributes.checked : target.attributes.checked = operation.checked;
|
|
1576
|
-
plan.push(operation);
|
|
1577
|
-
} else if (operation.type === "insert_child") {
|
|
1578
|
-
if (!exactKeys(operation, ["type", "parent_key", "before_key", "node"]) || !validResourceIdentifier(operation.parent_key) ||
|
|
1579
|
-
(operation.before_key !== null && !validResourceIdentifier(operation.before_key))) throw new Error("plugin UI insertion is invalid");
|
|
1580
|
-
const parent = graph.get(operation.parent_key);
|
|
1581
|
-
if (!parent || parent.type !== "element") throw new Error("plugin UI insertion target is invalid");
|
|
1582
|
-
const state = { nodes: 0 };
|
|
1583
|
-
const built = buildWorkerSubtree(operation.node, state, graphDepth(graph, operation.parent_key) + 1, undefined);
|
|
1584
|
-
if (graph.count() + state.nodes > maxRenderNodes) throw new Error("plugin UI insertion exceeds node limits");
|
|
1585
|
-
for (const record of built.graph.values()) graph.add(record);
|
|
1586
|
-
attachGraphNode(graph, operation.node.key, operation.parent_key, operation.before_key);
|
|
1587
|
-
plan.push({ ...operation, built });
|
|
1588
|
-
} else if (operation.type === "remove_child") {
|
|
1589
|
-
if (!exactKeys(operation, ["type", "target_key"]) || !validResourceIdentifier(operation.target_key)) throw new Error("plugin UI removal is invalid");
|
|
1590
|
-
const target = graph.get(operation.target_key);
|
|
1591
|
-
if (!target || !target.parentKey) throw new Error("plugin UI removal target is invalid");
|
|
1592
|
-
if (graphSubtreeHasTransferredCanvas(graph, operation.target_key)) throw new Error("transferred canvas cannot be removed");
|
|
1593
|
-
const removedKeys = graphSubtreeKeys(graph, operation.target_key);
|
|
1594
|
-
detachGraphNode(graph, operation.target_key);
|
|
1595
|
-
for (const key of removedKeys) graph.remove(key);
|
|
1596
|
-
plan.push({ ...operation, removedKeys });
|
|
1597
|
-
} else if (operation.type === "move_child") {
|
|
1598
|
-
if (!exactKeys(operation, ["type", "target_key", "parent_key", "before_key"]) || !validResourceIdentifier(operation.target_key) ||
|
|
1599
|
-
!validResourceIdentifier(operation.parent_key) || (operation.before_key !== null && !validResourceIdentifier(operation.before_key))) {
|
|
1600
|
-
throw new Error("plugin UI move is invalid");
|
|
1601
|
-
}
|
|
1602
|
-
const target = graph.get(operation.target_key);
|
|
1603
|
-
const parent = graph.get(operation.parent_key);
|
|
1604
|
-
if (!target || !target.parentKey || !parent || parent.type !== "element" ||
|
|
1605
|
-
graphParentChainContains(graph, operation.parent_key, operation.target_key) ||
|
|
1606
|
-
graphSubtreeHasTransferredCanvas(graph, operation.target_key)) throw new Error("plugin UI move target is invalid");
|
|
1607
|
-
if (graphDepth(graph, operation.parent_key) + graphSubtreeHeight(graph, operation.target_key) > maxRenderDepth) {
|
|
1608
|
-
throw new Error("plugin UI move exceeds depth limits");
|
|
1609
|
-
}
|
|
1610
|
-
detachGraphNode(graph, operation.target_key);
|
|
1611
|
-
attachGraphNode(graph, operation.target_key, operation.parent_key, operation.before_key);
|
|
1612
|
-
plan.push(operation);
|
|
1613
|
-
} else {
|
|
1614
|
-
throw new Error("plugin UI patch operation type is unsupported");
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
return { graph, plan };
|
|
1618
|
-
};
|
|
1619
|
-
const applyPatchOperation = (operation) => {
|
|
1620
|
-
const parent = operation.parent_key && uiNodes.get(operation.parent_key);
|
|
1621
|
-
const target = operation.target_key && uiNodes.get(operation.target_key);
|
|
1622
|
-
if (operation.type === "set_text") {
|
|
1623
|
-
target.nodeValue = operation.text;
|
|
1624
|
-
} else if (operation.type === "patch_attributes") {
|
|
1625
|
-
for (const name of operation.remove) {
|
|
1626
|
-
target.removeAttribute(name);
|
|
1627
|
-
if (name.toLowerCase() === "autofocus") target.removeAttribute("data-redevplugin-renderer-autofocus");
|
|
1628
|
-
}
|
|
1629
|
-
for (const [name, value] of Object.entries(operation.set)) applyWorkerAttribute(target, target.tagName.toLowerCase(), name, value);
|
|
1630
|
-
} else if (operation.type === "patch_control") {
|
|
1631
|
-
const edit = controlEdits.get(operation.target_key) || { revision: 0, isComposing: false };
|
|
1632
|
-
if (edit.revision !== operation.edit_revision || edit.isComposing) return;
|
|
1633
|
-
if (operation.value !== undefined && "value" in target) target.value = operation.value === null ? "" : operation.value;
|
|
1634
|
-
if (operation.checked !== undefined && "checked" in target) target.checked = operation.checked === true;
|
|
1635
|
-
} else if (operation.type === "insert_child") {
|
|
1636
|
-
const before = operation.before_key === null ? null : uiNodes.get(operation.before_key);
|
|
1637
|
-
parent.insertBefore(operation.built.node, before || null);
|
|
1638
|
-
for (const [key, node] of operation.built.nodes) uiNodes.set(key, node);
|
|
1639
|
-
} else if (operation.type === "remove_child") {
|
|
1640
|
-
for (const key of operation.removedKeys) uiNodes.delete(key);
|
|
1641
|
-
target.remove();
|
|
1642
|
-
} else if (operation.type === "move_child") {
|
|
1643
|
-
const before = operation.before_key === null ? null : uiNodes.get(operation.before_key);
|
|
1644
|
-
parent.insertBefore(target, before || null);
|
|
1645
|
-
}
|
|
1646
|
-
};
|
|
1647
|
-
const commitOnRenderOpportunity = (commit) => new Promise((resolve, reject) => {
|
|
1648
|
-
if (runtimeDisposed) return resolve(false);
|
|
1649
|
-
let settled = false;
|
|
1650
|
-
let frameID;
|
|
1651
|
-
let fallbackTimer;
|
|
1652
|
-
const finish = (shouldCommit) => {
|
|
1653
|
-
if (settled) return;
|
|
1654
|
-
settled = true;
|
|
1655
|
-
if (frameID !== undefined) cancelAnimationFrame(frameID);
|
|
1656
|
-
if (fallbackTimer !== undefined) clearTimeout(fallbackTimer);
|
|
1657
|
-
pendingRendererCommits.delete(cancel);
|
|
1658
|
-
if (!shouldCommit || runtimeDisposed) return resolve(false);
|
|
1659
|
-
try { commit(); resolve(true); } catch (error) { reject(error); }
|
|
1660
|
-
};
|
|
1661
|
-
const cancel = () => finish(false);
|
|
1662
|
-
pendingRendererCommits.add(cancel);
|
|
1663
|
-
frameID = requestAnimationFrame(() => finish(true));
|
|
1664
|
-
fallbackTimer = setTimeout(() => finish(true), rendererCommitFallbackMs);
|
|
1665
|
-
});
|
|
1666
|
-
const mountWorkerTree = async (tree) => {
|
|
1667
|
-
if (uiRevision !== 0) throw new Error("plugin UI mount may only occur once");
|
|
1668
|
-
const built = buildWorkerSubtree(tree, { nodes: 0 }, 1, undefined);
|
|
1669
|
-
const root = built.node;
|
|
1670
|
-
if (!(root instanceof Element)) throw new Error("plugin UI root must be an element");
|
|
1671
|
-
validateCanvasIdentifiers(root);
|
|
1672
|
-
const committed = await commitOnRenderOpportunity(() => {
|
|
1673
|
-
const renderState = captureRenderState();
|
|
1674
|
-
document.body.replaceChildren(root);
|
|
1675
|
-
restoreRenderState(renderState);
|
|
1676
|
-
});
|
|
1677
|
-
if (!committed) return false;
|
|
1678
|
-
uiGraph = built.graph;
|
|
1679
|
-
uiNodes = built.nodes;
|
|
1680
|
-
uiRevision = 1;
|
|
1681
|
-
sendParent({ type: "redevplugin.surface.first_commit" });
|
|
1682
|
-
return true;
|
|
1683
|
-
};
|
|
1684
|
-
const patchWorkerTree = async (baseRevision, revision, operations) => {
|
|
1685
|
-
if (uiRevision < 1 || baseRevision !== uiRevision || revision !== baseRevision + 1) throw new Error("plugin UI patch revision is invalid");
|
|
1686
|
-
const validated = validatePatch(operations);
|
|
1687
|
-
const committed = await commitOnRenderOpportunity(() => {
|
|
1688
|
-
const renderState = captureRenderState();
|
|
1689
|
-
for (const operation of validated.plan) applyPatchOperation(operation);
|
|
1690
|
-
validated.graph.commit();
|
|
1691
|
-
restoreRenderState(renderState);
|
|
1692
|
-
});
|
|
1693
|
-
if (!committed) return false;
|
|
1694
|
-
uiRevision = revision;
|
|
1695
|
-
return true;
|
|
1696
|
-
};
|
|
1697
|
-
const actionPayload = (event, element, eventType = event.type) => {
|
|
1698
|
-
const target = event.target instanceof Element ? event.target.closest("[data-redevplugin-key]") : element;
|
|
1699
|
-
const targetKey = target?.getAttribute("data-redevplugin-key") || element.getAttribute("data-redevplugin-key");
|
|
1700
|
-
if (!validResourceIdentifier(targetKey)) throw new Error("plugin UI action target is invalid");
|
|
1701
|
-
const edit = controlEdits.get(targetKey) || { revision: 0, isComposing: false };
|
|
1702
|
-
if (eventType === "input" || eventType === "change") edit.revision += 1;
|
|
1703
|
-
edit.isComposing = Boolean(event.isComposing || edit.isComposing);
|
|
1704
|
-
controlEdits.set(targetKey, edit);
|
|
1705
|
-
const payload = {
|
|
1706
|
-
type: "redevplugin.ui.action",
|
|
1707
|
-
action: element.getAttribute("data-redevplugin-action"),
|
|
1708
|
-
event: eventType,
|
|
1709
|
-
target_key: targetKey,
|
|
1710
|
-
edit_revision: edit.revision,
|
|
1711
|
-
is_composing: edit.isComposing,
|
|
1712
|
-
};
|
|
1713
|
-
const eventTarget = eventType === "click" && element.tagName === "BUTTON" ? element : event.target;
|
|
1714
|
-
if (eventTarget && typeof eventTarget.value === "string") payload.value = eventTarget.value.slice(0, maxTextLength);
|
|
1715
|
-
if (eventTarget && typeof eventTarget.checked === "boolean") payload.checked = eventTarget.checked;
|
|
1716
|
-
if (eventType === "submit" && element.tagName === "FORM") {
|
|
1717
|
-
const values = {};
|
|
1718
|
-
let count = 0;
|
|
1719
|
-
for (const [name, value] of new FormData(element)) {
|
|
1720
|
-
if (typeof value !== "string" || !validIdentifier(name) || count >= maxFormFields) continue;
|
|
1721
|
-
values[name] = value.slice(0, maxTextLength);
|
|
1722
|
-
count += 1;
|
|
1723
|
-
}
|
|
1724
|
-
payload.form_data = values;
|
|
1725
|
-
}
|
|
1726
|
-
return payload;
|
|
1727
|
-
};
|
|
1728
|
-
document.addEventListener("compositionstart", (event) => {
|
|
1729
|
-
const element = event.target instanceof Element ? event.target.closest("[data-redevplugin-key]") : null;
|
|
1730
|
-
const key = element?.getAttribute("data-redevplugin-key");
|
|
1731
|
-
if (!validResourceIdentifier(key)) return;
|
|
1732
|
-
const edit = controlEdits.get(key) || { revision: 0, isComposing: false };
|
|
1733
|
-
edit.isComposing = true;
|
|
1734
|
-
controlEdits.set(key, edit);
|
|
1735
|
-
}, true);
|
|
1736
|
-
document.addEventListener("compositionend", (event) => {
|
|
1737
|
-
const element = event.target instanceof Element ? event.target.closest("[data-redevplugin-key]") : null;
|
|
1738
|
-
const key = element?.getAttribute("data-redevplugin-key");
|
|
1739
|
-
if (!validResourceIdentifier(key)) return;
|
|
1740
|
-
const edit = controlEdits.get(key) || { revision: 0, isComposing: true };
|
|
1741
|
-
edit.isComposing = false;
|
|
1742
|
-
controlEdits.set(key, edit);
|
|
1743
|
-
}, true);
|
|
1744
|
-
const handleAction = (event) => {
|
|
1745
|
-
if (pendingQuiesceID) return;
|
|
1746
|
-
if (!["click", "input", "change", "submit"].includes(event.type)) return;
|
|
1747
|
-
const origin = event.target instanceof Element ? event.target : null;
|
|
1748
|
-
const element = origin ? origin.closest("[data-redevplugin-action]") : null;
|
|
1749
|
-
if (!element || !document.body.contains(element)) return;
|
|
1750
|
-
if (element.tagName === "FORM" && event.type !== "submit") {
|
|
1751
|
-
const submitButton = origin ? origin.closest("button") : null;
|
|
1752
|
-
const buttonType = submitButton && element.contains(submitButton) ? (submitButton.getAttribute("type") || "submit").toLowerCase() : "";
|
|
1753
|
-
if (event.type === "click" && buttonType === "submit") {
|
|
1754
|
-
event.preventDefault();
|
|
1755
|
-
const sequence = sendInteraction("action", event.target, { action: element.getAttribute("data-redevplugin-action") });
|
|
1756
|
-
if (sequence > 0) pendingFileExportAction = { sequence, expires_at: performance.now() + fileExportActionWindowMs };
|
|
1757
|
-
sendWorker(actionPayload(event, element, "submit"));
|
|
1758
|
-
}
|
|
1759
|
-
return;
|
|
1760
|
-
}
|
|
1761
|
-
if (event.type === "submit" || (event.type === "click" && element.tagName === "BUTTON")) event.preventDefault();
|
|
1762
|
-
const action = element.getAttribute("data-redevplugin-action");
|
|
1763
|
-
if (!validIdentifier(action)) return;
|
|
1764
|
-
const sequence = sendInteraction("action", event.target, { action });
|
|
1765
|
-
if (sequence > 0 && (event.type === "click" || event.type === "submit")) {
|
|
1766
|
-
pendingFileExportAction = { sequence, expires_at: performance.now() + fileExportActionWindowMs };
|
|
1767
|
-
}
|
|
1768
|
-
sendWorker(actionPayload(event, element));
|
|
1769
|
-
};
|
|
1770
|
-
for (const eventType of ["click", "input", "change", "submit"]) document.addEventListener(eventType, handleAction, true);
|
|
1771
|
-
const focusableModalElements = (modal) => Array.from(modal.querySelectorAll('button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'))
|
|
1772
|
-
.filter((element) => element instanceof HTMLElement && element.tabIndex >= 0 && element.getClientRects().length > 0);
|
|
1773
|
-
document.addEventListener("keydown", (event) => {
|
|
1774
|
-
if (event.key === "Tab" && !event.defaultPrevented && !event.repeat) {
|
|
1775
|
-
const modal = document.body.querySelector('[role="dialog"][aria-modal="true"]');
|
|
1776
|
-
if (modal instanceof HTMLElement) {
|
|
1777
|
-
const focusable = focusableModalElements(modal);
|
|
1778
|
-
if (focusable.length === 0) {
|
|
1779
|
-
event.preventDefault();
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
const first = focusable[0];
|
|
1783
|
-
const last = focusable[focusable.length - 1];
|
|
1784
|
-
const active = document.activeElement;
|
|
1785
|
-
const target = event.shiftKey
|
|
1786
|
-
? (active === first || !modal.contains(active) ? last : undefined)
|
|
1787
|
-
: (active === last || !modal.contains(active) ? first : undefined);
|
|
1788
|
-
if (target instanceof HTMLElement) {
|
|
1789
|
-
event.preventDefault();
|
|
1790
|
-
event.stopPropagation();
|
|
1791
|
-
try { target.focus({ preventScroll: true }); } catch { target.focus(); }
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
return;
|
|
1795
|
-
}
|
|
1796
|
-
if (event.key !== "Escape" || event.defaultPrevented || event.repeat) return;
|
|
1797
|
-
const origin = event.target instanceof Element ? event.target : document.activeElement instanceof Element ? document.activeElement : null;
|
|
1798
|
-
const owner = origin ? origin.closest("[data-redevplugin-escape-action]") : null;
|
|
1799
|
-
const action = owner?.getAttribute("data-redevplugin-escape-action");
|
|
1800
|
-
if (!owner || !document.body.contains(owner) || !validIdentifier(action)) return;
|
|
1801
|
-
event.preventDefault();
|
|
1802
|
-
event.stopPropagation();
|
|
1803
|
-
const targetKey = owner.getAttribute("data-redevplugin-key");
|
|
1804
|
-
if (!validResourceIdentifier(targetKey)) return;
|
|
1805
|
-
const edit = controlEdits.get(targetKey) || { revision: 0, isComposing: false };
|
|
1806
|
-
sendInteraction("action", origin, { action });
|
|
1807
|
-
sendWorker({
|
|
1808
|
-
type: "redevplugin.ui.action",
|
|
1809
|
-
action,
|
|
1810
|
-
event: "escape",
|
|
1811
|
-
target_key: targetKey,
|
|
1812
|
-
edit_revision: edit.revision,
|
|
1813
|
-
is_composing: edit.isComposing,
|
|
1814
|
-
});
|
|
1815
|
-
}, true);
|
|
1816
|
-
const pumpAssets = () => {
|
|
1817
|
-
while (activeAssetReads < maxConcurrentAssetReads && queuedAssets.length > 0) {
|
|
1818
|
-
const asset = queuedAssets.shift();
|
|
1819
|
-
const requestID = "asset_request_" + (++assetSequence);
|
|
1820
|
-
pendingAssets.set(requestID, asset);
|
|
1821
|
-
activeAssetReads += 1;
|
|
1822
|
-
sendParent({ type: "redevplugin.surface.asset.read", request_id: requestID, binding_id: asset.binding_id, path: asset.path, sha256: asset.sha256 });
|
|
1823
|
-
}
|
|
1824
|
-
};
|
|
1825
|
-
const loadAssets = () => {
|
|
1826
|
-
queuedAssets.push(...currentDocument.assets);
|
|
1827
|
-
pumpAssets();
|
|
1828
|
-
};
|
|
1829
|
-
const failTransferRequest = (id, error) => {
|
|
1830
|
-
pendingImageRequests.delete(id);
|
|
1831
|
-
activeImageDecodes.delete(id);
|
|
1832
|
-
const reservedPixels = imageReservations.get(id);
|
|
1833
|
-
if (reservedPixels !== undefined) {
|
|
1834
|
-
imageReservations.delete(id);
|
|
1835
|
-
retainedImageCount = Math.max(0, retainedImageCount - 1);
|
|
1836
|
-
retainedImagePixels = Math.max(0, retainedImagePixels - reservedPixels);
|
|
1837
|
-
}
|
|
1838
|
-
completeWorkerRequest(id);
|
|
1839
|
-
sendWorker({ type: "redevplugin.bridge.response", id, ok: false, error_code: "PLUGIN_CONTRACT_MISMATCH", error: String(error).slice(0, 512) });
|
|
1840
|
-
};
|
|
1841
|
-
const imageType = (contentType) => String(contentType).split(";", 1)[0].trim().toLowerCase();
|
|
1842
|
-
const bytesMatch = (bytes, offset, values) => values.every((value, index) => bytes[offset + index] === value);
|
|
1843
|
-
const asciiMatches = (bytes, offset, value) => Array.from(value).every((character, index) => bytes[offset + index] === character.charCodeAt(0));
|
|
1844
|
-
const detectRasterImageType = (bytes) => {
|
|
1845
|
-
if (bytes.length >= 8 && bytesMatch(bytes, 0, [137, 80, 78, 71, 13, 10, 26, 10])) return "image/png";
|
|
1846
|
-
if (bytes.length >= 2 && bytesMatch(bytes, 0, [255, 216])) return "image/jpeg";
|
|
1847
|
-
if (bytes.length >= 6 && (asciiMatches(bytes, 0, "GIF87a") || asciiMatches(bytes, 0, "GIF89a"))) return "image/gif";
|
|
1848
|
-
if (bytes.length >= 12 && asciiMatches(bytes, 0, "RIFF") && asciiMatches(bytes, 8, "WEBP")) return "image/webp";
|
|
1849
|
-
return "";
|
|
1850
|
-
};
|
|
1851
|
-
const uint16BE = (bytes, offset) => (bytes[offset] << 8) | bytes[offset + 1];
|
|
1852
|
-
const uint16LE = (bytes, offset) => bytes[offset] | (bytes[offset + 1] << 8);
|
|
1853
|
-
const uint24LE = (bytes, offset) => bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16);
|
|
1854
|
-
const uint32BE = (bytes, offset) => ((bytes[offset] * 0x1000000) + (bytes[offset + 1] << 16) + (bytes[offset + 2] << 8) + bytes[offset + 3]) >>> 0;
|
|
1855
|
-
const readImageDimensions = (bytes, contentType) => {
|
|
1856
|
-
const type = imageType(contentType);
|
|
1857
|
-
let width = 0;
|
|
1858
|
-
let height = 0;
|
|
1859
|
-
if (type === "image/png") {
|
|
1860
|
-
if (bytes.length < 24 || !bytesMatch(bytes, 0, [137, 80, 78, 71, 13, 10, 26, 10]) || !asciiMatches(bytes, 12, "IHDR")) throw new Error("PNG dimensions are invalid");
|
|
1861
|
-
width = uint32BE(bytes, 16);
|
|
1862
|
-
height = uint32BE(bytes, 20);
|
|
1863
|
-
} else if (type === "image/gif") {
|
|
1864
|
-
if (bytes.length < 10 || (!asciiMatches(bytes, 0, "GIF87a") && !asciiMatches(bytes, 0, "GIF89a"))) throw new Error("GIF dimensions are invalid");
|
|
1865
|
-
width = uint16LE(bytes, 6);
|
|
1866
|
-
height = uint16LE(bytes, 8);
|
|
1867
|
-
} else if (type === "image/jpeg") {
|
|
1868
|
-
if (bytes.length < 4 || !bytesMatch(bytes, 0, [255, 216])) throw new Error("JPEG dimensions are invalid");
|
|
1869
|
-
let offset = 2;
|
|
1870
|
-
const startOfFrame = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
|
|
1871
|
-
while (offset + 4 <= bytes.length) {
|
|
1872
|
-
if (bytes[offset] !== 255) throw new Error("JPEG marker sequence is invalid");
|
|
1873
|
-
while (offset < bytes.length && bytes[offset] === 255) offset += 1;
|
|
1874
|
-
const marker = bytes[offset++];
|
|
1875
|
-
if (marker === 217 || marker === 218) break;
|
|
1876
|
-
if (marker === 1 || (marker >= 208 && marker <= 215)) continue;
|
|
1877
|
-
if (offset + 2 > bytes.length) break;
|
|
1878
|
-
const length = uint16BE(bytes, offset);
|
|
1879
|
-
if (length < 2 || offset + length > bytes.length) throw new Error("JPEG segment length is invalid");
|
|
1880
|
-
if (startOfFrame.has(marker)) {
|
|
1881
|
-
if (length < 7) throw new Error("JPEG frame dimensions are invalid");
|
|
1882
|
-
height = uint16BE(bytes, offset + 3);
|
|
1883
|
-
width = uint16BE(bytes, offset + 5);
|
|
1884
|
-
break;
|
|
1885
|
-
}
|
|
1886
|
-
offset += length;
|
|
1887
|
-
}
|
|
1888
|
-
} else if (type === "image/webp") {
|
|
1889
|
-
if (bytes.length < 30 || !asciiMatches(bytes, 0, "RIFF") || !asciiMatches(bytes, 8, "WEBP")) throw new Error("WebP dimensions are invalid");
|
|
1890
|
-
if (asciiMatches(bytes, 12, "VP8X")) {
|
|
1891
|
-
width = uint24LE(bytes, 24) + 1;
|
|
1892
|
-
height = uint24LE(bytes, 27) + 1;
|
|
1893
|
-
} else if (asciiMatches(bytes, 12, "VP8L") && bytes[20] === 47) {
|
|
1894
|
-
width = 1 + bytes[21] + ((bytes[22] & 63) << 8);
|
|
1895
|
-
height = 1 + (bytes[22] >> 6) + (bytes[23] << 2) + ((bytes[24] & 15) << 10);
|
|
1896
|
-
} else if (asciiMatches(bytes, 12, "VP8 ") && bytesMatch(bytes, 23, [157, 1, 42])) {
|
|
1897
|
-
width = uint16LE(bytes, 26) & 0x3fff;
|
|
1898
|
-
height = uint16LE(bytes, 28) & 0x3fff;
|
|
1899
|
-
}
|
|
1900
|
-
} else {
|
|
1901
|
-
throw new Error("plugin raster image type is unsupported");
|
|
1902
|
-
}
|
|
1903
|
-
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0 || width > maxImageDimension || height > maxImageDimension) {
|
|
1904
|
-
throw new Error("decoded image dimensions exceed the renderer budget");
|
|
1905
|
-
}
|
|
1906
|
-
return { width, height, pixels: width * height };
|
|
1907
|
-
};
|
|
1908
|
-
const retainImagePixels = (pixels) => {
|
|
1909
|
-
if (!Number.isSafeInteger(pixels) || pixels <= 0 || retainedImageCount >= maxImageCount || retainedImagePixels + pixels > maxImageTotalPixels) {
|
|
1910
|
-
throw new Error("decoded image assets exceed the renderer pixel budget");
|
|
1911
|
-
}
|
|
1912
|
-
retainedImageCount += 1;
|
|
1913
|
-
retainedImagePixels += pixels;
|
|
1914
|
-
};
|
|
1915
|
-
const deliverImageAsset = async (id, assetID, asset) => {
|
|
1916
|
-
if (!pendingImageRequests.has(id)) pendingImageRequests.set(id, { assetID, asset });
|
|
1917
|
-
const bytes = verifiedAssetBytes.get(asset.binding_id);
|
|
1918
|
-
if (!bytes) {
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
|
-
if (activeImageDecodes.has(id)) return;
|
|
1922
|
-
activeImageDecodes.add(id);
|
|
1923
|
-
let image;
|
|
1924
|
-
try {
|
|
1925
|
-
const rasterType = imageAssetTypes.get(asset.binding_id) || imageType(asset.content_type);
|
|
1926
|
-
const dimensions = imageAssetDimensions.get(asset.binding_id) || readImageDimensions(bytes, rasterType);
|
|
1927
|
-
retainImagePixels(dimensions.pixels);
|
|
1928
|
-
imageReservations.set(id, dimensions.pixels);
|
|
1929
|
-
image = await createImageBitmap(new Blob([bytes], { type: rasterType }));
|
|
1930
|
-
if (!pendingWorkerRequests.has(id) || !pendingImageRequests.has(id)) {
|
|
1931
|
-
image.close();
|
|
1932
|
-
const reservedPixels = imageReservations.get(id);
|
|
1933
|
-
if (reservedPixels !== undefined) {
|
|
1934
|
-
imageReservations.delete(id);
|
|
1935
|
-
retainedImageCount = Math.max(0, retainedImageCount - 1);
|
|
1936
|
-
retainedImagePixels = Math.max(0, retainedImagePixels - reservedPixels);
|
|
1937
|
-
}
|
|
1938
|
-
activeImageDecodes.delete(id);
|
|
1939
|
-
return;
|
|
1940
|
-
}
|
|
1941
|
-
if (!image || image.width !== dimensions.width || image.height !== dimensions.height) throw new Error("decoded image dimensions changed after validation");
|
|
1942
|
-
pendingImageRequests.delete(id);
|
|
1943
|
-
activeImageDecodes.delete(id);
|
|
1944
|
-
sendWorkerTransfer({ type: "redevplugin.ui.asset.image.ready", id, asset_id: assetID, image, width: image.width, height: image.height }, [image]);
|
|
1945
|
-
imageReservations.delete(id);
|
|
1946
|
-
completeWorkerRequest(id);
|
|
1947
|
-
} catch (error) {
|
|
1948
|
-
try { image && image.close(); } catch {}
|
|
1949
|
-
failTransferRequest(id, error && error.message || "plugin image asset could not be decoded");
|
|
1950
|
-
}
|
|
1951
|
-
};
|
|
1952
|
-
const applyAsset = async (asset, contentBase64) => {
|
|
1953
|
-
if (contentBase64.length !== Math.ceil(asset.size / 3) * 4) throw new Error("plugin asset encoded size mismatch");
|
|
1954
|
-
const binary = atob(contentBase64);
|
|
1955
|
-
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
1956
|
-
if (bytes.byteLength !== asset.size) throw new Error("plugin asset size mismatch");
|
|
1957
|
-
const digestBytes = redevpluginIntegrity.sha256(bytes);
|
|
1958
|
-
const actualDigest = "sha256:" + Array.from(digestBytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1959
|
-
if (actualDigest !== asset.sha256) throw new Error("plugin asset bytes failed SHA-256 verification");
|
|
1960
|
-
const declaredType = imageType(asset.content_type);
|
|
1961
|
-
const rasterType = detectRasterImageType(bytes);
|
|
1962
|
-
if (rasterType || (declaredType.startsWith("image/") && declaredType !== "image/svg+xml")) {
|
|
1963
|
-
const validatedType = rasterType || declaredType;
|
|
1964
|
-
const dimensions = readImageDimensions(bytes, validatedType);
|
|
1965
|
-
retainImagePixels(dimensions.pixels);
|
|
1966
|
-
imageAssetDimensions.set(asset.binding_id, dimensions);
|
|
1967
|
-
imageAssetTypes.set(asset.binding_id, validatedType);
|
|
1968
|
-
}
|
|
1969
|
-
verifiedAssetBytes.set(asset.binding_id, bytes);
|
|
1970
|
-
const url = URL.createObjectURL(new Blob([bytes], { type: rasterType || asset.content_type }));
|
|
1971
|
-
blobURLs.add(url);
|
|
1972
|
-
document.documentElement.style.setProperty("--redevplugin-asset-" + asset.binding_id, 'url("' + url.replaceAll('"', '%22') + '")');
|
|
1973
|
-
for (const element of document.querySelectorAll('[data-redevplugin-asset-binding="' + asset.binding_id + '"]')) {
|
|
1974
|
-
const attribute = element.getAttribute("data-redevplugin-asset-attr");
|
|
1975
|
-
if (attribute === "src" || attribute === "poster") element.setAttribute(attribute, url);
|
|
1976
|
-
element.removeAttribute("data-redevplugin-asset-binding");
|
|
1977
|
-
element.removeAttribute("data-redevplugin-asset-attr");
|
|
1978
|
-
}
|
|
1979
|
-
for (const [id, pending] of pendingImageRequests) {
|
|
1980
|
-
if (pending.asset.binding_id === asset.binding_id) void deliverImageAsset(id, pending.assetID, pending.asset);
|
|
1981
|
-
}
|
|
1982
|
-
};
|
|
1983
|
-
const workerRuntime = () => [
|
|
1984
|
-
'{',
|
|
1985
|
-
'"use strict";',
|
|
1986
|
-
'const __rpTimer = setTimeout(() => close(), 30000);',
|
|
1987
|
-
'const __rpListener = (event) => {',
|
|
1988
|
-
' const value = event.data;',
|
|
1989
|
-
' if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== "port_roles,surface_handle,type" || value.type !== "redevplugin.worker.initialize" || typeof value.surface_handle !== "string" || !Array.isArray(value.port_roles) || value.port_roles.join(",") !== "runtime_control,plugin_bridge" || !event.ports || event.ports.length !== 2) return;',
|
|
1990
|
-
' clearTimeout(__rpTimer); removeEventListener("message", __rpListener);',
|
|
1991
|
-
' const __rpSurfaceHandle = value.surface_handle;',
|
|
1992
|
-
' const __rpControlPort = event.ports[0];',
|
|
1993
|
-
' const __rpPort = event.ports[1];',
|
|
1994
|
-
'const __rpControlPost = __rpControlPort.postMessage.bind(__rpControlPort);',
|
|
1995
|
-
'const __rpControlStart = __rpControlPort.start.bind(__rpControlPort);',
|
|
1996
|
-
'const __rpControlClose = __rpControlPort.close.bind(__rpControlPort);',
|
|
1997
|
-
'const __rpControlAddEventListener = __rpControlPort.addEventListener.bind(__rpControlPort);',
|
|
1998
|
-
'const __rpGetPrototypeOf = Object.getPrototypeOf;',
|
|
1999
|
-
'const __rpGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;',
|
|
2000
|
-
'const __rpDefineProperty = Object.defineProperty;',
|
|
2001
|
-
'const __rpHasOwn = Object.prototype.hasOwnProperty;',
|
|
2002
|
-
'const __rpObjectKeys = Object.keys;',
|
|
2003
|
-
'const __rpApply = Reflect.apply;',
|
|
2004
|
-
'const __rpAddEventListener = addEventListener.bind(globalThis);',
|
|
2005
|
-
'const __rpRemoveEventListener = removeEventListener.bind(globalThis);',
|
|
2006
|
-
'const __rpSetTimeout = setTimeout.bind(globalThis);',
|
|
2007
|
-
'const __rpClearTimeout = clearTimeout.bind(globalThis);',
|
|
2008
|
-
'const __rpNumberIsSafeInteger = Number.isSafeInteger.bind(Number);',
|
|
2009
|
-
'const __rpOffscreenCanvas = globalThis.OffscreenCanvas;',
|
|
2010
|
-
'const __rpCanvasPrototype = __rpOffscreenCanvas && __rpOffscreenCanvas.prototype;',
|
|
2011
|
-
'const __rpTrackedCanvases = [];',
|
|
2012
|
-
'const __rpBlocked = () => { throw new TypeError("API is unavailable in the ReDevPlugin worker sandbox"); };',
|
|
2013
|
-
'const __rpSealDescriptor = (owner, name, value) => {',
|
|
2014
|
-
' const descriptor = __rpGetOwnPropertyDescriptor(owner, name);',
|
|
2015
|
-
' if (!descriptor) return;',
|
|
2016
|
-
' if (descriptor.configurable) __rpDefineProperty(owner, name, { configurable: false, enumerable: Boolean(descriptor.enumerable), writable: false, value });',
|
|
2017
|
-
' else if (Object.prototype.hasOwnProperty.call(descriptor, "value") && descriptor.writable) __rpDefineProperty(owner, name, { writable: false, value });',
|
|
2018
|
-
' else if (!Object.prototype.hasOwnProperty.call(descriptor, "value") || descriptor.value !== value) throw new TypeError("Unable to seal worker API " + name);',
|
|
2019
|
-
' const sealed = __rpGetOwnPropertyDescriptor(owner, name);',
|
|
2020
|
-
' if (!sealed || !Object.prototype.hasOwnProperty.call(sealed, "value") || sealed.configurable || sealed.writable || sealed.value !== value) throw new TypeError("Worker API remained available: " + name);',
|
|
2021
|
-
'};',
|
|
2022
|
-
'const __rpSealChain = (target, name, value) => {',
|
|
2023
|
-
' let current = target;',
|
|
2024
|
-
' let found = false;',
|
|
2025
|
-
' while (current) {',
|
|
2026
|
-
' if (__rpHasOwn.call(current, name)) { found = true; __rpSealDescriptor(current, name, value); }',
|
|
2027
|
-
' current = __rpGetPrototypeOf(current);',
|
|
2028
|
-
' }',
|
|
2029
|
-
' if (!found || !__rpHasOwn.call(target, name)) __rpDefineProperty(target, name, { configurable: false, enumerable: false, writable: false, value });',
|
|
2030
|
-
' __rpSealDescriptor(target, name, value);',
|
|
2031
|
-
'};',
|
|
2032
|
-
'const __rpSealMessagePortMethod = (name) => {',
|
|
2033
|
-
' let current = __rpGetPrototypeOf(__rpControlPort);',
|
|
2034
|
-
' let found = false;',
|
|
2035
|
-
' while (current) {',
|
|
2036
|
-
' const descriptor = __rpGetOwnPropertyDescriptor(current, name);',
|
|
2037
|
-
' if (descriptor && __rpHasOwn.call(descriptor, "value") && typeof descriptor.value === "function") { found = true; __rpSealDescriptor(current, name, descriptor.value); }',
|
|
2038
|
-
' current = __rpGetPrototypeOf(current);',
|
|
2039
|
-
' }',
|
|
2040
|
-
' if (!found) throw new TypeError("MessagePort method is unavailable: " + name);',
|
|
2041
|
-
'};',
|
|
2042
|
-
'const __rpTrackCanvas = (canvas) => {',
|
|
2043
|
-
' for (let index = 0; index < __rpTrackedCanvases.length; index += 1) if (__rpTrackedCanvases[index] === canvas) return;',
|
|
2044
|
-
' if (__rpTrackedCanvases.length >= ' + JSON.stringify(maxCanvasCount) + ') throw new RangeError("plugin canvas count exceeds the worker budget");',
|
|
2045
|
-
' __rpTrackedCanvases.push(canvas);',
|
|
2046
|
-
'};',
|
|
2047
|
-
'const __rpInstallCanvasBudget = () => {',
|
|
2048
|
-
' if (!__rpCanvasPrototype) return;',
|
|
2049
|
-
' const widthDescriptor = __rpGetOwnPropertyDescriptor(__rpCanvasPrototype, "width");',
|
|
2050
|
-
' const heightDescriptor = __rpGetOwnPropertyDescriptor(__rpCanvasPrototype, "height");',
|
|
2051
|
-
' const contextDescriptor = __rpGetOwnPropertyDescriptor(__rpCanvasPrototype, "getContext");',
|
|
2052
|
-
' const convertDescriptor = __rpGetOwnPropertyDescriptor(__rpCanvasPrototype, "convertToBlob");',
|
|
2053
|
-
' if (!widthDescriptor || !heightDescriptor || typeof widthDescriptor.get !== "function" || typeof widthDescriptor.set !== "function" || typeof heightDescriptor.get !== "function" || typeof heightDescriptor.set !== "function" || !contextDescriptor || typeof contextDescriptor.value !== "function" || !convertDescriptor || typeof convertDescriptor.value !== "function") throw new TypeError("OffscreenCanvas descriptors are unavailable");',
|
|
2054
|
-
' const readWidth = (canvas) => __rpApply(widthDescriptor.get, canvas, []);',
|
|
2055
|
-
' const readHeight = (canvas) => __rpApply(heightDescriptor.get, canvas, []);',
|
|
2056
|
-
' const validateResize = (canvas, axis, value) => {',
|
|
2057
|
-
' if (!__rpNumberIsSafeInteger(value) || value <= 0 || value > ' + JSON.stringify(maxCanvasDimension) + ') throw new RangeError("plugin canvas resize exceeds the worker dimension budget");',
|
|
2058
|
-
' __rpTrackCanvas(canvas);',
|
|
2059
|
-
' let pixels = 0;',
|
|
2060
|
-
' for (let index = 0; index < __rpTrackedCanvases.length; index += 1) {',
|
|
2061
|
-
' const current = __rpTrackedCanvases[index];',
|
|
2062
|
-
' const width = current === canvas && axis === "width" ? value : readWidth(current);',
|
|
2063
|
-
' const height = current === canvas && axis === "height" ? value : readHeight(current);',
|
|
2064
|
-
' pixels += width * height;',
|
|
2065
|
-
' if (pixels > ' + JSON.stringify(maxCanvasTotalPixels) + ') throw new RangeError("plugin canvas resize exceeds the worker pixel budget");',
|
|
2066
|
-
' }',
|
|
2067
|
-
' };',
|
|
2068
|
-
' __rpDefineProperty(__rpCanvasPrototype, "width", { configurable: false, enumerable: Boolean(widthDescriptor.enumerable), get() { return readWidth(this); }, set(value) { validateResize(this, "width", value); __rpApply(widthDescriptor.set, this, [value]); } });',
|
|
2069
|
-
' __rpDefineProperty(__rpCanvasPrototype, "height", { configurable: false, enumerable: Boolean(heightDescriptor.enumerable), get() { return readHeight(this); }, set(value) { validateResize(this, "height", value); __rpApply(heightDescriptor.set, this, [value]); } });',
|
|
2070
|
-
' __rpDefineProperty(__rpCanvasPrototype, "getContext", { configurable: false, enumerable: Boolean(contextDescriptor.enumerable), writable: false, value(type, ...options) { if (${opaqueSurfaceCanvasContextTypes.map((type) => 'type !== ' + JSON.stringify(type)).join(' && ')}) throw new TypeError("canvas context type is unavailable in the ReDevPlugin worker sandbox"); __rpTrackCanvas(this); return __rpApply(contextDescriptor.value, this, [type, ...options]); } });',
|
|
2071
|
-
' __rpSealDescriptor(__rpCanvasPrototype, "constructor", undefined);',
|
|
2072
|
-
' __rpSealDescriptor(__rpCanvasPrototype, "convertToBlob", convertDescriptor.value);',
|
|
2073
|
-
' __rpSealDescriptor(__rpCanvasPrototype, "transferToImageBitmap", __rpBlocked);',
|
|
2074
|
-
'};',
|
|
2075
|
-
'try {',
|
|
2076
|
-
' for (const name of ["postMessage", "start", "close", "addEventListener", "removeEventListener"]) __rpSealMessagePortMethod(name);',
|
|
2077
|
-
' __rpInstallCanvasBudget();',
|
|
2078
|
-
' for (const [name, value] of Object.entries({fetch:__rpBlocked,XMLHttpRequest:undefined,WebSocket:undefined,EventSource:undefined,WebTransport:undefined,Worker:undefined,SharedWorker:undefined,indexedDB:undefined,caches:undefined,RTCPeerConnection:undefined,webkitRTCPeerConnection:undefined,BroadcastChannel:undefined,importScripts:undefined,postMessage:undefined,eval:undefined,Function:undefined,Blob:undefined,File:undefined,FileReader:undefined,FileReaderSync:undefined,OffscreenCanvas:undefined,ImageBitmap:undefined,createImageBitmap:undefined})) __rpSealChain(globalThis, name, value);',
|
|
2079
|
-
' for (const [name, value] of Object.entries({storage:undefined,sendBeacon:undefined,serviceWorker:undefined})) __rpSealChain(navigator, name, value);',
|
|
2080
|
-
' const __rpFunctionPrototypes = [__rpGetPrototypeOf(function() {}), __rpGetPrototypeOf(async function() {}), __rpGetPrototypeOf(function*() {}), __rpGetPrototypeOf(async function*() {})];',
|
|
2081
|
-
' for (const prototype of __rpFunctionPrototypes) __rpSealDescriptor(prototype, "constructor", undefined);',
|
|
2082
|
-
'} catch (error) {',
|
|
2083
|
-
' try { __rpControlPost({ type: "redevplugin.worker.error", error: String(error && error.message || error).slice(0, 512) }); } catch {}',
|
|
2084
|
-
' try { __rpControlClose(); } catch {}',
|
|
2085
|
-
' try { __rpPort.close(); } catch {}',
|
|
2086
|
-
' close();',
|
|
2087
|
-
' return;',
|
|
2088
|
-
'}',
|
|
2089
|
-
'let __rpClaimed = false;',
|
|
2090
|
-
'const __rpBridge = Object.freeze({ claim() { if (__rpClaimed) return undefined; __rpClaimed = true; return Object.freeze({ surfaceHandle: __rpSurfaceHandle, port: __rpPort }); } });',
|
|
2091
|
-
'Object.defineProperty(globalThis, ' + JSON.stringify(workerGlobalKey) + ', { configurable: false, enumerable: false, writable: false, value: __rpBridge });',
|
|
2092
|
-
'__rpControlStart();',
|
|
2093
|
-
'__rpPort.start();',
|
|
2094
|
-
'const __rpControlListener = (event) => {',
|
|
2095
|
-
' const message = event.data;',
|
|
2096
|
-
' if (!message || typeof message !== "object" || Array.isArray(message)) return;',
|
|
2097
|
-
' const keys = __rpObjectKeys(message).sort();',
|
|
2098
|
-
' if (keys.length !== 2 || keys[0] !== "ping_id" || keys[1] !== "type" || message.type !== "redevplugin.worker.ping" || typeof message.ping_id !== "string" || !/^ping_[1-9][0-9]{0,15}$/.test(message.ping_id)) return;',
|
|
2099
|
-
' __rpControlPost({ type: "redevplugin.worker.pong", ping_id: message.ping_id });',
|
|
2100
|
-
'};',
|
|
2101
|
-
'__rpControlAddEventListener("message", __rpControlListener);',
|
|
2102
|
-
'let __rpFailed = false;',
|
|
2103
|
-
'const __rpReportFailure = (error) => { if (__rpFailed) return; __rpFailed = true; __rpControlPost({ type: "redevplugin.worker.error", error: String(error && error.message || error).slice(0, 512) }); };',
|
|
2104
|
-
'addEventListener("error", (event) => __rpReportFailure(event.error || event.message || "plugin worker failed"), { capture: true });',
|
|
2105
|
-
'addEventListener("unhandledrejection", (event) => __rpReportFailure(event.reason || "plugin worker rejected"), { capture: true });',
|
|
2106
|
-
'void (async () => {',
|
|
2107
|
-
' try {',
|
|
2108
|
-
' __redevpluginPluginMain();',
|
|
2109
|
-
' setTimeout(() => { if (!__rpFailed) __rpControlPost({ type: "redevplugin.worker.ready" }); }, 0);',
|
|
2110
|
-
' } catch (error) {',
|
|
2111
|
-
' __rpReportFailure(error);',
|
|
2112
|
-
' }',
|
|
2113
|
-
'})();',
|
|
2114
|
-
'};',
|
|
2115
|
-
'addEventListener("message", __rpListener);',
|
|
2116
|
-
'}'
|
|
2117
|
-
].join("\\n");
|
|
2118
|
-
const startWorker = (surfaceDocument) => {
|
|
2119
|
-
const source = 'const __redevpluginPluginMain = () => {\\n"use strict";\\n' + surfaceDocument.worker.content + '\\n};\\n' + workerRuntime();
|
|
2120
|
-
const url = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
|
|
2121
|
-
blobURLs.add(url);
|
|
2122
|
-
worker = new Worker(url, { name: "redevplugin-surface" });
|
|
2123
|
-
worker.addEventListener("error", (event) => fail(event.message || "plugin worker failed to load"));
|
|
2124
|
-
worker.addEventListener("messageerror", () => fail("plugin worker message could not be decoded"));
|
|
2125
|
-
const controlChannel = new MessageChannel();
|
|
2126
|
-
const pluginChannel = new MessageChannel();
|
|
2127
|
-
workerControlPort = controlChannel.port1;
|
|
2128
|
-
workerControlPort.addEventListener("message", onWorkerControlMessage);
|
|
2129
|
-
workerControlPort.start();
|
|
2130
|
-
workerPort = pluginChannel.port1;
|
|
2131
|
-
workerPort.addEventListener("message", onWorkerMessage);
|
|
2132
|
-
workerPort.start();
|
|
2133
|
-
worker.postMessage({ type: "redevplugin.worker.initialize", surface_handle: surfaceHandle, port_roles: ["runtime_control", "plugin_bridge"] }, [controlChannel.port2, pluginChannel.port2]);
|
|
2134
|
-
if (currentContext) sendWorker({ type: "redevplugin.bridge.context", context: currentContext });
|
|
2135
|
-
};
|
|
2136
|
-
const validCall = (value) => exactKeys(value, ["type", "request"]) && value.type === "redevplugin.bridge.call" && isRecord(value.request) && Object.keys(value.request).every((key) => ["id", "method", "params"].includes(key)) && typeof value.request.id === "string" && value.request.id.length <= 128 && typeof value.request.method === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(value.request.method) && (value.request.params === undefined || isRecord(value.request.params));
|
|
2137
|
-
const requestID = (value, expectedKind) => {
|
|
2138
|
-
if (typeof value !== "string") return undefined;
|
|
2139
|
-
const match = /^(rpc|execution|render|canvas|asset|keyboard|file)_([1-9][0-9]{0,15})$/.exec(value);
|
|
2140
|
-
if (!match || match[1] !== expectedKind) return undefined;
|
|
2141
|
-
const sequence = Number(match[2]);
|
|
2142
|
-
return Number.isSafeInteger(sequence) ? { kind: match[1], sequence } : undefined;
|
|
2143
|
-
};
|
|
2144
|
-
const acceptWorkerRequest = (id, kind) => {
|
|
2145
|
-
const parsed = requestID(id, kind);
|
|
2146
|
-
if (!parsed || parsed.sequence <= requestSequence[kind] || pendingWorkerRequests.size >= maxInFlightRequests) return false;
|
|
2147
|
-
requestSequence[kind] = parsed.sequence;
|
|
2148
|
-
pendingWorkerRequests.add(id);
|
|
2149
|
-
return true;
|
|
2150
|
-
};
|
|
2151
|
-
const completeWorkerRequest = (id) => pendingWorkerRequests.delete(id);
|
|
2152
|
-
const validKeyboardBinding = (value) => exactKeys(value, ["binding_id", "event", "code", "repeat", "alt_key", "ctrl_key", "meta_key", "shift_key", "target_key", "target_kind"]) &&
|
|
2153
|
-
validIdentifier(value.binding_id) && ["keydown", "keyup"].includes(value.event) &&
|
|
2154
|
-
typeof value.code === "string" && value.code.length > 0 && value.code.length <= 64 &&
|
|
2155
|
-
typeof value.repeat === "boolean" && typeof value.alt_key === "boolean" && typeof value.ctrl_key === "boolean" &&
|
|
2156
|
-
typeof value.meta_key === "boolean" && typeof value.shift_key === "boolean" &&
|
|
2157
|
-
(value.target_key === null || validResourceIdentifier(value.target_key)) &&
|
|
2158
|
-
["canvas", "editable", "control", "surface"].includes(value.target_kind);
|
|
2159
|
-
const validKeyboardBindings = (value) => {
|
|
2160
|
-
if (!Array.isArray(value) || value.length > maxKeyboardBindings || !value.every(validKeyboardBinding)) return false;
|
|
2161
|
-
const ids = new Set();
|
|
2162
|
-
const matchers = new Set();
|
|
2163
|
-
for (const binding of value) {
|
|
2164
|
-
if (ids.has(binding.binding_id)) return false;
|
|
2165
|
-
ids.add(binding.binding_id);
|
|
2166
|
-
const matcher = JSON.stringify([binding.event, binding.code, binding.repeat, binding.alt_key, binding.ctrl_key, binding.meta_key, binding.shift_key, binding.target_key, binding.target_kind]);
|
|
2167
|
-
if (matchers.has(matcher)) return false;
|
|
2168
|
-
matchers.add(matcher);
|
|
2169
|
-
}
|
|
2170
|
-
return true;
|
|
2171
|
-
};
|
|
2172
|
-
const completeExecutionQueryRequest = (id) => {
|
|
2173
|
-
const executionID = executionQueryRequests.get(id);
|
|
2174
|
-
executionQueryRequests.delete(id);
|
|
2175
|
-
const state = executionQueryStates.get(executionID);
|
|
2176
|
-
if (state) { state.inFlight = false; state.lastSeen = performance.now(); }
|
|
2177
|
-
completeWorkerRequest(id);
|
|
2178
|
-
};
|
|
2179
|
-
const refillSnapshotTokens = (tokens, refillAt, rate, burst, now) => ({
|
|
2180
|
-
tokens: Math.min(burst, tokens + Math.max(0, now - refillAt) * rate / 1000),
|
|
2181
|
-
refillAt: Math.max(refillAt, now),
|
|
2182
|
-
});
|
|
2183
|
-
const executionQueryRateLimit = (executionID, requestID) => {
|
|
2184
|
-
const now = performance.now();
|
|
2185
|
-
const surface = refillSnapshotTokens(executionQuerySurfaceTokens, executionQuerySurfaceRefillAt, 8, 8, now);
|
|
2186
|
-
executionQuerySurfaceTokens = surface.tokens;
|
|
2187
|
-
executionQuerySurfaceRefillAt = surface.refillAt;
|
|
2188
|
-
if (executionQuerySurfaceTokens < 1) return Math.max(500, Math.min(10000, Math.ceil((1 - executionQuerySurfaceTokens) / 8 * 1000)));
|
|
2189
|
-
executionQuerySurfaceTokens -= 1;
|
|
2190
|
-
let existing = executionQueryStates.get(executionID);
|
|
2191
|
-
if (!existing) {
|
|
2192
|
-
if (now >= executionQueryNextPruneAt) {
|
|
2193
|
-
for (const [retainedExecutionID, state] of executionQueryStates) {
|
|
2194
|
-
if (!state.inFlight && now - state.lastSeen >= executionQueryStateIdleMS) executionQueryStates.delete(retainedExecutionID);
|
|
2195
|
-
}
|
|
2196
|
-
executionQueryNextPruneAt = now + 30000;
|
|
2197
|
-
}
|
|
2198
|
-
if (executionQueryStates.size >= maxExecutionQueryStates) return 10000;
|
|
2199
|
-
existing = { tokens: 2, refillAt: now, lastSeen: now, inFlight: false };
|
|
2200
|
-
executionQueryStates.set(executionID, existing);
|
|
2201
|
-
}
|
|
2202
|
-
const operation = refillSnapshotTokens(existing.tokens, existing.refillAt, 2, 2, now);
|
|
2203
|
-
existing.tokens = operation.tokens;
|
|
2204
|
-
existing.refillAt = operation.refillAt;
|
|
2205
|
-
existing.lastSeen = now;
|
|
2206
|
-
let retryAfterMS = existing.inFlight ? 500 : 0;
|
|
2207
|
-
if (existing.tokens < 1) retryAfterMS = Math.max(retryAfterMS, Math.ceil((1 - existing.tokens) / 2 * 1000));
|
|
2208
|
-
if (retryAfterMS > 0) return Math.max(500, Math.min(10000, retryAfterMS));
|
|
2209
|
-
existing.tokens -= 1;
|
|
2210
|
-
existing.inFlight = true;
|
|
2211
|
-
executionQueryRequests.set(requestID, executionID);
|
|
2212
|
-
return 0;
|
|
2213
|
-
};
|
|
2214
|
-
const renderRateAllowed = () => {
|
|
2215
|
-
const now = performance.now();
|
|
2216
|
-
if (now - renderWindowStartedAt >= 1000) { renderWindowStartedAt = now; renderCount = 0; }
|
|
2217
|
-
if (renderCount >= maxRendersPerSecond) return false;
|
|
2218
|
-
renderCount += 1;
|
|
2219
|
-
return true;
|
|
2220
|
-
};
|
|
2221
|
-
const rejectWorkerRequest = (id, error, errorCode = "PLUGIN_INVALID_REQUEST") => sendWorker({ type: "redevplugin.bridge.response", id, ok: false, error_code: errorCode, error });
|
|
2222
|
-
const findCanvas = (canvasID) => {
|
|
2223
|
-
for (const element of document.querySelectorAll("canvas[data-redevplugin-canvas]")) {
|
|
2224
|
-
if (element.getAttribute("data-redevplugin-canvas") === canvasID) return element;
|
|
2225
|
-
}
|
|
2226
|
-
return undefined;
|
|
2227
|
-
};
|
|
2228
|
-
const updateCanvasAccessibility = (id, canvasID, label, description) => {
|
|
2229
|
-
const runtime = canvasRuntimes.get(canvasID);
|
|
2230
|
-
if (!runtime || typeof label !== "string" || label.length < 1 || label.length > 256 || typeof description !== "string" || description.length > 1024) {
|
|
2231
|
-
completeWorkerRequest(id);
|
|
2232
|
-
return rejectWorkerRequest(id, "plugin canvas accessibility state is invalid");
|
|
2233
|
-
}
|
|
2234
|
-
if (!runtime.description) {
|
|
2235
|
-
const status = document.createElement("span");
|
|
2236
|
-
status.id = "redevplugin-canvas-description-" + canvasID.replace(/[^A-Za-z0-9_-]/g, "-");
|
|
2237
|
-
status.style.position = "absolute";
|
|
2238
|
-
status.style.width = "1px";
|
|
2239
|
-
status.style.height = "1px";
|
|
2240
|
-
status.style.padding = "0";
|
|
2241
|
-
status.style.margin = "-1px";
|
|
2242
|
-
status.style.overflow = "hidden";
|
|
2243
|
-
status.style.clip = "rect(0, 0, 0, 0)";
|
|
2244
|
-
status.style.whiteSpace = "nowrap";
|
|
2245
|
-
status.style.border = "0";
|
|
2246
|
-
runtime.canvas.insertAdjacentElement("afterend", status);
|
|
2247
|
-
runtime.canvas.setAttribute("aria-describedby", status.id);
|
|
2248
|
-
runtime.description = status;
|
|
2249
|
-
}
|
|
2250
|
-
runtime.canvas.setAttribute("aria-label", label);
|
|
2251
|
-
runtime.description.textContent = description;
|
|
2252
|
-
completeWorkerRequest(id);
|
|
2253
|
-
sendWorker({ type: "redevplugin.bridge.response", id, ok: true });
|
|
2254
|
-
};
|
|
2255
|
-
const canvasMetrics = (canvas) => {
|
|
2256
|
-
const rect = canvas.getBoundingClientRect();
|
|
2257
|
-
if (rect.width === 0 || rect.height === 0) return undefined;
|
|
2258
|
-
const cssWidth = Math.ceil(rect.width);
|
|
2259
|
-
const cssHeight = Math.ceil(rect.height);
|
|
2260
|
-
const devicePixelRatio = Math.min(4, Math.max(0.5, Number(globalThis.devicePixelRatio) || 1));
|
|
2261
|
-
if (!Number.isSafeInteger(cssWidth) || !Number.isSafeInteger(cssHeight) || cssWidth <= 0 || cssHeight <= 0 || cssWidth > maxCanvasDimension || cssHeight > maxCanvasDimension) {
|
|
2262
|
-
throw new Error("plugin canvas dimensions exceed the renderer budget");
|
|
2263
|
-
}
|
|
2264
|
-
const pixelWidth = Math.ceil(cssWidth * devicePixelRatio);
|
|
2265
|
-
const pixelHeight = Math.ceil(cssHeight * devicePixelRatio);
|
|
2266
|
-
if (pixelWidth > maxCanvasDimension || pixelHeight > maxCanvasDimension) throw new Error("plugin canvas dimensions exceed the renderer budget");
|
|
2267
|
-
const pixelCount = pixelWidth * pixelHeight;
|
|
2268
|
-
return { cssWidth, cssHeight, devicePixelRatio, pixelCount };
|
|
2269
|
-
};
|
|
2270
|
-
const openCanvas = (id, canvasID) => {
|
|
2271
|
-
const canvas = findCanvas(canvasID);
|
|
2272
|
-
if (!canvas || canvasRuntimes.has(canvasID) || typeof canvas.transferControlToOffscreen !== "function" || typeof ResizeObserver !== "function") {
|
|
2273
|
-
completeWorkerRequest(id);
|
|
2274
|
-
return rejectWorkerRequest(id, "plugin canvas is missing, already transferred, or unsupported");
|
|
2275
|
-
}
|
|
2276
|
-
if (canvasRuntimes.size >= maxCanvasCount) {
|
|
2277
|
-
completeWorkerRequest(id);
|
|
2278
|
-
return rejectWorkerRequest(id, "plugin canvas resources exceed renderer limits");
|
|
2279
|
-
}
|
|
2280
|
-
if (canvas.tabIndex < 0) canvas.tabIndex = 0;
|
|
2281
|
-
let pointerWindowStartedAt = 0;
|
|
2282
|
-
let pointerEvents = 0;
|
|
2283
|
-
const listeners = [];
|
|
2284
|
-
const listen = (type, handler) => {
|
|
2285
|
-
canvas.addEventListener(type, handler, { passive: false });
|
|
2286
|
-
listeners.push([type, handler]);
|
|
2287
|
-
};
|
|
2288
|
-
const sendInput = (event) => sendWorker({ type: "redevplugin.ui.canvas.input", canvas_id: canvasID, event });
|
|
2289
|
-
const sendResize = () => {
|
|
2290
|
-
if (runtimeDisposed || canvasRuntimes.get(canvasID) !== runtime) return;
|
|
2291
|
-
const metrics = canvasMetrics(canvas);
|
|
2292
|
-
if (!metrics) return;
|
|
2293
|
-
let totalPixels = metrics.pixelCount;
|
|
2294
|
-
for (const [identifier, active] of canvasRuntimes) {
|
|
2295
|
-
if (identifier !== canvasID) totalPixels += active.metrics?.pixelCount ?? 0;
|
|
2296
|
-
}
|
|
2297
|
-
if (totalPixels > maxCanvasTotalPixels) throw new Error("plugin canvases exceed the renderer pixel budget");
|
|
2298
|
-
if (runtime.openRequestID !== undefined) {
|
|
2299
|
-
const offscreen = canvas.transferControlToOffscreen();
|
|
2300
|
-
runtime.metrics = metrics;
|
|
2301
|
-
runtime.openRequestID = undefined;
|
|
2302
|
-
completeWorkerRequest(id);
|
|
2303
|
-
sendWorkerTransfer({ type: "redevplugin.ui.canvas.ready", id, canvas_id: canvasID, canvas: offscreen, css_width: metrics.cssWidth, css_height: metrics.cssHeight, device_pixel_ratio: metrics.devicePixelRatio }, [offscreen]);
|
|
2304
|
-
return;
|
|
2305
|
-
}
|
|
2306
|
-
if (runtime.metrics?.cssWidth === metrics.cssWidth && runtime.metrics?.cssHeight === metrics.cssHeight && runtime.metrics?.devicePixelRatio === metrics.devicePixelRatio) return;
|
|
2307
|
-
runtime.metrics = metrics;
|
|
2308
|
-
sendInput({ type: "resize", css_width: metrics.cssWidth, css_height: metrics.cssHeight, device_pixel_ratio: metrics.devicePixelRatio });
|
|
2309
|
-
};
|
|
2310
|
-
const updateCanvas = () => {
|
|
2311
|
-
try { sendResize(); }
|
|
2312
|
-
catch (error) {
|
|
2313
|
-
if (runtime.openRequestID === undefined) return fail(error && error.message || "plugin canvas resize exceeded renderer limits");
|
|
2314
|
-
dispose();
|
|
2315
|
-
canvasRuntimes.delete(canvasID);
|
|
2316
|
-
completeWorkerRequest(id);
|
|
2317
|
-
rejectWorkerRequest(id, String(error && error.message || "plugin canvas transfer failed").slice(0, 512));
|
|
2318
|
-
}
|
|
2319
|
-
};
|
|
2320
|
-
const handlePointer = (event) => {
|
|
2321
|
-
const now = performance.now();
|
|
2322
|
-
if (now - pointerWindowStartedAt >= 1000) { pointerWindowStartedAt = now; pointerEvents = 0; }
|
|
2323
|
-
if (event.type === "pointermove") {
|
|
2324
|
-
if (pointerEvents >= maxCanvasPointerEventsPerSecond) return;
|
|
2325
|
-
pointerEvents += 1;
|
|
2326
|
-
}
|
|
2327
|
-
event.preventDefault();
|
|
2328
|
-
if (event.type === "pointerdown") {
|
|
2329
|
-
try { canvas.focus({ preventScroll: true }); } catch { canvas.focus(); }
|
|
2330
|
-
try { canvas.setPointerCapture(event.pointerId); } catch {}
|
|
2331
|
-
}
|
|
2332
|
-
if (event.type === "pointerup" || event.type === "pointercancel") {
|
|
2333
|
-
try { canvas.releasePointerCapture(event.pointerId); } catch {}
|
|
2334
|
-
}
|
|
2335
|
-
const rect = canvas.getBoundingClientRect();
|
|
2336
|
-
sendInput({
|
|
2337
|
-
type: "pointer",
|
|
2338
|
-
event: event.type,
|
|
2339
|
-
pointer_id: Math.max(0, Number.isSafeInteger(event.pointerId) ? event.pointerId : 0),
|
|
2340
|
-
pointer_type: ["mouse", "pen", "touch"].includes(event.pointerType) ? event.pointerType : "unknown",
|
|
2341
|
-
buttons: Math.min(31, Math.max(0, Number.isSafeInteger(event.buttons) ? event.buttons : 0)),
|
|
2342
|
-
button: Math.min(4, Math.max(-1, Number.isSafeInteger(event.button) ? event.button : -1)),
|
|
2343
|
-
x: Math.min(32768, Math.max(-16384, event.clientX - rect.left)),
|
|
2344
|
-
y: Math.min(32768, Math.max(-16384, event.clientY - rect.top)),
|
|
2345
|
-
pressure: Math.min(1, Math.max(0, Number.isFinite(event.pressure) ? event.pressure : 0)),
|
|
2346
|
-
});
|
|
2347
|
-
};
|
|
2348
|
-
const handleKey = (event) => {
|
|
2349
|
-
event.preventDefault();
|
|
2350
|
-
sendInput({
|
|
2351
|
-
type: "key",
|
|
2352
|
-
event: event.type,
|
|
2353
|
-
code: String(event.code || "").slice(0, 64),
|
|
2354
|
-
key: String(event.key || "").slice(0, 64),
|
|
2355
|
-
repeat: Boolean(event.repeat),
|
|
2356
|
-
alt_key: Boolean(event.altKey),
|
|
2357
|
-
ctrl_key: Boolean(event.ctrlKey),
|
|
2358
|
-
meta_key: Boolean(event.metaKey),
|
|
2359
|
-
shift_key: Boolean(event.shiftKey),
|
|
2360
|
-
});
|
|
2361
|
-
};
|
|
2362
|
-
const handleWheel = (event) => {
|
|
2363
|
-
event.preventDefault();
|
|
2364
|
-
const rect = canvas.getBoundingClientRect();
|
|
2365
|
-
sendInput({
|
|
2366
|
-
type: "wheel",
|
|
2367
|
-
x: Math.min(32768, Math.max(-16384, event.clientX - rect.left)),
|
|
2368
|
-
y: Math.min(32768, Math.max(-16384, event.clientY - rect.top)),
|
|
2369
|
-
delta_x: Math.min(32768, Math.max(-32768, Number.isFinite(event.deltaX) ? event.deltaX : 0)),
|
|
2370
|
-
delta_y: Math.min(32768, Math.max(-32768, Number.isFinite(event.deltaY) ? event.deltaY : 0)),
|
|
2371
|
-
delta_mode: [0, 1, 2].includes(event.deltaMode) ? event.deltaMode : 0,
|
|
2372
|
-
alt_key: Boolean(event.altKey),
|
|
2373
|
-
ctrl_key: Boolean(event.ctrlKey),
|
|
2374
|
-
meta_key: Boolean(event.metaKey),
|
|
2375
|
-
shift_key: Boolean(event.shiftKey),
|
|
2376
|
-
});
|
|
2377
|
-
};
|
|
2378
|
-
for (const type of ["pointerdown", "pointermove", "pointerup", "pointercancel"]) listen(type, handlePointer);
|
|
2379
|
-
for (const type of ["keydown", "keyup"]) listen(type, handleKey);
|
|
2380
|
-
listen("wheel", handleWheel);
|
|
2381
|
-
listen("focus", () => sendInput({ type: "focus" }));
|
|
2382
|
-
listen("blur", () => sendInput({ type: "blur" }));
|
|
2383
|
-
const observer = new ResizeObserver(updateCanvas);
|
|
2384
|
-
const runtime = { canvas, description: undefined, dispose: undefined, metrics: undefined, openRequestID: id };
|
|
2385
|
-
const dispose = () => {
|
|
2386
|
-
observer.disconnect();
|
|
2387
|
-
for (const [type, handler] of listeners) canvas.removeEventListener(type, handler);
|
|
2388
|
-
runtime.description?.remove();
|
|
2389
|
-
};
|
|
2390
|
-
runtime.dispose = dispose;
|
|
2391
|
-
canvasRuntimes.set(canvasID, runtime);
|
|
2392
|
-
observer.observe(canvas);
|
|
2393
|
-
updateCanvas();
|
|
2394
|
-
};
|
|
2395
|
-
const scheduleWorkerHeartbeat = () => {
|
|
2396
|
-
if (workerHeartbeatTimer) clearTimeout(workerHeartbeatTimer);
|
|
2397
|
-
workerHeartbeatTimer = undefined;
|
|
2398
|
-
if (!workerReady || !workerControlPort || workerHeartbeatPendingID) return;
|
|
2399
|
-
workerHeartbeatTimer = setTimeout(() => {
|
|
2400
|
-
workerHeartbeatTimer = undefined;
|
|
2401
|
-
if (!workerReady || !workerControlPort || workerHeartbeatPendingID) return;
|
|
2402
|
-
const pingID = "ping_" + (++workerHeartbeatSequence);
|
|
2403
|
-
workerHeartbeatPendingID = pingID;
|
|
2404
|
-
try {
|
|
2405
|
-
workerControlPort.postMessage({ type: "redevplugin.worker.ping", ping_id: pingID });
|
|
2406
|
-
} catch {
|
|
2407
|
-
fail("plugin worker heartbeat could not be sent");
|
|
2408
|
-
return;
|
|
2409
|
-
}
|
|
2410
|
-
workerHeartbeatTimeout = setTimeout(() => {
|
|
2411
|
-
workerHeartbeatTimeout = undefined;
|
|
2412
|
-
if (workerHeartbeatPendingID !== pingID) return;
|
|
2413
|
-
workerHeartbeatPendingID = undefined;
|
|
2414
|
-
fail("plugin worker heartbeat timed out");
|
|
2415
|
-
}, workerHeartbeatTimeoutMs);
|
|
2416
|
-
}, workerHeartbeatIntervalMs);
|
|
2417
|
-
};
|
|
2418
|
-
const onWorkerControlMessage = (event) => {
|
|
2419
|
-
if (!withinLimit(event.data)) return;
|
|
2420
|
-
const message = event.data;
|
|
2421
|
-
if (exactKeys(message, ["type"]) && message.type === "redevplugin.worker.ready") {
|
|
2422
|
-
if (workerReady) return;
|
|
2423
|
-
workerReady = true;
|
|
2424
|
-
sendParent({ type: "redevplugin.surface.worker_ready" });
|
|
2425
|
-
scheduleWorkerHeartbeat();
|
|
2426
|
-
return;
|
|
2427
|
-
}
|
|
2428
|
-
if (exactKeys(message, ["type", "ping_id"]) && message.type === "redevplugin.worker.pong" && message.ping_id === workerHeartbeatPendingID) {
|
|
2429
|
-
if (workerHeartbeatTimeout) clearTimeout(workerHeartbeatTimeout);
|
|
2430
|
-
workerHeartbeatTimeout = undefined;
|
|
2431
|
-
workerHeartbeatPendingID = undefined;
|
|
2432
|
-
scheduleWorkerHeartbeat();
|
|
2433
|
-
return;
|
|
2434
|
-
}
|
|
2435
|
-
if (exactKeys(message, ["type", "error"]) && message.type === "redevplugin.worker.error" && typeof message.error === "string") {
|
|
2436
|
-
fail(message.error);
|
|
2437
|
-
return;
|
|
2438
|
-
}
|
|
2439
|
-
};
|
|
2440
|
-
const onWorkerMessage = (event) => {
|
|
2441
|
-
const message = event.data;
|
|
2442
|
-
if (isRecord(message) && message.type === "redevplugin.ui.file.export" && typeof message.id === "string") {
|
|
2443
|
-
if (!validWorkerFileExport(message)) return rejectWorkerRequest(message.id, "plugin file export request is invalid");
|
|
2444
|
-
if (!acceptWorkerRequest(message.id, "file")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2445
|
-
const action = pendingFileExportAction;
|
|
2446
|
-
if (!action || performance.now() > action.expires_at) {
|
|
2447
|
-
completeWorkerRequest(message.id);
|
|
2448
|
-
pendingFileExportAction = undefined;
|
|
2449
|
-
return rejectWorkerRequest(message.id, "plugin file export requires a recent user action", "PLUGIN_ACTION_DENIED");
|
|
2450
|
-
}
|
|
2451
|
-
pendingFileExportAction = undefined;
|
|
2452
|
-
sendParentTransfer({
|
|
2453
|
-
type: "redevplugin.surface.file.export",
|
|
2454
|
-
frame_generation_id: frameGenerationID,
|
|
2455
|
-
surface_handle: surfaceHandle,
|
|
2456
|
-
action_sequence: action.sequence,
|
|
2457
|
-
id: message.id,
|
|
2458
|
-
file_name: message.file_name,
|
|
2459
|
-
media_type: message.media_type,
|
|
2460
|
-
content: message.content,
|
|
2461
|
-
}, [message.content]);
|
|
2462
|
-
return;
|
|
2463
|
-
}
|
|
2464
|
-
if (!withinLimit(message)) return;
|
|
2465
|
-
if (exactKeys(message, ["type", "quiesce_id"]) && message.type === "redevplugin.bridge.lifecycle_ack" && validOpaqueHandle(message.quiesce_id, "quiesce") && message.quiesce_id === pendingQuiesceID) {
|
|
2466
|
-
pendingQuiesceID = undefined;
|
|
2467
|
-
sendParent({ type: "redevplugin.surface.quiesce_ack", quiesce_id: message.quiesce_id });
|
|
2468
|
-
terminateQuiescedWorker();
|
|
2469
|
-
return;
|
|
2470
|
-
}
|
|
2471
|
-
if (validCall(message)) {
|
|
2472
|
-
if (!acceptWorkerRequest(message.request.id, "rpc")) return rejectWorkerRequest(message.request.id, "duplicate, replayed, or excessive plugin request");
|
|
2473
|
-
sendParent(message);
|
|
2474
|
-
return;
|
|
2475
|
-
}
|
|
2476
|
-
if (isRecord(message) && Object.keys(message).every((key) => ["type", "id", "execution_id", "reason"].includes(key)) &&
|
|
2477
|
-
message.type === "redevplugin.bridge.execution.cancel" && typeof message.id === "string" &&
|
|
2478
|
-
validOpaqueHandle(message.execution_id, "execution") && (message.reason === undefined || (typeof message.reason === "string" && message.reason.length <= 256))) {
|
|
2479
|
-
if (!acceptWorkerRequest(message.id, "execution")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2480
|
-
sendParent(message);
|
|
2481
|
-
return;
|
|
2482
|
-
}
|
|
2483
|
-
if (exactKeys(message, ["type", "id", "execution_id"]) && message.type === "redevplugin.bridge.execution.query" &&
|
|
2484
|
-
typeof message.id === "string" && validOpaqueHandle(message.execution_id, "execution")) {
|
|
2485
|
-
if (!acceptWorkerRequest(message.id, "execution")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2486
|
-
const retryAfterMS = executionQueryRateLimit(message.execution_id, message.id);
|
|
2487
|
-
if (retryAfterMS > 0) {
|
|
2488
|
-
completeWorkerRequest(message.id);
|
|
2489
|
-
return sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: false, error_code: "PLUGIN_EXECUTION_BLOCKED", error: "plugin execution query rate limited" });
|
|
2490
|
-
}
|
|
2491
|
-
sendParent(message);
|
|
2492
|
-
return;
|
|
2493
|
-
}
|
|
2494
|
-
if (exactKeys(message, ["type", "id", "execution_id", "after_cursor"]) && message.type === "redevplugin.bridge.execution.events" &&
|
|
2495
|
-
typeof message.id === "string" && validOpaqueHandle(message.execution_id, "execution") && Number.isSafeInteger(message.after_cursor) && message.after_cursor >= 0) {
|
|
2496
|
-
if (!acceptWorkerRequest(message.id, "execution")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2497
|
-
sendParent(message);
|
|
2498
|
-
return;
|
|
2499
|
-
}
|
|
2500
|
-
if (exactKeys(message, ["type", "id", "canvas_id"]) && message.type === "redevplugin.ui.canvas.open" && typeof message.id === "string" && validResourceIdentifier(message.canvas_id)) {
|
|
2501
|
-
if (!acceptWorkerRequest(message.id, "canvas")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2502
|
-
openCanvas(message.id, message.canvas_id);
|
|
2503
|
-
return;
|
|
2504
|
-
}
|
|
2505
|
-
if (exactKeys(message, ["type", "id", "canvas_id", "label", "description"]) && message.type === "redevplugin.ui.canvas.accessibility" && typeof message.id === "string" && validResourceIdentifier(message.canvas_id)) {
|
|
2506
|
-
if (!acceptWorkerRequest(message.id, "canvas")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2507
|
-
updateCanvasAccessibility(message.id, message.canvas_id, message.label, message.description);
|
|
2508
|
-
return;
|
|
2509
|
-
}
|
|
2510
|
-
if (exactKeys(message, ["type", "id", "bindings"]) && message.type === "redevplugin.ui.keyboard.bindings" && typeof message.id === "string") {
|
|
2511
|
-
if (!acceptWorkerRequest(message.id, "keyboard")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2512
|
-
if (!validKeyboardBindings(message.bindings)) {
|
|
2513
|
-
completeWorkerRequest(message.id);
|
|
2514
|
-
return rejectWorkerRequest(message.id, "plugin surface keyboard bindings are invalid");
|
|
2515
|
-
}
|
|
2516
|
-
keyboardBindings = message.bindings.map((binding) => ({ ...binding }));
|
|
2517
|
-
completeWorkerRequest(message.id);
|
|
2518
|
-
sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
|
|
2519
|
-
return;
|
|
2520
|
-
}
|
|
2521
|
-
if (exactKeys(message, ["type", "id", "asset_id"]) && message.type === "redevplugin.ui.asset.image.open" && typeof message.id === "string" && validResourceIdentifier(message.asset_id)) {
|
|
2522
|
-
if (!acceptWorkerRequest(message.id, "asset")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2523
|
-
const asset = assetByLogicalID.get(message.asset_id);
|
|
2524
|
-
if (!asset || !String(asset.content_type).toLowerCase().startsWith("image/") || String(asset.content_type).toLowerCase() === "image/svg+xml") {
|
|
2525
|
-
completeWorkerRequest(message.id);
|
|
2526
|
-
return rejectWorkerRequest(message.id, "plugin image asset is not declared");
|
|
2527
|
-
}
|
|
2528
|
-
void deliverImageAsset(message.id, message.asset_id, asset);
|
|
2529
|
-
return;
|
|
2530
|
-
}
|
|
2531
|
-
if (exactKeys(message, ["type", "id", "revision", "tree"]) && message.type === "redevplugin.ui.mount" &&
|
|
2532
|
-
typeof message.id === "string" && message.revision === 1) {
|
|
2533
|
-
if (!acceptWorkerRequest(message.id, "render")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2534
|
-
if (!renderRateAllowed()) {
|
|
2535
|
-
completeWorkerRequest(message.id);
|
|
2536
|
-
return rejectWorkerRequest(message.id, "plugin render rate limit exceeded");
|
|
2537
|
-
}
|
|
2538
|
-
void mountWorkerTree(message.tree).then((committed) => {
|
|
2539
|
-
completeWorkerRequest(message.id);
|
|
2540
|
-
if (committed) sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
|
|
2541
|
-
}, (error) => fail(error && error.message || "plugin UI mount violated the protocol"));
|
|
2542
|
-
return;
|
|
2543
|
-
}
|
|
2544
|
-
if (exactKeys(message, ["type", "id", "base_revision", "revision", "operations"]) && message.type === "redevplugin.ui.patch" &&
|
|
2545
|
-
typeof message.id === "string" && Number.isSafeInteger(message.base_revision) && Number.isSafeInteger(message.revision)) {
|
|
2546
|
-
if (!acceptWorkerRequest(message.id, "render")) return rejectWorkerRequest(message.id, "duplicate, replayed, or excessive plugin request");
|
|
2547
|
-
if (!renderRateAllowed()) {
|
|
2548
|
-
completeWorkerRequest(message.id);
|
|
2549
|
-
return rejectWorkerRequest(message.id, "plugin render rate limit exceeded");
|
|
2550
|
-
}
|
|
2551
|
-
void patchWorkerTree(message.base_revision, message.revision, message.operations).then((committed) => {
|
|
2552
|
-
completeWorkerRequest(message.id);
|
|
2553
|
-
if (committed) sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
|
|
2554
|
-
}, (error) => fail(error && error.message || "plugin UI patch violated the protocol"));
|
|
2555
|
-
return;
|
|
2556
|
-
}
|
|
2557
|
-
if (exactKeys(message, ["type", "id"]) && message.type === "redevplugin.bridge.cancel" && typeof message.id === "string") {
|
|
2558
|
-
if (!pendingWorkerRequests.has(message.id)) return rejectWorkerRequest(message.id, "plugin request is not pending");
|
|
2559
|
-
for (const [canvasID, runtime] of canvasRuntimes) {
|
|
2560
|
-
if (runtime.openRequestID !== message.id) continue;
|
|
2561
|
-
runtime.dispose();
|
|
2562
|
-
canvasRuntimes.delete(canvasID);
|
|
2563
|
-
completeWorkerRequest(message.id);
|
|
2564
|
-
return;
|
|
2565
|
-
}
|
|
2566
|
-
if (pendingImageRequests.has(message.id)) {
|
|
2567
|
-
pendingImageRequests.delete(message.id);
|
|
2568
|
-
completeWorkerRequest(message.id);
|
|
2569
|
-
return;
|
|
2570
|
-
}
|
|
2571
|
-
if (executionQueryRequests.has(message.id)) completeExecutionQueryRequest(message.id);
|
|
2572
|
-
else completeWorkerRequest(message.id);
|
|
2573
|
-
sendParent(message);
|
|
2574
|
-
return;
|
|
2575
|
-
}
|
|
2576
|
-
};
|
|
2577
|
-
const onParentMessage = async (event) => {
|
|
2578
|
-
const message = event.data;
|
|
2579
|
-
if (!initialized) {
|
|
2580
|
-
const initKeys = ["type", "frame_generation_id", "surface_handle", "document", "context"];
|
|
2581
|
-
if (!isRecord(message) || !Object.keys(message).every((key) => initKeys.includes(key)) || !initKeys.slice(0, 4).every((key) => Object.prototype.hasOwnProperty.call(message, key)) || message.type !== "redevplugin.surface.initialize" || message.frame_generation_id !== frameGenerationID || !validOpaqueHandle(message.surface_handle, "surface") || !validDocument(message.document) || (message.context !== undefined && !validSurfaceContext(message.context))) return fail("invalid private initialize message");
|
|
2582
|
-
initialized = true;
|
|
2583
|
-
surfaceHandle = message.surface_handle;
|
|
2584
|
-
currentDocument = message.document;
|
|
2585
|
-
currentContext = message.context;
|
|
2586
|
-
try { applyStaticDocument(currentDocument); if (currentContext) applySurfaceContext(currentContext); startWorker(currentDocument); }
|
|
2587
|
-
catch (error) { return fail(error); }
|
|
2588
|
-
// Readiness describes applied initialization, not a browser paint. Hidden
|
|
2589
|
-
// and offscreen opaque frames may never receive an animation frame.
|
|
2590
|
-
sendParent({ type: "redevplugin.surface.renderer_ready" });
|
|
2591
|
-
loadAssets();
|
|
2592
|
-
return;
|
|
2593
|
-
}
|
|
2594
|
-
if (isRecord(message) && exactKeys(message, ["type", "frame_generation_id", "surface_handle", "context"]) && message.type === "redevplugin.surface.context" && message.frame_generation_id === frameGenerationID && message.surface_handle === surfaceHandle && validSurfaceContext(message.context)) {
|
|
2595
|
-
if (currentContext && message.context.revision <= currentContext.revision) return;
|
|
2596
|
-
currentContext = message.context;
|
|
2597
|
-
applySurfaceContext(currentContext);
|
|
2598
|
-
sendWorker({ type: "redevplugin.bridge.context", context: currentContext });
|
|
2599
|
-
return;
|
|
2600
|
-
}
|
|
2601
|
-
if (message && message.type === "redevplugin.bridge.response" && typeof message.id === "string" && pendingWorkerRequests.has(message.id) && withinLimit(message)) {
|
|
2602
|
-
if (executionQueryRequests.has(message.id)) completeExecutionQueryRequest(message.id);
|
|
2603
|
-
else completeWorkerRequest(message.id);
|
|
2604
|
-
sendWorker(message);
|
|
2605
|
-
return;
|
|
2606
|
-
}
|
|
2607
|
-
if (message && message.type === "redevplugin.bridge.lifecycle" && isRecord(message.event) && ["ready", "visible", "hidden", "dispose"].includes(message.event.type) && Object.keys(message).every((key) => ["type", "event", "quiesce_id"].includes(key)) && (message.quiesce_id === undefined || (message.event.type === "dispose" && validOpaqueHandle(message.quiesce_id, "quiesce")))) {
|
|
2608
|
-
pendingQuiesceID = message.quiesce_id;
|
|
2609
|
-
sendWorker(message);
|
|
2610
|
-
if (message.event.type === "dispose" && !message.quiesce_id) disposeRuntime();
|
|
2611
|
-
return;
|
|
2612
|
-
}
|
|
2613
|
-
if (exactKeys(message, ["type", "request_id", "binding_id", "ok", "path", "sha256", "content_type", "content_base64"]) && message.type === "redevplugin.surface.asset.response" && typeof message.content_base64 === "string" && message.content_base64.length <= maxPrivateAssetBase64Length) {
|
|
2614
|
-
const asset = pendingAssets.get(message.request_id);
|
|
2615
|
-
pendingAssets.delete(message.request_id);
|
|
2616
|
-
if (asset) activeAssetReads = Math.max(0, activeAssetReads - 1);
|
|
2617
|
-
if (!asset || message.ok !== true || message.binding_id !== asset.binding_id || message.path !== asset.path || message.sha256 !== asset.sha256 || message.content_type !== asset.content_type || typeof message.content_base64 !== "string") {
|
|
2618
|
-
fail("plugin asset response did not match the prepared document");
|
|
2619
|
-
return;
|
|
2620
|
-
}
|
|
2621
|
-
try {
|
|
2622
|
-
await applyAsset(asset, message.content_base64);
|
|
2623
|
-
} catch {
|
|
2624
|
-
fail("plugin asset response failed renderer validation");
|
|
2625
|
-
return;
|
|
2626
|
-
}
|
|
2627
|
-
pumpAssets();
|
|
2628
|
-
}
|
|
2629
|
-
};
|
|
2630
|
-
const onWindowMessage = (event) => {
|
|
2631
|
-
if (accepted || event.source !== parent || !event.ports || event.ports.length !== 1) return;
|
|
2632
|
-
const message = event.data;
|
|
2633
|
-
if (!exactKeys(message, ["type", "frame_generation_id"]) || message.type !== "redevplugin.surface.port" || !validOpaqueHandle(message.frame_generation_id, "frame")) return;
|
|
2634
|
-
accepted = true;
|
|
2635
|
-
frameGenerationID = message.frame_generation_id;
|
|
2636
|
-
removeEventListener("message", onWindowMessage);
|
|
2637
|
-
parentPort = event.ports[0];
|
|
2638
|
-
parentPort.addEventListener("message", onParentMessage);
|
|
2639
|
-
parentPort.start();
|
|
2640
|
-
parentPort.postMessage({ type: "redevplugin.surface.port_ack", frame_generation_id: frameGenerationID });
|
|
2641
|
-
};
|
|
2642
|
-
addEventListener("message", onWindowMessage);
|
|
2643
|
-
})();`;
|
|
2644
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="${escapeHTMLAttribute(csp)}"><title>Plugin</title></head><body><script nonce="${scriptNonce}">${bootstrapScript}</script></body></html>`;
|
|
2645
|
-
}
|
|
2646
558
|
class PluginSurfaceHostImplementation {
|
|
2647
559
|
element;
|
|
2648
560
|
surfaceInstanceId;
|
|
2649
561
|
bootstrap;
|
|
2650
562
|
bridgeChannelId;
|
|
2651
|
-
frameGenerationId;
|
|
563
|
+
#frameGenerationId;
|
|
564
|
+
get frameGenerationId() { return this.#frameGenerationId; }
|
|
2652
565
|
surfaceHandle;
|
|
2653
566
|
#iframe;
|
|
2654
567
|
#transport;
|
|
2655
568
|
#createMessageChannel;
|
|
2656
569
|
#bridgeReady = false;
|
|
2657
|
-
#
|
|
570
|
+
#applicationInitialized = false;
|
|
2658
571
|
#loadTimeoutMs;
|
|
2659
572
|
#requestTimeoutMs;
|
|
2660
573
|
#leaseRenewalLeadMs;
|
|
2661
574
|
#reloadLimiter;
|
|
2662
575
|
#confirm;
|
|
576
|
+
#prepareWebOrigin;
|
|
2663
577
|
#onOpeningProgress;
|
|
2664
578
|
#onInteraction;
|
|
2665
579
|
#onFileExport;
|
|
580
|
+
#confirmClose;
|
|
581
|
+
#closeDecision;
|
|
582
|
+
#requestedClose;
|
|
2666
583
|
#onError;
|
|
2667
584
|
#lastInteractionSequence = 0;
|
|
2668
|
-
#pendingFileExportAction;
|
|
2669
585
|
#interactionWindowStartedAt = 0;
|
|
2670
586
|
#interactionWindowCount = 0;
|
|
2671
587
|
#abortController = new AbortController();
|
|
@@ -2676,12 +592,9 @@ class PluginSurfaceHostImplementation {
|
|
|
2676
592
|
#leaseRenewalPromise;
|
|
2677
593
|
#assetSession;
|
|
2678
594
|
#assetSessionID;
|
|
2679
|
-
#document;
|
|
2680
595
|
#surfaceContext;
|
|
2681
|
-
#assets = new Map();
|
|
2682
596
|
#pendingRequestControllers = new Map();
|
|
2683
597
|
#activeTransportRequests = 0;
|
|
2684
|
-
#activeAssetReads = 0;
|
|
2685
598
|
#transportIdle;
|
|
2686
599
|
#port;
|
|
2687
600
|
#openSignals;
|
|
@@ -2691,8 +604,11 @@ class PluginSurfaceHostImplementation {
|
|
|
2691
604
|
#openingStage = "preparing";
|
|
2692
605
|
#openingPending = new Set();
|
|
2693
606
|
#quiesce;
|
|
2694
|
-
#
|
|
2695
|
-
#
|
|
607
|
+
#document;
|
|
608
|
+
#navigationPath;
|
|
609
|
+
#documentID;
|
|
610
|
+
#navigationPending = false;
|
|
611
|
+
#navigationTimer;
|
|
2696
612
|
#closePromise;
|
|
2697
613
|
#revokePromise;
|
|
2698
614
|
#unregisterSurfaceScope;
|
|
@@ -2703,24 +619,40 @@ class PluginSurfaceHostImplementation {
|
|
|
2703
619
|
#onPortMessage = (event) => {
|
|
2704
620
|
void this.#handlePortMessage(event);
|
|
2705
621
|
};
|
|
2706
|
-
#
|
|
2707
|
-
if (this.#disposed || !this.#
|
|
2708
|
-
return;
|
|
2709
|
-
if (!this.#frameLoaded) {
|
|
2710
|
-
this.#frameLoaded = true;
|
|
2711
|
-
this.#initialFrameLoad?.resolve();
|
|
622
|
+
#onEntryMessage = (event) => {
|
|
623
|
+
if (this.#disposed || !this.#document || event.source !== this.#iframe.contentWindow || event.origin !== this.#document.origin)
|
|
2712
624
|
return;
|
|
625
|
+
if (hasExactKeys(event.data, ["type", "entry_path", "document_id"]) && event.data.type === "redevplugin.surface.entry_ready" && typeof event.data.entry_path === "string" && validOpaqueHandle(event.data.document_id, "document")) {
|
|
626
|
+
const path = event.data.entry_path;
|
|
627
|
+
if (!path.startsWith("/") || path.startsWith("//") || path.length > 2048 || path.includes("\\") || new URL(path, this.#document.origin).origin !== this.#document.origin)
|
|
628
|
+
return;
|
|
629
|
+
if (event.data.document_id === this.#documentID)
|
|
630
|
+
return;
|
|
631
|
+
if (this.#documentID && !this.#ready && !this.#navigationPending) {
|
|
632
|
+
void this.#failSurface(new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_FAILED", "Application navigated before its connection completed; reopen the application"));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (this.#ready)
|
|
636
|
+
this.#beginNavigation();
|
|
637
|
+
this.#documentID = event.data.document_id;
|
|
638
|
+
this.#navigationPath = path;
|
|
639
|
+
if (this.#navigationPending) {
|
|
640
|
+
void this.#resumeNavigation();
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
this.#openingPending.delete("entry_ready");
|
|
644
|
+
this.#openSignals?.entryReady.resolve();
|
|
645
|
+
}
|
|
646
|
+
else if (hasExactKeys(event.data, ["type", "error"]) && event.data.type === "redevplugin.surface.entry_error" && typeof event.data.error === "string") {
|
|
647
|
+
this.#openSignals?.entryReady.reject(new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_FAILED", event.data.error));
|
|
2713
648
|
}
|
|
2714
|
-
const decision = this.#reloadLimiter.recordCrash();
|
|
2715
|
-
const error = new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_FAILED", `Plugin iframe reloaded unexpectedly (attempt ${decision.attempt})`);
|
|
2716
|
-
void this.#failSurface(error);
|
|
2717
649
|
};
|
|
2718
650
|
constructor(options, transport, iframe) {
|
|
2719
651
|
this.element = iframe;
|
|
2720
652
|
this.bootstrap = options.bootstrap;
|
|
2721
653
|
this.surfaceInstanceId = options.bootstrap.surfaceInstanceId;
|
|
2722
654
|
this.bridgeChannelId = options.bridgeChannelId ?? randomOpaqueHandle("bridge");
|
|
2723
|
-
this
|
|
655
|
+
this.#frameGenerationId = randomOpaqueHandle("frame");
|
|
2724
656
|
this.surfaceHandle = randomOpaqueHandle("surface");
|
|
2725
657
|
this.#iframe = iframe;
|
|
2726
658
|
this.#transport = transport;
|
|
@@ -2730,9 +662,11 @@ class PluginSurfaceHostImplementation {
|
|
|
2730
662
|
this.#leaseRenewalLeadMs = normalizeLeaseRenewalLead(options.leaseRenewalLeadMs);
|
|
2731
663
|
this.#reloadLimiter = options.reloadLimiter ?? new PluginSurfaceReloadLimiter();
|
|
2732
664
|
this.#confirm = options.confirm;
|
|
665
|
+
this.#prepareWebOrigin = options.prepareWebOrigin;
|
|
2733
666
|
this.#onOpeningProgress = options.onOpeningProgress;
|
|
2734
667
|
this.#onInteraction = options.onInteraction;
|
|
2735
668
|
this.#onFileExport = options.onFileExport;
|
|
669
|
+
this.#confirmClose = options.confirmClose;
|
|
2736
670
|
this.#onError = options.onError;
|
|
2737
671
|
this.#surfaceContext = options.surfaceContext === undefined ? undefined : normalizePluginSurfaceContext(options.surfaceContext);
|
|
2738
672
|
hardenPluginSurfaceFrame(this.#iframe);
|
|
@@ -2746,7 +680,7 @@ class PluginSurfaceHostImplementation {
|
|
|
2746
680
|
this.#opened = true;
|
|
2747
681
|
this.#openingFailure = undefined;
|
|
2748
682
|
this.#openingStartedAt = performance.now();
|
|
2749
|
-
this.#setOpeningStage("preparing", ["
|
|
683
|
+
this.#setOpeningStage("preparing", ["prepare"]);
|
|
2750
684
|
const progressTimer = setInterval(() => {
|
|
2751
685
|
if (!this.#ready && !this.#disposed) {
|
|
2752
686
|
const progress = this.#openingProgress();
|
|
@@ -2755,24 +689,17 @@ class PluginSurfaceHostImplementation {
|
|
|
2755
689
|
}, openingProgressDelayMs);
|
|
2756
690
|
const signals = {
|
|
2757
691
|
portAcknowledged: deferred(),
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
firstCommit: deferred(),
|
|
692
|
+
entryReady: deferred(),
|
|
693
|
+
appReady: deferred(),
|
|
2761
694
|
};
|
|
2762
695
|
void signals.portAcknowledged.promise.catch(() => undefined);
|
|
2763
|
-
void signals.
|
|
2764
|
-
void signals.
|
|
2765
|
-
void signals.firstCommit.promise.catch(() => undefined);
|
|
696
|
+
void signals.entryReady.promise.catch(() => undefined);
|
|
697
|
+
void signals.appReady.promise.catch(() => undefined);
|
|
2766
698
|
this.#openSignals = signals;
|
|
2767
|
-
|
|
2768
|
-
this.#frameLoaded = false;
|
|
2769
|
-
const initialFrameLoad = deferred();
|
|
2770
|
-
this.#initialFrameLoad = initialFrameLoad;
|
|
2771
|
-
this.#iframe.addEventListener("load", this.#onFrameLoad);
|
|
699
|
+
globalThis.addEventListener("message", this.#onEntryMessage);
|
|
2772
700
|
hardenPluginSurfaceFrame(this.#iframe);
|
|
2773
|
-
this.#iframe.srcdoc = createOpaquePluginBootstrapHTML({ scriptNonce });
|
|
2774
701
|
try {
|
|
2775
|
-
await withTimeout(this.#completeOpening(signals
|
|
702
|
+
await withTimeout(this.#completeOpening(signals), this.#loadTimeoutMs, "Plugin surface opening timed out");
|
|
2776
703
|
}
|
|
2777
704
|
catch (error) {
|
|
2778
705
|
let bridgeError = this.#openingFailure ?? toBridgeError(error, "PLUGIN_BRIDGE_HANDSHAKE_FAILED");
|
|
@@ -2791,26 +718,44 @@ class PluginSurfaceHostImplementation {
|
|
|
2791
718
|
this.#openingPending.clear();
|
|
2792
719
|
this.#openSignals = undefined;
|
|
2793
720
|
this.#openingFailure = undefined;
|
|
2794
|
-
this.#initialFrameLoad = undefined;
|
|
2795
721
|
}
|
|
2796
722
|
}
|
|
2797
|
-
async #completeOpening(signals
|
|
2798
|
-
const
|
|
2799
|
-
this.#prepareSurface().then((value) => {
|
|
2800
|
-
this.#openingPending.delete("prepare");
|
|
2801
|
-
return value;
|
|
2802
|
-
}),
|
|
2803
|
-
initialFrameLoad.then(() => { this.#openingPending.delete("frame_load"); }),
|
|
2804
|
-
]);
|
|
723
|
+
async #completeOpening(signals) {
|
|
724
|
+
const preparation = await this.#prepareSurface();
|
|
2805
725
|
this.#assertActive();
|
|
2806
726
|
validateSurfacePreparation(this.bootstrap, preparation);
|
|
727
|
+
if (preparation.document.parent_origin !== globalThis.location.origin) {
|
|
728
|
+
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Web surface parent origin does not match this host");
|
|
729
|
+
}
|
|
2807
730
|
this.#assetSession = preparation.asset_session;
|
|
2808
731
|
this.#assetSessionID = preparation.asset_session_id;
|
|
2809
732
|
this.#document = preparation.document;
|
|
2810
|
-
this.#
|
|
733
|
+
if (this.#prepareWebOrigin) {
|
|
734
|
+
this.#setOpeningStage("connecting", ["origin"]);
|
|
735
|
+
await abortableWebOriginPreparation(this.#prepareWebOrigin(Object.freeze({
|
|
736
|
+
origin: preparation.document.origin,
|
|
737
|
+
parentOrigin: preparation.document.parent_origin,
|
|
738
|
+
pluginInstanceId: this.bootstrap.pluginInstanceId,
|
|
739
|
+
surfaceInstanceId: this.surfaceInstanceId,
|
|
740
|
+
signal: this.#abortController.signal,
|
|
741
|
+
})), this.#abortController.signal);
|
|
742
|
+
this.#assertActive();
|
|
743
|
+
}
|
|
744
|
+
this.#setOpeningStage("connecting", ["entry_ready"]);
|
|
745
|
+
this.#iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
|
|
746
|
+
this.#iframe.src = preparation.document.bootstrap_url;
|
|
747
|
+
await signals.entryReady.promise;
|
|
748
|
+
this.#assertActive();
|
|
749
|
+
await this.#connectApplication(signals, this.#navigationPath ?? new URL(preparation.document.entry_url).pathname);
|
|
750
|
+
}
|
|
751
|
+
async #connectApplication(signals, entryPath) {
|
|
752
|
+
const preparation = { document: this.#document };
|
|
2811
753
|
this.#setOpeningStage("connecting", ["port_ack"]);
|
|
2812
754
|
const channel = this.#createMessageChannel();
|
|
2813
755
|
this.#port = channel.port1;
|
|
756
|
+
const activePort = this.#port;
|
|
757
|
+
this.#onPortMessage = event => { if (this.#port === activePort)
|
|
758
|
+
void this.#handlePortMessage(event); };
|
|
2814
759
|
this.#port.addEventListener("message", this.#onPortMessage);
|
|
2815
760
|
this.#port.start();
|
|
2816
761
|
const iframeWindow = this.#iframe.contentWindow;
|
|
@@ -2820,36 +765,87 @@ class PluginSurfaceHostImplementation {
|
|
|
2820
765
|
iframeWindow.postMessage({
|
|
2821
766
|
type: "redevplugin.surface.port",
|
|
2822
767
|
frame_generation_id: this.frameGenerationId,
|
|
2823
|
-
|
|
768
|
+
document_id: this.#documentID,
|
|
769
|
+
}, preparation.document.origin, [channel.port2]);
|
|
2824
770
|
await signals.portAcknowledged.promise;
|
|
2825
771
|
this.#assertActive();
|
|
2826
772
|
this.#setOpeningStage("authorizing", ["token"]);
|
|
2827
|
-
|
|
773
|
+
if (this.#leaseRenewalPromise)
|
|
774
|
+
await this.#leaseRenewalPromise;
|
|
775
|
+
const token = await this.#mintBridgeToken(this.#gatewayToken);
|
|
2828
776
|
this.#assertActive();
|
|
2829
777
|
this.#applyLease(token);
|
|
2830
|
-
this.#setOpeningStage("initializing", ["
|
|
2831
|
-
this.#
|
|
778
|
+
this.#setOpeningStage("initializing", ["app_ready"]);
|
|
779
|
+
this.#postToApplication(removeUndefined({
|
|
2832
780
|
type: "redevplugin.surface.initialize",
|
|
2833
781
|
frame_generation_id: this.frameGenerationId,
|
|
2834
782
|
surface_handle: this.surfaceHandle,
|
|
2835
|
-
|
|
783
|
+
entry_path: entryPath,
|
|
2836
784
|
context: this.#surfaceContext,
|
|
2837
785
|
}));
|
|
2838
|
-
this.#
|
|
2839
|
-
await Promise.all([signals.rendererReady.promise, signals.workerReady.promise]);
|
|
2840
|
-
this.#assertActive();
|
|
786
|
+
this.#applicationInitialized = true;
|
|
2841
787
|
this.#bridgeReady = true;
|
|
2842
|
-
this.#
|
|
2843
|
-
|
|
2844
|
-
this.#postToRenderer({ type: "redevplugin.bridge.lifecycle", event: { type: "ready" } });
|
|
2845
|
-
await signals.firstCommit.promise;
|
|
788
|
+
this.#postToApplication({ type: "redevplugin.bridge.lifecycle", event: { type: "ready" } });
|
|
789
|
+
await signals.appReady.promise;
|
|
2846
790
|
this.#assertActive();
|
|
791
|
+
this.#bridgeReady = true;
|
|
2847
792
|
this.#ready = true;
|
|
793
|
+
this.#scheduleLeaseRenewal();
|
|
794
|
+
this.#postToApplication({ type: "redevplugin.bridge.lifecycle", event: { type: "ready" } });
|
|
2848
795
|
this.#reloadLimiter.recordHealthyLoad();
|
|
2849
796
|
}
|
|
797
|
+
#beginNavigation() {
|
|
798
|
+
if (this.#disposed || this.#navigationPending)
|
|
799
|
+
return;
|
|
800
|
+
if (!this.#ready && this.#openSignals) {
|
|
801
|
+
void this.#failSurface(new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_FAILED", "Application navigated before its connection completed; reopen the application"));
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
this.#navigationPending = true;
|
|
805
|
+
clearTimeout(this.#leaseRenewalTimer);
|
|
806
|
+
this.#leaseRenewalTimer = undefined;
|
|
807
|
+
this.#ready = false;
|
|
808
|
+
this.#bridgeReady = false;
|
|
809
|
+
this.#applicationInitialized = false;
|
|
810
|
+
this.#lastInteractionSequence = 0;
|
|
811
|
+
this.#frameGenerationId = randomOpaqueHandle("frame");
|
|
812
|
+
for (const controller of this.#pendingRequestControllers.values())
|
|
813
|
+
controller.abort();
|
|
814
|
+
this.#pendingRequestControllers.clear();
|
|
815
|
+
this.#cancelCloseDecision();
|
|
816
|
+
this.#port?.removeEventListener("message", this.#onPortMessage);
|
|
817
|
+
this.#port?.close();
|
|
818
|
+
this.#port = undefined;
|
|
819
|
+
this.#navigationTimer = setTimeout(() => {
|
|
820
|
+
void this.#failSurface(new PluginBridgeError("PLUGIN_BRIDGE_TIMEOUT", "Application document did not reconnect"));
|
|
821
|
+
}, this.#loadTimeoutMs);
|
|
822
|
+
}
|
|
823
|
+
async #resumeNavigation() {
|
|
824
|
+
if (!this.#navigationPending || this.#disposed)
|
|
825
|
+
return;
|
|
826
|
+
this.#navigationPending = false;
|
|
827
|
+
clearTimeout(this.#navigationTimer);
|
|
828
|
+
this.#navigationTimer = undefined;
|
|
829
|
+
const signals = { entryReady: deferred(), portAcknowledged: deferred(), appReady: deferred() };
|
|
830
|
+
for (const signal of Object.values(signals))
|
|
831
|
+
void signal.promise.catch(() => undefined);
|
|
832
|
+
this.#openSignals = signals;
|
|
833
|
+
try {
|
|
834
|
+
await withTimeout(this.#connectApplication(signals, this.#navigationPath), this.#loadTimeoutMs, "Application document reconnection timed out");
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
const failure = this.#openingFailure ?? toBridgeError(error, "PLUGIN_BRIDGE_HANDSHAKE_FAILED");
|
|
838
|
+
this.#reportError(failure);
|
|
839
|
+
await this.#failSurface(failure);
|
|
840
|
+
}
|
|
841
|
+
finally {
|
|
842
|
+
if (this.#openSignals === signals)
|
|
843
|
+
this.#openSignals = undefined;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
2850
846
|
sendLifecycle(event) {
|
|
2851
847
|
this.#assertReady();
|
|
2852
|
-
this.#
|
|
848
|
+
this.#postToApplication({ type: "redevplugin.bridge.lifecycle", event });
|
|
2853
849
|
}
|
|
2854
850
|
updateContext(context) {
|
|
2855
851
|
this.#assertActive();
|
|
@@ -2858,11 +854,65 @@ class PluginSurfaceHostImplementation {
|
|
|
2858
854
|
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin surface context revision must increase monotonically");
|
|
2859
855
|
}
|
|
2860
856
|
this.#surfaceContext = normalized;
|
|
2861
|
-
if (this.#port && this.#
|
|
2862
|
-
this.#
|
|
857
|
+
if (this.#port && this.#applicationInitialized) {
|
|
858
|
+
this.#postToApplication({ type: "redevplugin.surface.context", frame_generation_id: this.frameGenerationId, surface_handle: this.surfaceHandle, context: normalized });
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/** User-initiated close. Cancellation leaves the live application and lease intact. */
|
|
862
|
+
requestClose() {
|
|
863
|
+
if (this.#requestedClose)
|
|
864
|
+
return this.#requestedClose;
|
|
865
|
+
const pending = this.#requestClose();
|
|
866
|
+
this.#requestedClose = pending;
|
|
867
|
+
void pending.finally(() => { if (this.#requestedClose === pending)
|
|
868
|
+
this.#requestedClose = undefined; }).catch(() => undefined);
|
|
869
|
+
return pending;
|
|
870
|
+
}
|
|
871
|
+
async #requestClose() {
|
|
872
|
+
if (this.#disposed)
|
|
873
|
+
return this.close();
|
|
874
|
+
if (!this.#ready)
|
|
875
|
+
return undefined;
|
|
876
|
+
const generation = this.#frameGenerationId;
|
|
877
|
+
const decision = { id: randomOpaqueHandle("close"), result: deferred(), controller: new AbortController() };
|
|
878
|
+
this.#closeDecision = decision;
|
|
879
|
+
try {
|
|
880
|
+
let allow = false;
|
|
881
|
+
let reason = "unsaved";
|
|
882
|
+
try {
|
|
883
|
+
this.#postToApplication({ type: "redevplugin.bridge.close_request", close_id: decision.id });
|
|
884
|
+
allow = await withTimeout(decision.result.promise, Math.min(this.#requestTimeoutMs, maxSurfaceQuiesceMs), "Application close decision timed out");
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
reason = "unresponsive";
|
|
888
|
+
}
|
|
889
|
+
if (decision.controller.signal.aborted || generation !== this.#frameGenerationId)
|
|
890
|
+
return undefined;
|
|
891
|
+
if (!allow && this.#confirmClose) {
|
|
892
|
+
try {
|
|
893
|
+
allow = confirmationDecisionAccepted(await abortableConfirmationDecision(this.#confirmClose({ reason, signal: decision.controller.signal }), decision.controller.signal));
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
allow = false;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (!allow || decision.controller.signal.aborted || generation !== this.#frameGenerationId)
|
|
900
|
+
return undefined;
|
|
901
|
+
return this.close();
|
|
2863
902
|
}
|
|
903
|
+
finally {
|
|
904
|
+
if (this.#closeDecision === decision)
|
|
905
|
+
this.#closeDecision = undefined;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
#cancelCloseDecision() {
|
|
909
|
+
const decision = this.#closeDecision;
|
|
910
|
+
this.#closeDecision = undefined;
|
|
911
|
+
decision?.controller.abort();
|
|
912
|
+
decision?.result.reject(new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", "Application close decision was cancelled"));
|
|
2864
913
|
}
|
|
2865
914
|
close() {
|
|
915
|
+
this.#cancelCloseDecision();
|
|
2866
916
|
if (this.#disposed) {
|
|
2867
917
|
const result = {
|
|
2868
918
|
quiesce: { outcome: "not_ready", durationMs: 0 },
|
|
@@ -2922,34 +972,32 @@ class PluginSurfaceHostImplementation {
|
|
|
2922
972
|
if (this.#disposed)
|
|
2923
973
|
return;
|
|
2924
974
|
this.#disposed = true;
|
|
975
|
+
clearTimeout(this.#navigationTimer);
|
|
976
|
+
this.#navigationPending = false;
|
|
2925
977
|
this.#unregisterSurfaceScope?.();
|
|
2926
978
|
this.#unregisterSurfaceScope = undefined;
|
|
2927
979
|
this.#ready = false;
|
|
2928
980
|
this.#bridgeReady = false;
|
|
2929
|
-
this.#
|
|
981
|
+
this.#applicationInitialized = false;
|
|
2930
982
|
if (this.#leaseRenewalTimer)
|
|
2931
983
|
clearTimeout(this.#leaseRenewalTimer);
|
|
2932
984
|
this.#leaseRenewalTimer = undefined;
|
|
2933
|
-
|
|
985
|
+
globalThis.removeEventListener("message", this.#onEntryMessage);
|
|
2934
986
|
this.#abortController.abort();
|
|
2935
987
|
for (const controller of this.#pendingRequestControllers.values())
|
|
2936
988
|
controller.abort();
|
|
2937
989
|
this.#pendingRequestControllers.clear();
|
|
2938
990
|
const disposedError = new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", "Plugin surface host was disposed");
|
|
2939
|
-
this.#openSignals?.
|
|
2940
|
-
this.#openSignals?.
|
|
2941
|
-
this.#openSignals?.firstCommit.reject(disposedError);
|
|
991
|
+
this.#openSignals?.entryReady.reject(disposedError);
|
|
992
|
+
this.#openSignals?.appReady.reject(disposedError);
|
|
2942
993
|
this.#openSignals?.portAcknowledged.reject(disposedError);
|
|
2943
|
-
this.#
|
|
994
|
+
this.#cancelCloseDecision();
|
|
2944
995
|
this.#quiesce?.acknowledged.reject(disposedError);
|
|
2945
996
|
this.#quiesce = undefined;
|
|
2946
997
|
this.#gatewayToken = undefined;
|
|
2947
998
|
this.#leaseExpiresAtMs = 0;
|
|
2948
999
|
this.#assetSession = undefined;
|
|
2949
1000
|
this.#assetSessionID = undefined;
|
|
2950
|
-
this.#document = undefined;
|
|
2951
|
-
this.#assets.clear();
|
|
2952
|
-
this.#pendingFileExportAction = undefined;
|
|
2953
1001
|
if (this.#port) {
|
|
2954
1002
|
try {
|
|
2955
1003
|
this.#port.postMessage({ type: "redevplugin.bridge.lifecycle", event: { type: "dispose" } });
|
|
@@ -2961,7 +1009,8 @@ class PluginSurfaceHostImplementation {
|
|
|
2961
1009
|
this.#port.close();
|
|
2962
1010
|
this.#port = undefined;
|
|
2963
1011
|
}
|
|
2964
|
-
this.#iframe.
|
|
1012
|
+
this.#iframe.setAttribute("sandbox", "allow-scripts allow-forms");
|
|
1013
|
+
this.#iframe.src = "about:blank";
|
|
2965
1014
|
}
|
|
2966
1015
|
#invalidateLocal() {
|
|
2967
1016
|
this.#disposeLocal();
|
|
@@ -2984,25 +1033,23 @@ class PluginSurfaceHostImplementation {
|
|
|
2984
1033
|
}
|
|
2985
1034
|
if (!messageWithinLimit(data))
|
|
2986
1035
|
return;
|
|
2987
|
-
if (hasExactKeys(data, ["type"]) && data.type === "redevplugin.surface.renderer_ready") {
|
|
2988
|
-
this.#openingPending.delete("renderer_ready");
|
|
2989
|
-
this.#openSignals?.rendererReady.resolve();
|
|
2990
|
-
return;
|
|
2991
|
-
}
|
|
2992
1036
|
if (hasExactKeys(data, ["type", "frame_generation_id"]) &&
|
|
2993
1037
|
data.type === "redevplugin.surface.port_ack" && data.frame_generation_id === this.frameGenerationId) {
|
|
2994
1038
|
this.#openingPending.delete("port_ack");
|
|
2995
1039
|
this.#openSignals?.portAcknowledged.resolve();
|
|
2996
1040
|
return;
|
|
2997
1041
|
}
|
|
2998
|
-
if (hasExactKeys(data, ["type"]) && data.type === "redevplugin.surface.
|
|
2999
|
-
this.#openingPending.delete("
|
|
3000
|
-
this.#openSignals?.
|
|
1042
|
+
if (hasExactKeys(data, ["type", "frame_generation_id"]) && data.type === "redevplugin.surface.app_ready" && data.frame_generation_id === this.frameGenerationId) {
|
|
1043
|
+
this.#openingPending.delete("app_ready");
|
|
1044
|
+
this.#openSignals?.appReady.resolve();
|
|
3001
1045
|
return;
|
|
3002
1046
|
}
|
|
3003
|
-
if (hasExactKeys(data, ["type"]) && data.type === "redevplugin.surface.
|
|
3004
|
-
this.#
|
|
3005
|
-
|
|
1047
|
+
if (hasExactKeys(data, ["type", "frame_generation_id"]) && data.type === "redevplugin.surface.navigation" && data.frame_generation_id === this.frameGenerationId) {
|
|
1048
|
+
this.#beginNavigation();
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (hasExactKeys(data, ["type", "close_id", "allow"]) && data.type === "redevplugin.bridge.close_response" && typeof data.allow === "boolean" && data.close_id === this.#closeDecision?.id) {
|
|
1052
|
+
this.#closeDecision?.result.resolve(data.allow);
|
|
3006
1053
|
return;
|
|
3007
1054
|
}
|
|
3008
1055
|
if (isSurfaceInteractionMessage(data)) {
|
|
@@ -3038,9 +1085,6 @@ class PluginSurfaceHostImplementation {
|
|
|
3038
1085
|
await this.#handleExecutionEvents(data);
|
|
3039
1086
|
return;
|
|
3040
1087
|
}
|
|
3041
|
-
if (isAssetReadMessage(data)) {
|
|
3042
|
-
await this.#handleAssetRead(data);
|
|
3043
|
-
}
|
|
3044
1088
|
}
|
|
3045
1089
|
catch (error) {
|
|
3046
1090
|
await this.#failSurface(toBridgeError(error, "PLUGIN_CONTRACT_MISMATCH"));
|
|
@@ -3053,12 +1097,6 @@ class PluginSurfaceHostImplementation {
|
|
|
3053
1097
|
return;
|
|
3054
1098
|
this.#lastInteractionSequence = message.sequence;
|
|
3055
1099
|
const now = performance.now();
|
|
3056
|
-
if (message.kind === "action") {
|
|
3057
|
-
this.#pendingFileExportAction = {
|
|
3058
|
-
sequence: message.sequence,
|
|
3059
|
-
expiresAt: now + pluginSurfaceFileExportActionWindowMs,
|
|
3060
|
-
};
|
|
3061
|
-
}
|
|
3062
1100
|
if (now - this.#interactionWindowStartedAt >= 1000) {
|
|
3063
1101
|
this.#interactionWindowStartedAt = now;
|
|
3064
1102
|
this.#interactionWindowCount = 0;
|
|
@@ -3094,12 +1132,6 @@ class PluginSurfaceHostImplementation {
|
|
|
3094
1132
|
}
|
|
3095
1133
|
if (message.frame_generation_id !== this.frameGenerationId || message.surface_handle !== this.surfaceHandle)
|
|
3096
1134
|
return;
|
|
3097
|
-
const action = this.#pendingFileExportAction;
|
|
3098
|
-
if (!action || action.sequence !== message.action_sequence || performance.now() > action.expiresAt) {
|
|
3099
|
-
this.#postError(message.id, "PLUGIN_ACTION_DENIED", "Plugin file export requires a recent user action");
|
|
3100
|
-
return;
|
|
3101
|
-
}
|
|
3102
|
-
this.#pendingFileExportAction = undefined;
|
|
3103
1135
|
if (!this.#onFileExport) {
|
|
3104
1136
|
this.#postError(message.id, "PLUGIN_FEATURE_NOT_CONFIGURED", "Plugin file export is not configured by the host");
|
|
3105
1137
|
return;
|
|
@@ -3122,7 +1154,8 @@ class PluginSurfaceHostImplementation {
|
|
|
3122
1154
|
this.#postError(message.id, bridgeError.errorCode, bridgeError.message, bridgeError.details, bridgeError.mutationOutcome);
|
|
3123
1155
|
}
|
|
3124
1156
|
finally {
|
|
3125
|
-
this.#pendingRequestControllers.
|
|
1157
|
+
if (this.#pendingRequestControllers.get(message.id) === controller)
|
|
1158
|
+
this.#pendingRequestControllers.delete(message.id);
|
|
3126
1159
|
}
|
|
3127
1160
|
}
|
|
3128
1161
|
async #handleCall(request) {
|
|
@@ -3147,7 +1180,8 @@ class PluginSurfaceHostImplementation {
|
|
|
3147
1180
|
this.#postError(request.id, bridgeError.errorCode, bridgeError.message, bridgeError.details, bridgeError.mutationOutcome);
|
|
3148
1181
|
}
|
|
3149
1182
|
finally {
|
|
3150
|
-
this.#pendingRequestControllers.
|
|
1183
|
+
if (this.#pendingRequestControllers.get(request.id) === controller)
|
|
1184
|
+
this.#pendingRequestControllers.delete(request.id);
|
|
3151
1185
|
}
|
|
3152
1186
|
}
|
|
3153
1187
|
async #handleConfirmationRequired(request, originalError, signal) {
|
|
@@ -3201,7 +1235,8 @@ class PluginSurfaceHostImplementation {
|
|
|
3201
1235
|
this.#postError(message.id, bridgeError.errorCode, bridgeError.message, bridgeError.details, bridgeError.mutationOutcome);
|
|
3202
1236
|
}
|
|
3203
1237
|
finally {
|
|
3204
|
-
this.#pendingRequestControllers.
|
|
1238
|
+
if (this.#pendingRequestControllers.get(message.id) === controller)
|
|
1239
|
+
this.#pendingRequestControllers.delete(message.id);
|
|
3205
1240
|
}
|
|
3206
1241
|
}
|
|
3207
1242
|
async #handleExecutionQuery(message) {
|
|
@@ -3221,7 +1256,8 @@ class PluginSurfaceHostImplementation {
|
|
|
3221
1256
|
this.#postError(message.id, bridgeError.errorCode, bridgeError.message, bridgeError.details, bridgeError.mutationOutcome);
|
|
3222
1257
|
}
|
|
3223
1258
|
finally {
|
|
3224
|
-
this.#pendingRequestControllers.
|
|
1259
|
+
if (this.#pendingRequestControllers.get(message.id) === controller)
|
|
1260
|
+
this.#pendingRequestControllers.delete(message.id);
|
|
3225
1261
|
}
|
|
3226
1262
|
}
|
|
3227
1263
|
async #handleExecutionEvents(message) {
|
|
@@ -3241,40 +1277,8 @@ class PluginSurfaceHostImplementation {
|
|
|
3241
1277
|
this.#postError(message.id, bridgeError.errorCode, bridgeError.message, bridgeError.details, bridgeError.mutationOutcome);
|
|
3242
1278
|
}
|
|
3243
1279
|
finally {
|
|
3244
|
-
this.#pendingRequestControllers.
|
|
3245
|
-
|
|
3246
|
-
}
|
|
3247
|
-
async #handleAssetRead(message) {
|
|
3248
|
-
if (this.#activeAssetReads >= maxConcurrentAssetReads) {
|
|
3249
|
-
throw new PluginBridgeError("PLUGIN_INVALID_REQUEST", "Plugin asset reads exceed the concurrency limit");
|
|
3250
|
-
}
|
|
3251
|
-
const asset = this.#assets.get(message.binding_id);
|
|
3252
|
-
if (!asset || asset.path !== message.path || asset.sha256 !== message.sha256 || !this.#assetSession || !this.#assetSessionID) {
|
|
3253
|
-
throw new PluginBridgeError("PLUGIN_ASSET_SESSION_INVALID", "Plugin asset request did not match the prepared surface document");
|
|
3254
|
-
}
|
|
3255
|
-
this.#activeAssetReads += 1;
|
|
3256
|
-
try {
|
|
3257
|
-
const result = await this.#postJSON(`/_redevplugin/api/plugins/surfaces/${encodeURIComponent(this.bootstrap.surfaceInstanceId)}/assets/read`, () => ({
|
|
3258
|
-
asset_session: this.#assetSession,
|
|
3259
|
-
asset_session_id: this.#assetSessionID,
|
|
3260
|
-
binding_id: asset.binding_id,
|
|
3261
|
-
}));
|
|
3262
|
-
if (!isAssetReadResult(result) || result.path !== asset.path || result.sha256 !== asset.sha256 || result.content_type !== asset.content_type) {
|
|
3263
|
-
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin asset endpoint returned mismatched content");
|
|
3264
|
-
}
|
|
3265
|
-
this.#postToRenderer({
|
|
3266
|
-
type: "redevplugin.surface.asset.response",
|
|
3267
|
-
request_id: message.request_id,
|
|
3268
|
-
binding_id: asset.binding_id,
|
|
3269
|
-
ok: true,
|
|
3270
|
-
path: result.path,
|
|
3271
|
-
sha256: result.sha256,
|
|
3272
|
-
content_type: result.content_type,
|
|
3273
|
-
content_base64: result.content_base64,
|
|
3274
|
-
});
|
|
3275
|
-
}
|
|
3276
|
-
finally {
|
|
3277
|
-
this.#activeAssetReads -= 1;
|
|
1280
|
+
if (this.#pendingRequestControllers.get(message.id) === controller)
|
|
1281
|
+
this.#pendingRequestControllers.delete(message.id);
|
|
3278
1282
|
}
|
|
3279
1283
|
}
|
|
3280
1284
|
#registerPendingRequest(id) {
|
|
@@ -3456,7 +1460,7 @@ class PluginSurfaceHostImplementation {
|
|
|
3456
1460
|
#scheduleLeaseRenewal() {
|
|
3457
1461
|
if (this.#leaseRenewalTimer)
|
|
3458
1462
|
clearTimeout(this.#leaseRenewalTimer);
|
|
3459
|
-
if (this.#disposed || this.#leaseRenewAtMs <= 0)
|
|
1463
|
+
if (this.#disposed || !this.#ready || this.#leaseRenewAtMs <= 0)
|
|
3460
1464
|
return;
|
|
3461
1465
|
this.#leaseRenewalTimer = setTimeout(() => {
|
|
3462
1466
|
this.#leaseRenewalTimer = undefined;
|
|
@@ -3520,7 +1524,7 @@ class PluginSurfaceHostImplementation {
|
|
|
3520
1524
|
this.#quiesce = quiesce;
|
|
3521
1525
|
this.#ready = false;
|
|
3522
1526
|
try {
|
|
3523
|
-
this.#
|
|
1527
|
+
this.#postToApplication({
|
|
3524
1528
|
type: "redevplugin.bridge.lifecycle",
|
|
3525
1529
|
event: { type: "dispose" },
|
|
3526
1530
|
quiesce_id: quiesce.id,
|
|
@@ -3544,12 +1548,11 @@ class PluginSurfaceHostImplementation {
|
|
|
3544
1548
|
const opening = !this.#ready && this.#openSignals !== undefined;
|
|
3545
1549
|
this.#ready = false;
|
|
3546
1550
|
this.#bridgeReady = false;
|
|
3547
|
-
this.#
|
|
1551
|
+
this.#applicationInitialized = false;
|
|
3548
1552
|
const terminalError = opening ? (this.#openingFailure ??= error) : error;
|
|
3549
1553
|
this.#openSignals?.portAcknowledged.reject(terminalError);
|
|
3550
|
-
this.#openSignals?.
|
|
3551
|
-
this.#openSignals?.
|
|
3552
|
-
this.#openSignals?.firstCommit.reject(terminalError);
|
|
1554
|
+
this.#openSignals?.entryReady.reject(terminalError);
|
|
1555
|
+
this.#openSignals?.appReady.reject(terminalError);
|
|
3553
1556
|
if (!opening)
|
|
3554
1557
|
this.#reportError(terminalError);
|
|
3555
1558
|
const revoke = this.#revokeSurface(true);
|
|
@@ -3562,7 +1565,7 @@ class PluginSurfaceHostImplementation {
|
|
|
3562
1565
|
this.#postError(id, "PLUGIN_JSON_LIMIT_EXCEEDED", "Plugin response exceeds the bridge message limit");
|
|
3563
1566
|
return;
|
|
3564
1567
|
}
|
|
3565
|
-
this.#
|
|
1568
|
+
this.#postToApplication(response);
|
|
3566
1569
|
}
|
|
3567
1570
|
#postError(id, errorCode, error, details, mutationOutcome) {
|
|
3568
1571
|
const errorDetails = details === undefined ? undefined : normalizePluginJSONObject(details);
|
|
@@ -3576,7 +1579,7 @@ class PluginSurfaceHostImplementation {
|
|
|
3576
1579
|
mutation_outcome: mutationOutcome,
|
|
3577
1580
|
});
|
|
3578
1581
|
if (!messageWithinLimit(response)) {
|
|
3579
|
-
this.#
|
|
1582
|
+
this.#postToApplication(removeUndefined({
|
|
3580
1583
|
type: "redevplugin.bridge.response",
|
|
3581
1584
|
id,
|
|
3582
1585
|
ok: false,
|
|
@@ -3586,9 +1589,9 @@ class PluginSurfaceHostImplementation {
|
|
|
3586
1589
|
}));
|
|
3587
1590
|
return;
|
|
3588
1591
|
}
|
|
3589
|
-
this.#
|
|
1592
|
+
this.#postToApplication(response);
|
|
3590
1593
|
}
|
|
3591
|
-
#
|
|
1594
|
+
#postToApplication(message) {
|
|
3592
1595
|
if (!this.#port) {
|
|
3593
1596
|
throw new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_REQUIRED", "Plugin surface port is not ready");
|
|
3594
1597
|
}
|
|
@@ -3898,6 +1901,31 @@ export class PluginSurfaceSlot {
|
|
|
3898
1901
|
throw error;
|
|
3899
1902
|
}
|
|
3900
1903
|
}
|
|
1904
|
+
/** Ask before removing a user-owned application; forced scope invalidation bypasses this. */
|
|
1905
|
+
requestClose() {
|
|
1906
|
+
this.#assertActive();
|
|
1907
|
+
return this.#tail.then(async () => {
|
|
1908
|
+
if (this.#disposed)
|
|
1909
|
+
return undefined;
|
|
1910
|
+
const host = this.#active;
|
|
1911
|
+
if (!host)
|
|
1912
|
+
return undefined;
|
|
1913
|
+
const result = await host.requestClose();
|
|
1914
|
+
if (!result)
|
|
1915
|
+
return undefined;
|
|
1916
|
+
if (this.#active === host) {
|
|
1917
|
+
this.#active = undefined;
|
|
1918
|
+
if (!this.#disposed)
|
|
1919
|
+
this.#setState("empty");
|
|
1920
|
+
}
|
|
1921
|
+
host.element.remove();
|
|
1922
|
+
try {
|
|
1923
|
+
this.#onSurfaceClosed?.(result);
|
|
1924
|
+
}
|
|
1925
|
+
catch { /* Presentation observers do not own revocation. */ }
|
|
1926
|
+
return result;
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
3901
1929
|
async close() {
|
|
3902
1930
|
this.#assertActive();
|
|
3903
1931
|
this.#transitionController?.abort();
|
|
@@ -4073,9 +2101,8 @@ function isSurfaceFileExportCandidate(value) {
|
|
|
4073
2101
|
}
|
|
4074
2102
|
function isSurfaceFileExportMessage(value) {
|
|
4075
2103
|
return hasExactKeys(value, [
|
|
4076
|
-
"type", "frame_generation_id", "surface_handle", "
|
|
2104
|
+
"type", "frame_generation_id", "surface_handle", "id", "file_name", "media_type", "content",
|
|
4077
2105
|
]) && validOpaqueHandle(value.frame_generation_id, "frame") && validOpaqueHandle(value.surface_handle, "surface") &&
|
|
4078
|
-
Number.isSafeInteger(value.action_sequence) && Number(value.action_sequence) > 0 &&
|
|
4079
2106
|
validBridgeRequestID(value.id, "file") && validFileExportName(value.file_name) &&
|
|
4080
2107
|
validFileExportMediaType(value.media_type) && value.content instanceof ArrayBuffer &&
|
|
4081
2108
|
value.content.byteLength > 0 && value.content.byteLength <= pluginSurfaceFileExportMaxBytes;
|
|
@@ -4105,14 +2132,14 @@ function isSurfaceRevocationReconciliation(value) {
|
|
|
4105
2132
|
typeof value.revoked === "boolean" &&
|
|
4106
2133
|
(value.revoked ? value.state === "closed" && value.previous_state === "active" : true);
|
|
4107
2134
|
}
|
|
4108
|
-
function
|
|
4109
|
-
const value = globalThis[
|
|
2135
|
+
function claimWebPluginBridge() {
|
|
2136
|
+
const value = globalThis[webPluginBridgeGlobalKey];
|
|
4110
2137
|
if (!isRecord(value) || typeof value.claim !== "function") {
|
|
4111
|
-
throw new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_REQUIRED", "
|
|
2138
|
+
throw new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_REQUIRED", "Web application bridge is unavailable");
|
|
4112
2139
|
}
|
|
4113
2140
|
const claimed = value.claim();
|
|
4114
2141
|
if (!claimed || !isMessagePortLike(claimed.port) || !validOpaqueHandle(claimed.surfaceHandle, "surface")) {
|
|
4115
|
-
throw new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_REQUIRED", "
|
|
2142
|
+
throw new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_REQUIRED", "Web application bridge was already claimed");
|
|
4116
2143
|
}
|
|
4117
2144
|
return claimed;
|
|
4118
2145
|
}
|
|
@@ -4138,11 +2165,9 @@ function createPluginSurfaceFrame() {
|
|
|
4138
2165
|
}
|
|
4139
2166
|
function hardenPluginSurfaceFrame(frame) {
|
|
4140
2167
|
frame.setAttribute("src", "about:blank");
|
|
4141
|
-
frame.setAttribute("sandbox", "allow-scripts");
|
|
2168
|
+
frame.setAttribute("sandbox", "allow-scripts allow-forms");
|
|
4142
2169
|
frame.setAttribute("allow", deniedPluginPermissionsPolicy);
|
|
4143
2170
|
frame.setAttribute("referrerpolicy", "no-referrer");
|
|
4144
|
-
if ("credentialless" in frame)
|
|
4145
|
-
frame.credentialless = true;
|
|
4146
2171
|
}
|
|
4147
2172
|
const deniedPluginPermissionsPolicy = [
|
|
4148
2173
|
"accelerometer 'none'",
|
|
@@ -4170,15 +2195,6 @@ const deniedPluginPermissionsPolicy = [
|
|
|
4170
2195
|
"xr-spatial-tracking 'none'",
|
|
4171
2196
|
].join("; ");
|
|
4172
2197
|
const openingProgressDelayMs = 300;
|
|
4173
|
-
function randomOpaqueNonce() {
|
|
4174
|
-
const cryptoLike = globalThis.crypto;
|
|
4175
|
-
if (!cryptoLike?.getRandomValues) {
|
|
4176
|
-
throw new PluginBridgeError("PLUGIN_BRIDGE_HANDSHAKE_FAILED", "Web Crypto random values are unavailable");
|
|
4177
|
-
}
|
|
4178
|
-
const bytes = new Uint8Array(24);
|
|
4179
|
-
cryptoLike.getRandomValues(bytes);
|
|
4180
|
-
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4181
|
-
}
|
|
4182
2198
|
function randomOpaqueHandle(prefix) {
|
|
4183
2199
|
const cryptoLike = globalThis.crypto;
|
|
4184
2200
|
if (!cryptoLike?.getRandomValues) {
|
|
@@ -4188,9 +2204,6 @@ function randomOpaqueHandle(prefix) {
|
|
|
4188
2204
|
cryptoLike.getRandomValues(bytes);
|
|
4189
2205
|
return `${prefix}_${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
4190
2206
|
}
|
|
4191
|
-
function escapeHTMLAttribute(value) {
|
|
4192
|
-
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
4193
|
-
}
|
|
4194
2207
|
function validateHostBootstrap(bootstrap) {
|
|
4195
2208
|
for (const value of [
|
|
4196
2209
|
bootstrap.pluginId,
|
|
@@ -4217,7 +2230,7 @@ function validateHostBootstrap(bootstrap) {
|
|
|
4217
2230
|
}
|
|
4218
2231
|
function validateSurfacePreparation(bootstrap, preparation) {
|
|
4219
2232
|
if (!isSurfacePreparationResult(preparation)) {
|
|
4220
|
-
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin surface prepare returned an invalid
|
|
2233
|
+
throw new PluginBridgeError("PLUGIN_CONTRACT_MISMATCH", "Plugin surface prepare returned an invalid Web document");
|
|
4221
2234
|
}
|
|
4222
2235
|
if (preparation.management_revision !== bootstrap.managementRevision || preparation.revoke_epoch !== bootstrap.revokeEpoch) {
|
|
4223
2236
|
throw new PluginBridgeError("PLUGIN_GATEWAY_TOKEN_INVALID", "Plugin surface state changed during prepare");
|
|
@@ -4225,8 +2238,7 @@ function validateSurfacePreparation(bootstrap, preparation) {
|
|
|
4225
2238
|
if (preparation.asset_session_nonce !== bootstrap.assetSessionNonce) {
|
|
4226
2239
|
throw new PluginBridgeError("PLUGIN_ASSET_SESSION_INVALID", "Plugin asset session nonce changed during prepare");
|
|
4227
2240
|
}
|
|
4228
|
-
if (preparation.entry_path !== bootstrap.entryPath || preparation.entry_sha256 !== bootstrap.entrySHA256
|
|
4229
|
-
preparation.document.entry_path !== bootstrap.entryPath || preparation.document.entry_sha256 !== bootstrap.entrySHA256) {
|
|
2241
|
+
if (preparation.entry_path !== bootstrap.entryPath || preparation.entry_sha256 !== bootstrap.entrySHA256) {
|
|
4230
2242
|
throw new PluginBridgeError("PLUGIN_ASSET_SESSION_INVALID", "Plugin surface entry changed during prepare");
|
|
4231
2243
|
}
|
|
4232
2244
|
}
|
|
@@ -4254,54 +2266,24 @@ function isSurfacePreparationResult(value) {
|
|
|
4254
2266
|
Number.isSafeInteger(value.management_revision) && Number(value.management_revision) >= 1 &&
|
|
4255
2267
|
Number.isSafeInteger(value.revoke_epoch) && Number(value.revoke_epoch) >= 1 &&
|
|
4256
2268
|
Number.isFinite(issuedAt) && Number.isFinite(expiresAt) && expiresAt > issuedAt &&
|
|
4257
|
-
|
|
2269
|
+
isWebSurfaceDocument(value.document);
|
|
4258
2270
|
}
|
|
4259
|
-
function
|
|
4260
|
-
if (!
|
|
4261
|
-
"schema_version",
|
|
4262
|
-
"entry_path",
|
|
4263
|
-
"entry_sha256",
|
|
4264
|
-
"title",
|
|
4265
|
-
"language",
|
|
4266
|
-
"direction",
|
|
4267
|
-
"body_html",
|
|
4268
|
-
"styles",
|
|
4269
|
-
"worker",
|
|
4270
|
-
"assets",
|
|
4271
|
-
"critical_bytes",
|
|
4272
|
-
]))
|
|
4273
|
-
return false;
|
|
4274
|
-
if (!Array.isArray(value.assets) || value.assets.length > maxOpaqueSurfaceLazyAssets)
|
|
2271
|
+
export function isWebSurfaceDocument(value) {
|
|
2272
|
+
if (!hasExactKeys(value, ["schema_version", "origin", "parent_origin", "bootstrap_url", "entry_url"]) || value.schema_version !== webSurfaceDocumentSchemaVersion)
|
|
4275
2273
|
return false;
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
for (const asset of value.assets) {
|
|
4279
|
-
if (!hasExactKeys(asset, ["binding_id", "logical_ids", "path", "sha256", "size", "content_type"]) ||
|
|
4280
|
-
!validOpaqueHandle(asset.binding_id, "asset") || !validPackagePath(asset.path) || !validSHA256(asset.sha256) ||
|
|
4281
|
-
!Array.isArray(asset.logical_ids) || asset.logical_ids.length < 1 || asset.logical_ids.length > 16 ||
|
|
4282
|
-
asset.logical_ids.some((logicalID) => !validUIIdentifier(logicalID) || logicalIDs.has(logicalID)) ||
|
|
4283
|
-
!Number.isSafeInteger(asset.size) || Number(asset.size) < 0 || Number(asset.size) > maxOpaqueSurfaceLazyBytes ||
|
|
4284
|
-
typeof asset.content_type !== "string" || asset.content_type.length < 1 || asset.content_type.length > 256) {
|
|
4285
|
-
return false;
|
|
4286
|
-
}
|
|
4287
|
-
for (const logicalID of asset.logical_ids)
|
|
4288
|
-
logicalIDs.add(logicalID);
|
|
4289
|
-
lazyBytes += Number(asset.size);
|
|
4290
|
-
if (!Number.isSafeInteger(lazyBytes) || lazyBytes > maxOpaqueSurfaceLazyBytes)
|
|
2274
|
+
try {
|
|
2275
|
+
if ([value.origin, value.parent_origin, value.bootstrap_url, value.entry_url].some(v => typeof v !== "string"))
|
|
4291
2276
|
return false;
|
|
2277
|
+
const origin = new URL(String(value.origin)), parent = new URL(String(value.parent_origin));
|
|
2278
|
+
const bootstrap = new URL(String(value.bootstrap_url)), entry = new URL(String(value.entry_url));
|
|
2279
|
+
const validOrigin = (u, raw) => u.origin === raw && !u.username && !u.password && (u.protocol === "https:" || (u.protocol === "http:" && (u.hostname === "localhost" || u.hostname.endsWith(".localhost"))));
|
|
2280
|
+
return validOrigin(origin, value.origin) && validOrigin(parent, value.parent_origin) && origin.hostname !== parent.hostname &&
|
|
2281
|
+
bootstrap.origin === origin.origin && !bootstrap.username && !bootstrap.password && bootstrap.pathname === "/_redevplugin_web/bootstrap" && !bootstrap.search && /^#[a-f0-9]{64}$/.test(bootstrap.hash) &&
|
|
2282
|
+
entry.origin === origin.origin && !entry.username && !entry.password && !entry.search && !entry.hash && !entry.pathname.startsWith("/_redevplugin_web/");
|
|
2283
|
+
}
|
|
2284
|
+
catch {
|
|
2285
|
+
return false;
|
|
4292
2286
|
}
|
|
4293
|
-
return value.schema_version === opaqueSurfaceDocumentSchemaVersion &&
|
|
4294
|
-
validPackagePath(value.entry_path) &&
|
|
4295
|
-
validSHA256(value.entry_sha256) &&
|
|
4296
|
-
(value.title === undefined || (typeof value.title === "string" && value.title.length <= 256)) &&
|
|
4297
|
-
(value.language === undefined || (typeof value.language === "string" && value.language.length <= 64)) &&
|
|
4298
|
-
(value.direction === undefined || value.direction === "ltr" || value.direction === "rtl" || value.direction === "auto") &&
|
|
4299
|
-
typeof value.body_html === "string" && value.body_html.length <= 4 * 1024 * 1024 &&
|
|
4300
|
-
Array.isArray(value.styles) && value.styles.every((style) => hasExactKeys(style, ["path", "sha256", "content"]) && validPackagePath(style.path) && validSHA256(style.sha256) && typeof style.content === "string" && style.content.length <= 2 * 1024 * 1024) &&
|
|
4301
|
-
hasExactKeys(value.worker, ["path", "sha256", "type", "content"]) && validPackagePath(value.worker.path) && validSHA256(value.worker.sha256) && value.worker.type === "classic" && typeof value.worker.content === "string" && value.worker.content.length <= 4 * 1024 * 1024 &&
|
|
4302
|
-
new Set(value.assets.map((asset) => asset.binding_id)).size === value.assets.length &&
|
|
4303
|
-
new Set(value.assets.map((asset) => asset.path)).size === value.assets.length &&
|
|
4304
|
-
Number.isSafeInteger(value.critical_bytes) && Number(value.critical_bytes) >= 0 && Number(value.critical_bytes) <= 8 * 1024 * 1024;
|
|
4305
2287
|
}
|
|
4306
2288
|
function isPluginBridgeContextMessage(value) {
|
|
4307
2289
|
return hasExactKeys(value, ["type", "context"]) && value.type === "redevplugin.bridge.context" && isPluginSurfaceContext(value.context);
|
|
@@ -4421,14 +2403,6 @@ function isBridgeCancelMessage(value) {
|
|
|
4421
2403
|
value.type === "redevplugin.bridge.cancel" &&
|
|
4422
2404
|
validBridgeRequestID(value.id);
|
|
4423
2405
|
}
|
|
4424
|
-
function isAssetReadMessage(value) {
|
|
4425
|
-
return hasExactKeys(value, ["type", "request_id", "binding_id", "path", "sha256"]) &&
|
|
4426
|
-
value.type === "redevplugin.surface.asset.read" &&
|
|
4427
|
-
typeof value.request_id === "string" && value.request_id.length > 0 && value.request_id.length <= 128 &&
|
|
4428
|
-
validOpaqueHandle(value.binding_id, "asset") &&
|
|
4429
|
-
validPackagePath(value.path) &&
|
|
4430
|
-
validSHA256(value.sha256);
|
|
4431
|
-
}
|
|
4432
2406
|
function isBridgeResponseCandidate(value) {
|
|
4433
2407
|
return isRecord(value) && value.type === "redevplugin.bridge.response" && typeof value.id === "string";
|
|
4434
2408
|
}
|
|
@@ -4456,11 +2430,6 @@ function isBridgeResponse(value) {
|
|
|
4456
2430
|
return false;
|
|
4457
2431
|
}
|
|
4458
2432
|
}
|
|
4459
|
-
function validCanvasAccessibilityState(value) {
|
|
4460
|
-
return hasExactKeys(value, ["label", "description"]) &&
|
|
4461
|
-
typeof value.label === "string" && value.label.length > 0 && value.label.length <= 256 &&
|
|
4462
|
-
typeof value.description === "string" && value.description.length <= 1024;
|
|
4463
|
-
}
|
|
4464
2433
|
function isWorkerErrorDetails(value) {
|
|
4465
2434
|
return hasExactKeys(value, ["worker_error_code", "worker_error_message", "worker_error_origin"]) &&
|
|
4466
2435
|
typeof value.worker_error_code === "string" && businessErrorCodePattern.test(value.worker_error_code) &&
|
|
@@ -4545,179 +2514,6 @@ function isLifecycleMessage(value) {
|
|
|
4545
2514
|
(value.event.type === "ready" || value.event.type === "visible" || value.event.type === "hidden" || value.event.type === "dispose") &&
|
|
4546
2515
|
(value.quiesce_id === undefined || (value.event.type === "dispose" && validOpaqueHandle(value.quiesce_id, "quiesce")));
|
|
4547
2516
|
}
|
|
4548
|
-
function isActionMessage(value) {
|
|
4549
|
-
return hasAllowedKeys(value, ["type", "action", "event", "target_key", "edit_revision", "is_composing", "value", "checked", "form_data"]) &&
|
|
4550
|
-
value.type === "redevplugin.ui.action" &&
|
|
4551
|
-
validActionID(value.action) &&
|
|
4552
|
-
(value.event === "click" || value.event === "input" || value.event === "change" || value.event === "submit" || value.event === "escape") &&
|
|
4553
|
-
validUIIdentifier(value.target_key) && Number.isSafeInteger(value.edit_revision) && Number(value.edit_revision) >= 0 &&
|
|
4554
|
-
typeof value.is_composing === "boolean" &&
|
|
4555
|
-
(value.value == null || (typeof value.value === "string" && value.value.length <= 65536)) &&
|
|
4556
|
-
(value.checked == null || typeof value.checked === "boolean") &&
|
|
4557
|
-
(value.form_data == null || (isRecord(value.form_data) && Object.entries(value.form_data).every(([key, item]) => validActionID(key) && typeof item === "string" && item.length <= 65536)));
|
|
4558
|
-
}
|
|
4559
|
-
function publicActionEvent(value) {
|
|
4560
|
-
return removeUndefined({
|
|
4561
|
-
action: value.action,
|
|
4562
|
-
event: value.event,
|
|
4563
|
-
targetKey: value.target_key,
|
|
4564
|
-
editRevision: value.edit_revision,
|
|
4565
|
-
isComposing: value.is_composing,
|
|
4566
|
-
value: value.value,
|
|
4567
|
-
checked: value.checked,
|
|
4568
|
-
form_data: value.form_data,
|
|
4569
|
-
});
|
|
4570
|
-
}
|
|
4571
|
-
function publicKeyboardInputMessage(value) {
|
|
4572
|
-
if (!isRecord(value) || !hasExactKeys(value, ["type", "event"]) || value.type !== "redevplugin.ui.keyboard.input" || !isRecord(value.event))
|
|
4573
|
-
return undefined;
|
|
4574
|
-
const event = value.event;
|
|
4575
|
-
if (!hasExactKeys(event, ["event", "code", "key", "repeat", "alt_key", "ctrl_key", "meta_key", "shift_key", "is_composing", "target_key", "target_kind", "default_prevented", "binding_id"]) ||
|
|
4576
|
-
(event.event !== "keydown" && event.event !== "keyup") || typeof event.code !== "string" || event.code.length > 64 ||
|
|
4577
|
-
typeof event.key !== "string" || event.key.length > 64 || typeof event.repeat !== "boolean" ||
|
|
4578
|
-
typeof event.alt_key !== "boolean" || typeof event.ctrl_key !== "boolean" || typeof event.meta_key !== "boolean" ||
|
|
4579
|
-
typeof event.shift_key !== "boolean" || typeof event.is_composing !== "boolean" ||
|
|
4580
|
-
(event.target_key !== null && !validUIIdentifier(event.target_key)) ||
|
|
4581
|
-
(event.target_kind !== "canvas" && event.target_kind !== "editable" && event.target_kind !== "control" && event.target_kind !== "surface") ||
|
|
4582
|
-
typeof event.default_prevented !== "boolean" || (event.binding_id !== null && !validActionID(event.binding_id)))
|
|
4583
|
-
return undefined;
|
|
4584
|
-
return {
|
|
4585
|
-
event: event.event,
|
|
4586
|
-
code: event.code,
|
|
4587
|
-
key: event.key,
|
|
4588
|
-
repeat: event.repeat,
|
|
4589
|
-
altKey: event.alt_key,
|
|
4590
|
-
ctrlKey: event.ctrl_key,
|
|
4591
|
-
metaKey: event.meta_key,
|
|
4592
|
-
shiftKey: event.shift_key,
|
|
4593
|
-
isComposing: event.is_composing,
|
|
4594
|
-
targetKey: event.target_key,
|
|
4595
|
-
targetKind: event.target_kind,
|
|
4596
|
-
defaultPrevented: event.default_prevented,
|
|
4597
|
-
bindingId: event.binding_id,
|
|
4598
|
-
};
|
|
4599
|
-
}
|
|
4600
|
-
function isCanvasReadyCandidate(value) {
|
|
4601
|
-
return isRecord(value) && value.type === "redevplugin.ui.canvas.ready" && typeof value.id === "string" && typeof value.canvas_id === "string";
|
|
4602
|
-
}
|
|
4603
|
-
function isCanvasReadyMessage(value) {
|
|
4604
|
-
return hasExactKeys(value, ["type", "id", "canvas_id", "canvas", "css_width", "css_height", "device_pixel_ratio"]) &&
|
|
4605
|
-
value.type === "redevplugin.ui.canvas.ready" && validBridgeRequestID(value.id, "canvas") && validUIIdentifier(value.canvas_id) &&
|
|
4606
|
-
isOffscreenCanvasLike(value.canvas) && validSurfaceDimension(value.css_width) && validSurfaceDimension(value.css_height) &&
|
|
4607
|
-
validDevicePixelRatio(value.device_pixel_ratio);
|
|
4608
|
-
}
|
|
4609
|
-
function isImageReadyCandidate(value) {
|
|
4610
|
-
return isRecord(value) && value.type === "redevplugin.ui.asset.image.ready" && typeof value.id === "string" && typeof value.asset_id === "string";
|
|
4611
|
-
}
|
|
4612
|
-
function isImageReadyMessage(value) {
|
|
4613
|
-
return hasExactKeys(value, ["type", "id", "asset_id", "image", "width", "height"]) &&
|
|
4614
|
-
value.type === "redevplugin.ui.asset.image.ready" && validBridgeRequestID(value.id, "asset") && validUIIdentifier(value.asset_id) &&
|
|
4615
|
-
isImageBitmapLike(value.image) && Number.isInteger(value.width) && Number(value.width) > 0 && Number(value.width) <= opaqueSurfaceRenderLimits.max_image_dimension &&
|
|
4616
|
-
Number.isInteger(value.height) && Number(value.height) > 0 && Number(value.height) <= opaqueSurfaceRenderLimits.max_image_dimension &&
|
|
4617
|
-
value.image.width === value.width && value.image.height === value.height;
|
|
4618
|
-
}
|
|
4619
|
-
function publicCanvasInputMessage(value) {
|
|
4620
|
-
if (!hasExactKeys(value, ["type", "canvas_id", "event"]) || value.type !== "redevplugin.ui.canvas.input" ||
|
|
4621
|
-
!validUIIdentifier(value.canvas_id) || !isRecord(value.event))
|
|
4622
|
-
return undefined;
|
|
4623
|
-
const event = value.event;
|
|
4624
|
-
if (hasExactKeys(event, ["type"]) && (event.type === "focus" || event.type === "blur")) {
|
|
4625
|
-
return { canvasId: value.canvas_id, event: { type: event.type } };
|
|
4626
|
-
}
|
|
4627
|
-
if (hasExactKeys(event, ["type", "css_width", "css_height", "device_pixel_ratio"]) && event.type === "resize" &&
|
|
4628
|
-
validSurfaceDimension(event.css_width) && validSurfaceDimension(event.css_height) && validDevicePixelRatio(event.device_pixel_ratio)) {
|
|
4629
|
-
return {
|
|
4630
|
-
canvasId: value.canvas_id,
|
|
4631
|
-
event: { type: "resize", cssWidth: event.css_width, cssHeight: event.css_height, devicePixelRatio: event.device_pixel_ratio },
|
|
4632
|
-
};
|
|
4633
|
-
}
|
|
4634
|
-
if (hasExactKeys(event, ["type", "event", "code", "key", "repeat", "alt_key", "ctrl_key", "meta_key", "shift_key"]) &&
|
|
4635
|
-
event.type === "key" && (event.event === "keydown" || event.event === "keyup") &&
|
|
4636
|
-
typeof event.code === "string" && event.code.length <= 64 && typeof event.key === "string" && event.key.length <= 64 &&
|
|
4637
|
-
typeof event.repeat === "boolean" && typeof event.alt_key === "boolean" && typeof event.ctrl_key === "boolean" &&
|
|
4638
|
-
typeof event.meta_key === "boolean" && typeof event.shift_key === "boolean") {
|
|
4639
|
-
return {
|
|
4640
|
-
canvasId: value.canvas_id,
|
|
4641
|
-
event: {
|
|
4642
|
-
type: "key",
|
|
4643
|
-
event: event.event,
|
|
4644
|
-
code: event.code,
|
|
4645
|
-
key: event.key,
|
|
4646
|
-
repeat: event.repeat,
|
|
4647
|
-
altKey: event.alt_key,
|
|
4648
|
-
ctrlKey: event.ctrl_key,
|
|
4649
|
-
metaKey: event.meta_key,
|
|
4650
|
-
shiftKey: event.shift_key,
|
|
4651
|
-
},
|
|
4652
|
-
};
|
|
4653
|
-
}
|
|
4654
|
-
if (hasExactKeys(event, ["type", "event", "pointer_id", "pointer_type", "buttons", "button", "x", "y", "pressure"]) &&
|
|
4655
|
-
event.type === "pointer" && ["pointerdown", "pointermove", "pointerup", "pointercancel"].includes(String(event.event)) &&
|
|
4656
|
-
Number.isInteger(event.pointer_id) && Number(event.pointer_id) >= 0 &&
|
|
4657
|
-
["mouse", "pen", "touch", "unknown"].includes(String(event.pointer_type)) &&
|
|
4658
|
-
Number.isInteger(event.buttons) && Number(event.buttons) >= 0 && Number(event.buttons) <= 31 &&
|
|
4659
|
-
Number.isInteger(event.button) && Number(event.button) >= -1 && Number(event.button) <= 4 &&
|
|
4660
|
-
validCanvasCoordinate(event.x) && validCanvasCoordinate(event.y) &&
|
|
4661
|
-
typeof event.pressure === "number" && Number.isFinite(event.pressure) && event.pressure >= 0 && event.pressure <= 1) {
|
|
4662
|
-
return {
|
|
4663
|
-
canvasId: value.canvas_id,
|
|
4664
|
-
event: {
|
|
4665
|
-
type: "pointer",
|
|
4666
|
-
event: event.event,
|
|
4667
|
-
pointerId: Number(event.pointer_id),
|
|
4668
|
-
pointerType: event.pointer_type,
|
|
4669
|
-
buttons: Number(event.buttons),
|
|
4670
|
-
button: Number(event.button),
|
|
4671
|
-
x: event.x,
|
|
4672
|
-
y: event.y,
|
|
4673
|
-
pressure: event.pressure,
|
|
4674
|
-
},
|
|
4675
|
-
};
|
|
4676
|
-
}
|
|
4677
|
-
if (hasExactKeys(event, ["type", "x", "y", "delta_x", "delta_y", "delta_mode", "alt_key", "ctrl_key", "meta_key", "shift_key"]) &&
|
|
4678
|
-
event.type === "wheel" && validCanvasCoordinate(event.x) && validCanvasCoordinate(event.y) &&
|
|
4679
|
-
validCanvasWheelDelta(event.delta_x) && validCanvasWheelDelta(event.delta_y) &&
|
|
4680
|
-
(event.delta_mode === 0 || event.delta_mode === 1 || event.delta_mode === 2) &&
|
|
4681
|
-
typeof event.alt_key === "boolean" && typeof event.ctrl_key === "boolean" &&
|
|
4682
|
-
typeof event.meta_key === "boolean" && typeof event.shift_key === "boolean") {
|
|
4683
|
-
return {
|
|
4684
|
-
canvasId: value.canvas_id,
|
|
4685
|
-
event: {
|
|
4686
|
-
type: "wheel",
|
|
4687
|
-
x: event.x,
|
|
4688
|
-
y: event.y,
|
|
4689
|
-
deltaX: event.delta_x,
|
|
4690
|
-
deltaY: event.delta_y,
|
|
4691
|
-
deltaMode: event.delta_mode,
|
|
4692
|
-
altKey: event.alt_key,
|
|
4693
|
-
ctrlKey: event.ctrl_key,
|
|
4694
|
-
metaKey: event.meta_key,
|
|
4695
|
-
shiftKey: event.shift_key,
|
|
4696
|
-
},
|
|
4697
|
-
};
|
|
4698
|
-
}
|
|
4699
|
-
return undefined;
|
|
4700
|
-
}
|
|
4701
|
-
function isOffscreenCanvasLike(value) {
|
|
4702
|
-
return isRecord(value) && Number.isInteger(value.width) && Number(value.width) > 0 && Number(value.width) <= opaqueSurfaceRenderLimits.max_canvas_dimension &&
|
|
4703
|
-
Number.isInteger(value.height) && Number(value.height) > 0 && Number(value.height) <= opaqueSurfaceRenderLimits.max_canvas_dimension && typeof value.getContext === "function";
|
|
4704
|
-
}
|
|
4705
|
-
function isImageBitmapLike(value) {
|
|
4706
|
-
return isRecord(value) && Number.isInteger(value.width) && Number(value.width) > 0 && Number(value.width) <= opaqueSurfaceRenderLimits.max_image_dimension &&
|
|
4707
|
-
Number.isInteger(value.height) && Number(value.height) > 0 && Number(value.height) <= opaqueSurfaceRenderLimits.max_image_dimension && typeof value.close === "function";
|
|
4708
|
-
}
|
|
4709
|
-
function validSurfaceDimension(value) {
|
|
4710
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= opaqueSurfaceRenderLimits.max_canvas_dimension;
|
|
4711
|
-
}
|
|
4712
|
-
function validDevicePixelRatio(value) {
|
|
4713
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0.5 && value <= 4;
|
|
4714
|
-
}
|
|
4715
|
-
function validCanvasCoordinate(value) {
|
|
4716
|
-
return typeof value === "number" && Number.isFinite(value) && value >= -16384 && value <= 32768;
|
|
4717
|
-
}
|
|
4718
|
-
function validCanvasWheelDelta(value) {
|
|
4719
|
-
return typeof value === "number" && Number.isFinite(value) && value >= -32768 && value <= 32768;
|
|
4720
|
-
}
|
|
4721
2517
|
function isMessagePortLike(value) {
|
|
4722
2518
|
return isRecord(value) &&
|
|
4723
2519
|
typeof value.postMessage === "function" &&
|
|
@@ -4726,12 +2522,6 @@ function isMessagePortLike(value) {
|
|
|
4726
2522
|
typeof value.start === "function" &&
|
|
4727
2523
|
typeof value.close === "function";
|
|
4728
2524
|
}
|
|
4729
|
-
function isAssetReadResult(value) {
|
|
4730
|
-
return hasExactKeys(value, ["path", "sha256", "content_type", "content_base64"]) &&
|
|
4731
|
-
validPackagePath(value.path) && validSHA256(value.sha256) &&
|
|
4732
|
-
typeof value.content_type === "string" && value.content_type.length > 0 && value.content_type.length <= 256 &&
|
|
4733
|
-
typeof value.content_base64 === "string";
|
|
4734
|
-
}
|
|
4735
2525
|
function isGatewayTokenResult(value) {
|
|
4736
2526
|
return hasExactKeys(value, ["plugin_gateway_token", "plugin_gateway_token_id", "asset_session", "asset_session_id", "issued_at", "expires_at"]) &&
|
|
4737
2527
|
typeof value.plugin_gateway_token === "string" && value.plugin_gateway_token.length > 0 &&
|
|
@@ -4755,7 +2545,7 @@ const pluginMethodPattern = new RegExp("^[-A-Za-z0-9._:]{1,256}$");
|
|
|
4755
2545
|
const pluginActionPattern = new RegExp("^[-A-Za-z0-9._:]{1,128}$");
|
|
4756
2546
|
const pluginUIIdentifierPattern = new RegExp("^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$");
|
|
4757
2547
|
const opaqueHandlePattern = new RegExp("^[-A-Za-z0-9_]{8,160}$");
|
|
4758
|
-
const bridgeRequestIDPattern = /^(rpc|execution|
|
|
2548
|
+
const bridgeRequestIDPattern = /^(rpc|execution|file)_([1-9][0-9]{0,15})$/;
|
|
4759
2549
|
function validBridgeRequestID(value, expectedKind) {
|
|
4760
2550
|
if (typeof value !== "string")
|
|
4761
2551
|
return false;
|
|
@@ -4764,34 +2554,6 @@ function validBridgeRequestID(value, expectedKind) {
|
|
|
4764
2554
|
return false;
|
|
4765
2555
|
return Number.isSafeInteger(Number(match[2]));
|
|
4766
2556
|
}
|
|
4767
|
-
function validKeyboardBinding(value) {
|
|
4768
|
-
return isRecord(value) && hasExactKeys(value, ["id", "event", "code", "repeat", "altKey", "ctrlKey", "metaKey", "shiftKey", "targetKey", "targetKind"]) &&
|
|
4769
|
-
validActionID(value.id) && (value.event === "keydown" || value.event === "keyup") &&
|
|
4770
|
-
typeof value.code === "string" && value.code.length > 0 && value.code.length <= 64 &&
|
|
4771
|
-
typeof value.repeat === "boolean" && typeof value.altKey === "boolean" && typeof value.ctrlKey === "boolean" &&
|
|
4772
|
-
typeof value.metaKey === "boolean" && typeof value.shiftKey === "boolean" &&
|
|
4773
|
-
(value.targetKey === null || validUIIdentifier(value.targetKey)) &&
|
|
4774
|
-
(value.targetKind === "canvas" || value.targetKind === "editable" || value.targetKind === "control" || value.targetKind === "surface");
|
|
4775
|
-
}
|
|
4776
|
-
function validKeyboardBindings(value) {
|
|
4777
|
-
if (!Array.isArray(value) || value.length > 128 || !value.every(validKeyboardBinding))
|
|
4778
|
-
return false;
|
|
4779
|
-
const ids = new Set();
|
|
4780
|
-
const matchers = new Set();
|
|
4781
|
-
for (const binding of value) {
|
|
4782
|
-
if (ids.has(binding.id))
|
|
4783
|
-
return false;
|
|
4784
|
-
ids.add(binding.id);
|
|
4785
|
-
const matcher = JSON.stringify([
|
|
4786
|
-
binding.event, binding.code, binding.repeat, binding.altKey, binding.ctrlKey,
|
|
4787
|
-
binding.metaKey, binding.shiftKey, binding.targetKey, binding.targetKind,
|
|
4788
|
-
]);
|
|
4789
|
-
if (matchers.has(matcher))
|
|
4790
|
-
return false;
|
|
4791
|
-
matchers.add(matcher);
|
|
4792
|
-
}
|
|
4793
|
-
return true;
|
|
4794
|
-
}
|
|
4795
2557
|
function validMethod(value) {
|
|
4796
2558
|
return typeof value === "string" && pluginMethodPattern.test(value);
|
|
4797
2559
|
}
|
|
@@ -4918,6 +2680,15 @@ function requestCancelledError(options, posted) {
|
|
|
4918
2680
|
return streamReadAbortedError();
|
|
4919
2681
|
return new PluginBridgeError("PLUGIN_BRIDGE_CANCELLED", "Plugin bridge request was cancelled", undefined, undefined, mutationOutcome(options, posted));
|
|
4920
2682
|
}
|
|
2683
|
+
function abortableWebOriginPreparation(operation, signal) {
|
|
2684
|
+
return new Promise((resolve, reject) => {
|
|
2685
|
+
const onAbort = () => reject(new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", "Web origin preparation was aborted"));
|
|
2686
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2687
|
+
Promise.resolve(operation).then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
2688
|
+
if (signal.aborted)
|
|
2689
|
+
onAbort();
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
4921
2692
|
function abortableConfirmationDecision(decision, signal) {
|
|
4922
2693
|
if (signal.aborted) {
|
|
4923
2694
|
return Promise.reject(new PluginBridgeError("PLUGIN_BRIDGE_DISPOSED", "Plugin confirmation was aborted"));
|