@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.
- package/lib/client/data-connection.d.ts +61 -7
- package/lib/client/data-connection.d.ts.map +1 -1
- package/lib/client/data-connection.js +110 -16
- package/lib/client/data-connection.js.map +1 -1
- package/lib/client/data-events.d.ts +5 -1
- package/lib/client/data-events.d.ts.map +1 -1
- package/lib/client/data-events.js +5 -1
- package/lib/client/data-events.js.map +1 -1
- package/lib/client/data-session.d.ts +60 -23
- package/lib/client/data-session.d.ts.map +1 -1
- package/lib/client/data-session.js +97 -23
- package/lib/client/data-session.js.map +1 -1
- package/lib/data/data-server-protocol.d.ts +13 -9
- package/lib/data/data-server-protocol.d.ts.map +1 -1
- package/lib/data/events.d.ts +14 -1
- package/lib/data/events.d.ts.map +1 -1
- package/lib/transfer-document.d.ts +9 -0
- package/lib/transfer-document.d.ts.map +1 -1
- package/lib/transfer-document.js.map +1 -1
- package/package.json +1 -1
- package/src/client/data-connection.ts +162 -44
- package/src/client/data-events.ts +5 -1
- package/src/client/data-session.ts +146 -39
- package/src/data/data-server-protocol.ts +13 -9
- package/src/data/events.ts +14 -1
- package/src/transfer-document.ts +10 -0
|
@@ -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
|
-
/**
|
|
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
|
|
74
|
-
* ignores the `languageId`, `version` and `text` seeds of `OpenModelArgs`,
|
|
75
|
-
*
|
|
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<
|
|
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
|
|
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
|
-
|
|
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
|
|
973
|
-
*
|
|
974
|
-
*
|
|
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
|
|
1002
|
-
*
|
|
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
|
|
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
|
-
|
|
1057
|
-
|
|
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
|
|
1078
|
-
sets.set(
|
|
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>(
|
|
1333
|
-
|
|
1334
|
-
|
|
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
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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<
|
|
198
|
+
watchModelDocument(args: WatchModelDocumentArgs): Promise<TransferWatchedDocument>;
|
|
195
199
|
|
|
196
200
|
/**
|
|
197
201
|
* Stop watching `(uri, clientId)` previously started by
|
package/src/data/events.ts
CHANGED
|
@@ -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`. */
|
package/src/transfer-document.ts
CHANGED
|
@@ -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
|