Skip to content

Commit a9152c1

Browse files
Add a basic implementation of CachingLocator.
1 parent 8ec292e commit a9152c1

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import '../../common/extensions';
5+
import { createDeferred } from '../../common/utils/async';
6+
import { PythonEnvInfo } from '../base/info';
7+
import {
8+
ILocator,
9+
IPythonEnvsIterator,
10+
PythonEnvUpdatedEvent,
11+
PythonLocatorQuery,
12+
} from '../base/locator';
13+
import { PythonEnvsWatcher } from '../base/watcher';
14+
15+
type PythonEnvID = string;
16+
17+
function getEnvID(env: PythonEnvInfo | string): PythonEnvID {
18+
if (typeof env === 'string') {
19+
return env;
20+
}
21+
return env.executable.filename;
22+
}
23+
24+
function isSameEnv(env1: PythonEnvInfo, env2: PythonEnvInfo): boolean {
25+
return getEnvID(env1) === getEnvID(env2);
26+
}
27+
28+
function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) => boolean {
29+
return (env) => {
30+
if (query.kinds !== undefined) {
31+
if (!query.kinds.includes(env.kind)) {
32+
return false;
33+
}
34+
}
35+
if (query.searchLocations !== undefined) {
36+
if (env.searchLocation !== undefined) {
37+
// XXX Are URLs comparable like this?
38+
if (!query.searchLocations.includes(env.searchLocation)) {
39+
return false;
40+
}
41+
}
42+
}
43+
return true;
44+
};
45+
}
46+
47+
async function getEnvs(iterator: IPythonEnvsIterator): Promise<PythonEnvInfo[]> {
48+
const envs: PythonEnvInfo[] = [];
49+
50+
const updatesDone = createDeferred<void>();
51+
if (iterator.onUpdated === undefined) {
52+
updatesDone.resolve();
53+
} else {
54+
iterator.onUpdated((event: PythonEnvUpdatedEvent | null) => {
55+
if (event === null) {
56+
updatesDone.resolve();
57+
return;
58+
}
59+
const oldEnv = envs.find((e) => isSameEnv(e, event.old));
60+
if (oldEnv === undefined) {
61+
// XXX log or fail
62+
} else {
63+
envs[envs.indexOf(oldEnv)] = event.new;
64+
}
65+
});
66+
}
67+
68+
let result = await iterator.next();
69+
while (!result.done) {
70+
envs.push(result.value);
71+
// eslint-disable-next-line no-await-in-loop
72+
result = await iterator.next();
73+
}
74+
75+
await updatesDone.promise;
76+
return envs;
77+
}
78+
79+
interface IEnvsCache {
80+
initialize(): Promise<void>;
81+
listAll(): Promise<PythonEnvInfo[] | undefined>;
82+
getEnv(id: PythonEnvID): Promise<PythonEnvInfo | undefined>;
83+
setAll(envs: PythonEnvInfo[]): Promise<void>;
84+
flush(): Promise<void>;
85+
}
86+
87+
export class CachingLocator extends PythonEnvsWatcher implements ILocator {
88+
private readonly initializing = createDeferred<void>();
89+
90+
constructor(
91+
private readonly cache: IEnvsCache,
92+
private readonly locator: ILocator,
93+
) {
94+
super();
95+
locator.onChanged((event) => {
96+
this.refresh()
97+
.then(() => this.fire(event))
98+
.ignoreErrors();
99+
});
100+
}
101+
102+
public async initialize(): Promise<void> {
103+
await this.cache.initialize();
104+
const envs = await this.cache.listAll();
105+
if (envs !== undefined) {
106+
this.initializing.resolve();
107+
await this.refresh();
108+
} else {
109+
// There is nothing in the cache, so we must wait for the
110+
// initial refresh to finish before allowing iteration.
111+
await this.refresh();
112+
this.initializing.resolve();
113+
}
114+
}
115+
116+
public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator {
117+
return this.iterFromCache(query);
118+
}
119+
120+
public async resolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
121+
// If necessary we could be more aggressive about invalidating
122+
// the cached value.
123+
const envID = getEnvID(env);
124+
const cached = await this.cache.getEnv(envID);
125+
if (cached !== undefined) {
126+
return cached;
127+
}
128+
// Fall back to the underlying locator.
129+
const envs = await this.cache.listAll();
130+
const resolved = await this.locator.resolveEnv(env);
131+
if (resolved !== undefined) {
132+
envs!.push(resolved);
133+
await this.update(envs!);
134+
}
135+
return resolved;
136+
}
137+
138+
private async* iterFromCache(query?: PythonLocatorQuery): IPythonEnvsIterator {
139+
// XXX For now we wait for the initial refresh to finish...
140+
await this.initializing.promise;
141+
142+
const envs = await this.cache.listAll();
143+
if (envs === undefined) {
144+
throw Error('this should be unreachable');
145+
}
146+
if (await this.needsRefresh(envs)) {
147+
// Refresh in the background.
148+
this.refresh().ignoreErrors();
149+
}
150+
if (query !== undefined) {
151+
const filter = getQueryFilter(query);
152+
yield* envs.filter(filter);
153+
} else {
154+
yield* envs;
155+
}
156+
}
157+
158+
// eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-unused-vars
159+
private async needsRefresh(_envs: PythonEnvInfo[]): Promise<boolean> {
160+
// XXX
161+
// For now we never refresh. Options:
162+
// * every X minutes (via `initialize()`
163+
// * if at least X minutes have elapsed
164+
// * if some "stale" check on any known env fails
165+
return false;
166+
}
167+
168+
private async refresh(): Promise<void> {
169+
const iterator = this.locator.iterEnvs();
170+
const envs = await getEnvs(iterator);
171+
await this.update(envs);
172+
}
173+
174+
private async update(envs: PythonEnvInfo[]): Promise<void> {
175+
// If necessary, we could skip if there are no changes.
176+
await this.cache.setAll(envs);
177+
await this.cache.flush();
178+
this.fire({}); // Emit an "onCHanged" event.
179+
}
180+
}

0 commit comments

Comments
 (0)