fix(storage): isolate workspace refresh state
- Discard stale provider refreshes and queued previews - Guard malformed deflate data and listener failures - Start periodic refresh without an immediate callback
This commit is contained in:
parent
c2bcb21fcf
commit
b21509cc8d
|
|
@ -102,8 +102,14 @@ async function readEntryPayload(
|
|||
: null
|
||||
}
|
||||
if (entry.method !== 8) return null
|
||||
const output = inflateSync(compressed, { out: new Uint8Array(maxOutput + 1) })
|
||||
return output.byteLength === entry.outputSize && hasPNGSignature(output) ? output : null
|
||||
const output = (() => {
|
||||
try {
|
||||
return inflateSync(compressed, { out: new Uint8Array(maxOutput + 1) })
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})()
|
||||
return output?.byteLength === entry.outputSize && hasPNGSignature(output) ? output : null
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export function createDocumentPreviews<Item extends DocumentWorkspaceItem>(
|
|||
while (active.size < concurrency) {
|
||||
const id = previewQueue.shift()
|
||||
if (!id) break
|
||||
queued.delete(id)
|
||||
if (!queued.delete(id)) continue
|
||||
if (active.has(id) || previewUrls.value[id]) continue
|
||||
active.add(id)
|
||||
void runLoad(id, previewGenerations.get(id) ?? 0)
|
||||
|
|
@ -108,7 +108,11 @@ export function createDocumentPreviews<Item extends DocumentWorkspaceItem>(
|
|||
for (const id of trackedIds) {
|
||||
if (previous.get(id) === current.get(id)) continue
|
||||
removeURL(id)
|
||||
if (!current.has(id) || active.has(id) || queued.has(id)) continue
|
||||
if (!current.has(id)) {
|
||||
queued.delete(id)
|
||||
continue
|
||||
}
|
||||
if (active.has(id) || queued.has(id)) continue
|
||||
queued.add(id)
|
||||
previewQueue.push(id)
|
||||
}
|
||||
|
|
@ -125,6 +129,7 @@ export function createDocumentPreviews<Item extends DocumentWorkspaceItem>(
|
|||
for (const id of ids) removeURL(id)
|
||||
previewQueue.length = 0
|
||||
queued.clear()
|
||||
previewErrors.value = {}
|
||||
}
|
||||
|
||||
function observePreview(element: Element | null, id: string): () => void {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export function useDocumentWorkspace<Item extends DocumentWorkspaceItem>(
|
|||
}
|
||||
},
|
||||
options.refreshInterval,
|
||||
{ immediate: false }
|
||||
{ immediate: true, immediateCallback: false }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,13 @@ type StorageWorkspaceListener = (event: StorageWorkspaceEvent) => void
|
|||
const listeners = new Set<StorageWorkspaceListener>()
|
||||
|
||||
export function emitStorageWorkspaceEvent(event: StorageWorkspaceEvent): void {
|
||||
for (const listener of listeners) listener(event)
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener(event)
|
||||
} catch (error) {
|
||||
console.error('[Storage] Workspace event listener failed:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function onStorageWorkspaceEvent(listener: StorageWorkspaceListener): () => void {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export function createStorageWorkspaceSource(
|
|||
updatedAt: metadata.updatedAt,
|
||||
metadataAuthoritative: true
|
||||
}))
|
||||
if (activeStorageProviderID.value !== providerID) return []
|
||||
onSnapshot({ documents, configured })
|
||||
return documents
|
||||
}
|
||||
|
|
@ -65,6 +66,7 @@ export function createStorageWorkspaceSource(
|
|||
lastSyncError: null
|
||||
})
|
||||
}
|
||||
if (activeStorageProviderID.value !== providerID) return []
|
||||
onSnapshot({ documents: reconciliation.documents, configured })
|
||||
return reconciliation.documents
|
||||
},
|
||||
|
|
|
|||
|
|
@ -41,6 +41,32 @@ describe('fig ranged thumbnail extraction', () => {
|
|||
expect(await extractFigThumbnailFromReader(memoryReader(bytes, []))).toBeNull()
|
||||
})
|
||||
|
||||
test('returns null when deflated thumbnail data is corrupt', async () => {
|
||||
const png = new Uint8Array([
|
||||
0x89,
|
||||
0x50,
|
||||
0x4e,
|
||||
0x47,
|
||||
0x0d,
|
||||
0x0a,
|
||||
0x1a,
|
||||
0x0a,
|
||||
...Array.from({ length: 64 }, () => 1)
|
||||
])
|
||||
const bytes = zipSync({ 'thumbnail.png': png })
|
||||
const name = new TextEncoder().encode('thumbnail.png')
|
||||
const nameOffset = bytes.findIndex((byte, index) =>
|
||||
name.every((nameByte, nameIndex) => bytes[index + nameIndex] === nameByte)
|
||||
)
|
||||
const headerOffset = nameOffset - 30
|
||||
const header = new DataView(bytes.buffer, bytes.byteOffset + headerOffset, 30)
|
||||
expect(header.getUint16(8, true)).toBe(8)
|
||||
const dataOffset = nameOffset + name.byteLength + header.getUint16(28, true)
|
||||
bytes.fill(0xff, dataOffset, dataOffset + header.getUint32(18, true))
|
||||
|
||||
expect(await extractFigThumbnailFromReader(memoryReader(bytes, []))).toBeNull()
|
||||
})
|
||||
|
||||
test('rejects thumbnails above configured output limits', async () => {
|
||||
const bytes = new Uint8Array(readFileSync('tests/fixtures/gold-preview.fig'))
|
||||
const thumbnail = await extractFigThumbnailFromReader(memoryReader(bytes, []), {
|
||||
|
|
|
|||
Loading…
Reference in a new issue