106 lines
4.1 KiB
TypeScript
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 }
|
|
}
|
|
}
|