mirror of
https://github.com/denoland/deno.git
synced 2025-09-26 20:29:11 +00:00
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:
parent
e421451a70
commit
b2fd724c46
2 changed files with 259 additions and 0 deletions
117
ext/kv/01_db.ts
117
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");
|
||||
|
|
|
@ -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)'));
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue