-
-
Notifications
You must be signed in to change notification settings - Fork 160
/
file-locker.ts
89 lines (77 loc) · 2.56 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 * as fs from 'fs'
import * as path from 'path'
import type {Umzug} from './umzug'
export type 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)
}
}