From 9e3d2c87428ed2994edb4351b216f6821a2b6c37 Mon Sep 17 00:00:00 2001 From: Nicolas Cantu Date: Fri, 9 Jan 2026 02:26:48 +0100 Subject: [PATCH] lint fix wip --- components/Nip95ConfigManager.tsx | 8 +- components/RelayManager.tsx | 8 +- lib/articlePublisherHelpersPresentation.ts | 13 ++- lib/contentDeliveryVerification.ts | 33 ++++--- lib/markdownRenderer.tsx | 10 +- lib/nip95.ts | 19 +++- lib/nostrTagSystemFilter.ts | 32 +++--- lib/publishWorker.ts | 38 ++++--- lib/swClient.ts | 25 ++++- lib/writeService.ts | 109 +++++++++++++++++---- 10 files changed, 219 insertions(+), 76 deletions(-) diff --git a/components/Nip95ConfigManager.tsx b/components/Nip95ConfigManager.tsx index 0729541..500bdd0 100644 --- a/components/Nip95ConfigManager.tsx +++ b/components/Nip95ConfigManager.tsx @@ -72,13 +72,15 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps): function handleDragStart(e: React.DragEvent, id: string): void { setDraggedId(id) - e.dataTransfer.effectAllowed = 'move' - e.dataTransfer.setData('text/plain', id) + const { dataTransfer } = e + dataTransfer.effectAllowed = 'move' + dataTransfer.setData('text/plain', id) } function handleDragOver(e: React.DragEvent, id: string): void { e.preventDefault() - e.dataTransfer.dropEffect = 'move' + const { dataTransfer } = e + dataTransfer.dropEffect = 'move' setDragOverId(id) } diff --git a/components/RelayManager.tsx b/components/RelayManager.tsx index c6d3c35..dcb9413 100644 --- a/components/RelayManager.tsx +++ b/components/RelayManager.tsx @@ -95,13 +95,15 @@ export function RelayManager({ onConfigChange }: RelayManagerProps): React.React function handleDragStart(e: React.DragEvent, id: string): void { setDraggedId(id) - e.dataTransfer.effectAllowed = 'move' - e.dataTransfer.setData('text/plain', id) + const { dataTransfer } = e + dataTransfer.effectAllowed = 'move' + dataTransfer.setData('text/plain', id) } function handleDragOver(e: React.DragEvent, id: string): void { e.preventDefault() - e.dataTransfer.dropEffect = 'move' + const { dataTransfer } = e + dataTransfer.dropEffect = 'move' setDragOverId(id) } diff --git a/lib/articlePublisherHelpersPresentation.ts b/lib/articlePublisherHelpersPresentation.ts index 92f60da..41b8658 100644 --- a/lib/articlePublisherHelpersPresentation.ts +++ b/lib/articlePublisherHelpersPresentation.ts @@ -319,23 +319,30 @@ export async function fetchAuthorPresentationFromPool( // Cache the result if found if (value && events.length > 0) { - const event = events.find(e => e.id === value.id) || events[0] + const event = events.find((e) => e.id === value.id) ?? events[0] if (event) { const tags = extractTagsFromEvent(event) if (value.hash) { // Calculate totalSponsoring from cache before storing const { getAuthorSponsoring } = await import('./sponsoring') - value.totalSponsoring = await getAuthorSponsoring(value.pubkey) + const totalSponsoring = await getAuthorSponsoring(value.pubkey) + const cachedValue: import('@/types/nostr').AuthorPresentationArticle = { + ...value, + totalSponsoring, + } const { writeObjectToCache } = await import('./helpers/writeObjectHelper') await writeObjectToCache({ objectType: 'author', hash: value.hash, event, - parsed: value, + parsed: cachedValue, version: tags.version, hidden: tags.hidden, index: value.index, }) + + resolve(cachedValue) + return } } } diff --git a/lib/contentDeliveryVerification.ts b/lib/contentDeliveryVerification.ts index 54e0c38..fb3d99f 100644 --- a/lib/contentDeliveryVerification.ts +++ b/lib/contentDeliveryVerification.ts @@ -52,27 +52,38 @@ function setupContentDeliveryHandlers( finalize: (result: ContentDeliveryStatus) => void, isResolved: () => boolean ): void { + let currentStatus = status + sub.on('event', (event: Event) => { - status.published = true - status.verifiedOnRelay = true - status.messageEventId = event.id - status.retrievable = true - finalize(status) + currentStatus = { + ...currentStatus, + published: true, + verifiedOnRelay: true, + messageEventId: event.id, + retrievable: true, + } + finalize(currentStatus) }) sub.on('eose', () => { - if (!status.published) { - status.error = 'Message not found on relay' + if (!currentStatus.published) { + currentStatus = { + ...currentStatus, + error: 'Message not found on relay', + } } - finalize(status) + finalize(currentStatus) }) setTimeout(() => { if (!isResolved()) { - if (!status.published) { - status.error = 'Timeout waiting for message verification' + if (!currentStatus.published) { + currentStatus = { + ...currentStatus, + error: 'Timeout waiting for message verification', + } } - finalize(status) + finalize(currentStatus) } }, 5000) } diff --git a/lib/markdownRenderer.tsx b/lib/markdownRenderer.tsx index 8e72093..693f9b8 100644 --- a/lib/markdownRenderer.tsx +++ b/lib/markdownRenderer.tsx @@ -116,16 +116,17 @@ function handleCodeBlock( state: RenderState, elements: React.ReactElement[] ): void { + const nextState = state if (state.inCodeBlock) { elements.push(
         {state.codeBlockContent.join('\n')}
       
) - state.codeBlockContent = [] - state.inCodeBlock = false + nextState.codeBlockContent = [] + nextState.inCodeBlock = false } else { - state.inCodeBlock = true + nextState.inCodeBlock = true } } @@ -135,6 +136,7 @@ function closeListIfNeeded( state: RenderState, elements: React.ReactElement[] ): void { + const nextState = state if (state.currentList.length > 0 && !line.startsWith('- ') && !line.startsWith('* ') && line.trim() !== '') { elements.push(
    @@ -143,7 +145,7 @@ function closeListIfNeeded( ))}
) - state.currentList = [] + nextState.currentList = [] } } diff --git a/lib/nip95.ts b/lib/nip95.ts index 34627a8..cb1bb72 100644 --- a/lib/nip95.ts +++ b/lib/nip95.ts @@ -9,6 +9,19 @@ const MAX_VIDEO_BYTES = 45 * 1024 * 1024 const IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'] const VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime'] +interface UnlockRequiredError extends Error { + unlockRequired: true +} + +function createUnlockRequiredError(): UnlockRequiredError { + const error = Object.assign(new Error('UNLOCK_REQUIRED'), { unlockRequired: true as const }) + return error +} + +function isUnlockRequiredError(error: Error): error is UnlockRequiredError { + return (error as Partial).unlockRequired === true +} + function assertBrowser(): void { if (typeof window === 'undefined') { throw new Error('NIP-95 upload is only available in the browser') @@ -141,9 +154,7 @@ export async function uploadNip95Media(file: File): Promise { } else if (!isUnlocked) { // Throw a special error that can be caught to trigger unlock modal // This error should propagate to the caller, not be caught here - const unlockError = new Error('UNLOCK_REQUIRED') - ;(unlockError as any).unlockRequired = true - throw unlockError + throw createUnlockRequiredError() } else { console.warn('NIP-98 authentication required for nostrcheck.me but not available. Skipping endpoint.') // Skip this endpoint @@ -179,7 +190,7 @@ export async function uploadNip95Media(file: File): Promise { const errorMessage = error.message // If unlock is required, propagate the error immediately - if (errorMessage === 'UNLOCK_REQUIRED' || (error as any).unlockRequired) { + if (errorMessage === 'UNLOCK_REQUIRED' || isUnlockRequiredError(error)) { throw error } diff --git a/lib/nostrTagSystemFilter.ts b/lib/nostrTagSystemFilter.ts index 8c8f336..3d3b9dd 100644 --- a/lib/nostrTagSystemFilter.ts +++ b/lib/nostrTagSystemFilter.ts @@ -1,15 +1,25 @@ import type { TagType, TagCategory } from './nostrTagSystemTypes' -export function addSimpleTagFilter(filter: Record, tagName: string, condition: boolean): void { +export function addSimpleTagFilter( + filter: Record, + tagName: string, + condition: boolean +): Record { if (condition) { - filter[`#${tagName}`] = [''] + return { ...filter, [`#${tagName}`]: [''] } } + return filter } -export function addValueTagFilter(filter: Record, tagName: string, value: string | undefined): void { +export function addValueTagFilter( + filter: Record, + tagName: string, + value: string | undefined +): Record { if (value) { - filter[`#${tagName}`] = [value] + return { ...filter, [`#${tagName}`]: [value] } } + return filter } export function buildTagFilter(params: { @@ -23,7 +33,7 @@ export function buildTagFilter(params: { articleId?: string authorPubkey?: string }): Record { - const filter: Record = { + let filter: Record = { kinds: [1], // All are kind 1 notes } @@ -33,12 +43,12 @@ export function buildTagFilter(params: { if (params.category) { filter[`#${params.category}`] = [''] } - addValueTagFilter(filter, 'id', params.id) - addValueTagFilter(filter, 'service', params.service) - addSimpleTagFilter(filter, 'paywall', params.paywall === true) - addSimpleTagFilter(filter, 'payment', params.payment === true) - addValueTagFilter(filter, 'series', params.seriesId) - addValueTagFilter(filter, 'article', params.articleId) + filter = addValueTagFilter(filter, 'id', params.id) + filter = addValueTagFilter(filter, 'service', params.service) + filter = addSimpleTagFilter(filter, 'paywall', params.paywall === true) + filter = addSimpleTagFilter(filter, 'payment', params.payment === true) + filter = addValueTagFilter(filter, 'series', params.seriesId) + filter = addValueTagFilter(filter, 'article', params.articleId) if (params.authorPubkey) { filter.authors = [params.authorPubkey] diff --git a/lib/publishWorker.ts b/lib/publishWorker.ts index f390256..1f96686 100644 --- a/lib/publishWorker.ts +++ b/lib/publishWorker.ts @@ -138,9 +138,9 @@ class PublishWorkerService { } // Process all unpublished objects - const objectsToProcess = Array.from(this.unpublishedObjects.values()) - for (const obj of objectsToProcess) { - await this.attemptPublish(obj) + const objectsToProcess = Array.from(this.unpublishedObjects.entries()) + for (const [key, obj] of objectsToProcess) { + await this.attemptPublish({ key, obj }) } } catch (error) { console.error('[PublishWorker] Error processing unpublished objects:', error) @@ -153,7 +153,8 @@ class PublishWorkerService { * Attempt to publish an unpublished object * Uses websocketService to route events to Service Worker */ - private async attemptPublish(obj: UnpublishedObject): Promise { + private async attemptPublish(params: { key: string; obj: UnpublishedObject }): Promise { + const {obj} = params try { const { websocketService } = await import('./websocketService') @@ -207,26 +208,31 @@ class PublishWorkerService { await writeService.updatePublished(obj.objectType, obj.id, successfulRelays) console.warn(`[PublishWorker] Successfully published ${obj.objectType}:${obj.id} to ${successfulRelays.length} relay(s)`) // Remove from unpublished map - this.unpublishedObjects.delete(`${obj.objectType}:${obj.id}`) + this.unpublishedObjects.delete(params.key) } else { - // All relays failed, increment retry count - obj.retryCount++ - obj.lastRetryAt = Date.now() - console.warn(`[PublishWorker] All relays failed for ${obj.objectType}:${obj.id}, retry count: ${obj.retryCount}/${MAX_RETRIES_PER_OBJECT}`) + const current = this.unpublishedObjects.get(params.key) + const next = current + ? { ...current, retryCount: current.retryCount + 1, lastRetryAt: Date.now() } + : { ...obj, retryCount: obj.retryCount + 1, lastRetryAt: Date.now() } + this.unpublishedObjects.set(params.key, next) + + console.warn(`[PublishWorker] All relays failed for ${obj.objectType}:${obj.id}, retry count: ${next.retryCount}/${MAX_RETRIES_PER_OBJECT}`) // Remove if max retries reached - if (obj.retryCount >= MAX_RETRIES_PER_OBJECT) { - this.unpublishedObjects.delete(`${obj.objectType}:${obj.id}`) + if (next.retryCount >= MAX_RETRIES_PER_OBJECT) { + this.unpublishedObjects.delete(params.key) } } } catch (error) { console.error(`[PublishWorker] Error publishing ${obj.objectType}:${obj.id}:`, error) - // Increment retry count on error - obj.retryCount++ - obj.lastRetryAt = Date.now() + const current = this.unpublishedObjects.get(params.key) + const next = current + ? { ...current, retryCount: current.retryCount + 1, lastRetryAt: Date.now() } + : { ...params.obj, retryCount: params.obj.retryCount + 1, lastRetryAt: Date.now() } + this.unpublishedObjects.set(params.key, next) - if (obj.retryCount >= MAX_RETRIES_PER_OBJECT) { - this.unpublishedObjects.delete(`${obj.objectType}:${obj.id}`) + if (next.retryCount >= MAX_RETRIES_PER_OBJECT) { + this.unpublishedObjects.delete(params.key) } } } diff --git a/lib/swClient.ts b/lib/swClient.ts index cc09472..88041ba 100644 --- a/lib/swClient.ts +++ b/lib/swClient.ts @@ -12,6 +12,17 @@ interface SWResponse { data?: unknown } +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +function isSWResponse(value: unknown): value is SWResponse { + if (!isRecord(value)) { + return false + } + return typeof value.type === 'string' +} + class ServiceWorkerClient { private registration: ServiceWorkerRegistration | null = null private messageHandlers: Map void>> = new Map() @@ -36,7 +47,7 @@ class ServiceWorkerClient { console.warn('[SWClient] Service Worker registered:', registration.scope) // Listen for messages from Service Worker - navigator.serviceWorker.addEventListener('message', (event) => { + navigator.serviceWorker.addEventListener('message', (event: MessageEvent) => { this.handleMessage(event.data) }) @@ -110,8 +121,12 @@ class ServiceWorkerClient { reject(new Error('Service Worker response timeout')) }, timeout) - messageChannel.port1.onmessage = (event) => { + messageChannel.port1.onmessage = (event: MessageEvent) => { clearTimeout(timeoutId) + if (!isSWResponse(event.data)) { + reject(new Error('Invalid Service Worker response format')) + return + } resolve(event.data) } @@ -124,7 +139,11 @@ class ServiceWorkerClient { /** * Handle messages from Service Worker */ - private handleMessage(message: SWResponse): void { + private handleMessage(message: unknown): void { + if (!isSWResponse(message)) { + console.warn('[SWClient] Ignoring invalid message from Service Worker', { message }) + return + } const handlers = this.messageHandlers.get(message.type) if (handlers) { handlers.forEach((handler) => { diff --git a/lib/writeService.ts b/lib/writeService.ts index 4978130..b83f9d2 100644 --- a/lib/writeService.ts +++ b/lib/writeService.ts @@ -35,6 +35,43 @@ interface LogPublicationParams { objectId?: string } +interface WorkerMessageEnvelope { + type: string + data?: unknown +} + +interface WorkerErrorData { + originalType: string | undefined + taskId: string | undefined + error: string | undefined +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +function isWorkerMessageEnvelope(value: unknown): value is WorkerMessageEnvelope { + if (!isRecord(value)) { + return false + } + return typeof value.type === 'string' +} + +function readString(value: unknown): string | undefined { + return typeof value === 'string' ? value : undefined +} + +function readWorkerErrorData(value: unknown): WorkerErrorData { + if (!isRecord(value)) { + return { originalType: undefined, taskId: undefined, error: undefined } + } + return { + originalType: readString(value.originalType), + taskId: readString(value.taskId), + error: readString(value.error), + } +} + class WriteService { private writeWorker: Worker | null = null private initPromise: Promise | null = null @@ -74,10 +111,13 @@ class WriteService { // Worker dans public/ pour Next.js this.writeWorker = new Worker('/writeWorker.js', { type: 'classic' }) - this.writeWorker.addEventListener('message', (event) => { - const { type, data } = event.data - if (type === 'ERROR') { - console.error('[WriteService] Worker error:', data) + this.writeWorker.addEventListener('message', (event: MessageEvent) => { + if (!isWorkerMessageEnvelope(event.data)) { + console.error('[WriteService] Received invalid worker message envelope', { data: event.data }) + return + } + if (event.data.type === 'ERROR') { + console.error('[WriteService] Worker error:', event.data.data) } }) @@ -100,8 +140,8 @@ class WriteService { }, 2000) // Le worker est prêt quand il répond - const readyHandler = (event: MessageEvent): void => { - if (event.data?.type === 'WORKER_READY') { + const readyHandler = (event: MessageEvent): void => { + if (isWorkerMessageEnvelope(event.data) && event.data.type === 'WORKER_READY') { clearTimeout(readyTimeout) this.writeWorker?.removeEventListener('message', readyHandler) resolve() @@ -133,15 +173,24 @@ class WriteService { }, 10000) const handler = (event: MessageEvent): void => { - const { type, data } = event.data - if (type === 'WRITE_OBJECT_SUCCESS' && data.hash === params.hash) { + if (!isWorkerMessageEnvelope(event.data)) { + return + } + const responseType = event.data.type + const responseData = event.data.data + + if (responseType === 'WRITE_OBJECT_SUCCESS' && isRecord(responseData) && responseData.hash === params.hash) { clearTimeout(timeout) this.writeWorker?.removeEventListener('message', handler) resolve() - } else if (type === 'ERROR' && data.originalType === 'WRITE_OBJECT') { + } else if (responseType === 'ERROR') { + const errorData = readWorkerErrorData(responseData) + if (errorData.originalType !== 'WRITE_OBJECT') { + return + } clearTimeout(timeout) this.writeWorker?.removeEventListener('message', handler) - reject(new Error(data.error)) + reject(new Error(errorData.error ?? 'Write worker error')) } } @@ -201,15 +250,27 @@ class WriteService { }, 10000) const handler = (event: MessageEvent): void => { - const { type, data } = event.data - if (type === 'UPDATE_PUBLISHED_SUCCESS' && data.id === id) { + if (!isWorkerMessageEnvelope(event.data)) { + return + } + const responseType = event.data.type + const responseData = event.data.data + + if (responseType === 'UPDATE_PUBLISHED_SUCCESS' && isRecord(responseData) && responseData.id === id) { clearTimeout(timeout) this.writeWorker?.removeEventListener('message', handler) resolve() - } else if (type === 'ERROR' && (data.originalType === 'UPDATE_PUBLISHED' || data.taskId?.startsWith('UPDATE_PUBLISHED'))) { + } else if (responseType === 'ERROR') { + const errorData = readWorkerErrorData(responseData) + const {taskId} = errorData + const isUpdatePublished = + errorData.originalType === 'UPDATE_PUBLISHED' || (taskId !== undefined && taskId.startsWith('UPDATE_PUBLISHED')) + if (!isUpdatePublished) { + return + } clearTimeout(timeout) this.writeWorker?.removeEventListener('message', handler) - reject(new Error(data.error)) + reject(new Error(errorData.error ?? 'Write worker error')) } } @@ -247,15 +308,27 @@ class WriteService { }, 10000) const handler = (event: MessageEvent): void => { - const { type: responseType, data: responseData } = event.data - if (responseType === 'CREATE_NOTIFICATION_SUCCESS' && responseData.eventId === params.eventId) { + if (!isWorkerMessageEnvelope(event.data)) { + return + } + const responseType = event.data.type + const responseData = event.data.data + + if (responseType === 'CREATE_NOTIFICATION_SUCCESS' && isRecord(responseData) && responseData.eventId === params.eventId) { clearTimeout(timeout) this.writeWorker?.removeEventListener('message', handler) resolve() - } else if (responseType === 'ERROR' && (responseData.originalType === 'CREATE_NOTIFICATION' || responseData.taskId?.startsWith('CREATE_NOTIFICATION'))) { + } else if (responseType === 'ERROR') { + const errorData = readWorkerErrorData(responseData) + const {taskId} = errorData + const isCreateNotification = + errorData.originalType === 'CREATE_NOTIFICATION' || (taskId !== undefined && taskId.startsWith('CREATE_NOTIFICATION')) + if (!isCreateNotification) { + return + } clearTimeout(timeout) this.writeWorker?.removeEventListener('message', handler) - reject(new Error(responseData.error)) + reject(new Error(errorData.error ?? 'Write worker error')) } }