Skip to content

Commit d37d5ae

Browse files
committed
fix(core): keep large numeric attribute keys as an object when unflattening
An object whose keys are all large numbers (for example millisecond timestamps) was being turned into an array, so Array(maxIndex + 1) threw "Invalid array length" (or allocated a huge array). Only rebuild an array when the keys are real array indices (< 2^32 - 1), otherwise return the object as-is.
1 parent fbd6df3 commit d37d5ae

3 files changed

Lines changed: 38 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/core": patch
3+
---
4+
5+
Fix a crash when unflattening attributes that hold an object whose keys are all large numbers, such as millisecond timestamps. Those values now come back as an object instead of throwing an "Invalid array length" error.

packages/core/src/v3/utils/flattenAttributes.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export const CIRCULAR_REFERENCE_SENTINEL = "$@circular((";
55

66
const DEFAULT_MAX_DEPTH = 128;
77

8+
// The largest value a JS array length can hold. A numeric key at or above this
9+
// is not a real array index, so we never try to build an array that big.
10+
const MAX_ARRAY_INDEX = 2 ** 32 - 1;
11+
812
// This property name would let a crafted key walk into Object.prototype during
913
// reconstruction and pollute the shared process.
1014
const PROTOTYPE_POLLUTION_KEY = "__proto__";
@@ -342,13 +346,20 @@ export function unflattenAttributes(
342346
// Convert the result to an array if all top-level keys are numeric indices.
343347
// Guard against an empty result (e.g. every key was skipped as unsafe), which
344348
// would otherwise produce Array(-Infinity) and throw.
345-
if (Object.keys(result).length > 0 && Object.keys(result).every((k) => /^\d+$/.test(k))) {
346-
const maxIndex = Math.max(...Object.keys(result).map((k) => parseInt(k)));
347-
const arrayResult = Array(maxIndex + 1);
348-
for (const key in result) {
349-
arrayResult[parseInt(key)] = result[key];
349+
const topLevelKeys = Object.keys(result);
350+
if (topLevelKeys.length > 0 && topLevelKeys.every((k) => /^\d+$/.test(k))) {
351+
const maxIndex = topLevelKeys.reduce((max, k) => Math.max(max, parseInt(k)), 0);
352+
// Only rebuild an array when every key is a real array index (< 2^32 - 1). A
353+
// larger numeric key, like a millisecond timestamp used as an object key, is
354+
// not an array index, so keep the object form instead of throwing "Invalid
355+
// array length" or allocating a huge array.
356+
if (maxIndex < MAX_ARRAY_INDEX) {
357+
const arrayResult = Array(maxIndex + 1);
358+
for (const key of topLevelKeys) {
359+
arrayResult[parseInt(key)] = result[key];
360+
}
361+
return arrayResult as any;
350362
}
351-
return arrayResult as any;
352363
}
353364

354365
return result;

packages/core/test/flattenAttributes.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,4 +707,20 @@ describe("unflattenAttributes", () => {
707707
a: { b: ["indexed"] },
708708
});
709709
});
710+
711+
it("keeps large numeric keys as an object instead of throwing Invalid array length", () => {
712+
expect(() => unflattenAttributes({ "1699999999999": "value" })).not.toThrow();
713+
expect(unflattenAttributes({ "1699999999999": "value" })).toEqual({
714+
"1699999999999": "value",
715+
});
716+
});
717+
718+
it("keeps a nested object of large numeric keys as an object", () => {
719+
expect(() =>
720+
unflattenAttributes({ "a.1699999999999": "x", "a.1700000000000": "y" })
721+
).not.toThrow();
722+
expect(unflattenAttributes({ "a.1699999999999": "x", "a.1700000000000": "y" })).toEqual({
723+
a: { "1699999999999": "x", "1700000000000": "y" },
724+
});
725+
});
710726
});

0 commit comments

Comments
 (0)