Setting the file. One moment. Progress Log · Wix Replatform · wix/skills · Skills Docsfunction error
— line 125
This file
- Number
- 47.27
- Position
- 27 of 34
- Type
- JavaScript
- Size
- 5 KB
- Lines
- 224
lib/progress-log.js
JavaScript·224 lines·5 KB
,
'warning'
]);
8
9function slugify(value) {
10 return String(value || 'script')
11 .toLowerCase()
12 .replace(/[^a-z0-9]+/g, '-')
13 .replace(/^-+|-+$/g, '') || 'script';
14}
15
16function defaultRunId(script) {
17 return `${slugify(script)}-${Date.now()}-${crypto.randomBytes(2).toString('hex')}`;
18}
19
20function parseProgressArgs(argv, env = process.env) {
21 const args = [];
22 const progress = {
23 progressLog: env.PROGRESS_LOG || null,
24 runId: env.RUN_ID || null,
25 parentRunId: env.PARENT_RUN_ID || null,
26 };
27
28 for (let i = 0; i < argv.length; i += 1) {
29 const arg = argv[i];
30 if (arg === '--progress-log') {
31 progress.progressLog = argv[i + 1];
32 i += 1;
33 } else if (arg === '--run-id') {
34 progress.runId = argv[i + 1];
35 i += 1;
36 } else if (arg === '--parent-run-id') {
37 progress.parentRunId = argv[i + 1];
38 i += 1;
39 } else {
40 args.push(arg);
41 }
42 }
43
44 return { args, progress };
45}
46
47function statusForEvent(event) {
48 if (event === 'complete') {
49 return 'completed';
50 }
51 if (event === 'error') {
52 return 'failed';
53 }
54 if (RUNNING_EVENTS.has(event)) {
55 return 'running';
56 }
57 throw new Error(`unknown progress event: ${event}`);
58}
59
60function createProgressLogger(options = {}) {
61 const progressLog = options.progressLog || null;
62 const script = options.script || 'script';
63 const runId = options.runId || defaultRunId(script);
64 const parentRunId = options.parentRunId || null;
65 const heartbeatIntervalMs = options.heartbeatIntervalMs || 30000;
66
67 let seq = 0;
68 let started = false;
69 let terminal = false;
70 let lastProgressAt = 0;
71 let heartbeatTimer = null;
72 let heartbeatContext = null;
73
74 function enabled() {
75 return Boolean(progressLog);
76 }
77
78 function append(event, message, fields = {}) {
79 if (!enabled()) {
80 return null;
81 }
82 fs.mkdirSync(path.dirname(progressLog), { recursive: true });
83 seq += 1;
84 const record = {
85 ts: new Date().toISOString(),
86 runId,
87 ...(parentRunId ? { parentRunId } : {}),
88 script,
89 event,
90 status: statusForEvent(event),
91 seq,
92 ...fields,
93 message: String(message || event),
94 };
95 fs.appendFileSync(progressLog, `${JSON.stringify(record)}\n`, { encoding: 'utf8', flag: 'a' });
96 if (event !== 'heartbeat') {
97 lastProgressAt = Date.now();
98 }
99 if (event === 'start') {
100 started = true;
101 }
102 if (event === 'complete' || event === 'error') {
103 terminal = true;
104 stopHeartbeat();
105 }
106 return record;
107 }
108
109 function start(message, fields = {}) {
110 return append('start', message, fields);
111 }
112
113 function progress(message, fields = {}) {
114 return append('progress', message, fields);
115 }
116
117 function heartbeat(message, fields = {}) {
118 return append('heartbeat', message, fields);
119 }
120
121 function warn(message, fields = {}) {
122 return append('warning', message, fields);
123 }
124
125 function error(message, fields = {}) {
126 return append('error', message, fields);
127 }
128
129 function complete(message, fields = {}) {
130 return append('complete', message, fields);
131 }
132
133 function startHeartbeat(fields = {}) {
134 if (!enabled() || heartbeatTimer) {
135 return;
136 }
137 heartbeatContext = fields;
138 heartbeatTimer = setInterval(() => {
139 if (terminal) {
140 stopHeartbeat();
141 return;
142 }
143 const elapsed = Date.now() - lastProgressAt;
144 if (elapsed >= heartbeatIntervalMs) {
145 heartbeat(fields.message || 'Still running', fields);
146 }
147 }, Math.max(1000, Math.min(heartbeatIntervalMs, 5000)));
148 heartbeatTimer.unref?.();
149 }
150
151 function stopHeartbeat() {
152 if (heartbeatTimer) {
153 clearInterval(heartbeatTimer);
154 heartbeatTimer = null;
155 heartbeatContext = null;
156 }
157 }
158
159 async function withHeartbeat(fields, fn) {
160 startHeartbeat(fields);
161 try {
162 return await fn();
163 } finally {
164 stopHeartbeat();
165 }
166 }
167
168 function childEnv(extra = {}) {
169 return {
170 ...extra,
171 ...(progressLog ? { PROGRESS_LOG: progressLog, PARENT_RUN_ID: runId } : {}),
172 };
173 }
174
175 function installTerminalHandlers() {
176 if (!enabled()) {
177 return;
178 }
179 process.on('exit', (code) => {
180 if (started && !terminal && code !== 0) {
181 append('error', `Process exited before writing a terminal progress record`, {
182 exitCode: code,
183 ...(heartbeatContext || {}),
184 });
185 }
186 });
187 process.on('uncaughtException', (err) => {
188 if (started && !terminal) {
189 error(err && err.message ? err.message : 'Uncaught exception', { errorName: err && err.name });
190 }
191 throw err;
192 });
193 process.on('unhandledRejection', (reason) => {
194 if (started && !terminal) {
195 error(reason && reason.message ? reason.message : 'Unhandled rejection', { errorName: reason && reason.name });
196 }
197 });
198 }
199
200 installTerminalHandlers();
201
202 return {
203 progressLog,
204 script,
205 runId,
206 parentRunId,
207 enabled,
208 start,
209 progress,
210 heartbeat,
211 warn,
212 error,
213 complete,
214 startHeartbeat,
215 stopHeartbeat,
216 withHeartbeat,
217 childEnv,
218 };
219}
220
221module.exports = {
222 createProgressLogger,
223 parseProgressArgs,
224};