Files
superwork/vscode/src/codeReader/deferredAnswers.ts
T

106 lines
4.1 KiB
TypeScript

import { RETRY_DELAYS } from './nameResolution'
/** 排一遍视图数据用的:同步地问(交出已经查到的),和这一遍问了哪些问题。 */
export interface AnswerSession<Q, A> {
ask: (question: Q) => A | undefined
questions: () => Q[]
}
/**
* 查一个问题:查到了是答案,查了没有是 null;语言扩展没答(还没起来)是 undefined,已知的留着不动,隔一会儿再查。
*/
export type Answer<Q, A> = (question: Q) => Promise<A | null | undefined>
/**
* 一份打开着的文档里,排视图数据时要问、却要异步才查得到的问题(库规则问的名字的定义、调用的名字定义成哪种块)的答案,
* 按问题的 id 记,id 一样的问题按一个查。这里不碰 VS Code。
* 排视图数据是同步的:先交出已经查到的(文档改了也照旧用上一版查到的),记下这一遍问了什么;发出预览后再查,
* 每个版本同一个问题查一次(语言扩展没答的照 RETRY_DELAYS 隔一会儿再查),查到的和已知的不一样就 changed,宿主按新结果重排、重发。
*/
export class DeferredAnswers<Q, A> {
/** 查到的:查了没有是 null */
private readonly known = new Map<string, A | null>()
private version: number | undefined
/** 眼下这个版本查过(或正在查)的问题 */
private readonly checked = new Set<string>()
private timer: ReturnType<typeof setTimeout> | undefined
/** 查到的答案变过几次:按眼下的答案排出来的东西(对比用的旧版本)比这个数就知道还能不能接着用 */
private changes = 0
constructor(
private readonly idOf: (question: Q) => string,
/** 两个答案对问的人来说一样不一样 */
private readonly same: (a: A | null | undefined, b: A | null | undefined) => boolean,
private readonly changed: () => void,
private readonly delays: readonly number[] = RETRY_DELAYS,
private readonly schedule: (run: () => void, ms: number) => ReturnType<typeof setTimeout> = setTimeout,
) {}
get revision(): number {
return this.changes
}
session(): AnswerSession<Q, A> {
const asked = new Map<string, Q>()
return {
ask: (question) => {
const id = this.idOf(question)
if (!asked.has(id))
asked.set(id, question)
return this.known.get(id) ?? undefined
},
questions: () => [...asked.values()],
}
}
/** 查 version 这一版问的问题:这个版本查过的不再查。 */
resolve(version: number, questions: readonly Q[], answer: Answer<Q, A>): void {
if (this.version !== version) {
clearTimeout(this.timer)
this.version = version
this.checked.clear()
}
const fresh = questions.filter(question => !this.checked.has(this.idOf(question)))
for (const question of fresh)
this.checked.add(this.idOf(question))
if (fresh.length === 0)
return
const attempt = (pending: readonly Q[], tries: number): void => {
void this.check(pending, answer).then(({ unanswered, changed }) => {
if (this.version !== version)
return
if (changed)
this.changed()
if (unanswered.length > 0 && tries < this.delays.length)
this.timer = this.schedule(() => attempt(unanswered, tries + 1), this.delays[tries])
})
}
attempt(fresh, 0)
}
dispose(): void {
clearTimeout(this.timer)
this.version = undefined
}
/** 查一遍:没答的问题留着已知的不动,交回去等下一次再查。 */
private async check(questions: readonly Q[], answer: Answer<Q, A>): Promise<{ unanswered: Q[], changed: boolean }> {
const unanswered: Q[] = []
let changed = false
await Promise.all(questions.map(async (question) => {
const found = await answer(question)
if (found === undefined) {
unanswered.push(question)
return
}
const id = this.idOf(question)
if (!this.same(this.known.get(id), found)) {
changed = true
this.changes++
}
this.known.set(id, found)
}))
return { unanswered, changed }
}
}