Load user data before updating, prefer newer events in repository over old ones with the same created_at, add list update utils

This commit is contained in:
Jon Staab
2025-08-01 13:09:50 -07:00
parent bd67f2763d
commit 1f5f869f7c
9 changed files with 119 additions and 11 deletions
+54 -1
View File
@@ -4,6 +4,8 @@ import {
sendManagementRequest, sendManagementRequest,
ManagementRequest, ManagementRequest,
addToListPublicly, addToListPublicly,
addToListPrivately,
updateList,
EventTemplate, EventTemplate,
removeFromList, removeFromList,
makeHttpAuth, makeHttpAuth,
@@ -30,10 +32,15 @@ import {Nip59, stamp} from "@welshman/signer"
import {Router, addMaximalFallbacks} from "@welshman/router" import {Router, addMaximalFallbacks} from "@welshman/router"
import { import {
userRelaySelections, userRelaySelections,
loadUserRelaySelections,
userInboxRelaySelections, userInboxRelaySelections,
loadUserInboxRelaySelections,
userFollows, userFollows,
loadUserFollows,
userMutes, userMutes,
loadUserMutes,
userPins, userPins,
loadUserPins,
} from "./user.js" } from "./user.js"
import {nip44EncryptToSelf, signer} from "./session.js" import {nip44EncryptToSelf, signer} from "./session.js"
import {ThunkOptions, MergedThunk, publishThunk} from "./thunk.js" import {ThunkOptions, MergedThunk, publishThunk} from "./thunk.js"
@@ -41,6 +48,8 @@ import {ThunkOptions, MergedThunk, publishThunk} from "./thunk.js"
// NIP 65 // NIP 65
export const removeRelay = async (url: string, mode: RelayMode) => { export const removeRelay = async (url: string, mode: RelayMode) => {
await loadUserRelaySelections([], true)
const list = get(userRelaySelections) || makeList({kind: RELAYS}) const list = get(userRelaySelections) || makeList({kind: RELAYS})
const dup = getRelayTags(getListTags(list)).find(nthEq(1, url)) const dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
const alt = mode === RelayMode.Read ? RelayMode.Write : RelayMode.Read const alt = mode === RelayMode.Read ? RelayMode.Write : RelayMode.Read
@@ -61,6 +70,8 @@ export const removeRelay = async (url: string, mode: RelayMode) => {
} }
export const addRelay = async (url: string, mode: RelayMode) => { export const addRelay = async (url: string, mode: RelayMode) => {
await loadUserRelaySelections([], true)
const list = get(userRelaySelections) || makeList({kind: RELAYS}) const list = get(userRelaySelections) || makeList({kind: RELAYS})
const dup = getRelayTags(getListTags(list)).find(nthEq(1, url)) const dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
const tag = removeNil(["r", url, dup && dup[2] !== mode ? undefined : mode]) const tag = removeNil(["r", url, dup && dup[2] !== mode ? undefined : mode])
@@ -74,6 +85,8 @@ export const addRelay = async (url: string, mode: RelayMode) => {
// NIP 17 // NIP 17
export const removeInboxRelay = async (url: string) => { export const removeInboxRelay = async (url: string) => {
await loadUserInboxRelaySelections([], true)
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS}) const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
const event = await removeFromList(list, url).reconcile(nip44EncryptToSelf) const event = await removeFromList(list, url).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -82,6 +95,8 @@ export const removeInboxRelay = async (url: string) => {
} }
export const addInboxRelay = async (url: string) => { export const addInboxRelay = async (url: string) => {
await loadUserInboxRelaySelections([], true)
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS}) const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
const event = await addToListPublicly(list, ["relay", url]).reconcile(nip44EncryptToSelf) const event = await addToListPublicly(list, ["relay", url]).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -102,6 +117,8 @@ export const setProfile = (profile: Profile) => {
// NIP 02 // NIP 02
export const unfollow = async (value: string) => { export const unfollow = async (value: string) => {
await loadUserFollows([], true)
const list = get(userFollows) || makeList({kind: FOLLOWS}) const list = get(userFollows) || makeList({kind: FOLLOWS})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf) const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -110,6 +127,8 @@ export const unfollow = async (value: string) => {
} }
export const follow = async (tag: string[]) => { export const follow = async (tag: string[]) => {
await loadUserFollows([], true)
const list = get(userFollows) || makeList({kind: FOLLOWS}) const list = get(userFollows) || makeList({kind: FOLLOWS})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf) const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -118,6 +137,8 @@ export const follow = async (tag: string[]) => {
} }
export const unmute = async (value: string) => { export const unmute = async (value: string) => {
await loadUserMutes([], true)
const list = get(userMutes) || makeList({kind: MUTES}) const list = get(userMutes) || makeList({kind: MUTES})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf) const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -125,7 +146,9 @@ export const unmute = async (value: string) => {
return publishThunk({event, relays}) return publishThunk({event, relays})
} }
export const mute = async (tag: string[]) => { export const mutePublicly = async (tag: string[]) => {
await loadUserMutes([], true)
const list = get(userMutes) || makeList({kind: MUTES}) const list = get(userMutes) || makeList({kind: MUTES})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf) const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -133,7 +156,35 @@ export const mute = async (tag: string[]) => {
return publishThunk({event, relays}) return publishThunk({event, relays})
} }
export const mutePrivately = async (tag: string[]) => {
await loadUserMutes([], true)
const list = get(userMutes) || makeList({kind: MUTES})
const event = await addToListPrivately(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
return publishThunk({event, relays})
}
export const setMutes = async ({
publicTags,
privateTags,
}: {
publicTags?: string[][]
privateTags?: string[][]
}) => {
await loadUserMutes([], true)
const list = get(userMutes) || makeList({kind: MUTES})
const event = await updateList(list, {publicTags, privateTags}).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
return publishThunk({event, relays})
}
export const unpin = async (value: string) => { export const unpin = async (value: string) => {
await loadUserPins([], true)
const list = get(userPins) || makeList({kind: PINS}) const list = get(userPins) || makeList({kind: PINS})
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf) const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
@@ -142,6 +193,8 @@ export const unpin = async (value: string) => {
} }
export const pin = async (tag: string[]) => { export const pin = async (tag: string[]) => {
await loadUserPins([], true)
const list = get(userPins) || makeList({kind: PINS}) const list = get(userPins) || makeList({kind: PINS})
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf) const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls() const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
+27 -1
View File
@@ -14,9 +14,11 @@ import {
} from "./relaySelections.js" } from "./relaySelections.js"
import {wotGraph} from "./wot.js" import {wotGraph} from "./wot.js"
export type UserDataLoader = (pubkey: string, relays?: string[], force?: boolean) => unknown
export type MakeUserDataOptions<T> = { export type MakeUserDataOptions<T> = {
mapStore: Readable<Map<string, T>> mapStore: Readable<Map<string, T>>
loadItem: (pubkey: string) => unknown loadItem: UserDataLoader
} }
const makeUserData = <T>({mapStore, loadItem}: MakeUserDataOptions<T>) => const makeUserData = <T>({mapStore, loadItem}: MakeUserDataOptions<T>) =>
@@ -30,41 +32,65 @@ const makeUserData = <T>({mapStore, loadItem}: MakeUserDataOptions<T>) =>
}), }),
) )
const makeUserLoader =
(loadItem: UserDataLoader) =>
async (relays: string[] = [], force = false) => {
const $pubkey = pubkey.get()
if ($pubkey) {
await loadItem($pubkey, relays, force)
}
}
export const userProfile = makeUserData({ export const userProfile = makeUserData({
mapStore: profilesByPubkey, mapStore: profilesByPubkey,
loadItem: loadProfile, loadItem: loadProfile,
}) })
export const loadUserProfile = makeUserLoader(loadProfile)
export const userFollows = makeUserData({ export const userFollows = makeUserData({
mapStore: followsByPubkey, mapStore: followsByPubkey,
loadItem: loadFollows, loadItem: loadFollows,
}) })
export const loadUserFollows = makeUserLoader(loadFollows)
export const userMutes = makeUserData({ export const userMutes = makeUserData({
mapStore: mutesByPubkey, mapStore: mutesByPubkey,
loadItem: loadMutes, loadItem: loadMutes,
}) })
export const loadUserMutes = makeUserLoader(loadMutes)
export const userPins = makeUserData({ export const userPins = makeUserData({
mapStore: pinsByPubkey, mapStore: pinsByPubkey,
loadItem: loadPins, loadItem: loadPins,
}) })
export const loadUserPins = makeUserLoader(loadPins)
export const userRelaySelections = makeUserData({ export const userRelaySelections = makeUserData({
mapStore: relaySelectionsByPubkey, mapStore: relaySelectionsByPubkey,
loadItem: loadRelaySelections, loadItem: loadRelaySelections,
}) })
export const loadUserRelaySelections = makeUserLoader(loadRelaySelections)
export const userInboxRelaySelections = makeUserData({ export const userInboxRelaySelections = makeUserData({
mapStore: inboxRelaySelectionsByPubkey, mapStore: inboxRelaySelectionsByPubkey,
loadItem: loadInboxRelaySelections, loadItem: loadInboxRelaySelections,
}) })
export const loadUserInboxRelaySelections = makeUserLoader(loadInboxRelaySelections)
export const userBlossomServers = makeUserData({ export const userBlossomServers = makeUserData({
mapStore: blossomServersByPubkey, mapStore: blossomServersByPubkey,
loadItem: loadBlossomServers, loadItem: loadBlossomServers,
}) })
export const loadUserBlossomServers = makeUserLoader(loadBlossomServers)
export const getUserWotScore = (tpk: string) => wotGraph.get().get(tpk) || 0 export const getUserWotScore = (tpk: string) => wotGraph.get().get(tpk) || 0
export const deriveUserWotScore = (tpk: string) => derived(wotGraph, $g => $g.get(tpk) || 0) export const deriveUserWotScore = (tpk: string) => derived(wotGraph, $g => $g.get(tpk) || 0)
+8
View File
@@ -14,6 +14,14 @@ import {
} from "./message.js" } from "./message.js"
import {Socket, SocketStatus, SocketEvent} from "./socket.js" import {Socket, SocketStatus, SocketEvent} from "./socket.js"
import {AuthStatus, AuthStateEvent} from "./auth.js" import {AuthStatus, AuthStateEvent} from "./auth.js"
import {Unsubscriber} from "./util.js"
/**
* The contract for socket policies
* @param socket - a Socket object
* @return a cleanup function
*/
export type SocketPolicy = (socket: Socket) => Unsubscriber
/** /**
* Handles auth-related message management: * Handles auth-related message management:
+1 -1
View File
@@ -215,7 +215,7 @@ export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
if (duplicate) { if (duplicate) {
// If our event is younger than the duplicate, we're done // If our event is younger than the duplicate, we're done
if (event.created_at <= duplicate.created_at) { if (event.created_at < duplicate.created_at) {
return false return false
} }
+2 -2
View File
@@ -69,8 +69,8 @@ export type SignerMethodWrapper = <T>(method: string, thunk: () => Promise<T>) =
export class WrappedSigner extends Emitter implements ISigner { export class WrappedSigner extends Emitter implements ISigner {
constructor( constructor(
private signer: ISigner, readonly signer: ISigner,
private wrapMethod: SignerMethodWrapper, readonly wrapMethod: SignerMethodWrapper,
) { ) {
super() super()
} }
+3 -3
View File
@@ -46,7 +46,7 @@ export const makeCachedLoader = <T>({
const pending = new Map<string, Promise<T | void>>() const pending = new Map<string, Promise<T | void>>()
const loadAttempts = new Map<string, number>() const loadAttempts = new Map<string, number>()
return async (key: string, relays: string[] = []) => { return async (key: string, relays: string[] = [], force = false) => {
const stale = indexStore.get().get(key) const stale = indexStore.get().get(key)
// If we have no loader function, nothing we can do // If we have no loader function, nothing we can do
@@ -57,7 +57,7 @@ export const makeCachedLoader = <T>({
const freshness = getFreshness(name, key) const freshness = getFreshness(name, key)
// If we have an item, reload if it's stale // If we have an item, reload if it's stale
if (stale && freshness > now() - 3600) { if (stale && freshness > now() - 3600 && !force) {
return stale return stale
} }
@@ -69,7 +69,7 @@ export const makeCachedLoader = <T>({
const attempt = loadAttempts.get(key) || 0 const attempt = loadAttempts.get(key) || 0
// Use exponential backoff to throttle attempts // Use exponential backoff to throttle attempts
if (freshness > now() - Math.pow(2, attempt)) { if (freshness > now() - Math.pow(2, attempt) && !force) {
return stale return stale
} }
+2 -2
View File
@@ -39,13 +39,13 @@ export class Encryptable<T extends EventTemplate> {
*/ */
async reconcile(encrypt: Encrypt) { async reconcile(encrypt: Encrypt) {
const encryptContent = () => { const encryptContent = () => {
if (!this.updates.content) return null if (!this.updates.content) return undefined
return encrypt(this.updates.content) return encrypt(this.updates.content)
} }
const encryptTags = () => { const encryptTags = () => {
if (!this.updates.tags) return null if (!this.updates.tags) return undefined
return Promise.all( return Promise.all(
this.updates.tags.map(async tag => { this.updates.tags.map(async tag => {
+19
View File
@@ -89,6 +89,25 @@ export const addToListPrivately = (list: List, ...tags: string[][]) => {
}) })
} }
export const updateList = (
list: List,
{publicTags, privateTags}: {publicTags?: string[][]; privateTags?: string[][]},
) => {
const template = {
kind: list.kind,
content: list.event?.content || "",
tags: publicTags || list.publicTags,
}
const updates: EncryptableUpdates = {}
if (privateTags) {
updates.content = JSON.stringify(privateTags)
}
return new Encryptable(template, updates)
}
export const getRelaysFromList = (list?: List, mode?: RelayMode): string[] => { export const getRelaysFromList = (list?: List, mode?: RelayMode): string[] => {
let tags = getRelayTags(getListTags(list)) let tags = getRelayTags(getListTags(list))
+3 -1
View File
@@ -119,4 +119,6 @@ export const uniqTags = (tags: string[][]) => uniqBy(t => t.slice(0, 2).join(":"
export const tagsFromIMeta = (imeta: string[]) => imeta.map((m: string) => m.split(" ")) export const tagsFromIMeta = (imeta: string[]) => imeta.map((m: string) => m.split(" "))
export const tagger = (name: string) => (value: string) => [name, value] export const tagger =
(name: string) =>
(value: string, ...args: unknown[]) => [name, value]