effective-indexer 0.2.7 → 0.2.9

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/LICENSE CHANGED
@@ -1,12 +1,21 @@
1
- Effective Indexer License
1
+ MIT License
2
+
2
3
  Copyright (c) 2026 Aleksandr Shenshin
3
4
 
4
- This project is available under the PolyForm Noncommercial License 1.0.0.
5
- License text: https://polyformproject.org/licenses/noncommercial/1.0.0/
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
6
11
 
7
- You may use, modify, and distribute this software only for noncommercial purposes
8
- as defined by that license.
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
9
14
 
10
- Commercial use requires a separate paid commercial license agreement.
11
- For commercial licensing inquiries, contact:
12
- Aleksandr Shenshin <shenshin@me.com>
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -345,8 +345,6 @@ npm run check
345
345
 
346
346
  ## License
347
347
 
348
- Free for noncommercial use under PolyForm Noncommercial 1.0.0.
349
- Commercial use requires a paid license — see `LICENSE`.
350
- Contact: Aleksandr Shenshin <shenshin@me.com>.
348
+ MIT do whatever you want.
351
349
 
352
350
  Repository: [github.com/cybervoid0/effective-indexer](https://github.com/cybervoid0/effective-indexer)
package/dist/index.cjs CHANGED
@@ -179,12 +179,22 @@ var RpcProviderLive = effect.Layer.effect(
179
179
  cause: e
180
180
  })
181
181
  }).pipe(
182
- effect.Effect.map((block) => ({
183
- number: block.number,
184
- hash: block.hash,
185
- parentHash: block.parentHash,
186
- timestamp: block.timestamp
187
- }))
182
+ effect.Effect.flatMap((block) => {
183
+ if (!block) {
184
+ return effect.Effect.fail(
185
+ new RpcError({
186
+ reason: `Block ${blockNumber} not found`,
187
+ method: "eth_getBlockByNumber"
188
+ })
189
+ );
190
+ }
191
+ return effect.Effect.succeed({
192
+ number: block.number,
193
+ hash: block.hash,
194
+ parentHash: block.parentHash,
195
+ timestamp: block.timestamp
196
+ });
197
+ })
188
198
  );
189
199
  return { getBlockNumber, getLogs, getBlock };
190
200
  })
@@ -203,7 +213,7 @@ var BlockCursorLive = effect.Layer.effect(
203
213
  const pollOnce = effect.Effect.gen(function* () {
204
214
  const current = yield* rpc.getBlockNumber;
205
215
  const confirmations = BigInt(config.network.polling.confirmations);
206
- const confirmed = current - confirmations;
216
+ const confirmed = current > confirmations ? current - confirmations : 0n;
207
217
  const prev = yield* effect.Ref.get(lastSeen);
208
218
  const isInitialized = yield* effect.Ref.get(initialized);
209
219
  if (!isInitialized) {
@@ -220,10 +230,11 @@ var BlockCursorLive = effect.Layer.effect(
220
230
  yield* effect.Effect.logTrace("No new confirmed blocks");
221
231
  return [];
222
232
  }
223
- const blocks = [];
224
- for (let block = prev + 1n; block <= confirmed; block += 1n) {
225
- blocks.push(block);
226
- }
233
+ const count = Number(confirmed - prev);
234
+ const blocks = Array.from(
235
+ { length: count },
236
+ (_, i) => prev + 1n + BigInt(i)
237
+ );
227
238
  yield* effect.Ref.set(lastSeen, confirmed);
228
239
  yield* effect.Effect.logTrace("Blocks emitted").pipe(
229
240
  effect.Effect.annotateLogs({
@@ -246,15 +257,31 @@ var wrapSqlError = (operation) => (e) => new StorageError({
246
257
  operation,
247
258
  cause: e
248
259
  });
260
+ var buildWhereClause = (query) => {
261
+ const pairs = [
262
+ [query.contractName, "contract_name = ?"],
263
+ [query.eventName, "event_name = ?"],
264
+ [
265
+ query.fromBlock !== void 0 ? Number(query.fromBlock) : void 0,
266
+ "block_number >= ?"
267
+ ],
268
+ [
269
+ query.toBlock !== void 0 ? Number(query.toBlock) : void 0,
270
+ "block_number <= ?"
271
+ ],
272
+ [query.txHash, "tx_hash = ?"]
273
+ ];
274
+ const active = pairs.filter(([v]) => v !== void 0);
275
+ const where = active.length > 0 ? `WHERE ${active.map(([, c]) => c).join(" AND ")}` : "";
276
+ const params = active.map(([v]) => v);
277
+ return { where, params };
278
+ };
249
279
  var toJsonValue = (_key, value) => typeof value === "bigint" ? value.toString() : value;
250
280
  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
- };
281
+ var chunkEvents = (events, size) => Array.from(
282
+ { length: Math.ceil(events.length / size) },
283
+ (_, i) => events.slice(i * size, (i + 1) * size)
284
+ );
258
285
  var Storage = class extends effect.Context.Tag("effective-indexer/Storage")() {
259
286
  };
260
287
  var StorageLive = effect.Layer.effect(
@@ -262,6 +289,7 @@ var StorageLive = effect.Layer.effect(
262
289
  effect.Effect.gen(function* () {
263
290
  const sql$1 = yield* sql.SqlClient.SqlClient;
264
291
  const initialize = effect.Effect.gen(function* () {
292
+ yield* sql$1`PRAGMA journal_mode=WAL`;
265
293
  yield* sql$1`
266
294
  CREATE TABLE IF NOT EXISTS events (
267
295
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -297,7 +325,7 @@ var StorageLive = effect.Layer.effect(
297
325
  if (events.length === 0) {
298
326
  return;
299
327
  }
300
- for (const batch of chunkEvents(events, INSERT_BATCH_SIZE)) {
328
+ yield* effect.Effect.forEach(chunkEvents(events, INSERT_BATCH_SIZE), (batch) => {
301
329
  const placeholders = batch.map(() => "(?, ?, ?, ?, ?, ?, ?)").join(", ");
302
330
  const params = batch.flatMap((event) => {
303
331
  const argsJson = JSON.stringify(event.args, toJsonValue);
@@ -313,44 +341,22 @@ var StorageLive = effect.Layer.effect(
313
341
  argsJson
314
342
  ];
315
343
  });
316
- yield* sql$1.unsafe(
344
+ return sql$1.unsafe(
317
345
  `INSERT OR IGNORE INTO events (contract_name, event_name, block_number, tx_hash, log_index, timestamp, args)
318
346
  VALUES ${placeholders}`,
319
347
  params
320
348
  );
321
- }
349
+ });
322
350
  }).pipe(effect.Effect.mapError(wrapSqlError("insertEvents")));
323
351
  const deleteEventsFrom = (blockNumber) => sql$1`DELETE FROM events WHERE block_number >= ${Number(blockNumber)}`.pipe(
324
352
  effect.Effect.asVoid,
325
353
  effect.Effect.mapError(wrapSqlError("deleteEventsFrom"))
326
354
  );
327
355
  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 ")}` : "";
356
+ const { where, params } = buildWhereClause(query);
351
357
  const order = query.order === "desc" ? "DESC" : "ASC";
352
- const limit = query.limit ?? 1e3;
353
- const offset = query.offset ?? 0;
358
+ const limit = Math.max(1, Math.min(query.limit ?? 1e3, 1e4));
359
+ const offset = Math.max(0, query.offset ?? 0);
354
360
  const rows = yield* sql$1.unsafe(
355
361
  `SELECT * FROM events ${where} ORDER BY block_number ${order}, log_index ASC LIMIT ? OFFSET ?`,
356
362
  [...params, limit, offset]
@@ -362,32 +368,10 @@ var StorageLive = effect.Layer.effect(
362
368
  const rows2 = yield* sql$1`SELECT COUNT(*) as count FROM events`;
363
369
  return rows2[0]?.count ?? 0;
364
370
  }
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 ")}`;
371
+ const { where, params } = buildWhereClause(query);
388
372
  const rows = yield* sql$1.unsafe(
389
373
  `SELECT COUNT(*) as count FROM events ${where}`,
390
- params
374
+ [...params]
391
375
  );
392
376
  return rows[0]?.count ?? 0;
393
377
  }).pipe(effect.Effect.mapError(wrapSqlError("countEvents")));
@@ -672,9 +656,7 @@ var ProgressRendererLive = effect.Layer.effect(
672
656
  });
673
657
  logUpdate(lines.join("\n"));
674
658
  } else {
675
- for (const s of snapshots) {
676
- yield* effect.Effect.log(buildLine(s, config));
677
- }
659
+ yield* effect.Effect.forEach(snapshots, (s) => effect.Effect.log(buildLine(s, config)));
678
660
  }
679
661
  });
680
662
  return {
@@ -745,13 +727,13 @@ var fetchLogs = (params) => effect.Stream.unwrap(
745
727
  const config = yield* Config;
746
728
  const rpc = yield* RpcProvider;
747
729
  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
- }
730
+ const span = params.toBlock - params.fromBlock + 1n;
731
+ const numChunks = span > 0n ? Number((span + chunkSize - 1n) / chunkSize) : 0;
732
+ const chunks = Array.from({ length: numChunks }, (_, i) => {
733
+ const from = params.fromBlock + BigInt(i) * chunkSize;
734
+ const to = from + chunkSize - 1n > params.toBlock ? params.toBlock : from + chunkSize - 1n;
735
+ return { from, to };
736
+ });
755
737
  if (chunks.length === 0) {
756
738
  return effect.Stream.empty;
757
739
  }
@@ -762,7 +744,7 @@ var fetchLogs = (params) => effect.Stream.unwrap(
762
744
  const { baseDelayMs, maxDelayMs } = config.network.logs.retry;
763
745
  const maxRetries = config.network.logs.maxRetries;
764
746
  const attempt = yield* effect.Ref.make(0);
765
- return yield* rpc.getLogs({
747
+ const logs = yield* rpc.getLogs({
766
748
  address: params.address,
767
749
  topics: [params.topics],
768
750
  fromBlock: chunk.from,
@@ -800,15 +782,16 @@ var fetchLogs = (params) => effect.Stream.unwrap(
800
782
  )
801
783
  ),
802
784
  effect.Effect.tap(
803
- (logs) => effect.Effect.logTrace("Logs fetched").pipe(
785
+ (result) => effect.Effect.logTrace("Logs fetched").pipe(
804
786
  effect.Effect.annotateLogs({
805
787
  from: chunk.from.toString(),
806
788
  to: chunk.to.toString(),
807
- count: logs.length.toString()
789
+ count: result.length.toString()
808
790
  })
809
791
  )
810
792
  )
811
793
  );
794
+ return { logs, chunkEnd: chunk.to };
812
795
  }),
813
796
  { concurrency }
814
797
  )
@@ -859,13 +842,11 @@ var ReorgDetectorLive = effect.Layer.effect(
859
842
  }
860
843
  }
861
844
  yield* effect.Ref.update(blockHashBuffer, (buf) => {
862
- const newBuf = new Map(buf);
863
- newBuf.set(block.number, block.hash);
864
845
  const minBlock = block.number - BigInt(reorgDepth);
865
- for (const key of newBuf.keys()) {
866
- if (key < minBlock) newBuf.delete(key);
867
- }
868
- return newBuf;
846
+ const entries = Array.from(buf.entries()).filter(
847
+ ([k]) => k >= minBlock
848
+ );
849
+ return new Map([...entries, [block.number, block.hash]]);
869
850
  });
870
851
  yield* storage.insertBlockHash(block.number, block.hash);
871
852
  });
@@ -949,48 +930,48 @@ var indexContract = (contract) => effect.Stream.unwrap(
949
930
  toBlock: currentHead
950
931
  }).pipe(
951
932
  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(
933
+ (chunk) => effect.Effect.gen(function* () {
934
+ const { logs: rawLogs, chunkEnd } = chunk;
935
+ const blockCache = /* @__PURE__ */ new Map();
936
+ const withTimestamp = rawLogs.length > 0 ? yield* effect.Effect.gen(function* () {
937
+ const decoded = yield* decoder.decodeBatch(
989
938
  contract.name,
990
- lastBlock.blockNumber,
991
- lastBlock.blockHash
939
+ contract.abi,
940
+ rawLogs
992
941
  );
993
- }
942
+ const blockNumbers = [
943
+ ...new Set(decoded.map((d) => d.blockNumber))
944
+ ];
945
+ const enriched = yield* effect.Effect.forEach(
946
+ blockNumbers,
947
+ (bn) => effect.Effect.gen(function* () {
948
+ const blockInfo = yield* getBlockWithRetry(bn);
949
+ blockCache.set(bn, blockInfo);
950
+ yield* reorgDetector.verifyBlock(blockInfo);
951
+ return decoded.filter((e) => e.blockNumber === bn).map((e) => ({
952
+ ...e,
953
+ timestamp: blockInfo.timestamp
954
+ }));
955
+ }),
956
+ { concurrency: 1 }
957
+ );
958
+ const events = enriched.flat();
959
+ yield* storage.insertEvents(
960
+ events.map((e) => ({
961
+ contractName: e.contractName,
962
+ eventName: e.eventName,
963
+ blockNumber: e.blockNumber,
964
+ txHash: e.txHash,
965
+ logIndex: e.logIndex,
966
+ timestamp: e.timestamp,
967
+ args: e.args
968
+ }))
969
+ );
970
+ return events;
971
+ }) : [];
972
+ const cached = blockCache.get(chunkEnd);
973
+ const chunkEndHash = cached ? cached.hash : (yield* getBlockWithRetry(chunkEnd)).hash;
974
+ yield* checkpoint.save(contract.name, chunkEnd, chunkEndHash);
994
975
  const chunkSize = BigInt(config.network.logs.chunkSize);
995
976
  const lastProcessed = yield* effect.Ref.modify(
996
977
  processedBlocksRef,
@@ -1009,7 +990,7 @@ var indexContract = (contract) => effect.Stream.unwrap(
1009
990
  yield* effect.Effect.logDebug("Chunk indexed").pipe(
1010
991
  effect.Effect.annotateLogs({
1011
992
  events: withTimestamp.length.toString(),
1012
- checkpoint: lastBlock ? lastBlock.blockNumber.toString() : "none"
993
+ checkpoint: chunkEnd.toString()
1013
994
  })
1014
995
  );
1015
996
  return withTimestamp;
@@ -1190,15 +1171,11 @@ var QueryApiLive = effect.Layer.effect(
1190
1171
  return { getEvents, getEventCount, getLatestBlock };
1191
1172
  })
1192
1173
  );
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
- };
1174
+ var toConfigMap = (env) => new Map(
1175
+ Object.entries(env).filter(
1176
+ (entry) => typeof entry[1] === "string"
1177
+ )
1178
+ );
1202
1179
  var getProvider = (env) => effect.ConfigProvider.fromMap(toConfigMap(env ?? process.env));
1203
1180
  var readOptionalString = (name, provider) => effect.Effect.runSync(
1204
1181
  effect.Effect.withConfigProvider(provider)(