@effected/images 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ImageBackend.js +5 -0
- package/ImageBackendError.js +17 -6
- package/README.md +1 -1
- package/cache.d.ts +13 -0
- package/internal/causeMessage.js +39 -0
- package/package.json +1 -1
package/ImageBackend.js
CHANGED
|
@@ -50,6 +50,7 @@ var ImageBackend = class ImageBackend extends Context.Service()("@effected/image
|
|
|
50
50
|
for (const [ext, contentType] of FILES) {
|
|
51
51
|
const bytes = yield* fs.readFile(fileFor(key, ext)).pipe(Effect.map(Option.some), Effect.catchReason("PlatformError", "NotFound", () => Effect.succeedNone), Effect.mapError((cause) => new ImageBackendError({
|
|
52
52
|
operation: "get",
|
|
53
|
+
reason: "backend",
|
|
53
54
|
key,
|
|
54
55
|
cause
|
|
55
56
|
})));
|
|
@@ -65,6 +66,7 @@ var ImageBackend = class ImageBackend extends Context.Service()("@effected/image
|
|
|
65
66
|
const ext = params.contentType !== void 0 && Object.hasOwn(EXTENSION_BY_TYPE, params.contentType) ? EXTENSION_BY_TYPE[params.contentType] : void 0;
|
|
66
67
|
if (ext === void 0) return yield* new ImageBackendError({
|
|
67
68
|
operation: "set",
|
|
69
|
+
reason: "unsupported-content-type",
|
|
68
70
|
key: params.key,
|
|
69
71
|
cause: /* @__PURE__ */ new Error(`unsupported content type ${JSON.stringify(params.contentType ?? null)}`)
|
|
70
72
|
});
|
|
@@ -78,6 +80,7 @@ var ImageBackend = class ImageBackend extends Context.Service()("@effected/image
|
|
|
78
80
|
yield* fs.rename(temp, target);
|
|
79
81
|
}).pipe(Effect.onError(() => fs.remove(temp, { force: true }).pipe(Effect.ignore)), Effect.mapError((cause) => new ImageBackendError({
|
|
80
82
|
operation: "set",
|
|
83
|
+
reason: "backend",
|
|
81
84
|
key: params.key,
|
|
82
85
|
cause
|
|
83
86
|
})));
|
|
@@ -101,11 +104,13 @@ var ImageBackend = class ImageBackend extends Context.Service()("@effected/image
|
|
|
101
104
|
contentType: entry.contentType
|
|
102
105
|
}))), Effect.mapError((cause) => new ImageBackendError({
|
|
103
106
|
operation: "get",
|
|
107
|
+
reason: "backend",
|
|
104
108
|
key: k,
|
|
105
109
|
cause
|
|
106
110
|
})), Effect.withSpan("ImageBackend.get")),
|
|
107
111
|
set: (params) => source.set(params).pipe(Effect.mapError((cause) => new ImageBackendError({
|
|
108
112
|
operation: "set",
|
|
113
|
+
reason: "backend",
|
|
109
114
|
key: params.key,
|
|
110
115
|
cause
|
|
111
116
|
})), Effect.withSpan("ImageBackend.set"))
|
package/ImageBackendError.js
CHANGED
|
@@ -1,26 +1,37 @@
|
|
|
1
|
+
import { causeMessage } from "./internal/causeMessage.js";
|
|
1
2
|
import { Schema } from "effect";
|
|
2
3
|
|
|
3
4
|
// src/ImageBackendError.ts
|
|
4
|
-
const causeMessage = (cause) => {
|
|
5
|
-
let current = cause;
|
|
6
|
-
for (let depth = 0; depth < 8 && current instanceof Error && current.cause instanceof Error; depth++) current = current.cause;
|
|
7
|
-
return current instanceof Error ? current.message : String(current);
|
|
8
|
-
};
|
|
9
5
|
/**
|
|
10
6
|
* An image backend read or write failed.
|
|
11
7
|
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* `reason` says why, so a caller branches on a literal rather than reading
|
|
10
|
+
* the message: `backend` is a failure of the store underneath (the file
|
|
11
|
+
* system for `layerDirectory`, the adapted service for `layerFrom`), and
|
|
12
|
+
* `unsupported-content-type` is a `set` whose content type the directory
|
|
13
|
+
* backend has no file extension for. `backend` names the source rather than
|
|
14
|
+
* claiming file IO, because `layerFrom` wraps any source failure — a store
|
|
15
|
+
* `Cache` error included — under it.
|
|
16
|
+
*
|
|
17
|
+
* `reason` is required: a hand-written backend states which case it hit.
|
|
18
|
+
*
|
|
12
19
|
* @public
|
|
13
20
|
*/
|
|
14
21
|
var ImageBackendError = class extends Schema.TaggedError()("ImageBackendError", {
|
|
15
22
|
/** The backend operation that failed. */
|
|
16
23
|
operation: Schema.Literals(["get", "set"]),
|
|
24
|
+
/** Why it failed: the store underneath, or a content type the backend cannot store. */
|
|
25
|
+
reason: Schema.Literals(["backend", "unsupported-content-type"]),
|
|
17
26
|
/** The key involved. */
|
|
18
27
|
key: Schema.String,
|
|
19
28
|
/** The underlying failure, preserved structurally. */
|
|
20
29
|
cause: Schema.Defect()
|
|
21
30
|
}) {
|
|
22
31
|
get message() {
|
|
23
|
-
|
|
32
|
+
const summary = `Image backend ${this.operation} failed for key "${this.key}" (${this.reason})`;
|
|
33
|
+
const detail = causeMessage(this.cause);
|
|
34
|
+
return detail === void 0 ? summary : `${summary}: ${detail}`;
|
|
24
35
|
}
|
|
25
36
|
};
|
|
26
37
|
|
package/README.md
CHANGED
|
@@ -142,7 +142,7 @@ The directory backend stores `<directory>/<key>.<ext>` through a temp file and a
|
|
|
142
142
|
- `ImageFormat` — the literal union `png | jpeg | gif | webp | avif`.
|
|
143
143
|
- `ImageParseError` — the one parse failure, with a `reason` and, once a signature matched, the `format`.
|
|
144
144
|
- `ImageCacheKey` / `ImageCacheKeyError` — content-addressed keys from schema-encoded parameters; failure reasons `encode`, `non-json` and `digest`.
|
|
145
|
-
- `ImageBackend` / `ImageBackendError` — the storage port, a directory backend, a no-store backend and a structural adapter
|
|
145
|
+
- `ImageBackend` / `ImageBackendError` — the storage port, a directory backend, a no-store backend and a structural adapter; failure reasons `backend` and `unsupported-content-type`.
|
|
146
146
|
- `ImageCache` / `ImageGenerateError` — `getOrGenerate` with an optional `accept` format list that narrows the result's format.
|
|
147
147
|
|
|
148
148
|
SVG, BMP, ICO, TIFF, HEIC, JPEG XL, EXIF orientation, image transformation and cache eviction are out of scope.
|
package/cache.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import * as Images from "@effected/images";
|
|
|
3
3
|
declare const ImageBackendError_base: Schema.Class<ImageBackendError, Schema.TaggedStruct<"ImageBackendError", {
|
|
4
4
|
/** The backend operation that failed. */
|
|
5
5
|
readonly operation: Schema.Literals<readonly ["get", "set"]>;
|
|
6
|
+
/** Why it failed: the store underneath, or a content type the backend cannot store. */
|
|
7
|
+
readonly reason: Schema.Literals<readonly ["backend", "unsupported-content-type"]>;
|
|
6
8
|
/** The key involved. */
|
|
7
9
|
readonly key: Schema.String;
|
|
8
10
|
/** The underlying failure, preserved structurally. */
|
|
@@ -11,6 +13,17 @@ declare const ImageBackendError_base: Schema.Class<ImageBackendError, Schema.Tag
|
|
|
11
13
|
/**
|
|
12
14
|
* An image backend read or write failed.
|
|
13
15
|
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* `reason` says why, so a caller branches on a literal rather than reading
|
|
18
|
+
* the message: `backend` is a failure of the store underneath (the file
|
|
19
|
+
* system for `layerDirectory`, the adapted service for `layerFrom`), and
|
|
20
|
+
* `unsupported-content-type` is a `set` whose content type the directory
|
|
21
|
+
* backend has no file extension for. `backend` names the source rather than
|
|
22
|
+
* claiming file IO, because `layerFrom` wraps any source failure — a store
|
|
23
|
+
* `Cache` error included — under it.
|
|
24
|
+
*
|
|
25
|
+
* `reason` is required: a hand-written backend states which case it hit.
|
|
26
|
+
*
|
|
14
27
|
* @public
|
|
15
28
|
*/
|
|
16
29
|
export declare class ImageBackendError extends ImageBackendError_base {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Predicate } from "effect";
|
|
2
|
+
|
|
3
|
+
// src/internal/causeMessage.ts
|
|
4
|
+
/** How many nodes of a cause chain the walk reads before stopping, so a cyclic chain cannot spin. */
|
|
5
|
+
const CAUSE_DEPTH = 8;
|
|
6
|
+
/** The message a single node carries: a thrown string, or a non-empty string `message` property. */
|
|
7
|
+
const messageAt = (node) => {
|
|
8
|
+
const text = Predicate.isString(node) ? node : Predicate.hasProperty(node, "message") && Predicate.isString(node.message) ? node.message : void 0;
|
|
9
|
+
return text === void 0 || text.trim() === "" ? void 0 : text;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Every distinct non-empty message a preserved cause chain carries, nearest
|
|
13
|
+
* first and joined with `: `, or `undefined` when nothing along it has one.
|
|
14
|
+
*
|
|
15
|
+
* A `Schema.Defect()` field holds whatever was thrown, so the read is guarded
|
|
16
|
+
* rather than assumed: `Predicate.hasProperty` + `Predicate.isString` accepts
|
|
17
|
+
* an `Error`, a cross-realm `Error` that fails `instanceof`, a plain
|
|
18
|
+
* `{ message }` object and a thrown string alike. An empty or whitespace-only
|
|
19
|
+
* message counts as none. The walk follows `cause` links because a wrapper's
|
|
20
|
+
* text is often the generic summary and the driver's text underneath is the
|
|
21
|
+
* actionable detail, yet a wrapper that composes its own context must not
|
|
22
|
+
* lose it either; so nothing is dropped, and a deeper text the nearer one
|
|
23
|
+
* already contains is not repeated. The walk is bounded, so a cycle terminates.
|
|
24
|
+
*/
|
|
25
|
+
const causeMessage = (cause) => {
|
|
26
|
+
let message;
|
|
27
|
+
let node = cause;
|
|
28
|
+
for (let depth = 0; depth < CAUSE_DEPTH && node !== void 0 && node !== null; depth++) {
|
|
29
|
+
const text = messageAt(node);
|
|
30
|
+
if (text !== void 0) {
|
|
31
|
+
if (message === void 0 || text.includes(message)) message = text;
|
|
32
|
+
else if (!message.includes(text)) message = `${message}: ${text}`;
|
|
33
|
+
}
|
|
34
|
+
node = Predicate.hasProperty(node, "cause") ? node.cause : void 0;
|
|
35
|
+
}
|
|
36
|
+
return message;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export { causeMessage };
|