fix(sdk): fs adapter flush() tracks in-flight writes; add to T13 contract suite (#1425)

* fix(sdk): fs adapter flush() tracks in-flight writes; add to T13 contract suite

* fix(sdk): document flush() first-error rejection semantics

Promise.all rejects on first write failure; errors also surface via
persist:error event channel per write.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>

---------

Co-authored-by: Miguel Ángel <miguel07alm@protonmail.com>
This commit is contained in:
Vance Ingalls
2026-06-15 01:57:54 -07:00
committed by GitHub
co-authored by Claude Sonnet 4.6 Miguel Ángel
parent 577a689860
commit 0a30011abd
2 changed files with 25 additions and 1 deletions
+16 -1
View File
@@ -18,6 +18,7 @@ class FsAdapter implements PersistAdapter {
private readonly root: string; private readonly root: string;
private readonly maxVersions: number; private readonly maxVersions: number;
private errorHandlers: Array<(e: PersistErrorEvent) => void> = []; private errorHandlers: Array<(e: PersistErrorEvent) => void> = [];
private readonly inflightWrites = new Set<Promise<void>>();
private _writeLocks = new Map<string, Promise<void>>(); private _writeLocks = new Map<string, Promise<void>>();
constructor(opts: FsAdapterOptions) { constructor(opts: FsAdapterOptions) {
@@ -35,6 +36,16 @@ class FsAdapter implements PersistAdapter {
} }
async write(path: string, content: string): Promise<void> { async write(path: string, content: string): Promise<void> {
const p = this.doWrite(path, content);
this.inflightWrites.add(p);
try {
await p;
} finally {
this.inflightWrites.delete(p);
}
}
private async doWrite(path: string, content: string): Promise<void> {
try { try {
const abs = this.abs(path); const abs = this.abs(path);
await mkdir(dirname(abs), { recursive: true }); await mkdir(dirname(abs), { recursive: true });
@@ -45,7 +56,11 @@ class FsAdapter implements PersistAdapter {
} }
} }
async flush(): Promise<void> {} async flush(): Promise<void> {
// Promise.all rejects on the first write failure; per-write errors are also
// surfaced individually through the persist:error event channel.
await Promise.all([...this.inflightWrites]);
}
async listVersions(path: string): Promise<PersistVersionEntry[]> { async listVersions(path: string): Promise<PersistVersionEntry[]> {
const dir = this.versionsDir(path); const dir = this.versionsDir(path);
@@ -9,8 +9,12 @@
* runPersistAdapterContract("s3", () => createS3Adapter({ bucket, prefix })) * runPersistAdapterContract("s3", () => createS3Adapter({ bucket, prefix }))
*/ */
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { createMemoryAdapter } from "./memory.js"; import { createMemoryAdapter } from "./memory.js";
import { createFsAdapter } from "./fs.js";
import type { PersistAdapter } from "./types.js"; import type { PersistAdapter } from "./types.js";
export function runPersistAdapterContract( export function runPersistAdapterContract(
@@ -126,3 +130,8 @@ export function runPersistAdapterContract(
// Run the suite against the memory adapter immediately // Run the suite against the memory adapter immediately
runPersistAdapterContract("memory", createMemoryAdapter); runPersistAdapterContract("memory", createMemoryAdapter);
// Run against the fs adapter — each test gets an isolated tmpdir
runPersistAdapterContract("fs", () =>
createFsAdapter({ root: mkdtempSync(join(tmpdir(), "hf-fs-test-")) }),
);