@floegence/redevplugin-ui 3.0.20 → 3.0.21

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.
Files changed (2) hide show
  1. package/dist/surface.js +39 -9
  2. package/package.json +2 -2
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();
@@ -956,7 +959,13 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
956
959
  sendParent({ type: "redevplugin.surface.error", error: String(message).slice(0, 512) });
957
960
  disposeRuntime();
958
961
  };
962
+ const cancelPendingRendererCommits = () => {
963
+ for (const cancel of [...pendingRendererCommits]) cancel();
964
+ };
959
965
  const disposeRuntime = () => {
966
+ if (runtimeDisposed) return;
967
+ runtimeDisposed = true;
968
+ cancelPendingRendererCommits();
960
969
  if (workerHeartbeatTimer) clearTimeout(workerHeartbeatTimer);
961
970
  if (workerHeartbeatTimeout) clearTimeout(workerHeartbeatTimeout);
962
971
  workerHeartbeatTimer = undefined;
@@ -997,6 +1006,7 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
997
1006
  assetByLogicalID.clear();
998
1007
  };
999
1008
  const terminateQuiescedWorker = () => {
1009
+ cancelPendingRendererCommits();
1000
1010
  if (workerHeartbeatTimer) clearTimeout(workerHeartbeatTimer);
1001
1011
  if (workerHeartbeatTimeout) clearTimeout(workerHeartbeatTimeout);
1002
1012
  workerHeartbeatTimer = undefined;
@@ -1486,9 +1496,25 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
1486
1496
  parent.insertBefore(target, before || null);
1487
1497
  }
1488
1498
  };
1489
- const onAnimationFrame = (commit) => new Promise((resolve, reject) => requestAnimationFrame(() => {
1490
- try { commit(); resolve(); } catch (error) { reject(error); }
1491
- }));
1499
+ const commitOnRenderOpportunity = (commit) => new Promise((resolve, reject) => {
1500
+ if (runtimeDisposed) return resolve(false);
1501
+ let settled = false;
1502
+ let frameID;
1503
+ let fallbackTimer;
1504
+ const finish = (shouldCommit) => {
1505
+ if (settled) return;
1506
+ settled = true;
1507
+ if (frameID !== undefined) cancelAnimationFrame(frameID);
1508
+ if (fallbackTimer !== undefined) clearTimeout(fallbackTimer);
1509
+ pendingRendererCommits.delete(cancel);
1510
+ if (!shouldCommit || runtimeDisposed) return resolve(false);
1511
+ try { commit(); resolve(true); } catch (error) { reject(error); }
1512
+ };
1513
+ const cancel = () => finish(false);
1514
+ pendingRendererCommits.add(cancel);
1515
+ frameID = requestAnimationFrame(() => finish(true));
1516
+ fallbackTimer = setTimeout(() => finish(true), rendererCommitFallbackMs);
1517
+ });
1492
1518
  const mountWorkerTree = async (tree) => {
1493
1519
  if (uiRevision !== 0) throw new Error("plugin UI mount may only occur once");
1494
1520
  const built = buildWorkerSubtree(tree, { nodes: 0 }, 1, undefined);
@@ -1496,25 +1522,29 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
1496
1522
  if (!(root instanceof Element)) throw new Error("plugin UI root must be an element");
1497
1523
  validateCanvasIdentifiers(root);
1498
1524
  const renderState = captureRenderState();
1499
- await onAnimationFrame(() => {
1525
+ const committed = await commitOnRenderOpportunity(() => {
1500
1526
  document.body.replaceChildren(root);
1501
1527
  restoreRenderState(renderState);
1502
1528
  });
1529
+ if (!committed) return false;
1503
1530
  uiGraph = built.graph;
1504
1531
  uiNodes = built.nodes;
1505
1532
  uiRevision = 1;
1506
1533
  sendParent({ type: "redevplugin.surface.first_commit" });
1534
+ return true;
1507
1535
  };
1508
1536
  const patchWorkerTree = async (baseRevision, revision, operations) => {
1509
1537
  if (uiRevision < 1 || baseRevision !== uiRevision || revision !== baseRevision + 1) throw new Error("plugin UI patch revision is invalid");
1510
1538
  const validated = validatePatch(operations);
1511
1539
  const renderState = captureRenderState();
1512
- await onAnimationFrame(() => {
1540
+ const committed = await commitOnRenderOpportunity(() => {
1513
1541
  for (const operation of validated.plan) applyPatchOperation(operation);
1514
1542
  validated.graph.commit();
1515
1543
  restoreRenderState(renderState);
1516
1544
  });
1545
+ if (!committed) return false;
1517
1546
  uiRevision = revision;
1547
+ return true;
1518
1548
  };
1519
1549
  const actionPayload = (event, element, eventType = event.type) => {
1520
1550
  const target = event.target instanceof Element ? event.target.closest("[data-redevplugin-key]") : element;
@@ -2272,9 +2302,9 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
2272
2302
  completeWorkerRequest(message.id);
2273
2303
  return rejectWorkerRequest(message.id, "plugin render rate limit exceeded");
2274
2304
  }
2275
- void mountWorkerTree(message.tree).then(() => {
2305
+ void mountWorkerTree(message.tree).then((committed) => {
2276
2306
  completeWorkerRequest(message.id);
2277
- sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
2307
+ if (committed) sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
2278
2308
  }, (error) => fail(error && error.message || "plugin UI mount violated the protocol"));
2279
2309
  return;
2280
2310
  }
@@ -2285,9 +2315,9 @@ export function createOpaquePluginBootstrapHTML(options = {}) {
2285
2315
  completeWorkerRequest(message.id);
2286
2316
  return rejectWorkerRequest(message.id, "plugin render rate limit exceeded");
2287
2317
  }
2288
- void patchWorkerTree(message.base_revision, message.revision, message.operations).then(() => {
2318
+ void patchWorkerTree(message.base_revision, message.revision, message.operations).then((committed) => {
2289
2319
  completeWorkerRequest(message.id);
2290
- sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
2320
+ if (committed) sendWorker({ type: "redevplugin.bridge.response", id: message.id, ok: true });
2291
2321
  }, (error) => fail(error && error.message || "plugin UI patch violated the protocol"));
2292
2322
  return;
2293
2323
  }
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.21",
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.21"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"