ChatStore.sys.mjs (23676B)
1 /* 2 This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 5 6 const lazy = {}; 7 8 ChromeUtils.defineESModuleGetters(lazy, { 9 Sqlite: "resource://gre/modules/Sqlite.sys.mjs", 10 }); 11 12 ChromeUtils.defineLazyGetter(lazy, "log", function () { 13 return console.createInstance({ 14 prefix: "ChatStore", 15 maxLogLevelPref: "browser.aiwindow.chatStore.loglevel", 16 }); 17 }); 18 19 import { 20 CONVERSATION_TABLE, 21 CONVERSATION_UPDATED_DATE_INDEX, 22 CONVERSATION_INSERT, 23 MESSAGE_TABLE, 24 MESSAGE_ORDINAL_INDEX, 25 MESSAGE_URL_INDEX, 26 MESSAGE_CREATED_DATE_INDEX, 27 MESSAGE_CONV_ID_INDEX, 28 MESSAGE_INSERT, 29 CONVERSATIONS_MOST_RECENT, 30 CONVERSATION_BY_ID, 31 CONVERSATIONS_BY_DATE, 32 CONVERSATIONS_BY_URL, 33 CONVERSATIONS_CONTENT_SEARCH, 34 CONVERSATIONS_CONTENT_SEARCH_BY_ROLE, 35 CONVERSATIONS_HISTORY_SEARCH, 36 MESSAGES_BY_DATE, 37 MESSAGES_BY_DATE_AND_ROLE, 38 DELETE_CONVERSATION_BY_ID, 39 CONVERSATIONS_OLDEST, 40 CONVERSATION_HISTORY, 41 ESCAPE_CHAR, 42 getConversationMessagesSql, 43 } from "./ChatSql.sys.mjs"; 44 45 import { ChatMinimal } from "./ChatMessage.sys.mjs"; 46 47 export { ChatConversation } from "./ChatConversation.sys.mjs"; 48 export { ChatMessage, ChatMinimal } from "./ChatMessage.sys.mjs"; 49 export { 50 CONVERSATION_STATUS, 51 MESSAGE_ROLE, 52 INSIGHTS_FLAG_SOURCE, 53 } from "./ChatConstants.sys.mjs"; 54 55 import { 56 CURRENT_SCHEMA_VERSION, 57 DB_FOLDER_PATH, 58 DB_FILE_NAME, 59 PREF_BRANCH, 60 CONVERSATION_STATUS, 61 } from "./ChatConstants.sys.mjs"; 62 63 import { 64 parseConversationRow, 65 parseMessageRows, 66 parseChatHistoryViewRows, 67 toJSONOrNull, 68 } from "./ChatUtils.sys.mjs"; 69 70 // NOTE: Reference to migrations file, migrations.mjs has an example 71 // migration function set up for a migration, and the eslint-disable-next-line 72 // should be removed once we create the first migration. 73 // 74 // eslint-disable-next-line no-unused-vars 75 import { migrations } from "./ChatMigrations.sys.mjs"; 76 77 const MAX_DB_SIZE_BYTES = 75 * 1024 * 1024; 78 const SORTS = ["ASC", "DESC"]; 79 80 /** 81 * Simple interface to store and retrieve chat conversations and messages. 82 * 83 * @todo Bug 2005409 84 * Move this documentation to Firefox source docs 85 * 86 * See: https://docs.google.com/document/d/1VlwmGbMhPIe-tmeKWinHuPh50VC9QrWEeQQ5V-UvEso/edit?tab=t.klqqibndv3zk 87 * 88 * @example 89 * let { ChatStore, ChatConversation, ChatMessage, MESSAGE_ROLE } = 90 * ChromeUtils.importESModule("resource:///modules/aiwindow/ui/modules/ChatStore.sys.mjs"); 91 * const chatStore = new ChatStore(); 92 * const conversation = new ChatConversation({ 93 * title: "title", 94 * description: "description", 95 * pageUrl: new URL("https://mozilla.com/"), 96 * pageMeta: { one: 1, two: 2 }, 97 * }); 98 * const msg1 = new ChatMessage({ 99 * ordinal: 0, 100 * role: MESSAGE_ROLE.USER, 101 * modelId: "test", 102 * params: { one: "one" }, 103 * usage: { two: "two", content: "some content" }, 104 * }); 105 * const msg2 = new ChatMessage({ 106 * ordinal: 1, 107 * role: MESSAGE_ROLE.ASSISTANT, 108 * modelId: "test", 109 * params: { one: "one" }, 110 * usage: { two: "two", content: "some content 2" }, 111 * }); 112 * conversation.messages = [msg1, msg2]; 113 * await chatStore.updateConversation(conversation); 114 * // Or findConversationsByDate, findConversationsByURL. 115 * const foundConversation = 116 * await chatStore.findConversationById(conversation.id); 117 * 118 * @typedef {object} ChatStore 119 * 120 * @property {*} x ? 121 */ 122 export class ChatStore { 123 #asyncShutdownBlocker; 124 #conn; 125 #promiseConn; 126 127 constructor() { 128 this.#asyncShutdownBlocker = async () => { 129 await this.#closeConnection(); 130 }; 131 } 132 133 /** 134 * Updates a conversation's saved state in the SQLite db 135 * 136 * @param {ChatConversation} conversation 137 */ 138 async updateConversation(conversation) { 139 await this.#ensureDatabase().catch(e => { 140 lazy.log.error("Could not ensure a database connection."); 141 throw e; 142 }); 143 144 const pageUrl = URL.parse(conversation.pageUrl); 145 146 await this.#conn 147 .executeTransaction(async () => { 148 await this.#conn.executeCached(CONVERSATION_INSERT, { 149 conv_id: conversation.id, 150 title: conversation.title, 151 description: conversation.description, 152 page_url: pageUrl?.href ?? null, 153 page_meta: toJSONOrNull(conversation.pageMeta), 154 created_date: conversation.createdDate, 155 updated_date: conversation.updatedDate, 156 status: conversation.status, 157 active_branch_tip_message_id: conversation.activeBranchTipMessageId, 158 }); 159 160 const messages = conversation.messages.map(m => ({ 161 message_id: m.id, 162 conv_id: conversation.id, 163 created_date: m.createdDate, 164 parent_message_id: m.parentMessageId, 165 revision_root_message_id: m.revisionRootMessageId, 166 ordinal: m.ordinal, 167 is_active_branch: m.isActiveBranch ? 1 : 0, 168 role: m.role, 169 model_id: m.modelId, 170 params: toJSONOrNull(m.params), 171 content: toJSONOrNull(m.content), 172 usage: toJSONOrNull(m.usage), 173 page_url: m.pageUrl?.href || "", 174 turn_index: m.turnIndex, 175 insights_enabled: m.insightsEnabled, 176 insights_flag_source: m.insightsFlagSource, 177 insights_applied_jsonb: toJSONOrNull(m.insightsApplied), 178 web_search_queries_jsonb: toJSONOrNull(m.webSearchQueries), 179 })); 180 await this.#conn.executeCached(MESSAGE_INSERT, messages); 181 }) 182 .catch(e => { 183 lazy.log.error("Transaction failed to execute"); 184 throw e; 185 }); 186 } 187 188 /** 189 * Gets a list of oldest conversations 190 * 191 * @param {number} numberOfConversations - How many conversations to retrieve 192 * @returns {Array<ChatMinimal>} - List of ChatMinimal items 193 */ 194 async findOldestConversations(numberOfConversations) { 195 await this.#ensureDatabase().catch(e => { 196 lazy.log.error("Could not ensure a database connection."); 197 throw e; 198 }); 199 200 const rows = await this.#conn 201 .executeCached(CONVERSATIONS_OLDEST, { 202 limit: numberOfConversations, 203 }) 204 .catch(e => { 205 lazy.log.error("Could not retrieve oldest conversations."); 206 throw e; 207 }); 208 209 return rows.map(row => { 210 return new ChatMinimal({ 211 convId: row.getResultByName("conv_id"), 212 title: row.getResultByName("title"), 213 }); 214 }); 215 } 216 217 /** 218 * Gets a list of most recent conversations 219 * 220 * @param {number} numberOfConversations - How many conversations to retrieve 221 * @returns {Array<ChatMinimal>} - List of ChatMinimal items 222 */ 223 async findRecentConversations(numberOfConversations) { 224 await this.#ensureDatabase().catch(e => { 225 lazy.log.error("Could not ensure a database connection."); 226 throw e; 227 }); 228 229 const rows = await this.#conn 230 .executeCached(CONVERSATIONS_MOST_RECENT, { 231 limit: numberOfConversations, 232 }) 233 .catch(e => { 234 lazy.log.error("Could not retrieve most recent conversations."); 235 throw e; 236 }); 237 238 return rows.map(row => { 239 return new ChatMinimal({ 240 convId: row.getResultByName("conv_id"), 241 title: row.getResultByName("title"), 242 }); 243 }); 244 } 245 246 /** 247 * Gets a Conversation using it's id 248 * 249 * @param {string} conversationId - The ID of the conversation to retrieve 250 * 251 * @returns {ChatConversation} - The conversation and its messages 252 */ 253 async findConversationById(conversationId) { 254 const conversations = await this.#findConversationsWithMessages( 255 CONVERSATION_BY_ID, 256 { 257 conv_id: conversationId, 258 } 259 ); 260 261 return conversations[0] ?? null; 262 } 263 264 /** 265 * Finds conversations between a specified start and end date 266 * 267 * @param {number} startDate - Start time epoch format 268 * @param {number} endDate - End time epoch format 269 * 270 * @returns {Array<ChatConversation>} - The conversations and their messages 271 */ 272 async findConversationsByDate(startDate, endDate) { 273 return this.#findConversationsWithMessages(CONVERSATIONS_BY_DATE, { 274 start_date: startDate, 275 end_date: endDate, 276 }); 277 } 278 279 /** 280 * Finds conversations between a specified start and end date 281 * 282 * @param {URL} pageUrl - The URL to find conversations for 283 * 284 * @returns {Array<ChatConversation>} - The conversations and their messages 285 */ 286 async findConversationsByURL(pageUrl) { 287 return this.#findConversationsWithMessages(CONVERSATIONS_BY_URL, { 288 page_url: pageUrl.href, 289 }); 290 } 291 292 /** 293 * Search for messages that happened between the specified start 294 * and end dates, optionally, filter the messages by a specific 295 * message role type. 296 * 297 * @param {Date} startDate - The start date, inclusive 298 * @param {Date} [endDate=new Date()] - The end date, inclusive 299 * @param {MessageRole} [role=-1] - The message role type to filter by, one of 0|1|2|3 300 * as defined by the constant MESSAGE_ROLE 301 * @param {number} [limit=-1] - The max number of messages to retrieve 302 * @param {number} [offset=-1] - The number or messages to skip from the result set 303 * 304 * @returns {Array<ChatMessage>} - An array of ChatMessage entries 305 */ 306 async findMessagesByDate( 307 startDate, 308 endDate = new Date(), 309 role = -1, 310 limit = -1, 311 offset = -1 312 ) { 313 const params = { 314 start_date: startDate.getTime(), 315 end_date: endDate.getTime(), 316 limit, 317 offset, 318 }; 319 320 let sql = MESSAGES_BY_DATE; 321 if (role > -1) { 322 sql = MESSAGES_BY_DATE_AND_ROLE; 323 params.role = role; 324 } 325 326 let rows = await this.#conn.executeCached(sql, params); 327 328 return parseMessageRows(rows); 329 } 330 331 #escapeForLike(searchString) { 332 return searchString 333 .replaceAll(ESCAPE_CHAR, `${ESCAPE_CHAR}${ESCAPE_CHAR}`) 334 .replaceAll("%", `${ESCAPE_CHAR}%`) 335 .replaceAll("_", `${ESCAPE_CHAR}_`); 336 } 337 338 /** 339 * Searches through the message.content JSON object to find a particular 340 * object path that contains a partial string match of a value. 341 * 342 * @param {string} keyChain - The object key chain to look through, 343 * like obj.field1.field2 344 * @param {MessageRole} [role=-1] - A message role to search for 345 * 346 * @returns {Array<ChatConversation>} - An array of conversations with messages 347 * that contain a message that matches the search string at the given content 348 * object path 349 */ 350 async searchContent(keyChain, role = -1) { 351 const path = `$.${keyChain}`; 352 353 const query = 354 role > -1 355 ? CONVERSATIONS_CONTENT_SEARCH_BY_ROLE 356 : CONVERSATIONS_CONTENT_SEARCH; 357 358 const params = { path }; 359 360 if (role > -1) { 361 params.role = role; 362 } 363 364 const rows = await this.#conn.executeCached(query, params); 365 366 if (!rows.length) { 367 return []; 368 } 369 370 const conversations = rows.map(parseConversationRow); 371 372 return await this.#getMessagesForConversations(conversations); 373 } 374 375 /** 376 * Searches for conversations where the conversation title, or the conversation 377 * contains a user message where the search string contains a partial match 378 * in the message.content.body field 379 * 380 * @param {string} searchString - The string to search with for conversations 381 * 382 * @returns {Array<ChatConversation>} - An array of conversations with messages 383 * that contain a message that matches the search string in the conversation 384 * titles 385 */ 386 async search(searchString) { 387 const path = `$.body`; 388 const pattern = `%${this.#escapeForLike(searchString)}%`; 389 390 const rows = await this.#conn.executeCached(CONVERSATIONS_HISTORY_SEARCH, { 391 path, 392 pattern, 393 }); 394 395 if (!rows.length) { 396 return []; 397 } 398 399 const conversations = rows.map(parseConversationRow); 400 401 return await this.#getMessagesForConversations(conversations); 402 } 403 404 /** 405 * Gets a list of chat history items to display in Chat History view. 406 * 407 * @param {number} [pageNumber=1] - The page number to get, 1 based indexing 408 * @param {number} [pageSize=20] - Number of items to get per page 409 * @param {string} [sort="desc"] - desc|asc The sorting order based on updated_date for conversations 410 */ 411 async chatHistoryView(pageNumber = 1, pageSize = 20, sort = "desc") { 412 const sorting = SORTS.find(item => item === sort.toUpperCase()) ?? "DESC"; 413 const offset = pageSize * (pageNumber - 1); 414 const limit = pageSize; 415 const params = { 416 limit, 417 offset, 418 }; 419 420 const rows = await this.#conn.executeCached( 421 CONVERSATION_HISTORY.replace("{sort}", sorting), 422 params 423 ); 424 425 return parseChatHistoryViewRows(rows); 426 } 427 428 /** 429 * Prunes the database of old conversations in order to get the 430 * database file size to the specified maximum size. 431 * 432 * @todo Bug 2005411 433 * Review the requirements for db pruning and set up invocation schedule, and refactor 434 * to use dbstat 435 * 436 * @param {number} [reduceByPercentage=0.05] - Percentage to reduce db file size by 437 * @param {number} [maxDbSizeBytes=MAX_DB_SIZE_BYTES] - Db max file size 438 */ 439 async pruneDatabase( 440 reduceByPercentage = 0.05, 441 maxDbSizeBytes = MAX_DB_SIZE_BYTES 442 ) { 443 if (!IOUtils.exists(this.databaseFilePath)) { 444 return; 445 } 446 447 const DELETE_BATCH_SIZE = 50; 448 449 const getPragmaInt = async name => { 450 const result = await this.#conn.execute(`PRAGMA ${name}`); 451 return result[0].getInt32(0); 452 }; 453 454 // compute the logical DB size in bytes using SQLite's page_size, 455 // page_count, and freelist_count 456 const getLogicalDbSizeBytes = async () => { 457 const pageSize = await getPragmaInt("page_size"); 458 const pageCount = await getPragmaInt("page_count"); 459 const freelistCount = await getPragmaInt("freelist_count"); 460 461 // Logical used pages = total pages - free pages 462 const usedPages = pageCount - freelistCount; 463 const lSize = usedPages * pageSize; 464 465 return lSize; 466 }; 467 468 let logicalSize = await getLogicalDbSizeBytes(); 469 if (logicalSize < maxDbSizeBytes) { 470 return; 471 } 472 473 const targetLogicalSize = Math.max( 474 0, 475 logicalSize * (1 - reduceByPercentage) 476 ); 477 478 const MAX_ITERATIONS = 100; 479 // how many "no file size change" batches we tolerate 480 const MAX_STAGNANT = 5; 481 let iterations = 0; 482 let stagnantIterations = 0; 483 484 while ( 485 logicalSize > targetLogicalSize && 486 iterations < MAX_ITERATIONS && 487 stagnantIterations < MAX_STAGNANT 488 ) { 489 iterations++; 490 491 const recentChats = await this.findOldestConversations(DELETE_BATCH_SIZE); 492 493 if (!recentChats.length) { 494 break; 495 } 496 497 for (const chat of recentChats) { 498 await this.deleteConversationById(chat.id); 499 } 500 501 const newLogicalSize = await getLogicalDbSizeBytes(); 502 if (newLogicalSize >= logicalSize) { 503 stagnantIterations++; 504 } else { 505 stagnantIterations = 0; 506 } 507 508 logicalSize = newLogicalSize; 509 } 510 511 // Actually reclaim disk space. 512 await this.#conn.execute("PRAGMA incremental_vacuum;"); 513 } 514 515 /** 516 * Returns the file size of the database. 517 * Establishes a connection first to make sure the 518 * database exists. 519 * 520 * @returns {number} - The file size in bytes 521 */ 522 async getDatabaseSize() { 523 await this.#ensureDatabase(); 524 525 const stats = await IOUtils.stat(this.databaseFilePath); 526 return stats.size; 527 } 528 529 /** 530 * Deletes a particular conversation using it's id 531 * 532 * @param {string} id - The conv_id of a conversation row to delete 533 */ 534 async deleteConversationById(id) { 535 await this.#ensureDatabase(); 536 537 await this.#conn.execute(DELETE_CONVERSATION_BY_ID, { 538 conv_id: id, 539 }); 540 } 541 542 /** 543 * This method is meant to only be used for testing cleanup 544 */ 545 async destroyDatabase() { 546 await this.#removeDatabaseFiles(); 547 } 548 549 /** 550 * Gets the version of the schema currently set in the database. 551 * 552 * @returns {number} 553 */ 554 async getDatabaseSchemaVersion() { 555 if (!this.#conn) { 556 await this.#ensureDatabase(); 557 } 558 559 return this.#conn.getSchemaVersion(); 560 } 561 562 async #getMessagesForConversations(conversations) { 563 const convs = conversations.reduce((convMap, conv) => { 564 convMap[conv.id] = conv; 565 566 return convMap; 567 }, {}); 568 569 // Find all the messages for all the conversations. 570 const rows = await this.#conn 571 .executeCached( 572 getConversationMessagesSql(conversations.length), 573 conversations.map(c => c.id) 574 ) 575 .catch(e => { 576 lazy.log.error("Could not retrieve messages for conversatons"); 577 lazy.log.error(`${e.message}\n${e.stack}`); 578 579 return []; 580 }); 581 582 // TODO: retrieve TTL content. 583 584 parseMessageRows(rows).forEach(message => { 585 const conversation = convs[message.convId]; 586 if (conversation) { 587 conversation.messages.push(message); 588 } 589 }); 590 591 return conversations; 592 } 593 594 async #openConnection() { 595 lazy.log.debug("Opening new connection"); 596 597 try { 598 const confConfig = { path: this.databaseFilePath }; 599 this.#conn = await lazy.Sqlite.openConnection(confConfig); 600 } catch (e) { 601 lazy.log.error("openConnection() could not open db:", e.message); 602 throw e; 603 } 604 605 lazy.Sqlite.shutdown.addBlocker( 606 "ChatStore: Shutdown", 607 this.#asyncShutdownBlocker 608 ); 609 610 try { 611 // TODO: remove this after switching pruneDatabase() to use dbstat 612 await this.#conn.execute("PRAGMA page_size = 4096;"); 613 // Setup WAL journaling, as it is generally faster. 614 await this.#conn.execute("PRAGMA journal_mode = WAL;"); 615 await this.#conn.execute("PRAGMA wal_autocheckpoint = 16;"); 616 617 // Store VACUUM information to be used by the VacuumManager. 618 await this.#conn.execute("PRAGMA auto_vacuum = INCREMENTAL;"); 619 await this.#conn.execute("PRAGMA foreign_keys = ON;"); 620 } catch (e) { 621 lazy.log.warn("Configuring SQLite PRAGMA settings: ", e.message); 622 } 623 } 624 625 async #closeConnection() { 626 if (!this.#conn) { 627 return; 628 } 629 630 lazy.log.debug("Closing connection"); 631 lazy.Sqlite.shutdown.removeBlocker(this.#asyncShutdownBlocker); 632 try { 633 await this.#conn.close(); 634 } catch (e) { 635 lazy.log.warn(`Error closing connection: ${e.message}`); 636 } 637 this.#conn = null; 638 } 639 640 /** 641 * @todo Bug 2005412 642 * Discuss implications of multiple instances of ChatStore 643 * and the potential issues with migrations/schemas. 644 */ 645 async #ensureDatabase() { 646 if (this.#promiseConn) { 647 return this.#promiseConn; 648 } 649 650 let deferred = Promise.withResolvers(); 651 this.#promiseConn = deferred.promise; 652 if (this.#removeDatabaseOnStartup) { 653 lazy.log.debug("Removing database on startup"); 654 try { 655 await this.#removeDatabaseFiles(); 656 } catch (e) { 657 deferred.reject(new Error("Could not remove the database files")); 658 return deferred.promise; 659 } 660 } 661 662 try { 663 await this.#openConnection(); 664 } catch (e) { 665 if ( 666 e.result == Cr.NS_ERROR_FILE_CORRUPTED || 667 e.errors?.some(error => error.result == Ci.mozIStorageError.NOTADB) 668 ) { 669 lazy.log.warn("Invalid database detected, removing it.", e); 670 await this.#removeDatabaseFiles(); 671 } 672 } 673 674 if (!this.#conn) { 675 try { 676 await this.#openConnection(); 677 } catch (e) { 678 lazy.log.error("Could not open the database connection.", e); 679 deferred.reject(new Error("Could not open the database connection")); 680 return deferred.promise; 681 } 682 } 683 684 try { 685 await this.#initializeSchema(); 686 } catch (e) { 687 lazy.log.warn( 688 "Failed to initialize the database schema, recreating the database.", 689 e 690 ); 691 // If the schema cannot be initialized try to create a new database file. 692 await this.#removeDatabaseFiles(); 693 } 694 695 deferred.resolve(this.#conn); 696 return this.#promiseConn; 697 } 698 699 async setSchemaVersion(version) { 700 await this.#conn.setSchemaVersion(version); 701 } 702 703 async #initializeSchema() { 704 const version = await this.getDatabaseSchemaVersion(); 705 706 if (version == this.CURRENT_SCHEMA_VERSION) { 707 return; 708 } 709 710 if (version > this.CURRENT_SCHEMA_VERSION) { 711 await this.setSchemaVersion(this.CURRENT_SCHEMA_VERSION); 712 return; 713 } 714 715 // Must migrate the schema. 716 await this.#conn.executeTransaction(async () => { 717 if (version == 0) { 718 // This is a newly created database, just create the entities. 719 await this.#createDatabaseEntities(); 720 await this.#conn.setSchemaVersion(this.CURRENT_SCHEMA_VERSION); 721 // eslint-disable-next-line no-useless-return 722 return; 723 } 724 725 await this.applyMigrations(); 726 await this.setSchemaVersion(this.CURRENT_SCHEMA_VERSION); 727 }); 728 } 729 730 async applyMigrations() { 731 for (const migration of migrations) { 732 if (typeof migration !== "function") { 733 continue; 734 } 735 736 await migration(this.#conn, this.CURRENT_SCHEMA_VERSION); 737 } 738 } 739 740 async #removeDatabaseFiles() { 741 lazy.log.debug("Removing database files"); 742 await this.#closeConnection(); 743 try { 744 for (let file of [ 745 this.databaseFilePath, 746 PathUtils.join(DB_FOLDER_PATH, this.databaseFileName + "-wal"), 747 PathUtils.join(DB_FOLDER_PATH, this.databaseFileName + "-shm"), 748 ]) { 749 lazy.log.debug(`Removing ${file}`); 750 await IOUtils.remove(file, { 751 retryReadonly: true, 752 recursive: true, 753 ignoreAbsent: true, 754 }); 755 } 756 this.#removeDatabaseOnStartup = false; 757 } catch (e) { 758 lazy.log.warn("Failed to remove database files", e); 759 // Try to clear on next startup. 760 this.#removeDatabaseOnStartup = true; 761 // Re-throw the exception for the caller. 762 throw e; 763 } 764 } 765 766 async #findConversationsWithMessages(sql, queryParams) { 767 await this.#ensureDatabase().catch(e => { 768 lazy.log.error("Could not ensure a database connection."); 769 lazy.log.error(`${e.message}\n${e.stack}`); 770 771 return []; 772 }); 773 774 // @todo Bug 2005414 775 // Check summary first, find the one with the largest end_ordinal. 776 // If not found retrieve all messages. 777 // If found compare end_ordinal of the summary with active branch ordinal 778 // to determine if extra messages must be retrieved. 779 let rows = await this.#conn.executeCached(sql, queryParams); 780 781 const conversations = rows.map(parseConversationRow); 782 783 return await this.#getMessagesForConversations(conversations); 784 } 785 786 async #createDatabaseEntities() { 787 await this.#conn.execute(CONVERSATION_TABLE); 788 await this.#conn.execute(CONVERSATION_UPDATED_DATE_INDEX); 789 await this.#conn.execute(MESSAGE_TABLE); 790 await this.#conn.execute(MESSAGE_ORDINAL_INDEX); 791 await this.#conn.execute(MESSAGE_URL_INDEX); 792 await this.#conn.execute(MESSAGE_CREATED_DATE_INDEX); 793 await this.#conn.execute(MESSAGE_CONV_ID_INDEX); 794 } 795 796 get #removeDatabaseOnStartup() { 797 return Services.prefs.getBoolPref( 798 `${PREF_BRANCH}.removeDatabaseOnStartup`, 799 false 800 ); 801 } 802 803 set #removeDatabaseOnStartup(value) { 804 lazy.log.debug(`Setting removeDatabaseOnStartup to ${value}`); 805 Services.prefs.setBoolPref(`${PREF_BRANCH}.removeDatabaseOnStartup`, value); 806 } 807 808 static get CONVERSATION_STATUS() { 809 return CONVERSATION_STATUS; 810 } 811 812 get CURRENT_SCHEMA_VERSION() { 813 return CURRENT_SCHEMA_VERSION; 814 } 815 816 get connection() { 817 return this.#conn; 818 } 819 820 get databaseFileName() { 821 return DB_FILE_NAME; 822 } 823 824 get databaseFilePath() { 825 return PathUtils.join(PathUtils.profileDir, this.databaseFileName); 826 } 827 }