@hydranium/data-server 1.0.0-next.201 → 1.0.0-next.203
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/data-server.d.ts +42 -102
- package/lib/data-server.d.ts.map +1 -1
- package/lib/data-server.js +89 -193
- package/lib/data-server.js.map +1 -1
- package/package.json +7 -7
- package/src/data-server.ts +102 -220
package/src/data-server.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type CloseModelArgs,
|
|
24
24
|
type HydraniumMessageData,
|
|
25
25
|
type HydraniumResponseError,
|
|
26
|
+
type CanonicalUri,
|
|
26
27
|
type Disposable,
|
|
27
28
|
type ElementSource,
|
|
28
29
|
type FindNextNameArgs,
|
|
@@ -120,22 +121,23 @@ import type { DataServerDiagnosticsProvider, DataServerProfileCapture } from './
|
|
|
120
121
|
import type {
|
|
121
122
|
ClientSession,
|
|
122
123
|
ClientSessionWriteArgs,
|
|
123
|
-
ClientTextDocumentChangeEvent,
|
|
124
|
-
DocumentDirtyChangedEvent,
|
|
125
124
|
HydraniumLanguageServices,
|
|
126
125
|
LogNameOptions,
|
|
127
126
|
AstDiagnostic,
|
|
128
127
|
AstDocument,
|
|
128
|
+
AstDocumentSavedEvent,
|
|
129
129
|
EncodedTransferDocument,
|
|
130
|
+
ModelDeletedEvent,
|
|
131
|
+
ModelDirtyChangedEvent,
|
|
130
132
|
ModelService,
|
|
133
|
+
ModelUpdatedEvent,
|
|
131
134
|
ProjectChangeEvent,
|
|
132
135
|
ServerSharedServices,
|
|
133
136
|
SessionEndCause,
|
|
134
137
|
TransferEncoder
|
|
135
138
|
} from '@hydranium/core';
|
|
136
|
-
import type
|
|
137
|
-
import { type
|
|
138
|
-
import { type CancellationToken, type MessageConnection, ResponseError } from 'vscode-jsonrpc';
|
|
139
|
+
import { type AstNode, DocumentState, UriUtils, type URI } from '@hydranium/langium';
|
|
140
|
+
import { type MessageConnection, ResponseError } from 'vscode-jsonrpc';
|
|
139
141
|
|
|
140
142
|
/**
|
|
141
143
|
* Domain separator between text and diagnostics inputs of
|
|
@@ -152,18 +154,6 @@ const FINGERPRINT_SEPARATOR = '\0';
|
|
|
152
154
|
*/
|
|
153
155
|
const SERVER_CANCELLED = -32802;
|
|
154
156
|
|
|
155
|
-
/**
|
|
156
|
-
* A session a data connection registered with a resume token, and how to end
|
|
157
|
-
* it from another connection. See {@link DataServer.resumableSessions}.
|
|
158
|
-
*/
|
|
159
|
-
export interface ResumableSession {
|
|
160
|
-
readonly token: string;
|
|
161
|
-
/** End the session as its connection closing would. */
|
|
162
|
-
readonly end: () => void;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const resumableByModelService = new WeakMap<object, Map<string, ResumableSession>>();
|
|
166
|
-
|
|
167
157
|
/**
|
|
168
158
|
* Which observable state {@link DataServer.computeDocumentFingerprint} hashes to
|
|
169
159
|
* de-dup `onDocumentUpdated` emissions.
|
|
@@ -428,11 +418,11 @@ export interface DataServerUriWatchRecord {
|
|
|
428
418
|
* any names supplied via {@link DataServerOptions.additionalMethods}) and
|
|
429
419
|
* builds the {@link DataClientProtocol} notification proxy on the same
|
|
430
420
|
* connection in one {@link createRpcProxy} call (binding `this` as its
|
|
431
|
-
* `localTarget`) under the configured namespace, and
|
|
432
|
-
* at the configured phase
|
|
433
|
-
* per-document `clientProxy.onDocumentUpdated` for watched URIs
|
|
434
|
-
* `
|
|
435
|
-
*
|
|
421
|
+
* `localTarget`) under the configured namespace, and follows documents
|
|
422
|
+
* through `ModelService`'s events: at the configured phase, `onModelUpdated`
|
|
423
|
+
* dispatches per-document `clientProxy.onDocumentUpdated` for watched URIs and
|
|
424
|
+
* `onModelsBuilt` a single `clientProxy.onDocumentsBuilt` naming the batch's
|
|
425
|
+
* unwatched documents.
|
|
436
426
|
*
|
|
437
427
|
* The data-server requires the full {@link ServerSharedServices}
|
|
438
428
|
* shape — `HydraniumTextDocuments` for client-attributed updates,
|
|
@@ -536,8 +526,8 @@ export class DataServer<
|
|
|
536
526
|
// framework's own rejections.
|
|
537
527
|
renderErrorMessage: error => this.services.MessageRenderer.renderError(error)
|
|
538
528
|
});
|
|
539
|
-
this.disposables.push(this.
|
|
540
|
-
this.
|
|
529
|
+
this.disposables.push(this.modelService.onModelDeleted(event => this.dispatchDeleteEvent(event)));
|
|
530
|
+
this.subscribeToModelUpdates();
|
|
541
531
|
this.subscribeToTextDocumentSaves();
|
|
542
532
|
this.subscribeToDirtyChanges();
|
|
543
533
|
this.subscribeToTextDocumentCloses();
|
|
@@ -577,7 +567,7 @@ export class DataServer<
|
|
|
577
567
|
}
|
|
578
568
|
// AFTER `disposables.dispose()`, deliberately: ending a session releases
|
|
579
569
|
// each document it was the last to hold, and this server's own
|
|
580
|
-
// `
|
|
570
|
+
// `onModelReleased` subscription would otherwise mark a revert broadcast
|
|
581
571
|
// for a connection that is already gone.
|
|
582
572
|
this.disposables.dispose();
|
|
583
573
|
for (const [clientId, session] of this.clientSessions) {
|
|
@@ -603,20 +593,11 @@ export class DataServer<
|
|
|
603
593
|
if (this.disposed) {
|
|
604
594
|
throw new SessionClosedError(args.clientId);
|
|
605
595
|
}
|
|
606
|
-
const
|
|
607
|
-
const previous = resumable.get(args.clientId);
|
|
608
|
-
if (previous && args.resumeToken !== undefined && previous.token === args.resumeToken) {
|
|
609
|
-
previous.end();
|
|
610
|
-
}
|
|
611
|
-
const session = this.modelService.createSession(args.label, args.clientId);
|
|
596
|
+
const session = this.modelService.createSession(args.label, args.clientId, { resumeToken: args.resumeToken });
|
|
612
597
|
this.clientSessions.set(args.clientId, session);
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
// last to have open wait out the grace, and the new session's opens
|
|
617
|
-
// find their unsaved text.
|
|
618
|
-
resumable.set(args.clientId, { token: args.resumeToken, end: () => this.endSession(args.clientId, session, 'lost') });
|
|
619
|
-
}
|
|
598
|
+
// A client registering again on another connection ends this session
|
|
599
|
+
// there, and its watches here would otherwise outlive it.
|
|
600
|
+
session.onDidDispose(() => this.dropWatches(args.clientId));
|
|
620
601
|
}
|
|
621
602
|
|
|
622
603
|
/**
|
|
@@ -637,37 +618,16 @@ export class DataServer<
|
|
|
637
618
|
session: ClientSession<AstNode, AstDiagnostic, TTransfer>,
|
|
638
619
|
cause: SessionEndCause = 'closed'
|
|
639
620
|
): void {
|
|
621
|
+
this.dropWatches(clientId);
|
|
622
|
+
session.dispose(cause);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
protected dropWatches(clientId: string): void {
|
|
640
626
|
for (const [uri, record] of this.uriWatchRecords) {
|
|
641
627
|
if (record.watchers.delete(clientId)) {
|
|
642
628
|
this.pruneUriWatchRecord(uri);
|
|
643
629
|
}
|
|
644
630
|
}
|
|
645
|
-
session.dispose(cause);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* The resumable sessions of this services tree, by client id, across all of
|
|
650
|
-
* its data connections: a client resumes on another connection than the one
|
|
651
|
-
* that registered it. An entry leaves when its session ends, by any path, so
|
|
652
|
-
* it keeps no ended connection alive.
|
|
653
|
-
*
|
|
654
|
-
* A takeover by resume token ends the old session even when its connection
|
|
655
|
-
* is still alive, so two clients that shared an id and its token would end
|
|
656
|
-
* each other's sessions. And the token is a guard against colliding with a
|
|
657
|
-
* session the server has not yet seen end, not a secret: the wire carries no
|
|
658
|
-
* authentication, and a peer that learns the token can end the session.
|
|
659
|
-
*/
|
|
660
|
-
protected resumableSessions(): Map<string, ResumableSession> {
|
|
661
|
-
let sessions = resumableByModelService.get(this.modelService);
|
|
662
|
-
if (!sessions) {
|
|
663
|
-
const created = new Map<string, ResumableSession>();
|
|
664
|
-
// Subscribed once per services tree, and never disposed: it lives as
|
|
665
|
-
// long as the text store it listens to.
|
|
666
|
-
this.services.workspace.TextDocuments.onDidCloseSession(event => created.delete(event.clientId));
|
|
667
|
-
resumableByModelService.set(this.modelService, created);
|
|
668
|
-
sessions = created;
|
|
669
|
-
}
|
|
670
|
-
return sessions;
|
|
671
631
|
}
|
|
672
632
|
|
|
673
633
|
async createModelDocument(args: CreateModelDocumentArgs): Promise<TransferDocument<TTransfer, TDiagnostic>> {
|
|
@@ -801,7 +761,7 @@ export class DataServer<
|
|
|
801
761
|
this.uriWatchRecords.set(uri, record);
|
|
802
762
|
record.watchers.add(args.clientId);
|
|
803
763
|
if (record.fingerprint === undefined) {
|
|
804
|
-
const document = this.
|
|
764
|
+
const document = this.modelService.getDocument(args.uri);
|
|
805
765
|
if (document && !this.services.workspace.ModelLedger.isPlaceholder(document.parseResult.value)) {
|
|
806
766
|
record.fingerprint = this.computeDocumentFingerprint(document.parseResult.value, this.encoder.toTransferDocument(document));
|
|
807
767
|
record.fingerprintVersion = this.services.workspace.ModelLedger.versionOf(document.parseResult.value);
|
|
@@ -856,6 +816,7 @@ export class DataServer<
|
|
|
856
816
|
* LSP head resolves.
|
|
857
817
|
*
|
|
858
818
|
* Adopters can still override for head-specific URI policy by subclassing.
|
|
819
|
+
* The result keys watches only; a document is looked up by its own URI.
|
|
859
820
|
*/
|
|
860
821
|
protected canonicalKey(uri: string): string {
|
|
861
822
|
return this.services.workspace.DocumentUriPolicy.canonicalUri(uri);
|
|
@@ -1019,79 +980,32 @@ export class DataServer<
|
|
|
1019
980
|
}
|
|
1020
981
|
|
|
1021
982
|
/**
|
|
1022
|
-
* Pick the language that owns a reference source
|
|
983
|
+
* Pick the language that owns a reference source: the registry's
|
|
984
|
+
* `getLanguageFor` over the source's declared language, URI and AST type,
|
|
985
|
+
* with an {@link isElementSource} routed by the document that holds the
|
|
986
|
+
* element (see {@link findElementDocumentUri}) before its type, and
|
|
987
|
+
* {@link fallbackReferenceLanguage}, adopter policy, last.
|
|
1023
988
|
*
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1027
|
-
* 1. A URI-bearing source ({@link isDocumentSource}/{@link isSyntheticSource})
|
|
1028
|
-
* whose URI resolves to a registered language routes by URI.
|
|
1029
|
-
* 2. A single-language workspace always routes to that one language — so
|
|
1030
|
-
* single-language adopters never reach the steps below, and never pay
|
|
1031
|
-
* for them.
|
|
1032
|
-
* 3. An {@link isElementSource} source carries no URI, so in a
|
|
1033
|
-
* multi-language workspace it is routed via the document that holds the
|
|
1034
|
-
* element (see {@link findElementDocumentUri}).
|
|
1035
|
-
* 4. A source carrying an AST type is routed by that type when exactly one
|
|
1036
|
-
* registered grammar can produce it (see
|
|
1037
|
-
* {@link resolveReferenceLanguageByType}, over the registry's own type
|
|
1038
|
-
* index). This is what resolves a create-element flow's synthetic source
|
|
1039
|
-
* on a bare directory URI without asking the adopter.
|
|
1040
|
-
* 5. Anything still unresolved falls to {@link fallbackReferenceLanguage},
|
|
1041
|
-
* which is adopter policy.
|
|
989
|
+
* An unregistered declared language falls through rather than failing: the
|
|
990
|
+
* id crosses the wire, so a client built against a head with one more
|
|
991
|
+
* grammar would otherwise lose queries the other signals answer.
|
|
1042
992
|
*/
|
|
1043
993
|
protected resolveReferenceLanguage(source: ReferenceSource): HydraniumLanguageServices | undefined {
|
|
1044
994
|
const registry = this.services.ServiceRegistry;
|
|
1045
|
-
if (isSyntheticSource(source) && source.language !== undefined) {
|
|
1046
|
-
const declared = registry.getServicesById(source.language);
|
|
1047
|
-
if (declared) {
|
|
1048
|
-
return declared;
|
|
1049
|
-
}
|
|
1050
|
-
// Falling through on an UNREGISTERED id rather than failing: the id
|
|
1051
|
-
// crosses the wire, so a client built against a head with one more
|
|
1052
|
-
// grammar would otherwise lose queries the steps below can answer.
|
|
1053
|
-
}
|
|
1054
|
-
// `getServicesFor` (non-throwing, one ladder walk) gates the URI lookup:
|
|
1055
|
-
// an extensionless / unregistered URI falls through to the steps below
|
|
1056
|
-
// rather than throwing "no services for the extension ''".
|
|
1057
|
-
const uri = isDocumentSource(source) || isSyntheticSource(source) ? UriUtils.toUri(source.uri) : undefined;
|
|
1058
|
-
const byUri = uri && registry.getServicesFor(uri);
|
|
1059
|
-
if (byUri) {
|
|
1060
|
-
return byUri;
|
|
1061
|
-
}
|
|
1062
|
-
const all = registry.all;
|
|
1063
|
-
if (all.length === 1) {
|
|
1064
|
-
return all[0];
|
|
1065
|
-
}
|
|
1066
995
|
if (isElementSource(source)) {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
996
|
+
// The index lookup runs only once the cheaper signals abstained.
|
|
997
|
+
return (
|
|
998
|
+
registry.getLanguageFor({}) ??
|
|
999
|
+
registry.getLanguageFor({ target: this.findElementDocumentUri(source), type: source.type }) ??
|
|
1000
|
+
this.fallbackReferenceLanguage(source)
|
|
1001
|
+
);
|
|
1072
1002
|
}
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
* {@link isSyntheticSource}'s `type` (the transient node being created) or an
|
|
1080
|
-
* {@link isElementSource}'s optional narrowing `type`.
|
|
1081
|
-
*
|
|
1082
|
-
* Answers only when EXACTLY ONE registered grammar can produce the type.
|
|
1083
|
-
* Several can when the type comes from a grammar both import, and then the
|
|
1084
|
-
* type genuinely does not identify a language — that is a fall-through to
|
|
1085
|
-
* adopter policy, not a coin toss. Note this asks which grammar can *produce*
|
|
1086
|
-
* the type, not which mentions it: a grammar that merely cross-references a
|
|
1087
|
-
* type can never hold a node of it.
|
|
1088
|
-
*/
|
|
1089
|
-
protected resolveReferenceLanguageByType(source: ReferenceSource): HydraniumLanguageServices | undefined {
|
|
1090
|
-
const type = isSyntheticSource(source) ? source.type : isElementSource(source) ? source.type : undefined;
|
|
1091
|
-
// The registry owns the type index — it is the one place that knows when
|
|
1092
|
-
// the registered set changed, so unlike a private memo here it cannot go
|
|
1093
|
-
// stale on a language registered after the first lookup.
|
|
1094
|
-
return type ? this.services.ServiceRegistry.soleServicesByType(type) : undefined;
|
|
1003
|
+
const hint = isSyntheticSource(source)
|
|
1004
|
+
? { languageId: source.language, target: source.uri, type: source.type }
|
|
1005
|
+
: isDocumentSource(source)
|
|
1006
|
+
? { target: source.uri }
|
|
1007
|
+
: {};
|
|
1008
|
+
return registry.getLanguageFor(hint) ?? this.fallbackReferenceLanguage(source);
|
|
1095
1009
|
}
|
|
1096
1010
|
|
|
1097
1011
|
/**
|
|
@@ -1101,15 +1015,14 @@ export class DataServer<
|
|
|
1101
1015
|
* disambiguates names that repeat across types (honouring grammar
|
|
1102
1016
|
* subtyping through `AstReflection.isSubtype`).
|
|
1103
1017
|
*
|
|
1104
|
-
* Only reached
|
|
1105
|
-
*
|
|
1106
|
-
* step 2.
|
|
1018
|
+
* Only reached in a multi-language workspace, once the declared language
|
|
1019
|
+
* and the URI have abstained.
|
|
1107
1020
|
*
|
|
1108
1021
|
* Abstains when the name matches elements in more than one DOCUMENT — the
|
|
1109
1022
|
* index spans every language and is filled in build order, so "the
|
|
1110
|
-
* first match" would be file-watch order rather than an answer.
|
|
1111
|
-
* then falls through to the type
|
|
1112
|
-
*
|
|
1023
|
+
* first match" would be file-watch order rather than an answer. Routing
|
|
1024
|
+
* then falls through to the element's type, which abstains on ties in the
|
|
1025
|
+
* same way, and finally to adopter policy.
|
|
1113
1026
|
*/
|
|
1114
1027
|
protected findElementDocumentUri(source: ElementSource): URI | undefined {
|
|
1115
1028
|
const matches = this.services.workspace.IndexManager.getElementsByName(source.name, source.type);
|
|
@@ -1226,24 +1139,15 @@ export class DataServer<
|
|
|
1226
1139
|
}
|
|
1227
1140
|
|
|
1228
1141
|
/**
|
|
1229
|
-
*
|
|
1230
|
-
*
|
|
1231
|
-
*
|
|
1232
|
-
* regardless of subscriber count.
|
|
1142
|
+
* Follow every document at the configured
|
|
1143
|
+
* {@link DataServerOptions.subscriptionPhase}, once per connection rather
|
|
1144
|
+
* than once per watch, so the cost stays flat in the number of watches.
|
|
1233
1145
|
*/
|
|
1234
|
-
protected
|
|
1235
|
-
this.
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
);
|
|
1240
|
-
// The BUILD-phase hook, not the per-document one: this notification is one
|
|
1241
|
-
// message per build rather than one per document, and Langium hands the
|
|
1242
|
-
// whole batch over here. It also does not fire for a cancelled build,
|
|
1243
|
-
// which is what the per-document dispatch has to check by hand.
|
|
1244
|
-
this.disposables.push(
|
|
1245
|
-
this.services.workspace.DocumentBuilder.onBuildPhase(this.options.subscriptionPhase, built => this.dispatchBuiltEvent(built))
|
|
1246
|
-
);
|
|
1146
|
+
protected subscribeToModelUpdates(): void {
|
|
1147
|
+
const phase = this.options.subscriptionPhase;
|
|
1148
|
+
this.disposables.push(this.modelService.onModelUpdated(event => this.dispatchPhaseEvent(event), { phase }));
|
|
1149
|
+
// One message per build rather than one per document.
|
|
1150
|
+
this.disposables.push(this.modelService.onModelsBuilt(event => this.dispatchBuiltEvent(event.uris), { phase }));
|
|
1247
1151
|
}
|
|
1248
1152
|
|
|
1249
1153
|
/**
|
|
@@ -1274,17 +1178,9 @@ export class DataServer<
|
|
|
1274
1178
|
* dependents produces nothing at all, and workspace initialisation produces
|
|
1275
1179
|
* nothing because it does not build to this phase. The ceiling is a
|
|
1276
1180
|
* whole-workspace rebuild at the subscription phase: one message, URIs only.
|
|
1277
|
-
*
|
|
1278
|
-
* The URI is canonicalised for the same reason {@link uriWatchRecords} is keyed
|
|
1279
|
-
* that way, and is untested for the same reason as its twin in
|
|
1280
|
-
* {@link dispatchDeleteEvents}: the builder reports URIs out of its own
|
|
1281
|
-
* store, so a non-canonical one cannot be produced without a fixture
|
|
1282
|
-
* asserting a shape the real system never emits.
|
|
1283
1181
|
*/
|
|
1284
|
-
protected dispatchBuiltEvent(built: readonly
|
|
1285
|
-
const uris = built
|
|
1286
|
-
.map(document => this.canonicalKey(document.uri.toString()))
|
|
1287
|
-
.filter(uri => !this.uriWatchRecords.get(uri)?.watchers.size);
|
|
1182
|
+
protected dispatchBuiltEvent(built: readonly CanonicalUri[]): void {
|
|
1183
|
+
const uris = built.map(uri => this.canonicalKey(uri)).filter(uri => !this.uriWatchRecords.get(uri)?.watchers.size);
|
|
1288
1184
|
if (uris.length === 0) {
|
|
1289
1185
|
return;
|
|
1290
1186
|
}
|
|
@@ -1293,13 +1189,12 @@ export class DataServer<
|
|
|
1293
1189
|
}
|
|
1294
1190
|
|
|
1295
1191
|
/**
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1300
|
-
* or any other client writing through `notifyDidSaveTextDocument`.
|
|
1192
|
+
* Follow every save so a single `onDocumentSaved` wire notification fires
|
|
1193
|
+
* for ANY save of a subscribed URI — regardless of whether the save
|
|
1194
|
+
* originated from the data-server's RPC `saveModelDocument`, the LSP head's
|
|
1195
|
+
* text-editor save, or any other client.
|
|
1301
1196
|
*
|
|
1302
|
-
* Architectural symmetry with {@link
|
|
1197
|
+
* Architectural symmetry with {@link subscribeToModelUpdates}: every
|
|
1303
1198
|
* subscribed client sees every state change to documents they care about,
|
|
1304
1199
|
* regardless of which client triggered it. Firing `onDocumentSaved` only
|
|
1305
1200
|
* from the data-server's own RPC path is a bug, not an optimisation: an
|
|
@@ -1307,12 +1202,12 @@ export class DataServer<
|
|
|
1307
1202
|
* clients, which never clear their dirty state.
|
|
1308
1203
|
*/
|
|
1309
1204
|
protected subscribeToTextDocumentSaves(): void {
|
|
1310
|
-
this.disposables.push(this.
|
|
1205
|
+
this.disposables.push(this.modelService.onModelSaved(event => this.dispatchSaveEvent(event)));
|
|
1311
1206
|
}
|
|
1312
1207
|
|
|
1313
1208
|
/** Relay each change of a document's dirty state; see {@link dispatchDirtyEvent}. */
|
|
1314
1209
|
protected subscribeToDirtyChanges(): void {
|
|
1315
|
-
this.disposables.push(this.
|
|
1210
|
+
this.disposables.push(this.modelService.onDirtyChanged(event => this.dispatchDirtyEvent(event)));
|
|
1316
1211
|
}
|
|
1317
1212
|
|
|
1318
1213
|
/**
|
|
@@ -1320,7 +1215,7 @@ export class DataServer<
|
|
|
1320
1215
|
* {@link dispatchSaveEvent} is: the answer at any one moment travels on
|
|
1321
1216
|
* every document sent, so a client that watches nothing reads it there.
|
|
1322
1217
|
*/
|
|
1323
|
-
protected dispatchDirtyEvent(event:
|
|
1218
|
+
protected dispatchDirtyEvent(event: ModelDirtyChangedEvent): void {
|
|
1324
1219
|
const uri = this.canonicalKey(event.uri);
|
|
1325
1220
|
if (this.uriWatchRecords.get(uri)?.watchers.size) {
|
|
1326
1221
|
this.clientProxy.onDocumentDirtyChanged({ uri, text: event.text });
|
|
@@ -1346,7 +1241,7 @@ export class DataServer<
|
|
|
1346
1241
|
*/
|
|
1347
1242
|
protected subscribeToTextDocumentCloses(): void {
|
|
1348
1243
|
this.disposables.push(
|
|
1349
|
-
this.
|
|
1244
|
+
this.modelService.onModelReleased(event => {
|
|
1350
1245
|
const uri = this.canonicalKey(event.uri);
|
|
1351
1246
|
const record = this.uriWatchRecords.get(uri) ?? { watchers: new Set<string>() };
|
|
1352
1247
|
record.revertPending = true;
|
|
@@ -1375,15 +1270,9 @@ export class DataServer<
|
|
|
1375
1270
|
* in the other direction: a transition that matters enough is delivered
|
|
1376
1271
|
* without a subscription.
|
|
1377
1272
|
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
* That ordering also puts this notification ahead of the `'rebuilt'` events
|
|
1382
|
-
* for the dependents whose references the deletion just broke.
|
|
1383
|
-
*
|
|
1384
|
-
* The `deleted` list arrives already expanded to concrete document URIs —
|
|
1385
|
-
* `deleteDocuments` resolves a directory URI to the documents beneath it — so
|
|
1386
|
-
* no caller has to handle a directory here.
|
|
1273
|
+
* A deletion is announced before the build that follows it, so this goes
|
|
1274
|
+
* out ahead of the `'rebuilt'` events for the dependents whose references
|
|
1275
|
+
* the deletion just broke.
|
|
1387
1276
|
*
|
|
1388
1277
|
* The URI's fingerprint and revert mark are dropped: they describe a
|
|
1389
1278
|
* document that no longer exists. A kept fingerprint is adopted as the
|
|
@@ -1394,36 +1283,29 @@ export class DataServer<
|
|
|
1394
1283
|
* re-subscription, and a client that answers the deletion by closing
|
|
1395
1284
|
* releases the watch through `closeModelDocument` anyway.
|
|
1396
1285
|
*/
|
|
1397
|
-
protected
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
this.pruneUriWatchRecord(uri);
|
|
1407
|
-
}
|
|
1408
|
-
this.clientProxy.onDocumentDeleted({ uri });
|
|
1286
|
+
protected dispatchDeleteEvent(event: ModelDeletedEvent): void {
|
|
1287
|
+
const uri = this.canonicalKey(event.uri);
|
|
1288
|
+
const record = this.uriWatchRecords.get(uri);
|
|
1289
|
+
if (record) {
|
|
1290
|
+
record.fingerprint = undefined;
|
|
1291
|
+
record.fingerprintVersion = undefined;
|
|
1292
|
+
record.revertPending = undefined;
|
|
1293
|
+
record.sentVersion = undefined;
|
|
1294
|
+
this.pruneUriWatchRecord(uri);
|
|
1409
1295
|
}
|
|
1296
|
+
this.clientProxy.onDocumentDeleted({ uri });
|
|
1410
1297
|
}
|
|
1411
1298
|
|
|
1412
1299
|
/** Fan out a save event for the document's URI, gated on the URI's watchers. */
|
|
1413
|
-
protected dispatchSaveEvent(event:
|
|
1414
|
-
// The save event arrives under the CLIENT URI the text store keys by (e.g. a
|
|
1415
|
-
// symlink path S); `uriWatchRecords` and `dispatchPhaseEvent` key by the
|
|
1416
|
-
// CANONICAL identity R. Canonicalize before both the gate and the envelope so
|
|
1417
|
-
// a save of a symlinked file isn't silently dropped (and the envelope resolves
|
|
1418
|
-
// the R-keyed document rather than missing into an empty one).
|
|
1300
|
+
protected dispatchSaveEvent(event: AstDocumentSavedEvent<AstNode>): void {
|
|
1419
1301
|
const uri = this.canonicalKey(event.document.uri);
|
|
1420
1302
|
if (!this.uriWatchRecords.get(uri)?.watchers.size) {
|
|
1421
1303
|
return;
|
|
1422
1304
|
}
|
|
1423
|
-
const response = this.envelope(UriUtils.toUri(uri));
|
|
1305
|
+
const response = this.envelope(UriUtils.toUri(event.document.uri));
|
|
1424
1306
|
const wireEvent: TransferDocumentSavedEvent<TTransfer, TDiagnostic> = {
|
|
1425
1307
|
document: response,
|
|
1426
|
-
sourceClientId: event.
|
|
1308
|
+
sourceClientId: event.sourceClientId
|
|
1427
1309
|
};
|
|
1428
1310
|
this.clientProxy.onDocumentSaved(wireEvent);
|
|
1429
1311
|
}
|
|
@@ -1459,16 +1341,8 @@ export class DataServer<
|
|
|
1459
1341
|
* digest keeps its memory constant in document size; see
|
|
1460
1342
|
* {@link computeDocumentFingerprint} for the inputs.
|
|
1461
1343
|
*/
|
|
1462
|
-
protected dispatchPhaseEvent(
|
|
1463
|
-
|
|
1464
|
-
// Build preempted by a concurrent write lock (or other cancel source) —
|
|
1465
|
-
// the subscription event is stale by the time it would fire. Skip
|
|
1466
|
-
// emission so RPC subscribers don't surface intermediate states.
|
|
1467
|
-
// A pending revert mark is deliberately NOT consumed here: the
|
|
1468
|
-
// follow-up build re-fires this phase and broadcasts then.
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
const uri = document.uri.toString();
|
|
1344
|
+
protected dispatchPhaseEvent(event: ModelUpdatedEvent<AstNode>): void {
|
|
1345
|
+
const uri = this.canonicalKey(event.document.uri);
|
|
1472
1346
|
const record = this.uriWatchRecords.get(uri);
|
|
1473
1347
|
const revertedOnClose = record?.revertPending === true;
|
|
1474
1348
|
if (!record || (record.watchers.size === 0 && !revertedOnClose)) {
|
|
@@ -1484,6 +1358,12 @@ export class DataServer<
|
|
|
1484
1358
|
record.revertPending = undefined;
|
|
1485
1359
|
this.pruneUriWatchRecord(uri);
|
|
1486
1360
|
}
|
|
1361
|
+
// The encoder reads the built document, which the event's holds for this
|
|
1362
|
+
// synchronous run.
|
|
1363
|
+
const document = this.modelService.getDocument(event.document.uri);
|
|
1364
|
+
if (!document) {
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1487
1367
|
const fingerprint = this.computeDocumentFingerprint(document.parseResult.value, this.encoder.toTransferDocument(document));
|
|
1488
1368
|
const stamped = this.services.workspace.ModelLedger.versionOf(document.parseResult.value);
|
|
1489
1369
|
if (record.fingerprint === fingerprint && record.fingerprintVersion === stamped) {
|
|
@@ -1496,22 +1376,24 @@ export class DataServer<
|
|
|
1496
1376
|
}
|
|
1497
1377
|
record.fingerprint = fingerprint;
|
|
1498
1378
|
record.fingerprintVersion = stamped;
|
|
1499
|
-
// The
|
|
1379
|
+
// The event's attribution, so this head names the same client as the
|
|
1500
1380
|
// in-process heads do for one build, except for a version this head
|
|
1501
1381
|
// already sent; see `DataServerUriWatchRecord.sentVersion`.
|
|
1502
|
-
const version = this.
|
|
1382
|
+
const version = this.modelService.isOpen(event.document.uri) ? document.textDocument.version : undefined;
|
|
1503
1383
|
const { reason, sourceClientId } =
|
|
1504
1384
|
version !== undefined && record.sentVersion === version
|
|
1505
1385
|
? { reason: 'rebuilt' as const, sourceClientId: UNKNOWN_CLIENT_ID }
|
|
1506
|
-
:
|
|
1386
|
+
: event;
|
|
1507
1387
|
record.sentVersion = version;
|
|
1508
|
-
const
|
|
1388
|
+
const wireEvent: TransferDocumentUpdatedEvent<TTransfer, TDiagnostic> = {
|
|
1509
1389
|
document: this.envelope(document.uri, fingerprint),
|
|
1510
1390
|
sourceClientId: revertedOnClose ? REVERT_ON_CLOSE_CLIENT_ID : sourceClientId,
|
|
1511
1391
|
reason
|
|
1512
1392
|
};
|
|
1513
|
-
this.tracer
|
|
1514
|
-
|
|
1393
|
+
this.tracer
|
|
1394
|
+
.withUri(uri)
|
|
1395
|
+
.debug(`Emit onDocumentUpdated v${stamped} (reason=${wireEvent.reason}, sourceClientId=${wireEvent.sourceClientId})`);
|
|
1396
|
+
this.clientProxy.onDocumentUpdated(wireEvent);
|
|
1515
1397
|
}
|
|
1516
1398
|
|
|
1517
1399
|
/**
|