@eclipse-emfcloud/model-service-theia
Version:
Model service Theia
276 lines • 11.9 kB
JavaScript
;
// *****************************************************************************
// Copyright (C) 2023-2024 STMicroelectronics.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: MIT License which is
// available at https://opensource.org/licenses/MIT.
//
// SPDX-License-Identifier: EPL-2.0 OR MIT
// *****************************************************************************
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FrontendModelHubSubscriberImpl = exports.FrontendModelHubSubscriber = void 0;
const promise_util_1 = require("@theia/core/lib/common/promise-util");
const inversify_1 = require("@theia/core/shared/inversify");
const fast_json_patch_1 = require("fast-json-patch");
exports.FrontendModelHubSubscriber = Symbol('FrontendModelHubSubscriber');
let FrontendModelHubSubscriberImpl = class FrontendModelHubSubscriberImpl {
constructor() {
this.client = {
onModelChanged: (subscriptionId, modelId, delta) => {
this.updateModelCache(subscriptionId, modelId, delta);
this.lookupSubs(subscriptionId, modelId)?.then(({ subs, model }) => subs.forEach((sub) => sub.onModelChanged?.(modelId, model, delta)));
},
onModelDirtyState: (subscriptionId, modelId, dirty) => {
this.lookupSubs(subscriptionId, modelId)?.then(({ subs, model }) => subs.forEach((sub) => sub.onModelDirtyState?.(modelId, model, dirty)));
},
onModelValidated: (subscriptionId, modelId, diagnostic) => {
this.lookupSubs(subscriptionId, modelId)?.then(({ subs, model }) => subs.forEach((sub) => sub.onModelValidated?.(modelId, model, diagnostic)));
},
onModelLoaded: (subscriptionId, modelId) => {
this.lookupSubs(subscriptionId, modelId)?.then(({ subs }) => {
subs.forEach((sub) => sub.onModelLoaded?.(modelId));
});
},
onModelUnloaded: (subscriptionId, modelId) => {
const subs = this.lookupSubs(subscriptionId, modelId);
if (subs) {
subs
.then(({ subs, model }) => {
subs.forEach((sub) => sub.onModelUnloaded?.(modelId, model));
})
.finally(() => this.removeModelCache(subscriptionId, modelId));
}
else {
this.removeModelCache(subscriptionId, modelId);
}
},
onModelHubDisposed: (subscriptionId) => {
this.lookupHubSubs(subscriptionId)?.then((subs) => {
subs.forEach((sub) => sub.onModelHubDisposed());
});
},
closeSubscription: (id) => {
this.closeSub(id);
},
//
// Model hub tracking
//
onModelHubCreated: (context) => {
this.knownModelHubs.add(context);
this.trackingSubs.forEach((sub) => sub.onModelHubCreated?.(context));
},
onModelHubDestroyed: (context) => {
this.closeContext(context);
this.knownModelHubs.delete(context);
this.trackingSubs.forEach((sub) => sub.onModelHubDestroyed?.(context));
},
};
this.subscriptions = [];
this.trackingSubs = [];
this.modelCaches = new Map();
this.modelHub = new promise_util_1.Deferred();
this.knownModelHubs = new Set();
/**
* My own subscription to the model hub that implements
* the pipeline of incremental model caching and client
* subscription multiplexing.
*/
this.subscriptionPipelines = new Map();
}
setModelHub(modelHub) {
this.modelHub.resolve(modelHub);
}
/**
* Look up a subscription and the model that will have to be passed along
* to any of its call-backs that are subsequently invoked.
*
* @param subscriptionId the subscription to look up
* @param modelId a model to resolve to pass on to the subscription
* @returns resolved subscription and model, as long as both are resolved
*/
lookupSubs(subscriptionId, modelId) {
const context = this.getSubscriptionContext(subscriptionId);
if (context === undefined) {
return undefined;
}
const subs = this.subscriptions.filter((sub) => sub.context === context &&
(!sub.modelIds.length || sub.modelIds.includes(modelId)));
if (!subs.length) {
return undefined;
}
return this.getModel(context, modelId).then((model) => ({
subs,
model,
}));
}
/**
* Look up a subscription for the purpose of invoking hub lifecycle call-backs.
* As for now the only call-back is `onModelHubDisposed`, we return only subs
* that have this call-back.
*
* @param subscriptionId the subscription to look up
* @returns resolved hub subscriptions
*/
lookupHubSubs(subscriptionId) {
const context = this.getSubscriptionContext(subscriptionId);
if (context === undefined) {
return undefined;
}
const hasOnModelHubDisposed = (sub) => {
return sub.context === context && sub.onModelHubDisposed !== undefined;
};
return Promise.resolve(this.subscriptions.filter(hasOnModelHubDisposed));
}
getSubscriptionContext(subscriptionId) {
for (const [context, pipeline] of this.subscriptionPipelines) {
if (subscriptionId === pipeline.id) {
return context;
}
}
return undefined;
}
/**
* React to the notification from the backend that a subscription
* was closed on that end by closing the corresponding frontend
* subscriptions.
*
* @param subscriptionId a backend subscription that was closed
*/
closeSub(subscriptionId) {
const context = this.getSubscriptionContext(subscriptionId);
if (context !== undefined) {
this.closeContext(context);
}
}
closeContext(context) {
this.subscriptionPipelines.delete(context);
const newSubs = this.subscriptions.filter((sub) => sub.context !== context);
this.subscriptions.length = 0;
this.subscriptions.push(...newSubs);
}
updateModelCache(subscriptionId, modelId, delta) {
const context = this.getSubscriptionContext(subscriptionId);
if (context === undefined) {
// Not our subscription: do not update the cache
return;
}
const modelCache = this.getModelCache(context);
if (delta && delta.length && modelCache.has(modelId)) {
const model = modelCache.get(modelId);
try {
// Because this patch came in over the RPC wire, we don't need
// to worry about it having references to objects in the model
// that it changes and so being, itself, changed as a side-effect
// of applying it to the model.
// Therefore, we do not need to first clone the patch as recommended
// by the `applyPatch` API documentation
(0, fast_json_patch_1.applyPatch)(model, delta);
}
catch (error) {
// Invalidate our cache because now we're out of sync
modelCache.delete(modelId);
console.warn(`Error applying received model delta to frontend model cache ${modelId}. Remove cached model to synchronize on next access.`, error);
}
}
}
removeModelCache(subscriptionId, modelId) {
const context = this.getSubscriptionContext(subscriptionId);
if (context === undefined) {
// Not our subscription: do not update the cache
return;
}
this.getModelCache(context).delete(modelId);
}
async getModel(context, modelId) {
// Need this to keep model cache updated
await this.ensureSubscriptionPipeline(context);
const modelCache = this.getModelCache(context);
let result = modelCache.get(modelId);
if (result === undefined) {
const hub = await this.modelHub.promise;
result = await hub.getModel(context, modelId);
modelCache.set(modelId, result);
}
return result;
}
getModelCache(context) {
let result = this.modelCaches.get(context);
if (result === undefined) {
result = new Map();
this.modelCaches.set(context, result);
}
return result;
}
async subscribe(context, ...modelIds) {
await this.ensureSubscriptionPipeline(context);
const subscription = {
context,
modelIds,
close: () => {
const index = this.subscriptions.indexOf(subscription);
if (index >= 0) {
this.subscriptions.splice(index, 1);
}
},
};
this.subscriptions.push(subscription);
return subscription;
}
/**
* Ensure that I am myself subscribed to the model hub to maintain my model cache
* and multiplex my clients' subscriptions.
*/
async ensureSubscriptionPipeline(context) {
if (!this.subscriptionPipelines.has(context)) {
const hub = await this.modelHub.promise;
const newPipeline = await hub.subscribe(context);
this.subscriptionPipelines.set(context, newPipeline);
}
}
//
// Model hub tracking
//
trackModelHubs() {
let _onModelHubCreated;
const knownModelHubs = this.knownModelHubs;
const result = {
close: () => {
const index = this.trackingSubs.indexOf(result);
if (index >= 0) {
this.trackingSubs.splice(index, 1);
}
},
get onModelHubCreated() {
return _onModelHubCreated;
},
set onModelHubCreated(onModelHubCreated) {
_onModelHubCreated = onModelHubCreated;
if (onModelHubCreated) {
knownModelHubs.forEach((context) => onModelHubCreated(context));
}
},
};
this.trackingSubs.push(result);
return result;
}
isModelHubAvailable(context) {
return this.knownModelHubs.has(context);
}
};
exports.FrontendModelHubSubscriberImpl = FrontendModelHubSubscriberImpl;
exports.FrontendModelHubSubscriberImpl = FrontendModelHubSubscriberImpl = __decorate([
(0, inversify_1.injectable)()
], FrontendModelHubSubscriberImpl);
//# sourceMappingURL=frontend-model-hub-subscriber.js.map