From 2b9fdfe4d5f65d4d0bcc7864cfd67aa787d70f20 Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Thu, 1 Oct 2026 16:57:21 +0200 Subject: [PATCH 01/44] Move the SDK demo to sdk-target-based-on-call-view-model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `sdk/` directory is about to become the MatrixRTC SDK package, so the demo that lives there — one bundle of Element Call's CallViewModel for a plain page or widget — moves aside, unchanged. It keeps building under a script and vite config of its own, `pnpm build:sdk-target-based-on-call-view-model`, into its own `dist` rather than the repository root's, and no longer copies the developer's config.json out of `public`. Its README says which script to run and where the output lands. Co-Authored-By: Claude Fable 5.1 --- docs/agents/architecture.md | 3 +- knip.ts | 2 +- package.json | 4 +-- .../README.md | 13 +++++--- .../helper.ts | 0 .../index.html | 0 .../main.ts | 0 tsconfig.json | 2 +- ...-target-based-on-call-view-model.config.ts | 32 +++++++++++++++++++ vite-sdk.config.ts | 28 ---------------- 10 files changed, 46 insertions(+), 38 deletions(-) rename {sdk => sdk-target-based-on-call-view-model}/README.md (82%) rename {sdk => sdk-target-based-on-call-view-model}/helper.ts (100%) rename {sdk => sdk-target-based-on-call-view-model}/index.html (100%) rename {sdk => sdk-target-based-on-call-view-model}/main.ts (100%) create mode 100644 vite-sdk-target-based-on-call-view-model.config.ts delete mode 100644 vite-sdk.config.ts diff --git a/docs/agents/architecture.md b/docs/agents/architecture.md index 59b0940e3..fb39fdb92 100644 --- a/docs/agents/architecture.md +++ b/docs/agents/architecture.md @@ -72,7 +72,8 @@ Code that builds in only one is a bug. - `build:full` — standalone app, also widget mode. - `build:embedded` — `@element-hq/element-call-embedded`. -- `build:sdk` — SDK library, entry `sdk/main.ts`. +- `build:sdk-target-based-on-call-view-model` — the SDK demo, one bundle of the + `CallViewModel`; entry `sdk-target-based-on-call-view-model/main.ts`. - `build:component` — `@element-hq/element-call-component`, sources in `component/` (its own pnpm project; run pnpm from the repo root). Host API is in the README; `pnpm lint:externals` rejects an import of a `react` / `react-dom` / diff --git a/knip.ts b/knip.ts index d9de80c8c..919d2473e 100644 --- a/knip.ts +++ b/knip.ts @@ -12,7 +12,7 @@ export default { config: [ "vite.config.ts", "vite-embedded.config.ts", - "vite-sdk.config.ts", + "vite-sdk-target-based-on-call-view-model.config.ts", "vite-component.config.ts", "vite-component-dev.config.ts", ], diff --git a/package.json b/package.json index b30f57ce3..e583fb1f2 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,7 @@ "build:embedded": "pnpm build:full --config vite-embedded.config.js", "build:embedded:production": "pnpm build:embedded", "build:embedded:development": "pnpm build:embedded --mode development", - "build:sdk:development": "pnpm build:sdk --mode development", - "build:sdk": "pnpm build:full --config vite-sdk.config.js", - "build:sdk:production": "pnpm build:sdk", + "build:sdk-target-based-on-call-view-model": "pnpm build:full --config vite-sdk-target-based-on-call-view-model.config.js", "build:component": "pnpm build:component:js && pnpm build:component:types", "build:component:js": "pnpm build:full --config vite-component.config.js", "build:component:types": "tsc -p component/tsconfig.build.json", diff --git a/sdk/README.md b/sdk-target-based-on-call-view-model/README.md similarity index 82% rename from sdk/README.md rename to sdk-target-based-on-call-view-model/README.md index 7102ba29c..806a6357f 100644 --- a/sdk/README.md +++ b/sdk-target-based-on-call-view-model/README.md @@ -1,4 +1,8 @@ -# SDK mode (EXPERIMENTAL) +# SDK mode (EXPERIMENTAL, superseded) + +> Superseded by the `sdk/` package (`@element-hq/matrixrtc-sdk`) and its harness in +> `sdk/dev`; see `sdk-plan.md`. This folder still builds, with its own script, until +> that package can do what this does. EC can be build in sdk mode. This will result in a compiled js file that can be imported in very simple webapps. @@ -14,12 +18,13 @@ To get started run ``` pnpm install -pnpm build:sdk +pnpm build:sdk-target-based-on-call-view-model ``` -in the repository root. +in the repository root. (`pnpm build:sdk` builds the `sdk/` package instead.) -It will create a `dist` folder containing the compiled js file. +It will create `sdk-target-based-on-call-view-model/dist` containing the compiled js +file, `matrixrtc-sdk.js`. This file needs to be hosted. Locally (via `npx serve -l 81234 --cors`) or on a remote server. diff --git a/sdk/helper.ts b/sdk-target-based-on-call-view-model/helper.ts similarity index 100% rename from sdk/helper.ts rename to sdk-target-based-on-call-view-model/helper.ts diff --git a/sdk/index.html b/sdk-target-based-on-call-view-model/index.html similarity index 100% rename from sdk/index.html rename to sdk-target-based-on-call-view-model/index.html diff --git a/sdk/main.ts b/sdk-target-based-on-call-view-model/main.ts similarity index 100% rename from sdk/main.ts rename to sdk-target-based-on-call-view-model/main.ts diff --git a/tsconfig.json b/tsconfig.json index aba6c5ec0..d7643f4b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -53,7 +53,7 @@ "./src/**/*.ts", "./src/**/*.tsx", "./playwright/**/*.ts", - "./sdk/**/*.ts", + "./sdk-target-based-on-call-view-model/**/*.ts", "./component/**/*.ts", "./component/**/*.tsx" ], diff --git a/vite-sdk-target-based-on-call-view-model.config.ts b/vite-sdk-target-based-on-call-view-model.config.ts new file mode 100644 index 000000000..c0cf8f3f0 --- /dev/null +++ b/vite-sdk-target-based-on-call-view-model.config.ts @@ -0,0 +1,32 @@ +/* +Copyright 2025 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { defineConfig } from "vite"; +import nodePolyfills from "vite-plugin-node-stdlib-browser"; + +// Config for the SDK demo that predates the sdk/ package: a single bundle of +// Element Call's CallViewModel for a plain web page or widget. Kept building +// until the harness in sdk/dev replaces it (see sdk-plan.md). +export default defineConfig(() => ({ + worker: { format: "es" as const }, + // Relative URLs, so the bundle can be hosted under any path + base: "./", + // Otherwise the developer's own config.json in `public` is copied into the output + publicDir: false, + build: { + outDir: "sdk-target-based-on-call-view-model/dist", + sourcemap: true, + manifest: true, + lib: { + formats: ["es" as const], + entry: "./sdk-target-based-on-call-view-model/main.ts", + name: "MatrixrtcSdk", + fileName: "matrixrtc-sdk", + }, + }, + plugins: [nodePolyfills()], +})); diff --git a/vite-sdk.config.ts b/vite-sdk.config.ts deleted file mode 100644 index 48fb6f220..000000000 --- a/vite-sdk.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2025 Element Creations Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE in the repository root for full details. -*/ - -import { defineConfig, mergeConfig } from "vite"; -import nodePolyfills from "vite-plugin-node-stdlib-browser"; - -const base = "./"; - -// Config for embedded deployments (possibly hosted under a non-root path) -export default defineConfig(() => ({ - worker: { format: "es" as const }, - base, // Use relative URLs to allow the app to be hosted under any path - build: { - sourcemap: true, - manifest: true, - lib: { - formats: ["es" as const], - entry: "./sdk/main.ts", - name: "MatrixrtcSdk", - fileName: "matrixrtc-sdk", - }, - }, - plugins: [nodePolyfills()], -})); From 9910496b9078273d8b5d8a93bb2741d04e479bda Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Thu, 1 Oct 2026 16:57:34 +0200 Subject: [PATCH 02/44] Add the MatrixRTC SDK package, its harness and a failing smoke test Set up `sdk/` as a package shaped exactly like `component/`: its own package.json with peer dependencies, the pnpm workspace marker so a host can install it as a git dependency, a build tsconfig for type emission, and a vite lib config with an externals list that `pnpm lint:externals` now checks for both library builds. The package holds only the interface for now. `createRtcSession` returns an empty object; `RtcSession`, `RtcMember`, `MemberMedia` and `TransportMetadata` are the contract Element Call's `CallViewModel` will be rebuilt on (see sdk-plan.md). The shared primitives are re-exported from `src` until the first implementation slice moves them. `sdk/dev` is the smallest consumer of the SDK: a plain page that logs in, joins a room and renders one tile per member, served by `pnpm dev:sdk` on port 3002. Playwright starts it as a third web server, and `playwright/sdk/smoke.spec.ts` has two browsers join one session. It fails on the status line with the SDK's own error until there is an implementation behind the interface. Co-Authored-By: Claude Fable 5.1 --- docs/agents/architecture.md | 9 +- docs/agents/testing.md | 4 + knip.ts | 2 + package.json | 10 +- playwright.config.ts | 13 ++ playwright/sdk/harness.ts | 88 +++++++++ playwright/sdk/smoke.spec.ts | 56 ++++++ scripts/check-component-externals.mjs | 165 +++++++++------- sdk/README.md | 74 +++++++ sdk/dev/index.html | 56 ++++++ sdk/dev/main.ts | 157 +++++++++++++++ sdk/dev/session.ts | 49 +++++ sdk/index.ts | 271 ++++++++++++++++++++++++++ sdk/package.json | 37 ++++ sdk/pnpm-workspace.yaml | 14 ++ sdk/tsconfig.build.json | 18 ++ tsconfig.json | 5 + vite-sdk-dev.config.ts | 49 +++++ vite-sdk.config.ts | 65 ++++++ vite.config.ts | 6 + vitest.config.ts | 1 + 21 files changed, 1070 insertions(+), 79 deletions(-) create mode 100644 playwright/sdk/harness.ts create mode 100644 playwright/sdk/smoke.spec.ts create mode 100644 sdk/README.md create mode 100644 sdk/dev/index.html create mode 100644 sdk/dev/main.ts create mode 100644 sdk/dev/session.ts create mode 100644 sdk/index.ts create mode 100644 sdk/package.json create mode 100644 sdk/pnpm-workspace.yaml create mode 100644 sdk/tsconfig.build.json create mode 100644 vite-sdk-dev.config.ts create mode 100644 vite-sdk.config.ts diff --git a/docs/agents/architecture.md b/docs/agents/architecture.md index fb39fdb92..5b765d280 100644 --- a/docs/agents/architecture.md +++ b/docs/agents/architecture.md @@ -72,8 +72,13 @@ Code that builds in only one is a bug. - `build:full` — standalone app, also widget mode. - `build:embedded` — `@element-hq/element-call-embedded`. -- `build:sdk-target-based-on-call-view-model` — the SDK demo, one bundle of the - `CallViewModel`; entry `sdk-target-based-on-call-view-model/main.ts`. +- `build:sdk` — `@element-hq/matrixrtc-sdk`, sources in `sdk/`, entry `sdk/index.ts`. + Packaged exactly like the component (own pnpm project, run pnpm from the repo + root, types from `sdk/tsconfig.build.json`, externals checked by + `pnpm lint:externals`). Interface only so far; the design is `sdk-plan.md`. +- `build:sdk-target-based-on-call-view-model` — the SDK demo that predates the + package, one bundle of the `CallViewModel`; entry + `sdk-target-based-on-call-view-model/main.ts`. - `build:component` — `@element-hq/element-call-component`, sources in `component/` (its own pnpm project; run pnpm from the repo root). Host API is in the README; `pnpm lint:externals` rejects an import of a `react` / `react-dom` / diff --git a/docs/agents/testing.md b/docs/agents/testing.md index 35a9548a8..d1a325897 100644 --- a/docs/agents/testing.md +++ b/docs/agents/testing.md @@ -39,6 +39,9 @@ translations. The browser is Playwright's, so a fresh clone needs - `playwright/component/` — the component in a host page, via the harness on port 3001 that Playwright starts as a second web server. Catches container-relative layout, styles escaping the root, two instances on a page, host-bridge reports. +- `playwright/sdk/` — the MatrixRTC SDK through its harness on port 3002, a page + with none of Element Call on it. The smoke test is what the implementation is + built against. - `playwright/mobile/` — Pixel 7, `mobile` project only. Test what a user observes: the peer sees the change, it survives a reconnect, it is @@ -50,4 +53,5 @@ pnpm test # unit + storybook pnpm backend # Synapse + LiveKit, required for e2e pnpm test:playwright # or :open pnpm dev:component # component harness, port 3001 +pnpm dev:sdk # SDK harness, port 3002 ``` diff --git a/knip.ts b/knip.ts index 919d2473e..fc3f240e5 100644 --- a/knip.ts +++ b/knip.ts @@ -12,6 +12,8 @@ export default { config: [ "vite.config.ts", "vite-embedded.config.ts", + "vite-sdk.config.ts", + "vite-sdk-dev.config.ts", "vite-sdk-target-based-on-call-view-model.config.ts", "vite-component.config.ts", "vite-component-dev.config.ts", diff --git a/package.json b/package.json index e583fb1f2..3127614c5 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev:full": "vite", "dev:embedded": "vite --config vite-embedded.config.js", "dev:component": "vite --config vite-component-dev.config.ts", + "dev:sdk": "vite --config vite-sdk-dev.config.ts", "build": "pnpm build:full", "build:full": "node --max-old-space-size=16384 node_modules/vite/bin/vite.js build", "build:full:production": "pnpm build:full", @@ -14,6 +15,11 @@ "build:embedded": "pnpm build:full --config vite-embedded.config.js", "build:embedded:production": "pnpm build:embedded", "build:embedded:development": "pnpm build:embedded --mode development", + "build:sdk": "pnpm build:sdk:js && pnpm build:sdk:types", + "build:sdk:js": "pnpm build:full --config vite-sdk.config.js", + "build:sdk:types": "tsc -p sdk/tsconfig.build.json", + "build:sdk:production": "pnpm build:sdk", + "build:sdk:development": "pnpm build:sdk:js --mode development && pnpm build:sdk:types", "build:sdk-target-based-on-call-view-model": "pnpm build:full --config vite-sdk-target-based-on-call-view-model.config.js", "build:component": "pnpm build:component:js && pnpm build:component:types", "build:component:js": "pnpm build:full --config vite-component.config.js", @@ -24,8 +30,8 @@ "format": "oxfmt", "format:check": "oxfmt --check; rc=$?; [[ $rc -ne 0 ]] && printf '\\033[46;30m INFO \\033[0m To fix, run: pnpm format\\n' >&2; exit $rc", "lint": "pnpm lint:types && pnpm lint:oxlint && pnpm lint:knip && pnpm lint:externals", - "lint:oxlint": "oxlint src component playwright", - "lint:oxlint-fix": "oxlint --fix src component playwright", + "lint:oxlint": "oxlint src component sdk playwright", + "lint:oxlint-fix": "oxlint --fix src component sdk playwright", "lint:knip": "knip", "lint:externals": "node scripts/check-component-externals.mjs", "lint:types": "tsc", diff --git a/playwright.config.ts b/playwright.config.ts index e6dcc5249..847b87c84 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -12,6 +12,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { COMPONENT_HARNESS_URL } from "./playwright/component/harness.ts"; +import { SDK_HARNESS_URL } from "./playwright/sdk/harness.ts"; const baseURL = process.env.USE_DOCKER ? "http://localhost:8080" @@ -146,5 +147,17 @@ export default defineConfig({ timeout: 500, }, }, + { + // The harness that uses the MatrixRTC SDK without Element Call. A Vite + // dev server for the same reason as the component's. + command: "pnpm dev:sdk", + url: SDK_HARNESS_URL, + reuseExistingServer: !process.env.CI, + ignoreHTTPSErrors: true, + gracefulShutdown: { + signal: "SIGTERM", + timeout: 500, + }, + }, ], }); diff --git a/playwright/sdk/harness.ts b/playwright/sdk/harness.ts new file mode 100644 index 000000000..999500eb2 --- /dev/null +++ b/playwright/sdk/harness.ts @@ -0,0 +1,88 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { expect, type Page } from "@playwright/test"; + +import { SynapseAdmin } from "../utils/synapse-admin.ts"; + +/** + * Where the SDK harness is served — `sdk/dev`, a page that uses the SDK the way + * a host would, with none of Element Call in it. + */ +export const SDK_HARNESS_URL = "https://localhost:3002"; + +const HOMESERVER_URL = "https://synapse.m.localhost"; +const PASSWORD = "foobarbaz1!"; + +/** + * Registers two users through the Synapse admin API and has the first create a + * public room, so that the second can join it by id without an invite. + */ +export async function createUsersAndRoom( + name: string, +): Promise<{ usernames: [string, string]; roomId: string }> { + const admin = SynapseAdmin.forHomeserver(HOMESERVER_URL); + const usernames: [string, string] = [ + `${name}_a_${Date.now()}`, + `${name}_b_${Date.now()}`, + ]; + const [{ access_token: accessToken }] = await Promise.all( + usernames.map(async (username, index) => + admin.registerUser(username, PASSWORD, `${name} ${"AB"[index]}`), + ), + ); + + const response = await fetch( + `${HOMESERVER_URL}/_matrix/client/v3/createRoom`, + { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: `${name}'s session`, + preset: "public_chat", + }), + }, + ); + if (!response.ok) + throw new Error( + `Could not create a room: ${response.status} ${await response.text()}`, + ); + const { room_id: roomId } = (await response.json()) as { room_id: string }; + + return { usernames, roomId }; +} + +/** + * Opens the harness signed in as the given user and waits until the SDK + * reports the session as joined. The status line carries the SDK's error if + * it does not get there, so the failure says why. + */ +export async function startHarness( + page: Page, + username: string, + roomId: string, +): Promise { + const query = new URLSearchParams({ + homeserver: HOMESERVER_URL, + username, + password: PASSWORD, + room: roomId, + }); + await page.goto(`${SDK_HARNESS_URL}/?${query.toString()}`); + await page.getByRole("button", { name: "Start" }).click(); + + // A login, a crypto setup and an initial sync happen first. An error is + // final, so it is not worth waiting out the timeout for "Joined" after one. + const status = page.getByTestId("status"); + await expect(status).toHaveText(/^(Joined|Error)/, { timeout: 120_000 }); + const text = await status.textContent(); + if (text !== "Joined") + throw new Error(`The harness did not join the session: ${text}`); +} diff --git a/playwright/sdk/smoke.spec.ts b/playwright/sdk/smoke.spec.ts new file mode 100644 index 000000000..70a4b80c0 --- /dev/null +++ b/playwright/sdk/smoke.spec.ts @@ -0,0 +1,56 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { type Browser, expect, type Page, test } from "@playwright/test"; + +import { createUsersAndRoom, startHarness } from "./harness.ts"; + +/** + * The MatrixRTC SDK driven through its harness in `sdk/dev`: no Element Call + * on the page, only the SDK's public API. + * + * This is the smoke test the implementation is built against. It fails until + * `createRtcSession` does something, and it fails on the status line first, so + * the SDK's own error is what the report shows. + */ + +// Two browsers each log in, set up crypto and sync before anything is on +// screen, then wait for media to connect +test.describe.configure({ timeout: 300_000 }); + +test("two browsers see each other in one session", async ({ browser }) => { + const { usernames, roomId } = await createUsersAndRoom("sdksmoke"); + const [pageA, pageB] = await Promise.all( + usernames.map(async () => newPage(browser)), + ); + + await Promise.all([ + startHarness(pageA, usernames[0], roomId), + startHarness(pageB, usernames[1], roomId), + ]); + + // Each page shows itself and the other, each tile named by its user + for (const page of [pageA, pageB]) { + await expect(page.getByTestId("member")).toHaveCount(2, { + timeout: 60_000, + }); + for (const username of usernames) + await expect( + page.getByTestId("member").filter({ hasText: username }), + ).toHaveCount(1); + } +}); + +/** + * A context of its own per user, since a browser profile holds one login. No + * permissions to grant: each browser is launched with fake media that is + * handed out without asking (see playwright.config.ts). + */ +async function newPage(browser: Browser): Promise { + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + return context.newPage(); +} diff --git a/scripts/check-component-externals.mjs b/scripts/check-component-externals.mjs index e088682d7..d27416736 100644 --- a/scripts/check-component-externals.mjs +++ b/scripts/check-component-externals.mjs @@ -6,57 +6,72 @@ Please see LICENSE in the repository root for full details. */ /** - * Checks that the component build leaves the packages a host must supply to - * the host. + * Checks that the library builds leave the packages a host must supply to the + * host. * * A host application already has React, the Matrix SDK and Compound, * and a second copy of any of them is worse than dead weight: React would hold * two sets of hooks, the Matrix client would run two sync loops, and a second * Compound would style the tooltips it floats into the host's body with class - * names the host's stylesheet does not know. So the - * component build lists them as external — but that list has to name every - * subpath, since the bundler silently ignores the pattern and callback forms - * of the option, and an import it does not cover is bundled with no warning at - * all. That is the failure this guards against. + * names the host's stylesheet does not know. So each library build lists them + * as external — but that list has to name every subpath, since the bundler + * silently ignores the pattern and callback forms of the option, and an import + * it does not cover is bundled with no warning at all. That is the failure + * this guards against. * - * It reads the list from the build config itself, so there is one copy of it, - * and compares it against every import of those packages in the source. + * It reads each list from the build config itself, so there is one copy of it + * per build, and compares it against every import of those packages in the + * source. * * The comparison is deliberately over-approximate: it looks at all of `src` - * rather than only the modules the component actually pulls in, so it will - * sometimes ask for a subpath that only the standalone app imports. Listing - * one the component never imports costs nothing — the bundler ignores it — - * whereas missing one costs a duplicate package. + * rather than only the modules a build actually pulls in, so it will sometimes + * ask for a subpath that only the standalone app imports. Listing one a build + * never imports costs nothing — the bundler ignores it — whereas missing one + * costs a duplicate package. */ import { readdir, readFile } from "node:fs/promises"; import { join } from "node:path"; import { loadConfigFromFile } from "vite"; -const CONFIG = "vite-component.config.ts"; -const SOURCES = ["src", "component"]; -// The development harness is a host, not part of the component: it is the -// one that imports what a host supplies (Compound's stylesheets, say). -const EXCLUDED = ["component/dev"]; - -/** The packages whose duplication would break a host, rather than merely enlarge it. */ -const MUST_BE_EXTERNAL = [ - "react", - "react-dom", - "matrix-js-sdk", - "@vector-im/compound-web", - "@vector-im/compound-design-tokens", +/** + * The library builds, each with the directories it is built from and the + * packages whose duplication would break a host rather than merely enlarge it. + * A development harness is a host, not part of the library: it is the one that + * imports what a host supplies (Compound's stylesheets, say). + */ +const TARGETS = [ + { + config: "vite-component.config.ts", + what: "the component build", + sources: ["src", "component"], + excluded: ["component/dev"], + mustBeExternal: [ + "react", + "react-dom", + "matrix-js-sdk", + "@vector-im/compound-web", + "@vector-im/compound-design-tokens", + ], + }, + { + config: "vite-sdk.config.ts", + what: "the SDK build", + sources: ["src", "sdk"], + excluded: ["sdk/dev"], + mustBeExternal: ["matrix-js-sdk", "livekit-client", "rxjs"], + }, ]; const isTestFile = (name) => name.includes(".test.") || name.includes(".stories."); /** Every source file under the given directories, recursively. */ -async function* sourceFiles(dir) { +async function* sourceFiles(dir, excluded) { for (const entry of await readdir(dir, { withFileTypes: true })) { const path = join(dir, entry.name); - if (EXCLUDED.includes(path)) continue; - if (entry.isDirectory()) yield* sourceFiles(path); + if (excluded.includes(path)) continue; + if (entry.isDirectory()) yield* sourceFiles(path, excluded); else if (/\.(ts|tsx)$/.test(entry.name) && !isTestFile(entry.name)) yield path; } @@ -88,60 +103,60 @@ function imports(source) { * with the host's copy of anything. Worker sub-builds do not inherit this * option anyway. */ -const mustBeExternal = (specifier) => +const mustBeExternal = (specifier, packages) => !specifier.includes("?") && - MUST_BE_EXTERNAL.some( - (pkg) => specifier === pkg || specifier.startsWith(`${pkg}/`), - ); + packages.some((pkg) => specifier === pkg || specifier.startsWith(`${pkg}/`)); -const loaded = await loadConfigFromFile( - { command: "build", mode: "production" }, - CONFIG, -); -if (loaded === null) { - console.error(`Could not load ${CONFIG}`); - process.exit(1); -} -const declared = new Set(loaded.config.build?.rollupOptions?.external ?? []); -if (declared.size === 0) { - console.error( - `${CONFIG} declares nothing external. Either the option moved, or the ` + - `list is empty; either way this check is not looking at what it thinks.`, +/** @returns Whether the target's externals list covers its imports. */ +async function check({ + config, + what, + sources, + excluded, + mustBeExternal: packages, +}) { + const loaded = await loadConfigFromFile( + { command: "build", mode: "production" }, + config, ); - process.exit(1); -} - -// Where each missing specifier is imported, so the message can point at it -const missing = new Map(); -const seen = new Set(); -for (const dir of SOURCES) - for await (const file of sourceFiles(dir)) { - const source = await readFile(file, "utf8"); - for (const specifier of imports(source)) { - if (!mustBeExternal(specifier)) continue; - seen.add(specifier); - if (declared.has(specifier)) continue; - const files = missing.get(specifier) ?? []; - files.push(file); - missing.set(specifier, files); - } + if (loaded === null) { + console.error(`Could not load ${config}`); + return false; + } + const declared = new Set(loaded.config.build?.rollupOptions?.external ?? []); + if (declared.size === 0) { + console.error( + `${config} declares nothing external. Either the option moved, or the ` + + `list is empty; either way this check is not looking at what it thinks.`, + ); + return false; } -if (missing.size > 0) { + // Where each missing specifier is imported, so the message can point at it + const missing = new Map(); + for (const dir of sources) + for await (const file of sourceFiles(dir, excluded)) { + const source = await readFile(file, "utf8"); + for (const specifier of imports(source)) { + if (!mustBeExternal(specifier, packages)) continue; + if (declared.has(specifier)) continue; + const files = missing.get(specifier) ?? []; + files.push(file); + missing.set(specifier, files); + } + } + + if (missing.size === 0) return true; console.error( - `${CONFIG} does not declare these imports external, so the component ` + - `build would bundle its own copy of them:\n`, + `${config} does not declare these imports external, so ${what} ` + + `would bundle its own copy of them:\n`, ); for (const [specifier, files] of [...missing].sort()) console.error(` ${specifier}\n imported by ${files.join(", ")}`); - console.error(`\nAdd each one to the \`external\` list in ${CONFIG}.`); - process.exit(1); + console.error(`\nAdd each one to the \`external\` list in ${config}.\n`); + return false; } -// Deliberately no complaint about declarations nothing imports. Some of them -// cannot be seen from the source at all — `react/jsx-runtime` is injected by -// the JSX transform — and an extra declaration is inert, so there is nothing -// to warn about. -console.log( - `${declared.size} external declarations cover all ${seen.size} imports of ${MUST_BE_EXTERNAL.join(", ")}.`, -); +let ok = true; +for (const target of TARGETS) ok = (await check(target)) && ok; +if (!ok) process.exit(1); diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 000000000..e750513c3 --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,74 @@ +# MatrixRTC SDK (EXPERIMENTAL) + +`@element-hq/matrixrtc-sdk` is the call model under Element Call, on its own: +MatrixRTC memberships and transports, LiveKit connections, publishing, E2EE keys and +the media of every member, as observables. It has no UI. Element Call's own +`CallViewModel` is meant to become one consumer of it; the design is in +[`sdk-plan.md`](../sdk-plan.md). + +**Status:** interface only. `createRtcSession` returns an object that does nothing. +The development harness and its e2e test exist so the implementation can be built +against them. + +## Using it + +```ts +import { + constant, + createRtcSession, + E2eeType, + MatrixRTCMode, + ObservableScope, +} from "@element-hq/matrixrtc-sdk"; + +const scope = new ObservableScope(); +const session = createRtcSession( + scope, + client, // a matrix-js-sdk MatrixClient, logged in and syncing + room, // the matrix-js-sdk Room to hold the session in + { + microphoneEnabled$: constant(true), + cameraEnabled$: constant(true), + audioInputDeviceId$: constant(undefined), + videoInputDeviceId$: constant(undefined), + videoProcessor$: constant(undefined), + }, + { + encryptionSystem: { kind: E2eeType.PER_PARTICIPANT }, + matrixRTCMode: MatrixRTCMode.Compatibility, + }, +); +session.join(); + +session.remoteMembers$.subscribe((members) => { + // each member has displayName$, media$ and more; a media track is rendered + // by handing it a