Skip to content

Commit b393e5e

Browse files
committed
js-adapter: fix shared state
1 parent 7468e7c commit b393e5e

File tree

5 files changed

+158
-62
lines changed

5 files changed

+158
-62
lines changed

example/js/main.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
completeWorkflowSignal,
99
getCurrentStatusQuery,
1010
} from './workflow';
11-
import * as fs from 'fs/promises';
1211
import { ReplayMode, setReplayMode, replay, setBreakpoints } from '@temporal/replayer-adapter-nodejs';
1312

1413
// ====================
@@ -87,7 +86,7 @@ async function replayFromFile(historyPath: string = './history.json'): Promise<v
8786
try {
8887
// Configure adapter for standalone replay
8988
setReplayMode(ReplayMode.STANDALONE);
90-
// setBreakpoints([3, 9])
89+
setBreakpoints([9, 15])
9190
const opts = {
9291
historyFilePath: historyPath,
9392
workerReplayOptions: {
@@ -100,8 +99,9 @@ async function replayFromFile(historyPath: string = './history.json'): Promise<v
10099
]
101100
}
102101
},
102+
name: 'hehe'
103103
} as any; // adapter types
104-
104+
105105
await replay(opts, exampleWorkflow);
106106

107107
console.log('Replay completed successfully');

replayer-adapter-nodejs/README.md

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,12 @@ await replay(opts, YourWorkflowClass);
9393

9494
### Using Interceptors Directly
9595

96+
The interceptors are automatically included when using the `replay` function, but you can also use them directly with a Temporal Worker:
97+
9698
```typescript
9799
import { Worker } from '@temporalio/worker';
98-
import { workflowInterceptors, activityInterceptors } from '@temporal/replayer-adapter-nodejs';
100+
import { interceptors as workflowInterceptors } from '@temporal/replayer-adapter-nodejs/dist/workflow-interceptors';
101+
import { activityInterceptors } from '@temporal/replayer-adapter-nodejs/dist/activity-interceptors';
99102

100103
const worker = Worker.create({
101104
taskQueue: 'your-task-queue',
@@ -143,10 +146,10 @@ Replay workflow with history from JSON file.
143146
### Interceptors
144147

145148
#### `workflowInterceptors`
146-
Workflow interceptor factory for debugging support.
149+
Workflow interceptor factory for debugging support. Automatically injected when using the `replay` function.
147150

148151
#### `activityInterceptors`
149-
Activity interceptor factory for debugging support.
152+
Activity interceptor factory for debugging support. Automatically injected when using the `replay` function.
150153

151154
## Environment Variables
152155

@@ -157,7 +160,48 @@ Activity interceptor factory for debugging support.
157160
See the `example/` directory for complete examples of:
158161
- Standalone workflow replay with breakpoints
159162
- IDE-integrated workflow debugging
160-
- Custom interceptor usage
163+
164+
### Basic Usage Example
165+
166+
```typescript
167+
import {
168+
ReplayMode,
169+
ReplayOptions,
170+
setReplayMode,
171+
setBreakpoints,
172+
replay
173+
} from '@temporal/replayer-adapter-nodejs';
174+
175+
// Example workflow
176+
async function greetingWorkflow(name: string): Promise<string> {
177+
return `Hello, ${name}!`;
178+
}
179+
180+
async function main() {
181+
// Standalone mode
182+
setReplayMode(ReplayMode.STANDALONE);
183+
setBreakpoints([1, 5, 10]);
184+
185+
const standaloneOpts: ReplayOptions = {
186+
historyFilePath: './example-history.json',
187+
workerReplayOptions: {
188+
workflowsPath: require.resolve('./workflows'),
189+
}
190+
};
191+
192+
await replay(standaloneOpts, greetingWorkflow);
193+
194+
// IDE mode
195+
setReplayMode(ReplayMode.IDE);
196+
const ideOpts: ReplayOptions = {
197+
workerReplayOptions: {
198+
workflowsPath: require.resolve('./workflows'),
199+
}
200+
};
201+
202+
await replay(ideOpts, greetingWorkflow);
203+
}
204+
```
161205

162206
## Architecture
163207

@@ -168,12 +212,41 @@ This replayer adapter follows the same architecture as the Go and Python impleme
168212
3. **HTTP Client**: Communicate with IDE debugger for breakpoint status and highlighting
169213
4. **History Loading**: Support loading from both JSON files and HTTP endpoints
170214

215+
### Key Components
216+
217+
- **`replayer.ts`**: Main replay logic and breakpoint handling
218+
- **`workflow-interceptors.ts`**: Workflow interceptors for debugging support
219+
- **`activity-interceptors.ts`**: Activity interceptors for debugging support
220+
- **`types.ts`**: Type definitions and state management
221+
- **`http-client.ts`**: HTTP communication with IDE debugger
222+
171223
## Compatibility
172224

173225
- Node.js 16.x or higher
174-
- Temporal TypeScript SDK 1.15.0 or higher
226+
- Temporal TypeScript SDK 1.12.0 or higher
175227
- TypeScript 4.9.0 or higher
176228

229+
## Development
230+
231+
### Building
232+
233+
```bash
234+
npm run build
235+
```
236+
237+
### Development Mode
238+
239+
```bash
240+
npm run dev
241+
```
242+
243+
### Clean Build
244+
245+
```bash
246+
npm run clean
247+
npm run build
248+
```
249+
177250
## Contributing
178251

179252
This package follows the same patterns as the existing Go and Python replayer adapters. When making changes, ensure compatibility across all three implementations.

replayer-adapter-nodejs/src/index.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,15 @@
88
* @version 0.1.0
99
*/
1010

11-
// Export types
12-
export { ReplayMode, ReplayOptions } from './types';
13-
1411
// Export main functions
1512
export {
16-
setReplayMode,
17-
setBreakpoints,
18-
isBreakpoint,
19-
highlightCurrentEventInIDE,
20-
raiseSentinelBreakpoint,
21-
getHistoryFromIDE,
2213
replay,
23-
replayWithHistory,
24-
replayWithJsonFile,
2514
} from './replayer';
2615

27-
// Export interceptors
28-
export { interceptors as workflowInterceptors } from './workflow-interceptors';
29-
export { activityInterceptors } from './activity-interceptors';
30-
31-
// Export HTTP client utilities
32-
export { httpGet, httpPost, HttpResponse } from './http-client';
16+
// Export config setters
17+
export {
18+
setBreakpoints,
19+
setReplayMode,
20+
ReplayMode,
21+
ReplayOptions
22+
} from './types';

replayer-adapter-nodejs/src/replayer.ts

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,27 @@ import * as fs from 'fs/promises';
22
import { Worker } from '@temporalio/worker';
33
import { historyFromJSON } from '@temporalio/common/lib/proto-utils';
44
import { temporal } from '@temporalio/proto';
5-
import { workflowInfo } from '@temporalio/workflow';
6-
import { ReplayMode, ReplayOptions } from './types';
5+
import { getLNSE, ReplayMode, ReplayOptions, setDebuggerAddr, setLNSE, getBreakpoints, getReplayMode, getDebuggerAddr } from './types';
76
import { httpGet, httpPost } from './http-client';
87
import { activityInterceptors } from './activity-interceptors';
98

10-
// Global state
11-
let mode: ReplayMode = ReplayMode.STANDALONE;
12-
// TODO: make this updatable, currently hard-coded to test other things
13-
let breakpoints: Set<number> = new Set([9,15]);
14-
let lastNotifiedStartEvent: number = -1;
15-
let debuggerAddr: string = '';
169

17-
/**
18-
* Set the replay mode (standalone or IDE)
19-
*/
20-
export function setReplayMode(m: ReplayMode): void {
21-
mode = m;
22-
}
23-
24-
/**
25-
* Set breakpoints for standalone mode
26-
*/
27-
export function setBreakpoints(eventIds: number[]): void {
28-
breakpoints = new Set(eventIds);
29-
}
3010

3111
/**
3212
* Check if the given event ID is a breakpoint
3313
*/
3414
export function isBreakpoint(eventId: number): boolean {
35-
switch (mode) {
15+
switch (getReplayMode()) {
3616
case ReplayMode.STANDALONE:
37-
console.log(`Standalone checking breakpoints: ${Array.from(breakpoints)}, eventId: ${eventId}`);
38-
if (breakpoints.has(eventId)) {
17+
console.log(`Standalone checking breakpoints: ${Array.from(getBreakpoints())}, eventId: ${eventId}`);
18+
if (getBreakpoints().has(eventId)) {
3919
console.log(`Hit breakpoint at eventId: ${eventId}`);
4020
return true;
4121
}
4222
return false;
4323

4424
case ReplayMode.IDE:
45-
if (!debuggerAddr) {
25+
if (!getDebuggerAddr()) {
4626
return false;
4727
}
4828

@@ -63,17 +43,17 @@ export function isBreakpoint(eventId: number): boolean {
6343
* Send highlight request to IDE for current event
6444
*/
6545
export function highlightCurrentEventInIDE(eventId: number): void {
66-
if (!debuggerAddr) {
46+
if (!getDebuggerAddr()) {
6747
console.warn('debuggerAddr is empty, cannot send highlight request');
6848
return;
6949
}
7050

71-
console.log(`Sending highlight request for event ${eventId} to ${debuggerAddr}/current-event`);
51+
console.log(`Sending highlight request for event ${eventId} to ${getDebuggerAddr()}/current-event`);
7252

7353
const payload = JSON.stringify({ eventId });
7454

7555
try {
76-
sendHighlightRequest(debuggerAddr, payload);
56+
sendHighlightRequest(getDebuggerAddr(), payload);
7757
console.log(`✓ Successfully highlighted event ${eventId} in debugger UI`);
7858
} catch (error) {
7959
console.warn(`Failed to send highlight request: ${error}`);
@@ -96,15 +76,15 @@ export function raiseSentinelBreakpoint(caller: string, info?: any): void {
9676
}
9777

9878
if (eventId !== undefined) {
99-
if (eventId <= lastNotifiedStartEvent) {
79+
if (eventId <= getLNSE()) {
10080
return;
10181
}
102-
lastNotifiedStartEvent = eventId;
82+
setLNSE(eventId);
10383
console.log(`runner notified at ${caller}, eventId: ${eventId}`);
10484

10585
if (isBreakpoint(eventId)) {
10686
console.log(`Pause at event ${eventId}`);
107-
if (mode === ReplayMode.IDE) {
87+
if (getReplayMode() === ReplayMode.IDE) {
10888
highlightCurrentEventInIDE(eventId);
10989
}
11090
debugger;
@@ -120,13 +100,13 @@ export async function getHistoryFromIDE(): Promise<temporal.api.history.v1.IHist
120100
const runnerAddr = `http://127.0.0.1:${port}`;
121101

122102
try {
123-
const response = await httpGet(`${runnerAddr}/history`);
103+
const response = await httpGet(`${getDebuggerAddr()}/history`);
124104
if (response.statusCode !== 200) {
125105
throw new Error(`HTTP error! status: ${response.statusCode}`);
126106
}
127107

128108
const historyData = JSON.parse(response.body);
129-
debuggerAddr = runnerAddr;
109+
setDebuggerAddr(runnerAddr);
130110
return historyData;
131111
} catch (error) {
132112
console.error(`Could not get history from IDE: ${error}`);
@@ -138,9 +118,9 @@ export async function getHistoryFromIDE(): Promise<temporal.api.history.v1.IHist
138118
* Main replay function that handles both standalone and IDE modes
139119
*/
140120
export async function replay(opts: ReplayOptions, workflow: any): Promise<void> {
141-
console.log(`Replaying in mode ${mode}`);
121+
console.log(`Replaying in mode ${getReplayMode()}`);
142122

143-
if (mode === ReplayMode.STANDALONE) {
123+
if (getReplayMode() === ReplayMode.STANDALONE) {
144124
console.log('Replaying in standalone mode');
145125
return replayWithJsonFile(opts.workerReplayOptions || {}, workflow, opts.historyFilePath!);
146126
} else {
@@ -199,7 +179,7 @@ function checkBreakpointWithIDE(eventId: number): boolean {
199179
try {
200180
// This should be an async call in practice, but for simplicity keeping it sync
201181
// In a production implementation, you'd want to cache breakpoints or make this async
202-
const response = httpGet(`${debuggerAddr}/breakpoints`, 2000);
182+
const response = httpGet(`${getDebuggerAddr()}/breakpoints`, 2000);
203183
response.then((res) => {
204184
if (res.statusCode === 200) {
205185
const payload = JSON.parse(res.body);

replayer-adapter-nodejs/src/types.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,57 @@ export interface ReplayOptions {
2323
* Path to the history JSON file (only used in STANDALONE mode)
2424
*/
2525
historyFilePath?: string;
26-
}
26+
}
27+
28+
29+
export var mode: ReplayMode = ReplayMode.STANDALONE;
30+
// TODO: make this updatable, currently hard-coded to test other things
31+
export var breakpoints: Set<number> = new Set([9,15]);
32+
export var lastNotifiedStartEvent: number = -1;
33+
export var debuggerAddr: string = '';
34+
35+
36+
/**
37+
* Set breakpoints for standalone mode
38+
*/
39+
export function setBreakpoints(eventIds: number[]): void {
40+
breakpoints = new Set(eventIds);
41+
}
42+
43+
export function getBreakpoints() {
44+
return breakpoints;
45+
}
46+
47+
/**
48+
* Set the replay mode (standalone or IDE)
49+
*/
50+
export function setReplayMode(m: ReplayMode): void {
51+
mode = m;
52+
}
53+
54+
export function getReplayMode(): ReplayMode {
55+
return mode;
56+
}
57+
58+
/**
59+
* Set debugger addr
60+
*/
61+
62+
export function setDebuggerAddr(addr: string): void {
63+
debuggerAddr = addr;
64+
}
65+
66+
export function getDebuggerAddr(): string {
67+
return debuggerAddr;
68+
}
69+
70+
/**
71+
* Set lastNotifiedStartEvent
72+
*/
73+
export function setLNSE(eventId: number): void {
74+
lastNotifiedStartEvent = eventId;
75+
}
76+
77+
export function getLNSE(): number {
78+
return lastNotifiedStartEvent;
79+
}

0 commit comments

Comments
 (0)