@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 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"))
@@ -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
- return `Image backend ${this.operation} failed for key "${this.key}": ${causeMessage(this.cause)}`;
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/images",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "private": false,
5
5
  "description": "Image facts (format, MIME type, dimensions) from bytes, and a generated-image cache, for Effect.",
6
6
  "keywords": [