@accede/node-red-contrib-odbcwritenow 1.0.4 → 1.0.5

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 CHANGED
@@ -40,6 +40,8 @@ Editor fields:
40
40
  - `Name` (optional)
41
41
  - `What` (required): dataset/resource name, for example `sales_invoice_item`
42
42
  - `OrderBy` (optional): default sort expression
43
+ - `MaxRetries` (optional, default `3`): number of retries for timeout/token errors
44
+ - `RetryBackoffMs` (optional, default `500`): base delay in milliseconds; each retry uses exponential backoff (`base * 2^attempt`)
43
45
  - `APIKey` (required unless provided in `msg.apikey`)
44
46
 
45
47
  ## Runtime Inputs (`msg`)
@@ -78,7 +80,7 @@ You can override behavior per message:
78
80
  ## Notes
79
81
 
80
82
  - Base URL is currently fixed to `https://myobsync.accede.com.au`.
81
- - Timeout and token error responses are retried recursively.
83
+ - Timeout and token error responses are retried using bounded exponential backoff.
82
84
 
83
85
  ## License
84
86
 
package/odbcwritenow.html CHANGED
@@ -6,7 +6,9 @@
6
6
  name: {value:""},
7
7
  apikey: {value:""},
8
8
  what: {value:""},
9
- orderby: {value:""}
9
+ orderby: {value:""},
10
+ maxRetries: {value:3},
11
+ retryBackoffMs: {value:500}
10
12
  },
11
13
  inputs: 1,
12
14
  outputs: 2,
@@ -30,6 +32,14 @@
30
32
  <label for="node-input-orderby"><i class="fa fa-lock"></i> OrderBy</label>
31
33
  <input type="text" id="node-input-orderby" placeholder="OrderBy">
32
34
  </div>
35
+ <div class="form-row">
36
+ <label for="node-input-maxRetries"><i class="fa fa-repeat"></i> MaxRetries</label>
37
+ <input type="number" id="node-input-maxRetries" min="0" placeholder="3">
38
+ </div>
39
+ <div class="form-row">
40
+ <label for="node-input-retryBackoffMs"><i class="fa fa-clock-o"></i> RetryBackoffMs</label>
41
+ <input type="number" id="node-input-retryBackoffMs" min="0" placeholder="500">
42
+ </div>
33
43
  <div class="form-row">
34
44
  <label for="node-input-apikey"><i class="fa fa-lock"></i> APIKey</label>
35
45
  <input type="text" id="node-input-apikey" placeholder="APIKey">
@@ -39,4 +49,3 @@
39
49
  <script type="text/html" data-help-name="odbcwritenow-get">
40
50
  <p>odbcwritenow downloader</p>
41
51
  </script>
42
-
package/odbcwritenow.js CHANGED
@@ -1,48 +1,67 @@
1
- async function DoImport(msg, url, node) {
1
+ function sleep(ms) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
4
+
5
+ async function DoImport(msg, url, node, maxRetries, baseBackoffMs) {
2
6
  console.log('>', url)
3
7
  msg.nodata = false
4
8
  delete msg.complete
5
- node.status({ fill: "blue", shape: "ring", text: `Fetching #${msg.page}: ${msg.what}` })
6
- var datastr = ""
7
- try {
8
- const response = await fetch(url);
9
-
10
- datastr = await response.text();
11
- if (datastr.toLowerCase().includes("no data found")) {
12
- console.log("no data found");
13
- msg.nodata = true
14
- msg.complete = true
15
- msg.payload = []
16
- node.status({ fill: "green", shape: "ring", text: `#${msg.page}: No data found` })
17
- return node.send([null, msg]);
18
- }
9
+ let datastr = ""
10
+ let attempt = 0
19
11
 
20
- //Errors and retrys etc
21
- if (datastr.toLowerCase().includes("timeout")) {
22
- node.status({ fill: "red", shape: "ring", text: `#${msg.page}: MYOB Gateway Timeout` })
23
- console.error("******* MYOB Gateway Timeout", msg.retry, msg.page, url)
24
- msg.retry = msg.retry + 1
25
- return await DoImport(msg, url, node)
26
- }
27
- if (datastr.toLowerCase().includes("token error")) {
28
- node.status({ fill: "red", shape: "ring", text: `#${msg.page}: MYOB Token Error` })
29
- console.error("******* MYOB Token Error", msg.retry, msg.page, url)
30
- msg.retry = msg.retry + 1
31
- return await DoImport(msg, url, node)
12
+ while (attempt <= maxRetries) {
13
+ msg.retry = attempt
14
+ node.status({ fill: "blue", shape: "ring", text: `Fetching #${msg.page}: ${msg.what} (try ${attempt + 1}/${maxRetries + 1})` })
15
+
16
+ try {
17
+ const response = await fetch(url);
18
+ if (!response.ok) {
19
+ throw new Error(`HTTP ${response.status} ${response.statusText}`)
20
+ }
21
+ datastr = await response.text();
22
+
23
+ if (datastr.toLowerCase().includes("no data found")) {
24
+ console.log("no data found");
25
+ msg.nodata = true
26
+ msg.complete = true
27
+ msg.payload = []
28
+ node.status({ fill: "green", shape: "ring", text: `#${msg.page}: No data found` })
29
+ return node.send([null, msg]);
30
+ }
31
+
32
+ // Retry only for gateway timeout and token error responses.
33
+ if (datastr.toLowerCase().includes("timeout") || datastr.toLowerCase().includes("token error")) {
34
+ const isLastTry = attempt >= maxRetries
35
+ const kind = datastr.toLowerCase().includes("timeout") ? "MYOB Gateway Timeout" : "MYOB Token Error"
36
+
37
+ if (isLastTry) {
38
+ node.status({ fill: "red", shape: "ring", text: `#${msg.page}: ${kind} (max retries reached)` })
39
+ return node.error(`${kind} after ${maxRetries + 1} attempts`, msg)
40
+ }
41
+
42
+ const delayMs = Math.max(0, baseBackoffMs) * Math.pow(2, attempt)
43
+ node.status({ fill: "red", shape: "ring", text: `#${msg.page}: ${kind}, retrying in ${delayMs}ms` })
44
+ console.error("*******", kind, attempt, msg.page, `retry in ${delayMs}ms`)
45
+ attempt += 1
46
+ await sleep(delayMs)
47
+ continue
48
+ }
49
+
50
+ //---------------------------------
51
+ // Everything looks good.
52
+ const data = JSON.parse(datastr);
53
+ msg.rows = data.length
54
+ console.log("Page:", msg.page, "Rows:", data.length)
55
+
56
+ node.status({ fill: "green", shape: "dot", text: `#${msg.page}: ${msg.rows} Rows` })
57
+ msg.payload = data;
58
+ return node.send([msg, null]);
59
+ } catch (error) {
60
+ node.status({ fill: "red", shape: "ring", text: error.message })
61
+ console.error(error.message)
62
+ console.log(datastr)
63
+ return
32
64
  }
33
- //---------------------------------
34
- //Everything looks good
35
- const data = JSON.parse(datastr);
36
- msg.rows = data.length
37
- console.log("Page:", data.page, "Rows:", data.length)
38
-
39
- node.status({ fill: "green", shape: "dot", text: `#${msg.page}: ${msg.rows} Rows` })
40
- msg.payload = data;
41
- node.send([msg, null]);
42
- } catch (error) {
43
- node.status({ fill: "red", shape: "ring", text: error.message })
44
- console.error(error.message)
45
- console.log(datastr)
46
65
  }
47
66
 
48
67
  }
@@ -54,16 +73,43 @@ module.exports = function(RED) {
54
73
  var node = this;
55
74
  node.status({ text: `` })
56
75
  node.on('input', async function(msg) {
57
- const page = parseInt(encodeURIComponent(msg.page || 0));
58
- const apikey = encodeURIComponent(msg.apikey || config.apikey);
59
- const what = encodeURIComponent(config.what)
76
+ const pageRaw = msg.page === undefined || msg.page === null || msg.page === "" ? 0 : msg.page
77
+ const page = parseInt(pageRaw, 10)
78
+ const apikeyRaw = msg.apikey || config.apikey
79
+ const whatRaw = config.what
60
80
  const orderby = encodeURIComponent(msg.orderby || config.orderby);
61
81
 
82
+ if (!whatRaw || String(whatRaw).trim().length === 0) {
83
+ node.status({ fill: "red", shape: "ring", text: "Invalid config: 'what' is required" })
84
+ node.error("Invalid config: 'what' is required", msg)
85
+ return
86
+ }
87
+
88
+ if (!apikeyRaw || String(apikeyRaw).trim().length === 0) {
89
+ node.status({ fill: "red", shape: "ring", text: "Missing API key" })
90
+ node.error("Missing API key: set config.apikey or msg.apikey", msg)
91
+ return
92
+ }
93
+
94
+ if (Number.isNaN(page) || page < 0) {
95
+ node.status({ fill: "red", shape: "ring", text: "Invalid page: must be >= 0" })
96
+ node.error(`Invalid page '${pageRaw}': must be a non-negative integer`, msg)
97
+ return
98
+ }
99
+
100
+ const apikey = encodeURIComponent(apikeyRaw);
101
+ const what = encodeURIComponent(whatRaw)
102
+
62
103
 
63
104
 
64
105
  msg.page = page
65
- msg.what = what
66
- msg.retry = 0
106
+ msg.what = whatRaw
107
+ const maxRetries = Number.isInteger(parseInt(config.maxRetries, 10))
108
+ ? Math.max(0, parseInt(config.maxRetries, 10))
109
+ : 3
110
+ const backoffMs = Number.isInteger(parseInt(config.retryBackoffMs, 10))
111
+ ? Math.max(0, parseInt(config.retryBackoffMs, 10))
112
+ : 500
67
113
 
68
114
  var orderbystr = "";
69
115
  var filtersstr = "";
@@ -87,11 +133,10 @@ module.exports = function(RED) {
87
133
  }
88
134
 
89
135
  const url = `https://myobsync.accede.com.au/download/${what}/json/${page}?apikey=${apikey}${filtersstr}${orderbystr}`;
90
- DoImport(msg, url, node)
136
+ DoImport(msg, url, node, maxRetries, backoffMs)
91
137
 
92
138
  });
93
139
  }
94
140
  RED.nodes.registerType("odbcwritenow-get", ODBCWriteNowGet);
95
141
 
96
142
  }
97
-
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@accede/node-red-contrib-odbcwritenow",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "",
5
- "main": "index.js",
5
+ "main": "odbcwritenow.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
8
8
  },