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.
Files changed (61) hide show
  1. package/README.md +129 -15
  2. package/dist/core-buffered-file.d.ts +2 -2
  3. package/dist/core-buffered-file.js +182 -0
  4. package/dist/core-file.d.ts +1 -1
  5. package/dist/core-file.js +105 -0
  6. package/dist/core-memory.d.ts +1 -1
  7. package/dist/core-memory.js +152 -0
  8. package/dist/core.js +1 -0
  9. package/dist/database.d.ts +174 -49
  10. package/dist/database.js +3047 -0
  11. package/dist/exceptions.d.ts +4 -0
  12. package/dist/exceptions.js +62 -0
  13. package/dist/hasher.js +49 -0
  14. package/dist/index.d.ts +30 -26
  15. package/dist/index.js +35 -3923
  16. package/dist/read-array-list.d.ts +4 -3
  17. package/dist/read-array-list.js +41 -0
  18. package/dist/read-counted-hash-map.d.ts +2 -2
  19. package/dist/read-counted-hash-map.js +19 -0
  20. package/dist/read-counted-hash-set.d.ts +2 -2
  21. package/dist/read-counted-hash-set.js +19 -0
  22. package/dist/read-cursor.d.ts +16 -5
  23. package/dist/read-cursor.js +678 -0
  24. package/dist/read-hash-map.d.ts +4 -4
  25. package/dist/read-hash-map.js +65 -0
  26. package/dist/read-hash-set.d.ts +4 -4
  27. package/dist/read-hash-set.js +47 -0
  28. package/dist/read-linked-array-list.d.ts +4 -3
  29. package/dist/read-linked-array-list.js +41 -0
  30. package/dist/read-sorted-map.d.ts +21 -0
  31. package/dist/read-sorted-map.js +73 -0
  32. package/dist/read-sorted-set.d.ts +19 -0
  33. package/dist/read-sorted-set.js +65 -0
  34. package/dist/slot-pointer.d.ts +1 -1
  35. package/dist/slot-pointer.js +11 -0
  36. package/dist/slot.d.ts +2 -2
  37. package/dist/slot.js +41 -0
  38. package/dist/slotted.d.ts +1 -1
  39. package/dist/slotted.js +1 -0
  40. package/dist/tag.d.ts +3 -1
  41. package/dist/tag.js +25 -0
  42. package/dist/write-array-list.d.ts +4 -4
  43. package/dist/write-array-list.js +42 -0
  44. package/dist/write-counted-hash-map.d.ts +2 -2
  45. package/dist/write-counted-hash-map.js +18 -0
  46. package/dist/write-counted-hash-set.d.ts +2 -2
  47. package/dist/write-counted-hash-set.js +18 -0
  48. package/dist/write-cursor.d.ts +5 -5
  49. package/dist/write-cursor.js +124 -0
  50. package/dist/write-hash-map.d.ts +4 -4
  51. package/dist/write-hash-map.js +90 -0
  52. package/dist/write-hash-set.d.ts +4 -4
  53. package/dist/write-hash-set.js +59 -0
  54. package/dist/write-linked-array-list.d.ts +4 -4
  55. package/dist/write-linked-array-list.js +52 -0
  56. package/dist/write-sorted-map.d.ts +12 -0
  57. package/dist/write-sorted-map.js +37 -0
  58. package/dist/write-sorted-set.d.ts +10 -0
  59. package/dist/write-sorted-set.js +29 -0
  60. package/dist/writeable-data.js +68 -0
  61. 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
- expect(new TextDecoder().decode(fooValue)).toBe('foo');
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
- expect(fruits.count()).toBe(3);
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
- expect(new TextDecoder().decode(appleValue)).toBe('apple');
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
- All data structures use the hash array mapped trie, invented by Phil Bagwell. The `LinkedArrayList` is based on his later work on RRB trees. These data structures were originally made immutable and widely available by Rich Hickey in Clojure. To my knowledge, they haven't been available in any open source database until xitdb.
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
- expect(new TextDecoder().decode(randomNumber.formatTag!)).toBe('bi');
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
- expect(food.count()).toBe(6);
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
- expect(fruits.count()).toBe(3);
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
- expect(cities.count()).toBe(4);
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
- expect(bigCities.count()).toBe(4);
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
- expect(cities.count()).toBe(4);
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
- expect(bigCities.count()).toBe(2);
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
- expect(lineCount).toBe(50);
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
- expect(header.hashSize).toBe(20);
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
- expect(Hasher.idToString(header.hashId)).toBe("sha1");
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
- expect(history.count()).toBe(1);
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
+ }
@@ -1,4 +1,4 @@
1
- import type { Core, DataReader, DataWriter } from './core';
1
+ import type { Core, DataReader, DataWriter } from './core.js';
2
2
  export declare class CoreFile implements Core {
3
3
  filePath: string;
4
4
  private _position;
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import type { Core, DataReader, DataWriter } from './core';
1
+ import type { Core, DataReader, DataWriter } from './core.js';
2
2
  export declare class CoreMemory implements Core {
3
3
  memory: RandomAccessMemory;
4
4
  constructor();