xitdb 0.13.0 → 0.15.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 +129 -15
- package/dist/core-buffered-file.d.ts +2 -2
- package/dist/core-buffered-file.js +182 -0
- package/dist/core-file.d.ts +1 -1
- package/dist/core-file.js +105 -0
- package/dist/core-memory.d.ts +1 -1
- package/dist/core-memory.js +152 -0
- package/dist/core.js +1 -0
- package/dist/database.d.ts +174 -49
- package/dist/database.js +3047 -0
- package/dist/exceptions.d.ts +4 -0
- package/dist/exceptions.js +62 -0
- package/dist/hasher.js +49 -0
- package/dist/index.d.ts +30 -26
- package/dist/index.js +35 -3923
- package/dist/read-array-list.d.ts +4 -3
- package/dist/read-array-list.js +41 -0
- package/dist/read-counted-hash-map.d.ts +2 -2
- package/dist/read-counted-hash-map.js +19 -0
- package/dist/read-counted-hash-set.d.ts +2 -2
- package/dist/read-counted-hash-set.js +19 -0
- package/dist/read-cursor.d.ts +16 -5
- package/dist/read-cursor.js +678 -0
- package/dist/read-hash-map.d.ts +4 -4
- package/dist/read-hash-map.js +65 -0
- package/dist/read-hash-set.d.ts +4 -4
- package/dist/read-hash-set.js +47 -0
- package/dist/read-linked-array-list.d.ts +4 -3
- package/dist/read-linked-array-list.js +41 -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 +4 -4
- package/dist/write-array-list.js +42 -0
- package/dist/write-counted-hash-map.d.ts +2 -2
- package/dist/write-counted-hash-map.js +18 -0
- package/dist/write-counted-hash-set.d.ts +2 -2
- package/dist/write-counted-hash-set.js +18 -0
- package/dist/write-cursor.d.ts +5 -5
- package/dist/write-cursor.js +124 -0
- package/dist/write-hash-map.d.ts +4 -4
- package/dist/write-hash-map.js +90 -0
- package/dist/write-hash-set.d.ts +4 -4
- package/dist/write-hash-set.js +59 -0
- package/dist/write-linked-array-list.d.ts +4 -4
- 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
|
@@ -26,6 +26,7 @@ This database was originally made for the [xit version control system](https://g
|
|
|
26
26
|
* [Initializing a Database](#initializing-a-database)
|
|
27
27
|
* [Types](#types)
|
|
28
28
|
* [Cloning and Undoing](#cloning-and-undoing)
|
|
29
|
+
* [Sorting and Paginating](#sorting-and-paginating)
|
|
29
30
|
* [Large Byte Arrays](#large-byte-arrays)
|
|
30
31
|
* [Iterators](#iterators)
|
|
31
32
|
* [Hashing](#hashing)
|
|
@@ -97,18 +98,18 @@ const moment = new ReadHashMap(momentCursor!);
|
|
|
97
98
|
// the cursor to "foo" and then calling readBytes on it
|
|
98
99
|
const fooCursor = moment.getCursor('foo');
|
|
99
100
|
const fooValue = fooCursor!.readBytes(MAX_READ_BYTES);
|
|
100
|
-
|
|
101
|
+
assert.strictEqual(new TextDecoder().decode(fooValue), 'foo');
|
|
101
102
|
|
|
102
103
|
// to get the "fruits" list, we get the cursor to it and
|
|
103
104
|
// then pass it to the ReadArrayList constructor
|
|
104
105
|
const fruitsCursor = moment.getCursor('fruits');
|
|
105
106
|
const fruits = new ReadArrayList(fruitsCursor!);
|
|
106
|
-
|
|
107
|
+
assert.strictEqual(fruits.count(), 3);
|
|
107
108
|
|
|
108
109
|
// now we can get the first item from the fruits list and read it
|
|
109
110
|
const appleCursor = fruits.getCursor(0);
|
|
110
111
|
const appleValue = appleCursor!.readBytes(MAX_READ_BYTES);
|
|
111
|
-
|
|
112
|
+
assert.strictEqual(new TextDecoder().decode(appleValue), 'apple');
|
|
112
113
|
```
|
|
113
114
|
|
|
114
115
|
## Initializing a Database
|
|
@@ -132,8 +133,9 @@ In xitdb there are a variety of immutable data structures that you can nest arbi
|
|
|
132
133
|
* `CountedHashMap` and `CountedHashSet` are just a `HashMap` and `HashSet` that maintain a count of their contents
|
|
133
134
|
* `ArrayList` is a growable array
|
|
134
135
|
* `LinkedArrayList` is like an `ArrayList` that can also be efficiently sliced and concatenated
|
|
136
|
+
* `SortedMap` and `SortedSet` are like a `HashMap` and `HashSet` where the keys are byte arrays kept in lexicographic order
|
|
135
137
|
|
|
136
|
-
|
|
138
|
+
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.
|
|
137
139
|
|
|
138
140
|
There are also scalar types you can store in the above-mentioned data structures:
|
|
139
141
|
|
|
@@ -157,7 +159,7 @@ Then, you can read it like this:
|
|
|
157
159
|
```typescript
|
|
158
160
|
const randomNumberCursor = moment.getCursor('random-number');
|
|
159
161
|
const randomNumber = randomNumberCursor!.readBytesObject(MAX_READ_BYTES);
|
|
160
|
-
|
|
162
|
+
assert.strictEqual(new TextDecoder().decode(randomNumber.formatTag!), 'bi');
|
|
161
163
|
const randomBigInt = randomNumber.value;
|
|
162
164
|
```
|
|
163
165
|
|
|
@@ -191,12 +193,12 @@ const moment = new ReadHashMap(momentCursor!);
|
|
|
191
193
|
// the food list includes the fruits
|
|
192
194
|
const foodCursor = moment.getCursor('food');
|
|
193
195
|
const food = new ReadArrayList(foodCursor!);
|
|
194
|
-
|
|
196
|
+
assert.strictEqual(food.count(), 6);
|
|
195
197
|
|
|
196
198
|
// ...but the fruits list hasn't been changed
|
|
197
199
|
const fruitsCursor = moment.getCursor('fruits');
|
|
198
200
|
const fruits = new ReadArrayList(fruitsCursor!);
|
|
199
|
-
|
|
201
|
+
assert.strictEqual(fruits.count(), 3);
|
|
200
202
|
```
|
|
201
203
|
|
|
202
204
|
Before we continue, let's save the latest history index, so we can revert back to this moment of the database later:
|
|
@@ -232,12 +234,12 @@ const moment = new ReadHashMap(momentCursor!);
|
|
|
232
234
|
// the cities list contains all four
|
|
233
235
|
const citiesCursor = moment.getCursor('cities');
|
|
234
236
|
const cities = new ReadArrayList(citiesCursor!);
|
|
235
|
-
|
|
237
|
+
assert.strictEqual(cities.count(), 4);
|
|
236
238
|
|
|
237
239
|
// ..but so does big-cities! we did not intend to mutate this
|
|
238
240
|
const bigCitiesCursor = moment.getCursor('big-cities');
|
|
239
241
|
const bigCities = new ReadArrayList(bigCitiesCursor!);
|
|
240
|
-
|
|
242
|
+
assert.strictEqual(bigCities.count(), 4);
|
|
241
243
|
```
|
|
242
244
|
|
|
243
245
|
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.
|
|
@@ -278,14 +280,124 @@ const moment = new ReadHashMap(momentCursor!);
|
|
|
278
280
|
// the cities list contains all four
|
|
279
281
|
const citiesCursor = moment.getCursor('cities');
|
|
280
282
|
const cities = new ReadArrayList(citiesCursor!);
|
|
281
|
-
|
|
283
|
+
assert.strictEqual(cities.count(), 4);
|
|
282
284
|
|
|
283
285
|
// and big-cities only contains the original two
|
|
284
286
|
const bigCitiesCursor = moment.getCursor('big-cities');
|
|
285
287
|
const bigCities = new ReadArrayList(bigCitiesCursor!);
|
|
286
|
-
|
|
288
|
+
assert.strictEqual(bigCities.count(), 2);
|
|
287
289
|
```
|
|
288
290
|
|
|
291
|
+
## Sorting and Paginating
|
|
292
|
+
|
|
293
|
+
The `Hash`-based structures are great for looking data up by key, but they store their contents in hash order, which is meaningless to a human. You may need to display data in a sensible order (like newest posts first or users by signup date) and show it one page at a time. Relational databases like SQLite have this built-in: you declare a `CREATE INDEX`, write `ORDER BY created_ts LIMIT 20 OFFSET 40`, and the query planner maintains the index and seeks into it for you.
|
|
294
|
+
|
|
295
|
+
In xitdb there are no built-in indexes, so you build and maintain them yourself. That's a little more code, but the index is just another data structure: a `SortedMap` whose keys are crafted to sort the way you want. You keep it in sync by writing to it in the same transaction that writes the primary data.
|
|
296
|
+
|
|
297
|
+
Let's model the storage a basic blog would need: a collection of posts we look up by id, plus a secondary index that lets us list them oldest-first with pagination. The primary store is a `HashMap` from post id to the post's fields (like a row keyed by its primary key). The secondary index is a `SortedMap` keyed by creation time, whose value is the post id to look up.
|
|
298
|
+
|
|
299
|
+
The trick is the key. A `SortedMap` orders its keys lexicographically by their raw bytes, so we encode the timestamp as a big-endian integer (so byte order matches chronological order) and append the post id to break ties between posts created in the same second and keep every key unique:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// build a SortedMap key that sorts by creation time. the big-endian
|
|
303
|
+
// timestamp makes byte order match chronological order; the post id is
|
|
304
|
+
// appended so two posts with the same timestamp still get distinct keys.
|
|
305
|
+
function orderKey(timestamp: number, postId: Uint8Array): Uint8Array {
|
|
306
|
+
const key = new Uint8Array(8 + postId.length);
|
|
307
|
+
new DataView(key.buffer).setBigUint64(0, BigInt(timestamp), false); // false = big-endian
|
|
308
|
+
key.set(postId, 8);
|
|
309
|
+
return key;
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Now we write some posts. On each insert we write the post into the primary map and add an entry to the secondary index (keeping both in sync is your job, not the database's):
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
interface Post {
|
|
317
|
+
id: string;
|
|
318
|
+
title: string;
|
|
319
|
+
createdTs: number;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// post ids are fixed-length so the timestamp tie-breaker stays aligned
|
|
323
|
+
const newPosts: Post[] = [
|
|
324
|
+
{ id: 'post000000000001', title: 'Hello, world', createdTs: 1_700_000_000 },
|
|
325
|
+
{ id: 'post000000000002', title: 'Second post', createdTs: 1_700_000_500 },
|
|
326
|
+
{ id: 'post000000000003', title: 'Third post', createdTs: 1_700_001_000 },
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
history.appendContext(history.getSlot(-1), (cursor) => {
|
|
330
|
+
const moment = new WriteHashMap(cursor);
|
|
331
|
+
|
|
332
|
+
// the primary store: a HashMap from post id to the post's fields
|
|
333
|
+
const idToPostCursor = moment.putCursor('id->post');
|
|
334
|
+
const idToPost = new WriteHashMap(idToPostCursor);
|
|
335
|
+
|
|
336
|
+
// the secondary index: a SortedMap ordered by creation time. there's
|
|
337
|
+
// no CREATE INDEX here, so we maintain it ourselves on every write.
|
|
338
|
+
const createdTsToPostIdCursor = moment.putCursor('created-ts->post-id');
|
|
339
|
+
const createdTsToPostId = new WriteSortedMap(createdTsToPostIdCursor);
|
|
340
|
+
|
|
341
|
+
for (const post of newPosts) {
|
|
342
|
+
// write the post into the primary map under its id
|
|
343
|
+
const postCursor = idToPost.putCursor(post.id);
|
|
344
|
+
const postMap = new WriteHashMap(postCursor);
|
|
345
|
+
postMap.put('title', new Bytes(post.title));
|
|
346
|
+
postMap.put('created-ts', new Uint(post.createdTs));
|
|
347
|
+
|
|
348
|
+
// add an entry to the secondary index. the key sorts by time,
|
|
349
|
+
// and the value is the post id we'll use to look the post back up.
|
|
350
|
+
const orderKeyBytes = orderKey(post.createdTs, new TextEncoder().encode(post.id));
|
|
351
|
+
createdTsToPostId.put(orderKeyBytes, new Bytes(post.id));
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
To display a page, we walk the `SortedMap` instead of the `HashMap`. A web app would take a `pageSize` and an `after` offset from the request (something like `/posts?after=20`), so this is the xitdb equivalent of `ORDER BY created_ts LIMIT pageSize OFFSET after`:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
const momentCursor = history.getCursor(-1);
|
|
360
|
+
const moment = new ReadHashMap(momentCursor!);
|
|
361
|
+
|
|
362
|
+
const idToPostCursor = moment.getCursor('id->post');
|
|
363
|
+
const idToPost = new ReadHashMap(idToPostCursor!);
|
|
364
|
+
|
|
365
|
+
const createdTsToPostIdCursor = moment.getCursor('created-ts->post-id');
|
|
366
|
+
const createdTsToPostId = new ReadSortedMap(createdTsToPostIdCursor!);
|
|
367
|
+
|
|
368
|
+
// a web request would supply these; here we just grab the first page
|
|
369
|
+
const pageSize = 2;
|
|
370
|
+
const after = 0;
|
|
371
|
+
|
|
372
|
+
const count = createdTsToPostId.count();
|
|
373
|
+
const end = Math.min(after + pageSize, count);
|
|
374
|
+
|
|
375
|
+
// seek straight to the start of the page, then walk forward one entry at a
|
|
376
|
+
// time. because SortedMap is a count-augmented B+tree, iteratorFromIndex
|
|
377
|
+
// finds rank `after` in O(log n) without scanning the entries it skips, so
|
|
378
|
+
// jumping to page 500 is just as cheap as page 1.
|
|
379
|
+
const decoder = new TextDecoder();
|
|
380
|
+
const iter = createdTsToPostId.iteratorFromIndex(after);
|
|
381
|
+
for (let i = after; i < end && iter.hasNext(); i++) {
|
|
382
|
+
const idCursor = iter.next()!;
|
|
383
|
+
const idKv = idCursor.readKeyValuePair();
|
|
384
|
+
|
|
385
|
+
// the index entry's value is the post id; use it to read the
|
|
386
|
+
// full post out of the primary map
|
|
387
|
+
const postId = decoder.decode(idKv.valueCursor.readBytes(MAX_READ_BYTES));
|
|
388
|
+
|
|
389
|
+
const postCursor = idToPost.getCursor(postId);
|
|
390
|
+
const postMap = new ReadHashMap(postCursor!);
|
|
391
|
+
const titleCursor = postMap.getCursor('title');
|
|
392
|
+
const title = decoder.decode(titleCursor!.readBytes(MAX_READ_BYTES));
|
|
393
|
+
|
|
394
|
+
// a real app would render this into the page's HTML
|
|
395
|
+
console.log(title);
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
This works for any ordering you need: sort by a username with a string key, by score with a big-endian integer key, or build several `SortedMap` indexes over the same primary `HashMap` to offer the data in different orders. With xitdb you "bring your own index". It takes a bit more effort than the declarative convenience of SQL databases, but it gives you more explicit control, and avoids the common problem in SQL where queries silently become inefficient due to not using indexes. In xitdb, inefficiency is hard to miss because you are always writing your queries as imperative code and the indexes are always explicit.
|
|
400
|
+
|
|
289
401
|
## Large Byte Arrays
|
|
290
402
|
|
|
291
403
|
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:
|
|
@@ -315,7 +427,7 @@ for (let n; (n = cursorReader.read(buf)) > 0; ) {
|
|
|
315
427
|
}
|
|
316
428
|
}
|
|
317
429
|
if (line.length > 0) lineCount++;
|
|
318
|
-
|
|
430
|
+
assert.strictEqual(lineCount, 50);
|
|
319
431
|
```
|
|
320
432
|
|
|
321
433
|
## Iterators
|
|
@@ -360,6 +472,8 @@ The above code iterates over `people`, which is an `ArrayList`, and for each per
|
|
|
360
472
|
|
|
361
473
|
The iteration of the `HashMap` looks the same with `HashSet`, `CountedHashMap`, and `CountedHashSet`. When iterating, you call `readKeyValuePair` on the cursor and can read the `keyCursor` and `valueCursor` from it. In maps, `put` sets the key and value. In sets, `put` only sets the key; the value will always have a tag type of `NONE`.
|
|
362
474
|
|
|
475
|
+
`ArrayList` and `LinkedArrayList` also have an `iteratorFrom` method, which starts the iterator from the given index. `SortedMap` and `SortedSet` have `iteratorFrom` and `iteratorFromIndex` to start the iterator from a key or index respectively. This is especially useful for pagination: you can seek straight to the start of a page and walk forward only as far as you need. See the [Sorting and Paginating](#sorting-and-paginating) section for an example.
|
|
476
|
+
|
|
363
477
|
## Hashing
|
|
364
478
|
|
|
365
479
|
The hashing data structures will create the hash for you when you call methods like `put` or `getCursor` and provide the key as a string or `Bytes`. If you want to do the hashing yourself, there is an overload of those methods that take a `Uint8Array` as the key, which should be the hash that you computed.
|
|
@@ -377,7 +491,7 @@ The size of the hash in bytes will be stored in the database's header. If you tr
|
|
|
377
491
|
```typescript
|
|
378
492
|
core.seek(0);
|
|
379
493
|
const header = Header.read(core);
|
|
380
|
-
|
|
494
|
+
assert.strictEqual(header.hashSize, 20);
|
|
381
495
|
```
|
|
382
496
|
|
|
383
497
|
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:
|
|
@@ -391,7 +505,7 @@ The hash id is only written to the database header when it is first initialized.
|
|
|
391
505
|
```typescript
|
|
392
506
|
core.seek(0);
|
|
393
507
|
const header = Header.read(core);
|
|
394
|
-
|
|
508
|
+
assert.strictEqual(Hasher.idToString(header.hashId), "sha1");
|
|
395
509
|
```
|
|
396
510
|
|
|
397
511
|
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:
|
|
@@ -431,7 +545,7 @@ const compactDb = db.compact(compactCore);
|
|
|
431
545
|
|
|
432
546
|
// read from the new compacted db
|
|
433
547
|
const history = new ReadArrayList(compactDb.rootCursor());
|
|
434
|
-
|
|
548
|
+
assert.strictEqual(history.count(), 1);
|
|
435
549
|
```
|
|
436
550
|
|
|
437
551
|
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,5 +1,5 @@
|
|
|
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
5
|
constructor(filePath: string, bufferSize?: number);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { CoreFile } from './core-file.js';
|
|
2
|
+
import { CoreMemory } from './core-memory.js';
|
|
3
|
+
export class CoreBufferedFile {
|
|
4
|
+
file;
|
|
5
|
+
constructor(filePath, bufferSize) {
|
|
6
|
+
this.file = new RandomAccessBufferedFile(filePath, bufferSize);
|
|
7
|
+
}
|
|
8
|
+
reader() {
|
|
9
|
+
return this.file;
|
|
10
|
+
}
|
|
11
|
+
writer() {
|
|
12
|
+
return this.file;
|
|
13
|
+
}
|
|
14
|
+
length() {
|
|
15
|
+
return this.file.length();
|
|
16
|
+
}
|
|
17
|
+
seek(pos) {
|
|
18
|
+
this.file.seek(pos);
|
|
19
|
+
}
|
|
20
|
+
position() {
|
|
21
|
+
return this.file.position();
|
|
22
|
+
}
|
|
23
|
+
setLength(len) {
|
|
24
|
+
this.file.setLength(len);
|
|
25
|
+
}
|
|
26
|
+
flush() {
|
|
27
|
+
this.file.flush();
|
|
28
|
+
}
|
|
29
|
+
sync() {
|
|
30
|
+
this.file.sync();
|
|
31
|
+
}
|
|
32
|
+
[Symbol.dispose]() {
|
|
33
|
+
this.file.file[Symbol.dispose]();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const DEFAULT_BUFFER_SIZE = 8 * 1024 * 1024; // 8MB
|
|
37
|
+
class RandomAccessBufferedFile {
|
|
38
|
+
file;
|
|
39
|
+
memory;
|
|
40
|
+
bufferSize; // flushes when the memory is >= this size
|
|
41
|
+
filePos;
|
|
42
|
+
memoryPos;
|
|
43
|
+
constructor(filePath, bufferSize = DEFAULT_BUFFER_SIZE) {
|
|
44
|
+
this.file = new CoreFile(filePath);
|
|
45
|
+
this.memory = new CoreMemory();
|
|
46
|
+
this.bufferSize = bufferSize;
|
|
47
|
+
this.filePos = 0;
|
|
48
|
+
this.memoryPos = 0;
|
|
49
|
+
}
|
|
50
|
+
seek(pos) {
|
|
51
|
+
// flush if we are going past the end of the in-memory buffer
|
|
52
|
+
if (pos > this.memoryPos + this.memory.length()) {
|
|
53
|
+
this.flush();
|
|
54
|
+
}
|
|
55
|
+
this.filePos = pos;
|
|
56
|
+
// if the buffer is empty, set its position to this offset as well
|
|
57
|
+
if (this.memory.length() === 0) {
|
|
58
|
+
this.memoryPos = pos;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
length() {
|
|
62
|
+
return Math.max(this.memoryPos + this.memory.length(), this.file.length());
|
|
63
|
+
}
|
|
64
|
+
position() {
|
|
65
|
+
return this.filePos;
|
|
66
|
+
}
|
|
67
|
+
setLength(len) {
|
|
68
|
+
this.flush();
|
|
69
|
+
this.file.setLength(len);
|
|
70
|
+
this.filePos = Math.min(len, this.filePos);
|
|
71
|
+
}
|
|
72
|
+
flush() {
|
|
73
|
+
if (this.memory.length() > 0) {
|
|
74
|
+
this.file.seek(this.memoryPos);
|
|
75
|
+
this.file.writer().write(this.memory.memory.toByteArray());
|
|
76
|
+
this.memoryPos = 0;
|
|
77
|
+
this.memory.memory.reset();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
sync() {
|
|
81
|
+
this.flush();
|
|
82
|
+
this.file.sync();
|
|
83
|
+
}
|
|
84
|
+
// DataWriter interface
|
|
85
|
+
write(buffer) {
|
|
86
|
+
if (this.memory.length() + buffer.length > this.bufferSize) {
|
|
87
|
+
this.flush();
|
|
88
|
+
}
|
|
89
|
+
if (this.filePos >= this.memoryPos && this.filePos <= this.memoryPos + this.memory.length()) {
|
|
90
|
+
this.memory.seek(this.filePos - this.memoryPos);
|
|
91
|
+
this.memory.memory.write(buffer);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// a direct disk write that overlaps the buffered region would be
|
|
95
|
+
// clobbered by a later flush of stale buffer bytes, so flush first
|
|
96
|
+
if (this.filePos < this.memoryPos + this.memory.length() && this.filePos + buffer.length > this.memoryPos) {
|
|
97
|
+
this.flush();
|
|
98
|
+
}
|
|
99
|
+
// Write directly to file
|
|
100
|
+
this.file.seek(this.filePos);
|
|
101
|
+
this.file.writer().write(buffer);
|
|
102
|
+
}
|
|
103
|
+
this.filePos += buffer.length;
|
|
104
|
+
}
|
|
105
|
+
writeByte(v) {
|
|
106
|
+
this.write(new Uint8Array([v & 0xff]));
|
|
107
|
+
}
|
|
108
|
+
writeShort(v) {
|
|
109
|
+
const buffer = new ArrayBuffer(2);
|
|
110
|
+
const view = new DataView(buffer);
|
|
111
|
+
view.setInt16(0, v & 0xffff, false); // big-endian
|
|
112
|
+
this.write(new Uint8Array(buffer));
|
|
113
|
+
}
|
|
114
|
+
writeLong(v) {
|
|
115
|
+
const buffer = new ArrayBuffer(8);
|
|
116
|
+
const view = new DataView(buffer);
|
|
117
|
+
view.setBigInt64(0, BigInt(v), false);
|
|
118
|
+
this.write(new Uint8Array(buffer));
|
|
119
|
+
}
|
|
120
|
+
// DataReader interface
|
|
121
|
+
readFully(buffer) {
|
|
122
|
+
let pos = 0;
|
|
123
|
+
// read from the disk -- before the in-memory buffer
|
|
124
|
+
if (this.filePos < this.memoryPos) {
|
|
125
|
+
const sizeBeforeMem = Math.min(this.memoryPos - this.filePos, buffer.length);
|
|
126
|
+
const tempBuffer = new Uint8Array(sizeBeforeMem);
|
|
127
|
+
this.file.seek(this.filePos);
|
|
128
|
+
this.file.reader().readFully(tempBuffer);
|
|
129
|
+
buffer.set(tempBuffer, pos);
|
|
130
|
+
pos += sizeBeforeMem;
|
|
131
|
+
this.filePos += sizeBeforeMem;
|
|
132
|
+
}
|
|
133
|
+
if (pos === buffer.length)
|
|
134
|
+
return;
|
|
135
|
+
// read from the in-memory buffer
|
|
136
|
+
if (this.filePos >= this.memoryPos && this.filePos < this.memoryPos + this.memory.length()) {
|
|
137
|
+
const memPos = this.filePos - this.memoryPos;
|
|
138
|
+
const sizeInMem = Math.min(this.memory.length() - memPos, buffer.length - pos);
|
|
139
|
+
this.memory.seek(memPos);
|
|
140
|
+
const memBuffer = new Uint8Array(sizeInMem);
|
|
141
|
+
this.memory.memory.readFully(memBuffer);
|
|
142
|
+
buffer.set(memBuffer, pos);
|
|
143
|
+
pos += sizeInMem;
|
|
144
|
+
this.filePos += sizeInMem;
|
|
145
|
+
}
|
|
146
|
+
if (pos === buffer.length)
|
|
147
|
+
return;
|
|
148
|
+
// read from the disk -- after the in-memory buffer
|
|
149
|
+
if (this.filePos >= this.memoryPos + this.memory.length()) {
|
|
150
|
+
const sizeAfterMem = buffer.length - pos;
|
|
151
|
+
const tempBuffer = new Uint8Array(sizeAfterMem);
|
|
152
|
+
this.file.seek(this.filePos);
|
|
153
|
+
this.file.reader().readFully(tempBuffer);
|
|
154
|
+
buffer.set(tempBuffer, pos);
|
|
155
|
+
pos += sizeAfterMem;
|
|
156
|
+
this.filePos += sizeAfterMem;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
readByte() {
|
|
160
|
+
const bytes = new Uint8Array(1);
|
|
161
|
+
this.readFully(bytes);
|
|
162
|
+
return bytes[0];
|
|
163
|
+
}
|
|
164
|
+
readShort() {
|
|
165
|
+
const bytes = new Uint8Array(2);
|
|
166
|
+
this.readFully(bytes);
|
|
167
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
168
|
+
return view.getInt16(0, false); // big-endian
|
|
169
|
+
}
|
|
170
|
+
readInt() {
|
|
171
|
+
const bytes = new Uint8Array(4);
|
|
172
|
+
this.readFully(bytes);
|
|
173
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
174
|
+
return view.getInt32(0, false); // big-endian
|
|
175
|
+
}
|
|
176
|
+
readLong() {
|
|
177
|
+
const bytes = new Uint8Array(8);
|
|
178
|
+
this.readFully(bytes);
|
|
179
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
180
|
+
return Number(view.getBigInt64(0, false));
|
|
181
|
+
}
|
|
182
|
+
}
|
package/dist/core-file.d.ts
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
export class CoreFile {
|
|
3
|
+
filePath;
|
|
4
|
+
_position = 0;
|
|
5
|
+
fd;
|
|
6
|
+
constructor(filePath) {
|
|
7
|
+
this.filePath = filePath;
|
|
8
|
+
// Create file if it doesn't exist
|
|
9
|
+
try {
|
|
10
|
+
fs.accessSync(filePath);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
fs.writeFileSync(filePath, new Uint8Array(0));
|
|
14
|
+
}
|
|
15
|
+
// Open file for reading and writing
|
|
16
|
+
this.fd = fs.openSync(filePath, 'r+');
|
|
17
|
+
}
|
|
18
|
+
reader() {
|
|
19
|
+
return new FileDataReader(this);
|
|
20
|
+
}
|
|
21
|
+
writer() {
|
|
22
|
+
return new FileDataWriter(this);
|
|
23
|
+
}
|
|
24
|
+
length() {
|
|
25
|
+
const stats = fs.fstatSync(this.fd);
|
|
26
|
+
return stats.size;
|
|
27
|
+
}
|
|
28
|
+
seek(pos) {
|
|
29
|
+
this._position = pos;
|
|
30
|
+
}
|
|
31
|
+
position() {
|
|
32
|
+
return this._position;
|
|
33
|
+
}
|
|
34
|
+
setLength(len) {
|
|
35
|
+
fs.ftruncateSync(this.fd, len);
|
|
36
|
+
}
|
|
37
|
+
flush() {
|
|
38
|
+
}
|
|
39
|
+
sync() {
|
|
40
|
+
fs.fsyncSync(this.fd);
|
|
41
|
+
}
|
|
42
|
+
[Symbol.dispose]() {
|
|
43
|
+
fs.closeSync(this.fd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
class FileDataReader {
|
|
47
|
+
core;
|
|
48
|
+
constructor(core) {
|
|
49
|
+
this.core = core;
|
|
50
|
+
}
|
|
51
|
+
readFully(b) {
|
|
52
|
+
const position = this.core.position();
|
|
53
|
+
fs.readSync(this.core.fd, b, 0, b.length, position);
|
|
54
|
+
this.core.seek(position + b.length);
|
|
55
|
+
}
|
|
56
|
+
readByte() {
|
|
57
|
+
const bytes = new Uint8Array(1);
|
|
58
|
+
this.readFully(bytes);
|
|
59
|
+
return bytes[0];
|
|
60
|
+
}
|
|
61
|
+
readShort() {
|
|
62
|
+
const bytes = new Uint8Array(2);
|
|
63
|
+
this.readFully(bytes);
|
|
64
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
65
|
+
return view.getInt16(0, false);
|
|
66
|
+
}
|
|
67
|
+
readInt() {
|
|
68
|
+
const bytes = new Uint8Array(4);
|
|
69
|
+
this.readFully(bytes);
|
|
70
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
71
|
+
return view.getInt32(0, false);
|
|
72
|
+
}
|
|
73
|
+
readLong() {
|
|
74
|
+
const bytes = new Uint8Array(8);
|
|
75
|
+
this.readFully(bytes);
|
|
76
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
77
|
+
return Number(view.getBigInt64(0, false));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
class FileDataWriter {
|
|
81
|
+
core;
|
|
82
|
+
constructor(core) {
|
|
83
|
+
this.core = core;
|
|
84
|
+
}
|
|
85
|
+
write(buffer) {
|
|
86
|
+
const position = this.core.position();
|
|
87
|
+
fs.writeSync(this.core.fd, buffer, 0, buffer.length, position);
|
|
88
|
+
this.core.seek(position + buffer.length);
|
|
89
|
+
}
|
|
90
|
+
writeByte(v) {
|
|
91
|
+
this.write(new Uint8Array([v & 0xff]));
|
|
92
|
+
}
|
|
93
|
+
writeShort(v) {
|
|
94
|
+
const buffer = new ArrayBuffer(2);
|
|
95
|
+
const view = new DataView(buffer);
|
|
96
|
+
view.setInt16(0, v, false);
|
|
97
|
+
this.write(new Uint8Array(buffer));
|
|
98
|
+
}
|
|
99
|
+
writeLong(v) {
|
|
100
|
+
const buffer = new ArrayBuffer(8);
|
|
101
|
+
const view = new DataView(buffer);
|
|
102
|
+
view.setBigInt64(0, BigInt(v), false);
|
|
103
|
+
this.write(new Uint8Array(buffer));
|
|
104
|
+
}
|
|
105
|
+
}
|
package/dist/core-memory.d.ts
CHANGED