Fleet Health
Stadium-board health across a meshed pack of nodes — without letting a down neighbour take your local /health with it.
Per-node readiness (withReadiness → GET /health / NodeStatus) stays local. FleetHealth is a separate glass on the same mesh Telemetry uses: leaf for this node, fleet fields that fold peers with Effect Exit kept so silence never lies.
Declare the glass
One tag. Distribute it across the droplets you actually run.
class class DropletEastclass DropletEast {
key: Identifier;
Service: {
protocol: Context.Service.Shape<typeof RpcClient.Protocol>;
};
}
DropletEast extends import NodeNode.Tag<class DropletEastclass DropletEast {
key: Identifier;
Service: {
protocol: Context.Service.Shape<typeof RpcClient.Protocol>;
};
}
DropletEast>()("app/DropletEast") {}
class class DropletWestclass DropletWest {
key: Identifier;
Service: {
protocol: Context.Service.Shape<typeof RpcClient.Protocol>;
};
}
DropletWest extends import NodeNode.Tag<class DropletWestclass DropletWest {
key: Identifier;
Service: {
protocol: Context.Service.Shape<typeof RpcClient.Protocol>;
};
}
DropletWest>()("app/DropletWest") {}
class class MeshHealthMeshHealth extends import FleetHealthFleetHealth.Tag<class MeshHealthMeshHealth>()().pipe(
import HyperlinkHyperlink.distributed([class DropletEastclass DropletEast {
key: Identifier;
Service: {
protocol: Context.Service.Shape<typeof RpcClient.Protocol>;
};
of: (this: void, self: NodeProtocol) => NodeProtocol;
context: (self: NodeProtocol) => Context<DropletEast>;
use: (f: (service: NodeProtocol) => Effect<A, E, R>) => Effect<A, E, DropletEast | R>;
useSync: (f: (service: NodeProtocol) => A) => Effect<A, never, DropletEast>;
Identifier: Identifier;
stack: string | undefined;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
url: undefined;
path: undefined;
kind: undefined;
logs: unknown;
onConflict: OnConflict;
}
DropletEast, class DropletWestclass DropletWest {
key: Identifier;
Service: {
protocol: Context.Service.Shape<typeof RpcClient.Protocol>;
};
of: (this: void, self: NodeProtocol) => NodeProtocol;
context: (self: NodeProtocol) => Context<DropletWest>;
use: (f: (service: NodeProtocol) => Effect<A, E, R>) => Effect<A, E, DropletWest | R>;
useSync: (f: (service: NodeProtocol) => A) => Effect<A, never, DropletWest>;
Identifier: Identifier;
stack: string | undefined;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
url: undefined;
path: undefined;
kind: undefined;
logs: unknown;
onConflict: OnConflict;
}
DropletWest]),
) {}Serve with peers + optional readiness
Pass the same readiness rows /health uses when you want the leaf to match NodeStatus. Discharge the mesh with Hyperlink.peersLayer (or FleetHealth.alone for a single node).
const const readiness: Effect.Effect<
{
key: string
kind: string
ready: boolean
}[],
never,
never
>
const readiness: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
readiness = import EffectEffect.const succeed: <{
key: string;
kind: string;
ready: boolean;
}[]>(value: {
key: string;
kind: string;
ready: boolean;
}[]) => Effect.Effect<{
key: string;
kind: string;
ready: boolean;
}[], never, never>
Creates an Effect that always succeeds with a given value.
When to use
Use when an effect should complete successfully with a specific value without any errors
or external dependencies.
Example (Creating a successful effect)
import { Effect } from "effect"
// Creating an effect that represents a successful scenario
//
// ┌─── Effect<number, never, never>
// ▼
const success = Effect.succeed(42)
succeed([
{ key: stringkey: "app/Cache", kind: stringkind: "hyperlink-ts/Hyperlink", ready: booleanready: true },
])
import FleetHealthFleetHealth.serve(class MeshHealthMeshHealth, { readiness: Effect.Effect<
{
key: string
kind: string
ready: boolean
}[],
never,
never
>
(property) readiness: {
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
}
readiness }).pipe(
import LayerLayer.const provide: <any>(that: any) => <A, E, R>(self: Layer.Layer<A, E, R>) => Layer.Layer<A, any, any> (+3 overloads)Feeds the output services of the dependency layer into the requirements of
this layer, returning a layer that only provides the services from this layer.
When to use
Use when you need to hide an implementation dependency layer from callers.
Details
In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is
built first and is used to satisfy the requirements of serviceLayer.
Example (Providing layer dependencies)
import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)
provide(import HyperlinkHyperlink.peersLayer(class MeshHealthMeshHealth, class DropletEastclass DropletEast {
key: Identifier;
Service: {
protocol: Context.Service.Shape<typeof RpcClient.Protocol>;
};
of: (this: void, self: NodeProtocol) => NodeProtocol;
context: (self: NodeProtocol) => Context<DropletEast>;
use: (f: (service: NodeProtocol) => Effect.Effect<A, E, R>) => Effect.Effect<A, E, DropletEast | R>;
useSync: (f: (service: NodeProtocol) => A) => Effect.Effect<A, never, DropletEast>;
Identifier: Identifier;
stack: string | undefined;
pipe: { <A>(this: A): A; <A, B = never>(this: A, ab: (_: A) => B): B; <A, B = never, C = never>(this: A, ab: (_: A) => B, bc: (_: B) => C): C; <A, B = never, C = never, D = never>(this: A, ab: (_: A) => B, bc: (_: B) => C, cd: (_: C) => D): D; <…;
toString: () => string;
toJSON: () => unknown;
url: undefined;
path: undefined;
kind: undefined;
logs: unknown;
onConflict: OnConflict;
}
DropletEast)),
)Read the board
| Field | Scope | Meaning |
|---|---|---|
local | leaf | This nodes ok / degraded + resource rows |
byNode | fleet | Reachable (peers local) or Unreachable (Exit failure) |
status | fleet | ok · degraded · partial (any unreachable) |
Unreachable ≠ ready: false. A cold cache is degraded; a dead peer is unreachable.
const const glass: anyglass = yield* class MeshHealthMeshHealth
const const local: anylocal = yield* const glass: anyglass.local
// local: FleetHealth.LocalHealth — { status: "ok" | "degraded", resources: … }
const const byNode: anybyNode = yield* const glass: anyglass.byNode
// byNode: Record<string, FleetHealth.NodeReport>
// Reachable → { _tag: "Reachable", status, resources }
// Unreachable → { _tag: "Unreachable" }
const const status: anystatus = yield* const glass: anyglass.status
// status: "ok" | "degraded" | "partial"
Peers only expose the leaf (local). byNode / status are Hyperlink.fleet — excluded from fan-out so a fold cant re-aggregate an aggregate. When you need to keep every peer Exit yourself, use MultiNode.combineByNodeExit (FleetHealth does); combineByNode / Hyperlink.fleetHealth still skip-omit for metric-style folds.
What not to do
Do not fold peers inside withReadiness — that cascades one nodes failure into neighbours /health. Standards forbid it; FleetHealth exists so the fold is explicit and client-shaped.
Runnable form: pnpm run example:fleet-health-glass. See also Telemetry and Fleets & Peers.