Skip to content

Commit

Permalink
refactor(transport): background sessions improved
Browse files Browse the repository at this point in the history
  • Loading branch information
marekrjpolak authored and mroz22 committed Oct 2, 2024
1 parent 6566b02 commit 7644107
Show file tree
Hide file tree
Showing 10 changed files with 133 additions and 234 deletions.
26 changes: 3 additions & 23 deletions packages/connect/setupJest.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
/* WARNING! This file should be imported ONLY in tests! */

import { AbstractApiTransport, UsbApi, SessionsBackground } from '@trezor/transport';
import { AbstractApiTransport, UsbApi } from '@trezor/transport';
import { DeviceModelInternal, type Features, type FirmwareRelease } from './src/types';

class TestTransport extends AbstractApiTransport {
name = 'TestTransport' as any;

init() {
return this.scheduleAction(() => {
const sessionsBackground = new SessionsBackground();
this.sessionsClient.init({
requestFn: params => sessionsBackground.handleMessage(params),
registerBackgroundCallbacks: onDescriptorsCallback => {
sessionsBackground.on('descriptors', descriptors => {
onDescriptorsCallback(descriptors);
});
},
});
this.stopped = false;

return this.sessionsClient.handshake();
});
}
}

// mock of navigator.usb
Expand Down Expand Up @@ -52,14 +35,11 @@ const createTransportApi = (override = {}) =>
...override,
}) as unknown as UsbApi;

export const createTestTransport = (apiMethods = {}) => {
const transport = new TestTransport({
export const createTestTransport = (apiMethods = {}) =>
new TestTransport({
api: createTransportApi(apiMethods),
});

return transport;
};

export const getDeviceFeatures = (feat?: Partial<Features>): Features => ({
vendor: 'trezor.io',
major_version: 2,
Expand Down
10 changes: 1 addition & 9 deletions packages/transport-bridge/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,7 @@ export const createCore = (apiArg: 'usb' | 'udp' | AbstractApi, logger?: Log) =>
let api: AbstractApi;

const sessionsBackground = new SessionsBackground();

const sessionsClient = new SessionsClient();
sessionsClient.init({
requestFn: args => sessionsBackground.handleMessage(args),
registerBackgroundCallbacks: () => {},
});
sessionsBackground.on('descriptors', descriptors => {
sessionsClient.emit('descriptors', descriptors);
});
const sessionsClient = new SessionsClient(sessionsBackground);

if (typeof apiArg === 'string') {
api =
Expand Down
133 changes: 50 additions & 83 deletions packages/transport/src/sessions/background-browser.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,69 @@
import type { Descriptor } from '../types';
import { SessionsBackground } from './background';

import { RegisterBackgroundCallbacks } from './types';

const defaultSessionsBackgroundUrl =
window.location.origin +
`${process.env.ASSET_PREFIX || ''}/workers/sessions-background-sharedworker.js`
// just in case so that whoever defines ASSET_PREFIX does not need to worry about trailing slashes
.replace(/\/+/g, '/');
import { HandleMessageParams, HandleMessageResponse, SessionsBackgroundInterface } from './types';

/**
* calling initBackgroundInBrowser initiates sessions-background for browser based environments and returns:
* - `requestFn` which is used to send messages to sessions background
* - `registerBackgroundCallbacks` which is used to accept information about descriptors change from
* creating BrowserSessionsBackground initiates sessions-background for browser based environments and provides:
* - `handleMessage` which is used to send messages to sessions background
* - `on` which is used to accept information about descriptors change from
* another tab and notify local transport
* if possible sessions background utilizes native Sharedworker. If for whatever reason
* Sharedworker is not available, it fallbacks to local module behavior
* Sharedworker is not available, the constructor throws an error.
*/
export const initBackgroundInBrowser = async (
sessionsBackgroundUrl = defaultSessionsBackgroundUrl,
): Promise<{
background: SessionsBackground | SharedWorker;
requestFn: SessionsBackground['handleMessage'];
registerBackgroundCallbacks: RegisterBackgroundCallbacks;
}> => {
try {
// fetch to validate - failed fetch via SharedWorker constructor does not throw. It even hangs resulting in all kinds of weird behaviors
await fetch(sessionsBackgroundUrl, { method: 'HEAD' }).then(response => {
if (!response.ok) {
throw new Error(
`Failed to fetch sessions-background SharedWorker from url: ${sessionsBackgroundUrl}`,
);
}
});
const background = new SharedWorker(sessionsBackgroundUrl, {
export class BrowserSessionsBackground implements SessionsBackgroundInterface {
private readonly background;

constructor(sessionsBackgroundUrl: string) {
this.background = new SharedWorker(sessionsBackgroundUrl, {
name: '@trezor/connect-web transport sessions worker',
});
}

const requestFn: SessionsBackground['handleMessage'] = (
params: Parameters<SessionsBackground['handleMessage']>[0],
) =>
new Promise(resolve => {
const onmessage = (message: MessageEvent<any>) => {
if (params.id === message.data.id) {
resolve(message.data);
background.port.removeEventListener('message', onmessage);
}
};
handleMessage<M extends HandleMessageParams>(params: M): Promise<HandleMessageResponse<M>> {
const { background } = this;

background.port.addEventListener('message', onmessage);
return new Promise(resolve => {
const onmessage = (message: MessageEvent<any>) => {
if (params.id === message.data.id) {
resolve(message.data);
background.port.removeEventListener('message', onmessage);
}
};

background.port.onmessageerror = message => {
// not sure under what circumstances this error occurs. let's observe it during testing
console.error('background-browser onmessageerror,', message);
background.port.addEventListener('message', onmessage);

background.port.removeEventListener('message', onmessage);
};
background.port.postMessage(params);
});
background.port.onmessageerror = message => {
// not sure under what circumstances this error occurs. let's observe it during testing
console.error('background-browser onmessageerror,', message);

const registerBackgroundCallbacks: RegisterBackgroundCallbacks = onDescriptorsCallback => {
background.port.addEventListener(
'message',
(
e: MessageEvent<
// either standard response from sessions background (we ignore this one)
| Awaited<ReturnType<SessionsBackground['handleMessage']>>
// or artificially broadcasted message to all clients (see background-sharedworker)
| { type: 'descriptors'; payload: Descriptor[] }
>,
) => {
if ('type' in e?.data) {
if (e.data.type === 'descriptors') {
onDescriptorsCallback(e.data.payload);
}
}
},
);
};
background.port.removeEventListener('message', onmessage);
};
background.port.postMessage(params);
});
}

return { background, requestFn, registerBackgroundCallbacks };
} catch (err) {
console.warn(
'Unable to load background-sharedworker. Falling back to use local module. Say bye bye to tabs synchronization. Error details: ',
err.message,
on(_event: 'descriptors', listener: (descriptors: Descriptor[]) => void): void {
this.background.port.addEventListener(
'message',
(
e: MessageEvent<
// either standard response from sessions background (we ignore this one)
| Awaited<ReturnType<SessionsBackground['handleMessage']>>
// or artificially broadcasted message to all clients (see background-sharedworker)
| { type: 'descriptors'; payload: Descriptor[] }
>,
) => {
if ('type' in e?.data) {
if (e.data.type === 'descriptors') {
listener(e.data.payload);
}
}
},
);
}

const background = new SessionsBackground();
const registerBackgroundCallbacks: RegisterBackgroundCallbacks = onDescriptorsCallback => {
background.on('descriptors', descriptors => {
onDescriptorsCallback(descriptors);
});
};

return {
background,
requestFn: background.handleMessage.bind(background),
registerBackgroundCallbacks,
};
dispose() {
/* is it needed? */
}
};
}
19 changes: 12 additions & 7 deletions packages/transport/src/sessions/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
GetPathBySessionRequest,
HandleMessageParams,
HandleMessageResponse,
SessionsBackgroundInterface,
} from './types';
import type { Descriptor, PathInternal, Success } from '../types';
import { PathPublic, Session } from '../types';
Expand All @@ -35,13 +36,16 @@ type DescriptorsDict = Record<PathInternal, Descriptor>;
// in nodeusb, enumeration operation takes ~3 seconds
const lockDuration = 1000 * 4;

export class SessionsBackground extends TypedEmitter<{
/**
* updated descriptors (session has changed)
* note: we can't send diff from here (see abstract transport) although it would make sense, because we need to support also bridge which does not use this sessions background.
*/
descriptors: Descriptor[];
}> {
export class SessionsBackground
extends TypedEmitter<{
/**
* updated descriptors (session has changed)
* note: we can't send diff from here (see abstract transport) although it would make sense, because we need to support also bridge which does not use this sessions background.
*/
descriptors: Descriptor[];
}>
implements SessionsBackgroundInterface
{
/**
* Dictionary where key is path and value is Descriptor
*/
Expand Down Expand Up @@ -323,5 +327,6 @@ export class SessionsBackground extends TypedEmitter<{
this.locksTimeoutQueue.forEach(timeout => clearTimeout(timeout));
this.descriptors = {};
this.lastSessionId = 0;
this.removeAllListeners();
}
}
47 changes: 18 additions & 29 deletions packages/transport/src/sessions/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
ReleaseDoneRequest,
GetPathBySessionRequest,
AcquireDoneRequest,
RegisterBackgroundCallbacks,
SessionsBackgroundInterface,
HandleMessageParams,
} from './types';
import { SessionsBackground } from './background';

/**
* SessionsClient gives you API for communication with SessionsBackground.
Expand All @@ -20,39 +20,28 @@ import { SessionsBackground } from './background';
export class SessionsClient extends TypedEmitter<{
descriptors: Descriptor[];
}> {
// request method responsible for communication with sessions background.
private request: SessionsBackground['handleMessage'] = () => {
throw new Error('SessionsClient: request method not provided');
};

// used only for debugging - discriminating sessions clients in sessions background log
private caller = getWeakRandomId(3);
private id: number = 0;
private id;
private background;

public init({
requestFn,
registerBackgroundCallbacks,
}: {
requestFn: SessionsBackground['handleMessage'];
registerBackgroundCallbacks?: RegisterBackgroundCallbacks;
}) {
constructor(background: SessionsBackgroundInterface) {
super();
this.id = 0;
this.request = params => {
if (!requestFn) {
throw new Error('SessionsClient: requestFn not provided');
}
params.caller = this.caller;
params.id = this.id;
this.id++;
this.background = background;
background.on('descriptors', descriptors => this.emit('descriptors', descriptors));
}

public setBackground(background: SessionsBackgroundInterface) {
this.background.dispose();

return requestFn(params);
};
this.id = 0;
this.background = background;
background.on('descriptors', descriptors => this.emit('descriptors', descriptors));
}

if (registerBackgroundCallbacks) {
registerBackgroundCallbacks(descriptors => {
this.emit('descriptors', descriptors);
});
}
private request<M extends HandleMessageParams>(params: M) {
return this.background.handleMessage({ ...params, caller: this.caller, id: this.id++ });
}

public handshake() {
Expand Down
8 changes: 5 additions & 3 deletions packages/transport/src/sessions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ export type HandleMessageResponse<P> = P extends { type: infer T }
: never
: never;

export type RegisterBackgroundCallbacks = (
onDescriptorsCallback: (args: Descriptor[]) => void,
) => void;
export interface SessionsBackgroundInterface {
on(event: 'descriptors', listener: (descriptors: Descriptor[]) => void): void;
handleMessage<M extends HandleMessageParams>(message: M): Promise<HandleMessageResponse<M>>;
dispose(): void;
}
18 changes: 5 additions & 13 deletions packages/transport/src/transports/abstractApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { receiveAndParse } from '../utils/receive';
import { SessionsClient } from '../sessions/client';
import * as ERRORS from '../errors';
import { Session } from '../types';
import { SessionsBackgroundInterface } from '../sessions/types';

interface ConstructorParams extends AbstractTransportParams {
api: AbstractApi;
Expand All @@ -24,30 +25,21 @@ interface ConstructorParams extends AbstractTransportParams {
export abstract class AbstractApiTransport extends AbstractTransport {
// sessions client is a standardized interface for communicating with sessions backend
// which can live in couple of context (shared worker, local module, websocket server etc)
protected sessionsClient = new SessionsClient();
private sessionsBackground = new SessionsBackground();
protected sessionsClient: SessionsClient;
protected sessionsBackground: SessionsBackgroundInterface;

protected api: AbstractApi;

constructor({ messages, api, logger }: ConstructorParams) {
super({ messages, logger });
this.api = api;
this.sessionsBackground = new SessionsBackground();
this.sessionsClient = new SessionsClient(this.sessionsBackground);
}

public init({ signal }: AbstractTransportMethodParams<'init'> = {}) {
return this.scheduleAction(
async () => {
// in nodeusb there is no synchronization yet. this is a followup and needs to be decided
// so far, sessionsClient has direct access to sessionBackground
this.sessionsClient.init({
requestFn: args => this.sessionsBackground.handleMessage(args),
registerBackgroundCallbacks: () => {},
});

this.sessionsBackground.on('descriptors', descriptors => {
this.sessionsClient.emit('descriptors', descriptors);
});

const handshakeRes = await this.sessionsClient.handshake();
this.stopped = !handshakeRes.success;

Expand Down
Loading

0 comments on commit 7644107

Please sign in to comment.