@fnet/cli 0.3.11 → 0.3.12
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/package.json
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
{% include "src/default/macros/block-header.js.njk" %}
|
|
2
|
+
|
|
3
|
+
{% include "src/default/macros/block-next-header.js.njk" %}
|
|
4
|
+
|
|
5
|
+
{% include "src/default/macros/block-modules-header.js.njk" %}
|
|
6
|
+
|
|
7
|
+
export default function Block(context){
|
|
8
|
+
|
|
9
|
+
{% include "src/default/macros/block-body-header.js.njk" %}
|
|
10
|
+
|
|
11
|
+
this.run= function (){
|
|
12
|
+
|
|
13
|
+
{% include "src/default/macros/block-entry-args.js.njk" %}
|
|
14
|
+
|
|
15
|
+
return new Promise(async (resolve,reject)=>{
|
|
16
|
+
{% include "src/default/macros/block-run-header.js.njk" %}
|
|
17
|
+
|
|
18
|
+
{% include "src/default/macros/page.js.njk" %}
|
|
19
|
+
|
|
20
|
+
{% include "src/default/macros/block-modules.js.njk" %}
|
|
21
|
+
|
|
22
|
+
// HTTP configuration
|
|
23
|
+
let url = {{ context.transform.http.url | safe }};
|
|
24
|
+
let method = "{{ context.transform.http.method }}";
|
|
25
|
+
let headers = {{ context.transform.http.headers | dump | safe }};
|
|
26
|
+
let timeout = {{ context.transform.http.timeout }};
|
|
27
|
+
{% if context.transform.http.body !== undefined %}
|
|
28
|
+
let body = {% if context.transform.http.body is string %}{{ context.transform.http.body | safe }}{% else %}{{ context.transform.http.body | dump | safe }}{% endif %};
|
|
29
|
+
{% endif %}
|
|
30
|
+
{% if context.transform.http.params %}
|
|
31
|
+
let params = {{ context.transform.http.params | dump | safe }};
|
|
32
|
+
{% endif %}
|
|
33
|
+
|
|
34
|
+
// Create HTTP context for modules
|
|
35
|
+
const httpContext = { url, method, headers, timeout, params: {% if context.transform.http.params %}params{% else %}undefined{% endif %}, body: {% if context.transform.http.body !== undefined %}body{% else %}undefined{% endif %} };
|
|
36
|
+
|
|
37
|
+
// If any property is a module function (m::), call it with HTTP context
|
|
38
|
+
if (typeof url === 'function') {
|
|
39
|
+
url = await url(httpContext);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typeof method === 'function') {
|
|
43
|
+
method = await method(httpContext);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof headers === 'function') {
|
|
47
|
+
headers = await headers(httpContext);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof timeout === 'function') {
|
|
51
|
+
timeout = await timeout(httpContext);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
{% if context.transform.http.params %}
|
|
55
|
+
if (typeof params === 'function') {
|
|
56
|
+
params = await params(httpContext);
|
|
57
|
+
}
|
|
58
|
+
{% endif %}
|
|
59
|
+
|
|
60
|
+
{% if context.transform.http.body !== undefined %}
|
|
61
|
+
if (typeof body === 'function') {
|
|
62
|
+
body = await body(httpContext);
|
|
63
|
+
}
|
|
64
|
+
{% endif %}
|
|
65
|
+
|
|
66
|
+
// Build URL with query parameters if provided
|
|
67
|
+
let finalUrl = url;
|
|
68
|
+
{% if context.transform.http.params %}
|
|
69
|
+
if (params && Object.keys(params).length > 0) {
|
|
70
|
+
const queryString = new URLSearchParams(params).toString();
|
|
71
|
+
finalUrl = url + (url.includes('?') ? '&' : '?') + queryString;
|
|
72
|
+
}
|
|
73
|
+
{% endif %}
|
|
74
|
+
|
|
75
|
+
// Prepare fetch options
|
|
76
|
+
const fetchOptions = {
|
|
77
|
+
method: method,
|
|
78
|
+
headers: headers
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Add body if present (and not GET/HEAD)
|
|
82
|
+
{% if context.transform.http.body %}
|
|
83
|
+
if (body !== undefined && method !== 'GET' && method !== 'HEAD') {
|
|
84
|
+
// Auto JSON.stringify if body is object
|
|
85
|
+
if (typeof body === 'object' && body !== null) {
|
|
86
|
+
fetchOptions.body = JSON.stringify(body);
|
|
87
|
+
// Auto set Content-Type if not already set
|
|
88
|
+
if (!fetchOptions.headers['Content-Type'] && !fetchOptions.headers['content-type']) {
|
|
89
|
+
fetchOptions.headers['Content-Type'] = 'application/json';
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
fetchOptions.body = body;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
{% endif %}
|
|
96
|
+
|
|
97
|
+
// Create abort controller for timeout
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
100
|
+
fetchOptions.signal = controller.signal;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Execute HTTP request
|
|
104
|
+
const response = await fetch(finalUrl, fetchOptions);
|
|
105
|
+
clearTimeout(timeoutId);
|
|
106
|
+
|
|
107
|
+
// Check if response is successful (2xx)
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Parse response based on Content-Type
|
|
113
|
+
const contentType = response.headers.get('content-type');
|
|
114
|
+
let result;
|
|
115
|
+
|
|
116
|
+
if (contentType && contentType.includes('application/json')) {
|
|
117
|
+
result = await response.json();
|
|
118
|
+
} else if (contentType && contentType.includes('text/')) {
|
|
119
|
+
result = await response.text();
|
|
120
|
+
} else {
|
|
121
|
+
// Try JSON first, fallback to text
|
|
122
|
+
try {
|
|
123
|
+
result = await response.json();
|
|
124
|
+
} catch (e) {
|
|
125
|
+
result = await response.text();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Store result in flow variable
|
|
130
|
+
flow.set('result', result);
|
|
131
|
+
|
|
132
|
+
{% include "src/default/macros/block-assign.js.njk" %}
|
|
133
|
+
|
|
134
|
+
} catch (error) {
|
|
135
|
+
clearTimeout(timeoutId);
|
|
136
|
+
|
|
137
|
+
// Handle timeout error
|
|
138
|
+
if (error.name === 'AbortError') {
|
|
139
|
+
const timeoutError = new Error(`HTTP request timeout after ${timeout}ms: ${finalUrl}`);
|
|
140
|
+
onError ? onError(timeoutError) : reject(timeoutError);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Handle other errors
|
|
145
|
+
onError ? onError(error) : reject(error);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
{% include "src/default/macros/block-next.js.njk" %}
|
|
150
|
+
|
|
151
|
+
{% include "src/default/macros/block-run-footer.js.njk" %}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
{% include "src/default/macros/block-footer.js.njk" %}
|
|
157
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{% include "src/default/macros/block-header.js.njk" %}
|
|
2
|
+
|
|
3
|
+
{% include "src/default/macros/block-next-header.js.njk" %}
|
|
4
|
+
|
|
5
|
+
{% include "src/default/macros/block-modules-header.js.njk" %}
|
|
6
|
+
|
|
7
|
+
{% for child in childs %}
|
|
8
|
+
{% if not child.definition.dynamic %}
|
|
9
|
+
// RETRYABLE CHILD: {{child.indexKey}}
|
|
10
|
+
import {{child.codeKey}} from "./{{child.codeKey}}.js";
|
|
11
|
+
{% endif %}
|
|
12
|
+
{% endfor %}
|
|
13
|
+
|
|
14
|
+
export default function Block(context){
|
|
15
|
+
|
|
16
|
+
{% include "src/default/macros/block-body-header.js.njk" %}
|
|
17
|
+
|
|
18
|
+
this.run= function (){
|
|
19
|
+
|
|
20
|
+
{% include "src/default/macros/block-entry-args.js.njk" %}
|
|
21
|
+
|
|
22
|
+
return new Promise(async (resolve,reject)=>{
|
|
23
|
+
{% include "src/default/macros/block-run-header.js.njk" %}
|
|
24
|
+
|
|
25
|
+
{% include "src/default/macros/page.js.njk" %}
|
|
26
|
+
|
|
27
|
+
// Retry configuration (always an object)
|
|
28
|
+
const attempts = {{ context.transform.retry.attempts }};
|
|
29
|
+
const initialDelay = {{ context.transform.retry.delay }};
|
|
30
|
+
const backoff = "{{ context.transform.retry.backoff }}";
|
|
31
|
+
const maxDelay = {% if context.transform.retry.maxDelay %}{{ context.transform.retry.maxDelay }}{% else %}null{% endif %};
|
|
32
|
+
|
|
33
|
+
{% if childs[0] %}
|
|
34
|
+
// Create child block instance
|
|
35
|
+
const retryableBlock = new {{childs[0].codeKey}}({
|
|
36
|
+
parent: _this,
|
|
37
|
+
engine: engine,
|
|
38
|
+
flow: flow
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Retry logic with backoff
|
|
42
|
+
let lastError;
|
|
43
|
+
let currentDelay = initialDelay;
|
|
44
|
+
|
|
45
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
46
|
+
try {
|
|
47
|
+
// Execute child block
|
|
48
|
+
await retryableBlock.run();
|
|
49
|
+
|
|
50
|
+
// Success! Break out of retry loop
|
|
51
|
+
break;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
lastError = error;
|
|
54
|
+
|
|
55
|
+
// Last attempt failed - throw error
|
|
56
|
+
if (attempt === attempts) {
|
|
57
|
+
throw new Error(`Retry failed after ${attempts} attempts: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Wait before retry
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, currentDelay));
|
|
62
|
+
|
|
63
|
+
// Calculate next delay based on backoff strategy
|
|
64
|
+
if (backoff === 'exponential') {
|
|
65
|
+
currentDelay = currentDelay * 2;
|
|
66
|
+
} else if (backoff === 'linear') {
|
|
67
|
+
currentDelay = currentDelay + initialDelay;
|
|
68
|
+
}
|
|
69
|
+
// 'fixed' keeps currentDelay unchanged
|
|
70
|
+
|
|
71
|
+
// Apply maxDelay cap if specified
|
|
72
|
+
if (maxDelay && currentDelay > maxDelay) {
|
|
73
|
+
currentDelay = maxDelay;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
{% endif %}
|
|
78
|
+
|
|
79
|
+
{% include "src/default/macros/block-next.js.njk" %}
|
|
80
|
+
|
|
81
|
+
{% include "src/default/macros/block-run-footer.js.njk" %}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
{% include "src/default/macros/block-footer.js.njk" %}
|
|
87
|
+
|
|
@@ -27,7 +27,17 @@ export default function Block(context){
|
|
|
27
27
|
// Import node-cron for scheduling
|
|
28
28
|
const cron = await import('node-cron');
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// Schedule configuration
|
|
31
|
+
const cronExpression = {{ context.transform.schedule.cron | safe }};
|
|
32
|
+
const timezone = {{ context.transform.schedule.timezone | dump | safe }};
|
|
33
|
+
const enabled = {{ context.transform.schedule.enabled | dump | safe }};
|
|
34
|
+
|
|
35
|
+
// Check if schedule is enabled
|
|
36
|
+
if (!enabled) {
|
|
37
|
+
// Schedule is disabled, skip to next block
|
|
38
|
+
{% include "src/default/macros/block-next.js.njk" %}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
31
41
|
|
|
32
42
|
// Validate cron expression
|
|
33
43
|
if (!cron.validate(cronExpression)) {
|
|
@@ -44,7 +54,8 @@ export default function Block(context){
|
|
|
44
54
|
});
|
|
45
55
|
{% endif %}
|
|
46
56
|
|
|
47
|
-
// Schedule the task
|
|
57
|
+
// Schedule the task with optional timezone
|
|
58
|
+
const scheduleOptions = timezone ? { timezone } : {};
|
|
48
59
|
const task = cron.schedule(cronExpression, async () => {
|
|
49
60
|
try {
|
|
50
61
|
{% if childs[0] %}
|
|
@@ -53,7 +64,7 @@ export default function Block(context){
|
|
|
53
64
|
} catch (error) {
|
|
54
65
|
console.error(`Scheduled task '{{indexKey}}' failed:`, error);
|
|
55
66
|
}
|
|
56
|
-
});
|
|
67
|
+
}, scheduleOptions);
|
|
57
68
|
|
|
58
69
|
// Start the scheduled task
|
|
59
70
|
task.start();
|