Skip to content

Commit

Permalink
Separate ledger. Refactor. Optimize locking. Add vacuum.
Browse files Browse the repository at this point in the history
  • Loading branch information
Hexagon committed May 10, 2024
1 parent 1fd34ec commit 34b5e76
Show file tree
Hide file tree
Showing 8 changed files with 609 additions and 329 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,13 @@ await kvStore.close();

- `KV` class
- `open(filepath)`
- `set(key, value, overwrite?)`
- `set(key, value)`
- `get(key)`
- `getMany(key)`
- `delete(key)`
- `beginTransaction()`
- `endTransaction()`
- `vacuum()`
- `close()`
- `KVKey` class (Detail the constructor and methods)
- `KVKeyRange` interface
Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export const LOCK_DEFAULT_MAX_RETRIES = 32;
export const LOCK_DEFAULT_INITIAL_RETRY_INTERVAL_MS = 20; // Increased with itself on each retry, so the actual retry interval is 20, 40, 60 etc. 32 and 20 become about 10 seconds total.
export const LOCK_STALE_TIMEOUT_S = 60_000;

export const SUPPORTED_LEDGER_VERSIONS = ["ALPH"];

export const SYNC_INTERVAL_MS = 1_000;
20 changes: 5 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { KVKey, KVKeyRange } from "./key.ts";
import { type KVFinishedTransaction, KVOperation } from "./transaction.ts";

/**
* Represents content of a node within the KVIndex tree.
Expand Down Expand Up @@ -34,14 +33,11 @@ export class KVIndex {

/**
* Adds an entry to the index.
* @param transaction - The transaction to add
* @throws {Error} If 'overwrite' is false and a duplicate key is found.
*/
add(transaction: KVFinishedTransaction) {
add(key: KVKey, offset: number) {
let current = this.index;
let lastPart;
for (const part of transaction.key.get()) {
lastPart = part;
for (const part of key.get()) {
const currentPart = current.children?.get(part as string | number);
if (currentPart) {
current = currentPart;
Expand All @@ -53,23 +49,17 @@ export class KVIndex {
current = newObj;
}
}
if (current!.reference === undefined) {
current!.reference = transaction.offset;
} else if (transaction.oper === KVOperation.UPSERT) {
current!.reference = transaction.offset;
} else {
throw new Error(`Duplicate key: ${lastPart}`);
}
current!.reference = offset;
}

/**
* Removes an entry from the index based on a provided key.
* @param transaction - The transaction to remove.
* @returns The removed data row reference, or undefined if the key was not found.
*/
delete(transaction: KVFinishedTransaction): number | undefined {
delete(key: KVKey): number | undefined {
let current = this.index;
for (const part of transaction.key.get()) {
for (const part of key.get()) {
const currentPart = current.children.get(part as (string | number));
if (!currentPart || !currentPart.children) { // Key path not found
return undefined;
Expand Down
36 changes: 23 additions & 13 deletions src/kv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ test("KV: set, get and delete (numbers and strings)", async () => {

assertEquals(await kvStore.get(["name"]), null);
assertEquals((await kvStore.get(["age"]))?.data, 30);

await kvStore.close();
});

test("KV: set, get and delete (big numbers)", async () => {
Expand All @@ -33,12 +35,16 @@ test("KV: set, get and delete (big numbers)", async () => {
54645645646546345634523452345234545464,
);

kvStore.close();

const kvStore2 = new KV();
await kvStore2.open(tempFilePrefix);
assertEquals(
(await kvStore2.get(["num", 54645645646546345634523452345234545464]))?.data,
54645645646546345634523452345234545464,
);

kvStore2.close();
});

test("KV: set, get and delete (objects)", async () => {
Expand All @@ -55,6 +61,8 @@ test("KV: set, get and delete (objects)", async () => {

assertEquals(await kvStore.get(["name"]), null);
assertEquals((await kvStore.get(["age"]))?.data, { data: 30 });

await kvStore.close();
});

test("KV: set, get and delete (dates)", async () => {
Expand All @@ -73,20 +81,8 @@ test("KV: set, get and delete (dates)", async () => {
);
await kvStore.delete(["pointintime"]);
assertEquals(await kvStore.get(["pointintime"]), null);
});

test("KV: throws on duplicate key insertion", async () => {
const tempFilePrefix = await tempfile();
const kvStore = new KV();
await kvStore.open(tempFilePrefix);

await kvStore.set(["name"], "Alice");

assertRejects(
async () => await kvStore.set(["name"], "Bob"),
Error,
"Duplicate key: Key already exists",
);
await kvStore.close();
});

test("KV: throws when trying to delete a non-existing key", async () => {
Expand All @@ -98,6 +94,8 @@ test("KV: throws when trying to delete a non-existing key", async () => {
async () => await kvStore.delete(["unknownKey"]),
Error,
); // We don't have a specific error type for this yet

await kvStore.close();
});

test("KV: supports multi-level nested keys", async () => {
Expand All @@ -110,6 +108,8 @@ test("KV: supports multi-level nested keys", async () => {

assertEquals((await kvStore.get(["data", "user", "name"]))?.data, "Alice");
assertEquals((await kvStore.get(["data", "system", "version"]))?.data, 1.2);

await kvStore.close();
});

test("KV: supports multi-level nested keys with numbers", async () => {
Expand All @@ -123,6 +123,8 @@ test("KV: supports multi-level nested keys with numbers", async () => {
assertEquals((await kvStore.get(["data", "user", 4]))?.data, "Alice");
assertEquals((await kvStore.get(["data", "system", 4]))?.data, 1.2);
assertEquals(await kvStore.get(["data", "system", 5]), null);

await kvStore.close();
});

test("KV: supports numeric key ranges", async () => {
Expand All @@ -141,6 +143,8 @@ test("KV: supports numeric key ranges", async () => {
assertEquals(rangeResults[0].data, "Value 7");
assertEquals(rangeResults[1].data, "Value 8");
assertEquals(rangeResults[2].data, "Value 9");

await kvStore.close();
});

test("KV: supports additional levels after numeric key ranges", async () => {
Expand All @@ -164,6 +168,8 @@ test("KV: supports additional levels after numeric key ranges", async () => {
assertEquals(rangeResults[0].data, "Value 7 in doc1");
assertEquals(rangeResults[1].data, "Value 8 in doc1");
assertEquals(rangeResults[2].data, "Value 9 in doc1");

await kvStore.close();
});

test("KV: supports empty numeric key ranges to get all", async () => {
Expand All @@ -186,6 +192,8 @@ test("KV: supports empty numeric key ranges to get all", async () => {
assertEquals(rangeResults3.length, 12);
const rangeResults4 = await kvStore.getMany(["data"]);
assertEquals(rangeResults4.length, 12);

await kvStore.close();
});

test("KV: supports string key ranges", async () => {
Expand All @@ -205,4 +213,6 @@ test("KV: supports string key ranges", async () => {
}]);
assertEquals(rangeResults.length, 2);
assertEquals(rangeResults[0].data, "Document A");

await kvStore.close();
});
Loading

0 comments on commit 34b5e76

Please sign in to comment.