musora-content-services 2.118.1 → 2.119.0

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 (217) hide show
  1. package/.coderabbit.yaml +0 -0
  2. package/.editorconfig +0 -0
  3. package/.github/pull_request_template.md +0 -0
  4. package/.github/workflows/conventional-commits.yaml +0 -0
  5. package/.github/workflows/docs.js.yml +0 -0
  6. package/.github/workflows/node.js.yml +0 -0
  7. package/.prettierignore +0 -0
  8. package/.prettierrc +0 -0
  9. package/CHANGELOG.md +7 -0
  10. package/CLAUDE.md +0 -0
  11. package/README.md +0 -0
  12. package/babel.config.cjs +0 -0
  13. package/jsdoc.json +0 -0
  14. package/package.json +1 -1
  15. package/src/constants/award-assets.js +0 -0
  16. package/src/constants/membership-permissions.ts +0 -0
  17. package/src/filterBuilder.js +0 -0
  18. package/src/infrastructure/http/HttpClient.ts +0 -0
  19. package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
  20. package/src/infrastructure/http/index.ts +0 -0
  21. package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
  22. package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
  23. package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
  24. package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
  25. package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
  26. package/src/lib/ads/monoid.ts +0 -0
  27. package/src/lib/ads/semigroup.ts +0 -0
  28. package/src/lib/brands.ts +0 -0
  29. package/src/lib/lastUpdated.js +0 -0
  30. package/src/lib/sanity/filter.ts +0 -0
  31. package/src/lib/sanity/query.ts +0 -0
  32. package/src/services/api/types.js +0 -0
  33. package/src/services/api/types.ts +0 -0
  34. package/src/services/awards/award-callbacks.js +0 -0
  35. package/src/services/awards/award-query.js +0 -0
  36. package/src/services/awards/internal/.indexignore +0 -0
  37. package/src/services/awards/internal/award-definitions.js +0 -0
  38. package/src/services/awards/internal/award-events.js +0 -0
  39. package/src/services/awards/internal/award-manager.js +0 -0
  40. package/src/services/awards/internal/certificate-builder.js +0 -0
  41. package/src/services/awards/internal/completion-data-generator.js +0 -0
  42. package/src/services/awards/internal/content-progress-observer.js +0 -0
  43. package/src/services/awards/internal/image-utils.js +0 -0
  44. package/src/services/awards/internal/message-generator.js +0 -0
  45. package/src/services/awards/internal/types.js +0 -0
  46. package/src/services/awards/types.d.ts +0 -0
  47. package/src/services/awards/types.js +0 -0
  48. package/src/services/content/artist.ts +0 -0
  49. package/src/services/content/content.ts +0 -0
  50. package/src/services/content/genre.ts +0 -0
  51. package/src/services/content/instructor.ts +0 -0
  52. package/src/services/content-org/content-org.js +0 -0
  53. package/src/services/content-org/guided-courses.ts +0 -0
  54. package/src/services/content-org/learning-paths.ts +0 -0
  55. package/src/services/content-org/playlists-types.js +0 -0
  56. package/src/services/content-org/playlists.js +0 -0
  57. package/src/services/contentAggregator.js +0 -0
  58. package/src/services/contentLikes.js +0 -0
  59. package/src/services/contentProgress.js +0 -0
  60. package/src/services/dateUtils.js +0 -0
  61. package/src/services/eventsAPI.js +0 -0
  62. package/src/services/forums/categories.ts +0 -0
  63. package/src/services/forums/forums.ts +0 -0
  64. package/src/services/forums/posts.ts +0 -0
  65. package/src/services/forums/threads.ts +0 -0
  66. package/src/services/forums/types.ts +0 -0
  67. package/src/services/gamification/awards.ts +0 -0
  68. package/src/services/gamification/gamification.js +0 -0
  69. package/src/services/imageSRCBuilder.js +0 -0
  70. package/src/services/imageSRCVerify.js +0 -0
  71. package/src/services/liveTesting.ts +0 -0
  72. package/src/services/permissions/PermissionsAdapter.ts +0 -0
  73. package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
  74. package/src/services/permissions/PermissionsV1Adapter.ts +0 -0
  75. package/src/services/permissions/PermissionsV2Adapter.ts +0 -0
  76. package/src/services/permissions/README.md +0 -0
  77. package/src/services/permissions/index.ts +0 -0
  78. package/src/services/progress-events.js +0 -0
  79. package/src/services/progress-row/rows/.indexignore +0 -0
  80. package/src/services/progress-row/rows/content-card.js +0 -0
  81. package/src/services/progress-row/rows/playlist-card.js +0 -0
  82. package/src/services/railcontent.js +0 -0
  83. package/src/services/reporting/README.md +0 -0
  84. package/src/services/reporting/reporting.ts +0 -0
  85. package/src/services/reporting/types.ts +0 -0
  86. package/src/services/sentry/.indexignore +0 -0
  87. package/src/services/sentry/index.ts +0 -0
  88. package/src/services/sync/.indexignore +0 -0
  89. package/src/services/sync/adapters/factory.ts +0 -0
  90. package/src/services/sync/adapters/lokijs.ts +0 -0
  91. package/src/services/sync/adapters/sqlite.ts +0 -0
  92. package/src/services/sync/context/index.ts +0 -0
  93. package/src/services/sync/context/providers/base.ts +0 -0
  94. package/src/services/sync/context/providers/connectivity.ts +0 -0
  95. package/src/services/sync/context/providers/durability.ts +0 -0
  96. package/src/services/sync/context/providers/index.ts +0 -0
  97. package/src/services/sync/context/providers/session.ts +0 -0
  98. package/src/services/sync/context/providers/tabs.ts +0 -0
  99. package/src/services/sync/context/providers/visibility.ts +0 -0
  100. package/src/services/sync/database/factory.ts +0 -0
  101. package/src/services/sync/effects/index.ts +0 -0
  102. package/src/services/sync/effects/logout-warning.ts +0 -0
  103. package/src/services/sync/errors/boundary.ts +0 -0
  104. package/src/services/sync/errors/index.ts +0 -0
  105. package/src/services/sync/errors/validators.ts +0 -0
  106. package/src/services/sync/fetch.ts +0 -0
  107. package/src/services/sync/index.ts +0 -0
  108. package/src/services/sync/manager.ts +4 -0
  109. package/src/services/sync/models/Base.ts +0 -0
  110. package/src/services/sync/models/ContentLike.ts +0 -0
  111. package/src/services/sync/models/ContentProgress.ts +0 -0
  112. package/src/services/sync/models/Practice.ts +0 -0
  113. package/src/services/sync/models/PracticeDayNote.ts +0 -0
  114. package/src/services/sync/models/UserAwardProgress.ts +0 -0
  115. package/src/services/sync/models/index.ts +0 -0
  116. package/src/services/sync/repositories/base.ts +18 -0
  117. package/src/services/sync/repositories/content-likes.ts +0 -0
  118. package/src/services/sync/repositories/content-progress.ts +0 -0
  119. package/src/services/sync/repositories/index.ts +0 -0
  120. package/src/services/sync/repositories/practice-day-notes.ts +0 -0
  121. package/src/services/sync/repositories/practices.ts +0 -0
  122. package/src/services/sync/repositories/user-award-progress.ts +0 -0
  123. package/src/services/sync/repository-proxy.ts +0 -0
  124. package/src/services/sync/resolver.ts +0 -0
  125. package/src/services/sync/retry.ts +0 -0
  126. package/src/services/sync/run-scope.ts +0 -0
  127. package/src/services/sync/schema/index.ts +0 -0
  128. package/src/services/sync/serializers/index.ts +0 -0
  129. package/src/services/sync/serializers/model.ts +0 -0
  130. package/src/services/sync/serializers/raw.ts +0 -0
  131. package/src/services/sync/store/index.ts +154 -33
  132. package/src/services/sync/store/push-coalescer.ts +0 -0
  133. package/src/services/sync/store-configs.ts +0 -0
  134. package/src/services/sync/strategies/base.ts +0 -0
  135. package/src/services/sync/strategies/index.ts +0 -0
  136. package/src/services/sync/strategies/initial.ts +0 -0
  137. package/src/services/sync/strategies/polling.ts +0 -0
  138. package/src/services/sync/telemetry/flood-prevention.ts +0 -0
  139. package/src/services/sync/telemetry/index.ts +0 -0
  140. package/src/services/sync/telemetry/sampling.ts +0 -0
  141. package/src/services/sync/utils/event-emitter.ts +0 -0
  142. package/src/services/sync/utils/index.ts +0 -0
  143. package/src/services/sync/utils/throttle.ts +0 -0
  144. package/src/services/sync/utils/timers.ts +0 -0
  145. package/src/services/urlBuilder.ts +0 -0
  146. package/src/services/user/account.ts +0 -0
  147. package/src/services/user/chat.js +0 -0
  148. package/src/services/user/interests.js +0 -0
  149. package/src/services/user/management.js +0 -0
  150. package/src/services/user/memberships.ts +0 -0
  151. package/src/services/user/notifications.js +0 -0
  152. package/src/services/user/onboarding.ts +0 -0
  153. package/src/services/user/payments.ts +0 -0
  154. package/src/services/user/permissions.js +0 -0
  155. package/src/services/user/profile.js +0 -0
  156. package/src/services/user/types.d.ts +0 -0
  157. package/src/services/user/types.js +0 -0
  158. package/src/services/user/user-management-system.js +0 -0
  159. package/src/services/userActivity.js +5 -48
  160. package/test/HttpClient.test.js +0 -0
  161. package/test/awards/award-alacarte-observer.test.js +0 -0
  162. package/test/awards/award-auto-refresh.test.js +0 -0
  163. package/test/awards/award-calculations.test.js +0 -0
  164. package/test/awards/award-certificate-display.test.js +0 -0
  165. package/test/awards/award-collection-edge-cases.test.js +0 -0
  166. package/test/awards/award-collection-filtering.test.js +0 -0
  167. package/test/awards/award-completion-flow.test.js +0 -0
  168. package/test/awards/award-exclusion-handling.test.js +0 -0
  169. package/test/awards/award-multi-lesson.test.js +0 -0
  170. package/test/awards/award-observer-integration.test.js +0 -0
  171. package/test/awards/award-query-messages.test.js +0 -0
  172. package/test/awards/award-user-collection.test.js +0 -0
  173. package/test/awards/duplicate-prevention.test.js +0 -0
  174. package/test/awards/helpers/completion-mock.js +0 -0
  175. package/test/awards/helpers/index.js +0 -0
  176. package/test/awards/helpers/mock-setup.js +0 -0
  177. package/test/awards/helpers/progress-emitter.js +0 -0
  178. package/test/awards/message-generator.test.js +0 -0
  179. package/test/content.test.js +0 -0
  180. package/test/contentLikes.test.js +0 -0
  181. package/test/contentProgress.test.js +0 -0
  182. package/test/dataContext.test.js +0 -0
  183. package/test/forum.test.js +0 -0
  184. package/test/imageSRCBuilder.test.js +0 -0
  185. package/test/imageSRCVerify.test.js +0 -0
  186. package/test/initializeTests.js +0 -0
  187. package/test/learningPaths.test.js +0 -0
  188. package/test/lib/__snapshots__/filter.test.ts.snap +0 -0
  189. package/test/lib/filter.test.ts +0 -0
  190. package/test/lib/lastUpdated.test.js +0 -0
  191. package/test/lib/query.test.ts +0 -0
  192. package/test/live/contentProgressLive.test.js +0 -0
  193. package/test/live/railcontentLive.test.js +0 -0
  194. package/test/log.js +0 -0
  195. package/test/mockData/award-definitions.js +0 -0
  196. package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
  197. package/test/mockData/mockData_progress_content.json +0 -0
  198. package/test/mockData/mockData_sanity_progress_content.json +0 -0
  199. package/test/mockData/mockData_user_practices.json +0 -0
  200. package/test/notifications.test.js +0 -0
  201. package/test/progressRows.test.js +0 -0
  202. package/test/sanityQueryService.test.js +0 -0
  203. package/test/streakMessage.test.js +0 -0
  204. package/test/sync/adapter.ts +0 -0
  205. package/test/sync/initialize-sync-manager.js +0 -0
  206. package/test/sync/models/award-database-integration.test.js +0 -0
  207. package/test/user/permissions.test.js +0 -0
  208. package/test/userActivity.test.js +0 -0
  209. package/tools/generate-index.cjs +0 -0
  210. package/.claude/settings.local.json +0 -16
  211. package/.yarnrc.yml +0 -1
  212. package/check_content.js +0 -30
  213. package/check_content.mjs +0 -32
  214. package/test/logout.test.js +0 -199
  215. package/test/reporting.test.js +0 -132
  216. package/test_owned_navigate.js +0 -74
  217. package/tsconfig.json +0 -17
@@ -1,4 +1,4 @@
1
- import { Database, Q, type Collection, type RecordId } from '@nozbe/watermelondb'
1
+ import { Database, Q, Query, type Collection, type RecordId } from '@nozbe/watermelondb'
2
2
  import { RawSerializer, ModelSerializer } from '../serializers'
3
3
  import { ModelClass, SyncToken, SyncEntry, SyncContext, EpochMs } from '..'
4
4
  import { SyncPullResponse, SyncPushResponse, SyncPullFetchFailureResponse, PushPayload, SyncStorePushResultSuccess, SyncStorePushResultFailure } from '../fetch'
@@ -17,7 +17,6 @@ import { type WriterInterface } from '@nozbe/watermelondb/Database/WorkQueue'
17
17
  import type LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
18
18
  import { SyncError } from '../errors'
19
19
 
20
-
21
20
  type SyncPull = (
22
21
  session: BaseSessionProvider,
23
22
  previousFetchToken: SyncToken | null,
@@ -39,6 +38,8 @@ export type SyncStoreConfig<TModel extends BaseModel = BaseModel> = {
39
38
  export default class SyncStore<TModel extends BaseModel = BaseModel> {
40
39
  static readonly PULL_THROTTLE_INTERVAL = 2_000
41
40
  static readonly PUSH_THROTTLE_INTERVAL = 1_000
41
+ static readonly DELETED_RECORD_GRACE_PERIOD = 60_000 // 60s
42
+ static readonly CLEANUP_INTERVAL = 60_000 * 60 // 1hr
42
43
 
43
44
  readonly telemetry: SyncTelemetry
44
45
  readonly context: SyncContext
@@ -60,6 +61,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
60
61
  private pushCoalescer = new PushCoalescer()
61
62
 
62
63
  private emitter = new EventEmitter()
64
+ private cleanupTimer: NodeJS.Timeout | null = null
63
65
 
64
66
  private lastFetchTokenKey: string
65
67
 
@@ -91,12 +93,18 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
91
93
  this.lastFetchTokenKey = `last_fetch_token:${this.model.table}`
92
94
 
93
95
  this.telemetry = telemetry
96
+
97
+ this.startCleanupTimer()
94
98
  }
95
99
 
96
100
  on = this.emitter.on.bind(this.emitter)
97
101
  off = this.emitter.off.bind(this.emitter)
98
102
  private emit = this.emitter.emit.bind(this.emitter)
99
103
 
104
+ destroy() {
105
+ this.stopCleanupTimer()
106
+ }
107
+
100
108
  async requestSync(reason: string) {
101
109
  inBoundary(ctx => {
102
110
  this.telemetry.trace(
@@ -176,6 +184,10 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
176
184
  return this.queryRecordIds(...args)
177
185
  }
178
186
 
187
+ async queryAllDeletedIds(...args: Q.Clause[]) {
188
+ return this.queryMaybeDeletedRecordIds(...args)
189
+ }
190
+
179
191
  async queryOne(...args: Q.Clause[]) {
180
192
  const record = await this.queryRecord(...args)
181
193
  return record ? this.modelSerializer.toPlainObject(record) : null
@@ -359,6 +371,41 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
359
371
  })
360
372
  }
361
373
 
374
+ async restoreOne(id: RecordId, span?: Span) {
375
+ return this.restoreSome([id], span).then(r => r[0])
376
+ }
377
+
378
+ async restoreSome(ids: RecordId[], span?: Span) {
379
+ return this.runScope.abortable(async () => {
380
+ const records = await this.telemeterizedWrite(span, async writer => {
381
+ const records = await writer.callReader(() => this.queryMaybeDeletedRecords(
382
+ Q.where('id', Q.oneOf(ids)),
383
+ Q.where('_status', 'deleted')
384
+ ))
385
+
386
+ const destroyBuilds = records.map(record => new this.model(this.collection, { id: record.id }).prepareDestroyPermanently())
387
+ const createBuilds = records.map(record => this.collection.prepareCreate((r) => {
388
+ Object.keys(record._raw).forEach((key) => {
389
+ r._raw[key] = record._raw[key]
390
+ })
391
+ r._raw._status = 'updated'
392
+ }))
393
+
394
+ await writer.batch(...destroyBuilds)
395
+ await writer.batch(...createBuilds)
396
+
397
+ return createBuilds
398
+ })
399
+
400
+ this.emit('upserted', records)
401
+
402
+ this.pushUnsyncedWithRetry(span)
403
+ await this.ensurePersistence()
404
+
405
+ return records.map((record) => this.modelSerializer.toPlainObject(record))
406
+ })
407
+ }
408
+
362
409
  async importUpsert(recordRaws: TModel['_raw'][]) {
363
410
  await this.runScope.abortable(async () => {
364
411
  await this.telemeterizedWrite(undefined, async writer => {
@@ -637,31 +684,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
637
684
  return this.db.read(async () => {
638
685
  const undeletedRecords = await this.collection.query(...args).fetch()
639
686
 
640
- const serializedQuery = this.collection.query(...args).serialize()
641
- const adjustedQuery = {
642
- ...serializedQuery,
643
- description: {
644
- ...serializedQuery.description,
645
- where: [
646
- // remove the default "not deleted" clause added by WatermelonDB
647
- ...serializedQuery.description.where.filter(
648
- (w) =>
649
- !(
650
- w.type === 'where' &&
651
- w.left === '_status' &&
652
- w.comparison &&
653
- w.comparison.operator === 'notEq' &&
654
- w.comparison.right &&
655
- 'value' in w.comparison.right &&
656
- w.comparison.right.value === 'deleted'
657
- )
658
- ),
659
-
660
- // and add our own "include deleted" clause
661
- Q.where('_status', Q.eq('deleted'))
662
- ],
663
- },
664
- }
687
+ const adjustedQuery = this.maybeDeletedQuery(this.collection.query(...args))
665
688
 
666
689
  // NOTE: constructing models in this way is a bit of a hack,
667
690
  // but since deleted records aren't "resurrectable" in WatermelonDB anyway,
@@ -682,6 +705,54 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
682
705
  })
683
706
  }
684
707
 
708
+ /**
709
+ * Query records including ones marked as deleted
710
+ * WatermelonDB by default excludes deleted records from queries
711
+ */
712
+ private async queryMaybeDeletedRecordIds(...args: Q.Clause[]) {
713
+ return this.db.read(async () => {
714
+ const undeletedRecordIds = await this.collection.query(...args).fetchIds()
715
+
716
+ const adjustedQuery = this.maybeDeletedQuery(this.collection.query(...args))
717
+ const deletedRecordIds = (await this.db.adapter.unsafeQueryRaw(adjustedQuery)).map(r => r.id)
718
+
719
+ return [
720
+ ...undeletedRecordIds,
721
+ ...deletedRecordIds,
722
+ ]
723
+ })
724
+ }
725
+
726
+ private maybeDeletedQuery(query: Query<TModel>) {
727
+ const serializedQuery = query.serialize()
728
+ const adjustedQuery = {
729
+ ...serializedQuery,
730
+ description: {
731
+ ...serializedQuery.description,
732
+ where: [
733
+ // remove the default "not deleted" clause added by WatermelonDB
734
+ ...serializedQuery.description.where.filter(
735
+ (w) =>
736
+ !(
737
+ w.type === 'where' &&
738
+ w.left === '_status' &&
739
+ w.comparison &&
740
+ w.comparison.operator === 'notEq' &&
741
+ w.comparison.right &&
742
+ 'value' in w.comparison.right &&
743
+ w.comparison.right.value === 'deleted'
744
+ )
745
+ ),
746
+
747
+ // and add our own "include deleted" clause
748
+ Q.where('_status', Q.eq('deleted'))
749
+ ],
750
+ },
751
+ }
752
+
753
+ return adjustedQuery
754
+ }
755
+
685
756
  // Avoid lazy persistence to IndexedDB
686
757
  // to eliminate data loss risk due to tab close/crash before flush to IndexedDB
687
758
  // https://github.com/Nozbe/WatermelonDB/issues/1329
@@ -739,6 +810,9 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
739
810
  }
740
811
 
741
812
  private async buildWriteBatchesFromEntries(writer: WriterInterface, entries: SyncEntry[], freshSync: boolean) {
813
+ // Clean up old deleted records during pull operations
814
+ await this.cleanupOldDeletedRecords(writer)
815
+
742
816
  // if this is a fresh sync and there are no existing records, we can skip more sophisticated conflict resolution
743
817
  if (freshSync) {
744
818
  if ((await writer.callReader(() => this.queryMaybeDeletedRecords())).length === 0) {
@@ -747,7 +821,7 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
747
821
  .filter((e) => !e.meta.lifecycle.deleted_at)
748
822
  .forEach((entry) => resolver.againstNone(entry))
749
823
 
750
- return this.prepareRecords(resolver.result)
824
+ return this.prepareRecords(resolver.result, new Map())
751
825
  }
752
826
  }
753
827
 
@@ -789,17 +863,26 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
789
863
  }
790
864
  })
791
865
 
792
- return this.prepareRecords(resolver.result)
866
+ return this.prepareRecords(resolver.result, existingRecordsMap)
793
867
  }
794
868
 
795
- private prepareRecords(result: SyncResolution) {
869
+ private prepareRecords(result: SyncResolution, existingRecordsMap: Map<RecordId, TModel>) {
796
870
  if (Object.values(result).find((c) => c.length)) {
797
871
  this.telemetry.debug(`[store:${this.model.table}] Writing changes`, { changes: result })
798
872
  }
799
873
 
800
- const destroyedBuilds = result.idsForDestroy.map((id) => {
801
- return new this.model(this.collection, { id }).prepareDestroyPermanently()
802
- })
874
+ const destroyedBuilds = result.idsForDestroy
875
+ .filter(id => {
876
+ // Only permanently delete if updated_at is older than grace period
877
+ const record = existingRecordsMap.get(id)
878
+ if (!record) return true // If no record found, safe to destroy
879
+
880
+ const gracePeriodAgo = Date.now() - SyncStore.DELETED_RECORD_GRACE_PERIOD
881
+ return record.updated_at < gracePeriodAgo
882
+ })
883
+ .map((id) => {
884
+ return new this.model(this.collection, { id }).prepareDestroyPermanently()
885
+ })
803
886
  const createdBuilds = result.entriesForCreate.map((entry) => {
804
887
  return this.collection.prepareCreate((r) => {
805
888
  Object.entries(entry.record!).forEach(([key, value]) => {
@@ -856,4 +939,42 @@ export default class SyncStore<TModel extends BaseModel = BaseModel> {
856
939
  private isLokiAdapter(adapter: any): adapter is LokiJSAdapter {
857
940
  return adapter._driver && 'loki' in adapter._driver
858
941
  }
942
+
943
+ private startCleanupTimer() {
944
+ this.cleanupTimer = setInterval(() => {
945
+ this.runScope.abortable(async () => {
946
+ this.telemeterizedWrite(undefined, async (writer) => {
947
+ await this.cleanupOldDeletedRecords(writer)
948
+ })
949
+ })
950
+ }, SyncStore.CLEANUP_INTERVAL)
951
+ }
952
+
953
+ private stopCleanupTimer() {
954
+ if (this.cleanupTimer) {
955
+ clearInterval(this.cleanupTimer)
956
+ this.cleanupTimer = null
957
+ }
958
+ }
959
+
960
+ /** Destroy permanently records past their grace period
961
+ * (we need to keep records around after being marked deleted
962
+ * for undo purposes, so we don't discard them in writeEntries
963
+ * (after a server push), but instead every hour or so)
964
+ */
965
+ private async cleanupOldDeletedRecords(writer: WriterInterface) {
966
+ const gracePeriodAgo = Date.now() - SyncStore.DELETED_RECORD_GRACE_PERIOD
967
+
968
+ const oldDeletedRecords = await writer.callReader(() => this.queryMaybeDeletedRecords(
969
+ Q.where('_status', 'deleted'),
970
+ Q.where('updated_at', Q.lt(gracePeriodAgo))
971
+ ))
972
+
973
+ if (oldDeletedRecords.length > 0) {
974
+ this.telemetry.debug(`[store:${this.model.table}] Cleaning up ${oldDeletedRecords.length} old deleted records`)
975
+
976
+ const destroyBuilds = oldDeletedRecords.map(record => record.prepareDestroyPermanently())
977
+ return writer.batch(...destroyBuilds)
978
+ }
979
+ }
859
980
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -341,7 +341,7 @@ export async function removeUserPractice(id) {
341
341
  }
342
342
 
343
343
  /**
344
- * Restores a previously deleted user's practice session by ID, updating both the local and remote activity context.
344
+ * Restores a previously deleted user's practice session by ID
345
345
  *
346
346
  * @param {number} id - The unique identifier of the practice session to be restored.
347
347
  * @returns {Promise<Object>} - A promise that resolves to the response containing the restored practice session data.
@@ -353,35 +353,7 @@ export async function removeUserPractice(id) {
353
353
  * .catch(error => console.error(error));
354
354
  */
355
355
  export async function restoreUserPractice(id) {
356
- const url = `/api/user/practices/v1/practices/restore${buildQueryString([id])}`
357
- const response = await PUT(url, null)
358
- if (response?.data?.length) {
359
- const restoredPractice = response.data.find((p) => p.id === id)
360
- if (restoredPractice) {
361
- await userActivityContext.updateLocal(async function (localContext) {
362
- if (!localContext.data[DATA_KEY_PRACTICES][restoredPractice.day]) {
363
- localContext.data[DATA_KEY_PRACTICES][restoredPractice.day] = []
364
- }
365
- response.data.forEach((restoredPractice) => {
366
- localContext.data[DATA_KEY_PRACTICES][restoredPractice.day].push({
367
- id: restoredPractice.id,
368
- duration_seconds: restoredPractice.duration_seconds,
369
- })
370
- })
371
- })
372
- }
373
- }
374
- const formattedMeta = await formatPracticeMeta(response.data || [])
375
- const practiceDuration = formattedMeta.reduce(
376
- (total, practice) => total + (practice.duration || 0),
377
- 0
378
- )
379
- return {
380
- data: formattedMeta,
381
- message: response.message,
382
- version: response.version,
383
- practiceDuration,
384
- }
356
+ return await db.practices.restoreOne(id)
385
357
  }
386
358
 
387
359
  /**
@@ -422,25 +394,10 @@ export async function deletePracticeSession(day) {
422
394
  * .catch(error => console.error("Restore failed:", error));
423
395
  */
424
396
  export async function restorePracticeSession(date) {
425
- const url = `/api/user/practices/v1/practices/restore?date=${date}`
426
- const response = await PUT(url, null)
427
-
428
- if (response?.data) {
429
- await userActivityContext.updateLocal(async function (localContext) {
430
- if (!localContext.data[DATA_KEY_PRACTICES][date]) {
431
- localContext.data[DATA_KEY_PRACTICES][date] = []
432
- }
433
-
434
- response.data.forEach((restoredPractice) => {
435
- localContext.data[DATA_KEY_PRACTICES][date].push({
436
- id: restoredPractice.id,
437
- duration_seconds: restoredPractice.duration_seconds,
438
- })
439
- })
440
- })
441
- }
397
+ const ids = await db.practices.queryAllDeletedIds(Q.where('date', date))
398
+ const response = await db.practices.restoreSome(ids.data)
442
399
 
443
- const formattedMeta = await formatPracticeMeta(response?.data)
400
+ const formattedMeta = await formatPracticeMeta(response.data)
444
401
  const practiceDuration = formattedMeta.reduce(
445
402
  (total, practice) => total + (practice.duration || 0),
446
403
  0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/test/log.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,16 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Read(//app/musora-platform-backend/**)",
5
- "Read(//app/musora-platform-frontend/**)",
6
- "Bash(find:*)",
7
- "Bash(sed:*)",
8
- "Read(//app/**)",
9
- "Bash(cat:*)",
10
- "Bash(docker exec:*)",
11
- "Bash(npm config:*)"
12
- ],
13
- "deny": [],
14
- "ask": []
15
- }
16
- }
package/.yarnrc.yml DELETED
@@ -1 +0,0 @@
1
- nodeLinker: node-modules
package/check_content.js DELETED
@@ -1,30 +0,0 @@
1
- const { initializeService } = require('./src/services/config.js');
2
- const { fetchByRailContentIds } = require('./src/services/sanity.js');
3
- require('dotenv/config');
4
-
5
- async function checkContent() {
6
- initializeService({
7
- sanityConfig: {
8
- token: process.env.SANITY_TOKEN,
9
- projectId: process.env.SANITY_PROJECT_ID,
10
- dataset: process.env.SANITY_DATASET,
11
- version: process.env.SANITY_VERSION || '2021-06-07',
12
- },
13
- railcontentConfig: {
14
- token: process.env.RAILCONTENT_TOKEN,
15
- userId: process.env.RAILCONTENT_USER_ID,
16
- baseUrl: process.env.RAILCONTENT_BASE_URL,
17
- authToken: process.env.RAILCONTENT_AUTH_TOKEN,
18
- },
19
- baseUrl: process.env.RAILCONTENT_BASE_URL,
20
- localStorage: null,
21
- isMA: false,
22
- });
23
-
24
- console.log('Checking railcontent_id: 421814');
25
- const contents = await fetchByRailContentIds([421814]);
26
- console.log('Results:', JSON.stringify(contents, null, 2));
27
- console.log('Found:', contents.length, 'items');
28
- }
29
-
30
- checkContent().catch(console.error);
package/check_content.mjs DELETED
@@ -1,32 +0,0 @@
1
- import { initializeService } from './src/services/config.js';
2
- import { fetchByRailContentIds } from './src/services/sanity.js';
3
- import dotenv from 'dotenv';
4
-
5
- dotenv.config();
6
-
7
- async function checkContent() {
8
- initializeService({
9
- sanityConfig: {
10
- token: process.env.SANITY_TOKEN,
11
- projectId: process.env.SANITY_PROJECT_ID,
12
- dataset: process.env.SANITY_DATASET,
13
- version: process.env.SANITY_VERSION || '2021-06-07',
14
- },
15
- railcontentConfig: {
16
- token: process.env.RAILCONTENT_TOKEN,
17
- userId: process.env.RAILCONTENT_USER_ID,
18
- baseUrl: process.env.RAILCONTENT_BASE_URL,
19
- authToken: process.env.RAILCONTENT_AUTH_TOKEN,
20
- },
21
- baseUrl: process.env.RAILCONTENT_BASE_URL,
22
- localStorage: null,
23
- isMA: false,
24
- });
25
-
26
- console.log('Checking railcontent_id: 421814');
27
- const contents = await fetchByRailContentIds([421814]);
28
- console.log('Results:', JSON.stringify(contents, null, 2));
29
- console.log('Found:', contents.length, 'items');
30
- }
31
-
32
- checkContent().catch(console.error);