SDK - fix unsubscribe function for events (#3659)
Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/env"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import preset from 'ts-jest/presets/index.js'
|
||||
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
export default {
|
||||
...preset.defaults,
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: 'tsconfig.jest.json',
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -27,7 +27,8 @@
|
||||
"prebuild:dev": "yarn build:dependencies",
|
||||
"build:dev": "yarn build:dev:only-this",
|
||||
"build:dev:only-this": "scripts/build.sh",
|
||||
"build:local": "run-s build:dependencies:nym-client-wasm build:dev:only-this"
|
||||
"build:local": "run-s build:dependencies:nym-client-wasm build:dev:only-this",
|
||||
"test": "node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js -c=jest.config.mjs --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@npmcli/node-gyp": "^3.0.0",
|
||||
@@ -37,6 +38,11 @@
|
||||
"node-gyp": "^9.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@babel/plugin-transform-async-to-generator": "^7.14.5",
|
||||
"@babel/preset-env": "^7.15.0",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
"@rollup/plugin-inject": "^5.0.3",
|
||||
@@ -48,6 +54,8 @@
|
||||
"@rollup/plugin-wasm": "^6.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
@@ -63,6 +71,9 @@
|
||||
"rollup": "^3.9.1",
|
||||
"rollup-plugin-base64": "^1.0.1",
|
||||
"rollup-plugin-web-worker-loader": "^1.6.1",
|
||||
"typescript": "^4.8.4"
|
||||
"typescript": "^4.8.4",
|
||||
"jest": "^29.5.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@ import InlineWasmWebWorker from 'web-worker:./worker';
|
||||
import {
|
||||
BinaryMessageReceivedEvent,
|
||||
ConnectedEvent,
|
||||
EventHandlerFn,
|
||||
EventKinds,
|
||||
IWebWorker,
|
||||
IWebWorkerAsync,
|
||||
IWebWorkerEvents,
|
||||
LoadedEvent,
|
||||
MimeTypes,
|
||||
StringMessageReceivedEvent,
|
||||
RawMessageReceivedEvent,
|
||||
StringMessageReceivedEvent,
|
||||
} from './types';
|
||||
import { createSubscriptions } from './subscriptions';
|
||||
|
||||
/**
|
||||
* Client for the Nym mixnet.
|
||||
@@ -33,25 +33,13 @@ export const createNymMixnetClient = async (options?: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
const worker = await createWorker();
|
||||
|
||||
// stores the subscriptions for events
|
||||
const subscriptions: {
|
||||
[key: string]: Array<EventHandlerFn<unknown>>;
|
||||
} = {};
|
||||
|
||||
/**
|
||||
* Helper method to get typed subscriptions
|
||||
*/
|
||||
const getSubscriptions = <E>(key: EventKinds): Array<EventHandlerFn<E>> => {
|
||||
if (!subscriptions[key]) {
|
||||
subscriptions[key] = [];
|
||||
}
|
||||
return subscriptions[key] as Array<EventHandlerFn<E>>;
|
||||
};
|
||||
const subscriptions = createSubscriptions();
|
||||
const { getSubscriptions, addSubscription } = subscriptions;
|
||||
|
||||
// listen to messages from the worker, parse them and let the subscribers handle them, catching any unhandled exceptions
|
||||
worker.addEventListener('message', (msg) => {
|
||||
if (msg.data && msg.data.kind) {
|
||||
const subscribers = subscriptions[msg.data.kind];
|
||||
const subscribers = getSubscriptions(msg.data.kind);
|
||||
(subscribers || []).forEach((s) => {
|
||||
try {
|
||||
// let the subscriber handle the message
|
||||
@@ -66,36 +54,14 @@ export const createNymMixnetClient = async (options?: {
|
||||
|
||||
// manage the subscribers, returning self-unsubscribe methods
|
||||
const events: IWebWorkerEvents = {
|
||||
subscribeToConnected: (handler) => {
|
||||
getSubscriptions<ConnectedEvent>(EventKinds.Connected).push(handler);
|
||||
return () => {
|
||||
getSubscriptions<ConnectedEvent>(EventKinds.Connected).unshift(handler);
|
||||
};
|
||||
},
|
||||
subscribeToLoaded: (handler) => {
|
||||
getSubscriptions<LoadedEvent>(EventKinds.Loaded).push(handler);
|
||||
return () => {
|
||||
getSubscriptions<LoadedEvent>(EventKinds.Loaded).unshift(handler);
|
||||
};
|
||||
},
|
||||
subscribeToTextMessageReceivedEvent: (handler) => {
|
||||
getSubscriptions<StringMessageReceivedEvent>(EventKinds.StringMessageReceived).push(handler);
|
||||
return () => {
|
||||
getSubscriptions<StringMessageReceivedEvent>(EventKinds.StringMessageReceived).unshift(handler);
|
||||
};
|
||||
},
|
||||
subscribeToBinaryMessageReceivedEvent: (handler) => {
|
||||
getSubscriptions<BinaryMessageReceivedEvent>(EventKinds.BinaryMessageReceived).push(handler);
|
||||
return () => {
|
||||
getSubscriptions<BinaryMessageReceivedEvent>(EventKinds.BinaryMessageReceived).unshift(handler);
|
||||
};
|
||||
},
|
||||
subscribeToRawMessageReceivedEvent: (handler) => {
|
||||
getSubscriptions<RawMessageReceivedEvent>(EventKinds.RawMessageReceived).push(handler);
|
||||
return () => {
|
||||
getSubscriptions<RawMessageReceivedEvent>(EventKinds.RawMessageReceived).unshift(handler);
|
||||
};
|
||||
},
|
||||
subscribeToConnected: (handler) => addSubscription<ConnectedEvent>(EventKinds.Connected, handler),
|
||||
subscribeToLoaded: (handler) => addSubscription<LoadedEvent>(EventKinds.Loaded, handler),
|
||||
subscribeToTextMessageReceivedEvent: (handler) =>
|
||||
addSubscription<StringMessageReceivedEvent>(EventKinds.StringMessageReceived, handler),
|
||||
subscribeToBinaryMessageReceivedEvent: (handler) =>
|
||||
addSubscription<BinaryMessageReceivedEvent>(EventKinds.BinaryMessageReceived, handler),
|
||||
subscribeToRawMessageReceivedEvent: (handler) =>
|
||||
addSubscription<RawMessageReceivedEvent>(EventKinds.RawMessageReceived, handler),
|
||||
};
|
||||
|
||||
// let comlink handle interop with the web worker
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { createSubscriptions } from './subscriptions';
|
||||
import { EventKinds, MimeTypes, StringMessageReceivedEvent } from './types';
|
||||
|
||||
describe('wasm subscription manager', () => {
|
||||
test('works with default values', () => {
|
||||
const { getSubscriptions, fireEvent, addSubscription } = createSubscriptions();
|
||||
|
||||
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(0);
|
||||
|
||||
// the event should fire and not fail
|
||||
fireEvent(EventKinds.StringMessageReceived, {});
|
||||
|
||||
// mock a handler, fire events and check that it was called
|
||||
const mockHandler = jest.fn();
|
||||
addSubscription(EventKinds.StringMessageReceived, mockHandler);
|
||||
fireEvent(EventKinds.StringMessageReceived, {});
|
||||
expect(mockHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('adding and removing subscriptions works as expected', () => {
|
||||
const { addSubscription, getSubscriptions, fireEvent } = createSubscriptions();
|
||||
|
||||
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(0);
|
||||
|
||||
const callStats: number[] = [0, 0, 0];
|
||||
|
||||
const showDebug = false;
|
||||
|
||||
const handler1 = (e: StringMessageReceivedEvent) => {
|
||||
if (showDebug) {
|
||||
console.log('handler1', e);
|
||||
}
|
||||
callStats[0] += 1;
|
||||
};
|
||||
const handler2 = (e: StringMessageReceivedEvent) => {
|
||||
if (showDebug) {
|
||||
console.log('handler2', e);
|
||||
}
|
||||
callStats[1] += 1;
|
||||
};
|
||||
const handler3 = (e: StringMessageReceivedEvent) => {
|
||||
if (showDebug) {
|
||||
console.log('handler3', e);
|
||||
}
|
||||
callStats[2] += 1;
|
||||
};
|
||||
|
||||
const unsubcribeFn1 = addSubscription(EventKinds.StringMessageReceived, handler1);
|
||||
const unsubcribeFn2 = addSubscription(EventKinds.StringMessageReceived, handler2);
|
||||
const unsubcribeFn3 = addSubscription(EventKinds.StringMessageReceived, handler3);
|
||||
|
||||
const event: StringMessageReceivedEvent = {
|
||||
kind: EventKinds.StringMessageReceived,
|
||||
args: {
|
||||
payload: 'Testing',
|
||||
mimeType: MimeTypes.TextPlain,
|
||||
payloadRaw: new Uint8Array(),
|
||||
},
|
||||
};
|
||||
|
||||
// fire and expect all handlers to get message
|
||||
fireEvent(EventKinds.StringMessageReceived, event);
|
||||
expect(callStats[0]).toBe(1);
|
||||
expect(callStats[1]).toBe(1);
|
||||
expect(callStats[2]).toBe(1);
|
||||
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(3);
|
||||
|
||||
// unscribe and fire again
|
||||
unsubcribeFn2();
|
||||
fireEvent(EventKinds.StringMessageReceived, event);
|
||||
expect(callStats[0]).toBe(2);
|
||||
expect(callStats[1]).toBe(1);
|
||||
expect(callStats[2]).toBe(2);
|
||||
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(2);
|
||||
|
||||
// unscribe and fire again
|
||||
unsubcribeFn3();
|
||||
fireEvent(EventKinds.StringMessageReceived, event);
|
||||
expect(callStats[0]).toBe(3);
|
||||
expect(callStats[1]).toBe(1);
|
||||
expect(callStats[2]).toBe(2);
|
||||
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(1);
|
||||
|
||||
// unscribe and fire again
|
||||
unsubcribeFn1();
|
||||
fireEvent(EventKinds.StringMessageReceived, event);
|
||||
expect(callStats[0]).toBe(3);
|
||||
expect(callStats[1]).toBe(1);
|
||||
expect(callStats[2]).toBe(2);
|
||||
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(0);
|
||||
|
||||
// nothing is subscribed, so fire again and check
|
||||
fireEvent(EventKinds.StringMessageReceived, event);
|
||||
expect(callStats[0]).toBe(3);
|
||||
expect(callStats[1]).toBe(1);
|
||||
expect(callStats[2]).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { EventHandlerFn } from './types';
|
||||
import { EventKinds } from './types';
|
||||
|
||||
type ISubscriptions = {
|
||||
[key: string]: Array<EventHandlerFn<unknown>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a subscription manager.
|
||||
*/
|
||||
export const createSubscriptions = () => {
|
||||
// stores the subscriptions for events
|
||||
const subscriptions: ISubscriptions = {};
|
||||
|
||||
/**
|
||||
* Helper method to get typed subscriptions.
|
||||
*/
|
||||
const getSubscriptions = <E>(key: EventKinds): Array<EventHandlerFn<E>> => {
|
||||
if (!subscriptions[key]) {
|
||||
subscriptions[key] = [];
|
||||
}
|
||||
return subscriptions[key] as Array<EventHandlerFn<E>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a subscription.
|
||||
*/
|
||||
const removeSubscription = <E>(key: EventKinds, handler: EventHandlerFn<E>) => {
|
||||
if (!subscriptions[key]) {
|
||||
subscriptions[key] = [];
|
||||
}
|
||||
const items: Array<EventHandlerFn<unknown>> = (subscriptions[key] as Array<EventHandlerFn<unknown>>).filter(
|
||||
(h) => h !== handler,
|
||||
);
|
||||
subscriptions[key] = items;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add typed subscription.
|
||||
*/
|
||||
const addSubscription = <E>(key: EventKinds, handler: EventHandlerFn<E>) => {
|
||||
getSubscriptions(key).push(handler as EventHandlerFn<unknown>);
|
||||
|
||||
return () => {
|
||||
removeSubscription(key, handler);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Fires an event.
|
||||
*/
|
||||
const fireEvent = <E>(key: EventKinds, event: E) => {
|
||||
getSubscriptions(key).forEach((handler) => {
|
||||
try {
|
||||
handler(event);
|
||||
} catch (e: any) {
|
||||
console.error(`Unhandled exception in handler for ${key}: `, e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
getSubscriptions,
|
||||
addSubscription,
|
||||
removeSubscription,
|
||||
fireEvent,
|
||||
subscriptions,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2021",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext",
|
||||
"webworker"
|
||||
],
|
||||
"module": "CommonJS",
|
||||
"target": "es5",
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"declaration": true,
|
||||
"baseUrl": ".",
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"jest.*",
|
||||
"webpack.config.js",
|
||||
"webpack.prod.js",
|
||||
"webpack.common.js",
|
||||
"node_modules",
|
||||
"**/node_modules",
|
||||
"dist",
|
||||
"**/dist",
|
||||
"scripts",
|
||||
"jest",
|
||||
"__tests__",
|
||||
"**/__tests__",
|
||||
"__jest__",
|
||||
"**/__jest__",
|
||||
"config/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user