@hydranium/data-server 1.0.0-next.9 → 1.0.0-next.91

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.
@@ -9,12 +9,15 @@
9
9
 
10
10
  import {
11
11
  createRpcProxy,
12
+ defineMessage,
12
13
  DisposableCollection,
13
14
  isDocumentSource,
14
15
  isElementSource,
15
16
  isSyntheticSource,
17
+ messageError,
16
18
  ReferenceSource,
17
19
  type CloseModelArgs,
20
+ type HydraniumResponseError,
18
21
  type Disposable,
19
22
  type ElementSource,
20
23
  type FindNextNameArgs,
@@ -28,8 +31,9 @@ import {
28
31
  type ReferenceTarget,
29
32
  type Tracer,
30
33
  type TransferDiagnostic,
31
- type TransferDocument,
32
- type TransferElement
34
+ TransferDocument,
35
+ type TransferElement,
36
+ asSnapshotVersion
33
37
  } from '@hydranium/protocol';
34
38
  import {
35
39
  DATA_SERVER_DIAGNOSTICS_METHODS,
@@ -52,11 +56,34 @@ import {
52
56
  } from '@hydranium/protocol/data';
53
57
  import { REVERT_ON_CLOSE_CLIENT_ID, UNKNOWN_CLIENT_ID } from '@hydranium/core';
54
58
  import { defaultDataServerDiagnostics } from './default-diagnostics.js';
59
+
60
+ /**
61
+ * Raised when a profiling command arrives with no capture running.
62
+ *
63
+ * A `ResponseError` rather than a plain `Error` so the identity survives to the
64
+ * response boundary, which is where the message is rendered. An unwrapped throw
65
+ * reaches the client as a generic internal error instead: no code for a caller
66
+ * to switch on, and nothing for the boundary to render from.
67
+ */
68
+ export const NO_ACTIVE_PROFILE = defineMessage(
69
+ 'hydranium/data-server/no-active-profile',
70
+ 'No profiling capture is active; call startProfiling first.'
71
+ );
72
+
73
+ /**
74
+ * The JSON-RPC code, unrelated to the catalogue code above: this one is numeric,
75
+ * survives reconstruction and is what a caller switches on.
76
+ */
77
+ export const NO_ACTIVE_PROFILE_CODE = 1002;
78
+
79
+ export const noActiveProfileError = (): HydraniumResponseError => messageError(NO_ACTIVE_PROFILE_CODE, NO_ACTIVE_PROFILE);
80
+
55
81
  import type { DataServerDiagnosticsProvider, DataServerProfileCapture } from './diagnostics-provider.js';
56
82
  import type {
57
83
  ClientTextDocumentChangeEvent,
58
84
  HydraniumLanguageServices,
59
85
  LogNameOptions,
86
+ AstDiagnostic,
60
87
  ModelService,
61
88
  ProjectChangeEvent,
62
89
  ServerSharedServices,
@@ -258,9 +285,9 @@ export interface DataServerOptions extends LogNameOptions {
258
285
  * When supplied, every inbound data-server RPC is timed into this collector
259
286
  * (via the `createRpcProxy` binding) and exposed through
260
287
  * {@link DataServerDiagnosticsProtocol.getLatency}. A head that also runs an
261
- * LSP connection can pass the SAME collector to
262
- * `instrumentLspConnection(connection, latency)` so one report covers both
263
- * heads. Absent by default (no timing overhead).
288
+ * LSP connection can pass the SAME collector through `lspLatencyOptions` to
289
+ * that connection, so one report covers both heads. Absent by default (no
290
+ * timing overhead).
264
291
  */
265
292
  readonly latency?: LatencyCollector;
266
293
  }
@@ -301,9 +328,11 @@ interface ResolvedDataServerOptions {
301
328
  * any names supplied via {@link DataServerOptions.additionalMethods}) and
302
329
  * builds the {@link DataClientProtocol} notification proxy on the same
303
330
  * connection in one {@link createRpcProxy} call (binding `this` as its
304
- * `localTarget`) under the configured namespace, and subscribes one
305
- * `DocumentBuilder.onDocumentPhase` listener per configured phase that
306
- * dispatches subscription events via `clientProxy.onDocumentUpdated`.
331
+ * `localTarget`) under the configured namespace, and subscribes two listeners
332
+ * at the configured phase: a `DocumentBuilder.onDocumentPhase` one dispatching
333
+ * per-document `clientProxy.onDocumentUpdated` for watched URIs, and a
334
+ * `DocumentBuilder.onBuildPhase` one dispatching a single
335
+ * `clientProxy.onDocumentsBuilt` naming the batch's unwatched documents.
307
336
  *
308
337
  * The data-server requires the full {@link ServerSharedServices}
309
338
  * shape — `HydraniumTextDocuments` for client-attributed updates,
@@ -356,10 +385,12 @@ export class DataServer<
356
385
  /**
357
386
  * Snapshot of the most recent `DocumentBuilder.onUpdate` event. Drives
358
387
  * `reason` discrimination on outbound `onDocumentUpdated` notifications:
359
- * a URI in the `changed` list emits `'changed'`, in `deleted` emits
360
- * `'deleted'`, otherwise `'rebuilt'` (cascade rebuild from a dependent
361
- * URI's change). The same mechanism `AstDocumentManager.onUpdate` uses on
362
- * the LSP side, so reason fidelity stays consistent between heads.
388
+ * a URI in the `changed` list emits `'changed'`, otherwise `'rebuilt'`
389
+ * (cascade rebuild from a dependent URI's change). The same mechanism
390
+ * `AstDocumentManager.onUpdate` uses on the LSP side, so reason fidelity
391
+ * stays consistent between heads. The `deleted` half is read by
392
+ * {@link dispatchDeleteEvents} rather than by the reason discrimination,
393
+ * a deletion travelling on its own channel.
363
394
  */
364
395
  protected lastBuildUpdate?: { changed: readonly URI[]; deleted: readonly URI[] };
365
396
  /**
@@ -420,7 +451,7 @@ export class DataServer<
420
451
  /** Per-method RPC latency collector, when the head opted in via {@link DataServerOptions.latency}. */
421
452
  protected readonly latency?: LatencyCollector;
422
453
  /** Encoder pulled from DI. Adopters rebind `services.model.TransferEncoder` to a typed-overlay subclass. */
423
- protected readonly encoder: TransferEncoder<unknown, TDiagnostic>;
454
+ protected readonly encoder: TransferEncoder<TDiagnostic>;
424
455
  /**
425
456
  * In-process workspace facade — the lifecycle delegate `get` / `update` / `save` go through.
426
457
  * The facade's AstDocument diagnostic shape is intentionally typed `unknown` here: adopters
@@ -429,7 +460,7 @@ export class DataServer<
429
460
  * `astDocumentToTransferDocument` accepts both shapes (wire-shape or LSP-shape) and projects
430
461
  * to wire shape on the return — see `TransferEncoder.astDocumentToTransferDocument`.
431
462
  */
432
- protected readonly modelService: ModelService<AstNode, unknown, TTransfer>;
463
+ protected readonly modelService: ModelService<AstNode, AstDiagnostic, TTransfer>;
433
464
 
434
465
  constructor(
435
466
  protected readonly connection: MessageConnection,
@@ -448,8 +479,8 @@ export class DataServer<
448
479
  'Did you compose `createServerSharedModule(ctx)` into your shared module?'
449
480
  );
450
481
  }
451
- this.encoder = encoder as TransferEncoder<unknown, TDiagnostic>;
452
- this.modelService = modelService as ModelService<AstNode, unknown, TTransfer>;
482
+ this.encoder = encoder as TransferEncoder<TDiagnostic>;
483
+ this.modelService = modelService as ModelService<AstNode, AstDiagnostic, TTransfer>;
453
484
  const excluded = new Set<string>(this.options.excludedMethods);
454
485
  const registeredMethods = [
455
486
  ...DATA_SERVER_PROTOCOL_METHODS,
@@ -465,11 +496,16 @@ export class DataServer<
465
496
  methodNamespace: this.options.methodNamespace,
466
497
  localTarget: this,
467
498
  localMethods: registeredMethods as readonly (keyof this & string)[],
468
- latency: this.latency
499
+ latency: this.latency,
500
+ // Bound at the chokepoint rather than per handler, so an adopter's
501
+ // `additionalMethods` are rendered on the same terms as the
502
+ // framework's own rejections.
503
+ renderErrorMessage: error => this.services.MessageRenderer.renderError(error)
469
504
  });
470
505
  this.disposables.push(
471
506
  this.services.workspace.DocumentBuilder.onUpdate((changed, deleted) => {
472
507
  this.lastBuildUpdate = { changed, deleted };
508
+ this.dispatchDeleteEvents(deleted);
473
509
  })
474
510
  );
475
511
  this.subscribeToDocumentBuilder();
@@ -547,11 +583,11 @@ export class DataServer<
547
583
  // multi-client synced version whenever the open seeded the synced document
548
584
  // with a caller-supplied version id — and the synced version is what
549
585
  // `ModelService.update`'s optimistic-concurrency gate compares
550
- // `baseVersion` against, so reporting the snapshot's would make the
586
+ // `basedOn` against, so reporting the snapshot's would make the
551
587
  // caller's first tagged write self-conflict. A genuine concurrent edit
552
588
  // still trips the gate and is reconciled by the caller's replay rather
553
589
  // than predicted here.
554
- return { ...document, version: this.services.workspace.TextDocuments.version(args.uri) };
590
+ return { ...document, version: asSnapshotVersion(this.services.workspace.TextDocuments.version(args.uri)) };
555
591
  }
556
592
 
557
593
  async closeModelDocument(args: CloseModelArgs): Promise<void> {
@@ -764,7 +800,7 @@ export class DataServer<
764
800
  // synthetic source — and the create-element flow driving this method is
765
801
  // the one that produces directory URIs, on which a bare
766
802
  // `getServices(uri)` throws while `findReferenceCandidates` succeeds.
767
- const nameProvider = this.resolveReferenceServices(ReferenceSource.synthetic(args.uri, args.type)).NameProvider;
803
+ const nameProvider = this.resolveReferenceServices(ReferenceSource.synthetic(args.uri, args.type, args.language)).NameProvider;
768
804
  const tier = args.tier ?? 'project';
769
805
  if (tier === 'public') {
770
806
  return nameProvider.findNextProjectQualifiedName(args.type, args.proposal);
@@ -811,6 +847,9 @@ export class DataServer<
811
847
  /**
812
848
  * Pick the language that owns a reference source, in order:
813
849
  *
850
+ * 0. A {@link isSyntheticSource} naming its own `language` routes there.
851
+ * First because every step below infers, and an inference must not
852
+ * override a caller that has said which grammar it means.
814
853
  * 1. A URI-bearing source ({@link isDocumentSource}/{@link isSyntheticSource})
815
854
  * whose URI resolves to a registered language routes by URI.
816
855
  * 2. A single-language workspace always routes to that one language — so
@@ -829,6 +868,15 @@ export class DataServer<
829
868
  */
830
869
  protected resolveReferenceLanguage(source: ReferenceSource): HydraniumLanguageServices | undefined {
831
870
  const registry = this.services.ServiceRegistry;
871
+ if (isSyntheticSource(source) && source.language !== undefined) {
872
+ const declared = registry.getServicesById(source.language);
873
+ if (declared) {
874
+ return declared;
875
+ }
876
+ // Falling through on an UNREGISTERED id rather than failing: the id
877
+ // crosses the wire, so a client built against a head with one more
878
+ // grammar would otherwise lose queries the steps below can answer.
879
+ }
832
880
  // `getServicesFor` (non-throwing, one ladder walk) gates the URI lookup:
833
881
  // an extensionless / unregistered URI falls through to the steps below
834
882
  // rather than throwing "no services for the extension ''".
@@ -945,14 +993,9 @@ export class DataServer<
945
993
  // decides policy at its own layer; `root` is optional on the envelope
946
994
  // so the compiler forces that decision. Adopters preferring to throw
947
995
  // override `envelope`.
948
- return {
949
- uri: uri.toString(),
950
- version: 0,
951
- root: undefined,
952
- diagnostics: [] as TDiagnostic[]
953
- };
996
+ return TransferDocument.absent<TTransfer, TDiagnostic>(uri.toString());
954
997
  }
955
- return this.encoder.toTransferDocument<AstNode>(document) as unknown as TransferDocument<TTransfer, TDiagnostic>;
998
+ return this.encoder.toTransferDocument(document) as unknown as TransferDocument<TTransfer, TDiagnostic>;
956
999
  }
957
1000
 
958
1001
  /**
@@ -967,6 +1010,57 @@ export class DataServer<
967
1010
  this.dispatchPhaseEvent(document, cancelToken)
968
1011
  )
969
1012
  );
1013
+ // The BUILD-phase hook, not the per-document one: this notification is one
1014
+ // message per build rather than one per document, and Langium hands the
1015
+ // whole batch over here. It also does not fire for a cancelled build,
1016
+ // which is what the per-document dispatch has to check by hand.
1017
+ this.disposables.push(
1018
+ this.services.workspace.DocumentBuilder.onBuildPhase(this.options.subscriptionPhase, built => this.dispatchBuiltEvent(built))
1019
+ );
1020
+ }
1021
+
1022
+ /**
1023
+ * Report the documents that reached {@link DataServerOptions.subscriptionPhase}
1024
+ * and that NOBODY on this
1025
+ * connection is watching — the ones a client was not told about through
1026
+ * {@link dispatchPhaseEvent}, which is gated per URI.
1027
+ *
1028
+ * **The gap this closes is the one no other source can observe.** A document
1029
+ * the client watches, it hears about already. A file changed on disk, the
1030
+ * host's own filesystem watcher reports — and on a browser host, where the
1031
+ * workspace lives behind this head, an external change cannot happen at all.
1032
+ * What is left, and what nothing outside the server can see, is a document
1033
+ * rebuilt because something it DEPENDS ON changed: its file never changed, so
1034
+ * a filesystem watcher is silent by construction, and it has no subscriber, so
1035
+ * the update channel is silent by design. A consumer displaying data derived
1036
+ * from that document — a tree label, a decorator — otherwise goes stale with
1037
+ * no signal from any source, and repairs itself the moment someone opens the
1038
+ * file to investigate, which is what makes it expensive to diagnose later.
1039
+ *
1040
+ * Payload-free on purpose: URIs only, so a client re-reads what it displays
1041
+ * rather than being handed transfer documents it did not ask for. That is the
1042
+ * property the subscription map protects, and it is preserved here by
1043
+ * carrying no document rather than by gating the message.
1044
+ *
1045
+ * Quiet in the common case. Editing a document that an editor has open leaves
1046
+ * that URI watched and therefore out of this set, so a build with no
1047
+ * dependents produces nothing at all, and workspace initialisation produces
1048
+ * nothing because it does not build to this phase. The ceiling is a
1049
+ * whole-workspace rebuild at the subscription phase: one message, URIs only.
1050
+ *
1051
+ * The URI is canonicalised for the same reason the subscription map is keyed
1052
+ * that way, and is untested for the same reason as its twin in
1053
+ * {@link dispatchDeleteEvents}: the builder reports URIs out of its own
1054
+ * store, so a non-canonical one cannot be produced without a fixture
1055
+ * asserting a shape the real system never emits.
1056
+ */
1057
+ protected dispatchBuiltEvent(built: readonly LangiumDocument[]): void {
1058
+ const uris = built.map(document => this.canonicalKey(document.uri.toString())).filter(uri => !this.subscriptions.has(uri));
1059
+ if (uris.length === 0) {
1060
+ return;
1061
+ }
1062
+ this.tracer.debug(`Emit onDocumentsBuilt: ${uris.length} unwatched document(s)`);
1063
+ this.clientProxy.onDocumentsBuilt({ uris });
970
1064
  }
971
1065
 
972
1066
  /**
@@ -1004,6 +1098,56 @@ export class DataServer<
1004
1098
  );
1005
1099
  }
1006
1100
 
1101
+ /**
1102
+ * Fan out a deletion for each removed URI, to EVERY client on the connection
1103
+ * rather than only to the ones watching that URI.
1104
+ *
1105
+ * **Deliberately ungated, where {@link dispatchPhaseEvent} and
1106
+ * {@link dispatchSaveEvent} are gated.** Those two carry a built document and
1107
+ * fire on every build, so the subscription map is what keeps bandwidth
1108
+ * proportional to what a client asked for. A deletion is neither: it is one
1109
+ * URI, it is rare, and it reports the workspace's STRUCTURE rather than a
1110
+ * document's content. Gating it forces any consumer that displays the
1111
+ * workspace — a model tree, a file decorator — to learn about disappearances
1112
+ * from a filesystem watcher instead, which a client hosted in a browser has
1113
+ * no way to run: its workspace lives behind the head. A client that does not
1114
+ * care filters on the URI, which costs it a comparison.
1115
+ *
1116
+ * The precedent is the last-close revert broadcast (see
1117
+ * {@link pendingRevertBroadcasts}), where the same judgement was already made
1118
+ * in the other direction: a transition that matters enough is delivered
1119
+ * without a subscription.
1120
+ *
1121
+ * Runs from the `DocumentBuilder.onUpdate` listener rather than from a phase
1122
+ * listener, which is the only place a deletion is observable: `update`
1123
+ * removes the document before deriving the rebuild set, so it is never built.
1124
+ * That ordering also puts this notification ahead of the `'rebuilt'` events
1125
+ * for the dependents whose references the deletion just broke.
1126
+ *
1127
+ * The `deleted` list arrives already expanded to concrete document URIs —
1128
+ * `deleteDocuments` resolves a directory URI to the documents beneath it — so
1129
+ * no caller has to handle a directory here.
1130
+ *
1131
+ * Per-URI derived state is dropped for the same reason it is dropped
1132
+ * anywhere: it describes a document that no longer exists.
1133
+ * {@link lastEmittedFingerprint} in particular can outlive its subscriptions
1134
+ * through the last-close revert path — left behind, it is adopted as the
1135
+ * baseline by the next {@link watchModelDocument} and suppresses the first
1136
+ * emit after the file returns with its previous content.
1137
+ *
1138
+ * The subscription itself SURVIVES: a recreated file resumes delivering to
1139
+ * the same watchers with no re-subscription, and a client that answers the
1140
+ * deletion by closing releases the watch through `closeModelDocument` anyway.
1141
+ */
1142
+ protected dispatchDeleteEvents(deleted: readonly URI[]): void {
1143
+ for (const removed of deleted) {
1144
+ const uri = this.canonicalKey(removed.toString());
1145
+ this.lastEmittedFingerprint.delete(uri);
1146
+ this.pendingRevertBroadcasts.delete(uri);
1147
+ this.clientProxy.onDocumentDeleted({ uri });
1148
+ }
1149
+ }
1150
+
1007
1151
  /** Fan out a save event for the document's URI, gated by the subscription map. */
1008
1152
  protected dispatchSaveEvent(event: ClientTextDocumentChangeEvent<TextDocument>): void {
1009
1153
  // The save event arrives under the CLIENT URI the text store keys by (e.g. a
@@ -1122,26 +1266,19 @@ export class DataServer<
1122
1266
  /**
1123
1267
  * Discriminate the reason for a phase-event-driven update notification.
1124
1268
  * Uses the most recent `DocumentBuilder.onUpdate` snapshot:
1125
- * - URI in the `deleted` list → `'deleted'`
1126
1269
  * - URI in the `changed` list → `'changed'` (the URI was passed to
1127
1270
  * `documentBuilder.update(changed, deleted)`, which spans `didChange`
1128
1271
  * text-document events and programmatic `update([uri], [])` calls).
1129
1272
  * - Otherwise → `'rebuilt'` (cascade re-derivation: this URI was rebuilt
1130
1273
  * because something it depends on changed; its own text wasn't flagged).
1131
1274
  *
1132
- * `'saved'` is NOT emitted from this code path — saves take the dedicated
1133
- * `DataClientProtocol.onDocumentSaved` channel; adopters that want a
1134
- * unified update stream synthesise `'saved'` in their bridge layer.
1275
+ * `'saved'` is NOT emitted here — saves take the dedicated
1276
+ * `DataClientProtocol.onDocumentSaved` channel, and adopters wanting a
1277
+ * unified stream synthesise it in their bridge layer. A deletion is not a
1278
+ * reason at all; see {@link dispatchDeleteEvents}.
1135
1279
  */
1136
1280
  protected resolveUpdateReason(uri: URI): TransferDocumentUpdatedEvent<TTransfer, TDiagnostic>['reason'] {
1137
- const last = this.lastBuildUpdate;
1138
- if (last?.deleted.some(deleted => UriUtils.equals(deleted, uri))) {
1139
- return 'deleted';
1140
- }
1141
- if (last?.changed.some(changed => UriUtils.equals(changed, uri))) {
1142
- return 'changed';
1143
- }
1144
- return 'rebuilt';
1281
+ return this.lastBuildUpdate?.changed.some(changed => UriUtils.equals(changed, uri)) ? 'changed' : 'rebuilt';
1145
1282
  }
1146
1283
 
1147
1284
  /**
@@ -1243,7 +1380,7 @@ export class DataServer<
1243
1380
 
1244
1381
  async stopProfiling(args: StopProfilingArgs): Promise<string> {
1245
1382
  if (!this.activeProfile) {
1246
- throw new Error('No profiling capture is active; call startProfiling first.');
1383
+ throw noActiveProfileError();
1247
1384
  }
1248
1385
  const capture = this.activeProfile;
1249
1386
  this.activeProfile = undefined;
@@ -0,0 +1,19 @@
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
+ * Every user-facing message `@hydranium/data-server` raises.
12
+ *
13
+ * Declarations stay beside their call sites and are re-exported here, so this is
14
+ * enumeration rather than centralization: a code's package segment has to name
15
+ * the package that raises it. The English is a fallback, not a contract; the
16
+ * code is the contract, and renaming one is a breaking change.
17
+ */
18
+
19
+ export { NO_ACTIVE_PROFILE, NO_ACTIVE_PROFILE_CODE, noActiveProfileError } from '../data-server.js';
@@ -14,7 +14,9 @@ import {
14
14
  type DataClientProtocol,
15
15
  type DataServerProtocol,
16
16
  type ProjectsChangedEvent,
17
+ type TransferDocumentDeletedEvent,
17
18
  type TransferDocumentSavedEvent,
19
+ type TransferDocumentsBuiltEvent,
18
20
  type TransferDocumentUpdatedEvent
19
21
  } from '@hydranium/protocol/data';
20
22
  import { type Harness, makeCapturingDataClient } from '@hydranium/protocol/testing';
@@ -87,6 +89,10 @@ export interface DataServerHarness<
87
89
  readonly events: ReadonlyArray<TransferDocumentUpdatedEvent<TTransfer, TDiagnostic>>;
88
90
  /** Captured `onDocumentSaved` events. */
89
91
  readonly saves: ReadonlyArray<TransferDocumentSavedEvent<TTransfer, TDiagnostic>>;
92
+ /** Captured `onDocumentDeleted` events. */
93
+ readonly deletions: ReadonlyArray<TransferDocumentDeletedEvent>;
94
+ /** Captured `onDocumentsBuilt` events, one per build that had unwatched documents. */
95
+ readonly builds: ReadonlyArray<TransferDocumentsBuiltEvent>;
90
96
  /** Captured `onProjectsChanged` events. */
91
97
  readonly projectsChanges: ReadonlyArray<ProjectsChangedEvent<TProject>>;
92
98
  /** Dispose the underlying duplex pair. Idempotent. */
@@ -115,7 +121,7 @@ export function makeDataServerHarness<
115
121
  const server = options.server(pair.left);
116
122
 
117
123
  // The capture half is the shared client double, not a local copy: the same
118
- // recording semantics (all three channels, an override replacing rather than
124
+ // recording semantics (every channel, an override replacing rather than
119
125
  // supplementing) then hold for a client-side suite that stands the double up
120
126
  // without a server, so an assertion learnt against one reads the same in the
121
127
  // other.
@@ -123,6 +129,8 @@ export function makeDataServerHarness<
123
129
  client: localClient,
124
130
  updates: events,
125
131
  saves,
132
+ deletions,
133
+ builds,
126
134
  projectsChanges
127
135
  } = makeCapturingDataClient<TTransfer, TDiagnostic, TProject>(options.client);
128
136
 
@@ -141,6 +149,8 @@ export function makeDataServerHarness<
141
149
  pair,
142
150
  events,
143
151
  saves,
152
+ deletions,
153
+ builds,
144
154
  projectsChanges,
145
155
  dispose: () => pair.dispose()
146
156
  };