@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
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@cogcoin/client`
2
2
 
3
- `@cogcoin/client@1.1.9` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
3
+ `@cogcoin/client@1.1.10` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
4
4
 
5
5
  Use Node 22 or newer.
6
6
 
@@ -10,6 +10,8 @@ export declare class DefaultManagedBitcoindClient implements ManagedBitcoindClie
10
10
  constructor(client: SyncRecoveryClient, store: ClientStoreAdapter, node: ManagedBitcoindNodeHandle, rpc: BitcoinRpcClient, progress: ManagedProgressController, bootstrap: AssumeUtxoBootstrapController, startHeight: number, syncDebounceMs: number, dataDir: string, walletRootId: string, startupTimeoutMs: number | undefined, shutdownTimeoutMs: number | undefined, fetchImpl: typeof fetch | undefined);
11
11
  getTip(): Promise<import("../../types.js").ClientTip | null>;
12
12
  getState(): Promise<import("@cogcoin/indexer/types").IndexerState>;
13
+ readMirrorSnapshot(): Promise<import("../../types.js").ClientMirrorSnapshot>;
14
+ readMirrorDelta(afterHeight: number): Promise<import("../../types.js").ClientMirrorDelta>;
13
15
  applyBlock(block: BitcoinBlock): Promise<import("../../types.js").ApplyBlockResult>;
14
16
  rewindToHeight(height: number): Promise<import("../../types.js").ClientTip | null>;
15
17
  syncToTip(): Promise<SyncResult>;
@@ -64,6 +64,12 @@ export class DefaultManagedBitcoindClient {
64
64
  async getState() {
65
65
  return this.#client.getState();
66
66
  }
67
+ async readMirrorSnapshot() {
68
+ return this.#client.readMirrorSnapshot();
69
+ }
70
+ async readMirrorDelta(afterHeight) {
71
+ return this.#client.readMirrorDelta(afterHeight);
72
+ }
67
73
  async applyBlock(block) {
68
74
  return this.#client.applyBlock(block);
69
75
  }
@@ -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
+ import type { IndexerDaemonRuntimeState } from "./types.js";
5
+ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorCode: string): Promise<T>;
6
+ export declare function recordBackgroundFollowFailure(options: {
7
+ state: IndexerDaemonRuntimeState;
8
+ message: string;
9
+ writeStatus(): Promise<ManagedIndexerDaemonStatus>;
10
+ }): Promise<void>;
11
+ export declare function pauseBackgroundFollow(options: {
12
+ state: IndexerDaemonRuntimeState;
13
+ }): Promise<void>;
14
+ export declare function resumeBackgroundFollow(options: {
15
+ dataDir: string;
16
+ databasePath: string;
17
+ walletRootId: string;
18
+ paths: ReturnType<typeof resolveManagedServicePaths>;
19
+ state: IndexerDaemonRuntimeState;
20
+ genesisParameters: GenesisParameters;
21
+ forceResumeErrorEnv: string;
22
+ writeStatus(): Promise<ManagedIndexerDaemonStatus>;
23
+ }): Promise<void>;
@@ -0,0 +1,132 @@
1
+ import { openManagedBitcoindClientInternal } from "../client.js";
2
+ import { DEFAULT_SNAPSHOT_METADATA } from "../bootstrap.js";
3
+ import { openSqliteStore } from "../../sqlite/index.js";
4
+ import { normalizeCogcoinProcessingStartHeight } from "../processing-start-height.js";
5
+ import { createBootstrapProgress } from "../progress/formatting.js";
6
+ import { resolveManagedServicePaths } from "../service-paths.js";
7
+ import { readManagedBitcoindStatus } from "./status.js";
8
+ export async function withTimeout(promise, timeoutMs, errorCode) {
9
+ let timeoutId = null;
10
+ try {
11
+ return await Promise.race([
12
+ promise,
13
+ new Promise((_, reject) => {
14
+ timeoutId = setTimeout(() => reject(new Error(errorCode)), timeoutMs);
15
+ }),
16
+ ]);
17
+ }
18
+ finally {
19
+ if (timeoutId !== null) {
20
+ clearTimeout(timeoutId);
21
+ }
22
+ }
23
+ }
24
+ export async function recordBackgroundFollowFailure(options) {
25
+ const now = Date.now();
26
+ options.state.heartbeatAtUnixMs = now;
27
+ options.state.updatedAtUnixMs = now;
28
+ options.state.state = "failed";
29
+ options.state.lastError = options.message;
30
+ options.state.backgroundFollowError = options.message;
31
+ options.state.backgroundFollowActive = false;
32
+ options.state.bootstrapPhase = "error";
33
+ options.state.bootstrapProgress = {
34
+ ...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
35
+ blocks: options.state.coreBestHeight,
36
+ headers: options.state.coreBestHeight,
37
+ targetHeight: options.state.coreBestHeight,
38
+ message: options.message,
39
+ lastError: options.message,
40
+ updatedAt: now,
41
+ };
42
+ options.state.cogcoinSyncHeight = options.state.appliedTipHeight;
43
+ options.state.cogcoinSyncTargetHeight = options.state.coreBestHeight;
44
+ await options.writeStatus();
45
+ }
46
+ export async function pauseBackgroundFollow(options) {
47
+ const pendingResume = options.state.backgroundResumePromise;
48
+ options.state.backgroundResumePromise = null;
49
+ await pendingResume?.catch(() => undefined);
50
+ const client = options.state.backgroundClient;
51
+ const store = options.state.backgroundStore;
52
+ options.state.backgroundClient = null;
53
+ options.state.backgroundStore = null;
54
+ await client?.close().catch(() => undefined);
55
+ await store?.close().catch(() => undefined);
56
+ options.state.backgroundFollowError = null;
57
+ options.state.backgroundFollowActive = false;
58
+ options.state.bootstrapPhase = "paused";
59
+ options.state.bootstrapProgress = createBootstrapProgress("paused", DEFAULT_SNAPSHOT_METADATA);
60
+ options.state.cogcoinSyncHeight = options.state.appliedTipHeight;
61
+ options.state.cogcoinSyncTargetHeight = options.state.coreBestHeight;
62
+ }
63
+ export async function resumeBackgroundFollow(options) {
64
+ if (options.state.backgroundClient !== null) {
65
+ return;
66
+ }
67
+ if (options.state.backgroundResumePromise !== null) {
68
+ return options.state.backgroundResumePromise;
69
+ }
70
+ options.state.backgroundResumePromise = (async () => {
71
+ let store = null;
72
+ try {
73
+ const forcedResumeError = process.env[options.forceResumeErrorEnv]?.trim();
74
+ if (forcedResumeError) {
75
+ throw new Error(forcedResumeError);
76
+ }
77
+ const bitcoindStatus = await readManagedBitcoindStatus(options.paths);
78
+ store = await openSqliteStore({ filename: options.databasePath });
79
+ const openedStore = store;
80
+ const chain = bitcoindStatus?.chain ?? "main";
81
+ const startHeight = normalizeCogcoinProcessingStartHeight({
82
+ chain,
83
+ startHeight: bitcoindStatus?.startHeight,
84
+ genesisParameters: options.genesisParameters,
85
+ });
86
+ const client = await openManagedBitcoindClientInternal({
87
+ store: openedStore,
88
+ dataDir: options.dataDir,
89
+ chain,
90
+ startHeight,
91
+ walletRootId: options.walletRootId,
92
+ progressOutput: "none",
93
+ });
94
+ options.state.backgroundStore = openedStore;
95
+ options.state.backgroundClient = client;
96
+ options.state.backgroundFollowError = null;
97
+ options.state.backgroundFollowActive = true;
98
+ void client.startFollowingTip().catch(async (error) => {
99
+ if (options.state.backgroundClient !== client || options.state.backgroundStore !== openedStore) {
100
+ return;
101
+ }
102
+ options.state.backgroundClient = null;
103
+ options.state.backgroundStore = null;
104
+ options.state.backgroundFollowActive = false;
105
+ await client.close().catch(() => undefined);
106
+ await openedStore.close().catch(() => undefined);
107
+ const message = error instanceof Error ? error.message : String(error);
108
+ await recordBackgroundFollowFailure({
109
+ state: options.state,
110
+ message,
111
+ writeStatus: options.writeStatus,
112
+ }).catch(() => undefined);
113
+ });
114
+ }
115
+ catch (error) {
116
+ await store?.close().catch(() => undefined);
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ await recordBackgroundFollowFailure({
119
+ state: options.state,
120
+ message,
121
+ writeStatus: options.writeStatus,
122
+ }).catch(() => undefined);
123
+ throw error;
124
+ }
125
+ })();
126
+ try {
127
+ await options.state.backgroundResumePromise;
128
+ }
129
+ finally {
130
+ options.state.backgroundResumePromise = null;
131
+ }
132
+ }
@@ -0,0 +1,12 @@
1
+ import type { ManagedIndexerDaemonProbeResult } from "../managed-runtime/types.js";
2
+ import type { IndexerDaemonClient, ManagedIndexerDaemonOwnership, ManagedIndexerDaemonServiceLifetime } from "./types.js";
3
+ interface IndexerDaemonClientCloseOptions {
4
+ serviceLifetime: ManagedIndexerDaemonServiceLifetime;
5
+ ownership: ManagedIndexerDaemonOwnership;
6
+ shutdownOwnedDaemon?: (() => Promise<void>) | null;
7
+ requestTimeoutMs?: number;
8
+ resumeBackgroundFollowRequestTimeoutMs?: number;
9
+ }
10
+ export declare function createIndexerDaemonClient(socketPath: string, closeOptions?: IndexerDaemonClientCloseOptions | null): IndexerDaemonClient;
11
+ export declare function probeIndexerDaemonAtSocket(socketPath: string, expectedWalletRootId: string): Promise<ManagedIndexerDaemonProbeResult<IndexerDaemonClient>>;
12
+ export {};
@@ -0,0 +1,137 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import net from "node:net";
3
+ import { mapIndexerDaemonTransportError, mapIndexerDaemonValidationError, validateIndexerDaemonStatus, } from "../managed-runtime/indexer-policy.js";
4
+ const INDEXER_DAEMON_REQUEST_TIMEOUT_MS = 15_000;
5
+ const INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS = 35_000;
6
+ export function createIndexerDaemonClient(socketPath, closeOptions = null) {
7
+ let closed = false;
8
+ async function sendRequest(request) {
9
+ return new Promise((resolve, reject) => {
10
+ const socket = net.createConnection(socketPath);
11
+ let buffer = "";
12
+ let settled = false;
13
+ const finish = (handler) => {
14
+ if (settled) {
15
+ return;
16
+ }
17
+ settled = true;
18
+ socket.destroy();
19
+ handler();
20
+ };
21
+ socket.setTimeout(request.method === "ResumeBackgroundFollow"
22
+ ? closeOptions?.resumeBackgroundFollowRequestTimeoutMs ?? INDEXER_DAEMON_RESUME_BACKGROUND_FOLLOW_REQUEST_TIMEOUT_MS
23
+ : closeOptions?.requestTimeoutMs ?? INDEXER_DAEMON_REQUEST_TIMEOUT_MS);
24
+ socket.on("connect", () => {
25
+ socket.write(`${JSON.stringify(request)}\n`);
26
+ });
27
+ socket.on("data", (chunk) => {
28
+ buffer += chunk.toString("utf8");
29
+ let newlineIndex = buffer.indexOf("\n");
30
+ while (newlineIndex >= 0) {
31
+ const line = buffer.slice(0, newlineIndex);
32
+ buffer = buffer.slice(newlineIndex + 1);
33
+ if (line.trim().length === 0) {
34
+ newlineIndex = buffer.indexOf("\n");
35
+ continue;
36
+ }
37
+ let response;
38
+ try {
39
+ response = JSON.parse(line);
40
+ }
41
+ catch (error) {
42
+ finish(() => reject(error));
43
+ return;
44
+ }
45
+ if (response.id !== request.id) {
46
+ newlineIndex = buffer.indexOf("\n");
47
+ continue;
48
+ }
49
+ if (!response.ok) {
50
+ finish(() => reject(new Error(response.error ?? "indexer_daemon_request_failed")));
51
+ return;
52
+ }
53
+ finish(() => resolve(response.result));
54
+ return;
55
+ }
56
+ });
57
+ socket.on("timeout", () => {
58
+ finish(() => reject(new Error("indexer_daemon_request_timeout")));
59
+ });
60
+ socket.on("error", (error) => {
61
+ finish(() => reject(error));
62
+ });
63
+ socket.on("end", () => {
64
+ if (!settled) {
65
+ finish(() => reject(new Error("indexer_daemon_connection_closed")));
66
+ }
67
+ });
68
+ });
69
+ }
70
+ return {
71
+ getStatus() {
72
+ return sendRequest({
73
+ id: randomUUID(),
74
+ method: "GetStatus",
75
+ });
76
+ },
77
+ openSnapshot() {
78
+ return sendRequest({
79
+ id: randomUUID(),
80
+ method: "OpenSnapshot",
81
+ });
82
+ },
83
+ readSnapshot(token) {
84
+ return sendRequest({
85
+ id: randomUUID(),
86
+ method: "ReadSnapshot",
87
+ token,
88
+ });
89
+ },
90
+ async closeSnapshot(token) {
91
+ await sendRequest({
92
+ id: randomUUID(),
93
+ method: "CloseSnapshot",
94
+ token,
95
+ });
96
+ },
97
+ async resumeBackgroundFollow() {
98
+ await sendRequest({
99
+ id: randomUUID(),
100
+ method: "ResumeBackgroundFollow",
101
+ });
102
+ },
103
+ async close() {
104
+ if (closed) {
105
+ return;
106
+ }
107
+ closed = true;
108
+ if (closeOptions === null || closeOptions.serviceLifetime !== "ephemeral" || closeOptions.ownership === "attached") {
109
+ return;
110
+ }
111
+ await closeOptions.shutdownOwnedDaemon?.();
112
+ },
113
+ };
114
+ }
115
+ export async function probeIndexerDaemonAtSocket(socketPath, expectedWalletRootId) {
116
+ const client = createIndexerDaemonClient(socketPath);
117
+ try {
118
+ const status = await client.getStatus();
119
+ try {
120
+ validateIndexerDaemonStatus(status, expectedWalletRootId);
121
+ return {
122
+ compatibility: "compatible",
123
+ status,
124
+ client,
125
+ error: null,
126
+ };
127
+ }
128
+ catch (error) {
129
+ await client.close().catch(() => undefined);
130
+ return mapIndexerDaemonValidationError(error, status);
131
+ }
132
+ }
133
+ catch (error) {
134
+ await client.close().catch(() => undefined);
135
+ return mapIndexerDaemonTransportError(error);
136
+ }
137
+ }
@@ -0,0 +1,30 @@
1
+ import type { ManagedIndexerDaemonProbeResult } from "../managed-runtime/types.js";
2
+ import type { ManagedIndexerDaemonObservedStatus } from "../types.js";
3
+ import type { CoherentIndexerSnapshotLease, IndexerDaemonClient, IndexerDaemonStopResult, ManagedIndexerDaemonServiceLifetime } from "./types.js";
4
+ export type { IndexerDaemonCompatibility } from "../managed-runtime/types.js";
5
+ export declare const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
6
+ export type IndexerDaemonProbeResult = ManagedIndexerDaemonProbeResult<IndexerDaemonClient>;
7
+ export declare function probeIndexerDaemon(options: {
8
+ dataDir: string;
9
+ walletRootId?: string;
10
+ }): Promise<IndexerDaemonProbeResult>;
11
+ export declare function readSnapshotWithRetry(daemon: IndexerDaemonClient, expectedWalletRootId: string): Promise<CoherentIndexerSnapshotLease>;
12
+ export declare function readObservedIndexerDaemonStatus(options: {
13
+ dataDir: string;
14
+ walletRootId?: string;
15
+ }): Promise<ManagedIndexerDaemonObservedStatus | null>;
16
+ export declare function attachOrStartIndexerDaemon(options: {
17
+ dataDir: string;
18
+ databasePath: string;
19
+ walletRootId?: string;
20
+ startupTimeoutMs?: number;
21
+ shutdownTimeoutMs?: number;
22
+ serviceLifetime?: ManagedIndexerDaemonServiceLifetime;
23
+ ensureBackgroundFollow?: boolean;
24
+ expectedBinaryVersion?: string | null;
25
+ }): Promise<IndexerDaemonClient>;
26
+ export declare function stopIndexerDaemonService(options: {
27
+ dataDir: string;
28
+ walletRootId?: string;
29
+ shutdownTimeoutMs?: number;
30
+ }): Promise<IndexerDaemonStopResult>;
@@ -0,0 +1,153 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import { acquireFileLock, FileLockBusyError } from "../../wallet/fs/lock.js";
5
+ import { buildManagedIndexerStatusFromSnapshotHandle, validateIndexerSnapshotHandle, validateIndexerSnapshotPayload, } from "../managed-runtime/indexer-policy.js";
6
+ import { attachOrStartManagedIndexerRuntime } from "../managed-runtime/indexer-runtime.js";
7
+ import { readJsonFileIfPresent } from "../managed-runtime/status.js";
8
+ import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "../service-paths.js";
9
+ import { createIndexerDaemonClient, probeIndexerDaemonAtSocket, } from "./client.js";
10
+ import { DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS, DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS, sleep, stopIndexerDaemonService as stopIndexerDaemonServiceInternal, stopIndexerDaemonServiceWithLockHeld, waitForIndexerDaemon, } from "./process.js";
11
+ export const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
12
+ const INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE = "indexer_daemon_background_follow_not_active";
13
+ export async function probeIndexerDaemon(options) {
14
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
15
+ const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
16
+ return probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
17
+ }
18
+ export async function readSnapshotWithRetry(daemon, expectedWalletRootId) {
19
+ let lastError = null;
20
+ for (let attempt = 0; attempt < 2; attempt += 1) {
21
+ const handle = await daemon.openSnapshot();
22
+ try {
23
+ validateIndexerSnapshotHandle(handle, expectedWalletRootId);
24
+ const payload = await daemon.readSnapshot(handle.token);
25
+ validateIndexerSnapshotPayload(payload, handle, expectedWalletRootId);
26
+ return {
27
+ payload,
28
+ status: buildManagedIndexerStatusFromSnapshotHandle(handle),
29
+ };
30
+ }
31
+ catch (error) {
32
+ lastError = error;
33
+ if (!(error instanceof Error)
34
+ || (error.message !== "indexer_daemon_snapshot_invalid" && error.message !== "indexer_daemon_snapshot_rotated")
35
+ || attempt > 0) {
36
+ throw error;
37
+ }
38
+ }
39
+ finally {
40
+ await daemon.closeSnapshot(handle.token).catch(() => undefined);
41
+ }
42
+ }
43
+ throw lastError instanceof Error ? lastError : new Error("indexer_daemon_snapshot_invalid");
44
+ }
45
+ export async function readObservedIndexerDaemonStatus(options) {
46
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
47
+ const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
48
+ return readJsonFileIfPresent(paths.indexerDaemonStatusPath);
49
+ }
50
+ export async function attachOrStartIndexerDaemon(options) {
51
+ const requestBackgroundFollow = async (client, observedStatus = null) => {
52
+ if (options.ensureBackgroundFollow !== true) {
53
+ return client;
54
+ }
55
+ if (observedStatus?.backgroundFollowActive === true) {
56
+ return client;
57
+ }
58
+ await client.resumeBackgroundFollow();
59
+ const status = await client.getStatus();
60
+ if (status.backgroundFollowActive !== true) {
61
+ throw new Error(INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE);
62
+ }
63
+ return client;
64
+ };
65
+ const walletRootId = options.walletRootId ?? UNINITIALIZED_WALLET_ROOT_ID;
66
+ const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
67
+ const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS;
68
+ const serviceLifetime = options.serviceLifetime ?? "persistent";
69
+ const expectedBinaryVersion = options.expectedBinaryVersion ?? null;
70
+ const startDaemon = async () => {
71
+ await mkdir(paths.indexerServiceRoot, { recursive: true });
72
+ const daemonEntryPath = fileURLToPath(new URL("../indexer-daemon-main.js", import.meta.url));
73
+ const spawnOptions = serviceLifetime === "ephemeral"
74
+ ? {
75
+ stdio: "ignore",
76
+ }
77
+ : {
78
+ detached: true,
79
+ stdio: "ignore",
80
+ };
81
+ const child = spawn(process.execPath, [
82
+ daemonEntryPath,
83
+ `--data-dir=${options.dataDir}`,
84
+ `--database-path=${options.databasePath}`,
85
+ `--wallet-root-id=${walletRootId}`,
86
+ ], {
87
+ ...spawnOptions,
88
+ });
89
+ if (serviceLifetime !== "ephemeral") {
90
+ child.unref();
91
+ }
92
+ try {
93
+ await waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs);
94
+ }
95
+ catch (error) {
96
+ if (child.pid !== undefined) {
97
+ try {
98
+ process.kill(child.pid, "SIGTERM");
99
+ }
100
+ catch {
101
+ // ignore shutdown failures while unwinding startup errors
102
+ }
103
+ }
104
+ throw error;
105
+ }
106
+ return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
107
+ serviceLifetime,
108
+ ownership: "started",
109
+ shutdownOwnedDaemon: async () => {
110
+ await stopIndexerDaemonService({
111
+ dataDir: options.dataDir,
112
+ walletRootId,
113
+ shutdownTimeoutMs: options.shutdownTimeoutMs,
114
+ });
115
+ },
116
+ });
117
+ };
118
+ return attachOrStartManagedIndexerRuntime({
119
+ ...options,
120
+ walletRootId,
121
+ startupTimeoutMs,
122
+ expectedBinaryVersion,
123
+ }, {
124
+ getPaths: (runtimeOptions) => resolveManagedServicePaths(runtimeOptions.dataDir, runtimeOptions.walletRootId),
125
+ probeDaemon: async (runtimeOptions, runtimePaths) => probeIndexerDaemonAtSocket(runtimePaths.indexerDaemonSocketPath, runtimeOptions.walletRootId),
126
+ requestBackgroundFollow,
127
+ closeClient: async (client) => {
128
+ await client.close();
129
+ },
130
+ acquireStartLock: async (runtimeOptions, runtimePaths) => acquireFileLock(runtimePaths.indexerDaemonLockPath, {
131
+ purpose: "indexer-daemon-start",
132
+ walletRootId: runtimeOptions.walletRootId,
133
+ dataDir: runtimeOptions.dataDir,
134
+ databasePath: runtimeOptions.databasePath,
135
+ }),
136
+ startDaemon: async () => startDaemon(),
137
+ stopWithLockHeld: async (runtimeOptions, _runtimePaths, processId) => stopIndexerDaemonServiceWithLockHeld({
138
+ dataDir: runtimeOptions.dataDir,
139
+ walletRootId: runtimeOptions.walletRootId,
140
+ shutdownTimeoutMs: runtimeOptions.shutdownTimeoutMs,
141
+ paths: resolveManagedServicePaths(runtimeOptions.dataDir, runtimeOptions.walletRootId),
142
+ processId,
143
+ }),
144
+ isLockBusyError: (error) => error instanceof FileLockBusyError,
145
+ sleep,
146
+ });
147
+ }
148
+ export async function stopIndexerDaemonService(options) {
149
+ return stopIndexerDaemonServiceInternal({
150
+ ...options,
151
+ shutdownTimeoutMs: options.shutdownTimeoutMs ?? DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS,
152
+ });
153
+ }
@@ -0,0 +1,35 @@
1
+ import type { ManagedIndexerDaemonStatus } from "../types.js";
2
+ import { resolveManagedServicePaths } from "../service-paths.js";
3
+ import type { IndexerDaemonStopResult } from "./types.js";
4
+ export declare const DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS = 30000;
5
+ export declare const DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS = 5000;
6
+ export declare function sleep(ms: number): Promise<void>;
7
+ export declare function isIndexerDaemonProcessAlive(pid: number | null): Promise<boolean>;
8
+ export declare function waitForIndexerDaemonProcessExit(pid: number, timeoutMs: number, errorCode: string): Promise<void>;
9
+ export declare function clearIndexerDaemonRuntimeArtifacts(paths: ReturnType<typeof resolveManagedServicePaths>): Promise<void>;
10
+ export declare function ignoreIndexerDaemonProcessNotFound(error: unknown): void;
11
+ export declare function stopIndexerDaemonServiceWithLockHeld(options: {
12
+ dataDir: string;
13
+ walletRootId?: string;
14
+ shutdownTimeoutMs?: number;
15
+ paths?: ReturnType<typeof resolveManagedServicePaths>;
16
+ processId?: number | null;
17
+ }): Promise<IndexerDaemonStopResult>;
18
+ export declare function waitForIndexerDaemon(dataDir: string, walletRootId: string, timeoutMs: number): Promise<void>;
19
+ export declare function stopIndexerDaemonService(options: {
20
+ dataDir: string;
21
+ walletRootId?: string;
22
+ shutdownTimeoutMs?: number;
23
+ }): Promise<IndexerDaemonStopResult>;
24
+ export declare function shutdownIndexerDaemonForTesting(options: {
25
+ dataDir: string;
26
+ walletRootId?: string;
27
+ }): Promise<void>;
28
+ export declare function readIndexerDaemonStatusForTesting(options: {
29
+ dataDir: string;
30
+ walletRootId?: string;
31
+ }): Promise<ManagedIndexerDaemonStatus | null>;
32
+ export declare function writeIndexerDaemonStatusForTesting(options: {
33
+ dataDir: string;
34
+ walletRootId?: string;
35
+ }, status: ManagedIndexerDaemonStatus): Promise<void>;