matrix-js-sdk

repository·develop·Indexed 24 days ago

https://github.com/matrix-org/matrix-js-sdk

A comprehensive Client-Server SDK for JavaScript and TypeScript (version 42.0.0) used to build Matrix clients for browsers and Node.js. It provides a high-level object model for the Matrix protocol, handling synchronization, state management, WebRTC calling, and end-to-end encryption (E2EE) via Rust cryptography bindings.

Tokens
29.2K
Snippets
41
Records
205
Agent score
77%

What's inside matrix-js-sdk

  1. What the Matrix JavaScript SDK provides

    develop

    The SDK provides a full object model around the Matrix Client-Server API. Key features include:

    • Syncing: Automatically handles /sync calls.
    • State Management: Manages room members, typing indicators, power levels, and membership changes.
    • High-level Abstractions: Exposes Rooms, RoomState, RoomMembers, and Users objects.
    • Local Echo: Messages appear as 'sending' in the timeline immediately after being sent to prevent UI gaps.
    • Resilience: Automatically retries requests due to network errors or rate limiting, and handles message queueing.
    • Advanced Features: Handles WebRTC calling and room initial sync upon accepting invites.
  2. Quickstart: Initialize and start a Matrix client

    develop

    To get started, import the SDK, create a client instance with a baseUrl, and call startClient(). You can use client.once to listen for the initial sync event to ensure the client is prepared.

    import * as sdk from "matrix-js-sdk";
    const client = sdk.createClient({ baseUrl: "https://matrix.org" });
    
    // Start the client
    await client.startClient({ initialSyncLimit: 10 });
    
    // Wait for the sync state to be PREPARED
    client.once(sdk.ClientEvent.sync, function (state, prevState, res) {
        if (state === "PREPARED") {
            console.log("prepared");
        } else {
            console.log(state);
            process.exit(1);
        }
    });
  3. TypeScript type and variable usage best practices

    develop

    Follow these rules for managing types and variables:

    • Variable Declaration: One variable declaration per line. If a variable is not receiving a value on declaration, its type must be explicitly defined:
      let errorMessage: string;
    • Booleans: Ensure variables intended to be boolean are explicitly cast to boolean (e.g., using !! or Boolean()) to avoid storing truthy/falsy values of other types.
    • Constants and Mutability: Use const for constants and let for mutable variables.
    • Type Exhaustiveness: Describe types exhaustively (ensure noImplicitAny would pass). Avoid any; if any is used, a comment explaining why must be included.
    • Visibility: Always declare member visibility (public, private, or protected).
    • Private Members: Private members should not be prefixed with an underscore unless required to resolve a naming conflict (e.g., a getter wanting the same name as an underlying variable).
    • Readonly: Prefer readonly members over getters backed by a variable, unless an internal setter is required. public static members should also be readonly when possible.
    • Interfaces vs Types:
      • Use Interfaces for object definitions.
      • Use Types for parameter-value-only declarations.
      • Prefer a type definition (like an interface) over an inline type.
    • Optionality: Prefer a type like X | null instead of truly optional parameters (?). For example, when a room ID is required but might not be known yet, use room: string | null to force the caller to acknowledge the null state.
    • Imports: Use import instead of require.
    let errorMessage: string;
    
    const isRealUser = !!userId && ...; // good
    const isRealUser = userId && ...;   // bad: isRealUser is userId's type, not a boolean
    
    interface MyObject {
        hasString: boolean;
    }
    
    type Options = MyObject | string;
    
    function doThing(arg: Options) {
        // ...
    }
    
    function doThingWithRoom(
        thing: string,
        room: string | null, // require the caller to specify
    ) {
        // ...
    }
  4. Migrate from legacy crypto to Rust crypto

    develop

    If your application used the legacy MatrixClient.initLegacyCrypto() stack, you must migrate existing devices to the Rust stack.

    Migration Steps:

    1. When calling sdk.createClient, provide the legacy cryptoStore and pickleKey in the options object.
    2. Call await matrixClient.initRustCrypto(). The migration will trigger automatically.

    Monitoring Progress: You can listen to the CryptoEvent.LegacyCryptoStoreMigrationProgress event. The migration is complete when both progress and total equal -1.

    Note: The legacy MatrixClient.crypto object is deprecated. You should now use MatrixClient.getCrypto() and migrate any calls to deprecated MatrixClient methods to the CryptoApi interface.

    // You should provide the legacy crypto store and the pickle key to the matrix client in order to migrate the data.
    const matrixClient = sdk.createClient({
        cryptoStore: myCryptoStore,
        pickleKey: myPickleKey,
        baseUrl: "http://localhost:8008",
        accessToken: myAccessToken,
        userId: myUserId,
    });
    
    // The migration will be done automatically when you call `initRustCrypto`.
    await matrixClient.initRustCrypto();
    
    // To follow the migration progress, listen to the LegacyCryptoStoreMigrationProgress event:
    matrixClient.on(CryptoEvent.LegacyCryptoStoreMigrationProgress, (progress, total) => {
        // When progress === total === -1, the migration is finished.
    });
  5. General coding and formatting standards

    develop

    All code in the repository must adhere to these formatting rules:

    • Formatter: Files must be formatted with Oxfmt.
    • Line Limit: 120 character limit per line (unless the existing file uses a lower limit).
    • Indentation: 4 spaces per tab.
    • Newlines: Use Unix-style newlines.
    • File Endings: Every file must have a single empty line at the end.
    • Whitespace: Lines must be trimmed of all excess whitespace, including blank lines.
    • Readability: Long lines should be broken up for readability.
  6. Organizing TypeScript files and imports

    develop

    To maintain consistency, organize files and imports as follows:

    File Structure

    • One Entity Per File: There should be approximately one interface, class, or enum per file.
    • Exceptions: Files named types.ts, global.d.ts, or ending in -types.ts can contain multiple definitions.
    • Naming: The file name should match the interface, class, or enum name.
    • Bulk Functions: Utility functions can be grouped in files named foo-utils.ts or utils/foo.ts.

    Import Ordering

    1. External module imports first.
    2. Internal imports second.

    File Content Sequence

    1. License header
    2. Imports
    3. Constants
    4. Enums
    5. Interfaces
    6. Functions
    7. Classes (ordered by: static properties $\rightarrow$ properties $\rightarrow$ constructors $\rightarrow$ getters/setters $\rightarrow$ protected/abstract functions $\rightarrow$ public/private functions $\rightarrow$ static functions)
  7. Writing tests in TypeScript

    develop

    Tests must be written in TypeScript and follow this structure:

    1. Mocks: Declare mocks below imports, but above everything else.
    2. Template: Use the describe/it pattern. Use "it should..." terminology for test descriptions.
    3. Structure:
      • Use beforeEach and afterEach for setup/teardown.
      • Place test-specific variables and function calls/expectations inside the it block.

    For utility classes, nest describe blocks for each function being tested.

    // Describe the class, component, or file name.
    describe("FooComponent", () => {
        // all test inspecific variables go here
    
        beforeEach(() => {
            // exclude if not used.
        });
    
        afterEach(() => {
            // exclude if not used.
        });
    
        // Use "it should..." terminology
        it("should call the correct API", async () => {
            // test-specific variables go here
            // function calls/state changes go here
            // expectations go here
        });
    });
    
    // If the file being tested is a utility class:
    describe("foo-utils", () => {
        describe("firstUtilFunction", () => {
            it("should...", async () => {
                // ...
            });
        });
    });
  8. Set up cross-signing

    develop

    To enable cross-signing (used for verifying devices and other users), call CryptoApi.bootstrapCrossSigning(). You must provide the authUploadDeviceSigningKeys callback, which is responsible for uploading newly generated public cross-signing keys to the Matrix server.

    matrixClient.getCrypto().bootstrapCrossSigning({
        authUploadDeviceSigningKeys: async (makeRequest) => {
            return makeRequest(authDict);
        },
    });
  9. Run the Node.js example terminal app

    develop

    The example-app is a functional terminal application that allows you to view a user's room list, join rooms, send messages, and view room membership lists.

    To use the example app, you must first configure your credentials in app.js by providing your homeserver, access_token, and user_id.

    Once configured, follow these steps to run the application:

    1. Install dependencies using npm.
    2. Execute the application using node.

    Commands:

    $ npm install
    $ node app
  10. Request persistent storage using the Storage Standard API

    develop

    To prevent the browser from deleting your storage during low-space conditions, you can use the navigator.storage.persist() API to request persistent storage.

    • Chrome: Often grants persistent storage automatically based on user interaction criteria without a prompt.
    • Firefox: Typically shows a prompt to the user to grant permission. To revert to non-persistent storage, you must revoke the permission and clear the site data.
  11. Handle authenticated media downloads

    develop

    For servers supporting MSC3916, you must include an Authorization header when downloading or thumbnailing media. Use mxcUrlToHttp with the useAuthentication flag set to true, then perform a fetch using the client's access token.

    const downloadUrl = client.mxcUrlToHttp(
        /*mxcUrl=*/ "mxc://example.org/abc123",
        /*width=*/ undefined,
        /*height=*/ undefined,
        /*resizeMethod=*/ undefined,
        /*allowDirectLinks=*/ false,
        /*allowRedirects=*/ true,
        /*useAuthentication=*/ true,
    );
    
    const img = await fetch(downloadUrl, {
        headers: {
            Authorization: `Bearer ${client.getAccessToken()}`,
        },
    });