Skip to content

Commit a1bd030

Browse files
committed
Add transactions and transaction log
1 parent d4781f6 commit a1bd030

File tree

11 files changed

+502
-193
lines changed

11 files changed

+502
-193
lines changed

README.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
**cross/kv**
22

3-
## Efficient cross-runtime Key/Value database for JavaScript and TypeScript
3+
A cross-platform, hierarchical Key/Value database for JavaScript and TypeScript,
4+
that work in all major runtimes (Node.js, Deno and Bun).
45

56
### **Features**
67

78
- **Simple Key/Value Storage:** Store and retrieve data easily using
89
hierarchical keys.
9-
- **Cross-Runtime Compatibility:** Works in Node.js, Deno, browser environments,
10-
and potentially other JavaScript runtimes.
11-
- **Flexible Data Types:** Support for strings, numbers, objects, dates, and
12-
more.
10+
- **Cross-Runtime Compatibility:** Works in Node.js, Deno and Bun.
11+
- **Flexible Data Types:** Support for strings, numbers, objects, dates, maps,
12+
sets and more.
1313
- **Hierarchical Structure:** Organize data with multi-level keys for a logical
1414
structure.
15-
- **Key Ranges:** Retrieve ranges of data efficiently using `KVKeyRange`
16-
objects.
17-
- **Indexed:** Data is indexed to provide faster lookups (detail how your
18-
indexing works).
15+
- **Key Ranges:** Retrieve ranges of data efficiently using key ranges.
16+
- **Indexed:** In-memory index to provide faster lookups of large datasets.
1917

2018
### **Installation**
2119

@@ -62,7 +60,7 @@ import { CrossKV } from "@cross/kv";
6260
const kvStore = new CrossKV();
6361

6462
// Open the database
65-
await kvStore.open("./lab/db19");
63+
await kvStore.open("./mydatabase/");
6664

6765
// Store some values/documents indexed by users.by_id.<id>
6866
await kvStore.set(["users", "by_id", 1], {

deno.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
{
22
"name": "@cross/kv",
3-
"version": "0.0.3",
3+
"version": "0.0.4",
44
"exports": {
55
".": "./mod.ts"
66
},
77
"imports": {
88
"@cross/fs": "jsr:@cross/fs@^0.1.11",
9+
"@cross/runtime": "jsr:@cross/runtime@^1.0.0",
910
"@cross/test": "jsr:@cross/test@^0.0.9",
1011
"@std/assert": "jsr:@std/assert@^0.224.0",
1112
"@std/path": "jsr:@std/path@^0.224.0",
1213
"cbor-x": "npm:cbor-x@^1.5.9"
1314
},
1415
"publish": {
15-
"exclude": [".github", "*.test.ts"]
16+
"exclude": [".github", "*.test.ts", "*.bench.ts"]
1617
},
1718
"tasks": {
1819
"check": "deno fmt --check && deno lint && deno check mod.ts && deno doc --lint mod.ts && deno run -A mod.ts --slim --ignore-unused",
20+
"bench": "deno bench -A --unstable-kv",
1921
"check-deps": "deno run -rA jsr:@check/deps"
2022
}
2123
}

mydatabase/.data

Whitespace-only changes.

mydatabase/.idx

Whitespace-only changes.

src/index.test.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/index.ts

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,46 @@
1-
import { exists, readFile, writeFile } from "@cross/fs";
21
import type { KVKey, KVKeyRange } from "./key.ts";
3-
import { decode, encode } from "cbor-x";
2+
import { type KVFinishedTransaction, KVOperation } from "./transaction.ts";
43

5-
// Nested class to represent a node in the index tree
4+
/**
5+
* Represents content of a node within the KVIndex tree.
6+
*/
67
interface KVIndexContent {
7-
children: KVIndexNode;
8+
/**
9+
* Holds references to child nodes in the index.
10+
*/
11+
children: KVIndexNodes;
12+
/**
13+
* Optional reference to a data offset. Present for leaf nodes.
14+
*/
815
reference?: number;
916
}
1017

11-
type KVIndexNode = Map<string | number, KVIndexContent>;
18+
/**
19+
* A Map containing child keys and their corresponding Node within the tree.
20+
*/
21+
type KVIndexNodes = Map<string | number, KVIndexContent>;
1222

23+
/**
24+
* In-memory representation of a Key-Value index enabling efficient key-based lookups.
25+
* It uses a tree-like structure for fast prefix and range-based searches.
26+
*/
1327
export class KVIndex {
14-
private indexPath: string;
1528
private index: KVIndexContent;
16-
private isDirty: boolean = false;
17-
constructor(indexPath: string) {
18-
this.indexPath = indexPath;
29+
constructor() {
1930
this.index = {
2031
children: new Map(),
2132
};
2233
}
2334

24-
add(key: KVKey, entry: number, overwrite: boolean = false) {
35+
/**
36+
* Adds an entry to the index.
37+
* @param transaction - The transaction to add
38+
* @throws {Error} If 'overwrite' is false and a duplicate key is found.
39+
*/
40+
add(transaction: KVFinishedTransaction) {
2541
let current = this.index;
2642
let lastPart;
27-
for (const part of key.get()) {
43+
for (const part of transaction.key.get()) {
2844
lastPart = part;
2945
const currentPart = current.children?.get(part as string | number);
3046
if (currentPart) {
@@ -38,19 +54,22 @@ export class KVIndex {
3854
}
3955
}
4056
if (current!.reference === undefined) {
41-
current!.reference = entry;
42-
} else if (overwrite) {
43-
/* ToDo: Some sort of callback if overwritten? */
44-
current!.reference = entry;
57+
current!.reference = transaction.offset;
58+
} else if (transaction.oper === KVOperation.UPSERT) {
59+
current!.reference = transaction.offset;
4560
} else {
4661
throw new Error(`Duplicate key: ${lastPart}`);
4762
}
48-
this.isDirty = true;
4963
}
5064

51-
delete(key: KVKey): number | undefined {
65+
/**
66+
* Removes an entry from the index based on a provided key.
67+
* @param transaction - The transaction to remove.
68+
* @returns The removed data row reference, or undefined if the key was not found.
69+
*/
70+
delete(transaction: KVFinishedTransaction): number | undefined {
5271
let current = this.index;
53-
for (const part of key.get()) {
72+
for (const part of transaction.key.get()) {
5473
const currentPart = current.children.get(part as (string | number));
5574
if (!currentPart || !currentPart.children) { // Key path not found
5675
return undefined;
@@ -62,15 +81,17 @@ export class KVIndex {
6281
const oldReference = current.reference;
6382
current.reference = undefined;
6483
delete current.reference;
65-
this.isDirty = true;
66-
67-
/* ToDo recursive cleanup if (!current.children.size) {
68-
delete current.children;
69-
}*/
7084

85+
// No need to cleanup as leftover nodes will be lost at next rebuild
7186
return oldReference;
7287
}
7388

89+
/**
90+
* Retrieves a list of data row references associated with a given key.
91+
* Supports prefix and range-based searches.
92+
* @param key - The key to search for (can include ranges)
93+
* @returns An array of data row references.
94+
*/
7495
get(key: KVKey): number[] {
7596
const resultSet: number[] = [];
7697

@@ -119,23 +140,4 @@ export class KVIndex {
119140

120141
return resultSet;
121142
}
122-
123-
async loadIndex(): Promise<KVIndexContent> {
124-
if (await exists(this.indexPath)) {
125-
const fileContents = await readFile(this.indexPath);
126-
try {
127-
const index = decode(fileContents);
128-
this.index = index as KVIndexContent;
129-
this.isDirty = false;
130-
} catch (_e) { /* Ignore for now */ }
131-
}
132-
return this.index;
133-
}
134-
135-
async saveIndex(): Promise<void> {
136-
if (!this.isDirty) return;
137-
const serializedIndex = encode(this.index);
138-
await writeFile(this.indexPath, serializedIndex);
139-
this.isDirty = false;
140-
}
141143
}

src/kv.bench.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { tempfile } from "@cross/fs";
2+
import { CrossKV } from "./kv.ts";
3+
4+
const DATABASE_FILE_CROSS = await tempfile();
5+
const DATABASE_FILE_DENO = await tempfile();
6+
7+
// Your setup and cleanup functions
8+
async function setupCrossKV() {
9+
const kvStore = new CrossKV();
10+
await kvStore.open(DATABASE_FILE_CROSS);
11+
return kvStore;
12+
}
13+
14+
async function setupDenoKV() {
15+
const kvStore = await Deno.openKv(DATABASE_FILE_DENO);
16+
return kvStore;
17+
}
18+
19+
/*async function cleanup() {
20+
denoStore.close();
21+
await crossStore.close();
22+
}*/
23+
24+
const crossStore = await setupCrossKV();
25+
const denoStore = await setupDenoKV();
26+
let crossIter = 0;
27+
let denoIter = 0;
28+
29+
await Deno.bench("cross_kv_set", async () => {
30+
await crossStore.set(["testKey", crossIter++], { data: "testData" });
31+
});
32+
33+
await Deno.bench("deno_kv_set", async () => {
34+
await denoStore.set(["testKey", denoIter++], { data: "testData" });
35+
});
36+
37+
await Deno.bench("cross_kv_get", async () => {
38+
await crossStore.get(["testKey", crossIter--]);
39+
});
40+
41+
await Deno.bench("deno_kv_get", async () => {
42+
await denoStore.get(["testKey", crossIter--]);
43+
});
44+
45+
//await cleanup();

src/kv.test.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { assertEquals, assertNotEquals, assertRejects } from "@std/assert";
22
import { test } from "@cross/test";
33
import { CrossKV } from "./kv.ts";
4+
import { tempfile } from "@cross/fs";
45

56
test("CrossKV: set, get and delete (numbers and strings)", async () => {
6-
const tempFilePrefix = await Deno.makeTempFile();
7+
const tempFilePrefix = await tempfile();
78
const kvStore = new CrossKV();
89
await kvStore.open(tempFilePrefix);
910
await kvStore.set(["name"], "Alice");
@@ -19,7 +20,7 @@ test("CrossKV: set, get and delete (numbers and strings)", async () => {
1920
});
2021

2122
test("CrossKV: set, get and delete (objects)", async () => {
22-
const tempFilePrefix = await Deno.makeTempFile();
23+
const tempFilePrefix = await tempfile();
2324
const kvStore = new CrossKV();
2425
await kvStore.open(tempFilePrefix);
2526
await kvStore.set(["name"], { data: "Alice" });
@@ -35,7 +36,7 @@ test("CrossKV: set, get and delete (objects)", async () => {
3536
});
3637

3738
test("CrossKV: set, get and delete (dates)", async () => {
38-
const tempFilePrefix = await Deno.makeTempFile();
39+
const tempFilePrefix = await tempfile();
3940
const kvStore = new CrossKV();
4041
await kvStore.open(tempFilePrefix);
4142
const date = new Date();
@@ -50,7 +51,7 @@ test("CrossKV: set, get and delete (dates)", async () => {
5051
});
5152

5253
test("CrossKV: throws on duplicate key insertion", async () => {
53-
const tempFilePrefix = await Deno.makeTempFile();
54+
const tempFilePrefix = await tempfile();
5455
const kvStore = new CrossKV();
5556
await kvStore.open(tempFilePrefix);
5657

@@ -59,23 +60,23 @@ test("CrossKV: throws on duplicate key insertion", async () => {
5960
assertRejects(
6061
async () => await kvStore.set(["name"], "Bob"),
6162
Error,
62-
"Duplicate key: name",
63+
"Duplicate key: Key already exists",
6364
);
6465
});
6566

6667
test("CrossKV: throws when trying to delete a non-existing key", async () => {
67-
const tempFilePrefix = await Deno.makeTempFile();
68+
const tempFilePrefix = await tempfile();
6869
const kvStore = new CrossKV();
6970
await kvStore.open(tempFilePrefix);
7071

71-
assertRejects(
72-
() => kvStore.delete(["unknownKey"]),
72+
await assertRejects(
73+
async () => await kvStore.delete(["unknownKey"]),
7374
Error,
7475
); // We don't have a specific error type for this yet
7576
});
7677

7778
test("CrossKV: supports multi-level nested keys", async () => {
78-
const tempFilePrefix = await Deno.makeTempFile();
79+
const tempFilePrefix = await tempfile();
7980
const kvStore = new CrossKV();
8081
await kvStore.open(tempFilePrefix);
8182

@@ -87,7 +88,7 @@ test("CrossKV: supports multi-level nested keys", async () => {
8788
});
8889

8990
test("CrossKV: supports multi-level nested keys with numbers", async () => {
90-
const tempFilePrefix = await Deno.makeTempFile();
91+
const tempFilePrefix = await tempfile();
9192
const kvStore = new CrossKV();
9293
await kvStore.open(tempFilePrefix);
9394

@@ -100,7 +101,7 @@ test("CrossKV: supports multi-level nested keys with numbers", async () => {
100101
});
101102

102103
test("CrossKV: supports numeric key ranges", async () => {
103-
const tempFilePrefix = await Deno.makeTempFile();
104+
const tempFilePrefix = await tempfile();
104105
const kvStore = new CrossKV();
105106
await kvStore.open(tempFilePrefix);
106107

@@ -119,7 +120,7 @@ test("CrossKV: supports numeric key ranges", async () => {
119120
});
120121

121122
test("CrossKV: supports string key ranges", async () => {
122-
const tempFilePrefix = await Deno.makeTempFile();
123+
const tempFilePrefix = await tempfile();
123124
const kvStore = new CrossKV();
124125
await kvStore.open(tempFilePrefix);
125126

0 commit comments

Comments
 (0)