Skip to content

💡 Looking for the Legacy Package Setup Guide?

Setup

All frameworks should follow this configuration before continuing on to their framework specific setup guide.

Configure the Build Plugin

WarpDrive uses a babel plugin to inject app-specific configuration allowing us to provide advanced dev-mode debugging features, deprecation management, and canary feature toggles.

For Ember.js, this plugin comes built-in to the toolchain and all you need to do is provide it the desired configuration in ember-cli-build. For all other projects, the configuration is done inside of the app's babel configuration file.

babel.config.mjs
ts
import { setConfig } from '@warp-drive/core/build-config';
import { buildMacros } from '@embroider/macros/babel';

const Macros = buildMacros({
  configure: (config) => {
    setConfig(config, {
      // this should be the most recent <major>.<minor> version for
      // which all deprecations have been fully resolved
      // and should be updated when that changes
      // for new apps it should be the version you installed
      // for universal apps this MUST be at least 5.6
      compatWith: '5.6'
    });
  },
});

export default {
  plugins: [
    // babel-plugin-debug-macros is temporarily needed
    // to convert deprecation/warn calls into console.warn
    [
      'babel-plugin-debug-macros',
      {
        flags: [],

        debugTools: {
          isDebug: true,
          source: '@ember/debug',
          assertPredicateIndex: 1,
        },
      },
      'ember-data-specific-macros-stripping-test',
    ],
    ...Macros.babelMacros,
  ],
};

Configure the Store

The Store is the central piece of the WarpDrive experience, linking together how we handle requests, the schemas for what our data looks like, how to cache it, and what sort of reactive objects to create for that data.

Here's an example final configuration. Below we'll show each bit in parts and discuss what each does.

💡 TIP

In frameworks that do DI via contexts, you will want to provide the store via context near the application root.

In frameworks like emberjs which use a service injection pattern you will want to place the store file in the appropriate location such as <app>/services/store.ts

In other frameworks you will want to create a singleton store in module state that you will import and use when needed.

services/store.ts
ts
import { CacheHandler, Fetch, RequestManager, Store } from '@warp-drive/core';
import {
  instantiateRecord,
  registerDerivations,
  SchemaService,
  teardownRecord
} from '@warp-drive/core/reactive';
import { DefaultCachePolicy } from '@warp-drive/core/store';
import type { CacheCapabilitiesManager, ResourceKey } from '@warp-drive/core/types';
import { JSONAPICache } from '@warp-drive/json-api';

export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  lifetimes = new DefaultCachePolicy({
    apiCacheHardExpires: 15 * 60 * 1000, // 15 minutes
    apiCacheSoftExpires: 1 * 30 * 1000, // 30 seconds
    constraints: {
	  headers: {
        'X-WarpDrive-Expires': true,
        'Cache-Control': true,
        'Expires': true,
	  }
    }
  });

  createSchemaService() {
    const schema = new SchemaService();
    registerDerivations(schema);
    return schema;
  }

  createCache(capabilities: CacheCapabilitiesManager) {
    return new JSONAPICache(capabilities);
  }

  instantiateRecord(key: ResourceKey, createArgs?: Record<string, unknown>) {
    return instantiateRecord(this, key, createArgs);
  }

  teardownRecord(record: unknown): void {
    return teardownRecord(record);
  }
}

Start With A Store

The store is the central piece of the WarpDrive experience. It functions as a coordinator, linking together requests for data with schemas, caching and reactivity.

While it's easy to use just WarpDrive's request management, most apps will find they require far more than basic fetch management. For this reason it's often best to start with a Store even when you aren't sure yet.

services/store.ts
ts
import { Store } from '@warp-drive/core';

export default class AppStore extends Store {}

Add Basic Request Management

RequestManager provides a chain-of-responsibility style pipeline for helping you handle centralized concerns around requesting and updating data from your backend.

💡 Guide

→ Learn more about Making Requests

services/store.ts
ts
import { Fetch, RequestManager, Store } from '@warp-drive/core'; 

export default class AppStore extends Store {
  requestManager = new RequestManager() 
    .use([Fetch]);
}

Add a Source for Schema for your Data

WarpDrive uses simple JSON schemas to define the shape and features of reactive objects. Schemas may seem simple, but they come packed with features that will help you build incredible applications.

💡 Guide

→ Learn more about Resource Schemas

services/store.ts
ts
import { Fetch, RequestManager, Store } from '@warp-drive/core';
import {  
  registerDerivations,
  SchemaService,
} from '@warp-drive/core/reactive';

export default class AppStore extends Store {
  requestManager = new RequestManager()
    .use([Fetch]);

  createSchemaService() { 
    const schema = new SchemaService();
    registerDerivations(schema);
    return schema;
  }

}

Add a Cache

Do you really need a cache? Are sunsets beautiful? Caching is what powers features like immutability, mutation management, and allows WarpDrive to understand your relational data.

Some caches are simple request/response maps. WarpDrive's is not. The Cache deeply understands the structure of your data, ensuring your data remains consistent both within and across requests.

Out of the box, WarpDrive provides a Cache that expects the {JSON:API} format. This format excels at simiplifying common complex problems around cache consistency and information density. Most APIs can be quickly adapted to work with it, but if a cache built to understand another format would do better it just needs to follow the same interface.

services/store.ts
ts
import { CacheHandler, Fetch, RequestManager, Store } from '@warp-drive/core'; 
import {
  registerDerivations,
  SchemaService,
} from '@warp-drive/core/reactive';
import type {  
  CacheCapabilitiesManager
} from '@warp-drive/core/types';
import { JSONAPICache } from '@warp-drive/json-api';

export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler); 

  createSchemaService() {
    const schema = new SchemaService();
    registerDerivations(schema);
    return schema;
  }

  createCache(capabilities: CacheCapabilitiesManager) { 
    return new JSONAPICache(capabilities);
  }
}

Setup Your Data to be Reactive

While it is possible to use WarpDrive to store and retrieve raw json, you'd be missing out on the best part. Reactive objects transform raw cached data into rich, reactive data. The resulting objects are immutable, always displaying the latest state in the cache while preventing accidental or unsafe mutation in your app.

services/store.ts
ts
import { CacheHandler, Fetch, RequestManager, Store } from '@warp-drive/core';
import type {
  CacheCapabilitiesManager,
  ResourceKey 
} from '@warp-drive/core/types';
import {
  instantiateRecord, 
  registerDerivations,
  SchemaService,
  teardownRecord 
} from '@warp-drive/core/reactive';
import { JSONAPICache } from '@warp-drive/json-api';

export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  createSchemaService() {
    const schema = new SchemaService();
    registerDerivations(schema);
    return schema;
  }

  createCache(capabilities: CacheCapabilitiesManager) {
    return new JSONAPICache(capabilities);
  }

  instantiateRecord(key: ResourceKey, createArgs?: Record<string, unknown>) { 
    return instantiateRecord(this, key, createArgs);
  }

  teardownRecord(record: unknown): void { 
    return teardownRecord(record);
  }
}

Decide How Long Requests are Valid for with a CachePolicy

And of course, what's a great cache without an eviction policy?

WarpDrive provides an interface for creating Cache Policies. Whenever a request is made, the policy is checked to determine if the current cached representation is still valid.

Policies also have the ability to subscribe to cache updates and issue invalidation notifications. The <Request /> component subscribes to these notifications and will trigger a reload if necessary if an invalidated request is in active use, letting you craft advanced policies that meet your product's needs.

WarpDrive provides a basic CachePolicy with a number of great defaults that is a great starting point for most applications. We configure this basic policy below.

The basic policy will invalidate requests based on caching and date headers available on request responses, falling back to a simple time based policy.

services/store.ts
ts
import { CacheHandler, Fetch, RequestManager, Store } from '@warp-drive/core';
import {
  instantiateRecord,
  registerDerivations,
  SchemaService,
  teardownRecord
} from '@warp-drive/core/reactive';
import { DefaultCachePolicy } from '@warp-drive/core/store'; 
import type { CacheCapabilitiesManager, ResourceKey } from '@warp-drive/core/types';
import { JSONAPICache } from '@warp-drive/json-api';


export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  lifetimes = new DefaultCachePolicy({ 
    apiCacheHardExpires: 15 * 60 * 1000, // 15 minutes
    apiCacheSoftExpires: 1 * 30 * 1000, // 30 seconds
    constraints: {
      headers: {
        'X-WarpDrive-Expires': true,
        'Cache-Control': true,
        'Expires': true,
      }
    }
  });

  createSchemaService() {
    const schema = new SchemaService();
    registerDerivations(schema);
    return schema;
  }

  createCache(capabilities: CacheCapabilitiesManager) {
    return new JSONAPICache(capabilities);
  }

  instantiateRecord(key: ResourceKey, createArgs?: Record<string, unknown>) {
    return instantiateRecord(this, key, createArgs);
  }

  teardownRecord(record: unknown): void {
    return teardownRecord(record);
  }
}

Configure Your Framework

The final setup step is to configure reactivity for your framework. See each framework's guide for this step.

Configure ESLint

🚧 Under Construction

Released under the MIT License.