跳到主要内容

插件作用域契约迁移

插件拆卸从「插件自己记账 + destroy()」改成「宿主发作用域 + 逆序释放」。本页分两部分:插件作者需要改什么,应用开发者会观察到什么变化。

内置插件(graph / storage / workspace / search / working-tree)已全部迁移,升级即可,无需改调用代码——但请通读下面「行为变化」一节。

插件作者​

1. install() 接收作用域​

-install() {
- this.#channel = new BroadcastChannel('example');
- this.rxdb.addEventListener(ENTITY_LOCAL_CREATE_EVENT, this.#onCreate);
-}
+install(scope: LifecycleScope) {
+ scope.acquire(() => {
+ const channel = new BroadcastChannel('example');
+ this.#channel = channel;
+ return () => {
+ this.#channel = undefined;
+ channel.close();
+ };
+ }, 'example:channel');
+
+ scope.acquire(() => {
+ this.rxdb.addEventListener(ENTITY_LOCAL_CREATE_EVENT, this.#onCreate);
+ return () => this.rxdb.removeEventListener(ENTITY_LOCAL_CREATE_EVENT, this.#onCreate);
+ }, 'example:entity-create');
+}

拆开写不是风格问题:一次 acquire() 只能包一步可能抛错的获取,否则第二步抛错时第一步造出的资源不在清单里,宿主够不着。完整规则见编写插件。

2. 删掉手写的 rollback()​

install() 抛错时宿主会把已登记的部分逆序释放掉,再把原错误传播给 connect()。插件不要在自己的 catch 里做补偿性清理。

3. 声明 lifecycle: 'scoped'​

export class RxDBPluginExample extends RxDBPluginBase implements IRxDBPlugin {
+ readonly lifecycle = 'scoped' as const;
readonly name: Uncapitalize<string> = 'example';

声明后宿主释放完作用域就收手,不再调用 destroy()。

4. 处理 destroy()​

情况做法
清理动作已全部搬进作用域删掉 destroy()
需要同时兼容旧宿主保留 destroy(),实现为 this.#scope?.dispose(),并标 @deprecated
还需要复位插件自身状态(状态机、内部缓存)登记一条只做复位的作用域条目,仍然声明 lifecycle

destroy() 未被移除,只是废弃:未声明 lifecycle 的插件仍会在作用域释放之后被调用一次。

过渡期:按版本与 API 稳定性策略的废弃周期,destroy() 至少保留一个次版本(1.0 后为一个主版本周期),移除在破坏性版本中进行并记入迁移指南。过渡期内它的调用契约不变,第三方插件不必赶在本次升级里改完。

5. 构造函数只留注册期的事​

发布 rxdb.xxx 这类摘不掉的身份属性留在构造函数;IndexedDB store、channel、订阅、监听器一律推迟到 install()。作用域不跨纪元复用,别把它存到实例字段上跨纪元读。

行为变化​

以下都是有意的语义变更,升级后可直接观察到。

插件拆卸从并发改成逆序串行​

disconnectAll() 过去用 Promise.all 并发调用各插件的 destroy(),现在按逆插入序串行拆卸。插件之间存在事实上的依赖(搜索插件的索引建在工作区插件的实体上),并发拆卸会让后装的插件在先装的插件已经拆到一半时还在读它。

影响:拆卸耗时变成各插件之和而非最大值。任一插件抛错只记日志、不短路,disconnectAll() 始终 resolve。

断开连接不再是插件的终态​

rxdb.workspace、rxdb.searchPlugin 这类身份属性在断开连接后依然存在,重新 connect() 会进入新纪元并复用同一个插件实例。

过去需要「重建 RxDB 实例」才能恢复的场景,现在直接重连即可。

在两个纪元之间调用需要纪元资源的方法会拿到明确的错误,而不是永久失效:

await db.disconnectAll();
await db.workspace.flush();
// Error: workspace plugin is not installed in the current connection epoch

await db.searchPlugin.ready;
// SearchError: plugin is destroyed — the connection epoch that installed it was released; await db.connect() again

await db.connect() 之后两者恢复可用。

两个插件的 ready 口径不同,别照着彼此推断:searchPlugin.ready 一个连接纪元一格,connect() 之前与安装期间 pending,成功 resolve、失败 reject 原始错误、纪元释放后 reject destroyed(如上);而 workspace.ready 在未安装时直接 resolve——它只表示「首次 install() 已结算」,不是可用性判据。工作区的可用性由 flush() 这类方法自己抛错表达。

workspace.changes$ 不再在拆卸时 complete​

插件实例跨纪元存活,它的流也就必须比任何一个纪元活得久。断开连接时 changes$ 只是停止发射,不会 complete。

依赖 complete 信号收尾的订阅需要改用 takeUntil 之类的显式终止条件:

-db.workspace.changes$.subscribe({ next: onChange, complete: onTeardown });
+db.workspace.changes$.pipe(takeUntil(this.destroyed$)).subscribe(onChange);

框架绑定(Angular / React / Vue)已经按各自的组件生命周期退订,使用绑定的代码不受影响。

工作区资源获取失败改从 install() 抛出​

IndexedDB 打开失败、BroadcastChannel 创建失败等过去发生在插件构造函数里,也就是 use() 调用点;现在推迟到 install()。错误从 use() 的位置转移到 connect():

const db = new RxDB({ /* … */ });
-try {
- db.use(rxDBPluginWorkspace);
-} catch (err) { /* … */ }
+db.use(rxDBPluginWorkspace);
+try {
+ await db.connect();
+} catch (err) { /* … */ }

跨 tab 消息只在活纪元内投递​

BroadcastChannel 按纪元建立和关闭。断开连接后的草稿增删不会广播给其他 tab,也不会抛错——静默跳过。需要跨 tab 同步就先确保连接是活的。

db.storage 的可用窗口收窄到连接期间​

存储服务过去在 use() 时就建好并一直挂着;现在它是纪元资源:connect() 装上、disconnectAll() 摘掉。

db.use(rxDBPluginStorage);
-await db.storage.list(); // use() 之后即可用
+await db.connect();
+await db.storage.list(); // 连接期间才有

db.storage 的类型仍是非可选的 RxdbFileStorage——它描述的是连接期间的形态,也是唯一该碰它的时候。断开连接后属性会被删掉,此时读到的是 undefined,类型不会提醒你。断连后仍要跑的收尾逻辑(撤销 object URL 之类)要么放在 disconnectAll() 之前,要么自己先判空。

插件实例上的 storage 访问器则如实标成了可选:

-readonly storage: RxdbFileStorage;
+get storage(): RxdbFileStorage | undefined;

直接持有插件实例(而不是走 db.storage)的代码需要补一次判空。

同一个 RxDB 实例上装了两个 storage 插件实例时,后装的那个整个 install() 都是空操作——不再有 #ownsStorage 之类的记账,「谁装谁拆」由作用域保证。

搜索插件不再自己发起 connect()​

@aiao/rxdb-plugin-search 过去在 install() 里自己调 db.connect(localAdapterName) 把连接顶起来。现在它改为声明 inject: ['adapter:local'],由调度器在本地适配器就绪之后再装——插件不再替你连接。

db.use(rxDBPluginSearch);
+await db.connect('sqlite-wasm'); // 现在必须显式连接
await db.search('keyword');

漏掉这一行的表现是:插件停在等待态,db.search() 抛「插件没装」,控制台上有一句

[RxDB] Plugin 'search' is not installed: unsatisfied dependencies [adapter:local]

按路由懒加载的应用要把 connect() 放进进入搜索页的守卫里,别指望搜索插件顺带把连接带起来。

反过来,这条依赖也换掉了旧的死锁绕法:install() 里不再需要 firstValueFrom(adapterConnected$) 之类的等待,被调用即代表依赖已就绪,同步读 db.localAdapterSync 即可。详见编写插件的依赖注入一节。

相关​