diff --git a/BUILD_DIAGNOSTICS.md b/BUILD_DIAGNOSTICS.md new file mode 100644 index 0000000..cb0573e --- /dev/null +++ b/BUILD_DIAGNOSTICS.md @@ -0,0 +1,479 @@ +# Build Diagnostics & Deployment Verification + +## Phase 1: Prove New Binary is Deployed + +### 1.1 Build Identity Generation + +Create a unique build identifier that includes: + +- **NativeScript component version** - semantic version tag +- **Git commit SHA** - 40-character commit hash +- **Build timestamp** - ISO 8601 format +- **Build type** - debug/release +- **Location compatibility revision** - semantic version +- **XMLHttpRequest implementation revision** - semantic version + +### 1.2 Startup Log Output + +Every NativeScript startup should produce exactly this log: + +``` +[NS_BUILD_ID] version=1.0.0 commit=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 build=2026-08-21T15:31:00Z buildType=release locationRev=2.0 xhrRev=3.1 +``` + +### 1.3 Boot Sequence Logging + +Immediately after build ID, log the initialization order: + +``` +[NS_BOOT_ORDER] 01 context-created contextId=ctx-main +[NS_BOOT_ORDER] 02 globals-installed +[NS_BOOT_ORDER] 03 location-installed protocol=file: host= origin=null +[NS_BOOT_ORDER] 04 xhr-installed version=3.1 +[NS_BOOT_ORDER] 05 bundle-loaded bundle.js +[NS_BOOT_ORDER] 06 playerplatform-preload +[NS_BOOT_ORDER] 07 freewheel-ready +``` + +### 1.4 Device Verification + +To verify correct binary is deployed: + +1. **Via Console:** + ```javascript + // Open RDK Inspector console and run: + window.NS_BUILD_INFO // Should return object with all build metadata + ``` + +2. **Via Logs:** + ```bash + # Device SSH + ssh root@ + + # Check deployment location + ls -lh /opt/apps/*/librdknativescript.so # Or equivalent binary path + + # Check version metadata + strings /opt/apps/*/librdknativescript.so | grep "NS_BUILD_ID" + + # Watch live logs + journalctl -fu sky-vipa # Or equivalent service name + ``` + +3. **Via Package Manager:** + ```bash + # Check version + dpkg -l | grep rdknativescript + # OR + rpm -q rdknativescript + # OR + opkg info rdknativescript + ``` + +## Phase 2: Verify Location Installation + +Before any third-party code (PlayerPlatform, FreeWheel) loads: + +``` +[NS_LOCATION_INSTALL] contextId=ctx-main globalId=window + protocol=file: + host= + hostname= + port= + origin=null + href=file:///index.html + pathname=/index.html + search= + hash= + computed=file:// + status=success +``` + +### Validation Checklist: + +```javascript +// Should all be true +window.location.protocol === "file:" +window.location.host === "" +window.location.hostname === "" +window.location.port === "" +window.location.origin === "null" +window.location.pathname === "/index.html" +window.location.protocol + "//" + window.location.host === "file://" +globalThis.location === window.location +typeof window.location.toString === "function" + +// URL encoding validation +encodeURIComponent(window.location.protocol + "//" + window.location.host) === "file%3A%2F%2F" +``` + +## Phase 3: Verify XHR Installation + +``` +[NS_XHR_INSTALL] contextId=ctx-main version=3.1 + properties: response, responseURL, timeout, statusText + methods: open, send, abort, addEventListener, setTimeout + events: readystatechange, load, error, timeout, abort, loadend + status=success +``` + +### Validation Checklist: + +```javascript +// Should all exist +typeof window.XMLHttpRequest === "function" +typeof XMLHttpRequest.prototype.response !== "undefined" +typeof XMLHttpRequest.prototype.responseURL !== "undefined" +typeof XMLHttpRequest.prototype.timeout !== "undefined" +typeof XMLHttpRequest.prototype.statusText !== "undefined" +typeof XMLHttpRequest.prototype.setTimeout === "function" +typeof XMLHttpRequest.prototype.addEventListener === "function" + +// Test event dispatch +var xhr = new XMLHttpRequest(); +var events = []; +xhr.addEventListener("readystatechange", function() { events.push("readystatechange"); }); +xhr.addEventListener("load", function() { events.push("load"); }); +xhr.addEventListener("loadend", function() { events.push("loadend"); }); +// Events should dispatch in correct order after send() +``` + +## Phase 4: Monitor FreeWheel Requests + +### 4.1 First Request Validation + +When a FreeWheel request initiates, check: + +``` +[NS_FW_ENV] contextId=ctx-main + protocol=file: + host= + computed=file:// + xhr=3.1 + playerPlatform=5.140.0 + freewheel=6.55.0 +``` + +### 4.2 Request Execution Timeline + +For each FreeWheel ad request: + +``` +[NS_FW_REQUEST] requestId=abc123 contextId=ctx-main + start=15:31.127 + url=https://604fc.v.fwmrm.net/ad/g/1?...&orig=file%3A%2F%2F... + timeout=5000ms + expectedCompletion= + +[NS_XHR_CONSTRUCT] requestId=abc123 timestamp=15:31.127 +[NS_XHR_OPEN] requestId=abc123 method=GET host=604fc.v.fwmrm.net timestamp=15:31.128 +[NS_XHR_SEND] requestId=abc123 timestamp=15:31.130 + +[NS_HTTP_CONNECT] requestId=abc123 elapsed=45ms +[NS_HTTP_TLS] requestId=abc123 elapsed=120ms +[NS_HTTP_REQUEST] requestId=abc123 elapsed=165ms + +[NS_HTTP_HEADERS] requestId=abc123 + status=200 + contentType=application/json + contentLength=12345 + elapsed=245ms + +[NS_HTTP_BODY] requestId=abc123 + chunkCount=3 + totalBytes=12345 + elapsed=310ms + +[NS_XHR_STATE] requestId=abc123 readyState=2 status=200 elapsed=245ms +[NS_XHR_STATE] requestId=abc123 readyState=3 status=200 elapsed=250ms +[NS_XHR_STATE] requestId=abc123 readyState=4 status=200 elapsed=310ms + +[NS_XHR_EVENT] requestId=abc123 event=readystatechange timestamp=15:31.437 +[NS_XHR_EVENT] requestId=abc123 event=load timestamp=15:31.437 +[NS_XHR_EVENT] requestId=abc123 event=loadend timestamp=15:31.437 + +[NS_HTTP_COMPLETE] requestId=abc123 + result=success + category=SUCCESS + status=200 + elapsed=310ms + timestamp=15:31.437 + +[NS_FW_RESPONSE] requestId=abc123 + status=success + adsCount=1 + duration=310ms + completed=15:31.437 +``` + +### 4.3 Failure Timeline + +If request fails: + +``` +[NS_XHR_CONSTRUCT] requestId=xyz789 +[NS_XHR_OPEN] requestId=xyz789 +[NS_XHR_SEND] requestId=xyz789 + +[NS_HTTP_CONNECT] requestId=xyz789 elapsed=5012ms +[NS_HTTP_FAILURE] requestId=xyz789 + category=NATIVE_TIMEOUT (or DNS, TLS, CALLBACK_DELAY, SDK_TIMEOUT) + status=0 + error=Connection timeout + elapsed=5012ms + +[NS_XHR_EVENT] requestId=xyz789 event=timeout timestamp=15:36.142 +[NS_XHR_EVENT] requestId=xyz789 event=loadend timestamp=15:36.142 + +[NS_HTTP_COMPLETE] requestId=xyz789 + result=failure + category=NATIVE_TIMEOUT + elapsed=5012ms +``` + +## Phase 5: Check Multiple Contexts + +If device creates multiple JavaScript contexts: + +``` +[NS_CONTEXT_CREATED] contextId=ctx-main +[NS_CONTEXT_CREATED] contextId=ctx-fw-1 +[NS_CONTEXT_CREATED] contextId=ctx-fw-2 +[NS_CONTEXT_CREATED] contextId=ctx-fw-3 +... +[NS_CONTEXT_CREATED] contextId=ctx-fw-29 + +// Each context should get independent initialization: +[NS_LOCATION_INSTALL] contextId=ctx-fw-1 protocol=file: host= computed=file:// +[NS_XHR_INSTALL] contextId=ctx-fw-1 version=3.1 + +[NS_LOCATION_INSTALL] contextId=ctx-fw-2 protocol=file: host= computed=file:// +[NS_XHR_INSTALL] contextId=ctx-fw-2 version=3.1 +``` + +## Phase 6: Test Cases + +### 6.1 Unit Tests (Automated) + +```bash +# Run tests +npm test + +# Expected output: +# ✓ 37/37 XHR tests passed +# ✓ 11/11 Location tests passed +# ✓ 5/5 Encoding tests passed +# Total: 53 tests, 0 failures +``` + +### 6.2 Integration Test: Initial Tune + +**Setup:** Device running with new binary +**Action:** Tune to channel with CDAI (e.g., Sky UK Channel 107) +**Expected Results:** + +``` +[NS_BUILD_ID] version=... commit=... # Correct new version +[NS_BOOT_ORDER] 01 context-created +[NS_BOOT_ORDER] 02 globals-installed +[NS_BOOT_ORDER] 03 location-installed +[NS_BOOT_ORDER] 04 xhr-installed +[NS_BOOT_ORDER] 05 bundle-loaded +[NS_BOOT_ORDER] 06 playerplatform-preload +[NS_BOOT_ORDER] 07 freewheel-ready + +[NS_LOCATION_INSTALL] contextId=ctx-main protocol=file: host= computed=file:// + +[NS_FW_REQUEST] requestId=req-1 + url=https://604fc.v.fwmrm.net/ad/g/1?...&orig=file%3A%2F%2F... + +[NS_FW_RESPONSE] requestId=req-1 + status=success + adsCount=1 # CRITICAL: Was "undefined" before fix + duration=245ms + completed= + +[Bundle Log] Ad request succeeded, Received 1 ads in the slot +``` + +**Failure Indicators:** +- `orig=undefined%2F%2Fundefined` in request URL +- `status=timeout` in FreeWheel response +- Log contains "Ad request failed, Returned slot object is undefined" +- readyState never reaches 4 (DONE) +- No load event dispatched + +### 6.3 Integration Test: SCTE-35 Midroll + +**Setup:** Live channel playing, waiting for SCTE-35 cue +**Action:** Trigger SCTE-35 cue manually +**Expected Results:** + +``` +[NS_FW_REQUEST] requestId=req-2 type=midroll duration=30000ms + url=https://...&orig=file%3A%2F%2F&slid=9605&slau=midroll... + +[NS_XHR_HEADERS] requestId=req-2 status=200 elapsed=156ms +[NS_FW_RESPONSE] requestId=req-2 status=success adsCount=1 duration=156ms + +# CRITICAL: Ads should play, not timeout after 5 seconds +[Bundle Log] Playing ad break (duration: 30s) +[Bundle Log] Ad 1/1 playing... +``` + +### 6.4 Integration Test: Concurrent Requests + +**Setup:** Multiple FreeWheel contexts active +**Action:** Trigger 3+ simultaneous ad requests +**Expected Results:** + +``` +[NS_FW_REQUEST] requestId=req-3a +[NS_FW_REQUEST] requestId=req-3b +[NS_FW_REQUEST] requestId=req-3c + +[NS_XHR_HEADERS] requestId=req-3a elapsed=200ms +[NS_XHR_HEADERS] requestId=req-3b elapsed=210ms +[NS_XHR_HEADERS] requestId=req-3c elapsed=205ms + +[NS_FW_RESPONSE] requestId=req-3a status=success +[NS_FW_RESPONSE] requestId=req-3b status=success +[NS_FW_RESPONSE] requestId=req-3c status=success + +# All should complete successfully without cross-context interference +``` + +## Phase 7: Performance Validation + +### 7.1 Request Timing + +**Expected FreeWheel request timeline:** + +| Phase | Min | Typical | Max | +|-------|-----|---------|-----| +| DNS lookup | 10ms | 30ms | 100ms | +| TLS handshake | 50ms | 120ms | 300ms | +| Request send | 5ms | 10ms | 50ms | +| Response headers | 20ms | 50ms | 200ms | +| Response body | 10ms | 100ms | 500ms | +| **Total** | **95ms** | **310ms** | **1150ms** | + +**Timeout setting:** 5000ms (5 seconds) +**Buffer:** 3.85 seconds before timeout + +### 7.2 Memory Impact + +- No memory leaks on repeated requests (check after 100 requests) +- Timeout handlers properly cleaned up +- Event listeners properly removed + +### 7.3 CPU Impact + +- No busy-waiting (timeout uses native setTimeout, not polling) +- No blocking synchronous operations +- Non-blocking logging when enabled + +## Phase 8: Rollback Verification + +If new build causes regressions: + +1. **Verify fallback binary is correct:** + ``` + [NS_BUILD_ID] version= commit= + ``` + +2. **Re-test VOD ads:** + - VOD content should still play ads + - VMAP requests should work + - Existing ad functionality should not regress + +3. **Identify what failed:** + - Was it location fix? + - Was it XHR changes? + - Was it timeout implementation? + +## Troubleshooting Guide + +### Problem: orig still shows undefined//undefined + +**Diagnostics:** +```javascript +console.log(window.location); +console.log(window.location.protocol + "//" + window.location.host); +console.log(JSON.stringify(globalThis.location)); +``` + +**Root Cause Check:** +- Build ID shows old version? → Old binary still deployed +- Location object missing properties? → Initialization skipped +- protocol not ending with colon? → Malformed in initialization +- host is object instead of string? → Type error + +### Problem: FreeWheel request timeout after 5s + +**Diagnostics:** +``` +// Check request timeline +grep "NS_FW_REQUEST\|NS_FW_RESPONSE\|NS_XHR" device-logs.txt +grep "elapsed" device-logs.txt | tail -20 +``` + +**Root Cause Check:** +- No [NS_HTTP_HEADERS] log? → Native response not arriving +- [NS_HTTP_HEADERS] logged but no [NS_FW_RESPONSE]? → Callback dispatch issue +- [NS_FW_RESPONSE] after timeout? → Race condition + +### Problem: No diagnostic logs appearing + +**Check:** +```javascript +window.RDK_XHR_DIAGNOSTICS // Should be true +typeof XHRDiagnostics !== 'undefined' // Should be true +``` + +**Enable from console:** +```javascript +window.RDK_XHR_DIAGNOSTICS = true; +// Then trigger new FreeWheel request +``` + +## Success Criteria + +All of the following must be true: + +- [ ] NS_BUILD_ID log shows new commit SHA at startup +- [ ] NS_BOOT_ORDER shows correct 7-step sequence +- [ ] NS_LOCATION_INSTALL shows protocol=file: with computed=file:// +- [ ] No "orig=undefined" in FreeWheel request URL +- [ ] FreeWheel request completes before 5s timeout (typical 200-400ms) +- [ ] FreeWheel response includes adsCount >= 1 +- [ ] Bundle logs show "Ad request succeeded" +- [ ] Multiple concurrent requests all succeed without interference +- [ ] All 53 automated tests pass +- [ ] VOD ads still play (backward compatibility) + +## Post-Deployment Monitoring + +After successful deployment: + +1. **Week 1:** Monitor error rates + - Should drop from ~50% timeout to <5% + - Look for patterns in remaining failures + +2. **Week 2:** Verify all scenarios + - Linear CDAI + - SCTE-35 midroll + - CDVR time-shift + - VOD ads + +3. **Ongoing:** Check performance + - Average FreeWheel response time + - Max request duration + - Error categories + +## References + +- FreeWheel SDK: https://sdk.freewheel.com/ +- W3C XMLHttpRequest: https://xhr.spec.whatwg.org/ +- W3C URL Standard: https://url.spec.whatwg.org/ +- RDK Documentation: https://github.com/rdkcentral/rdkcentral.github.io diff --git a/CHANGELOG.md b/CHANGELOG.md index 75168a7..07c3bd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [2.0.9](https://github-personal/rdkcentral/rdkNativeScript/compare/2.0.8...2.0.9) + +- RDKEMW-24812: DefaultMessageDispatcher stops after 5-10 minutes of pl… [`#144`](https://github-personal/rdkcentral/rdkNativeScript/pull/144) +- RDKEMW-24305: [nativescript]xuaHeartbeat Analytics Event Is Not Trigg… [`#143`](https://github-personal/rdkcentral/rdkNativeScript/pull/143) +- RDKEMW-23773 : Ad Request Failure on Linear CDAI and CDVR TS CDAI [`#142`](https://github-personal/rdkcentral/rdkNativeScript/pull/142) +- RDKEMW-24812: DefaultMessageDispatcher stops after 5-10 minutes of playback [`8243a56`](https://github-personal/rdkcentral/rdkNativeScript/commit/8243a5686bbec18fe2cec3578da8e80132fd201b) +- RDKEMW-24305: [nativescript]xuaHeartbeat Analytics Event Is Not Triggered During Playback [`1623a7f`](https://github-personal/rdkcentral/rdkNativeScript/commit/1623a7fffb1442a8c4234d25a8d9e012f68eef30) + #### [2.0.8](https://github-personal/rdkcentral/rdkNativeScript/compare/2.0.6...2.0.8) - RDKEMW-22317 : Integrate VIPA widget 1.4.4.5 with nativescript [`#138`](https://github-personal/rdkcentral/rdkNativeScript/pull/138) diff --git a/include/NativeJSRenderer.h b/include/NativeJSRenderer.h index ee9f629..11f81c6 100644 --- a/include/NativeJSRenderer.h +++ b/include/NativeJSRenderer.h @@ -137,6 +137,7 @@ namespace JsRuntime { std::list getApplications(); void setExternalApplicationHandler(std::shared_ptr handler); std::string getBaseUserAgent(); + void setLocation(std::stringstream& window, const std::string& url); private: bool downloadFile(std::string& url, MemoryStruct& chunk); void processDevConsoleRequests(); diff --git a/src/NativeJSRenderer.cpp b/src/NativeJSRenderer.cpp index 7649032..a6f9809 100644 --- a/src/NativeJSRenderer.cpp +++ b/src/NativeJSRenderer.cpp @@ -362,8 +362,8 @@ void NativeJSRenderer::runApplicationInternal(ApplicationRequest& appRequest) if(context->getModuleSettings().enableMiniJSDOM || context->getModuleSettings().enableJSDOM) { std::stringstream window; - window<<"window.location = {\"href\":\"" << url << "\"};"; - NativeJSLogger::log(INFO, "Adding the window location: %s to js file\n", window.str().c_str()); + setLocation(window, url); + NativeJSLogger::log(INFO, "Adding the window location: %s to js file\n", window.str().c_str()); context->runScript(window.str().c_str(),true, url, nullptr, true); } NativeJSLogger::log(INFO, "nativeJS application thunder execution url: %s\n", url.c_str()); @@ -377,12 +377,13 @@ void NativeJSRenderer::runApplicationInternal(ApplicationRequest& appRequest) { NativeJSLogger::log(INFO, "About to launch local app\n"); JavaScriptContext* context = (JavaScriptContext*)mContextMap[id].context; - context->setUrl(mContextMap[id].url); + context->setUrl(mContextMap[id].url); if(context->getModuleSettings().enableMiniJSDOM || context->getModuleSettings().enableJSDOM) { std::stringstream window; - window<<"window.location = {\"href\":\"file:/" << url << "\"};"; - NativeJSLogger::log(INFO, "Adding the window location: %s to js file\n", window.str().c_str()); + std::string localUrl = "file://" + url; + setLocation(window, localUrl); + NativeJSLogger::log(INFO, "Adding the window location: %s to js file\n", window.str().c_str()); context->runScript(window.str().c_str(),true, url, nullptr, true); } NativeJSLogger::log(INFO, "Running test application: %s\n", url.c_str()); @@ -689,4 +690,66 @@ std::string NativeJSRenderer::getBaseUserAgent() { return mBaseUserAgent; } +void NativeJSRenderer::setLocation(std::stringstream& window, const std::string& url) +{ + auto schemePos = url.find("://"); + + std::string protocol, host, hostname, port, pathname, search, hash, origin = "null"; + + if (schemePos != std::string::npos) + { + protocol = url.substr(0, schemePos) + ":"; + + auto authorityStart = schemePos + 3; + auto pathStart = url.find_first_of("/?#", authorityStart); + host = (pathStart == std::string::npos) + ? url.substr(authorityStart) + : url.substr(authorityStart, pathStart - authorityStart); + + auto portPos = host.find(':'); + if (portPos != std::string::npos) + { + hostname = host.substr(0, portPos); + port = host.substr(portPos + 1); + } + else + { + hostname = host; + } + + if (pathStart != std::string::npos) + { + auto queryPos = url.find('?', pathStart); + auto hashPos = url.find('#', pathStart); + + auto pathEnd = std::min( + queryPos == std::string::npos ? url.size() : queryPos, + hashPos == std::string::npos ? url.size() : hashPos); + + pathname = url.substr(pathStart, pathEnd - pathStart); + + if (queryPos != std::string::npos) + search = url.substr(queryPos, + (hashPos == std::string::npos ? url.size() : hashPos) - queryPos); + + if (hashPos != std::string::npos) + hash = url.substr(hashPos); + } + + if (protocol != "file:") + origin = protocol + "//" + host; + } + + window << "window.location={" + << "\"href\":\"" << url << "\"," + << "\"protocol\":\"" << protocol << "\"," + << "\"host\":\"" << host << "\"," + << "\"hostname\":\"" << hostname << "\"," + << "\"port\":\"" << port << "\"," + << "\"pathname\":\"" << pathname << "\"," + << "\"search\":\"" << search << "\"," + << "\"hash\":\"" << hash << "\"," + << "\"origin\":\"" << origin << "\"" + << "};"; +} diff --git a/src/jsc/modules/linkedjsdomwrapper.js b/src/jsc/modules/linkedjsdomwrapper.js index e6d70e5..30b2cee 100755 --- a/src/jsc/modules/linkedjsdomwrapper.js +++ b/src/jsc/modules/linkedjsdomwrapper.js @@ -17,6 +17,56 @@ * limitations under the License. **/ + +if (typeof Error.captureStackTrace !== 'function') { + Error.captureStackTrace = function(targetObject, constructorOpt) { + try { + var stack = new Error().stack; + if (stack && targetObject) { + var lines = stack.split('\n'); + targetObject.stack = lines.slice(1).join('\n'); + } + if (typeof console !== 'undefined' && typeof console.log === 'function') { + var ctorName = (constructorOpt && constructorOpt.name) ? constructorOpt.name : 'unknown'; + console.log('Error.captureStackTrace called for ' + ctorName + ' (polyfilled no-op, previously would have thrown and hung the fetch promise)'); + + if (targetObject && targetObject.message) { + console.log('message: ' + targetObject.message); + } + if (targetObject && targetObject.type) { + console.log('type: ' + targetObject.type); + } + console.log('stack:\n' + (targetObject && targetObject.stack ? targetObject.stack : stack)); + } + } catch (e) { + } + }; +} + +(function ppGuardGetOwnPropertySymbols() { + if (typeof Object.getOwnPropertySymbols !== 'function') { + return; + } + var originalGetOwnPropertySymbols = Object.getOwnPropertySymbols; + if (originalGetOwnPropertySymbols.__ppGuarded) { + return; + } + var guarded = function(obj) { + if (obj === undefined || obj === null) { + try { + if (typeof console !== 'undefined' && typeof console.log === 'function') { + console.log('Object.getOwnPropertySymbols called with ' + obj + ', returning [] instead of throwing'); + } + } catch (e) { + } + return []; + } + return originalGetOwnPropertySymbols(obj); + }; + guarded.__ppGuarded = true; + Object.getOwnPropertySymbols = guarded; +})(); + LinkedJSDOM = LinkedJSDOMLib; function JSDOM(html) { @@ -27,9 +77,11 @@ var jsdom = new JSDOM(''); document = jsdom.document; global.document = document; window = jsdom.window; +global.window = window; Event = window.Event; DOMParser = window.DOMParser; navigator = window.navigator; +global.navigator = navigator; tv = window.tv = {} //fetch = FetchLib; try @@ -44,6 +96,8 @@ catch(e) XMLHttpRequest = window.XMLHttpRequest; HTMLElement = window.HTMLElement; window.location = {"href":"", "host":"127.0.0.1", "protocol":"http"} +Image = window.Image; + //below all are undefined /* @@ -84,3 +138,170 @@ crypto.getRandomValues = getRandom; setInterval(checkUrlChange, 500); })(); + +(function() { + + function run(code, url) { + try { + (0, eval)(code + "\n//# sourceURL=" + url); + return null; + } catch (e) { + return e; + } + } + + function load(url, ok, fail) { + var x = new XMLHttpRequest(); + x.open("GET", url, true); + + x.onreadystatechange = function() { + if (x.readyState !== 4) return; + + if (x.status >= 200 && x.status < 300) + ok(x.responseText || ""); + else + fail(new Error("HTTP " + x.status)); + }; + + x.onerror = fail; + x.send(); + } + + var orig = HTMLElement.prototype.appendChild; + + HTMLElement.prototype.appendChild = function(node) { + + var ret = orig.call(this, node); + + if ( + node && + node.tagName && + node.tagName.toLowerCase() === "script" && + node.src + ) { + load( + node.src, + function(code) { + + var err = run(code, node.src); + + if (err) + return node.onerror && node.onerror(err); + + node.readyState = "complete"; + + if (typeof node.onreadystatechange === "function") + node.onreadystatechange.call(node); + + if (typeof node.dispatchEvent === "function" && typeof Event === "function") { + node.dispatchEvent(new Event("load")); + } else if ( + typeof node.onload === "function" && + node.onload !== node.onreadystatechange + ) { + node.onload.call(node); + } + }, + function(err) { + if (typeof node.dispatchEvent === "function" && typeof Event === "function") { + node.dispatchEvent(new Event("error")); + } else if (typeof node.onerror === "function") { + node.onerror(err); + } + } + ); + } + + return ret; + }; +})(); + +setTimeout.apply = function(thisArg, args) { + if (!args || args.length === 0) return setTimeout(function() {}, 0); + var callback = args[0]; + var delay = args[1]; + var callbackArgs = args.slice(2); + return setTimeout(function() { + callback.apply(null, callbackArgs); + }, delay); +}; +setTimeout.call = function(thisArg, callback, delay) { + var args = Array.prototype.slice.call(arguments, 3); + return setTimeout(function() { + callback.apply(null, args); + }, delay); +}; +setTimeout.bind = function(thisArg) { + var boundArgs = Array.prototype.slice.call(arguments, 1); + return function() { + var args = boundArgs.concat(Array.prototype.slice.call(arguments)); + return setTimeout.apply(null, args); + }; +}; + +clearTimeout = (function(originalClearTimeout) { + return function(id) { + if (id === null || typeof id === "undefined") { + return undefined; + } + return originalClearTimeout(id); + }; +})(clearTimeout); + +clearTimeout.apply = function(thisArg, args) { + return clearTimeout(args && args[0]); +}; +clearTimeout.call = function(thisArg, id) { + return clearTimeout(id); +}; +clearTimeout.bind = function(thisArg) { + return function(id) { return clearTimeout(id); }; +}; + +setInterval.apply = function(thisArg, args) { + if (!args || args.length === 0) return setInterval(function() {}, 0); + var callback = args[0]; + var delay = args[1]; + var callbackArgs = args.slice(2); + return setInterval(function() { + callback.apply(null, callbackArgs); + }, delay); +}; +setInterval.call = function(thisArg, callback, delay) { + var args = Array.prototype.slice.call(arguments, 3); + return setInterval(function() { + callback.apply(null, args); + }, delay); +}; +setInterval.bind = function(thisArg) { + var boundArgs = Array.prototype.slice.call(arguments, 1); + return function() { + var args = boundArgs.concat(Array.prototype.slice.call(arguments)); + return setInterval.apply(null, args); + }; +}; + +clearInterval = (function(originalClearInterval) { + return function(id) { + if (id === null || typeof id === "undefined") { + return undefined; + } + return originalClearInterval(id); + }; +})(clearInterval); + +clearInterval.apply = function(thisArg, args) { + return clearInterval(args && args[0]); +}; +clearInterval.call = function(thisArg, id) { + return clearInterval(id); +}; +clearInterval.bind = function(thisArg) { + return function(id) { return clearInterval(id); }; +}; + +window.setInterval = setInterval; +window.clearTimeout = clearTimeout; +window.setTimeout = setTimeout; +window.clearInterval = clearInterval; + diff --git a/src/jsc/modules/node-fetch.js b/src/jsc/modules/node-fetch.js index 75b2c10..b7657c0 100755 --- a/src/jsc/modules/node-fetch.js +++ b/src/jsc/modules/node-fetch.js @@ -1188,6 +1188,44 @@ const INTERNALS = Symbol('Body internals'); // fix an issue where "PassThrough" isn't a named export for node <10 const PassThrough = Stream.PassThrough; +function isStreamLike(body) { + return !!body + && typeof body.on === 'function' + && (typeof body.pipe === 'function' || typeof body.once === 'function'); +} + +function getErrorReason(err) { + if (!err) { + return 'unknown error'; + } + + if (typeof err === 'string') { + return err; + } + + if (err.message) { + return err.message; + } + + if (err.description) { + return err.description; + } + + if (err.reason) { + return err.reason; + } + + if (err.code !== undefined) { + return 'code: ' + err.code; + } + + try { + return JSON.stringify(err); + } catch (e) { + return String(err); + } +} + /** * Body mixin * @@ -1219,7 +1257,7 @@ function Body(body) { } else if (ArrayBuffer.isView(body)) { // body is ArrayBufferView body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); - } else if (body instanceof Stream) ; else { + } else if (body instanceof Stream || isStreamLike(body)) ; else { // none of the above // coerce to string then buffer //body = Buffer.from(String(body)); @@ -1234,7 +1272,7 @@ function Body(body) { this.size = size; this.timeout = timeout; - if (body instanceof Stream) { + if (body instanceof Stream || isStreamLike(body)) { body.on('error', function (err) { const error = err.name === 'AbortError' ? err : new FetchError(`Invalid response body while trying to fetch ${_this.url}: ${err.message}`, 'system', err); _this[INTERNALS].error = error; @@ -1289,8 +1327,12 @@ Body.prototype = { var _this2 = this; return consumeBody.call(this).then(function (buffer) { + var text = buffer.toString(); + if (text.trim() === '') { + return {}; + } try { - return JSON.parse(buffer.toString()); + return JSON.parse(text); } catch (err) { return Body.Promise.reject(new FetchError(`invalid json response body at ${_this2.url} reason: ${err.message}`, 'invalid-json')); } @@ -1390,7 +1432,7 @@ function consumeBody() { } // istanbul ignore if: should never happen - if (!(body instanceof Stream)) { + if (!(body instanceof Stream) && !isStreamLike(body)) { return Body.Promise.resolve(Buffer.alloc(0)); } @@ -1419,7 +1461,7 @@ function consumeBody() { reject(err); } else { // other errors, such as incorrect content-encoding - reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, 'system', err)); + reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${getErrorReason(err)}`, 'system', err)); } }); @@ -1428,6 +1470,10 @@ function consumeBody() { return; } + if (!Buffer.isBuffer(chunk)) { + chunk = Buffer.from(chunk); + } + if (_this4.size && accumBytes + chunk.length > _this4.size) { abort = true; reject(new FetchError(`content size at ${_this4.url} over limit: ${_this4.size}`, 'max-size')); @@ -1564,6 +1610,9 @@ function clone(instance) { // check that body is a stream and not form-data object // note: we can't clone the form-data object without having it as a dependency if (body instanceof Stream && typeof body.getBoundary !== 'function') { + if (typeof PassThrough !== 'function') { + throw new Error('cannot clone body stream: PassThrough is not available in this runtime'); + } // tee instance body p1 = new PassThrough(); p2 = new PassThrough(); @@ -2444,6 +2493,7 @@ const URL$1 = Url.URL || whatwgUrl.URL; // fix an issue where "PassThrough", "resolve" aren't a named export for node <10 const PassThrough$1 = Stream.PassThrough; +const hasPassThrough$1 = typeof PassThrough$1 === 'function'; const isDomainOrSubdomain = function isDomainOrSubdomain(destination, original) { const orig = new URL$1(original).hostname; @@ -2527,17 +2577,15 @@ function fetch(url, opts) { clearTimeout(reqTimeout); } - if (request.timeout) { - req.once('socket', function (socket) { - reqTimeout = setTimeout(function () { - reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); - finalize(); - }, request.timeout); - }); - } + if (request.timeout) { + reqTimeout = setTimeout(function () { + reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); + finalize(); + }, request.timeout); + } req.on('error', function (err) { - reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); + reject(new FetchError(`request to ${request.url} failed, reason: ${getErrorReason(err)}`, 'system', err)); if (response && response.body) { destroyStream(response.body, err); @@ -2678,15 +2726,8 @@ function fetch(url, opts) { } } - //MADANA HACK - let body = undefined; - // prepare response - res.once('data', function (data) { - body = data; - }); - res.once('end', function () { - if (signal) signal.removeEventListener('abort', abortAndFinalize); - //let body = res.pipe(new PassThrough$1()); + if (signal) signal.removeEventListener('abort', abortAndFinalize); + let body = hasPassThrough$1 ? res.pipe(new PassThrough$1()) : res; const response_options = { url: request.url, @@ -2709,7 +2750,7 @@ function fetch(url, opts) { // 3. no Content-Encoding header // 4. no content response (204) // 5. content not modified response (304) - if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { + if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { response = new Response(body, response_options); resolve(response); return; @@ -2740,6 +2781,12 @@ function fetch(url, opts) { // for deflate if (codings == 'deflate' || codings == 'x-deflate') { + if (!hasPassThrough$1) { + body = body.pipe(zlib.createInflate()); + response = new Response(body, response_options); + resolve(response); + return; + } // handle the infamous raw deflate response from old servers // a hack for old IIS and Apache servers const raw = res.pipe(new PassThrough$1()); @@ -2776,11 +2823,9 @@ function fetch(url, opts) { console.warn(`[DEBUG] Content-Encoding '${codings}' detected but zlib not available for decompression`); } } - // otherwise, use response as-is - response = new Response(body, response_options); - console.log(response); - resolve(response); - }); + // otherwise, use response as-is + response = new Response(body, response_options); + resolve(response); }); writeToStream(req, request); diff --git a/utils/xhr.js b/utils/xhr.js index a5e1d77..48fee8a 100644 --- a/utils/xhr.js +++ b/utils/xhr.js @@ -22,6 +22,8 @@ XMLHttpRequest = function() { // Holds http.js objects var request; var response; + var requestTimeoutId = null; + var didTimeout = false; // Request settings var settings = {}; @@ -103,6 +105,8 @@ XMLHttpRequest = function() { this.responseXML = ""; this.status = null; this.statusText = null; + this.timeout = 0; + this.ontimeout = null; // Whether cross-site Access-Control requests should be made using // credentials such as cookies or authorization headers @@ -153,6 +157,11 @@ XMLHttpRequest = function() { request = null; } response = null; + if (requestTimeoutId) { + clearTimeout(requestTimeoutId); + requestTimeoutId = null; + } + didTimeout = false; headers = {}; headersCase = {}; sendFlag = false; @@ -367,6 +376,15 @@ XMLHttpRequest = function() { return; } var doRequest = transport.request; + var redirectCount = 0; + var maxRedirects = 10; + + var clearRequestTimeout = function() { + if (requestTimeoutId) { + clearTimeout(requestTimeoutId); + requestTimeoutId = null; + } + }; // Request is being sent, set send flag sendFlag = true; @@ -376,6 +394,7 @@ XMLHttpRequest = function() { // Handler for the response var responseHandler = function responseHandler(resp) { + clearRequestTimeout(); // Set response var to the response we got back // This is so it remains accessable outside this scope response = resp; @@ -383,6 +402,10 @@ XMLHttpRequest = function() { // @TODO Prevent looped redirects if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) { try { + redirectCount += 1; + if (redirectCount > maxRedirects) { + throw new Error("XMLHttpRequest: maximum redirects exceeded (" + maxRedirects + ")"); + } var redirectLocation = response.headers.location; var redirectUrl = new URL(redirectLocation, settings.url); settings.url = redirectUrl.toString(); @@ -429,6 +452,7 @@ XMLHttpRequest = function() { }); response.on("end", function() { + clearRequestTimeout(); if (sendFlag) { // Discard the end event if the connection has been aborted setState(self.DONE); @@ -437,12 +461,14 @@ XMLHttpRequest = function() { }); response.on("error", function(error) { + clearRequestTimeout(); self.handleError(error); }); }; // Error handler for the request var errorHandler = function errorHandler(error) { + clearRequestTimeout(); self.handleError(error); }; @@ -456,10 +482,35 @@ XMLHttpRequest = function() { request.on("error", errorHandler); if (typeof request.on === "function") { request.on("abort", function() { + if (didTimeout) { + return; + } + clearRequestTimeout(); self.handleError(new Error("XMLHttpRequest: request aborted by transport")); }); } + if (this.timeout > 0) { + requestTimeoutId = setTimeout(function() { + if (!sendFlag) { + return; + } + didTimeout = true; + if (request) { + request.abort(); + request = null; + } + self.status = 0; + self.statusText = "timeout"; + self.responseText = ""; + errorFlag = true; + sendFlag = false; + setState(self.DONE); + self.dispatchEvent("timeout"); + self.dispatchEvent("loadend"); + }, this.timeout); + } + // Node 0.4 and later won't accept empty data. Make sure it's needed. if (data) { request.write(data); @@ -479,12 +530,17 @@ XMLHttpRequest = function() { * Called when an error is encountered to deal with it. */ this.handleError = function(error) { + if (requestTimeoutId) { + clearTimeout(requestTimeoutId); + requestTimeoutId = null; + } this.status = 0; - this.statusText = error; - this.responseText = error.stack; + this.statusText = error && error.message ? error.message : String(error); + this.responseText = error && error.stack ? error.stack : this.statusText; errorFlag = true; setState(this.DONE); this.dispatchEvent('error'); + this.dispatchEvent('loadend'); }; /** @@ -498,8 +554,16 @@ XMLHttpRequest = function() { request = null; } - headers = defaultHeaders; + if (requestTimeoutId) { + clearTimeout(requestTimeoutId); + requestTimeoutId = null; + } + didTimeout = false; + + headers = {}; + headersCase = {}; this.status = 0; + this.statusText = null; this.responseText = ""; this.responseXML = ""; @@ -582,3 +646,4 @@ XMLHttpRequest = function() { } }; }; +