Skip to content

Commit d789eda

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 5a84f21 commit d789eda

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { type DurableObjectState, type ExecutionContext } from '@cloudflare/workers-types';
2+
3+
type ContextType = ExecutionContext | DurableObjectState;
4+
type OverridesStore = Map<string | symbol, (...args: unknown[]) => unknown>;
5+
6+
/**
7+
* Creates a new copy of the given execution context, optionally overriding methods.
8+
*
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.
11+
*/
12+
export function copyExecutionContext<T extends ContextType>(ctx: T): T {
13+
if (!ctx) return ctx;
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);
27+
}
28+
29+
/**
30+
* Creates a property descriptor for a given method on a context object, enabling custom getter and setter behavior.
31+
*
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.
36+
*/
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+
},
45+
46+
get: () => {
47+
if (store.has(method)) return store.get(method);
48+
return Reflect.get(ctx, method).bind(ctx);
49+
},
50+
};
51+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { type Mocked, describe, expect, it, vi } from 'vitest';
2+
import { copyExecutionContext } from '../src/utils/copyExecutionContext';
3+
4+
describe('Copy of the execution context', () => {
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;
18+
const copy = copyExecutionContext(context);
19+
copy[method] = vi.fn();
20+
expect(context[method]).not.toBe(copy[method]);
21+
});
22+
23+
it('Overridden method was called', async () => {
24+
const context = {
25+
[method]: vi.fn(),
26+
} as any;
27+
const copy = copyExecutionContext(context);
28+
const overridden = vi.fn();
29+
copy[method] = overridden;
30+
copy[method]();
31+
expect(overridden).toBeCalled();
32+
expect(context[method]).not.toBeCalled();
33+
});
34+
});
35+
36+
it('No side effects', async () => {
37+
const context = makeExecutionContextMock();
38+
expect(() => copyExecutionContext(Object.freeze(context))).not.toThrow(
39+
/Cannot define property \w+, object is not extensible/,
40+
);
41+
});
42+
it('Respects symbols', async () => {
43+
const s = Symbol('test');
44+
const context = makeExecutionContextMock<ExecutionContext & { [s]: unknown }>();
45+
context[s] = {};
46+
const copy = copyExecutionContext(context);
47+
expect(copy[s]).toBe(context[s]);
48+
});
49+
});
50+
51+
function makeExecutionContextMock<T extends ExecutionContext>() {
52+
return {
53+
waitUntil: vi.fn(),
54+
passThroughOnException: vi.fn(),
55+
} as unknown as Mocked<T>;
56+
}

0 commit comments

Comments
 (0)