n8nac 0.9.9-next.df19681 → 0.9.9-next.e754dc0
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/README.md +12 -6
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +3 -9
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/sync.d.ts +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +22 -11
- package/dist/commands/sync.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/services/cli-api.d.ts +2 -1
- package/dist/core/services/cli-api.d.ts.map +1 -1
- package/dist/core/services/cli-api.js +2 -1
- package/dist/core/services/cli-api.js.map +1 -1
- package/dist/core/services/resolution-manager.d.ts +2 -2
- package/dist/core/services/resolution-manager.d.ts.map +1 -1
- package/dist/core/services/resolution-manager.js +16 -2
- package/dist/core/services/resolution-manager.js.map +1 -1
- package/dist/core/services/state-manager.d.ts +4 -3
- package/dist/core/services/state-manager.d.ts.map +1 -1
- package/dist/core/services/state-manager.js +2 -2
- package/dist/core/services/state-manager.js.map +1 -1
- package/dist/core/services/sync-engine.d.ts +2 -2
- package/dist/core/services/sync-engine.d.ts.map +1 -1
- package/dist/core/services/sync-engine.js +60 -116
- package/dist/core/services/sync-engine.js.map +1 -1
- package/dist/core/services/sync-manager.d.ts +0 -3
- package/dist/core/services/sync-manager.d.ts.map +1 -1
- package/dist/core/services/sync-manager.js +7 -19
- package/dist/core/services/sync-manager.js.map +1 -1
- package/dist/core/services/{watcher.d.ts → workflow-state-tracker.d.ts} +6 -47
- package/dist/core/services/workflow-state-tracker.d.ts.map +1 -0
- package/dist/core/services/{watcher.js → workflow-state-tracker.js} +90 -468
- package/dist/core/services/workflow-state-tracker.js.map +1 -0
- package/dist/core/types.d.ts +0 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +0 -1
- package/dist/core/types.js.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/core/services/watcher.d.ts.map +0 -1
- package/dist/core/services/watcher.js.map +0 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import EventEmitter from 'events';
|
|
4
|
-
import chokidar from 'chokidar';
|
|
5
4
|
import { WorkflowTransformerAdapter } from './workflow-transformer-adapter.js';
|
|
6
5
|
import { HashUtils } from './hash-utils.js';
|
|
7
6
|
import { WorkflowSyncStatus } from '../types.js';
|
|
@@ -17,8 +16,7 @@ import { WorkflowSyncStatus } from '../types.js';
|
|
|
17
16
|
*
|
|
18
17
|
* Never performs synchronization actions - only observes reality.
|
|
19
18
|
*/
|
|
20
|
-
export class
|
|
21
|
-
watcherSubscription = null;
|
|
19
|
+
export class WorkflowStateTracker extends EventEmitter {
|
|
22
20
|
client;
|
|
23
21
|
directory;
|
|
24
22
|
syncInactive;
|
|
@@ -26,7 +24,6 @@ export class Watcher extends EventEmitter {
|
|
|
26
24
|
projectId;
|
|
27
25
|
stateFilePath;
|
|
28
26
|
isConnected = true;
|
|
29
|
-
isInitializing = false;
|
|
30
27
|
/** True during the first refreshRemoteState() call — suppresses status broadcasts */
|
|
31
28
|
isInitialRemoteLoad = false;
|
|
32
29
|
// Internal state tracking
|
|
@@ -36,13 +33,6 @@ export class Watcher extends EventEmitter {
|
|
|
36
33
|
idToFileMap = new Map(); // workflowId -> filename
|
|
37
34
|
lastKnownStatuses = new Map(); // workflowId or filename -> status
|
|
38
35
|
remoteIds = new Set(); // workflowId
|
|
39
|
-
// Concurrency control
|
|
40
|
-
isPaused = new Set(); // IDs for which observation is paused
|
|
41
|
-
syncInProgress = new Set(); // IDs currently being synced
|
|
42
|
-
pausedFilenames = new Set(); // Filenames for which observation is paused (for workflows without ID yet)
|
|
43
|
-
// Potential renames: when we see an add event for a workflow ID that already exists,
|
|
44
|
-
// we track it here to match with subsequent unlink events
|
|
45
|
-
potentialRenames = new Map();
|
|
46
36
|
// Lightweight remote state cache
|
|
47
37
|
remoteTimestamps = new Map(); // workflowId -> updatedAt
|
|
48
38
|
/** Canonical display name for each remote workflow (id is the unique key, NOT the name). */
|
|
@@ -58,309 +48,12 @@ export class Watcher extends EventEmitter {
|
|
|
58
48
|
// Restore persisted mappings immediately so 'pull' and other commands can find workflows
|
|
59
49
|
this.restoreMappingsFromState();
|
|
60
50
|
}
|
|
61
|
-
async start() {
|
|
62
|
-
if (this.watcherSubscription)
|
|
63
|
-
return;
|
|
64
|
-
this.isInitializing = true;
|
|
65
|
-
// Don't fetch remote state on startup (no batch operations)
|
|
66
|
-
// Remote state will be populated incrementally through single-workflow fetch operations
|
|
67
|
-
// Skip connection test - assume connected, will fail on first fetch if not
|
|
68
|
-
await this.refreshLocalState();
|
|
69
|
-
// Restore persisted ID → filename mappings from state
|
|
70
|
-
// This ensures stable filename assignment even when remote workflows have duplicate names
|
|
71
|
-
this.restoreMappingsFromState();
|
|
72
|
-
this.isInitializing = false;
|
|
73
|
-
// Local Watch with Chokidar
|
|
74
|
-
this.watcherSubscription = chokidar.watch(this.directory, {
|
|
75
|
-
ignored: [
|
|
76
|
-
'**/.n8n-state.json',
|
|
77
|
-
'**/.git/**',
|
|
78
|
-
/(^|[\/\\])\../
|
|
79
|
-
],
|
|
80
|
-
ignoreInitial: true,
|
|
81
|
-
persistent: true,
|
|
82
|
-
awaitWriteFinish: {
|
|
83
|
-
stabilityThreshold: 100,
|
|
84
|
-
pollInterval: 50
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
// Wait for watcher to be ready
|
|
88
|
-
await new Promise((resolve) => {
|
|
89
|
-
this.watcherSubscription?.once('ready', resolve);
|
|
90
|
-
});
|
|
91
|
-
this.watcherSubscription
|
|
92
|
-
.on('add', (filePath) => {
|
|
93
|
-
const filename = path.basename(filePath);
|
|
94
|
-
if (filename.startsWith('.'))
|
|
95
|
-
return;
|
|
96
|
-
this.onLocalChange(filePath);
|
|
97
|
-
})
|
|
98
|
-
.on('change', (filePath) => {
|
|
99
|
-
const filename = path.basename(filePath);
|
|
100
|
-
if (filename.startsWith('.'))
|
|
101
|
-
return;
|
|
102
|
-
this.onLocalChange(filePath);
|
|
103
|
-
})
|
|
104
|
-
.on('unlink', (filePath) => {
|
|
105
|
-
const filename = path.basename(filePath);
|
|
106
|
-
if (filename.startsWith('.'))
|
|
107
|
-
return;
|
|
108
|
-
this.onLocalDelete(filePath);
|
|
109
|
-
})
|
|
110
|
-
.on('error', (error) => {
|
|
111
|
-
this.emit('error', error);
|
|
112
|
-
});
|
|
113
|
-
this.emit('ready');
|
|
114
|
-
}
|
|
115
|
-
async stop() {
|
|
116
|
-
if (this.watcherSubscription) {
|
|
117
|
-
await this.watcherSubscription.close();
|
|
118
|
-
this.watcherSubscription = null;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
51
|
getDirectory() {
|
|
122
52
|
return this.directory;
|
|
123
53
|
}
|
|
124
54
|
getFilenameForId(id) {
|
|
125
55
|
return this.idToFileMap.get(id);
|
|
126
56
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Pause observation for a workflow during sync operations
|
|
129
|
-
*/
|
|
130
|
-
pauseObservation(workflowId) {
|
|
131
|
-
this.isPaused.add(workflowId);
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Resume observation after sync operations
|
|
135
|
-
*/
|
|
136
|
-
resumeObservation(workflowId) {
|
|
137
|
-
this.isPaused.delete(workflowId);
|
|
138
|
-
// Don't force refresh here to avoid unnecessary API calls
|
|
139
|
-
// In git-like sync, remote state is updated explicitly via fetch command
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Pause observation for a filename (for workflows without ID yet)
|
|
143
|
-
*/
|
|
144
|
-
pauseObservationByFilename(filename) {
|
|
145
|
-
this.pausedFilenames.add(filename);
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Resume observation for a filename
|
|
149
|
-
*/
|
|
150
|
-
resumeObservationByFilename(filename) {
|
|
151
|
-
this.pausedFilenames.delete(filename);
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Mark a workflow as being synced (prevents race conditions)
|
|
155
|
-
*/
|
|
156
|
-
markSyncInProgress(workflowId) {
|
|
157
|
-
this.syncInProgress.add(workflowId);
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Mark a workflow as no longer being synced
|
|
161
|
-
*/
|
|
162
|
-
markSyncComplete(workflowId) {
|
|
163
|
-
this.syncInProgress.delete(workflowId);
|
|
164
|
-
}
|
|
165
|
-
async onLocalChange(filePath) {
|
|
166
|
-
const filename = path.basename(filePath);
|
|
167
|
-
console.log(`[Watcher] onLocalChange: ${filename}`);
|
|
168
|
-
if (!filename.endsWith('.workflow.ts'))
|
|
169
|
-
return;
|
|
170
|
-
const content = this.readJsonFile(filePath);
|
|
171
|
-
if (!content) {
|
|
172
|
-
console.log(`[Watcher] ❌ Cannot read file content for ${filename} - readJsonFile returned null`);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
console.log(`[Watcher] ✅ File content read for ${filename}, ID=${content.id}`);
|
|
176
|
-
// Check if filename is paused (for workflows without ID)
|
|
177
|
-
if (this.pausedFilenames.has(filename)) {
|
|
178
|
-
console.log(`[Watcher] ⏸️ Filename ${filename} is paused, ignoring change`);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
let workflowId = content.id || this.fileToIdMap.get(filename);
|
|
182
|
-
if (workflowId && (this.isPaused.has(workflowId) || this.syncInProgress.has(workflowId))) {
|
|
183
|
-
console.log(`[Watcher] ⏸️ Workflow ${workflowId} is paused or sync in progress, ignoring change`);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
// Check for duplicate ID (following architectural plan)
|
|
187
|
-
if (content.id) {
|
|
188
|
-
const existingFilename = this.idToFileMap.get(content.id);
|
|
189
|
-
if (existingFilename && existingFilename !== filename) {
|
|
190
|
-
// Check if the existing file still exists on disk
|
|
191
|
-
const existingFilePath = path.join(this.directory, existingFilename);
|
|
192
|
-
const fileExists = fs.existsSync(existingFilePath);
|
|
193
|
-
if (!fileExists) {
|
|
194
|
-
// The existing file doesn't exist - this is likely a rename
|
|
195
|
-
// Update mappings to point to the new filename
|
|
196
|
-
this.fileToIdMap.delete(existingFilename);
|
|
197
|
-
this.fileToIdMap.set(filename, content.id);
|
|
198
|
-
this.idToFileMap.set(content.id, filename);
|
|
199
|
-
// PERSIST: Update filename in state to prevent "ghost" workflows after restart
|
|
200
|
-
const state = this.loadState();
|
|
201
|
-
if (state.workflows[content.id]) {
|
|
202
|
-
state.workflows[content.id].filename = filename;
|
|
203
|
-
this.saveState(state);
|
|
204
|
-
}
|
|
205
|
-
// Emit rename event
|
|
206
|
-
this.emit('fileRenamed', {
|
|
207
|
-
workflowId: content.id,
|
|
208
|
-
oldFilename: existingFilename,
|
|
209
|
-
newFilename: filename
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
// File exists - this could be a rename where add happened before unlink
|
|
214
|
-
// Track as potential rename and wait for unlink event
|
|
215
|
-
this.potentialRenames.set(content.id, {
|
|
216
|
-
newFilename: filename,
|
|
217
|
-
timestamp: Date.now()
|
|
218
|
-
});
|
|
219
|
-
// File exists - this is a DUPLICATE ID (copy-paste)
|
|
220
|
-
// Principle: Keep ID only in the oldest file, remove from the new one
|
|
221
|
-
// DUPLICAT DÉTECTÉ pendant le watch → supprimer l'ID du nouveau fichier
|
|
222
|
-
// Remove ID from the new file
|
|
223
|
-
const currentContent = this.readJsonFile(filePath);
|
|
224
|
-
if (currentContent && currentContent.id === content.id) {
|
|
225
|
-
delete currentContent.id;
|
|
226
|
-
await this.writeWorkflowFile(filename, currentContent);
|
|
227
|
-
// Re-read the TypeScript content and compute hash
|
|
228
|
-
const tsContent = fs.readFileSync(filePath, 'utf-8');
|
|
229
|
-
try {
|
|
230
|
-
const hash = await WorkflowTransformerAdapter.hashWorkflow(tsContent);
|
|
231
|
-
const workflowId = this.fileToIdMap.get(filename);
|
|
232
|
-
this.localHashes.set(filename, hash);
|
|
233
|
-
this.broadcastStatus(filename, workflowId);
|
|
234
|
-
}
|
|
235
|
-
catch (parseErr) {
|
|
236
|
-
console.error(`[Watcher] ❌ Cannot parse "${filename}" after duplicate-ID removal: ${parseErr.message}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return; // Stop processing this file as it's being modified
|
|
240
|
-
// Don't return - continue processing as normal
|
|
241
|
-
// The unlink event should come soon and trigger rename detection
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
// IMPORTANT: Hash is calculated on the SANITIZED version
|
|
246
|
-
// This means versionId, versionCounter, pinData, etc. are ignored
|
|
247
|
-
// The file on disk can contain these fields, but they won't affect the hash
|
|
248
|
-
const tsContent = fs.readFileSync(filePath, 'utf-8');
|
|
249
|
-
let hash;
|
|
250
|
-
try {
|
|
251
|
-
hash = await WorkflowTransformerAdapter.hashWorkflow(tsContent);
|
|
252
|
-
}
|
|
253
|
-
catch (parseErr) {
|
|
254
|
-
// Parsing failed (e.g. invalid identifier characters like → in the class name
|
|
255
|
-
// cause ts-morph to silently drop the class body, resulting in a 0-node compile
|
|
256
|
-
// which would be mistaken as a local modification and pushed, wiping the remote).
|
|
257
|
-
// Log the problem and abort – do NOT update localHashes so the file is not pushed.
|
|
258
|
-
console.error(`[Watcher] ❌ Cannot parse "${filename}" – skipping hash/status update to prevent data loss.\n` +
|
|
259
|
-
` Cause: ${parseErr.message}\n` +
|
|
260
|
-
` Tip: Make sure the class name contains only valid ASCII/identifier characters ` +
|
|
261
|
-
`(→ U+2192 and similar symbols are not allowed in TypeScript identifiers).`);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
console.log(`[Watcher] 🔢 Hash computed for ${filename}: ${hash.substring(0, 8)}...`);
|
|
265
|
-
this.localHashes.set(filename, hash);
|
|
266
|
-
if (workflowId) {
|
|
267
|
-
this.fileToIdMap.set(filename, workflowId);
|
|
268
|
-
this.idToFileMap.set(workflowId, filename);
|
|
269
|
-
}
|
|
270
|
-
console.log(`[Watcher] 📡 Broadcasting status for ${filename}...`);
|
|
271
|
-
this.broadcastStatus(filename, workflowId);
|
|
272
|
-
}
|
|
273
|
-
async onLocalDelete(filePath) {
|
|
274
|
-
const filename = path.basename(filePath);
|
|
275
|
-
let workflowId = this.fileToIdMap.get(filename);
|
|
276
|
-
// If workflowId not found via filename mapping, try to find it via state
|
|
277
|
-
if (!workflowId) {
|
|
278
|
-
const state = this.loadState();
|
|
279
|
-
for (const [id, stateData] of Object.entries(state.workflows)) {
|
|
280
|
-
const mappedFilename = this.idToFileMap.get(id);
|
|
281
|
-
if (mappedFilename === filename) {
|
|
282
|
-
workflowId = id;
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
// Check if this is a potential rename (add happened before unlink)
|
|
288
|
-
if (workflowId) {
|
|
289
|
-
const potentialRename = this.potentialRenames.get(workflowId);
|
|
290
|
-
if (potentialRename) {
|
|
291
|
-
this.potentialRenames.delete(workflowId);
|
|
292
|
-
// Handle as rename
|
|
293
|
-
this.handleRename(workflowId, filename, potentialRename.newFilename);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (workflowId && (this.isPaused.has(workflowId) || this.syncInProgress.has(workflowId))) {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
// Handle deletion directly
|
|
301
|
-
await this.handleLocalDelete(filename, workflowId);
|
|
302
|
-
}
|
|
303
|
-
async handleLocalDelete(filename, workflowId) {
|
|
304
|
-
// Final check: is this actually a rename?
|
|
305
|
-
if (workflowId) {
|
|
306
|
-
// Check if the workflow ID appears in another file
|
|
307
|
-
const otherFilename = this.findFilenameByWorkflowId(workflowId);
|
|
308
|
-
if (otherFilename && otherFilename !== filename) {
|
|
309
|
-
// This is a rename, not a deletion!
|
|
310
|
-
this.handleRename(workflowId, filename, otherFilename);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
// When a local file is deleted we simply clear the lastSyncedHash from state
|
|
315
|
-
// so that calculateStatus() naturally returns EXIST_ONLY_REMOTELY
|
|
316
|
-
// (remoteHash present, no lastSyncedHash, no localHash).
|
|
317
|
-
// No archiving is needed here – the remote copy is untouched.
|
|
318
|
-
if (workflowId) {
|
|
319
|
-
const state = this.loadState();
|
|
320
|
-
if (state.workflows[workflowId]) {
|
|
321
|
-
state.workflows[workflowId].lastSyncedHash = undefined;
|
|
322
|
-
this.saveState(state);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
// Clean up local hash for deleted file
|
|
326
|
-
this.localHashes.delete(filename);
|
|
327
|
-
// Broadcast the new status (EXIST_ONLY_REMOTELY if remote exists, or gone entirely)
|
|
328
|
-
this.broadcastStatus(filename, workflowId);
|
|
329
|
-
// CRITICAL: DO NOT delete ID→filename mappings
|
|
330
|
-
// Mappings must persist so the workflow re-appears with the same filename
|
|
331
|
-
// when it is pulled again. Mappings are only cleaned up via removeWorkflowState().
|
|
332
|
-
}
|
|
333
|
-
handleRename(workflowId, oldFilename, newFilename) {
|
|
334
|
-
// Update mappings
|
|
335
|
-
this.fileToIdMap.delete(oldFilename);
|
|
336
|
-
this.fileToIdMap.set(newFilename, workflowId);
|
|
337
|
-
this.idToFileMap.set(workflowId, newFilename);
|
|
338
|
-
// Update local hash mapping
|
|
339
|
-
const oldHash = this.localHashes.get(oldFilename);
|
|
340
|
-
if (oldHash) {
|
|
341
|
-
this.localHashes.delete(oldFilename);
|
|
342
|
-
this.localHashes.set(newFilename, oldHash);
|
|
343
|
-
}
|
|
344
|
-
// PERSIST: Update filename in state to prevent "ghost" workflows after restart
|
|
345
|
-
if (workflowId) {
|
|
346
|
-
const state = this.loadState();
|
|
347
|
-
if (state.workflows[workflowId]) {
|
|
348
|
-
state.workflows[workflowId].filename = newFilename;
|
|
349
|
-
this.saveState(state);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
// Emit rename event
|
|
353
|
-
this.emit('fileRenamed', {
|
|
354
|
-
workflowId,
|
|
355
|
-
oldFilename,
|
|
356
|
-
newFilename
|
|
357
|
-
});
|
|
358
|
-
// Broadcast status with new filename
|
|
359
|
-
this.broadcastStatus(newFilename, workflowId);
|
|
360
|
-
// Also broadcast status for old filename to clear it from UI
|
|
361
|
-
// Since it's no longer in localHashes or mappings, it will be handled correctly
|
|
362
|
-
this.broadcastStatus(oldFilename, undefined);
|
|
363
|
-
}
|
|
364
57
|
async refreshLocalState() {
|
|
365
58
|
if (!fs.existsSync(this.directory)) {
|
|
366
59
|
console.log(`[DEBUG] refreshLocalState: Directory missing: ${this.directory}`);
|
|
@@ -388,8 +81,7 @@ export class Watcher extends EventEmitter {
|
|
|
388
81
|
const filePath = path.join(this.directory, filename);
|
|
389
82
|
const content = this.readJsonFile(filePath); // Quick ID extraction
|
|
390
83
|
if (content) {
|
|
391
|
-
|
|
392
|
-
fileContents.push({ filename, content, mtime: stat.mtimeMs });
|
|
84
|
+
fileContents.push({ filename, content });
|
|
393
85
|
// Compute hash from TypeScript file directly
|
|
394
86
|
const tsContent = fs.readFileSync(filePath, 'utf-8');
|
|
395
87
|
try {
|
|
@@ -400,7 +92,7 @@ export class Watcher extends EventEmitter {
|
|
|
400
92
|
newlyTracked.push(filename);
|
|
401
93
|
}
|
|
402
94
|
catch (parseErr) {
|
|
403
|
-
console.error(`[
|
|
95
|
+
console.error(`[WorkflowStateTracker] ❌ Cannot parse "${filename}" during local scan – skipping.\n` +
|
|
404
96
|
` Cause: ${parseErr.message}\n` +
|
|
405
97
|
` Tip: Make sure the class name contains only valid ASCII/identifier characters ` +
|
|
406
98
|
`(→ U+2192 and similar symbols are not allowed in TypeScript identifiers).`);
|
|
@@ -408,29 +100,44 @@ export class Watcher extends EventEmitter {
|
|
|
408
100
|
}
|
|
409
101
|
}
|
|
410
102
|
}
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
103
|
+
// Second pass: build file→ID mappings from actual file content (scan-wins).
|
|
104
|
+
//
|
|
105
|
+
// For IDs that exist on disk, the scan result is authoritative — this correctly
|
|
106
|
+
// handles renames (new filename contains the same @workflow({ id: "..." }) decorator).
|
|
107
|
+
// Mappings for remote-only workflows (set by fetch/updateSingleRemoteState and not
|
|
108
|
+
// present in local files) are left untouched.
|
|
109
|
+
//
|
|
110
|
+
// Duplicate ID handling (copy-paste, option A — no file modification):
|
|
111
|
+
// Sort claimants alphabetically → first file wins, others get no mapping
|
|
112
|
+
// → treated as EXIST_ONLY_LOCALLY, resolved by pushing (gets a new id)
|
|
113
|
+
const idClaims = new Map();
|
|
416
114
|
for (const { filename, content } of fileContents) {
|
|
417
115
|
if (content?.id) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
116
|
+
if (!idClaims.has(content.id))
|
|
117
|
+
idClaims.set(content.id, []);
|
|
118
|
+
idClaims.get(content.id).push(filename);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const [id, claimants] of idClaims) {
|
|
122
|
+
// Remove the stale filename entry for this ID before setting the scan result
|
|
123
|
+
const staleFilename = this.idToFileMap.get(id);
|
|
124
|
+
if (staleFilename) {
|
|
125
|
+
this.fileToIdMap.delete(staleFilename);
|
|
126
|
+
}
|
|
127
|
+
const sorted = [...claimants].sort();
|
|
128
|
+
const winner = sorted[0];
|
|
129
|
+
if (sorted.length > 1) {
|
|
130
|
+
console.warn(`[WorkflowStateTracker] ⚠️ Duplicate ID "${id}" in [${sorted.join(', ')}]` +
|
|
131
|
+
` → "${winner}" wins (alphabetical). Others treated as new workflows.`);
|
|
132
|
+
}
|
|
133
|
+
this.fileToIdMap.set(winner, id);
|
|
134
|
+
this.idToFileMap.set(id, winner);
|
|
135
|
+
}
|
|
136
|
+
// Clean up fileToIdMap entries for files that no longer exist on disk.
|
|
137
|
+
// (idToFileMap for deleted-locally workflows is intentionally kept for EXIST_ONLY_REMOTELY.)
|
|
138
|
+
for (const existingFilename of Array.from(this.fileToIdMap.keys())) {
|
|
139
|
+
if (!currentFiles.has(existingFilename)) {
|
|
140
|
+
this.fileToIdMap.delete(existingFilename);
|
|
434
141
|
}
|
|
435
142
|
}
|
|
436
143
|
// Broadcast status for newly-tracked files (includes ID-less local-only files)
|
|
@@ -441,51 +148,6 @@ export class Watcher extends EventEmitter {
|
|
|
441
148
|
this.broadcastStatus(filename, workflowId);
|
|
442
149
|
}
|
|
443
150
|
}
|
|
444
|
-
/**
|
|
445
|
-
* Resolve duplicate IDs in local files (following architectural plan)
|
|
446
|
-
* Principle: Keep ID only in the oldest file, remove from others
|
|
447
|
-
*/
|
|
448
|
-
async resolveDuplicateIds(fileContents) {
|
|
449
|
-
// Group files by workflow ID
|
|
450
|
-
const filesById = new Map();
|
|
451
|
-
for (const { filename, content, mtime } of fileContents) {
|
|
452
|
-
if (content?.id) {
|
|
453
|
-
const workflowId = content.id;
|
|
454
|
-
if (!filesById.has(workflowId)) {
|
|
455
|
-
filesById.set(workflowId, []);
|
|
456
|
-
}
|
|
457
|
-
filesById.get(workflowId).push({ filename, mtime });
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
// For each duplicate ID, keep only in oldest file
|
|
461
|
-
for (const [workflowId, fileList] of filesById.entries()) {
|
|
462
|
-
if (fileList.length > 1) {
|
|
463
|
-
// Sort by modification time (oldest first)
|
|
464
|
-
fileList.sort((a, b) => a.mtime - b.mtime);
|
|
465
|
-
const oldestFile = fileList[0].filename;
|
|
466
|
-
const duplicates = fileList.slice(1);
|
|
467
|
-
// Remove ID from duplicate files
|
|
468
|
-
for (const { filename: dupFilename } of duplicates) {
|
|
469
|
-
const filePath = path.join(this.directory, dupFilename);
|
|
470
|
-
const content = this.readJsonFile(filePath);
|
|
471
|
-
if (content) {
|
|
472
|
-
delete content.id;
|
|
473
|
-
await this.writeWorkflowFile(dupFilename, content);
|
|
474
|
-
// Update local hash for the modified file
|
|
475
|
-
const tsContent = fs.readFileSync(filePath, 'utf-8');
|
|
476
|
-
const hash = await WorkflowTransformerAdapter.hashWorkflow(tsContent);
|
|
477
|
-
this.localHashes.set(dupFilename, hash);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
// Emit event for UI (if needed)
|
|
481
|
-
this.emit('duplicateIdResolved', {
|
|
482
|
-
workflowId,
|
|
483
|
-
keptInFilename: oldestFile,
|
|
484
|
-
removedFromFilenames: duplicates.map(d => d.filename)
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
151
|
/**
|
|
490
152
|
* Lightweight fetch strategy:
|
|
491
153
|
* 1. Fetch only IDs and updatedAt timestamps
|
|
@@ -512,8 +174,6 @@ export class Watcher extends EventEmitter {
|
|
|
512
174
|
for (const wf of remoteWorkflows) {
|
|
513
175
|
if (this.shouldIgnore(wf))
|
|
514
176
|
continue;
|
|
515
|
-
if (this.isPaused.has(wf.id) || this.syncInProgress.has(wf.id))
|
|
516
|
-
continue;
|
|
517
177
|
this.remoteIds.add(wf.id);
|
|
518
178
|
// Store canonical name keyed by ID (names are NOT unique in n8n)
|
|
519
179
|
if (wf.name)
|
|
@@ -550,16 +210,7 @@ export class Watcher extends EventEmitter {
|
|
|
550
210
|
// New workflow - establish mapping
|
|
551
211
|
this.idToFileMap.set(wf.id, filename);
|
|
552
212
|
this.fileToIdMap.set(filename, wf.id);
|
|
553
|
-
//
|
|
554
|
-
const state = this.loadState();
|
|
555
|
-
if (!state.workflows[wf.id]) {
|
|
556
|
-
state.workflows[wf.id] = {
|
|
557
|
-
filename: filename,
|
|
558
|
-
lastSyncedHash: undefined,
|
|
559
|
-
lastSyncedAt: undefined
|
|
560
|
-
};
|
|
561
|
-
this.saveState(state);
|
|
562
|
-
}
|
|
213
|
+
// No longer persist filename to state (mappings are rebuilt from file scan).
|
|
563
214
|
}
|
|
564
215
|
else if (previousFilename !== filename) {
|
|
565
216
|
// Filename changed
|
|
@@ -665,11 +316,9 @@ export class Watcher extends EventEmitter {
|
|
|
665
316
|
*/
|
|
666
317
|
async updateWorkflowState(id, hash, remoteUpdatedAt) {
|
|
667
318
|
const state = this.loadState();
|
|
668
|
-
const filename = this.idToFileMap.get(id) || '';
|
|
669
319
|
state.workflows[id] = {
|
|
670
320
|
lastSyncedHash: hash,
|
|
671
|
-
lastSyncedAt: remoteUpdatedAt || new Date().toISOString()
|
|
672
|
-
filename: filename
|
|
321
|
+
lastSyncedAt: remoteUpdatedAt || new Date().toISOString()
|
|
673
322
|
};
|
|
674
323
|
this.saveState(state);
|
|
675
324
|
}
|
|
@@ -712,21 +361,12 @@ export class Watcher extends EventEmitter {
|
|
|
712
361
|
return { workflows: {} };
|
|
713
362
|
}
|
|
714
363
|
/**
|
|
715
|
-
*
|
|
716
|
-
*
|
|
364
|
+
* No-op: file→ID mappings are now built exclusively by refreshLocalState()
|
|
365
|
+
* which scans the @workflow({ id: "..." }) decorator in each *.workflow.ts.
|
|
366
|
+
* This guarantees correct reconciliation after a local rename.
|
|
717
367
|
*/
|
|
718
368
|
restoreMappingsFromState() {
|
|
719
|
-
|
|
720
|
-
for (const [id, workflowState] of Object.entries(state.workflows)) {
|
|
721
|
-
const ws = workflowState;
|
|
722
|
-
if (ws.filename) {
|
|
723
|
-
// Only set if not already mapped (current session takes precedence)
|
|
724
|
-
if (!this.idToFileMap.has(id)) {
|
|
725
|
-
this.idToFileMap.set(id, ws.filename);
|
|
726
|
-
this.fileToIdMap.set(ws.filename, id);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
369
|
+
// Intentionally empty — mappings are built by refreshLocalState() scan.
|
|
730
370
|
}
|
|
731
371
|
/**
|
|
732
372
|
* Save state to .n8n-state.json
|
|
@@ -741,17 +381,15 @@ export class Watcher extends EventEmitter {
|
|
|
741
381
|
return HashUtils.computeHash(content);
|
|
742
382
|
}
|
|
743
383
|
broadcastStatus(filename, workflowId) {
|
|
744
|
-
|
|
745
|
-
return;
|
|
746
|
-
// Suppress during initial remote load — avoids spurious "Change detected" on startup
|
|
384
|
+
// Suppress during initial remote load — avoids spurious “Change detected” on startup
|
|
747
385
|
if (this.isInitialRemoteLoad)
|
|
748
386
|
return;
|
|
749
387
|
const status = this.calculateStatus(filename, workflowId);
|
|
750
388
|
const key = workflowId || filename;
|
|
751
389
|
const lastStatus = this.lastKnownStatuses.get(key);
|
|
752
|
-
console.log(`[
|
|
390
|
+
console.log(`[WorkflowStateTracker] Status for ${filename}: ${status} (last: ${lastStatus || 'none'})`);
|
|
753
391
|
if (status !== lastStatus) {
|
|
754
|
-
console.log(`[
|
|
392
|
+
console.log(`[WorkflowStateTracker] 🔔 Status changed! Emitting statusChange event`);
|
|
755
393
|
this.lastKnownStatuses.set(key, status);
|
|
756
394
|
this.emit('statusChange', {
|
|
757
395
|
filename,
|
|
@@ -760,7 +398,7 @@ export class Watcher extends EventEmitter {
|
|
|
760
398
|
});
|
|
761
399
|
}
|
|
762
400
|
else {
|
|
763
|
-
console.log(`[
|
|
401
|
+
console.log(`[WorkflowStateTracker] Status unchanged, not emitting event`);
|
|
764
402
|
}
|
|
765
403
|
}
|
|
766
404
|
calculateStatus(filename, workflowId) {
|
|
@@ -779,7 +417,7 @@ export class Watcher extends EventEmitter {
|
|
|
779
417
|
const lastSyncedHash = baseState?.lastSyncedHash;
|
|
780
418
|
// Debug logging for new files
|
|
781
419
|
if (!workflowId && localHash) {
|
|
782
|
-
console.log(`[
|
|
420
|
+
console.log(`[WorkflowStateTracker] 🆕 calculateStatus for NEW file: ${filename}`);
|
|
783
421
|
console.log(` localHash: ${localHash ? localHash.substring(0, 8) : 'none'}`);
|
|
784
422
|
console.log(` lastSyncedHash: ${lastSyncedHash ? lastSyncedHash.substring(0, 8) : 'none'}`);
|
|
785
423
|
console.log(` remoteHash: ${remoteHash ? remoteHash.substring(0, 8) : 'none'}`);
|
|
@@ -797,22 +435,12 @@ export class Watcher extends EventEmitter {
|
|
|
797
435
|
const remoteModified = remoteHash && remoteHash !== lastSyncedHash;
|
|
798
436
|
if (localModified && remoteModified)
|
|
799
437
|
return WorkflowSyncStatus.CONFLICT;
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
// Remote updated but local untouched — treat as TRACKED, user can pull explicitly.
|
|
804
|
-
if (remoteModified && localHash === lastSyncedHash)
|
|
805
|
-
return WorkflowSyncStatus.TRACKED;
|
|
806
|
-
// localUnchanged && remoteHash unknown (not yet fetched by this watcher instance):
|
|
807
|
-
// local matches last synced state — safe to report TRACKED.
|
|
808
|
-
// This prevents a spurious CONFLICT when an external process (CLI, another SyncManager
|
|
809
|
-
// instance) pulls a workflow and writes lastSyncedHash to state while this watcher has
|
|
810
|
-
// never fetched the remote hash for the workflow (remoteHashes cache is empty here).
|
|
811
|
-
if (!localModified)
|
|
812
|
-
return WorkflowSyncStatus.TRACKED;
|
|
438
|
+
// All other combinations (single-side or no modification) → TRACKED.
|
|
439
|
+
// Pull guards detect local modifications via direct hash comparison.
|
|
440
|
+
return WorkflowSyncStatus.TRACKED;
|
|
813
441
|
}
|
|
814
442
|
// Fallback for edge cases
|
|
815
|
-
console.warn(`[
|
|
443
|
+
console.warn(`[WorkflowStateTracker] ⚠️ CONFLICT fallback for ${filename}:`, { localHash: !!localHash, remoteHash: !!remoteHash, lastSyncedHash: !!lastSyncedHash, workflowId });
|
|
816
444
|
return WorkflowSyncStatus.CONFLICT;
|
|
817
445
|
}
|
|
818
446
|
shouldIgnore(wf) {
|
|
@@ -910,15 +538,6 @@ export class Watcher extends EventEmitter {
|
|
|
910
538
|
return null;
|
|
911
539
|
}
|
|
912
540
|
}
|
|
913
|
-
async writeWorkflowFile(filename, workflow) {
|
|
914
|
-
const filePath = path.join(this.directory, filename);
|
|
915
|
-
// Always write as TypeScript
|
|
916
|
-
const tsCode = await WorkflowTransformerAdapter.convertToTypeScript(workflow, {
|
|
917
|
-
format: true,
|
|
918
|
-
commentStyle: 'verbose'
|
|
919
|
-
});
|
|
920
|
-
fs.writeFileSync(filePath, tsCode, 'utf-8');
|
|
921
|
-
}
|
|
922
541
|
getFileToIdMap() {
|
|
923
542
|
return this.fileToIdMap;
|
|
924
543
|
}
|
|
@@ -931,7 +550,7 @@ export class Watcher extends EventEmitter {
|
|
|
931
550
|
}
|
|
932
551
|
/**
|
|
933
552
|
* Lightweight list of workflows with basic status (local only, remote only, both)
|
|
934
|
-
* Does NOT compute hashes, compile TypeScript, or determine detailed status (
|
|
553
|
+
* Does NOT compute hashes, compile TypeScript, or determine detailed status (CONFLICT)
|
|
935
554
|
*/
|
|
936
555
|
async getLightweightList() {
|
|
937
556
|
const results = new Map();
|
|
@@ -939,17 +558,23 @@ export class Watcher extends EventEmitter {
|
|
|
939
558
|
// 1. Process all local files (just check existence, no hash computation)
|
|
940
559
|
for (const filename of this.getLocalWorkflowFilenames()) {
|
|
941
560
|
const workflowId = this.fileToIdMap.get(filename);
|
|
942
|
-
|
|
561
|
+
// A workflow is considered "known on remote" if:
|
|
562
|
+
// a) remoteIds has it (populated by refreshRemoteState)
|
|
563
|
+
// b) remoteHashes has it (populated by finalizeSync after push/pull)
|
|
564
|
+
// c) lastSyncedHash exists in state (written by any CLI/VSCode process,
|
|
565
|
+
// survives cross-process CLI operations like resolve)
|
|
566
|
+
const remoteKnown = workflowId
|
|
567
|
+
? (this.remoteIds.has(workflowId)
|
|
568
|
+
|| this.remoteHashes.has(workflowId)
|
|
569
|
+
|| !!state.workflows[workflowId]?.lastSyncedHash)
|
|
570
|
+
: false;
|
|
943
571
|
// Determine basic status
|
|
944
572
|
let status;
|
|
945
|
-
if (workflowId &&
|
|
573
|
+
if (workflowId && remoteKnown) {
|
|
946
574
|
status = WorkflowSyncStatus.TRACKED; // Both exist
|
|
947
575
|
}
|
|
948
|
-
else if (workflowId && !remoteExists) {
|
|
949
|
-
status = WorkflowSyncStatus.EXIST_ONLY_LOCALLY; // Local only (but has ID)
|
|
950
|
-
}
|
|
951
576
|
else {
|
|
952
|
-
status = WorkflowSyncStatus.EXIST_ONLY_LOCALLY; // New
|
|
577
|
+
status = WorkflowSyncStatus.EXIST_ONLY_LOCALLY; // New or not yet pushed
|
|
953
578
|
}
|
|
954
579
|
// Prefer the remote canonical name (keyed by ID, not by name since names are non-unique).
|
|
955
580
|
// For local-only files, extract the name from the @workflow decorator for an accurate display.
|
|
@@ -971,9 +596,11 @@ export class Watcher extends EventEmitter {
|
|
|
971
596
|
}
|
|
972
597
|
// 2. Process all remote workflows not yet in results
|
|
973
598
|
for (const workflowId of this.remoteIds) {
|
|
974
|
-
//
|
|
975
|
-
|
|
976
|
-
const filename =
|
|
599
|
+
// Scan-wins: idToFileMap (rebuilt from @workflow decorator) is authoritative.
|
|
600
|
+
// Fall back to deprecated persisted filename for old state files during transition.
|
|
601
|
+
const filename = this.idToFileMap.get(workflowId)
|
|
602
|
+
|| state.workflows[workflowId]?.filename
|
|
603
|
+
|| `${workflowId}.workflow.ts`;
|
|
977
604
|
if (!results.has(filename)) {
|
|
978
605
|
// Prefer the actual remote name (stored by ID to avoid name-collision issues)
|
|
979
606
|
// Fallback to filename-derived name only if remote name is not available
|
|
@@ -1007,7 +634,7 @@ export class Watcher extends EventEmitter {
|
|
|
1007
634
|
}
|
|
1008
635
|
}
|
|
1009
636
|
catch (error) {
|
|
1010
|
-
console.debug('[
|
|
637
|
+
console.debug('[WorkflowStateTracker] Failed to read local directory:', error);
|
|
1011
638
|
}
|
|
1012
639
|
return filenames;
|
|
1013
640
|
}
|
|
@@ -1031,13 +658,13 @@ export class Watcher extends EventEmitter {
|
|
|
1031
658
|
}
|
|
1032
659
|
}
|
|
1033
660
|
catch (e) {
|
|
1034
|
-
console.warn(`[
|
|
661
|
+
console.warn(`[WorkflowStateTracker] Failed to parse local workflow ${filename}:`, e);
|
|
1035
662
|
}
|
|
1036
663
|
}
|
|
1037
664
|
}
|
|
1038
665
|
}
|
|
1039
666
|
catch (error) {
|
|
1040
|
-
console.debug('[
|
|
667
|
+
console.debug('[WorkflowStateTracker] Failed to load workflow metadata for status matrix:', error);
|
|
1041
668
|
}
|
|
1042
669
|
// 1. Process all local files
|
|
1043
670
|
for (const [filename, hash] of this.localHashes.entries()) {
|
|
@@ -1058,9 +685,11 @@ export class Watcher extends EventEmitter {
|
|
|
1058
685
|
}
|
|
1059
686
|
// 2. Process all remote workflows not yet in results
|
|
1060
687
|
for (const [workflowId, remoteHash] of this.remoteHashes.entries()) {
|
|
1061
|
-
//
|
|
1062
|
-
|
|
1063
|
-
const filename =
|
|
688
|
+
// Scan-wins: idToFileMap (rebuilt from @workflow decorator) is authoritative.
|
|
689
|
+
// Fall back to deprecated persisted filename for old state files during transition.
|
|
690
|
+
const filename = this.idToFileMap.get(workflowId)
|
|
691
|
+
|| state.workflows[workflowId]?.filename
|
|
692
|
+
|| `${workflowId}.workflow.ts`;
|
|
1064
693
|
if (!results.has(filename)) {
|
|
1065
694
|
const status = this.calculateStatus(filename, workflowId);
|
|
1066
695
|
const workflow = workflowsMap.get(workflowId);
|
|
@@ -1079,9 +708,11 @@ export class Watcher extends EventEmitter {
|
|
|
1079
708
|
}
|
|
1080
709
|
// 3. Process tracked but deleted workflows
|
|
1081
710
|
for (const id of Object.keys(state.workflows)) {
|
|
1082
|
-
//
|
|
1083
|
-
|
|
1084
|
-
const filename =
|
|
711
|
+
// Scan-wins: idToFileMap (rebuilt from @workflow decorator) is authoritative.
|
|
712
|
+
// Fall back to deprecated persisted filename for old state files during transition.
|
|
713
|
+
const filename = this.idToFileMap.get(id)
|
|
714
|
+
|| state.workflows[id]?.filename
|
|
715
|
+
|| `${id}.workflow.ts`;
|
|
1085
716
|
if (!results.has(filename)) {
|
|
1086
717
|
const status = this.calculateStatus(filename, id);
|
|
1087
718
|
const workflow = workflowsMap.get(id);
|
|
@@ -1145,7 +776,7 @@ export class Watcher extends EventEmitter {
|
|
|
1145
776
|
}
|
|
1146
777
|
}
|
|
1147
778
|
catch (error) {
|
|
1148
|
-
console.warn(`[
|
|
779
|
+
console.warn(`[WorkflowStateTracker] Failed to read local workflow ${filename}:`, error);
|
|
1149
780
|
}
|
|
1150
781
|
}
|
|
1151
782
|
// 2. For remote-only workflows, fetch from API
|
|
@@ -1159,7 +790,7 @@ export class Watcher extends EventEmitter {
|
|
|
1159
790
|
}
|
|
1160
791
|
}
|
|
1161
792
|
catch (error) {
|
|
1162
|
-
console.warn(`[
|
|
793
|
+
console.warn(`[WorkflowStateTracker] Failed to fetch remote workflow ${workflowId}:`, error);
|
|
1163
794
|
}
|
|
1164
795
|
}
|
|
1165
796
|
}
|
|
@@ -1242,16 +873,7 @@ export class Watcher extends EventEmitter {
|
|
|
1242
873
|
}
|
|
1243
874
|
this.idToFileMap.set(remoteWf.id, filename);
|
|
1244
875
|
this.fileToIdMap.set(filename, remoteWf.id);
|
|
1245
|
-
//
|
|
1246
|
-
const state = this.loadState();
|
|
1247
|
-
if (!state.workflows[remoteWf.id]) {
|
|
1248
|
-
state.workflows[remoteWf.id] = {
|
|
1249
|
-
filename: filename,
|
|
1250
|
-
lastSyncedHash: undefined,
|
|
1251
|
-
lastSyncedAt: undefined
|
|
1252
|
-
};
|
|
1253
|
-
this.saveState(state);
|
|
1254
|
-
}
|
|
876
|
+
// No longer persist filename to state (mappings are rebuilt from file scan).
|
|
1255
877
|
}
|
|
1256
878
|
// Broadcast status update
|
|
1257
879
|
const filename = this.idToFileMap.get(remoteWf.id);
|
|
@@ -1260,8 +882,8 @@ export class Watcher extends EventEmitter {
|
|
|
1260
882
|
}
|
|
1261
883
|
}
|
|
1262
884
|
catch (error) {
|
|
1263
|
-
console.error(`[
|
|
885
|
+
console.error(`[WorkflowStateTracker] Failed to update single remote state for ${remoteWf.id}:`, error);
|
|
1264
886
|
}
|
|
1265
887
|
}
|
|
1266
888
|
}
|
|
1267
|
-
//# sourceMappingURL=
|
|
889
|
+
//# sourceMappingURL=workflow-state-tracker.js.map
|