diff --git a/package-lock.json b/package-lock.json index 888a7adf1..9ec58c701 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cldmv/slothlet", - "version": "2.3.0", + "version": "2.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cldmv/slothlet", - "version": "2.3.0", + "version": "2.3.1", "license": "Apache-2.0", "devDependencies": { "@eslint/css": "^0.10.0", diff --git a/package.json b/package.json index e6dfad9a8..bcee3e3c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cldmv/slothlet", - "version": "2.3.0", + "version": "2.3.1", "moduleVersions": { "lazy": "1.0.0", "eager": "1.0.0" diff --git a/src/lib/runtime/runtime.mjs b/src/lib/runtime/runtime.mjs index defb68190..563001b69 100644 --- a/src/lib/runtime/runtime.mjs +++ b/src/lib/runtime/runtime.mjs @@ -84,6 +84,9 @@ const EXCLUDED_CONSTRUCTORS = new Set([Object, Array, Promise, Date, RegExp, Err // Array of constructor functions for instanceof checks const EXCLUDED_INSTANCEOF_CLASSES = [ArrayBuffer, Map, Set, WeakMap, WeakSet]; +// Promise method names that need special context-preserving handling +const PROMISE_METHODS = new Set(["then", "catch", "finally"]); + /** * @function runtime_shouldWrapMethod * @internal @@ -253,6 +256,7 @@ function runtime_wrapClassInstance(instance, ctx, wrapFn, instanceCache) { export const makeWrapper = (ctx) => { const cache = new WeakMap(); const instanceCache = new WeakMap(); + const promiseMethodCache = new WeakMap(); // Memoize Promise method wrappers const wrap = (val) => { if (val == null || (typeof val !== "object" && typeof val !== "function")) return val; if (cache.has(val)) return cache.get(val); @@ -291,11 +295,66 @@ export const makeWrapper = (ctx) => { return result; }, get(target, prop, receiver) { - return wrap(Reflect.get(target, prop, receiver)); + const value = Reflect.get(target, prop, receiver); + + // Special handling for Promise methods to preserve prototype chain and context + // Support both native Promises and thenables (objects with callable 'then') + const isPromiseMethod = typeof value === "function" && PROMISE_METHODS.has(prop); + const isNativePromise = util.types.isPromise(target); + const hasThen = typeof target?.then === "function"; + + if (isPromiseMethod && (isNativePromise || hasThen)) { + // Memoize Promise method wrappers to preserve function identity + let targetMethodCache = promiseMethodCache.get(target); + if (!targetMethodCache) { + targetMethodCache = new Map(); + promiseMethodCache.set(target, targetMethodCache); + } + + if (targetMethodCache.has(prop)) { + return targetMethodCache.get(prop); + } + + const wrappedMethod = function (...args) { + // Wrap callback functions to preserve runtime context + const wrappedArgs = args.map((arg) => { + if (typeof arg === "function") { + return function (...callbackArgs) { + return runWithCtx(ctx, arg, undefined, callbackArgs); + }; + } + return arg; + }); + + // Use Reflect.apply for cross-realm safety and avoid overridden apply + const result = Reflect.apply(value, target, wrappedArgs); + // The result might be a new Promise that also needs context wrapping + return wrap(result); + }; + + targetMethodCache.set(prop, wrappedMethod); + return wrappedMethod; + } + + return wrap(value); + }, + set(target, prop, value, receiver) { + // Invalidate cached Promise method wrapper for this (target, prop) + const methodCache = promiseMethodCache.get(target); + if (methodCache && methodCache.has(prop)) { + methodCache.delete(prop); + } + return Reflect.set(target, prop, value, receiver); }, - set: Reflect.set, defineProperty: Reflect.defineProperty, - deleteProperty: Reflect.deleteProperty, + deleteProperty(target, prop) { + // Invalidate cached Promise method wrapper for this (target, prop) + const methodCache = promiseMethodCache.get(target); + if (methodCache && methodCache.has(prop)) { + methodCache.delete(prop); + } + return Reflect.deleteProperty(target, prop); + }, ownKeys: Reflect.ownKeys, getOwnPropertyDescriptor: Reflect.getOwnPropertyDescriptor, has: Reflect.has diff --git a/types/src/lib/runtime/runtime.d.mts.map b/types/src/lib/runtime/runtime.d.mts.map index 3f9e9bcb6..66e064285 100644 --- a/types/src/lib/runtime/runtime.d.mts.map +++ b/types/src/lib/runtime/runtime.d.mts.map @@ -1 +1 @@ -{"version":3,"file":"runtime.d.mts","sourceRoot":"","sources":["../../../../src/lib/runtime/runtime.mjs"],"names":[],"mappings":"AA0CO,gCAdI,MAAM,yBAEN,GAAG,gBAED,GAAG,CA6Bf;AAiBM,0BAZM,MAAM,GAAC,IAAI,CAY0B;AAsB3C,iCAjBI,MAAM,YAwDhB;AA4RD;;;;;;;;;;;;;GAaG;AACH,mBATU,WAAS,MAAM,CAS6B;AAEtD;;;;;;;;;;;;;GAaG;AACH,sBATU,MAAM,CAS4C;AAE5D;;;;;;;;;;;;;GAaG;AACH,wBATU,MAAM,CASgD"} \ No newline at end of file +{"version":3,"file":"runtime.d.mts","sourceRoot":"","sources":["../../../../src/lib/runtime/runtime.mjs"],"names":[],"mappings":"AA0CO,gCAdI,MAAM,yBAEN,GAAG,gBAED,GAAG,CA6Bf;AAiBM,0BAZM,MAAM,GAAC,IAAI,CAY0B;AA8K3C,iCAjBI,MAAM,YAoFhB;AA4RD;;;;;;;;;;;;;GAaG;AACH,mBATU,WAAS,MAAM,CAS6B;AAEtD;;;;;;;;;;;;;GAaG;AACH,sBATU,MAAM,CAS4C;AAE5D;;;;;;;;;;;;;GAaG;AACH,wBATU,MAAM,CASgD"} \ No newline at end of file