xitdb 0.1.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.
@@ -0,0 +1,46 @@
1
+ This is an example program that reads a xitdb file and prints out its most recent value (the last item of the top-level ArrayList). From the root of this repo, run:
2
+
3
+ ```
4
+ bun run example/dump.ts tests/fixtures/test.db
5
+ ```
6
+
7
+ ...and it will print:
8
+
9
+ ```
10
+ ArrayList[2]:
11
+ HashMap{7}:
12
+ (none):
13
+ LinkedArrayList[1]:
14
+ [0]:
15
+ "Wash the car"
16
+ (none):
17
+ HashSet{1}: ["a"]
18
+ (none):
19
+ ArrayList[2]:
20
+ [0]:
21
+ HashMap{2}:
22
+ (none):
23
+ 26 (uint)
24
+ (none):
25
+ "Alice"
26
+ [1]:
27
+ HashMap{2}:
28
+ (none):
29
+ 42 (uint)
30
+ (none):
31
+ "Bob"
32
+ (none):
33
+ "foo"
34
+ "fruits":
35
+ ArrayList[2]:
36
+ [0]:
37
+ "lemon"
38
+ [1]:
39
+ "pear"
40
+ (none):
41
+ CountedHashMap{1}:
42
+ "a":
43
+ 2 (uint)
44
+ (none):
45
+ CountedHashSet{1}: ["a"]
46
+ ```
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env bun
2
+ import {
3
+ Database,
4
+ Hasher,
5
+ CoreBufferedFile,
6
+ ReadArrayList,
7
+ ReadCursor,
8
+ Tag,
9
+ } from '../src';
10
+
11
+ async function formatKey(cursor: ReadCursor): Promise<string> {
12
+ const tag = cursor.slotPtr.slot.tag;
13
+
14
+ switch (tag) {
15
+ case Tag.NONE:
16
+ return '(none)';
17
+ case Tag.BYTES:
18
+ case Tag.SHORT_BYTES: {
19
+ const bytes = await cursor.readBytes(null);
20
+ const text = new TextDecoder().decode(bytes);
21
+ return `"${text}"`;
22
+ }
23
+ case Tag.UINT:
24
+ return `${cursor.readUint()}`;
25
+ case Tag.INT:
26
+ return `${cursor.readInt()}`;
27
+ case Tag.FLOAT:
28
+ return `${cursor.readFloat()}`;
29
+ default:
30
+ return `<key tag: ${tag}>`;
31
+ }
32
+ }
33
+
34
+ async function printValue(cursor: ReadCursor, indent: string): Promise<void> {
35
+ const tag = cursor.slotPtr.slot.tag;
36
+
37
+ switch (tag) {
38
+ case Tag.NONE:
39
+ console.log(`${indent}(none)`);
40
+ break;
41
+
42
+ case Tag.ARRAY_LIST: {
43
+ const list = new ReadArrayList(cursor);
44
+ const count = await list.count();
45
+ console.log(`${indent}ArrayList[${count}]:`);
46
+ if (indent == '') {
47
+ const itemCursor = await list.getCursor(count - 1);
48
+ if (itemCursor) {
49
+ await printValue(itemCursor, indent + ' ');
50
+ }
51
+ } else {
52
+ for (let i = 0; i < count; i++) {
53
+ const itemCursor = await list.getCursor(i);
54
+ if (itemCursor) {
55
+ console.log(`${indent} [${i}]:`);
56
+ await printValue(itemCursor, indent + ' ');
57
+ }
58
+ }
59
+ }
60
+ break;
61
+ }
62
+
63
+ case Tag.HASH_MAP:
64
+ case Tag.COUNTED_HASH_MAP: {
65
+ const iter = cursor.iterator();
66
+ await iter.init();
67
+ const entries: Array<{ key: string; valueCursor: ReadCursor }> = [];
68
+
69
+ while (await iter.hasNext()) {
70
+ const kvPairCursor = await iter.next();
71
+ if (kvPairCursor) {
72
+ const kvPair = await kvPairCursor.readKeyValuePair();
73
+ const key = await formatKey(kvPair.keyCursor);
74
+ entries.push({ key, valueCursor: kvPair.valueCursor });
75
+ }
76
+ }
77
+
78
+ const prefix = tag === Tag.COUNTED_HASH_MAP ? 'CountedHashMap' : 'HashMap';
79
+ console.log(`${indent}${prefix}{${entries.length}}:`);
80
+
81
+ for (const entry of entries) {
82
+ console.log(`${indent} ${entry.key}:`);
83
+ await printValue(entry.valueCursor, indent + ' ');
84
+ }
85
+ break;
86
+ }
87
+
88
+ case Tag.HASH_SET:
89
+ case Tag.COUNTED_HASH_SET: {
90
+ const iter = cursor.iterator();
91
+ await iter.init();
92
+ const keys: string[] = [];
93
+
94
+ while (await iter.hasNext()) {
95
+ const kvPairCursor = await iter.next();
96
+ if (kvPairCursor) {
97
+ const kvPair = await kvPairCursor.readKeyValuePair();
98
+ const key = await formatKey(kvPair.keyCursor);
99
+ keys.push(key);
100
+ }
101
+ }
102
+
103
+ const prefix = tag === Tag.COUNTED_HASH_SET ? 'CountedHashSet' : 'HashSet';
104
+ console.log(`${indent}${prefix}{${keys.length}}: [${keys.join(', ')}]`);
105
+ break;
106
+ }
107
+
108
+ case Tag.LINKED_ARRAY_LIST: {
109
+ const count = await cursor.count();
110
+ console.log(`${indent}LinkedArrayList[${count}]:`);
111
+ const iter = cursor.iterator();
112
+ await iter.init();
113
+ let i = 0;
114
+ while (await iter.hasNext()) {
115
+ const itemCursor = await iter.next();
116
+ if (itemCursor) {
117
+ console.log(`${indent} [${i}]:`);
118
+ await printValue(itemCursor, indent + ' ');
119
+ }
120
+ i++;
121
+ }
122
+ break;
123
+ }
124
+
125
+ case Tag.BYTES:
126
+ case Tag.SHORT_BYTES: {
127
+ const bytesObj = await cursor.readBytesObject(null);
128
+ const text = new TextDecoder().decode(bytesObj.value);
129
+ const isPrintable = /^[\x20-\x7E\n\r\t]*$/.test(text);
130
+
131
+ if (isPrintable && text.length <= 100) {
132
+ if (bytesObj.formatTag) {
133
+ const formatTag = new TextDecoder().decode(bytesObj.formatTag);
134
+ console.log(`${indent}"${text}" (format: ${formatTag})`);
135
+ } else {
136
+ console.log(`${indent}"${text}"`);
137
+ }
138
+ } else {
139
+ const preview = bytesObj.value.slice(0, 16);
140
+ const hex = Array.from(preview).map(b => b.toString(16).padStart(2, '0')).join(' ');
141
+ if (bytesObj.formatTag) {
142
+ const formatTag = new TextDecoder().decode(bytesObj.formatTag);
143
+ console.log(`${indent}<${bytesObj.value.length} bytes: ${hex}...> (format: ${formatTag})`);
144
+ } else {
145
+ console.log(`${indent}<${bytesObj.value.length} bytes: ${hex}...>`);
146
+ }
147
+ }
148
+ break;
149
+ }
150
+
151
+ case Tag.UINT: {
152
+ const value = cursor.readUint();
153
+ console.log(`${indent}${value} (uint)`);
154
+ break;
155
+ }
156
+
157
+ case Tag.INT: {
158
+ const value = cursor.readInt();
159
+ console.log(`${indent}${value} (int)`);
160
+ break;
161
+ }
162
+
163
+ case Tag.FLOAT: {
164
+ const value = cursor.readFloat();
165
+ console.log(`${indent}${value} (float)`);
166
+ break;
167
+ }
168
+
169
+ default:
170
+ console.log(`${indent}<unknown tag: ${tag}>`);
171
+ }
172
+ }
173
+
174
+ async function main() {
175
+ const args = process.argv.slice(2);
176
+
177
+ if (args.length < 1) {
178
+ console.error('Usage: bun run dump.ts <database-file>');
179
+ process.exit(1);
180
+ }
181
+
182
+ const filePath = args[0];
183
+
184
+ try {
185
+ const core = await CoreBufferedFile.create(filePath);
186
+ const hasher = new Hasher('SHA-1');
187
+ const db = await Database.create(core, hasher);
188
+
189
+ console.log(`Database: ${filePath}`);
190
+ console.log('---');
191
+
192
+ const rootCursor = await db.rootCursor();
193
+ await printValue(rootCursor, '');
194
+
195
+ } catch (error) {
196
+ console.error(`Error reading database: ${error}`);
197
+ process.exit(1);
198
+ }
199
+ }
200
+
201
+ main();
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "xitdb",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun",
9
+ "build:types": "tsc --emitDeclarationOnly",
10
+ "test": "bun test",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "devDependencies": {
14
+ "@types/bun": "latest",
15
+ "typescript": "^5.3.0"
16
+ }
17
+ }
@@ -0,0 +1,226 @@
1
+ import type { Core, DataReader, DataWriter } from './core';
2
+ import { CoreFile } from './core-file';
3
+ import { CoreMemory } from './core-memory';
4
+
5
+ export class CoreBufferedFile implements Core {
6
+ public file: RandomAccessBufferedFile;
7
+
8
+ constructor(file: RandomAccessBufferedFile) {
9
+ this.file = file;
10
+ }
11
+
12
+ static async create(filePath: string, bufferSize?: number): Promise<CoreBufferedFile> {
13
+ const file = await RandomAccessBufferedFile.create(filePath, bufferSize);
14
+ return new CoreBufferedFile(file);
15
+ }
16
+
17
+ reader(): DataReader {
18
+ return this.file;
19
+ }
20
+
21
+ writer(): DataWriter {
22
+ return this.file;
23
+ }
24
+
25
+ async length(): Promise<number> {
26
+ return await this.file.length();
27
+ }
28
+
29
+ async seek(pos: number): Promise<void> {
30
+ await this.file.seek(pos);
31
+ }
32
+
33
+ position(): number {
34
+ return this.file.position();
35
+ }
36
+
37
+ async setLength(len: number): Promise<void> {
38
+ await this.file.setLength(len);
39
+ }
40
+
41
+ async flush(): Promise<void> {
42
+ await this.file.flush();
43
+ }
44
+
45
+ async sync(): Promise<void> {
46
+ await this.file.sync();
47
+ }
48
+
49
+ [Symbol.dispose]() {
50
+ this.file.file[Symbol.dispose]();
51
+ }
52
+ }
53
+
54
+ const DEFAULT_BUFFER_SIZE = 8 * 1024 * 1024; // 8MB
55
+
56
+ class RandomAccessBufferedFile implements DataReader, DataWriter {
57
+ public file: CoreFile;
58
+ private memory: CoreMemory;
59
+ private bufferSize: number; // flushes when the memory is >= this size
60
+ private filePos: number;
61
+ private memoryPos: number;
62
+
63
+ private constructor(file: CoreFile, bufferSize: number) {
64
+ this.file = file;
65
+ this.memory = new CoreMemory();
66
+ this.bufferSize = bufferSize;
67
+ this.filePos = 0;
68
+ this.memoryPos = 0;
69
+ }
70
+
71
+ static async create(filePath: string, bufferSize: number = DEFAULT_BUFFER_SIZE): Promise<RandomAccessBufferedFile> {
72
+ const file = await CoreFile.create(filePath);
73
+ return new RandomAccessBufferedFile(file, bufferSize);
74
+ }
75
+
76
+ async seek(pos: number): Promise<void> {
77
+ // flush if we are going past the end of the in-memory buffer
78
+ if (pos > this.memoryPos + await this.memory.length()) {
79
+ await this.flush();
80
+ }
81
+
82
+ this.filePos = pos;
83
+
84
+ // if the buffer is empty, set its position to this offset as well
85
+ if (await this.memory.length() === 0) {
86
+ this.memoryPos = pos;
87
+ }
88
+ }
89
+
90
+ async length(): Promise<number> {
91
+ return Math.max(this.memoryPos + await this.memory.length(), await this.file.length());
92
+ }
93
+
94
+ position(): number {
95
+ return this.filePos;
96
+ }
97
+
98
+ async setLength(len: number): Promise<void> {
99
+ await this.flush();
100
+ await this.file.setLength(len);
101
+ this.filePos = Math.min(len, this.filePos);
102
+ }
103
+
104
+ async flush(): Promise<void> {
105
+ if (await this.memory.length() > 0) {
106
+ await this.file.seek(this.memoryPos);
107
+ await this.file.writer().write(this.memory.memory.toByteArray());
108
+
109
+ this.memoryPos = 0;
110
+ this.memory.memory.reset();
111
+ }
112
+ }
113
+
114
+ async sync(): Promise<void> {
115
+ await this.flush();
116
+ await this.file.sync();
117
+ }
118
+
119
+ // DataWriter interface
120
+
121
+ async write(buffer: Uint8Array): Promise<void> {
122
+ if (await this.memory.length() + buffer.length > this.bufferSize) {
123
+ await this.flush();
124
+ }
125
+
126
+ if (this.filePos >= this.memoryPos && this.filePos <= this.memoryPos + await this.memory.length()) {
127
+ this.memory.seek(this.filePos - this.memoryPos);
128
+ await this.memory.memory.write(buffer);
129
+ } else {
130
+ // Write directly to file
131
+ await this.file.seek(this.filePos);
132
+ await this.file.writer().write(buffer);
133
+ }
134
+
135
+ this.filePos += buffer.length;
136
+ }
137
+
138
+ async writeByte(v: number): Promise<void> {
139
+ await this.write(new Uint8Array([v & 0xff]));
140
+ }
141
+
142
+ async writeShort(v: number): Promise<void> {
143
+ const buffer = new ArrayBuffer(2);
144
+ const view = new DataView(buffer);
145
+ view.setInt16(0, v & 0xffff, false); // big-endian
146
+ await this.write(new Uint8Array(buffer));
147
+ }
148
+
149
+ async writeLong(v: number): Promise<void> {
150
+ const buffer = new ArrayBuffer(8);
151
+ const view = new DataView(buffer);
152
+ view.setBigInt64(0, BigInt(v), false);
153
+ await this.write(new Uint8Array(buffer));
154
+ }
155
+
156
+ // DataReader interface
157
+
158
+ async readFully(buffer: Uint8Array): Promise<void> {
159
+ let pos = 0;
160
+
161
+ // read from the disk -- before the in-memory buffer
162
+ if (this.filePos < this.memoryPos) {
163
+ const sizeBeforeMem = Math.min(this.memoryPos - this.filePos, buffer.length);
164
+ const tempBuffer = new Uint8Array(sizeBeforeMem);
165
+ await this.file.seek(this.filePos);
166
+ await this.file.reader().readFully(tempBuffer);
167
+ buffer.set(tempBuffer, pos);
168
+ pos += sizeBeforeMem;
169
+ this.filePos += sizeBeforeMem;
170
+ }
171
+
172
+ if (pos === buffer.length) return;
173
+
174
+ // read from the in-memory buffer
175
+ if (this.filePos >= this.memoryPos && this.filePos < this.memoryPos + await this.memory.length()) {
176
+ const memPos = this.filePos - this.memoryPos;
177
+ const sizeInMem = Math.min(await this.memory.length() - memPos, buffer.length - pos);
178
+ this.memory.seek(memPos);
179
+ const memBuffer = new Uint8Array(sizeInMem);
180
+ await this.memory.memory.readFully(memBuffer);
181
+ buffer.set(memBuffer, pos);
182
+ pos += sizeInMem;
183
+ this.filePos += sizeInMem;
184
+ }
185
+
186
+ if (pos === buffer.length) return;
187
+
188
+ // read from the disk -- after the in-memory buffer
189
+ if (this.filePos >= this.memoryPos + await this.memory.length()) {
190
+ const sizeAfterMem = buffer.length - pos;
191
+ const tempBuffer = new Uint8Array(sizeAfterMem);
192
+ await this.file.seek(this.filePos);
193
+ await this.file.reader().readFully(tempBuffer);
194
+ buffer.set(tempBuffer, pos);
195
+ pos += sizeAfterMem;
196
+ this.filePos += sizeAfterMem;
197
+ }
198
+ }
199
+
200
+ async readByte(): Promise<number> {
201
+ const bytes = new Uint8Array(1);
202
+ await this.readFully(bytes);
203
+ return bytes[0];
204
+ }
205
+
206
+ async readShort(): Promise<number> {
207
+ const bytes = new Uint8Array(2);
208
+ await this.readFully(bytes);
209
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
210
+ return view.getInt16(0, false); // big-endian
211
+ }
212
+
213
+ async readInt(): Promise<number> {
214
+ const bytes = new Uint8Array(4);
215
+ await this.readFully(bytes);
216
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
217
+ return view.getInt32(0, false); // big-endian
218
+ }
219
+
220
+ async readLong(): Promise<number> {
221
+ const bytes = new Uint8Array(8);
222
+ await this.readFully(bytes);
223
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
224
+ return Number(view.getBigInt64(0, false));
225
+ }
226
+ }
@@ -0,0 +1,137 @@
1
+ import type { Core, DataReader, DataWriter } from './core';
2
+ import * as fs from 'fs/promises';
3
+ import type { FileHandle } from 'fs/promises';
4
+
5
+ export class CoreFile implements Core {
6
+ public filePath: string;
7
+ private _position: number = 0;
8
+ public fileHandle: FileHandle;
9
+
10
+ private constructor(filePath: string, fileHandle: FileHandle) {
11
+ this.filePath = filePath;
12
+ this.fileHandle = fileHandle;
13
+ }
14
+
15
+ static async create(filePath: string): Promise<CoreFile> {
16
+ // Create file if it doesn't exist
17
+ try {
18
+ await fs.access(filePath);
19
+ } catch {
20
+ await fs.writeFile(filePath, new Uint8Array(0));
21
+ }
22
+ // Open file handle for reading and writing
23
+ const fileHandle = await fs.open(filePath, 'r+');
24
+ return new CoreFile(filePath, fileHandle);
25
+ }
26
+
27
+ reader(): DataReader {
28
+ return new FileDataReader(this);
29
+ }
30
+
31
+ writer(): DataWriter {
32
+ return new FileDataWriter(this);
33
+ }
34
+
35
+ async length(): Promise<number> {
36
+ const stats = await this.fileHandle.stat();
37
+ return stats.size;
38
+ }
39
+
40
+ async seek(pos: number): Promise<void> {
41
+ this._position = pos;
42
+ }
43
+
44
+ position(): number {
45
+ return this._position;
46
+ }
47
+
48
+ async setLength(len: number): Promise<void> {
49
+ await this.fileHandle.truncate(len);
50
+ }
51
+
52
+ async flush(): Promise<void> {
53
+ }
54
+
55
+ async sync(): Promise<void> {
56
+ await this.fileHandle.sync();
57
+ }
58
+
59
+ [Symbol.dispose]() {
60
+ import("fs").then(fs => {
61
+ fs.closeSync(this.fileHandle.fd);
62
+ });
63
+ }
64
+ }
65
+
66
+ class FileDataReader implements DataReader {
67
+ private core: CoreFile;
68
+
69
+ constructor(core: CoreFile) {
70
+ this.core = core;
71
+ }
72
+
73
+ async readFully(b: Uint8Array): Promise<void> {
74
+ const position = this.core.position();
75
+ await this.core.fileHandle.readv([b], position);
76
+ this.core.seek(position + b.length);
77
+ }
78
+
79
+ async readByte(): Promise<number> {
80
+ const bytes = new Uint8Array(1);
81
+ await this.readFully(bytes);
82
+ return bytes[0];
83
+ }
84
+
85
+ async readShort(): Promise<number> {
86
+ const bytes = new Uint8Array(2);
87
+ await this.readFully(bytes);
88
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
89
+ return view.getInt16(0, false);
90
+ }
91
+
92
+ async readInt(): Promise<number> {
93
+ const bytes = new Uint8Array(4);
94
+ await this.readFully(bytes);
95
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
96
+ return view.getInt32(0, false);
97
+ }
98
+
99
+ async readLong(): Promise<number> {
100
+ const bytes = new Uint8Array(8);
101
+ await this.readFully(bytes);
102
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
103
+ return Number(view.getBigInt64(0, false));
104
+ }
105
+ }
106
+
107
+ class FileDataWriter implements DataWriter {
108
+ private core: CoreFile;
109
+
110
+ constructor(core: CoreFile) {
111
+ this.core = core;
112
+ }
113
+
114
+ async write(buffer: Uint8Array): Promise<void> {
115
+ const position = this.core.position();
116
+ await this.core.fileHandle.writev([buffer], position);
117
+ this.core.seek(position + buffer.length);
118
+ }
119
+
120
+ async writeByte(v: number): Promise<void> {
121
+ await this.write(new Uint8Array([v & 0xff]));
122
+ }
123
+
124
+ async writeShort(v: number): Promise<void> {
125
+ const buffer = new ArrayBuffer(2);
126
+ const view = new DataView(buffer);
127
+ view.setInt16(0, v, false);
128
+ await this.write(new Uint8Array(buffer));
129
+ }
130
+
131
+ async writeLong(v: number): Promise<void> {
132
+ const buffer = new ArrayBuffer(8);
133
+ const view = new DataView(buffer);
134
+ view.setBigInt64(0, BigInt(v), false);
135
+ await this.write(new Uint8Array(buffer));
136
+ }
137
+ }