UNPKG

@eclipse-emfcloud/model-service-theia

Version:
276 lines 11.9 kB
"use strict"; // ***************************************************************************** // 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