import { describe, expect, test } from 'bun:test'
import { gzipSync } from 'node:zlib'
import { MAX_BLOB_SIZE, MAX_SITE_SIZE, MAX_SITE_SIZE_SUPPORTER } from '@wispplace/constants'
import { MAX_REDIRECT_FILE_BYTES } from '@wispplace/fs-utils'
import type { Record as WispFsRecord } from '@wispplace/lexicons/types/place/wisp/fs'
import type { Record as WispSettings } from '@wispplace/lexicons/types/place/wisp/settings'
import { measureDecompressedSize, type StorageMetadata } from '@wispplace/tiered-storage'
import {
AsyncWorkGate,
assertLogicalFileSizeWithinLimit,
createRewrittenHtmlMetadata,
createSourceIdentityMetadata,
executeSiteDelete,
getLogicalFileSizeLimit,
getStoredUncompressedSize,
handleSettingsDelete,
handleSettingsUpdate,
handleSiteCreateOrUpdate,
isDevLocalPdsFetchEnabled,
listSiteRecordsForDid,
planFileChanges,
type RevalidationResources,
readPdsRecordJsonResponse,
reserveAndWriteWithinLogicalBudget,
resolveCacheWriterResourceConfig,
type SettingsWriteDependencies,
type SiteDeleteDependencies,
SiteLogicalQuotaExceededError,
SiteLogicalSizeBudget,
type SiteRecordListDependencies,
type SiteUpdateHandlerDependencies,
validateUncompressedSiteSize,
} from './cache-writer'
import { createRevalidationResourceContext, RevalidationDeadlineError } from './revalidate-resources'
function deferred<T = void>() {
let resolve!: (value: T | PromiseLike<T>) => void
const promise = new Promise<T>((resolvePromise) => {
resolve = resolvePromise
})
return { promise, resolve }
}
function serializedLock(): SiteUpdateHandlerDependencies['withSiteWriteLock'] &
SettingsWriteDependencies['withSiteWriteLock'] {
let tail: Promise<void> = Promise.resolve()
return async <T>(
_did: string,
_rkey: string,
operation: (signal: AbortSignal) => Promise<T>,
lockSignal?: AbortSignal,
): Promise<T> => {
const previous = tail
let release!: () => void
tail = new Promise<void>((resolve) => {
release = resolve
})
await previous
try {
return await operation(lockSignal ?? new AbortController().signal)
} finally {
release()
}
}
}
function siteRecord(site: string): WispFsRecord {
return {
$type: 'place.wisp.fs',
site,
root: { type: 'directory', entries: [] },
createdAt: '2024-01-01T00:00:00.000Z',
} as WispFsRecord
}
function settingsRecord(directoryListing: boolean): WispSettings {
return {
$type: 'place.wisp.settings',
directoryListing,
cleanUrls: true,
}
}
function storageMetadata(customMetadata: Record<string, string>): StorageMetadata {
return {
key: 'did:plc:test/site/assets/app.css',
size: 42,
createdAt: new Date(0),
lastAccessed: new Date(0),
accessCount: 0,
compressed: false,
checksum: '',
customMetadata,
}
}
describe('PDS getRecord absence responses', () => {
test('accepts the ATProto RecordNotFound XRPC error as authoritative absence', async () => {
const response = new Response(JSON.stringify({ error: 'RecordNotFound', message: 'not found' }), {
status: 400,
headers: { 'content-type': 'application/json' },
})
expect(await readPdsRecordJsonResponse(response)).toEqual({ kind: 'absent' })
})
test('keeps an unrelated HTTP 400 retryable', async () => {
const response = new Response(JSON.stringify({ error: 'InvalidRequest' }), {
status: 400,
headers: { 'content-type': 'application/json' },
})
await expect(readPdsRecordJsonResponse(response)).rejects.toMatchObject({ status: 400 })
})
test('accepts a gateway HTTP 404 as authoritative absence', async () => {
expect(await readPdsRecordJsonResponse(new Response(null, { status: 404 }))).toEqual({ kind: 'absent' })
})
test('returns a successful bounded JSON response', async () => {
const value = { value: { $type: 'place.wisp.fs' }, cid: 'bafytest' }
expect(await readPdsRecordJsonResponse(new Response(JSON.stringify(value)))).toEqual({
kind: 'present',
value,
})
})
})
describe('locked site/settings reconciliation', () => {
test('fetches site state after lock acquisition and never materializes a stale hint', async () => {
const firstFetchStarted = deferred<void>()
const allowFirstFetch = deferred<void>()
let authoritative = { record: siteRecord('C1'), cid: 'cid-C1' }
let fetches = 0
const materialized: Array<{ site: string; cid: string }> = []
const lock = serializedLock()
const dependencies: SiteUpdateHandlerDependencies = {
fetchAuthoritativeSiteRecord: async () => {
fetches++
if (fetches === 1) {
firstFetchStarted.resolve()
await allowFirstFetch.promise
}
return authoritative
},
materializeCurrentRecord: async (_did, _rkey, record, cid) => {
materialized.push({ site: record.site, cid })
},
withSiteWriteLock: lock,
}
const stale = handleSiteCreateOrUpdate('did:plc:test', 'site', siteRecord('C1'), 'cid-C1', undefined, dependencies)
await firstFetchStarted.promise
authoritative = { record: siteRecord('C2'), cid: 'cid-C2' }
const fresh = handleSiteCreateOrUpdate('did:plc:test', 'site', siteRecord('C2'), 'cid-C2', undefined, dependencies)
await Promise.resolve()
expect(materialized).toEqual([])
allowFirstFetch.resolve()
await Promise.all([stale, fresh])
expect(materialized).toEqual([
{ site: 'C2', cid: 'cid-C2' },
{ site: 'C2', cid: 'cid-C2' },
])
})
test('treats an absent site update as a no-op rather than a delete', async () => {
let materializations = 0
const dependencies: SiteUpdateHandlerDependencies = {
fetchAuthoritativeSiteRecord: async () => null,
materializeCurrentRecord: async () => {
materializations++
},
withSiteWriteLock: async (_did, _rkey, operation, _signal) =>
await operation(_signal ?? new AbortController().signal),
}
await handleSiteCreateOrUpdate('did:plc:test', 'site', siteRecord('hint'), 'cid-hint', undefined, dependencies)
expect(materializations).toBe(0)
})
test('re-reads authoritative settings after lock and never writes a stale update hint', async () => {
const firstWriteStarted = deferred<void>()
const allowFirstWrite = deferred<void>()
const writes: Array<{ directoryListing: boolean; cid: string }> = []
const lookupContexts: RevalidationResources[] = []
const dependencies: SettingsWriteDependencies = {
fetchSettingsRecordOutcome: async (_did, _rkey, _endpoint, resources) => {
if (!resources) throw new Error('expected a settings resource context')
lookupContexts.push(resources)
return {
kind: 'present',
record: settingsRecord(true),
cid: 'cid-C2',
}
},
upsertSiteSettingsCache: async (_did, _rkey, cid, settings) => {
writes.push({ directoryListing: settings.directoryListing, cid })
if (writes.length === 1) {
firstWriteStarted.resolve()
await allowFirstWrite.promise
}
},
deleteSiteSettingsCache: async () => undefined,
publishCacheInvalidation: async () => undefined,
withSiteWriteLock: serializedLock(),
}
const staleResources = createRevalidationResourceContext(1_000, 8)
const freshResources = createRevalidationResourceContext(1_000, 8)
// The event payload is stale C1, but the PDS already contains C2.
const stale = handleSettingsUpdate(
'did:plc:test',
'site',
settingsRecord(false),
'cid-C1',
{ resources: staleResources },
dependencies,
)
await firstWriteStarted.promise
const fresh = handleSettingsUpdate(
'did:plc:test',
'site',
settingsRecord(true),
'cid-C2',
{ resources: freshResources },
dependencies,
)
await Promise.resolve()
expect(writes).toEqual([{ directoryListing: true, cid: 'cid-C2' }])
allowFirstWrite.resolve()
await Promise.all([stale, fresh])
expect(writes).toEqual([
{ directoryListing: true, cid: 'cid-C2' },
{ directoryListing: true, cid: 'cid-C2' },
])
expect(lookupContexts).toEqual([staleResources, freshResources])
staleResources.close()
freshResources.close()
})
test('reconciles a stale delete to the current settings record instead of deleting fresh state', async () => {
const firstWriteStarted = deferred<void>()
const allowFirstWrite = deferred<void>()
const writes: Array<{ directoryListing: boolean; cid: string }> = []
let deletes = 0
const dependencies: SettingsWriteDependencies = {
fetchSettingsRecordOutcome: async () => ({
kind: 'present',
record: settingsRecord(true),
cid: 'cid-C2',
}),
upsertSiteSettingsCache: async (_did, _rkey, cid, settings) => {
writes.push({ directoryListing: settings.directoryListing, cid })
if (writes.length === 1) {
firstWriteStarted.resolve()
await allowFirstWrite.promise
}
},
deleteSiteSettingsCache: async () => {
deletes++
},
publishCacheInvalidation: async () => undefined,
withSiteWriteLock: serializedLock(),
}
const fresh = handleSettingsUpdate('did:plc:test', 'site', settingsRecord(true), 'cid-C2', undefined, dependencies)
await firstWriteStarted.promise
const staleDelete = handleSettingsDelete('did:plc:test', 'site', dependencies)
await Promise.resolve()
expect(deletes).toBe(0)
allowFirstWrite.resolve()
await Promise.all([fresh, staleDelete])
expect(writes).toEqual([
{ directoryListing: true, cid: 'cid-C2' },
{ directoryListing: true, cid: 'cid-C2' },
])
expect(deletes).toBe(0)
})
test('deletes settings only when the authoritative record is absent', async () => {
let writes = 0
let deletes = 0
const dependencies: SettingsWriteDependencies = {
fetchSettingsRecordOutcome: async () => ({ kind: 'absent' }),
upsertSiteSettingsCache: async () => {
writes++
},
deleteSiteSettingsCache: async () => {
deletes++
},
publishCacheInvalidation: async () => undefined,
withSiteWriteLock: async (_did, _rkey, operation, _signal) =>
await operation(_signal ?? new AbortController().signal),
}
await handleSettingsUpdate('did:plc:test', 'site', settingsRecord(false), 'cid-C1', undefined, dependencies)
expect(writes).toBe(0)
expect(deletes).toBe(1)
})
test('does not mutate settings cache when authoritative lookup is retryable or invalid', async () => {
let lookupCount = 0
let writes = 0
let deletes = 0
const dependencies: SettingsWriteDependencies = {
fetchSettingsRecordOutcome: async () => {
lookupCount++
return lookupCount === 1
? { kind: 'retryable' as const, error: 'FETCH_FAILED' as const }
: { kind: 'retryable' as const, error: 'INVALID_RECORD' as const }
},
upsertSiteSettingsCache: async () => {
writes++
},
deleteSiteSettingsCache: async () => {
deletes++
},
publishCacheInvalidation: async () => undefined,
withSiteWriteLock: async (_did, _rkey, operation, _signal) =>
await operation(_signal ?? new AbortController().signal),
}
await expect(
handleSettingsUpdate('did:plc:test', 'site', settingsRecord(false), 'cid-C1', undefined, dependencies),
).rejects.toMatchObject({ name: 'AuthoritativeSettingsRecordError', code: 'FETCH_FAILED' })
await expect(handleSettingsDelete('did:plc:test', 'site', dependencies)).rejects.toMatchObject({
name: 'AuthoritativeSettingsRecordError',
code: 'INVALID_RECORD',
})
expect(writes).toBe(0)
expect(deletes).toBe(0)
})
test('shares one transfer budget with the authoritative settings lookup', async () => {
const resources = createRevalidationResourceContext(1_000, 8)
let lookupResources: Parameters<NonNullable<SettingsWriteDependencies['fetchSettingsRecordOutcome']>>[3]
let writes = 0
const dependencies: SettingsWriteDependencies = {
fetchSettingsRecordOutcome: async (_did, _rkey, _endpoint, currentResources) => {
lookupResources = currentResources
currentResources?.transferBudget.consume(3)
currentResources?.transferBudget.consume(5)
return { kind: 'present', record: settingsRecord(true), cid: 'cid-C2' }
},
upsertSiteSettingsCache: async () => {
writes++
},
deleteSiteSettingsCache: async () => undefined,
publishCacheInvalidation: async () => undefined,
withSiteWriteLock: async (_did, _rkey, operation, _signal) =>
await operation(_signal ?? new AbortController().signal),
}
try {
await handleSettingsUpdate('did:plc:test', 'site', settingsRecord(false), 'cid-C1', { resources }, dependencies)
// The lock wrapper rebinds only the signal; the budget and deadline
// must remain the caller's shared instances.
expect(lookupResources?.transferBudget).toBe(resources.transferBudget)
expect(lookupResources?.deadlineAt).toBe(resources.deadlineAt)
expect(resources.transferBudget.consumedBytes).toBe(8)
expect(writes).toBe(1)
} finally {
resources.close()
}
})
test('expires before lock acquisition and performs no settings mutation', async () => {
const lockStarted = deferred<void>()
const releaseLock = deferred<void>()
const lock = serializedLock()
const lockHolder = lock('did:plc:test', 'site', async () => {
lockStarted.resolve()
await releaseLock.promise
})
await lockStarted.promise
const resources = createRevalidationResourceContext(10, 1024)
let lookups = 0
let writes = 0
const dependencies: SettingsWriteDependencies = {
fetchSettingsRecordOutcome: async () => {
lookups++
return { kind: 'present', record: settingsRecord(true), cid: 'cid-C2' }
},
upsertSiteSettingsCache: async () => {
writes++
},
deleteSiteSettingsCache: async () => undefined,
publishCacheInvalidation: async () => undefined,
withSiteWriteLock: lock,
}
const update = handleSettingsUpdate(
'did:plc:test',
'site',
settingsRecord(false),
'cid-C1',
{ resources },
dependencies,
)
try {
await new Promise((resolve) => setTimeout(resolve, 25))
expect(resources.signal.aborted).toBe(true)
releaseLock.resolve()
await expect(update).rejects.toBeInstanceOf(RevalidationDeadlineError)
expect(lookups).toBe(0)
expect(writes).toBe(0)
} finally {
releaseLock.resolve()
await lockHolder
resources.close()
}
})
test('checks resources after authoritative lookup before settings mutation', async () => {
const upstream = new AbortController()
const resources = createRevalidationResourceContext(1_000, 1024, upstream.signal)
let writes = 0
const dependencies: SettingsWriteDependencies = {
fetchSettingsRecordOutcome: async () => {
upstream.abort(new Error('firehose stopped'))
return { kind: 'present', record: settingsRecord(true), cid: 'cid-C2' }
},
upsertSiteSettingsCache: async () => {
writes++
},
deleteSiteSettingsCache: async () => undefined,
publishCacheInvalidation: async () => undefined,
withSiteWriteLock: async (_did, _rkey, operation, _signal) =>
await operation(_signal ?? new AbortController().signal),
}
try {
await expect(
handleSettingsUpdate('did:plc:test', 'site', settingsRecord(false), 'cid-C1', { resources }, dependencies),
).rejects.toThrow('firehose stopped')
expect(writes).toBe(0)
} finally {
resources.close()
}
})
})
describe('devnet PDS fetch admission', () => {
test('requires every explicit localhost gate', () => {
const enabled = {
NODE_ENV: 'development',
LOCAL_DEV: 'true',
WISP_ALLOW_LOCALHOST_FETCH: '1',
}
expect(isDevLocalPdsFetchEnabled(enabled)).toBe(true)
expect(isDevLocalPdsFetchEnabled({ ...enabled, NODE_ENV: 'production' })).toBe(false)
expect(isDevLocalPdsFetchEnabled({ ...enabled, LOCAL_DEV: 'false' })).toBe(false)
expect(isDevLocalPdsFetchEnabled({ ...enabled, WISP_ALLOW_LOCALHOST_FETCH: '0' })).toBe(false)
expect(isDevLocalPdsFetchEnabled({})).toBe(false)
})
})
describe('uncompressed site-size accounting', () => {
test('rejects many individually valid gzip expansions that exceed the site quota together', () => {
const files = Array.from({ length: 10 }, (_value, index) => ({ path: `assets/${index}.txt` }))
// These are the bounded decompressed sizes measured during ingestion. Their
// compressed manifest blobs can all be tiny and still must not bypass quota.
const expandedSize = Math.floor(MAX_SITE_SIZE / files.length) + 1
const sizes = new Map(files.map((file) => [file.path, expandedSize]))
expect(() => validateUncompressedSiteSize(files, sizes, MAX_SITE_SIZE)).toThrow(
'Site exceeds uncompressed size limit',
)
})
test('uses bounded gzip measurements rather than compressed blob lengths for aggregate quota', async () => {
const original = Buffer.alloc(64 * 1024, 'a')
const compressed = gzipSync(original)
expect(compressed.byteLength).toBeLessThan(original.byteLength)
const files = [{ path: 'one.css' }, { path: 'two.css' }, { path: 'three.css' }]
const sizes = new Map<string, number>()
for (const file of files) {
sizes.set(file.path, await measureDecompressedSize(compressed, original.byteLength))
}
expect(() => validateUncompressedSiteSize(files, sizes, original.byteLength * 2)).toThrow(
'Site exceeds uncompressed size limit',
)
})
test('accepts a logical total exactly at the site quota', () => {
const files = [{ path: 'a.txt' }, { path: 'b.txt' }]
const sizes = new Map([
['a.txt', MAX_SITE_SIZE / 2],
['b.txt', MAX_SITE_SIZE / 2],
])
expect(validateUncompressedSiteSize(files, sizes, MAX_SITE_SIZE)).toBe(MAX_SITE_SIZE)
})
})
describe('cached source identity metadata', () => {
const file = {
path: 'assets/app.css',
cid: 'bafyreinewsourcecid',
ownerDid: 'did:plc:source',
mimeType: 'text/css',
base64: false,
}
test('marks a same-path object with an old source CID for refresh', () => {
const metadata = storageMetadata({
sourceCid: 'bafyreioldsourcecid',
sourceDid: file.ownerDid,
mimeType: file.mimeType,
base64: 'false',
uncompressedSize: '42',
})
// The update loop treats null as a required re-download, even when the
// storage key/path and other metadata are unchanged.
expect(getStoredUncompressedSize(metadata, file)).toBeNull()
})
test('accepts matching source identity metadata for unchanged-file accounting', () => {
const metadata = storageMetadata({
sourceCid: file.cid,
sourceDid: file.ownerDid,
mimeType: file.mimeType,
base64: 'false',
uncompressedSize: '42',
})
expect(getStoredUncompressedSize(metadata, file)).toBe(42)
})
test('recognizes normalized identity metadata for gzip _redirects sources', () => {
const redirectsFile = { ...file, path: '_redirects', encoding: 'gzip' as const, mimeType: 'text/plain' }
const metadata = storageMetadata({
sourceCid: redirectsFile.cid,
sourceDid: redirectsFile.ownerDid,
mimeType: redirectsFile.mimeType,
base64: 'false',
uncompressedSize: '42',
})
expect(getStoredUncompressedSize(metadata, redirectsFile)).toBe(42)
})
test('puts the original source identity, not a PDS URL, on rewritten HTML', () => {
const metadata = createRewrittenHtmlMetadata(file)
expect(metadata).toEqual({
mimeType: 'text/html',
sourceCid: file.cid,
sourceDid: file.ownerDid,
})
expect(metadata).not.toHaveProperty('sourceUrl')
})
test('bounds source identity metadata values before writing', () => {
expect(() => createSourceIdentityMetadata({ ...file, cid: 'a'.repeat(513) })).toThrow('Invalid source CID')
expect(() => createSourceIdentityMetadata({ ...file, ownerDid: 'https://pds.example' })).toThrow(
'Invalid source DID',
)
})
})
const LISTED_SITE_DID = 'did:plc:listingtest'
const LISTED_SITE_RECORD = {
$type: 'place.wisp.fs',
site: 'site',
root: { type: 'directory', entries: [] },
createdAt: '2024-01-01T00:00:00.000Z',
}
function listedSiteRow(rkey: string, did = LISTED_SITE_DID): Record<string, unknown> {
return {
uri: `at://${did}/place.wisp.fs/${rkey}`,
cid: 'bafyreilistrecordcid',
value: LISTED_SITE_RECORD,
}
}
function siteRecordListDependencies(pages: unknown[]): SiteRecordListDependencies {
let nextPage = 0
return {
resolvePdsEndpoint: async () => 'https://pds.example',
fetchPage: async () => {
const page = pages[nextPage]
nextPage++
if (page === undefined) throw new Error('Unexpected extra record-list page request')
return page
},
}
}
describe('bounded site record listing', () => {
test('fails the full DID scan when a cursor repeats', async () => {
const dependencies = siteRecordListDependencies([
{ records: [listedSiteRow('first')], cursor: 'repeat-me' },
{ records: [listedSiteRow('second')], cursor: 'repeat-me' },
])
await expect(listSiteRecordsForDid(LISTED_SITE_DID, dependencies)).rejects.toMatchObject({
code: 'REPEATED_CURSOR',
})
})
test('rejects an oversized opaque cursor before a follow-up request', async () => {
const dependencies = siteRecordListDependencies([
{ records: [listedSiteRow('first')], cursor: 'x'.repeat(4 * 1024 + 1) },
])
await expect(listSiteRecordsForDid(LISTED_SITE_DID, dependencies)).rejects.toMatchObject({ code: 'CURSOR_LIMIT' })
})
test('rejects a row whose canonical AT-URI belongs to another DID or collection', async () => {
const wrongDid = siteRecordListDependencies([{ records: [listedSiteRow('site', 'did:plc:other')] }])
await expect(listSiteRecordsForDid(LISTED_SITE_DID, wrongDid)).rejects.toMatchObject({ code: 'INVALID_URI' })
const wrongCollection = siteRecordListDependencies([
{
records: [{ ...listedSiteRow('site'), uri: `at://${LISTED_SITE_DID}/place.wisp.subfs/site` }],
},
])
await expect(listSiteRecordsForDid(LISTED_SITE_DID, wrongCollection)).rejects.toMatchObject({ code: 'INVALID_URI' })
})
test('does not return a partial DID scan when a later PDS page fails', async () => {
let calls = 0
const dependencies: SiteRecordListDependencies = {
resolvePdsEndpoint: async () => 'https://pds.example',
fetchPage: async () => {
calls++
if (calls === 1) return { records: [listedSiteRow('first')], cursor: 'next' }
throw new Error('HTTP 503')
},
}
await expect(listSiteRecordsForDid(LISTED_SITE_DID, dependencies)).rejects.toThrow('HTTP 503')
})
test('rejects a page beyond the aggregate logical byte budget before parsing rows', async () => {
const hugeValue = {
...LISTED_SITE_RECORD,
padding: 'x'.repeat(10 * 1024 * 1024),
}
const dependencies = siteRecordListDependencies([
{
records: [{ ...listedSiteRow('site'), value: hugeValue }],
},
])
await expect(listSiteRecordsForDid(LISTED_SITE_DID, dependencies)).rejects.toMatchObject({
code: 'LOGICAL_SIZE_LIMIT',
})
})
})
describe('ingest resource and logical quota guards', () => {
test('plans changed and removed files before the accounting and download stages', () => {
const files = [
{ path: 'unchanged.css', cid: 'same' },
{ path: 'changed.js', cid: 'new' },
{ path: 'page.html', cid: 'same-page' },
]
const oldFileCids = {
'unchanged.css': 'same',
'changed.js': 'old',
'page.html': 'same-page',
'removed.txt': 'gone',
}
const standard = planFileChanges(files, oldFileCids, false, false)
expect([...standard.downloadPaths]).toEqual(['changed.js'])
expect(standard.pathsToDelete).toEqual(['removed.txt'])
const forcedRewrite = planFileChanges(files, oldFileCids, false, true)
expect([...forcedRewrite.downloadPaths]).toEqual(['changed.js', 'page.html'])
const duplicatePath = planFileChanges(
[
{ path: 'duplicate.txt', cid: 'old' },
{ path: 'duplicate.txt', cid: 'new' },
],
{ 'duplicate.txt': 'old' },
false,
false,
)
expect(duplicatePath.downloadFileCids.get('duplicate.txt')).toBe('new')
})
test('falls back to safe resource settings for invalid backoff and concurrency values', () => {
expect(
resolveCacheWriterResourceConfig({
BLOB_500_BACKOFF_MS: 'not-a-number',
FIREHOSE_DOWNLOAD_CONCURRENCY: '20',
}),
).toEqual({ blob500BackoffMs: 10 * 60 * 1000, downloadConcurrency: 3 })
expect(resolveCacheWriterResourceConfig({ BLOB_500_BACKOFF_MS: '1000' }).blob500BackoffMs).toBe(1000)
})
test('bounds concurrent blob work at three operations', async () => {
const gate = new AsyncWorkGate(3)
let active = 0
let maxActive = 0
await Promise.all(
Array.from({ length: 20 }, () =>
gate.run(async () => {
active++
maxActive = Math.max(maxActive, active)
await Promise.resolve()
active--
}),
),
)
expect(maxActive).toBe(3)
})
test('releases a heavy-work permit after an aborted operation', async () => {
const gate = new AsyncWorkGate(1)
let nextOperationRan = false
const aborted = gate.run(async () => {
await Promise.resolve()
throw new Error('aborted fetch')
})
const next = gate.run(async () => {
nextOperationRan = true
})
await expect(aborted).rejects.toThrow('aborted fetch')
await next
expect(nextOperationRan).toBe(true)
})
test('reserves unchanged logical sizes before accepting new writes', () => {
const budget = new SiteLogicalSizeBudget(100, new Map([['unchanged.css', 60]]))
expect(budget.reserve('new.css', 40)).toBe(true)
expect(budget.totalSize).toBe(100)
expect(() => budget.reserve('one-too-many.css', 1)).toThrow(SiteLogicalQuotaExceededError)
})
test('stops simulated writes before a lying compressed size can exceed the quota', async () => {
const budget = new SiteLogicalSizeBudget(100)
const writes: string[] = []
const writeAfterLogicalMeasurement = (path: string, logicalSize: number) =>
reserveAndWriteWithinLogicalBudget(budget, path, logicalSize, async () => {
writes.push(path)
})
await writeAfterLogicalMeasurement('first.css', 60)
await expect(writeAfterLogicalMeasurement('bomb.css', 50)).rejects.toBeInstanceOf(SiteLogicalQuotaExceededError)
expect(writes).toEqual(['first.css'])
expect(budget.totalSize).toBe(60)
})
test('does not double-charge a failed-write retry for the same immutable file', async () => {
const budget = new SiteLogicalSizeBudget(100)
let successfulWrites = 0
await expect(
reserveAndWriteWithinLogicalBudget(budget, 'retry.css', 75, async () => {
throw new Error('temporary storage failure')
}),
).rejects.toThrow('temporary storage failure')
await reserveAndWriteWithinLogicalBudget(budget, 'retry.css', 75, async () => {
successfulWrites++
})
expect(successfulWrites).toBe(1)
expect(budget.totalSize).toBe(75)
expect(budget.reservedFileCount).toBe(1)
})
test('enforces the supporter quota before the next file write', () => {
const budget = new SiteLogicalSizeBudget(MAX_SITE_SIZE_SUPPORTER)
for (let index = 0; index < 3; index++) {
budget.reserve(`large-${index}.bin`, MAX_BLOB_SIZE)
}
const remaining = MAX_SITE_SIZE_SUPPORTER - budget.totalSize
expect(() => budget.reserve('too-large.bin', remaining + 1)).toThrow(SiteLogicalQuotaExceededError)
expect(budget.totalSize).toBe(MAX_BLOB_SIZE * 3)
})
test('uses the shared redirect byte cap as an inclusive logical ingest boundary', () => {
expect(getLogicalFileSizeLimit('_redirects')).toBe(MAX_REDIRECT_FILE_BYTES)
expect(() => assertLogicalFileSizeWithinLimit('_redirects', MAX_REDIRECT_FILE_BYTES)).not.toThrow()
expect(() => assertLogicalFileSizeWithinLimit('_redirects', MAX_REDIRECT_FILE_BYTES + 1)).toThrow(
'_redirects exceeds',
)
expect(getLogicalFileSizeLimit('assets/app.js')).toBe(MAX_BLOB_SIZE)
})
})
interface SiteDeleteTestHarness {
dependencies: SiteDeleteDependencies
publications: Array<{ action: 'updating' | 'delete'; token?: string }>
}
function createSiteDeleteTestHarness(
events: string[],
options: { files?: string[]; deleteFileFailure?: Error; publisherFailure?: boolean } = {},
): SiteDeleteTestHarness {
const publications: Array<{ action: 'updating' | 'delete'; token?: string }> = []
const files = options.files ?? ['did:plc:test/site/index.html', 'did:plc:test/site/app.js']
return {
dependencies: {
listFiles: async (prefix) => {
events.push(`list:${prefix}`)
return files
},
deleteFile: async (key) => {
events.push(`delete-file:${key}`)
if (options.deleteFileFailure) throw options.deleteFileFailure
},
markSiteCacheDeleted: async (did, rkey) => {
events.push(`tombstone-db:${did}/${rkey}`)
},
publishCacheInvalidation: async (_did, _rkey, action, token) => {
if (action !== 'updating' && action !== 'delete') throw new Error(`Unexpected action ${action}`)
events.push(`publish:${action}`)
publications.push({ action, token })
if (options.publisherFailure) throw new Error('publisher unavailable')
},
},
publications,
}
}
describe('site delete invalidation ordering', () => {
test('marks updating before destructive work and completes with a matching delete token', async () => {
const events: string[] = []
const { dependencies, publications } = createSiteDeleteTestHarness(events)
await executeSiteDelete('did:plc:test', 'site', dependencies)
expect(events).toEqual([
'publish:updating',
'list:did:plc:test/site/',
'delete-file:did:plc:test/site/index.html',
'delete-file:did:plc:test/site/app.js',
'tombstone-db:did:plc:test/site',
'publish:delete',
])
expect(publications).toHaveLength(2)
expect(publications[0]).toMatchObject({ action: 'updating', token: expect.any(String) })
expect(publications[1]).toEqual({ action: 'delete', token: publications[0]?.token })
})
test('leaves the updating marker in place when destructive deletion fails', async () => {
const events: string[] = []
const { dependencies, publications } = createSiteDeleteTestHarness(events, {
files: ['did:plc:test/site/index.html'],
deleteFileFailure: new Error('storage unavailable'),
})
await expect(executeSiteDelete('did:plc:test', 'site', dependencies)).rejects.toThrow('storage unavailable')
expect(events).toEqual(['publish:updating', 'list:did:plc:test/site/', 'delete-file:did:plc:test/site/index.html'])
expect(publications.map((publication) => publication.action)).toEqual(['updating'])
})
test('does not let invalidation publisher failures prevent a complete delete', async () => {
const events: string[] = []
const { dependencies, publications } = createSiteDeleteTestHarness(events, {
files: [],
publisherFailure: true,
})
await expect(executeSiteDelete('did:plc:test', 'site', dependencies)).resolves.toBeUndefined()
expect(events).toEqual([
'publish:updating',
'list:did:plc:test/site/',
'tombstone-db:did:plc:test/site',
'publish:delete',
])
expect(publications).toHaveLength(2)
})
})