effective-indexer 0.2.8 → 0.2.10

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/dist/index.cjs CHANGED
@@ -117,13 +117,16 @@ var pickLoggerLayer = (format) => {
117
117
  return effect.Logger.pretty;
118
118
  }
119
119
  };
120
- var LoggerLive = (config) => {
121
- const level = config.enableTelemetry ? parseLogLevel(config.logLevel) : effect.LogLevel.Error;
122
- return effect.Layer.merge(
123
- pickLoggerLayer(config.logFormat),
124
- effect.Logger.minimumLogLevel(level)
125
- );
126
- };
120
+ var LoggerLive = effect.Layer.unwrapEffect(
121
+ effect.Effect.gen(function* () {
122
+ const config = yield* Config;
123
+ const level = config.enableTelemetry ? parseLogLevel(config.logLevel) : effect.LogLevel.Error;
124
+ return effect.Layer.merge(
125
+ pickLoggerLayer(config.logFormat),
126
+ effect.Logger.minimumLogLevel(level)
127
+ );
128
+ })
129
+ );
127
130
  var toHexQuantity = (value) => `0x${value.toString(16)}`;
128
131
  var RpcProvider = class extends effect.Context.Tag("effective-indexer/RpcProvider")() {
129
132
  };
@@ -179,12 +182,22 @@ var RpcProviderLive = effect.Layer.effect(
179
182
  cause: e
180
183
  })
181
184
  }).pipe(
182
- effect.Effect.map((block) => ({
183
- number: block.number,
184
- hash: block.hash,
185
- parentHash: block.parentHash,
186
- timestamp: block.timestamp
187
- }))
185
+ effect.Effect.flatMap((block) => {
186
+ if (!block) {
187
+ return effect.Effect.fail(
188
+ new RpcError({
189
+ reason: `Block ${blockNumber} not found`,
190
+ method: "eth_getBlockByNumber"
191
+ })
192
+ );
193
+ }
194
+ return effect.Effect.succeed({
195
+ number: block.number,
196
+ hash: block.hash,
197
+ parentHash: block.parentHash,
198
+ timestamp: block.timestamp
199
+ });
200
+ })
188
201
  );
189
202
  return { getBlockNumber, getLogs, getBlock };
190
203
  })
@@ -203,7 +216,7 @@ var BlockCursorLive = effect.Layer.effect(
203
216
  const pollOnce = effect.Effect.gen(function* () {
204
217
  const current = yield* rpc.getBlockNumber;
205
218
  const confirmations = BigInt(config.network.polling.confirmations);
206
- const confirmed = current - confirmations;
219
+ const confirmed = current > confirmations ? current - confirmations : 0n;
207
220
  const prev = yield* effect.Ref.get(lastSeen);
208
221
  const isInitialized = yield* effect.Ref.get(initialized);
209
222
  if (!isInitialized) {
@@ -220,10 +233,11 @@ var BlockCursorLive = effect.Layer.effect(
220
233
  yield* effect.Effect.logTrace("No new confirmed blocks");
221
234
  return [];
222
235
  }
223
- const blocks = [];
224
- for (let block = prev + 1n; block <= confirmed; block += 1n) {
225
- blocks.push(block);
226
- }
236
+ const count = Number(confirmed - prev);
237
+ const blocks = Array.from(
238
+ { length: count },
239
+ (_, i) => prev + 1n + BigInt(i)
240
+ );
227
241
  yield* effect.Ref.set(lastSeen, confirmed);
228
242
  yield* effect.Effect.logTrace("Blocks emitted").pipe(
229
243
  effect.Effect.annotateLogs({
@@ -246,15 +260,31 @@ var wrapSqlError = (operation) => (e) => new StorageError({
246
260
  operation,
247
261
  cause: e
248
262
  });
263
+ var buildWhereClause = (query) => {
264
+ const pairs = [
265
+ [query.contractName, "contract_name = ?"],
266
+ [query.eventName, "event_name = ?"],
267
+ [
268
+ query.fromBlock !== void 0 ? Number(query.fromBlock) : void 0,
269
+ "block_number >= ?"
270
+ ],
271
+ [
272
+ query.toBlock !== void 0 ? Number(query.toBlock) : void 0,
273
+ "block_number <= ?"
274
+ ],
275
+ [query.txHash, "tx_hash = ?"]
276
+ ];
277
+ const active = pairs.filter(([v]) => v !== void 0);
278
+ const where = active.length > 0 ? `WHERE ${active.map(([, c]) => c).join(" AND ")}` : "";
279
+ const params = active.map(([v]) => v);
280
+ return { where, params };
281
+ };
249
282
  var toJsonValue = (_key, value) => typeof value === "bigint" ? value.toString() : value;
250
283
  var INSERT_BATCH_SIZE = 250;
251
- var chunkEvents = (events, size) => {
252
- const chunks = [];
253
- for (let i = 0; i < events.length; i += size) {
254
- chunks.push(events.slice(i, i + size));
255
- }
256
- return chunks;
257
- };
284
+ var chunkEvents = (events, size) => Array.from(
285
+ { length: Math.ceil(events.length / size) },
286
+ (_, i) => events.slice(i * size, (i + 1) * size)
287
+ );
258
288
  var Storage = class extends effect.Context.Tag("effective-indexer/Storage")() {
259
289
  };
260
290
  var StorageLive = effect.Layer.effect(
@@ -262,6 +292,7 @@ var StorageLive = effect.Layer.effect(
262
292
  effect.Effect.gen(function* () {
263
293
  const sql$1 = yield* sql.SqlClient.SqlClient;
264
294
  const initialize = effect.Effect.gen(function* () {
295
+ yield* sql$1`PRAGMA journal_mode=WAL`;
265
296
  yield* sql$1`
266
297
  CREATE TABLE IF NOT EXISTS events (
267
298
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -297,7 +328,7 @@ var StorageLive = effect.Layer.effect(
297
328
  if (events.length === 0) {
298
329
  return;
299
330
  }
300
- for (const batch of chunkEvents(events, INSERT_BATCH_SIZE)) {
331
+ yield* effect.Effect.forEach(chunkEvents(events, INSERT_BATCH_SIZE), (batch) => {
301
332
  const placeholders = batch.map(() => "(?, ?, ?, ?, ?, ?, ?)").join(", ");
302
333
  const params = batch.flatMap((event) => {
303
334
  const argsJson = JSON.stringify(event.args, toJsonValue);
@@ -313,44 +344,22 @@ var StorageLive = effect.Layer.effect(
313
344
  argsJson
314
345
  ];
315
346
  });
316
- yield* sql$1.unsafe(
347
+ return sql$1.unsafe(
317
348
  `INSERT OR IGNORE INTO events (contract_name, event_name, block_number, tx_hash, log_index, timestamp, args)
318
349
  VALUES ${placeholders}`,
319
350
  params
320
351
  );
321
- }
352
+ });
322
353
  }).pipe(effect.Effect.mapError(wrapSqlError("insertEvents")));
323
354
  const deleteEventsFrom = (blockNumber) => sql$1`DELETE FROM events WHERE block_number >= ${Number(blockNumber)}`.pipe(
324
355
  effect.Effect.asVoid,
325
356
  effect.Effect.mapError(wrapSqlError("deleteEventsFrom"))
326
357
  );
327
358
  const queryEvents = (query) => effect.Effect.gen(function* () {
328
- const conditions = [];
329
- const params = [];
330
- if (query.contractName !== void 0) {
331
- conditions.push("contract_name = ?");
332
- params.push(query.contractName);
333
- }
334
- if (query.eventName !== void 0) {
335
- conditions.push("event_name = ?");
336
- params.push(query.eventName);
337
- }
338
- if (query.fromBlock !== void 0) {
339
- conditions.push("block_number >= ?");
340
- params.push(Number(query.fromBlock));
341
- }
342
- if (query.toBlock !== void 0) {
343
- conditions.push("block_number <= ?");
344
- params.push(Number(query.toBlock));
345
- }
346
- if (query.txHash !== void 0) {
347
- conditions.push("tx_hash = ?");
348
- params.push(query.txHash);
349
- }
350
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
359
+ const { where, params } = buildWhereClause(query);
351
360
  const order = query.order === "desc" ? "DESC" : "ASC";
352
- const limit = query.limit ?? 1e3;
353
- const offset = query.offset ?? 0;
361
+ const limit = Math.max(1, Math.min(query.limit ?? 1e3, 1e4));
362
+ const offset = Math.max(0, query.offset ?? 0);
354
363
  const rows = yield* sql$1.unsafe(
355
364
  `SELECT * FROM events ${where} ORDER BY block_number ${order}, log_index ASC LIMIT ? OFFSET ?`,
356
365
  [...params, limit, offset]
@@ -358,36 +367,10 @@ var StorageLive = effect.Layer.effect(
358
367
  return rows;
359
368
  }).pipe(effect.Effect.mapError(wrapSqlError("queryEvents")));
360
369
  const countEvents = (query) => effect.Effect.gen(function* () {
361
- if (!query || !query.contractName && !query.eventName && !query.fromBlock && !query.toBlock && !query.txHash) {
362
- const rows2 = yield* sql$1`SELECT COUNT(*) as count FROM events`;
363
- return rows2[0]?.count ?? 0;
364
- }
365
- const conditions = [];
366
- const params = [];
367
- if (query.contractName !== void 0) {
368
- conditions.push("contract_name = ?");
369
- params.push(query.contractName);
370
- }
371
- if (query.eventName !== void 0) {
372
- conditions.push("event_name = ?");
373
- params.push(query.eventName);
374
- }
375
- if (query.fromBlock !== void 0) {
376
- conditions.push("block_number >= ?");
377
- params.push(Number(query.fromBlock));
378
- }
379
- if (query.toBlock !== void 0) {
380
- conditions.push("block_number <= ?");
381
- params.push(Number(query.toBlock));
382
- }
383
- if (query.txHash !== void 0) {
384
- conditions.push("tx_hash = ?");
385
- params.push(query.txHash);
386
- }
387
- const where = `WHERE ${conditions.join(" AND ")}`;
388
- const rows = yield* sql$1.unsafe(
370
+ const { where, params } = buildWhereClause(query ?? {});
371
+ const rows = where === "" ? yield* sql$1`SELECT COUNT(*) as count FROM events` : yield* sql$1.unsafe(
389
372
  `SELECT COUNT(*) as count FROM events ${where}`,
390
- params
373
+ [...params]
391
374
  );
392
375
  return rows[0]?.count ?? 0;
393
376
  }).pipe(effect.Effect.mapError(wrapSqlError("countEvents")));
@@ -672,9 +655,7 @@ var ProgressRendererLive = effect.Layer.effect(
672
655
  });
673
656
  logUpdate(lines.join("\n"));
674
657
  } else {
675
- for (const s of snapshots) {
676
- yield* effect.Effect.log(buildLine(s, config));
677
- }
658
+ yield* effect.Effect.forEach(snapshots, (s) => effect.Effect.log(buildLine(s, config)));
678
659
  }
679
660
  });
680
661
  return {
@@ -745,24 +726,32 @@ var fetchLogs = (params) => effect.Stream.unwrap(
745
726
  const config = yield* Config;
746
727
  const rpc = yield* RpcProvider;
747
728
  const chunkSize = BigInt(config.network.logs.chunkSize);
748
- const chunks = [];
749
- let cursor = params.fromBlock;
750
- while (cursor <= params.toBlock) {
751
- const end = cursor + chunkSize - 1n > params.toBlock ? params.toBlock : cursor + chunkSize - 1n;
752
- chunks.push({ from: cursor, to: end });
753
- cursor = end + 1n;
754
- }
729
+ const { baseDelayMs, maxDelayMs } = config.network.logs.retry;
730
+ const maxRetries = config.network.logs.maxRetries;
731
+ const concurrency = config.network.logs.parallelRequests;
732
+ const retrySchedule = effect.Schedule.exponential(
733
+ effect.Duration.millis(baseDelayMs)
734
+ ).pipe(
735
+ effect.Schedule.delayed(
736
+ (d) => effect.Duration.millis(Math.min(effect.Duration.toMillis(d), maxDelayMs))
737
+ ),
738
+ effect.Schedule.compose(effect.Schedule.recurs(maxRetries))
739
+ );
740
+ const span = params.toBlock - params.fromBlock + 1n;
741
+ const numChunks = span > 0n ? Number((span + chunkSize - 1n) / chunkSize) : 0;
742
+ const chunks = Array.from({ length: numChunks }, (_, i) => {
743
+ const from = params.fromBlock + BigInt(i) * chunkSize;
744
+ const to = from + chunkSize - 1n > params.toBlock ? params.toBlock : from + chunkSize - 1n;
745
+ return { from, to };
746
+ });
755
747
  if (chunks.length === 0) {
756
748
  return effect.Stream.empty;
757
749
  }
758
- const concurrency = config.network.logs.parallelRequests;
759
750
  return effect.Stream.fromIterable(chunks).pipe(
760
751
  effect.Stream.mapEffect(
761
752
  (chunk) => effect.Effect.gen(function* () {
762
- const { baseDelayMs, maxDelayMs } = config.network.logs.retry;
763
- const maxRetries = config.network.logs.maxRetries;
764
753
  const attempt = yield* effect.Ref.make(0);
765
- return yield* rpc.getLogs({
754
+ const logs = yield* rpc.getLogs({
766
755
  address: params.address,
767
756
  topics: [params.topics],
768
757
  fromBlock: chunk.from,
@@ -789,26 +778,18 @@ var fetchLogs = (params) => effect.Stream.unwrap(
789
778
  );
790
779
  })
791
780
  ),
792
- effect.Effect.retry(
793
- effect.Schedule.exponential(effect.Duration.millis(baseDelayMs)).pipe(
794
- effect.Schedule.delayed(
795
- (d) => effect.Duration.millis(
796
- Math.min(effect.Duration.toMillis(d), maxDelayMs)
797
- )
798
- ),
799
- effect.Schedule.compose(effect.Schedule.recurs(maxRetries))
800
- )
801
- ),
781
+ effect.Effect.retry(retrySchedule),
802
782
  effect.Effect.tap(
803
- (logs) => effect.Effect.logTrace("Logs fetched").pipe(
783
+ (result) => effect.Effect.logTrace("Logs fetched").pipe(
804
784
  effect.Effect.annotateLogs({
805
785
  from: chunk.from.toString(),
806
786
  to: chunk.to.toString(),
807
- count: logs.length.toString()
787
+ count: result.length.toString()
808
788
  })
809
789
  )
810
790
  )
811
791
  );
792
+ return { logs, chunkEnd: chunk.to };
812
793
  }),
813
794
  { concurrency }
814
795
  )
@@ -859,13 +840,11 @@ var ReorgDetectorLive = effect.Layer.effect(
859
840
  }
860
841
  }
861
842
  yield* effect.Ref.update(blockHashBuffer, (buf) => {
862
- const newBuf = new Map(buf);
863
- newBuf.set(block.number, block.hash);
864
843
  const minBlock = block.number - BigInt(reorgDepth);
865
- for (const key of newBuf.keys()) {
866
- if (key < minBlock) newBuf.delete(key);
867
- }
868
- return newBuf;
844
+ const entries = Array.from(buf.entries()).filter(
845
+ ([k]) => k >= minBlock
846
+ );
847
+ return new Map([...entries, [block.number, block.hash]]);
869
848
  });
870
849
  yield* storage.insertBlockHash(block.number, block.hash);
871
850
  });
@@ -949,48 +928,48 @@ var indexContract = (contract) => effect.Stream.unwrap(
949
928
  toBlock: currentHead
950
929
  }).pipe(
951
930
  effect.Stream.mapEffect(
952
- (rawLogs) => effect.Effect.gen(function* () {
953
- if (rawLogs.length === 0) return [];
954
- const decoded = yield* decoder.decodeBatch(
955
- contract.name,
956
- contract.abi,
957
- rawLogs
958
- );
959
- const blockNumbers = [
960
- ...new Set(decoded.map((d) => d.blockNumber))
961
- ];
962
- const withTimestamp = [];
963
- for (const bn of blockNumbers) {
964
- const blockInfo = yield* getBlockWithRetry(bn);
965
- yield* reorgDetector.verifyBlock(blockInfo);
966
- for (const event of decoded) {
967
- if (event.blockNumber === bn) {
968
- withTimestamp.push({
969
- ...event,
970
- timestamp: blockInfo.timestamp
971
- });
972
- }
973
- }
974
- }
975
- yield* storage.insertEvents(
976
- withTimestamp.map((e) => ({
977
- contractName: e.contractName,
978
- eventName: e.eventName,
979
- blockNumber: e.blockNumber,
980
- txHash: e.txHash,
981
- logIndex: e.logIndex,
982
- timestamp: e.timestamp,
983
- args: e.args
984
- }))
985
- );
986
- const lastBlock = withTimestamp[withTimestamp.length - 1];
987
- if (lastBlock) {
988
- yield* checkpoint.save(
931
+ (chunk) => effect.Effect.gen(function* () {
932
+ const { logs: rawLogs, chunkEnd } = chunk;
933
+ const blockCache = /* @__PURE__ */ new Map();
934
+ const withTimestamp = rawLogs.length > 0 ? yield* effect.Effect.gen(function* () {
935
+ const decoded = yield* decoder.decodeBatch(
989
936
  contract.name,
990
- lastBlock.blockNumber,
991
- lastBlock.blockHash
937
+ contract.abi,
938
+ rawLogs
992
939
  );
993
- }
940
+ const blockNumbers = [
941
+ ...new Set(decoded.map((d) => d.blockNumber))
942
+ ];
943
+ const enriched = yield* effect.Effect.forEach(
944
+ blockNumbers,
945
+ (bn) => effect.Effect.gen(function* () {
946
+ const blockInfo = yield* getBlockWithRetry(bn);
947
+ blockCache.set(bn, blockInfo);
948
+ yield* reorgDetector.verifyBlock(blockInfo);
949
+ return decoded.filter((e) => e.blockNumber === bn).map((e) => ({
950
+ ...e,
951
+ timestamp: blockInfo.timestamp
952
+ }));
953
+ }),
954
+ { concurrency: 1 }
955
+ );
956
+ const events = enriched.flat();
957
+ yield* storage.insertEvents(
958
+ events.map((e) => ({
959
+ contractName: e.contractName,
960
+ eventName: e.eventName,
961
+ blockNumber: e.blockNumber,
962
+ txHash: e.txHash,
963
+ logIndex: e.logIndex,
964
+ timestamp: e.timestamp,
965
+ args: e.args
966
+ }))
967
+ );
968
+ return events;
969
+ }) : [];
970
+ const cached = blockCache.get(chunkEnd);
971
+ const chunkEndHash = cached ? cached.hash : (yield* getBlockWithRetry(chunkEnd)).hash;
972
+ yield* checkpoint.save(contract.name, chunkEnd, chunkEndHash);
994
973
  const chunkSize = BigInt(config.network.logs.chunkSize);
995
974
  const lastProcessed = yield* effect.Ref.modify(
996
975
  processedBlocksRef,
@@ -1009,7 +988,7 @@ var indexContract = (contract) => effect.Stream.unwrap(
1009
988
  yield* effect.Effect.logDebug("Chunk indexed").pipe(
1010
989
  effect.Effect.annotateLogs({
1011
990
  events: withTimestamp.length.toString(),
1012
- checkpoint: lastBlock ? lastBlock.blockNumber.toString() : "none"
991
+ checkpoint: chunkEnd.toString()
1013
992
  })
1014
993
  );
1015
994
  return withTimestamp;
@@ -1190,15 +1169,11 @@ var QueryApiLive = effect.Layer.effect(
1190
1169
  return { getEvents, getEventCount, getLatestBlock };
1191
1170
  })
1192
1171
  );
1193
- var toConfigMap = (env) => {
1194
- const map = /* @__PURE__ */ new Map();
1195
- for (const [key, value] of Object.entries(env)) {
1196
- if (typeof value === "string") {
1197
- map.set(key, value);
1198
- }
1199
- }
1200
- return map;
1201
- };
1172
+ var toConfigMap = (env) => new Map(
1173
+ Object.entries(env).filter(
1174
+ (entry) => typeof entry[1] === "string"
1175
+ )
1176
+ );
1202
1177
  var getProvider = (env) => effect.ConfigProvider.fromMap(toConfigMap(env ?? process.env));
1203
1178
  var readOptionalString = (name, provider) => effect.Effect.runSync(
1204
1179
  effect.Effect.withConfigProvider(provider)(
@@ -1266,12 +1241,10 @@ var createWebhookNotifier = (webhookUrl, init) => async (notification) => {
1266
1241
  }
1267
1242
  };
1268
1243
  var buildLayers = (config) => {
1269
- const resolved = resolveConfig(config);
1270
1244
  const ConfigLayer = ConfigLive(config);
1271
1245
  const SqliteLayer = sqlSqliteNode.SqliteClient.layer({
1272
1246
  filename: config.dbPath ?? "./indexer.db"
1273
1247
  });
1274
- const LoggerLayer = LoggerLive(resolved);
1275
1248
  const FoundationLayer = effect.Layer.merge(ConfigLayer, SqliteLayer);
1276
1249
  const StorageLayer = StorageLive.pipe(effect.Layer.provide(FoundationLayer));
1277
1250
  const RpcLayer = RpcProviderLive.pipe(effect.Layer.provide(ConfigLayer));
@@ -1290,6 +1263,7 @@ var buildLayers = (config) => {
1290
1263
  const ProgressRendererLayer = ProgressRendererLive.pipe(
1291
1264
  effect.Layer.provide(effect.Layer.merge(ConfigLayer, ProgressReporterLayer))
1292
1265
  );
1266
+ const LoggerLayer = LoggerLive.pipe(effect.Layer.provide(ConfigLayer));
1293
1267
  return effect.Layer.mergeAll(
1294
1268
  ConfigLayer,
1295
1269
  StorageLayer,
@@ -1410,27 +1384,22 @@ var runIndexerWorker = async (config, options) => {
1410
1384
  maxRetryDelayMs: options?.recovery?.maxRetryDelayMs ?? config.worker?.recovery?.maxRetryDelayMs ?? 3e4,
1411
1385
  backoffFactor: options?.recovery?.backoffFactor ?? config.worker?.recovery?.backoffFactor ?? 2
1412
1386
  };
1413
- const signalHandlers = /* @__PURE__ */ new Map();
1414
1387
  let stopRequested = false;
1415
1388
  let activeIndexer = null;
1416
- const stopSignalPromise = new Promise((resolve) => {
1417
- for (const signal of signals) {
1418
- const handler = () => {
1419
- stopRequested = true;
1420
- resolve();
1421
- };
1422
- signalHandlers.set(signal, handler);
1423
- runtime.process.on(signal, handler);
1424
- }
1389
+ let resolveStopSignal;
1390
+ const stopSignalPromise = new Promise((r) => {
1391
+ resolveStopSignal = r;
1425
1392
  });
1426
- const cleanup = () => {
1427
- for (const signal of signals) {
1428
- const handler = signalHandlers.get(signal);
1429
- if (handler !== void 0) {
1430
- runtime.process.off(signal, handler);
1431
- }
1432
- }
1393
+ const requestStop = () => {
1394
+ stopRequested = true;
1395
+ resolveStopSignal?.();
1433
1396
  };
1397
+ signals.forEach((signal) => {
1398
+ runtime.process.on(signal, requestStop);
1399
+ });
1400
+ const cleanup = () => signals.forEach((signal) => {
1401
+ runtime.process.off(signal, requestStop);
1402
+ });
1434
1403
  const stopActiveIndexer = async () => {
1435
1404
  if (activeIndexer !== null) {
1436
1405
  try {