Nostr: fix typing bus + metrics
This commit is contained in:
parent
1f2013299c
commit
f00cc5464e
@ -41,6 +41,11 @@ export type RateLimitMetricName = "rate_limit.per_sender" | "rate_limit.global";
|
|||||||
|
|
||||||
export type DecryptMetricName = "decrypt.success" | "decrypt.failure";
|
export type DecryptMetricName = "decrypt.success" | "decrypt.failure";
|
||||||
|
|
||||||
|
export type TypingMetricName =
|
||||||
|
| "typing.start.sent"
|
||||||
|
| "typing.stop.sent"
|
||||||
|
| "typing.error";
|
||||||
|
|
||||||
export type MemoryMetricName =
|
export type MemoryMetricName =
|
||||||
| "memory.seen_tracker_size"
|
| "memory.seen_tracker_size"
|
||||||
| "memory.rate_limiter_entries";
|
| "memory.rate_limiter_entries";
|
||||||
@ -50,6 +55,7 @@ export type MetricName =
|
|||||||
| RelayMetricName
|
| RelayMetricName
|
||||||
| RateLimitMetricName
|
| RateLimitMetricName
|
||||||
| DecryptMetricName
|
| DecryptMetricName
|
||||||
|
| TypingMetricName
|
||||||
| MemoryMetricName;
|
| MemoryMetricName;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -128,6 +134,13 @@ export interface MetricsSnapshot {
|
|||||||
failure: number;
|
failure: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Typing indicator stats */
|
||||||
|
typing: {
|
||||||
|
startSent: number;
|
||||||
|
stopSent: number;
|
||||||
|
errors: number;
|
||||||
|
};
|
||||||
|
|
||||||
/** Memory/capacity stats */
|
/** Memory/capacity stats */
|
||||||
memory: {
|
memory: {
|
||||||
seenTrackerSize: number;
|
seenTrackerSize: number;
|
||||||
@ -213,6 +226,13 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics {
|
|||||||
failure: 0,
|
failure: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Typing indicator stats
|
||||||
|
const typing = {
|
||||||
|
startSent: 0,
|
||||||
|
stopSent: 0,
|
||||||
|
errors: 0,
|
||||||
|
};
|
||||||
|
|
||||||
// Memory stats (updated via gauge-style metrics)
|
// Memory stats (updated via gauge-style metrics)
|
||||||
const memory = {
|
const memory = {
|
||||||
seenTrackerSize: 0,
|
seenTrackerSize: 0,
|
||||||
@ -371,6 +391,16 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics {
|
|||||||
decrypt.failure += value;
|
decrypt.failure += value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "typing.start.sent":
|
||||||
|
typing.startSent += value;
|
||||||
|
break;
|
||||||
|
case "typing.stop.sent":
|
||||||
|
typing.stopSent += value;
|
||||||
|
break;
|
||||||
|
case "typing.error":
|
||||||
|
typing.errors += value;
|
||||||
|
break;
|
||||||
|
|
||||||
// Memory (gauge-style - value replaces, not adds)
|
// Memory (gauge-style - value replaces, not adds)
|
||||||
case "memory.seen_tracker_size":
|
case "memory.seen_tracker_size":
|
||||||
memory.seenTrackerSize = value;
|
memory.seenTrackerSize = value;
|
||||||
@ -396,6 +426,7 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics {
|
|||||||
relays: relaysObj,
|
relays: relaysObj,
|
||||||
rateLimiting: { ...rateLimiting },
|
rateLimiting: { ...rateLimiting },
|
||||||
decrypt: { ...decrypt },
|
decrypt: { ...decrypt },
|
||||||
|
typing: { ...typing },
|
||||||
memory: { ...memory },
|
memory: { ...memory },
|
||||||
snapshotAt: Date.now(),
|
snapshotAt: Date.now(),
|
||||||
};
|
};
|
||||||
@ -422,6 +453,9 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics {
|
|||||||
rateLimiting.globalHits = 0;
|
rateLimiting.globalHits = 0;
|
||||||
decrypt.success = 0;
|
decrypt.success = 0;
|
||||||
decrypt.failure = 0;
|
decrypt.failure = 0;
|
||||||
|
typing.startSent = 0;
|
||||||
|
typing.stopSent = 0;
|
||||||
|
typing.errors = 0;
|
||||||
memory.seenTrackerSize = 0;
|
memory.seenTrackerSize = 0;
|
||||||
memory.rateLimiterEntries = 0;
|
memory.rateLimiterEntries = 0;
|
||||||
}
|
}
|
||||||
@ -452,6 +486,7 @@ export function createNoopMetrics(): NostrMetrics {
|
|||||||
relays: {},
|
relays: {},
|
||||||
rateLimiting: { perSenderHits: 0, globalHits: 0 },
|
rateLimiting: { perSenderHits: 0, globalHits: 0 },
|
||||||
decrypt: { success: 0, failure: 0 },
|
decrypt: { success: 0, failure: 0 },
|
||||||
|
typing: { startSent: 0, stopSent: 0, errors: 0 },
|
||||||
memory: { seenTrackerSize: 0, rateLimiterEntries: 0 },
|
memory: { seenTrackerSize: 0, rateLimiterEntries: 0 },
|
||||||
snapshotAt: 0,
|
snapshotAt: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -51,6 +51,11 @@ const CIRCUIT_BREAKER_RESET_MS = 30000; // 30 seconds before half-open
|
|||||||
// Health tracker configuration
|
// Health tracker configuration
|
||||||
const HEALTH_WINDOW_MS = 60000; // 1 minute window for health stats
|
const HEALTH_WINDOW_MS = 60000; // 1 minute window for health stats
|
||||||
|
|
||||||
|
// Typing indicator configuration (NIP-01 ephemeral events)
|
||||||
|
const TYPING_KIND = 20001; // Community convention for typing indicators
|
||||||
|
const TYPING_TTL_SEC = 30; // 30 second expiration
|
||||||
|
const TYPING_THROTTLE_MS = 5000; // Max 1 event per 5 seconds per recipient
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -66,7 +71,8 @@ export interface NostrBusOptions {
|
|||||||
onMessage: (
|
onMessage: (
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
text: string,
|
text: string,
|
||||||
reply: (text: string) => Promise<void>
|
reply: (text: string) => Promise<void>,
|
||||||
|
eventId: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
/** Called on errors (optional) */
|
/** Called on errors (optional) */
|
||||||
onError?: (error: Error, context: string) => void;
|
onError?: (error: Error, context: string) => void;
|
||||||
@ -101,6 +107,10 @@ export interface NostrBusHandle {
|
|||||||
lastPublishedEventId: string | null;
|
lastPublishedEventId: string | null;
|
||||||
lastPublishResults: Record<string, "ok" | "failed" | "timeout"> | null;
|
lastPublishResults: Record<string, "ok" | "failed" | "timeout"> | null;
|
||||||
}>;
|
}>;
|
||||||
|
/** Send typing indicator start (kind 20001) */
|
||||||
|
sendTypingStart: (toPubkey: string, conversationEventId?: string) => Promise<void>;
|
||||||
|
/** Send typing indicator stop (kind 20001) */
|
||||||
|
sendTypingStop: (toPubkey: string, conversationEventId?: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -496,7 +506,7 @@ export async function startNostrBus(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Call the message handler
|
// Call the message handler
|
||||||
await onMessage(event.pubkey, plaintext, replyTo);
|
await onMessage(event.pubkey, plaintext, replyTo, event.id);
|
||||||
|
|
||||||
// Mark as processed
|
// Mark as processed
|
||||||
metrics.emit("event.processed");
|
metrics.emit("event.processed");
|
||||||
@ -512,26 +522,21 @@ export async function startNostrBus(
|
|||||||
|
|
||||||
const sub = pool.subscribeMany(
|
const sub = pool.subscribeMany(
|
||||||
relays,
|
relays,
|
||||||
[{ kinds: [4], "#p": [pk], since }],
|
{ kinds: [4], "#p": [pk], since },
|
||||||
{
|
{
|
||||||
onevent: handleEvent,
|
onevent: handleEvent,
|
||||||
oneose: () => {
|
oneose: () => {
|
||||||
// EOSE handler - called when all stored events have been received
|
|
||||||
for (const relay of relays) {
|
for (const relay of relays) {
|
||||||
metrics.emit("relay.message.eose", 1, { relay });
|
metrics.emit("relay.message.eose", 1, { relay });
|
||||||
}
|
}
|
||||||
onEose?.(relays.join(", "));
|
onEose?.(relays.join(", "));
|
||||||
},
|
},
|
||||||
onclose: (reason) => {
|
onclose: (reason) => {
|
||||||
// Handle subscription close
|
|
||||||
for (const relay of relays) {
|
for (const relay of relays) {
|
||||||
metrics.emit("relay.message.closed", 1, { relay });
|
metrics.emit("relay.message.closed", 1, { relay });
|
||||||
options.onDisconnect?.(relay);
|
options.onDisconnect?.(relay);
|
||||||
}
|
}
|
||||||
onError?.(
|
onError?.(new Error(`Subscription closed: ${reason}`), "subscription");
|
||||||
new Error(`Subscription closed: ${reason}`),
|
|
||||||
"subscription"
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -590,6 +595,17 @@ export async function startNostrBus(
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Create typing controller for throttled typing indicators
|
||||||
|
const typingController = createTypingController(
|
||||||
|
pool,
|
||||||
|
sk,
|
||||||
|
relays,
|
||||||
|
metrics,
|
||||||
|
circuitBreakers,
|
||||||
|
healthTracker,
|
||||||
|
onError
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
close: () => {
|
close: () => {
|
||||||
sub.close();
|
sub.close();
|
||||||
@ -610,6 +626,8 @@ export async function startNostrBus(
|
|||||||
getMetrics: () => metrics.getSnapshot(),
|
getMetrics: () => metrics.getSnapshot(),
|
||||||
publishProfile,
|
publishProfile,
|
||||||
getProfileState,
|
getProfileState,
|
||||||
|
sendTypingStart: typingController.sendTypingStart,
|
||||||
|
sendTypingStop: typingController.sendTypingStop,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -646,6 +664,7 @@ async function sendEncryptedDm(
|
|||||||
const sortedRelays = healthTracker.getSortedRelays(relays);
|
const sortedRelays = healthTracker.getSortedRelays(relays);
|
||||||
|
|
||||||
// Try relays in order of health, respecting circuit breakers
|
// Try relays in order of health, respecting circuit breakers
|
||||||
|
let successCount = 0;
|
||||||
let lastError: Error | undefined;
|
let lastError: Error | undefined;
|
||||||
for (const relay of sortedRelays) {
|
for (const relay of sortedRelays) {
|
||||||
const cb = circuitBreakers.get(relay);
|
const cb = circuitBreakers.get(relay);
|
||||||
@ -663,7 +682,7 @@ async function sendEncryptedDm(
|
|||||||
// Record success
|
// Record success
|
||||||
cb?.recordSuccess();
|
cb?.recordSuccess();
|
||||||
healthTracker.recordSuccess(relay, latency);
|
healthTracker.recordSuccess(relay, latency);
|
||||||
|
successCount++;
|
||||||
return; // Success - exit early
|
return; // Success - exit early
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
lastError = err as Error;
|
lastError = err as Error;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user