Skip to content

Commit cad2efc

Browse files
authored
fix: Allows creating external keystore with the Worker (#1569)
* feat: enable external keystore through callbacks in worker thread * fix: handle callback requests and change base to main * chore: rebase main
1 parent d89a820 commit cad2efc

File tree

5 files changed

+139
-14
lines changed

5 files changed

+139
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.12.6 (TBD)
4+
5+
* Enables Workers with `createClientWithExternalKeystore` via callbacks ([#1569](https://github.com/0xMiden/miden-client/pull/1569))
6+
37
## 0.12.5 (2025-12-01)
48

59
* Removed the top-level await from the web-client JS entry point by lazily loading the WASM module, allowing `@demox-labs/miden-sdk` to be imported normally (including in Next.js SSR builds), and updated the worker bootstrap to match.

crates/web-client/js/constants.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
export const WorkerAction = Object.freeze({
22
INIT: "init",
33
CALL_METHOD: "callMethod",
4+
EXECUTE_CALLBACK: "executeCallback",
5+
});
6+
7+
export const CallbackType = Object.freeze({
8+
GET_KEY: "getKey",
9+
INSERT_KEY: "insertKey",
10+
SIGN: "sign",
411
});
512

613
export const MethodName = Object.freeze({

crates/web-client/js/index.js

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import loadWasm from "./wasm.js";
2-
import { MethodName, WorkerAction } from "./constants.js";
2+
import { CallbackType, MethodName, WorkerAction } from "./constants.js";
33
export * from "../Cargo.toml";
44

55
const buildTypedArraysExport = (exportObject) => {
@@ -104,12 +104,7 @@ export class WebClient {
104104
this.signCb = signCb;
105105

106106
// Check if Web Workers are available.
107-
if (
108-
typeof Worker !== "undefined" &&
109-
!this.getKeyCb &&
110-
!this.insertKeyCb &&
111-
!this.signCb
112-
) {
107+
if (typeof Worker !== "undefined") {
113108
console.log("WebClient: Web Workers are available.");
114109
// Create the worker.
115110
this.worker = new Worker(
@@ -131,7 +126,7 @@ export class WebClient {
131126
});
132127

133128
// Listen for messages from the worker.
134-
this.worker.addEventListener("message", (event) => {
129+
this.worker.addEventListener("message", async (event) => {
135130
const data = event.data;
136131

137132
// Worker script loaded.
@@ -146,6 +141,36 @@ export class WebClient {
146141
return;
147142
}
148143

144+
if (data.action === WorkerAction.EXECUTE_CALLBACK) {
145+
const { callbackType, args, requestId } = data;
146+
try {
147+
const callbackMapping = {
148+
[CallbackType.GET_KEY]: this.getKeyCb,
149+
[CallbackType.INSERT_KEY]: this.insertKeyCb,
150+
[CallbackType.SIGN]: this.signCb,
151+
};
152+
if (!callbackMapping[callbackType]) {
153+
throw new Error(`Callback ${callbackType} not available`);
154+
}
155+
const callbackFunction = callbackMapping[callbackType];
156+
let result = callbackFunction.apply(this, args);
157+
if (result instanceof Promise) {
158+
result = await result;
159+
}
160+
161+
this.worker.postMessage({
162+
callbackResult: result,
163+
callbackRequestId: requestId,
164+
});
165+
} catch (error) {
166+
this.worker.postMessage({
167+
callbackError: error.message,
168+
callbackRequestId: requestId,
169+
});
170+
}
171+
return;
172+
}
173+
149174
// Handle responses for method calls.
150175
const { requestId, error, result, methodName } = data;
151176
if (requestId && this.pendingRequests.has(requestId)) {
@@ -171,9 +196,9 @@ export class WebClient {
171196
this.rpcUrl,
172197
this.noteTransportUrl,
173198
this.seed,
174-
this.getKeyCb,
175-
this.insertKeyCb,
176-
this.signCb,
199+
!!this.getKeyCb,
200+
!!this.insertKeyCb,
201+
!!this.signCb,
177202
],
178203
});
179204
});

crates/web-client/js/types/index.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,23 @@ export declare class WebClient extends WasmWebClient {
102102
seed?: string
103103
): Promise<WebClient & WasmWebClient>;
104104

105+
static createClientWithExternalKeystore(
106+
rpcUrl?: string,
107+
noteTransportUrl?: string,
108+
seed?: string,
109+
getKeyCb?: (
110+
pubKey: Uint8Array
111+
) => Promise<Uint8Array | null | undefined> | Uint8Array | null | undefined,
112+
insertKeyCb?: (
113+
pubKey: Uint8Array,
114+
secretKey: Uint8Array
115+
) => Promise<void> | void,
116+
signCb?: (
117+
pubKey: Uint8Array,
118+
signingInputs: Uint8Array
119+
) => Promise<Uint8Array> | Uint8Array
120+
): Promise<WebClient & WasmWebClient>;
121+
105122
/** Returns the default transaction prover configured on the client. */
106123
defaultTransactionProver(): TransactionProver;
107124

crates/web-client/js/workers/web-client-methods-worker.js

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import loadWasm from "../../dist/wasm.js";
2-
import { MethodName, WorkerAction } from "../constants.js";
2+
import { CallbackType, MethodName, WorkerAction } from "../constants.js";
33

44
let wasmModule = null;
55

@@ -54,6 +54,52 @@ let ready = false; // Indicates if the worker is fully initialized.
5454
let messageQueue = []; // Queue for sequential processing.
5555
let processing = false; // Flag to ensure one message is processed at a time.
5656

57+
// Track pending callback requests
58+
let pendingCallbacks = new Map();
59+
60+
// Define proxy functions for callbacks that communicate with main thread
61+
const callbackProxies = {
62+
getKey: async (pubKey) => {
63+
return new Promise((resolve, reject) => {
64+
const requestId = `${CallbackType.GET_KEY}-${Date.now()}-${Math.random()}`;
65+
pendingCallbacks.set(requestId, { resolve, reject });
66+
67+
self.postMessage({
68+
action: WorkerAction.EXECUTE_CALLBACK,
69+
callbackType: CallbackType.GET_KEY,
70+
args: [pubKey],
71+
requestId,
72+
});
73+
});
74+
},
75+
insertKey: async (pubKey, secretKey) => {
76+
return new Promise((resolve, reject) => {
77+
const requestId = `${CallbackType.INSERT_KEY}-${Date.now()}-${Math.random()}`;
78+
pendingCallbacks.set(requestId, { resolve, reject });
79+
80+
self.postMessage({
81+
action: WorkerAction.EXECUTE_CALLBACK,
82+
callbackType: CallbackType.INSERT_KEY,
83+
args: [pubKey, secretKey],
84+
requestId,
85+
});
86+
});
87+
},
88+
sign: async (pubKey, signingInputs) => {
89+
return new Promise((resolve, reject) => {
90+
const requestId = `${CallbackType.SIGN}-${Date.now()}-${Math.random()}`;
91+
pendingCallbacks.set(requestId, { resolve, reject });
92+
93+
self.postMessage({
94+
action: WorkerAction.EXECUTE_CALLBACK,
95+
callbackType: CallbackType.SIGN,
96+
args: [pubKey, signingInputs],
97+
requestId,
98+
});
99+
});
100+
},
101+
};
102+
57103
// Define a mapping from method names to handler functions.
58104
const methodHandlers = {
59105
[MethodName.NEW_WALLET]: async (args) => {
@@ -235,11 +281,23 @@ async function processMessage(event) {
235281
const { action, args, methodName, requestId } = event.data;
236282
try {
237283
if (action === WorkerAction.INIT) {
238-
const [rpcUrl, noteTransportUrl, seed] = args;
239284
// Initialize the WASM WebClient.
285+
const [rpcUrl, noteTransportUrl, seed, getKey, insertKey, sign] = args;
240286
const wasm = await getWasmOrThrow();
241287
wasmWebClient = new wasm.WebClient();
242-
await wasmWebClient.createClient(rpcUrl, noteTransportUrl, seed);
288+
// Initialize the WASM WebClient.
289+
if (getKey || insertKey || sign) {
290+
await wasmWebClient.createClientWithExternalKeystore(
291+
rpcUrl,
292+
noteTransportUrl,
293+
seed,
294+
getKey && callbackProxies.getKey,
295+
insertKey && callbackProxies.insertKey,
296+
sign && callbackProxies.sign
297+
);
298+
} else {
299+
await wasmWebClient.createClient(rpcUrl, noteTransportUrl, seed);
300+
}
243301

244302
wasmSeed = seed;
245303
ready = true;
@@ -287,6 +345,20 @@ async function processQueue() {
287345

288346
// Enqueue incoming messages and process them sequentially.
289347
self.onmessage = (event) => {
348+
if (
349+
event.data.callbackRequestId &&
350+
pendingCallbacks.has(event.data.callbackRequestId)
351+
) {
352+
const { callbackRequestId, callbackResult, callbackError } = event.data;
353+
const { resolve, reject } = pendingCallbacks.get(callbackRequestId);
354+
pendingCallbacks.delete(callbackRequestId);
355+
if (!callbackError) {
356+
resolve(callbackResult);
357+
} else {
358+
reject(new Error(callbackError));
359+
}
360+
return;
361+
}
290362
messageQueue.push(event);
291363
processQueue();
292364
};

0 commit comments

Comments
 (0)