@openrewrite/rewrite 8.92.3 → 8.92.5

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/src/rpc/queue.ts CHANGED
@@ -19,6 +19,8 @@ import {saveTrace, trace} from "./trace";
19
19
  import {updateIfChanged} from "../util";
20
20
  import {isRef, ReferenceMap} from "../reference";
21
21
 
22
+ const EMPTY_LIST: readonly never[] = Object.freeze([]);
23
+
22
24
  /**
23
25
  * Interface representing an RPC codec that defines methods
24
26
  * for sending and receiving objects in an RPC communication.
@@ -316,18 +318,71 @@ export class RpcSendQueue {
316
318
  }
317
319
  }
318
320
 
321
+ /**
322
+ * Collapses repeated strings decoded from RPC messages to a single instance. Shared across every
323
+ * {@link RpcReceiveQueue} of a connection so the same discriminators, enum values and whitespace
324
+ * are deduplicated across the whole run rather than only within one received object.
325
+ */
326
+ export class StringInternTable {
327
+ private readonly strings = new Map<string, string>();
328
+
329
+ constructor(private readonly maxEntries = 1 << 16,
330
+ private readonly maxValueLength = 200) {
331
+ }
332
+
333
+ /**
334
+ * Interns a `kind`/`valueType` discriminator. This set is bounded by the number of LST node
335
+ * types, so it is always interned and never subject to the cap.
336
+ */
337
+ internType(value: string): string {
338
+ const existing = this.strings.get(value);
339
+ if (existing !== undefined) {
340
+ return existing;
341
+ }
342
+ this.strings.set(value, value);
343
+ return value;
344
+ }
345
+
346
+ /**
347
+ * Interns a scalar string value, which is unbounded in principle. Only short strings are
348
+ * interned (whitespace and short tokens dominate the duplication), and the table stops growing
349
+ * at a cap so it can never turn into a leak; past either limit the original is returned as-is.
350
+ */
351
+ internValue(value: string): string {
352
+ if (value.length > this.maxValueLength) {
353
+ return value;
354
+ }
355
+ const existing = this.strings.get(value);
356
+ if (existing !== undefined) {
357
+ return existing;
358
+ }
359
+ if (this.strings.size >= this.maxEntries) {
360
+ return value;
361
+ }
362
+ this.strings.set(value, value);
363
+ return value;
364
+ }
365
+
366
+ clear(): void {
367
+ this.strings.clear();
368
+ }
369
+
370
+ get size(): number {
371
+ return this.strings.size;
372
+ }
373
+ }
374
+
319
375
  export class RpcReceiveQueue {
320
376
  private batch: RpcObjectData[] = [];
321
377
  private batchIndex = 0;
322
378
  private sinceYield = 0;
323
379
 
324
- private readonly internedStrings = new Map<string, string>();
325
-
326
380
  constructor(private readonly refs: Map<number, any>,
327
381
  private readonly sourceFileType: string | undefined,
328
382
  private readonly pull: () => Promise<RpcObjectData[]>,
329
383
  private readonly logger: rpc.Logger | undefined,
330
- private readonly trace: boolean) {
384
+ private readonly trace: boolean,
385
+ private readonly internedStrings: StringInternTable = new StringInternTable()) {
331
386
  }
332
387
 
333
388
  /**
@@ -371,10 +426,12 @@ export class RpcReceiveQueue {
371
426
  }
372
427
  return this.receive(markers, async m => {
373
428
  return saveTrace(this.trace, async () => {
374
- return updateIfChanged(markers!, {
375
- id: await this.receive(m.id),
376
- markers: (await this.receiveList(m.markers))!,
377
- });
429
+ const id = await this.receive(m.id);
430
+ const markerList = (await this.receiveList(m.markers))!;
431
+ if (markerList.length === 0) {
432
+ return emptyMarkers;
433
+ }
434
+ return updateIfChanged(markers!, {id, markers: markerList});
378
435
  })
379
436
  })
380
437
  }
@@ -435,8 +492,8 @@ export class RpcReceiveQueue {
435
492
  after = await codec.rpcReceive(before, this);
436
493
  } else if (message.value !== undefined) {
437
494
  after = message.valueType ?
438
- {kind: this.intern(message.valueType), ...message.value} :
439
- typeof message.value === "string" ? this.intern(message.value) : message.value;
495
+ {kind: this.internedStrings.internType(message.valueType), ...message.value} :
496
+ typeof message.value === "string" ? this.internedStrings.internValue(message.value) : message.value;
440
497
  } else if (message.state === RpcObjectState.ADD && message.valueType) {
441
498
  throw new Error(
442
499
  `No RPC codec registered on the TypeScript side for '${message.valueType}'. ` +
@@ -463,7 +520,7 @@ export class RpcReceiveQueue {
463
520
  before: T[] | undefined,
464
521
  onChange?: (before: T) => T | Promise<T | undefined> | undefined
465
522
  ): Promise<T[]> {
466
- return (await this.receiveList(before, onChange)) ?? [];
523
+ return (await this.receiveList(before, onChange)) ?? (EMPTY_LIST as unknown as T[]);
467
524
  }
468
525
 
469
526
  receiveList<T>(
@@ -501,6 +558,9 @@ export class RpcReceiveQueue {
501
558
  if (!positions) {
502
559
  throw new Error(`Expected positions array but got: ${JSON.stringify(d)}`);
503
560
  }
561
+ if (positions.length === 0) {
562
+ return EMPTY_LIST as unknown as T[];
563
+ }
504
564
  const after: T[] = new Array(positions.length);
505
565
  for (let i = 0; i < positions.length; i++) {
506
566
  const beforeIdx = positions[i];
@@ -519,16 +579,7 @@ export class RpcReceiveQueue {
519
579
  if (codec?.rpcNew) {
520
580
  return codec.rpcNew();
521
581
  }
522
- return {kind: this.intern(type)} as T;
523
- }
524
-
525
- private intern(value: string): string {
526
- const existing = this.internedStrings.get(value);
527
- if (existing !== undefined) {
528
- return existing;
529
- }
530
- this.internedStrings.set(value, value);
531
- return value;
582
+ return {kind: this.internedStrings.internType(type)} as T;
532
583
  }
533
584
  }
534
585
 
@@ -40,7 +40,7 @@ import {
40
40
  import {DataTableStore} from "../data-table";
41
41
  import {RecipeMarketplace} from "../marketplace";
42
42
  import {initializeMetricsCsv, setCacheSizeProvider} from "./request/metrics";
43
- import {RpcObjectData, RpcObjectState, RpcReceiveQueue} from "./queue";
43
+ import {RpcObjectData, RpcObjectState, RpcReceiveQueue, StringInternTable} from "./queue";
44
44
  import {RpcRecipe} from "./recipe";
45
45
  import {ExecutionContext} from "../execution";
46
46
  import {InstallRecipes, InstallRecipesResponse} from "./request/install-recipes";
@@ -68,6 +68,11 @@ export class RewriteRpc {
68
68
  readonly remoteRefs: Map<number, any> = new Map();
69
69
  readonly localRefs: ReferenceMap = new ReferenceMap();
70
70
 
71
+ // One table for the whole connection so repeated discriminators, enum values and whitespace
72
+ // decoded across the many getObject calls of a run collapse to a single string instance rather
73
+ // than one per received object.
74
+ private readonly internedStrings = new StringInternTable();
75
+
71
76
  // Ref high-water per source file, captured before it is first visited so an Evict rolls
72
77
  // back exactly the refs it introduced. `send` = localRefs snapshot, `recvMax` = max remoteRefs key.
73
78
  readonly refCheckpoints: Map<string, { send: number, recvMax: number }> = new Map();
@@ -158,6 +163,7 @@ export class RewriteRpc {
158
163
  this.remoteRefs.clear();
159
164
  this.localRefs.clear();
160
165
  this.refCheckpoints.clear();
166
+ this.internedStrings.clear();
161
167
  preparedRecipes.clear();
162
168
  this.remoteLanguages = undefined;
163
169
  };
@@ -262,7 +268,7 @@ export class RewriteRpc {
262
268
  nextPage = requestPage();
263
269
  }
264
270
  return page;
265
- }, this.logger, this.traceGetObject.receive);
271
+ }, this.logger, this.traceGetObject.receive, this.internedStrings);
266
272
 
267
273
  let remoteObject: P;
268
274
  try {
package/src/util.ts CHANGED
@@ -50,16 +50,66 @@ export function trimIndent(str: string | null | undefined): string {
50
50
  .trim();
51
51
  }
52
52
 
53
+ /**
54
+ * A compiled `(original, updates) => merged` builder that names every resulting property in an
55
+ * object literal. V8 sizes a literal's in-object storage to the properties it names, so the merged
56
+ * node keeps its fields inline; the `{...original, ...updates}` spread of a shared, megamorphic
57
+ * function instead lands in an out-of-object `system / PropertyArray` (millions of them across an
58
+ * RPC-received LST forest). Builders are keyed and cached by the pair of property-name lists so a
59
+ * given shape compiles once.
60
+ */
61
+ type MergeBuilder = (original: any, updates: any) => any;
62
+
63
+ const mergeBuilders = new Map<string, MergeBuilder>();
64
+
65
+ function mergeBuilderFor(originalKeys: string[], updateKeys: string[]): MergeBuilder | undefined {
66
+ const cacheKey = JSON.stringify(originalKeys) + JSON.stringify(updateKeys);
67
+ let builder = mergeBuilders.get(cacheKey);
68
+ if (builder === undefined) {
69
+ const fromUpdates = new Set(updateKeys);
70
+ const names = originalKeys.slice();
71
+ for (const key of updateKeys) {
72
+ if (!names.includes(key)) {
73
+ names.push(key);
74
+ }
75
+ }
76
+ const body = "return {" + names
77
+ .map(name => `${JSON.stringify(name)}:(${fromUpdates.has(name) ? "u" : "o"})[${JSON.stringify(name)}]`)
78
+ .join(",") + "};";
79
+ try {
80
+ builder = new Function("o", "u", body) as MergeBuilder;
81
+ } catch {
82
+ builder = (o, u) => ({...o, ...u});
83
+ }
84
+ mergeBuilders.set(cacheKey, builder);
85
+ }
86
+ return builder;
87
+ }
88
+
53
89
  /**
54
90
  * Helper function to create a new object only if any properties have changed.
55
91
  * Compares each property in updates with the original object.
56
92
  * Returns the original object if nothing changed, or a new object with updates applied.
57
93
  */
58
94
  export function updateIfChanged<O extends object>(original: O, updates: Partial<O>): O {
95
+ let changed = false;
59
96
  for (const key in updates) {
60
97
  if (updates[key] !== original[key]) {
61
- return { ...original, ...updates };
98
+ changed = true;
99
+ break;
100
+ }
101
+ }
102
+ if (!changed) {
103
+ return original;
104
+ }
105
+ // A merged node built through a shared spread overflows into a PropertyArray; a compiled
106
+ // literal builder keeps its fields inline. Symbol-keyed originals fall back to the spread,
107
+ // since only string keys survive the JSON-serialized builder body.
108
+ if (typeof original === "object" && Object.getOwnPropertySymbols(original).length === 0) {
109
+ const builder = mergeBuilderFor(Object.keys(original), Object.keys(updates as object));
110
+ if (builder !== undefined) {
111
+ return builder(original, updates);
62
112
  }
63
113
  }
64
- return original;
114
+ return {...original, ...updates};
65
115
  }