xitdb 0.14.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 +113 -0
- package/dist/read-array-list.d.ts +1 -0
- package/dist/read-array-list.js +6 -0
- package/dist/read-cursor.d.ts +6 -1
- package/dist/read-cursor.js +110 -9
- package/dist/read-linked-array-list.d.ts +1 -0
- package/dist/read-linked-array-list.js +6 -0
- package/package.json +1 -1
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)
|
|
@@ -287,6 +288,116 @@ const bigCities = new ReadArrayList(bigCitiesCursor!);
|
|
|
287
288
|
assert.strictEqual(bigCities.count(), 2);
|
|
288
289
|
```
|
|
289
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
|
+
|
|
290
401
|
## Large Byte Arrays
|
|
291
402
|
|
|
292
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:
|
|
@@ -361,6 +472,8 @@ The above code iterates over `people`, which is an `ArrayList`, and for each per
|
|
|
361
472
|
|
|
362
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`.
|
|
363
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
|
+
|
|
364
477
|
## Hashing
|
|
365
478
|
|
|
366
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.
|
|
@@ -8,6 +8,7 @@ export declare class ReadArrayList implements Slotted {
|
|
|
8
8
|
slot(): Slot;
|
|
9
9
|
count(): number;
|
|
10
10
|
iterator(): CursorIterator;
|
|
11
|
+
iteratorFrom(index: number): CursorIterator;
|
|
11
12
|
[Symbol.iterator](): Iterator<ReadCursor>;
|
|
12
13
|
getCursor(index: number): ReadCursor | null;
|
|
13
14
|
getSlot(index: number): Slot | null;
|
package/dist/read-array-list.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CursorIterator } from './read-cursor.js';
|
|
1
2
|
import { ArrayListGet } from './database.js';
|
|
2
3
|
import { UnexpectedTagException } from './exceptions.js';
|
|
3
4
|
export class ReadArrayList {
|
|
@@ -23,6 +24,11 @@ export class ReadArrayList {
|
|
|
23
24
|
iterator() {
|
|
24
25
|
return this.cursor.iterator();
|
|
25
26
|
}
|
|
27
|
+
// iterate starting at the given index, seeking straight to it instead of
|
|
28
|
+
// walking from the front. negative indexes count from the end.
|
|
29
|
+
iteratorFrom(index) {
|
|
30
|
+
return CursorIterator.initArrayListFromIndex(this.cursor, index);
|
|
31
|
+
}
|
|
26
32
|
*[Symbol.iterator]() {
|
|
27
33
|
yield* this.cursor;
|
|
28
34
|
}
|
package/dist/read-cursor.d.ts
CHANGED
|
@@ -48,13 +48,18 @@ export declare class CursorIterator {
|
|
|
48
48
|
private stack;
|
|
49
49
|
private nextCursorMaybe;
|
|
50
50
|
constructor(cursor: ReadCursor);
|
|
51
|
+
private static resolveStartIndex;
|
|
51
52
|
static initSortedFromIndex(cursor: ReadCursor, startIndex: number): CursorIterator;
|
|
53
|
+
static initArrayListFromIndex(cursor: ReadCursor, startIndex: number): CursorIterator;
|
|
54
|
+
static initLinkedArrayListFromIndex(cursor: ReadCursor, startIndex: number): CursorIterator;
|
|
52
55
|
static initSortedFromKey(cursor: ReadCursor, startKey: Uint8Array): CursorIterator;
|
|
53
56
|
private static sortedRootPtr;
|
|
54
57
|
private static sortedStackFromIndex;
|
|
58
|
+
private static arrayListStackFromIndex;
|
|
59
|
+
private static btreeStackFromIndex;
|
|
55
60
|
private static sortedStackFromKey;
|
|
56
61
|
init(): void;
|
|
57
|
-
|
|
62
|
+
static readSlotBlock(cursor: ReadCursor, position: number): Slot[];
|
|
58
63
|
private initStack;
|
|
59
64
|
hasNext(): boolean;
|
|
60
65
|
next(): ReadCursor | null;
|
package/dist/read-cursor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Slot } from './slot.js';
|
|
2
2
|
import { SlotPointer } from './slot-pointer.js';
|
|
3
|
-
import { WriteMode, ArrayListHeader, BTreeHeader, KeyValuePair, INDEX_BLOCK_SIZE, BTREE_NODE_HEADER_SIZE, BTreeNodeKind, SLOT_COUNT, } from './database.js';
|
|
3
|
+
import { WriteMode, ArrayListHeader, BTreeHeader, KeyValuePair, INDEX_BLOCK_SIZE, BTREE_NODE_HEADER_SIZE, BTreeNodeKind, SLOT_COUNT, BIT_COUNT, } from './database.js';
|
|
4
4
|
import { UnexpectedTagException, StreamTooLongException, EndOfStreamException, InvalidOffsetException, KeyNotFoundException, ExpectedUnsignedLongException, } from './exceptions.js';
|
|
5
5
|
import { Bytes } from './writeable-data.js';
|
|
6
6
|
export class KeyValuePairCursor {
|
|
@@ -321,19 +321,77 @@ export class CursorIterator {
|
|
|
321
321
|
constructor(cursor) {
|
|
322
322
|
this.cursor = cursor;
|
|
323
323
|
}
|
|
324
|
+
// resolve a possibly-negative start index against `size` to a 0-based rank.
|
|
325
|
+
// negatives count from the end (-1 is the last entry); anything out of range
|
|
326
|
+
// returns -1 (yield nothing).
|
|
327
|
+
static resolveStartIndex(index, size) {
|
|
328
|
+
const resolved = index < 0 ? index + size : index;
|
|
329
|
+
if (resolved < 0 || resolved >= size)
|
|
330
|
+
return -1;
|
|
331
|
+
return resolved;
|
|
332
|
+
}
|
|
324
333
|
// start a sorted-map iterator at the entry with rank startIndex (the count descent),
|
|
325
|
-
// iterating in key order from there
|
|
334
|
+
// iterating in key order from there. negative indexes count from the end.
|
|
326
335
|
static initSortedFromIndex(cursor, startIndex) {
|
|
327
|
-
const total = cursor.count();
|
|
328
336
|
const it = new CursorIterator(cursor);
|
|
329
337
|
// an unwritten map is NONE (like iterator()): yield nothing
|
|
330
|
-
if (cursor.slotPtr.slot.tag === 0 /* Tag.NONE */
|
|
338
|
+
if (cursor.slotPtr.slot.tag === 0 /* Tag.NONE */) {
|
|
339
|
+
return it;
|
|
340
|
+
}
|
|
341
|
+
const total = cursor.count();
|
|
342
|
+
const idx = CursorIterator.resolveStartIndex(startIndex, total);
|
|
343
|
+
if (idx < 0) {
|
|
331
344
|
return it;
|
|
332
345
|
}
|
|
333
346
|
const rootPtr = CursorIterator.sortedRootPtr(cursor);
|
|
334
347
|
it.size = total;
|
|
335
|
-
it.index =
|
|
336
|
-
it.stack = CursorIterator.sortedStackFromIndex(cursor, rootPtr,
|
|
348
|
+
it.index = idx;
|
|
349
|
+
it.stack = CursorIterator.sortedStackFromIndex(cursor, rootPtr, idx);
|
|
350
|
+
return it;
|
|
351
|
+
}
|
|
352
|
+
// start an array-list iterator at startIndex, descending the radix trie
|
|
353
|
+
// straight to that index. negatives count from the end; out of range (or an
|
|
354
|
+
// unwritten list) yields nothing.
|
|
355
|
+
static initArrayListFromIndex(cursor, startIndex) {
|
|
356
|
+
const it = new CursorIterator(cursor);
|
|
357
|
+
if (cursor.slotPtr.slot.tag !== 2 /* Tag.ARRAY_LIST */) {
|
|
358
|
+
return it;
|
|
359
|
+
}
|
|
360
|
+
cursor.db.core.seek(Number(cursor.slotPtr.slot.value));
|
|
361
|
+
const reader = cursor.db.core.reader();
|
|
362
|
+
const headerBytes = new Uint8Array(ArrayListHeader.LENGTH);
|
|
363
|
+
reader.readFully(headerBytes);
|
|
364
|
+
const header = ArrayListHeader.fromBytes(headerBytes);
|
|
365
|
+
const idx = CursorIterator.resolveStartIndex(startIndex, header.size);
|
|
366
|
+
if (idx < 0) {
|
|
367
|
+
return it;
|
|
368
|
+
}
|
|
369
|
+
const lastKey = header.size - 1;
|
|
370
|
+
const shift = lastKey < SLOT_COUNT ? 0 : Math.floor(Math.log(lastKey) / Math.log(SLOT_COUNT));
|
|
371
|
+
it.size = header.size;
|
|
372
|
+
it.index = idx;
|
|
373
|
+
it.stack = CursorIterator.arrayListStackFromIndex(cursor, header.ptr, idx, shift);
|
|
374
|
+
return it;
|
|
375
|
+
}
|
|
376
|
+
// start a linked-array-list iterator at startIndex, descending the
|
|
377
|
+
// count-augmented b-tree straight to that index. negatives count from the end.
|
|
378
|
+
static initLinkedArrayListFromIndex(cursor, startIndex) {
|
|
379
|
+
const it = new CursorIterator(cursor);
|
|
380
|
+
if (cursor.slotPtr.slot.tag !== 3 /* Tag.LINKED_ARRAY_LIST */) {
|
|
381
|
+
return it;
|
|
382
|
+
}
|
|
383
|
+
cursor.db.core.seek(Number(cursor.slotPtr.slot.value));
|
|
384
|
+
const reader = cursor.db.core.reader();
|
|
385
|
+
const headerBytes = new Uint8Array(BTreeHeader.LENGTH);
|
|
386
|
+
reader.readFully(headerBytes);
|
|
387
|
+
const header = BTreeHeader.fromBytes(headerBytes);
|
|
388
|
+
const idx = CursorIterator.resolveStartIndex(startIndex, header.size);
|
|
389
|
+
if (idx < 0) {
|
|
390
|
+
return it;
|
|
391
|
+
}
|
|
392
|
+
it.size = header.size;
|
|
393
|
+
it.index = idx;
|
|
394
|
+
it.stack = CursorIterator.btreeStackFromIndex(cursor, header.rootPtr, idx);
|
|
337
395
|
return it;
|
|
338
396
|
}
|
|
339
397
|
// start a sorted-map iterator at the first entry with key >= startKey
|
|
@@ -387,6 +445,49 @@ export class CursorIterator {
|
|
|
387
445
|
}
|
|
388
446
|
}
|
|
389
447
|
}
|
|
448
|
+
// descend the array-list radix trie to startIndex, pushing one IteratorLevel
|
|
449
|
+
// per tier with its index set to that tier's child slot. nextInternal then
|
|
450
|
+
// walks forward from there.
|
|
451
|
+
static arrayListStackFromIndex(cursor, rootPtr, startIndex, shift) {
|
|
452
|
+
const stack = [];
|
|
453
|
+
let pos = rootPtr;
|
|
454
|
+
let sh = shift;
|
|
455
|
+
while (true) {
|
|
456
|
+
const block = CursorIterator.readSlotBlock(cursor, pos);
|
|
457
|
+
const i = (startIndex >>> (sh * BIT_COUNT)) & (SLOT_COUNT - 1);
|
|
458
|
+
stack.push(new IteratorLevel(pos, block, i));
|
|
459
|
+
if (sh === 0)
|
|
460
|
+
return stack;
|
|
461
|
+
// every tier above the leaf is a populated INDEX slot for any
|
|
462
|
+
// startIndex < size, so this child always exists
|
|
463
|
+
pos = Number(block[i].value);
|
|
464
|
+
sh -= 1;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// descend the linked-array-list count b-tree to startIndex; the positional
|
|
468
|
+
// analog of sortedStackFromIndex (no separator keys).
|
|
469
|
+
static btreeStackFromIndex(cursor, rootPtr, startIndex) {
|
|
470
|
+
const stack = [];
|
|
471
|
+
let nodePtr = rootPtr;
|
|
472
|
+
let rem = startIndex;
|
|
473
|
+
while (true) {
|
|
474
|
+
const node = cursor.db.readBTreeNode(nodePtr);
|
|
475
|
+
const position = nodePtr + BTREE_NODE_HEADER_SIZE;
|
|
476
|
+
if (node.kind === BTreeNodeKind.LEAF) {
|
|
477
|
+
stack.push(new IteratorLevel(position, node.values, rem));
|
|
478
|
+
return stack;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
let i = 0;
|
|
482
|
+
while (i + 1 < node.num && rem >= node.counts[i]) {
|
|
483
|
+
rem -= node.counts[i];
|
|
484
|
+
i++;
|
|
485
|
+
}
|
|
486
|
+
stack.push(new IteratorLevel(position, node.children, i));
|
|
487
|
+
nodePtr = Number(node.children[i].value);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
390
491
|
static sortedStackFromKey(cursor, rootPtr, key) {
|
|
391
492
|
const stack = [];
|
|
392
493
|
let nodePtr = rootPtr;
|
|
@@ -471,7 +572,7 @@ export class CursorIterator {
|
|
|
471
572
|
}
|
|
472
573
|
// read a 16-slot index block (the iterable structures all use 9-byte slots in
|
|
473
574
|
// their index/node blocks)
|
|
474
|
-
readSlotBlock(cursor, position) {
|
|
575
|
+
static readSlotBlock(cursor, position) {
|
|
475
576
|
cursor.db.core.seek(position);
|
|
476
577
|
const reader = cursor.db.core.reader();
|
|
477
578
|
const indexBlockBytes = new Uint8Array(INDEX_BLOCK_SIZE);
|
|
@@ -484,7 +585,7 @@ export class CursorIterator {
|
|
|
484
585
|
return indexBlock;
|
|
485
586
|
}
|
|
486
587
|
initStack(cursor, position) {
|
|
487
|
-
return [new IteratorLevel(position,
|
|
588
|
+
return [new IteratorLevel(position, CursorIterator.readSlotBlock(cursor, position), 0)];
|
|
488
589
|
}
|
|
489
590
|
hasNext() {
|
|
490
591
|
switch (this.cursor.slotPtr.slot.tag) {
|
|
@@ -557,7 +658,7 @@ export class CursorIterator {
|
|
|
557
658
|
if (nextSlot.tag === 1 /* Tag.INDEX */) {
|
|
558
659
|
// nodeOffset skips a b-tree node's kind+num header
|
|
559
660
|
const nextPos = Number(nextSlot.value) + nodeOffset;
|
|
560
|
-
this.stack.push(new IteratorLevel(nextPos,
|
|
661
|
+
this.stack.push(new IteratorLevel(nextPos, CursorIterator.readSlotBlock(this.cursor, nextPos), 0));
|
|
561
662
|
continue;
|
|
562
663
|
}
|
|
563
664
|
else {
|
|
@@ -8,6 +8,7 @@ export declare class ReadLinkedArrayList implements Slotted {
|
|
|
8
8
|
slot(): Slot;
|
|
9
9
|
count(): number;
|
|
10
10
|
iterator(): CursorIterator;
|
|
11
|
+
iteratorFrom(index: number): CursorIterator;
|
|
11
12
|
[Symbol.iterator](): Iterator<ReadCursor>;
|
|
12
13
|
getCursor(index: number): ReadCursor | null;
|
|
13
14
|
getSlot(index: number): Slot | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CursorIterator } from './read-cursor.js';
|
|
1
2
|
import { LinkedArrayListGet } from './database.js';
|
|
2
3
|
import { UnexpectedTagException } from './exceptions.js';
|
|
3
4
|
export class ReadLinkedArrayList {
|
|
@@ -23,6 +24,11 @@ export class ReadLinkedArrayList {
|
|
|
23
24
|
iterator() {
|
|
24
25
|
return this.cursor.iterator();
|
|
25
26
|
}
|
|
27
|
+
// iterate starting at the given index, seeking straight to it instead of
|
|
28
|
+
// walking from the front. negative indexes count from the end.
|
|
29
|
+
iteratorFrom(index) {
|
|
30
|
+
return CursorIterator.initLinkedArrayListFromIndex(this.cursor, index);
|
|
31
|
+
}
|
|
26
32
|
*[Symbol.iterator]() {
|
|
27
33
|
yield* this.cursor;
|
|
28
34
|
}
|