Skip to content

Commit 1b62d85

Browse files
committed
feat: support Duration values and lock.acquire options
1 parent 0339e2e commit 1b62d85

8 files changed

Lines changed: 173 additions & 59 deletions

File tree

packages/verrou/src/errors.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import { createError } from '@poppinss/utils'
22

3+
/**
4+
* Thrown when the lock is not acquired in the allotted time
5+
*/
36
export const E_LOCK_TIMEOUT = createError(
47
`Lock was not acquired in the allotted time`,
58
'E_LOCK_TIMEOUT',
69
)
710

11+
/**
12+
* Thrown when user tries to update/release/extend a lock that is not acquired by them
13+
*/
814
export const E_LOCK_NOT_OWNED = createError(
915
'Looks like you are trying to update a lock that is not acquired by you',
1016
'E_LOCK_NOT_OWNED',
1117
)
1218

19+
/**
20+
* Thrown when the underlying store throws an error while saving/deleting/reading a lock
21+
*/
1322
export const E_LOCK_STORAGE_ERROR = createError<[{ message: string }]>(
1423
'Lock storage error: %s',
1524
'E_LOCK_STORAGE_ERROR',

packages/verrou/src/helpers.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import string from '@poppinss/utils/string'
33
import type { Duration } from './types/main.js'
44

55
/**
6-
* Resolve a TTL value to a number in milliseconds
6+
* Resolve a Duration to a number in milliseconds
77
*/
8-
export function resolveDuration(ttl?: Duration, defaultTtl: Duration = 30_000) {
9-
if (typeof ttl === 'number') return ttl
10-
if (ttl === null) return undefined
8+
export function resolveDuration(duration?: Duration, defaultValue: Duration = 30_000) {
9+
if (typeof duration === 'number') return duration
10+
if (duration === null) return undefined
1111

12-
if (ttl === undefined) {
13-
if (typeof defaultTtl === 'number') return defaultTtl
14-
if (typeof defaultTtl === 'string') return string.milliseconds.parse(defaultTtl)
12+
if (duration === undefined) {
13+
if (typeof defaultValue === 'number') return defaultValue
14+
if (typeof defaultValue === 'string') return string.milliseconds.parse(defaultValue)
1515

1616
return undefined
1717
}
1818

19-
return string.milliseconds.parse(ttl)
19+
return string.milliseconds.parse(duration)
2020
}

packages/verrou/src/lock.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { resolveDuration } from './helpers.js'
66
import type {
77
Duration,
88
LockAcquireOptions,
9-
LockFactoryConfig,
9+
ResolvedLockConfig,
1010
LockStore,
1111
SerializedLock,
1212
} from './types/main.js'
@@ -16,13 +16,13 @@ export class Lock {
1616
#owner: string
1717
#lockStore: LockStore
1818
#ttl: number | null = null
19-
#config: LockFactoryConfig
19+
#config: ResolvedLockConfig
2020
#expirationTime: number | null = null
2121

2222
constructor(
2323
key: string,
2424
lockStore: LockStore,
25-
config: LockFactoryConfig,
25+
config: ResolvedLockConfig,
2626
owner?: string,
2727
ttl?: number | null,
2828
expirationTime?: number | null,
@@ -64,14 +64,15 @@ export class Lock {
6464
/**
6565
* Acquire the lock
6666
*/
67-
async acquire(options?: LockAcquireOptions) {
67+
async acquire(options: LockAcquireOptions = {}) {
6868
this.#expirationTime = null
6969

7070
let attemptsDone = 0
71-
const start = Date.now()
72-
const attemptsMax =
73-
options?.retry?.attempts ?? this.#config.retry.attempts ?? Number.POSITIVE_INFINITY
71+
const attemptsMax = options.retry?.attempts ?? this.#config.retry.attempts
72+
const delay = resolveDuration(options.retry?.delay, this.#config.retry.delay)
73+
const timeout = resolveDuration(options.retry?.timeout, this.#config.retry.timeout)
7474

75+
const start = Date.now()
7576
while (attemptsDone++ < attemptsMax) {
7677
const now = Date.now()
7778
const result = await this.#lockStore.save(this.#key, this.#owner, this.#ttl)
@@ -83,11 +84,9 @@ export class Lock {
8384
if (attemptsDone === attemptsMax) throw new E_LOCK_TIMEOUT()
8485

8586
const elapsed = Date.now() - start
86-
if (this.#config.retry.timeout && elapsed > this.#config.retry.timeout) {
87-
throw new E_LOCK_TIMEOUT()
88-
}
87+
if (timeout && elapsed > timeout) throw new E_LOCK_TIMEOUT()
8988

90-
await setTimeout(this.#config.retry.delay ?? 250)
89+
await setTimeout(delay)
9190
}
9291

9392
this.#config.logger.debug({ key: this.#key }, 'Lock acquired')

packages/verrou/src/lock_factory.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,52 @@ import { Lock } from './lock.js'
44
import { resolveDuration } from './helpers.js'
55
import type {
66
Duration,
7-
LockFactoryConfig,
7+
ResolvedLockConfig,
88
LockFactoryOptions,
99
LockStore,
1010
SerializedLock,
1111
} from './types/main.js'
1212

1313
export class LockFactory {
1414
/**
15-
* Default TTL for locks. 30 seconds.
15+
* Default configuration values
1616
*/
17-
static #kDefaultTtl = 30_000
17+
static #kDefaults = {
18+
ttl: 30_000,
19+
retry: {
20+
attempts: Number.POSITIVE_INFINITY,
21+
delay: 250,
22+
timeout: undefined,
23+
},
24+
}
25+
26+
/**
27+
* The store used to persist locks
28+
*/
29+
#store: LockStore
1830

1931
/**
2032
* Resolved LockFactory configuration
2133
*/
22-
#config: LockFactoryConfig
34+
#config: ResolvedLockConfig
2335

24-
constructor(
25-
protected readonly store: LockStore,
26-
options: LockFactoryOptions = {},
27-
) {
36+
constructor(store: LockStore, options: LockFactoryOptions = {}) {
37+
this.#store = store
2838
this.#config = {
29-
retry: { attempts: null, delay: 250, ...options.retry },
39+
retry: {
40+
attempts: options.retry?.attempts ?? LockFactory.#kDefaults.retry.attempts,
41+
delay: resolveDuration(options.retry?.delay, null) ?? LockFactory.#kDefaults.retry.delay,
42+
timeout: resolveDuration(options.retry?.timeout, null),
43+
},
3044
logger: (options.logger ?? noopLogger()).child({ pkg: 'verrou' }),
3145
}
3246
}
3347

3448
/**
3549
* Create a new lock
3650
*/
37-
createLock(name: string, ttl: Duration = LockFactory.#kDefaultTtl) {
38-
return new Lock(name, this.store, this.#config, undefined, resolveDuration(ttl))
51+
createLock(name: string, ttl: Duration = LockFactory.#kDefaults.ttl) {
52+
return new Lock(name, this.#store, this.#config, undefined, resolveDuration(ttl))
3953
}
4054

4155
/**
@@ -44,13 +58,13 @@ export class LockFactory {
4458
* that acquired it.
4559
*/
4660
restoreLock(lock: SerializedLock) {
47-
return new Lock(lock.key, this.store, this.#config, lock.owner, lock.ttl, lock.expirationTime)
61+
return new Lock(lock.key, this.#store, this.#config, lock.owner, lock.ttl, lock.expirationTime)
4862
}
4963

5064
/**
5165
* Disconnect the store ( if applicable )
5266
*/
5367
disconnect() {
54-
return this.store.disconnect()
68+
return this.#store.disconnect()
5569
}
5670
}

packages/verrou/src/types/main.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ export * from './drivers.js'
1111
* - Null to indicate no expiration
1212
*/
1313
export type Duration = number | string | null
14+
export type StoreFactory = { driver: { factory: () => LockStore } }
1415

15-
export type StoreFactory = {
16-
driver: { factory: () => LockStore }
17-
}
18-
16+
/**
17+
* Shape of a lock serialized through the `Lock.serialize` method
18+
*/
1919
export interface SerializedLock {
2020
key: string
2121
owner: string
@@ -26,36 +26,51 @@ export interface SerializedLock {
2626
export interface RetryConfig {
2727
/**
2828
* The number of times to retry the operation before giving up.
29-
* Null means retry indefinitely
3029
*
31-
* @default null
30+
* @default Number.POSITIVE_INFINITY
3231
*/
33-
attempts?: number | null
32+
attempts?: number
3433

3534
/**
3635
* The delay between retries
3736
*
3837
* @default 250ms
3938
*/
40-
delay?: number
39+
delay?: Duration
4140

4241
/**
4342
* The maximum amount of time before giving up to acquire the lock
43+
* If not specified, the operation will retry indefinitely
44+
*
45+
* @default undefined ( no timeout )
4446
*/
45-
timeout?: number
47+
timeout?: Duration
4648
}
4749

50+
/**
51+
* Options passed to the Lock.acquire method
52+
*/
4853
export interface LockAcquireOptions {
4954
retry?: RetryConfig
5055
}
5156

57+
/**
58+
* Options passed to the LockFactory constructor
59+
*/
5260
export interface LockFactoryOptions {
5361
retry?: RetryConfig
5462
logger?: Logger
5563
}
5664

57-
export interface LockFactoryConfig {
58-
retry: RetryConfig
65+
/**
66+
* Resolved configuration values
67+
*/
68+
export interface ResolvedLockConfig {
69+
retry: {
70+
attempts: number
71+
delay: number
72+
timeout?: number
73+
}
5974
logger: Logger
6075
}
6176

packages/verrou/test_helpers/null_store.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { LockStore } from '../src/types/main.js'
22

3+
/**
4+
* A null store that does nothing
5+
* Useful for testing and extending/overriding
6+
*/
37
export class NullStore implements LockStore {
48
async save(_key: string): Promise<boolean> {
59
return true

0 commit comments

Comments
 (0)