UNPKG

@moonwall/cli

Version:

Testing framework for the Moon family of projects

171 lines (168 loc) 5.76 kB
// 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 };