Last updated

Service Providers are a powerful feature in Supranim that allows you to manage your application's services in a clean and organized way. They are responsible for registering services, and bootstrapping your application.

🔗Types of Service Providers

Supranim provides multiple types of service providers. A service provider can be registered as a Singleton, Global, ThreadService, ThreadPoolService, WebService or UnixService for standalone microservices and thread-based servers.

🔗Singleton Service Provider

A Singleton Service Provider is a service that is instantiated only once and shared across the entire application. It is useful for services that maintain state or need to be accessed globally, such as a database connection or a cache.

import supranim/core/services

initService Counter[Singleton]:
  description = "In-process hit counter"
  state do:
    type Counter = object
      hits: int
  api do:
    proc counter*(): ptr Counter =
      ## Return the singleton instance, creating it on first use
      getCounterInstance(
        proc(instance: ptr Counter) =
          {.gcsafe.}:
            instance[] = Counter(hits: 0)
      )

    proc hit*() =
      ## Record one hit
      inc counter()[].hits

    proc hits*(): int =
      ## Total hits recorded
      counter()[].hits

🔗Global Service Provider

A Global Service Provider is a namespace-style service without an instance type: only the state/api blocks are emitted, with no get<Name>Instance accessor. It is useful for grouping pure functions and constants under a service name.

import std/options
import pkg/mimedb
import supranim/core/services

initService Mime[Global]:
  description = "Shared MIME lookup helpers"
  state do:
    const DefaultMimeType = "application/octet-stream"
  api do:
    proc mimeType*(ext: string): string =
      ## Return the MIME type for `ext`, falling back to a default
      getMimeType(ext).get(DefaultMimeType)

    proc isKnownExtension*(ext: string): bool =
      ## True when `ext` exists in the MIME database
      isExtension(ext)

    proc isCompressibleType*(mimeType: string): bool =
      ## True when `mimeType` is marked as compressible
      isCompressible(getMimeInfo(mimeType))

🔗ThreadService Provider

A ThreadService runs user code in its own thread for as long as it wants and talks to the main application through the application-owned Chan[ServiceMsg]. It cannot be built standalone. (ChannelService is a deprecated alias of ThreadService.)

import std/strutils
import supranim/application
import supranim/core/services

initService Worker[ThreadService]:
  description = "Uppercase worker thread"
  queueSize = 32
  autoStart = false

  thread do:
    let ch = getWorkerChannel()
    while true:
      let msg = ch[].recv()
      if isServiceStop(msg):
        break
      ch[].send(ServiceMsg(id: msg.id, action: "done:" & msg.action,
        payload: msg.payload.toUpperAscii()))

  api do:
    proc submit*(app: Application, action, payload: string,
        id: int64 = 0): bool =
      ## Queue one job for the worker. Returns false when the queue is full.
      app.sendServiceMsg("Worker", action, payload, id)

When autoStart is false, the service does not start on its own. Start and stop it with the generated start<Name>Service and stop<Name>Service procs:

# generated function as `start<ServiceName>Service`
app().startWorkerService()

assert app().hasServiceChan("Worker")
assert app().submit("upper", "hello world", 1)

let reply = app().recvServiceMsg("Worker")
assert app().stopWorkerService()

The string name ("Worker") is only used for channel lookup and messaging (hasServiceChan, sendServiceMsg, recvServiceMsg). Starting itself uses the typed proc.

🔗ThreadPoolService Provider

A ThreadPoolService owns a TaskManager (see Background Tasks & Queues): N anonymous workers run submitted jobs while callbacks fire serialized on the pool dispatch thread, and a private scheduler thread drives delayed, repeating and wall-clock tasks. Jobs are submitted via the api handles, there is no worker body and no routes, ws or thread do: block. It cannot be built standalone.

import supranim/application
import supranim/core/services

initService Jobs[ThreadPoolService]:
  description = "CPU-bound job pool example"
  poolSize = 2
  autoStart = false

  shared do:
    type FibJob = object
      n: int

  api do:
    proc fib(n: int): int =
      ## Iterative Fibonacci, our CPU-bound demo job
      var a = 0
      var b = 1
      for _ in 2..n:
        let t = a + b
        a = b
        b = t
      b

    proc submitFib*(n: int, done: proc(res: int) {.closure.}): JobId =
      ## Run `fib(n)` off the calling thread. Returns a `JobId` for
      ## `cancelJob` (`JobId(0)` when the pool is down). `done` fires
      ## on the pool dispatch thread when the result is ready.
      getJobsPool().submit(
        job = proc(): int {.closure.} = fib(n),
        cb = done,
        onError = proc(err: ref CatchableError) {.closure.} =
          echo "job failed: ", err.msg)

    proc cancelFib*(id: JobId): bool =
      ## Drop a still-queued `fib` job: true means it will never run
      ## (silent). A running or finished job returns false and
      ## delivers normally.
      getJobsPool().cancelJob(id)

🔗WebService Provider

A WebService Provider is a standalone service that runs its own web server. It is useful for creating REST API microservices that need to run independently from the main application.

import supranim/microservice

initService Greeter[WebService]:
  description = "Hello microservice example"
  port = 8765

  routes do:
    get "/hello":
      req.respond(200, %*{"message": "hello from a standalone service"})

    get "/hello/{name:slug}":
      req.respond(200, %*{"message": "hello " & req.params["name"]})

A WebService can also run a standalone WebSocket server with a ws do: block instead of routes do::

import supranim/microservice

initService Echo[WebService]:
  description = "WebSocket echo example"
  port = 8766

  ws do:
    wss.onOpen(proc(ws: WsConnection) {.closure.} =
      echo "ws client connected"
    )
    wss.onMessage(proc(ws: WsConnection, kind: WsFrameKind,
        data: openArray[byte]) {.closure.} =
      if kind == wsText:
        ws.sendText("echo: " & cast[string](data))
    )
    wss.onClose(proc(ws: WsConnection, code: int,
        reason: string) {.closure.} =
      echo "ws client left: ", code, " ", reason
    )

🔗UnixService Provider

A UnixService Provider is the same as a WebService Provider, but served over a Unix domain socket instead of TCP, which makes it ideal for fast local IPC. It supports socketPath and socketMode fields.

import supranim/microservice

initService LocalGreeter[UnixService]:
  description = "Local IPC microservice example"
  socketPath = "/tmp/supranim_example.sock"

  routes do:
    get "/ping":
      req.respond(200, %*{"pong": true})