lint fix wip

This commit is contained in:
Nicolas Cantu 2026-01-09 02:26:48 +01:00
parent bb5cfa758c
commit 9e3d2c8742
10 changed files with 219 additions and 76 deletions

View File

@ -72,13 +72,15 @@ export function Nip95ConfigManager({ onConfigChange }: Nip95ConfigManagerProps):
function handleDragStart(e: React.DragEvent<HTMLDivElement>, id: string): void { function handleDragStart(e: React.DragEvent<HTMLDivElement>, id: string): void {
setDraggedId(id) setDraggedId(id)
e.dataTransfer.effectAllowed = 'move' const { dataTransfer } = e
e.dataTransfer.setData('text/plain', id) dataTransfer.effectAllowed = 'move'
dataTransfer.setData('text/plain', id)
} }
function handleDragOver(e: React.DragEvent<HTMLDivElement>, id: string): void { function handleDragOver(e: React.DragEvent<HTMLDivElement>, id: string): void {
e.preventDefault() e.preventDefault()
e.dataTransfer.dropEffect = 'move' const { dataTransfer } = e
dataTransfer.dropEffect = 'move'
setDragOverId(id) setDragOverId(id)
} }

View File

@ -95,13 +95,15 @@ export function RelayManager({ onConfigChange }: RelayManagerProps): React.React
function handleDragStart(e: React.DragEvent<HTMLDivElement>, id: string): void { function handleDragStart(e: React.DragEvent<HTMLDivElement>, id: string): void {
setDraggedId(id) setDraggedId(id)
e.dataTransfer.effectAllowed = 'move' const { dataTransfer } = e
e.dataTransfer.setData('text/plain', id) dataTransfer.effectAllowed = 'move'
dataTransfer.setData('text/plain', id)
} }
function handleDragOver(e: React.DragEvent<HTMLDivElement>, id: string): void { function handleDragOver(e: React.DragEvent<HTMLDivElement>, id: string): void {
e.preventDefault() e.preventDefault()
e.dataTransfer.dropEffect = 'move' const { dataTransfer } = e
dataTransfer.dropEffect = 'move'
setDragOverId(id) setDragOverId(id)
} }

View File

@ -319,23 +319,30 @@ export async function fetchAuthorPresentationFromPool(
// Cache the result if found // Cache the result if found
if (value && events.length > 0) { 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) { if (event) {
const tags = extractTagsFromEvent(event) const tags = extractTagsFromEvent(event)
if (value.hash) { if (value.hash) {
// Calculate totalSponsoring from cache before storing // Calculate totalSponsoring from cache before storing
const { getAuthorSponsoring } = await import('./sponsoring') 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') const { writeObjectToCache } = await import('./helpers/writeObjectHelper')
await writeObjectToCache({ await writeObjectToCache({
objectType: 'author', objectType: 'author',
hash: value.hash, hash: value.hash,
event, event,
parsed: value, parsed: cachedValue,
version: tags.version, version: tags.version,
hidden: tags.hidden, hidden: tags.hidden,
index: value.index, index: value.index,
}) })
resolve(cachedValue)
return
} }
} }
} }

View File

@ -52,27 +52,38 @@ function setupContentDeliveryHandlers(
finalize: (result: ContentDeliveryStatus) => void, finalize: (result: ContentDeliveryStatus) => void,
isResolved: () => boolean isResolved: () => boolean
): void { ): void {
let currentStatus = status
sub.on('event', (event: Event) => { sub.on('event', (event: Event) => {
status.published = true currentStatus = {
status.verifiedOnRelay = true ...currentStatus,
status.messageEventId = event.id published: true,
status.retrievable = true verifiedOnRelay: true,
finalize(status) messageEventId: event.id,
retrievable: true,
}
finalize(currentStatus)
}) })
sub.on('eose', () => { sub.on('eose', () => {
if (!status.published) { if (!currentStatus.published) {
status.error = 'Message not found on relay' currentStatus = {
...currentStatus,
error: 'Message not found on relay',
}
} }
finalize(status) finalize(currentStatus)
}) })
setTimeout(() => { setTimeout(() => {
if (!isResolved()) { if (!isResolved()) {
if (!status.published) { if (!currentStatus.published) {
status.error = 'Timeout waiting for message verification' currentStatus = {
...currentStatus,
error: 'Timeout waiting for message verification',
}
} }
finalize(status) finalize(currentStatus)
} }
}, 5000) }, 5000)
} }

View File

@ -116,16 +116,17 @@ function handleCodeBlock(
state: RenderState, state: RenderState,
elements: React.ReactElement[] elements: React.ReactElement[]
): void { ): void {
const nextState = state
if (state.inCodeBlock) { if (state.inCodeBlock) {
elements.push( elements.push(
<pre key={`code-${index}`} className="bg-cyber-darker border border-neon-cyan/20 p-4 rounded-lg overflow-x-auto my-4 text-neon-cyan font-mono text-sm"> <pre key={`code-${index}`} className="bg-cyber-darker border border-neon-cyan/20 p-4 rounded-lg overflow-x-auto my-4 text-neon-cyan font-mono text-sm">
<code>{state.codeBlockContent.join('\n')}</code> <code>{state.codeBlockContent.join('\n')}</code>
</pre> </pre>
) )
state.codeBlockContent = [] nextState.codeBlockContent = []
state.inCodeBlock = false nextState.inCodeBlock = false
} else { } else {
state.inCodeBlock = true nextState.inCodeBlock = true
} }
} }
@ -135,6 +136,7 @@ function closeListIfNeeded(
state: RenderState, state: RenderState,
elements: React.ReactElement[] elements: React.ReactElement[]
): void { ): void {
const nextState = state
if (state.currentList.length > 0 && !line.startsWith('- ') && !line.startsWith('* ') && line.trim() !== '') { if (state.currentList.length > 0 && !line.startsWith('- ') && !line.startsWith('* ') && line.trim() !== '') {
elements.push( elements.push(
<ul key={`list-${index}`} className="list-disc list-inside mb-4 space-y-1 text-cyber-accent marker:text-neon-cyan"> <ul key={`list-${index}`} className="list-disc list-inside mb-4 space-y-1 text-cyber-accent marker:text-neon-cyan">
@ -143,7 +145,7 @@ function closeListIfNeeded(
))} ))}
</ul> </ul>
) )
state.currentList = [] nextState.currentList = []
} }
} }

View File

@ -9,6 +9,19 @@ const MAX_VIDEO_BYTES = 45 * 1024 * 1024
const IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'] const IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp']
const VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime'] 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<UnlockRequiredError>).unlockRequired === true
}
function assertBrowser(): void { function assertBrowser(): void {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
throw new Error('NIP-95 upload is only available in the browser') throw new Error('NIP-95 upload is only available in the browser')
@ -141,9 +154,7 @@ export async function uploadNip95Media(file: File): Promise<MediaRef> {
} else if (!isUnlocked) { } else if (!isUnlocked) {
// Throw a special error that can be caught to trigger unlock modal // Throw a special error that can be caught to trigger unlock modal
// This error should propagate to the caller, not be caught here // This error should propagate to the caller, not be caught here
const unlockError = new Error('UNLOCK_REQUIRED') throw createUnlockRequiredError()
;(unlockError as any).unlockRequired = true
throw unlockError
} else { } else {
console.warn('NIP-98 authentication required for nostrcheck.me but not available. Skipping endpoint.') console.warn('NIP-98 authentication required for nostrcheck.me but not available. Skipping endpoint.')
// Skip this endpoint // Skip this endpoint
@ -179,7 +190,7 @@ export async function uploadNip95Media(file: File): Promise<MediaRef> {
const errorMessage = error.message const errorMessage = error.message
// If unlock is required, propagate the error immediately // If unlock is required, propagate the error immediately
if (errorMessage === 'UNLOCK_REQUIRED' || (error as any).unlockRequired) { if (errorMessage === 'UNLOCK_REQUIRED' || isUnlockRequiredError(error)) {
throw error throw error
} }

View File

@ -1,15 +1,25 @@
import type { TagType, TagCategory } from './nostrTagSystemTypes' import type { TagType, TagCategory } from './nostrTagSystemTypes'
export function addSimpleTagFilter(filter: Record<string, string[] | number[]>, tagName: string, condition: boolean): void { export function addSimpleTagFilter(
filter: Record<string, string[] | number[]>,
tagName: string,
condition: boolean
): Record<string, string[] | number[]> {
if (condition) { if (condition) {
filter[`#${tagName}`] = [''] return { ...filter, [`#${tagName}`]: [''] }
} }
return filter
} }
export function addValueTagFilter(filter: Record<string, string[] | number[]>, tagName: string, value: string | undefined): void { export function addValueTagFilter(
filter: Record<string, string[] | number[]>,
tagName: string,
value: string | undefined
): Record<string, string[] | number[]> {
if (value) { if (value) {
filter[`#${tagName}`] = [value] return { ...filter, [`#${tagName}`]: [value] }
} }
return filter
} }
export function buildTagFilter(params: { export function buildTagFilter(params: {
@ -23,7 +33,7 @@ export function buildTagFilter(params: {
articleId?: string articleId?: string
authorPubkey?: string authorPubkey?: string
}): Record<string, string[] | number[]> { }): Record<string, string[] | number[]> {
const filter: Record<string, string[] | number[]> = { let filter: Record<string, string[] | number[]> = {
kinds: [1], // All are kind 1 notes kinds: [1], // All are kind 1 notes
} }
@ -33,12 +43,12 @@ export function buildTagFilter(params: {
if (params.category) { if (params.category) {
filter[`#${params.category}`] = [''] filter[`#${params.category}`] = ['']
} }
addValueTagFilter(filter, 'id', params.id) filter = addValueTagFilter(filter, 'id', params.id)
addValueTagFilter(filter, 'service', params.service) filter = addValueTagFilter(filter, 'service', params.service)
addSimpleTagFilter(filter, 'paywall', params.paywall === true) filter = addSimpleTagFilter(filter, 'paywall', params.paywall === true)
addSimpleTagFilter(filter, 'payment', params.payment === true) filter = addSimpleTagFilter(filter, 'payment', params.payment === true)
addValueTagFilter(filter, 'series', params.seriesId) filter = addValueTagFilter(filter, 'series', params.seriesId)
addValueTagFilter(filter, 'article', params.articleId) filter = addValueTagFilter(filter, 'article', params.articleId)
if (params.authorPubkey) { if (params.authorPubkey) {
filter.authors = [params.authorPubkey] filter.authors = [params.authorPubkey]

View File

@ -138,9 +138,9 @@ class PublishWorkerService {
} }
// Process all unpublished objects // Process all unpublished objects
const objectsToProcess = Array.from(this.unpublishedObjects.values()) const objectsToProcess = Array.from(this.unpublishedObjects.entries())
for (const obj of objectsToProcess) { for (const [key, obj] of objectsToProcess) {
await this.attemptPublish(obj) await this.attemptPublish({ key, obj })
} }
} catch (error) { } catch (error) {
console.error('[PublishWorker] Error processing unpublished objects:', error) console.error('[PublishWorker] Error processing unpublished objects:', error)
@ -153,7 +153,8 @@ class PublishWorkerService {
* Attempt to publish an unpublished object * Attempt to publish an unpublished object
* Uses websocketService to route events to Service Worker * Uses websocketService to route events to Service Worker
*/ */
private async attemptPublish(obj: UnpublishedObject): Promise<void> { private async attemptPublish(params: { key: string; obj: UnpublishedObject }): Promise<void> {
const {obj} = params
try { try {
const { websocketService } = await import('./websocketService') const { websocketService } = await import('./websocketService')
@ -207,26 +208,31 @@ class PublishWorkerService {
await writeService.updatePublished(obj.objectType, obj.id, successfulRelays) await writeService.updatePublished(obj.objectType, obj.id, successfulRelays)
console.warn(`[PublishWorker] Successfully published ${obj.objectType}:${obj.id} to ${successfulRelays.length} relay(s)`) console.warn(`[PublishWorker] Successfully published ${obj.objectType}:${obj.id} to ${successfulRelays.length} relay(s)`)
// Remove from unpublished map // Remove from unpublished map
this.unpublishedObjects.delete(`${obj.objectType}:${obj.id}`) this.unpublishedObjects.delete(params.key)
} else { } else {
// All relays failed, increment retry count const current = this.unpublishedObjects.get(params.key)
obj.retryCount++ const next = current
obj.lastRetryAt = Date.now() ? { ...current, retryCount: current.retryCount + 1, lastRetryAt: Date.now() }
console.warn(`[PublishWorker] All relays failed for ${obj.objectType}:${obj.id}, retry count: ${obj.retryCount}/${MAX_RETRIES_PER_OBJECT}`) : { ...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 // Remove if max retries reached
if (obj.retryCount >= MAX_RETRIES_PER_OBJECT) { if (next.retryCount >= MAX_RETRIES_PER_OBJECT) {
this.unpublishedObjects.delete(`${obj.objectType}:${obj.id}`) this.unpublishedObjects.delete(params.key)
} }
} }
} catch (error) { } catch (error) {
console.error(`[PublishWorker] Error publishing ${obj.objectType}:${obj.id}:`, error) console.error(`[PublishWorker] Error publishing ${obj.objectType}:${obj.id}:`, error)
// Increment retry count on error const current = this.unpublishedObjects.get(params.key)
obj.retryCount++ const next = current
obj.lastRetryAt = Date.now() ? { ...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) { if (next.retryCount >= MAX_RETRIES_PER_OBJECT) {
this.unpublishedObjects.delete(`${obj.objectType}:${obj.id}`) this.unpublishedObjects.delete(params.key)
} }
} }
} }

View File

@ -12,6 +12,17 @@ interface SWResponse {
data?: unknown data?: unknown
} }
function isRecord(value: unknown): value is Record<string, unknown> {
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 { class ServiceWorkerClient {
private registration: ServiceWorkerRegistration | null = null private registration: ServiceWorkerRegistration | null = null
private messageHandlers: Map<string, Array<(data: unknown) => void>> = new Map() private messageHandlers: Map<string, Array<(data: unknown) => void>> = new Map()
@ -36,7 +47,7 @@ class ServiceWorkerClient {
console.warn('[SWClient] Service Worker registered:', registration.scope) console.warn('[SWClient] Service Worker registered:', registration.scope)
// Listen for messages from Service Worker // Listen for messages from Service Worker
navigator.serviceWorker.addEventListener('message', (event) => { navigator.serviceWorker.addEventListener('message', (event: MessageEvent<unknown>) => {
this.handleMessage(event.data) this.handleMessage(event.data)
}) })
@ -110,8 +121,12 @@ class ServiceWorkerClient {
reject(new Error('Service Worker response timeout')) reject(new Error('Service Worker response timeout'))
}, timeout) }, timeout)
messageChannel.port1.onmessage = (event) => { messageChannel.port1.onmessage = (event: MessageEvent<unknown>) => {
clearTimeout(timeoutId) clearTimeout(timeoutId)
if (!isSWResponse(event.data)) {
reject(new Error('Invalid Service Worker response format'))
return
}
resolve(event.data) resolve(event.data)
} }
@ -124,7 +139,11 @@ class ServiceWorkerClient {
/** /**
* Handle messages from Service Worker * 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) const handlers = this.messageHandlers.get(message.type)
if (handlers) { if (handlers) {
handlers.forEach((handler) => { handlers.forEach((handler) => {

View File

@ -35,6 +35,43 @@ interface LogPublicationParams {
objectId?: string 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<string, unknown> {
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 { class WriteService {
private writeWorker: Worker | null = null private writeWorker: Worker | null = null
private initPromise: Promise<void> | null = null private initPromise: Promise<void> | null = null
@ -74,10 +111,13 @@ class WriteService {
// Worker dans public/ pour Next.js // Worker dans public/ pour Next.js
this.writeWorker = new Worker('/writeWorker.js', { type: 'classic' }) this.writeWorker = new Worker('/writeWorker.js', { type: 'classic' })
this.writeWorker.addEventListener('message', (event) => { this.writeWorker.addEventListener('message', (event: MessageEvent<unknown>) => {
const { type, data } = event.data if (!isWorkerMessageEnvelope(event.data)) {
if (type === 'ERROR') { console.error('[WriteService] Received invalid worker message envelope', { data: event.data })
console.error('[WriteService] Worker error:', data) return
}
if (event.data.type === 'ERROR') {
console.error('[WriteService] Worker error:', event.data.data)
} }
}) })
@ -100,8 +140,8 @@ class WriteService {
}, 2000) }, 2000)
// Le worker est prêt quand il répond // Le worker est prêt quand il répond
const readyHandler = (event: MessageEvent): void => { const readyHandler = (event: MessageEvent<unknown>): void => {
if (event.data?.type === 'WORKER_READY') { if (isWorkerMessageEnvelope(event.data) && event.data.type === 'WORKER_READY') {
clearTimeout(readyTimeout) clearTimeout(readyTimeout)
this.writeWorker?.removeEventListener('message', readyHandler) this.writeWorker?.removeEventListener('message', readyHandler)
resolve() resolve()
@ -133,15 +173,24 @@ class WriteService {
}, 10000) }, 10000)
const handler = (event: MessageEvent): void => { const handler = (event: MessageEvent): void => {
const { type, data } = event.data if (!isWorkerMessageEnvelope(event.data)) {
if (type === 'WRITE_OBJECT_SUCCESS' && data.hash === params.hash) { return
}
const responseType = event.data.type
const responseData = event.data.data
if (responseType === 'WRITE_OBJECT_SUCCESS' && isRecord(responseData) && responseData.hash === params.hash) {
clearTimeout(timeout) clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler) this.writeWorker?.removeEventListener('message', handler)
resolve() 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) clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler) 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) }, 10000)
const handler = (event: MessageEvent): void => { const handler = (event: MessageEvent): void => {
const { type, data } = event.data if (!isWorkerMessageEnvelope(event.data)) {
if (type === 'UPDATE_PUBLISHED_SUCCESS' && data.id === id) { return
}
const responseType = event.data.type
const responseData = event.data.data
if (responseType === 'UPDATE_PUBLISHED_SUCCESS' && isRecord(responseData) && responseData.id === id) {
clearTimeout(timeout) clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler) this.writeWorker?.removeEventListener('message', handler)
resolve() 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) clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler) 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) }, 10000)
const handler = (event: MessageEvent): void => { const handler = (event: MessageEvent): void => {
const { type: responseType, data: responseData } = event.data if (!isWorkerMessageEnvelope(event.data)) {
if (responseType === 'CREATE_NOTIFICATION_SUCCESS' && responseData.eventId === params.eventId) { return
}
const responseType = event.data.type
const responseData = event.data.data
if (responseType === 'CREATE_NOTIFICATION_SUCCESS' && isRecord(responseData) && responseData.eventId === params.eventId) {
clearTimeout(timeout) clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler) this.writeWorker?.removeEventListener('message', handler)
resolve() 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) clearTimeout(timeout)
this.writeWorker?.removeEventListener('message', handler) this.writeWorker?.removeEventListener('message', handler)
reject(new Error(responseData.error)) reject(new Error(errorData.error ?? 'Write worker error'))
} }
} }