@hydranium/data-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/README.md +5 -0
- package/lib/data-server.d.ts +94 -53
- package/lib/data-server.d.ts.map +1 -1
- package/lib/data-server.js +178 -135
- package/lib/data-server.js.map +1 -1
- package/package.json +7 -7
- package/src/data-server.ts +212 -135
package/src/data-server.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
isSyntheticSource,
|
|
17
17
|
messageError,
|
|
18
18
|
ReferenceSource,
|
|
19
|
+
SessionClosedError,
|
|
19
20
|
type CloseModelArgs,
|
|
20
21
|
type HydraniumResponseError,
|
|
21
22
|
type Disposable,
|
|
@@ -46,13 +47,17 @@ import {
|
|
|
46
47
|
type StartProfilingArgs,
|
|
47
48
|
type StopProfilingArgs,
|
|
48
49
|
type WriteServerHeapSnapshotArgs,
|
|
50
|
+
type CloseSessionArgs,
|
|
51
|
+
type CreateModelDocumentArgs,
|
|
52
|
+
type CreateSessionArgs,
|
|
49
53
|
type GetModelDocumentArgs,
|
|
50
54
|
type GetProjectForUriArgs,
|
|
51
55
|
type TransferSaveDocumentArgs,
|
|
52
56
|
type WatchModelDocumentArgs,
|
|
53
57
|
type TransferDocumentSavedEvent,
|
|
54
58
|
type TransferDocumentUpdatedEvent,
|
|
55
|
-
type TransferUpdateDocumentArgs
|
|
59
|
+
type TransferUpdateDocumentArgs,
|
|
60
|
+
type TransferUpdateDocumentsArgs
|
|
56
61
|
} from '@hydranium/protocol/data';
|
|
57
62
|
import { REVERT_ON_CLOSE_CLIENT_ID, UNKNOWN_CLIENT_ID } from '@hydranium/core';
|
|
58
63
|
import { defaultDataServerDiagnostics } from './default-diagnostics.js';
|
|
@@ -99,6 +104,8 @@ export const referenceSettleTimeoutError = (elapsedMs: number): HydraniumRespons
|
|
|
99
104
|
|
|
100
105
|
import type { DataServerDiagnosticsProvider, DataServerProfileCapture } from './diagnostics-provider.js';
|
|
101
106
|
import type {
|
|
107
|
+
ClientSession,
|
|
108
|
+
ClientSessionWriteArgs,
|
|
102
109
|
ClientTextDocumentChangeEvent,
|
|
103
110
|
HydraniumLanguageServices,
|
|
104
111
|
LogNameOptions,
|
|
@@ -106,6 +113,7 @@ import type {
|
|
|
106
113
|
ModelService,
|
|
107
114
|
ProjectChangeEvent,
|
|
108
115
|
ServerSharedServices,
|
|
116
|
+
SessionEndCause,
|
|
109
117
|
TransferEncoder
|
|
110
118
|
} from '@hydranium/core';
|
|
111
119
|
import type { TextDocument } from 'vscode-languageserver-textdocument';
|
|
@@ -150,6 +158,18 @@ function fingerprintHash(parts: readonly string[]): string {
|
|
|
150
158
|
return high + low;
|
|
151
159
|
}
|
|
152
160
|
|
|
161
|
+
/**
|
|
162
|
+
* A session a data connection registered with a resume token, and how to end
|
|
163
|
+
* it from another connection. See {@link DataServer.resumableSessions}.
|
|
164
|
+
*/
|
|
165
|
+
export interface ResumableSession {
|
|
166
|
+
readonly token: string;
|
|
167
|
+
/** End the session as its connection closing would. */
|
|
168
|
+
readonly end: () => void;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const resumableByModelService = new WeakMap<object, Map<string, ResumableSession>>();
|
|
172
|
+
|
|
153
173
|
/**
|
|
154
174
|
* Which observable state {@link DataServer.computeDocumentFingerprint} hashes to
|
|
155
175
|
* de-dup `onDocumentUpdated` emissions.
|
|
@@ -410,17 +430,15 @@ export class DataServer<
|
|
|
410
430
|
/** Subscription bookkeeping: URI → set of clientIds that subscribed for that URI. */
|
|
411
431
|
protected readonly subscriptions = new Map<string, Set<string>>();
|
|
412
432
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
433
|
+
* The client sessions this connection registered, by client id. A document
|
|
434
|
+
* request carrying one of these ids acts as that session; one carrying any
|
|
435
|
+
* other id fails.
|
|
415
436
|
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
* suppressed. The store has no notion of which connection a clientId reached
|
|
420
|
-
* it over, so the connection-scoped set has to live here; teaching it a client
|
|
421
|
-
* identity is the more robust and much larger alternative.
|
|
437
|
+
* Entries stay after {@link dispose} has ended their sessions, so a request
|
|
438
|
+
* still running from before the connection closed reaches the ended handle
|
|
439
|
+
* and fails with the session's own error.
|
|
422
440
|
*/
|
|
423
|
-
protected readonly
|
|
441
|
+
protected readonly clientSessions = new Map<string, ClientSession<AstNode, AstDiagnostic, TTransfer>>();
|
|
424
442
|
/** Typed client proxy — sends `data-server/on*` notifications back over the same wire. */
|
|
425
443
|
protected readonly clientProxy: DataClientProtocol<TTransfer, TDiagnostic, TProject>;
|
|
426
444
|
protected readonly disposables = new DisposableCollection();
|
|
@@ -472,12 +490,12 @@ export class DataServer<
|
|
|
472
490
|
*/
|
|
473
491
|
protected readonly lastEmittedFingerprint = new Map<string, string>();
|
|
474
492
|
/**
|
|
475
|
-
* URIs
|
|
476
|
-
* document from its disk content (discarding unsaved
|
|
477
|
-
* but {@link dispatchPhaseEvent} gates on the
|
|
478
|
-
* last close typically also removed the last
|
|
479
|
-
* only ever fetch via `getModelDocument` would
|
|
480
|
-
* discarded state forever. Marked on the
|
|
493
|
+
* URIs the text store just released after their last close. The store
|
|
494
|
+
* rebuilds such a document from its disk content (discarding unsaved
|
|
495
|
+
* in-session edits), but {@link dispatchPhaseEvent} gates on the
|
|
496
|
+
* subscription map — and the last close typically also removed the last
|
|
497
|
+
* watcher, so consumers that only ever fetch via `getModelDocument` would
|
|
498
|
+
* keep showing the discarded state forever. Marked on the release (see
|
|
481
499
|
* {@link subscribeToTextDocumentCloses}) and consumed by
|
|
482
500
|
* {@link dispatchPhaseEvent}, which broadcasts the following rebuild's
|
|
483
501
|
* phase event even without a subscription — de-duplicated against
|
|
@@ -556,8 +574,10 @@ export class DataServer<
|
|
|
556
574
|
this.subscribeToProjectManager();
|
|
557
575
|
// Self-register teardown so an adopter that keeps no reference to the
|
|
558
576
|
// server still releases per-connection state, with no lifecycle hook of
|
|
559
|
-
// its own.
|
|
560
|
-
|
|
577
|
+
// its own. The client did not close anything itself, so its sessions end
|
|
578
|
+
// as lost and each document it was the last to have open waits out the
|
|
579
|
+
// store's revert grace. See `dispose`.
|
|
580
|
+
this.disposables.push(connection.onClose(() => this.dispose('lost')));
|
|
561
581
|
}
|
|
562
582
|
|
|
563
583
|
/**
|
|
@@ -565,16 +585,19 @@ export class DataServer<
|
|
|
565
585
|
* handlers and every listener — and clear the subscription map and the
|
|
566
586
|
* per-URI emission-fingerprint cache, so a long-lived shared services
|
|
567
587
|
* bundle does not retain per-connection memory after the connection
|
|
568
|
-
* closes. Also
|
|
569
|
-
*
|
|
570
|
-
*
|
|
588
|
+
* closes. Also ends every session this connection registered, which closes
|
|
589
|
+
* every document it has open: the SHARED store's state rather than this
|
|
590
|
+
* server's, which outlives the connection unless released here.
|
|
571
591
|
*
|
|
572
|
-
* Idempotent: subsequent calls are no-ops.
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
592
|
+
* Idempotent: subsequent calls are no-ops. Runs by itself when the
|
|
593
|
+
* connection closes, so adopters who don't hold a reference still get
|
|
594
|
+
* per-connection cleanup; adopters that DO hold a reference may call
|
|
595
|
+
* `dispose()` directly for early teardown. The sessions end with `cause`:
|
|
596
|
+
* `'closed'` reverts every document whose last open they close at once,
|
|
597
|
+
* and `'lost'`, the connection's own close, lets each wait out the revert
|
|
598
|
+
* grace.
|
|
576
599
|
*/
|
|
577
|
-
dispose(): void {
|
|
600
|
+
dispose(cause: SessionEndCause = 'closed'): void {
|
|
578
601
|
this.disposed = true;
|
|
579
602
|
// Release an in-flight interactive capture so the process-wide inspector
|
|
580
603
|
// singleton is not left active after the connection closes.
|
|
@@ -583,129 +606,184 @@ export class DataServer<
|
|
|
583
606
|
this.activeProfile = undefined;
|
|
584
607
|
void capture.stop({}).catch(() => undefined);
|
|
585
608
|
}
|
|
586
|
-
// AFTER `disposables.dispose()`, deliberately:
|
|
587
|
-
//
|
|
588
|
-
//
|
|
609
|
+
// AFTER `disposables.dispose()`, deliberately: ending a session releases
|
|
610
|
+
// each document it was the last to hold, and this server's own
|
|
611
|
+
// `onDidCloseLastOpen` listener would otherwise mark a revert broadcast
|
|
612
|
+
// for a connection that is already gone.
|
|
589
613
|
this.disposables.dispose();
|
|
590
|
-
this.
|
|
614
|
+
for (const [clientId, session] of this.clientSessions) {
|
|
615
|
+
this.endSession(clientId, session, cause);
|
|
616
|
+
}
|
|
591
617
|
this.subscriptions.clear();
|
|
592
618
|
this.lastEmittedFingerprint.clear();
|
|
593
619
|
this.pendingRevertBroadcasts.clear();
|
|
594
620
|
}
|
|
595
621
|
|
|
596
622
|
// ============================================================
|
|
597
|
-
// DataServerProtocol implementation
|
|
623
|
+
// DataServerProtocol implementation
|
|
598
624
|
// ============================================================
|
|
599
625
|
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
// the wire boundary via
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
async
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
await this.modelService.open(args);
|
|
613
|
-
// Record the hold so `dispose` can release it for a client that never
|
|
614
|
-
// closes. Keyed by the URI as given, because that is what `close` takes.
|
|
615
|
-
let holders = this.openedDocuments.get(args.uri);
|
|
616
|
-
if (!holders) {
|
|
617
|
-
holders = new Set<string>();
|
|
618
|
-
this.openedDocuments.set(args.uri, holders);
|
|
626
|
+
// Reads go to `ModelService`; a request that opens or writes a document
|
|
627
|
+
// goes to the session this connection registered under its `clientId`.
|
|
628
|
+
// Each answer is encoded on the wire boundary via
|
|
629
|
+
// `encoder.astDocumentToTransferDocument`. To change how documents are
|
|
630
|
+
// read, override on the ModelService subclass; to change how they are
|
|
631
|
+
// written, on the session class the bound `ClientSessionFactory` builds.
|
|
632
|
+
|
|
633
|
+
async createSession(args: CreateSessionArgs): Promise<void> {
|
|
634
|
+
// After teardown nothing would ever end a session registered now, and its
|
|
635
|
+
// id would stay taken for the life of the process.
|
|
636
|
+
if (this.disposed) {
|
|
637
|
+
throw new SessionClosedError(args.clientId);
|
|
619
638
|
}
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
|
|
633
|
-
try {
|
|
634
|
-
// Untracked only once the close has actually LANDED. Dropping the
|
|
635
|
-
// record first gives a failed close the same effect as a
|
|
636
|
-
// successful one: the hold survives on the document store while
|
|
637
|
-
// the connection-close drain — the only thing left that would
|
|
638
|
-
// release it — has no record of it.
|
|
639
|
-
await this.modelService.close({ uri: args.uri, clientId: args.clientId });
|
|
640
|
-
this.forgetOpenDocument(args.uri, args.clientId);
|
|
641
|
-
} catch {
|
|
642
|
-
// Swallowed so the caller receives the reason the OPEN failed
|
|
643
|
-
// rather than a secondary one about the cleanup. The record stays
|
|
644
|
-
// behind deliberately, so teardown retries the release.
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
throw error;
|
|
639
|
+
const resumable = this.resumableSessions();
|
|
640
|
+
const previous = resumable.get(args.clientId);
|
|
641
|
+
if (previous && args.resumeToken !== undefined && previous.token === args.resumeToken) {
|
|
642
|
+
previous.end();
|
|
643
|
+
}
|
|
644
|
+
const session = this.modelService.createSession(args.label, args.clientId);
|
|
645
|
+
this.clientSessions.set(args.clientId, session);
|
|
646
|
+
if (args.resumeToken !== undefined) {
|
|
647
|
+
// Taken over by a client registering again after its connection
|
|
648
|
+
// dropped, so the old session ends as lost: the documents it was the
|
|
649
|
+
// last to have open wait out the grace, and the new session's opens
|
|
650
|
+
// find their unsaved text.
|
|
651
|
+
resumable.set(args.clientId, { token: args.resumeToken, end: () => this.endSession(args.clientId, session, 'lost') });
|
|
648
652
|
}
|
|
649
|
-
// Project the authoritative client-facing version onto the open snapshot.
|
|
650
|
-
// `getModelDocument` encodes the freshly-built AST snapshot, whose version
|
|
651
|
-
// is the `LangiumDocument`'s own `textDocument.version`. That lags the
|
|
652
|
-
// multi-client synced version whenever the open seeded the synced document
|
|
653
|
-
// with a caller-supplied version id — and the synced version is what
|
|
654
|
-
// `ModelService.update`'s optimistic-concurrency gate compares
|
|
655
|
-
// `basedOn` against, so reporting the snapshot's would make the
|
|
656
|
-
// caller's first tagged write self-conflict. A genuine concurrent edit
|
|
657
|
-
// still trips the gate and is reconciled by the caller's replay rather
|
|
658
|
-
// than predicted here.
|
|
659
|
-
return { ...document, version: asSnapshotVersion(this.services.workspace.TextDocuments.version(args.uri)) };
|
|
660
653
|
}
|
|
661
654
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
655
|
+
/**
|
|
656
|
+
* End a session this connection registered. The ended handle stays in
|
|
657
|
+
* {@link clientSessions}, as it does after {@link dispose}, so a request for
|
|
658
|
+
* the id still arriving fails with the ended session's own error.
|
|
659
|
+
*/
|
|
660
|
+
async closeSession(args: CloseSessionArgs): Promise<void> {
|
|
661
|
+
const session = this.clientSessions.get(args.clientId);
|
|
662
|
+
if (session) {
|
|
663
|
+
this.endSession(args.clientId, session);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** End `session`, registered here under `clientId`, with `cause`, and drop its watches on this connection. */
|
|
668
|
+
protected endSession(
|
|
669
|
+
clientId: string,
|
|
670
|
+
session: ClientSession<AstNode, AstDiagnostic, TTransfer>,
|
|
671
|
+
cause: SessionEndCause = 'closed'
|
|
672
|
+
): void {
|
|
673
|
+
for (const [uri, watchers] of this.subscriptions) {
|
|
674
|
+
if (watchers.delete(clientId) && watchers.size === 0) {
|
|
675
|
+
this.subscriptions.delete(uri);
|
|
676
|
+
this.lastEmittedFingerprint.delete(uri);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
session.dispose(cause);
|
|
669
680
|
}
|
|
670
681
|
|
|
671
682
|
/**
|
|
672
|
-
*
|
|
673
|
-
*
|
|
683
|
+
* The resumable sessions of this services tree, by client id, across all of
|
|
684
|
+
* its data connections: a client resumes on another connection than the one
|
|
685
|
+
* that registered it. An entry leaves when its session ends, by any path, so
|
|
686
|
+
* it keeps no ended connection alive.
|
|
687
|
+
*
|
|
688
|
+
* A takeover by resume token ends the old session even when its connection
|
|
689
|
+
* is still alive, so two clients that shared an id and its token would end
|
|
690
|
+
* each other's sessions. And the token is a guard against colliding with a
|
|
691
|
+
* session the server has not yet seen end, not a secret: the wire carries no
|
|
692
|
+
* authentication, and a peer that learns the token can end the session.
|
|
674
693
|
*/
|
|
675
|
-
protected
|
|
676
|
-
|
|
677
|
-
if (!
|
|
678
|
-
|
|
694
|
+
protected resumableSessions(): Map<string, ResumableSession> {
|
|
695
|
+
let sessions = resumableByModelService.get(this.modelService);
|
|
696
|
+
if (!sessions) {
|
|
697
|
+
const created = new Map<string, ResumableSession>();
|
|
698
|
+
// Subscribed once per services tree, and never disposed: it lives as
|
|
699
|
+
// long as the text store it listens to.
|
|
700
|
+
this.services.workspace.TextDocuments.onDidCloseSession(event => created.delete(event.clientId));
|
|
701
|
+
resumableByModelService.set(this.modelService, created);
|
|
702
|
+
sessions = created;
|
|
679
703
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
704
|
+
return sessions;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async createModelDocument(args: CreateModelDocumentArgs): Promise<TransferDocument<TTransfer, TDiagnostic>> {
|
|
708
|
+
const session = this.requireSession(args.clientId);
|
|
709
|
+
await session.create(args.uri, args.text);
|
|
710
|
+
return this.openedSnapshot(args.uri, () => session.close(args.uri));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async updateModelDocuments(args: TransferUpdateDocumentsArgs<TTransfer>): Promise<TransferDocument<TTransfer, TDiagnostic>[]> {
|
|
714
|
+
const astDocuments = await this.requireSession(args.clientId).updateAll({
|
|
715
|
+
updates: args.updates.map(update => this.toSessionWrite(update))
|
|
716
|
+
});
|
|
717
|
+
return astDocuments.map(
|
|
718
|
+
astDocument =>
|
|
719
|
+
this.encoder.astDocumentToTransferDocument(astDocument as never) as unknown as TransferDocument<TTransfer, TDiagnostic>
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* The session write a wire write request carries. Only the fields a session
|
|
725
|
+
* takes: a field an adopter adds to a request reaches the session only
|
|
726
|
+
* through an override of this.
|
|
727
|
+
*/
|
|
728
|
+
protected toSessionWrite(request: Omit<TransferUpdateDocumentArgs<TTransfer>, 'clientId'>): ClientSessionWriteArgs<TTransfer> {
|
|
729
|
+
return { uri: request.uri, model: request.model, basedOn: request.basedOn };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* The session this connection registered under `clientId`, which every
|
|
734
|
+
* document request acts as. An id that is not one fails with the
|
|
735
|
+
* closed-session code: opening or writing under it would leave an open that
|
|
736
|
+
* no session end ever closes.
|
|
737
|
+
*/
|
|
738
|
+
protected requireSession(clientId: string): ClientSession<AstNode, AstDiagnostic, TTransfer> {
|
|
739
|
+
const session = this.clientSessions.get(clientId);
|
|
740
|
+
if (!session) {
|
|
741
|
+
throw new SessionClosedError(clientId, `Client session ${clientId} was never registered on this connection`);
|
|
683
742
|
}
|
|
743
|
+
return session;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async openModelDocument(args: Pick<OpenModelArgs, 'uri' | 'clientId' | 'options'>): Promise<TransferDocument<TTransfer, TDiagnostic>> {
|
|
747
|
+
const session = this.requireSession(args.clientId);
|
|
748
|
+
const wasOpen = this.services.workspace.TextDocuments.isOpenInClient(args.uri, args.clientId);
|
|
749
|
+
await session.open(args.uri, args.options);
|
|
750
|
+
// A repeat open keeps the earlier one, which is still in use.
|
|
751
|
+
return this.openedSnapshot(args.uri, wasOpen ? undefined : () => session.close(args.uri));
|
|
684
752
|
}
|
|
685
753
|
|
|
686
754
|
/**
|
|
687
|
-
*
|
|
688
|
-
*
|
|
755
|
+
* The snapshot a session's open or create answers with. When the read
|
|
756
|
+
* fails, `rollback` undoes the open this call made before the failure is
|
|
757
|
+
* rethrown: the caller sees no open, so nothing on its side would ever close
|
|
758
|
+
* it.
|
|
689
759
|
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
* exceptional, so the leak accumulates there.
|
|
696
|
-
*
|
|
697
|
-
* Runs from {@link dispose}, which is synchronous, so each close is fired and
|
|
698
|
-
* its failure swallowed — a teardown must not reject, and a URI whose close
|
|
699
|
-
* fails is no worse off than it was before this drain existed.
|
|
760
|
+
* Versioned with the store's shared version rather than the snapshot's own:
|
|
761
|
+
* the built document's `textDocument.version` lags the store's whenever the
|
|
762
|
+
* store assigned the version, and the store's is what a write's `basedOn` is
|
|
763
|
+
* compared against, so reporting the snapshot's would make the caller's
|
|
764
|
+
* first based-on write conflict with itself.
|
|
700
765
|
*/
|
|
701
|
-
protected
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
766
|
+
protected async openedSnapshot(uri: string, rollback?: () => Promise<void>): Promise<TransferDocument<TTransfer, TDiagnostic>> {
|
|
767
|
+
let document: TransferDocument<TTransfer, TDiagnostic>;
|
|
768
|
+
try {
|
|
769
|
+
document = await this.getModelDocument({ uri });
|
|
770
|
+
} catch (error: unknown) {
|
|
771
|
+
// The open's failure is the one the caller needs, not the cleanup's.
|
|
772
|
+
await rollback?.().catch(() => undefined);
|
|
773
|
+
throw error;
|
|
708
774
|
}
|
|
775
|
+
return { ...document, version: asSnapshotVersion(this.services.workspace.TextDocuments.version(uri)) };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async closeModelDocument(args: CloseModelArgs): Promise<void> {
|
|
779
|
+
// Through the handle, so a close under an ended session fails as that
|
|
780
|
+
// session's calls do, and a session class's own close is honoured.
|
|
781
|
+
const session = this.requireSession(args.clientId);
|
|
782
|
+
// Closing also releases the watch for (uri, clientId) — a forgotten
|
|
783
|
+
// unwatch would otherwise leak phase-event dispatch until the connection
|
|
784
|
+
// closes. Idempotent: a close without a prior watch is a no-op.
|
|
785
|
+
await this.unwatchModelDocument({ uri: args.uri, clientId: args.clientId });
|
|
786
|
+
await session.close(args.uri);
|
|
709
787
|
}
|
|
710
788
|
|
|
711
789
|
async getModelDocument(args: GetModelDocumentArgs): Promise<TransferDocument<TTransfer, TDiagnostic>> {
|
|
@@ -725,12 +803,12 @@ export class DataServer<
|
|
|
725
803
|
}
|
|
726
804
|
|
|
727
805
|
async updateModelDocument(args: TransferUpdateDocumentArgs<TTransfer>): Promise<TransferDocument<TTransfer, TDiagnostic>> {
|
|
728
|
-
const astDocument = await this.
|
|
806
|
+
const astDocument = await this.requireSession(args.clientId).update(this.toSessionWrite(args));
|
|
729
807
|
return this.encoder.astDocumentToTransferDocument(astDocument as never) as unknown as TransferDocument<TTransfer, TDiagnostic>;
|
|
730
808
|
}
|
|
731
809
|
|
|
732
810
|
async saveModelDocument(args: TransferSaveDocumentArgs<TTransfer>): Promise<TransferDocument<TTransfer, TDiagnostic>> {
|
|
733
|
-
const astDocument = await this.
|
|
811
|
+
const astDocument = await this.requireSession(args.clientId).save(this.toSessionWrite(args));
|
|
734
812
|
return this.encoder.astDocumentToTransferDocument(astDocument as never) as unknown as TransferDocument<TTransfer, TDiagnostic>;
|
|
735
813
|
}
|
|
736
814
|
|
|
@@ -1196,18 +1274,17 @@ export class DataServer<
|
|
|
1196
1274
|
}
|
|
1197
1275
|
|
|
1198
1276
|
/**
|
|
1199
|
-
* Mark the
|
|
1200
|
-
* {@link pendingRevertBroadcasts}).
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1277
|
+
* Mark each document the store releases after its last close (see
|
|
1278
|
+
* {@link pendingRevertBroadcasts}). A release, not the close itself: a
|
|
1279
|
+
* document whose last client lost its connection is released only once
|
|
1280
|
+
* the revert grace runs out, or when another client opens it meanwhile, and
|
|
1281
|
+
* not at all when a client lost from it opens it again within its own
|
|
1282
|
+
* grace.
|
|
1203
1283
|
*/
|
|
1204
1284
|
protected subscribeToTextDocumentCloses(): void {
|
|
1205
|
-
const textDocuments = this.services.workspace.TextDocuments;
|
|
1206
1285
|
this.disposables.push(
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
this.pendingRevertBroadcasts.add(this.canonicalKey(event.document.uri));
|
|
1210
|
-
}
|
|
1286
|
+
this.services.workspace.TextDocuments.onDidCloseLastOpen(event => {
|
|
1287
|
+
this.pendingRevertBroadcasts.add(this.canonicalKey(event.uri));
|
|
1211
1288
|
})
|
|
1212
1289
|
);
|
|
1213
1290
|
}
|