@hydranium/protocol 1.0.0-next.185 → 1.0.0-next.188
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.js +2 -2
- package/lib/client/data-connection.js.map +1 -1
- package/lib/client/data-port.d.ts +1 -1
- package/lib/client/data-session.d.ts +39 -21
- package/lib/client/data-session.d.ts.map +1 -1
- package/lib/client/data-session.js +69 -43
- package/lib/client/data-session.js.map +1 -1
- package/lib/data/data-server-protocol.d.ts +2 -4
- package/lib/data/data-server-protocol.d.ts.map +1 -1
- package/lib/data/events.d.ts +11 -5
- package/lib/data/events.d.ts.map +1 -1
- package/lib/data/requests.d.ts +4 -4
- package/lib/data/requests.d.ts.map +1 -1
- package/lib/errors.d.ts +31 -25
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +58 -23
- package/lib/errors.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/messages/index.d.ts +2 -2
- package/lib/messages/index.d.ts.map +1 -1
- package/lib/messages/index.js +3 -2
- package/lib/messages/index.js.map +1 -1
- package/lib/model-service/base-version.d.ts +64 -0
- package/lib/model-service/base-version.d.ts.map +1 -0
- package/lib/model-service/{based-on.js → base-version.js} +21 -15
- package/lib/model-service/base-version.js.map +1 -0
- package/lib/model-service/index.d.ts +1 -1
- package/lib/model-service/index.d.ts.map +1 -1
- package/lib/model-service/index.js +2 -2
- package/lib/model-service/index.js.map +1 -1
- package/lib/patch-merge.d.ts +11 -9
- package/lib/patch-merge.d.ts.map +1 -1
- package/lib/patch-merge.js +2 -2
- package/lib/patch-merge.js.map +1 -1
- package/lib/reconcile-write.d.ts +65 -0
- package/lib/reconcile-write.d.ts.map +1 -0
- package/lib/reconcile-write.js +67 -0
- package/lib/reconcile-write.js.map +1 -0
- package/lib/rpc/create-rpc-proxy.d.ts +3 -0
- package/lib/rpc/create-rpc-proxy.d.ts.map +1 -1
- package/lib/rpc/create-rpc-proxy.js +6 -1
- package/lib/rpc/create-rpc-proxy.js.map +1 -1
- package/lib/transfer-document.d.ts +59 -61
- package/lib/transfer-document.d.ts.map +1 -1
- package/lib/transfer-document.js +15 -20
- package/lib/transfer-document.js.map +1 -1
- package/lib/util.d.ts +1 -1
- package/lib/util.js +1 -1
- package/package.json +1 -1
- package/src/client/data-connection.ts +2 -2
- package/src/client/data-port.ts +1 -1
- package/src/client/data-session.ts +89 -51
- package/src/data/data-server-protocol.ts +2 -4
- package/src/data/events.ts +11 -5
- package/src/data/requests.ts +4 -4
- package/src/errors.ts +71 -31
- package/src/index.ts +1 -0
- package/src/messages/index.ts +2 -2
- package/src/model-service/base-version.ts +72 -0
- package/src/model-service/index.ts +2 -2
- package/src/patch-merge.ts +11 -9
- package/src/reconcile-write.ts +124 -0
- package/src/rpc/create-rpc-proxy.ts +6 -1
- package/src/transfer-document.ts +70 -65
- package/src/util.ts +1 -1
- package/lib/model-service/based-on.d.ts +0 -57
- package/lib/model-service/based-on.d.ts.map +0 -1
- package/lib/model-service/based-on.js.map +0 -1
- package/src/model-service/based-on.ts +0 -63
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transfer-document.js","sourceRoot":"","sources":["../src/transfer-document.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;AAIlF
|
|
1
|
+
{"version":3,"file":"transfer-document.js","sourceRoot":"","sources":["../src/transfer-document.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;AAIlF,+DAAmG;AA4EnG,IAAiB,gBAAgB,CAmDhC;AAnDD,WAAiB,gBAAgB;IAC9B;;;OAGG;IACH,SAAgB,QAAQ,CACrB,QAAkD;QAElD,OAAO,QAAQ,CAAC,KAAK,KAAK,SAAS,CAAC;IACvC,CAAC;IAJe,iBAAA,QAAQ,WAIvB,CAAA;IAED;;;;;OAKG;IACH,SAAgB,YAAY,CACzB,QAAkD;QAElD,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,kBAAkB,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,QAAQ,CAAC;IACnB,CAAC;IAPe,iBAAA,YAAY,eAO3B,CAAA;IAED,4DAA4D;IAC5D,SAAgB,MAAM,CACnB,GAAW;QAEX,OAAO,EAAE,GAAG,EAAE,CAAC;IAClB,CAAC;IAJe,iBAAA,MAAM,SAIrB,CAAA;IAED;;;OAGG;IACH,SAAgB,MAAM,CACnB,GAAW,EACX,OAAe,EACf,IAAe,EACf,IAAY,EACZ,WAA2B,EAC3B,IAAgB;QAEhB,OAAO;YACJ,GAAG;YACH,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAA,6BAAc,EAAC,OAAO,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE;YAC9G,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;SACzC,CAAC;IACL,CAAC;IAbe,iBAAA,MAAM,SAarB,CAAA;AACJ,CAAC,EAnDgB,gBAAgB,aAAhB,gBAAgB,GAAhB,gBAAgB,QAmDhC"}
|
package/lib/util.d.ts
CHANGED
|
@@ -146,7 +146,7 @@ export declare function unreachable(input: never): never;
|
|
|
146
146
|
export declare function getAt<T = unknown>(obj: unknown, path: string[]): T | undefined;
|
|
147
147
|
/**
|
|
148
148
|
* A stable, non-cryptographic digest of `text`, equal on every host for equal
|
|
149
|
-
* text: the `
|
|
149
|
+
* text: the `TextState.hash` a data server stamps, so a client can
|
|
150
150
|
* hash text of its own to compare. An array is read as its elements joined, so
|
|
151
151
|
* a large text is hashed in parts without concatenating it. It tells whether a
|
|
152
152
|
* text changed, not who could forge one.
|
package/lib/util.js
CHANGED
|
@@ -211,7 +211,7 @@ function getAt(obj, path) {
|
|
|
211
211
|
}
|
|
212
212
|
/**
|
|
213
213
|
* A stable, non-cryptographic digest of `text`, equal on every host for equal
|
|
214
|
-
* text: the `
|
|
214
|
+
* text: the `TextState.hash` a data server stamps, so a client can
|
|
215
215
|
* hash text of its own to compare. An array is read as its elements joined, so
|
|
216
216
|
* a large text is hashed in parts without concatenating it. It tells whether a
|
|
217
217
|
* text changed, not who could forge one.
|
package/package.json
CHANGED
|
@@ -172,7 +172,7 @@ export class DataConnection<
|
|
|
172
172
|
connected: () => this.connected(),
|
|
173
173
|
reportError: (error, reported) => this.reportError(error, reported),
|
|
174
174
|
restoreDirty: event => {
|
|
175
|
-
if (this.dirtyStates.get(event.uri) !== event.dirty) {
|
|
175
|
+
if (this.dirtyStates.get(event.uri) !== (event.text?.dirty ?? false)) {
|
|
176
176
|
this.deliverDirty(event);
|
|
177
177
|
}
|
|
178
178
|
},
|
|
@@ -234,7 +234,7 @@ export class DataConnection<
|
|
|
234
234
|
|
|
235
235
|
/** Record `event` in {@link dirtyStates} and hand it to the client. */
|
|
236
236
|
protected deliverDirty(event: TransferDocumentDirtyChangedEvent): void {
|
|
237
|
-
this.dirtyStates.set(event.uri, event.dirty);
|
|
237
|
+
this.dirtyStates.set(event.uri, event.text?.dirty ?? false);
|
|
238
238
|
const client = this.client as unknown as Partial<Pick<DataClientProtocol<TTransfer>, 'onDocumentDirtyChanged'>>;
|
|
239
239
|
client.onDocumentDirtyChanged?.(event);
|
|
240
240
|
}
|
package/src/client/data-port.ts
CHANGED
|
@@ -27,7 +27,7 @@ import type { RpcConnectionLifecycle } from './rpc-connection';
|
|
|
27
27
|
* `DataServerProtocol` surface, so wrapping it would re-derive the framework's
|
|
28
28
|
* pass-throughs in a second place and lose the `as const satisfies keyof`
|
|
29
29
|
* method allowlists, which cannot drift. Everything a form or a tree actually
|
|
30
|
-
* does — the wire contract, the open/watch/update/close sequence, `
|
|
30
|
+
* does — the wire contract, the open/watch/update/close sequence, `baseVersion`
|
|
31
31
|
* conflict handling, echo filtering by `sourceClientId` — is host-invariant and
|
|
32
32
|
* lives above this interface. What varies between hosts is exactly the
|
|
33
33
|
* members below.
|
|
@@ -11,12 +11,12 @@ import { type Disposable, Emitter, type Event } from 'vscode-jsonrpc';
|
|
|
11
11
|
import { type Clock, SystemClock } from '../clock';
|
|
12
12
|
import type { DataServerProtocol, DiagnosticOf, TransferDocumentDirtyChangedEvent, TransferDocumentUpdatedEvent } from '../data';
|
|
13
13
|
import { isConflictError, SessionClosedError } from '../errors';
|
|
14
|
-
import { type
|
|
14
|
+
import { type BaseVersion, isModelVersion, type ModelVersion, UNRECORDED_VERSION } from '../model-service/base-version';
|
|
15
15
|
import { type ResolvedMessage, defineMessage, describeError, resolve } from '../messages/primitives';
|
|
16
16
|
import type { OpenModelArgs } from '../model-server';
|
|
17
17
|
import type { MaybePromise } from '../util';
|
|
18
18
|
import type { RpcProxy } from '../rpc';
|
|
19
|
-
import type { TransferDocument } from '../transfer-document';
|
|
19
|
+
import type { TextState, TransferDocument } from '../transfer-document';
|
|
20
20
|
import type { TransferElement } from '../transfer-element';
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -29,6 +29,12 @@ export const DATA_SESSION_RESTORE_FAILED = defineMessage(
|
|
|
29
29
|
'Could not restore {uri} after reconnecting to the data server: {detail}'
|
|
30
30
|
);
|
|
31
31
|
|
|
32
|
+
/** A write or open of `{uri}` that the data server answered without a model. */
|
|
33
|
+
export const DATA_SESSION_ANSWER_WITHOUT_MODEL = defineMessage(
|
|
34
|
+
'hydranium/protocol/data-session-answer-without-model',
|
|
35
|
+
'The data server answered a write of {uri} without a model; the session counts that answer as older than every version.'
|
|
36
|
+
);
|
|
37
|
+
|
|
32
38
|
/**
|
|
33
39
|
* A session re-opened documents after its connection dropped and cannot put
|
|
34
40
|
* back what it wrote to them since their last save: another client changed
|
|
@@ -110,14 +116,18 @@ export interface DataSessionUnsavedWrite<
|
|
|
110
116
|
TServer extends DataServerProtocol<TTransfer, DiagnosticOf<TServer>> = DataServerProtocol<TTransfer>
|
|
111
117
|
> {
|
|
112
118
|
/**
|
|
113
|
-
* The
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
119
|
+
* The `text.hash` of the text the first of these writes was based on.
|
|
120
|
+
* `undefined` when the session could not tell, for a write based on
|
|
121
|
+
* `'any'` or on a version it could not read back, and such a write is
|
|
122
|
+
* never sent again: the session cannot tell another client's text from the
|
|
123
|
+
* text it wrote over.
|
|
118
124
|
*/
|
|
119
125
|
readonly baseHash: string | undefined;
|
|
120
|
-
|
|
126
|
+
/**
|
|
127
|
+
* The model version the last of these writes was answered with, which
|
|
128
|
+
* orders its answers, and the `text.hash` of that answer.
|
|
129
|
+
*/
|
|
130
|
+
readonly answer: { readonly version: ModelVersion; readonly hash?: string };
|
|
121
131
|
/**
|
|
122
132
|
* The call that made the last write, and for an `updateDocuments` call this
|
|
123
133
|
* URI's index in it. Records whose `updates` is the same object were
|
|
@@ -251,18 +261,18 @@ export class DataSession<
|
|
|
251
261
|
/** Per URI written since its last save, what {@link restore} needs to write it again. */
|
|
252
262
|
protected readonly unsavedWrites = new Map<string, DataSessionUnsavedWrite<TTransfer, TServer>>();
|
|
253
263
|
/**
|
|
254
|
-
* Per URI this session has open, the
|
|
255
|
-
*
|
|
256
|
-
*
|
|
264
|
+
* Per URI this session has open, the text of the last document one of its
|
|
265
|
+
* own calls was answered with, which spares the read a first unsaved write
|
|
266
|
+
* otherwise makes for its base; see {@link baseHashOf}.
|
|
257
267
|
*/
|
|
258
|
-
protected readonly lastAnswers = new Map<string,
|
|
268
|
+
protected readonly lastAnswers = new Map<string, TextState>();
|
|
259
269
|
/**
|
|
260
|
-
* Per URI this session has open, the version its last save of it was
|
|
270
|
+
* Per URI this session has open, the model version its last save of it was
|
|
261
271
|
* answered with; see {@link recordWrite}. Only a save sets it: an open
|
|
262
272
|
* answers at the version of a write still in flight as well, and that
|
|
263
273
|
* write is not saved.
|
|
264
274
|
*/
|
|
265
|
-
protected readonly savedVersions = new Map<string,
|
|
275
|
+
protected readonly savedVersions = new Map<string, ModelVersion>();
|
|
266
276
|
/** Per URI, this session's calls still in flight on it, which a close waits for. */
|
|
267
277
|
protected readonly inFlight = new Map<string, Set<Promise<unknown>>>();
|
|
268
278
|
/** This session's saves still in flight, which a host's exit waits for; see {@link hasSavesInFlight}. */
|
|
@@ -347,8 +357,8 @@ export class DataSession<
|
|
|
347
357
|
* view misreads as a concurrent third-party write, losing whatever the user
|
|
348
358
|
* had typed.
|
|
349
359
|
*
|
|
350
|
-
* Note that the returned snapshot's
|
|
351
|
-
*
|
|
360
|
+
* Note that the returned snapshot's model usually has no `diagnostics`:
|
|
361
|
+
* `open` settles at the integrity landmark, not at validation.
|
|
352
362
|
* Validity arrives asynchronously on `onDocumentUpdated`, or synchronously
|
|
353
363
|
* from `getModelDocument({ includeDiagnostics: true })`.
|
|
354
364
|
*/
|
|
@@ -389,7 +399,7 @@ export class DataSession<
|
|
|
389
399
|
}
|
|
390
400
|
this.openUris.add(uri);
|
|
391
401
|
this.serverUris.set(uri, document.uri);
|
|
392
|
-
this.
|
|
402
|
+
this.recordText(uri, document);
|
|
393
403
|
// Keyed as the server keys its notifications, which may not be how the
|
|
394
404
|
// caller spelled the URI.
|
|
395
405
|
this.host.forgetDirty?.(document.uri);
|
|
@@ -444,7 +454,7 @@ export class DataSession<
|
|
|
444
454
|
return this.track([args.uri], async () => {
|
|
445
455
|
const server = await this.connected();
|
|
446
456
|
const update = { ...args };
|
|
447
|
-
const baseHash = await this.baseHashOf(server, args.uri, args.
|
|
457
|
+
const baseHash = await this.baseHashOf(server, args.uri, args.baseVersion);
|
|
448
458
|
const document = await server.updateModelDocument({ ...args, clientId: this.clientId });
|
|
449
459
|
this.recordWrite(args.uri, document, baseHash, { update });
|
|
450
460
|
return document;
|
|
@@ -464,7 +474,7 @@ export class DataSession<
|
|
|
464
474
|
// records together.
|
|
465
475
|
const updates = { ...args, updates: args.updates.map(update => ({ ...update })) };
|
|
466
476
|
const server = await this.connected();
|
|
467
|
-
const baseHashes = await Promise.all(args.updates.map(update => this.baseHashOf(server, update.uri, update.
|
|
477
|
+
const baseHashes = await Promise.all(args.updates.map(update => this.baseHashOf(server, update.uri, update.baseVersion)));
|
|
468
478
|
const documents = await server.updateModelDocuments({ ...args, clientId: this.clientId });
|
|
469
479
|
args.updates.forEach((update, index) => this.recordWrite(update.uri, documents[index], baseHashes[index], { updates, index }));
|
|
470
480
|
return documents;
|
|
@@ -483,11 +493,12 @@ export class DataSession<
|
|
|
483
493
|
if (this.unsavedWrites.get(args.uri) === unsaved) {
|
|
484
494
|
this.unsavedWrites.delete(args.uri);
|
|
485
495
|
}
|
|
486
|
-
this.
|
|
496
|
+
const answer = this.answerOf(args.uri, document);
|
|
497
|
+
this.recordText(args.uri, document);
|
|
487
498
|
// The higher of two saves' answers, which may arrive out of order.
|
|
488
499
|
const saved = this.savedVersions.get(args.uri);
|
|
489
|
-
if (saved === undefined ||
|
|
490
|
-
this.savedVersions.set(args.uri,
|
|
500
|
+
if (saved === undefined || answer.version > saved) {
|
|
501
|
+
this.savedVersions.set(args.uri, answer.version);
|
|
491
502
|
}
|
|
492
503
|
return document;
|
|
493
504
|
});
|
|
@@ -620,7 +631,7 @@ export class DataSession<
|
|
|
620
631
|
* can still be reported lost that way.
|
|
621
632
|
*/
|
|
622
633
|
protected forgetSavedWrite(event: TransferDocumentDirtyChangedEvent): void {
|
|
623
|
-
if (event.dirty) {
|
|
634
|
+
if (event.text?.dirty) {
|
|
624
635
|
return;
|
|
625
636
|
}
|
|
626
637
|
// The flip names the server's key, which is not always the caller's
|
|
@@ -675,7 +686,7 @@ export class DataSession<
|
|
|
675
686
|
* what it wrote since their last save where the re-open lost it, and tell
|
|
676
687
|
* the host which of them lost it for good.
|
|
677
688
|
*
|
|
678
|
-
* Decided per document by the re-opened
|
|
689
|
+
* Decided per document by the re-opened document's `text.hash`,
|
|
679
690
|
* in {@link restoreOutcome}. Versions cannot decide a write: a revert moves
|
|
680
691
|
* the version on and a restarted server numbers afresh, so the version the
|
|
681
692
|
* last write was answered with never matches where a write is needed, and
|
|
@@ -717,7 +728,7 @@ export class DataSession<
|
|
|
717
728
|
await server.watchModelDocument({ uri, clientId: this.clientId });
|
|
718
729
|
if (this.openUris.has(uri)) {
|
|
719
730
|
this.serverUris.set(uri, document.uri);
|
|
720
|
-
this.
|
|
731
|
+
this.recordText(uri, document);
|
|
721
732
|
}
|
|
722
733
|
reopened.set(uri, document);
|
|
723
734
|
} catch (error: unknown) {
|
|
@@ -742,12 +753,12 @@ export class DataSession<
|
|
|
742
753
|
if (!this.openUris.has(uri)) {
|
|
743
754
|
continue;
|
|
744
755
|
}
|
|
745
|
-
// A failed read leaves the client's dirty
|
|
746
|
-
// document is restored all the same.
|
|
756
|
+
// A failed read, or one without text, leaves the client's dirty
|
|
757
|
+
// state as it was; the document is restored all the same.
|
|
747
758
|
const current = await server.getModelDocument({ uri }).catch(() => undefined);
|
|
748
|
-
if (current?.
|
|
759
|
+
if (current?.text !== undefined) {
|
|
749
760
|
try {
|
|
750
|
-
this.host.restoreDirty?.({ uri: current.uri,
|
|
761
|
+
this.host.restoreDirty?.({ uri: current.uri, text: current.text });
|
|
751
762
|
} catch {
|
|
752
763
|
// The client's listener failed, not the restore: the server
|
|
753
764
|
// has the document open and watched, so it stays restored.
|
|
@@ -765,20 +776,21 @@ export class DataSession<
|
|
|
765
776
|
* `write` (`'kept'`), holds the text the write started from, so the write
|
|
766
777
|
* can be sent again (`'resend'`), or holds something else (`'lost'`).
|
|
767
778
|
*
|
|
768
|
-
* A document without
|
|
769
|
-
* counts as kept only at the version the write was answered with.
|
|
779
|
+
* A document without `text` comes from a server that sends none, and
|
|
780
|
+
* counts as kept only at the version the write was answered with. One
|
|
781
|
+
* without a model has no version to send the write again on.
|
|
770
782
|
*/
|
|
771
783
|
protected restoreOutcome(
|
|
772
784
|
write: DataSessionUnsavedWrite<TTransfer, TServer>,
|
|
773
785
|
document: DataSessionDocument<TTransfer, TServer>
|
|
774
786
|
): 'kept' | 'resend' | 'lost' {
|
|
775
|
-
if (document.
|
|
776
|
-
return document.version === write.answer.version ? 'kept' : 'lost';
|
|
787
|
+
if (document.text === undefined) {
|
|
788
|
+
return document.model?.version === write.answer.version ? 'kept' : 'lost';
|
|
777
789
|
}
|
|
778
|
-
if (document.
|
|
790
|
+
if (document.text.hash === write.answer.hash) {
|
|
779
791
|
return 'kept';
|
|
780
792
|
}
|
|
781
|
-
return write.baseHash !== undefined && document.
|
|
793
|
+
return document.model && write.baseHash !== undefined && document.text.hash === write.baseHash ? 'resend' : 'lost';
|
|
782
794
|
}
|
|
783
795
|
|
|
784
796
|
/**
|
|
@@ -808,7 +820,7 @@ export class DataSession<
|
|
|
808
820
|
// on from the re-opened version, not from the one the record holds.
|
|
809
821
|
for (const { uri, write, document, outcome } of outcomes) {
|
|
810
822
|
if (outcome === 'kept' && document) {
|
|
811
|
-
this.unsavedWrites.set(uri, { ...write, answer:
|
|
823
|
+
this.unsavedWrites.set(uri, { ...write, answer: this.answerOf(uri, document) });
|
|
812
824
|
}
|
|
813
825
|
}
|
|
814
826
|
// A document that could not be re-opened is forgotten, and was reported
|
|
@@ -823,7 +835,7 @@ export class DataSession<
|
|
|
823
835
|
// In the order of the call's updates, which is the order of its answers.
|
|
824
836
|
const resend = outcomes
|
|
825
837
|
.flatMap(({ uri, write, document, outcome }) =>
|
|
826
|
-
outcome === 'resend' && document ? [{ uri, write,
|
|
838
|
+
outcome === 'resend' && document?.model ? [{ uri, write, baseVersion: document.model.version }] : []
|
|
827
839
|
)
|
|
828
840
|
.sort((left, right) => indexOf(left.write) - indexOf(right.write));
|
|
829
841
|
if (resend.length === 0) {
|
|
@@ -833,15 +845,15 @@ export class DataSession<
|
|
|
833
845
|
try {
|
|
834
846
|
const { call } = resend[0].write;
|
|
835
847
|
if ('update' in call) {
|
|
836
|
-
documents = [await server.updateModelDocument({ ...call.update,
|
|
848
|
+
documents = [await server.updateModelDocument({ ...call.update, baseVersion: resend[0].baseVersion, clientId: this.clientId })];
|
|
837
849
|
} else {
|
|
838
|
-
const
|
|
850
|
+
const baseVersion = new Map(resend.map(member => [indexOf(member.write), member.baseVersion]));
|
|
839
851
|
documents = await server.updateModelDocuments({
|
|
840
852
|
...call.updates,
|
|
841
853
|
clientId: this.clientId,
|
|
842
854
|
updates: call.updates.updates.flatMap((update, index) => {
|
|
843
|
-
const version =
|
|
844
|
-
return version === undefined ? [] : [{ ...update,
|
|
855
|
+
const version = baseVersion.get(index);
|
|
856
|
+
return version === undefined ? [] : [{ ...update, baseVersion: version }];
|
|
845
857
|
})
|
|
846
858
|
});
|
|
847
859
|
}
|
|
@@ -849,13 +861,13 @@ export class DataSession<
|
|
|
849
861
|
if (isConflictError(error)) {
|
|
850
862
|
return lose();
|
|
851
863
|
}
|
|
852
|
-
for (const { uri, write,
|
|
864
|
+
for (const { uri, write, baseVersion } of resend) {
|
|
853
865
|
this.host.reportError?.(error, resolve(DATA_SESSION_RESTORE_FAILED, { uri, detail: describeError(error) }));
|
|
854
866
|
// Kept for the next restore, and numbered from the re-opened
|
|
855
867
|
// version, as the kept records are: a record numbered by a server
|
|
856
868
|
// that has since restarted would have later answers ignored.
|
|
857
869
|
if (current(uri, write)) {
|
|
858
|
-
this.unsavedWrites.set(uri, { ...write, answer: { ...write.answer, version:
|
|
870
|
+
this.unsavedWrites.set(uri, { ...write, answer: { ...write.answer, version: baseVersion } });
|
|
859
871
|
}
|
|
860
872
|
}
|
|
861
873
|
return [];
|
|
@@ -872,7 +884,7 @@ export class DataSession<
|
|
|
872
884
|
}
|
|
873
885
|
|
|
874
886
|
/**
|
|
875
|
-
* The text hash of the version `
|
|
887
|
+
* The text hash of the version `baseVersion` names for `uri`, for a write that
|
|
876
888
|
* starts the URI's record in {@link unsavedWrites}; `undefined` for any
|
|
877
889
|
* other write, whose record keeps the base it has.
|
|
878
890
|
*
|
|
@@ -881,16 +893,16 @@ export class DataSession<
|
|
|
881
893
|
* changes only with its version, so the text at a version is the text the
|
|
882
894
|
* write, once it passes the gate, was applied to.
|
|
883
895
|
*/
|
|
884
|
-
protected async baseHashOf(server: RpcProxy<TServer>, uri: string,
|
|
885
|
-
if (this.unsavedWrites.has(uri) || !
|
|
896
|
+
protected async baseHashOf(server: RpcProxy<TServer>, uri: string, baseVersion: BaseVersion): Promise<string | undefined> {
|
|
897
|
+
if (this.unsavedWrites.has(uri) || !isModelVersion(baseVersion)) {
|
|
886
898
|
return undefined;
|
|
887
899
|
}
|
|
888
900
|
const known = this.lastAnswers.get(uri);
|
|
889
|
-
if (known?.version ===
|
|
890
|
-
return known.
|
|
901
|
+
if (known?.version === baseVersion) {
|
|
902
|
+
return known.hash;
|
|
891
903
|
}
|
|
892
904
|
const read = await server.getModelDocument({ uri }).catch(() => undefined);
|
|
893
|
-
return read?.version ===
|
|
905
|
+
return read?.text?.version === baseVersion ? read.text.hash : undefined;
|
|
894
906
|
}
|
|
895
907
|
|
|
896
908
|
/**
|
|
@@ -910,7 +922,7 @@ export class DataSession<
|
|
|
910
922
|
baseHash: string | undefined,
|
|
911
923
|
call: DataSessionUnsavedWrite<TTransfer, TServer>['call']
|
|
912
924
|
): void {
|
|
913
|
-
const answer =
|
|
925
|
+
const answer = this.answerOf(uri, document);
|
|
914
926
|
const kept = this.unsavedWrites.get(uri);
|
|
915
927
|
if (kept && answer.version < kept.answer.version) {
|
|
916
928
|
return;
|
|
@@ -920,7 +932,33 @@ export class DataSession<
|
|
|
920
932
|
return;
|
|
921
933
|
}
|
|
922
934
|
this.unsavedWrites.set(uri, { baseHash: kept ? kept.baseHash : baseHash, answer, call });
|
|
923
|
-
this.
|
|
935
|
+
this.recordText(uri, document);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* What {@link DataSessionUnsavedWrite.answer} keeps of `document`. Every write
|
|
940
|
+
* and open answers with a model; one without is reported and counted older
|
|
941
|
+
* than every version, so it never replaces a real answer.
|
|
942
|
+
*/
|
|
943
|
+
protected answerOf(
|
|
944
|
+
uri: string,
|
|
945
|
+
document: DataSessionDocument<TTransfer, TServer>
|
|
946
|
+
): DataSessionUnsavedWrite<TTransfer, TServer>['answer'] {
|
|
947
|
+
if (!document.model) {
|
|
948
|
+
const reported = resolve(DATA_SESSION_ANSWER_WITHOUT_MODEL, { uri });
|
|
949
|
+
this.host.reportError?.(new Error(reported.text), reported);
|
|
950
|
+
return { version: UNRECORDED_VERSION, hash: document.text?.hash };
|
|
951
|
+
}
|
|
952
|
+
return { version: document.model.version, hash: document.text?.hash };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** Keep `document`'s text in {@link lastAnswers}, or forget it for a server that sends none. */
|
|
956
|
+
protected recordText(uri: string, document: DataSessionDocument<TTransfer, TServer>): void {
|
|
957
|
+
if (document.text) {
|
|
958
|
+
this.lastAnswers.set(uri, document.text);
|
|
959
|
+
} else {
|
|
960
|
+
this.lastAnswers.delete(uri);
|
|
961
|
+
}
|
|
924
962
|
}
|
|
925
963
|
|
|
926
964
|
/** Run `call`, counted as in flight on each of `uris` until it settles. */
|
|
@@ -100,10 +100,8 @@ export interface DocumentServerProtocol<TTransfer extends TransferElement, TDiag
|
|
|
100
100
|
* as every document request here does. The document is read from disk
|
|
101
101
|
* unless a client has it open already, so the request carries no
|
|
102
102
|
* `languageId`, `version` or `text` seed. Returns the document at
|
|
103
|
-
* the server's configured target phase
|
|
104
|
-
*
|
|
105
|
-
* text-document store so the caller's first `basedOn` write cannot
|
|
106
|
-
* self-conflict. A repeat open changes nothing.
|
|
103
|
+
* the server's configured target phase, as {@link getModelDocument} does.
|
|
104
|
+
* A repeat open changes nothing.
|
|
107
105
|
*
|
|
108
106
|
* Pair with {@link watchModelDocument} to receive subsequent build-phase
|
|
109
107
|
* events (open returns a one-shot snapshot; later validation diagnostics
|
package/src/data/events.ts
CHANGED
|
@@ -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 } from '../transfer-document';
|
|
13
|
+
import type { TextState, TransferDocument } from '../transfer-document';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Why an update event fired. Subscribers filter on reason for behaviour
|
|
@@ -106,14 +106,20 @@ export type TransferDocumentSavedListener<
|
|
|
106
106
|
/**
|
|
107
107
|
* Delivered on the data-server when a watched document's text starts or stops
|
|
108
108
|
* differing from its file, as the server last knew the file. The current
|
|
109
|
-
* answer rides on every transfer document the server sends as `dirty`;
|
|
110
|
-
* carries only its changes, so a client that follows it needs no rebuild
|
|
111
|
-
* learn of a save.
|
|
109
|
+
* answer rides on every transfer document the server sends as `text.dirty`;
|
|
110
|
+
* this carries only its changes, so a client that follows it needs no rebuild
|
|
111
|
+
* to learn of a save.
|
|
112
112
|
*/
|
|
113
113
|
export interface TransferDocumentDirtyChangedEvent {
|
|
114
114
|
/** Canonical URI, keyed as the subscription is. */
|
|
115
115
|
readonly uri: string;
|
|
116
|
-
|
|
116
|
+
/**
|
|
117
|
+
* The text the answer was decided on. It is sent before that text is
|
|
118
|
+
* built, so a `text.version` ahead of the model a client holds means an
|
|
119
|
+
* update at this version or a later one follows. Absent when the document
|
|
120
|
+
* no longer exists, or when the build that follows its release failed.
|
|
121
|
+
*/
|
|
122
|
+
readonly text?: TextState;
|
|
117
123
|
}
|
|
118
124
|
|
|
119
125
|
/** Callback shape for `DataClientProtocol.onDocumentDirtyChanged`. */
|
package/src/data/requests.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import type {
|
|
10
|
+
import type { BaseVersion } from '../model-service/base-version';
|
|
11
11
|
|
|
12
12
|
/** Get the current state of a single document. The server returns the latest built version. */
|
|
13
13
|
export interface GetModelDocumentArgs {
|
|
@@ -53,16 +53,16 @@ export interface TransferUpdateDocumentArgs<TTransfer> {
|
|
|
53
53
|
/** The whole structured model root, or its serialised textual form. */
|
|
54
54
|
model: TTransfer | string;
|
|
55
55
|
/**
|
|
56
|
-
* What this write was authored against. A `
|
|
56
|
+
* What this write was authored against. A `ModelVersion` is compared
|
|
57
57
|
* against the server's current text-document version for `uri` and throws
|
|
58
|
-
* `ConflictError` on mismatch; `'
|
|
58
|
+
* `ConflictError` on mismatch; `'any'` writes unconditionally.
|
|
59
59
|
*
|
|
60
60
|
* **Required so that an ungated write is a decision rather than an
|
|
61
61
|
* omission.** An optional gate is indistinguishable from a forgotten one at
|
|
62
62
|
* the call site, and a file of twenty writes hides the one that lost the
|
|
63
63
|
* field. Nothing else here can see that, since the defect is an absence.
|
|
64
64
|
*/
|
|
65
|
-
|
|
65
|
+
baseVersion: BaseVersion;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/** Update a document the session `clientId` has open, then persist it to disk. */
|