Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Binary file added local_deps/zod_4.3.6.tgz
Binary file not shown.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@
"@types/react": "18.2.79",
"@types/react-dom": "18.2.25",
"apache-arrow": "20.x - 21.x",
"globby/fast-glob": "3.3.3"
"globby/fast-glob": "3.3.3",
"zod": "file:./local_deps/zod_4.3.6.tgz"
},
"dependencies": {
"@a2a-js/sdk": "0.3.4",
Expand Down Expand Up @@ -1548,7 +1549,7 @@
"yaml": "2.8.3",
"yauzl": "3.2.1",
"yazl": "3.3.1",
"zod": "4.3.6",
"zod": "file:./local_deps/zod_4.3.6.tgz",
"zod-to-json-schema": "3.25.0"
},
"devDependencies": {
Expand Down
50 changes: 49 additions & 1 deletion src/platform/packages/shared/kbn-lazy-object/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,51 @@
# @kbn/lazy-object

Empty package generated by @kbn/generate
Utilities for deferring the construction of objects until they are first used,
so that modules that declare many objects at load-time don't pay the
construction cost (time and memory) for the ones that are never touched.

The package offers several variants that differ in granularity, caching, and
how they're authored:

## `lazyObject(obj)` + Babel plugin

Author object literals normally; a Babel plugin rewrites `lazyObject({ ... })`
call sites into `createLazyObjectFromFactories({ key: () => expr, ... })` so
each property is built on first access and cached forever. At runtime without
the Babel plugin this is an identity function.

Use when: you want ergonomic lazy fields on an object without changing source
style. Requires the Babel plugin in the build.

## `createLazyObjectFromFactories(factories)`

Runtime-only version of the above. Takes an object whose values are factory
functions and returns an object whose properties materialize on first read
(cached forever). No build-time support needed.

Use when: you want per-property laziness without the Babel plugin.

## `createLazyObjectFromAnnotations(obj)` + `annotateLazy(fn)`

Like `createLazyObjectFromFactories`, but you mark individual factory values
with `annotateLazy(...)` so an object can mix eagerly-defined fields with
lazily-computed ones.

Use when: only some fields of an object benefit from laziness.

## `lazyGCableObject(factory)`

Whole-object lazy with GC-reclaimable caching. Returns a Proxy that builds the
underlying object on first property access, caches it behind a `WeakRef`, and
lets the GC reclaim it once no consumer is holding a reference. The next access
rebuilds it.

Use when: you declare many similar objects at module-load time, expect only a
subset to be used, and want transiently-used ones to be collectible rather than
pinned for the process lifetime.

## Metrics

`getLazyObjectMetrics()` returns a `{ count, called }` snapshot for the
annotation-based variants (how many lazy keys were registered vs. materialized),
useful when evaluating whether laziness is paying off in a given module graph.
1 change: 1 addition & 0 deletions src/platform/packages/shared/kbn-lazy-object/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
export { createLazyObjectFromFactories } from './src/create_lazy_object_from_factories';
export { getLazyObjectMetrics } from './src/metrics';
export { lazyObject } from './src/lazy_object';
export { lazyGCableObject } from './src/lazy_gcable_object';
export {
createLazyObjectFromAnnotations,
annotateLazy,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { lazyGCableObject } from './lazy_gcable_object';

describe('lazyGCableObject', () => {
it('defers factory invocation until first property access', () => {
const factory = jest.fn(() => ({ value: 1 }));
const obj = lazyGCableObject(factory);

expect(factory).not.toHaveBeenCalled();

// Access a property to trigger materialization.
void obj.value;

expect(factory).toHaveBeenCalledTimes(1);
});

it('caches the materialized object while it is still reachable', () => {
const factory = jest.fn(() => ({ value: 1 }));
const obj = lazyGCableObject(factory);

// A single retained reference pins the instance, so all reads reuse it.
expect(obj.value).toBe(1);
expect(obj.value).toBe(1);
expect(obj.value).toBe(1);

expect(factory).toHaveBeenCalledTimes(1);
});

// The materialized object is held via `WeakRef`, so if the GC reclaims it
// between turns (under memory pressure), the next access rebuilds it from
// the factory. We verify the rebuild path by simulating cache eviction —
// V8's heuristics are not deterministic enough to assert collection itself.
it('rebuilds the object after the WeakRef is cleared', () => {
const factory = jest.fn(() => ({ value: 1 }));
const RealWeakRef = globalThis.WeakRef;

let onlyRef: WeakRef<object> | undefined;

class EvictableWeakRef<T extends object> {
private target: T | undefined;
constructor(target: T) {
this.target = target;
onlyRef = this as unknown as WeakRef<object>;
}
deref(): T | undefined {
return this.target;
}
evict(): void {
this.target = undefined;
}
}
(globalThis as { WeakRef: unknown }).WeakRef = EvictableWeakRef;

try {
const obj = lazyGCableObject(factory);

void obj.value;
expect(factory).toHaveBeenCalledTimes(1);

// Simulate the GC reclaiming the object.
(onlyRef as unknown as EvictableWeakRef<object>).evict();

void obj.value;
expect(factory).toHaveBeenCalledTimes(2);
} finally {
(globalThis as { WeakRef: unknown }).WeakRef = RealWeakRef;
}
});

it('binds function-valued properties to the materialized object', () => {
const obj = lazyGCableObject(() => ({
value: 42,
getValue() {
return this.value;
},
}));

// Destructuring loses the original `this`; the Proxy must bind the method
// to the materialized target so the call still resolves correctly.
const { getValue } = obj;
expect(getValue()).toBe(42);
expect(obj.getValue()).toBe(42);
});

it('supports the `in` operator via the has trap', () => {
const obj = lazyGCableObject(() => ({ a: 1, b: 2 }));

expect('a' in obj).toBe(true);
expect('missing' in obj).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

/**
* Wraps an object factory in a Proxy that defers construction of the underlying
* object until any property is first accessed. The materialized object is cached
* behind a `WeakRef`, so once no external consumer keeps it alive the GC is free
* to reclaim it; the next access rebuilds it from the factory. Function-valued
* properties are bound to the materialized object so methods observe a stable
* `this`.
*
* Intended for cases where many objects are declared at module-load time but
* only a subset is used at runtime. Unused entries stay as a single Proxy
* instance plus a closure, keeping baseline heap low; transiently-used entries
* are collectible after their last reference is dropped.
*
* Trade-off: if the same object is used repeatedly across GC cycles without
* callers retaining a reference, each cycle pays the cost of rebuilding it.
* Hold on to a reference (e.g. `const o = LazyThing; o.method(...)` inside a
* hot path) if that matters.
*
* Caveat: `instanceof` checks on the returned value will be `false` because the
* Proxy target is an empty object. Structural checks on properties of the
* materialized object work as expected.
*/
export function lazyGCableObject<T extends object>(factory: () => T): T {
let ref: WeakRef<T> | undefined;
const materialize = (): T => {
const cached = ref?.deref();
if (cached) {
return cached;
}
const fresh = factory();
ref = new WeakRef(fresh);
return fresh;
};

return new Proxy({} as T, {
get(_target, prop) {
const real = materialize() as unknown as Record<PropertyKey, unknown>;
const value = real[prop];
if (typeof value === 'function') {
return (value as (...args: unknown[]) => unknown).bind(real);
}
return value;
},
has(_target, prop) {
return prop in (materialize() as unknown as object);
},
});
}
Comment on lines +44 to +57
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low src/lazy_gcable_object.ts:44

The Proxy lacks a set trap, so assignments like lazyThing.prop = value write to the empty {} target rather than the materialized object. The value is stored on the proxy target but never propagated to the real object; subsequent reads return the unmodified value from the materialized object, causing the assignment to be silently lost.

-  return new Proxy({} as T, {
-    get(_target, prop) {
-      const real = materialize() as unknown as Record<PropertyKey, unknown>;
-      const value = real[prop];
-      if (typeof value === 'function') {
-        return (value as (...args: unknown[]) => unknown).bind(real);
-      }
-      return value;
-    },
-    has(_target, prop) {
-      return prop in (materialize() as unknown as object);
-    },
-  });
🤖 Copy this AI Prompt to have your agent fix this:
In file src/platform/packages/shared/kbn-lazy-object/src/lazy_gcable_object.ts around lines 44-57:

The `Proxy` lacks a `set` trap, so assignments like `lazyThing.prop = value` write to the empty `{}` target rather than the materialized object. The value is stored on the proxy target but never propagated to the real object; subsequent reads return the unmodified value from the materialized object, causing the assignment to be silently lost.

Evidence trail:
src/platform/packages/shared/kbn-lazy-object/src/lazy_gcable_object.ts lines 44-53 show the Proxy with only `get` and `has` traps, no `set` trap. JavaScript Proxy spec: without a `set` trap, assignments go to the target (empty `{}`) while the `get` trap reads from `materialize()`.

Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@
* version: not applicable
*/

import { z } from '@kbn/zod/v4';
import { z, lazySchema } from '@kbn/zod/v4';

export const PlatformErrorResponse = lazySchema(() =>
z.object({
statusCode: z.number().int(),
error: z.string(),
message: z.string(),
})
);
export type PlatformErrorResponse = z.infer<typeof PlatformErrorResponse>;
export const PlatformErrorResponse = z.object({
statusCode: z.number().int(),
error: z.string(),
message: z.string(),
});

export const SiemErrorResponse = lazySchema(() =>
z.object({
status_code: z.number().int(),
message: z.string(),
})
);
export type SiemErrorResponse = z.infer<typeof SiemErrorResponse>;
export const SiemErrorResponse = z.object({
status_code: z.number().int(),
message: z.string(),
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@
* version: not applicable
*/

import { z } from '@kbn/zod/v4';
import { z, lazySchema } from '@kbn/zod/v4';
import { isNonEmptyString } from '@kbn/zod-helpers/v4';

/**
* A string that does not contain only whitespace characters
*/
export const NonEmptyString = lazySchema(() => z.string().min(1).superRefine(isNonEmptyString));
export type NonEmptyString = z.infer<typeof NonEmptyString>;
export const NonEmptyString = z.string().min(1).superRefine(isNonEmptyString);

/**
* A universally unique identifier
*/
export const UUID = lazySchema(() => z.string().uuid());
export type UUID = z.infer<typeof UUID>;
export const UUID = z.string().uuid();
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

{{> disclaimer}}

import { z } from '@kbn/zod/v4';
import { z, lazySchema } from '@kbn/zod/v4';
{{#if useZodHelpers}}
import { isValidDateMath, isNonEmptyString, ArrayFromString, BooleanFromString } from '@kbn/zod-helpers/v4';
{{/if}}
Expand All @@ -30,18 +30,18 @@ import {
{{#if (isCircularSchema @key)}}
export type {{transformSchemaName @key}} = {{> ts_type}};
export type {{transformSchemaName @key}}Input = {{> ts_input_type }};
export const {{transformSchemaName @key}}: z.ZodType<{{transformSchemaName @key}}, {{transformSchemaName @key}}Input> = {{> zod_schema_item }};
export const {{transformSchemaName @key}}: z.ZodType<{{transformSchemaName @key}}, {{transformSchemaName @key}}Input> = lazySchema(() => {{> zod_schema_item }} as z.ZodType<{{transformSchemaName @key}}, {{transformSchemaName @key}}Input>);
{{else}}
{{#if (shouldCastExplicitly this)}}
{{!-- We need this temporary type to infer from it below, but in the end we want to export as a casted {{transformSchemaName @key}} type --}}
{{!-- error TS7056: The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed. --}}
export const {{transformSchemaName @key}}Internal = {{> zod_schema_item}};
export const {{transformSchemaName @key}}Internal = lazySchema(() => {{> zod_schema_item}});

export type {{transformSchemaName @key}} = z.infer<typeof {{transformSchemaName @key}}Internal>;
export const {{transformSchemaName @key}} = {{transformSchemaName @key}}Internal as z.ZodType<{{transformSchemaName @key}}>;
{{else}}
export const {{transformSchemaName @key}} = lazySchema(() => {{> zod_schema_item}});
export type {{transformSchemaName @key}} = z.infer<typeof {{transformSchemaName @key}}>;
export const {{transformSchemaName @key}} = {{> zod_schema_item}};
{{/if }}
{{/if}}
{{#if enum}}
Expand All @@ -60,8 +60,8 @@ export const {{transformSchemaName @key}}Enum = {{transformSchemaName @key}}.enu
* {{{requestQuery.description}}}
*/
{{/if}}
export const {{operationId}}RequestQuery = lazySchema(() => {{> zod_query_item requestQuery }});
export type {{operationId}}RequestQuery = z.infer<typeof {{operationId}}RequestQuery>;
export const {{operationId}}RequestQuery = {{> zod_query_item requestQuery }};
export type {{operationId}}RequestQueryInput = z.input<typeof {{operationId}}RequestQuery>;
{{/if}}

Expand All @@ -71,8 +71,8 @@ export type {{operationId}}RequestQueryInput = z.input<typeof {{operationId}}Req
* {{{requestParams.description}}}
*/
{{/if}}
export const {{operationId}}RequestParams = lazySchema(() => {{> zod_schema_item requestParams }});
export type {{operationId}}RequestParams = z.infer<typeof {{operationId}}RequestParams>;
export const {{operationId}}RequestParams = {{> zod_schema_item requestParams }};
export type {{operationId}}RequestParamsInput = z.input<typeof {{operationId}}RequestParams>;
{{/if}}

Expand All @@ -82,8 +82,8 @@ export type {{operationId}}RequestParamsInput = z.input<typeof {{operationId}}Re
* {{{requestBody.description}}}
*/
{{/if}}
export const {{operationId}}RequestBody = lazySchema(() => {{> zod_schema_item requestBody }});
export type {{operationId}}RequestBody = z.infer<typeof {{operationId}}RequestBody>;
export const {{operationId}}RequestBody = {{> zod_schema_item requestBody }};
export type {{operationId}}RequestBodyInput = z.input<typeof {{operationId}}RequestBody>;
{{/if}}

Expand All @@ -93,7 +93,7 @@ export type {{operationId}}RequestBodyInput = z.input<typeof {{operationId}}Requ
* {{{response.description}}}
*/
{{/if}}
export const {{operationId}}Response = lazySchema(() => {{> zod_schema_item response }});
export type {{operationId}}Response = z.infer<typeof {{operationId}}Response>;
export const {{operationId}}Response = {{> zod_schema_item response }};
{{/if}}
{{/each}}
1 change: 1 addition & 0 deletions src/platform/packages/shared/kbn-zod/v4/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@

export * from 'zod/v4';
export { isZod } from './util';
export { lazySchema } from './lazy_schema';
Loading
Loading