|
|
@@ -54,6 +54,67 @@ type OptimisticRemoveInput = {
|
|
|
messageID: string
|
|
|
}
|
|
|
|
|
|
+type OptimisticItem = {
|
|
|
+ message: Message
|
|
|
+ parts: Part[]
|
|
|
+}
|
|
|
+
|
|
|
+type MessagePage = {
|
|
|
+ session: Message[]
|
|
|
+ part: { id: string; part: Part[] }[]
|
|
|
+ cursor?: string
|
|
|
+ complete: boolean
|
|
|
+}
|
|
|
+
|
|
|
+const hasParts = (parts: Part[] | undefined, want: Part[]) => {
|
|
|
+ if (!parts) return want.length === 0
|
|
|
+ return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found)
|
|
|
+}
|
|
|
+
|
|
|
+const mergeParts = (parts: Part[] | undefined, want: Part[]) => {
|
|
|
+ if (!parts) return sortParts(want)
|
|
|
+ const next = [...parts]
|
|
|
+ let changed = false
|
|
|
+ for (const part of want) {
|
|
|
+ const result = Binary.search(next, part.id, (item) => item.id)
|
|
|
+ if (result.found) continue
|
|
|
+ next.splice(result.index, 0, part)
|
|
|
+ changed = true
|
|
|
+ }
|
|
|
+ if (!changed) return parts
|
|
|
+ return next
|
|
|
+}
|
|
|
+
|
|
|
+export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) {
|
|
|
+ if (items.length === 0) return { ...page, confirmed: [] as string[] }
|
|
|
+
|
|
|
+ const session = [...page.session]
|
|
|
+ const part = new Map(page.part.map((item) => [item.id, sortParts(item.part)]))
|
|
|
+ const confirmed: string[] = []
|
|
|
+
|
|
|
+ for (const item of items) {
|
|
|
+ const result = Binary.search(session, item.message.id, (message) => message.id)
|
|
|
+ const found = result.found
|
|
|
+ if (!found) session.splice(result.index, 0, item.message)
|
|
|
+
|
|
|
+ const current = part.get(item.message.id)
|
|
|
+ if (found && hasParts(current, item.parts)) {
|
|
|
+ confirmed.push(item.message.id)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ part.set(item.message.id, mergeParts(current, item.parts))
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ cursor: page.cursor,
|
|
|
+ complete: page.complete,
|
|
|
+ session,
|
|
|
+ part: [...part.entries()].sort((a, b) => cmp(a[0], b[0])).map(([id, part]) => ({ id, part })),
|
|
|
+ confirmed,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddInput) {
|
|
|
const messages = draft.message[input.sessionID]
|
|
|
if (messages) {
|
|
|
@@ -121,6 +182,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
const inflight = new Map<string, Promise<void>>()
|
|
|
const inflightDiff = new Map<string, Promise<void>>()
|
|
|
const inflightTodo = new Map<string, Promise<void>>()
|
|
|
+ const optimistic = new Map<string, Map<string, OptimisticItem>>()
|
|
|
const maxDirs = 30
|
|
|
const seen = new Map<string, Set<string>>()
|
|
|
const [meta, setMeta] = createStore({
|
|
|
@@ -137,6 +199,33 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
return undefined
|
|
|
}
|
|
|
|
|
|
+ const setOptimistic = (directory: string, sessionID: string, item: OptimisticItem) => {
|
|
|
+ const key = keyFor(directory, sessionID)
|
|
|
+ const list = optimistic.get(key)
|
|
|
+ if (list) {
|
|
|
+ list.set(item.message.id, { message: item.message, parts: sortParts(item.parts) })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ optimistic.set(key, new Map([[item.message.id, { message: item.message, parts: sortParts(item.parts) }]]))
|
|
|
+ }
|
|
|
+
|
|
|
+ const clearOptimistic = (directory: string, sessionID: string, messageID?: string) => {
|
|
|
+ const key = keyFor(directory, sessionID)
|
|
|
+ if (!messageID) {
|
|
|
+ optimistic.delete(key)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const list = optimistic.get(key)
|
|
|
+ if (!list) return
|
|
|
+ list.delete(messageID)
|
|
|
+ if (list.size === 0) optimistic.delete(key)
|
|
|
+ }
|
|
|
+
|
|
|
+ const getOptimistic = (directory: string, sessionID: string) => [
|
|
|
+ ...(optimistic.get(keyFor(directory, sessionID))?.values() ?? []),
|
|
|
+ ]
|
|
|
+
|
|
|
const seenFor = (directory: string) => {
|
|
|
const existing = seen.get(directory)
|
|
|
if (existing) {
|
|
|
@@ -159,6 +248,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
|
|
|
const clearMeta = (directory: string, sessionIDs: string[]) => {
|
|
|
if (sessionIDs.length === 0) return
|
|
|
+ for (const sessionID of sessionIDs) {
|
|
|
+ clearOptimistic(directory, sessionID)
|
|
|
+ }
|
|
|
setMeta(
|
|
|
produce((draft) => {
|
|
|
for (const sessionID of sessionIDs) {
|
|
|
@@ -232,8 +324,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
|
|
|
setMeta("loading", key, true)
|
|
|
await fetchMessages(input)
|
|
|
- .then((next) => {
|
|
|
+ .then((page) => {
|
|
|
if (!tracked(input.directory, input.sessionID)) return
|
|
|
+ const next = mergeOptimisticPage(page, getOptimistic(input.directory, input.sessionID))
|
|
|
+ for (const messageID of next.confirmed) {
|
|
|
+ clearOptimistic(input.directory, input.sessionID, messageID)
|
|
|
+ }
|
|
|
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
|
|
|
@@ -290,11 +386,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
get: getSession,
|
|
|
optimistic: {
|
|
|
add(input: { directory?: string; sessionID: string; message: Message; parts: Part[] }) {
|
|
|
+ const directory = input.directory ?? sdk.directory
|
|
|
const [, setStore] = target(input.directory)
|
|
|
+ setOptimistic(directory, input.sessionID, { message: input.message, parts: input.parts })
|
|
|
setOptimisticAdd(setStore as (...args: unknown[]) => void, input)
|
|
|
},
|
|
|
remove(input: { directory?: string; sessionID: string; messageID: string }) {
|
|
|
+ const directory = input.directory ?? sdk.directory
|
|
|
const [, setStore] = target(input.directory)
|
|
|
+ clearOptimistic(directory, input.sessionID, input.messageID)
|
|
|
setOptimisticRemove(setStore as (...args: unknown[]) => void, input)
|
|
|
},
|
|
|
},
|
|
|
@@ -316,6 +416,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
variant: input.variant,
|
|
|
}
|
|
|
const [, setStore] = target()
|
|
|
+ setOptimistic(sdk.directory, input.sessionID, { message, parts: input.parts })
|
|
|
setOptimisticAdd(setStore as (...args: unknown[]) => void, {
|
|
|
sessionID: input.sessionID,
|
|
|
message,
|