Skip to content

Commit

Permalink
stash sexy things
Browse files Browse the repository at this point in the history
  • Loading branch information
runspired committed Jun 14, 2024
1 parent 296b190 commit 600729f
Show file tree
Hide file tree
Showing 16 changed files with 177 additions and 134 deletions.
102 changes: 2 additions & 100 deletions packages/experiments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,9 @@

<h3 align="center">Experiments that may or may not make it into the project core</h3>
<p align="center">Ideal for Having Fun</p>
<p align="center">SharedWorker + IndexedDB robust request deduplication and replay</p>

> ⚠️ ***Experimental*** ⚠️
- :electron: Dedupe requests across multiple tabs and windows
- ♻️ Replay requests reliably in any order and still get the latest state of all associated resources
- 📶 Load new tabs or windows without ever hitting network
- 💪 Control the Cache lifetime with confidence


## Installation

Install using your javascript package manager of choice. For instance with [pnpm](https://pnpm.io/)
Expand All @@ -33,98 +26,7 @@ Install using your javascript package manager of choice. For instance with [pnpm
pnpm add @warp-drive/experiments
```

## 🚀 Setup

Using the Data Worker requires a small bit of configuration

1. [Configuring the Worker]()
2. [Configuring the Worker Build]()
3. [Configuring your App]()

### 1. Configuring the Worker

In `<project>/workers/ember-data-cache-worker.js`

```ts
import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';
import { CachePolicy } from '@ember-data/request-utils';
import { DataWorker, CacheHandler } from '@warp-drive/experiments';
import DataStore from '@ember-data/store';

const CONFIG = {
apiCacheHardExpires: 120_000, // 2 minutes
apiCacheSoftExpires: 30_000, // 30 seconds
};

class Store extends DataStore {
constructor(args) {
super(args);

const manager = (this.requestManager = new RequestManager());
manager.use([Fetch]);

// this CacheHandler differs from the Store's in that it does not
// instantiate records for the response. It insteads takes the
// ResponseDocument from the cache and caches the request, document
// and resource data involved into indexeddb before returning the
// original raw response
manager.useCache(CacheHandler);

// our indexeddb cache will respect lifetimes, so registering
// a lifetimes service (even if not this one) is important!
this.lifetimes = new CachePolicy(CONFIG);
}
## Current Experiments

// we still use an in-mem cache in the worker in order to ensure
// the ability to use an indexeddb cache is opaque to your format.
// we cache by resource and document, its up to the cache to ensure
// it can give us this information when desired.
//
// however!
// we do not need to implement the instantiateRecord/teardownRecord
// hooks for in the worker.
createCache(capabilities: CacheCapabilitiesManager): Cache {
return new JSONAPICache(capabilities);
}
}
- [PersistedCache](./src/persisted-cache/README.md)

export default DataWorker.create(Store);
```

### 2. Configuring the Worker Build

Coming Soon
### 3. Configuring Your App

In `app/services/store.ts`

```ts
import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';
import { CachePolicy } from '@ember-data/request-utils';
import DataStore, { CacheHandler } from '@ember-data/store';
import { WorkerFetch } from 'warp-drive/experiments';

const CONFIG = {
apiCacheHardExpires: 120_000, // 2 minutes
apiCacheSoftExpires: 30_000, // 30 seconds
};

export default class Store extends DataStore {
constructor(args) {
super(args);

const manager = (this.requestManager = new RequestManager());
const workerUrl = new URL('./ember-data-cache-worker.js', import.meta.url)
const workerFetch = new WorkerFetch(this, workerUrl);

manager.use([workerFetch, Fetch]);
manager.useCache(CacheHandler);

// our indexeddb cache will respect lifetimes, so registering
// a lifetimes service (even if not this one) is important!
this.lifetimes = new CachePolicy(CONFIG);
}
}
```
5 changes: 5 additions & 0 deletions packages/experiments/addon-main.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const { addonShim } = require('@warp-drive/build-config/addon-shim.cjs');

module.exports = addonShim(__dirname);
11 changes: 11 additions & 0 deletions packages/experiments/babel.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { macros } from '@warp-drive/build-config/babel-macros';

export default {
plugins: [
...macros(),
[
'@babel/plugin-transform-typescript',
{ allExtensions: true, onlyRemoveTypeImports: true, allowDeclareFields: true },
],
],
};
22 changes: 22 additions & 0 deletions packages/experiments/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// @ts-check
import { globalIgnores } from '@warp-drive/internal-config/eslint/ignore.js';
import * as node from '@warp-drive/internal-config/eslint/node.js';
import * as typescript from '@warp-drive/internal-config/eslint/typescript.js';

/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
// all ================
globalIgnores(),

// browser (js/ts) ================
typescript.browser({
srcDirs: ['src'],
allowedImports: [],
}),

// node (module) ================
node.esm(),

// node (script) ================
node.cjs(),
];
26 changes: 17 additions & 9 deletions packages/experiments/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@warp-drive/experiments",
"description": "Experimental features for EmberData/WarpDrive",
"version": "5.4.0-alpha.10",
"version": "0.0.1-alpha.83",
"private": true,
"license": "MIT",
"author": "Chris Thoburn <[email protected]>",
Expand All @@ -18,14 +18,22 @@
"volta": {
"extends": "../../package.json"
},
"files": [],
"files": [
"dist",
"unstable-preview-types",
"CHANGELOG.md",
"README.md",
"LICENSE.md",
"NCC-1701-a-blue.svg",
"NCC-1701-a.svg"
],
"scripts": {},
"devDependencies": {
"@warp-drive/core-types": "workspace:0.0.0-alpha.56",
"@warp-drive/build-config": "workspace:0.0.0-alpha.7",
"@ember-data/request": "workspace:5.4.0-alpha.70",
"@ember-data/request-utils": "workspace:5.4.0-alpha.70",
"@ember-data/store": "workspace:5.4.0-alpha.70",
"@ember-data/tracking": "workspace:5.4.0-alpha.70"
"@warp-drive/core-types": "workspace:0.0.0-alpha.69",
"@warp-drive/build-config": "workspace:0.0.0-alpha.20",
"@ember-data/request": "workspace:5.4.0-alpha.83",
"@ember-data/request-utils": "workspace:5.4.0-alpha.83",
"@ember-data/store": "workspace:5.4.0-alpha.83",
"@ember-data/tracking": "workspace:5.4.0-alpha.83"
}
}
}
3 changes: 0 additions & 3 deletions packages/experiments/src/index.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/experiments/src/persisted-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PersistedCache } from './persisted-cache/index.ts';
79 changes: 79 additions & 0 deletions packages/experiments/src/persisted-cache/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<p align="center">
<img
class="project-logo"
src="../../NCC-1701-a-blue.svg#gh-light-mode-only"
alt="WarpDrive"
width="120px"
title="WarpDrive" />
<img
class="project-logo"
src="../../NCC-1701-a.svg#gh-dark-mode-only"
alt="WarpDrive"
width="120px"
title="WarpDrive" />
</p>

<h3 align="center">PersistedCache</h3>


- ⚡️ Load new tabs or windows without ever hitting network
- ♻️ Replay requests reliably in any order and still get the latest state of all associated resources

## Install

```cli
pnpm add @warp-drive/experiments
```

Or use favorite your javascript package manager.

## Configure



## How it Works

### Insertion

Only "clean" (remote) state is persisted. Dirty state is not currently persisted, and thus
a "refresh" of the page or opening a tab will result in new data.

The PersistedCache wraps a Cache implementation.

Whenever a request result is `put` into the cache, the result is used
to construct a new cache entry for indexeddb for the document and associated resources.

Whenever a save request commits, similarly the new state of any associated resources
is persisted to indexeddb.

When a `store.push` occurs (resulting in a `put` without an associated document), only
the associated resource state is updated in indexeddb.

### Sync

Whenever IndexedDB is updated, any resources currently in the tab's in-memory cache
will update.

### Retrieval

Requests saved to IndexedDB are replayed by using the `PersistedFetch` handler.
This handler will check whether the request exists in the persisted cache
and resolve it using the registered CachePolicy to determine staleness.

CachePolicies which invalidate requests based on in-memory lists may fail
to invalidate a persisted request since it was not known to the policy at
the point of invalidation. This can be handled (for now) by integrating with IndexedDB,
though we expect this handling to improve in the future once PersistedCache is
combined with DataWorker, as the CachePolicy would execute from within the worker.

CachePolicies may also need to take into account that they might be asked about the
expiration of the same request twice: first by the primary in-memory cache handler,
and then again by the persisted-cache handler should the in-memory cache not handle the
request.

Prior to the second inquiry, the document will be loaded into the in-memory cache so
that the CachePolicy can be applied to it.

### Cache Header

Responses served from indexeddb will have the header `X-WarpDrive-Cache: IndexedDB`
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import type { CacheHandler, Handler, NextFn, RequestContext, StructuredDocument } from "@ember-data/request";
import { StableExistingRecordIdentifier } from "@warp-drive/core-types/identifier";
import { ResourceDocument } from "@warp-drive/core-types/spec/document";
import type { CacheHandler, Handler, NextFn, RequestContext, StructuredDocument } from '@ember-data/request';
import { StableExistingRecordIdentifier } from '@warp-drive/core-types/identifier';
import { ResourceDocument } from '@warp-drive/core-types/spec/document';

const EmberDataCacheVersion = 1;
const WarpDriveCacheVersion = 1;

class PersistedCacheFetch implements Handler {
request<T>(context: RequestContext, next: NextFn<T>) {
return next(context.request);
}
}

/**
* A CacheHandler that wraps another CacheHandler to enable persisted caching
* of requests and responses.
*/
export class PersistedCacheHandler implements CacheHandler {
declare _fetch: PersistedCacheFetch;
declare _handler: CacheHandler;
declare _db: IDBDatabase | null;
declare _setup: Promise<void>;

async _setupCache(): Promise<void> {
const request = indexedDB.open('EmberDataCache', EmberDataCacheVersion);
const request = indexedDB.open('WarpDriveCache', WarpDriveCacheVersion);

await new Promise((resolve, reject) => {
request.onerror = reject;
Expand All @@ -38,7 +42,9 @@ export class PersistedCacheHandler implements CacheHandler {
const result: IDBDatabase = (event.target as unknown as { result: IDBDatabase }).result;

if (!result) {
throw new Error('Unable to upgrade IndexedDB database for PersistedCache: no IDBDatabase present on \`event.target.result\`');
throw new Error(
'Unable to upgrade IndexedDB database for PersistedCache: no IDBDatabase present on `event.target.result`'
);
}
// its not clear from the docs if (1) this method can be a promise or (2) how things like oncomplete
// for createObjectStore are handled.
Expand All @@ -58,7 +64,8 @@ export class PersistedCacheHandler implements CacheHandler {
}

request<T>(context: RequestContext, next: NextFn<T>) {
const nextFn = ((req: RequestContext['request']) => this._fetch.request(Object.assign({}, context, { request: req }), next)) as NextFn<T>;
const nextFn = ((req: RequestContext['request']) =>
this._fetch.request(Object.assign({}, context, { request: req }), next)) as NextFn<T>;

if (!this._db) {
return this._handler.request(context, nextFn);
Expand Down Expand Up @@ -100,7 +107,6 @@ export class PersistedCacheHandler implements CacheHandler {
}
}


async function upgradeCache(db: IDBDatabase, oldVersion: number): Promise<void> {
const promises: Promise<void>[] = [];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { StableRecordIdentifier } from "@warp-drive/core-types";
import type { Cache, ChangedAttributesHash, RelationshipDiff } from "@warp-drive/core-types/cache";
import type { ResourceBlob } from "@warp-drive/core-types/cache/aliases";
import type { Change } from "@warp-drive/core-types/cache/change";
import type { Mutation } from "@warp-drive/core-types/cache/mutations";
import type { Operation } from "@warp-drive/core-types/cache/operations";
import type { StableDocumentIdentifier, StableExistingRecordIdentifier } from "@warp-drive/core-types/identifier";
import type { TypeFromInstanceOrString } from "@warp-drive/core-types/record";
import type { ResourceDocument, SingleResourceDataDocument } from "@warp-drive/core-types/spec/document";
import type { StableRecordIdentifier } from '@warp-drive/core-types';
import type { Cache, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache';
import type { ResourceBlob } from '@warp-drive/core-types/cache/aliases';
import type { Change } from '@warp-drive/core-types/cache/change';
import type { Mutation } from '@warp-drive/core-types/cache/mutations';
import type { Operation } from '@warp-drive/core-types/cache/operations';
import type { StableDocumentIdentifier, StableExistingRecordIdentifier } from '@warp-drive/core-types/identifier';
import type { TypeFromInstanceOrString } from '@warp-drive/core-types/record';
import type { ResourceDocument, SingleResourceDataDocument } from '@warp-drive/core-types/spec/document';
import type { RequestContext, StructuredDataDocument, StructuredDocument } from '@warp-drive/core-types/request';
import { ApiError } from "@warp-drive/core-types/spec/error";
import { Value } from "@warp-drive/core-types/json/raw";
import { CollectionRelationship, ResourceRelationship } from "@warp-drive/core-types/cache/relationship";
import { ApiError } from '@warp-drive/core-types/spec/error';
import { Value } from '@warp-drive/core-types/json/raw';
import { CollectionRelationship, ResourceRelationship } from '@warp-drive/core-types/cache/relationship';
/**
* The PersistedCache wraps a Cache to enhance it with
* IndexedDB Persistence.
Expand Down Expand Up @@ -482,7 +482,6 @@ export class PersistedCache implements Cache {
return this._cache.rollbackRelationships(identifier);
}


// Relationships
// =============

Expand Down
2 changes: 1 addition & 1 deletion packages/experiments/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@
},
{
"path": "../build-config"
},
}
]
}
Loading

0 comments on commit 600729f

Please sign in to comment.