Repository navigation
Add provider-neutral object storage and S3-compatible adapter - #237
Conversation
| await storage.put('images/logo.png', imageBytes, { contentType: 'image/png' }); | ||
| const url = storage.publicUrl('images/logo.png'); | ||
| // https://assets.example.com/assets/images/logo.png | ||
| await storage.close(); |
There was a problem hiding this comment.
I would make Storage disposable so we could use using to run .close() automatically.
There was a problem hiding this comment.
Implemented in bde4139. ObjectStorage now extends AsyncDisposable, so both concrete adapters and interface-typed storage work with await using storage = .... The hook delegates to the existing idempotent close().
The examples now use await using because closing waits for active operations and multipart cleanup. Documentation also clarifies that application-wide instances live until shutdown and borrowed instances are not disposed by handlers.
Added type and runtime coverage for normal scope exit, exceptions, explicit close followed by automatic disposal, unread downloads, and waiting for delayed multipart cleanup before releasing the client.
Validation passed: lint, build, 4,485 tests with no type errors, both website typechecks, and all 9 Garage integration tests.
Description
Add F04–F05: applications can store assets through one provider-neutral interface and switch S3-compatible providers through configuration.
@cleverbrush/storagesupplies typed put/get/stat/copy/delete contracts, portable errors and stable public URL mapping without an S3 SDK dependency.@cleverbrush/storage-s3implements the contract with explicit endpoint, region, bucket, credentials, addressing style and key prefix.The shared
ObjectStoragecontract supportsawait usingto await automatic cleanup on scope exit, including exceptions. Explicitclose()remains idempotent. The adapter streams downloads and bounds multipart upload buffering. Cancellation reaches individual S3 requests, owned streams are released, and unfinished uploads receive cleanup attempts. Copy preserves metadata within the configured bucket; deletion is idempotent. Errors omit raw provider requests and credentials. Public asset URLs are configured independently from authenticated endpoints.Includes package registration, a minor changeset, documentation and typed-upload/HTTP-stream examples. Adds a shared adapter contract suite and a dedicated CI job using an official Garage v2.3.0 image pinned by digest. The same integration command can target an explicitly designated external test bucket, using unique prefixes and cleanup.
Garage is verified by integration tests. Hetzner configuration follows its documented S3 interface; live Hetzner testing is opt-in and was not performed. Signed URLs, direct browser uploads, bucket provisioning, asset migration and application rewrites remain out of scope. Native server-side copy limits apply, and failed-upload cleanup requires a reachable provider.
Type of Change
Validation
npm run lint— passednpm run build— all 24 tasks passednpm run test— 4,485 tests across 229 files passed; no type errorsnpm run typecheck:schema-site— passednpm run typecheck:docs-site— passednpm run test:storage:integration— all 9 Garage tests passedgit diff --check— passedCoverage includes a multipart byte-for-byte round trip, metadata copies, prefix mapping, empty/replaced objects, missing objects, invalid credentials, interrupted-upload cleanup, slow-provider backpressure, cancellation, unread downloads, automatic disposal with delayed multipart cleanup, malformed responses and late stream errors. The core suite also runs against a test-only in-memory adapter.
Checklist
npm run lintand fixed any issuesnpm run testand all tests pass