chrome-in-iframe 1.0.1 → 2.0.1

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/README.md CHANGED
@@ -8,6 +8,20 @@ Bridge Chrome extension APIs from a Chrome extension page into an embedded ifram
8
8
 
9
9
  This is useful when an extension page such as `options.html`, `popup.html`, or another `chrome-extension://...` page embeds an iframe, but the iframe itself cannot directly access Chrome extension APIs.
10
10
 
11
+ ## Module Format
12
+
13
+ **This package is ESM-only.** It ships a single ECMAScript module and exposes no CommonJS entry point. Use it from an ESM project (Vite, Rollup, webpack 5+, Parcel 2+, modern bundlers, or native `<script type="module">`).
14
+
15
+ `require('chrome-in-iframe')` will not work.
16
+
17
+ ```json
18
+ {
19
+ "type": "module"
20
+ }
21
+ ```
22
+
23
+ If your tooling still emits CommonJS, configure it to leave `chrome-in-iframe` as an ESM external, or migrate the consuming project to ESM.
24
+
11
25
  ## Install
12
26
 
13
27
  ```bash
@@ -133,9 +147,9 @@ window.addEventListener('beforeunload', () => {
133
147
 
134
148
  ## Security Options
135
149
 
136
- `targetOrigin` and `allowedOrigin` are optional. If omitted, messages are sent with `'*'` and incoming messages are only checked against the expected iframe or parent window source (via `MessageEvent.source`).
150
+ `targetOrigin` and `allowedOrigin` are optional. When `targetOrigin` is not supplied, the library resolves it from `iframe.src` / `contentWindow.location.origin` (on the extension side) or from `document.referrer` / `location.ancestorOrigins` (on the iframe side); it only falls back to `'*'` when none of those are available. Incoming messages are checked against both the expected window source and, by default, the same origin resolved for `targetOrigin`.
137
151
 
138
- > **Warning**: When both sides are Chrome extension pages under the same `chrome-extension://` origin, the default behavior is safe. However, if your iframe loads a **third-party page** or a page from a **different origin**, leaving `allowedOrigin` unset means **any window** can send forged `postMessage` messages and impersonate Chrome API calls. In that case, always set `allowedOrigin` to the expected origin.
152
+ > **Warning**: If origin auto-detection cannot resolve a concrete origin and falls back to `'*'`, incoming messages from the expected window source are allowed from any origin. For third-party iframes, navigable iframes, or any page from a different origin, pass explicit `targetOrigin` and `allowedOrigin`.
139
153
 
140
154
  For production, pass origins when you know them.
141
155
 
@@ -160,6 +174,17 @@ const { chrome } = connectChromeInIframe({
160
174
  });
161
175
  ```
162
176
 
177
+ ## Data Types Across the Bridge
178
+
179
+ The bridge serializes `undefined`, `null`, all primitive types (including `bigint`, `NaN`, `Infinity`, `-0`), plain objects (string- and symbol-keyed), arrays, functions, `Error` (preserves `name`, `stack`, and `cause`), `Date`, `RegExp`, `Map`, and `Set`. Circular references throw.
180
+
181
+ A few inherent limitations of any RPC-style bridge:
182
+
183
+ - **Object identity is not preserved.** A `Date`, `Map`, `Set`, or plain object that crosses the bridge is reconstructed on the other side. `===` between the original and the round-tripped value is `false`.
184
+ - **Map / Set keyed on objects loses lookup.** If you put a `Date` (or any object) into a `Map` and send the `Map` across, `map.get(originalDate)` on the receiving side does **not** find it — the deserialized `Map` holds a different `Date` instance. Key on strings or numbers when the `Map` is meant to travel.
185
+ - **Non-global symbols cannot cross.** Use `Symbol.for(...)` or a well-known symbol; ad-hoc `Symbol('local')` throws on serialization.
186
+ - **Class prototypes are erased.** A `class Foo { ... }` instance arrives as a plain object with the same enumerable properties. `instanceof Foo` is `false`.
187
+
163
188
  ## Reading Properties
164
189
 
165
190
  Chrome API methods can be called directly:
@@ -168,13 +193,54 @@ Chrome API methods can be called directly:
168
193
  const tabs = await chrome.tabs.query({ active: true });
169
194
  ```
170
195
 
171
- Readable properties should be read through `get()` or `$get()`:
196
+ Readable properties — anything that is not a function call — should be read through `get()` (returned from `connectChromeInIframe`) or `$get()` (available on every proxy node).
197
+
198
+ ### Path Forms
199
+
200
+ `$get` / `get` accept three equivalent shapes for the path:
172
201
 
173
202
  ```ts
174
- const extensionId = await get<string>('runtime.id');
175
- const platformOs = await chrome.runtime.$get<string>('PlatformOs');
203
+ // 1. Array form (recommended) — each segment is taken verbatim
204
+ await get<string>(['runtime', 'id']);
205
+
206
+ // 2. Multiple string arguments — each argument is one segment
207
+ await get<string>('runtime', 'id');
208
+
209
+ // 3. Single dotted string — split on `.`
210
+ await get<string>('runtime.id');
176
211
  ```
177
212
 
213
+ You can also start from a nested proxy node:
214
+
215
+ ```ts
216
+ await chrome.runtime.$get<string>('id');
217
+ await chrome.runtime.$get<string>(['id']);
218
+ ```
219
+
220
+ Symbols are allowed as path keys (must be `Symbol.for(...)` or a well-known symbol so identity survives the wire):
221
+
222
+ ```ts
223
+ const queryKey = Symbol.for('queryBySymbol');
224
+ await get(queryKey);
225
+ ```
226
+
227
+ ### Dotted Keys: Use the Array Form
228
+
229
+ The dotted-string form is convenient, but it **splits on every `.` it finds**, so it cannot address keys that contain a dot — for example `chrome.storage.local`'s users sometimes store entries under names like `'user.email'` or `'flag.v2'`. To address those safely, use the array form (or pass each segment as its own argument):
230
+
231
+ ```ts
232
+ // ❌ Wrong — the dotted form would try to read `local.user.email`
233
+ await get('storage.local.user.email');
234
+
235
+ // ✅ Correct — the array form treats `user.email` as a single key
236
+ await get(['storage', 'local', 'user.email']);
237
+
238
+ // ✅ Also correct — multiple arguments are kept verbatim
239
+ await get('storage', 'local', 'user.email');
240
+ ```
241
+
242
+ Rule of thumb: **prefer the array form whenever the path is dynamic or comes from data you don't fully control.** Reserve the dotted-string form for short, hand-written, static lookups.
243
+
178
244
  ## API
179
245
 
180
246
  ### `exposeChromeInIframe(options)`
@@ -188,6 +254,11 @@ const bridge = exposeChromeInIframe({
188
254
  targetOrigin,
189
255
  allowedOrigin,
190
256
  chromeApi,
257
+ timeout,
258
+ functionCacheMax,
259
+ functionCacheTtl,
260
+ remoteCallbackCacheMax,
261
+ remoteCallbackCacheTtl,
191
262
  });
192
263
  ```
193
264
 
@@ -195,15 +266,18 @@ Options:
195
266
 
196
267
  - `iframe`: the iframe element to bridge.
197
268
  - `key`: optional shared bridge key. Use the same key on both sides.
198
- - `targetOrigin`: optional origin used by `postMessage`.
199
- - `allowedOrigin`: optional origin, origin list, or predicate used to filter incoming messages.
269
+ - `targetOrigin`: optional origin used by `postMessage`. Falls back to a value resolved from `iframe.src`.
270
+ - `allowedOrigin`: optional origin, origin list, or predicate used to filter incoming messages. Defaults to the resolved `targetOrigin` when it is a concrete origin.
200
271
  - `chromeApi`: optional custom Chrome-like object. Defaults to `globalThis.chrome`.
272
+ - `timeout`: optional per-call timeout in ms (default `30000`).
273
+ - `functionCacheMax` / `functionCacheTtl`: tune the LRU that stores non-persistent callbacks the local side has sent.
274
+ - `remoteCallbackCacheMax` / `remoteCallbackCacheTtl`: tune the LRU that stores callbacks the remote side has handed to us.
201
275
 
202
276
  Returns:
203
277
 
204
278
  - `proxy`: the bridged Chrome API proxy.
205
- - `get(path)`: read a property from the bridged Chrome API.
206
- - `destroy()`: remove message listeners and clear bridge state.
279
+ - `get(path)`: read a property from the bridged Chrome API. See [Path Forms](#path-forms).
280
+ - `destroy()`: remove message listeners, clear bridge state, and tell the remote peer to release any callbacks it is holding on our behalf.
207
281
 
208
282
  ### `connectChromeInIframe(options)`
209
283
 
@@ -214,6 +288,11 @@ const { chrome, get, destroy } = connectChromeInIframe({
214
288
  key,
215
289
  targetOrigin,
216
290
  allowedOrigin,
291
+ timeout,
292
+ functionCacheMax,
293
+ functionCacheTtl,
294
+ remoteCallbackCacheMax,
295
+ remoteCallbackCacheTtl,
217
296
  });
218
297
  ```
219
298
 
@@ -221,6 +300,21 @@ Returns:
221
300
 
222
301
  - `chrome`: a proxy for the extension page's Chrome API.
223
302
  - `proxy`: the same object as `chrome`.
224
- - `get(path)`: read a property from the bridged Chrome API.
225
- - `destroy()`: remove message listeners and clear bridge state.
303
+ - `get(path)`: read a property from the bridged Chrome API. See [Path Forms](#path-forms).
304
+ - `destroy()`: remove message listeners, clear bridge state, and notify the remote peer.
305
+
306
+ ### `setLogger(logger)`
307
+
308
+ Customize or silence library warnings. All internal warnings carry a `[chrome-in-iframe]` prefix and a scope tag.
226
309
 
310
+ ```ts
311
+ import { setLogger } from 'chrome-in-iframe';
312
+
313
+ // Silence all warnings
314
+ setLogger(null);
315
+
316
+ // Route warnings to a custom sink (e.g. Sentry)
317
+ setLogger((scope, ...args) => {
318
+ captureMessage(`[chrome-in-iframe] ${scope}`, { extra: { args } });
319
+ });
320
+ ```
package/README.zh-CN.md CHANGED
@@ -8,6 +8,20 @@
8
8
 
9
9
  当扩展页面(如 `options.html`、`popup.html` 或其他 `chrome-extension://...` 页面)嵌入了 iframe,但 iframe 本身无法直接访问 Chrome 扩展 API 时,这个库非常有用。
10
10
 
11
+ ## 模块格式
12
+
13
+ **本包只提供 ESM。** 发布产物是单个 ECMAScript 模块,没有 CommonJS 入口。请在 ESM 项目中使用(Vite、Rollup、webpack 5+、Parcel 2+、其他现代打包器,或浏览器原生 `<script type="module">`)。
14
+
15
+ `require('chrome-in-iframe')` 无法工作。
16
+
17
+ ```json
18
+ {
19
+ "type": "module"
20
+ }
21
+ ```
22
+
23
+ 如果你的构建工具仍然产出 CommonJS,请将 `chrome-in-iframe` 配置为 ESM external,或将消费方项目迁移到 ESM。
24
+
11
25
  ## 安装
12
26
 
13
27
  ```bash
@@ -133,11 +147,11 @@ window.addEventListener('beforeunload', () => {
133
147
 
134
148
  ## 安全选项
135
149
 
136
- `targetOrigin` 和 `allowedOrigin` 是可选的。如果省略,消息将使用 `'*'` 发送,传入的消息仅通过 `MessageEvent.source` 与预期的 iframe 或父窗口进行校验。
150
+ `targetOrigin` 和 `allowedOrigin` 是可选的。当不传 `targetOrigin` 时,库会自动从 `iframe.src` / `contentWindow.location.origin`(扩展侧)或 `document.referrer` / `location.ancestorOrigins`(iframe 侧)推导出 origin;只有在以上来源都不可用时才回退到 `'*'`。传入的消息默认同时校验预期的 window source,以及为 `targetOrigin` 推导出的同一个 origin。
137
151
 
138
- > **警告**:当两侧都是同一 `chrome-extension://` origin 下的 Chrome 扩展页面时,默认行为是安全的。但如果 iframe 加载的是**第三方页面**或来自**不同 origin** 的页面,不设置 `allowedOrigin` 意味着**任何窗口**都可以发送伪造的 `postMessage` 消息来冒充 Chrome API 调用。在这种情况下,务必将 `allowedOrigin` 设置为预期的 origin。
152
+ > **警告**:如果自动推导无法得到具体 origin 并回退到 `'*'`,来自预期 window source 的传入消息会允许任意 origin。对于第三方 iframe、可能被导航的 iframe,或任何不同 origin 的页面,请显式传入 `targetOrigin` `allowedOrigin`。
139
153
 
140
- 在生产环境中,如果知道具体来源,建议传入 origin。
154
+ 在生产环境中,如果知道具体来源,建议显式传入 origin。
141
155
 
142
156
  ### 扩展页面
143
157
 
@@ -160,6 +174,17 @@ const { chrome } = connectChromeInIframe({
160
174
  });
161
175
  ```
162
176
 
177
+ ## 跨桥的数据类型
178
+
179
+ 桥接层支持 `undefined`、`null`、所有原始类型(含 `bigint`、`NaN`、`Infinity`、`-0`)、普通对象(字符串键和 symbol 键都行)、数组、函数、`Error`(保留 `name`、`stack`、`cause`)、`Date`、`RegExp`、`Map`、`Set`。循环引用会抛错。
180
+
181
+ 任何 RPC 桥接都有的固有限制:
182
+
183
+ - **对象身份不被保留。** 跨桥的 `Date`、`Map`、`Set`、普通对象会在对端被重新构造。原对象和往返后的对象之间 `===` 为 `false`。
184
+ - **以对象为 key 的 Map / Set 会丢失查找能力。** 把 `Date`(或任何对象)作为 `Map` 的 key,跨桥之后,对端 `map.get(originalDate)` 找不到 —— 反序列化得到的 `Map` 里持有的是另一个 `Date` 实例。需要跨桥传输的 `Map`,请用字符串或数字作 key。
185
+ - **非全局 symbol 不能跨桥。** 用 `Symbol.for(...)` 或 well-known symbol;临时 `Symbol('local')` 在序列化时抛错。
186
+ - **类原型会被擦除。** `class Foo { ... }` 的实例到对端只是一个普通对象,只保留可枚举字段;`instanceof Foo` 为 `false`。
187
+
163
188
  ## 读取属性
164
189
 
165
190
  Chrome API 方法可以直接调用:
@@ -168,13 +193,54 @@ Chrome API 方法可以直接调用:
168
193
  const tabs = await chrome.tabs.query({ active: true });
169
194
  ```
170
195
 
171
- 可读属性应通过 `get()` `$get()` 读取:
196
+ 非函数的可读属性应通过 `get()`(由 `connectChromeInIframe` 返回)或代理上任意节点的 `$get()` 读取。
197
+
198
+ ### 路径形式
199
+
200
+ `$get` / `get` 支持三种等价的路径写法:
172
201
 
173
202
  ```ts
174
- const extensionId = await get<string>('runtime.id');
175
- const platformOs = await chrome.runtime.$get<string>('PlatformOs');
203
+ // 1. 数组形式(推荐)—— 每个元素都被视为单独一段
204
+ await get<string>(['runtime', 'id']);
205
+
206
+ // 2. 多参数字符串 —— 每个参数都是单独一段
207
+ await get<string>('runtime', 'id');
208
+
209
+ // 3. 单个带 `.` 的字符串 —— 按 `.` 自动拆分
210
+ await get<string>('runtime.id');
211
+ ```
212
+
213
+ 也可以从代理的中间节点开始:
214
+
215
+ ```ts
216
+ await chrome.runtime.$get<string>('id');
217
+ await chrome.runtime.$get<string>(['id']);
218
+ ```
219
+
220
+ Symbol 也可以作为路径片段(必须是 `Symbol.for(...)` 或 well-known symbol,以便在序列化后仍能保持身份):
221
+
222
+ ```ts
223
+ const queryKey = Symbol.for('queryBySymbol');
224
+ await get(queryKey);
176
225
  ```
177
226
 
227
+ ### 含 `.` 的键:请用数组形式
228
+
229
+ 字符串带 `.` 的写法很方便,但会**对每一个 `.` 都做拆分**,所以无法访问 key 本身就包含 `.` 的属性 —— 例如 `chrome.storage.local` 的使用者经常会存 `'user.email'`、`'flag.v2'` 这类条目。要安全访问它们,必须改用数组形式(或把每段当作独立参数传):
230
+
231
+ ```ts
232
+ // ❌ 错误 —— 字符串形式会把它当作 `local → user → email` 三段
233
+ await get('storage.local.user.email');
234
+
235
+ // ✅ 正确 —— 数组形式把 `user.email` 当作一段
236
+ await get(['storage', 'local', 'user.email']);
237
+
238
+ // ✅ 也正确 —— 多参数形式同样保留原样
239
+ await get('storage', 'local', 'user.email');
240
+ ```
241
+
242
+ 经验法则:**路径只要是动态的,或者来自不完全可控的数据源,就用数组形式。** 字符串带 `.` 的形式留给短小、手写、静态的查询。
243
+
178
244
  ## API
179
245
 
180
246
  ### `exposeChromeInIframe(options)`
@@ -188,22 +254,30 @@ const bridge = exposeChromeInIframe({
188
254
  targetOrigin,
189
255
  allowedOrigin,
190
256
  chromeApi,
257
+ timeout,
258
+ functionCacheMax,
259
+ functionCacheTtl,
260
+ remoteCallbackCacheMax,
261
+ remoteCallbackCacheTtl,
191
262
  });
192
263
  ```
193
264
 
194
265
  选项:
195
266
 
196
267
  - `iframe`:要桥接的 iframe 元素。
197
- - `key`:可选的共享桥接密钥。两侧使用相同的 key。
198
- - `targetOrigin`:可选,`postMessage` 使用的 origin
199
- - `allowedOrigin`:可选,用于过滤传入消息的 origin、origin 列表或判断函数。
268
+ - `key`:可选的共享桥接 key。两侧使用相同的 key
269
+ - `targetOrigin`:可选,`postMessage` 使用的 origin。不传时由 `iframe.src` 推导。
270
+ - `allowedOrigin`:可选,用于过滤传入消息;可以是 origin、origin 列表或判断函数。默认使用解析后的 `targetOrigin`(当它是具体 origin 时)。
200
271
  - `chromeApi`:可选,自定义的 Chrome API 对象。默认为 `globalThis.chrome`。
272
+ - `timeout`:可选,单次调用的超时时长(毫秒,默认 `30000`)。
273
+ - `functionCacheMax` / `functionCacheTtl`:调整本端发出的非持久 callback 的 LRU 缓存。
274
+ - `remoteCallbackCacheMax` / `remoteCallbackCacheTtl`:调整对端发给本端的 callback 的 LRU 缓存。
201
275
 
202
276
  返回值:
203
277
 
204
278
  - `proxy`:桥接后的 Chrome API 代理。
205
- - `get(path)`:从桥接的 Chrome API 中读取属性。
206
- - `destroy()`:移除消息监听器并清除桥接状态。
279
+ - `get(path)`:从桥接的 Chrome API 中读取属性。详见 [路径形式](#路径形式)。
280
+ - `destroy()`:移除消息监听器,清理桥接状态,并通知对端释放它为我们持有的 callback。
207
281
 
208
282
  ### `connectChromeInIframe(options)`
209
283
 
@@ -214,6 +288,11 @@ const { chrome, get, destroy } = connectChromeInIframe({
214
288
  key,
215
289
  targetOrigin,
216
290
  allowedOrigin,
291
+ timeout,
292
+ functionCacheMax,
293
+ functionCacheTtl,
294
+ remoteCallbackCacheMax,
295
+ remoteCallbackCacheTtl,
217
296
  });
218
297
  ```
219
298
 
@@ -221,5 +300,21 @@ const { chrome, get, destroy } = connectChromeInIframe({
221
300
 
222
301
  - `chrome`:扩展页面 Chrome API 的代理。
223
302
  - `proxy`:与 `chrome` 相同的对象。
224
- - `get(path)`:从桥接的 Chrome API 中读取属性。
225
- - `destroy()`:移除消息监听器并清除桥接状态。
303
+ - `get(path)`:从桥接的 Chrome API 中读取属性。详见 [路径形式](#路径形式)。
304
+ - `destroy()`:移除消息监听器,清理桥接状态,并通知对端。
305
+
306
+ ### `setLogger(logger)`
307
+
308
+ 自定义或静默库内警告。所有内部警告均带有 `[chrome-in-iframe]` 前缀和 scope 标签。
309
+
310
+ ```ts
311
+ import { setLogger } from 'chrome-in-iframe';
312
+
313
+ // 静默所有警告
314
+ setLogger(null);
315
+
316
+ // 将警告路由到自定义 sink(如 Sentry)
317
+ setLogger((scope, ...args) => {
318
+ captureMessage(`[chrome-in-iframe] ${scope}`, { extra: { args } });
319
+ });
320
+ ```
package/dist/api.d.ts CHANGED
@@ -11,6 +11,10 @@ export interface ConnectionOptions {
11
11
  timeout?: number;
12
12
  targetOrigin?: string;
13
13
  allowedOrigin?: AllowedOrigin;
14
+ functionCacheMax?: number;
15
+ functionCacheTtl?: number;
16
+ remoteCallbackCacheMax?: number;
17
+ remoteCallbackCacheTtl?: number;
14
18
  }
15
19
  export interface SetupInMainWindowOptions<T = unknown> extends ConnectionOptions {
16
20
  iframe: HTMLIFrameElement;
@@ -1,3 +1,3 @@
1
1
  import type { ProcessorRegistry } from '../processor/registry';
2
2
  import type { ClientContext, MessageChannel, MessagePoster } from './types';
3
- export declare function createMessageChannel(poster: MessagePoster, key: string, context: ClientContext, processorRegistry: ProcessorRegistry): MessageChannel;
3
+ export declare function createMessageChannel(poster: MessagePoster, key: string, instanceId: string, context: ClientContext, processorRegistry: ProcessorRegistry): MessageChannel;
@@ -1,2 +1,4 @@
1
1
  import type { Callable, CallbackCacheOptions, MessageValue } from './types';
2
- export declare function deserialize(arg: MessageValue, generateCallback: (id: string, args: MessageValue[], options?: CallbackCacheOptions) => MessageValue, getRemoteCallback?: (id: string, invoke: (args: MessageValue[]) => void, options?: CallbackCacheOptions) => Callable, options?: CallbackCacheOptions): MessageValue;
2
+ export type GenerateCallbackFn = (id: string, args: MessageValue[], options?: CallbackCacheOptions) => MessageValue;
3
+ export type GetRemoteCallbackFn = (id: string, invoke: (args: MessageValue[]) => MessageValue, options?: CallbackCacheOptions) => Callable;
4
+ export declare function deserialize(arg: MessageValue, generateCallback: GenerateCallbackFn, getRemoteCallback?: GetRemoteCallbackFn): MessageValue;
@@ -1,3 +1,4 @@
1
- import type { MessageValue, PathKey } from './types';
1
+ import type { PathKey } from './types';
2
2
  export declare function isListenerRegistrationPath(path: PathKey[]): boolean;
3
- export declare function isLikelyListenerPath(path: PathKey[], value?: MessageValue): boolean;
3
+ export declare function isListenerRemovalPath(path: PathKey[]): boolean;
4
+ export declare function isLikelyListenerPath(path: PathKey[]): boolean;
@@ -1,2 +1,2 @@
1
1
  import type { MessagePoster, MessageSender } from './types';
2
- export declare function createMessageSender(poster: MessagePoster, key: string): MessageSender;
2
+ export declare function createMessageSender(poster: MessagePoster, key: string, instanceId: string): MessageSender;
@@ -1,2 +1,4 @@
1
1
  import type { Callable, CallbackCacheOptions, MessageValue } from './types';
2
- export declare function serialize(arg: MessageValue, registerFunction: (fn: Callable, options?: CallbackCacheOptions) => string, options?: CallbackCacheOptions): MessageValue;
2
+ export declare const TYPE_KEY = "$cii$";
3
+ export type RegisterFunctionFn = (fn: Callable, thisArg: MessageValue, options?: CallbackCacheOptions) => string;
4
+ export declare function serialize(arg: MessageValue, registerFunction: RegisterFunctionFn, options?: CallbackCacheOptions): MessageValue;
@@ -40,12 +40,20 @@ export interface Messages {
40
40
  data?: MessageValue;
41
41
  error?: SerializedError;
42
42
  };
43
+ releaseCallbacks: {
44
+ ids: string[];
45
+ };
46
+ destroyEndpoint: {
47
+ instanceId: string;
48
+ };
43
49
  }
44
50
  export type MessageType = keyof Messages;
45
51
  export type MessageBody<K extends MessageType = MessageType> = {
46
52
  type: K;
47
53
  key: string;
48
54
  data: Messages[K];
55
+ senderInstanceId: string;
56
+ targetInstanceId?: string;
49
57
  };
50
58
  export interface MessagePoster {
51
59
  postMessage(message: string): void;
@@ -56,36 +64,51 @@ export interface PromiseCallbacks {
56
64
  resolve: (value: MessageValue) => void;
57
65
  reject: (reason: MessageValue) => void;
58
66
  timer: ReturnType<typeof setTimeout>;
67
+ onResolve?: () => void;
68
+ onReject?: () => void;
59
69
  }
60
70
  export interface ClientContext {
61
71
  getDelegateTarget(): MessageValue;
62
72
  getMessageChannel(): MessageChannel;
63
- getFunctionCache(): LRUCache<string, Callable>;
64
- getPersistentFunctionCache(): Map<string, Callable>;
65
- registerFunction(fn: Callable, options?: CallbackCacheOptions): string;
66
- getRemoteCallback(id: string, invoke: (args: MessageValue[]) => MessageValue, options?: CallbackCacheOptions): Callable;
73
+ getInstanceId(): string;
74
+ getFunctionCache(): LRUCache<string, CallbackEntry>;
75
+ getPersistentFunctionCache(): Map<string, CallbackEntry>;
76
+ registerFunction(fn: Callable, thisArg: MessageValue, options?: CallbackCacheOptions): string;
77
+ getRemoteCallback(id: string, invoke: (args: MessageValue[]) => MessageValue, options: CallbackCacheOptions | undefined, remoteInstanceId: string): Callable;
67
78
  getAndRemovePendingPromise(id: RequestId): PromiseCallbacks | undefined;
68
79
  invoke(path: PathKey[], args: MessageValue[]): Promise<MessageValue>;
69
80
  accessProperty(path: PathKey[]): Promise<MessageValue>;
70
81
  invokeFunctionById(id: string, args: MessageValue[], options?: CallbackCacheOptions): Promise<MessageValue>;
71
- registerPendingPromise(id: RequestId, callbacks: PromiseCallbacks): void;
72
- destroy(): void;
82
+ handleRemoteDestroy(instanceId: string): void;
83
+ releaseRemoteCallbacks(remoteInstanceId: string, ids: readonly string[]): void;
84
+ dropLocalCallback(id: string): void;
85
+ bindMethod(fn: Callable, owner: MessageValue): Callable;
86
+ /** @internal Exposed for tests; do not use in application code. */
87
+ getRemoteCallbackCounts(remoteInstanceId: string): {
88
+ lru: number;
89
+ persistent: number;
90
+ };
91
+ destroy(notifyRemote?: boolean): void;
73
92
  }
74
93
  export interface CallbackCacheOptions {
75
94
  persistent?: boolean;
76
95
  }
96
+ export interface CallbackEntry {
97
+ fn: Callable;
98
+ thisArg: MessageValue;
99
+ }
77
100
  export interface MessageChannel {
78
101
  getSender(): MessageSender;
79
- getContext(): ClientContext;
80
- getPoster(): MessagePoster;
81
- getKey(): string;
82
102
  destroy(): void;
83
103
  }
84
104
  export interface MessageSender {
85
- sendMessage<K extends MessageType>(type: K, data: Messages[K]): void;
105
+ sendMessage<K extends MessageType>(type: K, data: Messages[K], targetInstanceId?: string): void;
86
106
  }
87
107
  export interface RequestHandler<K extends MessageType = MessageType> {
88
- (data: Messages[K], ctx: ClientContext): void;
108
+ (data: Messages[K], ctx: ClientContext, meta: MessageMeta): void;
109
+ }
110
+ export interface MessageMeta {
111
+ senderInstanceId: string;
89
112
  }
90
113
  export type ProxyNode = {
91
114
  parent?: ProxyNode;
@@ -3,3 +3,4 @@ export declare const WELL_KNOWN_SYMBOLS: Record<string, symbol>;
3
3
  export declare function getWellKnownSymbolName(value: symbol): string | undefined;
4
4
  export declare function getWellKnownSymbol(name: string): symbol | undefined;
5
5
  export declare function serializeThrownError(err: unknown): SerializedError;
6
+ export declare function isPromiseLike(value: unknown): value is PromiseLike<unknown>;
@@ -2,5 +2,9 @@ import type { ClientContext, MessageChannel, MessageValue } from '../channel/typ
2
2
  export interface ClientContextOptions {
3
3
  delegateTarget?: MessageValue;
4
4
  timeout?: number;
5
+ functionCacheMax?: number;
6
+ functionCacheTtl?: number;
7
+ remoteCallbackCacheMax?: number;
8
+ remoteCallbackCacheTtl?: number;
5
9
  }
6
- export declare function createClientContext(getMessageChannel: () => MessageChannel, options?: ClientContextOptions): ClientContext;
10
+ export declare function createClientContext(getMessageChannel: () => MessageChannel, instanceId: string, options?: ClientContextOptions): ClientContext;
@@ -1,4 +1,4 @@
1
- import type { MessagePoster } from '../channel/types';
1
+ import type { ClientContext, MessagePoster } from '../channel/types';
2
2
  import { type ClientContextOptions } from './context';
3
3
  export interface EndpointOptions {
4
4
  poster: MessagePoster;
@@ -8,5 +8,7 @@ export interface EndpointOptions {
8
8
  export interface Endpoint<TProxy = unknown> {
9
9
  proxy: TProxy;
10
10
  destroy: () => void;
11
+ /** @internal Exposed for tests; do not use in application code. */
12
+ getContext: () => ClientContext;
11
13
  }
12
14
  export declare function createEndpoint<TProxy = unknown>(options: EndpointOptions): Endpoint<TProxy>;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { connectChromeInIframe, exposeChromeInIframe, setupInIframe, setupInMainWindow } from './api';
2
+ export { setLogger } from './log';
3
+ export type { Logger } from './log';
2
4
  export type { AllowedOrigin, PropertyAccess, PropertyPathPart, ConnectionHandle, ChromeIframeHandle, ExposeChromeInIframeOptions, SetupInIframeOptions, SetupInMainWindowOptions, ConnectionOptions, } from './api';
3
5
  export type { Messages, MessageType, MessageBody, MessagePoster, ClientContext, MessageChannel, MessageSender, ProxyNode, ListenerCallback, SerializedError, } from './channel/types';