@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
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host 侧 PGlite 数据目录的读写工具(US-217)。
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* 备份时逐块读出一棵数据目录树,恢复时判定目标是否为空、落盘与清理。遍历格式与浏览器端
|
|
6
|
+
* `walkEmscriptenDataDir` 逐项一致:相对路径以 `/` 连接、不含根条目,子项按码点排序,
|
|
7
|
+
* 父目录总在子项之前,运行态文件(`postmaster.pid` 等)在任何深度都不产出。
|
|
8
|
+
*
|
|
9
|
+
* @module pglite-host/pglite-host-data-dir
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
DESKTOP_PGLITE_EXCLUDED_FILES,
|
|
14
|
+
DESKTOP_PGLITE_MAX_DATA_CHUNK_BYTES,
|
|
15
|
+
RxDBAdapterDesktopError,
|
|
16
|
+
type DesktopPgliteDataDirItem,
|
|
17
|
+
type RxDBAdapterDesktopErrorCode
|
|
18
|
+
} from '@aiao/rxdb-adapter-sqlite-core/desktop-host';
|
|
19
|
+
import { lstat, open, readdir, rm } from 'node:fs/promises';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
|
+
|
|
22
|
+
/** 文件系统错误码到桌面错误码;表外的一律按 host 自身故障上报,`cause` 保留原始错误。 */
|
|
23
|
+
const IO_ERROR_CODES = new Map<string, RxDBAdapterDesktopErrorCode>([
|
|
24
|
+
['ENOSPC', 'disk_full'],
|
|
25
|
+
['EDQUOT', 'disk_full'],
|
|
26
|
+
['EACCES', 'permission_denied'],
|
|
27
|
+
['EPERM', 'permission_denied'],
|
|
28
|
+
['EROFS', 'permission_denied'],
|
|
29
|
+
['ENOENT', 'file_not_found']
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 取 Node 文件系统错误上的 errno 名(`ENOENT` 等)。
|
|
34
|
+
*
|
|
35
|
+
* @param error - 原始错误
|
|
36
|
+
* @returns errno 名;不是 errno 形状的错误时为 `undefined`
|
|
37
|
+
*/
|
|
38
|
+
export const errnoOf = (error: unknown): string | undefined => {
|
|
39
|
+
const code = (error as { code?: unknown } | null | undefined)?.code;
|
|
40
|
+
return typeof code === 'string' ? code : undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 把文件系统错误归一成桌面错误。
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* 已经是桌面错误的原样透传:遍历中途抛出的「文件变短」之类的判定比外层的笼统描述更准。
|
|
48
|
+
* 磁盘写满必须单独成码,renderer 据此报 `storage_full`,调用方才知道腾出空间后重试即可。
|
|
49
|
+
*
|
|
50
|
+
* @param error - 原始错误
|
|
51
|
+
* @param detail - 面向开发者的描述
|
|
52
|
+
* @returns 桌面错误
|
|
53
|
+
*/
|
|
54
|
+
export const toHostIoError = (error: unknown, detail: string): RxDBAdapterDesktopError => {
|
|
55
|
+
if (error instanceof RxDBAdapterDesktopError) return error;
|
|
56
|
+
const code = IO_ERROR_CODES.get(errnoOf(error) ?? '') ?? 'host_internal_error';
|
|
57
|
+
return new RxDBAdapterDesktopError(code, detail, { cause: error });
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const childNames = async (directory: string): Promise<string[]> =>
|
|
61
|
+
(await readdir(directory)).filter(name => !DESKTOP_PGLITE_EXCLUDED_FILES.has(name)).sort();
|
|
62
|
+
|
|
63
|
+
async function* readFileChunks(file: string, size: number, path: string): AsyncGenerator<DesktopPgliteDataDirItem> {
|
|
64
|
+
const handle = await open(file, 'r');
|
|
65
|
+
try {
|
|
66
|
+
let position = 0;
|
|
67
|
+
while (position < size) {
|
|
68
|
+
// 每块独占一个恰好大小的 ArrayBuffer:结构化克隆会把整个 backing buffer 拷过进程边界,
|
|
69
|
+
// 复用一块 64K 缓冲再切片出去,一个几字节的尾块也要搬满 64K(AC#9、AC#21)。
|
|
70
|
+
const bytes = new Uint8Array(Math.min(DESKTOP_PGLITE_MAX_DATA_CHUNK_BYTES, size - position));
|
|
71
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, position);
|
|
72
|
+
if (bytesRead <= 0) {
|
|
73
|
+
throw new RxDBAdapterDesktopError('host_internal_error', `data file shrank while being read: ${path}`);
|
|
74
|
+
}
|
|
75
|
+
yield { type: 'data', bytes: bytesRead === bytes.length ? bytes : bytes.slice(0, bytesRead) };
|
|
76
|
+
position += bytesRead;
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
await handle.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function* walk(directory: string, prefix: string): AsyncGenerator<DesktopPgliteDataDirItem> {
|
|
84
|
+
for (const name of await childNames(directory)) {
|
|
85
|
+
const full = join(directory, name);
|
|
86
|
+
const path = prefix === '' ? name : `${prefix}/${name}`;
|
|
87
|
+
const stats = await lstat(full);
|
|
88
|
+
if (stats.isDirectory()) {
|
|
89
|
+
yield { type: 'entry', header: { path, kind: 'directory', size: 0 } };
|
|
90
|
+
yield* walk(full, path);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!stats.isFile()) {
|
|
94
|
+
throw new RxDBAdapterDesktopError('host_internal_error', `unsupported file type in the data directory: ${path}`);
|
|
95
|
+
}
|
|
96
|
+
yield { type: 'entry', header: { path, kind: 'file', size: stats.size } };
|
|
97
|
+
yield* readFileChunks(full, stats.size, path);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 按确定顺序逐块读出一个 PGlite 数据目录。
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* 调用方必须已经占住该目录上唯一的运行时连接并做完 `CHECKPOINT`:遍历期间没有语句能改动文件,
|
|
106
|
+
* 目录树因此就是一个一致快照。同一时刻只有一块数据在途,与库大小无关;消费方不取下一项,
|
|
107
|
+
* 就不会读下一块。提前结束(`return()`)时当前文件句柄在 `finally` 里关闭。
|
|
108
|
+
*
|
|
109
|
+
* @param directory - 数据目录的物理路径
|
|
110
|
+
* @returns 数据目录各项;每个数据块独占自己的 `ArrayBuffer`
|
|
111
|
+
* @throws {@link RxDBAdapterDesktopError} `disk_full` / `permission_denied` / `file_not_found` / `host_internal_error`
|
|
112
|
+
*/
|
|
113
|
+
export async function* walkPgliteDataDirectory(directory: string): AsyncGenerator<DesktopPgliteDataDirItem> {
|
|
114
|
+
try {
|
|
115
|
+
yield* walk(directory, '');
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw toHostIoError(error, 'failed to read the PGlite data directory');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 判定恢复目标是否可写:不存在,或是一个空目录。
|
|
123
|
+
*
|
|
124
|
+
* @remarks
|
|
125
|
+
* 同名的普通文件算「不空」而不是 I/O 故障:它同样意味着那里已经有别的东西,恢复不能覆盖。
|
|
126
|
+
*
|
|
127
|
+
* @param directory - 数据目录的物理路径
|
|
128
|
+
* @returns 可以在此恢复时为 `true`
|
|
129
|
+
* @throws {@link RxDBAdapterDesktopError} 目录读不出来时
|
|
130
|
+
*/
|
|
131
|
+
export const isEmptyOrMissing = async (directory: string): Promise<boolean> => {
|
|
132
|
+
try {
|
|
133
|
+
return (await readdir(directory)).length === 0;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const errno = errnoOf(error);
|
|
136
|
+
if (errno === 'ENOENT') return true;
|
|
137
|
+
if (errno === 'ENOTDIR') return false;
|
|
138
|
+
throw toHostIoError(error, 'failed to inspect the restore target');
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 数据目录里是否有 `PG_VERSION`。
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* PGlite 打开一个没有 `PG_VERSION` 的目录时会**就地 initdb**。恢复校验前必须先确认它在,
|
|
147
|
+
* 否则一份缺了它的归档会被悄悄换成一个全新的空库,然后以「系统表水位不符」之类不相干的
|
|
148
|
+
* 原因失败,甚至通过校验。
|
|
149
|
+
*
|
|
150
|
+
* @param directory - 数据目录的物理路径
|
|
151
|
+
* @returns 存在且是普通文件时为 `true`
|
|
152
|
+
*/
|
|
153
|
+
export const hasPgVersion = async (directory: string): Promise<boolean> => {
|
|
154
|
+
try {
|
|
155
|
+
return (await lstat(join(directory, 'PG_VERSION'))).isFile();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (errnoOf(error) === 'ENOENT') return false;
|
|
158
|
+
throw toHostIoError(error, 'failed to inspect the PGlite data directory');
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const syncHandle = async (path: string, flags: string): Promise<void> => {
|
|
163
|
+
const handle = await open(path, flags);
|
|
164
|
+
try {
|
|
165
|
+
await handle.sync();
|
|
166
|
+
} finally {
|
|
167
|
+
await handle.close();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 目录项的持久化。
|
|
173
|
+
*
|
|
174
|
+
* @remarks
|
|
175
|
+
* Windows 上目录打不开成可 fsync 的句柄,NTFS 的元数据日志随文件的 flush 一并落盘,跳过即可。
|
|
176
|
+
*/
|
|
177
|
+
const syncDirectory = async (directory: string): Promise<void> => {
|
|
178
|
+
if (process.platform === 'win32') return;
|
|
179
|
+
await syncHandle(directory, 'r');
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const syncTreeEntries = async (directory: string): Promise<void> => {
|
|
183
|
+
for (const name of await readdir(directory)) {
|
|
184
|
+
const full = join(directory, name);
|
|
185
|
+
if ((await lstat(full)).isDirectory()) await syncTreeEntries(full);
|
|
186
|
+
// `r+` 而不是 `r`:Windows 的 FlushFileBuffers 要求句柄带写权限。
|
|
187
|
+
else await syncHandle(full, 'r+');
|
|
188
|
+
}
|
|
189
|
+
await syncDirectory(directory);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 把整棵数据目录(连同它在父目录里的目录项)逐一落盘。
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* 恢复标记只能在这一步完成之后删除:先删标记再掉电,重启后看到的是一个「没有标记、
|
|
197
|
+
* 却缺了几页」的库,与完整恢复无从区分(AC#11)。
|
|
198
|
+
*
|
|
199
|
+
* @param directory - 数据目录的物理路径
|
|
200
|
+
* @throws {@link RxDBAdapterDesktopError} 落盘失败时
|
|
201
|
+
*/
|
|
202
|
+
export const syncPgliteDataDirectory = async (directory: string): Promise<void> => {
|
|
203
|
+
try {
|
|
204
|
+
await syncTreeEntries(directory);
|
|
205
|
+
await syncDirectory(dirname(directory));
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw toHostIoError(error, 'failed to persist the restored PGlite data directory');
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 删除一棵数据目录;不存在时视为已删除。
|
|
213
|
+
*
|
|
214
|
+
* @param directory - 数据目录的物理路径
|
|
215
|
+
* @throws {@link RxDBAdapterDesktopError} 删除失败时
|
|
216
|
+
*/
|
|
217
|
+
export const removePgliteDataDirectory = async (directory: string): Promise<void> => {
|
|
218
|
+
try {
|
|
219
|
+
// 重试针对 Windows:杀毒软件与索引服务会短暂占住刚关闭的文件,第一次删常以 EBUSY 失败。
|
|
220
|
+
await rm(directory, { recursive: true, force: true, maxRetries: 3 });
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw toHostIoError(error, 'failed to remove the PGlite data directory');
|
|
223
|
+
}
|
|
224
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PGlite 数据目录的跨进程独占锁与恢复标记(US-217)。
|
|
3
|
+
*
|
|
4
|
+
* @module pglite-host/pglite-host-lock
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { RxDBAdapterDesktopError, type RxDBAdapterDesktopErrorCode } from '@aiao/rxdb-adapter-sqlite-core/desktop-host';
|
|
8
|
+
import { mkdirSync } from 'node:fs';
|
|
9
|
+
import { basename, dirname, join } from 'node:path';
|
|
10
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
11
|
+
import { toHostIoError } from './pglite-host-data-dir.js';
|
|
12
|
+
|
|
13
|
+
/** 一个已持有的数据目录锁。 */
|
|
14
|
+
export interface PgliteDirectoryLock {
|
|
15
|
+
/** 目录上是否留有未完成恢复的标记。 */
|
|
16
|
+
hasMarker(): boolean;
|
|
17
|
+
/** 持久化恢复标记;返回时标记已经落盘。 */
|
|
18
|
+
markRestoring(): void;
|
|
19
|
+
/** 删除恢复标记;返回时删除已经落盘。 */
|
|
20
|
+
clearRestoring(): void;
|
|
21
|
+
/** 释放锁;重复调用无副作用。 */
|
|
22
|
+
release(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 数据目录对应的锁文件路径:与数据目录同级的隐藏文件。
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* 库名不能以 `.` 开头({@link assertValidDesktopDatabaseName}),锁文件名因此不会与任何库名撞车;
|
|
30
|
+
* 放在数据目录外面,恢复删除整个目录时锁与标记都还在。
|
|
31
|
+
*
|
|
32
|
+
* @param directory - 数据目录的物理路径
|
|
33
|
+
* @returns 锁文件的物理路径
|
|
34
|
+
*/
|
|
35
|
+
export const pgliteLockPathOf = (directory: string): string =>
|
|
36
|
+
join(dirname(directory), `.${basename(directory)}.rxdb-lock`);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 取锁的脚本。
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* - `busy_timeout = 0`:撞锁立刻失败。`node:sqlite` 是同步接口,等锁会卡住主进程的整条线程。
|
|
43
|
+
* - `locking_mode = EXCLUSIVE` + `journal_mode = DELETE`:第一个写事务拿到的 EXCLUSIVE 锁一直持有到
|
|
44
|
+
* 连接关闭,别的连接连读库头都做不到。撞锁发生在 `journal_mode` 这一步——它要读库头。
|
|
45
|
+
* - `synchronous = FULL`:标记的写入与删除在返回前落盘,掉电后看到的一定是最后一次提交的结果。
|
|
46
|
+
*/
|
|
47
|
+
const ACQUIRE_SQL = `
|
|
48
|
+
PRAGMA busy_timeout = 0;
|
|
49
|
+
PRAGMA locking_mode = EXCLUSIVE;
|
|
50
|
+
PRAGMA journal_mode = DELETE;
|
|
51
|
+
PRAGMA synchronous = FULL;
|
|
52
|
+
BEGIN EXCLUSIVE;
|
|
53
|
+
CREATE TABLE IF NOT EXISTS holder (id INTEGER PRIMARY KEY, pid INTEGER NOT NULL);
|
|
54
|
+
CREATE TABLE IF NOT EXISTS restore_marker (id INTEGER PRIMARY KEY, created_at TEXT NOT NULL);
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
/** SQLite 主结果码 → 桌面错误码;扩展码先取低 8 位。 */
|
|
58
|
+
const LOCK_ERROR_CODES = new Map<number, RxDBAdapterDesktopErrorCode>([
|
|
59
|
+
[3, 'permission_denied'], // SQLITE_PERM
|
|
60
|
+
[5, 'database_busy'], // SQLITE_BUSY
|
|
61
|
+
[6, 'database_busy'], // SQLITE_LOCKED
|
|
62
|
+
[8, 'permission_denied'], // SQLITE_READONLY
|
|
63
|
+
[13, 'disk_full'], // SQLITE_FULL
|
|
64
|
+
[23, 'permission_denied'] // SQLITE_AUTH
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const toLockError = (
|
|
68
|
+
error: unknown,
|
|
69
|
+
fallback: RxDBAdapterDesktopErrorCode,
|
|
70
|
+
detail: string
|
|
71
|
+
): RxDBAdapterDesktopError => {
|
|
72
|
+
const errcode = (error as { errcode?: unknown } | null | undefined)?.errcode;
|
|
73
|
+
const code = typeof errcode === 'number' ? LOCK_ERROR_CODES.get(errcode & 0xff) : undefined;
|
|
74
|
+
return new RxDBAdapterDesktopError(code ?? fallback, detail, { cause: error });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const openLockDatabase = (path: string, directory: string): DatabaseSync => {
|
|
78
|
+
try {
|
|
79
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw toHostIoError(error, 'failed to create the parent of the PGlite data directory');
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return new DatabaseSync(path);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw toLockError(
|
|
87
|
+
error,
|
|
88
|
+
'open_failed',
|
|
89
|
+
`failed to open the lock of PGlite data directory "${basename(directory)}"`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 取得一个 PGlite 数据目录的独占使用权。
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* Node 上的 PGlite(NODEFS)自己没有任何跨进程锁:两个进程打开同一个数据目录会各自写坏对方。
|
|
99
|
+
* 这里借 SQLite 的文件锁(POSIX `fcntl` / Windows `LockFileEx`)实现独占:
|
|
100
|
+
*
|
|
101
|
+
* - 锁由操作系统持有,进程被强杀时随之释放,不会像 pid 文件那样留下需要人工判断的陈旧锁,
|
|
102
|
+
* 也不怕 pid 被复用(AC#11);
|
|
103
|
+
* - 同一进程内的第二条连接同样撞锁,同一 host 里的重复打开也被挡住;
|
|
104
|
+
* - 恢复标记与锁在同一个文件里,写入在拿锁之后、提交即落盘,任何改动目标的动作都排在它后面。
|
|
105
|
+
*
|
|
106
|
+
* 锁文件本身从不删除:删掉一个仍被别的进程锁着的文件,后来者会在新文件上拿到「独占」,锁就裂成两把。
|
|
107
|
+
*
|
|
108
|
+
* @param directory - 数据目录的物理路径;不要求存在,其父目录不存在时会被创建
|
|
109
|
+
* @returns 已持有的锁
|
|
110
|
+
* @throws {@link RxDBAdapterDesktopError} `database_busy`:别的连接(其他进程或同一进程内)正持有该目录;
|
|
111
|
+
* `permission_denied` / `disk_full` / `open_failed`:锁文件无法创建或写入
|
|
112
|
+
*/
|
|
113
|
+
export const acquirePgliteDirectoryLock = (directory: string): PgliteDirectoryLock => {
|
|
114
|
+
const db = openLockDatabase(pgliteLockPathOf(directory), directory);
|
|
115
|
+
try {
|
|
116
|
+
db.exec(ACQUIRE_SQL);
|
|
117
|
+
db.prepare('INSERT OR REPLACE INTO holder (id, pid) VALUES (1, ?)').run(process.pid);
|
|
118
|
+
db.exec('COMMIT');
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// 关闭连接会回滚未提交的取锁事务并释放已拿到的文件锁
|
|
121
|
+
db.close();
|
|
122
|
+
throw toLockError(error, 'open_failed', `failed to lock PGlite data directory "${basename(directory)}"`);
|
|
123
|
+
}
|
|
124
|
+
let released = false;
|
|
125
|
+
const markerIo = <T>(run: () => T, detail: string): T => {
|
|
126
|
+
try {
|
|
127
|
+
return run();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw toLockError(error, 'host_internal_error', detail);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
hasMarker: () =>
|
|
134
|
+
markerIo(
|
|
135
|
+
() => db.prepare('SELECT 1 FROM restore_marker LIMIT 1').get() !== undefined,
|
|
136
|
+
'failed to read the restore marker'
|
|
137
|
+
),
|
|
138
|
+
markRestoring: () =>
|
|
139
|
+
markerIo(() => {
|
|
140
|
+
db.prepare('INSERT OR REPLACE INTO restore_marker (id, created_at) VALUES (1, ?)').run(
|
|
141
|
+
new Date().toISOString()
|
|
142
|
+
);
|
|
143
|
+
}, 'failed to persist the restore marker'),
|
|
144
|
+
clearRestoring: () =>
|
|
145
|
+
markerIo(() => {
|
|
146
|
+
db.exec('DELETE FROM restore_marker');
|
|
147
|
+
}, 'failed to remove the restore marker'),
|
|
148
|
+
release: () => {
|
|
149
|
+
if (released) return;
|
|
150
|
+
released = true;
|
|
151
|
+
db.close();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
};
|