Skip to content

Commit 5dfc197

Browse files
author
cod1k
committed
Refactor copyExecutionContext for improved flexibility
Reworked `copyExecutionContext` to use a dynamic property descriptor approach, enabling method overrides without altering the original object. Expanded test cases to cover additional methods and verify override behavior.
1 parent 04d2fbf commit 5dfc197

File tree

2 files changed

+62
-49
lines changed

2 files changed

+62
-49
lines changed
Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,51 @@
11
import { type DurableObjectState, type ExecutionContext } from '@cloudflare/workers-types';
22

3-
const kBound = Symbol.for('kBound');
4-
5-
const defaultPropertyOptions: PropertyDescriptor = {
6-
enumerable: true,
7-
configurable: true,
8-
writable: true,
9-
};
3+
type ContextType = ExecutionContext | DurableObjectState;
4+
type OverridesStore = Map<string | symbol, (...args: unknown[]) => unknown>;
105

116
/**
12-
* Clones the given execution context by creating a shallow copy while ensuring the binding of specific methods.
7+
* Creates a new copy of the given execution context, optionally overriding methods.
138
*
14-
* @param {ExecutionContext|DurableObjectState|void} ctx - The execution context to clone. Can be void.
15-
* @return {ExecutionContext|DurableObjectState|void} A cloned execution context with bound methods, or the original void value if no context was provided.
9+
* @param {ContextType|void} ctx - The execution context to be copied. Can be of type `ContextType` or `void`.
10+
* @return {ContextType|void} A new execution context with the same properties and overridden methods if applicable.
1611
*/
17-
export function copyExecutionContext<T extends ExecutionContext | DurableObjectState>(ctx: T): T {
12+
export function copyExecutionContext<T extends ContextType>(ctx: T): T {
1813
if (!ctx) return ctx;
19-
return Object.create(ctx, {
20-
waitUntil: { ...defaultPropertyOptions, value: copyAndBindMethod(ctx, 'waitUntil') },
21-
...('passThroughOnException' in ctx && {
22-
passThroughOnException: { ...defaultPropertyOptions, value: copyAndBindMethod(ctx, 'passThroughOnException') },
23-
}),
24-
});
14+
15+
const overrides: OverridesStore = new Map();
16+
const contextPrototype = Object.getPrototypeOf(ctx);
17+
const descriptors = Object.getOwnPropertyNames(contextPrototype).reduce((prevDescriptors, methodName) => {
18+
if (methodName === 'constructor') return prevDescriptors;
19+
const pd = makeMethodDescriptor(overrides, ctx, methodName as keyof ContextType);
20+
return {
21+
...prevDescriptors,
22+
[methodName]: pd,
23+
};
24+
}, {});
25+
26+
return Object.create(ctx, descriptors);
2527
}
2628

2729
/**
28-
* Copies a method from the given object and ensures the copied method remains bound to the original object's context.
30+
* Creates a property descriptor for a given method on a context object, enabling custom getter and setter behavior.
2931
*
30-
* @param {object} obj - The object containing the method to be copied and bound.
31-
* @param {string|symbol} method - The key of the method within the object to be copied and bound.
32-
* @return {Function} - The copied and bound method, or the original property if it is not a function.
32+
* @param store - The OverridesStore instance used to manage method overrides.
33+
* @param ctx - The context object from which the method originates.
34+
* @param method - The key of the method on the context object to create a descriptor for.
35+
* @return A property descriptor with custom getter and setter functionalities for the specified method.
3336
*/
34-
function copyAndBindMethod<T, K extends keyof T>(obj: T, method: K): T[K] {
35-
const methodImpl = obj[method];
36-
if (typeof methodImpl !== 'function') return methodImpl;
37-
if ((methodImpl as T[K] & { [kBound]?: boolean })[kBound]) return methodImpl;
38-
const bound = methodImpl.bind(obj);
37+
function makeMethodDescriptor(store: OverridesStore, ctx: ContextType, method: keyof ContextType): PropertyDescriptor {
38+
return {
39+
configurable: true,
40+
enumerable: true,
41+
set: newValue => {
42+
store.set(method, newValue);
43+
return true;
44+
},
3945

40-
return new Proxy(bound, {
41-
get: (target, prop, receiver) => {
42-
if (kBound === prop) return true;
43-
if ('bind' === prop) return () => receiver;
44-
return Reflect.get(target, prop, receiver);
46+
get: () => {
47+
if (store.has(method)) return store.get(method);
48+
return Reflect.get(ctx, method).bind(ctx);
4549
},
46-
});
50+
};
4751
}
Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,35 @@
1-
import { type ExecutionContext } from '@cloudflare/workers-types';
21
import { type Mocked, describe, expect, it, vi } from 'vitest';
32
import { copyExecutionContext } from '../src/utils/copyExecutionContext';
43

54
describe('Copy of the execution context', () => {
6-
describe.for<keyof ExecutionContext>(['waitUntil', 'passThroughOnException'])('%s', method => {
7-
it('Was not bound more than once', async () => {
8-
const context = makeExecutionContextMock();
5+
describe.for([
6+
'waitUntil',
7+
'passThroughOnException',
8+
'acceptWebSocket',
9+
'blockConcurrencyWhile',
10+
'getWebSockets',
11+
'arbitraryMethod',
12+
'anythingElse',
13+
])('%s', method => {
14+
it('Override without changing original', async () => {
15+
const context = {
16+
[method]: vi.fn(),
17+
} as any;
918
const copy = copyExecutionContext(context);
10-
const copy_of_copy = copyExecutionContext(copy);
11-
12-
expect(copy[method]).toBe(copy_of_copy[method]);
19+
copy[method] = vi.fn();
20+
expect(context[method]).not.toBe(copy[method]);
1321
});
14-
it('Copied method is bound to the original', async () => {
15-
const context = makeExecutionContextMock();
16-
const copy = copyExecutionContext(context);
1722

18-
expect(copy[method]()).toBe(context);
19-
});
20-
it('Copied method "rebind" prevention', async () => {
21-
const context = makeExecutionContextMock();
23+
it('Overridden method was called', async () => {
24+
const context = {
25+
[method]: vi.fn(),
26+
} as any;
2227
const copy = copyExecutionContext(context);
23-
expect(copy[method].bind('test')).toBe(copy[method]);
28+
const overridden = vi.fn();
29+
copy[method] = overridden;
30+
copy[method]();
31+
expect(overridden).toBeCalled();
32+
expect(context[method]).not.toBeCalled();
2433
});
2534
});
2635

@@ -41,7 +50,7 @@ describe('Copy of the execution context', () => {
4150

4251
function makeExecutionContextMock<T extends ExecutionContext>() {
4352
return {
44-
waitUntil: vi.fn().mockReturnThis(),
45-
passThroughOnException: vi.fn().mockReturnThis(),
53+
waitUntil: vi.fn(),
54+
passThroughOnException: vi.fn(),
4655
} as unknown as Mocked<T>;
4756
}

0 commit comments

Comments
 (0)