@dimina-kit/electron-deck 0.1.0-dev.20260701083540 → 0.1.0-dev.20260702145840

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 (42) hide show
  1. package/dist/client/index.d.ts +2 -0
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +92 -12
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/client/layout-client.d.ts +35 -14
  6. package/dist/client/layout-client.d.ts.map +1 -1
  7. package/dist/client/placement-publisher.d.ts +14 -0
  8. package/dist/client/placement-publisher.d.ts.map +1 -0
  9. package/dist/dock-react/index.js +1 -1
  10. package/dist/{electron-deck-C9_Yu3fd.js → electron-deck-B9fun93z.js} +105 -78
  11. package/dist/electron-deck-B9fun93z.js.map +1 -0
  12. package/dist/host/index.js +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/internal/deck-app.d.ts +43 -11
  15. package/dist/internal/deck-app.d.ts.map +1 -1
  16. package/dist/internal/wire-transport.d.ts +17 -14
  17. package/dist/internal/wire-transport.d.ts.map +1 -1
  18. package/dist/layout/index.d.ts +4 -0
  19. package/dist/layout/index.d.ts.map +1 -1
  20. package/dist/layout/index.js +2 -2
  21. package/dist/layout/placement-reconcile.d.ts +50 -0
  22. package/dist/layout/placement-reconcile.d.ts.map +1 -0
  23. package/dist/layout/snapshot-reconcile.d.ts +28 -0
  24. package/dist/layout/snapshot-reconcile.d.ts.map +1 -0
  25. package/dist/{layout-B1t15b_q.js → layout-ByOiOdnc.js} +267 -2
  26. package/dist/layout-ByOiOdnc.js.map +1 -0
  27. package/dist/main/index.js +1 -1
  28. package/dist/main/view-handle.d.ts.map +1 -1
  29. package/dist/preload/index.cjs +4 -4
  30. package/dist/preload/index.cjs.map +1 -1
  31. package/dist/preload/index.d.ts +1 -1
  32. package/dist/preload/index.js +4 -4
  33. package/dist/preload/index.js.map +1 -1
  34. package/dist/{protocol-BJqo-vP2.js → protocol-B6TOT9AP.js} +2 -2
  35. package/dist/protocol-B6TOT9AP.js.map +1 -0
  36. package/dist/shared/protocol.d.ts +1 -1
  37. package/dist/{view-handle-CR-yWNfr.js → view-handle-DDhpBuW-.js} +2 -2
  38. package/dist/{view-handle-CR-yWNfr.js.map → view-handle-DDhpBuW-.js.map} +1 -1
  39. package/package.json +2 -2
  40. package/dist/electron-deck-C9_Yu3fd.js.map +0 -1
  41. package/dist/layout-B1t15b_q.js.map +0 -1
  42. package/dist/protocol-BJqo-vP2.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { n as DeckRemoteError, r as EventNotBoundError } from "./errors-NE2ig7L5.js";
2
- import { i as DeckChannel, t as BRIDGE_PROTOCOL_VERSION } from "./protocol-BJqo-vP2.js";
3
- import { i as createScope, r as createCompositor, t as createViewHandle } from "./view-handle-CR-yWNfr.js";
2
+ import { i as DeckChannel, t as BRIDGE_PROTOCOL_VERSION } from "./protocol-B6TOT9AP.js";
3
+ import { i as createScope, r as createCompositor, t as createViewHandle } from "./view-handle-DDhpBuW-.js";
4
+ import { i as reconcile, n as dispatchOps, r as createInitialState, t as cleanSnapshot } from "./layout-ByOiOdnc.js";
4
5
  import { randomUUID } from "node:crypto";
5
6
  //#region src/events.ts
6
7
  var internals = /* @__PURE__ */ new WeakMap();
@@ -395,15 +396,15 @@ var WireTransport = class {
395
396
  deps;
396
397
  state = "idle";
397
398
  busSubscription = null;
398
- /** Whether the optional Place / LayoutSubscribe handlers were registered
399
+ /** Whether the optional Snapshot / LayoutSubscribe handlers were registered
399
400
  * (either eagerly at start() via deps, or lazily via armSlotChannels) — so
400
401
  * dispose() only removes channels it actually registered. */
401
- placeRegistered = false;
402
+ snapshotRegistered = false;
402
403
  layoutSubscribeRegistered = false;
403
404
  /** Effective slot callbacks. Populated from deps at start() (eager path, used
404
405
  * by the wire unit tests) OR by {@link armSlotChannels} (lazy path, used by
405
406
  * deck-app so an app with no anchored views registers only Invoke + Probe). */
406
- onPlaceCb = null;
407
+ onSnapshotCb = null;
407
408
  onLayoutSubscribeCb = null;
408
409
  constructor(deps) {
409
410
  this.deps = deps;
@@ -418,10 +419,10 @@ var WireTransport = class {
418
419
  cleanups.push(() => this.tryRemoveHandler(DeckChannel.Probe));
419
420
  this.deps.ipcMain.handle(DeckChannel.Invoke, (event, ...args) => this.handleInvoke(event?.sender?.id, event?.senderFrame, event?.sender?.mainFrame, args[0]));
420
421
  cleanups.push(() => this.tryRemoveHandler(DeckChannel.Invoke));
421
- if (this.deps.onPlace) {
422
- this.onPlaceCb = this.deps.onPlace;
423
- this.registerPlaceHandler();
424
- cleanups.push(() => this.unregisterPlaceHandler());
422
+ if (this.deps.onSnapshot) {
423
+ this.onSnapshotCb = this.deps.onSnapshot;
424
+ this.registerSnapshotHandler();
425
+ cleanups.push(() => this.unregisterSnapshotHandler());
425
426
  }
426
427
  if (this.deps.onLayoutSubscribe) {
427
428
  this.onLayoutSubscribeCb = this.deps.onLayoutSubscribe;
@@ -454,15 +455,15 @@ var WireTransport = class {
454
455
  console.error(`[electron-deck] removeHandler(${channel}) failed:`, e);
455
456
  }
456
457
  }
457
- registerPlaceHandler() {
458
- if (this.placeRegistered) return;
459
- this.deps.ipcMain.handle(DeckChannel.Place, (event, ...args) => this.handlePlace(event?.sender?.id, event?.senderFrame, event?.sender?.mainFrame, args[0]));
460
- this.placeRegistered = true;
458
+ registerSnapshotHandler() {
459
+ if (this.snapshotRegistered) return;
460
+ this.deps.ipcMain.handle(DeckChannel.Snapshot, (event, ...args) => this.handleSnapshot(event?.sender?.id, event?.senderFrame, event?.sender?.mainFrame, args[0]));
461
+ this.snapshotRegistered = true;
461
462
  }
462
- unregisterPlaceHandler() {
463
- if (!this.placeRegistered) return;
464
- this.placeRegistered = false;
465
- this.tryRemoveHandler(DeckChannel.Place);
463
+ unregisterSnapshotHandler() {
464
+ if (!this.snapshotRegistered) return;
465
+ this.snapshotRegistered = false;
466
+ this.tryRemoveHandler(DeckChannel.Snapshot);
466
467
  }
467
468
  registerLayoutSubscribeHandler() {
468
469
  if (this.layoutSubscribeRegistered) return;
@@ -482,11 +483,11 @@ var WireTransport = class {
482
483
  * no anchored views never registers these channels (keeping the wire's
483
484
  * handler footprint at Invoke + Probe until a slot is actually minted).
484
485
  */
485
- armSlotChannels(onPlace, onLayoutSubscribe) {
486
- this.onPlaceCb = onPlace;
486
+ armSlotChannels(onSnapshot, onLayoutSubscribe) {
487
+ this.onSnapshotCb = onSnapshot;
487
488
  this.onLayoutSubscribeCb = onLayoutSubscribe;
488
489
  if (this.state !== "started") return;
489
- this.registerPlaceHandler();
490
+ this.registerSnapshotHandler();
490
491
  this.registerLayoutSubscribeHandler();
491
492
  }
492
493
  dispose() {
@@ -505,12 +506,12 @@ var WireTransport = class {
505
506
  } catch (e) {
506
507
  console.error("[electron-deck] removeHandler(probe) failed:", e);
507
508
  }
508
- if (this.placeRegistered) {
509
- this.placeRegistered = false;
509
+ if (this.snapshotRegistered) {
510
+ this.snapshotRegistered = false;
510
511
  try {
511
- this.deps.ipcMain.removeHandler(DeckChannel.Place);
512
+ this.deps.ipcMain.removeHandler(DeckChannel.Snapshot);
512
513
  } catch (e) {
513
- console.error("[electron-deck] removeHandler(place) failed:", e);
514
+ console.error("[electron-deck] removeHandler(snapshot) failed:", e);
514
515
  }
515
516
  }
516
517
  if (this.layoutSubscribeRegistered) {
@@ -576,17 +577,16 @@ var WireTransport = class {
576
577
  }
577
578
  }
578
579
  /**
579
- * `__electron-deck:place` inbound. Same gate as {@link handleInvoke}
580
- * (trust + main-frame). On any failure (malformed payload, untrusted sender,
581
- * sub-frame) → DROP silently (return undefined; never call `onPlace`).
580
+ * `__electron-deck:snapshot` inbound. Same gate as {@link handleInvoke} (trust +
581
+ * main-frame). On any gate failure (non-object payload, untrusted sender,
582
+ * sub-frame) → DROP silently. The payload's inner shape is opaque to the wire —
583
+ * authorized + validated downstream.
582
584
  */
583
- handlePlace(senderId, senderFrame, mainFrame, rawMsg) {
584
- if (rawMsg === null || typeof rawMsg !== "object" || Array.isArray(rawMsg)) return;
585
- const msg = rawMsg;
586
- if (typeof msg.slotToken !== "string") return;
585
+ handleSnapshot(senderId, senderFrame, mainFrame, rawSnapshot) {
586
+ if (rawSnapshot === null || typeof rawSnapshot !== "object" || Array.isArray(rawSnapshot)) return;
587
587
  if (typeof senderId !== "number" || !this.deps.senderPolicy.isTrusted(senderId)) return;
588
588
  if (!this.isMainFrameSender(senderFrame, mainFrame)) return;
589
- this.onPlaceCb?.(senderId, msg.slotToken, msg.placement);
589
+ this.onSnapshotCb?.(senderId, rawSnapshot);
590
590
  }
591
591
  /**
592
592
  * `__electron-deck:layout-subscribe` inbound. Same gate as {@link handleInvoke}.
@@ -875,12 +875,31 @@ var DeckApp = class {
875
875
  /**
876
876
  * slot-token registry (view-handle.md「slot-token 握手」/ capability-and-lifecycle.md「anchor slotToken 原子下发」):
877
877
  * each anchored `placeIn` mints an unguessable token bound to (viewId, slotId,
878
- * authorizedWcId). The `__electron-deck:place` apply path looks the token up,
879
- * checks the sender is the authorized wc (anti-spoof), validates the placement,
880
- * then `apply`s it. `resend` re-pushes the slot-grant (layout-subscribe replay).
878
+ * authorizedWcId, zone). The `__electron-deck:snapshot` apply path authorizes
879
+ * each of the renderer's per-view tokens (token known AND authorized to the
880
+ * sender wc), DERIVES the view's identity (viewId) + z-order (zone→layer) from
881
+ * this registry (never trusting the renderer-reported fields), reconciles the
882
+ * cleaned window-level table, and drives each view's `apply`. `resend` re-pushes
883
+ * the slot-grant (layout-subscribe replay).
881
884
  */
882
885
  slotTokens = /* @__PURE__ */ new Map();
883
886
  slotSeq = 0;
887
+ /**
888
+ * Per-control-wc level-triggered reconciler state (keyed by authorizedWcId).
889
+ * The renderer publishes a whole window-level desired-placement table each
890
+ * frame; `reconcile` diffs it against the last-applied actual and emits ops, so
891
+ * a lost or spurious per-view edge self-corrects instead of sticking a view
892
+ * detached (the white-screen failure mode). Reset on layout-subscribe (reload)
893
+ * and dropped on wc revoke.
894
+ */
895
+ reconcileStates = /* @__PURE__ */ new Map();
896
+ /**
897
+ * Per-control-wc strictly-monotonic generation counter. Bumped on every
898
+ * layout-subscribe and stamped into each slot-grant, so a reload's higher
899
+ * generation resets the reconciler regardless of IPC ordering (a late in-flight
900
+ * pre-reload snapshot carries a lower generation → rejected).
901
+ */
902
+ perWcGeneration = /* @__PURE__ */ new Map();
884
903
  _runtime = null;
885
904
  startCalled = false;
886
905
  shutdownPromise = null;
@@ -1627,32 +1646,58 @@ var DeckApp = class {
1627
1646
  * builds can't serve a renderer slot anyway).
1628
1647
  */
1629
1648
  ensureSlotChannelsArmed() {
1630
- this.wireTransport?.armSlotChannels((senderId, slotToken, placement) => this.handleSlotPlace(senderId, slotToken, placement), (senderId) => this.handleLayoutSubscribe(senderId));
1649
+ this.wireTransport?.armSlotChannels((senderId, rawSnapshot) => this.handleSnapshot(senderId, rawSnapshot), (senderId) => this.handleLayoutSubscribe(senderId));
1631
1650
  }
1632
1651
  /**
1633
- * slot-token apply path (`__electron-deck:place`). The token is the credential:
1634
- * look it up, verify the sender IS the wc the token was granted to (anti-spoof),
1635
- * validate the placement, then apply. Any failure → DROP (silent).
1652
+ * slot-token apply path (`__electron-deck:snapshot`). The renderer publishes a
1653
+ * whole window-level desired-placement table; each entry's slotToken is the
1654
+ * credential. Steps:
1655
+ * 1. AUTHORIZE + clean: `cleanSnapshot` keeps only views whose token is known
1656
+ * AND authorized to `senderId` (anti-spoof), deriving each view's identity
1657
+ * (viewId) + z-order (zone→layer) from the registry — never from the
1658
+ * renderer-reported fields. A non-empty snapshot that fully fails
1659
+ * authorization is rejected wholesale (returns null) so a transient token
1660
+ * failure is NOT read as "detach everything".
1661
+ * 2. RECONCILE the cleaned table against this wc's last-applied actual state
1662
+ * → an ordered op list (level-triggered; a lost/spurious frame self-heals).
1663
+ * 3. DISPATCH the ops onto each view's `applyPlacement` sink.
1636
1664
  */
1637
- handleSlotPlace(senderId, slotToken, placement) {
1638
- const entry = this.slotTokens.get(slotToken);
1639
- if (!entry) return;
1640
- if (entry.authorizedWcId !== senderId) return;
1641
- if (!isValidPlacement(placement)) return;
1642
- entry.apply(placement);
1665
+ handleSnapshot(senderId, rawSnapshot) {
1666
+ const clean = cleanSnapshot(rawSnapshot, (slotToken) => {
1667
+ const entry = this.slotTokens.get(slotToken);
1668
+ if (!entry || entry.authorizedWcId !== senderId) return null;
1669
+ return {
1670
+ viewId: entry.viewId,
1671
+ layer: entry.zone
1672
+ };
1673
+ });
1674
+ if (!clean) return;
1675
+ const { state, ops } = reconcile(this.reconcileStates.get(senderId) ?? createInitialState(), clean);
1676
+ this.reconcileStates.set(senderId, state);
1677
+ const applyByViewId = /* @__PURE__ */ new Map();
1678
+ for (const entry of this.slotTokens.values()) if (entry.authorizedWcId === senderId) applyByViewId.set(entry.viewId, entry.apply);
1679
+ dispatchOps(ops, state, (viewId) => applyByViewId.get(viewId) ?? null);
1643
1680
  }
1644
1681
  /**
1645
1682
  * Per-wc layout-subscribe replay (`__electron-deck:layout-subscribe`). When the
1646
- * authorized control wc (re)subscribes (e.g. after reload), re-push every grant
1647
- * it owns so it can re-bind its DOM slots. A DIFFERENT wc's subscribe never
1648
- * receives another wc's grants.
1683
+ * authorized control wc (re)subscribes (e.g. after reload), BUMP its generation
1684
+ * and RESET its reconciler (a reload restarts the renderer's epoch at 0; without
1685
+ * the reset the fresh low-epoch snapshots would be rejected as stale), then
1686
+ * re-push every grant it owns — now carrying the bumped generation — so it can
1687
+ * re-bind its DOM slots. A DIFFERENT wc's subscribe never touches another wc's
1688
+ * state or grants.
1649
1689
  */
1650
1690
  handleLayoutSubscribe(senderId) {
1691
+ this.perWcGeneration.set(senderId, (this.perWcGeneration.get(senderId) ?? 0) + 1);
1692
+ this.reconcileStates.delete(senderId);
1651
1693
  for (const entry of this.slotTokens.values()) if (entry.authorizedWcId === senderId) entry.resend();
1652
1694
  }
1653
- /** Drop every slot token authorized to `wcId` (window-close / shutdown hygiene). */
1695
+ /** Drop every slot token + reconciler/generation state authorized to `wcId`
1696
+ * (window-close / shutdown hygiene). */
1654
1697
  revokeSlotTokensForWc(wcId) {
1655
1698
  for (const [token, entry] of this.slotTokens) if (entry.authorizedWcId === wcId) this.slotTokens.delete(token);
1699
+ this.reconcileStates.delete(wcId);
1700
+ this.perWcGeneration.delete(wcId);
1656
1701
  }
1657
1702
  handleSubWindowClosed(win, wc) {
1658
1703
  this.trackedWindows.delete(win);
@@ -1934,24 +1979,26 @@ var DeckApp = class {
1934
1979
  let placedSubstrate = null;
1935
1980
  let slotToken;
1936
1981
  let placed = false;
1937
- const mintSlotToken = (controlWc, anchor) => {
1982
+ const mintSlotToken = (controlWc, anchor, zone) => {
1938
1983
  this.ensureSlotChannelsArmed();
1939
1984
  const authorizedWcId = controlWc.id;
1940
1985
  const token = randomUUID();
1941
1986
  slotToken = token;
1942
1987
  ++this.slotSeq;
1943
- const grant = {
1944
- viewId,
1945
- slotId: anchor,
1946
- slotToken: token
1947
- };
1948
1988
  const resend = () => {
1989
+ const grant = {
1990
+ viewId,
1991
+ slotId: anchor,
1992
+ slotToken: token,
1993
+ generation: this.perWcGeneration.get(authorizedWcId) ?? 0
1994
+ };
1949
1995
  controlWc.send(DeckChannel.SlotGrant, grant);
1950
1996
  };
1951
1997
  this.slotTokens.set(token, {
1952
1998
  viewId,
1953
1999
  slotId: anchor,
1954
2000
  authorizedWcId,
2001
+ zone,
1955
2002
  resend,
1956
2003
  apply: (p) => {
1957
2004
  hostHandle.applyPlacement(p);
@@ -1975,7 +2022,7 @@ var DeckApp = class {
1975
2022
  placed = true;
1976
2023
  disarmRootClose();
1977
2024
  const anchor = placeOpts.anchor;
1978
- if (typeof anchor === "string" && anchor.length > 0) mintSlotToken(controlWc, anchor);
2025
+ if (typeof anchor === "string" && anchor.length > 0) mintSlotToken(controlWc, anchor, placeOpts.zone ?? 0);
1979
2026
  return hostHandle;
1980
2027
  },
1981
2028
  moveTo: async (win, moveOpts) => {
@@ -2024,7 +2071,7 @@ var DeckApp = class {
2024
2071
  slotToken = void 0;
2025
2072
  }
2026
2073
  const anchor = moveOpts.anchor;
2027
- if (typeof anchor === "string" && anchor.length > 0) mintSlotToken(destControlWc, anchor);
2074
+ if (typeof anchor === "string" && anchor.length > 0) mintSlotToken(destControlWc, anchor, moveOpts.zone ?? 0);
2028
2075
  },
2029
2076
  applyPlacement: (p) => {
2030
2077
  inner.applyPlacement(p);
@@ -2111,26 +2158,6 @@ var DeckApp = class {
2111
2158
  };
2112
2159
  }
2113
2160
  };
2114
- /**
2115
- * Validate an inbound `__electron-deck:place` placement before applying it.
2116
- * `{visible:false}` is always OK; `{visible:true, bounds:{x,y,width,height}}`
2117
- * requires width≥0 && height≥0 — x/y may be ANY finite number (negative origin
2118
- * is legitimate for scroll-follow). Anything else is rejected.
2119
- */
2120
- function isValidPlacement(placement) {
2121
- if (placement === null || typeof placement !== "object") return false;
2122
- const p = placement;
2123
- if (p.visible === false) return true;
2124
- if (p.visible !== true) return false;
2125
- const b = p.bounds;
2126
- if (b === null || typeof b !== "object") return false;
2127
- const { x, y, width, height } = b;
2128
- if (typeof x !== "number" || !Number.isFinite(x)) return false;
2129
- if (typeof y !== "number" || !Number.isFinite(y)) return false;
2130
- if (typeof width !== "number" || !Number.isFinite(width) || width < 0) return false;
2131
- if (typeof height !== "number" || !Number.isFinite(height) || height < 0) return false;
2132
- return true;
2133
- }
2134
2161
  async function runWithTimeout(work, timeoutMs) {
2135
2162
  let timer;
2136
2163
  const timeout = new Promise((_, rej) => {
@@ -2286,4 +2313,4 @@ function validateHandlerMap(fieldName, value) {
2286
2313
  //#endregion
2287
2314
  export { WireTransport as a, InMemoryTypedIpcRegistry as c, createControlBus as i, EventBus as l, startElectronDeck as n, createCapabilityRegistry as o, validateConfig as r, createTrustSet as s, electronDeck as t, defineEvent as u };
2288
2315
 
2289
- //# sourceMappingURL=electron-deck-C9_Yu3fd.js.map
2316
+ //# sourceMappingURL=electron-deck-B9fun93z.js.map