musora-content-services 2.167.2 → 2.167.4
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/.agent/decisions/2026-07-20-mu2-1724-songs-lessons-counts.md +33 -0
- package/.agent/decisions/2026-07-20-query-builder-tostring.md +37 -0
- package/.idea/codeStyles/Project.xml +61 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/modules.xml +8 -0
- package/.idea/musora-content-services.iml +8 -0
- package/.idea/php.xml +19 -0
- package/.idea/prettier.xml +6 -0
- package/.idea/vcs.xml +7 -0
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +10 -0
- package/package.json +2 -1
- package/src/contentTypeConfig.js +1 -0
- package/src/lib/sanity/query.ts +5 -0
- package/src/services/config.js +15 -0
- package/src/services/content/artist.ts +1 -3
- package/src/services/content/counts.ts +47 -0
- package/src/services/content/genre.ts +1 -3
- package/src/services/content/instructor.ts +1 -3
- package/src/services/multi-user-accounts/multi-user-accounts.ts +1 -0
- package/src/services/userActivity.js +2 -0
- package/src/version-info.js +1 -0
- package/.claude/settings.local.json +0 -53
- package/test/initializeTests.js +0 -60
- package/test/integration/awards/award-exclusion-handling.test.ts +0 -275
- package/test/integration/awards/internal/image-utils.test.ts +0 -86
- package/test/integration/content-progress/__mocks__/mocks.ts +0 -104
- package/test/integration/content-progress/contentProgress.test.ts +0 -335
- package/test/integration/content-progress/e2eOfflineProgress.test.ts +0 -352
- package/test/integration/content-progress/e2eProgress.test.ts +0 -612
- package/test/integration/content-progress/getters.test.ts +0 -334
- package/test/integration/content-progress/helpers.test.ts +0 -263
- package/test/integration/content-progress/offlineContentProgress.test.ts +0 -226
- package/test/integration/forums.test.ts +0 -209
- package/test/integration/infrastructure/FetchRequestExecutor.test.ts +0 -88
- package/test/integration/initializeTestDB.ts +0 -80
- package/test/integration/learning-paths.test.ts +0 -854
- package/test/integration/notifications.test.ts +0 -178
- package/test/integration/progress-row/content-card.test.ts +0 -283
- package/test/integration/sync/adapters/idb-errors.test.ts +0 -144
- package/test/integration/sync/adapters/sqlite-errors.test.ts +0 -173
- package/test/integration/sync/fetch.test.ts +0 -224
- package/test/integration/sync/repositories/content-likes.test.ts +0 -99
- package/test/integration/sync/repositories/practices.test.ts +0 -179
- package/test/integration/sync/repositories/progress.test.ts +0 -245
- package/test/integration/sync/repositories/user-award-progress.static.test.ts +0 -68
- package/test/integration/sync/repositories/user-award-progress.test.ts +0 -387
- package/test/integration/sync/store/cross-user-protection.test.ts +0 -217
- package/test/integration/sync/store/store-idb.test.ts +0 -180
- package/test/integration/sync/store/store.test.ts +0 -361
- package/test/integration/user/permissions.test.js +0 -20
- package/test/integration/userActivity.test.ts +0 -102
- package/test/live/README.md +0 -29
- package/test/live/content.test.js +0 -116
- package/test/localStorageMock.js +0 -21
- package/test/log.js +0 -5
- package/test/mockData/award-definitions.js +0 -171
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -35
- package/test/mockData/mockData_progress_content.json +0 -182
- package/test/mockData/mockData_sanity_progress_content.json +0 -1451
- package/test/mockData/mockData_user_practices.json +0 -9
- package/test/setupConsole.js +0 -6
- package/test/setupNetworkGuard.js +0 -3
- package/test/setupTimers.js +0 -13
- package/test/unit/HttpClient.test.ts +0 -267
- package/test/unit/awards/award-alacarte-observer.test.ts +0 -199
- package/test/unit/awards/award-auto-refresh.test.ts +0 -84
- package/test/unit/awards/award-calculations.test.ts +0 -34
- package/test/unit/awards/award-callbacks.test.ts +0 -144
- package/test/unit/awards/award-certificate-display.test.ts +0 -330
- package/test/unit/awards/award-collection-edge-cases.test.ts +0 -212
- package/test/unit/awards/award-collection-filtering.test.ts +0 -287
- package/test/unit/awards/award-completion-flow.test.ts +0 -306
- package/test/unit/awards/award-multi-lesson.test.ts +0 -243
- package/test/unit/awards/award-observer-integration.test.ts +0 -327
- package/test/unit/awards/award-query-messages.test.ts +0 -448
- package/test/unit/awards/award-user-collection.test.ts +0 -416
- package/test/unit/awards/duplicate-prevention.test.ts +0 -120
- package/test/unit/awards/helpers/completion-mock.ts +0 -56
- package/test/unit/awards/helpers/index.ts +0 -3
- package/test/unit/awards/helpers/mock-setup.ts +0 -79
- package/test/unit/awards/helpers/progress-emitter.ts +0 -45
- package/test/unit/awards/message-generator.test.ts +0 -162
- package/test/unit/content-progress/bubbleTrickle.test.ts +0 -322
- package/test/unit/content-progress/helpers.test.ts +0 -329
- package/test/unit/content-progress/navigateTo.test.ts +0 -392
- package/test/unit/contentLikes.test.ts +0 -62
- package/test/unit/contentMetaData.test.ts +0 -58
- package/test/unit/dataContext.test.ts +0 -38
- package/test/unit/dateUtils.test.ts +0 -188
- package/test/unit/endScreen.test.js +0 -712
- package/test/unit/eventsAPI.test.ts +0 -159
- package/test/unit/imageSRCBuilder.test.ts +0 -37
- package/test/unit/imageSRCVerify.test.ts +0 -155
- package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +0 -39
- package/test/unit/infrastructure/sanity/ContentClient.test.ts +0 -168
- package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +0 -93
- package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +0 -174
- package/test/unit/infrastructure/sanity/SanityClient.test.ts +0 -140
- package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -5
- package/test/unit/lib/filter.test.ts +0 -1154
- package/test/unit/lib/lastUpdated.test.ts +0 -21
- package/test/unit/lib/query.test.ts +0 -1469
- package/test/unit/lib/sanity/decorators/base.test.ts +0 -368
- package/test/unit/lib/sanity/decorators/navigate-to.test.ts +0 -361
- package/test/unit/lib/sanity/decorators/need-access.test.ts +0 -89
- package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +0 -85
- package/test/unit/lib/sanity/decorators/page-type.test.ts +0 -81
- package/test/unit/playerSettings.test.ts +0 -73
- package/test/unit/progress-row/content-card.test.ts +0 -314
- package/test/unit/progress-row/playlist-card.test.ts +0 -104
- package/test/unit/progressRows.test.ts +0 -243
- package/test/unit/recommendations.test.js +0 -47
- package/test/unit/sanityQueryService.test.ts +0 -200
- package/test/unit/search.test.ts +0 -44
- package/test/unit/sentry.test.ts +0 -62
- package/test/unit/services/progress-internal/bubble.test.ts +0 -279
- package/test/unit/services/progress.test.ts +0 -396
- package/test/unit/streakMessage.test.ts +0 -251
- package/test/unit/sync/context.test.ts +0 -51
- package/test/unit/sync/effects/logout-warning.test.ts +0 -158
- package/test/unit/sync/effects/push-failure-notification.test.ts +0 -196
- package/test/unit/sync/errors/sync-errors.test.ts +0 -106
- package/test/unit/sync/errors/validators.test.ts +0 -61
- package/test/unit/sync/helpers/TestModel.ts +0 -44
- package/test/unit/sync/helpers/index.ts +0 -172
- package/test/unit/sync/manager.test.ts +0 -303
- package/test/unit/sync/models/user-award-progress.test.ts +0 -82
- package/test/unit/sync/resolver.test.ts +0 -229
- package/test/unit/sync/retry.test.ts +0 -314
- package/test/unit/sync/run-scope.test.ts +0 -23
- package/test/unit/sync/stale-record-cleanup.test.ts +0 -107
- package/test/unit/sync/store/push-coalescer.test.ts +0 -156
- package/test/unit/sync/store-configs.test.ts +0 -37
- package/test/unit/sync/telemetry/sync-telemetry.test.ts +0 -118
- package/test/unit/sync/utils/event-emitter.test.ts +0 -64
- package/test/unit/sync/utils/throttle.test.ts +0 -245
- package/test/unit/url-builder.test.ts +0 -72
- package/test/unit/user/account.test.ts +0 -66
- package/test/unit/user/permissions.test.js +0 -191
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { Database } from '@nozbe/watermelondb'
|
|
2
|
-
import { makeTelemetry, makeContext, makePullMock, makePushMock } from '../../../unit/sync/helpers/index'
|
|
3
|
-
import TestModel, { makeTestDatabase } from '../../../unit/sync/helpers/TestModel'
|
|
4
|
-
import SyncStore from '@/services/sync/store/index'
|
|
5
|
-
import SyncRetry from '@/services/sync/retry'
|
|
6
|
-
import SyncRunScope from '@/services/sync/run-scope'
|
|
7
|
-
import type { SyncUserScope } from '@/services/sync/index'
|
|
8
|
-
import { SyncError } from '@/services/sync/errors/index'
|
|
9
|
-
|
|
10
|
-
// ---
|
|
11
|
-
|
|
12
|
-
let db: Database
|
|
13
|
-
|
|
14
|
-
function makeStore(userScope: SyncUserScope, pull = makePullMock()) {
|
|
15
|
-
const context = makeContext()
|
|
16
|
-
const telemetry = makeTelemetry(userScope)
|
|
17
|
-
const retry = new SyncRetry(context, telemetry)
|
|
18
|
-
const runScope = new SyncRunScope()
|
|
19
|
-
|
|
20
|
-
return new SyncStore<TestModel>(
|
|
21
|
-
{ model: TestModel, pull, push: makePushMock() },
|
|
22
|
-
userScope,
|
|
23
|
-
context,
|
|
24
|
-
db,
|
|
25
|
-
retry,
|
|
26
|
-
runScope,
|
|
27
|
-
telemetry
|
|
28
|
-
)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function makeScope(overrides: Partial<SyncUserScope> = {}): SyncUserScope {
|
|
32
|
-
return {
|
|
33
|
-
initialId: 1,
|
|
34
|
-
getCurrentId: () => 1,
|
|
35
|
-
...overrides,
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function makePullResponse(overrides: Partial<{ intendedUserId: number }> = {}) {
|
|
40
|
-
return jest.fn().mockResolvedValue({
|
|
41
|
-
ok: true,
|
|
42
|
-
entries: [],
|
|
43
|
-
token: Date.now(),
|
|
44
|
-
previousToken: null,
|
|
45
|
-
intendedUserId: 1,
|
|
46
|
-
...overrides,
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
db = makeTestDatabase()
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
afterEach(async () => {
|
|
55
|
-
await db.write(async () => db.unsafeResetDatabase())
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
// ---
|
|
59
|
-
|
|
60
|
-
describe('write protection (paranoidWrite)', () => {
|
|
61
|
-
describe('same user', () => {
|
|
62
|
-
test('write proceeds when getCurrentId matches initialId', async () => {
|
|
63
|
-
const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => 1 }))
|
|
64
|
-
const result = await store.insertOne(r => { r.value = 'hello' })
|
|
65
|
-
store.destroy()
|
|
66
|
-
|
|
67
|
-
expect(result.value).toBe('hello')
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
describe('different user', () => {
|
|
72
|
-
test('throws SyncError when getCurrentId returns different id', async () => {
|
|
73
|
-
const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => 2 }))
|
|
74
|
-
|
|
75
|
-
await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(SyncError)
|
|
76
|
-
store.destroy()
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test('error message indicates cross-user write', async () => {
|
|
80
|
-
const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => 2 }))
|
|
81
|
-
|
|
82
|
-
await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(
|
|
83
|
-
'Aborted cross-user write operation'
|
|
84
|
-
)
|
|
85
|
-
store.destroy()
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
describe('null getCurrentId without fetchCurrentId', () => {
|
|
90
|
-
test('throws SyncError', async () => {
|
|
91
|
-
const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => null }))
|
|
92
|
-
|
|
93
|
-
await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(SyncError)
|
|
94
|
-
store.destroy()
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
test('error message indicates cross-user write', async () => {
|
|
98
|
-
const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => null }))
|
|
99
|
-
|
|
100
|
-
await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(
|
|
101
|
-
'Aborted cross-user write operation'
|
|
102
|
-
)
|
|
103
|
-
store.destroy()
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
describe('fetchCurrentId fallback', () => {
|
|
108
|
-
test('fetchCurrentId is called when getCurrentId returns null', async () => {
|
|
109
|
-
const fetchCurrentId = jest.fn().mockResolvedValue(1)
|
|
110
|
-
const store = makeStore(makeScope({ initialId: 1, getCurrentId: () => null, fetchCurrentId }))
|
|
111
|
-
|
|
112
|
-
await store.insertOne(r => { r.value = 'hello' })
|
|
113
|
-
store.destroy()
|
|
114
|
-
|
|
115
|
-
expect(fetchCurrentId).toHaveBeenCalledTimes(1)
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
test('write proceeds when fetchCurrentId resolves to same id', async () => {
|
|
119
|
-
const store = makeStore(makeScope({
|
|
120
|
-
initialId: 1,
|
|
121
|
-
getCurrentId: () => null,
|
|
122
|
-
fetchCurrentId: () => Promise.resolve(1),
|
|
123
|
-
}))
|
|
124
|
-
|
|
125
|
-
const result = await store.insertOne(r => { r.value = 'resolved' })
|
|
126
|
-
store.destroy()
|
|
127
|
-
|
|
128
|
-
expect(result.value).toBe('resolved')
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test('throws when fetchCurrentId resolves to different id', async () => {
|
|
132
|
-
const store = makeStore(makeScope({
|
|
133
|
-
initialId: 1,
|
|
134
|
-
getCurrentId: () => null,
|
|
135
|
-
fetchCurrentId: () => Promise.resolve(2),
|
|
136
|
-
}))
|
|
137
|
-
|
|
138
|
-
await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(
|
|
139
|
-
'Aborted cross-user write operation'
|
|
140
|
-
)
|
|
141
|
-
store.destroy()
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
test('throws SyncError when fetchCurrentId rejects', async () => {
|
|
145
|
-
const store = makeStore(makeScope({
|
|
146
|
-
initialId: 1,
|
|
147
|
-
getCurrentId: () => null,
|
|
148
|
-
fetchCurrentId: () => Promise.reject(new Error('network error')),
|
|
149
|
-
}))
|
|
150
|
-
|
|
151
|
-
await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toBeInstanceOf(SyncError)
|
|
152
|
-
store.destroy()
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
test('error message indicates fetchCurrentId failed', async () => {
|
|
156
|
-
const store = makeStore(makeScope({
|
|
157
|
-
initialId: 1,
|
|
158
|
-
getCurrentId: () => null,
|
|
159
|
-
fetchCurrentId: () => Promise.reject(new Error('network error')),
|
|
160
|
-
}))
|
|
161
|
-
|
|
162
|
-
await expect(store.insertOne(r => { r.value = 'hello' })).rejects.toThrow(
|
|
163
|
-
'Aborted cross-user write operation after fetchCurrentId failed'
|
|
164
|
-
)
|
|
165
|
-
store.destroy()
|
|
166
|
-
})
|
|
167
|
-
})
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
// ---
|
|
171
|
-
|
|
172
|
-
describe('pull protection (intendedUserId)', () => {
|
|
173
|
-
test('pull succeeds when intendedUserId matches initialId', async () => {
|
|
174
|
-
const store = makeStore(makeScope({ initialId: 1 }), makePullResponse({ intendedUserId: 1 }))
|
|
175
|
-
|
|
176
|
-
await expect(store.pull('test')).resolves.not.toThrow()
|
|
177
|
-
store.destroy()
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
test('throws SyncError when intendedUserId does not match initialId', async () => {
|
|
181
|
-
const store = makeStore(makeScope({ initialId: 1 }), makePullResponse({ intendedUserId: 2 }))
|
|
182
|
-
|
|
183
|
-
await expect(store.pull('test')).rejects.toThrow(SyncError)
|
|
184
|
-
store.destroy()
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
test('throws when intendedUserId does not match currentId', async () => {
|
|
188
|
-
const store = makeStore(
|
|
189
|
-
makeScope({ initialId: 1, getCurrentId: () => 2 }),
|
|
190
|
-
makePullResponse({ intendedUserId: 1 })
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
await expect(store.pull('test')).rejects.toThrow(SyncError)
|
|
194
|
-
store.destroy()
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
test('error message indicates intended user mismatch', async () => {
|
|
198
|
-
const store = makeStore(makeScope({ initialId: 1 }), makePullResponse({ intendedUserId: 2 }))
|
|
199
|
-
|
|
200
|
-
await expect(store.pull('test')).rejects.toThrow('Intended user ID does not match')
|
|
201
|
-
store.destroy()
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
test('throws SyncError when getCurrentId is null and fetchCurrentId rejects during pull', async () => {
|
|
205
|
-
const store = makeStore(
|
|
206
|
-
makeScope({
|
|
207
|
-
initialId: 1,
|
|
208
|
-
getCurrentId: () => null,
|
|
209
|
-
fetchCurrentId: () => Promise.reject(new Error('auth error')),
|
|
210
|
-
}),
|
|
211
|
-
makePullResponse({ intendedUserId: 1 })
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
await expect(store.pull('test')).rejects.toThrow(SyncError)
|
|
215
|
-
store.destroy()
|
|
216
|
-
})
|
|
217
|
-
})
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @jest-environment jsdom
|
|
3
|
-
*
|
|
4
|
-
* Tests SyncStore with the IncrementalIndexedDB-backed LokiJS adapter.
|
|
5
|
-
*
|
|
6
|
-
* Scope: IDB-specific behavior only (fetch token persistence, initialization,
|
|
7
|
-
* and bootstrapping — data loading from IDB into a new Database instance).
|
|
8
|
-
* General CRUD/event/coalescing behavior is covered in store.test.ts, which uses
|
|
9
|
-
* the plain LokiJS in-memory adapter.
|
|
10
|
-
*
|
|
11
|
-
* Concurrency note: loki.saveDatabase() with fakeIndexedDB's IncrementalIDBAdapter
|
|
12
|
-
* produces conflicting IDB transactions when multiple writes happen in rapid sequence
|
|
13
|
-
* (IncrementalIDBAdapter auto-save overlaps with ensurePersistence()'s saveDatabase()).
|
|
14
|
-
* Bootstrapping tests use a single write per "session" to stay within the safe window.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { IDBFactory } from 'fake-indexeddb'
|
|
18
|
-
import { makeTelemetry, makeContext, makeUserScope, makePullMock, makePushMock, TEST_USER_ID } from '../../../unit/sync/helpers/index'
|
|
19
|
-
import TestModel, { makeTestDatabase } from '../../../unit/sync/helpers/TestModel'
|
|
20
|
-
import SyncStore from '@/services/sync/store/index'
|
|
21
|
-
import SyncRetry from '@/services/sync/retry'
|
|
22
|
-
import SyncRunScope from '@/services/sync/run-scope'
|
|
23
|
-
import type { SyncPullResponse } from '@/services/sync/fetch'
|
|
24
|
-
|
|
25
|
-
// Fresh IDB instance per test — prevents cross-test transaction leakage
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
;(global as any).indexedDB = new IDBFactory()
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
function makeStore(overrides: { pull?: any; push?: any; dbName?: string } = {}) {
|
|
31
|
-
const db = makeTestDatabase({ useIncrementalIndexedDB: true, dbName: overrides.dbName })
|
|
32
|
-
const userScope = makeUserScope()
|
|
33
|
-
const context = makeContext()
|
|
34
|
-
const telemetry = makeTelemetry(userScope)
|
|
35
|
-
const retry = new SyncRetry(context, telemetry)
|
|
36
|
-
const runScope = new SyncRunScope()
|
|
37
|
-
|
|
38
|
-
const store = new SyncStore<TestModel>(
|
|
39
|
-
{
|
|
40
|
-
model: TestModel,
|
|
41
|
-
pull: overrides.pull ?? makePullMock(),
|
|
42
|
-
push: overrides.push ?? makePushMock(),
|
|
43
|
-
},
|
|
44
|
-
userScope,
|
|
45
|
-
context,
|
|
46
|
-
db,
|
|
47
|
-
retry,
|
|
48
|
-
runScope,
|
|
49
|
-
telemetry
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
return { store, db }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ---
|
|
56
|
-
|
|
57
|
-
describe('fetch token persistence (IDB localStorage)', () => {
|
|
58
|
-
test('getLastFetchToken returns null before any pull', async () => {
|
|
59
|
-
const { store } = makeStore()
|
|
60
|
-
const token = await store.getLastFetchToken()
|
|
61
|
-
store.destroy()
|
|
62
|
-
|
|
63
|
-
expect(token).toBeNull()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test('getLastFetchToken is set after successful pull', async () => {
|
|
67
|
-
const expectedToken = 1700000000000
|
|
68
|
-
const pull = jest.fn().mockResolvedValue({
|
|
69
|
-
ok: true,
|
|
70
|
-
entries: [],
|
|
71
|
-
token: expectedToken,
|
|
72
|
-
intendedUserId: TEST_USER_ID,
|
|
73
|
-
previousToken: null,
|
|
74
|
-
} as Extract<SyncPullResponse, { ok: true }>)
|
|
75
|
-
|
|
76
|
-
const { store } = makeStore({ pull })
|
|
77
|
-
await store.pullRecords()
|
|
78
|
-
const token = await store.getLastFetchToken()
|
|
79
|
-
store.destroy()
|
|
80
|
-
|
|
81
|
-
expect(token).toBe(expectedToken)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test('pullCompleted event fires after successful pull', async () => {
|
|
85
|
-
const handler = jest.fn()
|
|
86
|
-
const { store } = makeStore()
|
|
87
|
-
store.on('pullCompleted', handler)
|
|
88
|
-
|
|
89
|
-
await store.pullRecords()
|
|
90
|
-
store.destroy()
|
|
91
|
-
|
|
92
|
-
expect(handler).toHaveBeenCalledTimes(1)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('failed pull does not set fetch token', async () => {
|
|
96
|
-
const pull = jest.fn().mockResolvedValue({ ok: false, failureType: 'fetch', isRetryable: false })
|
|
97
|
-
|
|
98
|
-
const { store } = makeStore({ pull })
|
|
99
|
-
await store.pullRecords()
|
|
100
|
-
const token = await store.getLastFetchToken()
|
|
101
|
-
store.destroy()
|
|
102
|
-
|
|
103
|
-
expect(token).toBeNull()
|
|
104
|
-
})
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
// ---
|
|
108
|
-
|
|
109
|
-
describe('IDB bootstrapping (prior session data loaded into new Database instance)', () => {
|
|
110
|
-
const BOOTSTRAP_DB = 'test-bootstrap'
|
|
111
|
-
|
|
112
|
-
test('records written locally are readable from a new Database instance', async () => {
|
|
113
|
-
const { store: session1 } = makeStore({ dbName: BOOTSTRAP_DB })
|
|
114
|
-
await session1.upsertOne('boot-rec-1', r => {
|
|
115
|
-
r.value = 'persisted-value'
|
|
116
|
-
r.score = 42
|
|
117
|
-
})
|
|
118
|
-
session1.destroy()
|
|
119
|
-
|
|
120
|
-
const { store: session2 } = makeStore({ dbName: BOOTSTRAP_DB })
|
|
121
|
-
const records = await session2.readAll()
|
|
122
|
-
session2.destroy()
|
|
123
|
-
|
|
124
|
-
expect(records).toHaveLength(1)
|
|
125
|
-
expect(records[0].id).toBe('boot-rec-1')
|
|
126
|
-
expect(records[0].value).toBe('persisted-value')
|
|
127
|
-
expect(records[0].score).toBe(42)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
test('fetch token written during pull is readable from a new Database instance', async () => {
|
|
131
|
-
const TOKEN = 1700000000000
|
|
132
|
-
const pull = jest.fn().mockResolvedValue({
|
|
133
|
-
ok: true,
|
|
134
|
-
entries: [],
|
|
135
|
-
token: TOKEN,
|
|
136
|
-
intendedUserId: TEST_USER_ID,
|
|
137
|
-
previousToken: null,
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
const { store: session1 } = makeStore({ pull, dbName: BOOTSTRAP_DB })
|
|
141
|
-
await session1.pullRecords()
|
|
142
|
-
// pull doesn't call ensurePersistence — flush explicitly so IDB has the token
|
|
143
|
-
await (session1 as any).ensurePersistence()
|
|
144
|
-
session1.destroy()
|
|
145
|
-
|
|
146
|
-
const { store: session2 } = makeStore({ dbName: BOOTSTRAP_DB })
|
|
147
|
-
const token = await session2.getLastFetchToken()
|
|
148
|
-
session2.destroy()
|
|
149
|
-
|
|
150
|
-
expect(token).toBe(TOKEN)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
// Two sequential upserts — if this flakes with IDB transaction conflicts,
|
|
154
|
-
// collapse them into a single upsertSome call instead.
|
|
155
|
-
test('multiple records survive a restart', async () => {
|
|
156
|
-
const { store: session1 } = makeStore({ dbName: BOOTSTRAP_DB })
|
|
157
|
-
await session1.upsertOne('boot-a', r => { r.value = 'alpha'; r.score = 1 })
|
|
158
|
-
await session1.upsertOne('boot-b', r => { r.value = 'beta'; r.score = 2 })
|
|
159
|
-
session1.destroy()
|
|
160
|
-
|
|
161
|
-
const { store: session2 } = makeStore({ dbName: BOOTSTRAP_DB })
|
|
162
|
-
const records = await session2.readAll()
|
|
163
|
-
session2.destroy()
|
|
164
|
-
|
|
165
|
-
expect(records).toHaveLength(2)
|
|
166
|
-
const byId = Object.fromEntries(records.map(r => [r.id, r]))
|
|
167
|
-
expect(byId['boot-a'].value).toBe('alpha')
|
|
168
|
-
expect(byId['boot-b'].value).toBe('beta')
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
test('new Database instance with no prior IDB data starts empty', async () => {
|
|
172
|
-
const { store } = makeStore({ dbName: BOOTSTRAP_DB })
|
|
173
|
-
const records = await store.readAll()
|
|
174
|
-
const token = await store.getLastFetchToken()
|
|
175
|
-
store.destroy()
|
|
176
|
-
|
|
177
|
-
expect(records).toHaveLength(0)
|
|
178
|
-
expect(token).toBeNull()
|
|
179
|
-
})
|
|
180
|
-
})
|