@hydranium/protocol 1.0.0-next.284 → 1.0.0-next.287
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 +32 -8
- package/lib/client/data-session.d.ts.map +1 -1
- package/lib/client/data-session.js +81 -15
- package/lib/client/data-session.js.map +1 -1
- package/lib/data/data-server-protocol.d.ts +23 -14
- 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 +89 -16
- package/src/data/data-server-protocol.ts +23 -14
- package/src/data/events.ts +14 -1
- package/src/transfer-document.ts +10 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import { Disposable, Emitter, type Event } from 'vscode-jsonrpc';
|
|
11
|
-
import { FRAMEWORK_CLIENT_IDS } from '../client-ids';
|
|
11
|
+
import { FRAMEWORK_CLIENT_IDS, UNKNOWN_CLIENT_ID } from '../client-ids';
|
|
12
12
|
import {
|
|
13
13
|
DATA_CLIENT_PROTOCOL_METHODS,
|
|
14
14
|
DATA_SERVER_WIRE_PREFIX,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type TransferDocumentDirtyChangedEvent,
|
|
21
21
|
type TransferDocumentUpdatedEvent
|
|
22
22
|
} from '../data';
|
|
23
|
+
import type { TransferWatchedDocument } from '../transfer-document';
|
|
23
24
|
import { DuplicateClientIdError, ReservedClientIdError } from '../errors';
|
|
24
25
|
import type { Logger } from '../logger';
|
|
25
26
|
import { defineMessage, describeError, describeErrorWithStack, resolve } from '../messages/primitives';
|
|
@@ -49,6 +50,31 @@ export const DATA_CONNECTION_SESSION_RESTORE_FAILED = defineMessage(
|
|
|
49
50
|
'Could not restore a session after reconnecting to the data server: {detail}'
|
|
50
51
|
);
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* A watch {@link DataConnection.watchDocument} keeps, with what the client last
|
|
55
|
+
* heard of its document: a reconnect whose watch answers that no document
|
|
56
|
+
* exists tells the client of a deletion only where it had heard one did.
|
|
57
|
+
*/
|
|
58
|
+
export interface DataConnectionWatch {
|
|
59
|
+
/** The URI as the caller named it, which the watch is sent under. */
|
|
60
|
+
readonly uri: string;
|
|
61
|
+
/** The key a watch of it last answered with, which events name; absent until one answered. */
|
|
62
|
+
readonly serverUri?: string;
|
|
63
|
+
/** Whether the client last heard that the document exists. */
|
|
64
|
+
readonly exists: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What an event heard while a watch was in flight said of its document, and the event's position. */
|
|
68
|
+
export interface DataConnectionHeardEvent {
|
|
69
|
+
readonly exists: boolean;
|
|
70
|
+
readonly sequence: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether the document exists, by the newer of a watch's answer and an event heard while the watch was in flight. */
|
|
74
|
+
function existsAfter(heard: DataConnectionHeardEvent | undefined, answer: TransferWatchedDocument): boolean {
|
|
75
|
+
return heard !== undefined && heard.sequence > answer.sequence ? heard.exists : answer.exists;
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
/** Options for {@link DataConnection}. */
|
|
53
79
|
export interface DataConnectionOptions<
|
|
54
80
|
TTransfer extends TransferElement = TransferElement,
|
|
@@ -143,11 +169,11 @@ export class DataConnection<
|
|
|
143
169
|
/** Backs each session's {@link DataSessionHost.onDidDeleteDocument}. */
|
|
144
170
|
protected readonly documentDeletedEmitter = new Emitter<TransferDocumentDeletedEvent>();
|
|
145
171
|
/**
|
|
146
|
-
* Per id {@link watchDocument} watches under, the
|
|
172
|
+
* Per id {@link watchDocument} watches under, the watch, which
|
|
147
173
|
* {@link generationReady} sends again to every later generation until the
|
|
148
174
|
* watch's handle is disposed.
|
|
149
175
|
*/
|
|
150
|
-
protected readonly watches = new Map<string,
|
|
176
|
+
protected readonly watches = new Map<string, DataConnectionWatch>();
|
|
151
177
|
/**
|
|
152
178
|
* Per id, the URI of a watch whose handle was disposed with no ready
|
|
153
179
|
* generation to unwatch on. {@link generationReady} sends the unwatch to the
|
|
@@ -155,6 +181,19 @@ export class DataConnection<
|
|
|
155
181
|
* watches were sent leaves them on a connection the port may hand back.
|
|
156
182
|
*/
|
|
157
183
|
protected readonly pendingUnwatches = new Map<string, string>();
|
|
184
|
+
/**
|
|
185
|
+
* The keys of the deletions the current generation's reconnect told the
|
|
186
|
+
* client of, so a URI several sessions and watches follow is told once.
|
|
187
|
+
*/
|
|
188
|
+
protected readonly toldDeletions = new Set<string>();
|
|
189
|
+
/**
|
|
190
|
+
* One map per watch in flight, keyed by document, of what the last event
|
|
191
|
+
* heard since the watch was sent said of its existence, and the event's
|
|
192
|
+
* position. The server may take the answer before an event goes out, so
|
|
193
|
+
* an event heard in flight wins over the answer where its position is
|
|
194
|
+
* past the one the answer carries.
|
|
195
|
+
*/
|
|
196
|
+
protected readonly heardInFlight = new Set<Map<string, DataConnectionHeardEvent>>();
|
|
158
197
|
/** The port's logger, or one that logs nothing. */
|
|
159
198
|
protected readonly logger: Logger;
|
|
160
199
|
/** Backs {@link onDidReconnect}. */
|
|
@@ -230,6 +269,8 @@ export class DataConnection<
|
|
|
230
269
|
connected: () => this.connected(),
|
|
231
270
|
reportError: (error, reported) => this.reportError(error, reported),
|
|
232
271
|
restoreDirty: event => this.restoreDirty(event),
|
|
272
|
+
// Without it the session reports the unsaved text it loses.
|
|
273
|
+
restoreDeletion: this.hearsDeletions ? event => this.restoreDeletion(event) : undefined,
|
|
233
274
|
forgetDirty: uri => this.dirtyStates.delete(uri),
|
|
234
275
|
onDidChangeDirty: this.dirtyChangedEmitter.event,
|
|
235
276
|
onDidUpdateDocument: this.documentUpdatedEmitter.event,
|
|
@@ -255,22 +296,28 @@ export class DataConnection<
|
|
|
255
296
|
* for as long as the caller follows it. The watch runs under an id of its
|
|
256
297
|
* own, `label` plus `#` plus a random UUID, and needs no session: a session
|
|
257
298
|
* closing the document would also end its own watch of it. Every later
|
|
258
|
-
* connection that becomes ready gets the watch again
|
|
259
|
-
* unwatches.
|
|
299
|
+
* connection that becomes ready gets the watch again, and tells the client
|
|
300
|
+
* of a deletion while it was down. Disposing the handle unwatches.
|
|
260
301
|
*/
|
|
261
302
|
async watchDocument(uri: string, label = 'watch'): Promise<Disposable> {
|
|
262
303
|
const clientId = `${label}#${randomUuid()}`;
|
|
263
304
|
const server = await this.connected();
|
|
264
305
|
// Kept only from here: kept during the wait, a generation that became
|
|
265
306
|
// ready meanwhile would send the watch a second time.
|
|
266
|
-
this.watches.set(clientId, uri);
|
|
307
|
+
this.watches.set(clientId, { uri, exists: false });
|
|
308
|
+
const heard = new Map<string, DataConnectionHeardEvent>();
|
|
309
|
+
this.heardInFlight.add(heard);
|
|
310
|
+
let answer: TransferWatchedDocument;
|
|
267
311
|
try {
|
|
268
|
-
await server.watchModelDocument({ uri, clientId });
|
|
312
|
+
answer = await server.watchModelDocument({ uri, clientId });
|
|
269
313
|
} catch (error: unknown) {
|
|
270
314
|
this.watches.delete(clientId);
|
|
271
315
|
throw error;
|
|
316
|
+
} finally {
|
|
317
|
+
this.heardInFlight.delete(heard);
|
|
272
318
|
}
|
|
273
319
|
this.logger.debug(`Watch ${uri} as ${clientId}`);
|
|
320
|
+
this.watches.set(clientId, { uri, serverUri: answer.uri, exists: existsAfter(heard.get(answer.uri), answer) });
|
|
274
321
|
return Disposable.create(() => {
|
|
275
322
|
if (!this.watches.delete(clientId)) {
|
|
276
323
|
return;
|
|
@@ -307,9 +354,9 @@ export class DataConnection<
|
|
|
307
354
|
*/
|
|
308
355
|
protected override localTarget(): TClient {
|
|
309
356
|
const client = this.client as unknown as Record<string, unknown>;
|
|
310
|
-
const hearsDirty =
|
|
311
|
-
const hearsUpdates =
|
|
312
|
-
const hearsDeletions =
|
|
357
|
+
const hearsDirty = this.bindsClientMethod('onDocumentDirtyChanged');
|
|
358
|
+
const hearsUpdates = this.bindsClientMethod('onDocumentUpdated');
|
|
359
|
+
const hearsDeletions = this.hearsDeletions;
|
|
313
360
|
if (!hearsDirty && !hearsUpdates && !hearsDeletions) {
|
|
314
361
|
return this.client;
|
|
315
362
|
}
|
|
@@ -329,6 +376,9 @@ export class DataConnection<
|
|
|
329
376
|
if (hearsUpdates) {
|
|
330
377
|
const forward = target.onDocumentUpdated as (event: TransferDocumentUpdatedEvent<TTransfer, DiagnosticOf<TServer>>) => void;
|
|
331
378
|
target.onDocumentUpdated = (event: TransferDocumentUpdatedEvent<TTransfer, DiagnosticOf<TServer>>): void => {
|
|
379
|
+
if (event.document.model) {
|
|
380
|
+
this.markWatches(event.document.uri, true, event.sequence);
|
|
381
|
+
}
|
|
332
382
|
this.documentUpdatedEmitter.fire(event);
|
|
333
383
|
forward(event);
|
|
334
384
|
};
|
|
@@ -336,6 +386,10 @@ export class DataConnection<
|
|
|
336
386
|
if (hearsDeletions) {
|
|
337
387
|
const forward = target.onDocumentDeleted as (event: TransferDocumentDeletedEvent) => void;
|
|
338
388
|
target.onDocumentDeleted = (event: TransferDocumentDeletedEvent): void => {
|
|
389
|
+
// Held gone, so a reconnect tells the client of it no second time.
|
|
390
|
+
// A server sending no position has its deletion count as sent
|
|
391
|
+
// before any answer.
|
|
392
|
+
this.markWatches(event.uri, false, event.sequence ?? 0);
|
|
339
393
|
this.documentDeletedEmitter.fire(event);
|
|
340
394
|
forward(event);
|
|
341
395
|
};
|
|
@@ -343,6 +397,50 @@ export class DataConnection<
|
|
|
343
397
|
return target as unknown as TClient;
|
|
344
398
|
}
|
|
345
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Whether the client binds `onDocumentDeleted`. Without it no deletion
|
|
402
|
+
* reaches the client or the sessions, the server's or one a reconnect
|
|
403
|
+
* found, and a session's restore reports what a gone document took.
|
|
404
|
+
*/
|
|
405
|
+
protected get hearsDeletions(): boolean {
|
|
406
|
+
return this.bindsClientMethod('onDocumentDeleted');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Whether `name` is among the methods bound for the client, and the client implements it. */
|
|
410
|
+
protected bindsClientMethod(name: string): boolean {
|
|
411
|
+
const client = this.client as unknown as Record<string, unknown>;
|
|
412
|
+
return (this.clientMethods as readonly string[]).includes(name) && typeof client[name] === 'function';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Hand the client a deletion a session's restore or a watch sent again
|
|
417
|
+
* found, which no event brought while the connection was down, unless
|
|
418
|
+
* {@link toldDeletions} says this generation told it already. The sessions
|
|
419
|
+
* do not hear it: each finds what it lost in its own restore.
|
|
420
|
+
*/
|
|
421
|
+
protected restoreDeletion(event: TransferDocumentDeletedEvent): void {
|
|
422
|
+
if (this.toldDeletions.has(event.uri)) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
this.toldDeletions.add(event.uri);
|
|
426
|
+
const client = this.client as unknown as Pick<DataClientProtocol<TTransfer>, 'onDocumentDeleted'>;
|
|
427
|
+
client.onDocumentDeleted(event);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Record in every watch whose answer named `uri` whether the document
|
|
432
|
+
* exists, and in {@link heardInFlight} for the watches not answered yet,
|
|
433
|
+
* with the event's position.
|
|
434
|
+
*/
|
|
435
|
+
protected markWatches(uri: string, exists: boolean, sequence: number): void {
|
|
436
|
+
this.watches.forEach((watch, clientId) => {
|
|
437
|
+
if (watch.serverUri === uri) {
|
|
438
|
+
this.watches.set(clientId, { ...watch, exists });
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
this.heardInFlight.forEach(heard => heard.set(uri, { exists, sequence }));
|
|
442
|
+
}
|
|
443
|
+
|
|
346
444
|
/**
|
|
347
445
|
* Hand a dirty state a restore read to the client, unless it is what
|
|
348
446
|
* {@link dirtyStates} says the client was last told; a URI it holds
|
|
@@ -357,8 +455,10 @@ export class DataConnection<
|
|
|
357
455
|
/** Record `event` in {@link dirtyStates} and hand it to the client. */
|
|
358
456
|
protected deliverDirty(event: TransferDocumentDirtyChangedEvent): void {
|
|
359
457
|
this.dirtyStates.set(event.uri, event.text?.dirty ?? false);
|
|
360
|
-
|
|
361
|
-
|
|
458
|
+
if (this.bindsClientMethod('onDocumentDirtyChanged')) {
|
|
459
|
+
const client = this.client as unknown as Pick<DataClientProtocol<TTransfer>, 'onDocumentDirtyChanged'>;
|
|
460
|
+
client.onDocumentDirtyChanged(event);
|
|
461
|
+
}
|
|
362
462
|
}
|
|
363
463
|
|
|
364
464
|
/**
|
|
@@ -414,8 +514,9 @@ export class DataConnection<
|
|
|
414
514
|
*/
|
|
415
515
|
protected override generationReady(generation: RpcConnectionGeneration<TServer>): void {
|
|
416
516
|
super.generationReady(generation);
|
|
517
|
+
this.toldDeletions.clear();
|
|
417
518
|
this.reconnectSessions();
|
|
418
|
-
this.watches.forEach((
|
|
519
|
+
this.watches.forEach((watch, clientId) => this.watchAgain(generation, clientId, watch.uri));
|
|
419
520
|
this.pendingUnwatches.forEach((uri, clientId) => this.unwatch(generation, clientId, uri));
|
|
420
521
|
this.pendingUnwatches.clear();
|
|
421
522
|
if (this.readyBefore) {
|
|
@@ -438,41 +539,58 @@ export class DataConnection<
|
|
|
438
539
|
* `generation` is still current: otherwise the next generation sends it
|
|
439
540
|
* again.
|
|
440
541
|
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
* the
|
|
542
|
+
* A document the client last heard exists that the watch's answer says
|
|
543
|
+
* is gone goes to the client as a deletion; once the watch is in place,
|
|
544
|
+
* the document's dirty state is read and goes through
|
|
545
|
+
* {@link restoreDirty}, as a session's restore does. Neither reached
|
|
546
|
+
* anyone while the connection was down.
|
|
444
547
|
*/
|
|
445
548
|
protected watchAgain(generation: RpcConnectionGeneration<TServer>, clientId: string, uri: string): void {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
549
|
+
const heard = new Map<string, DataConnectionHeardEvent>();
|
|
550
|
+
this.heardInFlight.add(heard);
|
|
551
|
+
generation.server
|
|
552
|
+
.watchModelDocument({ uri, clientId })
|
|
553
|
+
.finally(() => this.heardInFlight.delete(heard))
|
|
554
|
+
.then(
|
|
555
|
+
answer => {
|
|
556
|
+
const known = this.watches.get(clientId);
|
|
557
|
+
if (!known) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
this.logger.debug(`Watch ${uri} again as ${clientId}`);
|
|
561
|
+
const exists = existsAfter(heard.get(answer.uri), answer);
|
|
562
|
+
this.watches.set(clientId, { uri, serverUri: answer.uri, exists });
|
|
563
|
+
if (known.exists && !exists && this.hearsDeletions) {
|
|
564
|
+
try {
|
|
565
|
+
this.restoreDeletion({ uri: answer.uri, sourceClientId: UNKNOWN_CLIENT_ID });
|
|
566
|
+
} catch {
|
|
567
|
+
// The client's listener failed, not the watch, which stays kept.
|
|
460
568
|
}
|
|
461
|
-
}
|
|
462
|
-
(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
569
|
+
}
|
|
570
|
+
generation.server.getModelDocument({ uri }).then(
|
|
571
|
+
current => {
|
|
572
|
+
if (current.text && this.watches.has(clientId)) {
|
|
573
|
+
try {
|
|
574
|
+
this.restoreDirty({ uri: current.uri, text: current.text });
|
|
575
|
+
} catch {
|
|
576
|
+
// The client's listener failed, not the watch, which stays kept.
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
() => undefined
|
|
581
|
+
);
|
|
582
|
+
},
|
|
583
|
+
(error: unknown) => {
|
|
584
|
+
if (!this.watches.has(clientId)) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (this.generation !== generation) {
|
|
588
|
+
this.logger.debug(`Watch of ${uri} as ${clientId} cut short by a lost connection; the next one sends it again`);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this.reportError(error, resolve(DATA_CONNECTION_WATCH_RESTORE_FAILED, { uri, detail: describeError(error) }));
|
|
472
592
|
}
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
);
|
|
593
|
+
);
|
|
476
594
|
}
|
|
477
595
|
|
|
478
596
|
/**
|
|
@@ -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,
|
|
@@ -190,6 +191,11 @@ function indexOf(write: DataSessionUnsavedWrite<TransferElement, DataServerProto
|
|
|
190
191
|
return 'index' in write.call ? write.call.index : 0;
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
/** The call `write` was last written by, which a restore writes again as one set. */
|
|
195
|
+
function setOf(write: DataSessionUnsavedWrite<TransferElement, DataServerProtocol<TransferElement>>): object {
|
|
196
|
+
return 'update' in write.call ? write.call.update : write.call.updates;
|
|
197
|
+
}
|
|
198
|
+
|
|
193
199
|
/**
|
|
194
200
|
* What a {@link DataSession} needs from the connection that minted it.
|
|
195
201
|
*
|
|
@@ -206,6 +212,13 @@ export interface DataSessionHost<TTransfer extends TransferElement, TServer exte
|
|
|
206
212
|
* is what the client was last told of the URI since the URI's last open.
|
|
207
213
|
*/
|
|
208
214
|
restoreDirty?(event: TransferDocumentDirtyChangedEvent): void;
|
|
215
|
+
/**
|
|
216
|
+
* Tell the connection's client of a document a restore found gone, whose
|
|
217
|
+
* deletion reached no one while the connection was down, as the server
|
|
218
|
+
* tells it of a deletion over a live connection. Without it, the restore
|
|
219
|
+
* reports the unsaved text such a document took with it as lost.
|
|
220
|
+
*/
|
|
221
|
+
restoreDeletion?(event: TransferDocumentDeletedEvent): void;
|
|
209
222
|
/**
|
|
210
223
|
* Forget what the client was told of `uri`'s dirty state: an open just
|
|
211
224
|
* made answered its caller afresh, and the client heard none of it, or a
|
|
@@ -229,7 +242,7 @@ export interface DataSessionHost<TTransfer extends TransferElement, TServer exte
|
|
|
229
242
|
/**
|
|
230
243
|
* Fires with each deletion the server sends. A session forgets a document
|
|
231
244
|
* whose file was deleted; without this event it keeps it as open until a
|
|
232
|
-
* restore finds it gone
|
|
245
|
+
* restore finds it gone.
|
|
233
246
|
*/
|
|
234
247
|
readonly onDidDeleteDocument?: Event<TransferDocumentDeletedEvent>;
|
|
235
248
|
/** Where the session logs the documents it forgets as gone. */
|
|
@@ -292,8 +305,9 @@ export class DataSession<
|
|
|
292
305
|
/**
|
|
293
306
|
* How long {@link closeDocument}, {@link deleteDocument} and {@link dispose}
|
|
294
307
|
* wait for this session's calls in flight before going on anyway. Long,
|
|
295
|
-
* because
|
|
296
|
-
*
|
|
308
|
+
* because a close sent while a save is still writing fails that save. It
|
|
309
|
+
* need not outlast the server's language-client settle after the write,
|
|
310
|
+
* which a close does not fail.
|
|
297
311
|
*/
|
|
298
312
|
protected readonly settleBeforeCloseMs: number = 10_000;
|
|
299
313
|
/**
|
|
@@ -378,6 +392,13 @@ export class DataSession<
|
|
|
378
392
|
* over a connection that is going away.
|
|
379
393
|
*/
|
|
380
394
|
protected detached = false;
|
|
395
|
+
/**
|
|
396
|
+
* Write sets, keyed as a restore groups them, that lost a document to
|
|
397
|
+
* another client's deletion. A restore cannot write such a set again
|
|
398
|
+
* whole, so it writes none of it. Weak, so a set goes once no write
|
|
399
|
+
* record holds it.
|
|
400
|
+
*/
|
|
401
|
+
protected readonly brokenSets = new WeakSet<object>();
|
|
381
402
|
/** The subscription to the host's `onDidChangeDirty`, disposed with the session. */
|
|
382
403
|
protected readonly dirtySubscription?: Disposable;
|
|
383
404
|
/** The subscription to the host's `onDidUpdateDocument`, disposed with the session. */
|
|
@@ -917,7 +938,10 @@ export class DataSession<
|
|
|
917
938
|
* let a restore re-open the file that is gone. Unwatched rather than
|
|
918
939
|
* closed: the server keeps the watch, and a close could end a re-open of a
|
|
919
940
|
* re-created file still in flight. Unsaved writes are logged, not
|
|
920
|
-
* reported: the client hears the deletion too.
|
|
941
|
+
* reported: the client hears the deletion too. A document this session
|
|
942
|
+
* created and never saved, deleted by no session, is reported as well: a
|
|
943
|
+
* drop released it. An unsaved write breaks the set it was written with,
|
|
944
|
+
* see {@link brokenSets}.
|
|
921
945
|
*
|
|
922
946
|
* This session's own deletion is neither unwatched nor logged as dropping
|
|
923
947
|
* unsaved text: its delete released the watch, and its caller chose to
|
|
@@ -934,7 +958,18 @@ export class DataSession<
|
|
|
934
958
|
this.forgetDocument(uri);
|
|
935
959
|
continue;
|
|
936
960
|
}
|
|
961
|
+
const write = this.unsavedWrites.get(uri);
|
|
962
|
+
if (write) {
|
|
963
|
+
this.brokenSets.add(setOf(write));
|
|
964
|
+
}
|
|
965
|
+
// Never saved, so no file a watcher could see removed: a deletion
|
|
966
|
+
// naming no session is the release of the text a drop left behind.
|
|
967
|
+
const released = this.createdUris.has(uri) && event.sourceClientId === UNKNOWN_CLIENT_ID;
|
|
937
968
|
this.forgetGoneDocument(uri);
|
|
969
|
+
if (released) {
|
|
970
|
+
const reported = resolve(DATA_SESSION_UNSAVED_LOST, { uris: uri });
|
|
971
|
+
this.host.reportError?.(new Error(reported.text), reported);
|
|
972
|
+
}
|
|
938
973
|
this.connected()
|
|
939
974
|
.then(server => server.unwatchModelDocument({ uri, clientId: this.clientId }))
|
|
940
975
|
.catch(() => undefined);
|
|
@@ -987,7 +1022,7 @@ export class DataSession<
|
|
|
987
1022
|
* write, so an edit arriving in between conflicts, and a conflict is not
|
|
988
1023
|
* retried. Documents last written by one {@link updateDocuments} are sent
|
|
989
1024
|
* again by one, and only when every one of them may be; a document that
|
|
990
|
-
* could not be re-opened blocks its set.
|
|
1025
|
+
* could not be re-opened, or that another client deleted, blocks its set.
|
|
991
1026
|
*
|
|
992
1027
|
* No caller is waiting, so the outcomes go through the host: one report
|
|
993
1028
|
* naming every document whose unsaved text is gone, whose record is then
|
|
@@ -997,8 +1032,13 @@ export class DataSession<
|
|
|
997
1032
|
* restore to decide again. A document closed while the restore runs, or
|
|
998
1033
|
* whose file is deleted then, is neither restored nor reported. One gone
|
|
999
1034
|
* while the connection was down, which the server reads back without a
|
|
1000
|
-
* model, is forgotten, and goes
|
|
1001
|
-
*
|
|
1035
|
+
* model, is forgotten, and its deletion goes to the host as the deletion
|
|
1036
|
+
* no event brought; unsaved text it took with it is logged, as for a
|
|
1037
|
+
* deletion heard live, and reported as well for a document the session
|
|
1038
|
+
* created and never saved, which the drop released. A host that cannot
|
|
1039
|
+
* tell the client has the unsaved text of either reported. Once the session
|
|
1040
|
+
* is disposed, its caller has ended it, and the restore returns at its
|
|
1041
|
+
* next await: nothing more goes to the server or the host.
|
|
1002
1042
|
*
|
|
1003
1043
|
* Each document's dirty state goes to the host too, read once the watch is
|
|
1004
1044
|
* in place and any write is sent: a flip while the connection was down, or
|
|
@@ -1020,6 +1060,9 @@ export class DataSession<
|
|
|
1020
1060
|
}
|
|
1021
1061
|
try {
|
|
1022
1062
|
const document = await server.openModelDocument({ uri, clientId: this.clientId });
|
|
1063
|
+
if (this.disposed) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1023
1066
|
if (!this.openUris.has(uri)) {
|
|
1024
1067
|
// Forgotten while it re-opened. A close of the caller's would
|
|
1025
1068
|
// follow, but a deletion sends none.
|
|
@@ -1036,6 +1079,9 @@ export class DataSession<
|
|
|
1036
1079
|
await server.closeModelDocument({ uri, clientId: this.clientId }).catch(() => undefined);
|
|
1037
1080
|
throw error;
|
|
1038
1081
|
});
|
|
1082
|
+
if (this.disposed) {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1039
1085
|
if (this.openUris.has(uri)) {
|
|
1040
1086
|
this.recordText(uri, document);
|
|
1041
1087
|
}
|
|
@@ -1044,22 +1090,42 @@ export class DataSession<
|
|
|
1044
1090
|
// Gone while the connection was down, which no deletion told the
|
|
1045
1091
|
// session of: the server answers a read of it without a model.
|
|
1046
1092
|
const read = await server.getModelDocument({ uri }).catch(() => undefined);
|
|
1093
|
+
if (this.disposed) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1047
1096
|
// Forgotten meanwhile, so nothing is lost.
|
|
1048
1097
|
if (!this.openUris.has(uri)) {
|
|
1049
1098
|
continue;
|
|
1050
1099
|
}
|
|
1051
1100
|
if (read && !read.model) {
|
|
1052
|
-
// No deletion reached the client either, so unsaved text the
|
|
1053
|
-
// document took with it is reported, as a drop's other losses are.
|
|
1054
1101
|
const write = this.unsavedWrites.get(uri);
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1102
|
+
// Named as the client last heard it, by the open's answer.
|
|
1103
|
+
const serverUri = this.opens.get(uri)?.serverUri ?? uri;
|
|
1104
|
+
// Never saved, so the drop released it and no one deleted it.
|
|
1105
|
+
const created = this.createdUris.has(uri);
|
|
1106
|
+
const unsaved = this.forgetGoneDocument(uri);
|
|
1058
1107
|
// Kept for `reapply`, where it blocks its set as a document
|
|
1059
1108
|
// that could not be re-opened does, and goes.
|
|
1060
1109
|
if (write) {
|
|
1061
1110
|
this.unsavedWrites.set(uri, write);
|
|
1062
1111
|
}
|
|
1112
|
+
if (!this.host.restoreDeletion) {
|
|
1113
|
+
// No client hears of the deletion, so the loss is reported.
|
|
1114
|
+
if (unsaved) {
|
|
1115
|
+
lost.push(uri);
|
|
1116
|
+
}
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
// The client hears of the deletion, so unsaved text is logged,
|
|
1120
|
+
// as for one heard live; the drop's release is reported too.
|
|
1121
|
+
if (created) {
|
|
1122
|
+
lost.push(uri);
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
this.host.restoreDeletion({ uri: serverUri, sourceClientId: UNKNOWN_CLIENT_ID });
|
|
1126
|
+
} catch {
|
|
1127
|
+
// The client's listener failed, not the restore.
|
|
1128
|
+
}
|
|
1063
1129
|
continue;
|
|
1064
1130
|
}
|
|
1065
1131
|
// Its record goes with its set's, in `reapply`, which it blocks.
|
|
@@ -1073,11 +1139,14 @@ export class DataSession<
|
|
|
1073
1139
|
}
|
|
1074
1140
|
const sets = new Map<object, [string, DataSessionUnsavedWrite<TTransfer, TServer>][]>();
|
|
1075
1141
|
for (const [uri, write] of writes) {
|
|
1076
|
-
const
|
|
1077
|
-
sets.set(
|
|
1142
|
+
const set = setOf(write);
|
|
1143
|
+
sets.set(set, [...(sets.get(set) ?? []), [uri, write]]);
|
|
1078
1144
|
}
|
|
1079
1145
|
for (const members of sets.values()) {
|
|
1080
1146
|
lost.push(...(await this.reapply(server, members, reopened)));
|
|
1147
|
+
if (this.disposed) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1081
1150
|
}
|
|
1082
1151
|
for (const uri of reopened.keys()) {
|
|
1083
1152
|
if (!this.openUris.has(uri)) {
|
|
@@ -1086,6 +1155,9 @@ export class DataSession<
|
|
|
1086
1155
|
// A failed read, or one without text, leaves the client's dirty
|
|
1087
1156
|
// state as it was; the document is restored all the same.
|
|
1088
1157
|
const current = await server.getModelDocument({ uri }).catch(() => undefined);
|
|
1158
|
+
if (this.disposed) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1089
1161
|
// Again after the read: one closed or deleted meanwhile has no
|
|
1090
1162
|
// state to tell.
|
|
1091
1163
|
if (current?.text !== undefined && this.openUris.has(uri)) {
|
|
@@ -1139,7 +1211,8 @@ export class DataSession<
|
|
|
1139
1211
|
*
|
|
1140
1212
|
* A member whose record is no longer the one the restore started with was
|
|
1141
1213
|
* closed, saved or disposed meanwhile, and is left out: writing it would
|
|
1142
|
-
* put back text its caller discarded.
|
|
1214
|
+
* put back text its caller discarded. A set in {@link brokenSets} is
|
|
1215
|
+
* not written again.
|
|
1143
1216
|
*/
|
|
1144
1217
|
protected async reapply(
|
|
1145
1218
|
server: RpcProxy<TServer>,
|
|
@@ -1171,7 +1244,7 @@ export class DataSession<
|
|
|
1171
1244
|
});
|
|
1172
1245
|
return gone.filter(member => member.outcome === 'lost' || member.outcome === 'resend').map(member => member.uri);
|
|
1173
1246
|
};
|
|
1174
|
-
if (outcomes.some(member => member.outcome === 'lost' || member.outcome === 'failed')) {
|
|
1247
|
+
if (this.brokenSets.has(setOf(members[0][1])) || outcomes.some(member => member.outcome === 'lost' || member.outcome === 'failed')) {
|
|
1175
1248
|
return lose();
|
|
1176
1249
|
}
|
|
1177
1250
|
// In the order of the call's updates, which is the order of its answers.
|
|
@@ -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 {
|
|
@@ -141,10 +141,13 @@ export interface DocumentServerProtocol<TTransfer extends TransferElement, TDiag
|
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
143
|
* Write a document as the session `clientId`, which must have it open, and
|
|
144
|
-
* persist it to disk. The response is the
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
144
|
+
* persist it to disk. The response is the build of the text written, with
|
|
145
|
+
* its version in `persisted`, sent once the language client has answered
|
|
146
|
+
* the push of that text, applied or refused, or a server-side bound on
|
|
147
|
+
* that wait passes; it does not wait again for a push that wait already
|
|
148
|
+
* gave up on and the client has not answered since. Save semantics depend
|
|
149
|
+
* on the filesystem-provider wiring; rejection paths surface as awaited
|
|
150
|
+
* promise rejections.
|
|
148
151
|
*/
|
|
149
152
|
saveModelDocument(args: TransferSaveDocumentArgs<TTransfer>): Promise<TransferSavedDocument<TTransfer, TDiagnostic>>;
|
|
150
153
|
|
|
@@ -154,7 +157,9 @@ export interface DocumentServerProtocol<TTransfer extends TransferElement, TDiag
|
|
|
154
157
|
* unsaved text keeps its formatting. `baseVersion` is checked in the step
|
|
155
158
|
* that takes the text, so text that moved on fails with a `ConflictError`
|
|
156
159
|
* code. The saved event names this session, whoever wrote the text.
|
|
157
|
-
*
|
|
160
|
+
* Answers after the same language-client wait as {@link saveModelDocument},
|
|
161
|
+
* with the document's latest build, read after that wait, so another
|
|
162
|
+
* session's write during it shows; `persisted` carries the version written.
|
|
158
163
|
*/
|
|
159
164
|
persistModelDocument(args: TransferPersistDocumentArgs): Promise<TransferSavedDocument<TTransfer, TDiagnostic>>;
|
|
160
165
|
|
|
@@ -178,15 +183,19 @@ export interface DocumentServerProtocol<TTransfer extends TransferElement, TDiag
|
|
|
178
183
|
* {@link DocumentClientProtocol.onDocumentUpdated} on the paired wire
|
|
179
184
|
* for every build-phase event on `uri`.
|
|
180
185
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
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.
|
|
188
197
|
*/
|
|
189
|
-
watchModelDocument(args: WatchModelDocumentArgs): Promise<
|
|
198
|
+
watchModelDocument(args: WatchModelDocumentArgs): Promise<TransferWatchedDocument>;
|
|
190
199
|
|
|
191
200
|
/**
|
|
192
201
|
* Stop watching `(uri, clientId)` previously started by
|