@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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework instrumentation — the typed event stream that lets a package observe
|
|
3
|
+
* the framework without patching it. Core seams (the query builder, the HTTP
|
|
4
|
+
* kernel, the queue, the cache, …) call `instrument(event, payload)`; anything
|
|
5
|
+
* that wants to watch subscribes with `listen()`. The mail layer already works
|
|
6
|
+
* this way (`mail.sent`); this generalises it.
|
|
7
|
+
*
|
|
8
|
+
* These events are deliberately fire-and-forget: `instrument()` never blocks the
|
|
9
|
+
* work it describes, and a listener that throws can't break a query or a
|
|
10
|
+
* request. That's the difference between instrumentation and business logic.
|
|
11
|
+
*
|
|
12
|
+
* A **request id** ties a request to everything that happened inside it — the
|
|
13
|
+
* queries it ran, the logs it wrote, the jobs it dispatched. The kernel opens a
|
|
14
|
+
* request scope with `runRequest(id, …)`; anything emitted inside it can read
|
|
15
|
+
* `currentRequestId()` to attribute itself. It flows through `await` chains via
|
|
16
|
+
* `AsyncLocalStorage`, so nothing has to be threaded by hand.
|
|
17
|
+
*/
|
|
18
|
+
/** A SQL statement that ran on a connection. */
|
|
19
|
+
export interface QueryEvent {
|
|
20
|
+
sql: string;
|
|
21
|
+
bindings: unknown[];
|
|
22
|
+
/** Wall-clock time the statement took, in milliseconds. */
|
|
23
|
+
durationMs: number;
|
|
24
|
+
/** The registered connection name it ran on. */
|
|
25
|
+
connection: string;
|
|
26
|
+
/** "select" (row-returning) or "write" (insert/update/delete/DDL). */
|
|
27
|
+
kind: "select" | "write";
|
|
28
|
+
/** The request that ran it, if any. */
|
|
29
|
+
requestId?: string;
|
|
30
|
+
}
|
|
31
|
+
/** A request that finished (or threw). */
|
|
32
|
+
export interface RequestEvent {
|
|
33
|
+
id: string;
|
|
34
|
+
method: string;
|
|
35
|
+
path: string;
|
|
36
|
+
status: number;
|
|
37
|
+
durationMs: number;
|
|
38
|
+
/** Request headers, lower-cased (auth/cookies are redacted by watchers). */
|
|
39
|
+
headers: Record<string, string>;
|
|
40
|
+
ip?: string;
|
|
41
|
+
}
|
|
42
|
+
/** An error that reached the HTTP kernel. */
|
|
43
|
+
export interface ExceptionEvent {
|
|
44
|
+
error: unknown;
|
|
45
|
+
method?: string;
|
|
46
|
+
path?: string;
|
|
47
|
+
/** The status it rendered as. */
|
|
48
|
+
status: number;
|
|
49
|
+
requestId?: string;
|
|
50
|
+
}
|
|
51
|
+
/** A queued job at one point in its lifecycle. */
|
|
52
|
+
export interface JobEvent {
|
|
53
|
+
job: string;
|
|
54
|
+
payload?: unknown;
|
|
55
|
+
/** Set on `job.processed` / `job.failed`. */
|
|
56
|
+
durationMs?: number;
|
|
57
|
+
/** Set on `job.failed`. */
|
|
58
|
+
error?: unknown;
|
|
59
|
+
requestId?: string;
|
|
60
|
+
}
|
|
61
|
+
/** A cache lookup. */
|
|
62
|
+
export interface CacheEvent {
|
|
63
|
+
key: string;
|
|
64
|
+
store: string;
|
|
65
|
+
requestId?: string;
|
|
66
|
+
}
|
|
67
|
+
/** A notification that was sent. */
|
|
68
|
+
export interface NotificationEvent {
|
|
69
|
+
notification: string;
|
|
70
|
+
channels: string[];
|
|
71
|
+
notifiable?: unknown;
|
|
72
|
+
requestId?: string;
|
|
73
|
+
}
|
|
74
|
+
/** A scheduled task that ran. */
|
|
75
|
+
export interface ScheduleEvent {
|
|
76
|
+
task: string;
|
|
77
|
+
durationMs: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Register the instrumentation events on the framework's typed event registry,
|
|
81
|
+
* so `listen("db.query", (e) => …)` gets a fully-typed `e`. Any code that
|
|
82
|
+
* augments `EventsList` merges with this — declaring your own events elsewhere
|
|
83
|
+
* still works.
|
|
84
|
+
*/
|
|
85
|
+
declare module "./events.js" {
|
|
86
|
+
interface EventsList {
|
|
87
|
+
"db.query": QueryEvent;
|
|
88
|
+
"request.handled": RequestEvent;
|
|
89
|
+
"exception": ExceptionEvent;
|
|
90
|
+
"job.processing": JobEvent;
|
|
91
|
+
"job.processed": JobEvent;
|
|
92
|
+
"job.failed": JobEvent;
|
|
93
|
+
"cache.hit": CacheEvent;
|
|
94
|
+
"cache.miss": CacheEvent;
|
|
95
|
+
"notification.sent": NotificationEvent;
|
|
96
|
+
"schedule.task.run": ScheduleEvent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** The instrumentation event names, for iteration and toggling. */
|
|
100
|
+
export type InstrumentEvent = "db.query" | "request.handled" | "exception" | "job.processing" | "job.processed" | "job.failed" | "cache.hit" | "cache.miss" | "notification.sent" | "schedule.task.run";
|
|
101
|
+
/** A random id for a request/batch. 16 hex chars is plenty to be unique. */
|
|
102
|
+
export declare function newRequestId(): string;
|
|
103
|
+
/** Run `fn` inside a request scope, so everything it does can be attributed. */
|
|
104
|
+
export declare function runRequest<T>(id: string, fn: () => T): T;
|
|
105
|
+
/** The id of the request currently in scope, if any. */
|
|
106
|
+
export declare function currentRequestId(): string | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* Emit an instrumentation event — fire-and-forget. Never throws, never blocks:
|
|
109
|
+
* a broken watcher can't take down the request it was watching, and the work
|
|
110
|
+
* being measured doesn't wait on the measurement. A no-op when there's no live
|
|
111
|
+
* application (e.g. a unit test that never booted one).
|
|
112
|
+
*/
|
|
113
|
+
export declare function instrument<E extends InstrumentEvent>(event: E, payload: import("./events.js").EventsList[E]): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework instrumentation — the typed event stream that lets a package observe
|
|
3
|
+
* the framework without patching it. Core seams (the query builder, the HTTP
|
|
4
|
+
* kernel, the queue, the cache, …) call `instrument(event, payload)`; anything
|
|
5
|
+
* that wants to watch subscribes with `listen()`. The mail layer already works
|
|
6
|
+
* this way (`mail.sent`); this generalises it.
|
|
7
|
+
*
|
|
8
|
+
* These events are deliberately fire-and-forget: `instrument()` never blocks the
|
|
9
|
+
* work it describes, and a listener that throws can't break a query or a
|
|
10
|
+
* request. That's the difference between instrumentation and business logic.
|
|
11
|
+
*
|
|
12
|
+
* A **request id** ties a request to everything that happened inside it — the
|
|
13
|
+
* queries it ran, the logs it wrote, the jobs it dispatched. The kernel opens a
|
|
14
|
+
* request scope with `runRequest(id, …)`; anything emitted inside it can read
|
|
15
|
+
* `currentRequestId()` to attribute itself. It flows through `await` chains via
|
|
16
|
+
* `AsyncLocalStorage`, so nothing has to be threaded by hand.
|
|
17
|
+
*/
|
|
18
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
19
|
+
import { emit, hasApplication } from "./helpers.js";
|
|
20
|
+
/* ------------------------------ request scope ----------------------------- */
|
|
21
|
+
const store = new AsyncLocalStorage();
|
|
22
|
+
/** A random id for a request/batch. 16 hex chars is plenty to be unique. */
|
|
23
|
+
export function newRequestId() {
|
|
24
|
+
const bytes = new Uint8Array(8);
|
|
25
|
+
crypto.getRandomValues(bytes);
|
|
26
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
27
|
+
}
|
|
28
|
+
/** Run `fn` inside a request scope, so everything it does can be attributed. */
|
|
29
|
+
export function runRequest(id, fn) {
|
|
30
|
+
return store.run({ id }, fn);
|
|
31
|
+
}
|
|
32
|
+
/** The id of the request currently in scope, if any. */
|
|
33
|
+
export function currentRequestId() {
|
|
34
|
+
return store.getStore()?.id;
|
|
35
|
+
}
|
|
36
|
+
/* -------------------------------- emitting -------------------------------- */
|
|
37
|
+
/**
|
|
38
|
+
* Emit an instrumentation event — fire-and-forget. Never throws, never blocks:
|
|
39
|
+
* a broken watcher can't take down the request it was watching, and the work
|
|
40
|
+
* being measured doesn't wait on the measurement. A no-op when there's no live
|
|
41
|
+
* application (e.g. a unit test that never booted one).
|
|
42
|
+
*/
|
|
43
|
+
export function instrument(event, payload) {
|
|
44
|
+
if (!hasApplication())
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
void Promise.resolve(emit(event, payload)).catch(() => { });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// hasApplication() raced with teardown — instrumentation stays silent.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distributed locks — "only one of you may do this at a time", across processes
|
|
3
|
+
* and across nodes. The counterpart to the cache's stampede protection: that
|
|
4
|
+
* collapses concurrent work *inside one isolate*, this one coordinates work
|
|
5
|
+
* *between* them.
|
|
6
|
+
*
|
|
7
|
+
* const [ran] = await lock("invoice:42").run(async () => charge(invoice));
|
|
8
|
+
* if (!ran) return; // someone else is already charging it
|
|
9
|
+
*
|
|
10
|
+
* Like every other backend in keel, the store is a small pluggable seam and the
|
|
11
|
+
* core imports no driver — `MemoryLockStore` is the built-in default (per
|
|
12
|
+
* isolate, so it's for tests and single-process apps); the locks guide has Redis
|
|
13
|
+
* and database recipes for the real thing.
|
|
14
|
+
*
|
|
15
|
+
* ## Two safety properties, both easy to get wrong
|
|
16
|
+
*
|
|
17
|
+
* **Ownership.** Every acquisition mints a random owner token, and release and
|
|
18
|
+
* extend only succeed for the owner. Without that, a lock whose TTL expired
|
|
19
|
+
* mid-work gets picked up by process B, and process A's `release()` — arriving
|
|
20
|
+
* late — would delete *B's* lock, letting a third process in. A store must
|
|
21
|
+
* compare-and-delete, not just delete.
|
|
22
|
+
*
|
|
23
|
+
* **TTL.** A lock always expires. If a holder crashes, the lock must not be held
|
|
24
|
+
* forever, so there's no "hold until released" mode. Pick a TTL longer than the
|
|
25
|
+
* work; if the work might outrun it, `extend()` from inside.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* The bridge to a lock backend.
|
|
29
|
+
*
|
|
30
|
+
* Every method is keyed by `owner` for the reason above: implementations MUST
|
|
31
|
+
* make `acquire` atomic (set-if-absent) and `release`/`extend` conditional on
|
|
32
|
+
* the owner matching. A store that can't do that isn't a lock.
|
|
33
|
+
*/
|
|
34
|
+
export interface LockStore {
|
|
35
|
+
/** Set the key if absent. Returns whether this owner got it. Must be atomic. */
|
|
36
|
+
acquire(key: string, owner: string, ttlMs: number): Promise<boolean>;
|
|
37
|
+
/** Delete the key only if `owner` still holds it. Returns whether it did. */
|
|
38
|
+
release(key: string, owner: string): Promise<boolean>;
|
|
39
|
+
/** Push the expiry out, only if `owner` still holds it. */
|
|
40
|
+
extend(key: string, owner: string, ttlMs: number): Promise<boolean>;
|
|
41
|
+
/** Whether anyone currently holds the key. */
|
|
42
|
+
isLocked(key: string): Promise<boolean>;
|
|
43
|
+
/** Milliseconds until the key expires, or null if nobody holds it. */
|
|
44
|
+
remainingTime(key: string): Promise<number | null>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* An in-memory `LockStore` — the default. Per-isolate, so it coordinates
|
|
48
|
+
* *within* one process and nothing more: perfect for tests and single-process
|
|
49
|
+
* apps, useless across a cluster. Point `setLockStore()` at Redis for that.
|
|
50
|
+
*/
|
|
51
|
+
export declare class MemoryLockStore implements LockStore {
|
|
52
|
+
private entries;
|
|
53
|
+
/** The live entry for a key, dropping it if it has expired. */
|
|
54
|
+
private live;
|
|
55
|
+
acquire(key: string, owner: string, ttlMs: number): Promise<boolean>;
|
|
56
|
+
release(key: string, owner: string): Promise<boolean>;
|
|
57
|
+
extend(key: string, owner: string, ttlMs: number): Promise<boolean>;
|
|
58
|
+
isLocked(key: string): Promise<boolean>;
|
|
59
|
+
remainingTime(key: string): Promise<number | null>;
|
|
60
|
+
}
|
|
61
|
+
export interface AcquireOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Give up after this many milliseconds of waiting. Default: 0 — don't wait at
|
|
64
|
+
* all, fail immediately if the lock is held.
|
|
65
|
+
*/
|
|
66
|
+
timeout?: number;
|
|
67
|
+
/** Milliseconds between attempts while waiting. Default: 50. */
|
|
68
|
+
retryDelay?: number;
|
|
69
|
+
}
|
|
70
|
+
/** A lock that has expired or was never held. */
|
|
71
|
+
export declare class LockNotHeldError extends Error {
|
|
72
|
+
constructor(key: string);
|
|
73
|
+
}
|
|
74
|
+
export declare class Lock {
|
|
75
|
+
readonly key: string;
|
|
76
|
+
/** How long the lock is held before it expires on its own. Milliseconds. */
|
|
77
|
+
readonly ttlMs: number;
|
|
78
|
+
private store;
|
|
79
|
+
/** Non-null only while this instance holds the lock. */
|
|
80
|
+
private owner?;
|
|
81
|
+
constructor(key: string,
|
|
82
|
+
/** How long the lock is held before it expires on its own. Milliseconds. */
|
|
83
|
+
ttlMs: number, store: LockStore, owner?: string);
|
|
84
|
+
/**
|
|
85
|
+
* Take the lock, run `fn`, and always give it back — the form you want almost
|
|
86
|
+
* every time, because the `finally` is what stops a throwing callback from
|
|
87
|
+
* leaving the lock held until its TTL runs out.
|
|
88
|
+
*
|
|
89
|
+
* Returns `[ran, result]`: `ran` is false if someone else holds it, in which
|
|
90
|
+
* case `fn` never ran.
|
|
91
|
+
*
|
|
92
|
+
* const [ran, invoice] = await lock("invoice:42").run(() => charge(id));
|
|
93
|
+
* if (!ran) return; // another worker is on it
|
|
94
|
+
*/
|
|
95
|
+
run<T>(fn: () => Promise<T> | T, options?: AcquireOptions): Promise<[boolean, T | undefined]>;
|
|
96
|
+
/** `run()`, but never waits: if the lock is held, give up at once. */
|
|
97
|
+
runImmediately<T>(fn: () => Promise<T> | T): Promise<[boolean, T | undefined]>;
|
|
98
|
+
/**
|
|
99
|
+
* Take the lock, waiting up to `timeout` for it. Returns whether we got it.
|
|
100
|
+
*
|
|
101
|
+
* Prefer `run()` — with a bare `acquire()` you own the `try/finally`, and a
|
|
102
|
+
* throw between here and `release()` leaks the lock for the rest of its TTL.
|
|
103
|
+
*/
|
|
104
|
+
acquire(options?: AcquireOptions): Promise<boolean>;
|
|
105
|
+
/** Take the lock only if it's free right now. */
|
|
106
|
+
acquireImmediately(): Promise<boolean>;
|
|
107
|
+
/** Give the lock back. A no-op if we no longer hold it (it may have expired). */
|
|
108
|
+
release(): Promise<boolean>;
|
|
109
|
+
/**
|
|
110
|
+
* Push the expiry out — for work that might outrun the TTL. Throws
|
|
111
|
+
* `LockNotHeldError` if we've already lost it, because the alternative (a
|
|
112
|
+
* silent no-op) would let you carry on believing you hold a lock you don't.
|
|
113
|
+
*/
|
|
114
|
+
extend(ttlMs?: number): Promise<void>;
|
|
115
|
+
/** Whether *anyone* holds this key — not necessarily us. */
|
|
116
|
+
isLocked(): Promise<boolean>;
|
|
117
|
+
/** Whether we held this lock but no longer do. */
|
|
118
|
+
isExpired(): Promise<boolean>;
|
|
119
|
+
/** Milliseconds until the lock expires, or null if nobody holds it. */
|
|
120
|
+
getRemainingTime(): Promise<number | null>;
|
|
121
|
+
/**
|
|
122
|
+
* Freeze the lock (key, TTL, owner token) to a string, so another process can
|
|
123
|
+
* `restoreLock()` it and release or extend the *same* lock — the handoff you
|
|
124
|
+
* need when one process takes the lock and another finishes the work.
|
|
125
|
+
*/
|
|
126
|
+
serialize(): string;
|
|
127
|
+
}
|
|
128
|
+
/** Register the lock store used by `lock()`. */
|
|
129
|
+
export declare function setLockStore(next: LockStore): LockStore;
|
|
130
|
+
/** The active lock store. */
|
|
131
|
+
export declare function getLockStore(): LockStore;
|
|
132
|
+
/**
|
|
133
|
+
* A lock on `key`, held for `ttlMs` once acquired (default 30s).
|
|
134
|
+
*
|
|
135
|
+
* const [ran] = await lock("invoice:42").run(() => charge(invoice));
|
|
136
|
+
*/
|
|
137
|
+
export declare function lock(key: string, ttlMs?: number): Lock;
|
|
138
|
+
/** Rebuild a lock from `serialize()` — same key, same TTL, same owner token. */
|
|
139
|
+
export declare function restoreLock(serialized: string): Lock;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distributed locks — "only one of you may do this at a time", across processes
|
|
3
|
+
* and across nodes. The counterpart to the cache's stampede protection: that
|
|
4
|
+
* collapses concurrent work *inside one isolate*, this one coordinates work
|
|
5
|
+
* *between* them.
|
|
6
|
+
*
|
|
7
|
+
* const [ran] = await lock("invoice:42").run(async () => charge(invoice));
|
|
8
|
+
* if (!ran) return; // someone else is already charging it
|
|
9
|
+
*
|
|
10
|
+
* Like every other backend in keel, the store is a small pluggable seam and the
|
|
11
|
+
* core imports no driver — `MemoryLockStore` is the built-in default (per
|
|
12
|
+
* isolate, so it's for tests and single-process apps); the locks guide has Redis
|
|
13
|
+
* and database recipes for the real thing.
|
|
14
|
+
*
|
|
15
|
+
* ## Two safety properties, both easy to get wrong
|
|
16
|
+
*
|
|
17
|
+
* **Ownership.** Every acquisition mints a random owner token, and release and
|
|
18
|
+
* extend only succeed for the owner. Without that, a lock whose TTL expired
|
|
19
|
+
* mid-work gets picked up by process B, and process A's `release()` — arriving
|
|
20
|
+
* late — would delete *B's* lock, letting a third process in. A store must
|
|
21
|
+
* compare-and-delete, not just delete.
|
|
22
|
+
*
|
|
23
|
+
* **TTL.** A lock always expires. If a holder crashes, the lock must not be held
|
|
24
|
+
* forever, so there's no "hold until released" mode. Pick a TTL longer than the
|
|
25
|
+
* work; if the work might outrun it, `extend()` from inside.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* An in-memory `LockStore` — the default. Per-isolate, so it coordinates
|
|
29
|
+
* *within* one process and nothing more: perfect for tests and single-process
|
|
30
|
+
* apps, useless across a cluster. Point `setLockStore()` at Redis for that.
|
|
31
|
+
*/
|
|
32
|
+
export class MemoryLockStore {
|
|
33
|
+
entries = new Map();
|
|
34
|
+
/** The live entry for a key, dropping it if it has expired. */
|
|
35
|
+
live(key) {
|
|
36
|
+
const entry = this.entries.get(key);
|
|
37
|
+
if (!entry)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (entry.expiresAt <= Date.now()) {
|
|
40
|
+
this.entries.delete(key);
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
async acquire(key, owner, ttlMs) {
|
|
46
|
+
if (this.live(key))
|
|
47
|
+
return false;
|
|
48
|
+
this.entries.set(key, { owner, expiresAt: Date.now() + ttlMs });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
async release(key, owner) {
|
|
52
|
+
const entry = this.live(key);
|
|
53
|
+
if (!entry || entry.owner !== owner)
|
|
54
|
+
return false;
|
|
55
|
+
this.entries.delete(key);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
async extend(key, owner, ttlMs) {
|
|
59
|
+
const entry = this.live(key);
|
|
60
|
+
if (!entry || entry.owner !== owner)
|
|
61
|
+
return false;
|
|
62
|
+
entry.expiresAt = Date.now() + ttlMs;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
async isLocked(key) {
|
|
66
|
+
return this.live(key) !== undefined;
|
|
67
|
+
}
|
|
68
|
+
async remainingTime(key) {
|
|
69
|
+
const entry = this.live(key);
|
|
70
|
+
return entry ? entry.expiresAt - Date.now() : null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** A lock that has expired or was never held. */
|
|
74
|
+
export class LockNotHeldError extends Error {
|
|
75
|
+
constructor(key) {
|
|
76
|
+
super(`The lock "${key}" is not held by you — it expired or was released.`);
|
|
77
|
+
this.name = "LockNotHeldError";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
81
|
+
/** A random owner token. Web Crypto, so it works on Node and the edge. */
|
|
82
|
+
function mintOwner() {
|
|
83
|
+
return crypto.randomUUID();
|
|
84
|
+
}
|
|
85
|
+
/* ---------------------------------- lock ---------------------------------- */
|
|
86
|
+
export class Lock {
|
|
87
|
+
key;
|
|
88
|
+
ttlMs;
|
|
89
|
+
store;
|
|
90
|
+
/** Non-null only while this instance holds the lock. */
|
|
91
|
+
owner;
|
|
92
|
+
constructor(key,
|
|
93
|
+
/** How long the lock is held before it expires on its own. Milliseconds. */
|
|
94
|
+
ttlMs, store, owner) {
|
|
95
|
+
this.key = key;
|
|
96
|
+
this.ttlMs = ttlMs;
|
|
97
|
+
this.store = store;
|
|
98
|
+
this.owner = owner;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Take the lock, run `fn`, and always give it back — the form you want almost
|
|
102
|
+
* every time, because the `finally` is what stops a throwing callback from
|
|
103
|
+
* leaving the lock held until its TTL runs out.
|
|
104
|
+
*
|
|
105
|
+
* Returns `[ran, result]`: `ran` is false if someone else holds it, in which
|
|
106
|
+
* case `fn` never ran.
|
|
107
|
+
*
|
|
108
|
+
* const [ran, invoice] = await lock("invoice:42").run(() => charge(id));
|
|
109
|
+
* if (!ran) return; // another worker is on it
|
|
110
|
+
*/
|
|
111
|
+
async run(fn, options = {}) {
|
|
112
|
+
if (!(await this.acquire(options)))
|
|
113
|
+
return [false, undefined];
|
|
114
|
+
try {
|
|
115
|
+
return [true, await fn()];
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await this.release();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** `run()`, but never waits: if the lock is held, give up at once. */
|
|
122
|
+
runImmediately(fn) {
|
|
123
|
+
return this.run(fn, { timeout: 0 });
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Take the lock, waiting up to `timeout` for it. Returns whether we got it.
|
|
127
|
+
*
|
|
128
|
+
* Prefer `run()` — with a bare `acquire()` you own the `try/finally`, and a
|
|
129
|
+
* throw between here and `release()` leaks the lock for the rest of its TTL.
|
|
130
|
+
*/
|
|
131
|
+
async acquire(options = {}) {
|
|
132
|
+
const { timeout = 0, retryDelay = 50 } = options;
|
|
133
|
+
const deadline = Date.now() + timeout;
|
|
134
|
+
const owner = mintOwner();
|
|
135
|
+
for (;;) {
|
|
136
|
+
if (await this.store.acquire(this.key, owner, this.ttlMs)) {
|
|
137
|
+
this.owner = owner;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
// Wait only if there's time left to wait *and* to sleep before the deadline.
|
|
141
|
+
if (Date.now() + retryDelay >= deadline)
|
|
142
|
+
return false;
|
|
143
|
+
await sleep(retryDelay);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Take the lock only if it's free right now. */
|
|
147
|
+
acquireImmediately() {
|
|
148
|
+
return this.acquire({ timeout: 0 });
|
|
149
|
+
}
|
|
150
|
+
/** Give the lock back. A no-op if we no longer hold it (it may have expired). */
|
|
151
|
+
async release() {
|
|
152
|
+
if (!this.owner)
|
|
153
|
+
return false;
|
|
154
|
+
const released = await this.store.release(this.key, this.owner);
|
|
155
|
+
this.owner = undefined;
|
|
156
|
+
return released;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Push the expiry out — for work that might outrun the TTL. Throws
|
|
160
|
+
* `LockNotHeldError` if we've already lost it, because the alternative (a
|
|
161
|
+
* silent no-op) would let you carry on believing you hold a lock you don't.
|
|
162
|
+
*/
|
|
163
|
+
async extend(ttlMs = this.ttlMs) {
|
|
164
|
+
if (!this.owner || !(await this.store.extend(this.key, this.owner, ttlMs))) {
|
|
165
|
+
this.owner = undefined;
|
|
166
|
+
throw new LockNotHeldError(this.key);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Whether *anyone* holds this key — not necessarily us. */
|
|
170
|
+
isLocked() {
|
|
171
|
+
return this.store.isLocked(this.key);
|
|
172
|
+
}
|
|
173
|
+
/** Whether we held this lock but no longer do. */
|
|
174
|
+
async isExpired() {
|
|
175
|
+
if (!this.owner)
|
|
176
|
+
return false; // never acquired — not the same as expired
|
|
177
|
+
return !(await this.store.isLocked(this.key));
|
|
178
|
+
}
|
|
179
|
+
/** Milliseconds until the lock expires, or null if nobody holds it. */
|
|
180
|
+
getRemainingTime() {
|
|
181
|
+
return this.store.remainingTime(this.key);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Freeze the lock (key, TTL, owner token) to a string, so another process can
|
|
185
|
+
* `restoreLock()` it and release or extend the *same* lock — the handoff you
|
|
186
|
+
* need when one process takes the lock and another finishes the work.
|
|
187
|
+
*/
|
|
188
|
+
serialize() {
|
|
189
|
+
return JSON.stringify({ key: this.key, ttlMs: this.ttlMs, owner: this.owner });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/* --------------------------------- global --------------------------------- */
|
|
193
|
+
let store = new MemoryLockStore();
|
|
194
|
+
/** Register the lock store used by `lock()`. */
|
|
195
|
+
export function setLockStore(next) {
|
|
196
|
+
store = next;
|
|
197
|
+
return store;
|
|
198
|
+
}
|
|
199
|
+
/** The active lock store. */
|
|
200
|
+
export function getLockStore() {
|
|
201
|
+
return store;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* A lock on `key`, held for `ttlMs` once acquired (default 30s).
|
|
205
|
+
*
|
|
206
|
+
* const [ran] = await lock("invoice:42").run(() => charge(invoice));
|
|
207
|
+
*/
|
|
208
|
+
export function lock(key, ttlMs = 30_000) {
|
|
209
|
+
return new Lock(key, ttlMs, store);
|
|
210
|
+
}
|
|
211
|
+
/** Rebuild a lock from `serialize()` — same key, same TTL, same owner token. */
|
|
212
|
+
export function restoreLock(serialized) {
|
|
213
|
+
const { key, ttlMs, owner } = JSON.parse(serialized);
|
|
214
|
+
return new Lock(key, ttlMs, store, owner);
|
|
215
|
+
}
|
package/dist/core/logger.d.ts
CHANGED
|
@@ -5,8 +5,37 @@
|
|
|
5
5
|
*
|
|
6
6
|
* logger().info("user registered", { userId: user.id });
|
|
7
7
|
* logger().error("payment failed", { orderId, error });
|
|
8
|
+
*
|
|
9
|
+
* Output goes through a `Sink` — `console` by default, but any function, so logs
|
|
10
|
+
* can go to a file, an HTTP collector, or a buffer in tests. Register extra
|
|
11
|
+
* loggers by name (`setLogger(audit, "audit")`) when a subsystem needs its own
|
|
12
|
+
* level or destination.
|
|
8
13
|
*/
|
|
9
|
-
|
|
14
|
+
/** Levels, quietest last. `trace` is the most verbose; `fatal` the most severe. */
|
|
15
|
+
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
|
16
|
+
/** One log event, as handed to a `Sink`. */
|
|
17
|
+
export interface LogRecord {
|
|
18
|
+
level: LogLevel;
|
|
19
|
+
/** ISO 8601. */
|
|
20
|
+
time: string;
|
|
21
|
+
msg: string;
|
|
22
|
+
/** Bindings merged with the call's context, after redaction. */
|
|
23
|
+
fields: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
/** Where log lines go. Return nothing; a sink that throws is a sink that lies. */
|
|
26
|
+
export type Sink = (record: LogRecord) => void;
|
|
27
|
+
export interface RedactOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Field paths to redact — a top-level key (`"password"`), a dot path
|
|
30
|
+
* (`"req.headers.authorization"`), or a wildcard segment (`"*.password"`,
|
|
31
|
+
* `"user.*.token"`).
|
|
32
|
+
*/
|
|
33
|
+
paths: string[];
|
|
34
|
+
/** What to replace matched values with. Default: `"[redacted]"`. */
|
|
35
|
+
censor?: string;
|
|
36
|
+
/** Delete the key outright instead of censoring it. Default: false. */
|
|
37
|
+
remove?: boolean;
|
|
38
|
+
}
|
|
10
39
|
export interface LoggerOptions {
|
|
11
40
|
/** Minimum level to emit. Default: "info". */
|
|
12
41
|
level?: LogLevel;
|
|
@@ -15,20 +44,76 @@ export interface LoggerOptions {
|
|
|
15
44
|
/** Fields merged into every log line. */
|
|
16
45
|
bindings?: Record<string, unknown>;
|
|
17
46
|
/**
|
|
18
|
-
* Field paths to redact
|
|
19
|
-
*
|
|
47
|
+
* Field paths to redact. A `string[]` is shorthand for `{ paths }` — matched
|
|
48
|
+
* values become `"[redacted]"`.
|
|
20
49
|
*/
|
|
21
|
-
redact?: string[];
|
|
50
|
+
redact?: string[] | RedactOptions;
|
|
51
|
+
/** Where lines go. Default: the console (`console.log` / `.warn` / `.error`). */
|
|
52
|
+
sink?: Sink;
|
|
53
|
+
/** Silence the logger entirely — nothing is emitted, at any level. */
|
|
54
|
+
enabled?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** The default sink: JSON to stdout, or a pretty single line when `pretty`. */
|
|
57
|
+
export declare function consoleSink(pretty?: boolean): Sink;
|
|
58
|
+
/** A sink that collects records in memory — for tests. Assert on `.records`. */
|
|
59
|
+
export declare class MemorySink {
|
|
60
|
+
readonly records: LogRecord[];
|
|
61
|
+
/** The sink function to hand to `LoggerOptions.sink`. */
|
|
62
|
+
readonly sink: Sink;
|
|
63
|
+
/** Records at one level. */
|
|
64
|
+
at(level: LogLevel): LogRecord[];
|
|
65
|
+
/** The messages logged, in order. */
|
|
66
|
+
messages(): string[];
|
|
67
|
+
clear(): void;
|
|
22
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Observe every log record emitted anywhere in the app. Returns an unsubscribe.
|
|
71
|
+
* A tap that throws is ignored — watching logs must never break logging.
|
|
72
|
+
*
|
|
73
|
+
* const off = tapLogs((record) => store.record(record));
|
|
74
|
+
*/
|
|
75
|
+
export declare function tapLogs(sink: Sink): () => void;
|
|
23
76
|
export declare class Logger {
|
|
24
77
|
private options;
|
|
25
78
|
private threshold;
|
|
79
|
+
private redact?;
|
|
80
|
+
private sink;
|
|
26
81
|
constructor(options?: LoggerOptions);
|
|
82
|
+
/**
|
|
83
|
+
* Whether a level would be emitted. Check it before building an expensive
|
|
84
|
+
* context object — the work of assembling it happens whether or not the line
|
|
85
|
+
* is ever written.
|
|
86
|
+
*
|
|
87
|
+
* if (logger().isLevelEnabled("debug")) {
|
|
88
|
+
* logger().debug("state", { snapshot: expensiveSnapshot() });
|
|
89
|
+
* }
|
|
90
|
+
*/
|
|
91
|
+
isLevelEnabled(level: LogLevel): boolean;
|
|
92
|
+
/** Run `fn` only if `level` would be emitted — the callback form of the above. */
|
|
93
|
+
ifLevelEnabled(level: LogLevel, fn: (log: Logger) => void): void;
|
|
27
94
|
private write;
|
|
95
|
+
trace(message: string, context?: Record<string, unknown>): void;
|
|
28
96
|
debug(message: string, context?: Record<string, unknown>): void;
|
|
29
97
|
info(message: string, context?: Record<string, unknown>): void;
|
|
30
98
|
warn(message: string, context?: Record<string, unknown>): void;
|
|
31
99
|
error(message: string, context?: Record<string, unknown>): void;
|
|
100
|
+
/** An unrecoverable failure — the loudest level. */
|
|
101
|
+
fatal(message: string, context?: Record<string, unknown>): void;
|
|
102
|
+
/** Log at a level chosen at runtime. */
|
|
103
|
+
log(level: LogLevel, message: string, context?: Record<string, unknown>): void;
|
|
32
104
|
/** A child logger with additional bound fields (e.g. a request id). */
|
|
33
105
|
child(bindings: Record<string, unknown>): Logger;
|
|
34
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Register a logger under a name, so a subsystem can have its own level or
|
|
109
|
+
* destination:
|
|
110
|
+
*
|
|
111
|
+
* setLogger(new Logger({ level: "trace", sink: auditSink }), "audit");
|
|
112
|
+
* namedLogger("audit").trace("permission granted", { userId });
|
|
113
|
+
*
|
|
114
|
+
* The default logger is the application's `Logger` singleton — resolve that with
|
|
115
|
+
* the global `logger()` helper, not this.
|
|
116
|
+
*/
|
|
117
|
+
export declare function setLogger(instance: Logger, name: string): Logger;
|
|
118
|
+
/** A logger registered with `setLogger`. Throws for an unknown name. */
|
|
119
|
+
export declare function namedLogger(name: string): Logger;
|