Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions src/FinalizationHelper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { ApiStatic } from "./api/ApiStatic";
import { ExtKeyNative } from "./api/ExtKeyNative";

interface NativeObjInfo {
ptr: number;
onFree: () => Promise<void>;
Expand Down Expand Up @@ -29,7 +26,6 @@ export class FinalizationHelper {

private constructor(private wasmLib: any) {
this.finalizationRegistry = new FinalizationRegistry((onCleanup) => {
const api = ApiStatic.getInstance();
this.finalizationQueue.push(onCleanup.onFree);
this.scheduleCleanup();
});
Expand Down
26 changes: 0 additions & 26 deletions src/api/ApiStatic.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ limitations under the License.
*/

import { EndpointFactory } from "./service/EndpointFactory";
import { NativeError } from "./api/NativeError";
import { NativeError } from "./native/NativeError";
import {
EventQueue,
StoreApi,
Expand Down
80 changes: 80 additions & 0 deletions src/ioc/Container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
type Factory<T> = (c: Container) => Promise<T>;

interface Registration<T> {
factory: Factory<T>;
singleton: boolean;
cached: Promise<T> | undefined;
}

/**
* Minimal async IoC container.
*
* - Singletons: factory is called once; all subsequent resolves return the same Promise.
* - Values: pre-constructed instance stored as a resolved singleton.
*
* Circular dependencies are broken by resolving inside callback closures that are
* called after all registrations have been set up, not at construction time.
*/
export class Container {
private readonly reg = new Map<string | symbol, Registration<unknown>>();

/**
* Registers a singleton factory for `token`. The factory is called at most
* once; all subsequent `resolve` calls return the same cached `Promise`.
*/
registerSingleton<T>(token: string | symbol, factory: Factory<T>): void {
this.reg.set(token, {
factory: factory as Factory<unknown>,
singleton: true,
cached: undefined,
});
}

/**
* Registers a pre-constructed `value` as a resolved singleton for `token`.
* Equivalent to `registerSingleton(token, () => Promise.resolve(value))`.
*/
registerValue<T>(token: string | symbol, value: T): void {
const resolved = Promise.resolve(value);
this.reg.set(token, { factory: () => resolved, singleton: true, cached: resolved });
}

/**
* Resolves the value registered for `token`.
* For singletons, returns the cached `Promise` after the first call.
* @throws if nothing has been registered for `token`.
*/
resolve<T>(token: string | symbol): Promise<T> {
const entry = this.reg.get(token);
if (!entry) throw new Error(`Container: no registration for token "${String(token)}"`);
if (entry.singleton) {
if (!entry.cached) entry.cached = entry.factory(this);
return entry.cached as Promise<T>;
}
return entry.factory(this) as Promise<T>;
}
}

/**
* Application-lifetime container.
* Holds singletons that are created once during `EndpointFactory.setup()` and
* live for the entire lifetime of the application: EventQueue, CryptoApi, the
* raw WASM Api handle, etc.
*/
export class GlobalContainer extends Container {}

/**
* Per-connection scoped container.
* Created once per `EndpointFactory.connect()` / `connectPublic()` call.
* Holds ThreadApi, StoreApi, KvdbApi, EventApi, InboxApi, StreamApi — all
* tied to a single authenticated Connection instance.
*/
export class ConnectionContainer extends Container {}

/**
* Per-stream-session scoped container.
* Created once per `EndpointFactory.createStreamApi()` call inside a
* ConnectionContainer. Holds the entire WebRTC sub-graph: KeyStore,
* PeerConnectionManager, E2eeWorker, AudioManager, etc.
*/
export class WebRtcContainer extends Container {}
53 changes: 53 additions & 0 deletions src/ioc/Tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* IoC container token registry.
*
* `global:` — created once during EndpointFactory.setup(), live for the application lifetime.
* `conn:` — created once per Connection (connect / connectPublic call), scoped to that connection.
* `rtc:` — created once per createStreamApi() call, scoped to that connection's stream session.
*/
export const T = {
// -------------------------------------------------------------------------
// Global scope
// -------------------------------------------------------------------------
Api: "global:Api",
AssetsBasePath: "global:AssetsBasePath",
EventQueue: "global:EventQueue",
CryptoApi: "global:CryptoApi",

// -------------------------------------------------------------------------
// Connection scope (one container per Connection instance)
// -------------------------------------------------------------------------
ConnectionPtr: "conn:ConnectionPtr",
ThreadApi: "conn:ThreadApi",
StoreApi: "conn:StoreApi",
KvdbApi: "conn:KvdbApi",
EventApi: "conn:EventApi",
InboxApi: "conn:InboxApi",
StreamApi: "conn:StreamApi",

// -------------------------------------------------------------------------
// WebRTC sub-graph (connection-scoped, one per createStreamApi call)
// -------------------------------------------------------------------------
KeyStore: "rtc:KeyStore",
DataChannelCryptor: "rtc:DataChannelCryptor",
DataChannelSession: "rtc:DataChannelSession",
StateChangeDispatcher: "rtc:StateChangeDispatcher",
ListenerRegistry: "rtc:ListenerRegistry",
E2eeWorker: "rtc:E2eeWorker",
E2eeTransformManager: "rtc:E2eeTransformManager",
AudioManager: "rtc:AudioManager",
PeerConnectionFactory: "rtc:PeerConnectionFactory",
PeerConnectionManager: "rtc:PeerConnectionManager",
PublisherManager: "rtc:PublisherManager",
SubscriberManager: "rtc:SubscriberManager",
KeySyncManager: "rtc:KeySyncManager",
WebRtcClient: "rtc:WebRtcClient",

// -------------------------------------------------------------------------
// API layer (connection-scoped)
// -------------------------------------------------------------------------
StreamApiNative: "api:StreamApiNative",
WebRtcInterfaceImpl: "api:WebRtcInterfaceImpl",
} as const;

export type Token = (typeof T)[keyof typeof T];
160 changes: 160 additions & 0 deletions src/ioc/buildConnectionApis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Api } from "../native/Api";
import { CryptoApiNative } from "../native/CryptoApiNative";
import { EventApiNative } from "../native/EventApiNative";
import { EventQueueNative } from "../native/EventQueueNative";
import { InboxApiNative } from "../native/InboxApiNative";
import { KvdbApiNative } from "../native/KvdbApiNative";
import { StoreApiNative } from "../native/StoreApiNative";
import { StreamApiNative } from "../native/StreamApiNative";
import { ThreadApiNative } from "../native/ThreadApiNative";
import { WebRtcInterfaceImpl } from "../webStreams/WebRtcInterfaceImpl";
import { Connection } from "../service/Connection";
import { CryptoApi } from "../service/CryptoApi";
import { EventApi } from "../service/EventApi";
import { EventQueue } from "../service/EventQueue";
import { InboxApi } from "../service/InboxApi";
import { KvdbApi } from "../service/KvdbApi";
import { StoreApi } from "../service/StoreApi";
import { StreamApi } from "../service/StreamApi";
import { ThreadApi } from "../service/ThreadApi";
import { WebRtcClient } from "../webStreams/WebRtcClient";
import { GlobalContainer, ConnectionContainer, WebRtcContainer } from "./Container";
import { T } from "./Tokens";
import { registerWebRtcServices } from "./buildWebRtcClient";

/**
* Registers all global-scope singletons into the provided GlobalContainer.
* Call once during EndpointFactory.init().
*/
export function registerGlobalServices(c: GlobalContainer, api: Api, assetsBasePath: string): void {
c.registerValue(T.Api, api);
c.registerValue(T.AssetsBasePath, assetsBasePath);

c.registerSingleton(T.EventQueue, async (c) => {
const a = await c.resolve<Api>(T.Api);
const native = new EventQueueNative(a);
const ptr = await native.newEventQueue();
return new EventQueue(native, ptr);
});

c.registerSingleton(T.CryptoApi, async (c) => {
const a = await c.resolve<Api>(T.Api);
const native = new CryptoApiNative(a);
const ptr = await native.newApi();
await native.create(ptr, []);
return new CryptoApi(native, ptr);
});
}

/**
* Registers all connection-scoped API singletons into the provided ConnectionContainer.
*
* Dependency graph (resolved lazily to allow any creation order):
*
* ThreadApi ──┐
* StoreApi ──┼──► InboxApi
* EventApi ──┼──► StreamApi (also needs WebRTC sub-graph)
* KvdbApi │
* └── (connection instance shared via T.ConnectionPtr)
*
* Call once per Connection, after registering T.ConnectionPtr.
*/
export function registerConnectionServices(
c: ConnectionContainer,
api: Api,
assetsBasePath: string,
): void {
c.registerValue(T.AssetsBasePath, assetsBasePath);

c.registerSingleton(T.ThreadApi, async (c) => {
const conn = await c.resolve<Connection>(T.ConnectionPtr);
if (conn.hasApi("threads")) {
throw new Error("ThreadApi already registered for given connection.");
}
const native = new ThreadApiNative(api);
const ptr = await native.newApi(conn.servicePtr);
await native.create(ptr, []);
conn.registerApi("threads", ptr, native);
return new ThreadApi(native, ptr);
});

c.registerSingleton(T.StoreApi, async (c) => {
const conn = await c.resolve<Connection>(T.ConnectionPtr);
if (conn.hasApi("stores")) {
throw new Error("StoreApi already registered for given connection.");
}
const native = new StoreApiNative(api);
const ptr = await native.newApi(conn.servicePtr);
conn.registerApi("stores", ptr, native);
await native.create(ptr, []);
return new StoreApi(native, ptr);
});

c.registerSingleton(T.KvdbApi, async (c) => {
const conn = await c.resolve<Connection>(T.ConnectionPtr);
if (conn.hasApi("kvdbs")) {
throw new Error("KvdbApi already registered for given connection.");
}
const native = new KvdbApiNative(api);
const ptr = await native.newApi(conn.servicePtr);
await native.create(ptr, []);
conn.registerApi("kvdbs", ptr, native);
return new KvdbApi(native, ptr);
});

c.registerSingleton(T.EventApi, async (c) => {
const conn = await c.resolve<Connection>(T.ConnectionPtr);
if (conn.hasApi("events")) {
throw new Error("EventApi already registered for given connection.");
}
const native = new EventApiNative(api);
const ptr = await native.newApi(conn.servicePtr);
await native.create(ptr, []);
conn.registerApi("events", ptr, native);
return new EventApi(native, ptr);
});

// InboxApi depends on ThreadApi + StoreApi — resolved lazily from this same container.
c.registerSingleton(T.InboxApi, async (c) => {
const conn = await c.resolve<Connection>(T.ConnectionPtr);
if (conn.hasApi("inboxes")) {
throw new Error("InboxApi already registered for given connection.");
}
const threadApi = await c.resolve<ThreadApi>(T.ThreadApi);
const storeApi = await c.resolve<StoreApi>(T.StoreApi);
const native = new InboxApiNative(api);
const ptr = await native.newApi(conn.servicePtr, threadApi.servicePtr, storeApi.servicePtr);
await native.create(ptr, []);
conn.registerApi("inboxes", ptr, native);
return new InboxApi(native, ptr);
});

// StreamApi depends on EventApi + a fresh WebRTC sub-graph.
c.registerSingleton(T.StreamApi, async (c) => {
const conn = await c.resolve<Connection>(T.ConnectionPtr);
if (conn.hasApi("streams")) {
throw new Error("StreamApi already registered for given connection.");
}
const eventApi = await c.resolve<EventApi>(T.EventApi);

// Each StreamApi gets its own isolated WebRTC sub-graph container.
const rtc = new WebRtcContainer();
rtc.registerValue(T.AssetsBasePath, assetsBasePath);
registerWebRtcServices(rtc);

const webRtcClient = await rtc.resolve<WebRtcClient>(T.WebRtcClient);
const webRtcInterfaceImpl = new WebRtcInterfaceImpl(webRtcClient);
const native = new StreamApiNative(api, webRtcInterfaceImpl);
const ptr = await native.newApi(conn.servicePtr, eventApi.servicePtr);

webRtcClient.bindApiInterface({
trickle: (sessionId, candidate) => native.trickle(ptr, [sessionId, candidate]),
acceptOffer: (sessionId, sdp) => native.acceptOfferOnReconfigure(ptr, [sessionId, sdp]),
});

await native.create(ptr, []);
const streamApi = new StreamApi(native, ptr, webRtcClient);
conn.registerApi("streams", ptr, native, streamApi);
return streamApi;
});
}
Loading