diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b50f74d --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +web/ diff --git a/.pubignore b/.pubignore new file mode 100644 index 0000000..f8eb9f0 --- /dev/null +++ b/.pubignore @@ -0,0 +1,5 @@ +.vscode/ +*.code-workspace + +doc/ +web/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b93dfa9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0-dev.1 + +- Initial version. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68d0af1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2023 Yaroslav Vorobev and contributors. All rights reserved. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..30318db --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Fetch API. + +This package provides JavaScript bindings to [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + +## Features + +* Full request parameters coverage +* Abort requests +* Read response + * As text + * As Blob + * As Stream of Uint8List +* Support streaming of data +* Get access to redirect status +* Support non-200 responses diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..f30b9f1 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1 @@ +include: package:zekfad_lints/untyped/dart.yaml diff --git a/example/fetch_api_example.dart b/example/fetch_api_example.dart new file mode 100644 index 0000000..1136a70 --- /dev/null +++ b/example/fetch_api_example.dart @@ -0,0 +1,10 @@ +import 'package:fetch_api/fetch_api.dart'; + + +void main() async { + final response = await fetch('https://proxy.cors.sh/https://example.com', RequestInit( + mode: RequestMode.cors, + ),); + + print(await response.text()); +} diff --git a/lib/fetch_api.dart b/lib/fetch_api.dart new file mode 100644 index 0000000..87c4e8e --- /dev/null +++ b/lib/fetch_api.dart @@ -0,0 +1,10 @@ +export 'src/abort_controller.dart'; +export 'src/abort_signal.dart'; +export 'src/fetch.dart'; +export 'src/headers.dart'; +export 'src/iterator_wrapper.dart'; +export 'src/readable_stream.dart'; +export 'src/readable_stream_default_reader.dart'; +export 'src/readable_stream_default_reader_chunk.dart'; +export 'src/request_init.dart'; +export 'src/response.dart'; diff --git a/lib/src/_js.dart b/lib/src/_js.dart new file mode 100644 index 0000000..4dfc733 --- /dev/null +++ b/lib/src/_js.dart @@ -0,0 +1,17 @@ +import 'dart:js'; +import 'dart:js_util'; + +export 'dart:js'; +export 'dart:js_util'; +export 'package:js/js.dart'; + + +typedef Promise = Object; + +extension JsObjectMapExtension on Map { + dynamic toJsObject() => jsify(this); +} + +extension JsArrayListExtension on List { + JsArray toJsArray() => JsArray.from(this); +} diff --git a/lib/src/abort_controller.dart b/lib/src/abort_controller.dart new file mode 100644 index 0000000..a2e195c --- /dev/null +++ b/lib/src/abort_controller.dart @@ -0,0 +1,20 @@ +import '_js.dart'; +import 'abort_signal.dart'; + + +@JS() +@staticInterop +class AbortController { + /// Creates a new [AbortController] object instance. + external factory AbortController(); +} + +extension AbortControllerExtension on AbortController { + /// Returns an `AbortSignal` object instance, which can be used to + /// communicate with, or to abort, a DOM request. + external final AbortSignal signal; + + /// Aborts a DOM request before it has completed. This is able to abort + /// fetch requests, consumption of any response bodies, and streams. + external void abort([dynamic reason]); +} diff --git a/lib/src/abort_signal.dart b/lib/src/abort_signal.dart new file mode 100644 index 0000000..603ee0d --- /dev/null +++ b/lib/src/abort_signal.dart @@ -0,0 +1,8 @@ +import '_js.dart'; + + +@JS() +@staticInterop +class AbortSignal { + external factory AbortSignal(); +} diff --git a/lib/src/fetch.dart b/lib/src/fetch.dart new file mode 100644 index 0000000..d5f2bad --- /dev/null +++ b/lib/src/fetch.dart @@ -0,0 +1,10 @@ +import '_js.dart'; +import 'request_init.dart'; +import 'response.dart'; + + +@JS('fetch') +external Promise _fetch(dynamic resource, RequestInit? options); + +Future fetch(dynamic resource, [RequestInit? options]) => + promiseToFuture(_fetch(resource, options)); diff --git a/lib/src/headers.dart b/lib/src/headers.dart new file mode 100644 index 0000000..63412b3 --- /dev/null +++ b/lib/src/headers.dart @@ -0,0 +1,85 @@ +import '_js.dart'; +import 'iterator.dart' as js; +import 'iterator_wrapper.dart'; + + +@JS() +@staticInterop +class Headers { + /// Creates empty [Headers]. + factory Headers() => Headers._(); + + /// Creates [Headers] from [Map]. + factory Headers.fromMap(Map init) => + Headers._init(init.toJsObject()); + + /// Creates [Headers] from array of 2 items arrays. + factory Headers.fromArray(List> init) { + final _init = JsArray>(); + for (final header in init) { + if (header.length != 2) + throw Exception('Bad argument'); + + _init.add(header.toJsArray()); + } + return Headers._init(_init); + } + + /// Creates a new [Headers] object. + @JS('Headers') + external factory Headers._(); + + /// Creates a new [Headers] object. + @JS('Headers') + external factory Headers._init(dynamic init); +} + +extension HeadersExtension on Headers { + /// Appends a new value onto an existing header inside a [Headers] object, + /// or adds the header if it does not already exist. + external void append(String name, String value); + + /// Deletes a header from a [Headers] object. + external void delete(String name); + + /// Returns an `iterator` allowing to go through all key/value pairs contained + /// in this object. + @JS('entries') + external js.Iterator> _entries(); + + // forEach() + + /// Returns a [String] sequence of all the values of a header within + /// a [Headers] object with a given name. + external String? get(String name); + + /// Returns a [bool] stating whether a [Headers] object contains + /// a certain header. + external bool has(String name); + + /// Returns an `iterator` allowing you to go through all keys of the key/value + /// pairs contained in this object. + @JS('keys') + external js.Iterator _keys(); + + /// Sets a new value for an existing header inside a [Headers] object, + /// or adds the header if it does not already exist. + external void set(String name, String value); + + /// Returns an `iterator` allowing you to go through all values of + /// the key/value pairs contained in this object. + @JS('values') + external js.Iterator _values(); + + /// Returns an [IteratorWrapper] allowing to go through all key/value pairs + /// contained in this object. + IteratorWrapper> entries() => IteratorWrapper(_entries()); + + /// Returns an [IteratorWrapper] allowing you to go through all keys of the + /// key/value pairs contained in this object. + IteratorWrapper keys() => IteratorWrapper(_keys()); + + /// Returns an [IteratorWrapper] allowing you to go through all values of + /// the key/value pairs contained in this object. + IteratorWrapper values() => IteratorWrapper(_values()); +} diff --git a/lib/src/iterator.dart b/lib/src/iterator.dart new file mode 100644 index 0000000..efa3e73 --- /dev/null +++ b/lib/src/iterator.dart @@ -0,0 +1,15 @@ +import '_js.dart'; +import 'iterator_result.dart'; + + +@JS() +@anonymous +class Iterator { + /// A function that accepts zero or one argument and returns an object + /// conforming to the [IteratorResult] interface. + /// + /// If a non-object value gets returned (such as false or undefined) + /// when a built-in language feature (such as for...of) is using the iterator, + /// a TypeError ("iterator.next() returned a non-object value") will be thrown. + external IteratorResult next(); +} diff --git a/lib/src/iterator_result.dart b/lib/src/iterator_result.dart new file mode 100644 index 0000000..fa83cc9 --- /dev/null +++ b/lib/src/iterator_result.dart @@ -0,0 +1,31 @@ +import '_js.dart'; + + +@JS() +@anonymous +class IteratorResult { + factory IteratorResult({ + bool done = false, + T? value, + }) => IteratorResult._( + done: done, + value: value, + ); + + external factory IteratorResult._({ + bool? done, + T? value, + }); + + /// A boolean that's `false` if the iterator was able to produce + /// the next value in the sequence. (This is equivalent to not specifying + /// the done property altogether.) + /// + /// Has the value `true` if the iterator has completed its sequence. + /// In this case, value optionally specifies the return value of the iterator. + external bool? done; + + /// Any JavaScript value returned by the iterator. + /// Can be omitted when [done] is `true`. + external T? value; +} diff --git a/lib/src/iterator_wrapper.dart b/lib/src/iterator_wrapper.dart new file mode 100644 index 0000000..f5afd97 --- /dev/null +++ b/lib/src/iterator_wrapper.dart @@ -0,0 +1,27 @@ +import 'dart:collection'; +import 'dart:core'; +import 'dart:core' as core; +import 'iterator.dart' as js; + + +/// Wrapper on top of JS iterator, providing [Iterable] and [Iterator] APIs. +class IteratorWrapper extends IterableBase implements core.Iterator { + IteratorWrapper(this._iterator); + + final js.Iterator _iterator; + + T? _current; + + @override + T get current => _current!; + + @override + bool moveNext() { + final next = _iterator.next(); + _current = next.value; + return !(next.done ?? false); + } + + @override + Iterator get iterator => this; +} diff --git a/lib/src/readable_stream.dart b/lib/src/readable_stream.dart new file mode 100644 index 0000000..039d53f --- /dev/null +++ b/lib/src/readable_stream.dart @@ -0,0 +1,54 @@ +import '_js.dart'; +import 'readable_stream_default_reader.dart'; + + +@JS() +@staticInterop +class ReadableStream { + /// Creates and returns a readable stream object from the given handlers. + external factory ReadableStream([ + dynamic underlyingSource, + dynamic queuingStrategy, + ]); +} + +extension ReadableStreamExtension on ReadableStream { + /// Returns a [bool] indicating whether or not the readable stream + /// is locked to a reader. + external final bool locked; + + /// Returns a `Promise` that resolves when the stream is canceled. + /// Calling this method signals a loss of interest in the stream by a consumer. + /// The supplied [reason] argument will be given to the underlying source, + /// which may or may not use it. + @JS('cancel') + external Promise _cancel([dynamic reason]); + + // pipeThrough() + // pipeTo() + + /// Creates a reader and locks the stream to it. + /// While the stream is locked, no other reader can be acquired until this one + /// is released. + /// + /// Implementation note: BYOP reader is unsupported, and therefore + /// no optional arguments provided. + external ReadableStreamDefaultReader getReader(); + + /// The [_tee] method tees this readable stream, returning a two-element + /// array containing the two resulting branches as new [ReadableStream] + /// instances. Each of those streams receives the same incoming data. + @JS('tee') + external List _tee(); + + /// Returns a [Future] that resolves when the stream is canceled. + /// Calling this method signals a loss of interest in the stream by a consumer. + /// The supplied [reason] argument will be given to the underlying source, + /// which may or may not use it. + Future cancel([T? reason]) => promiseToFuture(_cancel(reason)); + + /// The [tee] method tees this readable stream, returning a two-element + /// array containing the two resulting branches as new [ReadableStream] + /// instances. Each of those streams receives the same incoming data. + List tee() => _tee().cast(); +} diff --git a/lib/src/readable_stream_default_reader.dart b/lib/src/readable_stream_default_reader.dart new file mode 100644 index 0000000..190267e --- /dev/null +++ b/lib/src/readable_stream_default_reader.dart @@ -0,0 +1,64 @@ +import 'dart:typed_data'; + +import '_js.dart'; +import 'readable_stream.dart'; +import 'readable_stream_default_reader_chunk.dart'; + + +@JS() +@staticInterop +class ReadableStreamDefaultReader { + /// Creates and returns a [ReadableStreamDefaultReader] object instance. + external factory ReadableStreamDefaultReader(ReadableStream stream); +} + +extension ReadableStreamDefaultReaderExtension on ReadableStreamDefaultReader { + /// Returns a `Promise` that fulfills when the stream closes, + /// or rejects if the stream throws an error or the reader's lock is released. + /// This property enables you to write code that responds to an end to + /// the streaming process. + external final Promise closed; + + /// Returns a `Promise` that resolves when the stream is canceled. + /// Calling this method signals a loss of interest in the stream by a consumer. + /// The supplied [reason] argument will be given to the underlying source, + /// which may or may not use it. + @JS('cancel') + external Promise _cancel([dynamic reason]); + + /// Returns a promise providing access to the next chunk in the stream's + /// internal queue. + @JS('read') + external Promise _read(); + + /// Releases the reader's lock on the stream. + external void releaseLock(); + + /// Returns a [Future] that resolves when the stream is canceled. + /// Calling this method signals a loss of interest in the stream by a consumer. + /// The supplied [reason] argument will be given to the underlying source, + /// which may or may not use it. + Future cancel([T? reason]) => promiseToFuture(_cancel(reason)); + + /// Returns a [Future] providing access to the next chunk in the stream's + /// internal queue. + Future read() => promiseToFuture(_read()); + + /// Returns a [Future] that fulfills when the stream closes, + /// or rejects if the stream throws an error or the reader's lock is released. + /// This property enables you to write code that responds to an end to + /// the streaming process. + // ignore: unnecessary_this + Future get readerClosed => promiseToFuture(this.closed); + + /// Reads stream via [read] and returns chunks as soon as they are available. + Stream readAsStream() async* { + ReadableStreamDefaultReaderChunk chunk; + do { + chunk = await read(); + if (chunk.value != null) + yield chunk.value!; + } while (!chunk.done); + return; + } +} diff --git a/lib/src/readable_stream_default_reader_chunk.dart b/lib/src/readable_stream_default_reader_chunk.dart new file mode 100644 index 0000000..71fe44f --- /dev/null +++ b/lib/src/readable_stream_default_reader_chunk.dart @@ -0,0 +1,15 @@ +import 'dart:typed_data'; +import '_js.dart'; + + +@JS() +@anonymous +class ReadableStreamDefaultReaderChunk { + external factory ReadableStreamDefaultReaderChunk({ + Uint8List? value, + bool done, + }); + + external Uint8List? value; + external bool done; +} diff --git a/lib/src/request_init.dart b/lib/src/request_init.dart new file mode 100644 index 0000000..c95062e --- /dev/null +++ b/lib/src/request_init.dart @@ -0,0 +1,6 @@ +export 'request_init/request_cache.dart'; +export 'request_init/request_credentials.dart'; +export 'request_init/request_init.dart'; +export 'request_init/request_mode.dart'; +export 'request_init/request_redirect.dart'; +export 'request_init/request_referrer_policy.dart'; diff --git a/lib/src/request_init/request_cache.dart b/lib/src/request_init/request_cache.dart new file mode 100644 index 0000000..c3a900f --- /dev/null +++ b/lib/src/request_init/request_cache.dart @@ -0,0 +1,20 @@ +/// Mode indicating how the request will interact with the browser's HTTP +/// cache. +enum RequestCache { + byDefault('default'), + noStore('no-store'), + reload('reload'), + noCache('no-cache'), + forceCache('force-cache'), + onlyIfCached('only-if-cached'); + + const RequestCache(this.value); + + factory RequestCache.from(String value) => + values.firstWhere((element) => element.value == value); + + final String value; + + @override + String toString() => value; +} diff --git a/lib/src/request_init/request_credentials.dart b/lib/src/request_init/request_credentials.dart new file mode 100644 index 0000000..3d237a2 --- /dev/null +++ b/lib/src/request_init/request_credentials.dart @@ -0,0 +1,23 @@ +/// Controls what browsers do with credentials (cookies, HTTP authentication +/// entries, and TLS client certificates). +enum RequestCredentials { + /// Tells browsers to include credentials with requests to same-origin URLs, + /// and use any credentials sent back in responses from same-origin URLs. + sameOrigin('same-origin'), + /// Tells browsers to exclude credentials from the request, and ignore + /// any credentials sent back in the response (e.g., any Set-Cookie header). + omit('omit'), + /// Tells browsers to include credentials in both same- and cross-origin + /// requests, and always use any credentials sent back in responses. + cors('include'); + + const RequestCredentials(this.value); + + factory RequestCredentials.from(String value) => + values.firstWhere((element) => element.value == value); + + final String value; + + @override + String toString() => value; +} diff --git a/lib/src/request_init/request_init.dart b/lib/src/request_init/request_init.dart new file mode 100644 index 0000000..e46ffea --- /dev/null +++ b/lib/src/request_init/request_init.dart @@ -0,0 +1,143 @@ +import '../_js.dart'; +import '../abort_signal.dart'; +import '../headers.dart'; + +import 'request_cache.dart'; +import 'request_credentials.dart'; +import 'request_mode.dart'; +import 'request_redirect.dart'; +import 'request_referrer_policy.dart'; + + +/// An object containing any custom settings that you want to apply to the +/// request. +@JS() +@anonymous +class RequestInit { + factory RequestInit({ + String method = 'GET', + Headers? headers, + dynamic body, + RequestMode mode = RequestMode.noCors, + RequestCredentials credentials = RequestCredentials.sameOrigin, + RequestCache cache = RequestCache.byDefault, + RequestRedirect redirect = RequestRedirect.follow, + String referrer = '', + RequestReferrerPolicy referrerPolicy = RequestReferrerPolicy.strictOriginWhenCrossOrigin, + String integrity = '', + bool keepalive = false, + AbortSignal? signal, + }) => RequestInit._( + method: method, + headers: headers ?? Headers(), + body: body, + mode: mode.toString(), + credentials: credentials.toString(), + cache: cache.toString(), + redirect: redirect.toString(), + referrer: referrer, + referrerPolicy: referrerPolicy.toString(), + integrity: integrity, + keepalive: keepalive, + signal: signal, + ); + + external factory RequestInit._({ + String? method, + Headers? headers, + dynamic body, + String? mode, + String? credentials, + String? cache, + String? redirect, + String? referrer, + String? referrerPolicy, + String? integrity, + bool? keepalive, + AbortSignal? signal, + }); + + /// The request method, e.g., `GET`, POST. + /// + /// Note that the `Origin` header is not set on Fetch requests with a method + /// of `HEAD` or `GET`. + external String method; + + /// Any headers you want to add to your request, contained within a [Headers] + /// object or an object literal with [String] values. + /// + /// Note that some names are forbidden. + external Headers headers; + + /// Any body that you want to add to your request: this can be a Blob, + /// an ArrayBuffer, a TypedArray, a DataView, a FormData, a URLSearchParams, + /// string object or literal, or a ReadableStream object. + /// This latest possibility is still experimental; check the compatibility + /// information to verify you can use it. + /// + /// Note that a request using the GET or HEAD method cannot have a body. + external dynamic body; + + /// The mode you want to use for the request + external String mode; + + /// Controls what browsers do with credentials (cookies, HTTP authentication + /// entries, and TLS client certificates). + external String credentials; + + /// A string indicating how the request will interact with the browser's + /// HTTP cache. + external String cache; + + /// How to handle a redirect response. + external String redirect; + + /// A string specifying the referrer of the request. + /// This can be a same-origin URL, `about:client`, or an empty string. + external String referrer; + + /// Specifies the referrer policy to use for the request. + external String referrerPolicy; + + /// Contains the subresource integrity value of the request + /// (e.g.,`sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=`) + external String integrity; + + /// The `keepalive` option can be used to allow the request to outlive + /// the page. Fetch with the `keepalive` flag is a replacement for the + /// `Navigator.sendBeacon()` API. + external bool keepalive; + + /// An [AbortSignal] object instance; allows you to communicate + /// with a fetch request and abort it if desired via an `AbortController`. + external AbortSignal? signal; +} + +extension RequestInitExtension on RequestInit { + /// The mode you want to use for the request + RequestMode get requestMode => RequestMode.from(mode); + set requestMode(RequestMode requestMode) => + mode = requestMode.toString(); + + /// Controls what browsers do with credentials (cookies, HTTP authentication + /// entries, and TLS client certificates). + RequestCredentials get requestCredentials => RequestCredentials.from(credentials); + set requestCredentials(RequestCredentials requestCredentials) => + credentials = requestCredentials.toString(); + + /// A string indicating how the request will interact with the browser's + /// HTTP cache. + RequestCache get requestCache => RequestCache.from(cache); + set requestCache(RequestCache requestCache) => + cache = requestCache.toString(); + + /// How to handle a redirect response. + RequestRedirect get requestRedirect => RequestRedirect.from(redirect); + set requestRedirect(RequestRedirect requestRedirect) => + redirect = requestRedirect.toString(); + + /// Specifies the referrer policy to use for the request. + RequestReferrerPolicy get requestReferrerPolicy => RequestReferrerPolicy.from(referrerPolicy); + set requestReferrerPolicy(RequestReferrerPolicy requestReferrerPolicy) => + referrerPolicy = requestReferrerPolicy.toString(); +} diff --git a/lib/src/request_init/request_mode.dart b/lib/src/request_init/request_mode.dart new file mode 100644 index 0000000..c68db40 --- /dev/null +++ b/lib/src/request_init/request_mode.dart @@ -0,0 +1,16 @@ +/// The mode you want to use for the request +enum RequestMode { + noCors('no-cors'), + cors('cors'), + sameOrigin('same-origin'); + + const RequestMode(this.mode); + + factory RequestMode.from(String mode) => + values.firstWhere((element) => element.mode == mode); + + final String mode; + + @override + String toString() => mode; +} diff --git a/lib/src/request_init/request_redirect.dart b/lib/src/request_init/request_redirect.dart new file mode 100644 index 0000000..8b53814 --- /dev/null +++ b/lib/src/request_init/request_redirect.dart @@ -0,0 +1,19 @@ +/// How to handle a redirect response. +enum RequestRedirect { + /// Automatically follow redirects. + follow('follow'), + /// Abort with an error if a redirect occurs. + error('error'), + /// Caller intends to process the response in another context. + manual('manual'); + + const RequestRedirect(this.value); + + factory RequestRedirect.from(String value) => + values.firstWhere((element) => element.value == value); + + final String value; + + @override + String toString() => value; +} diff --git a/lib/src/request_init/request_referrer_policy.dart b/lib/src/request_init/request_referrer_policy.dart new file mode 100644 index 0000000..8975a4f --- /dev/null +++ b/lib/src/request_init/request_referrer_policy.dart @@ -0,0 +1,21 @@ +/// Specifies the referrer policy to use for the request. +enum RequestReferrerPolicy { + strictOriginWhenCrossOrigin('strict-origin-when-cross-origin'), + noReferrer('no-referrer'), + noReferrerWhenDowngrade('no-referrer-when-downgrade'), + sameOrigin('same-origin'), + origin('origin'), + strictOrigin('strict-origin'), + originWhenCrossOrigin('origin-when-cross-origin'), + unsafeUrl('unsafe-url'); + + const RequestReferrerPolicy(this.value); + + factory RequestReferrerPolicy.from(String value) => + values.firstWhere((element) => element.value == value); + + final String value; + + @override + String toString() => value; +} diff --git a/lib/src/response.dart b/lib/src/response.dart new file mode 100644 index 0000000..eda34db --- /dev/null +++ b/lib/src/response.dart @@ -0,0 +1,2 @@ +export 'response/response.dart'; +export 'response/response_options.dart'; diff --git a/lib/src/response/response.dart b/lib/src/response/response.dart new file mode 100644 index 0000000..cb273fa --- /dev/null +++ b/lib/src/response/response.dart @@ -0,0 +1,111 @@ +import 'dart:html' show Blob, FormData; +import 'dart:typed_data'; + +import '../_js.dart'; +import '../headers.dart'; +import '../readable_stream.dart'; +import 'response_options.dart'; +import 'response_type.dart'; + + +@JS() +@staticInterop +class Response { + external factory Response([dynamic body, ResponseOptions? options]); + external static Response error(); + external static Response redirect(String url, [int status = 302]); +} + +extension ResponseExtension on Response { + /// A [ReadableStream] of the body contents. + external final ReadableStream? body; + + /// Stores a boolean value that declares whether the body has been used in a + /// response yet. + external final bool bodyUsed; + + /// The Headers object associated with the response. + external final Headers headers; + + /// A boolean indicating whether the response was successful (status + /// in the range `200` –`299`) or not. + external final bool ok; + + /// Indicates whether or not the response is the result of a redirect + /// (that is, its URL list has more than one entry). + external final bool redirected; + + /// The status code of the response. (This will be `200` for a success). + external final int status; + + /// The status message corresponding to the status code. (e.g., `OK` for `200`). + external final String statusText; + + /// A [Promise] resolving to a [Headers] object, associated with + /// the response with [headers] for values of the HTTP Trailer header. + external final Promise? trailers; + + /// The type of the response (e.g., basic, cors). + external final String type; + + /// The URL of the response. + external final String url; + + /// Returns a promise that resolves with an [ByteBuffer] representation of + /// the response body. + @JS('arrayBuffer') + external Promise _arrayBuffer(); + + /// Returns a promise that resolves with a [Blob] representation of + /// the response body. + @JS('blob') + external Promise _blob(); + + /// Creates a clone of a [Response] object. + external Response clone(); + + /// Returns a promise that resolves with a [FormData] representation of + /// the response body. + @JS('formData') + external Promise _formData(); + + /// Returns a promise that resolves with the result of parsing the response + /// body text as JSON. + @JS('json') + external Promise _json(); + + /// Returns a promise that resolves with a text representation of + /// the response body. + @JS('text') + external Promise _text(); + + /// [trailers] converted to Dart's [Future]. + Future? responseTrailers() => + // ignore: unnecessary_this + this.trailers == null ? null : promiseToFuture(this.trailers!); + + /// The type of the response (e.g., basic, cors). + // ignore: unnecessary_this + ResponseType get responseType => ResponseType.from(this.type); + + /// Returns a [Future] that resolves with an [ByteBuffer] representation of + /// the response body. + Future arrayBuffer() => promiseToFuture(_arrayBuffer()); + + /// Returns a [Future] that resolves with a [Blob] representation of + /// the response body. + Future blob() => promiseToFuture(_blob()); + + /// Returns a [Future] that resolves with a [FormData] representation of + /// the response body. + Future formData() => promiseToFuture(_formData()); + + /// Returns a [Future] that resolves with the result of parsing the response + /// body text as JSON. + Future json() => + promiseToFuture(_json()).then(dartify); + + /// Returns a promise that resolves with a text representation of + /// the response body. + Future text() => promiseToFuture(_text()); +} diff --git a/lib/src/response/response_options.dart b/lib/src/response/response_options.dart new file mode 100644 index 0000000..882aee8 --- /dev/null +++ b/lib/src/response/response_options.dart @@ -0,0 +1,26 @@ +import '../_js.dart'; +import '../headers.dart'; + + +/// An object containing any custom settings that you want to apply to the +/// request. +@JS() +@anonymous +class ResponseOptions { + external factory ResponseOptions({ + int? status, + String? statusText, + Headers? headers, + }); + + /// The status code for the response, e.g., `200`. + external int? status; + + /// The status message associated with the status code, e.g., `OK`. + external String? statusText; + + /// Any headers you want to add to your response, contained within a [Headers] + /// object or object literal of [String] key/value pairs (see HTTP headers + /// for a reference). + external Headers? headers; +} diff --git a/lib/src/response/response_type.dart b/lib/src/response/response_type.dart new file mode 100644 index 0000000..9445e91 --- /dev/null +++ b/lib/src/response/response_type.dart @@ -0,0 +1,32 @@ +import 'response.dart'; + + +/// The type of the response. +enum ResponseType { + /// Normal, same origin response, with all headers exposed except "Set-Cookie". + basic('basic'), + /// Response was received from a valid cross-origin request. + /// Certain headers and the body may be accessed. + cors('cors'), + /// Network error. No useful information describing the error is available. + /// The [Response]'s status is 0, headers are empty and immutable. + /// This is the type for a [Response] obtained from [Response.error]. + error('error'), + /// Response for "no-cors" request to cross-origin resource. + /// Severely restricted. + opaque('opaque'), + /// The fetch request was made with `redirect: "manual"`. + /// The [Response]'s status is `0`, headers are empty, body is `null` and + /// trailer is empty. + opaqueRedirect('opaqueredirect'); + + const ResponseType(this.value); + + factory ResponseType.from(String value) => + values.firstWhere((element) => element.value == value); + + final String value; + + @override + String toString() => value; +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..d337d5b --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,21 @@ +name: fetch_api +description: JavaScript bindings for Fetch API, allowing a flexible HTTP requests. +version: 1.0.0-dev.1 +homepage: https://github.com/Zekfad/fetch_api +repository: https://github.com/Zekfad/fetch_api +issue_tracker: https://github.com/Zekfad/fetch_api/issues +# documentation: https://pub.dev/documentation/fetch_api/latest/ + +platforms: + web: + +environment: + sdk: '>=2.18.0 <4.0.0' + +dependencies: + js: ^0.6.6 + +dev_dependencies: + build_runner: ^2.3.3 + build_web_compilers: ^3.2.7 + zekfad_lints: ^1.2.0