Proxy Decorator Pattern
Proxy Decorator Pattern
The proxy decorator pattern creates a wrapper object that controls access to another object, intercepting method calls to add behavior like lazy initialization, logging, or access control.
The Proxy
implements the same interface as the target (dynamically without having to write boilerplate code), making it transparent to clients.
Note: Use libraries instead of self implementation
There are caveats to handle proxy pattern appropriately that would warrant looking for libraries if proxy pattern is needed.
Like having appropriate handling for
if (prop === 'then' || prop === 'toJSON')
Simple Examples:
GT-Sandbox-Snapshot: In this example, the proxy defers object creation until first use—method calls wait for the real object to become available, then forward automatically once it's ready.
Code
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Factory that creates a proxy wrapping deferred initialization
function createDeferredClient(clientPromise: Promise<ApiClient>): ApiClient {
let resolvedClient: ApiClient | undefined;
// Cache the resolved client
clientPromise
.then(c => resolvedClient = c);
return new Proxy({} as ApiClient, {
get: (_, prop: keyof ApiClient) => {
return async (...args: any[]) => {
let client;
if (resolvedClient) {
client = resolvedClient;
} else {
myLog("In-Proxy", "client is NOT yet defined awaiting on client Promise");
client = await clientPromise;
}
return (client[prop] as any)(...args);
};
}
});
}
describe('Proxy Pattern - Deferred Client', () => {
it('should defer method calls until client is ready (factory approach)', async () => {
console.log("");
myLog("In-Test", "should defer method calls until client is ready (factory approach)...")
// Setup: Create a promise we control
// [Deferred Promise Resolution Pattern](http://www.glassthought.com/notes/3twym7huaodrb22qva8d4lf)
let resolveClient: (client: ApiClient) => void;
const clientPromise = new Promise<ApiClient>(resolve => {
resolveClient = resolve;
});
// Create deferred client directly (no separate factory call)
const client = createDeferredClient(clientPromise);
// Start calls before real client exists (they'll wait)
myLog("In-Test", "calling: client.getName()")
const namePromise = client.getName();
myLog("In-Test", "calling: client.getCount()")
const countPromise = client.getCount();
// Simulate async client initialization (e.g., after 100ms)
let delay = 100;
myLog("In-Test", `setTimeout to resolve client (after ${delay}ms)`);
setTimeout(() => {
myLog("In-Test", "resolving the client")
resolveClient!(new RealApiClient());
}, delay);
// Calls complete after client becomes available
expect(await namePromise).toBe('Real Client');
expect(await countPromise).toBe(42);
});
it('should work with methods that have parameters', async () => {
console.log("");
myLog("In-Test", "should work with methods that have parameters...");
const client = createDeferredClient(Promise.resolve(new RealApiClient()));
const results = await client.search('typescript proxy');
expect(results).toEqual(['result for: typescript proxy']);
});
});
// Simulated client interface
interface ApiClient {
getName(): Promise<string>;
getCount(): Promise<number>;
search(query: string): Promise<string[]>;
}
// Real implementation (would be created later)
class RealApiClient implements ApiClient {
async getName() {
myLog("In-RealClient", "real getName() called")
return 'Real Client';
}
async getCount() {
myLog("In-RealClient", "real getCount() called")
return 42;
}
async search(query: string) {
myLog("In-RealClient", `real search("${query}") called`)
return [`result for: ${query}`];
}
}
const startMillis = Date.now();
function myLog(where: "In-Test" | "In-Proxy" | "In-RealClient", msg: string) {
console.log(`[elapsed: ${String(Date.now() - startMillis).padStart(4)}][${where}] ${msg}`);
}
Command to reproduce:
gt.sandbox.checkout.commit ba9797f26d8ef5c76854 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./run.sh"
Recorded output of command:
up to date, audited 179 packages in 3s
33 packages are looking for funding
run `npm fund` for details
2 moderate severity vulnerabilities
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
> glassthought-sandbox@1.0.0 test
> vitest run
RUN v0.34.6 /home/nickolaykondratyev/git_repos/glassthought-sandbox
stdout | src/main.test.ts > Proxy Pattern - Deferred Client > should defer method calls until client is ready (factory approach)
[elapsed: 1][In-Test] should defer method calls until client is ready (factory approach)...
[elapsed: 1][In-Test] calling: client.getName()
[elapsed: 2][In-Proxy] client is NOT yet defined awaiting on client Promise
[elapsed: 2][In-Test] calling: client.getCount()
[elapsed: 2][In-Proxy] client is NOT yet defined awaiting on client Promise
[elapsed: 2][In-Test] setTimeout to resolve client (after 100ms)
stdout | src/main.test.ts > Proxy Pattern - Deferred Client > should defer method calls until client is ready (factory approach)
[elapsed: 102][In-Test] resolving the client
[elapsed: 102][In-RealClient] real getName() called
[elapsed: 103][In-RealClient] real getCount() called
stdout | src/main.test.ts > Proxy Pattern - Deferred Client > should work with methods that have parameters
[elapsed: 104][In-Test] should work with methods that have parameters...
[elapsed: 104][In-Proxy] client is NOT yet defined awaiting on client Promise
[elapsed: 104][In-RealClient] real search("typescript proxy") called
✓ src/main.test.ts (2 tests) 105ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 12:41:21
Duration 613ms (transform 200ms, setup 0ms, collect 13ms, tests 105ms, environment 0ms, prepare 234ms)
GT-Sandbox-Snapshot: In this example Proxy throws if its called prior to real client being present.
Code
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Factory that creates a proxy wrapping deferred initialization
function createDeferredClient(clientPromise: Promise<ApiClient>): ApiClient {
let resolvedClient: ApiClient | undefined;
// Cache the resolved client
clientPromise
.then(c => resolvedClient = c);
return new Proxy({} as ApiClient, {
get: (_, prop: keyof ApiClient) => {
return async (...args: any[]) => {
let client;
if (resolvedClient) {
client = resolvedClient;
} else {
throw Error("Not ready yet you shouldn't be calling me!")
}
return (client[prop] as any)(...args);
};
}
});
}
describe('Proxy Pattern - Deferred Client', () => {
it('Proxy throws if not ready yet', async () => {
console.log("");
// Setup: Create a promise we control
// [Deferred Promise Resolution Pattern](http://www.glassthought.com/notes/3twym7huaodrb22qva8d4lf)
let resolveClient: (client: ApiClient) => void;
const clientPromise = new Promise<ApiClient>(resolve => {
resolveClient = resolve;
});
// Create deferred client directly (no separate factory call)
const client = createDeferredClient(clientPromise);
// Start calls before real client exists (they'll wait)
myLog("In-Test", "calling: client.getName()")
// expect to throw
await expect(client.getName()).rejects.toThrow("Not ready yet you shouldn't be calling me!");
});
});
// Simulated client interface
interface ApiClient {
getName(): Promise<string>;
getCount(): Promise<number>;
search(query: string): Promise<string[]>;
}
// Real implementation (would be created later)
class RealApiClient implements ApiClient {
async getName() {
myLog("In-RealClient", "real getName() called")
return 'Real Client';
}
async getCount() {
myLog("In-RealClient", "real getCount() called")
return 42;
}
async search(query: string) {
myLog("In-RealClient", `real search("${query}") called`)
return [`result for: ${query}`];
}
}
const startMillis = Date.now();
function myLog(where: "In-Test" | "In-Proxy" | "In-RealClient", msg: string) {
console.log(`[elapsed: ${String(Date.now() - startMillis).padStart(4)}][${where}] ${msg}`);
}
Command to reproduce:
gt.sandbox.checkout.commit 98d13ea135c3eac3c0c7 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./run.sh"
Recorded output of command:
up to date, audited 179 packages in 1s
33 packages are looking for funding
run `npm fund` for details
2 moderate severity vulnerabilities
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
> glassthought-sandbox@1.0.0 test
> vitest run
RUN v0.34.6 /home/nickolaykondratyev/git_repos/glassthought-sandbox
✓ src/main.test.ts (1 test) 2ms
stdout | src/main.test.ts > Proxy Pattern - Deferred Client > Proxy throws if not ready yet
[elapsed: 1][In-Test] calling: client.getName()
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 12:46:03
Duration 462ms (transform 52ms, setup 0ms, collect 12ms, tests 2ms, environment 0ms, prepare 120ms)