Skip to content

Promise-first registry API (additive, dual callback/promise) [v1 0.x] #1528

Description

@ricardo-devis-agullo

Part of the OpenComponents v1 roadmap (see V1.md), Track 1 (non-breaking 0.x work). This lands additively on 0.x so that the eventual v1 clean break is "just remove the callbacks".

Problem Statement

As a registry operator, the only way to start, stop, or register plugins on a registry is by passing a Node-style callback. registry.start(cb), registry.close(cb), and registry.register(plugin, cb) are all callback-only — start is internally async but still reports its result ({ app, server }) through the callback and resolves to nothing useful. This forces callback-style boot code, makes error handling awkward, and blocks operators from writing modern async/await deployment scripts. It is also the single biggest obstacle to the promise-only registry API planned for v1.

Solution

As a registry operator, I want the registry lifecycle methods to return promises while still accepting the existing optional callback, so that I can await registry.start() today, get { app, server } back as a resolved value, and migrate to modern async boot code without any breaking change. Passing a callback continues to work exactly as before (and emits a soft deprecation notice pointing at the promise form), so nobody is forced to change anything on 0.x.

User Stories

  1. As a registry operator, I want await registry.start() to resolve to { app, server }, so that I can boot my registry with async/await instead of a callback pyramid.
  2. As a registry operator, I want registry.start() to reject with a real Error when startup fails, so that I can use try/catch for boot errors.
  3. As a registry operator, I want await registry.close() to resolve when the server and metadata store have fully closed, so that I can await graceful shutdown in tests and deploy scripts.
  4. As a registry operator, I want registry.close() on a not-yet-listening registry to behave the same as today (report the "not opened" condition), so that existing shutdown logic is unaffected.
  5. As a registry operator, I want await registry.register(plugin) to resolve once the plugin has been queued/registered, so that I can register plugins in an async flow.
  6. As a registry operator still using callbacks, I want registry.start(cb), close(cb), and register(plugin, cb) to keep working with identical semantics, so that my current code needs no changes on 0.x.
  7. As a registry operator passing a callback, I want a one-time, non-fatal deprecation notice that points me to the promise form, so that I know how to migrate before v1 removes callbacks.
  8. As a registry operator, I want mixing forms (e.g. awaiting the returned promise while also passing a callback) to not double-invoke my callback or double-settle the promise, so that behavior is predictable during migration.
  9. As a TypeScript registry operator, I want the return types of start/close/register to reflect the promise (and the optional callback overload), so that my editor guides me toward the modern form.
  10. As a maintainer, I want the promise and callback paths to share a single implementation, so that the two forms can never diverge in behavior.
  11. As a maintainer, I want the eventual v1 removal of the callback form to be a mechanical deletion, so that the promise API is already the tested, blessed path before v1.

Implementation Decisions

  • Modify the public registry factory (the module that returns the RegistryType object) so that start, close, and register each return a promise and accept the current optional trailing callback (dual API for 0.x).
  • RegistryType interface is updated so start resolves to { app, server }, close resolves to void, and register resolves to void; the callback overloads remain declared for back-compat.
  • The promise and callback code paths must be backed by a single internal implementation (the callback form is a thin adapter over the promise form, or vice-versa) to guarantee identical behavior.
  • Startup failures reject/callback with a genuine Error object rather than a raw string or err.msg.
  • When a caller supplies a callback, emit a single process deprecation notice (not per-call spam) steering them to the promise form. Reuse the existing deprecation-notice mechanism if the "Deprecation-warning pass" issue lands first; otherwise a minimal local emitter is acceptable and can be consolidated later.
  • No change to what start actually does (plugin init, router creation, repository init, app start, listen) — only how the result/errors are surfaced.
  • Out of the box, close continues to also close the metadata store as it does today.

Testing Decisions

  • Good tests here assert external behavior of the public Registry API only — that awaiting resolves/rejects with the right values and that the callback overload still fires with the same arguments — not internal wiring.
  • Test at the single highest seam: the public Registry factory / RegistryType object (start/close/register). This is an existing seam; no new seam is introduced.
  • Cover: promise resolves to { app, server }; promise rejects with Error on a forced startup failure; callback form still receives (null, { app, server }) and (err); close resolves after listening and reports "not opened" when never started; register resolves; a callback caller triggers exactly one deprecation notice; supplying both a callback and awaiting does not double-settle.
  • Prior art: existing registry start/close integration tests in the oc package test suite — extend them with promise-form assertions rather than adding a parallel harness.

Out of Scope

  • Removing the callback API (that is the v1 change, not 0.x).
  • Making storage/metadata/server adapter methods promise-only, and making plugin register internals promise-only (tracked separately).
  • The node SSR oc-client promise API — lives in a separate repository (opencomponents/oc-client) and is coordinated there.
  • Any change to the HTTP surface, config schema, or component contract.

Further Notes

  • This is intentionally additive and safe to ship at any point on 0.x.
  • Once shipped, the v1 work becomes a mechanical removal of the callback overloads and the deprecation notice.
  • The adapter-neutral error code work (replacing the hardcoded EXPRESS_ERROR event code) is related but tracked as a v1/registry item; this issue should not change event codes.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions