Skip to content

Commit db1af39

Browse files
authored
Publish a dual CJS/ESM package with platform-specific loaders (#167)
Proper async loading of the WASM module, with custom loaders for different environments.
1 parent 62e004f commit db1af39

15 files changed

+1604
-1518
lines changed
File renamed without changes.
File renamed without changes.

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
# UNRELEASED
22

33
- Update matrix-rusk-sdk to `e99939db857ca`.
4+
- The published package is now a proper dual CommonJS/ESM package.
5+
- The WebAssembly module is now loaded using `fetch` on Web platforms, reducing
6+
the bundle size significantly, as well as the time it takes to compile it.
7+
([#167](https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/pull/167))
8+
9+
**BREAKING CHANGES**
10+
11+
- The WebAssembly module is no longer synchronously loaded on Web platforms
12+
when used. This means that the `initAsync` function **must** be called before any
13+
other functions are used. The behaviour is unchanged and still available on
14+
Node.js.
415

516
# matrix-sdk-crypto-wasm v11.0.0
617

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,27 @@ Encryption](https://en.wikipedia.org/wiki/End-to-end_encryption)) for
2727
2828
2. Import the library into your project and initialise it.
2929
30-
It is recommended that you use a dynamic import, particularly in a Web
31-
environment, because the WASM artifiact is large:
30+
On Web platforms, the library must be initialised by calling `initAsync`
31+
before it can be used, else it will throw an error. This is also recommended
32+
on other platforms, as it allows the WebAssembly module to be loaded
33+
asynchronously.
3234
3335
```javascript
36+
import { initAsync, Tracing, LoggerLevel, OlmMachine, UserId, DeviceId } from "@matrix-org/matrix-sdk-crypto-wasm";
37+
3438
async function loadCrypto(userId, deviceId) {
35-
const matrixSdkCrypto = await import("@matrix-org/matrix-sdk-crypto-wasm");
36-
await matrixSdkCrypto.initAsync();
39+
// Do this before any other calls to the library
40+
await initAsync();
3741
3842
// Optional: enable tracing in the rust-sdk
39-
new matrixSdkCrypto.Tracing(matrixSdkCrypto.LoggerLevel.Trace).turnOn();
43+
new Tracing(LoggerLevel.Trace).turnOn();
4044
4145
// Create a new OlmMachine
4246
//
4347
// The following will use an in-memory store. It is recommended to use
4448
// indexedDB where that is available.
4549
// See https://matrix-org.github.io/matrix-rust-sdk-crypto-wasm/classes/OlmMachine.html#initialize
46-
const olmMachine = await matrixSdkCrypto.OlmMachine.initialize(
47-
new matrixSdkCrypto.UserId(userId),
48-
new matrixSdkCrypto.DeviceId(deviceId),
49-
);
50+
const olmMachine = await OlmMachine.initialize(new UserId(userId), new DeviceId(deviceId));
5051
5152
return olmMachine;
5253
}

index.d.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2024 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
export * from "./pkg/matrix_sdk_crypto_wasm.d";
16+
17+
/**
18+
* Load the WebAssembly module in the background, if it has not already been loaded.
19+
*
20+
* Returns a promise which will resolve once the other methods are ready.
21+
*
22+
* @returns {Promise<void>}
23+
*/
24+
export function initAsync(): Promise<void>;

index.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2024 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// @ts-check
16+
17+
/**
18+
* This is the entrypoint on non-node CommonJS environments.
19+
* `asyncLoad` will load the WASM module using a `fetch` call.
20+
*/
21+
22+
const bindings = require("./pkg/matrix_sdk_crypto_wasm_bg.cjs");
23+
24+
const moduleUrl = require.resolve("./pkg/matrix_sdk_crypto_wasm_bg.wasm");
25+
26+
// We want to throw an error if the user tries to use the bindings before
27+
// calling `initAsync`.
28+
bindings.__wbg_set_wasm(
29+
new Proxy(
30+
{},
31+
{
32+
get() {
33+
throw new Error(
34+
"@matrix-org/matrix-sdk-crypto-wasm was used before it was initialized. Call `initAsync` first.",
35+
);
36+
},
37+
},
38+
),
39+
);
40+
41+
/**
42+
* Stores a promise of the `loadModule` call
43+
* @type {Promise<void> | null}
44+
*/
45+
let modPromise = null;
46+
47+
/**
48+
* Loads the WASM module asynchronously
49+
*
50+
* @returns {Promise<void>}
51+
*/
52+
async function loadModule() {
53+
let mod;
54+
if (typeof WebAssembly.compileStreaming === "function") {
55+
mod = await WebAssembly.compileStreaming(fetch(moduleUrl));
56+
} else {
57+
// Fallback to fetch and compile
58+
const response = await fetch(moduleUrl);
59+
if (!response.ok) {
60+
throw new Error(`Failed to fetch wasm module: ${moduleUrl}`);
61+
}
62+
const bytes = await response.arrayBuffer();
63+
mod = await WebAssembly.compile(bytes);
64+
}
65+
66+
/** @type {{exports: typeof import("./pkg/matrix_sdk_crypto_wasm_bg.wasm.d")}} */
67+
// @ts-expect-error: Typescript doesn't know what the instance exports exactly
68+
const instance = new WebAssembly.Instance(mod, {
69+
// @ts-expect-error: The bindings don't exactly match the 'ExportValue' type
70+
"./matrix_sdk_crypto_wasm_bg.js": bindings,
71+
});
72+
73+
bindings.__wbg_set_wasm(instance.exports);
74+
instance.exports.__wbindgen_start();
75+
}
76+
77+
/**
78+
* Load the WebAssembly module in the background, if it has not already been loaded.
79+
*
80+
* Returns a promise which will resolve once the other methods are ready.
81+
*
82+
* @returns {Promise<void>}
83+
*/
84+
async function initAsync() {
85+
if (!modPromise) modPromise = loadModule();
86+
await modPromise;
87+
}
88+
89+
module.exports = {
90+
// Re-export everything from the generated javascript wrappers
91+
...bindings,
92+
initAsync,
93+
};

index.mjs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2024 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// @ts-check
16+
17+
/**
18+
* This is the entrypoint on non-node ESM environments (such as Element Web).
19+
* `asyncLoad` will load the WASM module using a `fetch` call.
20+
*/
21+
22+
import * as bindings from "./pkg/matrix_sdk_crypto_wasm_bg.js";
23+
24+
const moduleUrl = new URL("./pkg/matrix_sdk_crypto_wasm_bg.wasm", import.meta.url);
25+
26+
// We want to throw an error if the user tries to use the bindings before
27+
// calling `initAsync`.
28+
bindings.__wbg_set_wasm(
29+
new Proxy(
30+
{},
31+
{
32+
get() {
33+
throw new Error(
34+
"@matrix-org/matrix-sdk-crypto-wasm was used before it was initialized. Call `initAsync` first.",
35+
);
36+
},
37+
},
38+
),
39+
);
40+
41+
/**
42+
* Stores a promise of the `loadModule` call
43+
* @type {Promise<void> | null}
44+
*/
45+
let modPromise = null;
46+
47+
/**
48+
* Loads the WASM module asynchronously
49+
*
50+
* @returns {Promise<void>}
51+
*/
52+
async function loadModule() {
53+
let mod;
54+
if (typeof WebAssembly.compileStreaming === "function") {
55+
mod = await WebAssembly.compileStreaming(fetch(moduleUrl));
56+
} else {
57+
// Fallback to fetch and compile
58+
const response = await fetch(moduleUrl);
59+
if (!response.ok) {
60+
throw new Error(`Failed to fetch wasm module: ${moduleUrl}`);
61+
}
62+
const bytes = await response.arrayBuffer();
63+
mod = await WebAssembly.compile(bytes);
64+
}
65+
66+
/** @type {{exports: typeof import("./pkg/matrix_sdk_crypto_wasm_bg.wasm.d")}} */
67+
// @ts-expect-error: Typescript doesn't know what the instance exports exactly
68+
const instance = new WebAssembly.Instance(mod, {
69+
// @ts-expect-error: The bindings don't exactly match the 'ExportValue' type
70+
"./matrix_sdk_crypto_wasm_bg.js": bindings,
71+
});
72+
73+
bindings.__wbg_set_wasm(instance.exports);
74+
instance.exports.__wbindgen_start();
75+
}
76+
77+
/**
78+
* Load the WebAssembly module in the background, if it has not already been loaded.
79+
*
80+
* Returns a promise which will resolve once the other methods are ready.
81+
*
82+
* @returns {Promise<void>}
83+
*/
84+
export async function initAsync() {
85+
if (!modPromise) modPromise = loadModule();
86+
await modPromise;
87+
}
88+
89+
// Re-export everything from the generated javascript wrappers
90+
export * from "./pkg/matrix_sdk_crypto_wasm_bg.js";

node.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2024 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// @ts-check
16+
17+
/**
18+
* This is the entrypoint on node-compatible CommonJS environments.
19+
* `asyncLoad` will use `fs.readFile` to load the WASM module.
20+
*/
21+
22+
const { readFileSync } = require("node:fs");
23+
const { readFile } = require("node:fs/promises");
24+
const path = require("node:path");
25+
const bindings = require("./pkg/matrix_sdk_crypto_wasm_bg.cjs");
26+
27+
const filename = path.join(__dirname, "pkg/matrix_sdk_crypto_wasm_bg.wasm");
28+
29+
// In node environments, we want to automatically load the WASM module
30+
// synchronously if the consumer did not call `initAsync`. To do so, we install
31+
// a `Proxy` that will intercept calls to the WASM module.
32+
bindings.__wbg_set_wasm(
33+
new Proxy(
34+
{},
35+
{
36+
get(_target, prop) {
37+
const mod = loadModuleSync();
38+
return initInstance(mod)[prop];
39+
},
40+
},
41+
),
42+
);
43+
44+
/**
45+
* Stores a promise which resolves to the WebAssembly module
46+
* @type {Promise<WebAssembly.Module> | null}
47+
*/
48+
let modPromise = null;
49+
50+
/**
51+
* Tracks whether the module has been instantiated or not
52+
* @type {boolean}
53+
*/
54+
let initialised = false;
55+
56+
/**
57+
* Loads the WASM module synchronously
58+
*
59+
* It will throw if there is an attempt to laod the module asynchronously running
60+
*
61+
* @returns {WebAssembly.Module}
62+
*/
63+
function loadModuleSync() {
64+
if (modPromise) throw new Error("The WASM module is being loadded asynchronously but hasn't finished");
65+
const bytes = readFileSync(filename);
66+
return new WebAssembly.Module(bytes);
67+
}
68+
69+
/**
70+
* Loads the WASM module asynchronously
71+
*
72+
* @returns {Promise<WebAssembly.Module>}
73+
*/
74+
async function loadModule() {
75+
const bytes = await readFile(filename);
76+
return await WebAssembly.compile(bytes);
77+
}
78+
79+
/**
80+
* Initializes the WASM module and returns the exports from the WASM module.
81+
*
82+
* @param {WebAssembly.Module} mod
83+
* @returns {typeof import("./pkg/matrix_sdk_crypto_wasm_bg.wasm.d")}
84+
*/
85+
function initInstance(mod) {
86+
if (initialised) throw new Error("initInstance called twice");
87+
88+
/** @type {{exports: typeof import("./pkg/matrix_sdk_crypto_wasm_bg.wasm.d")}} */
89+
// @ts-expect-error: Typescript doesn't know what the instance exports exactly
90+
const instance = new WebAssembly.Instance(mod, {
91+
// @ts-expect-error: The bindings don't exactly match the 'ExportValue' type
92+
"./matrix_sdk_crypto_wasm_bg.js": bindings,
93+
});
94+
95+
bindings.__wbg_set_wasm(instance.exports);
96+
instance.exports.__wbindgen_start();
97+
initialised = true;
98+
return instance.exports;
99+
}
100+
101+
/**
102+
* Load the WebAssembly module in the background, if it has not already been loaded.
103+
*
104+
* Returns a promise which will resolve once the other methods are ready.
105+
*
106+
* @returns {Promise<void>}
107+
*/
108+
async function initAsync() {
109+
if (initialised) return;
110+
if (!modPromise) modPromise = loadModule().then(initInstance);
111+
await modPromise;
112+
}
113+
114+
module.exports = {
115+
// Re-export everything from the generated javascript wrappers
116+
...bindings,
117+
initAsync,
118+
};

0 commit comments

Comments
 (0)