Add memoize and batcher, bump versions

This commit is contained in:
Jon Staab
2024-08-12 11:17:27 -07:00
parent a8d0f5bc4f
commit 5d2186825b
15 changed files with 132 additions and 108 deletions
+23 -39
View File
@@ -2169,9 +2169,9 @@
}
},
"node_modules/nostr-tools": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.0.tgz",
"integrity": "sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==",
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.2.tgz",
"integrity": "sha512-Bq3Ug0SZFtgtL1+0wCnAe8AJtI7yx/00/a2nUug9SkhfOwlKS92Tef12iCK9FdwXw+oFZWMtRnSwcLayQso+xA==",
"dependencies": {
"@noble/ciphers": "^0.5.1",
"@noble/curves": "1.2.0",
@@ -3283,12 +3283,12 @@
},
"packages/dvm": {
"name": "@welshman/dvm",
"version": "0.0.3",
"version": "0.0.4",
"license": "MIT",
"dependencies": {
"@welshman/lib": "0.0.13",
"@welshman/net": "0.0.16",
"@welshman/util": "0.0.24",
"@welshman/lib": "0.0.14",
"@welshman/net": "0.0.18",
"@welshman/util": "0.0.25",
"nostr-tools": "^2.7.0"
},
"devDependencies": {
@@ -3297,29 +3297,11 @@
"typescript": "~5.1.6"
}
},
"packages/dvm/node_modules/@scure/base": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz",
"integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"packages/dvm/node_modules/@welshman/net": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@welshman/net/-/net-0.0.16.tgz",
"integrity": "sha512-9NoNKs3BxMs7biyfhLtQFs0rJP5e8raMMjCXYL0+WaHTSPi1VaKBzXGlsikF713pJ0xm5kigAxa6wquonsWHtg==",
"dependencies": {
"@welshman/lib": "0.0.12",
"@welshman/util": "0.0.23",
"isomorphic-ws": "^5.0.0",
"ws": "^8.16.0"
}
},
"packages/dvm/node_modules/@welshman/net/node_modules/@welshman/lib": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@welshman/lib/-/lib-0.0.12.tgz",
"integrity": "sha512-865dbPRbXdpZXNkAXz0KDgW0Yq6krC9Pz59cycfe6k75fDO1w4jA30UlW4E1mJuGQycmBtyi9sAkUPP9LG4Srw==",
"extraneous": true,
"dependencies": {
"@scure/base": "^1.1.6",
"@types/events": "^3.0.3",
@@ -3332,6 +3314,7 @@
"version": "0.0.23",
"resolved": "https://registry.npmjs.org/@welshman/util/-/util-0.0.23.tgz",
"integrity": "sha512-xV9RnPvO3XZ163TD/5ga/vCnLtejMpkIu9JlfiO/iyHJ1DdObq9WkyuHTgVDNxkrPBf74KuoF9gnub9rLMCj/w==",
"extraneous": true,
"dependencies": {
"@welshman/lib": "0.0.12",
"nostr-tools": "^2.3.2"
@@ -3339,10 +3322,10 @@
},
"packages/feeds": {
"name": "@welshman/feeds",
"version": "0.0.13",
"version": "0.0.14",
"license": "MIT",
"dependencies": {
"@welshman/util": "0.0.24"
"@welshman/util": "0.0.25"
},
"devDependencies": {
"gts": "^5.0.1",
@@ -3352,7 +3335,7 @@
},
"packages/lib": {
"name": "@welshman/lib",
"version": "0.0.13",
"version": "0.0.14",
"license": "MIT",
"dependencies": {
"@scure/base": "^1.1.6",
@@ -3376,11 +3359,11 @@
},
"packages/net": {
"name": "@welshman/net",
"version": "0.0.17",
"version": "0.0.18",
"license": "MIT",
"dependencies": {
"@welshman/lib": "0.0.13",
"@welshman/util": "0.0.24",
"@welshman/lib": "0.0.14",
"@welshman/util": "0.0.25",
"isomorphic-ws": "^5.0.0",
"ws": "^8.16.0"
},
@@ -3392,12 +3375,13 @@
},
"packages/signer": {
"name": "@welshman/signer",
"version": "0.0.1",
"version": "0.0.2",
"license": "MIT",
"dependencies": {
"@welshman/lib": "^0.0.13",
"@welshman/net": "^0.0.17",
"@welshman/util": "^0.0.24"
"@welshman/lib": "0.0.14",
"@welshman/net": "0.0.18",
"@welshman/util": "0.0.25",
"nostr-tools": "^2.7.2"
},
"devDependencies": {
"gts": "^5.0.1",
@@ -3407,7 +3391,7 @@
},
"packages/store": {
"name": "@welshman/store",
"version": "0.0.1",
"version": "0.0.2",
"license": "MIT",
"dependencies": {
"svelte": "^4.2.18"
@@ -3420,10 +3404,10 @@
},
"packages/util": {
"name": "@welshman/util",
"version": "0.0.24",
"version": "0.0.25",
"license": "MIT",
"dependencies": {
"@welshman/lib": "0.0.13",
"@welshman/lib": "0.0.14",
"nostr-tools": "^2.3.2"
},
"devDependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@welshman/content",
"version": "0.0.6",
"version": "0.0.7",
"author": "hodlbod",
"license": "MIT",
"description": "A collection of utilities for parsing nostr note content.",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@welshman/dvm",
"version": "0.0.3",
"version": "0.0.4",
"author": "hodlbod",
"license": "MIT",
"description": "A collection of utilities for building nostr DVMs.",
@@ -31,9 +31,9 @@
"typescript": "~5.1.6"
},
"dependencies": {
"@welshman/lib": "0.0.13",
"@welshman/net": "0.0.16",
"@welshman/util": "0.0.24",
"@welshman/lib": "0.0.14",
"@welshman/net": "0.0.18",
"@welshman/util": "0.0.25",
"nostr-tools": "^2.7.0"
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@welshman/feeds",
"version": "0.0.13",
"version": "0.0.14",
"author": "hodlbod",
"license": "MIT",
"description": "Utilities for building dynamic nostr feeds.",
@@ -31,6 +31,6 @@
"typescript": "~5.1.6"
},
"dependencies": {
"@welshman/util": "0.0.24"
"@welshman/util": "0.0.25"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@welshman/lib",
"version": "0.0.13",
"version": "0.0.14",
"author": "hodlbod",
"license": "MIT",
"description": "A collection of utilities.",
+38
View File
@@ -355,6 +355,20 @@ export const once = (f: (...args: any) => void) => {
}
}
export const memoize = <T>(f: (...args: any[]) => T) => {
let prevArgs: any[]
let result: T
return (...args: any[]) => {
if (!equals(prevArgs, args)) {
prevArgs = args
result = f(...args)
}
return result
}
}
export const batch = <T>(t: number, f: (xs: T[]) => void) => {
const xs: T[] = []
const cb = throttle(t, () => xs.length > 0 && f(xs.splice(0)))
@@ -365,6 +379,30 @@ export const batch = <T>(t: number, f: (xs: T[]) => void) => {
}
}
export const batcher = <T, U>(t: number, execute: (request: T[]) => U[] | Promise<U[]>) => {
const queue: {request: T, resolve: (x: U) => void}[] = []
const _execute = async () => {
const items = queue.splice(0)
const results = await execute(items.map(item => item.request))
if (results.length !== items.length) {
throw new Error("Execute must return a promise for each request")
}
results.forEach(async (r, i) => items[i].resolve(await r))
}
return (request: T): Promise<U> =>
new Promise(resolve => {
if (queue.length === 0) {
setTimeout(_execute, t)
}
queue.push({request, resolve})
})
}
export const addToKey = <T>(m: Record<string, Set<T>>, k: string, v: T) => {
const s = m[k] || new Set<T>()
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@welshman/net",
"version": "0.0.17",
"version": "0.0.18",
"author": "hodlbod",
"license": "MIT",
"description": "Utilities for connecting with nostr relays.",
@@ -31,8 +31,8 @@
"typescript": "~5.1.6"
},
"dependencies": {
"@welshman/lib": "0.0.13",
"@welshman/util": "0.0.24",
"@welshman/lib": "0.0.14",
"@welshman/util": "0.0.25",
"isomorphic-ws": "^5.0.0",
"ws": "^8.16.0"
}
+3 -2
View File
@@ -1,9 +1,10 @@
import {Emitter} from '@welshman/lib'
import type {TrustedEvent} from '@welshman/util'
import {Relay, LOCAL_RELAY_URL} from '@welshman/util'
import type {Message} from '../Socket'
export class Local extends Emitter {
constructor(readonly relay: Relay) {
export class Local<T extends TrustedEvent> extends Emitter {
constructor(readonly relay: Relay<T>) {
super()
relay.on('*', this.onMessage)
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@welshman/signer",
"version": "0.0.1",
"version": "0.0.2",
"author": "hodlbod",
"license": "MIT",
"description": "A nostr signer implemenation supporting several login methods.",
@@ -31,8 +31,9 @@
"typescript": "~5.1.6"
},
"dependencies": {
"@welshman/lib": "^0.0.13",
"@welshman/net": "^0.0.17",
"@welshman/util": "^0.0.24"
"@welshman/lib": "0.0.14",
"@welshman/net": "0.0.18",
"@welshman/util": "0.0.25",
"nostr-tools": "^2.7.2"
}
}
+11 -11
View File
@@ -55,7 +55,7 @@ export class Nip46Broker extends Emitter {
this.#sub = this.subscribe()
}
subscribe() {
subscribe = () => {
const sub = subscribe({
relays: this.handler.relays,
filters: [
@@ -91,7 +91,7 @@ export class Nip46Broker extends Emitter {
return sub
}
async request(method: string, params: string[], admin = false) {
request = async (method: string, params: string[], admin = false) => {
// nsecbunker has a race condition
await this.#ready
@@ -105,7 +105,7 @@ export class Nip46Broker extends Emitter {
publish({event, relays: this.handler.relays})
this.once(`auth-${id}`, res => {
window.open(res.result, "Coracle", "width=600,height=800,popup=yes")
window.open(res.error, "Coracle", "width=600,height=800,popup=yes")
})
return new Promise<string>((resolve, reject) => {
@@ -119,7 +119,7 @@ export class Nip46Broker extends Emitter {
})
}
createAccount(username: string) {
createAccount = (username: string) => {
if (!this.handler.domain) {
throw new Error("Unable to create an account without a handler domain")
}
@@ -127,7 +127,7 @@ export class Nip46Broker extends Emitter {
return this.request("create_account", [username, this.handler.domain, "", Perms], true)
}
async connect(token = "") {
connect = async (token = "") => {
if (!this.#connectResult) {
const params = [this.pubkey, token, Perms]
@@ -137,27 +137,27 @@ export class Nip46Broker extends Emitter {
return this.#connectResult === "ack"
}
async signEvent(event: EventTemplate) {
signEvent = async (event: EventTemplate) => {
return JSON.parse(await this.request("sign_event", [JSON.stringify(event)]) as string)
}
nip04Encrypt(pk: string, message: string) {
nip04Encrypt = (pk: string, message: string) => {
return this.request("nip04_encrypt", [pk, message])
}
nip04Decrypt(pk: string, message: string) {
nip04Decrypt = (pk: string, message: string) => {
return this.request("nip04_decrypt", [pk, message])
}
nip44Encrypt(pk: string, message: string) {
nip44Encrypt = (pk: string, message: string) => {
return this.request("nip44_encrypt", [pk, message])
}
nip44Decrypt(pk: string, message: string) {
nip44Decrypt = (pk: string, message: string) => {
return this.request("nip44_decrypt", [pk, message])
}
teardown() {
teardown = () => {
this.#closed = true
this.#sub?.close()
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@welshman/store",
"version": "0.0.1",
"version": "0.0.2",
"author": "hodlbod",
"license": "MIT",
"description": "A collection of utilities based on svelte/store for use with welshman",
+14 -14
View File
@@ -68,8 +68,8 @@ export function withGetter<T>(store: Readable<T> | Writable<T>) {
export const throttled = <T>(delay: number, store: Readable<T>) =>
custom(set => store.subscribe(throttle(delay, set)))
export const createEventStore = (repository: Repository) => {
let subs: Sub<TrustedEvent[]>[] = []
export const createEventStore = <E extends TrustedEvent>(repository: Repository<E>) => {
let subs: Sub<E[]>[] = []
const onUpdate = throttle(300, () => {
const $events = repository.dump()
@@ -81,8 +81,8 @@ export const createEventStore = (repository: Repository) => {
return {
get: () => repository.dump(),
set: (events: TrustedEvent[]) => repository.load(events),
subscribe: (f: Sub<TrustedEvent[]>) => {
set: (events: E[]) => repository.load(events),
subscribe: (f: Sub<E[]>) => {
f(repository.dump())
subs.push(f)
@@ -102,7 +102,7 @@ export const createEventStore = (repository: Repository) => {
}
}
export const deriveEventsMapped = <T>({
export const deriveEventsMapped = <T, E extends TrustedEvent>({
filters,
repository,
eventToItem,
@@ -110,9 +110,9 @@ export const deriveEventsMapped = <T>({
includeDeleted = false,
}: {
filters: Filter[]
repository: Repository,
eventToItem: (event: TrustedEvent) => T
itemToEvent: (item: T) => TrustedEvent
repository: Repository<E>,
eventToItem: (event: E) => T
itemToEvent: (item: T) => E
includeDeleted?: boolean
}) =>
custom<T[]>(setter => {
@@ -120,7 +120,7 @@ export const deriveEventsMapped = <T>({
setter(data)
const onUpdate = batch(300, (updates: {added: TrustedEvent[]; removed: Set<string>}[]) => {
const onUpdate = batch(300, (updates: {added: E[]; removed: Set<string>}[]) => {
const removed = new Set()
const added = new Map()
@@ -150,7 +150,7 @@ export const deriveEventsMapped = <T>({
if (!includeDeleted && removed.size > 0) {
const [deleted, ok] = partition(
(item: T) => getIdAndAddress(itemToEvent(item)).some(id => removed.has(id)),
(item: T) => getIdAndAddress(itemToEvent(item)).some((id: string) => removed.has(id)),
data,
)
@@ -170,15 +170,15 @@ export const deriveEventsMapped = <T>({
return () => repository.off("update", onUpdate)
})
export const deriveEvents = (repository: Repository, opts: {filters: Filter[]; includeDeleted?: boolean}) =>
deriveEventsMapped<TrustedEvent>({
export const deriveEvents = <E extends TrustedEvent>(repository: Repository<E>, opts: {filters: Filter[]; includeDeleted?: boolean}) =>
deriveEventsMapped<E, E>({
...opts,
repository,
eventToItem: identity,
itemToEvent: identity,
})
export const deriveEvent = (repository: Repository, idOrAddress: string) =>
export const deriveEvent = <E extends TrustedEvent>(repository: Repository<E>, idOrAddress: string) =>
derived(
deriveEvents(repository, {
filters: getIdFilters([idOrAddress]),
@@ -187,7 +187,7 @@ export const deriveEvent = (repository: Repository, idOrAddress: string) =>
first
)
export const deriveIsDeletedByAddress = (repository: Repository, event: TrustedEvent) =>
export const deriveIsDeletedByAddress = <E extends TrustedEvent>(repository: Repository<E>, event: E) =>
custom<boolean>(setter => {
setter(repository.isDeletedByAddress(event))
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@welshman/util",
"version": "0.0.24",
"version": "0.0.25",
"author": "hodlbod",
"license": "MIT",
"description": "A collection of nostr-related utilities.",
@@ -31,7 +31,7 @@
"typescript": "~5.1.6"
},
"dependencies": {
"@welshman/lib": "0.0.13",
"@welshman/lib": "0.0.14",
"nostr-tools": "^2.3.2"
}
}
+4 -4
View File
@@ -48,22 +48,22 @@ export const normalizeRelayUrl = (url: string, {allowInsecure = false}: Normaliz
return prefix + url
}
export class Relay extends Emitter {
export class Relay<T extends TrustedEvent> extends Emitter {
subs = new Map<string, Filter[]>()
constructor(readonly repository: Repository) {
constructor(readonly repository: Repository<T>) {
super()
}
send(type: string, ...message: any[]) {
switch(type) {
case 'EVENT': return this.handleEVENT(message as [TrustedEvent])
case 'EVENT': return this.handleEVENT(message as [T])
case 'CLOSE': return this.handleCLOSE(message as [string])
case 'REQ': return this.handleREQ(message as [string, ...Filter[]])
}
}
handleEVENT([event]: [TrustedEvent]) {
handleEVENT([event]: [T]) {
this.repository.publish(event)
// Callers generally expect async relays
+20 -20
View File
@@ -10,13 +10,13 @@ export const DAY = 86400
const getDay = (ts: number) => Math.floor(ts / DAY)
export class Repository extends Emitter {
eventsById = new Map<string, TrustedEvent>()
eventsByWrap = new Map<string, TrustedEvent>()
eventsByAddress = new Map<string, TrustedEvent>()
eventsByTag = new Map<string, TrustedEvent[]>()
eventsByDay = new Map<number, TrustedEvent[]>()
eventsByAuthor = new Map<string, TrustedEvent[]>()
export class Repository<T extends TrustedEvent> extends Emitter {
eventsById = new Map<string, T>()
eventsByWrap = new Map<string, T>()
eventsByAddress = new Map<string, T>()
eventsByTag = new Map<string, T[]>()
eventsByDay = new Map<number, T[]>()
eventsByAuthor = new Map<string, T[]>()
deletes = new Map<string, number>()
// Dump/load/clear
@@ -25,7 +25,7 @@ export class Repository extends Emitter {
return Array.from(this.eventsById.values())
}
load = async (events: TrustedEvent[], chunkSize = 1000) => {
load = async (events: T[], chunkSize = 1000) => {
this.clear()
const added = []
@@ -69,7 +69,7 @@ export class Repository extends Emitter {
: this.eventsById.get(idOrAddress)
}
hasEvent = (event: TrustedEvent) => {
hasEvent = (event: T) => {
const duplicate = (
this.eventsById.get(event.id) ||
this.eventsByAddress.get(getAddress(event))
@@ -79,12 +79,12 @@ export class Repository extends Emitter {
}
query = (filters: Filter[], {includeDeleted = false} = {}) => {
const result: TrustedEvent[][] = []
const result: T[][] = []
for (let filter of filters) {
let events: TrustedEvent[] = Array.from(this.eventsById.values())
let events: T[] = Array.from(this.eventsById.values())
if (filter.ids) {
events = filter.ids!.map(id => this.eventsById.get(id)).filter(identity) as TrustedEvent[]
events = filter.ids!.map(id => this.eventsById.get(id)).filter(identity) as T[]
filter = omit(['ids'], filter)
} else if (filter.authors) {
events = uniq(filter.authors!.flatMap(pubkey => this.eventsByAuthor.get(pubkey) || []))
@@ -112,8 +112,8 @@ export class Repository extends Emitter {
}
}
const chunk: TrustedEvent[] = []
for (const event of sortBy((e: TrustedEvent) => -e.created_at, events)) {
const chunk: T[] = []
for (const event of sortBy((e: T) => -e.created_at, events)) {
if (filter.limit && chunk.length >= filter.limit) {
break
}
@@ -133,7 +133,7 @@ export class Repository extends Emitter {
return uniq(flatten(result))
}
publish = (event: TrustedEvent, {shouldNotify = true} = {}): boolean => {
publish = (event: T, {shouldNotify = true} = {}): boolean => {
if (!isTrustedEvent(event)) {
throw new Error("Invalid event published to Repository", event)
}
@@ -203,19 +203,19 @@ export class Repository extends Emitter {
return true
}
isDeletedByAddress = (event: TrustedEvent) => (this.deletes.get(getAddress(event)) || 0) > event.created_at
isDeletedByAddress = (event: T) => (this.deletes.get(getAddress(event)) || 0) > event.created_at
isDeletedById = (event: TrustedEvent) => (this.deletes.get(event.id) || 0) > event.created_at
isDeletedById = (event: T) => (this.deletes.get(event.id) || 0) > event.created_at
isDeleted = (event: TrustedEvent) => this.isDeletedByAddress(event) || this.isDeletedById(event)
isDeleted = (event: T) => this.isDeletedByAddress(event) || this.isDeletedById(event)
// Utilities
_updateIndex<K>(m: Map<K, TrustedEvent[]>, k: K, e: TrustedEvent, duplicate?: TrustedEvent) {
_updateIndex<K>(m: Map<K, T[]>, k: K, e: T, duplicate?: T) {
let a = m.get(k) || []
if (duplicate) {
a = a.filter((x: TrustedEvent) => x !== duplicate)
a = a.filter((x: T) => x !== duplicate)
}
a.push(e)