This JavaScript module records exactly everything you do inside of JavaScript using JavaScript Proxies without actually executing the operations. Operations are recorded and can be automatically replayed in a different context.
The JavaScript Recorder uses ES6 Proxies to intercept and record all JavaScript operations without executing them:
- Property access (get/set)
- Function calls
- Constructor invocations
- Nested object operations
Key Feature: Operations are NOT executed during recording - they only execute during replay. This allows you to "code" against APIs that don't exist in the current context (like DOM APIs in a Worker), record those operations, and replay them in the correct context (like the main thread).
npm install javascript-recorderOr simply clone and use:
git clone https://github.com/jimmywarting/javascript-recorder.gitNote: This package is ES module only. You must use import syntax, not require().
import { Recorder, createRecordHandler } from './recorder.js';
// Create a recorder instance
const recorder = new Recorder({ autoReplay: false });
// Create a recording handler
const recordHandler = createRecordHandler(recorder);
// Wrap an object with the recording proxy
const proxied = new Proxy({}, recordHandler);
// Operations are recorded but NOT executed
const value = proxied.document.createElement('div'); // Does NOT actually create an element
proxied.body.append(value); // Does NOT actually append anything
// View recordings
console.log(recorder.getRecordings());
// [
// { type: 'get', property: 'document', resultId: 'obj_0', ... },
// { type: 'get', property: 'createElement', resultId: 'obj_1', ... },
// { type: 'apply', args: ['div'], resultId: 'obj_2', ... },
// { type: 'get', property: 'body', resultId: 'obj_3', ... },
// { type: 'get', property: 'append', resultId: 'obj_4', ... },
// { type: 'apply', args: [{ __recordedObjectId: 'obj_2' }], ... }
// ]// In a Worker
import { Recorder, createRecordHandler } from './recorder.js';
// Create recorder with automatic replay enabled
// Provide the real context where operations should execute
const recorder = new Recorder({
replayContext: window, // or any target context
autoReplay: true
});
const recordHandler = createRecordHandler(recorder);
const proxiedWindow = new Proxy({}, recordHandler);
// "Code" against the DOM without actually touching it
const ref = proxiedWindow.document.createElement('div');
proxiedWindow.document.body.append(ref);
// Operations are automatically replayed on the next microtask
// in the real window context!const recorder = new Recorder({ autoReplay: false });
const handler = createRecordHandler(recorder);
const proxied = new Proxy({}, handler);
// Record operations
const ref = proxied.document.createElement('div');
proxied.document.body.append(ref);
// Later, replay in actual context
const realWindow = window; // or any real context
recorder.replay(realWindow);
// Now the operations execute in the real context// window.js (Main Thread)
import { Recorder, createRecordHandler } from './recorder.js';
const messageChannel = new MessageChannel();
const port1 = messageChannel.port1; // For receiving operations
const port2 = messageChannel.port2; // Send to worker
// Set up recorder to receive and replay operations
const recorder = new Recorder({
port: port1,
replayContext: window,
autoReplay: true
});
// Send port2 to worker
worker.postMessage({ port: port2 }, [port2]);
// worker.js (Worker Thread)
// Receive port from main thread
self.onmessage = (event) => {
const port = event.data.port;
// Create recorder that sends operations through the port
const recorder = new Recorder({
port: port,
autoReplay: true
});
const handler = createRecordHandler(recorder);
const proxiedWindow = new Proxy({}, handler);
// Record operations - they'll be sent to main thread and executed there
const ref = proxiedWindow.document.createElement('div');
proxiedWindow.document.body.append(ref);
// Operations are sent through MessagePort and replayed on main thread!
};import { Recorder, createRecordHandler } from './recorder.js';
const recorder = new Recorder({ autoReplay: false });
const handler = createRecordHandler(recorder);
const proxiedWindow = new Proxy({}, handler);
// Using the `using` keyword for automatic disposal (when supported)
{
using ref = proxiedWindow.document.createElement('div');
proxiedWindow.document.body.append(ref);
// ref is automatically disposed when exiting the block
// This decrements reference counts for proper cleanup
}
// Without `using`, objects rely on FinalizationRegistry for cleanup
// (non-deterministic, happens during garbage collection)
const div = proxiedWindow.document.createElement('span');
// div will be cleaned up eventually when garbage collectedconst recorder = new Recorder({
autoReplay: false,
useFinalization: true // Enable automatic cleanup (default: true)
});
// Objects are automatically tracked with FinalizationRegistry
// When they are garbage collected, ref counts are decremented
// Best practice: Use `using` keyword for deterministic cleanup
// FinalizationRegistry provides a safety net if you forgetThe main recorder class that stores all recorded operations.
new Recorder({
replayContext: null, // Context for automatic replay (default: null)
autoReplay: true, // Enable automatic replay on microtask (default: true)
port: null, // MessagePort for cross-context communication (default: null)
useFinalization: true, // Enable FinalizationRegistry for automatic cleanup (default: true)
debug: false // Enable debug logging for finalization (default: false)
})Note on reference counting: When Symbol.dispose is available, all created proxies are tracked with reference counts. This allows:
- Manual cleanup via
usingkeyword (deterministic) - Automatic cleanup via FinalizationRegistry when garbage collected (fallback)
This prevents memory leaks in long-running applications where proxies might not be explicitly disposed.
#### Methods
- `record(operation)` - Record an operation (usually called internally)
- `getRecordings()` - Get all recorded operations
- `clear()` - Clear all recordings
- `pause()` - Pause recording
- `resume()` - Resume recording
- `setReplayContext(context)` - Set the context for automatic replay
- `replay(context)` - Manually replay recorded operations in a given context
- `incrementRefCount(objectId)` - Increment reference count for an object
- `decrementRefCount(objectId)` - Decrement reference count for an object
- `registerForFinalization(proxy, objectId)` - Register a proxy for automatic cleanup
- `unregisterFromFinalization(proxy)` - Unregister a proxy from automatic cleanup
- `[Symbol.dispose]()` - Dispose of the recorder and clean up resources
### `createRecordedObject(recorder, target)`
Creates a recorded object handle that supports the `using` keyword and Symbol.dispose.
**Parameters:**
- `recorder` - A `Recorder` instance
- `target` - (Optional) The target to wrap
**Returns:** A `RecordedObjectHandle` that supports automatic cleanup
### `RecordedObjectHandle`
A wrapper class that provides automatic reference counting with Symbol.dispose support.
**Properties:**
- `value` - The proxied object
**Methods:**
- `[Symbol.dispose]()` - Automatically decrements reference counts
## Browser Testing
### Interactive Test Pages
Open these HTML pages in a web browser to see the recorder in action:
```bash
# Serve the files with a local web server
python3 -m http.server 8000
# or
npx serve .
Main test pages:
test-browser.html- Comprehensive testing with real DOM APIsdemo-using-finalization.html- Interactive demos ofusingkeyword and finalization
Comprehensive test suite with real browser APIs:
- Basic DOM recording and replay
- MessagePort communication simulation
- Real Web Worker integration
- Complex DOM operations with styles and events
Interactive demonstrations of memory management features:
- Demo 1: Using keyword for deterministic cleanup
- Demo 2: FinalizationRegistry for automatic garbage collection
- Demo 3: Side-by-side comparison of both approaches
- Demo 4: Reference counting visualization
Open http://localhost:8000/demo-using-finalization.html for interactive demos!
Each recorded operation is an object with the following structure:
{
type: 'get' | 'set' | 'apply' | 'construct',
target: 'string', // object identifier
property: 'string', // for get/set operations
args: Array, // for apply/construct operations
value: any, // for set operations
receiver: 'string', // receiver identifier
constructorName: 'string',// for construct operations
resultId: 'string' // identifier for the result object
}Object References: When an argument is a previously recorded object, it's serialized as:
{ __recordedObjectId: 'obj_N' }This allows the replay system to properly resolve object relationships.
See the example files for complete working examples:
example-no-exec.js- Demonstrates non-executing recording with automatic replayexample-messageport.js- Shows MessagePort-based cross-context communicationexample-dispose.js- Demonstrates Symbol.dispose and reference countingexample-using.js- Shows correct usage ofusingkeyword with proxiesexample-finalization.js- Demonstrates FinalizationRegistry for automatic cleanupexample-rtc.js- Shows the RTCPeerConnection use caseexample.js- General usage examplestest-browser.html- Interactive browser tests with real DOM APIsrecorder-worker.js- Web Worker example for browser testing
Run Node.js examples:
node example-messageport.js
node example-using.js
node --expose-gc example-finalization.js # Requires --expose-gc flagRun browser tests:
# Start a local web server
python3 -m http.server 8000
# Open http://localhost:8000/test-browser.htmlRun the test suite:
npm testOr directly:
node test.jsThe recorder uses ES6 Proxy traps to intercept operations without executing them:
-
Property Access (get): When you access a property, the
gettrap records it and returns a dummy proxy instead of the actual value. -
Property Assignment (set): When you assign a value, the
settrap records the operation but doesn't actually set anything. -
Function Calls (apply): When you call a function, the
applytrap records the call with its arguments but doesn't execute the function. It returns a dummy proxy. -
Constructor Calls (construct): When you use
newwith a constructor, theconstructtrap records the instantiation but doesn't create the object. It returns a dummy proxy. -
Object Reference Tracking: All returned proxies are tracked with unique IDs. When a proxy is used as an argument, it's serialized as an object ID reference.
-
Automatic Replay: If
autoReplayis enabled and areplayContextis set, all recorded operations are automatically replayed on the next microtask in the real context.
All objects in recordings are referenced by their IDs, allowing proper reconstruction during replay.
- Worker-to-Main-Thread Communication: Record DOM operations in a Worker, automatically replay them on the main thread
- Testing: Record operations for test replay without side effects
- Debugging: Track all operations without executing them
- API Mocking: "Code" against APIs that don't exist in the current context
- Operation Queuing: Batch operations and replay them later
- Cross-Context Execution: Record in one environment, execute in another
- Minimal performance overhead due to proxy wrapping
- Return values during recording are dummy proxies, not real values
- Some native APIs may have special behavior that's hard to replay
- Circular references are handled through ID tracking
MIT
Jimmy Wärting
Contributions are welcome! Please feel free to submit a Pull Request.