| 1 | import * as Predicate from "effect/Predicate"; |
| 2 | import * as EffectRecord from "effect/Record"; |
| 3 | import * as Redacted from "effect/Redacted"; |
| 4 | |
| 5 | export type Primitive = |
| 6 | | never |
| 7 | | undefined |
| 8 | | null |
| 9 | | boolean |
| 10 | | number |
| 11 | | string |
| 12 | | bigint |
| 13 | | symbol; |
| 14 | |
| 15 | export const isPrimitive = (value: any): value is Primitive => |
| 16 | value === undefined || |
| 17 | value === null || |
| 18 | typeof value === "boolean" || |
| 19 | typeof value === "number" || |
| 20 | typeof value === "string" || |
| 21 | typeof value === "symbol" || |
| 22 | typeof value === "bigint"; |
| 23 | |
| 24 | export const isPlainObject = ( |
| 25 | value: unknown, |
| 26 | ): value is Record<string, unknown> => { |
| 27 | if (!Predicate.isObject(value)) return false; |
| 28 | return Object.getPrototypeOf(value) === Object.prototype; |
| 29 | }; |
| 30 | |
| 31 | /** |
| 32 | * Is `value` plain data the engine's deep walkers may traverse — an array, |
| 33 | * or an object whose prototype is `Object.prototype`/`null`? |
| 34 | * |
| 35 | * Everything else (class instances: effect's Effect/Layer/Context, Dates, |
| 36 | * SDK config objects, functions, ...) is a LEAF for every deep walker in |
| 37 | * the engine. The engine cannot meaningfully evaluate, diff, or persist |
| 38 | * *through* a foreign class instance — rebuilding one entry-by-entry strips |
| 39 | * its prototype — and walking one is not safe: effect ≥4.0.0-beta.103's |
| 40 | * Context is self-referential (`cacheRoot` points back at itself), which |
| 41 | * sent the naive walk-everything traversal into unbounded recursion |
| 42 | * (#1082). Resources and Outputs are detected structurally *before* this |
| 43 | * gate, so dependencies declared in plain props are unaffected. |
| 44 | */ |
| 45 | export const isPlainData = ( |
| 46 | value: unknown, |
| 47 | ): value is Record<string, unknown> | unknown[] => { |
| 48 | if (Array.isArray(value)) return true; |
| 49 | if (typeof value !== "object" || value === null) return false; |
| 50 | const proto = Object.getPrototypeOf(value); |
| 51 | return proto === Object.prototype || proto === null; |
| 52 | }; |
| 53 | |
| 54 | /** |
| 55 | * Rebuild a plain-data value by mapping each child through `walk`, |
| 56 | * preserving the shape (array → array, object → object). |
| 57 | * |
| 58 | * Cycle-guarded by ancestor path: a value that appears on its own ancestor |
| 59 | * chain is a true cycle and becomes `undefined` (it could never serialize |
| 60 | * or persist anyway). The guard is scoped to the current path — NOT a |
| 61 | * visited-set — so a diamond (the same object legitimately referenced from |
| 62 | * two places) is rebuilt in both places. Sync DFS only: add/delete around |
| 63 | * the recursion is race-free. (#1082) |
| 64 | */ |
| 65 | export const mapPlainData = ( |
| 66 | value: Record<string, unknown> | unknown[], |
| 67 | ancestors: WeakSet<object>, |
| 68 | walk: (child: unknown) => unknown, |
| 69 | ): unknown => { |
| 70 | if (ancestors.has(value)) return undefined; |
| 71 | ancestors.add(value); |
| 72 | try { |
| 73 | return Array.isArray(value) |
| 74 | ? value.map(walk) |
| 75 | : Object.fromEntries( |
| 76 | Object.entries(value).map(([key, child]) => [key, walk(child)]), |
| 77 | ); |
| 78 | } finally { |
| 79 | ancestors.delete(value); |
| 80 | } |
| 81 | }; |
| 82 | |
| 83 | export const stripFields = <T>(value: T, empty: null | undefined): T => { |
| 84 | if (Array.isArray(value)) { |
| 85 | return value.map((item) => stripFields(item, empty)) as T; |
| 86 | } |
| 87 | if (!isPlainObject(value)) return value; |
| 88 | return EffectRecord.map( |
| 89 | EffectRecord.filter(value, (entry) => entry !== empty), |
| 90 | (entry) => stripFields(entry, empty), |
| 91 | ) as T; |
| 92 | }; |
| 93 | |
| 94 | export const stripNullFields = <T>(value: T): T => stripFields(value, null); |
| 95 | |
| 96 | export const stripUndefinedFields = <T>(value: T): T => |
| 97 | stripFields(value, undefined); |
| 98 | |
| 99 | type UnwrapRedacted<T> = |
| 100 | T extends Redacted.Redacted<infer U> |
| 101 | ? U |
| 102 | : T extends Record<string, any> |
| 103 | ? { [K in keyof T]: UnwrapRedacted<T[K]> } |
| 104 | : T extends Array<infer U> |
| 105 | ? Array<UnwrapRedacted<U>> |
| 106 | : T; |
| 107 | |
| 108 | export const unwrapRedacted = <T>(value: T): UnwrapRedacted<T> => { |
| 109 | if (Redacted.isRedacted(value)) { |
| 110 | return Redacted.value(value) as UnwrapRedacted<T>; |
| 111 | } |
| 112 | if (Array.isArray(value)) { |
| 113 | return value.map(unwrapRedacted) as UnwrapRedacted<T>; |
| 114 | } |
| 115 | if (!isPlainObject(value)) return value as UnwrapRedacted<T>; |
| 116 | return EffectRecord.map(value, unwrapRedacted) as UnwrapRedacted<T>; |
| 117 | }; |