@zerotal/arch 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
package/docs/lock.md ADDED
@@ -0,0 +1,454 @@
1
+ ---
2
+ title: Locking
3
+ description: Coordinate critical sections across processes and servers with atomic, owner-guarded distributed locks.
4
+ ---
5
+
6
+ # Locking
7
+
8
+ `zerotal/lock` provides atomic distributed locks — mutual-exclusion primitives
9
+ that stop two processes or requests from running the same critical section at the
10
+ same time. Each acquisition carries a unique owner token, so an expired lock can
11
+ never be released by a late holder.
12
+
13
+ ## Getting Started
14
+
15
+ Locking ships as part of `@zerotal/core` — there is nothing extra to install. Import it from the `zerotal/lock` subpath:
16
+
17
+ ```ts
18
+ import { Lock, LockProvider } from "zerotal/lock";
19
+ ```
20
+
21
+ ## Register the provider
22
+
23
+ Add `LockProvider` to the providers array in `bootstrap/providers.ts`:
24
+
25
+ ```ts
26
+ // bootstrap/providers.ts
27
+ import { LockProvider } from "zerotal/lock";
28
+
29
+ const providers = [
30
+ // …your other providers
31
+ LockProvider,
32
+ ];
33
+
34
+ export default providers;
35
+ ```
36
+
37
+ Registering the provider switches on the following:
38
+
39
+ - `onRegister` — binds a `LockManager` as a lazy singleton on the `"lock"`
40
+ container key, selecting the driver from `config/lock.ts`.
41
+ - `onBooted` — pre-resolves the manager so the synchronous `Lock` facade works
42
+ immediately after boot.
43
+ - `onStopping` — disposes the driver (timers, connections), so nothing leaks
44
+ between boots or test suites.
45
+
46
+ The provider activates in the `web`, `worker`, `console`, and `test` environments.
47
+
48
+ ## Configuration
49
+
50
+ Create `config/lock.ts`. Use the `LockConfig()` helper so every field stays
51
+ type-checked and unset fields fall back to their defaults:
52
+
53
+ ```ts
54
+ // config/lock.ts
55
+ import { LockConfig } from "zerotal/lock";
56
+
57
+ export default LockConfig({
58
+ driver: "memory", // 'memory' | 'sqlite' | 'redis'
59
+ prefix: "zerotal_lock:", // prepended to every lock key
60
+ sqlite: {
61
+ path: ":memory:", // SQLite file path; ':memory:' for in-process
62
+ },
63
+ });
64
+ ```
65
+
66
+ | Field | Required | Default | Description |
67
+ | ------------- | -------- | ----------------- | ------------------------------------------------------ |
68
+ | `driver` | no | `"memory"` | Storage backend: `memory`, `sqlite`, or `redis`. |
69
+ | `prefix` | no | `"zerotal_lock:"` | Key prefix prepended to every lock key. |
70
+ | `sqlite.path` | no | `":memory:"` | SQLite file path; `':memory:'` keeps locks in-process. |
71
+
72
+ The `redis` driver reads its connection from `REDIS_URL` in the environment and
73
+ needs no config block of its own.
74
+
75
+ > **Warning** — SQLite path matters. `path: ':memory:'` is private to a single
76
+ > process, so it behaves like the `memory` driver. To coordinate locks across
77
+ > processes on one host, point `path` at a **file** (e.g. `storage/locks.sqlite`).
78
+
79
+ ### Which driver should I use?
80
+
81
+ | Driver | Scope | Use case |
82
+ | -------- | ----------------------- | ----------------------------------------------- |
83
+ | `memory` | Per-process | Development, tests, single-instance deployments |
84
+ | `sqlite` | Cross-process, one host | Multiple workers on the same server |
85
+ | `redis` | Cross-host | Multiple servers — requires `REDIS_URL` in env |
86
+
87
+ ## Basic usage
88
+
89
+ The `Lock` facade resolves the live `LockManager` from the container on every
90
+ call. `Lock.try` acquires once, runs your callback, and always releases — even if
91
+ the callback throws:
92
+
93
+ ```ts
94
+ // in a controller or service
95
+ import { Lock } from "zerotal/lock";
96
+
97
+ await Lock.try("invoice:123", 10, async () => {
98
+ // Only one process can run this at a time
99
+ await processInvoice(123);
100
+ });
101
+ ```
102
+
103
+ The second argument (`10`) is the TTL in seconds — the lock is force-released
104
+ after this time even if the callback hasn't finished, preventing deadlocks on
105
+ crashes.
106
+
107
+ ## The Lock facade
108
+
109
+ ### Lock.try — fail fast
110
+
111
+ Acquire once, run the callback, release. Throws `LockNotAcquiredError`
112
+ immediately if the lock is already held:
113
+
114
+ ```ts
115
+ // in a controller
116
+ import { Lock, LockNotAcquiredError } from "zerotal/lock";
117
+
118
+ try {
119
+ await Lock.try("invoice:123", 10, async () => {
120
+ await processInvoice(123);
121
+ });
122
+ } catch (err) {
123
+ if (err instanceof LockNotAcquiredError) {
124
+ // Another process is already handling invoice 123
125
+ return ctx.json({ error: "Processing in progress" }, 409);
126
+ }
127
+ throw err;
128
+ }
129
+ ```
130
+
131
+ ### Lock.block — wait for the lock
132
+
133
+ Wait up to `options.timeout` seconds for the lock to become free, then run the
134
+ callback:
135
+
136
+ ```ts
137
+ // in a service
138
+ import { Lock } from "zerotal/lock";
139
+
140
+ await Lock.block(
141
+ "report:export",
142
+ 30,
143
+ async () => {
144
+ await generateReport();
145
+ },
146
+ { timeout: 60, retryDelay: 200 },
147
+ ); // wait up to 60s, poll every 200ms
148
+ ```
149
+
150
+ | Option | Default | Description |
151
+ | ------------ | ------- | --------------------------------------- |
152
+ | `timeout` | TTL | Maximum seconds to wait before throwing |
153
+ | `retryDelay` | `100` | Milliseconds between polling attempts |
154
+
155
+ ### Lock.make — manual handle
156
+
157
+ For complex flows where you need explicit acquire/release control. `Lock.make`
158
+ returns a `ManagedLock` but does **not** acquire it — call `.acquire()` or
159
+ `.block()` yourself:
160
+
161
+ ```ts
162
+ // in a service
163
+ import { Lock } from "zerotal/lock";
164
+
165
+ const lock = Lock.make("payment:456", 15); // 15-second TTL
166
+
167
+ if (await lock.acquire()) {
168
+ try {
169
+ await processPayment(456);
170
+ } finally {
171
+ await lock.release();
172
+ }
173
+ } else {
174
+ // Lock is busy
175
+ }
176
+ ```
177
+
178
+ ## How locking works — TTL & ownership
179
+
180
+ Two mechanisms keep locks safe across crashes and races:
181
+
182
+ - **TTL** — every lock has a time-to-live (the seconds argument). The backend
183
+ auto-expires it after that, so a process that crashes mid-section can never
184
+ deadlock the key forever.
185
+ - **Owner token** — each acquisition gets a unique random token (`crypto.randomUUID()`).
186
+ `release()` only deletes the key if you're _still_ the owner. If your lock
187
+ expired and another process re-acquired it, your release is a guarded no-op —
188
+ you can't free someone else's lock.
189
+
190
+ > **Danger** — A lock that is not refreshed expires _while you're still working_
191
+ > if the callback outlives its TTL, and a second worker can acquire it. Either
192
+ > size the TTL above your worst-case duration, or — better — refresh it, which is
193
+ > what the next section is about.
194
+
195
+ The owner guard also means **`Lock.try` / `Lock.block` release automatically** even
196
+ when the callback throws — the `finally` runs `release()`, which is owner-checked.
197
+
198
+ ## Long-running work
199
+
200
+ Sizing a TTL forces an unpleasant trade. Too short and the lock evaporates mid-job;
201
+ too long and a crashed holder blocks the key for however long you guessed. Neither
202
+ number is knowable in advance, because the TTL is being asked to answer two
203
+ different questions at once.
204
+
205
+ Refreshing separates them. Pass `refresh: true` and the lock is extended in the
206
+ background for as long as the callback runs:
207
+
208
+ ```typescript
209
+ await Lock.block(
210
+ "report:monthly",
211
+ 60,
212
+ async (lock, signal) => {
213
+ await buildReport({ signal }); // may take an hour
214
+ },
215
+ { refresh: true },
216
+ );
217
+ ```
218
+
219
+ The TTL now means only **how long after a crash before someone else may take
220
+ over** — a decision, rather than a guess. Sixty seconds is a reasonable answer
221
+ whether the job takes a minute or a day.
222
+
223
+ Refreshing happens every `refreshEvery` seconds, defaulting to a third of the TTL
224
+ so a single missed beat is survivable.
225
+
226
+ ### When the lock is lost anyway
227
+
228
+ A refresh can fail — the process stalled long enough for the TTL to lapse, and
229
+ another holder took the key. That is not something to paper over: the work in
230
+ flight is no longer exclusive, and carrying on would mean two holders both
231
+ believing they are the only one.
232
+
233
+ So the callback's `AbortSignal` is aborted and `LockLostError` is thrown:
234
+
235
+ ```typescript
236
+ try {
237
+ await Lock.block("report:monthly", 60, run, { refresh: true });
238
+ } catch (err) {
239
+ if (err instanceof Lock.Lost) {
240
+ // Started, but cannot be trusted to have finished exclusively.
241
+ return;
242
+ }
243
+ throw err;
244
+ }
245
+ ```
246
+
247
+ Both callback arguments are additive — an existing zero-argument callback is
248
+ still valid, and nothing written before refreshing existed needs to change.
249
+
250
+ > **The signal is a request, not a guarantee.** Work that ignores it keeps
251
+ > running, outside the lock it thinks it holds. If a job can do damage after
252
+ > losing exclusivity, it has to check `signal.aborted` between steps — nothing
253
+ > can stop it from the outside.
254
+
255
+ ### Refreshing by hand
256
+
257
+ A manual [`ManagedLock`](#lockmake-manual-handle) exposes the same thing directly,
258
+ for flows that span steps rather than sitting inside one callback:
259
+
260
+ ```typescript
261
+ const lock = Lock.make("import:batch", 60);
262
+ if (await lock.acquire()) {
263
+ try {
264
+ for (const chunk of chunks) {
265
+ if (!(await lock.refresh())) throw new Error("lost the import lock");
266
+ await process(chunk);
267
+ }
268
+ } finally {
269
+ await lock.release();
270
+ }
271
+ }
272
+ ```
273
+
274
+ `refresh()` returns `false` when the lock is gone, and clears `isAcquired` so it
275
+ stops claiming otherwise. `lock.expiresAt` is a **client-side estimate** from the
276
+ last acquire or refresh — useful for deciding when to refresh next, not for
277
+ deciding whether you still hold the lock. Only the driver knows that, and asking
278
+ it is what `refresh()` does.
279
+
280
+ > **Custom drivers.** `extend` is optional on the `LockDriver` contract, so a
281
+ > driver written before this existed still compiles. Refreshing falls back to
282
+ > `acquire(key, owner, ttl)`, which is an owner-guarded refresh on all three
283
+ > built-in drivers.
284
+
285
+ ## Common patterns
286
+
287
+ ### Idempotent job processing
288
+
289
+ ```ts
290
+ // app/jobs/ProcessOrderJob.ts
291
+ import { Lock } from "zerotal/lock";
292
+
293
+ export class ProcessOrderJob extends Job {
294
+ async handle() {
295
+ await Lock.try(`order:${this.orderId}`, 60, async () => {
296
+ const order = await Order.findOrFail(this.orderId);
297
+ if (order.processed) return; // already done — exit early
298
+ await fulfillOrder(order);
299
+ await order.update({ processed: true });
300
+ });
301
+ }
302
+ }
303
+ ```
304
+
305
+ ### Rate-limited report generation
306
+
307
+ ```ts
308
+ // app/controllers/ReportController.ts
309
+ import { Lock, LockNotAcquiredError } from "zerotal/lock";
310
+
311
+ export class ReportController {
312
+ async generate(ctx: HttpContext) {
313
+ try {
314
+ await Lock.block(
315
+ `report:${ctx.user.id}`,
316
+ 300,
317
+ async () => {
318
+ const report = await buildReport(ctx.user);
319
+ await report.save();
320
+ },
321
+ { timeout: 5 },
322
+ ); // tell the user within 5s if it's busy
323
+ return ctx.json({ status: "done" });
324
+ } catch (err) {
325
+ if (err instanceof LockNotAcquiredError) {
326
+ return ctx.json({ error: "Report already generating" }, 429);
327
+ }
328
+ throw err;
329
+ }
330
+ }
331
+ }
332
+ ```
333
+
334
+ ## Error handling
335
+
336
+ Contention surfaces as a single typed error, `LockNotAcquiredError`:
337
+
338
+ - It carries the contended `key` (`err.key`).
339
+ - Its HTTP status is `409 Conflict`, so if it bubbles up to the framework error
340
+ handler unhandled, the client gets a 409 automatically.
341
+
342
+ The `Lock` facade re-exports it as `Lock.NotAcquired` for terse catch blocks:
343
+
344
+ ```ts
345
+ // in a controller
346
+ import { Lock } from "zerotal/lock";
347
+
348
+ try {
349
+ await Lock.try("invoice:123", 10, () => processInvoice(123));
350
+ } catch (err) {
351
+ if (err instanceof Lock.NotAcquired) {
352
+ return ctx.json({ error: "Already processing", key: err.key }, 409);
353
+ }
354
+ throw err;
355
+ }
356
+ ```
357
+
358
+ ## Custom drivers
359
+
360
+ A driver is any object implementing the `LockDriver` contract — each method must
361
+ be atomic at the backend level:
362
+
363
+ ```ts
364
+ // app/lock/MyLockDriver.ts
365
+ import type { LockDriver } from "zerotal/lock";
366
+
367
+ export class MyLockDriver implements LockDriver {
368
+ acquire(key: string, owner: string, ttlSeconds: number): Promise<boolean> {
369
+ /* … */
370
+ }
371
+ release(key: string, owner: string): Promise<boolean> {
372
+ /* owner-guarded */
373
+ }
374
+ forceRelease(key: string): Promise<void> {
375
+ /* … */
376
+ }
377
+ exists(key: string): Promise<boolean> {
378
+ /* … */
379
+ }
380
+ dispose?(): void {
381
+ /* release timers / connections */
382
+ }
383
+ }
384
+ ```
385
+
386
+ Bind a `LockManager` built around it in a provider that runs **after**
387
+ `LockProvider` (last write wins on the `"lock"` key):
388
+
389
+ ```ts
390
+ // app/providers/AppServiceProvider.ts
391
+ import { ServiceProvider } from "zerotal";
392
+ import { LockManager } from "zerotal/lock";
393
+ import { MyLockDriver } from "../lock/MyLockDriver.ts";
394
+
395
+ export class AppServiceProvider extends ServiceProvider {
396
+ override onRegister(): void {
397
+ this.app.container.singleton("lock", () => new LockManager(new MyLockDriver()));
398
+ }
399
+ }
400
+ ```
401
+
402
+ ## Testing
403
+
404
+ Use the `memory` driver in tests — no external dependencies needed:
405
+
406
+ ```ts
407
+ // config/lock.ts
408
+ import { LockConfig } from "zerotal/lock";
409
+
410
+ export default LockConfig({
411
+ driver: Bun.env.APP_ENV === "test" ? "memory" : "redis",
412
+ });
413
+ ```
414
+
415
+ `memory` is per-process, which is exactly what a test run wants: each test process
416
+ sees an isolated, deterministic lock table with no network or file I/O.
417
+
418
+ ## References
419
+
420
+ `Lock` facade — static entry point resolved from the container on each call:
421
+
422
+ | Method | Signature | Description |
423
+ | ------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------- |
424
+ | `try` | `try<T>(key: string, ttlSeconds: number, cb: () => Promise<T> \| T): Promise<T>` | Acquire once, run `cb`, always release. Throws if held. |
425
+ | `block` | `block<T>(key, ttlSeconds, cb, options?: BlockOptions): Promise<T>` | Wait up to `options.timeout` seconds, run `cb`, always release. |
426
+ | `make` | `make(key: string, ttlSeconds: number): ManagedLock` | Build a manual handle. Does **not** acquire. |
427
+ | `NotAcquired` | `typeof LockNotAcquiredError` | Re-export of the contention error for catch blocks. |
428
+
429
+ `ManagedLock` — a single named lock instance returned by `Lock.make`:
430
+
431
+ | Member | Signature | Description |
432
+ | ---------------- | ------------------------------------------------------------- | --------------------------------------------------- |
433
+ | `acquire()` | `acquire(): Promise<boolean>` | Try once — `true` if acquired. |
434
+ | `block()` | `block(timeoutSeconds: number, retryDelayMs?): Promise<void>` | Wait up to `timeoutSeconds`, throws on timeout. |
435
+ | `release()` | `release(): Promise<void>` | Release — no-op if not acquired or already expired. |
436
+ | `forceRelease()` | `forceRelease(): Promise<void>` | Unconditionally remove, regardless of owner. |
437
+ | `key` | `get key(): string` | The lock key. |
438
+ | `isAcquired` | `get isAcquired(): boolean` | Whether this instance currently holds the lock. |
439
+
440
+ `LockDriver` — the contract a storage backend must implement (all methods atomic):
441
+
442
+ | Method | Signature | Description |
443
+ | ---------------- | --------------------------------------------------------------------------- | --------------------------------------------------- |
444
+ | `acquire()` | `acquire(key: string, owner: string, ttlSeconds: number): Promise<boolean>` | `true` when acquired, `false` if held. |
445
+ | `release()` | `release(key: string, owner: string): Promise<boolean>` | Owner-guarded delete; `true` when released. |
446
+ | `forceRelease()` | `forceRelease(key: string): Promise<void>` | Unconditional delete. |
447
+ | `exists()` | `exists(key: string): Promise<boolean>` | `true` if the lock is currently held. |
448
+ | `dispose?()` | `dispose?(): void` | Release background resources (timers, connections). |
449
+
450
+ ## Next steps
451
+
452
+ - [Scheduler](/docs/scheduler#preventing-overlapping-runs) — `withoutOverlapping` builds on the same idea for cron tasks.
453
+ - [Queue](/docs/queue) — pair locks with jobs for idempotent processing.
454
+ - [Cache](/docs/cache) — same driver story (memory / sqlite / redis) for cached values.