@dazn/kopytko-framework 1.4.2 → 2.0.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 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.4.2",
3
+ "version": "2.0.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.2.2"
28
+ "@dazn/kopytko-utils": "^2.3.2"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@dazn/eslint-plugin-kopytko": "^2.1.0",
32
32
  "@dazn/kopytko-packager": "^1.2.3",
33
- "@dazn/kopytko-unit-testing-framework": "^2.0.1",
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
- headers.append(m._options.headers)
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 (m._options.method = "GET")
75
+ if (method = "GET")
71
76
  m._urlTransfer.asyncGetToString()
72
- else if (m._options.method = "POST" OR m._options.method = "PUT" OR m._options.method = "DELETE")
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/getProperty.brs from @dazn/kopytko-utils
2
-
3
- ' The HttpResponse object
4
- ' @typedef {Object} HttpResponse~HttpResponseNode
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} response
16
- ' @param {String} response.id
17
- ' @param {String} response.rawData
18
- ' @param {Integer} response.httpStatusCode
19
- ' @param {String} response.failureReason
20
- ' @param {Object} response.headers
21
- ' @param {Object} response.requestOptions
22
- function HttpResponse(response as Object) as Object
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._HTTP_SUCCESS = 200
26
- prototype._HTTP_FAILURE = 400
27
- prototype._ACCEPTED_CONTENT_TYPE = "application/json"
28
- prototype._CONTENT_TYPE_HEADER = "content-type"
29
-
30
- prototype._id = Invalid
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
- if (m._data = Invalid)
51
- m._data = {}
52
- end if
53
- end if
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
- m._id = response.id
56
- m._failureReason = response.failureReason
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 {HttpResponse~HttpResponseNode}
36
+ ' @returns {HttpResponseModel}
66
37
  prototype.toNode = function () as Object
67
- responseNode = CreateObject("roSGNode", "Node")
68
- responseNode.id = m._id
69
- responseNode.addFields({
38
+ responseNode = CreateObject("roSGNode", "HttpResponseModel")
39
+ responseNode.setFields({
40
+ failureReason: m._failureReason,
70
41
  headers: m._headers,
71
42
  httpStatusCode: m._httpStatusCode,
72
- isSuccess: m._isSuccess,
73
- rawData: m._data,
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
- if (NOT m._isSuccess)
79
- responseNode.addFields({ failureReason: m._failureReason })
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
- return responseNode
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._checkSuccess = function () as Boolean
87
- return (m._httpStatusCode >= m._HTTP_SUCCESS AND m._httpStatusCode < m._HTTP_FAILURE)
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 _constructor(prototype, response)
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
- ' Performs HTTP request
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 {HttpResponse|Invalid}
32
+ ' @returns {HttpResponseModel|Invalid}
23
33
  prototype.fetch = function (options as Object) as Object
24
- request = HttpRequest(options, m._httpInterceptors).setMessagePort(m._port)
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._waitForResponse = function (request as Object) as Object
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._waitForMessage()
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._getTimeoutResponse(request)
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._waitForMessage = function () as Object
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
- return HttpResponse({
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
- ' @private
71
- prototype._getTimeoutResponse = function (request as Object) as Object
72
- return HttpResponse({
73
- httpStatusCode: m._TIMEOUT_ERROR_CODE,
74
- id: request.getId(),
75
- }).toNode()
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,8 @@
1
+ function HttpStatusCodes() as Object
2
+ return {
3
+ SUCCESS: 200,
4
+ REDIRECTION: 300,
5
+ NOT_MODIFIED: 304,
6
+ FAILURE: 400,
7
+ }
8
+ end function
@@ -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,42 @@
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()
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
+ return prototype
42
+ 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
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8" ?>
2
+
3
+ <component name="HttpRequest" extends="Request">
4
+ <script type="text/brightscript" uri="Http.request.brs" />
5
+ </component>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="utf-8" ?>
2
+
3
+ <component name="HttpRequestResultModel" extends="Node">
4
+ <interface>
5
+ <field id="data" type="node" />
6
+ <field id="isSuccess" type="boolean" />
7
+ </interface>
8
+ </component>
@@ -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
- ' Implement by each child
16
+ ' @abstract
16
17
  sub runRequest()
17
18
  end sub
18
19
 
19
- ' Eventually implement by child
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
- ' Eventually implement by child
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
- ' Implement by child
32
+ ' Override by child to use parsers and potentially return a specific node type
29
33
  function parseResponseData(data as Object) as Object
30
- return {}
34
+ parsedData = CreateObject("roSGNode", "Node")
35
+ parsedData.addFields(data)
36
+
37
+ return parsedData
31
38
  end function
32
39
 
33
- ' Implement by child
40
+ ' Override by child for returning a specific data structure and/or node type
34
41
  function generateErrorData(response as Object) as Object
35
- ' Structure:
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="response" type="node" />
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.top.id = "request_mock"
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="response" type="node" />
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
- task.observeFieldScoped("response", "createRequest_onPromiseResponse")
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 createRequest_onPromiseResponse(event as Object)
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
- response = event.getData()
70
+ result = event.getData()
65
71
  if (getProperty(request, "signal.abort", false))
66
72
  requestPromise.reject(createRequest_createAbortedRequestError())
67
- else if (response.isSuccess)
68
- requestPromise.resolve(response.data)
73
+ else if (result.isSuccess)
74
+ requestPromise.resolve(result.data)
69
75
  else
70
- requestPromise.reject(response.data)
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
@@ -0,0 +1,3 @@
1
+ function kopytkoWait(timeout as Integer, port as Object) as Object
2
+ return Wait(timeout, port)
3
+ end function
@@ -1,9 +0,0 @@
1
- <?xml version="1.0" encoding="utf-8" ?>
2
-
3
- <component name="HttpServiceMock" extends="Group">
4
- <interface>
5
- <field id="abort" type="string" />
6
- <field id="requestOptions" type="assocarray" />
7
- <field id="response" type="node" />
8
- </interface>
9
- </component>