@interactive-inc/claude-funnel 0.58.0 → 0.59.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/dist/bin.js +354 -293
- package/dist/claude.d.ts +5 -5
- package/dist/claude.js +1 -1
- package/dist/connectors/discord.d.ts +3 -3
- package/dist/connectors/gh.d.ts +4 -4
- package/dist/connectors/schedule.d.ts +1 -1
- package/dist/connectors/slack.d.ts +2 -2
- package/dist/diagnostics.d.ts +1 -1
- package/dist/docs.d.ts +1 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/{file-process-guard-DI1742H5.d.ts → file-process-guard-B3IFCj_G.d.ts} +5 -5
- package/dist/{funnel-diagnostics-qWy5tPSq.d.ts → funnel-diagnostics-K-wON25Y.d.ts} +1 -1
- package/dist/{funnel-doctor-BF3Rdgk0.d.ts → funnel-doctor-vxO96TCA.d.ts} +2 -2
- package/dist/funnel-log-sqlite-sink-B_5_4ybn.js +301 -0
- package/dist/{funnel-recovery-BUBsu7WX.d.ts → funnel-recovery-COExL9MD.d.ts} +1 -1
- package/dist/gateway.d.ts +2 -2
- package/dist/gateway.js +1 -1
- package/dist/{index-tP67P1Sy.d.ts → index-Conbxl5O.d.ts} +748 -166
- package/dist/index.d.ts +16 -16
- package/dist/index.js +142 -83
- package/dist/{local-config-sync-BY20ixEV.d.ts → local-config-sync--f739oCJ.d.ts} +8 -8
- package/dist/local-config.d.ts +2 -2
- package/dist/local-config.js +1 -1
- package/dist/logger.d.ts +384 -0
- package/dist/logger.js +281 -0
- package/dist/{memory-diagnostic-log-CvqobDDs.js → memory-diagnostic-log-5LzwJ_F7.js} +1 -298
- package/dist/{memory-token-prompter-DOgptiIb.d.ts → memory-token-prompter-BlFwK9k7.d.ts} +2 -2
- package/dist/{profiles-EHTeCOqB.d.ts → profiles-g2qGVOWv.d.ts} +3 -3
- package/dist/profiles.d.ts +1 -1
- package/dist/recovery.d.ts +1 -1
- package/dist/{schedule-listener-DKh0hnkK.d.ts → schedule-listener-DoMPjHZj.d.ts} +2 -2
- package/dist/{settings-reader-CBrgz01o.d.ts → settings-reader-DPwqOVUm.d.ts} +1 -1
- package/dist/{slack-listener-DFW9vck4.d.ts → slack-listener-Dj9NFbAJ.d.ts} +1 -1
- package/package.json +2 -2
- /package/dist/{connector-adapter-BkYC6qiK.d.ts → connector-adapter-DGacCppE.d.ts} +0 -0
- /package/dist/{diagnostic-log-Bxe7Bbvw.d.ts → diagnostic-log-Cb3v8P7p.d.ts} +0 -0
- /package/dist/{discord-connector-schema-CWHVNIcB.d.ts → discord-connector-schema-CQyfDkLD.d.ts} +0 -0
- /package/dist/{file-system-Wub9Nto4.d.ts → file-system-DxpnnUVb.d.ts} +0 -0
- /package/dist/{funnel-docs-dXPokzr5.d.ts → funnel-docs-DYBs1-H_.d.ts} +0 -0
- /package/dist/{gh-connector-schema-CU1ojfIF.d.ts → gh-connector-schema-CZzwzvqY.d.ts} +0 -0
- /package/dist/{memory-token-prompter-vBXxY20-.js → memory-token-prompter-C7vREzCL.js} +0 -0
- /package/dist/{process-runner-D5I_jhYQ.d.ts → process-runner-Cx5O_fTf.d.ts} +0 -0
- /package/dist/{settings-schema-zhnMIa8I.d.ts → settings-schema-1hh11jnN.d.ts} +0 -0
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
//#region lib/logger/funnel-log-entry.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Wrapper that `FunnelLog.emit` puts around every event before handing it
|
|
4
|
+
* to a sink. `seq` is monotonic across the lifetime of the underlying store —
|
|
5
|
+
* sinks persist it as the primary key so replay (and broadcaster seeding
|
|
6
|
+
* after restart) is an indexed range scan, not a full table walk. `ts` is
|
|
7
|
+
* epoch milliseconds. `event` is the caller-defined payload validated by the
|
|
8
|
+
* Zod schema passed to the bus.
|
|
9
|
+
*/
|
|
10
|
+
type FunnelLogEntry<E> = {
|
|
11
|
+
seq: number;
|
|
12
|
+
ts: number;
|
|
13
|
+
event: E;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region lib/logger/funnel-log-sink.d.ts
|
|
17
|
+
/**
|
|
18
|
+
* Relay sink. Receives records that already have a `seq` assigned by the
|
|
19
|
+
* primary and stores or forwards them — memory ring, stdout, network push,
|
|
20
|
+
* a second SQLite mirror, etc. Does not generate seq itself, so any number
|
|
21
|
+
* can be attached and they all observe the same monotonic stream.
|
|
22
|
+
*
|
|
23
|
+
* `write` returns `void` on success or an `Error` the bus surfaces via
|
|
24
|
+
* `onSinkError`. Throwing is also tolerated (the bus catches), but
|
|
25
|
+
* returning is preferred so the failure path is part of the type.
|
|
26
|
+
*/
|
|
27
|
+
type FunnelLogSink<E> = {
|
|
28
|
+
write(record: FunnelLogEntry<E>): void | Error;
|
|
29
|
+
close?(): void;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Primary sink. Owns the canonical seq sequence for the bus. `insert` is
|
|
33
|
+
* the atomic boundary — it assigns a seq strictly greater than every
|
|
34
|
+
* previously assigned one, persists the record, and returns it. SQLite
|
|
35
|
+
* implementations get atomicity for free by delegating to `INTEGER PRIMARY
|
|
36
|
+
* KEY` so two processes sharing one database file see one monotonic
|
|
37
|
+
* stream without bus-level coordination.
|
|
38
|
+
*
|
|
39
|
+
* `getMaxSeq` is the highest seq currently in the sink — used for
|
|
40
|
+
* observability and for replay seeding by clients reading the store.
|
|
41
|
+
*/
|
|
42
|
+
type FunnelLogPrimarySink<E> = {
|
|
43
|
+
insert(input: {
|
|
44
|
+
ts: number;
|
|
45
|
+
event: E;
|
|
46
|
+
}): FunnelLogEntry<E> | Error;
|
|
47
|
+
getMaxSeq(): number;
|
|
48
|
+
close?(): void;
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region lib/logger/funnel-log.d.ts
|
|
52
|
+
type Listener<E> = (record: FunnelLogEntry<E>) => void;
|
|
53
|
+
type SinkErrorHandler<E> = (error: Error, record: FunnelLogEntry<E>, sink: FunnelLogSink<E>) => void;
|
|
54
|
+
type FunnelLogValidator<E> = (event: unknown) => {
|
|
55
|
+
success: true;
|
|
56
|
+
data: E;
|
|
57
|
+
} | {
|
|
58
|
+
success: false;
|
|
59
|
+
error: Error;
|
|
60
|
+
};
|
|
61
|
+
type Props$5<E> = {
|
|
62
|
+
/** Validates each event before emission. Use `schema.safeParse` from any validation library, or a plain function. */validate: FunnelLogValidator<E>; /** Owns seq assignment + durability. Use `FunnelLogSqliteSink` for multi-process safety. */
|
|
63
|
+
primary: FunnelLogPrimarySink<E>; /** Optional fanout for already-sequenced records (memory ring, stdout, network mirror). */
|
|
64
|
+
relays?: ReadonlyArray<FunnelLogSink<E>>; /** Override for tests. Defaults to `Date.now`. */
|
|
65
|
+
now?: () => number; /** Observer for relay failures. Default: silently swallow. */
|
|
66
|
+
onSinkError?: SinkErrorHandler<E>;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Validated event log bus. Three responsibilities and nothing else:
|
|
70
|
+
* validate the event, delegate seq + persistence to the primary sink, and
|
|
71
|
+
* fan the resulting record out to relays and live subscribers.
|
|
72
|
+
*
|
|
73
|
+
* Splitting "primary" from "relays" makes the seq invariant honest: there
|
|
74
|
+
* is exactly one source of truth (the primary's atomic insert). Two
|
|
75
|
+
* `FunnelLog` instances pointed at the same SQLite file therefore see
|
|
76
|
+
* one monotonic stream without bus-level coordination. Relays mirror
|
|
77
|
+
* already-sequenced records, so they can be added or removed without
|
|
78
|
+
* affecting correctness.
|
|
79
|
+
*
|
|
80
|
+
* Failure isolation:
|
|
81
|
+
* - Primary failure short-circuits emit and is returned to the caller.
|
|
82
|
+
* - Relay failures never block the primary path — they surface via the
|
|
83
|
+
* optional `onSinkError` callback so the caller can observe without
|
|
84
|
+
* being interrupted.
|
|
85
|
+
* - A subscriber that throws is contained; the rest of the fanout
|
|
86
|
+
* completes normally.
|
|
87
|
+
*/
|
|
88
|
+
declare class FunnelLog<E> {
|
|
89
|
+
private readonly validate;
|
|
90
|
+
private readonly primary;
|
|
91
|
+
private readonly relays;
|
|
92
|
+
private readonly now;
|
|
93
|
+
private readonly onSinkError;
|
|
94
|
+
private readonly listeners;
|
|
95
|
+
constructor(props: Props$5<E>);
|
|
96
|
+
emit(event: E): FunnelLogEntry<E> | Error;
|
|
97
|
+
subscribe(listener: Listener<E>): () => void;
|
|
98
|
+
getMaxSeq(): number;
|
|
99
|
+
close(): void;
|
|
100
|
+
private callPrimary;
|
|
101
|
+
private fanOutToRelays;
|
|
102
|
+
private callRelay;
|
|
103
|
+
private fanOutToListeners;
|
|
104
|
+
private callClose;
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region lib/logger/funnel-log-sqlite-sink.d.ts
|
|
108
|
+
type IndexValues<I extends ReadonlyArray<string>> = Record<I[number], string | null>;
|
|
109
|
+
/**
|
|
110
|
+
* Constructor props. The shape narrows on `I`: when no indexes are
|
|
111
|
+
* declared (the default), `extractIndexes` is forbidden; when indexes
|
|
112
|
+
* are declared, both `indexes` and `extractIndexes` are required and
|
|
113
|
+
* `extractIndexes` is type-checked against the index keys.
|
|
114
|
+
*/
|
|
115
|
+
type Props$4<E, I extends ReadonlyArray<string>> = I extends readonly [] ? {
|
|
116
|
+
path: string;
|
|
117
|
+
maxRows?: number;
|
|
118
|
+
maxAgeMs?: number;
|
|
119
|
+
maxBytes?: number;
|
|
120
|
+
targetBytes?: number;
|
|
121
|
+
now?: () => number;
|
|
122
|
+
indexes?: I;
|
|
123
|
+
extractIndexes?: never;
|
|
124
|
+
} : {
|
|
125
|
+
path: string;
|
|
126
|
+
maxRows?: number;
|
|
127
|
+
maxAgeMs?: number;
|
|
128
|
+
maxBytes?: number;
|
|
129
|
+
targetBytes?: number;
|
|
130
|
+
now?: () => number;
|
|
131
|
+
indexes: I;
|
|
132
|
+
extractIndexes: (event: E) => IndexValues<I>;
|
|
133
|
+
};
|
|
134
|
+
type QueryFilter<I extends ReadonlyArray<string>> = {
|
|
135
|
+
/** Return only records with seq strictly greater than this. */sinceSeq?: number; /** Filter by the top-level `event.type` discriminator. */
|
|
136
|
+
type?: string; /** Filter by indexed columns. Keys are constrained to the declared `indexes`. */
|
|
137
|
+
where?: Partial<IndexValues<I>>; /** Maximum rows returned. Default 1000. */
|
|
138
|
+
limit?: number;
|
|
139
|
+
/**
|
|
140
|
+
* Which end of the seq range to take when `limit` clips the result.
|
|
141
|
+
* "asc" (default) returns the oldest matching rows; "desc" returns the
|
|
142
|
+
* newest. Rows are always sorted ascending by seq before returning, so the
|
|
143
|
+
* caller sees a chronological slice either way — "desc" just picks the tail.
|
|
144
|
+
*/
|
|
145
|
+
order?: "asc" | "desc";
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* SQLite-backed sink built on `bun:sqlite`. Implements both primary and
|
|
149
|
+
* relay roles so the same instance can own seq generation for one bus and
|
|
150
|
+
* mirror records from another (e.g. cross-process replication, restore
|
|
151
|
+
* from a backup stream).
|
|
152
|
+
*
|
|
153
|
+
* Concurrency model: seq is `INTEGER PRIMARY KEY`, so SQLite assigns it
|
|
154
|
+
* atomically via `lastInsertRowid`. Two `FunnelLog` instances pointed
|
|
155
|
+
* at the same database file therefore see one monotonically increasing
|
|
156
|
+
* seq stream without any bus-level coordination — the database itself is
|
|
157
|
+
* the synchronization point.
|
|
158
|
+
*
|
|
159
|
+
* Schema is version-managed via `PRAGMA user_version`. Migrations are
|
|
160
|
+
* append-only and run in a transaction on every construct so a partial
|
|
161
|
+
* upgrade rolls back cleanly. Caller-defined `indexes` are layered on top
|
|
162
|
+
* via `ALTER TABLE ADD COLUMN` + `CREATE INDEX IF NOT EXISTS`, so adding
|
|
163
|
+
* a new index to an existing database is a no-downtime operation.
|
|
164
|
+
*
|
|
165
|
+
* Type safety: the second generic parameter `I` is the literal tuple of
|
|
166
|
+
* index column names. `extractIndexes` and `query({ where })` are
|
|
167
|
+
* both type-checked against this tuple, so a typo at the call site is a
|
|
168
|
+
* compile-time error rather than a silent miss at runtime.
|
|
169
|
+
*
|
|
170
|
+
* Retention is bounded by `maxRows` and/or `maxAgeMs`. Both run on every
|
|
171
|
+
* insert as a single indexed DELETE that no-ops below the cap.
|
|
172
|
+
*
|
|
173
|
+
* Bulk inserts use `insertMany`, which wraps the batch in one transaction
|
|
174
|
+
* for ~10–100x throughput at the cost of one fsync per batch instead of
|
|
175
|
+
* one per row.
|
|
176
|
+
*/
|
|
177
|
+
declare class FunnelLogSqliteSink<E, const I extends ReadonlyArray<string> = readonly []> implements FunnelLogPrimarySink<E>, FunnelLogSink<E> {
|
|
178
|
+
private readonly db;
|
|
179
|
+
private readonly maxRows;
|
|
180
|
+
private readonly maxAgeMs;
|
|
181
|
+
private readonly maxBytes;
|
|
182
|
+
private readonly targetBytes;
|
|
183
|
+
private readonly now;
|
|
184
|
+
private readonly indexes;
|
|
185
|
+
private readonly extractIndexes;
|
|
186
|
+
private readonly insertStmt;
|
|
187
|
+
private readonly insertWithSeqStmt;
|
|
188
|
+
private readonly maxSeqStmt;
|
|
189
|
+
private readonly countStmt;
|
|
190
|
+
private readonly trimRowsStmt;
|
|
191
|
+
private readonly trimAgeStmt;
|
|
192
|
+
private readonly trimOldestStmt;
|
|
193
|
+
private insertsSinceByteCheck;
|
|
194
|
+
constructor(props: Props$4<E, I>);
|
|
195
|
+
insert(input: {
|
|
196
|
+
ts: number;
|
|
197
|
+
event: E;
|
|
198
|
+
}): FunnelLogEntry<E> | Error;
|
|
199
|
+
insertMany(inputs: ReadonlyArray<{
|
|
200
|
+
ts: number;
|
|
201
|
+
event: E;
|
|
202
|
+
}>): FunnelLogEntry<E>[] | Error;
|
|
203
|
+
write(record: FunnelLogEntry<E>): void | Error;
|
|
204
|
+
getMaxSeq(): number;
|
|
205
|
+
query(props?: QueryFilter<I>): FunnelLogEntry<E>[];
|
|
206
|
+
/**
|
|
207
|
+
* Current schema version. Useful for diagnostics and for tests that want
|
|
208
|
+
* to verify migrations ran. Reads `PRAGMA user_version` once per call.
|
|
209
|
+
*/
|
|
210
|
+
getSchemaVersion(): number;
|
|
211
|
+
close(): void;
|
|
212
|
+
private buildInsertParams;
|
|
213
|
+
private appendWhereConditions;
|
|
214
|
+
private trim;
|
|
215
|
+
/**
|
|
216
|
+
* Throttled byte-size enforcement. Only every BYTE_CHECK_INTERVAL inserts do
|
|
217
|
+
* we measure the file; on overflow we estimate how many of the oldest rows to
|
|
218
|
+
* drop to land near targetBytes (by the byte/row ratio), delete them in one
|
|
219
|
+
* statement, then VACUUM once to return the freed pages to the filesystem (a
|
|
220
|
+
* plain DELETE only frees pages inside the file). One DELETE + one VACUUM per
|
|
221
|
+
* overflow keeps the expensive rewrite rare — the file must refill the whole
|
|
222
|
+
* maxBytes→targetBytes delta before the next overflow can trigger.
|
|
223
|
+
*/
|
|
224
|
+
private maybeTrimBytes;
|
|
225
|
+
private byteSize;
|
|
226
|
+
/** Drop every row and reclaim the file space. Used by `<log>.clear()`. */
|
|
227
|
+
clear(): void;
|
|
228
|
+
private syncIndexColumns;
|
|
229
|
+
private migrate;
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region lib/logger/funnel-log-memory-sink.d.ts
|
|
233
|
+
type Props$3 = {
|
|
234
|
+
/** Hard cap on retained records. The oldest is evicted on overflow. 0 disables retention. */capacity?: number;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* In-memory ring buffer that doubles as primary or relay. As primary it
|
|
238
|
+
* owns its own seq counter (single-process only — for multi-process
|
|
239
|
+
* safety, use `FunnelLogSqliteSink` as primary and place this as a
|
|
240
|
+
* relay). As relay it accepts whatever seq the primary assigned and
|
|
241
|
+
* advances its own counter to match, so `getMaxSeq` stays meaningful.
|
|
242
|
+
*
|
|
243
|
+
* Useful as a test double, as a short-window replay buffer paired with a
|
|
244
|
+
* persistent primary (covering reconnects without round-tripping disk),
|
|
245
|
+
* or as a backing store for live subscribers.
|
|
246
|
+
*/
|
|
247
|
+
declare class FunnelLogMemorySink<E> implements FunnelLogPrimarySink<E>, FunnelLogSink<E> {
|
|
248
|
+
private readonly capacity;
|
|
249
|
+
private readonly buffer;
|
|
250
|
+
private seq;
|
|
251
|
+
constructor(props?: Props$3);
|
|
252
|
+
insert(input: {
|
|
253
|
+
ts: number;
|
|
254
|
+
event: E;
|
|
255
|
+
}): FunnelLogEntry<E>;
|
|
256
|
+
write(record: FunnelLogEntry<E>): void;
|
|
257
|
+
getMaxSeq(): number;
|
|
258
|
+
query(): ReadonlyArray<FunnelLogEntry<E>>;
|
|
259
|
+
clear(): void;
|
|
260
|
+
private append;
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region lib/logger/funnel-text-entry.d.ts
|
|
264
|
+
type FunnelTextLevel = "info" | "warn" | "error";
|
|
265
|
+
/**
|
|
266
|
+
* One human-facing diagnostic log entry. Distinct from `FunnelLogEntry`
|
|
267
|
+
* (which wraps a schema-validated domain event) — this is the free-form,
|
|
268
|
+
* for-humans-tailing-a-log shape: a level, a message, and optional meta.
|
|
269
|
+
*
|
|
270
|
+
* `meta` is `null` rather than `undefined` when absent so writers can
|
|
271
|
+
* persist a uniform shape (no missing-key ambiguity in JSON Lines).
|
|
272
|
+
*/
|
|
273
|
+
type FunnelTextEntry = {
|
|
274
|
+
ts: number;
|
|
275
|
+
level: FunnelTextLevel;
|
|
276
|
+
message: string;
|
|
277
|
+
meta: Record<string, unknown> | null;
|
|
278
|
+
};
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region lib/logger/funnel-text-writer.d.ts
|
|
281
|
+
/**
|
|
282
|
+
* Plugin port for `FunnelTextLog`. Writers decide where diagnostic
|
|
283
|
+
* records land — stdout, JSONL file, syslog, network, etc. — without the
|
|
284
|
+
* logger having to know about persistence shape.
|
|
285
|
+
*
|
|
286
|
+
* `write` returns `void` on success or an `Error` the logger surfaces via
|
|
287
|
+
* `onWriteError`. Throwing is also tolerated; the logger catches.
|
|
288
|
+
*/
|
|
289
|
+
type FunnelTextWriter = {
|
|
290
|
+
write(record: FunnelTextEntry): void | Error;
|
|
291
|
+
close?(): void;
|
|
292
|
+
};
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region lib/logger/funnel-text-log.d.ts
|
|
295
|
+
type WriteErrorHandler = (error: Error, record: FunnelTextEntry) => void;
|
|
296
|
+
type Props$2 = {
|
|
297
|
+
/** Where records go. Use `FunnelTextStdoutWriter`, `FunnelTextFileWriter`, or your own. */writer: FunnelTextWriter; /** Minimum level to emit. Lower-rank records are dropped. Default: "info". */
|
|
298
|
+
level?: FunnelTextLevel; /** Override for tests. Defaults to `Date.now`. */
|
|
299
|
+
now?: () => number; /** Observer for writer failures. Default: silently swallow. */
|
|
300
|
+
onWriteError?: WriteErrorHandler;
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Human-facing diagnostic logger. The companion to `FunnelLog`: where
|
|
304
|
+
* `FunnelLog` is for schema-validated, replayable domain events,
|
|
305
|
+
* `FunnelTextLog` is for free-form info/warn/error messages destined
|
|
306
|
+
* for a human tailing a log or skimming during incident response.
|
|
307
|
+
*
|
|
308
|
+
* Keeping the two separate matters operationally:
|
|
309
|
+
* - Diagnostics typically out-volume domain events 10–1000x; mixing
|
|
310
|
+
* them in the same store would push events out under retention.
|
|
311
|
+
* - Diagnostics are unstructured by design; mixing them in would defeat
|
|
312
|
+
* the schema-first guarantee that makes domain events replayable.
|
|
313
|
+
* - Different audiences and queries (humans grep `tail -f` vs. tools
|
|
314
|
+
* query `WHERE seq > ?`).
|
|
315
|
+
*
|
|
316
|
+
* The writer is a port. Level gating happens here so writers receive only
|
|
317
|
+
* what is worth persisting. Failure isolation matches `FunnelLog`: a
|
|
318
|
+
* writer that throws or returns Error is contained, surfaced via
|
|
319
|
+
* `onWriteError`, and never blocks the caller.
|
|
320
|
+
*/
|
|
321
|
+
declare class FunnelTextLog {
|
|
322
|
+
private readonly writer;
|
|
323
|
+
private readonly minRank;
|
|
324
|
+
private readonly now;
|
|
325
|
+
private readonly onWriteError;
|
|
326
|
+
constructor(props: Props$2);
|
|
327
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
328
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
329
|
+
error(message: string, meta?: Record<string, unknown>): void;
|
|
330
|
+
close(): void;
|
|
331
|
+
private emit;
|
|
332
|
+
private callWriter;
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region lib/logger/funnel-text-file-writer.d.ts
|
|
336
|
+
type Props$1 = {
|
|
337
|
+
/** Filesystem path. Parent directory is created on construct. */path: string;
|
|
338
|
+
/**
|
|
339
|
+
* Optional size cap in bytes. When the next write would push the file
|
|
340
|
+
* over the cap, the existing file becomes `<path>.1` (replacing any
|
|
341
|
+
* prior `.1`) and a fresh file takes its place. Single-keep rotation —
|
|
342
|
+
* a second cycle drops the previous `.1`.
|
|
343
|
+
*/
|
|
344
|
+
maxBytes?: number;
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* Appends one JSON line per record to a file. Optional one-keep size
|
|
348
|
+
* rotation. Designed for diagnostic logs a human tails (`tail -f file |
|
|
349
|
+
* jq`); not for replay or queries — use `FunnelLogSqliteSink` if you
|
|
350
|
+
* need indexed lookups.
|
|
351
|
+
*
|
|
352
|
+
* Writes are synchronous (`appendFileSync`), so each line is durable
|
|
353
|
+
* before `write` returns. Throughput matches the OS file cache; for
|
|
354
|
+
* high-volume logging consider buffering at the call site or using a
|
|
355
|
+
* different writer.
|
|
356
|
+
*/
|
|
357
|
+
declare class FunnelTextFileWriter implements FunnelTextWriter {
|
|
358
|
+
private readonly path;
|
|
359
|
+
private readonly maxBytes;
|
|
360
|
+
constructor(props: Props$1);
|
|
361
|
+
write(record: FunnelTextEntry): void | Error;
|
|
362
|
+
private ensureDir;
|
|
363
|
+
private rotateIfNeeded;
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region lib/logger/funnel-text-stdout-writer.d.ts
|
|
367
|
+
type Stream = {
|
|
368
|
+
write(s: string): void;
|
|
369
|
+
};
|
|
370
|
+
type Props = {
|
|
371
|
+
/** Override for tests. Defaults to `process.stdout`. */out?: Stream;
|
|
372
|
+
};
|
|
373
|
+
/**
|
|
374
|
+
* Writes one JSON line per record to stdout. Useful as the default writer
|
|
375
|
+
* for foreground daemons, dev runs, and short-lived processes where a
|
|
376
|
+
* file-backed log would be overkill.
|
|
377
|
+
*/
|
|
378
|
+
declare class FunnelTextStdoutWriter implements FunnelTextWriter {
|
|
379
|
+
private readonly out;
|
|
380
|
+
constructor(props?: Props);
|
|
381
|
+
write(record: FunnelTextEntry): void;
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
384
|
+
export { FunnelLog, FunnelLogEntry, FunnelLogMemorySink, FunnelLogPrimarySink, FunnelLogSink, FunnelLogSqliteSink, FunnelLogValidator, FunnelTextEntry, FunnelTextFileWriter, FunnelTextLevel, FunnelTextLog, FunnelTextStdoutWriter, FunnelTextWriter };
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { t as FunnelLogSqliteSink } from "./funnel-log-sqlite-sink-B_5_4ybn.js";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync, unlinkSync } from "node:fs";
|
|
4
|
+
//#region lib/logger/funnel-log.ts
|
|
5
|
+
/**
|
|
6
|
+
* Validated event log bus. Three responsibilities and nothing else:
|
|
7
|
+
* validate the event, delegate seq + persistence to the primary sink, and
|
|
8
|
+
* fan the resulting record out to relays and live subscribers.
|
|
9
|
+
*
|
|
10
|
+
* Splitting "primary" from "relays" makes the seq invariant honest: there
|
|
11
|
+
* is exactly one source of truth (the primary's atomic insert). Two
|
|
12
|
+
* `FunnelLog` instances pointed at the same SQLite file therefore see
|
|
13
|
+
* one monotonic stream without bus-level coordination. Relays mirror
|
|
14
|
+
* already-sequenced records, so they can be added or removed without
|
|
15
|
+
* affecting correctness.
|
|
16
|
+
*
|
|
17
|
+
* Failure isolation:
|
|
18
|
+
* - Primary failure short-circuits emit and is returned to the caller.
|
|
19
|
+
* - Relay failures never block the primary path — they surface via the
|
|
20
|
+
* optional `onSinkError` callback so the caller can observe without
|
|
21
|
+
* being interrupted.
|
|
22
|
+
* - A subscriber that throws is contained; the rest of the fanout
|
|
23
|
+
* completes normally.
|
|
24
|
+
*/
|
|
25
|
+
var FunnelLog = class {
|
|
26
|
+
validate;
|
|
27
|
+
primary;
|
|
28
|
+
relays;
|
|
29
|
+
now;
|
|
30
|
+
onSinkError;
|
|
31
|
+
listeners = /* @__PURE__ */ new Set();
|
|
32
|
+
constructor(props) {
|
|
33
|
+
this.validate = props.validate;
|
|
34
|
+
this.primary = props.primary;
|
|
35
|
+
this.relays = props.relays ?? [];
|
|
36
|
+
this.now = props.now ?? (() => Date.now());
|
|
37
|
+
this.onSinkError = props.onSinkError ?? null;
|
|
38
|
+
}
|
|
39
|
+
emit(event) {
|
|
40
|
+
const parsed = this.validate(event);
|
|
41
|
+
if (!parsed.success) return parsed.error;
|
|
42
|
+
const result = this.callPrimary(parsed.data);
|
|
43
|
+
if (result instanceof Error) return result;
|
|
44
|
+
this.fanOutToRelays(result);
|
|
45
|
+
this.fanOutToListeners(result);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
subscribe(listener) {
|
|
49
|
+
this.listeners.add(listener);
|
|
50
|
+
return () => {
|
|
51
|
+
this.listeners.delete(listener);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
getMaxSeq() {
|
|
55
|
+
return this.primary.getMaxSeq();
|
|
56
|
+
}
|
|
57
|
+
close() {
|
|
58
|
+
this.listeners.clear();
|
|
59
|
+
this.callClose(this.primary);
|
|
60
|
+
for (const relay of this.relays) this.callClose(relay);
|
|
61
|
+
}
|
|
62
|
+
callPrimary(event) {
|
|
63
|
+
try {
|
|
64
|
+
return this.primary.insert({
|
|
65
|
+
ts: this.now(),
|
|
66
|
+
event
|
|
67
|
+
});
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
fanOutToRelays(record) {
|
|
73
|
+
for (const relay of this.relays) {
|
|
74
|
+
const error = this.callRelay(relay, record);
|
|
75
|
+
if (!error) continue;
|
|
76
|
+
if (this.onSinkError) this.onSinkError(error, record, relay);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
callRelay(relay, record) {
|
|
80
|
+
try {
|
|
81
|
+
const outcome = relay.write(record);
|
|
82
|
+
return outcome instanceof Error ? outcome : null;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
fanOutToListeners(record) {
|
|
88
|
+
for (const listener of this.listeners) try {
|
|
89
|
+
listener(record);
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
callClose(sink) {
|
|
93
|
+
if (!sink.close) return;
|
|
94
|
+
try {
|
|
95
|
+
sink.close();
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region lib/logger/funnel-log-memory-sink.ts
|
|
101
|
+
/**
|
|
102
|
+
* In-memory ring buffer that doubles as primary or relay. As primary it
|
|
103
|
+
* owns its own seq counter (single-process only — for multi-process
|
|
104
|
+
* safety, use `FunnelLogSqliteSink` as primary and place this as a
|
|
105
|
+
* relay). As relay it accepts whatever seq the primary assigned and
|
|
106
|
+
* advances its own counter to match, so `getMaxSeq` stays meaningful.
|
|
107
|
+
*
|
|
108
|
+
* Useful as a test double, as a short-window replay buffer paired with a
|
|
109
|
+
* persistent primary (covering reconnects without round-tripping disk),
|
|
110
|
+
* or as a backing store for live subscribers.
|
|
111
|
+
*/
|
|
112
|
+
var FunnelLogMemorySink = class {
|
|
113
|
+
capacity;
|
|
114
|
+
buffer = [];
|
|
115
|
+
seq = 0;
|
|
116
|
+
constructor(props = {}) {
|
|
117
|
+
this.capacity = Math.max(0, props.capacity ?? 1e3);
|
|
118
|
+
}
|
|
119
|
+
insert(input) {
|
|
120
|
+
this.seq += 1;
|
|
121
|
+
const record = {
|
|
122
|
+
seq: this.seq,
|
|
123
|
+
ts: input.ts,
|
|
124
|
+
event: input.event
|
|
125
|
+
};
|
|
126
|
+
this.append(record);
|
|
127
|
+
return record;
|
|
128
|
+
}
|
|
129
|
+
write(record) {
|
|
130
|
+
if (record.seq > this.seq) this.seq = record.seq;
|
|
131
|
+
this.append(record);
|
|
132
|
+
}
|
|
133
|
+
getMaxSeq() {
|
|
134
|
+
return this.seq;
|
|
135
|
+
}
|
|
136
|
+
query() {
|
|
137
|
+
return this.buffer;
|
|
138
|
+
}
|
|
139
|
+
clear() {
|
|
140
|
+
this.buffer.length = 0;
|
|
141
|
+
this.seq = 0;
|
|
142
|
+
}
|
|
143
|
+
append(record) {
|
|
144
|
+
if (this.capacity === 0) return;
|
|
145
|
+
this.buffer.push(record);
|
|
146
|
+
if (this.buffer.length > this.capacity) this.buffer.shift();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region lib/logger/funnel-text-log.ts
|
|
151
|
+
const LEVEL_RANK = {
|
|
152
|
+
info: 0,
|
|
153
|
+
warn: 1,
|
|
154
|
+
error: 2
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Human-facing diagnostic logger. The companion to `FunnelLog`: where
|
|
158
|
+
* `FunnelLog` is for schema-validated, replayable domain events,
|
|
159
|
+
* `FunnelTextLog` is for free-form info/warn/error messages destined
|
|
160
|
+
* for a human tailing a log or skimming during incident response.
|
|
161
|
+
*
|
|
162
|
+
* Keeping the two separate matters operationally:
|
|
163
|
+
* - Diagnostics typically out-volume domain events 10–1000x; mixing
|
|
164
|
+
* them in the same store would push events out under retention.
|
|
165
|
+
* - Diagnostics are unstructured by design; mixing them in would defeat
|
|
166
|
+
* the schema-first guarantee that makes domain events replayable.
|
|
167
|
+
* - Different audiences and queries (humans grep `tail -f` vs. tools
|
|
168
|
+
* query `WHERE seq > ?`).
|
|
169
|
+
*
|
|
170
|
+
* The writer is a port. Level gating happens here so writers receive only
|
|
171
|
+
* what is worth persisting. Failure isolation matches `FunnelLog`: a
|
|
172
|
+
* writer that throws or returns Error is contained, surfaced via
|
|
173
|
+
* `onWriteError`, and never blocks the caller.
|
|
174
|
+
*/
|
|
175
|
+
var FunnelTextLog = class {
|
|
176
|
+
writer;
|
|
177
|
+
minRank;
|
|
178
|
+
now;
|
|
179
|
+
onWriteError;
|
|
180
|
+
constructor(props) {
|
|
181
|
+
this.writer = props.writer;
|
|
182
|
+
this.minRank = LEVEL_RANK[props.level ?? "info"];
|
|
183
|
+
this.now = props.now ?? (() => Date.now());
|
|
184
|
+
this.onWriteError = props.onWriteError ?? null;
|
|
185
|
+
}
|
|
186
|
+
info(message, meta) {
|
|
187
|
+
this.emit("info", message, meta);
|
|
188
|
+
}
|
|
189
|
+
warn(message, meta) {
|
|
190
|
+
this.emit("warn", message, meta);
|
|
191
|
+
}
|
|
192
|
+
error(message, meta) {
|
|
193
|
+
this.emit("error", message, meta);
|
|
194
|
+
}
|
|
195
|
+
close() {
|
|
196
|
+
if (!this.writer.close) return;
|
|
197
|
+
try {
|
|
198
|
+
this.writer.close();
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
emit(level, message, meta) {
|
|
202
|
+
if (LEVEL_RANK[level] < this.minRank) return;
|
|
203
|
+
const record = {
|
|
204
|
+
ts: this.now(),
|
|
205
|
+
level,
|
|
206
|
+
message,
|
|
207
|
+
meta: meta ?? null
|
|
208
|
+
};
|
|
209
|
+
const error = this.callWriter(record);
|
|
210
|
+
if (error && this.onWriteError) this.onWriteError(error, record);
|
|
211
|
+
}
|
|
212
|
+
callWriter(record) {
|
|
213
|
+
try {
|
|
214
|
+
const outcome = this.writer.write(record);
|
|
215
|
+
return outcome instanceof Error ? outcome : null;
|
|
216
|
+
} catch (e) {
|
|
217
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region lib/logger/funnel-text-file-writer.ts
|
|
223
|
+
/**
|
|
224
|
+
* Appends one JSON line per record to a file. Optional one-keep size
|
|
225
|
+
* rotation. Designed for diagnostic logs a human tails (`tail -f file |
|
|
226
|
+
* jq`); not for replay or queries — use `FunnelLogSqliteSink` if you
|
|
227
|
+
* need indexed lookups.
|
|
228
|
+
*
|
|
229
|
+
* Writes are synchronous (`appendFileSync`), so each line is durable
|
|
230
|
+
* before `write` returns. Throughput matches the OS file cache; for
|
|
231
|
+
* high-volume logging consider buffering at the call site or using a
|
|
232
|
+
* different writer.
|
|
233
|
+
*/
|
|
234
|
+
var FunnelTextFileWriter = class {
|
|
235
|
+
path;
|
|
236
|
+
maxBytes;
|
|
237
|
+
constructor(props) {
|
|
238
|
+
this.path = props.path;
|
|
239
|
+
this.maxBytes = props.maxBytes ?? null;
|
|
240
|
+
this.ensureDir();
|
|
241
|
+
}
|
|
242
|
+
write(record) {
|
|
243
|
+
try {
|
|
244
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
245
|
+
this.rotateIfNeeded(Buffer.byteLength(line));
|
|
246
|
+
appendFileSync(this.path, line);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
ensureDir() {
|
|
252
|
+
const dir = dirname(this.path);
|
|
253
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
254
|
+
}
|
|
255
|
+
rotateIfNeeded(incomingBytes) {
|
|
256
|
+
if (this.maxBytes === null) return;
|
|
257
|
+
if (!existsSync(this.path)) return;
|
|
258
|
+
if (statSync(this.path).size + incomingBytes <= this.maxBytes) return;
|
|
259
|
+
const backup = `${this.path}.1`;
|
|
260
|
+
if (existsSync(backup)) unlinkSync(backup);
|
|
261
|
+
renameSync(this.path, backup);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region lib/logger/funnel-text-stdout-writer.ts
|
|
266
|
+
/**
|
|
267
|
+
* Writes one JSON line per record to stdout. Useful as the default writer
|
|
268
|
+
* for foreground daemons, dev runs, and short-lived processes where a
|
|
269
|
+
* file-backed log would be overkill.
|
|
270
|
+
*/
|
|
271
|
+
var FunnelTextStdoutWriter = class {
|
|
272
|
+
out;
|
|
273
|
+
constructor(props = {}) {
|
|
274
|
+
this.out = props.out ?? process.stdout;
|
|
275
|
+
}
|
|
276
|
+
write(record) {
|
|
277
|
+
this.out.write(`${JSON.stringify(record)}\n`);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
//#endregion
|
|
281
|
+
export { FunnelLog, FunnelLogMemorySink, FunnelLogSqliteSink, FunnelTextFileWriter, FunnelTextLog, FunnelTextStdoutWriter };
|