@floegence/redevplugin-ui 3.0.20 → 3.0.22

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/plugin.d.ts CHANGED
@@ -2,5 +2,5 @@ export { PluginBridgeClient } from "./surface.js";
2
2
  export { PluginBridgeError } from "./errors.js";
3
3
  export { pluginAPIMajor } from "./contracts.gen.js";
4
4
  export { callCapabilityOperation, callCapabilityStream, callCapabilitySync, isCapabilityBusinessError, } from "./capability-client.js";
5
- export type { BridgeLifecycleEvent, MessagePortLike, PluginBridgeClientOptions, PluginBridgeRequestOptions, PluginCanvasAccessibilityState, PluginCanvasInputEvent, PluginCanvasKeyEvent, PluginCanvasPointerEvent, PluginCanvasResizeEvent, PluginCanvasSurface, PluginJSONObject, PluginJSONValue, PluginMethodResult, PluginExecutionFailureCode, PluginExecutionSnapshot, PluginExecutionProgress, PluginExecutionEvent, PluginExecutionEventList, PluginSurfaceContext, PluginUIActionEvent, PluginUIAttributeValue, PluginUIElementVNode, PluginUIPatchOperation, PluginUIVNode, } from "./surface.js";
5
+ export type { BridgeLifecycleEvent, MessagePortLike, PluginBridgeClientOptions, PluginBridgeRequestOptions, PluginCanvasAccessibilityState, PluginCanvasInputEvent, PluginCanvasKeyEvent, PluginCanvasPointerEvent, PluginCanvasWheelEvent, PluginCanvasResizeEvent, PluginCanvasSurface, PluginJSONObject, PluginJSONValue, PluginMethodResult, PluginExecutionFailureCode, PluginExecutionSnapshot, PluginExecutionProgress, PluginExecutionEvent, PluginExecutionEventList, PluginSurfaceContext, PluginUIActionEvent, PluginUIAttributeValue, PluginUIElementVNode, PluginUIPatchOperation, PluginUIVNode, } from "./surface.js";
6
6
  export type { PluginCapabilitySchema, PluginCapabilityEffect, PluginCapabilityOperationContract, PluginCapabilityStreamContract, PluginCapabilitySyncContract, PluginCapabilityStreamEvent, PluginCapabilityStreamReadResult, PluginExecution, PluginExecutionTerminalStatus, PluginExecutionWaitOptions, PluginStream, } from "./capability-client.js";
package/dist/surface.d.ts CHANGED
@@ -136,7 +136,19 @@ export type PluginCanvasPointerEvent = {
136
136
  y: number;
137
137
  pressure: number;
138
138
  };
139
- export type PluginCanvasInputEvent = PluginCanvasFocusEvent | PluginCanvasResizeEvent | PluginCanvasKeyEvent | PluginCanvasPointerEvent;
139
+ export type PluginCanvasWheelEvent = {
140
+ type: "wheel";
141
+ x: number;
142
+ y: number;
143
+ deltaX: number;
144
+ deltaY: number;
145
+ deltaMode: 0 | 1 | 2;
146
+ altKey: boolean;
147
+ ctrlKey: boolean;
148
+ metaKey: boolean;
149
+ shiftKey: boolean;
150
+ };
151
+ export type PluginCanvasInputEvent = PluginCanvasFocusEvent | PluginCanvasResizeEvent | PluginCanvasKeyEvent | PluginCanvasPointerEvent | PluginCanvasWheelEvent;
140
152
  export type PluginMethodResult<T = unknown> = {
141
153
  data: T;
142
154
  execution_id?: string;
package/dist/surface.js CHANGED
@@ -751,6 +751,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
751
751
  const maxImageTotalPixels = ${opaqueSurfaceRenderLimits.max_image_total_pixels};
752
752
  const workerHeartbeatIntervalMs = ${opaqueSurfaceRenderLimits.worker_heartbeat_interval_ms};
753
753
  const workerHeartbeatTimeoutMs = ${opaqueSurfaceRenderLimits.worker_heartbeat_timeout_ms};
754
+ const rendererCommitFallbackMs = 100;
754
755
  const maxCriticalDocumentBytes = ${8 * 1024 * 1024};
755
756
  const maxPrivateAssetBase64Length = ${Math.ceil((32 * 1024 * 1024) / 3) * 4};
756
757
  const maxLazyAssetCount = ${maxOpaqueSurfaceLazyAssets};
@@ -863,7 +864,9 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
863
864
  let workerHeartbeatTimer;
864
865
  let workerHeartbeatTimeout;
865
866
  let pendingQuiesceID;
867
+ let runtimeDisposed = false;
866
868
  const pendingWorkerRequests = new Set();
869
+ const pendingRendererCommits = new Set();
867
870
  const requestSequence = { rpc: 0, execution: 0, render: 0, canvas: 0, asset: 0 };
868
871
  let executionQuerySurfaceTokens = 8;
869
872
  let executionQuerySurfaceRefillAt = performance.now();
@@ -934,7 +937,12 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
934
937
  };
935
938
  document.addEventListener("pointerdown", (event) => sendInteraction("activation", event.target), true);
936
939
  document.addEventListener("focusin", (event) => sendInteraction("focus", event.target), true);
937
- document.addEventListener("wheel", (event) => sendInteraction("wheel", event.target, { localScroll: ownsLocalScroll(event) }), { capture: true, passive: true });
940
+ const ownsCanvasWheel = (target) => {
941
+ const element = target instanceof Element ? target.closest("canvas[data-redevplugin-canvas]") : null;
942
+ const canvasID = element?.getAttribute("data-redevplugin-canvas");
943
+ return validResourceIdentifier(canvasID) && canvasRuntimes.has(canvasID);
944
+ };
945
+ document.addEventListener("wheel", (event) => sendInteraction("wheel", event.target, { localScroll: ownsCanvasWheel(event.target) || ownsLocalScroll(event) }), { capture: true, passive: true });
938
946
  document.addEventListener("selectionchange", () => {
939
947
  const selection = document.getSelection();
940
948
  const target = selection?.anchorNode instanceof Element ? selection.anchorNode : selection?.anchorNode?.parentElement;
@@ -956,7 +964,13 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
956
964
  sendParent({ type: "redevplugin.surface.error", error: String(message).slice(0, 512) });
957
965
  disposeRuntime();
958
966
  };
967
+ const cancelPendingRendererCommits = () => {
968
+ for (const cancel of [...pendingRendererCommits]) cancel();
969
+ };
959
970
  const disposeRuntime = () => {
971
+ if (runtimeDisposed) return;
972
+ runtimeDisposed = true;
973
+ cancelPendingRendererCommits();
960
974
  if (workerHeartbeatTimer) clearTimeout(workerHeartbeatTimer);
961
975
  if (workerHeartbeatTimeout) clearTimeout(workerHeartbeatTimeout);
962
976
  workerHeartbeatTimer = undefined;
@@ -997,6 +1011,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
997
1011
  assetByLogicalID.clear();
998
1012
  };
999
1013
  const terminateQuiescedWorker = () => {
1014
+ cancelPendingRendererCommits();
1000
1015
  if (workerHeartbeatTimer) clearTimeout(workerHeartbeatTimer);
1001
1016
  if (workerHeartbeatTimeout) clearTimeout(workerHeartbeatTimeout);
1002
1017
  workerHeartbeatTimer = undefined;
@@ -1486,9 +1501,25 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
1486
1501
  parent.insertBefore(target, before || null);
1487
1502
  }
1488
1503
  };
1489
- const onAnimationFrame = (commit) => new Promise((resolve, reject) => requestAnimationFrame(() => {
1490
- try { commit(); resolve(); } catch (error) { reject(error); }
1491
- }));
1504
+ const commitOnRenderOpportunity = (commit) => new Promise((resolve, reject) => {
1505
+ if (runtimeDisposed) return resolve(false);
1506
+ let settled = false;
1507
+ let frameID;
1508
+ let fallbackTimer;
1509
+ const finish = (shouldCommit) => {
1510
+ if (settled) return;
1511
+ settled = true;
1512
+ if (frameID !== undefined) cancelAnimationFrame(frameID);
1513
+ if (fallbackTimer !== undefined) clearTimeout(fallbackTimer);
1514
+ pendingRendererCommits.delete(cancel);
1515
+ if (!shouldCommit || runtimeDisposed) return resolve(false);
1516
+ try { commit(); resolve(true); } catch (error) { reject(error); }
1517
+ };
1518
+ const cancel = () => finish(false);
1519
+ pendingRendererCommits.add(cancel);
1520
+ frameID = requestAnimationFrame(() => finish(true));
1521
+ fallbackTimer = setTimeout(() => finish(true), rendererCommitFallbackMs);
1522
+ });
1492
1523
  const mountWorkerTree = async (tree) => {
1493
1524
  if (uiRevision !== 0) throw new Error("plugin UI mount may only occur once");
1494
1525
  const built = buildWorkerSubtree(tree, { nodes: 0 }, 1, undefined);
@@ -1496,25 +1527,29 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
1496
1527
  if (!(root instanceof Element)) throw new Error("plugin UI root must be an element");
1497
1528
  validateCanvasIdentifiers(root);
1498
1529
  const renderState = captureRenderState();
1499
- await onAnimationFrame(() => {
1530
+ const committed = await commitOnRenderOpportunity(() => {
1500
1531
  document.body.replaceChildren(root);
1501
1532
  restoreRenderState(renderState);
1502
1533
  });
1534
+ if (!committed) return false;
1503
1535
  uiGraph = built.graph;
1504
1536
  uiNodes = built.nodes;
1505
1537
  uiRevision = 1;
1506
1538
  sendParent({ type: "redevplugin.surface.first_commit" });
1539
+ return true;
1507
1540
  };
1508
1541
  const patchWorkerTree = async (baseRevision, revision, operations) => {
1509
1542
  if (uiRevision < 1 || baseRevision !== uiRevision || revision !== baseRevision + 1) throw new Error("plugin UI patch revision is invalid");
1510
1543
  const validated = validatePatch(operations);
1511
1544
  const renderState = captureRenderState();
1512
- await onAnimationFrame(() => {
1545
+ const committed = await commitOnRenderOpportunity(() => {
1513
1546
  for (const operation of validated.plan) applyPatchOperation(operation);
1514
1547
  validated.graph.commit();
1515
1548
  restoreRenderState(renderState);
1516
1549
  });
1550
+ if (!committed) return false;
1517
1551
  uiRevision = revision;
1552
+ return true;
1518
1553
  };
1519
1554
  const actionPayload = (event, element, eventType = event.type) => {
1520
1555
  const target = event.target instanceof Element ? event.target.closest("[data-redevplugin-key]") : element;
@@ -2129,8 +2164,25 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
2129
2164
  shift_key: Boolean(event.shiftKey),
2130
2165
  });
2131
2166
  };
2167
+ const handleWheel = (event) => {
2168
+ event.preventDefault();
2169
+ const rect = canvas.getBoundingClientRect();
2170
+ sendInput({
2171
+ type: "wheel",
2172
+ x: Math.min(32768, Math.max(-16384, event.clientX - rect.left)),
2173
+ y: Math.min(32768, Math.max(-16384, event.clientY - rect.top)),
2174
+ delta_x: Math.min(32768, Math.max(-32768, Number.isFinite(event.deltaX) ? event.deltaX : 0)),
2175
+ delta_y: Math.min(32768, Math.max(-32768, Number.isFinite(event.deltaY) ? event.deltaY : 0)),
2176
+ delta_mode: [0, 1, 2].includes(event.deltaMode) ? event.deltaMode : 0,
2177
+ alt_key: Boolean(event.altKey),
2178
+ ctrl_key: Boolean(event.ctrlKey),
2179
+ meta_key: Boolean(event.metaKey),
2180
+ shift_key: Boolean(event.shiftKey),
2181
+ });
2182
+ };
2132
2183
  for (const type of ["pointerdown", "pointermove", "pointerup", "pointercancel"]) listen(type, handlePointer);
2133
2184
  for (const type of ["keydown", "keyup"]) listen(type, handleKey);
2185
+ listen("wheel", handleWheel);
2134
2186
  listen("focus", () => sendInput({ type: "focus" }));
2135
2187
  listen("blur", () => sendInput({ type: "blur" }));
2136
2188
  const observer = new ResizeObserver(() => {
@@ -2272,9 +2324,9 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
2272
2324
  completeWorkerRequest(message.id);
2273
2325
  return rejectWorkerRequest(message.id, "plugin render rate limit exceeded");
2274
2326
  }
2275
- void mountWorkerTree(message.tree).then(() => {
2327
+ void mountWorkerTree(message.tree).then((committed) => {
2276
2328
  completeWorkerRequest(message.id);
2277
- sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
2329
+ if (committed) sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
2278
2330
  }, (error) => fail(error && error.message || "plugin UI mount violated the protocol"));
2279
2331
  return;
2280
2332
  }
@@ -2285,9 +2337,9 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
2285
2337
  completeWorkerRequest(message.id);
2286
2338
  return rejectWorkerRequest(message.id, "plugin render rate limit exceeded");
2287
2339
  }
2288
- void patchWorkerTree(message.base_revision, message.revision, message.operations).then(() => {
2340
+ void patchWorkerTree(message.base_revision, message.revision, message.operations).then((committed) => {
2289
2341
  completeWorkerRequest(message.id);
2290
- sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
2342
+ if (committed) sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
2291
2343
  }, (error) => fail(error && error.message || "plugin UI patch violated the protocol"));
2292
2344
  return;
2293
2345
  }
@@ -4268,6 +4320,28 @@ function publicCanvasInputMessage(value) {
4268
4320
  },
4269
4321
  };
4270
4322
  }
4323
+ if (hasExactKeys(event, ["type", "x", "y", "delta_x", "delta_y", "delta_mode", "alt_key", "ctrl_key", "meta_key", "shift_key"]) &&
4324
+ event.type === "wheel" && validCanvasCoordinate(event.x) && validCanvasCoordinate(event.y) &&
4325
+ validCanvasWheelDelta(event.delta_x) && validCanvasWheelDelta(event.delta_y) &&
4326
+ (event.delta_mode === 0 || event.delta_mode === 1 || event.delta_mode === 2) &&
4327
+ typeof event.alt_key === "boolean" && typeof event.ctrl_key === "boolean" &&
4328
+ typeof event.meta_key === "boolean" && typeof event.shift_key === "boolean") {
4329
+ return {
4330
+ canvasId: value.canvas_id,
4331
+ event: {
4332
+ type: "wheel",
4333
+ x: event.x,
4334
+ y: event.y,
4335
+ deltaX: event.delta_x,
4336
+ deltaY: event.delta_y,
4337
+ deltaMode: event.delta_mode,
4338
+ altKey: event.alt_key,
4339
+ ctrlKey: event.ctrl_key,
4340
+ metaKey: event.meta_key,
4341
+ shiftKey: event.shift_key,
4342
+ },
4343
+ };
4344
+ }
4271
4345
  return undefined;
4272
4346
  }
4273
4347
  function isOffscreenCanvasLike(value) {
@@ -4287,6 +4361,9 @@ function validDevicePixelRatio(value) {
4287
4361
  function validCanvasCoordinate(value) {
4288
4362
  return typeof value === "number" && Number.isFinite(value) && value >= -16384 && value <= 32768;
4289
4363
  }
4364
+ function validCanvasWheelDelta(value) {
4365
+ return typeof value === "number" && Number.isFinite(value) && value >= -32768 && value <= 32768;
4366
+ }
4290
4367
  function isMessagePortLike(value) {
4291
4368
  return isRecord(value) &&
4292
4369
  typeof value.postMessage === "function" &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/redevplugin-ui",
3
- "version": "3.0.20",
3
+ "version": "3.0.22",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -40,7 +40,7 @@
40
40
  "dist"
41
41
  ],
42
42
  "dependencies": {
43
- "@floegence/redevplugin-contracts": "3.0.20"
43
+ "@floegence/redevplugin-contracts": "3.0.22"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"