@dazn/kopytko-framework 1.4.2 → 2.1.0
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 +6 -0
- package/package.json +4 -4
- package/src/components/http/HttpRequest.brs +29 -5
- package/src/components/http/HttpResponse.brs +111 -70
- package/src/components/http/HttpResponse.model.xml +15 -0
- package/src/components/http/HttpResponseCreator.brs +46 -0
- package/src/components/http/HttpService.brs +51 -29
- package/src/components/http/HttpStatusCodes.const.brs +8 -0
- package/src/components/http/cache/CachedHttpResponse.brs +44 -0
- package/src/components/http/cache/HttpCache.brs +47 -0
- package/src/components/http/cache/_mocks/CachedHttpResponse.mock.brs +35 -0
- package/src/components/http/request/Http.request.brs +80 -0
- package/src/components/http/request/Http.request.xml +5 -0
- package/src/components/http/request/HttpRequestResult.model.xml +8 -0
- package/src/components/http/request/Request.brs +14 -10
- package/src/components/http/request/Request.xml +3 -1
- package/src/components/http/request/_mocks/Request.mock.brs +17 -1
- package/src/components/http/request/_mocks/Request.mock.xml +3 -1
- package/src/components/http/request/createRequest.brs +15 -8
- package/src/components/utils/imfFixdateToSeconds.brs +27 -0
- package/src/components/utils/kopytkoWait.brs +3 -0
- package/src/components/http/_mocks/HttpService.mock.xml +0 -9
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ and improving its maintainability overall. It is highly inspired by the Javascri
|
|
|
6
6
|
of its patterns as well as its API, making it extremely friendly for someone coming from a React components environment.
|
|
7
7
|
It is also inspired by router of the Javascript Angular framework and some other mechanisms taken from the Javascript world.
|
|
8
8
|
|
|
9
|
+
# Kopytko Roku Ecosystem
|
|
10
|
+
|
|
9
11
|
Kopytko Framework is part of Kopytko Roku Ecosystem which consists of:
|
|
10
12
|
- [Kopytko Framework](https://github.com/getndazn/kopytko-framework),
|
|
11
13
|
- [Kopytko Utils](https://github.com/getndazn/kopytko-utils) - a collection of modern utility functions for Brightscript applications,
|
|
@@ -28,3 +30,7 @@ and used in apps configured by Kopytko Packager.
|
|
|
28
30
|
- Store: a mechanism to store reusable data. Docs available in [docs/store.md](docs/store.md)
|
|
29
31
|
- Theme: manages UI configuration and allows easy use in any place. Full documentation available in [docs/theme.md](docs/theme.md)
|
|
30
32
|
|
|
33
|
+
|
|
34
|
+
## Versions migration
|
|
35
|
+
|
|
36
|
+
To update Kopytko Framework to the latest major version, please follow the [Versions migration guide](docs/versions-migration-guide.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dazn/kopytko-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A modern Roku's Brightscript framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"brightscript",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"test": "node node_modules/@dazn/kopytko-unit-testing-framework/scripts/test.js"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@dazn/kopytko-utils": "^2.
|
|
28
|
+
"@dazn/kopytko-utils": "^2.4.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@dazn/eslint-plugin-kopytko": "^2.1.0",
|
|
32
|
-
"@dazn/kopytko-packager": "^1.2.
|
|
33
|
-
"@dazn/kopytko-unit-testing-framework": "^2.
|
|
32
|
+
"@dazn/kopytko-packager": "^1.2.4",
|
|
33
|
+
"@dazn/kopytko-unit-testing-framework": "^2.3.1",
|
|
34
34
|
"eslint": "^8.21.0"
|
|
35
35
|
},
|
|
36
36
|
"license": "MIT",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
' @property {String} url
|
|
10
10
|
' @property {Object} queryParams
|
|
11
11
|
' @property {String} method
|
|
12
|
+
' @property {Boolean} compression
|
|
12
13
|
' @property {Object} headers
|
|
13
14
|
' @property {Integer} timeout
|
|
14
15
|
' @property {Object} body
|
|
@@ -27,6 +28,7 @@ function HttpRequest(options as Object, httpInterceptors = [] as Object) as Obje
|
|
|
27
28
|
"Accept": "application/json",
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
prototype._headers = prototype._DEFAULT_HEADERS
|
|
30
32
|
prototype._httpInterceptors = httpInterceptors
|
|
31
33
|
prototype._options = options
|
|
32
34
|
prototype._urlTransfer = UrlTransfer()
|
|
@@ -49,12 +51,10 @@ function HttpRequest(options as Object, httpInterceptors = [] as Object) as Obje
|
|
|
49
51
|
m._urlTransfer.initClientCertificates()
|
|
50
52
|
end if
|
|
51
53
|
|
|
52
|
-
headers = m._DEFAULT_HEADERS
|
|
53
54
|
if (Type(m._options.headers) = "roAssociativeArray")
|
|
54
|
-
|
|
55
|
+
m._headers.append(m._options.headers)
|
|
55
56
|
end if
|
|
56
57
|
|
|
57
|
-
m._urlTransfer.setHeaders(headers)
|
|
58
58
|
m._timeout = ternary(m._options.timeout <> Invalid AND m._options.timeout <> 0, m._options.timeout, m._FALLBACK_TIMEOUT)
|
|
59
59
|
|
|
60
60
|
return m
|
|
@@ -63,13 +63,18 @@ function HttpRequest(options as Object, httpInterceptors = [] as Object) as Obje
|
|
|
63
63
|
' Performs actual request.
|
|
64
64
|
' @returns {Object} - Returns instance
|
|
65
65
|
prototype.send = function () as Object
|
|
66
|
+
method = m.getMethod()
|
|
67
|
+
|
|
68
|
+
if (method <> "POST" AND method <> "PUT") then m._headers.delete("Content-Type")
|
|
69
|
+
m._urlTransfer.setHeaders(m._headers)
|
|
70
|
+
|
|
66
71
|
for each interceptor in m._httpInterceptors
|
|
67
72
|
interceptor.interceptRequest(m, m._urlTransfer)
|
|
68
73
|
end for
|
|
69
74
|
|
|
70
|
-
if (
|
|
75
|
+
if (method = "GET")
|
|
71
76
|
m._urlTransfer.asyncGetToString()
|
|
72
|
-
else if (
|
|
77
|
+
else if (method = "POST" OR method = "PUT" OR method = "DELETE")
|
|
73
78
|
body = ""
|
|
74
79
|
|
|
75
80
|
if (m._options.body <> Invalid)
|
|
@@ -119,6 +124,16 @@ function HttpRequest(options as Object, httpInterceptors = [] as Object) as Obje
|
|
|
119
124
|
return m._urlTransfer.getUrl()
|
|
120
125
|
end function
|
|
121
126
|
|
|
127
|
+
' @returns {String}
|
|
128
|
+
prototype.getEscapedUrl = function () as String
|
|
129
|
+
return m._urlTransfer.escape(m.getUrl())
|
|
130
|
+
end function
|
|
131
|
+
|
|
132
|
+
' @returns {String}
|
|
133
|
+
prototype.getMethod = function () as String
|
|
134
|
+
return m._options.method
|
|
135
|
+
end function
|
|
136
|
+
|
|
122
137
|
' Cancels request that times out.
|
|
123
138
|
' @returns {Boolean}
|
|
124
139
|
prototype.isTimedOut = function () as Boolean
|
|
@@ -131,6 +146,15 @@ function HttpRequest(options as Object, httpInterceptors = [] as Object) as Obje
|
|
|
131
146
|
return isTimedOut
|
|
132
147
|
end function
|
|
133
148
|
|
|
149
|
+
' @returns {Boolean}
|
|
150
|
+
prototype.isCachingEnabled = function () as Boolean
|
|
151
|
+
return getProperty(m.options, "enableCaching", true)
|
|
152
|
+
end function
|
|
153
|
+
|
|
154
|
+
prototype.setHeader = sub (name as String, value as Dynamic)
|
|
155
|
+
m._headers[name] = value
|
|
156
|
+
end sub
|
|
157
|
+
|
|
134
158
|
' Aborts active request.
|
|
135
159
|
prototype.abort = sub ()
|
|
136
160
|
m._urlTransfer.asyncCancel()
|
|
@@ -1,91 +1,132 @@
|
|
|
1
|
-
' @import /components/
|
|
2
|
-
|
|
3
|
-
'
|
|
4
|
-
|
|
5
|
-
' @property {String} id
|
|
6
|
-
' @property {Integer} httpStatusCode
|
|
7
|
-
' @property {Object} rawData
|
|
8
|
-
' @property {Object} requestOptions
|
|
9
|
-
' @property {Node} data
|
|
10
|
-
' @property {String} ?failureReason
|
|
11
|
-
|
|
12
|
-
' The class parses response to JSON when application/json mimetype is detected.
|
|
13
|
-
' WARNING: the class must be used on the Task threads.
|
|
1
|
+
' @import /components/rokuComponents/DateTime.brs from @dazn/kopytko-utils
|
|
2
|
+
' @import /components/http/HttpStatusCodes.const.brs
|
|
3
|
+
' @import /components/utils/imfFixdateToSeconds.brs
|
|
4
|
+
|
|
14
5
|
' @class
|
|
15
|
-
' @param {Object}
|
|
16
|
-
' @param {
|
|
17
|
-
' @param {String}
|
|
18
|
-
' @param {
|
|
19
|
-
' @param {
|
|
20
|
-
' @param {
|
|
21
|
-
' @param {Object}
|
|
22
|
-
|
|
6
|
+
' @param {Object} responseData
|
|
7
|
+
' @param {Object} responseData.content
|
|
8
|
+
' @param {String} responseData.id
|
|
9
|
+
' @param {String} responseData.failureReason
|
|
10
|
+
' @param {Object} responseData.headers
|
|
11
|
+
' @param {Integer} responseData.httpStatusCode
|
|
12
|
+
' @param {Object} responseData.requestOptions
|
|
13
|
+
' @param {Integer} [responseData.time]
|
|
14
|
+
function HttpResponse(responseData as Object) as Object
|
|
23
15
|
prototype = {}
|
|
24
16
|
|
|
25
|
-
prototype.
|
|
26
|
-
prototype.
|
|
27
|
-
prototype.
|
|
28
|
-
prototype.
|
|
29
|
-
|
|
30
|
-
prototype.
|
|
31
|
-
prototype._data = {}
|
|
32
|
-
prototype._httpStatusCode = -1
|
|
33
|
-
prototype._headers = {}
|
|
34
|
-
prototype._requestOptions = Invalid
|
|
35
|
-
prototype._isSuccess = Invalid
|
|
36
|
-
prototype._failureReason = "OK"
|
|
37
|
-
|
|
38
|
-
' @constructor
|
|
39
|
-
' @param {Object} m - instance reference
|
|
40
|
-
' @param {Object} response
|
|
41
|
-
_constructor = function (m as Object, response as Object) as Object
|
|
42
|
-
if (Type(response.headers) = "roAssociativeArray")
|
|
43
|
-
m._headers = response.headers
|
|
44
|
-
end if
|
|
45
|
-
|
|
46
|
-
isAcceptedContentType = (getProperty(m._headers, [m._CONTENT_TYPE_HEADER], "").instr(m._ACCEPTED_CONTENT_TYPE) > -1)
|
|
47
|
-
if (isAcceptedContentType AND getProperty(response, ["rawData"], "") <> "")
|
|
48
|
-
m._data = ParseJSON(response.rawData)
|
|
17
|
+
prototype.MAX_AGE_NOT_ALLOWED = -1
|
|
18
|
+
prototype._CACHE_CONTROL_MAX_AGE = "max-age="
|
|
19
|
+
prototype._CACHE_CONTROL_NO_CACHE = "no-cache"
|
|
20
|
+
prototype._CACHE_CONTROL_NO_STORE = "no-store"
|
|
21
|
+
prototype._HEADER_CACHE_CONTROL = "Cache-Control"
|
|
22
|
+
prototype._HEADER_EXPIRES = "Expires"
|
|
49
23
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
24
|
+
prototype._id = responseData.id
|
|
25
|
+
prototype._failureReason = getProperty(responseData, "failureReason", "OK")
|
|
26
|
+
prototype._headers = getProperty(responseData, "headers", {})
|
|
27
|
+
prototype._httpStatusCode = getProperty(responseData, "httpStatusCode", -1)
|
|
28
|
+
prototype._rawData = getProperty(responseData, "rawData", {})
|
|
29
|
+
prototype._requestOptions = responseData.requestOptions
|
|
30
|
+
prototype._time = DateTime().asSeconds()
|
|
54
31
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
m._httpStatusCode = response.httpStatusCode
|
|
58
|
-
m._requestOptions = response.requestOptions
|
|
59
|
-
m._isSuccess = m._checkSuccess()
|
|
60
|
-
|
|
61
|
-
return m
|
|
62
|
-
end function
|
|
32
|
+
prototype._maxAgeRegex = Invalid
|
|
33
|
+
prototype._statusCodes = HttpStatusCodes()
|
|
63
34
|
|
|
64
35
|
' Casts response object to node.
|
|
65
|
-
' @returns {
|
|
36
|
+
' @returns {HttpResponseModel}
|
|
66
37
|
prototype.toNode = function () as Object
|
|
67
|
-
responseNode = CreateObject("roSGNode", "
|
|
68
|
-
responseNode.
|
|
69
|
-
|
|
38
|
+
responseNode = CreateObject("roSGNode", "HttpResponseModel")
|
|
39
|
+
responseNode.setFields({
|
|
40
|
+
failureReason: m._failureReason,
|
|
70
41
|
headers: m._headers,
|
|
71
42
|
httpStatusCode: m._httpStatusCode,
|
|
72
|
-
|
|
73
|
-
|
|
43
|
+
id: m._id,
|
|
44
|
+
isReusable: m.isReusable(),
|
|
45
|
+
isSuccess: m._isSuccess(),
|
|
46
|
+
maxAge: m.getMaxAge(),
|
|
47
|
+
rawData: m._rawData,
|
|
74
48
|
requestOptions: m._requestOptions,
|
|
75
49
|
})
|
|
76
|
-
responseNode.addField("data", "node", false)
|
|
77
50
|
|
|
78
|
-
|
|
79
|
-
|
|
51
|
+
return responseNode
|
|
52
|
+
end function
|
|
53
|
+
|
|
54
|
+
' @returns {Object}
|
|
55
|
+
prototype.serialise = function () as Object
|
|
56
|
+
return {
|
|
57
|
+
failureReason: m._failureReason,
|
|
58
|
+
headers: m._headers,
|
|
59
|
+
httpStatusCode: m._httpStatusCode,
|
|
60
|
+
id: m._id,
|
|
61
|
+
rawData: m._rawData,
|
|
62
|
+
requestOptions: m._requestOptions,
|
|
63
|
+
time: m._time,
|
|
64
|
+
}
|
|
65
|
+
end function
|
|
66
|
+
|
|
67
|
+
' @returns {Object}
|
|
68
|
+
prototype.getHeaders = function () as Object
|
|
69
|
+
return m._headers
|
|
70
|
+
end function
|
|
71
|
+
|
|
72
|
+
' @returns {Integer}
|
|
73
|
+
prototype.getStatusCode = function () as Integer
|
|
74
|
+
return m._httpStatusCode
|
|
75
|
+
end function
|
|
76
|
+
|
|
77
|
+
' Returns maximum time of storing the cached response in seconds.
|
|
78
|
+
' Returns 0 in case of no specified maxiumum time
|
|
79
|
+
' Returns -1 in case of not allowed caching (no-cache header) or max age not in the future
|
|
80
|
+
' @returns {Integer}
|
|
81
|
+
prototype.getMaxAge = function () as Integer
|
|
82
|
+
cacheControl = m._headers[m._HEADER_CACHE_CONTROL]
|
|
83
|
+
if (cacheControl <> Invalid)
|
|
84
|
+
if (cacheControl.inStr(m._CACHE_CONTROL_NO_CACHE) > 0)
|
|
85
|
+
return m.MAX_AGE_NOT_ALLOWED
|
|
86
|
+
end if
|
|
87
|
+
|
|
88
|
+
maxAgeMatches = m._getMaxAgeRegex().match(cacheControl)
|
|
89
|
+
if (NOT maxAgeMatches.isEmpty())
|
|
90
|
+
return maxAgeMatches[1].toInt()
|
|
91
|
+
end if
|
|
80
92
|
end if
|
|
81
93
|
|
|
82
|
-
|
|
94
|
+
expires = m._headers[m._HEADER_EXPIRES]
|
|
95
|
+
if (expires <> Invalid)
|
|
96
|
+
expiresInSeconds = imfFixdateToSeconds(expires)
|
|
97
|
+
if (expiresInSeconds > 0)
|
|
98
|
+
maxAge = expiresInSeconds - DateTime().asSeconds()
|
|
99
|
+
if (maxAge < 0) then return m.MAX_AGE_NOT_ALLOWED
|
|
100
|
+
|
|
101
|
+
return maxAge
|
|
102
|
+
end if
|
|
103
|
+
end if
|
|
104
|
+
|
|
105
|
+
return 0
|
|
106
|
+
end function
|
|
107
|
+
|
|
108
|
+
' Checks whether response is reusable (has no "no-store" Cache-Control header)
|
|
109
|
+
' @returns {Boolean}
|
|
110
|
+
prototype.isReusable = function () as Boolean
|
|
111
|
+
cacheControl = m._headers[m._HEADER_CACHE_CONTROL]
|
|
112
|
+
if (cacheControl = Invalid)
|
|
113
|
+
return false
|
|
114
|
+
end if
|
|
115
|
+
|
|
116
|
+
return cacheControl.inStr(m._CACHE_CONTROL_NO_STORE) = -1
|
|
117
|
+
end function
|
|
118
|
+
|
|
119
|
+
' @protected
|
|
120
|
+
prototype._getMaxAgeRegex = function () as Object
|
|
121
|
+
if (m._maxAgeRegex = Invalid) then m._maxAgeRegex = CreateObject("roRegex", m._CACHE_CONTROL_MAX_AGE + "(\d+)", "i")
|
|
122
|
+
|
|
123
|
+
return m._maxAgeRegex
|
|
83
124
|
end function
|
|
84
125
|
|
|
85
126
|
' @private
|
|
86
|
-
prototype.
|
|
87
|
-
return (m._httpStatusCode >= m.
|
|
127
|
+
prototype._isSuccess = function () as Boolean
|
|
128
|
+
return (m._httpStatusCode >= m._statusCodes.SUCCESS AND m._httpStatusCode < m._statusCodes.FAILURE)
|
|
88
129
|
end function
|
|
89
130
|
|
|
90
|
-
return
|
|
131
|
+
return prototype
|
|
91
132
|
end function
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
|
2
|
+
|
|
3
|
+
<component name="HttpResponseModel" extends="Node">
|
|
4
|
+
<interface>
|
|
5
|
+
<field id="data" type="node" /> <!-- @deprecated - HttpRequestResultModel is set as Request task's result field instead of HttpResponseModel -->
|
|
6
|
+
<field id="failureReason" type="string" />
|
|
7
|
+
<field id="headers" type="assocarray" />
|
|
8
|
+
<field id="httpStatusCode" type="integer" />
|
|
9
|
+
<field id="isReusable" type="boolean" />
|
|
10
|
+
<field id="isSuccess" type="boolean" />
|
|
11
|
+
<field id="maxAge" type="integer" />
|
|
12
|
+
<field id="rawData" type="assocarray" />
|
|
13
|
+
<field id="requestOptions" type="assocarray" />
|
|
14
|
+
</interface>
|
|
15
|
+
</component>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
' @import /components/getProperty.brs from @dazn/kopytko-utils
|
|
2
|
+
' @import /components/http/HttpResponse.brs
|
|
3
|
+
|
|
4
|
+
' @class
|
|
5
|
+
function HttpResponseCreator() as Object
|
|
6
|
+
prototype = {}
|
|
7
|
+
|
|
8
|
+
prototype._CONTENT_TYPE_HEADER = "content-type"
|
|
9
|
+
prototype._JSON_CONTENT_TYPE = "application/json"
|
|
10
|
+
|
|
11
|
+
' Parses response's content to JSON when application/json mimetype is detected.
|
|
12
|
+
' Warning: should be used on a render thread due to parsing json
|
|
13
|
+
' @param {roUrlEvent} urlEvent
|
|
14
|
+
' @param {HttpRequest} request
|
|
15
|
+
' @returns {HttpResponse}
|
|
16
|
+
prototype.create = function (urlEvent as Object, request as Object) as Object
|
|
17
|
+
return HttpResponse({
|
|
18
|
+
failureReason: urlEvent.getFailureReason(),
|
|
19
|
+
headers: urlEvent.getResponseHeaders(),
|
|
20
|
+
httpStatusCode: urlEvent.getResponseCode(),
|
|
21
|
+
id: request.getId(),
|
|
22
|
+
rawData: m._parseUrlEventContent(urlEvent),
|
|
23
|
+
requestOptions: request.getOptions(),
|
|
24
|
+
})
|
|
25
|
+
end function
|
|
26
|
+
|
|
27
|
+
' @private
|
|
28
|
+
prototype._parseUrlEventContent = function (urlEvent as Object) as Object
|
|
29
|
+
if (NOT m._isJsonResponse(urlEvent)) then return {}
|
|
30
|
+
|
|
31
|
+
content = urlEvent.getString()
|
|
32
|
+
if (content = "") then return {}
|
|
33
|
+
|
|
34
|
+
data = ParseJSON(content)
|
|
35
|
+
if (data = Invalid) then return {}
|
|
36
|
+
|
|
37
|
+
return data
|
|
38
|
+
end function
|
|
39
|
+
|
|
40
|
+
' @private
|
|
41
|
+
prototype._isJsonResponse = function (urlEvent as Object) as Boolean
|
|
42
|
+
return getProperty(urlEvent.getResponseHeaders(), [m._CONTENT_TYPE_HEADER], "").instr(m._JSON_CONTENT_TYPE) > -1
|
|
43
|
+
end function
|
|
44
|
+
|
|
45
|
+
return prototype
|
|
46
|
+
end function
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
' @import /components/getProperty.brs from @dazn/kopytko-utils
|
|
1
2
|
' @import /components/getType.brs from @dazn/kopytko-utils
|
|
3
|
+
' @import /components/http/cache/HttpCache.brs
|
|
2
4
|
' @import /components/http/HttpRequest.brs
|
|
3
5
|
' @import /components/http/HttpResponse.brs
|
|
6
|
+
' @import /components/http/HttpResponseCreator.brs
|
|
7
|
+
' @import /components/http/HttpStatusCodes.const.brs
|
|
8
|
+
' @import /components/utils/kopytkoWait.brs
|
|
4
9
|
|
|
5
10
|
' WARNING: the service must be used on the Task threads.
|
|
6
11
|
' @class
|
|
@@ -17,62 +22,79 @@ function HttpService(port as Object, httpInterceptors = [] as Object) as Object
|
|
|
17
22
|
prototype._httpInterceptors = httpInterceptors
|
|
18
23
|
prototype._port = port
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
prototype._cache = HttpCache()
|
|
26
|
+
prototype._responseCreator = HttpResponseCreator()
|
|
27
|
+
prototype._statusCodes = HttpStatusCodes()
|
|
28
|
+
|
|
29
|
+
' Performs HTTP request or returns a stored response
|
|
30
|
+
' Warning: if a stored response is returned, HttpInterceptors are omitted
|
|
21
31
|
' @param {HttpRequest~Options} options
|
|
22
|
-
' @returns {
|
|
32
|
+
' @returns {HttpResponseModel|Invalid}
|
|
23
33
|
prototype.fetch = function (options as Object) as Object
|
|
24
|
-
request = HttpRequest(options, m._httpInterceptors)
|
|
34
|
+
request = HttpRequest(options, m._httpInterceptors)
|
|
35
|
+
request.setMessagePort(m._port)
|
|
36
|
+
|
|
37
|
+
cachedResponse = m._getCachedResponse(request)
|
|
38
|
+
if (cachedResponse <> Invalid)
|
|
39
|
+
if (NOT cachedResponse.hasExpired())
|
|
40
|
+
return cachedResponse.toNode()
|
|
41
|
+
end if
|
|
42
|
+
|
|
43
|
+
eTag = getProperty(cachedResponse.getHeaders(), "eTag", "")
|
|
44
|
+
if (eTag <> "")
|
|
45
|
+
request.setHeader("If-None-Match", eTag)
|
|
46
|
+
end if
|
|
47
|
+
end if
|
|
48
|
+
|
|
25
49
|
request.send()
|
|
26
50
|
|
|
27
|
-
return m._waitForResponse(request)
|
|
51
|
+
return m._waitForResponse(request, cachedResponse)
|
|
28
52
|
end function
|
|
29
53
|
|
|
30
54
|
' @private
|
|
31
|
-
prototype.
|
|
55
|
+
prototype._getCachedResponse = function (request as Object) as Object
|
|
56
|
+
if (NOT request.isCachingEnabled()) then return Invalid
|
|
57
|
+
|
|
58
|
+
return m._cache.read(request.getEscapedUrl())
|
|
59
|
+
end function
|
|
60
|
+
|
|
61
|
+
' @private
|
|
62
|
+
prototype._waitForResponse = function (request as Object, cachedResponse as Object) as Object
|
|
32
63
|
while (true)
|
|
33
|
-
message = m.
|
|
64
|
+
message = kopytkoWait(m._TIMEOUT_INTERVAL_CHECK, m._port)
|
|
34
65
|
|
|
35
66
|
if (getType(message) = "roUrlEvent")
|
|
36
67
|
if (message.getInt() = m._HTTP_REQUEST_COMPLETED)
|
|
37
|
-
return m._handleResponse(request, message)
|
|
68
|
+
return m._handleResponse(request, message, cachedResponse)
|
|
38
69
|
end if
|
|
39
70
|
else if (getType(message) = "roSGNodeEvent" AND message.getField() = "abort")
|
|
40
71
|
request.abort()
|
|
41
72
|
|
|
42
73
|
return Invalid
|
|
43
74
|
else if (message = Invalid AND request.isTimedOut())
|
|
44
|
-
return m.
|
|
75
|
+
return HttpResponse({ httpStatusCode: m._TIMEOUT_ERROR_CODE, id: request.getId() }).toNode()
|
|
45
76
|
end if
|
|
46
77
|
end while
|
|
47
78
|
end function
|
|
48
79
|
|
|
49
80
|
' @private
|
|
50
|
-
prototype.
|
|
51
|
-
return Wait(m._TIMEOUT_INTERVAL_CHECK, m._port)
|
|
52
|
-
end function
|
|
53
|
-
|
|
54
|
-
' @private
|
|
55
|
-
prototype._handleResponse = function (request as Object, urlEvent as Object) as Object
|
|
81
|
+
prototype._handleResponse = function (request as Object, urlEvent as Object, cachedResponse as Object) as Object
|
|
56
82
|
for each interceptor in m._httpInterceptors
|
|
57
83
|
interceptor.interceptResponse(request, urlEvent)
|
|
58
84
|
end for
|
|
59
85
|
|
|
60
|
-
|
|
61
|
-
rawData: urlEvent.getString(),
|
|
62
|
-
httpStatusCode: urlEvent.getResponseCode(),
|
|
63
|
-
failureReason: urlEvent.getFailureReason(),
|
|
64
|
-
id: request.getId(),
|
|
65
|
-
headers: urlEvent.getResponseHeaders(),
|
|
66
|
-
requestOptions: request.getOptions(),
|
|
67
|
-
}).toNode()
|
|
68
|
-
end function
|
|
86
|
+
response = m._responseCreator.create(urlEvent, request)
|
|
69
87
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
responseCode = response.getStatusCode()
|
|
89
|
+
if (responseCode >= m._statusCodes.SUCCESS AND responseCode < m._statusCodes.REDIRECTION)
|
|
90
|
+
if (request.getMethod() = "GET" AND response.isReusable())
|
|
91
|
+
m._cache.store(request, response)
|
|
92
|
+
end if
|
|
93
|
+
else if (responseCode = m._statusCodes.NOT_MODIFIED)
|
|
94
|
+
return m._cache.prolong(request, cachedResponse, response.getMaxAge()).toNode()
|
|
95
|
+
end if
|
|
96
|
+
|
|
97
|
+
return response.toNode()
|
|
76
98
|
end function
|
|
77
99
|
|
|
78
100
|
return prototype
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
' @import /components/getProperty.brs from @dazn/kopytko-utils
|
|
2
|
+
' @import /components/rokuComponents/DateTime.brs from @dazn/kopytko-utils
|
|
3
|
+
' @import /components/http/HttpResponse.brs
|
|
4
|
+
function CachedHttpResponse(responseData as Object) as Object
|
|
5
|
+
prototype = HttpResponse(responseData)
|
|
6
|
+
|
|
7
|
+
' @constructor
|
|
8
|
+
' @param {Object} m - instance reference
|
|
9
|
+
' @param {Object} responseData
|
|
10
|
+
_constructor = function (m as Object, responseData as Object) as Object
|
|
11
|
+
if (responseData.time <> Invalid)
|
|
12
|
+
m._time = responseData.time
|
|
13
|
+
end if
|
|
14
|
+
|
|
15
|
+
return m
|
|
16
|
+
end function
|
|
17
|
+
|
|
18
|
+
' @returns {Boolean} - true if response is expired based on its maxAge and time values
|
|
19
|
+
prototype.hasExpired = function () as Boolean
|
|
20
|
+
return DateTime().asSeconds() > m._time + m.getMaxAge()
|
|
21
|
+
end function
|
|
22
|
+
|
|
23
|
+
' Updates cache max-age value
|
|
24
|
+
' @param {Integer} maxAge
|
|
25
|
+
prototype.setRevalidatedCache = sub (maxAge as Integer)
|
|
26
|
+
m._time = DateTime().asSeconds()
|
|
27
|
+
|
|
28
|
+
cacheControl = getProperty(m._headers, m._HEADER_CACHE_CONTROL, "")
|
|
29
|
+
if (cacheControl <> "")
|
|
30
|
+
newCacheControl = m._getMaxAgeRegex().replace(cacheControl, m._CACHE_CONTROL_MAX_AGE + maxAge.toStr())
|
|
31
|
+
if (newCacheControl = cacheControl AND NOT m._getMaxAgeRegex().isMatch(cacheControl))
|
|
32
|
+
newCacheControl += ", " + m._CACHE_CONTROL_MAX_AGE + maxAge.toStr()
|
|
33
|
+
end if
|
|
34
|
+
else
|
|
35
|
+
newCacheControl = m._CACHE_CONTROL_MAX_AGE + maxAge.toStr()
|
|
36
|
+
end if
|
|
37
|
+
|
|
38
|
+
' Cache-Control max-age is handier to use, so let's switch to it
|
|
39
|
+
m._headers[m._HEADER_CACHE_CONTROL] = newCacheControl
|
|
40
|
+
m._headers.delete(m._HEADER_EXPIRES)
|
|
41
|
+
end sub
|
|
42
|
+
|
|
43
|
+
return _constructor(prototype, responseData)
|
|
44
|
+
end function
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
' @import /components/CacheFS.brs from @dazn/kopytko-utils
|
|
2
|
+
' @import /components/ternary.brs from @dazn/kopytko-utils
|
|
3
|
+
' @import /components/http/cache/CachedHttpResponse.brs
|
|
4
|
+
function HttpCache() as Object
|
|
5
|
+
prototype = {}
|
|
6
|
+
|
|
7
|
+
prototype._cacheFS = CacheFS("httpCache")
|
|
8
|
+
|
|
9
|
+
' @param {String} escapedUrl
|
|
10
|
+
' @returns {CachedHttpResponse|Invalid}
|
|
11
|
+
prototype.read = function (escapedUrl as String) as Object
|
|
12
|
+
serialisedResponse = m._cacheFS.read(escapedUrl)
|
|
13
|
+
if (serialisedResponse = Invalid) then return Invalid
|
|
14
|
+
|
|
15
|
+
return CachedHttpResponse(serialisedResponse)
|
|
16
|
+
end function
|
|
17
|
+
|
|
18
|
+
' @param {HttpRequest} request
|
|
19
|
+
' @param {HttpResponse} response
|
|
20
|
+
' @returns {Boolean} - true if response has been stored
|
|
21
|
+
prototype.store = function (request as Object, response as Object) as Boolean
|
|
22
|
+
maxAge = response.getMaxAge()
|
|
23
|
+
if (maxAge = response.MAX_AGE_NOT_ALLOWED) then return false
|
|
24
|
+
|
|
25
|
+
return m._cacheFS.write(request.getEscapedUrl(), response.serialise())
|
|
26
|
+
end function
|
|
27
|
+
|
|
28
|
+
' @param {HttpRequest} request
|
|
29
|
+
' @param {CachedHttpResponse} response
|
|
30
|
+
' @param {Integer} newMaxAge
|
|
31
|
+
' @returns {CachedHttpResponse}
|
|
32
|
+
prototype.prolong = function (request as Object, response as Object, newMaxAge as Integer) as Object
|
|
33
|
+
escapedUrl = request.getEscapedUrl()
|
|
34
|
+
|
|
35
|
+
response.setRevalidatedCache(newMaxAge)
|
|
36
|
+
m._cacheFS.write(escapedUrl, response.serialise())
|
|
37
|
+
|
|
38
|
+
return response
|
|
39
|
+
end function
|
|
40
|
+
|
|
41
|
+
' @returns {Boolean} - true if the cache in scope has been cleared
|
|
42
|
+
prototype.clear = function () as Boolean
|
|
43
|
+
return m._cacheFS.clear()
|
|
44
|
+
end function
|
|
45
|
+
|
|
46
|
+
return prototype
|
|
47
|
+
end function
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
' @import /components/_mocks/Mock.brs from @dazn/kopytko-unit-testing-framework
|
|
2
|
+
' Required because Kopytko Unit Testing Framework doesn't mock inherited methods
|
|
3
|
+
function CachedHttpResponse(responseData as Object) as Object
|
|
4
|
+
return Mock({
|
|
5
|
+
testComponent: m,
|
|
6
|
+
name: "CachedHttpResponse",
|
|
7
|
+
constructorParams: { responseData: responseData },
|
|
8
|
+
methods: {
|
|
9
|
+
hasExpired: function () as Boolean
|
|
10
|
+
return m.hasExpiredMock("hasExpired", {}, "Boolean")
|
|
11
|
+
end function,
|
|
12
|
+
setRevalidatedCache: sub (maxAge as Integer)
|
|
13
|
+
m.setRevalidatedCacheMock("setRevalidatedCache", { maxAge: maxAge })
|
|
14
|
+
end sub,
|
|
15
|
+
toNode: function () as Object
|
|
16
|
+
return m.toNodeMock("toNode", {}, "Object")
|
|
17
|
+
end function,
|
|
18
|
+
serialise: function () as Object
|
|
19
|
+
return m.serialiseMock("serialise", {}, "Object")
|
|
20
|
+
end function,
|
|
21
|
+
getHeaders: function () as Object
|
|
22
|
+
return m.getHeadersMock("getHeaders", {}, "Object")
|
|
23
|
+
end function,
|
|
24
|
+
getStatusCode: function () as Object
|
|
25
|
+
return m.getStatusCodeMock("getStatusCode", {}, "Integer")
|
|
26
|
+
end function,
|
|
27
|
+
getMaxAge: function () as Object
|
|
28
|
+
return m.getMaxAgeMock("getMaxAge", {}, "Integer")
|
|
29
|
+
end function,
|
|
30
|
+
isReusable: function () as Object
|
|
31
|
+
return m.isReusableMock("isReusable", {}, "Boolean")
|
|
32
|
+
end function,
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
end function
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
' @import /components/getProperty.brs from @dazn/kopytko-utils
|
|
2
|
+
' @import /components/http/HttpService.brs
|
|
3
|
+
|
|
4
|
+
' Accessing data owned by different threads (render or other tasks) cause a Rendezvous, except accessing render thread-owned data in the `init` function
|
|
5
|
+
' because it's called on the render thread. That's why:
|
|
6
|
+
' - Try to prepare all and only required data before running a task
|
|
7
|
+
' - Avoid using Nodes owned by other threads after running a task
|
|
8
|
+
' - Nodes created inside the 'init' function of a task are owned by the render thread
|
|
9
|
+
' - If a node is read inside a running task it doesn't change ownership - each read operation on nodes owned by a different thread causes a Rendezvous
|
|
10
|
+
' - Rendezvous may happen also between task threads - each task node is owned by the render thread
|
|
11
|
+
sub init()
|
|
12
|
+
m.top.observeFieldScoped("options", "_onOptionsChange")
|
|
13
|
+
m.top.observeFieldScoped("state", "_onStateChange")
|
|
14
|
+
|
|
15
|
+
m._requestOptions.append({ method: "GET" })
|
|
16
|
+
end sub
|
|
17
|
+
|
|
18
|
+
sub runRequest()
|
|
19
|
+
_httpService = HttpService(m._port, getHttpInterceptors())
|
|
20
|
+
response = _httpService.fetch(m._requestOptions)
|
|
21
|
+
|
|
22
|
+
' When response is Invalid that means http request is aborted (check HttpService _waitForResponse method).
|
|
23
|
+
' Handler for aborting request is in createRequest function.
|
|
24
|
+
if (response = Invalid) then return
|
|
25
|
+
|
|
26
|
+
handleResponse(response)
|
|
27
|
+
end sub
|
|
28
|
+
|
|
29
|
+
' @protected
|
|
30
|
+
' @param {Node<HttpResponseModel>} response
|
|
31
|
+
sub handleResponse(response as Object)
|
|
32
|
+
result = CreateObject("roSGNode", "HttpRequestResultModel")
|
|
33
|
+
result.isSuccess = response.isSuccess
|
|
34
|
+
|
|
35
|
+
if (response.isSuccess)
|
|
36
|
+
result.data = parseResponse(response)
|
|
37
|
+
else
|
|
38
|
+
result.data = generateErrorData(response)
|
|
39
|
+
end if
|
|
40
|
+
|
|
41
|
+
m.top.result = result
|
|
42
|
+
end sub
|
|
43
|
+
|
|
44
|
+
' Override by child to return a list of objects implementing HttpInterceptor interface which will be passed to HttpService
|
|
45
|
+
' @protected
|
|
46
|
+
' @returns {HttpInterceptor[]}
|
|
47
|
+
function getHttpInterceptors() as Object
|
|
48
|
+
return []
|
|
49
|
+
end function
|
|
50
|
+
|
|
51
|
+
' Override by child to use parsers and potentially return a specific node type
|
|
52
|
+
' @protected
|
|
53
|
+
' @param {Node<HttpResponseModel>} response
|
|
54
|
+
' @returns {Node}
|
|
55
|
+
function parseResponse(response as Object) as Object
|
|
56
|
+
parsedData = CreateObject("roSGNode", "Node")
|
|
57
|
+
parsedData.addFields(response.rawData)
|
|
58
|
+
|
|
59
|
+
return parsedData
|
|
60
|
+
end function
|
|
61
|
+
|
|
62
|
+
' Reacting to m.top field's change instead of reading it in runRequest avoids rendezvous
|
|
63
|
+
' @private
|
|
64
|
+
sub _onOptionsChange(event as Object)
|
|
65
|
+
options = event.getData()
|
|
66
|
+
|
|
67
|
+
m._requestOptions.enableCaching = getProperty(options, "enableCaching", true)
|
|
68
|
+
end sub
|
|
69
|
+
|
|
70
|
+
' Allows to rerun the same instance of a task
|
|
71
|
+
' @private
|
|
72
|
+
sub _onStateChange(event as Object)
|
|
73
|
+
state = event.getData()
|
|
74
|
+
m.top.unobserveFieldScoped("abort")
|
|
75
|
+
|
|
76
|
+
if (LCase(state) = "run")
|
|
77
|
+
m._port = CreateObject("roMessagePort")
|
|
78
|
+
m.top.observeFieldScoped("abort", m._port)
|
|
79
|
+
end if
|
|
80
|
+
end sub
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
' @import /components/uuid.brs from @dazn/kopytko-utils
|
|
2
|
+
' @deprecated - this file will be detached from Request.xml file and deleted
|
|
2
3
|
sub init()
|
|
3
4
|
m._id = uuid()
|
|
4
5
|
|
|
@@ -12,30 +13,33 @@ sub init()
|
|
|
12
13
|
initRequest()
|
|
13
14
|
end sub
|
|
14
15
|
|
|
15
|
-
'
|
|
16
|
+
' @abstract
|
|
16
17
|
sub runRequest()
|
|
17
18
|
end sub
|
|
18
19
|
|
|
19
|
-
'
|
|
20
|
+
' Abstract method to prepare data before running a task to avoid rendezvous (becaused it's called on the render thread)
|
|
21
|
+
' The same can be achieved by adding the code to child's init() function
|
|
22
|
+
' @deprecated
|
|
23
|
+
' @protected
|
|
20
24
|
sub initRequest()
|
|
21
25
|
end sub
|
|
22
26
|
|
|
23
|
-
'
|
|
27
|
+
' Override by child to setup request options
|
|
24
28
|
function getRequestOptions(data as Object) as Object
|
|
25
29
|
return {}
|
|
26
30
|
end function
|
|
27
31
|
|
|
28
|
-
'
|
|
32
|
+
' Override by child to use parsers and potentially return a specific node type
|
|
29
33
|
function parseResponseData(data as Object) as Object
|
|
30
|
-
|
|
34
|
+
parsedData = CreateObject("roSGNode", "Node")
|
|
35
|
+
parsedData.addFields(data)
|
|
36
|
+
|
|
37
|
+
return parsedData
|
|
31
38
|
end function
|
|
32
39
|
|
|
33
|
-
'
|
|
40
|
+
' Override by child for returning a specific data structure and/or node type
|
|
34
41
|
function generateErrorData(response as Object) as Object
|
|
35
|
-
|
|
36
|
-
' { code: "", message: "" }
|
|
37
|
-
|
|
38
|
-
return {}
|
|
42
|
+
return response
|
|
39
43
|
end function
|
|
40
44
|
|
|
41
45
|
sub _onDataChange(event as Object)
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
<interface>
|
|
5
5
|
<field id="abort" type="boolean" />
|
|
6
6
|
<field id="data" type="assocarray" />
|
|
7
|
-
<field id="
|
|
7
|
+
<field id="options" type="assocarray" />
|
|
8
|
+
<field id="response" type="node" /> <!-- @deprecated - use "result" instead -->
|
|
9
|
+
<field id="result" type="node" />
|
|
8
10
|
</interface>
|
|
9
11
|
|
|
10
12
|
<script type="text/brightscript" uri="Request.brs" />
|
|
@@ -1,3 +1,19 @@
|
|
|
1
1
|
sub init()
|
|
2
|
-
m.
|
|
2
|
+
m._id = "request_mock"
|
|
3
|
+
|
|
4
|
+
m.top.id = m._id
|
|
5
|
+
|
|
6
|
+
m._requestOptions = { id: m._id }
|
|
7
|
+
|
|
8
|
+
m.top.observeFieldScoped("data", "_onDataChange")
|
|
9
|
+
end sub
|
|
10
|
+
|
|
11
|
+
function getRequestOptions(data as Object) as Object
|
|
12
|
+
return data
|
|
13
|
+
end function
|
|
14
|
+
|
|
15
|
+
sub _onDataChange(event as Object)
|
|
16
|
+
data = event.getData()
|
|
17
|
+
|
|
18
|
+
m._requestOptions.append(getRequestOptions(data))
|
|
3
19
|
end sub
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
<component name="RequestMock" extends="Node">
|
|
4
4
|
<interface>
|
|
5
|
+
<field id="abort" type="boolean" />
|
|
5
6
|
<field id="data" type="assocarray" />
|
|
6
|
-
<field id="
|
|
7
|
+
<field id="options" type="assocarray" />
|
|
8
|
+
<field id="result" type="node" />
|
|
7
9
|
</interface>
|
|
8
10
|
|
|
9
11
|
<script type="text/brightscript" uri="Request.mock.brs" />
|
|
@@ -19,7 +19,12 @@ function createRequest(task as Dynamic, data = {} as Object, options = {} as Obj
|
|
|
19
19
|
end if
|
|
20
20
|
|
|
21
21
|
task.data = data
|
|
22
|
-
|
|
22
|
+
if (options.taskOptions <> Invalid)
|
|
23
|
+
task.options = options.taskOptions
|
|
24
|
+
end if
|
|
25
|
+
|
|
26
|
+
task.observeFieldScoped("response", "createRequest_onPromiseResult") ' @todo remove once response field is removed
|
|
27
|
+
task.observeFieldScoped("result", "createRequest_onPromiseResult")
|
|
23
28
|
|
|
24
29
|
if (m._requests = Invalid)
|
|
25
30
|
m._requests = {}
|
|
@@ -42,7 +47,7 @@ function createRequest(task as Dynamic, data = {} as Object, options = {} as Obj
|
|
|
42
47
|
return requestPromise
|
|
43
48
|
end function
|
|
44
49
|
|
|
45
|
-
sub
|
|
50
|
+
sub createRequest_onPromiseResult(event as Object)
|
|
46
51
|
if (m._requests = Invalid)
|
|
47
52
|
return
|
|
48
53
|
end if
|
|
@@ -56,18 +61,19 @@ sub createRequest_onPromiseResponse(event as Object)
|
|
|
56
61
|
|
|
57
62
|
' We stop task manually to make sure that state is changed and task can be rerun
|
|
58
63
|
request.task.control = "stop"
|
|
59
|
-
request.task.unobserveFieldScoped("response")
|
|
64
|
+
request.task.unobserveFieldScoped("response") ' @todo remove once response field is removed
|
|
65
|
+
request.task.unobserveFieldScoped("result")
|
|
60
66
|
createRequest_unsubscribeSignalIfNecessary(request)
|
|
61
67
|
|
|
62
68
|
requestPromise = request.promise
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
result = event.getData()
|
|
65
71
|
if (getProperty(request, "signal.abort", false))
|
|
66
72
|
requestPromise.reject(createRequest_createAbortedRequestError())
|
|
67
|
-
else if (
|
|
68
|
-
requestPromise.resolve(
|
|
73
|
+
else if (result.isSuccess)
|
|
74
|
+
requestPromise.resolve(result.data)
|
|
69
75
|
else
|
|
70
|
-
requestPromise.reject(
|
|
76
|
+
requestPromise.reject(result.data)
|
|
71
77
|
end if
|
|
72
78
|
|
|
73
79
|
m._requests.delete(requestId)
|
|
@@ -95,7 +101,8 @@ sub createRequest_onAbortSignal(event as Object)
|
|
|
95
101
|
for each requestItem in m._requests.items()
|
|
96
102
|
request = requestItem.value
|
|
97
103
|
if (signal.isSameNode(request.signal))
|
|
98
|
-
request.task.unobserveFieldScoped("response")
|
|
104
|
+
request.task.unobserveFieldScoped("response") ' @todo remove once response field is removed
|
|
105
|
+
request.task.unobserveFieldScoped("result")
|
|
99
106
|
request.task.abort = true
|
|
100
107
|
request.task.control = "stop"
|
|
101
108
|
request.promise.reject(createRequest_createAbortedRequestError())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
' @import /components/ArrayUtils.brs from @dazn/kopytko-utils
|
|
2
|
+
' @import /components/rokuComponents/DateTime.brs from @dazn/kopytko-utils
|
|
3
|
+
|
|
4
|
+
' Convert from IMF-fixdate format (Internet Message Format) to seconds
|
|
5
|
+
' @param {String} IMFFixdate - the IMF-Fixdate string (Eg. "Sun, 06 Nov 1994 08:49:37 GMT")
|
|
6
|
+
' @returns {Integer} the datetime in seconds (Eg. 784111777) or -1 for incorrect IMF-fixdate
|
|
7
|
+
function imfFixdateToSeconds(imfFixdate as String) as Integer
|
|
8
|
+
regex = CreateObject("roRegex", "(\d+) (\w{3}) (\d+) (\d+):(\d+):(\d+)", "")
|
|
9
|
+
|
|
10
|
+
matches = regex.match(imfFixdate)
|
|
11
|
+
if (matches.isEmpty()) then return -1
|
|
12
|
+
|
|
13
|
+
MONTH_SHORT_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
14
|
+
|
|
15
|
+
day = matches[1]
|
|
16
|
+
month = (ArrayUtils().findIndex(MONTH_SHORT_NAMES, matches[2]) + 1).toStr()
|
|
17
|
+
year = matches[3]
|
|
18
|
+
hour = matches[4]
|
|
19
|
+
minute = matches[5]
|
|
20
|
+
second = matches[6]
|
|
21
|
+
|
|
22
|
+
_dateTime = DateTime()
|
|
23
|
+
ISOString = year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second
|
|
24
|
+
_dateTime.fromISO8601String(isoString)
|
|
25
|
+
|
|
26
|
+
return _dateTime.asSeconds()
|
|
27
|
+
end function
|