forked from sequelize/umzug
-
Notifications
You must be signed in to change notification settings - Fork 0
/
file-locker.ts
89 lines (77 loc) · 2.51 KB
/
file-locker.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import fs = require('fs');
import path = require('path');
import { Umzug } from './umzug';
export interface FileLockerOptions {
path: string;
fs?: typeof fs;
}
/**
* Simple locker using the filesystem. Only one lock can be held per file. An error will be thrown if the
* lock file already exists.
*
* @example
* const umzug = new Umzug({ ... })
* FileLocker.attach(umzug, { path: 'path/to/lockfile' })
*
* @docs
* To wait for the lock to be free instead of throwing, you could extend it (the below example uses `setInterval`,
* but depending on your use-case, you may want to use a library with retry/backoff):
*
* @example
* class WaitingFileLocker extends FileLocker {
* async getLock() {
* return new Promise(resolve => setInterval(
* () => super.getLock().then(resolve).catch(),
* 500,
* )
* }
* }
*
* const locker = new WaitingFileLocker({ path: 'path/to/lockfile' })
* locker.attachTo(umzug)
*/
export class FileLocker {
private readonly lockFile: string;
private readonly fs: typeof fs;
constructor(params: FileLockerOptions) {
this.lockFile = params.path;
this.fs = params.fs ?? fs;
}
/** Attach `beforeAll` and `afterAll` events to an umzug instance which use the specified filepath */
static attach(umzug: Umzug, params: FileLockerOptions): void {
const locker = new FileLocker(params);
locker.attachTo(umzug);
}
/** Attach lock handlers to `beforeCommand` and `afterCommand` events on an umzug instance */
attachTo(umzug: Umzug): void {
umzug.on('beforeCommand', async () => this.getLock());
umzug.on('afterCommand', async () => this.releaseLock());
}
private async readFile(filepath: string): Promise<string | undefined> {
return this.fs.promises.readFile(filepath).then(
buf => buf.toString(),
() => undefined
);
}
private async writeFile(filepath: string, content: string): Promise<void> {
await this.fs.promises.mkdir(path.dirname(filepath), { recursive: true });
await this.fs.promises.writeFile(filepath, content);
}
private async removeFile(filepath: string): Promise<void> {
await this.fs.promises.unlink(filepath);
}
async getLock(): Promise<void> {
const existing = await this.readFile(this.lockFile);
if (existing) {
throw new Error(`Can't acquire lock. ${this.lockFile} exists`);
}
await this.writeFile(this.lockFile, 'lock');
}
async releaseLock(): Promise<void> {
const existing = await this.readFile(this.lockFile);
if (!existing) {
throw new Error(`Nothing to unlock`);
}
await this.removeFile(this.lockFile);
}
}