2 * Common indexedDB-access methods, only for use by the ResourceLoader modules this directory.
5 const config = require( './config.json' );
6 const dbName = mw.config.get( 'wgDBname' ) + '_editRecovery';
7 const editRecoveryExpiry = config.EditRecoveryExpiry;
8 const objectStoreName = 'unsaved-page-data';
12 // TODO: Document Promise objects as native promises, not jQuery ones.
16 * @return {jQuery.Promise} Promise which resolves on success
18 function openDatabaseLocal() {
19 return new Promise( ( resolve, reject ) => {
20 const schemaNumber = 3;
21 const openRequest = window.indexedDB.open( dbName, schemaNumber );
22 openRequest.addEventListener( 'upgradeneeded', upgradeDatabase );
23 openRequest.addEventListener( 'success', ( event ) => {
24 db = event.target.result;
27 openRequest.addEventListener( 'error', ( event ) => {
28 reject( 'EditRecovery error: ' + event.target.error );
35 * @param {Object} versionChangeEvent
37 function upgradeDatabase( versionChangeEvent ) {
38 const keyPathParts = [ 'pageName', 'section' ];
41 db = versionChangeEvent.target.result;
42 if ( !db.objectStoreNames.contains( objectStoreName ) ) {
43 // ObjectStore does not yet exist, create it.
44 objectStore = db.createObjectStore( objectStoreName, { keyPath: keyPathParts } );
46 // ObjectStore exists, but needs to be upgraded.
47 objectStore = versionChangeEvent.target.transaction.objectStore( objectStoreName );
50 // Create indexes if they don't exist.
51 if ( !objectStore.indexNames.contains( 'pageName-section' ) ) {
52 objectStore.createIndex( 'pageName-section', keyPathParts, { unique: true } );
54 if ( !objectStore.indexNames.contains( 'expiry' ) ) {
55 objectStore.createIndex( 'expiry', 'expiry' );
58 // Delete old indexes.
59 if ( objectStore.indexNames.contains( 'lastModified' ) ) {
60 objectStore.deleteIndex( 'lastModified' );
62 if ( objectStore.indexNames.contains( 'expiryDate' ) ) {
63 objectStore.deleteIndex( 'expiryDate' );
68 * Load data relating to a specific page and section
71 * @param {string} pageName The current page name (with underscores)
72 * @param {string|null} section The section ID, or null if the whole page is being edited
73 * @return {jQuery.Promise} Promise which resolves with the page data on success, or rejects with an error message.
75 function loadData( pageName, section ) {
76 return new Promise( ( resolve, reject ) => {
78 reject( 'DB not opened' );
80 const transaction = db.transaction( objectStoreName, 'readonly' );
81 const key = [ pageName, section || '' ];
82 const findExisting = transaction
83 .objectStore( objectStoreName )
85 findExisting.addEventListener( 'success', () => {
86 resolve( findExisting.result );
91 function loadAllData() {
92 return new Promise( ( resolve, reject ) => {
94 reject( 'DB not opened' );
96 const transaction = db.transaction( objectStoreName, 'readonly' );
97 const requestAll = transaction
98 .objectStore( objectStoreName )
100 requestAll.addEventListener( 'success', () => {
101 resolve( requestAll.result );
107 * Save data for a specific page and section
110 * @param {string} pageName The current page name (with underscores)
111 * @param {string|null} section The section ID, or null if the whole page is being edited
112 * @param {Object} pageData The page data to save
113 * @return {jQuery.Promise} Promise which resolves on success, or rejects with an error message.
115 function saveData( pageName, section, pageData ) {
116 return new Promise( ( resolve, reject ) => {
118 reject( 'DB not opened' );
121 // Add indexed fields.
122 pageData.pageName = pageName;
123 pageData.section = section || '';
124 pageData.expiry = getExpiryDate( editRecoveryExpiry );
126 const transaction = db.transaction( objectStoreName, 'readwrite' );
127 const objectStore = transaction.objectStore( objectStoreName );
129 const request = objectStore.put( pageData );
130 request.addEventListener( 'success', ( event ) => {
133 request.addEventListener( 'error', ( event ) => {
134 reject( 'Error saving data: ' + event.target.errorCode );
140 * Delete data relating to a specific page
143 * @param {string} pageName The current page name (with underscores)
144 * @param {string|null} section The section ID, or null if the whole page is being edited
145 * @return {jQuery.Promise} Promise which resolves on success, or rejects with an error message.
147 function deleteData( pageName, section ) {
148 return new Promise( ( resolve, reject ) => {
150 reject( 'DB not opened' );
153 const transaction = db.transaction( objectStoreName, 'readwrite' );
154 const objectStore = transaction.objectStore( objectStoreName );
156 const request = objectStore.delete( [ pageName, section || '' ] );
157 request.addEventListener( 'success', resolve );
158 request.addEventListener( 'error', () => {
159 reject( 'Error opening cursor' );
165 * Returns the date diff seconds in the future
168 * @param {number} diff Seconds in the future
169 * @return {number} Timestamp of diff days in the future
171 function getExpiryDate( diff ) {
172 return ( Date.now() / 1000 ) + diff;
176 * Delete expired data
179 * @return {jQuery.Promise} Promise which resolves on success, or rejects with an error message.
181 function deleteExpiredData() {
182 return new Promise( ( resolve, reject ) => {
184 reject( 'DB not opened' );
187 const transaction = db.transaction( objectStoreName, 'readwrite' );
188 const objectStore = transaction.objectStore( objectStoreName );
189 const expiry = objectStore.index( 'expiry' );
190 const now = Date.now() / 1000;
192 const expired = expiry.getAll( IDBKeyRange.upperBound( now, true ) );
194 expired.addEventListener( 'success', ( event ) => {
195 const cursors = event.target.result;
196 if ( cursors.length > 0 ) {
197 const deletions = [];
198 cursors.forEach( ( cursor ) => {
199 deletions.push( deleteData( cursor.pageName, cursor.section ) );
201 Promise.all( deletions ).then( resolve );
207 expired.addEventListener( 'error', () => {
208 reject( 'Error getting filtered data' );
218 function closeDatabase() {
225 openDatabase: openDatabaseLocal,