@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.
Files changed (108) hide show
  1. package/lib/documents/ast-document-manager.d.ts +8 -44
  2. package/lib/documents/ast-document-manager.d.ts.map +1 -1
  3. package/lib/documents/ast-document-manager.js +22 -41
  4. package/lib/documents/ast-document-manager.js.map +1 -1
  5. package/lib/documents/client-session-errors.d.ts +4 -33
  6. package/lib/documents/client-session-errors.d.ts.map +1 -1
  7. package/lib/documents/client-session-errors.js +4 -46
  8. package/lib/documents/client-session-errors.js.map +1 -1
  9. package/lib/documents/client-session-registry.d.ts +40 -20
  10. package/lib/documents/client-session-registry.d.ts.map +1 -1
  11. package/lib/documents/client-session-registry.js +33 -20
  12. package/lib/documents/client-session-registry.js.map +1 -1
  13. package/lib/documents/file-system-task-queue.d.ts +49 -0
  14. package/lib/documents/file-system-task-queue.d.ts.map +1 -0
  15. package/lib/documents/file-system-task-queue.js +34 -0
  16. package/lib/documents/file-system-task-queue.js.map +1 -0
  17. package/lib/documents/hydranium-text-documents.d.ts +138 -13
  18. package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
  19. package/lib/documents/hydranium-text-documents.js +283 -40
  20. package/lib/documents/hydranium-text-documents.js.map +1 -1
  21. package/lib/documents/index.d.ts +1 -0
  22. package/lib/documents/index.d.ts.map +1 -1
  23. package/lib/documents/index.js +1 -0
  24. package/lib/documents/index.js.map +1 -1
  25. package/lib/langium/document-builder/document-builder.d.ts +15 -2
  26. package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
  27. package/lib/langium/document-builder/document-builder.js +15 -2
  28. package/lib/langium/document-builder/document-builder.js.map +1 -1
  29. package/lib/langium/integrity/integrity-service.d.ts +3 -2
  30. package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
  31. package/lib/langium/integrity/integrity-service.js +9 -6
  32. package/lib/langium/integrity/integrity-service.js.map +1 -1
  33. package/lib/langium/language-module.d.ts +2 -2
  34. package/lib/langium/language-module.js +2 -2
  35. package/lib/langium/language-module.js.map +1 -1
  36. package/lib/langium/model-service/client-session.d.ts +203 -24
  37. package/lib/langium/model-service/client-session.d.ts.map +1 -1
  38. package/lib/langium/model-service/client-session.js +233 -29
  39. package/lib/langium/model-service/client-session.js.map +1 -1
  40. package/lib/langium/model-service/model-service.d.ts +62 -217
  41. package/lib/langium/model-service/model-service.d.ts.map +1 -1
  42. package/lib/langium/model-service/model-service.js +54 -357
  43. package/lib/langium/model-service/model-service.js.map +1 -1
  44. package/lib/langium/module.d.ts +11 -1
  45. package/lib/langium/module.d.ts.map +1 -1
  46. package/lib/langium/module.js +5 -1
  47. package/lib/langium/module.js.map +1 -1
  48. package/lib/langium/update-rewrite/update-rewrite.d.ts +1 -1
  49. package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
  50. package/lib/langium/workspace/in-memory-file-system-provider.js +10 -2
  51. package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
  52. package/lib/langium/workspace/write-lock-scope.d.ts +1 -1
  53. package/lib/langium/workspace/write-lock-scope.js +1 -1
  54. package/lib/lsp/hydranium-document-update-handler.d.ts +69 -67
  55. package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
  56. package/lib/lsp/hydranium-document-update-handler.js +111 -89
  57. package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
  58. package/lib/lsp/shared-module.d.ts +4 -8
  59. package/lib/lsp/shared-module.d.ts.map +1 -1
  60. package/lib/lsp/shared-module.js.map +1 -1
  61. package/lib/lsp/start-language-server.d.ts +3 -4
  62. package/lib/lsp/start-language-server.d.ts.map +1 -1
  63. package/lib/lsp/start-language-server.js +3 -4
  64. package/lib/lsp/start-language-server.js.map +1 -1
  65. package/lib/testing/make-test-services.d.ts +16 -4
  66. package/lib/testing/make-test-services.d.ts.map +1 -1
  67. package/lib/testing/make-test-services.js +6 -2
  68. package/lib/testing/make-test-services.js.map +1 -1
  69. package/lib/testing/node/lsp-server-connection.d.ts +1 -1
  70. package/lib/testing/node/scratch-workspace.js +1 -1
  71. package/lib/testing/run-update-pipeline.d.ts +4 -4
  72. package/lib/testing/run-update-pipeline.d.ts.map +1 -1
  73. package/lib/testing/stub-ast-document-manager.d.ts +5 -5
  74. package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
  75. package/lib/testing/stub-ast-document-manager.js +6 -8
  76. package/lib/testing/stub-ast-document-manager.js.map +1 -1
  77. package/lib/testing/stub-hydranium-text-documents.d.ts +10 -5
  78. package/lib/testing/stub-hydranium-text-documents.d.ts.map +1 -1
  79. package/lib/testing/stub-hydranium-text-documents.js +16 -9
  80. package/lib/testing/stub-hydranium-text-documents.js.map +1 -1
  81. package/lib/testing/stub-model-service.d.ts +2 -2
  82. package/lib/testing/stub-model-service.js +2 -2
  83. package/package.json +5 -5
  84. package/src/documents/ast-document-manager.ts +27 -78
  85. package/src/documents/client-session-errors.ts +11 -43
  86. package/src/documents/client-session-registry.ts +54 -28
  87. package/src/documents/file-system-task-queue.ts +68 -0
  88. package/src/documents/hydranium-text-documents.ts +348 -44
  89. package/src/documents/index.ts +1 -0
  90. package/src/langium/document-builder/document-builder.ts +16 -2
  91. package/src/langium/integrity/integrity-service.ts +9 -6
  92. package/src/langium/language-module.ts +4 -4
  93. package/src/langium/model-service/client-session.ts +363 -39
  94. package/src/langium/model-service/model-service.ts +91 -420
  95. package/src/langium/module.ts +14 -2
  96. package/src/langium/update-rewrite/update-rewrite.ts +1 -1
  97. package/src/langium/workspace/in-memory-file-system-provider.ts +11 -2
  98. package/src/langium/workspace/write-lock-scope.ts +1 -1
  99. package/src/lsp/hydranium-document-update-handler.ts +138 -92
  100. package/src/lsp/shared-module.ts +4 -8
  101. package/src/lsp/start-language-server.ts +3 -4
  102. package/src/testing/make-test-services.ts +25 -6
  103. package/src/testing/node/lsp-server-connection.ts +1 -1
  104. package/src/testing/node/scratch-workspace.ts +1 -1
  105. package/src/testing/run-update-pipeline.ts +4 -4
  106. package/src/testing/stub-ast-document-manager.ts +10 -24
  107. package/src/testing/stub-hydranium-text-documents.ts +29 -16
  108. 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 per
17
- * `(session, uri)` for as long as that open lasts.
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`. Stored per open rather than
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
- * `'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.
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. 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.
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
- /** Each client's opens, with the options it opened them with. */
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`, with no options. Returns `false` when
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 Map();
151
+ opens = new Set();
134
152
  this.opensByClient.set(clientId, opens);
135
153
  }
136
- opens.set(uri, undefined);
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)?.keys() ?? [])];
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
- /** 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);
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
- /** 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
- }
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 open without announcing anything. Subscriptions
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
+ }