@hydranium/data-server 1.0.0-next.135 → 1.0.0-next.140
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 +107 -146
- package/lib/data-server.d.ts.map +1 -1
- package/lib/data-server.js +138 -185
- package/lib/data-server.js.map +1 -1
- package/package.json +7 -7
- package/src/data-server.ts +173 -194
package/lib/data-server.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
|
-
import { createRpcProxy, defineMessage, DisposableCollection, HYDRANIUM_ERROR_CODES, isDocumentSource, isElementSource, isSyntheticSource, messageData, messageError, ReferenceSource, SessionClosedError, TIMED_OUT, TransferDocument, asSnapshotVersion } from '@hydranium/protocol';
|
|
9
|
+
import { createRpcProxy, defineMessage, DisposableCollection, HYDRANIUM_ERROR_CODES, isDocumentSource, isElementSource, isSyntheticSource, messageData, messageError, ReferenceSource, SessionClosedError, TIMED_OUT, TransferDocument, UNKNOWN_CLIENT_ID, asSnapshotVersion } from '@hydranium/protocol';
|
|
10
10
|
import { DATA_SERVER_DIAGNOSTICS_METHODS, DATA_SERVER_PROTOCOL_METHODS, DATA_SERVER_WIRE_PREFIX } from '@hydranium/protocol/data';
|
|
11
|
-
import { REVERT_ON_CLOSE_CLIENT_ID
|
|
11
|
+
import { REVERT_ON_CLOSE_CLIENT_ID } from '@hydranium/core';
|
|
12
12
|
import { defaultDataServerDiagnostics } from './default-diagnostics.js';
|
|
13
13
|
/**
|
|
14
14
|
* Raised when a profiling command arrives with no capture running.
|
|
@@ -160,8 +160,8 @@ export class DataServer {
|
|
|
160
160
|
referenceSettleTimeoutMs: 60_000
|
|
161
161
|
};
|
|
162
162
|
options;
|
|
163
|
-
/**
|
|
164
|
-
|
|
163
|
+
/** This connection's {@link DataServerUriWatchRecord} per canonical URI; see there for when a record lives. */
|
|
164
|
+
uriWatchRecords = new Map();
|
|
165
165
|
/**
|
|
166
166
|
* The client sessions this connection registered, by client id. A document
|
|
167
167
|
* request carrying one of these ids acts as that session; one carrying any
|
|
@@ -175,67 +175,6 @@ export class DataServer {
|
|
|
175
175
|
/** Typed client proxy — sends `data-server/on*` notifications back over the same wire. */
|
|
176
176
|
clientProxy;
|
|
177
177
|
disposables = new DisposableCollection();
|
|
178
|
-
/**
|
|
179
|
-
* Snapshot of the most recent `DocumentBuilder.onUpdate` event. Drives
|
|
180
|
-
* `reason` discrimination on outbound `onDocumentUpdated` notifications:
|
|
181
|
-
* a URI in the `changed` list emits `'changed'`, otherwise `'rebuilt'`
|
|
182
|
-
* (cascade rebuild from a dependent URI's change). The same mechanism
|
|
183
|
-
* `AstDocumentManager.onUpdate` uses on the LSP side, so reason fidelity
|
|
184
|
-
* stays consistent between heads. The `deleted` half is read by
|
|
185
|
-
* {@link dispatchDeleteEvents} rather than by the reason discrimination,
|
|
186
|
-
* a deletion travelling on its own channel.
|
|
187
|
-
*/
|
|
188
|
-
lastBuildUpdate;
|
|
189
|
-
/**
|
|
190
|
-
* Per-URI hash of the last emitted (text + diagnostics) state,
|
|
191
|
-
* used by {@link dispatchPhaseEvent} to suppress duplicate
|
|
192
|
-
* `onDocumentUpdated` notifications for rebuilds that produce no
|
|
193
|
-
* observable change since the last emit.
|
|
194
|
-
*
|
|
195
|
-
* Two scenarios routinely produce such rebuilds:
|
|
196
|
-
* 1. **Refresh-triggered rebuilds.** When a second client attaches to a
|
|
197
|
-
* document already open in another client,
|
|
198
|
-
* `HydraniumTextDocuments.refreshContent` fires `onDidChangeContent`
|
|
199
|
-
* purely to re-trigger the build pipeline — the text and diagnostics
|
|
200
|
-
* are identical to the prior emit.
|
|
201
|
-
* 2. **Cascade rebuilds with no diagnostic delta.** Langium rebuilds a
|
|
202
|
-
* document when a dependency changes; if the rebuild produces the
|
|
203
|
-
* same diagnostics, there is nothing new to communicate to subscribers.
|
|
204
|
-
*
|
|
205
|
-
* Without this filter, both scenarios reach subscribers as wire-side
|
|
206
|
-
* `'changed'` events. A subscriber that interprets `'changed'` as
|
|
207
|
-
* "another client wrote new content" and resets its in-memory root to
|
|
208
|
-
* the server view then misreads the spurious event as a concurrent
|
|
209
|
-
* third-party write and loses the user's edits.
|
|
210
|
-
*
|
|
211
|
-
* Initialised on {@link watchModelDocument} to the current
|
|
212
|
-
* document fingerprint so the FIRST phase event after a fresh
|
|
213
|
-
* subscription is also de-duplicated against the state subscribers
|
|
214
|
-
* obtained via `getModelDocument` — they do not need a redundant phase
|
|
215
|
-
* notification for the state they just fetched.
|
|
216
|
-
*
|
|
217
|
-
* Cleared in {@link unwatchModelDocument} when the last
|
|
218
|
-
* subscriber for a URI leaves so memory does not accumulate.
|
|
219
|
-
*
|
|
220
|
-
* Stored as a cyrb53 hex digest (16 chars per URI) rather than the raw
|
|
221
|
-
* text + diagnostics serialisation so the memory cost is constant in
|
|
222
|
-
* document size. See {@link computeDocumentFingerprint} for the inputs.
|
|
223
|
-
*/
|
|
224
|
-
lastEmittedFingerprint = new Map();
|
|
225
|
-
/**
|
|
226
|
-
* URIs the text store just released after their last close. The store
|
|
227
|
-
* rebuilds such a document from its disk content (discarding unsaved
|
|
228
|
-
* in-session edits), but {@link dispatchPhaseEvent} gates on the
|
|
229
|
-
* subscription map — and the last close typically also removed the last
|
|
230
|
-
* watcher, so consumers that only ever fetch via `getModelDocument` would
|
|
231
|
-
* keep showing the discarded state forever. Marked on the release (see
|
|
232
|
-
* {@link subscribeToTextDocumentCloses}) and consumed by
|
|
233
|
-
* {@link dispatchPhaseEvent}, which broadcasts the following rebuild's
|
|
234
|
-
* phase event even without a subscription — de-duplicated against
|
|
235
|
-
* {@link lastEmittedFingerprint} where an entry survives, so a close
|
|
236
|
-
* whose disk state equals the last emitted state stays silent.
|
|
237
|
-
*/
|
|
238
|
-
pendingRevertBroadcasts = new Set();
|
|
239
178
|
tracer;
|
|
240
179
|
/** The in-flight interactive profile capture, held between {@link startProfiling} and {@link stopProfiling}. */
|
|
241
180
|
activeProfile;
|
|
@@ -286,10 +225,7 @@ export class DataServer {
|
|
|
286
225
|
// framework's own rejections.
|
|
287
226
|
renderErrorMessage: error => this.services.MessageRenderer.renderError(error)
|
|
288
227
|
});
|
|
289
|
-
this.disposables.push(this.services.workspace.DocumentBuilder.onUpdate((
|
|
290
|
-
this.lastBuildUpdate = { changed, deleted };
|
|
291
|
-
this.dispatchDeleteEvents(deleted);
|
|
292
|
-
}));
|
|
228
|
+
this.disposables.push(this.services.workspace.DocumentBuilder.onUpdate((_changed, deleted) => this.dispatchDeleteEvents(deleted)));
|
|
293
229
|
this.subscribeToDocumentBuilder();
|
|
294
230
|
this.subscribeToTextDocumentSaves();
|
|
295
231
|
this.subscribeToDirtyChanges();
|
|
@@ -304,10 +240,9 @@ export class DataServer {
|
|
|
304
240
|
}
|
|
305
241
|
/**
|
|
306
242
|
* Release everything the constructor wired up — the bound protocol
|
|
307
|
-
* handlers and every listener — and clear
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* closes. Also ends every session this connection registered, which closes
|
|
243
|
+
* handlers and every listener — and clear {@link uriWatchRecords}, so a
|
|
244
|
+
* long-lived shared services bundle does not retain per-connection memory
|
|
245
|
+
* after the connection closes. Also ends every session this connection registered, which closes
|
|
311
246
|
* every document it has open: the SHARED store's state rather than this
|
|
312
247
|
* server's, which outlives the connection unless released here.
|
|
313
248
|
*
|
|
@@ -336,9 +271,7 @@ export class DataServer {
|
|
|
336
271
|
for (const [clientId, session] of this.clientSessions) {
|
|
337
272
|
this.endSession(clientId, session, cause);
|
|
338
273
|
}
|
|
339
|
-
this.
|
|
340
|
-
this.lastEmittedFingerprint.clear();
|
|
341
|
-
this.pendingRevertBroadcasts.clear();
|
|
274
|
+
this.uriWatchRecords.clear();
|
|
342
275
|
}
|
|
343
276
|
// ============================================================
|
|
344
277
|
// DataServerProtocol implementation
|
|
@@ -384,10 +317,9 @@ export class DataServer {
|
|
|
384
317
|
}
|
|
385
318
|
/** End `session`, registered here under `clientId`, with `cause`, and drop its watches on this connection. */
|
|
386
319
|
endSession(clientId, session, cause = 'closed') {
|
|
387
|
-
for (const [uri,
|
|
388
|
-
if (watchers.delete(clientId)
|
|
389
|
-
this.
|
|
390
|
-
this.lastEmittedFingerprint.delete(uri);
|
|
320
|
+
for (const [uri, record] of this.uriWatchRecords) {
|
|
321
|
+
if (record.watchers.delete(clientId)) {
|
|
322
|
+
this.pruneUriWatchRecord(uri);
|
|
391
323
|
}
|
|
392
324
|
}
|
|
393
325
|
session.dispose(cause);
|
|
@@ -517,56 +449,63 @@ export class DataServer {
|
|
|
517
449
|
* fan out to the wired `clientProxy.onDocumentUpdated`. The
|
|
518
450
|
* bidirectional pattern means the event channel is the client
|
|
519
451
|
* notification surface, not a returned handle — this method only
|
|
520
|
-
* registers the
|
|
452
|
+
* registers the watcher in {@link uriWatchRecords}.
|
|
521
453
|
*
|
|
522
|
-
* Also baselines the
|
|
523
|
-
* {@link
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
* redundant phase notification for that same state.
|
|
454
|
+
* Also baselines the URI's emission fingerprint (see
|
|
455
|
+
* {@link dispatchPhaseEvent}) to the document's current state when the
|
|
456
|
+
* first watcher for the URI registers. This guarantees that any phase event firing immediately after
|
|
457
|
+
* the watch with no observable change is suppressed — watchers obtain
|
|
458
|
+
* initial state via `getModelDocument` (or {@link openModelDocument}) and
|
|
459
|
+
* do not need a redundant phase notification for that same state.
|
|
529
460
|
*/
|
|
530
461
|
async watchModelDocument(args) {
|
|
531
462
|
const uri = this.canonicalKey(args.uri);
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
subscribers.add(args.clientId);
|
|
538
|
-
if (!this.lastEmittedFingerprint.has(uri)) {
|
|
463
|
+
const record = this.uriWatchRecords.get(uri) ?? { watchers: new Set() };
|
|
464
|
+
this.uriWatchRecords.set(uri, record);
|
|
465
|
+
record.watchers.add(args.clientId);
|
|
466
|
+
if (record.fingerprint === undefined) {
|
|
539
467
|
const document = this.services.workspace.LangiumDocuments.getDocument(UriUtils.toUri(uri));
|
|
540
468
|
if (document) {
|
|
541
|
-
|
|
469
|
+
record.fingerprint = this.computeDocumentFingerprint(document);
|
|
542
470
|
}
|
|
543
471
|
}
|
|
544
472
|
}
|
|
545
473
|
/**
|
|
546
474
|
* Remove a watch for `(uri, clientId)` previously created by
|
|
547
475
|
* {@link watchModelDocument}. Idempotent — unwatching twice is a no-op.
|
|
548
|
-
* Dispatch for `uri` stops once no watchers remain,
|
|
549
|
-
*
|
|
550
|
-
* re-baselines against the then-current document state rather than a
|
|
551
|
-
* stale snapshot from the previous watch.
|
|
476
|
+
* Dispatch for `uri` stops once no watchers remain, and the emission
|
|
477
|
+
* fingerprint goes with the last watcher (see {@link pruneUriWatchRecord}).
|
|
552
478
|
*/
|
|
553
479
|
async unwatchModelDocument(args) {
|
|
554
480
|
const uri = this.canonicalKey(args.uri);
|
|
555
|
-
|
|
556
|
-
|
|
481
|
+
if (this.uriWatchRecords.get(uri)?.watchers.delete(args.clientId)) {
|
|
482
|
+
this.pruneUriWatchRecord(uri);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Drop what `uri` no longer needs: the
|
|
487
|
+
* {@link DataServerUriWatchRecord.fingerprint} once no client watches it,
|
|
488
|
+
* and the whole record once it holds no
|
|
489
|
+
* {@link DataServerUriWatchRecord.revertPending} mark either. Every path
|
|
490
|
+
* that removes a watcher or consumes a mark ends here.
|
|
491
|
+
*/
|
|
492
|
+
pruneUriWatchRecord(uri) {
|
|
493
|
+
const record = this.uriWatchRecords.get(uri);
|
|
494
|
+
if (!record || record.watchers.size > 0) {
|
|
557
495
|
return;
|
|
558
496
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
497
|
+
if (record.revertPending) {
|
|
498
|
+
record.fingerprint = undefined;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
this.uriWatchRecords.delete(uri);
|
|
563
502
|
}
|
|
564
503
|
}
|
|
565
504
|
/**
|
|
566
|
-
* Canonicalise a URI string for use as a
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
505
|
+
* Canonicalise a URI string for use as a {@link uriWatchRecords} key, via the
|
|
506
|
+
* shared `DocumentUriPolicy`. Callers may send non-canonical URIs
|
|
507
|
+
* (drive-letter casing, percent-encoding differences, or a symlink path)
|
|
508
|
+
* over the wire; the dispatch side keys by
|
|
570
509
|
* `document.uri.toString()` from `LangiumDocuments`, so writer keys must
|
|
571
510
|
* canonicalise to the same form or events silently fail to deliver.
|
|
572
511
|
* Routing through the seam (rather than a bare `UriUtils.normalize`)
|
|
@@ -914,7 +853,7 @@ export class DataServer {
|
|
|
914
853
|
*
|
|
915
854
|
* Payload-free on purpose: URIs only, so a client re-reads what it displays
|
|
916
855
|
* rather than being handed transfer documents it did not ask for. That is the
|
|
917
|
-
* property the
|
|
856
|
+
* property the watcher gate protects, and it is preserved here by
|
|
918
857
|
* carrying no document rather than by gating the message.
|
|
919
858
|
*
|
|
920
859
|
* Quiet in the common case. Editing a document that an editor has open leaves
|
|
@@ -923,14 +862,16 @@ export class DataServer {
|
|
|
923
862
|
* nothing because it does not build to this phase. The ceiling is a
|
|
924
863
|
* whole-workspace rebuild at the subscription phase: one message, URIs only.
|
|
925
864
|
*
|
|
926
|
-
* The URI is canonicalised for the same reason
|
|
865
|
+
* The URI is canonicalised for the same reason {@link uriWatchRecords} is keyed
|
|
927
866
|
* that way, and is untested for the same reason as its twin in
|
|
928
867
|
* {@link dispatchDeleteEvents}: the builder reports URIs out of its own
|
|
929
868
|
* store, so a non-canonical one cannot be produced without a fixture
|
|
930
869
|
* asserting a shape the real system never emits.
|
|
931
870
|
*/
|
|
932
871
|
dispatchBuiltEvent(built) {
|
|
933
|
-
const uris = built
|
|
872
|
+
const uris = built
|
|
873
|
+
.map(document => this.canonicalKey(document.uri.toString()))
|
|
874
|
+
.filter(uri => !this.uriWatchRecords.get(uri)?.watchers.size);
|
|
934
875
|
if (uris.length === 0) {
|
|
935
876
|
return;
|
|
936
877
|
}
|
|
@@ -959,27 +900,39 @@ export class DataServer {
|
|
|
959
900
|
this.disposables.push(this.services.workspace.TextDocuments.onDidChangeDirty(event => this.dispatchDirtyEvent(event)));
|
|
960
901
|
}
|
|
961
902
|
/**
|
|
962
|
-
* Send a dirty flip to the connection, gated
|
|
903
|
+
* Send a dirty flip to the connection, gated on the URI's watchers as
|
|
963
904
|
* {@link dispatchSaveEvent} is: the answer at any one moment travels on
|
|
964
905
|
* every document sent, so a client that watches nothing reads it there.
|
|
965
906
|
*/
|
|
966
907
|
dispatchDirtyEvent(event) {
|
|
967
908
|
const uri = this.canonicalKey(event.uri);
|
|
968
|
-
if (this.
|
|
909
|
+
if (this.uriWatchRecords.get(uri)?.watchers.size) {
|
|
969
910
|
this.clientProxy.onDocumentDirtyChanged({ uri, dirty: event.dirty });
|
|
970
911
|
}
|
|
971
912
|
}
|
|
972
913
|
/**
|
|
973
|
-
* Mark each document the store releases after its last close
|
|
974
|
-
* {@link
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
914
|
+
* Mark each document the store releases after its last close, so
|
|
915
|
+
* {@link dispatchPhaseEvent} broadcasts the following rebuild even without
|
|
916
|
+
* a watcher. The store rebuilds such a document from its disk content,
|
|
917
|
+
* discarding unsaved in-session edits, and the last close typically also
|
|
918
|
+
* removed the last watcher: without the broadcast, a consumer that only
|
|
919
|
+
* ever fetches via `getModelDocument` keeps showing the discarded state.
|
|
920
|
+
* The broadcast is de-duplicated against the fingerprint while a watcher
|
|
921
|
+
* holds one, so a close whose disk state equals the last emitted state
|
|
922
|
+
* stays silent; with no watcher there is no fingerprint, and every revert
|
|
923
|
+
* is sent.
|
|
924
|
+
*
|
|
925
|
+
* A release, not the close itself: a document whose last client lost its
|
|
926
|
+
* connection is released only once the revert grace runs out, or when
|
|
927
|
+
* another client opens it meanwhile, and not at all when a client lost
|
|
928
|
+
* from it opens it again within its own grace.
|
|
979
929
|
*/
|
|
980
930
|
subscribeToTextDocumentCloses() {
|
|
981
931
|
this.disposables.push(this.services.workspace.TextDocuments.onDidCloseLastOpen(event => {
|
|
982
|
-
this.
|
|
932
|
+
const uri = this.canonicalKey(event.uri);
|
|
933
|
+
const record = this.uriWatchRecords.get(uri) ?? { watchers: new Set() };
|
|
934
|
+
record.revertPending = true;
|
|
935
|
+
this.uriWatchRecords.set(uri, record);
|
|
983
936
|
}));
|
|
984
937
|
}
|
|
985
938
|
/**
|
|
@@ -988,7 +941,7 @@ export class DataServer {
|
|
|
988
941
|
*
|
|
989
942
|
* **Deliberately ungated, where {@link dispatchPhaseEvent} and
|
|
990
943
|
* {@link dispatchSaveEvent} are gated.** Those two carry a built document and
|
|
991
|
-
* fire on every build, so the
|
|
944
|
+
* fire on every build, so the watcher gate is what keeps bandwidth
|
|
992
945
|
* proportional to what a client asked for. A deletion is neither: it is one
|
|
993
946
|
* URI, it is rare, and it reports the workspace's STRUCTURE rather than a
|
|
994
947
|
* document's content. Gating it forces any consumer that displays the
|
|
@@ -998,7 +951,7 @@ export class DataServer {
|
|
|
998
951
|
* care filters on the URI, which costs it a comparison.
|
|
999
952
|
*
|
|
1000
953
|
* The precedent is the last-close revert broadcast (see
|
|
1001
|
-
* {@link
|
|
954
|
+
* {@link subscribeToTextDocumentCloses}), where the same judgement was already made
|
|
1002
955
|
* in the other direction: a transition that matters enough is delivered
|
|
1003
956
|
* without a subscription.
|
|
1004
957
|
*
|
|
@@ -1012,34 +965,37 @@ export class DataServer {
|
|
|
1012
965
|
* `deleteDocuments` resolves a directory URI to the documents beneath it — so
|
|
1013
966
|
* no caller has to handle a directory here.
|
|
1014
967
|
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
* baseline by the next {@link watchModelDocument} and suppresses the first
|
|
1020
|
-
* emit after the file returns with its previous content.
|
|
968
|
+
* The URI's fingerprint and revert mark are dropped: they describe a
|
|
969
|
+
* document that no longer exists. A kept fingerprint is adopted as the
|
|
970
|
+
* baseline and suppresses the first emit after the file returns with its
|
|
971
|
+
* previous content.
|
|
1021
972
|
*
|
|
1022
|
-
* The
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
973
|
+
* The watchers SURVIVE: a recreated file resumes delivering to them with no
|
|
974
|
+
* re-subscription, and a client that answers the deletion by closing
|
|
975
|
+
* releases the watch through `closeModelDocument` anyway.
|
|
1025
976
|
*/
|
|
1026
977
|
dispatchDeleteEvents(deleted) {
|
|
1027
978
|
for (const removed of deleted) {
|
|
1028
979
|
const uri = this.canonicalKey(removed.toString());
|
|
1029
|
-
this.
|
|
1030
|
-
|
|
980
|
+
const record = this.uriWatchRecords.get(uri);
|
|
981
|
+
if (record) {
|
|
982
|
+
record.fingerprint = undefined;
|
|
983
|
+
record.revertPending = undefined;
|
|
984
|
+
record.sentVersion = undefined;
|
|
985
|
+
this.pruneUriWatchRecord(uri);
|
|
986
|
+
}
|
|
1031
987
|
this.clientProxy.onDocumentDeleted({ uri });
|
|
1032
988
|
}
|
|
1033
989
|
}
|
|
1034
|
-
/** Fan out a save event for the document's URI, gated
|
|
990
|
+
/** Fan out a save event for the document's URI, gated on the URI's watchers. */
|
|
1035
991
|
dispatchSaveEvent(event) {
|
|
1036
992
|
// The save event arrives under the CLIENT URI the text store keys by (e.g. a
|
|
1037
|
-
// symlink path S);
|
|
993
|
+
// symlink path S); `uriWatchRecords` and `dispatchPhaseEvent` key by the
|
|
1038
994
|
// CANONICAL identity R. Canonicalize before both the gate and the envelope so
|
|
1039
995
|
// a save of a symlinked file isn't silently dropped (and the envelope resolves
|
|
1040
996
|
// the R-keyed document rather than missing into an empty one).
|
|
1041
997
|
const uri = this.canonicalKey(event.document.uri);
|
|
1042
|
-
if (!this.
|
|
998
|
+
if (!this.uriWatchRecords.get(uri)?.watchers.size) {
|
|
1043
999
|
return;
|
|
1044
1000
|
}
|
|
1045
1001
|
const response = this.envelope(UriUtils.toUri(uri));
|
|
@@ -1058,13 +1014,25 @@ export class DataServer {
|
|
|
1058
1014
|
* out to multiple local subscribers with an `Emitter<T>` — the
|
|
1059
1015
|
* framework deliberately does NOT promise multi-listener semantics.
|
|
1060
1016
|
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1017
|
+
* An event for a URI no client watches is NOT sent over the wire, so
|
|
1018
|
+
* bandwidth scales with watched URIs rather than with phase events; a
|
|
1019
|
+
* pending revert mark is the exception (see
|
|
1020
|
+
* {@link subscribeToTextDocumentCloses}).
|
|
1021
|
+
*
|
|
1022
|
+
* An event for a rebuild with no observable change since the last emit is
|
|
1023
|
+
* suppressed against the URI's fingerprint. Such rebuilds are routine: a
|
|
1024
|
+
* second client attaching to an open document makes
|
|
1025
|
+
* `HydraniumTextDocuments.refreshContent` fire `onDidChangeContent` purely
|
|
1026
|
+
* to re-trigger the build, and Langium rebuilds a document when a
|
|
1027
|
+
* dependency changes even if its diagnostics come out the same. Sent at a
|
|
1028
|
+
* version this head has not sent, they reach watchers as `'changed'`, and a
|
|
1029
|
+
* watcher that reads `'changed'` as another client's write and resets its
|
|
1030
|
+
* root to the server view loses the user's edits. The first {@link watchModelDocument} baselines the
|
|
1031
|
+
* fingerprint, so the first event after a watch is de-duplicated against
|
|
1032
|
+
* the state the watcher just fetched; it goes with the last watcher, so the
|
|
1033
|
+
* next first watch re-baselines rather than adopting a stale digest. A
|
|
1034
|
+
* cyrb53 digest keeps its memory constant in document size; see
|
|
1035
|
+
* {@link computeDocumentFingerprint} for the inputs.
|
|
1068
1036
|
*/
|
|
1069
1037
|
dispatchPhaseEvent(document, cancelToken) {
|
|
1070
1038
|
if (cancelToken.isCancellationRequested) {
|
|
@@ -1076,16 +1044,23 @@ export class DataServer {
|
|
|
1076
1044
|
return;
|
|
1077
1045
|
}
|
|
1078
1046
|
const uri = document.uri.toString();
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
// the author lookup would yield.
|
|
1083
|
-
const revertedOnClose = this.pendingRevertBroadcasts.delete(uri);
|
|
1084
|
-
if (!this.subscriptions.has(uri) && !revertedOnClose) {
|
|
1047
|
+
const record = this.uriWatchRecords.get(uri);
|
|
1048
|
+
const revertedOnClose = record?.revertPending === true;
|
|
1049
|
+
if (!record || (record.watchers.size === 0 && !revertedOnClose)) {
|
|
1085
1050
|
return;
|
|
1086
1051
|
}
|
|
1052
|
+
if (revertedOnClose) {
|
|
1053
|
+
// Consumed even when watchers exist — the regular dispatch below
|
|
1054
|
+
// serves them, and the mark's attribution is more precise than the
|
|
1055
|
+
// post-close unknown client the manager's attribution would yield.
|
|
1056
|
+
// With no watcher the record goes now, so the fingerprint written
|
|
1057
|
+
// below lands on a detached record and the next first watch
|
|
1058
|
+
// re-baselines instead of adopting it.
|
|
1059
|
+
record.revertPending = undefined;
|
|
1060
|
+
this.pruneUriWatchRecord(uri);
|
|
1061
|
+
}
|
|
1087
1062
|
const fingerprint = this.computeDocumentFingerprint(document);
|
|
1088
|
-
if (
|
|
1063
|
+
if (record.fingerprint === fingerprint) {
|
|
1089
1064
|
// A rebuild that produced no observable change since the last emit —
|
|
1090
1065
|
// suppressed so RPC subscribers don't see a no-op broadcast. Logged at
|
|
1091
1066
|
// debug so a *needed* re-broadcast wrongly suppressed by this dedup
|
|
@@ -1093,11 +1068,19 @@ export class DataServer {
|
|
|
1093
1068
|
this.tracer.withUri(uri).debug(`Suppress onDocumentUpdated v${document.textDocument.version}: fingerprint unchanged`);
|
|
1094
1069
|
return;
|
|
1095
1070
|
}
|
|
1096
|
-
|
|
1071
|
+
record.fingerprint = fingerprint;
|
|
1072
|
+
// The manager's attribution, so this head names the same client as the
|
|
1073
|
+
// in-process heads do for one build, except for a version this head
|
|
1074
|
+
// already sent; see `DataServerUriWatchRecord.sentVersion`.
|
|
1075
|
+
const version = this.services.workspace.AstDocumentManager.isOpen(uri) ? document.textDocument.version : undefined;
|
|
1076
|
+
const { reason, sourceClientId } = version !== undefined && record.sentVersion === version
|
|
1077
|
+
? { reason: 'rebuilt', sourceClientId: UNKNOWN_CLIENT_ID }
|
|
1078
|
+
: this.services.workspace.AstDocumentManager.attributeUpdate(document);
|
|
1079
|
+
record.sentVersion = version;
|
|
1097
1080
|
const event = {
|
|
1098
1081
|
document: this.envelope(document.uri),
|
|
1099
|
-
sourceClientId: revertedOnClose ? REVERT_ON_CLOSE_CLIENT_ID :
|
|
1100
|
-
reason
|
|
1082
|
+
sourceClientId: revertedOnClose ? REVERT_ON_CLOSE_CLIENT_ID : sourceClientId,
|
|
1083
|
+
reason
|
|
1101
1084
|
};
|
|
1102
1085
|
this.tracer
|
|
1103
1086
|
.withUri(uri)
|
|
@@ -1136,23 +1119,6 @@ export class DataServer {
|
|
|
1136
1119
|
additionalFingerprintInputs(_document) {
|
|
1137
1120
|
return [];
|
|
1138
1121
|
}
|
|
1139
|
-
/**
|
|
1140
|
-
* Discriminate the reason for a phase-event-driven update notification.
|
|
1141
|
-
* Uses the most recent `DocumentBuilder.onUpdate` snapshot:
|
|
1142
|
-
* - URI in the `changed` list → `'changed'` (the URI was passed to
|
|
1143
|
-
* `documentBuilder.update(changed, deleted)`, which spans `didChange`
|
|
1144
|
-
* text-document events and programmatic `update([uri], [])` calls).
|
|
1145
|
-
* - Otherwise → `'rebuilt'` (cascade re-derivation: this URI was rebuilt
|
|
1146
|
-
* because something it depends on changed; its own text wasn't flagged).
|
|
1147
|
-
*
|
|
1148
|
-
* `'saved'` is NOT emitted here — saves take the dedicated
|
|
1149
|
-
* `DataClientProtocol.onDocumentSaved` channel, and adopters wanting a
|
|
1150
|
-
* unified stream synthesise it in their bridge layer. A deletion is not a
|
|
1151
|
-
* reason at all; see {@link dispatchDeleteEvents}.
|
|
1152
|
-
*/
|
|
1153
|
-
resolveUpdateReason(uri) {
|
|
1154
|
-
return this.lastBuildUpdate?.changed.some(changed => UriUtils.equals(changed, uri)) ? 'changed' : 'rebuilt';
|
|
1155
|
-
}
|
|
1156
1122
|
/**
|
|
1157
1123
|
* Subscribe to the project tier's change channel and re-fan registry
|
|
1158
1124
|
* diffs into per-project wire notifications. The internal
|
|
@@ -1194,19 +1160,6 @@ export class DataServer {
|
|
|
1194
1160
|
this.clientProxy.onProjectsChanged({ project: entry.snapshot, reason: 'removed' });
|
|
1195
1161
|
}
|
|
1196
1162
|
}
|
|
1197
|
-
/**
|
|
1198
|
-
* Resolve the wire-level `sourceClientId` for `document`'s events — the client
|
|
1199
|
-
* that authored its current version, from the version-author history on
|
|
1200
|
-
* `HydraniumTextDocuments`. The protocol-level counterpart of the internal
|
|
1201
|
-
* `AstDocumentManager.getAuthor`: a framework-internal rebuild has no author,
|
|
1202
|
-
* so this surfaces the {@link UNKNOWN_CLIENT_ID} presentation default. Adopters
|
|
1203
|
-
* that rebuild through non-text-document channels override to derive a
|
|
1204
|
-
* source id of their own.
|
|
1205
|
-
*/
|
|
1206
|
-
resolveSourceClientId(document) {
|
|
1207
|
-
const author = this.services.workspace.TextDocuments.getAuthor(document.textDocument.uri, document.textDocument.version);
|
|
1208
|
-
return author ?? UNKNOWN_CLIENT_ID;
|
|
1209
|
-
}
|
|
1210
1163
|
// --- Diagnostics (DataServerDiagnosticsProtocol) ---------------------------
|
|
1211
1164
|
// Computed in THIS process: the data-server child holds the model store, so
|
|
1212
1165
|
// these snapshots reflect the heap that actually carries the workspace
|