Skip to content

Commit 6b0755b

Browse files
authored
fix(agent): use minute precision when rounded expiry is at least 60s in future (#1181)
In the previous behavior, assuming the current time is 10:00:00 and there's no clock drift: - if a user provides `deltaInMs` equal to 85s, then rounded expiry is 10:01:25; - if a user provides `deltaInMs` equal to 95s (i.e., larger than before), then rounded expiry is 10:01:00 (i.e., _earlier_ than before although the deltaInMs is _larger_). With the new logic, we round the expiry to the nearest minute only if the rounding is at least 60 seconds in the future, otherwise we use second precision. --- Thanks @mraszyk for spotting the bug and proposing the solution!
1 parent 0bf4b1b commit 6b0755b

File tree

3 files changed

+76
-67
lines changed

3 files changed

+76
-67
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## [Unreleased]
44

5+
- fix(agent): use minute precision when rounded expiry is at least 60s in future
6+
57
## [4.2.1] - 2025-10-24
68

79
## [4.2.0] - 2025-10-22

packages/agent/src/agent/http/transforms.test.ts

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ const SECONDS_TO_MILLISECONDS = 1_000;
55
const MILLISECONDS_TO_NANOSECONDS = BigInt(1_000_000);
66
const MINUTES_TO_SECONDS = 60;
77

8+
const CURRENT_TIME = new Date(1619459231314); // 2021-04-26T17:47:11.314Z
9+
810
jest.useFakeTimers();
911
test('it should round down to the nearest minute', () => {
10-
// 2021-04-26T17:47:11.314Z - high precision
11-
jest.setSystemTime(new Date(1619459231314));
12+
jest.setSystemTime(CURRENT_TIME);
1213

1314
const expiry = Expiry.fromDeltaInMilliseconds(5 * MINUTES_TO_SECONDS * SECONDS_TO_MILLISECONDS);
1415
expect(expiry['__expiry__']).toEqual(BigInt(1619459520000) * MILLISECONDS_TO_NANOSECONDS);
@@ -17,9 +18,8 @@ test('it should round down to the nearest minute', () => {
1718
expect(expiryDate.toISOString()).toBe('2021-04-26T17:52:00.000Z');
1819
});
1920

20-
test('it should round down to the nearest second if less than 90 seconds', () => {
21-
// 2021-04-26T17:47:11.314Z - high precision
22-
jest.setSystemTime(new Date(1619459231314));
21+
test('it should round down to the nearest second when minute rounding would be less than 60s in future', () => {
22+
jest.setSystemTime(CURRENT_TIME);
2323

2424
const expiry = Expiry.fromDeltaInMilliseconds(89 * SECONDS_TO_MILLISECONDS);
2525
expect(expiry['__expiry__']).toEqual(BigInt(1619459320000) * MILLISECONDS_TO_NANOSECONDS);
@@ -39,131 +39,133 @@ test('it should handle zero delta', () => {
3939
expect(expiryDate.toISOString()).toBe('2021-04-26T17:47:11.000Z');
4040
});
4141

42-
describe('deltaInMsAfterThresholdCheck parameter', () => {
42+
describe('clockDriftInMs parameter', () => {
4343
beforeEach(() => {
4444
// Set a fixed time for consistent testing
45-
jest.setSystemTime(new Date(1619459231314)); // 2021-04-26T17:47:11.314Z
45+
jest.setSystemTime(CURRENT_TIME);
4646
});
4747

48-
describe('when deltaInMs is less than 90 seconds (rounds to seconds)', () => {
49-
test('should add deltaInMsAfterThresholdCheck before rounding to seconds', () => {
48+
describe('when minute rounding would be less than 60s in future (rounds to seconds)', () => {
49+
test('should add clockDriftInMs before rounding to seconds', () => {
5050
const baseDelta = 30 * SECONDS_TO_MILLISECONDS; // 30 seconds
51-
const additionalDelta = 1500; // 1.5 seconds
51+
const clockDrift = 1500; // 1.5 seconds
5252

53-
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, additionalDelta);
53+
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, clockDrift);
5454

55-
// Expected: (1619459231314 + 30000 + 1500) rounded down to nearest second
55+
// Expected: correctedNow + delta = (1619459231314 + 1500) + 30000 = 1619459262814, rounds to 1619459262000
5656
expect(expiry['__expiry__']).toEqual(BigInt(1619459262000) * MILLISECONDS_TO_NANOSECONDS);
5757

5858
const expiryDate = new Date(Number(expiry['__expiry__'] / MILLISECONDS_TO_NANOSECONDS));
5959
expect(expiryDate.toISOString()).toBe('2021-04-26T17:47:42.000Z');
6060
});
6161

62-
test('should handle zero base delta with positive additional delta', () => {
62+
test('should handle zero base delta with positive clock drift', () => {
6363
const baseDelta = 0;
64-
const additionalDelta = 1000; // 1 second
64+
const clockDrift = 1000; // 1 second
6565

66-
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, additionalDelta);
66+
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, clockDrift);
6767

68-
// Expected: (1619459231314 + 0 + 1000) rounded down to nearest second
68+
// Expected: correctedNow + delta = (1619459231314 + 1000) + 0 = 1619459232314, rounds to 1619459232000
6969
expect(expiry['__expiry__']).toEqual(BigInt(1619459232000) * MILLISECONDS_TO_NANOSECONDS);
7070

7171
const expiryDate = new Date(Number(expiry['__expiry__'] / MILLISECONDS_TO_NANOSECONDS));
7272
expect(expiryDate.toISOString()).toBe('2021-04-26T17:47:12.000Z');
7373
});
7474

75-
test('should handle zero base delta with negative additional delta', () => {
75+
test('should handle zero base delta with negative clock drift', () => {
7676
const baseDelta = 0;
77-
const additionalDelta = -500; // -0.5 seconds
77+
const clockDrift = -500; // -0.5 seconds
7878

79-
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, additionalDelta);
79+
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, clockDrift);
8080

81-
// Expected: (1619459231314 + 0 - 500) rounded down to nearest second
81+
// Expected: correctedNow + delta = (1619459231314 - 500) + 0 = 1619459230814, rounds to 1619459230000
8282
expect(expiry['__expiry__']).toEqual(BigInt(1619459230000) * MILLISECONDS_TO_NANOSECONDS);
8383

8484
const expiryDate = new Date(Number(expiry['__expiry__'] / MILLISECONDS_TO_NANOSECONDS));
8585
expect(expiryDate.toISOString()).toBe('2021-04-26T17:47:10.000Z');
8686
});
8787

88-
test('should handle negative deltaInMsAfterThresholdCheck', () => {
88+
test('should handle negative clockDriftInMs', () => {
8989
const baseDelta = 50 * SECONDS_TO_MILLISECONDS; // 50 seconds
90-
const additionalDelta = -2000; // -2 seconds
90+
const clockDrift = -2000; // -2 seconds
9191

92-
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, additionalDelta);
92+
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, clockDrift);
9393

94-
// Expected: (1619459231314 + 50000 - 2000) rounded down to nearest second
94+
// Expected: correctedNow + delta = (1619459231314 - 2000) + 50000 = 1619459279314, rounds to 1619459279000
9595
expect(expiry['__expiry__']).toEqual(BigInt(1619459279000) * MILLISECONDS_TO_NANOSECONDS);
9696

9797
const expiryDate = new Date(Number(expiry['__expiry__'] / MILLISECONDS_TO_NANOSECONDS));
9898
expect(expiryDate.toISOString()).toBe('2021-04-26T17:47:59.000Z');
9999
});
100100

101-
test('should handle large deltaInMsAfterThresholdCheck that pushes over threshold', () => {
102-
const baseDelta = 80 * SECONDS_TO_MILLISECONDS; // 80 seconds (under 90s threshold)
103-
const additionalDelta = 15000; // 15 seconds (total 95s, should still round to seconds)
101+
test('should handle large clockDriftInMs', () => {
102+
const baseDelta = 80 * SECONDS_TO_MILLISECONDS; // 80 seconds
103+
const clockDrift = 15000; // 15 seconds
104104

105-
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, additionalDelta);
105+
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, clockDrift);
106106

107-
// Even though total is 95s, the base delta is still under 90s, so it rounds to seconds
108-
// Expected: (1619459231314 + 80000 + 15000) rounded down to nearest second
107+
// Expected: correctedNow + delta = (1619459231314 + 15000) + 80000 = 1619459326314, rounds to 1619459326000
109108
expect(expiry['__expiry__']).toEqual(BigInt(1619459326000) * MILLISECONDS_TO_NANOSECONDS);
110109

111110
const expiryDate = new Date(Number(expiry['__expiry__'] / MILLISECONDS_TO_NANOSECONDS));
112111
expect(expiryDate.toISOString()).toBe('2021-04-26T17:48:46.000Z');
113112
});
114113

115-
test('should handle very large deltaInMsAfterThresholdCheck', () => {
114+
test('should handle very large clockDriftInMs', () => {
116115
const baseDelta = 1 * MINUTES_TO_SECONDS * SECONDS_TO_MILLISECONDS; // 1 minute
117-
const additionalDelta = 5 * MINUTES_TO_SECONDS * SECONDS_TO_MILLISECONDS; // 5 minutes
116+
const clockDrift = 5 * MINUTES_TO_SECONDS * SECONDS_TO_MILLISECONDS; // 5 minutes
118117

119-
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, additionalDelta);
118+
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, clockDrift);
120119

121-
// Expected: (1619459231314 + 60000 + 300000) rounded down to nearest second
120+
// Expected: correctedNow + delta = (1619459231314 + 300000) + 60000 = 1619459591314, rounds to 1619459591000
122121
expect(expiry['__expiry__']).toEqual(BigInt(1619459591000) * MILLISECONDS_TO_NANOSECONDS);
123122

124123
const expiryDate = new Date(Number(expiry['__expiry__'] / MILLISECONDS_TO_NANOSECONDS));
125124
expect(expiryDate.toISOString()).toBe('2021-04-26T17:53:11.000Z');
126125
});
127126
});
128127

129-
describe('when deltaInMs is 90 seconds or more (rounds to minutes)', () => {
130-
test('should add deltaInMsAfterThresholdCheck before rounding to minutes', () => {
128+
describe('when minute rounding is at least 60s in future (rounds to minutes)', () => {
129+
test('should add clockDriftInMs before rounding to minutes', () => {
131130
const baseDelta = 2 * MINUTES_TO_SECONDS * SECONDS_TO_MILLISECONDS; // 2 minutes
132-
const additionalDelta = 30000; // 30 seconds
131+
const clockDrift = 30000; // 30 seconds
133132

134-
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, additionalDelta);
133+
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, clockDrift);
135134

136-
// Expected: (1619459231314 + 120000 + 30000) rounded down to nearest minute
135+
// Expected: correctedNow + delta = (1619459231314 + 30000) + 120000 = 1619459381314
136+
// Rounded down to the nearest minute
137137
expect(expiry['__expiry__']).toEqual(BigInt(1619459340000) * MILLISECONDS_TO_NANOSECONDS);
138138

139139
const expiryDate = new Date(Number(expiry['__expiry__'] / MILLISECONDS_TO_NANOSECONDS));
140140
expect(expiryDate.toISOString()).toBe('2021-04-26T17:49:00.000Z');
141141
});
142142

143-
test('should handle negative deltaInMsAfterThresholdCheck', () => {
143+
test('should handle negative clockDriftInMs', () => {
144144
const baseDelta = 2 * MINUTES_TO_SECONDS * SECONDS_TO_MILLISECONDS; // 2 minutes
145-
const additionalDelta = -45000; // -45 seconds
145+
const clockDrift = -45000; // -45 seconds
146146

147-
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, additionalDelta);
147+
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, clockDrift);
148148

149-
// Expected: (1619459231314 + 120000 - 45000) rounded down to nearest minute
149+
// Expected: correctedNow + delta = (1619459231314 - 45000) + 120000 = 1619459306314
150+
// Rounded down to the nearest minute
150151
expect(expiry['__expiry__']).toEqual(BigInt(1619459280000) * MILLISECONDS_TO_NANOSECONDS);
151152

152153
const expiryDate = new Date(Number(expiry['__expiry__'] / MILLISECONDS_TO_NANOSECONDS));
153154
expect(expiryDate.toISOString()).toBe('2021-04-26T17:48:00.000Z');
154155
});
155156

156-
test('should handle exactly 90 seconds base delta', () => {
157-
const baseDelta = 90 * SECONDS_TO_MILLISECONDS; // Exactly 90 seconds
158-
const additionalDelta = 5000; // 5 seconds
157+
test('should use second precision when minute rounding falls below 60s threshold', () => {
158+
const baseDelta = 90 * SECONDS_TO_MILLISECONDS;
159+
const clockDrift = 5000; // 5 seconds
159160

160-
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, additionalDelta);
161+
const expiry = Expiry.fromDeltaInMilliseconds(baseDelta, clockDrift);
161162

162-
// Expected: (1619459231314 + 90000 + 5000) rounded down to nearest minute
163-
expect(expiry['__expiry__']).toEqual(BigInt(1619459280000) * MILLISECONDS_TO_NANOSECONDS);
163+
// Expected: correctedNow + delta = (1619459231314 + 5000) + 90000 = 1619459326314
164+
// Rounded down to the nearest second
165+
expect(expiry['__expiry__']).toEqual(BigInt(1619459326000) * MILLISECONDS_TO_NANOSECONDS);
164166

165167
const expiryDate = new Date(Number(expiry['__expiry__'] / MILLISECONDS_TO_NANOSECONDS));
166-
expect(expiryDate.toISOString()).toBe('2021-04-26T17:48:00.000Z');
168+
expect(expiryDate.toISOString()).toBe('2021-04-26T17:48:46.000Z');
167169
});
168170
});
169171
});

packages/agent/src/agent/http/transforms.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ export const JSON_KEY_EXPIRY = '__expiry__';
1313
const SECONDS_TO_MILLISECONDS = BigInt(1_000);
1414
const MILLISECONDS_TO_NANOSECONDS = BigInt(1_000_000);
1515
const MINUTES_TO_SECONDS = BigInt(60);
16+
const MINUTES_TO_MILLISECONDS = MINUTES_TO_SECONDS * SECONDS_TO_MILLISECONDS;
1617

17-
const EXPIRY_DELTA_THRESHOLD_MILLISECONDS = BigInt(90) * SECONDS_TO_MILLISECONDS;
18+
const EXPIRY_DELTA_THRESHOLD_MILLISECONDS = BigInt(1) * MINUTES_TO_MILLISECONDS;
1819

19-
function roundMillisToSeconds(millis: bigint): bigint {
20-
return millis / SECONDS_TO_MILLISECONDS;
20+
function roundToSecond(millis: bigint): bigint {
21+
return (millis / SECONDS_TO_MILLISECONDS) * SECONDS_TO_MILLISECONDS;
2122
}
2223

23-
function roundMillisToMinutes(millis: bigint): bigint {
24-
return roundMillisToSeconds(millis) / MINUTES_TO_SECONDS;
24+
function roundToMinute(millis: bigint): bigint {
25+
return (millis / MINUTES_TO_MILLISECONDS) * MINUTES_TO_MILLISECONDS;
2526
}
2627

2728
export type JsonnableExpiry = {
@@ -35,25 +36,29 @@ export class Expiry {
3536

3637
/**
3738
* Creates an Expiry object from a delta in milliseconds.
38-
* If the delta is less than 90 seconds, the expiry is rounded down to the nearest second.
39-
* Otherwise, the expiry is rounded down to the nearest minute.
39+
* The expiry is calculated as: current_time + delta + clock_drift
40+
* The resulting expiry is then rounded:
41+
* - If rounding down to the nearest minute still provides at least 60 seconds in the future, use minute precision
42+
* - Otherwise, use second precision
4043
* @param deltaInMs The milliseconds to add to the current time.
41-
* @param clockDriftMs The milliseconds to add to the current time, typically the clock drift between IC network clock and the client's clock. Defaults to `0` if not provided.
44+
* @param clockDriftInMs The milliseconds to add to the current time, typically the clock drift between IC network clock and the client's clock. Defaults to `0` if not provided.
4245
* @returns {Expiry} The constructed Expiry object.
4346
*/
44-
public static fromDeltaInMilliseconds(deltaInMs: number, clockDriftMs: number = 0): Expiry {
45-
const deltaMs = BigInt(deltaInMs);
46-
const expiryMs = BigInt(Date.now()) + deltaMs + BigInt(clockDriftMs);
47+
public static fromDeltaInMilliseconds(deltaInMs: number, clockDriftInMs: number = 0): Expiry {
48+
const correctedNowMs = BigInt(Date.now()) + BigInt(clockDriftInMs);
49+
const expiryMs = correctedNowMs + BigInt(deltaInMs);
4750

48-
let roundedExpirySeconds: bigint;
49-
if (deltaMs < EXPIRY_DELTA_THRESHOLD_MILLISECONDS) {
50-
roundedExpirySeconds = roundMillisToSeconds(expiryMs);
51+
const roundedToMinute = roundToMinute(expiryMs);
52+
53+
let roundedExpiryMs: bigint;
54+
if (roundedToMinute >= correctedNowMs + EXPIRY_DELTA_THRESHOLD_MILLISECONDS) {
55+
roundedExpiryMs = roundedToMinute;
5156
} else {
52-
const roundedExpiryMinutes = roundMillisToMinutes(expiryMs);
53-
roundedExpirySeconds = roundedExpiryMinutes * MINUTES_TO_SECONDS;
57+
const roundedToSecond = roundToSecond(expiryMs);
58+
roundedExpiryMs = roundedToSecond;
5459
}
5560

56-
return new Expiry(roundedExpirySeconds * SECONDS_TO_MILLISECONDS * MILLISECONDS_TO_NANOSECONDS);
61+
return new Expiry(roundedExpiryMs * MILLISECONDS_TO_NANOSECONDS);
5762
}
5863

5964
public toBigInt(): bigint {

0 commit comments

Comments
 (0)