@hydranium/core 1.0.0-next.117 → 1.0.0-next.123
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/documents/ast-document-manager.d.ts +8 -44
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +22 -41
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/client-session-errors.d.ts +4 -33
- package/lib/documents/client-session-errors.d.ts.map +1 -1
- package/lib/documents/client-session-errors.js +4 -46
- package/lib/documents/client-session-errors.js.map +1 -1
- package/lib/documents/client-session-registry.d.ts +40 -20
- package/lib/documents/client-session-registry.d.ts.map +1 -1
- package/lib/documents/client-session-registry.js +33 -20
- package/lib/documents/client-session-registry.js.map +1 -1
- package/lib/documents/file-system-task-queue.d.ts +49 -0
- package/lib/documents/file-system-task-queue.d.ts.map +1 -0
- package/lib/documents/file-system-task-queue.js +34 -0
- package/lib/documents/file-system-task-queue.js.map +1 -0
- package/lib/documents/hydranium-text-documents.d.ts +138 -13
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +283 -40
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/index.d.ts +1 -0
- package/lib/documents/index.d.ts.map +1 -1
- package/lib/documents/index.js +1 -0
- package/lib/documents/index.js.map +1 -1
- package/lib/langium/document-builder/document-builder.d.ts +15 -2
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +15 -2
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +3 -2
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +9 -6
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/language-module.d.ts +2 -2
- package/lib/langium/language-module.js +2 -2
- package/lib/langium/language-module.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts +203 -24
- package/lib/langium/model-service/client-session.d.ts.map +1 -1
- package/lib/langium/model-service/client-session.js +233 -29
- package/lib/langium/model-service/client-session.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +62 -217
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +54 -357
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +11 -1
- package/lib/langium/module.d.ts.map +1 -1
- package/lib/langium/module.js +5 -1
- package/lib/langium/module.js.map +1 -1
- package/lib/langium/update-rewrite/update-rewrite.d.ts +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.js +10 -2
- package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
- package/lib/langium/workspace/write-lock-scope.d.ts +1 -1
- package/lib/langium/workspace/write-lock-scope.js +1 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts +69 -67
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +111 -89
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/lsp/shared-module.d.ts +4 -8
- package/lib/lsp/shared-module.d.ts.map +1 -1
- package/lib/lsp/shared-module.js.map +1 -1
- package/lib/lsp/start-language-server.d.ts +3 -4
- package/lib/lsp/start-language-server.d.ts.map +1 -1
- package/lib/lsp/start-language-server.js +3 -4
- package/lib/lsp/start-language-server.js.map +1 -1
- package/lib/testing/make-test-services.d.ts +16 -4
- package/lib/testing/make-test-services.d.ts.map +1 -1
- package/lib/testing/make-test-services.js +6 -2
- package/lib/testing/make-test-services.js.map +1 -1
- package/lib/testing/node/lsp-server-connection.d.ts +1 -1
- package/lib/testing/node/scratch-workspace.js +1 -1
- package/lib/testing/run-update-pipeline.d.ts +4 -4
- package/lib/testing/run-update-pipeline.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +5 -5
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +6 -8
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.d.ts +10 -5
- package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
- package/lib/testing/stub-hydranium-text-documents.js +16 -9
- package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
- package/lib/testing/stub-model-service.d.ts +2 -2
- package/lib/testing/stub-model-service.js +2 -2
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +27 -78
- package/src/documents/client-session-errors.ts +11 -43
- package/src/documents/client-session-registry.ts +54 -28
- package/src/documents/file-system-task-queue.ts +68 -0
- package/src/documents/hydranium-text-documents.ts +348 -44
- package/src/documents/index.ts +1 -0
- package/src/langium/document-builder/document-builder.ts +16 -2
- package/src/langium/integrity/integrity-service.ts +9 -6
- package/src/langium/language-module.ts +4 -4
- package/src/langium/model-service/client-session.ts +363 -39
- package/src/langium/model-service/model-service.ts +91 -420
- package/src/langium/module.ts +14 -2
- package/src/langium/update-rewrite/update-rewrite.ts +1 -1
- package/src/langium/workspace/in-memory-file-system-provider.ts +11 -2
- package/src/langium/workspace/write-lock-scope.ts +1 -1
- package/src/lsp/hydranium-document-update-handler.ts +138 -92
- package/src/lsp/shared-module.ts +4 -8
- package/src/lsp/start-language-server.ts +3 -4
- package/src/testing/make-test-services.ts +25 -6
- package/src/testing/node/lsp-server-connection.ts +1 -1
- package/src/testing/node/scratch-workspace.ts +1 -1
- package/src/testing/run-update-pipeline.ts +4 -4
- package/src/testing/stub-ast-document-manager.ts +10 -24
- package/src/testing/stub-hydranium-text-documents.ts +29 -16
- package/src/testing/stub-model-service.ts +2 -2
|
@@ -8,25 +8,28 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import { type CanonicalUri, FRAMEWORK_CLIENT_IDS } from '@hydranium/protocol';
|
|
11
|
-
import { Emitter, type Event } from 'vscode-languageserver';
|
|
11
|
+
import { type Disposable, Emitter, type Event } from 'vscode-languageserver';
|
|
12
12
|
import { INTEGRITY_CLIENT_ID } from '../langium/integrity/integrity-rule.js';
|
|
13
13
|
import { DuplicateClientIdError, SessionClosedError } from './client-session-errors.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* What a client session states about one of its opens, kept
|
|
17
|
-
*
|
|
16
|
+
* What a client session states about one of its opens, kept by the session
|
|
17
|
+
* and readable only while that open lasts.
|
|
18
18
|
*
|
|
19
19
|
* Any object type: an adopter declares the fields its own open path reads and
|
|
20
|
-
* passes that type as a session's `TOpenOptions`.
|
|
20
|
+
* passes that type as a session's `TOpenOptions`. Kept per open rather than
|
|
21
21
|
* per document, so two sessions opening one document with different options
|
|
22
22
|
* each keep their own.
|
|
23
23
|
*/
|
|
24
24
|
export type OpenOptions = object;
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* `'
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* A session is `'live'` from its registration, which is synchronous, so no
|
|
28
|
+
* state comes before it. `'closing'` covers the span in which the session's
|
|
29
|
+
* opens are being closed: the id is still taken, and the session can open
|
|
30
|
+
* nothing more, so an open issued from a close listener cannot outlive the
|
|
31
|
+
* session that made it. An id with no state has no session: it was never
|
|
32
|
+
* registered, or has closed.
|
|
30
33
|
*/
|
|
31
34
|
export type ClientSessionState = 'live' | 'closing';
|
|
32
35
|
|
|
@@ -35,6 +38,15 @@ export interface ClientSessionClosedEvent {
|
|
|
35
38
|
readonly clientId: string;
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Why a close happened. `'lost'` is a close caused by the client's connection
|
|
43
|
+
* going away rather than by the client, and only such a last close waits out
|
|
44
|
+
* the store's revert grace: a client that closed or ended on purpose has
|
|
45
|
+
* discarded its unsaved edits, while one that lost its connection may be about
|
|
46
|
+
* to register again and open the document once more.
|
|
47
|
+
*/
|
|
48
|
+
export type SessionEndCause = 'closed' | 'lost';
|
|
49
|
+
|
|
38
50
|
/**
|
|
39
51
|
* The ids no client session may be started under: the wire's well-known ids and
|
|
40
52
|
* the integrity author. Each names a framework participant rather than a
|
|
@@ -48,11 +60,11 @@ export const RESERVED_CLIENT_IDS: readonly string[] = [...FRAMEWORK_CLIENT_IDS,
|
|
|
48
60
|
* sessions — the open facts every head shares, composed by
|
|
49
61
|
* `HydraniumTextDocuments`.
|
|
50
62
|
*
|
|
51
|
-
* Opens are recorded for every client id, registered or not.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
63
|
+
* Opens are recorded for every client id, registered or not. The language
|
|
64
|
+
* client opens under its reserved id without a session, over the LSP
|
|
65
|
+
* connection, and its opens are tracked exactly as a session's are; only the
|
|
66
|
+
* session table ignores it. Requiring registration before an open would refuse
|
|
67
|
+
* the editor.
|
|
56
68
|
*
|
|
57
69
|
* One open per `(client, uri)`, with no reference count: a repeat open changes
|
|
58
70
|
* nothing, and one close ends it.
|
|
@@ -64,10 +76,16 @@ export const RESERVED_CLIENT_IDS: readonly string[] = [...FRAMEWORK_CLIENT_IDS,
|
|
|
64
76
|
export class ClientSessionRegistry {
|
|
65
77
|
protected readonly sessions = new Map<string, ClientSessionState>();
|
|
66
78
|
protected readonly clientsByUri = new Map<CanonicalUri, Set<string>>();
|
|
67
|
-
|
|
68
|
-
protected readonly opensByClient = new Map<string, Map<CanonicalUri, OpenOptions | undefined>>();
|
|
79
|
+
protected readonly opensByClient = new Map<string, Set<CanonicalUri>>();
|
|
69
80
|
protected readonly reservedIds: ReadonlySet<string> = new Set(RESERVED_CLIENT_IDS);
|
|
70
81
|
protected readonly sessionClosedEmitter = new Emitter<ClientSessionClosedEvent>();
|
|
82
|
+
/**
|
|
83
|
+
* The documents whose last open closed with a lost connection and whose
|
|
84
|
+
* revert is deferred, each with the timer that runs it. Any open of such a
|
|
85
|
+
* document cancels its timer; the store decides whether the opener keeps the
|
|
86
|
+
* unsaved text.
|
|
87
|
+
*/
|
|
88
|
+
protected readonly pendingReverts = new Map<CanonicalUri, Disposable>();
|
|
71
89
|
|
|
72
90
|
/** Fires once a session has been removed from the table, after all its opens closed. */
|
|
73
91
|
get onDidCloseSession(): Event<ClientSessionClosedEvent> {
|
|
@@ -116,7 +134,7 @@ export class ClientSessionRegistry {
|
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
/**
|
|
119
|
-
* Record that `clientId` opened `uri
|
|
137
|
+
* Record that `clientId` opened `uri`. Returns `false` when
|
|
120
138
|
* it already had it open, in which case nothing changes.
|
|
121
139
|
*
|
|
122
140
|
* Throws {@link SessionClosedError} for a session that is closing.
|
|
@@ -130,10 +148,10 @@ export class ClientSessionRegistry {
|
|
|
130
148
|
return false;
|
|
131
149
|
}
|
|
132
150
|
if (!opens) {
|
|
133
|
-
opens = new
|
|
151
|
+
opens = new Set();
|
|
134
152
|
this.opensByClient.set(clientId, opens);
|
|
135
153
|
}
|
|
136
|
-
opens.
|
|
154
|
+
opens.add(uri);
|
|
137
155
|
let clients = this.clientsByUri.get(uri);
|
|
138
156
|
if (!clients) {
|
|
139
157
|
clients = new Set();
|
|
@@ -177,7 +195,7 @@ export class ClientSessionRegistry {
|
|
|
177
195
|
|
|
178
196
|
/** The URIs `clientId` has open, in the order it opened them. */
|
|
179
197
|
opensOf(clientId: string): CanonicalUri[] {
|
|
180
|
-
return [...(this.opensByClient.get(clientId)
|
|
198
|
+
return [...(this.opensByClient.get(clientId) ?? [])];
|
|
181
199
|
}
|
|
182
200
|
|
|
183
201
|
/** Every document open in at least one client, with the clients that have it open. */
|
|
@@ -185,21 +203,25 @@ export class ClientSessionRegistry {
|
|
|
185
203
|
return [...this.clientsByUri].map(([uri, clients]) => ({ uri, clients: [...clients] }));
|
|
186
204
|
}
|
|
187
205
|
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
|
|
206
|
+
/** Record `timer` as the deferred revert of `uri`. */
|
|
207
|
+
deferRevert(uri: CanonicalUri, timer: Disposable): void {
|
|
208
|
+
this.pendingReverts.set(uri, timer);
|
|
191
209
|
}
|
|
192
210
|
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
211
|
+
/** Whether the revert of `uri` is deferred. */
|
|
212
|
+
isRevertPending(uri: CanonicalUri): boolean {
|
|
213
|
+
return this.pendingReverts.has(uri);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Cancel the deferred revert of `uri`, if one is pending. */
|
|
217
|
+
cancelRevert(uri: CanonicalUri): void {
|
|
218
|
+
this.pendingReverts.get(uri)?.dispose();
|
|
219
|
+
this.pendingReverts.delete(uri);
|
|
199
220
|
}
|
|
200
221
|
|
|
201
222
|
/**
|
|
202
|
-
* Forget every session and
|
|
223
|
+
* Forget every session, open and deferred revert without announcing
|
|
224
|
+
* anything, for a test double that resets between tests. Subscriptions
|
|
203
225
|
* stay, so a listener registered before the clear hears the sessions that
|
|
204
226
|
* end after it.
|
|
205
227
|
*/
|
|
@@ -207,5 +229,9 @@ export class ClientSessionRegistry {
|
|
|
207
229
|
this.sessions.clear();
|
|
208
230
|
this.clientsByUri.clear();
|
|
209
231
|
this.opensByClient.clear();
|
|
232
|
+
for (const timer of this.pendingReverts.values()) {
|
|
233
|
+
timer.dispose();
|
|
234
|
+
}
|
|
235
|
+
this.pendingReverts.clear();
|
|
210
236
|
}
|
|
211
237
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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 } from '@hydranium/protocol';
|
|
11
|
+
import { type ServerSharedServices } from '../langium/module.js';
|
|
12
|
+
import { type DocumentUriPolicy } from '../langium/workspace/document-uri-policy.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One queue of file-system tasks per file, shared by every server-side reader
|
|
16
|
+
* and writer of a file whose accesses must not interleave with its saves.
|
|
17
|
+
*
|
|
18
|
+
* Two writes of one file that bypass the queue can land in either order, and
|
|
19
|
+
* the older text then stays on disk. Tasks of different files run in
|
|
20
|
+
* parallel.
|
|
21
|
+
*/
|
|
22
|
+
export interface FileSystemTaskQueue {
|
|
23
|
+
/**
|
|
24
|
+
* Run `task` once every task already enqueued for `uri` has settled, and
|
|
25
|
+
* before any enqueued after it. `uri` is keyed by its canonical identity, so
|
|
26
|
+
* every spelling of one file shares a queue. A task that has nothing to do
|
|
27
|
+
* still resolves once the tasks before it have settled.
|
|
28
|
+
*
|
|
29
|
+
* A task must not await a build, nor a save, another task or an open of a
|
|
30
|
+
* document no client has open, all of the same file: a build's integrity
|
|
31
|
+
* repair enqueues its own write and the build waits for it, and such an
|
|
32
|
+
* open reads the file here, so a task waiting on either never resolves,
|
|
33
|
+
* which wedges the file's queue for good.
|
|
34
|
+
*/
|
|
35
|
+
enqueue<T>(uri: string, task: () => Promise<T>): Promise<T>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class DefaultFileSystemTaskQueue implements FileSystemTaskQueue {
|
|
39
|
+
/**
|
|
40
|
+
* Per canonical URI, the settling of the last task enqueued for it; the next
|
|
41
|
+
* task chains behind it. An entry leaves once its file's queue is idle. Kept
|
|
42
|
+
* outside the workspace lock, whose `write` cancels the write before it: a
|
|
43
|
+
* save cancelled that way reports success for text it never wrote.
|
|
44
|
+
*/
|
|
45
|
+
protected readonly queues = new Map<CanonicalUri, Promise<void>>();
|
|
46
|
+
|
|
47
|
+
protected readonly uriPolicy: DocumentUriPolicy;
|
|
48
|
+
|
|
49
|
+
constructor(services: { readonly workspace: Pick<ServerSharedServices['workspace'], 'DocumentUriPolicy'> }) {
|
|
50
|
+
this.uriPolicy = services.workspace.DocumentUriPolicy;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
enqueue<T>(uri: string, task: () => Promise<T>): Promise<T> {
|
|
54
|
+
const key = this.uriPolicy.canonicalUri(uri);
|
|
55
|
+
const result = (this.queues.get(key) ?? Promise.resolve()).then(task);
|
|
56
|
+
const settled = result.then(
|
|
57
|
+
() => undefined,
|
|
58
|
+
() => undefined
|
|
59
|
+
);
|
|
60
|
+
this.queues.set(key, settled);
|
|
61
|
+
void settled.then(() => {
|
|
62
|
+
if (this.queues.get(key) === settled) {
|
|
63
|
+
this.queues.delete(key);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
}
|