mvcc-api 1.0.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/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/cjs/index.cjs +665 -0
- package/dist/esm/index.mjs +633 -0
- package/dist/types/core/async/Manager.d.ts +14 -0
- package/dist/types/core/async/Strategy.d.ts +7 -0
- package/dist/types/core/async/Transaction.d.ts +7 -0
- package/dist/types/core/async/index.d.ts +3 -0
- package/dist/types/core/base/Manager.d.ts +58 -0
- package/dist/types/core/base/Strategy.d.ts +32 -0
- package/dist/types/core/base/Transaction.d.ts +59 -0
- package/dist/types/core/base/index.d.ts +3 -0
- package/dist/types/core/sync/Manager.d.ts +11 -0
- package/dist/types/core/sync/Strategy.d.ts +7 -0
- package/dist/types/core/sync/Transaction.d.ts +7 -0
- package/dist/types/core/sync/index.d.ts +3 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/types/index.d.ts +5 -0
- package/package.json +44 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
AsyncMVCCManager: () => AsyncMVCCManager,
|
|
24
|
+
AsyncMVCCStrategy: () => AsyncMVCCStrategy,
|
|
25
|
+
AsyncMVCCTransaction: () => AsyncMVCCTransaction,
|
|
26
|
+
SyncMVCCManager: () => SyncMVCCManager,
|
|
27
|
+
SyncMVCCStrategy: () => SyncMVCCStrategy,
|
|
28
|
+
SyncMVCCTransaction: () => SyncMVCCTransaction
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(src_exports);
|
|
31
|
+
|
|
32
|
+
// src/core/base/Manager.ts
|
|
33
|
+
var MVCCManager = class {
|
|
34
|
+
version;
|
|
35
|
+
strategy;
|
|
36
|
+
versionIndex;
|
|
37
|
+
activeTransactions;
|
|
38
|
+
deletedCache;
|
|
39
|
+
constructor(strategy) {
|
|
40
|
+
this.strategy = strategy;
|
|
41
|
+
this.version = 0;
|
|
42
|
+
this.activeTransactions = /* @__PURE__ */ new Set();
|
|
43
|
+
this.deletedCache = /* @__PURE__ */ new Map();
|
|
44
|
+
this.versionIndex = /* @__PURE__ */ new Map();
|
|
45
|
+
}
|
|
46
|
+
_removeTransaction(tx) {
|
|
47
|
+
this.activeTransactions.delete(tx);
|
|
48
|
+
this._cleanupDeletedCache();
|
|
49
|
+
}
|
|
50
|
+
_cleanupDeletedCache() {
|
|
51
|
+
let minVersion = this.version;
|
|
52
|
+
for (const tx of this.activeTransactions) {
|
|
53
|
+
minVersion = Math.min(minVersion, tx.snapshotVersion);
|
|
54
|
+
}
|
|
55
|
+
for (const [key, versions] of this.versionIndex.entries()) {
|
|
56
|
+
const toKeep = [];
|
|
57
|
+
let keptOldVersion = false;
|
|
58
|
+
let i = versions.length;
|
|
59
|
+
while (i--) {
|
|
60
|
+
const v = versions[i];
|
|
61
|
+
if (v.version > minVersion) {
|
|
62
|
+
toKeep.unshift(v);
|
|
63
|
+
} else if (!keptOldVersion) {
|
|
64
|
+
toKeep.unshift(v);
|
|
65
|
+
keptOldVersion = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (toKeep.length === 0) {
|
|
69
|
+
this.versionIndex.delete(key);
|
|
70
|
+
} else {
|
|
71
|
+
this.versionIndex.set(key, toKeep);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const [key, versions] of this.deletedCache.entries()) {
|
|
75
|
+
const filtered = versions.filter((v) => v.deletedAtVersion >= minVersion);
|
|
76
|
+
if (filtered.length === 0) {
|
|
77
|
+
this.deletedCache.delete(key);
|
|
78
|
+
} else {
|
|
79
|
+
this.deletedCache.set(key, filtered);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/core/base/Strategy.ts
|
|
86
|
+
var MVCCStrategy = class {
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/core/base/Transaction.ts
|
|
90
|
+
var MVCCTransaction = class {
|
|
91
|
+
manager;
|
|
92
|
+
committed;
|
|
93
|
+
snapshotVersion;
|
|
94
|
+
writeBuffer;
|
|
95
|
+
deleteBuffer;
|
|
96
|
+
constructor(manager, snapshotVersion) {
|
|
97
|
+
this.manager = manager;
|
|
98
|
+
this.snapshotVersion = snapshotVersion;
|
|
99
|
+
this.writeBuffer = /* @__PURE__ */ new Map();
|
|
100
|
+
this.deleteBuffer = /* @__PURE__ */ new Set();
|
|
101
|
+
this.committed = false;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Schedules a creation (insert) of a key-value pair.
|
|
105
|
+
* Throws if the transaction is already committed.
|
|
106
|
+
* @param key The key to create.
|
|
107
|
+
* @param value The value to store.
|
|
108
|
+
* @returns The transaction instance for chaining.
|
|
109
|
+
*/
|
|
110
|
+
create(key, value) {
|
|
111
|
+
if (this.committed) throw new Error("Transaction already committed");
|
|
112
|
+
this.writeBuffer.set(key, value);
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Schedules a write (update) of a key-value pair.
|
|
117
|
+
* Overwrites any existing value in the buffer.
|
|
118
|
+
* @param key The key to write.
|
|
119
|
+
* @param value The value to store.
|
|
120
|
+
* @returns The transaction instance for chaining.
|
|
121
|
+
*/
|
|
122
|
+
write(key, value) {
|
|
123
|
+
if (this.committed) throw new Error("Transaction already committed");
|
|
124
|
+
this.writeBuffer.set(key, value);
|
|
125
|
+
this.deleteBuffer.delete(key);
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Schedules a deletion of a key.
|
|
130
|
+
* @param key The key to delete.
|
|
131
|
+
* @returns The transaction instance for chaining.
|
|
132
|
+
*/
|
|
133
|
+
delete(key) {
|
|
134
|
+
if (this.committed) throw new Error("Transaction already committed");
|
|
135
|
+
this.deleteBuffer.add(key);
|
|
136
|
+
this.writeBuffer.delete(key);
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Rolls back the transaction.
|
|
141
|
+
* Clears all buffers and marks the transaction as finished.
|
|
142
|
+
* @returns The transaction instance.
|
|
143
|
+
*/
|
|
144
|
+
rollback() {
|
|
145
|
+
this.writeBuffer.clear();
|
|
146
|
+
this.deleteBuffer.clear();
|
|
147
|
+
this.committed = true;
|
|
148
|
+
this.manager._removeTransaction(this);
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/core/sync/Transaction.ts
|
|
154
|
+
var SyncMVCCTransaction = class extends MVCCTransaction {
|
|
155
|
+
read(key) {
|
|
156
|
+
if (this.committed) throw new Error("Transaction already committed");
|
|
157
|
+
if (this.writeBuffer.has(key)) {
|
|
158
|
+
return this.writeBuffer.get(key);
|
|
159
|
+
}
|
|
160
|
+
if (this.deleteBuffer.has(key)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return this.manager._diskRead(key, this.snapshotVersion);
|
|
164
|
+
}
|
|
165
|
+
commit() {
|
|
166
|
+
if (this.committed) throw new Error("Transaction already committed");
|
|
167
|
+
this.manager._commit(this);
|
|
168
|
+
this.committed = true;
|
|
169
|
+
this.manager._removeTransaction(this);
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// src/core/sync/Manager.ts
|
|
175
|
+
var SyncMVCCManager = class extends MVCCManager {
|
|
176
|
+
constructor(strategy) {
|
|
177
|
+
super(strategy);
|
|
178
|
+
}
|
|
179
|
+
createTransaction() {
|
|
180
|
+
const tx = new SyncMVCCTransaction(this, this.version);
|
|
181
|
+
this.activeTransactions.add(tx);
|
|
182
|
+
return tx;
|
|
183
|
+
}
|
|
184
|
+
_diskWrite(key, value, version) {
|
|
185
|
+
if (this.strategy.exists(key)) {
|
|
186
|
+
const oldValue = this.strategy.read(key);
|
|
187
|
+
if (!this.deletedCache.has(key)) {
|
|
188
|
+
this.deletedCache.set(key, []);
|
|
189
|
+
}
|
|
190
|
+
this.deletedCache.get(key).push({ value: oldValue, deletedAtVersion: version });
|
|
191
|
+
}
|
|
192
|
+
this.strategy.write(key, value);
|
|
193
|
+
if (!this.versionIndex.has(key)) {
|
|
194
|
+
this.versionIndex.set(key, []);
|
|
195
|
+
}
|
|
196
|
+
this.versionIndex.get(key).push({ version, exists: true });
|
|
197
|
+
}
|
|
198
|
+
_diskRead(key, snapshotVersion) {
|
|
199
|
+
if (!this.versionIndex.has(key) && !this.deletedCache.has(key)) {
|
|
200
|
+
if (this.strategy.exists(key)) {
|
|
201
|
+
return this.strategy.read(key);
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const versions = this.versionIndex.get(key);
|
|
206
|
+
if (versions && versions.length > 0) {
|
|
207
|
+
const visibleVersions = versions.filter((v) => v.version <= snapshotVersion && v.exists);
|
|
208
|
+
if (visibleVersions.length > 0) {
|
|
209
|
+
const newerVersions = versions.filter((v) => v.version > snapshotVersion);
|
|
210
|
+
if (newerVersions.length === 0) {
|
|
211
|
+
return this.strategy.read(key);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const cached = this.deletedCache.get(key);
|
|
216
|
+
if (cached) {
|
|
217
|
+
const visibleEntries = cached.filter((v) => v.deletedAtVersion > snapshotVersion);
|
|
218
|
+
if (visibleEntries.length > 0) {
|
|
219
|
+
visibleEntries.sort((a, b) => a.deletedAtVersion - b.deletedAtVersion);
|
|
220
|
+
return visibleEntries[0].value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
_diskDelete(key, snapshotVersion) {
|
|
226
|
+
if (this.strategy.exists(key)) {
|
|
227
|
+
const data = this.strategy.read(key);
|
|
228
|
+
if (!this.deletedCache.has(key)) {
|
|
229
|
+
this.deletedCache.set(key, []);
|
|
230
|
+
}
|
|
231
|
+
this.deletedCache.get(key).push({ deletedAtVersion: snapshotVersion, value: data });
|
|
232
|
+
this.strategy.delete(key);
|
|
233
|
+
}
|
|
234
|
+
if (!this.versionIndex.has(key)) {
|
|
235
|
+
this.versionIndex.set(key, []);
|
|
236
|
+
}
|
|
237
|
+
this.versionIndex.get(key).push({ version: snapshotVersion, exists: false });
|
|
238
|
+
}
|
|
239
|
+
_commit(tx) {
|
|
240
|
+
const isReadOnly = tx.writeBuffer.size === 0 && tx.deleteBuffer.size === 0;
|
|
241
|
+
if (!isReadOnly && this.version > tx.snapshotVersion) {
|
|
242
|
+
const affectedKeys = /* @__PURE__ */ new Set([
|
|
243
|
+
...tx.writeBuffer.keys(),
|
|
244
|
+
...tx.deleteBuffer
|
|
245
|
+
]);
|
|
246
|
+
for (const key of affectedKeys) {
|
|
247
|
+
const versions = this.versionIndex.get(key);
|
|
248
|
+
if (versions && versions.length > 0) {
|
|
249
|
+
const latestVersion = versions[versions.length - 1].version;
|
|
250
|
+
if (latestVersion > tx.snapshotVersion) {
|
|
251
|
+
throw new Error(`Commit conflict: file '${key}' was modified by another transaction`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
this.version++;
|
|
257
|
+
for (const key of tx.deleteBuffer) {
|
|
258
|
+
this._diskDelete(key, this.version);
|
|
259
|
+
}
|
|
260
|
+
for (const [key, value] of tx.writeBuffer) {
|
|
261
|
+
this._diskWrite(key, value, this.version);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// src/core/sync/Strategy.ts
|
|
267
|
+
var SyncMVCCStrategy = class extends MVCCStrategy {
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// node_modules/ryoiki/dist/esm/index.mjs
|
|
271
|
+
var Ryoiki = class _Ryoiki {
|
|
272
|
+
readings;
|
|
273
|
+
writings;
|
|
274
|
+
readQueue;
|
|
275
|
+
writeQueue;
|
|
276
|
+
static async CatchError(promise) {
|
|
277
|
+
return await promise.then((v) => [void 0, v]).catch((err) => [err]);
|
|
278
|
+
}
|
|
279
|
+
static IsRangeOverlap(a, b) {
|
|
280
|
+
const [start1, end1] = a;
|
|
281
|
+
const [start2, end2] = b;
|
|
282
|
+
if (end1 <= start2 || end2 <= start1) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
static ERR_ALREADY_EXISTS(lockId) {
|
|
288
|
+
return new Error(`The '${lockId}' task already existing in queue or running.`);
|
|
289
|
+
}
|
|
290
|
+
static ERR_NOT_EXISTS(lockId) {
|
|
291
|
+
return new Error(`The '${lockId}' task not existing in task queue.`);
|
|
292
|
+
}
|
|
293
|
+
static ERR_TIMEOUT(lockId, timeout) {
|
|
294
|
+
return new Error(`The task with ID '${lockId}' failed to acquire the lock within the timeout(${timeout}ms).`);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Constructs a new instance of the Ryoiki class.
|
|
298
|
+
*/
|
|
299
|
+
constructor() {
|
|
300
|
+
this.readings = /* @__PURE__ */ new Map();
|
|
301
|
+
this.writings = /* @__PURE__ */ new Map();
|
|
302
|
+
this.readQueue = /* @__PURE__ */ new Map();
|
|
303
|
+
this.writeQueue = /* @__PURE__ */ new Map();
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Creates a range based on a start value and length.
|
|
307
|
+
* @param start - The starting value of the range.
|
|
308
|
+
* @param length - The length of the range.
|
|
309
|
+
* @returns A range tuple [start, start + length].
|
|
310
|
+
*/
|
|
311
|
+
range(start, length) {
|
|
312
|
+
return [start, start + length];
|
|
313
|
+
}
|
|
314
|
+
rangeOverlapping(tasks, range) {
|
|
315
|
+
return Array.from(tasks.values()).some((t) => _Ryoiki.IsRangeOverlap(t.range, range));
|
|
316
|
+
}
|
|
317
|
+
isSameRange(a, b) {
|
|
318
|
+
const [a1, a2] = a;
|
|
319
|
+
const [b1, b2] = b;
|
|
320
|
+
return a1 === b1 && a2 === b2;
|
|
321
|
+
}
|
|
322
|
+
fetchUnitAndRun(queue, workspaces) {
|
|
323
|
+
for (const [id, unit] of queue) {
|
|
324
|
+
if (!unit.condition()) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
this._alloc(queue, workspaces, id);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
_handleOverload(args, handlers, argPatterns) {
|
|
331
|
+
for (const [key, pattern] of Object.entries(argPatterns)) {
|
|
332
|
+
if (this._matchArgs(args, pattern)) {
|
|
333
|
+
return handlers[key](...args);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
throw new Error("Invalid arguments");
|
|
337
|
+
}
|
|
338
|
+
_matchArgs(args, pattern) {
|
|
339
|
+
return args.every((arg, index) => {
|
|
340
|
+
const expectedType = pattern[index];
|
|
341
|
+
if (expectedType === void 0) return typeof arg === "undefined";
|
|
342
|
+
if (expectedType === Function) return typeof arg === "function";
|
|
343
|
+
if (expectedType === Number) return typeof arg === "number";
|
|
344
|
+
if (expectedType === Array) return Array.isArray(arg);
|
|
345
|
+
return false;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
_createRandomId() {
|
|
349
|
+
const timestamp = Date.now().toString(36);
|
|
350
|
+
const random = Math.random().toString(36).substring(2);
|
|
351
|
+
return `${timestamp}${random}`;
|
|
352
|
+
}
|
|
353
|
+
_alloc(queue, workspaces, lockId) {
|
|
354
|
+
const unit = queue.get(lockId);
|
|
355
|
+
if (!unit) {
|
|
356
|
+
throw _Ryoiki.ERR_NOT_EXISTS(lockId);
|
|
357
|
+
}
|
|
358
|
+
workspaces.set(lockId, unit);
|
|
359
|
+
queue.delete(lockId);
|
|
360
|
+
unit.alloc();
|
|
361
|
+
}
|
|
362
|
+
_free(workspaces, lockId) {
|
|
363
|
+
const unit = workspaces.get(lockId);
|
|
364
|
+
if (!unit) {
|
|
365
|
+
throw _Ryoiki.ERR_NOT_EXISTS(lockId);
|
|
366
|
+
}
|
|
367
|
+
workspaces.delete(lockId);
|
|
368
|
+
unit.free();
|
|
369
|
+
}
|
|
370
|
+
_lock(queue, range, timeout, task, condition) {
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
let timeoutId = null;
|
|
373
|
+
if (timeout >= 0) {
|
|
374
|
+
timeoutId = setTimeout(() => {
|
|
375
|
+
reject(_Ryoiki.ERR_TIMEOUT(id, timeout));
|
|
376
|
+
}, timeout);
|
|
377
|
+
}
|
|
378
|
+
const id = this._createRandomId();
|
|
379
|
+
const alloc = async () => {
|
|
380
|
+
if (timeoutId !== null) {
|
|
381
|
+
clearTimeout(timeoutId);
|
|
382
|
+
}
|
|
383
|
+
const [err, v] = await _Ryoiki.CatchError(task(id));
|
|
384
|
+
if (err) reject(err);
|
|
385
|
+
else resolve(v);
|
|
386
|
+
};
|
|
387
|
+
const fetch = () => {
|
|
388
|
+
this.fetchUnitAndRun(this.readQueue, this.readings);
|
|
389
|
+
this.fetchUnitAndRun(this.writeQueue, this.writings);
|
|
390
|
+
};
|
|
391
|
+
queue.set(id, { id, range, condition, alloc, free: fetch });
|
|
392
|
+
fetch();
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
_checkWorking(range, workspaces) {
|
|
396
|
+
let isLocked = false;
|
|
397
|
+
for (const lock of workspaces.values()) {
|
|
398
|
+
if (_Ryoiki.IsRangeOverlap(range, lock.range)) {
|
|
399
|
+
isLocked = true;
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return isLocked;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Checks if there is any active read lock within the specified range.
|
|
407
|
+
* @param range The range to check for active read locks.
|
|
408
|
+
* @returns `true` if there is an active read lock within the range, `false` otherwise.
|
|
409
|
+
*/
|
|
410
|
+
isReading(range) {
|
|
411
|
+
return this._checkWorking(range, this.readings);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Checks if there is any active write lock within the specified range.
|
|
415
|
+
* @param range The range to check for active write locks.
|
|
416
|
+
* @returns `true` if there is an active write lock within the range, `false` otherwise.
|
|
417
|
+
*/
|
|
418
|
+
isWriting(range) {
|
|
419
|
+
return this._checkWorking(range, this.writings);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Checks if a read lock can be acquired within the specified range.
|
|
423
|
+
* @param range The range to check for read lock availability.
|
|
424
|
+
* @returns `true` if a read lock can be acquired, `false` otherwise.
|
|
425
|
+
*/
|
|
426
|
+
canRead(range) {
|
|
427
|
+
const writing = this.isWriting(range);
|
|
428
|
+
return !writing;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Checks if a write lock can be acquired within the specified range.
|
|
432
|
+
* @param range The range to check for write lock availability.
|
|
433
|
+
* @returns `true` if a write lock can be acquired, `false` otherwise.
|
|
434
|
+
*/
|
|
435
|
+
canWrite(range) {
|
|
436
|
+
const reading = this.isReading(range);
|
|
437
|
+
const writing = this.isWriting(range);
|
|
438
|
+
return !reading && !writing;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Internal implementation of the read lock. Handles both overloads.
|
|
442
|
+
* @template T - The return type of the task.
|
|
443
|
+
* @param arg0 - Either a range or a task callback.
|
|
444
|
+
* If a range is provided, the task is the second argument.
|
|
445
|
+
* @param arg1 - The task to execute, required if a range is provided.
|
|
446
|
+
* @param arg2 - The timeout for acquiring the lock.
|
|
447
|
+
* If the lock cannot be acquired within this period, an error will be thrown.
|
|
448
|
+
* If this value is not provided, no timeout will be set.
|
|
449
|
+
* @returns A promise resolving to the result of the task execution.
|
|
450
|
+
*/
|
|
451
|
+
readLock(arg0, arg1, arg2) {
|
|
452
|
+
const [range, task, timeout] = this._handleOverload(
|
|
453
|
+
[arg0, arg1, arg2],
|
|
454
|
+
{
|
|
455
|
+
rangeTask: (range2, task2) => [range2, task2, -1],
|
|
456
|
+
rangeTaskTimeout: (range2, task2, timeout2) => [range2, task2, timeout2],
|
|
457
|
+
task: (task2) => [[-Infinity, Infinity], task2, -1],
|
|
458
|
+
taskTimeout: (task2, timeout2) => [[-Infinity, Infinity], task2, timeout2]
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
task: [Function],
|
|
462
|
+
taskTimeout: [Function, Number],
|
|
463
|
+
rangeTask: [Array, Function],
|
|
464
|
+
rangeTaskTimeout: [Array, Function, Number]
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
return this._lock(
|
|
468
|
+
this.readQueue,
|
|
469
|
+
range,
|
|
470
|
+
timeout,
|
|
471
|
+
task,
|
|
472
|
+
() => !this.rangeOverlapping(this.writings, range)
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Internal implementation of the write lock. Handles both overloads.
|
|
477
|
+
* @template T - The return type of the task.
|
|
478
|
+
* @param arg0 - Either a range or a task callback.
|
|
479
|
+
* If a range is provided, the task is the second argument.
|
|
480
|
+
* @param arg1 - The task to execute, required if a range is provided.
|
|
481
|
+
* @param arg2 - The timeout for acquiring the lock.
|
|
482
|
+
* If the lock cannot be acquired within this period, an error will be thrown.
|
|
483
|
+
* If this value is not provided, no timeout will be set.
|
|
484
|
+
* @returns A promise resolving to the result of the task execution.
|
|
485
|
+
*/
|
|
486
|
+
writeLock(arg0, arg1, arg2) {
|
|
487
|
+
const [range, task, timeout] = this._handleOverload(
|
|
488
|
+
[arg0, arg1, arg2],
|
|
489
|
+
{
|
|
490
|
+
rangeTask: (range2, task2) => [range2, task2, -1],
|
|
491
|
+
rangeTaskTimeout: (range2, task2, timeout2) => [range2, task2, timeout2],
|
|
492
|
+
task: (task2) => [[-Infinity, Infinity], task2, -1],
|
|
493
|
+
taskTimeout: (task2, timeout2) => [[-Infinity, Infinity], task2, timeout2]
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
task: [Function],
|
|
497
|
+
taskTimeout: [Function, Number],
|
|
498
|
+
rangeTask: [Array, Function],
|
|
499
|
+
rangeTaskTimeout: [Array, Function, Number]
|
|
500
|
+
}
|
|
501
|
+
);
|
|
502
|
+
return this._lock(
|
|
503
|
+
this.writeQueue,
|
|
504
|
+
range,
|
|
505
|
+
timeout,
|
|
506
|
+
task,
|
|
507
|
+
() => {
|
|
508
|
+
return !this.rangeOverlapping(this.writings, range) && !this.rangeOverlapping(this.readings, range);
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Releases a read lock by its lock ID.
|
|
514
|
+
* @param lockId - The unique identifier for the lock to release.
|
|
515
|
+
*/
|
|
516
|
+
readUnlock(lockId) {
|
|
517
|
+
this._free(this.readings, lockId);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Releases a write lock by its lock ID.
|
|
521
|
+
* @param lockId - The unique identifier for the lock to release.
|
|
522
|
+
*/
|
|
523
|
+
writeUnlock(lockId) {
|
|
524
|
+
this._free(this.writings, lockId);
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// src/core/async/Transaction.ts
|
|
529
|
+
var AsyncMVCCTransaction = class extends MVCCTransaction {
|
|
530
|
+
async read(key) {
|
|
531
|
+
if (this.committed) throw new Error("Transaction already committed");
|
|
532
|
+
if (this.writeBuffer.has(key)) {
|
|
533
|
+
return this.writeBuffer.get(key);
|
|
534
|
+
}
|
|
535
|
+
if (this.deleteBuffer.has(key)) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
return this.manager._diskRead(key, this.snapshotVersion);
|
|
539
|
+
}
|
|
540
|
+
async commit() {
|
|
541
|
+
return this.manager.writeLock(async () => {
|
|
542
|
+
if (this.committed) throw new Error("Transaction already committed");
|
|
543
|
+
await this.manager._commit(this);
|
|
544
|
+
this.committed = true;
|
|
545
|
+
this.manager._removeTransaction(this);
|
|
546
|
+
return this;
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// src/core/async/Manager.ts
|
|
552
|
+
var AsyncMVCCManager = class extends MVCCManager {
|
|
553
|
+
lock;
|
|
554
|
+
constructor(strategy) {
|
|
555
|
+
super(strategy);
|
|
556
|
+
this.lock = new Ryoiki();
|
|
557
|
+
}
|
|
558
|
+
createTransaction() {
|
|
559
|
+
const tx = new AsyncMVCCTransaction(this, this.version);
|
|
560
|
+
this.activeTransactions.add(tx);
|
|
561
|
+
return tx;
|
|
562
|
+
}
|
|
563
|
+
async writeLock(fn) {
|
|
564
|
+
let lockId;
|
|
565
|
+
return this.lock.writeLock(async (_lockId) => {
|
|
566
|
+
lockId = _lockId;
|
|
567
|
+
return fn();
|
|
568
|
+
}).finally(() => {
|
|
569
|
+
this.lock.writeUnlock(lockId);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
async _diskWrite(key, value, version) {
|
|
573
|
+
if (await this.strategy.exists(key)) {
|
|
574
|
+
const oldValue = await this.strategy.read(key);
|
|
575
|
+
if (!this.deletedCache.has(key)) {
|
|
576
|
+
this.deletedCache.set(key, []);
|
|
577
|
+
}
|
|
578
|
+
this.deletedCache.get(key).push({ value: oldValue, deletedAtVersion: version });
|
|
579
|
+
}
|
|
580
|
+
await this.strategy.write(key, value);
|
|
581
|
+
if (!this.versionIndex.has(key)) {
|
|
582
|
+
this.versionIndex.set(key, []);
|
|
583
|
+
}
|
|
584
|
+
this.versionIndex.get(key).push({ version, exists: true });
|
|
585
|
+
}
|
|
586
|
+
async _diskRead(key, snapshotVersion) {
|
|
587
|
+
if (!this.versionIndex.has(key) && !this.deletedCache.has(key)) {
|
|
588
|
+
if (await this.strategy.exists(key)) {
|
|
589
|
+
return this.strategy.read(key);
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
const versions = this.versionIndex.get(key);
|
|
594
|
+
if (versions && versions.length > 0) {
|
|
595
|
+
const visibleVersions = versions.filter((v) => v.version <= snapshotVersion && v.exists);
|
|
596
|
+
if (visibleVersions.length > 0) {
|
|
597
|
+
const newerVersions = versions.filter((v) => v.version > snapshotVersion);
|
|
598
|
+
if (newerVersions.length === 0) {
|
|
599
|
+
return this.strategy.read(key);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const cached = this.deletedCache.get(key);
|
|
604
|
+
if (cached) {
|
|
605
|
+
const visibleEntries = cached.filter((v) => v.deletedAtVersion > snapshotVersion);
|
|
606
|
+
if (visibleEntries.length > 0) {
|
|
607
|
+
visibleEntries.sort((a, b) => a.deletedAtVersion - b.deletedAtVersion);
|
|
608
|
+
return visibleEntries[0].value;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
async _diskDelete(key, snapshotVersion) {
|
|
614
|
+
if (await this.strategy.exists(key)) {
|
|
615
|
+
const data = await this.strategy.read(key);
|
|
616
|
+
if (!this.deletedCache.has(key)) {
|
|
617
|
+
this.deletedCache.set(key, []);
|
|
618
|
+
}
|
|
619
|
+
this.deletedCache.get(key).push({ deletedAtVersion: snapshotVersion, value: data });
|
|
620
|
+
await this.strategy.delete(key);
|
|
621
|
+
}
|
|
622
|
+
if (!this.versionIndex.has(key)) {
|
|
623
|
+
this.versionIndex.set(key, []);
|
|
624
|
+
}
|
|
625
|
+
this.versionIndex.get(key).push({ version: snapshotVersion, exists: false });
|
|
626
|
+
}
|
|
627
|
+
async _commit(tx) {
|
|
628
|
+
const isReadOnly = tx.writeBuffer.size === 0 && tx.deleteBuffer.size === 0;
|
|
629
|
+
if (!isReadOnly && this.version > tx.snapshotVersion) {
|
|
630
|
+
const affectedKeys = /* @__PURE__ */ new Set([
|
|
631
|
+
...tx.writeBuffer.keys(),
|
|
632
|
+
...tx.deleteBuffer
|
|
633
|
+
]);
|
|
634
|
+
for (const key of affectedKeys) {
|
|
635
|
+
const versions = this.versionIndex.get(key);
|
|
636
|
+
if (versions && versions.length > 0) {
|
|
637
|
+
const latestVersion = versions[versions.length - 1].version;
|
|
638
|
+
if (latestVersion > tx.snapshotVersion) {
|
|
639
|
+
throw new Error(`Commit conflict: file '${key}' was modified by another transaction`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
this.version++;
|
|
645
|
+
for (const key of tx.deleteBuffer) {
|
|
646
|
+
await this._diskDelete(key, this.version);
|
|
647
|
+
}
|
|
648
|
+
for (const [key, value] of tx.writeBuffer) {
|
|
649
|
+
await this._diskWrite(key, value, this.version);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
// src/core/async/Strategy.ts
|
|
655
|
+
var AsyncMVCCStrategy = class extends MVCCStrategy {
|
|
656
|
+
};
|
|
657
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
658
|
+
0 && (module.exports = {
|
|
659
|
+
AsyncMVCCManager,
|
|
660
|
+
AsyncMVCCStrategy,
|
|
661
|
+
AsyncMVCCTransaction,
|
|
662
|
+
SyncMVCCManager,
|
|
663
|
+
SyncMVCCStrategy,
|
|
664
|
+
SyncMVCCTransaction
|
|
665
|
+
});
|