| 1 | import * as Effect from "effect/Effect"; |
| 2 | import { stableValue } from "./stable.ts"; |
| 3 | |
| 4 | type Input = ArrayBuffer | Uint8Array | string; |
| 5 | |
| 6 | export const sha256 = (input: Input) => |
| 7 | Effect.promise(async () => { |
| 8 | const digest = await crypto.subtle.digest("SHA-256", toArrayBuffer(input)); |
| 9 | const hashArray = Array.from(new Uint8Array(digest)); |
| 10 | return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); |
| 11 | }); |
| 12 | |
| 13 | export const sha256Object = (input: object) => |
| 14 | sha256(JSON.stringify(stableValue(input))); |
| 15 | |
| 16 | /** |
| 17 | * Stable sha256 hex digest of a resolved Task input. Unlike |
| 18 | * {@link sha256Object} this does not stabilize key ordering — it hashes the |
| 19 | * raw `JSON.stringify(input ?? null)` so identical inputs map to identical |
| 20 | * hashes for caching/equality checks. |
| 21 | */ |
| 22 | export const hashInput = (input: unknown): Effect.Effect<string> => |
| 23 | sha256(JSON.stringify(input ?? null)); |
| 24 | |
| 25 | const toArrayBuffer = (input: Input) => { |
| 26 | if (input instanceof ArrayBuffer) { |
| 27 | return input; |
| 28 | } |
| 29 | if (typeof input === "string") { |
| 30 | return new TextEncoder().encode(input); |
| 31 | } |
| 32 | return input.buffer.slice( |
| 33 | input.byteOffset, |
| 34 | input.byteOffset + input.byteLength, |
| 35 | ) as ArrayBuffer; |
| 36 | }; |