Cross-Devframe Services

ctx.services lets one devframe expose a typed, namespaced capability visible to every devframe. Two tiers: in-process services (provide/get) share live objects between devframes; wire services also register RPC and advertise to RPC clients.

ctx.services lets one devframe expose a typed, namespaced capability visible to every devframe. Two tiers: in-process services (provide/get) share live objects between devframes; wire services also register RPC and advertise to RPC clients.

The Node-Side API reference collects the host methods and the definition/descriptor fields as lookup tables.

Providing a service

Augment DevframeServicesRegistry with your id and type, then provide in setup:

export interface SourcesService {
  register: (entry: SourceEntry) => () => void
}

declare module 'devframe' {
  interface DevframeServicesRegistry {
    'my-plugin:sources': SourcesService
  }
}

export function setup(ctx: DevframeNodeContext) {
  ctx.services.provide('my-plugin:sources', createSourcesService())
}

Service ids prefix the provider's id (<devframe-id>:<service>), unique per context — a second provide() under a taken id throws DF0037. provide() returns a revoke; guard idempotent setup with has(id).

Consuming a service

A consumer loads it with a types-only import (import type {} from '@my-org/my-plugin'), then reaches it in setup:

ctx.services.whenAvailable('my-plugin:sources', (sources) => {
  sources.register({ id: 'other-plugin:state', data: () => state })
})

Prefer whenAvailable over get: it fires immediately if provided, else on provide, and re-fires on revoke/re-provide. get(id) returns the implementation or undefined (ids without an augmentation as unknown).

The DevframeServicesHost API

DevframeServicesHost exposes provide(id, service) => revoke, get(id), has(id), whenAvailable(id, cb) => unsubscribe, and keys(); plus a wire tier install(input, options?) => Promise<api | undefined> and ready() => Promise<void>.

Wire services

A wire service is a shared node-side capability, an npm module.

Shipping one

A service package's default export is a factory returning DevframeServiceDefinition:

export default function createOpenService(options?: OpenServiceOptions): DevframeServiceDefinition<OpenServiceApi, OpenServiceOptions> {
  return {
    package: '@devframes/service-open', // the registry key
    version: '1.0.0', // advertised; checked against declared ranges
    scope: 'devframes:service:open', // RPC namespace
    options,
    setup(ctx, { options }) {
      // `ctx` is pre-scoped: this registers `devframes:service:open:open-in-editor`
      ctx.rpc.register({ name: 'open-in-editor', handler: input => api.openInEditor(input) })
      return api // the node API served from ctx.services.get(package)
    },
  }
}

Two declaration merges type it: RPC ids into DevframeRpcServerFunctions, package → scope into DevframeServicesScopeRegistry.

Declaring

Services are declarative: a devframe lists what it consumes; a hub, shared ones on initHub. The adapter resolves each package — for a devframe, against its own dependencies via importMetaUrl.

// devframe side — on the definition
defineDevframe({
  importMetaUrl: import.meta.url, // resolution base for the declared packages
  services: [
    { package: '@devframes/service-open' },
    { package: '@devframes/service-shiki', version: '^1', options: { langs: ['vue'] } },
  ],
})

A hub lists shared, constructed services on initHub({ services: [createShikiService(opts)] }).

Entries are optional; uninstalled packages are skipped (has() === false). Mark one required: true to fail hard: DF0067 on a missing package, DF0068 on an unsatisfied version range (otherwise a range mismatch only warns, DF0069).

Lifecycle: ready before setup

The hub constructs every declared service (all devframes plus initHub) once — deep-merging option sets (objects recurse, arrays union-dedupe, scalars later-win; override with mergeOptions) — before any setup(ctx) runs, so setup consumes services synchronously via ctx.services.get(pkg).

For a runtime-only service, ctx.services.install(input) builds immediately; re-installing a constructed package returns the existing API, warning DF0066 if options can't merge.

Feature-detecting on the RPC client

Installed services are advertised via devframe:services shared state, mirrored on the RPC client's rpc.services:

const rpc = await connectDevframe()

if (rpc.services.has('@devframes/service-open')) {
  const open = rpc.services.get('@devframes/service-open')!
  await open.rpc.call('open-in-editor', { path })
}

A reactive UI subscribes via rpc.services.state(). has()/get()/keys() are synchronous snapshots, empty before the first sync; each handle carries the version and meta.

Built-in services

Three first-party wire services ship ready to install, each with its own page under Add-ons › Services:

Services, RPC, or shared state?

  • Services — node-to-node in-process live references, never crossing a wire.
  • RPC — browser-to-node: an RPC client calls a named function.
  • Shared state — serializable data synced node side ↔ RPC clients.

A service serves other devframes, RPC surfaces or coding agents, a wire service both.