@aiao/rxdb-adapter-electron 0.0.26 → 0.0.28

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.
Files changed (59) hide show
  1. package/README.md +122 -21
  2. package/dist/RxDBAdapterElectron.d.ts +27 -1
  3. package/dist/RxDBAdapterElectron.d.ts.map +1 -1
  4. package/dist/electron-sqlite-host.d.ts.map +1 -1
  5. package/dist/host.d.ts +1 -1
  6. package/dist/host.d.ts.map +1 -1
  7. package/dist/host.js +46 -40
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +20 -6
  11. package/dist/node-sqlite-engine.d.ts +10 -0
  12. package/dist/node-sqlite-engine.d.ts.map +1 -1
  13. package/dist/pglite/RxDBAdapterElectronPGlite.d.ts +53 -1
  14. package/dist/pglite/RxDBAdapterElectronPGlite.d.ts.map +1 -1
  15. package/dist/pglite/desktop-pglite-client.d.ts +27 -1
  16. package/dist/pglite/desktop-pglite-client.d.ts.map +1 -1
  17. package/dist/pglite/desktop-pglite-results.d.ts +14 -0
  18. package/dist/pglite/desktop-pglite-results.d.ts.map +1 -0
  19. package/dist/pglite/electron-pglite-backup-error.d.ts +32 -0
  20. package/dist/pglite/electron-pglite-backup-error.d.ts.map +1 -0
  21. package/dist/pglite/pglite-adapter.interface.d.ts +12 -0
  22. package/dist/pglite/pglite-adapter.interface.d.ts.map +1 -1
  23. package/dist/pglite/restore-electron-pglite-database.d.ts +68 -0
  24. package/dist/pglite/restore-electron-pglite-database.d.ts.map +1 -0
  25. package/dist/pglite-host/electron-pglite-host.d.ts +47 -32
  26. package/dist/pglite-host/electron-pglite-host.d.ts.map +1 -1
  27. package/dist/pglite-host/pglite-host-data-dir.d.ts +75 -0
  28. package/dist/pglite-host/pglite-host-data-dir.d.ts.map +1 -0
  29. package/dist/pglite-host/pglite-host-lock.d.ts +48 -0
  30. package/dist/pglite-host/pglite-host-lock.d.ts.map +1 -0
  31. package/dist/pglite-host/pglite-host-restore.d.ts +38 -0
  32. package/dist/pglite-host/pglite-host-restore.d.ts.map +1 -0
  33. package/dist/pglite-host/pglite-host-runtime.d.ts +90 -0
  34. package/dist/pglite-host/pglite-host-runtime.d.ts.map +1 -0
  35. package/dist/pglite-host.d.ts +2 -2
  36. package/dist/pglite-host.d.ts.map +1 -1
  37. package/dist/pglite-host.js +643 -120
  38. package/dist/pglite.d.ts +2 -0
  39. package/dist/pglite.d.ts.map +1 -1
  40. package/dist/pglite.js +341 -65
  41. package/package.json +13 -10
  42. package/src/RxDBAdapterElectron.ts +40 -1
  43. package/src/electron-sqlite-host.ts +3 -0
  44. package/src/host.ts +1 -0
  45. package/src/index.ts +1 -0
  46. package/src/node-sqlite-engine.ts +20 -1
  47. package/src/pglite/RxDBAdapterElectronPGlite.ts +80 -5
  48. package/src/pglite/desktop-pglite-client.ts +111 -17
  49. package/src/pglite/desktop-pglite-results.ts +24 -0
  50. package/src/pglite/electron-pglite-backup-error.ts +128 -0
  51. package/src/pglite/pglite-adapter.interface.ts +14 -0
  52. package/src/pglite/restore-electron-pglite-database.ts +429 -0
  53. package/src/pglite-host/electron-pglite-host.ts +409 -160
  54. package/src/pglite-host/pglite-host-data-dir.ts +224 -0
  55. package/src/pglite-host/pglite-host-lock.ts +154 -0
  56. package/src/pglite-host/pglite-host-restore.ts +460 -0
  57. package/src/pglite-host/pglite-host-runtime.ts +166 -0
  58. package/src/pglite-host.ts +7 -0
  59. package/src/pglite.ts +9 -0
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Electron PGlite 的恢复入口(renderer 侧,US-217 阶段 C)。
3
+ *
4
+ * @module pglite/restore-electron-pglite-database
5
+ */
6
+
7
+ import {
8
+ assertRxDBBackupCompatible,
9
+ getRxDBBackupAuthDomain,
10
+ getRxDBBackupSchemaFingerprint,
11
+ RXDB_CHANGE_CODEC_VERSION,
12
+ RXDB_SYSTEM_SCHEMA_VERSION,
13
+ RxDBBackupArchiveReader,
14
+ RxDBBackupError,
15
+ type RxDB,
16
+ type RxDBBackupCompatibility,
17
+ type RxDBBackupEntryHeader,
18
+ type RxDBBackupManifest,
19
+ type RxDBBackupTrailer,
20
+ type RxDBRestoreResult
21
+ } from '@aiao/rxdb';
22
+ import {
23
+ PGLITE_BACKUP_ENGINE,
24
+ toPGliteEngineInfo,
25
+ verifyPGliteRestored,
26
+ type PGliteQueryable,
27
+ type PGliteRestoreOptions
28
+ } from '@aiao/rxdb-adapter-pglite';
29
+ import {
30
+ assertDesktopPgliteResponse,
31
+ assertValidDesktopDatabaseName,
32
+ parseDesktopPgliteBackupItem,
33
+ parseDesktopPgliteEngineResult,
34
+ parseDesktopPgliteHandshakeResult,
35
+ resolveDesktopHostTransport,
36
+ RxDBAdapterDesktopError,
37
+ type DesktopHostTransport,
38
+ type DesktopPgliteDataDirItem,
39
+ type DesktopPgliteEngineResult,
40
+ type DesktopPgliteParam,
41
+ type DesktopPgliteQueryResult,
42
+ type DesktopPgliteRestoreStepRequest
43
+ } from '@aiao/rxdb-adapter-sqlite-core/desktop-host';
44
+ import { toDesktopPGliteResults } from './desktop-pglite-results.js';
45
+ import { toElectronPGliteBackupError } from './electron-pglite-backup-error.js';
46
+ import { ADAPTER_NAME, resolveDataDirectoryName, type ElectronPGliteOptions } from './pglite-adapter.interface.js';
47
+
48
+ /**
49
+ * 恢复目标:一个**尚未连接**的 RxDB 实例,加上它将来连接时交给
50
+ * {@link RxDBAdapterElectronPGlite} 的同一份选项。
51
+ *
52
+ * @remarks
53
+ * 数据目录名、传输层、实体结构指纹与加密认证域都按这份配置判定;与之后连接时的选项不一致,
54
+ * 恢复出来的库就会落在另一个目录,或者被判成不兼容。
55
+ */
56
+ export interface ElectronPGliteRestoreTarget {
57
+ readonly rxdb: RxDB;
58
+ readonly options: ElectronPGliteOptions;
59
+ }
60
+
61
+ type RestoreStepKind = Exclude<DesktopPgliteRestoreStepRequest['kind'], 'pg.restore.abort'>;
62
+
63
+ const aborted = (signal: AbortSignal): RxDBBackupError =>
64
+ new RxDBBackupError('aborted', 'Desktop PGlite restore was aborted', { cause: signal.reason });
65
+
66
+ const throwIfAborted = (signal: AbortSignal | undefined): void => {
67
+ if (signal?.aborted) throw aborted(signal);
68
+ };
69
+
70
+ /**
71
+ * 目标实例本身必须还没连接:已连接的实例有自己的库,恢复结果既不能替换它、也不能被它领取。
72
+ *
73
+ * @remarks
74
+ * `connected$` 是 BehaviorSubject 派生流,订阅时同步给出当前值;本包不依赖 rxjs,直接订阅一次再退订。
75
+ */
76
+ const assertTargetDisconnected = (rxdb: RxDB): void => {
77
+ let connected = false;
78
+ rxdb.connected$
79
+ .subscribe(value => {
80
+ connected = value;
81
+ })
82
+ .unsubscribe();
83
+ if (!connected) return;
84
+ throw new RxDBBackupError('target_busy', `RxDB "${rxdb.config.dbName}" is already connected`, {
85
+ details: { field: 'rxdb', actual: rxdb.config.dbName }
86
+ });
87
+ };
88
+
89
+ /** 协议版本不一致时 host 与 renderer 对恢复步骤的理解不同,必须在发出任何恢复请求之前拒绝(AC#21)。 */
90
+ const handshake = async (transport: DesktopHostTransport): Promise<void> => {
91
+ const response = assertDesktopPgliteResponse('pg.handshake', await transport.request({ kind: 'pg.handshake' }));
92
+ parseDesktopPgliteHandshakeResult(response.result);
93
+ };
94
+
95
+ const engineOf = async (transport: DesktopHostTransport): Promise<DesktopPgliteEngineResult> => {
96
+ const response = assertDesktopPgliteResponse('pg.engine', await transport.request({ kind: 'pg.engine' }));
97
+ return parseDesktopPgliteEngineResult(response.result);
98
+ };
99
+
100
+ /**
101
+ * 目标侧的兼容性期望。
102
+ *
103
+ * @remarks
104
+ * 目标库此时还不存在,引擎版本与扩展只能问 host:它回答的是将来打开这个目录的那个运行时。
105
+ * 目标实例会被 `init()`(计算实体集合),但不会连接。
106
+ */
107
+ const targetCompatibility = (rxdb: RxDB, engine: DesktopPgliteEngineResult): RxDBBackupCompatibility => {
108
+ rxdb.init();
109
+ return {
110
+ adapterName: ADAPTER_NAME,
111
+ engine: PGLITE_BACKUP_ENGINE,
112
+ engineCompatibility: toPGliteEngineInfo(engine.serverVersion).compatibility,
113
+ extensions: engine.extensions,
114
+ systemSchemaVersion: RXDB_SYSTEM_SCHEMA_VERSION,
115
+ changeCodecVersion: RXDB_CHANGE_CODEC_VERSION,
116
+ schemaFingerprint: getRxDBBackupSchemaFingerprint(rxdb),
117
+ authDomain: getRxDBBackupAuthDomain(rxdb)
118
+ };
119
+ };
120
+
121
+ /** 一次已取得独占权的 host 侧恢复。 */
122
+ interface HostRestore {
123
+ step(kind: RestoreStepKind): Promise<void>;
124
+ write(item: DesktopPgliteDataDirItem): Promise<void>;
125
+ query(sql: string, params: readonly DesktopPgliteParam[]): Promise<DesktopPgliteQueryResult>;
126
+ abort(): Promise<void>;
127
+ }
128
+
129
+ /**
130
+ * 取得目标目录的独占权并检查它是否可以恢复。
131
+ *
132
+ * @remarks
133
+ * host **先**取目录锁、**再**查未完成标记与空状态,独占一直保持到 `commit` 或 `abort`:
134
+ * 目录正被打开或正在恢复报 `database_busy`,有未完成标记报 `restore_incomplete`,
135
+ * 已有数据库(哪怕只是 initdb 的产物)报 `target_not_empty`。
136
+ */
137
+ const beginHostRestore = async (transport: DesktopHostTransport, dataDirectoryName: string): Promise<HostRestore> => {
138
+ const begun = assertDesktopPgliteResponse(
139
+ 'pg.restore.begin',
140
+ await transport.request({ kind: 'pg.restore.begin', storage: { engine: 'pglite', dataDirectoryName } })
141
+ );
142
+ const { restoreId } = begun.result;
143
+ return {
144
+ step: async kind => {
145
+ assertDesktopPgliteResponse(kind, await transport.request({ kind, restoreId }));
146
+ },
147
+ write: async item => {
148
+ assertDesktopPgliteResponse(
149
+ 'pg.restore.write',
150
+ await transport.request({ kind: 'pg.restore.write', restoreId, item })
151
+ );
152
+ },
153
+ query: async (sql, params) => {
154
+ const response = assertDesktopPgliteResponse(
155
+ 'pg.restore.query',
156
+ await transport.request({ kind: 'pg.restore.query', restoreId, sql, params })
157
+ );
158
+ return response.result;
159
+ },
160
+ abort: async () => {
161
+ assertDesktopPgliteResponse('pg.restore.abort', await transport.request({ kind: 'pg.restore.abort', restoreId }));
162
+ }
163
+ };
164
+ };
165
+
166
+ /**
167
+ * 归档条目头转成 host 写入项。
168
+ *
169
+ * @remarks
170
+ * 归档读取器只保证路径是安全的 POSIX 相对路径;host 还要求每一段在 Windows 上也合法、且不是运行态
171
+ * 文件。由本 adapter 备份出来的归档必然满足(备份方向走同一套校验),不满足只能是归档被改过,
172
+ * 所以在发给 host 之前按损坏拒绝,而不是让 host 以协议错误拒绝后被误判成「组合不支持」。
173
+ */
174
+ const toHostEntry = (header: RxDBBackupEntryHeader): DesktopPgliteDataDirItem => {
175
+ const item: DesktopPgliteDataDirItem = {
176
+ type: 'entry',
177
+ header: { path: header.path, kind: header.kind, size: header.size }
178
+ };
179
+ try {
180
+ parseDesktopPgliteBackupItem(item);
181
+ } catch (error) {
182
+ throw new RxDBBackupError(
183
+ 'corrupt_archive',
184
+ `Archive entry ${JSON.stringify(header.path)} cannot be restored into a desktop data directory`,
185
+ { details: { field: 'path', actual: header.path }, cause: error }
186
+ );
187
+ }
188
+ return item;
189
+ };
190
+
191
+ /**
192
+ * 数据块必须独占自己的 `ArrayBuffer`。
193
+ *
194
+ * @remarks
195
+ * 结构化克隆按整个底层 buffer 复制:一个视图过 IPC,会把它背后的整块内存一起带过去,
196
+ * host 也会以超限拒绝。读取器产出的帧本来就独占 buffer,这里只在例外时复制一次。
197
+ */
198
+ const ownedChunk = (bytes: Uint8Array): Uint8Array<ArrayBuffer> =>
199
+ bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength ?
200
+ (bytes as Uint8Array<ArrayBuffer>)
201
+ : bytes.slice();
202
+
203
+ /**
204
+ * 把归档逐项写进 host 的暂存目录。
205
+ *
206
+ * @remarks
207
+ * 每一项都等 host 确认写入之后才读下一项:读取节奏跟着 host 的磁盘走,内存里最多停留一个数据帧,
208
+ * 与归档大小无关(AC#9、AC#21)。
209
+ */
210
+ const writeArchive = async (archive: RxDBBackupArchiveReader, host: HostRestore): Promise<RxDBBackupTrailer> => {
211
+ let item = await archive.next();
212
+ while (item.type !== 'end') {
213
+ await host.write(
214
+ item.type === 'entry' ? toHostEntry(item.header) : { type: 'data', bytes: ownedChunk(item.bytes) }
215
+ );
216
+ item = await archive.next();
217
+ }
218
+ return item.trailer;
219
+ };
220
+
221
+ /**
222
+ * 在 host 的私有实例上核对引擎与系统表水位。
223
+ *
224
+ * @remarks
225
+ * 校验语句在一个摘要正确的库上执行失败(比如缺系统表),说明归档里装的不是 manifest 声明的那个库,
226
+ * 与浏览器端「PostgreSQL 起不来」同样归为损坏。
227
+ */
228
+ const verifyOnHost = async (host: HostRestore, manifest: RxDBBackupManifest): Promise<void> => {
229
+ const db: PGliteQueryable = {
230
+ query: async <T>(sql: string, params: unknown[] = []) =>
231
+ toDesktopPGliteResults<T>(await host.query(sql, params as readonly DesktopPgliteParam[]))
232
+ };
233
+ try {
234
+ await verifyPGliteRestored(db, manifest);
235
+ } catch (error) {
236
+ if (!(error instanceof RxDBAdapterDesktopError && error.code === 'statement_failed')) throw error;
237
+ throw new RxDBBackupError('corrupt_archive', 'Restored database cannot answer the verification queries', {
238
+ cause: error
239
+ });
240
+ }
241
+ };
242
+
243
+ /** 恢复运行时:独占已取得,归档读到了 manifest 并判定兼容。 */
244
+ interface RestoreSession {
245
+ readonly host: HostRestore;
246
+ readonly archive: RxDBBackupArchiveReader;
247
+ readonly manifest: RxDBBackupManifest;
248
+ readonly options: PGliteRestoreOptions;
249
+ }
250
+
251
+ /**
252
+ * 从未完成标记落盘到提交。
253
+ *
254
+ * @remarks
255
+ * 顺序与浏览器端的 IndexedDB 目标一致:标记 → 写入 → 启动 → 校验 → 落盘 → 删标记。标记之后到
256
+ * 删标记之前任何一刻进程被杀,下一次打开都会以 `restore_incomplete` 拒绝,而不是把半截库当成正常库。
257
+ */
258
+ const runRestore = async (session: RestoreSession): Promise<RxDBBackupTrailer> => {
259
+ const { host, archive, options } = session;
260
+ await host.step('pg.restore.prepare');
261
+ await options.onStage?.('marker-written');
262
+ throwIfAborted(options.signal);
263
+ const trailer = await writeArchive(archive, host);
264
+ await host.step('pg.restore.open');
265
+ await options.onStage?.('files-written');
266
+ throwIfAborted(options.signal);
267
+ await verifyOnHost(host, session.manifest);
268
+ await options.onStage?.('verified');
269
+ throwIfAborted(options.signal);
270
+ await host.step('pg.restore.persist');
271
+ await options.onStage?.('persisted');
272
+ await host.step('pg.restore.commit');
273
+ return trailer;
274
+ };
275
+
276
+ /**
277
+ * 失败后让 host 把目标退回「从未恢复过」,并交还独占。
278
+ *
279
+ * @remarks
280
+ * 未完成标记已写下之后连 abort 都没成功(host 断开、删目录失败),目标上就留着一份需要清理的残留:
281
+ * 此时报 `cleanup_pending`。标记写下之前的 abort 失败不留残留,照原始失败的码报;但独占可能还在 host
282
+ * 上(直到这个窗口断开才交还),清理失败因此不能丢。两种情况下 `cause` 都是装着原始失败与清理失败的
283
+ * `AggregateError`。
284
+ */
285
+ const abandon = async (
286
+ host: HostRestore,
287
+ marked: boolean,
288
+ dataDirectoryName: string,
289
+ cause: unknown
290
+ ): Promise<never> => {
291
+ try {
292
+ await host.abort();
293
+ } catch (cleanupError) {
294
+ const both = new AggregateError([cause, cleanupError], 'Restore and cleanup both failed');
295
+ if (!marked) {
296
+ const failure = toElectronPGliteBackupError(cause, dataDirectoryName, 'restore');
297
+ throw new RxDBBackupError(failure.code, failure.message, { details: failure.details, cause: both });
298
+ }
299
+ throw new RxDBBackupError(
300
+ 'cleanup_pending',
301
+ `Desktop PGlite restore failed and its partial data could not be removed; call cleanupIncompleteElectronPGliteRestore()`,
302
+ {
303
+ details: { field: 'dataDirectoryName', actual: dataDirectoryName },
304
+ cause: both
305
+ }
306
+ );
307
+ }
308
+ throw cause;
309
+ };
310
+
311
+ const restoreOnHost = async (
312
+ sourceReader: ReadableStreamDefaultReader<Uint8Array>,
313
+ target: ElectronPGliteRestoreTarget,
314
+ dataDirectoryName: string,
315
+ options: PGliteRestoreOptions
316
+ ): Promise<RxDBRestoreResult> => {
317
+ const transport = target.options.transport ?? resolveDesktopHostTransport();
318
+ await handshake(transport);
319
+ const expected = targetCompatibility(target.rxdb, await engineOf(transport));
320
+ throwIfAborted(options.signal);
321
+ const host = await beginHostRestore(transport, dataDirectoryName);
322
+ let marked = false;
323
+ try {
324
+ const archive = new RxDBBackupArchiveReader(sourceReader, options.signal);
325
+ const manifest = await archive.readManifest();
326
+ assertRxDBBackupCompatible(manifest, expected);
327
+ // 从这里起 host 可能已经写下未完成标记(请求发出去了、应答却没回来也算)。
328
+ marked = true;
329
+ const trailer = await runRestore({ host, archive, manifest, options });
330
+ return { ...trailer, manifest, scope: manifest.scope };
331
+ } catch (error) {
332
+ return abandon(host, marked, dataDirectoryName, error);
333
+ }
334
+ };
335
+
336
+ /**
337
+ * 把 {@link RxDBAdapterElectronPGlite.backup} 产出的归档恢复进 host 上的一个新数据目录。
338
+ *
339
+ * @remarks
340
+ * 只写入**空**目标,不做合并或覆盖。步骤:
341
+ *
342
+ * 1. 在读取归档之前拒绝已连接的目标实例(`target_busy`,`details.field` 为 `rxdb`)、协议版本不一致
343
+ * 或未启用备份的 host(`unsupported_combination`)。
344
+ * 2. host **先**取得目录的独占权,**再**检查未完成标记与空状态;独占一直保持到提交或失败处理结束,
345
+ * 期间其他窗口的 `connect`、其他恢复与清理请求都被拒绝。
346
+ * 3. 读 manifest,在写入第一个字节之前判定兼容性:适配器、引擎大版本、扩展(目标 host 必须全部提供)、
347
+ * 系统表 / 变更编码版本、实体结构指纹与加密认证域(`incompatible_archive` /
348
+ * `auth_domain_mismatch`)。
349
+ * 4. host 持久化未完成标记,归档逐项写进目录;读到结尾、摘要通过后才在私有实例上启动 PostgreSQL,
350
+ * 核对引擎与系统表水位,关闭实例并逐一 fsync,最后删除标记、交还独占。
351
+ *
352
+ * 任一步失败都由 host 删除目录与标记,目标回到从未恢复过的状态;连这一步也失败时报
353
+ * `cleanup_pending`,调 {@link cleanupIncompleteElectronPGliteRestore} 重试。恢复途中窗口或进程
354
+ * 退出时标记留在盘上,下一次连接以 `restore_incomplete` 拒绝,同样用它清理后再恢复。
355
+ *
356
+ * 归档只含数据库:结果与 manifest 的 `scope.externalFiles` 恒为 `excluded`,外置文件须另行备份恢复。
357
+ * 输入流在失败时被取消,成功时只释放读锁。桌面错误码按 {@link toElectronPGliteBackupError} 归一。
358
+ *
359
+ * @param source - 归档字节流
360
+ * @param target - 尚未连接的目标实例与其 adapter 选项
361
+ * @param options - 取消信号与阶段回调;四个阶段都会触发
362
+ * @returns 结束标记(条目数、字节数、SHA-256)、manifest 与范围
363
+ * @throws RxDBBackupError `target_busy` / `target_not_empty` / `restore_in_progress` / `restore_incomplete` /
364
+ * `unsupported_combination` / `incompatible_archive` / `auth_domain_mismatch` / `corrupt_archive` /
365
+ * `truncated_archive` / `aborted` / `io_error` / `storage_full` / `cleanup_pending`
366
+ *
367
+ * @example
368
+ * ```typescript
369
+ * const target = { rxdb, options: {} };
370
+ * await restoreElectronPGliteDatabase(file.stream(), target);
371
+ * rxdb.adapter(ELECTRON_PGLITE_ADAPTER_NAME, db => new RxDBAdapterElectronPGlite(db, target.options));
372
+ * await rxdb.connect(ELECTRON_PGLITE_ADAPTER_NAME);
373
+ * ```
374
+ */
375
+ export const restoreElectronPGliteDatabase = async (
376
+ source: ReadableStream<Uint8Array>,
377
+ target: ElectronPGliteRestoreTarget,
378
+ options: PGliteRestoreOptions = {}
379
+ ): Promise<RxDBRestoreResult> => {
380
+ const sourceReader = source.getReader();
381
+ const dataDirectoryName = resolveDataDirectoryName(target.rxdb.config.dbName, target.options);
382
+ try {
383
+ throwIfAborted(options.signal);
384
+ assertValidDesktopDatabaseName(dataDirectoryName);
385
+ assertTargetDisconnected(target.rxdb);
386
+ const result = await restoreOnHost(sourceReader, target, dataDirectoryName, options);
387
+ sourceReader.releaseLock();
388
+ return result;
389
+ } catch (error) {
390
+ await sourceReader.cancel(error).catch(() => undefined);
391
+ throw toElectronPGliteBackupError(error, dataDirectoryName, 'restore');
392
+ }
393
+ };
394
+
395
+ /**
396
+ * 清理一次没做完的恢复(窗口或进程在恢复中途退出、或恢复失败后清理本身也失败)。
397
+ *
398
+ * @remarks
399
+ * host 先取目录的独占权:目录正被打开或正在恢复时报 `target_busy`,从不删别人正在用的库。
400
+ * 没有未完成标记时什么都不做——一个正常的库永远不会被这里删掉。
401
+ *
402
+ * @param target - 恢复时使用的同一个目标
403
+ * @returns 确实清理了残留时为 `true`
404
+ * @throws RxDBBackupError `target_busy` / `unsupported_combination` / `cleanup_pending`
405
+ */
406
+ export const cleanupIncompleteElectronPGliteRestore = async (target: ElectronPGliteRestoreTarget): Promise<boolean> => {
407
+ const dataDirectoryName = resolveDataDirectoryName(target.rxdb.config.dbName, target.options);
408
+ try {
409
+ assertValidDesktopDatabaseName(dataDirectoryName);
410
+ const transport = target.options.transport ?? resolveDesktopHostTransport();
411
+ await handshake(transport);
412
+ const response = assertDesktopPgliteResponse(
413
+ 'pg.restore.cleanup',
414
+ await transport.request({ kind: 'pg.restore.cleanup', storage: { engine: 'pglite', dataDirectoryName } })
415
+ );
416
+ return response.result.cleaned;
417
+ } catch (error) {
418
+ const mapped = toElectronPGliteBackupError(error, dataDirectoryName, 'restore');
419
+ if (mapped.code !== 'io_error') throw mapped;
420
+ throw new RxDBBackupError(
421
+ 'cleanup_pending',
422
+ `Failed to clean up the incomplete restore of "${dataDirectoryName}"`,
423
+ {
424
+ details: { field: 'dataDirectoryName', actual: dataDirectoryName },
425
+ cause: error
426
+ }
427
+ );
428
+ }
429
+ };