@secondlayer/subgraphs 3.26.0 → 3.26.2

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.
@@ -348,6 +348,39 @@ interface SubgraphDefinition {
348
348
  /** Run `fn` (a single block's write, or a reorg's whole rewind) holding the
349
349
  * per-subgraph block lock. Exported for reorg.ts. */
350
350
  declare function withSubgraphBlockLock<T>(name: string, fn: () => Promise<T>): Promise<T>;
351
+ /**
352
+ * Cross-process half of the block lock: a transaction-scoped Postgres advisory
353
+ * lock keyed `subgraph-block:<name>`, held for the duration of `fn`.
354
+ *
355
+ * Held, not merely acquired: each block's own write transaction is opened
356
+ * INSIDE `processBlock` (block-processor.ts — `targetDb.transaction()` for
357
+ * managed subgraphs, `route.dataDb.transaction()` for BYO), and that boundary
358
+ * is load-bearing for the f069 replay guard, so the lock cannot be taken on
359
+ * the writing connection. Instead this wraps the write in a separate
360
+ * lock-holding transaction on the target DB, exactly as the reorg lock does
361
+ * (reorg.ts): `pg_advisory_xact_lock` releases only when THIS transaction
362
+ * commits, regardless of which other connections `fn` awaits meanwhile.
363
+ * Transaction-scoped rather than session-scoped so a crashed walk can never
364
+ * strand the lock.
365
+ *
366
+ * The lock lives on the target DB even for BYO subgraphs — same as the reorg
367
+ * lock — because it coordinates control-plane walkers, not data-plane rows.
368
+ *
369
+ * Connection budget: this holds one pooled target-DB connection while `fn`
370
+ * takes another for the write, so an in-flight block costs 2 of
371
+ * `DATABASE_POOL_MAX` (default 20). Concurrent block writes are bounded by
372
+ * catch-up fan-out (5 subgraphs) plus operation concurrency (1) = 6 blocks =
373
+ * 12 connections. Raising either concurrency knob must be checked against the
374
+ * pool, or the wrapping transactions can starve the writes they wrap.
375
+ */
376
+ declare function withSubgraphBlockAdvisoryLock<T>(name: string, fn: () => Promise<T>): Promise<T>;
377
+ /**
378
+ * One block's write, holding BOTH halves of the block lock in the documented
379
+ * order: the in-process mutex, then the cross-process advisory lock. Every
380
+ * walk that writes a subgraph's schema — live catch-up, reindex, backfill —
381
+ * must go through this, or it serializes against nothing.
382
+ */
383
+ declare function withSubgraphBlockWriteLock<T>(name: string, fn: () => Promise<T>): Promise<T>;
351
384
  /** Bump a subgraph's reorg epoch. Call BEFORE acquiring the block lock so the
352
385
  * bump is visible to any concurrent catch-up immediately. Exported for
353
386
  * reorg.ts. */
@@ -371,4 +404,4 @@ declare function resolveCatchupBatchConfig(env?: CatchupBatchEnv): CatchupBatchC
371
404
  * plan-aware pipeline prefetching.
372
405
  */
373
406
  declare function catchUpSubgraph(subgraph: SubgraphDefinition, subgraphName: string): Promise<number>;
374
- export { withSubgraphBlockLock, resolveCatchupBatchConfig, catchUpSubgraph, bumpReorgEpoch };
407
+ export { withSubgraphBlockWriteLock, withSubgraphBlockLock, withSubgraphBlockAdvisoryLock, resolveCatchupBatchConfig, catchUpSubgraph, bumpReorgEpoch };
@@ -605,14 +605,10 @@ class SubgraphContext {
605
605
  this.ops.length = 0;
606
606
  const statements = this.buildStatements(opsToFlush);
607
607
  if ("isTransaction" in this.db) {
608
- for (const stmt of statements) {
609
- await sql.raw(stmt).execute(this.db);
610
- }
608
+ await this.execStatements(this.db, statements);
611
609
  } else {
612
610
  await this.db.transaction().execute(async (tx) => {
613
- for (const stmt of statements) {
614
- await sql.raw(stmt).execute(tx);
615
- }
611
+ await this.execStatements(tx, statements);
616
612
  });
617
613
  }
618
614
  const writes = opsToFlush.map((op, rowIndex) => {
@@ -631,6 +627,25 @@ class SubgraphContext {
631
627
  });
632
628
  return { count: opsToFlush.length, writes };
633
629
  }
630
+ async execStatements(executor, statements) {
631
+ const affected = (r) => r.numAffectedRows ?? 0n;
632
+ for (const stmt of statements) {
633
+ if (typeof stmt === "string") {
634
+ await sql.raw(stmt).execute(executor);
635
+ continue;
636
+ }
637
+ const updated = await sql.raw(stmt.update).execute(executor);
638
+ if (affected(updated) > 0n)
639
+ continue;
640
+ const inserted = await sql.raw(stmt.insert).execute(executor);
641
+ if (affected(inserted) > 0n)
642
+ continue;
643
+ const retried = await sql.raw(stmt.update).execute(executor);
644
+ if (affected(retried) === 0n) {
645
+ throw new Error(`increment could not be applied to ${this.pgSchemaName}.${stmt.table} where ${stmt.keyPred}: row neither updated nor inserted`);
646
+ }
647
+ }
648
+ }
634
649
  prepareInsert(op) {
635
650
  const upsertKeys = op.data._upsert_keys;
636
651
  const data = { ...op.data };
@@ -710,7 +725,6 @@ class SubgraphContext {
710
725
  for (const r of batch.rows.values()) {
711
726
  const keyPred = batch.keyCols.map((k) => `"${k}" = ${escapeLiteral(r.keys[k])}`).join(" AND ");
712
727
  const updSet = batch.deltaCols.map((c) => `"${c}" = COALESCE("${c}", 0) + (${String(r.deltas[c] ?? 0n)})`).join(", ");
713
- statements.push(`UPDATE ${qualifiedTable} SET ${updSet} WHERE ${keyPred}`);
714
728
  const insertVals = [
715
729
  ...batch.keyCols.map((k) => escapeLiteral(r.keys[k])),
716
730
  ...batch.deltaCols.map((c) => String(r.deltas[c] ?? 0n)),
@@ -718,7 +732,13 @@ class SubgraphContext {
718
732
  escapeLiteral(r.meta.txId),
719
733
  "NOW()"
720
734
  ];
721
- statements.push(`INSERT INTO ${qualifiedTable} (${cols.map((c) => `"${c}"`).join(", ")}) ` + `SELECT ${insertVals.join(", ")} ` + `WHERE NOT EXISTS (SELECT 1 FROM ${qualifiedTable} WHERE ${keyPred})`);
735
+ statements.push({
736
+ kind: "increment",
737
+ table: batch.table,
738
+ keyPred,
739
+ update: `UPDATE ${qualifiedTable} SET ${updSet} WHERE ${keyPred}`,
740
+ insert: `INSERT INTO ${qualifiedTable} (${cols.map((c) => `"${c}"`).join(", ")}) ` + `SELECT ${insertVals.join(", ")} ` + `WHERE NOT EXISTS (SELECT 1 FROM ${qualifiedTable} WHERE ${keyPred})`
741
+ });
722
742
  }
723
743
  incBatch = null;
724
744
  incBatchKey = "";
@@ -3025,7 +3045,7 @@ class StatsAccumulator {
3025
3045
  }
3026
3046
 
3027
3047
  // src/runtime/catchup.ts
3028
- import { getTargetDb as getTargetDb2 } from "@secondlayer/shared/db";
3048
+ import { getTargetDb as getTargetDb2, sql as sql4 } from "@secondlayer/shared/db";
3029
3049
  import { getSubgraph } from "@secondlayer/shared/db/queries/subgraphs";
3030
3050
  import { logger as logger8 } from "@secondlayer/shared/logger";
3031
3051
 
@@ -3096,6 +3116,15 @@ async function withSubgraphBlockLock(name, fn) {
3096
3116
  release();
3097
3117
  }
3098
3118
  }
3119
+ async function withSubgraphBlockAdvisoryLock(name, fn) {
3120
+ return getTargetDb2().transaction().execute(async (lockTx) => {
3121
+ await sql4`SELECT pg_advisory_xact_lock(hashtext(${`subgraph-block:${name}`}))`.execute(lockTx);
3122
+ return fn();
3123
+ });
3124
+ }
3125
+ async function withSubgraphBlockWriteLock(name, fn) {
3126
+ return withSubgraphBlockLock(name, () => withSubgraphBlockAdvisoryLock(name, fn));
3127
+ }
3099
3128
  function bumpReorgEpoch(name) {
3100
3129
  reorgEpoch.set(name, (reorgEpoch.get(name) ?? 0) + 1);
3101
3130
  }
@@ -3224,7 +3253,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
3224
3253
  const preloaded = blockData;
3225
3254
  let result;
3226
3255
  try {
3227
- result = await withSubgraphBlockLock(subgraphName, async () => {
3256
+ result = await withSubgraphBlockWriteLock(subgraphName, async () => {
3228
3257
  if (getReorgEpoch(subgraphName) !== walkEpoch) {
3229
3258
  throw new ReorgEpochChangedError(subgraphName);
3230
3259
  }
@@ -3284,11 +3313,13 @@ async function catchUpSubgraph(subgraph, subgraphName) {
3284
3313
  }
3285
3314
  }
3286
3315
  export {
3316
+ withSubgraphBlockWriteLock,
3287
3317
  withSubgraphBlockLock,
3318
+ withSubgraphBlockAdvisoryLock,
3288
3319
  resolveCatchupBatchConfig,
3289
3320
  catchUpSubgraph,
3290
3321
  bumpReorgEpoch
3291
3322
  };
3292
3323
 
3293
- //# debugId=8CB91249FBEDCD6B64756E2164756E21
3324
+ //# debugId=376967D5C07D633664756E2164756E21
3294
3325
  //# sourceMappingURL=catchup.js.map