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/CHANGELOG.md ADDED
@@ -0,0 +1,304 @@
1
+ # Changelog
2
+
3
+ ## 2.0.1
4
+
5
+ ### Bug fixes
6
+
7
+ - `handleReleaseCallbacks` now also evicts the corresponding entries from the
8
+ receiver's `remoteCallbacksByOwner` / `persistentRemoteCallbacksByOwner`
9
+ maps. Previously only the receiver's own `functionCache` /
10
+ `persistentFunctionCache` were touched, so the host kept the wrapper
11
+ function for every `chrome.*.onX.addListener(handler)` indefinitely — a
12
+ real leak in long-lived service workers with frequent listener churn.
13
+ - Reordered the `removeListener` flow on the sender side: the
14
+ `invokeRequest` is now dispatched before the `releaseCallbacks` so the
15
+ receiver can still resolve the cached wrapper, call
16
+ `chrome.*.removeListener(W)` correctly, and only afterwards drop it.
17
+ - The error response handlers (`invokeResponse`, `accessPropertyResponse`,
18
+ `invokeFunctionByIdResponse`) no longer clobber the local Error's stack
19
+ with `undefined` when the remote payload has no `stack` field.
20
+ - A function reused as a listener for several events (e.g. both
21
+ `tabs.onActivated.addListener(handler)` and
22
+ `tabs.onUpdated.addListener(handler)`) is now reference-counted on the
23
+ sender side. Removing one registration no longer evicts the shared id, so
24
+ the other listener keeps working.
25
+ - `handleInvokeRequest` rejects empty-path invocations with a clear error
26
+ instead of bottoming out at `'undefined' is not a function`.
27
+ - Mid-path read errors now report the actual parent value (`null` /
28
+ `undefined`) plus the full traversed prefix, instead of stringifying the
29
+ previous path segment.
30
+ - `isAllowedOrigin` distinguishes `undefined` (no restriction) from `''`
31
+ (empty allow-list); the empty string is no longer treated as
32
+ "allow everything".
33
+ - When `allowedOrigin` is omitted, incoming `postMessage` events are now
34
+ filtered against the same concrete origin resolved for `targetOrigin`.
35
+ This keeps the default bridge configuration from accepting messages
36
+ from an unexpected origin while still preserving the documented `'*'`
37
+ fallback when origin auto-detection cannot resolve one.
38
+ - `invoke` / `accessProperty` / `invokeFunctionById` now clear the pending
39
+ Map entry and timer when `serialize` / `sendMessage` throws synchronously.
40
+ Previously the entry lingered until the timeout fired.
41
+ - `timeout` is clamped to the default when given a non-finite or
42
+ non-positive value, so accidental `timeout: 0` no longer rejects every
43
+ call instantly.
44
+ - Listener registration/removal is now transactional. Failed
45
+ `addListener` calls roll back local persistent callback state and ask the
46
+ peer to release any wrapper it created; failed `removeListener` calls no
47
+ longer invalidate a callback that the remote side still has registered.
48
+ - Remote method and callback results now treat any thenable as async instead
49
+ of relying on `instanceof Promise`, so cross-realm Promises and custom
50
+ thenables resolve/reject correctly.
51
+ - `destroy()` no longer warns when the only failure is an already-detached
52
+ iframe whose `contentWindow` is unavailable during shutdown.
53
+ - Nested function arguments inside `addListener` payloads are now
54
+ release-tracked. Previously only top-level function arguments were
55
+ collected for cleanup, so a call like
56
+ `addListener({ filter, handler })` left `handler` pinned in the
57
+ persistent cache even after the matching `removeListener`.
58
+ - The `setTimeout` branch in `invoke` / `accessProperty` /
59
+ `invokeFunctionById` now triggers the pending entry's `onReject`
60
+ before rejecting. A timed-out `addListener` no longer leaks its
61
+ persistent registration.
62
+ - `hasListener` is no longer classified as a listener-registration path.
63
+ Its argument used to be pinned as persistent indefinitely, because
64
+ `hasListener` has no `removeListener` counterpart that would refcount
65
+ it back down.
66
+ - Sender-side function ids are now keyed by `(fn, thisArg)` rather than
67
+ just `fn`. The same function reused under different owners (e.g. one
68
+ method shared across two parent objects) no longer overwrites the
69
+ first registration's `thisArg`.
70
+ - `accessPropertyRequest` now rejects intermediate-`null` /
71
+ `undefined` traversal with the same diagnostic format as
72
+ `invokeRequest` (`Cannot read property 'X' of null (at 'a.b')`),
73
+ instead of silently returning the partial value. Leaf
74
+ `null` / `undefined` still resolves normally.
75
+ - Endpoint `destroy(notifyRemote=true)` now sends a `releaseCallbacks`
76
+ for every remote callback this endpoint is still holding before
77
+ emitting `destroyEndpoint`. This evicts the peer's persistent-flagged
78
+ methods that paths like `$get('runtime.onMessage')` had pinned on its
79
+ side — fixing a leak in long-lived hosts whose clients open and close.
80
+ - `handleReleaseCallbacks` now fully cleans `functionIds` and
81
+ `persistentRefcount` via the new `ctx.dropLocalCallback(id)`, not just
82
+ the two outer caches.
83
+ - `accessPropertyRequest` / `invokeRequest` no-delegate-target checks
84
+ now use explicit `=== undefined || === null` instead of `!target`, so
85
+ a delegate that is `0` / `false` (e.g. a numeric-rooted custom RPC
86
+ target) is no longer misreported as missing.
87
+ - `isMessageEnvelope` rejects messages whose `senderInstanceId` or
88
+ `type` is the empty string, in addition to the previous non-string
89
+ check.
90
+ - Error payload extras are now restored with own-property definitions
91
+ rather than direct assignment, so fields such as `__proto__` remain data
92
+ and cannot mutate the deserialized Error object's prototype.
93
+
94
+ ### Performance / resource
95
+
96
+ - The `message` listener now does a cheap pre-filter (string check + `'{'`
97
+ start + `"key":<jsonStringifyOfKey>` substring) before `JSON.parse`, so
98
+ unrelated `window.postMessage` traffic from other libraries is rejected
99
+ without parsing. The match string handles JSON-escaped key values, so
100
+ keys containing `"` or `\` route correctly.
101
+ - Well-known symbol reverse lookup is now O(1) via a `Map<symbol, string>`
102
+ built once at module load.
103
+ - The deep payload validators run only after `handler` lookup succeeds; the
104
+ cheap envelope check (type/key/senderInstanceId shape) runs first.
105
+ - `createWindowPoster.addEventListener` is idempotent for the same callback:
106
+ re-registering with the same listener instance unregisters the previous
107
+ DOM listener first, preventing accidental window-listener leaks.
108
+ - `handleReleaseCallbacks` caps the batch at 100 000 ids, warning and
109
+ truncating instead of silently dropping the whole batch on overflow.
110
+ - `endpoint.destroy()` is idempotent — extra calls are no-ops, so an
111
+ accidental double-destroy from cleanup hooks no longer pokes the
112
+ underlying poster twice.
113
+ - `createWindowPoster`'s listener map is keyed by `(name, callback)`
114
+ rather than just `callback`. The same callback registered against
115
+ different event names no longer overwrites each other.
116
+ - `bindMethod`'s WeakMap cache is now owned by each `ClientContext`
117
+ rather than being module-global. Multiple endpoints in the same
118
+ process no longer share bound-method state.
119
+
120
+ ### API
121
+
122
+ - New: `setLogger(logger | null)` and the `Logger` type are exported from
123
+ the package entry. Pass `null` to silence all library warnings, or pass
124
+ your own function to route them through a custom sink (e.g. Sentry).
125
+
126
+ ### Removed (internal type surface)
127
+
128
+ - The following members were removed from interfaces that are re-exported
129
+ for advanced use. They had no internal or external callers; removing them
130
+ is technically a type-level breaking change for anyone who imported and
131
+ asserted against these shapes, but the runtime behavior is unchanged:
132
+ - `ClientContext.registerPendingPromise`
133
+ - `MessageChannel.getContext`, `getPoster`, `getKey`, `getInstanceId`
134
+
135
+ ### Logging
136
+
137
+ - Extracted a unified `warn()` utility (`src/log.ts`) with a `[chrome-in-iframe]` prefix and scope tag for all internal warnings.
138
+ - Replaced all silent `catch` blocks (comment-only / bare `return`) with `warn()` calls so previously swallowed errors are now visible in the console. Affected areas: origin derivation (`deriveOriginFromIframe`, `deriveParentOrigin`, `parseOrigin`), message dispatch (`createMessageChannel`), deserialization (`deserializeError`, `deserializeWrappedObject`, `resolveObjectKey`), and client lifecycle (`notifyReleaseCallbacks`, `destroy`).
139
+ - Migrated existing ad-hoc `console.warn` calls to use the centralized `warn()` utility.
140
+
141
+ ### Package metadata
142
+
143
+ - `exports` map now exposes top-level `types` / `import` / `default`
144
+ conditions and `"./package.json"` for tooling that wants to read the
145
+ manifest. Fixes resolution under stricter bundlers (webpack 5 strict
146
+ exports) without changing the canonical entry.
147
+ - Removed the `clean` and `test:watch` scripts. `build` now runs `rollup -c`
148
+ directly without a preceding `clean` step.
149
+
150
+ ### Tests
151
+
152
+ - New `test/chrome-api.test.ts` with 53 tests covering all 12 Chrome MV3 API
153
+ patterns through the bridge: Promise async methods (various return types),
154
+ synchronous methods proxied as async, Port objects, event listeners
155
+ (addListener/removeListener/hasListener, multi-arg callbacks, sendResponse),
156
+ nested namespaces, optional leading parameters, static properties via `$get`,
157
+ complex data round-trips, CRUD registration, multi-client event isolation,
158
+ error propagation, and timeout behavior.
159
+ - 8 new regression tests in `test/bridge.test.ts` covering each of the
160
+ newly fixed defects: nested-callback release on `removeListener`,
161
+ persistent rollback on `addListener` timeout, non-persistent
162
+ `hasListener` argument, mid-path null/undefined diagnostics through
163
+ `$get`, distinct `(fn, thisArg)` entries for the same function, host
164
+ persistent-cache cleanup on client `destroy()`, idempotent
165
+ `endpoint.destroy()`, and empty-`senderInstanceId` envelope rejection.
166
+
167
+ ## 2.0.0
168
+
169
+ Breaking changes — both peers of the bridge must run the same major version.
170
+
171
+ ### Serialization protocol rewrite
172
+
173
+ - Replaced magic-string prefixes (`$undefined$`, `$function$id`, …) with
174
+ object tags (`{ $cii$: 'undef' }`, `{ $cii$: 'fn', id }`, …). Plain strings
175
+ that previously collided with reserved markers now round-trip correctly.
176
+ - Added serialization support for `Date`, `RegExp`, `Map`, `Set`, `BigInt`,
177
+ `NaN`, `Infinity`, `-Infinity`, and `-0`.
178
+ - Plain objects whose own keys include the reserved `$cii$` marker or contain
179
+ symbol-typed keys are wrapped as entry lists, so they round-trip without
180
+ ambiguity. `Reflect.ownKeys` is used so own symbol keys are preserved.
181
+ Non-global `Symbol(...)` keys are silently skipped (`Symbol.for(...)` and
182
+ well-known symbols still round-trip), so user objects with private brand
183
+ symbols no longer cause serialization to throw.
184
+ - `Error` serialization preserves `name` (so `TypeError` arrives as a
185
+ `TypeError`), `cause` (recursive), `stack`, and additional own enumerable
186
+ string-keyed properties (e.g. custom `code` fields).
187
+ - Tagged payloads are validated; malformed `date` / `regexp` / `bigint` /
188
+ `fn` payloads throw informative `TypeError`s instead of producing
189
+ `Invalid Date` or silently undefined values. `deserializeWrappedObject`
190
+ warns to the console when an entry has an unresolvable key instead of
191
+ silently dropping it.
192
+
193
+ ### Memory and lifecycle
194
+
195
+ - `removeListener(callback)` clears the local persistent function cache and
196
+ asks the remote peer to drop its `remoteCallbacks` entry. Long-running
197
+ add/remove cycles no longer leak callbacks.
198
+ - `destroy()` notifies the remote peer so it can release callbacks it holds
199
+ on behalf of this endpoint.
200
+ - Each endpoint gets a unique `instanceId`; responses are addressed to the
201
+ originating endpoint so multiple clients sharing a key no longer process
202
+ duplicate requests or responses.
203
+ - Remote callback caches are partitioned by remote `instanceId`. When one
204
+ client of a shared-key bridge calls `destroy()`, only the callbacks owned
205
+ by that client are released — peers (other clients) keep working.
206
+ `handleDestroyEndpoint` uses the message envelope's `senderInstanceId`
207
+ rather than wire-level `data.instanceId`, so a peer cannot forge a
208
+ destroy message that targets another endpoint's cache.
209
+ - `handleReleaseCallbacks` caps batches at 10 000 ids to bound worst-case
210
+ work from malformed or hostile messages.
211
+ - Nested callbacks no longer inherit the outer `persistent` flag through
212
+ deserialization; the wire `persistent` field is the sole source of truth.
213
+ - Non-`on[A-Z]` methods returned via `$get` are no longer flagged
214
+ `persistent`, so short-lived methods are LRU-evicted instead of pinned
215
+ forever.
216
+ - `functionIds` is cleaned up when the underlying `functionCache` LRU
217
+ evicts an entry, so long-lived endpoints that register many short-lived
218
+ callbacks no longer accumulate stale `(fn → id)` mappings.
219
+ - `releaseCallbacks` is dispatched synchronously rather than via
220
+ `queueMicrotask`, eliminating a race where a `removeListener` invoke
221
+ request could outrun the corresponding release notification.
222
+ - Response handlers (`invokeResponse`, `accessPropertyResponse`,
223
+ `invokeFunctionByIdResponse`) reject the pending promise if
224
+ deserialization throws (e.g. corrupted wire payload). Previously the
225
+ pending entry was already cleared by the lookup, leaving the promise
226
+ permanently unresolved.
227
+
228
+ ### Method binding
229
+
230
+ - Methods returned through `$get(path)` are bound to their owner before
231
+ serialization, so calling them via the proxy no longer loses `this`.
232
+ - Methods reached through normal `chrome.x.y.method()` invocations are
233
+ applied with `Reflect.apply(fn, owner, args)` on the host side as well.
234
+ - Methods retrieved via `$get` are bound once per `(method, owner)` pair,
235
+ so repeat access reuses the same callback id instead of bloating the
236
+ cache. Non-object owners are not cached (they cannot occur via real
237
+ property paths but would otherwise produce misleading shared bindings).
238
+
239
+ ### Security defaults
240
+
241
+ - `targetOrigin` defaults to the iframe's resolved origin (parsed from
242
+ `iframe.src` or `contentWindow.location.origin`). The iframe side derives
243
+ the parent origin from `document.referrer` / `location.ancestorOrigins`.
244
+ When automatic derivation fails (cross-origin without referrer info), the
245
+ resolver warns and falls back to `'*'`; previously it incorrectly fell
246
+ back to the current window's origin, which would silently drop messages
247
+ bound for a different origin iframe.
248
+
249
+ ### Proxy ergonomics
250
+
251
+ - The client proxy returns `undefined` for common host-introspection paths
252
+ (`then`, `catch`, `finally`, `toJSON`, `toString`, `valueOf`,
253
+ `Symbol.iterator`, `Symbol.isConcatSpreadable`, `Symbol.toStringTag`, …)
254
+ and for `Function.prototype` reflection fields (`length`, `name`,
255
+ `prototype`, `arguments`, `caller`, `bind`, `call`, `apply`). Reflection
256
+ libraries and `Function.prototype.bind.call(proxy, …)` no longer create
257
+ spurious child proxy nodes.
258
+ - `Symbol.toPrimitive` returns a function that produces
259
+ `'[chrome-in-iframe proxy]'`, so `String(proxy)` and template strings no
260
+ longer throw.
261
+ - All proxy nodes share a single no-op target function to avoid per-access
262
+ allocation.
263
+
264
+ ### Configuration
265
+
266
+ - New options on `ConnectionOptions`: `functionCacheMax`, `functionCacheTtl`,
267
+ `remoteCallbackCacheMax`, `remoteCallbackCacheTtl`. Tune them when you
268
+ need callbacks that live longer than the 5-minute LRU default.
269
+
270
+ ### `$get` path semantics
271
+
272
+ - Only the single-string form splits on `.` (e.g. `$get('runtime.id')`).
273
+ Multi-argument calls now treat each argument as a single path segment, so
274
+ keys that contain `.` (e.g. `'flag.v2'`) are addressable via
275
+ `$get('storage', 'local', 'flag.v2')` or the array form
276
+ `$get(['storage', 'local', 'flag.v2'])`.
277
+ - Previously, every string argument was split on `.`, which made dotted
278
+ keys unreachable through `get` / `$get`.
279
+
280
+ ### Module format
281
+
282
+ - The package is **ESM-only**. There is no CommonJS entry point.
283
+ `require('chrome-in-iframe')` will not work — use it from an ESM project
284
+ or configure your bundler to treat it as an ESM external.
285
+
286
+ ### Package metadata
287
+
288
+ - Added `engines`, `sideEffects: false`.
289
+ - `prepublishOnly` runs `typecheck`, `test`, then `build`.
290
+ - `lint` covers both `src` and `test`.
291
+ - Bundled `LICENSE` and `CHANGELOG.md` into the published tarball.
292
+ - `Endpoint.getContext` is marked `@internal` — it is only retained for
293
+ tests and should not be used by application code.
294
+
295
+ ## 1.0.1
296
+
297
+ - Upgrade `nanoid` to `^5.1.11`.
298
+ - Update the example extension ID in the README from
299
+ `chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofhb` to
300
+ `chrome-extension://dieppkgacabfjngoepghlpaapeikfcdc`.
301
+
302
+ ## 1.0.0
303
+
304
+ Initial public release.
@@ -0,0 +1,203 @@
1
+ # 更新日志
2
+
3
+ ## 2.0.1
4
+
5
+ ### Bug 修复
6
+
7
+ - `handleReleaseCallbacks` 现在会同时清理接收方 `remoteCallbacksByOwner` /
8
+ `persistentRemoteCallbacksByOwner` 中对应的条目。此前只清理接收方自身
9
+ 的 `functionCache` / `persistentFunctionCache`,导致每次
10
+ `chrome.*.onX.addListener(handler)` 在宿主端留下的包裹函数永远不会被
11
+ 释放 —— 在长期运行的 service worker 中频繁增删监听器会造成真实泄漏。
12
+ - 调整 `removeListener` 的客户端发送顺序:先发送 `invokeRequest`,再发送
13
+ `releaseCallbacks`。这样接收方可以先用缓存中的包裹函数调用
14
+ `chrome.*.removeListener(W)`,然后再清理缓存。
15
+ - 错误响应处理(`invokeResponse` / `accessPropertyResponse` /
16
+ `invokeFunctionByIdResponse`)不再在远端 payload 没有 `stack` 字段时
17
+ 用 `undefined` 覆盖本地 `Error` 的 stack。
18
+ - 同一个函数被注册为多个事件的监听器(如同时绑定
19
+ `tabs.onActivated.addListener(handler)` 和
20
+ `tabs.onUpdated.addListener(handler)`)时,客户端现在按 callback id
21
+ 做引用计数。移除其中一处不再让另一处失效。
22
+ - `handleInvokeRequest` 在 path 为空时直接返回明确的错误,而不是落到
23
+ `'undefined' is not a function`。
24
+ - 中间路径解到 null / undefined 时的错误信息现在准确报告父值类型,并附
25
+ 上已访问的路径前缀。
26
+ - `isAllowedOrigin` 区分 `undefined`(不限制)与 `''`(空白名单)。空字符串
27
+ 不再被当作"放行所有 origin"。
28
+ - 未传 `allowedOrigin` 时,传入的 `postMessage` 现在会默认按
29
+ `targetOrigin` 推导出的同一个具体 origin 过滤。这样默认桥接配置不会
30
+ 接收来自意外 origin 的消息,同时仍保留自动推导失败时回退到 `'*'` 的
31
+ 既有行为。
32
+ - `invoke` / `accessProperty` / `invokeFunctionById` 在 `serialize` /
33
+ `sendMessage` 同步抛错时会清掉 pending 条目和 timer,不再等到 timeout
34
+ 才回收。
35
+ - 非有限或非正的 `timeout` 值会回落到默认值,避免 `timeout: 0` 导致每
36
+ 次调用瞬间被拒。
37
+ - 监听器注册/移除现在是事务式的。失败的 `addListener` 会回滚本地持久
38
+ callback 状态,并通知对端释放已创建的包裹函数;失败的
39
+ `removeListener` 不再让仍注册在远端的 callback 失效。
40
+ - 远端方法和 callback 返回值现在按 thenable 判断异步,不再依赖
41
+ `instanceof Promise`,因此跨 realm Promise 和自定义 thenable 可以正确
42
+ resolve/reject。
43
+ - `destroy()` 在关闭阶段如果只是因为 iframe 已分离、`contentWindow`
44
+ 不可用而通知失败,不再输出 warning。
45
+ - `addListener` 参数中嵌套对象内的回调函数现在也参与释放计数。此前
46
+ 只有顶层函数参数会被收集到回滚清单中,所以
47
+ `addListener({ filter, handler })` 这种调用即使后续 `removeListener`,
48
+ `handler` 也会一直留在持久缓存里。
49
+ - `invoke` / `accessProperty` / `invokeFunctionById` 的 `setTimeout`
50
+ 分支现在会在 reject 之前触发挂起项的 `onReject`。`addListener`
51
+ 调用超时不再造成持久注册泄漏。
52
+ - `hasListener` 不再被识别为监听器注册路径。其参数此前会被永久标记
53
+ 为 persistent —— 因为 `hasListener` 没有对应的 `removeListener`
54
+ 可以把引用计数减回去。
55
+ - 客户端的函数 id 现在以 `(fn, thisArg)` 为联合键,而不是仅以 `fn`
56
+ 为键。同一函数被复用到不同 owner 上(例如同一方法挂在两个父对象
57
+ 上)不再覆盖前一次注册的 `thisArg`。
58
+ - `accessPropertyRequest` 中间节点解到 null / undefined 时,现在
59
+ 和 `invokeRequest` 一样抛出 `Cannot read property 'X' of null
60
+ (at 'a.b')` 的诊断错误,不再静默返回中间值。叶子节点为
61
+ null / undefined 仍正常返回。
62
+ - 端点 `destroy(notifyRemote=true)` 现在会在发送 `destroyEndpoint`
63
+ 之前,先发送一条 `releaseCallbacks`,列出本端当前持有的所有远端
64
+ callback id。这样对端就可以释放被 `$get('runtime.onMessage')`
65
+ 这类路径标记为 persistent 的方法 —— 修复了长期运行宿主里客户端
66
+ 反复 open/close 时的累积泄漏。
67
+ - `handleReleaseCallbacks` 现在通过新增的 `ctx.dropLocalCallback(id)`
68
+ 彻底清理 `functionIds` 和 `persistentRefcount`,而不是只清理外层
69
+ 两个缓存。
70
+ - `accessPropertyRequest` / `invokeRequest` 的"无 delegate target"
71
+ 判断改为显式 `=== undefined || === null`,不再用 `!target`。这样
72
+ `0` / `false`(例如以数字为根的自定义 RPC target)不会被误报为
73
+ "未配置 delegate target"。
74
+ - `isMessageEnvelope` 在原有的非字符串校验之外,还会拒绝
75
+ `senderInstanceId` / `type` 为空字符串的消息。
76
+ - Error payload 的额外字段现在通过 own-property 定义恢复,而不是直接赋值。
77
+ 因此 `__proto__` 等字段会被保留为普通数据,不会改变反序列化后 Error
78
+ 对象的原型。
79
+
80
+ ### 性能 / 资源
81
+
82
+ - `message` 监听器在 `JSON.parse` 之前先做廉价过滤(字符串检查 + `'{'`
83
+ 起始 + `"key":<JSON.stringify(key)>` 子串)。其它库通过
84
+ `window.postMessage` 发出的无关消息不再被解析。匹配串经过 JSON 转义,
85
+ 含 `"` 或 `\` 的 key 也能正确路由。
86
+ - 知名 Symbol 反向查找改为模块加载时一次性建立的 `Map<symbol, string>`,
87
+ O(1) 替代之前的 O(n)。
88
+ - envelope(type / key / senderInstanceId 形状)的廉价校验先做;深层
89
+ payload 校验延后到 handler 命中之后再执行。
90
+ - `createWindowPoster.addEventListener` 对同一 callback 幂等:再次注册
91
+ 会先卸掉旧的 DOM listener,避免 window-listener 泄漏。
92
+ - `handleReleaseCallbacks` 将单批上限改为 100 000 个 id,超过时截断
93
+ 并输出警告,而不是直接丢弃整批。
94
+ - `endpoint.destroy()` 改为幂等——重复调用直接 no-op,避免清理钩子
95
+ 里不小心的 double-destroy 让底层 poster 被处理两次。
96
+ - `createWindowPoster` 的 listener map 改为以 `(name, callback)` 为
97
+ 联合键,不再仅以 `callback` 为键。同一 callback 注册到不同事件名
98
+ 时不会再相互覆盖。
99
+ - `bindMethod` 的 WeakMap 缓存改由每个 `ClientContext` 各自持有,
100
+ 不再是模块级全局。同一进程中多个端点不再共享绑定方法状态。
101
+
102
+ ### API
103
+
104
+ - 新增:从包入口导出 `setLogger(logger | null)` 和 `Logger` 类型。传
105
+ `null` 可彻底静默库内警告,也可传入自定义函数把日志路由到 Sentry 等
106
+ 外部 sink。
107
+
108
+ ### 移除(内部类型层面)
109
+
110
+ - 以下接口成员被移除。它们在源码和测试中都没有调用方,从未被实际使
111
+ 用;运行时行为完全不变,仅在严格断言这些接口形状的下游代码看来属
112
+ 于类型层面的破坏性变更:
113
+ - `ClientContext.registerPendingPromise`
114
+ - `MessageChannel.getContext` / `getPoster` / `getKey` / `getInstanceId`
115
+
116
+ ### 日志
117
+
118
+ - 提取统一的 `warn()` 工具函数(`src/log.ts`),所有内部警告均带有 `[chrome-in-iframe]` 前缀和 scope 标签。
119
+ - 将所有静默 `catch` 块(仅注释 / 空 `return`)替换为 `warn()` 调用,此前被吞掉的错误现在会在控制台输出。涉及区域:源推导(`deriveOriginFromIframe`、`deriveParentOrigin`、`parseOrigin`)、消息分发(`createMessageChannel`)、反序列化(`deserializeError`、`deserializeWrappedObject`、`resolveObjectKey`)以及客户端生命周期(`notifyReleaseCallbacks`、`destroy`)。
120
+ - 将现有的裸 `console.warn` 调用迁移至集中的 `warn()` 工具。
121
+
122
+ ### 包元数据
123
+
124
+ - `exports` map 新增顶层 `types` / `import` / `default` 条件以及
125
+ `"./package.json"` 子路径,修复 webpack 5 严格 exports 等工具链下的
126
+ 解析问题。规范入口未变。
127
+ - 移除 `clean` 和 `test:watch` 脚本,`build` 直接运行 `rollup -c`,不再前置清理步骤。
128
+
129
+ ### 测试
130
+
131
+ - 新增 `test/chrome-api.test.ts`,包含 53 个测试用例,覆盖全部 12 种 Chrome MV3 API 调用模式的桥接测试:Promise 异步方法(各种返回类型)、同步方法代理、Port 对象、事件监听器(addListener/removeListener/hasListener、多参数回调、sendResponse)、嵌套命名空间、可选前导参数、$get 静态属性、复杂数据往返、CRUD 注册模式、多客户端事件隔离、错误传播、超时行为。
132
+ - 在 `test/bridge.test.ts` 末尾新增 8 个针对上述新修复点的回归测试:`removeListener` 时嵌套回调被释放、`addListener` 超时回滚持久注册、`hasListener` 参数不被标记为 persistent、通过 `$get` 时中间路径 null/undefined 的诊断错误、同一函数不同 owner 产生不同 entry、客户端 `destroy()` 触发宿主端持久缓存清零、`endpoint.destroy()` 幂等、空 `senderInstanceId` 信封被拒收。
133
+
134
+ ## 2.0.0
135
+
136
+ 破坏性变更 — 桥接的两端必须运行相同的主版本号。
137
+
138
+ ### 序列化协议重写
139
+
140
+ - 将魔法字符串前缀(`$undefined$`、`$function$id` 等)替换为对象标签(`{ $cii$: 'undef' }`、`{ $cii$: 'fn', id }` 等)。与保留标记冲突的普通字符串现在可以正确地往返传输。
141
+ - 新增对 `Date`、`RegExp`、`Map`、`Set`、`BigInt`、`NaN`、`Infinity`、`-Infinity` 和 `-0` 的序列化支持。
142
+ - 自有键包含保留标记 `$cii$` 或包含 Symbol 类型键的普通对象会被包装为条目列表,从而无歧义地往返传输。使用 `Reflect.ownKeys` 以保留自有 Symbol 键。非全局的 `Symbol(...)` 键会被静默跳过(`Symbol.for(...)` 和知名 Symbol 仍然可以正常传输),因此带有私有 brand Symbol 的用户对象不再导致序列化抛出异常。
143
+ - `Error` 序列化保留 `name`(因此 `TypeError` 传输后仍为 `TypeError`)、`cause`(递归)、`stack` 以及额外的自有可枚举字符串键属性(如自定义 `code` 字段)。
144
+ - 标签载荷会进行校验;格式错误的 `date` / `regexp` / `bigint` / `fn` 载荷会抛出带有描述信息的 `TypeError`,而不是产生 `Invalid Date` 或静默返回 undefined。当条目包含无法解析的键时,`deserializeWrappedObject` 会向控制台输出警告,而非静默丢弃。
145
+
146
+ ### 内存与生命周期
147
+
148
+ - `removeListener(callback)` 会清除本地持久化函数缓存,并通知远端对等节点丢弃其 `remoteCallbacks` 条目。长时间运行的添加/移除循环不再泄漏回调函数。
149
+ - `destroy()` 会通知远端对等节点,使其可以释放为该端点持有的回调函数。
150
+ - 每个端点获得唯一的 `instanceId`;响应会被发送到发起请求的端点,因此共享同一个 key 的多个客户端不再处理重复的请求或响应。
151
+ - 远端回调缓存按远端 `instanceId` 进行分区。当共享 key 的桥接中某个客户端调用 `destroy()` 时,只有该客户端拥有的回调会被释放 — 其他客户端的对等节点继续正常工作。`handleDestroyEndpoint` 使用消息信封中的 `senderInstanceId` 而非线路层的 `data.instanceId`,因此对等节点无法伪造针对其他端点缓存的销毁消息。
152
+ - `handleReleaseCallbacks` 将批量操作限制为最多 10,000 个 ID,以限制来自格式错误或恶意消息的最大工作量。
153
+ - 嵌套回调不再通过反序列化继承外层的 `persistent` 标志;线路上的 `persistent` 字段是唯一的来源。
154
+ - 通过 `$get` 返回的非 `on[A-Z]` 方法不再被标记为 `persistent`,因此短期方法会被 LRU 淘汰而非永久保留。
155
+ - 当底层 `functionCache` LRU 淘汰条目时,`functionIds` 会被同步清理,因此注册了许多短期回调的长期端点不再积累过期的 `(fn → id)` 映射。
156
+ - `releaseCallbacks` 现在同步派发而非通过 `queueMicrotask`,消除了 `removeListener` 调用请求可能抢先于对应释放通知的竞态条件。
157
+ - 响应处理器(`invokeResponse`、`accessPropertyResponse`、`invokeFunctionByIdResponse`)在反序列化抛出异常时(如损坏的线路载荷)会拒绝挂起的 Promise。此前,挂起条目在查找时已被清除,导致 Promise 永远无法 resolve。
158
+
159
+ ### 方法绑定
160
+
161
+ - 通过 `$get(path)` 返回的方法在序列化前会绑定到其所有者对象,因此通过代理调用它们不再丢失 `this`。
162
+ - 通过正常 `chrome.x.y.method()` 调用链到达的方法在宿主端也会使用 `Reflect.apply(fn, owner, args)` 进行调用。
163
+ - 通过 `$get` 检索的方法按 `(method, owner)` 对绑定一次,因此重复访问会复用相同的回调 ID 而不会使缓存膨胀。非对象类型的所有者不会被缓存(它们不会通过真实的属性路径出现,否则会产生误导性的共享绑定)。
164
+
165
+ ### 安全默认值
166
+
167
+ - `targetOrigin` 默认为 iframe 解析后的源(从 `iframe.src` 或 `contentWindow.location.origin` 解析)。iframe 侧从 `document.referrer` / `location.ancestorOrigins` 推导父级源。当自动推导失败时(跨域且无 referrer 信息),解析器会发出警告并回退到 `'*'`;此前它错误地回退到当前窗口的源,这会静默丢弃发往不同源 iframe 的消息。
168
+
169
+ ### 代理易用性
170
+
171
+ - 客户端代理对常见的宿主内省路径(`then`、`catch`、`finally`、`toJSON`、`toString`、`valueOf`、`Symbol.iterator`、`Symbol.isConcatSpreadable`、`Symbol.toStringTag` 等)以及 `Function.prototype` 反射字段(`length`、`name`、`prototype`、`arguments`、`caller`、`bind`、`call`、`apply`)返回 `undefined`。反射库和 `Function.prototype.bind.call(proxy, …)` 不再创建多余的子代理节点。
172
+ - `Symbol.toPrimitive` 返回一个生成 `'[chrome-in-iframe proxy]'` 的函数,因此 `String(proxy)` 和模板字符串不再抛出异常。
173
+ - 所有代理节点共享一个单一的无操作目标函数,避免每次访问时的内存分配。
174
+
175
+ ### 配置
176
+
177
+ - `ConnectionOptions` 新增选项:`functionCacheMax`、`functionCacheTtl`、`remoteCallbackCacheMax`、`remoteCallbackCacheTtl`。当需要回调存活超过 5 分钟的 LRU 默认值时,可以调整这些参数。
178
+
179
+ ### `$get` 路径语义
180
+
181
+ - 只有单字符串形式才会按 `.` 分割(如 `$get('runtime.id')`)。多参数调用现在将每个参数视为单个路径段,因此包含 `.` 的键(如 `'flag.v2'`)可以通过 `$get('storage', 'local', 'flag.v2')` 或数组形式 `$get(['storage', 'local', 'flag.v2'])` 访问。
182
+ - 此前,每个字符串参数都会按 `.` 分割,导致带点的键无法通过 `get` / `$get` 访问。
183
+
184
+ ### 模块格式
185
+
186
+ - 本包**仅支持 ESM**。没有 CommonJS 入口。`require('chrome-in-iframe')` 将无法工作 — 请在 ESM 项目中使用或配置打包器将其作为 ESM 外部依赖处理。
187
+
188
+ ### 包元数据
189
+
190
+ - 添加了 `engines`、`sideEffects: false`。
191
+ - `prepublishOnly` 依次运行 `typecheck`、`test` 和 `build`。
192
+ - `lint` 同时覆盖 `src` 和 `test` 目录。
193
+ - 将 `LICENSE` 和 `CHANGELOG.md` 打包到发布 tarball 中。
194
+ - `Endpoint.getContext` 标记为 `@internal` — 仅保留用于测试,不应在应用代码中使用。
195
+
196
+ ## 1.0.1
197
+
198
+ - 升级 `nanoid` 至 `^5.1.11`。
199
+ - 将 README 中的示例扩展 ID 从 `chrome-extension://modkelfkcfjpgbfmnbnllalkiogfofhb` 更新为 `chrome-extension://dieppkgacabfjngoepghlpaapeikfcdc`。
200
+
201
+ ## 1.0.0
202
+
203
+ 首次公开发布。
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 GumerLee
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.