@dimina-kit/electron-deck 0.1.0-dev.20260610082053

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/dist/client/index.d.ts +20 -0
  4. package/dist/client/index.d.ts.map +1 -0
  5. package/dist/client/index.js +310 -0
  6. package/dist/client/index.js.map +1 -0
  7. package/dist/client/layout-client.d.ts +51 -0
  8. package/dist/client/layout-client.d.ts.map +1 -0
  9. package/dist/electron-deck-DfFKeFEC.js +2295 -0
  10. package/dist/electron-deck-DfFKeFEC.js.map +1 -0
  11. package/dist/electron-deck.d.ts +57 -0
  12. package/dist/electron-deck.d.ts.map +1 -0
  13. package/dist/errors-NE2ig7L5.js +53 -0
  14. package/dist/errors-NE2ig7L5.js.map +1 -0
  15. package/dist/errors.d.ts +33 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/events.d.ts +14 -0
  18. package/dist/events.d.ts.map +1 -0
  19. package/dist/host/capability.d.ts +50 -0
  20. package/dist/host/capability.d.ts.map +1 -0
  21. package/dist/host/control-bus.d.ts +92 -0
  22. package/dist/host/control-bus.d.ts.map +1 -0
  23. package/dist/host/index.d.ts +42 -0
  24. package/dist/host/index.d.ts.map +1 -0
  25. package/dist/host/index.js +2 -0
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +3 -0
  29. package/dist/internal/deck-app.d.ts +415 -0
  30. package/dist/internal/deck-app.d.ts.map +1 -0
  31. package/dist/internal/electron-types.d.ts +131 -0
  32. package/dist/internal/electron-types.d.ts.map +1 -0
  33. package/dist/internal/event-bus.d.ts +29 -0
  34. package/dist/internal/event-bus.d.ts.map +1 -0
  35. package/dist/internal/ipc-registry-memory.d.ts +25 -0
  36. package/dist/internal/ipc-registry-memory.d.ts.map +1 -0
  37. package/dist/internal/lifecycle-manager.d.ts +28 -0
  38. package/dist/internal/lifecycle-manager.d.ts.map +1 -0
  39. package/dist/internal/resource-registry.d.ts +9 -0
  40. package/dist/internal/resource-registry.d.ts.map +1 -0
  41. package/dist/internal/trust-set.d.ts +53 -0
  42. package/dist/internal/trust-set.d.ts.map +1 -0
  43. package/dist/internal/wire-transport.d.ts +159 -0
  44. package/dist/internal/wire-transport.d.ts.map +1 -0
  45. package/dist/main/compositor.d.ts +123 -0
  46. package/dist/main/compositor.d.ts.map +1 -0
  47. package/dist/main/connection.d.ts +37 -0
  48. package/dist/main/connection.d.ts.map +1 -0
  49. package/dist/main/debug-tap.d.ts +55 -0
  50. package/dist/main/debug-tap.d.ts.map +1 -0
  51. package/dist/main/disposable.d.ts +18 -0
  52. package/dist/main/disposable.d.ts.map +1 -0
  53. package/dist/main/index.d.ts +16 -0
  54. package/dist/main/index.d.ts.map +1 -0
  55. package/dist/main/index.js +152 -0
  56. package/dist/main/index.js.map +1 -0
  57. package/dist/main/logger.d.ts +15 -0
  58. package/dist/main/logger.d.ts.map +1 -0
  59. package/dist/main/scope.d.ts +52 -0
  60. package/dist/main/scope.d.ts.map +1 -0
  61. package/dist/main/view-handle.d.ts +131 -0
  62. package/dist/main/view-handle.d.ts.map +1 -0
  63. package/dist/preload/index.cjs +105 -0
  64. package/dist/preload/index.cjs.map +1 -0
  65. package/dist/preload/index.d.ts +43 -0
  66. package/dist/preload/index.d.ts.map +1 -0
  67. package/dist/preload/index.js +80 -0
  68. package/dist/preload/index.js.map +1 -0
  69. package/dist/protocol-BJqo-vP2.js +23 -0
  70. package/dist/protocol-BJqo-vP2.js.map +1 -0
  71. package/dist/shared/protocol.d.ts +76 -0
  72. package/dist/shared/protocol.d.ts.map +1 -0
  73. package/dist/types.d.ts +504 -0
  74. package/dist/types.d.ts.map +1 -0
  75. package/dist/view-handle-CR-yWNfr.js +701 -0
  76. package/dist/view-handle-CR-yWNfr.js.map +1 -0
  77. package/package.json +86 -0
@@ -0,0 +1,152 @@
1
+ import { a as createLogger, c as toDisposable, i as createScope, n as CommitError, o as setLogLevel, r as createCompositor, s as DisposableRegistry, t as createViewHandle } from "../view-handle-CR-yWNfr.js";
2
+ //#region src/main/connection.ts
3
+ var log = createLogger("connection");
4
+ var NOOP_DISPOSABLE = { dispose() {} };
5
+ function toDispose(d) {
6
+ return typeof d === "function" ? d : () => d.dispose();
7
+ }
8
+ function createConnectionRegistry() {
9
+ const byId = /* @__PURE__ */ new Map();
10
+ /**
11
+ * Dispose a resource handed to a dead connection's `own()` — immediately,
12
+ * exactly once, never delegating to a disposed segment (that would throw).
13
+ * Both sync throws and async rejections are caught/logged so a late teardown
14
+ * can never escape as an unhandledRejection in the Electron main process.
15
+ */
16
+ function disposeLate(d) {
17
+ try {
18
+ const r = toDispose(d)();
19
+ if (r && typeof r.then === "function") r.catch((e) => log.error("late own() async disposer rejected", e));
20
+ } catch (e) {
21
+ log.error("late own() resource disposer threw", e);
22
+ }
23
+ }
24
+ /**
25
+ * A connection for an already-destroyed webContents. Not registered, never
26
+ * alive, never arms a `'destroyed'` hook (it would never fire on a dead wc —
27
+ * that path would otherwise mint a permanently-alive leaking zombie). `own()`
28
+ * disposes immediately so callers racing teardown still don't leak.
29
+ */
30
+ function makeDeadConnection(wc) {
31
+ return {
32
+ id: wc.id,
33
+ webContents: wc,
34
+ get alive() {
35
+ return false;
36
+ },
37
+ own(d) {
38
+ disposeLate(d);
39
+ return NOOP_DISPOSABLE;
40
+ },
41
+ on() {
42
+ return NOOP_DISPOSABLE;
43
+ }
44
+ };
45
+ }
46
+ function build(wc) {
47
+ const id = wc.id;
48
+ let segment = new DisposableRegistry();
49
+ let alive = true;
50
+ const resetListeners = /* @__PURE__ */ new Set();
51
+ const closedListeners = /* @__PURE__ */ new Set();
52
+ function emit(ev) {
53
+ const set = ev === "reset" ? resetListeners : closedListeners;
54
+ for (const cb of [...set]) try {
55
+ cb();
56
+ } catch (e) {
57
+ log.error(`listener for "${ev}" threw`, e);
58
+ }
59
+ }
60
+ const connection = {
61
+ id,
62
+ webContents: wc,
63
+ get alive() {
64
+ return alive;
65
+ },
66
+ own(d) {
67
+ if (!alive) {
68
+ disposeLate(d);
69
+ return NOOP_DISPOSABLE;
70
+ }
71
+ return segment.add(d);
72
+ },
73
+ on(ev, cb) {
74
+ const set = ev === "reset" ? resetListeners : closedListeners;
75
+ set.add(cb);
76
+ let removed = false;
77
+ return { dispose() {
78
+ if (removed) return;
79
+ removed = true;
80
+ set.delete(cb);
81
+ } };
82
+ }
83
+ };
84
+ function reset() {
85
+ if (!alive) return;
86
+ const old = segment;
87
+ segment = new DisposableRegistry();
88
+ old.disposeAll().catch((e) => log.error("reset disposeAll threw", e));
89
+ emit("reset");
90
+ }
91
+ function close() {
92
+ if (!alive) return;
93
+ alive = false;
94
+ byId.delete(id);
95
+ segment.disposeAll().catch((e) => log.error("close disposeAll threw", e));
96
+ emit("closed");
97
+ }
98
+ wc.once("destroyed", () => {
99
+ close();
100
+ });
101
+ return {
102
+ connection,
103
+ reset,
104
+ close
105
+ };
106
+ }
107
+ function acquire(wc) {
108
+ const existing = byId.get(wc.id);
109
+ if (existing) return existing.connection;
110
+ if (wc.isDestroyed?.()) return makeDeadConnection(wc);
111
+ const handle = build(wc);
112
+ byId.set(wc.id, handle);
113
+ return handle.connection;
114
+ }
115
+ function reset(id) {
116
+ byId.get(id)?.reset();
117
+ }
118
+ return {
119
+ acquire,
120
+ get: (id) => byId.get(id)?.connection,
121
+ all: () => [...byId.values()].map((h) => h.connection),
122
+ reset
123
+ };
124
+ }
125
+ //#endregion
126
+ //#region src/main/debug-tap.ts
127
+ var DEFAULT_CAPACITY = 1e3;
128
+ function createDebugTap(options = {}) {
129
+ const enabled = options.enabled ?? false;
130
+ const capacity = options.capacity && options.capacity > 0 ? Math.floor(options.capacity) : DEFAULT_CAPACITY;
131
+ let buffer = [];
132
+ return {
133
+ get enabled() {
134
+ return enabled;
135
+ },
136
+ record(entry) {
137
+ if (!enabled) return;
138
+ buffer.push(entry);
139
+ if (buffer.length > capacity) buffer.splice(0, buffer.length - capacity);
140
+ },
141
+ entries() {
142
+ return buffer.slice();
143
+ },
144
+ clear() {
145
+ buffer = [];
146
+ }
147
+ };
148
+ }
149
+ //#endregion
150
+ export { CommitError, DisposableRegistry, createCompositor, createConnectionRegistry, createDebugTap, createLogger, createScope, createViewHandle, setLogLevel, toDisposable };
151
+
152
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/main/connection.ts","../../src/main/debug-tap.ts"],"sourcesContent":["/**\n * Connection-layer primitive (`Connection` / `ConnectionRegistry`) described in\n * packages/electron-deck/docs/foundation.md §4.\n *\n * A `Connection` is one trusted webContents (keyed by the unforgeable `wc.id`).\n * It holds a single `DisposableRegistry` as its \"lifetime segment\" container.\n * The segment tears down deterministically (LIFO) on hard-destroy\n * (`wc.once('destroyed')`) or soft-reuse (`reset(id)`):\n * - hard destroy → dispose segment, fire 'closed' once, de-register.\n * - soft reset → dispose segment, swap in a fresh registry, fire 'reset',\n * connection stays alive & registered.\n *\n * §4.3: the terminal hook is `'destroyed'` (NOT `'render-process-gone'`), and\n * `DisposableRegistry.disposeAll` is async (LIFO) — so close/reset are async.\n */\nimport type { WebContents } from 'electron'\n\nimport { DisposableRegistry, type Disposable } from './disposable.js'\nimport { createLogger } from './logger.js'\n\nconst log = createLogger('connection')\n\nexport interface Connection {\n readonly id: number\n readonly webContents: WebContents\n readonly alive: boolean\n /** Register a resource cleaned up with the current lifetime segment; the\n * returned Disposable releases it early. */\n own(d: Disposable | (() => void)): Disposable\n /** Connection-level lifecycle events; returns an unsubscribe Disposable. */\n on(ev: 'reset' | 'closed', cb: () => void): Disposable\n}\n\nexport interface ConnectionRegistry {\n /** Build/fetch the connection for a (trusted) webContents; idempotent. */\n acquire(wc: WebContents): Connection\n get(id: number): Connection | undefined\n all(): readonly Connection[]\n /** Soft reuse: dispose the current segment, then swap in a fresh registry. */\n reset(id: number): void\n}\n\ntype LifecycleEvent = 'reset' | 'closed'\n\nconst NOOP_DISPOSABLE: Disposable = { dispose() {} }\n\nfunction toDispose(d: Disposable | (() => void)): () => void | Promise<void> {\n return typeof d === 'function' ? d : () => d.dispose()\n}\n\n/** Internal handle so the registry can drive a connection's lifecycle. */\ninterface ConnectionHandle {\n connection: Connection\n /** Dispose the current segment and swap in a fresh one; fire 'reset'. */\n reset(): void\n /** Terminal close: dispose segment, fire 'closed' once, de-register. */\n close(): void\n}\n\nexport function createConnectionRegistry(): ConnectionRegistry {\n const byId = new Map<number, ConnectionHandle>()\n\n /**\n * Dispose a resource handed to a dead connection's `own()` — immediately,\n * exactly once, never delegating to a disposed segment (that would throw).\n * Both sync throws and async rejections are caught/logged so a late teardown\n * can never escape as an unhandledRejection in the Electron main process.\n */\n function disposeLate(d: Disposable | (() => void)): void {\n try {\n const r = toDispose(d)()\n if (r && typeof (r as Promise<void>).then === 'function') {\n ;(r as Promise<void>).catch((e) => log.error('late own() async disposer rejected', e))\n }\n } catch (e) {\n log.error('late own() resource disposer threw', e)\n }\n }\n\n /**\n * A connection for an already-destroyed webContents. Not registered, never\n * alive, never arms a `'destroyed'` hook (it would never fire on a dead wc —\n * that path would otherwise mint a permanently-alive leaking zombie). `own()`\n * disposes immediately so callers racing teardown still don't leak.\n */\n function makeDeadConnection(wc: WebContents): Connection {\n return {\n id: wc.id,\n webContents: wc,\n get alive() {\n return false\n },\n own(d) {\n disposeLate(d)\n return NOOP_DISPOSABLE\n },\n on() {\n return NOOP_DISPOSABLE\n },\n }\n }\n\n function build(wc: WebContents): ConnectionHandle {\n const id = wc.id\n // Current lifetime segment. Replaced wholesale on reset (never re-used\n // after disposeAll, which permanently poisons the instance).\n let segment = new DisposableRegistry()\n let alive = true\n\n const resetListeners = new Set<() => void>()\n const closedListeners = new Set<() => void>()\n\n function emit(ev: LifecycleEvent): void {\n const set = ev === 'reset' ? resetListeners : closedListeners\n // Isolate faults so one throwing listener can't block the rest (§9).\n for (const cb of [...set]) {\n try {\n cb()\n } catch (e) {\n log.error(`listener for \"${ev}\" threw`, e)\n }\n }\n }\n\n const connection: Connection = {\n id,\n webContents: wc,\n get alive() {\n return alive\n },\n own(d) {\n // Race safety (§8): after close, do not delegate to the disposed\n // segment (that throws). Dispose the late resource immediately and\n // hand back a harmless no-op handle.\n if (!alive) {\n disposeLate(d)\n return NOOP_DISPOSABLE\n }\n return segment.add(d)\n },\n on(ev, cb) {\n const set = ev === 'reset' ? resetListeners : closedListeners\n set.add(cb)\n let removed = false\n return {\n dispose() {\n if (removed) return\n removed = true\n set.delete(cb)\n },\n }\n },\n }\n\n function reset(): void {\n if (!alive) return\n // Dispose the old segment, then swap in a fresh registry BEFORE firing\n // 'reset' so any listener (or subsequent own()) sees an open segment.\n // disposeAll is async; old `own` handles become no-ops because they\n // reference the now-replaced/released segment.\n const old = segment\n segment = new DisposableRegistry()\n void old.disposeAll().catch((e) => log.error('reset disposeAll threw', e))\n emit('reset')\n }\n\n function close(): void {\n if (!alive) return\n alive = false\n byId.delete(id)\n void segment.disposeAll().catch((e) => log.error('close disposeAll threw', e))\n emit('closed')\n }\n\n // Hard destroy — the real terminal hook (§4.3). `once` auto-removes and the\n // `alive` guard inside close() makes repeated triggers idempotent.\n wc.once('destroyed', () => {\n close()\n })\n\n return { connection, reset, close }\n }\n\n function acquire(wc: WebContents): Connection {\n const existing = byId.get(wc.id)\n if (existing) return existing.connection\n // Re-acquiring an already-destroyed wc must not mint a live connection: its\n // `'destroyed'` event already fired and won't fire again, so the connection\n // would stay alive & registered forever (zombie + leak). Hand back a dead,\n // unregistered connection instead.\n if (wc.isDestroyed?.()) return makeDeadConnection(wc)\n const handle = build(wc)\n byId.set(wc.id, handle)\n return handle.connection\n }\n\n function reset(id: number): void {\n byId.get(id)?.reset()\n }\n\n return {\n acquire,\n get: (id) => byId.get(id)?.connection,\n all: () => [...byId.values()].map((h) => h.connection),\n reset,\n }\n}\n","/**\n * debugTap (foundation.md §7) — a flag-gated ring buffer for observing an\n * IPC / cross-wc bridge message stream.\n *\n * Hung on a dispatch chokepoint (e.g. bridge-router's SERVICE_INVOKE /\n * RENDER_INVOKE / PUBLISH / API_RESPONSE handlers), it records per-message\n * metadata — connection id / channel / direction / appSession / duration /\n * error — into a bounded ring so a hidden panel (or a test) can inspect the\n * last N messages when debugging the cross-wc state machine.\n *\n * Design constraints:\n * - Default OFF: `record()` is a near-free no-op when disabled, so the hot\n * path costs nothing in production unless the flag is set.\n * - Caller-supplied `ts`: the electron-deck package never calls `Date.now()`\n * (determinism / resumability), so each entry carries a timestamp the\n * caller stamps.\n * - Bounded: a finite ring (default 1000) evicts oldest-first; it never grows\n * without bound.\n */\n\nexport interface DebugTapEntry {\n /** Caller-stamped timestamp (ms). The primitive never reads the clock. */\n ts: number\n /** The channel / message kind (e.g. 'SERVICE_INVOKE', 'API_RESPONSE'). */\n channel: string\n /** Ingress (main received) vs egress (main sent). */\n direction: 'in' | 'out'\n /** webContents.id of the connection this message is attributed to. */\n connectionId?: number\n /** bridge-router app session this message routes within, if known. */\n appSessionId?: string\n /** For request/response pairs: how long the handler took (ms). */\n durationMs?: number\n /** Error message if the message produced a failure. */\n error?: string\n /** Short human-readable summary of the payload (no large blobs). */\n summary?: string\n}\n\nexport interface DebugTap {\n /** Whether recording is on. When false, `record` is a no-op. */\n readonly enabled: boolean\n /** Append one entry to the ring (no-op when disabled). */\n record(entry: DebugTapEntry): void\n /** Snapshot of buffered entries, oldest → newest. Safe to retain/mutate. */\n entries(): readonly DebugTapEntry[]\n /** Empty the ring. */\n clear(): void\n}\n\nexport interface DebugTapOptions {\n /** Default false — the tap is OFF unless explicitly enabled. */\n enabled?: boolean\n /** Max retained entries; oldest evicted past this. Default 1000. */\n capacity?: number\n}\n\nconst DEFAULT_CAPACITY = 1000\n\nexport function createDebugTap(options: DebugTapOptions = {}): DebugTap {\n const enabled = options.enabled ?? false\n const capacity =\n options.capacity && options.capacity > 0 ? Math.floor(options.capacity) : DEFAULT_CAPACITY\n\n // Simple bounded buffer: push to the tail, shift the head when over capacity.\n // For the modest capacities debugTap targets this is plenty; the hot path is\n // the disabled no-op, not eviction throughput.\n let buffer: DebugTapEntry[] = []\n\n return {\n get enabled() {\n return enabled\n },\n record(entry: DebugTapEntry): void {\n if (!enabled) return\n buffer.push(entry)\n if (buffer.length > capacity) {\n buffer.splice(0, buffer.length - capacity)\n }\n },\n entries(): readonly DebugTapEntry[] {\n // Return a copy so a retained snapshot is stable and callers can't mutate\n // the live ring.\n return buffer.slice()\n },\n clear(): void {\n buffer = []\n },\n }\n}\n"],"mappings":";;AAoBA,IAAM,MAAM,aAAa,YAAY;AAwBrC,IAAM,kBAA8B,EAAE,UAAU,CAAC,EAAE;AAEnD,SAAS,UAAU,GAA0D;CAC3E,OAAO,OAAO,MAAM,aAAa,UAAU,EAAE,QAAQ;AACvD;AAWA,SAAgB,2BAA+C;CAC7D,MAAM,uBAAO,IAAI,IAA8B;;;;;;;CAQ/C,SAAS,YAAY,GAAoC;EACvD,IAAI;GACF,MAAM,IAAI,UAAU,CAAC,EAAE;GACvB,IAAI,KAAK,OAAQ,EAAoB,SAAS,YAC3C,EAAqB,OAAO,MAAM,IAAI,MAAM,sCAAsC,CAAC,CAAC;EAEzF,SAAS,GAAG;GACV,IAAI,MAAM,sCAAsC,CAAC;EACnD;CACF;;;;;;;CAQA,SAAS,mBAAmB,IAA6B;EACvD,OAAO;GACL,IAAI,GAAG;GACP,aAAa;GACb,IAAI,QAAQ;IACV,OAAO;GACT;GACA,IAAI,GAAG;IACL,YAAY,CAAC;IACb,OAAO;GACT;GACA,KAAK;IACH,OAAO;GACT;EACF;CACF;CAEA,SAAS,MAAM,IAAmC;EAChD,MAAM,KAAK,GAAG;EAGd,IAAI,UAAU,IAAI,mBAAmB;EACrC,IAAI,QAAQ;EAEZ,MAAM,iCAAiB,IAAI,IAAgB;EAC3C,MAAM,kCAAkB,IAAI,IAAgB;EAE5C,SAAS,KAAK,IAA0B;GACtC,MAAM,MAAM,OAAO,UAAU,iBAAiB;GAE9C,KAAK,MAAM,MAAM,CAAC,GAAG,GAAG,GACtB,IAAI;IACF,GAAG;GACL,SAAS,GAAG;IACV,IAAI,MAAM,iBAAiB,GAAG,UAAU,CAAC;GAC3C;EAEJ;EAEA,MAAM,aAAyB;GAC7B;GACA,aAAa;GACb,IAAI,QAAQ;IACV,OAAO;GACT;GACA,IAAI,GAAG;IAIL,IAAI,CAAC,OAAO;KACV,YAAY,CAAC;KACb,OAAO;IACT;IACA,OAAO,QAAQ,IAAI,CAAC;GACtB;GACA,GAAG,IAAI,IAAI;IACT,MAAM,MAAM,OAAO,UAAU,iBAAiB;IAC9C,IAAI,IAAI,EAAE;IACV,IAAI,UAAU;IACd,OAAO,EACL,UAAU;KACR,IAAI,SAAS;KACb,UAAU;KACV,IAAI,OAAO,EAAE;IACf,EACF;GACF;EACF;EAEA,SAAS,QAAc;GACrB,IAAI,CAAC,OAAO;GAKZ,MAAM,MAAM;GACZ,UAAU,IAAI,mBAAmB;GACjC,IAAS,WAAW,EAAE,OAAO,MAAM,IAAI,MAAM,0BAA0B,CAAC,CAAC;GACzE,KAAK,OAAO;EACd;EAEA,SAAS,QAAc;GACrB,IAAI,CAAC,OAAO;GACZ,QAAQ;GACR,KAAK,OAAO,EAAE;GACd,QAAa,WAAW,EAAE,OAAO,MAAM,IAAI,MAAM,0BAA0B,CAAC,CAAC;GAC7E,KAAK,QAAQ;EACf;EAIA,GAAG,KAAK,mBAAmB;GACzB,MAAM;EACR,CAAC;EAED,OAAO;GAAE;GAAY;GAAO;EAAM;CACpC;CAEA,SAAS,QAAQ,IAA6B;EAC5C,MAAM,WAAW,KAAK,IAAI,GAAG,EAAE;EAC/B,IAAI,UAAU,OAAO,SAAS;EAK9B,IAAI,GAAG,cAAc,GAAG,OAAO,mBAAmB,EAAE;EACpD,MAAM,SAAS,MAAM,EAAE;EACvB,KAAK,IAAI,GAAG,IAAI,MAAM;EACtB,OAAO,OAAO;CAChB;CAEA,SAAS,MAAM,IAAkB;EAC/B,KAAK,IAAI,EAAE,GAAG,MAAM;CACtB;CAEA,OAAO;EACL;EACA,MAAM,OAAO,KAAK,IAAI,EAAE,GAAG;EAC3B,WAAW,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,KAAK,MAAM,EAAE,UAAU;EACrD;CACF;AACF;;;ACrJA,IAAM,mBAAmB;AAEzB,SAAgB,eAAe,UAA2B,CAAC,GAAa;CACtE,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,WACJ,QAAQ,YAAY,QAAQ,WAAW,IAAI,KAAK,MAAM,QAAQ,QAAQ,IAAI;CAK5E,IAAI,SAA0B,CAAC;CAE/B,OAAO;EACL,IAAI,UAAU;GACZ,OAAO;EACT;EACA,OAAO,OAA4B;GACjC,IAAI,CAAC,SAAS;GACd,OAAO,KAAK,KAAK;GACjB,IAAI,OAAO,SAAS,UAClB,OAAO,OAAO,GAAG,OAAO,SAAS,QAAQ;EAE7C;EACA,UAAoC;GAGlC,OAAO,OAAO,MAAM;EACtB;EACA,QAAc;GACZ,SAAS,CAAC;EACZ;CACF;AACF"}
@@ -0,0 +1,15 @@
1
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ export interface Logger {
3
+ debug(message: string, ...args: unknown[]): void;
4
+ info(message: string, ...args: unknown[]): void;
5
+ warn(message: string, ...args: unknown[]): void;
6
+ error(message: string, ...args: unknown[]): void;
7
+ }
8
+ /**
9
+ * Create a tagged logger for a specific module.
10
+ * All output goes to console with structured prefix.
11
+ */
12
+ export declare function createLogger(tag: string): Logger;
13
+ export declare function setLogLevel(level: LogLevel): void;
14
+ export {};
15
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/main/logger.ts"],"names":[],"mappings":"AAAA,KAAK,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AA2BnD,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;IAC/C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;IAC/C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CACjD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAehD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAEjD"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `Scope` — engine-agnostic nested-lifetime primitive, generalizing the
3
+ * Connection/Disposable semantics in this package (foundation.md §4).
4
+ *
5
+ * A `Scope` owns a single "lifetime segment". Resources bound via `own()` and
6
+ * sub-scopes created via `child()` are released together — both on `reset()`
7
+ * (soft reuse: dispose the segment, open a fresh one, stay alive) and `close()`
8
+ * (terminal: dispose everything, die). Teardown is LIFO and, crucially,
9
+ * `reset()`/`close()` are COMPLETION FENCES: their Promise resolves (and the
10
+ * `'reset'`/`'closed'` listeners fire) only AFTER the underlying async
11
+ * disposeAll has fully finished — unlike `connection.ts`, which fires the
12
+ * teardown and forgets (`void ...disposeAll()`).
13
+ *
14
+ * Cross-layer LIFO: a scope's teardown disposes its child sub-scopes (deepest
15
+ * first, recursively) BEFORE its own directly-owned resources, so a grandchild
16
+ * tears down before a child, which tears down before the root.
17
+ */
18
+ import { type Disposable } from './disposable.js';
19
+ export interface Scope {
20
+ readonly alive: boolean;
21
+ /** Bind a resource to the current lifetime segment; released by both reset()
22
+ * and close() (LIFO). The returned Disposable releases it early (once). After
23
+ * the scope is closed, the resource is disposed immediately (leak protection)
24
+ * and a no-op handle is returned. */
25
+ own(d: Disposable | (() => void)): Disposable;
26
+ /** Create a sub-scope bound to the current segment: a parent reset()/close()
27
+ * cascades into it. A child close() does not affect the parent. */
28
+ child(): Scope;
29
+ /** Soft reuse: await LIFO disposeAll of the current segment (children first,
30
+ * then owned resources — async disposers truly complete), THEN open a fresh
31
+ * segment and fire 'reset'. Scope stays alive. */
32
+ reset(): Promise<void>;
33
+ /** Terminal: await LIFO disposeAll of the segment, mark dead, fire 'closed'
34
+ * once. Idempotent. */
35
+ close(): Promise<void>;
36
+ /** Subscribe to a lifecycle event; returns an unsubscribe Disposable. */
37
+ on(event: 'reset' | 'closed', cb: () => void): Disposable;
38
+ /**
39
+ * Re-parent `child` from THIS scope's current segment onto `newParent`'s
40
+ * current segment WITHOUT resetting or closing it. The child's own()ed
41
+ * resources stay live and neither 'reset' nor 'closed' fire on it; only the
42
+ * cascade ownership moves (who tears it down from now on).
43
+ *
44
+ * If `this` or `newParent` has a teardown in flight, adopt WAITS for that
45
+ * fence (it does not throw), then re-reads/re-validates against the fresh
46
+ * segment. Validation failures (dead this/newParent, non-direct-child, cycle)
47
+ * reject. On every path the child stays attached to EXACTLY one segment.
48
+ */
49
+ adopt(child: Scope, newParent: Scope): Promise<void>;
50
+ }
51
+ export declare function createScope(): Scope;
52
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/main/scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAsB,KAAK,UAAU,EAAkB,MAAM,iBAAiB,CAAA;AAKrF,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB;;;yCAGqC;IACrC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,UAAU,CAAA;IAC7C;uEACmE;IACnE,KAAK,IAAI,KAAK,CAAA;IACd;;sDAEkD;IAClD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB;2BACuB;IACvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,yEAAyE;IACzE,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,UAAU,CAAA;IACzD;;;;;;;;;;OAUG;IACH,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACrD;AAgGD,wBAAgB,WAAW,IAAI,KAAK,CA0SnC"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `ViewHandle` — the per-view orchestrator (build-plan §2(a)/(b)/(d)). One handle
3
+ * owns ONE native view and threads it through a window's z-planner. It composes
4
+ * three INJECTED primitives and nothing else (no deck-app, no Electron):
5
+ * - a {@link NativeView} (its `ref` + a `setBounds` sink) — the native surface;
6
+ * - a {@link Scope} (`deps.scope`) — the view's home/native-view lifetime;
7
+ * - a {@link PlaceTarget} = `{ compositor, windowScope }` — a window's
8
+ * z-planner + lifetime, handed to {@link ViewHandle.placeIn}.
9
+ *
10
+ * Lifetime. `placeIn` adopts a `viewScope` that is a CHILD of the target
11
+ * window's `windowScope`, so closing the windowScope cascades into the handle
12
+ * (scope.ts cross-layer LIFO): the native view is detached and the placement
13
+ * sink goes inert.
14
+ *
15
+ * gap#1. The handle drives `setBounds` DIRECTLY on its native view; the
16
+ * Compositor stays a pure z-order planner (mount/unmount/commit only) and never
17
+ * sees geometry.
18
+ *
19
+ * A4 teardown order (build-plan §2(b)). The viewScope owns the native detach
20
+ * FIRST and the sink-disable LAST, so LIFO teardown runs the sink-disable
21
+ * (STEP0) BEFORE the detach (STEP1): a late `place` frame can never drive a
22
+ * native effect on a half-torn-down view.
23
+ *
24
+ * Cross-window move (build-plan §2(d) / A1.3.2). {@link ViewHandle.moveTo}
25
+ * migrates the view to another `{ compositor, windowScope }` as TWO independent
26
+ * Compositor commits, guarded by a per-view async mutex (THE migrationLock —
27
+ * each handle is one view). The current placement is a MUTABLE token so the
28
+ * detach `own()` and `applyPlacement` always follow the CURRENT window after a
29
+ * move. `rehome:true` re-parents the viewScope via {@link Scope.adopt} so
30
+ * lifetime follows display; without it, lifetime stays under the src window.
31
+ */
32
+ import type { Scope } from './scope.js';
33
+ import type { Compositor, NativeViewRef } from './compositor.js';
34
+ /** A screen-space rectangle, in CSS px. Mirrors `@dimina-kit/view-anchor`'s
35
+ * `Bounds` (electron-deck does not depend on view-anchor in this increment). */
36
+ export interface Bounds {
37
+ x: number;
38
+ y: number;
39
+ width: number;
40
+ height: number;
41
+ }
42
+ /**
43
+ * Explicit visibility + geometry for a native view. Structurally identical to
44
+ * the `@dimina-kit/view-anchor` `Placement` export; mirrored locally so this
45
+ * increment adds no new package dependency.
46
+ */
47
+ export type Placement = {
48
+ visible: true;
49
+ bounds: Bounds;
50
+ } | {
51
+ visible: false;
52
+ };
53
+ /** The native surface a handle drives: its z-order identity (`ref`) plus the
54
+ * `setBounds` sink the handle calls directly (gap#1). `destroy` (optional)
55
+ * destroys the backing native view (its WebContents) — owned by the viewScope so
56
+ * it runs on teardown AFTER the detach (keepAlive B3.1 lifetime/leak fix).
57
+ * Optional so fakes that don't model a native WebContents stay valid.
58
+ *
59
+ * `webContents` / `capturePage` (optional) expose the backing native view's
60
+ * WebContents and a screenshot pass-through, so a handle accessor can recover
61
+ * them without re-deriving the WebContentsView from the window's content view.
62
+ * Optional so geometry-only fakes stay valid. */
63
+ export interface NativeView {
64
+ readonly ref: NativeViewRef;
65
+ setBounds(b: Bounds): void;
66
+ destroy?(): void;
67
+ readonly webContents?: unknown;
68
+ capturePage?(): Promise<unknown>;
69
+ }
70
+ /** A window's z-planner + lifetime, handed to {@link ViewHandle.placeIn}. The
71
+ * handle's per-placement teardown scope is a CHILD of `windowScope`. */
72
+ export interface PlaceTarget {
73
+ compositor: Compositor;
74
+ windowScope: Scope;
75
+ }
76
+ export interface ViewHandle {
77
+ /** Mount the native view into the target window (mount + commit) and adopt a
78
+ * per-placement viewScope under the target's `windowScope`. Chainable. */
79
+ placeIn(target: PlaceTarget, opts: {
80
+ zone?: number;
81
+ }): ViewHandle;
82
+ /** The placement sink. Drops frames once disposed (idempotent late IPC).
83
+ * `visible:true` ensures mounted + drives `setBounds` directly; `visible:false`
84
+ * detaches (unmount + commit) but keeps the native view alive. */
85
+ applyPlacement(p: Placement): void;
86
+ /**
87
+ * Cross-window move (build-plan §2(d) / A1.3.2). Migrate the view from its
88
+ * current placement (`src`) to `dest` as TWO independent Compositor commits,
89
+ * serialized by a per-view async mutex (migrationLock):
90
+ *
91
+ * AT_SRC → DETACHED → AT_DEST (happy path)
92
+ * └→ (src.commit throws) → AT_SRC (rethrow, no side effect)
93
+ * DETACHED → (dest.commit throws) → ROLLBACK → AT_SRC (rethrow dest error)
94
+ * └→ CLOSED (src re-mount ALSO throws)
95
+ *
96
+ * On success the compositor token moves to `dest` (later `applyPlacement`
97
+ * drives the dest host). With `rehome:true`, the viewScope is re-parented under
98
+ * dest's `windowScope` (lifetime follows display). gap#2: moveTo moves DISPLAY
99
+ * (and, with rehome, LIFETIME) — it does NOT carry capability grants; the dest
100
+ * window's own control layer issues its own grant. Terminal (Promise<void>, not
101
+ * chainable).
102
+ */
103
+ moveTo(dest: PlaceTarget, opts: {
104
+ zone?: number;
105
+ rehome?: boolean;
106
+ }): Promise<void>;
107
+ /** Tear down this placement: run the viewScope's A4 owns (sink-disable then
108
+ * native detach, via the LIFO completion fence). Idempotent. */
109
+ dispose(): Promise<void>;
110
+ /** The backing native view's WebContents (pass-through from {@link NativeView}).
111
+ * Available immediately — the handle owns its view before any placeIn. */
112
+ readonly webContents: unknown;
113
+ /** The view's LIVE screen-space rect when it is currently placed AND visible;
114
+ * `null` before the first placement, after `applyPlacement({visible:false})`,
115
+ * and after `dispose()`. Tracks the last applied `visible:true` bounds. */
116
+ bounds(): Bounds | null;
117
+ /** Screenshot pass-through to the native view's `capturePage()`. */
118
+ capturePage(): Promise<unknown>;
119
+ }
120
+ export interface ViewHandleDeps {
121
+ nativeView: NativeView;
122
+ scope: Scope;
123
+ /** Optional bookkeeping hook, fired whenever the viewScope tears down (window-
124
+ * close cascade OR explicit dispose). The deck-app uses it to drop the view
125
+ * from its keepAlive group — a window-close cascades the viewScope directly
126
+ * (NOT via the host wrapper's dispose), so group cleanup must hang off the
127
+ * scope to fire on that path too (KA-2). Any order — it is pure bookkeeping. */
128
+ onDispose?(): void;
129
+ }
130
+ export declare function createViewHandle(deps: ViewHandleDeps): ViewHandle;
131
+ //# sourceMappingURL=view-handle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-handle.d.ts","sourceRoot":"","sources":["../../src/main/view-handle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEhE;iFACiF;AACjF,MAAM,WAAW,MAAM;IACrB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAA;CAAE,CAAA;AAE9E;;;;;;;;;kDASkD;AAClD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAA;IAC3B,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,OAAO,CAAC,IAAI,IAAI,CAAA;IAChB,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B,WAAW,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CACjC;AAED;yEACyE;AACzE,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,UAAU,CAAA;IACtB,WAAW,EAAE,KAAK,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;IACzB;+EAC2E;IAC3E,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,UAAU,CAAA;IACjE;;uEAEmE;IACnE,cAAc,CAAC,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IAClC;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnF;qEACiE;IACjE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACxB;+EAC2E;IAC3E,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B;;gFAE4E;IAC5E,MAAM,IAAI,MAAM,GAAG,IAAI,CAAA;IACvB,oEAAoE;IACpE,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,UAAU,CAAA;IACtB,KAAK,EAAE,KAAK,CAAA;IACZ;;;;qFAIiF;IACjF,SAAS,CAAC,IAAI,IAAI,CAAA;CACnB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,CAsUjE"}
@@ -0,0 +1,105 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let electron = require("electron");
3
+ //#region src/shared/protocol.ts
4
+ /** Bridge global 默认挂的全局名(contextBridge.exposeInMainWorld) */
5
+ var DEFAULT_BRIDGE_GLOBAL = "__electronDeckBridge";
6
+ /**
7
+ * Slot-token LAYOUT bridge 默认挂的全局名。`exposeDeckLayoutBridge()` 默认暴露到
8
+ * 此名,renderer 的 `createDeckLayoutClient({ bridge: window.__electronDeckLayoutBridge })`
9
+ * 读取同名。单一来源避免 preload helper 与 client 之间字符串漂移。
10
+ */
11
+ var DEFAULT_LAYOUT_BRIDGE_GLOBAL = "__electronDeckLayoutBridge";
12
+ /** Bridge protocol semver;client 在 ready() 时校验 major 一致 */
13
+ var BRIDGE_PROTOCOL_VERSION = "1.0.0";
14
+ var DeckChannel = {
15
+ Invoke: "__electron-deck:invoke",
16
+ Event: "__electron-deck:event",
17
+ Probe: "__electron-deck:probe",
18
+ Place: "__electron-deck:place",
19
+ SlotGrant: "__electron-deck:slot-grant",
20
+ LayoutSubscribe: "__electron-deck:layout-subscribe"
21
+ };
22
+ //#endregion
23
+ //#region src/preload/index.ts
24
+ /**
25
+ * 在 host preload 内调用,把 framework typed RPC + event push bridge 暴露到
26
+ * webview window:
27
+ *
28
+ * ```ts
29
+ * import { contextBridge, ipcRenderer } from 'electron'
30
+ * import { exposeDeckBridge } from '@dimina-kit/electron-deck/preload'
31
+ * exposeDeckBridge()
32
+ * ```
33
+ *
34
+ * 见 `DeckBridge` 接口(`shared/protocol.ts`)。
35
+ */
36
+ function exposeDeckBridge(options) {
37
+ if (typeof electron.contextBridge?.exposeInMainWorld !== "function" || typeof electron.ipcRenderer?.invoke !== "function") throw new Error("exposeDeckBridge: must be called from a preload script (electron contextBridge / ipcRenderer unavailable)");
38
+ const globalName = options?.globalName ?? "__electronDeckBridge";
39
+ if (globalThis[globalName] !== void 0) throw new Error(`Deck bridge already exposed at "${globalName}"`);
40
+ const bridge = {
41
+ version: BRIDGE_PROTOCOL_VERSION,
42
+ probe() {
43
+ return electron.ipcRenderer.invoke(DeckChannel.Probe);
44
+ },
45
+ invoke(req) {
46
+ return electron.ipcRenderer.invoke(DeckChannel.Invoke, req);
47
+ },
48
+ onEvent(listener) {
49
+ const wrapped = (_event, env) => {
50
+ listener(env);
51
+ };
52
+ electron.ipcRenderer.on(DeckChannel.Event, wrapped);
53
+ return () => {
54
+ electron.ipcRenderer.removeListener(DeckChannel.Event, wrapped);
55
+ };
56
+ }
57
+ };
58
+ electron.contextBridge.exposeInMainWorld(globalName, bridge);
59
+ }
60
+ /**
61
+ * 在 host preload 内调用,把三条 slot-token LAYOUT channel(`slot-grant` PUSH /
62
+ * `place` send / `layout-subscribe` invoke)封装成一个 `LayoutBridge`-shaped
63
+ * 对象暴露到 webview window,供 renderer:
64
+ *
65
+ * ```ts
66
+ * import { exposeDeckLayoutBridge } from '@dimina-kit/electron-deck/preload'
67
+ * exposeDeckLayoutBridge()
68
+ * // renderer:
69
+ * createDeckLayoutClient({ bridge: window.__electronDeckLayoutBridge })
70
+ * ```
71
+ *
72
+ * channel 名一律取自框架 `DeckChannel`(不手抄字符串)。`onSlotGrant` 返回一个
73
+ * 纯 unsubscribe 函数(可跨 contextBridge),不是 Disposable 对象。
74
+ */
75
+ function exposeDeckLayoutBridge(options) {
76
+ if (typeof electron.contextBridge?.exposeInMainWorld !== "function" || typeof electron.ipcRenderer?.on !== "function") throw new Error("exposeDeckLayoutBridge: must be called from a preload script (electron contextBridge / ipcRenderer unavailable)");
77
+ const globalName = options?.globalName ?? "__electronDeckLayoutBridge";
78
+ if (globalThis[globalName] !== void 0) throw new Error(`Deck layout bridge already exposed at "${globalName}"`);
79
+ electron.contextBridge.exposeInMainWorld(globalName, {
80
+ onSlotGrant(cb) {
81
+ const listener = (_event, grant) => {
82
+ cb(grant);
83
+ };
84
+ electron.ipcRenderer.on(DeckChannel.SlotGrant, listener);
85
+ return () => {
86
+ electron.ipcRenderer.removeListener(DeckChannel.SlotGrant, listener);
87
+ };
88
+ },
89
+ sendPlace(msg) {
90
+ electron.ipcRenderer.invoke(DeckChannel.Place, msg).catch(() => {});
91
+ },
92
+ subscribe() {
93
+ electron.ipcRenderer.invoke(DeckChannel.LayoutSubscribe).catch(() => {});
94
+ }
95
+ });
96
+ }
97
+ //#endregion
98
+ exports.BRIDGE_PROTOCOL_VERSION = BRIDGE_PROTOCOL_VERSION;
99
+ exports.DEFAULT_BRIDGE_GLOBAL = DEFAULT_BRIDGE_GLOBAL;
100
+ exports.DEFAULT_LAYOUT_BRIDGE_GLOBAL = DEFAULT_LAYOUT_BRIDGE_GLOBAL;
101
+ exports.DeckChannel = DeckChannel;
102
+ exports.exposeDeckBridge = exposeDeckBridge;
103
+ exports.exposeDeckLayoutBridge = exposeDeckLayoutBridge;
104
+
105
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/shared/protocol.ts","../../src/preload/index.ts"],"sourcesContent":["/**\n * Deck framework wire protocol —— main ↔ webview 之间的 channel 名 + 帧\n * 形态。SoT 在此,preload / client / main runtime 都从这里 import。\n *\n * 设计:把 declared `hostServices` / `simulatorApis` / `events` 三类全部走\n * **两个** 统一 channel,避免 channel name 爆炸 / 难以加 senderPolicy 白名单。\n * - `__electron-deck:invoke` — webview → main RPC (ipcRenderer.invoke)\n * - `__electron-deck:event` — main → webview event push (webContents.send)\n * - `__electron-deck:probe` — webview → main 探活,bridge ready 检查\n *\n * 帧用 JSON 对象一层 envelope,便于扩展 + 校验。\n *\n * @internal\n */\n\nimport type { JsonValue } from '../types.js'\n\n/** Bridge global 默认挂的全局名(contextBridge.exposeInMainWorld) */\nexport const DEFAULT_BRIDGE_GLOBAL = '__electronDeckBridge'\n\n/**\n * Slot-token LAYOUT bridge 默认挂的全局名。`exposeDeckLayoutBridge()` 默认暴露到\n * 此名,renderer 的 `createDeckLayoutClient({ bridge: window.__electronDeckLayoutBridge })`\n * 读取同名。单一来源避免 preload helper 与 client 之间字符串漂移。\n */\nexport const DEFAULT_LAYOUT_BRIDGE_GLOBAL = '__electronDeckLayoutBridge'\n\n/** Bridge protocol semver;client 在 ready() 时校验 major 一致 */\nexport const BRIDGE_PROTOCOL_VERSION = '1.0.0'\n\nexport const DeckChannel = {\n\tInvoke: '__electron-deck:invoke',\n\tEvent: '__electron-deck:event',\n\tProbe: '__electron-deck:probe',\n\tPlace: '__electron-deck:place',\n\tSlotGrant: '__electron-deck:slot-grant',\n\tLayoutSubscribe: '__electron-deck:layout-subscribe',\n} as const\n\nexport type InvokeKind = 'host' | 'simulator'\n\nexport interface InvokeRequest {\n\treadonly kind: InvokeKind\n\treadonly name: string\n\treadonly args: readonly JsonValue[]\n}\n\nexport interface InvokeSuccess<R extends JsonValue = JsonValue> {\n\treadonly ok: true\n\treadonly result: R\n}\n\nexport interface InvokeFailure {\n\treadonly ok: false\n\treadonly error: {\n\t\treadonly remoteName: string\n\t\treadonly message: string\n\t\treadonly code?: string\n\t}\n}\n\nexport type InvokeResponse<R extends JsonValue = JsonValue> =\n\t| InvokeSuccess<R>\n\t| InvokeFailure\n\nexport interface EventEnvelope<P extends JsonValue = JsonValue> {\n\treadonly name: string\n\treadonly payload: P\n}\n\nexport interface ProbeResponse {\n\treadonly ready: true\n\treadonly version: typeof BRIDGE_PROTOCOL_VERSION\n}\n\n/**\n * Bridge global 暴露到 webview window 的 shape。preload 把它通过\n * `contextBridge.exposeInMainWorld(globalName, bridge)` 注入;webview-side\n * `createDeckClient()` 通过 `globalThis[globalName]` 读取。\n *\n * 注意所有方法必须是 contextBridge-friendly(plain values + serializable\n * arguments),不要把 Map / Set / Date / Promise.race 等 leak 进 bridge 接口。\n */\nexport interface DeckBridge {\n\treadonly version: typeof BRIDGE_PROTOCOL_VERSION\n\tprobe(): Promise<ProbeResponse>\n\tinvoke(req: InvokeRequest): Promise<InvokeResponse>\n\t/** 订阅 event channel;返回 unsubscribe 函数(不是 Disposable,因为要跨 contextBridge) */\n\tonEvent(listener: (env: EventEnvelope) => void): () => void\n}\n","import { contextBridge, ipcRenderer } from 'electron'\nimport {\n\tBRIDGE_PROTOCOL_VERSION,\n\tDEFAULT_BRIDGE_GLOBAL,\n\tDEFAULT_LAYOUT_BRIDGE_GLOBAL,\n\tDeckChannel,\n} from '../shared/protocol.js'\nimport type {\n\tEventEnvelope,\n\tInvokeRequest,\n\tInvokeResponse,\n\tProbeResponse,\n\tDeckBridge,\n} from '../shared/protocol.js'\nimport type { LayoutBridge, SlotGrant } from '../client/layout-client.js'\n\nexport interface ExposeBridgeOptions {\n\t/** 暴露到 window 的全局名,默认 `__electronDeckBridge` */\n\treadonly globalName?: string\n}\n\n/**\n * 在 host preload 内调用,把 framework typed RPC + event push bridge 暴露到\n * webview window:\n *\n * ```ts\n * import { contextBridge, ipcRenderer } from 'electron'\n * import { exposeDeckBridge } from '@dimina-kit/electron-deck/preload'\n * exposeDeckBridge()\n * ```\n *\n * 见 `DeckBridge` 接口(`shared/protocol.ts`)。\n */\nexport function exposeDeckBridge(options?: ExposeBridgeOptions): void {\n\tif (typeof contextBridge?.exposeInMainWorld !== 'function' || typeof ipcRenderer?.invoke !== 'function') {\n\t\tthrow new Error('exposeDeckBridge: must be called from a preload script (electron contextBridge / ipcRenderer unavailable)')\n\t}\n\n\tconst globalName = options?.globalName ?? DEFAULT_BRIDGE_GLOBAL\n\t// 自检 globalThis —— contextBridge 内部也维护去重,但我们抢先抛更明确的诊断\n\tconst g = globalThis as unknown as Record<string, unknown>\n\tif (g[globalName] !== undefined) {\n\t\tthrow new Error(`Deck bridge already exposed at \"${globalName}\"`)\n\t}\n\n\tconst bridge: DeckBridge = {\n\t\tversion: BRIDGE_PROTOCOL_VERSION,\n\t\tprobe(): Promise<ProbeResponse> {\n\t\t\treturn ipcRenderer.invoke(DeckChannel.Probe) as Promise<ProbeResponse>\n\t\t},\n\t\tinvoke(req: InvokeRequest): Promise<InvokeResponse> {\n\t\t\treturn ipcRenderer.invoke(DeckChannel.Invoke, req) as Promise<InvokeResponse>\n\t\t},\n\t\tonEvent(listener: (env: EventEnvelope) => void): () => void {\n\t\t\tconst wrapped = (_event: unknown, env: EventEnvelope): void => {\n\t\t\t\tlistener(env)\n\t\t\t}\n\t\t\tipcRenderer.on(DeckChannel.Event, wrapped)\n\t\t\treturn () => {\n\t\t\t\tipcRenderer.removeListener(DeckChannel.Event, wrapped)\n\t\t\t}\n\t\t},\n\t}\n\n\tcontextBridge.exposeInMainWorld(globalName, bridge)\n}\n\nexport interface ExposeLayoutBridgeOptions {\n\t/** 暴露到 window 的全局名,默认 `__electronDeckLayoutBridge` */\n\treadonly globalName?: string\n}\n\n/**\n * 在 host preload 内调用,把三条 slot-token LAYOUT channel(`slot-grant` PUSH /\n * `place` send / `layout-subscribe` invoke)封装成一个 `LayoutBridge`-shaped\n * 对象暴露到 webview window,供 renderer:\n *\n * ```ts\n * import { exposeDeckLayoutBridge } from '@dimina-kit/electron-deck/preload'\n * exposeDeckLayoutBridge()\n * // renderer:\n * createDeckLayoutClient({ bridge: window.__electronDeckLayoutBridge })\n * ```\n *\n * channel 名一律取自框架 `DeckChannel`(不手抄字符串)。`onSlotGrant` 返回一个\n * 纯 unsubscribe 函数(可跨 contextBridge),不是 Disposable 对象。\n */\nexport function exposeDeckLayoutBridge(options?: ExposeLayoutBridgeOptions): void {\n\tif (typeof contextBridge?.exposeInMainWorld !== 'function' || typeof ipcRenderer?.on !== 'function') {\n\t\tthrow new Error('exposeDeckLayoutBridge: must be called from a preload script (electron contextBridge / ipcRenderer unavailable)')\n\t}\n\n\tconst globalName = options?.globalName ?? DEFAULT_LAYOUT_BRIDGE_GLOBAL\n\tconst g = globalThis as unknown as Record<string, unknown>\n\tif (g[globalName] !== undefined) {\n\t\tthrow new Error(`Deck layout bridge already exposed at \"${globalName}\"`)\n\t}\n\n\tconst bridge: LayoutBridge = {\n\t\tonSlotGrant(cb: (grant: SlotGrant) => void): () => void {\n\t\t\tconst listener = (_event: unknown, grant: SlotGrant): void => {\n\t\t\t\tcb(grant)\n\t\t\t}\n\t\t\tipcRenderer.on(DeckChannel.SlotGrant, listener)\n\t\t\treturn () => {\n\t\t\t\tipcRenderer.removeListener(DeckChannel.SlotGrant, listener)\n\t\t\t}\n\t\t},\n\t\tsendPlace(msg): void {\n\t\t\tvoid ipcRenderer.invoke(DeckChannel.Place, msg).catch(() => {})\n\t\t},\n\t\tsubscribe(): void {\n\t\t\tvoid ipcRenderer.invoke(DeckChannel.LayoutSubscribe).catch(() => {})\n\t\t},\n\t}\n\n\tcontextBridge.exposeInMainWorld(globalName, bridge)\n}\n\nexport type {\n\tEventEnvelope,\n\tInvokeRequest,\n\tInvokeResponse,\n\tProbeResponse,\n\tDeckBridge,\n\tLayoutBridge,\n\tSlotGrant,\n}\nexport { BRIDGE_PROTOCOL_VERSION, DEFAULT_BRIDGE_GLOBAL, DEFAULT_LAYOUT_BRIDGE_GLOBAL, DeckChannel }\n"],"mappings":";;;;AAkBA,IAAa,wBAAwB;;;;;;AAOrC,IAAa,+BAA+B;;AAG5C,IAAa,0BAA0B;AAEvC,IAAa,cAAc;CAC1B,QAAQ;CACR,OAAO;CACP,OAAO;CACP,OAAO;CACP,WAAW;CACX,iBAAiB;AAClB;;;;;;;;;;;;;;;ACJA,SAAgB,iBAAiB,SAAqC;CACrE,IAAI,OAAO,SAAA,eAAe,sBAAsB,cAAc,OAAO,SAAA,aAAa,WAAW,YAC5F,MAAM,IAAI,MAAM,2GAA2G;CAG5H,MAAM,aAAa,SAAS,cAAA;CAG5B,IAAI,WAAE,gBAAgB,KAAA,GACrB,MAAM,IAAI,MAAM,mCAAmC,WAAW,EAAE;CAGjE,MAAM,SAAqB;EAC1B,SAAS;EACT,QAAgC;GAC/B,OAAO,SAAA,YAAY,OAAO,YAAY,KAAK;EAC5C;EACA,OAAO,KAA6C;GACnD,OAAO,SAAA,YAAY,OAAO,YAAY,QAAQ,GAAG;EAClD;EACA,QAAQ,UAAoD;GAC3D,MAAM,WAAW,QAAiB,QAA6B;IAC9D,SAAS,GAAG;GACb;GACA,SAAA,YAAY,GAAG,YAAY,OAAO,OAAO;GACzC,aAAa;IACZ,SAAA,YAAY,eAAe,YAAY,OAAO,OAAO;GACtD;EACD;CACD;CAEA,SAAA,cAAc,kBAAkB,YAAY,MAAM;AACnD;;;;;;;;;;;;;;;;AAsBA,SAAgB,uBAAuB,SAA2C;CACjF,IAAI,OAAO,SAAA,eAAe,sBAAsB,cAAc,OAAO,SAAA,aAAa,OAAO,YACxF,MAAM,IAAI,MAAM,iHAAiH;CAGlI,MAAM,aAAa,SAAS,cAAA;CAE5B,IAAI,WAAE,gBAAgB,KAAA,GACrB,MAAM,IAAI,MAAM,0CAA0C,WAAW,EAAE;CAqBxE,SAAA,cAAc,kBAAkB,YAAY;EAjB3C,YAAY,IAA4C;GACvD,MAAM,YAAY,QAAiB,UAA2B;IAC7D,GAAG,KAAK;GACT;GACA,SAAA,YAAY,GAAG,YAAY,WAAW,QAAQ;GAC9C,aAAa;IACZ,SAAA,YAAY,eAAe,YAAY,WAAW,QAAQ;GAC3D;EACD;EACA,UAAU,KAAW;GACpB,SAAK,YAAY,OAAO,YAAY,OAAO,GAAG,EAAE,YAAY,CAAC,CAAC;EAC/D;EACA,YAAkB;GACjB,SAAK,YAAY,OAAO,YAAY,eAAe,EAAE,YAAY,CAAC,CAAC;EACpE;CAG2C,CAAM;AACnD"}
@@ -0,0 +1,43 @@
1
+ import { BRIDGE_PROTOCOL_VERSION, DEFAULT_BRIDGE_GLOBAL, DEFAULT_LAYOUT_BRIDGE_GLOBAL, DeckChannel } from '../shared/protocol.js';
2
+ import type { EventEnvelope, InvokeRequest, InvokeResponse, ProbeResponse, DeckBridge } from '../shared/protocol.js';
3
+ import type { LayoutBridge, SlotGrant } from '../client/layout-client.js';
4
+ export interface ExposeBridgeOptions {
5
+ /** 暴露到 window 的全局名,默认 `__electronDeckBridge` */
6
+ readonly globalName?: string;
7
+ }
8
+ /**
9
+ * 在 host preload 内调用,把 framework typed RPC + event push bridge 暴露到
10
+ * webview window:
11
+ *
12
+ * ```ts
13
+ * import { contextBridge, ipcRenderer } from 'electron'
14
+ * import { exposeDeckBridge } from '@dimina-kit/electron-deck/preload'
15
+ * exposeDeckBridge()
16
+ * ```
17
+ *
18
+ * 见 `DeckBridge` 接口(`shared/protocol.ts`)。
19
+ */
20
+ export declare function exposeDeckBridge(options?: ExposeBridgeOptions): void;
21
+ export interface ExposeLayoutBridgeOptions {
22
+ /** 暴露到 window 的全局名,默认 `__electronDeckLayoutBridge` */
23
+ readonly globalName?: string;
24
+ }
25
+ /**
26
+ * 在 host preload 内调用,把三条 slot-token LAYOUT channel(`slot-grant` PUSH /
27
+ * `place` send / `layout-subscribe` invoke)封装成一个 `LayoutBridge`-shaped
28
+ * 对象暴露到 webview window,供 renderer:
29
+ *
30
+ * ```ts
31
+ * import { exposeDeckLayoutBridge } from '@dimina-kit/electron-deck/preload'
32
+ * exposeDeckLayoutBridge()
33
+ * // renderer:
34
+ * createDeckLayoutClient({ bridge: window.__electronDeckLayoutBridge })
35
+ * ```
36
+ *
37
+ * channel 名一律取自框架 `DeckChannel`(不手抄字符串)。`onSlotGrant` 返回一个
38
+ * 纯 unsubscribe 函数(可跨 contextBridge),不是 Disposable 对象。
39
+ */
40
+ export declare function exposeDeckLayoutBridge(options?: ExposeLayoutBridgeOptions): void;
41
+ export type { EventEnvelope, InvokeRequest, InvokeResponse, ProbeResponse, DeckBridge, LayoutBridge, SlotGrant, };
42
+ export { BRIDGE_PROTOCOL_VERSION, DEFAULT_BRIDGE_GLOBAL, DEFAULT_LAYOUT_BRIDGE_GLOBAL, DeckChannel };
43
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/preload/index.ts"],"names":[],"mappings":"AACA,OAAO,EACN,uBAAuB,EACvB,qBAAqB,EACrB,4BAA4B,EAC5B,WAAW,EACX,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EACX,aAAa,EACb,aAAa,EACb,cAAc,EACd,aAAa,EACb,UAAU,EACV,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEzE,MAAM,WAAW,mBAAmB;IACnC,gDAAgD;IAChD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAgCpE;AAED,MAAM,WAAW,yBAAyB;IACzC,sDAAsD;IACtD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,yBAAyB,GAAG,IAAI,CA8BhF;AAED,YAAY,EACX,aAAa,EACb,aAAa,EACb,cAAc,EACd,aAAa,EACb,UAAU,EACV,YAAY,EACZ,SAAS,GACT,CAAA;AACD,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,4BAA4B,EAAE,WAAW,EAAE,CAAA"}
@@ -0,0 +1,80 @@
1
+ import { i as DeckChannel, n as DEFAULT_BRIDGE_GLOBAL, r as DEFAULT_LAYOUT_BRIDGE_GLOBAL, t as BRIDGE_PROTOCOL_VERSION } from "../protocol-BJqo-vP2.js";
2
+ import { contextBridge, ipcRenderer } from "electron";
3
+ //#region src/preload/index.ts
4
+ /**
5
+ * 在 host preload 内调用,把 framework typed RPC + event push bridge 暴露到
6
+ * webview window:
7
+ *
8
+ * ```ts
9
+ * import { contextBridge, ipcRenderer } from 'electron'
10
+ * import { exposeDeckBridge } from '@dimina-kit/electron-deck/preload'
11
+ * exposeDeckBridge()
12
+ * ```
13
+ *
14
+ * 见 `DeckBridge` 接口(`shared/protocol.ts`)。
15
+ */
16
+ function exposeDeckBridge(options) {
17
+ if (typeof contextBridge?.exposeInMainWorld !== "function" || typeof ipcRenderer?.invoke !== "function") throw new Error("exposeDeckBridge: must be called from a preload script (electron contextBridge / ipcRenderer unavailable)");
18
+ const globalName = options?.globalName ?? "__electronDeckBridge";
19
+ if (globalThis[globalName] !== void 0) throw new Error(`Deck bridge already exposed at "${globalName}"`);
20
+ const bridge = {
21
+ version: BRIDGE_PROTOCOL_VERSION,
22
+ probe() {
23
+ return ipcRenderer.invoke(DeckChannel.Probe);
24
+ },
25
+ invoke(req) {
26
+ return ipcRenderer.invoke(DeckChannel.Invoke, req);
27
+ },
28
+ onEvent(listener) {
29
+ const wrapped = (_event, env) => {
30
+ listener(env);
31
+ };
32
+ ipcRenderer.on(DeckChannel.Event, wrapped);
33
+ return () => {
34
+ ipcRenderer.removeListener(DeckChannel.Event, wrapped);
35
+ };
36
+ }
37
+ };
38
+ contextBridge.exposeInMainWorld(globalName, bridge);
39
+ }
40
+ /**
41
+ * 在 host preload 内调用,把三条 slot-token LAYOUT channel(`slot-grant` PUSH /
42
+ * `place` send / `layout-subscribe` invoke)封装成一个 `LayoutBridge`-shaped
43
+ * 对象暴露到 webview window,供 renderer:
44
+ *
45
+ * ```ts
46
+ * import { exposeDeckLayoutBridge } from '@dimina-kit/electron-deck/preload'
47
+ * exposeDeckLayoutBridge()
48
+ * // renderer:
49
+ * createDeckLayoutClient({ bridge: window.__electronDeckLayoutBridge })
50
+ * ```
51
+ *
52
+ * channel 名一律取自框架 `DeckChannel`(不手抄字符串)。`onSlotGrant` 返回一个
53
+ * 纯 unsubscribe 函数(可跨 contextBridge),不是 Disposable 对象。
54
+ */
55
+ function exposeDeckLayoutBridge(options) {
56
+ if (typeof contextBridge?.exposeInMainWorld !== "function" || typeof ipcRenderer?.on !== "function") throw new Error("exposeDeckLayoutBridge: must be called from a preload script (electron contextBridge / ipcRenderer unavailable)");
57
+ const globalName = options?.globalName ?? "__electronDeckLayoutBridge";
58
+ if (globalThis[globalName] !== void 0) throw new Error(`Deck layout bridge already exposed at "${globalName}"`);
59
+ contextBridge.exposeInMainWorld(globalName, {
60
+ onSlotGrant(cb) {
61
+ const listener = (_event, grant) => {
62
+ cb(grant);
63
+ };
64
+ ipcRenderer.on(DeckChannel.SlotGrant, listener);
65
+ return () => {
66
+ ipcRenderer.removeListener(DeckChannel.SlotGrant, listener);
67
+ };
68
+ },
69
+ sendPlace(msg) {
70
+ ipcRenderer.invoke(DeckChannel.Place, msg).catch(() => {});
71
+ },
72
+ subscribe() {
73
+ ipcRenderer.invoke(DeckChannel.LayoutSubscribe).catch(() => {});
74
+ }
75
+ });
76
+ }
77
+ //#endregion
78
+ export { BRIDGE_PROTOCOL_VERSION, DEFAULT_BRIDGE_GLOBAL, DEFAULT_LAYOUT_BRIDGE_GLOBAL, DeckChannel, exposeDeckBridge, exposeDeckLayoutBridge };
79
+
80
+ //# sourceMappingURL=index.js.map