@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.
- package/AGENTS.md +169 -0
- package/README.md +44 -6
- package/bin/keel-mcp.mjs +9 -0
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +91 -0
- package/dist/api/resource.js +188 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +8 -2
- package/dist/core/cache.d.ts +81 -5
- package/dist/core/cache.js +188 -23
- package/dist/core/cli/stubs.d.ts +26 -0
- package/dist/core/cli/stubs.js +225 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +39 -13
- package/dist/core/index.js +19 -6
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +89 -4
- package/dist/core/logger.js +167 -22
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +120 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +325 -14
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +299 -7
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +170 -1
- package/dist/core/testing.js +504 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +20 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +3206 -0
- package/docs/ai.md +128 -0
- package/docs/api-resources.md +118 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +385 -0
- package/docs/console.md +536 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +631 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +121 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +208 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +166 -0
- package/docs/examples/mail.ts +263 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +215 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/i18n.md +302 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +436 -0
- package/docs/mail.md +751 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +452 -0
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +314 -0
- package/docs/testing.md +376 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +19816 -0
- package/llms.txt +127 -0
- 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.
|