xitdb 0.12.0 → 0.14.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 +120 -118
- package/dist/core-buffered-file.d.ts +23 -25
- package/dist/core-buffered-file.js +182 -0
- package/dist/core-file.d.ts +8 -10
- package/dist/core-file.js +105 -0
- package/dist/core-memory.d.ts +15 -15
- package/dist/core-memory.js +152 -0
- package/dist/core.d.ts +14 -14
- package/dist/core.js +1 -0
- package/dist/database.d.ts +204 -80
- package/dist/database.js +3047 -0
- package/dist/exceptions.d.ts +4 -0
- package/dist/exceptions.js +62 -0
- package/dist/hasher.d.ts +2 -1
- package/dist/hasher.js +49 -0
- package/dist/index.d.ts +30 -26
- package/dist/index.js +35 -3986
- package/dist/read-array-list.d.ts +8 -8
- package/dist/read-array-list.js +35 -0
- package/dist/read-counted-hash-map.d.ts +3 -3
- package/dist/read-counted-hash-map.js +19 -0
- package/dist/read-counted-hash-set.d.ts +3 -3
- package/dist/read-counted-hash-set.js +19 -0
- package/dist/read-cursor.d.ts +29 -23
- package/dist/read-cursor.js +577 -0
- package/dist/read-hash-map.d.ts +22 -22
- package/dist/read-hash-map.js +65 -0
- package/dist/read-hash-set.d.ts +13 -13
- package/dist/read-hash-set.js +47 -0
- package/dist/read-linked-array-list.d.ts +8 -8
- package/dist/read-linked-array-list.js +35 -0
- package/dist/read-sorted-map.d.ts +21 -0
- package/dist/read-sorted-map.js +73 -0
- package/dist/read-sorted-set.d.ts +19 -0
- package/dist/read-sorted-set.js +65 -0
- package/dist/slot-pointer.d.ts +1 -1
- package/dist/slot-pointer.js +11 -0
- package/dist/slot.d.ts +2 -2
- package/dist/slot.js +41 -0
- package/dist/slotted.d.ts +1 -1
- package/dist/slotted.js +1 -0
- package/dist/tag.d.ts +3 -1
- package/dist/tag.js +25 -0
- package/dist/write-array-list.d.ts +13 -14
- package/dist/write-array-list.js +42 -0
- package/dist/write-counted-hash-map.d.ts +4 -5
- package/dist/write-counted-hash-map.js +18 -0
- package/dist/write-counted-hash-set.d.ts +4 -5
- package/dist/write-counted-hash-set.js +18 -0
- package/dist/write-cursor.d.ts +15 -15
- package/dist/write-cursor.js +124 -0
- package/dist/write-hash-map.d.ts +22 -23
- package/dist/write-hash-map.js +90 -0
- package/dist/write-hash-set.d.ts +16 -17
- package/dist/write-hash-set.js +59 -0
- package/dist/write-linked-array-list.d.ts +16 -17
- package/dist/write-linked-array-list.js +52 -0
- package/dist/write-sorted-map.d.ts +12 -0
- package/dist/write-sorted-map.js +37 -0
- package/dist/write-sorted-set.d.ts +10 -0
- package/dist/write-sorted-set.js +29 -0
- package/dist/writeable-data.js +68 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* Reads never block writes, and a database can be read from multiple threads/processes without locks.
|
|
18
18
|
* No query engine of any kind. You just write data structures (primarily an `ArrayList` and `HashMap`) that can be nested arbitrarily.
|
|
19
19
|
* No dependencies besides the JavaScript standard library.
|
|
20
|
+
* Fully synchronous API — no async/await needed.
|
|
20
21
|
* Available [on npm](https://www.npmjs.com/package/xitdb).
|
|
21
22
|
|
|
22
23
|
This database was originally made for the [xit version control system](https://github.com/xit-vcs/xit), but I bet it has a lot of potential for other projects. The combination of being immutable and having an API similar to in-memory data structures is pretty powerful. Consider using it [instead of SQLite](https://gist.github.com/xeubie/03a0724484e1111ef4c05d72a935c42c) for your TypeScript projects: it's simpler, it's pure TypeScript, and it creates no impedance mismatch with your program the way SQL databases do.
|
|
@@ -36,13 +37,13 @@ In this example, we create a new database, write some data in a transaction, and
|
|
|
36
37
|
|
|
37
38
|
```typescript
|
|
38
39
|
// init the db
|
|
39
|
-
using core =
|
|
40
|
+
using core = new CoreBufferedFile('main.db');
|
|
40
41
|
const hasher = new Hasher('SHA-1');
|
|
41
|
-
const db =
|
|
42
|
+
const db = new Database(core, hasher);
|
|
42
43
|
|
|
43
44
|
// to get the benefits of immutability, the top-level data structure
|
|
44
45
|
// must be an ArrayList, so each transaction is stored as an item in it
|
|
45
|
-
const history =
|
|
46
|
+
const history = new WriteArrayList(db.rootCursor());
|
|
46
47
|
|
|
47
48
|
// this is how a transaction is executed. we call history.appendContext,
|
|
48
49
|
// providing it with the most recent copy of the db and a context
|
|
@@ -61,53 +62,53 @@ const history = await WriteArrayList.create(db.rootCursor());
|
|
|
61
62
|
// {"name": "Alice", "age": 25},
|
|
62
63
|
// {"name": "Bob", "age": 42}
|
|
63
64
|
// ]}
|
|
64
|
-
|
|
65
|
-
const moment =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const fruitsCursor =
|
|
71
|
-
const fruits =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const peopleCursor =
|
|
77
|
-
const people =
|
|
78
|
-
|
|
79
|
-
const aliceCursor =
|
|
80
|
-
const alice =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const bobCursor =
|
|
85
|
-
const bob =
|
|
86
|
-
|
|
87
|
-
|
|
65
|
+
history.appendContext(history.getSlot(-1), (cursor) => {
|
|
66
|
+
const moment = new WriteHashMap(cursor);
|
|
67
|
+
|
|
68
|
+
moment.put('foo', new Bytes('foo'));
|
|
69
|
+
moment.put('bar', new Bytes('bar'));
|
|
70
|
+
|
|
71
|
+
const fruitsCursor = moment.putCursor('fruits');
|
|
72
|
+
const fruits = new WriteArrayList(fruitsCursor);
|
|
73
|
+
fruits.append(new Bytes('apple'));
|
|
74
|
+
fruits.append(new Bytes('pear'));
|
|
75
|
+
fruits.append(new Bytes('grape'));
|
|
76
|
+
|
|
77
|
+
const peopleCursor = moment.putCursor('people');
|
|
78
|
+
const people = new WriteArrayList(peopleCursor);
|
|
79
|
+
|
|
80
|
+
const aliceCursor = people.appendCursor();
|
|
81
|
+
const alice = new WriteHashMap(aliceCursor);
|
|
82
|
+
alice.put('name', new Bytes('Alice'));
|
|
83
|
+
alice.put('age', new Uint(25));
|
|
84
|
+
|
|
85
|
+
const bobCursor = people.appendCursor();
|
|
86
|
+
const bob = new WriteHashMap(bobCursor);
|
|
87
|
+
bob.put('name', new Bytes('Bob'));
|
|
88
|
+
bob.put('age', new Uint(42));
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
// get the most recent copy of the database, like a moment
|
|
91
92
|
// in time. the -1 index will return the last index in the list.
|
|
92
|
-
const momentCursor =
|
|
93
|
+
const momentCursor = history.getCursor(-1);
|
|
93
94
|
const moment = new ReadHashMap(momentCursor!);
|
|
94
95
|
|
|
95
96
|
// we can read the value of "foo" from the map by getting
|
|
96
97
|
// the cursor to "foo" and then calling readBytes on it
|
|
97
|
-
const fooCursor =
|
|
98
|
-
const fooValue =
|
|
99
|
-
|
|
98
|
+
const fooCursor = moment.getCursor('foo');
|
|
99
|
+
const fooValue = fooCursor!.readBytes(MAX_READ_BYTES);
|
|
100
|
+
assert.strictEqual(new TextDecoder().decode(fooValue), 'foo');
|
|
100
101
|
|
|
101
102
|
// to get the "fruits" list, we get the cursor to it and
|
|
102
103
|
// then pass it to the ReadArrayList constructor
|
|
103
|
-
const fruitsCursor =
|
|
104
|
+
const fruitsCursor = moment.getCursor('fruits');
|
|
104
105
|
const fruits = new ReadArrayList(fruitsCursor!);
|
|
105
|
-
|
|
106
|
+
assert.strictEqual(fruits.count(), 3);
|
|
106
107
|
|
|
107
108
|
// now we can get the first item from the fruits list and read it
|
|
108
|
-
const appleCursor =
|
|
109
|
-
const appleValue =
|
|
110
|
-
|
|
109
|
+
const appleCursor = fruits.getCursor(0);
|
|
110
|
+
const appleValue = appleCursor!.readBytes(MAX_READ_BYTES);
|
|
111
|
+
assert.strictEqual(new TextDecoder().decode(appleValue), 'apple');
|
|
111
112
|
```
|
|
112
113
|
|
|
113
114
|
## Initializing a Database
|
|
@@ -131,8 +132,9 @@ In xitdb there are a variety of immutable data structures that you can nest arbi
|
|
|
131
132
|
* `CountedHashMap` and `CountedHashSet` are just a `HashMap` and `HashSet` that maintain a count of their contents
|
|
132
133
|
* `ArrayList` is a growable array
|
|
133
134
|
* `LinkedArrayList` is like an `ArrayList` that can also be efficiently sliced and concatenated
|
|
135
|
+
* `SortedMap` and `SortedSet` are like a `HashMap` and `HashSet` where the keys are byte arrays kept in lexicographic order
|
|
134
136
|
|
|
135
|
-
|
|
137
|
+
The `Hash`-based data structures and the `Arraylist` use the hash array mapped trie, invented by Phil Bagwell (originally made immutable and widely available by Rich Hickey in Clojure). The `LinkedArrayList`, `SortedMap`, and `SortedSet` are based on a B-tree.
|
|
136
138
|
|
|
137
139
|
There are also scalar types you can store in the above-mentioned data structures:
|
|
138
140
|
|
|
@@ -148,15 +150,15 @@ In xitdb, you can optionally store a format tag with a byte array. A format tag
|
|
|
148
150
|
```typescript
|
|
149
151
|
const randomBytes = new Uint8Array(32);
|
|
150
152
|
crypto.getRandomValues(randomBytes);
|
|
151
|
-
|
|
153
|
+
moment.put('random-number', new Bytes(randomBytes, new TextEncoder().encode('bi')));
|
|
152
154
|
```
|
|
153
155
|
|
|
154
156
|
Then, you can read it like this:
|
|
155
157
|
|
|
156
158
|
```typescript
|
|
157
|
-
const randomNumberCursor =
|
|
158
|
-
const randomNumber =
|
|
159
|
-
|
|
159
|
+
const randomNumberCursor = moment.getCursor('random-number');
|
|
160
|
+
const randomNumber = randomNumberCursor!.readBytesObject(MAX_READ_BYTES);
|
|
161
|
+
assert.strictEqual(new TextDecoder().decode(randomNumber.formatTag!), 'bi');
|
|
160
162
|
const randomBigInt = randomNumber.value;
|
|
161
163
|
```
|
|
162
164
|
|
|
@@ -167,76 +169,76 @@ There are many types you may want to store this way. Maybe an ISO-8601 date like
|
|
|
167
169
|
A powerful feature of immutable data is fast cloning. Any data structure can be instantly cloned and changed without affecting the original. Starting with the example code above, we can make a new transaction that creates a "food" list based on the existing "fruits" list:
|
|
168
170
|
|
|
169
171
|
```typescript
|
|
170
|
-
|
|
171
|
-
const moment =
|
|
172
|
+
history.appendContext(history.getSlot(-1), (cursor) => {
|
|
173
|
+
const moment = new WriteHashMap(cursor);
|
|
172
174
|
|
|
173
|
-
const fruitsCursor =
|
|
175
|
+
const fruitsCursor = moment.getCursor('fruits');
|
|
174
176
|
const fruits = new ReadArrayList(fruitsCursor!);
|
|
175
177
|
|
|
176
178
|
// create a new key called "food" whose initial value is
|
|
177
179
|
// based on the "fruits" list
|
|
178
|
-
const foodCursor =
|
|
179
|
-
|
|
180
|
+
const foodCursor = moment.putCursor('food');
|
|
181
|
+
foodCursor.write(fruits.slot());
|
|
180
182
|
|
|
181
|
-
const food =
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
const food = new WriteArrayList(foodCursor);
|
|
184
|
+
food.append(new Bytes('eggs'));
|
|
185
|
+
food.append(new Bytes('rice'));
|
|
186
|
+
food.append(new Bytes('fish'));
|
|
185
187
|
});
|
|
186
188
|
|
|
187
|
-
const momentCursor =
|
|
189
|
+
const momentCursor = history.getCursor(-1);
|
|
188
190
|
const moment = new ReadHashMap(momentCursor!);
|
|
189
191
|
|
|
190
192
|
// the food list includes the fruits
|
|
191
|
-
const foodCursor =
|
|
193
|
+
const foodCursor = moment.getCursor('food');
|
|
192
194
|
const food = new ReadArrayList(foodCursor!);
|
|
193
|
-
|
|
195
|
+
assert.strictEqual(food.count(), 6);
|
|
194
196
|
|
|
195
197
|
// ...but the fruits list hasn't been changed
|
|
196
|
-
const fruitsCursor =
|
|
198
|
+
const fruitsCursor = moment.getCursor('fruits');
|
|
197
199
|
const fruits = new ReadArrayList(fruitsCursor!);
|
|
198
|
-
|
|
200
|
+
assert.strictEqual(fruits.count(), 3);
|
|
199
201
|
```
|
|
200
202
|
|
|
201
203
|
Before we continue, let's save the latest history index, so we can revert back to this moment of the database later:
|
|
202
204
|
|
|
203
205
|
```typescript
|
|
204
|
-
const historyIndex =
|
|
206
|
+
const historyIndex = history.count() - 1;
|
|
205
207
|
```
|
|
206
208
|
|
|
207
209
|
There's one catch you'll run into when cloning. If we try cloning a data structure that was created in the same transaction, it doesn't seem to work:
|
|
208
210
|
|
|
209
211
|
```typescript
|
|
210
|
-
|
|
211
|
-
const moment =
|
|
212
|
+
history.appendContext(history.getSlot(-1), (cursor) => {
|
|
213
|
+
const moment = new WriteHashMap(cursor);
|
|
212
214
|
|
|
213
|
-
const bigCitiesCursor =
|
|
214
|
-
const bigCities =
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
const bigCitiesCursor = moment.putCursor('big-cities');
|
|
216
|
+
const bigCities = new WriteArrayList(bigCitiesCursor);
|
|
217
|
+
bigCities.append(new Bytes('New York, NY'));
|
|
218
|
+
bigCities.append(new Bytes('Los Angeles, CA'));
|
|
217
219
|
|
|
218
220
|
// create a new key called "cities" whose initial value is
|
|
219
221
|
// based on the "big-cities" list
|
|
220
|
-
const citiesCursor =
|
|
221
|
-
|
|
222
|
+
const citiesCursor = moment.putCursor('cities');
|
|
223
|
+
citiesCursor.write(bigCities.slot());
|
|
222
224
|
|
|
223
|
-
const cities =
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
const cities = new WriteArrayList(citiesCursor);
|
|
226
|
+
cities.append(new Bytes('Charleston, SC'));
|
|
227
|
+
cities.append(new Bytes('Louisville, KY'));
|
|
226
228
|
});
|
|
227
229
|
|
|
228
|
-
const momentCursor =
|
|
230
|
+
const momentCursor = history.getCursor(-1);
|
|
229
231
|
const moment = new ReadHashMap(momentCursor!);
|
|
230
232
|
|
|
231
233
|
// the cities list contains all four
|
|
232
|
-
const citiesCursor =
|
|
234
|
+
const citiesCursor = moment.getCursor('cities');
|
|
233
235
|
const cities = new ReadArrayList(citiesCursor!);
|
|
234
|
-
|
|
236
|
+
assert.strictEqual(cities.count(), 4);
|
|
235
237
|
|
|
236
238
|
// ..but so does big-cities! we did not intend to mutate this
|
|
237
|
-
const bigCitiesCursor =
|
|
239
|
+
const bigCitiesCursor = moment.getCursor('big-cities');
|
|
238
240
|
const bigCities = new ReadArrayList(bigCitiesCursor!);
|
|
239
|
-
|
|
241
|
+
assert.strictEqual(bigCities.count(), 4);
|
|
240
242
|
```
|
|
241
243
|
|
|
242
244
|
The reason that `big-cities` was mutated is because all data in a given transaction is temporarily mutable. This is a very important optimization, but in this case, it's not what we want.
|
|
@@ -244,45 +246,45 @@ The reason that `big-cities` was mutated is because all data in a given transact
|
|
|
244
246
|
To show how to fix this, let's first undo the transaction we just made. Here we use the `historyIndex` we saved before to revert back to the older database moment:
|
|
245
247
|
|
|
246
248
|
```typescript
|
|
247
|
-
|
|
249
|
+
history.append(history.getSlot(historyIndex)!);
|
|
248
250
|
```
|
|
249
251
|
|
|
250
252
|
This time, after making the "big cities" list, we call `freeze`, which tells xitdb to consider all data made so far in the transaction to be immutable. After that, we can clone it into the "cities" list and it will work the way we wanted:
|
|
251
253
|
|
|
252
254
|
```typescript
|
|
253
|
-
|
|
254
|
-
const moment =
|
|
255
|
+
history.appendContext(history.getSlot(-1), (cursor) => {
|
|
256
|
+
const moment = new WriteHashMap(cursor);
|
|
255
257
|
|
|
256
|
-
const bigCitiesCursor =
|
|
257
|
-
const bigCities =
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
const bigCitiesCursor = moment.putCursor('big-cities');
|
|
259
|
+
const bigCities = new WriteArrayList(bigCitiesCursor);
|
|
260
|
+
bigCities.append(new Bytes('New York, NY'));
|
|
261
|
+
bigCities.append(new Bytes('Los Angeles, CA'));
|
|
260
262
|
|
|
261
263
|
// freeze here, so big-cities won't be mutated
|
|
262
264
|
cursor.db.freeze();
|
|
263
265
|
|
|
264
266
|
// create a new key called "cities" whose initial value is
|
|
265
267
|
// based on the "big-cities" list
|
|
266
|
-
const citiesCursor =
|
|
267
|
-
|
|
268
|
+
const citiesCursor = moment.putCursor('cities');
|
|
269
|
+
citiesCursor.write(bigCities.slot());
|
|
268
270
|
|
|
269
|
-
const cities =
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
const cities = new WriteArrayList(citiesCursor);
|
|
272
|
+
cities.append(new Bytes('Charleston, SC'));
|
|
273
|
+
cities.append(new Bytes('Louisville, KY'));
|
|
272
274
|
});
|
|
273
275
|
|
|
274
|
-
const momentCursor =
|
|
276
|
+
const momentCursor = history.getCursor(-1);
|
|
275
277
|
const moment = new ReadHashMap(momentCursor!);
|
|
276
278
|
|
|
277
279
|
// the cities list contains all four
|
|
278
|
-
const citiesCursor =
|
|
280
|
+
const citiesCursor = moment.getCursor('cities');
|
|
279
281
|
const cities = new ReadArrayList(citiesCursor!);
|
|
280
|
-
|
|
282
|
+
assert.strictEqual(cities.count(), 4);
|
|
281
283
|
|
|
282
284
|
// and big-cities only contains the original two
|
|
283
|
-
const bigCitiesCursor =
|
|
285
|
+
const bigCitiesCursor = moment.getCursor('big-cities');
|
|
284
286
|
const bigCities = new ReadArrayList(bigCitiesCursor!);
|
|
285
|
-
|
|
287
|
+
assert.strictEqual(bigCities.count(), 2);
|
|
286
288
|
```
|
|
287
289
|
|
|
288
290
|
## Large Byte Arrays
|
|
@@ -290,12 +292,12 @@ expect(await bigCities.count()).toBe(2);
|
|
|
290
292
|
When reading and writing large byte arrays, you probably don't want to have all of their contents in memory at once. To incrementally write to a byte array, just get a writer from a cursor:
|
|
291
293
|
|
|
292
294
|
```typescript
|
|
293
|
-
const longTextCursor =
|
|
294
|
-
const cursorWriter =
|
|
295
|
+
const longTextCursor = moment.putCursor('long-text');
|
|
296
|
+
const cursorWriter = longTextCursor.writer();
|
|
295
297
|
for (let i = 0; i < 50; i++) {
|
|
296
|
-
|
|
298
|
+
cursorWriter.write(new TextEncoder().encode('hello, world\n'));
|
|
297
299
|
}
|
|
298
|
-
|
|
300
|
+
cursorWriter.finish(); // remember to call this!
|
|
299
301
|
```
|
|
300
302
|
|
|
301
303
|
If you need to set a format tag for the byte array, put it in the `formatTag` field of the writer before you call `finish`.
|
|
@@ -303,18 +305,18 @@ If you need to set a format tag for the byte array, put it in the `formatTag` fi
|
|
|
303
305
|
To read a byte array incrementally, get a reader from a cursor:
|
|
304
306
|
|
|
305
307
|
```typescript
|
|
306
|
-
const longTextCursor =
|
|
307
|
-
const cursorReader =
|
|
308
|
+
const longTextCursor = moment.getCursor('long-text');
|
|
309
|
+
const cursorReader = longTextCursor!.reader();
|
|
308
310
|
let lineCount = 0, line: number[] = [];
|
|
309
311
|
const buf = new Uint8Array(1024);
|
|
310
|
-
for (let n; (n =
|
|
312
|
+
for (let n; (n = cursorReader.read(buf)) > 0; ) {
|
|
311
313
|
for (let i = 0; i < n; i++) {
|
|
312
314
|
if (buf[i] === 0x0A) { lineCount++; line = []; }
|
|
313
315
|
else line.push(buf[i]);
|
|
314
316
|
}
|
|
315
317
|
}
|
|
316
318
|
if (line.length > 0) lineCount++;
|
|
317
|
-
|
|
319
|
+
assert.strictEqual(lineCount, 50);
|
|
318
320
|
```
|
|
319
321
|
|
|
320
322
|
## Iterators
|
|
@@ -322,24 +324,24 @@ expect(lineCount).toBe(50);
|
|
|
322
324
|
All data structures support iteration. Here's an example of iterating over an `ArrayList` and printing all of the keys and values of each `HashMap` contained in it:
|
|
323
325
|
|
|
324
326
|
```typescript
|
|
325
|
-
const peopleCursor =
|
|
327
|
+
const peopleCursor = moment.getCursor('people');
|
|
326
328
|
const people = new ReadArrayList(peopleCursor!);
|
|
327
329
|
|
|
328
|
-
const peopleIter =
|
|
329
|
-
while (
|
|
330
|
-
const personCursor =
|
|
330
|
+
const peopleIter = people.iterator();
|
|
331
|
+
while (peopleIter.hasNext()) {
|
|
332
|
+
const personCursor = peopleIter.next();
|
|
331
333
|
const person = new ReadHashMap(personCursor!);
|
|
332
|
-
const personIter =
|
|
333
|
-
while (
|
|
334
|
-
const kvPairCursor =
|
|
335
|
-
const kvPair =
|
|
334
|
+
const personIter = person.iterator();
|
|
335
|
+
while (personIter.hasNext()) {
|
|
336
|
+
const kvPairCursor = personIter.next();
|
|
337
|
+
const kvPair = kvPairCursor!.readKeyValuePair();
|
|
336
338
|
|
|
337
|
-
const key = new TextDecoder().decode(
|
|
339
|
+
const key = new TextDecoder().decode(kvPair.keyCursor.readBytes(MAX_READ_BYTES));
|
|
338
340
|
|
|
339
341
|
switch (kvPair.valueCursor.slot().tag) {
|
|
340
342
|
case Tag.SHORT_BYTES:
|
|
341
343
|
case Tag.BYTES:
|
|
342
|
-
console.log(`${key}: ${new TextDecoder().decode(
|
|
344
|
+
console.log(`${key}: ${new TextDecoder().decode(kvPair.valueCursor.readBytes(MAX_READ_BYTES))}`);
|
|
343
345
|
break;
|
|
344
346
|
case Tag.UINT:
|
|
345
347
|
console.log(`${key}: ${kvPair.valueCursor.readUint()}`);
|
|
@@ -366,17 +368,17 @@ The hashing data structures will create the hash for you when you call methods l
|
|
|
366
368
|
When initializing a database, you tell xitdb how to hash with the `Hasher`. If you're using SHA-1, it will look like this:
|
|
367
369
|
|
|
368
370
|
```typescript
|
|
369
|
-
using core =
|
|
371
|
+
using core = new CoreBufferedFile('main.db');
|
|
370
372
|
const hasher = new Hasher('SHA-1');
|
|
371
|
-
const db =
|
|
373
|
+
const db = new Database(core, hasher);
|
|
372
374
|
```
|
|
373
375
|
|
|
374
376
|
The size of the hash in bytes will be stored in the database's header. If you try opening it later with a hashing algorithm that has the wrong hash size, it will throw an exception. If you are unsure what hash size the database uses, this creates a chicken-and-egg problem. You can read the header before initializing the database like this:
|
|
375
377
|
|
|
376
378
|
```typescript
|
|
377
|
-
|
|
378
|
-
const header =
|
|
379
|
-
|
|
379
|
+
core.seek(0);
|
|
380
|
+
const header = Header.read(core);
|
|
381
|
+
assert.strictEqual(header.hashSize, 20);
|
|
380
382
|
```
|
|
381
383
|
|
|
382
384
|
The hash size alone does not disambiguate hashing algorithms, though. In addition, xitdb reserves four bytes in the header that you can use to put the name of the algorithm. You must provide it in the `Hasher` constructor:
|
|
@@ -388,9 +390,9 @@ const hasher = new Hasher('SHA-1', Hasher.stringToId('sha1'));
|
|
|
388
390
|
The hash id is only written to the database header when it is first initialized. When you open it later, the hash id in the `Hasher` is ignored. You can read the hash id of an existing database like this:
|
|
389
391
|
|
|
390
392
|
```typescript
|
|
391
|
-
|
|
392
|
-
const header =
|
|
393
|
-
|
|
393
|
+
core.seek(0);
|
|
394
|
+
const header = Header.read(core);
|
|
395
|
+
assert.strictEqual(Hasher.idToString(header.hashId), "sha1");
|
|
394
396
|
```
|
|
395
397
|
|
|
396
398
|
If you want to use SHA-256, I recommend using `sha2` as the hash id. You can then distinguish between SHA-256 and SHA-512 using the hash size, like this:
|
|
@@ -425,12 +427,12 @@ switch (hashIdStr) {
|
|
|
425
427
|
Normally, an immutable database grows forever, because old data is never deleted. To reclaim disk space and clear the history, xitdb supports compaction. This involves completely rebuilding the database file to only contain the data accessible from the latest copy (i.e., "moment") of the database.
|
|
426
428
|
|
|
427
429
|
```typescript
|
|
428
|
-
using compactCore =
|
|
429
|
-
const compactDb =
|
|
430
|
+
using compactCore = new CoreBufferedFile('compact.db');
|
|
431
|
+
const compactDb = db.compact(compactCore);
|
|
430
432
|
|
|
431
433
|
// read from the new compacted db
|
|
432
434
|
const history = new ReadArrayList(compactDb.rootCursor());
|
|
433
|
-
|
|
435
|
+
assert.strictEqual(history.count(), 1);
|
|
434
436
|
```
|
|
435
437
|
|
|
436
438
|
This compacted database will be in a separate file. If you want to delete the original database and replace it with this one, you'll need to do that yourself. It is not possible to compact a database in-place (using the same file as the target database); doing so would fail and would render your original database unreadable.
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import type { Core, DataReader, DataWriter } from './core';
|
|
2
|
-
import { CoreFile } from './core-file';
|
|
1
|
+
import type { Core, DataReader, DataWriter } from './core.js';
|
|
2
|
+
import { CoreFile } from './core-file.js';
|
|
3
3
|
export declare class CoreBufferedFile implements Core {
|
|
4
4
|
file: RandomAccessBufferedFile;
|
|
5
|
-
constructor(
|
|
6
|
-
static create(filePath: string, bufferSize?: number): Promise<CoreBufferedFile>;
|
|
5
|
+
constructor(filePath: string, bufferSize?: number);
|
|
7
6
|
reader(): DataReader;
|
|
8
7
|
writer(): DataWriter;
|
|
9
|
-
length():
|
|
10
|
-
seek(pos: number):
|
|
8
|
+
length(): number;
|
|
9
|
+
seek(pos: number): void;
|
|
11
10
|
position(): number;
|
|
12
|
-
setLength(len: number):
|
|
13
|
-
flush():
|
|
14
|
-
sync():
|
|
11
|
+
setLength(len: number): void;
|
|
12
|
+
flush(): void;
|
|
13
|
+
sync(): void;
|
|
15
14
|
[Symbol.dispose](): void;
|
|
16
15
|
}
|
|
17
16
|
declare class RandomAccessBufferedFile implements DataReader, DataWriter {
|
|
@@ -20,22 +19,21 @@ declare class RandomAccessBufferedFile implements DataReader, DataWriter {
|
|
|
20
19
|
private bufferSize;
|
|
21
20
|
private filePos;
|
|
22
21
|
private memoryPos;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
length(): Promise<number>;
|
|
22
|
+
constructor(filePath: string, bufferSize?: number);
|
|
23
|
+
seek(pos: number): void;
|
|
24
|
+
length(): number;
|
|
27
25
|
position(): number;
|
|
28
|
-
setLength(len: number):
|
|
29
|
-
flush():
|
|
30
|
-
sync():
|
|
31
|
-
write(buffer: Uint8Array):
|
|
32
|
-
writeByte(v: number):
|
|
33
|
-
writeShort(v: number):
|
|
34
|
-
writeLong(v: number):
|
|
35
|
-
readFully(buffer: Uint8Array):
|
|
36
|
-
readByte():
|
|
37
|
-
readShort():
|
|
38
|
-
readInt():
|
|
39
|
-
readLong():
|
|
26
|
+
setLength(len: number): void;
|
|
27
|
+
flush(): void;
|
|
28
|
+
sync(): void;
|
|
29
|
+
write(buffer: Uint8Array): void;
|
|
30
|
+
writeByte(v: number): void;
|
|
31
|
+
writeShort(v: number): void;
|
|
32
|
+
writeLong(v: number): void;
|
|
33
|
+
readFully(buffer: Uint8Array): void;
|
|
34
|
+
readByte(): number;
|
|
35
|
+
readShort(): number;
|
|
36
|
+
readInt(): number;
|
|
37
|
+
readLong(): number;
|
|
40
38
|
}
|
|
41
39
|
export {};
|