effective-indexer 0.2.4 → 0.2.6
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 +10 -5
- package/README.md +10 -12
- package/dist/index.cjs +250 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -59
- package/dist/index.d.ts +90 -59
- package/dist/index.js +249 -83
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/LICENSE
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
Effective Indexer License
|
|
1
2
|
Copyright (c) 2026 Aleksandr Shenshin
|
|
2
|
-
All rights reserved.
|
|
3
3
|
|
|
4
|
-
This
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
This project is available under the PolyForm Noncommercial License 1.0.0.
|
|
5
|
+
License text: https://polyformproject.org/licenses/noncommercial/1.0.0/
|
|
6
|
+
|
|
7
|
+
You may use, modify, and distribute this software only for noncommercial purposes
|
|
8
|
+
as defined by that license.
|
|
9
|
+
|
|
10
|
+
Commercial use requires a separate paid commercial license agreement.
|
|
11
|
+
For commercial licensing inquiries, contact:
|
|
12
|
+
Aleksandr Shenshin <shenshin@me.com>
|
package/README.md
CHANGED
|
@@ -20,6 +20,12 @@ npm install effective-indexer effect
|
|
|
20
20
|
|
|
21
21
|
`effect` is a peer dependency.
|
|
22
22
|
|
|
23
|
+
## License
|
|
24
|
+
|
|
25
|
+
Free for noncommercial use under PolyForm Noncommercial 1.0.0.
|
|
26
|
+
Commercial use requires a paid commercial license (see `LICENSE`).
|
|
27
|
+
Contact: Aleksandr Shenshin <shenshin@me.com>.
|
|
28
|
+
|
|
23
29
|
## 5-minute setup
|
|
24
30
|
|
|
25
31
|
### 1) Create `indexer.config.ts`
|
|
@@ -112,13 +118,13 @@ await indexer.stop()
|
|
|
112
118
|
|
|
113
119
|
## Public API
|
|
114
120
|
|
|
115
|
-
- `defineIndexerConfig(config)`
|
|
121
|
+
- `defineIndexerConfig(config)`
|
|
116
122
|
Identity helper for typed config files (Hardhat-style).
|
|
117
|
-
- `resolveIndexerConfigFromEnv(config, options?)`
|
|
123
|
+
- `resolveIndexerConfigFromEnv(config, options?)`
|
|
118
124
|
Resolves `{{ENV_VAR}}` placeholders and optional RPC URL override.
|
|
119
|
-
- `runIndexerWorker(config, options?)`
|
|
125
|
+
- `runIndexerWorker(config, options?)`
|
|
120
126
|
Runs long-lived worker with built-in DB directory creation and graceful shutdown.
|
|
121
|
-
- `Indexer.create(config)`
|
|
127
|
+
- `Indexer.create(config)`
|
|
122
128
|
Returns handle: `start()`, `stop()`, `query()`, `count()`.
|
|
123
129
|
|
|
124
130
|
## Config essentials
|
|
@@ -483,11 +489,3 @@ npm run typecheck
|
|
|
483
489
|
npm run test
|
|
484
490
|
npm run check
|
|
485
491
|
```
|
|
486
|
-
|
|
487
|
-
### Live Integration Tests
|
|
488
|
-
|
|
489
|
-
Integration tests read RPC URLs from `.env` (mainnet) and `.env.test` (testnet) using `EVM_RPC_URL`.
|
|
490
|
-
|
|
491
|
-
```bash
|
|
492
|
-
npm run test:integration
|
|
493
|
-
```
|
package/dist/index.cjs
CHANGED
|
@@ -53,22 +53,26 @@ var resolveTelemetry = (config) => {
|
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
55
|
};
|
|
56
|
-
var
|
|
56
|
+
var resolveConfigEffect = (config) => effect.Effect.gen(function* () {
|
|
57
57
|
const network = resolveNetwork(config);
|
|
58
58
|
const telemetry = resolveTelemetry(config);
|
|
59
59
|
const pr = network.logs.parallelRequests;
|
|
60
60
|
if (!Number.isInteger(pr) || pr < 1) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
return yield* effect.Effect.fail(
|
|
62
|
+
new ConfigError({
|
|
63
|
+
reason: "parallelRequests must be an integer >= 1",
|
|
64
|
+
field: "network.logs.parallelRequests"
|
|
65
|
+
})
|
|
66
|
+
);
|
|
65
67
|
}
|
|
66
68
|
const pi = telemetry.progress.intervalMs;
|
|
67
69
|
if (!Number.isInteger(pi) || !Number.isFinite(pi) || pi < 500) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
return yield* effect.Effect.fail(
|
|
71
|
+
new ConfigError({
|
|
72
|
+
reason: "telemetry.progress.intervalMs must be an integer >= 500",
|
|
73
|
+
field: "telemetry.progress.intervalMs"
|
|
74
|
+
})
|
|
75
|
+
);
|
|
72
76
|
}
|
|
73
77
|
return {
|
|
74
78
|
rpcUrl: config.rpcUrl,
|
|
@@ -80,10 +84,11 @@ var resolveConfig = (config) => {
|
|
|
80
84
|
logFormat: config.logFormat ?? "pretty",
|
|
81
85
|
enableTelemetry: config.enableTelemetry ?? true
|
|
82
86
|
};
|
|
83
|
-
};
|
|
87
|
+
});
|
|
88
|
+
var resolveConfig = (config) => effect.Effect.runSync(resolveConfigEffect(config));
|
|
84
89
|
var Config = class extends effect.Context.Tag("effective-indexer/Config")() {
|
|
85
90
|
};
|
|
86
|
-
var ConfigLive = (raw) => effect.Layer.
|
|
91
|
+
var ConfigLive = (raw) => effect.Layer.effect(Config, resolveConfigEffect(raw));
|
|
87
92
|
var parseLogLevel = (level) => {
|
|
88
93
|
switch (level) {
|
|
89
94
|
case "trace":
|
|
@@ -242,6 +247,14 @@ var wrapSqlError = (operation) => (e) => new StorageError({
|
|
|
242
247
|
cause: e
|
|
243
248
|
});
|
|
244
249
|
var toJsonValue = (_key, value) => typeof value === "bigint" ? value.toString() : value;
|
|
250
|
+
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
|
+
};
|
|
245
258
|
var Storage = class extends effect.Context.Tag("effective-indexer/Storage")() {
|
|
246
259
|
};
|
|
247
260
|
var StorageLive = effect.Layer.effect(
|
|
@@ -281,14 +294,30 @@ var StorageLive = effect.Layer.effect(
|
|
|
281
294
|
yield* effect.Effect.logDebug("Storage schema initialized");
|
|
282
295
|
}).pipe(effect.Effect.mapError(wrapSqlError("initialize")));
|
|
283
296
|
const insertEvents = (events) => effect.Effect.gen(function* () {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
297
|
+
if (events.length === 0) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
for (const batch of chunkEvents(events, INSERT_BATCH_SIZE)) {
|
|
301
|
+
const placeholders = batch.map(() => "(?, ?, ?, ?, ?, ?, ?)").join(", ");
|
|
302
|
+
const params = batch.flatMap((event) => {
|
|
303
|
+
const argsJson = JSON.stringify(event.args, toJsonValue);
|
|
304
|
+
const blockNum = Number(event.blockNumber);
|
|
305
|
+
const ts = event.timestamp !== null ? Number(event.timestamp) : null;
|
|
306
|
+
return [
|
|
307
|
+
event.contractName,
|
|
308
|
+
event.eventName,
|
|
309
|
+
blockNum,
|
|
310
|
+
event.txHash,
|
|
311
|
+
event.logIndex,
|
|
312
|
+
ts,
|
|
313
|
+
argsJson
|
|
314
|
+
];
|
|
315
|
+
});
|
|
316
|
+
yield* sql$1.unsafe(
|
|
317
|
+
`INSERT OR IGNORE INTO events (contract_name, event_name, block_number, tx_hash, log_index, timestamp, args)
|
|
318
|
+
VALUES ${placeholders}`,
|
|
319
|
+
params
|
|
320
|
+
);
|
|
292
321
|
}
|
|
293
322
|
}).pipe(effect.Effect.mapError(wrapSqlError("insertEvents")));
|
|
294
323
|
const deleteEventsFrom = (blockNumber) => sql$1`DELETE FROM events WHERE block_number >= ${Number(blockNumber)}`.pipe(
|
|
@@ -479,8 +508,20 @@ var EventDecoderLive = effect.Layer.succeed(EventDecoder, {
|
|
|
479
508
|
try: () => decodeLog(contractName, abi, log),
|
|
480
509
|
catch: (e) => new DecodeError({ reason: String(e), log, cause: e })
|
|
481
510
|
}),
|
|
482
|
-
decodeBatch: (contractName, abi, logs) => effect.Effect.
|
|
483
|
-
logs
|
|
511
|
+
decodeBatch: (contractName, abi, logs) => effect.Effect.forEach(
|
|
512
|
+
logs,
|
|
513
|
+
(log) => effect.Effect.gen(function* () {
|
|
514
|
+
const decoded = decodeLog(contractName, abi, log);
|
|
515
|
+
if (decoded === null) {
|
|
516
|
+
return yield* effect.Effect.fail(
|
|
517
|
+
new DecodeError({
|
|
518
|
+
reason: "Failed to decode log with provided ABI",
|
|
519
|
+
log
|
|
520
|
+
})
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
return decoded;
|
|
524
|
+
})
|
|
484
525
|
)
|
|
485
526
|
});
|
|
486
527
|
var computeSnapshot = (p) => {
|
|
@@ -672,22 +713,33 @@ var ProgressRendererLive = effect.Layer.effect(
|
|
|
672
713
|
};
|
|
673
714
|
})
|
|
674
715
|
);
|
|
675
|
-
var
|
|
676
|
-
|
|
677
|
-
|
|
716
|
+
var buildTopicFilterEffect = (abi, eventNames) => effect.Effect.forEach(
|
|
717
|
+
eventNames,
|
|
718
|
+
(name) => effect.Effect.gen(function* () {
|
|
678
719
|
const abiEvent = abi.find(
|
|
679
720
|
(item) => item.type === "event" && item.name === name
|
|
680
721
|
);
|
|
681
722
|
if (!abiEvent || abiEvent.type !== "event") {
|
|
682
|
-
|
|
723
|
+
return yield* effect.Effect.fail(
|
|
724
|
+
new ConfigError({
|
|
725
|
+
reason: `Event "${name}" not found in ABI`,
|
|
726
|
+
field: "contracts.events"
|
|
727
|
+
})
|
|
728
|
+
);
|
|
683
729
|
}
|
|
684
730
|
const encoded = viem.encodeEventTopics({ abi: [abiEvent], eventName: name });
|
|
685
|
-
|
|
686
|
-
|
|
731
|
+
const topic = encoded[0];
|
|
732
|
+
if (topic === void 0) {
|
|
733
|
+
return yield* effect.Effect.fail(
|
|
734
|
+
new ConfigError({
|
|
735
|
+
reason: `Failed to encode topic for event "${name}"`,
|
|
736
|
+
field: "contracts.events"
|
|
737
|
+
})
|
|
738
|
+
);
|
|
687
739
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
740
|
+
return topic;
|
|
741
|
+
})
|
|
742
|
+
);
|
|
691
743
|
var fetchLogs = (params) => effect.Stream.unwrap(
|
|
692
744
|
effect.Effect.gen(function* () {
|
|
693
745
|
const config = yield* Config;
|
|
@@ -847,6 +899,28 @@ var indexContract = (contract) => effect.Stream.unwrap(
|
|
|
847
899
|
const blockCursor = yield* BlockCursor;
|
|
848
900
|
const progress = yield* ProgressReporter;
|
|
849
901
|
const renderer = yield* ProgressRenderer;
|
|
902
|
+
const { baseDelayMs, maxDelayMs } = config.network.logs.retry;
|
|
903
|
+
const maxRetries = config.network.logs.maxRetries;
|
|
904
|
+
const blockRetrySchedule = effect.Schedule.exponential(
|
|
905
|
+
effect.Duration.millis(baseDelayMs)
|
|
906
|
+
).pipe(
|
|
907
|
+
effect.Schedule.delayed(
|
|
908
|
+
(duration) => effect.Duration.millis(Math.min(effect.Duration.toMillis(duration), maxDelayMs))
|
|
909
|
+
),
|
|
910
|
+
effect.Schedule.compose(effect.Schedule.recurs(maxRetries))
|
|
911
|
+
);
|
|
912
|
+
const getBlockWithRetry = (blockNumber) => rpc.getBlock(blockNumber).pipe(
|
|
913
|
+
effect.Effect.tapError(
|
|
914
|
+
(err) => effect.Effect.logDebug("RPC getBlock failed, retrying").pipe(
|
|
915
|
+
effect.Effect.annotateLogs({
|
|
916
|
+
block: blockNumber.toString(),
|
|
917
|
+
reason: err.reason,
|
|
918
|
+
method: "eth_getBlockByNumber"
|
|
919
|
+
})
|
|
920
|
+
)
|
|
921
|
+
),
|
|
922
|
+
effect.Effect.retry(blockRetrySchedule)
|
|
923
|
+
);
|
|
850
924
|
const startBlock = yield* checkpoint.getStartBlock(
|
|
851
925
|
contract.name,
|
|
852
926
|
contract.startBlock ?? 0n
|
|
@@ -858,7 +932,10 @@ var indexContract = (contract) => effect.Stream.unwrap(
|
|
|
858
932
|
toBlock: currentHead.toString()
|
|
859
933
|
})
|
|
860
934
|
);
|
|
861
|
-
const topics =
|
|
935
|
+
const topics = yield* buildTopicFilterEffect(
|
|
936
|
+
contract.abi,
|
|
937
|
+
contract.events
|
|
938
|
+
);
|
|
862
939
|
const needsBackfill = startBlock <= currentHead;
|
|
863
940
|
const totalBackfillBlocks = currentHead - startBlock + 1n;
|
|
864
941
|
if (needsBackfill) {
|
|
@@ -884,7 +961,7 @@ var indexContract = (contract) => effect.Stream.unwrap(
|
|
|
884
961
|
];
|
|
885
962
|
const withTimestamp = [];
|
|
886
963
|
for (const bn of blockNumbers) {
|
|
887
|
-
const blockInfo = yield*
|
|
964
|
+
const blockInfo = yield* getBlockWithRetry(bn);
|
|
888
965
|
yield* reorgDetector.verifyBlock(blockInfo);
|
|
889
966
|
for (const event of decoded) {
|
|
890
967
|
if (event.blockNumber === bn) {
|
|
@@ -943,7 +1020,7 @@ var indexContract = (contract) => effect.Stream.unwrap(
|
|
|
943
1020
|
const liveStream = blockCursor.liveBlocks.pipe(
|
|
944
1021
|
effect.Stream.mapEffect(
|
|
945
1022
|
(blockNumber) => effect.Effect.gen(function* () {
|
|
946
|
-
const blockInfo = yield*
|
|
1023
|
+
const blockInfo = yield* getBlockWithRetry(blockNumber);
|
|
947
1024
|
const reorgResult = yield* effect.Effect.either(
|
|
948
1025
|
reorgDetector.verifyBlock(blockInfo)
|
|
949
1026
|
);
|
|
@@ -1062,6 +1139,14 @@ var parseStoredEvent = (row) => ({
|
|
|
1062
1139
|
timestamp: row.timestamp,
|
|
1063
1140
|
args: JSON.parse(row.args)
|
|
1064
1141
|
});
|
|
1142
|
+
var parseStoredEventEffect = (row) => effect.Effect.try({
|
|
1143
|
+
try: () => parseStoredEvent(row),
|
|
1144
|
+
catch: (e) => new StorageError({
|
|
1145
|
+
reason: String(e),
|
|
1146
|
+
operation: "parseStoredEvent",
|
|
1147
|
+
cause: e
|
|
1148
|
+
})
|
|
1149
|
+
});
|
|
1065
1150
|
var QueryApi = class extends effect.Context.Tag("effective-indexer/QueryApi")() {
|
|
1066
1151
|
};
|
|
1067
1152
|
var QueryApiLive = effect.Layer.effect(
|
|
@@ -1073,7 +1158,7 @@ var QueryApiLive = effect.Layer.effect(
|
|
|
1073
1158
|
(_, v) => typeof v === "bigint" ? v.toString() : v
|
|
1074
1159
|
);
|
|
1075
1160
|
const getEvents = (query) => storage.queryEvents(query ?? {}).pipe(
|
|
1076
|
-
effect.Effect.
|
|
1161
|
+
effect.Effect.flatMap((rows) => effect.Effect.forEach(rows, parseStoredEventEffect)),
|
|
1077
1162
|
effect.Effect.timed,
|
|
1078
1163
|
effect.Effect.tap(
|
|
1079
1164
|
([duration, results]) => effect.Effect.logDebug("Query executed").pipe(
|
|
@@ -1165,6 +1250,21 @@ var resolveIndexerConfigFromEnv = (config, options) => {
|
|
|
1165
1250
|
};
|
|
1166
1251
|
|
|
1167
1252
|
// src/index.ts
|
|
1253
|
+
var createWebhookNotifier = (webhookUrl, init) => async (notification) => {
|
|
1254
|
+
const response = await fetch(webhookUrl, {
|
|
1255
|
+
method: "POST",
|
|
1256
|
+
headers: {
|
|
1257
|
+
"content-type": "application/json",
|
|
1258
|
+
...init?.headers ?? {}
|
|
1259
|
+
},
|
|
1260
|
+
body: JSON.stringify(notification)
|
|
1261
|
+
});
|
|
1262
|
+
if (!response.ok) {
|
|
1263
|
+
throw new Error(
|
|
1264
|
+
`Notification webhook failed with status ${response.status}`
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1168
1268
|
var buildLayers = (config) => {
|
|
1169
1269
|
const resolved = resolveConfig(config);
|
|
1170
1270
|
const ConfigLayer = ConfigLive(config);
|
|
@@ -1209,6 +1309,8 @@ var createIndexer = (config) => {
|
|
|
1209
1309
|
let abortController = null;
|
|
1210
1310
|
const runtime = effect.ManagedRuntime.make(ServicesLive);
|
|
1211
1311
|
let runningPromise = null;
|
|
1312
|
+
let stopPromise = null;
|
|
1313
|
+
let disposed = false;
|
|
1212
1314
|
return {
|
|
1213
1315
|
start: async () => {
|
|
1214
1316
|
if (runningPromise !== null) {
|
|
@@ -1226,21 +1328,41 @@ var createIndexer = (config) => {
|
|
|
1226
1328
|
});
|
|
1227
1329
|
},
|
|
1228
1330
|
stop: async () => {
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1331
|
+
if (disposed) {
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (stopPromise !== null) {
|
|
1335
|
+
return stopPromise;
|
|
1336
|
+
}
|
|
1337
|
+
stopPromise = (async () => {
|
|
1338
|
+
abortController?.abort();
|
|
1339
|
+
const wasAborted = abortController?.signal.aborted ?? false;
|
|
1340
|
+
if (runningPromise !== null) {
|
|
1341
|
+
try {
|
|
1342
|
+
await runningPromise;
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (!wasAborted) {
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
} finally {
|
|
1348
|
+
runningPromise = null;
|
|
1237
1349
|
}
|
|
1238
|
-
} finally {
|
|
1239
|
-
runningPromise = null;
|
|
1240
1350
|
}
|
|
1351
|
+
await runtime.dispose();
|
|
1352
|
+
abortController = null;
|
|
1353
|
+
disposed = true;
|
|
1354
|
+
})();
|
|
1355
|
+
try {
|
|
1356
|
+
await stopPromise;
|
|
1357
|
+
} finally {
|
|
1358
|
+
stopPromise = null;
|
|
1241
1359
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1360
|
+
},
|
|
1361
|
+
waitForExit: async () => {
|
|
1362
|
+
if (runningPromise === null) {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
await runningPromise;
|
|
1244
1366
|
},
|
|
1245
1367
|
query: async (q) => {
|
|
1246
1368
|
return runtime.runPromise(
|
|
@@ -1261,9 +1383,7 @@ var createIndexer = (config) => {
|
|
|
1261
1383
|
};
|
|
1262
1384
|
};
|
|
1263
1385
|
var defaultWorkerRuntime = {
|
|
1264
|
-
process
|
|
1265
|
-
setInterval,
|
|
1266
|
-
clearInterval
|
|
1386
|
+
process
|
|
1267
1387
|
};
|
|
1268
1388
|
var resolveDbPath = (config) => config.dbPath ?? "./indexer.db";
|
|
1269
1389
|
var ensureDbDirectory = async (config) => {
|
|
@@ -1280,46 +1400,92 @@ var runIndexerWorker = async (config, options) => {
|
|
|
1280
1400
|
const runtime = options?.runtime ?? defaultWorkerRuntime;
|
|
1281
1401
|
const create = options?.createIndexer ?? createIndexer;
|
|
1282
1402
|
const signals = options?.shutdownSignals ?? ["SIGINT", "SIGTERM"];
|
|
1283
|
-
const
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
if (handler !== void 0) {
|
|
1298
|
-
runtime.process.off(signal, handler);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
};
|
|
1302
|
-
const handleStop = async () => {
|
|
1303
|
-
if (stopping) {
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
stopping = true;
|
|
1307
|
-
cleanup();
|
|
1308
|
-
try {
|
|
1309
|
-
await indexer.stop();
|
|
1310
|
-
resolve();
|
|
1311
|
-
} catch (error) {
|
|
1312
|
-
reject(error);
|
|
1313
|
-
}
|
|
1314
|
-
};
|
|
1403
|
+
const webhookUrl = config.worker?.alert?.webhookUrl;
|
|
1404
|
+
const configNotifier = webhookUrl && webhookUrl.length > 0 ? createWebhookNotifier(webhookUrl) : null;
|
|
1405
|
+
const onRecoveryFailure = options?.onRecoveryFailure ?? configNotifier ?? void 0;
|
|
1406
|
+
const recovery = {
|
|
1407
|
+
enabled: options?.recovery?.enabled ?? config.worker?.recovery?.enabled ?? true,
|
|
1408
|
+
maxRecoveryDurationMs: options?.recovery?.maxRecoveryDurationMs ?? config.worker?.recovery?.maxRecoveryDurationMs ?? 15 * 60 * 1e3,
|
|
1409
|
+
initialRetryDelayMs: options?.recovery?.initialRetryDelayMs ?? config.worker?.recovery?.initialRetryDelayMs ?? 1e3,
|
|
1410
|
+
maxRetryDelayMs: options?.recovery?.maxRetryDelayMs ?? config.worker?.recovery?.maxRetryDelayMs ?? 3e4,
|
|
1411
|
+
backoffFactor: options?.recovery?.backoffFactor ?? config.worker?.recovery?.backoffFactor ?? 2
|
|
1412
|
+
};
|
|
1413
|
+
const signalHandlers = /* @__PURE__ */ new Map();
|
|
1414
|
+
let stopRequested = false;
|
|
1415
|
+
let activeIndexer = null;
|
|
1416
|
+
const stopSignalPromise = new Promise((resolve) => {
|
|
1315
1417
|
for (const signal of signals) {
|
|
1316
1418
|
const handler = () => {
|
|
1317
|
-
|
|
1419
|
+
stopRequested = true;
|
|
1420
|
+
resolve();
|
|
1318
1421
|
};
|
|
1319
1422
|
signalHandlers.set(signal, handler);
|
|
1320
1423
|
runtime.process.on(signal, handler);
|
|
1321
1424
|
}
|
|
1322
1425
|
});
|
|
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
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
const stopActiveIndexer = async () => {
|
|
1435
|
+
if (activeIndexer !== null) {
|
|
1436
|
+
try {
|
|
1437
|
+
await activeIndexer.stop();
|
|
1438
|
+
} finally {
|
|
1439
|
+
activeIndexer = null;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
let firstFailureAt = null;
|
|
1444
|
+
let attempts = 0;
|
|
1445
|
+
try {
|
|
1446
|
+
while (!stopRequested) {
|
|
1447
|
+
activeIndexer = create(config);
|
|
1448
|
+
try {
|
|
1449
|
+
await activeIndexer.start();
|
|
1450
|
+
await Promise.race([stopSignalPromise, activeIndexer.waitForExit()]);
|
|
1451
|
+
if (stopRequested) {
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
throw new Error("Indexer worker exited unexpectedly");
|
|
1455
|
+
} catch (error) {
|
|
1456
|
+
if (stopRequested) {
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
attempts += 1;
|
|
1460
|
+
firstFailureAt = firstFailureAt ?? Date.now();
|
|
1461
|
+
const recoveryDurationMs = Date.now() - firstFailureAt;
|
|
1462
|
+
await stopActiveIndexer();
|
|
1463
|
+
if (!recovery.enabled || recoveryDurationMs >= recovery.maxRecoveryDurationMs) {
|
|
1464
|
+
if (onRecoveryFailure) {
|
|
1465
|
+
await onRecoveryFailure({
|
|
1466
|
+
attempts,
|
|
1467
|
+
recoveryDurationMs,
|
|
1468
|
+
error,
|
|
1469
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
throw error;
|
|
1473
|
+
}
|
|
1474
|
+
const delay = Math.min(
|
|
1475
|
+
recovery.initialRetryDelayMs * recovery.backoffFactor ** Math.max(attempts - 1, 0),
|
|
1476
|
+
recovery.maxRetryDelayMs
|
|
1477
|
+
);
|
|
1478
|
+
console.error(
|
|
1479
|
+
`Indexer worker crashed, retrying in ${delay}ms (attempt ${attempts})`,
|
|
1480
|
+
error
|
|
1481
|
+
);
|
|
1482
|
+
await effect.Effect.runPromise(effect.Effect.sleep(effect.Duration.millis(delay)));
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
} finally {
|
|
1486
|
+
await stopActiveIndexer();
|
|
1487
|
+
cleanup();
|
|
1488
|
+
}
|
|
1323
1489
|
};
|
|
1324
1490
|
var Indexer = {
|
|
1325
1491
|
create: createIndexer
|
|
@@ -1349,8 +1515,10 @@ exports.Storage = Storage;
|
|
|
1349
1515
|
exports.StorageLive = StorageLive;
|
|
1350
1516
|
exports.computeSnapshot = computeSnapshot;
|
|
1351
1517
|
exports.createIndexer = createIndexer;
|
|
1518
|
+
exports.createWebhookNotifier = createWebhookNotifier;
|
|
1352
1519
|
exports.defineIndexerConfig = defineIndexerConfig;
|
|
1353
1520
|
exports.resolveConfig = resolveConfig;
|
|
1521
|
+
exports.resolveConfigEffect = resolveConfigEffect;
|
|
1354
1522
|
exports.resolveIndexerConfigFromEnv = resolveIndexerConfigFromEnv;
|
|
1355
1523
|
exports.runIndexerWorker = runIndexerWorker;
|
|
1356
1524
|
//# sourceMappingURL=index.cjs.map
|