@hydranium/protocol 1.0.0-next.267 → 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 +19 -5
- 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/lib/logger.d.ts +5 -5
- package/lib/logger.d.ts.map +1 -1
- package/lib/messages/primitives.d.ts +8 -0
- package/lib/messages/primitives.d.ts.map +1 -1
- package/lib/messages/primitives.js +29 -0
- package/lib/messages/primitives.js.map +1 -1
- package/lib/tracer.d.ts.map +1 -1
- package/lib/tracer.js +2 -1
- package/lib/tracer.js.map +1 -1
- package/package.json +1 -1
- package/src/client/data-connection.ts +21 -6
- package/src/client/data-session.ts +332 -75
- package/src/logger.ts +5 -5
- package/src/messages/primitives.ts +30 -0
- package/src/tracer.ts +2 -1
|
@@ -14,6 +14,7 @@ const clock_1 = require("../clock");
|
|
|
14
14
|
const errors_1 = require("../errors");
|
|
15
15
|
const base_version_1 = require("../model-service/base-version");
|
|
16
16
|
const primitives_1 = require("../messages/primitives");
|
|
17
|
+
const noop_logger_1 = require("../noop-logger");
|
|
17
18
|
const random_uuid_1 = require("../random-uuid");
|
|
18
19
|
/**
|
|
19
20
|
* A session could not re-open a document it had open after its connection
|
|
@@ -91,17 +92,17 @@ class DataSession {
|
|
|
91
92
|
/** URIs this session has open, re-opened after a reconnect. */
|
|
92
93
|
openUris = new Set();
|
|
93
94
|
/**
|
|
94
|
-
* Per URI in {@link openUris},
|
|
95
|
-
*
|
|
96
|
-
*
|
|
95
|
+
* Per URI in {@link openUris}, or whose open answered and whose watch is
|
|
96
|
+
* still under way, the open it is under. A repeat open keeps it, as the
|
|
97
|
+
* server keeps its earlier open.
|
|
97
98
|
*/
|
|
98
|
-
|
|
99
|
+
opens = new Map();
|
|
99
100
|
/**
|
|
100
|
-
* Per URI,
|
|
101
|
-
*
|
|
102
|
-
*
|
|
101
|
+
* Per URI, the opens of it this session has under way. Such a URI counts
|
|
102
|
+
* as open for {@link withOpenDocument}, which would otherwise close it
|
|
103
|
+
* under the open that is still being made.
|
|
103
104
|
*/
|
|
104
|
-
|
|
105
|
+
openings = new Map();
|
|
105
106
|
/** Per URI written since its last save, what {@link restore} needs to write it again. */
|
|
106
107
|
unsavedWrites = new Map();
|
|
107
108
|
/**
|
|
@@ -118,6 +119,13 @@ class DataSession {
|
|
|
118
119
|
* version of a write still in flight as well, and that write is not saved.
|
|
119
120
|
*/
|
|
120
121
|
savedVersions = new Map();
|
|
122
|
+
/**
|
|
123
|
+
* URIs this session created whose whole text is still its own and unsaved,
|
|
124
|
+
* even with no write recorded: no save, clean flip, other client's write or
|
|
125
|
+
* reported loss has come since the create answered; see
|
|
126
|
+
* {@link forgetGoneDocument}.
|
|
127
|
+
*/
|
|
128
|
+
createdUris = new Set();
|
|
121
129
|
/** Per URI, this session's calls still in flight on it, which a close waits for. */
|
|
122
130
|
inFlight = new Map();
|
|
123
131
|
/** This session's saves still in flight, which a host's exit waits for; see {@link hasSavesInFlight}. */
|
|
@@ -156,12 +164,18 @@ class DataSession {
|
|
|
156
164
|
dirtySubscription;
|
|
157
165
|
/** The subscription to the host's `onDidUpdateDocument`, disposed with the session. */
|
|
158
166
|
updateSubscription;
|
|
167
|
+
/** The subscription to the host's `onDidDeleteDocument`, disposed with the session. */
|
|
168
|
+
deletionSubscription;
|
|
169
|
+
/** The host's logger, or one that logs nothing. */
|
|
170
|
+
logger;
|
|
159
171
|
constructor(clientId, host, label) {
|
|
160
172
|
this.clientId = clientId;
|
|
161
173
|
this.host = host;
|
|
162
174
|
this.label = label;
|
|
175
|
+
this.logger = host.logger ?? new noop_logger_1.NoopLogger();
|
|
163
176
|
this.dirtySubscription = host.onDidChangeDirty?.(event => this.forgetSavedWrite(event));
|
|
164
177
|
this.updateSubscription = host.onDidUpdateDocument?.(event => this.forgetSupersededWrite(event));
|
|
178
|
+
this.deletionSubscription = host.onDidDeleteDocument?.(event => this.forgetDeletedDocument(event));
|
|
165
179
|
}
|
|
166
180
|
/**
|
|
167
181
|
* The connected, READY server proxy once this session is registered on it,
|
|
@@ -207,10 +221,7 @@ class DataSession {
|
|
|
207
221
|
* from `getModelDocument({ includeDiagnostics: true })`.
|
|
208
222
|
*/
|
|
209
223
|
openDocument(args) {
|
|
210
|
-
return this.trackOpen(args.uri,
|
|
211
|
-
const server = await this.connected();
|
|
212
|
-
return this.watchOpened(server, args.uri, await server.openModelDocument({ ...args, clientId: this.clientId }));
|
|
213
|
-
});
|
|
224
|
+
return this.trackOpen(args.uri, opening => this.openAndWatch(args.uri, opening, false, server => server.openModelDocument({ ...args, clientId: this.clientId })));
|
|
214
225
|
}
|
|
215
226
|
/**
|
|
216
227
|
* Create a document that exists nowhere yet, open and watched for this
|
|
@@ -218,26 +229,64 @@ class DataSession {
|
|
|
218
229
|
* refuses a URI that exists on disk or that any client has open.
|
|
219
230
|
*/
|
|
220
231
|
createDocument(args) {
|
|
221
|
-
return this.trackOpen(args.uri,
|
|
232
|
+
return this.trackOpen(args.uri, opening => this.openAndWatch(args.uri, opening, true, server => server.createModelDocument({ ...args, clientId: this.clientId })));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Send `open` and watch what it answered, again for as long as a deletion
|
|
236
|
+
* heard while the watch is under way closes that open: sent after the
|
|
237
|
+
* deletion, the open fails as an open of a gone file does, or opens the
|
|
238
|
+
* file that is back. `created` marks the document in {@link createdUris}
|
|
239
|
+
* once the open answers, before the watch, so an event that clears the mark
|
|
240
|
+
* meanwhile clears it for good.
|
|
241
|
+
*
|
|
242
|
+
* An `opening` a close marked answers with the document, recording nothing
|
|
243
|
+
* and sending nothing again; one it marked before the open answered is
|
|
244
|
+
* undone, since the server may have taken the close first. A failed watch
|
|
245
|
+
* is undone too: the caller sees a failed open, and so would never close it.
|
|
246
|
+
*/
|
|
247
|
+
async openAndWatch(uri, opening, created, open) {
|
|
248
|
+
for (;;) {
|
|
222
249
|
const server = await this.connected();
|
|
223
|
-
|
|
224
|
-
|
|
250
|
+
const document = await open(server);
|
|
251
|
+
if (opening.closed) {
|
|
252
|
+
await this.undoOpen(server, uri, opening);
|
|
253
|
+
return document;
|
|
254
|
+
}
|
|
255
|
+
if (created) {
|
|
256
|
+
this.createdUris.add(uri);
|
|
257
|
+
}
|
|
258
|
+
let watched;
|
|
259
|
+
try {
|
|
260
|
+
watched = await this.watchOpened(server, uri, document);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
await this.undoOpen(server, uri, opening);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
if (watched || opening.closed) {
|
|
267
|
+
return watched ?? document;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
225
270
|
}
|
|
226
271
|
/**
|
|
227
|
-
* Watch a document this session just opened, and record it as open.
|
|
228
|
-
*
|
|
229
|
-
*
|
|
272
|
+
* Watch a document this session just opened, and record it as open.
|
|
273
|
+
* Answers `undefined`, recording nothing, when a deletion heard while the
|
|
274
|
+
* watch is under way has ended the open already, and the document,
|
|
275
|
+
* recording nothing, when a close has. A failed watch rejects with the open
|
|
276
|
+
* still recorded, for its caller to undo, see {@link undoOpen}.
|
|
230
277
|
*/
|
|
231
278
|
async watchOpened(server, uri, document) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
279
|
+
// Recorded before the watch, so a deletion heard while it is under way
|
|
280
|
+
// finds this open, which the server made before the deletion.
|
|
281
|
+
const open = this.opens.get(uri) ?? { serverUri: document.uri, deleted: false };
|
|
282
|
+
this.opens.set(uri, open);
|
|
283
|
+
await server.watchModelDocument({ uri, clientId: this.clientId });
|
|
284
|
+
if (this.opens.get(uri) !== open) {
|
|
285
|
+
// Sent again, it ends as an open sent after the deletion would; a
|
|
286
|
+
// close is the caller's later call, and stands.
|
|
287
|
+
return open.deleted ? undefined : document;
|
|
238
288
|
}
|
|
239
289
|
this.openUris.add(uri);
|
|
240
|
-
this.serverUris.set(uri, document.uri);
|
|
241
290
|
this.recordText(uri, document);
|
|
242
291
|
// Keyed as the server keys its notifications, which may not be how the
|
|
243
292
|
// caller spelled the URI.
|
|
@@ -253,18 +302,49 @@ class DataSession {
|
|
|
253
302
|
this.assertLive();
|
|
254
303
|
await this.settle(this.inFlight.get(args.uri));
|
|
255
304
|
// Forgotten before the close is sent, so a reconnect in between does not
|
|
256
|
-
// re-open a document the caller has closed
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
this.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
this.
|
|
263
|
-
this.savedVersions.delete(args.uri);
|
|
264
|
-
this.host.forgetDirty?.(serverUri);
|
|
305
|
+
// re-open a document the caller has closed. Every open of it under way
|
|
306
|
+
// by now is marked, even one called after this close, so it neither
|
|
307
|
+
// records it again nor is sent again.
|
|
308
|
+
this.openings.get(args.uri)?.forEach(opening => {
|
|
309
|
+
opening.closed = true;
|
|
310
|
+
});
|
|
311
|
+
this.forgetDocument(args.uri);
|
|
265
312
|
const server = await this.connected();
|
|
266
313
|
await server.closeModelDocument({ ...args, clientId: this.clientId });
|
|
267
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Close `uri` again after `opening` failed or was closed while under way,
|
|
317
|
+
* and drop what it recorded, unless the session still relies on the open,
|
|
318
|
+
* see {@link hasOpen}: the server holds one open per session and URI, so
|
|
319
|
+
* the close would end that one too.
|
|
320
|
+
*/
|
|
321
|
+
async undoOpen(server, uri, opening) {
|
|
322
|
+
if (this.hasOpen(uri, opening)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
this.opens.delete(uri);
|
|
326
|
+
this.createdUris.delete(uri);
|
|
327
|
+
await server.closeModelDocument({ uri, clientId: this.clientId }).catch(() => undefined);
|
|
328
|
+
}
|
|
329
|
+
/** Drop everything this session keeps of `uri`, which it no longer has open. */
|
|
330
|
+
forgetDocument(uri) {
|
|
331
|
+
const serverUri = this.opens.get(uri)?.serverUri ?? uri;
|
|
332
|
+
this.openUris.delete(uri);
|
|
333
|
+
this.opens.delete(uri);
|
|
334
|
+
this.unsavedWrites.delete(uri);
|
|
335
|
+
this.lastAnswers.delete(uri);
|
|
336
|
+
this.savedVersions.delete(uri);
|
|
337
|
+
this.createdUris.delete(uri);
|
|
338
|
+
this.host.forgetDirty?.(serverUri);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Whether this session has `uri` open, or has an open of it under way that
|
|
342
|
+
* no close has marked, other than `except`. A marked open records nothing,
|
|
343
|
+
* so nothing can rely on it.
|
|
344
|
+
*/
|
|
345
|
+
hasOpen(uri, except) {
|
|
346
|
+
return this.openUris.has(uri) || [...(this.openings.get(uri) ?? [])].some(opening => opening !== except && !opening.closed);
|
|
347
|
+
}
|
|
268
348
|
/**
|
|
269
349
|
* Open `args.uri`, run `fn` with the opened snapshot, and close it once
|
|
270
350
|
* `fn` settles, whether it returned or threw. A URI this session already
|
|
@@ -272,7 +352,7 @@ class DataSession {
|
|
|
272
352
|
* undoes only the open this call made.
|
|
273
353
|
*/
|
|
274
354
|
async withOpenDocument(args, fn) {
|
|
275
|
-
const alreadyOpen = this.
|
|
355
|
+
const alreadyOpen = this.hasOpen(args.uri);
|
|
276
356
|
const document = await this.openDocument(args);
|
|
277
357
|
try {
|
|
278
358
|
return await fn(document);
|
|
@@ -289,8 +369,9 @@ class DataSession {
|
|
|
289
369
|
const server = await this.connected();
|
|
290
370
|
const update = { ...args };
|
|
291
371
|
const baseHash = await this.baseHashOf(server, args.uri, args.baseVersion);
|
|
372
|
+
const open = this.opens.get(args.uri);
|
|
292
373
|
const document = await server.updateModelDocument({ ...args, clientId: this.clientId });
|
|
293
|
-
this.recordWrite(args.uri, document, baseHash, { update });
|
|
374
|
+
this.recordWrite(args.uri, open, document, baseHash, { update });
|
|
294
375
|
return document;
|
|
295
376
|
});
|
|
296
377
|
}
|
|
@@ -306,8 +387,9 @@ class DataSession {
|
|
|
306
387
|
const updates = { ...args, updates: args.updates.map(update => ({ ...update })) };
|
|
307
388
|
const server = await this.connected();
|
|
308
389
|
const baseHashes = await Promise.all(args.updates.map(update => this.baseHashOf(server, update.uri, update.baseVersion)));
|
|
390
|
+
const opens = args.updates.map(update => this.opens.get(update.uri));
|
|
309
391
|
const documents = await server.updateModelDocuments({ ...args, clientId: this.clientId });
|
|
310
|
-
args.updates.forEach((update, index) => this.recordWrite(update.uri, documents[index], baseHashes[index], { updates, index }));
|
|
392
|
+
args.updates.forEach((update, index) => this.recordWrite(update.uri, opens[index], documents[index], baseHashes[index], { updates, index }));
|
|
311
393
|
return documents;
|
|
312
394
|
});
|
|
313
395
|
}
|
|
@@ -326,7 +408,13 @@ class DataSession {
|
|
|
326
408
|
trackSave(uri, send) {
|
|
327
409
|
const saving = this.track([uri], async () => {
|
|
328
410
|
const server = await this.connected();
|
|
411
|
+
const open = this.opens.get(uri);
|
|
329
412
|
const document = await send(server);
|
|
413
|
+
// Sent under no open, or under one a close or deletion has ended
|
|
414
|
+
// since: recorded, it would outlive that open.
|
|
415
|
+
if (!open || this.opens.get(uri) !== open) {
|
|
416
|
+
return document;
|
|
417
|
+
}
|
|
330
418
|
// Kept only when its last answer is newer than the text written. Kept
|
|
331
419
|
// whenever it changed while the save ran, a record the save covered
|
|
332
420
|
// survives, and a restore then reports it lost.
|
|
@@ -335,6 +423,7 @@ class DataSession {
|
|
|
335
423
|
this.unsavedWrites.delete(uri);
|
|
336
424
|
}
|
|
337
425
|
this.recordText(uri, document);
|
|
426
|
+
this.createdUris.delete(uri);
|
|
338
427
|
// The higher of two saves' answers, which may arrive out of order.
|
|
339
428
|
const saved = this.savedVersions.get(uri);
|
|
340
429
|
if (saved === undefined || document.persisted.version > saved) {
|
|
@@ -420,23 +509,25 @@ class DataSession {
|
|
|
420
509
|
detach() {
|
|
421
510
|
this.detached = true;
|
|
422
511
|
this.openUris.clear();
|
|
423
|
-
this.
|
|
512
|
+
this.opens.clear();
|
|
424
513
|
this.unsavedWrites.clear();
|
|
425
514
|
this.lastAnswers.clear();
|
|
426
515
|
this.savedVersions.clear();
|
|
516
|
+
this.createdUris.clear();
|
|
427
517
|
if (!this.disposed) {
|
|
428
518
|
this.disposed = true;
|
|
429
519
|
this.fireDispose();
|
|
430
520
|
}
|
|
431
521
|
}
|
|
432
522
|
/**
|
|
433
|
-
* Dispose {@link dirtySubscription}
|
|
434
|
-
* {@link onDidDispose} and dispose its
|
|
435
|
-
* nothing.
|
|
523
|
+
* Dispose {@link dirtySubscription}, {@link updateSubscription} and
|
|
524
|
+
* {@link deletionSubscription}, fire {@link onDidDispose} and dispose its
|
|
525
|
+
* emitter, so a second call fires nothing.
|
|
436
526
|
*/
|
|
437
527
|
fireDispose() {
|
|
438
528
|
this.dirtySubscription?.dispose();
|
|
439
529
|
this.updateSubscription?.dispose();
|
|
530
|
+
this.deletionSubscription?.dispose();
|
|
440
531
|
this.disposeEmitter.fire(undefined);
|
|
441
532
|
this.disposeEmitter.dispose();
|
|
442
533
|
}
|
|
@@ -454,9 +545,10 @@ class DataSession {
|
|
|
454
545
|
}
|
|
455
546
|
/**
|
|
456
547
|
* Drop the record in {@link unsavedWrites} of a document this session has
|
|
457
|
-
* open once the server says it turned clean
|
|
458
|
-
*
|
|
459
|
-
* another client's save
|
|
548
|
+
* open once the server says it turned clean, and its mark in
|
|
549
|
+
* {@link createdUris}: it holds no unsaved text then, and a record kept
|
|
550
|
+
* would have a restore report the write lost once another client's save
|
|
551
|
+
* changed the file.
|
|
460
552
|
*
|
|
461
553
|
* The server sends a flip only for a document someone watches, so a
|
|
462
554
|
* document the session writes without watching it keeps its record, and
|
|
@@ -468,9 +560,10 @@ class DataSession {
|
|
|
468
560
|
}
|
|
469
561
|
// The flip names the server's key, which is not always the caller's
|
|
470
562
|
// spelling that the records are kept under.
|
|
471
|
-
for (const [uri,
|
|
472
|
-
if (serverUri === event.uri) {
|
|
563
|
+
for (const [uri, open] of this.opens) {
|
|
564
|
+
if (open.serverUri === event.uri) {
|
|
473
565
|
this.unsavedWrites.delete(uri);
|
|
566
|
+
this.createdUris.delete(uri);
|
|
474
567
|
}
|
|
475
568
|
}
|
|
476
569
|
}
|
|
@@ -488,18 +581,61 @@ class DataSession {
|
|
|
488
581
|
* `'rebuilt'` event carries no new text. Like {@link forgetSavedWrite}, it
|
|
489
582
|
* depends on the server sending the event, which it does only for a
|
|
490
583
|
* document someone watches.
|
|
584
|
+
*
|
|
585
|
+
* The mark in {@link createdUris} goes on any such event: the unsaved text
|
|
586
|
+
* is then the other client's, and a record still kept reports the
|
|
587
|
+
* session's own.
|
|
491
588
|
*/
|
|
492
589
|
forgetSupersededWrite(event) {
|
|
493
590
|
if (event.reason !== 'changed' || this.isOwnEcho(event.sourceClientId)) {
|
|
494
591
|
return;
|
|
495
592
|
}
|
|
496
|
-
for (const [uri,
|
|
593
|
+
for (const [uri, open] of this.opens) {
|
|
594
|
+
if (open.serverUri !== event.document.uri) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
this.createdUris.delete(uri);
|
|
497
598
|
const write = this.unsavedWrites.get(uri);
|
|
498
|
-
if (
|
|
599
|
+
if (write && this.restoreOutcome(write, event.document) !== 'kept') {
|
|
499
600
|
this.unsavedWrites.delete(uri);
|
|
500
601
|
}
|
|
501
602
|
}
|
|
502
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Forget a document whose file was deleted, which closed this session's
|
|
606
|
+
* open. At once rather than after the calls in flight settle, which would
|
|
607
|
+
* let a restore re-open the file that is gone. Unwatched rather than
|
|
608
|
+
* closed: the server keeps the watch, and a close could end a re-open of a
|
|
609
|
+
* re-created file still in flight. Unsaved writes are logged, not
|
|
610
|
+
* reported: the client hears the deletion too.
|
|
611
|
+
*/
|
|
612
|
+
forgetDeletedDocument(event) {
|
|
613
|
+
for (const [uri, open] of this.opens) {
|
|
614
|
+
if (open.serverUri !== event.uri) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
open.deleted = true;
|
|
618
|
+
this.forgetGoneDocument(uri);
|
|
619
|
+
this.connected()
|
|
620
|
+
.then(server => server.unwatchModelDocument({ uri, clientId: this.clientId }))
|
|
621
|
+
.catch(() => undefined);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* {@link forgetDocument} a document that is gone, logging any unsaved text
|
|
626
|
+
* it drops, written or created; returns whether it dropped some.
|
|
627
|
+
*/
|
|
628
|
+
forgetGoneDocument(uri) {
|
|
629
|
+
const unsaved = this.unsavedWrites.has(uri) || this.createdUris.has(uri);
|
|
630
|
+
if (unsaved) {
|
|
631
|
+
this.logger.withUri(uri).warn('Dropped unsaved text: the document is gone');
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
this.logger.withUri(uri).debug('Forgot the document: it is gone');
|
|
635
|
+
}
|
|
636
|
+
this.forgetDocument(uri);
|
|
637
|
+
return unsaved;
|
|
638
|
+
}
|
|
503
639
|
/**
|
|
504
640
|
* Register on `server`, and after a reconnect restore what the ended session
|
|
505
641
|
* had. Calls issued meanwhile wait for this.
|
|
@@ -537,7 +673,11 @@ class DataSession {
|
|
|
537
673
|
* dropped, and one per document that could not be re-opened or written
|
|
538
674
|
* again for another reason than a conflict. A document that could not be
|
|
539
675
|
* re-opened is forgotten; a failed write keeps its record, for the next
|
|
540
|
-
* restore to decide again.
|
|
676
|
+
* restore to decide again. A document closed while the restore runs, or
|
|
677
|
+
* whose file is deleted then, is neither restored nor reported. One gone
|
|
678
|
+
* while the connection was down, which the server reads back without a
|
|
679
|
+
* model, is forgotten, and goes into the report of lost unsaved text when
|
|
680
|
+
* the session wrote it since its last save, or created it and never saved it.
|
|
541
681
|
*
|
|
542
682
|
* Each document's dirty state goes to the host too, read once the watch is
|
|
543
683
|
* in place and any write is sent: a flip while the connection was down, or
|
|
@@ -551,22 +691,63 @@ class DataSession {
|
|
|
551
691
|
// server numbers afresh, below the versions saved before it.
|
|
552
692
|
this.savedVersions.clear();
|
|
553
693
|
const reopened = new Map();
|
|
694
|
+
const lost = [];
|
|
554
695
|
for (const uri of [...this.openUris]) {
|
|
696
|
+
// Closed, or its file deleted, while the restore ran.
|
|
697
|
+
if (!this.openUris.has(uri)) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
555
700
|
try {
|
|
556
701
|
const document = await server.openModelDocument({ uri, clientId: this.clientId });
|
|
557
|
-
|
|
702
|
+
if (!this.openUris.has(uri)) {
|
|
703
|
+
// Forgotten while it re-opened. A close of the caller's would
|
|
704
|
+
// follow, but a deletion sends none.
|
|
705
|
+
await server.closeModelDocument({ uri, clientId: this.clientId }).catch(() => undefined);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
// Recorded before the watch, so a deletion heard while it is under
|
|
709
|
+
// way finds the re-open under the URI it answered with, which a
|
|
710
|
+
// restarted server may key differently.
|
|
711
|
+
this.opens.set(uri, { serverUri: document.uri, deleted: false });
|
|
712
|
+
await server.watchModelDocument({ uri, clientId: this.clientId }).catch(async (error) => {
|
|
713
|
+
// Closed, or the server holds the re-open until the session
|
|
714
|
+
// ends; no other call relies on it while the restore runs.
|
|
715
|
+
await server.closeModelDocument({ uri, clientId: this.clientId }).catch(() => undefined);
|
|
716
|
+
throw error;
|
|
717
|
+
});
|
|
558
718
|
if (this.openUris.has(uri)) {
|
|
559
|
-
this.serverUris.set(uri, document.uri);
|
|
560
719
|
this.recordText(uri, document);
|
|
561
720
|
}
|
|
562
721
|
reopened.set(uri, document);
|
|
563
722
|
}
|
|
564
723
|
catch (error) {
|
|
724
|
+
// Gone while the connection was down, which no deletion told the
|
|
725
|
+
// session of: the server answers a read of it without a model.
|
|
726
|
+
const read = await server.getModelDocument({ uri }).catch(() => undefined);
|
|
727
|
+
// Forgotten meanwhile, so nothing is lost.
|
|
728
|
+
if (!this.openUris.has(uri)) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (read && !read.model) {
|
|
732
|
+
// No deletion reached the client either, so unsaved text the
|
|
733
|
+
// document took with it is reported, as a drop's other losses are.
|
|
734
|
+
const write = this.unsavedWrites.get(uri);
|
|
735
|
+
if (this.forgetGoneDocument(uri)) {
|
|
736
|
+
lost.push(uri);
|
|
737
|
+
}
|
|
738
|
+
// Kept for `reapply`, where it blocks its set as a document
|
|
739
|
+
// that could not be re-opened does, and goes.
|
|
740
|
+
if (write) {
|
|
741
|
+
this.unsavedWrites.set(uri, write);
|
|
742
|
+
}
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
565
745
|
// Its record goes with its set's, in `reapply`, which it blocks.
|
|
566
746
|
this.openUris.delete(uri);
|
|
567
|
-
this.
|
|
747
|
+
this.opens.delete(uri);
|
|
568
748
|
this.lastAnswers.delete(uri);
|
|
569
749
|
this.savedVersions.delete(uri);
|
|
750
|
+
this.createdUris.delete(uri);
|
|
570
751
|
this.host.reportError?.(error, (0, primitives_1.resolve)(exports.DATA_SESSION_RESTORE_FAILED, { uri, detail: (0, primitives_1.describeError)(error) }));
|
|
571
752
|
}
|
|
572
753
|
}
|
|
@@ -575,7 +756,6 @@ class DataSession {
|
|
|
575
756
|
const call = 'update' in write.call ? write.call.update : write.call.updates;
|
|
576
757
|
sets.set(call, [...(sets.get(call) ?? []), [uri, write]]);
|
|
577
758
|
}
|
|
578
|
-
const lost = [];
|
|
579
759
|
for (const members of sets.values()) {
|
|
580
760
|
lost.push(...(await this.reapply(server, members, reopened)));
|
|
581
761
|
}
|
|
@@ -586,7 +766,15 @@ class DataSession {
|
|
|
586
766
|
// A failed read, or one without text, leaves the client's dirty
|
|
587
767
|
// state as it was; the document is restored all the same.
|
|
588
768
|
const current = await server.getModelDocument({ uri }).catch(() => undefined);
|
|
589
|
-
|
|
769
|
+
// Again after the read: one closed or deleted meanwhile has no
|
|
770
|
+
// state to tell.
|
|
771
|
+
if (current?.text !== undefined && this.openUris.has(uri)) {
|
|
772
|
+
// A created document reads clean only once a save gave it a
|
|
773
|
+
// baseline, so its text is no longer unsaved. A write record
|
|
774
|
+
// stays: a reverted document reads clean too, its write unsent.
|
|
775
|
+
if (!current.text.dirty) {
|
|
776
|
+
this.createdUris.delete(uri);
|
|
777
|
+
}
|
|
590
778
|
try {
|
|
591
779
|
this.host.restoreDirty?.({ uri: current.uri, text: current.text });
|
|
592
780
|
}
|
|
@@ -645,10 +833,14 @@ class DataSession {
|
|
|
645
833
|
}
|
|
646
834
|
}
|
|
647
835
|
// A document that could not be re-opened is forgotten, and was reported
|
|
648
|
-
// as such.
|
|
836
|
+
// as such; one closed, saved or deleted while a write ran lost nothing.
|
|
649
837
|
const lose = () => {
|
|
650
|
-
outcomes.filter(member => member.outcome !== 'kept'
|
|
651
|
-
|
|
838
|
+
const gone = outcomes.filter(member => member.outcome !== 'kept' && current(member.uri, member.write));
|
|
839
|
+
gone.forEach(member => {
|
|
840
|
+
this.unsavedWrites.delete(member.uri);
|
|
841
|
+
this.createdUris.delete(member.uri);
|
|
842
|
+
});
|
|
843
|
+
return gone.filter(member => member.outcome === 'lost' || member.outcome === 'resend').map(member => member.uri);
|
|
652
844
|
};
|
|
653
845
|
if (outcomes.some(member => member.outcome === 'lost' || member.outcome === 'failed')) {
|
|
654
846
|
return lose();
|
|
@@ -660,6 +852,7 @@ class DataSession {
|
|
|
660
852
|
if (resend.length === 0) {
|
|
661
853
|
return [];
|
|
662
854
|
}
|
|
855
|
+
const opens = resend.map(member => this.opens.get(member.uri));
|
|
663
856
|
let documents;
|
|
664
857
|
try {
|
|
665
858
|
const { call } = resend[0].write;
|
|
@@ -683,13 +876,15 @@ class DataSession {
|
|
|
683
876
|
return lose();
|
|
684
877
|
}
|
|
685
878
|
for (const { uri, write, baseVersion } of resend) {
|
|
879
|
+
// Closed, saved or deleted while the write ran: nothing to restore.
|
|
880
|
+
if (!current(uri, write)) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
686
883
|
this.host.reportError?.(error, (0, primitives_1.resolve)(exports.DATA_SESSION_RESTORE_FAILED, { uri, detail: (0, primitives_1.describeError)(error) }));
|
|
687
884
|
// Kept for the next restore, and numbered from the re-opened
|
|
688
885
|
// version, as the kept records are: a record numbered by a server
|
|
689
886
|
// that has since restarted would have later answers ignored.
|
|
690
|
-
|
|
691
|
-
this.unsavedWrites.set(uri, { ...write, answer: { ...write.answer, version: baseVersion } });
|
|
692
|
-
}
|
|
887
|
+
this.unsavedWrites.set(uri, { ...write, answer: { ...write.answer, version: baseVersion } });
|
|
693
888
|
}
|
|
694
889
|
return [];
|
|
695
890
|
}
|
|
@@ -698,7 +893,7 @@ class DataSession {
|
|
|
698
893
|
// Recorded afresh, since the answer of a restarted server may be
|
|
699
894
|
// numbered below the one it replaces.
|
|
700
895
|
this.unsavedWrites.delete(member.uri);
|
|
701
|
-
this.recordWrite(member.uri, documents[index], member.write.baseHash, member.write.call);
|
|
896
|
+
this.recordWrite(member.uri, opens[index], documents[index], member.write.baseHash, member.write.call);
|
|
702
897
|
}
|
|
703
898
|
});
|
|
704
899
|
return [];
|
|
@@ -725,17 +920,22 @@ class DataSession {
|
|
|
725
920
|
return read?.text?.version === baseVersion ? read.text.hash : undefined;
|
|
726
921
|
}
|
|
727
922
|
/**
|
|
728
|
-
* Record `document` as the answer to a write of `uri
|
|
729
|
-
* the URI's record if it has one. An answer
|
|
730
|
-
*
|
|
731
|
-
*
|
|
923
|
+
* Record `document` as the answer to a write of `uri` sent under `open`,
|
|
924
|
+
* keeping the base of the URI's record if it has one. An answer sent under
|
|
925
|
+
* no open, or under one that has ended, is ignored. So is one numbered below
|
|
926
|
+
* the record's: two writes of `uri` in flight at once may answer out of
|
|
927
|
+
* order, and the record holds the one applied last.
|
|
732
928
|
*
|
|
733
929
|
* So is one numbered at or below the URI's entry in {@link savedVersions}.
|
|
734
930
|
* The save came after that write was applied, so the save persisted its
|
|
735
931
|
* text or a later write replaced it, and its answer carries the text as it
|
|
736
932
|
* was when sent, which a restore would report as lost.
|
|
737
933
|
*/
|
|
738
|
-
recordWrite(uri, document, baseHash, call) {
|
|
934
|
+
recordWrite(uri, open, document, baseHash, call) {
|
|
935
|
+
// Recorded, it would outlive that open.
|
|
936
|
+
if (!open || this.opens.get(uri) !== open) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
739
939
|
const answer = this.answerOf(uri, document);
|
|
740
940
|
const kept = this.unsavedWrites.get(uri);
|
|
741
941
|
if (kept && answer.version < kept.answer.version) {
|
|
@@ -780,19 +980,18 @@ class DataSession {
|
|
|
780
980
|
running.then(done, done);
|
|
781
981
|
return running;
|
|
782
982
|
}
|
|
783
|
-
/** {@link track} an open of `uri`,
|
|
983
|
+
/** {@link track} an open of `uri`, kept in {@link openings} until it settles. */
|
|
784
984
|
trackOpen(uri, call) {
|
|
785
|
-
|
|
985
|
+
const opening = { closed: false };
|
|
986
|
+
const openings = this.openings.get(uri) ?? new Set();
|
|
987
|
+
this.openings.set(uri, openings.add(opening));
|
|
786
988
|
const done = () => {
|
|
787
|
-
|
|
788
|
-
if (
|
|
789
|
-
this.
|
|
790
|
-
}
|
|
791
|
-
else {
|
|
792
|
-
this.openingUris.delete(uri);
|
|
989
|
+
openings.delete(opening);
|
|
990
|
+
if (openings.size === 0 && this.openings.get(uri) === openings) {
|
|
991
|
+
this.openings.delete(uri);
|
|
793
992
|
}
|
|
794
993
|
};
|
|
795
|
-
const running = this.track([uri], call);
|
|
994
|
+
const running = this.track([uri], () => call(opening));
|
|
796
995
|
running.then(done, done);
|
|
797
996
|
return running;
|
|
798
997
|
}
|