Skip to content

Commit 27957d1

Browse files
committed
Cleanup
1 parent 86b5b25 commit 27957d1

File tree

10 files changed

+148
-35
lines changed

10 files changed

+148
-35
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
## Unrelesed
1+
## 0.15.11
2+
3+
- Remove option doLock from `.sync()`
4+
- Remove unused code from `key.ts`
5+
- Add benchmark for getting data that does not exist
6+
- Rename internal function `toAbsolutePath` to `toNormalizedAbsolutePath` and
7+
clarify code with additional comments
8+
- Adds various missing test to reduce the risk for regression bugs
29

310
## 0.15.10
411

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cross/kv",
3-
"version": "0.15.10",
3+
"version": "0.15.11",
44
"exports": {
55
".": "./mod.ts",
66
"./cli": "./src/cli/mod.ts"

src/lib/key.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,13 @@ export class KVKeyInstance {
6767
private key: KVQuery | KVKey;
6868
private isQuery: boolean;
6969
public byteLength?: number;
70-
hasData: boolean = false;
7170
constructor(
72-
key: KVQuery | KVKey | Uint8Array | DataView,
71+
key: KVQuery | KVKey | DataView,
7372
isQuery: boolean = false,
7473
validate: boolean = true,
7574
) {
76-
if (key instanceof Uint8Array) {
77-
this.key = this.fromUint8Array(
78-
new DataView(key.buffer, key.byteOffset, key.byteLength),
79-
);
80-
this.hasData = true;
81-
} else if (key instanceof DataView) {
75+
if (key instanceof DataView) {
8276
this.key = this.fromUint8Array(key);
83-
this.hasData = true;
8477
} else {
8578
this.key = key;
8679
}
@@ -134,6 +127,7 @@ export class KVKeyInstance {
134127
keyArray.set(bytes, keyOffset);
135128
keyOffset += bytes.length;
136129
}
130+
137131
return keyArray;
138132
}
139133

@@ -225,6 +219,15 @@ export class KVKeyInstance {
225219
'Ranges must have only "from" and/or "to" keys',
226220
);
227221
}
222+
223+
// Check for not mixing number from with string to and vice versa
224+
if (
225+
(typeof element.from === "number" &&
226+
typeof element.to === "string") ||
227+
(typeof element.from === "string" && typeof element.to === "number")
228+
) {
229+
throw new TypeError("Cannot mix string and number in ranges");
230+
}
228231
}
229232

230233
if (

src/lib/kv.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -291,26 +291,20 @@ export class KV extends EventEmitter {
291291
* - Automatically run on adding data
292292
* - Can be manually triggered for full consistency before data retrieval (iterate(), listAll(), get())
293293
*
294-
* @param doLock - (Optional) Locks the database before synchronization. Defaults to true. Always true unless called internally.
295-
*
296294
* @emits sync - Emits an event with the synchronization result:
297295
* - `result`: "ready" | "blocked" | "success" | "ledgerInvalidated" | "error"
298296
* - `error`: Error object (if an error occurred) or null
299297
*
300298
* @throws {Error} If an unexpected error occurs during synchronization.
301299
*/
302-
public async sync(doLock = false): Promise<KVSyncResult> {
300+
public async sync(): Promise<KVSyncResult> {
303301
// Throw if database isn't open
304302
this.ensureOpen();
305303

306304
// Synchronization Logic (with lock if needed)
307305
let result: KVSyncResult["result"] = "ready";
308306
let error: Error | null = null;
309-
let lockSucceeded = false; // Keeping track as we only want to unlock the database later, if the locking operation succeeded
310307
try {
311-
if (doLock) await this.ledger?.lock();
312-
lockSucceeded = true;
313-
314308
const newTransactions = await this.ledger?.sync(this.disableIndex);
315309

316310
if (newTransactions === null) { // Ledger invalidated
@@ -336,7 +330,6 @@ export class KV extends EventEmitter {
336330
result = "error";
337331
error = new Error("Error during ledger sync", { cause: syncError });
338332
} finally {
339-
if (doLock && lockSucceeded) await this.ledger?.unlock();
340333
// @ts-ignore .emit exists
341334
this.emit("sync", { result, error });
342335
}

src/lib/ledger.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
ensureFile,
33
rawOpen,
44
readAtPosition,
5-
toAbsolutePath,
5+
toNormalizedAbsolutePath,
66
writeAtPosition,
77
} from "./utils/file.ts";
88
import {
@@ -71,7 +71,7 @@ export class KVLedger {
7171
};
7272

7373
constructor(filePath: string, maxCacheSizeMBytes: number) {
74-
this.dataPath = toAbsolutePath(filePath);
74+
this.dataPath = toNormalizedAbsolutePath(filePath);
7575
this.cache = new KVLedgerCache(maxCacheSizeMBytes * 1024 * 1024);
7676
}
7777

src/lib/transaction.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,9 @@ export class KVTransaction {
133133
this.operation = operation;
134134
this.timestamp = timestamp;
135135
if (value) {
136-
this.data = new Uint8Array(encode(value));
137-
this.hash = await sha1(this.data!);
136+
const valueData = new Uint8Array(encode(value));
137+
this.data = valueData;
138+
this.hash = await sha1(valueData);
138139
}
139140
}
140141

src/lib/utils/file.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ import { CurrentRuntime, Runtime } from "@cross/runtime";
33
import { cwd, isDir, isFile, mkdir } from "@cross/fs";
44
import { dirname, isAbsolute, join, resolve } from "@std/path";
55

6-
export function toAbsolutePath(filename: string): string {
6+
export function toNormalizedAbsolutePath(filename: string): string {
77
let filePath;
88
if (isAbsolute(filename)) {
9+
// Even if the input is already an absolute path, it might not be normalized:
10+
// - It could contain ".." or "." segments that need to be resolved.
11+
// - It might have extra separators that need to be cleaned up.
12+
// - It might not use the correct separator for the current OS.
913
filePath = resolve(filename);
1014
} else {
15+
// If the input is relative, combine it with the current working directory
16+
// and then resolve to get a fully normalized absolute path.
1117
filePath = resolve(join(cwd(), filename));
1218
}
1319
return filePath;

test/key.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,98 @@ test("KVKeyInstance: parse error on invalid range format", () => {
212212
KVKeyInstance.parse("users.>=abc.<=123", false);
213213
}, TypeError);
214214
});
215+
216+
test("KVKeyInstance: Throws if trying to use something other than an array for a key", () => {
217+
assertThrows(
218+
() => new KVKeyInstance("users.data.user123" as unknown as KVKey),
219+
TypeError,
220+
"Key must be an array",
221+
);
222+
});
223+
224+
test("KVKeyInstance: Throws if trying to use something other than a string or number for a key element", () => {
225+
assertThrows(
226+
// @ts-ignore expected to be wrong
227+
() => new KVKeyInstance(["users", null]),
228+
TypeError,
229+
"Key ranges are only allowed in queries",
230+
);
231+
});
232+
233+
test("KVKeyInstance: Throws if trying to use an object with extra properties for a key element", () => {
234+
assertThrows(
235+
// @ts-ignore extected to be wrong
236+
() => new KVKeyInstance(["users", { from: 100, extra: "value" }], true),
237+
TypeError,
238+
"Ranges must have only",
239+
);
240+
});
241+
242+
test("KVKeyInstance: Throws if trying to mix strings and numbers in a range for a key element", () => {
243+
assertThrows(
244+
() => new KVKeyInstance(["users", { from: "abc", to: 123 }], true),
245+
TypeError,
246+
"Cannot mix string and number in ranges",
247+
);
248+
});
249+
250+
test("KVKeyInstance: Throws if trying to use an object with invalid range values for a key element", () => {
251+
assertThrows(
252+
() => new KVKeyInstance(["users", { from: 123, to: "abc" }], true, true),
253+
TypeError,
254+
"Cannot mix string and number in ranges",
255+
);
256+
});
257+
258+
test("KVKeyInstance: throws when parsing a key with empty elements", () => {
259+
assertThrows(
260+
() => KVKeyInstance.parse("users..profile", false),
261+
TypeError,
262+
"Key ranges are only allowed in queries",
263+
);
264+
});
265+
266+
test("KVKeyInstance: stringify and parse with empty elements", () => {
267+
// Empty object represents an empty element
268+
const queryWithEmpty: KVQuery = ["users", {}, "profile"];
269+
const queryWithRangeAndEmpty: KVQuery = ["data", { from: 10, to: 20 }, {}];
270+
271+
const parsedKey = KVKeyInstance.parse("users..profile", true);
272+
assertEquals(parsedKey, queryWithEmpty);
273+
274+
const queryInstance = new KVKeyInstance(queryWithRangeAndEmpty, true);
275+
assertEquals(queryInstance.stringify(), "data.>=#10<=#20.");
276+
277+
const parsedQuery = KVKeyInstance.parse("data.>=#10<=#20.", true);
278+
assertEquals(parsedQuery, queryWithRangeAndEmpty);
279+
});
280+
281+
test("KVKeyInstance: parse with leading dots should throw", () => {
282+
assertThrows(
283+
() => KVKeyInstance.parse(`.data.>=a<=z`, false),
284+
TypeError,
285+
"Ranges are not allowed in keys.",
286+
);
287+
});
288+
289+
test("KVKeyInstance: stringify and parse mixed-type keys", () => {
290+
const mixedKey: KVKey = ["users", 123, "profile"];
291+
const instance = new KVKeyInstance(mixedKey);
292+
293+
const stringified = instance.stringify();
294+
assertEquals(stringified, "users.#123.profile");
295+
296+
const parsed = KVKeyInstance.parse(stringified, false);
297+
assertEquals(parsed, mixedKey);
298+
});
299+
300+
test("KVKeyInstance: toUint8Array and fromUint8Array roundtrip", () => {
301+
const key: KVKey = ["users", "john_doe", 42];
302+
const instance = new KVKeyInstance(key);
303+
const uint8Array = instance.toUint8Array();
304+
305+
const dataView = new DataView(uint8Array.buffer);
306+
const newKeyInstance = new KVKeyInstance(dataView);
307+
308+
assertEquals(newKeyInstance.get(), key);
309+
});

test/kv.bench.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,15 @@ async function setupDenoKV() {
1818

1919
const crossStore = await setupKV();
2020
const denoStore = await setupDenoKV();
21-
let crossIterTransaction = 0;
22-
let denoIterTransaction = 0;
2321

2422
await Deno.bench("cross_kv_set_100_atomic", async () => {
2523
await crossStore.beginTransaction();
2624
for (let i = 0; i < 100; i++) {
27-
await crossStore.set(["testKey", crossIterTransaction++], {
25+
const randomUUID = crypto.randomUUID();
26+
await crossStore.set(["testKey", randomUUID], {
2827
data: {
2928
data: "testData",
30-
i: crossIterTransaction,
29+
i: randomUUID,
3130
more: {
3231
"test": "data",
3332
"with1": new Date(),
@@ -47,10 +46,11 @@ await Deno.bench("cross_kv_set_100_atomic", async () => {
4746
await Deno.bench("deno_kv_set_100_atomic", async () => {
4847
const at = denoStore.atomic();
4948
for (let i = 0; i < 100; i++) {
50-
at.set(["testKey", denoIterTransaction++], {
49+
const randomUUID = crypto.randomUUID();
50+
at.set(["testKey", randomUUID], {
5151
data: {
5252
data: "testData",
53-
i: denoIterTransaction,
53+
i: randomUUID,
5454
more: {
5555
"test": "data",
5656
"with1": new Date(),
@@ -108,9 +108,17 @@ await Deno.bench("deno_kv_set", async () => {
108108
});
109109

110110
await Deno.bench("cross_kv_get", async () => {
111-
await crossStore.get(["testKey", 3]);
111+
await crossStore.get(["testKey2", 3]);
112112
});
113113

114114
await Deno.bench("deno_kv_get", async () => {
115-
await denoStore.get(["testKey", 3]);
115+
await denoStore.get(["testKey2", 3]);
116+
});
117+
118+
await Deno.bench("cross_kv_get_nonexisting", async () => {
119+
await crossStore.get(["testKey2", "eh"]);
120+
});
121+
122+
await Deno.bench("deno_kv_get_nonexisting", async () => {
123+
await denoStore.get(["testKey2", "eh"]);
116124
});

test/kv.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ test("KV: watch functionality - basic matching", async () => {
427427
});
428428

429429
await kvStore.set(watchedKey, { name: "Alice", age: 30 });
430-
await kvStore.sync(true); // Manual sync to trigger the watch callback
430+
await kvStore.sync();
431431

432432
assertEquals(receivedTransaction!.key, watchedKey);
433433
assertEquals(receivedTransaction!.data, { name: "Alice", age: 30 });
@@ -451,7 +451,7 @@ test("KV: watch functionality - recursive matching", async () => {
451451
await kvStore.set(["users", "user1"], "Alice");
452452
await kvStore.set(["users", "user2"], "Bob");
453453
await kvStore.set(["data", "other"], "Not watched");
454-
await kvStore.sync(true); // Not needed, but trigger to ensure no duplicate calls occurr
454+
await kvStore.sync();
455455

456456
assertEquals(receivedTransactions.length, 2);
457457
assertEquals(receivedTransactions[0].data, "Alice");
@@ -537,7 +537,7 @@ test("KV: watch functionality - no match", async () => {
537537

538538
// Add data under a different key
539539
await kvStore.set(["data", "something"], "else");
540-
await kvStore.sync(true);
540+
await kvStore.sync();
541541

542542
assertEquals(callbackCalled, false, "Callback should not have been called");
543543

0 commit comments

Comments
 (0)