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 {
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<HTMLDivElement>, id: string): void {
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
const { dataTransfer } = e
dataTransfer.dropEffect = 'move'
setDragOverId(id)
}

View File

@ -95,13 +95,15 @@ export function RelayManager({ onConfigChange }: RelayManagerProps): React.React
function handleDragStart(e: React.DragEvent<HTMLDivElement>, 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<HTMLDivElement>, id: string): void {
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
const { dataTransfer } = e
dataTransfer.dropEffect = 'move'
setDragOverId(id)
}

View File

@ -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
}
}
}

View File

@ -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)
}

View File

@ -116,16 +116,17 @@ function handleCodeBlock(
state: RenderState,
elements: React.ReactElement[]
): void {
const nextState = state
if (state.inCodeBlock) {
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">
<code>{state.codeBlockContent.join('\n')}</code>
</pre>
)
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(
<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>
)
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 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 {
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<MediaRef> {
} 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<MediaRef> {
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
}

View File

@ -1,15 +1,25 @@
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) {
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) {
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<string, string[] | number[]> {
const filter: Record<string, string[] | number[]> = {
let filter: Record<string, string[] | number[]> = {
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]

View File

@ -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<void> {
private async attemptPublish(params: { key: string; obj: UnpublishedObject }): Promise<void> {
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)
}
}
}

View File

@ -12,6 +12,17 @@ interface SWResponse {
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 {
private registration: ServiceWorkerRegistration | null = null
private messageHandlers: Map<string, Array<(data: unknown) => 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<unknown>) => {
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<unknown>) => {
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) => {

View File

@ -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<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 {
private writeWorker: Worker | null = null
private initPromise: Promise<void> | 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<unknown>) => {
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<unknown>): 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'))
}
}