Offline first react (#421)

* initial commit

* Added OfflineHTTPService implementation to file

* Put interfaces into their own files

* Create a state object to allow the render method to have a variable to print.

* Tested Gets, need to update comments

* Add JSDoc

* Created Readme.md
This commit is contained in:
Austin Breslin 2018-02-26 08:30:58 +00:00 committed by Vesa Juvonen
parent 158beeb9b4
commit c177a89297
29 changed files with 18370 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

32
samples/react-offline-first/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.4.1",
"libraryName": "react-offline-first",
"libraryId": "0c2c55d9-8ce6-4303-a642-ff8eefbc9d2c",
"environment": "spo"
}
}

View File

@ -0,0 +1,78 @@
# Offline First React Webpart built using LocalForage, Whatwg-Fetch, ES6-Promise
## Summary
Sample Webpart that demonstrates how to use offline storage in a way that is more
offline first.
This webpart would not be possible without the great tools provided, please
read the documentation for
* [LocalForage](https://github.com/localForage/localForage)
* [ES6-Promise](https://github.com/stefanpenner/es6-promise)
* [Whatwg-Fetch](https://github.com/whatwg/fetch)
![Sample of the offline first webpart](./assets/webpart-screenshot.jpg)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-GA-green.svg)
## Applies to
* [SharePoint Framework Developer Preview](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Solution
Solution |Author(s)
--------------------|---------
react-offline-first |[Austin Breslin](https://github.com/AustinBreslinDev)
## Version history
Version |Date | Comments
------- |---- | --------
0.0.1 |17/Feb/18 | Got the HTTP Requests to work.
0.0.2 |17/Feb/18 | Seperated interfaces into their own files.
0.0.3 |18/Feb/18 | Updated JSDocs, and and created readme.
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repo
- In the command line run:
- `npm install`
- `tsd install`
- `gulp serve`
- Open the *workbench* on your Office 365 Developer tenant
- Test out the web part
## Features
This webpart demonstrates using the OfflineFirstHTTPService how to do offline
first HTTP Requests.
HTTP Get requests are first retrieved from an offline storage system. Either
localStorage, IndexedDB, WebSQL or SessionStorage, but maybe not in that order.
Then the live request is using fetch, then stored locally, errors are placed into
a queue for when available.
HTTP Posts/Updates/Deletes are executed differently to GET requests, they always
make a HTTP request using fetch, but on failure are added to the queue.
The Queue system collects all failed HTTP Requests and when the user is online
will try to make the requests again, all requests are async so will not cause
any blocking for the user.
This Web Part illustrates the following concepts on top of the SharePoint Framework:
- Using offline first techniques, store locally, retrieve locally before making
HTTP requests.
- Async looping, can be found in the Queue System.
- Performance techniques for browsers, async looping does not block render,
using local storage first reduces the time to draw the inital page load.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-offline-first" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"react-offline-first-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/reactOfflineFirst/ReactOfflineFirstWebPart.js",
"manifest": "./src/webparts/reactOfflineFirst/ReactOfflineFirstWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ReactOfflineFirstWebPartStrings": "lib/webparts/reactOfflineFirst/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-offline-first",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-offline-first-client-side-solution",
"id": "0c2c55d9-8ce6-4303-a642-ff8eefbc9d2c",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/react-offline-first.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,37 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
"displayAsWarning": true,
"removeExistingRules": true,
"useDefaultConfigAsBase": false,
"lintConfig": {
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,7 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);

17465
samples/react-offline-first/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
{
"name": "react-offline-first",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "~1.4.1",
"@microsoft/sp-lodash-subset": "~1.4.1",
"@microsoft/sp-office-ui-fabric-core": "~1.4.1",
"@microsoft/sp-webpart-base": "~1.4.1",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"localforage": "^1.5.6",
"react": "15.6.2",
"react-dom": "15.6.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,26 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "660518b0-a60c-4371-8e67-d178c5ccffcb",
"alias": "ReactOfflineFirstWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "react-offline-first" },
"description": { "default": "react-offline-first description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "react-offline-first"
}
}]
}

View File

@ -0,0 +1,56 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import * as strings from 'ReactOfflineFirstWebPartStrings';
import ReactOfflineFirst from './components/ReactOfflineFirst';
import { IReactOfflineFirstProps } from './components/IReactOfflineFirstProps';
export interface IReactOfflineFirstWebPartProps {
description: string;
}
export default class ReactOfflineFirstWebPart extends BaseClientSideWebPart<IReactOfflineFirstWebPartProps> {
public render(): void {
const element: React.ReactElement<IReactOfflineFirstProps > = React.createElement(
ReactOfflineFirst,
{
description: this.properties.description
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,3 @@
export interface IReactOfflineFirstProps {
description: string;
}

View File

@ -0,0 +1,3 @@
export interface IReactOfflineFirstState {
listOfGitHubRepos: any[];
}

View File

@ -0,0 +1,74 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.reactOfflineFirst {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,116 @@
import * as React from 'react';
import styles from './ReactOfflineFirst.module.scss';
import { IReactOfflineFirstProps } from './IReactOfflineFirstProps';
import { escape } from '@microsoft/sp-lodash-subset';
import { IReactOfflineFirstState } from './IReactOfflineFirstState';
import { IOfflineStorageRequest } from '../services/IOfflineStorageRequest';
import { IOfflineStorageItem } from '../services/IOffllineStorageItem';
import { OfflineFirstHTTPService } from '../services/OfflineFirstHTTPService';
/**
* Uses the OfflineFistHTTPService as a demo of how to use it.
* @export
* @class ReactOfflineFirst
* @extends {React.Component<IReactOfflineFirstProps, IReactOfflineFirstState>}
*/
export default class ReactOfflineFirst extends React.Component<IReactOfflineFirstProps, IReactOfflineFirstState> {
private offlineHTTP: OfflineFirstHTTPService;
/**
* Creates an instance of ReactOfflineFirst.
* @param {IReactOfflineFirstProps} props
* @memberof ReactOfflineFirst
*/
public constructor(props: IReactOfflineFirstProps) {
super(props);
this.state = {
listOfGitHubRepos : []
} as IReactOfflineFirstState;
this.offlineHTTP = new OfflineFirstHTTPService();
}
/**
* Normal SPFX Webpart lifecycle.
* But uses localStorage.
* @memberof ReactOfflineFirst
*/
public componentDidMount(): void {
const demoUrl: string = "https://api.github.com/orgs/SharePoint/repos";
const demoRequest: RequestInfo = {
url: demoUrl
} as RequestInfo;
const demoOfflineRequest: IOfflineStorageRequest = {key: "demoGet", value:{ requestInfo: demoRequest, requestInit: null}} as IOfflineStorageRequest;
this.offlineHTTP.getFromLocal(demoOfflineRequest.key)
.then( (offlineItem: any) => {
this.setState({listOfGitHubRepos: offlineItem},
() => {
console.log('retrieved items from offline.');
//setstate callback.
this.componentDidMountWithLocal(demoOfflineRequest);
});
})
.catch((error: Error|any) => {
// Wasn't found in local storage, try live versuon.
this.componentDidMountWithLocal(demoOfflineRequest);
console.error(error);
});
}
/**
* Custom SPFX Webpart lifecycle using setState optional callback.
* Retrieves a live version. If the user is offline nothing will
* happen, but the user will still get a result if they got an
* item from localstorage.
* @private
* @param {any} demoOfflineRequest
* @memberof ReactOfflineFirst
*/
private componentDidMountWithLocal(demoOfflineRequest):void {
this.offlineHTTP.getFromServer(demoOfflineRequest)
.then( (onlineItem: any) => {
this.setState({listOfGitHubRepos: onlineItem},
() => {
console.log('retrieved items from online.');
//set state callback.
});
})
.catch((error: Error|any) => {
console.error(error);
});
}
/**
* Normal SPFX Webpart lifecycle.
* @returns {React.ReactElement<IReactOfflineFirstProps>}
* @memberof ReactOfflineFirst
*/
public render(): React.ReactElement<IReactOfflineFirstProps> {
return (
<div className={ styles.reactOfflineFirst }>
<div className={ styles.container }>
<div className={ styles.row }>
<div className={ styles.column }>
<span className={ styles.title }>Welcome to SharePoint!</span>
<p className={ styles.subTitle }>Customize SharePoint experiences using Web Parts.</p>
<p className={ styles.description }>{escape(this.props.description)}</p>
<a href="https://aka.ms/spfx" className={ styles.button }>
<span className={ styles.label }>Learn more</span>
</a>
{
this.state.listOfGitHubRepos &&
this.state.listOfGitHubRepos.map((repo) => {
return (
<div>
{repo.full_name}
</div>
);
})
}
</div>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IReactOfflineFirstWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'ReactOfflineFirstWebPartStrings' {
const strings: IReactOfflineFirstWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,5 @@
import { IOfflineStorageRequestValue } from './IOfflineStorageRequestValue';
export interface IOfflineStorageRequest {
key: string;
value: IOfflineStorageRequestValue;
}

View File

@ -0,0 +1,4 @@
export interface IOfflineStorageRequestValue {
requestInfo: RequestInfo;
requestInit: RequestInit;
}

View File

@ -0,0 +1,5 @@
import { IOfflineStorageRequestValue } from './IOfflineStorageRequestValue';
export interface IOfflineStorageItem {
offlineItem: any;
onlineItem: Promise<any>;
}

View File

@ -0,0 +1,291 @@
import * as localforage from 'localforage';
import { IOfflineStorageRequest } from './IOfflineStorageRequest';
import { IOfflineStorageItem } from './IOffllineStorageItem';
/*
Fetch and Promise.
Comes from https://www.npmjs.com/package/@microsoft/sp-polyfills
*/
/**
* Tries to wrap HTTP Requests so that they can be retrieved from
* offline storage first, and making the HTTP Request at the same time.
* Stores failed HTTP Requests in a Queue for when device is online again.
*
* NB: Not all browsers support Offline/Online, fallback is HTTP failures.
* @export
* @class OfflineFirstHTTPService
*/
export class OfflineFirstHTTPService {
private _IsOnline: boolean;
private _LiveLocalForage: LocalForage;
private _QueueLocalForage: LocalForage;
/**
* Creates an instance of GitHubService.
* Registers Offline, Online listeners to get the
* HTTP post/update/delete request queue
* @memberof GitHubService
*/
public constructor() {
// only initially works on older Firefox
this._IsOnline = window.navigator.onLine || !(window['mozInnerScreenX'] == null);
this._LiveLocalForage = localforage.createInstance({name: "live"});
this._QueueLocalForage = localforage.createInstance({name: "queue"});
window.addEventListener("online",this.syncChangesToServer);
window.addEventListener("offline",this.handleOfflineEvent);
}
/**
* Posts/Updates/Delets should attempt to send HTTP Request
* to server, on success is normal. On failure the request
* should be queue for when online.
* @param {IOfflineStorageRequest} offlineStorageRequest
* @returns {Promise<any>}
* @memberof OfflineFirstHTTPService
*/
public post(offlineStorageRequest: IOfflineStorageRequest): Promise<any> {
return new Promise<any>( (resolve, reject) => {
fetch(offlineStorageRequest.value.requestInfo['url'], offlineStorageRequest.value.requestInit)
.then((response: Response) => {
if (response.ok) {
resolve(response.json());
} else {
this.addRequestToQueue(offlineStorageRequest);
reject(offlineStorageRequest);
}
})
.catch(() => {
reject(offlineStorageRequest);
this.addRequestToQueue(offlineStorageRequest);
});
});
}
/**
* Returns http request on success like using fetch,
* Errors should be qeued for re-attempts.
* @param {IOfflineStorageRequest} offlineStorageRequest
* @returns {Promise<any>}
* @memberof OfflineFirstHTTPService
*/
public getFromServer(offlineStorageRequest: IOfflineStorageRequest ): Promise<any> {
return new Promise<any>( (resolve: any, reject: any) => {
fetch(offlineStorageRequest.value.requestInfo['url'], offlineStorageRequest.value.requestInit)
.then( (json: Response) => {
if (json.ok) {
return json.json();
} else {
reject();
}
})
.then( (json) => {
this.setToLocal(offlineStorageRequest.key, json)
.then(() => {
console.log("Stored Demo JSON Offline.");
resolve(json);
})
.catch(() => {
console.log("Failed to store Demo JSON Offline");
reject(json);
});
})
.catch((error: Error|any) => {
this.addRequestToQueue(offlineStorageRequest);
console.error(error);
});
});
}
/**
* Offline Storage getter, that waits for offline storage
* to be initalized.
* @param {string} itemKey
* @returns {Promise<any>}
* @memberof OfflineFirstHTTPService
*/
public getFromLocal(itemKey: string): Promise<any> {
return new Promise<any>((resolve: any, reject: any) => {
this._LiveLocalForage.ready()
.then(() => {
return this._LiveLocalForage.getItem(itemKey);
})
.then((itemFromOffline: any) => {
resolve(itemFromOffline);
})
.catch( (error: Error|any) => {
reject();
});
});
}
/**
* Offline Storage setter, that waits for offline storage
* to be initalized.
* @param {string} key
* @param {string} value
* @returns {Promise<any>}
* @memberof OfflineFirstHTTPService
*/
public setToLocal(key: string, value: string): Promise<any> {
return new Promise<any>( (resolve: any, reject: any) => {
this._LiveLocalForage.ready()
.then(() => {
return this._LiveLocalForage.setItem(key, value);
})
.then( () => {
resolve();
})
.catch((error: Error|any) => {
reject(error);
});
});
}
/**
* Iterates through the queue to sync changes to the server.
* Decides what type of sync needs to be done.
* On success removes all items from queue.
* On failure leaves remaing items in the queue.
* If a item in the queue fails, this stops executing.
* @private
* @param {Event} e
* @memberof OfflineFirstHTTPService
*/
private syncChangesToServer(e: Event): void {
this._IsOnline = true;
this._QueueLocalForage.ready()
.then(() => {
return this._QueueLocalForage.keys();
})
.then( (keys) => {
return Promise.all(keys.map(this.chooseSyncProcess));
})
.then(() => {
console.log("Successfully synced with server");
})
.catch(() => {
this._IsOnline = false;
console.log("Did not sync with server, will retry when online.");
});
}
/**
* Gets the key from the offline storage and then
* re-attempts that HTTP request.
* On Success removes it from the queue
* On Failure leaves it in the queue
* @private
* @param {string} key
* @returns {Promise<any>}
* @memberof OfflineFirstHTTPService
*/
private chooseSyncProcess(key: string): Promise<any> {
return new Promise<any>( (resolve: any, reject: any) => {
this._QueueLocalForage.getItem(key)
.then((offlineStorageItem: IOfflineStorageRequest) => {
const requestInit = offlineStorageItem.value.requestInit;
if (requestInit === null || requestInit === undefined || requestInit.method === "GET") {
return this.syncGetsFromServer(offlineStorageItem);
}
return this.syncPostsToServer(offlineStorageItem);
})
.catch(() => {
reject();
});
});
}
/**
* Sends HTTP Posts/Updates/Deletes to the server that were in the queue.
* Successfull HTTP Requests get removed from the Queue.
* Unsuccessfull requests wait for the device to be online again.
* @private
* @param {IOfflineStorageRequest} offlineStorageRequest
* @memberof OfflineFirstHTTPService
*/
private syncPostsToServer(offlineStorageRequest: IOfflineStorageRequest): void {
this._QueueLocalForage.ready()
.then(() => {
return this.post(offlineStorageRequest);
})
.then((response: Response) => {
if (response.ok) {
return response.json();
}
return;
})
.then(() => {
return this._QueueLocalForage.removeItem(offlineStorageRequest.key);
})
.catch(() => {
console.log("Failed to sync to server, will retry when online.");
});
}
/**
* Sends HTTP Gets to the server that were in the queue.
* Sucessfull requests get removed from the queue and added to offline storage.
* Unsucessfull requests remain in the queue and wait for device to be online again.
* @private
* @param {IOfflineStorageRequest} offlineStorageRequest
* @memberof OfflineFirstHTTPService
*/
private syncGetsFromServer(offlineStorageRequest: IOfflineStorageRequest): void {
this._QueueLocalForage.ready()
.then( () => {
return this.getFromServer(offlineStorageRequest);
})
.then((response: Response) => {
if (response.ok) {
return response.json();
}
return;
})
.then((json: any) => {
return this._LiveLocalForage.setItem(offlineStorageRequest.key, json);
})
.then(() => {
return this._QueueLocalForage.removeItem(offlineStorageRequest.key);
})
.catch((error: Error | any) => {
console.log("Failed to sync " + offlineStorageRequest.key);
console.error(error);
});
}
/**
* Offline/Online is not completly supported by all browsers yet.
* This is here for future rather than present.
* In future checks will be made before all requests.
* Currently requests are attempted no matter status of Offline/Online.
* @private
* @param {Event} e
* @memberof OfflineFirstHTTPService
*/
private handleOfflineEvent(e: Event): void {
this._IsOnline = false;
}
/**
* Adds a HTTP Request to the Queue and will be called when
* the device is online again.
* @private
* @param {IOfflineStorageRequest} failedRequest
* @memberof OfflineFirstHTTPService
*/
private addRequestToQueue(failedRequest: IOfflineStorageRequest): void {
this._QueueLocalForage.ready()
.then( () => {
return this._QueueLocalForage.setItem(failedRequest.key, failedRequest.value);
})
.then(() => {
console.log("Added failedRequest " + failedRequest + " to the queue for later.");
})
.catch(() => {
console.log("Failed to add failedRequest " + failedRequest + " to the queue for later.");
});
}
}

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}

View File

@ -0,0 +1,3 @@
{
"extends": "./config/tslint.json"
}