Merge main for current release and TypeScript compatibility

This commit is contained in:
vorotamoroz
2026-09-08 17:26:23 +00:00
27 changed files with 451 additions and 584 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "self-hosted-livesync-cli",
"private": true,
"version": "1.0.26-cli",
"version": "1.0.27-cli",
"main": "dist/index.cjs",
"type": "module",
"scripts": {
@@ -51,7 +51,7 @@
"werift": "^0.24.4"
},
"devDependencies": {
"typescript": "5.9.3",
"typescript": "6.0.3",
"vite": "^8.0.16",
"vitest": "^4.1.8"
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "livesync-webapp",
"private": true,
"version": "1.0.26-webapp",
"version": "1.0.27-webapp",
"type": "module",
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
"scripts": {
@@ -20,7 +20,7 @@
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"svelte": "5.56.3",
"typescript": "5.9.3",
"typescript": "6.0.3",
"vite": "^8.0.16"
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "webpeer",
"private": true,
"version": "1.0.26-webpeer",
"version": "1.0.27-webpeer",
"type": "module",
"scripts": {
"dev": "vite",
@@ -23,7 +23,7 @@
"@tsconfig/svelte": "^5.0.8",
"svelte": "5.56.3",
"svelte-check": "^4.6.0",
"typescript": "5.9.3",
"typescript": "6.0.3",
"vite": "^8.0.16"
}
}
@@ -1,7 +1,7 @@
// This file is based on a file that was published by the @remotely-save, under the Apache 2 License.
// I would love to express my deepest gratitude to the original authors for their hard work and dedication. Without their contributions, this project would not have been possible.
// This file was originally based on code published by @remotely-save under the Apache License 2.0.
// I would like to express my gratitude to the original authors for their work.
//
// Original Implementation is here: https://github.com/remotely-save/remotely-save/blob/28b99557a864ef59c19d2ad96101196e401718f0/src/remoteForS3.ts
// Original implementation: https://github.com/remotely-save/remotely-save/blob/28b99557a864ef59c19d2ad96101196e401718f0/src/remoteForS3.ts
import { FetchHttpHandler, type FetchHttpHandlerOptions } from "@smithy/fetch-http-handler";
import { HttpRequest, HttpResponse } from "@smithy/protocol-http";
@@ -102,6 +102,7 @@ export class ObsHttpHandler extends FetchHttpHandler {
method: method,
url: url,
contentType: contentType,
throw: false,
};
const raceOfPromises = [
@@ -1,9 +1,10 @@
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { HttpRequest } from "@smithy/protocol-http";
import { beforeEach, describe, expect, it, vi } from "vitest";
const requestUrlMock = vi.hoisted(() =>
vi.fn<
(param: { body?: string | ArrayBuffer }) => Promise<{
(param: { body?: string | ArrayBuffer; throw?: boolean }) => Promise<{
headers: Record<string, string>;
status: number;
arrayBuffer: ArrayBuffer;
@@ -28,6 +29,42 @@ function requestWithBody(body: unknown) {
});
}
function mockS3ErrorResponse(status: number, code?: string) {
requestUrlMock.mockImplementation(async (param) => {
if (param.throw !== false) {
throw new Error(`Request failed, status ${status}`);
}
return {
headers: { "content-type": "application/xml" },
status,
arrayBuffer: new TextEncoder().encode(code ? `<Error><Code>${code}</Code></Error>` : "").buffer,
};
});
}
function createS3Client() {
return new S3Client({
region: "us-east-1",
credentials: {
accessKeyId: "access-key",
secretAccessKey: "secret-key",
},
endpoint: "https://objects.example.com",
forcePathStyle: true,
maxAttempts: 1,
requestHandler: new ObsHttpHandler(),
});
}
function getMissingObject(client: S3Client) {
return client.send(
new GetObjectCommand({
Bucket: "bucket",
Key: "missing.json",
})
);
}
describe("ObsHttpHandler request bodies", () => {
beforeEach(() => {
requestUrlMock.mockReset();
@@ -58,3 +95,56 @@ describe("ObsHttpHandler request bodies", () => {
expect(requestUrlMock).not.toHaveBeenCalled();
});
});
describe("ObsHttpHandler response handling", () => {
beforeEach(() => {
requestUrlMock.mockReset();
});
it("returns an HTTP error response to the Smithy client", async () => {
mockS3ErrorResponse(404, "NoSuchKey");
const request = new HttpRequest({
protocol: "https:",
hostname: "objects.example.com",
method: "GET",
path: "/bucket/missing.json",
headers: {},
});
const result = await new ObsHttpHandler().handle(request);
expect(requestUrlMock).toHaveBeenCalledWith(expect.objectContaining({ throw: false }));
expect(result.response.statusCode).toBe(404);
});
it.each([
{ code: "NoSuchKey", name: "NoSuchKey" },
{ code: undefined, name: "NotFound" },
])("lets the S3 client classify a missing object as $name", async ({ code, name }) => {
mockS3ErrorResponse(404, code);
await expect(getMissingObject(createS3Client())).rejects.toMatchObject({
name,
$metadata: { httpStatusCode: 404 },
});
});
it.each([
{ status: 403, code: "AccessDenied" },
{ status: 500, code: "InternalError" },
])("keeps an S3 $status response distinct from a missing object", async ({ status, code }) => {
mockS3ErrorResponse(status, code);
await expect(getMissingObject(createS3Client())).rejects.toMatchObject({
name: code,
$metadata: { httpStatusCode: status },
});
});
it("preserves a transport failure", async () => {
const failure = new Error("network failed");
requestUrlMock.mockRejectedValue(failure);
await expect(new ObsHttpHandler().handle(requestWithBody(new ArrayBuffer(0)))).rejects.toBe(failure);
});
});