Working on suspending/resuming

This commit is contained in:
Jonathan Staab 2022-11-30 07:52:14 -08:00
parent 3c60add04f
commit 159306be67
7 changed files with 94 additions and 137 deletions

View File

@ -35,6 +35,7 @@
let menuIcon
let scrollY
let suspendedSubs = []
export let url = ""
@ -47,6 +48,7 @@
})
modal.subscribe($modal => {
// Keep scroll position on body, but don't allow scrolling
if ($modal) {
scrollY = window.scrollY

View File

@ -1,5 +1,5 @@
<script>
import {onMount} from 'svelte'
import {onMount, onDestroy} from 'svelte'
import {writable} from 'svelte/store'
import {find, propEq} from 'ramda'
import {notesLoader, notesListener} from "src/state/app"
@ -8,13 +8,20 @@
export let note
const notes = writable([])
let onScroll
const notes = writable([note])
let loader
let listener
onMount(async () => {
const loader = await notesLoader(notes, {ids: [note.id]}, {isInModal: true})
const listener = await notesListener(notes, [,
{'#e': [note.id]},
const opts = {isInModal: true}
if (note.created_at) {
opts.since = note.created_at
}
loader = await notesLoader(notes, {ids: [note.id]}, opts)
listener = await notesListener(notes, [
{kinds: [1, 5, 7], '#e': [note.id]},
// We can't target reaction deletes by e tag, so get them
// all so we can support toggling like/flags for our user
{kinds: [5], authors: $user ? [$user.pubkey] : []}
@ -23,13 +30,16 @@
notes.subscribe($notes => {
note = find(propEq('id', note.id), $notes) || note
})
})
onScroll = loader.onScroll
return loader.unsub
onDestroy(() => {
loader?.unsub()
listener?.unsub()
})
</script>
<svelte:window on:scroll={loader?.onScroll} />
{#if note.pubkey}
<Note showEntire note={note} />
{#each note.replies as r (r.id)}

View File

@ -59,13 +59,9 @@
return top + height < bodyRect.height
}
return channels.watcher.sub({
filter: {
limit: 100,
kinds: [42, 43, 44],
'#e': [room],
},
cb: e => {
return await channels.listener.sub(
{limit: 100, kinds: [42, 43, 44], '#e': [room]},
e => {
switcherFn(e.kind, {
42: () => {
messages = messages.concat(e)
@ -86,7 +82,7 @@
44: () => null,
})
},
})
)
})
const edit = () => {

View File

@ -1,33 +1,43 @@
<script>
import {onMount} from 'svelte'
import {onMount, onDestroy} from 'svelte'
import {writable} from 'svelte/store'
import {navigate} from "svelte-routing"
import Anchor from "src/partials/Anchor.svelte"
import Note from "src/partials/Note.svelte"
import {relays} from "src/state/nostr"
import {notesLoader, notesListener} from "src/state/app"
import {notesLoader, notesListener, modal} from "src/state/app"
const notes = writable([])
let onScroll
let loader
let listener
const createNote = () => {
navigate("/notes/new")
}
onMount(async () => {
const loader = await notesLoader(notes, {kinds: [1]}, {showParents: true})
const listener = await notesListener(notes, {kinds: [1]})
loader = await notesLoader(notes, {kinds: [1]}, {showParents: true})
listener = await notesListener(notes, {kinds: [1, 5, 7]})
onScroll = loader.onScroll
// When a modal opens, suspend our subscriptions
modal.subscribe(async $modal => {
if ($modal) {
loader.cursor.stop()
listener.unsub()
} else {
loader.cursor.start()
listener = await notesListener(notes, {kinds: [1, 5, 7]})
}
})
})
return () => {
loader.unsub()
listener.unsub()
}
onDestroy(() => {
loader?.unsub()
listener?.unsub()
})
</script>
<svelte:window on:scroll={onScroll} />
<svelte:window on:scroll={loader?.onScroll} />
<ul class="py-8 flex flex-col gap-2 max-w-xl m-auto">
{#each (notes ? $notes : []) as n (n.id)}

View File

@ -1,5 +1,5 @@
<script>
import {onMount} from 'svelte'
import {onMount, onDestroy} from 'svelte'
import {writable} from 'svelte/store'
import {fly} from 'svelte/transition'
import Note from "src/partials/Note.svelte"
@ -8,26 +8,26 @@
export let pubkey
let user
const notes = writable([])
let onScroll
let user
let loader
let listener
$: user = $accounts[pubkey]
onMount(async () => {
const filter = {kinds: [1], authors: [pubkey]}
const loader = await notesLoader(notes, filter, {showParents: true})
const listener = await notesListener(notes, filter)
loader = await notesLoader(notes, {kinds: [1], authors: [pubkey]}, {showParents: true})
listener = await notesListener(notes, {kinds: [1, 5, 7], authors: [pubkey]})
})
onScroll = loader.onScroll
return () => {
loader.unsub()
listener.unsub()
}
onDestroy(() => {
loader.unsub()
listener.unsub()
})
</script>
<svelte:window on:scroll={loader?.onScroll} />
{#if user}
<div class="max-w-2xl m-auto flex flex-col gap-4 py-8 px-4">
<div class="flex flex-col gap-4" in:fly={{y: 20}}>

View File

@ -1,11 +1,11 @@
import {when, prop, identity, whereEq, reverse, uniq, sortBy, uniqBy, find, last, pluck, groupBy} from 'ramda'
import {when, assoc, prop, identity, whereEq, reverse, uniq, sortBy, uniqBy, find, last, pluck, groupBy} from 'ramda'
import {debounce} from 'throttle-debounce'
import {writable, get} from 'svelte/store'
import {navigate} from "svelte-routing"
import {switcherFn} from 'hurdak/lib/hurdak'
import {switcherFn, ensurePlural} from 'hurdak/lib/hurdak'
import {getLocalJson, setLocalJson, now, timedelta, sleep} from "src/util/misc"
import {user} from 'src/state/user'
import {_channels, filterMatches, Cursor, channels, relays, findReplyTo} from 'src/state/nostr'
import {filterMatches, Cursor, channels, relays, findReplyTo} from 'src/state/nostr'
export const modal = writable(null)
@ -68,7 +68,7 @@ export const annotateNotesChunk = async (chunk, {showParents = false} = {}) => {
if (showParents && parentIds.length) {
// Find parents of replies to provide context
const parents = await _channels.getter.all({
const parents = await channels.getter.all({
kinds: [1],
ids: parentIds,
})
@ -82,12 +82,12 @@ export const annotateNotesChunk = async (chunk, {showParents = false} = {}) => {
return chunk
}
const replies = await _channels.getter.all({
const replies = await channels.getter.all({
kinds: [1],
'#e': pluck('id', chunk),
})
const reactions = await _channels.getter.all({
const reactions = await channels.getter.all({
kinds: [7],
'#e': pluck('id', chunk.concat(replies)),
})
@ -123,6 +123,7 @@ export const notesLoader = async (
showParents = false,
delta = timedelta(1, 'hours'),
isInModal = false,
since = 1633046400, // nostr epoch
} = {}
) => {
const cursor = new Cursor(filter, delta)
@ -142,8 +143,8 @@ export const notesLoader = async (
break
}
// If we've gone back to the network's inception we're done
if (cursor.since <= 1633046400) {
// Stop if we've gone back far enough
if (cursor.since <= since) {
break
}
@ -165,6 +166,7 @@ export const notesLoader = async (
onScroll()
return {
cursor,
onScroll,
unsub: () => {
cursor.stop()
@ -194,8 +196,8 @@ export const notesListener = async (notes, filter) => {
reactions: n.reactions.filter(e => !ids.includes(e.id)),
}))
return await _channels.listener.sub(
{kinds: [1, 5, 7], since: now()},
return await channels.listener.sub(
ensurePlural(filter).map(assoc('since', now())),
e => switcherFn(e.kind, {
1: async () => {
const id = findReplyTo(e)
@ -216,7 +218,6 @@ export const notesListener = async (notes, filter) => {
notes.update($notes => deleteNotes($notes, ids))
},
7: () => {
console.log(e)
const id = findReplyTo(e)
updateNote(id, n => ({...n, reactions: n.reactions.concat(e)}))

View File

@ -1,5 +1,4 @@
import {writable} from 'svelte/store'
import {debounce} from 'throttle-debounce'
import {relayPool, getPublicKey} from 'nostr-tools'
import {last, intersection, uniqBy, prop} from 'ramda'
import {first, noop, ensurePlural} from 'hurdak/lib/hurdak'
@ -51,25 +50,34 @@ export class Channel {
this.name = name
this.p = Promise.resolve()
}
sub(filter, cb, onEose = noop) {
this.p = this.p.then(() => {
const sub = nostr.sub({filter, cb}, this.name, onEose)
async sub(filter, cb, onEose = noop) {
// Make sure callers have to wait for the previous sub to be done
// before they can get a new one.
await this.p
return () => sub.unsub()
let resolve
const sub = nostr.sub({filter, cb}, this.name, r => {
onEose(r)
resolve()
})
return this.p
this.p = new Promise(r => {
resolve = r
})
return sub
}
all(filter) {
/* eslint no-async-promise-executor: 0 */
return new Promise(async resolve => {
const result = []
const unsub = await this.sub(
const sub = await this.sub(
filter,
e => result.push(e),
r => {
unsub()
sub.unsub()
resolve(result)
},
@ -78,7 +86,7 @@ export class Channel {
}
}
export const _channels = {
export const channels = {
listener: new Channel('listener'),
getter: new Channel('getter'),
}
@ -92,13 +100,13 @@ export class Cursor {
this.delta = delta
this.since = now() - delta
this.until = now()
this.unsub = null
this.sub = null
this.q = []
this.p = Promise.resolve()
}
async start() {
if (!this.unsub) {
this.unsub = await _channels.getter.sub(
if (!this.sub) {
this.sub = await channels.getter.sub(
this.filter.map(f => ({...f, since: this.since, until: this.until})),
e => this.onEvent(e),
r => this.onEose(r)
@ -106,9 +114,9 @@ export class Cursor {
}
}
stop() {
if (this.unsub) {
this.unsub()
this.unsub = null
if (this.sub) {
this.sub.unsub()
this.sub = null
}
}
restart() {
@ -133,83 +141,13 @@ export class Cursor {
while (true) {
await new Promise(requestAnimationFrame)
if (!this.unsub) {
if (!this.sub) {
return this.q.splice(0)
}
}
}
}
// Track who is subscribing, so we don't go over our limit
const channel = name => {
let active = false
let promise = Promise.resolve('init')
const _chan = {
sub: params => {
if (active) {
console.error(`Channel ${name} is already active.`)
}
active = true
const sub = nostr.sub(params)
return () => {
active = false
sub.unsub()
}
},
all: filter => {
// Wait for any other subscriptions to finish
promise = promise.then(() => {
return new Promise(resolve => {
// Collect results
let result = []
// As long as events are coming in, don't resolve. When
// events are no longer streaming, resolve and close the subscription
const done = debounce(300, () => {
unsub()
resolve(result)
})
// Create our usbscription, every time we get an event, attempt to complete
const unsub = _chan.sub({
filter,
cb: e => {
result.push(e)
done()
},
})
// If our filter doesn't match anything, be sure to resolve the promise
setTimeout(done, 1000)
})
})
return promise
},
first: async filter => {
return first(await channels.getter.all({...filter, limit: 1}))
},
last: async filter => {
return last(await channels.getter.all({...filter}))
},
}
return _chan
}
export const channels = {
watcher: channel('main'),
getter: channel('getter'),
}
// Augment nostr with some extra methods
nostr.login = privkey => {