@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
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Datastore } from './storage/datastore/Datastore.js';
|
|
2
|
+
export type { AutoCommitConfig, AutoCommitFrequencyInput, CapacityConfig, CapacityPolicy, DatastoreConfig, DuplicateKeyPolicy, DatastoreDriver, DatastoreDriverController, DatastoreDriverInitContext, DatastoreDriverInitResult, DatastoreDriverSnapshot, DatastoreErrorEvent, DatastoreErrorListener, DatastoreKeyDefinition, EntryId, FileBackendConfig, FileTargetByDirectoryConfig, FileTargetByPathConfig, FileTargetConfig, InputRecord, IndexedDBConfig, KeyedRecord, LocalStorageConfig, OpfsConfig, PersistedRecord, RecordPayload, SyncStorageConfig, } from './types.js';
|
|
3
|
+
export { BinaryFormatError, ClosedDatastoreError, ConfigurationError, DatabaseLockedError, FrostpillarError, IndexCorruptionError, InvalidQueryRangeError, PageCorruptionError, QuotaExceededError, StorageEngineError, UnsupportedBackendError, ValidationError, } from './errors/index.js';
|
|
4
|
+
export { localStorageDriver } from './drivers/localStorage.js';
|
|
5
|
+
export { indexedDBDriver } from './drivers/indexedDB.js';
|
|
6
|
+
export { opfsDriver } from './drivers/opfs.js';
|
|
7
|
+
export { syncStorageDriver } from './drivers/syncStorage.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Datastore } from './storage/datastore/Datastore.js';
|
|
2
|
+
export { BinaryFormatError, ClosedDatastoreError, ConfigurationError, DatabaseLockedError, FrostpillarError, IndexCorruptionError, InvalidQueryRangeError, PageCorruptionError, QuotaExceededError, StorageEngineError, UnsupportedBackendError, ValidationError, } from './errors/index.js';
|
|
3
|
+
export { localStorageDriver } from './drivers/localStorage.js';
|
|
4
|
+
export { indexedDBDriver } from './drivers/indexedDB.js';
|
|
5
|
+
export { opfsDriver } from './drivers/opfs.js';
|
|
6
|
+
export { syncStorageDriver } from './drivers/syncStorage.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FileAutoCommitState } from './types.js';
|
|
2
|
+
export declare abstract class AsyncDurableAutoCommitController {
|
|
3
|
+
private readonly autoCommit;
|
|
4
|
+
private readonly onAutoCommitError;
|
|
5
|
+
private pendingAutoCommitBytes;
|
|
6
|
+
private dirtyFromClear;
|
|
7
|
+
private autoCommitTimer;
|
|
8
|
+
private commitInFlight;
|
|
9
|
+
private pendingForegroundCommitRequest;
|
|
10
|
+
private pendingBackgroundCommitRequest;
|
|
11
|
+
private closed;
|
|
12
|
+
protected constructor(autoCommit: FileAutoCommitState, onAutoCommitError: (error: unknown) => void);
|
|
13
|
+
handleRecordAppended(encodedBytes: number): Promise<void>;
|
|
14
|
+
handleCleared(): Promise<void>;
|
|
15
|
+
commitNow(): Promise<void>;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
protected getPendingAutoCommitBytes(): number;
|
|
18
|
+
protected abstract executeSingleCommit(): Promise<void>;
|
|
19
|
+
protected onCloseAfterDrain(): Promise<void>;
|
|
20
|
+
private waitForCommitSettlement;
|
|
21
|
+
private queueCommitRequest;
|
|
22
|
+
private runCommitLoop;
|
|
23
|
+
private startAutoCommitSchedule;
|
|
24
|
+
private stopAutoCommitSchedule;
|
|
25
|
+
private handleAutoCommitTick;
|
|
26
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { toErrorInstance } from '../../errors/index.js';
|
|
2
|
+
export class AsyncDurableAutoCommitController {
|
|
3
|
+
autoCommit;
|
|
4
|
+
onAutoCommitError;
|
|
5
|
+
pendingAutoCommitBytes;
|
|
6
|
+
dirtyFromClear;
|
|
7
|
+
autoCommitTimer;
|
|
8
|
+
commitInFlight;
|
|
9
|
+
pendingForegroundCommitRequest;
|
|
10
|
+
pendingBackgroundCommitRequest;
|
|
11
|
+
closed;
|
|
12
|
+
constructor(autoCommit, onAutoCommitError) {
|
|
13
|
+
this.autoCommit = autoCommit;
|
|
14
|
+
this.onAutoCommitError = onAutoCommitError;
|
|
15
|
+
this.pendingAutoCommitBytes = 0;
|
|
16
|
+
this.dirtyFromClear = false;
|
|
17
|
+
this.autoCommitTimer = null;
|
|
18
|
+
this.commitInFlight = null;
|
|
19
|
+
this.pendingForegroundCommitRequest = false;
|
|
20
|
+
this.pendingBackgroundCommitRequest = false;
|
|
21
|
+
this.closed = false;
|
|
22
|
+
this.startAutoCommitSchedule();
|
|
23
|
+
}
|
|
24
|
+
handleRecordAppended(encodedBytes) {
|
|
25
|
+
if (this.autoCommit.frequency === 'immediate') {
|
|
26
|
+
return this.commitNow();
|
|
27
|
+
}
|
|
28
|
+
this.pendingAutoCommitBytes += encodedBytes;
|
|
29
|
+
if (this.autoCommit.maxPendingBytes !== null &&
|
|
30
|
+
this.pendingAutoCommitBytes >= this.autoCommit.maxPendingBytes) {
|
|
31
|
+
return this.queueCommitRequest('foreground');
|
|
32
|
+
}
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
}
|
|
35
|
+
handleCleared() {
|
|
36
|
+
this.dirtyFromClear = true;
|
|
37
|
+
if (this.autoCommit.frequency === 'immediate') {
|
|
38
|
+
return this.commitNow();
|
|
39
|
+
}
|
|
40
|
+
return this.queueCommitRequest('background');
|
|
41
|
+
}
|
|
42
|
+
commitNow() {
|
|
43
|
+
return this.queueCommitRequest('foreground');
|
|
44
|
+
}
|
|
45
|
+
async close() {
|
|
46
|
+
if (this.closed) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.closed = true;
|
|
50
|
+
this.stopAutoCommitSchedule();
|
|
51
|
+
await this.waitForCommitSettlement();
|
|
52
|
+
let flushError = null;
|
|
53
|
+
if (this.pendingAutoCommitBytes > 0 || this.dirtyFromClear) {
|
|
54
|
+
try {
|
|
55
|
+
await this.executeSingleCommit();
|
|
56
|
+
this.pendingAutoCommitBytes = 0;
|
|
57
|
+
this.dirtyFromClear = false;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
flushError = toErrorInstance(error, 'Final close-time flush commit failed with a non-Error value.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
let drainError = null;
|
|
64
|
+
try {
|
|
65
|
+
await this.onCloseAfterDrain();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
drainError = toErrorInstance(error, 'onCloseAfterDrain failed with a non-Error value.');
|
|
69
|
+
}
|
|
70
|
+
if (flushError !== null && drainError !== null) {
|
|
71
|
+
throw createCloseAggregateError(flushError, drainError);
|
|
72
|
+
}
|
|
73
|
+
if (flushError !== null) {
|
|
74
|
+
throw flushError;
|
|
75
|
+
}
|
|
76
|
+
if (drainError !== null) {
|
|
77
|
+
throw drainError;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
getPendingAutoCommitBytes() {
|
|
81
|
+
return this.pendingAutoCommitBytes;
|
|
82
|
+
}
|
|
83
|
+
onCloseAfterDrain() {
|
|
84
|
+
return Promise.resolve();
|
|
85
|
+
}
|
|
86
|
+
waitForCommitSettlement() {
|
|
87
|
+
if (this.commitInFlight === null) {
|
|
88
|
+
return Promise.resolve();
|
|
89
|
+
}
|
|
90
|
+
return this.commitInFlight
|
|
91
|
+
.then(() => undefined)
|
|
92
|
+
.catch(() => undefined);
|
|
93
|
+
}
|
|
94
|
+
queueCommitRequest(requestType) {
|
|
95
|
+
if (requestType === 'foreground') {
|
|
96
|
+
this.pendingForegroundCommitRequest = true;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.pendingBackgroundCommitRequest = true;
|
|
100
|
+
}
|
|
101
|
+
if (this.commitInFlight === null) {
|
|
102
|
+
this.commitInFlight = this.runCommitLoop().finally(() => {
|
|
103
|
+
this.commitInFlight = null;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (requestType === 'background') {
|
|
107
|
+
return Promise.resolve();
|
|
108
|
+
}
|
|
109
|
+
return this.commitInFlight;
|
|
110
|
+
}
|
|
111
|
+
async runCommitLoop() {
|
|
112
|
+
let shouldContinue = true;
|
|
113
|
+
while (shouldContinue) {
|
|
114
|
+
const runForeground = this.pendingForegroundCommitRequest;
|
|
115
|
+
const runBackground = this.pendingBackgroundCommitRequest;
|
|
116
|
+
const runClear = this.dirtyFromClear;
|
|
117
|
+
this.pendingForegroundCommitRequest = false;
|
|
118
|
+
this.pendingBackgroundCommitRequest = false;
|
|
119
|
+
this.dirtyFromClear = false;
|
|
120
|
+
const shouldRunCommit = runForeground || (runBackground && (this.pendingAutoCommitBytes > 0 || runClear));
|
|
121
|
+
if (!shouldRunCommit) {
|
|
122
|
+
shouldContinue = false;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const committedPendingBytes = this.pendingAutoCommitBytes;
|
|
127
|
+
await this.executeSingleCommit();
|
|
128
|
+
this.pendingAutoCommitBytes = Math.max(0, this.pendingAutoCommitBytes - committedPendingBytes);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (runClear) {
|
|
132
|
+
this.dirtyFromClear = true;
|
|
133
|
+
}
|
|
134
|
+
if (runForeground) {
|
|
135
|
+
throw toErrorInstance(error, 'Foreground auto-commit failed with a non-Error value.');
|
|
136
|
+
}
|
|
137
|
+
this.onAutoCommitError(error);
|
|
138
|
+
}
|
|
139
|
+
if (!this.pendingForegroundCommitRequest && !this.pendingBackgroundCommitRequest) {
|
|
140
|
+
shouldContinue = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
startAutoCommitSchedule() {
|
|
145
|
+
if (this.autoCommit.frequency !== 'scheduled' ||
|
|
146
|
+
this.autoCommit.intervalMs === null) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.autoCommitTimer = setInterval(() => {
|
|
150
|
+
this.handleAutoCommitTick();
|
|
151
|
+
}, this.autoCommit.intervalMs);
|
|
152
|
+
if (typeof this.autoCommitTimer === 'object' && this.autoCommitTimer !== null && 'unref' in this.autoCommitTimer) {
|
|
153
|
+
this.autoCommitTimer.unref();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
stopAutoCommitSchedule() {
|
|
157
|
+
if (this.autoCommitTimer === null) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
clearInterval(this.autoCommitTimer);
|
|
161
|
+
this.autoCommitTimer = null;
|
|
162
|
+
}
|
|
163
|
+
handleAutoCommitTick() {
|
|
164
|
+
if (this.closed) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (this.pendingAutoCommitBytes <= 0 && !this.dirtyFromClear) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
void this.queueCommitRequest('background');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const readAggregateErrorConstructor = () => {
|
|
174
|
+
const candidate = globalThis.AggregateError;
|
|
175
|
+
if (typeof candidate !== 'function') {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return candidate;
|
|
179
|
+
};
|
|
180
|
+
const createCloseAggregateError = (flushError, drainError) => {
|
|
181
|
+
const aggregateErrorConstructor = readAggregateErrorConstructor();
|
|
182
|
+
if (aggregateErrorConstructor !== null) {
|
|
183
|
+
return new aggregateErrorConstructor([flushError, drainError], 'Close failed: both final flush and drain produced errors.');
|
|
184
|
+
}
|
|
185
|
+
const fallbackError = new Error('Close failed: both final flush and drain produced errors.');
|
|
186
|
+
fallbackError.errors = [flushError, drainError];
|
|
187
|
+
return fallbackError;
|
|
188
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class AsyncMutex {
|
|
2
|
+
queue = [];
|
|
3
|
+
head = 0;
|
|
4
|
+
locked = false;
|
|
5
|
+
acquire() {
|
|
6
|
+
if (!this.locked) {
|
|
7
|
+
this.locked = true;
|
|
8
|
+
return Promise.resolve(this.createRelease());
|
|
9
|
+
}
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
this.queue.push(() => resolve(this.createRelease()));
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
createRelease() {
|
|
15
|
+
let released = false;
|
|
16
|
+
return () => {
|
|
17
|
+
if (released)
|
|
18
|
+
return;
|
|
19
|
+
released = true;
|
|
20
|
+
if (this.head < this.queue.length) {
|
|
21
|
+
const next = this.queue[this.head];
|
|
22
|
+
this.queue[this.head] = undefined; // allow GC
|
|
23
|
+
this.head += 1;
|
|
24
|
+
// Compact when more than half the array is dead entries and above threshold
|
|
25
|
+
if (this.head > 1024 && this.head > (this.queue.length >>> 1)) {
|
|
26
|
+
this.queue = this.queue.slice(this.head);
|
|
27
|
+
this.head = 0;
|
|
28
|
+
}
|
|
29
|
+
next();
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this.queue.length = 0;
|
|
33
|
+
this.head = 0;
|
|
34
|
+
this.locked = false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { StorageEngineError } from '../../errors/index.js';
|
|
2
|
+
export const emitAutoCommitErrorToListeners = (listeners, error) => {
|
|
3
|
+
const storageError = error instanceof StorageEngineError
|
|
4
|
+
? error
|
|
5
|
+
: new StorageEngineError(error instanceof Error
|
|
6
|
+
? error.message
|
|
7
|
+
: 'Unknown auto-commit storage failure.', { cause: error });
|
|
8
|
+
const event = {
|
|
9
|
+
source: 'autoCommit',
|
|
10
|
+
error: storageError,
|
|
11
|
+
occurredAt: Date.now(),
|
|
12
|
+
};
|
|
13
|
+
for (const listener of listeners) {
|
|
14
|
+
try {
|
|
15
|
+
const delivered = listener(event);
|
|
16
|
+
void Promise.resolve(delivered).catch(() => undefined);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// listener isolation by contract
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IndexCorruptionError, QuotaExceededError } from '../../errors/index.js';
|
|
2
|
+
export const enforceCapacityPolicy = (capacityState, currentSizeBytes, encodedBytes, getRecordCount, evictOldestRecord) => {
|
|
3
|
+
if (capacityState === null) {
|
|
4
|
+
return currentSizeBytes;
|
|
5
|
+
}
|
|
6
|
+
if (encodedBytes > capacityState.maxSizeBytes) {
|
|
7
|
+
throw new QuotaExceededError('Record exceeds configured capacity.maxSize boundary.');
|
|
8
|
+
}
|
|
9
|
+
if (capacityState.policy === 'strict') {
|
|
10
|
+
if (currentSizeBytes + encodedBytes > capacityState.maxSizeBytes) {
|
|
11
|
+
throw new QuotaExceededError('Insert exceeds configured capacity.maxSize under strict policy.');
|
|
12
|
+
}
|
|
13
|
+
return currentSizeBytes;
|
|
14
|
+
}
|
|
15
|
+
let nextSizeBytes = currentSizeBytes;
|
|
16
|
+
while (nextSizeBytes + encodedBytes > capacityState.maxSizeBytes) {
|
|
17
|
+
if (getRecordCount() === 0) {
|
|
18
|
+
throw new QuotaExceededError('Record cannot fit in turnover policy with empty datastore.');
|
|
19
|
+
}
|
|
20
|
+
const evictedBytes = evictOldestRecord();
|
|
21
|
+
if (!Number.isSafeInteger(evictedBytes) || evictedBytes <= 0) {
|
|
22
|
+
throw new IndexCorruptionError('Turnover eviction reported non-progressing reclaimed bytes.');
|
|
23
|
+
}
|
|
24
|
+
nextSizeBytes -= evictedBytes;
|
|
25
|
+
}
|
|
26
|
+
return Math.max(0, nextSizeBytes);
|
|
27
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ConfigurationError } from '../../errors/index.js';
|
|
2
|
+
import { parseCapacityConfig } from '../config/config.shared.js';
|
|
3
|
+
const resolveCapacityConfigWithBackendLimit = (config) => {
|
|
4
|
+
if (config.capacity === undefined) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
if (config.capacity.maxSize !== 'backendLimit') {
|
|
8
|
+
return config.capacity;
|
|
9
|
+
}
|
|
10
|
+
if (config.driver === undefined) {
|
|
11
|
+
throw new ConfigurationError('capacity.maxSize "backendLimit" requires a durable driver.');
|
|
12
|
+
}
|
|
13
|
+
if (config.driver.resolveBackendLimitBytes === undefined) {
|
|
14
|
+
throw new ConfigurationError('capacity.maxSize "backendLimit" is not supported by the selected driver.');
|
|
15
|
+
}
|
|
16
|
+
const resolvedMaxSize = config.driver.resolveBackendLimitBytes();
|
|
17
|
+
return {
|
|
18
|
+
...config.capacity,
|
|
19
|
+
maxSize: resolvedMaxSize,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export const resolveCapacityState = (config) => {
|
|
23
|
+
const resolvedCapacityConfig = resolveCapacityConfigWithBackendLimit(config);
|
|
24
|
+
return parseCapacityConfig(resolvedCapacityConfig);
|
|
25
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RecordPayload } from '../../types.js';
|
|
2
|
+
export declare const computeUtf8ByteLength: (value: string) => number;
|
|
3
|
+
/**
|
|
4
|
+
* Compute the UTF-8 byte length of a string as it would appear in JSON output
|
|
5
|
+
* (i.e., with JSON escaping applied), INCLUDING the surrounding quotes.
|
|
6
|
+
*/
|
|
7
|
+
export declare const estimateJsonStringBytes: (value: string) => number;
|
|
8
|
+
/**
|
|
9
|
+
* Estimate the UTF-8 byte length of `JSON.stringify(value)` by walking the
|
|
10
|
+
* object tree structurally. Does NOT call JSON.stringify.
|
|
11
|
+
*
|
|
12
|
+
* Supports: null, boolean, number, string, plain objects.
|
|
13
|
+
* Arrays are NOT supported (payloads don't contain arrays).
|
|
14
|
+
*/
|
|
15
|
+
export declare const estimateObjectSizeBytes: (value: unknown) => number;
|
|
16
|
+
export declare const estimateRecordSizeBytes: (key: unknown, payload: RecordPayload) => number;
|
|
17
|
+
export declare const estimateKeySizeBytes: (key: unknown) => number;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// P8: Platform-native UTF-8 byte length with JS fallback for browsers
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
const computeUtf8ByteLengthJs = (value) => {
|
|
5
|
+
let bytes = 0;
|
|
6
|
+
for (let i = 0; i < value.length; i++) {
|
|
7
|
+
const code = value.charCodeAt(i);
|
|
8
|
+
if (code <= 0x7f) {
|
|
9
|
+
bytes += 1;
|
|
10
|
+
}
|
|
11
|
+
else if (code <= 0x7ff) {
|
|
12
|
+
bytes += 2;
|
|
13
|
+
}
|
|
14
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
15
|
+
const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0;
|
|
16
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
17
|
+
bytes += 4;
|
|
18
|
+
i++; // skip low surrogate
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
bytes += 3; // lone high surrogate → U+FFFD replacement (3 bytes)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
25
|
+
bytes += 3; // lone low surrogate → U+FFFD replacement (3 bytes)
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
bytes += 3;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return bytes;
|
|
32
|
+
};
|
|
33
|
+
const hasBuffer = typeof Buffer !== 'undefined' && typeof Buffer.byteLength === 'function';
|
|
34
|
+
export const computeUtf8ByteLength = hasBuffer
|
|
35
|
+
? (value) => Buffer.byteLength(value, 'utf8')
|
|
36
|
+
: computeUtf8ByteLengthJs;
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// P9: Structural size estimation — walk object tree without JSON.stringify
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
/**
|
|
41
|
+
* Compute the UTF-8 byte length of a string as it would appear in JSON output
|
|
42
|
+
* (i.e., with JSON escaping applied), INCLUDING the surrounding quotes.
|
|
43
|
+
*/
|
|
44
|
+
export const estimateJsonStringBytes = (value) => {
|
|
45
|
+
// 2 bytes for the surrounding quotes
|
|
46
|
+
let bytes = 2;
|
|
47
|
+
for (let i = 0; i < value.length; i++) {
|
|
48
|
+
const code = value.charCodeAt(i);
|
|
49
|
+
// JSON.stringify escapes: " → \", \\ → \\, \n → \\n, \r → \\r, \t → \\t,
|
|
50
|
+
// \b → \\b, \f → \\f, and U+0000–U+001F → \\uXXXX (6 chars)
|
|
51
|
+
if (code === 0x22 || code === 0x5c) {
|
|
52
|
+
// " or \ → 2 ASCII bytes
|
|
53
|
+
bytes += 2;
|
|
54
|
+
}
|
|
55
|
+
else if (code <= 0x1f) {
|
|
56
|
+
// Control characters: \b(8), \t(9), \n(10), \f(12), \r(13) → 2 bytes each
|
|
57
|
+
// Others → \uXXXX → 6 bytes each
|
|
58
|
+
if (code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
|
|
59
|
+
bytes += 2;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
bytes += 6;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (code <= 0x7f) {
|
|
66
|
+
bytes += 1;
|
|
67
|
+
}
|
|
68
|
+
else if (code <= 0x7ff) {
|
|
69
|
+
bytes += 2;
|
|
70
|
+
}
|
|
71
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
72
|
+
const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0;
|
|
73
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
74
|
+
bytes += 4;
|
|
75
|
+
i++; // skip low surrogate
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
bytes += 6; // lone high surrogate → JSON.stringify emits \uDXXX (6 ASCII bytes)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
82
|
+
bytes += 6; // lone low surrogate → JSON.stringify emits \uDXXX (6 ASCII bytes)
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
bytes += 3;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return bytes;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Estimate the UTF-8 byte length of `JSON.stringify(value)` by walking the
|
|
92
|
+
* object tree structurally. Does NOT call JSON.stringify.
|
|
93
|
+
*
|
|
94
|
+
* Supports: null, boolean, number, string, plain objects.
|
|
95
|
+
* Arrays are NOT supported (payloads don't contain arrays).
|
|
96
|
+
*/
|
|
97
|
+
export const estimateObjectSizeBytes = (value) => {
|
|
98
|
+
if (value === null) {
|
|
99
|
+
return 4; // "null"
|
|
100
|
+
}
|
|
101
|
+
switch (typeof value) {
|
|
102
|
+
case 'boolean':
|
|
103
|
+
return value ? 4 : 5; // "true" / "false"
|
|
104
|
+
case 'number':
|
|
105
|
+
// Use String() for correctness with all number representations
|
|
106
|
+
return String(value).length;
|
|
107
|
+
case 'string':
|
|
108
|
+
return estimateJsonStringBytes(value);
|
|
109
|
+
case 'object': {
|
|
110
|
+
const obj = value;
|
|
111
|
+
let size = 2; // { }
|
|
112
|
+
let visibleCount = 0;
|
|
113
|
+
for (const k of Object.keys(obj)) {
|
|
114
|
+
const v = obj[k];
|
|
115
|
+
// JSON.stringify omits undefined values
|
|
116
|
+
if (v === undefined) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (visibleCount > 0) {
|
|
120
|
+
size += 1; // comma
|
|
121
|
+
}
|
|
122
|
+
// key: quoted key string + colon
|
|
123
|
+
size += estimateJsonStringBytes(k) + 1;
|
|
124
|
+
// value
|
|
125
|
+
size += estimateObjectSizeBytes(v);
|
|
126
|
+
visibleCount++;
|
|
127
|
+
}
|
|
128
|
+
return size;
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// P9: Refactored estimateRecordSizeBytes using structural estimation
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// JSON_ROOT_WRAPPER_OVERHEAD = 15 matches: [key,{"payload":...}]
|
|
138
|
+
// Breakdown: [ + key + , + {"payload": + payload_json + } + ] = 1+1+10+1+1+1 = 15 overhead
|
|
139
|
+
const JSON_ROOT_WRAPPER_OVERHEAD = 15;
|
|
140
|
+
export const estimateRecordSizeBytes = (key, payload) => {
|
|
141
|
+
return estimateObjectSizeBytes(key) + estimateObjectSizeBytes(payload) + JSON_ROOT_WRAPPER_OVERHEAD;
|
|
142
|
+
};
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// P10: estimateKeySizeBytes
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
export const estimateKeySizeBytes = (key) => {
|
|
147
|
+
return estimateObjectSizeBytes(key);
|
|
148
|
+
};
|