@secondlayer/subgraphs 3.10.0 → 3.12.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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Typed on-chain indexing for Stacks. Declare event filters + column schema with `defineSubgraph()`; the runtime decodes blocks, matches filters, runs your handlers inside a transactional context, and exposes the result as a Postgres schema you query over REST or SQL.
4
4
 
5
+ Subgraphs have a visibility: managed deploys default `visibility: public` — anon-readable at `/v1/subgraphs/<name>/<table>`, in a single global public namespace claimed on publish (409 `PUBLIC_NAME_TAKEN` if the name is taken). BYO-database deploys default private. Override with `--visibility public|private` at deploy, or flip later with `sl subgraphs publish|unpublish <name>`.
6
+
5
7
  Subgraph rows fan out to HTTP subscribers through a post-flush outbox emitter — signed Standard Webhooks POSTs with retries, circuit breaker, and replay.
6
8
 
7
9
  ## Install
@@ -114,6 +116,9 @@ reads from there. Deploy with a connection string:
114
116
  sl subgraphs deploy subgraphs/my.ts --database-url "postgres://user:pass@your-host:5432/db"
115
117
  ```
116
118
 
119
+ BYO deploys default `visibility: private` (managed deploys default public);
120
+ pass `--visibility public` or `sl subgraphs publish <name>` to open reads.
121
+
117
122
  The connection string is stored encrypted at rest (AES-GCM, keyed by
118
123
  `SECONDLAYER_SECRETS_KEY`) and never returned in API responses. The server
119
124
  verifies the connection before deploying. Once deployed, query the
@@ -237,6 +237,9 @@ interface SubgraphDefinition {
237
237
  description?: string;
238
238
  /** Block height to start indexing from (default: 1) */
239
239
  startBlock?: number;
240
+ /** 'concurrent' = tip-first: live at tip now, history backfills behind.
241
+ * Requires order-tolerant handlers. Default 'blocking'. */
242
+ backfillMode?: "blocking" | "concurrent";
240
243
  /** Named source filters — keys become handler keys */
241
244
  sources: Record<string, SubgraphFilter>;
242
245
  /** Tables in this subgraph */
@@ -252,6 +255,8 @@ interface ReindexOptions {
252
255
  fromBlock?: number;
253
256
  toBlock?: number;
254
257
  schemaName?: string;
258
+ /** Op row to receive processed_events on each progress flush. */
259
+ operationId?: string;
255
260
  signal?: AbortSignal;
256
261
  }
257
262
  /**
@@ -269,6 +274,7 @@ declare function reindexSubgraph(def: SubgraphDefinition, opts?: ReindexOptions)
269
274
  */
270
275
  declare function resumeReindex(def: SubgraphDefinition, opts: {
271
276
  schemaName: string
277
+ operationId?: string
272
278
  signal?: AbortSignal
273
279
  }): Promise<{
274
280
  processed: number
@@ -282,6 +288,7 @@ declare function backfillSubgraph(def: SubgraphDefinition, opts: {
282
288
  fromBlock: number
283
289
  toBlock: number
284
290
  schemaName?: string
291
+ operationId?: string
285
292
  signal?: AbortSignal
286
293
  }): Promise<{
287
294
  processed: number
@@ -727,6 +734,14 @@ interface ColumnDiff {
727
734
  * Compare two multi-table subgraph schemas and return differences.
728
735
  */
729
736
  declare function diffSchema(existing: SubgraphSchema, incoming: SubgraphSchema): TableDiff;
737
+ /**
738
+ * Returns true if the diff contains any breaking changes
739
+ * (removed tables, removed columns, or changed column types).
740
+ */
741
+ declare function hasBreakingChanges(diff: TableDiff): {
742
+ breaking: boolean
743
+ reasons: string[]
744
+ };
730
745
  interface DeployDiff {
731
746
  addedTables: string[];
732
747
  removedTables: string[];
@@ -794,4 +809,17 @@ declare function deploySchema(db: AnyDb, def: SubgraphDefinition, handlerPath: s
794
809
  version: string
795
810
  diff?: DeployDiff
796
811
  }>;
797
- export { validateSubgraphDefinition, resumeReindex, renderDeployPlan, reindexSubgraph, pgSchemaName, generateSubgraphSQL, generatePrismaSchema, generateKyselySchema, generateIndexSchema, generateDrizzleSchema, diffSchema, deploySchema, defineSubgraph, backfillSubgraph, WriteRow, WhereInput, TypedSubgraphDefinition, TypedSubgraphContext, TypedHandlers, TxMeta, TableDiff, SystemRow, SubscribeOptions, SubgraphTableClient, SubgraphTable, SubgraphSchema, SubgraphHandler, SubgraphFilter, SubgraphDefinition, SubgraphContext, SubgraphColumn, StxTransferPayload, StxTransferFilter, StxMintPayload, StxMintFilter, StxLockPayload, StxLockFilter, StxBurnPayload, StxBurnFilter, RowValue, ReindexOptions, PrismaGenOptions, PrintEventPayload, PrintEventFor, PrintEventFilter, NftTransferPayload, NftTransferFilter, NftMintPayload, NftMintFilter, NftBurnPayload, NftBurnFilter, KyselyGenOptions, InferTableRow, InferSubgraphClient, InferColumnType, IndexCodegenTarget, IndexCodegenOptions, INDEX_CODEGEN_TABLES, GeneratedSQL, FtTransferPayload, FtTransferFilter, FtMintPayload, FtMintFilter, FtBurnPayload, FtBurnFilter, FindManyOptions, EventForFilter, DrizzleGenOptions, DeployPlan, DeployDiff, ContractDeployPayload, ContractDeployFilter, ContractCallPayload, ContractCallFilter, ContractCallEvent, ComputedValue, ComparisonFilter, ColumnType, ColumnToTS, ColumnDiff, ByoMigrationPlan, ByoBreakingChangeError, AnyEvent, AggregateSpec, AggregateResult };
812
+ /** A (decoded event type, optional contract scope) pair the sparse probe
813
+ * checks. Contract scoping is what makes token-subgraph reindexes leap over
814
+ * everything that isn't their token. */
815
+ type SparseProbeTarget = {
816
+ eventType: string
817
+ contractId?: string
818
+ };
819
+ /** Sparse scanning is sound only when EVERY source is an event-type filter —
820
+ * a contract_call/contract_deploy source matches transactions, which the
821
+ * event probe can't see. */
822
+ declare function canSparseScan(subgraph: SubgraphDefinition): boolean;
823
+ /** Probe targets for a subgraph's filters: decoded type + contract scope when\\n* the filter pins one (assetIdentifier "SP….contract::asset" or contractId). */
824
+ declare function sparseProbeTargets(subgraph: SubgraphDefinition): SparseProbeTarget[];
825
+ export { validateSubgraphDefinition, sparseProbeTargets, resumeReindex, renderDeployPlan, reindexSubgraph, pgSchemaName, hasBreakingChanges, generateSubgraphSQL, generatePrismaSchema, generateKyselySchema, generateIndexSchema, generateDrizzleSchema, diffSchema, deploySchema, defineSubgraph, canSparseScan, backfillSubgraph, WriteRow, WhereInput, TypedSubgraphDefinition, TypedSubgraphContext, TypedHandlers, TxMeta, TableDiff, SystemRow, SubscribeOptions, SubgraphTableClient, SubgraphTable, SubgraphSchema, SubgraphHandler, SubgraphFilter, SubgraphDefinition, SubgraphContext, SubgraphColumn, StxTransferPayload, StxTransferFilter, StxMintPayload, StxMintFilter, StxLockPayload, StxLockFilter, StxBurnPayload, StxBurnFilter, SparseProbeTarget, RowValue, ReindexOptions, PrismaGenOptions, PrintEventPayload, PrintEventFor, PrintEventFilter, NftTransferPayload, NftTransferFilter, NftMintPayload, NftMintFilter, NftBurnPayload, NftBurnFilter, KyselyGenOptions, InferTableRow, InferSubgraphClient, InferColumnType, IndexCodegenTarget, IndexCodegenOptions, INDEX_CODEGEN_TABLES, GeneratedSQL, FtTransferPayload, FtTransferFilter, FtMintPayload, FtMintFilter, FtBurnPayload, FtBurnFilter, FindManyOptions, EventForFilter, DrizzleGenOptions, DeployPlan, DeployDiff, ContractDeployPayload, ContractDeployFilter, ContractCallPayload, ContractCallFilter, ContractCallEvent, ComputedValue, ComparisonFilter, ColumnType, ColumnToTS, ColumnDiff, ByoMigrationPlan, ByoBreakingChangeError, AnyEvent, AggregateSpec, AggregateResult };
package/dist/src/index.js CHANGED
@@ -69,6 +69,7 @@ var SubgraphDefinitionSchema = z.object({
69
69
  version: z.string().optional(),
70
70
  description: z.string().optional(),
71
71
  startBlock: z.number().int().nonnegative().optional(),
72
+ backfillMode: z.enum(["blocking", "concurrent"]).optional(),
72
73
  sources: z.record(z.string(), SubgraphFilterSchema).refine((s) => Object.keys(s).length > 0, "Must have at least one source"),
73
74
  schema: SubgraphSchemaSchema,
74
75
  handlers: z.record(z.string(), z.any())
@@ -1195,6 +1196,28 @@ function reconstructEvent(e) {
1195
1196
  }
1196
1197
 
1197
1198
  // src/runtime/block-source.ts
1199
+ function canSparseScan(subgraph) {
1200
+ if (Array.isArray(subgraph.sources))
1201
+ return false;
1202
+ const filters = sourceFilters(subgraph);
1203
+ if (filters.length === 0)
1204
+ return false;
1205
+ return filters.every((f) => Boolean(EVENT_FILTER_TO_INDEX_TYPE[f.type]));
1206
+ }
1207
+ function sparseProbeTargets(subgraph) {
1208
+ const targets = new Map;
1209
+ for (const f of sourceFilters(subgraph)) {
1210
+ const eventType = EVENT_FILTER_TO_INDEX_TYPE[f.type];
1211
+ if (!eventType)
1212
+ continue;
1213
+ const scoped = f;
1214
+ const contractId = scoped.contractId ?? scoped.assetIdentifier?.split("::")[0];
1215
+ const key = `${eventType}|${contractId ?? ""}`;
1216
+ targets.set(key, { eventType, ...contractId ? { contractId } : {} });
1217
+ }
1218
+ return [...targets.values()];
1219
+ }
1220
+
1198
1221
  class PostgresBlockSource {
1199
1222
  async getTip() {
1200
1223
  const progress = await getSourceDb().selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
@@ -1257,9 +1280,18 @@ function isStreamsIndexEligible(subgraph) {
1257
1280
  class PublicApiBlockSource {
1258
1281
  http;
1259
1282
  eventTypes;
1260
- constructor(http, eventTypes) {
1283
+ probeTargets;
1284
+ constructor(http, eventTypes, probeTargets) {
1261
1285
  this.http = http;
1262
1286
  this.eventTypes = eventTypes;
1287
+ this.probeTargets = probeTargets;
1288
+ }
1289
+ async nextDataHeight(afterHeight, untilHeight) {
1290
+ if (!this.probeTargets?.length)
1291
+ return afterHeight + 1;
1292
+ const hits = await Promise.all(this.probeTargets.map((t) => this.http.firstEventHeight(t.eventType, afterHeight + 1, untilHeight, t.contractId)));
1293
+ const found = hits.filter((h) => h !== null);
1294
+ return found.length ? Math.min(...found) : null;
1263
1295
  }
1264
1296
  getTip() {
1265
1297
  return this.http.getIndexTip();
@@ -1323,6 +1355,15 @@ class FallbackBlockSource {
1323
1355
  return this.fallback.loadBlockRange(fromHeight, toHeight);
1324
1356
  }
1325
1357
  }
1358
+ async nextDataHeight(afterHeight, untilHeight) {
1359
+ if (!this.primary.nextDataHeight)
1360
+ return afterHeight + 1;
1361
+ try {
1362
+ return await this.primary.nextDataHeight(afterHeight, untilHeight);
1363
+ } catch {
1364
+ return afterHeight + 1;
1365
+ }
1366
+ }
1326
1367
  }
1327
1368
  var postgresBlockSource = new PostgresBlockSource;
1328
1369
  function buildHttpClient() {
@@ -1335,7 +1376,7 @@ function buildHttpClient() {
1335
1376
  }
1336
1377
  function resolveBlockSource(subgraph) {
1337
1378
  if (process.env.SUBGRAPH_SOURCE === "streams-index" && subgraph && isStreamsIndexEligible(subgraph)) {
1338
- return new FallbackBlockSource(new PublicApiBlockSource(buildHttpClient(), referencedIndexEventTypes(subgraph)), postgresBlockSource);
1379
+ return new FallbackBlockSource(new PublicApiBlockSource(buildHttpClient(), referencedIndexEventTypes(subgraph), canSparseScan(subgraph) ? sparseProbeTargets(subgraph) : undefined), postgresBlockSource);
1339
1380
  }
1340
1381
  if (process.env.SUBGRAPH_SOURCE === "streams-index" && subgraph) {
1341
1382
  logger3.debug("Subgraph not streams-index eligible, using DB tap", {
@@ -1818,6 +1859,7 @@ import {
1818
1859
  recordGapBatch,
1819
1860
  resolveGaps
1820
1861
  } from "@secondlayer/shared/db/queries/subgraph-gaps";
1862
+ import { updateOperationProcessedEvents } from "@secondlayer/shared/db/queries/subgraph-operations";
1821
1863
  import {
1822
1864
  recordSubgraphProcessed as recordSubgraphProcessed2,
1823
1865
  updateSubgraphStatus as updateSubgraphStatus2
@@ -2005,6 +2047,7 @@ async function processBlockRange(def, opts) {
2005
2047
  let batchSize = batchConfig.defaultBatchSize;
2006
2048
  let currentHeight = fromBlock;
2007
2049
  let aborted = false;
2050
+ const sparse = Boolean(source.nextDataHeight && canSparseScan(def));
2008
2051
  const flushHealth = async () => {
2009
2052
  if (pendingEventsProcessed === 0 && pendingErrors === 0)
2010
2053
  return;
@@ -2035,6 +2078,7 @@ async function processBlockRange(def, opts) {
2035
2078
  nextBatchPromise = source.loadBlockRange(nextStart, nextBatchEnd);
2036
2079
  }
2037
2080
  const batchFailedBlocks = [];
2081
+ let batchMatched = 0;
2038
2082
  for (let height = currentHeight;height <= batchEnd; height++) {
2039
2083
  const blockData = batch.get(height);
2040
2084
  if (!blockData) {
@@ -2064,6 +2108,7 @@ async function processBlockRange(def, opts) {
2064
2108
  continue;
2065
2109
  }
2066
2110
  blocksProcessed++;
2111
+ batchMatched += result.matched;
2067
2112
  totalEventsProcessed += result.processed;
2068
2113
  totalErrors += result.errors;
2069
2114
  pendingEventsProcessed += result.processed;
@@ -2081,6 +2126,9 @@ async function processBlockRange(def, opts) {
2081
2126
  const shouldFlushProgress = blocksProcessed % 100 === 0 || now - lastProgressFlushAt >= PROGRESS_FLUSH_INTERVAL_MS;
2082
2127
  if (shouldFlushProgress) {
2083
2128
  await updateSubgraphStatus2(targetDb, subgraphName, status, height);
2129
+ if (opts.operationId) {
2130
+ await updateOperationProcessedEvents(targetDb, opts.operationId, totalEventsProcessed).catch(() => {});
2131
+ }
2084
2132
  lastProgressFlushAt = now;
2085
2133
  }
2086
2134
  if (blocksProcessed % LOG_INTERVAL === 0) {
@@ -2109,6 +2157,27 @@ async function processBlockRange(def, opts) {
2109
2157
  });
2110
2158
  });
2111
2159
  }
2160
+ if (sparse && batchMatched === 0 && batchEnd < toBlock && source.nextDataHeight) {
2161
+ const next = await source.nextDataHeight(batchEnd, toBlock);
2162
+ const jumpTo = next === null ? toBlock + 1 : Math.max(next, batchEnd + 1);
2163
+ if (jumpTo > batchEnd + 1) {
2164
+ const skipped = Math.min(jumpTo, toBlock + 1) - (batchEnd + 1);
2165
+ blocksProcessed += skipped;
2166
+ await updateSubgraphStatus2(targetDb, subgraphName, status, jumpTo - 1);
2167
+ logger6.info("Sparse skip", {
2168
+ subgraph: subgraphName,
2169
+ from: batchEnd + 1,
2170
+ to: jumpTo - 1,
2171
+ skipped
2172
+ });
2173
+ currentHeight = jumpTo;
2174
+ if (currentHeight <= toBlock) {
2175
+ nextBatchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
2176
+ nextBatchPromise = source.loadBlockRange(currentHeight, nextBatchEnd);
2177
+ }
2178
+ continue;
2179
+ }
2180
+ }
2112
2181
  const avg = avgEventsPerBlock(batch);
2113
2182
  if (avg > 50)
2114
2183
  batchSize = Math.max(Math.round(batchSize * 0.5), batchConfig.minBatchSize);
@@ -2177,6 +2246,7 @@ async function reindexSubgraph(def, opts) {
2177
2246
  isCatchup: false,
2178
2247
  apiKeyId: null,
2179
2248
  subgraphId: subgraphRow?.id,
2249
+ operationId: opts?.operationId,
2180
2250
  signal: opts?.signal
2181
2251
  });
2182
2252
  if (result.aborted) {
@@ -2252,6 +2322,7 @@ async function resumeReindex(def, opts) {
2252
2322
  isCatchup: false,
2253
2323
  apiKeyId: null,
2254
2324
  subgraphId: row.id,
2325
+ operationId: opts.operationId,
2255
2326
  signal: opts.signal
2256
2327
  });
2257
2328
  if (result.aborted) {
@@ -2300,6 +2371,7 @@ async function backfillSubgraph(def, opts) {
2300
2371
  isCatchup: false,
2301
2372
  apiKeyId: null,
2302
2373
  subgraphId: subgraphRow?.id,
2374
+ operationId: opts.operationId,
2303
2375
  signal: opts.signal
2304
2376
  });
2305
2377
  if (result.aborted) {
@@ -3174,10 +3246,12 @@ function getDefault(type) {
3174
3246
  }
3175
3247
  export {
3176
3248
  validateSubgraphDefinition,
3249
+ sparseProbeTargets,
3177
3250
  resumeReindex,
3178
3251
  renderDeployPlan,
3179
3252
  reindexSubgraph,
3180
3253
  pgSchemaName,
3254
+ hasBreakingChanges,
3181
3255
  generateSubgraphSQL,
3182
3256
  generatePrismaSchema,
3183
3257
  generateKyselySchema,
@@ -3186,10 +3260,11 @@ export {
3186
3260
  diffSchema,
3187
3261
  deploySchema,
3188
3262
  defineSubgraph,
3263
+ canSparseScan,
3189
3264
  backfillSubgraph,
3190
3265
  INDEX_CODEGEN_TABLES,
3191
3266
  ByoBreakingChangeError
3192
3267
  };
3193
3268
 
3194
- //# debugId=44829C409B3F962864756E2164756E21
3269
+ //# debugId=415665ECDA2A436C64756E2164756E21
3195
3270
  //# sourceMappingURL=index.js.map