1import * as Predicate from "effect/Predicate";
2import * as EffectRecord from "effect/Record";
3import * as Redacted from "effect/Redacted";
4
5export type Primitive =
6 | never
7 | undefined
8 | null
9 | boolean
10 | number
11 | string
12 | bigint
13 | symbol;
14
15export 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
24export 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 */
45export 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 */
65export 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
83export 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
94export const stripNullFields = <T>(value: T): T => stripFields(value, null);
95
96export const stripUndefinedFields = <T>(value: T): T =>
97 stripFields(value, undefined);
98
99type 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
108export 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};