@hydranium/protocol 1.0.0-next.268 → 1.0.0-next.269
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 +6 -3
- package/lib/client/data-connection.d.ts.map +1 -1
- package/lib/client/data-connection.js +18 -4
- package/lib/client/data-connection.js.map +1 -1
- package/lib/client/data-session.d.ts +125 -27
- package/lib/client/data-session.d.ts.map +1 -1
- package/lib/client/data-session.js +274 -75
- package/lib/client/data-session.js.map +1 -1
- package/package.json +1 -1
- package/src/client/data-connection.ts +19 -4
- package/src/client/data-session.ts +332 -75
|
@@ -9,11 +9,19 @@
|
|
|
9
9
|
|
|
10
10
|
import { type Disposable, Emitter, type Event } from 'vscode-jsonrpc';
|
|
11
11
|
import { type Clock, SystemClock } from '../clock';
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
DataServerProtocol,
|
|
14
|
+
DiagnosticOf,
|
|
15
|
+
TransferDocumentDeletedEvent,
|
|
16
|
+
TransferDocumentDirtyChangedEvent,
|
|
17
|
+
TransferDocumentUpdatedEvent
|
|
18
|
+
} from '../data';
|
|
13
19
|
import { isConflictError, SessionClosedError } from '../errors';
|
|
20
|
+
import type { Logger } from '../logger';
|
|
14
21
|
import { type BaseVersion, isModelVersion, type ModelVersion, type TextVersion, UNRECORDED_VERSION } from '../model-service/base-version';
|
|
15
22
|
import { type ResolvedMessage, defineMessage, describeError, resolve } from '../messages/primitives';
|
|
16
23
|
import type { OpenModelArgs } from '../model-server';
|
|
24
|
+
import { NoopLogger } from '../noop-logger';
|
|
17
25
|
import { randomUuid } from '../random-uuid';
|
|
18
26
|
import type { MaybePromise } from '../util';
|
|
19
27
|
import type { RpcProxy } from '../rpc';
|
|
@@ -146,6 +154,31 @@ export interface DataSessionUnsavedWrite<
|
|
|
146
154
|
| { readonly updates: DataSessionUpdatesArgs<TTransfer, TServer>; readonly index: number };
|
|
147
155
|
}
|
|
148
156
|
|
|
157
|
+
/**
|
|
158
|
+
* One open of a URI by a {@link DataSession}, from the answer to its open,
|
|
159
|
+
* create or re-open until a close or a deletion ends it. A call records its
|
|
160
|
+
* answer only while the open it was sent under is still the URI's: checking
|
|
161
|
+
* that the URI is open accepts an answer from before a close or deletion
|
|
162
|
+
* once the URI is open again.
|
|
163
|
+
*/
|
|
164
|
+
export interface DataSessionOpen {
|
|
165
|
+
/** The URI the open's answer named, which the server keys its notifications by. */
|
|
166
|
+
readonly serverUri: string;
|
|
167
|
+
/** Whether a deletion ended the open, which an open still watching sends again; a close does not. */
|
|
168
|
+
deleted: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* One open or create of a URI by a {@link DataSession}, from its call until it
|
|
173
|
+
* settles. A close of the URI while it is under way marks it: it then records
|
|
174
|
+
* nothing and is not sent again, so the close stands. Without the mark, a
|
|
175
|
+
* deletion during its watch has it sent again after the close.
|
|
176
|
+
*/
|
|
177
|
+
export interface DataSessionOpening {
|
|
178
|
+
/** Whether a close of the URI came while this open was under way. */
|
|
179
|
+
closed: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
149
182
|
/** Where `write` stands in the `updateDocuments` call it was last written by; `0` for a single update. */
|
|
150
183
|
function indexOf(write: DataSessionUnsavedWrite<TransferElement, DataServerProtocol<TransferElement>>): number {
|
|
151
184
|
return 'index' in write.call ? write.call.index : 0;
|
|
@@ -187,6 +220,14 @@ export interface DataSessionHost<TTransfer extends TransferElement, TServer exte
|
|
|
187
220
|
* this event it keeps it, and a restore reports the write lost.
|
|
188
221
|
*/
|
|
189
222
|
readonly onDidUpdateDocument?: Event<TransferDocumentUpdatedEvent<TTransfer, DiagnosticOf<TServer>>>;
|
|
223
|
+
/**
|
|
224
|
+
* Fires with each deletion the server sends. A session forgets a document
|
|
225
|
+
* whose file was deleted; without this event it keeps it as open until a
|
|
226
|
+
* restore finds it gone, and reports its unsaved text lost only then.
|
|
227
|
+
*/
|
|
228
|
+
readonly onDidDeleteDocument?: Event<TransferDocumentDeletedEvent>;
|
|
229
|
+
/** Where the session logs the documents it forgets as gone. */
|
|
230
|
+
readonly logger?: Logger;
|
|
190
231
|
}
|
|
191
232
|
|
|
192
233
|
/**
|
|
@@ -255,17 +296,17 @@ export class DataSession<
|
|
|
255
296
|
/** URIs this session has open, re-opened after a reconnect. */
|
|
256
297
|
protected readonly openUris = new Set<string>();
|
|
257
298
|
/**
|
|
258
|
-
* Per URI in {@link openUris},
|
|
259
|
-
*
|
|
260
|
-
*
|
|
299
|
+
* Per URI in {@link openUris}, or whose open answered and whose watch is
|
|
300
|
+
* still under way, the open it is under. A repeat open keeps it, as the
|
|
301
|
+
* server keeps its earlier open.
|
|
261
302
|
*/
|
|
262
|
-
protected readonly
|
|
303
|
+
protected readonly opens = new Map<string, DataSessionOpen>();
|
|
263
304
|
/**
|
|
264
|
-
* Per URI,
|
|
265
|
-
*
|
|
266
|
-
*
|
|
305
|
+
* Per URI, the opens of it this session has under way. Such a URI counts
|
|
306
|
+
* as open for {@link withOpenDocument}, which would otherwise close it
|
|
307
|
+
* under the open that is still being made.
|
|
267
308
|
*/
|
|
268
|
-
protected readonly
|
|
309
|
+
protected readonly openings = new Map<string, Set<DataSessionOpening>>();
|
|
269
310
|
/** Per URI written since its last save, what {@link restore} needs to write it again. */
|
|
270
311
|
protected readonly unsavedWrites = new Map<string, DataSessionUnsavedWrite<TTransfer, TServer>>();
|
|
271
312
|
/**
|
|
@@ -282,6 +323,13 @@ export class DataSession<
|
|
|
282
323
|
* version of a write still in flight as well, and that write is not saved.
|
|
283
324
|
*/
|
|
284
325
|
protected readonly savedVersions = new Map<string, TextVersion>();
|
|
326
|
+
/**
|
|
327
|
+
* URIs this session created whose whole text is still its own and unsaved,
|
|
328
|
+
* even with no write recorded: no save, clean flip, other client's write or
|
|
329
|
+
* reported loss has come since the create answered; see
|
|
330
|
+
* {@link forgetGoneDocument}.
|
|
331
|
+
*/
|
|
332
|
+
protected readonly createdUris = new Set<string>();
|
|
285
333
|
/** Per URI, this session's calls still in flight on it, which a close waits for. */
|
|
286
334
|
protected readonly inFlight = new Map<string, Set<Promise<unknown>>>();
|
|
287
335
|
/** This session's saves still in flight, which a host's exit waits for; see {@link hasSavesInFlight}. */
|
|
@@ -320,14 +368,20 @@ export class DataSession<
|
|
|
320
368
|
protected readonly dirtySubscription?: Disposable;
|
|
321
369
|
/** The subscription to the host's `onDidUpdateDocument`, disposed with the session. */
|
|
322
370
|
protected readonly updateSubscription?: Disposable;
|
|
371
|
+
/** The subscription to the host's `onDidDeleteDocument`, disposed with the session. */
|
|
372
|
+
protected readonly deletionSubscription?: Disposable;
|
|
373
|
+
/** The host's logger, or one that logs nothing. */
|
|
374
|
+
protected readonly logger: Logger;
|
|
323
375
|
|
|
324
376
|
constructor(
|
|
325
377
|
readonly clientId: string,
|
|
326
378
|
protected readonly host: DataSessionHost<TTransfer, TServer>,
|
|
327
379
|
readonly label: string
|
|
328
380
|
) {
|
|
381
|
+
this.logger = host.logger ?? new NoopLogger();
|
|
329
382
|
this.dirtySubscription = host.onDidChangeDirty?.(event => this.forgetSavedWrite(event));
|
|
330
383
|
this.updateSubscription = host.onDidUpdateDocument?.(event => this.forgetSupersededWrite(event));
|
|
384
|
+
this.deletionSubscription = host.onDidDeleteDocument?.(event => this.forgetDeletedDocument(event));
|
|
331
385
|
}
|
|
332
386
|
|
|
333
387
|
/**
|
|
@@ -375,10 +429,9 @@ export class DataSession<
|
|
|
375
429
|
* from `getModelDocument({ includeDiagnostics: true })`.
|
|
376
430
|
*/
|
|
377
431
|
openDocument(args: DataSessionOpenArgs<TTransfer, TServer>): Promise<DataSessionDocument<TTransfer, TServer>> {
|
|
378
|
-
return this.trackOpen(args.uri,
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
});
|
|
432
|
+
return this.trackOpen(args.uri, opening =>
|
|
433
|
+
this.openAndWatch(args.uri, opening, false, server => server.openModelDocument({ ...args, clientId: this.clientId }))
|
|
434
|
+
);
|
|
382
435
|
}
|
|
383
436
|
|
|
384
437
|
/**
|
|
@@ -387,30 +440,76 @@ export class DataSession<
|
|
|
387
440
|
* refuses a URI that exists on disk or that any client has open.
|
|
388
441
|
*/
|
|
389
442
|
createDocument(args: DataSessionCreateArgs<TTransfer, TServer>): Promise<DataSessionDocument<TTransfer, TServer>> {
|
|
390
|
-
return this.trackOpen(args.uri,
|
|
443
|
+
return this.trackOpen(args.uri, opening =>
|
|
444
|
+
this.openAndWatch(args.uri, opening, true, server => server.createModelDocument({ ...args, clientId: this.clientId }))
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Send `open` and watch what it answered, again for as long as a deletion
|
|
450
|
+
* heard while the watch is under way closes that open: sent after the
|
|
451
|
+
* deletion, the open fails as an open of a gone file does, or opens the
|
|
452
|
+
* file that is back. `created` marks the document in {@link createdUris}
|
|
453
|
+
* once the open answers, before the watch, so an event that clears the mark
|
|
454
|
+
* meanwhile clears it for good.
|
|
455
|
+
*
|
|
456
|
+
* An `opening` a close marked answers with the document, recording nothing
|
|
457
|
+
* and sending nothing again; one it marked before the open answered is
|
|
458
|
+
* undone, since the server may have taken the close first. A failed watch
|
|
459
|
+
* is undone too: the caller sees a failed open, and so would never close it.
|
|
460
|
+
*/
|
|
461
|
+
protected async openAndWatch(
|
|
462
|
+
uri: string,
|
|
463
|
+
opening: DataSessionOpening,
|
|
464
|
+
created: boolean,
|
|
465
|
+
open: (server: RpcProxy<TServer>) => Promise<DataSessionDocument<TTransfer, TServer>>
|
|
466
|
+
): Promise<DataSessionDocument<TTransfer, TServer>> {
|
|
467
|
+
for (;;) {
|
|
391
468
|
const server = await this.connected();
|
|
392
|
-
|
|
393
|
-
|
|
469
|
+
const document = await open(server);
|
|
470
|
+
if (opening.closed) {
|
|
471
|
+
await this.undoOpen(server, uri, opening);
|
|
472
|
+
return document;
|
|
473
|
+
}
|
|
474
|
+
if (created) {
|
|
475
|
+
this.createdUris.add(uri);
|
|
476
|
+
}
|
|
477
|
+
let watched: DataSessionDocument<TTransfer, TServer> | undefined;
|
|
478
|
+
try {
|
|
479
|
+
watched = await this.watchOpened(server, uri, document);
|
|
480
|
+
} catch (error: unknown) {
|
|
481
|
+
await this.undoOpen(server, uri, opening);
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
if (watched || opening.closed) {
|
|
485
|
+
return watched ?? document;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
394
488
|
}
|
|
395
489
|
|
|
396
490
|
/**
|
|
397
|
-
* Watch a document this session just opened, and record it as open.
|
|
398
|
-
*
|
|
399
|
-
*
|
|
491
|
+
* Watch a document this session just opened, and record it as open.
|
|
492
|
+
* Answers `undefined`, recording nothing, when a deletion heard while the
|
|
493
|
+
* watch is under way has ended the open already, and the document,
|
|
494
|
+
* recording nothing, when a close has. A failed watch rejects with the open
|
|
495
|
+
* still recorded, for its caller to undo, see {@link undoOpen}.
|
|
400
496
|
*/
|
|
401
497
|
protected async watchOpened(
|
|
402
498
|
server: RpcProxy<TServer>,
|
|
403
499
|
uri: string,
|
|
404
500
|
document: DataSessionDocument<TTransfer, TServer>
|
|
405
|
-
): Promise<DataSessionDocument<TTransfer, TServer
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
501
|
+
): Promise<DataSessionDocument<TTransfer, TServer> | undefined> {
|
|
502
|
+
// Recorded before the watch, so a deletion heard while it is under way
|
|
503
|
+
// finds this open, which the server made before the deletion.
|
|
504
|
+
const open = this.opens.get(uri) ?? { serverUri: document.uri, deleted: false };
|
|
505
|
+
this.opens.set(uri, open);
|
|
506
|
+
await server.watchModelDocument({ uri, clientId: this.clientId });
|
|
507
|
+
if (this.opens.get(uri) !== open) {
|
|
508
|
+
// Sent again, it ends as an open sent after the deletion would; a
|
|
509
|
+
// close is the caller's later call, and stands.
|
|
510
|
+
return open.deleted ? undefined : document;
|
|
411
511
|
}
|
|
412
512
|
this.openUris.add(uri);
|
|
413
|
-
this.serverUris.set(uri, document.uri);
|
|
414
513
|
this.recordText(uri, document);
|
|
415
514
|
// Keyed as the server keys its notifications, which may not be how the
|
|
416
515
|
// caller spelled the URI.
|
|
@@ -427,19 +526,53 @@ export class DataSession<
|
|
|
427
526
|
this.assertLive();
|
|
428
527
|
await this.settle(this.inFlight.get(args.uri));
|
|
429
528
|
// Forgotten before the close is sent, so a reconnect in between does not
|
|
430
|
-
// re-open a document the caller has closed
|
|
431
|
-
//
|
|
432
|
-
|
|
433
|
-
this.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this.
|
|
437
|
-
this.savedVersions.delete(args.uri);
|
|
438
|
-
this.host.forgetDirty?.(serverUri);
|
|
529
|
+
// re-open a document the caller has closed. Every open of it under way
|
|
530
|
+
// by now is marked, even one called after this close, so it neither
|
|
531
|
+
// records it again nor is sent again.
|
|
532
|
+
this.openings.get(args.uri)?.forEach(opening => {
|
|
533
|
+
opening.closed = true;
|
|
534
|
+
});
|
|
535
|
+
this.forgetDocument(args.uri);
|
|
439
536
|
const server = await this.connected();
|
|
440
537
|
await server.closeModelDocument({ ...args, clientId: this.clientId });
|
|
441
538
|
}
|
|
442
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Close `uri` again after `opening` failed or was closed while under way,
|
|
542
|
+
* and drop what it recorded, unless the session still relies on the open,
|
|
543
|
+
* see {@link hasOpen}: the server holds one open per session and URI, so
|
|
544
|
+
* the close would end that one too.
|
|
545
|
+
*/
|
|
546
|
+
protected async undoOpen(server: RpcProxy<TServer>, uri: string, opening: DataSessionOpening): Promise<void> {
|
|
547
|
+
if (this.hasOpen(uri, opening)) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
this.opens.delete(uri);
|
|
551
|
+
this.createdUris.delete(uri);
|
|
552
|
+
await server.closeModelDocument({ uri, clientId: this.clientId }).catch(() => undefined);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Drop everything this session keeps of `uri`, which it no longer has open. */
|
|
556
|
+
protected forgetDocument(uri: string): void {
|
|
557
|
+
const serverUri = this.opens.get(uri)?.serverUri ?? uri;
|
|
558
|
+
this.openUris.delete(uri);
|
|
559
|
+
this.opens.delete(uri);
|
|
560
|
+
this.unsavedWrites.delete(uri);
|
|
561
|
+
this.lastAnswers.delete(uri);
|
|
562
|
+
this.savedVersions.delete(uri);
|
|
563
|
+
this.createdUris.delete(uri);
|
|
564
|
+
this.host.forgetDirty?.(serverUri);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Whether this session has `uri` open, or has an open of it under way that
|
|
569
|
+
* no close has marked, other than `except`. A marked open records nothing,
|
|
570
|
+
* so nothing can rely on it.
|
|
571
|
+
*/
|
|
572
|
+
protected hasOpen(uri: string, except?: DataSessionOpening): boolean {
|
|
573
|
+
return this.openUris.has(uri) || [...(this.openings.get(uri) ?? [])].some(opening => opening !== except && !opening.closed);
|
|
574
|
+
}
|
|
575
|
+
|
|
443
576
|
/**
|
|
444
577
|
* Open `args.uri`, run `fn` with the opened snapshot, and close it once
|
|
445
578
|
* `fn` settles, whether it returned or threw. A URI this session already
|
|
@@ -450,7 +583,7 @@ export class DataSession<
|
|
|
450
583
|
args: DataSessionOpenArgs<TTransfer, TServer>,
|
|
451
584
|
fn: (document: DataSessionDocument<TTransfer, TServer>) => MaybePromise<T>
|
|
452
585
|
): Promise<T> {
|
|
453
|
-
const alreadyOpen = this.
|
|
586
|
+
const alreadyOpen = this.hasOpen(args.uri);
|
|
454
587
|
const document = await this.openDocument(args);
|
|
455
588
|
try {
|
|
456
589
|
return await fn(document);
|
|
@@ -467,8 +600,9 @@ export class DataSession<
|
|
|
467
600
|
const server = await this.connected();
|
|
468
601
|
const update = { ...args };
|
|
469
602
|
const baseHash = await this.baseHashOf(server, args.uri, args.baseVersion);
|
|
603
|
+
const open = this.opens.get(args.uri);
|
|
470
604
|
const document = await server.updateModelDocument({ ...args, clientId: this.clientId });
|
|
471
|
-
this.recordWrite(args.uri, document, baseHash, { update });
|
|
605
|
+
this.recordWrite(args.uri, open, document, baseHash, { update });
|
|
472
606
|
return document;
|
|
473
607
|
});
|
|
474
608
|
}
|
|
@@ -487,8 +621,11 @@ export class DataSession<
|
|
|
487
621
|
const updates = { ...args, updates: args.updates.map(update => ({ ...update })) };
|
|
488
622
|
const server = await this.connected();
|
|
489
623
|
const baseHashes = await Promise.all(args.updates.map(update => this.baseHashOf(server, update.uri, update.baseVersion)));
|
|
624
|
+
const opens = args.updates.map(update => this.opens.get(update.uri));
|
|
490
625
|
const documents = await server.updateModelDocuments({ ...args, clientId: this.clientId });
|
|
491
|
-
args.updates.forEach((update, index) =>
|
|
626
|
+
args.updates.forEach((update, index) =>
|
|
627
|
+
this.recordWrite(update.uri, opens[index], documents[index], baseHashes[index], { updates, index })
|
|
628
|
+
);
|
|
492
629
|
return documents;
|
|
493
630
|
}
|
|
494
631
|
);
|
|
@@ -514,7 +651,13 @@ export class DataSession<
|
|
|
514
651
|
): Promise<TransferSavedDocument<TTransfer, DiagnosticOf<TServer>>> {
|
|
515
652
|
const saving = this.track([uri], async () => {
|
|
516
653
|
const server = await this.connected();
|
|
654
|
+
const open = this.opens.get(uri);
|
|
517
655
|
const document = await send(server);
|
|
656
|
+
// Sent under no open, or under one a close or deletion has ended
|
|
657
|
+
// since: recorded, it would outlive that open.
|
|
658
|
+
if (!open || this.opens.get(uri) !== open) {
|
|
659
|
+
return document;
|
|
660
|
+
}
|
|
518
661
|
// Kept only when its last answer is newer than the text written. Kept
|
|
519
662
|
// whenever it changed while the save ran, a record the save covered
|
|
520
663
|
// survives, and a restore then reports it lost.
|
|
@@ -523,6 +666,7 @@ export class DataSession<
|
|
|
523
666
|
this.unsavedWrites.delete(uri);
|
|
524
667
|
}
|
|
525
668
|
this.recordText(uri, document);
|
|
669
|
+
this.createdUris.delete(uri);
|
|
526
670
|
// The higher of two saves' answers, which may arrive out of order.
|
|
527
671
|
const saved = this.savedVersions.get(uri);
|
|
528
672
|
if (saved === undefined || document.persisted.version > saved) {
|
|
@@ -614,10 +758,11 @@ export class DataSession<
|
|
|
614
758
|
detach(): void {
|
|
615
759
|
this.detached = true;
|
|
616
760
|
this.openUris.clear();
|
|
617
|
-
this.
|
|
761
|
+
this.opens.clear();
|
|
618
762
|
this.unsavedWrites.clear();
|
|
619
763
|
this.lastAnswers.clear();
|
|
620
764
|
this.savedVersions.clear();
|
|
765
|
+
this.createdUris.clear();
|
|
621
766
|
if (!this.disposed) {
|
|
622
767
|
this.disposed = true;
|
|
623
768
|
this.fireDispose();
|
|
@@ -625,13 +770,14 @@ export class DataSession<
|
|
|
625
770
|
}
|
|
626
771
|
|
|
627
772
|
/**
|
|
628
|
-
* Dispose {@link dirtySubscription}
|
|
629
|
-
* {@link onDidDispose} and dispose its
|
|
630
|
-
* nothing.
|
|
773
|
+
* Dispose {@link dirtySubscription}, {@link updateSubscription} and
|
|
774
|
+
* {@link deletionSubscription}, fire {@link onDidDispose} and dispose its
|
|
775
|
+
* emitter, so a second call fires nothing.
|
|
631
776
|
*/
|
|
632
777
|
protected fireDispose(): void {
|
|
633
778
|
this.dirtySubscription?.dispose();
|
|
634
779
|
this.updateSubscription?.dispose();
|
|
780
|
+
this.deletionSubscription?.dispose();
|
|
635
781
|
this.disposeEmitter.fire(undefined);
|
|
636
782
|
this.disposeEmitter.dispose();
|
|
637
783
|
}
|
|
@@ -651,9 +797,10 @@ export class DataSession<
|
|
|
651
797
|
|
|
652
798
|
/**
|
|
653
799
|
* Drop the record in {@link unsavedWrites} of a document this session has
|
|
654
|
-
* open once the server says it turned clean
|
|
655
|
-
*
|
|
656
|
-
* another client's save
|
|
800
|
+
* open once the server says it turned clean, and its mark in
|
|
801
|
+
* {@link createdUris}: it holds no unsaved text then, and a record kept
|
|
802
|
+
* would have a restore report the write lost once another client's save
|
|
803
|
+
* changed the file.
|
|
657
804
|
*
|
|
658
805
|
* The server sends a flip only for a document someone watches, so a
|
|
659
806
|
* document the session writes without watching it keeps its record, and
|
|
@@ -665,9 +812,10 @@ export class DataSession<
|
|
|
665
812
|
}
|
|
666
813
|
// The flip names the server's key, which is not always the caller's
|
|
667
814
|
// spelling that the records are kept under.
|
|
668
|
-
for (const [uri,
|
|
669
|
-
if (serverUri === event.uri) {
|
|
815
|
+
for (const [uri, open] of this.opens) {
|
|
816
|
+
if (open.serverUri === event.uri) {
|
|
670
817
|
this.unsavedWrites.delete(uri);
|
|
818
|
+
this.createdUris.delete(uri);
|
|
671
819
|
}
|
|
672
820
|
}
|
|
673
821
|
}
|
|
@@ -686,19 +834,63 @@ export class DataSession<
|
|
|
686
834
|
* `'rebuilt'` event carries no new text. Like {@link forgetSavedWrite}, it
|
|
687
835
|
* depends on the server sending the event, which it does only for a
|
|
688
836
|
* document someone watches.
|
|
837
|
+
*
|
|
838
|
+
* The mark in {@link createdUris} goes on any such event: the unsaved text
|
|
839
|
+
* is then the other client's, and a record still kept reports the
|
|
840
|
+
* session's own.
|
|
689
841
|
*/
|
|
690
842
|
protected forgetSupersededWrite(event: TransferDocumentUpdatedEvent<TTransfer, DiagnosticOf<TServer>>): void {
|
|
691
843
|
if (event.reason !== 'changed' || this.isOwnEcho(event.sourceClientId)) {
|
|
692
844
|
return;
|
|
693
845
|
}
|
|
694
|
-
for (const [uri,
|
|
846
|
+
for (const [uri, open] of this.opens) {
|
|
847
|
+
if (open.serverUri !== event.document.uri) {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
this.createdUris.delete(uri);
|
|
695
851
|
const write = this.unsavedWrites.get(uri);
|
|
696
|
-
if (
|
|
852
|
+
if (write && this.restoreOutcome(write, event.document) !== 'kept') {
|
|
697
853
|
this.unsavedWrites.delete(uri);
|
|
698
854
|
}
|
|
699
855
|
}
|
|
700
856
|
}
|
|
701
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Forget a document whose file was deleted, which closed this session's
|
|
860
|
+
* open. At once rather than after the calls in flight settle, which would
|
|
861
|
+
* let a restore re-open the file that is gone. Unwatched rather than
|
|
862
|
+
* closed: the server keeps the watch, and a close could end a re-open of a
|
|
863
|
+
* re-created file still in flight. Unsaved writes are logged, not
|
|
864
|
+
* reported: the client hears the deletion too.
|
|
865
|
+
*/
|
|
866
|
+
protected forgetDeletedDocument(event: TransferDocumentDeletedEvent): void {
|
|
867
|
+
for (const [uri, open] of this.opens) {
|
|
868
|
+
if (open.serverUri !== event.uri) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
open.deleted = true;
|
|
872
|
+
this.forgetGoneDocument(uri);
|
|
873
|
+
this.connected()
|
|
874
|
+
.then(server => server.unwatchModelDocument({ uri, clientId: this.clientId }))
|
|
875
|
+
.catch(() => undefined);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* {@link forgetDocument} a document that is gone, logging any unsaved text
|
|
881
|
+
* it drops, written or created; returns whether it dropped some.
|
|
882
|
+
*/
|
|
883
|
+
protected forgetGoneDocument(uri: string): boolean {
|
|
884
|
+
const unsaved = this.unsavedWrites.has(uri) || this.createdUris.has(uri);
|
|
885
|
+
if (unsaved) {
|
|
886
|
+
this.logger.withUri(uri).warn('Dropped unsaved text: the document is gone');
|
|
887
|
+
} else {
|
|
888
|
+
this.logger.withUri(uri).debug('Forgot the document: it is gone');
|
|
889
|
+
}
|
|
890
|
+
this.forgetDocument(uri);
|
|
891
|
+
return unsaved;
|
|
892
|
+
}
|
|
893
|
+
|
|
702
894
|
/**
|
|
703
895
|
* Register on `server`, and after a reconnect restore what the ended session
|
|
704
896
|
* had. Calls issued meanwhile wait for this.
|
|
@@ -737,7 +929,11 @@ export class DataSession<
|
|
|
737
929
|
* dropped, and one per document that could not be re-opened or written
|
|
738
930
|
* again for another reason than a conflict. A document that could not be
|
|
739
931
|
* re-opened is forgotten; a failed write keeps its record, for the next
|
|
740
|
-
* restore to decide again.
|
|
932
|
+
* restore to decide again. A document closed while the restore runs, or
|
|
933
|
+
* whose file is deleted then, is neither restored nor reported. One gone
|
|
934
|
+
* while the connection was down, which the server reads back without a
|
|
935
|
+
* model, is forgotten, and goes into the report of lost unsaved text when
|
|
936
|
+
* the session wrote it since its last save, or created it and never saved it.
|
|
741
937
|
*
|
|
742
938
|
* Each document's dirty state goes to the host too, read once the watch is
|
|
743
939
|
* in place and any write is sent: a flip while the connection was down, or
|
|
@@ -751,21 +947,62 @@ export class DataSession<
|
|
|
751
947
|
// server numbers afresh, below the versions saved before it.
|
|
752
948
|
this.savedVersions.clear();
|
|
753
949
|
const reopened = new Map<string, DataSessionDocument<TTransfer, TServer>>();
|
|
950
|
+
const lost: string[] = [];
|
|
754
951
|
for (const uri of [...this.openUris]) {
|
|
952
|
+
// Closed, or its file deleted, while the restore ran.
|
|
953
|
+
if (!this.openUris.has(uri)) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
755
956
|
try {
|
|
756
957
|
const document = await server.openModelDocument({ uri, clientId: this.clientId });
|
|
757
|
-
|
|
958
|
+
if (!this.openUris.has(uri)) {
|
|
959
|
+
// Forgotten while it re-opened. A close of the caller's would
|
|
960
|
+
// follow, but a deletion sends none.
|
|
961
|
+
await server.closeModelDocument({ uri, clientId: this.clientId }).catch(() => undefined);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
// Recorded before the watch, so a deletion heard while it is under
|
|
965
|
+
// way finds the re-open under the URI it answered with, which a
|
|
966
|
+
// restarted server may key differently.
|
|
967
|
+
this.opens.set(uri, { serverUri: document.uri, deleted: false });
|
|
968
|
+
await server.watchModelDocument({ uri, clientId: this.clientId }).catch(async (error: unknown) => {
|
|
969
|
+
// Closed, or the server holds the re-open until the session
|
|
970
|
+
// ends; no other call relies on it while the restore runs.
|
|
971
|
+
await server.closeModelDocument({ uri, clientId: this.clientId }).catch(() => undefined);
|
|
972
|
+
throw error;
|
|
973
|
+
});
|
|
758
974
|
if (this.openUris.has(uri)) {
|
|
759
|
-
this.serverUris.set(uri, document.uri);
|
|
760
975
|
this.recordText(uri, document);
|
|
761
976
|
}
|
|
762
977
|
reopened.set(uri, document);
|
|
763
978
|
} catch (error: unknown) {
|
|
979
|
+
// Gone while the connection was down, which no deletion told the
|
|
980
|
+
// session of: the server answers a read of it without a model.
|
|
981
|
+
const read = await server.getModelDocument({ uri }).catch(() => undefined);
|
|
982
|
+
// Forgotten meanwhile, so nothing is lost.
|
|
983
|
+
if (!this.openUris.has(uri)) {
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (read && !read.model) {
|
|
987
|
+
// No deletion reached the client either, so unsaved text the
|
|
988
|
+
// document took with it is reported, as a drop's other losses are.
|
|
989
|
+
const write = this.unsavedWrites.get(uri);
|
|
990
|
+
if (this.forgetGoneDocument(uri)) {
|
|
991
|
+
lost.push(uri);
|
|
992
|
+
}
|
|
993
|
+
// Kept for `reapply`, where it blocks its set as a document
|
|
994
|
+
// that could not be re-opened does, and goes.
|
|
995
|
+
if (write) {
|
|
996
|
+
this.unsavedWrites.set(uri, write);
|
|
997
|
+
}
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
764
1000
|
// Its record goes with its set's, in `reapply`, which it blocks.
|
|
765
1001
|
this.openUris.delete(uri);
|
|
766
|
-
this.
|
|
1002
|
+
this.opens.delete(uri);
|
|
767
1003
|
this.lastAnswers.delete(uri);
|
|
768
1004
|
this.savedVersions.delete(uri);
|
|
1005
|
+
this.createdUris.delete(uri);
|
|
769
1006
|
this.host.reportError?.(error, resolve(DATA_SESSION_RESTORE_FAILED, { uri, detail: describeError(error) }));
|
|
770
1007
|
}
|
|
771
1008
|
}
|
|
@@ -774,7 +1011,6 @@ export class DataSession<
|
|
|
774
1011
|
const call = 'update' in write.call ? write.call.update : write.call.updates;
|
|
775
1012
|
sets.set(call, [...(sets.get(call) ?? []), [uri, write]]);
|
|
776
1013
|
}
|
|
777
|
-
const lost: string[] = [];
|
|
778
1014
|
for (const members of sets.values()) {
|
|
779
1015
|
lost.push(...(await this.reapply(server, members, reopened)));
|
|
780
1016
|
}
|
|
@@ -785,7 +1021,15 @@ export class DataSession<
|
|
|
785
1021
|
// A failed read, or one without text, leaves the client's dirty
|
|
786
1022
|
// state as it was; the document is restored all the same.
|
|
787
1023
|
const current = await server.getModelDocument({ uri }).catch(() => undefined);
|
|
788
|
-
|
|
1024
|
+
// Again after the read: one closed or deleted meanwhile has no
|
|
1025
|
+
// state to tell.
|
|
1026
|
+
if (current?.text !== undefined && this.openUris.has(uri)) {
|
|
1027
|
+
// A created document reads clean only once a save gave it a
|
|
1028
|
+
// baseline, so its text is no longer unsaved. A write record
|
|
1029
|
+
// stays: a reverted document reads clean too, its write unsent.
|
|
1030
|
+
if (!current.text.dirty) {
|
|
1031
|
+
this.createdUris.delete(uri);
|
|
1032
|
+
}
|
|
789
1033
|
try {
|
|
790
1034
|
this.host.restoreDirty?.({ uri: current.uri, text: current.text });
|
|
791
1035
|
} catch {
|
|
@@ -853,10 +1097,14 @@ export class DataSession<
|
|
|
853
1097
|
}
|
|
854
1098
|
}
|
|
855
1099
|
// A document that could not be re-opened is forgotten, and was reported
|
|
856
|
-
// as such.
|
|
1100
|
+
// as such; one closed, saved or deleted while a write ran lost nothing.
|
|
857
1101
|
const lose = (): string[] => {
|
|
858
|
-
outcomes.filter(member => member.outcome !== 'kept'
|
|
859
|
-
|
|
1102
|
+
const gone = outcomes.filter(member => member.outcome !== 'kept' && current(member.uri, member.write));
|
|
1103
|
+
gone.forEach(member => {
|
|
1104
|
+
this.unsavedWrites.delete(member.uri);
|
|
1105
|
+
this.createdUris.delete(member.uri);
|
|
1106
|
+
});
|
|
1107
|
+
return gone.filter(member => member.outcome === 'lost' || member.outcome === 'resend').map(member => member.uri);
|
|
860
1108
|
};
|
|
861
1109
|
if (outcomes.some(member => member.outcome === 'lost' || member.outcome === 'failed')) {
|
|
862
1110
|
return lose();
|
|
@@ -870,6 +1118,7 @@ export class DataSession<
|
|
|
870
1118
|
if (resend.length === 0) {
|
|
871
1119
|
return [];
|
|
872
1120
|
}
|
|
1121
|
+
const opens = resend.map(member => this.opens.get(member.uri));
|
|
873
1122
|
let documents: DataSessionDocument<TTransfer, TServer>[];
|
|
874
1123
|
try {
|
|
875
1124
|
const { call } = resend[0].write;
|
|
@@ -891,13 +1140,15 @@ export class DataSession<
|
|
|
891
1140
|
return lose();
|
|
892
1141
|
}
|
|
893
1142
|
for (const { uri, write, baseVersion } of resend) {
|
|
1143
|
+
// Closed, saved or deleted while the write ran: nothing to restore.
|
|
1144
|
+
if (!current(uri, write)) {
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
894
1147
|
this.host.reportError?.(error, resolve(DATA_SESSION_RESTORE_FAILED, { uri, detail: describeError(error) }));
|
|
895
1148
|
// Kept for the next restore, and numbered from the re-opened
|
|
896
1149
|
// version, as the kept records are: a record numbered by a server
|
|
897
1150
|
// that has since restarted would have later answers ignored.
|
|
898
|
-
|
|
899
|
-
this.unsavedWrites.set(uri, { ...write, answer: { ...write.answer, version: baseVersion } });
|
|
900
|
-
}
|
|
1151
|
+
this.unsavedWrites.set(uri, { ...write, answer: { ...write.answer, version: baseVersion } });
|
|
901
1152
|
}
|
|
902
1153
|
return [];
|
|
903
1154
|
}
|
|
@@ -906,7 +1157,7 @@ export class DataSession<
|
|
|
906
1157
|
// Recorded afresh, since the answer of a restarted server may be
|
|
907
1158
|
// numbered below the one it replaces.
|
|
908
1159
|
this.unsavedWrites.delete(member.uri);
|
|
909
|
-
this.recordWrite(member.uri, documents[index], member.write.baseHash, member.write.call);
|
|
1160
|
+
this.recordWrite(member.uri, opens[index], documents[index], member.write.baseHash, member.write.call);
|
|
910
1161
|
}
|
|
911
1162
|
});
|
|
912
1163
|
return [];
|
|
@@ -935,10 +1186,11 @@ export class DataSession<
|
|
|
935
1186
|
}
|
|
936
1187
|
|
|
937
1188
|
/**
|
|
938
|
-
* Record `document` as the answer to a write of `uri
|
|
939
|
-
* the URI's record if it has one. An answer
|
|
940
|
-
*
|
|
941
|
-
*
|
|
1189
|
+
* Record `document` as the answer to a write of `uri` sent under `open`,
|
|
1190
|
+
* keeping the base of the URI's record if it has one. An answer sent under
|
|
1191
|
+
* no open, or under one that has ended, is ignored. So is one numbered below
|
|
1192
|
+
* the record's: two writes of `uri` in flight at once may answer out of
|
|
1193
|
+
* order, and the record holds the one applied last.
|
|
942
1194
|
*
|
|
943
1195
|
* So is one numbered at or below the URI's entry in {@link savedVersions}.
|
|
944
1196
|
* The save came after that write was applied, so the save persisted its
|
|
@@ -947,10 +1199,15 @@ export class DataSession<
|
|
|
947
1199
|
*/
|
|
948
1200
|
protected recordWrite(
|
|
949
1201
|
uri: string,
|
|
1202
|
+
open: DataSessionOpen | undefined,
|
|
950
1203
|
document: DataSessionDocument<TTransfer, TServer>,
|
|
951
1204
|
baseHash: string | undefined,
|
|
952
1205
|
call: DataSessionUnsavedWrite<TTransfer, TServer>['call']
|
|
953
1206
|
): void {
|
|
1207
|
+
// Recorded, it would outlive that open.
|
|
1208
|
+
if (!open || this.opens.get(uri) !== open) {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
954
1211
|
const answer = this.answerOf(uri, document);
|
|
955
1212
|
const kept = this.unsavedWrites.get(uri);
|
|
956
1213
|
if (kept && answer.version < kept.answer.version) {
|
|
@@ -1001,18 +1258,18 @@ export class DataSession<
|
|
|
1001
1258
|
return running;
|
|
1002
1259
|
}
|
|
1003
1260
|
|
|
1004
|
-
/** {@link track} an open of `uri`,
|
|
1005
|
-
protected trackOpen<T>(uri: string, call: () => Promise<T>): Promise<T> {
|
|
1006
|
-
|
|
1261
|
+
/** {@link track} an open of `uri`, kept in {@link openings} until it settles. */
|
|
1262
|
+
protected trackOpen<T>(uri: string, call: (opening: DataSessionOpening) => Promise<T>): Promise<T> {
|
|
1263
|
+
const opening: DataSessionOpening = { closed: false };
|
|
1264
|
+
const openings = this.openings.get(uri) ?? new Set<DataSessionOpening>();
|
|
1265
|
+
this.openings.set(uri, openings.add(opening));
|
|
1007
1266
|
const done = (): void => {
|
|
1008
|
-
|
|
1009
|
-
if (
|
|
1010
|
-
this.
|
|
1011
|
-
} else {
|
|
1012
|
-
this.openingUris.delete(uri);
|
|
1267
|
+
openings.delete(opening);
|
|
1268
|
+
if (openings.size === 0 && this.openings.get(uri) === openings) {
|
|
1269
|
+
this.openings.delete(uri);
|
|
1013
1270
|
}
|
|
1014
1271
|
};
|
|
1015
|
-
const running = this.track([uri], call);
|
|
1272
|
+
const running = this.track([uri], () => call(opening));
|
|
1016
1273
|
running.then(done, done);
|
|
1017
1274
|
return running;
|
|
1018
1275
|
}
|