@dereekb/firebase-server 12.7.0 → 13.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/LICENSE +1 -1
  2. package/index.cjs.default.js +1 -0
  3. package/index.cjs.js +2859 -0
  4. package/index.cjs.mjs +2 -0
  5. package/index.esm.js +2699 -0
  6. package/mailgun/index.cjs.default.js +1 -0
  7. package/mailgun/{src/lib/auth.mailgun.js → index.cjs.js} +6 -6
  8. package/mailgun/index.cjs.mjs +2 -0
  9. package/mailgun/index.esm.js +39 -0
  10. package/mailgun/package.json +20 -5
  11. package/model/index.cjs.default.js +1 -0
  12. package/model/index.cjs.js +4767 -0
  13. package/model/index.cjs.mjs +2 -0
  14. package/model/index.d.ts +1 -0
  15. package/model/index.esm.js +4630 -0
  16. package/model/package.json +28 -4
  17. package/model/src/lib/notification/notification.module.d.ts +10 -10
  18. package/model/src/lib/storagefile/storagefile.task.service.handler.d.ts +3 -14
  19. package/package.json +49 -47
  20. package/src/lib/auth/auth.context.d.ts +1 -1
  21. package/src/lib/auth/auth.service.d.ts +1 -1
  22. package/src/lib/function/assert.d.ts +2 -2
  23. package/src/lib/function/context.d.ts +1 -1
  24. package/src/lib/function/error.d.ts +0 -8
  25. package/src/lib/index.d.ts +1 -0
  26. package/src/lib/nest/app.d.ts +1 -1
  27. package/src/lib/nest/development/development.app.function.d.ts +1 -2
  28. package/src/lib/nest/function/call.d.ts +1 -1
  29. package/src/lib/nest/function/context.d.ts +0 -4
  30. package/src/lib/nest/function/index.d.ts +0 -1
  31. package/src/lib/nest/function/nest.d.ts +1 -1
  32. package/src/lib/nest/function/v2/blocking.d.ts +3 -2
  33. package/src/lib/nest/model/crud.assert.function.d.ts +0 -4
  34. package/src/lib/storage/storage.d.ts +1 -1
  35. package/src/lib/type.d.ts +9 -0
  36. package/test/index.cjs.default.js +1 -0
  37. package/test/index.cjs.js +1258 -0
  38. package/test/index.cjs.mjs +2 -0
  39. package/test/index.d.ts +1 -0
  40. package/test/index.esm.js +1181 -0
  41. package/test/package.json +31 -4
  42. package/test/src/lib/firebase/firebase.admin.auth.d.ts +22 -13
  43. package/test/src/lib/firebase/firebase.admin.collection.d.ts +6 -6
  44. package/test/src/lib/firebase/firebase.admin.d.ts +10 -10
  45. package/test/src/lib/firebase/firebase.admin.function.d.ts +9 -9
  46. package/test/src/lib/firebase/firebase.admin.nest.d.ts +8 -8
  47. package/test/src/lib/firebase/firebase.admin.nest.function.d.ts +9 -9
  48. package/test/src/lib/firebase/firebase.test.d.ts +30 -0
  49. package/test/src/lib/firebase/index.d.ts +1 -1
  50. package/test/src/lib/firestore/firestore.admin.d.ts +3 -3
  51. package/test/src/lib/firestore/firestore.d.ts +2 -2
  52. package/test/src/lib/storage/storage.admin.d.ts +3 -3
  53. package/test/src/lib/storage/storage.d.ts +2 -2
  54. package/zoho/LICENSE +1 -1
  55. package/zoho/index.cjs.default.js +1 -0
  56. package/zoho/index.cjs.js +82 -85
  57. package/zoho/index.cjs.mjs +2 -0
  58. package/zoho/index.d.ts +1 -0
  59. package/zoho/index.esm.js +84 -87
  60. package/zoho/package.json +15 -15
  61. package/CHANGELOG.md +0 -2237
  62. package/mailgun/src/index.js +0 -5
  63. package/mailgun/src/index.js.map +0 -1
  64. package/mailgun/src/lib/auth.mailgun.js.map +0 -1
  65. package/mailgun/src/lib/index.js +0 -5
  66. package/mailgun/src/lib/index.js.map +0 -1
  67. package/model/src/index.js +0 -5
  68. package/model/src/index.js.map +0 -1
  69. package/model/src/lib/index.js +0 -7
  70. package/model/src/lib/index.js.map +0 -1
  71. package/model/src/lib/mailgun/index.js +0 -5
  72. package/model/src/lib/mailgun/index.js.map +0 -1
  73. package/model/src/lib/mailgun/notification.send.service.mailgun.js +0 -68
  74. package/model/src/lib/mailgun/notification.send.service.mailgun.js.map +0 -1
  75. package/model/src/lib/notification/index.js +0 -20
  76. package/model/src/lib/notification/index.js.map +0 -1
  77. package/model/src/lib/notification/notification.action.init.service.js +0 -230
  78. package/model/src/lib/notification/notification.action.init.service.js.map +0 -1
  79. package/model/src/lib/notification/notification.action.service.js +0 -1487
  80. package/model/src/lib/notification/notification.action.service.js.map +0 -1
  81. package/model/src/lib/notification/notification.config.js +0 -13
  82. package/model/src/lib/notification/notification.config.js.map +0 -1
  83. package/model/src/lib/notification/notification.config.service.js +0 -60
  84. package/model/src/lib/notification/notification.config.service.js.map +0 -1
  85. package/model/src/lib/notification/notification.create.run.js +0 -59
  86. package/model/src/lib/notification/notification.create.run.js.map +0 -1
  87. package/model/src/lib/notification/notification.error.js +0 -87
  88. package/model/src/lib/notification/notification.error.js.map +0 -1
  89. package/model/src/lib/notification/notification.expedite.service.js +0 -112
  90. package/model/src/lib/notification/notification.expedite.service.js.map +0 -1
  91. package/model/src/lib/notification/notification.module.js +0 -106
  92. package/model/src/lib/notification/notification.module.js.map +0 -1
  93. package/model/src/lib/notification/notification.send.js +0 -3
  94. package/model/src/lib/notification/notification.send.js.map +0 -1
  95. package/model/src/lib/notification/notification.send.service.js +0 -10
  96. package/model/src/lib/notification/notification.send.service.js.map +0 -1
  97. package/model/src/lib/notification/notification.send.service.notificationsummary.js +0 -104
  98. package/model/src/lib/notification/notification.send.service.notificationsummary.js.map +0 -1
  99. package/model/src/lib/notification/notification.send.service.text.js +0 -29
  100. package/model/src/lib/notification/notification.send.service.text.js.map +0 -1
  101. package/model/src/lib/notification/notification.task.service.handler.js +0 -65
  102. package/model/src/lib/notification/notification.task.service.handler.js.map +0 -1
  103. package/model/src/lib/notification/notification.task.service.js +0 -10
  104. package/model/src/lib/notification/notification.task.service.js.map +0 -1
  105. package/model/src/lib/notification/notification.task.service.util.js +0 -27
  106. package/model/src/lib/notification/notification.task.service.util.js.map +0 -1
  107. package/model/src/lib/notification/notification.task.subtask.handler.js +0 -256
  108. package/model/src/lib/notification/notification.task.subtask.handler.js.map +0 -1
  109. package/model/src/lib/notification/notification.util.js +0 -478
  110. package/model/src/lib/notification/notification.util.js.map +0 -1
  111. package/model/src/lib/storagefile/index.js +0 -12
  112. package/model/src/lib/storagefile/index.js.map +0 -1
  113. package/model/src/lib/storagefile/storagefile.action.init.service.js +0 -155
  114. package/model/src/lib/storagefile/storagefile.action.init.service.js.map +0 -1
  115. package/model/src/lib/storagefile/storagefile.action.server.js +0 -797
  116. package/model/src/lib/storagefile/storagefile.action.server.js.map +0 -1
  117. package/model/src/lib/storagefile/storagefile.error.js +0 -106
  118. package/model/src/lib/storagefile/storagefile.error.js.map +0 -1
  119. package/model/src/lib/storagefile/storagefile.module.js +0 -64
  120. package/model/src/lib/storagefile/storagefile.module.js.map +0 -1
  121. package/model/src/lib/storagefile/storagefile.task.service.handler.js +0 -287
  122. package/model/src/lib/storagefile/storagefile.task.service.handler.js.map +0 -1
  123. package/model/src/lib/storagefile/storagefile.upload.service.initializer.js +0 -180
  124. package/model/src/lib/storagefile/storagefile.upload.service.initializer.js.map +0 -1
  125. package/model/src/lib/storagefile/storagefile.upload.service.js +0 -10
  126. package/model/src/lib/storagefile/storagefile.upload.service.js.map +0 -1
  127. package/model/src/lib/storagefile/storagefile.util.js +0 -54
  128. package/model/src/lib/storagefile/storagefile.util.js.map +0 -1
  129. package/src/index.js +0 -5
  130. package/src/index.js.map +0 -1
  131. package/src/lib/auth/auth.context.js +0 -13
  132. package/src/lib/auth/auth.context.js.map +0 -1
  133. package/src/lib/auth/auth.service.error.js +0 -34
  134. package/src/lib/auth/auth.service.error.js.map +0 -1
  135. package/src/lib/auth/auth.service.js +0 -427
  136. package/src/lib/auth/auth.service.js.map +0 -1
  137. package/src/lib/auth/auth.util.js +0 -23
  138. package/src/lib/auth/auth.util.js.map +0 -1
  139. package/src/lib/auth/index.js +0 -8
  140. package/src/lib/auth/index.js.map +0 -1
  141. package/src/lib/env/env.service.js +0 -7
  142. package/src/lib/env/env.service.js.map +0 -1
  143. package/src/lib/env/index.js +0 -5
  144. package/src/lib/env/index.js.map +0 -1
  145. package/src/lib/firestore/array.js +0 -34
  146. package/src/lib/firestore/array.js.map +0 -1
  147. package/src/lib/firestore/driver.accessor.batch.js +0 -93
  148. package/src/lib/firestore/driver.accessor.batch.js.map +0 -1
  149. package/src/lib/firestore/driver.accessor.default.js +0 -62
  150. package/src/lib/firestore/driver.accessor.default.js.map +0 -1
  151. package/src/lib/firestore/driver.accessor.js +0 -50
  152. package/src/lib/firestore/driver.accessor.js.map +0 -1
  153. package/src/lib/firestore/driver.accessor.transaction.js +0 -96
  154. package/src/lib/firestore/driver.accessor.transaction.js.map +0 -1
  155. package/src/lib/firestore/driver.js +0 -14
  156. package/src/lib/firestore/driver.js.map +0 -1
  157. package/src/lib/firestore/driver.query.js +0 -55
  158. package/src/lib/firestore/driver.query.js.map +0 -1
  159. package/src/lib/firestore/firestore.js +0 -10
  160. package/src/lib/firestore/firestore.js.map +0 -1
  161. package/src/lib/firestore/increment.js +0 -17
  162. package/src/lib/firestore/increment.js.map +0 -1
  163. package/src/lib/firestore/index.js +0 -9
  164. package/src/lib/firestore/index.js.map +0 -1
  165. package/src/lib/function/assert.js +0 -68
  166. package/src/lib/function/assert.js.map +0 -1
  167. package/src/lib/function/context.js +0 -14
  168. package/src/lib/function/context.js.map +0 -1
  169. package/src/lib/function/error.auth.js +0 -25
  170. package/src/lib/function/error.auth.js.map +0 -1
  171. package/src/lib/function/error.js +0 -221
  172. package/src/lib/function/error.js.map +0 -1
  173. package/src/lib/function/index.js +0 -9
  174. package/src/lib/function/index.js.map +0 -1
  175. package/src/lib/function/type.js +0 -3
  176. package/src/lib/function/type.js.map +0 -1
  177. package/src/lib/index.js +0 -11
  178. package/src/lib/index.js.map +0 -1
  179. package/src/lib/nest/app.js +0 -114
  180. package/src/lib/nest/app.js.map +0 -1
  181. package/src/lib/nest/auth/auth.module.js +0 -60
  182. package/src/lib/nest/auth/auth.module.js.map +0 -1
  183. package/src/lib/nest/auth/auth.util.js +0 -72
  184. package/src/lib/nest/auth/auth.util.js.map +0 -1
  185. package/src/lib/nest/auth/index.js +0 -6
  186. package/src/lib/nest/auth/index.js.map +0 -1
  187. package/src/lib/nest/development/development.app.function.js +0 -38
  188. package/src/lib/nest/development/development.app.function.js.map +0 -1
  189. package/src/lib/nest/development/development.assert.function.js +0 -3
  190. package/src/lib/nest/development/development.assert.function.js.map +0 -1
  191. package/src/lib/nest/development/development.function.js +0 -41
  192. package/src/lib/nest/development/development.function.js.map +0 -1
  193. package/src/lib/nest/development/development.schedule.function.error.js +0 -35
  194. package/src/lib/nest/development/development.schedule.function.error.js.map +0 -1
  195. package/src/lib/nest/development/development.schedule.function.js +0 -54
  196. package/src/lib/nest/development/development.schedule.function.js.map +0 -1
  197. package/src/lib/nest/development/index.js +0 -9
  198. package/src/lib/nest/development/index.js.map +0 -1
  199. package/src/lib/nest/env/env.service.js +0 -19
  200. package/src/lib/nest/env/env.service.js.map +0 -1
  201. package/src/lib/nest/env/env.util.js +0 -12
  202. package/src/lib/nest/env/env.util.js.map +0 -1
  203. package/src/lib/nest/env/index.js +0 -6
  204. package/src/lib/nest/env/index.js.map +0 -1
  205. package/src/lib/nest/firebase/firebase.module.js +0 -17
  206. package/src/lib/nest/firebase/firebase.module.js.map +0 -1
  207. package/src/lib/nest/firebase/index.js +0 -5
  208. package/src/lib/nest/firebase/index.js.map +0 -1
  209. package/src/lib/nest/firestore/firestore.module.js +0 -86
  210. package/src/lib/nest/firestore/firestore.module.js.map +0 -1
  211. package/src/lib/nest/firestore/index.js +0 -5
  212. package/src/lib/nest/firestore/index.js.map +0 -1
  213. package/src/lib/nest/function/call.js +0 -46
  214. package/src/lib/nest/function/call.js.map +0 -1
  215. package/src/lib/nest/function/context.js +0 -79
  216. package/src/lib/nest/function/context.js.map +0 -1
  217. package/src/lib/nest/function/index.js +0 -10
  218. package/src/lib/nest/function/index.js.map +0 -1
  219. package/src/lib/nest/function/nest.js +0 -17
  220. package/src/lib/nest/function/nest.js.map +0 -1
  221. package/src/lib/nest/function/schedule.js +0 -8
  222. package/src/lib/nest/function/schedule.js.map +0 -1
  223. package/src/lib/nest/function/v1/call.d.ts +0 -59
  224. package/src/lib/nest/function/v1/call.js +0 -55
  225. package/src/lib/nest/function/v1/call.js.map +0 -1
  226. package/src/lib/nest/function/v1/event.d.ts +0 -80
  227. package/src/lib/nest/function/v1/event.js +0 -52
  228. package/src/lib/nest/function/v1/event.js.map +0 -1
  229. package/src/lib/nest/function/v1/index.d.ts +0 -3
  230. package/src/lib/nest/function/v1/index.js +0 -7
  231. package/src/lib/nest/function/v1/index.js.map +0 -1
  232. package/src/lib/nest/function/v1/schedule.d.ts +0 -47
  233. package/src/lib/nest/function/v1/schedule.js +0 -68
  234. package/src/lib/nest/function/v1/schedule.js.map +0 -1
  235. package/src/lib/nest/function/v2/blocking.js +0 -38
  236. package/src/lib/nest/function/v2/blocking.js.map +0 -1
  237. package/src/lib/nest/function/v2/call.js +0 -31
  238. package/src/lib/nest/function/v2/call.js.map +0 -1
  239. package/src/lib/nest/function/v2/event.js +0 -25
  240. package/src/lib/nest/function/v2/event.js.map +0 -1
  241. package/src/lib/nest/function/v2/index.js +0 -9
  242. package/src/lib/nest/function/v2/index.js.map +0 -1
  243. package/src/lib/nest/function/v2/schedule.js +0 -56
  244. package/src/lib/nest/function/v2/schedule.js.map +0 -1
  245. package/src/lib/nest/function/v2/taskqueue.js +0 -26
  246. package/src/lib/nest/function/v2/taskqueue.js.map +0 -1
  247. package/src/lib/nest/index.js +0 -15
  248. package/src/lib/nest/index.js.map +0 -1
  249. package/src/lib/nest/middleware/appcheck.decorator.js +0 -12
  250. package/src/lib/nest/middleware/appcheck.decorator.js.map +0 -1
  251. package/src/lib/nest/middleware/appcheck.js +0 -3
  252. package/src/lib/nest/middleware/appcheck.js.map +0 -1
  253. package/src/lib/nest/middleware/appcheck.middleware.js +0 -74
  254. package/src/lib/nest/middleware/appcheck.middleware.js.map +0 -1
  255. package/src/lib/nest/middleware/appcheck.module.js +0 -21
  256. package/src/lib/nest/middleware/appcheck.module.js.map +0 -1
  257. package/src/lib/nest/middleware/globalprefix.js +0 -11
  258. package/src/lib/nest/middleware/globalprefix.js.map +0 -1
  259. package/src/lib/nest/middleware/index.js +0 -10
  260. package/src/lib/nest/middleware/index.js.map +0 -1
  261. package/src/lib/nest/middleware/rawbody.middleware.js +0 -16
  262. package/src/lib/nest/middleware/rawbody.middleware.js.map +0 -1
  263. package/src/lib/nest/middleware/webhook.js +0 -24
  264. package/src/lib/nest/middleware/webhook.js.map +0 -1
  265. package/src/lib/nest/model/call.model.function.js +0 -73
  266. package/src/lib/nest/model/call.model.function.js.map +0 -1
  267. package/src/lib/nest/model/create.model.function.js +0 -27
  268. package/src/lib/nest/model/create.model.function.js.map +0 -1
  269. package/src/lib/nest/model/crud.assert.function.js +0 -3
  270. package/src/lib/nest/model/crud.assert.function.js.map +0 -1
  271. package/src/lib/nest/model/delete.model.function.js +0 -27
  272. package/src/lib/nest/model/delete.model.function.js.map +0 -1
  273. package/src/lib/nest/model/index.js +0 -11
  274. package/src/lib/nest/model/index.js.map +0 -1
  275. package/src/lib/nest/model/permission.error.js +0 -24
  276. package/src/lib/nest/model/permission.error.js.map +0 -1
  277. package/src/lib/nest/model/read.model.function.js +0 -27
  278. package/src/lib/nest/model/read.model.function.js.map +0 -1
  279. package/src/lib/nest/model/specifier.function.js +0 -35
  280. package/src/lib/nest/model/specifier.function.js.map +0 -1
  281. package/src/lib/nest/model/update.model.function.js +0 -27
  282. package/src/lib/nest/model/update.model.function.js.map +0 -1
  283. package/src/lib/nest/nest.provider.js +0 -89
  284. package/src/lib/nest/nest.provider.js.map +0 -1
  285. package/src/lib/nest/storage/index.js +0 -5
  286. package/src/lib/nest/storage/index.js.map +0 -1
  287. package/src/lib/nest/storage/storage.module.js +0 -112
  288. package/src/lib/nest/storage/storage.module.js.map +0 -1
  289. package/src/lib/storage/driver.accessor.js +0 -299
  290. package/src/lib/storage/driver.accessor.js.map +0 -1
  291. package/src/lib/storage/driver.js +0 -12
  292. package/src/lib/storage/driver.js.map +0 -1
  293. package/src/lib/storage/index.js +0 -8
  294. package/src/lib/storage/index.js.map +0 -1
  295. package/src/lib/storage/storage.js +0 -20
  296. package/src/lib/storage/storage.js.map +0 -1
  297. package/src/lib/storage/storage.service.js +0 -26
  298. package/src/lib/storage/storage.service.js.map +0 -1
  299. package/test/src/index.js +0 -5
  300. package/test/src/index.js.map +0 -1
  301. package/test/src/lib/firebase/firebase.admin.auth.js +0 -260
  302. package/test/src/lib/firebase/firebase.admin.auth.js.map +0 -1
  303. package/test/src/lib/firebase/firebase.admin.collection.js +0 -108
  304. package/test/src/lib/firebase/firebase.admin.collection.js.map +0 -1
  305. package/test/src/lib/firebase/firebase.admin.function.js +0 -132
  306. package/test/src/lib/firebase/firebase.admin.function.js.map +0 -1
  307. package/test/src/lib/firebase/firebase.admin.js +0 -174
  308. package/test/src/lib/firebase/firebase.admin.js.map +0 -1
  309. package/test/src/lib/firebase/firebase.admin.nest.function.callable.context.js +0 -42
  310. package/test/src/lib/firebase/firebase.admin.nest.function.callable.context.js.map +0 -1
  311. package/test/src/lib/firebase/firebase.admin.nest.function.cloud.context.js +0 -40
  312. package/test/src/lib/firebase/firebase.admin.nest.function.cloud.context.js.map +0 -1
  313. package/test/src/lib/firebase/firebase.admin.nest.function.js +0 -64
  314. package/test/src/lib/firebase/firebase.admin.nest.function.js.map +0 -1
  315. package/test/src/lib/firebase/firebase.admin.nest.js +0 -107
  316. package/test/src/lib/firebase/firebase.admin.nest.js.map +0 -1
  317. package/test/src/lib/firebase/firebase.admin.test.server.js +0 -37
  318. package/test/src/lib/firebase/firebase.admin.test.server.js.map +0 -1
  319. package/test/src/lib/firebase/firebase.function.js +0 -58
  320. package/test/src/lib/firebase/firebase.function.js.map +0 -1
  321. package/test/src/lib/firebase/firebase.jest.d.ts +0 -21
  322. package/test/src/lib/firebase/firebase.jest.js +0 -45
  323. package/test/src/lib/firebase/firebase.jest.js.map +0 -1
  324. package/test/src/lib/firebase/firebase.js +0 -74
  325. package/test/src/lib/firebase/firebase.js.map +0 -1
  326. package/test/src/lib/firebase/index.js +0 -15
  327. package/test/src/lib/firebase/index.js.map +0 -1
  328. package/test/src/lib/firestore/firestore.admin.js +0 -21
  329. package/test/src/lib/firestore/firestore.admin.js.map +0 -1
  330. package/test/src/lib/firestore/firestore.js +0 -57
  331. package/test/src/lib/firestore/firestore.js.map +0 -1
  332. package/test/src/lib/firestore/index.js +0 -6
  333. package/test/src/lib/firestore/index.js.map +0 -1
  334. package/test/src/lib/index.js +0 -7
  335. package/test/src/lib/index.js.map +0 -1
  336. package/test/src/lib/storage/index.js +0 -6
  337. package/test/src/lib/storage/index.js.map +0 -1
  338. package/test/src/lib/storage/storage.admin.js +0 -21
  339. package/test/src/lib/storage/storage.admin.js.map +0 -1
  340. package/test/src/lib/storage/storage.js +0 -59
  341. package/test/src/lib/storage/storage.js.map +0 -1
  342. /package/{zoho/index.cjs.d.ts → index.d.ts} +0 -0
  343. /package/{zoho/index.esm.d.ts → mailgun/index.d.ts} +0 -0
@@ -0,0 +1,4630 @@
1
+ import { mapObjectKeysToLowercase, multiValueMapBuilder, batch, runAsyncTasksForValues, asArray, pushArrayItemsIntoArray, UNSET_INDEX_NUMBER, makeValuesGroupMap, performAsyncTasks, separateValues, filterMaybeArrayValues, takeFront, makeModelMap, removeValuesAtIndexesFromArrayCopy, areEqualPOJOValues, asPromise, mapIdentityFunction, computeNextFreeIndexOnSortedValuesFunction, readIndexNumber, filterOnlyUndefinedValues, iterablesAreSetEquivalent, dateOrMillisecondsToDate, cutStringFunction, takeLast, unique, isThrottled, isPast, expirationDetails, unixDateTimeSecondsNumberFromDate, slashPathDetails, mergeSlashPaths, ModelRelationUtility, MS_IN_MINUTE, dateFromDateOrTimeMillisecondsNumber, cachedGetter, ZIP_FILE_MIME_TYPE, documentFileExtensionForMimeType, useCallback, MS_IN_HOUR, MAP_IDENTITY, pushItemOrArrayItemsIntoArray, asDecisionFunction } from '@dereekb/util';
2
+ import { yearWeekCode, findMaxDate, isSameDate, findMinDate } from '@dereekb/date';
3
+ import { CREATE_NOTIFICATION_ID_REQUIRED_ERROR_CODE, NOTIFICATION_BOX_DOES_NOT_EXIST_ERROR_CODE, NOTIFICATION_BOX_EXCLUSION_TARGET_INVALID_ERROR_CODE, NOTIFICATION_BOX_EXISTS_FOR_MODEL_ERROR_CODE, NOTIFICATION_BOX_RECIPIENT_DOES_NOT_EXIST_ERROR_CODE, NOTIFICATION_MODEL_ALREADY_INITIALIZED_ERROR_CODE, NOTIFICATION_USER_BLOCKED_FROM_BEING_ADD_TO_RECIPIENTS_ERROR_CODE, NOTIFICATION_USER_INVALID_UID_FOR_CREATE_ERROR_CODE, NOTIFICATION_USER_LOCKED_CONFIG_FROM_BEING_UPDATED_ERROR_CODE, DEFAULT_NOTIFICATION_TEMPLATE_TYPE, NotificationRecipientSendFlag, allowedNotificationRecipients, effectiveNotificationBoxRecipientTemplateConfig, loadDocumentsForIds, getDocumentSnapshotDataPairsWithData, notificationSendExclusionCanSendFunction, mergeNotificationUserDefaultNotificationBoxRecipientConfig, mergeNotificationBoxRecipientTemplateConfigs, firestoreDummyKey, mergeNotificationBoxRecipients, mergeNotificationUserNotificationBoxRecipientConfigs, applyExclusionsToNotificationUserNotificationBoxRecipientConfigs, CleanupSentNotificationsParams, notificationsReadyForCleanupQuery, getDocumentSnapshotDataPairs, NotificationSendType, loadDocumentsForDocumentReferencesFromValues, shouldSaveNotificationToNotificationWeek, CreateNotificationBoxParams, loadNotificationBoxDocumentForReferencePair, CreateNotificationSummaryParams, notificationSummaryIdForModel, CreateNotificationUserParams, iterateFirestoreDocumentSnapshotPairs, notificationUsersFlaggedForNeedsSyncQuery, ResyncNotificationUserParams, effectiveNotificationBoxRecipientConfig, SendNotificationParams, getDocumentSnapshotData, inferKeyFromTwoWayFlatFirestoreModelKey, setIdAndKeyFromKeyIdRefOnDocumentData, NotificationSendState, notificationSendFlagsImplyIsComplete, SendQueuedNotificationsParams, mergeNotificationSendMessagesResult, UpdateNotificationBoxParams, updateNotificationUserNotificationSendExclusions, UpdateNotificationBoxRecipientParams, notificationBoxRecipientTemplateConfigArrayToRecord, updateNotificationRecipient, UpdateNotificationSummaryParams, UpdateNotificationUserParams, updateNotificationUserDefaultNotificationBoxRecipientConfig, updateNotificationUserNotificationBoxRecipientConfigs, calculateNsForNotificationUserNotificationBoxRecipientConfigs, notificationsPastSendAtTimeQuery, InitializeAllApplicableNotificationBoxesParams, InitializeAllApplicableNotificationSummariesParams, InitializeNotificationModelParams, firestoreModelKeyCollectionName, notificationBoxesFlaggedForNeedsInitializationQuery, notificationSummariesFlaggedForNeedsInitializationQuery, noContentNotificationMessageFunctionFactory, createNotificationDocumentPair, _createNotificationDocumentFromPair, NOTIFICATION_SUMMARY_EMBEDDED_NOTIFICATION_ITEM_SUBJECT_MAX_LENGTH, NOTIFICATION_SUMMARY_EMBEDDED_NOTIFICATION_ITEM_MESSAGE_MAX_LENGTH, sortNotificationItemsFunction, NOTIFICATION_SUMMARY_ITEM_LIMIT, delayCompletion, completeSubtaskProcessingAndScheduleCleanupTaskResult, NOTIFICATION_TASK_SUBTASK_CHECKPOINT_PROCESSING, NOTIFICATION_TASK_SUBTASK_CHECKPOINT_CLEANUP, notificationTaskComplete, DEFAULT_NOTIFICATION_TASK_SUBTASK_CLEANUP_RETRY_ATTEMPTS, notificationTaskDelayRetry, DEFAULT_NOTIFICATION_TASK_SUBTASK_CLEANUP_RETRY_DELAY, STORAGE_FILE_GROUP_CREATE_INPUT_ERROR_CODE, STORAGE_FILE_ALREADY_PROCESSED_ERROR_CODE, STORAGE_FILE_CANNOT_BE_DELETED_YET_ERROR_CODE, STORAGE_FILE_GROUP_QUEUED_FOR_INITIALIZATION_ERROR_CODE, STORAGE_FILE_MODEL_ALREADY_INITIALIZED_ERROR_CODE, STORAGE_FILE_NOT_FLAGGED_FOR_DELETION_ERROR_CODE, STORAGE_FILE_NOT_FLAGGED_FOR_GROUPS_SYNC_ERROR_CODE, STORAGE_FILE_PROCESSING_NOT_ALLOWED_FOR_INVALID_STATE_ERROR_CODE, STORAGE_FILE_PROCESSING_NOT_AVAILABLE_FOR_TYPE_ERROR_CODE, STORAGE_FILE_PROCESSING_NOT_QUEUED_FOR_PROCESSING_ERROR_CODE, UPLOADED_FILE_DOES_NOT_EXIST_ERROR_CODE, UPLOADED_FILE_INITIALIZATION_DISCARDED_ERROR_CODE, UPLOADED_FILE_INITIALIZATION_FAILED_ERROR_CODE, UPLOADED_FILE_NOT_ALLOWED_TO_BE_INITIALIZED_ERROR_CODE, StorageFileProcessingState, STORAGE_FILE_PROCESSING_STUCK_THROTTLE_CHECK_MS, calculateStorageFileGroupEmbeddedFileUpdate, CreateStorageFileParams, CreateStorageFileGroupParams, loadStorageFileGroupDocumentForReferencePair, DeleteAllQueuedStorageFilesParams, storageFilesQueuedForDeleteQuery, DeleteStorageFileParams, DownloadStorageFileParams, InitializeAllStorageFilesFromUploadsParams, UPLOADS_FOLDER_PATH, iterateStorageListFilesByEachFile, InitializeStorageFileFromUploadParams, ProcessAllQueuedStorageFilesParams, storageFilesQueuedForProcessingQuery, ProcessStorageFileParams, RegenerateAllFlaggedStorageFileGroupsContentParams, storageFileGroupsFlaggedForContentRegenerationQuery, RegenerateStorageFileGroupContentParams, createStorageFileDocumentPairFactory, StorageFileCreationType, getDocumentSnapshotDataPair, calculateStorageFileGroupRegeneration, storageFileGroupZipFileStoragePath, STORAGE_FILE_GROUP_ZIP_STORAGE_FILE_PURPOSE, SyncAllFlaggedStorageFilesWithGroupsParams, iterateFirestoreDocumentSnapshotPairBatches, storageFileFlaggedForSyncWithGroupsQuery, SyncStorageFileWithGroupsParams, UpdateStorageFileParams, UpdateStorageFileGroupParams, StorageFileState, createNotificationDocument, storageFileProcessingNotificationTaskTemplate, InitializeAllApplicableStorageFileGroupsParams, InitializeStorageFileModelParams, storageFileGroupsFlaggedForNeedsInitializationQuery, STORAGE_FILE_GROUP_ZIP_STORAGE_FILE_PURPOSE_CREATE_ZIP_SUBTASK, storedFileReaderFactory, STORAGE_FILE_PROCESSING_NOTIFICATION_TASK_TYPE, copyStoragePath, STORAGE_FILE_GROUP_ZIP_INFO_JSON_FILE_NAME, notificationSubtaskComplete, limitUploadFileTypeDeterminer, combineUploadFileTypeDeterminers, STORAGEFILE_RELATED_FILE_METADATA_KEY, storageFilePurposeAndUserQuery } from '@dereekb/firebase';
4
+ import { preconditionConflictError, assertSnapshotData, internalServerError } from '@dereekb/firebase-server';
5
+ import { hoursToMilliseconds, isFuture, addMinutes, addHours, addSeconds, addDays } from 'date-fns';
6
+ import { Optional, Inject, Injectable, Module, Global } from '@nestjs/common';
7
+ import { ConfigModule } from '@nestjs/config';
8
+ import { BaseError } from 'make-error';
9
+ import archiver from 'archiver';
10
+
11
+ const MAILGUN_NOTIFICATION_EMAIL_SEND_SERVICE_DEFAULT_MAX_BATCH_SIZE_PER_REQUEST = 50;
12
+ function mailgunNotificationEmailSendService(config) {
13
+ const { mailgunService, defaultSendTemplateName, maxBatchSizePerRequest: inputMaxBatchSizePerRequest, messageBuilders: inputMessageBuilders } = config;
14
+ const lowercaseKeysMessageBuilders = mapObjectKeysToLowercase(inputMessageBuilders);
15
+ const maxBatchSizePerRequest = inputMaxBatchSizePerRequest ?? MAILGUN_NOTIFICATION_EMAIL_SEND_SERVICE_DEFAULT_MAX_BATCH_SIZE_PER_REQUEST;
16
+ const sendService = {
17
+ async buildSendInstanceForEmailNotificationMessages(notificationMessages) {
18
+ const templateMap = multiValueMapBuilder();
19
+ // group by templates
20
+ notificationMessages.forEach((x) => {
21
+ const sendTemplateName = x.emailContent?.sendTemplateName ?? x.content.sendTemplateName ?? defaultSendTemplateName;
22
+ if (sendTemplateName == null) {
23
+ throw new Error(`mailgunNotificationEmailSendService(): A sendTemplateName for a message was not available and no default was provided. Consider configuring a default send template.`);
24
+ }
25
+ templateMap.add(sendTemplateName, x);
26
+ });
27
+ // build send batches
28
+ const messageSendBatches = templateMap.entries().flatMap(([templateType, messages]) => {
29
+ return batch(messages, maxBatchSizePerRequest).map((x) => [templateType, x]);
30
+ });
31
+ // create the template requests
32
+ const templateRequestArrays = await Promise.all(messageSendBatches.map(async ([sendTemplateName, messages]) => {
33
+ const sendTemplateNameToLowercase = sendTemplateName.toLowerCase();
34
+ const builderForKey = lowercaseKeysMessageBuilders[sendTemplateNameToLowercase];
35
+ if (!builderForKey) {
36
+ throw new Error(`mailgunNotificationEmailSendService(): A template builder was not available for template type "${sendTemplateName}".`);
37
+ }
38
+ else {
39
+ const input = { mailgunService, sendTemplateName, messages };
40
+ return builderForKey(input);
41
+ }
42
+ }));
43
+ const templateRequests = templateRequestArrays.flat();
44
+ const sendFn = async () => {
45
+ const success = [];
46
+ const failed = [];
47
+ // send the template emails
48
+ await runAsyncTasksForValues(templateRequests, (x) => {
49
+ const recipients = asArray(x.to).map((z) => z.email);
50
+ return mailgunService
51
+ .sendTemplateEmail(x)
52
+ .then(() => {
53
+ pushArrayItemsIntoArray(success, recipients);
54
+ })
55
+ .catch((e) => {
56
+ pushArrayItemsIntoArray(failed, recipients);
57
+ console.error('mailgunNotificationEmailSendService(): failed sending template emails', e);
58
+ // suppress error
59
+ });
60
+ }, { maxParallelTasks: 3 });
61
+ const result = {
62
+ success,
63
+ failed,
64
+ ignored: []
65
+ };
66
+ return result;
67
+ };
68
+ return sendFn;
69
+ }
70
+ };
71
+ return sendService;
72
+ }
73
+
74
+ function createNotificationIdRequiredError() {
75
+ return preconditionConflictError({
76
+ message: `The required id was not present when attempting to create a Notification.`,
77
+ code: CREATE_NOTIFICATION_ID_REQUIRED_ERROR_CODE
78
+ });
79
+ }
80
+ function notificationModelAlreadyInitializedError() {
81
+ return preconditionConflictError({
82
+ message: `This model has already been initialized.`,
83
+ code: NOTIFICATION_MODEL_ALREADY_INITIALIZED_ERROR_CODE
84
+ });
85
+ }
86
+ function notificationBoxUnregistredModelTypeInitializationError(key) {
87
+ return preconditionConflictError({
88
+ message: `This NotificationBox is associated with an unregistered model type.`,
89
+ code: NOTIFICATION_MODEL_ALREADY_INITIALIZED_ERROR_CODE,
90
+ data: {
91
+ key
92
+ }
93
+ });
94
+ }
95
+ function notificationBoxDoesNotExist() {
96
+ return preconditionConflictError({
97
+ message: `A NotificationBox does not exist for this model.`,
98
+ code: NOTIFICATION_BOX_DOES_NOT_EXIST_ERROR_CODE
99
+ });
100
+ }
101
+ function notificationBoxExclusionTargetInvalidError() {
102
+ return preconditionConflictError({
103
+ message: `The target for exclusion is invalid. The target recipient on the NotificationBox must be exist on the NotificationBox and have a uid to be excluded.`,
104
+ code: NOTIFICATION_BOX_EXCLUSION_TARGET_INVALID_ERROR_CODE
105
+ });
106
+ }
107
+ function notificationBoxExistsForModelError() {
108
+ return preconditionConflictError({
109
+ message: `A NotificationBox already exists for this model.`,
110
+ code: NOTIFICATION_BOX_EXISTS_FOR_MODEL_ERROR_CODE
111
+ });
112
+ }
113
+ function notificationBoxRecipientDoesNotExistsError() {
114
+ return preconditionConflictError({
115
+ message: `An existing NotificationBox recipient for the target does not exist. You must pass insert=true to create a new recipient.`,
116
+ code: NOTIFICATION_BOX_RECIPIENT_DOES_NOT_EXIST_ERROR_CODE
117
+ });
118
+ }
119
+ function notificationUserInvalidUidForCreateError(uid) {
120
+ return preconditionConflictError({
121
+ message: `The user with the uid '${uid}' does not exist. Cannot create a NotificationUser for them.`,
122
+ code: NOTIFICATION_USER_INVALID_UID_FOR_CREATE_ERROR_CODE,
123
+ data: {
124
+ uid
125
+ }
126
+ });
127
+ }
128
+ function notificationUserBlockedFromBeingAddedToRecipientsError(uid) {
129
+ return preconditionConflictError({
130
+ message: `The user with the uid '${uid}' has blocked themselves from from being added recipients.`,
131
+ code: NOTIFICATION_USER_BLOCKED_FROM_BEING_ADD_TO_RECIPIENTS_ERROR_CODE,
132
+ data: {
133
+ uid
134
+ }
135
+ });
136
+ }
137
+ function notificationUserLockedConfigFromBeingUpdatedError(uid) {
138
+ return preconditionConflictError({
139
+ message: `The user with the uid '${uid}' has locked their config from being updated.`,
140
+ code: NOTIFICATION_USER_LOCKED_CONFIG_FROM_BEING_UPDATED_ERROR_CODE,
141
+ data: {
142
+ uid
143
+ }
144
+ });
145
+ }
146
+
147
+ // MARK: Create NotificationSummary
148
+ function makeNewNotificationSummaryTemplate(model) {
149
+ return {
150
+ cat: new Date(),
151
+ m: model,
152
+ o: firestoreDummyKey(),
153
+ s: true,
154
+ n: []
155
+ };
156
+ }
157
+ /**
158
+ * "Expands" the input into recipients for emails, texts, etc.
159
+ *
160
+ * Recipients may come from the NotificationBox, Notification or from the global recipients.
161
+ *
162
+ * Recipients are each configurable and may be defined with as little info as a single contact info, or have multiple contact info pieces associated with them.
163
+ *
164
+ * @param input
165
+ * @returns
166
+ */
167
+ async function expandNotificationRecipients(input) {
168
+ const { notificationUserAccessor, authService, notification, notificationBox, globalRecipients: inputGlobalRecipients, recipientFlagOverride, notificationSummaryIdForUid: inputNotificationSummaryIdForUid, onlySendToExplicitlyEnabledRecipients: inputOnlySendToExplicitlyEnabledRecipients, onlyTextExplicitlyEnabledRecipients: inputOnlyTextExplicitlyEnabledRecipients } = input;
169
+ const notificationBoxId = notificationBox?.id;
170
+ const notificationSummaryIdForUid = inputNotificationSummaryIdForUid ?? (() => undefined);
171
+ const notificationTemplateType = notification.n.t || DEFAULT_NOTIFICATION_TEMPLATE_TYPE;
172
+ const recipientFlag = recipientFlagOverride ?? notification.rf ?? NotificationRecipientSendFlag.NORMAL;
173
+ const onlyTextExplicitlyEnabledRecipients = inputOnlyTextExplicitlyEnabledRecipients !== false; // defaults to true
174
+ const onlySendToExplicitlyEnabledRecipients = inputOnlySendToExplicitlyEnabledRecipients === true; // defaults to false
175
+ const onlyEmailExplicitlyEnabledRecipients = onlySendToExplicitlyEnabledRecipients;
176
+ const onlySendNotificationSummaryExplicitlyEnabledRecipients = onlySendToExplicitlyEnabledRecipients;
177
+ const { canSendToGlobalRecipients, canSendToBoxRecipients, canSendToExplicitRecipients } = allowedNotificationRecipients(recipientFlag);
178
+ const initialExplicitRecipients = canSendToExplicitRecipients ? notification.r : [];
179
+ const initialGlobalRecipients = canSendToGlobalRecipients && inputGlobalRecipients ? inputGlobalRecipients : [];
180
+ const explicitRecipients = initialExplicitRecipients.map((x) => ({
181
+ ...x,
182
+ ...effectiveNotificationBoxRecipientTemplateConfig(x)
183
+ }));
184
+ const globalRecipients = initialGlobalRecipients.map((x) => ({
185
+ ...x,
186
+ ...effectiveNotificationBoxRecipientTemplateConfig(x)
187
+ }));
188
+ const explicitAndGlobalRecipients = [...explicitRecipients, ...globalRecipients];
189
+ const allBoxRecipientConfigs = canSendToBoxRecipients && notificationBox ? notificationBox.r : [];
190
+ const recipientUids = new Set();
191
+ const relevantBoxRecipientConfigs = [];
192
+ // find all recipients in the NotificationBox with the target template type flagged for them.
193
+ allBoxRecipientConfigs.forEach((x) => {
194
+ // ignore opt-out flagged recipients and excluded recipients
195
+ if (!x.f && !x.x) {
196
+ const relevantConfig = x.c[notificationTemplateType];
197
+ const effectiveTemplateConfig = relevantConfig ? effectiveNotificationBoxRecipientTemplateConfig(relevantConfig) : undefined;
198
+ if (!effectiveTemplateConfig || effectiveTemplateConfig.st || effectiveTemplateConfig.se || effectiveTemplateConfig.sp || effectiveTemplateConfig.st) {
199
+ relevantBoxRecipientConfigs.push({
200
+ recipient: x,
201
+ effectiveTemplateConfig
202
+ });
203
+ if (x.uid) {
204
+ recipientUids.add(x.uid);
205
+ }
206
+ }
207
+ }
208
+ });
209
+ // add other recipients to the map
210
+ const nonNotificationBoxUidRecipientConfigs = new Map();
211
+ explicitAndGlobalRecipients.forEach((x) => {
212
+ const { uid } = x;
213
+ if (uid && !recipientUids.has(uid)) {
214
+ // if already in recipientUids then they are a box recipient and we don't have to try and load them.
215
+ nonNotificationBoxUidRecipientConfigs.set(uid, x);
216
+ }
217
+ });
218
+ const otherNotificationUserUidOptOuts = new Set();
219
+ const otherNotificationUserUidSendExclusions = new Set();
220
+ const notificationUserRecipientConfigs = new Map();
221
+ if (nonNotificationBoxUidRecipientConfigs.size > 0) {
222
+ const nonNotificationBoxRecipientUids = Array.from(nonNotificationBoxUidRecipientConfigs.keys());
223
+ const notificationUserDocuments = loadDocumentsForIds(notificationUserAccessor, nonNotificationBoxRecipientUids);
224
+ // Attempt to load the NotificationUser for each uid.
225
+ // Not guranteed to exist, but those that do we want to their configurations to decide opt-in/opt-out, as well as override the input recipient configuration for the Notification.
226
+ const notificationUsers = await getDocumentSnapshotDataPairsWithData(notificationUserDocuments);
227
+ notificationUsers.forEach((x) => {
228
+ const { data: notificationUser } = x;
229
+ const { x: exclusions, dc, gc } = notificationUser;
230
+ const canSendNotification = notificationSendExclusionCanSendFunction(exclusions);
231
+ const effectiveConfig = mergeNotificationUserDefaultNotificationBoxRecipientConfig(dc, gc);
232
+ const uid = x.document.id;
233
+ notificationUserRecipientConfigs.set(uid, effectiveConfig);
234
+ // check if flagged for opt out on the global/default config
235
+ if (effectiveConfig.f) {
236
+ // if flagged for opt out, add to set
237
+ otherNotificationUserUidOptOuts.add(uid);
238
+ }
239
+ const isAllowedToSend = notificationBoxId ? canSendNotification(notificationBoxId) : true;
240
+ if (!isAllowedToSend) {
241
+ otherNotificationUserUidSendExclusions.add(uid);
242
+ }
243
+ });
244
+ }
245
+ /**
246
+ * Other NotificationRecipientWithConfig
247
+ */
248
+ const otherRecipientConfigs = new Map();
249
+ const explicitOtherRecipientEmailAddresses = new Map();
250
+ const explicitOtherRecipientTextNumbers = new Map();
251
+ const explicitOtherRecipientNotificationSummaryIds = new Map();
252
+ explicitAndGlobalRecipients.forEach((x) => {
253
+ const uid = x.uid;
254
+ if (uid) {
255
+ if (otherNotificationUserUidOptOuts.has(uid) || otherNotificationUserUidSendExclusions.has(uid)) {
256
+ return; // do not add to the recipients at all, user has opted out or send is excluded
257
+ }
258
+ const notificationUserRecipientConfig = notificationUserRecipientConfigs.get(uid);
259
+ if (notificationUserRecipientConfig != null) {
260
+ const userTemplateTypeConfig = notificationUserRecipientConfig.c[notificationTemplateType] ?? {};
261
+ const templateConfig = mergeNotificationBoxRecipientTemplateConfigs(effectiveNotificationBoxRecipientTemplateConfig(userTemplateTypeConfig), x);
262
+ // replace the input NotificationRecipientWithConfig with the user's config
263
+ x = {
264
+ ...notificationUserRecipientConfig,
265
+ ...effectiveNotificationBoxRecipientTemplateConfig(templateConfig),
266
+ uid
267
+ };
268
+ }
269
+ recipientUids.add(uid);
270
+ otherRecipientConfigs.set(uid, x);
271
+ }
272
+ if (x.e) {
273
+ explicitOtherRecipientEmailAddresses.set(x.e.toLowerCase(), x);
274
+ }
275
+ if (x.t) {
276
+ explicitOtherRecipientTextNumbers.set(x.t, x);
277
+ }
278
+ if (x.s) {
279
+ explicitOtherRecipientNotificationSummaryIds.set(x.s, x);
280
+ }
281
+ });
282
+ // load user details from auth service
283
+ const allUserDetails = await Promise.all(Array.from(recipientUids).map((uid) => authService
284
+ .userContext(uid)
285
+ .loadDetails()
286
+ .then((details) => [uid, details])
287
+ .catch(() => [uid, undefined])));
288
+ const userDetailsMap = new Map(allUserDetails);
289
+ const _internal = {
290
+ userDetailsMap,
291
+ explicitRecipients,
292
+ globalRecipients,
293
+ allBoxRecipientConfigs,
294
+ relevantBoxRecipientConfigs,
295
+ recipientUids,
296
+ otherRecipientConfigs,
297
+ explicitOtherRecipientEmailAddresses,
298
+ explicitOtherRecipientTextNumbers,
299
+ explicitOtherRecipientNotificationSummaryIds,
300
+ otherNotificationUserUidOptOuts,
301
+ otherNotificationUserUidSendExclusions,
302
+ nonNotificationBoxUidRecipientConfigs,
303
+ notificationUserRecipientConfigs
304
+ };
305
+ // make all email recipients
306
+ const emails = [];
307
+ const emailUidsSet = new Set();
308
+ function checkShouldSendEmail(sendEmailEnabled) {
309
+ return (!onlyEmailExplicitlyEnabledRecipients && sendEmailEnabled !== false) || (onlyEmailExplicitlyEnabledRecipients && sendEmailEnabled === true);
310
+ }
311
+ // start with all box recipients
312
+ relevantBoxRecipientConfigs.forEach((x) => {
313
+ const { recipient } = x;
314
+ const { uid, e: overrideRecipientEmail, n: overrideRecipientName } = recipient;
315
+ const userDetails = uid ? userDetailsMap.get(uid) : undefined;
316
+ const otherRecipientForUser = uid ? otherRecipientConfigs.get(uid) : undefined;
317
+ const sendEmailEnabled = x.effectiveTemplateConfig?.se;
318
+ const shouldSendEmail = checkShouldSendEmail(sendEmailEnabled);
319
+ if (shouldSendEmail && !emailUidsSet.has(uid ?? '')) {
320
+ const e = overrideRecipientEmail ?? userDetails?.email; // use override email or the default email
321
+ if (e) {
322
+ const n = overrideRecipientName ?? userDetails?.displayName;
323
+ const emailAddress = e.toLowerCase();
324
+ explicitOtherRecipientEmailAddresses.delete(emailAddress); // don't double-send to the same email
325
+ const emailRecipient = {
326
+ emailAddress,
327
+ name: n,
328
+ boxRecipient: x,
329
+ otherRecipient: otherRecipientForUser
330
+ };
331
+ emails.push(emailRecipient);
332
+ if (uid) {
333
+ emailUidsSet.add(uid);
334
+ }
335
+ }
336
+ }
337
+ });
338
+ otherRecipientConfigs.forEach((x, uid) => {
339
+ // add users who existing in the system at this step, then other recipients in the next step
340
+ const userDetails = userDetailsMap.get(uid);
341
+ if (userDetails) {
342
+ const { email: userEmailAddress, displayName } = userDetails;
343
+ const sendEmailEnabled = x.se;
344
+ const shouldSendEmail = checkShouldSendEmail(sendEmailEnabled);
345
+ if (userEmailAddress && shouldSendEmail && !emailUidsSet.has(uid)) {
346
+ const emailAddress = userEmailAddress.toLowerCase();
347
+ const name = displayName || x.n;
348
+ const emailRecipient = {
349
+ emailAddress,
350
+ name,
351
+ otherRecipient: x
352
+ };
353
+ emails.push(emailRecipient);
354
+ emailUidsSet.add(uid);
355
+ explicitOtherRecipientEmailAddresses.delete(emailAddress);
356
+ }
357
+ }
358
+ });
359
+ explicitOtherRecipientEmailAddresses.forEach((x, emailAddress) => {
360
+ const sendEmailEnabled = x.se;
361
+ const shouldSendEmail = checkShouldSendEmail(sendEmailEnabled);
362
+ if (shouldSendEmail) {
363
+ const emailRecipient = {
364
+ emailAddress: emailAddress,
365
+ name: x.n,
366
+ otherRecipient: x
367
+ };
368
+ emails.push(emailRecipient);
369
+ }
370
+ });
371
+ // make all text recipients
372
+ // text recipients should be explicitly enabled, or marked true
373
+ const texts = [];
374
+ const textUidsSet = new Set();
375
+ function checkShouldSendText(sendTextEnabled) {
376
+ return (onlyTextExplicitlyEnabledRecipients && sendTextEnabled === true) || (!onlyTextExplicitlyEnabledRecipients && sendTextEnabled !== false);
377
+ }
378
+ relevantBoxRecipientConfigs.forEach((x) => {
379
+ const { recipient } = x;
380
+ const { uid } = recipient;
381
+ const userDetails = uid ? userDetailsMap.get(uid) : undefined;
382
+ const otherRecipientForUser = uid ? otherRecipientConfigs.get(uid) : undefined;
383
+ // only send a text if explicitly enabled
384
+ const sendTextEnabled = x.effectiveTemplateConfig?.st;
385
+ const shouldSendText = checkShouldSendText(sendTextEnabled);
386
+ if (shouldSendText && !textUidsSet.has(uid ?? '')) {
387
+ const t = x.recipient.t ?? userDetails?.phoneNumber; // use override phoneNumber or the default phone
388
+ if (t) {
389
+ const name = userDetails?.displayName ?? x.recipient.n;
390
+ const phoneNumber = t;
391
+ explicitOtherRecipientTextNumbers.delete(phoneNumber); // don't double-send to the same text phone number
392
+ const textRecipient = {
393
+ phoneNumber,
394
+ name,
395
+ boxRecipient: x,
396
+ otherRecipient: otherRecipientForUser
397
+ };
398
+ texts.push(textRecipient);
399
+ if (uid) {
400
+ textUidsSet.add(uid);
401
+ }
402
+ }
403
+ }
404
+ });
405
+ otherRecipientConfigs.forEach((x, uid) => {
406
+ // add users who existing in the system at this step, then other recipients in the next step
407
+ const userDetails = userDetailsMap.get(uid);
408
+ if (userDetails) {
409
+ const { phoneNumber, displayName } = userDetails;
410
+ const sendTextEnabled = x.st;
411
+ const sendText = checkShouldSendText(sendTextEnabled);
412
+ if (phoneNumber != null && sendText && !textUidsSet.has(uid)) {
413
+ const name = displayName || x.n;
414
+ const textRecipient = {
415
+ phoneNumber: phoneNumber,
416
+ name,
417
+ otherRecipient: x
418
+ };
419
+ texts.push(textRecipient);
420
+ textUidsSet.add(uid);
421
+ explicitOtherRecipientTextNumbers.delete(phoneNumber); // don't double-send to the same text phone number
422
+ }
423
+ }
424
+ });
425
+ explicitOtherRecipientTextNumbers.forEach((x, t) => {
426
+ const sendTextEnabled = x.st;
427
+ const shouldSendText = checkShouldSendText(sendTextEnabled);
428
+ if (shouldSendText) {
429
+ const textRecipient = {
430
+ phoneNumber: t,
431
+ name: x.n,
432
+ otherRecipient: x
433
+ };
434
+ texts.push(textRecipient);
435
+ }
436
+ });
437
+ // TODO: Add push notification details...
438
+ // make all notification summary recipients
439
+ const notificationSummaries = [];
440
+ const notificationSummaryKeysSet = new Set();
441
+ const notificationSummaryUidsSet = new Set();
442
+ function checkShouldSendNotificationSummary(sendNotificationSummaryEnabled) {
443
+ return (!onlySendNotificationSummaryExplicitlyEnabledRecipients && sendNotificationSummaryEnabled !== false) || (onlySendNotificationSummaryExplicitlyEnabledRecipients && sendNotificationSummaryEnabled === true);
444
+ }
445
+ relevantBoxRecipientConfigs.forEach((x) => {
446
+ const { recipient } = x;
447
+ const { uid } = recipient;
448
+ const userDetails = uid ? userDetailsMap.get(uid) : undefined;
449
+ const otherRecipientForUser = uid ? otherRecipientConfigs.get(uid) : undefined;
450
+ const sendNotificationSummaryEnabled = x.effectiveTemplateConfig?.sn;
451
+ const shouldSendNotificationSummary = checkShouldSendNotificationSummary(sendNotificationSummaryEnabled);
452
+ if (shouldSendNotificationSummary) {
453
+ let notificationSummaryId;
454
+ if (uid) {
455
+ // only use the uid (and ignore recipient config) if uid is defined
456
+ notificationSummaryId = notificationSummaryIdForUid(uid);
457
+ notificationSummaryUidsSet.add(uid);
458
+ }
459
+ else if (x.recipient.s) {
460
+ notificationSummaryId = x.recipient.s;
461
+ }
462
+ if (notificationSummaryId) {
463
+ const name = userDetails?.displayName ?? x.recipient.n;
464
+ notificationSummaries.push({
465
+ notificationSummaryId,
466
+ boxRecipient: x,
467
+ otherRecipient: otherRecipientForUser,
468
+ name
469
+ });
470
+ explicitOtherRecipientNotificationSummaryIds.delete(notificationSummaryId); // don't double send
471
+ }
472
+ }
473
+ });
474
+ otherRecipientConfigs.forEach((x, uid) => {
475
+ const userDetails = userDetailsMap.get(uid);
476
+ if (userDetails) {
477
+ const { displayName } = userDetails;
478
+ const sendNotificationSummaryEnabled = x.sn;
479
+ const shouldSendNotificationSummary = checkShouldSendNotificationSummary(sendNotificationSummaryEnabled);
480
+ if (shouldSendNotificationSummary && !notificationSummaryUidsSet.has(uid ?? '')) {
481
+ let notificationSummaryId;
482
+ if (uid) {
483
+ notificationSummaryId = notificationSummaryIdForUid(uid);
484
+ notificationSummaryUidsSet.add(uid);
485
+ }
486
+ else if (x.s) {
487
+ notificationSummaryId = x.s;
488
+ }
489
+ if (notificationSummaryId) {
490
+ if (!notificationSummaryKeysSet.has(notificationSummaryId)) {
491
+ const name = displayName || x.n;
492
+ const notificationSummary = {
493
+ notificationSummaryId,
494
+ otherRecipient: x,
495
+ name
496
+ };
497
+ notificationSummaries.push(notificationSummary);
498
+ explicitOtherRecipientNotificationSummaryIds.delete(notificationSummaryId);
499
+ }
500
+ }
501
+ }
502
+ }
503
+ });
504
+ explicitOtherRecipientNotificationSummaryIds.forEach((x, notificationSummaryId) => {
505
+ const sendNotificationSummaryEnabled = x.sn;
506
+ const shouldSendNotificationSummary = checkShouldSendNotificationSummary(sendNotificationSummaryEnabled);
507
+ if (shouldSendNotificationSummary) {
508
+ const notificationSummary = {
509
+ notificationSummaryId,
510
+ otherRecipient: x,
511
+ name: x.n
512
+ };
513
+ notificationSummaries.push(notificationSummary);
514
+ }
515
+ });
516
+ // results
517
+ const result = {
518
+ _internal,
519
+ emails,
520
+ texts,
521
+ notificationSummaries
522
+ };
523
+ return result;
524
+ }
525
+ function updateNotificationUserNotificationBoxRecipientConfig(input) {
526
+ const { notificationBoxId, notificationUserId, notificationUser, insertingRecipientIntoNotificationBox, removeRecipientFromNotificationBox, notificationBoxRecipient } = input;
527
+ const currentNotificationUserBoxIndex = notificationUser.bc.findIndex((x) => x.nb === notificationBoxId);
528
+ const currentNotificationUserBoxIndexExists = currentNotificationUserBoxIndex !== -1;
529
+ const currentNotificationUserBoxGlobalConfig = notificationUser.gc;
530
+ const currentNotificationUserBoxConfig = notificationUser.bc[currentNotificationUserBoxIndex] ?? {};
531
+ /**
532
+ * If bc is updated then the user should be updated too
533
+ */
534
+ let updatedBc;
535
+ let updatedNotificationBoxRecipient;
536
+ if (removeRecipientFromNotificationBox) {
537
+ // flag as removed in the NotificationUser details if not already flagged as such
538
+ if (currentNotificationUserBoxIndexExists && currentNotificationUserBoxConfig.rm !== true) {
539
+ updatedBc = [...notificationUser.bc];
540
+ updatedBc[currentNotificationUserBoxIndex] = {
541
+ ...currentNotificationUserBoxConfig,
542
+ nb: notificationBoxId, // set the NotificationBox id
543
+ c: currentNotificationUserBoxConfig.c ?? {},
544
+ i: UNSET_INDEX_NUMBER, // index should be cleared and set to -1
545
+ ns: false, // sync'd
546
+ rm: true
547
+ };
548
+ }
549
+ }
550
+ else if (notificationBoxRecipient != null) {
551
+ const { ns: currentConfigNeedsSync, lk: lockedFromChanges, bk: blockedFromAdd } = {
552
+ ns: currentNotificationUserBoxConfig.ns,
553
+ lk: currentNotificationUserBoxGlobalConfig.lk ?? currentNotificationUserBoxConfig.lk,
554
+ bk: currentNotificationUserBoxGlobalConfig.bk ?? currentNotificationUserBoxConfig.bk
555
+ };
556
+ // if we're re-inserting, then take the prevous config and restore as it was and remove the rm tag
557
+ let updateWithNotificationBoxRecipient;
558
+ if (insertingRecipientIntoNotificationBox) {
559
+ // does not exist in the NotificationBox currently
560
+ if (blockedFromAdd) {
561
+ throw notificationUserBlockedFromBeingAddedToRecipientsError(notificationUserId);
562
+ }
563
+ else if (lockedFromChanges) {
564
+ // ignored the notificationBoxRecipient's updates
565
+ updateWithNotificationBoxRecipient = currentNotificationUserBoxConfig;
566
+ }
567
+ else {
568
+ updateWithNotificationBoxRecipient = mergeNotificationBoxRecipients(notificationBoxRecipient, currentNotificationUserBoxConfig);
569
+ }
570
+ }
571
+ else {
572
+ // if locked from changes, throw error
573
+ if (lockedFromChanges) {
574
+ throw notificationUserLockedConfigFromBeingUpdatedError(notificationUserId);
575
+ }
576
+ else if (currentConfigNeedsSync) {
577
+ // if needs sync, then merge changes from the config into the notificationBoxRecipient
578
+ updateWithNotificationBoxRecipient = mergeNotificationBoxRecipients(notificationBoxRecipient, currentNotificationUserBoxConfig);
579
+ }
580
+ else {
581
+ // use as-is
582
+ updateWithNotificationBoxRecipient = notificationBoxRecipient;
583
+ }
584
+ }
585
+ const updatedNotificationUserBoxEntry = mergeNotificationUserNotificationBoxRecipientConfigs({
586
+ ...currentNotificationUserBoxConfig,
587
+ i: notificationBoxRecipient.i,
588
+ c: currentNotificationUserBoxConfig.c ?? {},
589
+ nb: notificationBoxId, // set the NotificationBox id
590
+ rm: false // remove/clear the removed flag
591
+ }, updateWithNotificationBoxRecipient);
592
+ updatedBc = [...notificationUser.bc];
593
+ if (currentNotificationUserBoxIndexExists) {
594
+ updatedBc[currentNotificationUserBoxIndex] = updatedNotificationUserBoxEntry;
595
+ }
596
+ else {
597
+ updatedBc.push(updatedNotificationUserBoxEntry);
598
+ }
599
+ // re-apply exclusions to the updated config(s)
600
+ const withExclusions = applyExclusionsToNotificationUserNotificationBoxRecipientConfigs({
601
+ notificationUser,
602
+ bc: updatedBc,
603
+ recalculateNs: false
604
+ });
605
+ updatedBc = withExclusions.bc;
606
+ // sync index with input NotificationBoxRecipient
607
+ updatedNotificationUserBoxEntry.i = notificationBoxRecipient.i;
608
+ updatedNotificationBoxRecipient = updatedNotificationUserBoxEntry;
609
+ }
610
+ return {
611
+ updatedBc,
612
+ updatedNotificationBoxRecipient
613
+ };
614
+ }
615
+
616
+ /**
617
+ * Removes the completed checkpoints from the inputCompletions array based on the handleTaskResult.
618
+ *
619
+ * @param inputCompletions
620
+ * @param handleTaskResult
621
+ * @returns
622
+ */
623
+ function removeFromCompletionsArrayWithTaskResult(inputCompletions, handleTaskResult) {
624
+ const { removeAllCompletedCheckpoints, removeFromCompletedCheckpoints } = handleTaskResult;
625
+ let result;
626
+ if (removeAllCompletedCheckpoints) {
627
+ result = [];
628
+ }
629
+ else if (removeFromCompletedCheckpoints != null) {
630
+ const removeFromCompletionsSet = new Set(asArray(removeFromCompletedCheckpoints));
631
+ result = inputCompletions.filter((x) => !removeFromCompletionsSet.has(x));
632
+ }
633
+ else {
634
+ result = inputCompletions;
635
+ }
636
+ return result;
637
+ }
638
+
639
+ /**
640
+ * Injection token for the BaseNotificationServerActionsContext
641
+ */
642
+ const BASE_NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN = 'BASE_NOTIFICATION_SERVER_ACTION_CONTEXT';
643
+ /**
644
+ * Injection token for the NotificationServerActionsContext
645
+ */
646
+ const NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN = 'NOTIFICATION_SERVER_ACTION_CONTEXT';
647
+ class NotificationServerActions {
648
+ }
649
+ function notificationServerActions(context) {
650
+ return {
651
+ createNotificationUser: createNotificationUserFactory(context),
652
+ updateNotificationUser: updateNotificationUserFactory(context),
653
+ resyncNotificationUser: resyncNotificationUserFactory(context),
654
+ resyncAllNotificationUsers: resyncAllNotificationUsersFactory(context),
655
+ createNotificationSummary: createNotificationSummaryFactory(context),
656
+ updateNotificationSummary: updateNotificationSummaryFactory(context),
657
+ createNotificationBox: createNotificationBoxFactory(context),
658
+ updateNotificationBox: updateNotificationBoxFactory(context),
659
+ updateNotificationBoxRecipient: updateNotificationBoxRecipientFactory(context),
660
+ sendNotification: sendNotificationFactory(context),
661
+ sendQueuedNotifications: sendQueuedNotificationsFactory(context),
662
+ cleanupSentNotifications: cleanupSentNotificationsFactory(context)
663
+ };
664
+ }
665
+ // MARK: Actions
666
+ function createNotificationUserFactory(context) {
667
+ const { firebaseServerActionTransformFunctionFactory, notificationUserCollection, authService } = context;
668
+ return firebaseServerActionTransformFunctionFactory(CreateNotificationUserParams, async (params) => {
669
+ const { uid } = params;
670
+ return async () => {
671
+ // assert they exist in the auth system
672
+ const userContext = authService.userContext(uid);
673
+ const userExistsInAuth = await userContext.exists();
674
+ if (!userExistsInAuth) {
675
+ throw notificationUserInvalidUidForCreateError(uid);
676
+ }
677
+ const notificationUserDocument = notificationUserCollection.documentAccessor().loadDocumentForId(uid);
678
+ const newUserTemplate = {
679
+ uid,
680
+ x: [],
681
+ bc: [],
682
+ b: [],
683
+ dc: {
684
+ c: {}
685
+ },
686
+ gc: {
687
+ c: {}
688
+ }
689
+ };
690
+ await notificationUserDocument.create(newUserTemplate);
691
+ return notificationUserDocument;
692
+ };
693
+ });
694
+ }
695
+ function updateNotificationUserFactory(context) {
696
+ const { firestoreContext, firebaseServerActionTransformFunctionFactory, notificationUserCollection, appNotificationTemplateTypeInfoRecordService } = context;
697
+ return firebaseServerActionTransformFunctionFactory(UpdateNotificationUserParams, async (params) => {
698
+ const { gc: inputGc, dc: inputDc, bc: inputBc } = params;
699
+ return async (notificationUserDocument) => {
700
+ await firestoreContext.runTransaction(async (transaction) => {
701
+ const notificationUserDocumentInTransaction = notificationUserCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(notificationUserDocument);
702
+ const notificationUser = await assertSnapshotData(notificationUserDocumentInTransaction);
703
+ const updateTemplate = {};
704
+ const allKnownNotificationTypes = appNotificationTemplateTypeInfoRecordService.getAllKnownTemplateTypes();
705
+ if (inputDc != null) {
706
+ updateTemplate.dc = updateNotificationUserDefaultNotificationBoxRecipientConfig(notificationUser.dc, inputDc, allKnownNotificationTypes);
707
+ }
708
+ if (inputGc != null) {
709
+ const nextGc = updateNotificationUserDefaultNotificationBoxRecipientConfig(notificationUser.gc, inputGc, allKnownNotificationTypes);
710
+ if (!areEqualPOJOValues(notificationUser.gc, nextGc)) {
711
+ updateTemplate.gc = nextGc;
712
+ // iterate and update any box config that has the effective recipient change
713
+ updateTemplate.bc = notificationUser.bc.map((currentConfig) => {
714
+ // check item isn't already marked for sync or marked as removed
715
+ if (currentConfig.ns === true || currentConfig.rm === true) {
716
+ return currentConfig;
717
+ }
718
+ const currentEffectiveRecipient = effectiveNotificationBoxRecipientConfig({
719
+ uid: notificationUser.uid,
720
+ appNotificationTemplateTypeInfoRecordService,
721
+ gc: notificationUser.gc,
722
+ boxConfig: currentConfig
723
+ });
724
+ const nextEffectiveRecipient = effectiveNotificationBoxRecipientConfig({
725
+ uid: notificationUser.uid,
726
+ appNotificationTemplateTypeInfoRecordService,
727
+ gc: nextGc,
728
+ boxConfig: currentConfig
729
+ });
730
+ const effectiveConfigChanged = !areEqualPOJOValues(currentEffectiveRecipient, nextEffectiveRecipient);
731
+ return effectiveConfigChanged ? { ...currentConfig, ns: true } : currentConfig;
732
+ });
733
+ }
734
+ }
735
+ if (inputBc != null) {
736
+ const updateTemplateBc = updateNotificationUserNotificationBoxRecipientConfigs(updateTemplate.bc ?? notificationUser.bc, inputBc, appNotificationTemplateTypeInfoRecordService);
737
+ if (updateTemplateBc != null) {
738
+ // re-apply exclusions to the updated configs
739
+ const withExclusions = applyExclusionsToNotificationUserNotificationBoxRecipientConfigs({
740
+ notificationUser,
741
+ bc: updateTemplateBc,
742
+ recalculateNs: false
743
+ });
744
+ updateTemplate.bc = withExclusions.bc;
745
+ updateTemplate.b = updateTemplateBc.map((x) => x.nb);
746
+ }
747
+ }
748
+ // if bc is being updated, then also update ns
749
+ if (updateTemplate.bc != null) {
750
+ updateTemplate.ns = calculateNsForNotificationUserNotificationBoxRecipientConfigs(updateTemplate.bc);
751
+ }
752
+ await notificationUserDocumentInTransaction.update(updateTemplate);
753
+ });
754
+ return notificationUserDocument;
755
+ };
756
+ });
757
+ }
758
+ const MAX_NOTIFICATION_BOXES_TO_UPDATE_PER_BATCH = 50;
759
+ function resyncNotificationUserFactory(context) {
760
+ const { firestoreContext, firebaseServerActionTransformFunctionFactory, notificationBoxCollection, notificationUserCollection, appNotificationTemplateTypeInfoRecordService } = context;
761
+ return firebaseServerActionTransformFunctionFactory(ResyncNotificationUserParams, async () => {
762
+ return async (notificationUserDocument) => {
763
+ // run updates in batches
764
+ let notificationBoxesUpdated = 0;
765
+ let hasMoreNotificationBoxesToSync = true;
766
+ while (hasMoreNotificationBoxesToSync) {
767
+ const batchResult = await firestoreContext.runTransaction(async (transaction) => {
768
+ const notificationUserDocumentInTransaction = notificationUserCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(notificationUserDocument);
769
+ const notificationUser = await assertSnapshotData(notificationUserDocumentInTransaction);
770
+ const { gc } = notificationUser;
771
+ const notificationBoxConfigsToSync = notificationUser.bc.filter((x) => x.ns);
772
+ const notificationBoxConfigsToSyncInThisBatch = takeFront(notificationBoxConfigsToSync, MAX_NOTIFICATION_BOXES_TO_UPDATE_PER_BATCH);
773
+ /**
774
+ * These are the actual number of NotificationBox values that had recipients updated.
775
+ */
776
+ let notificationBoxesUpdatedInBatch = 0;
777
+ let hasUnsyncedNotificationBoxConfigs = false;
778
+ if (notificationBoxConfigsToSyncInThisBatch.length > 0) {
779
+ const notificationBoxConfigsToSyncInThisBatchMap = makeModelMap(notificationBoxConfigsToSyncInThisBatch, (x) => x.nb);
780
+ const notificationBoxIdsToSyncInThisBatch = Array.from(notificationBoxConfigsToSyncInThisBatchMap.keys());
781
+ const notificationBoxDocuments = loadDocumentsForIds(notificationBoxCollection.documentAccessorForTransaction(transaction), notificationBoxIdsToSyncInThisBatch);
782
+ const notificationBoxDocumentSnapshotDataPairs = await getDocumentSnapshotDataPairs(notificationBoxDocuments);
783
+ const notificationBoxConfigsToRemoveFromNotificationUser = new Set();
784
+ const notificationUserNotificationBoxConfigsToMarkAsRemoved = new Set();
785
+ const nextRecipientsMap = new Map();
786
+ // update each NotificationBoxDocument
787
+ await performAsyncTasks(notificationBoxDocumentSnapshotDataPairs, async (notificationBoxDocumentSnapshotDataPair) => {
788
+ const { data: notificationBox, document } = notificationBoxDocumentSnapshotDataPair;
789
+ const nb = document.id;
790
+ const notificationUserNotificationBoxConfig = notificationBoxConfigsToSyncInThisBatchMap.get(nb); // always exists
791
+ if (!notificationBox) {
792
+ // if the entire NotificationBox no longer exists, flag to remove it from the user as a cleanup measure
793
+ notificationBoxConfigsToRemoveFromNotificationUser.add(nb);
794
+ }
795
+ else {
796
+ // update in the NotificationBox
797
+ const recipientIndex = notificationBox.r.findIndex((x) => x.uid === notificationUser.uid);
798
+ let r;
799
+ if (recipientIndex === -1) {
800
+ // if they are not in the NotificationBox, then mark them as removed on the user
801
+ notificationUserNotificationBoxConfigsToMarkAsRemoved.add(nb);
802
+ }
803
+ else if (notificationUserNotificationBoxConfig.rm) {
804
+ // remove from the notification box if it is flagged
805
+ r = removeValuesAtIndexesFromArrayCopy(notificationBox.r, recipientIndex);
806
+ }
807
+ else {
808
+ const { m } = notificationBox;
809
+ const recipient = notificationBox.r[recipientIndex];
810
+ const nextRecipient = effectiveNotificationBoxRecipientConfig({
811
+ uid: notificationUser.uid,
812
+ m,
813
+ appNotificationTemplateTypeInfoRecordService,
814
+ gc,
815
+ boxConfig: notificationUserNotificationBoxConfig,
816
+ recipient
817
+ });
818
+ const recipientHasChange = !areEqualPOJOValues(nextRecipient, recipient);
819
+ // only update recipients if the next/new recipient is not equal to the existing one
820
+ if (recipientHasChange) {
821
+ r = [...notificationBox.r];
822
+ r[recipientIndex] = nextRecipient;
823
+ nextRecipientsMap.set(nb, nextRecipient);
824
+ }
825
+ else {
826
+ nextRecipientsMap.set(nb, recipient);
827
+ }
828
+ }
829
+ // update recipients if needed
830
+ if (r != null) {
831
+ await document.update({ r });
832
+ notificationBoxesUpdatedInBatch += 1;
833
+ }
834
+ }
835
+ });
836
+ // Update the NotificationUser
837
+ const notificationBoxIdsSynced = new Set(notificationBoxIdsToSyncInThisBatch);
838
+ // start nextConfigs off as a new array with none of the sync'd ids
839
+ const nextConfigs = notificationBoxConfigsToSyncInThisBatch.filter((x) => !notificationBoxIdsSynced.has(x.nb));
840
+ notificationBoxIdsToSyncInThisBatch.forEach((nb) => {
841
+ let nextConfig;
842
+ if (notificationBoxConfigsToRemoveFromNotificationUser.has(nb)) ;
843
+ else {
844
+ const existingConfig = notificationBoxConfigsToSyncInThisBatchMap.get(nb);
845
+ if (notificationUserNotificationBoxConfigsToMarkAsRemoved.has(nb) || existingConfig.rm) {
846
+ // if the recipient was being removed or is marked as removed, then update the config to confirm removal
847
+ nextConfig = {
848
+ ...existingConfig,
849
+ nb,
850
+ rm: true,
851
+ i: UNSET_INDEX_NUMBER
852
+ };
853
+ }
854
+ else {
855
+ // else, use the updated recipient and keep/copy the
856
+ const updatedRecipient = nextRecipientsMap.get(nb);
857
+ nextConfig = {
858
+ ...existingConfig,
859
+ nb,
860
+ rm: false, // mark as not removed
861
+ i: updatedRecipient.i ?? UNSET_INDEX_NUMBER
862
+ };
863
+ }
864
+ }
865
+ if (nextConfig != null) {
866
+ nextConfig.ns = false; // mark as synced
867
+ nextConfigs.push(nextConfig);
868
+ }
869
+ });
870
+ const ns = nextConfigs.some((x) => x.ns);
871
+ await notificationUserDocumentInTransaction.update({ bc: nextConfigs, ns });
872
+ hasUnsyncedNotificationBoxConfigs = ns;
873
+ }
874
+ const batchResult = {
875
+ hasMoreNotificationBoxesToSync: hasUnsyncedNotificationBoxConfigs,
876
+ notificationBoxesUpdatedInBatch
877
+ };
878
+ return batchResult;
879
+ });
880
+ hasMoreNotificationBoxesToSync = batchResult.hasMoreNotificationBoxesToSync;
881
+ notificationBoxesUpdated += batchResult.notificationBoxesUpdatedInBatch;
882
+ }
883
+ const result = {
884
+ notificationBoxesUpdated
885
+ };
886
+ return result;
887
+ };
888
+ });
889
+ }
890
+ function resyncAllNotificationUsersFactory(context) {
891
+ const { notificationUserCollection } = context;
892
+ const resyncNotificationUser = resyncNotificationUserFactory(context);
893
+ return async () => {
894
+ let notificationBoxesUpdated = 0;
895
+ const resyncNotificationUserParams = { key: firestoreDummyKey() };
896
+ const resyncNotificationUserInstance = await resyncNotificationUser(resyncNotificationUserParams);
897
+ const iterateResult = await iterateFirestoreDocumentSnapshotPairs({
898
+ documentAccessor: notificationUserCollection.documentAccessor(),
899
+ iterateSnapshotPair: async (snapshotPair) => {
900
+ const { document: notificationUserDocument } = snapshotPair;
901
+ const result = await resyncNotificationUserInstance(notificationUserDocument);
902
+ notificationBoxesUpdated += result.notificationBoxesUpdated;
903
+ },
904
+ constraintsFactory: () => notificationUsersFlaggedForNeedsSyncQuery(),
905
+ snapshotsPerformTasksConfig: {
906
+ // prevent NotificationUsers with the same NotificationBoxes from being updated/sync'd at the same time
907
+ nonConcurrentTaskKeyFactory: (x) => {
908
+ const notificationBoxIdsToSync = x
909
+ .data()
910
+ .bc.filter((x) => x.ns)
911
+ .map((x) => x.nb);
912
+ return notificationBoxIdsToSync;
913
+ }
914
+ },
915
+ queryFactory: notificationUserCollection,
916
+ batchSize: undefined,
917
+ performTasksConfig: {
918
+ maxParallelTasks: 10
919
+ }
920
+ });
921
+ const result = {
922
+ notificationUsersResynced: iterateResult.totalSnapshotsVisited,
923
+ notificationBoxesUpdated
924
+ };
925
+ return result;
926
+ };
927
+ }
928
+ function createNotificationSummaryFactory(context) {
929
+ const { firebaseServerActionTransformFunctionFactory, notificationSummaryCollection } = context;
930
+ return firebaseServerActionTransformFunctionFactory(CreateNotificationSummaryParams, async (params) => {
931
+ const { model } = params;
932
+ return async () => {
933
+ const notificationSummaryId = notificationSummaryIdForModel(model);
934
+ const notificationSummaryDocument = notificationSummaryCollection.documentAccessor().loadDocumentForId(notificationSummaryId);
935
+ const newSummaryTemplate = makeNewNotificationSummaryTemplate(model);
936
+ await notificationSummaryDocument.create(newSummaryTemplate);
937
+ return notificationSummaryDocument;
938
+ };
939
+ });
940
+ }
941
+ function updateNotificationSummaryFactory(context) {
942
+ const { firebaseServerActionTransformFunctionFactory } = context;
943
+ return firebaseServerActionTransformFunctionFactory(UpdateNotificationSummaryParams, async (params) => {
944
+ const { setReadAtTime, flagAllRead } = params;
945
+ return async (notificationSummaryDocument) => {
946
+ let updateTemplate;
947
+ if (setReadAtTime != null) {
948
+ updateTemplate = { rat: setReadAtTime };
949
+ }
950
+ else if (flagAllRead === true) {
951
+ updateTemplate = { rat: new Date() };
952
+ }
953
+ if (updateTemplate != null) {
954
+ await notificationSummaryDocument.update(updateTemplate);
955
+ }
956
+ return notificationSummaryDocument;
957
+ };
958
+ });
959
+ }
960
+ function createNotificationBoxInTransactionFactory(context) {
961
+ const { notificationBoxCollection } = context;
962
+ return async (params, transaction) => {
963
+ const { now: inputNow, skipCreate } = params;
964
+ const now = inputNow ?? new Date();
965
+ const notificationBoxDocument = loadNotificationBoxDocumentForReferencePair(params, notificationBoxCollection.documentAccessorForTransaction(transaction));
966
+ const notificationBoxTemplate = {
967
+ m: notificationBoxDocument.notificationBoxRelatedModelKey,
968
+ o: firestoreDummyKey(), // set during initialization
969
+ r: [],
970
+ cat: now,
971
+ w: yearWeekCode(now),
972
+ s: true // requires initialization
973
+ };
974
+ if (!skipCreate) {
975
+ await notificationBoxDocument.create(notificationBoxTemplate);
976
+ }
977
+ return {
978
+ notificationBoxTemplate,
979
+ notificationBoxDocument
980
+ };
981
+ };
982
+ }
983
+ function createNotificationBoxFactory(context) {
984
+ const { firestoreContext, notificationBoxCollection, firebaseServerActionTransformFunctionFactory } = context;
985
+ const createNotificationBoxInTransaction = createNotificationBoxInTransactionFactory(context);
986
+ return firebaseServerActionTransformFunctionFactory(CreateNotificationBoxParams, async (params) => {
987
+ const { model } = params;
988
+ return async () => {
989
+ const result = await firestoreContext.runTransaction(async (transaction) => {
990
+ const { notificationBoxDocument } = await createNotificationBoxInTransaction({ notificationBoxRelatedModelKey: model }, transaction);
991
+ return notificationBoxDocument;
992
+ });
993
+ return notificationBoxCollection.documentAccessor().loadDocumentFrom(result);
994
+ };
995
+ });
996
+ }
997
+ function updateNotificationBoxFactory({ firebaseServerActionTransformFunctionFactory }) {
998
+ return firebaseServerActionTransformFunctionFactory(UpdateNotificationBoxParams, async () => {
999
+ return async (notificationBoxDocument) => {
1000
+ // does nothing currently.
1001
+ return notificationBoxDocument;
1002
+ };
1003
+ });
1004
+ }
1005
+ function updateNotificationBoxRecipientExclusionInTransactionFactory(context) {
1006
+ const { notificationBoxCollection, notificationUserCollection } = context;
1007
+ return async (input, transaction) => {
1008
+ const { params } = input;
1009
+ const { uid: inputUid, i, setExclusion } = params;
1010
+ const notificationBoxDocument = loadNotificationBoxDocumentForReferencePair(input, notificationBoxCollection.documentAccessorForTransaction(transaction));
1011
+ let targetUid = inputUid;
1012
+ let result = undefined;
1013
+ if (setExclusion == null) {
1014
+ throw new Error('setExclusion was undefined. Maybe you wanted to call updateNotificationBoxRecipientInTransactionFactory() instead?');
1015
+ }
1016
+ else if (!inputUid && i != null) {
1017
+ // only load the notification box if targeting a recipient by index
1018
+ const notificationBox = await notificationBoxDocument.snapshotData();
1019
+ if (!notificationBox) {
1020
+ throw notificationBoxExclusionTargetInvalidError();
1021
+ }
1022
+ const targetRecipient = notificationBox.r.find((x) => x.i === i);
1023
+ if (!targetRecipient || !targetRecipient.uid) {
1024
+ throw notificationBoxExclusionTargetInvalidError();
1025
+ }
1026
+ else {
1027
+ targetUid = targetRecipient.uid;
1028
+ }
1029
+ }
1030
+ if (!targetUid) {
1031
+ throw notificationBoxExclusionTargetInvalidError();
1032
+ }
1033
+ const notificationUserDocument = await notificationUserCollection.documentAccessorForTransaction(transaction).loadDocumentForId(targetUid);
1034
+ const notificationUser = await notificationUserDocument.snapshotData();
1035
+ if (notificationUser) {
1036
+ // only update if the user exists
1037
+ const targetExclusions = [notificationBoxDocument.id];
1038
+ const { update: notificationUserUpdate } = updateNotificationUserNotificationSendExclusions({
1039
+ notificationUser,
1040
+ addExclusions: setExclusion ? targetExclusions : undefined,
1041
+ removeExclusions: setExclusion ? undefined : targetExclusions
1042
+ });
1043
+ await notificationUserDocument.update(notificationUserUpdate);
1044
+ result = {
1045
+ notificationUserUpdate
1046
+ };
1047
+ }
1048
+ return result;
1049
+ };
1050
+ }
1051
+ function updateNotificationBoxRecipientInTransactionFactory(context) {
1052
+ const { authService, notificationBoxCollection, notificationUserCollection } = context;
1053
+ const createNotificationBoxInTransaction = createNotificationBoxInTransactionFactory(context);
1054
+ return async (input, transaction) => {
1055
+ const { params, allowCreateNotificationBoxIfItDoesNotExist, throwErrorIfNotificationBoxDoesNotExist } = input;
1056
+ const { uid, i, insert, remove, configs: inputC, setExclusion } = params;
1057
+ const findRecipientFn = (x) => (uid != null && x.uid === uid) || (i != null && x.i === i);
1058
+ if (setExclusion != null) {
1059
+ throw new Error('exclusion update must be processed by updateNotificationBoxRecipientExclusionInTransactionFactory() function.');
1060
+ }
1061
+ const notificationBoxDocument = loadNotificationBoxDocumentForReferencePair(input, notificationBoxCollection.documentAccessorForTransaction(transaction));
1062
+ let notificationBox = await notificationBoxDocument.snapshotData();
1063
+ let createNotificationBox = false;
1064
+ let result = undefined;
1065
+ if (!notificationBox) {
1066
+ if (allowCreateNotificationBoxIfItDoesNotExist) {
1067
+ const { notificationBoxTemplate } = await createNotificationBoxInTransaction({
1068
+ notificationBoxDocument,
1069
+ skipCreate: true // don't create since we still need to read things for the transaction
1070
+ }, transaction);
1071
+ notificationBox = notificationBoxTemplate;
1072
+ createNotificationBox = true;
1073
+ }
1074
+ else if (throwErrorIfNotificationBoxDoesNotExist) {
1075
+ throw notificationBoxDoesNotExist();
1076
+ }
1077
+ }
1078
+ if (notificationBox) {
1079
+ const { m } = notificationBox;
1080
+ let r;
1081
+ let targetRecipientIndex = notificationBox.r.findIndex(findRecipientFn);
1082
+ const targetRecipient = notificationBox.r[targetRecipientIndex];
1083
+ let nextRecipient;
1084
+ if (remove) {
1085
+ if (targetRecipientIndex != null) {
1086
+ r = [...notificationBox.r]; // remove if they exist.
1087
+ delete r[targetRecipientIndex];
1088
+ }
1089
+ }
1090
+ else {
1091
+ if (!targetRecipient && !insert) {
1092
+ throw notificationBoxRecipientDoesNotExistsError();
1093
+ }
1094
+ const c = (inputC != null ? notificationBoxRecipientTemplateConfigArrayToRecord(inputC) : targetRecipient?.c) ?? {};
1095
+ nextRecipient = {
1096
+ uid,
1097
+ i: targetRecipient?.i ?? UNSET_INDEX_NUMBER,
1098
+ c,
1099
+ ...updateNotificationRecipient(targetRecipient ?? {}, params)
1100
+ };
1101
+ r = [...notificationBox.r];
1102
+ if (targetRecipient) {
1103
+ nextRecipient.i = targetRecipient.i;
1104
+ nextRecipient = mergeNotificationBoxRecipients(targetRecipient, nextRecipient);
1105
+ r[targetRecipientIndex] = nextRecipient; // override in the array
1106
+ }
1107
+ else {
1108
+ const nextI = computeNextFreeIndexOnSortedValuesFunction(readIndexNumber)(notificationBox.r); // r is sorted by index in ascending order, so the last value is the largest i
1109
+ nextRecipient.i = nextI;
1110
+ // should have the greatest i value, push to end
1111
+ r.push(nextRecipient);
1112
+ targetRecipientIndex = r.length - 1;
1113
+ }
1114
+ }
1115
+ // save changes to r if it has changed
1116
+ if (r != null) {
1117
+ const notificationUserId = targetRecipient?.uid ?? nextRecipient?.uid;
1118
+ // sync with the notification user's document, if it exists
1119
+ if (notificationUserId != null) {
1120
+ const notificationBoxId = notificationBoxDocument.id;
1121
+ const notificationUserDocument = await notificationUserCollection.documentAccessorForTransaction(transaction).loadDocumentForId(notificationUserId);
1122
+ let notificationUser = await notificationUserDocument.snapshotData();
1123
+ const createNotificationUser = !notificationUser && !remove && insert;
1124
+ if (createNotificationUser) {
1125
+ // assert they exist in the auth system
1126
+ const userContext = authService.userContext(notificationUserId);
1127
+ const userExistsInAuth = await userContext.exists();
1128
+ if (!userExistsInAuth) {
1129
+ throw notificationUserInvalidUidForCreateError(notificationUserId);
1130
+ }
1131
+ const notificationUserTemplate = {
1132
+ uid: notificationUserId,
1133
+ b: [],
1134
+ x: [],
1135
+ bc: [],
1136
+ ns: false,
1137
+ dc: {
1138
+ c: {}
1139
+ },
1140
+ gc: {
1141
+ c: {}
1142
+ }
1143
+ };
1144
+ notificationUser = notificationUserTemplate;
1145
+ }
1146
+ // if the user is being inserted or exists, then make updates
1147
+ if (notificationUser != null) {
1148
+ const { updatedBc, updatedNotificationBoxRecipient } = updateNotificationUserNotificationBoxRecipientConfig({
1149
+ notificationBoxId,
1150
+ notificationUserId,
1151
+ notificationUser,
1152
+ insertingRecipientIntoNotificationBox: insert,
1153
+ removeRecipientFromNotificationBox: remove,
1154
+ notificationBoxRecipient: nextRecipient
1155
+ });
1156
+ const updatedB = updatedBc ? updatedBc.map((x) => x.nb) : undefined;
1157
+ if (createNotificationUser) {
1158
+ const newUserTemplate = {
1159
+ ...notificationUser,
1160
+ bc: updatedBc ?? [],
1161
+ b: updatedB ?? []
1162
+ };
1163
+ await notificationUserDocument.create(newUserTemplate);
1164
+ }
1165
+ else if (updatedBc != null) {
1166
+ await notificationUserDocument.update({ bc: updatedBc, b: updatedB });
1167
+ }
1168
+ // Set if nextRecipient is updated/influence from existing configuration
1169
+ if (targetRecipientIndex != null && updatedNotificationBoxRecipient && !remove) {
1170
+ r[targetRecipientIndex] = updatedNotificationBoxRecipient; // set the updated value in r
1171
+ }
1172
+ }
1173
+ // else, if removing and they don't exist, nothing to update
1174
+ }
1175
+ const updatedNotificationBox = { ...notificationBox, r };
1176
+ let notificationBoxWasCreated = false;
1177
+ if (createNotificationBox) {
1178
+ await notificationBoxDocument.create(updatedNotificationBox);
1179
+ notificationBoxWasCreated = true;
1180
+ }
1181
+ else {
1182
+ await notificationBoxDocument.update({ r });
1183
+ }
1184
+ result = {
1185
+ updatedNotificationBox,
1186
+ notificationBoxWasCreated,
1187
+ notificationBoxDocument
1188
+ };
1189
+ }
1190
+ }
1191
+ return result;
1192
+ };
1193
+ }
1194
+ function updateNotificationBoxRecipientFactory(context) {
1195
+ const { firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
1196
+ const updateNotificationBoxRecipientInTransaction = updateNotificationBoxRecipientInTransactionFactory(context);
1197
+ const updateNotificationBoxRecipientExclusionInTransaction = updateNotificationBoxRecipientExclusionInTransactionFactory(context);
1198
+ return firebaseServerActionTransformFunctionFactory(UpdateNotificationBoxRecipientParams, async (params) => {
1199
+ return async (notificationBoxDocument) => {
1200
+ await firestoreContext.runTransaction(async (transaction) => {
1201
+ if (params.setExclusion != null) {
1202
+ await updateNotificationBoxRecipientExclusionInTransaction({
1203
+ params,
1204
+ notificationBoxDocument
1205
+ }, transaction);
1206
+ }
1207
+ else {
1208
+ await updateNotificationBoxRecipientInTransaction({
1209
+ params,
1210
+ throwErrorIfNotificationBoxDoesNotExist: true,
1211
+ notificationBoxDocument
1212
+ }, transaction);
1213
+ }
1214
+ });
1215
+ return notificationBoxDocument;
1216
+ };
1217
+ });
1218
+ }
1219
+ const UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY = 8;
1220
+ const UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS = 1;
1221
+ const UNKNOWN_NOTIFICATION_TASK_TYPE_HOURS_DELAY = 8;
1222
+ const UNKNOWN_NOTIFICATION_TASK_TYPE_DELETE_AFTER_RETRY_ATTEMPTS = 1;
1223
+ const KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY = UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY;
1224
+ const KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS = 5;
1225
+ const NOTIFICATION_MAX_SEND_ATTEMPTS = 5;
1226
+ const NOTIFICATION_BOX_NOT_INITIALIZED_DELAY_MINUTES = 8;
1227
+ /**
1228
+ * Minimum time in minutes that a notification task can be attempted again
1229
+ */
1230
+ const NOTIFICATION_TASK_MINIMUM_SET_AT_THROTTLE_TIME_MINUTES = 1;
1231
+ const NOTIFICATION_TASK_TYPE_MAX_SEND_ATTEMPTS = 5;
1232
+ const NOTIFICATION_TASK_TYPE_FAILURE_DELAY_HOURS = 3;
1233
+ const NOTIFICATION_TASK_TYPE_FAILURE_DELAY_MS = hoursToMilliseconds(NOTIFICATION_TASK_TYPE_FAILURE_DELAY_HOURS);
1234
+ function sendNotificationFactory(context) {
1235
+ const { appNotificationTemplateTypeInfoRecordService, notificationSendService, notificationTaskService, notificationTemplateService, authService, notificationBoxCollection, notificationCollectionGroup, notificationUserCollection, firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
1236
+ const createNotificationBoxInTransaction = createNotificationBoxInTransactionFactory(context);
1237
+ const notificationUserAccessor = notificationUserCollection.documentAccessor();
1238
+ return firebaseServerActionTransformFunctionFactory(SendNotificationParams, async (params) => {
1239
+ const { ignoreSendAtThrottle } = params;
1240
+ return async (inputNotificationDocument) => {
1241
+ const now = new Date();
1242
+ // Load the notification document outside of any potential context (transaction, etc.)
1243
+ const notificationDocument = notificationCollectionGroup.documentAccessor().loadDocumentFrom(inputNotificationDocument);
1244
+ const { nextSat, throttled, tryRun, isNotificationTask, notificationTaskHandler, notification, createdBox, notificationBoxNeedsInitialization, notificationBox, notificationBoxModelKey, deletedNotification, templateInstance, isConfiguredTemplateType, isKnownTemplateType, onlySendToExplicitlyEnabledRecipients, onlyTextExplicitlyEnabledRecipients } = await firestoreContext.runTransaction(async (transaction) => {
1245
+ const notificationBoxDocument = notificationBoxCollection.documentAccessorForTransaction(transaction).loadDocument(notificationDocument.parent);
1246
+ const notificationDocumentInTransaction = notificationCollectionGroup.documentAccessorForTransaction(transaction).loadDocumentFrom(notificationDocument);
1247
+ let [notificationBox, notification] = await Promise.all([getDocumentSnapshotData(notificationBoxDocument), getDocumentSnapshotData(notificationDocumentInTransaction)]);
1248
+ const model = inferKeyFromTwoWayFlatFirestoreModelKey(notificationBoxDocument.id);
1249
+ const isNotificationTask = notification?.st === NotificationSendType.TASK_NOTIFICATION;
1250
+ let tryRun = true;
1251
+ let throttled = false;
1252
+ let nextSat;
1253
+ if (!notification) {
1254
+ tryRun = false;
1255
+ }
1256
+ else if (!ignoreSendAtThrottle) {
1257
+ tryRun = !isFuture(notification.sat);
1258
+ if (!tryRun) {
1259
+ throttled = true;
1260
+ }
1261
+ }
1262
+ // always set nextSat if tryRun is true
1263
+ if (tryRun) {
1264
+ if (isNotificationTask) {
1265
+ // can try to run the task again in 1 minute
1266
+ nextSat = addMinutes(now, NOTIFICATION_TASK_MINIMUM_SET_AT_THROTTLE_TIME_MINUTES);
1267
+ }
1268
+ else {
1269
+ // update the next send type of non-tasks to try being sent again in 10 minutes, if they fail
1270
+ nextSat = addMinutes(now, 10);
1271
+ }
1272
+ }
1273
+ let createdBox = false;
1274
+ let deletedNotification = false;
1275
+ let notificationBoxNeedsInitialization = false;
1276
+ let isKnownTemplateType;
1277
+ let isConfiguredTemplateType;
1278
+ let onlySendToExplicitlyEnabledRecipients;
1279
+ let onlyTextExplicitlyEnabledRecipients;
1280
+ let templateInstance;
1281
+ let notificationTaskHandler;
1282
+ async function deleteNotification() {
1283
+ tryRun = false;
1284
+ await notificationDocumentInTransaction.accessor.delete();
1285
+ deletedNotification = true;
1286
+ }
1287
+ // create/init the notification box if necessary/configured.
1288
+ if (notification && tryRun) {
1289
+ // if we're still trying to run, check the template is ok. If not, cancel the run.
1290
+ const { t // notification task/template type
1291
+ } = notification.n;
1292
+ if (isNotificationTask) {
1293
+ notificationTaskHandler = notificationTaskService.taskHandlerForNotificationTaskType(t);
1294
+ if (notificationTaskHandler) {
1295
+ if (notification.a >= NOTIFICATION_TASK_TYPE_MAX_SEND_ATTEMPTS) {
1296
+ tryRun = false;
1297
+ console.warn(`Configured notification task of type "${t}" has reached the delete threshhold after being attempted ${notification.a} times. Deleting notification task.`);
1298
+ await deleteNotification();
1299
+ }
1300
+ }
1301
+ else {
1302
+ tryRun = false;
1303
+ const delay = UNKNOWN_NOTIFICATION_TASK_TYPE_HOURS_DELAY;
1304
+ if (notification.a < UNKNOWN_NOTIFICATION_TASK_TYPE_DELETE_AFTER_RETRY_ATTEMPTS) {
1305
+ console.warn(`Notification task type of "${t}" was found in a Notification but has no handler. Action is being delayed by ${delay} hours.`);
1306
+ nextSat = addHours(now, delay);
1307
+ }
1308
+ else {
1309
+ console.warn(`Notification task type of "${t}" was found in a Notification but has no handler. Action is being deleted.`);
1310
+ // delete the notification
1311
+ await deleteNotification();
1312
+ }
1313
+ }
1314
+ }
1315
+ else {
1316
+ templateInstance = notificationTemplateService.templateInstanceForType(t);
1317
+ isConfiguredTemplateType = templateInstance.isConfiguredType;
1318
+ const templateTypeInfo = appNotificationTemplateTypeInfoRecordService.appNotificationTemplateTypeInfoRecord[t];
1319
+ isKnownTemplateType = templateTypeInfo != null;
1320
+ onlySendToExplicitlyEnabledRecipients = notification.ois ?? templateTypeInfo?.onlySendToExplicitlyEnabledRecipients;
1321
+ onlyTextExplicitlyEnabledRecipients = notification.ots ?? templateTypeInfo?.onlyTextExplicitlyEnabledRecipients;
1322
+ if (!isConfiguredTemplateType) {
1323
+ // log the issue that an notification with an unconfigured type was queued
1324
+ const retryAttempts = isKnownTemplateType ? KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS : UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS;
1325
+ const delay = isKnownTemplateType ? KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY : UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY;
1326
+ if (notification.a < retryAttempts) {
1327
+ if (isKnownTemplateType) {
1328
+ console.warn(`Unconfigured but known template type of "${t}" (${templateTypeInfo.name}) was found in a Notification. Send is being delayed by ${delay} hours.`);
1329
+ }
1330
+ else {
1331
+ console.warn(`Unknown template type of "${t}" was found in a Notification. Send is being delayed by ${delay} hours.`);
1332
+ }
1333
+ // delay send for 12 hours, for a max of 24 hours incase it is an issue.
1334
+ nextSat = addHours(now, delay);
1335
+ tryRun = false;
1336
+ }
1337
+ else {
1338
+ console.warn(`Unconfigured template type of "${t}" was found in a Notification. The Notification has reached the delete threshhold after failing to send due to misconfiguration multiple times and is being deleted.`);
1339
+ // after attempting to send 3 times, delete it.
1340
+ await deleteNotification();
1341
+ }
1342
+ }
1343
+ // handle the notification box's absence
1344
+ if (!notificationBox && tryRun) {
1345
+ switch (notification.st) {
1346
+ case NotificationSendType.INIT_BOX_AND_SEND:
1347
+ const { notificationBoxTemplate } = await createNotificationBoxInTransaction({ notificationBoxDocument }, transaction);
1348
+ notificationBox = setIdAndKeyFromKeyIdRefOnDocumentData(notificationBoxTemplate, notificationBoxDocument);
1349
+ createdBox = true;
1350
+ break;
1351
+ case NotificationSendType.SEND_IF_BOX_EXISTS:
1352
+ // delete the notification since it won't get sent.
1353
+ await deleteNotification();
1354
+ break;
1355
+ case NotificationSendType.SEND_WITHOUT_CREATING_BOX:
1356
+ // continue with current tryRun
1357
+ break;
1358
+ }
1359
+ }
1360
+ // if the notification box is not initialized/synchronized yet, do not run.
1361
+ if (tryRun && notificationBox && notificationBox.s) {
1362
+ notificationBoxNeedsInitialization = true;
1363
+ tryRun = false;
1364
+ nextSat = addMinutes(now, NOTIFICATION_BOX_NOT_INITIALIZED_DELAY_MINUTES);
1365
+ }
1366
+ }
1367
+ }
1368
+ // update the notification send at time and attempt count
1369
+ if (notification != null && nextSat != null && !deletedNotification) {
1370
+ const isAtMaxAttempts = notification.a >= NOTIFICATION_MAX_SEND_ATTEMPTS;
1371
+ if (isAtMaxAttempts && notificationBoxNeedsInitialization) {
1372
+ await deleteNotification(); // just delete the notification if the box still hasn't been initialized successfully at this point.
1373
+ }
1374
+ // check if it was just deleted
1375
+ if (!deletedNotification) {
1376
+ const a = isNotificationTask && tryRun ? notification.a : notification.a + 1; // do not update a notification task's attempt count here, unless tryRun fails
1377
+ // NOTE: It is important to update sat so the notification task queue running doesn't get stuck in a query loop by notifications/tasks that have a sat value that is in the past, but was just run.
1378
+ await notificationDocumentInTransaction.update({ sat: nextSat, a });
1379
+ }
1380
+ }
1381
+ return {
1382
+ nextSat,
1383
+ throttled,
1384
+ isNotificationTask,
1385
+ deletedNotification,
1386
+ createdBox,
1387
+ notificationBoxModelKey: model,
1388
+ notificationBoxNeedsInitialization,
1389
+ notificationBox,
1390
+ notification,
1391
+ templateInstance,
1392
+ isKnownTemplateType,
1393
+ notificationTaskHandler,
1394
+ isConfiguredTemplateType,
1395
+ tryRun,
1396
+ onlySendToExplicitlyEnabledRecipients,
1397
+ onlyTextExplicitlyEnabledRecipients
1398
+ };
1399
+ });
1400
+ let success = false;
1401
+ let isUniqueNotificationTask = false;
1402
+ let uniqueNotificationTaskConflict = false;
1403
+ let sendEmailsResult;
1404
+ let sendTextsResult;
1405
+ let sendNotificationSummaryResult;
1406
+ let loadMessageFunctionFailure = false;
1407
+ let buildMessageFailure = false;
1408
+ let notificationMarkedDone = false;
1409
+ let notificationTaskCompletionType;
1410
+ let notificationTaskPartsRunCount = 0;
1411
+ let notificationTaskLoopingProtectionTriggered;
1412
+ let onSendAttemptedResult;
1413
+ let onSendSuccessResult;
1414
+ const notificationTemplateType = templateInstance?.type;
1415
+ if (isNotificationTask) {
1416
+ await handleNotificationTask();
1417
+ }
1418
+ else {
1419
+ await handleNormalNotification();
1420
+ }
1421
+ async function _runNotificationTaskNextPart(input) {
1422
+ const { notification, notificationTaskHandler, previouslyCompleteSubTasks } = input;
1423
+ const { n: item, cat, ut } = notification;
1424
+ let tryRunNextPart = false;
1425
+ let partNotificationTaskCompletionType;
1426
+ let partNotificationMarkedDone = false;
1427
+ let partTprReversal = false;
1428
+ let partSuccess = false;
1429
+ let nextCompleteSubTasks;
1430
+ const unique = ut ?? false;
1431
+ const notificationTask = {
1432
+ notificationDocument,
1433
+ totalSendAttempts: notification.a,
1434
+ currentCheckpointSendAttempts: notification.at ?? 0,
1435
+ taskType: item.t,
1436
+ item,
1437
+ data: item.d,
1438
+ checkpoints: notification.tpr,
1439
+ createdAt: cat,
1440
+ unique
1441
+ };
1442
+ // calculate results
1443
+ const notificationTemplate = {};
1444
+ // perform the task
1445
+ try {
1446
+ const handleTaskResult = await notificationTaskHandler.handleNotificationTask(notificationTask);
1447
+ const { completion, updateMetadata, delayUntil, canRunNextCheckpoint, allCompletedSubTasks } = handleTaskResult;
1448
+ partNotificationTaskCompletionType = completion;
1449
+ partSuccess = true;
1450
+ switch (completion) {
1451
+ case true:
1452
+ notificationTemplate.d = true; // mark as done
1453
+ break;
1454
+ case false:
1455
+ // failed
1456
+ notificationTemplate.a = notification.a + 1; // increase attempts count
1457
+ notificationTemplate.at = (notification.at ?? 0) + 1; // increase checkpoint attempts count
1458
+ // remove any completions, if applicable
1459
+ notificationTemplate.tpr = removeFromCompletionsArrayWithTaskResult(notification.tpr, handleTaskResult);
1460
+ partSuccess = false;
1461
+ break;
1462
+ default:
1463
+ // default case called if not true or false, which implies either a delay or partial completion
1464
+ // update the checkpoint attempts count
1465
+ if (Array.isArray(completion) && completion.length === 0) {
1466
+ notificationTemplate.at = (notification.at ?? 0) + 1; // increase checkpoint attempt/delays count
1467
+ tryRunNextPart = canRunNextCheckpoint === true && allCompletedSubTasks != null && delayUntil == null; // can only run the next part if subtasks were returned and there is no delay
1468
+ }
1469
+ else {
1470
+ tryRunNextPart = canRunNextCheckpoint === true && delayUntil == null; // can try the next part if there is no delayUntil and canRunNextCheckpoint is true
1471
+ notificationTemplate.at = 0; // reset checkpoint attempt/delay count
1472
+ }
1473
+ // add the checkpoint to the notification
1474
+ notificationTemplate.tpr = [
1475
+ ...removeFromCompletionsArrayWithTaskResult(notification.tpr, handleTaskResult), // remove any completions, if applicable
1476
+ ...asArray(completion)
1477
+ ];
1478
+ // calculate the updated notification item
1479
+ notificationTemplate.n = {
1480
+ ...notification.n,
1481
+ d: {
1482
+ ...notification.n.d,
1483
+ ...(updateMetadata ? filterOnlyUndefinedValues(updateMetadata) : undefined) // ignore any undefined values
1484
+ }
1485
+ };
1486
+ // can only run the next part if the tpr has changed, and the number of checkpoints completed has increased
1487
+ // if the tpr has not changed, then it is also considered a reversal
1488
+ if (tryRunNextPart) {
1489
+ const tprChanged = !iterablesAreSetEquivalent(notification.tpr, notificationTemplate.tpr);
1490
+ partTprReversal = !tprChanged || (tprChanged && notificationTemplate.tpr.length <= notification.tpr.length);
1491
+ if (allCompletedSubTasks != null) {
1492
+ switch (allCompletedSubTasks) {
1493
+ case true:
1494
+ case false:
1495
+ // only run if there is no tpr reversal flagged
1496
+ tryRunNextPart = !partTprReversal;
1497
+ break;
1498
+ default:
1499
+ // check subtask tpr changes
1500
+ nextCompleteSubTasks = asArray(allCompletedSubTasks);
1501
+ const subtaskTprChanged = !iterablesAreSetEquivalent(previouslyCompleteSubTasks, nextCompleteSubTasks);
1502
+ partTprReversal = !subtaskTprChanged || (subtaskTprChanged && nextCompleteSubTasks.length <= previouslyCompleteSubTasks.length);
1503
+ break;
1504
+ }
1505
+ }
1506
+ }
1507
+ break;
1508
+ }
1509
+ // do not update sat if the task is complete
1510
+ if (completion !== true && delayUntil != null) {
1511
+ // must be at least 20 seconds into the future from now, and/or the nextSat time to avoid parallel runs
1512
+ const minimumNextSatTime = addSeconds(new Date(), 20);
1513
+ notificationTemplate.sat = findMaxDate([dateOrMillisecondsToDate(delayUntil, now), nextSat, minimumNextSatTime]) ?? minimumNextSatTime;
1514
+ }
1515
+ partNotificationMarkedDone = notificationTemplate.d === true;
1516
+ }
1517
+ catch (e) {
1518
+ console.error(`Failed handling task for notification "${notification.key}" with type "${notificationTask.taskType}": `, e);
1519
+ notificationTemplate.a = notification.a + 1; // increase attempts count
1520
+ notificationTemplate.sat = dateOrMillisecondsToDate(NOTIFICATION_TASK_TYPE_FAILURE_DELAY_MS, now);
1521
+ partSuccess = false;
1522
+ }
1523
+ // notification tasks are read
1524
+ let saveTaskResult = true;
1525
+ if (unique) {
1526
+ isUniqueNotificationTask = true;
1527
+ const latestNotification = await notificationDocument.snapshotData();
1528
+ if (!latestNotification || !isSameDate(latestNotification.cat, notification.cat)) {
1529
+ saveTaskResult = false;
1530
+ uniqueNotificationTaskConflict = true;
1531
+ }
1532
+ }
1533
+ if (saveTaskResult) {
1534
+ await notificationDocument.update(notificationTemplate);
1535
+ }
1536
+ return {
1537
+ tryRunNextPart,
1538
+ partNotificationMarkedDone,
1539
+ partNotificationTaskCompletionType,
1540
+ partTprReversal,
1541
+ nextCompleteSubTasks,
1542
+ partSuccess
1543
+ };
1544
+ }
1545
+ /**
1546
+ * Notification task handling.
1547
+ *
1548
+ * Notification takss can have multiple async but sequential parts.
1549
+ *
1550
+ * Some of these parts may be able to be run immediately one after the other, instead of waiting for
1551
+ * another sendNotification() to complete on it.
1552
+ */
1553
+ async function handleNotificationTask() {
1554
+ const MAX_NOTIFICATION_TASK_PARTS_RUN_ALLOWED = 5;
1555
+ if (tryRun && notification != null && notificationTaskHandler) {
1556
+ let currentNotification = notification;
1557
+ let previouslyCompleteSubTasks = [];
1558
+ notificationTaskLoopingProtectionTriggered = false;
1559
+ notificationTaskPartsRunCount = 0;
1560
+ while (notificationTaskPartsRunCount < MAX_NOTIFICATION_TASK_PARTS_RUN_ALLOWED) {
1561
+ notificationTaskPartsRunCount += 1;
1562
+ const result = await _runNotificationTaskNextPart({
1563
+ notification: currentNotification,
1564
+ notificationTaskHandler,
1565
+ previouslyCompleteSubTasks
1566
+ });
1567
+ notificationTaskCompletionType = result.partNotificationTaskCompletionType;
1568
+ previouslyCompleteSubTasks = result.nextCompleteSubTasks ?? [];
1569
+ success = result.partSuccess;
1570
+ const tryRunNextPart = result.partSuccess && result.tryRunNextPart && !notificationTaskLoopingProtectionTriggered;
1571
+ notificationTaskLoopingProtectionTriggered = notificationTaskLoopingProtectionTriggered || result.partTprReversal; // update the flag if the TPR has been reversed
1572
+ if (tryRunNextPart) {
1573
+ const updatedNotificationData = await notificationDocument.snapshotData();
1574
+ if (updatedNotificationData) {
1575
+ currentNotification = setIdAndKeyFromKeyIdRefOnDocumentData(updatedNotificationData, notificationDocument);
1576
+ }
1577
+ else {
1578
+ break; // notification is unavailable now
1579
+ }
1580
+ }
1581
+ else {
1582
+ break; // escape the loop
1583
+ }
1584
+ }
1585
+ }
1586
+ }
1587
+ /**
1588
+ * Handles a normal (non-task) notification.
1589
+ */
1590
+ async function handleNormalNotification() {
1591
+ // notification is only null/undefined if it didn't exist.
1592
+ if (notification != null) {
1593
+ if (tryRun && templateInstance != null) {
1594
+ // first load the message function
1595
+ const messageFunction = await templateInstance
1596
+ .loadMessageFunction({
1597
+ item: notification.n,
1598
+ notification,
1599
+ notificationBox: {
1600
+ m: notificationBoxModelKey
1601
+ }
1602
+ })
1603
+ .catch((e) => {
1604
+ loadMessageFunctionFailure = true;
1605
+ success = false;
1606
+ console.error(`Failed loading message function for type ${notificationTemplateType}: `, e);
1607
+ return undefined;
1608
+ });
1609
+ if (messageFunction) {
1610
+ function filterOutNoContentNotificationMessages(messages) {
1611
+ return messages.filter((x) => !x.flag);
1612
+ }
1613
+ // expand recipients
1614
+ const { emails: emailRecipients, texts: textRecipients, notificationSummaries: notificationSummaryRecipients } = await expandNotificationRecipients({
1615
+ notification,
1616
+ notificationBox,
1617
+ authService,
1618
+ notificationUserAccessor,
1619
+ globalRecipients: messageFunction.globalRecipients,
1620
+ onlySendToExplicitlyEnabledRecipients,
1621
+ onlyTextExplicitlyEnabledRecipients,
1622
+ notificationSummaryIdForUid: notificationSendService.notificationSummaryIdForUidFunction
1623
+ });
1624
+ let { es, ts, ps, ns, esr: currentEsr, tsr: currentTsr } = notification;
1625
+ // do emails
1626
+ let esr;
1627
+ if (es === NotificationSendState.QUEUED || es === NotificationSendState.SENT_PARTIAL) {
1628
+ const emailRecipientsAlreadySentTo = new Set(currentEsr.map((x) => x.toLowerCase()));
1629
+ const emailInputContexts = emailRecipients
1630
+ .filter((x) => !emailRecipientsAlreadySentTo.has(x.emailAddress.toLowerCase()))
1631
+ .map((x) => {
1632
+ const context = {
1633
+ recipient: {
1634
+ n: x.name,
1635
+ e: x.emailAddress,
1636
+ t: x.phoneNumber
1637
+ }
1638
+ };
1639
+ return context;
1640
+ });
1641
+ const emailMessages = await Promise.all(emailInputContexts.map(messageFunction))
1642
+ .then(filterOutNoContentNotificationMessages)
1643
+ .catch((e) => {
1644
+ console.error(`Failed building message function for type ${notificationTemplateType}: `, e);
1645
+ buildMessageFailure = true;
1646
+ return undefined;
1647
+ });
1648
+ if (emailMessages?.length) {
1649
+ if (notificationSendService.emailSendService != null) {
1650
+ let sendInstance;
1651
+ try {
1652
+ sendInstance = await notificationSendService.emailSendService.buildSendInstanceForEmailNotificationMessages(emailMessages);
1653
+ }
1654
+ catch (e) {
1655
+ console.error(`Failed building email send instance for notification "${notification.id}" with type "${notificationTemplateType}": `, e);
1656
+ es = NotificationSendState.CONFIG_ERROR;
1657
+ }
1658
+ if (sendInstance) {
1659
+ try {
1660
+ sendEmailsResult = await sendInstance();
1661
+ }
1662
+ catch (e) {
1663
+ console.error(`Failed sending email notification "${notification.id}" with type "${notificationTemplateType}": `, e);
1664
+ es = NotificationSendState.SEND_ERROR;
1665
+ }
1666
+ }
1667
+ }
1668
+ else {
1669
+ console.error(`Failed sending email notification "${notification.id}" with type "${notificationTemplateType}" due to no email service being configured.`);
1670
+ es = NotificationSendState.CONFIG_ERROR;
1671
+ }
1672
+ if (sendEmailsResult != null) {
1673
+ const { success, failed } = sendEmailsResult;
1674
+ esr = success.length ? currentEsr.concat(success.map((x) => x.toLowerCase())) : undefined;
1675
+ if (failed.length > 0) {
1676
+ es = NotificationSendState.SENT_PARTIAL;
1677
+ }
1678
+ else {
1679
+ es = NotificationSendState.SENT;
1680
+ }
1681
+ }
1682
+ }
1683
+ else {
1684
+ es = NotificationSendState.SENT;
1685
+ }
1686
+ }
1687
+ // do phone numbers
1688
+ let tsr;
1689
+ if (ts === NotificationSendState.QUEUED || ts === NotificationSendState.SENT_PARTIAL) {
1690
+ const textRecipientsAlreadySentTo = new Set(currentTsr);
1691
+ const textInputContexts = textRecipients
1692
+ .filter((x) => !textRecipientsAlreadySentTo.has(x.phoneNumber))
1693
+ .map((x) => {
1694
+ const context = {
1695
+ recipient: {
1696
+ n: x.name,
1697
+ e: x.emailAddress,
1698
+ t: x.phoneNumber
1699
+ }
1700
+ };
1701
+ return context;
1702
+ });
1703
+ const textMessages = await Promise.all(textInputContexts.map(messageFunction))
1704
+ .then(filterOutNoContentNotificationMessages)
1705
+ .catch((e) => {
1706
+ console.error(`Failed building message function for type ${notificationTemplateType}: `, e);
1707
+ buildMessageFailure = true;
1708
+ return undefined;
1709
+ });
1710
+ if (textMessages?.length) {
1711
+ if (notificationSendService.textSendService != null) {
1712
+ let sendInstance;
1713
+ try {
1714
+ sendInstance = await notificationSendService.textSendService.buildSendInstanceForTextNotificationMessages(textMessages);
1715
+ }
1716
+ catch (e) {
1717
+ console.error(`Failed building text send instance for notification "${notification.id}" with type "${notificationTemplateType}": `, e);
1718
+ ts = NotificationSendState.CONFIG_ERROR;
1719
+ }
1720
+ if (sendInstance) {
1721
+ try {
1722
+ sendTextsResult = await sendInstance();
1723
+ }
1724
+ catch (e) {
1725
+ console.error(`Failed sending text notification "${notification.id}" with type "${notificationTemplateType}": `, e);
1726
+ ts = NotificationSendState.SEND_ERROR;
1727
+ }
1728
+ }
1729
+ }
1730
+ else {
1731
+ console.error(`Failed sending text notification "${notification.id}" with type "${notificationTemplateType}" due to no text service being configured.`);
1732
+ ts = NotificationSendState.CONFIG_ERROR;
1733
+ }
1734
+ if (sendTextsResult != null) {
1735
+ const { success, failed } = sendTextsResult;
1736
+ tsr = success.length ? currentTsr.concat(success) : undefined;
1737
+ if (failed.length > 0) {
1738
+ ts = NotificationSendState.SENT_PARTIAL;
1739
+ }
1740
+ else {
1741
+ ts = NotificationSendState.SENT;
1742
+ }
1743
+ }
1744
+ }
1745
+ else {
1746
+ ts = NotificationSendState.SENT;
1747
+ }
1748
+ }
1749
+ ps = NotificationSendState.NO_TRY;
1750
+ // NOTE: FCM token management will probably done with a separate system within Notification that stores FCMs for specific users in the app. May also use UIDs to determine who got the push notificdation or not...
1751
+ // do notification summaries
1752
+ if (ns === NotificationSendState.QUEUED || ns === NotificationSendState.SENT_PARTIAL) {
1753
+ const notificationSummaryInputContexts = notificationSummaryRecipients.map((x) => {
1754
+ const context = {
1755
+ recipient: {
1756
+ n: x.name,
1757
+ s: x.notificationSummaryId
1758
+ }
1759
+ };
1760
+ return context;
1761
+ });
1762
+ const notificationSummaryMessages = await Promise.all(notificationSummaryInputContexts.map(messageFunction))
1763
+ .then(filterOutNoContentNotificationMessages)
1764
+ .catch((e) => {
1765
+ console.error(`Failed building message function for type ${notificationTemplateType}: `, e);
1766
+ buildMessageFailure = true;
1767
+ return undefined;
1768
+ });
1769
+ if (notificationSummaryMessages?.length) {
1770
+ if (notificationSendService.notificationSummarySendService != null) {
1771
+ let sendInstance;
1772
+ try {
1773
+ sendInstance = await notificationSendService.notificationSummarySendService.buildSendInstanceForNotificationSummaryMessages(notificationSummaryMessages);
1774
+ }
1775
+ catch (e) {
1776
+ console.error(`Failed building notification summary send instance for notification "${notification.id}" with type "${notificationTemplateType}": `, e);
1777
+ ns = NotificationSendState.CONFIG_ERROR;
1778
+ }
1779
+ if (sendInstance) {
1780
+ try {
1781
+ sendNotificationSummaryResult = await sendInstance();
1782
+ ns = NotificationSendState.SENT;
1783
+ }
1784
+ catch (e) {
1785
+ console.error(`Failed sending notification summary notification "${notification.id}" with type "${notificationTemplateType}": `, e);
1786
+ ns = NotificationSendState.SEND_ERROR;
1787
+ }
1788
+ }
1789
+ }
1790
+ else {
1791
+ console.error(`Failed sending notification summary notification "${notification.id}" with type "${notificationTemplateType}" due to no notification summary service being configured.`);
1792
+ ns = NotificationSendState.CONFIG_ERROR;
1793
+ }
1794
+ }
1795
+ else {
1796
+ ns = NotificationSendState.SENT;
1797
+ }
1798
+ }
1799
+ // calculate results
1800
+ const notificationTemplate = { es, ts, ps, ns, esr, tsr };
1801
+ success = notificationSendFlagsImplyIsComplete(notificationTemplate);
1802
+ if (success) {
1803
+ notificationTemplate.d = true;
1804
+ }
1805
+ else {
1806
+ notificationTemplate.a = notification.a + 1;
1807
+ if (notificationTemplate.a >= NOTIFICATION_MAX_SEND_ATTEMPTS) {
1808
+ notificationTemplate.d = true;
1809
+ }
1810
+ }
1811
+ await notificationDocument.update(notificationTemplate);
1812
+ notificationMarkedDone = notificationTemplate.d === true;
1813
+ const callbackDetails = {
1814
+ success,
1815
+ updatedSendFlags: notificationTemplate,
1816
+ sendEmailsResult,
1817
+ sendTextsResult,
1818
+ sendNotificationSummaryResult
1819
+ };
1820
+ const { onSendAttempted, onSendSuccess } = messageFunction;
1821
+ // call onSendAttempted, if one is configured
1822
+ if (onSendAttempted) {
1823
+ onSendAttemptedResult = await asPromise(onSendAttempted(callbackDetails))
1824
+ .then((value) => {
1825
+ return { value };
1826
+ })
1827
+ .catch((e) => {
1828
+ console.warn(`Caught exception while calling onSendAttempted for notification "${notification.id}" with type "${notificationTemplateType}": `, e);
1829
+ return { error: e };
1830
+ });
1831
+ }
1832
+ // call onSendSuccess, if one is configured
1833
+ if (notificationMarkedDone && onSendSuccess) {
1834
+ onSendSuccessResult = await asPromise(onSendSuccess(callbackDetails))
1835
+ .then((value) => {
1836
+ return { value };
1837
+ })
1838
+ .catch((e) => {
1839
+ console.warn(`Caught exception while calling onSendSuccess for notification "${notification.id}" with type "${notificationTemplateType}": `, e);
1840
+ return { error: e };
1841
+ });
1842
+ }
1843
+ }
1844
+ }
1845
+ else {
1846
+ switch (notification.st) {
1847
+ case NotificationSendType.SEND_IF_BOX_EXISTS:
1848
+ // deleted successfully
1849
+ success = deletedNotification;
1850
+ break;
1851
+ }
1852
+ }
1853
+ }
1854
+ }
1855
+ const result = {
1856
+ notificationTemplateType,
1857
+ isKnownTemplateType,
1858
+ isNotificationTask,
1859
+ isUniqueNotificationTask,
1860
+ uniqueNotificationTaskConflict,
1861
+ isConfiguredTemplateType,
1862
+ throttled,
1863
+ exists: notification != null,
1864
+ boxExists: notificationBox != null,
1865
+ notificationTaskPartsRunCount,
1866
+ notificationTaskLoopingProtectionTriggered,
1867
+ notificationBoxNeedsInitialization,
1868
+ notificationTaskCompletionType,
1869
+ createdBox,
1870
+ deletedNotification,
1871
+ notificationMarkedDone,
1872
+ tryRun,
1873
+ success,
1874
+ sendEmailsResult,
1875
+ sendTextsResult,
1876
+ sendNotificationSummaryResult,
1877
+ loadMessageFunctionFailure,
1878
+ buildMessageFailure,
1879
+ onSendAttemptedResult,
1880
+ onSendSuccessResult
1881
+ };
1882
+ return result;
1883
+ };
1884
+ });
1885
+ }
1886
+ const SEND_QUEUE_NOTIFICATIONS_TASK_EXCESS_THRESHOLD = 5000;
1887
+ function sendQueuedNotificationsFactory(context) {
1888
+ const { firebaseServerActionTransformFunctionFactory, notificationCollectionGroup } = context;
1889
+ const sendNotification = sendNotificationFactory(context);
1890
+ return firebaseServerActionTransformFunctionFactory(SendQueuedNotificationsParams, async (params) => {
1891
+ const { maxSendNotificationLoops } = params;
1892
+ const maxLoops = maxSendNotificationLoops ?? Number.MAX_SAFE_INTEGER;
1893
+ const sendNotificationLoopsTaskExcessThreshold = params.sendNotificationLoopsTaskExcessThreshold ?? SEND_QUEUE_NOTIFICATIONS_TASK_EXCESS_THRESHOLD;
1894
+ return async (input) => {
1895
+ const maxParallelTasks = input?.maxParellelSendTasks ?? params.maxParellelSendTasks ?? 5;
1896
+ const onSendNotificationResult = input?.onSendNotificationResult ?? mapIdentityFunction();
1897
+ let notificationLoopCount = 0;
1898
+ let notificationBoxesCreated = 0;
1899
+ let notificationsDeleted = 0;
1900
+ let notificationTasksVisited = 0;
1901
+ let notificationsVisited = 0;
1902
+ let notificationsSucceeded = 0;
1903
+ let notificationsDelayed = 0;
1904
+ let notificationsFailed = 0;
1905
+ let sendEmailsResult;
1906
+ let sendTextsResult;
1907
+ let sendNotificationSummaryResult;
1908
+ const sendNotificationParams = { key: firestoreDummyKey(), throwErrorIfSent: false };
1909
+ const sendNotificationInstance = await sendNotification(sendNotificationParams);
1910
+ let excessLoopsDetected = false;
1911
+ const sendQueuedNotifications = async () => {
1912
+ const query = notificationCollectionGroup.queryDocument(notificationsPastSendAtTimeQuery());
1913
+ const notificationDocuments = await query.getDocs();
1914
+ const result = await performAsyncTasks(notificationDocuments, async (notificationDocument) => {
1915
+ const result = await sendNotificationInstance(notificationDocument);
1916
+ onSendNotificationResult(result, notificationDocument);
1917
+ return result;
1918
+ }, {
1919
+ maxParallelTasks
1920
+ });
1921
+ return result;
1922
+ };
1923
+ // iterate through all notification items that need to be synced
1924
+ while (notificationLoopCount < maxLoops) {
1925
+ const sendQueuedNotificationsResults = await sendQueuedNotifications();
1926
+ sendQueuedNotificationsResults.results.forEach((x) => {
1927
+ const result = x[1];
1928
+ if (result.success) {
1929
+ notificationsSucceeded += 1;
1930
+ }
1931
+ else if (result.createdBox || result.notificationBoxNeedsInitialization) {
1932
+ notificationsDelayed += 1;
1933
+ }
1934
+ else {
1935
+ notificationsFailed += 1;
1936
+ }
1937
+ if (result.isNotificationTask) {
1938
+ notificationTasksVisited += 1;
1939
+ }
1940
+ if (result.deletedNotification) {
1941
+ notificationsDeleted += 1;
1942
+ }
1943
+ if (result.createdBox) {
1944
+ notificationBoxesCreated += 1;
1945
+ }
1946
+ sendEmailsResult = mergeNotificationSendMessagesResult(sendEmailsResult, result.sendEmailsResult);
1947
+ sendTextsResult = mergeNotificationSendMessagesResult(sendTextsResult, result.sendTextsResult);
1948
+ sendNotificationSummaryResult = mergeNotificationSendMessagesResult(sendNotificationSummaryResult, result.sendNotificationSummaryResult);
1949
+ });
1950
+ const found = sendQueuedNotificationsResults.results.length;
1951
+ notificationsVisited += found;
1952
+ notificationLoopCount += 1;
1953
+ if (!found) {
1954
+ break;
1955
+ }
1956
+ else if (!excessLoopsDetected && notificationLoopCount > sendNotificationLoopsTaskExcessThreshold) {
1957
+ excessLoopsDetected = true;
1958
+ console.error(`sendQueuedNotifications(EXCESS_LOOPS_DETECTED): Exceeded send notification loops task excess threshold of ${sendNotificationLoopsTaskExcessThreshold}.`);
1959
+ // continue the loops
1960
+ }
1961
+ }
1962
+ const result = {
1963
+ excessLoopsDetected,
1964
+ notificationLoopCount,
1965
+ notificationBoxesCreated,
1966
+ notificationsDeleted,
1967
+ notificationTasksVisited,
1968
+ notificationsVisited,
1969
+ notificationsSucceeded,
1970
+ notificationsDelayed,
1971
+ notificationsFailed,
1972
+ sendEmailsResult,
1973
+ sendTextsResult,
1974
+ sendNotificationSummaryResult
1975
+ };
1976
+ return result;
1977
+ };
1978
+ });
1979
+ }
1980
+ function cleanupSentNotificationsFactory(context) {
1981
+ const { firestoreContext, firebaseServerActionTransformFunctionFactory, notificationCollectionGroup, notificationBoxCollection, notificationWeekCollectionFactory } = context;
1982
+ return firebaseServerActionTransformFunctionFactory(CleanupSentNotificationsParams, async () => {
1983
+ return async () => {
1984
+ let notificationBoxesUpdatesCount = 0;
1985
+ let notificationsDeleted = 0;
1986
+ const notificationTasksDeletedCount = 0;
1987
+ let notificationWeeksCreated = 0;
1988
+ let notificationWeeksUpdated = 0;
1989
+ // iterate through all Notification items that need to be cleaned up
1990
+ while (true) {
1991
+ const cleanupSentNotificationsResults = await cleanupSentNotifications();
1992
+ cleanupSentNotificationsResults.results.forEach((x) => {
1993
+ const { itemsDeleted, weeksCreated, weeksUpdated } = x[1];
1994
+ notificationsDeleted += itemsDeleted;
1995
+ notificationWeeksCreated += weeksCreated;
1996
+ notificationWeeksUpdated += weeksUpdated;
1997
+ });
1998
+ const notificationBoxesUpdated = cleanupSentNotificationsResults.results.length;
1999
+ notificationBoxesUpdatesCount += notificationBoxesUpdated;
2000
+ if (!notificationBoxesUpdated) {
2001
+ break;
2002
+ }
2003
+ }
2004
+ async function cleanupSentNotifications() {
2005
+ const query = notificationCollectionGroup.queryDocument(notificationsReadyForCleanupQuery());
2006
+ const notificationDocuments = await query.getDocs();
2007
+ const notificationDocumentsGroupedByNotificationBox = Array.from(makeValuesGroupMap(notificationDocuments, (x) => x.parent.id).values());
2008
+ const result = await performAsyncTasks(notificationDocumentsGroupedByNotificationBox, async (notificationDocumentsInSameBox) => {
2009
+ const allPairs = await getDocumentSnapshotDataPairs(notificationDocumentsInSameBox);
2010
+ const allPairsWithDataAndMarkedDeleted = allPairs.filter((x) => x.data?.d);
2011
+ const { included: taskPairsWithDataAndMarkedDeleted, excluded: normalPairsWithDataAndMarkedDeleted } = separateValues(allPairsWithDataAndMarkedDeleted, (x) => x.data?.st === NotificationSendType.TASK_NOTIFICATION);
2012
+ const pairsGroupedByWeek = Array.from(makeValuesGroupMap(normalPairsWithDataAndMarkedDeleted, (x) => yearWeekCode(x.data.sat)).entries());
2013
+ // batch incase there are a lot of new notifications to move to week
2014
+ const pairsGroupedByWeekInBatches = pairsGroupedByWeek
2015
+ .map((x) => {
2016
+ const batches = batch(x[1], 40);
2017
+ return batches.map((batch) => [x[0], batch]);
2018
+ })
2019
+ .flat();
2020
+ const notificationBoxDocument = await notificationBoxCollection.documentAccessor().loadDocument(notificationDocumentsInSameBox[0].parent);
2021
+ // create/update the NotificationWeek
2022
+ const notificationWeekResults = await performAsyncTasks(pairsGroupedByWeekInBatches, async ([yearWeekCode, notificationDocumentsInSameWeek]) => {
2023
+ return firestoreContext.runTransaction(async (transaction) => {
2024
+ const notificationWeekDocument = notificationWeekCollectionFactory(notificationBoxDocument).documentAccessorForTransaction(transaction).loadDocumentForId(`${yearWeekCode}`);
2025
+ const notificationDocumentsInTransaction = loadDocumentsForDocumentReferencesFromValues(notificationCollectionGroup.documentAccessorForTransaction(transaction), notificationDocumentsInSameWeek, (x) => x.snapshot.ref);
2026
+ const notificationWeek = await notificationWeekDocument.snapshotData();
2027
+ const newItems = filterMaybeArrayValues(notificationDocumentsInSameWeek.map((x) => {
2028
+ const data = x.data;
2029
+ const shouldSaveToNotificationWeek = shouldSaveNotificationToNotificationWeek(data);
2030
+ return shouldSaveToNotificationWeek ? data.n : undefined;
2031
+ }));
2032
+ const n = [...(notificationWeek?.n ?? []), ...newItems];
2033
+ if (!notificationWeek) {
2034
+ // create
2035
+ await notificationWeekDocument.create({
2036
+ w: yearWeekCode,
2037
+ n
2038
+ });
2039
+ }
2040
+ else {
2041
+ // update
2042
+ await notificationWeekDocument.update({
2043
+ n
2044
+ });
2045
+ }
2046
+ // delete the notification items
2047
+ await Promise.all(notificationDocumentsInTransaction.map((x) => x.accessor.delete()));
2048
+ return {
2049
+ created: !notificationWeek
2050
+ };
2051
+ });
2052
+ });
2053
+ // delete all the task notifications
2054
+ const writeBatch = firestoreContext.batch();
2055
+ const writeBatchAccessor = notificationCollectionGroup.documentAccessorForTransaction(writeBatch);
2056
+ await Promise.all(taskPairsWithDataAndMarkedDeleted.map((x) => writeBatchAccessor.loadDocumentFrom(x.document).accessor.delete()));
2057
+ await writeBatch.commit();
2058
+ let weeksCreated = 0;
2059
+ let weeksUpdated = 0;
2060
+ const tasksDeleted = taskPairsWithDataAndMarkedDeleted.length;
2061
+ notificationWeekResults.results.forEach((x) => {
2062
+ if (x[1].created) {
2063
+ weeksCreated += 1;
2064
+ }
2065
+ else {
2066
+ weeksUpdated += 1;
2067
+ }
2068
+ });
2069
+ const result = {
2070
+ weeksCreated,
2071
+ weeksUpdated,
2072
+ itemsDeleted: allPairsWithDataAndMarkedDeleted.length,
2073
+ tasksDeleted
2074
+ };
2075
+ return result;
2076
+ }, {
2077
+ maxParallelTasks: 10
2078
+ });
2079
+ return result;
2080
+ }
2081
+ const result = {
2082
+ notificationBoxesUpdatesCount,
2083
+ notificationTasksDeletedCount,
2084
+ notificationsDeleted,
2085
+ notificationWeeksCreated,
2086
+ notificationWeeksUpdated
2087
+ };
2088
+ return result;
2089
+ };
2090
+ });
2091
+ }
2092
+
2093
+ // MARK: NotificationInitServerActionsContextConfig
2094
+ /**
2095
+ * Token to access/override the NotificationTemplateService's defaults records.
2096
+ */
2097
+ const NOTIFICATION_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN = 'NOTIFICATION_INIT_SERVER_ACTIONS_CONTEXT_CONFIG';
2098
+ const MAKE_TEMPLATE_FOR_NOTIFICATION_RELATED_MODEL_INITIALIZATION_FUNCTION_DELETE_RESPONSE = false;
2099
+ class NotificationInitServerActions {
2100
+ }
2101
+ function notificationInitServerActions(context) {
2102
+ return {
2103
+ initializeNotificationBox: initializeNotificationBoxFactory(context),
2104
+ initializeAllApplicableNotificationBoxes: initializeAllApplicableNotificationBoxesFactory(context),
2105
+ initializeNotificationSummary: initializeNotificationSummaryFactory(context),
2106
+ initializeAllApplicableNotificationSummaries: initializeAllApplicableNotificationSummariesFactory(context)
2107
+ };
2108
+ }
2109
+ async function initializeNotificationModelInTransaction(input) {
2110
+ const { makeTemplateFunction, throwErrorIfAlreadyInitialized, transaction, document: documentInTransaction, data: notificationBox } = input;
2111
+ let initialized = false;
2112
+ const alreadyInitialized = !notificationBox.s;
2113
+ if (!alreadyInitialized) {
2114
+ const flatModelKey = documentInTransaction.id;
2115
+ const modelKey = inferKeyFromTwoWayFlatFirestoreModelKey(flatModelKey);
2116
+ const modelCollectionName = firestoreModelKeyCollectionName(modelKey);
2117
+ const input = {
2118
+ transaction,
2119
+ flatModelKey,
2120
+ modelKey,
2121
+ collectionName: modelCollectionName
2122
+ };
2123
+ const template = await makeTemplateFunction(input);
2124
+ if (template === false) {
2125
+ await documentInTransaction.accessor.delete();
2126
+ }
2127
+ else if (template == null) {
2128
+ await documentInTransaction.update({
2129
+ s: false, // set false when "f" is set true
2130
+ fi: true
2131
+ });
2132
+ }
2133
+ else {
2134
+ initialized = true;
2135
+ await documentInTransaction.update({
2136
+ //
2137
+ ...template,
2138
+ m: undefined, // should not be changed
2139
+ s: null, // is now initialized.
2140
+ fi: false // set false
2141
+ });
2142
+ }
2143
+ }
2144
+ else if (throwErrorIfAlreadyInitialized) {
2145
+ throw notificationModelAlreadyInitializedError();
2146
+ }
2147
+ return {
2148
+ initialized,
2149
+ alreadyInitialized
2150
+ };
2151
+ }
2152
+ function initializeNotificationBoxInTransactionFactory(context) {
2153
+ const { notificationBoxCollection, makeTemplateForNotificationBoxInitialization } = context;
2154
+ return async (params, notificationBoxDocument, transaction) => {
2155
+ const { throwErrorIfAlreadyInitialized } = params;
2156
+ const notificationBoxDocumentInTransaction = notificationBoxCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(notificationBoxDocument);
2157
+ const notificationBox = await assertSnapshotData(notificationBoxDocumentInTransaction);
2158
+ return initializeNotificationModelInTransaction({
2159
+ makeTemplateFunction: makeTemplateForNotificationBoxInitialization,
2160
+ throwErrorIfAlreadyInitialized,
2161
+ transaction,
2162
+ document: notificationBoxDocumentInTransaction,
2163
+ data: notificationBox
2164
+ });
2165
+ };
2166
+ }
2167
+ function initializeNotificationBoxFactory(context) {
2168
+ const { firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
2169
+ const initializeNotificationBoxInTransaction = initializeNotificationBoxInTransactionFactory(context);
2170
+ return firebaseServerActionTransformFunctionFactory(InitializeNotificationModelParams, async (params) => {
2171
+ return async (notificationBoxDocument) => {
2172
+ await firestoreContext.runTransaction((transaction) => initializeNotificationBoxInTransaction(params, notificationBoxDocument, transaction));
2173
+ return notificationBoxDocument;
2174
+ };
2175
+ });
2176
+ }
2177
+ function initializeAllApplicableNotificationBoxesFactory(context) {
2178
+ const { firestoreContext, firebaseServerActionTransformFunctionFactory, notificationBoxCollection, notificationCollectionGroup } = context;
2179
+ const initializeNotificationBoxInTransaction = initializeNotificationBoxInTransactionFactory(context);
2180
+ return firebaseServerActionTransformFunctionFactory(InitializeAllApplicableNotificationBoxesParams, async () => {
2181
+ return async () => {
2182
+ let notificationBoxesVisited = 0;
2183
+ let notificationBoxesSucceeded = 0;
2184
+ let notificationBoxesFailed = 0;
2185
+ let notificationBoxesAlreadyInitialized = 0;
2186
+ const initializeNotificationBoxParams = { key: firestoreDummyKey(), throwErrorIfAlreadyInitialized: false };
2187
+ async function initializeNotificationBoxes() {
2188
+ const query = notificationBoxCollection.queryDocument(notificationBoxesFlaggedForNeedsInitializationQuery());
2189
+ const notificationBoxDocuments = await query.getDocs();
2190
+ const result = await performAsyncTasks(notificationBoxDocuments, async (notificationBoxDocument) => {
2191
+ return firestoreContext.runTransaction((transaction) => initializeNotificationBoxInTransaction(initializeNotificationBoxParams, notificationBoxDocument, transaction));
2192
+ }, {
2193
+ maxParallelTasks: 5
2194
+ });
2195
+ return result;
2196
+ }
2197
+ // iterate through all NotificationBox items that need to be synced
2198
+ while (true) {
2199
+ const initializeNotificationBoxesResults = await initializeNotificationBoxes();
2200
+ initializeNotificationBoxesResults.results.forEach((x) => {
2201
+ const result = x[1];
2202
+ if (result.alreadyInitialized) {
2203
+ notificationBoxesAlreadyInitialized += 1;
2204
+ }
2205
+ else if (result.initialized) {
2206
+ notificationBoxesSucceeded += 1;
2207
+ }
2208
+ else {
2209
+ notificationBoxesFailed += 1;
2210
+ }
2211
+ });
2212
+ const found = initializeNotificationBoxesResults.results.length;
2213
+ notificationBoxesVisited += found;
2214
+ if (!found) {
2215
+ break;
2216
+ }
2217
+ }
2218
+ const result = {
2219
+ notificationBoxesVisited,
2220
+ notificationBoxesSucceeded,
2221
+ notificationBoxesFailed,
2222
+ notificationBoxesAlreadyInitialized
2223
+ };
2224
+ return result;
2225
+ };
2226
+ });
2227
+ }
2228
+ function initializeNotificationSummaryInTransactionFactory(context) {
2229
+ const { notificationSummaryCollection, makeTemplateForNotificationSummaryInitialization } = context;
2230
+ return async (params, notificationSummaryDocument, transaction) => {
2231
+ const { throwErrorIfAlreadyInitialized } = params;
2232
+ const notificationSummaryDocumentInTransaction = notificationSummaryCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(notificationSummaryDocument);
2233
+ const notificationSummary = await assertSnapshotData(notificationSummaryDocumentInTransaction);
2234
+ return initializeNotificationModelInTransaction({
2235
+ makeTemplateFunction: makeTemplateForNotificationSummaryInitialization,
2236
+ throwErrorIfAlreadyInitialized,
2237
+ transaction,
2238
+ document: notificationSummaryDocumentInTransaction,
2239
+ data: notificationSummary
2240
+ });
2241
+ };
2242
+ }
2243
+ function initializeNotificationSummaryFactory(context) {
2244
+ const { firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
2245
+ const initializeNotificationSummaryInTransaction = initializeNotificationSummaryInTransactionFactory(context);
2246
+ return firebaseServerActionTransformFunctionFactory(InitializeNotificationModelParams, async (params) => {
2247
+ return async (notificationSummaryDocument) => {
2248
+ await firestoreContext.runTransaction((transaction) => initializeNotificationSummaryInTransaction(params, notificationSummaryDocument, transaction));
2249
+ return notificationSummaryDocument;
2250
+ };
2251
+ });
2252
+ }
2253
+ function initializeAllApplicableNotificationSummariesFactory(context) {
2254
+ const { firestoreContext, firebaseServerActionTransformFunctionFactory, notificationSummaryCollection, notificationCollectionGroup } = context;
2255
+ const initializeNotificationSummaryInTransaction = initializeNotificationSummaryInTransactionFactory(context);
2256
+ return firebaseServerActionTransformFunctionFactory(InitializeAllApplicableNotificationSummariesParams, async () => {
2257
+ return async () => {
2258
+ let notificationSummariesVisited = 0;
2259
+ let notificationSummariesSucceeded = 0;
2260
+ let notificationSummariesFailed = 0;
2261
+ let notificationSummariesAlreadyInitialized = 0;
2262
+ const initializeNotificationSummaryParams = { key: firestoreDummyKey(), throwErrorIfAlreadyInitialized: false };
2263
+ async function initializeNotificationSummaries() {
2264
+ const query = notificationSummaryCollection.queryDocument(notificationSummariesFlaggedForNeedsInitializationQuery());
2265
+ const notificationSummaryDocuments = await query.getDocs();
2266
+ const result = await performAsyncTasks(notificationSummaryDocuments, async (notificationSummaryDocument) => {
2267
+ return firestoreContext.runTransaction((transaction) => initializeNotificationSummaryInTransaction(initializeNotificationSummaryParams, notificationSummaryDocument, transaction));
2268
+ }, {
2269
+ maxParallelTasks: 5
2270
+ });
2271
+ return result;
2272
+ }
2273
+ // iterate through all NotificationSummary items that need to be synced
2274
+ while (true) {
2275
+ const initializeNotificationSummariesResults = await initializeNotificationSummaries();
2276
+ initializeNotificationSummariesResults.results.forEach((x) => {
2277
+ const result = x[1];
2278
+ if (result.alreadyInitialized) {
2279
+ notificationSummariesAlreadyInitialized += 1;
2280
+ }
2281
+ else if (result.initialized) {
2282
+ notificationSummariesSucceeded += 1;
2283
+ }
2284
+ else {
2285
+ notificationSummariesFailed += 1;
2286
+ }
2287
+ });
2288
+ const found = initializeNotificationSummariesResults.results.length;
2289
+ notificationSummariesVisited += found;
2290
+ if (!found) {
2291
+ break;
2292
+ }
2293
+ }
2294
+ const result = {
2295
+ notificationSummariesVisited,
2296
+ notificationSummariesSucceeded,
2297
+ notificationSummariesFailed,
2298
+ notificationSummariesAlreadyInitialized
2299
+ };
2300
+ return result;
2301
+ };
2302
+ });
2303
+ }
2304
+
2305
+ /******************************************************************************
2306
+ Copyright (c) Microsoft Corporation.
2307
+
2308
+ Permission to use, copy, modify, and/or distribute this software for any
2309
+ purpose with or without fee is hereby granted.
2310
+
2311
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
2312
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
2313
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
2314
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
2315
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
2316
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
2317
+ PERFORMANCE OF THIS SOFTWARE.
2318
+ ***************************************************************************** */
2319
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
2320
+
2321
+
2322
+ function __decorate(decorators, target, key, desc) {
2323
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2324
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2325
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2326
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2327
+ }
2328
+
2329
+ function __param(paramIndex, decorator) {
2330
+ return function (target, key) { decorator(target, key, paramIndex); }
2331
+ }
2332
+
2333
+ function __metadata(metadataKey, metadataValue) {
2334
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
2335
+ }
2336
+
2337
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
2338
+ var e = new Error(message);
2339
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
2340
+ };
2341
+
2342
+ // MARK: Tokens
2343
+ /**
2344
+ * Token to access/override the NotificationTemplateService's defaults records.
2345
+ */
2346
+ const NOTIFICATION_TEMPLATE_SERVICE_DEFAULTS_OVERRIDE_TOKEN = 'NOTIFICATION_TEMPLATE_SERVICE_DEFAULTS_OVERRIDE';
2347
+ /**
2348
+ * Token to access the NotificationTemplateService's type configs array.
2349
+ */
2350
+ const NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY_TOKEN = 'NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY';
2351
+
2352
+ /**
2353
+ * Service dedicated to providing access to NotificationMessageFunctionFactory values for specific NotificationTemplateTypes.
2354
+ */
2355
+ let NotificationTemplateService = class NotificationTemplateService {
2356
+ _defaults;
2357
+ _config;
2358
+ constructor(_inputDefaults, _inputConfigs) {
2359
+ this._defaults = _inputDefaults ?? {};
2360
+ this._config = new Map();
2361
+ if (_inputConfigs != null) {
2362
+ _inputConfigs.forEach((x) => {
2363
+ this._config.set(x.type, x);
2364
+ });
2365
+ }
2366
+ }
2367
+ configPairForType(type) {
2368
+ return [this._defaults[type], this._config.get(type)];
2369
+ }
2370
+ templateInstanceForType(type) {
2371
+ return notificationTemplateServiceInstance(this, type);
2372
+ }
2373
+ };
2374
+ NotificationTemplateService = __decorate([
2375
+ __param(0, Optional()),
2376
+ __param(0, Inject(NOTIFICATION_TEMPLATE_SERVICE_DEFAULTS_OVERRIDE_TOKEN)),
2377
+ __param(1, Inject(NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY_TOKEN)),
2378
+ __metadata("design:paramtypes", [Object, Object])
2379
+ ], NotificationTemplateService);
2380
+ /**
2381
+ * Creates a NotificationTemplateServiceInstance.
2382
+ *
2383
+ * @param service
2384
+ * @param type
2385
+ * @returns
2386
+ */
2387
+ function notificationTemplateServiceInstance(service, type) {
2388
+ const pair = service.configPairForType(type);
2389
+ const isKnownType = pair[0] != null || pair[1] != null;
2390
+ const defaultFactory = noContentNotificationMessageFunctionFactory();
2391
+ const instanceConfig = pair[1];
2392
+ return {
2393
+ service,
2394
+ type,
2395
+ isConfiguredType: isKnownType,
2396
+ loadMessageFunction: async (config) => {
2397
+ const factory = instanceConfig?.factory ?? defaultFactory;
2398
+ return factory(config);
2399
+ }
2400
+ };
2401
+ }
2402
+
2403
+ /**
2404
+ * Alternative version of createNotificationDocument() that checks if the document exists, and can run it if it does instead of recreated it.
2405
+ *
2406
+ * Does not support the use of a Transaction, as running should occur outside of a transaction.
2407
+ *
2408
+ * @param input
2409
+ * @returns
2410
+ */
2411
+ async function createOrRunUniqueNotificationDocument(input) {
2412
+ const { expediteService, expediteInstance, updateNextRunAtTime, now: inputNow } = input;
2413
+ let sat = input.template.sat;
2414
+ if (updateNextRunAtTime != null) {
2415
+ sat = updateNextRunAtTime === true ? (inputNow ?? new Date()) : updateNextRunAtTime;
2416
+ }
2417
+ const pair = createNotificationDocumentPair({
2418
+ ...input,
2419
+ template: {
2420
+ ...input.template,
2421
+ sat
2422
+ }
2423
+ });
2424
+ if (!pair.notification.ut) {
2425
+ throw new Error('createOrRunUniqueNotificationDocument(): Notification is not flagged as unique.');
2426
+ }
2427
+ const pairExists = await pair.notificationDocument.exists();
2428
+ let result = {
2429
+ ...pair,
2430
+ notificationCreated: false
2431
+ };
2432
+ async function runNotificationTask() {
2433
+ if (expediteService != null) {
2434
+ result.runResult = await expediteService.sendNotification(pair.notificationDocument, input.sendNotificationOptions);
2435
+ }
2436
+ else if (expediteInstance != null) {
2437
+ expediteInstance.enqueue(pair.notificationDocument);
2438
+ result.runEnqueued = true;
2439
+ }
2440
+ else if (!result.notificationCreated && updateNextRunAtTime != null) {
2441
+ await pair.notificationDocument.update({
2442
+ sat: sat
2443
+ });
2444
+ }
2445
+ }
2446
+ if (pairExists) {
2447
+ await runNotificationTask();
2448
+ }
2449
+ else {
2450
+ result = await _createNotificationDocumentFromPair(input, pair);
2451
+ if (result.notificationCreated && input.runImmediatelyIfCreated) {
2452
+ await runNotificationTask();
2453
+ }
2454
+ }
2455
+ return result;
2456
+ }
2457
+
2458
+ /**
2459
+ * Service dedicated to providing access to NotificationMessageFunctionFactory values for specific NotificationTemplateTypes.
2460
+ */
2461
+ class NotificationSendService {
2462
+ }
2463
+
2464
+ /**
2465
+ * Service dedicated to providing access to NotificationMessageFunctionFactory values for specific NotificationTemplateTypes.
2466
+ */
2467
+ class NotificationTaskService {
2468
+ }
2469
+
2470
+ /**
2471
+ * Interface for a service that allows access to a NotificationServerActions instance and "expediting" the sending of notification(s) that should be emitted immediately for timeliness.
2472
+ *
2473
+ * @see MutableNotificationExpediteService is the default implementation.
2474
+ */
2475
+ class NotificationExpediteService {
2476
+ }
2477
+ /**
2478
+ * Creates a new NotificationExpediteServiceInstance with the input NotificationExpediteService.
2479
+
2480
+ * @param notificationExpediteService
2481
+ * @returns
2482
+ */
2483
+ function notificationExpediteServiceInstance(notificationExpediteService) {
2484
+ let _documentsToSend = [];
2485
+ const initialize = () => {
2486
+ _documentsToSend = []; // resets the documents to send
2487
+ };
2488
+ const enqueue = (notificationDocument) => {
2489
+ _documentsToSend.push(notificationDocument);
2490
+ };
2491
+ const enqueueCreateResult = (createResult) => {
2492
+ let enqueued = false;
2493
+ if (createResult.notificationDocument) {
2494
+ enqueue(createResult.notificationDocument);
2495
+ enqueued = true;
2496
+ }
2497
+ return enqueued;
2498
+ };
2499
+ const send = async (options) => {
2500
+ const results = await runAsyncTasksForValues(_documentsToSend, (x) => notificationExpediteService.sendNotification(x, options), {
2501
+ nonConcurrentTaskKeyFactory: (x) => x.parent.id // only send one notification at a time for a notification box
2502
+ });
2503
+ return results;
2504
+ };
2505
+ return {
2506
+ initialize,
2507
+ enqueue,
2508
+ enqueueCreateResult,
2509
+ send
2510
+ };
2511
+ }
2512
+ // MARK: Implementation
2513
+ /**
2514
+ * Service used to "expedite" the sending of a specific notification.
2515
+ *
2516
+ * Because the NotificationActionService is typically created after other action services are due to the dependency injection graph, this service is
2517
+ * created before the NotificationActionService is created, and then later updated by the NotificationActionService.
2518
+ *
2519
+ * It is best provided by provideMutableNotificationExpediteService() as a global provider.
2520
+ */
2521
+ let MutableNotificationExpediteService = class MutableNotificationExpediteService {
2522
+ _notificationServerActions;
2523
+ /**
2524
+ * Returns the configured NotificationServerActions instance.
2525
+ */
2526
+ getNotificationServerActions() {
2527
+ return this._notificationServerActions;
2528
+ }
2529
+ /**
2530
+ * Sets the NotificationServerActions instance to use.
2531
+ */
2532
+ setNotificationServerActions(notificationServerActions) {
2533
+ this._notificationServerActions = notificationServerActions;
2534
+ }
2535
+ async sendNotification(notificationDocument, options) {
2536
+ const sendNotification = await this._notificationServerActions.sendNotification({ key: firestoreDummyKey(), ...options });
2537
+ return sendNotification(notificationDocument);
2538
+ }
2539
+ expediteInstance() {
2540
+ return notificationExpediteServiceInstance(this);
2541
+ }
2542
+ };
2543
+ MutableNotificationExpediteService = __decorate([
2544
+ Injectable()
2545
+ ], MutableNotificationExpediteService);
2546
+ // MARK: Providers
2547
+ /**
2548
+ * Provides an instance of MutableNotificationExpediteService and NotificationExpediteService.
2549
+ *
2550
+ * This should generally be used in the global module of an app.
2551
+ */
2552
+ function provideMutableNotificationExpediteService() {
2553
+ return [
2554
+ MutableNotificationExpediteService,
2555
+ {
2556
+ provide: NotificationExpediteService,
2557
+ useExisting: MutableNotificationExpediteService
2558
+ }
2559
+ ];
2560
+ }
2561
+ /**
2562
+ * Convenience function that exports NotificationExpediteService and MutableNotificationExpediteService.
2563
+ *
2564
+ * This should generally be used in the global module of an app.
2565
+ */
2566
+ function exportMutableNotificationExpediteService() {
2567
+ return [NotificationExpediteService, MutableNotificationExpediteService];
2568
+ }
2569
+
2570
+ // MARK: Provider Factories
2571
+ function notificationServerActionsContextFactory(context, notificationTemplateService, notificationSendService, notificationTaskService, notificationsExpediteService) {
2572
+ return { ...context, notificationTemplateService, notificationSendService, notificationTaskService, notificationsExpediteService };
2573
+ }
2574
+ function notificationServerActionsFactory(context, mutableNotificationExpediteService) {
2575
+ return notificationServerActions(context);
2576
+ }
2577
+ function notificationInitServerActionsFactory(context, notificationInitServerActionsContextConfig) {
2578
+ return notificationInitServerActions({
2579
+ ...context,
2580
+ ...notificationInitServerActionsContextConfig
2581
+ });
2582
+ }
2583
+ /**
2584
+ * Convenience function used to generate ModuleMetadata for an app's NotificationModule.
2585
+ *
2586
+ * By default this module exports:
2587
+ * - NotificationServerActionContext (NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN)
2588
+ * - NotificationTemplateService
2589
+ * - NotificationServerActions
2590
+ * - NotificationInitServerActions
2591
+ * - NotificationExpediteService (MutableNotificationExpediteService is used as the existing, but it is not re-exported)
2592
+ *
2593
+ * Be sure the class that delares the module using this function also extends AbstractAppNotificationModule.
2594
+ *
2595
+ * @param provide
2596
+ * @param useFactory
2597
+ * @returns
2598
+ */
2599
+ function appNotificationModuleMetadata(config) {
2600
+ const { dependencyModule, imports, exports: exports$1, providers } = config;
2601
+ const dependencyModuleImport = dependencyModule ? [dependencyModule] : [];
2602
+ return {
2603
+ imports: [ConfigModule, ...dependencyModuleImport, ...(imports ?? [])],
2604
+ exports: [NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN, NotificationExpediteService, NotificationTemplateService, NotificationServerActions, NotificationInitServerActions, ...(exports$1 ?? [])],
2605
+ providers: [
2606
+ {
2607
+ provide: NotificationExpediteService,
2608
+ useExisting: MutableNotificationExpediteService
2609
+ },
2610
+ {
2611
+ provide: NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN,
2612
+ useFactory: notificationServerActionsContextFactory,
2613
+ inject: [BASE_NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN, NotificationTemplateService, NotificationSendService, NotificationTaskService, NotificationExpediteService]
2614
+ },
2615
+ {
2616
+ provide: NotificationTemplateService,
2617
+ useClass: NotificationTemplateService
2618
+ },
2619
+ {
2620
+ provide: NotificationServerActions,
2621
+ useFactory: notificationServerActionsFactory,
2622
+ inject: [NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN, NotificationExpediteService]
2623
+ },
2624
+ {
2625
+ provide: NotificationInitServerActions,
2626
+ useFactory: notificationInitServerActionsFactory,
2627
+ inject: [NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN, NOTIFICATION_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN]
2628
+ },
2629
+ ...(providers ?? [])
2630
+ ]
2631
+ };
2632
+ }
2633
+ /**
2634
+ * Abstract module that should be extended when using appNotificationModuleMetadata.
2635
+ */
2636
+ let AbstractAppNotificationModule = class AbstractAppNotificationModule {
2637
+ constructor(mutableNotificationExpediteService, actions) {
2638
+ mutableNotificationExpediteService.setNotificationServerActions(actions);
2639
+ }
2640
+ };
2641
+ AbstractAppNotificationModule = __decorate([
2642
+ Module({}),
2643
+ __param(0, Inject(MutableNotificationExpediteService)),
2644
+ __param(1, Inject(NotificationServerActions)),
2645
+ __metadata("design:paramtypes", [MutableNotificationExpediteService, NotificationServerActions])
2646
+ ], AbstractAppNotificationModule);
2647
+ /**
2648
+ * Pre-configured global provider for MutableNotificationExpediteService/NotificationExpediteService.
2649
+ */
2650
+ let GlobalNotificationModule = class GlobalNotificationModule {
2651
+ };
2652
+ GlobalNotificationModule = __decorate([
2653
+ Global(),
2654
+ Module({
2655
+ providers: provideMutableNotificationExpediteService(),
2656
+ exports: exportMutableNotificationExpediteService()
2657
+ })
2658
+ ], GlobalNotificationModule);
2659
+
2660
+ function firestoreNotificationSummarySendService(config) {
2661
+ const { context, allowCreateNotificationSummaries: inputAllowCreateNotificationSummaries } = config;
2662
+ const { firestoreContext, notificationSummaryCollection } = context;
2663
+ const allowCreateNotificationSummaries = inputAllowCreateNotificationSummaries ?? true;
2664
+ const sendService = {
2665
+ async buildSendInstanceForNotificationSummaryMessages(notificationMessages) {
2666
+ const messagesGroupedByNotificationSummaryMapBuilder = multiValueMapBuilder();
2667
+ notificationMessages.forEach((x) => {
2668
+ if (x.inputContext.recipient.s != null && x.item != null) {
2669
+ // only add to map builder if recipient id is defined
2670
+ messagesGroupedByNotificationSummaryMapBuilder.add(x.inputContext.recipient.s, x);
2671
+ }
2672
+ });
2673
+ const cutSubject = cutStringFunction({ maxLength: NOTIFICATION_SUMMARY_EMBEDDED_NOTIFICATION_ITEM_SUBJECT_MAX_LENGTH });
2674
+ const cutMessage = cutStringFunction({ maxLength: NOTIFICATION_SUMMARY_EMBEDDED_NOTIFICATION_ITEM_MESSAGE_MAX_LENGTH });
2675
+ const messagesGroups = messagesGroupedByNotificationSummaryMapBuilder.entries();
2676
+ return async () => {
2677
+ const success = [];
2678
+ const failed = [];
2679
+ const ignored = [];
2680
+ await runAsyncTasksForValues(messagesGroups, async ([notificationSummaryId, messages]) => {
2681
+ await firestoreContext
2682
+ .runTransaction(async (transaction) => {
2683
+ const notificationSummaryDocument = notificationSummaryCollection.documentAccessorForTransaction(transaction).loadDocumentForId(notificationSummaryId);
2684
+ const notificationSummary = await notificationSummaryDocument.snapshotData();
2685
+ let updated = false;
2686
+ let updateTemplate;
2687
+ const existingMessages = notificationSummary?.n ?? [];
2688
+ const existingMessageIds = new Set(existingMessages.map((x) => x.id));
2689
+ // ignore any repeat messages
2690
+ const messagesToSend = messages.filter((x) => !existingMessageIds.has(x.item.id));
2691
+ if (messagesToSend.length > 0) {
2692
+ // add the new items to existing n, then keep the last 1000
2693
+ const sortedN = existingMessages
2694
+ .concat(messagesToSend.map((x) => {
2695
+ let message = '';
2696
+ if (x.content.openingMessage) {
2697
+ message = x.content.openingMessage;
2698
+ }
2699
+ if (x.content.closingMessage) {
2700
+ message = (message ? message + '\n\n' : message) + x.content.closingMessage;
2701
+ }
2702
+ const item = {
2703
+ ...x.item,
2704
+ s: cutSubject(x.content.title),
2705
+ g: cutMessage(message)
2706
+ };
2707
+ return item;
2708
+ }))
2709
+ .sort(sortNotificationItemsFunction);
2710
+ const n = takeLast(sortedN, NOTIFICATION_SUMMARY_ITEM_LIMIT);
2711
+ updateTemplate = {
2712
+ n,
2713
+ lat: new Date()
2714
+ };
2715
+ }
2716
+ if (updateTemplate != null) {
2717
+ if (notificationSummary != null) {
2718
+ await notificationSummaryDocument.update(updateTemplate);
2719
+ updated = true;
2720
+ }
2721
+ else if (allowCreateNotificationSummaries) {
2722
+ // if it does not exist, and we are allowed to create new summaries, create it and add the new notifications
2723
+ const createTemplate = {
2724
+ ...makeNewNotificationSummaryTemplate(inferKeyFromTwoWayFlatFirestoreModelKey(notificationSummaryId)),
2725
+ ...updateTemplate
2726
+ };
2727
+ await notificationSummaryDocument.create(createTemplate);
2728
+ updated = true;
2729
+ }
2730
+ }
2731
+ return updated;
2732
+ })
2733
+ .then((updated) => {
2734
+ if (updated) {
2735
+ success.push(notificationSummaryId);
2736
+ }
2737
+ else {
2738
+ ignored.push(notificationSummaryId);
2739
+ }
2740
+ })
2741
+ .catch((e) => {
2742
+ console.error('firestoreNotificationSummarySendService(): failed updating notification summary', e);
2743
+ failed.push(notificationSummaryId);
2744
+ });
2745
+ });
2746
+ const sendResult = {
2747
+ success,
2748
+ failed,
2749
+ ignored
2750
+ };
2751
+ return sendResult;
2752
+ };
2753
+ }
2754
+ };
2755
+ return sendService;
2756
+ }
2757
+
2758
+ /**
2759
+ * NotificationTextSendService that ignores sending all messages.
2760
+ *
2761
+ * This is useful for cases where your app may eventually want to send text notifications and want the rest of your app configured like it currently does.
2762
+ *
2763
+ * @returns
2764
+ */
2765
+ function ignoreSendNotificationTextSendService() {
2766
+ const sendService = {
2767
+ async buildSendInstanceForTextNotificationMessages(notificationMessages) {
2768
+ return async () => {
2769
+ const success = [];
2770
+ const failed = [];
2771
+ const ignored = notificationMessages.map((x) => x.inputContext.recipient.t);
2772
+ const sendResult = {
2773
+ success,
2774
+ failed,
2775
+ ignored
2776
+ };
2777
+ return sendResult;
2778
+ };
2779
+ }
2780
+ };
2781
+ return sendService;
2782
+ }
2783
+
2784
+ /**
2785
+ * A basic NotificationTaskService implementation.
2786
+ */
2787
+ function notificationTaskService(config) {
2788
+ const { handlers: inputHandlers } = config;
2789
+ const handlers = {};
2790
+ inputHandlers.forEach((handlerConfig) => {
2791
+ const { type } = handlerConfig;
2792
+ handlers[type] = handlerForConfig(handlerConfig);
2793
+ });
2794
+ function handlerForConfig(handlerConfig) {
2795
+ const { flow: inputFlows, allowRunMultipleParts } = handlerConfig;
2796
+ const { included: checkpointFlows, excluded: nonCheckpointFlows } = separateValues(inputFlows, (x) => x.checkpoint != null);
2797
+ if (inputFlows.length === 0) {
2798
+ throw new Error('notificationTaskService(): NotificationTaskServiceTaskHandlerConfig must have at least one flow entry.');
2799
+ }
2800
+ else if (nonCheckpointFlows.length > 1) {
2801
+ throw new Error('notificationTaskService(): NotificationTaskServiceTaskHandlerConfig must not have more than one non-checkpoint flow.');
2802
+ }
2803
+ return {
2804
+ handleNotificationTask: async (notificationTask) => {
2805
+ const { checkpoints: completedCheckpoints } = notificationTask;
2806
+ let fn;
2807
+ switch (completedCheckpoints.length) {
2808
+ case 0:
2809
+ fn = (nonCheckpointFlows[0] ?? checkpointFlows[0])?.fn;
2810
+ break;
2811
+ default:
2812
+ const completedCheckpointsSet = new Set(completedCheckpoints);
2813
+ /**
2814
+ * Find the next flow function that hasn't had its checkpoint completed yet.
2815
+ */
2816
+ const nextCheckpoint = checkpointFlows.find((x) => !completedCheckpointsSet.has(x.checkpoint));
2817
+ fn = nextCheckpoint?.fn;
2818
+ break;
2819
+ }
2820
+ let result;
2821
+ if (fn) {
2822
+ result = await fn(notificationTask);
2823
+ // if allowRunMultipleParts is true, and the result doesn't have a canRunNextCheckpoint value, then set it to true.
2824
+ if (allowRunMultipleParts && result.canRunNextCheckpoint == null) {
2825
+ result.canRunNextCheckpoint = true;
2826
+ }
2827
+ }
2828
+ else {
2829
+ result = {
2830
+ completion: true // if there are no functions remaining, then the task is complete
2831
+ };
2832
+ }
2833
+ return result;
2834
+ }
2835
+ };
2836
+ }
2837
+ return {
2838
+ isKnownNotificationTaskType: (notificationTaskType) => {
2839
+ return handlers[notificationTaskType] !== undefined;
2840
+ },
2841
+ taskHandlerForNotificationTaskType: (notificationTaskType) => handlers[notificationTaskType]
2842
+ };
2843
+ }
2844
+
2845
+ /**
2846
+ * Creates a NotificationTaskSubtaskNotificationTaskHandlerFactory.
2847
+ */
2848
+ function notificationTaskSubtaskNotificationTaskHandlerFactory(factoryConfig) {
2849
+ const { taskType, subtaskHandlerFunctionName: subtaskHandlerName, inputFunction, defaultCleanup, cleanupFunction, buildUpdateMetadata: inputBuildUpdateMetadata } = factoryConfig;
2850
+ return (subtaskHandlerConfig) => {
2851
+ const { processors: inputProcessors, maxCleanupRetryAttempts: inputMaxCleanupRetryAttempts, cleanupRetryDelay: inputCleanupRetryDelay, defaultAllowRunMultipleParts } = subtaskHandlerConfig;
2852
+ const maxCleanupRetryAttempts = inputMaxCleanupRetryAttempts ?? DEFAULT_NOTIFICATION_TASK_SUBTASK_CLEANUP_RETRY_ATTEMPTS;
2853
+ const cleanupRetryDelay = inputCleanupRetryDelay ?? DEFAULT_NOTIFICATION_TASK_SUBTASK_CLEANUP_RETRY_DELAY;
2854
+ const buildUpdateMetadata = inputBuildUpdateMetadata ?? (() => undefined);
2855
+ const processors = {};
2856
+ inputProcessors.forEach((processorConfig) => {
2857
+ const { target } = processorConfig;
2858
+ processors[target] = processorFunctionForConfig(processorConfig);
2859
+ });
2860
+ /**
2861
+ * Structure is similar to notificationTaskService(), but contained to handle the subtasks.
2862
+ */
2863
+ function processorFunctionForConfig(processorConfig) {
2864
+ const { flow: inputFlows, cleanup, allowRunMultipleParts: processorAllowRunMultipleParts } = processorConfig;
2865
+ const { included: subtaskFlows, excluded: nonSubtaskFlows } = separateValues(inputFlows, (x) => x.subtask != null);
2866
+ const allowRunMultipleParts = processorAllowRunMultipleParts ?? defaultAllowRunMultipleParts;
2867
+ if (inputFlows.length === 0) {
2868
+ throw new Error(`${subtaskHandlerName}(): NotificationTaskSubtaskProcessorConfig must have at least one flow entry.`);
2869
+ }
2870
+ else if (nonSubtaskFlows.length > 1) {
2871
+ throw new Error(`${subtaskHandlerName}(): NotificationTaskSubtaskProcessorConfig must not have more than one non-subtask flow.`);
2872
+ }
2873
+ const allKnownSubtasks = unique(inputFlows.map((x) => x.subtask));
2874
+ return {
2875
+ process: async (input) => {
2876
+ const { notificationTask, completedSubtasks, subtaskData } = input;
2877
+ let fn;
2878
+ switch (completedSubtasks.length) {
2879
+ case 0:
2880
+ fn = (nonSubtaskFlows[0] ?? subtaskFlows[0])?.fn;
2881
+ break;
2882
+ default:
2883
+ const completedSubtasksSet = new Set(completedSubtasks);
2884
+ /**
2885
+ * Find the next flow function that hasn't had its checkpoint completed yet.
2886
+ */
2887
+ const nextSubtask = subtaskFlows.find((x) => !completedSubtasksSet.has(x.subtask));
2888
+ fn = nextSubtask?.fn;
2889
+ break;
2890
+ }
2891
+ let result;
2892
+ if (fn) {
2893
+ /*
2894
+ * This section is similar to handleNotificationTask() in notification.action.server.ts,
2895
+ * but is modified to handle the subtasks. The main difference is the attempt count is maintained,
2896
+ * and instead is available via the normal NotificationTask attempts details.
2897
+ */
2898
+ const subtaskResult = await fn(input);
2899
+ const { completion: subtaskCompletion, updateMetadata: subtaskUpdateMetadata, delayUntil, canRunNextCheckpoint } = subtaskResult;
2900
+ let allSubtasksDone = false;
2901
+ let sfps = completedSubtasks;
2902
+ // update the task metadata to reflect the changes
2903
+ switch (subtaskCompletion) {
2904
+ case true:
2905
+ allSubtasksDone = true;
2906
+ break;
2907
+ case false:
2908
+ // remove any completions, if applicable
2909
+ sfps = removeFromCompletionsArrayWithTaskResult(sfps, subtaskResult);
2910
+ break;
2911
+ default:
2912
+ sfps = unique([
2913
+ ...removeFromCompletionsArrayWithTaskResult(sfps, subtaskResult), // remove any completions, if applicable
2914
+ ...asArray(subtaskCompletion)
2915
+ ]);
2916
+ const completedSubtasksSet = new Set(sfps);
2917
+ const incompleteSubtasks = allKnownSubtasks.filter((x) => !completedSubtasksSet.has(x));
2918
+ allSubtasksDone = incompleteSubtasks.length === 0;
2919
+ break;
2920
+ }
2921
+ // configure the update metadata result
2922
+ const sd = {
2923
+ ...subtaskData,
2924
+ ...subtaskUpdateMetadata
2925
+ };
2926
+ /**
2927
+ * This is updating the metadata for the NotificationTask, which has a nested data
2928
+ */
2929
+ const baseUpdateMetadata = {
2930
+ ...notificationTask.data,
2931
+ sfps,
2932
+ sd
2933
+ };
2934
+ let updateMetadata = (await buildUpdateMetadata(baseUpdateMetadata, input));
2935
+ if (updateMetadata) {
2936
+ // inject sfps and sd back in
2937
+ updateMetadata = {
2938
+ ...updateMetadata,
2939
+ sfps,
2940
+ sd
2941
+ };
2942
+ }
2943
+ else {
2944
+ updateMetadata = baseUpdateMetadata;
2945
+ }
2946
+ const nextCanRunNextCheckpoint = canRunNextCheckpoint ?? allowRunMultipleParts;
2947
+ result = {
2948
+ completion: allSubtasksDone ? ['processing'] : delayCompletion(), // return processing until all subtasks are complete.
2949
+ updateMetadata,
2950
+ canRunNextCheckpoint: nextCanRunNextCheckpoint,
2951
+ allCompletedSubTasks: sfps,
2952
+ delayUntil // delay is passed through
2953
+ };
2954
+ }
2955
+ else {
2956
+ // no more subtasks to process, and no metadata changes. Mark as processing complete and continue.
2957
+ result = completeSubtaskProcessingAndScheduleCleanupTaskResult();
2958
+ }
2959
+ return result;
2960
+ },
2961
+ cleanup
2962
+ };
2963
+ }
2964
+ function useInputDataFactory(fn) {
2965
+ return async (notificationTask) => {
2966
+ const { data } = notificationTask;
2967
+ let result;
2968
+ if (data) {
2969
+ try {
2970
+ const inputFunctionResult = await inputFunction(data, notificationTask);
2971
+ result = await fn(notificationTask, inputFunctionResult, data);
2972
+ }
2973
+ catch (e) {
2974
+ if (e instanceof NotificationTaskSubTaskMissingRequiredDataTermination) {
2975
+ // Task is complete if the required data no longer exists. Nothing to cleanup.
2976
+ result = notificationTaskComplete();
2977
+ }
2978
+ else {
2979
+ // rethrow the error
2980
+ throw e;
2981
+ }
2982
+ }
2983
+ }
2984
+ else {
2985
+ // Improperly configured task. Complete immediately.
2986
+ result = notificationTaskComplete();
2987
+ }
2988
+ return result;
2989
+ };
2990
+ }
2991
+ const result = {
2992
+ type: taskType,
2993
+ flow: [
2994
+ {
2995
+ checkpoint: NOTIFICATION_TASK_SUBTASK_CHECKPOINT_PROCESSING,
2996
+ fn: useInputDataFactory(async (notificationTask, inputFunctionResult, data) => {
2997
+ let result;
2998
+ const baseInput = {
2999
+ ...inputFunctionResult,
3000
+ notificationTask
3001
+ };
3002
+ const { target } = baseInput;
3003
+ if (target) {
3004
+ const processor = processors[target];
3005
+ if (processor) {
3006
+ const { sd: subtaskData, sfps: completedSubtasks } = data;
3007
+ const input = {
3008
+ ...baseInput,
3009
+ target,
3010
+ completedSubtasks: completedSubtasks ?? [],
3011
+ subtaskData
3012
+ };
3013
+ result = await processor.process(input);
3014
+ }
3015
+ else {
3016
+ // processor is unknown. Complete the task.
3017
+ result = completeSubtaskProcessingAndScheduleCleanupTaskResult();
3018
+ }
3019
+ }
3020
+ else {
3021
+ // target is unknown. Complete the task.
3022
+ result = completeSubtaskProcessingAndScheduleCleanupTaskResult();
3023
+ }
3024
+ return result;
3025
+ })
3026
+ },
3027
+ {
3028
+ checkpoint: NOTIFICATION_TASK_SUBTASK_CHECKPOINT_CLEANUP,
3029
+ fn: useInputDataFactory(async (notificationTask, inputFunctionResult, data) => {
3030
+ let result;
3031
+ let cleanupFunctionInput = {
3032
+ ...inputFunctionResult,
3033
+ notificationTask
3034
+ };
3035
+ const { target } = cleanupFunctionInput;
3036
+ let cleanupInstructions;
3037
+ if (target) {
3038
+ const processor = processors[target];
3039
+ if (processor && processor.cleanup) {
3040
+ const { sd: subtaskData, sfps: completedSubtasks } = data;
3041
+ const input = {
3042
+ ...cleanupFunctionInput,
3043
+ notificationTask,
3044
+ completedSubtasks: completedSubtasks ?? [],
3045
+ target,
3046
+ subtaskData
3047
+ };
3048
+ cleanupInstructions = (await processor.cleanup(input, defaultCleanup)) ?? (await defaultCleanup(cleanupFunctionInput));
3049
+ cleanupFunctionInput = input;
3050
+ }
3051
+ else {
3052
+ // processor is unknown. Complete the task.
3053
+ cleanupInstructions = await defaultCleanup(cleanupFunctionInput);
3054
+ }
3055
+ }
3056
+ else {
3057
+ // target is unknown. Complete the task.
3058
+ cleanupInstructions = await defaultCleanup(cleanupFunctionInput);
3059
+ }
3060
+ if (cleanupInstructions.cleanupSuccess === false && notificationTask.currentCheckpointSendAttempts <= maxCleanupRetryAttempts) {
3061
+ result = notificationTaskDelayRetry(cleanupInstructions.delayRetryUntil ?? cleanupRetryDelay);
3062
+ }
3063
+ else {
3064
+ result = await cleanupFunction(cleanupFunctionInput, cleanupInstructions);
3065
+ }
3066
+ return result;
3067
+ })
3068
+ }
3069
+ ]
3070
+ };
3071
+ return result;
3072
+ };
3073
+ }
3074
+ // MARK: Internally Handled Errors
3075
+ /**
3076
+ * Thrown when a subtask no longer has data available to continue processing.
3077
+ *
3078
+ * The subtask will be marked as immediately complete, and no cleanup will occur.
3079
+ *
3080
+ * This is useful in cases where the underlying models or data that the subtask rely on are deleted (and those models were also required for cleanup) so the task can be marked as complete without attempting cleanup.
3081
+ */
3082
+ class NotificationTaskSubTaskMissingRequiredDataTermination extends BaseError {
3083
+ }
3084
+ /**
3085
+ * Creates a NotificationTaskSubTaskMissingRequiredDataTermination.
3086
+ */
3087
+ function notificationTaskSubTaskMissingRequiredDataTermination() {
3088
+ return new NotificationTaskSubTaskMissingRequiredDataTermination();
3089
+ }
3090
+
3091
+ function storageFileModelAlreadyInitializedError() {
3092
+ return preconditionConflictError({
3093
+ message: `This model has already been initialized.`,
3094
+ code: STORAGE_FILE_MODEL_ALREADY_INITIALIZED_ERROR_CODE
3095
+ });
3096
+ }
3097
+ function storageFileNotFlaggedForGroupsSyncError() {
3098
+ return preconditionConflictError({
3099
+ message: `This StorageFile has not been flagged for sync with its groups.`,
3100
+ code: STORAGE_FILE_NOT_FLAGGED_FOR_GROUPS_SYNC_ERROR_CODE
3101
+ });
3102
+ }
3103
+ function uploadedFileDoesNotExistError() {
3104
+ return preconditionConflictError({
3105
+ message: `The target uploaded file does not exist.`,
3106
+ code: UPLOADED_FILE_DOES_NOT_EXIST_ERROR_CODE
3107
+ });
3108
+ }
3109
+ function uploadedFileIsNotAllowedToBeInitializedError() {
3110
+ return preconditionConflictError({
3111
+ message: `The target uploaded file is not allowed to be initialized.`,
3112
+ code: UPLOADED_FILE_NOT_ALLOWED_TO_BE_INITIALIZED_ERROR_CODE
3113
+ });
3114
+ }
3115
+ function uploadedFileInitializationFailedError(data) {
3116
+ return internalServerError({
3117
+ message: `The target uploaded file initialization failed with result type "${data.resultType}".`,
3118
+ code: UPLOADED_FILE_INITIALIZATION_FAILED_ERROR_CODE,
3119
+ data: {
3120
+ resultType: data.resultType
3121
+ }
3122
+ });
3123
+ }
3124
+ function uploadedFileInitializationDiscardedError() {
3125
+ return internalServerError({
3126
+ message: `The target uploaded file initialization was discarded.`,
3127
+ code: UPLOADED_FILE_INITIALIZATION_DISCARDED_ERROR_CODE
3128
+ });
3129
+ }
3130
+ function storageFileProcessingNotAllowedForInvalidStateError() {
3131
+ return preconditionConflictError({
3132
+ message: `The target StorageFileDocument must be in an OK state to be processed and processing not flagged as SHOULD_NOT_PROCESS.`,
3133
+ code: STORAGE_FILE_PROCESSING_NOT_ALLOWED_FOR_INVALID_STATE_ERROR_CODE
3134
+ });
3135
+ }
3136
+ function storageFileProcessingNotQueuedForProcessingError() {
3137
+ return preconditionConflictError({
3138
+ message: `The target StorageFileDocument is not queued for processing.`,
3139
+ code: STORAGE_FILE_PROCESSING_NOT_QUEUED_FOR_PROCESSING_ERROR_CODE
3140
+ });
3141
+ }
3142
+ function storageFileProcessingNotAvailableForTypeError() {
3143
+ return preconditionConflictError({
3144
+ message: `The target StorageFileDocument is not available for processing.`,
3145
+ code: STORAGE_FILE_PROCESSING_NOT_AVAILABLE_FOR_TYPE_ERROR_CODE
3146
+ });
3147
+ }
3148
+ function storageFileAlreadyProcessedError() {
3149
+ return preconditionConflictError({
3150
+ message: `The target StorageFileDocument has already finished processing.`,
3151
+ code: STORAGE_FILE_ALREADY_PROCESSED_ERROR_CODE
3152
+ });
3153
+ }
3154
+ function storageFileNotFlaggedForDeletionError() {
3155
+ return preconditionConflictError({
3156
+ message: `The target StorageFileDocument is not flagged for deletion.`,
3157
+ code: STORAGE_FILE_NOT_FLAGGED_FOR_DELETION_ERROR_CODE
3158
+ });
3159
+ }
3160
+ function storageFileCannotBeDeletedYetError() {
3161
+ return preconditionConflictError({
3162
+ message: `The target StorageFileDocument cannot be deleted yet.`,
3163
+ code: STORAGE_FILE_CANNOT_BE_DELETED_YET_ERROR_CODE
3164
+ });
3165
+ }
3166
+ function storageFileGroupQueuedForInitializationError() {
3167
+ return preconditionConflictError({
3168
+ message: `The target StorageFileGroupDocument is queued for initialization.`,
3169
+ code: STORAGE_FILE_GROUP_QUEUED_FOR_INITIALIZATION_ERROR_CODE
3170
+ });
3171
+ }
3172
+ function createStorageFileGroupInputError() {
3173
+ return preconditionConflictError({
3174
+ message: `The model or storageFileId is required for creating a StorageFileGroup.`,
3175
+ code: STORAGE_FILE_GROUP_CREATE_INPUT_ERROR_CODE
3176
+ });
3177
+ }
3178
+
3179
+ /**
3180
+ * Injection token for the BaseStorageFileServerActionsContext
3181
+ */
3182
+ const BASE_STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN = 'BASE_STORAGE_FILE_SERVER_ACTION_CONTEXT';
3183
+ /**
3184
+ * Injection token for the StorageFileServerActionsContext
3185
+ */
3186
+ const STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN = 'STORAGE_FILE_SERVER_ACTION_CONTEXT';
3187
+ class StorageFileServerActions {
3188
+ }
3189
+ function storageFileServerActions(context) {
3190
+ return {
3191
+ createStorageFile: createStorageFileFactory(context),
3192
+ initializeAllStorageFilesFromUploads: initializeAllStorageFilesFromUploadsFactory(context),
3193
+ initializeStorageFileFromUpload: initializeStorageFileFromUploadFactory(context),
3194
+ updateStorageFile: updateStorageFileFactory(context),
3195
+ processAllQueuedStorageFiles: processAllQueuedStorageFilesFactory(context),
3196
+ processStorageFile: processStorageFileFactory(context),
3197
+ deleteAllQueuedStorageFiles: deleteAllQueuedStorageFilesFactory(context),
3198
+ deleteStorageFile: deleteStorageFileFactory(context),
3199
+ downloadStorageFile: downloadStorageFileFactory(context),
3200
+ createStorageFileGroup: createStorageFileGroupFactory(context),
3201
+ updateStorageFileGroup: updateStorageFileGroupFactory(context),
3202
+ syncStorageFileWithGroups: syncStorageFileWithGroupsFactory(context),
3203
+ syncAllFlaggedStorageFilesWithGroups: syncAllFlaggedStorageFilesWithGroupsFactory(context),
3204
+ regenerateStorageFileGroupContent: regenerateStorageFileGroupContentFactory(context),
3205
+ regenerateAllFlaggedStorageFileGroupsContent: regenerateAllFlaggedStorageFileGroupsContentFactory(context)
3206
+ };
3207
+ }
3208
+ // MARK: Actions
3209
+ function createStorageFileFactory(context) {
3210
+ const { storageFileCollection, firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
3211
+ return firebaseServerActionTransformFunctionFactory(CreateStorageFileParams, async (params) => {
3212
+ return async () => {
3213
+ const storageFileDocument = null;
3214
+ // TODO: check the file exists, and pull the metadata, and create the document
3215
+ return storageFileDocument;
3216
+ };
3217
+ });
3218
+ }
3219
+ function initializeAllStorageFilesFromUploadsFactory(context) {
3220
+ const { storageService, firebaseServerActionTransformFunctionFactory } = context;
3221
+ const _initializeStorageFileFromUploadFile = _initializeStorageFileFromUploadFileFactory(context);
3222
+ return firebaseServerActionTransformFunctionFactory(InitializeAllStorageFilesFromUploadsParams, async (params) => {
3223
+ const { folderPath, maxFilesToInitialize, overrideUploadsFolderPath } = params;
3224
+ const fullPath = mergeSlashPaths([overrideUploadsFolderPath ?? UPLOADS_FOLDER_PATH, folderPath]); // only targets the uploads folder
3225
+ return async () => {
3226
+ const folder = storageService.folder(fullPath);
3227
+ const modelKeys = [];
3228
+ let filesVisited = 0;
3229
+ let initializationsSuccessCount = 0;
3230
+ let initializationsFailureCount = 0;
3231
+ await iterateStorageListFilesByEachFile({
3232
+ folder,
3233
+ includeNestedResults: true,
3234
+ readItemsFromPageResult: (results) => results.result.files(),
3235
+ iterateEachPageItem: async (file) => {
3236
+ const fileInstance = file.file();
3237
+ const initializeResult = await _initializeStorageFileFromUploadFile({ file: fileInstance }).catch(() => null);
3238
+ filesVisited++;
3239
+ if (initializeResult) {
3240
+ initializationsSuccessCount++;
3241
+ modelKeys.push(initializeResult.key);
3242
+ }
3243
+ else {
3244
+ initializationsFailureCount++;
3245
+ }
3246
+ },
3247
+ /**
3248
+ * The maximum number of files to initialize at once.
3249
+ */
3250
+ iterateItemsLimit: maxFilesToInitialize ?? 1000,
3251
+ /**
3252
+ * Iterate four separate pages at a time
3253
+ */
3254
+ maxParallelPages: 4
3255
+ });
3256
+ const result = {
3257
+ modelKeys,
3258
+ filesVisited,
3259
+ initializationsSuccessCount,
3260
+ initializationsFailureCount
3261
+ };
3262
+ return result;
3263
+ };
3264
+ });
3265
+ }
3266
+ function _initializeStorageFileFromUploadFileFactory(context) {
3267
+ const { firestoreContext, storageFileInitializeFromUploadService, notificationExpediteService } = context;
3268
+ const processStorageFileInTransaction = _processStorageFileInTransactionFactory(context);
3269
+ return async (input) => {
3270
+ const { file, expediteProcessing } = input;
3271
+ const { bucketId, pathString } = file.storagePath;
3272
+ // file must exist
3273
+ const exists = await file.exists();
3274
+ if (!exists) {
3275
+ throw uploadedFileDoesNotExistError();
3276
+ }
3277
+ // file must be allowed to be initialized
3278
+ const isAllowedToBeInitialized = await storageFileInitializeFromUploadService.checkFileIsAllowedToBeInitialized(file);
3279
+ if (!isAllowedToBeInitialized) {
3280
+ throw uploadedFileIsNotAllowedToBeInitializedError();
3281
+ }
3282
+ let storageFileDocument;
3283
+ let initializationResult;
3284
+ let httpsError;
3285
+ try {
3286
+ initializationResult = await storageFileInitializeFromUploadService.initializeFromUpload({
3287
+ file
3288
+ });
3289
+ async function deleteFile() {
3290
+ try {
3291
+ // can now delete the uploaded file
3292
+ await file.delete();
3293
+ }
3294
+ catch (e) {
3295
+ // log errors here, but do nothing.
3296
+ console.error(`initializeStorageFileFromUpload(): Error deleting uploaded file (${bucketId}/${pathString})`, e);
3297
+ }
3298
+ }
3299
+ switch (initializationResult.resultType) {
3300
+ case 'success':
3301
+ await deleteFile();
3302
+ if (initializationResult.storageFileDocument) {
3303
+ storageFileDocument = initializationResult.storageFileDocument;
3304
+ // expedite processing if requested
3305
+ if (storageFileDocument != null && expediteProcessing) {
3306
+ const storageFile = await assertSnapshotData(storageFileDocument);
3307
+ if (storageFile.ps === StorageFileProcessingState.QUEUED_FOR_PROCESSING) {
3308
+ const expediteInstance = notificationExpediteService.expediteInstance();
3309
+ await firestoreContext.runTransaction(async (transaction) => {
3310
+ expediteInstance.initialize();
3311
+ await processStorageFileInTransaction({
3312
+ storageFileDocument: storageFileDocument,
3313
+ expediteInstance
3314
+ }, transaction);
3315
+ });
3316
+ await expediteInstance.send().catch(() => null);
3317
+ }
3318
+ }
3319
+ }
3320
+ else {
3321
+ httpsError = uploadedFileInitializationDiscardedError();
3322
+ }
3323
+ break;
3324
+ case 'initializer_error':
3325
+ if (initializationResult.initializationError) {
3326
+ throw initializationResult.initializationError; // re-throw the encountered error
3327
+ }
3328
+ break;
3329
+ case 'permanent_initializer_failure':
3330
+ // log the error
3331
+ if (initializationResult.initializationError) {
3332
+ console.warn(`initializeStorageFileFromUpload(): Permanent initializer failure for file (${bucketId}/${pathString})`, initializationResult.initializationError);
3333
+ }
3334
+ // delete the file
3335
+ await deleteFile();
3336
+ // return the error
3337
+ httpsError = uploadedFileInitializationFailedError({
3338
+ resultType: initializationResult.resultType,
3339
+ fileDeleted: true
3340
+ });
3341
+ break;
3342
+ case 'no_determiner_match':
3343
+ case 'no_initializer_configured':
3344
+ default:
3345
+ httpsError = uploadedFileInitializationFailedError({
3346
+ resultType: initializationResult.resultType
3347
+ });
3348
+ console.error(`initializeStorageFileFromUpload(): Unknown file type (${initializationResult.resultType}) encountered for storage file "${bucketId}/${pathString}".`);
3349
+ break;
3350
+ }
3351
+ }
3352
+ catch (e) {
3353
+ console.error(`initializeStorageFileFromUpload(): Error while initializing storage file (${bucketId}/${pathString}) from upload`, e);
3354
+ httpsError = uploadedFileInitializationFailedError({ resultType: 'initializer_error' });
3355
+ }
3356
+ if (httpsError) {
3357
+ throw httpsError;
3358
+ }
3359
+ else if (!storageFileDocument) {
3360
+ throw uploadedFileInitializationDiscardedError(); // throw again for redundancy
3361
+ }
3362
+ return storageFileDocument;
3363
+ };
3364
+ }
3365
+ function initializeStorageFileFromUploadFactory(context) {
3366
+ const { storageService, firebaseServerActionTransformFunctionFactory } = context;
3367
+ const _initializeStorageFileFromUploadFile = _initializeStorageFileFromUploadFileFactory(context);
3368
+ return firebaseServerActionTransformFunctionFactory(InitializeStorageFileFromUploadParams, async (params) => {
3369
+ const { bucketId, pathString, expediteProcessing } = params;
3370
+ return async () => {
3371
+ const file = storageService.file(bucketId == null ? pathString : { bucketId, pathString });
3372
+ return _initializeStorageFileFromUploadFile({ file, expediteProcessing });
3373
+ };
3374
+ });
3375
+ }
3376
+ function updateStorageFileFactory(context) {
3377
+ const { storageFileCollection, firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
3378
+ return firebaseServerActionTransformFunctionFactory(UpdateStorageFileParams, async (params) => {
3379
+ const { sdat } = params;
3380
+ return async (storageFileDocument) => {
3381
+ const updateTemplate = {
3382
+ sdat
3383
+ };
3384
+ await storageFileDocument.update(updateTemplate);
3385
+ return storageFileDocument;
3386
+ };
3387
+ });
3388
+ }
3389
+ function updateStorageFileGroupFactory(context) {
3390
+ const { firestoreContext, storageFileGroupCollection, firebaseServerActionTransformFunctionFactory } = context;
3391
+ return firebaseServerActionTransformFunctionFactory(UpdateStorageFileGroupParams, async (params) => {
3392
+ const { entries } = params;
3393
+ return async (storageFileGroupDocument) => {
3394
+ await firestoreContext.runTransaction(async (transaction) => {
3395
+ const storageFileGroupDocumentInTransaction = storageFileGroupCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(storageFileGroupDocument);
3396
+ const storageFileGroup = await assertSnapshotData(storageFileGroupDocumentInTransaction);
3397
+ let f = undefined;
3398
+ // update entries
3399
+ if (entries?.length) {
3400
+ f = ModelRelationUtility.updateCollection(storageFileGroup.f, entries, {
3401
+ readKey: (x) => x.s,
3402
+ merge: (existing, update) => {
3403
+ const n = update.n === undefined ? existing.n : update.n;
3404
+ return {
3405
+ ...existing,
3406
+ n
3407
+ };
3408
+ }
3409
+ });
3410
+ }
3411
+ const updateTemplate = {
3412
+ f
3413
+ };
3414
+ await storageFileGroupDocumentInTransaction.update(updateTemplate);
3415
+ });
3416
+ return storageFileGroupDocument;
3417
+ };
3418
+ });
3419
+ }
3420
+ function processAllQueuedStorageFilesFactory(context) {
3421
+ const { storageFileCollection, firebaseServerActionTransformFunctionFactory } = context;
3422
+ const processStorageFile = processStorageFileFactory(context);
3423
+ return firebaseServerActionTransformFunctionFactory(ProcessAllQueuedStorageFilesParams, async (params) => {
3424
+ return async () => {
3425
+ let storageFilesVisited = 0;
3426
+ let storageFilesProcessStarted = 0;
3427
+ let storageFilesFailedStarting = 0;
3428
+ const proceessStorageFileParams = {
3429
+ key: firestoreDummyKey()
3430
+ };
3431
+ const processStorageFileInstance = await processStorageFile(proceessStorageFileParams);
3432
+ await iterateFirestoreDocumentSnapshotPairs({
3433
+ documentAccessor: storageFileCollection.documentAccessor(),
3434
+ iterateSnapshotPair: async (snapshotPair) => {
3435
+ storageFilesVisited++;
3436
+ const processStorageFileResult = await processStorageFileInstance(snapshotPair.document).catch(() => null);
3437
+ if (processStorageFileResult) {
3438
+ storageFilesProcessStarted++;
3439
+ }
3440
+ else {
3441
+ storageFilesFailedStarting++;
3442
+ }
3443
+ },
3444
+ constraintsFactory: () => storageFilesQueuedForProcessingQuery(),
3445
+ queryFactory: storageFileCollection,
3446
+ batchSize: undefined,
3447
+ performTasksConfig: {
3448
+ maxParallelTasks: 10
3449
+ }
3450
+ });
3451
+ const result = {
3452
+ storageFilesVisited,
3453
+ storageFilesProcessStarted,
3454
+ storageFilesFailedStarting
3455
+ };
3456
+ return result;
3457
+ };
3458
+ });
3459
+ }
3460
+ function _processStorageFileInTransactionFactory(context) {
3461
+ const { storageFileCollection, notificationCollectionGroup } = context;
3462
+ return async (input, transaction) => {
3463
+ const { storageFileDocument, storageFile: inputStorageFile, params, expediteInstance } = input;
3464
+ const { checkRetryProcessing, forceRestartProcessing, processAgainIfSuccessful } = params ?? {};
3465
+ const storageFileDocumentInTransaction = storageFileCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(storageFileDocument);
3466
+ const storageFile = inputStorageFile ?? (await assertSnapshotData(storageFileDocumentInTransaction));
3467
+ async function beginProcessing(overrideExistingTask) {
3468
+ const state = storageFile.fs;
3469
+ // check the storageFile is in the OK state
3470
+ if (state !== StorageFileState.OK) {
3471
+ throw storageFileProcessingNotAllowedForInvalidStateError();
3472
+ }
3473
+ const createNotificationTaskResult = await createNotificationDocument({
3474
+ context,
3475
+ transaction,
3476
+ template: storageFileProcessingNotificationTaskTemplate({
3477
+ storageFileDocument,
3478
+ overrideExistingTask
3479
+ })
3480
+ });
3481
+ await storageFileDocumentInTransaction.update({
3482
+ ps: StorageFileProcessingState.PROCESSING,
3483
+ pat: new Date(), // set new processing start date
3484
+ pcat: null, // clear processing completion date
3485
+ pn: createNotificationTaskResult.notificationDocument.key
3486
+ });
3487
+ expediteInstance?.enqueueCreateResult(createNotificationTaskResult);
3488
+ }
3489
+ switch (storageFile.ps) {
3490
+ case StorageFileProcessingState.INIT_OR_NONE:
3491
+ // queue up for processing, unless it has no purpose
3492
+ if (!storageFile.p) {
3493
+ throw storageFileProcessingNotAvailableForTypeError();
3494
+ }
3495
+ else {
3496
+ await beginProcessing(false);
3497
+ }
3498
+ break;
3499
+ case StorageFileProcessingState.QUEUED_FOR_PROCESSING:
3500
+ // begin processing
3501
+ await beginProcessing(false);
3502
+ break;
3503
+ case StorageFileProcessingState.PROCESSING:
3504
+ // check if the processing task is still running
3505
+ const shouldCheckProcessing = !isThrottled(STORAGE_FILE_PROCESSING_STUCK_THROTTLE_CHECK_MS, storageFile.pat);
3506
+ if (!storageFile.pn) {
3507
+ await beginProcessing(true); // if no processing task is set, restart processing to recover from the broken state
3508
+ }
3509
+ else {
3510
+ const { pn } = storageFile;
3511
+ const notificationDocument = notificationCollectionGroup.documentAccessorForTransaction(transaction).loadDocumentForKey(pn);
3512
+ if (checkRetryProcessing || shouldCheckProcessing) {
3513
+ const notification = await notificationDocument.snapshotData();
3514
+ if (!notification) {
3515
+ // the notification document is missing. Re-begin processing
3516
+ await beginProcessing(true);
3517
+ }
3518
+ else if (notification.d || forceRestartProcessing) {
3519
+ // if the notification is somehow in the done state but the StorageFile never got notified in the same transaction, requeue.
3520
+ await beginProcessing(true);
3521
+ }
3522
+ // NOTE: We could look at the state of the notification task more, but at this point the task is probably still valid and still running,
3523
+ // so we can only wait on it. In general if the task still exists and is not yet done, then we should wait on it as the
3524
+ // task running system should complete eventually by design.
3525
+ }
3526
+ else if (expediteInstance) {
3527
+ // enqueue the existing notification to be run in the expedite instance
3528
+ expediteInstance.enqueue(notificationDocument);
3529
+ }
3530
+ }
3531
+ break;
3532
+ case StorageFileProcessingState.DO_NOT_PROCESS:
3533
+ throw storageFileProcessingNotQueuedForProcessingError();
3534
+ case StorageFileProcessingState.SUCCESS:
3535
+ if (forceRestartProcessing || processAgainIfSuccessful) {
3536
+ await beginProcessing(true);
3537
+ }
3538
+ else {
3539
+ throw storageFileAlreadyProcessedError();
3540
+ }
3541
+ break;
3542
+ }
3543
+ };
3544
+ }
3545
+ function processStorageFileFactory(context) {
3546
+ const { firestoreContext, notificationExpediteService, firebaseServerActionTransformFunctionFactory } = context;
3547
+ const processStorageFileInTransaction = _processStorageFileInTransactionFactory(context);
3548
+ return firebaseServerActionTransformFunctionFactory(ProcessStorageFileParams, async (params) => {
3549
+ const { runImmediately } = params;
3550
+ return async (storageFileDocument) => {
3551
+ const expediteInstance = notificationExpediteService.expediteInstance();
3552
+ await firestoreContext.runTransaction(async (transaction) => {
3553
+ expediteInstance.initialize();
3554
+ await processStorageFileInTransaction({
3555
+ storageFileDocument,
3556
+ params,
3557
+ expediteInstance
3558
+ }, transaction);
3559
+ });
3560
+ let expediteResult = null;
3561
+ // expedite the task if requested
3562
+ if (runImmediately) {
3563
+ expediteResult = await expediteInstance.send().then((x) => x[0]);
3564
+ }
3565
+ const result = {
3566
+ runImmediately: runImmediately ?? false,
3567
+ expediteResult
3568
+ };
3569
+ return result;
3570
+ };
3571
+ });
3572
+ }
3573
+ function deleteAllQueuedStorageFilesFactory(context) {
3574
+ const { storageFileCollection, firebaseServerActionTransformFunctionFactory } = context;
3575
+ const deleteStorageFile = deleteStorageFileFactory(context);
3576
+ return firebaseServerActionTransformFunctionFactory(DeleteAllQueuedStorageFilesParams, async (params) => {
3577
+ return async () => {
3578
+ let storageFilesVisited = 0;
3579
+ let storageFilesDeleted = 0;
3580
+ let storageFilesFailedDeleting = 0;
3581
+ const deleteStorageFileInstance = await deleteStorageFile({
3582
+ key: firestoreDummyKey()
3583
+ });
3584
+ await iterateFirestoreDocumentSnapshotPairs({
3585
+ documentAccessor: storageFileCollection.documentAccessor(),
3586
+ iterateSnapshotPair: async (snapshotPair) => {
3587
+ const { document: storageFileDocument } = snapshotPair;
3588
+ storageFilesVisited++;
3589
+ const deleteStorageFileResult = await deleteStorageFileInstance(storageFileDocument)
3590
+ .then(() => true)
3591
+ .catch(() => false);
3592
+ if (deleteStorageFileResult) {
3593
+ storageFilesDeleted++;
3594
+ }
3595
+ else {
3596
+ storageFilesFailedDeleting++;
3597
+ }
3598
+ },
3599
+ constraintsFactory: () => storageFilesQueuedForDeleteQuery(),
3600
+ queryFactory: storageFileCollection,
3601
+ batchSize: undefined,
3602
+ performTasksConfig: {
3603
+ maxParallelTasks: 10
3604
+ }
3605
+ });
3606
+ const result = {
3607
+ storageFilesDeleted,
3608
+ storageFilesFailedDeleting,
3609
+ storageFilesVisited
3610
+ };
3611
+ return result;
3612
+ };
3613
+ });
3614
+ }
3615
+ function deleteStorageFileFactory(context) {
3616
+ const { firestoreContext, storageService, storageFileCollection, firebaseServerActionTransformFunctionFactory } = context;
3617
+ const syncStorageFileWithGroupsInTransaction = _syncStorageFileWithGroupsInTransactionFactory(context);
3618
+ return firebaseServerActionTransformFunctionFactory(DeleteStorageFileParams, async (params) => {
3619
+ const { force } = params;
3620
+ return async (inputStorageFileDocument) => {
3621
+ await firestoreContext.runTransaction(async (transaction) => {
3622
+ const storageFileDocument = await storageFileCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(inputStorageFileDocument);
3623
+ const storageFile = await assertSnapshotData(storageFileDocument);
3624
+ const fileAccessor = storageService.file(storageFile);
3625
+ if (!force) {
3626
+ if (!storageFile.sdat) {
3627
+ throw storageFileNotFlaggedForDeletionError();
3628
+ }
3629
+ else if (!isPast(storageFile.sdat)) {
3630
+ throw storageFileCannotBeDeletedYetError();
3631
+ }
3632
+ }
3633
+ // remove the storage file from any groups
3634
+ await syncStorageFileWithGroupsInTransaction({ storageFileDocument, storageFile, force: true, removeAllStorageFileGroups: true }, transaction);
3635
+ // delete the file
3636
+ await fileAccessor.delete().catch(() => null);
3637
+ // delete the document
3638
+ await storageFileDocument.accessor.delete();
3639
+ });
3640
+ };
3641
+ });
3642
+ }
3643
+ function downloadStorageFileFactory(context) {
3644
+ const { storageService, firebaseServerActionTransformFunctionFactory, storageFileCollection } = context;
3645
+ return firebaseServerActionTransformFunctionFactory(DownloadStorageFileParams, async (params) => {
3646
+ const { key: targetStorageFileDocumentKey, asAdmin, expiresAt, expiresIn: inputExpiresIn, responseDisposition, responseContentType } = params;
3647
+ return async (storageFileDocument) => {
3648
+ // if the StorageFileDocument was not provided, set it from the target key
3649
+ if (!storageFileDocument) {
3650
+ storageFileDocument = storageFileCollection.documentAccessor().loadDocumentForKey(targetStorageFileDocumentKey);
3651
+ }
3652
+ const storageFile = await assertSnapshotData(storageFileDocument);
3653
+ const fileAccessor = storageService.file(storageFile);
3654
+ let result;
3655
+ if (fileAccessor.getSignedUrl) {
3656
+ const expiresIn = inputExpiresIn ?? MS_IN_MINUTE * 30;
3657
+ const expires = expirationDetails({ defaultExpiresFromDateToNow: true, expiresAt, expiresIn });
3658
+ let downloadUrlExpiresAt = expires.getExpirationDate();
3659
+ // if they're not an admin, limit the expiration to a max of 30 days.
3660
+ if (downloadUrlExpiresAt && !asAdmin) {
3661
+ const maxExpirationDate = addDays(new Date(), 30);
3662
+ downloadUrlExpiresAt = findMinDate([downloadUrlExpiresAt, maxExpirationDate]);
3663
+ }
3664
+ const [downloadUrl, metadata] = await Promise.all([
3665
+ fileAccessor.getSignedUrl({
3666
+ action: 'read',
3667
+ expiresAt: downloadUrlExpiresAt ?? undefined,
3668
+ responseDisposition: responseDisposition ?? undefined, // can be set by anyone
3669
+ responseType: asAdmin ? (responseContentType ?? undefined) : undefined // can only be set by admins
3670
+ }),
3671
+ fileAccessor.getMetadata()
3672
+ ]);
3673
+ result = {
3674
+ url: downloadUrl,
3675
+ fileName: metadata.name ? slashPathDetails(metadata.name).end : undefined,
3676
+ mimeType: responseContentType ?? metadata.contentType,
3677
+ expiresAt: unixDateTimeSecondsNumberFromDate(downloadUrlExpiresAt)
3678
+ };
3679
+ }
3680
+ else {
3681
+ throw internalServerError('Signed url function appears to not be avalable.');
3682
+ }
3683
+ return result;
3684
+ };
3685
+ });
3686
+ }
3687
+ function createStorageFileGroupInTransactionFactory(context) {
3688
+ const { storageFileGroupCollection } = context;
3689
+ return async (params, transaction) => {
3690
+ const { now: inputNow, skipCreate, template } = params;
3691
+ const now = inputNow ?? new Date();
3692
+ const storageFileGroupDocument = loadStorageFileGroupDocumentForReferencePair(params, storageFileGroupCollection.documentAccessorForTransaction(transaction));
3693
+ const storageFileGroupTemplate = {
3694
+ o: firestoreDummyKey(), // set during initialization
3695
+ cat: now,
3696
+ s: true, // requires initialization
3697
+ f: [],
3698
+ ...template
3699
+ };
3700
+ if (!skipCreate) {
3701
+ await storageFileGroupDocument.create(storageFileGroupTemplate);
3702
+ }
3703
+ return {
3704
+ storageFileGroupTemplate,
3705
+ storageFileGroupDocument
3706
+ };
3707
+ };
3708
+ }
3709
+ function createStorageFileGroupFactory(context) {
3710
+ const { firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
3711
+ const createStorageFileGroupInTransaction = createStorageFileGroupInTransactionFactory(context);
3712
+ return firebaseServerActionTransformFunctionFactory(CreateStorageFileGroupParams, async (params) => {
3713
+ const { model, storageFileId } = params;
3714
+ const storageFileGroupRelatedModelKey = model ? model : storageFileId ? inferKeyFromTwoWayFlatFirestoreModelKey(storageFileId) : undefined;
3715
+ if (!storageFileGroupRelatedModelKey) {
3716
+ throw createStorageFileGroupInputError();
3717
+ }
3718
+ return async () => {
3719
+ const result = await firestoreContext.runTransaction(async (transaction) => {
3720
+ const { storageFileGroupDocument } = await createStorageFileGroupInTransaction({ storageFileGroupRelatedModelKey }, transaction);
3721
+ return storageFileGroupDocument;
3722
+ });
3723
+ return result;
3724
+ };
3725
+ });
3726
+ }
3727
+ function _syncStorageFileWithGroupsInTransactionFactory(context) {
3728
+ const { storageFileCollection, storageFileGroupCollection } = context;
3729
+ const createStorageFileGroupInTransaction = createStorageFileGroupInTransactionFactory(context);
3730
+ return async (input, transaction) => {
3731
+ const { storageFileDocument, storageFile: inputStorageFile, force, removeAllStorageFileGroups, skipStorageFileUpdate } = input;
3732
+ const storageFileDocumentInTransaction = storageFileCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(storageFileDocument);
3733
+ const storageFileGroupDocumentAccessor = storageFileGroupCollection.documentAccessorForTransaction(transaction);
3734
+ const storageFile = inputStorageFile ?? (await assertSnapshotData(storageFileDocumentInTransaction));
3735
+ if (!storageFile.gs && !force) {
3736
+ throw storageFileNotFlaggedForGroupsSyncError();
3737
+ }
3738
+ const g = storageFile.g ?? [];
3739
+ const storageFileGroupDocuments = loadDocumentsForIds(storageFileGroupDocumentAccessor, g);
3740
+ const storageFileGroupPairs = await getDocumentSnapshotDataPairs(storageFileGroupDocuments);
3741
+ let storageFilesGroupsCreated = 0;
3742
+ let storageFilesGroupsUpdated = 0;
3743
+ await performAsyncTasks(storageFileGroupPairs, async (storageFileGroupPair) => {
3744
+ const { data: storageFileGroup, document: storageFileGroupDocument } = storageFileGroupPair;
3745
+ const existsInStorageFileGroup = storageFileGroup?.f.some((x) => x.s === storageFileDocument.id);
3746
+ const change = removeAllStorageFileGroups ? (existsInStorageFileGroup ? 'remove' : undefined) : !existsInStorageFileGroup ? 'add' : undefined;
3747
+ switch (change) {
3748
+ case 'add':
3749
+ // add it if it doesn't exist
3750
+ const createTemplate = calculateStorageFileGroupEmbeddedFileUpdate({
3751
+ storageFileGroup: storageFileGroup ?? { f: [] },
3752
+ insert: [
3753
+ {
3754
+ s: storageFileDocument.id
3755
+ }
3756
+ ],
3757
+ allowRecalculateRegenerateFlag: false
3758
+ });
3759
+ if (!storageFileGroup) {
3760
+ // if the group does not exist, then create it
3761
+ await createStorageFileGroupInTransaction({ storageFileGroupDocument, template: createTemplate }, transaction);
3762
+ storageFilesGroupsCreated += 1;
3763
+ }
3764
+ else {
3765
+ // if the group exists, then update it
3766
+ await storageFileGroupDocument.update(createTemplate);
3767
+ storageFilesGroupsUpdated += 1;
3768
+ }
3769
+ break;
3770
+ case 'remove':
3771
+ // remove it
3772
+ const removeTemplate = calculateStorageFileGroupEmbeddedFileUpdate({
3773
+ storageFileGroup: storageFileGroup ?? { f: [] },
3774
+ remove: [storageFileDocument.id]
3775
+ });
3776
+ await storageFileGroupDocument.update(removeTemplate);
3777
+ storageFilesGroupsUpdated += 1;
3778
+ break;
3779
+ }
3780
+ });
3781
+ const result = {
3782
+ storageFilesGroupsCreated,
3783
+ storageFilesGroupsUpdated
3784
+ };
3785
+ // update the storage file to no longer be flagged for sync
3786
+ if (!skipStorageFileUpdate) {
3787
+ await storageFileDocumentInTransaction.update({
3788
+ gs: false
3789
+ });
3790
+ }
3791
+ return result;
3792
+ };
3793
+ }
3794
+ function syncStorageFileWithGroupsFactory(context) {
3795
+ const { firestoreContext, storageFileCollection, storageFileGroupCollection, firebaseServerActionTransformFunctionFactory } = context;
3796
+ const syncStorageFileWithGroupsInTransaction = _syncStorageFileWithGroupsInTransactionFactory(context);
3797
+ return firebaseServerActionTransformFunctionFactory(SyncStorageFileWithGroupsParams, async (params) => {
3798
+ const { force } = params;
3799
+ return async (storageFileDocument) => {
3800
+ return firestoreContext.runTransaction(async (transaction) => syncStorageFileWithGroupsInTransaction({ storageFileDocument, force }, transaction));
3801
+ };
3802
+ });
3803
+ }
3804
+ function syncAllFlaggedStorageFilesWithGroupsFactory(context) {
3805
+ const { firebaseServerActionTransformFunctionFactory, storageFileCollection } = context;
3806
+ const syncStorageFileWithGroups = syncStorageFileWithGroupsFactory(context);
3807
+ return firebaseServerActionTransformFunctionFactory(SyncAllFlaggedStorageFilesWithGroupsParams, async (params) => {
3808
+ return async () => {
3809
+ const syncStorageFileWithGroupsInstance = await syncStorageFileWithGroups({
3810
+ key: firestoreDummyKey(),
3811
+ force: true // force anyways; they should all be flagged for sync when the query hits
3812
+ });
3813
+ let storageFilesSynced = 0;
3814
+ let storageFilesGroupsCreated = 0;
3815
+ let storageFilesGroupsUpdated = 0;
3816
+ await iterateFirestoreDocumentSnapshotPairBatches({
3817
+ documentAccessor: storageFileCollection.documentAccessor(),
3818
+ iterateSnapshotPairsBatch: async (snapshotPairBatch) => {
3819
+ // only sync StorageFiles that are flagged for sync
3820
+ await runAsyncTasksForValues(snapshotPairBatch.filter((x) => x.data.gs), async (snapshotPair) => {
3821
+ const { document: storageFileDocument } = snapshotPair;
3822
+ const result = await syncStorageFileWithGroupsInstance(storageFileDocument);
3823
+ storageFilesSynced += 1;
3824
+ storageFilesGroupsCreated += result.storageFilesGroupsCreated;
3825
+ storageFilesGroupsUpdated += result.storageFilesGroupsUpdated;
3826
+ }, {
3827
+ maxParallelTasks: 10, // can update 10 storageFiles/Groups at the same time
3828
+ nonConcurrentTaskKeyFactory: (x) => x.data.g // do not update the same group at the same time
3829
+ });
3830
+ },
3831
+ queryFactory: storageFileCollection,
3832
+ constraintsFactory: () => storageFileFlaggedForSyncWithGroupsQuery(),
3833
+ performTasksConfig: {
3834
+ sequential: true // run batches sequentially to avoid contention in updating a StorageFileGroup
3835
+ },
3836
+ totalSnapshotsLimit: 1000,
3837
+ limitPerCheckpoint: 100
3838
+ });
3839
+ const result = {
3840
+ storageFilesSynced,
3841
+ storageFilesGroupsCreated,
3842
+ storageFilesGroupsUpdated
3843
+ };
3844
+ return result;
3845
+ };
3846
+ });
3847
+ }
3848
+ function regenerateStorageFileGroupContentFactory(context) {
3849
+ const { firestoreContext, storageService, storageFileCollection, storageFileGroupCollection, firebaseServerActionTransformFunctionFactory } = context;
3850
+ const processStorageFileInTransaction = _processStorageFileInTransactionFactory(context);
3851
+ return firebaseServerActionTransformFunctionFactory(RegenerateStorageFileGroupContentParams, async (params) => {
3852
+ const { force } = params;
3853
+ const createStorageFileDocumentPair = createStorageFileDocumentPairFactory({
3854
+ defaultCreationType: StorageFileCreationType.FOR_STORAGE_FILE_GROUP
3855
+ });
3856
+ return async (storageFileGroupDocument) => {
3857
+ return firestoreContext.runTransaction(async (transaction) => {
3858
+ const storageFileGroupDocumentInTransaction = storageFileGroupCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(storageFileGroupDocument);
3859
+ const storageFileGroup = await assertSnapshotData(storageFileGroupDocumentInTransaction);
3860
+ const storageFileDocumentAccessor = storageFileCollection.documentAccessorForTransaction(transaction);
3861
+ const { o, zsf, s } = storageFileGroup;
3862
+ // must not be queued for initialization
3863
+ if (s) {
3864
+ throw storageFileGroupQueuedForInitializationError();
3865
+ }
3866
+ const existingZipStorageFileDocument = zsf ? storageFileDocumentAccessor.loadDocumentForId(zsf) : undefined;
3867
+ const [existingZipStorageFilePair] = await Promise.all([existingZipStorageFileDocument ? getDocumentSnapshotDataPair(existingZipStorageFileDocument) : undefined]);
3868
+ let contentStorageFilesFlaggedForProcessing = 0;
3869
+ const updateTemplate = {
3870
+ re: false // clear the regeneration flag
3871
+ };
3872
+ // For each content type, create/update/flag the StorageFile for processing that type
3873
+ const { regenerateZip } = calculateStorageFileGroupRegeneration({ storageFileGroup, force });
3874
+ if (regenerateZip) {
3875
+ // check that the storageFile exists, and if it doesn't, create a new one
3876
+ if (!existingZipStorageFilePair?.data) {
3877
+ const zipStorageFile = storageService.file(storageFileGroupZipFileStoragePath(storageFileGroupDocument.id));
3878
+ // create a new StorageFile
3879
+ const { storageFileDocument } = await createStorageFileDocumentPair({
3880
+ storagePathRef: zipStorageFile,
3881
+ accessor: storageFileDocumentAccessor,
3882
+ parentStorageFileGroup: storageFileGroupDocument,
3883
+ purpose: STORAGE_FILE_GROUP_ZIP_STORAGE_FILE_PURPOSE,
3884
+ shouldBeProcessed: true,
3885
+ ownershipKey: o,
3886
+ metadata: {
3887
+ sfg: storageFileGroupDocument.id
3888
+ }
3889
+ });
3890
+ updateTemplate.zsf = storageFileDocument.id;
3891
+ }
3892
+ else {
3893
+ // flag it for processing again
3894
+ await processStorageFileInTransaction({ params: { processAgainIfSuccessful: true }, storageFileDocument: existingZipStorageFilePair.document, storageFile: existingZipStorageFilePair.data }, transaction);
3895
+ }
3896
+ contentStorageFilesFlaggedForProcessing += 1;
3897
+ }
3898
+ // update the StorageFileGroup
3899
+ await storageFileGroupDocumentInTransaction.update(updateTemplate);
3900
+ const result = {
3901
+ contentStorageFilesFlaggedForProcessing
3902
+ };
3903
+ return result;
3904
+ });
3905
+ };
3906
+ });
3907
+ }
3908
+ function regenerateAllFlaggedStorageFileGroupsContentFactory(context) {
3909
+ const { firebaseServerActionTransformFunctionFactory, storageFileGroupCollection } = context;
3910
+ const regenerateStorageFileGroupContent = regenerateStorageFileGroupContentFactory(context);
3911
+ return firebaseServerActionTransformFunctionFactory(RegenerateAllFlaggedStorageFileGroupsContentParams, async (params) => {
3912
+ return async () => {
3913
+ const regenerateStorageFileGroupContentInstance = await regenerateStorageFileGroupContent({
3914
+ key: firestoreDummyKey()
3915
+ });
3916
+ let storageFileGroupsUpdated = 0;
3917
+ let contentStorageFilesFlaggedForProcessing = 0;
3918
+ await iterateFirestoreDocumentSnapshotPairs({
3919
+ documentAccessor: storageFileGroupCollection.documentAccessor(),
3920
+ iterateSnapshotPair: async (snapshotPair) => {
3921
+ const { data: storageFileGroup } = snapshotPair;
3922
+ if (!storageFileGroup.s) {
3923
+ const result = await regenerateStorageFileGroupContentInstance(snapshotPair.document);
3924
+ storageFileGroupsUpdated += 1;
3925
+ contentStorageFilesFlaggedForProcessing += result.contentStorageFilesFlaggedForProcessing;
3926
+ }
3927
+ },
3928
+ queryFactory: storageFileGroupCollection,
3929
+ constraintsFactory: () => storageFileGroupsFlaggedForContentRegenerationQuery(),
3930
+ performTasksConfig: {
3931
+ maxParallelTasks: 10
3932
+ },
3933
+ totalSnapshotsLimit: 1000,
3934
+ limitPerCheckpoint: 100
3935
+ });
3936
+ const result = {
3937
+ storageFileGroupsUpdated,
3938
+ contentStorageFilesFlaggedForProcessing
3939
+ };
3940
+ return result;
3941
+ };
3942
+ });
3943
+ }
3944
+
3945
+ // MARK: StorageFileInitServerActionsContextConfig
3946
+ /**
3947
+ * Token to access/override the StorageFileTemplateService's defaults records.
3948
+ */
3949
+ const STORAGE_FILE_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN = 'STORAGE_FILE_INIT_SERVER_ACTIONS_CONTEXT_CONFIG';
3950
+ const MAKE_TEMPLATE_FOR_STORAGEFILE_RELATED_MODEL_INITIALIZATION_FUNCTION_DELETE_RESPONSE = false;
3951
+ class StorageFileInitServerActions {
3952
+ }
3953
+ function storageFileInitServerActions(context) {
3954
+ return {
3955
+ initializeStorageFileGroup: initializeStorageFileGroupFactory(context),
3956
+ initializeAllApplicableStorageFileGroups: initializeAllApplicableStorageFileGroupsFactory(context)
3957
+ };
3958
+ }
3959
+ async function initializeStorageFileModelInTransaction(input) {
3960
+ const { makeTemplateFunction, throwErrorIfAlreadyInitialized, transaction, document: documentInTransaction, data: storageFileModel } = input;
3961
+ let initialized = false;
3962
+ const alreadyInitialized = !storageFileModel.s;
3963
+ if (!alreadyInitialized) {
3964
+ const flatModelKey = documentInTransaction.id;
3965
+ const modelKey = inferKeyFromTwoWayFlatFirestoreModelKey(flatModelKey);
3966
+ const modelCollectionName = firestoreModelKeyCollectionName(modelKey);
3967
+ const input = {
3968
+ transaction,
3969
+ flatModelKey,
3970
+ modelKey,
3971
+ collectionName: modelCollectionName
3972
+ };
3973
+ const template = await makeTemplateFunction(input);
3974
+ if (template === false) {
3975
+ await documentInTransaction.accessor.delete();
3976
+ }
3977
+ else if (template == null) {
3978
+ await documentInTransaction.update({
3979
+ s: false, // set false when "f" is set true
3980
+ fi: true
3981
+ });
3982
+ }
3983
+ else {
3984
+ initialized = true;
3985
+ await documentInTransaction.update({
3986
+ //
3987
+ ...template,
3988
+ m: undefined, // should not be changed
3989
+ s: null, // is now initialized.
3990
+ fi: false // set false
3991
+ });
3992
+ }
3993
+ }
3994
+ else if (throwErrorIfAlreadyInitialized) {
3995
+ throw storageFileModelAlreadyInitializedError();
3996
+ }
3997
+ return {
3998
+ initialized,
3999
+ alreadyInitialized
4000
+ };
4001
+ }
4002
+ function initializeStorageFileGroupInTransactionFactory(context) {
4003
+ const { storageFileGroupCollection, makeTemplateForStorageFileGroupInitialization } = context;
4004
+ return async (params, storageFileGroupDocument, transaction) => {
4005
+ const { throwErrorIfAlreadyInitialized } = params;
4006
+ const storageFileGroupDocumentInTransaction = storageFileGroupCollection.documentAccessorForTransaction(transaction).loadDocumentFrom(storageFileGroupDocument);
4007
+ const storageFileGroup = await assertSnapshotData(storageFileGroupDocumentInTransaction);
4008
+ return initializeStorageFileModelInTransaction({
4009
+ makeTemplateFunction: async (input) => {
4010
+ const baseTemplate = (await makeTemplateForStorageFileGroupInitialization(input));
4011
+ // template can only define o and any StorageFileGroupContentFlagsData properties
4012
+ return {
4013
+ o: baseTemplate.o,
4014
+ z: baseTemplate.z,
4015
+ re: true // always flag for content regeneration
4016
+ };
4017
+ },
4018
+ throwErrorIfAlreadyInitialized,
4019
+ transaction,
4020
+ document: storageFileGroupDocumentInTransaction,
4021
+ data: storageFileGroup
4022
+ });
4023
+ };
4024
+ }
4025
+ function initializeStorageFileGroupFactory(context) {
4026
+ const { firestoreContext, firebaseServerActionTransformFunctionFactory } = context;
4027
+ const initializeStorageFileGroupInTransaction = initializeStorageFileGroupInTransactionFactory(context);
4028
+ return firebaseServerActionTransformFunctionFactory(InitializeStorageFileModelParams, async (params) => {
4029
+ return async (storageFileGroupDocument) => {
4030
+ await firestoreContext.runTransaction((transaction) => initializeStorageFileGroupInTransaction(params, storageFileGroupDocument, transaction));
4031
+ return storageFileGroupDocument;
4032
+ };
4033
+ });
4034
+ }
4035
+ function initializeAllApplicableStorageFileGroupsFactory(context) {
4036
+ const { firestoreContext, firebaseServerActionTransformFunctionFactory, storageFileGroupCollection } = context;
4037
+ const initializeStorageFileGroupInTransaction = initializeStorageFileGroupInTransactionFactory(context);
4038
+ return firebaseServerActionTransformFunctionFactory(InitializeAllApplicableStorageFileGroupsParams, async () => {
4039
+ return async () => {
4040
+ let storageFileGroupsVisited = 0;
4041
+ let storageFileGroupsSucceeded = 0;
4042
+ let storageFileGroupsFailed = 0;
4043
+ let storageFileGroupsAlreadyInitialized = 0;
4044
+ const initializeStorageFileGroupParams = { key: firestoreDummyKey(), throwErrorIfAlreadyInitialized: false };
4045
+ async function initializeStorageFileGroups() {
4046
+ const query = storageFileGroupCollection.queryDocument(storageFileGroupsFlaggedForNeedsInitializationQuery());
4047
+ const storageFileGroupDocuments = await query.getDocs();
4048
+ const result = await performAsyncTasks(storageFileGroupDocuments, async (storageFileGroupDocument) => {
4049
+ return firestoreContext.runTransaction((transaction) => initializeStorageFileGroupInTransaction(initializeStorageFileGroupParams, storageFileGroupDocument, transaction));
4050
+ }, {
4051
+ maxParallelTasks: 5
4052
+ });
4053
+ return result;
4054
+ }
4055
+ while (true) {
4056
+ const initializeStorageFileGroupsResults = await initializeStorageFileGroups();
4057
+ initializeStorageFileGroupsResults.results.forEach((x) => {
4058
+ const result = x[1];
4059
+ if (result.alreadyInitialized) {
4060
+ storageFileGroupsAlreadyInitialized += 1;
4061
+ }
4062
+ else if (result.initialized) {
4063
+ storageFileGroupsSucceeded += 1;
4064
+ }
4065
+ else {
4066
+ storageFileGroupsFailed += 1;
4067
+ }
4068
+ });
4069
+ const found = initializeStorageFileGroupsResults.results.length;
4070
+ storageFileGroupsVisited += found;
4071
+ if (!found) {
4072
+ break;
4073
+ }
4074
+ }
4075
+ const result = {
4076
+ storageFileGroupsVisited,
4077
+ storageFileGroupsSucceeded,
4078
+ storageFileGroupsFailed,
4079
+ storageFileGroupsAlreadyInitialized
4080
+ };
4081
+ return result;
4082
+ };
4083
+ });
4084
+ }
4085
+
4086
+ /**
4087
+ * Service dedicated to initializing a StorageFileDocument value from an uploaded file.
4088
+ */
4089
+ class StorageFileInitializeFromUploadService {
4090
+ }
4091
+
4092
+ // MARK: Provider Factories
4093
+ function storageFileServerActionsContextFactory(context, storageFileInitializeFromUploadService) {
4094
+ return { ...context, storageFileInitializeFromUploadService };
4095
+ }
4096
+ function storageFileServerActionsFactory(context) {
4097
+ return storageFileServerActions(context);
4098
+ }
4099
+ function storageFileInitServerActionsFactory(context, storageFileInitServerActionsContextConfig) {
4100
+ return storageFileInitServerActions({
4101
+ ...context,
4102
+ ...storageFileInitServerActionsContextConfig
4103
+ });
4104
+ }
4105
+ /**
4106
+ * Convenience function used to generate ModuleMetadata for an app's StorageFileModule.
4107
+ *
4108
+ * By default this module exports:
4109
+ * - StorageFileServerActionContext (STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN)
4110
+ * - StorageFileServerActions
4111
+ * - StorageFileInitServerActions
4112
+ *
4113
+ * Be sure the class that delares the module using this function also extends AbstractAppStorageFileModule.
4114
+ *
4115
+ * @param provide
4116
+ * @param useFactory
4117
+ * @returns
4118
+ */
4119
+ function appStorageFileModuleMetadata(config) {
4120
+ const { dependencyModule, imports, exports: exports$1, providers } = config;
4121
+ const dependencyModuleImport = dependencyModule ? [dependencyModule] : [];
4122
+ return {
4123
+ imports: [ConfigModule, ...dependencyModuleImport, ...(imports ?? [])],
4124
+ exports: [STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN, StorageFileServerActions, StorageFileInitServerActions, ...(exports$1 ?? [])],
4125
+ providers: [
4126
+ {
4127
+ provide: STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN,
4128
+ useFactory: storageFileServerActionsContextFactory,
4129
+ inject: [BASE_STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN, StorageFileInitializeFromUploadService]
4130
+ },
4131
+ {
4132
+ provide: StorageFileServerActions,
4133
+ useFactory: storageFileServerActionsFactory,
4134
+ inject: [STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN]
4135
+ },
4136
+ {
4137
+ provide: StorageFileInitServerActions,
4138
+ useFactory: storageFileInitServerActionsFactory,
4139
+ inject: [STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN, STORAGE_FILE_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN]
4140
+ },
4141
+ ...(providers ?? [])
4142
+ ]
4143
+ };
4144
+ }
4145
+
4146
+ /**
4147
+ * Performs a query and flags the matching StorageFiles for deletion.
4148
+ *
4149
+ * @param input The input for the query and flagging.
4150
+ * @returns The result of the query and flagging.
4151
+ */
4152
+ async function queryAndFlagStorageFilesForDelete(input) {
4153
+ const { storageFileCollection, constraints, queuedForDeleteTime: inputQueueForDeleteTime, skipDeleteForKeys } = input;
4154
+ const queuedForDeleteTime = inputQueueForDeleteTime ?? true;
4155
+ const skipDeleteSet = new Set(asArray(skipDeleteForKeys));
4156
+ let visitedCount = 0;
4157
+ let queuedForDeleteCount = 0;
4158
+ await iterateFirestoreDocumentSnapshotPairs({
4159
+ documentAccessor: storageFileCollection.documentAccessor(),
4160
+ iterateSnapshotPair: async (snapshotPair) => {
4161
+ const { document, data: storageFile } = snapshotPair;
4162
+ if (!storageFile.sdat && !skipDeleteSet.has(storageFile.key)) {
4163
+ await document.update(markStorageFileForDeleteTemplate(queuedForDeleteTime));
4164
+ queuedForDeleteCount++;
4165
+ }
4166
+ visitedCount++;
4167
+ },
4168
+ queryFactory: storageFileCollection,
4169
+ constraintsFactory: () => constraints,
4170
+ batchSize: undefined,
4171
+ performTasksConfig: {
4172
+ maxParallelTasks: 20
4173
+ }
4174
+ });
4175
+ return {
4176
+ visitedCount,
4177
+ queuedForDeleteCount
4178
+ };
4179
+ }
4180
+ /**
4181
+ * Creates a template for updating a StorageFile to be queued for deletion at the input time.
4182
+ *
4183
+ * @param queueForDeleteTime When to delete the StorageFile. If true or unset, the StorageFile will be flagged to be deleted immediately.
4184
+ * @returns The update template for the StorageFile.
4185
+ */
4186
+ function markStorageFileForDeleteTemplate(queueForDeleteTime) {
4187
+ const updateTemplate = {
4188
+ sdat: queueForDeleteTime === true || queueForDeleteTime == null ? new Date() : dateFromDateOrTimeMillisecondsNumber(queueForDeleteTime),
4189
+ fs: StorageFileState.QUEUED_FOR_DELETE
4190
+ };
4191
+ return updateTemplate;
4192
+ }
4193
+
4194
+ const storageFileProcessingNotificationTaskHandlerDefaultCleanup = () => {
4195
+ return {
4196
+ cleanupSuccess: true,
4197
+ nextProcessingState: StorageFileProcessingState.SUCCESS,
4198
+ queueForDelete: false // do not queue for delete automatically
4199
+ };
4200
+ };
4201
+ /**
4202
+ * Creates a NotificationTaskServiceTaskHandlerConfig that handles the StorageFileProcessingNotificationTask.
4203
+ */
4204
+ function storageFileProcessingNotificationTaskHandler(config) {
4205
+ const { processors: inputProcessors, storageAccessor, storageFileFirestoreCollections, allStorageFileGroupProcessorConfig } = config;
4206
+ const storageFileDocumentAccessor = storageFileFirestoreCollections.storageFileCollection.documentAccessor();
4207
+ const makeFileDetailsAccessor = storedFileReaderFactory();
4208
+ const defaultCleanup = storageFileProcessingNotificationTaskHandlerDefaultCleanup;
4209
+ const processors = [...inputProcessors];
4210
+ if (allStorageFileGroupProcessorConfig !== false) {
4211
+ const storageFileGroupProcessors = allStorageFileGroupStorageFileProcessingPurposeSubtaskProcessors({
4212
+ ...allStorageFileGroupProcessorConfig,
4213
+ storageFileFirestoreCollections,
4214
+ storageAccessor
4215
+ });
4216
+ pushArrayItemsIntoArray(processors, storageFileGroupProcessors);
4217
+ }
4218
+ return notificationTaskSubtaskNotificationTaskHandlerFactory({
4219
+ taskType: STORAGE_FILE_PROCESSING_NOTIFICATION_TASK_TYPE,
4220
+ subtaskHandlerFunctionName: 'storageFileProcessingNotificationTaskHandler',
4221
+ inputFunction: async (data) => {
4222
+ const storageFileDocument = await storageFileDocumentAccessor.loadDocumentForId(data.storageFile);
4223
+ const loadStorageFile = cachedGetter(async () => {
4224
+ const storageFile = await getDocumentSnapshotData(storageFileDocument, true);
4225
+ if (!storageFile) {
4226
+ throw notificationTaskSubTaskMissingRequiredDataTermination();
4227
+ }
4228
+ return storageFile;
4229
+ });
4230
+ let purpose = data?.p;
4231
+ if (!purpose) {
4232
+ // attempt to load the purpose from the storage file, if it exists.
4233
+ purpose = (await loadStorageFile().then((x) => x.p));
4234
+ }
4235
+ let storagePath;
4236
+ if (data.storagePath) {
4237
+ storagePath = data.storagePath;
4238
+ }
4239
+ else {
4240
+ storagePath = await loadStorageFile().then((x) => ({ bucketId: x.bucketId, pathString: x.pathString }));
4241
+ }
4242
+ const file = storageAccessor.file(storagePath);
4243
+ const fileDetailsAccessor = makeFileDetailsAccessor(file);
4244
+ const input = {
4245
+ target: purpose,
4246
+ loadStorageFile,
4247
+ fileDetailsAccessor,
4248
+ storageFileDocument
4249
+ };
4250
+ return input;
4251
+ },
4252
+ buildUpdateMetadata: (baseUpdateMetadata, input) => {
4253
+ const { target } = input;
4254
+ return {
4255
+ ...baseUpdateMetadata,
4256
+ // always re-copy the target/storagePath for the next run so StorageFile does not have to be reloaded
4257
+ p: target,
4258
+ storagePath: copyStoragePath(input.fileDetailsAccessor.input)
4259
+ };
4260
+ },
4261
+ defaultCleanup,
4262
+ cleanupFunction: async function (input, cleanupInstructions) {
4263
+ const { storageFileDocument } = input;
4264
+ const { nextProcessingState, queueForDelete, flagResyncWithStorageFileGroups: syncWithStorageFileGroups } = cleanupInstructions;
4265
+ let updateTemplate = {
4266
+ ps: nextProcessingState ?? StorageFileProcessingState.SUCCESS,
4267
+ pcat: new Date(), // set new cleanup/completion date
4268
+ pn: null // clear reference
4269
+ };
4270
+ const shouldQueueForDelete = queueForDelete != null && queueForDelete !== false;
4271
+ if (shouldQueueForDelete) {
4272
+ updateTemplate = {
4273
+ ...updateTemplate,
4274
+ ...markStorageFileForDeleteTemplate(queueForDelete)
4275
+ };
4276
+ }
4277
+ else if (syncWithStorageFileGroups) {
4278
+ // resync with storage file groups
4279
+ updateTemplate.gs = true;
4280
+ }
4281
+ await storageFileDocument.update(updateTemplate);
4282
+ return notificationTaskComplete();
4283
+ }
4284
+ })({
4285
+ ...config,
4286
+ processors
4287
+ });
4288
+ }
4289
+ function allStorageFileGroupStorageFileProcessingPurposeSubtaskProcessors(config) {
4290
+ const { excludeZipProcessing } = config;
4291
+ const processors = [];
4292
+ if (!excludeZipProcessing) {
4293
+ processors.push(storageFileGroupZipStorageFileProcessingPurposeSubtaskProcessor(config));
4294
+ }
4295
+ return processors;
4296
+ }
4297
+ function storageFileGroupZipStorageFileProcessingPurposeSubtaskProcessor(config) {
4298
+ const { storageFileFirestoreCollections, storageAccessor, zip } = config;
4299
+ const { storageFileCollection, storageFileGroupCollection } = storageFileFirestoreCollections;
4300
+ const { maxNumberOfFilesToZipInParallel: inputMaxNumberOfFilesToZipInParallel, zipFileDisplayNameFunctionFactory: inputZipFileDisplayNameFunctionFactory, configureZipInfoJson: inputConfigureZipInfoJson, configureZipArchiverOptions: inputConfigureZipArchiverOptions, finalizeZipArchive } = zip ?? {};
4301
+ const maxNumberOfFilesToZipInParallel = inputMaxNumberOfFilesToZipInParallel ?? 3;
4302
+ const appendZipInfoJson = inputConfigureZipInfoJson !== false;
4303
+ const configureZipArchiverOptions = inputConfigureZipArchiverOptions ?? (() => ({ zlib: { level: 9 } }));
4304
+ const configureZipInfoJson = (appendZipInfoJson ? inputConfigureZipInfoJson : undefined) ?? MAP_IDENTITY;
4305
+ const zipFileDisplayNameFunctionFactory = inputZipFileDisplayNameFunctionFactory ?? (() => () => null);
4306
+ const storageFileGroupZipProcessorConfig = {
4307
+ target: STORAGE_FILE_GROUP_ZIP_STORAGE_FILE_PURPOSE,
4308
+ cleanup: zip?.cleanup,
4309
+ flow: [
4310
+ {
4311
+ subtask: STORAGE_FILE_GROUP_ZIP_STORAGE_FILE_PURPOSE_CREATE_ZIP_SUBTASK,
4312
+ fn: async (input) => {
4313
+ const { storageFileDocument, fileDetailsAccessor } = input;
4314
+ const storageFile = await input.loadStorageFile();
4315
+ const storageFileMetadata = storageFile.d;
4316
+ const storageFileGroupId = storageFileMetadata?.sfg;
4317
+ let result;
4318
+ async function flagStorageFileForDeletion() {
4319
+ await storageFileDocument.update(markStorageFileForDeleteTemplate());
4320
+ return notificationTaskComplete(); // skip cleanup step
4321
+ }
4322
+ if (storageFileGroupId) {
4323
+ const storageFileGroupDocument = storageFileGroupCollection.documentAccessor().loadDocumentForId(storageFileGroupId);
4324
+ const storageFileGroup = await storageFileGroupDocument.snapshotData();
4325
+ if (storageFileGroup) {
4326
+ const embeddedFilesMap = new Map(storageFileGroup.f.map((x) => [x.s, x]));
4327
+ const storageFileIdsToZip = storageFileGroup.f.map((x) => x.s);
4328
+ const storageFilesToZip = loadDocumentsForIds(storageFileCollection.documentAccessor(), storageFileIdsToZip);
4329
+ const storageFileDataPairsToZip = await getDocumentSnapshotDataPairs(storageFilesToZip);
4330
+ const zipFileDisplayNameFunction = await zipFileDisplayNameFunctionFactory({ storageFileGroup, storageFileGroupDocument });
4331
+ let flagCleanFileAssociations = undefined;
4332
+ // create a new file
4333
+ const zipFileAccessor = storageAccessor.file(fileDetailsAccessor.input);
4334
+ if (zipFileAccessor.uploadStream && zipFileAccessor.getStream) {
4335
+ const uploadStream = zipFileAccessor.uploadStream({
4336
+ contentType: ZIP_FILE_MIME_TYPE
4337
+ });
4338
+ const startedAt = new Date();
4339
+ const archiverOptions = await configureZipArchiverOptions({ input, storageFileGroup });
4340
+ const newArchive = archiver('zip', archiverOptions);
4341
+ // pipe the archive to the upload stream
4342
+ newArchive.pipe(uploadStream, { end: true });
4343
+ // upload each of the files to the archive
4344
+ await performAsyncTasks(storageFileDataPairsToZip, async (storageFileDataPair) => {
4345
+ const { data: storageFile } = storageFileDataPair;
4346
+ if (storageFile) {
4347
+ const { n: storageFileDisplayName } = storageFile;
4348
+ // make sure it references the storage file group
4349
+ const referencesStorageFileGroup = storageFile.g.some((x) => x === storageFileGroupId);
4350
+ if (referencesStorageFileGroup) {
4351
+ const fileAccessor = storageAccessor.file(storageFile);
4352
+ const metadata = await fileAccessor.getMetadata().catch(() => null);
4353
+ if (metadata) {
4354
+ const fileSlashPathDetails = slashPathDetails(metadata.name);
4355
+ const storageFileGroupEmbeddedFile = embeddedFilesMap.get(storageFile.id);
4356
+ const { n: embeddedFileNameOverride } = storageFileGroupEmbeddedFile;
4357
+ const nameFromFactory = await zipFileDisplayNameFunction({ metadata, fileAccessor, storageFile, storageFileDocument, storageFileGroupEmbeddedFile });
4358
+ let untypedName = nameFromFactory || storageFileDisplayName || embeddedFileNameOverride || fileSlashPathDetails.fileName;
4359
+ let extension;
4360
+ if (fileSlashPathDetails.typedFileExtension) {
4361
+ extension = fileSlashPathDetails.typedFileExtension;
4362
+ }
4363
+ else if (metadata.contentType) {
4364
+ extension = documentFileExtensionForMimeType(metadata.contentType);
4365
+ }
4366
+ // set the default name if still unset
4367
+ untypedName = untypedName || `sf_${storageFile.id}`;
4368
+ const name = extension ? `${untypedName}.${extension}` : untypedName;
4369
+ const fileStream = fileAccessor.getStream();
4370
+ await useCallback((x) => {
4371
+ // append the file to the archive
4372
+ newArchive.append(fileStream, {
4373
+ name
4374
+ });
4375
+ // if the stream errors, call back
4376
+ fileStream.on('error', (e) => x(e));
4377
+ // when the stream finishes, call back
4378
+ fileStream.on('finish', () => x());
4379
+ });
4380
+ }
4381
+ else {
4382
+ flagCleanFileAssociations = true;
4383
+ }
4384
+ }
4385
+ else {
4386
+ flagCleanFileAssociations = true;
4387
+ }
4388
+ }
4389
+ }, {
4390
+ maxParallelTasks: maxNumberOfFilesToZipInParallel
4391
+ });
4392
+ const finishedAt = new Date();
4393
+ // create the info.json file
4394
+ if (appendZipInfoJson) {
4395
+ const infoJson = await configureZipInfoJson({
4396
+ sfg: storageFileGroupId,
4397
+ sf: storageFileIdsToZip,
4398
+ s: startedAt.toISOString(),
4399
+ f: finishedAt.toISOString()
4400
+ });
4401
+ let infoJsonString;
4402
+ try {
4403
+ infoJsonString = JSON.stringify(infoJson);
4404
+ }
4405
+ catch (e) {
4406
+ console.error('storageFileGroupZipStorageFileProcessingPurposeSubtaskProcessor(): Failed to convert the info json to a string. Check your custom configureInfoJson() function.', e);
4407
+ }
4408
+ if (infoJsonString) {
4409
+ newArchive.append(infoJsonString, {
4410
+ name: STORAGE_FILE_GROUP_ZIP_INFO_JSON_FILE_NAME
4411
+ });
4412
+ }
4413
+ }
4414
+ // perform any other tasks using the zip archive
4415
+ if (finalizeZipArchive) {
4416
+ await finalizeZipArchive({
4417
+ input,
4418
+ storageFileGroup,
4419
+ archive: newArchive
4420
+ });
4421
+ }
4422
+ // finalize the archive
4423
+ await newArchive.finalize();
4424
+ // update the StorageFileGroup
4425
+ await storageFileGroupDocument.update({
4426
+ zat: finishedAt,
4427
+ c: flagCleanFileAssociations
4428
+ });
4429
+ // schedule/run the cleanup task
4430
+ result = notificationSubtaskComplete({
4431
+ canRunNextCheckpoint: true
4432
+ });
4433
+ }
4434
+ else {
4435
+ // uploadStream is not available for some reason? Should never occur.
4436
+ console.warn('storageFileGroupZipStorageFileProcessingPurposeSubtaskProcessor(): uploadStream is not available for some reason while creating a new zip.');
4437
+ result = notificationTaskDelayRetry(MS_IN_HOUR);
4438
+ }
4439
+ }
4440
+ else {
4441
+ // storage file group no longer exists. Flag the StorageFile for deletion.
4442
+ result = await flagStorageFileForDeletion();
4443
+ }
4444
+ }
4445
+ else {
4446
+ // improperly configured StorageFile for this type. Flag the StorageFile for deletion.
4447
+ result = await flagStorageFileForDeletion();
4448
+ }
4449
+ return result;
4450
+ }
4451
+ }
4452
+ ]
4453
+ };
4454
+ return storageFileGroupZipProcessorConfig;
4455
+ }
4456
+
4457
+ function storageFileInitializeFromUploadServiceInitializerResultPermanentFailure(error, createdFile) {
4458
+ return {
4459
+ error,
4460
+ permanentFailure: true,
4461
+ createdFile
4462
+ };
4463
+ }
4464
+ /**
4465
+ * A basic StorageFileInitializeFromUploadService implementation.
4466
+ */
4467
+ function storageFileInitializeFromUploadService(config) {
4468
+ const { storageService, storageFileCollection, initializer: inputInitializers, determiner: inputDeterminers, validate, checkFileIsAllowedToBeInitialized: inputCheckFileIsAllowedToBeInitialized, requireStorageFileRelatedFileMetadataBeSet } = config;
4469
+ const allDeterminers = [];
4470
+ const initializers = {};
4471
+ const detailsAccessorFactory = storedFileReaderFactory();
4472
+ if (inputDeterminers) {
4473
+ pushItemOrArrayItemsIntoArray(allDeterminers, inputDeterminers);
4474
+ }
4475
+ // iterate initializers
4476
+ inputInitializers.forEach((initializer) => {
4477
+ const { type: inputTypes, determiner: inputDeterminer } = initializer;
4478
+ const types = asArray(inputTypes);
4479
+ types.forEach((type) => {
4480
+ initializers[type] = initializer;
4481
+ });
4482
+ if (inputDeterminer) {
4483
+ const wrappedDeterminer = limitUploadFileTypeDeterminer(inputDeterminer, types);
4484
+ allDeterminers.push(wrappedDeterminer);
4485
+ }
4486
+ });
4487
+ const determiner = combineUploadFileTypeDeterminers({
4488
+ determiners: allDeterminers,
4489
+ ...{
4490
+ completeSearchOnFirstMatch: true,
4491
+ ...config.combineDeterminersConfig
4492
+ }
4493
+ });
4494
+ // validate initializers
4495
+ if (validate) {
4496
+ const allInitializerTypes = Object.keys(initializers);
4497
+ const allDeterminerTypes = new Set(determiner.getPossibleFileTypes());
4498
+ // all initializer types should have a corresponding determiner
4499
+ allInitializerTypes.forEach((type) => {
4500
+ if (!allDeterminerTypes.has(type)) {
4501
+ throw new Error(`Initializer type ${type} does not have a corresponding determiner.`);
4502
+ }
4503
+ });
4504
+ }
4505
+ async function determineUploadFileType(input) {
4506
+ const { file } = input;
4507
+ const fileDetailsAccessor = detailsAccessorFactory(file);
4508
+ return determiner.determine(fileDetailsAccessor);
4509
+ }
4510
+ return {
4511
+ checkFileIsAllowedToBeInitialized: inputCheckFileIsAllowedToBeInitialized ?? asDecisionFunction(true),
4512
+ determineUploadFileType,
4513
+ initializeFromUpload: async (input) => {
4514
+ const determinerResult = await determineUploadFileType(input);
4515
+ let resultType;
4516
+ let createdFilePath;
4517
+ let storageFileDocument;
4518
+ let processorError;
4519
+ let previousStorageFilesFlaggedForDeletion;
4520
+ if (determinerResult) {
4521
+ const { input: fileDetailsAccessor } = determinerResult;
4522
+ resultType = 'success';
4523
+ const initializer = initializers[determinerResult.type];
4524
+ if (initializer) {
4525
+ try {
4526
+ const initializerResult = await initializer.initialize({ determinerResult, fileDetailsAccessor });
4527
+ if (initializerResult.error) {
4528
+ const { error, permanentFailure, createdFile } = initializerResult;
4529
+ processorError = error;
4530
+ if (permanentFailure) {
4531
+ resultType = 'permanent_initializer_failure';
4532
+ }
4533
+ else {
4534
+ resultType = 'initializer_error';
4535
+ }
4536
+ // delete the created file
4537
+ if (createdFile != null) {
4538
+ await storageService
4539
+ .file(createdFile.storagePath)
4540
+ .delete()
4541
+ .catch(() => 0);
4542
+ }
4543
+ }
4544
+ else {
4545
+ let flagPreviousForDelete;
4546
+ if (initializerResult.createStorageFileResult) {
4547
+ const { createStorageFileResult, flagPreviousForDelete: flagPreviousForDeleteResult } = initializerResult;
4548
+ createdFilePath = copyStoragePath(createStorageFileResult.storageFile);
4549
+ storageFileDocument = createStorageFileResult.storageFileDocument;
4550
+ if (flagPreviousForDeleteResult) {
4551
+ if (typeof flagPreviousForDeleteResult === 'object') {
4552
+ flagPreviousForDelete = flagPreviousForDeleteResult;
4553
+ }
4554
+ else {
4555
+ const { p, pg, u } = createStorageFileResult.storageFile;
4556
+ if (!p || !u) {
4557
+ throw new Error('initializeFromUpload(): flagPreviousForDelete=true requires that the created StorageFile have a purpose (p) and user (u).');
4558
+ }
4559
+ flagPreviousForDelete = {
4560
+ purpose: p,
4561
+ purposeSubgroup: pg,
4562
+ user: u
4563
+ };
4564
+ }
4565
+ }
4566
+ }
4567
+ else {
4568
+ createdFilePath = copyStoragePath(initializerResult.createdFile.storagePath);
4569
+ storageFileDocument = initializerResult.storageFileDocument;
4570
+ flagPreviousForDelete = initializerResult.flagPreviousForDelete;
4571
+ }
4572
+ // sanitize the returned value, incase the result comes from a transaction
4573
+ if (storageFileDocument) {
4574
+ storageFileDocument = storageFileCollection.documentAccessor().loadDocumentFrom(storageFileDocument);
4575
+ }
4576
+ // set the metadata on the associated file
4577
+ try {
4578
+ const createdFile = storageService.file(createdFilePath);
4579
+ const fileMetadata = await createdFile.getMetadata();
4580
+ await createdFile.setMetadata({
4581
+ customMetadata: {
4582
+ ...fileMetadata.customMetadata,
4583
+ [STORAGEFILE_RELATED_FILE_METADATA_KEY]: storageFileDocument.id
4584
+ }
4585
+ });
4586
+ }
4587
+ catch (e) {
4588
+ // failed to set the metadata. It isn't strictly necessary, so don't throw an error unless configured to always throw an error
4589
+ if (requireStorageFileRelatedFileMetadataBeSet) {
4590
+ // mark the created item for delete
4591
+ await storageFileDocument.update(markStorageFileForDeleteTemplate());
4592
+ // throw the exception
4593
+ throw e;
4594
+ }
4595
+ }
4596
+ // if flagPreviousForDelete is set, flag the previous storage files for deletion
4597
+ if (flagPreviousForDelete) {
4598
+ const flagForDeleteResult = await queryAndFlagStorageFilesForDelete({
4599
+ storageFileCollection,
4600
+ constraints: storageFilePurposeAndUserQuery(flagPreviousForDelete),
4601
+ skipDeleteForKeys: [storageFileDocument.key]
4602
+ });
4603
+ previousStorageFilesFlaggedForDeletion = flagForDeleteResult.queuedForDeleteCount;
4604
+ }
4605
+ }
4606
+ }
4607
+ catch (e) {
4608
+ resultType = 'initializer_error';
4609
+ processorError = e;
4610
+ }
4611
+ }
4612
+ else {
4613
+ resultType = 'no_initializer_configured';
4614
+ }
4615
+ }
4616
+ else {
4617
+ resultType = 'no_determiner_match';
4618
+ }
4619
+ const result = {
4620
+ resultType,
4621
+ storageFileDocument,
4622
+ initializationError: processorError,
4623
+ previousStorageFilesFlaggedForDeletion
4624
+ };
4625
+ return result;
4626
+ }
4627
+ };
4628
+ }
4629
+
4630
+ export { AbstractAppNotificationModule, BASE_NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN, BASE_STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN, GlobalNotificationModule, KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS, KNOWN_BUT_UNCONFIGURED_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY, MAILGUN_NOTIFICATION_EMAIL_SEND_SERVICE_DEFAULT_MAX_BATCH_SIZE_PER_REQUEST, MAKE_TEMPLATE_FOR_NOTIFICATION_RELATED_MODEL_INITIALIZATION_FUNCTION_DELETE_RESPONSE, MAKE_TEMPLATE_FOR_STORAGEFILE_RELATED_MODEL_INITIALIZATION_FUNCTION_DELETE_RESPONSE, MutableNotificationExpediteService, NOTIFICATION_BOX_NOT_INITIALIZED_DELAY_MINUTES, NOTIFICATION_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN, NOTIFICATION_MAX_SEND_ATTEMPTS, NOTIFICATION_SERVER_ACTION_CONTEXT_TOKEN, NOTIFICATION_TASK_MINIMUM_SET_AT_THROTTLE_TIME_MINUTES, NOTIFICATION_TASK_TYPE_FAILURE_DELAY_HOURS, NOTIFICATION_TASK_TYPE_FAILURE_DELAY_MS, NOTIFICATION_TASK_TYPE_MAX_SEND_ATTEMPTS, NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY_TOKEN, NOTIFICATION_TEMPLATE_SERVICE_DEFAULTS_OVERRIDE_TOKEN, NotificationExpediteService, NotificationInitServerActions, NotificationSendService, NotificationServerActions, NotificationTaskService, NotificationTaskSubTaskMissingRequiredDataTermination, NotificationTemplateService, SEND_QUEUE_NOTIFICATIONS_TASK_EXCESS_THRESHOLD, STORAGE_FILE_INIT_SERVER_ACTIONS_CONTEXT_CONFIG_TOKEN, STORAGE_FILE_SERVER_ACTION_CONTEXT_TOKEN, StorageFileInitServerActions, StorageFileInitializeFromUploadService, StorageFileServerActions, UNKNOWN_NOTIFICATION_TASK_TYPE_DELETE_AFTER_RETRY_ATTEMPTS, UNKNOWN_NOTIFICATION_TASK_TYPE_HOURS_DELAY, UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_DELETE_AFTER_RETRY_ATTEMPTS, UNKNOWN_NOTIFICATION_TEMPLATE_TYPE_HOURS_DELAY, _initializeStorageFileFromUploadFileFactory, _processStorageFileInTransactionFactory, _syncStorageFileWithGroupsInTransactionFactory, allStorageFileGroupStorageFileProcessingPurposeSubtaskProcessors, appNotificationModuleMetadata, appStorageFileModuleMetadata, cleanupSentNotificationsFactory, createNotificationBoxFactory, createNotificationBoxInTransactionFactory, createNotificationIdRequiredError, createNotificationSummaryFactory, createNotificationUserFactory, createOrRunUniqueNotificationDocument, createStorageFileFactory, createStorageFileGroupFactory, createStorageFileGroupInTransactionFactory, createStorageFileGroupInputError, deleteAllQueuedStorageFilesFactory, deleteStorageFileFactory, downloadStorageFileFactory, expandNotificationRecipients, exportMutableNotificationExpediteService, firestoreNotificationSummarySendService, ignoreSendNotificationTextSendService, initializeAllApplicableNotificationBoxesFactory, initializeAllApplicableNotificationSummariesFactory, initializeAllApplicableStorageFileGroupsFactory, initializeAllStorageFilesFromUploadsFactory, initializeNotificationBoxFactory, initializeNotificationBoxInTransactionFactory, initializeNotificationModelInTransaction, initializeNotificationSummaryFactory, initializeNotificationSummaryInTransactionFactory, initializeStorageFileFromUploadFactory, initializeStorageFileGroupFactory, initializeStorageFileGroupInTransactionFactory, initializeStorageFileModelInTransaction, mailgunNotificationEmailSendService, makeNewNotificationSummaryTemplate, markStorageFileForDeleteTemplate, notificationBoxDoesNotExist, notificationBoxExclusionTargetInvalidError, notificationBoxExistsForModelError, notificationBoxRecipientDoesNotExistsError, notificationBoxUnregistredModelTypeInitializationError, notificationExpediteServiceInstance, notificationInitServerActions, notificationInitServerActionsFactory, notificationModelAlreadyInitializedError, notificationServerActions, notificationServerActionsContextFactory, notificationServerActionsFactory, notificationTaskService, notificationTaskSubTaskMissingRequiredDataTermination, notificationTaskSubtaskNotificationTaskHandlerFactory, notificationTemplateServiceInstance, notificationUserBlockedFromBeingAddedToRecipientsError, notificationUserInvalidUidForCreateError, notificationUserLockedConfigFromBeingUpdatedError, processAllQueuedStorageFilesFactory, processStorageFileFactory, provideMutableNotificationExpediteService, queryAndFlagStorageFilesForDelete, regenerateAllFlaggedStorageFileGroupsContentFactory, regenerateStorageFileGroupContentFactory, resyncAllNotificationUsersFactory, resyncNotificationUserFactory, sendNotificationFactory, sendQueuedNotificationsFactory, storageFileAlreadyProcessedError, storageFileCannotBeDeletedYetError, storageFileGroupQueuedForInitializationError, storageFileGroupZipStorageFileProcessingPurposeSubtaskProcessor, storageFileInitServerActions, storageFileInitServerActionsFactory, storageFileInitializeFromUploadService, storageFileInitializeFromUploadServiceInitializerResultPermanentFailure, storageFileModelAlreadyInitializedError, storageFileNotFlaggedForDeletionError, storageFileNotFlaggedForGroupsSyncError, storageFileProcessingNotAllowedForInvalidStateError, storageFileProcessingNotAvailableForTypeError, storageFileProcessingNotQueuedForProcessingError, storageFileProcessingNotificationTaskHandler, storageFileProcessingNotificationTaskHandlerDefaultCleanup, storageFileServerActions, storageFileServerActionsContextFactory, storageFileServerActionsFactory, syncAllFlaggedStorageFilesWithGroupsFactory, syncStorageFileWithGroupsFactory, updateNotificationBoxFactory, updateNotificationBoxRecipientExclusionInTransactionFactory, updateNotificationBoxRecipientFactory, updateNotificationBoxRecipientInTransactionFactory, updateNotificationSummaryFactory, updateNotificationUserFactory, updateNotificationUserNotificationBoxRecipientConfig, updateStorageFileFactory, updateStorageFileGroupFactory, uploadedFileDoesNotExistError, uploadedFileInitializationDiscardedError, uploadedFileInitializationFailedError, uploadedFileIsNotAllowedToBeInitializedError };