procxy 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -0
- package/README.md +936 -0
- package/dist/child/agent.d.ts +3 -0
- package/dist/child/agent.d.ts.map +1 -0
- package/dist/child/agent.js +126 -0
- package/dist/child/agent.js.map +1 -0
- package/dist/child/child-proxy.d.ts +23 -0
- package/dist/child/child-proxy.d.ts.map +1 -0
- package/dist/child/child-proxy.js +193 -0
- package/dist/child/child-proxy.js.map +1 -0
- package/dist/child/event-bridge.d.ts +15 -0
- package/dist/child/event-bridge.d.ts.map +1 -0
- package/dist/child/event-bridge.js +51 -0
- package/dist/child/event-bridge.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/parent/ipc-client.d.ts +31 -0
- package/dist/parent/ipc-client.d.ts.map +1 -0
- package/dist/parent/ipc-client.js +316 -0
- package/dist/parent/ipc-client.js.map +1 -0
- package/dist/parent/parent-proxy.d.ts +4 -0
- package/dist/parent/parent-proxy.d.ts.map +1 -0
- package/dist/parent/parent-proxy.js +76 -0
- package/dist/parent/parent-proxy.js.map +1 -0
- package/dist/parent/procxy.d.ts +6 -0
- package/dist/parent/procxy.d.ts.map +1 -0
- package/dist/parent/procxy.js +120 -0
- package/dist/parent/procxy.js.map +1 -0
- package/dist/shared/errors.d.ts +31 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +85 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/module-resolver.d.ts +6 -0
- package/dist/shared/module-resolver.d.ts.map +1 -0
- package/dist/shared/module-resolver.js +99 -0
- package/dist/shared/module-resolver.js.map +1 -0
- package/dist/shared/protocol.d.ts +86 -0
- package/dist/shared/protocol.d.ts.map +1 -0
- package/dist/shared/protocol.js +2 -0
- package/dist/shared/protocol.js.map +1 -0
- package/dist/shared/serialization.d.ts +20 -0
- package/dist/shared/serialization.d.ts.map +1 -0
- package/dist/shared/serialization.js +96 -0
- package/dist/shared/serialization.js.map +1 -0
- package/dist/types/options.d.ts +9 -0
- package/dist/types/options.d.ts.map +1 -0
- package/dist/types/options.js +2 -0
- package/dist/types/options.js.map +1 -0
- package/dist/types/procxy.d.ts +50 -0
- package/dist/types/procxy.d.ts.map +1 -0
- package/dist/types/procxy.js +2 -0
- package/dist/types/procxy.js.map +1 -0
- package/package.json +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
# Procxy ๐
|
|
2
|
+
|
|
3
|
+
> **procxy** */หprษk.si/* โ Transparent and type-safe process-based proxy for class instances
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/procxy)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](./tests)
|
|
9
|
+
[](./coverage)
|
|
10
|
+
[](./README.md#-performance)
|
|
11
|
+
[](https://github.com/sponsors/pradeepmouli)
|
|
12
|
+
|
|
13
|
+
> โ ๏ธ **Alpha Release** - API may change. Please report issues and provide feedback!
|
|
14
|
+
|
|
15
|
+
Run class instances in isolated child processes while interacting with them as if they were local objects. All method calls become async and are transparently forwarded over IPC with full TypeScript support.
|
|
16
|
+
|
|
17
|
+
## โจ Features
|
|
18
|
+
|
|
19
|
+
- **๐ฏ Type-Safe** - Full TypeScript support with IntelliSense autocomplete
|
|
20
|
+
- **๐ช Automagic Module Resolution** - Zero-config import path detection from your source code
|
|
21
|
+
- **โก Fast** - <10ms overhead per method call
|
|
22
|
+
- **๐ Events & Callbacks** - Transparent EventEmitter forwarding and bidirectional callback support
|
|
23
|
+
- **๐ Properties** - Read-only properties on parent, full read/write on child
|
|
24
|
+
- **๐ก๏ธ Error Handling** - Complete error propagation with stack traces
|
|
25
|
+
- **๐งน Lifecycle** - Automatic cleanup with disposable protocol (`using`/`await using`)
|
|
26
|
+
- **โ๏ธ Configurable** - Timeouts, retries, custom environment, working directory
|
|
27
|
+
- **๐ฆ Zero Dependencies** - Minimal bundle size (<50KB)
|
|
28
|
+
- **๐งช Well Tested** - See above
|
|
29
|
+
|
|
30
|
+
## ๐ฆ Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install procxy
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm add procxy
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
yarn add procxy
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## ๐ Quick Start
|
|
45
|
+
|
|
46
|
+
### Basic Usage
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { procxy } from 'procxy';
|
|
50
|
+
import { Calculator } from './calculator.js';
|
|
51
|
+
|
|
52
|
+
// Automatic module path detection (recommended)
|
|
53
|
+
const calc = await procxy(Calculator);
|
|
54
|
+
|
|
55
|
+
// Call methods (now async!)
|
|
56
|
+
const sum = await calc.add(5, 3); // 8
|
|
57
|
+
const product = await calc.multiply(4, 7); // 28
|
|
58
|
+
|
|
59
|
+
// Clean up
|
|
60
|
+
await calc.$terminate();
|
|
61
|
+
|
|
62
|
+
// Or with explicit module path (needed for dynamic imports)
|
|
63
|
+
const calc2 = await procxy(Calculator, './calculator.js');
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Using Disposables (Recommended)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { Calculator } from './calculator.js';
|
|
70
|
+
|
|
71
|
+
// Automatic cleanup with await using
|
|
72
|
+
await using calc = await procxy(Calculator);
|
|
73
|
+
const result = await calc.add(5, 3);
|
|
74
|
+
// Automatically terminated when scope exits
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Constructor Arguments
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { Worker } from './worker.js';
|
|
81
|
+
|
|
82
|
+
class Worker {
|
|
83
|
+
constructor(public name: string, public threads: number) {}
|
|
84
|
+
|
|
85
|
+
async process(data: string[]): Promise<string[]> {
|
|
86
|
+
// Heavy processing in isolated process
|
|
87
|
+
return data.map(s => s.toUpperCase());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Pass constructor arguments after options
|
|
92
|
+
const worker = await procxy(
|
|
93
|
+
Worker,
|
|
94
|
+
'./worker.js', // Can be omitted if Worker is imported
|
|
95
|
+
undefined, // options (or omit)
|
|
96
|
+
'MyWorker', // name argument
|
|
97
|
+
4 // threads argument
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const result = await worker.process(['hello', 'world']);
|
|
101
|
+
// ['HELLO', 'WORLD']
|
|
102
|
+
|
|
103
|
+
await worker.$terminate();
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## ๐ช Automagic Module Resolution
|
|
107
|
+
|
|
108
|
+
One of procxy's most convenient features is **automatic module path detection**. You don't need to manually specify where your class is locatedโprocxy figures it out by parsing your import statements at runtime.
|
|
109
|
+
|
|
110
|
+
### How It Works
|
|
111
|
+
|
|
112
|
+
When you call `procxy(MyClass)`, the library:
|
|
113
|
+
|
|
114
|
+
1. **Inspects the call stack** to find where `procxy()` was called
|
|
115
|
+
2. **Reads your source file** and parses import/require statements
|
|
116
|
+
3. **Matches the class name** to find the corresponding import path
|
|
117
|
+
4. **Resolves the absolute path** for the child process
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Your code
|
|
121
|
+
import { Calculator } from './math/calculator.js';
|
|
122
|
+
import { DataProcessor } from '@myorg/data-processor';
|
|
123
|
+
|
|
124
|
+
// Just works! No modulePath needed
|
|
125
|
+
const calc = await procxy(Calculator);
|
|
126
|
+
const processor = await procxy(DataProcessor);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Supported Import Styles
|
|
130
|
+
|
|
131
|
+
Procxy can detect module paths from:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// โ
ESM named imports
|
|
135
|
+
import { MyClass } from './path/to/module.js';
|
|
136
|
+
|
|
137
|
+
// โ
ESM default imports
|
|
138
|
+
import MyClass from './path/to/module.js';
|
|
139
|
+
|
|
140
|
+
// โ
CommonJS requires
|
|
141
|
+
const { MyClass } = require('./path/to/module');
|
|
142
|
+
|
|
143
|
+
// โ
Classes defined in the same file
|
|
144
|
+
class MyClass { ... }
|
|
145
|
+
const instance = await procxy(MyClass);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### When to Use Explicit Paths
|
|
149
|
+
|
|
150
|
+
There are cases where auto-detection won't work:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// โ Dynamic imports - must provide explicit path
|
|
154
|
+
const { Worker } = await import('./worker.js');
|
|
155
|
+
const worker = await procxy(Worker, './worker.js');
|
|
156
|
+
|
|
157
|
+
// โ Re-exported classes with different names
|
|
158
|
+
import { OriginalClass as AliasedClass } from './module.js';
|
|
159
|
+
const instance = await procxy(AliasedClass, './module.js');
|
|
160
|
+
|
|
161
|
+
// โ Classes from complex barrel exports
|
|
162
|
+
import { DeepNestedClass } from './barrel/index.js';
|
|
163
|
+
const instance = await procxy(DeepNestedClass, './barrel/deep/nested.js');
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Best Practices
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// ๐ Recommended: Let procxy detect the path
|
|
170
|
+
import { Calculator } from './calculator.js';
|
|
171
|
+
const calc = await procxy(Calculator);
|
|
172
|
+
|
|
173
|
+
// ๐ Also good: Explicit path when needed
|
|
174
|
+
const calc = await procxy(Calculator, './calculator.js');
|
|
175
|
+
|
|
176
|
+
// ๐ Avoid: Using anonymous classes (name required for resolution)
|
|
177
|
+
const calc = await procxy(class { add(a, b) { return a + b; } });
|
|
178
|
+
// Throws: Constructor must be a named class
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## ๐ API Reference
|
|
182
|
+
|
|
183
|
+
### `procxy<T>(Class, modulePath?, options?, ...constructorArgs)`
|
|
184
|
+
|
|
185
|
+
Creates a process-based proxy for a class instance.
|
|
186
|
+
|
|
187
|
+
**Parameters:**
|
|
188
|
+
|
|
189
|
+
- `Class: Constructor<T>` - The class constructor to instantiate
|
|
190
|
+
- `modulePath?: string` - **Optional** path to the module containing the class (auto-detected if omitted)
|
|
191
|
+
- `options?: ProcxyOptions` - Optional configuration
|
|
192
|
+
- `...constructorArgs` - Arguments for the class constructor
|
|
193
|
+
|
|
194
|
+
**Returns:** `Promise<Procxy<T>>` - Proxy object with all methods transformed to async
|
|
195
|
+
|
|
196
|
+
**Module Path Auto-Detection:**
|
|
197
|
+
|
|
198
|
+
procxy can automatically detect the module path by parsing your source file's import statements:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { Worker } from './worker.js'; // procxy detects this!
|
|
202
|
+
|
|
203
|
+
// No modulePath needed
|
|
204
|
+
const worker = await procxy(Worker);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The module path is auto-detected from:
|
|
208
|
+
- ESM named imports: `import { Class } from './path'`
|
|
209
|
+
- ESM default imports: `import Class from './path'`
|
|
210
|
+
- CommonJS imports: `const { Class } = require('./path')`
|
|
211
|
+
- Class definitions in the same file
|
|
212
|
+
|
|
213
|
+
**When to provide explicit modulePath:**
|
|
214
|
+
- Dynamic imports: `const { Worker } = await import('./worker')`
|
|
215
|
+
- Ambiguous import scenarios
|
|
216
|
+
- When importing from multiple locations
|
|
217
|
+
|
|
218
|
+
**Example:**
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
const worker = await procxy(Worker, './worker.js', {
|
|
222
|
+
timeout: 60000, // 60s per method call
|
|
223
|
+
retries: 3, // Retry 3 times on failure
|
|
224
|
+
env: { NODE_ENV: 'production' },
|
|
225
|
+
cwd: '/tmp'
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### `Procxy<T>` Type
|
|
230
|
+
|
|
231
|
+
The proxy type that wraps your class instance:
|
|
232
|
+
|
|
233
|
+
- **Methods**: All methods become `async` and return `Promise<ReturnType>`
|
|
234
|
+
- **Properties**: Public properties available as read-only on the parent proxy (updated from child)
|
|
235
|
+
- **Callbacks**: Function parameters are automatically proxied across process boundaries
|
|
236
|
+
- **Filtering**: Only JSON-serializable methods and properties are included
|
|
237
|
+
- **Lifecycle Methods**:
|
|
238
|
+
- `$terminate(): Promise<void>` - Terminate the child process
|
|
239
|
+
- `$process: ChildProcess` - Access the underlying process
|
|
240
|
+
- **Disposable**: Supports `using` and `await using` for automatic cleanup
|
|
241
|
+
|
|
242
|
+
### `ProcxyOptions`
|
|
243
|
+
|
|
244
|
+
Configuration options:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
interface ProcxyOptions {
|
|
248
|
+
timeout?: number; // Timeout per method call (default: 30000ms)
|
|
249
|
+
retries?: number; // Retry attempts (default: 3)
|
|
250
|
+
env?: Record<string, string>; // Custom environment variables
|
|
251
|
+
cwd?: string; // Working directory for child process
|
|
252
|
+
args?: Jsonifiable[]; // Command line arguments
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## ๐ฏ Use Cases
|
|
257
|
+
|
|
258
|
+
### CPU-Intensive Tasks
|
|
259
|
+
|
|
260
|
+
Offload heavy computations without blocking the event loop:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
class ImageProcessor {
|
|
264
|
+
async resize(image: Buffer, width: number): Promise<Buffer> {
|
|
265
|
+
// Heavy image processing
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
await using processor = await procxy(ImageProcessor, './image-processor.js');
|
|
270
|
+
const resized = await processor.resize(imageData, 800);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Isolated Execution
|
|
274
|
+
|
|
275
|
+
Run untrusted code in isolated processes:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
class SandboxedRunner {
|
|
279
|
+
async execute(code: string): Promise<any> {
|
|
280
|
+
// Execute in isolated environment
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const sandbox = await procxy(SandboxedRunner, './sandbox.js', {
|
|
285
|
+
timeout: 5000, // Kill after 5s
|
|
286
|
+
env: { NODE_ENV: 'sandbox' }
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### EventEmitter Support
|
|
291
|
+
|
|
292
|
+
Classes extending EventEmitter work transparently:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { EventEmitter } from 'events';
|
|
296
|
+
|
|
297
|
+
class DataStream extends EventEmitter {
|
|
298
|
+
async start(): Promise<void> {
|
|
299
|
+
// Stream data and emit events
|
|
300
|
+
this.emit('data', { chunk: 'example' });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const stream = await procxy(DataStream, './stream.js');
|
|
305
|
+
|
|
306
|
+
stream.on('data', (chunk) => {
|
|
307
|
+
console.log('Received:', chunk);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
await stream.start();
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Callback Support
|
|
314
|
+
|
|
315
|
+
Pass callbacks as function parameters and they'll be transparently proxied across the process boundary:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
class AsyncWorker {
|
|
319
|
+
async processWithCallback(
|
|
320
|
+
data: string[],
|
|
321
|
+
onProgress: (current: number, total: number) => void
|
|
322
|
+
): Promise<string[]> {
|
|
323
|
+
const result = [];
|
|
324
|
+
for (let i = 0; i < data.length; i++) {
|
|
325
|
+
result.push(data[i].toUpperCase());
|
|
326
|
+
onProgress(i + 1, data.length); // Invokes callback in parent
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const worker = await procxy(AsyncWorker, './async-worker.js');
|
|
333
|
+
|
|
334
|
+
const result = await worker.processWithCallback(
|
|
335
|
+
['hello', 'world'],
|
|
336
|
+
(current, total) => {
|
|
337
|
+
console.log(`Progress: ${current}/${total}`);
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Output:
|
|
342
|
+
// Progress: 1/2
|
|
343
|
+
// Progress: 2/2
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**How Callbacks Work:**
|
|
347
|
+
- Callback functions are serialized as callback IDs
|
|
348
|
+
- A registry maintains references in both processes
|
|
349
|
+
- IPC messages proxy callback invocations
|
|
350
|
+
- Automatic cleanup when the process terminates
|
|
351
|
+
- Bidirectional: parent can pass callbacks to child, child can invoke them
|
|
352
|
+
|
|
353
|
+
### Properties Support
|
|
354
|
+
|
|
355
|
+
Public properties are accessible as read-only on the parent and can be read/modified on the child:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
class Counter {
|
|
359
|
+
public count: number = 0;
|
|
360
|
+
public name: string = '';
|
|
361
|
+
|
|
362
|
+
increment(): void {
|
|
363
|
+
this.count++;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
setName(newName: string): void {
|
|
367
|
+
this.name = newName;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
getCount(): number {
|
|
371
|
+
return this.count;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const counter = await procxy(Counter, './counter.js');
|
|
376
|
+
|
|
377
|
+
// Properties are read-only on parent
|
|
378
|
+
console.log(counter.count); // 0 - synchronous read from property store
|
|
379
|
+
console.log(counter.name); // '' - synchronous read
|
|
380
|
+
|
|
381
|
+
// Modify via child methods
|
|
382
|
+
await counter.increment();
|
|
383
|
+
await counter.setName('MyCounter');
|
|
384
|
+
|
|
385
|
+
// Parent reads updated values
|
|
386
|
+
console.log(counter.count); // 1 - automatically synced from child
|
|
387
|
+
console.log(counter.name); // 'MyCounter' - automatically synced
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Property Synchronization:**
|
|
391
|
+
- Parent maintains a property store synchronized from the child
|
|
392
|
+
- Child can read properties directly (no IPC needed)
|
|
393
|
+
- Child can set properties (sends update to parent)
|
|
394
|
+
- Property updates are automatically synced after method calls
|
|
395
|
+
- Parent can only read properties (attempting to set throws an error)
|
|
396
|
+
- No race conditions: only the child can modify properties
|
|
397
|
+
|
|
398
|
+
## ๐ก๏ธ Error Handling
|
|
399
|
+
|
|
400
|
+
All errors from the child process are propagated with full stack traces:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import { TimeoutError, ChildCrashedError } from 'procxy';
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
await proxy.slowMethod();
|
|
407
|
+
} catch (err) {
|
|
408
|
+
if (err instanceof TimeoutError) {
|
|
409
|
+
console.log('Timeout after', err.timeoutMs, 'ms');
|
|
410
|
+
} else if (err instanceof ChildCrashedError) {
|
|
411
|
+
console.log('Child crashed with code:', err.exitCode);
|
|
412
|
+
} else {
|
|
413
|
+
console.log('Remote error:', err.message);
|
|
414
|
+
console.log('Stack trace:', err.stack);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Error Types
|
|
420
|
+
|
|
421
|
+
- `ProcxyError` - Base error class
|
|
422
|
+
- `TimeoutError` - Method call exceeded timeout
|
|
423
|
+
- `ChildCrashedError` - Child process exited unexpectedly
|
|
424
|
+
- `ModuleResolutionError` - Cannot find or load the module
|
|
425
|
+
- `SerializationError` - Arguments/return values not JSON-serializable
|
|
426
|
+
- `OptionsValidationError` - Invalid configuration options
|
|
427
|
+
|
|
428
|
+
## โ๏ธ Configuration
|
|
429
|
+
|
|
430
|
+
### Timeouts and Retries
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
const worker = await procxy(Worker, './worker.js', {
|
|
434
|
+
timeout: 60000, // 60 seconds per call
|
|
435
|
+
retries: 5 // Retry 5 times before failing
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Custom Environment
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
const worker = await procxy(Worker, './worker.js', {
|
|
443
|
+
env: {
|
|
444
|
+
NODE_ENV: 'production',
|
|
445
|
+
API_KEY: process.env.API_KEY,
|
|
446
|
+
LOG_LEVEL: 'debug'
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Working Directory
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
const worker = await procxy(Worker, './worker.js', {
|
|
455
|
+
cwd: '/tmp/workspace'
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## ๐ Limitations
|
|
460
|
+
|
|
461
|
+
1. **JSON Serialization** - Method arguments and return values must be JSON-serializable (functions are proxied as callbacks)
|
|
462
|
+
2. **Properties Read-Only on Parent** - Parent can only read properties, modifications must be done via child methods
|
|
463
|
+
3. **One-Way Events** - EventEmitter events only flow from child to parent (emit only works in child)
|
|
464
|
+
4. **Callback Context** - Callbacks are invoked with serialized arguments (e.g., no `this` binding)
|
|
465
|
+
|
|
466
|
+
## ๐งช Testing
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
# Run all tests
|
|
470
|
+
pnpm test
|
|
471
|
+
|
|
472
|
+
# Run tests with coverage
|
|
473
|
+
pnpm test:coverage
|
|
474
|
+
|
|
475
|
+
# Run specific test file
|
|
476
|
+
pnpm test tests/integration/basic-invocation.test.ts
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## ๐ Performance
|
|
480
|
+
|
|
481
|
+
- **Method call overhead**: <10ms average
|
|
482
|
+
- **Memory overhead**: <1MB per instance
|
|
483
|
+
- **Bundle size**: <50KB minified
|
|
484
|
+
- **Test coverage**: >90%
|
|
485
|
+
|
|
486
|
+
## ๐ง Troubleshooting
|
|
487
|
+
|
|
488
|
+
### Module Resolution Errors
|
|
489
|
+
|
|
490
|
+
If you get `ModuleResolutionError`, ensure the `modulePath` argument points to the correct file:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
// โ
Correct - explicit path
|
|
494
|
+
await procxy(Worker, './worker.js');
|
|
495
|
+
await procxy(Worker, '/absolute/path/to/worker.js');
|
|
496
|
+
|
|
497
|
+
// โ Wrong - missing module path
|
|
498
|
+
await procxy(Worker); // Error!
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Serialization Errors
|
|
502
|
+
|
|
503
|
+
Ensure all arguments and return values are JSON-serializable:
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
// โ
OK
|
|
507
|
+
await proxy.process({ name: 'test', count: 42 });
|
|
508
|
+
|
|
509
|
+
// โ Not OK - contains function
|
|
510
|
+
await proxy.process({ name: 'test', fn: () => {} });
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Timeout Issues
|
|
514
|
+
|
|
515
|
+
Increase timeout for long-running methods:
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
const worker = await procxy(Worker, './worker.js', {
|
|
519
|
+
timeout: 300000 // 5 minutes
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## ๐ค Contributing
|
|
524
|
+
|
|
525
|
+
Contributions welcome! Please read [CONTRIBUTING.md](./CONTRIBUTING.md) first.
|
|
526
|
+
|
|
527
|
+
## ๐ Future Enhancements
|
|
528
|
+
|
|
529
|
+
We're planning several exciting features for future releases:
|
|
530
|
+
|
|
531
|
+
### Computed Properties
|
|
532
|
+
|
|
533
|
+
Support for getters/setters with custom logic:
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
// This will be possible in a future release
|
|
537
|
+
class Temperature {
|
|
538
|
+
private _celsius: number = 0;
|
|
539
|
+
|
|
540
|
+
get fahrenheit(): number {
|
|
541
|
+
return (this._celsius * 9/5) + 32; // Computed property
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
set fahrenheit(value: number) {
|
|
545
|
+
this._celsius = (value - 32) * 5/9;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const temp = await procxy(Temperature, './temperature.js');
|
|
550
|
+
console.log(temp.fahrenheit); // Computed value from child
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Async Property Access
|
|
554
|
+
|
|
555
|
+
Lazy-load or compute properties asynchronously:
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
class DataLoader {
|
|
559
|
+
async $loadConfig(): Promise<Config> {
|
|
560
|
+
// Load from disk/network
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const loader = await procxy(DataLoader, './loader.js');
|
|
565
|
+
const config = await loader.$loadConfig();
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### TypedEmitter Integration
|
|
569
|
+
|
|
570
|
+
Full support for strongly-typed EventEmitter patterns:
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
type Events = {
|
|
574
|
+
progress: [number, number]; // [current, total]
|
|
575
|
+
complete: [result: string];
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
class Task extends EventEmitter<Events> {
|
|
579
|
+
async run(): Promise<string> {
|
|
580
|
+
this.emit('progress', 1, 10);
|
|
581
|
+
// ...
|
|
582
|
+
return 'done';
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
## ๐ More Examples
|
|
588
|
+
|
|
589
|
+
### Data Processing Pipeline
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
class ImageProcessor {
|
|
593
|
+
async resize(data: Buffer, width: number): Promise<Buffer> {
|
|
594
|
+
// Heavy computation in isolated process
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async compress(data: Buffer, quality: number): Promise<Buffer> {
|
|
598
|
+
// Another heavy operation
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
await using processor = await procxy(ImageProcessor, './image-processor.js');
|
|
603
|
+
|
|
604
|
+
let image = await processor.resize(imageData, 800);
|
|
605
|
+
image = await processor.compress(image, 85);
|
|
606
|
+
|
|
607
|
+
console.log('Processed:', image.length, 'bytes');
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Worker Pool Pattern
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
class WorkerPool {
|
|
614
|
+
private workers: any[] = [];
|
|
615
|
+
|
|
616
|
+
async initialize(poolSize: number): Promise<void> {
|
|
617
|
+
for (let i = 0; i < poolSize; i++) {
|
|
618
|
+
this.workers.push(await procxy(Worker, './worker.js'));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async execute(task: string): Promise<string> {
|
|
623
|
+
const worker = this.workers[Math.floor(Math.random() * this.workers.length)];
|
|
624
|
+
return await worker.processTask(task);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async shutdown(): Promise<void> {
|
|
628
|
+
await Promise.all(this.workers.map(w => w.$terminate()));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Real-Time Data Streaming
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
class DataStream extends EventEmitter {
|
|
637
|
+
async startStream(filter: (x: any) => boolean): Promise<void> {
|
|
638
|
+
// Start streaming data
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const stream = await procxy(DataStream, './stream.js');
|
|
643
|
+
|
|
644
|
+
let dataCount = 0;
|
|
645
|
+
stream.on('data', (chunk) => {
|
|
646
|
+
console.log('Chunk:', chunk);
|
|
647
|
+
dataCount++;
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
stream.on('complete', () => {
|
|
651
|
+
console.log('Total chunks:', dataCount);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
await stream.startStream(x => x.value > 100);
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Batch Processing with Progress
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
class BatchProcessor {
|
|
661
|
+
async processBatch(
|
|
662
|
+
items: string[],
|
|
663
|
+
onProgress: (processed: number, total: number, item: string) => void
|
|
664
|
+
): Promise<string[]> {
|
|
665
|
+
const results = [];
|
|
666
|
+
for (let i = 0; i < items.length; i++) {
|
|
667
|
+
const result = await this.heavyProcess(items[i]);
|
|
668
|
+
results.push(result);
|
|
669
|
+
onProgress(i + 1, items.length, items[i]);
|
|
670
|
+
}
|
|
671
|
+
return results;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private async heavyProcess(item: string): Promise<string> {
|
|
675
|
+
// CPU-intensive work
|
|
676
|
+
return item.toUpperCase();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const processor = await procxy(BatchProcessor, './batch-processor.js');
|
|
681
|
+
|
|
682
|
+
const results = await processor.processBatch(
|
|
683
|
+
['item1', 'item2', 'item3'],
|
|
684
|
+
(processed, total, current) => {
|
|
685
|
+
console.log(`Processed ${processed}/${total} (${current})`);
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
console.log('Results:', results);
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Async Factory Pattern
|
|
693
|
+
|
|
694
|
+
Use static factory methods or wrapper functions for cleaner initialization:
|
|
695
|
+
|
|
696
|
+
```typescript
|
|
697
|
+
class Database {
|
|
698
|
+
private connectionString: string = '';
|
|
699
|
+
private connected: boolean = false;
|
|
700
|
+
|
|
701
|
+
async connect(url: string): Promise<void> {
|
|
702
|
+
this.connectionString = url;
|
|
703
|
+
this.connected = true;
|
|
704
|
+
// Simulate connection setup
|
|
705
|
+
await new Promise(r => setTimeout(r, 100));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async query(sql: string): Promise<any[]> {
|
|
709
|
+
if (!this.connected) throw new Error('Not connected');
|
|
710
|
+
return [{ result: 'example' }];
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Factory function for cleaner API
|
|
715
|
+
async function createDatabase(url: string) {
|
|
716
|
+
const db = await procxy(Database, './database.js');
|
|
717
|
+
await db.connect(url);
|
|
718
|
+
return db;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Usage
|
|
722
|
+
const db = await createDatabase('postgres://localhost/mydb');
|
|
723
|
+
const results = await db.query('SELECT * FROM users');
|
|
724
|
+
await db.$terminate();
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Builder Pattern with Async Configuration
|
|
728
|
+
|
|
729
|
+
Build complex instances with fluent API and async setup:
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
class WorkerBuilder {
|
|
733
|
+
private name: string = 'Worker';
|
|
734
|
+
private threads: number = 1;
|
|
735
|
+
private timeout: number = 30000;
|
|
736
|
+
|
|
737
|
+
setName(name: string): void {
|
|
738
|
+
this.name = name;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
setThreads(threads: number): void {
|
|
742
|
+
this.threads = threads;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
setTimeout(ms: number): void {
|
|
746
|
+
this.timeout = ms;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async build(): Promise<Procxy<Worker>> {
|
|
750
|
+
const worker = await procxy(
|
|
751
|
+
Worker,
|
|
752
|
+
'./worker.js',
|
|
753
|
+
{ timeout: this.timeout },
|
|
754
|
+
this.name,
|
|
755
|
+
this.threads
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
// Async initialization
|
|
759
|
+
await worker.initialize();
|
|
760
|
+
return worker;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Usage with fluent API
|
|
765
|
+
const worker = await new WorkerBuilder()
|
|
766
|
+
.setName('ImageProcessor')
|
|
767
|
+
.setThreads(4)
|
|
768
|
+
.setTimeout(60000)
|
|
769
|
+
.build();
|
|
770
|
+
|
|
771
|
+
await worker.processImage(imageData);
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### Resource Pool with Async Initialization
|
|
775
|
+
|
|
776
|
+
Manage a pool of async-initialized resources:
|
|
777
|
+
|
|
778
|
+
```typescript
|
|
779
|
+
class AsyncResourcePool<T> {
|
|
780
|
+
private available: Procxy<T>[] = [];
|
|
781
|
+
private inUse = new Set<Procxy<T>>();
|
|
782
|
+
|
|
783
|
+
async initialize(
|
|
784
|
+
factory: () => Promise<Procxy<T>>,
|
|
785
|
+
poolSize: number
|
|
786
|
+
): Promise<void> {
|
|
787
|
+
for (let i = 0; i < poolSize; i++) {
|
|
788
|
+
const resource = await factory();
|
|
789
|
+
this.available.push(resource);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async acquire(): Promise<Procxy<T>> {
|
|
794
|
+
while (this.available.length === 0) {
|
|
795
|
+
// Wait for resource to be released
|
|
796
|
+
await new Promise(r => setTimeout(r, 10));
|
|
797
|
+
}
|
|
798
|
+
const resource = this.available.pop()!;
|
|
799
|
+
this.inUse.add(resource);
|
|
800
|
+
return resource;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
release(resource: Procxy<T>): void {
|
|
804
|
+
this.inUse.delete(resource);
|
|
805
|
+
this.available.push(resource);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async shutdown(): Promise<void> {
|
|
809
|
+
const allResources = [...this.available, ...this.inUse];
|
|
810
|
+
await Promise.all(allResources.map(r => r.$terminate()));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Usage
|
|
815
|
+
async function createWorker() {
|
|
816
|
+
return await procxy(Worker, './worker.js');
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const pool = new AsyncResourcePool<Worker>();
|
|
820
|
+
await pool.initialize(createWorker, 5);
|
|
821
|
+
|
|
822
|
+
// Acquire and use
|
|
823
|
+
const worker = await pool.acquire();
|
|
824
|
+
try {
|
|
825
|
+
const result = await worker.process(data);
|
|
826
|
+
} finally {
|
|
827
|
+
pool.release(worker);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Cleanup
|
|
831
|
+
await pool.shutdown();
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
### Lazy Initialization with Singleton Pattern
|
|
835
|
+
|
|
836
|
+
Create and cache instances on first use:
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
class LazyWorkerSingleton {
|
|
840
|
+
private static instance: Procxy<Worker> | null = null;
|
|
841
|
+
|
|
842
|
+
static async getInstance(): Promise<Procxy<Worker>> {
|
|
843
|
+
if (!this.instance) {
|
|
844
|
+
console.log('Initializing worker...');
|
|
845
|
+
this.instance = await procxy(Worker, './worker.js');
|
|
846
|
+
|
|
847
|
+
// Set up cleanup on process exit
|
|
848
|
+
process.on('exit', async () => {
|
|
849
|
+
if (this.instance) {
|
|
850
|
+
await this.instance.$terminate();
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
return this.instance;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
static async reset(): Promise<void> {
|
|
858
|
+
if (this.instance) {
|
|
859
|
+
await this.instance.$terminate();
|
|
860
|
+
this.instance = null;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Usage - only creates once
|
|
866
|
+
const worker1 = await LazyWorkerSingleton.getInstance();
|
|
867
|
+
const worker2 = await LazyWorkerSingleton.getInstance();
|
|
868
|
+
console.log(worker1 === worker2); // true - same instance
|
|
869
|
+
|
|
870
|
+
// Reset if needed
|
|
871
|
+
await LazyWorkerSingleton.reset();
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
### Dependency Injection with Async Resolution
|
|
875
|
+
|
|
876
|
+
Resolve dependencies asynchronously before using proxies:
|
|
877
|
+
|
|
878
|
+
```typescript
|
|
879
|
+
class ServiceContainer {
|
|
880
|
+
private services = new Map<string, any>();
|
|
881
|
+
|
|
882
|
+
async register<T>(
|
|
883
|
+
name: string,
|
|
884
|
+
factory: () => Promise<Procxy<T>>
|
|
885
|
+
): Promise<void> {
|
|
886
|
+
this.services.set(name, await factory());
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
get<T>(name: string): Procxy<T> {
|
|
890
|
+
const service = this.services.get(name);
|
|
891
|
+
if (!service) throw new Error(`Service '${name}' not found`);
|
|
892
|
+
return service;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async shutdown(): Promise<void> {
|
|
896
|
+
const promises = Array.from(this.services.values())
|
|
897
|
+
.map(service => service.$terminate?.());
|
|
898
|
+
await Promise.all(promises);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Setup
|
|
903
|
+
const container = new ServiceContainer();
|
|
904
|
+
|
|
905
|
+
await container.register('database', () =>
|
|
906
|
+
procxy(Database, './database.js')
|
|
907
|
+
);
|
|
908
|
+
|
|
909
|
+
await container.register('cache', () =>
|
|
910
|
+
procxy(Cache, './cache.js')
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
// Usage with injected dependencies
|
|
914
|
+
const db = container.get<Database>('database');
|
|
915
|
+
const cache = container.get<Cache>('cache');
|
|
916
|
+
|
|
917
|
+
await db.query('SELECT ...');
|
|
918
|
+
|
|
919
|
+
// Cleanup all
|
|
920
|
+
await container.shutdown();
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
## ๐ License
|
|
924
|
+
|
|
925
|
+
MIT
|
|
926
|
+
|
|
927
|
+
## ๐ Links
|
|
928
|
+
|
|
929
|
+
- [Documentation](https://github.com/pradeepmouli/procxy)
|
|
930
|
+
- [npm Package](https://www.npmjs.com/package/procxy)
|
|
931
|
+
- [Issue Tracker](https://github.com/pradeepmouli/procxy/issues)
|
|
932
|
+
- [Changelog](./CHANGELOG.md)
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
**Made with โค๏ธ by the Procxy team**
|