-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Mårten Wikström
committed
Sep 3, 2018
1 parent
da611e4
commit 8b97974
Showing
6 changed files
with
191 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { IQueryDescriptor } from "./query-descriptor"; | ||
|
||
/** @public */ | ||
export interface IPurgeOptions { | ||
activeQueries?: IQueryDescriptor[]; | ||
commandRetentionPeriod?: number; | ||
queryRetentionPeriod?: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import "../test-helpers/setup-fake-indexeddb"; | ||
import "../test-helpers/setup-text-encoding"; | ||
import "../test-helpers/setup-webcrypto"; | ||
|
||
import { createJsonCrypto } from "../api/create-json-crypto"; | ||
import { IDatastore } from "../api/datastore"; | ||
import { openDatastore } from "../api/open-datastore"; | ||
import { IQueryDescriptor } from "../api/query-descriptor"; | ||
|
||
describe("purge", () => { | ||
let store: IDatastore; | ||
let now: Date; | ||
|
||
beforeEach(async () => { | ||
const name = `test-${Math.floor(Math.random() * 9999999)}`; | ||
const crypto = await createJsonCrypto(); | ||
now = new Date(); | ||
store = await openDatastore({ name, crypto, now: () => now }); | ||
}); | ||
|
||
afterEach(() => store.close()); | ||
|
||
it("drops old queries", async () => { | ||
const query: IQueryDescriptor = { type: "x" }; | ||
await store.setQueryResult(query, "a", null); | ||
expect((await store.getQueryList()).length).toBe(1); | ||
now = new Date(now.getTime() + 60 * 60 * 1000); // one hour later | ||
await store.purge(); | ||
expect((await store.getQueryList()).length).toBe(1); // not dropped | ||
now = new Date(now.getTime() + 1000); // but one more second later | ||
await store.purge(); | ||
expect((await store.getQueryList()).length).toBe(0); // dropped! | ||
}); | ||
|
||
it("drops old queries with custom retention period", async () => { | ||
const query: IQueryDescriptor = { type: "x" }; | ||
await store.setQueryResult(query, "a", null); | ||
expect((await store.getQueryList()).length).toBe(1); | ||
now = new Date(now.getTime() + 1000); | ||
await store.purge({ queryRetentionPeriod: 1000 }); | ||
expect((await store.getQueryList()).length).toBe(1); // not dropped | ||
now = new Date(now.getTime() + 1); | ||
await store.purge({ queryRetentionPeriod: 1000 }); | ||
expect((await store.getQueryList()).length).toBe(0); // dropped! | ||
}); | ||
|
||
it("does not drop old and active queries", async () => { | ||
const query: IQueryDescriptor = { type: "x" }; | ||
await store.setQueryResult(query, "a", null); | ||
expect((await store.getQueryList()).length).toBe(1); | ||
now = new Date(now.getTime() + 2 * 60 * 60 * 1000); // two hours later | ||
await store.purge({ activeQueries: [ query ]}); | ||
expect((await store.getQueryList()).length).toBe(1); // not dropped (since it was active) | ||
}); | ||
|
||
it("drops old rejected commands", async () => { | ||
const cmd = await store.addCommand({ type: "x", target: "y" }); | ||
await store.setCommandRejected(cmd.key); | ||
expect((await store.getCommandList()).length).toBe(1); | ||
now = new Date(now.getTime() + 5 * 60 * 1000); // five minutes later | ||
await store.purge(); | ||
expect((await store.getCommandList()).length).toBe(1); // not dropped | ||
now = new Date(now.getTime() + 1000); // but one more second later | ||
await store.purge(); | ||
expect((await store.getCommandList()).length).toBe(0); // dropped! | ||
}); | ||
|
||
it("drops old rejected commands with custom retention period", async () => { | ||
const cmd = await store.addCommand({ type: "x", target: "y" }); | ||
await store.setCommandRejected(cmd.key); | ||
expect((await store.getCommandList()).length).toBe(1); | ||
now = new Date(now.getTime() + 1000); | ||
await store.purge({ commandRetentionPeriod: 1000 }); | ||
expect((await store.getCommandList()).length).toBe(1); // not dropped | ||
now = new Date(now.getTime() + 1); | ||
await store.purge({ commandRetentionPeriod: 1000 }); | ||
expect((await store.getCommandList()).length).toBe(0); // dropped! | ||
}); | ||
|
||
it("does not drop pending commands", async () => { | ||
const cmd = await store.addCommand({ type: "x", target: "y" }); | ||
expect((await store.getCommandList()).length).toBe(1); | ||
now = new Date(now.getTime() + 60 * 60 * 1000); // one hour later later | ||
await store.purge(); | ||
expect((await store.getCommandList()).length).toBe(1); // not dropped | ||
await store.setCommandRejected(cmd.key); | ||
now = new Date(now.getTime() + 6 * 60 * 1000); // six minutes later | ||
await store.purge(); | ||
expect((await store.getCommandList()).length).toBe(0); // dropped! | ||
}); | ||
|
||
it("drops synced accepted commands", async () => { | ||
const query: IQueryDescriptor = { type: "x" }; | ||
const cmd = await store.addCommand({ type: "x", target: "y" }); | ||
await store.setCommandAccepted(cmd.key, "b"); | ||
expect((await store.getCommandList()).length).toBe(1); // not dropped | ||
now = new Date(now.getTime() + 60 * 60 * 1000); // one hour later later | ||
await store.setQueryResult(query, "a", null); | ||
await store.purge(); | ||
expect((await store.getCommandList()).length).toBe(1); // not dropped | ||
await store.updateQueryResult(query, { commitBefore: "a", commitAfter: "b" }); | ||
await store.purge(); | ||
expect((await store.getCommandList()).length).toBe(0); // dropped! | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { IJsonObject } from "../api/json-value"; | ||
import { IPurgeOptions } from "../api/purge-options"; | ||
import { computeJsonHash } from "../json/compute-json-hash"; | ||
import { assert } from "../utils/assert"; | ||
import { DEBUG } from "../utils/env"; | ||
import { uint8ArrayToBase64Url } from "../utils/uint8-array-to-base64-url"; | ||
import { DatastoreContext } from "./datastore-context"; | ||
|
||
/** @internal */ | ||
export async function purge( | ||
context: DatastoreContext, | ||
options: IPurgeOptions = {}, | ||
): Promise<void> { | ||
// istanbul ignore else: debug assertion | ||
if (DEBUG) { | ||
assert(context instanceof DatastoreContext); | ||
} | ||
|
||
const { | ||
db, | ||
now, | ||
} = context; | ||
|
||
const { | ||
activeQueries = [], | ||
commandRetentionPeriod = 5 * 60 * 1000, // default is five minutes | ||
queryRetentionPeriod = 60 * 60 * 1000, // default is one hour | ||
} = options; | ||
|
||
const currentTime = now().getTime(); | ||
const keepCommandsAfter = new Date(currentTime - commandRetentionPeriod); | ||
const keepQueriesAfter = new Date(currentTime - queryRetentionPeriod); | ||
const activeQueryKeys = new Set(await Promise.all(activeQueries.map( | ||
async query => encodeKey(await computeJsonHash(query as any as IJsonObject))))); | ||
|
||
await db.transaction( | ||
"rw", | ||
db.commands, | ||
db.queries, | ||
db.results, | ||
async () => { | ||
// Delete queries with a timestamp older than the specified retention period, | ||
// but keep those that listed as "active". | ||
const oldQueryKeys = await db.queries.where("timestamp").below(keepQueriesAfter).primaryKeys(); | ||
const queryKeysToDrop = oldQueryKeys.filter(key => !activeQueryKeys.has(encodeKey(key))); | ||
await Promise.all([ | ||
db.queries.bulkDelete(queryKeysToDrop), | ||
db.results.bulkDelete(queryKeysToDrop), | ||
]); | ||
|
||
// Delete commands with a timestamp older than the specified retention period, | ||
// but keep all pending commands and all accepted commands with a commit | ||
// version higher than the commit of the most out of sync query. | ||
const mostOutOfSyncQuery = await db.queries.orderBy("commit").first(); | ||
const mostOutOfSyncCommit = mostOutOfSyncQuery ? mostOutOfSyncQuery.commit : ""; | ||
const oldCommands = await db.commands.where("timestamp").below(keepCommandsAfter).toArray(); | ||
const commandKeysToDrop = oldCommands.filter(cmd => | ||
cmd.resolved === true && // never drop pending commands | ||
cmd.commit <= mostOutOfSyncCommit, // keep accepted commands with an unsynced commit | ||
).map(cmd => cmd.key); | ||
await db.commands.bulkDelete(commandKeysToDrop); | ||
}, | ||
); | ||
} | ||
|
||
function encodeKey( | ||
key: ArrayBuffer, | ||
): string { | ||
return uint8ArrayToBase64Url(new Uint8Array(key)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters