Skip to content

Commit 7a13d8f

Browse files
authored
Merge pull request #32 from digidem/feat/custom-write-error
feat: add custom error for map write errors
2 parents 79f271f + 6f0e12a commit 7a13d8f

4 files changed

Lines changed: 75 additions & 7 deletions

File tree

src/context.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ export class Context {
134134
await writer.write(chunk)
135135
} catch (err) {
136136
await fsPromises.unlink(tempPath).catch(noop)
137-
throw err
137+
throw new errors.MAP_WRITE_ERROR({
138+
message: err instanceof Error ? err.message : undefined,
139+
cause: err,
140+
})
138141
}
139142
},
140143
close: async () => {
@@ -143,7 +146,10 @@ export class Context {
143146
await writer.close()
144147
} catch (err) {
145148
await fsPromises.unlink(tempPath).catch(noop)
146-
throw err
149+
throw new errors.MAP_WRITE_ERROR({
150+
message: err instanceof Error ? err.message : undefined,
151+
cause: err,
152+
})
147153
}
148154

149155
// Validate the uploaded map file BEFORE replacing the existing one

src/lib/errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ const errorsList = [
9898
message: 'Invalid map file',
9999
status: 400,
100100
},
101+
{
102+
code: 'MAP_WRITE_ERROR',
103+
message: 'Failed to write map data',
104+
status: 500,
105+
},
101106

102107
// Generic errors
103108
{

test/map-shares.test.ts

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
fetch as secretStreamFetch,
1313
Agent as SecretStreamAgent,
1414
} from 'secret-stream-http'
15-
import { describe, it, expect } from 'vitest'
15+
import { describe, it, expect, vi } from 'vitest'
1616

1717
import type { MapShareState } from '../src/types.js'
1818
import {
@@ -150,7 +150,9 @@ describe('Map Shares and Downloads', () => {
150150

151151
// Create a third device (another receiver)
152152
const receiver2KeyPair = SecretStreamAgent.keyPair(Buffer.alloc(32, 2))
153-
const receiver2DeviceId = Buffer.from(receiver2KeyPair.publicKey).toString('hex')
153+
const receiver2DeviceId = Buffer.from(
154+
receiver2KeyPair.publicKey,
155+
).toString('hex')
154156

155157
// Create share for first receiver
156158
const share1 = await createShare().json()
@@ -1104,7 +1106,9 @@ describe('Map Shares and Downloads', () => {
11041106

11051107
// Create a third device with different keys
11061108
const wrongKeyPair = SecretStreamAgent.keyPair()
1107-
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString('hex')
1109+
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString(
1110+
'hex',
1111+
)
11081112

11091113
// Create a share for a different device
11101114
const { shareId: shareId2 } = await sender
@@ -1134,7 +1138,9 @@ describe('Map Shares and Downloads', () => {
11341138

11351139
// Create a third device with different keys
11361140
const wrongKeyPair = SecretStreamAgent.keyPair()
1137-
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString('hex')
1141+
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString(
1142+
'hex',
1143+
)
11381144

11391145
// Create a share for wrongDeviceId
11401146
const { shareId } = await sender
@@ -1187,7 +1193,9 @@ describe('Map Shares and Downloads', () => {
11871193

11881194
// Create a third device
11891195
const wrongKeyPair = SecretStreamAgent.keyPair()
1190-
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString('hex')
1196+
const wrongDeviceId = Buffer.from(wrongKeyPair.publicKey).toString(
1197+
'hex',
1198+
)
11911199

11921200
// Create a share for wrongDeviceId
11931201
const { shareId } = await sender
@@ -1638,6 +1646,51 @@ describe('Map Shares and Downloads', () => {
16381646
expect(tempFiles).toHaveLength(0)
16391647
})
16401648

1649+
it('should report MAP_WRITE_ERROR when write fails during download', async (t) => {
1650+
const { createShare, createDownload, receiver } = await startServers(t)
1651+
const receiverDir = path.dirname(receiver.customMapPath)
1652+
const receiverBasename = path.basename(receiver.customMapPath)
1653+
1654+
// Mock fs.createWriteStream on the receiver to simulate disk-full
1655+
const originalCreateWriteStream = fs.createWriteStream
1656+
const spy = vi
1657+
.spyOn(fs, 'createWriteStream')
1658+
.mockImplementation((...args: any[]) => {
1659+
const stream = originalCreateWriteStream.apply(fs, args as any)
1660+
stream._write = (
1661+
_chunk: any,
1662+
_encoding: BufferEncoding,
1663+
callback: (error?: Error | null) => void,
1664+
) => {
1665+
callback(new Error('ENOSPC: no space left on device'))
1666+
}
1667+
return stream
1668+
})
1669+
t.onTestFinished(() => spy.mockRestore())
1670+
1671+
const share = await createShare().json()
1672+
const download = await createDownload(share).json<any>()
1673+
1674+
// Wait for download to fail
1675+
await eventsUntil(receiver, download.downloadId, 'error')
1676+
1677+
// Check download state has MAP_WRITE_ERROR
1678+
const downloadStatus = await receiver
1679+
.get(`downloads/${download.downloadId}`)
1680+
.json<any>()
1681+
expect(downloadStatus.status).toBe('error')
1682+
expect(downloadStatus).toHaveProperty('error.code', 'MAP_WRITE_ERROR')
1683+
1684+
await delay(100)
1685+
1686+
// Verify temp files are cleaned up
1687+
const files = fs.readdirSync(receiverDir)
1688+
const tempFiles = files.filter(
1689+
(f) => f.startsWith(receiverBasename) && f.includes('.download-'),
1690+
)
1691+
expect(tempFiles).toHaveLength(0)
1692+
})
1693+
16411694
it('should try next URL when first mapShareUrl fails during download', async (t) => {
16421695
const { createShare, sender, receiver } = await startServers(t)
16431696

test/maps.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,8 @@ describe('Map Upload', () => {
603603
})
604604

605605
expect(response.status).toBe(500)
606+
const error = await response.json()
607+
expect(error).toHaveProperty('code', 'MAP_WRITE_ERROR')
606608

607609
// Allow time for async cleanup to complete
608610
await delay(100)
@@ -646,6 +648,8 @@ describe('Map Upload', () => {
646648
})
647649

648650
expect(response.status).toBe(500)
651+
const error = await response.json()
652+
expect(error).toHaveProperty('code', 'MAP_WRITE_ERROR')
649653

650654
// Allow time for async cleanup to complete
651655
await delay(100)

0 commit comments

Comments
 (0)