@aiao/rxdb-adapter-electron 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +394 -2
- package/dist/RxDBAdapterElectron.d.ts +61 -0
- package/dist/RxDBAdapterElectron.d.ts.map +1 -0
- package/dist/electron-adapter.interface.d.ts +23 -0
- package/dist/electron-adapter.interface.d.ts.map +1 -0
- package/dist/electron-file-host.d.ts +52 -0
- package/dist/electron-file-host.d.ts.map +1 -0
- package/dist/electron-sqlite-host.d.ts +65 -0
- package/dist/electron-sqlite-host.d.ts.map +1 -0
- package/dist/host.d.ts +19 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +768 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/node-sqlite-engine.d.ts +97 -0
- package/dist/node-sqlite-engine.d.ts.map +1 -0
- package/dist/pglite/RxDBAdapterElectronPGlite.d.ts +96 -0
- package/dist/pglite/RxDBAdapterElectronPGlite.d.ts.map +1 -0
- package/dist/pglite/desktop-pglite-client.d.ts +144 -0
- package/dist/pglite/desktop-pglite-client.d.ts.map +1 -0
- 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 +68 -0
- package/dist/pglite/pglite-adapter.interface.d.ts.map +1 -0
- 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 +129 -0
- package/dist/pglite-host/electron-pglite-host.d.ts.map +1 -0
- 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 +17 -0
- package/dist/pglite-host.d.ts.map +1 -0
- package/dist/pglite-host.js +738 -0
- package/dist/pglite.d.ts +22 -0
- package/dist/pglite.d.ts.map +1 -0
- package/dist/pglite.js +493 -0
- package/dist/sqlite-script.d.ts +26 -0
- package/dist/sqlite-script.d.ts.map +1 -0
- package/package.json +94 -3
- package/src/RxDBAdapterElectron.ts +105 -0
- package/src/electron-adapter.interface.ts +25 -0
- package/src/electron-file-host.ts +758 -0
- package/src/electron-sqlite-host.ts +301 -0
- package/src/host.ts +64 -0
- package/src/index.ts +71 -0
- package/src/node-sqlite-engine.ts +631 -0
- package/src/pglite/RxDBAdapterElectronPGlite.ts +152 -0
- package/src/pglite/desktop-pglite-client.ts +614 -0
- 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 +78 -0
- package/src/pglite/restore-electron-pglite-database.ts +429 -0
- package/src/pglite-host/electron-pglite-host.ts +829 -0
- 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 +50 -0
- package/src/pglite.ts +51 -0
- package/src/sqlite-script.ts +134 -0
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 桌面 PGlite host:把 renderer 送来的 `pg.*` 请求派发到主进程持有的 PGlite 实例。
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* 本模块运行在**特权侧**,是唯一接触 PGlite 实例的地方。形状由 US-208 线 G 冻结为
|
|
6
|
+
* 「IPC 事务 ID 协议」:主进程持连接,renderer 用 host 签发的事务 ID 把多次 IPC 调用
|
|
7
|
+
* 串成一条真事务。
|
|
8
|
+
*
|
|
9
|
+
* PGlite 只提供 callback 形态的事务(`transaction(cb)`,返回即 COMMIT、抛出即 ROLLBACK),
|
|
10
|
+
* 而 `cb` 跨不了 IPC。本模块的做法是**把 callback 挂起**:进入 `cb` 后立刻 `await` 一个
|
|
11
|
+
* 由 host 持有的 promise,事务因此在 PostgreSQL 侧一直开着,`tx` 句柄留在表里供后续请求
|
|
12
|
+
* 复用。这不是绕开 PGlite 的事务语义,恰恰是照它的语义用——每条语句都真的走同一个 `tx`,
|
|
13
|
+
* 没有任何一条被拆进隐式事务(AC#2 明令禁止把多条独立请求包装成假事务)。
|
|
14
|
+
*
|
|
15
|
+
* @module electron-pglite-host
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
DESKTOP_PGLITE_PROTOCOL_VERSION,
|
|
20
|
+
parseDesktopPgliteRequest,
|
|
21
|
+
RxDBAdapterDesktopError,
|
|
22
|
+
type DesktopPgliteBackupBeginRequest,
|
|
23
|
+
type DesktopPgliteBackupCursorRequest,
|
|
24
|
+
type DesktopPgliteDataDirItem,
|
|
25
|
+
type DesktopPgliteEngineRequest,
|
|
26
|
+
type DesktopPgliteEngineResult,
|
|
27
|
+
type DesktopPgliteNotifyMessage,
|
|
28
|
+
type DesktopPgliteRequest,
|
|
29
|
+
type DesktopPgliteResponse
|
|
30
|
+
} from '@aiao/rxdb-adapter-sqlite-core/desktop-host';
|
|
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';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 逻辑位置的 scheme。
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* `pg.open` 回给 renderer 的 `resolvedLocation` 用它拼装,只表达「应用作用域内的某个数据
|
|
60
|
+
* 目录」,不含物理根目录(AC#5)。与 SQLite 侧的 `desktop-sqlite://app-scope` 刻意不同名:
|
|
61
|
+
* 两者的名字空间互不相干,共用一个 scheme 会让日志里的两类位置看起来可以互相替换。
|
|
62
|
+
*/
|
|
63
|
+
const LOGICAL_LOCATION_SCHEME = 'desktop-pglite://app-scope';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* host 代 renderer 订阅的 NOTIFY 频道。
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* 与 `@aiao/rxdb-adapter-pglite` 里 `PGliteClient` 监听的三张系统表一一对应。
|
|
70
|
+
* PostgreSQL 只把 `LISTEN` 过的频道推给连接,因此这份清单漏一项就等于对应的响应式
|
|
71
|
+
* 查询在桌面下永远不刷新——而浏览器下一切正常,排查时几乎不会怀疑到订阅清单上。
|
|
72
|
+
*/
|
|
73
|
+
export const DESKTOP_PGLITE_WATCH_CHANNELS: readonly string[] = Object.freeze([
|
|
74
|
+
'rxdb_change_notify',
|
|
75
|
+
'rxdb_branch_notify',
|
|
76
|
+
'rxdb_migration_notify'
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 备份与恢复所需的宿主配置(US-217)。
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* 不传时 host 只提供连接:`pg.engine`、`pg.backup.*`、`pg.restore.*` 一律报 `unsupported_operation`,
|
|
84
|
+
* `pg.open` 也不取目录锁,与引入备份之前的行为逐项一致。传了之后每个打开的数据目录旁边都有一把
|
|
85
|
+
* 跨进程的目录锁:同一目录上的恢复与连接互斥,另一个进程里的 host 也不例外(AC#12)。
|
|
86
|
+
*/
|
|
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[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** {@link createElectronPgliteHost} 的入参。 */
|
|
117
|
+
export interface ElectronPgliteHostOptions {
|
|
118
|
+
/**
|
|
119
|
+
* 按逻辑数据目录名创建一个 PGlite 运行时。
|
|
120
|
+
*
|
|
121
|
+
* @remarks
|
|
122
|
+
* **必填,没有默认实现**:默认值只能是「在某处建一个 PGlite」,而那个「某处」只有宿主
|
|
123
|
+
* 应用知道(Electron 下通常在 `app.getPath('userData')` 之下)。给一个兜底目录意味着
|
|
124
|
+
* 数据会静默落在谁也没打算用的位置,而症状是「重启后数据没了」。
|
|
125
|
+
*
|
|
126
|
+
* 传进来的名字已过白名单校验,不含任何路径分隔符,因此 `join(root, name)` 不会越出 `root`。
|
|
127
|
+
* 同一个名字同一时刻至多只有一个活着的运行时(AC#7);恢复校验同样经它打开目标目录。
|
|
128
|
+
*/
|
|
129
|
+
readonly createRuntime: (dataDirectoryName: string) => Promise<ElectronPgliteRuntime>;
|
|
130
|
+
/** 把裸 NOTIFY 送达对应会话的 renderer,例如 `webContents.send`。 */
|
|
131
|
+
readonly postNotify: (message: DesktopPgliteNotifyMessage) => void;
|
|
132
|
+
/**
|
|
133
|
+
* NOTIFY 送达失败时的上报口。
|
|
134
|
+
*
|
|
135
|
+
* @remarks
|
|
136
|
+
* 与 SQLite host 同理:窗口在通知投递途中被销毁是常规竞态,此时写入早已落库,
|
|
137
|
+
* 把送达失败当成写失败回给调用方只会诱发一次重复写入。不传则丢弃。
|
|
138
|
+
*/
|
|
139
|
+
readonly onDeliveryError?: (error: unknown) => void;
|
|
140
|
+
/** 备份与恢复支持;不传则不提供(见 {@link ElectronPgliteBackupOptions})。 */
|
|
141
|
+
readonly backup?: ElectronPgliteBackupOptions;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 桌面 PGlite host 实例。 */
|
|
145
|
+
export interface ElectronPgliteHost {
|
|
146
|
+
/**
|
|
147
|
+
* 处理一条来自 renderer 的请求。
|
|
148
|
+
*
|
|
149
|
+
* @remarks
|
|
150
|
+
* **永不 reject**:失败以 `kind: 'error'` 的应答返回,理由与 SQLite host 一致
|
|
151
|
+
* (`ipcRenderer.invoke` 在 reject 时会把错误压平成字符串,错误码随之丢失)。
|
|
152
|
+
*
|
|
153
|
+
* @param request - 未经校验的请求负载
|
|
154
|
+
* @param ownerId - 发起方的 `webContents.id`;会话与事务都按它归属
|
|
155
|
+
* @returns 协议应答
|
|
156
|
+
*/
|
|
157
|
+
handle(request: unknown, ownerId: number): Promise<DesktopPgliteResponse>;
|
|
158
|
+
/** 当前打开的会话数。 */
|
|
159
|
+
readonly openSessionCount: number;
|
|
160
|
+
/** 当前活着的 PGlite 实例数;同一个数据目录上的多个会话只算一个,恢复校验用的私有实例不计入。 */
|
|
161
|
+
readonly openInstanceCount: number;
|
|
162
|
+
/** 当前挂起的事务数;正常静止时应为 0。 */
|
|
163
|
+
readonly openTransactionCount: number;
|
|
164
|
+
/** 当前进行中的备份快照数;快照同样占着连接,但不计入 `openTransactionCount`。 */
|
|
165
|
+
readonly openBackupCount: number;
|
|
166
|
+
/** 当前进行中的恢复数。 */
|
|
167
|
+
readonly openRestoreCount: number;
|
|
168
|
+
/**
|
|
169
|
+
* 回收某个窗口名下的全部会话、事务、快照与恢复。
|
|
170
|
+
*
|
|
171
|
+
* @remarks
|
|
172
|
+
* 这是本方案的**前提而非收尾**:挂起的 callback 独占 PGlite 的连接锁,渲染进程崩在
|
|
173
|
+
* 事务中间而没人回收的话,之后任何查询都会永远排队——表征是「数据库不响应」,
|
|
174
|
+
* 与那次崩溃毫无关联线索。调用方必须把它挂到 `render-process-gone` 与 `destroyed`
|
|
175
|
+
* 两个事件上(AC#3)。被打断的恢复保留恢复标记,之后的连接据此被拒绝(AC#11)。
|
|
176
|
+
*
|
|
177
|
+
* @param ownerId - 已崩溃或已销毁的 `webContents.id`
|
|
178
|
+
* @returns 本次回滚掉的事务条数
|
|
179
|
+
*/
|
|
180
|
+
releaseOwner(ownerId: number): Promise<number>;
|
|
181
|
+
/** 关闭全部会话与实例、打断全部恢复,通常在应用退出前调用。 */
|
|
182
|
+
closeAll(): Promise<void>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 一个已经起来的运行时,以及启用备份时它在数据目录上持有的锁。 */
|
|
186
|
+
interface StartedRuntime {
|
|
187
|
+
readonly runtime: ElectronPgliteRuntime;
|
|
188
|
+
readonly lock: PgliteDirectoryLock | undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 一个数据目录上的运行时及其会话。 */
|
|
192
|
+
interface InstanceEntry {
|
|
193
|
+
readonly sessions: Set<string>;
|
|
194
|
+
/** 正在等 `ready` 的 `pg.open` 数;不为 0 时最后一条会话关掉也不释放实例。 */
|
|
195
|
+
pending: number;
|
|
196
|
+
readonly ready: Promise<StartedRuntime>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 一条会话。 */
|
|
200
|
+
interface SessionEntry {
|
|
201
|
+
readonly dataDirectoryName: string;
|
|
202
|
+
readonly owner: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** 一条挂起中的事务。 */
|
|
206
|
+
type TransactionEntry = SuspendedTransaction & { readonly sessionId: string };
|
|
207
|
+
|
|
208
|
+
/** 一次进行中的备份快照。 */
|
|
209
|
+
interface BackupEntry {
|
|
210
|
+
readonly sessionId: string;
|
|
211
|
+
readonly iterator: AsyncGenerator<DesktopPgliteDataDirItem>;
|
|
212
|
+
/** 读快照期间占住连接的空事务:它不结束,任何语句都改不了文件(AC#16)。 */
|
|
213
|
+
readonly held: SuspendedTransaction;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 启用备份时的配置与状态。 */
|
|
217
|
+
interface BackupSupport {
|
|
218
|
+
readonly options: ElectronPgliteBackupOptions;
|
|
219
|
+
/** 去重并排序后的扩展名。 */
|
|
220
|
+
readonly extensions: readonly string[];
|
|
221
|
+
readonly restores: PgliteRestoreCoordinator;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** 协议 v2 新增、只在启用备份时可用的请求。 */
|
|
225
|
+
type DesktopPgliteBackupRequest =
|
|
226
|
+
| DesktopPgliteEngineRequest
|
|
227
|
+
| DesktopPgliteBackupBeginRequest
|
|
228
|
+
| DesktopPgliteBackupCursorRequest
|
|
229
|
+
| DesktopPgliteRestoreRequest;
|
|
230
|
+
|
|
231
|
+
const toErrorResponse = (error: unknown): DesktopPgliteResponse => {
|
|
232
|
+
if (error instanceof RxDBAdapterDesktopError) {
|
|
233
|
+
return { kind: 'error', code: error.code, message: error.detail };
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
kind: 'error',
|
|
237
|
+
code: 'host_internal_error',
|
|
238
|
+
message: error instanceof Error ? error.message : String(error)
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
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
|
+
);
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 创建一个桌面 PGlite host。
|
|
253
|
+
*
|
|
254
|
+
* @remarks
|
|
255
|
+
* 与 SQLite host 相反,同一个数据目录上的多个会话**共享一个** PGlite 实例(AC#7):
|
|
256
|
+
* PGlite 是嵌入式单写者,同一份数据目录被两个实例同时打开会直接损坏它。同一 host 内跨窗口的
|
|
257
|
+
* 并发因此由这一条连接的排队来串行化;启用备份时每个实例还在数据目录旁持有一把跨进程的
|
|
258
|
+
* 目录锁,恢复据此确认目标没有被任何进程连着(AC#12)。
|
|
259
|
+
*
|
|
260
|
+
* @param options - host 配置
|
|
261
|
+
* @returns host 实例
|
|
262
|
+
*/
|
|
263
|
+
export function createElectronPgliteHost(options: ElectronPgliteHostOptions): ElectronPgliteHost {
|
|
264
|
+
const instances = new Map<string, InstanceEntry>();
|
|
265
|
+
const sessions = new Map<string, SessionEntry>();
|
|
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
|
+
);
|
|
291
|
+
|
|
292
|
+
const deliver = (sessionId: string, channel: string, payload: string): void => {
|
|
293
|
+
try {
|
|
294
|
+
options.postNotify({ kind: 'pg.notify', sessionId, channel, payload });
|
|
295
|
+
} catch (error) {
|
|
296
|
+
options.onDeliveryError?.(error);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
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
|
+
|
|
366
|
+
const createInstance = (dataDirectoryName: string): InstanceEntry => {
|
|
367
|
+
const sessionIds = new Set<string>();
|
|
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
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const requireSession = (sessionId: string, ownerId: number): SessionEntry => {
|
|
398
|
+
const session = sessions.get(sessionId);
|
|
399
|
+
if (!session) {
|
|
400
|
+
throw new RxDBAdapterDesktopError('session_closed', `session ${sessionId} is not open on this host`);
|
|
401
|
+
}
|
|
402
|
+
if (session.owner !== ownerId) {
|
|
403
|
+
throw new RxDBAdapterDesktopError('permission_denied', `session ${sessionId} belongs to another window`);
|
|
404
|
+
}
|
|
405
|
+
return session;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const requireRuntime = async (session: SessionEntry): Promise<ElectronPgliteRuntime> => {
|
|
409
|
+
const instance = instances.get(session.dataDirectoryName);
|
|
410
|
+
if (!instance) {
|
|
411
|
+
throw new RxDBAdapterDesktopError(
|
|
412
|
+
'host_internal_error',
|
|
413
|
+
`session references a released instance for ${session.dataDirectoryName}`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return (await instance.ready).runtime;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 取一条挂起中的事务。
|
|
421
|
+
*
|
|
422
|
+
* @remarks
|
|
423
|
+
* 同时核对 `sessionId`:事务 ID 是随机 UUID,猜不到,但**会话已经关掉、事务 ID 却被
|
|
424
|
+
* 复用**这条路径是猜得到的。归属对不上时报 `transaction_not_found` 而不是别的码,
|
|
425
|
+
* 因为对调用方而言这条事务确实不存在——它的语句一条都没有执行。
|
|
426
|
+
*/
|
|
427
|
+
const requireTransaction = (transactionId: string, sessionId: string): TransactionEntry => {
|
|
428
|
+
const entry = transactions.get(transactionId);
|
|
429
|
+
if (!entry || entry.sessionId !== sessionId) {
|
|
430
|
+
throw new RxDBAdapterDesktopError(
|
|
431
|
+
'transaction_not_found',
|
|
432
|
+
`transaction ${transactionId} is unknown, already finished, or not owned by session ${sessionId}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
return entry;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* 结掉一条挂起的事务,返回**提交失败**的原因(成功或回滚时为 `undefined`)。
|
|
440
|
+
*
|
|
441
|
+
* @remarks
|
|
442
|
+
* 返回值只在 `failure === null` 那一路才可能有值,这个区分是本函数的全部要点:
|
|
443
|
+
* 回滚是靠 `settle.reject(failure)` 让挂起的 callback 抛出来实现的,所以回滚路径上
|
|
444
|
+
* `finished` **必然** reject——把它一并当成失败,每次正常回滚都会变成错误帧。
|
|
445
|
+
* 而提交路径上 `finished` 一旦 reject 就是 COMMIT 自己失败(延迟约束、磁盘写满等),
|
|
446
|
+
* 吞掉它意味着告诉调用方「写成功了」而数据库里什么都没有。
|
|
447
|
+
*/
|
|
448
|
+
const settle = async (transactionId: string, failure: Error | null): Promise<Error | undefined> => {
|
|
449
|
+
const entry = transactions.get(transactionId);
|
|
450
|
+
if (!entry) return undefined;
|
|
451
|
+
transactions.delete(transactionId);
|
|
452
|
+
if (failure) entry.settle.reject(failure);
|
|
453
|
+
else entry.settle.resolve();
|
|
454
|
+
// 必须等 `transaction(...)` 自己落地:不等的话 COMMIT 还在飞,紧接着的读可能看不到
|
|
455
|
+
// 刚写进去的数据,而那会被误读成「事务语义不成立」。
|
|
456
|
+
const commitError = await entry.finished.then(
|
|
457
|
+
() => undefined,
|
|
458
|
+
(error: unknown) => error
|
|
459
|
+
);
|
|
460
|
+
if (failure || commitError === undefined) return undefined;
|
|
461
|
+
if (commitError instanceof RxDBAdapterDesktopError) return commitError;
|
|
462
|
+
return new RxDBAdapterDesktopError(
|
|
463
|
+
'statement_failed',
|
|
464
|
+
commitError instanceof Error ? commitError.message : String(commitError),
|
|
465
|
+
{ cause: commitError }
|
|
466
|
+
);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const open = async (
|
|
470
|
+
request: Extract<DesktopPgliteRequest, { kind: 'pg.open' }>,
|
|
471
|
+
ownerId: number
|
|
472
|
+
): Promise<DesktopPgliteResponse> => {
|
|
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:并发的恢复要么已经占了名字,要么会看见这个实例。
|
|
479
|
+
const instance = instances.get(dataDirectoryName) ?? createInstance(dataDirectoryName);
|
|
480
|
+
instances.set(dataDirectoryName, instance);
|
|
481
|
+
instance.pending += 1;
|
|
482
|
+
try {
|
|
483
|
+
await instance.ready;
|
|
484
|
+
} catch (error) {
|
|
485
|
+
// 起不来的实例不能留在表里:留着的话下一次 `pg.open` 会拿到同一个已经 reject 的
|
|
486
|
+
// promise,于是「修好配置再试一次」永远不可能成功。
|
|
487
|
+
if (instances.get(dataDirectoryName) === instance) instances.delete(dataDirectoryName);
|
|
488
|
+
throw error;
|
|
489
|
+
} finally {
|
|
490
|
+
instance.pending -= 1;
|
|
491
|
+
}
|
|
492
|
+
const sessionId = randomUUID();
|
|
493
|
+
instance.sessions.add(sessionId);
|
|
494
|
+
sessions.set(sessionId, { dataDirectoryName, owner: ownerId });
|
|
495
|
+
return {
|
|
496
|
+
kind: 'pg.open',
|
|
497
|
+
result: {
|
|
498
|
+
sessionId,
|
|
499
|
+
resolvedLocation: `${LOGICAL_LOCATION_SCHEME}/${dataDirectoryName}`,
|
|
500
|
+
protocolVersion: DESKTOP_PGLITE_PROTOCOL_VERSION
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
/** 关掉一条会话;数据目录上最后一条会话消失、且没有正在加入的 `pg.open` 时连实例一起释放。 */
|
|
506
|
+
const closeSession = async (sessionId: string): Promise<void> => {
|
|
507
|
+
const session = sessions.get(sessionId);
|
|
508
|
+
if (!session) return;
|
|
509
|
+
sessions.delete(sessionId);
|
|
510
|
+
const instance = instances.get(session.dataDirectoryName);
|
|
511
|
+
if (!instance) return;
|
|
512
|
+
instance.sessions.delete(sessionId);
|
|
513
|
+
if (instance.sessions.size > 0 || instance.pending > 0) return;
|
|
514
|
+
await releaseInstance(session.dataDirectoryName, instance);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
/** 回滚一条会话名下全部挂起的事务,返回条数。 */
|
|
518
|
+
const rollbackSessionTransactions = async (sessionId: string, reason: string): Promise<number> => {
|
|
519
|
+
const doomed = [...transactions.entries()].filter(([, entry]) => entry.sessionId === sessionId).map(([id]) => id);
|
|
520
|
+
for (const transactionId of doomed) {
|
|
521
|
+
await settle(transactionId, new RxDBAdapterDesktopError('transaction_not_found', reason));
|
|
522
|
+
}
|
|
523
|
+
return doomed.length;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const begin = async (
|
|
527
|
+
request: Extract<DesktopPgliteRequest, { kind: 'pg.begin' }>,
|
|
528
|
+
ownerId: number
|
|
529
|
+
): Promise<DesktopPgliteResponse> => {
|
|
530
|
+
const runtime = await requireRuntime(requireSession(request.sessionId, ownerId));
|
|
531
|
+
const held = await suspendTransaction(runtime, request.timeout);
|
|
532
|
+
const transactionId = randomUUID();
|
|
533
|
+
transactions.set(transactionId, { ...held, sessionId: request.sessionId });
|
|
534
|
+
return { kind: 'pg.begin', result: { transactionId } };
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const query = async (
|
|
538
|
+
request: Extract<DesktopPgliteRequest, { kind: 'pg.query' }>,
|
|
539
|
+
ownerId: number
|
|
540
|
+
): Promise<DesktopPgliteResponse> => {
|
|
541
|
+
const session = requireSession(request.sessionId, ownerId);
|
|
542
|
+
const params = [...request.params];
|
|
543
|
+
if (request.transactionId !== undefined) {
|
|
544
|
+
const entry = requireTransaction(request.transactionId, request.sessionId);
|
|
545
|
+
return { kind: 'pg.query', result: toWireResult(await runStatement(() => entry.tx.query(request.sql, params))) };
|
|
546
|
+
}
|
|
547
|
+
const runtime = await requireRuntime(session);
|
|
548
|
+
return { kind: 'pg.query', result: toWireResult(await runStatement(() => runtime.query(request.sql, params))) };
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const exec = async (
|
|
552
|
+
request: Extract<DesktopPgliteRequest, { kind: 'pg.exec' }>,
|
|
553
|
+
ownerId: number
|
|
554
|
+
): Promise<DesktopPgliteResponse> => {
|
|
555
|
+
const session = requireSession(request.sessionId, ownerId);
|
|
556
|
+
const target =
|
|
557
|
+
request.transactionId === undefined ?
|
|
558
|
+
await requireRuntime(session)
|
|
559
|
+
: requireTransaction(request.transactionId, request.sessionId).tx;
|
|
560
|
+
const results = await runStatement(() => target.exec(request.sql));
|
|
561
|
+
return { kind: 'pg.exec', result: results.map(toWireResult) };
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const end = async (
|
|
565
|
+
request: Extract<DesktopPgliteRequest, { kind: 'pg.commit' | 'pg.rollback' }>,
|
|
566
|
+
ownerId: number
|
|
567
|
+
): Promise<DesktopPgliteResponse> => {
|
|
568
|
+
requireSession(request.sessionId, ownerId);
|
|
569
|
+
requireTransaction(request.transactionId, request.sessionId);
|
|
570
|
+
const failure =
|
|
571
|
+
request.kind === 'pg.rollback' ? new RxDBAdapterDesktopError('write_aborted', 'rollback requested') : null;
|
|
572
|
+
const commitError = await settle(request.transactionId, failure);
|
|
573
|
+
// COMMIT 失败时事务已经被 PostgreSQL 回滚干净,这里只需把原因如实回给调用方——
|
|
574
|
+
// 无条件回成功等于让上层把「一条都没写进去」当成「写完了」。
|
|
575
|
+
if (commitError) throw commitError;
|
|
576
|
+
return { kind: request.kind };
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const version = async (
|
|
580
|
+
request: Extract<DesktopPgliteRequest, { kind: 'pg.version' }>,
|
|
581
|
+
ownerId: number
|
|
582
|
+
): Promise<DesktopPgliteResponse> => {
|
|
583
|
+
const runtime = await requireRuntime(requireSession(request.sessionId, ownerId));
|
|
584
|
+
const result = await runStatement(() => runtime.query('SELECT version() AS version'));
|
|
585
|
+
const value = result.rows[0]?.['version'];
|
|
586
|
+
if (typeof value !== 'string') {
|
|
587
|
+
throw new RxDBAdapterDesktopError('host_internal_error', 'PostgreSQL did not report a version string');
|
|
588
|
+
}
|
|
589
|
+
return { kind: 'pg.version', result: value };
|
|
590
|
+
};
|
|
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
|
+
|
|
743
|
+
const close = async (
|
|
744
|
+
request: Extract<DesktopPgliteRequest, { kind: 'pg.close' }>,
|
|
745
|
+
ownerId: number
|
|
746
|
+
): Promise<DesktopPgliteResponse> => {
|
|
747
|
+
requireSession(request.sessionId, ownerId);
|
|
748
|
+
await teardownSession(request.sessionId, `session ${request.sessionId} was closed`);
|
|
749
|
+
return { kind: 'pg.close' };
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* 按种类派发一条已通过协议校验的请求。
|
|
754
|
+
*
|
|
755
|
+
* @remarks
|
|
756
|
+
* 协议加了新种类而这里忘记补分支时,`default` 把它交给 `dispatchBackup`,后者的参数类型
|
|
757
|
+
* 不接受它,于是在 `tsc` 阶段就红,而不是在运行期变成怪异失败——与 SQLite host 的穷尽
|
|
758
|
+
* `switch` 同一个目的。
|
|
759
|
+
*/
|
|
760
|
+
const dispatch = (request: DesktopPgliteRequest, ownerId: number): Promise<DesktopPgliteResponse> => {
|
|
761
|
+
switch (request.kind) {
|
|
762
|
+
// 握手排在最前,且不碰会话表、不碰 `createRuntime`:它的全部意义就是让 renderer
|
|
763
|
+
// 在建目录之前把版本对上。这里一旦有任何副作用,磁盘上就会多出一棵 initdb 目录树,
|
|
764
|
+
// 而那时调用方连收拾它的把手都没有(AC#11)。
|
|
765
|
+
case 'pg.handshake':
|
|
766
|
+
return Promise.resolve({
|
|
767
|
+
kind: 'pg.handshake',
|
|
768
|
+
result: { protocolVersion: DESKTOP_PGLITE_PROTOCOL_VERSION }
|
|
769
|
+
});
|
|
770
|
+
case 'pg.open':
|
|
771
|
+
return open(request, ownerId);
|
|
772
|
+
case 'pg.query':
|
|
773
|
+
return query(request, ownerId);
|
|
774
|
+
case 'pg.exec':
|
|
775
|
+
return exec(request, ownerId);
|
|
776
|
+
case 'pg.begin':
|
|
777
|
+
return begin(request, ownerId);
|
|
778
|
+
case 'pg.commit':
|
|
779
|
+
case 'pg.rollback':
|
|
780
|
+
return end(request, ownerId);
|
|
781
|
+
case 'pg.version':
|
|
782
|
+
return version(request, ownerId);
|
|
783
|
+
case 'pg.close':
|
|
784
|
+
return close(request, ownerId);
|
|
785
|
+
default:
|
|
786
|
+
return dispatchBackup(request, ownerId);
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
handle: async (request: unknown, ownerId: number): Promise<DesktopPgliteResponse> => {
|
|
792
|
+
try {
|
|
793
|
+
return await dispatch(parseDesktopPgliteRequest(request), ownerId);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
return toErrorResponse(error);
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
get openSessionCount(): number {
|
|
799
|
+
return sessions.size;
|
|
800
|
+
},
|
|
801
|
+
get openInstanceCount(): number {
|
|
802
|
+
return instances.size;
|
|
803
|
+
},
|
|
804
|
+
get openTransactionCount(): number {
|
|
805
|
+
return transactions.size;
|
|
806
|
+
},
|
|
807
|
+
get openBackupCount(): number {
|
|
808
|
+
return backups.size;
|
|
809
|
+
},
|
|
810
|
+
get openRestoreCount(): number {
|
|
811
|
+
return support === undefined ? 0 : support.restores.openCount;
|
|
812
|
+
},
|
|
813
|
+
releaseOwner: async (ownerId: number): Promise<number> => {
|
|
814
|
+
const doomed = [...sessions.entries()].filter(([, session]) => session.owner === ownerId).map(([id]) => id);
|
|
815
|
+
let rolledBack = 0;
|
|
816
|
+
for (const sessionId of doomed) {
|
|
817
|
+
rolledBack += await teardownSession(sessionId, `owner ${ownerId} is gone`);
|
|
818
|
+
}
|
|
819
|
+
await support?.restores.releaseOwner(ownerId);
|
|
820
|
+
return rolledBack;
|
|
821
|
+
},
|
|
822
|
+
closeAll: async (): Promise<void> => {
|
|
823
|
+
for (const sessionId of [...sessions.keys()]) {
|
|
824
|
+
await teardownSession(sessionId, 'the host is shutting down');
|
|
825
|
+
}
|
|
826
|
+
await support?.restores.closeAll();
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|