@shaferllc/keel 0.66.0 → 0.74.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 (239) hide show
  1. package/AGENTS.md +169 -0
  2. package/README.md +44 -6
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/api/api.config.stub +9 -0
  5. package/dist/api/config.d.ts +13 -0
  6. package/dist/api/config.js +14 -0
  7. package/dist/api/index.d.ts +16 -0
  8. package/dist/api/index.js +13 -0
  9. package/dist/api/provider.d.ts +10 -0
  10. package/dist/api/provider.js +17 -0
  11. package/dist/api/query.d.ts +35 -0
  12. package/dist/api/query.js +42 -0
  13. package/dist/api/resource.d.ts +91 -0
  14. package/dist/api/resource.js +188 -0
  15. package/dist/core/application.d.ts +5 -5
  16. package/dist/core/application.js +8 -2
  17. package/dist/core/cache.d.ts +81 -5
  18. package/dist/core/cache.js +188 -23
  19. package/dist/core/cli/stubs.d.ts +26 -0
  20. package/dist/core/cli/stubs.js +225 -0
  21. package/dist/core/console-prompt.d.ts +79 -0
  22. package/dist/core/console-prompt.js +239 -0
  23. package/dist/core/console-ui.d.ts +96 -0
  24. package/dist/core/console-ui.js +187 -0
  25. package/dist/core/console.d.ts +188 -0
  26. package/dist/core/console.js +395 -0
  27. package/dist/core/database.d.ts +70 -1
  28. package/dist/core/database.js +174 -15
  29. package/dist/core/env.d.ts +96 -0
  30. package/dist/core/env.js +140 -0
  31. package/dist/core/events.d.ts +129 -5
  32. package/dist/core/events.js +165 -7
  33. package/dist/core/health.d.ts +141 -0
  34. package/dist/core/health.js +226 -0
  35. package/dist/core/helpers.d.ts +9 -3
  36. package/dist/core/helpers.js +11 -3
  37. package/dist/core/http/kernel.d.ts +2 -0
  38. package/dist/core/http/kernel.js +48 -0
  39. package/dist/core/http/router.d.ts +5 -5
  40. package/dist/core/http/router.js +5 -5
  41. package/dist/core/i18n.d.ts +162 -0
  42. package/dist/core/i18n.js +472 -0
  43. package/dist/core/index.d.ts +39 -13
  44. package/dist/core/index.js +19 -6
  45. package/dist/core/instrumentation.d.ts +113 -0
  46. package/dist/core/instrumentation.js +52 -0
  47. package/dist/core/lock.d.ts +139 -0
  48. package/dist/core/lock.js +215 -0
  49. package/dist/core/logger.d.ts +89 -4
  50. package/dist/core/logger.js +167 -22
  51. package/dist/core/mail.d.ts +128 -7
  52. package/dist/core/mail.js +264 -16
  53. package/dist/core/notification.js +10 -1
  54. package/dist/core/package.d.ts +120 -0
  55. package/dist/core/package.js +169 -0
  56. package/dist/core/pages.d.ts +108 -0
  57. package/dist/core/pages.js +199 -0
  58. package/dist/core/queue.d.ts +134 -9
  59. package/dist/core/queue.js +325 -14
  60. package/dist/core/repl.d.ts +33 -0
  61. package/dist/core/repl.js +88 -0
  62. package/dist/core/scheduler.js +6 -0
  63. package/dist/core/social.d.ts +4 -4
  64. package/dist/core/social.js +4 -4
  65. package/dist/core/storage.d.ts +159 -6
  66. package/dist/core/storage.js +299 -7
  67. package/dist/core/telemetry.d.ts +208 -0
  68. package/dist/core/telemetry.js +383 -0
  69. package/dist/core/template.d.ts +2 -3
  70. package/dist/core/template.js +2 -3
  71. package/dist/core/testing.d.ts +170 -1
  72. package/dist/core/testing.js +504 -2
  73. package/dist/db/d1.js +13 -0
  74. package/dist/db/pg.d.ts +13 -0
  75. package/dist/db/pg.js +46 -4
  76. package/dist/mcp/server.d.ts +19 -0
  77. package/dist/mcp/server.js +355 -0
  78. package/dist/openapi/config.d.ts +28 -0
  79. package/dist/openapi/config.js +25 -0
  80. package/dist/openapi/doc.d.ts +40 -0
  81. package/dist/openapi/doc.js +20 -0
  82. package/dist/openapi/export.d.ts +8 -0
  83. package/dist/openapi/export.js +19 -0
  84. package/dist/openapi/gate.d.ts +15 -0
  85. package/dist/openapi/gate.js +27 -0
  86. package/dist/openapi/index.d.ts +19 -0
  87. package/dist/openapi/index.js +15 -0
  88. package/dist/openapi/openapi.config.stub +29 -0
  89. package/dist/openapi/provider.d.ts +18 -0
  90. package/dist/openapi/provider.js +35 -0
  91. package/dist/openapi/routes.d.ts +9 -0
  92. package/dist/openapi/routes.js +23 -0
  93. package/dist/openapi/spec.d.ts +23 -0
  94. package/dist/openapi/spec.js +132 -0
  95. package/dist/openapi/ui.d.ts +8 -0
  96. package/dist/openapi/ui.js +31 -0
  97. package/dist/openapi/zod.d.ts +12 -0
  98. package/dist/openapi/zod.js +46 -0
  99. package/dist/watch/config.d.ts +33 -0
  100. package/dist/watch/config.js +38 -0
  101. package/dist/watch/entry.d.ts +53 -0
  102. package/dist/watch/entry.js +105 -0
  103. package/dist/watch/gate.d.ts +20 -0
  104. package/dist/watch/gate.js +32 -0
  105. package/dist/watch/index.d.ts +21 -0
  106. package/dist/watch/index.js +17 -0
  107. package/dist/watch/migration.d.ts +7 -0
  108. package/dist/watch/migration.js +20 -0
  109. package/dist/watch/provider.d.ts +22 -0
  110. package/dist/watch/provider.js +58 -0
  111. package/dist/watch/prune.d.ts +11 -0
  112. package/dist/watch/prune.js +20 -0
  113. package/dist/watch/recorder.d.ts +24 -0
  114. package/dist/watch/recorder.js +39 -0
  115. package/dist/watch/routes.d.ts +13 -0
  116. package/dist/watch/routes.js +55 -0
  117. package/dist/watch/store.d.ts +54 -0
  118. package/dist/watch/store.js +158 -0
  119. package/dist/watch/ui/dist/watch.css +1 -0
  120. package/dist/watch/ui/dist/watch.js +555 -0
  121. package/dist/watch/ui-shell.d.ts +12 -0
  122. package/dist/watch/ui-shell.js +24 -0
  123. package/dist/watch/watch.config.stub +47 -0
  124. package/dist/watch/watchers.d.ts +12 -0
  125. package/dist/watch/watchers.js +156 -0
  126. package/docs/ai-manifest.json +3206 -0
  127. package/docs/ai.md +128 -0
  128. package/docs/api-resources.md +118 -0
  129. package/docs/architecture.md +331 -0
  130. package/docs/authentication.md +453 -0
  131. package/docs/authorization.md +167 -0
  132. package/docs/broadcasting.md +137 -0
  133. package/docs/broker.md +500 -0
  134. package/docs/cache.md +558 -0
  135. package/docs/configuration.md +385 -0
  136. package/docs/console.md +536 -0
  137. package/docs/container.md +467 -0
  138. package/docs/controllers.md +265 -0
  139. package/docs/cors.md +51 -0
  140. package/docs/database.md +631 -0
  141. package/docs/debugging.md +129 -0
  142. package/docs/decorators.md +127 -0
  143. package/docs/errors.md +395 -0
  144. package/docs/events.md +496 -0
  145. package/docs/examples/architecture-app.ts +27 -0
  146. package/docs/examples/authentication.ts +61 -0
  147. package/docs/examples/authorization.ts +79 -0
  148. package/docs/examples/broadcasting.ts +60 -0
  149. package/docs/examples/broker-cache-validate.ts +34 -0
  150. package/docs/examples/broker-fault-tolerance.ts +29 -0
  151. package/docs/examples/broker-middleware.ts +27 -0
  152. package/docs/examples/broker.ts +203 -0
  153. package/docs/examples/cache.ts +222 -0
  154. package/docs/examples/configuration.ts +121 -0
  155. package/docs/examples/console.ts +134 -0
  156. package/docs/examples/container.ts +134 -0
  157. package/docs/examples/controllers.ts +86 -0
  158. package/docs/examples/database.ts +208 -0
  159. package/docs/examples/debugging.ts +41 -0
  160. package/docs/examples/decorators.ts +40 -0
  161. package/docs/examples/errors.ts +121 -0
  162. package/docs/examples/events.ts +204 -0
  163. package/docs/examples/factories.ts +84 -0
  164. package/docs/examples/hashing.ts +71 -0
  165. package/docs/examples/health.ts +94 -0
  166. package/docs/examples/helpers.ts +171 -0
  167. package/docs/examples/hooks.ts +54 -0
  168. package/docs/examples/i18n.ts +117 -0
  169. package/docs/examples/inertia.ts +81 -0
  170. package/docs/examples/locks.ts +120 -0
  171. package/docs/examples/logger.ts +166 -0
  172. package/docs/examples/mail.ts +263 -0
  173. package/docs/examples/middleware.ts +119 -0
  174. package/docs/examples/migrations.ts +126 -0
  175. package/docs/examples/models.ts +239 -0
  176. package/docs/examples/notification.ts +124 -0
  177. package/docs/examples/pages.ts +82 -0
  178. package/docs/examples/providers.ts +123 -0
  179. package/docs/examples/queues.ts +254 -0
  180. package/docs/examples/rate-limiting.ts +42 -0
  181. package/docs/examples/redis.ts +99 -0
  182. package/docs/examples/request-response.ts +197 -0
  183. package/docs/examples/routing.ts +186 -0
  184. package/docs/examples/scheduling.ts +62 -0
  185. package/docs/examples/sessions.ts +102 -0
  186. package/docs/examples/static-files.ts +63 -0
  187. package/docs/examples/storage.ts +132 -0
  188. package/docs/examples/telemetry.ts +127 -0
  189. package/docs/examples/templates.ts +58 -0
  190. package/docs/examples/testing.ts +215 -0
  191. package/docs/examples/transformer.ts +141 -0
  192. package/docs/examples/transformers.ts +49 -0
  193. package/docs/examples/url-builder.ts +86 -0
  194. package/docs/examples/validation.ts +102 -0
  195. package/docs/examples/views.tsx +62 -0
  196. package/docs/examples/vite.ts +106 -0
  197. package/docs/factories.md +166 -0
  198. package/docs/getting-started.md +290 -0
  199. package/docs/hashing.md +259 -0
  200. package/docs/health.md +225 -0
  201. package/docs/helpers.md +347 -0
  202. package/docs/hono.md +186 -0
  203. package/docs/hooks.md +118 -0
  204. package/docs/i18n.md +302 -0
  205. package/docs/inertia.md +241 -0
  206. package/docs/locks.md +323 -0
  207. package/docs/logger.md +436 -0
  208. package/docs/mail.md +751 -0
  209. package/docs/middleware.md +425 -0
  210. package/docs/migrations.md +476 -0
  211. package/docs/models.md +810 -0
  212. package/docs/notifications.md +474 -0
  213. package/docs/openapi.md +111 -0
  214. package/docs/packages.md +118 -0
  215. package/docs/pages.md +217 -0
  216. package/docs/providers.md +363 -0
  217. package/docs/queues.md +679 -0
  218. package/docs/rate-limiting.md +155 -0
  219. package/docs/redis.md +178 -0
  220. package/docs/request-response.md +953 -0
  221. package/docs/routing.md +804 -0
  222. package/docs/scheduling.md +110 -0
  223. package/docs/security.md +85 -0
  224. package/docs/sessions.md +354 -0
  225. package/docs/social-auth.md +174 -0
  226. package/docs/static-files.md +211 -0
  227. package/docs/storage.md +452 -0
  228. package/docs/telemetry.md +263 -0
  229. package/docs/templates.md +314 -0
  230. package/docs/testing.md +376 -0
  231. package/docs/transformers.md +381 -0
  232. package/docs/url-builder.md +295 -0
  233. package/docs/validation.md +288 -0
  234. package/docs/views.md +267 -0
  235. package/docs/vite.md +434 -0
  236. package/docs/watch.md +118 -0
  237. package/llms-full.txt +19816 -0
  238. package/llms.txt +127 -0
  239. package/package.json +42 -7
package/docs/locks.md ADDED
@@ -0,0 +1,323 @@
1
+ # Locks
2
+
3
+ "Only one of you may do this at a time" — across processes, across nodes.
4
+
5
+ ```ts
6
+ import { lock } from "@shaferllc/keel/core";
7
+
8
+ const [ran] = await lock("invoice:42").run(async () => {
9
+ await charge(invoice);
10
+ });
11
+
12
+ if (!ran) return; // another worker is already charging it
13
+ ```
14
+
15
+ This is the counterpart to the [cache](./cache.md)'s stampede protection. That
16
+ collapses concurrent work **inside one isolate**; a lock coordinates work
17
+ **between** them. Reach for a lock when doing the thing twice would be *wrong* —
18
+ charging a card, sending an invoice, running a migration — not merely wasteful.
19
+
20
+ Like every other backend in Keel, the store is a small pluggable seam and the core
21
+ imports no driver. `MemoryLockStore` is the default; it's per-isolate, so it
22
+ coordinates within one process and nothing more — fine for tests and
23
+ single-process apps, useless across a cluster. Point it at Redis for the real
24
+ thing.
25
+
26
+ ## `run()` — the form you want
27
+
28
+ ```ts
29
+ const [ran, result] = await lock("invoice:42").run(() => charge(invoice));
30
+ ```
31
+
32
+ It acquires, runs, and **always** releases — the `finally` is what stops a
33
+ throwing callback from leaving the lock held until its TTL runs out. It returns
34
+ `[ran, result]`; `ran` is `false` if someone else holds the lock, in which case
35
+ your callback never ran and `result` is `undefined`.
36
+
37
+ By default it doesn't wait: if the lock is taken, it gives up immediately. To wait
38
+ for it, pass a timeout:
39
+
40
+ ```ts
41
+ // Wait up to 5 seconds for the lock, checking every 100ms.
42
+ const [ran] = await lock("report").run(() => rebuild(), {
43
+ timeout: 5_000,
44
+ retryDelay: 100,
45
+ });
46
+ ```
47
+
48
+ `runImmediately()` is the explicit "never wait" spelling.
49
+
50
+ ## TTL and expiry
51
+
52
+ **A lock always expires.** There's no hold-forever mode, because a holder that
53
+ crashes would keep the lock forever and nothing would ever run again. The default
54
+ TTL is 30 seconds:
55
+
56
+ ```ts
57
+ lock("invoice:42"); // held for 30s once acquired
58
+ lock("nightly-report", 300_000); // 5 minutes
59
+ ```
60
+
61
+ Pick a TTL comfortably longer than the work. If the work might outrun it, `extend()`
62
+ from inside:
63
+
64
+ ```ts
65
+ await lock("import", 60_000).run(async () => {
66
+ for (const batch of batches) {
67
+ await process(batch);
68
+ await l.extend(); // push the expiry out another 60s
69
+ }
70
+ });
71
+ ```
72
+
73
+ `extend()` **throws** `LockNotHeldError` if you've already lost the lock. That's
74
+ deliberate: the alternative — silently doing nothing — would let you carry on
75
+ believing you hold a lock you don't.
76
+
77
+ ## Ownership, and why it matters
78
+
79
+ Every acquisition mints a random owner token, and `release()`/`extend()` only
80
+ succeed for the owner. This is not bookkeeping — it's the property that makes the
81
+ lock correct:
82
+
83
+ 1. Process **A** takes the lock with a 30s TTL.
84
+ 2. A's work takes longer than 30s. The lock **expires**.
85
+ 3. Process **B** takes the now-free lock and starts working.
86
+ 4. A finishes and calls `release()`.
87
+
88
+ Without ownership, A's release deletes **B's** lock, and a third process walks
89
+ straight in while B is still working. With it, A's release is a no-op that returns
90
+ `false`. A store must *compare-and-delete*, not just delete.
91
+
92
+ ## Manual acquisition
93
+
94
+ `acquire()` / `release()` are there when the lock's lifetime doesn't fit a single
95
+ callback. You own the `try/finally`:
96
+
97
+ ```ts
98
+ const l = lock("invoice:42");
99
+ if (!(await l.acquire({ timeout: 2_000 }))) return;
100
+
101
+ try {
102
+ await charge(invoice);
103
+ } finally {
104
+ await l.release(); // without this, the lock leaks for the rest of its TTL
105
+ }
106
+ ```
107
+
108
+ Prefer `run()`. It exists so you can't forget the `finally`.
109
+
110
+ ## Handing a lock to another process
111
+
112
+ `serialize()` freezes the key, TTL, and **owner token** to a string.
113
+ `restoreLock()` rebuilds it elsewhere — so one process can take the lock and
114
+ another can release or extend the *same* lock:
115
+
116
+ ```ts
117
+ // worker A
118
+ const l = lock("import:99", 60_000);
119
+ await l.acquire();
120
+ await queue.dispatch(new FinishImport(l.serialize()));
121
+
122
+ // worker B, later
123
+ const l = restoreLock(serialized);
124
+ await l.extend(60_000);
125
+ // ...finish the work...
126
+ await l.release();
127
+ ```
128
+
129
+ ## Inspecting a lock
130
+
131
+ ```ts
132
+ await l.isLocked(); // does anyone hold this key? (not necessarily you)
133
+ await l.isExpired(); // did we hold it and lose it?
134
+ await l.getRemainingTime(); // ms until expiry, or null if unheld
135
+ ```
136
+
137
+ ## Testing
138
+
139
+ The default `MemoryLockStore` needs no setup. Give each test a clean one so keys
140
+ can't leak between them:
141
+
142
+ ```ts
143
+ import { setLockStore, MemoryLockStore } from "@shaferllc/keel/core";
144
+
145
+ beforeEach(() => setLockStore(new MemoryLockStore()));
146
+ ```
147
+
148
+ ## Writing a store
149
+
150
+ A store is the `LockStore` interface. Implementations **must** make `acquire`
151
+ atomic (set-if-absent) and make `release`/`extend` conditional on the owner
152
+ matching — a store that can't do both isn't a lock, it's a suggestion.
153
+
154
+ ### Redis
155
+
156
+ Redis gives you both: `SET key owner PX ttl NX` is an atomic set-if-absent, and a
157
+ small Lua script makes release and extend compare-and-act. This example uses
158
+ [ioredis](https://github.com/redis/ioredis); any client with `set` and `eval` works
159
+ the same way.
160
+
161
+ ```ts
162
+ import type { LockStore } from "@shaferllc/keel/core";
163
+ import type { Redis } from "ioredis";
164
+
165
+ // Compare-and-delete: only delete if the value still matches our owner token.
166
+ const RELEASE = `
167
+ if redis.call("get", KEYS[1]) == ARGV[1] then
168
+ return redis.call("del", KEYS[1])
169
+ end
170
+ return 0
171
+ `;
172
+
173
+ // Compare-and-extend, same idea.
174
+ const EXTEND = `
175
+ if redis.call("get", KEYS[1]) == ARGV[1] then
176
+ return redis.call("pexpire", KEYS[1], ARGV[2])
177
+ end
178
+ return 0
179
+ `;
180
+
181
+ export const redisLockStore = (client: Redis): LockStore => ({
182
+ async acquire(key, owner, ttlMs) {
183
+ // NX = only if absent. This is the atomic bit.
184
+ const res = await client.set(key, owner, "PX", ttlMs, "NX");
185
+ return res === "OK";
186
+ },
187
+ async release(key, owner) {
188
+ return (await client.eval(RELEASE, 1, key, owner)) === 1;
189
+ },
190
+ async extend(key, owner, ttlMs) {
191
+ return (await client.eval(EXTEND, 1, key, owner, String(ttlMs))) === 1;
192
+ },
193
+ async isLocked(key) {
194
+ return (await client.exists(key)) === 1;
195
+ },
196
+ async remainingTime(key) {
197
+ const ms = await client.pttl(key);
198
+ return ms < 0 ? null : ms; // -1 = no expiry, -2 = no key
199
+ },
200
+ });
201
+ ```
202
+
203
+ ```ts
204
+ setLockStore(redisLockStore(client));
205
+ ```
206
+
207
+ ### Database
208
+
209
+ A unique primary key on `key` gives you the atomicity — the insert fails if
210
+ someone already holds it. Expired rows are treated as free.
211
+
212
+ ```sql
213
+ CREATE TABLE locks (
214
+ key TEXT PRIMARY KEY,
215
+ owner TEXT NOT NULL,
216
+ expires_at BIGINT NOT NULL
217
+ );
218
+ ```
219
+
220
+ ```ts
221
+ import { connection } from "@shaferllc/keel/core";
222
+
223
+ export const databaseLockStore = (table = "locks"): LockStore => ({
224
+ async acquire(key, owner, ttlMs) {
225
+ const now = Date.now();
226
+ // Clear the row first if it has expired, then insert. The PK makes the
227
+ // insert the atomic step: two racing writers, one unique-violation.
228
+ await connection().write(`DELETE FROM ${table} WHERE key = ? AND expires_at <= ?`, [key, now]);
229
+ try {
230
+ await connection().write(
231
+ `INSERT INTO ${table} (key, owner, expires_at) VALUES (?, ?, ?)`,
232
+ [key, owner, now + ttlMs],
233
+ );
234
+ return true;
235
+ } catch {
236
+ return false; // someone else holds it
237
+ }
238
+ },
239
+ async release(key, owner) {
240
+ const res = await connection().write(`DELETE FROM ${table} WHERE key = ? AND owner = ?`, [key, owner]);
241
+ return res.changes > 0;
242
+ },
243
+ async extend(key, owner, ttlMs) {
244
+ const res = await connection().write(
245
+ `UPDATE ${table} SET expires_at = ? WHERE key = ? AND owner = ? AND expires_at > ?`,
246
+ [Date.now() + ttlMs, key, owner, Date.now()],
247
+ );
248
+ return res.changes > 0;
249
+ },
250
+ async isLocked(key) {
251
+ const rows = await connection().select(
252
+ `SELECT 1 FROM ${table} WHERE key = ? AND expires_at > ?`,
253
+ [key, Date.now()],
254
+ );
255
+ return rows.length > 0;
256
+ },
257
+ async remainingTime(key) {
258
+ const rows = await connection().select(`SELECT expires_at FROM ${table} WHERE key = ?`, [key]);
259
+ const row = rows[0] as { expires_at: number } | undefined;
260
+ if (!row) return null;
261
+ const left = Number(row.expires_at) - Date.now();
262
+ return left > 0 ? left : null;
263
+ },
264
+ });
265
+ ```
266
+
267
+ Redis is the better fit if you have it — the database store pays a round trip per
268
+ operation and needs the expired rows cleaned up.
269
+
270
+ ---
271
+
272
+ ## API reference
273
+
274
+ ### `lock(key, ttlMs?)`
275
+
276
+ `lock(key: string, ttlMs?: number): Lock`
277
+
278
+ A lock on `key`, held for `ttlMs` once acquired (default `30_000`).
279
+
280
+ ### `Lock`
281
+
282
+ | Method | Signature |
283
+ |--------|-----------|
284
+ | `run` | `<T>(fn, options?: AcquireOptions) => Promise<[boolean, T \| undefined]>` — acquire, run, always release |
285
+ | `runImmediately` | `<T>(fn) => Promise<[boolean, T \| undefined]>` — never waits |
286
+ | `acquire` | `(options?: AcquireOptions) => Promise<boolean>` |
287
+ | `acquireImmediately` | `() => Promise<boolean>` |
288
+ | `release` | `() => Promise<boolean>` — false if we no longer hold it |
289
+ | `extend` | `(ttlMs?) => Promise<void>` — throws `LockNotHeldError` if lost |
290
+ | `isLocked` | `() => Promise<boolean>` — does *anyone* hold it |
291
+ | `isExpired` | `() => Promise<boolean>` — did *we* hold it and lose it |
292
+ | `getRemainingTime` | `() => Promise<number \| null>` — ms until expiry |
293
+ | `serialize` | `() => string` — key + TTL + owner token |
294
+
295
+ ### `restoreLock(serialized)`
296
+
297
+ `restoreLock(serialized: string): Lock` — rebuild a lock from `serialize()`, owner
298
+ token and all, so another process can release or extend it.
299
+
300
+ ### `setLockStore(store)` / `getLockStore()`
301
+
302
+ Register the store `lock()` uses, and read it back.
303
+
304
+ ### Interfaces & types
305
+
306
+ #### `LockStore`
307
+
308
+ `acquire(key, owner, ttlMs)` / `release(key, owner)` / `extend(key, owner, ttlMs)` /
309
+ `isLocked(key)` / `remainingTime(key)`. `acquire` must be atomic; `release` and
310
+ `extend` must be conditional on the owner.
311
+
312
+ #### `AcquireOptions`
313
+
314
+ `{ timeout?: number, retryDelay?: number }` — how long to wait for a held lock
315
+ (default `0`, don't wait) and how often to retry (default `50`ms).
316
+
317
+ #### `MemoryLockStore`
318
+
319
+ The default. Per-isolate — for tests and single-process apps.
320
+
321
+ #### `LockNotHeldError`
322
+
323
+ Thrown by `extend()` when the lock has expired or was never acquired.