You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

148 lines
4.6 KiB

import { isLikelyCachedNostrEvent } from '@/services/indexed-db.service'
import type { Event } from 'nostr-tools'
import { validateEvent, verifyEvent } from 'nostr-tools'
/** Max size for a pasted JSON blob (single event or small array). */
export const CACHE_IMPORT_MAX_PASTE_CHARS = 400_000
/** Max `.jsonl` upload size. */
export const CACHE_IMPORT_MAX_JSONL_FILE_BYTES = 2 * 1024 * 1024
/** Max lines processed from a `.jsonl` file. */
export const CACHE_IMPORT_MAX_JSONL_LINES = 500
/** Skip oversized single lines in JSONL. */
export const CACHE_IMPORT_MAX_JSONL_LINE_CHARS = 256_000
export type CacheImportParseIssue = { line?: number; message: string }
export type CacheImportParseResult = {
events: Event[]
issues: CacheImportParseIssue[]
}
function normalizeImportedEvent(raw: unknown): Event | null {
if (!isLikelyCachedNostrEvent(raw)) return null
const ev = raw as Event
if (typeof ev.created_at !== 'number' || !Number.isFinite(ev.created_at)) return null
if (typeof ev.sig !== 'string' || !ev.sig.trim()) return null
if (!validateEvent(ev)) return null
if (!verifyEvent(ev)) return null
return ev
}
function collectFromUnknown(
value: unknown,
line: number | undefined,
events: Event[],
issues: CacheImportParseIssue[]
): void {
if (Array.isArray(value)) {
if (value.length + events.length > CACHE_IMPORT_MAX_JSONL_LINES) {
issues.push({
line,
message: `Too many events (max ${CACHE_IMPORT_MAX_JSONL_LINES})`
})
return
}
for (let i = 0; i < value.length; i++) {
collectFromUnknown(value[i], line, events, issues)
if (events.length >= CACHE_IMPORT_MAX_JSONL_LINES) break
}
return
}
const ev = normalizeImportedEvent(value)
if (ev) {
events.push(ev)
return
}
issues.push({
line,
message: line != null ? 'Invalid or unsigned event on this line' : 'Invalid or unsigned Nostr event JSON'
})
}
/** Parse one pasted JSON value (object or array of events). */
export function parsePastedCacheImportJson(text: string): CacheImportParseResult {
const trimmed = text.trim()
if (!trimmed) {
return { events: [], issues: [{ message: 'Paste is empty' }] }
}
if (trimmed.length > CACHE_IMPORT_MAX_PASTE_CHARS) {
return {
events: [],
issues: [
{
message: `Paste exceeds ${Math.round(CACHE_IMPORT_MAX_PASTE_CHARS / 1024)} KB limit`
}
]
}
}
let parsed: unknown
try {
parsed = JSON.parse(trimmed) as unknown
} catch {
return { events: [], issues: [{ message: 'Invalid JSON' }] }
}
const events: Event[] = []
const issues: CacheImportParseIssue[] = []
collectFromUnknown(parsed, undefined, events, issues)
return { events, issues }
}
/** Parse newline-delimited JSON (one event per line). */
export function parseJsonlCacheImportText(text: string): CacheImportParseResult {
if (text.length > CACHE_IMPORT_MAX_JSONL_FILE_BYTES) {
return {
events: [],
issues: [
{
message: `File content exceeds ${Math.round(CACHE_IMPORT_MAX_JSONL_FILE_BYTES / (1024 * 1024))} MB limit`
}
]
}
}
const lines = text.split(/\r?\n/)
const events: Event[] = []
const issues: CacheImportParseIssue[] = []
let nonEmptyLines = 0
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
if (!line) continue
nonEmptyLines++
if (nonEmptyLines > CACHE_IMPORT_MAX_JSONL_LINES) {
issues.push({
line: i + 1,
message: `Stopped at ${CACHE_IMPORT_MAX_JSONL_LINES} events (file has more lines)`
})
break
}
if (line.length > CACHE_IMPORT_MAX_JSONL_LINE_CHARS) {
issues.push({ line: i + 1, message: 'Line too large' })
continue
}
let parsed: unknown
try {
parsed = JSON.parse(line) as unknown
} catch {
issues.push({ line: i + 1, message: 'Invalid JSON on this line' })
continue
}
const before = events.length
collectFromUnknown(parsed, i + 1, events, issues)
if (events.length === before && issues[issues.length - 1]?.line !== i + 1) {
issues.push({ line: i + 1, message: 'Invalid or unsigned event on this line' })
}
if (events.length >= CACHE_IMPORT_MAX_JSONL_LINES) break
}
if (nonEmptyLines === 0) {
issues.push({ message: 'File has no event lines' })
}
return { events, issues }
}
export function formatCacheImportLimits(): string {
const mb = Math.round(CACHE_IMPORT_MAX_JSONL_FILE_BYTES / (1024 * 1024))
const pasteKb = Math.round(CACHE_IMPORT_MAX_PASTE_CHARS / 1024)
return `${CACHE_IMPORT_MAX_JSONL_LINES} events max · ${mb} MB file · ${pasteKb} KB paste`
}