@salesforce/sf-embedding-bridge 2.2.2 → 2.2.3

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/index.esm.js CHANGED
@@ -1,5 +1,101 @@
1
- import { BOOTSTRAP_ENVELOPE_TYPE, PROTOCOL_NAME, HOST_META_DATA_PARAM, HEARTBEAT_TYPE, SHUTDOWN_TYPE, HOST_INITIALIZED_METHOD, EMBEDDING_INITIALIZED_METHOD, SfEmbeddingErrorCode, NON_RETRYABLE_CODES } from '@salesforce/sf-embedding-contract';
2
- import { MessageChannelTransport, JsonRpcClient, StandardErrorCode, makeJsonRpcError as makeJsonRpcError$1 } from '@salesforce/jsonrpc';
1
+ import { JsonRpcClient, isJsonRpcRequest, StandardErrorCode, makeJsonRpcError as makeJsonRpcError$1, MessageChannelTransport } from '@salesforce/jsonrpc';
2
+
3
+ // Bootstrap envelope vocabulary. Spec: EMBEDDING-PROTOCOL.md §3.1, §3.1.1,
4
+ // MC-7/MC-8/MC-10 in §2.2.
5
+ /** Wire identifier for this protocol. Carried verbatim on every bootstrap envelope. */
6
+ const PROTOCOL_NAME = "sf-embedding";
7
+ /** Discriminator for the host → embedding bootstrap envelope. */
8
+ const BOOTSTRAP_ENVELOPE_TYPE = "sf-embedding.bootstrap";
9
+ /** Discriminator for the embedding → host content-free heartbeat. */
10
+ const HEARTBEAT_TYPE = "sf-embedding/ready";
11
+ /**
12
+ * Discriminator for the embedding → host fail-closed shutdown signal
13
+ * (e.g., duplicate-transfer detection). Posted via window-level postMessage,
14
+ * not over the JSON-RPC port.
15
+ */
16
+ const SHUTDOWN_TYPE = "sf-embedding/shutdown";
17
+
18
+ // sf-embedding protocol-specific JSON-RPC error codes. Spec §8.2.
19
+ //
20
+ // Standard JSON-RPC codes (-32700, -32600..-32603) and transport-level codes
21
+ // (PORT_CLOSED, SERIALIZATION_FAILURE, REQUEST_TIMEOUT, BACKPRESSURE,
22
+ // REQUEST_CANCELLED) live in `@salesforce/jsonrpc`'s
23
+ // `StandardErrorCode` and are NOT re-exported here. Consumers that need the
24
+ // merged vocabulary compose them at the call site.
25
+ /** sf-embedding protocol-specific error codes (§8.2 numeric assignments). */
26
+ const SfEmbeddingErrorCode = {
27
+ /** Method exists at selected version but is not granted (§6.5). */
28
+ CAPABILITY_NOT_ALLOWED: -32010,
29
+ /** In-flight request gated on a capability that was just revoked (§6.6). */
30
+ CAPABILITY_REVOKED: -32011,
31
+ /** Frame arrived before the required handshake step completed (§5.2, §4.2). */
32
+ NOT_INITIALIZED: -32012,
33
+ /** Embedding's protocol version is not supported by the host (§4.2, §3.1.2). */
34
+ PROTOCOL_VERSION_MISMATCH: -32013,
35
+ /** Frame's origin does not match the bound peer origin (§3.1). */
36
+ ORIGIN_MISMATCH: -32014,
37
+ /** Catch-all — duplicate ui/select-version, duplicate ui/discover-capabilities, etc. */
38
+ PROTOCOL_VIOLATION: -32020,
39
+ /** Embedding observed a second valid bootstrap envelope and shut down (§3.1.2). */
40
+ BOOTSTRAP_DUPLICATE_TRANSFER: -32023,
41
+ };
42
+ /**
43
+ * Codes that are non-retryable by default (§8.4). Consumers building error
44
+ * payloads SHOULD set `retryable: false` automatically for these.
45
+ */
46
+ const NON_RETRYABLE_CODES = new Set([
47
+ SfEmbeddingErrorCode.CAPABILITY_NOT_ALLOWED,
48
+ SfEmbeddingErrorCode.CAPABILITY_REVOKED,
49
+ SfEmbeddingErrorCode.ORIGIN_MISMATCH,
50
+ ]);
51
+
52
+ // Bidirectional custom-event channel.
53
+ const EVENTS_DISPATCH_METHOD = "ui/events/dispatch";
54
+ const EVENTS_SUBSCRIBE_METHOD = "ui/events/subscribe";
55
+ const EVENTS_UNSUBSCRIBE_METHOD = "ui/events/unsubscribe";
56
+
57
+ // Host LWC reserved DOM event names and detail shapes. Spec §12.3.
58
+ //
59
+ // These are dispatched by the host LWC on its own element so application
60
+ // code on the page can react. The embedding never observes them.
61
+ /** Event names the host LWC dispatches on its own element. Reserved by the protocol. */
62
+ const HostLwcEvent = {
63
+ /** Synchronous with `ui/discover-capabilities` response; session reaches READY. */
64
+ READY: "sf-embedding.component.ready",
65
+ /** Misconfiguration, bootstrap failure, embedding-reported fatal, or protocol violation. */
66
+ ERROR: "sf-embedding.component.error",
67
+ };
68
+
69
+ // Host-supplied iframe URL metadata (per ADR 0029). Spec §3.1, MC-8/MC-10.
70
+ /** URL query-parameter name carrying the JSON-encoded `HostMetaData`. */
71
+ const HOST_META_DATA_PARAM = "hostMetaData";
72
+
73
+ // Shared error class for inbound-request handlers on either side of the wire.
74
+ // Routers translate it into a JSON-RPC error response with the carried `code`;
75
+ // any other thrown value falls back to INTERNAL_ERROR (-32603).
76
+ /** Throw from a request handler to surface a specific JSON-RPC error code. */
77
+ class JsonRpcHandlerError extends Error {
78
+ code;
79
+ constructor(code, message) {
80
+ super(message);
81
+ this.code = code;
82
+ this.name = "JsonRpcHandlerError";
83
+ }
84
+ }
85
+
86
+ // JSON-RPC method names on the wire today.
87
+ /** Host announces session identity to the embedding over the port. */
88
+ const HOST_INITIALIZED_METHOD = "ui/notifications/host-initialized";
89
+ /** Embedding acknowledges receipt of `host-initialized`. Fire-and-forget. */
90
+ const EMBEDDING_INITIALIZED_METHOD = "ui/notifications/embedding-initialized";
91
+
92
+ // Resize hint channel: embedding → host, fire-and-forget. CSS pixels.
93
+ const RESIZE_METHOD = "ui/notifications/resize";
94
+
95
+ // UI-state subscription axis. Snapshot in subscribe response; full-replacement change pushes.
96
+ const UI_STATE_SUBSCRIBE_METHOD = "ui/subscribe/ui-state";
97
+ const UI_STATE_UNSUBSCRIBE_METHOD = "ui/unsubscribe/ui-state";
98
+ const UI_STATE_CHANGED_METHOD = "ui/notifications/ui-state-changed";
3
99
 
4
100
  // Bootstrap envelope validator for the iframe (embedding) side of the
5
101
  // sf-embedding handshake.
@@ -89,6 +185,7 @@ function readHostMetaData(search) {
89
185
  return { instanceId, hostAppOrigin };
90
186
  }
91
187
 
188
+ const BRIDGE_PROTOCOL_VERSION = "1.0.0";
92
189
  class BootstrapFailureError extends Error {
93
190
  reason;
94
191
  constructor(reason) {
@@ -103,7 +200,8 @@ function installBootstrapListener() {
103
200
  return Promise.reject(new BootstrapFailureError("MISSING_HOST_METADATA"));
104
201
  }
105
202
  return new Promise((resolve) => {
106
- let accepted = false;
203
+ // Non-null after first accept; needed to close on duplicate transfer.
204
+ let acceptedPort = null;
107
205
  const handler = (event) => {
108
206
  const result = validateBootstrapEnvelope({
109
207
  data: event.data,
@@ -113,7 +211,7 @@ function installBootstrapListener() {
113
211
  expectedSource: window.parent,
114
212
  allowedOrigins: [meta.hostAppOrigin],
115
213
  expectedInstanceId: meta.instanceId,
116
- alreadyAccepted: accepted,
214
+ alreadyAccepted: acceptedPort !== null,
117
215
  });
118
216
  if (!result.ok) {
119
217
  if (result.reason === "DUPLICATE_TRANSFER") {
@@ -123,18 +221,26 @@ function installBootstrapListener() {
123
221
  catch {
124
222
  // ignore
125
223
  }
224
+ // Cannot disambiguate which envelope is real; fail closed.
225
+ try {
226
+ acceptedPort?.close();
227
+ }
228
+ catch {
229
+ // already-closed ports throw on some runtimes
230
+ }
231
+ acceptedPort = null;
232
+ console.error("[sf-embedding]", "DUPLICATE_PORT_TRANSFER: session shut down");
126
233
  window.removeEventListener("message", handler);
127
234
  }
128
235
  return;
129
236
  }
130
- accepted = true;
237
+ acceptedPort = result.port;
131
238
  resolve({ port: result.port, hostMetaData: meta });
132
- // Listener stays registered per spec §3.1 a second valid
133
- // envelope MUST trigger the fail-closed shutdown branch above.
239
+ // Listener stays registered: a second valid envelope must shut down.
134
240
  };
135
241
  window.addEventListener("message", handler);
136
242
  try {
137
- window.parent.postMessage({ type: HEARTBEAT_TYPE, instanceId: meta.instanceId }, meta.hostAppOrigin);
243
+ window.parent.postMessage({ type: HEARTBEAT_TYPE, instanceId: meta.instanceId, protocolVersion: BRIDGE_PROTOCOL_VERSION }, meta.hostAppOrigin);
138
244
  }
139
245
  catch {
140
246
  // ignore
@@ -142,10 +248,310 @@ function installBootstrapListener() {
142
248
  });
143
249
  }
144
250
 
251
+ /**
252
+ * EmbeddingResizer - Handles dynamic iframe/container resizing
253
+ * Uses ResizeObserver to monitor element size changes and notify the host
254
+ */
255
+ class EmbeddingResizer {
256
+ #observer = null;
257
+ #targetElement;
258
+ #onResize;
259
+ #onReady;
260
+ #waitForDOMReady;
261
+ #lastHeight = -1;
262
+ #scheduled = false;
263
+ #isObserving = false;
264
+ constructor(options) {
265
+ this.#targetElement = options.targetElement || document.body;
266
+ this.#onResize = options.onResize;
267
+ this.#onReady = options.onReady;
268
+ this.#waitForDOMReady = options.waitForDOMReady ?? true;
269
+ }
270
+ /**
271
+ * Start observing the target element for size changes
272
+ */
273
+ start() {
274
+ if (this.#isObserving) {
275
+ return;
276
+ }
277
+ if (this.#waitForDOMReady && document.readyState === "loading") {
278
+ document.addEventListener("DOMContentLoaded", () => this.#startObserver(), { once: true });
279
+ }
280
+ else {
281
+ this.#startObserver();
282
+ }
283
+ }
284
+ /**
285
+ * Stop observing and clean up resources
286
+ */
287
+ stop() {
288
+ if (this.#observer) {
289
+ this.#observer.disconnect();
290
+ this.#observer = null;
291
+ }
292
+ this.#isObserving = false;
293
+ this.#lastHeight = -1;
294
+ this.#scheduled = false;
295
+ }
296
+ /**
297
+ * Get the current observed height
298
+ */
299
+ getLastHeight() {
300
+ return this.#lastHeight;
301
+ }
302
+ /**
303
+ * Check if currently observing
304
+ */
305
+ isObserving() {
306
+ return this.#isObserving;
307
+ }
308
+ #startObserver = () => {
309
+ this.#observer = new ResizeObserver((entries) => {
310
+ this.#handleResize(entries);
311
+ });
312
+ this.#observer.observe(this.#targetElement, { box: "border-box" });
313
+ this.#isObserving = true;
314
+ // Invoke ready callback if provided
315
+ if (this.#onReady) {
316
+ this.#onReady();
317
+ }
318
+ };
319
+ #handleResize = (entries) => {
320
+ if (!entries || entries.length === 0) {
321
+ return;
322
+ }
323
+ const entry = entries[0];
324
+ const measuredHeight = this.#extractHeight(entry);
325
+ const heightPx = Math.max(0, Math.ceil(Number(measuredHeight)));
326
+ if (!Number.isFinite(heightPx)) {
327
+ return;
328
+ }
329
+ if (heightPx === this.#lastHeight) {
330
+ return;
331
+ }
332
+ this.#lastHeight = heightPx;
333
+ if (!this.#scheduled) {
334
+ this.#scheduled = true;
335
+ // eslint-disable-next-line @lwc/lwc/no-async-operation
336
+ requestAnimationFrame(() => {
337
+ this.#scheduled = false;
338
+ if (!this.#isObserving)
339
+ return;
340
+ try {
341
+ this.#onResize(this.#lastHeight);
342
+ }
343
+ catch {
344
+ // Swallow: a throwing consumer (e.g. disposed bridge) must not wedge the rAF loop.
345
+ }
346
+ });
347
+ }
348
+ };
349
+ #extractHeight = (entry) => {
350
+ // Try borderBoxSize first (more accurate)
351
+ if (entry.borderBoxSize) {
352
+ if (Array.isArray(entry.borderBoxSize)) {
353
+ return entry.borderBoxSize[0]?.blockSize || 0;
354
+ }
355
+ // Some browsers return a single object instead of array
356
+ return entry.borderBoxSize.blockSize || 0;
357
+ }
358
+ // Fallback to contentRect
359
+ if (entry.contentRect && typeof entry.contentRect.height === "number") {
360
+ return entry.contentRect.height;
361
+ }
362
+ // Last resort: getBoundingClientRect
363
+ if (entry.target && typeof entry.target.getBoundingClientRect === "function") {
364
+ return entry.target.getBoundingClientRect().height;
365
+ }
366
+ return 0;
367
+ };
368
+ }
369
+
370
+ /**
371
+ * JSON-RPC plumbing shared by both peers of the sf-embedding protocol.
372
+ *
373
+ * Two layers live here:
374
+ *
375
+ * - `BridgeClient` — the OUTBOUND half plus inbound responses/notifications.
376
+ * Extends the library `JsonRpcClient`, so it already correlates outbound
377
+ * `send()` requests with their inbound responses, dispatches inbound
378
+ * notifications to `onNotification` handlers, and stamps every outbound
379
+ * frame with a `_meta.traceId`. It owns the single `Transport`.
380
+ *
381
+ * - `JsonRpcRouter` — the INBOUND-REQUEST half. It HOLDS a `BridgeClient`
382
+ * and adds the one thing the client deliberately lacks: routing inbound
383
+ * JSON-RPC *requests* to registered handlers and replying success/error
384
+ * on the same transport (echoing `_meta` for tracing). Notifications,
385
+ * responses, and outbound traffic are delegated straight to the client —
386
+ * no duplicate handler tables.
387
+ *
388
+ * Request vs response/notification dispatch does not collide on the shared
389
+ * transport: `JsonRpcClient` ignores frames carrying both an `id` and a
390
+ * `method` (requests), so the router is free to claim them.
391
+ *
392
+ * Each peer subclasses `JsonRpcRouter` (see `HostBridge` /
393
+ * `EmbeddingBridge`): the subclass registers its inbound handlers in the
394
+ * constructor and exposes named methods for its outbound requests.
395
+ */
396
+ // makeJsonRpcError is the upstream factory; sf-embedding's NON_RETRYABLE_CODES wrapper
397
+ // (in sf-embedding-bridge/src/errors.ts) is bypassed here because `utils` cannot depend
398
+ // on the bridge package. A ratchet test in sf-embedding-contract guards the invariant
399
+ // that NON_RETRYABLE_CODES never overlaps the upstream RETRYABLE set.
400
+ function isRequestHandlerError(err) {
401
+ return err instanceof Error && typeof err.code === "number";
402
+ }
403
+ /**
404
+ * Outbound + response/notification half of a JSON-RPC peer, bound to one
405
+ * transport. Promotes the library client's `protected` send/notify/register
406
+ * methods to a `public` surface the router (and its subclasses) can call, and
407
+ * exposes `post`/`subscribe` so the router can reply to and listen for inbound
408
+ * requests over the same transport.
409
+ *
410
+ * Stamps `_meta.traceId` on every outbound frame; both peers want tracing.
411
+ */
412
+ class BridgeClient extends JsonRpcClient {
413
+ sharedTransport;
414
+ constructor(transport) {
415
+ super(transport);
416
+ this.sharedTransport = transport;
417
+ }
418
+ /** Send a request and await its correlated response. */
419
+ send(method, params) {
420
+ return this.request(method, params);
421
+ }
422
+ /** Send a fire-and-forget notification. */
423
+ notify(method, params) {
424
+ this.sendNotification(method, params);
425
+ }
426
+ /** Register an inbound-notification handler. */
427
+ onNotification(method, handler) {
428
+ this.registerNotificationHandler(method, handler);
429
+ }
430
+ /** @internal — used by JsonRpcRouter to reply to inbound requests. */
431
+ post(frame) {
432
+ this.sharedTransport.post(frame);
433
+ }
434
+ /** @internal — used by JsonRpcRouter to claim inbound requests. */
435
+ subscribe(callback) {
436
+ return this.sharedTransport.onMessage(callback);
437
+ }
438
+ /** Tear down the underlying transport if it is disposable (e.g. MessageChannelTransport closes its port). */
439
+ dispose() {
440
+ const transport = this.sharedTransport;
441
+ if (typeof transport.dispose === "function") {
442
+ transport.dispose();
443
+ }
444
+ }
445
+ /** Mints `_meta.traceId` for every outbound request and notification. */
446
+ getOutboundMeta(_method) {
447
+ return { traceId: crypto.randomUUID() };
448
+ }
449
+ }
450
+ /**
451
+ * Inbound-request router. Holds a `BridgeClient` for everything else
452
+ * (outbound requests/notifications, inbound responses, inbound notifications)
453
+ * and adds inbound-request routing + replies on the shared transport.
454
+ *
455
+ * Subclass it per peer: register handlers in the constructor, add named
456
+ * outbound methods that call `send`/`notify`.
457
+ */
458
+ class JsonRpcRouter {
459
+ client;
460
+ requestHandlers = new Map();
461
+ log;
462
+ unsubscribe = null;
463
+ constructor(client, log = () => { }) {
464
+ this.client = client;
465
+ this.log = log;
466
+ // Claim inbound *requests*; the client already handles responses and
467
+ // notifications on the same transport and ignores request frames.
468
+ this.unsubscribe = this.client.subscribe((data) => this.handle(data));
469
+ }
470
+ /** Register a handler for an inbound request method. Returns a disposer; re-registering overwrites. */
471
+ onRequest(method, handler) {
472
+ this.requestHandlers.set(method, handler);
473
+ return () => {
474
+ if (this.requestHandlers.get(method) === handler) {
475
+ this.requestHandlers.delete(method);
476
+ }
477
+ };
478
+ }
479
+ /** Register an inbound-notification handler (delegates to the client). */
480
+ onNotification(method, handler) {
481
+ this.client.onNotification(method, handler);
482
+ }
483
+ /** Send an outbound request and await its response (delegates to the client). */
484
+ send(method, params) {
485
+ return this.client.send(method, params);
486
+ }
487
+ /** Send an outbound notification (delegates to the client). */
488
+ notify(method, params) {
489
+ this.client.notify(method, params);
490
+ }
491
+ /**
492
+ * Tear down: drop the inbound-request subscription, clear handlers, and
493
+ * dispose the client's transport (closes the port). Idempotent.
494
+ */
495
+ dispose() {
496
+ if (this.unsubscribe) {
497
+ this.unsubscribe();
498
+ this.unsubscribe = null;
499
+ }
500
+ this.requestHandlers.clear();
501
+ this.client.dispose();
502
+ }
503
+ /** Route one inbound frame. Only requests are handled here; the rest is the client's. */
504
+ handle(data) {
505
+ if (!isJsonRpcRequest(data))
506
+ return;
507
+ this.handleRequest(data);
508
+ }
509
+ handleRequest(req) {
510
+ const handler = this.requestHandlers.get(req.method);
511
+ if (!handler) {
512
+ this.respondError(req.id, StandardErrorCode.METHOD_NOT_FOUND, `Method not found: ${req.method}`, req._meta);
513
+ return;
514
+ }
515
+ Promise.resolve()
516
+ .then(() => handler(req.params, req._meta))
517
+ .then((result) => this.respondSuccess(req.id, result, req._meta), (err) => {
518
+ const code = isRequestHandlerError(err) ? err.code : StandardErrorCode.INTERNAL_ERROR;
519
+ const message = err instanceof Error ? err.message : String(err);
520
+ this.respondError(req.id, code, message, req._meta);
521
+ });
522
+ }
523
+ respondSuccess(id, result, meta) {
524
+ const frame = { jsonrpc: "2.0", id, result };
525
+ if (meta)
526
+ frame._meta = meta;
527
+ this.postSafely(frame);
528
+ }
529
+ respondError(id, code, message, meta) {
530
+ const frame = {
531
+ jsonrpc: "2.0",
532
+ id,
533
+ error: makeJsonRpcError$1(code, { message }),
534
+ };
535
+ if (meta)
536
+ frame._meta = meta;
537
+ this.postSafely(frame);
538
+ }
539
+ postSafely(frame) {
540
+ try {
541
+ this.client.post(frame);
542
+ }
543
+ catch (err) {
544
+ // Transport.post should not throw on transient failure, but guard so a
545
+ // misbehaving transport can't take down request handling.
546
+ this.log("response post threw —", err);
547
+ }
548
+ }
549
+ }
550
+
145
551
  /** Identity emitted on `ui/notifications/embedding-initialized`. */
146
552
  const EMBEDDING_INFO = {
147
553
  name: "@salesforce/sf-embedding-bridge",
148
- version: "2.2.2",
554
+ version: "2.2.3",
149
555
  };
150
556
 
151
557
  class SessionFailureError extends Error {
@@ -159,10 +565,20 @@ class SessionFailureError extends Error {
159
565
  }
160
566
  }
161
567
  }
162
- class SessionClient extends JsonRpcClient {
163
- waitForHostInitialized() {
164
- return new Promise((resolve) => {
165
- this.registerNotificationHandler(HOST_INITIALIZED_METHOD, (params) => {
568
+ /**
569
+ * Embedding-side peer. One object that routes inbound requests (via
570
+ * `JsonRpcRouter`), handles inbound notifications, and exposes named methods
571
+ * for the embedding's outbound traffic. Register inbound handlers in the
572
+ * constructor; add outbound methods as the protocol grows.
573
+ */
574
+ class EmbeddingBridge extends JsonRpcRouter {
575
+ hostInitialized;
576
+ resizer = null;
577
+ constructor(transport) {
578
+ super(new BridgeClient(transport));
579
+ // Resolve when the host announces it has initialized.
580
+ this.hostInitialized = new Promise((resolve) => {
581
+ this.onNotification(HOST_INITIALIZED_METHOD, (params) => {
166
582
  const payload = params && typeof params === "object" && !Array.isArray(params)
167
583
  ? params
168
584
  : {};
@@ -170,48 +586,119 @@ class SessionClient extends JsonRpcClient {
170
586
  });
171
587
  });
172
588
  }
589
+ /** Await the host's `host-initialized` notification. */
590
+ waitForHostInitialized() {
591
+ return this.hostInitialized;
592
+ }
593
+ /** Announce that the embedding has initialized. */
173
594
  sendInitializedNotification() {
174
595
  const params = { embeddingInfo: EMBEDDING_INFO };
175
- this.sendNotification(EMBEDDING_INITIALIZED_METHOD, params);
596
+ this.notify(EMBEDDING_INITIALIZED_METHOD, params);
597
+ }
598
+ /** Subscribe to host UI-state. Returns the initial snapshot + active subscription id. */
599
+ sendUiStateSubscribe() {
600
+ return this.send(UI_STATE_SUBSCRIBE_METHOD, {});
601
+ }
602
+ /** Tear down an active UI-state subscription. */
603
+ sendUiStateUnsubscribe(subscriptionId) {
604
+ return this.send(UI_STATE_UNSUBSCRIBE_METHOD, { subscriptionId });
605
+ }
606
+ /** Register a handler for `ui/notifications/ui-state-changed`. */
607
+ onUiStateChanged(handler) {
608
+ this.onNotification(UI_STATE_CHANGED_METHOD, handler);
609
+ }
610
+ /** Fire-and-forget dispatch of a custom event (bidirectional `ui/events/dispatch`).
611
+ * `options` controls DOM-event flags on the receiving side; defaults bubble + composed = true. */
612
+ sendEventDispatch(eventType, detail, options) {
613
+ const params = { eventType, detail, ...options };
614
+ this.notify(EVENTS_DISPATCH_METHOD, params);
615
+ }
616
+ /** Subscribe to host-driven events. One subscription per session — local fan-out by eventType is the SDK's job. */
617
+ sendEventSubscribe() {
618
+ return this.send(EVENTS_SUBSCRIBE_METHOD, {});
619
+ }
620
+ /** Tear down a host-event subscription. */
621
+ sendEventUnsubscribe(subscriptionId) {
622
+ return this.send(EVENTS_UNSUBSCRIBE_METHOD, { subscriptionId });
623
+ }
624
+ /** Register a handler for inbound `ui/events/dispatch` (host → embedding). */
625
+ onEventDispatch(handler) {
626
+ this.onNotification(EVENTS_DISPATCH_METHOD, handler);
627
+ }
628
+ /** Fire-and-forget resize hint (`ui/notifications/resize`). */
629
+ sendResize(dimensions) {
630
+ this.notify(RESIZE_METHOD, dimensions);
631
+ }
632
+ /** Attach an auto-emit resizer; stopped on dispose. Replaces any prior resizer. */
633
+ attachResizer(resizer) {
634
+ if (this.resizer && this.resizer !== resizer)
635
+ this.resizer.stop();
636
+ this.resizer = resizer;
637
+ }
638
+ dispose() {
639
+ this.resizer?.stop();
640
+ this.resizer = null;
641
+ super.dispose();
176
642
  }
177
643
  }
178
- function rejectOnAbort(signal) {
644
+ // `cleanup` lets the caller drop our listener from the consumer-owned signal once the race resolves.
645
+ function rejectOnAbort(signal, cleanup) {
179
646
  if (!signal)
180
647
  return new Promise(() => { });
181
648
  if (signal.aborted)
182
649
  return Promise.reject(new SessionFailureError("ABORTED"));
183
650
  return new Promise((_, reject) => {
184
- signal.addEventListener("abort", () => reject(new SessionFailureError("ABORTED")), { once: true });
651
+ signal.addEventListener("abort", () => reject(new SessionFailureError("ABORTED")), {
652
+ once: true,
653
+ signal: cleanup,
654
+ });
185
655
  });
186
656
  }
187
657
  let sessionPromise = null;
188
- /**
189
- * Bootstraps the sf-embedding session. Single-attempt per iframe — recovery
190
- * from failure requires a host-driven remount (`port1` is one-shot; a new
191
- * session needs a new `MessageChannel` + `instanceId`).
192
- */
658
+ /** Single-attempt per iframe; recovery from failure requires a host-driven remount. */
193
659
  function bootstrapSession(options = {}) {
194
660
  if (sessionPromise)
195
661
  return sessionPromise;
196
662
  sessionPromise = installBootstrapListener()
197
663
  .then(async (capture) => {
198
664
  const transport = new MessageChannelTransport(capture.port);
199
- const client = new SessionClient(transport);
200
- const hostInitialized = await Promise.race([
201
- client.waitForHostInitialized(),
202
- rejectOnAbort(options.signal),
203
- ]);
204
- client.sendInitializedNotification();
205
- return {
206
- client,
207
- hostMetaData: capture.hostMetaData,
208
- hostInitialized,
209
- };
665
+ const bridge = new EmbeddingBridge(transport);
666
+ const abortCleanup = new AbortController();
667
+ try {
668
+ const hostInitialized = await Promise.race([
669
+ bridge.waitForHostInitialized(),
670
+ rejectOnAbort(options.signal, abortCleanup.signal),
671
+ ]);
672
+ bridge.sendInitializedNotification();
673
+ const handle = {
674
+ bridge,
675
+ hostMetaData: capture.hostMetaData,
676
+ hostInitialized,
677
+ };
678
+ // Auto-emit resize hints on document.body (height-only, RAF-coalesced).
679
+ // Bridge owns the resizer's lifecycle so dispose() stops the observer.
680
+ const resizer = new EmbeddingResizer({
681
+ waitForDOMReady: true,
682
+ onResize: (height) => bridge.sendResize({ height }),
683
+ });
684
+ bridge.attachResizer(resizer);
685
+ resizer.start();
686
+ return handle;
687
+ }
688
+ catch (err) {
689
+ // Abort or unexpected failure after construction: dispose so the port doesn't leak.
690
+ bridge.dispose();
691
+ throw err;
692
+ }
693
+ finally {
694
+ // Detach our abort listener from the consumer-owned signal.
695
+ abortCleanup.abort();
696
+ }
210
697
  })
211
698
  .catch((err) => {
212
699
  if (err instanceof SessionFailureError)
213
700
  throw err;
214
- if (err instanceof Error && "reason" in err) {
701
+ if (err instanceof BootstrapFailureError) {
215
702
  throw new SessionFailureError(err.reason, err);
216
703
  }
217
704
  throw err;
@@ -239,5 +726,5 @@ function makeJsonRpcError(code, options = {}) {
239
726
  return makeJsonRpcError$1(code, options);
240
727
  }
241
728
 
242
- export { BootstrapFailureError, ErrorCode, SessionFailureError, bootstrapSession, isBootstrapEnvelope, makeJsonRpcError, readHostMetaData, validateBootstrapEnvelope };
729
+ export { BOOTSTRAP_ENVELOPE_TYPE, BootstrapFailureError, BridgeClient, EMBEDDING_INITIALIZED_METHOD, EVENTS_DISPATCH_METHOD, EVENTS_SUBSCRIBE_METHOD, EVENTS_UNSUBSCRIBE_METHOD, EmbeddingBridge, ErrorCode, HEARTBEAT_TYPE, HOST_INITIALIZED_METHOD, HOST_META_DATA_PARAM, HostLwcEvent, JsonRpcHandlerError, JsonRpcRouter, NON_RETRYABLE_CODES, PROTOCOL_NAME, RESIZE_METHOD, SHUTDOWN_TYPE, SessionFailureError, SfEmbeddingErrorCode, UI_STATE_CHANGED_METHOD, UI_STATE_SUBSCRIBE_METHOD, UI_STATE_UNSUBSCRIBE_METHOD, bootstrapSession, isBootstrapEnvelope, makeJsonRpcError, readHostMetaData, validateBootstrapEnvelope };
243
730
  //# sourceMappingURL=index.esm.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/sf-embedding-bridge",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "private": false,
5
5
  "description": "JSON-RPC 2.0 bridge for the sf-embedding host ↔ embedding protocol",
6
6
  "license": "SEE LICENSE IN LICENSE.txt",
@@ -21,11 +21,18 @@
21
21
  "test": "jest --passWithNoTests"
22
22
  },
23
23
  "dependencies": {
24
- "@salesforce/sf-embedding-contract": "2.2.2",
25
- "@salesforce/jsonrpc": "^9.13.0"
24
+ "@salesforce/jsonrpc": "^10.12.3"
25
+ },
26
+ "devDependencies": {
27
+ "@salesforce/sf-embedding-contract": "2.2.3",
28
+ "utils": "2.2.3"
26
29
  },
27
30
  "files": [
28
31
  "dist/",
32
+ "!dist/__tests__/",
33
+ "!dist/__mocks__/",
34
+ "!dist/*.test.js",
35
+ "!dist/*.map",
29
36
  "README.md",
30
37
  "LICENSE.txt"
31
38
  ],
@@ -1 +0,0 @@
1
- {"version":3,"file":"bootstrap-envelope.d.ts","sourceRoot":"","sources":["../src/bootstrap-envelope.ts"],"names":[],"mappings":"AAUA,OAAO,EAA2B,KAAK,iBAAiB,EAAiB,MAAM,mCAAmC,CAAC;AAEnH,MAAM,MAAM,kCAAkC,GACxC,eAAe,GACf,cAAc,GACd,oBAAoB,GACpB,kBAAkB,GAClB,sBAAsB,GACtB,oBAAoB,CAAC;AAE3B,MAAM,MAAM,iCAAiC,GACvC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC5D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,kCAAkC,CAAA;CAAE,CAAC;AAEhE,MAAM,WAAW,gCAAgC;IAC7C,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IAClC,cAAc,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC1C,cAAc,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,OAAO,CAAC;CAC5B;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAQ9E;AAED,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,gCAAgC,GAAG,iCAAiC,CAYpH"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"bootstrap-listener.d.ts","sourceRoot":"","sources":["../src/bootstrap-listener.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,YAAY,EAAiB,MAAM,mCAAmC,CAAC;AAKrG,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,YAAY,CAAC;CAC9B;AAED,MAAM,MAAM,sBAAsB,GAAG,uBAAuB,CAAC;AAE7D,qBAAa,qBAAsB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;gBAC5B,MAAM,EAAE,sBAAsB;CAK7C;AAED,wBAAgB,wBAAwB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAkDrE"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"bootstrap-session.d.ts","sourceRoot":"","sources":["../src/bootstrap-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAA2B,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAIH,KAAK,YAAY,EACpB,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAAE,KAAK,sBAAsB,EAA4B,MAAM,sBAAsB,CAAC;AAG7F,MAAM,MAAM,sBAAsB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7D,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,aAAa,CAAC;IACtB,YAAY,EAAE,YAAY,CAAC;IAC3B,eAAe,EAAE,sBAAsB,CAAC;CAC3C;AAED,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,GAAG,SAAS,CAAC;AAEtE,qBAAa,mBAAoB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;gBAC1B,MAAM,EAAE,oBAAoB,EAAE,KAAK,CAAC,EAAE,OAAO;CAQ5D;AAED,MAAM,WAAW,uBAAuB;IACpC;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACxB;AA+BD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B,GAAG,OAAO,CAAC,aAAa,CAAC,CA+B9F"}