@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.
- package/README.md +122 -21
- package/dist/RxDBAdapterElectron.d.ts +27 -1
- package/dist/RxDBAdapterElectron.d.ts.map +1 -1
- package/dist/electron-sqlite-host.d.ts.map +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +46 -40
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -6
- package/dist/node-sqlite-engine.d.ts +10 -0
- package/dist/node-sqlite-engine.d.ts.map +1 -1
- package/dist/pglite/RxDBAdapterElectronPGlite.d.ts +53 -1
- package/dist/pglite/RxDBAdapterElectronPGlite.d.ts.map +1 -1
- package/dist/pglite/desktop-pglite-client.d.ts +27 -1
- package/dist/pglite/desktop-pglite-client.d.ts.map +1 -1
- package/dist/pglite/desktop-pglite-results.d.ts +14 -0
- package/dist/pglite/desktop-pglite-results.d.ts.map +1 -0
- package/dist/pglite/electron-pglite-backup-error.d.ts +32 -0
- package/dist/pglite/electron-pglite-backup-error.d.ts.map +1 -0
- package/dist/pglite/pglite-adapter.interface.d.ts +12 -0
- package/dist/pglite/pglite-adapter.interface.d.ts.map +1 -1
- package/dist/pglite/restore-electron-pglite-database.d.ts +68 -0
- package/dist/pglite/restore-electron-pglite-database.d.ts.map +1 -0
- package/dist/pglite-host/electron-pglite-host.d.ts +47 -32
- package/dist/pglite-host/electron-pglite-host.d.ts.map +1 -1
- package/dist/pglite-host/pglite-host-data-dir.d.ts +75 -0
- package/dist/pglite-host/pglite-host-data-dir.d.ts.map +1 -0
- package/dist/pglite-host/pglite-host-lock.d.ts +48 -0
- package/dist/pglite-host/pglite-host-lock.d.ts.map +1 -0
- package/dist/pglite-host/pglite-host-restore.d.ts +38 -0
- package/dist/pglite-host/pglite-host-restore.d.ts.map +1 -0
- package/dist/pglite-host/pglite-host-runtime.d.ts +90 -0
- package/dist/pglite-host/pglite-host-runtime.d.ts.map +1 -0
- package/dist/pglite-host.d.ts +2 -2
- package/dist/pglite-host.d.ts.map +1 -1
- package/dist/pglite-host.js +643 -120
- package/dist/pglite.d.ts +2 -0
- package/dist/pglite.d.ts.map +1 -1
- package/dist/pglite.js +341 -65
- package/package.json +13 -10
- package/src/RxDBAdapterElectron.ts +40 -1
- package/src/electron-sqlite-host.ts +3 -0
- package/src/host.ts +1 -0
- package/src/index.ts +1 -0
- package/src/node-sqlite-engine.ts +20 -1
- package/src/pglite/RxDBAdapterElectronPGlite.ts +80 -5
- package/src/pglite/desktop-pglite-client.ts +111 -17
- package/src/pglite/desktop-pglite-results.ts +24 -0
- package/src/pglite/electron-pglite-backup-error.ts +128 -0
- package/src/pglite/pglite-adapter.interface.ts +14 -0
- package/src/pglite/restore-electron-pglite-database.ts +429 -0
- package/src/pglite-host/electron-pglite-host.ts +409 -160
- package/src/pglite-host/pglite-host-data-dir.ts +224 -0
- package/src/pglite-host/pglite-host-lock.ts +154 -0
- package/src/pglite-host/pglite-host-restore.ts +460 -0
- package/src/pglite-host/pglite-host-runtime.ts +166 -0
- package/src/pglite-host.ts +7 -0
- package/src/pglite.ts +9 -0
|
@@ -19,12 +19,38 @@ import {
|
|
|
19
19
|
DESKTOP_PGLITE_PROTOCOL_VERSION,
|
|
20
20
|
parseDesktopPgliteRequest,
|
|
21
21
|
RxDBAdapterDesktopError,
|
|
22
|
+
type DesktopPgliteBackupBeginRequest,
|
|
23
|
+
type DesktopPgliteBackupCursorRequest,
|
|
24
|
+
type DesktopPgliteDataDirItem,
|
|
25
|
+
type DesktopPgliteEngineRequest,
|
|
26
|
+
type DesktopPgliteEngineResult,
|
|
22
27
|
type DesktopPgliteNotifyMessage,
|
|
23
|
-
type DesktopPgliteQueryResult,
|
|
24
28
|
type DesktopPgliteRequest,
|
|
25
29
|
type DesktopPgliteResponse
|
|
26
30
|
} from '@aiao/rxdb-adapter-sqlite-core/desktop-host';
|
|
27
31
|
import { randomUUID } from 'node:crypto';
|
|
32
|
+
import { hasPgVersion, walkPgliteDataDirectory } from './pglite-host-data-dir.js';
|
|
33
|
+
import { acquirePgliteDirectoryLock, type PgliteDirectoryLock } from './pglite-host-lock.js';
|
|
34
|
+
import {
|
|
35
|
+
createPgliteRestoreCoordinator,
|
|
36
|
+
type DesktopPgliteRestoreRequest,
|
|
37
|
+
type PgliteRestoreCoordinator
|
|
38
|
+
} from './pglite-host-restore.js';
|
|
39
|
+
import {
|
|
40
|
+
runStatement,
|
|
41
|
+
suspendTransaction,
|
|
42
|
+
toWireResult,
|
|
43
|
+
type ElectronPgliteProbeRuntime,
|
|
44
|
+
type ElectronPgliteRuntime,
|
|
45
|
+
type SuspendedTransaction
|
|
46
|
+
} from './pglite-host-runtime.js';
|
|
47
|
+
|
|
48
|
+
export type {
|
|
49
|
+
ElectronPgliteProbeRuntime,
|
|
50
|
+
ElectronPgliteRuntime,
|
|
51
|
+
ElectronPgliteRuntimeResult,
|
|
52
|
+
ElectronPgliteTransaction
|
|
53
|
+
} from './pglite-host-runtime.js';
|
|
28
54
|
|
|
29
55
|
/**
|
|
30
56
|
* 逻辑位置的 scheme。
|
|
@@ -50,34 +76,41 @@ export const DESKTOP_PGLITE_WATCH_CHANNELS: readonly string[] = Object.freeze([
|
|
|
50
76
|
'rxdb_migration_notify'
|
|
51
77
|
]);
|
|
52
78
|
|
|
53
|
-
/** 一条语句的结果;`ElectronPgliteRuntime` 与 PGlite 的 `Results` 在此结构相容。 */
|
|
54
|
-
export interface ElectronPgliteRuntimeResult {
|
|
55
|
-
readonly rows: Record<string, unknown>[];
|
|
56
|
-
readonly fields: { name: string; dataTypeID: number }[];
|
|
57
|
-
readonly affectedRows?: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** 事务句柄;对应 PGlite 的 `Transaction`,只声明 host 真正用到的两个操作。 */
|
|
61
|
-
export interface ElectronPgliteTransaction {
|
|
62
|
-
query(sql: string, params?: unknown[]): Promise<ElectronPgliteRuntimeResult>;
|
|
63
|
-
exec(sql: string): Promise<ElectronPgliteRuntimeResult[]>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
79
|
/**
|
|
67
|
-
*
|
|
80
|
+
* 备份与恢复所需的宿主配置(US-217)。
|
|
68
81
|
*
|
|
69
82
|
* @remarks
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* 两者都能直接传入,而这份清单也顺便说清了 host 只需要这五个操作。
|
|
83
|
+
* 不传时 host 只提供连接:`pg.engine`、`pg.backup.*`、`pg.restore.*` 一律报 `unsupported_operation`,
|
|
84
|
+
* `pg.open` 也不取目录锁,与引入备份之前的行为逐项一致。传了之后每个打开的数据目录旁边都有一把
|
|
85
|
+
* 跨进程的目录锁:同一目录上的恢复与连接互斥,另一个进程里的 host 也不例外(AC#12)。
|
|
74
86
|
*/
|
|
75
|
-
export interface
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
export interface ElectronPgliteBackupOptions {
|
|
88
|
+
/**
|
|
89
|
+
* 逻辑数据目录名到物理路径。
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* **必须**就是 `createRuntime` 为同一个名字打开的目录:备份从这里读文件,恢复往这里写文件,
|
|
93
|
+
* 目录锁与恢复标记也建在它旁边。两者不一致时备份报 `host_internal_error`,而不是悄悄读出别的目录。
|
|
94
|
+
* 传进来的名字已过白名单校验,不含任何路径分隔符。
|
|
95
|
+
*/
|
|
96
|
+
readonly resolveDataDirectory: (dataDirectoryName: string) => string;
|
|
97
|
+
/**
|
|
98
|
+
* 创建一个不落盘的探针运行时,用来回答 `pg.engine`。
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* 例如 `new PGlite()`(内存实例)。必须与 `createRuntime` 用同一份 PGlite、带同一组扩展,
|
|
102
|
+
* 否则回答的引擎版本对真实数据目录不成立。探针用完即关,结果在 host 生命周期内缓存;
|
|
103
|
+
* 起不来时不缓存,下一次 `pg.engine` 重试。
|
|
104
|
+
*/
|
|
105
|
+
readonly createProbeRuntime: () => Promise<ElectronPgliteProbeRuntime>;
|
|
106
|
+
/**
|
|
107
|
+
* `createRuntime` 为每个实例装载的扩展名。
|
|
108
|
+
*
|
|
109
|
+
* @remarks
|
|
110
|
+
* 只列影响数据兼容性的扩展,不含 `live`(它不改变数据目录);没有扩展时传空数组。
|
|
111
|
+
* 归档据此声明所需扩展,恢复端不满足时在写入目标之前拒绝(AC#5)。
|
|
112
|
+
*/
|
|
113
|
+
readonly extensions: readonly string[];
|
|
81
114
|
}
|
|
82
115
|
|
|
83
116
|
/** {@link createElectronPgliteHost} 的入参。 */
|
|
@@ -91,7 +124,7 @@ export interface ElectronPgliteHostOptions {
|
|
|
91
124
|
* 数据会静默落在谁也没打算用的位置,而症状是「重启后数据没了」。
|
|
92
125
|
*
|
|
93
126
|
* 传进来的名字已过白名单校验,不含任何路径分隔符,因此 `join(root, name)` 不会越出 `root`。
|
|
94
|
-
*
|
|
127
|
+
* 同一个名字同一时刻至多只有一个活着的运行时(AC#7);恢复校验同样经它打开目标目录。
|
|
95
128
|
*/
|
|
96
129
|
readonly createRuntime: (dataDirectoryName: string) => Promise<ElectronPgliteRuntime>;
|
|
97
130
|
/** 把裸 NOTIFY 送达对应会话的 renderer,例如 `webContents.send`。 */
|
|
@@ -104,6 +137,8 @@ export interface ElectronPgliteHostOptions {
|
|
|
104
137
|
* 把送达失败当成写失败回给调用方只会诱发一次重复写入。不传则丢弃。
|
|
105
138
|
*/
|
|
106
139
|
readonly onDeliveryError?: (error: unknown) => void;
|
|
140
|
+
/** 备份与恢复支持;不传则不提供(见 {@link ElectronPgliteBackupOptions})。 */
|
|
141
|
+
readonly backup?: ElectronPgliteBackupOptions;
|
|
107
142
|
}
|
|
108
143
|
|
|
109
144
|
/** 桌面 PGlite host 实例。 */
|
|
@@ -122,31 +157,43 @@ export interface ElectronPgliteHost {
|
|
|
122
157
|
handle(request: unknown, ownerId: number): Promise<DesktopPgliteResponse>;
|
|
123
158
|
/** 当前打开的会话数。 */
|
|
124
159
|
readonly openSessionCount: number;
|
|
125
|
-
/** 当前活着的 PGlite
|
|
160
|
+
/** 当前活着的 PGlite 实例数;同一个数据目录上的多个会话只算一个,恢复校验用的私有实例不计入。 */
|
|
126
161
|
readonly openInstanceCount: number;
|
|
127
162
|
/** 当前挂起的事务数;正常静止时应为 0。 */
|
|
128
163
|
readonly openTransactionCount: number;
|
|
164
|
+
/** 当前进行中的备份快照数;快照同样占着连接,但不计入 `openTransactionCount`。 */
|
|
165
|
+
readonly openBackupCount: number;
|
|
166
|
+
/** 当前进行中的恢复数。 */
|
|
167
|
+
readonly openRestoreCount: number;
|
|
129
168
|
/**
|
|
130
|
-
*
|
|
169
|
+
* 回收某个窗口名下的全部会话、事务、快照与恢复。
|
|
131
170
|
*
|
|
132
171
|
* @remarks
|
|
133
172
|
* 这是本方案的**前提而非收尾**:挂起的 callback 独占 PGlite 的连接锁,渲染进程崩在
|
|
134
173
|
* 事务中间而没人回收的话,之后任何查询都会永远排队——表征是「数据库不响应」,
|
|
135
174
|
* 与那次崩溃毫无关联线索。调用方必须把它挂到 `render-process-gone` 与 `destroyed`
|
|
136
|
-
* 两个事件上(AC#3)。
|
|
175
|
+
* 两个事件上(AC#3)。被打断的恢复保留恢复标记,之后的连接据此被拒绝(AC#11)。
|
|
137
176
|
*
|
|
138
177
|
* @param ownerId - 已崩溃或已销毁的 `webContents.id`
|
|
139
178
|
* @returns 本次回滚掉的事务条数
|
|
140
179
|
*/
|
|
141
180
|
releaseOwner(ownerId: number): Promise<number>;
|
|
142
|
-
/**
|
|
181
|
+
/** 关闭全部会话与实例、打断全部恢复,通常在应用退出前调用。 */
|
|
143
182
|
closeAll(): Promise<void>;
|
|
144
183
|
}
|
|
145
184
|
|
|
185
|
+
/** 一个已经起来的运行时,以及启用备份时它在数据目录上持有的锁。 */
|
|
186
|
+
interface StartedRuntime {
|
|
187
|
+
readonly runtime: ElectronPgliteRuntime;
|
|
188
|
+
readonly lock: PgliteDirectoryLock | undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
146
191
|
/** 一个数据目录上的运行时及其会话。 */
|
|
147
192
|
interface InstanceEntry {
|
|
148
193
|
readonly sessions: Set<string>;
|
|
149
|
-
|
|
194
|
+
/** 正在等 `ready` 的 `pg.open` 数;不为 0 时最后一条会话关掉也不释放实例。 */
|
|
195
|
+
pending: number;
|
|
196
|
+
readonly ready: Promise<StartedRuntime>;
|
|
150
197
|
}
|
|
151
198
|
|
|
152
199
|
/** 一条会话。 */
|
|
@@ -156,46 +203,30 @@ interface SessionEntry {
|
|
|
156
203
|
}
|
|
157
204
|
|
|
158
205
|
/** 一条挂起中的事务。 */
|
|
159
|
-
|
|
206
|
+
type TransactionEntry = SuspendedTransaction & { readonly sessionId: string };
|
|
207
|
+
|
|
208
|
+
/** 一次进行中的备份快照。 */
|
|
209
|
+
interface BackupEntry {
|
|
160
210
|
readonly sessionId: string;
|
|
161
|
-
readonly
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
readonly finished: Promise<unknown>;
|
|
211
|
+
readonly iterator: AsyncGenerator<DesktopPgliteDataDirItem>;
|
|
212
|
+
/** 读快照期间占住连接的空事务:它不结束,任何语句都改不了文件(AC#16)。 */
|
|
213
|
+
readonly held: SuspendedTransaction;
|
|
165
214
|
}
|
|
166
215
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
promise,
|
|
175
|
-
new Promise<typeof TIMED_OUT>(resolve => {
|
|
176
|
-
timer = setTimeout(() => resolve(TIMED_OUT), ms);
|
|
177
|
-
})
|
|
178
|
-
]);
|
|
179
|
-
} finally {
|
|
180
|
-
// 不清掉的话,一次成功的 begin 也会让 Node 的事件循环多活 `ms` 毫秒,
|
|
181
|
-
// 于是应用退出被推迟——在测试里表现为「跑完了但进程不退」。
|
|
182
|
-
clearTimeout(timer);
|
|
183
|
-
}
|
|
184
|
-
};
|
|
216
|
+
/** 启用备份时的配置与状态。 */
|
|
217
|
+
interface BackupSupport {
|
|
218
|
+
readonly options: ElectronPgliteBackupOptions;
|
|
219
|
+
/** 去重并排序后的扩展名。 */
|
|
220
|
+
readonly extensions: readonly string[];
|
|
221
|
+
readonly restores: PgliteRestoreCoordinator;
|
|
222
|
+
}
|
|
185
223
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
* `rows` 里的 bigint / Uint8Array / Date 都是结构化克隆原生支持的,原样带走(AC#1)。
|
|
193
|
-
*/
|
|
194
|
-
const toWireResult = (result: ElectronPgliteRuntimeResult): DesktopPgliteQueryResult => ({
|
|
195
|
-
rows: result.rows,
|
|
196
|
-
fields: result.fields.map(field => ({ name: field.name, dataTypeID: field.dataTypeID })),
|
|
197
|
-
affectedRows: result.affectedRows
|
|
198
|
-
});
|
|
224
|
+
/** 协议 v2 新增、只在启用备份时可用的请求。 */
|
|
225
|
+
type DesktopPgliteBackupRequest =
|
|
226
|
+
| DesktopPgliteEngineRequest
|
|
227
|
+
| DesktopPgliteBackupBeginRequest
|
|
228
|
+
| DesktopPgliteBackupCursorRequest
|
|
229
|
+
| DesktopPgliteRestoreRequest;
|
|
199
230
|
|
|
200
231
|
const toErrorResponse = (error: unknown): DesktopPgliteResponse => {
|
|
201
232
|
if (error instanceof RxDBAdapterDesktopError) {
|
|
@@ -208,31 +239,23 @@ const toErrorResponse = (error: unknown): DesktopPgliteResponse => {
|
|
|
208
239
|
};
|
|
209
240
|
};
|
|
210
241
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
return await run();
|
|
221
|
-
} catch (error) {
|
|
222
|
-
if (error instanceof RxDBAdapterDesktopError) throw error;
|
|
223
|
-
throw new RxDBAdapterDesktopError('statement_failed', error instanceof Error ? error.message : String(error), {
|
|
224
|
-
cause: error
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
};
|
|
242
|
+
const openFailed = (dataDirectoryName: string, cause: unknown): RxDBAdapterDesktopError =>
|
|
243
|
+
new RxDBAdapterDesktopError(
|
|
244
|
+
'open_failed',
|
|
245
|
+
`the application could not open a PGlite runtime for ${dataDirectoryName}`,
|
|
246
|
+
{
|
|
247
|
+
cause
|
|
248
|
+
}
|
|
249
|
+
);
|
|
228
250
|
|
|
229
251
|
/**
|
|
230
252
|
* 创建一个桌面 PGlite host。
|
|
231
253
|
*
|
|
232
254
|
* @remarks
|
|
233
255
|
* 与 SQLite host 相反,同一个数据目录上的多个会话**共享一个** PGlite 实例(AC#7):
|
|
234
|
-
* PGlite
|
|
235
|
-
*
|
|
256
|
+
* PGlite 是嵌入式单写者,同一份数据目录被两个实例同时打开会直接损坏它。同一 host 内跨窗口的
|
|
257
|
+
* 并发因此由这一条连接的排队来串行化;启用备份时每个实例还在数据目录旁持有一把跨进程的
|
|
258
|
+
* 目录锁,恢复据此确认目标没有被任何进程连着(AC#12)。
|
|
236
259
|
*
|
|
237
260
|
* @param options - host 配置
|
|
238
261
|
* @returns host 实例
|
|
@@ -241,6 +264,30 @@ export function createElectronPgliteHost(options: ElectronPgliteHostOptions): El
|
|
|
241
264
|
const instances = new Map<string, InstanceEntry>();
|
|
242
265
|
const sessions = new Map<string, SessionEntry>();
|
|
243
266
|
const transactions = new Map<string, TransactionEntry>();
|
|
267
|
+
const backups = new Map<string, BackupEntry>();
|
|
268
|
+
/** 正在关闭的实例:关完之前,同名的新连接与恢复都要等它交还目录锁。 */
|
|
269
|
+
const closing = new Map<string, Promise<void>>();
|
|
270
|
+
let engineProbe: Promise<DesktopPgliteEngineResult> | undefined;
|
|
271
|
+
|
|
272
|
+
/** 本 host 是否正连着该目录;正在关闭的实例先等它关完,免得恢复撞上自己 host 里的旧锁。 */
|
|
273
|
+
const isInUse = async (dataDirectoryName: string): Promise<boolean> => {
|
|
274
|
+
while (closing.has(dataDirectoryName)) await closing.get(dataDirectoryName);
|
|
275
|
+
return instances.has(dataDirectoryName);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const backupOptions = options.backup;
|
|
279
|
+
const support: BackupSupport | undefined =
|
|
280
|
+
backupOptions === undefined ? undefined : (
|
|
281
|
+
{
|
|
282
|
+
options: backupOptions,
|
|
283
|
+
extensions: [...new Set(backupOptions.extensions)].sort(),
|
|
284
|
+
restores: createPgliteRestoreCoordinator({
|
|
285
|
+
createRuntime: name => options.createRuntime(name),
|
|
286
|
+
resolveDataDirectory: name => backupOptions.resolveDataDirectory(name),
|
|
287
|
+
isInUse
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
);
|
|
244
291
|
|
|
245
292
|
const deliver = (sessionId: string, channel: string, payload: string): void => {
|
|
246
293
|
try {
|
|
@@ -250,20 +297,101 @@ export function createElectronPgliteHost(options: ElectronPgliteHostOptions): El
|
|
|
250
297
|
}
|
|
251
298
|
};
|
|
252
299
|
|
|
300
|
+
/**
|
|
301
|
+
* 取数据目录锁,并确认目录上没有未完成的恢复。
|
|
302
|
+
*
|
|
303
|
+
* @remarks
|
|
304
|
+
* 同步完成:`pg.open` 因此在第一个 tick 里就占住目录,之后才开始的恢复必然撞锁,
|
|
305
|
+
* 不存在「恢复查完空状态、连接随即打开」的窗口(AC#12)。未启用备份时不取锁。
|
|
306
|
+
*/
|
|
307
|
+
const lockDirectory = (dataDirectoryName: string): PgliteDirectoryLock | undefined => {
|
|
308
|
+
if (support === undefined) return undefined;
|
|
309
|
+
const lock = acquirePgliteDirectoryLock(support.options.resolveDataDirectory(dataDirectoryName));
|
|
310
|
+
let marked: boolean;
|
|
311
|
+
try {
|
|
312
|
+
marked = lock.hasMarker();
|
|
313
|
+
} catch (error) {
|
|
314
|
+
lock.release();
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
if (!marked) return lock;
|
|
318
|
+
lock.release();
|
|
319
|
+
// 半恢复的目录一旦被 PGlite 打开,缺的文件会被当成「本来就没有」,于是连上一个看似正常的坏库(AC#11)。
|
|
320
|
+
throw new RxDBAdapterDesktopError(
|
|
321
|
+
'restore_incomplete',
|
|
322
|
+
`a restore of "${dataDirectoryName}" did not finish; run pg.restore.cleanup before connecting`
|
|
323
|
+
);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const subscribeChannels = async (runtime: ElectronPgliteRuntime, sessionIds: ReadonlySet<string>): Promise<void> => {
|
|
327
|
+
// 订阅在这里一次性建好,而不是每开一个会话建一次:LISTEN 是连接级的,
|
|
328
|
+
// 重复订阅只会让同一条 NOTIFY 被回调多次,进而让 renderer 侧的批量窗口收到重复事件。
|
|
329
|
+
for (const channel of DESKTOP_PGLITE_WATCH_CHANNELS) {
|
|
330
|
+
await runtime.listen(channel, payload => {
|
|
331
|
+
for (const sessionId of sessionIds) deliver(sessionId, channel, payload);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const launchRuntime = async (
|
|
337
|
+
dataDirectoryName: string,
|
|
338
|
+
sessionIds: ReadonlySet<string>
|
|
339
|
+
): Promise<ElectronPgliteRuntime> => {
|
|
340
|
+
let runtime: ElectronPgliteRuntime;
|
|
341
|
+
try {
|
|
342
|
+
runtime = await options.createRuntime(dataDirectoryName);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
throw openFailed(dataDirectoryName, error);
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
await subscribeChannels(runtime, sessionIds);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
// 订阅失败的运行时必须关掉:它开着数据目录,重试时起的新实例会与它争同一份文件。
|
|
350
|
+
await runtime.close().catch(() => undefined);
|
|
351
|
+
throw openFailed(dataDirectoryName, error);
|
|
352
|
+
}
|
|
353
|
+
return runtime;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const startRuntime = async (dataDirectoryName: string, sessionIds: ReadonlySet<string>): Promise<StartedRuntime> => {
|
|
357
|
+
const lock = lockDirectory(dataDirectoryName);
|
|
358
|
+
try {
|
|
359
|
+
return { runtime: await launchRuntime(dataDirectoryName, sessionIds), lock };
|
|
360
|
+
} catch (error) {
|
|
361
|
+
lock?.release();
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
253
366
|
const createInstance = (dataDirectoryName: string): InstanceEntry => {
|
|
254
367
|
const sessionIds = new Set<string>();
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
368
|
+
return { sessions: sessionIds, pending: 0, ready: startRuntime(dataDirectoryName, sessionIds) };
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/** 关掉一个实例的运行时并交还目录锁;只有起来过、挂过会话的实例才会走到这里。 */
|
|
372
|
+
const shutdownInstance = async (instance: InstanceEntry): Promise<void> => {
|
|
373
|
+
const { runtime, lock } = await instance.ready;
|
|
374
|
+
try {
|
|
375
|
+
await runtime.close();
|
|
376
|
+
} finally {
|
|
377
|
+
lock?.release();
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/** 从表里摘掉实例并关掉它;摘表与登记「正在关闭」之间没有 await,同名的请求不会漏看。 */
|
|
382
|
+
const releaseInstance = async (dataDirectoryName: string, instance: InstanceEntry): Promise<void> => {
|
|
383
|
+
instances.delete(dataDirectoryName);
|
|
384
|
+
const done = shutdownInstance(instance);
|
|
385
|
+
const settled = done.then(
|
|
386
|
+
() => undefined,
|
|
387
|
+
() => undefined
|
|
388
|
+
);
|
|
389
|
+
closing.set(dataDirectoryName, settled);
|
|
390
|
+
try {
|
|
391
|
+
await done;
|
|
392
|
+
} finally {
|
|
393
|
+
if (closing.get(dataDirectoryName) === settled) closing.delete(dataDirectoryName);
|
|
394
|
+
}
|
|
267
395
|
};
|
|
268
396
|
|
|
269
397
|
const requireSession = (sessionId: string, ownerId: number): SessionEntry => {
|
|
@@ -285,7 +413,7 @@ export function createElectronPgliteHost(options: ElectronPgliteHostOptions): El
|
|
|
285
413
|
`session references a released instance for ${session.dataDirectoryName}`
|
|
286
414
|
);
|
|
287
415
|
}
|
|
288
|
-
return instance.ready;
|
|
416
|
+
return (await instance.ready).runtime;
|
|
289
417
|
};
|
|
290
418
|
|
|
291
419
|
/**
|
|
@@ -343,19 +471,23 @@ export function createElectronPgliteHost(options: ElectronPgliteHostOptions): El
|
|
|
343
471
|
ownerId: number
|
|
344
472
|
): Promise<DesktopPgliteResponse> => {
|
|
345
473
|
const { dataDirectoryName } = request.storage;
|
|
474
|
+
while (closing.has(dataDirectoryName)) await closing.get(dataDirectoryName);
|
|
475
|
+
if (support?.restores.isReserved(dataDirectoryName)) {
|
|
476
|
+
throw new RxDBAdapterDesktopError('restore_in_progress', `"${dataDirectoryName}" is being restored`);
|
|
477
|
+
}
|
|
478
|
+
// 从上面的检查到登记实例之间没有 await:并发的恢复要么已经占了名字,要么会看见这个实例。
|
|
346
479
|
const instance = instances.get(dataDirectoryName) ?? createInstance(dataDirectoryName);
|
|
347
480
|
instances.set(dataDirectoryName, instance);
|
|
481
|
+
instance.pending += 1;
|
|
348
482
|
try {
|
|
349
483
|
await instance.ready;
|
|
350
484
|
} catch (error) {
|
|
351
485
|
// 起不来的实例不能留在表里:留着的话下一次 `pg.open` 会拿到同一个已经 reject 的
|
|
352
486
|
// promise,于是「修好配置再试一次」永远不可能成功。
|
|
353
|
-
if (
|
|
354
|
-
throw
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
{ cause: error }
|
|
358
|
-
);
|
|
487
|
+
if (instances.get(dataDirectoryName) === instance) instances.delete(dataDirectoryName);
|
|
488
|
+
throw error;
|
|
489
|
+
} finally {
|
|
490
|
+
instance.pending -= 1;
|
|
359
491
|
}
|
|
360
492
|
const sessionId = randomUUID();
|
|
361
493
|
instance.sessions.add(sessionId);
|
|
@@ -370,7 +502,7 @@ export function createElectronPgliteHost(options: ElectronPgliteHostOptions): El
|
|
|
370
502
|
};
|
|
371
503
|
};
|
|
372
504
|
|
|
373
|
-
/**
|
|
505
|
+
/** 关掉一条会话;数据目录上最后一条会话消失、且没有正在加入的 `pg.open` 时连实例一起释放。 */
|
|
374
506
|
const closeSession = async (sessionId: string): Promise<void> => {
|
|
375
507
|
const session = sessions.get(sessionId);
|
|
376
508
|
if (!session) return;
|
|
@@ -378,10 +510,8 @@ export function createElectronPgliteHost(options: ElectronPgliteHostOptions): El
|
|
|
378
510
|
const instance = instances.get(session.dataDirectoryName);
|
|
379
511
|
if (!instance) return;
|
|
380
512
|
instance.sessions.delete(sessionId);
|
|
381
|
-
if (instance.sessions.size > 0) return;
|
|
382
|
-
|
|
383
|
-
const runtime = await instance.ready.catch(() => undefined);
|
|
384
|
-
await runtime?.close();
|
|
513
|
+
if (instance.sessions.size > 0 || instance.pending > 0) return;
|
|
514
|
+
await releaseInstance(session.dataDirectoryName, instance);
|
|
385
515
|
};
|
|
386
516
|
|
|
387
517
|
/** 回滚一条会话名下全部挂起的事务,返回条数。 */
|
|
@@ -398,42 +528,9 @@ export function createElectronPgliteHost(options: ElectronPgliteHostOptions): El
|
|
|
398
528
|
ownerId: number
|
|
399
529
|
): Promise<DesktopPgliteResponse> => {
|
|
400
530
|
const runtime = await requireRuntime(requireSession(request.sessionId, ownerId));
|
|
401
|
-
|
|
402
|
-
let settleHandles!: TransactionEntry['settle'];
|
|
403
|
-
const closed = new Promise<void>((resolve, reject) => {
|
|
404
|
-
settleHandles = { resolve, reject };
|
|
405
|
-
});
|
|
406
|
-
// 超时路径会在任何人 await 之前就 reject 它;先接住,免得变成未处理拒绝而打死进程。
|
|
407
|
-
closed.catch(() => undefined);
|
|
408
|
-
|
|
409
|
-
let markStarted!: (tx: ElectronPgliteTransaction) => void;
|
|
410
|
-
const started = new Promise<ElectronPgliteTransaction>(resolve => {
|
|
411
|
-
markStarted = resolve;
|
|
412
|
-
});
|
|
413
|
-
const finished = runtime.transaction(async tx => {
|
|
414
|
-
markStarted(tx);
|
|
415
|
-
// 事务从这里一直开着,直到 commit / rollback / 回收把 `closed` 结掉。
|
|
416
|
-
await closed;
|
|
417
|
-
});
|
|
418
|
-
finished.catch(() => undefined);
|
|
419
|
-
|
|
420
|
-
const tx = await raceTimeout(started, request.timeout);
|
|
421
|
-
if (tx === TIMED_OUT) {
|
|
422
|
-
// 关键在于 reject 而不是简单丢弃:被丢弃的 callback 迟早会在连接空出来的一瞬间
|
|
423
|
-
// 启动,然后永远挂在 `await closed` 上——于是「超时之后再也开不了事务」,
|
|
424
|
-
// 而现场看起来只是「数据库不响应」。reject 让它一进 callback 就抛,
|
|
425
|
-
// PGlite 随即回滚这条空事务并交还连接。
|
|
426
|
-
settleHandles.reject(
|
|
427
|
-
new RxDBAdapterDesktopError('transaction_unavailable', 'the begin that opened this transaction timed out')
|
|
428
|
-
);
|
|
429
|
-
throw new RxDBAdapterDesktopError(
|
|
430
|
-
'transaction_unavailable',
|
|
431
|
-
`waited ${request.timeout}ms for the PGlite connection but another transaction still holds it`
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
|
|
531
|
+
const held = await suspendTransaction(runtime, request.timeout);
|
|
435
532
|
const transactionId = randomUUID();
|
|
436
|
-
transactions.set(transactionId, { sessionId: request.sessionId
|
|
533
|
+
transactions.set(transactionId, { ...held, sessionId: request.sessionId });
|
|
437
534
|
return { kind: 'pg.begin', result: { transactionId } };
|
|
438
535
|
};
|
|
439
536
|
|
|
@@ -492,13 +589,163 @@ export function createElectronPgliteHost(options: ElectronPgliteHostOptions): El
|
|
|
492
589
|
return { kind: 'pg.version', result: value };
|
|
493
590
|
};
|
|
494
591
|
|
|
592
|
+
const requireBackupSupport = (): BackupSupport => {
|
|
593
|
+
if (support === undefined) {
|
|
594
|
+
throw new RxDBAdapterDesktopError('unsupported_operation', 'this host was created without backup support');
|
|
595
|
+
}
|
|
596
|
+
return support;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
/** 起一个探针问出引擎版本;探针不碰任何数据目录,也不占任何会话的连接。 */
|
|
600
|
+
const probeEngine = async (backup: BackupSupport): Promise<DesktopPgliteEngineResult> => {
|
|
601
|
+
let probe: ElectronPgliteProbeRuntime;
|
|
602
|
+
try {
|
|
603
|
+
probe = await backup.options.createProbeRuntime();
|
|
604
|
+
} catch (error) {
|
|
605
|
+
throw new RxDBAdapterDesktopError('open_failed', 'the application could not start the PGlite probe runtime', {
|
|
606
|
+
cause: error
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
const result = await runStatement(() => probe.query('SHOW server_version'));
|
|
611
|
+
const serverVersion = result.rows[0]?.['server_version'];
|
|
612
|
+
if (typeof serverVersion !== 'string' || serverVersion === '') {
|
|
613
|
+
throw new RxDBAdapterDesktopError('host_internal_error', 'PostgreSQL did not report server_version');
|
|
614
|
+
}
|
|
615
|
+
return { serverVersion, extensions: backup.extensions };
|
|
616
|
+
} finally {
|
|
617
|
+
await probe.close().catch(() => undefined);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const engine = async (backup: BackupSupport): Promise<DesktopPgliteResponse> => {
|
|
622
|
+
const probe = (engineProbe ??= probeEngine(backup));
|
|
623
|
+
try {
|
|
624
|
+
return { kind: 'pg.engine', result: await probe };
|
|
625
|
+
} catch (error) {
|
|
626
|
+
// 失败不缓存:探针起不来多半是环境问题(内存、WASM 加载),修好之后应当能重试。
|
|
627
|
+
if (engineProbe === probe) engineProbe = undefined;
|
|
628
|
+
throw error;
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const backupBegin = async (
|
|
633
|
+
backup: BackupSupport,
|
|
634
|
+
request: DesktopPgliteBackupBeginRequest,
|
|
635
|
+
ownerId: number
|
|
636
|
+
): Promise<DesktopPgliteResponse> => {
|
|
637
|
+
const session = requireSession(request.sessionId, ownerId);
|
|
638
|
+
// 先解析路径再占连接:解析抛错时不会留下一条没人收拾的挂起事务。
|
|
639
|
+
const directory = backup.options.resolveDataDirectory(session.dataDirectoryName);
|
|
640
|
+
const runtime = await requireRuntime(session);
|
|
641
|
+
const held = await suspendTransaction(runtime, request.timeout);
|
|
642
|
+
try {
|
|
643
|
+
// 挂起的空事务占住唯一的连接,此后没有语句能改动文件;CHECKPOINT 再把已提交的数据刷进
|
|
644
|
+
// 数据文件。目录树因此就是提交边界上的一致快照:在途事务要么已提交,要么还在排队(AC#16)。
|
|
645
|
+
await runStatement(() => held.tx.query('CHECKPOINT'));
|
|
646
|
+
if (!(await hasPgVersion(directory))) {
|
|
647
|
+
throw new RxDBAdapterDesktopError(
|
|
648
|
+
'host_internal_error',
|
|
649
|
+
`resolveDataDirectory does not point at the data directory of "${session.dataDirectoryName}"`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
} catch (error) {
|
|
653
|
+
held.settle.reject(new RxDBAdapterDesktopError('write_aborted', 'the snapshot was aborted'));
|
|
654
|
+
await held.finished.catch(() => undefined);
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
const backupId = randomUUID();
|
|
658
|
+
backups.set(backupId, { sessionId: request.sessionId, iterator: walkPgliteDataDirectory(directory), held });
|
|
659
|
+
return { kind: 'pg.backup.begin', result: { backupId } };
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
/** 取一次进行中的快照;核对会话的理由与 {@link requireTransaction} 相同。 */
|
|
663
|
+
const requireBackup = (backupId: string, sessionId: string): BackupEntry => {
|
|
664
|
+
const entry = backups.get(backupId);
|
|
665
|
+
if (!entry || entry.sessionId !== sessionId) {
|
|
666
|
+
throw new RxDBAdapterDesktopError(
|
|
667
|
+
'transaction_not_found',
|
|
668
|
+
`backup ${backupId} is unknown, already finished, or not owned by session ${sessionId}`
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
return entry;
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
/** 结束一次快照:先关遍历器(连同它开着的文件句柄),无论成败都放开连接。 */
|
|
675
|
+
const finishBackup = async (backupId: string): Promise<void> => {
|
|
676
|
+
const entry = backups.get(backupId);
|
|
677
|
+
if (!entry) return;
|
|
678
|
+
backups.delete(backupId);
|
|
679
|
+
try {
|
|
680
|
+
await entry.iterator.return(undefined);
|
|
681
|
+
} finally {
|
|
682
|
+
entry.held.settle.resolve();
|
|
683
|
+
await entry.held.finished.catch(() => undefined);
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
const backupNext = async (
|
|
688
|
+
request: DesktopPgliteBackupCursorRequest,
|
|
689
|
+
ownerId: number
|
|
690
|
+
): Promise<DesktopPgliteResponse> => {
|
|
691
|
+
requireSession(request.sessionId, ownerId);
|
|
692
|
+
const entry = requireBackup(request.backupId, request.sessionId);
|
|
693
|
+
let next: IteratorResult<DesktopPgliteDataDirItem>;
|
|
694
|
+
try {
|
|
695
|
+
next = await entry.iterator.next();
|
|
696
|
+
} catch (error) {
|
|
697
|
+
// 读失败的快照续不下去:连接必须立刻交还,否则排在后面的写入会一直等。
|
|
698
|
+
await finishBackup(request.backupId);
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
return { kind: 'pg.backup.next', result: next.done ? { type: 'end' } : next.value };
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const backupEnd = async (
|
|
705
|
+
request: DesktopPgliteBackupCursorRequest,
|
|
706
|
+
ownerId: number
|
|
707
|
+
): Promise<DesktopPgliteResponse> => {
|
|
708
|
+
requireSession(request.sessionId, ownerId);
|
|
709
|
+
requireBackup(request.backupId, request.sessionId);
|
|
710
|
+
await finishBackup(request.backupId);
|
|
711
|
+
return { kind: 'pg.backup.end' };
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const dispatchBackup = async (
|
|
715
|
+
request: DesktopPgliteBackupRequest,
|
|
716
|
+
ownerId: number
|
|
717
|
+
): Promise<DesktopPgliteResponse> => {
|
|
718
|
+
const backup = requireBackupSupport();
|
|
719
|
+
switch (request.kind) {
|
|
720
|
+
case 'pg.engine':
|
|
721
|
+
return engine(backup);
|
|
722
|
+
case 'pg.backup.begin':
|
|
723
|
+
return backupBegin(backup, request, ownerId);
|
|
724
|
+
case 'pg.backup.next':
|
|
725
|
+
return backupNext(request, ownerId);
|
|
726
|
+
case 'pg.backup.end':
|
|
727
|
+
return backupEnd(request, ownerId);
|
|
728
|
+
default:
|
|
729
|
+
return backup.restores.handle(request, ownerId);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
/** 收掉一条会话:先结束它的快照,再回滚它的事务,最后关会话;返回回滚掉的事务条数。 */
|
|
734
|
+
const teardownSession = async (sessionId: string, reason: string): Promise<number> => {
|
|
735
|
+
const doomed = [...backups.entries()].filter(([, entry]) => entry.sessionId === sessionId).map(([id]) => id);
|
|
736
|
+
// 关遍历器失败只会漏一个只读句柄,连接在 finishBackup 里总会交还;不能因此把会话留着。
|
|
737
|
+
for (const backupId of doomed) await finishBackup(backupId).catch(() => undefined);
|
|
738
|
+
const rolledBack = await rollbackSessionTransactions(sessionId, reason);
|
|
739
|
+
await closeSession(sessionId);
|
|
740
|
+
return rolledBack;
|
|
741
|
+
};
|
|
742
|
+
|
|
495
743
|
const close = async (
|
|
496
744
|
request: Extract<DesktopPgliteRequest, { kind: 'pg.close' }>,
|
|
497
745
|
ownerId: number
|
|
498
746
|
): Promise<DesktopPgliteResponse> => {
|
|
499
747
|
requireSession(request.sessionId, ownerId);
|
|
500
|
-
await
|
|
501
|
-
await closeSession(request.sessionId);
|
|
748
|
+
await teardownSession(request.sessionId, `session ${request.sessionId} was closed`);
|
|
502
749
|
return { kind: 'pg.close' };
|
|
503
750
|
};
|
|
504
751
|
|
|
@@ -506,8 +753,9 @@ export function createElectronPgliteHost(options: ElectronPgliteHostOptions): El
|
|
|
506
753
|
* 按种类派发一条已通过协议校验的请求。
|
|
507
754
|
*
|
|
508
755
|
* @remarks
|
|
509
|
-
*
|
|
510
|
-
*
|
|
756
|
+
* 协议加了新种类而这里忘记补分支时,`default` 把它交给 `dispatchBackup`,后者的参数类型
|
|
757
|
+
* 不接受它,于是在 `tsc` 阶段就红,而不是在运行期变成怪异失败——与 SQLite host 的穷尽
|
|
758
|
+
* `switch` 同一个目的。
|
|
511
759
|
*/
|
|
512
760
|
const dispatch = (request: DesktopPgliteRequest, ownerId: number): Promise<DesktopPgliteResponse> => {
|
|
513
761
|
switch (request.kind) {
|
|
@@ -534,13 +782,8 @@ export function createElectronPgliteHost(options: ElectronPgliteHostOptions): El
|
|
|
534
782
|
return version(request, ownerId);
|
|
535
783
|
case 'pg.close':
|
|
536
784
|
return close(request, ownerId);
|
|
537
|
-
default:
|
|
538
|
-
|
|
539
|
-
throw new RxDBAdapterDesktopError(
|
|
540
|
-
'protocol_violation',
|
|
541
|
-
`unsupported pglite request kind: ${String((_exhaustive as { kind?: unknown }).kind)}`
|
|
542
|
-
);
|
|
543
|
-
}
|
|
785
|
+
default:
|
|
786
|
+
return dispatchBackup(request, ownerId);
|
|
544
787
|
}
|
|
545
788
|
};
|
|
546
789
|
|
|
@@ -561,20 +804,26 @@ export function createElectronPgliteHost(options: ElectronPgliteHostOptions): El
|
|
|
561
804
|
get openTransactionCount(): number {
|
|
562
805
|
return transactions.size;
|
|
563
806
|
},
|
|
807
|
+
get openBackupCount(): number {
|
|
808
|
+
return backups.size;
|
|
809
|
+
},
|
|
810
|
+
get openRestoreCount(): number {
|
|
811
|
+
return support === undefined ? 0 : support.restores.openCount;
|
|
812
|
+
},
|
|
564
813
|
releaseOwner: async (ownerId: number): Promise<number> => {
|
|
565
814
|
const doomed = [...sessions.entries()].filter(([, session]) => session.owner === ownerId).map(([id]) => id);
|
|
566
815
|
let rolledBack = 0;
|
|
567
816
|
for (const sessionId of doomed) {
|
|
568
|
-
rolledBack += await
|
|
569
|
-
await closeSession(sessionId);
|
|
817
|
+
rolledBack += await teardownSession(sessionId, `owner ${ownerId} is gone`);
|
|
570
818
|
}
|
|
819
|
+
await support?.restores.releaseOwner(ownerId);
|
|
571
820
|
return rolledBack;
|
|
572
821
|
},
|
|
573
822
|
closeAll: async (): Promise<void> => {
|
|
574
823
|
for (const sessionId of [...sessions.keys()]) {
|
|
575
|
-
await
|
|
576
|
-
await closeSession(sessionId);
|
|
824
|
+
await teardownSession(sessionId, 'the host is shutting down');
|
|
577
825
|
}
|
|
826
|
+
await support?.restores.closeAll();
|
|
578
827
|
}
|
|
579
828
|
};
|
|
580
829
|
}
|