@moonwall/cli
Version:
Testing framework for the Moon family of projects
171 lines (168 loc) • 5.76 kB
JavaScript
// src/internal/effect/NodeReadinessService.ts
import { Socket } from "@effect/platform";
import * as NodeSocket from "@effect/platform-node/NodeSocket";
import { createLogger } from "@moonwall/util";
import { Context, Deferred, Effect, Fiber, Layer, Schedule } from "effect";
// src/internal/effect/errors.ts
import { Data } from "effect";
var PortDiscoveryError = class extends Data.TaggedError("PortDiscoveryError") {
};
var NodeLaunchError = class extends Data.TaggedError("NodeLaunchError") {
};
var NodeReadinessError = class extends Data.TaggedError("NodeReadinessError") {
};
var ProcessError = class extends Data.TaggedError("ProcessError") {
};
var StartupCacheError = class extends Data.TaggedError("StartupCacheError") {
};
var FileLockError = class extends Data.TaggedError("FileLockError") {
};
// src/internal/effect/NodeReadinessService.ts
var logger = createLogger({ name: "NodeReadinessService" });
var debug = logger.debug.bind(logger);
var NodeReadinessService = class extends Context.Tag("NodeReadinessService")() {
};
var sendRpcRequest = (method) => Effect.flatMap(
Deferred.make(),
(responseDeferred) => Effect.flatMap(
Socket.Socket,
(socket) => Effect.flatMap(socket.writer, (writer) => {
debug(`Checking method: ${method}`);
const request = new TextEncoder().encode(
JSON.stringify({
jsonrpc: "2.0",
id: Math.floor(Math.random() * 1e4),
method,
params: []
})
);
const handleMessages = socket.runRaw((data) => {
try {
const message = typeof data === "string" ? data : new TextDecoder().decode(data);
debug(`Got message for ${method}: ${message.substring(0, 100)}`);
const response = JSON.parse(message);
debug(`Parsed response: jsonrpc=${response.jsonrpc}, error=${response.error}`);
if (response.jsonrpc === "2.0" && !response.error) {
debug(`Method ${method} succeeded!`);
return Deferred.succeed(responseDeferred, true);
}
} catch (e) {
debug(`Parse error for ${method}: ${e}`);
}
return Effect.void;
});
return Effect.flatMap(
Effect.fork(handleMessages),
(messageFiber) => Effect.flatMap(
writer(request),
() => (
// Wait for either:
// 1. Deferred resolving with response (success)
// 2. Timeout (fails with error)
// Also check if the message fiber has failed
Deferred.await(responseDeferred).pipe(
Effect.timeoutFail({
duration: "2 seconds",
onTimeout: () => new Error("RPC request timed out waiting for response")
}),
// After getting result, check if fiber failed and prefer that error
Effect.flatMap(
(result) => Fiber.poll(messageFiber).pipe(
Effect.flatMap((pollResult) => {
if (pollResult._tag === "Some") {
const exit = pollResult.value;
if (exit._tag === "Failure") {
return Effect.failCause(exit.cause);
}
}
return Effect.succeed(result);
})
)
)
)
)
).pipe(
Effect.ensuring(Fiber.interrupt(messageFiber)),
Effect.catchAll(
(cause) => Effect.fail(
new NodeReadinessError({
cause,
port: 0,
// Will be filled in by caller
attemptsExhausted: 1
})
)
)
)
);
})
)
);
var attemptReadinessCheck = (config) => Effect.logDebug(
`Attempting readiness check on port ${config.port}, isEthereum: ${config.isEthereumChain}`
).pipe(
Effect.flatMap(
() => Effect.scoped(
Effect.flatMap(sendRpcRequest("system_chain"), (systemChainOk) => {
if (systemChainOk) {
return Effect.succeed(true);
}
if (config.isEthereumChain) {
return sendRpcRequest("eth_chainId");
}
return Effect.succeed(false);
})
).pipe(
Effect.timeoutFail({
duration: "3 seconds",
onTimeout: () => new NodeReadinessError({
cause: new Error("Readiness check timed out"),
port: config.port,
attemptsExhausted: 1
})
}),
Effect.catchAll(
(cause) => Effect.fail(
new NodeReadinessError({
cause,
port: config.port,
attemptsExhausted: 1
})
)
)
)
)
);
var checkReadyWithRetryInternal = (config) => {
const maxAttempts = config.maxAttempts || 200;
return attemptReadinessCheck(config).pipe(
Effect.retry(
Schedule.fixed("50 millis").pipe(Schedule.compose(Schedule.recurs(maxAttempts - 1)))
),
Effect.catchAll(
(error) => Effect.fail(
new NodeReadinessError({
cause: error,
port: config.port,
attemptsExhausted: maxAttempts
})
)
)
);
};
var checkReadyWithRetry = (config) => {
return checkReadyWithRetryInternal(config).pipe(
Effect.provide(NodeSocket.layerWebSocket(`ws://localhost:${config.port}`))
);
};
var NodeReadinessServiceLive = Layer.succeed(NodeReadinessService, {
checkReady: checkReadyWithRetry
});
var makeNodeReadinessServiceTest = (socketLayer) => Layer.succeed(NodeReadinessService, {
checkReady: (config) => checkReadyWithRetryInternal(config).pipe(Effect.provide(socketLayer))
});
export {
NodeReadinessService,
NodeReadinessServiceLive,
makeNodeReadinessServiceTest
};