fix(web): discard projection state across host generations
This commit is contained in:
1 parent
e101c448e5
commit
f3332af2a4
11 files changed
+178
-17
No files matched your search
@@ -114,7 +114,7 @@ export class SessionManager {
|
||||
private listPhase: SessionListPhase = 'pending'
|
||||
private listError: RemoteFailure | null = null
|
||||
private listInflight: Promise<void> | null = null
|
||||
/** Mutations arriving after a list request starts are replayed over its response. */
|
||||
/** Active list request's mutation log; its identity also fences completion after reconnect. */
|
||||
private listMutations: SessionListMutation[] | null = null
|
||||
private readonly addresses = new Map<SessionId, SubagentAddress>()
|
||||
private readonly catalogs = new Map<SessionId, SubagentCatalogSnapshot>()
|
||||
@@ -441,7 +441,7 @@ export class SessionManager {
|
||||
|
||||
// ---- List API ----
|
||||
|
||||
/** Full refresh via session.list (single-flight: an in-flight call is reused). */
|
||||
/** Full refresh via session.list (single-flight within one Host generation). */
|
||||
refreshList(): Promise<void> {
|
||||
if (this.listInflight !== null) return this.listInflight
|
||||
this.listState = 'loading'
|
||||
@@ -453,6 +453,7 @@ export class SessionManager {
|
||||
this.listInflight = (async () => {
|
||||
try {
|
||||
const result = await this.remote.session.list({})
|
||||
if (this.listMutations !== mutations) return
|
||||
if (result.ok) {
|
||||
const baseline: SessionSummary[] = this.listPhase === 'pending'
|
||||
? [...result.value.items]
|
||||
@@ -502,12 +503,15 @@ export class SessionManager {
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isRemoteFailure(error)) throw error
|
||||
if (this.listMutations !== mutations) return
|
||||
this.listState = 'error'
|
||||
this.listError = error
|
||||
} finally {
|
||||
this.listMutations = null
|
||||
this.listInflight = null
|
||||
this.notifier.markDirty()
|
||||
if (this.listMutations === mutations) {
|
||||
this.listMutations = null
|
||||
this.listInflight = null
|
||||
this.notifier.markDirty()
|
||||
}
|
||||
}
|
||||
})()
|
||||
return this.listInflight
|
||||
@@ -766,9 +770,14 @@ export class SessionManager {
|
||||
|
||||
/**
|
||||
* Repair one re-established Host-event generation with queryable baselines.
|
||||
* Discard old projection cuts before new queries, including cold Sessions
|
||||
* absent from the process-local control baseline.
|
||||
* Opened Session follow streams resume independently through API Gateway.
|
||||
*/
|
||||
handleConnected(): void {
|
||||
for (const store of this.projectionStores.values()) store.clear()
|
||||
this.listMutations = null
|
||||
this.listInflight = null
|
||||
void this.refreshList()
|
||||
const selectedAddress = this.selected === undefined ? undefined : this.addresses.get(this.selected)
|
||||
if (selectedAddress !== undefined) void this.refreshSubagents(selectedAddress.parentSessionId)
|
||||
|
||||
@@ -67,9 +67,9 @@ interface Channel {
|
||||
/**
|
||||
* One session's projection values. Framework semantics, uniform across every
|
||||
* key: a baseline seeds rows at its cut, a push frame updates one row, and in
|
||||
* both paths a lower-or-equal seq loses — a replayed frame cannot regress a
|
||||
* value, a stale baseline cannot overwrite a newer frame. A key the store has
|
||||
* never seen reads `undefined` (capability absent). Faces are identity-stable
|
||||
* both paths a lower-or-equal seq within the Host generation loses. A replayed
|
||||
* frame cannot regress a value; a stale baseline cannot overwrite a newer
|
||||
* frame. A key the store has never seen reads `undefined` (capability absent). Faces are identity-stable
|
||||
* per key (create-on-demand, cached) so the React side binds each exactly
|
||||
* once; the store-level channel (`subscribeAny`) serves coarse consumers (the
|
||||
* manager's list projection reads the `title` key).
|
||||
@@ -174,6 +174,14 @@ export class ProjectionValueStore {
|
||||
}
|
||||
}
|
||||
|
||||
/** Discard one Host generation's values and watermarks while preserving subscribed faces. */
|
||||
clear(): void {
|
||||
for (const key of this.rows.keys()) {
|
||||
this.rows.delete(key)
|
||||
this.changed(key)
|
||||
}
|
||||
}
|
||||
|
||||
private changed(key: string): void {
|
||||
this.valuesCache = undefined
|
||||
this.channels.get(key)?.notifier.markDirty()
|
||||
|
||||
Reference in new issue
Block a user