Dependency injection patterns
How @vibrant/lib and its consuming apps structure swappable dependencies. Each
pattern below links to the real file that best demonstrates it.
1. Provider interfaces
Behavior is defined by an interface in providers/interfaces/**; concrete
implementations live in providers/. Consumers depend on the interface type,
never the concrete class.
- Interface:
packages/lib/src/providers/interfaces/auth/IUsersProvider.ts - Implementations:
packages/lib/src/providers/PGUsersProvider.ts,packages/lib/src/providers/KeycloakUsersProvider.ts
2. Factories for swappable implementations
A factory function picks and assembles the right implementation(s) behind an interface return type, so callers never construct concrete classes directly.
createApiService()inpackages/lib/src/services/api/APIFactory.tsbuilds anIAPIServicefromAUTH_PROVIDER/API_PROVIDERconfig. When auth and data providers differ, it composes them withCompositeUsersProvider/CompositeApiServicerather than hard-coding one provider pair.createGatewaySetupClient()inpackages/lib/src/services/gateway/GatewaySetupClientFactory.tspicks a BLE, REST, or mockIGatewaySetupClientfrom aGatewayConnection.kind.
3. Constructor injection for the PostgREST transport
Providers take their transport client as an optional constructor parameter,
defaulting to the real singleton. This is what makes them unit-testable and
lets a provider be pointed at a different client (e.g. cloudPostgrest
instead of the gateway-routing postgrest) without changing its code.
class PGDevicesProvider implements IDevicesProvider {
constructor(
private readonly db: PostgrestDataClient = postgrest,
private readonly cloudDb: PostgrestDataClient = cloudPostgrest,
) {}
}
See packages/lib/src/providers/PGDevicesProvider.ts and the
PostgrestDataClient / PostgrestAuthClient / PostgrestLike /
PostgrestCloudClient interfaces in packages/lib/src/utils/postgrest.ts
that describe the minimal surface each provider actually needs.
Tests inject a fake instead of the real client:
const fakeDb: PostgrestDataClient = { get: jest.fn(), post: jest.fn(), ... };
const provider = new PGDevicesProvider(fakeDb);
Full example: packages/lib/src/providers/__tests__/PGDevicesProvider.test.ts.
Run it with npm run test --workspace @vibrant/lib.
4. App bootstrap through exported functions, not singletons
Apps configure @vibrant/lib by calling its exported functions
(createApiService, configureApiEndpoints), never by importing an internal
singleton and calling its mutator methods directly.
configureApiEndpoints()inpackages/lib/src/utils/postgrest.tsis the one seam for setting the cloud PostgREST base/auth URLs at startup.apps/partner/App.tsxcalls it duringinitializePartnerEnvironment()instead of reaching intocloudPostgrest.setBaseUrl(...)directly.
5. Mocks/fakes through the same factory seam
A fake implementation is added as another branch of the existing factory, not as a separate ad hoc code path the app has to select manually.
MockGatewaySetupClient(packages/lib/src/services/gateway/MockGatewaySetupClient.ts) implementsIGatewaySetupClientand is selected viacreateGatewaySetupClient({ kind: "mock" })-- the same call callers use for BLE/REST. App-specific mock detection (env var,isMockDeviceSetupEnabled()) stays inapps/partner/app/services/deviceSetup/mockDeviceSetup.ts, since that policy is app-specific; the swappable implementation itself lives inpackages/libnext to the real adapters.
6. Public package surface vs. optional native peer deps
@vibrant/lib's barrel (packages/lib/src/index.ts) re-exports BLE,
firmware, and social-auth modules that require optional native peer
dependencies (react-native-ble-plx, @playerdata/react-native-mcu-manager,
react-native-zeroconf, etc.). If an app imports the barrel without those
packages installed, Metro fails to resolve them at bundle time -- even for
code paths the app never calls, since Metro's dependency graph is built
statically.
Apps that don't have every optional peer dep installed use deep imports
instead of the barrel, e.g. @vibrant/lib/src/services/api/APIFactory
instead of @vibrant/lib. See apps/partner/App.tsx and
apps/partner/app/services/api/APIFactory.ts.
packages/lib/src/types/native-modules.d.ts provides ambient type shims so
typecheck stays green without installing every optional peer dependency.
Rule of thumb: before wiring a new shared factory branch into an app,
confirm the app has that branch's native peer deps installed. GatewaySetupClientFactory.ts
imports BLE code unconditionally at module scope (via GatewaySetupBLEClient.ts
-> VibrantBLEClient.ts), so any import from that file -- even just to reach
the REST or mock branch -- requires react-native-ble-plx to be installed in
the importing app. apps/partner/package.json added it for exactly this
reason when wiring in the mock gateway client.
Known gaps
Provider interfaces (e.g. IDevicesProvider) still return any for several
payload/return types. The dependency-inversion boundary is correct, but typed
payloads would make it easier to keep implementations honest. Not addressed
in this pass.