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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
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), andregistry.register(plugin, cb)are all callback-only —startis internallyasyncbut 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 modernasync/awaitdeployment 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 on0.x.User Stories
await registry.start()to resolve to{ app, server }, so that I can boot my registry with async/await instead of a callback pyramid.registry.start()to reject with a realErrorwhen startup fails, so that I can usetry/catchfor boot errors.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.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.await registry.register(plugin)to resolve once the plugin has been queued/registered, so that I can register plugins in an async flow.registry.start(cb),close(cb), andregister(plugin, cb)to keep working with identical semantics, so that my current code needs no changes on0.x.start/close/registerto reflect the promise (and the optional callback overload), so that my editor guides me toward the modern form.Implementation Decisions
RegistryTypeobject) so thatstart,close, andregistereach return a promise and accept the current optional trailing callback (dual API for0.x).RegistryTypeinterface is updated sostartresolves to{ app, server },closeresolves tovoid, andregisterresolves tovoid; the callback overloads remain declared for back-compat.Errorobject rather than a raw string orerr.msg.startactually does (plugin init, router creation, repository init, app start, listen) — only how the result/errors are surfaced.closecontinues to also close the metadata store as it does today.Testing Decisions
RegistryAPI only — that awaiting resolves/rejects with the right values and that the callback overload still fires with the same arguments — not internal wiring.Registryfactory /RegistryTypeobject (start/close/register). This is an existing seam; no new seam is introduced.{ app, server }; promise rejects withErroron a forced startup failure; callback form still receives(null, { app, server })and(err);closeresolves after listening and reports "not opened" when never started;registerresolves; a callback caller triggers exactly one deprecation notice; supplying both a callback and awaiting does not double-settle.ocpackage test suite — extend them with promise-form assertions rather than adding a parallel harness.Out of Scope
0.x).registerinternals promise-only (tracked separately).oc-clientpromise API — lives in a separate repository (opencomponents/oc-client) and is coordinated there.Further Notes
0.x.EXPRESS_ERRORevent code) is related but tracked as a v1/registry item; this issue should not change event codes.