Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
8.3 kB · 220 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221// The cache manager's rules: the policy decides freshness, the caller can// override it, a failed load is not remembered, and dropping one subject// leaves every other subject alone.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'import { ACCOUNT_SCOPES, POLICIES, type Scope, cached, dropAll, invalidate, invalidateAccount, invalidateSubject, peek, put, storageKey,} from './cache'
let store = new Map<string, unknown>()let local = new Map<string, unknown>()
/** One storage area over its own Map, so a test can tell them apart. */function area(backing: Map<string, unknown>) { return { get: async (keys: string | null) => { if (keys === null) return Object.fromEntries(backing) return backing.has(keys) ? { [keys]: backing.get(keys) } : {} }, set: async (items: Record<string, unknown>) => { for (const [k, v] of Object.entries(items)) backing.set(k, v) }, remove: async (keys: string | string[]) => { for (const k of [keys].flat()) backing.delete(k) }, }}
beforeEach(() => { store = new Map() local = new Map() vi.stubGlobal('chrome', { storage: { session: area(store), local: area(local) } })})
afterEach(() => { vi.unstubAllGlobals() vi.useRealTimers()})
const key = (scope: Scope, subject: string, name: string) => ({ scope, subject, name })
describe('storageKey', () => { it('puts the subject in the middle, so one account can be dropped whole', () => { expect(storageKey(key('own', 'did:plc:me', 'subs'))).toBe('own:did:plc:me/subs') })})
describe('cached', () => { it('loads once and serves the stored value after that', async () => { const load = vi.fn(async () => 'first') expect(await cached(key('own', 'did:plc:me', 'subs'), load)).toBe('first') load.mockResolvedValue('second') expect(await cached(key('own', 'did:plc:me', 'subs'), load)).toBe('first') expect(load).toHaveBeenCalledTimes(1) })
it('reloads when the user asks again', async () => { const load = vi.fn(async () => 'first') await cached(key('own', 'did:plc:me', 'subs'), load) load.mockResolvedValue('second') expect(await cached(key('own', 'did:plc:me', 'subs'), load, { refresh: true })).toBe('second') })
it('expires on the scope’s own policy, not on one shared number', async () => { vi.useFakeTimers() const load = vi.fn(async () => 'value') await cached(key('own', 'did:plc:me', 'subs'), load) await cached(key('world', 'at://pub', 'labels'), load)
// Past the hour `own` keeps, well inside the day `world` does. vi.advanceTimersByTime(POLICIES.own.ttlMs + 1) await cached(key('own', 'did:plc:me', 'subs'), load) await cached(key('world', 'at://pub', 'labels'), load) expect(load).toHaveBeenCalledTimes(3) })
it('takes an explicit ttl from a caller with a reason to differ', async () => { vi.useFakeTimers() const load = vi.fn(async () => 'value') const k = key('world', 'at://pub', 'labels') await cached(k, load, { ttlMs: 5 * 60 * 1000 }) vi.advanceTimersByTime(5 * 60 * 1000 + 1) await cached(k, load, { ttlMs: 5 * 60 * 1000 }) expect(load).toHaveBeenCalledTimes(2) })
it('does not remember a load that threw', async () => { const load = vi.fn(async () => { throw new Error('offline') }) await expect(cached(key('world', 'at://pub', 'labels'), load)).rejects.toThrow('offline') expect(store.size).toBe(0) })})
describe('the graph scope', () => { it('writes to disk, where a browser restart cannot take it', async () => { await cached(key('graph', 'did:plc:me', 'follows'), async () => ['did:plc:you']) expect(local.get('graph:did:plc:me/follows')).toBeDefined() // Everything else stays in the area the browser clears on close. expect(store.size).toBe(0) })
it('keeps a follow set for a day, not for the hour a write-backed scope gets', async () => { vi.useFakeTimers() const load = vi.fn(async () => ['did:plc:you']) await cached(key('graph', 'did:plc:me', 'follows'), load) vi.advanceTimersByTime(POLICIES.own.ttlMs + 1) await cached(key('graph', 'did:plc:me', 'follows'), load) expect(load).toHaveBeenCalledTimes(1)
vi.advanceTimersByTime(POLICIES.graph.ttlMs) await cached(key('graph', 'did:plc:me', 'follows'), load) expect(load).toHaveBeenCalledTimes(2) })})
describe('invalidateAccount', () => { it('drops every scope keyed by the account, not just the one', async () => { await put(key('own', 'did:plc:me', 'subs'), 1) await put(key('graph', 'did:plc:me', 'follows'), ['did:plc:you']) await put(key('world', 'at://pub', 'labels'), 2)
await invalidateAccount('did:plc:me')
expect(await peek(key('own', 'did:plc:me', 'subs'))).toBeUndefined() // The one a sign-out would otherwise leave on disk for the next account. expect(await peek(key('graph', 'did:plc:me', 'follows'))).toBeUndefined() expect(await peek(key('world', 'at://pub', 'labels'))).toBe(2) })
it('covers every account scope the policies declare', () => { const keyed = (Object.keys(POLICIES) as Scope[]).filter((s) => s !== 'world') expect([...ACCOUNT_SCOPES].sort()).toEqual(keyed.sort()) })})
describe('peek', () => { it('answers from the store or not at all', async () => { expect(await peek(key('world', 'at://pub', 'labels'))).toBeUndefined() await put(key('world', 'at://pub', 'labels'), ['spam']) expect(await peek(key('world', 'at://pub', 'labels'))).toEqual(['spam']) })})
describe('invalidate', () => { it('drops the one entry a write invalidated', async () => { await put(key('own', 'did:plc:me', 'subs'), 1) await put(key('own', 'did:plc:me', 'follows'), 2) await invalidate(key('own', 'did:plc:me', 'subs')) expect(await peek(key('own', 'did:plc:me', 'subs'))).toBeUndefined() // Subscribing did not change who you follow. expect(await peek(key('own', 'did:plc:me', 'follows'))).toBe(2) })})
describe('invalidateSubject', () => { it('drops one account whole and leaves everyone else', async () => { await put(key('own', 'did:plc:me', 'subs'), 1) await put(key('own', 'did:plc:me', 'follows'), 2) await put(key('own', 'did:plc:other', 'subs'), 3) await put(key('world', 'at://pub', 'labels'), 4)
await invalidateSubject('own', 'did:plc:me')
expect(await peek(key('own', 'did:plc:me', 'subs'))).toBeUndefined() expect(await peek(key('own', 'did:plc:me', 'follows'))).toBeUndefined() expect(await peek(key('own', 'did:plc:other', 'subs'))).toBe(3) expect(await peek(key('world', 'at://pub', 'labels'))).toBe(4) })
it('does not confuse a subject with one that starts the same way', async () => { await put(key('own', 'did:plc:me', 'subs'), 1) await put(key('own', 'did:plc:meta', 'subs'), 2) await invalidateSubject('own', 'did:plc:me') expect(await peek(key('own', 'did:plc:meta', 'subs'))).toBe(2) })})
describe('dropAll', () => { it('empties every scope, in both of the areas they live in', async () => { await put(key('own', 'did:plc:me', 'subs'), 1) await put(key('graph', 'did:plc:me', 'follows'), ['did:plc:you']) await put(key('world', 'at://pub', 'labels'), 2)
expect(await dropAll()).toBe(3)
expect(await peek(key('own', 'did:plc:me', 'subs'))).toBeUndefined() // On disk, so a sweep that only walked storage.session would miss it. expect(await peek(key('graph', 'did:plc:me', 'follows'))).toBeUndefined() expect(await peek(key('world', 'at://pub', 'labels'))).toBeUndefined() })
it('leaves what is not a cache, which is what stops it being a sign-out', async () => { local.set('session', { did: 'did:plc:me', handle: 'me.example' }) local.set('defaultReader', 'standard-reader') store.set('tab:7', { url: 'https://pub.example' }) await put(key('own', 'did:plc:me', 'subs'), 1)
expect(await dropAll()).toBe(1)
expect(local.get('session')).toEqual({ did: 'did:plc:me', handle: 'me.example' }) expect(local.get('defaultReader')).toBe('standard-reader') expect(store.get('tab:7')).toEqual({ url: 'https://pub.example' }) })
it('counts nothing when there was nothing cached', async () => { expect(await dropAll()).toBe(0) })})