@hydranium/protocol 1.0.0-next.285 → 1.0.0-next.288

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.
@@ -59,7 +59,11 @@ export class DataEvents<
59
59
  readonly onDidSaveDocument: Event<TransferDocumentSavedEvent<TTransfer, TDiagnostic>> = this.documentSavedEmitter.event;
60
60
  /** A watched document's text started or stopped differing from its file. */
61
61
  readonly onDidChangeDocumentDirty: Event<TransferDocumentDirtyChangedEvent> = this.documentDirtyChangedEmitter.event;
62
- /** A document's backing file was removed, watched or not. Any watch survives. */
62
+ /**
63
+ * A document's backing file was removed, watched or not. Any watch
64
+ * survives. After a reconnect, also a document a session had open or a
65
+ * watch followed that went while the connection was down.
66
+ */
63
67
  readonly onDidDeleteDocument: Event<TransferDocumentDeletedEvent> = this.documentDeletedEmitter.event;
64
68
  /** Documents built that nobody watches — re-read anything derived from them. */
65
69
  readonly onDidBuildDocuments: Event<TransferDocumentsBuiltEvent> = this.documentsBuiltEmitter.event;
@@ -8,6 +8,7 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  import { type Disposable, Emitter, type Event } from 'vscode-jsonrpc';
11
+ import { UNKNOWN_CLIENT_ID } from '../client-ids';
11
12
  import { type Clock, SystemClock } from '../clock';
12
13
  import type {
13
14
  DataServerProtocol,
@@ -20,7 +21,6 @@ import { isConflictError, SessionClosedError } from '../errors';
20
21
  import type { Logger } from '../logger';
21
22
  import { type BaseVersion, isModelVersion, type ModelVersion, type TextVersion, UNRECORDED_VERSION } from '../model-service/base-version';
22
23
  import { type ResolvedMessage, defineMessage, describeError, resolve } from '../messages/primitives';
23
- import type { OpenModelArgs } from '../model-server';
24
24
  import { NoopLogger } from '../noop-logger';
25
25
  import { randomUuid } from '../random-uuid';
26
26
  import type { MaybePromise } from '../util';
@@ -70,15 +70,24 @@ export type DataSessionArgs<TMethod extends (args: never) => unknown> = Omit<Par
70
70
 
71
71
  /**
72
72
  * Open a document through a session; the session supplies `clientId`. Only the
73
- * URI and the open's options: a session's open reads the file, and the server
74
- * ignores the `languageId`, `version` and `text` seeds of `OpenModelArgs`, so
75
- * accepting them here would let a caller believe they took effect.
73
+ * URI and the open's options, read off the server's signature: the server
74
+ * ignores the `languageId`, `version` and `text` seeds of `OpenModelArgs`, and
75
+ * a field an adopter adds never reaches the server session's open, so
76
+ * accepting either would let a caller believe it took effect.
76
77
  */
77
78
  export type DataSessionOpenArgs<
78
79
  TTransfer extends TransferElement,
79
- // oxlint-disable-next-line no-unused-vars -- TServer validates its own diagnostic constraint; Oxlint does not count that use.
80
80
  TServer extends DataServerProtocol<TTransfer, DiagnosticOf<TServer>> = DataServerProtocol<TTransfer>
81
- > = Pick<OpenModelArgs, 'uri' | 'options'>;
81
+ > = Pick<Parameters<TServer['openModelDocument']>[0], 'uri' | ('options' & keyof Parameters<TServer['openModelDocument']>[0])>;
82
+
83
+ /**
84
+ * The options an open through a session passes to `TServer`, `undefined` when
85
+ * it passed none; only `undefined` for a server whose open takes none.
86
+ */
87
+ export type DataSessionOpenOptions<
88
+ TTransfer extends TransferElement,
89
+ TServer extends DataServerProtocol<TTransfer, DiagnosticOf<TServer>> = DataServerProtocol<TTransfer>
90
+ > = DataSessionOpenArgs<TTransfer, TServer>['options' & keyof DataSessionOpenArgs<TTransfer, TServer>] | undefined;
82
91
 
83
92
  /** Create a document through a session; the session supplies `clientId`. */
84
93
  export type DataSessionCreateArgs<
@@ -167,9 +176,18 @@ export interface DataSessionUnsavedWrite<
167
176
  * that the URI is open accepts an answer from before a close or deletion
168
177
  * once the URI is open again.
169
178
  */
170
- export interface DataSessionOpen {
179
+ export interface DataSessionOpen<
180
+ TTransfer extends TransferElement,
181
+ TServer extends DataServerProtocol<TTransfer, DiagnosticOf<TServer>> = DataServerProtocol<TTransfer>
182
+ > {
171
183
  /** The URI the open's answer named, which the server keys its notifications by. */
172
184
  readonly serverUri: string;
185
+ /**
186
+ * The options the server opened under, which a re-open after a reconnect
187
+ * passes again: those of the earliest open of the URI under way when the
188
+ * first answered, since the server keeps the open that arrives first.
189
+ */
190
+ readonly options: DataSessionOpenOptions<TTransfer, TServer>;
173
191
  /** Whether a deletion ended the open, which an open still watching sends again; a close does not. */
174
192
  deleted: boolean;
175
193
  }
@@ -180,9 +198,14 @@ export interface DataSessionOpen {
180
198
  * nothing and is not sent again, so the close stands. Without the mark, a
181
199
  * deletion during its watch has it sent again after the close.
182
200
  */
183
- export interface DataSessionOpening {
201
+ export interface DataSessionOpening<
202
+ TTransfer extends TransferElement,
203
+ TServer extends DataServerProtocol<TTransfer, DiagnosticOf<TServer>> = DataServerProtocol<TTransfer>
204
+ > {
184
205
  /** Whether a close of the URI came while this open was under way. */
185
206
  closed: boolean;
207
+ /** The options the open was sent with; none for a create. */
208
+ readonly options: DataSessionOpenOptions<TTransfer, TServer>;
186
209
  }
187
210
 
188
211
  /** Where `write` stands in the `updateDocuments` call it was last written by; `0` for a single update. */
@@ -190,6 +213,11 @@ function indexOf(write: DataSessionUnsavedWrite<TransferElement, DataServerProto
190
213
  return 'index' in write.call ? write.call.index : 0;
191
214
  }
192
215
 
216
+ /** The call `write` was last written by, which a restore writes again as one set. */
217
+ function setOf(write: DataSessionUnsavedWrite<TransferElement, DataServerProtocol<TransferElement>>): object {
218
+ return 'update' in write.call ? write.call.update : write.call.updates;
219
+ }
220
+
193
221
  /**
194
222
  * What a {@link DataSession} needs from the connection that minted it.
195
223
  *
@@ -206,6 +234,13 @@ export interface DataSessionHost<TTransfer extends TransferElement, TServer exte
206
234
  * is what the client was last told of the URI since the URI's last open.
207
235
  */
208
236
  restoreDirty?(event: TransferDocumentDirtyChangedEvent): void;
237
+ /**
238
+ * Tell the connection's client of a document a restore found gone, whose
239
+ * deletion reached no one while the connection was down, as the server
240
+ * tells it of a deletion over a live connection. Without it, the restore
241
+ * reports the unsaved text such a document took with it as lost.
242
+ */
243
+ restoreDeletion?(event: TransferDocumentDeletedEvent): void;
209
244
  /**
210
245
  * Forget what the client was told of `uri`'s dirty state: an open just
211
246
  * made answered its caller afresh, and the client heard none of it, or a
@@ -229,7 +264,7 @@ export interface DataSessionHost<TTransfer extends TransferElement, TServer exte
229
264
  /**
230
265
  * Fires with each deletion the server sends. A session forgets a document
231
266
  * whose file was deleted; without this event it keeps it as open until a
232
- * restore finds it gone, and reports its unsaved text lost only then.
267
+ * restore finds it gone.
233
268
  */
234
269
  readonly onDidDeleteDocument?: Event<TransferDocumentDeletedEvent>;
235
270
  /** Where the session logs the documents it forgets as gone. */
@@ -310,13 +345,13 @@ export class DataSession<
310
345
  * still under way, the open it is under. A repeat open keeps it, as the
311
346
  * server keeps its earlier open.
312
347
  */
313
- protected readonly opens = new Map<string, DataSessionOpen>();
348
+ protected readonly opens = new Map<string, DataSessionOpen<TTransfer, TServer>>();
314
349
  /**
315
350
  * Per URI, the opens of it this session has under way. Such a URI counts
316
351
  * as open for {@link withOpenDocument}, which would otherwise close it
317
352
  * under the open that is still being made.
318
353
  */
319
- protected readonly openings = new Map<string, Set<DataSessionOpening>>();
354
+ protected readonly openings = new Map<string, Set<DataSessionOpening<TTransfer, TServer>>>();
320
355
  /**
321
356
  * Per URI, the closes and deletes of it this session has under way, which
322
357
  * an open or create called meanwhile waits for; see {@link trackEnding}.
@@ -379,6 +414,13 @@ export class DataSession<
379
414
  * over a connection that is going away.
380
415
  */
381
416
  protected detached = false;
417
+ /**
418
+ * Write sets, keyed as a restore groups them, that lost a document to
419
+ * another client's deletion. A restore cannot write such a set again
420
+ * whole, so it writes none of it. Weak, so a set goes once no write
421
+ * record holds it.
422
+ */
423
+ protected readonly brokenSets = new WeakSet<object>();
382
424
  /** The subscription to the host's `onDidChangeDirty`, disposed with the session. */
383
425
  protected readonly dirtySubscription?: Disposable;
384
426
  /** The subscription to the host's `onDidUpdateDocument`, disposed with the session. */
@@ -444,7 +486,7 @@ export class DataSession<
444
486
  * from `getModelDocument({ includeDiagnostics: true })`.
445
487
  */
446
488
  openDocument(args: DataSessionOpenArgs<TTransfer, TServer>): Promise<DataSessionDocument<TTransfer, TServer>> {
447
- return this.trackOpen(args.uri, opening =>
489
+ return this.trackOpen(args.uri, args.options, opening =>
448
490
  this.openAndWatch(args.uri, opening, false, server => server.openModelDocument({ ...args, clientId: this.clientId }))
449
491
  );
450
492
  }
@@ -455,7 +497,7 @@ export class DataSession<
455
497
  * refuses a URI that exists on disk or that any client has open.
456
498
  */
457
499
  createDocument(args: DataSessionCreateArgs<TTransfer, TServer>): Promise<DataSessionDocument<TTransfer, TServer>> {
458
- return this.trackOpen(args.uri, opening =>
500
+ return this.trackOpen(args.uri, undefined, opening =>
459
501
  this.openAndWatch(args.uri, opening, true, server => server.createModelDocument({ ...args, clientId: this.clientId }))
460
502
  );
461
503
  }
@@ -475,7 +517,7 @@ export class DataSession<
475
517
  */
476
518
  protected async openAndWatch(
477
519
  uri: string,
478
- opening: DataSessionOpening,
520
+ opening: DataSessionOpening<TTransfer, TServer>,
479
521
  created: boolean,
480
522
  open: (server: RpcProxy<TServer>) => Promise<DataSessionDocument<TTransfer, TServer>>
481
523
  ): Promise<DataSessionDocument<TTransfer, TServer>> {
@@ -516,7 +558,14 @@ export class DataSession<
516
558
  ): Promise<DataSessionDocument<TTransfer, TServer> | undefined> {
517
559
  // Recorded before the watch, so a deletion heard while it is under way
518
560
  // finds this open, which the server made before the deletion.
519
- const open = this.opens.get(uri) ?? { serverUri: document.uri, deleted: false };
561
+ // Under the options of the earliest open still under way, which the
562
+ // server took first; one that failed is gone from `openings` by now,
563
+ // as the connection handles each answer in a task of its own.
564
+ const open = this.opens.get(uri) ?? {
565
+ serverUri: document.uri,
566
+ deleted: false,
567
+ options: [...(this.openings.get(uri) ?? [])].find(opening => !opening.closed)?.options
568
+ };
520
569
  this.opens.set(uri, open);
521
570
  await server.watchModelDocument({ uri, clientId: this.clientId });
522
571
  if (this.opens.get(uri) !== open) {
@@ -562,7 +611,7 @@ export class DataSession<
562
611
  * see {@link hasOpen}: the server holds one open per session and URI, so
563
612
  * the close would end that one too.
564
613
  */
565
- protected async undoOpen(server: RpcProxy<TServer>, uri: string, opening: DataSessionOpening): Promise<void> {
614
+ protected async undoOpen(server: RpcProxy<TServer>, uri: string, opening: DataSessionOpening<TTransfer, TServer>): Promise<void> {
566
615
  if (this.hasOpen(uri, opening)) {
567
616
  return;
568
617
  }
@@ -588,7 +637,7 @@ export class DataSession<
588
637
  * no close has marked, other than `except`. A marked open records nothing,
589
638
  * so nothing can rely on it.
590
639
  */
591
- protected hasOpen(uri: string, except?: DataSessionOpening): boolean {
640
+ protected hasOpen(uri: string, except?: DataSessionOpening<TTransfer, TServer>): boolean {
592
641
  return this.openUris.has(uri) || [...(this.openings.get(uri) ?? [])].some(opening => opening !== except && !opening.closed);
593
642
  }
594
643
 
@@ -918,7 +967,10 @@ export class DataSession<
918
967
  * let a restore re-open the file that is gone. Unwatched rather than
919
968
  * closed: the server keeps the watch, and a close could end a re-open of a
920
969
  * re-created file still in flight. Unsaved writes are logged, not
921
- * reported: the client hears the deletion too.
970
+ * reported: the client hears the deletion too. A document this session
971
+ * created and never saved, deleted by no session, is reported as well: a
972
+ * drop released it. An unsaved write breaks the set it was written with,
973
+ * see {@link brokenSets}.
922
974
  *
923
975
  * This session's own deletion is neither unwatched nor logged as dropping
924
976
  * unsaved text: its delete released the watch, and its caller chose to
@@ -935,7 +987,18 @@ export class DataSession<
935
987
  this.forgetDocument(uri);
936
988
  continue;
937
989
  }
990
+ const write = this.unsavedWrites.get(uri);
991
+ if (write) {
992
+ this.brokenSets.add(setOf(write));
993
+ }
994
+ // Never saved, so no file a watcher could see removed: a deletion
995
+ // naming no session is the release of the text a drop left behind.
996
+ const released = this.createdUris.has(uri) && event.sourceClientId === UNKNOWN_CLIENT_ID;
938
997
  this.forgetGoneDocument(uri);
998
+ if (released) {
999
+ const reported = resolve(DATA_SESSION_UNSAVED_LOST, { uris: uri });
1000
+ this.host.reportError?.(new Error(reported.text), reported);
1001
+ }
939
1002
  this.connected()
940
1003
  .then(server => server.unwatchModelDocument({ uri, clientId: this.clientId }))
941
1004
  .catch(() => undefined);
@@ -969,9 +1032,10 @@ export class DataSession<
969
1032
  }
970
1033
 
971
1034
  /**
972
- * Re-open and re-watch every document the session had open, write again
973
- * what it wrote since their last save where the re-open lost it, and tell
974
- * the host which of them lost it for good.
1035
+ * Re-open, each under the options its {@link DataSessionOpen} holds, and
1036
+ * re-watch every document the session had open, write again what it wrote
1037
+ * since their last save where the re-open lost it, and tell the host which
1038
+ * of them lost it for good.
975
1039
  *
976
1040
  * Decided per document by the re-opened document's `text.hash`,
977
1041
  * in {@link restoreOutcome}. Versions cannot decide a write: a revert moves
@@ -988,7 +1052,7 @@ export class DataSession<
988
1052
  * write, so an edit arriving in between conflicts, and a conflict is not
989
1053
  * retried. Documents last written by one {@link updateDocuments} are sent
990
1054
  * again by one, and only when every one of them may be; a document that
991
- * could not be re-opened blocks its set.
1055
+ * could not be re-opened, or that another client deleted, blocks its set.
992
1056
  *
993
1057
  * No caller is waiting, so the outcomes go through the host: one report
994
1058
  * naming every document whose unsaved text is gone, whose record is then
@@ -998,8 +1062,13 @@ export class DataSession<
998
1062
  * restore to decide again. A document closed while the restore runs, or
999
1063
  * whose file is deleted then, is neither restored nor reported. One gone
1000
1064
  * while the connection was down, which the server reads back without a
1001
- * model, is forgotten, and goes into the report of lost unsaved text when
1002
- * the session wrote it since its last save, or created it and never saved it.
1065
+ * model, is forgotten, and its deletion goes to the host as the deletion
1066
+ * no event brought; unsaved text it took with it is logged, as for a
1067
+ * deletion heard live, and reported as well for a document the session
1068
+ * created and never saved, which the drop released. A host that cannot
1069
+ * tell the client has the unsaved text of either reported. Once the session
1070
+ * is disposed, its caller has ended it, and the restore returns at its
1071
+ * next await: nothing more goes to the server or the host.
1003
1072
  *
1004
1073
  * Each document's dirty state goes to the host too, read once the watch is
1005
1074
  * in place and any write is sent: a flip while the connection was down, or
@@ -1020,7 +1089,11 @@ export class DataSession<
1020
1089
  continue;
1021
1090
  }
1022
1091
  try {
1023
- const document = await server.openModelDocument({ uri, clientId: this.clientId });
1092
+ const options = this.opens.get(uri)?.options;
1093
+ const document = await server.openModelDocument({ uri, clientId: this.clientId, options });
1094
+ if (this.disposed) {
1095
+ return;
1096
+ }
1024
1097
  if (!this.openUris.has(uri)) {
1025
1098
  // Forgotten while it re-opened. A close of the caller's would
1026
1099
  // follow, but a deletion sends none.
@@ -1030,13 +1103,16 @@ export class DataSession<
1030
1103
  // Recorded before the watch, so a deletion heard while it is under
1031
1104
  // way finds the re-open under the URI it answered with, which a
1032
1105
  // restarted server may key differently.
1033
- this.opens.set(uri, { serverUri: document.uri, deleted: false });
1106
+ this.opens.set(uri, { serverUri: document.uri, deleted: false, options });
1034
1107
  await server.watchModelDocument({ uri, clientId: this.clientId }).catch(async (error: unknown) => {
1035
1108
  // Closed, or the server holds the re-open until the session
1036
1109
  // ends; no other call relies on it while the restore runs.
1037
1110
  await server.closeModelDocument({ uri, clientId: this.clientId }).catch(() => undefined);
1038
1111
  throw error;
1039
1112
  });
1113
+ if (this.disposed) {
1114
+ return;
1115
+ }
1040
1116
  if (this.openUris.has(uri)) {
1041
1117
  this.recordText(uri, document);
1042
1118
  }
@@ -1045,22 +1121,42 @@ export class DataSession<
1045
1121
  // Gone while the connection was down, which no deletion told the
1046
1122
  // session of: the server answers a read of it without a model.
1047
1123
  const read = await server.getModelDocument({ uri }).catch(() => undefined);
1124
+ if (this.disposed) {
1125
+ return;
1126
+ }
1048
1127
  // Forgotten meanwhile, so nothing is lost.
1049
1128
  if (!this.openUris.has(uri)) {
1050
1129
  continue;
1051
1130
  }
1052
1131
  if (read && !read.model) {
1053
- // No deletion reached the client either, so unsaved text the
1054
- // document took with it is reported, as a drop's other losses are.
1055
1132
  const write = this.unsavedWrites.get(uri);
1056
- if (this.forgetGoneDocument(uri)) {
1057
- lost.push(uri);
1058
- }
1133
+ // Named as the client last heard it, by the open's answer.
1134
+ const serverUri = this.opens.get(uri)?.serverUri ?? uri;
1135
+ // Never saved, so the drop released it and no one deleted it.
1136
+ const created = this.createdUris.has(uri);
1137
+ const unsaved = this.forgetGoneDocument(uri);
1059
1138
  // Kept for `reapply`, where it blocks its set as a document
1060
1139
  // that could not be re-opened does, and goes.
1061
1140
  if (write) {
1062
1141
  this.unsavedWrites.set(uri, write);
1063
1142
  }
1143
+ if (!this.host.restoreDeletion) {
1144
+ // No client hears of the deletion, so the loss is reported.
1145
+ if (unsaved) {
1146
+ lost.push(uri);
1147
+ }
1148
+ continue;
1149
+ }
1150
+ // The client hears of the deletion, so unsaved text is logged,
1151
+ // as for one heard live; the drop's release is reported too.
1152
+ if (created) {
1153
+ lost.push(uri);
1154
+ }
1155
+ try {
1156
+ this.host.restoreDeletion({ uri: serverUri, sourceClientId: UNKNOWN_CLIENT_ID });
1157
+ } catch {
1158
+ // The client's listener failed, not the restore.
1159
+ }
1064
1160
  continue;
1065
1161
  }
1066
1162
  // Its record goes with its set's, in `reapply`, which it blocks.
@@ -1074,11 +1170,14 @@ export class DataSession<
1074
1170
  }
1075
1171
  const sets = new Map<object, [string, DataSessionUnsavedWrite<TTransfer, TServer>][]>();
1076
1172
  for (const [uri, write] of writes) {
1077
- const call = 'update' in write.call ? write.call.update : write.call.updates;
1078
- sets.set(call, [...(sets.get(call) ?? []), [uri, write]]);
1173
+ const set = setOf(write);
1174
+ sets.set(set, [...(sets.get(set) ?? []), [uri, write]]);
1079
1175
  }
1080
1176
  for (const members of sets.values()) {
1081
1177
  lost.push(...(await this.reapply(server, members, reopened)));
1178
+ if (this.disposed) {
1179
+ return;
1180
+ }
1082
1181
  }
1083
1182
  for (const uri of reopened.keys()) {
1084
1183
  if (!this.openUris.has(uri)) {
@@ -1087,6 +1186,9 @@ export class DataSession<
1087
1186
  // A failed read, or one without text, leaves the client's dirty
1088
1187
  // state as it was; the document is restored all the same.
1089
1188
  const current = await server.getModelDocument({ uri }).catch(() => undefined);
1189
+ if (this.disposed) {
1190
+ return;
1191
+ }
1090
1192
  // Again after the read: one closed or deleted meanwhile has no
1091
1193
  // state to tell.
1092
1194
  if (current?.text !== undefined && this.openUris.has(uri)) {
@@ -1140,7 +1242,8 @@ export class DataSession<
1140
1242
  *
1141
1243
  * A member whose record is no longer the one the restore started with was
1142
1244
  * closed, saved or disposed meanwhile, and is left out: writing it would
1143
- * put back text its caller discarded.
1245
+ * put back text its caller discarded. A set in {@link brokenSets} is
1246
+ * not written again.
1144
1247
  */
1145
1248
  protected async reapply(
1146
1249
  server: RpcProxy<TServer>,
@@ -1172,7 +1275,7 @@ export class DataSession<
1172
1275
  });
1173
1276
  return gone.filter(member => member.outcome === 'lost' || member.outcome === 'resend').map(member => member.uri);
1174
1277
  };
1175
- if (outcomes.some(member => member.outcome === 'lost' || member.outcome === 'failed')) {
1278
+ if (this.brokenSets.has(setOf(members[0][1])) || outcomes.some(member => member.outcome === 'lost' || member.outcome === 'failed')) {
1176
1279
  return lose();
1177
1280
  }
1178
1281
  // In the order of the call's updates, which is the order of its answers.
@@ -1265,7 +1368,7 @@ export class DataSession<
1265
1368
  */
1266
1369
  protected recordWrite(
1267
1370
  uri: string,
1268
- open: DataSessionOpen | undefined,
1371
+ open: DataSessionOpen<TTransfer, TServer> | undefined,
1269
1372
  document: DataSessionDocument<TTransfer, TServer>,
1270
1373
  baseHash: string | undefined,
1271
1374
  call: DataSessionUnsavedWrite<TTransfer, TServer>['call']
@@ -1329,9 +1432,13 @@ export class DataSession<
1329
1432
  * and sent once the closes and deletes of `uri` called before it have
1330
1433
  * answered.
1331
1434
  */
1332
- protected trackOpen<T>(uri: string, call: (opening: DataSessionOpening) => Promise<T>): Promise<T> {
1333
- const opening: DataSessionOpening = { closed: false };
1334
- const openings = this.openings.get(uri) ?? new Set<DataSessionOpening>();
1435
+ protected trackOpen<T>(
1436
+ uri: string,
1437
+ options: DataSessionOpenOptions<TTransfer, TServer>,
1438
+ call: (opening: DataSessionOpening<TTransfer, TServer>) => Promise<T>
1439
+ ): Promise<T> {
1440
+ const opening: DataSessionOpening<TTransfer, TServer> = { closed: false, options };
1441
+ const openings = this.openings.get(uri) ?? new Set<DataSessionOpening<TTransfer, TServer>>();
1335
1442
  this.openings.set(uri, openings.add(opening));
1336
1443
  const done = (): void => {
1337
1444
  openings.delete(opening);
@@ -10,7 +10,7 @@
10
10
  import type { TransferDiagnostic } from '../transfer-diagnostic';
11
11
  import type { TransferElement } from '../transfer-element';
12
12
  import type { Project } from '../project';
13
- import type { TransferDocument, TransferSavedDocument } from '../transfer-document';
13
+ import type { TransferDocument, TransferSavedDocument, TransferWatchedDocument } from '../transfer-document';
14
14
  import type { CloseModelArgs, FindNextNameArgs, OpenModelArgs, ReferenceContext, ReferenceRequest } from '../model-server';
15
15
  import type { ReferenceCandidate, ReferenceTarget } from '../model-service/reference-candidate';
16
16
  import type {
@@ -183,15 +183,19 @@ export interface DocumentServerProtocol<TTransfer extends TransferElement, TDiag
183
183
  * {@link DocumentClientProtocol.onDocumentUpdated} on the paired wire
184
184
  * for every build-phase event on `uri`.
185
185
  *
186
- * Returns `Promise<void>` — the bidirectional pattern means the event
187
- * channel is the client interface, not a returned handle. Callers fan
188
- * out a single inbound `onDocumentUpdated` to multiple local listeners
189
- * with their own `Emitter<T>` (matching Theia's pattern). Pair every
190
- * `watchModelDocument` with {@link unwatchModelDocument} when no longer
191
- * needed; the server holds the watch record until the matching unwatch
192
- * lands.
186
+ * Answers with the key the watch's events name the document by, whether
187
+ * a document exists, taken once the watch is in place and the document
188
+ * is loaded as a read loads it, and the position of the last update or
189
+ * deletion event sent as of that moment, so an event the client heard
190
+ * while the watch was in flight tells by its own position whether it is
191
+ * newer than the answer. No handle: the event channel is the client
192
+ * interface. Callers fan out a single inbound `onDocumentUpdated` to
193
+ * multiple local listeners with their own `Emitter<T>` (matching Theia's
194
+ * pattern). Pair every `watchModelDocument` with
195
+ * {@link unwatchModelDocument} when no longer needed; the server holds the
196
+ * watch record until the matching unwatch lands.
193
197
  */
194
- watchModelDocument(args: WatchModelDocumentArgs): Promise<void>;
198
+ watchModelDocument(args: WatchModelDocumentArgs): Promise<TransferWatchedDocument>;
195
199
 
196
200
  /**
197
201
  * Stop watching `(uri, clientId)` previously started by
@@ -62,6 +62,12 @@ export interface TransferDocumentUpdatedEvent<
62
62
  */
63
63
  sourceClientId: string;
64
64
  reason: TransferDocumentUpdateReason;
65
+ /**
66
+ * The position of this event among the update and deletion events the
67
+ * server sent on this connection, which a watch's answer carries as of
68
+ * the moment it was taken: a greater one was sent later.
69
+ */
70
+ sequence: number;
65
71
  }
66
72
 
67
73
  /** Callback shape for `DataClientProtocol.onDocumentUpdated`. */
@@ -158,9 +164,16 @@ export interface TransferDocumentDeletedEvent {
158
164
  /**
159
165
  * The session whose `deleteModelDocument` removed the file, so it can tell
160
166
  * its own deletion from another's; the unknown-client id for a file
161
- * removed outside any session, as a file watcher reports one.
167
+ * removed outside any session, as a file watcher reports one, and for one
168
+ * a `DataConnection` found gone after a reconnect, which no event named.
162
169
  */
163
170
  readonly sourceClientId: string;
171
+ /**
172
+ * The position of this event among the update and deletion events the
173
+ * server sent on this connection, as on an update; absent on a deletion a
174
+ * `DataConnection` found after a reconnect, which the server never sent.
175
+ */
176
+ readonly sequence?: number;
164
177
  }
165
178
 
166
179
  /** Callback shape for `DataClientProtocol.onDocumentDeleted`. */
@@ -98,6 +98,16 @@ export type TransferSavedDocument<TTransfer extends TransferElement, TDiagnostic
98
98
  persisted: { version: TextVersion };
99
99
  };
100
100
 
101
+ /** What a watch answers, once it is in place. */
102
+ export interface TransferWatchedDocument {
103
+ /** The key the watch's events name the document by, which a symlinked URI does not spell. */
104
+ readonly uri: string;
105
+ /** Whether a document exists there; one the server fails to load counts, only a missing one does not. */
106
+ readonly exists: boolean;
107
+ /** The position of the last update or deletion event sent before `exists` was taken; an event past it was sent after. */
108
+ readonly sequence: number;
109
+ }
110
+
101
111
  export namespace TransferDocument {
102
112
  /**
103
113
  * Whether the server had this document. Branch on it where absence is an