Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/handbook/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,7 @@
## Function: indexZapReceipt

- **Purpose:** Validate provider pubkey (case-insensitive hex) and add sats once per receipt id. Callers verify the Nostr signature first. Persists a `nostr_zap_ingest` row (`indexed`, or `rejected` with reason `pubkey` / `amount` / `duplicate`); store throw logs `nostr.zap.ingest.record_failed` and does not change the boolean result.
- **Ingest dedupe:** One `nostr_zap_ingest` row is written per receipt per decision change per process (memory is per store instance and empty after a restart, so the first tick after boot may write one `rejected`/`duplicate` row per known receipt). A repeated identical `outcome:reason` is not written again. Receipts whose remembered decision is already terminal never reach this function: `indexOpenZapReceipts` skips them before validation.
- **Ingest dedupe:** One `nostr_zap_ingest` row is written per receipt per decision change per process (memory is per store instance and empty after a restart, so the first tick after boot may write one `rejected`/`duplicate` row per known receipt). A repeated identical `outcome:reason` is normally not written again, because the memory is consulted before the write; that is not a guarantee, since the memory is set only after the write resolves, worker ticks are not serialised, and a failed write leaves it untouched. Receipts whose remembered decision is already terminal never reach this function: `indexOpenZapReceipts` skips them before validation.
- **Inputs:** store, messageId, receipt, providerPubkey, amountSats; optional receiptEvent / noteEventId for debug rows.
- **Returns / side effects:** boolean; logs indexed/rejected; records ingest.
- **Used by:** `indexOpenZapReceipts` (worker tick).
Expand Down
2 changes: 1 addition & 1 deletion src/lib/conversation-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export interface ConversationStore {
updatePublishState(id: string, state: NostrPublishState): Promise<void>;
}

/** Idempotent SQL for conversation tables (DDL plus boot-time unwrap of `nostr_event` values stored as jsonb string scalars in `conversation_message`; matches `docs/schema/conversation.sql`). */
/** Idempotent SQL for conversation tables (DDL plus boot-time unwrap of `nostr_event` values stored as jsonb string scalars in `conversation_message`; `docs/schema/conversation.sql` mirrors the DDL and documents the boot repair statement by comment, the `DO $unwrap$` block lives only in this array). */
export const CONVERSATION_SCHEMA_SQL: readonly string[] = [
`CREATE TABLE IF NOT EXISTS conversation (
id uuid PRIMARY KEY,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/db-change.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import type { SqlClient } from '@/lib/auth/sql';

/** Idempotent SQL for the append-only change log (DDL plus one-time live `view_key` rewrite; matches `docs/schema/db_change.sql`). */
/** Idempotent SQL for the append-only change log (DDL, the `log_db_change` trigger body that on UPDATE stores an unchanged and unredacted bytea column as a `{unchanged, sha256, bytes}` reference instead of the full value, and the one-time live `view_key` rewrite; matches `docs/schema/db_change.sql`). */
export const DB_CHANGE_SCHEMA_SQL: readonly string[] = [
`CREATE EXTENSION IF NOT EXISTS pgcrypto;`,
`CREATE TABLE IF NOT EXISTS db_change (
Expand Down
2 changes: 1 addition & 1 deletion src/lib/message-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ export interface ZapIngestRow {
receipt: Record<string, unknown>;
}

/** Idempotent SQL for the forum table (DDL plus boot-time unwrap of `nostr_event` values stored as jsonb string scalars; matches `docs/schema/message.sql`). */
/** Idempotent SQL for the forum table (DDL plus boot-time unwrap of `nostr_event` values stored as jsonb string scalars; `docs/schema/message.sql` mirrors the DDL and documents the boot repair statement by comment, the `DO $unwrap$` block lives only in this array). */
export const MESSAGE_SCHEMA_SQL: readonly string[] = [
`CREATE TABLE IF NOT EXISTS message (
id uuid PRIMARY KEY,
Expand Down
35 changes: 32 additions & 3 deletions src/lib/nostr/zap-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ const providerPubkeyCache = new Map<string, ProviderCacheRow>();

/**
* Last persisted ingest `outcome:reason` per receipt id, keyed by message store.
* Empty after process restart; first tick may rewrite one row per known receipt.
* Empty after process restart; the first tick may then re-persist a forgotten
* decision, but only for the receipts that tick still queries. A receipt whose
* message has aged out of `listLatest` is never asked for again.
*
* Note the asymmetry with `MessageStore.deleteById`: both store adapters forget
* the receipt id when the message goes away and would record it again, but this
* map does not, so a terminal decision here keeps suppressing that write until
* the process restarts.
*/
const zapDecisions = new WeakMap<MessageStore, Map<string, string>>();

Expand Down Expand Up @@ -107,8 +114,9 @@ function receiptFrame(event: NostrEventFrame): Record<string, unknown> {

/**
* Persist an ingest decision without failing the tick.
* Skips the write when this process already persisted the same outcome:reason
* for the receipt id on this store instance.
* Skips the write when the memory already holds the same outcome:reason for the
* receipt id on this store instance. The memory is set only after the write
* resolves, so two overlapping ticks can both pass this check.
*
* @param store - Forum store.
* @param row - Ingest row.
Expand Down Expand Up @@ -162,6 +170,13 @@ function zapIngestRow(args: {
* The provider pubkey check is case-insensitive hex. Callers must already
* have verified the Nostr signature (`verifyEvent`).
*
* A repeated identical `outcome:reason` is normally not written again, because
* the memory is consulted before the write. That is not a guarantee: the memory
* is set only after the write resolves, worker ticks are not serialised, and a
* failed write leaves the memory untouched, so two overlapping ticks or a retry
* can still produce a second identical row. A later, different decision for that
* receipt always writes another ingest row.
*
* @param store - Forum store.
* @param messageId - Forum row id.
* @param receipt - Kind 9735.
Expand Down Expand Up @@ -264,6 +279,15 @@ export async function indexZapReceipt(args: {
* Query zap relays for kind:9735 receipts on recent forum notes and index
* validated ones.
*
* Receipts whose terminal decision this process already persisted are skipped,
* that is `indexed`, or `rejected` with reason `duplicate`. Every other
* rejection reason is re-validated on each tick and writes again whenever the
* decision changes, so a steady state writes no ingest rows only while the
* decisions themselves are stable. The memory is process-local, so the first
* tick after a restart may re-persist decisions it has forgotten, bounded by the
* receipts that tick queries at all. Ticks are not serialised, so the bound is
* per tick, not per receipt across concurrent ticks.
*
* @param args - Store, auth, querier, relay urls, timeout, clock, fetch.
* @returns Resolves when the tick's ingest pass finishes.
*/
Expand Down Expand Up @@ -337,6 +361,11 @@ export async function indexOpenZapReceipts(args: {
/**
* Validate and index one candidate receipt event.
*
* Returns right after id validation when this process already persisted a
* terminal decision for the receipt id on this store instance, that is
* `indexed`, or `rejected` with reason `duplicate`. Every other rejection
* reason is re-validated on each call.
*
* @param event - Queried frame.
* @param args - Ingest collaborators.
*/
Expand Down
Loading