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

Complete Refactor to TypeScript, Improved Test Coverage, and Bug Fixes #323

Open
wants to merge 111 commits into
base: master
Choose a base branch
from

Conversation

MichaelLeeHobbs
Copy link

@MichaelLeeHobbs MichaelLeeHobbs commented Apr 30, 2023

This pull request introduces a complete refactor of the codebase into TypeScript, maintaining compatibility with the documented API. Users should not experience any breaking changes, however, those relying on undocumented features may need to update their code. The update includes builds for both CommonJS (CJS) and EcmaScript Modules (ESM), as well as improved linting, enhanced test coverage to nearly 100%, and automatic documentation generation from code.

Additional changes include the implementation of a pre-commit hook to run linting and tests, the addition of numerous tests and bug fixes, and the inclusion of code coverage badges. The .npmignore file has been updated to ignore build artifacts for a lightweight library, and tsconfig files for both CJS and ESM builds have been added. The primary test has been converted to a TypeScript Jest test, with TSD tests updated for revised types. The package.json file has been updated to incorporate new scripts and dependencies. A debug library has also been added for debugging via DEBUG=cron-parser.

This pull request addresses and potentially fixes several issues (#112, #153, #156, #190, #222, #236, #269, #299, #309, #322). Issue #244 is fixed in strict mode only, and issues #273 and #309 may be resolved as well. Issue #249 is partially addressed through the implementation of strict mode, although it may not be entirely complete.

The commit history provides a detailed overview of the work in progress, including refactoring, cleanup, and updates to various components of the codebase.

This pull request still has several items that need to be addressed before it can be considered complete:

  1. There are currently 2 NEW tests failing that require attention.
  2. There are 6 FIXMEs and 3 TODOs in the code that need to be resolved.
  3. The README.md file needs to be updated to reflect the changes introduced in the pull request.
  4. The package.json version should be updated, and a manual linting process should be performed to remove any unused dependencies or configuration values.
  5. The necessity of the component.json file should be evaluated.
  6. Redundant TypeScript types ICronExpressionParserOptions and ICronParser should be resolved.
  7. Constant constraints should be refactored out of the code.
  8. Enums should be used instead of constants for better code readability and maintainability.
  9. The .editorconfig file should be removed in favor of using eslint for consistent code formatting.
  10. The archive folder should be removed from the repository.

See notes.md for more details.

… of coverage on CronParser, not sure why yet. Added a lot of FIXME's for things that don't make sense or aren't needed. Phase 2 basically done. Phase 3 will be a code review.
Copy link
Owner

@harrisiirak harrisiirak left a comment

Choose a reason for hiding this comment

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

I added couple of more comments!

notes.md Outdated Show resolved Hide resolved
notes.md Outdated Show resolved Hide resolved
notes.md Outdated Show resolved Hide resolved
notes.md Outdated Show resolved Hide resolved
src/CronExpression.ts Outdated Show resolved Hide resolved
src/CronFieldCollection.ts Outdated Show resolved Hide resolved
src/types.ts Outdated Show resolved Hide resolved
src/types.ts Outdated Show resolved Hide resolved
src/types.ts Outdated Show resolved Hide resolved
src/wrapper-index.cjs Outdated Show resolved Hide resolved
@MichaelLeeHobbs
Copy link
Author

@harrisiirak At my day job we have decided to pull the plug on using ESM. Not really an issue in this project but we have found it to be excruciatingly painful when working with a complex networking app. The biggest issue is testing but there are other issues like synthetic default imports, importing JSON, plugin scripts, file name extensions, and having to use run time flags, among others. If you want I could switch over to CJS in an hour or so for the work I have done.

@harrisiirak
Copy link
Owner

@harrisiirak At my day job we have decided to pull the plug on using ESM. Not really an issue in this project but we have found it to be excruciatingly painful when working with a complex networking app. The biggest issue is testing but there are other issues like synthetic default imports, importing JSON, plugin scripts, file name extensions, and having to use run time flags, among others. If you want I could switch over to CJS in an hour or so for the work I have done.

Going solely with CJS is probably less of a headache than including ESM in a hybrid build. If it doesn't require much effort, let's stick with CJS for now, and I'll consider using ESM when it becomes more mature.

@MichaelLeeHobbs
Copy link
Author

image

Not the most interesting example for #269 but I have been up since 3:30 am. More proof of concept as I've not used this Doc generator before.

@harrisiirak
Copy link
Owner

@MichaelLeeHobbs I'll try to give it a last view later today or tomorrow. It has been busy the last few days.

Copy link
Owner

@harrisiirak harrisiirak left a comment

Choose a reason for hiding this comment

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

Hey @MichaelLeeHobbs!

All the recent changes you made are looking good! Added couple new comments/requests - don't have anything more to add at the moment. There are still some open threads from previous PR reviews that requires your attention. Let me know if I can be of any help or assistance to you, so that we can wrap it up soon.

I've tested and benchmarked the changes locally. Package usage is still fairly simple (we probably need to update README.md to reflect interface changes and how parseExpression can be used through CronParser).

While benchmarking the code, I discovered that there there could be some possible regressions:

start pattern: * * * * * *
old end: 2023-06-05T16:17:47.000Z
old: 1.106s
new end: Mon Jun 05 2023 19:17:47 GMT+0300 (Eastern European Summer Time)
new: 1.572s
end pattern: * * * * * *

I haven't really investigated/profiled the code, but I can possibly look into the code and see if I've missed something that can possibly cause that.

Small script that I wrote for benchmarking old and new (this branch) codebase:

import * as parser from './old';
import { CronParser } from './new';

const oldParseExpression = parser.parseExpression;
const newParseExpression = CronParser.parseExpression;

function parseAndBenchMarkExpression(expression: string, iterations = 100000) {
  const currentDate = new Date();

  console.log(`start pattern: ${expression}`);
  console.time('old');
  {
    const result = oldParseExpression(expression, { currentDate });
    for (let i = 0, c = iterations; i < c; i++) {
      result.next();
    }
    console.log('old end:', result.next().toDate());
  }
  console.timeEnd('old');

  console.time('new');
  {
    const result = newParseExpression(expression, { currentDate });
    for (let i = 0, c = iterations; i < c; i++) {
      result.next();
    }
    console.log('new end:', result.next().toString());
  }
  console.timeEnd('new');

  console.log(`end pattern: ${expression}`);
}

parseAndBenchMarkExpression('* * * * * *');

"generate-badges": "jest-coverage-badges",
"test:types": "npm run build && tsd",
"test": "cross-env TZ=UTC npm run lint && npm run test:types && npm run test:coverage && npm run generate-badges",
"docs": "rimraf docs && typedoc --out docs --readme none --name 'CronParser' src"
Copy link
Owner

Choose a reason for hiding this comment

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

I was thinking about the documentation a bit. Let's remove docs/ folder from the repository at the moment. I think it potentially could create a bit too much noise in the future.

If it's required, it can be fairly easily generated locally or accessed publicly. We can automate generated documentation publishing (discussion here and a good example here) by using GH Pages action and add it to the pipeline. If you don't have time for it, I can add documentation generation automation myself.

notes.md Outdated Show resolved Hide resolved
src/CronExpression.ts Outdated Show resolved Hide resolved
src/fields/CronMonth.ts Outdated Show resolved Hide resolved
src/fields/CronMonth.ts Outdated Show resolved Hide resolved
Comment on lines +16 to +58
// var a = {
// 'second': {
// 'wildcard': false,
// 'values': [
// 0
// ]
// },
// 'minute': {
// 'wildcard': false,
// 'values': [
// 0,
// 30
// ]
// },
// 'hour': {
// 'wildcard': false,
// 'values': [
// 9
// ]
// },
// 'dayOfMonth': {
// 'wildcard': false,
// 'values': [
// 15
// ]
// },
// 'month': {
// 'wildcard': false,
// 'values': [
// 1
// ]
// },
// 'dayOfWeek': {
// 'wildcard': false,
// 'values': [
// 1,
// 2,
// 3,
// 4,
// 5
// ]
// }
// };
Copy link
Owner

Choose a reason for hiding this comment

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

Probably testing data?

notes.md Outdated Show resolved Hide resolved
notes.md Outdated Show resolved Hide resolved
* @memberof CronExpression
* @public
*/
next(): CronDate | { value: CronDate; done: boolean } {
Copy link
Owner

Choose a reason for hiding this comment

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

While it's actually as same as previously, I started thinking, maybe it makes sense to separate iterator and "date" access behaviour. Having proper-proper ES6 iterator interface fully implemented is a big bonus.

With proper typings, the annoying part is that if we want to use the result of this method, we always need to assert it as CronDate. This is something that should be also reflected in the documentation.

What I'm thinking:

  • nextDate and prevDate for "default" behaviour
  • next and prev as part of iterator implementation; iterator option can be deprecated and removed.

Such an API change would be nice to include in a major version release since adopting this version would require a couple of changes in one way or another.

MichaelLeeHobbs and others added 2 commits June 5, 2023 13:47
Co-authored-by: Harri Siirak <harri@siirak.ee>
Co-authored-by: Harri Siirak <harri@siirak.ee>
@MichaelLeeHobbs
Copy link
Author

Hey @MichaelLeeHobbs!

start pattern: * * * * * *
old end: 2023-06-05T16:17:47.000Z
old: 1.106s
new end: Mon Jun 05 2023 19:17:47 GMT+0300 (Eastern European Summer Time)
new: 1.572s
end pattern: * * * * * *

I'll look later but I think you have to run 1m iterations before node fully optimizes the code. I read an article on this years ago but can't find it now. In general, you ignore the first 1m calls then start your timer. This will be the best apples-to-apples comparison I believe. No matter what, we will need flame graphs.

@harrisiirak
Copy link
Owner

Hi @MichaelLeeHobbs!

Is there any way I can help you with this PR? Since a lot of work has already been done, I still hope to see this being merged.

If you really don't have any time work on this, I could in theory fork your branch and address all the open issues/questions myself and prepare final PR.

Best regards

@MichaelLeeHobbs
Copy link
Author

@harrisiirak feel free to fork. We are in crunch time for the next couple of months. I'm working some on the weekends and quite honestly after I'm done coding for work, I don't have it in me to do even more coding. I've invited you as a collaborator on the fork.

Also for your awareness, we are using the fork in production for a high-performance DICOM router. The includesDate method is called more than 100K times per day.

assert(atoms.length <= 6, 'Invalid cron expression, too many fields');
const defaults = ['*', '*', '*', '*', '*', '0'];
if (atoms.length < defaults.length) {
atoms.unshift(...defaults.slice(atoms.length));

Choose a reason for hiding this comment

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

Suggested change
atoms.unshift(...defaults.slice(atoms.length));
atoms.unshift(...defaults.slice(atoms.length).reverse());

See example:

const defaults = ['*', '*', '*', '*', '*', '0'];
const atoms = ['3', '15-25', 'jan,feb', '*'];
atoms.unshift(...defaults.slice(atoms.length));
console.log(atoms);
// prints ["*", "0", "3", "15-25", "jan,feb", "*"] 
// This will run every second of 0th minute, not expected

const atoms2 = ['3', '15-25', 'jan,feb', '*'];
atoms2.unshift(...defaults.slice(atoms2.length).reverse());
console.log(atoms2);
// prints ["0", "*", "3", "15-25", "jan,feb", "*"] 
// This will run every minute, as expected

@sommmen
Copy link

sommmen commented Aug 31, 2023

@harrisiirak @MichaelLeeHobbs Hiya, since it has been some time - what is the current status of this?

I see this PR is not published on npm anywhere - is it maybe possible to just merge this in and publish a beta package only or something?

@harrisiirak
Copy link
Owner

Hi @sommmen!

As you see from @MichaelLeeHobbs last comment, he don't have time work on it anymore. There are still some (minor) open stuff that needs be addressed before this PR can be merged. I volunteered myself (and I already forked this codebase) to solve these open issues, but I haven't really had any free time to work on it either. I'll try to find some time next week and finish it up - there aren't that much left to do, as @MichaelLeeHobbs has done a great job with this TS rewrite.

I see this PR is not published on npm anywhere - is it maybe possible to just merge this in and publish a beta package only or something?

If it's ready for merge (I'll probably open up a new PR and close this one after that), I'm planning to release it as a new major version (v5).

@sommmen
Copy link

sommmen commented Aug 31, 2023

Hi @sommmen!

As you see from @MichaelLeeHobbs last comment, he don't have time work on it anymore. There are still some (minor) open stuff that needs be addressed before this PR can be merged. I volunteered myself (and I already forked this codebase) to solve these open issues, but I haven't really had any free time to work on it either. I'll try to find some time next week and finish it up - there aren't that much left to do, as @MichaelLeeHobbs has done a great job with this TS rewrite.

I see this PR is not published on npm anywhere - is it maybe possible to just merge this in and publish a beta package only or something?

If it's ready for merge (I'll probably open up a new PR and close this one after that), I'm planning to release it as a new major version (v5).

Sure thing - i'm not a JS myself so I can't help out but i understand it takes time - i was just curious since it was some time ago.

Anyways i'm saying, from a consumers perspective, i don't mind a version that is nog 100% (feature) complete for a big rewrite like this so if its a safe build don't be shy with a new release :).

Thanks for your continued support to foss software.

@MichaelLeeHobbs
Copy link
Author

Yeah, sorry. Way underwater. A year-long project is in chaos as one side cannot deliver the data they said they could so multiple teams are all scrambling to work around this limitation. The problem is this data element is critical as it's the catalyst that kicks off the whole process. We will be swinging back around to the project that uses cron-parser but that will likely be a few months at best.

@harrisiirak
Copy link
Owner

harrisiirak commented Aug 31, 2023

@MichaelLeeHobbs no worries, it's completely understandable. I'll let you know if I'm wrapping up with final touches and create the final draft for this TS rewrite.

I was also able to find out what was degrading the performance. It appears that Object.freeze has some serious overhead. Without calling Object.freeze and returning a copy of values in CronField.values, the performance is even better than the old code.

@harrisiirak harrisiirak mentioned this pull request Aug 31, 2023
@MichaelLeeHobbs
Copy link
Author

@MichaelLeeHobbs no worries, it's completely understandable. I'll let you know if I'm wrapping up with final touches and create the final draft for this TS rewrite.

I was also able to find out what was degrading the performance. It appears that Object.freeze has some serious overhead. Without calling Object.freeze and returning a copy of values in CronField.values, the performance is even better than the old code.

I'd say drop it and let TypeScript warn people when they are making mistakes.

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.

4 participants