95 lines
9.9 KiB
Markdown
95 lines
9.9 KiB
Markdown
# Pass `SuaveplanError` subclasses through `StorageUriResolver` unchanged
|
|
|
|
**Package**: `@suaveplan/storage-uri` (behavioural change), `@suaveplan/drive-uri` (spec tightening only — no code change)
|
|
**Status**: PROPOSED
|
|
**Type**: behaviour fix
|
|
**Depends on**: `add-storage-uri` (in `openspec/changes/`), `add-drive-uri` (in `openspec/changes/`) — neither archived yet, so both deltas update in-place.
|
|
|
|
- **Spec delta (storage-uri)**: ./specs/storage-uri/spec.md
|
|
- **Spec delta (drive-uri)**: ./specs/drive-uri/spec.md
|
|
|
|
## Why
|
|
|
|
`StorageUriResolver` today re-wraps **every** non-`StorageUriError` rejection from a plugin's adapter as `STORAGE_URI.ADAPTER_ERROR` (or `STORAGE_URI.WRITE_UNSUPPORTED` for write throws). That was a reasonable default at the time of `add-storage-uri` — the resolver had no knowledge of any other error class — but it breaks the error-code discriminator system for every downstream plugin family that emits `SuaveplanError` subclasses.
|
|
|
|
Concretely: a `drive+mesh://` URI failing chunk-signature verification today reaches the consumer as `STORAGE_URI.ADAPTER_ERROR` with `cause.code === "DRIVE_URI.MESH_VERIFY_FAILED"` (or, transitively, `cause.cause.code === "DRIVE_MESH.SIG_VERIFY_FAILED"`). Consumers cannot write `switch (err.code)` against the meaningful code without inspecting `.cause` chains — defeating the whole point of having a per-package code registry. The same problem applies to every `DriveUriError`, every `StorageHttpError`, every `StorageS3Error`, and any future URI-plugin family.
|
|
|
|
The fix is local: teach the resolver to treat any `SuaveplanError` subclass as already-namespaced and pass it through untouched. Wrap only **non-Suaveplan** throwables (`Error`, plain objects, strings, etc.) — those still need the `STORAGE_URI.ADAPTER_ERROR` envelope for stable diagnostics. `StorageUriError` itself remains passed through (no double-wrap), unchanged from today.
|
|
|
|
This change also retires the spec deviations that `add-drive-uri` carried at the top of its delta ("the resolver wraps `DriveUriError` as `STORAGE_URI.ADAPTER_ERROR`, preserving the original on `cause`"). With the pass-through in place, the drive-uri scenarios can assert directly on `err.code === "DRIVE_URI.*"` without `.cause` indirection.
|
|
|
|
## What changes
|
|
|
|
- MODIFIED: `StorageUriResolver`'s error-wrap branch — change the existing `instanceof StorageUriError ? rethrow : wrap` discriminator to `instanceof SuaveplanError ? rethrow : wrap`. `StorageUriError extends SuaveplanError` so the existing `StorageUriError` pass-through is preserved as a special case of the new rule.
|
|
- MODIFIED: The `WRITE_UNSUPPORTED` re-wrap branch (for `write()` failures from the adapter) — same discriminator change. `SuaveplanError` subclasses thrown from `adapter.set` pass through with their original code; plain `Error` throws are still wrapped as `STORAGE_URI.WRITE_UNSUPPORTED`.
|
|
- MODIFIED: `storage-uri` spec delta — two requirements updated:
|
|
- "Adapter error re-wrapping": broaden the pass-through rule from `StorageUriError` to `SuaveplanError`; add a scenario asserting that a thrown `DriveUriError` reaches the caller with its original `code`.
|
|
- "Streaming writes with buffered fallback": broaden the same rule on the write path; the "read-only adapter surfaces `WRITE_UNSUPPORTED`" scenario now applies only to non-Suaveplan throwables. A new scenario covers the pass-through case.
|
|
- MODIFIED: `drive-uri` spec delta — five scenarios tightened to drop the `.cause` indirection. The two `**Note —** revisited in `fix-storage-uri-error-passthrough`...` annotations at the top of the drive-uri delta are removed (the first one); the second note (about signal threading) stays.
|
|
- ADDED: A new `storage-uri` test `src/resolver/resolver.passthrough.test.ts` (or extension of `resolver.test.ts`) covering:
|
|
- A thrown `SuaveplanError("FOO.BAR", ...)` passes through unchanged.
|
|
- A thrown plain `Error("disk fault")` is still wrapped as `STORAGE_URI.ADAPTER_ERROR` with `cause.message === "disk fault"`.
|
|
- A thrown `SuaveplanError` from `adapter.set` passes through (no `WRITE_UNSUPPORTED` wrap).
|
|
- MODIFIED: Existing `drive-uri` tests that assert on `err.cause.code` — switched to assert on `err.code` directly. The drive-uri test suite shrinks slightly in line count (no more `.cause` ladders) and stays at 100/100/100/100 coverage.
|
|
|
|
## Out of scope
|
|
|
|
- **Signal threading through `adapter.get`.** Separate concern, tracked under a future `add-signal-to-storage-like` change. Touches every `StorageLike` adapter; defer until a real consumer requires cancellable buffered reads. The signal-threading note in `add-drive-uri/specs/drive-uri/spec.md` stays.
|
|
- **Resolver-side translation of well-known downstream codes.** Tempting to map `DRIVE_MESH.SIG_VERIFY_FAILED` → some `STORAGE_URI.INTEGRITY_FAILED` umbrella code; rejected because it would force the resolver to know every downstream registry. The pass-through gives consumers the right code without the resolver having to translate.
|
|
- **`UriResolverLike` in `@suaveplan/types`.** No concrete second consumer; deferred until one appears.
|
|
- **Touching any other URI-plugin package.** This is a one-file behaviour change inside `storage-uri` plus spec tightening in `storage-uri` and `drive-uri`. Other plugin packages (`storage-http`, `storage-cdn-*`, etc.) inherit the new behaviour for free without code change.
|
|
|
|
## Source mapping
|
|
|
|
This is a follow-up to genesis-original changes; no upstream `suaveplan/` reference.
|
|
|
|
### Requirements with no source line
|
|
|
|
| Requirement | Justification |
|
|
|---|---|
|
|
| Pass-through trigger is `instanceof SuaveplanError`, not a list of known classes | The `defineCodes` discipline is the contract for "this is a structured, namespaced error"; `SuaveplanError` is its runtime carrier. Any current or future package that uses `defineCodes` automatically participates. Adding a class allowlist would force the resolver to grow whenever a new URI-plugin family lands. |
|
|
| Plain `Error` / non-`SuaveplanError` throws still wrap | Unstructured throws have no namespaced code; without the wrap, consumers cannot route diagnostics by code at all. The wrap remains the right default for foreign throwables. |
|
|
| Spec tightening lives in this change, not as a retroactive edit to `add-drive-uri` | Both `add-storage-uri` and `add-drive-uri` are still in `openspec/changes/` (not archived). The cleanest history is: each change captures the contract as authored; this change layers a `MODIFIED` delta on top. Once all three archive, the canonical specs reflect the final behaviour. |
|
|
|
|
## Verification approach
|
|
|
|
Every requirement carries a `**Verified by:**` line in the respective spec delta. The two new / tightened test files:
|
|
|
|
- `src/resolver/resolver.test.ts` — extended with two new `it(...)` blocks (`"passes through thrown SuaveplanError subclasses unchanged"`, `"still wraps non-SuaveplanError throwables as ADAPTER_ERROR"`).
|
|
- `src/resolver/resolver.writes.test.ts` — extended with one new `it(...)` block (`"passes through thrown SuaveplanError subclasses on write"`).
|
|
- `packages/drive/drive-uri/src/plugins/**/*.test.ts` — `.cause`-based assertions rewritten to assert on `err.code` directly. No new tests added; same scenarios, simpler assertions.
|
|
|
|
## Impact
|
|
|
|
### Affected dependents
|
|
|
|
Run `bunx turbo ls --filter=...@suaveplan/storage-uri` to enumerate.
|
|
|
|
- `@suaveplan/storage-uri` — [x] tests run / [x] docs reviewed / [ ] no action needed (this is the package being modified)
|
|
- `@suaveplan/drive-uri` — [x] tests run (assertions tightened) / [x] docs reviewed (spec deviations retired) / [ ] no action needed
|
|
- `@suaveplan/storage-file` — [ ] tests run / [ ] docs reviewed / [x] no action needed (does not throw `SuaveplanError` subclasses from `StorageLike` methods today; new pass-through path is a no-op for it)
|
|
- `@suaveplan/storage-http` — [ ] tests run / [ ] docs reviewed / [x] no action needed (throws `StorageHttpError extends SuaveplanError` — automatically benefits from pass-through but no breaking change since old behaviour was `ADAPTER_ERROR{cause:StorageHttpError}` which consumers were inspecting via `.cause` already; spec scenario in storage-uri's `WRITE_UNSUPPORTED` requirement is the only place this surfaced)
|
|
- `@suaveplan/storage-cdn`, `storage-cdn-rackspace`, `storage-cdn-bunnycdn` — [ ] tests run / [ ] docs reviewed / [x] no action needed (same as storage-http — `SuaveplanError` subclasses pass through; downstream tests will be more direct but currently pass via `.cause` inspection, which still works)
|
|
|
|
The `[ ] tests run` boxes for the four storage adapter packages are unchecked because the pass-through is purely permissive — consumers who were inspecting `.cause` see the same `cause` populated (the wrap still happens for plain `Error`, and `SuaveplanError` subclasses now reach the caller directly which is strictly more information). Run `bunx turbo test --filter=...@suaveplan/storage-uri` before merging to confirm no consumer relied on the wrap envelope itself (`err.code === "STORAGE_URI.ADAPTER_ERROR"`).
|
|
|
|
### Affected upstreams
|
|
|
|
- `@suaveplan/error` — None. `SuaveplanError` is consumed as-is via `instanceof`.
|
|
- `@suaveplan/types` — None.
|
|
|
|
### Behaviour change propagation (§13a)
|
|
|
|
**Old behaviour:** `resolver.read(uri)` rejects with `StorageUriError{code: "STORAGE_URI.ADAPTER_ERROR", cause: downstreamErr}` when the plugin's adapter throws.
|
|
|
|
**New behaviour:** `resolver.read(uri)` rejects with `downstreamErr` directly (same instance) when `downstreamErr instanceof SuaveplanError`; otherwise still rejects with `StorageUriError{code: "STORAGE_URI.ADAPTER_ERROR", cause: downstreamErr}`.
|
|
|
|
**Consumer search command:**
|
|
|
|
```bash
|
|
grep -rln 'STORAGE_URI\.ADAPTER_ERROR\|"STORAGE_URI.ADAPTER_ERROR"' packages/ \
|
|
| grep -v "/dist/\|/coverage/\|/storage-uri/"
|
|
```
|
|
|
|
**Recommended replacement:** consumers that currently `switch (err.cause?.code)` to route on the downstream code can now `switch (err.code)` directly. No migration is forced — the old `.cause` inspection still works (the wrap still happens for non-`SuaveplanError`).
|