diff --git a/ext/kv/01_db.ts b/ext/kv/01_db.ts index 276d9a0e1e..60c7b79160 100644 --- a/ext/kv/01_db.ts +++ b/ext/kv/01_db.ts @@ -16,6 +16,7 @@ import { } from "ext:core/ops"; const { ArrayFrom, + ArrayPrototypeJoin, ArrayPrototypeMap, ArrayPrototypePush, ArrayPrototypeReverse, @@ -567,6 +568,122 @@ class AtomicOperation { "'Deno.AtomicOperation' is not a promise: did you forget to call 'commit()'", ); } + + [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { + const operations = []; + + // Format checks + for (let i = 0; i < this.#checks.length; ++i) { + const check = this.#checks[i]; + const key = check[0]; + const versionstamp = check[1]; + const keyStr = inspect(key, inspectOptions); + const versionstampStr = versionstamp === null + ? "null" + : `"${versionstamp}"`; + ArrayPrototypePush( + operations, + ` check({ key: ${keyStr}, versionstamp: ${versionstampStr} })`, + ); + } + + // Format mutations + for (let i = 0; i < this.#mutations.length; ++i) { + const mutation = this.#mutations[i]; + const key = mutation[0]; + const type = mutation[1]; + const rawValue = mutation[2]; + const expireIn = mutation[3]; + const keyStr = inspect(key, inspectOptions); + + if (type === "delete") { + ArrayPrototypePush(operations, ` delete(${keyStr})`); + } else { + // Deserialize value for display + let value; + try { + if (rawValue === null) { + value = null; + } else { + switch (rawValue.kind) { + case "v8": + value = core.deserialize(rawValue.value, { forStorage: true }); + break; + case "bytes": + value = rawValue.value; + break; + case "u64": + value = new KvU64(rawValue.value); + break; + default: + value = rawValue; + } + } + } catch { + // If deserialization fails, show the raw value structure + value = `[${rawValue?.kind || "unknown"} value]`; + } + + const valueStr = inspect(value, inspectOptions); + + if (type === "set" && expireIn !== undefined) { + ArrayPrototypePush( + operations, + ` set(${keyStr}, ${valueStr}, { expireIn: ${expireIn} })`, + ); + } else { + ArrayPrototypePush(operations, ` ${type}(${keyStr}, ${valueStr})`); + } + } + } + + // Format enqueues + for (let i = 0; i < this.#enqueues.length; ++i) { + const enqueue = this.#enqueues[i]; + const serializedMessage = enqueue[0]; + const delay = enqueue[1]; + const keysIfUndelivered = enqueue[2]; + const backoffSchedule = enqueue[3]; + + // Deserialize message for display + let message; + try { + message = core.deserialize(serializedMessage, { forStorage: true }); + } catch { + message = "[serialized message]"; + } + + const messageStr = inspect(message, inspectOptions); + + if ( + delay === 0 && keysIfUndelivered.length === 0 && + backoffSchedule === null + ) { + ArrayPrototypePush(operations, ` enqueue(${messageStr})`); + } else { + const options = []; + if (delay !== 0) ArrayPrototypePush(options, `delay: ${delay}`); + if (keysIfUndelivered.length > 0) { + const keysStr = inspect(keysIfUndelivered, inspectOptions); + ArrayPrototypePush(options, `keysIfUndelivered: ${keysStr}`); + } + if (backoffSchedule !== null) { + const scheduleStr = inspect(backoffSchedule, inspectOptions); + ArrayPrototypePush(options, `backoffSchedule: ${scheduleStr}`); + } + ArrayPrototypePush( + operations, + ` enqueue(${messageStr}, { ${ArrayPrototypeJoin(options, ", ")} })`, + ); + } + } + + if (operations.length === 0) { + return "AtomicOperation (empty)"; + } + + return `AtomicOperation\n${ArrayPrototypeJoin(operations, "\n")}`; + } } const MIN_U64 = BigInt("0"); diff --git a/tests/unit/kv_test.ts b/tests/unit/kv_test.ts index 47e1305c94..c2ce1d2e16 100644 --- a/tests/unit/kv_test.ts +++ b/tests/unit/kv_test.ts @@ -2319,3 +2319,145 @@ Deno.test({ await completion; }, }); + +// AtomicOperation custom inspect tests +dbTest("AtomicOperation custom inspect - empty operation", (db) => { + const atomic = db.atomic(); + const inspected = Deno.inspect(atomic); + assertEquals(inspected, "AtomicOperation (empty)"); +}); + +dbTest("AtomicOperation custom inspect - with check operations", (db) => { + const atomic = db.atomic() + .check({ key: ["users", "alice"], versionstamp: "version123" }) + .check({ key: ["posts", 42], versionstamp: null }); + + const inspected = Deno.inspect(atomic); + assert(inspected.includes("AtomicOperation")); + assert( + inspected.includes( + 'check({ key: [ "users", "alice" ], versionstamp: "version123" })', + ), + ); + assert( + inspected.includes('check({ key: [ "posts", 42 ], versionstamp: null })'), + ); +}); + +dbTest("AtomicOperation custom inspect - with mutations", (db) => { + const atomic = db.atomic() + .set(["users", "bob"], { name: "Bob", age: 30 }) + .set(["temp", "data"], "temporary", { expireIn: 60000 }) + .delete(["old", "record"]) + .sum(["counters", "visits"], 5n); + + const inspected = Deno.inspect(atomic); + assert(inspected.includes("AtomicOperation")); + assert( + inspected.includes('set([ "users", "bob" ], { name: "Bob", age: 30 })'), + ); + assert( + inspected.includes( + 'set([ "temp", "data" ], "temporary", { expireIn: 60000 })', + ), + ); + assert(inspected.includes('delete([ "old", "record" ])')); + assert(inspected.includes('sum([ "counters", "visits" ], [Deno.KvU64: 5n])')); +}); + +dbTest("AtomicOperation custom inspect - with enqueue operations", (db) => { + const atomic = db.atomic() + .enqueue({ type: "email", to: "user@example.com" }) + .enqueue({ type: "reminder" }, { delay: 3600000 }) + .enqueue( + { type: "notification" }, + { keysIfUndelivered: [["failed_notifications", "batch1"]] }, + ) + .enqueue( + { type: "retry_task" }, + { + delay: 1000, + backoffSchedule: [1000, 2000, 4000], + keysIfUndelivered: [["failed_tasks"]], + }, + ); + + const inspected = Deno.inspect(atomic); + assert(inspected.includes("AtomicOperation")); + assert( + inspected.includes('enqueue({ type: "email", to: "user@example.com" })'), + ); + assert( + inspected.includes('enqueue({ type: "reminder" }, { delay: 3600000 })'), + ); + assert( + inspected.includes( + 'keysIfUndelivered: [ [ "failed_notifications", "batch1" ] ]', + ), + ); + assert(inspected.includes("backoffSchedule: [ 1000, 2000, 4000 ]")); +}); + +dbTest("AtomicOperation custom inspect - complex operation", (db) => { + const atomic = db.atomic() + .check({ key: ["users", "alice"], versionstamp: "v1" }) + .set(["users", "alice"], { name: "Alice Updated", version: 2 }) + .sum(["stats", "user_updates"], 1n) + .delete(["cache", "user_alice"]) + .enqueue({ type: "user_updated", userId: "alice" }); + + const inspected = Deno.inspect(atomic); + + // Verify the output contains all expected operations + assert(inspected.includes("AtomicOperation")); + assert(inspected.includes("check(")); + assert(inspected.includes("set(")); + assert(inspected.includes("sum(")); + assert(inspected.includes("delete(")); + assert(inspected.includes("enqueue(")); + + // Verify operations appear in the correct format + assert( + inspected.includes( + 'check({ key: [ "users", "alice" ], versionstamp: "v1" })', + ), + ); + assert( + inspected.includes( + 'set([ "users", "alice" ], { name: "Alice Updated", version: 2 })', + ), + ); + assert( + inspected.includes('sum([ "stats", "user_updates" ], [Deno.KvU64: 1n])'), + ); + assert(inspected.includes('delete([ "cache", "user_alice" ])')); + assert( + inspected.includes('enqueue({ type: "user_updated", userId: "alice" })'), + ); + + // Verify the structure - should start with "AtomicOperation" and have operations indented + const lines = inspected.split("\n"); + assertEquals(lines[0], "AtomicOperation"); + // Each operation should be indented with 2 spaces + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim()) { // Skip empty lines + assert(lines[i].startsWith(" ")); // Should start with indentation + } + } +}); + +dbTest("AtomicOperation custom inspect - handles special values", (db) => { + const uint8Array = new Uint8Array([1, 2, 3, 4]); + const atomic = db.atomic() + .set(["bytes"], uint8Array) + .set(["null"], null) + .set(["undefined"], undefined) + .set(["bigint"], 9007199254740991n); + + const inspected = Deno.inspect(atomic); + assert(inspected.includes("AtomicOperation")); + assert(inspected.includes('set([ "bytes" ], Uint8Array(4) [ 1, 2, 3, 4 ])')); + assert(inspected.includes('set([ "null" ], null)')); + assert(inspected.includes('set([ "undefined" ], undefined)')); + assert(inspected.includes('set([ "bigint" ], 9007199254740991n)')); +});