@spider-mesh/core
v2.0.45
Published
Lightweight microservice framework for typescript, auto discovery, load-balancing, fault-torrent, multiple transporters
Readme
Spider Mesh Core
@spider-mesh/core is the runtime-agnostic Spider Mesh package.
It provides:
- local microservice registration with decorators
- typed remote service linking through proxies
- RPC, discovery, and pubsub transporter contracts
- a
Registryfor remote peer state and RPC routing - a
SpiderMeshruntime for local services, transporters, and node metadata - NestJS helper adapters
Concrete transport implementations live in companion packages such as @spider-mesh/tcp and @spider-mesh/ws, or in your own custom transporters.
Install
bun add @spider-mesh/core rxjs reflect-metadataFor TypeScript projects using decorators, enable decorator metadata in your compiler settings.
Runtime Setup
Create a registry when you need remote peer discovery and RPC routing.
import { Registry, SpiderMesh } from '@spider-mesh/core'
const registry = new Registry()
const mesh = new SpiderMesh(registry)Register transporter instances on the runtime:
import { Http2Pubsub, Http2Rpc, UdpDiscovery } from '@spider-mesh/tcp'
mesh.registerTransporter(new UdpDiscovery())
mesh.registerTransporter(new Http2Rpc())
mesh.registerTransporter(new Http2Pubsub())Transporter capability is inferred by instance shape:
send()=> RPC transporterpublish()=> pubsub transporterbroadcast()=> discovery transporter
Local Services
Use @Microservice() on local service classes and instantiate them normally.
import { BeforeMicroserviceOnline, Microservice } from '@spider-mesh/core'
@Microservice({ version: '1.0.0' })
export class UserService {
private ready = false
@BeforeMicroserviceOnline()
async warmup() {
this.ready = true
}
async getUser(id: string) {
if (!this.ready) throw new Error('Service not ready')
return { id, name: 'Ada' }
}
}
new UserService()@Microservice() emits the constructed instance into the shared LOCAL_SERVICES$ stream. SpiderMesh subscribes to that stream and adds the service to local node metadata.
Remote Services
Create a typed remote client with RemoteServiceLinker.link().
import { RemoteServiceLinker } from '@spider-mesh/core'
type UserServiceContract = {
getUser(id: string): Promise<{ id: string; name: string }>
}
const users = RemoteServiceLinker.link<UserServiceContract>(mesh, {
service: 'UserService',
})
await users.wait()
const user = await users.getUser('42')Remote methods return RxJS observables and can also be awaited.
const user = await users.getUser('42')
users.getUser('42').subscribe(value => {
console.log(value)
})Waiting and watching
await users.wait(nodes => nodes.length > 0)
users.watch().subscribe(nodes => {
console.log(nodes.map(node => node.node_id))
})
console.log(users.nodes)Fan-out calls
users.__batch__getUser('42').subscribe(result => {
console.log(result)
})Each emission is either { node, data } or { node, error }.
RPC Options
The current RpcOptions shape is:
type RpcOptions<T = any> = {
service: string
method: string
args: any[]
fallback?: T
timeout?: number
retry?: number
node_id?: string
transporter?: string | { name?: string }
}Examples:
import { firstValueFrom } from 'rxjs'
await users.set({ timeout: 3000, retry: 2 }).getUser('42')
await firstValueFrom(mesh.callRemoteService({
service: 'UserService',
method: 'getUser',
args: ['42'],
transporter: 'Http2Rpc',
}))transporter may be:
- a registered transporter name string
- a class constructor (e.g.
Http2Rpc) - an object with a
nameproperty
Events
Use SpiderMesh.linkEvent() to bind a topic by event class name.
class UserCreatedEvent {
constructor(
public readonly id: string,
public readonly email: string,
) {}
}
const userCreated = mesh.linkEvent(UserCreatedEvent)
await userCreated.publish(new UserCreatedEvent('42', '[email protected]'))
const sub = userCreated.listen().subscribe(event => {
console.log(event.id)
})
sub.unsubscribe()Current event behavior:
publish()fans out through all registered pubsub transporterslisten()merges all transporter listeners into one shared stream- first subscribe adds the topic to local node metadata
- last unsubscribe removes the topic from local node metadata
- discovery transporters rebroadcast node metadata after topic changes
Registry
Registry stores remote peer and RPC routing state.
Current public methods:
getPeer(nodeId)upsertPeer(node)removePeer(nodeId)listPeers(service?)watch(service?)pickRpcNode(service, { node_id? })getRpcTransporterName(service)listTopicNodes(topic)
Transporter Contracts
The contract source of truth is src/types.ts.
RPC transporter
type RpcTransporter = Observable<RpcEvent> & {
linkRegistry?(registry: Registry): void
send(data: RpcRequestPacket | RpcCancelPacket | RpcResponsePacket): Promise<{ cancel: () => void }>
}send() returns a cancel function. For request packets, calling cancel() sends a RpcCancelPacket to the destination node, which causes the provider to stop any running stream. For response and cancel packets the returned cancel is a no-op.
SpiderMesh calls cancel() automatically when a subscriber unsubscribes from a stream that has not yet completed.
Packet types
type RpcRequestPacket = {
kind: 'request'
request_id: string
service: string
method: string
args: any[]
sender_node_id: string
destination_node_id?: string // explicit target; transporter falls back to registry round-robin when omitted
}
type RpcResponsePacket = {
kind: 'response'
request_id: string
data?: any
error?: SpiderMeshError | { code?: string; message: string }
completed?: boolean
destination_node_id?: string
}
type RpcCancelPacket = {
kind: 'cancel'
request_id: string
destination_node_id?: string
}Pubsub transporter
type PubsubTransporter = Observable<PubsubEvent> & {
publish<T>(topic: string, data: T): Promise<void>
listen<T>(topic: string): Observable<T>
linkRegistry?(registry: Registry): void
}Discovery transporter
type DiscoveryTransporter = Observable<DiscoveryEvent> & {
linkRegistry?(registry: Registry): void
broadcast(data: MdnsMessage<NodeMetadata>): Promise<void>
}NestJS Helpers
The package exports:
NestJSExposeMicroservice(factory, metadata?)NestJSLinkMicroservice(factory, transporter?)NestJSLinkEvent(factory)
NestJSLinkMicroservice(factory, transporter?) forwards the optional transporter selector into RemoteServiceLinker.link().
Companion Packages
Use companion packages when you need concrete transport implementations.
@spider-mesh/tcp@spider-mesh/ws
Keep runtime logic in @spider-mesh/core and import concrete transporters explicitly from the companion package.
Tests And Validation
The repository includes mock e2e coverage for:
- RPC routing by transporter selector
- async
RemoteServiceLinker.wait()behavior - topic metadata lifecycle for
linkEvent() - RPC routing across isolated child processes
Run tests with:
bun run test:e2eBuild with:
bun run buildNestJS Integration
Register SpiderMesh as a provider
import { Module } from '@nestjs/common'
import { Registry, SpiderMesh } from '@spider-mesh/core'
import { Http2Pubsub, Http2Rpc, UdpDiscovery } from '@spider-mesh/tcp'
@Module({
providers: [
{
provide: SpiderMesh,
useFactory: () => {
const registry = new Registry()
const mesh = new SpiderMesh(registry)
mesh.registerTransporter(new UdpDiscovery())
mesh.registerTransporter(new Http2Rpc())
mesh.registerTransporter(new Http2Pubsub())
return mesh
},
},
],
exports: [SpiderMesh],
})
export class MeshModule {}Expose a NestJS service as a microservice
import { Injectable, Module } from '@nestjs/common'
import { NestJSExposeMicroservice } from '@spider-mesh/core'
@Injectable()
export class BillingService {
async charge(orderId: string) {
return { orderId, status: 'ok' }
}
}
@Module({
providers: [
BillingService,
NestJSExposeMicroservice(BillingService, { boundedContext: 'billing' }),
],
})
export class BillingModule {}Inject a remote service proxy in NestJS
import { Inject, Injectable, Module } from '@nestjs/common'
import { NestJSLinkMicroservice } from '@spider-mesh/core'
class BillingService {
charge(orderId: string): Promise<{ orderId: string; status: string }> {
throw new Error('typing only')
}
}
@Injectable()
export class CheckoutService {
constructor(
@Inject(BillingService)
private readonly billing: BillingService,
) {}
async checkout(orderId: string) {
return this.billing.charge(orderId)
}
}
@Module({
providers: [
NestJSLinkMicroservice(BillingService),
CheckoutService,
],
})
export class CheckoutModule {}Pass a transporter only when you need to force a specific RPC transporter:
NestJSLinkMicroservice(BillingService, 'Http2Rpc')Inject an event binding in NestJS
import { Inject, Injectable, Module } from '@nestjs/common'
import { NestJSLinkEvent } from '@spider-mesh/core'
class UserCreatedEvent {
constructor(public readonly id: string) {}
}
@Injectable()
export class AuditService {
constructor(
@Inject(UserCreatedEvent)
private readonly userCreated: {
publish(data: UserCreatedEvent): Promise<void>
listen(): any
},
) {}
}
@Module({
providers: [
NestJSLinkEvent(UserCreatedEvent),
AuditService,
],
})
export class AuditModule {}Error Model
The core defines these error codes for RPC flows:
MICROSERVICE_OFFLINEMICROSERVICE_NOT_FOUNDMICROSERVICE_RPC_TIMEOUT
Environment Variables
SPIDERMESH_NAMESPACE: namespace of the current node, defaultdefaultSPIDERMESH_NODE_HOSTNAME: optional hostname attached to node metadata
Helpers
The package also exports:
LimitConcurrency(limit)andLimitConcurrentRunning(limit)for throttling async method executionMicroserviceErrorfor common RPC error codes
Notes
- This package is ESM-only.
- Repository source uses emitted
.jsrelative specifiers. LOCAL_SERVICES$is process-global inside one process.- Local services are registered when their class instances are constructed.
- Service identity is based on the class name.
- Event topic identity is based on the event class name.
- RPC target selection is round-robin unless you force
node_id. SpiderMeshowns RPC stream lifecycle, timeout, retry, and cancel behavior. When a subscriber unsubscribes before a stream completes,SpiderMeshcalls thecancel()function returned bytransporter.send(), which causes the provider to stop the running Observable.- Transporters focus on byte transport, pubsub topic IO, and discovery broadcasts.
- The root package entry intentionally focuses on runtime-agnostic APIs and shared contracts.
- If an AI agent is uncertain which import to use, prefer
@spider-mesh/corefirst, then opt into a companion transport package such as@spider-mesh/tcpor@spider-mesh/wsonly when a concrete transport is needed.
