You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

245 lines
7.7 KiB

11 months ago
  1. const { BackgroundService } = require("../utils/BackgroundWorkers");
  2. const prisma = require("../utils/prisma");
  3. const { SystemSettings } = require("./systemSettings");
  4. const { Telemetry } = require("./telemetry");
  5. /**
  6. * @typedef {('link'|'youtube'|'confluence'|'github'|'gitlab')} validFileType
  7. */
  8. const DocumentSyncQueue = {
  9. featureKey: "experimental_live_file_sync",
  10. // update the validFileTypes and .canWatch properties when adding elements here.
  11. validFileTypes: ["link", "youtube", "confluence", "github", "gitlab"],
  12. defaultStaleAfter: 604800000,
  13. maxRepeatFailures: 5, // How many times a run can fail in a row before pruning.
  14. writable: [],
  15. bootWorkers: function () {
  16. new BackgroundService().boot();
  17. },
  18. killWorkers: function () {
  19. new BackgroundService().stop();
  20. },
  21. /** Check is the Document Sync/Watch feature is enabled and can be used. */
  22. enabled: async function () {
  23. return (
  24. (await SystemSettings.get({ label: this.featureKey }))?.value ===
  25. "enabled"
  26. );
  27. },
  28. /**
  29. * @param {import("@prisma/client").document_sync_queues} queueRecord - queue record to calculate for
  30. */
  31. calcNextSync: function (queueRecord) {
  32. return new Date(Number(new Date()) + queueRecord.staleAfterMs);
  33. },
  34. /**
  35. * Check if the document can be watched based on the metadata fields
  36. * @param {object} metadata - metadata to check
  37. * @param {string} metadata.title - title of the document
  38. * @param {string} metadata.chunkSource - chunk source of the document
  39. * @returns {boolean} - true if the document can be watched, false otherwise
  40. */
  41. canWatch: function ({ title, chunkSource = null } = {}) {
  42. if (chunkSource.startsWith("link://") && title.endsWith(".html"))
  43. return true; // If is web-link material (prior to feature most chunkSources were links://)
  44. if (chunkSource.startsWith("youtube://")) return true; // If is a youtube link
  45. if (chunkSource.startsWith("confluence://")) return true; // If is a confluence document link
  46. if (chunkSource.startsWith("github://")) return true; // If is a GitHub file reference
  47. if (chunkSource.startsWith("gitlab://")) return true; // If is a GitLab file reference
  48. return false;
  49. },
  50. /**
  51. * Creates Queue record and updates document watch status to true on Document record
  52. * @param {import("@prisma/client").workspace_documents} document - document record to watch, must have `id`
  53. */
  54. watch: async function (document = null) {
  55. if (!document) return false;
  56. try {
  57. const { Document } = require("./documents");
  58. // Get all documents that are watched and share the same unique filename. If this value is
  59. // non-zero then we exit early so that we do not have duplicated watch queues for the same file
  60. // across many workspaces.
  61. const workspaceDocIds = (
  62. await Document.where({ filename: document.filename, watched: true })
  63. ).map((rec) => rec.id);
  64. const hasRecords =
  65. (await this.count({ workspaceDocId: { in: workspaceDocIds } })) > 0;
  66. if (hasRecords)
  67. throw new Error(
  68. `Cannot watch this document again - it already has a queue set.`
  69. );
  70. const queue = await prisma.document_sync_queues.create({
  71. data: {
  72. workspaceDocId: document.id,
  73. nextSyncAt: new Date(Number(new Date()) + this.defaultStaleAfter),
  74. },
  75. });
  76. await Document._updateAll(
  77. { filename: document.filename },
  78. { watched: true }
  79. );
  80. return queue || null;
  81. } catch (error) {
  82. console.error(error.message);
  83. return null;
  84. }
  85. },
  86. /**
  87. * Deletes Queue record and updates document watch status to false on Document record
  88. * @param {import("@prisma/client").workspace_documents} document - document record to unwatch, must have `id`
  89. */
  90. unwatch: async function (document = null) {
  91. if (!document) return false;
  92. try {
  93. const { Document } = require("./documents");
  94. // We could have been given a document to unwatch which is a clone of one that is already being watched but by another workspaceDocument id.
  95. // so in this instance we need to delete any queues related to this document by any WorkspaceDocumentId it is referenced by.
  96. const workspaceDocIds = (
  97. await Document.where({ filename: document.filename, watched: true })
  98. ).map((rec) => rec.id);
  99. await this.delete({ workspaceDocId: { in: workspaceDocIds } });
  100. await Document._updateAll(
  101. { filename: document.filename },
  102. { watched: false }
  103. );
  104. return true;
  105. } catch (error) {
  106. console.error(error.message);
  107. return false;
  108. }
  109. },
  110. _update: async function (id = null, data = {}) {
  111. if (!id) throw new Error("No id provided for update");
  112. try {
  113. await prisma.document_sync_queues.update({
  114. where: { id },
  115. data,
  116. });
  117. return true;
  118. } catch (error) {
  119. console.error(error.message);
  120. return false;
  121. }
  122. },
  123. get: async function (clause = {}) {
  124. try {
  125. const queue = await prisma.document_sync_queues.findFirst({
  126. where: clause,
  127. });
  128. return queue || null;
  129. } catch (error) {
  130. console.error(error.message);
  131. return null;
  132. }
  133. },
  134. where: async function (
  135. clause = {},
  136. limit = null,
  137. orderBy = null,
  138. include = {}
  139. ) {
  140. try {
  141. const results = await prisma.document_sync_queues.findMany({
  142. where: clause,
  143. ...(limit !== null ? { take: limit } : {}),
  144. ...(orderBy !== null ? { orderBy } : {}),
  145. ...(include !== null ? { include } : {}),
  146. });
  147. return results;
  148. } catch (error) {
  149. console.error(error.message);
  150. return [];
  151. }
  152. },
  153. count: async function (clause = {}, limit = null) {
  154. try {
  155. const count = await prisma.document_sync_queues.count({
  156. where: clause,
  157. ...(limit !== null ? { take: limit } : {}),
  158. });
  159. return count;
  160. } catch (error) {
  161. console.error("FAILED TO COUNT DOCUMENTS.", error.message);
  162. return 0;
  163. }
  164. },
  165. delete: async function (clause = {}) {
  166. try {
  167. await prisma.document_sync_queues.deleteMany({ where: clause });
  168. return true;
  169. } catch (error) {
  170. console.error(error.message);
  171. return false;
  172. }
  173. },
  174. /**
  175. * Gets the "stale" queues where the queue's nextSyncAt is less than the current time
  176. * @returns {Promise<(
  177. * import("@prisma/client").document_sync_queues &
  178. * { workspaceDoc: import("@prisma/client").workspace_documents &
  179. * { workspace: import("@prisma/client").workspaces }
  180. * })[]}>}
  181. */
  182. staleDocumentQueues: async function () {
  183. const queues = await this.where(
  184. {
  185. nextSyncAt: {
  186. lte: new Date().toISOString(),
  187. },
  188. },
  189. null,
  190. null,
  191. {
  192. workspaceDoc: {
  193. include: {
  194. workspace: true,
  195. },
  196. },
  197. }
  198. );
  199. return queues;
  200. },
  201. saveRun: async function (queueId = null, status = null, result = {}) {
  202. const { DocumentSyncRun } = require("./documentSyncRun");
  203. return DocumentSyncRun.save(queueId, status, result);
  204. },
  205. /**
  206. * Updates document to be watched/unwatched & creates or deletes any queue records and updated Document record `watched` status
  207. * @param {import("@prisma/client").workspace_documents} documentRecord
  208. * @param {boolean} watchStatus - indicate if queue record should be created or not.
  209. * @returns
  210. */
  211. toggleWatchStatus: async function (documentRecord, watchStatus = false) {
  212. if (!watchStatus) {
  213. await Telemetry.sendTelemetry("document_unwatched");
  214. await this.unwatch(documentRecord);
  215. return;
  216. }
  217. await this.watch(documentRecord);
  218. await Telemetry.sendTelemetry("document_watched");
  219. return;
  220. },
  221. };
  222. module.exports = { DocumentSyncQueue };