@open-mercato/shared 0.6.6-develop.5588.1.a8f6c51d1f → 0.6.6-develop.5594.1.30cd738303

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.
@@ -29,6 +29,10 @@ function pickDb(deps) {
29
29
  }
30
30
  return null;
31
31
  }
32
+ function isInactiveTransactionError(error) {
33
+ const message = error instanceof Error ? error.message : String(error ?? "");
34
+ return /transaction is already (committed|rolled back)/i.test(message);
35
+ }
32
36
  async function pruneExcessLogs(db, source) {
33
37
  const rows = await db.selectFrom("indexer_status_logs").select("id").where("source", "=", source).orderBy("occurred_at", "desc").orderBy("id", "desc").offset(MAX_LOGS_PER_SOURCE).limit(MAX_DELETE_BATCH).execute();
34
38
  if (!rows.length) return;
@@ -63,13 +67,17 @@ async function recordIndexerLog(deps, input) {
63
67
  occurred_at: occurredAt
64
68
  }).execute();
65
69
  } catch (error) {
66
- console.error("[indexers] Failed to persist indexer log", error);
70
+ if (!isInactiveTransactionError(error)) {
71
+ console.error("[indexers] Failed to persist indexer log", error);
72
+ }
67
73
  return;
68
74
  }
69
75
  try {
70
76
  await pruneExcessLogs(db, input.source);
71
77
  } catch (pruneError) {
72
- console.warn("[indexers] Failed to prune indexer logs", pruneError);
78
+ if (!isInactiveTransactionError(pruneError)) {
79
+ console.warn("[indexers] Failed to prune indexer logs", pruneError);
80
+ }
73
81
  }
74
82
  }
75
83
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/indexers/status-log.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport type { IndexerErrorSource } from './error-log'\n\nexport type IndexerLogLevel = 'info' | 'warn'\n\nexport type RecordIndexerLogInput = {\n source: IndexerErrorSource\n handler: string\n message: string\n level?: IndexerLogLevel\n entityType?: string | null\n recordId?: string | null\n tenantId?: string | null\n organizationId?: string | null\n details?: unknown\n}\n\ntype RecordIndexerLogDeps = {\n em?: EntityManager\n db?: Kysely<any>\n}\n\nconst MAX_MESSAGE_LENGTH = 4_096\nconst MAX_DELETE_BATCH = 5_000\nconst MAX_LOGS_PER_SOURCE = 10_000\n\nfunction truncate(input: string | null | undefined, limit: number): string | null {\n if (!input) return null\n return input.length > limit ? `${input.slice(0, limit - 3)}...` : input\n}\n\nfunction safeJson(value: unknown): unknown {\n if (value === undefined) return null\n try {\n return JSON.parse(JSON.stringify(value))\n } catch {\n if (value == null) return null\n if (typeof value === 'object') {\n return { note: 'unserializable', asString: String(value) }\n }\n return value\n }\n}\n\nfunction pickDb(deps: RecordIndexerLogDeps): Kysely<any> | null {\n if (deps.db) return deps.db\n if (deps.em) {\n try {\n return deps.em.getKysely<any>()\n } catch {\n return null\n }\n }\n return null\n}\n\nasync function pruneExcessLogs(db: Kysely<any>, source: IndexerErrorSource): Promise<void> {\n const rows = await db\n .selectFrom('indexer_status_logs' as any)\n .select('id' as any)\n .where('source' as any, '=', source)\n .orderBy('occurred_at' as any, 'desc')\n .orderBy('id' as any, 'desc')\n .offset(MAX_LOGS_PER_SOURCE)\n .limit(MAX_DELETE_BATCH)\n .execute()\n\n if (!rows.length) return\n const ids = rows.map((row: any) => row.id).filter(Boolean)\n if (!ids.length) return\n await db\n .deleteFrom('indexer_status_logs' as any)\n .where('id' as any, 'in', ids)\n .execute()\n}\n\nexport async function recordIndexerLog(\n deps: RecordIndexerLogDeps,\n input: RecordIndexerLogInput,\n): Promise<void> {\n const db = pickDb(deps)\n if (!db) {\n console.warn('[indexers] Unable to record indexer log (missing db connection)', {\n source: input.source,\n handler: input.handler,\n })\n return\n }\n\n const level: IndexerLogLevel = input.level === 'warn' ? 'warn' : 'info'\n const message = truncate(input.message, MAX_MESSAGE_LENGTH) ?? '\u2014'\n const details = safeJson(input.details)\n const occurredAt = new Date()\n\n try {\n await db\n .insertInto('indexer_status_logs' as any)\n .values({\n source: input.source,\n handler: input.handler,\n level,\n entity_type: input.entityType ?? null,\n record_id: input.recordId ?? null,\n tenant_id: input.tenantId ?? null,\n organization_id: input.organizationId ?? null,\n message,\n details: details === null ? null : sql`${JSON.stringify(details)}::jsonb`,\n occurred_at: occurredAt,\n } as any)\n .execute()\n } catch (error) {\n console.error('[indexers] Failed to persist indexer log', error)\n return\n }\n\n try {\n await pruneExcessLogs(db, input.source)\n } catch (pruneError) {\n console.warn('[indexers] Failed to prune indexer logs', pruneError)\n }\n}\n"],
5
- "mappings": "AACA,SAAsB,WAAW;AAsBjC,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,sBAAsB;AAE5B,SAAS,SAAS,OAAkC,OAA8B;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,SAAS,QAAQ,GAAG,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,QAAQ;AACpE;AAEA,SAAS,SAAS,OAAyB;AACzC,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI;AACF,WAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AAAA,EACzC,QAAQ;AACN,QAAI,SAAS,KAAM,QAAO;AAC1B,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,EAAE,MAAM,kBAAkB,UAAU,OAAO,KAAK,EAAE;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAO,MAAgD;AAC9D,MAAI,KAAK,GAAI,QAAO,KAAK;AACzB,MAAI,KAAK,IAAI;AACX,QAAI;AACF,aAAO,KAAK,GAAG,UAAe;AAAA,IAChC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,gBAAgB,IAAiB,QAA2C;AACzF,QAAM,OAAO,MAAM,GAChB,WAAW,qBAA4B,EACvC,OAAO,IAAW,EAClB,MAAM,UAAiB,KAAK,MAAM,EAClC,QAAQ,eAAsB,MAAM,EACpC,QAAQ,MAAa,MAAM,EAC3B,OAAO,mBAAmB,EAC1B,MAAM,gBAAgB,EACtB,QAAQ;AAEX,MAAI,CAAC,KAAK,OAAQ;AAClB,QAAM,MAAM,KAAK,IAAI,CAAC,QAAa,IAAI,EAAE,EAAE,OAAO,OAAO;AACzD,MAAI,CAAC,IAAI,OAAQ;AACjB,QAAM,GACH,WAAW,qBAA4B,EACvC,MAAM,MAAa,MAAM,GAAG,EAC5B,QAAQ;AACb;AAEA,eAAsB,iBACpB,MACA,OACe;AACf,QAAM,KAAK,OAAO,IAAI;AACtB,MAAI,CAAC,IAAI;AACP,YAAQ,KAAK,mEAAmE;AAAA,MAC9E,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,IACjB,CAAC;AACD;AAAA,EACF;AAEA,QAAM,QAAyB,MAAM,UAAU,SAAS,SAAS;AACjE,QAAM,UAAU,SAAS,MAAM,SAAS,kBAAkB,KAAK;AAC/D,QAAM,UAAU,SAAS,MAAM,OAAO;AACtC,QAAM,aAAa,oBAAI,KAAK;AAE5B,MAAI;AACF,UAAM,GACH,WAAW,qBAA4B,EACvC,OAAO;AAAA,MACN,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf;AAAA,MACA,aAAa,MAAM,cAAc;AAAA,MACjC,WAAW,MAAM,YAAY;AAAA,MAC7B,WAAW,MAAM,YAAY;AAAA,MAC7B,iBAAiB,MAAM,kBAAkB;AAAA,MACzC;AAAA,MACA,SAAS,YAAY,OAAO,OAAO,MAAM,KAAK,UAAU,OAAO,CAAC;AAAA,MAChE,aAAa;AAAA,IACf,CAAQ,EACP,QAAQ;AAAA,EACb,SAAS,OAAO;AACd,YAAQ,MAAM,4CAA4C,KAAK;AAC/D;AAAA,EACF;AAEA,MAAI;AACF,UAAM,gBAAgB,IAAI,MAAM,MAAM;AAAA,EACxC,SAAS,YAAY;AACnB,YAAQ,KAAK,2CAA2C,UAAU;AAAA,EACpE;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport type { IndexerErrorSource } from './error-log'\n\nexport type IndexerLogLevel = 'info' | 'warn'\n\nexport type RecordIndexerLogInput = {\n source: IndexerErrorSource\n handler: string\n message: string\n level?: IndexerLogLevel\n entityType?: string | null\n recordId?: string | null\n tenantId?: string | null\n organizationId?: string | null\n details?: unknown\n}\n\ntype RecordIndexerLogDeps = {\n em?: EntityManager\n db?: Kysely<any>\n}\n\nconst MAX_MESSAGE_LENGTH = 4_096\nconst MAX_DELETE_BATCH = 5_000\nconst MAX_LOGS_PER_SOURCE = 10_000\n\nfunction truncate(input: string | null | undefined, limit: number): string | null {\n if (!input) return null\n return input.length > limit ? `${input.slice(0, limit - 3)}...` : input\n}\n\nfunction safeJson(value: unknown): unknown {\n if (value === undefined) return null\n try {\n return JSON.parse(JSON.stringify(value))\n } catch {\n if (value == null) return null\n if (typeof value === 'object') {\n return { note: 'unserializable', asString: String(value) }\n }\n return value\n }\n}\n\nfunction pickDb(deps: RecordIndexerLogDeps): Kysely<any> | null {\n if (deps.db) return deps.db\n if (deps.em) {\n try {\n return deps.em.getKysely<any>()\n } catch {\n return null\n }\n }\n return null\n}\n\n/**\n * Indexer status logging is best-effort observability and must never ride on \u2014\n * or be noisy about \u2014 the caller's transaction lifecycle. When index maintenance\n * runs inline on a request `em` (e.g. an inline force reindex that emits the\n * vector-purge subscriber), the captured Kysely can be a transaction handle that\n * has already committed/rolled back, so a follow-up read/write throws\n * \"Transaction is already committed\". Treat that class of error as a quiet skip\n * rather than an error the operator must triage. A fresh-EM path (the events\n * worker) never hits this; this guard only de-noises the inline path.\n */\nfunction isInactiveTransactionError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error ?? '')\n return /transaction is already (committed|rolled back)/i.test(message)\n}\n\nasync function pruneExcessLogs(db: Kysely<any>, source: IndexerErrorSource): Promise<void> {\n const rows = await db\n .selectFrom('indexer_status_logs' as any)\n .select('id' as any)\n .where('source' as any, '=', source)\n .orderBy('occurred_at' as any, 'desc')\n .orderBy('id' as any, 'desc')\n .offset(MAX_LOGS_PER_SOURCE)\n .limit(MAX_DELETE_BATCH)\n .execute()\n\n if (!rows.length) return\n const ids = rows.map((row: any) => row.id).filter(Boolean)\n if (!ids.length) return\n await db\n .deleteFrom('indexer_status_logs' as any)\n .where('id' as any, 'in', ids)\n .execute()\n}\n\nexport async function recordIndexerLog(\n deps: RecordIndexerLogDeps,\n input: RecordIndexerLogInput,\n): Promise<void> {\n const db = pickDb(deps)\n if (!db) {\n console.warn('[indexers] Unable to record indexer log (missing db connection)', {\n source: input.source,\n handler: input.handler,\n })\n return\n }\n\n const level: IndexerLogLevel = input.level === 'warn' ? 'warn' : 'info'\n const message = truncate(input.message, MAX_MESSAGE_LENGTH) ?? '\u2014'\n const details = safeJson(input.details)\n const occurredAt = new Date()\n\n try {\n await db\n .insertInto('indexer_status_logs' as any)\n .values({\n source: input.source,\n handler: input.handler,\n level,\n entity_type: input.entityType ?? null,\n record_id: input.recordId ?? null,\n tenant_id: input.tenantId ?? null,\n organization_id: input.organizationId ?? null,\n message,\n details: details === null ? null : sql`${JSON.stringify(details)}::jsonb`,\n occurred_at: occurredAt,\n } as any)\n .execute()\n } catch (error) {\n // A committed/rolled-back caller transaction is an expected, harmless\n // condition for best-effort logging \u2014 skip quietly instead of surfacing it.\n if (!isInactiveTransactionError(error)) {\n console.error('[indexers] Failed to persist indexer log', error)\n }\n return\n }\n\n try {\n await pruneExcessLogs(db, input.source)\n } catch (pruneError) {\n if (!isInactiveTransactionError(pruneError)) {\n console.warn('[indexers] Failed to prune indexer logs', pruneError)\n }\n }\n}\n"],
5
+ "mappings": "AACA,SAAsB,WAAW;AAsBjC,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AACzB,MAAM,sBAAsB;AAE5B,SAAS,SAAS,OAAkC,OAA8B;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,SAAS,QAAQ,GAAG,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,QAAQ;AACpE;AAEA,SAAS,SAAS,OAAyB;AACzC,MAAI,UAAU,OAAW,QAAO;AAChC,MAAI;AACF,WAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AAAA,EACzC,QAAQ;AACN,QAAI,SAAS,KAAM,QAAO;AAC1B,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,EAAE,MAAM,kBAAkB,UAAU,OAAO,KAAK,EAAE;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAO,MAAgD;AAC9D,MAAI,KAAK,GAAI,QAAO,KAAK;AACzB,MAAI,KAAK,IAAI;AACX,QAAI;AACF,aAAO,KAAK,GAAG,UAAe;AAAA,IAChC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAYA,SAAS,2BAA2B,OAAyB;AAC3D,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,SAAS,EAAE;AAC3E,SAAO,kDAAkD,KAAK,OAAO;AACvE;AAEA,eAAe,gBAAgB,IAAiB,QAA2C;AACzF,QAAM,OAAO,MAAM,GAChB,WAAW,qBAA4B,EACvC,OAAO,IAAW,EAClB,MAAM,UAAiB,KAAK,MAAM,EAClC,QAAQ,eAAsB,MAAM,EACpC,QAAQ,MAAa,MAAM,EAC3B,OAAO,mBAAmB,EAC1B,MAAM,gBAAgB,EACtB,QAAQ;AAEX,MAAI,CAAC,KAAK,OAAQ;AAClB,QAAM,MAAM,KAAK,IAAI,CAAC,QAAa,IAAI,EAAE,EAAE,OAAO,OAAO;AACzD,MAAI,CAAC,IAAI,OAAQ;AACjB,QAAM,GACH,WAAW,qBAA4B,EACvC,MAAM,MAAa,MAAM,GAAG,EAC5B,QAAQ;AACb;AAEA,eAAsB,iBACpB,MACA,OACe;AACf,QAAM,KAAK,OAAO,IAAI;AACtB,MAAI,CAAC,IAAI;AACP,YAAQ,KAAK,mEAAmE;AAAA,MAC9E,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,IACjB,CAAC;AACD;AAAA,EACF;AAEA,QAAM,QAAyB,MAAM,UAAU,SAAS,SAAS;AACjE,QAAM,UAAU,SAAS,MAAM,SAAS,kBAAkB,KAAK;AAC/D,QAAM,UAAU,SAAS,MAAM,OAAO;AACtC,QAAM,aAAa,oBAAI,KAAK;AAE5B,MAAI;AACF,UAAM,GACH,WAAW,qBAA4B,EACvC,OAAO;AAAA,MACN,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf;AAAA,MACA,aAAa,MAAM,cAAc;AAAA,MACjC,WAAW,MAAM,YAAY;AAAA,MAC7B,WAAW,MAAM,YAAY;AAAA,MAC7B,iBAAiB,MAAM,kBAAkB;AAAA,MACzC;AAAA,MACA,SAAS,YAAY,OAAO,OAAO,MAAM,KAAK,UAAU,OAAO,CAAC;AAAA,MAChE,aAAa;AAAA,IACf,CAAQ,EACP,QAAQ;AAAA,EACb,SAAS,OAAO;AAGd,QAAI,CAAC,2BAA2B,KAAK,GAAG;AACtC,cAAQ,MAAM,4CAA4C,KAAK;AAAA,IACjE;AACA;AAAA,EACF;AAEA,MAAI;AACF,UAAM,gBAAgB,IAAI,MAAM,MAAM;AAAA,EACxC,SAAS,YAAY;AACnB,QAAI,CAAC,2BAA2B,UAAU,GAAG;AAC3C,cAAQ,KAAK,2CAA2C,UAAU;AAAA,IACpE;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.6-develop.5588.1.a8f6c51d1f";
1
+ const APP_VERSION = "0.6.6-develop.5594.1.30cd738303";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.6-develop.5588.1.a8f6c51d1f'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.6-develop.5594.1.30cd738303'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.6.6-develop.5588.1.a8f6c51d1f",
3
+ "version": "0.6.6-develop.5594.1.30cd738303",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -92,7 +92,7 @@
92
92
  "@mikro-orm/core": "^7.1.4",
93
93
  "@mikro-orm/decorators": "^7.1.4",
94
94
  "@mikro-orm/postgresql": "^7.1.4",
95
- "@open-mercato/cache": "0.6.6-develop.5588.1.a8f6c51d1f",
95
+ "@open-mercato/cache": "0.6.6-develop.5594.1.30cd738303",
96
96
  "dotenv": "^17.4.2",
97
97
  "rate-limiter-flexible": "^11.2.0",
98
98
  "re2js": "2.8.3",
@@ -0,0 +1,94 @@
1
+ import { recordIndexerLog } from '../status-log'
2
+
3
+ type Behaviors = {
4
+ insert?: () => unknown
5
+ select?: () => unknown
6
+ delete?: () => unknown
7
+ }
8
+
9
+ function makeBuilder(op: keyof Behaviors, behaviors: Behaviors): any {
10
+ const builder: any = {}
11
+ for (const method of ['values', 'select', 'where', 'orderBy', 'offset', 'limit']) {
12
+ builder[method] = () => builder
13
+ }
14
+ builder.execute = async () => {
15
+ const behavior = behaviors[op]
16
+ return behavior ? behavior() : []
17
+ }
18
+ return builder
19
+ }
20
+
21
+ function makeDb(behaviors: Behaviors): any {
22
+ return {
23
+ insertInto: () => makeBuilder('insert', behaviors),
24
+ selectFrom: () => makeBuilder('select', behaviors),
25
+ deleteFrom: () => makeBuilder('delete', behaviors),
26
+ }
27
+ }
28
+
29
+ const INPUT = { source: 'vector' as const, handler: 'event:test', message: 'hi' }
30
+
31
+ describe('recordIndexerLog — inactive-transaction de-noising', () => {
32
+ let errorSpy: jest.SpyInstance
33
+ let warnSpy: jest.SpyInstance
34
+
35
+ beforeEach(() => {
36
+ errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
37
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
38
+ })
39
+
40
+ afterEach(() => {
41
+ errorSpy.mockRestore()
42
+ warnSpy.mockRestore()
43
+ })
44
+
45
+ it('skips quietly when the insert hits an already-committed transaction', async () => {
46
+ const db = makeDb({
47
+ insert: () => {
48
+ throw new Error('Transaction is already committed')
49
+ },
50
+ })
51
+
52
+ await recordIndexerLog({ db }, INPUT)
53
+
54
+ expect(errorSpy).not.toHaveBeenCalled()
55
+ })
56
+
57
+ it('still logs an unexpected insert failure', async () => {
58
+ const db = makeDb({
59
+ insert: () => {
60
+ throw new Error('connection refused')
61
+ },
62
+ })
63
+
64
+ await recordIndexerLog({ db }, INPUT)
65
+
66
+ expect(errorSpy).toHaveBeenCalledTimes(1)
67
+ })
68
+
69
+ it('skips quietly when the prune hits a rolled-back transaction', async () => {
70
+ const db = makeDb({
71
+ insert: () => undefined,
72
+ select: () => {
73
+ throw new Error('Transaction is already rolled back')
74
+ },
75
+ })
76
+
77
+ await recordIndexerLog({ db }, INPUT)
78
+
79
+ expect(warnSpy).not.toHaveBeenCalled()
80
+ })
81
+
82
+ it('still warns on an unexpected prune failure', async () => {
83
+ const db = makeDb({
84
+ insert: () => undefined,
85
+ select: () => {
86
+ throw new Error('deadlock detected')
87
+ },
88
+ })
89
+
90
+ await recordIndexerLog({ db }, INPUT)
91
+
92
+ expect(warnSpy).toHaveBeenCalledTimes(1)
93
+ })
94
+ })
@@ -55,6 +55,21 @@ function pickDb(deps: RecordIndexerLogDeps): Kysely<any> | null {
55
55
  return null
56
56
  }
57
57
 
58
+ /**
59
+ * Indexer status logging is best-effort observability and must never ride on —
60
+ * or be noisy about — the caller's transaction lifecycle. When index maintenance
61
+ * runs inline on a request `em` (e.g. an inline force reindex that emits the
62
+ * vector-purge subscriber), the captured Kysely can be a transaction handle that
63
+ * has already committed/rolled back, so a follow-up read/write throws
64
+ * "Transaction is already committed". Treat that class of error as a quiet skip
65
+ * rather than an error the operator must triage. A fresh-EM path (the events
66
+ * worker) never hits this; this guard only de-noises the inline path.
67
+ */
68
+ function isInactiveTransactionError(error: unknown): boolean {
69
+ const message = error instanceof Error ? error.message : String(error ?? '')
70
+ return /transaction is already (committed|rolled back)/i.test(message)
71
+ }
72
+
58
73
  async function pruneExcessLogs(db: Kysely<any>, source: IndexerErrorSource): Promise<void> {
59
74
  const rows = await db
60
75
  .selectFrom('indexer_status_logs' as any)
@@ -110,13 +125,19 @@ export async function recordIndexerLog(
110
125
  } as any)
111
126
  .execute()
112
127
  } catch (error) {
113
- console.error('[indexers] Failed to persist indexer log', error)
128
+ // A committed/rolled-back caller transaction is an expected, harmless
129
+ // condition for best-effort logging — skip quietly instead of surfacing it.
130
+ if (!isInactiveTransactionError(error)) {
131
+ console.error('[indexers] Failed to persist indexer log', error)
132
+ }
114
133
  return
115
134
  }
116
135
 
117
136
  try {
118
137
  await pruneExcessLogs(db, input.source)
119
138
  } catch (pruneError) {
120
- console.warn('[indexers] Failed to prune indexer logs', pruneError)
139
+ if (!isInactiveTransactionError(pruneError)) {
140
+ console.warn('[indexers] Failed to prune indexer logs', pruneError)
141
+ }
121
142
  }
122
143
  }