@fougere/calls 0.5.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fougere contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @fougere/calls
2
+ > The dev panel — a bounded log of what this process dispatched
3
+ An optional extension. It **watches** rather than participates: it subscribes to
4
+ `app.observe`, which is passive and swallows an observer's own failure, and the ring
5
+ holds no reference to a body.
6
+
7
+ ```ts
8
+ extensions: [calls({ max: 500, panel: true })],
9
+ ```
10
+
11
+ What it sees that a middleware cannot: a call refused **before** any handler — an unknown
12
+ route, an entity hosted elsewhere, a call arriving while the door drains — and the route
13
+ kind of every call, so a local execution and a hop to another process read the same way.
14
+ Beside the calls it keeps a bounded ring of log lines, of errors, and — when
15
+ `@fougere/adapter-sql` is installed — of the statements each call issued.
16
+
17
+ No port is opened unless `panel` says so. The ring is served as the `rpc.calls` operation,
18
+ and the reader is `fougere devtools` over `/_fougere/call`, like any other consumer. An app
19
+ that never installed this package answers
20
+ `Unknown rpc operation 'calls'. It serves discover.`
21
+
22
+ ## Installation
23
+ ```bash
24
+ pnpm add -D @fougere/calls
25
+ ```
26
+
27
+ ---
28
+
29
+ Part of [Fougere](https://github.com/chok/fougere) — one schema, a gradient from
30
+ monolith to distributed, the same user code.
31
+ Reference documentation: [the site](https://fougere.dev/) (en/fr).
@@ -0,0 +1,45 @@
1
+ import { type CallPage, type CallRecord, type DispatchEvent } from '@fougere/core';
2
+ /**
3
+ * A bounded log of what this process dispatched.
4
+ *
5
+ * The five transitions of one call are folded into one record: `received` opens it,
6
+ * `resolved` names the route, `completed`/`failed` settle the verdict, `settled` closes
7
+ * the duration. They are matched by the identity of the `Call`, which is the same frozen
8
+ * object across all five.
9
+ */
10
+ export declare class CallRing {
11
+ private readonly max;
12
+ private readonly frondOf;
13
+ private readonly held;
14
+ private readonly open;
15
+ private seq;
16
+ private lost;
17
+ private readonly watching;
18
+ constructor(max?: number, frondOf?: (entity: string) => string | undefined);
19
+ /**
20
+ * KNOWN LIMIT — a call this process ORIGINATES carries no trace here.
21
+ *
22
+ * `trace()` writes the traceparent inside the middleware chain, so inside
23
+ * `route.execute(call)`; the observer saw the `Call` at `received`, and that object is
24
+ * frozen. An ARRIVING call is unaffected: its traceparent is already on the invocation
25
+ * the transport handed over, which is why a hosted frond shows traces and its consumer
26
+ * does not.
27
+ *
28
+ * Closing it means `DispatchEvent` carrying the invocation as it ENDED rather than as it
29
+ * arrived — a core change, deliberately not smuggled in here. Until then the Traces view
30
+ * speaks for the side that receives.
31
+ */
32
+ record(event: DispatchEvent): void;
33
+ private opened;
34
+ /**
35
+ * Be told when a record settles, and get the unsubscription back.
36
+ *
37
+ * A page that polls is a page that is always a little wrong; this is what lets a door
38
+ * push instead. Told at `settled` and not at `received`, because a record is only
39
+ * complete then — a reader would otherwise redraw the same line four times.
40
+ */
41
+ watch(told: (record: CallRecord) => void): () => void;
42
+ /** Everything above `cursor`, and what was lost while the reader was away. */
43
+ since(cursor: number, inFlight?: number): CallPage;
44
+ }
45
+ //# sourceMappingURL=CallRing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallRing.d.ts","sourceRoot":"","sources":["../src/CallRing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,QAAQ,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAa/F;;;;;;;GAOG;AACH,qBAAa,QAAQ;IAQjB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoB;IACzC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqC;IAC1D,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2C;IAEpE,YACmB,GAAG,SAAM,EACT,OAAO,GAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAA2B,EAChF;IAEJ;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CA0BjC;IAED,OAAO,CAAC,MAAM;IAsBd;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI,CAIpD;IAED,8EAA8E;IAC9E,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,QAAQ,CAO5C;CACF"}
@@ -0,0 +1,119 @@
1
+ import { RPC_ENTITY } from '@fougere/core';
2
+ /** The message and the code of a refusal, without the stack that carries a body. */
3
+ function refusalOf(error) {
4
+ if (error === null || error === undefined)
5
+ return { message: 'unknown refusal' };
6
+ const held = error;
7
+ return {
8
+ ...(typeof held.code === 'string' ? { code: held.code } : {}),
9
+ message: typeof held.message === 'string' ? held.message : String(error),
10
+ };
11
+ }
12
+ /**
13
+ * A bounded log of what this process dispatched.
14
+ *
15
+ * The five transitions of one call are folded into one record: `received` opens it,
16
+ * `resolved` names the route, `completed`/`failed` settle the verdict, `settled` closes
17
+ * the duration. They are matched by the identity of the `Call`, which is the same frozen
18
+ * object across all five.
19
+ */
20
+ export class CallRing {
21
+ max;
22
+ frondOf;
23
+ held = [];
24
+ open = new WeakMap();
25
+ seq = 0;
26
+ lost = 0;
27
+ watching = new Set();
28
+ constructor(max = 500, frondOf = () => undefined) {
29
+ this.max = max;
30
+ this.frondOf = frondOf;
31
+ }
32
+ /**
33
+ * KNOWN LIMIT — a call this process ORIGINATES carries no trace here.
34
+ *
35
+ * `trace()` writes the traceparent inside the middleware chain, so inside
36
+ * `route.execute(call)`; the observer saw the `Call` at `received`, and that object is
37
+ * frozen. An ARRIVING call is unaffected: its traceparent is already on the invocation
38
+ * the transport handed over, which is why a hosted frond shows traces and its consumer
39
+ * does not.
40
+ *
41
+ * Closing it means `DispatchEvent` carrying the invocation as it ENDED rather than as it
42
+ * arrived — a core change, deliberately not smuggled in here. Until then the Traces view
43
+ * speaks for the side that receives.
44
+ */
45
+ record(event) {
46
+ // A reader reaches this ring through `rpc`, so recording that would make the panel
47
+ // watch itself — one reader, one line, forever.
48
+ if (event.call.address.entity === RPC_ENTITY)
49
+ return;
50
+ if (event.stage === 'received')
51
+ return this.opened(event);
52
+ const record = this.open.get(event.call);
53
+ if (!record)
54
+ return;
55
+ if (event.stage === 'resolved' && event.routeKind)
56
+ record.route = event.routeKind;
57
+ if (event.stage === 'completed')
58
+ record.verdict = 'ok';
59
+ if (event.stage === 'failed') {
60
+ record.verdict = 'failed';
61
+ record.refusal = refusalOf(event.error);
62
+ }
63
+ if (event.stage === 'settled') {
64
+ record.ms = Date.now() - record.startedAt;
65
+ if (event.routeKind)
66
+ record.route = event.routeKind;
67
+ this.open.delete(event.call);
68
+ for (const told of this.watching) {
69
+ // A watcher's own failure is not the dispatch's problem — the same rule
70
+ // `DispatchLifecycle` applies to an observer.
71
+ try {
72
+ told(record);
73
+ }
74
+ catch { /* observational */ }
75
+ }
76
+ }
77
+ }
78
+ opened(event) {
79
+ const { entity, operation, surface } = event.call.address;
80
+ const frond = this.frondOf(entity);
81
+ const { trace, caller } = event.call.invocation;
82
+ const record = {
83
+ seq: ++this.seq,
84
+ ...(frond ? { frond } : {}),
85
+ entity,
86
+ operation,
87
+ ...(surface !== undefined ? { surface } : {}),
88
+ ...(trace ? { trace } : {}),
89
+ ...(caller ? { caller } : {}),
90
+ startedAt: Date.now(),
91
+ verdict: 'running',
92
+ };
93
+ this.held.push(record);
94
+ this.open.set(event.call, record);
95
+ if (this.held.length > this.max)
96
+ this.lost += this.held.splice(0, this.held.length - this.max).length;
97
+ }
98
+ /**
99
+ * Be told when a record settles, and get the unsubscription back.
100
+ *
101
+ * A page that polls is a page that is always a little wrong; this is what lets a door
102
+ * push instead. Told at `settled` and not at `received`, because a record is only
103
+ * complete then — a reader would otherwise redraw the same line four times.
104
+ */
105
+ watch(told) {
106
+ this.watching.add(told);
107
+ return () => this.watching.delete(told);
108
+ }
109
+ /** Everything above `cursor`, and what was lost while the reader was away. */
110
+ since(cursor, inFlight = 0) {
111
+ return {
112
+ calls: this.held.filter((record) => record.seq > cursor),
113
+ cursor: this.seq,
114
+ inFlight,
115
+ dropped: this.lost,
116
+ };
117
+ }
118
+ }
119
+ //# sourceMappingURL=CallRing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CallRing.js","sourceRoot":"","sources":["../src/CallRing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import { type Extension } from '@fougere/core';
2
+ import { type PanelOptions } from './panel.js';
3
+ export type { CallPage, CallRecord } from '@fougere/core';
4
+ export { CallRing } from './CallRing.js';
5
+ export { LogRing, QueryRing, ErrorRing } from './rings.js';
6
+ export type { LogLine, QueryLine, ErrorGroup } from './rings.js';
7
+ export { servePanel, type PanelOptions } from './panel.js';
8
+ /** The rpc operation this extension answers under. */
9
+ export declare const CALLS_OP = "calls";
10
+ export interface CallsOptions {
11
+ /** How many calls the ring keeps. Beyond it, the oldest are dropped and counted. */
12
+ max?: number;
13
+ /**
14
+ * Serve the page too, on its own loopback port. `true` takes a free one.
15
+ *
16
+ * Node only — it opens an `http` server, which workerd has not. Without it the extension
17
+ * stays universal and `fougere devtools` is the reader.
18
+ */
19
+ panel?: boolean | number | PanelOptions;
20
+ }
21
+ /**
22
+ * What this process dispatched, kept in a bounded ring and served as an rpc operation.
23
+ *
24
+ * It watches rather than participates: `app.observe` is passive, `DispatchLifecycle`
25
+ * swallows an observer's own failure, and the ring holds no reference to a body. What it
26
+ * sees that a middleware cannot: a call refused BEFORE any handler — an unknown route, an
27
+ * entity hosted elsewhere, a call arriving while the door drains — and the route kind of
28
+ * every call, so a local execution and a hop to another process read the same way.
29
+ *
30
+ * The reader is `fougere devtools`, over `/_fougere/call` like any other consumer. No port
31
+ * is opened here, and an app that never installed this package answers
32
+ * `Unknown rpc operation 'calls'. It serves discover.`
33
+ */
34
+ export declare function calls(options?: CallsOptions): Extension;
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,SAAS,EAA0B,MAAM,eAAe,CAAC;AAGxF,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC3D,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D,sDAAsD;AACtD,eAAO,MAAM,QAAQ,UAAU,CAAC;AAEhC,MAAM,WAAW,YAAY;IAC3B,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,YAAY,CAAC;CACzC;AAwGD;;;;;;;;;;;;GAYG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,SAAS,CA+D3D"}
package/dist/index.js ADDED
@@ -0,0 +1,179 @@
1
+ import { onLog } from '@fougere/core';
2
+ import { CallRing } from './CallRing.js';
3
+ import { ErrorRing, LogRing, QueryRing } from './rings.js';
4
+ import { servePanel } from './panel.js';
5
+ export { CallRing } from './CallRing.js';
6
+ export { LogRing, QueryRing, ErrorRing } from './rings.js';
7
+ export { servePanel } from './panel.js';
8
+ /** The rpc operation this extension answers under. */
9
+ export const CALLS_OP = 'calls';
10
+ /**
11
+ * What this process serves, read from the app itself.
12
+ *
13
+ * Not a second source: `operationsFor` is the same effective model the façade consumes, so
14
+ * the page shows what will actually answer — including the ops nobody has called yet, which
15
+ * is what makes an empty panel useful instead of blank.
16
+ */
17
+ function servedModel(app) {
18
+ return {
19
+ fronds: app.fronds.map((frond) => ({
20
+ name: frond.name,
21
+ // What the config SAYS. What the runtime saw is in the ring, under `route` — and the
22
+ // two disagree exactly when something is misconfigured, which is the whole point of
23
+ // showing them side by side. `rpc.topology` calls a frond remote because it ANSWERED.
24
+ declared: app.remotes[frond.name] ? 'remote' : 'local',
25
+ at: app.remotes[frond.name] ? hostOf(app.remotes[frond.name]) : null,
26
+ entities: frond.entities.map((entity) => entity.name),
27
+ operations: frond.handlers.flatMap((handler) => {
28
+ const ops = app.operationsFor(handler.address);
29
+ return [...(ops?.values() ?? [])].map((op) => ({
30
+ id: op.id,
31
+ operation: op.operation,
32
+ kind: op.kind,
33
+ address: `${op.handler.address}.${op.implementation.method}`,
34
+ handler: op.handler.className,
35
+ description: op.description ?? null,
36
+ input: nameOf(op.input),
37
+ output: nameOf(op.output),
38
+ cardinality: op.cardinality ?? null,
39
+ parameters: op.parameters.map((one) => ({
40
+ name: one.name,
41
+ type: one.type,
42
+ optional: one.optional,
43
+ binding: one.binding.source.kind,
44
+ })),
45
+ surfaces: op.exposure.surfaces,
46
+ adapters: op.exposure.adapters,
47
+ placement: op.placement.runtime,
48
+ file: op.implementation.filePath,
49
+ }));
50
+ }),
51
+ })),
52
+ };
53
+ }
54
+ /** Host and port only — a declared address may carry credentials, and this answer leaves. */
55
+ function hostOf(address) {
56
+ try {
57
+ const url = new URL(address);
58
+ return `${url.protocol}//${url.host}`;
59
+ }
60
+ catch {
61
+ return address;
62
+ }
63
+ }
64
+ function nameOf(schema) {
65
+ const held = schema;
66
+ if (!held)
67
+ return null;
68
+ return typeof held.getName === 'function' ? held.getName() : held.name ?? null;
69
+ }
70
+ /**
71
+ * `address -> frond`, resolved once at boot: a call's address does not carry its frond.
72
+ *
73
+ * Indexed on HANDLERS and not on entities, because a frond may hold no entity at all —
74
+ * `demos/observability` has two — and then every one of its calls came back unnamed. The
75
+ * entities are indexed after, and only where a handler left the address free: an entity
76
+ * with no handler is served by `Crud`, which answers under the same address.
77
+ */
78
+ function frondIndex(app) {
79
+ const byAddress = new Map();
80
+ for (const frond of app.fronds) {
81
+ for (const entity of frond.entities)
82
+ byAddress.set(entity.name, frond.name);
83
+ for (const handler of frond.handlers)
84
+ byAddress.set(handler.address, frond.name);
85
+ }
86
+ return (address) => byAddress.get(address);
87
+ }
88
+ /**
89
+ * Subscribe to SQL when there is SQL. Absence is an answer, not a blank tab.
90
+ *
91
+ * `logQueries` is installed in every Kysely this process builds, so the subscription is all
92
+ * that is missing — and it is taken here, in `up(app)`, like the other two.
93
+ */
94
+ async function queriesFrom(queries) {
95
+ try {
96
+ const { onQuery } = await import('@fougere/adapter-sql');
97
+ return onQuery((event) => queries.record(event));
98
+ }
99
+ catch {
100
+ return () => { };
101
+ }
102
+ }
103
+ function cursorOf(invocation) {
104
+ const body = invocation.body;
105
+ const since = Number(body?.since ?? 0);
106
+ return Number.isFinite(since) && since > 0 ? since : 0;
107
+ }
108
+ /**
109
+ * What this process dispatched, kept in a bounded ring and served as an rpc operation.
110
+ *
111
+ * It watches rather than participates: `app.observe` is passive, `DispatchLifecycle`
112
+ * swallows an observer's own failure, and the ring holds no reference to a body. What it
113
+ * sees that a middleware cannot: a call refused BEFORE any handler — an unknown route, an
114
+ * entity hosted elsewhere, a call arriving while the door drains — and the route kind of
115
+ * every call, so a local execution and a hop to another process read the same way.
116
+ *
117
+ * The reader is `fougere devtools`, over `/_fougere/call` like any other consumer. No port
118
+ * is opened here, and an app that never installed this package answers
119
+ * `Unknown rpc operation 'calls'. It serves discover.`
120
+ */
121
+ export function calls(options = {}) {
122
+ /**
123
+ * Per APP, not per extension. A host declares its extensions once, so the same instance
124
+ * goes up on the new app before the old one is released — one shared ring would mix two
125
+ * processes' worth of calls, and the older `down` would erase the newer subscription.
126
+ */
127
+ const stopping = new WeakMap();
128
+ return {
129
+ name: 'calls',
130
+ async up(app) {
131
+ const ring = new CallRing(options.max ?? 500, frondIndex(app));
132
+ const logs = new LogRing();
133
+ const errors = new ErrorRing();
134
+ const queries = new QueryRing();
135
+ const undo = [
136
+ app.observe((event) => {
137
+ ring.record(event);
138
+ if (event.stage === 'failed')
139
+ errors.fromDispatch(event);
140
+ }),
141
+ onLog((line) => {
142
+ logs.record(line);
143
+ errors.fromLog(line);
144
+ }),
145
+ // `@fougere/adapter-sql` is optional — an app on another storage, or none, simply
146
+ // never resolves it, and the Queries tab says there is none rather than staying
147
+ // blank. Dynamic on purpose: this package declares no dependency on it.
148
+ await queriesFrom(queries),
149
+ ];
150
+ const stop = () => { for (const one of undo)
151
+ one(); };
152
+ app.serveRpc(CALLS_OP, (invocation) => ring.since(cursorOf(invocation), app.inFlight()));
153
+ if (!options.panel) {
154
+ stopping.set(app, stop);
155
+ return;
156
+ }
157
+ const asked = typeof options.panel === 'object' ? options.panel : {};
158
+ const port = typeof options.panel === 'number' ? options.panel : asked.port;
159
+ const close = await servePanel(ring, {
160
+ ...asked,
161
+ ...(port !== undefined ? { port } : {}),
162
+ title: asked.title ?? app.fronds[0]?.name ?? 'fougere',
163
+ fronds: app.fronds.map((frond) => frond.name),
164
+ model: servedModel(app),
165
+ logs: (cursor) => logs.since(cursor),
166
+ errors: (cursor) => errors.since(cursor),
167
+ queries: (cursor) => queries.since(cursor),
168
+ inFlight: () => app.inFlight(),
169
+ announce: asked.announce ?? ((url) => console.log(` calls panel ${url}`)),
170
+ });
171
+ stopping.set(app, async () => { stop(); await close(); });
172
+ },
173
+ async down(app) {
174
+ await stopping.get(app)?.();
175
+ stopping.delete(app);
176
+ },
177
+ };
178
+ }
179
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/page.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The panel, as one file with no dependency.
3
+ *
4
+ * Inline on purpose: this package ships no framework and no build step, and a dev tool that
5
+ * needs `npm install` before it shows anything is a dev tool nobody opens. The page reads
6
+ * its own origin — the door that served it also serves its events — so there is no address
7
+ * to configure and no CORS to arrange.
8
+ *
9
+ * FOUR views, and the number is a decision. Six grew one tab at a time, each with its own
10
+ * empty state and its own caveat, and the result read as noise rather than as an answer.
11
+ * They fold along what a reader actually asks:
12
+ *
13
+ * Calls what happened, and did it hold — the screen that IS the argument
14
+ * Refused what did not, from two sources, since one of them is never a call
15
+ * Activity what the process did along the way — logs and statements are one stream,
16
+ * both chronological, both uncorrelated to a call; two tabs said that twice
17
+ * Served what it would answer if asked, filled before anything is called
18
+ *
19
+ * Traces lost its tab: it spoke for the receiving side only, and a trace already shows in a
20
+ * call's detail. A tab that half-delivers is worse than a field that says what it knows.
21
+ */
22
+ export declare function page(title: string): string;
23
+ //# sourceMappingURL=page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../src/page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAigB1C"}