| 1 | /*
|
|---|
| 2 | Copyright 2021 Google LLC
|
|---|
| 3 |
|
|---|
| 4 | Use of this source code is governed by an MIT-style
|
|---|
| 5 | license that can be found in the LICENSE file or at
|
|---|
| 6 | https://opensource.org/licenses/MIT.
|
|---|
| 7 | */
|
|---|
| 8 |
|
|---|
| 9 | import {openDB, DBSchema, IDBPDatabase} from 'idb';
|
|---|
| 10 | import {RequestData} from './StorableRequest.js';
|
|---|
| 11 | import '../_version.js';
|
|---|
| 12 |
|
|---|
| 13 | interface QueueDBSchema extends DBSchema {
|
|---|
| 14 | requests: {
|
|---|
| 15 | key: number;
|
|---|
| 16 | value: QueueStoreEntry;
|
|---|
| 17 | indexes: {queueName: string};
|
|---|
| 18 | };
|
|---|
| 19 | }
|
|---|
| 20 |
|
|---|
| 21 | const DB_VERSION = 3;
|
|---|
| 22 | const DB_NAME = 'workbox-background-sync';
|
|---|
| 23 | const REQUEST_OBJECT_STORE_NAME = 'requests';
|
|---|
| 24 | const QUEUE_NAME_INDEX = 'queueName';
|
|---|
| 25 |
|
|---|
| 26 | export interface UnidentifiedQueueStoreEntry {
|
|---|
| 27 | requestData: RequestData;
|
|---|
| 28 | timestamp: number;
|
|---|
| 29 | id?: number;
|
|---|
| 30 | queueName?: string;
|
|---|
| 31 | // We could use Record<string, unknown> as a type but that would be a breaking
|
|---|
| 32 | // change, better do it in next major release.
|
|---|
| 33 | // eslint-disable-next-line @typescript-eslint/ban-types
|
|---|
| 34 | metadata?: object;
|
|---|
| 35 | }
|
|---|
| 36 |
|
|---|
| 37 | export interface QueueStoreEntry extends UnidentifiedQueueStoreEntry {
|
|---|
| 38 | id: number;
|
|---|
| 39 | }
|
|---|
| 40 |
|
|---|
| 41 | /**
|
|---|
| 42 | * A class to interact directly an IndexedDB created specifically to save and
|
|---|
| 43 | * retrieve QueueStoreEntries. This class encapsulates all the schema details
|
|---|
| 44 | * to store the representation of a Queue.
|
|---|
| 45 | *
|
|---|
| 46 | * @private
|
|---|
| 47 | */
|
|---|
| 48 |
|
|---|
| 49 | export class QueueDb {
|
|---|
| 50 | private _db: IDBPDatabase<QueueDBSchema> | null = null;
|
|---|
| 51 |
|
|---|
| 52 | /**
|
|---|
| 53 | * Add QueueStoreEntry to underlying db.
|
|---|
| 54 | *
|
|---|
| 55 | * @param {UnidentifiedQueueStoreEntry} entry
|
|---|
| 56 | */
|
|---|
| 57 | async addEntry(entry: UnidentifiedQueueStoreEntry): Promise<void> {
|
|---|
| 58 | const db = await this.getDb();
|
|---|
| 59 | const tx = db.transaction(REQUEST_OBJECT_STORE_NAME, 'readwrite', {
|
|---|
| 60 | durability: 'relaxed',
|
|---|
| 61 | });
|
|---|
| 62 | await tx.store.add(entry as QueueStoreEntry);
|
|---|
| 63 | await tx.done;
|
|---|
| 64 | }
|
|---|
| 65 |
|
|---|
| 66 | /**
|
|---|
| 67 | * Returns the first entry id in the ObjectStore.
|
|---|
| 68 | *
|
|---|
| 69 | * @return {number | undefined}
|
|---|
| 70 | */
|
|---|
| 71 | async getFirstEntryId(): Promise<number | undefined> {
|
|---|
| 72 | const db = await this.getDb();
|
|---|
| 73 | const cursor = await db
|
|---|
| 74 | .transaction(REQUEST_OBJECT_STORE_NAME)
|
|---|
| 75 | .store.openCursor();
|
|---|
| 76 | return cursor?.value.id;
|
|---|
| 77 | }
|
|---|
| 78 |
|
|---|
| 79 | /**
|
|---|
| 80 | * Get all the entries filtered by index
|
|---|
| 81 | *
|
|---|
| 82 | * @param queueName
|
|---|
| 83 | * @return {Promise<QueueStoreEntry[]>}
|
|---|
| 84 | */
|
|---|
| 85 | async getAllEntriesByQueueName(
|
|---|
| 86 | queueName: string,
|
|---|
| 87 | ): Promise<QueueStoreEntry[]> {
|
|---|
| 88 | const db = await this.getDb();
|
|---|
| 89 | const results = await db.getAllFromIndex(
|
|---|
| 90 | REQUEST_OBJECT_STORE_NAME,
|
|---|
| 91 | QUEUE_NAME_INDEX,
|
|---|
| 92 | IDBKeyRange.only(queueName),
|
|---|
| 93 | );
|
|---|
| 94 | return results ? results : new Array<QueueStoreEntry>();
|
|---|
| 95 | }
|
|---|
| 96 |
|
|---|
| 97 | /**
|
|---|
| 98 | * Returns the number of entries filtered by index
|
|---|
| 99 | *
|
|---|
| 100 | * @param queueName
|
|---|
| 101 | * @return {Promise<number>}
|
|---|
| 102 | */
|
|---|
| 103 | async getEntryCountByQueueName(queueName: string): Promise<number> {
|
|---|
| 104 | const db = await this.getDb();
|
|---|
| 105 | return db.countFromIndex(
|
|---|
| 106 | REQUEST_OBJECT_STORE_NAME,
|
|---|
| 107 | QUEUE_NAME_INDEX,
|
|---|
| 108 | IDBKeyRange.only(queueName),
|
|---|
| 109 | );
|
|---|
| 110 | }
|
|---|
| 111 |
|
|---|
| 112 | /**
|
|---|
| 113 | * Deletes a single entry by id.
|
|---|
| 114 | *
|
|---|
| 115 | * @param {number} id the id of the entry to be deleted
|
|---|
| 116 | */
|
|---|
| 117 | async deleteEntry(id: number): Promise<void> {
|
|---|
| 118 | const db = await this.getDb();
|
|---|
| 119 | await db.delete(REQUEST_OBJECT_STORE_NAME, id);
|
|---|
| 120 | }
|
|---|
| 121 |
|
|---|
| 122 | /**
|
|---|
| 123 | *
|
|---|
| 124 | * @param queueName
|
|---|
| 125 | * @returns {Promise<QueueStoreEntry | undefined>}
|
|---|
| 126 | */
|
|---|
| 127 | async getFirstEntryByQueueName(
|
|---|
| 128 | queueName: string,
|
|---|
| 129 | ): Promise<QueueStoreEntry | undefined> {
|
|---|
| 130 | return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), 'next');
|
|---|
| 131 | }
|
|---|
| 132 |
|
|---|
| 133 | /**
|
|---|
| 134 | *
|
|---|
| 135 | * @param queueName
|
|---|
| 136 | * @returns {Promise<QueueStoreEntry | undefined>}
|
|---|
| 137 | */
|
|---|
| 138 | async getLastEntryByQueueName(
|
|---|
| 139 | queueName: string,
|
|---|
| 140 | ): Promise<QueueStoreEntry | undefined> {
|
|---|
| 141 | return await this.getEndEntryFromIndex(IDBKeyRange.only(queueName), 'prev');
|
|---|
| 142 | }
|
|---|
| 143 |
|
|---|
| 144 | /**
|
|---|
| 145 | * Returns either the first or the last entries, depending on direction.
|
|---|
| 146 | * Filtered by index.
|
|---|
| 147 | *
|
|---|
| 148 | * @param {IDBCursorDirection} direction
|
|---|
| 149 | * @param {IDBKeyRange} query
|
|---|
| 150 | * @return {Promise<QueueStoreEntry | undefined>}
|
|---|
| 151 | * @private
|
|---|
| 152 | */
|
|---|
| 153 | async getEndEntryFromIndex(
|
|---|
| 154 | query: IDBKeyRange,
|
|---|
| 155 | direction: IDBCursorDirection,
|
|---|
| 156 | ): Promise<QueueStoreEntry | undefined> {
|
|---|
| 157 | const db = await this.getDb();
|
|---|
| 158 |
|
|---|
| 159 | const cursor = await db
|
|---|
| 160 | .transaction(REQUEST_OBJECT_STORE_NAME)
|
|---|
| 161 | .store.index(QUEUE_NAME_INDEX)
|
|---|
| 162 | .openCursor(query, direction);
|
|---|
| 163 | return cursor?.value;
|
|---|
| 164 | }
|
|---|
| 165 |
|
|---|
| 166 | /**
|
|---|
| 167 | * Returns an open connection to the database.
|
|---|
| 168 | *
|
|---|
| 169 | * @private
|
|---|
| 170 | */
|
|---|
| 171 | private async getDb() {
|
|---|
| 172 | if (!this._db) {
|
|---|
| 173 | this._db = await openDB(DB_NAME, DB_VERSION, {
|
|---|
| 174 | upgrade: this._upgradeDb,
|
|---|
| 175 | });
|
|---|
| 176 | }
|
|---|
| 177 | return this._db;
|
|---|
| 178 | }
|
|---|
| 179 |
|
|---|
| 180 | /**
|
|---|
| 181 | * Upgrades QueueDB
|
|---|
| 182 | *
|
|---|
| 183 | * @param {IDBPDatabase<QueueDBSchema>} db
|
|---|
| 184 | * @param {number} oldVersion
|
|---|
| 185 | * @private
|
|---|
| 186 | */
|
|---|
| 187 | private _upgradeDb(db: IDBPDatabase<QueueDBSchema>, oldVersion: number) {
|
|---|
| 188 | if (oldVersion > 0 && oldVersion < DB_VERSION) {
|
|---|
| 189 | if (db.objectStoreNames.contains(REQUEST_OBJECT_STORE_NAME)) {
|
|---|
| 190 | db.deleteObjectStore(REQUEST_OBJECT_STORE_NAME);
|
|---|
| 191 | }
|
|---|
| 192 | }
|
|---|
| 193 |
|
|---|
| 194 | const objStore = db.createObjectStore(REQUEST_OBJECT_STORE_NAME, {
|
|---|
| 195 | autoIncrement: true,
|
|---|
| 196 | keyPath: 'id',
|
|---|
| 197 | });
|
|---|
| 198 | objStore.createIndex(QUEUE_NAME_INDEX, QUEUE_NAME_INDEX, {unique: false});
|
|---|
| 199 | }
|
|---|
| 200 | }
|
|---|