@cicctencent/json-orm 0.1.1
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 +154 -0
- package/dist/cjs/index.d.ts +25 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +66 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/json-backend-fs.d.ts +26 -0
- package/dist/cjs/json-backend-fs.d.ts.map +1 -0
- package/dist/cjs/json-backend-fs.js +278 -0
- package/dist/cjs/json-backend-fs.js.map +1 -0
- package/dist/cjs/json-collection.d.ts +70 -0
- package/dist/cjs/json-collection.d.ts.map +1 -0
- package/dist/cjs/json-collection.js +385 -0
- package/dist/cjs/json-collection.js.map +1 -0
- package/dist/cjs/json-config.d.ts +87 -0
- package/dist/cjs/json-config.d.ts.map +1 -0
- package/dist/cjs/json-config.js +77 -0
- package/dist/cjs/json-config.js.map +1 -0
- package/dist/cjs/json-datasource.d.ts +35 -0
- package/dist/cjs/json-datasource.d.ts.map +1 -0
- package/dist/cjs/json-datasource.js +131 -0
- package/dist/cjs/json-datasource.js.map +1 -0
- package/dist/cjs/json-entity-meta.d.ts +42 -0
- package/dist/cjs/json-entity-meta.d.ts.map +1 -0
- package/dist/cjs/json-entity-meta.js +128 -0
- package/dist/cjs/json-entity-meta.js.map +1 -0
- package/dist/cjs/json-query-builder.d.ts +83 -0
- package/dist/cjs/json-query-builder.d.ts.map +1 -0
- package/dist/cjs/json-query-builder.js +477 -0
- package/dist/cjs/json-query-builder.js.map +1 -0
- package/dist/cjs/json-repository.d.ts +59 -0
- package/dist/cjs/json-repository.d.ts.map +1 -0
- package/dist/cjs/json-repository.js +309 -0
- package/dist/cjs/json-repository.js.map +1 -0
- package/dist/cjs/json-sql-expr.d.ts +107 -0
- package/dist/cjs/json-sql-expr.d.ts.map +1 -0
- package/dist/cjs/json-sql-expr.js +857 -0
- package/dist/cjs/json-sql-expr.js.map +1 -0
- package/dist/cjs/json-sql-select.d.ts +9 -0
- package/dist/cjs/json-sql-select.d.ts.map +1 -0
- package/dist/cjs/json-sql-select.js +434 -0
- package/dist/cjs/json-sql-select.js.map +1 -0
- package/dist/cjs/json-sql-shared.d.ts +16 -0
- package/dist/cjs/json-sql-shared.d.ts.map +1 -0
- package/dist/cjs/json-sql-shared.js +12 -0
- package/dist/cjs/json-sql-shared.js.map +1 -0
- package/dist/cjs/json-sql.d.ts +8 -0
- package/dist/cjs/json-sql.d.ts.map +1 -0
- package/dist/cjs/json-sql.js +322 -0
- package/dist/cjs/json-sql.js.map +1 -0
- package/dist/cjs/json-where.d.ts +54 -0
- package/dist/cjs/json-where.d.ts.map +1 -0
- package/dist/cjs/json-where.js +352 -0
- package/dist/cjs/json-where.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/esm/index.d.ts +25 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/json-backend-fs.d.ts +26 -0
- package/dist/esm/json-backend-fs.d.ts.map +1 -0
- package/dist/esm/json-backend-fs.js +241 -0
- package/dist/esm/json-backend-fs.js.map +1 -0
- package/dist/esm/json-collection.d.ts +70 -0
- package/dist/esm/json-collection.d.ts.map +1 -0
- package/dist/esm/json-collection.js +346 -0
- package/dist/esm/json-collection.js.map +1 -0
- package/dist/esm/json-config.d.ts +87 -0
- package/dist/esm/json-config.d.ts.map +1 -0
- package/dist/esm/json-config.js +69 -0
- package/dist/esm/json-config.js.map +1 -0
- package/dist/esm/json-datasource.d.ts +35 -0
- package/dist/esm/json-datasource.d.ts.map +1 -0
- package/dist/esm/json-datasource.js +123 -0
- package/dist/esm/json-datasource.js.map +1 -0
- package/dist/esm/json-entity-meta.d.ts +42 -0
- package/dist/esm/json-entity-meta.d.ts.map +1 -0
- package/dist/esm/json-entity-meta.js +120 -0
- package/dist/esm/json-entity-meta.js.map +1 -0
- package/dist/esm/json-query-builder.d.ts +83 -0
- package/dist/esm/json-query-builder.d.ts.map +1 -0
- package/dist/esm/json-query-builder.js +473 -0
- package/dist/esm/json-query-builder.js.map +1 -0
- package/dist/esm/json-repository.d.ts +59 -0
- package/dist/esm/json-repository.d.ts.map +1 -0
- package/dist/esm/json-repository.js +304 -0
- package/dist/esm/json-repository.js.map +1 -0
- package/dist/esm/json-sql-expr.d.ts +107 -0
- package/dist/esm/json-sql-expr.d.ts.map +1 -0
- package/dist/esm/json-sql-expr.js +847 -0
- package/dist/esm/json-sql-expr.js.map +1 -0
- package/dist/esm/json-sql-select.d.ts +9 -0
- package/dist/esm/json-sql-select.d.ts.map +1 -0
- package/dist/esm/json-sql-select.js +429 -0
- package/dist/esm/json-sql-select.js.map +1 -0
- package/dist/esm/json-sql-shared.d.ts +16 -0
- package/dist/esm/json-sql-shared.d.ts.map +1 -0
- package/dist/esm/json-sql-shared.js +9 -0
- package/dist/esm/json-sql-shared.js.map +1 -0
- package/dist/esm/json-sql.d.ts +8 -0
- package/dist/esm/json-sql.d.ts.map +1 -0
- package/dist/esm/json-sql.js +319 -0
- package/dist/esm/json-sql.js.map +1 -0
- package/dist/esm/json-where.d.ts +54 -0
- package/dist/esm/json-where.d.ts.map +1 -0
- package/dist/esm/json-where.js +342 -0
- package/dist/esm/json-where.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/examples/01-basic-entity.ts +89 -0
- package/examples/02-query-builder.ts +114 -0
- package/examples/03-raw-sql.ts +105 -0
- package/examples/04-non-entity-table.ts +70 -0
- package/examples/05-custom-backend.ts +145 -0
- package/examples/06-find-operators.ts +75 -0
- package/examples/README.md +73 -0
- package/examples/tsconfig.json +20 -0
- package/package.json +53 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认存储后端:本地 JSON 文件(fs/promises)
|
|
3
|
+
*
|
|
4
|
+
* 语义等同改造前的本地落盘:`<根>/<表>/<主键>.json`,tmp+rename 原子写;
|
|
5
|
+
* 序列器与锁文件位于 `<根>/_meta/<表>.json`(迁出表目录,避免序列器写入
|
|
6
|
+
* 刷表目录 mtime,多进程下触发对等进程整表重载的失效风暴);
|
|
7
|
+
* 跨进程互斥经 `.lock` 文件(`O_EXCL` 原子创建 + 退避重试 + 孤儿锁回收)。
|
|
8
|
+
* 无第三方依赖,跨平台。
|
|
9
|
+
*/
|
|
10
|
+
import * as fsp from 'fs/promises';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
const META_SUFFIX = '/_meta.json';
|
|
13
|
+
/**
|
|
14
|
+
* 元信息路径判定(与平台分隔符无关):`<表>/_meta.json` → 表名,否则 null。
|
|
15
|
+
* Windows 下 `path.join(表, '_meta.json')` 产出 `表\_meta.json`,仅按 POSIX 分隔符
|
|
16
|
+
* 判断会把序列器写回表目录,使「迁出表目录」的设计失效。
|
|
17
|
+
*/
|
|
18
|
+
function metaTableOf(relPath) {
|
|
19
|
+
const norm = relPath.replace(/\\/g, '/');
|
|
20
|
+
if (!norm.endsWith(META_SUFFIX))
|
|
21
|
+
return null;
|
|
22
|
+
const table = norm.slice(0, -META_SUFFIX.length);
|
|
23
|
+
return table && !table.includes('/') ? table : null;
|
|
24
|
+
}
|
|
25
|
+
export class FsStorageBackend {
|
|
26
|
+
rootDir;
|
|
27
|
+
lockTimeoutMs;
|
|
28
|
+
lockStaleMs;
|
|
29
|
+
constructor(rootDir, options) {
|
|
30
|
+
this.rootDir = rootDir;
|
|
31
|
+
this.lockTimeoutMs = options?.lockTimeoutMs ?? 5000;
|
|
32
|
+
this.lockStaleMs = options?.lockStaleMs ?? 30000;
|
|
33
|
+
}
|
|
34
|
+
resolve(relPath) {
|
|
35
|
+
// 序列器/元信息迁出表目录:`<表>/_meta.json` → `<根>/_meta/<表>.json`
|
|
36
|
+
const table = metaTableOf(relPath);
|
|
37
|
+
if (table)
|
|
38
|
+
return path.join(this.rootDir, '_meta', `${table}.json`);
|
|
39
|
+
return path.join(this.rootDir, relPath);
|
|
40
|
+
}
|
|
41
|
+
async readFile(relPath) {
|
|
42
|
+
try {
|
|
43
|
+
return await fsp.readFile(this.resolve(relPath), 'utf8');
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err?.code === 'ENOENT')
|
|
47
|
+
return null;
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async writeFileAtomic(relPath, data) {
|
|
52
|
+
const file = this.resolve(relPath);
|
|
53
|
+
await fsp.mkdir(path.dirname(file), { recursive: true });
|
|
54
|
+
// 临时文件名含 pid + 随机量:并发写同一目标时各写独立 tmp,
|
|
55
|
+
// 避免内容交错、以及他人已 rename 走 tmp 后本进程 rename 抛 ENOENT
|
|
56
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
57
|
+
try {
|
|
58
|
+
await fsp.writeFile(tmp, data, 'utf8');
|
|
59
|
+
await fsp.rename(tmp, file);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
try {
|
|
63
|
+
await fsp.rm(tmp, { force: true });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* 清理自身临时文件失败不影响原始错误 */
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
if (metaTableOf(relPath) !== null) {
|
|
71
|
+
// 旧布局迁移:尽力清理 `<表>/_meta.json`;读路径不读旧位置,
|
|
72
|
+
// 序列连续性由集合层水位下限保证(清理失败无害,旧文件不再被读取)
|
|
73
|
+
try {
|
|
74
|
+
await fsp.rm(path.join(this.rootDir, relPath), { force: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* ignore */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async readDir(relPath) {
|
|
82
|
+
try {
|
|
83
|
+
return await fsp.readdir(this.resolve(relPath));
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
if (err?.code === 'ENOENT')
|
|
87
|
+
return [];
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async deleteFile(relPath) {
|
|
92
|
+
try {
|
|
93
|
+
await fsp.rm(this.resolve(relPath), { force: true });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* 文件已不存在则忽略 */
|
|
97
|
+
}
|
|
98
|
+
if (metaTableOf(relPath) !== null) {
|
|
99
|
+
/* 布局迁移:一并清理遗留的 <表>/_meta.json(幂等) */
|
|
100
|
+
try {
|
|
101
|
+
await fsp.rm(path.join(this.rootDir, relPath), { force: true });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
/* 同上 */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async mkdir(relPath) {
|
|
109
|
+
await fsp.mkdir(this.resolve(relPath), { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
async stat(relPath) {
|
|
112
|
+
try {
|
|
113
|
+
const s = await fsp.stat(this.resolve(relPath));
|
|
114
|
+
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (err?.code === 'ENOENT')
|
|
118
|
+
return null;
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async withLock(relPath, fn) {
|
|
123
|
+
const lockFile = `${this.resolve(relPath)}.lock`;
|
|
124
|
+
await fsp.mkdir(path.dirname(lockFile), { recursive: true });
|
|
125
|
+
const start = Date.now();
|
|
126
|
+
// 归属令牌:释放前校验,避免删除「已被回收、现由他人持有」的锁
|
|
127
|
+
const token = `${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
128
|
+
for (;;) {
|
|
129
|
+
// 超时判定置于循环开头:孤儿回收重试与 stat 失败重试分支同样受约束,避免热自旋
|
|
130
|
+
if (Date.now() - start > this.lockTimeoutMs) {
|
|
131
|
+
throw new Error(`[json-storage] 获取存储锁超时:${relPath}`);
|
|
132
|
+
}
|
|
133
|
+
let fd = null;
|
|
134
|
+
try {
|
|
135
|
+
fd = await fsp.open(lockFile, 'wx');
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (err?.code !== 'EEXIST')
|
|
139
|
+
throw err;
|
|
140
|
+
// 孤儿锁回收:mtime 超阈值视为持有者已崩溃,强制删除后重新竞争
|
|
141
|
+
// (临界区仅包裹毫秒级元数据操作,30s 余量充足;两进程同时回收时 O_EXCL 保证唯一胜者)
|
|
142
|
+
try {
|
|
143
|
+
const st = await fsp.stat(lockFile);
|
|
144
|
+
if (Date.now() - st.mtimeMs > this.lockStaleMs) {
|
|
145
|
+
await fsp.rm(lockFile, { force: true });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
continue; // 锁已在间隙被释放,直接重试
|
|
151
|
+
}
|
|
152
|
+
await new Promise((r) => setTimeout(r, 10 + Math.random() * 40));
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
// 写入持有者信息(token/PID/时间)便于孤儿锁诊断与归属校验
|
|
157
|
+
await fd.write(JSON.stringify({ token, pid: process.pid, at: new Date().toISOString() }));
|
|
158
|
+
await fd.close();
|
|
159
|
+
fd = null;
|
|
160
|
+
try {
|
|
161
|
+
return await fn();
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
await this.releaseLock(lockFile, token);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
if (fd) {
|
|
169
|
+
try {
|
|
170
|
+
await fd.close();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* ignore */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
await this.releaseLock(lockFile, token);
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** 释放锁:仅当锁内容中的令牌与本进程一致时删除(否则锁已被回收并被他人持有) */
|
|
182
|
+
async releaseLock(lockFile, token) {
|
|
183
|
+
try {
|
|
184
|
+
const raw = await fsp.readFile(lockFile, 'utf8');
|
|
185
|
+
const holder = JSON.parse(raw);
|
|
186
|
+
if (holder?.token !== token)
|
|
187
|
+
return; // 他人持有:保留,交由回收阈值兜底
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
/* 锁文件已不存在或不可解析:无需释放 */
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
await fsp.rm(lockFile, { force: true });
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
/* 释放失败仅留孤儿锁文件,超阈值后会被回收;不阻断 */
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async nextId(relPath, key) {
|
|
201
|
+
return this.withLock(relPath, async () => {
|
|
202
|
+
let seq = 0;
|
|
203
|
+
const raw = await this.readFile(relPath);
|
|
204
|
+
if (raw) {
|
|
205
|
+
try {
|
|
206
|
+
const obj = JSON.parse(raw);
|
|
207
|
+
const v = obj[key];
|
|
208
|
+
if (typeof v === 'number')
|
|
209
|
+
seq = v;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
/* 损坏的 _meta.json → 从 0 起 */
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
seq += 1;
|
|
216
|
+
await this.writeFileAtomic(relPath, JSON.stringify({ [key]: seq }));
|
|
217
|
+
return seq;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
async ensureSeqAtLeast(relPath, key, value) {
|
|
221
|
+
await this.withLock(relPath, async () => {
|
|
222
|
+
let seq = 0;
|
|
223
|
+
const raw = await this.readFile(relPath);
|
|
224
|
+
if (raw) {
|
|
225
|
+
try {
|
|
226
|
+
const obj = JSON.parse(raw);
|
|
227
|
+
const v = obj[key];
|
|
228
|
+
if (typeof v === 'number')
|
|
229
|
+
seq = v;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
/* 损坏的 _meta.json → 以传入 value 为准 */
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (value > seq) {
|
|
236
|
+
await this.writeFileAtomic(relPath, JSON.stringify({ [key]: value }));
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=json-backend-fs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-backend-fs.js","sourceRoot":"","sources":["../../src/json-backend-fs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type JsonStorageBackend } from './json-config.js';
|
|
2
|
+
import type { JsonEntityMeta } from './json-entity-meta.js';
|
|
3
|
+
/** 行对象:属性名 → 值(实体表)或原始列名 → 值(非实体表) */
|
|
4
|
+
export type JsonRow = Record<string, unknown>;
|
|
5
|
+
export declare class JsonCollection {
|
|
6
|
+
private meta;
|
|
7
|
+
readonly tableName: string;
|
|
8
|
+
/** 主键属性(实体表)或主键列(非实体表);未知时为 undefined */
|
|
9
|
+
readonly primaryProperty?: string;
|
|
10
|
+
/** 当前集合使用的后端实例(供 getCollection 检测运行时重注后重建) */
|
|
11
|
+
readonly backend: JsonStorageBackend;
|
|
12
|
+
private readonly metaRelPath;
|
|
13
|
+
private readonly reloadTtlMs;
|
|
14
|
+
private rows;
|
|
15
|
+
private loaded;
|
|
16
|
+
private seq;
|
|
17
|
+
/** 已知后备存储(表目录)mtime;外部变更触发重载(etag 优先判定) */
|
|
18
|
+
private cacheMtime;
|
|
19
|
+
/** 已知后备存储(表目录)etag;后端提供时优先于 mtime 判定外部变更 */
|
|
20
|
+
private cacheEtag;
|
|
21
|
+
/** 上次失效校验时间戳(ms),受 reloadTtlMs 节流 */
|
|
22
|
+
private lastCheckedAt;
|
|
23
|
+
/** 进行中的加载 Promise(single-flight:并发首载/重载共享,不重复整表 I/O) */
|
|
24
|
+
private loadPromise;
|
|
25
|
+
constructor(tableName: string, meta: JsonEntityMeta | null);
|
|
26
|
+
/**
|
|
27
|
+
* 表内全部行(惰性加载后返回**深拷贝**,调用方原地修改不会污染内存集合或静默分叉磁盘)。
|
|
28
|
+
* 与 TypeORM 一致:返回的行不会因未 save 而落库。
|
|
29
|
+
*/
|
|
30
|
+
all(): Promise<JsonRow[]>;
|
|
31
|
+
/** 按主键取行(返回深拷贝,调用方原地修改不影响内存集合与磁盘) */
|
|
32
|
+
get(pk: unknown): Promise<JsonRow | undefined>;
|
|
33
|
+
/** 按谓词查找(返回深拷贝,调用方原地修改不影响内存集合与磁盘) */
|
|
34
|
+
findWhere(pred: (row: JsonRow) => boolean): Promise<JsonRow[]>;
|
|
35
|
+
/** 写入(新增或整行覆盖),返回落盘后的行 */
|
|
36
|
+
write(row: JsonRow): Promise<JsonRow>;
|
|
37
|
+
/** 删除行,返回是否删除 */
|
|
38
|
+
delete(pk: unknown): Promise<boolean>;
|
|
39
|
+
/** 分配下一个自增主键(后端级原子序列器,跨进程唯一;不低于集合已知水位) */
|
|
40
|
+
nextId(): Promise<number>;
|
|
41
|
+
/** 清空内存缓存并删除落盘数据(TRUNCATE 与测试隔离使用) */
|
|
42
|
+
clear(): Promise<void>;
|
|
43
|
+
/** @deprecated 兼容别名,请使用 clear() */
|
|
44
|
+
clearForTest(): Promise<void>;
|
|
45
|
+
/** 读取前确保集合已加载,并按 TTL 节流做跨进程失效校验 */
|
|
46
|
+
private ensureLoaded;
|
|
47
|
+
/** 失效判定:任一侧提供 etag 时以 etag 不等为变更,否则以 mtime 增大为变更 */
|
|
48
|
+
private isExternalChange;
|
|
49
|
+
/** 记录一次已确认的后备状态(etag + mtime) */
|
|
50
|
+
private rememberStat;
|
|
51
|
+
/** 经后端把表目录读入内存(single-flight:并发加载共享同一 Promise) */
|
|
52
|
+
private loadFromBackend;
|
|
53
|
+
/** 加载体:局部构建后整体交换;加载前后 stat 不一致(窗口内并发写入)则有界重试 */
|
|
54
|
+
private doLoadFromBackend;
|
|
55
|
+
/** 写/删后同步已知后备状态,使下次失效校验以本进程写入为基准 */
|
|
56
|
+
private refreshCacheMtime;
|
|
57
|
+
private resolveKey;
|
|
58
|
+
private rowKey;
|
|
59
|
+
private rowFileRel;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 获取(或创建)表的集合单例。
|
|
63
|
+
* 运行时重注后端(configureJsonStorage)后,旧集合的内存缓存属于旧数据源,
|
|
64
|
+
* 检测到后端实例变更即丢弃重建(已外传的旧 Repository/Collection 引用不追踪:
|
|
65
|
+
* 重注应在首次访问前完成,测试中配合 resetCollectionsForTest)。
|
|
66
|
+
*/
|
|
67
|
+
export declare function getCollection(tableName: string, meta: JsonEntityMeta | null): JsonCollection;
|
|
68
|
+
/** 仅供测试:清空全部集合缓存 */
|
|
69
|
+
export declare function resetCollectionsForTest(): void;
|
|
70
|
+
//# sourceMappingURL=json-collection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-collection.d.ts","sourceRoot":"","sources":["../../src/json-collection.ts"],"names":[],"mappings":"AAgBA,OAAO,EAGL,KAAK,kBAAkB,EAExB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAK5D,sCAAsC;AACtC,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAuC9C,qBAAa,cAAc;IAsBM,OAAO,CAAC,IAAI;IArB3C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,yCAAyC;IACzC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAElC,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,OAAO,CAAC,IAAI,CAA8B;IAC1C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,GAAG,CAAqB;IAChC,2CAA2C;IAC3C,OAAO,CAAC,UAAU,CAAK;IACvB,4CAA4C;IAC5C,OAAO,CAAC,SAAS,CAAqB;IACtC,qCAAqC;IACrC,OAAO,CAAC,aAAa,CAAK;IAC1B,wDAAwD;IACxD,OAAO,CAAC,WAAW,CAA8B;gBAErC,SAAS,EAAE,MAAM,EAAU,IAAI,EAAE,cAAc,GAAG,IAAI;IAQlE;;;OAGG;IACG,GAAG,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAK/B,qCAAqC;IAC/B,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAMpD,qCAAqC;IAC/B,SAAS,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAKpE,0BAA0B;IACpB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAkB3C,iBAAiB;IACX,MAAM,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3C,0CAA0C;IACpC,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;IAe/B,sCAAsC;IAChC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB5B,mCAAmC;IACnC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAM7B,mCAAmC;YACrB,YAAY;IAuB1B,oDAAoD;IACpD,OAAO,CAAC,gBAAgB;IAOxB,iCAAiC;IACjC,OAAO,CAAC,YAAY;IAKpB,kDAAkD;IAClD,OAAO,CAAC,eAAe;IASvB,gDAAgD;YAClC,iBAAiB;IA+D/B,oCAAoC;YACtB,iBAAiB;IAU/B,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,UAAU;CAKnB;AAID;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,IAAI,GAAG,cAAc,CAW5F;AAED,oBAAoB;AACpB,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C"}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON 回退存储:文件型表集合(json-fallback-storage 规格)
|
|
3
|
+
*
|
|
4
|
+
* 落盘布局:`<根>/<表名>/<主键>.json`(每行一个 JSON 文件,属性名为键;
|
|
5
|
+
* 根经 `configureJsonStorage()` 注入的 `JsonStorageBackend` 解析),
|
|
6
|
+
* 自增序列器与表元信息存于 `<根>/_meta/<表名>.json`(与表目录分离,
|
|
7
|
+
* 序列器/锁写入不触发表级失效信号)。
|
|
8
|
+
*
|
|
9
|
+
* - 惰性加载:首次访问时把表目录经后端读入内存(single-flight 共享加载)
|
|
10
|
+
* - 原子写:先写临时文件再 rename,避免进程中断产生半截文件
|
|
11
|
+
* - datetime 类型列读取时还原为 Date(保证与 TypeORM 行为一致,可参与比较/排序)
|
|
12
|
+
* - 跨进程缓存失效:读取前经 `backend.stat(表目录)` 检测外部变更(etag 优先、
|
|
13
|
+
* mtime 兜底;stat 返回 null 视为无变更),变更则重载
|
|
14
|
+
* (`reloadTtlMs` 节流;默认 0 即每次访问校验)
|
|
15
|
+
*/
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { requireStorageBackend, getReloadTtlMs, } from "./json-config.js";
|
|
18
|
+
import { getNonEntityPrimaryColumn } from "./json-entity-meta.js";
|
|
19
|
+
const META_FILE = '_meta.json';
|
|
20
|
+
/**
|
|
21
|
+
* 深拷贝行值,保留 Date 等内置类型(递归,避免把 Date 折回 ISO 字符串)。
|
|
22
|
+
* 用于读路径隔离:调用方拿到的是独立副本,原地修改不会污染内存集合,
|
|
23
|
+
* 也不会在未 write 时与磁盘文件静默分叉(与 TypeORM「返回受管实体、未 save 不落库」一致)。
|
|
24
|
+
*/
|
|
25
|
+
function cloneValue(value) {
|
|
26
|
+
if (value === null || typeof value !== 'object')
|
|
27
|
+
return value;
|
|
28
|
+
if (value instanceof Date)
|
|
29
|
+
return new Date(value.getTime());
|
|
30
|
+
if (Array.isArray(value))
|
|
31
|
+
return value.map(cloneValue);
|
|
32
|
+
// 内置容器保真:退化为索引对象/空对象会让落盘形态被静默改写
|
|
33
|
+
if (Buffer.isBuffer(value))
|
|
34
|
+
return Buffer.from(value);
|
|
35
|
+
if (ArrayBuffer.isView(value)) {
|
|
36
|
+
return value.slice();
|
|
37
|
+
}
|
|
38
|
+
if (value instanceof ArrayBuffer)
|
|
39
|
+
return value.slice(0);
|
|
40
|
+
if (value instanceof Map)
|
|
41
|
+
return new Map([...value].map(([k, v]) => [k, cloneValue(v)]));
|
|
42
|
+
if (value instanceof Set)
|
|
43
|
+
return new Set([...value].map(cloneValue));
|
|
44
|
+
if (value instanceof RegExp)
|
|
45
|
+
return new RegExp(value.source, value.flags);
|
|
46
|
+
// 类实例等非普通对象按引用返回:宁可共享引用,也不把形态改写成裸对象
|
|
47
|
+
const proto = Object.getPrototypeOf(value);
|
|
48
|
+
if (proto !== Object.prototype && proto !== null)
|
|
49
|
+
return value;
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const k of Object.keys(value)) {
|
|
52
|
+
out[k] = cloneValue(value[k]);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** 行深拷贝(见 cloneValue;保留 Date) */
|
|
57
|
+
function cloneRow(row) {
|
|
58
|
+
return cloneValue(row);
|
|
59
|
+
}
|
|
60
|
+
export class JsonCollection {
|
|
61
|
+
meta;
|
|
62
|
+
tableName;
|
|
63
|
+
/** 主键属性(实体表)或主键列(非实体表);未知时为 undefined */
|
|
64
|
+
primaryProperty;
|
|
65
|
+
/** 当前集合使用的后端实例(供 getCollection 检测运行时重注后重建) */
|
|
66
|
+
backend;
|
|
67
|
+
metaRelPath;
|
|
68
|
+
reloadTtlMs;
|
|
69
|
+
rows = new Map();
|
|
70
|
+
loaded = false;
|
|
71
|
+
seq;
|
|
72
|
+
/** 已知后备存储(表目录)mtime;外部变更触发重载(etag 优先判定) */
|
|
73
|
+
cacheMtime = 0;
|
|
74
|
+
/** 已知后备存储(表目录)etag;后端提供时优先于 mtime 判定外部变更 */
|
|
75
|
+
cacheEtag;
|
|
76
|
+
/** 上次失效校验时间戳(ms),受 reloadTtlMs 节流 */
|
|
77
|
+
lastCheckedAt = 0;
|
|
78
|
+
/** 进行中的加载 Promise(single-flight:并发首载/重载共享,不重复整表 I/O) */
|
|
79
|
+
loadPromise = null;
|
|
80
|
+
constructor(tableName, meta) {
|
|
81
|
+
this.meta = meta;
|
|
82
|
+
this.tableName = tableName;
|
|
83
|
+
this.backend = requireStorageBackend();
|
|
84
|
+
this.metaRelPath = path.join(tableName, META_FILE);
|
|
85
|
+
this.reloadTtlMs = getReloadTtlMs();
|
|
86
|
+
this.primaryProperty = meta?.primaryProperty ?? getNonEntityPrimaryColumn(tableName);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 表内全部行(惰性加载后返回**深拷贝**,调用方原地修改不会污染内存集合或静默分叉磁盘)。
|
|
90
|
+
* 与 TypeORM 一致:返回的行不会因未 save 而落库。
|
|
91
|
+
*/
|
|
92
|
+
async all() {
|
|
93
|
+
await this.ensureLoaded();
|
|
94
|
+
return [...this.rows.values()].map(cloneRow);
|
|
95
|
+
}
|
|
96
|
+
/** 按主键取行(返回深拷贝,调用方原地修改不影响内存集合与磁盘) */
|
|
97
|
+
async get(pk) {
|
|
98
|
+
await this.ensureLoaded();
|
|
99
|
+
const row = this.rows.get(this.rowKey(pk));
|
|
100
|
+
return row ? cloneRow(row) : undefined;
|
|
101
|
+
}
|
|
102
|
+
/** 按谓词查找(返回深拷贝,调用方原地修改不影响内存集合与磁盘) */
|
|
103
|
+
async findWhere(pred) {
|
|
104
|
+
await this.ensureLoaded();
|
|
105
|
+
return [...this.rows.values()].filter(pred).map(cloneRow);
|
|
106
|
+
}
|
|
107
|
+
/** 写入(新增或整行覆盖),返回落盘后的行 */
|
|
108
|
+
async write(row) {
|
|
109
|
+
await this.ensureLoaded(true);
|
|
110
|
+
const pk = this.resolveKey(row);
|
|
111
|
+
// 显式写入更大的数字主键时抬升序列器,避免同进程/跨进程后续 nextId() 撞已有主键
|
|
112
|
+
const n = Number(row[this.primaryProperty]);
|
|
113
|
+
if (Number.isInteger(n) && n > (this.seq ?? 0)) {
|
|
114
|
+
this.seq = n;
|
|
115
|
+
await this.backend.ensureSeqAtLeast(this.metaRelPath, 'seq', n);
|
|
116
|
+
}
|
|
117
|
+
const stored = { ...row };
|
|
118
|
+
// 先落盘再更新内存:落盘失败不留「内存有、磁盘无」的幽灵行
|
|
119
|
+
await this.backend.writeFileAtomic(this.rowFileRel(pk), JSON.stringify(stored, null, 2));
|
|
120
|
+
// 内存快照存独立深拷贝:调用方原地修改返回值(或其嵌套对象)不污染集合
|
|
121
|
+
this.rows.set(pk, cloneRow(stored));
|
|
122
|
+
await this.refreshCacheMtime();
|
|
123
|
+
return stored;
|
|
124
|
+
}
|
|
125
|
+
/** 删除行,返回是否删除 */
|
|
126
|
+
async delete(pk) {
|
|
127
|
+
await this.ensureLoaded(true);
|
|
128
|
+
const key = this.rowKey(pk);
|
|
129
|
+
if (!this.rows.delete(key))
|
|
130
|
+
return false;
|
|
131
|
+
await this.backend.deleteFile(this.rowFileRel(key));
|
|
132
|
+
await this.refreshCacheMtime();
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
/** 分配下一个自增主键(后端级原子序列器,跨进程唯一;不低于集合已知水位) */
|
|
136
|
+
async nextId() {
|
|
137
|
+
await this.ensureLoaded(true);
|
|
138
|
+
let id = await this.backend.nextId(this.metaRelPath, 'seq');
|
|
139
|
+
const floor = this.seq ?? 0;
|
|
140
|
+
if (id <= floor) {
|
|
141
|
+
// 后端序列器损坏/丢失/被重置 → 回绕分配会撞存量主键并静默覆盖行:
|
|
142
|
+
// 先抬升后端序列器至水位再重取(异常路径,代价一次额外锁内往返)
|
|
143
|
+
await this.backend.ensureSeqAtLeast(this.metaRelPath, 'seq', floor);
|
|
144
|
+
id = await this.backend.nextId(this.metaRelPath, 'seq');
|
|
145
|
+
}
|
|
146
|
+
this.seq = id;
|
|
147
|
+
await this.refreshCacheMtime();
|
|
148
|
+
return id;
|
|
149
|
+
}
|
|
150
|
+
/** 清空内存缓存并删除落盘数据(TRUNCATE 与测试隔离使用) */
|
|
151
|
+
async clear() {
|
|
152
|
+
// 必须先加载:否则「本进程未读过该表」时 rows 为空,只删 meta 而行文件全残留
|
|
153
|
+
await this.ensureLoaded();
|
|
154
|
+
// 等待在飞的加载结束,避免其用已被删除的文件重新填充 rows
|
|
155
|
+
if (this.loadPromise)
|
|
156
|
+
await this.loadPromise;
|
|
157
|
+
const keys = [...this.rows.keys()];
|
|
158
|
+
for (const k of keys) {
|
|
159
|
+
await this.backend.deleteFile(this.rowFileRel(k));
|
|
160
|
+
}
|
|
161
|
+
await this.backend.deleteFile(this.metaRelPath);
|
|
162
|
+
this.rows.clear();
|
|
163
|
+
this.seq = undefined;
|
|
164
|
+
this.loaded = false;
|
|
165
|
+
this.loadPromise = null;
|
|
166
|
+
this.cacheMtime = 0;
|
|
167
|
+
this.cacheEtag = undefined;
|
|
168
|
+
this.lastCheckedAt = 0;
|
|
169
|
+
}
|
|
170
|
+
/** @deprecated 兼容别名,请使用 clear() */
|
|
171
|
+
clearForTest() {
|
|
172
|
+
return this.clear();
|
|
173
|
+
}
|
|
174
|
+
// ---- 内部实现 ----
|
|
175
|
+
/** 读取前确保集合已加载,并按 TTL 节流做跨进程失效校验 */
|
|
176
|
+
async ensureLoaded(force = false) {
|
|
177
|
+
if (this.loaded) {
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
// 写路径强制校验(绕过 TTL 节流):确保写入前快照已含他进程写入,
|
|
180
|
+
// 使写入后记录的水位与实际快照一致,不掩蔽他进程变更
|
|
181
|
+
if (!force && now - this.lastCheckedAt < this.reloadTtlMs)
|
|
182
|
+
return;
|
|
183
|
+
const st = await this.backend.stat(this.tableName);
|
|
184
|
+
this.lastCheckedAt = now;
|
|
185
|
+
// 表目录不存在:视为无变更,保留内存快照(避免每次访问全量重载的读放大)
|
|
186
|
+
if (!st)
|
|
187
|
+
return;
|
|
188
|
+
if (this.isExternalChange(st)) {
|
|
189
|
+
// 外部进程已变更 → 丢弃内存快照后重载
|
|
190
|
+
this.rows.clear();
|
|
191
|
+
this.seq = undefined;
|
|
192
|
+
this.loaded = false;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
this.rememberStat(st);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
await this.loadFromBackend();
|
|
200
|
+
}
|
|
201
|
+
/** 失效判定:任一侧提供 etag 时以 etag 不等为变更,否则以 mtime 增大为变更 */
|
|
202
|
+
isExternalChange(st) {
|
|
203
|
+
if (st.etag !== undefined || this.cacheEtag !== undefined) {
|
|
204
|
+
return st.etag !== this.cacheEtag;
|
|
205
|
+
}
|
|
206
|
+
return st.mtimeMs > this.cacheMtime;
|
|
207
|
+
}
|
|
208
|
+
/** 记录一次已确认的后备状态(etag + mtime) */
|
|
209
|
+
rememberStat(st) {
|
|
210
|
+
this.cacheMtime = st.mtimeMs;
|
|
211
|
+
this.cacheEtag = st.etag;
|
|
212
|
+
}
|
|
213
|
+
/** 经后端把表目录读入内存(single-flight:并发加载共享同一 Promise) */
|
|
214
|
+
loadFromBackend() {
|
|
215
|
+
if (!this.loadPromise) {
|
|
216
|
+
this.loadPromise = this.doLoadFromBackend().finally(() => {
|
|
217
|
+
this.loadPromise = null;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return this.loadPromise;
|
|
221
|
+
}
|
|
222
|
+
/** 加载体:局部构建后整体交换;加载前后 stat 不一致(窗口内并发写入)则有界重试 */
|
|
223
|
+
async doLoadFromBackend() {
|
|
224
|
+
const datetimeProps = this.meta?.datetimeProperties ?? new Set();
|
|
225
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
226
|
+
const before = await this.backend.stat(this.tableName);
|
|
227
|
+
const rows = new Map();
|
|
228
|
+
const entries = await this.backend.readDir(this.tableName);
|
|
229
|
+
for (const name of entries) {
|
|
230
|
+
if (!name.endsWith('.json') || name === META_FILE)
|
|
231
|
+
continue;
|
|
232
|
+
try {
|
|
233
|
+
const raw = await this.backend.readFile(path.join(this.tableName, name));
|
|
234
|
+
if (!raw)
|
|
235
|
+
continue;
|
|
236
|
+
const row = JSON.parse(raw);
|
|
237
|
+
// datetime 列还原为 Date(ISO 字符串 → Date,与 TypeORM 读取行为一致)
|
|
238
|
+
for (const prop of datetimeProps) {
|
|
239
|
+
const v = row[prop];
|
|
240
|
+
if (typeof v === 'string' && v)
|
|
241
|
+
row[prop] = new Date(v);
|
|
242
|
+
}
|
|
243
|
+
const key = this.primaryProperty
|
|
244
|
+
? this.rowKey(row[this.primaryProperty])
|
|
245
|
+
: decodeURIComponent(name.slice(0, -5));
|
|
246
|
+
if (key && key !== 'undefined')
|
|
247
|
+
rows.set(key, row);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
// 单个损坏文件不阻塞整表加载;fail-loud 记日志
|
|
251
|
+
// eslint-disable-next-line no-console
|
|
252
|
+
console.warn(`[json-storage] 跳过损坏文件 ${this.tableName}/${name}:`, err.message);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// 初始化序列器:取现有最大数字主键与 _meta.json 序列器较大者
|
|
256
|
+
let max = 0;
|
|
257
|
+
for (const key of rows.keys()) {
|
|
258
|
+
const n = Number(key);
|
|
259
|
+
if (Number.isInteger(n) && n > max)
|
|
260
|
+
max = n;
|
|
261
|
+
}
|
|
262
|
+
const metaRaw = await this.backend.readFile(this.metaRelPath);
|
|
263
|
+
if (metaRaw) {
|
|
264
|
+
try {
|
|
265
|
+
const metaFile = JSON.parse(metaRaw);
|
|
266
|
+
if (typeof metaFile.seq === 'number')
|
|
267
|
+
max = Math.max(max, metaFile.seq);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
/* 无/损坏 _meta.json → 以现有最大主键为准 */
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const after = await this.backend.stat(this.tableName);
|
|
274
|
+
// 加载窗口稳定:两侧 stat 一致(etag 与 mtime 均未变),或重试耗尽取最后快照
|
|
275
|
+
const stable = (before?.mtimeMs ?? 0) >= (after?.mtimeMs ?? 0) && (before?.etag ?? undefined) === (after?.etag ?? undefined);
|
|
276
|
+
if (stable || attempt === 2) {
|
|
277
|
+
this.rows = rows;
|
|
278
|
+
this.seq = max;
|
|
279
|
+
if (after)
|
|
280
|
+
this.rememberStat(after);
|
|
281
|
+
else {
|
|
282
|
+
// 目录不存在:水位基线记 0(而非墙上时钟),使目录后续出现时任何 mtime
|
|
283
|
+
// 都能被判定为变更(时钟回拨/备份恢复下 Date.now() 会永久失灵)
|
|
284
|
+
this.cacheMtime = 0;
|
|
285
|
+
this.cacheEtag = undefined;
|
|
286
|
+
}
|
|
287
|
+
this.loaded = true;
|
|
288
|
+
this.lastCheckedAt = Date.now();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/** 写/删后同步已知后备状态,使下次失效校验以本进程写入为基准 */
|
|
294
|
+
async refreshCacheMtime() {
|
|
295
|
+
const st = await this.backend.stat(this.tableName);
|
|
296
|
+
if (st)
|
|
297
|
+
this.rememberStat(st);
|
|
298
|
+
else {
|
|
299
|
+
this.cacheMtime = 0;
|
|
300
|
+
this.cacheEtag = undefined;
|
|
301
|
+
}
|
|
302
|
+
this.lastCheckedAt = Date.now();
|
|
303
|
+
}
|
|
304
|
+
resolveKey(row) {
|
|
305
|
+
if (!this.primaryProperty) {
|
|
306
|
+
throw new Error(`[json-storage] 表 ${this.tableName} 缺少主键定义,无法落盘行:${JSON.stringify(row).slice(0, 200)}`);
|
|
307
|
+
}
|
|
308
|
+
const pk = row[this.primaryProperty];
|
|
309
|
+
if (pk === undefined || pk === null || pk === '') {
|
|
310
|
+
throw new Error(`[json-storage] 表 ${this.tableName} 行主键 ${this.primaryProperty} 为空,无法落盘`);
|
|
311
|
+
}
|
|
312
|
+
return this.rowKey(pk);
|
|
313
|
+
}
|
|
314
|
+
rowKey(pk) {
|
|
315
|
+
return String(pk);
|
|
316
|
+
}
|
|
317
|
+
rowFileRel(key) {
|
|
318
|
+
// 字符串主键(如 cacheKey、lock_name)经 encodeURIComponent 保证文件名安全;
|
|
319
|
+
// `*` 未被 encodeURIComponent 转义,需显式转义(Windows 非法文件名字符)
|
|
320
|
+
return path.join(this.tableName, `${encodeURIComponent(key).replace(/\*/g, '%2A')}.json`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const collectionCache = new Map();
|
|
324
|
+
/**
|
|
325
|
+
* 获取(或创建)表的集合单例。
|
|
326
|
+
* 运行时重注后端(configureJsonStorage)后,旧集合的内存缓存属于旧数据源,
|
|
327
|
+
* 检测到后端实例变更即丢弃重建(已外传的旧 Repository/Collection 引用不追踪:
|
|
328
|
+
* 重注应在首次访问前完成,测试中配合 resetCollectionsForTest)。
|
|
329
|
+
*/
|
|
330
|
+
export function getCollection(tableName, meta) {
|
|
331
|
+
let c = collectionCache.get(tableName);
|
|
332
|
+
if (c && c.backend !== requireStorageBackend()) {
|
|
333
|
+
collectionCache.delete(tableName);
|
|
334
|
+
c = undefined;
|
|
335
|
+
}
|
|
336
|
+
if (!c) {
|
|
337
|
+
c = new JsonCollection(tableName, meta);
|
|
338
|
+
collectionCache.set(tableName, c);
|
|
339
|
+
}
|
|
340
|
+
return c;
|
|
341
|
+
}
|
|
342
|
+
/** 仅供测试:清空全部集合缓存 */
|
|
343
|
+
export function resetCollectionsForTest() {
|
|
344
|
+
collectionCache.clear();
|
|
345
|
+
}
|
|
346
|
+
//# sourceMappingURL=json-collection.js.map
|