@interactive-inc/claude-funnel 0.67.0 → 0.68.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/README.md +24 -3
- package/dist/bin.js +276 -270
- package/dist/{channels-BBA3GTAa.d.ts → channels-B0cifCfw.d.ts} +2 -1
- package/dist/claude.d.ts +2 -2
- package/dist/claude.js +5 -5
- package/dist/connector-descriptor-D4XtEsH8.d.ts +215 -0
- package/dist/connectors/discord.d.ts +3 -2
- package/dist/connectors/discord.js +1 -1
- package/dist/connectors/gh.d.ts +3 -2
- package/dist/connectors/gh.js +1 -1
- package/dist/connectors/schedule.d.ts +13 -11
- package/dist/connectors/schedule.js +3 -3
- package/dist/connectors/slack.d.ts +3 -2
- package/dist/connectors/slack.js +1 -1
- package/dist/diagnostic-event-CxMM5Bl2.js +78 -0
- package/dist/diagnostic-log-BsAg5qZK.d.ts +186 -0
- package/dist/diagnostics.d.ts +2 -2
- package/dist/diagnostics.js +3 -2
- package/dist/{discord-connector-CdjwPoty.js → discord-connector-CAcWifJn.js} +1 -1
- package/dist/docs.js +1 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/event-log.d.ts +2 -0
- package/dist/event-log.js +3 -0
- package/dist/{file-process-guard-DAYe9Ndt.d.ts → file-process-guard-D-QatrwL.d.ts} +1 -1
- package/dist/{flume-source-listener-CLtsdcT2.d.ts → flume-source-listener-Ba8_1Mg-.d.ts} +2 -1
- package/dist/{funnel-diagnostics-B-PCuv_E.js → funnel-diagnostics-CAG7hJP0.js} +82 -139
- package/dist/{funnel-diagnostics-CnKus3Jg.d.ts → funnel-diagnostics-CDrZznTB.d.ts} +10 -13
- package/dist/{funnel-docs-BFcqITFA.js → funnel-docs-CI4hMkhq.js} +8 -3
- package/dist/{funnel-doctor-CviUTINw.d.ts → funnel-doctor-CY4S5cnz.d.ts} +1 -1
- package/dist/gateway/daemon.js +59 -79
- package/dist/gateway.d.ts +2 -2
- package/dist/gateway.js +1 -1
- package/dist/{gh-connector-D-iyjl-d.js → gh-connector-DMwI6zJA.js} +1 -1
- package/dist/{index-B4RvEaW-.d.ts → index-fHrKn4jm.d.ts} +7 -2
- package/dist/index.d.ts +18 -14
- package/dist/index.js +124 -30
- package/dist/local-config.d.ts +1 -1
- package/dist/logger.d.ts +6 -270
- package/dist/logger.js +6 -141
- package/dist/memory-event-log-BY8XjFzi.js +147 -0
- package/dist/{memory-token-prompter-D3R_kgM_.d.ts → memory-token-prompter-B20tl_aU.d.ts} +1 -1
- package/dist/{schedule-connector-teeDMHUe.js → schedule-connector-BXUOYgwZ.js} +59 -95
- package/dist/schedule-connector-schema-CNV8PgWX.js +137 -0
- package/dist/schedule-connector-schema-Tq5kJul-.d.ts +127 -0
- package/dist/{slack-connector-BE0KAjCs.js → slack-connector-CC3V2VXQ.js} +1 -1
- package/dist/{slot-fields-5ERhytIA.js → slot-fields-eCWizJqS.js} +1 -1
- package/dist/{sqlite-diagnostic-log-BDxfXGZy.js → sqlite-diagnostic-log-Qs4cf9I_.js} +53 -84
- package/dist/sqlite-event-log-BYgCFMxC.d.ts +284 -0
- package/dist/{funnel-log-sqlite-sink-CXbs3qQp.js → sqlite-event-log-eZuXKszJ.js} +52 -11
- package/package.json +16 -1
- package/dist/connector-descriptor-F75ZI8Fh.d.ts +0 -391
- package/dist/diagnostic-sql-reader-CoBk_EFF.js +0 -83
- package/dist/schedule-connector-schema-C2AChpwX.js +0 -31
- package/dist/schedule-connector-schema-DBko_Bf7.d.ts +0 -48
|
@@ -3,8 +3,8 @@ import { t as NodeFunnelFileSystem } from "./node-file-system-xJx7dymE.js";
|
|
|
3
3
|
import { t as NodeFunnelProcessRunner } from "./node-process-runner-D4rzPl8Q.js";
|
|
4
4
|
import { n as FUNNEL_DIR, s as resolveFunnelPort } from "./settings-store-ZFWBhOnp.js";
|
|
5
5
|
import { t as FunnelAuthFailedError } from "./funnel-error-DoSBCogD.js";
|
|
6
|
-
import { t as
|
|
7
|
-
import { t as
|
|
6
|
+
import { n as diagnosticEventOfProcessed, t as diagnosticConnectionEventOf } from "./diagnostic-event-CxMM5Bl2.js";
|
|
7
|
+
import { t as SqliteEventLog } from "./sqlite-event-log-eZuXKszJ.js";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
10
10
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -159,13 +159,19 @@ var MemoryConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
161
|
queryRaw(query) {
|
|
162
|
-
const matched = this.raws.filter((event) =>
|
|
162
|
+
const matched = this.raws.filter((event) => {
|
|
163
|
+
if (!matches(event, query)) return false;
|
|
164
|
+
if (query.eventId !== void 0 && event.eventId !== query.eventId) return false;
|
|
165
|
+
return true;
|
|
166
|
+
});
|
|
163
167
|
return takeRecent(matched, query.limit);
|
|
164
168
|
}
|
|
165
169
|
queryProcessed(query) {
|
|
166
170
|
const matched = this.processeds.filter((event) => {
|
|
167
171
|
if (!matches(event, query)) return false;
|
|
172
|
+
if (query.eventId !== void 0 && event.eventId !== query.eventId) return false;
|
|
168
173
|
if (query.outcome !== void 0 && event.outcome !== query.outcome) return false;
|
|
174
|
+
if (query.outcomePrefix !== void 0 && !event.outcome.startsWith(query.outcomePrefix)) return false;
|
|
169
175
|
return true;
|
|
170
176
|
});
|
|
171
177
|
return takeRecent(matched, query.limit);
|
|
@@ -174,6 +180,7 @@ var MemoryConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
174
180
|
const matched = this.connections.filter((event) => {
|
|
175
181
|
if (!matches(event, query)) return false;
|
|
176
182
|
if (query.status !== void 0 && event.status !== query.status) return false;
|
|
183
|
+
if (query.statuses !== void 0 && !query.statuses.includes(event.status)) return false;
|
|
177
184
|
return true;
|
|
178
185
|
});
|
|
179
186
|
return takeRecent(matched, query.limit);
|
|
@@ -189,6 +196,7 @@ const matches = (event, query) => {
|
|
|
189
196
|
if (query.type !== void 0 && event.type !== query.type) return false;
|
|
190
197
|
if (query.connectorId !== void 0 && event.connectorId !== query.connectorId) return false;
|
|
191
198
|
if (query.channelId !== void 0 && event.channelId !== query.channelId) return false;
|
|
199
|
+
if ("seq" in event && query.seq !== void 0 && event.seq !== query.seq) return false;
|
|
192
200
|
return true;
|
|
193
201
|
};
|
|
194
202
|
const takeRecent = (events, limit) => {
|
|
@@ -452,7 +460,6 @@ var FunnelBroadcaster = class {
|
|
|
452
460
|
offset: this.latestOffset
|
|
453
461
|
};
|
|
454
462
|
const payload = JSON.stringify(event);
|
|
455
|
-
meta?.connector;
|
|
456
463
|
this.eventsBroadcast += 1;
|
|
457
464
|
this.lastBroadcastAt = this.now();
|
|
458
465
|
if (this.replayBufferSize > 0) {
|
|
@@ -579,7 +586,7 @@ var SqliteFunnelEventLog = class extends FunnelEventLog {
|
|
|
579
586
|
this.now = props.now ?? (() => Date.now());
|
|
580
587
|
this.logger = props.logger;
|
|
581
588
|
this.onError = props.onError;
|
|
582
|
-
this.sink = new
|
|
589
|
+
this.sink = new SqliteEventLog({
|
|
583
590
|
path: props.path,
|
|
584
591
|
indexes: ["channel_id", "connector_id"],
|
|
585
592
|
extractIndexes: (event) => ({
|
|
@@ -893,7 +900,7 @@ var FunnelListenerRegistry = class FunnelListenerRegistry {
|
|
|
893
900
|
async stopAll() {
|
|
894
901
|
this.stopHealthCheck();
|
|
895
902
|
this.pendingRetry.clear();
|
|
896
|
-
for (const [, entry] of
|
|
903
|
+
for (const [, entry] of this.running.entries()) await this.stop(entry.channelName, entry.config.name);
|
|
897
904
|
}
|
|
898
905
|
ensureStats(key) {
|
|
899
906
|
const existing = this.stats.get(key);
|
|
@@ -940,7 +947,7 @@ var FunnelListenerRegistry = class FunnelListenerRegistry {
|
|
|
940
947
|
this.healthCheckInFlight = true;
|
|
941
948
|
try {
|
|
942
949
|
const dead = [];
|
|
943
|
-
for (const [key, entry] of
|
|
950
|
+
for (const [key, entry] of this.running.entries()) {
|
|
944
951
|
if (entry.listener.isAlive()) {
|
|
945
952
|
this.failureCounts.delete(key);
|
|
946
953
|
continue;
|
|
@@ -953,7 +960,7 @@ var FunnelListenerRegistry = class FunnelListenerRegistry {
|
|
|
953
960
|
}
|
|
954
961
|
await Promise.all(dead.map((target) => this.recoverDead(target.channelName, target.connectorName, target.type)));
|
|
955
962
|
const retries = [];
|
|
956
|
-
for (const [key, pending] of
|
|
963
|
+
for (const [key, pending] of this.pendingRetry.entries()) {
|
|
957
964
|
if (this.running.has(key)) {
|
|
958
965
|
this.pendingRetry.delete(key);
|
|
959
966
|
continue;
|
|
@@ -1189,19 +1196,6 @@ const channelsPublishHandler = factory.createHandlers(zParam(z.object({ channel:
|
|
|
1189
1196
|
});
|
|
1190
1197
|
//#endregion
|
|
1191
1198
|
//#region lib/gateway/routes/debug.ts
|
|
1192
|
-
const extractPreview = (payload) => {
|
|
1193
|
-
if (typeof payload !== "string" || payload.length === 0) return null;
|
|
1194
|
-
try {
|
|
1195
|
-
const parsed = JSON.parse(payload);
|
|
1196
|
-
if (parsed !== null && typeof parsed === "object" && "text" in parsed) {
|
|
1197
|
-
const text = String(parsed.text);
|
|
1198
|
-
return text.length > 80 ? `${text.slice(0, 80)}…` : text;
|
|
1199
|
-
}
|
|
1200
|
-
} catch {
|
|
1201
|
-
return payload.length > 80 ? `${payload.slice(0, 80)}…` : payload;
|
|
1202
|
-
}
|
|
1203
|
-
return payload.length > 80 ? `${payload.slice(0, 80)}…` : payload;
|
|
1204
|
-
};
|
|
1205
1199
|
const buildChannelDiagnosis = (channel) => {
|
|
1206
1200
|
const rootCause = (channel.connectionErrors[channel.connectionErrors.length - 1] ?? null)?.detail ?? null;
|
|
1207
1201
|
if (channel.connectors.length === 0) return {
|
|
@@ -1250,11 +1244,6 @@ const debugHandler = factory.createHandlers(async (c) => {
|
|
|
1250
1244
|
const gatewayListeners = deps.registry.list();
|
|
1251
1245
|
const gatewayClients = deps.broadcaster.listChannels();
|
|
1252
1246
|
const metrics = deps.broadcaster.getMetrics();
|
|
1253
|
-
const tmpDir = funnelTmpDir();
|
|
1254
|
-
const rawPath = join(tmpDir, "connector-raw.db");
|
|
1255
|
-
const processedPath = join(tmpDir, "connector-processed.db");
|
|
1256
|
-
const connectionPath = join(tmpDir, "connector-connection.db");
|
|
1257
|
-
const hasStore = existsSync(rawPath) && existsSync(processedPath) && existsSync(connectionPath);
|
|
1258
1247
|
const channels = targetChannels.map((ch) => {
|
|
1259
1248
|
const listenerEntry = gatewayListeners.find((l) => l.channelName === ch.name) ?? null;
|
|
1260
1249
|
const listener = listenerEntry ? {
|
|
@@ -1264,61 +1253,30 @@ const debugHandler = factory.createHandlers(async (c) => {
|
|
|
1264
1253
|
lastEventAt: listenerEntry.lastEventAt
|
|
1265
1254
|
} : null;
|
|
1266
1255
|
const claudeClients = gatewayClients.filter((cl) => cl.channel === ch.id || cl.channel === ch.name).length;
|
|
1267
|
-
const recentEvents =
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
try {
|
|
1277
|
-
return reader.query("SELECT seq, ts, type, outcome, payload FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT 10", [ch.id]);
|
|
1278
|
-
} finally {
|
|
1279
|
-
reader.close();
|
|
1280
|
-
}
|
|
1281
|
-
})();
|
|
1282
|
-
if (!(rows instanceof Error)) for (const row of [...rows].reverse()) {
|
|
1283
|
-
const rawPayload = typeof row.payload === "string" ? row.payload : null;
|
|
1284
|
-
let payloadParsed = null;
|
|
1285
|
-
if (rawPayload) try {
|
|
1286
|
-
const parsed = JSON.parse(rawPayload);
|
|
1287
|
-
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) payloadParsed = parsed;
|
|
1288
|
-
} catch {
|
|
1289
|
-
payloadParsed = null;
|
|
1290
|
-
}
|
|
1291
|
-
recentEvents.push({
|
|
1292
|
-
seq: typeof row.seq === "number" ? row.seq : null,
|
|
1293
|
-
ts: typeof row.ts === "number" ? row.ts : null,
|
|
1294
|
-
type: typeof row.type === "string" ? row.type : "?",
|
|
1295
|
-
outcome: typeof row.outcome === "string" ? row.outcome : "?",
|
|
1296
|
-
payload: rawPayload,
|
|
1297
|
-
payloadParsed,
|
|
1298
|
-
preview: extractPreview(row.payload)
|
|
1299
|
-
});
|
|
1256
|
+
const recentEvents = (() => {
|
|
1257
|
+
if (!deps.diagnosticLog) return [];
|
|
1258
|
+
try {
|
|
1259
|
+
return deps.diagnosticLog.queryProcessed({
|
|
1260
|
+
channelId: ch.id,
|
|
1261
|
+
limit: 10
|
|
1262
|
+
}).map(diagnosticEventOfProcessed);
|
|
1263
|
+
} catch {
|
|
1264
|
+
return [];
|
|
1300
1265
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
})();
|
|
1314
|
-
if (!(errRows instanceof Error)) for (const row of [...errRows].reverse()) connectionErrors.push({
|
|
1315
|
-
ts: typeof row.ts === "number" ? row.ts : null,
|
|
1316
|
-
type: typeof row.type === "string" ? row.type : "?",
|
|
1317
|
-
status: typeof row.status === "string" ? row.status : "?",
|
|
1318
|
-
detail: typeof row.detail === "string" && row.detail.length > 0 ? row.detail : null
|
|
1319
|
-
});
|
|
1266
|
+
})();
|
|
1267
|
+
const needsConnErrors = listener && (!listener.alive || listener.errors > 0) || !listener;
|
|
1268
|
+
const connectionErrors = (() => {
|
|
1269
|
+
if (!needsConnErrors || !deps.diagnosticLog) return [];
|
|
1270
|
+
try {
|
|
1271
|
+
return deps.diagnosticLog.queryConnection({
|
|
1272
|
+
channelId: ch.id,
|
|
1273
|
+
statuses: ["auth-failed", "error"],
|
|
1274
|
+
limit: 3
|
|
1275
|
+
}).map(diagnosticConnectionEventOf);
|
|
1276
|
+
} catch {
|
|
1277
|
+
return [];
|
|
1320
1278
|
}
|
|
1321
|
-
}
|
|
1279
|
+
})();
|
|
1322
1280
|
const base = {
|
|
1323
1281
|
id: ch.id,
|
|
1324
1282
|
name: ch.name,
|
|
@@ -1468,6 +1426,7 @@ var FunnelGatewayServer = class {
|
|
|
1468
1426
|
nowMs;
|
|
1469
1427
|
extraRoutes;
|
|
1470
1428
|
ownsEventLog;
|
|
1429
|
+
diagnosticLog;
|
|
1471
1430
|
startedAt = null;
|
|
1472
1431
|
server = null;
|
|
1473
1432
|
disposed = false;
|
|
@@ -1489,6 +1448,7 @@ var FunnelGatewayServer = class {
|
|
|
1489
1448
|
this.token = deps.token ?? "";
|
|
1490
1449
|
this.allowInsecureHost = deps.allowInsecureHost ?? false;
|
|
1491
1450
|
this.extraRoutes = deps.extraRoutes ?? null;
|
|
1451
|
+
this.diagnosticLog = deps.diagnosticLog;
|
|
1492
1452
|
const clock = deps.clock;
|
|
1493
1453
|
this.nowMs = clock ? () => clock.millis() : () => Date.now();
|
|
1494
1454
|
if (deps.eventLog) {
|
|
@@ -1681,7 +1641,8 @@ var FunnelGatewayServer = class {
|
|
|
1681
1641
|
registry: this.registry,
|
|
1682
1642
|
channels: this.channels,
|
|
1683
1643
|
uptimeMs: () => this.startedAt ? this.nowMs() - this.startedAt : 0,
|
|
1684
|
-
emit: (input) => this.emit(input)
|
|
1644
|
+
emit: (input) => this.emit(input),
|
|
1645
|
+
diagnosticLog: this.diagnosticLog
|
|
1685
1646
|
});
|
|
1686
1647
|
return next();
|
|
1687
1648
|
});
|
|
@@ -1946,7 +1907,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
1946
1907
|
...ageCap,
|
|
1947
1908
|
...rawMax !== void 0 ? { maxRows: rawMax } : {}
|
|
1948
1909
|
};
|
|
1949
|
-
this.raw = new
|
|
1910
|
+
this.raw = new SqliteEventLog({
|
|
1950
1911
|
path: props.rawPath,
|
|
1951
1912
|
indexes: [
|
|
1952
1913
|
"event_id",
|
|
@@ -1960,7 +1921,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
1960
1921
|
}),
|
|
1961
1922
|
...rawCap
|
|
1962
1923
|
});
|
|
1963
|
-
this.processed = new
|
|
1924
|
+
this.processed = new SqliteEventLog({
|
|
1964
1925
|
path: props.processedPath,
|
|
1965
1926
|
indexes: [
|
|
1966
1927
|
"event_id",
|
|
@@ -1976,7 +1937,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
1976
1937
|
}),
|
|
1977
1938
|
...verdictCap
|
|
1978
1939
|
});
|
|
1979
|
-
this.connection = new
|
|
1940
|
+
this.connection = new SqliteEventLog({
|
|
1980
1941
|
path: props.connectionPath,
|
|
1981
1942
|
indexes: [
|
|
1982
1943
|
"connector_id",
|
|
@@ -2042,10 +2003,13 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
2042
2003
|
});
|
|
2043
2004
|
}
|
|
2044
2005
|
queryRaw(query) {
|
|
2006
|
+
const where = buildWhere(query);
|
|
2007
|
+
if (query.eventId !== void 0) where.event_id = query.eventId;
|
|
2045
2008
|
return this.raw.query({
|
|
2046
2009
|
...query.type !== void 0 ? { type: query.type } : {},
|
|
2010
|
+
...query.seq !== void 0 ? { seq: query.seq } : {},
|
|
2047
2011
|
...query.limit !== void 0 ? { limit: query.limit } : {},
|
|
2048
|
-
where
|
|
2012
|
+
where,
|
|
2049
2013
|
order: "desc"
|
|
2050
2014
|
}).map((record) => ({
|
|
2051
2015
|
seq: record.seq,
|
|
@@ -2059,11 +2023,14 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
2059
2023
|
}
|
|
2060
2024
|
queryProcessed(query) {
|
|
2061
2025
|
const where = buildWhere(query);
|
|
2026
|
+
if (query.eventId !== void 0) where.event_id = query.eventId;
|
|
2062
2027
|
if (query.outcome !== void 0) where.outcome = query.outcome;
|
|
2063
2028
|
return this.processed.query({
|
|
2064
2029
|
...query.type !== void 0 ? { type: query.type } : {},
|
|
2030
|
+
...query.seq !== void 0 ? { seq: query.seq } : {},
|
|
2065
2031
|
...query.limit !== void 0 ? { limit: query.limit } : {},
|
|
2066
2032
|
where,
|
|
2033
|
+
...query.outcomePrefix !== void 0 ? { wherePrefix: { outcome: query.outcomePrefix } } : {},
|
|
2067
2034
|
order: "desc"
|
|
2068
2035
|
}).map((record) => ({
|
|
2069
2036
|
seq: record.seq,
|
|
@@ -2081,8 +2048,10 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
2081
2048
|
if (query.status !== void 0) where.status = query.status;
|
|
2082
2049
|
return this.connection.query({
|
|
2083
2050
|
...query.type !== void 0 ? { type: query.type } : {},
|
|
2051
|
+
...query.seq !== void 0 ? { seq: query.seq } : {},
|
|
2084
2052
|
...query.limit !== void 0 ? { limit: query.limit } : {},
|
|
2085
2053
|
where,
|
|
2054
|
+
...query.statuses !== void 0 ? { whereIn: { status: query.statuses } } : {},
|
|
2086
2055
|
order: "desc"
|
|
2087
2056
|
}).map((record) => ({
|
|
2088
2057
|
seq: record.seq,
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
//#region lib/event-log/event-log-entry.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Wrapper that `EventLog.append` 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
|
+
* validation function passed to the log.
|
|
9
|
+
*/
|
|
10
|
+
type EventLogEntry<E> = {
|
|
11
|
+
seq: number;
|
|
12
|
+
ts: number;
|
|
13
|
+
event: E;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region lib/event-log/event-log-store.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 `EventLog` surfaces via
|
|
24
|
+
* `onSinkError`. Throwing is also tolerated (`EventLog` catches it), but
|
|
25
|
+
* returning is preferred so the failure path is part of the type.
|
|
26
|
+
*/
|
|
27
|
+
type EventLogRelay<E> = {
|
|
28
|
+
write(record: EventLogEntry<E>): void | Error;
|
|
29
|
+
close?(): void;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Primary sink. Owns the canonical seq sequence for the log. `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 coordinating through `EventLog`.
|
|
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 EventLogStore<E> = {
|
|
43
|
+
insert(input: {
|
|
44
|
+
ts: number;
|
|
45
|
+
event: E;
|
|
46
|
+
}): EventLogEntry<E> | Error;
|
|
47
|
+
getMaxSeq(): number;
|
|
48
|
+
close?(): void;
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region lib/event-log/event-log.d.ts
|
|
52
|
+
type Listener<E> = (record: EventLogEntry<E>) => void;
|
|
53
|
+
type SinkErrorHandler<E> = (error: Error, record: EventLogEntry<E>, sink: EventLogRelay<E>) => void;
|
|
54
|
+
type EventLogValidator<E> = (event: unknown) => {
|
|
55
|
+
success: true;
|
|
56
|
+
data: E;
|
|
57
|
+
} | {
|
|
58
|
+
success: false;
|
|
59
|
+
error: Error;
|
|
60
|
+
};
|
|
61
|
+
type EventLogProps<E> = {
|
|
62
|
+
/** Validates each event before emission. Use `schema.safeParse` from any validation library, or a plain function. */
|
|
63
|
+
validate: EventLogValidator<E>;
|
|
64
|
+
/** Owns seq assignment + durability. Use `SqliteEventLog` for multi-process safety. */
|
|
65
|
+
primary: EventLogStore<E>;
|
|
66
|
+
/** Optional fanout for already-sequenced records (memory ring, stdout, network mirror). */
|
|
67
|
+
relays?: ReadonlyArray<EventLogRelay<E>>;
|
|
68
|
+
/** Override for tests. Defaults to `Date.now`. */
|
|
69
|
+
now?: () => number;
|
|
70
|
+
/** Observer for relay failures. Default: silently swallow. */
|
|
71
|
+
onSinkError?: SinkErrorHandler<E>;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Validated event log. Three responsibilities and nothing else:
|
|
75
|
+
* validate the event, delegate seq + persistence to the primary sink, and
|
|
76
|
+
* fan the resulting record out to relays and live subscribers.
|
|
77
|
+
*
|
|
78
|
+
* Splitting "primary" from "relays" makes the seq invariant honest: there
|
|
79
|
+
* is exactly one source of truth (the primary's atomic insert). Two
|
|
80
|
+
* `EventLog` instances pointed at the same SQLite file therefore see
|
|
81
|
+
* one monotonic stream without coordinating with each other. Relays mirror
|
|
82
|
+
* already-sequenced records, so they can be added or removed without
|
|
83
|
+
* affecting correctness.
|
|
84
|
+
*
|
|
85
|
+
* Failure isolation:
|
|
86
|
+
* - Primary failure short-circuits append and is returned to the caller.
|
|
87
|
+
* - Relay failures never block the primary path — they surface via the
|
|
88
|
+
* optional `onSinkError` callback so the caller can observe without
|
|
89
|
+
* being interrupted.
|
|
90
|
+
* - A subscriber that throws is contained; the rest of the fanout
|
|
91
|
+
* completes normally.
|
|
92
|
+
*/
|
|
93
|
+
declare class EventLog<E> {
|
|
94
|
+
private readonly validate;
|
|
95
|
+
private readonly primary;
|
|
96
|
+
private readonly relays;
|
|
97
|
+
private readonly now;
|
|
98
|
+
private readonly onSinkError;
|
|
99
|
+
private readonly listeners;
|
|
100
|
+
constructor(props: EventLogProps<E>);
|
|
101
|
+
append(event: E): EventLogEntry<E> | Error;
|
|
102
|
+
subscribe(listener: Listener<E>): () => void;
|
|
103
|
+
getMaxSeq(): number;
|
|
104
|
+
close(): void;
|
|
105
|
+
private callPrimary;
|
|
106
|
+
private fanOutToRelays;
|
|
107
|
+
private callRelay;
|
|
108
|
+
private fanOutToListeners;
|
|
109
|
+
private callClose;
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region lib/event-log/memory-event-log.d.ts
|
|
113
|
+
type MemoryEventLogProps = {
|
|
114
|
+
/** Hard cap on retained records. The oldest is evicted on overflow. 0 disables retention. */
|
|
115
|
+
capacity?: number;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* In-memory ring buffer that doubles as primary or relay. As primary it
|
|
119
|
+
* owns its own seq counter (single-process only — for multi-process
|
|
120
|
+
* safety, use `SqliteEventLog` as primary and place this as a
|
|
121
|
+
* relay). As relay it accepts whatever seq the primary assigned and
|
|
122
|
+
* advances its own counter to match, so `getMaxSeq` stays meaningful.
|
|
123
|
+
*
|
|
124
|
+
* Useful as a test double, as a short-window replay buffer paired with a
|
|
125
|
+
* persistent primary (covering reconnects without round-tripping disk),
|
|
126
|
+
* or as a backing store for live subscribers.
|
|
127
|
+
*/
|
|
128
|
+
declare class MemoryEventLog<E> implements EventLogStore<E>, EventLogRelay<E> {
|
|
129
|
+
private readonly capacity;
|
|
130
|
+
private readonly buffer;
|
|
131
|
+
private seq;
|
|
132
|
+
constructor(props?: MemoryEventLogProps);
|
|
133
|
+
insert(input: {
|
|
134
|
+
ts: number;
|
|
135
|
+
event: E;
|
|
136
|
+
}): EventLogEntry<E>;
|
|
137
|
+
write(record: EventLogEntry<E>): void;
|
|
138
|
+
getMaxSeq(): number;
|
|
139
|
+
query(): ReadonlyArray<EventLogEntry<E>>;
|
|
140
|
+
clear(): void;
|
|
141
|
+
private append;
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region lib/event-log/sqlite-event-log.d.ts
|
|
145
|
+
type IndexValues<I extends ReadonlyArray<string>> = Record<I[number], string | null>;
|
|
146
|
+
type IndexPrefixes<I extends ReadonlyArray<string>> = Partial<Record<I[number], string>>;
|
|
147
|
+
type IndexSets<I extends ReadonlyArray<string>> = Partial<Record<I[number], ReadonlyArray<string | null>>>;
|
|
148
|
+
/**
|
|
149
|
+
* Constructor props. The shape narrows on `I`: when no indexes are
|
|
150
|
+
* declared (the default), `extractIndexes` is forbidden; when indexes
|
|
151
|
+
* are declared, both `indexes` and `extractIndexes` are required and
|
|
152
|
+
* `extractIndexes` is type-checked against the index keys.
|
|
153
|
+
*/
|
|
154
|
+
type SqliteEventLogProps<E, I extends ReadonlyArray<string>> = I extends readonly [] ? {
|
|
155
|
+
path: string;
|
|
156
|
+
maxRows?: number;
|
|
157
|
+
maxAgeMs?: number;
|
|
158
|
+
maxBytes?: number;
|
|
159
|
+
targetBytes?: number;
|
|
160
|
+
now?: () => number;
|
|
161
|
+
indexes?: I;
|
|
162
|
+
extractIndexes?: never;
|
|
163
|
+
} : {
|
|
164
|
+
path: string;
|
|
165
|
+
maxRows?: number;
|
|
166
|
+
maxAgeMs?: number;
|
|
167
|
+
maxBytes?: number;
|
|
168
|
+
targetBytes?: number;
|
|
169
|
+
now?: () => number;
|
|
170
|
+
indexes: I;
|
|
171
|
+
extractIndexes: (event: E) => IndexValues<I>;
|
|
172
|
+
};
|
|
173
|
+
type SqliteEventLogQuery<I extends ReadonlyArray<string>> = {
|
|
174
|
+
/** Return only records with seq strictly greater than this. */
|
|
175
|
+
sinceSeq?: number;
|
|
176
|
+
/** Match one sequence number exactly instead of using `sinceSeq`. */
|
|
177
|
+
seq?: number;
|
|
178
|
+
/** Filter by the top-level `event.type` discriminator. */
|
|
179
|
+
type?: string;
|
|
180
|
+
/** Filter by indexed columns. Keys are constrained to the declared `indexes`. */
|
|
181
|
+
where?: Partial<IndexValues<I>>;
|
|
182
|
+
/** Prefix-match indexed text columns. */
|
|
183
|
+
wherePrefix?: IndexPrefixes<I>;
|
|
184
|
+
/** Match any value in a set for indexed columns. */
|
|
185
|
+
whereIn?: IndexSets<I>;
|
|
186
|
+
/** Maximum rows returned. Default 1000. */
|
|
187
|
+
limit?: number;
|
|
188
|
+
/**
|
|
189
|
+
* Which end of the seq range to take when `limit` clips the result.
|
|
190
|
+
* "asc" (default) returns the oldest matching rows; "desc" returns the
|
|
191
|
+
* newest. Rows are always sorted ascending by seq before returning, so the
|
|
192
|
+
* caller sees a chronological slice either way — "desc" just picks the tail.
|
|
193
|
+
*/
|
|
194
|
+
order?: "asc" | "desc";
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* SQLite-backed sink built on `bun:sqlite`. Implements both primary and
|
|
198
|
+
* relay roles so the same instance can own seq generation for one log and
|
|
199
|
+
* mirror records from another (e.g. cross-process replication, restore
|
|
200
|
+
* from a backup stream).
|
|
201
|
+
*
|
|
202
|
+
* Concurrency model: seq is `INTEGER PRIMARY KEY`, so SQLite assigns it
|
|
203
|
+
* atomically via `lastInsertRowid`. Two `EventLog` instances pointed
|
|
204
|
+
* at the same database file therefore see one monotonically increasing
|
|
205
|
+
* seq stream without coordinating through `EventLog` — the database itself is
|
|
206
|
+
* the synchronization point.
|
|
207
|
+
*
|
|
208
|
+
* Schema is version-managed via `PRAGMA user_version`. Migrations are
|
|
209
|
+
* append-only and run in a transaction on every construct so a partial
|
|
210
|
+
* upgrade rolls back cleanly. Caller-defined `indexes` are layered on top
|
|
211
|
+
* via `ALTER TABLE ADD COLUMN` + `CREATE INDEX IF NOT EXISTS`, so adding
|
|
212
|
+
* a new index to an existing database is a no-downtime operation.
|
|
213
|
+
*
|
|
214
|
+
* Type safety: the second generic parameter `I` is the literal tuple of
|
|
215
|
+
* index column names. `extractIndexes` and `query({ where })` are
|
|
216
|
+
* both type-checked against this tuple, so a typo at the call site is a
|
|
217
|
+
* compile-time error rather than a silent miss at runtime.
|
|
218
|
+
*
|
|
219
|
+
* Retention is bounded by `maxRows` and/or `maxAgeMs`. Both run on every
|
|
220
|
+
* insert as a single indexed DELETE that no-ops below the cap.
|
|
221
|
+
*
|
|
222
|
+
* Bulk inserts use `insertMany`, which wraps the batch in one transaction
|
|
223
|
+
* for ~10–100x throughput at the cost of one fsync per batch instead of
|
|
224
|
+
* one per row.
|
|
225
|
+
*/
|
|
226
|
+
declare class SqliteEventLog<E, const I extends ReadonlyArray<string> = readonly []> implements EventLogStore<E>, EventLogRelay<E> {
|
|
227
|
+
private readonly db;
|
|
228
|
+
private readonly maxRows;
|
|
229
|
+
private readonly maxAgeMs;
|
|
230
|
+
private readonly maxBytes;
|
|
231
|
+
private readonly targetBytes;
|
|
232
|
+
private readonly now;
|
|
233
|
+
private readonly indexes;
|
|
234
|
+
private readonly extractIndexes;
|
|
235
|
+
private readonly insertStmt;
|
|
236
|
+
private readonly insertWithSeqStmt;
|
|
237
|
+
private readonly maxSeqStmt;
|
|
238
|
+
private readonly countStmt;
|
|
239
|
+
private readonly trimRowsStmt;
|
|
240
|
+
private readonly trimAgeStmt;
|
|
241
|
+
private readonly trimOldestStmt;
|
|
242
|
+
private insertsSinceByteCheck;
|
|
243
|
+
constructor(props: SqliteEventLogProps<E, I>);
|
|
244
|
+
insert(input: {
|
|
245
|
+
ts: number;
|
|
246
|
+
event: E;
|
|
247
|
+
}): EventLogEntry<E> | Error;
|
|
248
|
+
insertMany(inputs: ReadonlyArray<{
|
|
249
|
+
ts: number;
|
|
250
|
+
event: E;
|
|
251
|
+
}>): EventLogEntry<E>[] | Error;
|
|
252
|
+
write(record: EventLogEntry<E>): void | Error;
|
|
253
|
+
getMaxSeq(): number;
|
|
254
|
+
query(props?: SqliteEventLogQuery<I>): EventLogEntry<E>[];
|
|
255
|
+
/**
|
|
256
|
+
* Current schema version. Useful for diagnostics and for tests that want
|
|
257
|
+
* to verify migrations ran. Reads `PRAGMA user_version` once per call.
|
|
258
|
+
*/
|
|
259
|
+
getSchemaVersion(): number;
|
|
260
|
+
close(): void;
|
|
261
|
+
private buildInsertParams;
|
|
262
|
+
private appendWhereConditions;
|
|
263
|
+
private appendPrefixConditions;
|
|
264
|
+
private appendSetConditions;
|
|
265
|
+
private trim;
|
|
266
|
+
private indexKeys;
|
|
267
|
+
/**
|
|
268
|
+
* Throttled byte-size enforcement. Only every BYTE_CHECK_INTERVAL inserts do
|
|
269
|
+
* we measure the file; on overflow we estimate how many of the oldest rows to
|
|
270
|
+
* drop to land near targetBytes (by the byte/row ratio), delete them in one
|
|
271
|
+
* statement, then VACUUM once to return the freed pages to the filesystem (a
|
|
272
|
+
* plain DELETE only frees pages inside the file). One DELETE + one VACUUM per
|
|
273
|
+
* overflow keeps the expensive rewrite rare — the file must refill the whole
|
|
274
|
+
* maxBytes→targetBytes delta before the next overflow can trigger.
|
|
275
|
+
*/
|
|
276
|
+
private maybeTrimBytes;
|
|
277
|
+
private byteSize;
|
|
278
|
+
/** Drop every row and reclaim the file space. Used by `<log>.clear()`. */
|
|
279
|
+
clear(): void;
|
|
280
|
+
private syncIndexColumns;
|
|
281
|
+
private migrate;
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
export { MemoryEventLogProps as a, EventLogValidator as c, EventLogEntry as d, MemoryEventLog as i, EventLogRelay as l, SqliteEventLogProps as n, EventLog as o, SqliteEventLogQuery as r, EventLogProps as s, SqliteEventLog as t, EventLogStore as u };
|