152 lines
5.0 KiB
JavaScript
152 lines
5.0 KiB
JavaScript
/**
|
|
* Network provider registry (DC-140, specs/PRIVATE-NETWORK-SPEC.md §3).
|
|
*
|
|
* Single resolution point between feature modules and providers: no feature
|
|
* code may import a provider module directly — they call getActiveProvider().
|
|
*
|
|
* State is deliberately process-local and defaults to INACTIVE: no provider
|
|
* is active until the setup wizard (DC-152) connects one. Until then,
|
|
* getActiveProvider()/getCapabilities() throw ProviderNotActiveError.
|
|
*
|
|
* registerProvider(factory) — factory is a function ({ getCoordClient,
|
|
* localManager, execFileImpl } = {}) → provider instance. The tailscale
|
|
* factory defaults to lazily binding the existing singletons
|
|
* (src/context getCoordClient pattern + src/managers/tailscale-manager),
|
|
* but every dependency is injectable so tests stay hermetic.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { ProviderNotActiveError } = require('./errors');
|
|
const { CONTRACT_METHODS } = require('./provider');
|
|
|
|
// id → factory(provider deps) → provider instance
|
|
const _factories = new Map();
|
|
let _activeId = null;
|
|
let _activeProvider = null;
|
|
|
|
/** Lately-bound defaults (required lazily so jest.mock stays effective). */
|
|
function _defaultDeps() {
|
|
/* eslint-disable global-require -- lazy on purpose: keeps registry require-time
|
|
side effects at zero and respects jest module mocks. */
|
|
return {
|
|
localManager: require('../managers/tailscale-manager'),
|
|
execFileImpl: null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Register a provider factory under its id.
|
|
* @param {string} id - provider id, e.g. 'tailscale' (must match provider.id())
|
|
* @param {Function} factory - (deps) => provider instance
|
|
* @returns {void}
|
|
*/
|
|
function registerProvider(id, factory) {
|
|
if (typeof id !== 'string' || !id) {
|
|
throw new TypeError('registerProvider: id must be a non-empty string');
|
|
}
|
|
if (typeof factory !== 'function') {
|
|
throw new TypeError('registerProvider: factory must be a function');
|
|
}
|
|
_factories.set(id, factory);
|
|
// If this id is currently active, drop the cached instance so the next
|
|
// getActiveProvider() rebuilds from the (possibly new) factory.
|
|
if (_activeId === id) _activeProvider = null;
|
|
}
|
|
|
|
/** Build (or rebuild) the instance for `_activeId` from its factory. */
|
|
function _buildActive() {
|
|
const factory = _factories.get(_activeId);
|
|
const provider = factory ? factory(_defaultDeps()) : null;
|
|
if (!_isContractProvider(provider)) {
|
|
throw new Error('Provider "' + _activeId + '" does not satisfy the NetworkProvider contract');
|
|
}
|
|
_activeProvider = provider;
|
|
return _activeProvider;
|
|
}
|
|
|
|
/**
|
|
* True when the id refers to a registered provider whose instances satisfy
|
|
* the NetworkProvider contract surface (all CONTRACT_METHODS present).
|
|
*/
|
|
function _isContractProvider(p) {
|
|
if (!p) return false;
|
|
return CONTRACT_METHODS.every((m) => typeof p[m] === 'function');
|
|
}
|
|
|
|
/**
|
|
* Activate a registered provider. Rebuilding the instance resets any state
|
|
* the previous one carried.
|
|
* @param {string} id
|
|
* @returns {object} the newly active provider instance
|
|
* @throws {Error} when the id is not registered, or the instance built from
|
|
* it does not satisfy the documented contract.
|
|
*/
|
|
function setActiveProvider(id) {
|
|
const factory = _factories.get(id);
|
|
if (!factory) {
|
|
throw new Error('Unknown network provider: ' + String(id));
|
|
}
|
|
const provider = factory(_defaultDeps());
|
|
if (!_isContractProvider(provider)) {
|
|
throw new Error('Provider "' + id + '" does not satisfy the NetworkProvider contract');
|
|
}
|
|
if (typeof provider.id !== 'function' || provider.id() !== id) {
|
|
throw new Error('Provider "' + id + '" reports a different id');
|
|
}
|
|
_activeId = id;
|
|
_activeProvider = provider;
|
|
return _activeProvider;
|
|
}
|
|
|
|
/**
|
|
* The active provider instance. Feature modules' ONLY entry point into
|
|
* provider functionality.
|
|
* @throws {ProviderNotActiveError} when no provider has been activated
|
|
*/
|
|
function getActiveProvider() {
|
|
if (!_activeId) {
|
|
throw new ProviderNotActiveError(
|
|
'No active network provider. Complete the Private Network setup wizard first.'
|
|
);
|
|
}
|
|
// Factory may have been re-registered since activation → rebuild lazily.
|
|
return _activeProvider || _buildActive();
|
|
}
|
|
|
|
/**
|
|
* Capability flags of the active provider (spec §3): feature modules check
|
|
* these and degrade cleanly when a capability is absent.
|
|
* @returns {{invites:boolean, splitDnsApi:boolean, perPeerFirewall:boolean, selfHosted:boolean}}
|
|
* @throws {ProviderNotActiveError} when no provider has been activated
|
|
*/
|
|
function getCapabilities() {
|
|
return getActiveProvider().capabilities();
|
|
}
|
|
|
|
/** List registered provider ids (setup wizard mode picker). */
|
|
function listProviderIds() {
|
|
return [..._factories.keys()];
|
|
}
|
|
|
|
/** Deactivate (setup wizard disconnect / tests). Next getActiveProvider() throws. */
|
|
function reset() {
|
|
_activeId = null;
|
|
_activeProvider = null;
|
|
}
|
|
|
|
/** Currently active provider id, or null — read-only introspection. */
|
|
function getActiveProviderId() {
|
|
return _activeId;
|
|
}
|
|
|
|
module.exports = {
|
|
registerProvider,
|
|
setActiveProvider,
|
|
getActiveProvider,
|
|
getCapabilities,
|
|
listProviderIds,
|
|
getActiveProviderId,
|
|
reset,
|
|
};
|