diff --git a/Resources/ReactUnity/polyfills/abortcontroller.js b/Resources/ReactUnity/polyfills/abortcontroller.js new file mode 100644 index 00000000..98c05fae --- /dev/null +++ b/Resources/ReactUnity/polyfills/abortcontroller.js @@ -0,0 +1,385 @@ +https://cdn.jsdelivr.net/npm/abortcontroller-polyfill@1.7.5/dist/abortcontroller-polyfill-only.js +(function (factory) { + typeof define === 'function' && define.amd ? define(factory) : + factory(); +})((function () { 'use strict'; + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + Object.defineProperty(Constructor, "prototype", { + writable: false + }); + return Constructor; + } + + function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function"); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + writable: true, + configurable: true + } + }); + Object.defineProperty(subClass, "prototype", { + writable: false + }); + if (superClass) _setPrototypeOf(subClass, superClass); + } + + function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { + return o.__proto__ || Object.getPrototypeOf(o); + }; + return _getPrototypeOf(o); + } + + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + return _setPrototypeOf(o, p); + } + + function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) return false; + if (Reflect.construct.sham) return false; + if (typeof Proxy === "function") return true; + + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); + return true; + } catch (e) { + return false; + } + } + + function _assertThisInitialized(self) { + if (self === void 0) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return self; + } + + function _possibleConstructorReturn(self, call) { + if (call && (typeof call === "object" || typeof call === "function")) { + return call; + } else if (call !== void 0) { + throw new TypeError("Derived constructors may only return object or undefined"); + } + + return _assertThisInitialized(self); + } + + function _createSuper(Derived) { + var hasNativeReflectConstruct = _isNativeReflectConstruct(); + + return function _createSuperInternal() { + var Super = _getPrototypeOf(Derived), + result; + + if (hasNativeReflectConstruct) { + var NewTarget = _getPrototypeOf(this).constructor; + + result = Reflect.construct(Super, arguments, NewTarget); + } else { + result = Super.apply(this, arguments); + } + + return _possibleConstructorReturn(this, result); + }; + } + + function _superPropBase(object, property) { + while (!Object.prototype.hasOwnProperty.call(object, property)) { + object = _getPrototypeOf(object); + if (object === null) break; + } + + return object; + } + + function _get() { + if (typeof Reflect !== "undefined" && Reflect.get) { + _get = Reflect.get.bind(); + } else { + _get = function _get(target, property, receiver) { + var base = _superPropBase(target, property); + + if (!base) return; + var desc = Object.getOwnPropertyDescriptor(base, property); + + if (desc.get) { + return desc.get.call(arguments.length < 3 ? target : receiver); + } + + return desc.value; + }; + } + + return _get.apply(this, arguments); + } + + var Emitter = /*#__PURE__*/function () { + function Emitter() { + _classCallCheck(this, Emitter); + + Object.defineProperty(this, 'listeners', { + value: {}, + writable: true, + configurable: true + }); + } + + _createClass(Emitter, [{ + key: "addEventListener", + value: function addEventListener(type, callback, options) { + if (!(type in this.listeners)) { + this.listeners[type] = []; + } + + this.listeners[type].push({ + callback: callback, + options: options + }); + } + }, { + key: "removeEventListener", + value: function removeEventListener(type, callback) { + if (!(type in this.listeners)) { + return; + } + + var stack = this.listeners[type]; + + for (var i = 0, l = stack.length; i < l; i++) { + if (stack[i].callback === callback) { + stack.splice(i, 1); + return; + } + } + } + }, { + key: "dispatchEvent", + value: function dispatchEvent(event) { + if (!(event.type in this.listeners)) { + return; + } + + var stack = this.listeners[event.type]; + var stackToCall = stack.slice(); + + for (var i = 0, l = stackToCall.length; i < l; i++) { + var listener = stackToCall[i]; + + try { + listener.callback.call(this, event); + } catch (e) { + Promise.resolve().then(function () { + throw e; + }); + } + + if (listener.options && listener.options.once) { + this.removeEventListener(event.type, listener.callback); + } + } + + return !event.defaultPrevented; + } + }]); + + return Emitter; + }(); + + var AbortSignal = /*#__PURE__*/function (_Emitter) { + _inherits(AbortSignal, _Emitter); + + var _super = _createSuper(AbortSignal); + + function AbortSignal() { + var _this; + + _classCallCheck(this, AbortSignal); + + _this = _super.call(this); // Some versions of babel does not transpile super() correctly for IE <= 10, if the parent + // constructor has failed to run, then "this.listeners" will still be undefined and then we call + // the parent constructor directly instead as a workaround. For general details, see babel bug: + // https://github.com/babel/babel/issues/3041 + // This hack was added as a fix for the issue described here: + // https://github.com/Financial-Times/polyfill-library/pull/59#issuecomment-477558042 + + if (!_this.listeners) { + Emitter.call(_assertThisInitialized(_this)); + } // Compared to assignment, Object.defineProperty makes properties non-enumerable by default and + // we want Object.keys(new AbortController().signal) to be [] for compat with the native impl + + + Object.defineProperty(_assertThisInitialized(_this), 'aborted', { + value: false, + writable: true, + configurable: true + }); + Object.defineProperty(_assertThisInitialized(_this), 'onabort', { + value: null, + writable: true, + configurable: true + }); + Object.defineProperty(_assertThisInitialized(_this), 'reason', { + value: undefined, + writable: true, + configurable: true + }); + return _this; + } + + _createClass(AbortSignal, [{ + key: "toString", + value: function toString() { + return '[object AbortSignal]'; + } + }, { + key: "dispatchEvent", + value: function dispatchEvent(event) { + if (event.type === 'abort') { + this.aborted = true; + + if (typeof this.onabort === 'function') { + this.onabort.call(this, event); + } + } + + _get(_getPrototypeOf(AbortSignal.prototype), "dispatchEvent", this).call(this, event); + } + }]); + + return AbortSignal; + }(Emitter); + var AbortController = /*#__PURE__*/function () { + function AbortController() { + _classCallCheck(this, AbortController); + + // Compared to assignment, Object.defineProperty makes properties non-enumerable by default and + // we want Object.keys(new AbortController()) to be [] for compat with the native impl + Object.defineProperty(this, 'signal', { + value: new AbortSignal(), + writable: true, + configurable: true + }); + } + + _createClass(AbortController, [{ + key: "abort", + value: function abort(reason) { + var event; + + try { + event = new Event('abort'); + } catch (e) { + if (typeof document !== 'undefined') { + if (!document.createEvent) { + // For Internet Explorer 8: + event = document.createEventObject(); + event.type = 'abort'; + } else { + // For Internet Explorer 11: + event = document.createEvent('Event'); + event.initEvent('abort', false, false); + } + } else { + // Fallback where document isn't available: + event = { + type: 'abort', + bubbles: false, + cancelable: false + }; + } + } + + var signalReason = reason; + + if (signalReason === undefined) { + if (typeof document === 'undefined') { + signalReason = new Error('This operation was aborted'); + signalReason.name = 'AbortError'; + } else { + try { + signalReason = new DOMException('signal is aborted without reason'); + } catch (err) { + // IE 11 does not support calling the DOMException constructor, use a + // regular error object on it instead. + signalReason = new Error('This operation was aborted'); + signalReason.name = 'AbortError'; + } + } + } + + this.signal.reason = signalReason; + this.signal.dispatchEvent(event); + } + }, { + key: "toString", + value: function toString() { + return '[object AbortController]'; + } + }]); + + return AbortController; + }(); + + if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { + // These are necessary to make sure that we get correct output for: + // Object.prototype.toString.call(new AbortController()) + AbortController.prototype[Symbol.toStringTag] = 'AbortController'; + AbortSignal.prototype[Symbol.toStringTag] = 'AbortSignal'; + } + + function polyfillNeeded(self) { + if (self.__FORCE_INSTALL_ABORTCONTROLLER_POLYFILL) { + console.log('__FORCE_INSTALL_ABORTCONTROLLER_POLYFILL=true is set, will force install polyfill'); + return true; + } // Note that the "unfetch" minimal fetch polyfill defines fetch() without + // defining window.Request, and this polyfill need to work on top of unfetch + // so the below feature detection needs the !self.AbortController part. + // The Request.prototype check is also needed because Safari versions 11.1.2 + // up to and including 12.1.x has a window.AbortController present but still + // does NOT correctly implement abortable fetch: + // https://bugs.webkit.org/show_bug.cgi?id=174980#c2 + + + return typeof self.Request === 'function' && !self.Request.prototype.hasOwnProperty('signal') || !self.AbortController; + } + + (function (self) { + + if (!polyfillNeeded(self)) { + return; + } + + self.AbortController = AbortController; + self.AbortSignal = AbortSignal; + })(typeof self !== 'undefined' ? self : global); + +})); diff --git a/Resources/ReactUnity/polyfills/abortcontroller.js.meta b/Resources/ReactUnity/polyfills/abortcontroller.js.meta new file mode 100644 index 00000000..9eddf32a --- /dev/null +++ b/Resources/ReactUnity/polyfills/abortcontroller.js.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: b14017f71eca05b4e8b3e8722ee528ee +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: a6814ccfd69dca246adc5eab902a45db, type: 3} diff --git a/Resources/ReactUnity/polyfills/fetch.js b/Resources/ReactUnity/polyfills/fetch.js index d43f99d3..90415720 100644 --- a/Resources/ReactUnity/polyfills/fetch.js +++ b/Resources/ReactUnity/polyfills/fetch.js @@ -1,20 +1,24 @@ +// https://cdn.jsdelivr.net/npm/whatwg-fetch@3.6.20/dist/fetch.umd.js (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (factory((global.WHATWGFetch = {}))); }(this, (function (exports) { 'use strict'; - var global = + /* eslint-disable no-prototype-builtins */ + var g = (typeof globalThis !== 'undefined' && globalThis) || (typeof self !== 'undefined' && self) || - (typeof global !== 'undefined' && global); + // eslint-disable-next-line no-undef + (typeof global !== 'undefined' && global) || + {}; var support = { - searchParams: 'URLSearchParams' in global, - iterable: 'Symbol' in global && 'iterator' in Symbol, + searchParams: 'URLSearchParams' in g, + iterable: 'Symbol' in g && 'iterator' in Symbol, blob: - 'FileReader' in global && - 'Blob' in global && + 'FileReader' in g && + 'Blob' in g && (function() { try { new Blob(); @@ -23,8 +27,8 @@ return false } })(), - formData: 'FormData' in global, - arrayBuffer: 'ArrayBuffer' in global + formData: 'FormData' in g, + arrayBuffer: 'ArrayBuffer' in g }; function isDataView(obj) { @@ -56,7 +60,7 @@ name = String(name); } if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') { - throw new TypeError('Invalid character in header field name') + throw new TypeError('Invalid character in header field name: "' + name + '"') } return name.toLowerCase() } @@ -95,6 +99,9 @@ }, this); } else if (Array.isArray(headers)) { headers.forEach(function(header) { + if (header.length != 2) { + throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length) + } this.append(header[0], header[1]); }, this); } else if (headers) { @@ -165,6 +172,7 @@ } function consumed(body) { + if (body._noBody) return if (body.bodyUsed) { return Promise.reject(new TypeError('Already read')) } @@ -192,7 +200,9 @@ function readBlobAsText(blob) { var reader = new FileReader(); var promise = fileReaderReady(reader); - reader.readAsText(blob); + var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type); + var encoding = match ? match[1] : 'utf-8'; + reader.readAsText(blob, encoding); return promise } @@ -230,9 +240,11 @@ semantic of setting Request.bodyUsed in the constructor before _initBody is called. */ + // eslint-disable-next-line no-self-assign this.bodyUsed = this.bodyUsed; this._bodyInit = body; if (!body) { + this._noBody = true; this._bodyText = ''; } else if (typeof body === 'string') { this._bodyText = body; @@ -280,28 +292,29 @@ return Promise.resolve(new Blob([this._bodyText])) } }; + } - this.arrayBuffer = function() { - if (this._bodyArrayBuffer) { - var isConsumed = consumed(this); - if (isConsumed) { - return isConsumed - } - if (ArrayBuffer.isView(this._bodyArrayBuffer)) { - return Promise.resolve( - this._bodyArrayBuffer.buffer.slice( - this._bodyArrayBuffer.byteOffset, - this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength - ) + this.arrayBuffer = function() { + if (this._bodyArrayBuffer) { + var isConsumed = consumed(this); + if (isConsumed) { + return isConsumed + } else if (ArrayBuffer.isView(this._bodyArrayBuffer)) { + return Promise.resolve( + this._bodyArrayBuffer.buffer.slice( + this._bodyArrayBuffer.byteOffset, + this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength ) - } else { - return Promise.resolve(this._bodyArrayBuffer) - } + ) } else { - return this.blob().then(readBlobAsArrayBuffer) + return Promise.resolve(this._bodyArrayBuffer) } - }; - } + } else if (support.blob) { + return this.blob().then(readBlobAsArrayBuffer) + } else { + throw new Error('could not read as ArrayBuffer') + } + }; this.text = function() { var rejected = consumed(this); @@ -334,7 +347,7 @@ } // HTTP methods whose capitalization should be normalized - var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']; + var methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']; function normalizeMethod(method) { var upcased = method.toUpperCase(); @@ -375,7 +388,12 @@ } this.method = normalizeMethod(options.method || this.method || 'GET'); this.mode = options.mode || this.mode || null; - this.signal = options.signal || this.signal; + this.signal = options.signal || this.signal || (function () { + if ('AbortController' in g) { + var ctrl = new AbortController(); + return ctrl.signal; + } + }()); this.referrer = null; if ((this.method === 'GET' || this.method === 'HEAD') && body) { @@ -424,14 +442,26 @@ // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space // https://tools.ietf.org/html/rfc7230#section-3.2 var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' '); - preProcessedHeaders.split(/\r?\n/).forEach(function(line) { - var parts = line.split(':'); - var key = parts.shift().trim(); - if (key) { - var value = parts.join(':').trim(); - headers.append(key, value); - } - }); + // Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill + // https://github.com/github/fetch/issues/748 + // https://github.com/zloirock/core-js/issues/751 + preProcessedHeaders + .split('\r') + .map(function(header) { + return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header + }) + .forEach(function(line) { + var parts = line.split(':'); + var key = parts.shift().trim(); + if (key) { + var value = parts.join(':').trim(); + try { + headers.append(key, value); + } catch (error) { + console.warn('Response ' + error.message); + } + } + }); return headers } @@ -447,8 +477,11 @@ this.type = 'default'; this.status = options.status === undefined ? 200 : options.status; + if (this.status < 200 || this.status > 599) { + throw new RangeError("Failed to construct 'Response': The status provided (0) is outside the range [200, 599].") + } this.ok = this.status >= 200 && this.status < 300; - this.statusText = 'statusText' in options ? options.statusText : ''; + this.statusText = options.statusText === undefined ? '' : '' + options.statusText; this.headers = new Headers(options.headers); this.url = options.url || ''; this._initBody(bodyInit); @@ -466,7 +499,9 @@ }; Response.error = function() { - var response = new Response(null, {status: 0, statusText: ''}); + var response = new Response(null, {status: 200, statusText: ''}); + response.ok = false; + response.status = 0; response.type = 'error'; return response }; @@ -481,14 +516,17 @@ return new Response(null, {status: status, headers: {location: url}}) }; - exports.DOMException = function(message, name) { - this.message = message; - this.name = name; - var error = Error(message); - this.stack = error.stack; - }; - exports.DOMException.prototype = Object.create(Error.prototype); - exports.DOMException.prototype.constructor = exports.DOMException; + exports.DOMException = g.DOMException; + if (!exports.DOMException) { + exports.DOMException = function(message, name) { + this.message = message; + this.name = name; + var error = Error(message); + this.stack = error.stack; + }; + exports.DOMException.prototype = Object.create(Error.prototype); + exports.DOMException.prototype.constructor = exports.DOMException; + } function fetch(input, init) { return new Promise(function(resolve, reject) { @@ -506,10 +544,16 @@ xhr.onload = function() { var options = { - status: xhr.status, statusText: xhr.statusText, headers: parseHeaders(xhr.getAllResponseHeaders() || '') }; + // This check if specifically for when a user fetches a file locally from the file system + // Only if the status is out of a normal range + if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) { + options.status = 200; + } else { + options.status = xhr.status; + } options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL'); var body = 'response' in xhr ? xhr.response : xhr.responseText; setTimeout(function() { @@ -525,7 +569,7 @@ xhr.ontimeout = function() { setTimeout(function() { - reject(new TypeError('Network request failed')); + reject(new TypeError('Network request timed out')); }, 0); }; @@ -537,7 +581,7 @@ function fixUrl(url) { try { - return url === '' && global.location.href ? global.location.href : url + return url === '' && g.location.href ? g.location.href : url } catch (e) { return url } @@ -556,6 +600,7 @@ xhr.responseType = 'blob'; } else if ( support.arrayBuffer && + // TODO: This part is kept as before intentionally, because it fails in ReactUnity for some reason request.headers.get('Content-Type') && request.headers.get('Content-Type').indexOf('application/octet-stream') !== -1 ) { @@ -563,10 +608,17 @@ } } - if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers)) { + if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) { + var names = []; Object.getOwnPropertyNames(init.headers).forEach(function(name) { + names.push(normalizeName(name)); xhr.setRequestHeader(name, normalizeValue(init.headers[name])); }); + request.headers.forEach(function(value, name) { + if (names.indexOf(name) === -1) { + xhr.setRequestHeader(name, value); + } + }); } else { request.headers.forEach(function(value, name) { xhr.setRequestHeader(name, value); @@ -590,11 +642,11 @@ fetch.polyfill = true; - if (!global.fetch) { - global.fetch = fetch; - global.Headers = Headers; - global.Request = Request; - global.Response = Response; + if (!g.fetch) { + g.fetch = fetch; + g.Headers = Headers; + g.Request = Request; + g.Response = Response; } exports.Headers = Headers; diff --git a/Runtime/Scripting/EngineCapabilities.cs b/Runtime/Scripting/EngineCapabilities.cs index 7adf7ceb..a4187a5d 100644 --- a/Runtime/Scripting/EngineCapabilities.cs +++ b/Runtime/Scripting/EngineCapabilities.cs @@ -15,5 +15,6 @@ public enum EngineCapabilities URL = 64, Navigator = 128, Encoding = 256, + AbortController = 512, } } diff --git a/Runtime/Scripting/QuickJS/QuickJSEngine.cs b/Runtime/Scripting/QuickJS/QuickJSEngine.cs index 9a5408c9..45145986 100644 --- a/Runtime/Scripting/QuickJS/QuickJSEngine.cs +++ b/Runtime/Scripting/QuickJS/QuickJSEngine.cs @@ -27,6 +27,7 @@ public class QuickJSEngine : IJavaScriptEngine | EngineCapabilities.WebSocket | EngineCapabilities.Console | EngineCapabilities.Base64 + | EngineCapabilities.AbortController #endif | EngineCapabilities.None; diff --git a/Runtime/Scripting/ScriptContext.cs b/Runtime/Scripting/ScriptContext.cs index 35e8fd7e..2bb7e614 100644 --- a/Runtime/Scripting/ScriptContext.cs +++ b/Runtime/Scripting/ScriptContext.cs @@ -208,6 +208,9 @@ static void CreatePolyfills(IJavaScriptEngine engine) if (!engine.Capabilities.HasFlag(EngineCapabilities.Fetch)) engine.Execute(ResourcesHelper.GetPolyfill("fetch"), "ReactUnity/polyfills/fetch"); + + if (!engine.Capabilities.HasFlag(EngineCapabilities.AbortController)) + engine.Execute(ResourcesHelper.GetPolyfill("abortcontroller"), "ReactUnity/polyfills/abortcontroller"); } static void CreateScheduler(IJavaScriptEngine engine, ReactContext context) diff --git a/Tests/Runtime/Base/ReactiveTests.cs b/Tests/Runtime/Base/ReactiveTests.cs index f3eabeb1..18f2b4c4 100644 --- a/Tests/Runtime/Base/ReactiveTests.cs +++ b/Tests/Runtime/Base/ReactiveTests.cs @@ -204,6 +204,43 @@ public IEnumerator TestReactiveList() Assert.AreEqual("0", text.text); } + public class MyClass + { + public int value = 0; + public MyClass(int v) { this.value = v; } + } + + [UGUITest(Script = @" + export function App() { + const globals = ReactUnity.useGlobals(); + const w = ReactUnity.useReactiveValue(globals.testReactive); + return {w?.[0]?.value}; + } + ")] + public IEnumerator TestReactiveListIndexedAccess() + { + yield return null; + + var text = (Host.QuerySelector("text") as UGUI.TextComponent).Text; + Assert.AreEqual("", text.text); + + + var reactive = new ReactiveList() { + new MyClass(1), + new MyClass(2), + }; + + Globals.Set("testReactive", reactive); + yield return null; + yield return null; + Assert.AreEqual("1", text.text); + + reactive.RemoveAt(0); + yield return null; + yield return null; + Assert.AreEqual("2", text.text); + } + [UGUITest(Script = @" export function App() { const globals = ReactUnity.useGlobals();