musora-content-services 2.118.1 → 2.119.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.
- package/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/conventional-commits.yaml +0 -0
- package/.github/workflows/docs.js.yml +0 -0
- package/.github/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +0 -0
- package/README.md +0 -0
- package/babel.config.cjs +0 -0
- package/jsdoc.json +0 -0
- package/package.json +1 -1
- package/src/constants/award-assets.js +0 -0
- package/src/constants/membership-permissions.ts +0 -0
- package/src/filterBuilder.js +0 -0
- package/src/infrastructure/http/HttpClient.ts +0 -0
- package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
- package/src/infrastructure/http/index.ts +0 -0
- package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
- package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
- package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
- package/src/lib/ads/monoid.ts +0 -0
- package/src/lib/ads/semigroup.ts +0 -0
- package/src/lib/brands.ts +0 -0
- package/src/lib/lastUpdated.js +0 -0
- package/src/lib/sanity/filter.ts +0 -0
- package/src/lib/sanity/query.ts +0 -0
- package/src/services/api/types.js +0 -0
- package/src/services/api/types.ts +0 -0
- package/src/services/awards/award-callbacks.js +0 -0
- package/src/services/awards/award-query.js +0 -0
- package/src/services/awards/internal/.indexignore +0 -0
- package/src/services/awards/internal/award-definitions.js +0 -0
- package/src/services/awards/internal/award-events.js +0 -0
- package/src/services/awards/internal/award-manager.js +0 -0
- package/src/services/awards/internal/certificate-builder.js +0 -0
- package/src/services/awards/internal/completion-data-generator.js +0 -0
- package/src/services/awards/internal/content-progress-observer.js +0 -0
- package/src/services/awards/internal/image-utils.js +0 -0
- package/src/services/awards/internal/message-generator.js +0 -0
- package/src/services/awards/internal/types.js +0 -0
- package/src/services/awards/types.d.ts +0 -0
- package/src/services/awards/types.js +0 -0
- package/src/services/content/artist.ts +0 -0
- package/src/services/content/content.ts +0 -0
- package/src/services/content/genre.ts +0 -0
- package/src/services/content/instructor.ts +0 -0
- package/src/services/content-org/content-org.js +0 -0
- package/src/services/content-org/guided-courses.ts +0 -0
- package/src/services/content-org/learning-paths.ts +0 -0
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +0 -0
- package/src/services/contentAggregator.js +0 -0
- package/src/services/contentLikes.js +0 -0
- package/src/services/contentProgress.js +0 -0
- package/src/services/dateUtils.js +0 -0
- package/src/services/eventsAPI.js +0 -0
- package/src/services/forums/categories.ts +0 -0
- package/src/services/forums/forums.ts +0 -0
- package/src/services/forums/posts.ts +0 -0
- package/src/services/forums/threads.ts +0 -0
- package/src/services/forums/types.ts +0 -0
- package/src/services/gamification/awards.ts +0 -0
- package/src/services/gamification/gamification.js +0 -0
- package/src/services/imageSRCBuilder.js +0 -0
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/liveTesting.ts +0 -0
- package/src/services/permissions/PermissionsAdapter.ts +0 -0
- package/src/services/permissions/PermissionsAdapterFactory.ts +0 -0
- package/src/services/permissions/PermissionsV1Adapter.ts +0 -0
- package/src/services/permissions/PermissionsV2Adapter.ts +0 -0
- package/src/services/permissions/README.md +0 -0
- package/src/services/permissions/index.ts +0 -0
- package/src/services/progress-events.js +0 -0
- package/src/services/progress-row/rows/.indexignore +0 -0
- package/src/services/progress-row/rows/content-card.js +0 -0
- package/src/services/progress-row/rows/playlist-card.js +0 -0
- package/src/services/railcontent.js +0 -0
- package/src/services/reporting/README.md +0 -0
- package/src/services/reporting/reporting.ts +0 -0
- package/src/services/reporting/types.ts +0 -0
- package/src/services/sentry/.indexignore +0 -0
- package/src/services/sentry/index.ts +0 -0
- package/src/services/sync/.indexignore +0 -0
- package/src/services/sync/adapters/factory.ts +0 -0
- package/src/services/sync/adapters/lokijs.ts +0 -0
- package/src/services/sync/adapters/sqlite.ts +0 -0
- package/src/services/sync/context/index.ts +0 -0
- package/src/services/sync/context/providers/base.ts +0 -0
- package/src/services/sync/context/providers/connectivity.ts +0 -0
- package/src/services/sync/context/providers/durability.ts +0 -0
- package/src/services/sync/context/providers/index.ts +0 -0
- package/src/services/sync/context/providers/session.ts +0 -0
- package/src/services/sync/context/providers/tabs.ts +0 -0
- package/src/services/sync/context/providers/visibility.ts +0 -0
- package/src/services/sync/database/factory.ts +0 -0
- package/src/services/sync/effects/index.ts +0 -0
- package/src/services/sync/effects/logout-warning.ts +0 -0
- package/src/services/sync/errors/boundary.ts +0 -0
- package/src/services/sync/errors/index.ts +0 -0
- package/src/services/sync/errors/validators.ts +0 -0
- package/src/services/sync/fetch.ts +0 -0
- package/src/services/sync/index.ts +0 -0
- package/src/services/sync/manager.ts +4 -0
- package/src/services/sync/models/Base.ts +0 -0
- package/src/services/sync/models/ContentLike.ts +0 -0
- package/src/services/sync/models/ContentProgress.ts +0 -0
- package/src/services/sync/models/Practice.ts +0 -0
- package/src/services/sync/models/PracticeDayNote.ts +0 -0
- package/src/services/sync/models/UserAwardProgress.ts +0 -0
- package/src/services/sync/models/index.ts +0 -0
- package/src/services/sync/repositories/base.ts +18 -0
- package/src/services/sync/repositories/content-likes.ts +0 -0
- package/src/services/sync/repositories/content-progress.ts +0 -0
- package/src/services/sync/repositories/index.ts +0 -0
- package/src/services/sync/repositories/practice-day-notes.ts +0 -0
- package/src/services/sync/repositories/practices.ts +0 -0
- package/src/services/sync/repositories/user-award-progress.ts +0 -0
- package/src/services/sync/repository-proxy.ts +0 -0
- package/src/services/sync/resolver.ts +0 -0
- package/src/services/sync/retry.ts +0 -0
- package/src/services/sync/run-scope.ts +0 -0
- package/src/services/sync/schema/index.ts +0 -0
- package/src/services/sync/serializers/index.ts +0 -0
- package/src/services/sync/serializers/model.ts +0 -0
- package/src/services/sync/serializers/raw.ts +0 -0
- package/src/services/sync/store/index.ts +154 -33
- package/src/services/sync/store/push-coalescer.ts +0 -0
- package/src/services/sync/store-configs.ts +0 -0
- package/src/services/sync/strategies/base.ts +0 -0
- package/src/services/sync/strategies/index.ts +0 -0
- package/src/services/sync/strategies/initial.ts +0 -0
- package/src/services/sync/strategies/polling.ts +0 -0
- package/src/services/sync/telemetry/flood-prevention.ts +0 -0
- package/src/services/sync/telemetry/index.ts +0 -0
- package/src/services/sync/telemetry/sampling.ts +0 -0
- package/src/services/sync/utils/event-emitter.ts +0 -0
- package/src/services/sync/utils/index.ts +0 -0
- package/src/services/sync/utils/throttle.ts +0 -0
- package/src/services/sync/utils/timers.ts +0 -0
- package/src/services/urlBuilder.ts +0 -0
- package/src/services/user/account.ts +0 -0
- package/src/services/user/chat.js +0 -0
- package/src/services/user/interests.js +0 -0
- package/src/services/user/management.js +0 -0
- package/src/services/user/memberships.ts +0 -0
- package/src/services/user/notifications.js +0 -0
- package/src/services/user/onboarding.ts +0 -0
- package/src/services/user/payments.ts +0 -0
- package/src/services/user/permissions.js +0 -0
- package/src/services/user/profile.js +0 -0
- package/src/services/user/streakCalculator.ts +66 -0
- package/src/services/user/types.d.ts +0 -0
- package/src/services/user/types.js +0 -0
- package/src/services/user/user-management-system.js +0 -0
- package/src/services/userActivity.js +74 -103
- package/test/HttpClient.test.js +0 -0
- package/test/awards/award-alacarte-observer.test.js +0 -0
- package/test/awards/award-auto-refresh.test.js +0 -0
- package/test/awards/award-calculations.test.js +0 -0
- package/test/awards/award-certificate-display.test.js +0 -0
- package/test/awards/award-collection-edge-cases.test.js +0 -0
- package/test/awards/award-collection-filtering.test.js +0 -0
- package/test/awards/award-completion-flow.test.js +0 -0
- package/test/awards/award-exclusion-handling.test.js +0 -0
- package/test/awards/award-multi-lesson.test.js +0 -0
- package/test/awards/award-observer-integration.test.js +0 -0
- package/test/awards/award-query-messages.test.js +0 -0
- package/test/awards/award-user-collection.test.js +0 -0
- package/test/awards/duplicate-prevention.test.js +0 -0
- package/test/awards/helpers/completion-mock.js +0 -0
- package/test/awards/helpers/index.js +0 -0
- package/test/awards/helpers/mock-setup.js +0 -0
- package/test/awards/helpers/progress-emitter.js +0 -0
- package/test/awards/message-generator.test.js +0 -0
- package/test/content.test.js +0 -0
- package/test/contentLikes.test.js +0 -0
- package/test/contentProgress.test.js +0 -0
- package/test/dataContext.test.js +0 -0
- package/test/forum.test.js +0 -0
- package/test/imageSRCBuilder.test.js +0 -0
- package/test/imageSRCVerify.test.js +0 -0
- package/test/initializeTests.js +0 -0
- package/test/learningPaths.test.js +0 -0
- package/test/lib/__snapshots__/filter.test.ts.snap +0 -0
- package/test/lib/filter.test.ts +0 -0
- package/test/lib/lastUpdated.test.js +0 -0
- package/test/lib/query.test.ts +0 -0
- package/test/live/contentProgressLive.test.js +0 -0
- package/test/live/railcontentLive.test.js +0 -0
- package/test/log.js +0 -0
- package/test/mockData/award-definitions.js +0 -0
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
- package/test/mockData/mockData_progress_content.json +0 -0
- package/test/mockData/mockData_sanity_progress_content.json +0 -0
- package/test/mockData/mockData_user_practices.json +0 -0
- package/test/notifications.test.js +0 -0
- package/test/progressRows.test.js +0 -0
- package/test/sanityQueryService.test.js +0 -0
- package/test/streakMessage.test.js +0 -0
- package/test/sync/adapter.ts +0 -0
- package/test/sync/initialize-sync-manager.js +0 -0
- package/test/sync/models/award-database-integration.test.js +0 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +0 -0
- package/tools/generate-index.cjs +0 -0
- package/.claude/settings.local.json +0 -16
- package/.yarnrc.yml +0 -1
- package/check_content.js +0 -30
- package/check_content.mjs +0 -32
- package/test/logout.test.js +0 -199
- package/test/reporting.test.js +0 -132
- package/test_owned_navigate.js +0 -74
- 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
|
|
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
|
|
801
|
-
|
|
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
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { db } from '../sync'
|
|
2
|
+
import { getStreaksAndMessage } from '../../services/userActivity.js'
|
|
3
|
+
|
|
4
|
+
export interface StreakData {
|
|
5
|
+
currentDailyStreak: number
|
|
6
|
+
currentWeeklyStreak: number
|
|
7
|
+
streakMessage: string
|
|
8
|
+
calculatedAt: number // timestamp
|
|
9
|
+
lastPracticeDate: string | null
|
|
10
|
+
}
|
|
11
|
+
export interface PracticeData {
|
|
12
|
+
[date: string]: Array<{
|
|
13
|
+
id: string | number
|
|
14
|
+
duration_seconds: number
|
|
15
|
+
}>
|
|
16
|
+
}
|
|
17
|
+
class StreakCalculator {
|
|
18
|
+
private cache: StreakData | null = null
|
|
19
|
+
async getStreakData(): Promise<StreakData> {
|
|
20
|
+
if (this.cache) {
|
|
21
|
+
return this.cache
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return await this.recalculate()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async recalculate(): Promise<StreakData> {
|
|
28
|
+
const allPractices = await this.fetchAllPractices()
|
|
29
|
+
|
|
30
|
+
const { currentDailyStreak, currentWeeklyStreak, streakMessage } = getStreaksAndMessage(allPractices)
|
|
31
|
+
|
|
32
|
+
this.cache = {
|
|
33
|
+
currentDailyStreak: currentDailyStreak,
|
|
34
|
+
currentWeeklyStreak: currentWeeklyStreak,
|
|
35
|
+
streakMessage: streakMessage,
|
|
36
|
+
calculatedAt: Date.now(),
|
|
37
|
+
lastPracticeDate: this.getLastPracticeDate(allPractices)
|
|
38
|
+
}
|
|
39
|
+
return this.cache
|
|
40
|
+
}
|
|
41
|
+
invalidate(): void {
|
|
42
|
+
this.cache = null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async fetchAllPractices(): Promise<PracticeData> {
|
|
46
|
+
const query = await db.practices.queryAll()
|
|
47
|
+
|
|
48
|
+
return query.data.reduce((acc, practice) => {
|
|
49
|
+
acc[practice.date] = acc[practice.date] || []
|
|
50
|
+
acc[practice.date].push({
|
|
51
|
+
id: practice.id,
|
|
52
|
+
duration_seconds: practice.duration_seconds,
|
|
53
|
+
})
|
|
54
|
+
return acc
|
|
55
|
+
}, {} as PracticeData)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private getLastPracticeDate(practices: PracticeData): string | null {
|
|
59
|
+
const dates = Object.keys(practices).sort()
|
|
60
|
+
return dates.length > 0 ? dates[dates.length - 1] : null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
export const streakCalculator = new StreakCalculator()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|