Skip to content

Commit

Permalink
Merge pull request #95 from mozilla-services/localStorage-adapter
Browse files Browse the repository at this point in the history
[WiP] Added LocalStorage adapter.
  • Loading branch information
n1k0 committed Jul 27, 2015
2 parents d723930 + 2122c84 commit df5e77c
Show file tree
Hide file tree
Showing 14 changed files with 1,124 additions and 250 deletions.
277 changes: 257 additions & 20 deletions demo/kinto.dev.js

Large diffs are not rendered by default.

25 changes: 15 additions & 10 deletions demo/kinto.min.js

Large diffs are not rendered by default.

277 changes: 257 additions & 20 deletions dist/kinto.dev.js

Large diffs are not rendered by default.

25 changes: 15 additions & 10 deletions dist/kinto.min.js

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ const db = new Kinto(options);

- `remote`: The remote Kinto server endpoint root URL (eg. `"https://server/v1"`). Not that you *must* define a URL matching the version of the protocol the client supports, otherwise you'll get an error;
- `headers`: The default headers to pass for every HTTP request performed to the Cliquet server (eg. `{"Authorization": "Basic bWF0Og=="}`);
- `adapter`: The persistence layer adapter to use for saving data locally (default: `Kinto.adapters.IDB`); alternatively, a `Kinto.adapters.LocalStorage` adapter is also provided; last, if you plan on writing your own adapter, you can read more about how to do so in the [Extending Kinto.js](extending.md) section.

## Collections

Collections are persisted locally in indexedDB.
By default, collections are persisted locally in IndexedDB.

#### Notes

> A `localStorage` adapter is also available, though we suggest to stick with IndexedDB whenever you can, as it's faster, more reliable and accepts greater data quotas withouth requiring specific configuration.
Selecting a collection is done by calling the `collection()` method, passing it the resource name:

Expand Down
7 changes: 6 additions & 1 deletion docs/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Simply create a class extending from `Kinto.BaseAdapter`, which rather acts as a

```js
class MyAdapter extends Kinto.BaseAdapter {
constructor(dbname) {
super();
this.dbname = dbname;
}

create(record) {
}
Expand All @@ -26,4 +31,4 @@ Then create the Kinto object passing a reference to your adapter class:
const kinto = new Kinto({adapter: MyAdapter});
```

Read the `BaseAdapter` class [source code](https://github.com/mozilla-services/kinto.js/blob/master/src/adapters/base.js) to figure out what needs to be implemented exactly.
Read the `BaseAdapter` class [source code](https://github.com/mozilla-services/kinto.js/blob/master/src/adapters/base.js) to figure out what needs to be implemented exactly. [IDB](https://github.com/mozilla-services/kinto.js/blob/master/src/adapters/IDB.js) and [LocalStorage](https://github.com/mozilla-services/kinto.js/blob/master/src/adapters/localStorage.js) adapters are also worth a read if you need guidance writing your own.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
"scripts": {
"demo": "npm run dist && http-server demo",
"dist": "npm run dist-dev && npm run dist-prod",
"dist-dev": "browserify -s Kinto -x fake-indexeddb -d -e src/index.js -o dist/kinto.dev.js && cp dist/kinto.dev.js demo/",
"dist-prod": "browserify -s Kinto -x fake-indexeddb -g uglifyify -e src/index.js -o dist/kinto.min.js && cp dist/kinto.min.js demo/",
"dist-dev": "browserify -s Kinto -x fake-indexeddb -x localStorage -d -e src/index.js -o dist/kinto.dev.js && cp dist/kinto.dev.js demo/",
"dist-prod": "browserify -s Kinto -x fake-indexeddb -x localStorage -g uglifyify -e src/index.js -o dist/kinto.min.js && cp dist/kinto.min.js demo/",
"report-coverage": "npm run test-cover && ./node_modules/coveralls/bin/coveralls.js < ./coverage/lcov.info",
"tdd": "mocha -w --compilers js:babel/register 'test/**/*_test.js'",
"test": "npm run test-nocover",
"test-cover": "babel-node node_modules/.bin/isparta cover --report text $npm_package_config_ISPARTA_OPTS node_modules/.bin/_mocha -- 'test/**/*_test.js'",
"test-cover-html": "babel-node node_modules/.bin/isparta cover --report html $npm_package_config_ISPARTA_OPTS node_modules/.bin/_mocha -- 'test/**/*_test.js' && open coverage/index.html",
"test-nocover": "node_modules/.bin/_mocha --compilers js:babel/register 'test/**/*_test.js'",
"watch": "watchify -s Kinto -x fake-indexeddb -d -e src/index.js -o dist/kinto.dev.js -v"
"watch": "watchify -s Kinto -x fake-indexeddb -x localStorage -d -e src/index.js -o dist/kinto.dev.js -v"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -58,6 +58,7 @@
"http-server": "^0.8.0",
"isomorphic-fetch": "^2.1.0",
"isparta": "^3.0.3",
"localStorage": "^1.0.3",
"mocha": "^2.2.5",
"sinon": "^1.14.1",
"uglifyify": "^3.0.1",
Expand Down
173 changes: 173 additions & 0 deletions src/adapters/LocalStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"use strict";

import BaseAdapter from "./base.js";

const root = typeof window === "object" ? window : global;

// Only load localStorage in a nodejs environment
if (!root.hasOwnProperty("localStorage")) {
root.localStorage = require("localStorage");
}

export default class LocalStorage extends BaseAdapter {
constructor(dbname) {
super();
this._db = null;
this._keyStoreName = `${this.dbname}/__keys`;
this._keyLastModified = `${this.dbname}/__lastModified`;
// public properties
this.dbname = dbname;
}

_handleError(method, err) {
const error = new Error(method + "() " + err.message);
error.stack = err.stack;
return Promise.reject(error);
}

/**
* Retrieve all existing keys.
*
* @return {Array}
*/
get keys() {
return JSON.parse(localStorage.getItem(this._keyStoreName)) || [];
}

/**
* Set keys.
*
* @param {Array} keys
*/
set keys(keys) {
localStorage.setItem(this._keyStoreName, JSON.stringify(keys));
}

/**
* Deletes every records in the current collection.
*
* @return {Promise}
*/
clear() {
try {
localStorage.clear();
return Promise.resolve();
} catch (err) {
return this._handleError("clear", err);
}
}

/**
* Adds a record to the localStorage datastore.
*
* Note: An id value is required.
*
* @param {Object} record The record object, including an id.
* @return {Promise}
*/
create(record) {
if (this.keys.indexOf(record.id) !== -1)
return Promise.reject(new Error("Exists."));
try {
localStorage.setItem(`${this.dbname}/${record.id}`, JSON.stringify(record));
this.keys = this.keys.concat(record.id);
return Promise.resolve(record);
} catch(err) {
return this._handleError("create", err);
}
}

/**
* Updates a record from the localStorage datastore.
*
* @param {Object} record
* @return {Promise}
*/
update(record) {
if (this.keys.indexOf(record.id) === -1)
return Promise.reject(new Error("Doesn't exist."))
try {
localStorage.setItem(`${this.dbname}/${record.id}`, JSON.stringify(record));
return Promise.resolve(record);
} catch(err) {
return this._handleError("update", err);
}
}

/**
* Retrieve a record by its primary key from the localStorage datastore.
*
* @param {String} id The record id.
* @return {Promise}
*/
get(id) {
try {
return Promise.resolve(
JSON.parse(localStorage.getItem(`${this.dbname}/${id}`)) || undefined);
} catch(err) {
return this._handleError("get", err);
}
}

/**
* Deletes a record from the localStorage datastore.
*
* @param {String} id The record id.
* @return {Promise}
*/
delete(id) {
try {
localStorage.removeItem(`${this.dbname}/${id}`);
this.keys = this.keys.filter(key => key !== id);
return Promise.resolve(id);
} catch(err) {
return this._handleError("delete", err);
}
}

/**
* Lists all records from the localStorage datastore.
*
* @return {Promise}
*/
list() {
try {
return Promise.resolve(this.keys.map(id => {
return JSON.parse(localStorage.getItem(`${this.dbname}/${id}`))
}));
} catch(err) {
return this._handleError("list", err);
}
}

/**
* Store the lastModified value into metadata store.
*
* @param {Number} lastModified
* @param {Object} options
* @return {Promise}
*/
saveLastModified(lastModified) {
var value = parseInt(lastModified, 10);
try {
localStorage.setItem(this._keyLastModified, JSON.stringify(value));
return Promise.resolve(value);
} catch(err) {
return this._handleError("saveLastModified", err);
}
}

/**
* Retrieve saved lastModified value.
*
* @return {Promise}
*/
getLastModified() {
try {
const lastModified = JSON.parse(localStorage.getItem(this._keyLastModified));
return Promise.resolve(parseInt(lastModified, 10) || undefined);
} catch(err) {
return this._handleError("getLastModified", err);
}
}
}
12 changes: 9 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { EventEmitter } from "events";
import Api from "./api";
import Collection from "./collection";
import BaseAdapter from "./adapters/base";
import LocalStorage from "./adapters/LocalStorage";
import IDB from "./adapters/IDB";

const DEFAULT_BUCKET_NAME = "default";

Expand All @@ -19,8 +21,12 @@ export default class Kinto {
* their DB adapter.
* @return {BaseAdapter}
*/
static get BaseAdapter() {
return BaseAdapter;
static get adapters() {
return {
BaseAdapter: BaseAdapter,
LocalStorage: LocalStorage,
IDB: IDB,
}
}

/**
Expand Down Expand Up @@ -59,7 +65,7 @@ export default class Kinto {
if (!this._collections.hasOwnProperty(collName)) {
this._collections[collName] = new Collection(bucket, collName, api, {
events: this.events,
adapter: this._options.adapter,
adapter: this._options.adapter || Kinto.adapters.IDB,
});
}

Expand Down

0 comments on commit df5e77c

Please sign in to comment.