@cogcoin/client 1.1.9 → 1.1.11

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.
Files changed (66) hide show
  1. package/README.md +1 -1
  2. package/dist/bitcoind/client/managed-client.d.ts +2 -0
  3. package/dist/bitcoind/client/managed-client.js +6 -0
  4. package/dist/bitcoind/indexer-daemon/background-follow.d.ts +23 -0
  5. package/dist/bitcoind/indexer-daemon/background-follow.js +132 -0
  6. package/dist/bitcoind/indexer-daemon/client.d.ts +12 -0
  7. package/dist/bitcoind/indexer-daemon/client.js +137 -0
  8. package/dist/bitcoind/indexer-daemon/lifecycle.d.ts +30 -0
  9. package/dist/bitcoind/indexer-daemon/lifecycle.js +153 -0
  10. package/dist/bitcoind/indexer-daemon/process.d.ts +35 -0
  11. package/dist/bitcoind/indexer-daemon/process.js +140 -0
  12. package/dist/bitcoind/indexer-daemon/runtime.d.ts +23 -0
  13. package/dist/bitcoind/indexer-daemon/runtime.js +204 -0
  14. package/dist/bitcoind/indexer-daemon/server.d.ts +12 -0
  15. package/dist/bitcoind/indexer-daemon/server.js +87 -0
  16. package/dist/bitcoind/indexer-daemon/snapshot-leases.d.ts +23 -0
  17. package/dist/bitcoind/indexer-daemon/snapshot-leases.js +139 -0
  18. package/dist/bitcoind/indexer-daemon/status.d.ts +23 -0
  19. package/dist/bitcoind/indexer-daemon/status.js +282 -0
  20. package/dist/bitcoind/indexer-daemon/types.d.ts +141 -0
  21. package/dist/bitcoind/indexer-daemon/types.js +1 -0
  22. package/dist/bitcoind/indexer-daemon-main.js +14 -665
  23. package/dist/bitcoind/indexer-daemon.d.ts +4 -132
  24. package/dist/bitcoind/indexer-daemon.js +2 -417
  25. package/dist/bitcoind/managed-bitcoind-service-config.d.ts +30 -0
  26. package/dist/bitcoind/managed-bitcoind-service-config.js +202 -0
  27. package/dist/bitcoind/managed-bitcoind-service-lifecycle.d.ts +28 -0
  28. package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +296 -0
  29. package/dist/bitcoind/managed-bitcoind-service-process.d.ts +8 -0
  30. package/dist/bitcoind/managed-bitcoind-service-process.js +48 -0
  31. package/dist/bitcoind/managed-bitcoind-service-replica.d.ts +8 -0
  32. package/dist/bitcoind/managed-bitcoind-service-replica.js +142 -0
  33. package/dist/bitcoind/managed-bitcoind-service-status.d.ts +42 -0
  34. package/dist/bitcoind/managed-bitcoind-service-status.js +170 -0
  35. package/dist/bitcoind/managed-bitcoind-service-types.d.ts +36 -0
  36. package/dist/bitcoind/managed-bitcoind-service-types.js +1 -0
  37. package/dist/bitcoind/service.d.ts +7 -63
  38. package/dist/bitcoind/service.js +7 -797
  39. package/dist/cli/mining-format.js +6 -1
  40. package/dist/cli/wallet-format/balance.js +1 -1
  41. package/dist/client/default-client.d.ts +3 -1
  42. package/dist/client/default-client.js +22 -0
  43. package/dist/types.d.ts +13 -1
  44. package/dist/wallet/fs/atomic.d.ts +11 -2
  45. package/dist/wallet/fs/atomic.js +45 -5
  46. package/dist/wallet/mining/cycle.js +4 -4
  47. package/dist/wallet/mining/engine-types.d.ts +1 -0
  48. package/dist/wallet/mining/engine-types.js +9 -1
  49. package/dist/wallet/mining/projection.d.ts +1 -0
  50. package/dist/wallet/mining/projection.js +15 -1
  51. package/dist/wallet/mining/publish.js +3 -6
  52. package/dist/wallet/mining/runner.js +30 -18
  53. package/dist/wallet/mining/visualizer-sync.js +7 -9
  54. package/dist/wallet/mining/visualizer.js +9 -7
  55. package/dist/wallet/read/context.d.ts +4 -10
  56. package/dist/wallet/read/context.js +6 -228
  57. package/dist/wallet/read/local-state.d.ts +36 -0
  58. package/dist/wallet/read/local-state.js +259 -0
  59. package/dist/wallet/read/managed-bitcoind.d.ts +30 -0
  60. package/dist/wallet/read/managed-bitcoind.js +138 -0
  61. package/dist/wallet/read/managed-indexer.d.ts +23 -0
  62. package/dist/wallet/read/managed-indexer.js +87 -0
  63. package/dist/wallet/read/managed-services.d.ts +6 -21
  64. package/dist/wallet/read/managed-services.js +23 -196
  65. package/dist/wallet/read/types.d.ts +1 -0
  66. package/package.json +1 -1
@@ -1,23 +1,7 @@
1
- import { randomUUID } from "node:crypto";
2
- import net from "node:net";
3
- import { access, constants, mkdir, readFile, rm } from "node:fs/promises";
4
- import { loadBundledGenesisParameters, serializeIndexerState } from "@cogcoin/indexer";
5
- import { openManagedBitcoindClientInternal } from "./client.js";
6
- import { DEFAULT_SNAPSHOT_METADATA } from "./bootstrap.js";
7
- import { openClient } from "../client.js";
8
1
  import { readPackageVersionFromDisk } from "../package-version.js";
9
- import { openSqliteStore } from "../sqlite/index.js";
10
- import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
11
- import { createRpcClient } from "./node.js";
12
- import { normalizeCogcoinProcessingStartHeight } from "./processing-start-height.js";
13
- import { createBootstrapProgress } from "./progress/formatting.js";
2
+ import { loadBundledGenesisParameters } from "@cogcoin/indexer";
14
3
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
15
- import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "./types.js";
16
- const SNAPSHOT_TTL_MS = 30_000;
17
- const HEARTBEAT_INTERVAL_MS = 1_000;
18
- const FORCE_RESUME_ERROR_ENV = "COGCOIN_TEST_INDEXER_DAEMON_FORCE_RESUME_ERROR";
19
- const BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS = 30_000;
20
- const BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR = "indexer_daemon_background_follow_resume_timeout";
4
+ import { createIndexerDaemonRuntime } from "./indexer-daemon/runtime.js";
21
5
  function parseArg(name) {
22
6
  const prefix = `--${name}=`;
23
7
  const value = process.argv.find((entry) => entry.startsWith(prefix));
@@ -26,653 +10,26 @@ function parseArg(name) {
26
10
  }
27
11
  return value.slice(prefix.length);
28
12
  }
29
- async function readJsonFile(filePath) {
30
- try {
31
- return JSON.parse(await readFile(filePath, "utf8"));
32
- }
33
- catch (error) {
34
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
35
- return null;
36
- }
37
- throw error;
38
- }
39
- }
40
- async function readManagedBitcoindStatus(paths) {
41
- return readJsonFile(paths.bitcoindStatusPath);
42
- }
43
- async function withTimeout(promise, timeoutMs, errorCode) {
44
- let timeoutId = null;
45
- try {
46
- return await Promise.race([
47
- promise,
48
- new Promise((_, reject) => {
49
- timeoutId = setTimeout(() => reject(new Error(errorCode)), timeoutMs);
50
- }),
51
- ]);
52
- }
53
- finally {
54
- if (timeoutId !== null) {
55
- clearTimeout(timeoutId);
56
- }
57
- }
58
- }
59
- function createSnapshotKey(appliedTip) {
60
- return appliedTip === null
61
- ? "__null__"
62
- : [
63
- appliedTip.height,
64
- appliedTip.blockHashHex,
65
- appliedTip.stateHashHex ?? "",
66
- ].join(":");
67
- }
68
- function createManagedBitcoindCookieUnavailableMessage(cookieFile) {
69
- return `The managed Bitcoin RPC cookie file is unavailable at ${cookieFile} while preparing getblockchaininfo. The managed node is not running or is shutting down.`;
70
- }
71
- async function readCoreTipStatus(paths) {
72
- const runtimeConfig = await readJsonFile(paths.bitcoindRuntimeConfigPath).catch(() => null);
73
- if (runtimeConfig?.rpc === undefined || runtimeConfig.rpc === null) {
74
- return {
75
- rpcReachable: false,
76
- coreBestHeight: null,
77
- coreBestHash: null,
78
- error: "managed_bitcoind_runtime_config_unavailable",
79
- prerequisiteUnavailable: true,
80
- };
81
- }
82
- try {
83
- await access(runtimeConfig.rpc.cookieFile, constants.R_OK);
84
- }
85
- catch {
86
- return {
87
- rpcReachable: false,
88
- coreBestHeight: null,
89
- coreBestHash: null,
90
- error: createManagedBitcoindCookieUnavailableMessage(runtimeConfig.rpc.cookieFile),
91
- prerequisiteUnavailable: true,
92
- };
93
- }
94
- try {
95
- const rpc = createRpcClient(runtimeConfig.rpc);
96
- const info = await rpc.getBlockchainInfo();
97
- return {
98
- rpcReachable: true,
99
- coreBestHeight: info.blocks,
100
- coreBestHash: info.bestblockhash,
101
- error: null,
102
- prerequisiteUnavailable: false,
103
- };
104
- }
105
- catch (error) {
106
- return {
107
- rpcReachable: false,
108
- coreBestHeight: null,
109
- coreBestHash: null,
110
- error: error instanceof Error ? error.message : String(error),
111
- prerequisiteUnavailable: false,
112
- };
113
- }
114
- }
115
- async function readAppliedTipStatus(databasePath) {
116
- try {
117
- const store = await openSqliteStore({ filename: databasePath });
118
- try {
119
- const client = await openClient({ store });
120
- try {
121
- return {
122
- appliedTip: await client.getTip(),
123
- error: null,
124
- schemaMismatch: false,
125
- };
126
- }
127
- finally {
128
- await client.close();
129
- }
130
- }
131
- finally {
132
- await store.close().catch(() => undefined);
133
- }
134
- }
135
- catch (error) {
136
- const message = error instanceof Error ? error.message : String(error);
137
- return {
138
- appliedTip: null,
139
- error: message,
140
- schemaMismatch: message === "sqlite_store_schema_version_unsupported",
141
- };
142
- }
143
- }
144
- async function loadSnapshot(databasePath) {
145
- const store = await openSqliteStore({ filename: databasePath });
146
- try {
147
- const client = await openClient({ store });
148
- try {
149
- const [tip, state] = await Promise.all([client.getTip(), client.getState()]);
150
- return {
151
- token: randomUUID(),
152
- stateBase64: Buffer.from(serializeIndexerState(state)).toString("base64"),
153
- tip,
154
- expiresAtUnixMs: Date.now() + SNAPSHOT_TTL_MS,
155
- };
156
- }
157
- finally {
158
- await client.close();
159
- }
160
- }
161
- finally {
162
- await store.close().catch(() => undefined);
163
- }
164
- }
165
13
  async function main() {
166
14
  const dataDir = parseArg("data-dir");
167
15
  const databasePath = parseArg("database-path");
168
16
  const walletRootId = parseArg("wallet-root-id") || UNINITIALIZED_WALLET_ROOT_ID;
169
17
  const paths = resolveManagedServicePaths(dataDir, walletRootId);
170
- const daemonInstanceId = randomUUID();
171
- const binaryVersion = await readPackageVersionFromDisk().catch(() => "0.0.0");
172
- const genesisParameters = await loadBundledGenesisParameters();
173
- const startedAtUnixMs = Date.now();
174
- const snapshots = new Map();
175
- let state = "starting";
176
- let heartbeatAtUnixMs = startedAtUnixMs;
177
- let updatedAtUnixMs = startedAtUnixMs;
178
- let rpcReachable = false;
179
- let coreBestHeight = null;
180
- let coreBestHash = null;
181
- let appliedTipHeight = null;
182
- let appliedTipHash = null;
183
- let snapshotSeqCounter = 0;
184
- let snapshotSeq = null;
185
- let lastSnapshotKey;
186
- let lastAppliedAtUnixMs = null;
187
- let lastError = null;
188
- let hasSuccessfulCoreTipRefresh = false;
189
- let backgroundStore = null;
190
- let backgroundClient = null;
191
- let backgroundResumePromise = null;
192
- let backgroundFollowError = null;
193
- let backgroundFollowActive = false;
194
- let bootstrapPhase = "paused";
195
- let bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
196
- let cogcoinSyncHeight = null;
197
- let cogcoinSyncTargetHeight = null;
198
- await mkdir(paths.indexerServiceRoot, { recursive: true });
199
- await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
200
- const observeAppliedTip = (appliedTip, now) => {
201
- appliedTipHeight = appliedTip?.height ?? null;
202
- appliedTipHash = appliedTip?.blockHashHex ?? null;
203
- const snapshotKey = createSnapshotKey(appliedTip);
204
- if (lastSnapshotKey !== snapshotKey) {
205
- snapshotSeqCounter += 1;
206
- snapshotSeq = String(snapshotSeqCounter);
207
- lastSnapshotKey = snapshotKey;
208
- lastAppliedAtUnixMs = now;
209
- }
210
- };
211
- const deriveLeaseState = (coreStatus, appliedTip) => {
212
- if (coreStatus.error !== null) {
213
- return {
214
- state: coreStatus.prerequisiteUnavailable && !hasSuccessfulCoreTipRefresh ? "starting" : "failed",
215
- lastError: coreStatus.error,
216
- };
217
- }
218
- hasSuccessfulCoreTipRefresh = true;
219
- if (coreStatus.coreBestHeight !== null
220
- && appliedTip?.height !== undefined
221
- && coreStatus.coreBestHash !== null
222
- && appliedTip?.blockHashHex !== undefined) {
223
- return {
224
- state: coreStatus.coreBestHeight === appliedTip.height && coreStatus.coreBestHash === appliedTip.blockHashHex
225
- ? "synced"
226
- : "catching-up",
227
- lastError: null,
228
- };
229
- }
230
- return {
231
- state: "starting",
232
- lastError: null,
233
- };
234
- };
235
- const buildStatus = () => ({
236
- serviceApiVersion: INDEXER_DAEMON_SERVICE_API_VERSION,
237
- binaryVersion,
238
- buildId: null,
239
- updatedAtUnixMs,
18
+ const runtime = createIndexerDaemonRuntime({
19
+ dataDir,
20
+ databasePath,
240
21
  walletRootId,
241
- daemonInstanceId,
242
- schemaVersion: INDEXER_DAEMON_SCHEMA_VERSION,
243
- state,
244
- processId: process.pid ?? null,
245
- startedAtUnixMs,
246
- heartbeatAtUnixMs,
247
- ipcReady: true,
248
- rpcReachable,
249
- coreBestHeight,
250
- coreBestHash,
251
- appliedTipHeight,
252
- appliedTipHash,
253
- snapshotSeq,
254
- backlogBlocks: coreBestHeight === null || appliedTipHeight === null
255
- ? null
256
- : Math.max(coreBestHeight - appliedTipHeight, 0),
257
- reorgDepth: null,
258
- lastAppliedAtUnixMs,
259
- activeSnapshotCount: snapshots.size,
260
- lastError,
261
- backgroundFollowActive,
262
- bootstrapPhase,
263
- bootstrapProgress: { ...bootstrapProgress },
264
- cogcoinSyncHeight,
265
- cogcoinSyncTargetHeight,
22
+ paths,
23
+ binaryVersion: await readPackageVersionFromDisk().catch(() => "0.0.0"),
24
+ genesisParameters: await loadBundledGenesisParameters(),
266
25
  });
267
- const writeStatus = async () => {
268
- const status = buildStatus();
269
- await writeRuntimeStatusFile(paths.indexerDaemonStatusPath, status);
270
- return status;
271
- };
272
- const recordBackgroundFollowFailure = async (message) => {
273
- const now = Date.now();
274
- heartbeatAtUnixMs = now;
275
- updatedAtUnixMs = now;
276
- state = "failed";
277
- lastError = message;
278
- backgroundFollowError = message;
279
- backgroundFollowActive = false;
280
- bootstrapPhase = "error";
281
- bootstrapProgress = {
282
- ...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
283
- blocks: coreBestHeight,
284
- headers: coreBestHeight,
285
- targetHeight: coreBestHeight,
286
- message,
287
- lastError: message,
288
- updatedAt: now,
289
- };
290
- cogcoinSyncHeight = appliedTipHeight;
291
- cogcoinSyncTargetHeight = coreBestHeight;
292
- await writeStatus();
293
- };
294
- const refreshStatus = async () => {
295
- const now = Date.now();
296
- heartbeatAtUnixMs = now;
297
- updatedAtUnixMs = now;
298
- const [coreStatus, indexedStatus] = await Promise.all([
299
- readCoreTipStatus(paths),
300
- readAppliedTipStatus(databasePath),
301
- ]);
302
- const backgroundStatus = await backgroundClient?.getNodeStatus().catch(() => null) ?? null;
303
- if (backgroundStatus?.following === true) {
304
- backgroundFollowError = null;
305
- }
306
- rpcReachable = coreStatus.rpcReachable;
307
- coreBestHeight = coreStatus.coreBestHeight;
308
- coreBestHash = coreStatus.coreBestHash;
309
- observeAppliedTip(indexedStatus.appliedTip, now);
310
- backgroundFollowActive = backgroundStatus?.following ?? (backgroundClient !== null);
311
- bootstrapPhase = backgroundStatus?.bootstrapPhase ?? (backgroundFollowActive ? "follow_tip" : "paused");
312
- bootstrapProgress = backgroundStatus?.bootstrapProgress ?? createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA);
313
- cogcoinSyncHeight = backgroundStatus?.cogcoinSyncHeight ?? indexedStatus.appliedTip?.height ?? null;
314
- cogcoinSyncTargetHeight = backgroundStatus?.cogcoinSyncTargetHeight ?? coreStatus.coreBestHeight;
315
- if (backgroundStatus === null && backgroundFollowError !== null) {
316
- state = "failed";
317
- lastError = backgroundFollowError;
318
- backgroundFollowActive = false;
319
- bootstrapPhase = "error";
320
- bootstrapProgress = {
321
- ...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
322
- blocks: coreStatus.coreBestHeight,
323
- headers: coreStatus.coreBestHeight,
324
- targetHeight: coreStatus.coreBestHeight,
325
- message: backgroundFollowError,
326
- lastError: backgroundFollowError,
327
- updatedAt: now,
328
- };
329
- cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
330
- cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
331
- return writeStatus();
332
- }
333
- if (indexedStatus.schemaMismatch) {
334
- state = "schema-mismatch";
335
- lastError = indexedStatus.error;
336
- bootstrapPhase = "error";
337
- bootstrapProgress = {
338
- ...bootstrapProgress,
339
- phase: "error",
340
- message: indexedStatus.error ?? "Indexer schema mismatch.",
341
- lastError: indexedStatus.error,
342
- updatedAt: now,
343
- };
344
- return writeStatus();
345
- }
346
- if (indexedStatus.error !== null) {
347
- state = "failed";
348
- lastError = indexedStatus.error;
349
- bootstrapPhase = "error";
350
- bootstrapProgress = {
351
- ...bootstrapProgress,
352
- phase: "error",
353
- message: indexedStatus.error,
354
- lastError: indexedStatus.error,
355
- updatedAt: now,
356
- };
357
- return writeStatus();
358
- }
359
- const leaseState = deriveLeaseState(coreStatus, indexedStatus.appliedTip);
360
- state = leaseState.state;
361
- lastError = leaseState.lastError;
362
- if (lastError !== null) {
363
- bootstrapPhase = leaseState.state === "starting" ? "paused" : "error";
364
- bootstrapProgress = {
365
- ...bootstrapProgress,
366
- phase: bootstrapPhase,
367
- message: lastError,
368
- lastError,
369
- updatedAt: now,
370
- };
371
- }
372
- else if (backgroundStatus === null) {
373
- bootstrapPhase = leaseState.state === "synced" ? "follow_tip" : "paused";
374
- bootstrapProgress = {
375
- ...createBootstrapProgress(bootstrapPhase, DEFAULT_SNAPSHOT_METADATA),
376
- blocks: coreStatus.coreBestHeight,
377
- headers: coreStatus.coreBestHeight,
378
- targetHeight: coreStatus.coreBestHeight,
379
- updatedAt: now,
380
- };
381
- cogcoinSyncHeight = indexedStatus.appliedTip?.height ?? null;
382
- cogcoinSyncTargetHeight = coreStatus.coreBestHeight;
383
- }
384
- return writeStatus();
385
- };
386
- const pauseBackgroundFollow = async () => {
387
- const pendingResume = backgroundResumePromise;
388
- backgroundResumePromise = null;
389
- await pendingResume?.catch(() => undefined);
390
- const client = backgroundClient;
391
- const store = backgroundStore;
392
- backgroundClient = null;
393
- backgroundStore = null;
394
- await client?.close().catch(() => undefined);
395
- await store?.close().catch(() => undefined);
396
- backgroundFollowError = null;
397
- backgroundFollowActive = false;
398
- bootstrapPhase = "paused";
399
- bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
400
- cogcoinSyncHeight = appliedTipHeight;
401
- cogcoinSyncTargetHeight = coreBestHeight;
402
- };
403
- const resumeBackgroundFollow = async () => {
404
- if (backgroundClient !== null) {
26
+ let shuttingDown = false;
27
+ const shutdown = async () => {
28
+ if (shuttingDown) {
405
29
  return;
406
30
  }
407
- if (backgroundResumePromise !== null) {
408
- return backgroundResumePromise;
409
- }
410
- backgroundResumePromise = (async () => {
411
- let store = null;
412
- try {
413
- const forcedResumeError = process.env[FORCE_RESUME_ERROR_ENV]?.trim();
414
- if (forcedResumeError) {
415
- throw new Error(forcedResumeError);
416
- }
417
- const bitcoindStatus = await readManagedBitcoindStatus(paths);
418
- store = await openSqliteStore({ filename: databasePath });
419
- const openedStore = store;
420
- const chain = bitcoindStatus?.chain ?? "main";
421
- const startHeight = normalizeCogcoinProcessingStartHeight({
422
- chain,
423
- startHeight: bitcoindStatus?.startHeight,
424
- genesisParameters,
425
- });
426
- const client = await openManagedBitcoindClientInternal({
427
- store: openedStore,
428
- dataDir,
429
- chain,
430
- startHeight,
431
- walletRootId,
432
- progressOutput: "none",
433
- });
434
- backgroundStore = openedStore;
435
- backgroundClient = client;
436
- backgroundFollowError = null;
437
- backgroundFollowActive = true;
438
- void client.startFollowingTip().catch(async (error) => {
439
- if (backgroundClient !== client || backgroundStore !== openedStore) {
440
- return;
441
- }
442
- backgroundClient = null;
443
- backgroundStore = null;
444
- backgroundFollowActive = false;
445
- await client.close().catch(() => undefined);
446
- await openedStore.close().catch(() => undefined);
447
- const message = error instanceof Error ? error.message : String(error);
448
- await recordBackgroundFollowFailure(message).catch(() => undefined);
449
- });
450
- }
451
- catch (error) {
452
- await store?.close().catch(() => undefined);
453
- const message = error instanceof Error ? error.message : String(error);
454
- await recordBackgroundFollowFailure(message).catch(() => undefined);
455
- throw error;
456
- }
457
- })();
458
- try {
459
- await backgroundResumePromise;
460
- }
461
- finally {
462
- backgroundResumePromise = null;
463
- }
464
- };
465
- const heartbeat = setInterval(() => {
466
- void refreshStatus().catch(() => undefined);
467
- const now = Date.now();
468
- for (const [token, snapshot] of snapshots.entries()) {
469
- if (snapshot.expiresAtUnixMs <= now) {
470
- snapshots.delete(token);
471
- void writeStatus();
472
- }
473
- }
474
- }, HEARTBEAT_INTERVAL_MS);
475
- heartbeat.unref();
476
- const server = net.createServer((socket) => {
477
- let buffer = "";
478
- const writeResponse = (response) => {
479
- socket.write(`${JSON.stringify(response)}\n`);
480
- };
481
- socket.on("data", (chunk) => {
482
- buffer += chunk.toString("utf8");
483
- let newlineIndex = buffer.indexOf("\n");
484
- while (newlineIndex >= 0) {
485
- const line = buffer.slice(0, newlineIndex);
486
- buffer = buffer.slice(newlineIndex + 1);
487
- if (line.trim().length === 0) {
488
- newlineIndex = buffer.indexOf("\n");
489
- continue;
490
- }
491
- let request;
492
- try {
493
- request = JSON.parse(line);
494
- }
495
- catch (error) {
496
- writeResponse({
497
- id: "invalid",
498
- ok: false,
499
- error: error instanceof Error ? error.message : String(error),
500
- });
501
- newlineIndex = buffer.indexOf("\n");
502
- continue;
503
- }
504
- void (async () => {
505
- try {
506
- if (request.method === "GetStatus") {
507
- writeResponse({
508
- id: request.id,
509
- ok: true,
510
- result: buildStatus(),
511
- });
512
- return;
513
- }
514
- if (request.method === "OpenSnapshot") {
515
- const [snapshotMaterial, coreStatus] = await Promise.all([
516
- loadSnapshot(databasePath),
517
- readCoreTipStatus(paths),
518
- ]);
519
- const now = Date.now();
520
- heartbeatAtUnixMs = now;
521
- updatedAtUnixMs = now;
522
- rpcReachable = coreStatus.rpcReachable;
523
- coreBestHeight = coreStatus.coreBestHeight;
524
- coreBestHash = coreStatus.coreBestHash;
525
- observeAppliedTip(snapshotMaterial.tip, now);
526
- const leaseState = deriveLeaseState(coreStatus, snapshotMaterial.tip);
527
- state = leaseState.state;
528
- lastError = leaseState.lastError;
529
- const snapshot = {
530
- ...snapshotMaterial,
531
- serviceApiVersion: INDEXER_DAEMON_SERVICE_API_VERSION,
532
- schemaVersion: INDEXER_DAEMON_SCHEMA_VERSION,
533
- walletRootId,
534
- daemonInstanceId,
535
- processId: process.pid ?? null,
536
- startedAtUnixMs,
537
- snapshotSeq,
538
- tipHeight: snapshotMaterial.tip?.height ?? null,
539
- tipHash: snapshotMaterial.tip?.blockHashHex ?? null,
540
- openedAtUnixMs: now,
541
- };
542
- snapshots.set(snapshot.token, snapshot);
543
- const leaseStatus = await writeStatus();
544
- const result = {
545
- token: snapshot.token,
546
- expiresAtUnixMs: snapshot.expiresAtUnixMs,
547
- serviceApiVersion: snapshot.serviceApiVersion,
548
- binaryVersion,
549
- buildId: null,
550
- walletRootId: snapshot.walletRootId,
551
- daemonInstanceId: snapshot.daemonInstanceId,
552
- schemaVersion: snapshot.schemaVersion,
553
- processId: snapshot.processId,
554
- startedAtUnixMs: snapshot.startedAtUnixMs,
555
- state: leaseStatus.state,
556
- heartbeatAtUnixMs: leaseStatus.heartbeatAtUnixMs,
557
- rpcReachable: leaseStatus.rpcReachable,
558
- coreBestHeight: leaseStatus.coreBestHeight,
559
- coreBestHash: leaseStatus.coreBestHash,
560
- appliedTipHeight: leaseStatus.appliedTipHeight,
561
- appliedTipHash: leaseStatus.appliedTipHash,
562
- snapshotSeq: snapshot.snapshotSeq,
563
- backlogBlocks: leaseStatus.backlogBlocks,
564
- reorgDepth: leaseStatus.reorgDepth,
565
- lastAppliedAtUnixMs: leaseStatus.lastAppliedAtUnixMs,
566
- activeSnapshotCount: leaseStatus.activeSnapshotCount,
567
- lastError: leaseStatus.lastError,
568
- backgroundFollowActive: leaseStatus.backgroundFollowActive ?? false,
569
- bootstrapPhase: leaseStatus.bootstrapPhase ?? null,
570
- bootstrapProgress: leaseStatus.bootstrapProgress ?? null,
571
- cogcoinSyncHeight: leaseStatus.cogcoinSyncHeight ?? null,
572
- cogcoinSyncTargetHeight: leaseStatus.cogcoinSyncTargetHeight ?? null,
573
- tipHeight: snapshot.tipHeight,
574
- tipHash: snapshot.tipHash,
575
- openedAtUnixMs: snapshot.openedAtUnixMs,
576
- };
577
- writeResponse({
578
- id: request.id,
579
- ok: true,
580
- result,
581
- });
582
- return;
583
- }
584
- if (request.method === "ReadSnapshot") {
585
- const snapshot = request.token ? snapshots.get(request.token) : null;
586
- if (!snapshot || snapshot.expiresAtUnixMs <= Date.now()) {
587
- if (request.token) {
588
- snapshots.delete(request.token);
589
- await writeStatus();
590
- }
591
- throw new Error("indexer_daemon_snapshot_invalid");
592
- }
593
- if (snapshot.snapshotSeq !== snapshotSeq) {
594
- snapshots.delete(snapshot.token);
595
- await writeStatus();
596
- throw new Error("indexer_daemon_snapshot_rotated");
597
- }
598
- const result = {
599
- token: snapshot.token,
600
- stateBase64: snapshot.stateBase64,
601
- serviceApiVersion: snapshot.serviceApiVersion,
602
- schemaVersion: snapshot.schemaVersion,
603
- walletRootId: snapshot.walletRootId,
604
- daemonInstanceId: snapshot.daemonInstanceId,
605
- processId: snapshot.processId,
606
- startedAtUnixMs: snapshot.startedAtUnixMs,
607
- snapshotSeq: snapshot.snapshotSeq,
608
- tipHeight: snapshot.tipHeight,
609
- tipHash: snapshot.tipHash,
610
- openedAtUnixMs: snapshot.openedAtUnixMs,
611
- tip: snapshot.tip,
612
- expiresAtUnixMs: snapshot.expiresAtUnixMs,
613
- };
614
- writeResponse({
615
- id: request.id,
616
- ok: true,
617
- result,
618
- });
619
- return;
620
- }
621
- if (request.method === "CloseSnapshot") {
622
- if (request.token) {
623
- snapshots.delete(request.token);
624
- await writeStatus();
625
- }
626
- writeResponse({
627
- id: request.id,
628
- ok: true,
629
- result: null,
630
- });
631
- return;
632
- }
633
- if (request.method === "ResumeBackgroundFollow") {
634
- try {
635
- await withTimeout(resumeBackgroundFollow(), BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS, BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR);
636
- }
637
- catch (error) {
638
- if (error instanceof Error
639
- && error.message === BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR) {
640
- await recordBackgroundFollowFailure(error.message).catch(() => undefined);
641
- }
642
- throw error;
643
- }
644
- writeResponse({
645
- id: request.id,
646
- ok: true,
647
- result: null,
648
- });
649
- return;
650
- }
651
- throw new Error(`indexer_daemon_unknown_method_${request.method}`);
652
- }
653
- catch (error) {
654
- writeResponse({
655
- id: request.id,
656
- ok: false,
657
- error: error instanceof Error ? error.message : String(error),
658
- });
659
- }
660
- })();
661
- newlineIndex = buffer.indexOf("\n");
662
- }
663
- });
664
- });
665
- const shutdown = async () => {
666
- clearInterval(heartbeat);
667
- await pauseBackgroundFollow().catch(() => undefined);
668
- state = "stopping";
669
- heartbeatAtUnixMs = Date.now();
670
- updatedAtUnixMs = heartbeatAtUnixMs;
671
- await writeStatus().catch(() => undefined);
672
- await new Promise((resolve) => {
673
- server.close(() => resolve());
674
- });
675
- await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
31
+ shuttingDown = true;
32
+ await runtime.shutdown().catch(() => undefined);
676
33
  process.exit(0);
677
34
  };
678
35
  process.on("SIGTERM", () => {
@@ -681,14 +38,6 @@ async function main() {
681
38
  process.on("SIGINT", () => {
682
39
  void shutdown();
683
40
  });
684
- await new Promise((resolve, reject) => {
685
- server.once("error", reject);
686
- server.listen(paths.indexerDaemonSocketPath, async () => {
687
- server.off("error", reject);
688
- await writeStatus();
689
- await refreshStatus().catch(() => undefined);
690
- resolve();
691
- });
692
- });
41
+ await runtime.start();
693
42
  }
694
43
  await main();