SDK - fix unsubscribe function for events (#3659)

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
This commit is contained in:
Mark Sinclair
2023-07-18 14:12:00 +01:00
committed by GitHub
parent c12b20f1d6
commit 9d5b582908
8 changed files with 1021 additions and 59 deletions
+3
View File
@@ -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,
},
],
},
}
+13 -2
View File
@@ -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/*"
]
}
+768 -10
View File
File diff suppressed because it is too large Load Diff