fix(ui-conversation): dispose Inbox projection subscription
This commit is contained in:
1 parent
8c8c5b2680
commit
ba51e5483e
6 files changed
+27
-4
No files matched your search
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.md
|
||||
2026-07-30-web-queue-steer-action.md: 4135c465166d88873e5b4595758778839efe83ee
|
||||
2026-07-30-web-queue-steer-action.zh.md: 2717c8a53b6a46c4751b1a8bb1e563b7aa37a42e
|
||||
2026-07-30-web-queue-steer-action.md: 4193bd9fcb2d20def8b72ff160f3ab3fa0b3e7c4
|
||||
2026-07-30-web-queue-steer-action.zh.md: 15dd9719d449afbc5ddab38de31b91aca2ccfed7
|
||||
@@ -65,3 +65,5 @@ The keyless Web steering scenario queues a message through the real composer whi
|
||||
`inbox` is the domain-owned durable session projection. Pending steering survives reconnect and process restart and appears immediately from `next-step`; after claim, the durable conversation node follows through the ordinary event projection. The running bit can change between rendering and the synchronous operation, so an enabled action may return `steer-unavailable` while the product continues through Queue without reporting failure.
|
||||
|
||||
The explicit action changes delivery from an independently admitted turn to current-turn steering. Its two durable splices briefly expose the deletion and insertion as adjacent projection revisions, while client snapshot batching converges on the message moving from `next-turn` to `next-step`.
|
||||
|
||||
Claim removes the pending `next-step` value before the later `user/message` reaches the independent history stream, so a client may briefly render neither representation when those notifications cross separate flushes. The product accepts this transient gap instead of retaining a queue-specific handoff mirror; the durable log still converges on exactly one conversation node.
|
||||
@@ -65,3 +65,5 @@ Host schema 和代理测试覆盖新操作、两种类型化错误、Inbox 投
|
||||
`inbox` 是领域拥有的持久会话投影。待处理 steering 会从 `next-step` 立即出现,能在重连与进程重启后恢复;认领后,持久会话节点经普通事件投影跟进。running 标志位可能在渲染与同步操作之间发生变化,因此已启用的操作可能返回 `steer-unavailable`,而产品仍经 Queue 继续执行且不显示失败。
|
||||
|
||||
这项显式操作会把投递方式从经独立接纳的轮次改为当前轮次 steering。它的两条持久 splice 会短暂地把删除与插入暴露为相邻投影修订,而客户端快照批处理会收敛到消息从 `next-turn` 移入 `next-step`。
|
||||
|
||||
Claim 会先移除待处理的 `next-step` 值,随后 `user/message` 才到达独立的历史流;若两项通知跨越不同的客户端刷新,客户端可能短暂地两种表示都不渲染。产品接受这一瞬态空隙,以避免保留 Queue 专用的交接镜像;持久日志最终仍只产生一个会话节点。
|
||||
File renamed without changes.
@@ -159,6 +159,7 @@ export class SessionInputShell implements SessionInput {
|
||||
nodes: [ReferenceChipNode, TextRefNode],
|
||||
onError: (error) => { throw error },
|
||||
})
|
||||
this.state = createSnapshotStore<InputState>(this.compose())
|
||||
this.unregister = mergeRegister(
|
||||
registerPlainText(this.editor),
|
||||
registerHistory(this.editor, createEmptyHistoryState(), HISTORY_MERGE_DELAY_MS),
|
||||
@@ -166,9 +167,8 @@ export class SessionInputShell implements SessionInput {
|
||||
registerClaimDecoration(this.editor, () => this.activeClaimToken()),
|
||||
registerTextRefDecoration(this.editor, () => this.lexicon.getSnapshot(), () => this.activeClaimToken()),
|
||||
() => { this.lexiconOff?.() },
|
||||
deps.inbox?.subscribe(() => { this.publish() }) ?? (() => {}),
|
||||
)
|
||||
this.state = createSnapshotStore<InputState>(this.compose())
|
||||
deps.inbox?.subscribe(() => { this.publish() })
|
||||
}
|
||||
|
||||
// ---- editor plumbing ----
|
||||
|
||||
@@ -115,6 +115,25 @@ function bench(over?: {
|
||||
}
|
||||
|
||||
describe('matrix row: plain', () => {
|
||||
it('unsubscribes from the Inbox projection when disposed', () => {
|
||||
const unsubscribe = vi.fn()
|
||||
const subscribe = vi.fn(() => unsubscribe)
|
||||
const shell = new SessionInputShell({
|
||||
actx: SCTX,
|
||||
defaultSink: () => Promise.resolve({ kind: 'success' }),
|
||||
inbox: { getSnapshot: () => undefined, subscribe },
|
||||
commandImages: {
|
||||
serialize: () => Promise.resolve([]),
|
||||
release: () => {},
|
||||
unsupportedNotice: token => `${token.trim()} images-unsupported`,
|
||||
},
|
||||
})
|
||||
|
||||
expect(subscribe).toHaveBeenCalledOnce()
|
||||
shell.dispose()
|
||||
expect(unsubscribe).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('enter falls to the default sink; no claim on the currency; edits free', async () => {
|
||||
const { textarea, shell, sink } = bench()
|
||||
act(() => { shell.setDraft('普通消息') })
|
||||
|
||||
Reference in new issue
Block a user