@hydranium/core 1.0.0-next.115 → 1.0.0-next.117
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/README.md +3 -1
- package/lib/documents/ast-document-manager.d.ts +52 -2
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +67 -5
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/client-session-errors.d.ts +44 -0
- package/lib/documents/client-session-errors.d.ts.map +1 -0
- package/lib/documents/client-session-errors.js +57 -0
- package/lib/documents/client-session-errors.js.map +1 -0
- package/lib/documents/client-session-registry.d.ts +118 -0
- package/lib/documents/client-session-registry.d.ts.map +1 -0
- package/lib/documents/client-session-registry.js +171 -0
- package/lib/documents/client-session-registry.js.map +1 -0
- package/lib/documents/hydranium-text-documents.d.ts +69 -17
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +135 -44
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/index.d.ts +2 -0
- package/lib/documents/index.d.ts.map +1 -1
- package/lib/documents/index.js +2 -0
- package/lib/documents/index.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +8 -7
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +35 -27
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts +120 -0
- package/lib/langium/model-service/client-session.d.ts.map +1 -0
- package/lib/langium/model-service/client-session.js +175 -0
- package/lib/langium/model-service/client-session.js.map +1 -0
- package/lib/langium/model-service/index.d.ts +1 -0
- package/lib/langium/model-service/index.d.ts.map +1 -1
- package/lib/langium/model-service/index.js +1 -0
- package/lib/langium/model-service/index.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +57 -1
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +123 -9
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts +6 -0
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +20 -3
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +4 -3
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +3 -0
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.d.ts +13 -5
- package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.js +45 -14
- package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +107 -6
- package/src/documents/client-session-errors.ts +54 -0
- package/src/documents/client-session-registry.ts +211 -0
- package/src/documents/hydranium-text-documents.ts +150 -47
- package/src/documents/index.ts +2 -0
- package/src/langium/integrity/integrity-service.ts +36 -26
- package/src/langium/model-service/client-session.ts +257 -0
- package/src/langium/model-service/index.ts +1 -0
- package/src/langium/model-service/model-service.ts +151 -9
- package/src/lsp/hydranium-document-update-handler.ts +22 -4
- package/src/testing/stub-ast-document-manager.ts +7 -2
- package/src/testing/stub-hydranium-text-documents.ts +62 -18
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
import { Disposable } from 'vscode-languageserver';
|
|
10
10
|
import { LANGUAGE_CLIENT_ID } from '../documents/client-ids.js';
|
|
11
|
+
import { ClientSessionRegistry } from '../documents/client-session-registry.js';
|
|
11
12
|
/** Build a {@link StubHydraniumTextDocuments}. */
|
|
12
13
|
export function makeStubHydraniumTextDocuments() {
|
|
13
14
|
const docs = new Map();
|
|
@@ -19,7 +20,12 @@ export function makeStubHydraniumTextDocuments() {
|
|
|
19
20
|
let applyEditHandler = () => ({ applied: true });
|
|
20
21
|
const saveListeners = [];
|
|
21
22
|
const closeListeners = [];
|
|
22
|
-
|
|
23
|
+
// The real registry, so the session table, the opens, the closing state and
|
|
24
|
+
// the open options behave as the store's do. The stub's plain-string keys
|
|
25
|
+
// stand in for canonical URIs (one stub-boundary cast, like `fireClose`).
|
|
26
|
+
const sessions = new ClientSessionRegistry();
|
|
27
|
+
const key = (uri) => uri;
|
|
28
|
+
const stub = {
|
|
23
29
|
get changes() {
|
|
24
30
|
return changes;
|
|
25
31
|
},
|
|
@@ -100,21 +106,38 @@ export function makeStubHydraniumTextDocuments() {
|
|
|
100
106
|
isOpenInAnyClient(uri) {
|
|
101
107
|
return docs.has(uri) || languageClientOpen.has(uri);
|
|
102
108
|
},
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
+
isOpenInClient(uri, clientId) {
|
|
110
|
+
return sessions.isOpenIn(key(uri), clientId);
|
|
111
|
+
},
|
|
112
|
+
registerSession(clientId) {
|
|
113
|
+
sessions.register(clientId);
|
|
114
|
+
},
|
|
115
|
+
closeSession(clientId) {
|
|
116
|
+
if (!sessions.isRegistered(clientId)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
for (const uri of sessions.beginClose(clientId)) {
|
|
121
|
+
stub.fireClose(uri, clientId);
|
|
122
|
+
}
|
|
109
123
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
clients.add(LANGUAGE_CLIENT_ID);
|
|
113
|
-
clientsByUri.set(uri, clients);
|
|
124
|
+
finally {
|
|
125
|
+
sessions.unregister(clientId);
|
|
114
126
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
127
|
+
},
|
|
128
|
+
get onDidCloseSession() {
|
|
129
|
+
return sessions.onDidCloseSession;
|
|
130
|
+
},
|
|
131
|
+
openOptions(uri, clientId) {
|
|
132
|
+
return sessions.openOptions(key(uri), clientId);
|
|
133
|
+
},
|
|
134
|
+
setOpenOptions(uri, clientId, options) {
|
|
135
|
+
sessions.setOpenOptions(key(uri), clientId, options);
|
|
136
|
+
},
|
|
137
|
+
openDocuments() {
|
|
138
|
+
// From the registry that also answers `isOpenInClient`, which both
|
|
139
|
+
// seeding channels feed, so the two never disagree about an open.
|
|
140
|
+
return sessions.openDocuments();
|
|
118
141
|
},
|
|
119
142
|
setApplyEditHandler(handler) {
|
|
120
143
|
applyEditHandler = handler;
|
|
@@ -127,9 +150,11 @@ export function makeStubHydraniumTextDocuments() {
|
|
|
127
150
|
staged.push({ uri, text });
|
|
128
151
|
},
|
|
129
152
|
seedOpen(uri, text, clientId) {
|
|
153
|
+
sessions.addOpen(key(uri), clientId);
|
|
130
154
|
docs.set(uri, { uri, version: 1, text, clientId });
|
|
131
155
|
},
|
|
132
156
|
seedOpenInLanguageClient(uri) {
|
|
157
|
+
sessions.addOpen(key(uri), LANGUAGE_CLIENT_ID);
|
|
133
158
|
languageClientOpen.add(uri);
|
|
134
159
|
},
|
|
135
160
|
fireClose(uri, clientId) {
|
|
@@ -140,6 +165,7 @@ export function makeStubHydraniumTextDocuments() {
|
|
|
140
165
|
if (holder && holder.clientId === clientId) {
|
|
141
166
|
docs.delete(uri);
|
|
142
167
|
}
|
|
168
|
+
sessions.removeOpen(key(uri), clientId);
|
|
143
169
|
if (clientId === LANGUAGE_CLIENT_ID) {
|
|
144
170
|
languageClientOpen.delete(uri);
|
|
145
171
|
}
|
|
@@ -158,7 +184,12 @@ export function makeStubHydraniumTextDocuments() {
|
|
|
158
184
|
saveListeners.length = 0;
|
|
159
185
|
closeListeners.length = 0;
|
|
160
186
|
applyEditHandler = () => ({ applied: true });
|
|
187
|
+
// Cleared in place rather than replaced: a model service built on the
|
|
188
|
+
// stub subscribed to `onDidCloseSession` once, and a new registry would
|
|
189
|
+
// leave that subscription on one nothing fires.
|
|
190
|
+
sessions.clear();
|
|
161
191
|
}
|
|
162
192
|
};
|
|
193
|
+
return stub;
|
|
163
194
|
}
|
|
164
195
|
//# sourceMappingURL=stub-hydranium-text-documents.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stub-hydranium-text-documents.js","sourceRoot":"","sources":["../../src/testing/stub-hydranium-text-documents.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAIlF,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"stub-hydranium-text-documents.js","sourceRoot":"","sources":["../../src/testing/stub-hydranium-text-documents.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hydranium/core",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.117",
|
|
4
4
|
"description": "Foundational runtime of the hydranium framework: AST coordination layer (services, integrity, AST extension, serialization, multi-client documents) plus the LSP textual head at the /lsp subpath. Consumed by protocol-head packages (@hydranium/data-server, @hydranium/glsp-server).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hydranium",
|
|
@@ -104,8 +104,8 @@
|
|
|
104
104
|
"diff": "^5.2.0"
|
|
105
105
|
},
|
|
106
106
|
"devDependencies": {
|
|
107
|
-
"@hydranium/langium": "1.0.0-next.
|
|
108
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
107
|
+
"@hydranium/langium": "1.0.0-next.117",
|
|
108
|
+
"@hydranium/protocol": "1.0.0-next.117",
|
|
109
109
|
"@playwright/test": "^1.40.0",
|
|
110
110
|
"@types/diff": "^5.2.0",
|
|
111
111
|
"rimraf": "^5.0.0",
|
|
@@ -117,8 +117,8 @@
|
|
|
117
117
|
"vscode-languageserver-types": "^3.17.5"
|
|
118
118
|
},
|
|
119
119
|
"peerDependencies": {
|
|
120
|
-
"@hydranium/langium": "1.0.0-next.
|
|
121
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
120
|
+
"@hydranium/langium": "1.0.0-next.117",
|
|
121
|
+
"@hydranium/protocol": "1.0.0-next.117",
|
|
122
122
|
"@playwright/test": "^1.40.0",
|
|
123
123
|
"vscode-jsonrpc": "9.0.1",
|
|
124
124
|
"vscode-languageserver": "~10.0.1",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
type CanonicalUri,
|
|
11
12
|
type CloseModelArgs,
|
|
12
13
|
type Tracer,
|
|
13
14
|
type TransferSavedEvent,
|
|
@@ -178,7 +179,18 @@ export type AstDocumentSavedEvent<TAst extends AstNode, TDiagnostic extends AstD
|
|
|
178
179
|
>;
|
|
179
180
|
|
|
180
181
|
/** Construction options for {@link AstDocumentManager}. */
|
|
181
|
-
export
|
|
182
|
+
export interface AstDocumentManagerOptions extends LogNameOptions {
|
|
183
|
+
/**
|
|
184
|
+
* Skip a queued save of a URI when a newer save of it is queued behind. The
|
|
185
|
+
* newer save took its text later, so disk ends the same with fewer writes.
|
|
186
|
+
* A skipped save announces nothing and takes the newer save's outcome: it
|
|
187
|
+
* resolves when that one lands and rejects with its error when it fails,
|
|
188
|
+
* since disk then holds neither text. Defaults to `false`: every save
|
|
189
|
+
* writes. The framework binds the manager without options, so turning this
|
|
190
|
+
* on means rebinding `AstDocumentManager` with them.
|
|
191
|
+
*/
|
|
192
|
+
readonly coalesceSaves?: boolean;
|
|
193
|
+
}
|
|
182
194
|
|
|
183
195
|
/**
|
|
184
196
|
* The document slot the model server and the integrity service talk to:
|
|
@@ -201,8 +213,29 @@ export interface AstDocumentManager<TAst extends AstNode, TDiagnostic extends As
|
|
|
201
213
|
|
|
202
214
|
/** Apply `text` as `clientId`'s edit. Resolves to the resulting text-document version. */
|
|
203
215
|
update(uri: string, text: string, clientId: string): Promise<number>;
|
|
216
|
+
/**
|
|
217
|
+
* Write the document's current text to disk through the URI's disk queue.
|
|
218
|
+
* The text is taken when this is called, so saves of one URI land in the
|
|
219
|
+
* order they were called.
|
|
220
|
+
*/
|
|
204
221
|
save(uri: string, clientId: string): Promise<void>;
|
|
205
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Run `task` once every disk task already queued for `uri` has settled, and
|
|
225
|
+
* before any queued after it. Every server-side disk access of a file that
|
|
226
|
+
* must not interleave with its saves goes through here: two writes of one
|
|
227
|
+
* file that bypass it can land in either order, and the older text then
|
|
228
|
+
* stays on disk. Tasks of different URIs run in parallel. A task that has
|
|
229
|
+
* nothing to do still resolves once the tasks before it have settled.
|
|
230
|
+
*
|
|
231
|
+
* A task must not await a build, nor a save, another disk task or an open
|
|
232
|
+
* of a document no client has open, all of the same URI: a build's
|
|
233
|
+
* integrity repair queues its own write here and the build waits for it,
|
|
234
|
+
* and such an open reads the file here, so a task waiting on either queued
|
|
235
|
+
* behind it never resolves, which wedges the URI's queue for good.
|
|
236
|
+
*/
|
|
237
|
+
queueDiskTask<T>(uri: string, task: () => Promise<T>): Promise<T>;
|
|
238
|
+
|
|
206
239
|
onUpdate(uri: string, listener: (event: AstDocumentUpdatedEvent<TAst, TDiagnostic>) => void): Disposable;
|
|
207
240
|
onSave(uri: string, listener: (event: AstDocumentSavedEvent<TAst, TDiagnostic>) => void | Promise<void>): Disposable;
|
|
208
241
|
onClientClosed(uri: string, clientId: string, listener: () => void): Disposable;
|
|
@@ -251,6 +284,17 @@ export class DefaultAstDocumentManager<
|
|
|
251
284
|
> implements AstDocumentManager<TAst, TDiagnostic> {
|
|
252
285
|
protected lastUpdate?: UpdateInfo;
|
|
253
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Per canonical URI, the settling of the last disk task queued for it; the
|
|
289
|
+
* next task chains behind it. An entry leaves once its URI's queue is idle.
|
|
290
|
+
* Kept outside the workspace lock, whose `write` cancels the write before
|
|
291
|
+
* it: a save cancelled that way reports success for text it never wrote.
|
|
292
|
+
*/
|
|
293
|
+
protected readonly diskQueues = new Map<CanonicalUri, Promise<void>>();
|
|
294
|
+
/** Per canonical URI, the newest save queued for it, which {@link coalesceSaves} lets older queued saves defer to. */
|
|
295
|
+
protected readonly newestSaves = new Map<CanonicalUri, Promise<void>>();
|
|
296
|
+
protected readonly coalesceSaves: boolean;
|
|
297
|
+
|
|
254
298
|
protected readonly textDocuments: HydraniumTextDocuments<TextDocument>;
|
|
255
299
|
protected readonly fileSystemProvider: WritableFileSystemProvider;
|
|
256
300
|
protected readonly langiumDocs: LangiumDocuments;
|
|
@@ -268,12 +312,19 @@ export class DefaultAstDocumentManager<
|
|
|
268
312
|
this.documentBuilder = services.workspace.DocumentBuilder;
|
|
269
313
|
this.uriPolicy = services.workspace.DocumentUriPolicy;
|
|
270
314
|
this.tracer = services.Tracer.for(options.logName ?? 'AstDocumentManager').trace('instantiated');
|
|
315
|
+
this.coalesceSaves = options.coalesceSaves ?? false;
|
|
271
316
|
this.textDocuments.onDidOpen(event =>
|
|
272
317
|
this.open({ clientId: event.clientId, uri: event.document.uri, languageId: event.document.languageId })
|
|
273
318
|
);
|
|
274
319
|
this.textDocuments.onDidClose(event => this.close({ clientId: event.clientId, uri: event.document.uri }));
|
|
275
320
|
this.documentBuilder.onUpdate((changed, deleted) => {
|
|
276
321
|
this.lastUpdate = { changed, deleted };
|
|
322
|
+
// The only place a deletion is observable: `DocumentBuilder.update`
|
|
323
|
+
// removes a deleted document before it builds anything, so no phase
|
|
324
|
+
// listener ever sees it.
|
|
325
|
+
for (const uri of deleted) {
|
|
326
|
+
this.textDocuments.notifyDocumentDeleted(uri.toString());
|
|
327
|
+
}
|
|
277
328
|
});
|
|
278
329
|
// Content transitions for CLOSED documents bypass the text store: the
|
|
279
330
|
// last-close revert and watched-file changes rebuild from disk into a
|
|
@@ -463,6 +514,12 @@ export class DefaultAstDocumentManager<
|
|
|
463
514
|
* callers don't need to pass it — the manager owns content and version
|
|
464
515
|
* sequencing.
|
|
465
516
|
*
|
|
517
|
+
* The text is taken synchronously, before the save waits in the URI's disk
|
|
518
|
+
* queue; the compare and the write run in the queue. Taken later, a save
|
|
519
|
+
* that waits behind another writes whatever the store holds when its turn
|
|
520
|
+
* comes, edits made after the call included, and announces that text under
|
|
521
|
+
* its own client id.
|
|
522
|
+
*
|
|
466
523
|
* **The write is skipped when the file already holds that text; the
|
|
467
524
|
* notification is not.** A save announces that the content is on disk, which
|
|
468
525
|
* is true either way — so a subscriber clearing a dirty marker or reacting to
|
|
@@ -481,15 +538,52 @@ export class DefaultAstDocumentManager<
|
|
|
481
538
|
throw new Error(`Document ${uri} hasn't been opened for saving yet`);
|
|
482
539
|
}
|
|
483
540
|
const text = document.getText();
|
|
484
|
-
|
|
485
|
-
|
|
541
|
+
let supersededBy: Promise<void> | undefined;
|
|
542
|
+
const saved: Promise<void> = this.queueDiskTask(canonical, async () => {
|
|
543
|
+
const newest = this.newestSaves.get(canonical);
|
|
544
|
+
if (this.coalesceSaves && newest !== saved) {
|
|
545
|
+
supersededBy = newest;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
await this.writeSave(canonical, text, clientId);
|
|
549
|
+
})
|
|
550
|
+
.then(() => supersededBy)
|
|
551
|
+
.finally(() => {
|
|
552
|
+
if (this.newestSaves.get(canonical) === saved) {
|
|
553
|
+
this.newestSaves.delete(canonical);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
this.newestSaves.set(canonical, saved);
|
|
557
|
+
return saved;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Write `text` to `uri` unless the file already holds it, then announce the save. Runs inside the URI's disk queue. */
|
|
561
|
+
protected async writeSave(uri: CanonicalUri, text: string, clientId: string): Promise<void> {
|
|
562
|
+
if (!(await this.matchesDisk(uri, text))) {
|
|
563
|
+
await this.tracer.with(uri).time(
|
|
486
564
|
`Write file (${text.length} bytes, from ${clientId})`,
|
|
487
565
|
// Async write so a slow disk does not block the event loop for the duration of the fs call.
|
|
488
|
-
() => this.fileSystemProvider.writeFile(UriUtils.toUri(
|
|
566
|
+
() => this.fileSystemProvider.writeFile(UriUtils.toUri(uri), text),
|
|
489
567
|
'debug'
|
|
490
568
|
);
|
|
491
569
|
}
|
|
492
|
-
this.textDocuments.notifyDidSaveTextDocument({ textDocument: TextDocumentIdentifier.create(
|
|
570
|
+
this.textDocuments.notifyDidSaveTextDocument({ textDocument: TextDocumentIdentifier.create(uri), text }, clientId);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
queueDiskTask<T>(uri: string, task: () => Promise<T>): Promise<T> {
|
|
574
|
+
const key = this.uriPolicy.canonicalUri(uri);
|
|
575
|
+
const result = (this.diskQueues.get(key) ?? Promise.resolve()).then(task);
|
|
576
|
+
const settled = result.then(
|
|
577
|
+
() => undefined,
|
|
578
|
+
() => undefined
|
|
579
|
+
);
|
|
580
|
+
this.diskQueues.set(key, settled);
|
|
581
|
+
void settled.then(() => {
|
|
582
|
+
if (this.diskQueues.get(key) === settled) {
|
|
583
|
+
this.diskQueues.delete(key);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
return result;
|
|
493
587
|
}
|
|
494
588
|
|
|
495
589
|
/**
|
|
@@ -534,7 +628,14 @@ export class DefaultAstDocumentManager<
|
|
|
534
628
|
version = 0,
|
|
535
629
|
text?: string
|
|
536
630
|
): Promise<TextDocumentItem> {
|
|
537
|
-
|
|
631
|
+
// Read in the disk queue: a read beside a queued save of the file returns
|
|
632
|
+
// the text from before it, and the document then opens on that.
|
|
633
|
+
return {
|
|
634
|
+
uri,
|
|
635
|
+
languageId,
|
|
636
|
+
version,
|
|
637
|
+
text: text ?? (await this.queueDiskTask(uri, () => this.fileSystemProvider.readFile(UriUtils.toUri(uri))))
|
|
638
|
+
};
|
|
538
639
|
}
|
|
539
640
|
|
|
540
641
|
/**
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Thrown by every call on a client session after it ended, and by an open that
|
|
12
|
+
* was still in flight when its session ended.
|
|
13
|
+
*
|
|
14
|
+
* The message names the client id, so it is addressed to whoever composes the
|
|
15
|
+
* system rather than to an end user.
|
|
16
|
+
*/
|
|
17
|
+
export class SessionClosedError extends Error {
|
|
18
|
+
constructor(readonly clientId: string) {
|
|
19
|
+
super(`Client session ${clientId} is closed`);
|
|
20
|
+
this.name = 'SessionClosedError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Thrown when a client session writes a document it does not have open.
|
|
26
|
+
*
|
|
27
|
+
* A session writes only what it has open, so this is the answer both to a write
|
|
28
|
+
* that never opened and to one whose open was closed underneath it — by the
|
|
29
|
+
* session itself, or by the document being deleted.
|
|
30
|
+
*/
|
|
31
|
+
export class DocumentNotOpenError extends Error {
|
|
32
|
+
constructor(
|
|
33
|
+
readonly uri: string,
|
|
34
|
+
readonly clientId: string
|
|
35
|
+
) {
|
|
36
|
+
super(`Document ${uri} is not open in client session ${clientId}`);
|
|
37
|
+
this.name = 'DocumentNotOpenError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Thrown when a client session is started under an id that is already live in
|
|
43
|
+
* the process, or that the framework reserves for itself.
|
|
44
|
+
*
|
|
45
|
+
* Ids are unique process-wide because the id is also the author label on every
|
|
46
|
+
* version and the key a client recognises its own echoes by; two participants
|
|
47
|
+
* sharing one would each take the other's writes for their own.
|
|
48
|
+
*/
|
|
49
|
+
export class DuplicateClientIdError extends Error {
|
|
50
|
+
constructor(readonly clientId: string) {
|
|
51
|
+
super(`Client id ${clientId} is already in use`);
|
|
52
|
+
this.name = 'DuplicateClientIdError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { type CanonicalUri, FRAMEWORK_CLIENT_IDS } from '@hydranium/protocol';
|
|
11
|
+
import { Emitter, type Event } from 'vscode-languageserver';
|
|
12
|
+
import { INTEGRITY_CLIENT_ID } from '../langium/integrity/integrity-rule.js';
|
|
13
|
+
import { DuplicateClientIdError, SessionClosedError } from './client-session-errors.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What a client session states about one of its opens, kept per
|
|
17
|
+
* `(session, uri)` for as long as that open lasts.
|
|
18
|
+
*
|
|
19
|
+
* Any object type: an adopter declares the fields its own open path reads and
|
|
20
|
+
* passes that type as a session's `TOpenOptions`. Stored per open rather than
|
|
21
|
+
* per document, so two sessions opening one document with different options
|
|
22
|
+
* each keep their own.
|
|
23
|
+
*/
|
|
24
|
+
export type OpenOptions = object;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `'closing'` covers the span in which a session's opens are being closed. The
|
|
28
|
+
* id is still taken then, and the session can open nothing more, so an open
|
|
29
|
+
* issued from a close listener cannot outlive the session that made it.
|
|
30
|
+
*/
|
|
31
|
+
export type ClientSessionState = 'live' | 'closing';
|
|
32
|
+
|
|
33
|
+
/** Delivered by {@link ClientSessionRegistry.onDidCloseSession}. */
|
|
34
|
+
export interface ClientSessionClosedEvent {
|
|
35
|
+
readonly clientId: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The ids no client session may be started under: the wire's well-known ids and
|
|
40
|
+
* the integrity author. Each names a framework participant rather than a
|
|
41
|
+
* client, and a session holding one would have its writes read as that
|
|
42
|
+
* participant's.
|
|
43
|
+
*/
|
|
44
|
+
export const RESERVED_CLIENT_IDS: readonly string[] = [...FRAMEWORK_CLIENT_IDS, INTEGRITY_CLIENT_ID];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Which client has which document open, and which client ids are registered
|
|
48
|
+
* sessions — the open facts every head shares, composed by
|
|
49
|
+
* `HydraniumTextDocuments`.
|
|
50
|
+
*
|
|
51
|
+
* Opens are recorded for every client id, registered or not. An id that was
|
|
52
|
+
* never registered is a client that predates sessions and writes through the
|
|
53
|
+
* flat `clientId` calls; its opens are tracked exactly as a session's are, and
|
|
54
|
+
* only the session table ignores it. Requiring registration before an open
|
|
55
|
+
* would refuse every such client.
|
|
56
|
+
*
|
|
57
|
+
* One open per `(client, uri)`, with no reference count: a repeat open changes
|
|
58
|
+
* nothing, and one close ends it.
|
|
59
|
+
*
|
|
60
|
+
* Pure bookkeeping. Closing a document is the store's job, because a close has
|
|
61
|
+
* to reach the text store and its listeners; the store asks this class what to
|
|
62
|
+
* close and records each close here.
|
|
63
|
+
*/
|
|
64
|
+
export class ClientSessionRegistry {
|
|
65
|
+
protected readonly sessions = new Map<string, ClientSessionState>();
|
|
66
|
+
protected readonly clientsByUri = new Map<CanonicalUri, Set<string>>();
|
|
67
|
+
/** Each client's opens, with the options it opened them with. */
|
|
68
|
+
protected readonly opensByClient = new Map<string, Map<CanonicalUri, OpenOptions | undefined>>();
|
|
69
|
+
protected readonly reservedIds: ReadonlySet<string> = new Set(RESERVED_CLIENT_IDS);
|
|
70
|
+
protected readonly sessionClosedEmitter = new Emitter<ClientSessionClosedEvent>();
|
|
71
|
+
|
|
72
|
+
/** Fires once a session has been removed from the table, after all its opens closed. */
|
|
73
|
+
get onDidCloseSession(): Event<ClientSessionClosedEvent> {
|
|
74
|
+
return this.sessionClosedEmitter.event;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Enter `clientId` in the session table.
|
|
79
|
+
*
|
|
80
|
+
* Throws {@link DuplicateClientIdError} when the id is live anywhere in the
|
|
81
|
+
* process: reserved, registered (including by a session that is still
|
|
82
|
+
* closing), or holding opens as a client that is not a session. The last
|
|
83
|
+
* matters because the id is the author label and the echo key, and a
|
|
84
|
+
* session sharing one with another client would take that client's writes
|
|
85
|
+
* for its own.
|
|
86
|
+
*/
|
|
87
|
+
register(clientId: string): void {
|
|
88
|
+
if (this.reservedIds.has(clientId) || this.sessions.has(clientId) || this.opensByClient.has(clientId)) {
|
|
89
|
+
throw new DuplicateClientIdError(clientId);
|
|
90
|
+
}
|
|
91
|
+
this.sessions.set(clientId, 'live');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Whether `clientId` is in the session table, live or closing. */
|
|
95
|
+
isRegistered(clientId: string): boolean {
|
|
96
|
+
return this.sessions.has(clientId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Mark `clientId` as closing and return every URI it has open, as a copy the
|
|
101
|
+
* caller can close while this registry changes underneath it.
|
|
102
|
+
*/
|
|
103
|
+
beginClose(clientId: string): CanonicalUri[] {
|
|
104
|
+
if (this.sessions.has(clientId)) {
|
|
105
|
+
this.sessions.set(clientId, 'closing');
|
|
106
|
+
}
|
|
107
|
+
return this.opensOf(clientId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Remove `clientId` from the session table and announce it. A no-op for an id that is not registered. */
|
|
111
|
+
unregister(clientId: string): void {
|
|
112
|
+
if (!this.sessions.delete(clientId)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.sessionClosedEmitter.fire(Object.freeze({ clientId }));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Record that `clientId` opened `uri`, with no options. Returns `false` when
|
|
120
|
+
* it already had it open, in which case nothing changes.
|
|
121
|
+
*
|
|
122
|
+
* Throws {@link SessionClosedError} for a session that is closing.
|
|
123
|
+
*/
|
|
124
|
+
addOpen(uri: CanonicalUri, clientId: string): boolean {
|
|
125
|
+
if (this.sessions.get(clientId) === 'closing') {
|
|
126
|
+
throw new SessionClosedError(clientId);
|
|
127
|
+
}
|
|
128
|
+
let opens = this.opensByClient.get(clientId);
|
|
129
|
+
if (opens?.has(uri)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (!opens) {
|
|
133
|
+
opens = new Map();
|
|
134
|
+
this.opensByClient.set(clientId, opens);
|
|
135
|
+
}
|
|
136
|
+
opens.set(uri, undefined);
|
|
137
|
+
let clients = this.clientsByUri.get(uri);
|
|
138
|
+
if (!clients) {
|
|
139
|
+
clients = new Set();
|
|
140
|
+
this.clientsByUri.set(uri, clients);
|
|
141
|
+
}
|
|
142
|
+
clients.add(clientId);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Record that `clientId` closed `uri`. Returns `false` when it did not have it open. */
|
|
147
|
+
removeOpen(uri: CanonicalUri, clientId: string): boolean {
|
|
148
|
+
const opens = this.opensByClient.get(clientId);
|
|
149
|
+
if (!opens?.delete(uri)) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
if (opens.size === 0) {
|
|
153
|
+
this.opensByClient.delete(clientId);
|
|
154
|
+
}
|
|
155
|
+
const clients = this.clientsByUri.get(uri);
|
|
156
|
+
clients?.delete(clientId);
|
|
157
|
+
if (!clients?.size) {
|
|
158
|
+
this.clientsByUri.delete(uri);
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Whether `clientId` has `uri` open. */
|
|
164
|
+
isOpenIn(uri: CanonicalUri, clientId: string): boolean {
|
|
165
|
+
return !!this.clientsByUri.get(uri)?.has(clientId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Whether any client has `uri` open. */
|
|
169
|
+
isOpen(uri: CanonicalUri): boolean {
|
|
170
|
+
return this.clientsByUri.has(uri);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The clients that have `uri` open, in the order they opened it. */
|
|
174
|
+
clientsOf(uri: CanonicalUri): string[] {
|
|
175
|
+
return [...(this.clientsByUri.get(uri) ?? [])];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The URIs `clientId` has open, in the order it opened them. */
|
|
179
|
+
opensOf(clientId: string): CanonicalUri[] {
|
|
180
|
+
return [...(this.opensByClient.get(clientId)?.keys() ?? [])];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Every document open in at least one client, with the clients that have it open. */
|
|
184
|
+
openDocuments(): Array<{ readonly uri: CanonicalUri; readonly clients: string[] }> {
|
|
185
|
+
return [...this.clientsByUri].map(([uri, clients]) => ({ uri, clients: [...clients] }));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The options `clientId` opened `uri` with, or `undefined` when it gave none or does not have it open. */
|
|
189
|
+
openOptions(uri: CanonicalUri, clientId: string): OpenOptions | undefined {
|
|
190
|
+
return this.opensByClient.get(clientId)?.get(uri);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Replace the options of an existing open. Does nothing when `clientId` does not have `uri` open. */
|
|
194
|
+
setOpenOptions(uri: CanonicalUri, clientId: string, options: OpenOptions | undefined): void {
|
|
195
|
+
const opens = this.opensByClient.get(clientId);
|
|
196
|
+
if (opens?.has(uri)) {
|
|
197
|
+
opens.set(uri, options);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Forget every session and open without announcing anything. Subscriptions
|
|
203
|
+
* stay, so a listener registered before the clear hears the sessions that
|
|
204
|
+
* end after it.
|
|
205
|
+
*/
|
|
206
|
+
clear(): void {
|
|
207
|
+
this.sessions.clear();
|
|
208
|
+
this.clientsByUri.clear();
|
|
209
|
+
this.opensByClient.clear();
|
|
210
|
+
}
|
|
211
|
+
}
|