@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.
- package/README.md +1 -1
- package/dist/bitcoind/client/managed-client.d.ts +2 -0
- package/dist/bitcoind/client/managed-client.js +6 -0
- package/dist/bitcoind/indexer-daemon/background-follow.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/background-follow.js +132 -0
- package/dist/bitcoind/indexer-daemon/client.d.ts +12 -0
- package/dist/bitcoind/indexer-daemon/client.js +137 -0
- package/dist/bitcoind/indexer-daemon/lifecycle.d.ts +30 -0
- package/dist/bitcoind/indexer-daemon/lifecycle.js +153 -0
- package/dist/bitcoind/indexer-daemon/process.d.ts +35 -0
- package/dist/bitcoind/indexer-daemon/process.js +140 -0
- package/dist/bitcoind/indexer-daemon/runtime.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/runtime.js +204 -0
- package/dist/bitcoind/indexer-daemon/server.d.ts +12 -0
- package/dist/bitcoind/indexer-daemon/server.js +87 -0
- package/dist/bitcoind/indexer-daemon/snapshot-leases.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/snapshot-leases.js +139 -0
- package/dist/bitcoind/indexer-daemon/status.d.ts +23 -0
- package/dist/bitcoind/indexer-daemon/status.js +282 -0
- package/dist/bitcoind/indexer-daemon/types.d.ts +141 -0
- package/dist/bitcoind/indexer-daemon/types.js +1 -0
- package/dist/bitcoind/indexer-daemon-main.js +14 -665
- package/dist/bitcoind/indexer-daemon.d.ts +4 -132
- package/dist/bitcoind/indexer-daemon.js +2 -417
- package/dist/bitcoind/managed-bitcoind-service-config.d.ts +30 -0
- package/dist/bitcoind/managed-bitcoind-service-config.js +202 -0
- package/dist/bitcoind/managed-bitcoind-service-lifecycle.d.ts +28 -0
- package/dist/bitcoind/managed-bitcoind-service-lifecycle.js +296 -0
- package/dist/bitcoind/managed-bitcoind-service-process.d.ts +8 -0
- package/dist/bitcoind/managed-bitcoind-service-process.js +48 -0
- package/dist/bitcoind/managed-bitcoind-service-replica.d.ts +8 -0
- package/dist/bitcoind/managed-bitcoind-service-replica.js +142 -0
- package/dist/bitcoind/managed-bitcoind-service-status.d.ts +42 -0
- package/dist/bitcoind/managed-bitcoind-service-status.js +170 -0
- package/dist/bitcoind/managed-bitcoind-service-types.d.ts +36 -0
- package/dist/bitcoind/managed-bitcoind-service-types.js +1 -0
- package/dist/bitcoind/service.d.ts +7 -63
- package/dist/bitcoind/service.js +7 -797
- package/dist/cli/mining-format.js +6 -1
- package/dist/cli/wallet-format/balance.js +1 -1
- package/dist/client/default-client.d.ts +3 -1
- package/dist/client/default-client.js +22 -0
- package/dist/types.d.ts +13 -1
- package/dist/wallet/fs/atomic.d.ts +11 -2
- package/dist/wallet/fs/atomic.js +45 -5
- package/dist/wallet/mining/cycle.js +4 -4
- package/dist/wallet/mining/engine-types.d.ts +1 -0
- package/dist/wallet/mining/engine-types.js +9 -1
- package/dist/wallet/mining/projection.d.ts +1 -0
- package/dist/wallet/mining/projection.js +15 -1
- package/dist/wallet/mining/publish.js +3 -6
- package/dist/wallet/mining/runner.js +30 -18
- package/dist/wallet/mining/visualizer-sync.js +7 -9
- package/dist/wallet/mining/visualizer.js +9 -7
- package/dist/wallet/read/context.d.ts +4 -10
- package/dist/wallet/read/context.js +6 -228
- package/dist/wallet/read/local-state.d.ts +36 -0
- package/dist/wallet/read/local-state.js +259 -0
- package/dist/wallet/read/managed-bitcoind.d.ts +30 -0
- package/dist/wallet/read/managed-bitcoind.js +138 -0
- package/dist/wallet/read/managed-indexer.d.ts +23 -0
- package/dist/wallet/read/managed-indexer.js +87 -0
- package/dist/wallet/read/managed-services.d.ts +6 -21
- package/dist/wallet/read/managed-services.js +23 -196
- package/dist/wallet/read/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { acquireFileLock } from "../../wallet/fs/lock.js";
|
|
3
|
+
import { writeRuntimeStatusFile } from "../../wallet/fs/status-file.js";
|
|
4
|
+
import { readJsonFileIfPresent } from "../managed-runtime/status.js";
|
|
5
|
+
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "../service-paths.js";
|
|
6
|
+
import { probeIndexerDaemonAtSocket } from "./client.js";
|
|
7
|
+
export const DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS = 30_000;
|
|
8
|
+
export const DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
9
|
+
const FORCE_KILL_TIMEOUT_MS = 5_000;
|
|
10
|
+
export function sleep(ms) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
setTimeout(resolve, ms);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function isIndexerDaemonProcessAlive(pid) {
|
|
16
|
+
if (pid === null) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH") {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function waitForIndexerDaemonProcessExit(pid, timeoutMs, errorCode) {
|
|
31
|
+
const deadline = Date.now() + timeoutMs;
|
|
32
|
+
while (Date.now() < deadline) {
|
|
33
|
+
if (!await isIndexerDaemonProcessAlive(pid)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
await sleep(50);
|
|
37
|
+
}
|
|
38
|
+
throw new Error(errorCode);
|
|
39
|
+
}
|
|
40
|
+
export async function clearIndexerDaemonRuntimeArtifacts(paths) {
|
|
41
|
+
await rm(paths.indexerDaemonStatusPath, { force: true }).catch(() => undefined);
|
|
42
|
+
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
43
|
+
}
|
|
44
|
+
export function ignoreIndexerDaemonProcessNotFound(error) {
|
|
45
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function stopIndexerDaemonServiceWithLockHeld(options) {
|
|
50
|
+
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
51
|
+
const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
52
|
+
const status = await readJsonFileIfPresent(paths.indexerDaemonStatusPath);
|
|
53
|
+
const processId = options.processId ?? status?.processId ?? null;
|
|
54
|
+
if (status === null || processId === null || !await isIndexerDaemonProcessAlive(processId)) {
|
|
55
|
+
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
56
|
+
return {
|
|
57
|
+
status: "not-running",
|
|
58
|
+
walletRootId,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
process.kill(processId, "SIGTERM");
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
ignoreIndexerDaemonProcessNotFound(error);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
await waitForIndexerDaemonProcessExit(processId, options.shutdownTimeoutMs ?? DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS, "indexer_daemon_stop_timeout");
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!(error instanceof Error) || error.message !== "indexer_daemon_stop_timeout") {
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
process.kill(processId, "SIGKILL");
|
|
76
|
+
}
|
|
77
|
+
catch (killError) {
|
|
78
|
+
ignoreIndexerDaemonProcessNotFound(killError);
|
|
79
|
+
}
|
|
80
|
+
await waitForIndexerDaemonProcessExit(processId, FORCE_KILL_TIMEOUT_MS, "indexer_daemon_stop_timeout");
|
|
81
|
+
}
|
|
82
|
+
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
83
|
+
return {
|
|
84
|
+
status: "stopped",
|
|
85
|
+
walletRootId,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export async function waitForIndexerDaemon(dataDir, walletRootId, timeoutMs) {
|
|
89
|
+
const paths = resolveManagedServicePaths(dataDir, walletRootId);
|
|
90
|
+
const deadline = Date.now() + timeoutMs;
|
|
91
|
+
while (Date.now() < deadline) {
|
|
92
|
+
const probe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
93
|
+
if (probe.compatibility === "compatible" && probe.client !== null) {
|
|
94
|
+
await probe.client.close().catch(() => undefined);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (probe.compatibility !== "unreachable") {
|
|
98
|
+
throw new Error(probe.error ?? "indexer_daemon_protocol_error");
|
|
99
|
+
}
|
|
100
|
+
await sleep(250);
|
|
101
|
+
}
|
|
102
|
+
throw new Error("indexer_daemon_start_timeout");
|
|
103
|
+
}
|
|
104
|
+
export async function stopIndexerDaemonService(options) {
|
|
105
|
+
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
106
|
+
const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
107
|
+
const lock = await acquireFileLock(paths.indexerDaemonLockPath, {
|
|
108
|
+
purpose: "indexer-daemon-stop",
|
|
109
|
+
walletRootId,
|
|
110
|
+
dataDir: options.dataDir,
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
return await stopIndexerDaemonServiceWithLockHeld({
|
|
114
|
+
...options,
|
|
115
|
+
walletRootId,
|
|
116
|
+
paths,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await lock.release();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export async function shutdownIndexerDaemonForTesting(options) {
|
|
124
|
+
await stopIndexerDaemonService(options).catch(async () => {
|
|
125
|
+
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
126
|
+
const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
127
|
+
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
export async function readIndexerDaemonStatusForTesting(options) {
|
|
131
|
+
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
132
|
+
const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
133
|
+
return readJsonFileIfPresent(paths.indexerDaemonStatusPath);
|
|
134
|
+
}
|
|
135
|
+
export async function writeIndexerDaemonStatusForTesting(options, status) {
|
|
136
|
+
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
137
|
+
const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
138
|
+
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
139
|
+
await writeRuntimeStatusFile(paths.indexerDaemonStatusPath, status);
|
|
140
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { GenesisParameters } from "@cogcoin/indexer/types";
|
|
2
|
+
import { resolveManagedServicePaths } from "../service-paths.js";
|
|
3
|
+
import type { ManagedIndexerDaemonStatus } from "../types.js";
|
|
4
|
+
export interface ManagedIndexerDaemonRuntime {
|
|
5
|
+
start(): Promise<void>;
|
|
6
|
+
shutdown(): Promise<void>;
|
|
7
|
+
getStatus(): ManagedIndexerDaemonStatus;
|
|
8
|
+
}
|
|
9
|
+
export declare function createIndexerDaemonRuntime(options: {
|
|
10
|
+
dataDir: string;
|
|
11
|
+
databasePath: string;
|
|
12
|
+
walletRootId?: string;
|
|
13
|
+
paths?: ReturnType<typeof resolveManagedServicePaths>;
|
|
14
|
+
binaryVersion: string;
|
|
15
|
+
genesisParameters: GenesisParameters;
|
|
16
|
+
daemonInstanceId?: string;
|
|
17
|
+
startedAtUnixMs?: number;
|
|
18
|
+
snapshotTtlMs?: number;
|
|
19
|
+
heartbeatIntervalMs?: number;
|
|
20
|
+
backgroundFollowResumeTimeoutMs?: number;
|
|
21
|
+
backgroundFollowResumeTimeoutError?: string;
|
|
22
|
+
forceResumeErrorEnv?: string;
|
|
23
|
+
}): ManagedIndexerDaemonRuntime;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import { DEFAULT_SNAPSHOT_METADATA } from "../bootstrap.js";
|
|
4
|
+
import { createBootstrapProgress } from "../progress/formatting.js";
|
|
5
|
+
import { resolveManagedServicePaths } from "../service-paths.js";
|
|
6
|
+
import { UNINITIALIZED_WALLET_ROOT_ID } from "../service-paths.js";
|
|
7
|
+
import { pauseBackgroundFollow, recordBackgroundFollowFailure, resumeBackgroundFollow, withTimeout, } from "./background-follow.js";
|
|
8
|
+
import { createIndexerDaemonServer } from "./server.js";
|
|
9
|
+
import { buildIndexerDaemonStatus, deriveIndexerDaemonLeaseState, observeIndexerAppliedTip, readCoreTipStatus, refreshIndexerDaemonStatus, writeIndexerDaemonStatus, } from "./status.js";
|
|
10
|
+
import { closeSnapshotLease, createSnapshotHandle, loadSnapshotMaterial, pruneExpiredSnapshotLeases, readSnapshotLease, storeSnapshotLease, } from "./snapshot-leases.js";
|
|
11
|
+
const SNAPSHOT_TTL_MS = 30_000;
|
|
12
|
+
const HEARTBEAT_INTERVAL_MS = 1_000;
|
|
13
|
+
const FORCE_RESUME_ERROR_ENV = "COGCOIN_TEST_INDEXER_DAEMON_FORCE_RESUME_ERROR";
|
|
14
|
+
const BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS = 30_000;
|
|
15
|
+
const BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR = "indexer_daemon_background_follow_resume_timeout";
|
|
16
|
+
export function createIndexerDaemonRuntime(options) {
|
|
17
|
+
const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
|
|
18
|
+
const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
19
|
+
const snapshotTtlMs = options.snapshotTtlMs ?? SNAPSHOT_TTL_MS;
|
|
20
|
+
const heartbeatIntervalMs = options.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;
|
|
21
|
+
const backgroundFollowResumeTimeoutMs = options.backgroundFollowResumeTimeoutMs ?? BACKGROUND_FOLLOW_RESUME_TIMEOUT_MS;
|
|
22
|
+
const backgroundFollowResumeTimeoutError = options.backgroundFollowResumeTimeoutError
|
|
23
|
+
?? BACKGROUND_FOLLOW_RESUME_TIMEOUT_ERROR;
|
|
24
|
+
const forceResumeErrorEnv = options.forceResumeErrorEnv ?? FORCE_RESUME_ERROR_ENV;
|
|
25
|
+
const startedAtUnixMs = options.startedAtUnixMs ?? Date.now();
|
|
26
|
+
const state = {
|
|
27
|
+
daemonInstanceId: options.daemonInstanceId ?? randomUUID(),
|
|
28
|
+
binaryVersion: options.binaryVersion,
|
|
29
|
+
startedAtUnixMs,
|
|
30
|
+
walletRootId,
|
|
31
|
+
snapshots: new Map(),
|
|
32
|
+
state: "starting",
|
|
33
|
+
heartbeatAtUnixMs: startedAtUnixMs,
|
|
34
|
+
updatedAtUnixMs: startedAtUnixMs,
|
|
35
|
+
rpcReachable: false,
|
|
36
|
+
coreBestHeight: null,
|
|
37
|
+
coreBestHash: null,
|
|
38
|
+
appliedTipHeight: null,
|
|
39
|
+
appliedTipHash: null,
|
|
40
|
+
snapshotSeqCounter: 0,
|
|
41
|
+
snapshotSeq: null,
|
|
42
|
+
lastSnapshotKey: undefined,
|
|
43
|
+
lastAppliedAtUnixMs: null,
|
|
44
|
+
lastError: null,
|
|
45
|
+
hasSuccessfulCoreTipRefresh: false,
|
|
46
|
+
backgroundStore: null,
|
|
47
|
+
backgroundClient: null,
|
|
48
|
+
backgroundResumePromise: null,
|
|
49
|
+
backgroundFollowError: null,
|
|
50
|
+
backgroundFollowActive: false,
|
|
51
|
+
bootstrapPhase: "paused",
|
|
52
|
+
bootstrapProgress: createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA),
|
|
53
|
+
cogcoinSyncHeight: null,
|
|
54
|
+
cogcoinSyncTargetHeight: null,
|
|
55
|
+
};
|
|
56
|
+
let heartbeat = null;
|
|
57
|
+
let server = null;
|
|
58
|
+
let shutdownPromise = null;
|
|
59
|
+
const writeStatus = async () => writeIndexerDaemonStatus(paths, state);
|
|
60
|
+
const openSnapshot = async () => {
|
|
61
|
+
const [snapshotMaterial, coreStatus] = await Promise.all([
|
|
62
|
+
loadSnapshotMaterial(options.databasePath, snapshotTtlMs),
|
|
63
|
+
readCoreTipStatus(paths),
|
|
64
|
+
]);
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
state.heartbeatAtUnixMs = now;
|
|
67
|
+
state.updatedAtUnixMs = now;
|
|
68
|
+
state.rpcReachable = coreStatus.rpcReachable;
|
|
69
|
+
state.coreBestHeight = coreStatus.coreBestHeight;
|
|
70
|
+
state.coreBestHash = coreStatus.coreBestHash;
|
|
71
|
+
observeIndexerAppliedTip(state, snapshotMaterial.tip, now);
|
|
72
|
+
const leaseState = deriveIndexerDaemonLeaseState({
|
|
73
|
+
coreStatus,
|
|
74
|
+
appliedTip: snapshotMaterial.tip,
|
|
75
|
+
hasSuccessfulCoreTipRefresh: state.hasSuccessfulCoreTipRefresh,
|
|
76
|
+
});
|
|
77
|
+
state.hasSuccessfulCoreTipRefresh = leaseState.hasSuccessfulCoreTipRefresh;
|
|
78
|
+
state.state = leaseState.state;
|
|
79
|
+
state.lastError = leaseState.lastError;
|
|
80
|
+
const snapshot = storeSnapshotLease({
|
|
81
|
+
state,
|
|
82
|
+
material: snapshotMaterial,
|
|
83
|
+
nowUnixMs: now,
|
|
84
|
+
});
|
|
85
|
+
const status = await writeStatus();
|
|
86
|
+
return createSnapshotHandle({
|
|
87
|
+
snapshot,
|
|
88
|
+
status,
|
|
89
|
+
binaryVersion: state.binaryVersion,
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
const readSnapshot = async (token) => {
|
|
93
|
+
const result = readSnapshotLease({
|
|
94
|
+
state,
|
|
95
|
+
token,
|
|
96
|
+
});
|
|
97
|
+
if (result.changed) {
|
|
98
|
+
await writeStatus();
|
|
99
|
+
}
|
|
100
|
+
if (result.error !== null || result.payload === null) {
|
|
101
|
+
throw new Error(result.error ?? "indexer_daemon_snapshot_invalid");
|
|
102
|
+
}
|
|
103
|
+
return result.payload;
|
|
104
|
+
};
|
|
105
|
+
const closeSnapshot = async (token) => {
|
|
106
|
+
if (closeSnapshotLease(state, token)) {
|
|
107
|
+
await writeStatus();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const resumeFollow = async () => {
|
|
111
|
+
try {
|
|
112
|
+
await withTimeout(resumeBackgroundFollow({
|
|
113
|
+
dataDir: options.dataDir,
|
|
114
|
+
databasePath: options.databasePath,
|
|
115
|
+
walletRootId,
|
|
116
|
+
paths,
|
|
117
|
+
state,
|
|
118
|
+
genesisParameters: options.genesisParameters,
|
|
119
|
+
forceResumeErrorEnv,
|
|
120
|
+
writeStatus,
|
|
121
|
+
}), backgroundFollowResumeTimeoutMs, backgroundFollowResumeTimeoutError);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof Error
|
|
125
|
+
&& error.message === backgroundFollowResumeTimeoutError) {
|
|
126
|
+
await recordBackgroundFollowFailure({
|
|
127
|
+
state,
|
|
128
|
+
message: error.message,
|
|
129
|
+
writeStatus,
|
|
130
|
+
}).catch(() => undefined);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const tick = () => {
|
|
136
|
+
void refreshIndexerDaemonStatus({
|
|
137
|
+
databasePath: options.databasePath,
|
|
138
|
+
paths,
|
|
139
|
+
state,
|
|
140
|
+
}).catch(() => undefined);
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
if (pruneExpiredSnapshotLeases(state, now)) {
|
|
143
|
+
void writeStatus();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
getStatus() {
|
|
148
|
+
return buildIndexerDaemonStatus(state);
|
|
149
|
+
},
|
|
150
|
+
async start() {
|
|
151
|
+
if (server !== null) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
155
|
+
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
156
|
+
server = createIndexerDaemonServer({
|
|
157
|
+
getStatus: () => buildIndexerDaemonStatus(state),
|
|
158
|
+
openSnapshot,
|
|
159
|
+
readSnapshot,
|
|
160
|
+
closeSnapshot,
|
|
161
|
+
resumeBackgroundFollow: resumeFollow,
|
|
162
|
+
});
|
|
163
|
+
heartbeat = setInterval(tick, heartbeatIntervalMs);
|
|
164
|
+
heartbeat.unref();
|
|
165
|
+
await new Promise((resolve, reject) => {
|
|
166
|
+
server?.once("error", reject);
|
|
167
|
+
server?.listen(paths.indexerDaemonSocketPath, async () => {
|
|
168
|
+
server?.off("error", reject);
|
|
169
|
+
await writeStatus();
|
|
170
|
+
await refreshIndexerDaemonStatus({
|
|
171
|
+
databasePath: options.databasePath,
|
|
172
|
+
paths,
|
|
173
|
+
state,
|
|
174
|
+
}).catch(() => undefined);
|
|
175
|
+
resolve();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
async shutdown() {
|
|
180
|
+
if (shutdownPromise !== null) {
|
|
181
|
+
return shutdownPromise;
|
|
182
|
+
}
|
|
183
|
+
shutdownPromise = (async () => {
|
|
184
|
+
if (heartbeat !== null) {
|
|
185
|
+
clearInterval(heartbeat);
|
|
186
|
+
heartbeat = null;
|
|
187
|
+
}
|
|
188
|
+
await pauseBackgroundFollow({ state }).catch(() => undefined);
|
|
189
|
+
state.state = "stopping";
|
|
190
|
+
state.heartbeatAtUnixMs = Date.now();
|
|
191
|
+
state.updatedAtUnixMs = state.heartbeatAtUnixMs;
|
|
192
|
+
await writeStatus().catch(() => undefined);
|
|
193
|
+
if (server !== null) {
|
|
194
|
+
await new Promise((resolve) => {
|
|
195
|
+
server?.close(() => resolve());
|
|
196
|
+
});
|
|
197
|
+
server = null;
|
|
198
|
+
}
|
|
199
|
+
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
200
|
+
})();
|
|
201
|
+
return shutdownPromise;
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import type { IndexerSnapshotHandle, IndexerSnapshotPayload } from "./types.js";
|
|
3
|
+
import type { ManagedIndexerDaemonStatus } from "../types.js";
|
|
4
|
+
interface IndexerDaemonRequestHandlers {
|
|
5
|
+
getStatus(): ManagedIndexerDaemonStatus | Promise<ManagedIndexerDaemonStatus>;
|
|
6
|
+
openSnapshot(): Promise<IndexerSnapshotHandle>;
|
|
7
|
+
readSnapshot(token?: string): Promise<IndexerSnapshotPayload>;
|
|
8
|
+
closeSnapshot(token?: string): Promise<void>;
|
|
9
|
+
resumeBackgroundFollow(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare function createIndexerDaemonServer(handlers: IndexerDaemonRequestHandlers): net.Server;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
export function createIndexerDaemonServer(handlers) {
|
|
3
|
+
return net.createServer((socket) => {
|
|
4
|
+
let buffer = "";
|
|
5
|
+
const writeResponse = (response) => {
|
|
6
|
+
socket.write(`${JSON.stringify(response)}\n`);
|
|
7
|
+
};
|
|
8
|
+
socket.on("data", (chunk) => {
|
|
9
|
+
buffer += chunk.toString("utf8");
|
|
10
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
11
|
+
while (newlineIndex >= 0) {
|
|
12
|
+
const line = buffer.slice(0, newlineIndex);
|
|
13
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
14
|
+
if (line.trim().length === 0) {
|
|
15
|
+
newlineIndex = buffer.indexOf("\n");
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
let request;
|
|
19
|
+
try {
|
|
20
|
+
request = JSON.parse(line);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
writeResponse({
|
|
24
|
+
id: "invalid",
|
|
25
|
+
ok: false,
|
|
26
|
+
error: error instanceof Error ? error.message : String(error),
|
|
27
|
+
});
|
|
28
|
+
newlineIndex = buffer.indexOf("\n");
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
void (async () => {
|
|
32
|
+
try {
|
|
33
|
+
switch (request.method) {
|
|
34
|
+
case "GetStatus":
|
|
35
|
+
writeResponse({
|
|
36
|
+
id: request.id,
|
|
37
|
+
ok: true,
|
|
38
|
+
result: await handlers.getStatus(),
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
case "OpenSnapshot":
|
|
42
|
+
writeResponse({
|
|
43
|
+
id: request.id,
|
|
44
|
+
ok: true,
|
|
45
|
+
result: await handlers.openSnapshot(),
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
case "ReadSnapshot":
|
|
49
|
+
writeResponse({
|
|
50
|
+
id: request.id,
|
|
51
|
+
ok: true,
|
|
52
|
+
result: await handlers.readSnapshot(request.token),
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
case "CloseSnapshot":
|
|
56
|
+
await handlers.closeSnapshot(request.token);
|
|
57
|
+
writeResponse({
|
|
58
|
+
id: request.id,
|
|
59
|
+
ok: true,
|
|
60
|
+
result: null,
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
case "ResumeBackgroundFollow":
|
|
64
|
+
await handlers.resumeBackgroundFollow();
|
|
65
|
+
writeResponse({
|
|
66
|
+
id: request.id,
|
|
67
|
+
ok: true,
|
|
68
|
+
result: null,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`indexer_daemon_unknown_method_${request.method}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
writeResponse({
|
|
77
|
+
id: request.id,
|
|
78
|
+
ok: false,
|
|
79
|
+
error: error instanceof Error ? error.message : String(error),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
newlineIndex = buffer.indexOf("\n");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ManagedIndexerDaemonStatus } from "../types.js";
|
|
2
|
+
import type { IndexerDaemonRuntimeState, IndexerSnapshotHandle, IndexerSnapshotPayload, LoadedSnapshot, LoadedSnapshotMaterial } from "./types.js";
|
|
3
|
+
export declare function loadSnapshotMaterial(databasePath: string, snapshotTtlMs: number): Promise<LoadedSnapshotMaterial>;
|
|
4
|
+
export declare function storeSnapshotLease(options: {
|
|
5
|
+
state: IndexerDaemonRuntimeState;
|
|
6
|
+
material: LoadedSnapshotMaterial;
|
|
7
|
+
nowUnixMs: number;
|
|
8
|
+
}): LoadedSnapshot;
|
|
9
|
+
export declare function createSnapshotHandle(options: {
|
|
10
|
+
snapshot: LoadedSnapshot;
|
|
11
|
+
status: ManagedIndexerDaemonStatus;
|
|
12
|
+
binaryVersion: string;
|
|
13
|
+
}): IndexerSnapshotHandle;
|
|
14
|
+
export declare function readSnapshotLease(options: {
|
|
15
|
+
state: IndexerDaemonRuntimeState;
|
|
16
|
+
token?: string;
|
|
17
|
+
}): {
|
|
18
|
+
changed: boolean;
|
|
19
|
+
payload: IndexerSnapshotPayload | null;
|
|
20
|
+
error: string | null;
|
|
21
|
+
};
|
|
22
|
+
export declare function closeSnapshotLease(state: IndexerDaemonRuntimeState, token?: string): boolean;
|
|
23
|
+
export declare function pruneExpiredSnapshotLeases(state: IndexerDaemonRuntimeState, nowUnixMs: number): boolean;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { serializeIndexerState } from "@cogcoin/indexer";
|
|
3
|
+
import { openClient } from "../../client.js";
|
|
4
|
+
import { openSqliteStore } from "../../sqlite/index.js";
|
|
5
|
+
export async function loadSnapshotMaterial(databasePath, snapshotTtlMs) {
|
|
6
|
+
const store = await openSqliteStore({ filename: databasePath });
|
|
7
|
+
try {
|
|
8
|
+
const client = await openClient({ store });
|
|
9
|
+
try {
|
|
10
|
+
const [tip, state] = await Promise.all([client.getTip(), client.getState()]);
|
|
11
|
+
return {
|
|
12
|
+
token: randomUUID(),
|
|
13
|
+
stateBase64: Buffer.from(serializeIndexerState(state)).toString("base64"),
|
|
14
|
+
tip,
|
|
15
|
+
expiresAtUnixMs: Date.now() + snapshotTtlMs,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
await client.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await store.close().catch(() => undefined);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function storeSnapshotLease(options) {
|
|
27
|
+
const snapshot = {
|
|
28
|
+
...options.material,
|
|
29
|
+
serviceApiVersion: "cogcoin/indexer-ipc/v1",
|
|
30
|
+
schemaVersion: "cogcoin/indexer-db/v1",
|
|
31
|
+
walletRootId: options.state.walletRootId,
|
|
32
|
+
daemonInstanceId: options.state.daemonInstanceId,
|
|
33
|
+
processId: process.pid ?? null,
|
|
34
|
+
startedAtUnixMs: options.state.startedAtUnixMs,
|
|
35
|
+
snapshotSeq: options.state.snapshotSeq,
|
|
36
|
+
tipHeight: options.material.tip?.height ?? null,
|
|
37
|
+
tipHash: options.material.tip?.blockHashHex ?? null,
|
|
38
|
+
openedAtUnixMs: options.nowUnixMs,
|
|
39
|
+
};
|
|
40
|
+
options.state.snapshots.set(snapshot.token, snapshot);
|
|
41
|
+
return snapshot;
|
|
42
|
+
}
|
|
43
|
+
export function createSnapshotHandle(options) {
|
|
44
|
+
return {
|
|
45
|
+
token: options.snapshot.token,
|
|
46
|
+
expiresAtUnixMs: options.snapshot.expiresAtUnixMs,
|
|
47
|
+
serviceApiVersion: options.snapshot.serviceApiVersion,
|
|
48
|
+
binaryVersion: options.binaryVersion,
|
|
49
|
+
buildId: null,
|
|
50
|
+
walletRootId: options.snapshot.walletRootId,
|
|
51
|
+
daemonInstanceId: options.snapshot.daemonInstanceId,
|
|
52
|
+
schemaVersion: options.snapshot.schemaVersion,
|
|
53
|
+
processId: options.snapshot.processId,
|
|
54
|
+
startedAtUnixMs: options.snapshot.startedAtUnixMs,
|
|
55
|
+
state: options.status.state,
|
|
56
|
+
heartbeatAtUnixMs: options.status.heartbeatAtUnixMs,
|
|
57
|
+
rpcReachable: options.status.rpcReachable,
|
|
58
|
+
coreBestHeight: options.status.coreBestHeight,
|
|
59
|
+
coreBestHash: options.status.coreBestHash,
|
|
60
|
+
appliedTipHeight: options.status.appliedTipHeight,
|
|
61
|
+
appliedTipHash: options.status.appliedTipHash,
|
|
62
|
+
snapshotSeq: options.snapshot.snapshotSeq,
|
|
63
|
+
backlogBlocks: options.status.backlogBlocks,
|
|
64
|
+
reorgDepth: options.status.reorgDepth,
|
|
65
|
+
lastAppliedAtUnixMs: options.status.lastAppliedAtUnixMs,
|
|
66
|
+
activeSnapshotCount: options.status.activeSnapshotCount,
|
|
67
|
+
lastError: options.status.lastError,
|
|
68
|
+
backgroundFollowActive: options.status.backgroundFollowActive ?? false,
|
|
69
|
+
bootstrapPhase: options.status.bootstrapPhase ?? null,
|
|
70
|
+
bootstrapProgress: options.status.bootstrapProgress ?? null,
|
|
71
|
+
cogcoinSyncHeight: options.status.cogcoinSyncHeight ?? null,
|
|
72
|
+
cogcoinSyncTargetHeight: options.status.cogcoinSyncTargetHeight ?? null,
|
|
73
|
+
tipHeight: options.snapshot.tipHeight,
|
|
74
|
+
tipHash: options.snapshot.tipHash,
|
|
75
|
+
openedAtUnixMs: options.snapshot.openedAtUnixMs,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function readSnapshotLease(options) {
|
|
79
|
+
const snapshot = options.token ? options.state.snapshots.get(options.token) : null;
|
|
80
|
+
if (!snapshot || snapshot.expiresAtUnixMs <= Date.now()) {
|
|
81
|
+
if (options.token) {
|
|
82
|
+
options.state.snapshots.delete(options.token);
|
|
83
|
+
return {
|
|
84
|
+
changed: true,
|
|
85
|
+
payload: null,
|
|
86
|
+
error: "indexer_daemon_snapshot_invalid",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
changed: false,
|
|
91
|
+
payload: null,
|
|
92
|
+
error: "indexer_daemon_snapshot_invalid",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (snapshot.snapshotSeq !== options.state.snapshotSeq) {
|
|
96
|
+
options.state.snapshots.delete(snapshot.token);
|
|
97
|
+
return {
|
|
98
|
+
changed: true,
|
|
99
|
+
payload: null,
|
|
100
|
+
error: "indexer_daemon_snapshot_rotated",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
changed: false,
|
|
105
|
+
payload: {
|
|
106
|
+
token: snapshot.token,
|
|
107
|
+
stateBase64: snapshot.stateBase64,
|
|
108
|
+
serviceApiVersion: snapshot.serviceApiVersion,
|
|
109
|
+
schemaVersion: snapshot.schemaVersion,
|
|
110
|
+
walletRootId: snapshot.walletRootId,
|
|
111
|
+
daemonInstanceId: snapshot.daemonInstanceId,
|
|
112
|
+
processId: snapshot.processId,
|
|
113
|
+
startedAtUnixMs: snapshot.startedAtUnixMs,
|
|
114
|
+
snapshotSeq: snapshot.snapshotSeq,
|
|
115
|
+
tipHeight: snapshot.tipHeight,
|
|
116
|
+
tipHash: snapshot.tipHash,
|
|
117
|
+
openedAtUnixMs: snapshot.openedAtUnixMs,
|
|
118
|
+
tip: snapshot.tip,
|
|
119
|
+
expiresAtUnixMs: snapshot.expiresAtUnixMs,
|
|
120
|
+
},
|
|
121
|
+
error: null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export function closeSnapshotLease(state, token) {
|
|
125
|
+
if (!token) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return state.snapshots.delete(token);
|
|
129
|
+
}
|
|
130
|
+
export function pruneExpiredSnapshotLeases(state, nowUnixMs) {
|
|
131
|
+
let changed = false;
|
|
132
|
+
for (const [token, snapshot] of state.snapshots.entries()) {
|
|
133
|
+
if (snapshot.expiresAtUnixMs <= nowUnixMs) {
|
|
134
|
+
state.snapshots.delete(token);
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return changed;
|
|
139
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ClientTip } from "../../types.js";
|
|
2
|
+
import { resolveManagedServicePaths } from "../service-paths.js";
|
|
3
|
+
import type { ManagedBitcoindObservedStatus, ManagedIndexerDaemonStatus } from "../types.js";
|
|
4
|
+
import type { CoreTipStatus, IndexedTipStatus, IndexerDaemonLeaseStateResult, IndexerDaemonRuntimeState } from "./types.js";
|
|
5
|
+
export declare function readJsonFile<T>(filePath: string): Promise<T | null>;
|
|
6
|
+
export declare function readManagedBitcoindStatus(paths: ReturnType<typeof resolveManagedServicePaths>): Promise<ManagedBitcoindObservedStatus | null>;
|
|
7
|
+
export declare function createIndexerSnapshotKey(appliedTip: ClientTip | null): string;
|
|
8
|
+
export declare function createManagedBitcoindCookieUnavailableMessage(cookieFile: string): string;
|
|
9
|
+
export declare function readCoreTipStatus(paths: ReturnType<typeof resolveManagedServicePaths>): Promise<CoreTipStatus>;
|
|
10
|
+
export declare function readAppliedTipStatus(databasePath: string): Promise<IndexedTipStatus>;
|
|
11
|
+
export declare function observeIndexerAppliedTip(state: IndexerDaemonRuntimeState, appliedTip: ClientTip | null, nowUnixMs: number): void;
|
|
12
|
+
export declare function deriveIndexerDaemonLeaseState(options: {
|
|
13
|
+
coreStatus: CoreTipStatus;
|
|
14
|
+
appliedTip: ClientTip | null;
|
|
15
|
+
hasSuccessfulCoreTipRefresh: boolean;
|
|
16
|
+
}): IndexerDaemonLeaseStateResult;
|
|
17
|
+
export declare function buildIndexerDaemonStatus(state: IndexerDaemonRuntimeState): ManagedIndexerDaemonStatus;
|
|
18
|
+
export declare function writeIndexerDaemonStatus(paths: ReturnType<typeof resolveManagedServicePaths>, state: IndexerDaemonRuntimeState): Promise<ManagedIndexerDaemonStatus>;
|
|
19
|
+
export declare function refreshIndexerDaemonStatus(options: {
|
|
20
|
+
databasePath: string;
|
|
21
|
+
paths: ReturnType<typeof resolveManagedServicePaths>;
|
|
22
|
+
state: IndexerDaemonRuntimeState;
|
|
23
|
+
}): Promise<ManagedIndexerDaemonStatus>;
|