From 00c04978925852a522b7b62ee50043beb436c92f Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Tue, 19 Aug 2025 13:24:15 -0700 Subject: [PATCH] Add anonymous session, re-work thunk utilities --- packages/app/__tests__/thunk.test.ts | 10 +- packages/app/src/session.ts | 6 + packages/app/src/thunk.ts | 193 +++++++++++++++------------ packages/lib/src/Tools.ts | 5 + 4 files changed, 123 insertions(+), 91 deletions(-) diff --git a/packages/app/__tests__/thunk.test.ts b/packages/app/__tests__/thunk.test.ts index 95d83ef..b6f5259 100644 --- a/packages/app/__tests__/thunk.test.ts +++ b/packages/app/__tests__/thunk.test.ts @@ -12,7 +12,7 @@ import { prepEvent, publishThunk, thunkQueue, - walkThunks, + flattenThunks, } from "../src/thunk" const secret = makeSecret() @@ -46,19 +46,19 @@ describe("thunk", () => { const thunk2 = new Thunk(mockRequest) const merged = new MergedThunk([thunk1, thunk2]) - merged.controller.abort() + abortThunk(merged) expect(thunk1.controller.signal.aborted).toBe(true) expect(thunk2.controller.signal.aborted).toBe(true) }) }) - describe("walkThunks", () => { + describe("flattenThunks", () => { it("should iterate through nested thunks", () => { const thunk1 = new Thunk(mockRequest) const thunk2 = new Thunk(mockRequest) const merged = new MergedThunk([thunk1, thunk2]) - const thunks = Array.from(walkThunks([merged, thunk1])) + const thunks = Array.from(flattenThunks([merged, thunk1])) expect(thunks).toHaveLength(3) }) @@ -78,7 +78,7 @@ describe("thunk", () => { const removeEventSpy = vi.spyOn(repository, "removeEvent") const thunk = publishThunk(mockRequest) - thunk.controller.abort() + abortThunk(thunk) expect(removeEventSpy).toHaveBeenCalledWith(thunk.event.id) }) diff --git a/packages/app/src/session.ts b/packages/app/src/session.ts index fa70427..aefd9ba 100644 --- a/packages/app/src/session.ts +++ b/packages/app/src/session.ts @@ -19,6 +19,7 @@ export enum SessionMethod { Nip46 = "nip46", Nip55 = "nip55", Pubkey = "pubkey", + Anonymous = "anonymous", } export type SessionNip01 = { @@ -53,12 +54,17 @@ export type SessionPubkey = { pubkey: string } +export type SessionAnonymous = { + method: SessionMethod.Anonymous +} + export type SessionAnyMethod = | SessionNip01 | SessionNip07 | SessionNip46 | SessionNip55 | SessionPubkey + | SessionAnonymous export type Session = SessionAnyMethod & {wallet?: Wallet} & Record diff --git a/packages/app/src/thunk.ts b/packages/app/src/thunk.ts index df49fed..68a06c7 100644 --- a/packages/app/src/thunk.ts +++ b/packages/app/src/thunk.ts @@ -3,13 +3,12 @@ import {writable, get} from "svelte/store" import { TaskQueue, ifLet, + ensurePlural, dissoc, remove, defer, sleep, assoc, - spec, - nthEq, nth, } from "@welshman/lib" import {stamp, own, hash} from "@welshman/signer" @@ -68,6 +67,13 @@ export class Thunk { for (const relay of options.relays) { this.status[relay] = PublishStatus.Sending } + + this.controller.signal.addEventListener("abort", () => { + console.log("abort") + for (const relay of options.relays) { + this._setAborted(relay) + } + }) } _notify() { @@ -85,6 +91,26 @@ export class Thunk { this._notify() } + _setPending(relay: string) { + this.options.onPending?.(relay) + this.status[relay] = PublishStatus.Pending + this._notify() + } + + _setTimeout(relay: string) { + this.options.onTimeout?.(relay) + this.status[relay] = PublishStatus.Timeout + this.details[relay] = "Publish timed out" + this._notify() + } + + _setAborted(relay: string) { + this.options.onAborted?.(relay) + this.status[relay] = PublishStatus.Aborted + this.details[relay] = "Publish was aborted" + this._notify() + } + async publish() { let event = this.event @@ -149,21 +175,13 @@ export class Thunk { this._notify() }, onPending: (relay: string) => { - this.options.onPending?.(relay) - this.status[relay] = PublishStatus.Pending - this._notify() + this._setPending(relay) }, onTimeout: (relay: string) => { - this.options.onTimeout?.(relay) - this.status[relay] = PublishStatus.Timeout - this.details[relay] = "Publish timed out" - this._notify() + this._setTimeout(relay) }, onAborted: (relay: string) => { - this.options.onAborted?.(relay) - this.status[relay] = PublishStatus.Aborted - this.details[relay] = "Publish was aborted" - this._notify() + this._setAborted(relay) }, onComplete: () => { this.options.onComplete?.() @@ -187,24 +205,21 @@ export class Thunk { export class MergedThunk { _subs: Subscriber[] = [] - controller = new AbortController() status: PublishStatusByRelay = {} details: Record = {} constructor(readonly thunks: Thunk[]) { - const {Aborted, Failure, Timeout, Pending, Success} = PublishStatus - const relays = new Set(thunks.flatMap(thunk => Object.keys(thunk.options.relays))) + const {Aborted, Failure, Timeout, Pending, Sending, Success} = PublishStatus + const relays = new Set(thunks.flatMap(thunk => thunk.options.relays)) for (const thunk of thunks) { - this.controller.signal.addEventListener("abort", () => thunk.controller.abort()) - thunk.subscribe($thunk => { this.status = {} this.details = {} for (const relay of relays) { - for (const status of [Aborted, Failure, Timeout, Pending, Success]) { - const thunk = thunks.find(spec({[relay]: status})) + for (const status of [Aborted, Failure, Timeout, Pending, Sending, Success]) { + const thunk = thunks.find(t => t.status[relay] === status) if (thunk) { this.status[relay] = thunk.status[relay]! @@ -213,9 +228,11 @@ export class MergedThunk { } } + console.log(this.status) + this._notify() - if (thunks.filter(thunkIsComplete).length === thunks.length) { + if (thunks.every(thunkIsComplete)) { this._subs = [] } }) @@ -246,79 +263,65 @@ export const isThunk = (thunk: AbstractThunk): thunk is Thunk => thunk instanceo export const isMergedThunk = (thunk: AbstractThunk): thunk is MergedThunk => thunk instanceof MergedThunk -export const thunkHasStatus = (thunk: AbstractThunk, status: PublishStatus) => - Object.entries(thunk.status).some(nthEq(1, status)) +// Thunk status urls -export const thunkUrlsWithStatus = (thunk: AbstractThunk, status: PublishStatus) => - Object.entries(thunk.status).filter(nthEq(1, status)).map(nth(0)) - -export const thunkCompleteUrls = (thunk: AbstractThunk) => { - const incompleteStatuses = [PublishStatus.Sending, PublishStatus.Pending] +export const getThunkUrlsWithStatus = ( + statuses: PublishStatus | PublishStatus[], + thunk: AbstractThunk, +) => { + statuses = ensurePlural(statuses) return Object.entries(thunk.status) - .filter(([_, s]) => !incompleteStatuses.includes(s)) - .map(nth(1)) + .filter(([_, status]) => statuses.includes(status)) + .map(nth(0)) } -export const thunkIncompleteUrls = (thunk: AbstractThunk) => { - const incompleteStatuses = [PublishStatus.Sending, PublishStatus.Pending] +export const getCompleteThunkUrls = (thunk: AbstractThunk) => + getThunkUrlsWithStatus([PublishStatus.Sending, PublishStatus.Pending], thunk) - return Object.entries(thunk.status) - .filter(([_, s]) => incompleteStatuses.includes(s)) - .map(nth(1)) -} +export const getIncompleteThunkUrls = (thunk: AbstractThunk) => + getThunkUrlsWithStatus([PublishStatus.Sending, PublishStatus.Pending], thunk) -export const thunkIsComplete = (thunk: AbstractThunk) => thunkCompleteUrls(thunk).length > 0 +export const getFailedThunkUrls = (thunk: AbstractThunk) => + getThunkUrlsWithStatus([PublishStatus.Failure, PublishStatus.Timeout], thunk) -export const getThunkError = (thunk: Thunk) => - new Promise(resolve => { - thunk.subscribe($thunk => { - for (const [relay, status] of Object.entries($thunk.status)) { - if (status === PublishStatus.Failure) { - resolve($thunk.details[relay]) - } - } +// Thunk status checks - if (thunkIsComplete($thunk)) { - resolve("") - } - }) - }) +export const thunkHasStatus = (statuses: PublishStatus | PublishStatus[], thunk: AbstractThunk) => + getThunkUrlsWithStatus(statuses, thunk).length > 0 -export const waitForThunkStatus = (thunk: Thunk, status: PublishStatus) => - new Promise(resolve => { - thunk.subscribe($thunk => { - for (const [_, s] of Object.entries($thunk.status)) { - if (s === status) { - resolve(true) - } - } +export const thunkIsComplete = (thunk: AbstractThunk) => + !thunkHasStatus([PublishStatus.Sending, PublishStatus.Pending], thunk) - if (thunkIsComplete($thunk)) { - resolve(false) - } - }) - }) +// Thunk errors -export const waitForThunkCompletion = (thunk: Thunk) => - new Promise(resolve => { - thunk.subscribe($thunk => { - if (thunkIsComplete($thunk)) { - resolve() - } - }) - }) - -export function* walkThunks(thunks: AbstractThunk[]): Iterable { - for (const thunk of thunks) { - if (thunk instanceof MergedThunk) { - yield* walkThunks(thunk.thunks) - } else { - yield thunk +export const getThunkError = (thunk: Thunk) => { + for (const [relay, status] of Object.entries(thunk.status)) { + if (status === PublishStatus.Failure) { + return thunk.details[relay] } } + + if (thunkIsComplete(thunk)) { + return "" + } } +// Thunk utilities that return promises + +export const waitForThunkError = (thunk: Thunk) => + new Promise(resolve => { + thunk.subscribe($thunk => { + const error = getThunkError($thunk) + + if (error !== undefined) { + resolve(error) + } + }) + }) + +// Thunk state + export const thunks = writable>({}) export const thunkQueue = new TaskQueue({ @@ -328,6 +331,21 @@ export const thunkQueue = new TaskQueue({ }, }) +// Other thunk utilities + +export const mergeThunks = (thunks: AbstractThunk[]) => + new MergedThunk(Array.from(flattenThunks(thunks))) + +export function* flattenThunks(thunks: AbstractThunk[]): Iterable { + for (const thunk of thunks) { + if (isMergedThunk(thunk)) { + yield* flattenThunks(thunk.thunks) + } else { + yield thunk + } + } +} + export const publishThunk = (options: ThunkOptions) => { const thunk = new Thunk(options) @@ -337,15 +355,18 @@ export const publishThunk = (options: ThunkOptions) => { thunks.update(assoc(thunk.event.id, thunk)) - thunk.controller.signal.addEventListener("abort", () => { - repository.removeEvent(thunk.event.id) - }) - return thunk } -export const abortThunk = (thunk: Thunk) => { - thunk.controller.abort() - thunks.update(dissoc(thunk.event.id)) - repository.removeEvent(thunk.event.id) +export const abortThunk = (thunk: AbstractThunk) => { + for (const child of flattenThunks([thunk])) { + child.controller.abort() + thunks.update(dissoc(child.event.id)) + repository.removeEvent(child.event.id) + } } + +export const retryThunk = (thunk: AbstractThunk) => + isMergedThunk(thunk) + ? mergeThunks(thunk.thunks.map(t => publishThunk(t.options))) + : publishThunk(thunk.options) diff --git a/packages/lib/src/Tools.ts b/packages/lib/src/Tools.ts index c2d2b9e..68044ae 100644 --- a/packages/lib/src/Tools.ts +++ b/packages/lib/src/Tools.ts @@ -1062,6 +1062,11 @@ export const poll = ({interval = 300, condition, signal}: PollOptions) => } }, interval) + if (condition()) { + resolve() + clearInterval(int) + } + signal.addEventListener("abort", () => { resolve() clearInterval(int)