5.3.3 • Published 2 days ago

@ember-data/request v5.3.3

Weekly downloads
-
License
MIT
Repository
github
Last release
2 days ago

This package provides *Ember*Data's RequestManager, a framework agnostic library that can be integrated with any Javascript application to make fetch happen.

Installation

Install using your javascript package manager of choice. For instance with pnpm

pnpm add @ember-data/request

🚀 Basic Usage

A RequestManager provides a request/response flow in which configured handlers are successively given the opportunity to handle, modify, or pass-along a request.

The RequestManager on its own does not know how to fulfill requests. For this we must register at least one handler. A basic Fetch handler is provided that will take the request options provided and execute fetch.

import { RequestManager } from '@ember-data/request';
import { Fetch } from '@ember-data/request/fetch';
import { apiUrl } from './config';

// ... create manager and add our Fetch handler
const manager = new RequestManager();
manager.use([Fetch]);

// ... execute a request
const response = await manager.request({
  url: `${apiUrl}/users`
});

🪜 Architecture

A RequestManager receives a request and manages fulfillment via configured handlers. It may be used standalone from the rest of *Ember*Data and is not specific to any library or framework.

flowchart LR
    A[fa:fa-terminal App] <--> B{{fa:fa-sitemap RequestManager}}
    B <--> C[(fa:fa-database Source)]

Each handler may choose to fulfill the request using some source of data or to pass the request along to other handlers.

flowchart LR
    A[fa:fa-terminal App] <--> B{{fa:fa-sitemap RequestManager}}
    B <--> C(handler)
    C <--> E(handler)
    E <--> F(handler)
    C <--> D[(fa:fa-database Source)]
    E <--> G[(fa:fa-database Source)]
    F <--> H[(fa:fa-database Source)]

The same or a separate instance of a RequestManager may also be used to fulfill requests issued by *Ember*Data{Store}

flowchart LR
    A[fa:fa-terminal App] <--> D{fa:fa-code-fork Store}
    B{{fa:fa-sitemap RequestManager}} <--> C[(fa:fa-database Source)]
    D <--> E[(fa:fa-archive Cache)]
    D <--> B
    click D href "https://github.com/emberjs/data/tree/main/packages/store" "Go to @ember-data/store" _blank
    click E href "https://github.com/emberjs/data/tree/main/packages/json-api" "Go to @ember-data/json-api" _blank
    style D color:#58a6ff;
    style E color:#58a6ff;

When the same instance is used by both this allows for simple coordination throughout the application. Requests issued by the Store will use the in-memory cache and return hydrated responses, requests issued directly to the RequestManager will skip the in-memory cache and return raw responses.

flowchart LR
    A[fa:fa-terminal App] <--> B{{fa:fa-sitemap RequestManager}}
    B <--> C[(fa:fa-database Source)]
    A <--> D{fa:fa-code-fork Store}
    D <--> E[(fa:fa-archive Cache)]
    D <--> B
    click D href "https://github.com/emberjs/data/tree/main/packages/store" "Go to @ember-data/store" _blank
    click E href "https://github.com/emberjs/data/tree/main/packages/json-api" "Go to @ember-data/json-api" _blank
    style D color:#58a6ff;
    style E color:#58a6ff;

Usage

RequestManager has a single asyncronous method as it's API: request

class RequestManager {
  async request<T>(req: RequestInfo): Future<T>;
}

manager.request accepts a RequestInfo, an object containing the information necessary for the request to be handled successfully.

RequestInfo extends the options provided to fetch, and can accept a Request. All properties accepted by Request options and fetch options are valid on RequestInfo.

interface RequestInfo extends FetchOptions {
  url: string;
  /**
   * data that a handler should convert into 
   * the query (GET) or body (POST)
   */
  data?: Record<string, unknown>;
  /**
   * options specifically intended for handlers
   * to utilize to process the request
   */
  options?: Record<string, unknown>;
}

note: providing a signal is unnecessary as an AbortController is automatically provided if none is present.

manager.request returns a Future, which allows access to limited information about the request while it is still pending and fulfills with the final state when the request completes and the response has been read.

A Future is cancellable via abort.

Handlers may optionally expose a ReadableStream to the Future for streaming data; however, when doing so the handler should not resolve until it has fully read the response stream itself.

interface Future<T> extends Promise<StructuredDocument<T>> {
  abort(): void;

  async getStream(): ReadableStream | null;
}

A Future resolves or rejects with a StructuredDocument.

interface StructuredDocument<T> {
  request: RequestInfo;
  response: ResponseInfo | null;
  content?: T;
  error?: Error;
}

The RequestInfo specified by document.request is the same as originally provided to manager.request. If any handler fulfilled this request using different request info it is not represented here. This contract helps to ensure that retry and caching are possible since the original arguments are correctly preserved. This also allows handlers to "fork" the request or fulfill from multiple sources without the details of fulfillment muddying the original request.

The ResponseInfo is a serializable fulfilled subset of a Response if set via setResponse. If no response was ever set this will be null.

/**
 * All readonly properties available on a Response
 * 
 */
interface ResponseInfo {
  headers?: Record<string, string>;
  ok?: boolean;
  redirected?: boolean;
  status?: HTTPStatusCode;
  statusText?: string;
  type?: 'basic' | 'cors';
  url?: string;
}

Requests are fulfilled by handlers. A handler receives the request context as well as a next function with which to pass along a request to the next handler if it so chooses.

A handler may be any object with a request method. This allows both stateful and non-stateful handlers to be utilized.

If a handler calls next, it receives a Future which resolves to a StructuredDocument that it can then compose how it sees fit with its own response.

type NextFn<P> = (req: RequestInfo) => Future<P>;

interface Handler {
  async request<T>(context: RequestContext, next: NextFn<P>): T;
}

RequestContext contains a readonly version of the RequestInfo as well as a few methods for building up the StructuredDocument and Future that will be part of the response.

interface RequestContext<T> {
  readonly request: RequestInfo;

  setStream(stream: ReadableStream | Promise<ReadableStream>): void;
  setResponse(response: Response | ResponseInfo): void;
}

A basic fetch handler with support for streaming content updates while the download is still underway might look like the following, where we use response.clone() to tee the ReadableStream into two streams.

A more efficient handler might read from the response stream, building up the response content before passing along the chunk downstream.

const FetchHandler = {
  async request(context) {
    const response = await fetch(context.request);
    context.setResponse(reponse);
    context.setStream(response.clone().body);

    return response.json();
  }
}

Request handlers are registered by configuring the manager via use

manager.use([Handler1, Handler2])

Handlers will be invoked in the order they are registered ("fifo", first-in first-out), and may only be registered up until the first request is made. It is recommended but not required to register all handlers at one time in order to ensure explicitly visible handler ordering.

RequestManager.request and next differ from fetch in one crucial detail in that the outer Promise resolves only once the response stream has been processed.

For context, it helps to understand a few of the use-cases that RequestManager is intended to allow.

  • to manage and return streaming content (such as video files)
  • to fulfill a request from multiple sources or by splitting one request into multiple requests
    • for instance one API call for a user and another for the user's friends
    • or e.g. fulfilling part of the request from one source (one API, in-memory, localStorage, IndexedDB etc.) and the rest from another source (a different API, a WebWorker, etc.)
  • to coalesce multiple requests
  • to decorate a request with additional info
    • e.g. an Auth handler that ensures the correct tokens or headers or cookies are attached.

await fetch(<req>) resolves at the moment headers are received. This allows for the body of the request to be processed as a stream by application code while chunks are still being received by the browser.

When an app chooses to await response.json() what occurs is the browser reads the stream to completion and then returns the result. Additionally, this stream may only be read once.

The RequestManager preserves this ability to subscribe to and utilize the stream by either the application or the handler – thereby delivering the full power and flexibility of native APIs – without restricting developers in ways that lead to complicated workarounds.

Each handler may call setStream only once, but may do so at any time until the promise that the handler returns has resolved. The associated promise returned by calling future.getStream will resolve with the stream set by setStream if that method is called, or null if that method has not been called by the time that the handler's request method has resolved.

Handlers that do not create a stream of their own, but which call next, should defensively pipe the stream forward. While this is not required (see automatic currying below) it is better to do so in most cases as otherwise the stream may not become available to downstream handlers or the application until the upstream handler has fully read it.

context.setStream(future.getStream());

Handlers that either call next multiple times or otherwise have reason to create multiple fetch requests should either choose to return no stream, meaningfully combine the streams, or select a single prioritized stream.

Of course, any handler may choose to read and handle the stream, and return either no stream or a different stream in the process.

In order to simplify the common case for handlers which decorate a request, if next is called only a single time and setResponse was never called by the handler, the response set by the next handler in the chain will be applied to that handler's outcome. For instance, this makes the following pattern possible return (await next(<req>)).content;.

Similarly, if next is called only a single time and neither setStream nor getStream was called, we automatically curry the stream from the future returned by next onto the future returned by the handler.

Finally, if the return value of a handler is a Future, we curry content and errors as well, thus enabling the simplest form return next(<req>).

In the case of the Future being returned, Stream proxying is automatic and immediate and does not wait for the Future to resolve.

Using as a Service

Most applications will desire to have a single RequestManager instance, which can be achieved using module-state patterns for singletons, or for Ember applications by exporting the manager as a service.

services/request.ts

import { RequestManager } from '@ember-data/request';
import { Fetch } from '@ember-data/request/fetch';
import Auth from 'ember-simple-auth/ember-data-handler';

export default class extends RequestManager {
  constructor(createArgs) {
    super(createArgs);
    this.use([Auth, Fetch]);
  }
}

Using with @ember-data/store

To have a request service unique to a Store:

import Store from '@ember-data/store';
import { RequestManager } from '@ember-data/request';
import { Fetch } from '@ember-data/request/fetch';

class extends Store {
  requestManager = new RequestManager();

  constructor(args) {
    super(args);
    this.requestManager.use([Fetch]);
  }
}

Using with ember-data

If using the package ember-data, the following configuration will automatically be done in order to preserve the legacy Adapter and Serializer behavior. Additional handlers or a service injection like the above would need to be done by the consuming application in order to make broader use of RequestManager.

import Store from '@ember-data/store';
import { RequestManager } from '@ember-data/request';
import { LegacyHandler } from '@ember-data/legacy-network-handler';

export default class extends Store {
  requestManager = new RequestManager();

  constructor(args) {
    super(args);
    this.requestManager.use([LegacyHandler]);
  }
}

Because the application's store service (if present) will override the store supplied by ember-data, all that is required to define your own ordering and handlers is to supply a store service extending from @ember-data/store and configure as shown above.

For usage of the store's requestManager via store.request(<req>) see the Store documentation.

5.4.0-alpha.60

2 days ago

5.4.0-alpha.59

6 days ago

5.4.0-alpha.58

9 days ago

5.4.0-alpha.57

13 days ago

5.4.0-alpha.56

15 days ago

5.4.0-alpha.55

16 days ago

5.4.0-alpha.54

20 days ago

5.4.0-alpha.53

23 days ago

5.4.0-alpha.52

27 days ago

5.4.0-alpha.51

27 days ago

5.4.0-alpha.50

28 days ago

5.4.0-alpha.49

1 month ago

5.4.0-alpha.47

1 month ago

5.4.0-alpha.46

1 month ago

5.4.0-alpha.45

1 month ago

4.12.7

1 month ago

4.12.6

1 month ago

5.4.0-alpha.44

1 month ago

5.4.0-alpha.43

1 month ago

5.4.0-alpha.41

1 month ago

5.4.0-alpha.35

2 months ago

5.4.0-alpha.34

2 months ago

5.4.0-alpha.33

2 months ago

5.4.0-alpha.32

2 months ago

5.4.0-alpha.31

2 months ago

5.4.0-alpha.30

2 months ago

5.4.0-alpha.29

2 months ago

5.3.3

2 months ago

5.4.0-alpha.28

2 months ago

5.4.0-alpha.27

2 months ago

5.4.0-beta.4

2 months ago

5.3.2

2 months ago

5.4.0-alpha.26

2 months ago

5.4.0-beta.3

2 months ago

5.3.1

2 months ago

5.4.0-alpha.23

2 months ago

5.4.0-beta.2

2 months ago

5.4.0-alpha.21

2 months ago

5.4.0-alpha.20

2 months ago

5.4.0-alpha.19

2 months ago

5.4.0-alpha.22

2 months ago

5.4.0-alpha.17

3 months ago

4.12.5

5 months ago

5.3.0

8 months ago

5.3.0-beta.0

9 months ago

5.3.0-beta.2

8 months ago

5.3.0-beta.1

8 months ago

5.3.0-beta.4

8 months ago

5.3.0-beta.3

8 months ago

5.3.0-beta.5

8 months ago

5.3.0-alpha.0

10 months ago

5.3.0-alpha.6

10 months ago

5.3.0-alpha.5

10 months ago

5.3.0-alpha.8

10 months ago

5.3.0-alpha.7

10 months ago

5.3.0-alpha.2

10 months ago

5.3.0-alpha.1

10 months ago

5.3.0-alpha.4

10 months ago

5.3.0-alpha.3

10 months ago

5.3.0-alpha.9

9 months ago

5.0.1

10 months ago

5.5.0-alpha.9

7 months ago

5.5.0-alpha.3

7 months ago

5.5.0-alpha.4

7 months ago

5.5.0-alpha.0

8 months ago

5.5.0-alpha.1

8 months ago

5.5.0-alpha.2

7 months ago

5.2.0-beta.0

10 months ago

5.1.0-beta.1

10 months ago

5.5.0-alpha.11

7 months ago

5.5.0-alpha.10

7 months ago

5.1.2

9 months ago

4.12.3

10 months ago

5.1.1

10 months ago

4.12.4

7 months ago

5.1.0

10 months ago

5.4.0-alpha.0

9 months ago

5.4.0-alpha.1

9 months ago

5.4.0-alpha.2

8 months ago

5.4.0-alpha.3

8 months ago

5.4.0-alpha.4

8 months ago

5.4.0-alpha.5

8 months ago

5.4.0-alpha.6

8 months ago

5.4.0-alpha.7

8 months ago

5.4.0-alpha.8

8 months ago

5.4.0-alpha.9

8 months ago

5.4.0-alpha.10

8 months ago

5.3.0-alpha.11

9 months ago

5.3.0-alpha.10

9 months ago

5.3.0-alpha.13

9 months ago

5.3.0-alpha.12

9 months ago

5.3.0-alpha.15

9 months ago

5.3.0-alpha.14

9 months ago

4.12.1

10 months ago

4.12.2

10 months ago

5.2.0

9 months ago

5.4.0-alpha.16

8 months ago

5.4.0-alpha.15

8 months ago

5.4.0-alpha.14

8 months ago

5.4.0-alpha.13

8 months ago

5.4.0-alpha.12

8 months ago

5.4.0-alpha.11

8 months ago

5.4.0-beta.0

8 months ago

5.4.0-beta.1

7 months ago

5.2.0-alpha.5

10 months ago

5.2.0-alpha.4

10 months ago

5.2.0-alpha.3

11 months ago

5.2.0-alpha.2

11 months ago

5.2.0-alpha.1

11 months ago

5.1.0-beta.0

11 months ago

5.2.0-alpha.0

11 months ago

5.0.0

11 months ago

5.0.0-beta.2

11 months ago

5.1.0-alpha.15

11 months ago

5.1.0-alpha.14

11 months ago

5.1.0-alpha.13

11 months ago

5.1.0-alpha.12

11 months ago

5.0.0-beta.1

11 months ago

5.1.0-alpha.11

11 months ago

5.1.0-alpha.10

12 months ago

5.1.0-alpha.9

12 months ago

5.1.0-alpha.8

12 months ago

5.1.0-alpha.7

12 months ago

5.1.0-alpha.6

1 year ago

5.1.0-alpha.5

1 year ago

5.1.0-alpha.4

1 year ago

5.1.0-alpha.3

1 year ago

5.1.0-alpha.2

1 year ago

5.1.0-alpha.1

1 year ago

5.1.0-alpha.0

1 year ago

5.0.0-beta.0

1 year ago

4.12.0-beta.11

1 year ago

5.0.0-alpha.3

1 year ago

5.0.0-alpha.2

1 year ago

4.12.0-beta.10

1 year ago

5.0.0-alpha.1

1 year ago

5.0.0-alpha.0

1 year ago

4.12.0

1 year ago

4.12.0-beta.9

1 year ago

4.12.0-alpha.20

1 year ago

4.12.0-alpha.19

1 year ago

4.12.0-alpha.18

1 year ago

4.12.0-beta.8

1 year ago

4.12.0-alpha.17

1 year ago

4.12.0-beta.7

1 year ago

4.12.0-alpha.16

1 year ago

4.12.0-alpha.15

1 year ago

4.12.0-beta.6

1 year ago

4.12.0-beta.5

1 year ago

4.12.0-alpha.14

1 year ago

4.12.0-alpha.13

1 year ago

4.12.0-alpha.12

1 year ago

4.12.0-beta.4

1 year ago

4.12.0-alpha.11

1 year ago

4.12.0-alpha.10

1 year ago

4.12.0-alpha.9

1 year ago