@frostpillar/frostpillar-storage-engine 0.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-JA.md +1205 -0
- package/README.md +1204 -0
- package/dist/drivers/file.cjs +960 -0
- package/dist/drivers/file.d.ts +3 -0
- package/dist/drivers/file.js +18 -0
- package/dist/drivers/indexedDB.cjs +570 -0
- package/dist/drivers/indexedDB.d.ts +3 -0
- package/dist/drivers/indexedDB.js +18 -0
- package/dist/drivers/localStorage.cjs +668 -0
- package/dist/drivers/localStorage.d.ts +3 -0
- package/dist/drivers/localStorage.js +23 -0
- package/dist/drivers/opfs.cjs +550 -0
- package/dist/drivers/opfs.d.ts +3 -0
- package/dist/drivers/opfs.js +18 -0
- package/dist/drivers/syncStorage.cjs +898 -0
- package/dist/drivers/syncStorage.d.ts +3 -0
- package/dist/drivers/syncStorage.js +22 -0
- package/dist/drivers/validation.d.ts +1 -0
- package/dist/drivers/validation.js +8 -0
- package/dist/errors/index.d.ts +32 -0
- package/dist/errors/index.js +48 -0
- package/dist/frostpillar-storage-engine.min.js +1 -0
- package/dist/index.cjs +2957 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/storage/backend/asyncDurableAutoCommitController.d.ts +26 -0
- package/dist/storage/backend/asyncDurableAutoCommitController.js +188 -0
- package/dist/storage/backend/asyncMutex.d.ts +7 -0
- package/dist/storage/backend/asyncMutex.js +38 -0
- package/dist/storage/backend/autoCommit.d.ts +2 -0
- package/dist/storage/backend/autoCommit.js +22 -0
- package/dist/storage/backend/capacity.d.ts +2 -0
- package/dist/storage/backend/capacity.js +27 -0
- package/dist/storage/backend/capacityResolver.d.ts +3 -0
- package/dist/storage/backend/capacityResolver.js +25 -0
- package/dist/storage/backend/encoding.d.ts +17 -0
- package/dist/storage/backend/encoding.js +148 -0
- package/dist/storage/backend/types.d.ts +184 -0
- package/dist/storage/backend/types.js +1 -0
- package/dist/storage/btree/recordKeyIndexBTree.d.ts +39 -0
- package/dist/storage/btree/recordKeyIndexBTree.js +104 -0
- package/dist/storage/config/config.browser.d.ts +4 -0
- package/dist/storage/config/config.browser.js +8 -0
- package/dist/storage/config/config.d.ts +1 -0
- package/dist/storage/config/config.js +1 -0
- package/dist/storage/config/config.node.d.ts +4 -0
- package/dist/storage/config/config.node.js +74 -0
- package/dist/storage/config/config.shared.d.ts +6 -0
- package/dist/storage/config/config.shared.js +105 -0
- package/dist/storage/datastore/Datastore.d.ts +47 -0
- package/dist/storage/datastore/Datastore.js +525 -0
- package/dist/storage/datastore/datastoreClose.d.ts +12 -0
- package/dist/storage/datastore/datastoreClose.js +60 -0
- package/dist/storage/datastore/datastoreKeyDefinition.d.ts +7 -0
- package/dist/storage/datastore/datastoreKeyDefinition.js +60 -0
- package/dist/storage/datastore/datastoreLifecycle.d.ts +18 -0
- package/dist/storage/datastore/datastoreLifecycle.js +63 -0
- package/dist/storage/datastore/mutationById.d.ts +29 -0
- package/dist/storage/datastore/mutationById.js +71 -0
- package/dist/storage/drivers/IndexedDB/indexedDBBackend.d.ts +11 -0
- package/dist/storage/drivers/IndexedDB/indexedDBBackend.js +109 -0
- package/dist/storage/drivers/IndexedDB/indexedDBBackendController.d.ts +27 -0
- package/dist/storage/drivers/IndexedDB/indexedDBBackendController.js +60 -0
- package/dist/storage/drivers/IndexedDB/indexedDBConfig.d.ts +7 -0
- package/dist/storage/drivers/IndexedDB/indexedDBConfig.js +24 -0
- package/dist/storage/drivers/file/fileBackend.d.ts +5 -0
- package/dist/storage/drivers/file/fileBackend.js +168 -0
- package/dist/storage/drivers/file/fileBackendController.d.ts +31 -0
- package/dist/storage/drivers/file/fileBackendController.js +72 -0
- package/dist/storage/drivers/file/fileBackendSnapshot.d.ts +10 -0
- package/dist/storage/drivers/file/fileBackendSnapshot.js +166 -0
- package/dist/storage/drivers/localStorage/localStorageBackend.d.ts +10 -0
- package/dist/storage/drivers/localStorage/localStorageBackend.js +156 -0
- package/dist/storage/drivers/localStorage/localStorageBackendController.d.ts +24 -0
- package/dist/storage/drivers/localStorage/localStorageBackendController.js +35 -0
- package/dist/storage/drivers/localStorage/localStorageConfig.d.ts +10 -0
- package/dist/storage/drivers/localStorage/localStorageConfig.js +16 -0
- package/dist/storage/drivers/localStorage/localStorageLayout.d.ts +5 -0
- package/dist/storage/drivers/localStorage/localStorageLayout.js +29 -0
- package/dist/storage/drivers/opfs/opfsBackend.d.ts +12 -0
- package/dist/storage/drivers/opfs/opfsBackend.js +142 -0
- package/dist/storage/drivers/opfs/opfsBackendController.d.ts +26 -0
- package/dist/storage/drivers/opfs/opfsBackendController.js +44 -0
- package/dist/storage/drivers/syncStorage/syncStorageAdapter.d.ts +2 -0
- package/dist/storage/drivers/syncStorage/syncStorageAdapter.js +123 -0
- package/dist/storage/drivers/syncStorage/syncStorageBackend.d.ts +11 -0
- package/dist/storage/drivers/syncStorage/syncStorageBackend.js +169 -0
- package/dist/storage/drivers/syncStorage/syncStorageBackendController.d.ts +24 -0
- package/dist/storage/drivers/syncStorage/syncStorageBackendController.js +34 -0
- package/dist/storage/drivers/syncStorage/syncStorageChunkMaintenance.d.ts +2 -0
- package/dist/storage/drivers/syncStorage/syncStorageChunkMaintenance.js +28 -0
- package/dist/storage/drivers/syncStorage/syncStorageConfig.d.ts +13 -0
- package/dist/storage/drivers/syncStorage/syncStorageConfig.js +42 -0
- package/dist/storage/drivers/syncStorage/syncStorageQuota.d.ts +3 -0
- package/dist/storage/drivers/syncStorage/syncStorageQuota.js +45 -0
- package/dist/storage/record/ordering.d.ts +3 -0
- package/dist/storage/record/ordering.js +7 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.js +1 -0
- package/dist/validation/metadata.d.ts +1 -0
- package/dist/validation/metadata.js +7 -0
- package/dist/validation/payload.d.ts +7 -0
- package/dist/validation/payload.js +135 -0
- package/dist/validation/typeGuards.d.ts +1 -0
- package/dist/validation/typeGuards.js +7 -0
- package/package.json +110 -0
|
@@ -0,0 +1,960 @@
|
|
|
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/drivers/file.ts
|
|
21
|
+
var file_exports = {};
|
|
22
|
+
__export(file_exports, {
|
|
23
|
+
fileDriver: () => fileDriver
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(file_exports);
|
|
26
|
+
|
|
27
|
+
// src/storage/drivers/file/fileBackendController.ts
|
|
28
|
+
var import_node_fs4 = require("node:fs");
|
|
29
|
+
|
|
30
|
+
// src/errors/index.ts
|
|
31
|
+
var FrostpillarError = class extends Error {
|
|
32
|
+
constructor(message, options) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = new.target.name;
|
|
35
|
+
if (options !== void 0) {
|
|
36
|
+
this.cause = options.cause;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var ConfigurationError = class extends FrostpillarError {
|
|
41
|
+
};
|
|
42
|
+
var StorageEngineError = class extends FrostpillarError {
|
|
43
|
+
};
|
|
44
|
+
var DatabaseLockedError = class extends StorageEngineError {
|
|
45
|
+
};
|
|
46
|
+
var PageCorruptionError = class extends StorageEngineError {
|
|
47
|
+
};
|
|
48
|
+
var IndexCorruptionError = class extends StorageEngineError {
|
|
49
|
+
};
|
|
50
|
+
var toStorageEngineError = (error, fallbackMessage) => {
|
|
51
|
+
if (error instanceof StorageEngineError) {
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
return new StorageEngineError(`${fallbackMessage}: ${error.message}`, {
|
|
56
|
+
cause: error
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return new StorageEngineError(fallbackMessage, { cause: error });
|
|
60
|
+
};
|
|
61
|
+
var toErrorInstance = (error, fallbackMessage) => {
|
|
62
|
+
if (error instanceof Error) {
|
|
63
|
+
return error;
|
|
64
|
+
}
|
|
65
|
+
return new Error(fallbackMessage, { cause: error });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/storage/backend/asyncDurableAutoCommitController.ts
|
|
69
|
+
var AsyncDurableAutoCommitController = class {
|
|
70
|
+
constructor(autoCommit, onAutoCommitError) {
|
|
71
|
+
this.autoCommit = autoCommit;
|
|
72
|
+
this.onAutoCommitError = onAutoCommitError;
|
|
73
|
+
this.pendingAutoCommitBytes = 0;
|
|
74
|
+
this.dirtyFromClear = false;
|
|
75
|
+
this.autoCommitTimer = null;
|
|
76
|
+
this.commitInFlight = null;
|
|
77
|
+
this.pendingForegroundCommitRequest = false;
|
|
78
|
+
this.pendingBackgroundCommitRequest = false;
|
|
79
|
+
this.closed = false;
|
|
80
|
+
this.startAutoCommitSchedule();
|
|
81
|
+
}
|
|
82
|
+
handleRecordAppended(encodedBytes) {
|
|
83
|
+
if (this.autoCommit.frequency === "immediate") {
|
|
84
|
+
return this.commitNow();
|
|
85
|
+
}
|
|
86
|
+
this.pendingAutoCommitBytes += encodedBytes;
|
|
87
|
+
if (this.autoCommit.maxPendingBytes !== null && this.pendingAutoCommitBytes >= this.autoCommit.maxPendingBytes) {
|
|
88
|
+
return this.queueCommitRequest("foreground");
|
|
89
|
+
}
|
|
90
|
+
return Promise.resolve();
|
|
91
|
+
}
|
|
92
|
+
handleCleared() {
|
|
93
|
+
this.dirtyFromClear = true;
|
|
94
|
+
if (this.autoCommit.frequency === "immediate") {
|
|
95
|
+
return this.commitNow();
|
|
96
|
+
}
|
|
97
|
+
return this.queueCommitRequest("background");
|
|
98
|
+
}
|
|
99
|
+
commitNow() {
|
|
100
|
+
return this.queueCommitRequest("foreground");
|
|
101
|
+
}
|
|
102
|
+
async close() {
|
|
103
|
+
if (this.closed) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.closed = true;
|
|
107
|
+
this.stopAutoCommitSchedule();
|
|
108
|
+
await this.waitForCommitSettlement();
|
|
109
|
+
let flushError = null;
|
|
110
|
+
if (this.pendingAutoCommitBytes > 0 || this.dirtyFromClear) {
|
|
111
|
+
try {
|
|
112
|
+
await this.executeSingleCommit();
|
|
113
|
+
this.pendingAutoCommitBytes = 0;
|
|
114
|
+
this.dirtyFromClear = false;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
flushError = toErrorInstance(
|
|
117
|
+
error,
|
|
118
|
+
"Final close-time flush commit failed with a non-Error value."
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
let drainError = null;
|
|
123
|
+
try {
|
|
124
|
+
await this.onCloseAfterDrain();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
drainError = toErrorInstance(
|
|
127
|
+
error,
|
|
128
|
+
"onCloseAfterDrain failed with a non-Error value."
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (flushError !== null && drainError !== null) {
|
|
132
|
+
throw createCloseAggregateError(flushError, drainError);
|
|
133
|
+
}
|
|
134
|
+
if (flushError !== null) {
|
|
135
|
+
throw flushError;
|
|
136
|
+
}
|
|
137
|
+
if (drainError !== null) {
|
|
138
|
+
throw drainError;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
getPendingAutoCommitBytes() {
|
|
142
|
+
return this.pendingAutoCommitBytes;
|
|
143
|
+
}
|
|
144
|
+
onCloseAfterDrain() {
|
|
145
|
+
return Promise.resolve();
|
|
146
|
+
}
|
|
147
|
+
waitForCommitSettlement() {
|
|
148
|
+
if (this.commitInFlight === null) {
|
|
149
|
+
return Promise.resolve();
|
|
150
|
+
}
|
|
151
|
+
return this.commitInFlight.then(() => void 0).catch(() => void 0);
|
|
152
|
+
}
|
|
153
|
+
queueCommitRequest(requestType) {
|
|
154
|
+
if (requestType === "foreground") {
|
|
155
|
+
this.pendingForegroundCommitRequest = true;
|
|
156
|
+
} else {
|
|
157
|
+
this.pendingBackgroundCommitRequest = true;
|
|
158
|
+
}
|
|
159
|
+
if (this.commitInFlight === null) {
|
|
160
|
+
this.commitInFlight = this.runCommitLoop().finally(() => {
|
|
161
|
+
this.commitInFlight = null;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (requestType === "background") {
|
|
165
|
+
return Promise.resolve();
|
|
166
|
+
}
|
|
167
|
+
return this.commitInFlight;
|
|
168
|
+
}
|
|
169
|
+
async runCommitLoop() {
|
|
170
|
+
let shouldContinue = true;
|
|
171
|
+
while (shouldContinue) {
|
|
172
|
+
const runForeground = this.pendingForegroundCommitRequest;
|
|
173
|
+
const runBackground = this.pendingBackgroundCommitRequest;
|
|
174
|
+
const runClear = this.dirtyFromClear;
|
|
175
|
+
this.pendingForegroundCommitRequest = false;
|
|
176
|
+
this.pendingBackgroundCommitRequest = false;
|
|
177
|
+
this.dirtyFromClear = false;
|
|
178
|
+
const shouldRunCommit = runForeground || runBackground && (this.pendingAutoCommitBytes > 0 || runClear);
|
|
179
|
+
if (!shouldRunCommit) {
|
|
180
|
+
shouldContinue = false;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const committedPendingBytes = this.pendingAutoCommitBytes;
|
|
185
|
+
await this.executeSingleCommit();
|
|
186
|
+
this.pendingAutoCommitBytes = Math.max(
|
|
187
|
+
0,
|
|
188
|
+
this.pendingAutoCommitBytes - committedPendingBytes
|
|
189
|
+
);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (runClear) {
|
|
192
|
+
this.dirtyFromClear = true;
|
|
193
|
+
}
|
|
194
|
+
if (runForeground) {
|
|
195
|
+
throw toErrorInstance(
|
|
196
|
+
error,
|
|
197
|
+
"Foreground auto-commit failed with a non-Error value."
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
this.onAutoCommitError(error);
|
|
201
|
+
}
|
|
202
|
+
if (!this.pendingForegroundCommitRequest && !this.pendingBackgroundCommitRequest) {
|
|
203
|
+
shouldContinue = false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
startAutoCommitSchedule() {
|
|
208
|
+
if (this.autoCommit.frequency !== "scheduled" || this.autoCommit.intervalMs === null) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
this.autoCommitTimer = setInterval(() => {
|
|
212
|
+
this.handleAutoCommitTick();
|
|
213
|
+
}, this.autoCommit.intervalMs);
|
|
214
|
+
if (typeof this.autoCommitTimer === "object" && this.autoCommitTimer !== null && "unref" in this.autoCommitTimer) {
|
|
215
|
+
this.autoCommitTimer.unref();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
stopAutoCommitSchedule() {
|
|
219
|
+
if (this.autoCommitTimer === null) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
clearInterval(this.autoCommitTimer);
|
|
223
|
+
this.autoCommitTimer = null;
|
|
224
|
+
}
|
|
225
|
+
handleAutoCommitTick() {
|
|
226
|
+
if (this.closed) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (this.pendingAutoCommitBytes <= 0 && !this.dirtyFromClear) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
void this.queueCommitRequest("background");
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
var readAggregateErrorConstructor = () => {
|
|
236
|
+
const candidate = globalThis.AggregateError;
|
|
237
|
+
if (typeof candidate !== "function") {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return candidate;
|
|
241
|
+
};
|
|
242
|
+
var createCloseAggregateError = (flushError, drainError) => {
|
|
243
|
+
const aggregateErrorConstructor = readAggregateErrorConstructor();
|
|
244
|
+
if (aggregateErrorConstructor !== null) {
|
|
245
|
+
return new aggregateErrorConstructor(
|
|
246
|
+
[flushError, drainError],
|
|
247
|
+
"Close failed: both final flush and drain produced errors."
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const fallbackError = new Error(
|
|
251
|
+
"Close failed: both final flush and drain produced errors."
|
|
252
|
+
);
|
|
253
|
+
fallbackError.errors = [flushError, drainError];
|
|
254
|
+
return fallbackError;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// src/storage/config/config.shared.ts
|
|
258
|
+
var BYTE_SIZE_MULTIPLIER = {
|
|
259
|
+
B: 1,
|
|
260
|
+
KB: 1024,
|
|
261
|
+
MB: 1024 * 1024,
|
|
262
|
+
GB: 1024 * 1024 * 1024
|
|
263
|
+
};
|
|
264
|
+
var FREQUENCY_REGEX = /^(\d+)(ms|s|m|h)$/;
|
|
265
|
+
var FREQUENCY_MULTIPLIER = {
|
|
266
|
+
ms: 1,
|
|
267
|
+
s: 1e3,
|
|
268
|
+
m: 60 * 1e3,
|
|
269
|
+
h: 60 * 60 * 1e3
|
|
270
|
+
};
|
|
271
|
+
var parseFrequencyString = (frequency) => {
|
|
272
|
+
const matched = FREQUENCY_REGEX.exec(frequency);
|
|
273
|
+
if (matched === null) {
|
|
274
|
+
throw new ConfigurationError(
|
|
275
|
+
"autoCommit.frequency string must be one of: <positive>ms, <positive>s, <positive>m, <positive>h."
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const amount = Number(matched[1]);
|
|
279
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
280
|
+
throw new ConfigurationError(
|
|
281
|
+
"autoCommit.frequency string amount must be a positive safe integer."
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const unit = matched[2];
|
|
285
|
+
const multiplier = FREQUENCY_MULTIPLIER[unit];
|
|
286
|
+
const intervalMs = amount * multiplier;
|
|
287
|
+
if (!Number.isSafeInteger(intervalMs) || intervalMs <= 0) {
|
|
288
|
+
throw new ConfigurationError(
|
|
289
|
+
"autoCommit.frequency exceeds safe integer range."
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return intervalMs;
|
|
293
|
+
};
|
|
294
|
+
var parseAutoCommitConfig = (autoCommit) => {
|
|
295
|
+
if (autoCommit?.maxPendingBytes !== void 0) {
|
|
296
|
+
if (!Number.isSafeInteger(autoCommit.maxPendingBytes) || autoCommit.maxPendingBytes <= 0) {
|
|
297
|
+
throw new ConfigurationError(
|
|
298
|
+
"autoCommit.maxPendingBytes must be a positive safe integer."
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const maxPendingBytes = autoCommit?.maxPendingBytes ?? null;
|
|
303
|
+
const frequency = autoCommit?.frequency;
|
|
304
|
+
if (frequency === void 0 || frequency === "immediate") {
|
|
305
|
+
return { frequency: "immediate", intervalMs: null, maxPendingBytes };
|
|
306
|
+
}
|
|
307
|
+
if (typeof frequency === "number") {
|
|
308
|
+
if (!Number.isSafeInteger(frequency) || frequency <= 0) {
|
|
309
|
+
throw new ConfigurationError(
|
|
310
|
+
"autoCommit.frequency number must be a positive safe integer."
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return { frequency: "scheduled", intervalMs: frequency, maxPendingBytes };
|
|
314
|
+
}
|
|
315
|
+
const intervalMs = parseFrequencyString(frequency);
|
|
316
|
+
return { frequency: "scheduled", intervalMs, maxPendingBytes };
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// src/storage/drivers/file/fileBackend.ts
|
|
320
|
+
var import_node_fs2 = require("node:fs");
|
|
321
|
+
var import_node_path2 = require("node:path");
|
|
322
|
+
|
|
323
|
+
// src/storage/config/config.node.ts
|
|
324
|
+
var import_node_fs = require("node:fs");
|
|
325
|
+
var import_node_path = require("node:path");
|
|
326
|
+
var containsPathTraversalToken = (value) => {
|
|
327
|
+
return value.includes("..");
|
|
328
|
+
};
|
|
329
|
+
var hasPathSeparator = (value) => {
|
|
330
|
+
return value.includes("/") || value.includes("\\");
|
|
331
|
+
};
|
|
332
|
+
var isPathWithinBaseDirectory = (targetPath, baseDirectory) => {
|
|
333
|
+
const relativePath = (0, import_node_path.relative)(baseDirectory, targetPath);
|
|
334
|
+
return relativePath === "" || !relativePath.startsWith("..") && !(0, import_node_path.isAbsolute)(relativePath);
|
|
335
|
+
};
|
|
336
|
+
var resolveNearestExistingAncestor = (targetPath) => {
|
|
337
|
+
let currentPath = (0, import_node_path.resolve)(targetPath);
|
|
338
|
+
while (!(0, import_node_fs.existsSync)(currentPath)) {
|
|
339
|
+
const parentPath = (0, import_node_path.dirname)(currentPath);
|
|
340
|
+
if (parentPath === currentPath) {
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
currentPath = parentPath;
|
|
344
|
+
}
|
|
345
|
+
return currentPath;
|
|
346
|
+
};
|
|
347
|
+
var resolveCanonicalPathForContainment = (targetPath) => {
|
|
348
|
+
const resolvedTargetPath = (0, import_node_path.resolve)(targetPath);
|
|
349
|
+
const nearestExistingAncestor = resolveNearestExistingAncestor(resolvedTargetPath);
|
|
350
|
+
const canonicalAncestor = (0, import_node_fs.realpathSync)(nearestExistingAncestor);
|
|
351
|
+
const relativeSuffix = (0, import_node_path.relative)(nearestExistingAncestor, resolvedTargetPath);
|
|
352
|
+
return (0, import_node_path.resolve)((0, import_node_path.join)(canonicalAncestor, relativeSuffix));
|
|
353
|
+
};
|
|
354
|
+
var ensureCanonicalPathWithinWorkingDirectory = (targetPath, optionName) => {
|
|
355
|
+
const canonicalWorkingDirectory = (0, import_node_fs.realpathSync)((0, import_node_path.resolve)(process.cwd()));
|
|
356
|
+
const canonicalTargetPath = resolveCanonicalPathForContainment(targetPath);
|
|
357
|
+
if (!isPathWithinBaseDirectory(canonicalTargetPath, canonicalWorkingDirectory)) {
|
|
358
|
+
throw new ConfigurationError(
|
|
359
|
+
`${optionName} must stay within process.cwd().`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
var ensureSafeFileNameFragment = (value, optionName) => {
|
|
364
|
+
if (hasPathSeparator(value) || containsPathTraversalToken(value)) {
|
|
365
|
+
throw new ConfigurationError(
|
|
366
|
+
`${optionName} must not contain path separators or traversal tokens.`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
var resolveFileDataPath = (config) => {
|
|
371
|
+
if (config.filePath !== void 0 && config.target !== void 0) {
|
|
372
|
+
throw new ConfigurationError(
|
|
373
|
+
"filePath and target cannot be specified together."
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (config.filePath !== void 0) {
|
|
377
|
+
const resolvedFilePath2 = (0, import_node_path.resolve)(config.filePath);
|
|
378
|
+
ensureCanonicalPathWithinWorkingDirectory(resolvedFilePath2, "filePath");
|
|
379
|
+
return resolvedFilePath2;
|
|
380
|
+
}
|
|
381
|
+
if (config.target === void 0) {
|
|
382
|
+
return (0, import_node_path.resolve)("./frostpillar.fpdb");
|
|
383
|
+
}
|
|
384
|
+
if (config.target.kind === "path") {
|
|
385
|
+
const resolvedFilePath2 = (0, import_node_path.resolve)(config.target.filePath);
|
|
386
|
+
ensureCanonicalPathWithinWorkingDirectory(resolvedFilePath2, "target.filePath");
|
|
387
|
+
return resolvedFilePath2;
|
|
388
|
+
}
|
|
389
|
+
const directoryPath = (0, import_node_path.resolve)(config.target.directory);
|
|
390
|
+
ensureCanonicalPathWithinWorkingDirectory(directoryPath, "target.directory");
|
|
391
|
+
const filePrefix = config.target.filePrefix ?? "";
|
|
392
|
+
const fileName = config.target.fileName ?? "frostpillar";
|
|
393
|
+
ensureSafeFileNameFragment(filePrefix, "target.filePrefix");
|
|
394
|
+
ensureSafeFileNameFragment(fileName, "target.fileName");
|
|
395
|
+
const resolvedFilePath = (0, import_node_path.resolve)(
|
|
396
|
+
(0, import_node_path.join)(directoryPath, `${filePrefix}${fileName}.fpdb`)
|
|
397
|
+
);
|
|
398
|
+
if (!isPathWithinBaseDirectory(resolvedFilePath, directoryPath)) {
|
|
399
|
+
throw new ConfigurationError(
|
|
400
|
+
"Resolved file path must stay within target.directory."
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return resolvedFilePath;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// src/storage/drivers/file/fileBackend.ts
|
|
407
|
+
var toNodeErrorCode = (error) => {
|
|
408
|
+
if (error instanceof Error) {
|
|
409
|
+
const nodeError = error;
|
|
410
|
+
return nodeError.code;
|
|
411
|
+
}
|
|
412
|
+
return void 0;
|
|
413
|
+
};
|
|
414
|
+
var isProcessAlive = (pid) => {
|
|
415
|
+
try {
|
|
416
|
+
process.kill(pid, 0);
|
|
417
|
+
return true;
|
|
418
|
+
} catch (error) {
|
|
419
|
+
const code = error.code;
|
|
420
|
+
if (code === "ESRCH") {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
var tryRecoverStaleLock = (lockPath) => {
|
|
427
|
+
try {
|
|
428
|
+
const content = (0, import_node_fs2.readFileSync)(lockPath, "utf8");
|
|
429
|
+
const parsed = JSON.parse(content);
|
|
430
|
+
if (typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid)) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
if (isProcessAlive(parsed.pid)) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
(0, import_node_fs2.unlinkSync)(lockPath);
|
|
437
|
+
return true;
|
|
438
|
+
} catch {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
var writeLockFile = (lockPath) => {
|
|
443
|
+
const descriptor = (0, import_node_fs2.openSync)(lockPath, "wx");
|
|
444
|
+
try {
|
|
445
|
+
const pidContent = JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
446
|
+
(0, import_node_fs2.writeSync)(descriptor, pidContent, null, "utf8");
|
|
447
|
+
} finally {
|
|
448
|
+
(0, import_node_fs2.closeSync)(descriptor);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
var verifyLockOwnership = (lockPath) => {
|
|
452
|
+
try {
|
|
453
|
+
const content = (0, import_node_fs2.readFileSync)(lockPath, "utf8");
|
|
454
|
+
const parsed = JSON.parse(content);
|
|
455
|
+
if (parsed.pid !== process.pid) {
|
|
456
|
+
throw new DatabaseLockedError(
|
|
457
|
+
"Lock was overtaken by another process during stale lock recovery."
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (error instanceof DatabaseLockedError) {
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
throw new DatabaseLockedError(
|
|
465
|
+
"Lock file became unreadable during stale lock recovery."
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var acquireFileLock = (lockPath) => {
|
|
470
|
+
try {
|
|
471
|
+
writeLockFile(lockPath);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
const code = toNodeErrorCode(error);
|
|
474
|
+
if (code === "EEXIST") {
|
|
475
|
+
if (tryRecoverStaleLock(lockPath)) {
|
|
476
|
+
try {
|
|
477
|
+
writeLockFile(lockPath);
|
|
478
|
+
verifyLockOwnership(lockPath);
|
|
479
|
+
return;
|
|
480
|
+
} catch (retryError) {
|
|
481
|
+
if (retryError instanceof DatabaseLockedError) {
|
|
482
|
+
throw retryError;
|
|
483
|
+
}
|
|
484
|
+
throw new DatabaseLockedError(
|
|
485
|
+
"Datastore is locked by another process."
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
throw new DatabaseLockedError(
|
|
490
|
+
"Datastore is locked by another process."
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
throw toStorageEngineError(
|
|
494
|
+
error,
|
|
495
|
+
"Failed to acquire file lock."
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
var cleanupFileTempArtifacts = (backend) => {
|
|
500
|
+
try {
|
|
501
|
+
const sidecarTempPath = `${backend.sidecarPath}.tmp`;
|
|
502
|
+
if ((0, import_node_fs2.existsSync)(sidecarTempPath)) {
|
|
503
|
+
(0, import_node_fs2.unlinkSync)(sidecarTempPath);
|
|
504
|
+
}
|
|
505
|
+
const entries = (0, import_node_fs2.readdirSync)(backend.directoryPath);
|
|
506
|
+
const generationPrefix = `${backend.baseFileName}.g.`;
|
|
507
|
+
for (const entry of entries) {
|
|
508
|
+
if (entry.startsWith(generationPrefix) && entry.endsWith(".tmp")) {
|
|
509
|
+
(0, import_node_fs2.unlinkSync)((0, import_node_path2.join)(backend.directoryPath, entry));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} catch (error) {
|
|
513
|
+
throw toStorageEngineError(
|
|
514
|
+
error,
|
|
515
|
+
"Failed to cleanup temporary durability artifacts"
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
var createFileBackend = (config) => {
|
|
520
|
+
const dataFilePath = resolveFileDataPath(config);
|
|
521
|
+
const directoryPath = (0, import_node_path2.dirname)(dataFilePath);
|
|
522
|
+
const baseFileName = (0, import_node_path2.basename)(dataFilePath);
|
|
523
|
+
const sidecarPath = `${dataFilePath}.meta.json`;
|
|
524
|
+
const lockPath = `${dataFilePath}.lock`;
|
|
525
|
+
ensureCanonicalPathWithinWorkingDirectory(
|
|
526
|
+
dataFilePath,
|
|
527
|
+
"resolvedDataFilePath"
|
|
528
|
+
);
|
|
529
|
+
(0, import_node_fs2.mkdirSync)(directoryPath, { recursive: true });
|
|
530
|
+
acquireFileLock(lockPath);
|
|
531
|
+
const backend = {
|
|
532
|
+
dataFilePath,
|
|
533
|
+
directoryPath,
|
|
534
|
+
baseFileName,
|
|
535
|
+
sidecarPath,
|
|
536
|
+
lockPath,
|
|
537
|
+
activeDataFile: `${baseFileName}.g.0`,
|
|
538
|
+
commitId: 0,
|
|
539
|
+
lockAcquired: true
|
|
540
|
+
};
|
|
541
|
+
try {
|
|
542
|
+
cleanupFileTempArtifacts(backend);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
releaseFileLock(backend);
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
return backend;
|
|
548
|
+
};
|
|
549
|
+
var cleanupStaleGenerationFiles = (backend) => {
|
|
550
|
+
try {
|
|
551
|
+
const entries = (0, import_node_fs2.readdirSync)(backend.directoryPath);
|
|
552
|
+
const generationPrefix = `${backend.baseFileName}.g.`;
|
|
553
|
+
for (const entry of entries) {
|
|
554
|
+
if (entry.startsWith(generationPrefix) && !entry.endsWith(".tmp") && entry !== backend.activeDataFile) {
|
|
555
|
+
(0, import_node_fs2.unlinkSync)((0, import_node_path2.join)(backend.directoryPath, entry));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
var releaseFileLock = (backend) => {
|
|
562
|
+
try {
|
|
563
|
+
if ((0, import_node_fs2.existsSync)(backend.lockPath)) {
|
|
564
|
+
(0, import_node_fs2.unlinkSync)(backend.lockPath);
|
|
565
|
+
}
|
|
566
|
+
backend.lockAcquired = false;
|
|
567
|
+
} catch (error) {
|
|
568
|
+
throw toStorageEngineError(
|
|
569
|
+
error,
|
|
570
|
+
"Failed to release file lock during close()"
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// src/storage/drivers/file/fileBackendSnapshot.ts
|
|
576
|
+
var import_node_fs3 = require("node:fs");
|
|
577
|
+
var import_node_path3 = require("node:path");
|
|
578
|
+
|
|
579
|
+
// src/storage/backend/encoding.ts
|
|
580
|
+
var computeUtf8ByteLengthJs = (value) => {
|
|
581
|
+
let bytes = 0;
|
|
582
|
+
for (let i = 0; i < value.length; i++) {
|
|
583
|
+
const code = value.charCodeAt(i);
|
|
584
|
+
if (code <= 127) {
|
|
585
|
+
bytes += 1;
|
|
586
|
+
} else if (code <= 2047) {
|
|
587
|
+
bytes += 2;
|
|
588
|
+
} else if (code >= 55296 && code <= 56319) {
|
|
589
|
+
const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0;
|
|
590
|
+
if (next >= 56320 && next <= 57343) {
|
|
591
|
+
bytes += 4;
|
|
592
|
+
i++;
|
|
593
|
+
} else {
|
|
594
|
+
bytes += 3;
|
|
595
|
+
}
|
|
596
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
597
|
+
bytes += 3;
|
|
598
|
+
} else {
|
|
599
|
+
bytes += 3;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return bytes;
|
|
603
|
+
};
|
|
604
|
+
var hasBuffer = typeof Buffer !== "undefined" && typeof Buffer.byteLength === "function";
|
|
605
|
+
var computeUtf8ByteLength = hasBuffer ? (value) => Buffer.byteLength(value, "utf8") : computeUtf8ByteLengthJs;
|
|
606
|
+
|
|
607
|
+
// src/storage/btree/recordKeyIndexBTree.ts
|
|
608
|
+
var import_frostpillar_btree = require("@frostpillar/frostpillar-btree");
|
|
609
|
+
var clampComparatorResult = (compared) => {
|
|
610
|
+
if (compared === 0) return 0;
|
|
611
|
+
return compared < 0 ? -1 : 1;
|
|
612
|
+
};
|
|
613
|
+
var buildWrappedComparator = (compareKeys) => {
|
|
614
|
+
return (left, right) => {
|
|
615
|
+
const result = compareKeys(left, right);
|
|
616
|
+
if (result !== result) {
|
|
617
|
+
throw new IndexCorruptionError("key comparator must not return NaN.");
|
|
618
|
+
}
|
|
619
|
+
return clampComparatorResult(result);
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
var RecordKeyIndexBTree = class _RecordKeyIndexBTree {
|
|
623
|
+
constructor(config) {
|
|
624
|
+
const wrappedComparator = buildWrappedComparator(config.compareKeys);
|
|
625
|
+
const treeConfig = {
|
|
626
|
+
compareKeys: wrappedComparator,
|
|
627
|
+
duplicateKeys: config.duplicateKeys ?? "allow",
|
|
628
|
+
enableEntryIdLookup: true
|
|
629
|
+
};
|
|
630
|
+
this.tree = new import_frostpillar_btree.InMemoryBTree(treeConfig);
|
|
631
|
+
}
|
|
632
|
+
put(key, value) {
|
|
633
|
+
return this.tree.put(key, value);
|
|
634
|
+
}
|
|
635
|
+
putMany(entries) {
|
|
636
|
+
return this.tree.putMany(entries);
|
|
637
|
+
}
|
|
638
|
+
peekById(entryId) {
|
|
639
|
+
return this.tree.peekById(entryId);
|
|
640
|
+
}
|
|
641
|
+
updateById(entryId, value) {
|
|
642
|
+
return this.tree.updateById(entryId, value);
|
|
643
|
+
}
|
|
644
|
+
removeById(entryId) {
|
|
645
|
+
return this.tree.removeById(entryId);
|
|
646
|
+
}
|
|
647
|
+
rangeQuery(start, end) {
|
|
648
|
+
return this.tree.range(start, end);
|
|
649
|
+
}
|
|
650
|
+
deleteRange(start, end) {
|
|
651
|
+
return this.tree.deleteRange(start, end, {
|
|
652
|
+
lowerBound: "inclusive",
|
|
653
|
+
upperBound: "inclusive"
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
snapshot() {
|
|
657
|
+
return this.tree.snapshot();
|
|
658
|
+
}
|
|
659
|
+
peekLast() {
|
|
660
|
+
return this.tree.peekLast();
|
|
661
|
+
}
|
|
662
|
+
popFirst() {
|
|
663
|
+
return this.tree.popFirst();
|
|
664
|
+
}
|
|
665
|
+
size() {
|
|
666
|
+
return this.tree.size();
|
|
667
|
+
}
|
|
668
|
+
findFirst(key) {
|
|
669
|
+
return this.tree.findFirst(key);
|
|
670
|
+
}
|
|
671
|
+
findLast(key) {
|
|
672
|
+
return this.tree.findLast(key);
|
|
673
|
+
}
|
|
674
|
+
hasKey(key) {
|
|
675
|
+
return this.tree.hasKey(key);
|
|
676
|
+
}
|
|
677
|
+
keys() {
|
|
678
|
+
return this.tree.keys();
|
|
679
|
+
}
|
|
680
|
+
toJSON() {
|
|
681
|
+
return this.tree.toJSON();
|
|
682
|
+
}
|
|
683
|
+
static fromJSON(json, config) {
|
|
684
|
+
const wrappedComparator = buildWrappedComparator(config.compareKeys);
|
|
685
|
+
const adapter = Object.create(_RecordKeyIndexBTree.prototype);
|
|
686
|
+
const resolvedPolicy = config.duplicateKeys ?? "allow";
|
|
687
|
+
const patchedJSON = resolvedPolicy !== json.config.duplicateKeys ? { ...json, config: { ...json.config, duplicateKeys: resolvedPolicy } } : json;
|
|
688
|
+
adapter.tree = import_frostpillar_btree.InMemoryBTree.fromJSON(patchedJSON, wrappedComparator);
|
|
689
|
+
return adapter;
|
|
690
|
+
}
|
|
691
|
+
clear() {
|
|
692
|
+
this.tree.clear();
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
// src/storage/drivers/file/fileBackendSnapshot.ts
|
|
697
|
+
var writeFsync = (filePath, content) => {
|
|
698
|
+
const fd = (0, import_node_fs3.openSync)(filePath, "w");
|
|
699
|
+
try {
|
|
700
|
+
(0, import_node_fs3.writeSync)(fd, content, null, "utf8");
|
|
701
|
+
(0, import_node_fs3.fsyncSync)(fd);
|
|
702
|
+
} finally {
|
|
703
|
+
(0, import_node_fs3.closeSync)(fd);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
var fsyncDirectory = (dirPath) => {
|
|
707
|
+
const fd = (0, import_node_fs3.openSync)(dirPath, "r");
|
|
708
|
+
try {
|
|
709
|
+
(0, import_node_fs3.fsyncSync)(fd);
|
|
710
|
+
} finally {
|
|
711
|
+
(0, import_node_fs3.closeSync)(fd);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
var SIDE_CAR_MAGIC = "FPGE_META";
|
|
715
|
+
var GENERATION_MAGIC = "FPGE_DATA";
|
|
716
|
+
var FORMAT_VERSION = 2;
|
|
717
|
+
var noOpComparator = () => 0;
|
|
718
|
+
var createEmptyTreeJSON = () => {
|
|
719
|
+
const tree = new RecordKeyIndexBTree({
|
|
720
|
+
compareKeys: noOpComparator
|
|
721
|
+
});
|
|
722
|
+
return tree.toJSON();
|
|
723
|
+
};
|
|
724
|
+
var ensureNonNegativeSafeInteger = (value, field) => {
|
|
725
|
+
if (!Number.isSafeInteger(value) || typeof value !== "number" || value < 0) {
|
|
726
|
+
throw new PageCorruptionError(`${field} must be a non-negative safe integer.`);
|
|
727
|
+
}
|
|
728
|
+
return value;
|
|
729
|
+
};
|
|
730
|
+
var validateActiveDataFileName = (value, baseFileName) => {
|
|
731
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
732
|
+
throw new PageCorruptionError(
|
|
733
|
+
"sidecar.activeDataFile must be a non-empty string."
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
if (value.includes("/") || value.includes("\\")) {
|
|
737
|
+
throw new PageCorruptionError(
|
|
738
|
+
"sidecar.activeDataFile must be a file name without path separators."
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
const expectedPrefix = `${baseFileName}.g.`;
|
|
742
|
+
if (!value.startsWith(expectedPrefix)) {
|
|
743
|
+
throw new PageCorruptionError(
|
|
744
|
+
"sidecar.activeDataFile must follow committed generation file naming."
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
const commitSuffix = value.slice(expectedPrefix.length);
|
|
748
|
+
if (!/^\d+$/.test(commitSuffix)) {
|
|
749
|
+
throw new PageCorruptionError(
|
|
750
|
+
"sidecar.activeDataFile commit suffix must be an unsigned decimal integer."
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
return value;
|
|
754
|
+
};
|
|
755
|
+
var writeInitialFileSnapshot = (backend) => {
|
|
756
|
+
const generation = {
|
|
757
|
+
magic: GENERATION_MAGIC,
|
|
758
|
+
version: FORMAT_VERSION,
|
|
759
|
+
treeJSON: createEmptyTreeJSON()
|
|
760
|
+
};
|
|
761
|
+
const activeDataPath = (0, import_node_path3.join)(backend.directoryPath, backend.activeDataFile);
|
|
762
|
+
const sidecar = {
|
|
763
|
+
magic: SIDE_CAR_MAGIC,
|
|
764
|
+
version: FORMAT_VERSION,
|
|
765
|
+
activeDataFile: backend.activeDataFile,
|
|
766
|
+
commitId: backend.commitId
|
|
767
|
+
};
|
|
768
|
+
try {
|
|
769
|
+
writeFsync(activeDataPath, JSON.stringify(generation));
|
|
770
|
+
writeFsync(backend.sidecarPath, JSON.stringify(sidecar, null, 2));
|
|
771
|
+
} catch (error) {
|
|
772
|
+
throw toStorageEngineError(error, "Failed to initialize file backend snapshot");
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
var applySidecarToBackend = (backend, parsedSidecar) => {
|
|
776
|
+
if (parsedSidecar.magic !== SIDE_CAR_MAGIC || parsedSidecar.version !== FORMAT_VERSION) {
|
|
777
|
+
throw new PageCorruptionError("Invalid sidecar magic/version.");
|
|
778
|
+
}
|
|
779
|
+
backend.activeDataFile = validateActiveDataFileName(
|
|
780
|
+
parsedSidecar.activeDataFile,
|
|
781
|
+
backend.baseFileName
|
|
782
|
+
);
|
|
783
|
+
backend.commitId = ensureNonNegativeSafeInteger(
|
|
784
|
+
parsedSidecar.commitId,
|
|
785
|
+
"sidecar.commitId"
|
|
786
|
+
);
|
|
787
|
+
};
|
|
788
|
+
var loadAndValidateGenerationFile = (backend) => {
|
|
789
|
+
const activeDataPath = (0, import_node_path3.join)(backend.directoryPath, backend.activeDataFile);
|
|
790
|
+
if (!(0, import_node_fs3.existsSync)(activeDataPath)) {
|
|
791
|
+
throw new PageCorruptionError(
|
|
792
|
+
"Active generation file referenced by sidecar is missing."
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
const generationSource = (0, import_node_fs3.readFileSync)(activeDataPath, "utf8");
|
|
796
|
+
const parsedGeneration = JSON.parse(generationSource);
|
|
797
|
+
if (parsedGeneration.magic !== GENERATION_MAGIC || parsedGeneration.version !== FORMAT_VERSION) {
|
|
798
|
+
throw new PageCorruptionError("Invalid generation magic/version.");
|
|
799
|
+
}
|
|
800
|
+
const treeJsonSizeBytes = computeUtf8ByteLength(JSON.stringify(parsedGeneration.treeJSON));
|
|
801
|
+
return { generation: parsedGeneration, treeJsonSizeBytes };
|
|
802
|
+
};
|
|
803
|
+
var loadFileSnapshot = (backend) => {
|
|
804
|
+
try {
|
|
805
|
+
const sidecarSource = (0, import_node_fs3.readFileSync)(backend.sidecarPath, "utf8");
|
|
806
|
+
const parsedSidecar = JSON.parse(sidecarSource);
|
|
807
|
+
applySidecarToBackend(backend, parsedSidecar);
|
|
808
|
+
const validatedGeneration = loadAndValidateGenerationFile(backend);
|
|
809
|
+
const treeJSON = validatedGeneration.generation.treeJSON;
|
|
810
|
+
if (typeof treeJSON !== "object" || treeJSON === null || Array.isArray(treeJSON)) {
|
|
811
|
+
throw new PageCorruptionError("treeJSON must be a non-null plain object.");
|
|
812
|
+
}
|
|
813
|
+
const currentSizeBytes = validatedGeneration.treeJsonSizeBytes;
|
|
814
|
+
return { treeJSON, currentSizeBytes };
|
|
815
|
+
} catch (error) {
|
|
816
|
+
throw toStorageEngineError(error, "Failed to load file backend snapshot");
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
var commitFileBackendSnapshot = (backend, treeJSON) => {
|
|
820
|
+
if (backend.commitId >= Number.MAX_SAFE_INTEGER) {
|
|
821
|
+
throw new StorageEngineError("File backend commitId has reached Number.MAX_SAFE_INTEGER.");
|
|
822
|
+
}
|
|
823
|
+
const nextCommitId = backend.commitId + 1;
|
|
824
|
+
const nextActiveDataFile = `${backend.baseFileName}.g.${nextCommitId}`;
|
|
825
|
+
const generationTempPath = (0, import_node_path3.join)(
|
|
826
|
+
backend.directoryPath,
|
|
827
|
+
`${nextActiveDataFile}.tmp`
|
|
828
|
+
);
|
|
829
|
+
const generationPath = (0, import_node_path3.join)(backend.directoryPath, nextActiveDataFile);
|
|
830
|
+
const sidecarTempPath = `${backend.sidecarPath}.tmp`;
|
|
831
|
+
const generation = {
|
|
832
|
+
magic: GENERATION_MAGIC,
|
|
833
|
+
version: FORMAT_VERSION,
|
|
834
|
+
treeJSON
|
|
835
|
+
};
|
|
836
|
+
const sidecar = {
|
|
837
|
+
magic: SIDE_CAR_MAGIC,
|
|
838
|
+
version: FORMAT_VERSION,
|
|
839
|
+
activeDataFile: nextActiveDataFile,
|
|
840
|
+
commitId: nextCommitId
|
|
841
|
+
};
|
|
842
|
+
const previousActiveDataFile = backend.activeDataFile;
|
|
843
|
+
try {
|
|
844
|
+
writeFsync(generationTempPath, JSON.stringify(generation));
|
|
845
|
+
(0, import_node_fs3.renameSync)(generationTempPath, generationPath);
|
|
846
|
+
writeFsync(sidecarTempPath, JSON.stringify(sidecar, null, 2));
|
|
847
|
+
(0, import_node_fs3.renameSync)(sidecarTempPath, backend.sidecarPath);
|
|
848
|
+
fsyncDirectory(backend.directoryPath);
|
|
849
|
+
backend.activeDataFile = nextActiveDataFile;
|
|
850
|
+
backend.commitId = nextCommitId;
|
|
851
|
+
} catch (error) {
|
|
852
|
+
throw toStorageEngineError(error, "File commit failed");
|
|
853
|
+
}
|
|
854
|
+
if (previousActiveDataFile !== nextActiveDataFile) {
|
|
855
|
+
const previousPath = (0, import_node_path3.join)(backend.directoryPath, previousActiveDataFile);
|
|
856
|
+
try {
|
|
857
|
+
if ((0, import_node_fs3.existsSync)(previousPath)) {
|
|
858
|
+
(0, import_node_fs3.unlinkSync)(previousPath);
|
|
859
|
+
}
|
|
860
|
+
} catch {
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// src/storage/drivers/file/fileBackendController.ts
|
|
866
|
+
var FileBackendController = class _FileBackendController extends AsyncDurableAutoCommitController {
|
|
867
|
+
constructor(backend, autoCommit, getSnapshot, onAutoCommitError, testHooks) {
|
|
868
|
+
super(autoCommit, onAutoCommitError);
|
|
869
|
+
this.backend = backend;
|
|
870
|
+
this.getSnapshot = getSnapshot;
|
|
871
|
+
this.testHooks = testHooks;
|
|
872
|
+
}
|
|
873
|
+
static create(options) {
|
|
874
|
+
validateNoLegacyTestHooks(options.config);
|
|
875
|
+
const autoCommit = parseAutoCommitConfig(options.autoCommit);
|
|
876
|
+
const backend = createFileBackend(options.config);
|
|
877
|
+
let initialTreeJSON = null;
|
|
878
|
+
let initialCurrentSizeBytes = 0;
|
|
879
|
+
try {
|
|
880
|
+
if (!(0, import_node_fs4.existsSync)(backend.sidecarPath)) {
|
|
881
|
+
writeInitialFileSnapshot(backend);
|
|
882
|
+
} else {
|
|
883
|
+
const loaded = loadFileSnapshot(backend);
|
|
884
|
+
initialTreeJSON = loaded.treeJSON;
|
|
885
|
+
initialCurrentSizeBytes = loaded.currentSizeBytes;
|
|
886
|
+
cleanupStaleGenerationFiles(backend);
|
|
887
|
+
}
|
|
888
|
+
} catch (error) {
|
|
889
|
+
if (backend.lockAcquired) {
|
|
890
|
+
releaseFileLock(backend);
|
|
891
|
+
}
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
894
|
+
const controller = new _FileBackendController(
|
|
895
|
+
backend,
|
|
896
|
+
autoCommit,
|
|
897
|
+
options.getSnapshot,
|
|
898
|
+
options.onAutoCommitError,
|
|
899
|
+
normalizeTestHooks(options.testHooks)
|
|
900
|
+
);
|
|
901
|
+
return {
|
|
902
|
+
controller,
|
|
903
|
+
initialTreeJSON,
|
|
904
|
+
initialCurrentSizeBytes
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
async executeSingleCommit() {
|
|
908
|
+
await this.testHooks?.beforeCommit?.();
|
|
909
|
+
const snapshot = this.getSnapshot();
|
|
910
|
+
commitFileBackendSnapshot(
|
|
911
|
+
this.backend,
|
|
912
|
+
snapshot.treeJSON
|
|
913
|
+
);
|
|
914
|
+
await this.testHooks?.afterCommit?.();
|
|
915
|
+
}
|
|
916
|
+
onCloseAfterDrain() {
|
|
917
|
+
if (this.backend.lockAcquired) {
|
|
918
|
+
releaseFileLock(this.backend);
|
|
919
|
+
}
|
|
920
|
+
return Promise.resolve();
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
var validateNoLegacyTestHooks = (config) => {
|
|
924
|
+
const withLegacyHooks = config;
|
|
925
|
+
if (!("__testHooks" in withLegacyHooks)) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
throw new ConfigurationError(
|
|
929
|
+
"config.__testHooks is not supported. Pass testHooks via FileBackendController.create options."
|
|
930
|
+
);
|
|
931
|
+
};
|
|
932
|
+
var normalizeTestHooks = (testHooks) => {
|
|
933
|
+
if (testHooks === void 0) {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
return testHooks;
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// src/drivers/file.ts
|
|
940
|
+
var fileDriver = (options = {}) => {
|
|
941
|
+
return {
|
|
942
|
+
init: (callbacks) => {
|
|
943
|
+
const result = FileBackendController.create({
|
|
944
|
+
config: options,
|
|
945
|
+
autoCommit: callbacks.autoCommit,
|
|
946
|
+
getSnapshot: callbacks.getSnapshot,
|
|
947
|
+
onAutoCommitError: callbacks.onAutoCommitError
|
|
948
|
+
});
|
|
949
|
+
return {
|
|
950
|
+
controller: result.controller,
|
|
951
|
+
initialTreeJSON: result.initialTreeJSON,
|
|
952
|
+
initialCurrentSizeBytes: result.initialCurrentSizeBytes
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
};
|
|
957
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
958
|
+
0 && (module.exports = {
|
|
959
|
+
fileDriver
|
|
960
|
+
});
|