mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-08-07 08:09:27 +00:00
- feat(storage): server-side per-namespace byte-quota enforcement on upload + pin (bugboard #141). Rejects when (current logical usage + new bytes) × replication-factor would exceed the namespace's configured max_storage_bytes, with a typed STORAGE_QUOTA_EXCEEDED (HTTP 413) error. OPT-IN: a namespace with no namespace_quotas row (all namespaces today) is unlimited, so this ships dark and changes nothing until an operator sets a budget. Checks server-observed input size before Add (no orphan blob); fail-open on a transient quota-lookup error. Known follow-up (documented, deferred while opt-in/unused): concurrent-burst TOCTOU + fail-open hardening before GA. - feat(sdk): HttpClient.uploadFile accepts an external AbortSignal (bugboard #144). A caller abort terminates the in-flight upload at the socket and rejects with SDKError code "ABORTED" — distinct from the internal timeout ("TIMEOUT") and never retried; the retry loop also stops once the signal is aborted. IHttpTransport kept in sync.
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import { HttpClient } from "../../../src/core/http";
|
|
import { SDKError } from "../../../src/errors";
|
|
|
|
/**
|
|
* Bugboard #144 — HttpClient.uploadFile must accept an external AbortSignal and
|
|
* honor it for the in-flight request, distinguishably from the internal timeout:
|
|
* (1) a caller abort terminates the in-flight fetch (socket-level),
|
|
* (2) a caller abort rejects with code "ABORTED" (never "TIMEOUT") and is
|
|
* NEVER retried,
|
|
* (3) the internal timeout still surfaces as "TIMEOUT".
|
|
*/
|
|
describe("Bug #144 — uploadFile honors an external AbortSignal", () => {
|
|
// A fetch that stays in-flight until its signal aborts, then rejects with a
|
|
// DOM-style AbortError — mimicking a real cancelled upload.
|
|
function abortableFetch() {
|
|
return vi.fn((_url: any, opts: any) => {
|
|
return new Promise((_resolve, reject) => {
|
|
const signal: AbortSignal = opts.signal;
|
|
const fail = () => {
|
|
const e = new Error("The operation was aborted");
|
|
(e as any).name = "AbortError";
|
|
reject(e);
|
|
};
|
|
if (signal.aborted) return fail();
|
|
signal.addEventListener("abort", fail, { once: true });
|
|
// otherwise never resolves on its own (in-flight)
|
|
});
|
|
});
|
|
}
|
|
|
|
function client(fetchImpl: any, maxRetries = 0) {
|
|
return new HttpClient({
|
|
baseURL: "https://gw.example",
|
|
maxRetries,
|
|
timeout: 5000,
|
|
fetch: fetchImpl,
|
|
});
|
|
}
|
|
|
|
it("caller abort mid-flight → rejects code ABORTED, fetch called exactly once (no retry)", async () => {
|
|
const fetchImpl = abortableFetch();
|
|
const ac = new AbortController();
|
|
const c = client(fetchImpl, 3); // retries enabled — must NOT retry a caller abort
|
|
const p = c.uploadFile("/v1/storage/upload", new FormData(), {
|
|
signal: ac.signal,
|
|
});
|
|
ac.abort();
|
|
const err = await p.catch((e) => e);
|
|
expect(err).toBeInstanceOf(SDKError);
|
|
expect(err.code).toBe("ABORTED");
|
|
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("signal already aborted before start → rejects ABORTED, fetch never called", async () => {
|
|
const fetchImpl = vi.fn();
|
|
const ac = new AbortController();
|
|
ac.abort();
|
|
const err = await client(fetchImpl)
|
|
.uploadFile("/v1/storage/upload", new FormData(), { signal: ac.signal })
|
|
.catch((e) => e);
|
|
expect(err).toBeInstanceOf(SDKError);
|
|
expect(err.code).toBe("ABORTED");
|
|
expect(fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("internal timeout still surfaces as TIMEOUT (not ABORTED)", async () => {
|
|
const fetchImpl = abortableFetch();
|
|
const err = await client(fetchImpl)
|
|
.uploadFile("/v1/storage/upload", new FormData(), { timeout: 10 })
|
|
.catch((e) => e);
|
|
expect(err).toBeInstanceOf(SDKError);
|
|
expect(err.code).toBe("TIMEOUT");
|
|
});
|
|
|
|
it("no signal / not aborted → normal success", async () => {
|
|
const okFetch = vi.fn(
|
|
async () =>
|
|
new Response(JSON.stringify({ ok: true, cid: "Qm1" }), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
})
|
|
);
|
|
const res = await client(okFetch).uploadFile(
|
|
"/v1/storage/upload",
|
|
new FormData(),
|
|
{ signal: new AbortController().signal }
|
|
);
|
|
expect(res).toEqual({ ok: true, cid: "Qm1" });
|
|
});
|
|
});
|