@salesforce/sf-embedding-bridge 2.2.1-rc.8 → 2.2.3-rc.0
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/bootstrap-session.d.ts +44 -11
- package/dist/index.cjs.js +459 -31
- package/dist/index.d.ts +380 -12
- package/dist/index.esm.js +450 -33
- package/package.json +4 -3
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type HostMetaData } from "@salesforce/sf-embedding-contract";
|
|
1
|
+
import { type JsonRpcMeta, type Transport } from "@salesforce/jsonrpc";
|
|
2
|
+
import { type EventsDispatchParams, type EventsSubscribeResult, type HostMetaData, type ResizeParams, type UiStateChangedParams, type UiStateSubscribeResult } from "@salesforce/sf-embedding-contract";
|
|
3
|
+
import { EmbeddingResizer, JsonRpcRouter, type RequestHandler, type Unregister } from "utils";
|
|
3
4
|
import { type BootstrapFailureReason } from "./bootstrap-listener";
|
|
4
5
|
export type HostInitializedPayload = Record<string, unknown>;
|
|
5
6
|
export interface SessionHandle {
|
|
6
|
-
|
|
7
|
+
bridge: EmbeddingBridge;
|
|
7
8
|
hostMetaData: HostMetaData;
|
|
8
9
|
hostInitialized: HostInitializedPayload;
|
|
9
10
|
}
|
|
@@ -13,17 +14,49 @@ export declare class SessionFailureError extends Error {
|
|
|
13
14
|
constructor(reason: SessionFailureReason, cause?: unknown);
|
|
14
15
|
}
|
|
15
16
|
export interface BootstrapSessionOptions {
|
|
16
|
-
/**
|
|
17
|
-
* Cancellation signal. The browser MessagePort API has no port-close event,
|
|
18
|
-
* so the consumer owns the deadline (e.g. AbortSignal.timeout(30_000)).
|
|
19
|
-
* Honored only on the first call; subsequent calls return the cached promise.
|
|
20
|
-
*/
|
|
17
|
+
/** Cancellation signal. Consumer owns the deadline (e.g. AbortSignal.timeout(30_000)). */
|
|
21
18
|
signal?: AbortSignal;
|
|
22
19
|
}
|
|
23
20
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
21
|
+
* Embedding-side peer. One object that routes inbound requests (via
|
|
22
|
+
* `JsonRpcRouter`), handles inbound notifications, and exposes named methods
|
|
23
|
+
* for the embedding's outbound traffic. Register inbound handlers in the
|
|
24
|
+
* constructor; add outbound methods as the protocol grows.
|
|
27
25
|
*/
|
|
26
|
+
export declare class EmbeddingBridge extends JsonRpcRouter {
|
|
27
|
+
private readonly hostInitialized;
|
|
28
|
+
private resizer;
|
|
29
|
+
constructor(transport: Transport);
|
|
30
|
+
/** Await the host's `host-initialized` notification. */
|
|
31
|
+
waitForHostInitialized(): Promise<HostInitializedPayload>;
|
|
32
|
+
/** Announce that the embedding has initialized. */
|
|
33
|
+
sendInitializedNotification(): void;
|
|
34
|
+
/** Subscribe to host UI-state. Returns the initial snapshot + active subscription id. */
|
|
35
|
+
sendUiStateSubscribe(): Promise<UiStateSubscribeResult>;
|
|
36
|
+
/** Tear down an active UI-state subscription. */
|
|
37
|
+
sendUiStateUnsubscribe(subscriptionId: string): Promise<void>;
|
|
38
|
+
/** Register a handler for `ui/notifications/ui-state-changed`. */
|
|
39
|
+
onUiStateChanged(handler: (params: UiStateChangedParams, meta?: JsonRpcMeta) => void): void;
|
|
40
|
+
/** Fire-and-forget dispatch of a custom event (bidirectional `ui/events/dispatch`).
|
|
41
|
+
* `options` controls DOM-event flags on the receiving side; defaults bubble + composed = true. */
|
|
42
|
+
sendEventDispatch(eventType: string, detail: unknown, options?: {
|
|
43
|
+
bubbles?: boolean;
|
|
44
|
+
composed?: boolean;
|
|
45
|
+
cancelable?: boolean;
|
|
46
|
+
}): void;
|
|
47
|
+
/** Subscribe to host-driven events. One subscription per session — local fan-out by eventType is the SDK's job. */
|
|
48
|
+
sendEventSubscribe(): Promise<EventsSubscribeResult>;
|
|
49
|
+
/** Tear down a host-event subscription. */
|
|
50
|
+
sendEventUnsubscribe(subscriptionId: string): Promise<void>;
|
|
51
|
+
/** Register a handler for inbound `ui/events/dispatch` (host → embedding). */
|
|
52
|
+
onEventDispatch(handler: (params: EventsDispatchParams, meta?: JsonRpcMeta) => void): void;
|
|
53
|
+
/** Fire-and-forget resize hint (`ui/notifications/resize`). */
|
|
54
|
+
sendResize(dimensions: ResizeParams): void;
|
|
55
|
+
/** Attach an auto-emit resizer; stopped on dispose. Replaces any prior resizer. */
|
|
56
|
+
attachResizer(resizer: EmbeddingResizer): void;
|
|
57
|
+
dispose(): void;
|
|
58
|
+
}
|
|
59
|
+
export type { RequestHandler, Unregister };
|
|
60
|
+
/** Single-attempt per iframe; recovery from failure requires a host-driven remount. */
|
|
28
61
|
export declare function bootstrapSession(options?: BootstrapSessionOptions): Promise<SessionHandle>;
|
|
29
62
|
//# sourceMappingURL=bootstrap-session.d.ts.map
|
package/dist/index.cjs.js
CHANGED
|
@@ -51,6 +51,11 @@ const NON_RETRYABLE_CODES = new Set([
|
|
|
51
51
|
SfEmbeddingErrorCode.ORIGIN_MISMATCH,
|
|
52
52
|
]);
|
|
53
53
|
|
|
54
|
+
// Bidirectional custom-event channel.
|
|
55
|
+
const EVENTS_DISPATCH_METHOD = "ui/events/dispatch";
|
|
56
|
+
const EVENTS_SUBSCRIBE_METHOD = "ui/events/subscribe";
|
|
57
|
+
const EVENTS_UNSUBSCRIBE_METHOD = "ui/events/unsubscribe";
|
|
58
|
+
|
|
54
59
|
// Host LWC reserved DOM event names and detail shapes. Spec §12.3.
|
|
55
60
|
//
|
|
56
61
|
// These are dispatched by the host LWC on its own element so application
|
|
@@ -67,12 +72,33 @@ const HostLwcEvent = {
|
|
|
67
72
|
/** URL query-parameter name carrying the JSON-encoded `HostMetaData`. */
|
|
68
73
|
const HOST_META_DATA_PARAM = "hostMetaData";
|
|
69
74
|
|
|
75
|
+
// Shared error class for inbound-request handlers on either side of the wire.
|
|
76
|
+
// Routers translate it into a JSON-RPC error response with the carried `code`;
|
|
77
|
+
// any other thrown value falls back to INTERNAL_ERROR (-32603).
|
|
78
|
+
/** Throw from a request handler to surface a specific JSON-RPC error code. */
|
|
79
|
+
class JsonRpcHandlerError extends Error {
|
|
80
|
+
code;
|
|
81
|
+
constructor(code, message) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.code = code;
|
|
84
|
+
this.name = "JsonRpcHandlerError";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
// JSON-RPC method names on the wire today.
|
|
71
89
|
/** Host announces session identity to the embedding over the port. */
|
|
72
90
|
const HOST_INITIALIZED_METHOD = "ui/notifications/host-initialized";
|
|
73
91
|
/** Embedding acknowledges receipt of `host-initialized`. Fire-and-forget. */
|
|
74
92
|
const EMBEDDING_INITIALIZED_METHOD = "ui/notifications/embedding-initialized";
|
|
75
93
|
|
|
94
|
+
// Resize hint channel: embedding → host, fire-and-forget. CSS pixels.
|
|
95
|
+
const RESIZE_METHOD = "ui/notifications/resize";
|
|
96
|
+
|
|
97
|
+
// UI-state subscription axis. Snapshot in subscribe response; full-replacement change pushes.
|
|
98
|
+
const UI_STATE_SUBSCRIBE_METHOD = "ui/subscribe/ui-state";
|
|
99
|
+
const UI_STATE_UNSUBSCRIBE_METHOD = "ui/unsubscribe/ui-state";
|
|
100
|
+
const UI_STATE_CHANGED_METHOD = "ui/notifications/ui-state-changed";
|
|
101
|
+
|
|
76
102
|
// Bootstrap envelope validator for the iframe (embedding) side of the
|
|
77
103
|
// sf-embedding handshake.
|
|
78
104
|
//
|
|
@@ -161,6 +187,7 @@ function readHostMetaData(search) {
|
|
|
161
187
|
return { instanceId, hostAppOrigin };
|
|
162
188
|
}
|
|
163
189
|
|
|
190
|
+
const BRIDGE_PROTOCOL_VERSION = "1.0.0";
|
|
164
191
|
class BootstrapFailureError extends Error {
|
|
165
192
|
reason;
|
|
166
193
|
constructor(reason) {
|
|
@@ -175,7 +202,8 @@ function installBootstrapListener() {
|
|
|
175
202
|
return Promise.reject(new BootstrapFailureError("MISSING_HOST_METADATA"));
|
|
176
203
|
}
|
|
177
204
|
return new Promise((resolve) => {
|
|
178
|
-
|
|
205
|
+
// Non-null after first accept; needed to close on duplicate transfer.
|
|
206
|
+
let acceptedPort = null;
|
|
179
207
|
const handler = (event) => {
|
|
180
208
|
const result = validateBootstrapEnvelope({
|
|
181
209
|
data: event.data,
|
|
@@ -185,7 +213,7 @@ function installBootstrapListener() {
|
|
|
185
213
|
expectedSource: window.parent,
|
|
186
214
|
allowedOrigins: [meta.hostAppOrigin],
|
|
187
215
|
expectedInstanceId: meta.instanceId,
|
|
188
|
-
alreadyAccepted:
|
|
216
|
+
alreadyAccepted: acceptedPort !== null,
|
|
189
217
|
});
|
|
190
218
|
if (!result.ok) {
|
|
191
219
|
if (result.reason === "DUPLICATE_TRANSFER") {
|
|
@@ -195,18 +223,26 @@ function installBootstrapListener() {
|
|
|
195
223
|
catch {
|
|
196
224
|
// ignore
|
|
197
225
|
}
|
|
226
|
+
// Cannot disambiguate which envelope is real; fail closed.
|
|
227
|
+
try {
|
|
228
|
+
acceptedPort?.close();
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// already-closed ports throw on some runtimes
|
|
232
|
+
}
|
|
233
|
+
acceptedPort = null;
|
|
234
|
+
console.error("[sf-embedding]", "DUPLICATE_PORT_TRANSFER: session shut down");
|
|
198
235
|
window.removeEventListener("message", handler);
|
|
199
236
|
}
|
|
200
237
|
return;
|
|
201
238
|
}
|
|
202
|
-
|
|
239
|
+
acceptedPort = result.port;
|
|
203
240
|
resolve({ port: result.port, hostMetaData: meta });
|
|
204
|
-
// Listener stays registered
|
|
205
|
-
// envelope MUST trigger the fail-closed shutdown branch above.
|
|
241
|
+
// Listener stays registered: a second valid envelope must shut down.
|
|
206
242
|
};
|
|
207
243
|
window.addEventListener("message", handler);
|
|
208
244
|
try {
|
|
209
|
-
window.parent.postMessage({ type: HEARTBEAT_TYPE, instanceId: meta.instanceId }, meta.hostAppOrigin);
|
|
245
|
+
window.parent.postMessage({ type: HEARTBEAT_TYPE, instanceId: meta.instanceId, protocolVersion: BRIDGE_PROTOCOL_VERSION }, meta.hostAppOrigin);
|
|
210
246
|
}
|
|
211
247
|
catch {
|
|
212
248
|
// ignore
|
|
@@ -214,10 +250,310 @@ function installBootstrapListener() {
|
|
|
214
250
|
});
|
|
215
251
|
}
|
|
216
252
|
|
|
253
|
+
/**
|
|
254
|
+
* EmbeddingResizer - Handles dynamic iframe/container resizing
|
|
255
|
+
* Uses ResizeObserver to monitor element size changes and notify the host
|
|
256
|
+
*/
|
|
257
|
+
class EmbeddingResizer {
|
|
258
|
+
#observer = null;
|
|
259
|
+
#targetElement;
|
|
260
|
+
#onResize;
|
|
261
|
+
#onReady;
|
|
262
|
+
#waitForDOMReady;
|
|
263
|
+
#lastHeight = -1;
|
|
264
|
+
#scheduled = false;
|
|
265
|
+
#isObserving = false;
|
|
266
|
+
constructor(options) {
|
|
267
|
+
this.#targetElement = options.targetElement || document.body;
|
|
268
|
+
this.#onResize = options.onResize;
|
|
269
|
+
this.#onReady = options.onReady;
|
|
270
|
+
this.#waitForDOMReady = options.waitForDOMReady ?? true;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Start observing the target element for size changes
|
|
274
|
+
*/
|
|
275
|
+
start() {
|
|
276
|
+
if (this.#isObserving) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (this.#waitForDOMReady && document.readyState === "loading") {
|
|
280
|
+
document.addEventListener("DOMContentLoaded", () => this.#startObserver(), { once: true });
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
this.#startObserver();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Stop observing and clean up resources
|
|
288
|
+
*/
|
|
289
|
+
stop() {
|
|
290
|
+
if (this.#observer) {
|
|
291
|
+
this.#observer.disconnect();
|
|
292
|
+
this.#observer = null;
|
|
293
|
+
}
|
|
294
|
+
this.#isObserving = false;
|
|
295
|
+
this.#lastHeight = -1;
|
|
296
|
+
this.#scheduled = false;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Get the current observed height
|
|
300
|
+
*/
|
|
301
|
+
getLastHeight() {
|
|
302
|
+
return this.#lastHeight;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Check if currently observing
|
|
306
|
+
*/
|
|
307
|
+
isObserving() {
|
|
308
|
+
return this.#isObserving;
|
|
309
|
+
}
|
|
310
|
+
#startObserver = () => {
|
|
311
|
+
this.#observer = new ResizeObserver((entries) => {
|
|
312
|
+
this.#handleResize(entries);
|
|
313
|
+
});
|
|
314
|
+
this.#observer.observe(this.#targetElement, { box: "border-box" });
|
|
315
|
+
this.#isObserving = true;
|
|
316
|
+
// Invoke ready callback if provided
|
|
317
|
+
if (this.#onReady) {
|
|
318
|
+
this.#onReady();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
#handleResize = (entries) => {
|
|
322
|
+
if (!entries || entries.length === 0) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const entry = entries[0];
|
|
326
|
+
const measuredHeight = this.#extractHeight(entry);
|
|
327
|
+
const heightPx = Math.max(0, Math.ceil(Number(measuredHeight)));
|
|
328
|
+
if (!Number.isFinite(heightPx)) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (heightPx === this.#lastHeight) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.#lastHeight = heightPx;
|
|
335
|
+
if (!this.#scheduled) {
|
|
336
|
+
this.#scheduled = true;
|
|
337
|
+
// eslint-disable-next-line @lwc/lwc/no-async-operation
|
|
338
|
+
requestAnimationFrame(() => {
|
|
339
|
+
this.#scheduled = false;
|
|
340
|
+
if (!this.#isObserving)
|
|
341
|
+
return;
|
|
342
|
+
try {
|
|
343
|
+
this.#onResize(this.#lastHeight);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Swallow: a throwing consumer (e.g. disposed bridge) must not wedge the rAF loop.
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
#extractHeight = (entry) => {
|
|
352
|
+
// Try borderBoxSize first (more accurate)
|
|
353
|
+
if (entry.borderBoxSize) {
|
|
354
|
+
if (Array.isArray(entry.borderBoxSize)) {
|
|
355
|
+
return entry.borderBoxSize[0]?.blockSize || 0;
|
|
356
|
+
}
|
|
357
|
+
// Some browsers return a single object instead of array
|
|
358
|
+
return entry.borderBoxSize.blockSize || 0;
|
|
359
|
+
}
|
|
360
|
+
// Fallback to contentRect
|
|
361
|
+
if (entry.contentRect && typeof entry.contentRect.height === "number") {
|
|
362
|
+
return entry.contentRect.height;
|
|
363
|
+
}
|
|
364
|
+
// Last resort: getBoundingClientRect
|
|
365
|
+
if (entry.target && typeof entry.target.getBoundingClientRect === "function") {
|
|
366
|
+
return entry.target.getBoundingClientRect().height;
|
|
367
|
+
}
|
|
368
|
+
return 0;
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* JSON-RPC plumbing shared by both peers of the sf-embedding protocol.
|
|
374
|
+
*
|
|
375
|
+
* Two layers live here:
|
|
376
|
+
*
|
|
377
|
+
* - `BridgeClient` — the OUTBOUND half plus inbound responses/notifications.
|
|
378
|
+
* Extends the library `JsonRpcClient`, so it already correlates outbound
|
|
379
|
+
* `send()` requests with their inbound responses, dispatches inbound
|
|
380
|
+
* notifications to `onNotification` handlers, and stamps every outbound
|
|
381
|
+
* frame with a `_meta.traceId`. It owns the single `Transport`.
|
|
382
|
+
*
|
|
383
|
+
* - `JsonRpcRouter` — the INBOUND-REQUEST half. It HOLDS a `BridgeClient`
|
|
384
|
+
* and adds the one thing the client deliberately lacks: routing inbound
|
|
385
|
+
* JSON-RPC *requests* to registered handlers and replying success/error
|
|
386
|
+
* on the same transport (echoing `_meta` for tracing). Notifications,
|
|
387
|
+
* responses, and outbound traffic are delegated straight to the client —
|
|
388
|
+
* no duplicate handler tables.
|
|
389
|
+
*
|
|
390
|
+
* Request vs response/notification dispatch does not collide on the shared
|
|
391
|
+
* transport: `JsonRpcClient` ignores frames carrying both an `id` and a
|
|
392
|
+
* `method` (requests), so the router is free to claim them.
|
|
393
|
+
*
|
|
394
|
+
* Each peer subclasses `JsonRpcRouter` (see `HostBridge` /
|
|
395
|
+
* `EmbeddingBridge`): the subclass registers its inbound handlers in the
|
|
396
|
+
* constructor and exposes named methods for its outbound requests.
|
|
397
|
+
*/
|
|
398
|
+
// makeJsonRpcError is the upstream factory; sf-embedding's NON_RETRYABLE_CODES wrapper
|
|
399
|
+
// (in sf-embedding-bridge/src/errors.ts) is bypassed here because `utils` cannot depend
|
|
400
|
+
// on the bridge package. A ratchet test in sf-embedding-contract guards the invariant
|
|
401
|
+
// that NON_RETRYABLE_CODES never overlaps the upstream RETRYABLE set.
|
|
402
|
+
function isRequestHandlerError(err) {
|
|
403
|
+
return err instanceof Error && typeof err.code === "number";
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Outbound + response/notification half of a JSON-RPC peer, bound to one
|
|
407
|
+
* transport. Promotes the library client's `protected` send/notify/register
|
|
408
|
+
* methods to a `public` surface the router (and its subclasses) can call, and
|
|
409
|
+
* exposes `post`/`subscribe` so the router can reply to and listen for inbound
|
|
410
|
+
* requests over the same transport.
|
|
411
|
+
*
|
|
412
|
+
* Stamps `_meta.traceId` on every outbound frame; both peers want tracing.
|
|
413
|
+
*/
|
|
414
|
+
class BridgeClient extends jsonrpc.JsonRpcClient {
|
|
415
|
+
sharedTransport;
|
|
416
|
+
constructor(transport) {
|
|
417
|
+
super(transport);
|
|
418
|
+
this.sharedTransport = transport;
|
|
419
|
+
}
|
|
420
|
+
/** Send a request and await its correlated response. */
|
|
421
|
+
send(method, params) {
|
|
422
|
+
return this.request(method, params);
|
|
423
|
+
}
|
|
424
|
+
/** Send a fire-and-forget notification. */
|
|
425
|
+
notify(method, params) {
|
|
426
|
+
this.sendNotification(method, params);
|
|
427
|
+
}
|
|
428
|
+
/** Register an inbound-notification handler. */
|
|
429
|
+
onNotification(method, handler) {
|
|
430
|
+
this.registerNotificationHandler(method, handler);
|
|
431
|
+
}
|
|
432
|
+
/** @internal — used by JsonRpcRouter to reply to inbound requests. */
|
|
433
|
+
post(frame) {
|
|
434
|
+
this.sharedTransport.post(frame);
|
|
435
|
+
}
|
|
436
|
+
/** @internal — used by JsonRpcRouter to claim inbound requests. */
|
|
437
|
+
subscribe(callback) {
|
|
438
|
+
return this.sharedTransport.onMessage(callback);
|
|
439
|
+
}
|
|
440
|
+
/** Tear down the underlying transport if it is disposable (e.g. MessageChannelTransport closes its port). */
|
|
441
|
+
dispose() {
|
|
442
|
+
const transport = this.sharedTransport;
|
|
443
|
+
if (typeof transport.dispose === "function") {
|
|
444
|
+
transport.dispose();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/** Mints `_meta.traceId` for every outbound request and notification. */
|
|
448
|
+
getOutboundMeta(_method) {
|
|
449
|
+
return { traceId: crypto.randomUUID() };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Inbound-request router. Holds a `BridgeClient` for everything else
|
|
454
|
+
* (outbound requests/notifications, inbound responses, inbound notifications)
|
|
455
|
+
* and adds inbound-request routing + replies on the shared transport.
|
|
456
|
+
*
|
|
457
|
+
* Subclass it per peer: register handlers in the constructor, add named
|
|
458
|
+
* outbound methods that call `send`/`notify`.
|
|
459
|
+
*/
|
|
460
|
+
class JsonRpcRouter {
|
|
461
|
+
client;
|
|
462
|
+
requestHandlers = new Map();
|
|
463
|
+
log;
|
|
464
|
+
unsubscribe = null;
|
|
465
|
+
constructor(client, log = () => { }) {
|
|
466
|
+
this.client = client;
|
|
467
|
+
this.log = log;
|
|
468
|
+
// Claim inbound *requests*; the client already handles responses and
|
|
469
|
+
// notifications on the same transport and ignores request frames.
|
|
470
|
+
this.unsubscribe = this.client.subscribe((data) => this.handle(data));
|
|
471
|
+
}
|
|
472
|
+
/** Register a handler for an inbound request method. Returns a disposer; re-registering overwrites. */
|
|
473
|
+
onRequest(method, handler) {
|
|
474
|
+
this.requestHandlers.set(method, handler);
|
|
475
|
+
return () => {
|
|
476
|
+
if (this.requestHandlers.get(method) === handler) {
|
|
477
|
+
this.requestHandlers.delete(method);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/** Register an inbound-notification handler (delegates to the client). */
|
|
482
|
+
onNotification(method, handler) {
|
|
483
|
+
this.client.onNotification(method, handler);
|
|
484
|
+
}
|
|
485
|
+
/** Send an outbound request and await its response (delegates to the client). */
|
|
486
|
+
send(method, params) {
|
|
487
|
+
return this.client.send(method, params);
|
|
488
|
+
}
|
|
489
|
+
/** Send an outbound notification (delegates to the client). */
|
|
490
|
+
notify(method, params) {
|
|
491
|
+
this.client.notify(method, params);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Tear down: drop the inbound-request subscription, clear handlers, and
|
|
495
|
+
* dispose the client's transport (closes the port). Idempotent.
|
|
496
|
+
*/
|
|
497
|
+
dispose() {
|
|
498
|
+
if (this.unsubscribe) {
|
|
499
|
+
this.unsubscribe();
|
|
500
|
+
this.unsubscribe = null;
|
|
501
|
+
}
|
|
502
|
+
this.requestHandlers.clear();
|
|
503
|
+
this.client.dispose();
|
|
504
|
+
}
|
|
505
|
+
/** Route one inbound frame. Only requests are handled here; the rest is the client's. */
|
|
506
|
+
handle(data) {
|
|
507
|
+
if (!jsonrpc.isJsonRpcRequest(data))
|
|
508
|
+
return;
|
|
509
|
+
this.handleRequest(data);
|
|
510
|
+
}
|
|
511
|
+
handleRequest(req) {
|
|
512
|
+
const handler = this.requestHandlers.get(req.method);
|
|
513
|
+
if (!handler) {
|
|
514
|
+
this.respondError(req.id, jsonrpc.StandardErrorCode.METHOD_NOT_FOUND, `Method not found: ${req.method}`, req._meta);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
Promise.resolve()
|
|
518
|
+
.then(() => handler(req.params, req._meta))
|
|
519
|
+
.then((result) => this.respondSuccess(req.id, result, req._meta), (err) => {
|
|
520
|
+
const code = isRequestHandlerError(err) ? err.code : jsonrpc.StandardErrorCode.INTERNAL_ERROR;
|
|
521
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
522
|
+
this.respondError(req.id, code, message, req._meta);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
respondSuccess(id, result, meta) {
|
|
526
|
+
const frame = { jsonrpc: "2.0", id, result };
|
|
527
|
+
if (meta)
|
|
528
|
+
frame._meta = meta;
|
|
529
|
+
this.postSafely(frame);
|
|
530
|
+
}
|
|
531
|
+
respondError(id, code, message, meta) {
|
|
532
|
+
const frame = {
|
|
533
|
+
jsonrpc: "2.0",
|
|
534
|
+
id,
|
|
535
|
+
error: jsonrpc.makeJsonRpcError(code, { message }),
|
|
536
|
+
};
|
|
537
|
+
if (meta)
|
|
538
|
+
frame._meta = meta;
|
|
539
|
+
this.postSafely(frame);
|
|
540
|
+
}
|
|
541
|
+
postSafely(frame) {
|
|
542
|
+
try {
|
|
543
|
+
this.client.post(frame);
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
// Transport.post should not throw on transient failure, but guard so a
|
|
547
|
+
// misbehaving transport can't take down request handling.
|
|
548
|
+
this.log("response post threw —", err);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
217
553
|
/** Identity emitted on `ui/notifications/embedding-initialized`. */
|
|
218
554
|
const EMBEDDING_INFO = {
|
|
219
555
|
name: "@salesforce/sf-embedding-bridge",
|
|
220
|
-
version: "2.2.
|
|
556
|
+
version: "2.2.3-rc.0",
|
|
221
557
|
};
|
|
222
558
|
|
|
223
559
|
class SessionFailureError extends Error {
|
|
@@ -231,10 +567,20 @@ class SessionFailureError extends Error {
|
|
|
231
567
|
}
|
|
232
568
|
}
|
|
233
569
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
570
|
+
/**
|
|
571
|
+
* Embedding-side peer. One object that routes inbound requests (via
|
|
572
|
+
* `JsonRpcRouter`), handles inbound notifications, and exposes named methods
|
|
573
|
+
* for the embedding's outbound traffic. Register inbound handlers in the
|
|
574
|
+
* constructor; add outbound methods as the protocol grows.
|
|
575
|
+
*/
|
|
576
|
+
class EmbeddingBridge extends JsonRpcRouter {
|
|
577
|
+
hostInitialized;
|
|
578
|
+
resizer = null;
|
|
579
|
+
constructor(transport) {
|
|
580
|
+
super(new BridgeClient(transport));
|
|
581
|
+
// Resolve when the host announces it has initialized.
|
|
582
|
+
this.hostInitialized = new Promise((resolve) => {
|
|
583
|
+
this.onNotification(HOST_INITIALIZED_METHOD, (params) => {
|
|
238
584
|
const payload = params && typeof params === "object" && !Array.isArray(params)
|
|
239
585
|
? params
|
|
240
586
|
: {};
|
|
@@ -242,48 +588,119 @@ class SessionClient extends jsonrpc.JsonRpcClient {
|
|
|
242
588
|
});
|
|
243
589
|
});
|
|
244
590
|
}
|
|
591
|
+
/** Await the host's `host-initialized` notification. */
|
|
592
|
+
waitForHostInitialized() {
|
|
593
|
+
return this.hostInitialized;
|
|
594
|
+
}
|
|
595
|
+
/** Announce that the embedding has initialized. */
|
|
245
596
|
sendInitializedNotification() {
|
|
246
597
|
const params = { embeddingInfo: EMBEDDING_INFO };
|
|
247
|
-
this.
|
|
598
|
+
this.notify(EMBEDDING_INITIALIZED_METHOD, params);
|
|
599
|
+
}
|
|
600
|
+
/** Subscribe to host UI-state. Returns the initial snapshot + active subscription id. */
|
|
601
|
+
sendUiStateSubscribe() {
|
|
602
|
+
return this.send(UI_STATE_SUBSCRIBE_METHOD, {});
|
|
603
|
+
}
|
|
604
|
+
/** Tear down an active UI-state subscription. */
|
|
605
|
+
sendUiStateUnsubscribe(subscriptionId) {
|
|
606
|
+
return this.send(UI_STATE_UNSUBSCRIBE_METHOD, { subscriptionId });
|
|
607
|
+
}
|
|
608
|
+
/** Register a handler for `ui/notifications/ui-state-changed`. */
|
|
609
|
+
onUiStateChanged(handler) {
|
|
610
|
+
this.onNotification(UI_STATE_CHANGED_METHOD, handler);
|
|
611
|
+
}
|
|
612
|
+
/** Fire-and-forget dispatch of a custom event (bidirectional `ui/events/dispatch`).
|
|
613
|
+
* `options` controls DOM-event flags on the receiving side; defaults bubble + composed = true. */
|
|
614
|
+
sendEventDispatch(eventType, detail, options) {
|
|
615
|
+
const params = { eventType, detail, ...options };
|
|
616
|
+
this.notify(EVENTS_DISPATCH_METHOD, params);
|
|
617
|
+
}
|
|
618
|
+
/** Subscribe to host-driven events. One subscription per session — local fan-out by eventType is the SDK's job. */
|
|
619
|
+
sendEventSubscribe() {
|
|
620
|
+
return this.send(EVENTS_SUBSCRIBE_METHOD, {});
|
|
621
|
+
}
|
|
622
|
+
/** Tear down a host-event subscription. */
|
|
623
|
+
sendEventUnsubscribe(subscriptionId) {
|
|
624
|
+
return this.send(EVENTS_UNSUBSCRIBE_METHOD, { subscriptionId });
|
|
625
|
+
}
|
|
626
|
+
/** Register a handler for inbound `ui/events/dispatch` (host → embedding). */
|
|
627
|
+
onEventDispatch(handler) {
|
|
628
|
+
this.onNotification(EVENTS_DISPATCH_METHOD, handler);
|
|
629
|
+
}
|
|
630
|
+
/** Fire-and-forget resize hint (`ui/notifications/resize`). */
|
|
631
|
+
sendResize(dimensions) {
|
|
632
|
+
this.notify(RESIZE_METHOD, dimensions);
|
|
633
|
+
}
|
|
634
|
+
/** Attach an auto-emit resizer; stopped on dispose. Replaces any prior resizer. */
|
|
635
|
+
attachResizer(resizer) {
|
|
636
|
+
if (this.resizer && this.resizer !== resizer)
|
|
637
|
+
this.resizer.stop();
|
|
638
|
+
this.resizer = resizer;
|
|
639
|
+
}
|
|
640
|
+
dispose() {
|
|
641
|
+
this.resizer?.stop();
|
|
642
|
+
this.resizer = null;
|
|
643
|
+
super.dispose();
|
|
248
644
|
}
|
|
249
645
|
}
|
|
250
|
-
|
|
646
|
+
// `cleanup` lets the caller drop our listener from the consumer-owned signal once the race resolves.
|
|
647
|
+
function rejectOnAbort(signal, cleanup) {
|
|
251
648
|
if (!signal)
|
|
252
649
|
return new Promise(() => { });
|
|
253
650
|
if (signal.aborted)
|
|
254
651
|
return Promise.reject(new SessionFailureError("ABORTED"));
|
|
255
652
|
return new Promise((_, reject) => {
|
|
256
|
-
signal.addEventListener("abort", () => reject(new SessionFailureError("ABORTED")), {
|
|
653
|
+
signal.addEventListener("abort", () => reject(new SessionFailureError("ABORTED")), {
|
|
654
|
+
once: true,
|
|
655
|
+
signal: cleanup,
|
|
656
|
+
});
|
|
257
657
|
});
|
|
258
658
|
}
|
|
259
659
|
let sessionPromise = null;
|
|
260
|
-
/**
|
|
261
|
-
* Bootstraps the sf-embedding session. Single-attempt per iframe — recovery
|
|
262
|
-
* from failure requires a host-driven remount (`port1` is one-shot; a new
|
|
263
|
-
* session needs a new `MessageChannel` + `instanceId`).
|
|
264
|
-
*/
|
|
660
|
+
/** Single-attempt per iframe; recovery from failure requires a host-driven remount. */
|
|
265
661
|
function bootstrapSession(options = {}) {
|
|
266
662
|
if (sessionPromise)
|
|
267
663
|
return sessionPromise;
|
|
268
664
|
sessionPromise = installBootstrapListener()
|
|
269
665
|
.then(async (capture) => {
|
|
270
666
|
const transport = new jsonrpc.MessageChannelTransport(capture.port);
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
667
|
+
const bridge = new EmbeddingBridge(transport);
|
|
668
|
+
const abortCleanup = new AbortController();
|
|
669
|
+
try {
|
|
670
|
+
const hostInitialized = await Promise.race([
|
|
671
|
+
bridge.waitForHostInitialized(),
|
|
672
|
+
rejectOnAbort(options.signal, abortCleanup.signal),
|
|
673
|
+
]);
|
|
674
|
+
bridge.sendInitializedNotification();
|
|
675
|
+
const handle = {
|
|
676
|
+
bridge,
|
|
677
|
+
hostMetaData: capture.hostMetaData,
|
|
678
|
+
hostInitialized,
|
|
679
|
+
};
|
|
680
|
+
// Auto-emit resize hints on document.body (height-only, RAF-coalesced).
|
|
681
|
+
// Bridge owns the resizer's lifecycle so dispose() stops the observer.
|
|
682
|
+
const resizer = new EmbeddingResizer({
|
|
683
|
+
waitForDOMReady: true,
|
|
684
|
+
onResize: (height) => bridge.sendResize({ height }),
|
|
685
|
+
});
|
|
686
|
+
bridge.attachResizer(resizer);
|
|
687
|
+
resizer.start();
|
|
688
|
+
return handle;
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
// Abort or unexpected failure after construction: dispose so the port doesn't leak.
|
|
692
|
+
bridge.dispose();
|
|
693
|
+
throw err;
|
|
694
|
+
}
|
|
695
|
+
finally {
|
|
696
|
+
// Detach our abort listener from the consumer-owned signal.
|
|
697
|
+
abortCleanup.abort();
|
|
698
|
+
}
|
|
282
699
|
})
|
|
283
700
|
.catch((err) => {
|
|
284
701
|
if (err instanceof SessionFailureError)
|
|
285
702
|
throw err;
|
|
286
|
-
if (err instanceof
|
|
703
|
+
if (err instanceof BootstrapFailureError) {
|
|
287
704
|
throw new SessionFailureError(err.reason, err);
|
|
288
705
|
}
|
|
289
706
|
throw err;
|
|
@@ -313,17 +730,28 @@ function makeJsonRpcError(code, options = {}) {
|
|
|
313
730
|
|
|
314
731
|
exports.BOOTSTRAP_ENVELOPE_TYPE = BOOTSTRAP_ENVELOPE_TYPE;
|
|
315
732
|
exports.BootstrapFailureError = BootstrapFailureError;
|
|
733
|
+
exports.BridgeClient = BridgeClient;
|
|
316
734
|
exports.EMBEDDING_INITIALIZED_METHOD = EMBEDDING_INITIALIZED_METHOD;
|
|
735
|
+
exports.EVENTS_DISPATCH_METHOD = EVENTS_DISPATCH_METHOD;
|
|
736
|
+
exports.EVENTS_SUBSCRIBE_METHOD = EVENTS_SUBSCRIBE_METHOD;
|
|
737
|
+
exports.EVENTS_UNSUBSCRIBE_METHOD = EVENTS_UNSUBSCRIBE_METHOD;
|
|
738
|
+
exports.EmbeddingBridge = EmbeddingBridge;
|
|
317
739
|
exports.ErrorCode = ErrorCode;
|
|
318
740
|
exports.HEARTBEAT_TYPE = HEARTBEAT_TYPE;
|
|
319
741
|
exports.HOST_INITIALIZED_METHOD = HOST_INITIALIZED_METHOD;
|
|
320
742
|
exports.HOST_META_DATA_PARAM = HOST_META_DATA_PARAM;
|
|
321
743
|
exports.HostLwcEvent = HostLwcEvent;
|
|
744
|
+
exports.JsonRpcHandlerError = JsonRpcHandlerError;
|
|
745
|
+
exports.JsonRpcRouter = JsonRpcRouter;
|
|
322
746
|
exports.NON_RETRYABLE_CODES = NON_RETRYABLE_CODES;
|
|
323
747
|
exports.PROTOCOL_NAME = PROTOCOL_NAME;
|
|
748
|
+
exports.RESIZE_METHOD = RESIZE_METHOD;
|
|
324
749
|
exports.SHUTDOWN_TYPE = SHUTDOWN_TYPE;
|
|
325
750
|
exports.SessionFailureError = SessionFailureError;
|
|
326
751
|
exports.SfEmbeddingErrorCode = SfEmbeddingErrorCode;
|
|
752
|
+
exports.UI_STATE_CHANGED_METHOD = UI_STATE_CHANGED_METHOD;
|
|
753
|
+
exports.UI_STATE_SUBSCRIBE_METHOD = UI_STATE_SUBSCRIBE_METHOD;
|
|
754
|
+
exports.UI_STATE_UNSUBSCRIBE_METHOD = UI_STATE_UNSUBSCRIBE_METHOD;
|
|
327
755
|
exports.bootstrapSession = bootstrapSession;
|
|
328
756
|
exports.isBootstrapEnvelope = isBootstrapEnvelope;
|
|
329
757
|
exports.makeJsonRpcError = makeJsonRpcError;
|