|
|
@@ -32,6 +32,12 @@ const keyFor = (directory: string, id: string) => `${directory}\n${id}`
|
|
|
|
|
|
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
|
|
|
|
|
|
+function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
|
|
|
+ const map = new Map(a.map((item) => [item.id, item] as const))
|
|
|
+ for (const item of b) map.set(item.id, item)
|
|
|
+ return [...map.values()].sort((x, y) => cmp(x.id, y.id))
|
|
|
+}
|
|
|
+
|
|
|
type OptimisticStore = {
|
|
|
message: Record<string, Message[] | undefined>
|
|
|
part: Record<string, Part[] | undefined>
|
|
|
@@ -119,6 +125,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
const seen = new Map<string, Set<string>>()
|
|
|
const [meta, setMeta] = createStore({
|
|
|
limit: {} as Record<string, number>,
|
|
|
+ cursor: {} as Record<string, string | undefined>,
|
|
|
complete: {} as Record<string, boolean>,
|
|
|
loading: {} as Record<string, boolean>,
|
|
|
})
|
|
|
@@ -157,6 +164,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
for (const sessionID of sessionIDs) {
|
|
|
const key = keyFor(directory, sessionID)
|
|
|
delete draft.limit[key]
|
|
|
+ delete draft.cursor[key]
|
|
|
delete draft.complete[key]
|
|
|
delete draft.loading[key]
|
|
|
}
|
|
|
@@ -187,17 +195,24 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
evict(directory, setStore, stale)
|
|
|
}
|
|
|
|
|
|
- const fetchMessages = async (input: { client: typeof sdk.client; sessionID: string; limit: number }) => {
|
|
|
+ const fetchMessages = async (input: {
|
|
|
+ client: typeof sdk.client
|
|
|
+ sessionID: string
|
|
|
+ limit: number
|
|
|
+ before?: string
|
|
|
+ }) => {
|
|
|
const messages = await retry(() =>
|
|
|
- input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }),
|
|
|
+ input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }),
|
|
|
)
|
|
|
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
|
|
const session = items.map((x) => x.info).sort((a, b) => cmp(a.id, b.id))
|
|
|
const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) }))
|
|
|
+ const cursor = messages.response.headers.get("x-next-cursor") ?? undefined
|
|
|
return {
|
|
|
session,
|
|
|
part,
|
|
|
- complete: session.length < input.limit,
|
|
|
+ cursor,
|
|
|
+ complete: !cursor,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -209,6 +224,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
setStore: Setter
|
|
|
sessionID: string
|
|
|
limit: number
|
|
|
+ before?: string
|
|
|
+ mode?: "replace" | "prepend"
|
|
|
}) => {
|
|
|
const key = keyFor(input.directory, input.sessionID)
|
|
|
if (meta.loading[key]) return
|
|
|
@@ -217,17 +234,22 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
await fetchMessages(input)
|
|
|
.then((next) => {
|
|
|
if (!tracked(input.directory, input.sessionID)) return
|
|
|
+ const [store] = globalSync.child(input.directory, { bootstrap: false })
|
|
|
+ const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
|
|
|
+ const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
|
|
|
batch(() => {
|
|
|
- input.setStore("message", input.sessionID, reconcile(next.session, { key: "id" }))
|
|
|
+ input.setStore("message", input.sessionID, reconcile(message, { key: "id" }))
|
|
|
for (const p of next.part) {
|
|
|
input.setStore("part", p.id, p.part)
|
|
|
}
|
|
|
- setMeta("limit", key, input.limit)
|
|
|
+ setMeta("limit", key, message.length)
|
|
|
+ setMeta("cursor", key, next.cursor)
|
|
|
setMeta("complete", key, next.complete)
|
|
|
setSessionPrefetch({
|
|
|
directory: input.directory,
|
|
|
sessionID: input.sessionID,
|
|
|
- limit: input.limit,
|
|
|
+ limit: message.length,
|
|
|
+ cursor: next.cursor,
|
|
|
complete: next.complete,
|
|
|
})
|
|
|
})
|
|
|
@@ -312,6 +334,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
|
|
batch(() => {
|
|
|
setMeta("limit", key, seeded.limit)
|
|
|
+ setMeta("cursor", key, seeded.cursor)
|
|
|
setMeta("complete", key, seeded.complete)
|
|
|
setMeta("loading", key, false)
|
|
|
})
|
|
|
@@ -325,6 +348,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
if (seeded && store.message[sessionID] !== undefined && meta.limit[key] === undefined) {
|
|
|
batch(() => {
|
|
|
setMeta("limit", key, seeded.limit)
|
|
|
+ setMeta("cursor", key, seeded.cursor)
|
|
|
setMeta("complete", key, seeded.complete)
|
|
|
setMeta("loading", key, false)
|
|
|
})
|
|
|
@@ -420,7 +444,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
if (store.message[sessionID] === undefined) return false
|
|
|
if (meta.limit[key] === undefined) return false
|
|
|
if (meta.complete[key]) return false
|
|
|
- return true
|
|
|
+ return !!meta.cursor[key]
|
|
|
},
|
|
|
loading(sessionID: string) {
|
|
|
const key = keyFor(sdk.directory, sessionID)
|
|
|
@@ -435,14 +459,17 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
const step = count ?? messagePageSize
|
|
|
if (meta.loading[key]) return
|
|
|
if (meta.complete[key]) return
|
|
|
+ const before = meta.cursor[key]
|
|
|
+ if (!before) return
|
|
|
|
|
|
- const currentLimit = meta.limit[key] ?? messagePageSize
|
|
|
await loadMessages({
|
|
|
directory,
|
|
|
client,
|
|
|
setStore,
|
|
|
sessionID,
|
|
|
- limit: currentLimit + step,
|
|
|
+ limit: step,
|
|
|
+ before,
|
|
|
+ mode: "prepend",
|
|
|
})
|
|
|
},
|
|
|
},
|