feat(kv): implement custom inspect for AtomicOperation (#30077)

## Summary

Adds a custom inspect method to `AtomicOperation` to provide a readable
string representation of the queued operations (checks, mutations,
enqueues). This improves developer experience when debugging atomic
operations by showing the internal state in a human-readable format.

The implementation adds a custom `[Symbol.for("Deno.customInspect")]`
method that formats the operations into a structured string showing:
- Check operations with their keys and versionstamps
- Mutation operations (set, delete, sum) with keys and values
- Enqueue operations with payloads and options

Fixes #21034
This commit is contained in:
Luke Swithenbank 2025-08-13 18:09:22 +10:00 committed by GitHub
parent e421451a70
commit b2fd724c46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 259 additions and 0 deletions

View file

@ -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");

View file

@ -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)'));
});