Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publish a dual CJS/ESM package with platform-specific loaders #167

Merged
merged 21 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
sandhose marked this conversation as resolved.
Show resolved Hide resolved
File renamed without changes.
File renamed without changes.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# UNRELEASED

- Update matrix-rusk-sdk to `e99939db857ca`.
- The published package is now a proper dual CommonJS/ESM package.
- The WebAssembly module is now loaded using `fetch` on Web platforms, reducing
the bundle size significantly, as well as the time it takes to compile it.
richvdh marked this conversation as resolved.
Show resolved Hide resolved

**BREAKING CHANGES**

- The WebAssembly module is no longer synchronously loaded on Web platforms
when used. This means that the `initAsync` function **must** be called before any
other functions are used. The behaviour is unchanged and still available on
Node.js.

# matrix-sdk-crypto-wasm v11.0.0

Expand Down
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,25 @@ Encryption](https://en.wikipedia.org/wiki/End-to-end_encryption)) for
```

2. Import the library into your project and initialise it.

It is recommended that you use a dynamic import, particularly in a Web
environment, because the WASM artifiact is large:
The library must be initialised before it can be used, else it will throw an error.
richvdh marked this conversation as resolved.
Show resolved Hide resolved
On Web platforms, the library will use `fetch` to get the WebAssembly binary, which should be handled by most bundlers.
richvdh marked this conversation as resolved.
Show resolved Hide resolved

```javascript
import { initAsync, Tracing, LoggerLevel, OlmMachine, UserId, DeviceId } from "@matrix-org/matrix-sdk-crypto-wasm";

async function loadCrypto(userId, deviceId) {
const matrixSdkCrypto = await import("@matrix-org/matrix-sdk-crypto-wasm");
await matrixSdkCrypto.initAsync();
// Do this before any other calls to the library
await initAsync();

// Optional: enable tracing in the rust-sdk
new matrixSdkCrypto.Tracing(matrixSdkCrypto.LoggerLevel.Trace).turnOn();
new Tracing(LoggerLevel.Trace).turnOn();

// Create a new OlmMachine
//
// The following will use an in-memory store. It is recommended to use
// indexedDB where that is available.
// See https://matrix-org.github.io/matrix-rust-sdk-crypto-wasm/classes/OlmMachine.html#initialize
const olmMachine = await matrixSdkCrypto.OlmMachine.initialize(
new matrixSdkCrypto.UserId(userId),
new matrixSdkCrypto.DeviceId(deviceId),
);
const olmMachine = await OlmMachine.initialize(new UserId(userId), new DeviceId(deviceId));

return olmMachine;
}
Expand Down
24 changes: 24 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

export * from "./pkg/matrix_sdk_crypto_wasm.d";

/**
* Load the WebAssembly module in the background, if it has not already been loaded.
*
* Returns a promise which will resolve once the other methods are ready.
*
* @returns {Promise<void>}
*/
export function initAsync(): Promise<void>;
97 changes: 97 additions & 0 deletions index.js
richvdh marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// @ts-check

// This is the entrypoint on non-node CommonJS environments.
// `asyncLoad` will load the WASM module using a `fetch` call.
const bindings = require("./pkg/matrix_sdk_crypto_wasm_bg.cjs");
richvdh marked this conversation as resolved.
Show resolved Hide resolved

const moduleUrl = require.resolve("./pkg/matrix_sdk_crypto_wasm_bg.wasm");

// We want to throw an error if the user tries to use the bindings before
// calling `initAsync`.
bindings.__wbg_set_wasm(
new Proxy(
{},
{
get() {
throw new Error(
"@matrix-org/matrix-sdk-crypto-wasm was used before it was initialized. Call `initAsync` first.",
);
},
},
),
);

/**
* Stores a promise which resolves to the WebAssembly module
* @type {Promise<WebAssembly.Module> | null}
*/
let modPromise = null;

/**
* Tracks whether the module has been instanciated or not
richvdh marked this conversation as resolved.
Show resolved Hide resolved
* @type {boolean}
*/
let initialised = false;

/**
* Loads the WASM module asynchronously
*
* @returns {Promise<WebAssembly.Module>}
*/
async function loadModule() {
if (typeof WebAssembly.compileStreaming === "function") {
return await WebAssembly.compileStreaming(fetch(moduleUrl));
}

// Fallback to fetch and compile
const response = await fetch(moduleUrl);
if (!response.ok) {
throw new Error(`Failed to fetch wasm module: ${moduleUrl}`);
}
const bytes = await response.arrayBuffer();
return await WebAssembly.compile(bytes);
}

/**
* Load the WebAssembly module in the background, if it has not already been loaded.
*
* Returns a promise which will resolve once the other methods are ready.
*
* @returns {Promise<void>}
*/
async function initAsync() {
if (!modPromise) modPromise = loadModule();
const mod = await modPromise;

if (initialised) return;
initialised = true;
/** @type {{exports: typeof import("./pkg/matrix_sdk_crypto_wasm_bg.wasm.d")}} */
// @ts-expect-error: Typescript doesn't know what the instance exports exactly
const instance = new WebAssembly.Instance(mod, {
// @ts-expect-error: The bindings don't exactly match the 'ExportValue' type
"./matrix_sdk_crypto_wasm_bg.js": bindings,
});

bindings.__wbg_set_wasm(instance.exports);
instance.exports.__wbindgen_start();
richvdh marked this conversation as resolved.
Show resolved Hide resolved
}

module.exports = {
// Re-export everything from the generated javascript wrappers
...bindings,
initAsync,
};
94 changes: 94 additions & 0 deletions index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// @ts-check

// This is the entrypoint on non-node ESM environments (such as Element Web).
// `asyncLoad` will load the WASM module using a `fetch` call.
import * as bindings from "./pkg/matrix_sdk_crypto_wasm_bg.js";

const moduleUrl = new URL("./pkg/matrix_sdk_crypto_wasm_bg.wasm", import.meta.url);

// We want to throw an error if the user tries to use the bindings before
// calling `initAsync`.
bindings.__wbg_set_wasm(
new Proxy(
{},
{
get() {
throw new Error(
"@matrix-org/matrix-sdk-crypto-wasm was used before it was initialized. Call `initAsync` first.",
);
},
},
),
);

/**
* Stores a promise which resolves to the WebAssembly module
* @type {Promise<WebAssembly.Module> | null}
*/
let modPromise = null;

/**
* Tracks whether the module has been instanciated or not
* @type {boolean}
*/
let initialised = false;

/**
* Loads the WASM module asynchronously
*
* @returns {Promise<WebAssembly.Module>}
*/
async function loadModule() {
if (typeof WebAssembly.compileStreaming === "function") {
return await WebAssembly.compileStreaming(fetch(moduleUrl));
}

// Fallback to fetch and compile
const response = await fetch(moduleUrl);
if (!response.ok) {
throw new Error(`Failed to fetch wasm module: ${moduleUrl}`);
}
const bytes = await response.arrayBuffer();
return await WebAssembly.compile(bytes);
}

/**
* Load the WebAssembly module in the background, if it has not already been loaded.
*
* Returns a promise which will resolve once the other methods are ready.
*
* @returns {Promise<void>}
*/
async function initAsync() {
richvdh marked this conversation as resolved.
Show resolved Hide resolved
if (!modPromise) modPromise = loadModule();
const mod = await modPromise;

if (initialised) return;
initialised = true;
/** @type {{exports: typeof import("./pkg/matrix_sdk_crypto_wasm_bg.wasm.d")}} */
// @ts-expect-error: Typescript doesn't know what the instance exports exactly
const instance = new WebAssembly.Instance(mod, {
// @ts-expect-error: The bindings don't exactly match the 'ExportValue' type
"./matrix_sdk_crypto_wasm_bg.js": bindings,
});

bindings.__wbg_set_wasm(instance.exports);
instance.exports.__wbindgen_start();
}

// Re-export everything from the generated javascript wrappers
export * from "./pkg/matrix_sdk_crypto_wasm_bg.js";
114 changes: 114 additions & 0 deletions node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// @ts-check

// This is the entrypoint on node-compatible CommonJS environments.
// `asyncLoad` will use `fs.readFile` to load the WASM module.
const { readFileSync } = require("node:fs");
const { readFile } = require("node:fs/promises");
const path = require("node:path");
const bindings = require("./pkg/matrix_sdk_crypto_wasm_bg.cjs");

const filename = path.join(__dirname, "pkg/matrix_sdk_crypto_wasm_bg.wasm");

// We want to automatically load the WASM module in Node environments
// synchronously if the consumer did not call `initAsync`. To do so, we install
// a `Proxy` that will intercept calls to the WASM module
richvdh marked this conversation as resolved.
Show resolved Hide resolved
bindings.__wbg_set_wasm(
new Proxy(
{},
{
get(_target, prop) {
const mod = loadModuleSync();
return initInstance(mod)[prop];
},
},
),
);

/**
* Stores a promise which resolves to the WebAssembly module
* @type {Promise<WebAssembly.Module> | null}
*/
let modPromise = null;

/**
* Tracks whether the module has been instanciated or not
* @type {boolean}
*/
let initialised = false;

/**
* Loads the WASM module synchronously
*
* It will throw if there is an attempt to laod the module asynchronously running
*
* @returns {WebAssembly.Module}
*/
function loadModuleSync() {
if (modPromise) throw new Error("The WASM module is being loadded asynchronously but hasn't finished");
const bytes = readFileSync(filename);
return new WebAssembly.Module(bytes);
}

/**
* Loads the WASM module asynchronously
*
* @returns {Promise<WebAssembly.Module>}
*/
async function loadModule() {
const bytes = await readFile(filename);
return await WebAssembly.compile(bytes);
}

/**
* Initializes the WASM module and returns the exports from the WASM module.
*
* @param {WebAssembly.Module} mod
* @returns {typeof import("./pkg/matrix_sdk_crypto_wasm_bg.wasm.d")}
*/
function initInstance(mod) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function initInstance(mod) {
function initInstance(mod) {
if (initialised) {
// This should be unreachable
throw new Error("initInstance called twice");
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually throws if you call initAsync, it loads in the background, and then init it synchronously before it finishes; but I think it is fine to throw because the consumer shouldn't do that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually throws if you call initAsync, it loads in the background, and then init it synchronously before it finishes;

Will it? In that case, wouldn't modPromise be set, so that the synchronous init will fail early with an exception?

/** @type {{exports: typeof import("./pkg/matrix_sdk_crypto_wasm_bg.wasm.d")}} */
// @ts-expect-error: Typescript doesn't know what the instance exports exactly
const instance = new WebAssembly.Instance(mod, {
// @ts-expect-error: The bindings don't exactly match the 'ExportValue' type
"./matrix_sdk_crypto_wasm_bg.js": bindings,
});

bindings.__wbg_set_wasm(instance.exports);
instance.exports.__wbindgen_start();
sandhose marked this conversation as resolved.
Show resolved Hide resolved
return instance.exports;
}

/**
* Load the WebAssembly module in the background, if it has not already been loaded.
*
* Returns a promise which will resolve once the other methods are ready.
*
* @returns {Promise<void>}
*/
async function initAsync() {
if (!modPromise) modPromise = loadModule();
const mod = await modPromise;
if (initialised) return;
initialised = true;
richvdh marked this conversation as resolved.
Show resolved Hide resolved
initInstance(mod);
}
sandhose marked this conversation as resolved.
Show resolved Hide resolved

module.exports = {
// Re-export everything from the generated javascript wrappers
...bindings,
initAsync,
};
Loading