Files
offerpai_web/src/utils/channelBridge.ts
T
2026-07-17 20:00:24 +08:00

735 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* OfferPie 跨标签页通信桥接组件
* 用于浏览器插件与网站页面之间的纯本地通信(不依赖任何后端接口)
*
* 【使用说明】
* 1. 引入并创建实例:
* import { createChannelBridge } from './channelBridge'
* const bridge = createChannelBridge({ domain: 'offerpai.com', isPlugin: true })
*
* 2. 发送数据(单次,同时写缓存 + 广播):
* bridge.put<MyDataType>('offerpai.com', 'resumeStatus', { ... })
*
* 3. 高频发送(节流写缓存,每次都广播):
* bridge.emit<MyDataType>('offerpai.com', 'cursorPosition', { ... })
*
* 4. 主动获取一次数据(先广播问对方,60ms超时转IndexedDB):
* const result = await bridge.get<MyDataType>('offerpai.com', 'resumeStatus')
*
* 5. 持续监听数据变化(初始读一次缓存,后续实时接收 put/emit):
* const unwatch = bridge.watch<MyDataType>('offerpai.com', 'resumeStatus', (data) => { ... })
* // 取消监听
* unwatch()
*
* 【跨域模式 useCsl】
* 在 put/emit/get/watch 最后一个参数传 true,数据存储走 chrome.storage.local(通过 Background 中转)
* 适用于网页与插件在不同域名标签页之间的通信场景
* 示例:bridge.put<MyData>('offerpai.com', 'someData', { ... }, true)
* 注意:useCsl 模式依赖插件 Background Service Worker 在线
*
* 6. 销毁实例(页面卸载时调用):
* bridge.destroy()
*
* 【参数说明】
* - domain: 频道域名标识,不同网站项目用不同域名隔离
* - isPlugin: 是否为插件环境,默认 false(非插件时会启用 Web Lock 保活)
*
* 【数据类型要求】
* - 传递的数据必须有 TypeScript 类型定义(泛型约束 extends object
* - 组件自动为每条数据添加 createTime 和 updateTime 字段
*
* ============================================================
* 【通信数据名称注册表】
* 在此区域记录所有通信数据的名称、类型和用途说明
* 新增数据时在此补充,删除时移除对应行
* ------------------------------------------------------------
* | 数据名(name) | 中文名称 | 说明 |
* | ----------------------------- | -------------- | --------------------------------- |
* | offerpieBrowserPlugUsage | 插件使用状态 | 标识插件正在运行 { usage: string, version: string } |
* | offerpaiDeliveryLinkList | 投递链接列表 | 记录要投递职位的来源链接,目前用于解决投递网站的重定向 { linkList: string[] } |
* ============================================================
*/
// ============ 类型定义 ============
/** 组件自动附加的时间戳字段 */
export interface BridgeTimestamp {
/** 数据首次创建时间(格式:yyyy-MM-dd HH:mm:ss */
createTime: string
/** 数据最近更新时间(格式:yyyy-MM-dd HH:mm:ss */
updateTime: string
}
/** 带时间戳的完整数据包装 */
export type BridgeData<T extends object> = T & BridgeTimestamp
/** IndexedDB 存储记录结构 */
interface DBRecord {
/** 数据名称(唯一索引) */
name: string
/** JSON 数据本体 */
data: object
/** 创建时间 */
createTime: string
/** 更新时间 */
updateTime: string
}
/** BroadcastChannel 消息结构 */
interface ChannelMessage {
/** 消息类型 */
type: 'put' | 'emit' | 'get-request' | 'get-response' | 'csl-put-request' | 'csl-put-response' | 'csl-get-request' | 'csl-get-response'
/** 数据名称 */
name: string
/** 请求唯一ID(用于 get 请求-响应匹配) */
id?: string
/** 数据本体(put/emit/get-response 携带) */
data?: object
/** 创建时间 */
createTime?: string
/** 更新时间 */
updateTime?: string
/** 目标域名(csl 中转消息使用) */
domain?: string
}
/** watch 回调函数类型 */
type WatchCallback<T extends object> = (data: BridgeData<T> | null) => void
/** 创建实例的配置参数 */
export interface ChannelBridgeOptions {
/** 默认域名(可在方法调用时覆盖) */
domain: string
/** 是否为插件环境,默认 false。非插件时启用 Web Lock 保活 */
isPlugin?: boolean
}
// ============ 常量 ============
/** IndexedDB 数据库名称(含固定雪花ID保证唯一性) */
const DB_NAME = 'offerpie_comm_7394028156183472'
/** get 方法 BroadcastChannel 超时时间(毫秒) */
const GET_TIMEOUT_MS = 60
/** emit 方法节流写库间隔(毫秒) */
const EMIT_THROTTLE_MS = 500
/** chrome.storage.local 存储总 key */
const CSL_STORAGE_KEY = 'offerpie_comm_7394028156183472'
/** Background 消息类型前缀(用于 useCsl 模式) */
const CSL_MSG_PREFIX = 'OFFERPIE_CSL'
// ============ 工具函数 ============
/** 获取当前时间的 localDateTime 格式字符串:yyyy-MM-dd HH:mm:ss */
function nowLocalDateTime(): string {
const d = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
}
/** 生成简单唯一ID(用于 get 请求匹配) */
function uid(): string {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
}
// ============ chrome.storage.local 操作(useCsl 模式) ============
/** 检测 chrome.runtime.sendMessage 是否可用(插件环境) */
function isChromeRuntimeAvailable(): boolean {
return typeof chrome !== 'undefined' && !!chrome.runtime && !!chrome.runtime.sendMessage
}
/**
* 通过 Background 写入 chrome.storage.local
* 插件环境直接发 chrome.runtime.sendMessage
* 网页环境自动走 BroadcastChannel 中转给同域 Content Script 代理
*/
function cslPut(domain: string, name: string, data: object, createTime: string, updateTime: string): Promise<void> {
return new Promise((resolve, reject) => {
if (isChromeRuntimeAvailable()) {
// 插件环境:直接发给 Background
chrome.runtime.sendMessage(
{ type: `${CSL_MSG_PREFIX}_PUT`, domain, name, data, createTime, updateTime },
(response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message))
} else {
resolve()
}
}
)
} else {
// 网页环境:通过 BroadcastChannel 请求同域 Content Script 代理
const requestId = uid()
const ch = _getCslProxyChannel(domain)
const handler = (event: MessageEvent<ChannelMessage>) => {
if (event.data.type === 'csl-put-response' && event.data.id === requestId) {
resolve()
}
}
ch.addEventListener('message', handler)
ch.postMessage({ type: 'csl-put-request', id: requestId, name, data, createTime, updateTime, domain } as ChannelMessage)
// 超时兜底
setTimeout(() => {
ch.removeEventListener('message', handler)
resolve() // 即使超时也不阻塞
}, 3000)
}
})
}
/**
* 通过 Background 从 chrome.storage.local 读取数据
* 插件环境直接发 chrome.runtime.sendMessage
* 网页环境自动走 BroadcastChannel 中转给同域 Content Script 代理
*/
function cslGet(domain: string, name: string): Promise<DBRecord | null> {
return new Promise((resolve, reject) => {
if (isChromeRuntimeAvailable()) {
// 插件环境:直接发给 Background
chrome.runtime.sendMessage(
{ type: `${CSL_MSG_PREFIX}_GET`, domain, name },
(response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message))
} else {
resolve(response?.record || null)
}
}
)
} else {
// 网页环境:通过 BroadcastChannel 请求同域 Content Script 代理
const requestId = uid()
const ch = _getCslProxyChannel(domain)
const handler = (event: MessageEvent<ChannelMessage>) => {
if (event.data.type === 'csl-get-response' && event.data.id === requestId) {
ch.removeEventListener('message', handler)
const record = event.data.data && event.data.createTime && event.data.updateTime
? { name: event.data.name, data: event.data.data, createTime: event.data.createTime, updateTime: event.data.updateTime }
: null
resolve(record)
}
}
ch.addEventListener('message', handler)
ch.postMessage({ type: 'csl-get-request', id: requestId, name, domain } as ChannelMessage)
// 超时兜底
setTimeout(() => {
ch.removeEventListener('message', handler)
resolve(null)
}, 3000)
}
})
}
/** CSL 代理频道缓存 */
const _cslProxyChannels = new Map<string, BroadcastChannel>()
/** 获取 CSL 代理用的 BroadcastChannel(与主频道分开,避免消息混淆) */
function _getCslProxyChannel(domain: string): BroadcastChannel {
if (_cslProxyChannels.has(domain)) return _cslProxyChannels.get(domain)!
const ch = new BroadcastChannel(`offerpie-bridge-csl-${domain}`)
_cslProxyChannels.set(domain, ch)
return ch
}
// ============ IndexedDB 操作 ============
/** 数据库连接缓存(避免重复打开和版本冲突) */
const _dbCache = new Map<string, Promise<IDBDatabase>>()
/** 打开/创建 IndexedDB 数据库,按 domain 动态创建 ObjectStore,带连接缓存防并发冲突 */
function openDB(storeName: string): Promise<IDBDatabase> {
// 如果已有进行中或已完成的连接,直接复用
if (_dbCache.has(storeName)) {
return _dbCache.get(storeName)!.then(db => {
// 检查连接是否还有效(可能被 close 了)
try {
// 尝试创建事务验证连接有效性
if (db.objectStoreNames.contains(storeName)) {
return db
}
} catch {
// 连接已关闭,清除缓存重新打开
_dbCache.delete(storeName)
}
return openDB(storeName)
})
}
const promise = new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME)
request.onupgradeneeded = () => {
const db = request.result
if (!db.objectStoreNames.contains(storeName)) {
const store = db.createObjectStore(storeName, { keyPath: 'name' })
store.createIndex('name', 'name', { unique: true })
}
}
request.onsuccess = () => {
const db = request.result
if (!db.objectStoreNames.contains(storeName)) {
// store 不存在,需要升版本。关闭当前连接再重开
db.close()
_dbCache.delete(storeName)
const version = db.version + 1
const req2 = indexedDB.open(DB_NAME, version)
req2.onupgradeneeded = () => {
const db2 = req2.result
if (!db2.objectStoreNames.contains(storeName)) {
const store = db2.createObjectStore(storeName, { keyPath: 'name' })
store.createIndex('name', 'name', { unique: true })
}
}
req2.onsuccess = () => resolve(req2.result)
req2.onerror = () => { _dbCache.delete(storeName); reject(req2.error) }
req2.onblocked = () => {
// 其他标签页占用数据库导致升级被阻塞,超时后放弃
console.warn('[ChannelBridge] IndexedDB upgrade blocked, retrying...')
_dbCache.delete(storeName)
reject(new Error('IndexedDB upgrade blocked'))
}
} else {
resolve(db)
}
}
request.onerror = () => { _dbCache.delete(storeName); reject(request.error) }
request.onblocked = () => {
console.warn('[ChannelBridge] IndexedDB open blocked')
_dbCache.delete(storeName)
reject(new Error('IndexedDB open blocked'))
}
})
_dbCache.set(storeName, promise)
return promise
}
/** 写入或更新一条记录到 IndexedDB */
async function dbPut(storeName: string, record: DBRecord): Promise<void> {
try {
const db = await openDB(storeName)
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite')
const store = tx.objectStore(storeName)
store.put(record)
tx.oncomplete = () => resolve()
tx.onerror = () => reject(tx.error)
})
} catch (err) {
console.warn('[ChannelBridge] dbPut failed:', err)
}
}
/** 从 IndexedDB 读取一条记录 */
async function dbGet(storeName: string, name: string): Promise<DBRecord | null> {
try {
const db = await openDB(storeName)
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readonly')
const store = tx.objectStore(storeName)
const req = store.get(name)
req.onsuccess = () => resolve(req.result || null)
req.onerror = () => reject(req.error)
})
} catch (err) {
console.warn('[ChannelBridge] dbGet failed:', err)
return null
}
}
// ============ 核心:创建通信桥接实例 ============
export function createChannelBridge(options: ChannelBridgeOptions) {
const { domain, isPlugin = false } = options
/** BroadcastChannel 实例缓存(按 domain 隔离频道) */
const channels = new Map<string, BroadcastChannel>()
/** watch 订阅回调注册表:key = `${domain}::${name}` */
const watchers = new Map<string, Set<WatchCallback<any>>>()
/** emit 节流定时器:key = `${domain}::${name}` */
const emitTimers = new Map<string, ReturnType<typeof setTimeout>>()
/** emit 节流暂存最新数据:key = `${domain}::${name}` */
const emitPending = new Map<string, DBRecord>()
/** get 请求等待队列:key = requestId */
const getResolvers = new Map<string, (data: BridgeData<any> | null) => void>()
// --- Web Lock 保活(非插件环境) ---
if (!isPlugin && typeof navigator !== 'undefined' && navigator.locks) {
navigator.locks.request('offerpie-keep-alive', () => new Promise(() => {}))
}
/** 获取或创建指定 domain 的 BroadcastChannel */
function getChannel(channelDomain: string): BroadcastChannel {
if (channels.has(channelDomain)) return channels.get(channelDomain)!
const ch = new BroadcastChannel(`offerpie-bridge-${channelDomain}`)
ch.onmessage = (event: MessageEvent<ChannelMessage>) => handleMessage(channelDomain, event.data)
channels.set(channelDomain, ch)
return ch
}
/** 处理收到的 BroadcastChannel 消息 */
async function handleMessage(channelDomain: string, msg: ChannelMessage) {
const watchKey = `${channelDomain}::${msg.name}`
if (msg.type === 'put' || msg.type === 'emit') {
// 收到对方的数据推送,触发本地 watch 回调
const wrapped = msg.data && msg.createTime && msg.updateTime
? { ...msg.data, createTime: msg.createTime, updateTime: msg.updateTime } as BridgeData<any>
: null
const callbacks = watchers.get(watchKey)
if (callbacks) {
callbacks.forEach(cb => cb(wrapped))
}
}
if (msg.type === 'get-request' && msg.id) {
// 收到对方的 get 请求,从本地 IndexedDB 读取数据并回复
const record = await dbGet(channelDomain, msg.name)
const ch = getChannel(channelDomain)
const response: ChannelMessage = {
type: 'get-response',
name: msg.name,
id: msg.id,
data: record?.data,
createTime: record?.createTime,
updateTime: record?.updateTime,
}
ch.postMessage(response)
}
if (msg.type === 'get-response' && msg.id) {
// 收到对方对 get 请求的回复
const resolver = getResolvers.get(msg.id)
if (resolver) {
getResolvers.delete(msg.id)
if (msg.data && msg.createTime && msg.updateTime) {
resolver({ ...msg.data, createTime: msg.createTime, updateTime: msg.updateTime } as BridgeData<any>)
} else {
resolver(null)
}
}
}
}
// 初始化默认频道监听
getChannel(domain)
// --- CSL 代理监听(插件环境下,监听网页端的 CSL 中转请求) ---
if (isPlugin && isChromeRuntimeAvailable()) {
const cslProxyCh = new BroadcastChannel(`offerpie-bridge-csl-${domain}`)
cslProxyCh.onmessage = async (event: MessageEvent<ChannelMessage>) => {
const msg = event.data
if (msg.type === 'csl-put-request' && msg.id) {
// 代理写入 chrome.storage.local
const targetDomain = msg.domain || domain
await new Promise<void>((resolve) => {
chrome.runtime.sendMessage(
{ type: `${CSL_MSG_PREFIX}_PUT`, domain: targetDomain, name: msg.name, data: msg.data, createTime: msg.createTime, updateTime: msg.updateTime },
() => resolve()
)
})
cslProxyCh.postMessage({ type: 'csl-put-response', id: msg.id, name: msg.name } as ChannelMessage)
}
if (msg.type === 'csl-get-request' && msg.id) {
// 代理读取 chrome.storage.local
const targetDomain = msg.domain || domain
const response = await new Promise<any>((resolve) => {
chrome.runtime.sendMessage(
{ type: `${CSL_MSG_PREFIX}_GET`, domain: targetDomain, name: msg.name },
(res) => resolve(res)
)
})
const record = response?.record
cslProxyCh.postMessage({
type: 'csl-get-response',
id: msg.id,
name: msg.name,
data: record?.data,
createTime: record?.createTime,
updateTime: record?.updateTime,
} as ChannelMessage)
}
}
}
// --- 公开方法 ---
/**
* put - 单次发送数据(写入 IndexedDB + BroadcastChannel 广播)
* @param targetDomain 目标域名频道
* @param name 数据名称
* @param data 数据本体(需有 TS 类型定义)
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
*/
async function put<T extends object>(targetDomain: string, name: string, data: T, useCsl?: boolean): Promise<void> {
const now = nowLocalDateTime()
if (useCsl) {
// 跨域模式:通过 Background 读写 chrome.storage.local
const existing = await cslGet(targetDomain, name).catch(() => null)
const createTime = existing?.createTime || now
const updateTime = now
await cslPut(targetDomain, name, data, createTime, updateTime)
// 同时广播(同域标签页也能收到)
const ch = getChannel(targetDomain)
const msg: ChannelMessage = { type: 'put', name, data, createTime, updateTime }
ch.postMessage(msg)
return
}
// 默认模式:IndexedDB
const existing = await dbGet(targetDomain, name)
const createTime = existing?.createTime || now
const updateTime = now
const record: DBRecord = { name, data, createTime, updateTime }
// 写入 IndexedDB
await dbPut(targetDomain, record)
// BroadcastChannel 广播
const ch = getChannel(targetDomain)
const msg: ChannelMessage = { type: 'put', name, data, createTime, updateTime }
ch.postMessage(msg)
}
/**
* emit - 高频发送数据(每次都广播,节流 500ms 写一次 IndexedDB
* @param targetDomain 目标域名频道
* @param name 数据名称
* @param data 数据本体(需有 TS 类型定义)
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
*/
async function emit<T extends object>(targetDomain: string, name: string, data: T, useCsl?: boolean): Promise<void> {
const now = nowLocalDateTime()
const key = `${targetDomain}::${name}`
if (useCsl) {
// 跨域模式:每次广播,节流写 chrome.storage.local
let createTime: string
const pending = emitPending.get(key)
if (pending) {
createTime = pending.createTime
} else {
const existing = await cslGet(targetDomain, name).catch(() => null)
createTime = existing?.createTime || now
}
const updateTime = now
const record: DBRecord = { name, data, createTime, updateTime }
emitPending.set(key, record)
// BroadcastChannel 每次都广播
const ch = getChannel(targetDomain)
const msg: ChannelMessage = { type: 'emit', name, data, createTime, updateTime }
ch.postMessage(msg)
// 节流写 chrome.storage.local
if (!emitTimers.has(key)) {
emitTimers.set(key, setTimeout(async () => {
emitTimers.delete(key)
const latestRecord = emitPending.get(key)
if (latestRecord) {
emitPending.delete(key)
await cslPut(targetDomain, latestRecord.name, latestRecord.data, latestRecord.createTime, latestRecord.updateTime).catch(() => {})
}
}, EMIT_THROTTLE_MS))
}
return
}
// 默认模式:IndexedDB
// 先尝试获取 createTime(从暂存或内存缓存)
let createTime: string
const pending = emitPending.get(key)
if (pending) {
createTime = pending.createTime
} else {
// 只在首次查库获取 createTime,失败就用当前时间(不阻塞后续 emit)
let existing: DBRecord | null = null
try {
existing = await dbGet(targetDomain, name)
} catch {}
createTime = existing?.createTime || now
}
const updateTime = now
const record: DBRecord = { name, data, createTime, updateTime }
// 暂存最新数据(不再 delete,保证后续 emit 总能命中缓存不走 dbGet
emitPending.set(key, record)
// BroadcastChannel 每次都广播(保证 watch 实时性)
const ch = getChannel(targetDomain)
const msg: ChannelMessage = { type: 'emit', name, data, createTime, updateTime }
ch.postMessage(msg)
// 节流写库
if (!emitTimers.has(key)) {
emitTimers.set(key, setTimeout(async () => {
emitTimers.delete(key)
const latestRecord = emitPending.get(key)
if (latestRecord) {
// 注意:不再 delete emitPending,让下次 emit 继续命中缓存
await dbPut(targetDomain, latestRecord)
}
}, EMIT_THROTTLE_MS))
}
}
/**
* get - 主动获取一次数据(先 BroadcastChannel 请求,60ms 超时转 IndexedDB
* @param targetDomain 目标域名频道
* @param name 数据名称
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
* @returns 数据本体(含 createTime/updateTime),无数据返回 null
*/
async function get<T extends object>(targetDomain: string, name: string, useCsl?: boolean): Promise<BridgeData<T> | null> {
if (useCsl) {
// 跨域模式:先广播请求,超时读 chrome.storage.local
const ch = getChannel(targetDomain)
const requestId = uid()
const msg: ChannelMessage = { type: 'get-request', name, id: requestId }
ch.postMessage(msg)
const result = await new Promise<BridgeData<T> | null>((resolve) => {
getResolvers.set(requestId, resolve as any)
setTimeout(() => {
if (getResolvers.has(requestId)) {
getResolvers.delete(requestId)
resolve(null)
}
}, GET_TIMEOUT_MS)
})
if (result) return result
// 超时降级读 chrome.storage.local
const record = await cslGet(targetDomain, name).catch(() => null)
if (record) {
return { ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>
}
return null
}
// 默认模式:IndexedDB
const ch = getChannel(targetDomain)
const requestId = uid()
// 发起广播请求
const msg: ChannelMessage = { type: 'get-request', name, id: requestId }
ch.postMessage(msg)
// 等待响应或超时
const result = await new Promise<BridgeData<T> | null>((resolve) => {
getResolvers.set(requestId, resolve as any)
setTimeout(() => {
if (getResolvers.has(requestId)) {
getResolvers.delete(requestId)
resolve(null) // 超时,标记为未收到响应
}
}, GET_TIMEOUT_MS)
})
// 如果广播拿到了数据直接返回
if (result) return result
// 超时降级读 IndexedDB
const record = await dbGet(targetDomain, name)
if (record) {
return { ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>
}
return null
}
/**
* watch - 持续监听数据变化(初始读一次缓存,后续实时接收 put/emit 推送)
* @param targetDomain 目标域名频道
* @param name 数据名称
* @param callback 数据变化回调,参数为最新数据或 null
* @param useCsl 是否使用 chrome.storage.local 跨域模式(通过 Background 中转)
* @returns 取消监听的函数
*/
function watch<T extends object>(targetDomain: string, name: string, callback: WatchCallback<T>, useCsl?: boolean): () => void {
const key = `${targetDomain}::${name}`
// 确保频道已初始化
getChannel(targetDomain)
// 注册回调(BroadcastChannel 广播也能触发,不管 useCsl 与否)
if (!watchers.has(key)) watchers.set(key, new Set())
watchers.get(key)!.add(callback)
if (useCsl) {
// 跨域模式:初始读 chrome.storage.local
cslGet(targetDomain, name).then((record) => {
if (record) {
callback({ ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>)
} else {
callback(null)
}
}).catch(() => callback(null))
// 监听 chrome.storage.onChanged 事件
const storageListener = (changes: { [key: string]: chrome.storage.StorageChange }, areaName: string) => {
if (areaName !== 'local' || !changes[CSL_STORAGE_KEY]) return
const newValue = changes[CSL_STORAGE_KEY].newValue as Array<{ domain: string; value: DBRecord }> | undefined
if (!newValue) return
const item = newValue.find(i => i.domain === targetDomain && i.value?.name === name)
if (item && item.value) {
callback({ ...item.value.data, createTime: item.value.createTime, updateTime: item.value.updateTime } as BridgeData<T>)
}
}
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.onChanged) {
chrome.storage.onChanged.addListener(storageListener)
}
// 返回取消监听函数
return () => {
const set = watchers.get(key)
if (set) {
set.delete(callback)
if (set.size === 0) watchers.delete(key)
}
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.onChanged) {
chrome.storage.onChanged.removeListener(storageListener)
}
}
}
// 默认模式:初始读取 IndexedDB
dbGet(targetDomain, name).then((record) => {
if (record) {
callback({ ...record.data, createTime: record.createTime, updateTime: record.updateTime } as BridgeData<T>)
} else {
callback(null)
}
})
// 返回取消监听函数
return () => {
const set = watchers.get(key)
if (set) {
set.delete(callback)
if (set.size === 0) watchers.delete(key)
}
}
}
/**
* destroy - 销毁实例,关闭所有频道,清除定时器
*/
function destroy(): void {
channels.forEach(ch => ch.close())
channels.clear()
watchers.clear()
emitTimers.forEach(timer => clearTimeout(timer))
emitTimers.clear()
emitPending.clear()
getResolvers.clear()
}
return { put, emit, get, watch, destroy }
}