2026-06-08 22:33:38 +00:00
|
|
|
import {identity} from "@welshman/lib"
|
2025-01-28 21:04:37 +00:00
|
|
|
import * as nip19 from "nostr-tools/nip19"
|
2025-06-06 23:38:11 +00:00
|
|
|
import {range, DAY, hexToBytes, bytesToHex} from "@welshman/lib"
|
2025-01-28 21:04:37 +00:00
|
|
|
|
|
|
|
|
export const nsecEncode = (secret: string) => nip19.nsecEncode(hexToBytes(secret))
|
|
|
|
|
|
|
|
|
|
export const nsecDecode = (nsec: string) => {
|
|
|
|
|
const {type, data} = nip19.decode(nsec)
|
|
|
|
|
|
|
|
|
|
if (type !== "nsec") throw new Error(`Invalid nsec: ${nsec}`)
|
|
|
|
|
|
|
|
|
|
return bytesToHex(data)
|
|
|
|
|
}
|
2025-02-05 18:47:56 +00:00
|
|
|
|
2025-02-06 00:26:22 +00:00
|
|
|
export const day = (seconds: number) => Math.floor(seconds / DAY)
|
2025-02-05 18:47:56 +00:00
|
|
|
|
2025-02-05 23:15:50 +00:00
|
|
|
export const daysBetween = (start: number, end: number) => [...range(start, end, DAY)].map(day)
|
2025-05-07 22:04:35 +00:00
|
|
|
|
|
|
|
|
export const ucFirst = (s: string) => s.slice(0, 1).toUpperCase() + s.slice(1)
|
2026-01-23 23:35:54 +00:00
|
|
|
|
2026-02-05 20:51:59 +00:00
|
|
|
export const errorMessage = (err: unknown) => String(err).replace(/^.*Error: /, "")
|
|
|
|
|
|
2026-03-16 20:38:05 +00:00
|
|
|
export class AbortError extends Error {
|
|
|
|
|
constructor() {
|
|
|
|
|
super("Aborted")
|
|
|
|
|
this.name = "AbortError"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class TimeoutError extends Error {
|
|
|
|
|
constructor(message = "Timed out") {
|
|
|
|
|
super(message)
|
|
|
|
|
this.name = "TimeoutError"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Returns a promise that rejects with AbortError when signal aborts. Use with Promise.race. */
|
|
|
|
|
export const whenAborted = (signal?: AbortSignal) => {
|
|
|
|
|
if (!signal) return new Promise<never>(() => {})
|
|
|
|
|
|
|
|
|
|
return new Promise<never>((_, reject) => {
|
|
|
|
|
const onAborted = () => reject(new AbortError())
|
|
|
|
|
if (signal.aborted) onAborted()
|
|
|
|
|
else signal.addEventListener("abort", onAborted, {once: true})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-28 19:17:17 +00:00
|
|
|
/**
|
|
|
|
|
* Returns a promise that rejects with TimeoutError after ms. Use with Promise.race.
|
|
|
|
|
* Pass an optional signal to clear the timer when that signal aborts (self-cleaning).
|
|
|
|
|
*/
|
|
|
|
|
export const whenTimeout = (ms: number, opts: {message?: string; signal?: AbortSignal} = {}) => {
|
|
|
|
|
return new Promise<never>((_, reject) => {
|
|
|
|
|
const timeout = setTimeout(() => reject(new TimeoutError(opts.message)), ms)
|
|
|
|
|
opts.signal?.addEventListener("abort", () => clearTimeout(timeout), {once: true})
|
|
|
|
|
})
|
2026-03-16 20:38:05 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-23 23:35:54 +00:00
|
|
|
export const buildUrl = (base: string | URL, ...pathname: string[]) => {
|
|
|
|
|
const url = new URL(base)
|
|
|
|
|
|
2026-01-26 18:08:43 +00:00
|
|
|
url.pathname = "/" + pathname.join("/")
|
2026-01-23 23:35:54 +00:00
|
|
|
|
|
|
|
|
return url.toString()
|
|
|
|
|
}
|
2026-02-16 16:16:03 +00:00
|
|
|
|
|
|
|
|
export const addPeriod = (s: string) => (s + ".").replace(/\.+$/, ".")
|
2026-02-25 00:09:50 +00:00
|
|
|
|
|
|
|
|
export const normalizeTopic = (topic: string) => topic.trim().replace(/^#+/, "").toLowerCase()
|
2026-06-08 22:33:38 +00:00
|
|
|
|
|
|
|
|
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
|
|
|
|
|
|
|
|
|
|
export const stripPrefix = (m: string) => m.replace(/^\w+: /, "")
|