UNPKG

@oddjs/odd

Version:
243 lines (197 loc) 6.12 kB
import type { AppInfo } from "../appInfo.js" import type { CID } from "../common/cid.js" import type { Crypto, Reference } from "../components.js" import type { DistinctivePath, Partition } from "../path/index.js" import type { Maybe } from "../common/types.js" import type { Permissions } from "../permissions.js" import type { Session } from "../session.js" import * as DID from "../did/index.js" import * as Events from "../events.js" import { VERSION } from "../index.js" // CREATE export type Dependencies = { crypto: Crypto.Implementation reference: Reference.Implementation } type Config = { namespace: AppInfo | string session: Maybe<Session> capabilities?: Permissions dependencies: Dependencies eventEmitters: { fileSystem: Events.Emitter<Events.FileSystem> session: Events.Emitter<Events.Session<Session>> } } export async function create(config: Config): Promise<{ connect: (extensionId: string) => void disconnect: (extensionId: string) => void }> { let connection: Connection = { extensionId: null, connected: false } let listeners: Listeners return { connect: async (extensionId: string) => { connection = await connect(extensionId, config) // The extension may call connect more than once, but // listeners should only be added once if (!listeners) listeners = listen(connection, config) }, disconnect: async (extensionId: string) => { connection = await disconnect(extensionId, config) stopListening(config, listeners) } } } // CONNECTION type Connection = { extensionId: string | null connected: boolean } async function connect(extensionId: string, config: Config): Promise<Connection> { const state = await getState(config) globalThis.postMessage({ id: extensionId, type: "connect", timestamp: Date.now(), state }) return { extensionId, connected: true } } async function disconnect(extensionId: string, config: Config): Promise<Connection> { const state = await getState(config) globalThis.postMessage({ id: extensionId, type: "disconnect", timestamp: Date.now(), state }) return { extensionId, connected: false } } // LISTENERS type Listeners = { handleLocalChange: (params: { root: CID; path: DistinctivePath<[ Partition, ...string[] ]> }) => Promise<void> handlePublish: (params: { root: CID }) => Promise<void> handleSessionCreate: (params: { session: Session }) => Promise<void> handleSessionDestroy: (params: { username: string }) => Promise<void> } function listen(connection: Connection, config: Config): Listeners { async function handleLocalChange(params: { root: CID; path: DistinctivePath<[ Partition, ...string[] ]> }) { const { root, path } = params const state = await getState(config) globalThis.postMessage({ id: connection.extensionId, type: "fileSystem", timestamp: Date.now(), state, detail: { type: "local-change", root: root.toString(), path } }) } async function handlePublish(params: { root: CID }) { const { root } = params const state = await getState(config) globalThis.postMessage({ id: connection.extensionId, type: "fileSystem", timestamp: Date.now(), state, detail: { type: "publish", root: root.toString() } }) } async function handleSessionCreate(params: { session: Session }) { const { session } = params config = { ...config, session } const state = await getState(config) globalThis.postMessage({ id: connection.extensionId, type: "session", timestamp: Date.now(), state, detail: { type: "create", username: session.username } }) } async function handleSessionDestroy(params: { username: string }) { const { username } = params config = { ...config, session: null } const state = await getState(config) globalThis.postMessage({ id: connection.extensionId, type: "session", timestamp: Date.now(), state, detail: { type: "destroy", username } }) } config.eventEmitters.fileSystem.on("fileSystem:local-change", handleLocalChange) config.eventEmitters.fileSystem.on("fileSystem:publish", handlePublish) config.eventEmitters.session.on("session:create", handleSessionCreate) config.eventEmitters.session.on("session:destroy", handleSessionDestroy) return { handleLocalChange, handlePublish, handleSessionCreate, handleSessionDestroy } } function stopListening(config: Config, listeners: Listeners) { if (listeners) { config.eventEmitters.fileSystem.removeListener("fileSystem:local-change", listeners.handleLocalChange) config.eventEmitters.fileSystem.removeListener("fileSystem:publish", listeners.handlePublish) config.eventEmitters.session.removeListener("session:create", listeners.handleSessionCreate) config.eventEmitters.session.removeListener("session:destroy", listeners.handleSessionDestroy) } } // STATE type State = { app: { namespace: AppInfo | string capabilities?: Permissions } fileSystem: { dataRootCID: string | null } user: { username: string | null accountDID: string | null agentDID: string } odd: { version: string } } async function getState(config: Config): Promise<State> { const { capabilities, dependencies, namespace, session } = config const agentDID = await DID.agent(dependencies.crypto) let accountDID = null let username = null let dataRootCID = null if (session && session.username) { username = session.username accountDID = await dependencies.reference.didRoot.lookup(username) dataRootCID = await dependencies.reference.dataRoot.lookup(username) } return { app: { namespace, ...(capabilities ? { capabilities } : {}) }, fileSystem: { dataRootCID: dataRootCID?.toString() ?? null }, user: { username, accountDID, agentDID }, odd: { version: VERSION } } }