Repository navigation
Closure-captured numeric params read as 0 inside object-literal : Date method (@perryts/mysql MyDateTime.toDate shape) #858
Description
Repro (MySQL-free, no Fastify, 12 lines)
Drop into /tmp/repro.ts, perry compile repro.ts -o repro && ./repro:
function show(label: string, d: Date) {
console.log(label, "getTime=", d.getTime(), " iso=", d.toISOString());
}
function makeDT(y: number) {
return { toDate(): Date {
return new Date(Date.UTC(y, 4, 15, 17, 29, 35, 402)); // captured `y` reads as 0 inside this method
} };
}
const d = makeDT(2026).toDate();
console.log("INLINE:", d.getTime());
show("HELPER:", d);Observed (perry 0.5.914)
INLINE: -62155492224598
HELPER: getTime= -62155492224598 iso= 0000-05-15T17:29:36.598Z
Expected (tsx / node — same source)
INLINE: 1778866175402
HELPER: getTime= 1778866175402 iso= 2026-05-15T17:29:35.402Z
The reconstructed milliseconds -62155492224598 is exactly Date.UTC(0, 4, 15, 17, 29, 35, 402) — i.e. the captured y is read as 0 inside toDate() regardless of the value passed to makeDT(...). All other Date.UTC args are literals and arrive intact; only the closure-captured numeric y is corrupt.
Adding a NaN-guard branch (if (y===0) return new Date(NaN);) confirms it from the other side — makeDT(2026).toDate() returns Invalid Date because the guard sees y === 0.
Where this surfaces in real code
node_modules/@perryts/mysql/src/types/datetime.ts::makeMyDateTime is structurally identical:
function makeMyDateTime(y, mo, d, h, mi, sec, micros, raw) {
return { year:y, ..., toDate(): Date {
if (y===0 && mo===0 && d===0) return new Date(NaN);
return new Date(Date.UTC(y, mo-1, d, h, mi, sec, Math.round(micros/1000)));
}};
}So every DATETIME column decoded by @perryts/mysql produces a MyDateTime whose .toDate() returns either Invalid Date (the all-zero guard wins because every captured field reads as 0) or a year-0 garbage Date. Consumers like skelpo-shop-admin's toUser(row) then either crash in downstream formatters or silently store wrong timestamps. This is the proximate cause of the new SIGBUS in the shop-admin signup path at v0.5.914 (filed separately).
Notes from bisecting
- The bug only fires in certain file contexts — a tighter single-function file (
function makeDT(...); console.log(makeDT(2026).toDate().getTime())with no other declarations) compiles correctly. Adding a second function that declares a: Dateparameter type, or a few other shapes, makes it manifest reliably. So it's a type-analysis interaction across the module, not a property of themakeDTdefinition alone. - Object-literal method with
: Datereturn annotation + closure capture of a numeric outer param is the consistent shape. - The captured non-numeric references (
raw: stringin the realmakeMyDateTime) appear unaffected — only numeric captures zero out. - Counterpart shapes that DO work:
toDate: (): Date => new Date(Date.UTC(y, ...))(arrow-property),toDate: function(): Date { ... }(fn-prop), barefunction f(y): Date { ... }(standalone fn). The bug is specific to the shorthand-method-in-object-literal-with-: Date-return-and-closure-captured-numeric-params combination.
Context
Surfaced while verifying the v0.5.912 / v0.5.914 #748 fixes against the shop-admin native server. The wait_for_promise condvar fix and the Invalid-Date sentinel both landed correctly — confirmed via the previous minimal repros. This is a separate downstream bug in the same : Date / closure-capture region that's exposed once the Invalid-Date path is correct.
Reproduces deterministically at perry 0.5.914 (HEAD e18cdb07). Happy to add PERRY_DEBUG_* traces if useful — the dynamic-dispatch path for Date.UTC(...) inside the method's body is where I'd start looking (whether captured-arg slots get the f64 they should at the call site).
Activity