Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: be explicit when specifying time & replace moment with date-fns #1072

Merged
merged 5 commits into from
Nov 2, 2021

Conversation

theneva
Copy link
Contributor

@theneva theneva commented Oct 27, 2021

This uses date-fns for all (I hope) fiddling with time and durations.

I think this makes it way easier to understand which units are used (for example, milliseconds or seconds). It also removes the need for things like hard coded millisecond representations of 24 hours.

I took the liberty of replacing all usage of moment with date-fns, and removing the dependency. (I did not, however, update the docs page that uses moment in an example (website/docs/advanced/custom-activation-strategy.md)).

I'll write some inline PR comments in the places where I think the changes are less than obvious – but feel free to ask questions 😄

A small note: I've not been super strict about using just one of Date.now() and new Date(), since it doesn't matter. date-fns happily consumes both.

Do you think this makes sense?

Copy link
Contributor Author

@theneva theneva left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrote some comments to help the poor reader of this all-over-the-place diff.


const secureHeaders: (config: IUnleashConfig) => RequestHandler = (config) => {
if (config.secureHeaders) {
return helmet({
hsts: {
maxAge: 63072000,
maxAge: hoursToSeconds(24 * 365 * 2), // 2 non-leap years
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$ node
> require('date-fns').hoursToSeconds(24 * 365 * 2)
63072000

(There is no way to safely translate hours to days, due to daylight savings time. The same goes for days to years, due to leap years. Therefore (I believe), date-fns offers no utilities to make those conversions, and I just kept the original value. I think this is the only inline code comment I wrote.)

@@ -88,7 +88,7 @@ class StateController extends Controller {
includeTags,
includeEnvironments,
});
const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss');
const timestamp = formatDate(Date.now(), 'yyyy-MM-dd_HH-mm-ss');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$ node
> require('moment')().format('YYYY-MM-DD_HH-mm-ss')
'2021-10-27_15-06-43'
> require('date-fns').format(Date.now(), 'yyyy-MM-dd_HH-mm-ss')
'2021-10-27_15-07-01'

(The minute and seconds are different because I ran the commands 18 seconds apart.)

@@ -75,7 +74,7 @@ export default class AddonService {
async () => addonStore.getAll({ enabled: true }),
{
promise: true,
maxAge: ADDONS_CACHE_TIME,
maxAge: millisecondsInMinute,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the named constant adds any value, and I haven't seen this "pattern" anywhere else in the code, so I just inlined it since there's no need for the // 60s comment anymore.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer minutesToMilliseconds(1)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree! I'll switch to that everywhere so it's consistent.

await db.stores.clientInstanceStore.insert({
appName: 'demo-app-1',
instanceId: 'test-1',
strategies: ['default'],
started: 1516026938494,
started: clientStartedDate,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$ node
> new Date(1516026938494)
2018-01-15T14:35:38.494Z
> require('date-fns').parseISO('2018-01-15T14:35:38.494Z');
2018-01-15T14:35:38.494Z

I think it's nice to not have gigantic numbers just hanging out in the code, so I prefer it like this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed.

@@ -72,7 +73,7 @@ export default class SessionStore implements ISessionStore {
.insert({
sid: data.sid,
sess: JSON.stringify(data.sess),
expired: data.expired || new Date(Date.now() + 86400000),
expired: data.expired || addDays(Date.now(), 1),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$ node
> require('date-fns').hoursToMilliseconds(24)
86400000

originalMaxAge: 2880000,
expires: new Date(Date.now() + 86400000).toDateString(),
originalMaxAge: minutesToMilliseconds(48),
expires: addDays(Date.now(), 1).toDateString(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$ node
> require('date-fns').hoursToMilliseconds(24)
86400000

@@ -141,8 +142,8 @@ test("deleting a user should delete the user's sessions", async () => {
sid: 'xyz321',
sess: {
cookie: {
originalMaxAge: 2880000,
expires: new Date(Date.now() + 86400000).toDateString(),
originalMaxAge: minutesToMilliseconds(48),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$ node
> require('date-fns').minutesToMilliseconds(48)
2880000

I have no idea why this is 48 minutes, but it is, so I kept it. Let me know if I missed some kind of significance about this number 🤷

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see any good reason other than a small mistake when the test was written. I would assume 48 hours make more sense, as this is our default session length.

Maybe @chriswk remembers this part?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the code could just be confused about seconds and milliseconds. Even though the Max-Age and Expiry parts of a set-cookie header use seconds, express-session uses milliseconds (for compatibility with the JS Date, I guess). Multiplying the number by 1000 doesn't get us to 48 hours though 😅 I'm happy to change it to 48 hours if you prefer that – it is just a test, after all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think that's needed.

d1.setHours(10, 10, 11);
d2.setHours(11, 10, 11);
const d1 = set(Date.now(), { hours: 10, minutes: 10, seconds: 11 });
const d2 = addHours(d1, 1);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the spirit of d2 is to be d1 + 1 hour, so I changed it a bit. The values are the same, though

this.startTimer();
}

destroy() {
// https://github.com/nodejs/node/issues/9561
// clearTimeout(this.timer);
// this.timer = null;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe nodejs/node#9561 was fixed in 2016 and was patched into both Node 4 and 6, so this code should be safe to run 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ttl-list will be removed in Unleash v4.3.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright, should I just remove this particular change? I still need some diff in the file to avoid depending on moment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, good to remove the moment dependency. Feel free to fix the destroy to, its good to not leave timers hanging around if we do not have too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, I'll leave my original change in, then 😄

subSeconds,
} from 'date-fns';

/**
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went a little bit wild explaining this, because it took me a while to figure out what was happening. Let me know if you don't want this long explanation, or if this utility belongs somewhere else in the project!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining this, I learned something new, so good to keep it here!

We might reuse it later, but let's keep it here for now.

@@ -36,26 +36,22 @@ module.exports = class TTLList extends EventEmitter {
}

add(value, timestamp = new Date()) {
const ttl = moment(timestamp).add(this.expireAmount, this.expireType);
if (moment().isBefore(ttl)) {
const ttl = add(timestamp, { [this.expireType]: this.expireAmount });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a small issue with this: it doesn't support milliseconds. It shouldn't affect the production code (which only uses hours and minutes), but it does affect the tests. I have no idea why they pass… Pushing a fix in a sec

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 442c0b9. maybe it would be better to have the list not support milliseconds? I wanted to keep the the functional changes to a minimum

@ivarconr ivarconr self-requested a review October 28, 2021 11:44
Copy link
Member

@ivarconr ivarconr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good. just a few minor comments

src/lib/addons/addon.test.ts Outdated Show resolved Hide resolved
src/lib/create-config.ts Outdated Show resolved Hide resolved
src/lib/db/client-metrics-db.ts Outdated Show resolved Hide resolved
src/lib/metrics.ts Outdated Show resolved Hide resolved
@@ -75,7 +74,7 @@ export default class AddonService {
async () => addonStore.getAll({ enabled: true }),
{
promise: true,
maxAge: ADDONS_CACHE_TIME,
maxAge: millisecondsInMinute,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer minutesToMilliseconds(1)

src/lib/services/api-token-service.ts Outdated Show resolved Hide resolved
subSeconds,
} from 'date-fns';

/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining this, I learned something new, so good to keep it here!

We might reuse it later, but let's keep it here for now.

this.startTimer();
}

destroy() {
// https://github.com/nodejs/node/issues/9561
// clearTimeout(this.timer);
// this.timer = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, good to remove the moment dependency. Feel free to fix the destroy to, its good to not leave timers hanging around if we do not have too.

await db.stores.clientInstanceStore.insert({
appName: 'demo-app-1',
instanceId: 'test-1',
strategies: ['default'],
started: 1516026938494,
started: clientStartedDate,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed.

@@ -141,8 +142,8 @@ test("deleting a user should delete the user's sessions", async () => {
sid: 'xyz321',
sess: {
cookie: {
originalMaxAge: 2880000,
expires: new Date(Date.now() + 86400000).toDateString(),
originalMaxAge: minutesToMilliseconds(48),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see any good reason other than a small mistake when the test was written. I would assume 48 hours make more sense, as this is our default session length.

Maybe @chriswk remembers this part?

@@ -252,11 +295,11 @@ test('should have correct values for lastMinute', () => {
let c = metrics.getTogglesMetrics();
expect(c.lastMinute.toggle).toEqual({ yes: 20, no: 20 });

jest.advanceTimersByTime(10 * 1000);
jest.advanceTimersByTime(10_000);
Copy link
Contributor Author

@theneva theneva Nov 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, having played with it a bit, I think anything below 1 minute is better written as a constant with underscores than either 10 * 1000 or secondsToMilliseconds(10) in the case of jest.advanceTimersByTime. When reading the test, you already know that this function takes milliseconds, so it's better to avoid the noise of helper functions or maths

c = metrics.getTogglesMetrics();
expect(c.lastHour.toggle).toEqual({ yes: 41, no: 41 });

// at 30
jest.advanceTimersByTime(30 * 60 * 1000);
jest.advanceTimersByTime(minutesToMilliseconds(30));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

… but when we're dealing with minutes, the numbers are less easy to grok, so I prefer the helper functions over either a constant number of milliseconds or inline arithmetic


list.destroy();
jest.useRealTimers();
});

test('should add item created in the past but expiring in the future', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this to ensure it both works with milliseconds and to catch a bug that I almost introduced and the other tests didn't catch

@theneva
Copy link
Contributor Author

theneva commented Nov 2, 2021

Okay, I think this is ready for a final look 😄

Copy link
Member

@ivarconr ivarconr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@ivarconr ivarconr merged commit b47e228 into Unleash:master Nov 2, 2021
@theneva theneva deleted the explicit-times branch November 2, 2021 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants