@hydranium/glsp-server 1.0.0-next.117 → 1.0.0-next.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/command/hydranium-glsp-recording-command.d.ts +1 -1
- package/lib/command/hydranium-glsp-recording-command.js +1 -1
- package/lib/messages/index.d.ts +1 -1
- package/lib/messages/index.d.ts.map +1 -1
- package/lib/messages/index.js +1 -1
- package/lib/messages/index.js.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.d.ts +52 -8
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.js +77 -5
- package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.d.ts +4 -4
- package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.js +5 -10
- package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
- package/lib/state/reconcile-source-model-write.d.ts +2 -2
- package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
- package/lib/state/reconcile-source-model-write.js +1 -1
- package/lib/state/reconcile-source-model-write.js.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.d.ts +54 -74
- package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.js +77 -102
- package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +16 -15
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js +21 -25
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
- package/lib/storage/hydranium-glsp-storage.d.ts +103 -26
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
- package/lib/storage/hydranium-glsp-storage.js +199 -32
- package/lib/storage/hydranium-glsp-storage.js.map +1 -1
- package/package.json +7 -7
- package/src/command/hydranium-glsp-recording-command.ts +1 -1
- package/src/messages/index.ts +6 -1
- package/src/state/abstract-hydranium-glsp-state.ts +85 -8
- package/src/state/full-text-hydranium-glsp-state.ts +5 -10
- package/src/state/reconcile-source-model-write.ts +3 -3
- package/src/state/reconciling-multi-document-glsp-state.ts +78 -103
- package/src/state/reconciling-transfer-hydranium-glsp-state.ts +21 -25
- package/src/storage/hydranium-glsp-storage.ts +236 -33
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { MarkersReason, SetMarkersAction, StatusAction } from '@eclipse-glsp/protocol';
|
|
10
|
+
import { MarkersReason, MessageAction, SetMarkersAction, StatusAction } from '@eclipse-glsp/protocol';
|
|
11
11
|
import {
|
|
12
12
|
type Action,
|
|
13
13
|
ActionDispatcher,
|
|
@@ -31,11 +31,24 @@ import {
|
|
|
31
31
|
type SourceModelStorage,
|
|
32
32
|
TEMPORARY_CLIENT_ID
|
|
33
33
|
} from '@eclipse-glsp/server';
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
Debouncer,
|
|
36
|
+
defineMessage,
|
|
37
|
+
DisposableCollection,
|
|
38
|
+
isDuplicateClientIdError,
|
|
39
|
+
isSessionClosedError,
|
|
40
|
+
SessionClosedError
|
|
41
|
+
} from '@hydranium/protocol';
|
|
35
42
|
import { inject, injectable, optional, postConstruct } from 'inversify';
|
|
36
43
|
import { type AstNode } from '@hydranium/langium';
|
|
37
44
|
import { URI } from '@hydranium/langium';
|
|
38
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
AstDocument,
|
|
47
|
+
type AstDocumentSavedEvent,
|
|
48
|
+
type AstDocumentUpdatedEvent,
|
|
49
|
+
type ClientSession as ModelClientSession,
|
|
50
|
+
type ServerSharedServices
|
|
51
|
+
} from '@hydranium/core';
|
|
39
52
|
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
40
53
|
import { type AbstractHydraniumGlspState } from '../state/abstract-hydranium-glsp-state.js';
|
|
41
54
|
import { type HydraniumGlspSubmissionHandler } from '../submission/hydranium-glsp-submission-handler.js';
|
|
@@ -93,6 +106,21 @@ export const DIAGRAM_READONLY_PARSE_ERROR = defineMessage(
|
|
|
93
106
|
'Read-only: this document has a syntax error. Fix it to edit the diagram again.'
|
|
94
107
|
);
|
|
95
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The diagram's GLSP client id is still live as another participant's client
|
|
111
|
+
* id once the load has waited for it to free, so the diagram does not load.
|
|
112
|
+
*
|
|
113
|
+
* Rendered at the raise site, as {@link SAVE_TARGET_UNKNOWN} is and for the
|
|
114
|
+
* same reason, and dispatched to the client as a message of its own: a model
|
|
115
|
+
* request fails as a rejection whose detail is the English cause, which no user
|
|
116
|
+
* sees. It names the recovery: a host that numbers its diagram clients gives a
|
|
117
|
+
* reopened diagram a fresh id.
|
|
118
|
+
*/
|
|
119
|
+
export const DIAGRAM_SESSION_REFUSED = defineMessage(
|
|
120
|
+
'hydranium/glsp-server/diagram-session-refused',
|
|
121
|
+
'Could not open this diagram: its identifier is still in use by another editor. Close the diagram and open it again.'
|
|
122
|
+
);
|
|
123
|
+
|
|
96
124
|
/** Window (ms) over which back-to-back external rebuilds collapse into one resubmit. */
|
|
97
125
|
const EXTERNAL_SUBMIT_DEBOUNCE_MS = 250;
|
|
98
126
|
|
|
@@ -144,11 +172,21 @@ function isStructuralDiagnostic(diagnostic: unknown): boolean {
|
|
|
144
172
|
* whole flow, and select a {@link SaveDeliveryPolicy} via the bound option to
|
|
145
173
|
* tune how {@link saveSourceModel} delivers its result.
|
|
146
174
|
*
|
|
147
|
-
* **
|
|
148
|
-
*
|
|
149
|
-
* the
|
|
150
|
-
*
|
|
151
|
-
*
|
|
175
|
+
* **One client session per GLSP client session.** `init` registers the GLSP
|
|
176
|
+
* client id as a client session ({@link registerModelSession}) and hands it to
|
|
177
|
+
* the state as `modelSession`. The primary is opened through it on load, and
|
|
178
|
+
* every document that joins the write set as it joins
|
|
179
|
+
* ({@link openSecondary}); a document that leaves the write set stays open
|
|
180
|
+
* until the next save, so leaving never reverts the diagram's unsaved edits to
|
|
181
|
+
* it. Disposing the storage ends the session, which closes everything it has
|
|
182
|
+
* open.
|
|
183
|
+
*
|
|
184
|
+
* **Default `saveSourceModel` flow.** Flushes the stored text of every
|
|
185
|
+
* document the diagram's session has open via `AstDocumentManager.save`, with
|
|
186
|
+
* no serializer in the path — the update path already put the settled text in
|
|
187
|
+
* the store. The bound {@link SaveDeliveryPolicy} (default
|
|
188
|
+
* {@link DEFAULT_SAVE_DELIVERY_POLICY}, `await`) decides
|
|
189
|
+
* await-vs-fire-and-forget and failure handling.
|
|
152
190
|
*
|
|
153
191
|
* **tempId filter.** GLSP's `DefaultGlobalActionProvider` spins up a
|
|
154
192
|
* throwaway per-diagram-type container with upstream's {@link TEMPORARY_CLIENT_ID}
|
|
@@ -159,9 +197,9 @@ function isStructuralDiagnostic(diagnostic: unknown): boolean {
|
|
|
159
197
|
* **Lifecycle ordering.** `init` runs after DI resolution and parks the
|
|
160
198
|
* secondary-write-set watch, which outlives any single load.
|
|
161
199
|
* {@link sessionDisposed} calls {@link dispose}, which drains
|
|
162
|
-
* {@link toDispose} idempotently
|
|
163
|
-
* {@link doLoadSourceModel} is parked on
|
|
164
|
-
* them on client-detach.
|
|
200
|
+
* {@link toDispose} idempotently and then ends the client session. Every
|
|
201
|
+
* transient subscription created in {@link doLoadSourceModel} is parked on
|
|
202
|
+
* {@link toDispose} so the drain catches them on client-detach.
|
|
165
203
|
*/
|
|
166
204
|
@injectable()
|
|
167
205
|
export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
@@ -206,6 +244,25 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
206
244
|
*/
|
|
207
245
|
protected readonly secondarySubscriptions = new Map<string, Disposable>();
|
|
208
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Documents that left the write set since the last save. They stay open in
|
|
249
|
+
* the diagram's session until that save has persisted them, and are closed
|
|
250
|
+
* after it: closing a document as it leaves would revert the diagram's
|
|
251
|
+
* unsaved edits to it where the diagram was its last client.
|
|
252
|
+
*/
|
|
253
|
+
protected readonly departedSecondaries = new Set<string>();
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* How long a load waits for a client id another participant holds to free
|
|
257
|
+
* before refusing. A reloaded client can reconnect under its old id before
|
|
258
|
+
* the server has noticed the old connection close, which ends that
|
|
259
|
+
* connection's sessions a moment later.
|
|
260
|
+
*/
|
|
261
|
+
protected readonly sessionWaitMs: number = 2_000;
|
|
262
|
+
|
|
263
|
+
/** Set by {@link dispose}; a disposed storage registers no session again. */
|
|
264
|
+
protected disposed = false;
|
|
265
|
+
|
|
209
266
|
/**
|
|
210
267
|
* Whether an external resubmit has ever been dispatched. Gates the
|
|
211
268
|
* keep-the-last-valid-GModel branch, which needs to know a canvas is already
|
|
@@ -235,6 +292,7 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
235
292
|
if (this.state.clientId === TEMPORARY_CLIENT_ID) {
|
|
236
293
|
return;
|
|
237
294
|
}
|
|
295
|
+
this.state.modelSession = this.registerModelSession();
|
|
238
296
|
this.sessionManager.addListener(this, this.state.clientId);
|
|
239
297
|
// Watched for this storage's whole lifetime rather than per load. The load
|
|
240
298
|
// flow is a documented override point, so a subscription parked there would
|
|
@@ -243,6 +301,102 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
243
301
|
this.toDispose.push(this.state.onSecondaryUrisChanged(() => this.reconcileSecondarySubscriptions()));
|
|
244
302
|
}
|
|
245
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Register the diagram's client session under the GLSP client id, or
|
|
306
|
+
* `undefined` when another participant holds that id, which is logged.
|
|
307
|
+
*
|
|
308
|
+
* The diagram then refuses to load ({@link requireModelSession}). Taking
|
|
309
|
+
* the id over would end the other participant's session under it, and
|
|
310
|
+
* working without a session would share that participant's opens and
|
|
311
|
+
* echoes, so its close would take the diagram's documents with it.
|
|
312
|
+
*/
|
|
313
|
+
protected registerModelSession(): ModelClientSession<AstNode> | undefined {
|
|
314
|
+
try {
|
|
315
|
+
return this.sharedServices.model.ModelService.createSession('diagram', this.state.clientId);
|
|
316
|
+
} catch (error: unknown) {
|
|
317
|
+
if (!isDuplicateClientIdError(error)) {
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
this.logger.warn(`Client id ${this.state.clientId} is held by another participant: this diagram has no session yet`);
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The diagram's client session, registering it again when the id was held
|
|
327
|
+
* earlier. Throws `SessionClosedError` once the storage is disposed, and the
|
|
328
|
+
* refusal when the id is still held.
|
|
329
|
+
*/
|
|
330
|
+
protected requireModelSession(): ModelClientSession<AstNode> {
|
|
331
|
+
if (this.disposed) {
|
|
332
|
+
throw new SessionClosedError(
|
|
333
|
+
this.state.clientId,
|
|
334
|
+
`The diagram of ${this.state.clientId} has closed; its client session has ended`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (!this.state.modelSession) {
|
|
338
|
+
this.state.modelSession = this.registerModelSession();
|
|
339
|
+
}
|
|
340
|
+
const session = this.state.modelSession;
|
|
341
|
+
if (!session) {
|
|
342
|
+
throw new GLSPServerError(
|
|
343
|
+
this.sharedServices.MessageRenderer.renderMessage(DIAGRAM_SESSION_REFUSED),
|
|
344
|
+
`clientId=${this.state.clientId} is still held by another participant, so this diagram has no client session`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return session;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* {@link requireModelSession} for the load: while the id is held, wait up to
|
|
352
|
+
* {@link sessionWaitMs} for its holder to end before refusing, and tell the
|
|
353
|
+
* user when it does not.
|
|
354
|
+
*/
|
|
355
|
+
protected async awaitModelSession(): Promise<ModelClientSession<AstNode>> {
|
|
356
|
+
if (!this.state.modelSession && !this.disposed) {
|
|
357
|
+
this.state.modelSession = this.registerModelSession();
|
|
358
|
+
}
|
|
359
|
+
if (!this.state.modelSession && !this.disposed) {
|
|
360
|
+
await this.waitForSessionEnd(this.state.clientId, this.sessionWaitMs);
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
return this.requireModelSession();
|
|
364
|
+
} catch (error: unknown) {
|
|
365
|
+
// A diagram closed while it waited has no client left to tell.
|
|
366
|
+
if (isSessionClosedError(error)) {
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
this.logger.error(`Client id ${this.state.clientId} is still held by another participant: this diagram does not load`);
|
|
370
|
+
this.actionDispatcher
|
|
371
|
+
.dispatch(
|
|
372
|
+
MessageAction.create(this.sharedServices.MessageRenderer.renderMessage(DIAGRAM_SESSION_REFUSED), { severity: 'ERROR' })
|
|
373
|
+
)
|
|
374
|
+
.catch((dispatchError: unknown) =>
|
|
375
|
+
this.logger.warn(`Could not tell the client the diagram was refused: ${String(dispatchError)}`)
|
|
376
|
+
);
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Resolve once the client session under `clientId` ends, or after `timeoutMs`. */
|
|
382
|
+
protected waitForSessionEnd(clientId: string, timeoutMs: number): Promise<void> {
|
|
383
|
+
return new Promise<void>(resolve => {
|
|
384
|
+
const subscriptions = new DisposableCollection();
|
|
385
|
+
const done = (): void => {
|
|
386
|
+
subscriptions.dispose();
|
|
387
|
+
resolve();
|
|
388
|
+
};
|
|
389
|
+
subscriptions.push(this.sharedServices.Clock.setTimer(done, timeoutMs));
|
|
390
|
+
subscriptions.push(
|
|
391
|
+
this.sharedServices.workspace.TextDocuments.onDidCloseSession(event => {
|
|
392
|
+
if (event.clientId === clientId) {
|
|
393
|
+
done();
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
246
400
|
/**
|
|
247
401
|
* Default load flow — {@link doLoadSourceModel}. Adopters whose load flow
|
|
248
402
|
* needs more than the seams allow override this method.
|
|
@@ -258,8 +412,8 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
258
412
|
|
|
259
413
|
// Open FIRST: a workspace-scanned-but-never-didOpened document rebuilds here,
|
|
260
414
|
// so the capture below settles on a built root rather than a transient
|
|
261
|
-
// mid-rebuild one.
|
|
262
|
-
|
|
415
|
+
// mid-rebuild one. Closed when the session ends.
|
|
416
|
+
await (await this.awaitModelSession()).open(rootUri);
|
|
263
417
|
|
|
264
418
|
// GLSP's sessionDisposed is unreliable on Theia tab-close; dispose on client
|
|
265
419
|
// detach so reopens don't accumulate stale onModelUpdated listeners.
|
|
@@ -339,18 +493,44 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
339
493
|
if (!current.has(uri)) {
|
|
340
494
|
subscription.dispose();
|
|
341
495
|
this.secondarySubscriptions.delete(uri);
|
|
496
|
+
this.departedSecondaries.add(uri);
|
|
342
497
|
}
|
|
343
498
|
}
|
|
344
499
|
for (const uri of current) {
|
|
345
500
|
if (!this.secondarySubscriptions.has(uri)) {
|
|
501
|
+
this.departedSecondaries.delete(uri);
|
|
346
502
|
this.secondarySubscriptions.set(
|
|
347
503
|
uri,
|
|
348
504
|
this.sharedServices.model.ModelService.onModelUpdated(uri, event => this.handleSecondaryUpdated(uri, event))
|
|
349
505
|
);
|
|
506
|
+
this.openSecondary(uri);
|
|
350
507
|
}
|
|
351
508
|
}
|
|
352
509
|
}
|
|
353
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Open a document that joined the write set through the diagram's session,
|
|
513
|
+
* where it stays open until the diagram's next save after it leaves the set,
|
|
514
|
+
* or the diagram's end. A document that does not exist yet is left to the
|
|
515
|
+
* write that creates it, and a failed open is logged: the write that needs
|
|
516
|
+
* the document opens it again and fails there.
|
|
517
|
+
*/
|
|
518
|
+
protected openSecondary(uri: string): void {
|
|
519
|
+
const session = this.state.modelSession;
|
|
520
|
+
if (!session || !this.sharedServices.model.ModelService.getDocument(uri)) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
session.open(uri).catch((error: unknown) => {
|
|
524
|
+
const detail = `Could not open ${uri} for ${this.state.clientId}: ${error instanceof Error ? error.message : String(error)}`;
|
|
525
|
+
// A session ending while the open reads is the diagram closing.
|
|
526
|
+
if (isSessionClosedError(error)) {
|
|
527
|
+
this.logger.debug(detail);
|
|
528
|
+
} else {
|
|
529
|
+
this.logger.warn(detail);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
354
534
|
/**
|
|
355
535
|
* React to a rebuild of a SECONDARY document by resubmitting the diagram, so
|
|
356
536
|
* an external edit to (say) a layout file reaches the canvas.
|
|
@@ -606,23 +786,24 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
606
786
|
}
|
|
607
787
|
|
|
608
788
|
/**
|
|
609
|
-
* Default save flow: persist the store's current text for every document
|
|
610
|
-
* diagram
|
|
789
|
+
* Default save flow: persist the store's current text for every document the
|
|
790
|
+
* diagram's session has open.
|
|
611
791
|
*
|
|
612
792
|
* **A save persists, it does not author.** Every diagram gesture already
|
|
613
|
-
* reached the store through
|
|
793
|
+
* reached the store through the diagram's session, so the store holds the
|
|
614
794
|
* settled text and disk is the only thing behind. Re-serializing from the AST
|
|
615
795
|
* here cannot improve on that text and can only damage it: a serializer
|
|
616
|
-
* normalises formatting and
|
|
617
|
-
*
|
|
618
|
-
*
|
|
796
|
+
* normalises formatting, and the comments the write path carries over keep
|
|
797
|
+
* none of the hand-formatting around them, so a save would reflow a document
|
|
798
|
+
* nothing changed — which is what a pure bounds drag does to the semantic
|
|
799
|
+
* file when only its layout moved.
|
|
619
800
|
*
|
|
620
|
-
* **
|
|
621
|
-
* (`AbstractHydraniumGlspState.trackSecondaryDocument`)
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
* persist.
|
|
801
|
+
* **What is saved is the primary, every tracked secondary
|
|
802
|
+
* (`AbstractHydraniumGlspState.trackSecondaryDocument`) and every document
|
|
803
|
+
* that left the write set since the last save**, so a document the diagram
|
|
804
|
+
* wrote is persisted whether or not it is the one the client named. One the
|
|
805
|
+
* diagram's session does not have open is skipped, even when another client
|
|
806
|
+
* has it open: that client's unsaved edits are not the diagram's to persist.
|
|
626
807
|
*
|
|
627
808
|
* **A save therefore names documents the gesture did not aim at**, which is
|
|
628
809
|
* why an unchanged one is not rewritten even though the user asked for a
|
|
@@ -664,18 +845,33 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
664
845
|
}
|
|
665
846
|
|
|
666
847
|
/**
|
|
667
|
-
* Save the stored text of `primaryUri
|
|
848
|
+
* Save the stored text of `primaryUri`, every tracked secondary and every
|
|
849
|
+
* document that left the write set since the last save, each only where
|
|
850
|
+
* the diagram's session has it open; then close those that left.
|
|
668
851
|
*
|
|
669
852
|
* Deduplicated, because a state that tracks its own primary as a secondary
|
|
670
853
|
* would otherwise save it twice and fire two save notifications for one
|
|
671
|
-
* save.
|
|
672
|
-
*
|
|
854
|
+
* save. Every save is called in one synchronous step, so each takes its text
|
|
855
|
+
* before anything can close a document of the set: saved one after another,
|
|
856
|
+
* a GLSP session ending during the first write closes the rest unsaved.
|
|
673
857
|
*/
|
|
674
858
|
protected async flushWriteSet(primaryUri: string): Promise<void> {
|
|
859
|
+
// Refused like the load: without a session the client id's opens are
|
|
860
|
+
// another participant's, and this would save them.
|
|
861
|
+
this.requireModelSession();
|
|
675
862
|
const documents = this.sharedServices.workspace.AstDocumentManager;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
863
|
+
const textDocuments = this.sharedServices.workspace.TextDocuments;
|
|
864
|
+
const clientId = this.state.clientId;
|
|
865
|
+
// Taken with the targets: a document leaving the set during the writes
|
|
866
|
+
// was not saved by them, and stays open for the next save.
|
|
867
|
+
const departed = [...this.departedSecondaries];
|
|
868
|
+
const targets = [...new Set([primaryUri, ...this.state.secondaryUris, ...departed])].filter(uri =>
|
|
869
|
+
textDocuments.isOpenInClient(uri, clientId)
|
|
870
|
+
);
|
|
871
|
+
await Promise.all(targets.map(target => documents.save(target, clientId)));
|
|
872
|
+
for (const uri of departed) {
|
|
873
|
+
if (this.departedSecondaries.delete(uri) && uri !== this.state.sourceUri) {
|
|
874
|
+
await this.state.modelSession?.close(uri);
|
|
679
875
|
}
|
|
680
876
|
}
|
|
681
877
|
}
|
|
@@ -789,10 +985,14 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
789
985
|
}
|
|
790
986
|
|
|
791
987
|
/**
|
|
792
|
-
* Cancel any pending resubmit
|
|
988
|
+
* Cancel any pending resubmit, drain every subscription — those registered
|
|
793
989
|
* during {@link doLoadSourceModel} and the per-secondary ones, which live
|
|
794
|
-
* outside {@link toDispose} because they come and go with the write set
|
|
990
|
+
* outside {@link toDispose} because they come and go with the write set —
|
|
991
|
+
* and end the diagram's client session, closing everything it has open.
|
|
795
992
|
* Idempotent.
|
|
993
|
+
*
|
|
994
|
+
* The session ends last, once the detach listener is gone: its closes would
|
|
995
|
+
* otherwise report this storage's own teardown as a client detaching.
|
|
796
996
|
*/
|
|
797
997
|
dispose(): void {
|
|
798
998
|
this.resubmitDebouncer?.dispose();
|
|
@@ -801,5 +1001,8 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
801
1001
|
}
|
|
802
1002
|
this.secondarySubscriptions.clear();
|
|
803
1003
|
this.toDispose.dispose();
|
|
1004
|
+
this.disposed = true;
|
|
1005
|
+
this.state.modelSession?.dispose();
|
|
1006
|
+
this.state.modelSession = undefined;
|
|
804
1007
|
}
|
|
805
1008
|
}
|