objectMutationLog.ts ×25

Frontier kind: Code frontier

unlabeled · c_c7ce2331f27c

145 tests · 4222 LOC · 23 files · introduces 0 tests · 249 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
25 ranges249 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
596 ranges4222 lines · 23 files · Browse complete extent
All tests (intent)
145 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 249 introduced LOC across 25 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/workbench/contrib/chat/common/model/objectMutationLog.ts 249 introduced LOC · 25 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- objectMutationLog.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { assertNever } from '../../../../../base/common/assert.js';
7 > import { VSBuffer } from '../../../../../base/common/buffer.js';
8 > import { isUndefinedOrNull } from '../../../../../base/common/types.js';
9 >
10 > /**
11 > * Updates an error's message and stack trace with a prefix. In V8 the stack
12 > * string starts with "ErrorName: message\n at …", so we rebuild the header
13 > * after mutating the message.
14 > */
15 function prefixError(e: Error, prefix: string): void {
16 e.message = prefix + e.message;
22 }
23 }
25 > /**
26 > * Prepends a path segment to an error as it unwinds through nested extract
27 > * calls. Each level adds its segment so the final message reads e.g.
28 > * `.responses[2].content: Cannot read property 'x' of undefined`.
29 > */
30 function rethrowWithPathSegment(e: unknown, segment: string | number): never {
31 if (e instanceof Error) {
36 throw e;
37 }
39 > /** IMPORTANT: `Key` comes first. Then we should sort in order of least->most expensive to diff */
40 > const enum TransformKind {
41 > Key,
42 > Primitive,
43 > Array,
44 > Object,
45 > }
46 >
47 > /** Schema entries sorted with key properties first */
48 > export type SchemaEntries = [string, Transform<unknown, unknown>][];
49 >
50 > interface TransformBase<TFrom, TTo> {
51 > readonly kind: TransformKind;
52 > /** Extracts the serializable value from the source object */
53 > extract(from: TFrom): TTo;
54 > }
55 >
56 > /** Transform for primitive values (keys and values) that can be compared for equality */
57 > export interface TransformValue<TFrom, TTo> extends TransformBase<TFrom, TTo> {
58 > readonly kind: TransformKind.Key | TransformKind.Primitive;
59 > /** Compares two serialized values for equality */
60 > equals(a: TTo, b: TTo): boolean;
61 > }
62 >
63 > /** Transform for arrays with an item schema */
64 > export interface TransformArray<TFrom, TTo> extends TransformBase<TFrom, TTo> {
65 > readonly kind: TransformKind.Array;
66 > /** The schema for array items */
67 > readonly itemSchema: TransformObject<unknown, unknown> | TransformValue<unknown, unknown>;
68 > }
69 >
70 > /** Transform for objects with child properties */
71 > export interface TransformObject<TFrom, TTo> extends TransformBase<TFrom, TTo> {
72 > readonly kind: TransformKind.Object;
73 > /** Schema entries sorted with Key properties first */
74 > readonly children: SchemaEntries;
75 > /** Checks if the object is sealed (won't change). */
76 > sealed?(obj: TTo, wasSerialized: boolean): boolean;
77 > }
78 >
79 > export type Transform<TFrom, TTo> =
80 > | TransformValue<TFrom, TTo>
81 > | TransformArray<TFrom, TTo>
82 > | TransformObject<TFrom, TTo>;
83 >
84 > export type Schema<TFrom, TTo> = {
85 > [K in keyof Required<TTo>]: Transform<TFrom, TTo[K]>
86 > };
87 >
88 > /**
89 > * A primitive that will be tracked and compared first. If this is changed, the entire
90 > * object is thrown out and re-stored.
91 > */
92 > export function key<T, R = T>(comparator?: (a: R, b: R) => boolean): TransformValue<T, R> {
93 return {
94 kind: TransformKind.Key,
97 };
98 }
100 > /** A value that will be tracked and replaced if the comparator is not equal. */
101 > export function value<T, R extends string | number | boolean | undefined>(): TransformValue<T, R>;
102 > export function value<T, R>(comparator: (a: R, b: R) => boolean): TransformValue<T, R>;
103 > export function value<T, R>(comparator?: (a: R, b: R) => boolean): TransformValue<T, R> {
104 return {
105 kind: TransformKind.Primitive,
119 };
120 }
122 > /** An array that will use the schema to compare items positionally. */
123 > export function array<T, R>(schema: TransformObject<T, R> | TransformValue<T, R>): TransformArray<readonly T[], R[]> {
124 return {
125 kind: TransformKind.Array,
134 };
135 }
137 > export interface ObjectOptions<R> {
138 > /**
139 > * Returns true if the object is sealed and will never change again.
140 > * When comparing two sealed objects, only key fields are compared
141 > * (to detect replacement), but other fields are not diffed.
142 > */
143 > sealed?: (obj: R, wasSerialized: boolean) => boolean;
144 > }
145 >
146 > /** An object schema. */
147 > export function object<T, R extends object>(schema: Schema<T, R>, options?: ObjectOptions<R>): TransformObject<T, R> {
148 // Sort entries with key properties first for fast key checking
149 const entries = (Object.entries(schema) as [string, Transform<T, R[keyof R]>][]).sort(([, a], [, b]) => a.kind - b.kind);
169 };
170 }
172 > /**
173 > * Defines a getter on the object to extract a value, compared with the given schema.
174 > * It should return the value that will get serialized in the resulting log file.
175 > */
176 > export function t<T, O, R>(getter: (obj: T) => O, schema: Transform<O, R>): Transform<T, R> {
177 return {
178 ...schema,
180 };
181 }
183 > /** Shortcut for t(fn, value()) */
184 > export function v<T, R extends string | number | boolean | undefined>(getter: (obj: T) => R): TransformValue<T, R>;
185 > export function v<T, R>(getter: (obj: T) => R, comparator: (a: R, b: R) => boolean): TransformValue<T, R>;
186 > export function v<T, R>(getter: (obj: T) => R, comparator?: (a: R, b: R) => boolean): TransformValue<T, R> {
187 const inner = value(comparator!);
188 return {
191 };
192 }
194 >
195 > const enum EntryKind {
196 > /** Initial complete object state, valid only as the first entry */
197 > Initial = 0,
198 > /** Property update */
199 > Set = 1,
200 > /** Array push/splice. */
201 > Push = 2,
202 > /** Delete a property */
203 > Delete = 3,
204 > }
205 >
206 > type ObjectPath = (string | number)[];
207 >
208 > type Entry =
209 > | { kind: EntryKind.Initial; v: unknown }
210 > /** Update a property of an object, replacing it entirely */
211 > | { kind: EntryKind.Set; k: ObjectPath; v: unknown }
212 > /** Delete a property of an object */
213 > | { kind: EntryKind.Delete; k: ObjectPath }
214 > /** Pushes 0 or more new entries to an array. If `i` is set, everything after that index is removed */
215 > | { kind: EntryKind.Push; k: ObjectPath; v?: unknown[]; i?: number };
216 >
217 > const LF = VSBuffer.fromString('\n');
218 >
219 > /**
220 > * Per-string cap (in UTF-16 code units, matching `string.length`) applied when
221 > * {@link stringifyEntryWithFallback} retries after `JSON.stringify` throws
222 > * `RangeError: Invalid string length` (V8's max string length is ~512 MiB on
223 > * 64-bit). Any single string longer than this is replaced with a marker on
224 > * retry. Generous so it triggers only on outliers.
225 > */
226 > export const PERSIST_ENTRY_MAX_STRING_CHARS = 1 * 1024 * 1024;
227 >
228 > /**
229 > * Total-size budget (sum of `string.length` for tracked strings, in UTF-16
230 > * code units) for the retry of {@link stringifyEntryWithFallback}. Once the
231 > * cumulative tracked size during serialization exceeds this, remaining values
232 > * are replaced with a marker.
233 > *
234 > * This is an approximation: JSON escaping, property keys, and non-string
235 > * payload are not counted, so the actual output may be moderately larger.
236 > * The cap is sized well under V8's max string length to leave ample headroom
237 > * for that overhead.
238 > */
239 > export const PERSIST_ENTRY_MAX_TOTAL_CHARS = 100 * 1024 * 1024;
240 >
241 > const TRUNCATION_MARKER_PREFIX = '[VS Code: value truncated for persistence';
242 > const TRUNCATION_MARKER_TOTAL = `${TRUNCATION_MARKER_PREFIX}; entry exceeded size budget]`;
243 >
244 > /**
245 > * Wraps `JSON.stringify(entry)` with a safety net for the V8 max-string-length
246 > * limit. The common path is a single `JSON.stringify` with zero overhead. If
247 > * stringification throws `RangeError` (the resulting JSON would exceed V8's
248 > * ~512 MiB max string length — see microsoft/vscode#308843), retry with a
249 > * replacer that truncates oversized strings. Extensions sometimes put very
250 > * large content (browser dumps, command output, …) into chat result metadata;
251 > * losing the tail of one such value is dramatically better than losing the
252 > * entire chat session.
253 > */
254 > export function stringifyEntryWithFallback(entry: unknown): string {
255 try {
256 return JSON.stringify(entry);
262 }
263 }
265 > /**
266 > * Deep-clones `value` through JSON with the same V8 max-string-length safety net
267 > * as {@link stringifyEntryWithFallback}. Exported for testing only.
268 > */
269 > export function deepCloneWithFallback<T>(value: T): T {
270 return JSON.parse(stringifyEntryWithFallback(value)) as T;
271 }
273 > /**
274 > * Exported for testing only. Builds the stateful `JSON.stringify` replacer
275 > * used by {@link stringifyEntryWithFallback} on its retry path.
276 > *
277 > * Sizes are tracked in UTF-16 code units (`string.length`); JSON escaping,
278 > * property keys, and non-string payload are not counted.
279 > */
280 > export function makeTruncatingReplacer(maxStringChars: number, maxTotalChars: number): (key: string, value: unknown) => unknown {
281 let total = 0;
282 return (_key, val) => {
297 };
298 }
300 > /**
301 > * An implementation of an append-based mutation logger. Given a `Transform`
302 > * definition of an object, it can recreate it from a file on disk. It is
303 > * then stateful, and given a `write` call it can update the log in a minimal
304 > * way.
305 > */
306 > export class ObjectMutationLog<TFrom, TTo> {
307 > private _previous: TTo | undefined;
308 > private _entryCount = 0;
309 > private _hasPendingWrite = false;
310 > private _pendingPrevious: TTo | undefined;
311 > private _pendingEntryCount = 0;
312 >
313 > constructor(
314 private readonly _transform: Transform<TFrom, TTo>,
315 private readonly _compactAfterEntries = 512,
316 ) { }
318 > /**
319 > * Creates an initial log file from the given object.
320 > */
321 > createInitial(current: TFrom): VSBuffer {
322 return this.createInitialFromSerialized(this._transform.extract(current));
323 }
325 >
326 > /**
327 > * Creates an initial log file from the serialized object.
328 > *
329 > * Unlike {@link write}, this commits state immediately without requiring
330 > * {@link confirmWrite}. This is safe because the returned buffer contains
331 > * a self-contained `Initial` entry — if it fails to persist, no
332 > * incremental entries can be appended to a non-existent file.
333 > */
334 > createInitialFromSerialized(value: TTo): VSBuffer {
335 this._previous = value;
336 this._entryCount = 1;
339 return VSBuffer.fromString(stringifyEntryWithFallback(entry) + '\n');
340 }
342 > /**
343 > * Reads and reconstructs the state from a log file.
344 > */
345 > read(content: VSBuffer): TTo {
346 let state: unknown;
347 let lineCount = 0;
399 return state as TTo;
400 }
402 > /**
403 > * Writes updates to the log. Returns the operation type and data to write.
404 > * The caller **must** invoke {@link confirmWrite} after the data is
405 > * successfully persisted to commit the internal state. Without confirmation,
406 > * the next write is computed against the last confirmed state, and will only
407 > * produce a full initial entry when no confirmed state exists, preventing
408 > * corrupted log files when a write fails.
409 > */
410 > write(current: TFrom): { op: 'append' | 'replace'; data: VSBuffer } {
411 const currentValue = this._transform.extract(current);
412
450 return { op: 'append', data: VSBuffer.fromString(data) };
451 }
453 > /**
454 > * Commits the internal state after a successful write to disk.
455 > */
456 > confirmWrite(): void {
457 if (this._hasPendingWrite) {
458 this._previous = this._pendingPrevious;
461 }
462 }
464 > private _clearPending(): void {
465 this._hasPendingWrite = false;
466 this._pendingPrevious = undefined;
467 this._pendingEntryCount = 0;
468 }
470 > private _applySet(state: unknown, path: ObjectPath, value: unknown): void {
471 if (path.length === 0) {
472 return; // Root replacement handled by caller
480 current[path[path.length - 1]] = value;
481 }
483 > private _applyPush(state: unknown, path: ObjectPath, values: unknown[] | undefined, startIndex: number | undefined): void {
484 let current = state as Record<string | number, unknown>;
485 for (let i = 0; i < path.length - 1; i++) {
500 current[arrayKey] = arr;
501 }
503 > private _diff<T, R>(
504 transform: Transform<T, R>,
505 path: ObjectPath,
531 }
532 }
534 > private _diffObject(
535 children: SchemaEntries,
536 path: ObjectPath,
571 }
572 }
574 > private _diffArray<T, R>(
575 transform: TransformArray<T, R>,
576 path: ObjectPath,
644 }
645 }
647 > private _hasKeyMismatch(children: SchemaEntries, prev: unknown, curr: unknown): boolean {
648 const prevObj = prev as Record<string, unknown> | undefined;
649 const currObj = curr as Record<string, unknown>;