nupp.services

Typed service definitions and named implementations within one Lua state.

A declaration module owns one invariant Service handle. Register checked loaders or discover them through static package metadata; neither action executes a provider. Setup selects names before requiring consuming facades. Each facade assembles its implementation during top-level initialization and retains its actual methods for direct calls.

local services = require("nupp.services")
interface Codec
    readonly encode: function(value: string): string
end
local service: services.Service<Codec> = services.define("example.codec", 1)
service:register("plain", function(): Codec
    return {encode = function(value: string): string return value end}
end)
service:select("plain")
local codec = service:require()
assert(codec.encode("hello") == "hello")

The variable annotation supplies generic inference. Service is invariant: register accepts T while lookup returns T, so a handle cannot be reassigned to a different provider interface. Import canonical handles rather than redefining an existing ID. Provider exports may have extra members, but their required members, generics, ownership, and suspension behavior must satisfy the declared contract.

Successful named loads are cached by name. Unnamed resolution fixes the default selection, including optional absence. Failed loads are not cached as successful values; diagnostics include provider identity and dependency cycles. Named lookup does not itself select or freeze the default. All state is local to one Lua state.

Use assembly only while requiring a facade, then call retained operations. Applications must not perform service lookups in hot calls or resource cleanup. Worker configuration carries catalog names and setup modules, never provider instances or captured closures. See Service Providers for packaging and setup module examples.

Module contents

Types

TypeKindDescription
MemberinterfaceRuntime field-shape entry derived from a canonical provider interface.
ServicerecordA named, versioned contract with checked lazy loaders and cached values.

Functions

FunctionKindDescription
definefunctionDefines the unique handle owned by a service's declaration module.
dialectfunctionReturns the artifact's fixed target dialect.
setupWorkersfunctionRegisters an ordinary setup module for every child worker state.
workerConfigurationfunctionCaptures child initialization as loadable setup and catalog selections.

Types#

Memberinterface#

interface Member
    name: string
    kinds: {string}
end

Runtime field-shape entry derived from a canonical provider interface. Kinds use the reflected type vocabulary and may include nil for optional fields.

Fields

name#
name: string
kinds#
kinds: {string}

Servicerecord#

record Service<T>
    readonly id: string
    readonly api: integer
    function register(self: Service<T>, name: string, loader: function(): T): nil end

    function select(self: Service<T>, name: string): nil end

    function list(self: Service<T>): {string} end

    function lookup(self: Service<T>, name: string?): T? end

    function assemble(self: Service<T>, defaultLoader: function(): T, checkImplementation: (function(T): nil)?): T end

    function assembleOptional(
        self: Service<T>,
        defaultLoader: function(): T?,
        checkImplementation: (function(T): nil)?
    ): T? end

    function require(self: Service<T>, name: string?): T end
end

A named, versioned contract with checked lazy loaders and cached values.

Define this once in a declaration module. Registration and selection are setup operations; resolve during a facade's require-time initialization. The default freezes after successful resolution, while named implementations retain their independent cached identities.

Type parameters

NameDescription
T

Methods

register#
register: function register(self: Service<T>, name: string, loader: function(): T): nil

Registers a checked loader without executing it.

Names are unique within this handle. Registration does not select a default. A successful named load runs the loader once and caches its result; a failed load may be retried. Additional registration cannot change a frozen default.

Arguments
NameTypeDescription
selfService<T>
namestring
loaderfunction(): T
Returns
TypeDescription
nil
Raises
  • when the name is empty or already registered

select#
select: function select(self: Service<T>, name: string): nil

Chooses the name used by unnamed resolution.

Selection may precede registration. An absent selected name fails when the facade resolves; it does not fall back to a built-in default. Before resolution, another select may replace the choice. Once resolution starts, reselection is rejected, including from a provider's own loader.

Arguments
NameTypeDescription
selfService<T>
namestring
Returns
TypeDescription
nil
Raises
  • when the service is already resolving or resolved

list#
list: function list(self: Service<T>): {string}

Returns a fresh, sorted list of registered provider names.

Listing executes no loader and does not select or freeze the default. The result includes catalog registrations even if none has been loaded.

Arguments
NameTypeDescription
selfService<T>
Returns
TypeDescription
{string}
lookup#
lookup: function lookup(self: Service<T>, name: string?): T?

Loads a named implementation or resolves the unnamed default.

A missing explicit name returns nil without freezing the default. An unnamed call uses the selected name or facade default, validates the value, and freezes that outcome, including nil. A selected name that is absent is an error. Cached successful values retain identity on subsequent lookups.

Load and validation failures clear the in-progress state and may be retried. Runtime shape validation complements static typing; it cannot prove a Lua function's ownership, suspension, or behavioral guarantees.

Arguments
NameTypeDescription
selfService<T>
namestring?
Returns
TypeDescription
T?
Raises
  • when a selected provider is absent, cyclic, or fails to load

assemble#
assemble: function assemble(self: Service<T>, defaultLoader: function(): T, checkImplementation: (function(T): nil)?): T

Resolves a required facade with explicit selection taking precedence.

Call once in module top-level code and retain the result. defaultLoader performs ordinary host checks and method assembly. checkImplementation validates representation or other semantic requirements before publication. A previously resolved value is checked against a newly supplied validator.

Arguments
NameTypeDescription
selfService<T>
defaultLoaderfunction(): T
checkImplementation(function(T): nil)?
Returns
TypeDescription
T
Raises
  • when the service cannot resolve an implementation

assembleOptional#
assembleOptional: function assembleOptional(self: Service<T>, defaultLoader: function(): T?, checkImplementation: (function(T): nil)?): T?

Resolves an optional facade and freezes absence as well as presence.

defaultLoader may return nil when this host has no implementation. Explicit selection still requires that named provider to exist. Later registration does not turn a successfully resolved absence into an implementation.

Arguments
NameTypeDescription
selfService<T>
defaultLoaderfunction(): T?
checkImplementation(function(T): nil)?
Returns
TypeDescription
T?
require#
require: function require(self: Service<T>, name: string?): T

Returns a cached or newly loaded implementation that must be present.

Uses lookup's named/default rules, then rejects nil. Named require does not choose the default. Retain the result at module load time for direct calls.

Arguments
NameTypeDescription
selfService<T>
namestring?
Returns
TypeDescription
T
Raises
  • when no implementation is available or its loader fails

Fields

id#
id: string

Stable service identity shared with catalog declarations.

api#
api: integer

Positive contract API version required of every registered catalog entry.

Functions#

definefunction#

function define<T>(id: string, api: integer, defaultLoader: (function(): T?)?, members: {Member}?): Service<T>

Defines the unique handle owned by a service's declaration module.

id is nonempty and api is a positive integer. T is inferred from the receiving Service annotation or loader. defaultLoader is optional; a facade can supply assembly later. members describes runtime field kinds derived from the canonical interface. Extra provider members are permitted.

Matching catalog entries register loaders after checking service API and target compatibility. Loading one verifies that its contract module exports this exact handle. Definition itself never requires an implementation module.

Type parameters

NameDescription
T

Arguments

NameTypeDescription
idstring
apiinteger
defaultLoader(function(): T?)?
members{Member}?

Returns

TypeDescription
Service<T>

Raises

  • when the identity or API version is invalid or already defined

dialectfunction#

function dialect(): string

Returns the artifact's fixed target dialect.

Facades may use this for ordinary default assembly and representation checks. Changing provider names cannot change the dialect or generated calling convention.

Returns

TypeDescription
string

setupWorkersfunction#

function setupWorkers(moduleName: string): nil

Registers an ordinary setup module for every child worker state.

Include the module in the artifact's entry modules. It must register/select providers before requiring their consumers. Repeated names are deduplicated in registration order. Call before loading the workers facade, which captures and freezes initialization. Runtime-only registrations must be recreated by setup.

Arguments

NameTypeDescription
moduleNamestring

Returns

TypeDescription
nil

Raises

  • when the module name is invalid or worker initialization is fixed

workerConfigurationfunction#

function workerConfiguration(): string

Captures child initialization as loadable setup and catalog selections.

This is the worker-provider integration point. The first call fixes the setup list, then emits setup requires followed by catalog-backed named selections in service-ID order. Each destination runs it before loading consumer modules. Repeated calls return the same configuration; no provider objects or closures cross the lane boundary. An explicit setup module must recreate runtime-only selections that the artifact catalog cannot name.

Returns

TypeDescription
string

Raises

  • when a runtime-only selection has no explicit worker setup