From 0cbef8eb6088e5a9f9ada05be9df312fd8edeb97 Mon Sep 17 00:00:00 2001 From: hesprs <190185753+hesprs@users.noreply.github.com> Date: Wed, 23 Sep 2026 21:50:41 +0800 Subject: [PATCH 1/2] fix(s3): skip batching when only one object to delete --- modules.json | 2 +- packages/s3/src/optimizer.ts | 5 ++-- packages/s3/test/fs-s3.test.ts | 43 ++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/modules.json b/modules.json index 862d3764..fc8342b4 100644 --- a/modules.json +++ b/modules.json @@ -12,7 +12,7 @@ { "id": "s3", "name": "S3", - "version": "0.1.8", + "version": "0.1.9", "description": "S3 and S3-compatible backend support.", "icon": "server", "main": "https://sync.consensia.cc/modules/s3.js", diff --git a/packages/s3/src/optimizer.ts b/packages/s3/src/optimizer.ts index 42595d95..3efd5884 100644 --- a/packages/s3/src/optimizer.ts +++ b/packages/s3/src/optimizer.ts @@ -1,4 +1,4 @@ -import type { OptimizerInput, OptimizerOutput } from '@hesprs/sync-engine-sdk'; +import type { DeleteAtom, OptimizerInput, OptimizerOutput } from '@hesprs/sync-engine-sdk'; import { digOriginal } from '@hesprs/sync-engine-sdk'; import S3Fs, { BATCH_DELETE_MAX_KEYS } from './s3/fs'; @@ -9,10 +9,9 @@ export default function s3BatchDeleteOptimizer({ const original = digOriginal(fs); if (!(original instanceof S3Fs)) return undefined; const s3Fs = original; - type DeleteAtom = Extract<(typeof atoms)[number], { type: 'delete' }>; const deleteAtoms = atoms.filter((a): a is DeleteAtom => a.type === 'delete'); + if (deleteAtoms.length <= 1) return atoms; const otherAtoms = atoms.filter((a) => a.type !== 'delete'); - if (deleteAtoms.length === 0) return atoms; const batchGroups: Array> = []; for (let i = 0; i < deleteAtoms.length; i += BATCH_DELETE_MAX_KEYS) batchGroups.push(deleteAtoms.slice(i, i + BATCH_DELETE_MAX_KEYS)); diff --git a/packages/s3/test/fs-s3.test.ts b/packages/s3/test/fs-s3.test.ts index b6ef042d..72115fde 100644 --- a/packages/s3/test/fs-s3.test.ts +++ b/packages/s3/test/fs-s3.test.ts @@ -278,6 +278,49 @@ test('batch delete rejects only atoms with S3 partial failures', async () => { ); }); +test('batchDelete retries each key individually when the batch request fails', async () => { + const s3 = createS3Fs(); + const methods: Array = []; + parsedResponse = { Error: { Code: 'InternalError', Message: 'boom' } }; + s3.setRequest((url, params) => { + methods.push(params.method ?? ''); + if (params.method === 'POST') + return response({ + status: 500, + text: 'InternalErrorboom', + }); + expect(params.method).toBe('DELETE'); + if (url.endsWith('/broken.md')) + return response({ + status: 500, + text: 'InternalErrorboom', + }); + return response({ status: 204 }); + }); + const result = await s3.fs.batchDelete(['ok.md', 'broken.md']); + expect(result['ok.md']).toBe(true); + expect(result['broken.md']).toBe('S3 InternalError: boom'); + expect(methods).toStrictEqual(['POST', 'DELETE', 'DELETE']); +}); + +test('optimizer skips batching when there is a single delete atom', () => { + const s3 = createS3Fs(); + parsedResponse = {}; + const atoms: Array = ['solo.md'].map((key) => ({ + execute: () => {}, + key, + reject: () => {}, + resolve: () => {}, + type: 'delete', + })); + const optimized = s3BatchDeleteOptimizer({ + atoms, + executeAtom: (atom) => Promise.resolve(atom.execute()), + fs: s3.fs, + } satisfies OptimizerInput); + expect(optimized).toBe(atoms); +}); + test('move copies encoded source before deleting old key', async () => { const s3 = createS3Fs(); s3.setRequest((url, params) => { From 5320ddd4bdf37325346bb15a82dc4ead5747d5a7 Mon Sep 17 00:00:00 2001 From: hesprs <190185753+hesprs@users.noreply.github.com> Date: Wed, 23 Sep 2026 21:52:46 +0800 Subject: [PATCH 2/2] remove redundant test --- packages/s3/test/fs-s3.test.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/s3/test/fs-s3.test.ts b/packages/s3/test/fs-s3.test.ts index 72115fde..3b11d0d7 100644 --- a/packages/s3/test/fs-s3.test.ts +++ b/packages/s3/test/fs-s3.test.ts @@ -303,24 +303,6 @@ test('batchDelete retries each key individually when the batch request fails', a expect(methods).toStrictEqual(['POST', 'DELETE', 'DELETE']); }); -test('optimizer skips batching when there is a single delete atom', () => { - const s3 = createS3Fs(); - parsedResponse = {}; - const atoms: Array = ['solo.md'].map((key) => ({ - execute: () => {}, - key, - reject: () => {}, - resolve: () => {}, - type: 'delete', - })); - const optimized = s3BatchDeleteOptimizer({ - atoms, - executeAtom: (atom) => Promise.resolve(atom.execute()), - fs: s3.fs, - } satisfies OptimizerInput); - expect(optimized).toBe(atoms); -}); - test('move copies encoded source before deleting old key', async () => { const s3 = createS3Fs(); s3.setRequest((url, params) => {