Skip to content

Commit

Permalink
fix(jobs): execute close only once
Browse files Browse the repository at this point in the history
  • Loading branch information
TBonnin committed Nov 22, 2024
1 parent 1c3d47f commit 0c4be5f
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 5 deletions.
6 changes: 3 additions & 3 deletions packages/jobs/lib/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import './tracer.js';
import { Processor } from './processor/processor.js';
import { server } from './server.js';
import { deleteSyncsData } from './crons/deleteSyncsData.js';
import { getLogger, stringifyError } from '@nangohq/utils';
import { getLogger, stringifyError, once } from '@nangohq/utils';
import { timeoutLogsOperations } from './crons/timeoutLogsOperations.js';
import { envs } from './env.js';
import db from '@nangohq/database';
Expand Down Expand Up @@ -31,15 +31,15 @@ try {
};
void check();

const close = async () => {
const close = once(async () => {
logger.info('Closing...');
clearTimeout(healthCheck);
processor.stop();
await db.knex.destroy();
srv.close(() => {
process.exit();
});
};
});

process.on('SIGINT', () => {
logger.info('Received SIGINT...');
Expand Down
6 changes: 4 additions & 2 deletions packages/utils/lib/once.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// Ensures a function is only called once.
export function once<T extends any[]>(fn: (...args: T) => void): (...args: T) => void {
export function once<T extends any[], R>(fn: (...args: T) => R): (...args: T) => ReturnType<typeof fn> {
let called = false;
let result: R;

return function (...args: T) {
if (!called) {
called = true;
fn(...args);
result = fn(...args);
}
return result;
};
}
65 changes: 65 additions & 0 deletions packages/utils/lib/once.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { once } from './once.js';
import { describe, expect, it, vi } from 'vitest';

describe('once', () => {
it('should handle sync functions', () => {
const mockFn = vi.fn();
const onceFn = once(mockFn);

onceFn();
onceFn();
onceFn();

expect(mockFn).toHaveBeenCalledTimes(1);
});

it('should handle async functions', async () => {
const mockAsyncFn = vi.fn().mockImplementation(async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
});
const onceFn = once(mockAsyncFn);

await onceFn();
await onceFn();
await onceFn();

expect(mockAsyncFn).toHaveBeenCalledTimes(1);
});

it('should pass arguments correctly', () => {
const mockFn = vi.fn();
const onceFn = once(mockFn);

onceFn('first', 123);
onceFn('ignored', 456);

expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith('first', 123);
});

it('should memoize the result', async () => {
const mockFn = vi.fn().mockImplementation((n: number) => {
return n;
});
const onceFn = once(mockFn);

const res1 = await onceFn(1);
const res2 = await onceFn(2);
const res3 = await onceFn(3);

expect(res1).toStrictEqual(1);
expect(res2).toStrictEqual(1);
expect(res3).toStrictEqual(1);
expect(mockFn).toHaveBeenCalledTimes(1);
});

it('should handle errors', async () => {
const mockFn = vi.fn().mockRejectedValue(new Error('myerror'));
const onceFn = once(mockFn);

await expect(onceFn()).rejects.toThrow('myerror');
await expect(onceFn()).rejects.toThrow('myerror');
await expect(onceFn()).rejects.toThrow('myerror');
expect(mockFn).toHaveBeenCalledTimes(1);
});
});

0 comments on commit 0c4be5f

Please # to comment.