mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
store subscribing refactoring -- part 3
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
7b4e060067
commit
b690a27ebe
@ -1,11 +1,14 @@
|
|||||||
// Kubernetes watch-api client
|
// Kubernetes watch-api client
|
||||||
|
// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
|
||||||
|
|
||||||
import type { Cluster } from "../../main/cluster";
|
import type { Cluster } from "../../main/cluster";
|
||||||
import type { IKubeWatchEvent, IKubeWatchEventStreamEnd, IWatchRoutePayload } from "../../main/routes/watch-route";
|
import type { IKubeWatchEvent, IKubeWatchEventStreamEnd, IWatchRoutePayload } from "../../main/routes/watch-route";
|
||||||
import type { KubeObject } from "./kube-object";
|
import type { KubeObject } from "./kube-object";
|
||||||
import type { KubeObjectStore } from "../kube-object.store";
|
import type { KubeObjectStore } from "../kube-object.store";
|
||||||
|
import type { NamespaceStore } from "../components/+namespaces/namespace.store";
|
||||||
|
|
||||||
import { computed, observable, reaction } from "mobx";
|
import debounce from "lodash/debounce";
|
||||||
|
import { comparer, computed, observable, reaction } from "mobx";
|
||||||
import { autobind, EventEmitter } from "../utils";
|
import { autobind, EventEmitter } from "../utils";
|
||||||
import { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api";
|
import { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api";
|
||||||
import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api";
|
import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api";
|
||||||
@ -33,29 +36,58 @@ export interface IKubeWatchLog {
|
|||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class KubeWatchApi {
|
export class KubeWatchApi {
|
||||||
protected stream: ReadableStream<string>; // https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
|
private cluster: Cluster;
|
||||||
protected subscribers = observable.map<KubeApi, number>();
|
private namespaceStore: NamespaceStore;
|
||||||
protected reconnectTimeoutMs = 5000;
|
|
||||||
protected maxReconnectsOnError = 10;
|
private requestId = 0;
|
||||||
protected jsonBuffer = "";
|
private reader: ReadableStreamReader<string>;
|
||||||
protected splitter = "\n";
|
private subscribers = observable.map<KubeApi, number>();
|
||||||
|
private splitter = "\n";
|
||||||
|
private reconnectTimeoutMs = 5000;
|
||||||
|
private maxReconnectsOnError = 10;
|
||||||
|
|
||||||
// events
|
// events
|
||||||
onMessage = new EventEmitter<[IKubeWatchMessage]>();
|
public onMessage = new EventEmitter<[IKubeWatchMessage]>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
const { getHostedCluster } = await import("../../common/cluster-store");
|
||||||
|
const { namespaceStore } = await import("../components/+namespaces/namespace.store");
|
||||||
|
|
||||||
|
await namespaceStore.whenReady;
|
||||||
|
|
||||||
|
this.cluster = getHostedCluster();
|
||||||
|
this.namespaceStore = namespaceStore;
|
||||||
this.bindAutoConnect();
|
this.bindAutoConnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bindAutoConnect() {
|
private bindAutoConnect() {
|
||||||
return reaction(() => this.activeApis, () => this.connect(), {
|
const connect = debounce(() => this.connect(), 1000);
|
||||||
|
|
||||||
|
return reaction(() => this.activeApis, connect, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
delay: 500,
|
equals: comparer.structural,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get activeApis() {
|
@computed get activeApis(): string[] {
|
||||||
return Array.from(this.subscribers.keys());
|
const { cluster, namespaceStore } = this;
|
||||||
|
const activeApis = Array.from(this.subscribers.keys());
|
||||||
|
|
||||||
|
return activeApis.map(api => {
|
||||||
|
if (!cluster.isAllowedResource(api.kind)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (api.isNamespaced) {
|
||||||
|
return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace));
|
||||||
|
} else {
|
||||||
|
return api.getWatchUrl();
|
||||||
|
}
|
||||||
|
}).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubscribersCount(api: KubeApi) {
|
getSubscribersCount(api: KubeApi) {
|
||||||
@ -116,97 +148,76 @@ export class KubeWatchApi {
|
|||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async resolveCluster(): Promise<Cluster> {
|
protected async connect(apis = this.activeApis) {
|
||||||
const { getHostedCluster } = await import("../../common/cluster-store");
|
this.disconnect(); // close active connections first
|
||||||
|
|
||||||
return getHostedCluster();
|
if (!apis.length) {
|
||||||
}
|
|
||||||
|
|
||||||
protected async getRequestPayload(): Promise<IWatchRoutePayload> {
|
|
||||||
const cluster = await this.resolveCluster();
|
|
||||||
const { namespaceStore } = await import("../components/+namespaces/namespace.store");
|
|
||||||
|
|
||||||
await namespaceStore.whenReady;
|
|
||||||
|
|
||||||
return {
|
|
||||||
apis: this.activeApis.map(api => {
|
|
||||||
if (!cluster.isAllowedResource(api.kind)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api.isNamespaced) {
|
|
||||||
return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace));
|
|
||||||
} else {
|
|
||||||
return api.getWatchUrl();
|
|
||||||
}
|
|
||||||
}).flat()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async connect() {
|
|
||||||
this.disconnect(); // close active connection first
|
|
||||||
|
|
||||||
const payload = await this.getRequestPayload();
|
|
||||||
|
|
||||||
if (!payload.apis.length) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log({
|
this.log({
|
||||||
message: "Connecting",
|
message: "Connecting",
|
||||||
meta: payload,
|
meta: { apis }
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const req = await fetch(`${apiPrefix}/watch`, {
|
const requestId = ++this.requestId;
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
const request = await fetch(`${apiPrefix}/watch`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify({ apis } as IWatchRoutePayload),
|
||||||
keepalive: true,
|
signal: abortController.signal,
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json"
|
"content-type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.stream = req.body.pipeThrough(new TextDecoderStream());
|
// request above is stale since new request-id has been issued
|
||||||
this.stream.cancel = () => reader.cancel();
|
if (this.requestId !== requestId) {
|
||||||
|
abortController.abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const reader = this.stream.getReader();
|
let jsonBuffer = "";
|
||||||
|
const stream = request.body.pipeThrough(new TextDecoderStream());
|
||||||
|
const reader = stream.getReader();
|
||||||
|
|
||||||
|
this.reader = reader;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
if (done) break;
|
if (done) break; // exit
|
||||||
this.processStreamChunk(value);
|
|
||||||
|
const events = (jsonBuffer + value).split(this.splitter);
|
||||||
|
|
||||||
|
jsonBuffer = this.processBuffer(events);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log({ message: error });
|
this.log({ message: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async processStreamChunk(chunk: string) {
|
protected disconnect() {
|
||||||
const { jsonBuffer, splitter } = this;
|
this.reader?.cancel();
|
||||||
const eventsBuffer = (jsonBuffer + chunk).split(splitter);
|
this.reader = null;
|
||||||
let jsonEvent: string;
|
}
|
||||||
|
|
||||||
while (jsonEvent = eventsBuffer.shift()) {
|
// process received stream events, returns unprocessed buffer chunk if any
|
||||||
|
protected processBuffer(events: string[]): string {
|
||||||
|
for (let json of events) {
|
||||||
try {
|
try {
|
||||||
const kubeEvent: IKubeWatchEvent = JSON.parse(jsonEvent);
|
const kubeEvent: IKubeWatchEvent = JSON.parse(json);
|
||||||
const message = this.getMessage(kubeEvent);
|
const message = this.getMessage(kubeEvent);
|
||||||
|
|
||||||
this.onMessage.emit(message);
|
this.onMessage.emit(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
eventsBuffer.unshift(jsonEvent); // put unparsed json back to buffer
|
return json;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// save last unprocessed json-tail or reset buffer otherwise
|
return "";
|
||||||
this.jsonBuffer = eventsBuffer.join(splitter);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async disconnect() {
|
|
||||||
this.stream?.cancel();
|
|
||||||
this.stream = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getMessage(event: IKubeWatchEvent): IKubeWatchMessage {
|
protected getMessage(event: IKubeWatchEvent): IKubeWatchMessage {
|
||||||
|
|||||||
@ -207,7 +207,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
return [this.api];
|
return [this.api];
|
||||||
}
|
}
|
||||||
|
|
||||||
async subscribe(apis = this.getSubscribeApis()) {
|
async subscribe(apis = this.getSubscribeApis()): Promise<() => void> {
|
||||||
const cluster = await this.resolveCluster();
|
const cluster = await this.resolveCluster();
|
||||||
const allowedApis = apis.filter(api => cluster.isAllowedResource(api.kind));
|
const allowedApis = apis.filter(api => cluster.isAllowedResource(api.kind));
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user