mirror of
https://github.com/tompro/sattle.git
synced 2026-08-26 23:05:58 +00:00
feat: converge trusted mints on storage events
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
// Cross-tab events are hints, never authority: the current localStorage
|
||||
// value is the only state a listener may project into a tab.
|
||||
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
||||
|
||||
import {linkingPubKeyHex} from './keys'
|
||||
import {onTrustedMintsChange, readTrustedMints, type TrustedMint} from './trustedMints'
|
||||
import {stubLocalStorage} from './test-utils'
|
||||
|
||||
const STORAGE_KEY = 'sattle_trusted_mints'
|
||||
const OWNER_ID = linkingPubKeyHex(new Uint8Array(32).fill(7))
|
||||
const MINT_KEY = '02' + 'aa'.repeat(32)
|
||||
|
||||
const mint = (server: string): TrustedMint => ({
|
||||
server,
|
||||
mintPubkey: MINT_KEY,
|
||||
addedAt: 1,
|
||||
locked: false,
|
||||
})
|
||||
|
||||
const store = (mints: TrustedMint[]): void => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({version: 1, ownerId: OWNER_ID, mints}))
|
||||
}
|
||||
|
||||
const storageEvent = (key: string | null, newValue: string | null): Event => {
|
||||
const event = new Event('storage')
|
||||
Object.defineProperties(event, {
|
||||
key: {value: key},
|
||||
newValue: {value: newValue},
|
||||
})
|
||||
return event
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
stubLocalStorage()
|
||||
})
|
||||
|
||||
describe('trusted-mint storage-event convergence', () => {
|
||||
it('projects a replacement from current storage instead of event newValue', () => {
|
||||
// Given a tab observing an existing trusted mint
|
||||
const events = new EventTarget()
|
||||
vi.stubGlobal('window', events)
|
||||
store([mint('before.example')])
|
||||
const observed: string[][] = []
|
||||
const unsubscribe = onTrustedMintsChange(() => {
|
||||
observed.push(readTrustedMints(OWNER_ID).map((entry) => entry.server))
|
||||
})
|
||||
|
||||
try {
|
||||
// When another tab replaces storage but a delayed event carries obsolete bytes
|
||||
store([mint('current.example')])
|
||||
events.dispatchEvent(
|
||||
storageEvent(STORAGE_KEY, JSON.stringify({mints: [mint('before.example')]})),
|
||||
)
|
||||
|
||||
// Then the projection follows the live registry
|
||||
expect(observed).toEqual([['current.example']])
|
||||
} finally {
|
||||
unsubscribe()
|
||||
}
|
||||
})
|
||||
|
||||
it('projects removal from current storage instead of a stale replacement event', () => {
|
||||
// Given a tab observing a trusted mint
|
||||
const events = new EventTarget()
|
||||
vi.stubGlobal('window', events)
|
||||
store([mint('removed.example')])
|
||||
const observed: TrustedMint[][] = []
|
||||
const unsubscribe = onTrustedMintsChange(() => {
|
||||
observed.push(readTrustedMints(OWNER_ID))
|
||||
})
|
||||
|
||||
try {
|
||||
// When the registry is removed while the event claims it still exists
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
events.dispatchEvent(
|
||||
storageEvent(STORAGE_KEY, JSON.stringify({mints: [mint('removed.example')]})),
|
||||
)
|
||||
|
||||
// Then the live removal wins
|
||||
expect(observed).toEqual([[]])
|
||||
} finally {
|
||||
unsubscribe()
|
||||
}
|
||||
})
|
||||
|
||||
it('projects a clear event by rereading current localStorage', () => {
|
||||
// Given a tab observing a trusted mint
|
||||
const events = new EventTarget()
|
||||
vi.stubGlobal('window', events)
|
||||
store([mint('cleared.example')])
|
||||
const observed: TrustedMint[][] = []
|
||||
const unsubscribe = onTrustedMintsChange(() => {
|
||||
observed.push(readTrustedMints(OWNER_ID))
|
||||
})
|
||||
|
||||
try {
|
||||
// When another tab clears storage, which emits key null
|
||||
localStorage.clear()
|
||||
events.dispatchEvent(storageEvent(null, JSON.stringify({mints: [mint('cleared.example')]})))
|
||||
|
||||
// Then the live empty registry wins
|
||||
expect(observed).toEqual([[]])
|
||||
} finally {
|
||||
unsubscribe()
|
||||
}
|
||||
})
|
||||
|
||||
it('ignores a delayed event after a newer live registry has replaced it', () => {
|
||||
// Given an observer and an obsolete first registry
|
||||
const events = new EventTarget()
|
||||
vi.stubGlobal('window', events)
|
||||
store([mint('obsolete.example')])
|
||||
const observed: string[][] = []
|
||||
const unsubscribe = onTrustedMintsChange(() => {
|
||||
observed.push(readTrustedMints(OWNER_ID).map((entry) => entry.server))
|
||||
})
|
||||
|
||||
try {
|
||||
// When a newer registry is already live before the obsolete event arrives
|
||||
store([mint('live.example')])
|
||||
events.dispatchEvent(
|
||||
storageEvent(STORAGE_KEY, JSON.stringify({mints: [mint('obsolete.example')]})),
|
||||
)
|
||||
|
||||
// Then the obsolete event cannot resurrect its value
|
||||
expect(observed).toEqual([['live.example']])
|
||||
} finally {
|
||||
unsubscribe()
|
||||
}
|
||||
})
|
||||
|
||||
it('clears the projection for malformed current storage without throwing', () => {
|
||||
// Given an observer with a previously valid registry
|
||||
const events = new EventTarget()
|
||||
vi.stubGlobal('window', events)
|
||||
store([mint('valid.example')])
|
||||
const observed: TrustedMint[][] = []
|
||||
const unsubscribe = onTrustedMintsChange(() => {
|
||||
observed.push(readTrustedMints(OWNER_ID))
|
||||
})
|
||||
|
||||
try {
|
||||
// When the current value is malformed and its event claims a valid old value
|
||||
localStorage.setItem(STORAGE_KEY, '{')
|
||||
events.dispatchEvent(
|
||||
storageEvent(STORAGE_KEY, JSON.stringify({mints: [mint('valid.example')]})),
|
||||
)
|
||||
|
||||
// Then parsing the live value safely removes it from the projection
|
||||
expect(observed).toEqual([[]])
|
||||
} finally {
|
||||
unsubscribe()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
// Cross-tab storage events are hints only. Subscribers reread the live
|
||||
// registry for their active owner rather than projecting event.newValue.
|
||||
|
||||
export const TRUSTED_MINTS_STORAGE_KEY = 'sattle_trusted_mints'
|
||||
|
||||
const listeners = new Set<() => void>()
|
||||
|
||||
export const notifyStoredTrustedMintsChange = (): void => {
|
||||
for (const listener of listeners) listener()
|
||||
}
|
||||
|
||||
// Each subscription binds its own storage listener and removes exactly it on
|
||||
// unsubscribe: no shared reference counting, so an abandoned subscriber can
|
||||
// never keep another subscriber's window listener (or runtime) alive.
|
||||
export const onStoredTrustedMintsChange = (listener: () => void): (() => void) => {
|
||||
listeners.add(listener)
|
||||
const target = typeof window === 'undefined' ? null : window
|
||||
const onStorage = (event: StorageEvent): void => {
|
||||
if (event.key !== TRUSTED_MINTS_STORAGE_KEY && event.key !== null) return
|
||||
listener()
|
||||
}
|
||||
target?.addEventListener('storage', onStorage)
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
target?.removeEventListener('storage', onStorage)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user