@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fnet/cli",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "files": [
5
5
  "dist",
6
6
  "template"
@@ -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
- const cronExpression = {{ context.transform.schedule | safe }};
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();