mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 09:22:24 +00:00
🧹 Centralize SimpleEventEmitter class
Extract duplicated SimpleEventEmitter class from multiple service files into a shared utility file `js/utils/eventEmitter.js`. - Created `js/utils/eventEmitter.js` with unified implementation. - Refactored `AuthService`, `NostrService`, `ModerationService`, and `PlaybackService` to use the shared class. - Added support for constructor-defined context strings for improved error logging. - Added comprehensive unit tests for `SimpleEventEmitter`. - Verified changes with targeted service tests. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
84485e5607
commit
c6b7d5ab32
6 files changed
+126
-181
No files matched your search
@@ -7,6 +7,7 @@ import {
|
||||
getCurrentUserNpub,
|
||||
} from "../state/appState.js";
|
||||
import { userLogger } from "../utils/logger.js";
|
||||
import { SimpleEventEmitter } from "../utils/eventEmitter.js";
|
||||
import { requestDefaultExtensionPermissions } from "../nostr/defaultClient.js";
|
||||
import {
|
||||
getSavedProfiles,
|
||||
@@ -27,47 +28,6 @@ import getDefaultAuthProvider, {
|
||||
providers as defaultAuthProviders,
|
||||
} from "./authProviders/index.js";
|
||||
|
||||
class SimpleEventEmitter {
|
||||
constructor(logger = null) {
|
||||
this.logger = typeof logger === "function" ? logger : null;
|
||||
this.listeners = new Map();
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
if (typeof handler !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, new Set());
|
||||
}
|
||||
const handlers = this.listeners.get(eventName);
|
||||
handlers.add(handler);
|
||||
return () => {
|
||||
handlers.delete(handler);
|
||||
if (!handlers.size) {
|
||||
this.listeners.delete(eventName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
emit(eventName, detail) {
|
||||
const handlers = this.listeners.get(eventName);
|
||||
if (!handlers || !handlers.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const handler of Array.from(handlers)) {
|
||||
try {
|
||||
handler(detail);
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
this.logger(`AuthService listener for "${eventName}" threw`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HEX64_REGEX = /^[0-9a-f]{64}$/i;
|
||||
const FALLBACK_PROFILE = {
|
||||
name: "Unknown",
|
||||
@@ -116,7 +76,7 @@ export default class AuthService {
|
||||
} catch (logError) {
|
||||
userLogger.warn("[AuthService] logger threw", logError);
|
||||
}
|
||||
});
|
||||
}, "AuthService");
|
||||
|
||||
if (typeof getAuthProvider === "function") {
|
||||
this.resolveAuthProvider = (providerId) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { accessControl } from "../accessControl.js";
|
||||
import { userBlocks, USER_BLOCK_EVENTS } from "../userBlocks.js";
|
||||
import { buildReportEvent } from "../nostrEventSchemas.js";
|
||||
import logger from "../utils/logger.js";
|
||||
import { SimpleEventEmitter } from "../utils/eventEmitter.js";
|
||||
|
||||
const AUTOPLAY_TRUST_THRESHOLD = 1;
|
||||
const BLUR_TRUST_THRESHOLD = 1;
|
||||
@@ -25,54 +26,6 @@ function normalizeUserLogger(candidate) {
|
||||
return logger.user;
|
||||
}
|
||||
|
||||
class SimpleEventEmitter {
|
||||
constructor(logHandler = null) {
|
||||
this.listeners = new Map();
|
||||
this.logHandler = typeof logHandler === "function" ? logHandler : null;
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
if (typeof eventName !== "string" || typeof handler !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, new Set());
|
||||
}
|
||||
|
||||
const handlers = this.listeners.get(eventName);
|
||||
handlers.add(handler);
|
||||
|
||||
return () => {
|
||||
handlers.delete(handler);
|
||||
if (!handlers.size) {
|
||||
this.listeners.delete(eventName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
emit(eventName, detail) {
|
||||
const handlers = this.listeners.get(eventName);
|
||||
if (!handlers || !handlers.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const handler of Array.from(handlers)) {
|
||||
try {
|
||||
handler(detail);
|
||||
} catch (error) {
|
||||
if (this.logHandler) {
|
||||
try {
|
||||
this.logHandler(`moderationService listener for "${eventName}" threw`, error);
|
||||
} catch (logError) {
|
||||
logger.user.warn("[moderationService] listener logger threw", logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLogger(candidate) {
|
||||
if (typeof candidate === "function") {
|
||||
return candidate;
|
||||
@@ -549,7 +502,7 @@ export class ModerationService {
|
||||
} catch (logError) {
|
||||
logger.user.warn("[moderationService] logger threw", logError);
|
||||
}
|
||||
});
|
||||
}, "moderationService");
|
||||
|
||||
this.userBlockUnsubscribe = null;
|
||||
this.userBlockRefreshQueue = Promise.resolve();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { convertEventToVideo } from "../nostr/index.js";
|
||||
import { accessControl } from "../accessControl.js";
|
||||
import { ALLOW_NSFW_CONTENT } from "../config.js";
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
import { SimpleEventEmitter } from "../utils/eventEmitter.js";
|
||||
import { LRUCache } from "../utils/lruCache.js";
|
||||
import { describeAttachment, extractAttachmentsFromMessage } from "../attachments/attachmentUtils.js";
|
||||
import moderationService from "./moderationService.js";
|
||||
@@ -76,54 +77,6 @@ function persistFilteredVideos(videos) {
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleEventEmitter {
|
||||
constructor(logger = null) {
|
||||
this.logger = typeof logger === "function" ? logger : null;
|
||||
this.listeners = new Map();
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
if (typeof handler !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, new Set());
|
||||
}
|
||||
|
||||
const handlers = this.listeners.get(eventName);
|
||||
handlers.add(handler);
|
||||
|
||||
return () => {
|
||||
handlers.delete(handler);
|
||||
if (!handlers.size) {
|
||||
this.listeners.delete(eventName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
emit(eventName, detail) {
|
||||
const handlers = this.listeners.get(eventName);
|
||||
if (!handlers || !handlers.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const handler of Array.from(handlers)) {
|
||||
try {
|
||||
handler(detail);
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
try {
|
||||
this.logger(`nostrService listener for "${eventName}" threw`, error);
|
||||
} catch (logError) {
|
||||
userLogger.warn("[nostrService] listener logger threw", logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLogger(logger) {
|
||||
if (typeof logger === "function") {
|
||||
return logger;
|
||||
@@ -547,7 +500,7 @@ export class NostrService {
|
||||
} catch (logError) {
|
||||
userLogger.warn("[nostrService] logger threw", logError);
|
||||
}
|
||||
});
|
||||
}, "nostrService");
|
||||
this.videosMap = null;
|
||||
this.videosByAuthorIndex = null;
|
||||
this.authorIndexDirty = false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { userLogger } from "../utils/logger.js";
|
||||
import { SimpleEventEmitter } from "../utils/eventEmitter.js";
|
||||
import { PLAYBACK_START_TIMEOUT } from "../constants.js";
|
||||
// js/services/playbackService.js
|
||||
|
||||
@@ -19,46 +20,6 @@ import { PLAYBACK_START_TIMEOUT } from "../constants.js";
|
||||
* old session is cancelled and doesn't overwrite the new one.
|
||||
*/
|
||||
|
||||
class SimpleEventEmitter {
|
||||
constructor(logger = null) {
|
||||
this.logger = typeof logger === "function" ? logger : null;
|
||||
this.listeners = new Map();
|
||||
}
|
||||
|
||||
on(eventName, handler) {
|
||||
if (typeof handler !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, new Set());
|
||||
}
|
||||
const handlers = this.listeners.get(eventName);
|
||||
handlers.add(handler);
|
||||
return () => {
|
||||
handlers.delete(handler);
|
||||
if (!handlers.size) {
|
||||
this.listeners.delete(eventName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
emit(eventName, detail) {
|
||||
const handlers = this.listeners.get(eventName);
|
||||
if (!handlers || !handlers.size) {
|
||||
return;
|
||||
}
|
||||
for (const handler of Array.from(handlers)) {
|
||||
try {
|
||||
handler(detail);
|
||||
} catch (err) {
|
||||
if (this.logger) {
|
||||
this.logger("[PlaybackService] Listener error", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HOSTED_URL_SUCCESS_MESSAGE = "✅ Streaming from hosted URL";
|
||||
const DEFAULT_UNSUPPORTED_BTITH_MESSAGE =
|
||||
"This magnet link is missing a compatible BitTorrent v1 info hash.";
|
||||
@@ -288,7 +249,7 @@ export class PlaybackService {
|
||||
*/
|
||||
class PlaybackSession extends SimpleEventEmitter {
|
||||
constructor(service, options = {}) {
|
||||
super((message, ...args) => service.log(message, ...args));
|
||||
super((message, ...args) => service.log(message, ...args), "PlaybackService");
|
||||
this.service = service;
|
||||
this.options = options;
|
||||
this.watchdogCleanup = null;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* A lightweight event emitter used by services to communicate state changes
|
||||
* and other events to the UI and other parts of the application.
|
||||
*/
|
||||
export class SimpleEventEmitter {
|
||||
/**
|
||||
* @param {Function|null} logger - Optional logger function for error reporting.
|
||||
* @param {string} context - Optional context string for error messages.
|
||||
*/
|
||||
constructor(logger = null, context = "SimpleEventEmitter") {
|
||||
this.logger = typeof logger === "function" ? logger : null;
|
||||
this.context = typeof context === "string" ? context : "SimpleEventEmitter";
|
||||
this.listeners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a handler for a specific event.
|
||||
* @param {string} eventName
|
||||
* @param {Function} handler
|
||||
* @returns {Function} Unsubscribe function.
|
||||
*/
|
||||
on(eventName, handler) {
|
||||
if (typeof handler !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, new Set());
|
||||
}
|
||||
const handlers = this.listeners.get(eventName);
|
||||
handlers.add(handler);
|
||||
return () => {
|
||||
handlers.delete(handler);
|
||||
if (!handlers.size) {
|
||||
this.listeners.delete(eventName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event to all registered handlers.
|
||||
* @param {string} eventName
|
||||
* @param {any} detail
|
||||
*/
|
||||
emit(eventName, detail) {
|
||||
const handlers = this.listeners.get(eventName);
|
||||
if (!handlers || !handlers.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const handler of Array.from(handlers)) {
|
||||
try {
|
||||
handler(detail);
|
||||
} catch (error) {
|
||||
if (this.logger) {
|
||||
try {
|
||||
this.logger(`${this.context} listener for "${eventName}" threw`, error);
|
||||
} catch (logError) {
|
||||
// Fallback if the provided logger itself throws
|
||||
console.warn(`[${this.context}] listener logger threw`, logError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, test, mock } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { SimpleEventEmitter } from "../../js/utils/eventEmitter.js";
|
||||
|
||||
describe("SimpleEventEmitter", () => {
|
||||
test("should register and emit events", () => {
|
||||
const emitter = new SimpleEventEmitter();
|
||||
const handler = mock.fn();
|
||||
|
||||
emitter.on("test", handler);
|
||||
emitter.emit("test", { data: 123 });
|
||||
|
||||
assert.strictEqual(handler.mock.callCount(), 1);
|
||||
assert.deepStrictEqual(handler.mock.calls[0].arguments[0], { data: 123 });
|
||||
});
|
||||
|
||||
test("should support multiple handlers", () => {
|
||||
const emitter = new SimpleEventEmitter();
|
||||
const handler1 = mock.fn();
|
||||
const handler2 = mock.fn();
|
||||
|
||||
emitter.on("test", handler1);
|
||||
emitter.on("test", handler2);
|
||||
emitter.emit("test", "foo");
|
||||
|
||||
assert.strictEqual(handler1.mock.callCount(), 1);
|
||||
assert.strictEqual(handler2.mock.callCount(), 1);
|
||||
});
|
||||
|
||||
test("should unsubscribe correctly", () => {
|
||||
const emitter = new SimpleEventEmitter();
|
||||
const handler = mock.fn();
|
||||
|
||||
const unsub = emitter.on("test", handler);
|
||||
unsub();
|
||||
emitter.emit("test", "foo");
|
||||
|
||||
assert.strictEqual(handler.mock.callCount(), 0);
|
||||
});
|
||||
|
||||
test("should handle errors in handlers and use logger", () => {
|
||||
const logger = mock.fn();
|
||||
const emitter = new SimpleEventEmitter(logger, "TestContext");
|
||||
const errorHandler = () => { throw new Error("fail"); };
|
||||
|
||||
emitter.on("error-event", errorHandler);
|
||||
emitter.emit("error-event", "some-detail");
|
||||
|
||||
assert.strictEqual(logger.mock.callCount(), 1);
|
||||
assert.match(logger.mock.calls[0].arguments[0], /TestContext listener for "error-event" threw/);
|
||||
assert.strictEqual(logger.mock.calls[0].arguments[1].message, "fail");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user