@aztec/kv-store 0.0.0-test.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dest/config.d.ts +17 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +24 -0
- package/dest/indexeddb/array.d.ts +21 -0
- package/dest/indexeddb/array.d.ts.map +1 -0
- package/dest/indexeddb/array.js +104 -0
- package/dest/indexeddb/index.d.ts +7 -0
- package/dest/indexeddb/index.d.ts.map +1 -0
- package/dest/indexeddb/index.js +19 -0
- package/dest/indexeddb/map.d.ts +26 -0
- package/dest/indexeddb/map.d.ts.map +1 -0
- package/dest/indexeddb/map.js +133 -0
- package/dest/indexeddb/set.d.ts +17 -0
- package/dest/indexeddb/set.d.ts.map +1 -0
- package/dest/indexeddb/set.js +24 -0
- package/dest/indexeddb/singleton.d.ts +16 -0
- package/dest/indexeddb/singleton.d.ts.map +1 -0
- package/dest/indexeddb/singleton.js +37 -0
- package/dest/indexeddb/store.d.ts +97 -0
- package/dest/indexeddb/store.d.ts.map +1 -0
- package/dest/indexeddb/store.js +164 -0
- package/dest/interfaces/array.d.ts +80 -0
- package/dest/interfaces/array.d.ts.map +1 -0
- package/dest/interfaces/array.js +3 -0
- package/dest/interfaces/array_test_suite.d.ts +3 -0
- package/dest/interfaces/array_test_suite.d.ts.map +1 -0
- package/dest/interfaces/array_test_suite.js +104 -0
- package/dest/interfaces/common.d.ts +23 -0
- package/dest/interfaces/common.d.ts.map +1 -0
- package/dest/interfaces/common.js +3 -0
- package/dest/interfaces/counter.d.ts +59 -0
- package/dest/interfaces/counter.d.ts.map +1 -0
- package/dest/interfaces/counter.js +1 -0
- package/dest/interfaces/index.d.ts +8 -0
- package/dest/interfaces/index.d.ts.map +1 -0
- package/dest/interfaces/index.js +6 -0
- package/dest/interfaces/map.d.ts +134 -0
- package/dest/interfaces/map.d.ts.map +1 -0
- package/dest/interfaces/map.js +3 -0
- package/dest/interfaces/map_test_suite.d.ts +3 -0
- package/dest/interfaces/map_test_suite.d.ts.map +1 -0
- package/dest/interfaces/map_test_suite.js +151 -0
- package/dest/interfaces/set.d.ts +44 -0
- package/dest/interfaces/set.d.ts.map +1 -0
- package/dest/interfaces/set.js +1 -0
- package/dest/interfaces/set_test_suite.d.ts +3 -0
- package/dest/interfaces/set_test_suite.d.ts.map +1 -0
- package/dest/interfaces/set_test_suite.js +96 -0
- package/dest/interfaces/singleton.d.ts +29 -0
- package/dest/interfaces/singleton.d.ts.map +1 -0
- package/dest/interfaces/singleton.js +4 -0
- package/dest/interfaces/singleton_test_suite.d.ts +3 -0
- package/dest/interfaces/singleton_test_suite.d.ts.map +1 -0
- package/dest/interfaces/singleton_test_suite.js +30 -0
- package/dest/interfaces/store.d.ts +145 -0
- package/dest/interfaces/store.d.ts.map +1 -0
- package/dest/interfaces/store.js +1 -0
- package/dest/interfaces/store_test_suite.d.ts +3 -0
- package/dest/interfaces/store_test_suite.d.ts.map +1 -0
- package/dest/interfaces/store_test_suite.js +37 -0
- package/dest/interfaces/utils.d.ts +16 -0
- package/dest/interfaces/utils.d.ts.map +1 -0
- package/dest/interfaces/utils.js +16 -0
- package/dest/lmdb/array.d.ts +23 -0
- package/dest/lmdb/array.d.ts.map +1 -0
- package/dest/lmdb/array.js +117 -0
- package/dest/lmdb/counter.d.ts +19 -0
- package/dest/lmdb/counter.d.ts.map +1 -0
- package/dest/lmdb/counter.js +50 -0
- package/dest/lmdb/index.d.ts +12 -0
- package/dest/lmdb/index.d.ts.map +1 -0
- package/dest/lmdb/index.js +25 -0
- package/dest/lmdb/map.d.ts +52 -0
- package/dest/lmdb/map.d.ts.map +1 -0
- package/dest/lmdb/map.js +222 -0
- package/dest/lmdb/set.d.ts +18 -0
- package/dest/lmdb/set.d.ts.map +1 -0
- package/dest/lmdb/set.js +32 -0
- package/dest/lmdb/singleton.d.ts +14 -0
- package/dest/lmdb/singleton.d.ts.map +1 -0
- package/dest/lmdb/singleton.js +26 -0
- package/dest/lmdb/store.d.ts +110 -0
- package/dest/lmdb/store.d.ts.map +1 -0
- package/dest/lmdb/store.js +216 -0
- package/dest/lmdb-v2/array.d.ts +18 -0
- package/dest/lmdb-v2/array.d.ts.map +1 -0
- package/dest/lmdb-v2/array.js +101 -0
- package/dest/lmdb-v2/factory.d.ts +12 -0
- package/dest/lmdb-v2/factory.d.ts.map +1 -0
- package/dest/lmdb-v2/factory.js +60 -0
- package/dest/lmdb-v2/index.d.ts +3 -0
- package/dest/lmdb-v2/index.d.ts.map +1 -0
- package/dest/lmdb-v2/index.js +2 -0
- package/dest/lmdb-v2/map.d.ts +86 -0
- package/dest/lmdb-v2/map.d.ts.map +1 -0
- package/dest/lmdb-v2/map.js +193 -0
- package/dest/lmdb-v2/message.d.ts +112 -0
- package/dest/lmdb-v2/message.d.ts.map +1 -0
- package/dest/lmdb-v2/message.js +18 -0
- package/dest/lmdb-v2/read_transaction.d.ts +14 -0
- package/dest/lmdb-v2/read_transaction.d.ts.map +1 -0
- package/dest/lmdb-v2/read_transaction.js +101 -0
- package/dest/lmdb-v2/singleton.d.ts +12 -0
- package/dest/lmdb-v2/singleton.d.ts.map +1 -0
- package/dest/lmdb-v2/singleton.js +31 -0
- package/dest/lmdb-v2/store.d.ts +43 -0
- package/dest/lmdb-v2/store.d.ts.map +1 -0
- package/dest/lmdb-v2/store.js +181 -0
- package/dest/lmdb-v2/utils.d.ts +19 -0
- package/dest/lmdb-v2/utils.d.ts.map +1 -0
- package/dest/lmdb-v2/utils.js +126 -0
- package/dest/lmdb-v2/write_transaction.d.ts +19 -0
- package/dest/lmdb-v2/write_transaction.d.ts.map +1 -0
- package/dest/lmdb-v2/write_transaction.js +254 -0
- package/dest/stores/index.d.ts +2 -0
- package/dest/stores/index.d.ts.map +1 -0
- package/dest/stores/index.js +1 -0
- package/dest/stores/l2_tips_store.d.ts +13 -0
- package/dest/stores/l2_tips_store.d.ts.map +1 -0
- package/dest/stores/l2_tips_store.js +65 -0
- package/dest/utils.d.ts +12 -0
- package/dest/utils.d.ts.map +1 -0
- package/dest/utils.js +24 -0
- package/package.json +99 -0
- package/src/config.ts +34 -0
- package/src/indexeddb/array.ts +118 -0
- package/src/indexeddb/index.ts +29 -0
- package/src/indexeddb/map.ts +142 -0
- package/src/indexeddb/set.ts +37 -0
- package/src/indexeddb/singleton.ts +49 -0
- package/src/indexeddb/store.ts +197 -0
- package/src/interfaces/array.ts +90 -0
- package/src/interfaces/array_test_suite.ts +130 -0
- package/src/interfaces/common.ts +20 -0
- package/src/interfaces/counter.ts +65 -0
- package/src/interfaces/index.ts +7 -0
- package/src/interfaces/map.ts +151 -0
- package/src/interfaces/map_test_suite.ts +143 -0
- package/src/interfaces/set.ts +48 -0
- package/src/interfaces/set_test_suite.ts +81 -0
- package/src/interfaces/singleton.ts +29 -0
- package/src/interfaces/singleton_test_suite.ts +46 -0
- package/src/interfaces/store.ts +177 -0
- package/src/interfaces/store_test_suite.ts +56 -0
- package/src/interfaces/utils.ts +21 -0
- package/src/lmdb/array.ts +139 -0
- package/src/lmdb/counter.ts +67 -0
- package/src/lmdb/index.ts +37 -0
- package/src/lmdb/map.ts +248 -0
- package/src/lmdb/set.ts +45 -0
- package/src/lmdb/singleton.ts +35 -0
- package/src/lmdb/store.ts +263 -0
- package/src/lmdb-v2/array.ts +115 -0
- package/src/lmdb-v2/factory.ts +98 -0
- package/src/lmdb-v2/index.ts +2 -0
- package/src/lmdb-v2/map.ts +233 -0
- package/src/lmdb-v2/message.ts +146 -0
- package/src/lmdb-v2/read_transaction.ts +116 -0
- package/src/lmdb-v2/singleton.ts +34 -0
- package/src/lmdb-v2/store.ts +237 -0
- package/src/lmdb-v2/utils.ts +150 -0
- package/src/lmdb-v2/write_transaction.ts +314 -0
- package/src/stores/index.ts +1 -0
- package/src/stores/l2_tips_store.ts +70 -0
- package/src/utils.ts +40 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
2
|
+
import { Semaphore, SerialQueue } from '@aztec/foundation/queue';
|
|
3
|
+
import { MsgpackChannel, NativeLMDBStore } from '@aztec/native';
|
|
4
|
+
|
|
5
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
6
|
+
import { rm } from 'fs/promises';
|
|
7
|
+
|
|
8
|
+
import type { AztecAsyncArray } from '../interfaces/array.js';
|
|
9
|
+
import type { Key, StoreSize } from '../interfaces/common.js';
|
|
10
|
+
import type { AztecAsyncCounter } from '../interfaces/counter.js';
|
|
11
|
+
import type { AztecAsyncMap, AztecAsyncMultiMap } from '../interfaces/map.js';
|
|
12
|
+
import type { AztecAsyncSet } from '../interfaces/set.js';
|
|
13
|
+
import type { AztecAsyncSingleton } from '../interfaces/singleton.js';
|
|
14
|
+
import type { AztecAsyncKVStore } from '../interfaces/store.js';
|
|
15
|
+
import { LMDBArray } from './array.js';
|
|
16
|
+
import { LMDBMap, LMDBMultiMap } from './map.js';
|
|
17
|
+
import {
|
|
18
|
+
Database,
|
|
19
|
+
type LMDBMessageChannel,
|
|
20
|
+
LMDBMessageType,
|
|
21
|
+
type LMDBRequestBody,
|
|
22
|
+
type LMDBResponseBody,
|
|
23
|
+
} from './message.js';
|
|
24
|
+
import { ReadTransaction } from './read_transaction.js';
|
|
25
|
+
import { LMDBSingleValue } from './singleton.js';
|
|
26
|
+
import { WriteTransaction } from './write_transaction.js';
|
|
27
|
+
|
|
28
|
+
export class AztecLMDBStoreV2 implements AztecAsyncKVStore, LMDBMessageChannel {
|
|
29
|
+
private open = false;
|
|
30
|
+
private channel: MsgpackChannel<LMDBMessageType, LMDBRequestBody, LMDBResponseBody>;
|
|
31
|
+
private writerCtx = new AsyncLocalStorage<WriteTransaction>();
|
|
32
|
+
private writerQueue = new SerialQueue();
|
|
33
|
+
private availableCursors: Semaphore;
|
|
34
|
+
|
|
35
|
+
private constructor(
|
|
36
|
+
private dataDir: string,
|
|
37
|
+
mapSize: number,
|
|
38
|
+
maxReaders: number,
|
|
39
|
+
private log: Logger,
|
|
40
|
+
private cleanup?: () => Promise<void>,
|
|
41
|
+
) {
|
|
42
|
+
this.log.info(`Starting data store with maxReaders ${maxReaders}`);
|
|
43
|
+
this.channel = new MsgpackChannel(new NativeLMDBStore(dataDir, mapSize, maxReaders));
|
|
44
|
+
// leave one reader to always be available for regular, atomic, reads
|
|
45
|
+
this.availableCursors = new Semaphore(maxReaders - 1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public get dataDirectory(): string {
|
|
49
|
+
return this.dataDir;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private async start() {
|
|
53
|
+
this.writerQueue.start();
|
|
54
|
+
|
|
55
|
+
await this.channel.sendMessage(LMDBMessageType.OPEN_DATABASE, {
|
|
56
|
+
db: Database.DATA,
|
|
57
|
+
uniqueKeys: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await this.channel.sendMessage(LMDBMessageType.OPEN_DATABASE, {
|
|
61
|
+
db: Database.INDEX,
|
|
62
|
+
uniqueKeys: false,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
this.open = true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public static async new(
|
|
69
|
+
dataDir: string,
|
|
70
|
+
dbMapSizeKb: number = 10 * 1024 * 1024,
|
|
71
|
+
maxReaders: number = 16,
|
|
72
|
+
cleanup?: () => Promise<void>,
|
|
73
|
+
log = createLogger('kv-store:lmdb-v2'),
|
|
74
|
+
) {
|
|
75
|
+
const db = new AztecLMDBStoreV2(dataDir, dbMapSizeKb, maxReaders, log, cleanup);
|
|
76
|
+
await db.start();
|
|
77
|
+
return db;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public getReadTx(): ReadTransaction {
|
|
81
|
+
if (!this.open) {
|
|
82
|
+
throw new Error('Store is closed');
|
|
83
|
+
}
|
|
84
|
+
return new ReadTransaction(this);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public getCurrentWriteTx(): WriteTransaction | undefined {
|
|
88
|
+
if (!this.open) {
|
|
89
|
+
throw new Error('Store is closed');
|
|
90
|
+
}
|
|
91
|
+
const currentWrite = this.writerCtx.getStore();
|
|
92
|
+
return currentWrite;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
openMap<K extends Key, V>(name: string): AztecAsyncMap<K, V> {
|
|
96
|
+
return new LMDBMap(this, name);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
openMultiMap<K extends Key, V>(name: string): AztecAsyncMultiMap<K, V> {
|
|
100
|
+
return new LMDBMultiMap(this, name);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
openSingleton<T>(name: string): AztecAsyncSingleton<T> {
|
|
104
|
+
return new LMDBSingleValue(this, name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
openArray<T>(name: string): AztecAsyncArray<T> {
|
|
108
|
+
return new LMDBArray(this, name);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
openSet<K extends Key>(_name: string): AztecAsyncSet<K> {
|
|
112
|
+
throw new Error('Not implemented');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
openCounter<K extends Key>(_name: string): AztecAsyncCounter<K> {
|
|
116
|
+
throw new Error('Not implemented');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async transactionAsync<T extends Exclude<any, Promise<any>>>(
|
|
120
|
+
callback: (tx: WriteTransaction) => Promise<T>,
|
|
121
|
+
): Promise<T> {
|
|
122
|
+
if (!this.open) {
|
|
123
|
+
throw new Error('Store is closed');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// transactionAsync might be called recursively
|
|
127
|
+
// send any writes to the parent tx, but don't close it
|
|
128
|
+
// if the callback throws then the parent tx will rollback automatically
|
|
129
|
+
const currentTx = this.getCurrentWriteTx();
|
|
130
|
+
if (currentTx) {
|
|
131
|
+
return await callback(currentTx);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return this.writerQueue.put(async () => {
|
|
135
|
+
const tx = new WriteTransaction(this);
|
|
136
|
+
try {
|
|
137
|
+
const res = await this.writerCtx.run(tx, callback, tx);
|
|
138
|
+
await tx.commit();
|
|
139
|
+
return res;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
this.log.error(`Failed to commit transaction`, err);
|
|
142
|
+
throw err;
|
|
143
|
+
} finally {
|
|
144
|
+
tx.close();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
clear(): Promise<void> {
|
|
150
|
+
return Promise.resolve();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fork(): Promise<AztecAsyncKVStore> {
|
|
154
|
+
throw new Error('Not implemented');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async delete(): Promise<void> {
|
|
158
|
+
await this.close();
|
|
159
|
+
await rm(this.dataDir, { recursive: true, force: true, maxRetries: 3 });
|
|
160
|
+
this.log.verbose(`Deleted database files at ${this.dataDir}`);
|
|
161
|
+
await this.cleanup?.();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async close() {
|
|
165
|
+
if (!this.open) {
|
|
166
|
+
// already closed
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.open = false;
|
|
170
|
+
await this.writerQueue.cancel();
|
|
171
|
+
await this.channel.sendMessage(LMDBMessageType.CLOSE, undefined);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public async sendMessage<T extends LMDBMessageType>(
|
|
175
|
+
msgType: T,
|
|
176
|
+
body: LMDBRequestBody[T],
|
|
177
|
+
): Promise<LMDBResponseBody[T]> {
|
|
178
|
+
if (!this.open) {
|
|
179
|
+
throw new Error('Store is closed');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (msgType === LMDBMessageType.START_CURSOR) {
|
|
183
|
+
await this.availableCursors.acquire();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let response: LMDBResponseBody[T] | undefined = undefined;
|
|
187
|
+
try {
|
|
188
|
+
({ response } = await this.channel.sendMessage(msgType, body));
|
|
189
|
+
return response;
|
|
190
|
+
} finally {
|
|
191
|
+
if (
|
|
192
|
+
(msgType === LMDBMessageType.START_CURSOR && response === undefined) ||
|
|
193
|
+
msgType === LMDBMessageType.CLOSE_CURSOR ||
|
|
194
|
+
// it's possible for a START_CURSOR command to not return a cursor (e.g. db is empty)
|
|
195
|
+
(msgType === LMDBMessageType.START_CURSOR &&
|
|
196
|
+
typeof (response as LMDBResponseBody[LMDBMessageType.START_CURSOR]).cursor !== 'number')
|
|
197
|
+
) {
|
|
198
|
+
this.availableCursors.release();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public async estimateSize(): Promise<StoreSize> {
|
|
204
|
+
const resp = await this.sendMessage(LMDBMessageType.STATS, undefined);
|
|
205
|
+
return {
|
|
206
|
+
mappingSize: Number(resp.dbMapSizeBytes),
|
|
207
|
+
actualSize: resp.stats.reduce((s, db) => Number(db.totalUsedSize) + s, 0),
|
|
208
|
+
numItems: resp.stats.reduce((s, db) => Number(db.numDataItems) + s, 0),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function execInWriteTx<T>(store: AztecLMDBStoreV2, fn: (tx: WriteTransaction) => Promise<T>): Promise<T> {
|
|
214
|
+
const currentWrite = store.getCurrentWriteTx();
|
|
215
|
+
if (currentWrite) {
|
|
216
|
+
return fn(currentWrite);
|
|
217
|
+
} else {
|
|
218
|
+
return store.transactionAsync(fn);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function execInReadTx<T>(
|
|
223
|
+
store: AztecLMDBStoreV2,
|
|
224
|
+
fn: (tx: ReadTransaction) => T | Promise<T>,
|
|
225
|
+
): Promise<T> {
|
|
226
|
+
const currentWrite = store.getCurrentWriteTx();
|
|
227
|
+
if (currentWrite) {
|
|
228
|
+
return await fn(currentWrite);
|
|
229
|
+
} else {
|
|
230
|
+
const tx = store.getReadTx();
|
|
231
|
+
try {
|
|
232
|
+
return await fn(tx);
|
|
233
|
+
} finally {
|
|
234
|
+
tx.close();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { MAXIMUM_KEY, fromBufferKey, toBufferKey } from 'ordered-binary';
|
|
2
|
+
|
|
3
|
+
import type { Key } from '../interfaces/common.js';
|
|
4
|
+
|
|
5
|
+
type Cmp<T> = (a: T, b: T) => -1 | 0 | 1;
|
|
6
|
+
|
|
7
|
+
export function dedupeSortedArray<T>(arr: T[], cmp: Cmp<T>): void {
|
|
8
|
+
for (let i = 0; i < arr.length; i++) {
|
|
9
|
+
let j = i + 1;
|
|
10
|
+
for (; j < arr.length; j++) {
|
|
11
|
+
const res = cmp(arr[i], arr[j]);
|
|
12
|
+
if (res === 0) {
|
|
13
|
+
continue;
|
|
14
|
+
} else if (res < 0) {
|
|
15
|
+
break;
|
|
16
|
+
} else {
|
|
17
|
+
throw new Error('Array not sorted');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (j - i > 1) {
|
|
22
|
+
arr.splice(i + 1, j - i - 1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function insertIntoSortedArray<T>(arr: T[], item: T, cmp: (a: T, b: T) => number): void {
|
|
28
|
+
let left = 0;
|
|
29
|
+
let right = arr.length;
|
|
30
|
+
|
|
31
|
+
while (left < right) {
|
|
32
|
+
const mid = (left + right) >> 1;
|
|
33
|
+
const comparison = cmp(arr[mid], item);
|
|
34
|
+
|
|
35
|
+
if (comparison < 0) {
|
|
36
|
+
left = mid + 1;
|
|
37
|
+
} else {
|
|
38
|
+
right = mid;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
arr.splice(left, 0, item);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function findIndexInSortedArray<T, N>(values: T[], needle: N, cmp: (a: T, b: N) => number): number {
|
|
46
|
+
let start = 0;
|
|
47
|
+
let end = values.length - 1;
|
|
48
|
+
|
|
49
|
+
while (start <= end) {
|
|
50
|
+
const mid = start + (((end - start) / 2) | 0);
|
|
51
|
+
const res = cmp(values[mid], needle);
|
|
52
|
+
if (res === 0) {
|
|
53
|
+
return mid;
|
|
54
|
+
} else if (res > 0) {
|
|
55
|
+
end = mid - 1;
|
|
56
|
+
} else {
|
|
57
|
+
start = mid + 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return -1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function findInSortedArray<T, N>(values: T[], needle: N, cmp: (a: T, b: N) => number): T | undefined {
|
|
65
|
+
const idx = findIndexInSortedArray(values, needle, cmp);
|
|
66
|
+
return idx > -1 ? values[idx] : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function removeAnyOf<T, N>(arr: T[], vals: N[], cmp: (a: T, b: N) => -1 | 0 | 1): void {
|
|
70
|
+
let writeIdx = 0;
|
|
71
|
+
let readIdx = 0;
|
|
72
|
+
let valIdx = 0;
|
|
73
|
+
|
|
74
|
+
while (readIdx < arr.length && valIdx < vals.length) {
|
|
75
|
+
const comparison = cmp(arr[readIdx], vals[valIdx]);
|
|
76
|
+
|
|
77
|
+
if (comparison < 0) {
|
|
78
|
+
arr[writeIdx++] = arr[readIdx++];
|
|
79
|
+
} else if (comparison > 0) {
|
|
80
|
+
valIdx++;
|
|
81
|
+
} else {
|
|
82
|
+
readIdx++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
while (readIdx < arr.length) {
|
|
87
|
+
arr[writeIdx++] = arr[readIdx++];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
arr.length = writeIdx;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function removeFromSortedArray<T, N>(arr: T[], val: N, cmp: (a: T, b: N) => -1 | 0 | 1) {
|
|
94
|
+
const idx = findIndexInSortedArray(arr, val, cmp);
|
|
95
|
+
if (idx > -1) {
|
|
96
|
+
arr.splice(idx, 1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function merge<T>(arr: T[], toInsert: T[], cmp: (a: T, b: T) => -1 | 0 | 1): void {
|
|
101
|
+
const result = new Array<T>(arr.length + toInsert.length);
|
|
102
|
+
let i = 0,
|
|
103
|
+
j = 0,
|
|
104
|
+
k = 0;
|
|
105
|
+
|
|
106
|
+
while (i < arr.length && j < toInsert.length) {
|
|
107
|
+
result[k++] = cmp(arr[i], toInsert[j]) <= 0 ? arr[i++] : toInsert[j++];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
while (i < arr.length) {
|
|
111
|
+
result[k++] = arr[i++];
|
|
112
|
+
}
|
|
113
|
+
while (j < toInsert.length) {
|
|
114
|
+
result[k++] = toInsert[j++];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (i = 0; i < result.length; i++) {
|
|
118
|
+
arr[i] = result[i];
|
|
119
|
+
}
|
|
120
|
+
arr.length = result.length;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function keyCmp(a: [Uint8Array, Uint8Array[] | null], b: [Uint8Array, Uint8Array[] | null]): -1 | 0 | 1 {
|
|
124
|
+
return Buffer.compare(a[0], b[0]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function singleKeyCmp(a: [Uint8Array, Uint8Array[] | null], b: Uint8Array): -1 | 0 | 1 {
|
|
128
|
+
return Buffer.compare(a[0], b);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function minKey(prefix: string) {
|
|
132
|
+
return toBufferKey([prefix]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function maxKey(prefix: string) {
|
|
136
|
+
return toBufferKey([prefix, MAXIMUM_KEY]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function serializeKey(prefix: string, key: Key): Buffer {
|
|
140
|
+
return toBufferKey([prefix, key]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function deserializeKey<K extends Key>(prefix: string, key: Uint8Array): K | false {
|
|
144
|
+
const buf = Buffer.from(key);
|
|
145
|
+
const parsed = fromBufferKey(buf);
|
|
146
|
+
if (!Array.isArray(parsed) || parsed[0] !== prefix) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return parsed[1] as K;
|
|
150
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { type Batch, Database, LMDBMessageType } from './message.js';
|
|
2
|
+
import { ReadTransaction } from './read_transaction.js';
|
|
3
|
+
import {
|
|
4
|
+
dedupeSortedArray,
|
|
5
|
+
findInSortedArray,
|
|
6
|
+
findIndexInSortedArray,
|
|
7
|
+
insertIntoSortedArray,
|
|
8
|
+
keyCmp,
|
|
9
|
+
merge,
|
|
10
|
+
removeAnyOf,
|
|
11
|
+
removeFromSortedArray,
|
|
12
|
+
singleKeyCmp,
|
|
13
|
+
} from './utils.js';
|
|
14
|
+
|
|
15
|
+
export class WriteTransaction extends ReadTransaction {
|
|
16
|
+
// exposed for tests
|
|
17
|
+
public readonly dataBatch: Batch = {
|
|
18
|
+
addEntries: [],
|
|
19
|
+
removeEntries: [],
|
|
20
|
+
};
|
|
21
|
+
public readonly indexBatch: Batch = {
|
|
22
|
+
addEntries: [],
|
|
23
|
+
removeEntries: [],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
set(key: Uint8Array, value: Uint8Array): Promise<void> {
|
|
27
|
+
this.assertIsOpen();
|
|
28
|
+
|
|
29
|
+
const addEntry = findInSortedArray(this.dataBatch.addEntries, key, singleKeyCmp);
|
|
30
|
+
if (!addEntry) {
|
|
31
|
+
insertIntoSortedArray(this.dataBatch.addEntries, [key, [value]], keyCmp);
|
|
32
|
+
} else {
|
|
33
|
+
addEntry[1] = [value];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const removeEntryIndex = findIndexInSortedArray(this.dataBatch.removeEntries, key, singleKeyCmp);
|
|
37
|
+
if (removeEntryIndex > -1) {
|
|
38
|
+
this.dataBatch.removeEntries.splice(removeEntryIndex, 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Promise.resolve();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
remove(key: Uint8Array): Promise<void> {
|
|
45
|
+
const removeEntryIndex = findIndexInSortedArray(this.dataBatch.removeEntries, key, singleKeyCmp);
|
|
46
|
+
if (removeEntryIndex === -1) {
|
|
47
|
+
this.dataBatch.removeEntries.push([key, null]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const addEntryIndex = findIndexInSortedArray(this.dataBatch.addEntries, key, singleKeyCmp);
|
|
51
|
+
if (addEntryIndex > -1) {
|
|
52
|
+
this.dataBatch.addEntries.splice(addEntryIndex, 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public override async get(key: Buffer): Promise<Uint8Array | undefined> {
|
|
59
|
+
this.assertIsOpen();
|
|
60
|
+
|
|
61
|
+
const addEntry = findInSortedArray(this.dataBatch.addEntries, key, singleKeyCmp);
|
|
62
|
+
if (addEntry) {
|
|
63
|
+
return addEntry[1][0];
|
|
64
|
+
}
|
|
65
|
+
const removeEntryIdx = findIndexInSortedArray(this.dataBatch.removeEntries, key, singleKeyCmp);
|
|
66
|
+
if (removeEntryIdx > -1) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return await super.get(key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setIndex(key: Buffer, ...values: Buffer[]): Promise<void> {
|
|
74
|
+
this.assertIsOpen();
|
|
75
|
+
|
|
76
|
+
const addEntries = findInSortedArray(this.indexBatch.addEntries, key, singleKeyCmp);
|
|
77
|
+
const removeEntries = findInSortedArray(this.indexBatch.removeEntries, key, singleKeyCmp);
|
|
78
|
+
|
|
79
|
+
if (removeEntries) {
|
|
80
|
+
if (removeEntries[1]) {
|
|
81
|
+
// check if we were deleting these values and update
|
|
82
|
+
removeAnyOf(removeEntries[1], values, Buffer.compare);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!removeEntries[1] || removeEntries[1].length === 0) {
|
|
86
|
+
// either we were deleting the entire key previously
|
|
87
|
+
// or after cleaning up duplicates, we don't have anything else to delete
|
|
88
|
+
removeFromSortedArray(this.indexBatch.removeEntries, removeEntries, keyCmp);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (addEntries) {
|
|
93
|
+
merge(addEntries[1], values, Buffer.compare);
|
|
94
|
+
dedupeSortedArray(addEntries[1], Buffer.compare);
|
|
95
|
+
} else {
|
|
96
|
+
insertIntoSortedArray(this.indexBatch.addEntries, [key, values], keyCmp);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Promise.resolve();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
removeIndex(key: Buffer, ...values: Buffer[]): Promise<void> {
|
|
103
|
+
this.assertIsOpen();
|
|
104
|
+
|
|
105
|
+
const addEntries = findInSortedArray(this.indexBatch.addEntries, key, singleKeyCmp);
|
|
106
|
+
const removeEntries = findInSortedArray(this.indexBatch.removeEntries, key, singleKeyCmp);
|
|
107
|
+
|
|
108
|
+
if (values.length === 0) {
|
|
109
|
+
// special case, we're deleting the entire key
|
|
110
|
+
if (addEntries) {
|
|
111
|
+
removeFromSortedArray(this.indexBatch.addEntries, addEntries, keyCmp);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (removeEntries) {
|
|
115
|
+
removeEntries[1] = null;
|
|
116
|
+
} else {
|
|
117
|
+
insertIntoSortedArray(this.indexBatch.removeEntries, [key, null], keyCmp);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return Promise.resolve();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (addEntries) {
|
|
124
|
+
removeAnyOf(addEntries[1], values, Buffer.compare);
|
|
125
|
+
if (addEntries[1].length === 0) {
|
|
126
|
+
removeFromSortedArray(this.indexBatch.addEntries, addEntries, keyCmp);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (removeEntries) {
|
|
131
|
+
removeEntries[1] ??= [];
|
|
132
|
+
merge(removeEntries[1], values, Buffer.compare);
|
|
133
|
+
dedupeSortedArray(removeEntries[1], Buffer.compare);
|
|
134
|
+
} else {
|
|
135
|
+
insertIntoSortedArray(this.indexBatch.removeEntries, [key, values], keyCmp);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return Promise.resolve();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public override async getIndex(key: Buffer): Promise<Uint8Array[]> {
|
|
142
|
+
this.assertIsOpen();
|
|
143
|
+
|
|
144
|
+
const removeEntries = findInSortedArray(this.indexBatch.removeEntries, key, singleKeyCmp);
|
|
145
|
+
if (removeEntries && removeEntries[1] === null) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const addEntries = findInSortedArray(this.indexBatch.addEntries, key, singleKeyCmp);
|
|
150
|
+
const results = await super.getIndex(key);
|
|
151
|
+
|
|
152
|
+
if (addEntries) {
|
|
153
|
+
merge(results, addEntries[1], Buffer.compare);
|
|
154
|
+
dedupeSortedArray(results, Buffer.compare);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (removeEntries && Array.isArray(removeEntries[1])) {
|
|
158
|
+
removeAnyOf(results, removeEntries[1], Buffer.compare);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return results;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public override async *iterate(
|
|
165
|
+
startKey: Uint8Array,
|
|
166
|
+
endKey?: Uint8Array | undefined,
|
|
167
|
+
reverse?: boolean,
|
|
168
|
+
limit?: number,
|
|
169
|
+
): AsyncIterable<[Uint8Array, Uint8Array]> {
|
|
170
|
+
yield* this.#iterate(
|
|
171
|
+
super.iterate(startKey, endKey, reverse),
|
|
172
|
+
this.dataBatch,
|
|
173
|
+
startKey,
|
|
174
|
+
endKey,
|
|
175
|
+
reverse,
|
|
176
|
+
limit,
|
|
177
|
+
(committed, toAdd) => (toAdd.length > 0 ? toAdd[0] : committed),
|
|
178
|
+
vals => vals[0],
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public override async *iterateIndex(
|
|
183
|
+
startKey: Uint8Array,
|
|
184
|
+
endKey?: Uint8Array | undefined,
|
|
185
|
+
reverse?: boolean,
|
|
186
|
+
limit?: number,
|
|
187
|
+
): AsyncIterable<[Uint8Array, Uint8Array[]]> {
|
|
188
|
+
yield* this.#iterate(
|
|
189
|
+
super.iterateIndex(startKey, endKey, reverse),
|
|
190
|
+
this.indexBatch,
|
|
191
|
+
startKey,
|
|
192
|
+
endKey,
|
|
193
|
+
reverse,
|
|
194
|
+
limit,
|
|
195
|
+
(committed, toAdd, toRemove) => {
|
|
196
|
+
if (toAdd.length > 0) {
|
|
197
|
+
merge(committed, toAdd, Buffer.compare);
|
|
198
|
+
dedupeSortedArray(committed, Buffer.compare);
|
|
199
|
+
}
|
|
200
|
+
if (toRemove.length > 0) {
|
|
201
|
+
removeAnyOf(committed, toRemove, Buffer.compare);
|
|
202
|
+
}
|
|
203
|
+
return committed;
|
|
204
|
+
},
|
|
205
|
+
vals => vals,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async *#iterate<T>(
|
|
210
|
+
iterator: AsyncIterable<[Uint8Array, T]>,
|
|
211
|
+
batch: Batch,
|
|
212
|
+
startKey: Uint8Array,
|
|
213
|
+
endKey: Uint8Array | undefined,
|
|
214
|
+
reverse: boolean = false,
|
|
215
|
+
limit: number | undefined,
|
|
216
|
+
merge: (committed: T, toAdd: Uint8Array[], toRemove: Uint8Array[]) => T,
|
|
217
|
+
map: (vals: Uint8Array[]) => T,
|
|
218
|
+
): AsyncIterable<[Uint8Array, T]> {
|
|
219
|
+
this.assertIsOpen();
|
|
220
|
+
|
|
221
|
+
// make a copy of this in case we're running in reverse
|
|
222
|
+
const uncommittedEntries = [...batch.addEntries];
|
|
223
|
+
// used to check we're in the right order when comparing between a key and uncommittedEntries
|
|
224
|
+
let cmpDirection = -1;
|
|
225
|
+
if (reverse) {
|
|
226
|
+
cmpDirection = 1;
|
|
227
|
+
uncommittedEntries.reverse();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let uncommittedEntriesIdx = 0;
|
|
231
|
+
while (uncommittedEntriesIdx < uncommittedEntries.length) {
|
|
232
|
+
const entry = uncommittedEntries[uncommittedEntriesIdx];
|
|
233
|
+
// go to the first key in our cache that would be captured by the iterator
|
|
234
|
+
if (Buffer.compare(entry[0], startKey) !== cmpDirection) {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
uncommittedEntriesIdx++;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let count = 0;
|
|
241
|
+
// helper to early return if we've reached our limit
|
|
242
|
+
const checkLimit = typeof limit === 'number' ? () => count < limit : () => true;
|
|
243
|
+
for await (const [key, values] of iterator) {
|
|
244
|
+
// yield every key that we have cached that's captured by the iterator
|
|
245
|
+
while (uncommittedEntriesIdx < uncommittedEntries.length && checkLimit()) {
|
|
246
|
+
const entry = uncommittedEntries[uncommittedEntriesIdx];
|
|
247
|
+
if (endKey && Buffer.compare(entry[0], endKey) !== cmpDirection) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (Buffer.compare(entry[0], key) === cmpDirection) {
|
|
252
|
+
count++;
|
|
253
|
+
yield [entry[0], map(entry[1])];
|
|
254
|
+
} else {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
uncommittedEntriesIdx++;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!checkLimit()) {
|
|
261
|
+
// we reached the imposed `limit`
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const toRemove = findInSortedArray(batch.removeEntries, key, singleKeyCmp);
|
|
266
|
+
|
|
267
|
+
// at this point we've either exhausted all uncommitted entries,
|
|
268
|
+
// we reached a key strictly greater/smaller than `key`
|
|
269
|
+
// or we found the key itself
|
|
270
|
+
// check if it's the key and use the uncommitted value
|
|
271
|
+
let toAdd: Uint8Array[] = [];
|
|
272
|
+
if (
|
|
273
|
+
uncommittedEntriesIdx < uncommittedEntries.length &&
|
|
274
|
+
Buffer.compare(uncommittedEntries[uncommittedEntriesIdx][0], key) === 0
|
|
275
|
+
) {
|
|
276
|
+
toAdd = uncommittedEntries[uncommittedEntriesIdx][1];
|
|
277
|
+
uncommittedEntriesIdx++;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (toRemove && !toRemove[1]) {
|
|
281
|
+
// we were told to delete this key entirely
|
|
282
|
+
continue;
|
|
283
|
+
} else {
|
|
284
|
+
const mergedValues = merge(values, toAdd, toRemove?.[1] ?? []);
|
|
285
|
+
if (mergedValues) {
|
|
286
|
+
count++;
|
|
287
|
+
yield [key, mergedValues];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// emit all the uncommitted data that would be captured by this iterator
|
|
293
|
+
while (uncommittedEntriesIdx < uncommittedEntries.length && checkLimit()) {
|
|
294
|
+
const entry = uncommittedEntries[uncommittedEntriesIdx];
|
|
295
|
+
if (endKey && Buffer.compare(entry[0], endKey) !== cmpDirection) {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
count++;
|
|
299
|
+
yield [entry[0], map(entry[1])];
|
|
300
|
+
uncommittedEntriesIdx++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
public async commit() {
|
|
305
|
+
this.assertIsOpen();
|
|
306
|
+
this.close();
|
|
307
|
+
await this.channel.sendMessage(LMDBMessageType.BATCH, {
|
|
308
|
+
batches: new Map([
|
|
309
|
+
[Database.DATA, this.dataBatch],
|
|
310
|
+
[Database.INDEX, this.indexBatch],
|
|
311
|
+
]),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './l2_tips_store.js';
|