perf(app): don't remount directory layout

This commit is contained in:
Adam
2026-01-22 21:01:09 -06:00
parent c4d223eb99
commit 4afb46f571
11 changed files with 257 additions and 114 deletions

View File

@@ -32,8 +32,8 @@ export function DialogSelectFile() {
const dialog = useDialog() const dialog = useDialog()
const params = useParams() const params = useParams()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey())) const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey())) const view = createMemo(() => layout.view(sessionKey))
const state = { cleanup: undefined as (() => void) | void, committed: false } const state = { cleanup: undefined as (() => void) | void, committed: false }
const [grouped, setGrouped] = createSignal(false) const [grouped, setGrouped] = createSignal(false)
const common = [ const common = [

View File

@@ -167,8 +167,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
} }
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey())) const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey())) const view = createMemo(() => layout.view(sessionKey))
const recent = createMemo(() => { const recent = createMemo(() => {
const all = tabs().all() const all = tabs().all()

View File

@@ -21,8 +21,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const variant = createMemo(() => props.variant ?? "button") const variant = createMemo(() => props.variant ?? "button")
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey())) const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey())) const view = createMemo(() => layout.view(sessionKey))
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : [])) const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const cost = createMemo(() => { const cost = createMemo(() => {

View File

@@ -50,7 +50,7 @@ export function SessionHeader() {
const showShare = createMemo(() => shareEnabled() && !!currentSession()) const showShare = createMemo(() => shareEnabled() && !!currentSession())
const showReview = createMemo(() => !!currentSession()) const showReview = createMemo(() => !!currentSession())
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const view = createMemo(() => layout.view(sessionKey())) const view = createMemo(() => layout.view(sessionKey))
const [state, setState] = createStore({ const [state, setState] = createStore({
share: false, share: false,

View File

@@ -189,6 +189,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
const params = useParams() const params = useParams()
const language = useLanguage() const language = useLanguage()
const scope = createMemo(() => sdk.directory)
const directory = createMemo(() => sync.data.path.directory) const directory = createMemo(() => sync.data.path.directory)
function normalize(input: string) { function normalize(input: string) {
@@ -234,6 +236,12 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
file: {}, file: {},
}) })
createEffect(() => {
scope()
inflight.clear()
setStore("file", {})
})
const viewCache = new Map<string, ViewCacheEntry>() const viewCache = new Map<string, ViewCacheEntry>()
const disposeViews = () => { const disposeViews = () => {
@@ -284,12 +292,16 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
const path = normalize(input) const path = normalize(input)
if (!path) return Promise.resolve() if (!path) return Promise.resolve()
const directory = scope()
const key = `${directory}\n${path}`
const client = sdk.client
ensure(path) ensure(path)
const current = store.file[path] const current = store.file[path]
if (!options?.force && current?.loaded) return Promise.resolve() if (!options?.force && current?.loaded) return Promise.resolve()
const pending = inflight.get(path) const pending = inflight.get(key)
if (pending) return pending if (pending) return pending
setStore( setStore(
@@ -301,9 +313,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
}), }),
) )
const promise = sdk.client.file const promise = client.file
.read({ path }) .read({ path })
.then((x) => { .then((x) => {
if (scope() !== directory) return
setStore( setStore(
"file", "file",
path, path,
@@ -315,6 +328,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
) )
}) })
.catch((e) => { .catch((e) => {
if (scope() !== directory) return
setStore( setStore(
"file", "file",
path, path,
@@ -330,10 +344,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
}) })
}) })
.finally(() => { .finally(() => {
inflight.delete(path) inflight.delete(key)
}) })
inflight.set(path, promise) inflight.set(key, promise)
return promise return promise
} }

View File

@@ -1,5 +1,5 @@
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { batch, createEffect, createMemo, onCleanup, onMount } from "solid-js" import { batch, createEffect, createMemo, on, onCleanup, onMount, type Accessor } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync" import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk" import { useGlobalSDK } from "./global-sdk"
@@ -432,10 +432,24 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("mobileSidebar", "opened", (x) => !x) setStore("mobileSidebar", "opened", (x) => !x)
}, },
}, },
view(sessionKey: string) { view(sessionKey: string | Accessor<string>) {
touch(sessionKey) const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
scroll.seed(sessionKey)
const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} }) touch(key())
scroll.seed(key())
createEffect(
on(
key,
(value) => {
touch(value)
scroll.seed(value)
},
{ defer: true },
),
)
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
const terminalOpened = createMemo(() => store.terminal?.opened ?? false) const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true) const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
@@ -465,10 +479,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
return { return {
scroll(tab: string) { scroll(tab: string) {
return scroll.scroll(sessionKey, tab) return scroll.scroll(key(), tab)
}, },
setScroll(tab: string, pos: SessionScroll) { setScroll(tab: string, pos: SessionScroll) {
scroll.setScroll(sessionKey, tab, pos) scroll.setScroll(key(), tab, pos)
}, },
terminal: { terminal: {
opened: terminalOpened, opened: terminalOpened,
@@ -497,9 +511,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
review: { review: {
open: createMemo(() => s().reviewOpen), open: createMemo(() => s().reviewOpen),
setOpen(open: string[]) { setOpen(open: string[]) {
const current = store.sessionView[sessionKey] const session = key()
const current = store.sessionView[session]
if (!current) { if (!current) {
setStore("sessionView", sessionKey, { setStore("sessionView", session, {
scroll: {}, scroll: {},
reviewOpen: open, reviewOpen: open,
}) })
@@ -507,93 +522,111 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
} }
if (same(current.reviewOpen, open)) return if (same(current.reviewOpen, open)) return
setStore("sessionView", sessionKey, "reviewOpen", open) setStore("sessionView", session, "reviewOpen", open)
}, },
}, },
} }
}, },
tabs(sessionKey: string) { tabs(sessionKey: string | Accessor<string>) {
touch(sessionKey) const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey
const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
touch(key())
createEffect(
on(
key,
(value) => {
touch(value)
},
{ defer: true },
),
)
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
return { return {
tabs, tabs,
active: createMemo(() => tabs().active), active: createMemo(() => tabs().active),
all: createMemo(() => tabs().all), all: createMemo(() => tabs().all),
setActive(tab: string | undefined) { setActive(tab: string | undefined) {
if (!store.sessionTabs[sessionKey]) { const session = key()
setStore("sessionTabs", sessionKey, { all: [], active: tab }) if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: [], active: tab })
} else { } else {
setStore("sessionTabs", sessionKey, "active", tab) setStore("sessionTabs", session, "active", tab)
} }
}, },
setAll(all: string[]) { setAll(all: string[]) {
if (!store.sessionTabs[sessionKey]) { const session = key()
setStore("sessionTabs", sessionKey, { all, active: undefined }) if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all, active: undefined })
} else { } else {
setStore("sessionTabs", sessionKey, "all", all) setStore("sessionTabs", session, "all", all)
} }
}, },
async open(tab: string) { async open(tab: string) {
const current = store.sessionTabs[sessionKey] ?? { all: [] } const session = key()
const current = store.sessionTabs[session] ?? { all: [] }
if (tab === "review") { if (tab === "review") {
if (!store.sessionTabs[sessionKey]) { if (!store.sessionTabs[session]) {
setStore("sessionTabs", sessionKey, { all: [], active: tab }) setStore("sessionTabs", session, { all: [], active: tab })
return return
} }
setStore("sessionTabs", sessionKey, "active", tab) setStore("sessionTabs", session, "active", tab)
return return
} }
if (tab === "context") { if (tab === "context") {
const all = [tab, ...current.all.filter((x) => x !== tab)] const all = [tab, ...current.all.filter((x) => x !== tab)]
if (!store.sessionTabs[sessionKey]) { if (!store.sessionTabs[session]) {
setStore("sessionTabs", sessionKey, { all, active: tab }) setStore("sessionTabs", session, { all, active: tab })
return return
} }
setStore("sessionTabs", sessionKey, "all", all) setStore("sessionTabs", session, "all", all)
setStore("sessionTabs", sessionKey, "active", tab) setStore("sessionTabs", session, "active", tab)
return return
} }
if (!current.all.includes(tab)) { if (!current.all.includes(tab)) {
if (!store.sessionTabs[sessionKey]) { if (!store.sessionTabs[session]) {
setStore("sessionTabs", sessionKey, { all: [tab], active: tab }) setStore("sessionTabs", session, { all: [tab], active: tab })
return return
} }
setStore("sessionTabs", sessionKey, "all", [...current.all, tab]) setStore("sessionTabs", session, "all", [...current.all, tab])
setStore("sessionTabs", sessionKey, "active", tab) setStore("sessionTabs", session, "active", tab)
return return
} }
if (!store.sessionTabs[sessionKey]) { if (!store.sessionTabs[session]) {
setStore("sessionTabs", sessionKey, { all: current.all, active: tab }) setStore("sessionTabs", session, { all: current.all, active: tab })
return return
} }
setStore("sessionTabs", sessionKey, "active", tab) setStore("sessionTabs", session, "active", tab)
}, },
close(tab: string) { close(tab: string) {
const current = store.sessionTabs[sessionKey] const session = key()
const current = store.sessionTabs[session]
if (!current) return if (!current) return
const all = current.all.filter((x) => x !== tab) const all = current.all.filter((x) => x !== tab)
batch(() => { batch(() => {
setStore("sessionTabs", sessionKey, "all", all) setStore("sessionTabs", session, "all", all)
if (current.active !== tab) return if (current.active !== tab) return
const index = current.all.findIndex((f) => f === tab) const index = current.all.findIndex((f) => f === tab)
const next = all[index - 1] ?? all[0] const next = all[index - 1] ?? all[0]
setStore("sessionTabs", sessionKey, "active", next) setStore("sessionTabs", session, "active", next)
}) })
}, },
move(tab: string, to: number) { move(tab: string, to: number) {
const current = store.sessionTabs[sessionKey] const session = key()
const current = store.sessionTabs[session]
if (!current) return if (!current) return
const index = current.all.findIndex((f) => f === tab) const index = current.all.findIndex((f) => f === tab)
if (index === -1) return if (index === -1) return
setStore( setStore(
"sessionTabs", "sessionTabs",
sessionKey, session,
"all", "all",
produce((opened) => { produce((opened) => {
opened.splice(to, 0, opened.splice(index, 1)[0]) opened.splice(to, 0, opened.splice(index, 1)[0])

View File

@@ -1,5 +1,5 @@
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createMemo, onCleanup } from "solid-js" import { batch, createEffect, createMemo, onCleanup } from "solid-js"
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda" import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2" import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
@@ -338,6 +338,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])), node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
}) })
const scope = createMemo(() => sdk.directory)
createEffect(() => {
scope()
setStore("node", {})
})
// const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path))) // const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
// const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b))) // const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
@@ -394,10 +400,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const relative = (path: string) => path.replace(sync.data.path.directory + "/", "") const relative = (path: string) => path.replace(sync.data.path.directory + "/", "")
const load = async (path: string) => { const load = async (path: string) => {
const directory = scope()
const client = sdk.client
const relativePath = relative(path) const relativePath = relative(path)
await sdk.client.file await client.file
.read({ path: relativePath }) .read({ path: relativePath })
.then((x) => { .then((x) => {
if (scope() !== directory) return
if (!store.node[relativePath]) return if (!store.node[relativePath]) return
setStore( setStore(
"node", "node",
@@ -409,6 +418,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
) )
}) })
.catch((e) => { .catch((e) => {
if (scope() !== directory) return
showToast({ showToast({
variant: "error", variant: "error",
title: language.t("toast.file.loadFailed.title"), title: language.t("toast.file.loadFailed.title"),
@@ -453,9 +463,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
} }
const list = async (path: string) => { const list = async (path: string) => {
return sdk.client.file const directory = scope()
const client = sdk.client
return client.file
.list({ path: path + "/" }) .list({ path: path + "/" })
.then((x) => { .then((x) => {
if (scope() !== directory) return
setStore( setStore(
"node", "node",
produce((draft) => { produce((draft) => {

View File

@@ -1,7 +1,7 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client" import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus" import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js" import { createEffect, createMemo, onCleanup } from "solid-js"
import { useGlobalSDK } from "./global-sdk" import { useGlobalSDK } from "./global-sdk"
import { usePlatform } from "./platform" import { usePlatform } from "./platform"
@@ -10,22 +10,39 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
init: (props: { directory: string }) => { init: (props: { directory: string }) => {
const platform = usePlatform() const platform = usePlatform()
const globalSDK = useGlobalSDK() const globalSDK = useGlobalSDK()
const sdk = createOpencodeClient({
baseUrl: globalSDK.url, const directory = createMemo(() => props.directory)
fetch: platform.fetch, const client = createMemo(() =>
directory: props.directory, createOpencodeClient({
throwOnError: true, baseUrl: globalSDK.url,
}) fetch: platform.fetch,
directory: directory(),
throwOnError: true,
}),
)
const emitter = createGlobalEmitter<{ const emitter = createGlobalEmitter<{
[key in Event["type"]]: Extract<Event, { type: key }> [key in Event["type"]]: Extract<Event, { type: key }>
}>() }>()
const unsub = globalSDK.event.on(props.directory, (event) => { createEffect(() => {
emitter.emit(event.type, event) const unsub = globalSDK.event.on(directory(), (event) => {
emitter.emit(event.type, event)
})
onCleanup(unsub)
}) })
onCleanup(unsub)
return { directory: props.directory, client: sdk, event: emitter, url: globalSDK.url } return {
get directory() {
return directory()
},
get client() {
return client()
},
event: emitter,
get url() {
return globalSDK.url
},
}
}, },
}) })

View File

@@ -7,13 +7,20 @@ import { useGlobalSync } from "./global-sync"
import { useSDK } from "./sdk" import { useSDK } from "./sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2/client" import type { Message, Part } from "@opencode-ai/sdk/v2/client"
const keyFor = (directory: string, id: string) => `${directory}\n${id}`
export const { use: useSync, provider: SyncProvider } = createSimpleContext({ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync", name: "Sync",
init: () => { init: () => {
const globalSync = useGlobalSync() const globalSync = useGlobalSync()
const sdk = useSDK() const sdk = useSDK()
const [store, setStore] = globalSync.child(sdk.directory)
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/") type Child = ReturnType<(typeof globalSync)["child"]>
type Store = Child[0]
type Setter = Child[1]
const current = createMemo(() => globalSync.child(sdk.directory))
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
const chunk = 400 const chunk = 400
const inflight = new Map<string, Promise<void>>() const inflight = new Map<string, Promise<void>>()
const inflightDiff = new Map<string, Promise<void>>() const inflightDiff = new Map<string, Promise<void>>()
@@ -25,6 +32,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}) })
const getSession = (sessionID: string) => { const getSession = (sessionID: string) => {
const store = current()[0]
const match = Binary.search(store.session, sessionID, (s) => s.id) const match = Binary.search(store.session, sessionID, (s) => s.id)
if (match.found) return store.session[match.index] if (match.found) return store.session[match.index]
return undefined return undefined
@@ -35,22 +43,30 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
return Math.ceil(count / chunk) * chunk return Math.ceil(count / chunk) * chunk
} }
const hydrateMessages = (sessionID: string) => { const hydrateMessages = (directory: string, store: Store, sessionID: string) => {
if (meta.limit[sessionID] !== undefined) return const key = keyFor(directory, sessionID)
if (meta.limit[key] !== undefined) return
const messages = store.message[sessionID] const messages = store.message[sessionID]
if (!messages) return if (!messages) return
const limit = limitFor(messages.length) const limit = limitFor(messages.length)
setMeta("limit", sessionID, limit) setMeta("limit", key, limit)
setMeta("complete", sessionID, messages.length < limit) setMeta("complete", key, messages.length < limit)
} }
const loadMessages = async (sessionID: string, limit: number) => { const loadMessages = async (input: {
if (meta.loading[sessionID]) return directory: string
client: typeof sdk.client
setStore: Setter
sessionID: string
limit: number
}) => {
const key = keyFor(input.directory, input.sessionID)
if (meta.loading[key]) return
setMeta("loading", sessionID, true) setMeta("loading", key, true)
await retry(() => sdk.client.session.messages({ sessionID, limit })) await retry(() => input.client.session.messages({ sessionID: input.sessionID, limit: input.limit }))
.then((messages) => { .then((messages) => {
const items = (messages.data ?? []).filter((x) => !!x?.info?.id) const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
const next = items const next = items
@@ -60,10 +76,10 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
.sort((a, b) => a.id.localeCompare(b.id)) .sort((a, b) => a.id.localeCompare(b.id))
batch(() => { batch(() => {
setStore("message", sessionID, reconcile(next, { key: "id" })) input.setStore("message", input.sessionID, reconcile(next, { key: "id" }))
for (const message of items) { for (const message of items) {
setStore( input.setStore(
"part", "part",
message.info.id, message.info.id,
reconcile( reconcile(
@@ -76,25 +92,32 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
) )
} }
setMeta("limit", sessionID, limit) setMeta("limit", key, input.limit)
setMeta("complete", sessionID, next.length < limit) setMeta("complete", key, next.length < input.limit)
}) })
}) })
.finally(() => { .finally(() => {
setMeta("loading", sessionID, false) setMeta("loading", key, false)
}) })
} }
const set: (...args: Parameters<Setter>) => ReturnType<Setter> = (...args) => {
return current()[1](...args)
}
return { return {
data: store, get data() {
set: setStore, return current()[0]
},
set,
get status() { get status() {
return store.status return current()[0].status
}, },
get ready() { get ready() {
return store.status !== "loading" return current()[0].status !== "loading"
}, },
get project() { get project() {
const store = current()[0]
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id) const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
if (match.found) return globalSync.data.project[match.index] if (match.found) return globalSync.data.project[match.index]
return undefined return undefined
@@ -116,7 +139,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
agent: input.agent, agent: input.agent,
model: input.model, model: input.model,
} }
setStore( current()[1](
produce((draft) => { produce((draft) => {
const messages = draft.message[input.sessionID] const messages = draft.message[input.sessionID]
if (!messages) { if (!messages) {
@@ -133,20 +156,28 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
) )
}, },
async sync(sessionID: string) { async sync(sessionID: string) {
const hasSession = getSession(sessionID) !== undefined const directory = sdk.directory
hydrateMessages(sessionID) const client = sdk.client
const [store, setStore] = globalSync.child(directory)
const hasSession = (() => {
const match = Binary.search(store.session, sessionID, (s) => s.id)
return match.found
})()
hydrateMessages(directory, store, sessionID)
const hasMessages = store.message[sessionID] !== undefined const hasMessages = store.message[sessionID] !== undefined
if (hasSession && hasMessages) return if (hasSession && hasMessages) return
const pending = inflight.get(sessionID) const key = keyFor(directory, sessionID)
const pending = inflight.get(key)
if (pending) return pending if (pending) return pending
const limit = meta.limit[sessionID] ?? chunk const limit = meta.limit[key] ?? chunk
const sessionReq = hasSession const sessionReq = hasSession
? Promise.resolve() ? Promise.resolve()
: retry(() => sdk.client.session.get({ sessionID })).then((session) => { : retry(() => client.session.get({ sessionID })).then((session) => {
const data = session.data const data = session.data
if (!data) return if (!data) return
setStore( setStore(
@@ -162,72 +193,104 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
) )
}) })
const messagesReq = hasMessages ? Promise.resolve() : loadMessages(sessionID, limit) const messagesReq = hasMessages
? Promise.resolve()
: loadMessages({
directory,
client,
setStore,
sessionID,
limit,
})
const promise = Promise.all([sessionReq, messagesReq]) const promise = Promise.all([sessionReq, messagesReq])
.then(() => {}) .then(() => {})
.finally(() => { .finally(() => {
inflight.delete(sessionID) inflight.delete(key)
}) })
inflight.set(sessionID, promise) inflight.set(key, promise)
return promise return promise
}, },
async diff(sessionID: string) { async diff(sessionID: string) {
const directory = sdk.directory
const client = sdk.client
const [store, setStore] = globalSync.child(directory)
if (store.session_diff[sessionID] !== undefined) return if (store.session_diff[sessionID] !== undefined) return
const pending = inflightDiff.get(sessionID) const key = keyFor(directory, sessionID)
const pending = inflightDiff.get(key)
if (pending) return pending if (pending) return pending
const promise = retry(() => sdk.client.session.diff({ sessionID })) const promise = retry(() => client.session.diff({ sessionID }))
.then((diff) => { .then((diff) => {
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" })) setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
}) })
.finally(() => { .finally(() => {
inflightDiff.delete(sessionID) inflightDiff.delete(key)
}) })
inflightDiff.set(sessionID, promise) inflightDiff.set(key, promise)
return promise return promise
}, },
async todo(sessionID: string) { async todo(sessionID: string) {
const directory = sdk.directory
const client = sdk.client
const [store, setStore] = globalSync.child(directory)
if (store.todo[sessionID] !== undefined) return if (store.todo[sessionID] !== undefined) return
const pending = inflightTodo.get(sessionID) const key = keyFor(directory, sessionID)
const pending = inflightTodo.get(key)
if (pending) return pending if (pending) return pending
const promise = retry(() => sdk.client.session.todo({ sessionID })) const promise = retry(() => client.session.todo({ sessionID }))
.then((todo) => { .then((todo) => {
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" })) setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
}) })
.finally(() => { .finally(() => {
inflightTodo.delete(sessionID) inflightTodo.delete(key)
}) })
inflightTodo.set(sessionID, promise) inflightTodo.set(key, promise)
return promise return promise
}, },
history: { history: {
more(sessionID: string) { more(sessionID: string) {
const store = current()[0]
const key = keyFor(sdk.directory, sessionID)
if (store.message[sessionID] === undefined) return false if (store.message[sessionID] === undefined) return false
if (meta.limit[sessionID] === undefined) return false if (meta.limit[key] === undefined) return false
if (meta.complete[sessionID]) return false if (meta.complete[key]) return false
return true return true
}, },
loading(sessionID: string) { loading(sessionID: string) {
return meta.loading[sessionID] ?? false const key = keyFor(sdk.directory, sessionID)
return meta.loading[key] ?? false
}, },
async loadMore(sessionID: string, count = chunk) { async loadMore(sessionID: string, count = chunk) {
if (meta.loading[sessionID]) return const directory = sdk.directory
if (meta.complete[sessionID]) return const client = sdk.client
const [, setStore] = globalSync.child(directory)
const key = keyFor(directory, sessionID)
if (meta.loading[key]) return
if (meta.complete[key]) return
const current = meta.limit[sessionID] ?? chunk const currentLimit = meta.limit[key] ?? chunk
await loadMessages(sessionID, current + count) await loadMessages({
directory,
client,
setStore,
sessionID,
limit: currentLimit + count,
})
}, },
}, },
fetch: async (count = 10) => { fetch: async (count = 10) => {
const directory = sdk.directory
const client = sdk.client
const [store, setStore] = globalSync.child(directory)
setStore("limit", (x) => x + count) setStore("limit", (x) => x + count)
await sdk.client.session.list().then((x) => { await client.session.list().then((x) => {
const sessions = (x.data ?? []) const sessions = (x.data ?? [])
.filter((s) => !!s?.id) .filter((s) => !!s?.id)
.slice() .slice()
@@ -236,9 +299,12 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
setStore("session", reconcile(sessions, { key: "id" })) setStore("session", reconcile(sessions, { key: "id" }))
}) })
}, },
more: createMemo(() => store.session.length >= store.limit), more: createMemo(() => current()[0].session.length >= current()[0].limit),
archive: async (sessionID: string) => { archive: async (sessionID: string) => {
await sdk.client.session.update({ sessionID, time: { archived: Date.now() } }) const directory = sdk.directory
const client = sdk.client
const [, setStore] = globalSync.child(directory)
await client.session.update({ sessionID, time: { archived: Date.now() } })
setStore( setStore(
produce((draft) => { produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id) const match = Binary.search(draft.session, sessionID, (s) => s.id)
@@ -249,7 +315,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
}, },
absolute, absolute,
get directory() { get directory() {
return store.path.directory return current()[0].path.directory
}, },
} }
}, },

View File

@@ -16,7 +16,7 @@ export default function Layout(props: ParentProps) {
return base64Decode(params.dir!) return base64Decode(params.dir!)
}) })
return ( return (
<Show when={params.dir} keyed> <Show when={params.dir}>
<SDKProvider directory={directory()}> <SDKProvider directory={directory()}>
<SyncProvider> <SyncProvider>
{iife(() => { {iife(() => {

View File

@@ -199,8 +199,8 @@ export default function Page() {
const permission = usePermission() const permission = usePermission()
const [pendingMessage, setPendingMessage] = createSignal<string | undefined>(undefined) const [pendingMessage, setPendingMessage] = createSignal<string | undefined>(undefined)
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey())) const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey())) const view = createMemo(() => layout.view(sessionKey))
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
createEffect( createEffect(