commit 6fb816c4b3c21e8f99eb425e8be47b69099a7cb8 parent 42025a7bf4405ff0797eb8f17219f2b715c0f6da Author: triesap <triesap@radroots.dev> Date: Mon, 22 Dec 2025 21:49:23 +0000 workspace: streamline market lib and update deps - Remove apps-lib-market components/types/utils exports and add tmp placeholder - Simplify market kit/vite alias config and refresh .gitignore rules - Bump apps-lib-market version and add rimraf dev dependency - Adjust client keystore typing/error exports, move farm schema imports, add themes deps Diffstat:
33 files changed, 46 insertions(+), 2119 deletions(-)
diff --git a/apps-lib-market/.gitignore b/apps-lib-market/.gitignore @@ -1,18 +1,11 @@ node_modules - -# Output -.output -.vercel -.netlify -.wrangler -/.svelte-kit -/build dist .turbo +.svelte-kit -# OS -.DS_Store -Thumbs.db +# Logs +logs/ +*.log # Env .env @@ -20,24 +13,31 @@ Thumbs.db !.env.example !.env.test -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* -vite.config.dev* - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* +# OS +.DS_Store +Thumbs.db -# secrets +# Secrets *.pem +*.crt +*.key + +# Testing +test*.json -# local -.local -.vscode +# Editors +.vscode/ +.idea/ +*.iml + +# Notes notes*.txt notes*.md notes*.json -git-diff*.txt -justfile +tree*.txt +diff*.txt +prompt*.txt + +# Dev +.local* +justfile +\ No newline at end of file diff --git a/apps-lib-market/package.json b/apps-lib-market/package.json @@ -1,6 +1,6 @@ { "name": "@radroots/apps-lib-market", - "version": "0.0.0", + "version": "0.0.1", "private": true, "license": "GPLv3", "scripts": { @@ -42,6 +42,7 @@ "@sveltejs/vite-plugin-svelte": "^6.0.0", "@types/node": "^22.5.0", "publint": "^0.3.2", + "rimraf": "^6.0.1", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "5.8.3", diff --git a/apps-lib-market/src/lib/components/features/profile-listing.svelte b/apps-lib-market/src/lib/components/features/profile-listing.svelte @@ -1,122 +0,0 @@ -<script lang="ts"> - import type { - IndexedEventsStorePayload, - OrderBundle, - TradeFlowService, - TradeListingBundle, - } from "$root"; - - import type { RadrootsListing } from "@radroots/events-bindings"; - import { TRADE_LISTING_STAGE } from "@radroots/utils-nostr"; - import { - type TradeListingOrderRequestPayload, - } from "@radroots/trade-bindings"; - - let { - basis, - }: { - basis: { - trade: TradeFlowService | null; - listing_event: IndexedEventsStorePayload<RadrootsListing>; - }; - } = $props(); - - function build_order_payload(): TradeListingOrderRequestPayload { - const listing = basis.listing_event?.data; - if (!listing) throw new Error(`!listing`); - - const quantity = listing.quantities[0]; - if (!quantity) throw new Error(`!q`); - - const price = listing.prices[0]; - if (!price) throw new Error(`!price`); - - const payload: TradeListingOrderRequestPayload = { - price, - quantity, - }; - return payload; - } - - // ---- Reactive bundle + latest order selection ---- - const bundle = $derived<TradeListingBundle | undefined>( - basis.trade?.get_trade_listing_bundle(basis.listing_event.id) || - undefined, - ); - - function pick_latest_order( - b?: TradeListingBundle, - ): OrderBundle | undefined { - if (!b) return undefined; - let best: OrderBundle | undefined; - for (const [, ob] of b.pending_orders) { - if (!best || (ob.last_update_at ?? 0) > (best.last_update_at ?? 0)) - best = ob; - } - for (const [, ob] of b.orders) { - if (!best || (ob.last_update_at ?? 0) > (best.last_update_at ?? 0)) - best = ob; - } - return best; - } - - const latest_order = $derived<OrderBundle | undefined>( - pick_latest_order(bundle), - ); - const is_loading = $derived<boolean>(!!latest_order?.loading); - - const last_order_result = $derived( - latest_order?.results?.[TRADE_LISTING_STAGE.Order]?.at(-1), - ); - const last_feedback = $derived( - latest_order?.feedback?.[TRADE_LISTING_STAGE.Order]?.at(-1), - ); - - // ---- Actions ---- - async function handle_order_click() { - if (!basis.trade) return; - try { - const payload = build_order_payload(); - const res = await basis.trade.order_request( - basis.listing_event.id, - payload, - ); - - if (!res.ok) { - console.warn("order_request failed", res.error, res.request); - return; - } - - const { request, result, order_id } = res; - console.log("order created:", { - request_id: request.id, - order_id, - result_id: result.id, - }); - } catch (err) { - console.error("order_request threw", err); - } - } -</script> - -<div class="flex flex-col gap-2"> - <button class="px-3 py-1 rounded bg-ly1" onclick={handle_order_click}> - {is_loading ? "ordering..." : "order"} - </button> - - {#if last_order_result} - <pre class="text-xs break-all">{JSON.stringify( - last_order_result, - null, - 2, - )}</pre> - {/if} - - {#if last_feedback} - <pre class="text-xs break-all opacity-80">{JSON.stringify( - last_feedback, - null, - 2, - )}</pre> - {/if} -</div> diff --git a/apps-lib-market/src/lib/components/layouts/layout-column-entry.svelte b/apps-lib-market/src/lib/components/layouts/layout-column-entry.svelte @@ -1,14 +0,0 @@ -<script lang="ts"> - import type { Snippet } from "svelte"; - - let { - children, - basis, - }: { children: Snippet; basis?: { classes?: string } } = $props(); -</script> - -<div - class={`flex flex-col w-full desktop:w-[700px] justify-start items-start ${basis?.classes}`} -> - {@render children()} -</div> diff --git a/apps-lib-market/src/lib/components/layouts/layout-column-heading-display-simple.svelte b/apps-lib-market/src/lib/components/layouts/layout-column-heading-display-simple.svelte @@ -1,18 +0,0 @@ -<script lang="ts"> - import type { Snippet } from "svelte"; - - let { row1, row2, row3 }: { row1: Snippet; row2: Snippet; row3: Snippet } = - $props(); -</script> - -<div class={`flex flex-row w-full gap-2 justify-start items-center`}> - {@render row1()} -</div> -<div class={`flex flex-col w-full gap-1 justify-start items-center`}> - <div class={`flex flex-row w-full justify-start items-center`}> - {@render row2()} - </div> - <div class={`flex flex-row w-full gap-2 justify-start items-center`}> - {@render row3()} - </div> -</div> diff --git a/apps-lib-market/src/lib/components/layouts/layout-column-heading-view-buttons.svelte b/apps-lib-market/src/lib/components/layouts/layout-column-heading-view-buttons.svelte @@ -1,38 +0,0 @@ -<script lang="ts"> - import { Flex, Glyph } from "@radroots/apps-lib"; - import type { CallbackPromise } from "@radroots/utils"; - - let view_index = $state(0); -</script> - -<div - class={`grid grid-cols-12 flex flex-row h-[44px] w-full justify-around items-center border-b-line border-b-cloak_grey/20`} -> - {#snippet button(index: number, icon: string, callback: CallbackPromise)} - <button - class={`relative col-span-4 flex flex-col h-full justify-center items-center`} - onclick={async () => { - view_index = index; - await callback(); - }} - > - <Glyph - basis={{ - classes: `text-black_panther`, - size: `sm`, - key: icon, - }} - /> - {#if view_index === index} - <div - class={`absolute bottom-0 left-0 flex flex-row h-[2px] w-full justify-start items-center bg-cloak_grey/40 el-re`} - > - <Flex /> - </div> - {/if} - </button> - {/snippet} - {@render button(0, `grid-nine`, async () => {})} - {@render button(1, `monitor-play`, async () => {})} - {@render button(2, `user-rectangle`, async () => {})} -</div> diff --git a/apps-lib-market/src/lib/components/layouts/layout-column-heading.svelte b/apps-lib-market/src/lib/components/layouts/layout-column-heading.svelte @@ -1,34 +0,0 @@ -<script lang="ts"> - import { Flex } from "@radroots/apps-lib"; - import type { Snippet } from "svelte"; - - let { - heading, - subheading, - footer, - }: { - heading: Snippet; - subheading: Snippet; - footer?: Snippet; - } = $props(); -</script> - -<div class={`flex flex-col w-full pb-2 justify-start items-start`}> - <div - class={`flex flex-row h-[120px] w-full px-4 gap-4 justify-center items-center`} - > - <div - class={`flex flex-row flex-shrink-0 h-[96px] w-[96px] justify-center items-center bg-lime-500/20 rounded-sm`} - > - <Flex /> - </div> - <div - class={`flex flex-col h-full w-full gap-2 justify-center items-start`} - > - {@render heading()} - </div> - </div> - <div class={`flex flex-col w-full px-4 py-1 justify-center items-start`}> - {@render subheading()} - </div> -</div> diff --git a/apps-lib-market/src/lib/components/layouts/layout-column.svelte b/apps-lib-market/src/lib/components/layouts/layout-column.svelte @@ -1,11 +0,0 @@ -<script lang="ts"> - import type { Snippet } from "svelte"; - - let { children }: { children: Snippet } = $props(); -</script> - -<div - class={`flex flex-col w-full desktop:p-8 gap-4 desktop:gap-8 justify-center items-center`} -> - {@render children()} -</div> diff --git a/apps-lib-market/src/lib/index.ts b/apps-lib-market/src/lib/index.ts @@ -1,23 +1,2 @@ -export { default as ProfileListing } from "./components/features/profile-listing.svelte" -export { default as LayoutColumnEntry } from "./components/layouts/layout-column-entry.svelte" -export { default as LayoutColumnHeadingDisplaySimple } from "./components/layouts/layout-column-heading-display-simple.svelte" -export { default as LayoutColumnHeadingViewButtons } from "./components/layouts/layout-column-heading-view-buttons.svelte" -export { default as LayoutColumnHeading } from "./components/layouts/layout-column-heading.svelte" -export { default as LayoutColumn } from "./components/layouts/layout-column.svelte" -export * from "./types/profile/load.js" -export * from "./types/profile/view.js" -export * from "./utils/app/storage.js" -export * from "./utils/app/theme.js" -export * from "./utils/lib.js" -export * from "./utils/nostr/events/listing/manager.svelte.js" -export * from "./utils/nostr/events/profile/manager.svelte.js" -export * from "./utils/nostr/stores/indexed_store.svelte.js" -export * from "./utils/nostr/trade/listing/manager.svelte.js" -export * from "./utils/nostr/trade/listing/types.js" -export { default as ProfileIndexed } from "./views/profile/profile-indexed.svelte" -export { default as ProfileNetworkNip05 } from "./views/profile/profile-network-nip05.svelte" -export { default as ProfileNetworkNpub } from "./views/profile/profile-network-npub.svelte" -export { default as ProfileNetworkPublicKey } from "./views/profile/profile-network-public-key.svelte" -export { default as ProfileNetwork } from "./views/profile/profile-network.svelte" -export { default as Profile } from "./views/profile/profile.svelte" - +export const tmp = true; +export { default as Tmp } from "./tmp.svelte"; diff --git a/apps-lib-market/src/lib/tmp.svelte b/apps-lib-market/src/lib/tmp.svelte @@ -0,0 +1,4 @@ +<script lang="ts"> +</script> + +<div></div> diff --git a/apps-lib-market/src/lib/types/profile/load.ts b/apps-lib-market/src/lib/types/profile/load.ts @@ -1,20 +0,0 @@ -import type { RadrootsCommentEventMetadata, RadrootsListingEventMetadata, RadrootsProfileEventMetadata } from "@radroots/events-bindings"; - -export type PageLoadProfileData = { - public_key: string; - npub?: string; - events: PageLoadProfileDataEvents; -}; - -export type PageLoadProfileDataEvents = - ( - { - profile: RadrootsProfileEventMetadata; - } | { - profile: RadrootsProfileEventMetadata; - listings: RadrootsListingEventMetadata[]; - listing_comments: Record<string, RadrootsCommentEventMetadata[]>; - followers: number; - following: number; - } - ); diff --git a/apps-lib-market/src/lib/types/profile/view.ts b/apps-lib-market/src/lib/types/profile/view.ts @@ -1,23 +0,0 @@ -import type { PageLoadProfileData } from "$root"; - -export type IProfileView = IProfileViewIndexed | IProfileViewNetwork; - -export type IProfileViewIndexed = { - indexed: PageLoadProfileData; -}; - -export type IProfileViewNetwork = { - unknown?: IProfileViewNetworkPublicKey | IProfileViewNetworkNpub | IProfileViewNetworkNip05; -}; - -export type IProfileViewNetworkPublicKey = { - public_key: string; -}; - -export type IProfileViewNetworkNpub = { - npub: string; -}; - -export type IProfileViewNetworkNip05 = { - nip05: string; -}; -\ No newline at end of file diff --git a/apps-lib-market/src/lib/utils/_env.ts b/apps-lib-market/src/lib/utils/_env.ts @@ -1,6 +0,0 @@ -const RADROOTS_MARKET_RELAY_INDEXES_URL = import.meta.env.VITE_PUBLIC_RADROOTS_MARKET_RELAY_INDEXES_URL; -if (!RADROOTS_MARKET_RELAY_INDEXES_URL || typeof RADROOTS_MARKET_RELAY_INDEXES_URL !== 'string') throw new Error('Missing env var: VITE_PUBLIC_RADROOTS_MARKET_RELAY_INDEXES_URL'); - -export const _env = { - RADROOTS_MARKET_RELAY_INDEXES_URL, -} as const; -\ No newline at end of file diff --git a/apps-lib-market/src/lib/utils/app/storage.ts b/apps-lib-market/src/lib/utils/app/storage.ts @@ -1,21 +0,0 @@ -import { IdbLib, type ThemeMode } from "@radroots/apps-lib"; - -export type GlobalConfig = { - theme_mode: ThemeMode; - theme_key: string; - locale: string; - global_relays: string[]; - npub: string; -}; - -export type GlobalConfigKeys = keyof GlobalConfig; - - -export type PageSession = { - draftId: string; - lastScroll: number; -}; - -export type PageSessionKeys = keyof PageSession; - -export const idb = new IdbLib<GlobalConfigKeys, GlobalConfig, PageSessionKeys, PageSession>(); -\ No newline at end of file diff --git a/apps-lib-market/src/lib/utils/app/theme.ts b/apps-lib-market/src/lib/utils/app/theme.ts @@ -1,41 +0,0 @@ -import { browser } from "$app/environment"; -import { idb } from "$root"; -import { get_store, get_system_theme, theme_key, theme_mode, theme_reset, theme_set, theme_toggle } from "@radroots/apps-lib"; - -export const toggle_theme = async (): Promise<void> => { - await theme_toggle(async (mode) => { - await idb.save_global("theme_mode", mode); - }); -}; - -export const init_theme = async (): Promise<void> => { - let mode = await idb.read_global("theme_mode"); - let key = await idb.read_global("theme_key"); - - const mode_sys = get_system_theme(); - - if (!mode || mode !== mode_sys) { - mode = mode_sys; - await idb.save_global("theme_mode", mode); - } - - if (!key) { - key = `os`; - await idb.save_global("theme_key", key); - } - - const $mode = get_store(theme_mode); - const $key = get_store(theme_key); - - if (mode === $mode && key === $key) return; - - theme_mode.set(mode); - theme_key.set(key); - theme_reset.set(true); -}; - -theme_reset.subscribe((reset) => { - if (!reset || !browser) return; - theme_set(get_store(theme_key), get_store(theme_mode)); - theme_reset.set(false); -}) diff --git a/apps-lib-market/src/lib/utils/lib.ts b/apps-lib-market/src/lib/utils/lib.ts @@ -1 +0,0 @@ -export const head_title_suffix = `• Radroots Market` -\ No newline at end of file diff --git a/apps-lib-market/src/lib/utils/nostr/events/listing/manager.svelte.ts b/apps-lib-market/src/lib/utils/nostr/events/listing/manager.svelte.ts @@ -1,51 +0,0 @@ -import { create_indexed_events_store, type IndexedEventsStorePayload } from '$root'; -import type { RadrootsListing } from "@radroots/events-bindings"; -import { KIND_RADROOTS_LISTING, type RadrootsListingNostrEvent } from '@radroots/utils-nostr'; - -export function create_radroots_listing_manager(initial_indexed: RadrootsListingNostrEvent[] = []) { - const store = create_indexed_events_store<RadrootsListing>({ - key_of: (p) => p.data?.d_tag, - }); - - const to_indexed_payload = (r: RadrootsListingNostrEvent): IndexedEventsStorePayload<RadrootsListing> => ({ - id: r.id, - kind: KIND_RADROOTS_LISTING, - author: r.author, - published_at: r.published_at ?? 0, - data: r.listing, - source: 'indexed', - }); - - store.init( - (initial_indexed ?? []) - .filter((r) => r?.listing?.d_tag) - .map(to_indexed_payload), - ); - - const on_parsed_event = (parsed: RadrootsListingNostrEvent) => { - store.add({ - id: parsed.id, - kind: parsed.kind ?? KIND_RADROOTS_LISTING, - author: parsed.author, - published_at: parsed.published_at, - data: parsed.listing, - source: 'nostr', - }); - } - - const init_from_indexed = (rows: RadrootsListingNostrEvent[]) => { - store.init( - (rows ?? []) - .filter((r) => r?.listing?.d_tag) - .map(to_indexed_payload), - ); - } - - return { - get list() { return store.list; }, - get map() { return store.map; }, - init_from_indexed, - on_parsed_event, - to_indexed_payload, - }; -} diff --git a/apps-lib-market/src/lib/utils/nostr/events/profile/manager.svelte.ts b/apps-lib-market/src/lib/utils/nostr/events/profile/manager.svelte.ts @@ -1,51 +0,0 @@ -import { create_indexed_events_store, type IndexedEventsStorePayload } from '$root'; -import type { RadrootsProfile } from "@radroots/events-bindings"; -import { KIND_RADROOTS_PROFILE, type RadrootsProfileNostrEvent } from '@radroots/utils-nostr'; - -export function create_radroots_profile_manager(initial_indexed?: RadrootsProfileNostrEvent) { - const store = create_indexed_events_store<RadrootsProfile>({ - key_of: (p) => p.author, - }); - - const to_indexed_payload = (r: RadrootsProfileNostrEvent): IndexedEventsStorePayload<RadrootsProfile> => ({ - id: r.id, - kind: KIND_RADROOTS_PROFILE, - author: r.author, - published_at: r.published_at ?? 0, - data: r.profile, - source: 'indexed', - }); - - if (initial_indexed?.author && initial_indexed.profile) { - store.init([to_indexed_payload(initial_indexed)]); - } else { - store.init([]); - } - - const on_parsed_event = (parsed: RadrootsProfileNostrEvent) => { - store.add({ - id: parsed.id, - kind: parsed.kind ?? KIND_RADROOTS_PROFILE, - author: parsed.author, - published_at: parsed.published_at, - data: parsed.profile, - source: 'nostr', - }); - } - - const init_from_indexed = (row?: RadrootsProfileNostrEvent) => { - if (row?.author && row.profile) { - store.init([to_indexed_payload(row)]); - } else { - store.init([]); - } - } - - return { - get list() { return store.list; }, - get map() { return store.map; }, - init_from_indexed, - on_parsed_event, - to_indexed_payload, - }; -} diff --git a/apps-lib-market/src/lib/utils/nostr/stores/indexed_store.svelte.ts b/apps-lib-market/src/lib/utils/nostr/stores/indexed_store.svelte.ts @@ -1,56 +0,0 @@ -import type { NdkEventBasis } from '@radroots/utils-nostr'; -import { SvelteMap } from 'svelte/reactivity'; - -export type IndexedEventsStoreSource = 'indexed' | 'nostr'; - -export type IndexedEventsStorePayload<T> = NdkEventBasis<number> & { - data: T; - source: IndexedEventsStoreSource; -} - -export type CreateIndexedEventsStoreOptions<T> = { - key_of: (p: IndexedEventsStorePayload<T>) => string | undefined; - is_newer?: (a: IndexedEventsStorePayload<T>, b: IndexedEventsStorePayload<T>) => boolean; -} - -export const default_is_newer = <T>(a: IndexedEventsStorePayload<T>, b: IndexedEventsStorePayload<T>) => { - const at = a.published_at ?? 0; - const bt = b.published_at ?? 0; - if (at !== bt) return at > bt; - if (a.source !== b.source) return a.source === 'nostr'; - return a.id > b.id; -}; - -export function create_indexed_events_store<T>(opts: CreateIndexedEventsStoreOptions<T>) { - let map = $state(new SvelteMap<string, IndexedEventsStorePayload<T>>()); - - const add = (p: IndexedEventsStorePayload<T>) => { - const key = opts.key_of(p); - if (!key) return; - const existing = map.get(key); - const newer = existing ? (opts.is_newer ?? default_is_newer)(p, existing) : true; - if (newer) map.set(key, p); - }; - - const init = (items: IndexedEventsStorePayload<T>[]) => { - const m = new SvelteMap<string, IndexedEventsStorePayload<T>>(); - for (const it of items) { - const key = opts.key_of(it); - if (!key) continue; - const ex = m.get(key); - if (!ex || (opts.is_newer ?? default_is_newer)(it, ex)) m.set(key, it); - } - map = m; - }; - - const list_raw = $derived(Array.from(map.values())); - - const list = $derived([...list_raw].sort((a, b) => (b.published_at ?? 0) - (a.published_at ?? 0))); - - return { - get map() { return map; }, - get list() { return list; }, - add, - init, - }; -} diff --git a/apps-lib-market/src/lib/utils/nostr/trade/listing/manager.svelte.ts b/apps-lib-market/src/lib/utils/nostr/trade/listing/manager.svelte.ts @@ -1,817 +0,0 @@ -import NDK, { NDKEvent, NDKSubscription, NDKUser } from "@nostr-dev-kit/ndk"; -import { KIND_JOB_FEEDBACK } from "@radroots/events-bindings"; -import { MARKER_LISTING, type TradeListingAcceptRequest, type TradeListingConveyanceRequest, type TradeListingFulfillmentRequest, type TradeListingInvoiceRequest, type TradeListingOrderRequestPayload, type TradeListingPaymentProofRequest, type TradeListingReceiptRequest } from "@radroots/trade-bindings"; -import { time_now_ms } from "@radroots/utils"; -import { - KIND_RADROOTS_LISTING, - REQUEST_KINDS, - RESULT_KINDS, - TAG_E, - TRADE_LISTING_STAGE, - get_event_tag, - get_job_input_data_for_marker, - get_trade_listing_stage_from_event_kind, - ndk_event_trade_listing_accept_request, - ndk_event_trade_listing_conveyance_request, - ndk_event_trade_listing_fulfillment_request, - ndk_event_trade_listing_invoice_request, - ndk_event_trade_listing_order_request, - ndk_event_trade_listing_payment_request, - ndk_event_trade_listing_receipt_request, -} from "@radroots/utils-nostr"; -import type { TradeListingStageKind } from "@radroots/utils-nostr"; -import { SvelteMap, SvelteSet } from "svelte/reactivity"; -import type { - AcceptOptions, - ConveyanceOptions, - CreateTradeFlowServiceOptions, - FulfillmentOptions, - InvoiceOptions, - OrderBundle, - OrderRequestErr, - OrderRequestOk, - OrderRequestResult, - PaymentOptions, - ReceiptOptions, - StageActionErr, - StageActionOk, - StageActionResult, - StagePostInput, - StagePostOutput, - TradeFlowService, - TradeListingBundle, -} from "./types"; - -const MAX_ITEMS_PER_BUCKET = 50; - -type Waiter = { - since_ms: number; - resolve: (e: NDKEvent) => void; - reject: (err: Error) => void; - timer?: ReturnType<typeof setTimeout>; -}; - -function push_capped<T>(arr: T[], item: T, cap: number): void { - arr.push(item); - if (arr.length > cap) arr.splice(0, arr.length - cap); -} - -export class TradeFlowServiceImpl implements TradeFlowService { - private ndk: NDK; - private ndk_user_store: () => NDKUser; - - private sub: NDKSubscription | null = null; - - private events_to_thread = new Map<string, { listing_id: string; order_id?: string }>(); - private orphans_by_ref = new Map<string, NDKEvent[]>(); - private loading_ids = new SvelteSet<string>(); - private waiters_by_request = new Map<string, Set<Waiter>>(); - - private latest_update_event: NDKEvent | undefined = undefined; - private load_complete = false; - - private authors: string[] | undefined; - private kinds: number[]; - private default_timeout_ms: number; - - public listings = new SvelteMap<string, TradeListingBundle>(); - - private restarting = false; - - constructor(opts: CreateTradeFlowServiceOptions) { - this.ndk = opts.ndk; - this.ndk_user_store = opts.ndk_user_store; - - this.authors = opts.authors; - this.kinds = opts.kinds - ? opts.kinds - : [ - KIND_RADROOTS_LISTING, - ...Object.values(REQUEST_KINDS), - ...Object.values(RESULT_KINDS), - KIND_JOB_FEEDBACK, - ]; - this.default_timeout_ms = - typeof opts.default_timeout_ms === "number" ? opts.default_timeout_ms : 7000; - - this.restart_subscription(); - } - - get_latest_update(): NDKEvent | undefined { - return this.latest_update_event; - } - - set_filter_authors(authors?: string[] | undefined): void { - this.authors = authors; - this.restart_subscription(); - } - - set_filter_kinds(kinds: number[]): void { - this.kinds = kinds; - this.restart_subscription(); - } - - get_trade_listing_bundle(listing_id: string): TradeListingBundle | undefined { - return this.listings.get(listing_id); - } - - get_order_bundle(listing_id: string, order_id: string): OrderBundle | undefined { - const listing_bundle = this.listings.get(listing_id); - return listing_bundle ? listing_bundle.orders.get(order_id) : undefined; - } - - is_loading(event_id: string): boolean { - return this.loading_ids.has(event_id); - } - - on_event(ev: NDKEvent): void { - queueMicrotask(() => { - if (!this.restarting) this.ingest_event(ev); - }); - } - - async order_request( - listing_id: string, - payload: TradeListingOrderRequestPayload, - timeout_ms?: number - ): Promise<OrderRequestResult> { - try { - const request = await this.publish_request(() => { - const data = { event: { id: listing_id }, payload }; - return ndk_event_trade_listing_order_request({ - ndk: this.ndk, - ndk_user: this.ndk_user_store(), - data, - }); - }); - - this.events_to_thread.set(request.id, { listing_id }); - const listing_bundle = this.ensure_listing(listing_id); - const order_bundle = this.ensure_order(listing_bundle, "pending", request.id); - this.attach_event_to_order(order_bundle, request); - this.index_event(request, listing_id, undefined); - - try { - const result = await this.await_response_for(request.id, timeout_ms); - const order_id = result.id; - const bundle = this.get_order_bundle(listing_id, order_id); - const ok: OrderRequestOk = { ok: true, request, result, order_id, bundle }; - return ok; - } catch { - this.update_loading_by_request(request.id, false); - const err: OrderRequestErr = { ok: false, error: "error.timeout", request }; - return err; - } - } catch { - const err: OrderRequestErr = { ok: false, error: "error.failed_to_publish" }; - return err; - } - } - - async accept_request(opts: AcceptOptions): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Accept>> { - const { listing_id, order_id, timeout_ms } = opts; - const prereq_id = this.resolve_input_event_id(TRADE_LISTING_STAGE.Accept, listing_id, order_id); - if (!prereq_id) { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Accept> = { - ok: false, - stage: TRADE_LISTING_STAGE.Accept, - error: "error.missing_prerequisite", - }; - return err; - } - try { - const data: TradeListingAcceptRequest = { - order_result_event_id: order_id, - listing_event_id: listing_id, - }; - const request = await this.publish_request(() => - ndk_event_trade_listing_accept_request({ - ndk: this.ndk, - ndk_user: this.ndk_user_store(), - data, - }) - ); - return this.await_stage_result(TRADE_LISTING_STAGE.Accept, { - listing_id, - order_id, - request, - timeout_ms, - }); - } catch { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Accept> = { - ok: false, - stage: TRADE_LISTING_STAGE.Accept, - error: "error.failed_to_publish", - }; - return err; - } - } - - async conveyance_request( - opts: ConveyanceOptions - ): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Conveyance>> { - const { listing_id, order_id, method, timeout_ms } = opts; - const prereq_id = this.resolve_input_event_id( - TRADE_LISTING_STAGE.Conveyance, - listing_id, - order_id - ); - if (!prereq_id) { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Conveyance> = { - ok: false, - stage: TRADE_LISTING_STAGE.Conveyance, - error: "error.missing_prerequisite", - }; - return err; - } - try { - const data: TradeListingConveyanceRequest = { - accept_result_event_id: prereq_id, - method, - }; - const request = await this.publish_request(() => - ndk_event_trade_listing_conveyance_request({ - ndk: this.ndk, - ndk_user: this.ndk_user_store(), - data, - }) - ); - return this.await_stage_result(TRADE_LISTING_STAGE.Conveyance, { - listing_id, - order_id, - request, - timeout_ms, - }); - } catch { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Conveyance> = { - ok: false, - stage: TRADE_LISTING_STAGE.Conveyance, - error: "error.failed_to_publish", - }; - return err; - } - } - - async invoice_request( - opts: InvoiceOptions - ): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Invoice>> { - const { listing_id, order_id, timeout_ms } = opts; - const prereq_id = this.resolve_input_event_id(TRADE_LISTING_STAGE.Invoice, listing_id, order_id); - if (!prereq_id) { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Invoice> = { - ok: false, - stage: TRADE_LISTING_STAGE.Invoice, - error: "error.missing_prerequisite", - }; - return err; - } - try { - const data: TradeListingInvoiceRequest = { accept_result_event_id: prereq_id }; - const request = await this.publish_request(() => - ndk_event_trade_listing_invoice_request({ - ndk: this.ndk, - ndk_user: this.ndk_user_store(), - data, - }) - ); - return this.await_stage_result(TRADE_LISTING_STAGE.Invoice, { - listing_id, - order_id, - request, - timeout_ms, - }); - } catch { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Invoice> = { - ok: false, - stage: TRADE_LISTING_STAGE.Invoice, - error: "error.failed_to_publish", - }; - return err; - } - } - - async payment_request( - opts: PaymentOptions - ): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Payment>> { - const { listing_id, order_id, proof, timeout_ms } = opts; - const prereq_id = this.resolve_input_event_id(TRADE_LISTING_STAGE.Payment, listing_id, order_id); - if (!prereq_id) { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Payment> = { - ok: false, - stage: TRADE_LISTING_STAGE.Payment, - error: "error.missing_prerequisite", - }; - return err; - } - try { - const data: TradeListingPaymentProofRequest = { - invoice_result_event_id: prereq_id, - proof, - }; - const request = await this.publish_request(() => - ndk_event_trade_listing_payment_request({ - ndk: this.ndk, - ndk_user: this.ndk_user_store(), - data, - }) - ); - return this.await_stage_result(TRADE_LISTING_STAGE.Payment, { - listing_id, - order_id, - request, - timeout_ms, - }); - } catch { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Payment> = { - ok: false, - stage: TRADE_LISTING_STAGE.Payment, - error: "error.failed_to_publish", - }; - return err; - } - } - - async fulfillment_request( - opts: FulfillmentOptions - ): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Fulfillment>> { - const { listing_id, order_id, timeout_ms } = opts; - const prereq_id = this.resolve_input_event_id( - TRADE_LISTING_STAGE.Fulfillment, - listing_id, - order_id - ); - if (!prereq_id) { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Fulfillment> = { - ok: false, - stage: TRADE_LISTING_STAGE.Fulfillment, - error: "error.missing_prerequisite", - }; - return err; - } - try { - const data: TradeListingFulfillmentRequest = { payment_result_event_id: prereq_id }; - const request = await this.publish_request(() => - ndk_event_trade_listing_fulfillment_request({ - ndk: this.ndk, - ndk_user: this.ndk_user_store(), - data, - }) - ); - return this.await_stage_result(TRADE_LISTING_STAGE.Fulfillment, { - listing_id, - order_id, - request, - timeout_ms, - }); - } catch { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Fulfillment> = { - ok: false, - stage: TRADE_LISTING_STAGE.Fulfillment, - error: "error.failed_to_publish", - }; - return err; - } - } - - async receipt_request( - opts: ReceiptOptions - ): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Receipt>> { - const { listing_id, order_id, note, timeout_ms } = opts; - const prereq_id = this.resolve_input_event_id(TRADE_LISTING_STAGE.Receipt, listing_id, order_id); - if (!prereq_id) { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Receipt> = { - ok: false, - stage: TRADE_LISTING_STAGE.Receipt, - error: "error.missing_prerequisite", - }; - return err; - } - try { - const data: TradeListingReceiptRequest = note - ? { fulfillment_result_event_id: prereq_id, note } - : { fulfillment_result_event_id: prereq_id }; - const request = await this.publish_request(() => - ndk_event_trade_listing_receipt_request({ - ndk: this.ndk, - ndk_user: this.ndk_user_store(), - data, - }) - ); - return this.await_stage_result(TRADE_LISTING_STAGE.Receipt, { - listing_id, - order_id, - request, - timeout_ms, - }); - } catch { - const err: StageActionErr<typeof TRADE_LISTING_STAGE.Receipt> = { - ok: false, - stage: TRADE_LISTING_STAGE.Receipt, - error: "error.failed_to_publish", - }; - return err; - } - } - - post(input: StagePostInput): Promise<StagePostOutput> { - switch (input.stage) { - case TRADE_LISTING_STAGE.Accept: - return this.accept_request(input.opts); - case TRADE_LISTING_STAGE.Conveyance: - return this.conveyance_request(input.opts); - case TRADE_LISTING_STAGE.Invoice: - return this.invoice_request(input.opts); - case TRADE_LISTING_STAGE.Payment: - return this.payment_request(input.opts); - case TRADE_LISTING_STAGE.Fulfillment: - return this.fulfillment_request(input.opts); - case TRADE_LISTING_STAGE.Receipt: - return this.receipt_request(input.opts); - case TRADE_LISTING_STAGE.Cancel: - case TRADE_LISTING_STAGE.Refund: - return Promise.resolve({ - ok: false, - stage: input.stage, - error: "error.not_implemented", - }); - } - } - - destroy(): void { - if (this.sub) { - this.sub.stop(); - this.sub = null; - } - this.listings.clear(); - this.events_to_thread.clear(); - this.orphans_by_ref.clear(); - this.loading_ids.clear(); - this.latest_update_event = undefined; - this.load_complete = false; - - for (const set of this.waiters_by_request.values()) { - for (const w of set) { - if (w.timer) clearTimeout(w.timer); - w.reject(new Error("service destroyed")); - } - } - this.waiters_by_request.clear(); - } - - private async await_stage_result<S extends TradeListingStageKind>( - stage: S, - params: { - listing_id: string; - order_id: string; - request: NDKEvent; - timeout_ms?: number; - } - ): Promise<StageActionResult<S>> { - const { listing_id, order_id, request, timeout_ms } = params; - try { - const result = await this.await_response_for(request.id, timeout_ms); - const bundle = this.get_order_bundle(listing_id, order_id); - const ok: StageActionOk<S> = { ok: true, stage, request, result, order_id, bundle }; - return ok; - } catch { - this.update_loading_by_request(request.id, false); - const err: StageActionErr<S> = { ok: false, stage, error: "error.timeout", request }; - return err; - } - } - - private async publish_request(make: () => Promise<NDKEvent | undefined>): Promise<NDKEvent> { - const ev = await make(); - if (!ev) throw new Error("failed"); - queueMicrotask(() => this.ingest_event(ev)); - return ev; - } - - private async await_response_for(request_id: string, timeout_ms?: number): Promise<NDKEvent> { - this.loading_ids.add(request_id); - const since_ms = time_now_ms(); - - return new Promise<NDKEvent>((resolve, reject) => { - const cleanup = (w: Waiter) => { - const set = this.waiters_by_request.get(request_id); - if (set) { - set.delete(w); - if (set.size === 0) this.waiters_by_request.delete(request_id); - } - this.loading_ids.delete(request_id); - if (w.timer) clearTimeout(w.timer); - }; - - const waiter: Waiter = { - since_ms, - resolve: (e) => { - cleanup(waiter); - resolve(e); - }, - reject: () => { - cleanup(waiter); - reject(new Error("timeout")); - }, - }; - - const existing = this.waiters_by_request.get(request_id); - if (existing) existing.add(waiter); - else this.waiters_by_request.set(request_id, new Set<Waiter>([waiter])); - - const ms = typeof timeout_ms === "number" ? timeout_ms : this.default_timeout_ms; - waiter.timer = setTimeout(() => { - this.update_loading_by_request(request_id, false); - waiter.reject(new Error("timeout")); - }, ms); - }); - } - - private restart_subscription(): void { - if (this.sub) { - this.sub.stop(); - this.sub = null; - } - - this.listings.clear(); - this.events_to_thread.clear(); - this.orphans_by_ref.clear(); - this.loading_ids.clear(); - this.latest_update_event = undefined; - this.load_complete = false; - - const filter: { kinds: number[]; authors?: string[] } = { - kinds: this.kinds, - ...(Array.isArray(this.authors) ? { authors: this.authors } : {}), - }; - - const sub = this.ndk.subscribe(filter, { closeOnEose: false }); - - sub.on("event", (ev: NDKEvent) => { - queueMicrotask(() => { - if (!this.restarting) this.ingest_event(ev); - }); - }); - - sub.on("eose", () => { - this.load_complete = true; - }); - - sub.start(); - this.sub = sub; - this.restarting = false; - } - - private ensure_listing(listing_id: string): TradeListingBundle { - let listing_bundle = this.listings.get(listing_id); - if (!listing_bundle) { - listing_bundle = { listing: undefined, orders: new SvelteMap(), pending_orders: new SvelteMap() }; - this.listings.set(listing_id, listing_bundle); - } - return listing_bundle; - } - - private ensure_order( - listing_bundle: TradeListingBundle, - bucket: "pending" | "orders", - key: string - ): OrderBundle { - const map = bucket === "orders" ? listing_bundle.orders : listing_bundle.pending_orders; - let order_bundle = map.get(key); - if (!order_bundle) { - order_bundle = { - order_id: bucket === "orders" ? key : undefined, - listing_id: listing_bundle.listing ? listing_bundle.listing.id : "", - requests: Object.create(null), - results: Object.create(null), - feedback: Object.create(null), - started_at: time_now_ms(), - last_update_at: time_now_ms(), - loading: false, - }; - map.set(key, order_bundle); - } - return order_bundle; - } - - private attach_event_to_order(order_bundle: OrderBundle, ev: NDKEvent): void { - const stage = get_trade_listing_stage_from_event_kind(ev.kind); - if (!stage) return; - - const is_request_kind = Object.values(REQUEST_KINDS).includes(ev.kind); - const is_result_kind = Object.values(RESULT_KINDS).includes(ev.kind); - - if (is_request_kind) { - const arr = order_bundle.requests[stage] || (order_bundle.requests[stage] = []); - push_capped(arr, ev, MAX_ITEMS_PER_BUCKET); - if (ev.kind === REQUEST_KINDS.order) order_bundle.loading = true; - } else if (is_result_kind) { - const arr = order_bundle.results[stage] || (order_bundle.results[stage] = []); - push_capped(arr, ev, MAX_ITEMS_PER_BUCKET); - order_bundle.loading = false; - } else if (ev.kind === KIND_JOB_FEEDBACK) { - const arr = order_bundle.feedback[stage] || (order_bundle.feedback[stage] = []); - push_capped(arr, ev, MAX_ITEMS_PER_BUCKET); - } - - order_bundle.last_update_at = time_now_ms(); - } - - private index_event(ev: NDKEvent, listing_id: string, order_id: string | undefined): void { - if (!ev.id) return; - this.events_to_thread.set(ev.id, { listing_id, order_id }); - } - - private adopt_orphans(parent_id: string, listing_id: string, order_id?: string): void { - const children = this.orphans_by_ref.get(parent_id); - if (!children || children.length === 0) return; - this.orphans_by_ref.delete(parent_id); - for (const child of children) queueMicrotask(() => this.ingest_event(child)); - } - - private resolve_listing_id_from_ref(ref_id?: string): string | undefined { - if (!ref_id) return undefined; - const thread = this.events_to_thread.get(ref_id); - return thread ? thread.listing_id : undefined; - } - - private ingest_event(ev: NDKEvent): void { - if (!ev.id) return; - - if (ev.kind === KIND_RADROOTS_LISTING) { - const listing_id = ev.id; - const listing_bundle = this.ensure_listing(listing_id); - listing_bundle.listing = ev; - - for (const [, ob] of listing_bundle.orders) ob.listing_id = listing_id; - for (const [, ob] of listing_bundle.pending_orders) ob.listing_id = listing_id; - - this.index_event(ev, listing_id, undefined); - this.adopt_orphans(listing_id, listing_id, undefined); - return; - } - - const ref_req_id_raw = get_event_tag(ev.tags, TAG_E); - const ref_req_id = ref_req_id_raw ? ref_req_id_raw : undefined; - - if (ev.kind === REQUEST_KINDS.order) { - const listing_id = - get_job_input_data_for_marker(ev.tags, MARKER_LISTING) || - this.resolve_listing_id_from_ref(ref_req_id) || - ev.id; - - const listing_bundle = this.ensure_listing(listing_id); - const order_bundle = this.ensure_order(listing_bundle, "pending", ev.id); - - this.index_event(ev, listing_id, undefined); - this.attach_event_to_order(order_bundle, ev); - this.adopt_orphans(ev.id, listing_id, undefined); - return; - } - - if (ev.kind === RESULT_KINDS.order) { - const request_id = ref_req_id; - const listing_id = this.resolve_listing_id_from_ref(request_id || ""); - if (!listing_id) { - const arr = this.orphans_by_ref.get(request_id || "") || []; - push_capped(arr, ev, MAX_ITEMS_PER_BUCKET); - this.orphans_by_ref.set(request_id || "", arr); - return; - } - - const listing_bundle = this.ensure_listing(listing_id); - const order_id = ev.id; - - let order_bundle = request_id ? listing_bundle.pending_orders.get(request_id) : undefined; - if (request_id && listing_bundle.pending_orders.has(request_id)) - listing_bundle.pending_orders.delete(request_id); - if (!order_bundle) order_bundle = this.ensure_order(listing_bundle, "orders", order_id); - - if (order_bundle && !listing_bundle.orders.has(order_id)) { - order_bundle.order_id = order_id; - listing_bundle.orders.set(order_id, order_bundle); - } - - this.attach_event_to_order(order_bundle, ev); - this.index_event(ev, listing_id, order_id); - this.adopt_orphans(order_id, listing_id, order_id); - return; - } - - const listing_id = this.resolve_listing_id_from_ref(ref_req_id || ""); - if (!listing_id) { - const arr = this.orphans_by_ref.get(ref_req_id || "") || []; - push_capped(arr, ev, MAX_ITEMS_PER_BUCKET); - this.orphans_by_ref.set(ref_req_id || "", arr); - return; - } - - const listing_bundle = this.ensure_listing(listing_id); - const ref_thread = ref_req_id ? this.events_to_thread.get(ref_req_id) : undefined; - const order_id = ref_thread ? ref_thread.order_id : undefined; - - if (!order_id) { - if (ref_req_id && listing_bundle.pending_orders.has(ref_req_id)) { - const order_bundle = listing_bundle.pending_orders.get(ref_req_id); - if (order_bundle) { - this.attach_event_to_order(order_bundle, ev); - this.index_event(ev, listing_id, undefined); - this.adopt_orphans(ev.id, listing_id, undefined); - return; - } - } - const arr = this.orphans_by_ref.get(ref_req_id || "") || []; - push_capped(arr, ev, MAX_ITEMS_PER_BUCKET); - this.orphans_by_ref.set(ref_req_id || "", arr); - return; - } - - let order_bundle = listing_bundle.orders.get(order_id); - if (!order_bundle) { - order_bundle = this.ensure_order(listing_bundle, "orders", order_id); - listing_bundle.orders.set(order_id, order_bundle); - } - - this.attach_event_to_order(order_bundle, ev); - this.index_event(ev, listing_id, order_id); - this.adopt_orphans(ev.id, listing_id, order_id); - - const waiters = this.waiters_by_request.get(ref_req_id || ""); - if (waiters && waiters.size) { - const created_ms = (ev.created_at || 0) * 1000; - for (const w of Array.from(waiters)) { - if (created_ms > w.since_ms) w.resolve(ev); - } - } - - const is_result_or_feedback = - Object.values(RESULT_KINDS).includes(ev.kind) || ev.kind === KIND_JOB_FEEDBACK; - - if (this.load_complete && is_result_or_feedback) { - this.latest_update_event = ev; - } - } - - private update_loading_by_request(request_id: string, loading: boolean): void { - const thread = this.events_to_thread.get(request_id); - if (!thread) return; - - const listing_bundle = this.listings.get(thread.listing_id); - if (!listing_bundle) return; - - if (listing_bundle.pending_orders.has(request_id)) { - const order_bundle = listing_bundle.pending_orders.get(request_id); - if (order_bundle && order_bundle.loading !== loading) { - listing_bundle.pending_orders.set(request_id, { ...order_bundle, loading }); - } - return; - } - - if (thread.order_id && listing_bundle.orders.has(thread.order_id)) { - const order_bundle = listing_bundle.orders.get(thread.order_id); - if (order_bundle && order_bundle.loading !== loading) { - listing_bundle.orders.set(thread.order_id, { ...order_bundle, loading }); - } - } - } - - private resolve_input_event_id( - stage: Exclude<TradeListingStageKind, typeof TRADE_LISTING_STAGE.Order>, - listing_id: string, - order_id: string - ): string | undefined { - const bundle = this.get_order_bundle(listing_id, order_id); - if (!bundle) return undefined; - - const last_id = (arr?: NDKEvent[]) => { - if (!arr || arr.length === 0) return undefined; - return arr[arr.length - 1].id; - }; - - if ( - stage === TRADE_LISTING_STAGE.Accept || - stage === TRADE_LISTING_STAGE.Cancel || - stage === TRADE_LISTING_STAGE.Refund - ) { - return order_id; - } - if (stage === TRADE_LISTING_STAGE.Conveyance || stage === TRADE_LISTING_STAGE.Invoice) { - return last_id(bundle.results[TRADE_LISTING_STAGE.Accept]); - } - if (stage === TRADE_LISTING_STAGE.Payment) { - return last_id(bundle.results[TRADE_LISTING_STAGE.Invoice]); - } - if (stage === TRADE_LISTING_STAGE.Fulfillment) { - return last_id(bundle.results[TRADE_LISTING_STAGE.Payment]); - } - if (stage === TRADE_LISTING_STAGE.Receipt) { - return last_id(bundle.results[TRADE_LISTING_STAGE.Fulfillment]); - } - return undefined; - } -} - -export function create_trade_flow_service( - opts: CreateTradeFlowServiceOptions -): TradeFlowService { - return new TradeFlowServiceImpl(opts); -} diff --git a/apps-lib-market/src/lib/utils/nostr/trade/listing/types.ts b/apps-lib-market/src/lib/utils/nostr/trade/listing/types.ts @@ -1,184 +0,0 @@ -import { NDKEvent, NDKUser } from "@nostr-dev-kit/ndk"; -import type { ndk, StoreWritable } from "@radroots/apps-lib"; -import type { TradeListingConveyanceRequest, TradeListingOrderRequestPayload, TradeListingPaymentProofRequest } from "@radroots/trade-bindings"; -import { TRADE_LISTING_STAGE } from "@radroots/utils-nostr"; -import type { TradeListingStageKind } from "@radroots/utils-nostr"; -import type { SvelteMap } from "svelte/reactivity"; - -export type TradeListingStageKey = keyof typeof TRADE_LISTING_STAGE; - -export type TradeFlowServiceError = - | "error.failed_to_publish" - | "error.timeout" - | "error.missing_payload" - | "error.missing_order_id" - | "error.missing_prerequisite" - | "error.not_implemented" - | "error.service_destroyed"; - -export interface OrderBundle { - order_id?: string; - listing_id: string; - requests: Partial<Record<TradeListingStageKind, NDKEvent[]>>; - results: Partial<Record<TradeListingStageKind, NDKEvent[]>>; - feedback: Partial<Record<TradeListingStageKind, NDKEvent[]>>; - started_at?: number; - last_update_at?: number; - loading: boolean; -} - -export interface TradeListingBundle { - listing?: NDKEvent; - orders: SvelteMap<string, OrderBundle>; - pending_orders: SvelteMap<string, OrderBundle>; -} - -export type OrderRequestOk = { - ok: true; - request: NDKEvent; - result: NDKEvent; - order_id: string; - bundle?: OrderBundle; -}; - -export type OrderRequestErr = { - ok: false; - error: TradeFlowServiceError; - request?: NDKEvent; -}; - -export type OrderRequestResult = OrderRequestOk | OrderRequestErr; - -export type StageActionOk<S extends TradeListingStageKind> = { - ok: true; - stage: S; - request: NDKEvent; - result: NDKEvent; - order_id: string; - bundle?: OrderBundle; -}; - -export type StageActionErr<S extends TradeListingStageKind> = { - ok: false; - stage: S; - error: TradeFlowServiceError; - request?: NDKEvent; -}; - -export type StageActionResult<S extends TradeListingStageKind> = StageActionOk<S> | StageActionErr<S>; - -export type AcceptOptions = { - listing_id: string; - order_id: string; - timeout_ms?: number; -}; - -export type ConveyanceOptions = { - listing_id: string; - order_id: string; - method: TradeListingConveyanceRequest["method"]; - timeout_ms?: number; -}; - -export type InvoiceOptions = { - listing_id: string; - order_id: string; - timeout_ms?: number; -}; - -export type PaymentOptions = { - listing_id: string; - order_id: string; - proof: TradeListingPaymentProofRequest["proof"]; - timeout_ms?: number; -}; - -export type FulfillmentOptions = { - listing_id: string; - order_id: string; - timeout_ms?: number; -}; - -export type ReceiptOptions = { - listing_id: string; - order_id: string; - note?: string; - timeout_ms?: number; -}; - -export type CancelOptions = { - listing_id: string; - order_id: string; - timeout_ms?: number; -}; - -export type RefundOptions = { - listing_id: string; - order_id: string; - timeout_ms?: number; -}; - - -export type StagePostInput = - | { stage: typeof TRADE_LISTING_STAGE.Accept; opts: AcceptOptions } - | { stage: typeof TRADE_LISTING_STAGE.Conveyance; opts: ConveyanceOptions } - | { stage: typeof TRADE_LISTING_STAGE.Invoice; opts: InvoiceOptions } - | { stage: typeof TRADE_LISTING_STAGE.Payment; opts: PaymentOptions } - | { stage: typeof TRADE_LISTING_STAGE.Fulfillment; opts: FulfillmentOptions } - | { stage: typeof TRADE_LISTING_STAGE.Receipt; opts: ReceiptOptions } - | { stage: typeof TRADE_LISTING_STAGE.Cancel; opts: CancelOptions } - | { stage: typeof TRADE_LISTING_STAGE.Refund; opts: RefundOptions }; - -export type StagePostOutput = - | StageActionResult<typeof TRADE_LISTING_STAGE.Accept> - | StageActionResult<typeof TRADE_LISTING_STAGE.Conveyance> - | StageActionResult<typeof TRADE_LISTING_STAGE.Invoice> - | StageActionResult<typeof TRADE_LISTING_STAGE.Payment> - | StageActionResult<typeof TRADE_LISTING_STAGE.Fulfillment> - | StageActionResult<typeof TRADE_LISTING_STAGE.Receipt> - | StageActionErr<typeof TRADE_LISTING_STAGE.Cancel> - | StageActionErr<typeof TRADE_LISTING_STAGE.Refund>; - -export interface CreateTradeFlowServiceOptions { - ndk: StoreWritable<typeof ndk>; - ndk_user_store: () => NDKUser; - authors?: string[]; - kinds?: number[]; - default_timeout_ms?: number; -} - -export interface TradeFlowService { - listings: SvelteMap<string, TradeListingBundle>; - - get_latest_update(): NDKEvent | undefined; - - set_filter_authors(authors?: string[] | undefined): void; - set_filter_kinds(kinds: number[]): void; - - get_trade_listing_bundle(listing_id: string): TradeListingBundle | undefined; - get_order_bundle(listing_id: string, order_id: string): OrderBundle | undefined; - is_loading(event_id: string): boolean; - - on_event(ev: NDKEvent): void; - - order_request( - listing_id: string, - payload: TradeListingOrderRequestPayload, - timeout_ms?: number - ): Promise<OrderRequestResult>; - - accept_request(opts: AcceptOptions): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Accept>>; - conveyance_request( - opts: ConveyanceOptions - ): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Conveyance>>; - invoice_request(opts: InvoiceOptions): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Invoice>>; - payment_request(opts: PaymentOptions): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Payment>>; - fulfillment_request( - opts: FulfillmentOptions - ): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Fulfillment>>; - receipt_request(opts: ReceiptOptions): Promise<StageActionResult<typeof TRADE_LISTING_STAGE.Receipt>>; - - post(input: StagePostInput): Promise<StagePostOutput>; - - destroy(): void; -} diff --git a/apps-lib-market/src/lib/views/profile/profile-indexed.svelte b/apps-lib-market/src/lib/views/profile/profile-indexed.svelte @@ -1,430 +0,0 @@ -<script lang="ts"> - import LayoutColumnEntry from "$lib/components/layouts/layout-column-entry.svelte"; - import LayoutColumnHeadingDisplaySimple from "$lib/components/layouts/layout-column-heading-display-simple.svelte"; - import LayoutColumnHeadingViewButtons from "$lib/components/layouts/layout-column-heading-view-buttons.svelte"; - import LayoutColumnHeading from "$lib/components/layouts/layout-column-heading.svelte"; - import LayoutColumn from "$lib/components/layouts/layout-column.svelte"; - import { - create_radroots_listing_manager, - create_radroots_profile_manager, - create_trade_flow_service, - head_title_suffix, - type IndexedEventsStorePayload, - type IProfileViewIndexed, - type TradeFlowService, - } from "$root"; - import { - NDKKind, - type NDKEvent, - type NDKUserProfile, - } from "@nostr-dev-kit/ndk"; - import { Glyph, ndk } from "@radroots/apps-lib"; - import type { RadrootsCoreQuantityPrice } from "@radroots/core-bindings"; - import type { - RadrootsListing, - RadrootsListingEventMetadata, - RadrootsListingQuantity, - RadrootsProfile, - RadrootsProfileEventMetadata, - } from "@radroots/events-bindings"; - import { - on_ndk_event, - type RadrootsListingNostrEvent, - type RadrootsProfileNostrEvent, - } from "@radroots/utils-nostr"; - import { onDestroy, onMount } from "svelte"; - - let { basis }: { basis: IProfileViewIndexed } = $props(); - - let trade: TradeFlowService | null = $state(null); - - const listings_mgr = create_radroots_listing_manager(); - const profiles_mgr = create_radroots_profile_manager(); - - function to_indexed_listing_payload_from_metadata( - m: RadrootsListingEventMetadata, - ): IndexedEventsStorePayload<RadrootsListing> { - return { - id: m.id, - kind: 30402, - author: m.author, - published_at: m.published_at ?? 0, - data: m.listing, - source: "indexed", - }; - } - - function to_indexed_profile_payload_from_metadata( - m: RadrootsProfileEventMetadata, - ): IndexedEventsStorePayload<RadrootsProfile> { - return { - id: m.id, - kind: 0, - author: m.author, - published_at: m.published_at ?? 0, - data: m.profile, - source: "indexed", - }; - } - - function current_listings_meta(): RadrootsListingEventMetadata[] { - return "listings" in basis.indexed.events - ? basis.indexed.events.listings - : []; - } - - function current_profile_meta(): RadrootsProfileEventMetadata { - return basis.indexed.events.profile; - } - - let listings_buffer = $state<IndexedEventsStorePayload<RadrootsListing>[]>( - current_listings_meta() - .filter((r) => r.listing && r.listing.d_tag) - .map(to_indexed_listing_payload_from_metadata), - ); - - let profile_buffer = - $state<IndexedEventsStorePayload<RadrootsProfile> | null>( - current_profile_meta() - ? to_indexed_profile_payload_from_metadata( - current_profile_meta(), - ) - : null, - ); - - let have_live_listings = $state(false); - let have_live_profiles = $state(false); - - const listings_view = $derived( - have_live_listings ? listings_mgr.list : listings_buffer, - ); - const profile_view = $derived( - have_live_profiles ? profiles_mgr.list?.[0] : profile_buffer, - ); - - let last_pk = $state(basis.indexed.public_key); - $effect(() => { - if (basis.indexed.public_key !== last_pk) { - const new_listings_meta = current_listings_meta(); - const new_profile_meta = current_profile_meta(); - - listings_buffer = new_listings_meta - .filter((r) => r.listing && r.listing.d_tag) - .map(to_indexed_listing_payload_from_metadata); - - profile_buffer = new_profile_meta - ? to_indexed_profile_payload_from_metadata(new_profile_meta) - : null; - - have_live_listings = false; - have_live_profiles = false; - - trade?.set_filter_authors([basis.indexed.public_key]); - - last_pk = basis.indexed.public_key; - } - }); - - const sub = $ndk.subscribe( - { - kinds: [NDKKind.Metadata, NDKKind.Classified], - authors: [basis.indexed.public_key], - }, - undefined, - { - onEvent: (event: NDKEvent) => { - const parsed = on_ndk_event(event); - if (parsed && "listing" in parsed) { - listings_mgr.on_parsed_event( - parsed as RadrootsListingNostrEvent, - ); - if (!have_live_listings) have_live_listings = true; - } else if (parsed && "profile" in parsed) { - profiles_mgr.on_parsed_event( - parsed as RadrootsProfileNostrEvent, - ); - if (!have_live_profiles) have_live_profiles = true; - } - }, - }, - ); - - onMount(async () => { - trade = create_trade_flow_service({ - ndk: $ndk, - ndk_user_store: () => { - const u = $ndk.activeUser; - if (!u) throw new Error("No active NDK user/signer found."); - return u; - }, - }); - trade.set_filter_authors([basis.indexed.public_key]); - }); - - onDestroy(() => { - sub?.stop(); - trade?.destroy(); - }); - - let ndk_profile: NDKUserProfile | null = $state(null); - const data_user = $derived( - $ndk.getUser({ pubkey: basis.indexed.public_key }), - ); - $effect(() => { - data_user.fetchProfile().then((profile) => { - if (profile) ndk_profile = profile; - }); - }); - - const head_title = $derived( - `${ - basis.indexed.events.profile.profile.display_name || - basis.indexed.events.profile.profile.name - } (@${basis.indexed.events.profile.profile.name}) ${head_title_suffix}`, - ); - - function fmtQty(q: RadrootsListingQuantity): string { - const v = q && q.value ? q.value : undefined; - const amt = v && v.amount ? v.amount : ""; - const unit = v && v.unit ? v.unit : ""; - const lab = v && v.label ? v.label : q && q.label ? q.label : ""; - const pieces = [amt, unit, lab].filter((s) => s && `${s}`.length > 0); - return pieces.join(" "); - } - - function fmtPrice(p: RadrootsCoreQuantityPrice): string { - const a = p && p.amount ? p.amount : undefined; - const q = p && p.quantity ? p.quantity : undefined; - const price = a && a.amount ? a.amount : ""; - const cur = a && a.currency ? a.currency : ""; - const qamt = q && q.amount ? q.amount : ""; - const qun = q && q.unit ? q.unit : ""; - const left = [price, cur] - .filter((s) => s && `${s}`.length > 0) - .join(" "); - const right = [qamt, qun] - .filter((s) => s && `${s}`.length > 0) - .join(" "); - return right ? `${left} per ${right}` : left; - } - - function commentsFor(listingId: string) { - if (!("listings" in basis.indexed.events)) return []; - if (!("listing_comments" in basis.indexed.events)) return []; - const key = listingId.toLowerCase(); - const m = basis.indexed.events.listing_comments; - return m && m[key] ? m[key] : []; - } - - function toDate(ts?: number): string { - if (!ts) return ""; - try { - const d = new Date(ts * 1000); - return d.toLocaleDateString(); - } catch { - return ""; - } - } -</script> - -<svelte:head> - <title>{head_title}</title> - <meta name="description" content={``} /> - <meta property="og:title" content={head_title} /> - <meta property="og:description" content={``} /> -</svelte:head> - -<LayoutColumn> - <LayoutColumnEntry basis={{ classes: `bg-white` }}> - <LayoutColumnHeading> - {#snippet heading()} - <LayoutColumnHeadingDisplaySimple> - {#snippet row1()} - <p - class={`font-br font-[600] text-base text-black_panther`} - > - {basis.indexed.events.profile.profile.name} - </p> - <Glyph - basis={{ - classes: `text-lime-500`, - size: `sm`, - key: `plant`, - }} - /> - {/snippet} - {#snippet row2()} - <p - class={`font-br font-[400] text-sm text-black_panther`} - > - {basis.indexed.events.profile.profile - .display_name || - basis.indexed.events.profile.profile.name} - </p> - {/snippet} - {#snippet row3()} - <p - class={`font-rsfd font-[600] text-sm text-black_panther`} - > - {`${30}M followers`} - </p> - <p - class={`font-rsfd font-[600] text-sm text-black_panther`} - > - {`${209} following`} - </p> - {/snippet} - </LayoutColumnHeadingDisplaySimple> - {/snippet} - {#snippet subheading()} - <p class={`font-sans font-[400] text-sm text-black_panther`}> - {basis.indexed.events.profile.profile.about} - </p> - {/snippet} - </LayoutColumnHeading> - <LayoutColumnHeadingViewButtons /> - </LayoutColumnEntry> - - {#if "listings" in basis.indexed.events} - <LayoutColumnEntry basis={{ classes: `gap-4` }}> - {#each basis.indexed.events.listings as ev (ev.id)} - <div class={`relative flex w-full flex-col`}> - <div class={`flex w-full flex-col gap-2 p-4`}> - <div class={`flex w-full flex-row justify-between`}> - <p - class={`font-sans text-base font-[500] text-black_panther`} - > - {ev.listing.product.title} - </p> - <p - class={`font-sans text-xs font-[500] text-cloak_grey`} - > - {toDate(ev.published_at)} - </p> - </div> - <p - class={`font-sans text-sm font-[400] text-black_panther/80`} - > - {ev.listing.product.summary} - </p> - <div class={`flex w-full flex-wrap gap-2 pt-1`}> - <span - class={`rounded-sm bg-ly1 px-2 py-0.5 text-xs font-[600] text-black_panther/90`} - > - {ev.listing.product.category} - </span> - {#if ev.listing.product.process} - <span - class={`rounded-sm bg-ly1 px-2 py-0.5 text-xs font-[600] text-black_panther/90`} - > - {ev.listing.product.process} - </span> - {/if} - {#if ev.listing.product.year} - <span - class={`rounded-sm bg-ly1 px-2 py-0.5 text-xs font-[600] text-black_panther/90`} - > - {ev.listing.product.year} - </span> - {/if} - {#if ev.listing.product.lot} - <span - class={`rounded-sm bg-ly1 px-2 py-0.5 text-xs font-[600] text-black_panther/90`} - > - {ev.listing.product.lot} - </span> - {/if} - </div> - <div class={`flex w-full flex-row gap-4 pt-2`}> - <div class={`flex flex-col`}> - <p - class={`font-sans text-xs font-[700] text-black_panther/70`} - > - Quantities - </p> - <p - class={`font-sans text-sm text-black_panther/90`} - > - {ev.listing.quantities - .map((q) => fmtQty(q)) - .filter((s) => s.length > 0) - .join(", ")} - </p> - </div> - <div class={`flex flex-col`}> - <p - class={`font-sans text-xs font-[700] text-black_panther/70`} - > - Prices - </p> - <p - class={`font-sans text-sm text-black_panther/90`} - > - {ev.listing.prices - .map((p) => fmtPrice(p)) - .filter((s) => s.length > 0) - .join(" · ")} - </p> - </div> - </div> - <div class={`flex w-full flex-row gap-2 pt-2`}> - <p - class={`font-sans text-xs font-[700] text-black_panther/70`} - > - Location - </p> - <p - class={`font-sans text-sm text-black_panther/90`} - > - {#if ev.listing.location} - {ev.listing.location.primary} - {ev.listing.location.city - ? `, ${ev.listing.location.city}` - : ""} - {ev.listing.location.region - ? `, ${ev.listing.location.region}` - : ""} - {ev.listing.location.country - ? `, ${ev.listing.location.country.toUpperCase()}` - : ""} - {:else} - {"Unlisted"} - {/if} - </p> - </div> - </div> - - <div class={`flex w-full flex-col gap-2 p-4`}> - <p - class={`font-sans text-xs font-[700] uppercase tracking-wide text-black_panther/70`} - > - Comments - </p> - {#each commentsFor(ev.id) as c (c.id)} - <div - class={`flex w-full flex-col gap-1 rounded-sm bg-ly1/40 p-3`} - > - <p - class={`font-sans text-xs font-[600] text-black_panther/70`} - > - {toDate(c.published_at)} - </p> - <p - class={`font-sans text-sm font-[400] text-black_panther`} - > - {c.comment && c.comment.content - ? c.comment.content - : ""} - </p> - </div> - {:else} - <p class={`font-sans text-sm text-cloak_grey`}> - No comments yet - </p> - {/each} - </div> - </div> - {/each} - </LayoutColumnEntry> - {/if} -</LayoutColumn> diff --git a/apps-lib-market/src/lib/views/profile/profile-network-nip05.svelte b/apps-lib-market/src/lib/views/profile/profile-network-nip05.svelte @@ -1,22 +0,0 @@ -<script lang="ts"> - import { - ProfileNetworkPublicKey, - type IProfileViewNetworkNip05, - } from "$root"; - - let { - basis, - }: { - basis: IProfileViewNetworkNip05; - } = $props(); - - let public_key: string | undefined = $state(undefined); -</script> - -{#if public_key} - <ProfileNetworkPublicKey basis={{ public_key }} /> -{:else} - <p class={`font-sans font-[400] text-base text-ly0-gl`}> - {`could not find a public key for ${basis.nip05}`} - </p> -{/if} diff --git a/apps-lib-market/src/lib/views/profile/profile-network-npub.svelte b/apps-lib-market/src/lib/views/profile/profile-network-npub.svelte @@ -1,30 +0,0 @@ -<script lang="ts"> - import { - type IProfileViewNetworkNpub, - ProfileNetworkPublicKey, - } from "$root"; - import { lib_nostr_npub_decode } from "@radroots/utils-nostr"; - import { error } from "@sveltejs/kit"; - import { onMount } from "svelte"; - - let { - basis, - }: { - basis: IProfileViewNetworkNpub; - } = $props(); - - let public_key: string | undefined = $state(undefined); - - onMount(async () => { - public_key = lib_nostr_npub_decode(basis.npub); - if (!public_key) error(404, `invalid:public_key:${public_key}`); - }); -</script> - -{#if public_key} - <ProfileNetworkPublicKey basis={{ public_key }} /> -{:else} - <p class={`font-sans font-[400] text-base text-ly0-gl`}> - {`not a valid npub ${basis.npub}`} - </p> -{/if} diff --git a/apps-lib-market/src/lib/views/profile/profile-network-public-key.svelte b/apps-lib-market/src/lib/views/profile/profile-network-public-key.svelte @@ -1,18 +0,0 @@ -<script lang="ts"> - import { type IProfileViewNetworkPublicKey } from "$root"; - - let { - basis, - }: { - basis: IProfileViewNetworkPublicKey; - } = $props(); -</script> - -<div class={`flex flex-col w-full px-4 pt-8 gap-4 justify-start items-start`}> - <p class={`font-sans font-[400] text-base text-ly0-gl`}> - {`profile`} - </p> - <p class={`font-sans font-[400] text-base text-ly0-gl break-all`}> - {`public_key `}{basis.public_key} - </p> -</div> diff --git a/apps-lib-market/src/lib/views/profile/profile-network.svelte b/apps-lib-market/src/lib/views/profile/profile-network.svelte @@ -1,22 +0,0 @@ -<script lang="ts"> - import { - type IProfileViewNetwork, - ProfileNetworkNip05, - ProfileNetworkNpub, - ProfileNetworkPublicKey, - } from "$root"; - - let { basis }: { basis: IProfileViewNetwork } = $props(); -</script> - -{#if basis.unknown && `public_key` in basis.unknown} - <ProfileNetworkPublicKey basis={basis.unknown} /> -{:else if basis.unknown && `npub` in basis.unknown} - <ProfileNetworkNpub basis={basis.unknown} /> -{:else if basis.unknown && `nip05` in basis.unknown} - <ProfileNetworkNip05 basis={basis.unknown} /> -{:else} - <p class={`font-sans font-[400] text-base text-ly0-gl`}> - {`profile not found`} - </p> -{/if} diff --git a/apps-lib-market/src/lib/views/profile/profile.svelte b/apps-lib-market/src/lib/views/profile/profile.svelte @@ -1,19 +0,0 @@ -<script lang="ts"> - import { type IProfileView, ProfileIndexed, ProfileNetwork } from "$root"; - - let { - basis, - }: { - basis: IProfileView; - } = $props(); - - $effect(() => { - console.log(JSON.stringify(basis, null, 4), `basis`); - }); -</script> - -{#if `indexed` in basis} - <ProfileIndexed {basis} /> -{:else if `unknown` in basis && basis.unknown} - <ProfileNetwork {basis} /> -{/if} diff --git a/apps-lib-market/svelte.config.js b/apps-lib-market/svelte.config.js @@ -1,14 +1,10 @@ import adapter from '@sveltejs/adapter-auto'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; -/** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), kit: { adapter: adapter(), - alias: { - $root: './src/lib/index.js', - } }, }; diff --git a/apps-lib-market/vite.config.ts b/apps-lib-market/vite.config.ts @@ -2,10 +2,5 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - resolve: { - alias: { - '$root': '/src/lib/index.js', - } - }, - plugins: [sveltekit()] + plugins: [sveltekit() as any] }); diff --git a/apps-lib-pwa/src/lib/utils/farm/schema.ts b/apps-lib-pwa/src/lib/utils/farm/schema.ts @@ -1,6 +1,8 @@ import { dev } from "$app/environment"; import type { IViewFarmsAddSubmission, IViewFarmsProductsAddSubmitPayload } from "$lib/types/views/farms"; -import { form_fields, schema_geocode_result, schema_geolocation_point, util_rxp, zf_numf_pos, zf_numi_pos, zf_price } from "@radroots/utils"; +import { schema_geocode_result, schema_geolocation_point } from "@radroots/geo"; +import { form_fields, util_rxp, zf_numf_pos, zf_numi_pos, zf_price } from "@radroots/utils"; + import { z } from "zod"; export const schema_view_farms_add_submission: z.ZodSchema<IViewFarmsAddSubmission> = z.object({ diff --git a/client/src/error.ts b/client/src/error.ts @@ -4,7 +4,6 @@ export * from "./backup/error.js"; export * from "./datastore/error.js"; export * from "./fs/error.js"; export * from "./geolocation/error.js"; -export * from "./http/error.js"; export * from "./keystore/error.js"; export * from "./notifications/error.js"; export * from "./radroots/error.js"; diff --git a/client/src/keystore/web.ts b/client/src/keystore/web.ts @@ -40,11 +40,10 @@ export class WebKeystore implements IWebKeystore { private store_id: string; private legacy_key_config: LegacyKeyConfig; - constructor(config?: IdbClientConfig) { - const config_base = config ?? {}; + constructor(config?: Partial<IdbClientConfig>) { this.config = { - database: config_base.database ?? IDB_CONFIG_KEYSTORE.database, - store: config_base.store ?? IDB_CONFIG_KEYSTORE.store + database: config?.database ?? IDB_CONFIG_KEYSTORE.database, + store: config?.store ?? IDB_CONFIG_KEYSTORE.store }; this.store = null; this.store_id = `keystore:${this.config.database}:${this.config.store}`; diff --git a/themes/package.json b/themes/package.json @@ -32,6 +32,10 @@ "dev": "npm run watch", "watch": "tsc -w" }, + "dependencies": { + "daisyui": "^5.0.46", + "tailwindcss": "^4.1.11" + }, "devDependencies": { "@radroots/tsconfig": "workspace:*", "@types/node": "^25.0.3",