Skip to content

Commit

Permalink
refactor: use locks for consistency
Browse files Browse the repository at this point in the history
  • Loading branch information
jmike committed Jan 21, 2024
1 parent 18fe5aa commit 0443a0a
Show file tree
Hide file tree
Showing 36 changed files with 502 additions and 358 deletions.
110 changes: 110 additions & 0 deletions lib/adapters/fileMigrationHistoryLog/acquireLock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as Either from 'fp-ts/Either';
import { pipe } from 'fp-ts/lib/function';
import * as TaskEither from 'fp-ts/TaskEither';
import { lock } from 'proper-lockfile';
import { isValidationErrorLike } from 'zod-validation-error';

import {
AcquireLockError,
InvalidMigrationHistoryLogError,
MigrationHistoryLogWriteError,
ReleaseLockError,
} from '../../errors';
import { History } from '../../models';
import type { MigrationHistoryLog } from '../../ports';
import { FileSystemWriteError, writeFile } from '../../utils/fs';
import { makeReadHistory } from './readHistory';
import type { FileMigrationHistoryLogContext } from './types';
import { toMigrationHistoryLogWriteError } from './utils/toMigrationHistoryLogWriteError';

export function makeAcquireLock(
ctx: FileMigrationHistoryLogContext
): MigrationHistoryLog['acquireLock'] {
const { filePath } = ctx;

return function acquireLock() {
return pipe(
TaskEither.tryCatch(
() => lock(filePath.toString()),
(err) =>
new AcquireLockError(
`Unable to acquire lock on "${filePath}"`,
err instanceof Error ? { cause: err } : undefined
)
),
TaskEither.bindTo('releaseLockAsync'),
TaskEither.bindW('defaultValue', makeReadHistory(ctx)),
TaskEither.map(({ defaultValue, releaseLockAsync }) => {
let value = defaultValue;
let isLockReleased = false;

return {
// use getter to prevent mutation
get currentValue() {
return value;
},
persistHistory: (history) =>
pipe(
// ensure lock has not been released
isLockReleased,
Either.fromPredicate(
(flag) => flag === false,
() =>
new MigrationHistoryLogWriteError(
`Unable to write to migration history-log; lock has been released`
)
),
// update history on disk
Either.flatMap(() => History.serialize(history)),
TaskEither.fromEither,
TaskEither.flatMap((contents) => writeFile(filePath, contents)),
// handle errors
TaskEither.mapLeft((err) => {
if (isValidationErrorLike(err)) {
return new InvalidMigrationHistoryLogError(err.message, {
cause: err,
});
}

if (err instanceof FileSystemWriteError) {
return toMigrationHistoryLogWriteError(err);
}

return err;
}),
// update value with new history
TaskEither.tap(() => {
value = history;
return TaskEither.of(void 0);
})
),
releaseLock: () =>
pipe(
// ensure lock has not been released
isLockReleased,
TaskEither.fromPredicate(
(flag) => flag === false,
() => new ReleaseLockError(`Lock has already been released`)
),
// release lock
TaskEither.flatMap(() =>
TaskEither.tryCatch(
() => releaseLockAsync(),
(err) =>
new ReleaseLockError(
`Unable to release lock on "${filePath}"`,
err instanceof Error ? { cause: err } : undefined
)
)
),
// set flag to true
TaskEither.tap(() => {
isLockReleased = true;
return TaskEither.of(undefined);
})
),
};
})
);
};
}
42 changes: 0 additions & 42 deletions lib/adapters/fileMigrationHistoryLog/addExecutedMigration.ts

This file was deleted.

8 changes: 4 additions & 4 deletions lib/adapters/fileMigrationHistoryLog/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { MigrationHistoryLog } from '../../ports';
import { makeAddExecutedMigration } from './addExecutedMigration';
import { makeGetExecutedMigrations } from './getExecutedMigrations';
import { makeAcquireLock } from './acquireLock';
import { makeInit } from './init';
import { makeReadHistory } from './readHistory';
import type { FileMigrationHistoryLogContext } from './types';

export const fileMigrationHistoryLog: {
Expand All @@ -10,6 +10,6 @@ export const fileMigrationHistoryLog: {
) => MigrationHistoryLog[K];
} = {
init: makeInit,
getExecutedMigrations: makeGetExecutedMigrations,
addExecutedMigration: makeAddExecutedMigration,
readHistory: makeReadHistory,
acquireLock: makeAcquireLock,
};
11 changes: 5 additions & 6 deletions lib/adapters/fileMigrationHistoryLog/init.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { mkdir, rm } from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';

Expand All @@ -10,7 +9,7 @@ import {
} from 'jest-fp-ts-matchers';

import { MigrationHistoryLogWriteError } from '../../errors';
import { stat, writeFile } from '../../utils/fs';
import { mkdir, rm, stat, writeFile } from '../../utils/fs';
import { makeInit } from './init';
import type { FileMigrationHistoryLogContext } from './types';

Expand All @@ -32,7 +31,7 @@ describe('init()', () => {
TaskEither.fromTask
),
(resource) => TaskEither.right(resource),
() => TaskEither.fromTask(() => rm(ctx.filePath, { recursive: true }))
() => rm(ctx.filePath, { recursive: true })
)();
});

Expand All @@ -53,7 +52,7 @@ describe('init()', () => {
TaskEither.fromTask
),
(resource) => TaskEither.right(resource),
() => TaskEither.fromTask(() => rm(ctx.filePath, { recursive: true }))
() => rm(ctx.filePath, { recursive: true })
)();
});

Expand All @@ -64,7 +63,7 @@ describe('init()', () => {

return TaskEither.bracket(
pipe(
TaskEither.fromTask(() => mkdir(ctx.filePath, { recursive: true })),
mkdir(ctx.filePath, { recursive: true }),
TaskEither.flatMap(makeInit(ctx)),
expectLeftTaskEither((err) => {
expect(err).toBeInstanceOf(MigrationHistoryLogWriteError);
Expand All @@ -75,7 +74,7 @@ describe('init()', () => {
TaskEither.fromTask
),
(resource) => TaskEither.right(resource),
() => TaskEither.fromTask(() => rm(ctx.filePath, { recursive: true }))
() => rm(ctx.filePath, { recursive: true })
)();
});
});
6 changes: 3 additions & 3 deletions lib/adapters/fileMigrationHistoryLog/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
MigrationHistoryLogReadError,
MigrationHistoryLogWriteError,
} from '../../errors';
import { HistoryLog } from '../../models';
import { History } from '../../models';
import type { MigrationHistoryLog } from '../../ports';
import {
FileOrDirectoryNotFoundError,
Expand Down Expand Up @@ -50,8 +50,8 @@ export function makeInit(
if (err instanceof FileOrDirectoryNotFoundError) {
// initialize history-log
return pipe(
HistoryLog.emptyHistoryLog,
HistoryLog.serialize,
History.emptyHistoryLog,
History.serialize,
TaskEither.fromEither,
TaskEither.flatMap((content) => writeFile(filePath, content)),
TaskEither.mapLeft((err) => {
Expand Down
26 changes: 14 additions & 12 deletions lib/adapters/fileMigrationHistoryLog/mock/history-log.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
[
{
"checksum": "f524ee4ad943b8312921906d9ef52b1b",
"id": "20240101-one",
"executedAt": "2024-01-01T00:00:00Z"
},
{
"checksum": "8c8878ce2d3db81ed8bfdadfca8f5b6a",
"id": "20240103-two",
"executedAt": "2024-01-03T09:54:32Z"
}
]
{
"entries": [
{
"checksum": "f524ee4ad943b8312921906d9ef52b1b",
"id": "20240101-one",
"executedAt": "2024-01-01T00:00:00Z"
},
{
"checksum": "8c8878ce2d3db81ed8bfdadfca8f5b6a",
"id": "20240103-two",
"executedAt": "2024-01-03T09:54:32Z"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,36 @@ import {
} from 'jest-fp-ts-matchers';

import { InvalidMigrationHistoryLogError } from '../../errors';
import { makeGetExecutedMigrations } from './getExecutedMigrations';
import { makeReadHistory } from './readHistory';
import type { FileMigrationHistoryLogContext } from './types';

describe('getExecutedMigrations()', () => {
it('retrieves executed migrations from disk', async () => {
describe('readHistory()', () => {
it('retrieves history log from disk', async () => {
const ctx: FileMigrationHistoryLogContext = {
filePath: path.resolve(__dirname, './mock/history-log.json'),
};

const getExecutedMigrations = makeGetExecutedMigrations(ctx);
const readHistory = makeReadHistory(ctx);

return pipe(
getExecutedMigrations(),
expectRightTaskEither((logEntries) => {
expect(logEntries).toHaveLength(2);
expect(logEntries).toMatchInlineSnapshot(`
[
{
"checksum": "f524ee4ad943b8312921906d9ef52b1b",
"executedAt": 2024-01-01T00:00:00.000Z,
"id": "20240101-one",
},
{
"checksum": "8c8878ce2d3db81ed8bfdadfca8f5b6a",
"executedAt": 2024-01-03T09:54:32.000Z,
"id": "20240103-two",
},
]
readHistory(),
expectRightTaskEither((history) => {
expect(history.entries).toHaveLength(2);
expect(history).toMatchInlineSnapshot(`
{
"entries": [
{
"checksum": "f524ee4ad943b8312921906d9ef52b1b",
"executedAt": 2024-01-01T00:00:00.000Z,
"id": "20240101-one",
},
{
"checksum": "8c8878ce2d3db81ed8bfdadfca8f5b6a",
"executedAt": 2024-01-03T09:54:32.000Z,
"id": "20240103-two",
},
],
}
`);
})
)();
Expand All @@ -45,10 +47,10 @@ describe('getExecutedMigrations()', () => {
filePath: path.resolve(__dirname, './mock/invalid-history-log-1.json'),
};

const getExecutedMigrations = makeGetExecutedMigrations(ctx);
const readHistory = makeReadHistory(ctx);

return pipe(
getExecutedMigrations(),
readHistory(),
expectLeftTaskEither((err) => {
expect(err).toBeInstanceOf(InvalidMigrationHistoryLogError);
expect(err.message).toMatchInlineSnapshot(
Expand All @@ -63,10 +65,10 @@ describe('getExecutedMigrations()', () => {
filePath: path.resolve(__dirname, './mock/invalid-history-log-2.json'),
};

const getExecutedMigrations = makeGetExecutedMigrations(ctx);
const readHistory = makeReadHistory(ctx);

return pipe(
getExecutedMigrations(),
readHistory(),
expectLeftTaskEither((err) => {
expect(err).toBeInstanceOf(InvalidMigrationHistoryLogError);
expect(err.message).toMatchInlineSnapshot(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
MigrationHistoryLogNotFoundError,
MigrationHistoryLogReadError,
} from '../../errors';
import { HistoryLog } from '../../models';
import { History } from '../../models';
import { MigrationHistoryLog } from '../../ports';
import {
FileOrDirectoryNotFoundError,
Expand All @@ -16,14 +16,14 @@ import {
import type { FileMigrationHistoryLogContext } from './types';
import { toInvalidMigrationHistoryLogError } from './utils/toInvalidMigrationHistoryLogError';

export function makeGetExecutedMigrations(
export function makeReadHistory(
ctx: FileMigrationHistoryLogContext
): MigrationHistoryLog['getExecutedMigrations'] {
): MigrationHistoryLog['readHistory'] {
const { filePath } = ctx;

return function getExecutedMigrations() {
return function readHistory() {
return pipe(
readFile(filePath),
readFile(filePath, { encoding: 'utf8' }),
TaskEither.mapLeft((err) => {
if (err instanceof FileOrDirectoryNotFoundError) {
return new MigrationHistoryLogNotFoundError(
Expand All @@ -43,7 +43,7 @@ export function makeGetExecutedMigrations(
}),
TaskEither.flatMapEither((contents) => {
return pipe(
HistoryLog.deserialize(contents),
History.deserialize(contents),
Either.mapLeft(toInvalidMigrationHistoryLogError)
);
})
Expand Down
4 changes: 3 additions & 1 deletion lib/adapters/fileMigrationHistoryLog/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { PathLike } from 'fs';

export type FileMigrationHistoryLogContext = {
filePath: string;
filePath: PathLike;
};
Loading

0 comments on commit 0443a0a

Please sign in to comment.