Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added expiration options #49

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@

================================================================================
v1.1.1
================================================================================
Added: Expiration option, expires works with minutes,
or you can send a datetime and set the option
`isExpiresDate` to true.

================================================================================
v1.1.0
================================================================================
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "immortal-db",
"private": false,
"version": "1.1.0",
"version": "1.1.1",
"main": "dist/immortal-db.js",
"module": "src/index.js",
"types": "immortal-db.d.ts",
Expand Down
45 changes: 34 additions & 11 deletions src/cookie-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ const DEFAULT_COOKIE_TTL = 365 // Days.
// https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 for
// details on SameSite and cross-origin behavior.
const CROSS_ORIGIN_IFRAME = amIInsideACrossOriginIframe()
const DEFAULT_SECURE = (CROSS_ORIGIN_IFRAME ? true : false)
const DEFAULT_SAMESITE = (CROSS_ORIGIN_IFRAME ? 'None' : 'Lax')
const DEFAULT_SECURE = !!CROSS_ORIGIN_IFRAME
const DEFAULT_SAMESITE = CROSS_ORIGIN_IFRAME ? 'None' : 'Lax'

function amIInsideACrossOriginIframe () {
try {
Expand All @@ -28,17 +28,18 @@ function amIInsideACrossOriginIframe () {
// If inside a cross-origin iframe, raises: Uncaught
// DOMException: Blocked a frame with origin "..." from
// accessing a cross-origin frame.
return !Boolean(window.top.location.href)
return !window.top.location.href
} catch (err) {
return true
}
}

class CookieStore {
constructor ({
ttl = DEFAULT_COOKIE_TTL,
secure = DEFAULT_SECURE,
sameSite = DEFAULT_SAMESITE} = {}) {
ttl = DEFAULT_COOKIE_TTL,
secure = DEFAULT_SECURE,
sameSite = DEFAULT_SAMESITE,
} = {}) {
this.ttl = ttl
this.secure = secure
this.sameSite = sameSite
Expand All @@ -48,23 +49,45 @@ class CookieStore {

async get (key) {
const value = Cookies.get(key)
console.log(Cookies.expires)
return typeof value === 'string' ? value : undefined
}

async set (key, value) {
Cookies.set(key, value, this._constructCookieParams())
async set (key, value, options = { expires: 0, isExpiresDate: false }) {
let opts = {}
if (options && options.expires) {
opts.expires = options.isExpiresDate
? new Date(options.expires)
: new Date(new Date().getTime() + options.expires * 60 * 1000)
}
Cookies.set(key, value, this._constructCookieParams(opts))
}

async remove (key) {
Cookies.remove(key, this._constructCookieParams())
}

_constructCookieParams () {
return {
expires: this.ttl,
_constructCookieParams (
options = {
expires: this.tll,
secure: this.secure,
sameSite: this.sameSite,
},
) {
const opts = {
expires: this.tll,
secure: this.secure,
sameSite: this.sameSite,
}

const keys = Object.keys(opts)
for (let i = 0; i < keys.length; i++) {
if (options[keys[i]]) {
opts[keys[i]] = options[keys[i]]
}
}

return opts
}
}

Expand Down
62 changes: 44 additions & 18 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { LocalStorageStore, SessionStorageStore } from './web-storage'

const cl = console.log
const DEFAULT_KEY_PREFIX = '_immortal|'
const WINDOW_IS_DEFINED = (typeof window !== 'undefined')
const WINDOW_IS_DEFINED = typeof window !== 'undefined'

// Stores must implement asynchronous constructor, get(), set(), and
// remove() methods.
Expand Down Expand Up @@ -92,21 +92,25 @@ class ImmortalStorage {
// classes (whose definitions are synchronous) must be accepted in
// addition to instantiated store objects.
this.onReady = (async () => {
this.stores = (await Promise.all(
stores.map(async StoreClassOrInstance => {
if (typeof StoreClassOrInstance === 'object') { // Store instance.
return StoreClassOrInstance
} else { // Store class.
try {
return await new StoreClassOrInstance() // Instantiate instance.
} catch (err) {
// TODO(grun): Log (where?) that the <Store> constructor Promise
// failed.
return null
this.stores = (
await Promise.all(
stores.map(async StoreClassOrInstance => {
if (typeof StoreClassOrInstance === 'object') {
// Store instance.
return StoreClassOrInstance
} else {
// Store class.
try {
return await new StoreClassOrInstance() // Instantiate instance.
} catch (err) {
// TODO(grun): Log (where?) that the <Store> constructor Promise
// failed.
return null
}
}
}
}),
)).filter(Boolean)
}),
)
).filter(Boolean)
})()
}

Expand All @@ -125,6 +129,28 @@ class ImmortalStorage {
}),
)

const expiresValues = await Promise.all(
this.stores.map(async store => {
try {
return await store.getExpires(prefixedKey)
} catch (err) {}
}),
)

const countedExpires = Array.from(countUniques(expiresValues).entries())
countedExpires.sort((a, b) => a[1] <= b[1])
let expires
const [firstVal, firstCo] = arrayGet(countedExpires, 0, [undefined, 0])
const [secondVal, secondCo] = arrayGet(countedExpires, 1, [undefined, 0])
if (
firstCo > secondCo ||
(firstCo === secondCo && firstVal !== undefined)
) {
expires = firstVal
} else {
expires = secondVal
}

const counted = Array.from(countUniques(values).entries())
counted.sort((a, b) => a[1] <= b[1])

Expand All @@ -141,23 +167,23 @@ class ImmortalStorage {
}

if (value !== undefined) {
await this.set(key, value)
await this.set(key, value, { expires, isExpiresDate: true })
return value
} else {
await this.remove(key)
return _default
}
}

async set (key, value) {
async set (key, value, options = { expires: 0 }) {
await this.onReady

key = `${DEFAULT_KEY_PREFIX}${key}`

await Promise.all(
this.stores.map(async store => {
try {
await store.set(key, value)
await store.set(key, value, options)
} catch (err) {
cl(err)
}
Expand Down
45 changes: 42 additions & 3 deletions src/indexed-db.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ import {

const DEFAULT_DATABASE_NAME = 'ImmortalDB'
const DEFAULT_STORE_NAME = 'key-value-pairs'
const DEFAULT_EXPIRES_DB_NAME = 'ImmortalDBExp'

class IndexedDbStore {
constructor (dbName = DEFAULT_DATABASE_NAME, storeName = DEFAULT_STORE_NAME) {
constructor (
dbName = DEFAULT_DATABASE_NAME,
storeName = DEFAULT_STORE_NAME,
expiresDBName = DEFAULT_EXPIRES_DB_NAME,
) {
this.store = new Store(dbName, storeName)
this.expiresStore = new Store(expiresDBName, storeName)

return (async () => {
// Safari throws a SecurityError if IndexedDB.open() is called in a
Expand All @@ -34,6 +40,7 @@ class IndexedDbStore {
// Safari. Push the fix(es) upstream.
try {
await this.store._dbp
await this.expiresStore._dbp
} catch (err) {
if (err.name === 'SecurityError') {
return null // Failed to open an IndexedDB database.
Expand All @@ -47,17 +54,49 @@ class IndexedDbStore {
}

async get (key) {
const val = await this.getExpires(key)
if (val && val <= new Date().getTime()) {
await this.remove(key)
}

const value = await idbGet(key, this.store)
return typeof value === 'string' ? value : undefined
}

async set (key, value) {
async set (key, value, options = { expires: 0, isExpiresDate: false }) {
await idbSet(key, value, this.store)

if (options && options.expires) {
// If expire exists, update or add it.
await idbSet(
key,
options.isExpiresDate
? options.expires.toString()
: new Date(new Date().getTime() + options.expires * 60 * 1000)
.getTime()
.toString(),
this.expiresStore,
)
} else {
// If it doesn't exist, remove any existing expiration
await idbRemove(key, this.expiresStore)
}
}

async getExpires (key) {
const value = await idbGet(key, this.expiresStore)
return typeof value === 'string' ? +value : 0
}

async remove (key) {
await idbRemove(key, this.expiresStore)
await idbRemove(key, this.store)
}
}

export { IndexedDbStore, DEFAULT_DATABASE_NAME, DEFAULT_STORE_NAME }
export {
IndexedDbStore,
DEFAULT_DATABASE_NAME,
DEFAULT_STORE_NAME,
DEFAULT_EXPIRES_DB_NAME,
}
33 changes: 31 additions & 2 deletions src/web-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,53 @@
// License: MIT
//

const EXPIRES_PREFIX = 'exp_'

class StorageApiWrapper {
constructor (store) {
constructor (store, expiresPrefix = EXPIRES_PREFIX) {
this.store = store
this.expiresPrefix = expiresPrefix

return (async () => this)()
}

async get (key) {
const val = await this.getExpires(key)
if (val && val <= new Date().getTime()) {
await this.remove(key)
}

const value = this.store.getItem(key)
return typeof value === 'string' ? value : undefined
}

async set (key, value) {
async set (key, value, options = { expires: 0, isExpiresDate: false }) {
this.store.setItem(key, value)

if (options && options.expires) {
// If expire exists, update or add it.
this.store.setItem(
this.expiresPrefix + key,
options.isExpiresDate
? options.expires.toString()
: new Date(new Date().getTime() + options.expires * 60 * 1000)
.getTime()
.toString(),
)
} else {
// If it doesn't exist, remove any existing expiration
this.store.removeItem(this.expiresPrefix + key)
}
}

async getExpires (key) {
const value = await this.store.getItem(this.expiresPrefix + key)
return typeof value === 'string' ? +value : 0
}

async remove (key) {
this.store.removeItem(key)
this.store.removeItem(this.expiresPrefix + key)
}
}

Expand Down
5 changes: 4 additions & 1 deletion testing/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@

<span>Key</span><input id="key" type="text" value="keysup"><br>
<br>
<span>Value</span> <input id="value" type="text">
<span>Value</span> <input id="value" type="text"><br>
<br>
<span>Expires</span> <input id="expires" type="text" value="0"><br>
<br>
<button id="set" type="submit">Set</button>
<button id="get" type="submit">Get</button>
<button id="remove" type="submit">Remove</button>
Expand Down
8 changes: 7 additions & 1 deletion testing/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const cl = console.log
const CookieStore = ImmortalDB.CookieStore
const ImmortalStorage = ImmortalDB.ImmortalStorage
const idb = new idbKeyval.Store('ImmortalDB', 'key-value-pairs')
const idbExpires = new idbKeyval.Store('ImmortalDB', 'key-value-expires')


const POLL_TIMEOUT = 300 // Milliseconds.
const PREFIX = ImmortalDB.DEFAULT_KEY_PREFIX
Expand Down Expand Up @@ -40,11 +42,15 @@ function $ele (id) {

const $key = $ele('key')
const $value = $ele('value')
const $expires = $ele('expires')

$ele('get').addEventListener(
'click', async () => $value.value = await db.get($key.value), false)
$ele('set').addEventListener(
'click', async () => await db.set($key.value, $value.value), false)
'click', async () => {
const expires = +$expires.value
await db.set($key.value, $value.value, { expires })
}, false)
$ele('remove').addEventListener('click', async () => {
await db.remove($key.value)
$value.value = ''
Expand Down