Skip to content

@snort/shared Examples ​

Real-world usage of ExternalStore, TLV utilities, LNURL, work queues, and FeedCache from the Snort app.

ExternalStore for global state with React integration ​

Extend ExternalStore to create singleton state stores, then consume with useSyncExternalStore:

WalletStore — managing wallet configurations:

typescript
// Wallet/index.ts
export class WalletStore extends ExternalStore<WalletStoreSnapshot> {
  #configs: Array<WalletConfig>

  save() {
    const json = JSON.stringify(this.#configs)
    window.localStorage.setItem("wallet-config", json)
    this.notifyChange()
  }

  takeSnapshot(): WalletStoreSnapshot {
    return {
      configs: [...this.#configs],
      config: this.#configs.find(a => a.active),
      wallet: this.get(),
    }
  }
}

// Consumed via React:
export function useWallet() {
  const wallet = useSyncExternalStore<WalletStoreSnapshot>(
    h => Wallets.hook(h),
    () => Wallets.snapshot(),
  )
}

NoteCreatorStore — note creation UI state with selector support:

typescript
// State/NoteCreator.ts
class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
  #updateFn = (fn: (v: NoteCreatorDataSnapshot) => void) => {
    fn(this.#data)
    this.notifyChange(this.#data)  // pass data to skip snapshot for perf
  }

  takeSnapshot(): NoteCreatorDataSnapshot {
    const sn = { ...this.#data, reset: this.#resetFn, update: this.#updateFn }
    return sn as NoteCreatorDataSnapshot
  }
}

// Consumed with selector support:
export function useNoteCreator<T extends object = NoteCreatorDataSnapshot>(
  selector?: (v: NoteCreatorDataSnapshot) => T,
) {
  return useSyncExternalStoreWithSelector<NoteCreatorDataSnapshot, T>(
    c => NoteCreatorState.hook(c),
    () => NoteCreatorState.snapshot(),
    undefined,
    selector || defaultSelector,
  )
}

ToasterSlots — toast notification stack:

typescript
// Toaster/Toaster.tsx
class ToasterSlots extends ExternalStore<Array<ToastNotification>> {
  #stack: Array<ToastNotification> = []

  push(n: ToastNotification) {
    n.expire ??= unixNow() + 10
    this.#stack.push(n)
    this.notifyChange()
  }

  takeSnapshot(): ToastNotification[] {
    return [...this.#stack]
  }
}
export const Toastore = new ToasterSlots()

TLV encoding for chat IDs ​

typescript
// chat/nip17.ts
function computeChatId(u: UnwrappedGift, pk: string): string | undefined {
  const pTags = [...]
    .filter((v, i, a) => a.indexOf(v) === i)
    .sort()
    .filter(a => a !== pk)

  return encodeTLVEntries(
    "nchat17",
    ...pTags.map(v => ({
      value: v,
      type: TLVEntryType.Author,
      length: v.length,
    }) as TLVEntry),
  )
}

// Decoding:
const participants = decodeTLV(id)
  .filter(v => v.type === TLVEntryType.Author)
  .map(v => ({ type: "pubkey" as const, id: v.value as string }))

bech32ToHex / hexToBech32 for entity ID conversion ​

typescript
// Utils/index.ts
export function parseId(id: string) {
  const hrp = ["note", "npub", "nsec"]
  try {
    if (hrp.some(a => id.startsWith(a))) {
      return bech32ToHex(id)
    }
  } catch (_e) {}
  return id
}

export function eventLink(hex: string, relays?: Array<string> | string) {
  const encoded = relays
    ? encodeTLV(NostrPrefix.Event, hexToBytes(hex), Array.isArray(relays) ? relays : [relays])
    : hexToBech32(NostrPrefix.Note, hex)
  return `/${encoded}`
}

LNURL for ZapPool payouts ​

typescript
// ZapPoolController.ts
async payout(wallet: LNWallet) {
  for (const x of this.#store.values()) {
    const profile = await ProfilesCache.get(x.pubkey)
    const svc = new LNURL(profile.lud16 || profile.lud06 || "")
    await svc.load()
    const invoice = await svc.getInvoice(amtSend, `SnortZapPool: ${x.split}%`)
    if (invoice.pr) {
      const result = await wallet.payInvoice(invoice.pr)
    }
  }
}

processWorkQueue / barrierQueue for serialized async operations ​

typescript
// ZapperQueue.tsx
export const ZapperQueue: Array<WorkQueueItem> = []
processWorkQueue(ZapperQueue)

// FooterZapButton.tsx
await barrierQueue(ZapperQueue, async () => {
  const zapper = new Zapper(system, publisher)
  const result = await zapper.send(wallet, targets, amount)
})

FeedCache as a base class for refresh feeds ​

typescript
// RefreshFeedCache.ts
export abstract class RefreshFeedCache<T> extends FeedCache<TWithCreated<T>> {
  abstract buildSub(session: LoginSession, rb: RequestBuilder): void
  abstract onEvent(evs: Readonly<Array<TaggedNostrEvent>>, pubKey: string, pub?: EventPublisher): void

  protected newest(filter?: (e: TWithCreated<T>) => boolean) {
    let ret = 0
    this.cache.forEach(v => {
      if (!filter || filter(v)) {
        ret = v.created_at > ret ? v.created_at : ret
      }
    })
    return ret
  }
}