@hydranium/protocol 1.0.0-next.285 → 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 +29 -6
- package/lib/client/data-session.d.ts.map +1 -1
- package/lib/client/data-session.js +78 -13
- 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 +86 -14
- package/src/data/data-server-protocol.ts +13 -9
- 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. */
|
|
@@ -379,6 +392,13 @@ export class DataSession<
|
|
|
379
392
|
* over a connection that is going away.
|
|
380
393
|
*/
|
|
381
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>();
|
|
382
402
|
/** The subscription to the host's `onDidChangeDirty`, disposed with the session. */
|
|
383
403
|
protected readonly dirtySubscription?: Disposable;
|
|
384
404
|
/** The subscription to the host's `onDidUpdateDocument`, disposed with the session. */
|
|
@@ -918,7 +938,10 @@ export class DataSession<
|
|
|
918
938
|
* let a restore re-open the file that is gone. Unwatched rather than
|
|
919
939
|
* closed: the server keeps the watch, and a close could end a re-open of a
|
|
920
940
|
* re-created file still in flight. Unsaved writes are logged, not
|
|
921
|
-
* 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}.
|
|
922
945
|
*
|
|
923
946
|
* This session's own deletion is neither unwatched nor logged as dropping
|
|
924
947
|
* unsaved text: its delete released the watch, and its caller chose to
|
|
@@ -935,7 +958,18 @@ export class DataSession<
|
|
|
935
958
|
this.forgetDocument(uri);
|
|
936
959
|
continue;
|
|
937
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;
|
|
938
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
|
+
}
|
|
939
973
|
this.connected()
|
|
940
974
|
.then(server => server.unwatchModelDocument({ uri, clientId: this.clientId }))
|
|
941
975
|
.catch(() => undefined);
|
|
@@ -988,7 +1022,7 @@ export class DataSession<
|
|
|
988
1022
|
* write, so an edit arriving in between conflicts, and a conflict is not
|
|
989
1023
|
* retried. Documents last written by one {@link updateDocuments} are sent
|
|
990
1024
|
* again by one, and only when every one of them may be; a document that
|
|
991
|
-
* could not be re-opened blocks its set.
|
|
1025
|
+
* could not be re-opened, or that another client deleted, blocks its set.
|
|
992
1026
|
*
|
|
993
1027
|
* No caller is waiting, so the outcomes go through the host: one report
|
|
994
1028
|
* naming every document whose unsaved text is gone, whose record is then
|
|
@@ -998,8 +1032,13 @@ export class DataSession<
|
|
|
998
1032
|
* restore to decide again. A document closed while the restore runs, or
|
|
999
1033
|
* whose file is deleted then, is neither restored nor reported. One gone
|
|
1000
1034
|
* while the connection was down, which the server reads back without a
|
|
1001
|
-
* model, is forgotten, and goes
|
|
1002
|
-
*
|
|
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.
|
|
1003
1042
|
*
|
|
1004
1043
|
* Each document's dirty state goes to the host too, read once the watch is
|
|
1005
1044
|
* in place and any write is sent: a flip while the connection was down, or
|
|
@@ -1021,6 +1060,9 @@ export class DataSession<
|
|
|
1021
1060
|
}
|
|
1022
1061
|
try {
|
|
1023
1062
|
const document = await server.openModelDocument({ uri, clientId: this.clientId });
|
|
1063
|
+
if (this.disposed) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1024
1066
|
if (!this.openUris.has(uri)) {
|
|
1025
1067
|
// Forgotten while it re-opened. A close of the caller's would
|
|
1026
1068
|
// follow, but a deletion sends none.
|
|
@@ -1037,6 +1079,9 @@ export class DataSession<
|
|
|
1037
1079
|
await server.closeModelDocument({ uri, clientId: this.clientId }).catch(() => undefined);
|
|
1038
1080
|
throw error;
|
|
1039
1081
|
});
|
|
1082
|
+
if (this.disposed) {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1040
1085
|
if (this.openUris.has(uri)) {
|
|
1041
1086
|
this.recordText(uri, document);
|
|
1042
1087
|
}
|
|
@@ -1045,22 +1090,42 @@ export class DataSession<
|
|
|
1045
1090
|
// Gone while the connection was down, which no deletion told the
|
|
1046
1091
|
// session of: the server answers a read of it without a model.
|
|
1047
1092
|
const read = await server.getModelDocument({ uri }).catch(() => undefined);
|
|
1093
|
+
if (this.disposed) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1048
1096
|
// Forgotten meanwhile, so nothing is lost.
|
|
1049
1097
|
if (!this.openUris.has(uri)) {
|
|
1050
1098
|
continue;
|
|
1051
1099
|
}
|
|
1052
1100
|
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
1101
|
const write = this.unsavedWrites.get(uri);
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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);
|
|
1059
1107
|
// Kept for `reapply`, where it blocks its set as a document
|
|
1060
1108
|
// that could not be re-opened does, and goes.
|
|
1061
1109
|
if (write) {
|
|
1062
1110
|
this.unsavedWrites.set(uri, write);
|
|
1063
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
|
+
}
|
|
1064
1129
|
continue;
|
|
1065
1130
|
}
|
|
1066
1131
|
// Its record goes with its set's, in `reapply`, which it blocks.
|
|
@@ -1074,11 +1139,14 @@ export class DataSession<
|
|
|
1074
1139
|
}
|
|
1075
1140
|
const sets = new Map<object, [string, DataSessionUnsavedWrite<TTransfer, TServer>][]>();
|
|
1076
1141
|
for (const [uri, write] of writes) {
|
|
1077
|
-
const
|
|
1078
|
-
sets.set(
|
|
1142
|
+
const set = setOf(write);
|
|
1143
|
+
sets.set(set, [...(sets.get(set) ?? []), [uri, write]]);
|
|
1079
1144
|
}
|
|
1080
1145
|
for (const members of sets.values()) {
|
|
1081
1146
|
lost.push(...(await this.reapply(server, members, reopened)));
|
|
1147
|
+
if (this.disposed) {
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1082
1150
|
}
|
|
1083
1151
|
for (const uri of reopened.keys()) {
|
|
1084
1152
|
if (!this.openUris.has(uri)) {
|
|
@@ -1087,6 +1155,9 @@ export class DataSession<
|
|
|
1087
1155
|
// A failed read, or one without text, leaves the client's dirty
|
|
1088
1156
|
// state as it was; the document is restored all the same.
|
|
1089
1157
|
const current = await server.getModelDocument({ uri }).catch(() => undefined);
|
|
1158
|
+
if (this.disposed) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1090
1161
|
// Again after the read: one closed or deleted meanwhile has no
|
|
1091
1162
|
// state to tell.
|
|
1092
1163
|
if (current?.text !== undefined && this.openUris.has(uri)) {
|
|
@@ -1140,7 +1211,8 @@ export class DataSession<
|
|
|
1140
1211
|
*
|
|
1141
1212
|
* A member whose record is no longer the one the restore started with was
|
|
1142
1213
|
* closed, saved or disposed meanwhile, and is left out: writing it would
|
|
1143
|
-
* put back text its caller discarded.
|
|
1214
|
+
* put back text its caller discarded. A set in {@link brokenSets} is
|
|
1215
|
+
* not written again.
|
|
1144
1216
|
*/
|
|
1145
1217
|
protected async reapply(
|
|
1146
1218
|
server: RpcProxy<TServer>,
|
|
@@ -1172,7 +1244,7 @@ export class DataSession<
|
|
|
1172
1244
|
});
|
|
1173
1245
|
return gone.filter(member => member.outcome === 'lost' || member.outcome === 'resend').map(member => member.uri);
|
|
1174
1246
|
};
|
|
1175
|
-
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')) {
|
|
1176
1248
|
return lose();
|
|
1177
1249
|
}
|
|
1178
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 {
|
|
@@ -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
|