Files
n8n/packages/@n8n/expression-runtime/src/runtime/lazy-proxy.ts
T
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

184 lines
6.2 KiB
TypeScript

// ============================================================================
// Deep Lazy Proxy System
// ============================================================================
// For more information about Proxies see
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
// ---------------------------------------------------------------------------
// Proxy registry — used only for testing/introspection, not accessible from
// expression code. Avoids shadowing user data keys like __isProxy / __path.
// ---------------------------------------------------------------------------
const proxyPaths = new WeakMap<object, string[]>();
/** Returns true if `obj` is a deep lazy proxy created by createDeepLazyProxy. */
export function isLazyProxy(obj: unknown): boolean {
return typeof obj === 'object' && obj !== null && proxyPaths.has(obj as object);
}
/** Returns the basePath the proxy was created with, or undefined if not a proxy. */
export function getProxyPath(obj: object): string[] | undefined {
return proxyPaths.get(obj);
}
/**
* Creates a deep lazy-loading proxy for workflow data.
*
* This proxy system enables on-demand loading of nested properties across
* the isolate boundary using metadata-driven callbacks.
*
* Pattern:
* 1. When property accessed: Call __getValueAtPath([path]) to get metadata
* 2. Metadata indicates type: primitive, object, array, or function
* 3. For objects/arrays: Create nested proxy for lazy loading
* 4. For functions: Create wrapper that calls __callFunctionAtPath
* 5. Cache all fetched values in target to avoid repeated callbacks
*
* @param basePath - Current path in object tree (e.g., ['$json', 'user'])
* @returns Proxy object with lazy loading behavior
*/
export function createDeepLazyProxy(basePath: string[] = []): any {
const proxy = new Proxy({} as Record<string, unknown>, {
get(target: any, prop: string | symbol): unknown {
// Handle Symbol properties - return undefined
// Symbols like Symbol.toStringTag are accessed internally
// We can't transfer Symbols via isolated-vm
if (typeof prop === 'symbol') {
return undefined;
}
// Handle common Object.prototype methods within isolate
// Don't fetch from parent to avoid native function transfer issues
if (prop === 'toString') {
return function () {
return '[object Object]';
};
}
if (prop === 'valueOf') {
return function () {
return target;
};
}
// Check cache - if already fetched, return cached value
if (prop in target) {
return target[prop];
}
// Build path for this property
const path = [...basePath, prop as string];
// Call back to parent to get metadata/value
// Note: __getValueAtPath is an ivm.Reference set by bridge
const value = globalThis.__getValueAtPath.applySync(null, [path], {
arguments: { copy: true },
result: { copy: true },
});
// Handle undefined/null - cache and return
if (value === undefined || value === null) {
target[prop] = value;
return value;
}
// Handle functions - metadata: { __isFunction: true, __name: string }
if (value && typeof value === 'object' && value.__isFunction) {
// Create function wrapper that calls back to parent
target[prop] = function (...args: any[]) {
return globalThis.__callFunctionAtPath.applySync(null, [path, ...args], {
arguments: { copy: true },
result: { copy: true },
});
};
return target[prop];
}
// Handle arrays - metadata: { __isArray: true, __length: number }
if (value && typeof value === 'object' && value.__isArray) {
const arrayProxy = new Proxy([] as any[], {
get(arrTarget: any, arrProp: string | symbol): unknown {
// Symbols can't be transferred via isolated-vm; return undefined
if (typeof arrProp === 'symbol') {
return undefined;
}
// Handle array length
if (arrProp === 'length') {
return value.__length;
}
// Handle numeric index
const index = Number(arrProp);
if (!isNaN(index) && index >= 0) {
// Check cache
if (!(arrProp in arrTarget)) {
// Fetch element from parent
const element = globalThis.__getArrayElement.applySync(null, [path, index], {
arguments: { copy: true },
result: { copy: true },
});
// Handle element metadata (arrays and objects need proxies)
if (element && typeof element === 'object' && element.__isArray) {
const elementPath = [...path, String(index)];
arrTarget[arrProp] = createDeepLazyProxy(elementPath);
} else if (element && typeof element === 'object' && element.__isObject) {
// Object metadata: create nested proxy
const elementPath = [...path, String(index)];
arrTarget[arrProp] = createDeepLazyProxy(elementPath);
} else {
// Primitive element
arrTarget[arrProp] = element;
}
}
return arrTarget[arrProp];
}
// Array methods (map, filter, etc.)
// Note: These require full array - limitation for now
return arrTarget[arrProp];
},
});
target[prop] = arrayProxy;
return target[prop];
}
// Handle objects - metadata: { __isObject: true, __keys: string[] }
if (value && typeof value === 'object' && value.__isObject) {
// Create nested proxy for recursive lazy loading
target[prop] = createDeepLazyProxy(path);
return target[prop];
}
// Primitive value - cache and return
target[prop] = value;
return value;
},
has(target: any, prop: string | symbol): boolean {
// Implement 'in' operator support
// Example: '$json' in data
if (typeof prop === 'symbol') return false;
// Check cache first
if (prop in target) {
return true;
}
// Build path and check existence via callback
const path = [...basePath, prop as string];
const value = globalThis.__getValueAtPath.applySync(null, [path], {
arguments: { copy: true },
result: { copy: true },
});
// Property exists if value is not undefined
// Note: null values mean property exists but is null
return value !== undefined;
},
});
proxyPaths.set(proxy, basePath);
return proxy;
}