Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bundle size challenge] Test Web Locks API #2880

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 54 additions & 48 deletions packages/core/src/domain/session/sessionStoreOperations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,45 +50,49 @@ const EXPIRED_SESSION: SessionState = { isExpired: '1' }
sessionStoreStrategy.isLockEnabled && pending('lock-access required')
})

it('should persist session when process returns a value', () => {
it('should persist session when process returns a value', async () => {
sessionStoreStrategy.persistSession(initialSession)
processSpy.and.returnValue({ ...otherSession })

processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)
await processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)

expect(processSpy).toHaveBeenCalledWith(initialSession)
const expectedSession = { ...otherSession, expire: jasmine.any(String) }
expect(sessionStoreStrategy.retrieveSession()).toEqual(expectedSession)
expect(afterSpy).toHaveBeenCalledWith(expectedSession)
})

it('should clear session when process returns an expired session', () => {
it('should clear session when process returns an expired session', async () => {
sessionStoreStrategy.persistSession(initialSession)
processSpy.and.returnValue(EXPIRED_SESSION)

processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)
await processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)

expect(processSpy).toHaveBeenCalledWith(initialSession)
expect(sessionStoreStrategy.retrieveSession()).toEqual(EXPIRED_SESSION)
expect(afterSpy).toHaveBeenCalledWith(EXPIRED_SESSION)
})

it('should not persist session when process returns undefined', () => {
it('should not persist session when process returns undefined', async () => {
sessionStoreStrategy.persistSession(initialSession)
processSpy.and.returnValue(undefined)

processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)
await processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)

expect(processSpy).toHaveBeenCalledWith(initialSession)
expect(sessionStoreStrategy.retrieveSession()).toEqual(initialSession)
expect(afterSpy).toHaveBeenCalledWith(initialSession)
})

it('LOCK_MAX_TRIES value should not influence the behavior when lock mechanism is not enabled', () => {
it('LOCK_MAX_TRIES value should not influence the behavior when lock mechanism is not enabled', async () => {
sessionStoreStrategy.persistSession(initialSession)
processSpy.and.returnValue({ ...otherSession })

processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy, LOCK_MAX_TRIES)
await processSessionStoreOperations(
{ process: processSpy, after: afterSpy },
sessionStoreStrategy,
LOCK_MAX_TRIES
)

expect(processSpy).toHaveBeenCalledWith(initialSession)
const expectedSession = { ...otherSession, expire: jasmine.any(String) }
Expand All @@ -102,35 +106,35 @@ const EXPIRED_SESSION: SessionState = { isExpired: '1' }
!sessionStoreStrategy.isLockEnabled && pending('lock-access not enabled')
})

it('should persist session when process returns a value', () => {
it('should persist session when process returns a value', async () => {
sessionStoreStrategy.persistSession(initialSession)
processSpy.and.returnValue({ ...otherSession })

processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)
await processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)

expect(processSpy).toHaveBeenCalledWith(initialSession)
const expectedSession = { ...otherSession, expire: jasmine.any(String) }
expect(sessionStoreStrategy.retrieveSession()).toEqual(expectedSession)
expect(afterSpy).toHaveBeenCalledWith(expectedSession)
})

it('should clear session when process returns an expired session', () => {
it('should clear session when process returns an expired session', async () => {
sessionStoreStrategy.persistSession(initialSession)
processSpy.and.returnValue(EXPIRED_SESSION)

processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)
await processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)

expect(processSpy).toHaveBeenCalledWith(initialSession)

expect(sessionStoreStrategy.retrieveSession()).toEqual(EXPIRED_SESSION)
expect(afterSpy).toHaveBeenCalledWith(EXPIRED_SESSION)
})

it('should not persist session when process returns undefined', () => {
it('should not persist session when process returns undefined', async () => {
sessionStoreStrategy.persistSession(initialSession)
processSpy.and.returnValue(undefined)

processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)
await processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)

expect(processSpy).toHaveBeenCalledWith(initialSession)
expect(sessionStoreStrategy.retrieveSession()).toEqual(initialSession)
Expand Down Expand Up @@ -184,7 +188,7 @@ const EXPIRED_SESSION: SessionState = { isExpired: '1' }
lockConflict: 'onPostPersistLockCheck',
},
].forEach(({ description, lockConflict }) => {
it(description, (done) => {
it(description, async () => {
lockScenario({
[lockConflict]: () => ({
currentState: { ...initialSession, lock: 'locked' },
Expand All @@ -195,7 +199,7 @@ const EXPIRED_SESSION: SessionState = { isExpired: '1' }
sessionStoreStrategy.persistSession(initialSession)
processSpy.and.callFake((session) => ({ ...session, processed: 'processed' }) as SessionState)

processSessionStoreOperations(
await processSessionStoreOperations(
{
process: processSpy,
after: (afterSession) => {
Expand All @@ -215,35 +219,36 @@ const EXPIRED_SESSION: SessionState = { isExpired: '1' }
}
expect(sessionStoreStrategy.retrieveSession()).toEqual(expectedSession)
expect(afterSession).toEqual(expectedSession)
done()
},
},
sessionStoreStrategy
)
})
})

it('should abort after a max number of retry', () => {
const clock = mockClock()
it('should abort after a max number of retry', async () => {
await navigator.locks.request('session_store', { ifAvailable: true }, async (currentLock) => {
const clock = mockClock()

sessionStoreStrategy.persistSession(initialSession)
storage.setSpy.calls.reset()
sessionStoreStrategy.persistSession(initialSession)
storage.setSpy.calls.reset()

storage.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' }))
processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)
storage.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' }))
await processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy)

const lockMaxTries = sessionStoreStrategy.isLockEnabled ? LOCK_MAX_TRIES : 0
const lockRetryDelay = sessionStoreStrategy.isLockEnabled ? LOCK_RETRY_DELAY : 0
const lockMaxTries = sessionStoreStrategy.isLockEnabled ? LOCK_MAX_TRIES : 0
const lockRetryDelay = sessionStoreStrategy.isLockEnabled ? LOCK_RETRY_DELAY : 0

clock.tick(lockMaxTries * lockRetryDelay)
expect(processSpy).not.toHaveBeenCalled()
expect(afterSpy).not.toHaveBeenCalled()
expect(storage.setSpy).not.toHaveBeenCalled()
clock.tick(lockMaxTries * lockRetryDelay)
expect(processSpy).not.toHaveBeenCalled()
expect(afterSpy).not.toHaveBeenCalled()
expect(storage.setSpy).not.toHaveBeenCalled()

clock.cleanup()
clock.cleanup()
})
})

it('should execute cookie accesses in order', (done) => {
it('should execute cookie accesses in order', async () => {
lockScenario({
onInitialLockCheck: () => ({
currentState: { ...initialSession, lock: 'locked' }, // force to retry the first access later
Expand All @@ -252,24 +257,25 @@ const EXPIRED_SESSION: SessionState = { isExpired: '1' }
})
sessionStoreStrategy.persistSession(initialSession)

processSessionStoreOperations(
{
process: (session) => ({ ...session, value: 'foo' }),
after: afterSpy,
},
sessionStoreStrategy
)
processSessionStoreOperations(
{
process: (session) => ({ ...session, value: `${session.value || ''}bar` }),
after: (session) => {
expect(session.value).toBe('foobar')
expect(afterSpy).toHaveBeenCalled()
done()
await Promise.all([
processSessionStoreOperations(
{
process: (session) => ({ ...session, value: 'foo' }),
after: afterSpy,
},
},
sessionStoreStrategy
)
sessionStoreStrategy
),
processSessionStoreOperations(
{
process: (session) => ({ ...session, value: `${session.value || ''}bar` }),
after: (session) => {
expect(session.value).toBe('foobar')
expect(afterSpy).toHaveBeenCalled()
},
},
sessionStoreStrategy
),
])
})
})
})
Expand Down
126 changes: 46 additions & 80 deletions packages/core/src/domain/session/sessionStoreOperations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { setTimeout } from '../../tools/timer'
import { generateUUID } from '../../tools/utils/stringUtils'
import { assign } from '../../tools/utils/polyfills'
import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy'
import type { SessionState } from './sessionState'
import { expandSessionState, isSessionInExpiredState } from './sessionState'
Expand All @@ -15,103 +12,72 @@ export const LOCK_MAX_TRIES = 100
const bufferedOperations: Operations[] = []
let ongoingOperations: Operations | undefined

export function processSessionStoreOperations(
export async function processSessionStoreOperations(
operations: Operations,
sessionStoreStrategy: SessionStoreStrategy,
numberOfRetries = 0
) {
const { isLockEnabled, persistSession, expireSession } = sessionStoreStrategy
const persistWithLock = (session: SessionState) => persistSession(assign({}, session, { lock: currentLock }))
const retrieveStore = () => {
const session = sessionStoreStrategy.retrieveSession()
const lock = session.lock
await navigator.locks.request('session_store', { ifAvailable: true }, async (currentLock) => {
const { isLockEnabled, persistSession, expireSession } = sessionStoreStrategy

if (session.lock) {
delete session.lock
if (!ongoingOperations) {
ongoingOperations = operations
}

return {
session,
lock,
}
}

if (!ongoingOperations) {
ongoingOperations = operations
}
if (operations !== ongoingOperations) {
bufferedOperations.push(operations)
return
}
if (isLockEnabled && numberOfRetries >= LOCK_MAX_TRIES) {
next(sessionStoreStrategy)
return
}
let currentLock: string
let currentStore = retrieveStore()
if (isLockEnabled) {
// if someone has lock, retry later
if (currentStore.lock) {
retryLater(operations, sessionStoreStrategy, numberOfRetries)
if (operations !== ongoingOperations) {
bufferedOperations.push(operations)
return
}
// acquire lock
currentLock = generateUUID()
persistWithLock(currentStore.session)
// if lock is not acquired, retry later
currentStore = retrieveStore()
if (currentStore.lock !== currentLock) {
retryLater(operations, sessionStoreStrategy, numberOfRetries)

if (isLockEnabled && numberOfRetries >= LOCK_MAX_TRIES) {
await next(sessionStoreStrategy)
return
}
}
let processedSession = operations.process(currentStore.session)
if (isLockEnabled) {
// if lock corrupted after process, retry later
currentStore = retrieveStore()
if (currentStore.lock !== currentLock!) {
retryLater(operations, sessionStoreStrategy, numberOfRetries)
return

let session = sessionStoreStrategy.retrieveSession()
if (isLockEnabled) {
// if someone has lock, retry later
if (!currentLock) {
await retryLater(operations, sessionStoreStrategy, numberOfRetries)
return
}
}
}
if (processedSession) {
if (isSessionInExpiredState(processedSession)) {
expireSession()
} else {
expandSessionState(processedSession)
isLockEnabled ? persistWithLock(processedSession) : persistSession(processedSession)
let processedSession = operations.process(session)

if (processedSession) {
if (isSessionInExpiredState(processedSession)) {
expireSession()
} else {
expandSessionState(processedSession)
persistSession(processedSession)
}
}
}
if (isLockEnabled) {
// correctly handle lock around expiration would require to handle this case properly at several levels
// since we don't have evidence of lock issues around expiration, let's just not do the corruption check for it
if (!(processedSession && isSessionInExpiredState(processedSession))) {
// if lock corrupted after persist, retry later
currentStore = retrieveStore()
if (currentStore.lock !== currentLock!) {
retryLater(operations, sessionStoreStrategy, numberOfRetries)
return
if (isLockEnabled) {
// correctly handle lock around expiration would require to handle this case properly at several levels
// since we don't have evidence of lock issues around expiration, let's just not do the corruption check for it
if (!(processedSession && isSessionInExpiredState(processedSession))) {
// if lock corrupted after persist, retry later
session = sessionStoreStrategy.retrieveSession()
persistSession(session)
processedSession = session
}
persistSession(currentStore.session)
processedSession = currentStore.session
}
}
// call after even if session is not persisted in order to perform operations on
// up-to-date session state value => the value could have been modified by another tab
operations.after?.(processedSession || currentStore.session)
next(sessionStoreStrategy)
// call after even if session is not persisted in order to perform operations on
// up-to-date session state value => the value could have been modified by another tab
operations.after?.(processedSession || session)
await next(sessionStoreStrategy)
})
}

function retryLater(operations: Operations, sessionStore: SessionStoreStrategy, currentNumberOfRetries: number) {
setTimeout(() => {
processSessionStoreOperations(operations, sessionStore, currentNumberOfRetries + 1)
}, LOCK_RETRY_DELAY)
async function retryLater(operations: Operations, sessionStore: SessionStoreStrategy, currentNumberOfRetries: number) {
// setTimeout(() => {
await processSessionStoreOperations(operations, sessionStore, currentNumberOfRetries + 1)
// }, LOCK_RETRY_DELAY)
}

function next(sessionStore: SessionStoreStrategy) {
async function next(sessionStore: SessionStoreStrategy) {
ongoingOperations = undefined
const nextOperations = bufferedOperations.shift()
if (nextOperations) {
processSessionStoreOperations(nextOperations, sessionStore)
await processSessionStoreOperations(nextOperations, sessionStore)
}
}