// /
import { isPromise } from "./is-promise.js";
import { bin2string } from "./bin2text.js";
import { Option } from "./option.js";
import { Result } from "./result.js";
import { TxtEnDecoder, TxtEnDecoderSingleton } from "./txt-en-decoder.js";
import { CoerceURI } from "./uri.js";
import { isJSON } from "./is-json.js";
import { ReadonlyURL } from "./mutable-url.js";
import { FnSerialized, Level, LogSerializable, LogValue, Serialized } from "./types.js";
// export function sanitizeSerialize(lineEnd?: string): (key: unknown, val: unknown) => unknown {
// const cache = new Set();
// return function (this: unknown, key: unknown, value: unknown) {
// if (typeof value === "object" && value !== null) {
// // Duplicate reference found, discard key
// if (cache.has(value)) return "...";
// cache.add(value);
// }
// return lineEnd ? value + lineEnd : value;
// };
// }
export function asyncLogValue(_val: () => Promise): Promise {
// return Promise.resolve(logValue(val));
throw new Error("Not implemented");
}
export type LogValueArg = LogValue | Serialized | Serialized[] | FnSerialized | undefined | null;
export interface LogValueState {
readonly state?: Set;
readonly ignoreAttr: Option;
}
export function logValue(val: LogValueArg, ctx: LogValueState): LogValue {
return logValueInternal(val, {
...ctx,
state: ctx.state || new Set([Math.random()]),
});
}
type LogValueStateInternal = LogValueState & { readonly state: Set };
function logValueInternal(val: LogValueArg, ctx: LogValueStateInternal): LogValue {
ctx = {
...ctx,
state: ctx.state || new Set([Math.random()]),
} satisfies LogValueStateInternal;
switch (typeof val) {
case "function":
return new LogValue(val);
case "string": {
const resIsJson = isJSON(val);
if (resIsJson.isJSON) {
const ret = resIsJson.parsed;
if (typeof ret === "object" && ret !== null) {
return logValueInternal(ret, ctx);
}
}
const resIsURI = ReadonlyURL.from(val);
if (resIsURI.isOk()) {
return new LogValue(() => resIsURI.Ok().toString());
}
if (val.match(/[\n\r]/)) {
const lines = val.split(/[\n\r]+/).map((v) => v.trim());
return new LogValue(() => lines);
}
return new LogValue(() => val.toString());
}
case "number":
return new LogValue(() => val);
case "boolean":
return new LogValue(() => val);
case "object": {
if (val === null) {
return new LogValue(() => "null");
}
if (ArrayBuffer.isView(val)) {
try {
// should be injected
const decoder = TxtEnDecoderSingleton();
const asStr = decoder.decode(val);
const resIsJson = isJSON(asStr);
if (resIsJson.isJSON) {
const obj = JSON.parse(asStr) as LogValueArg;
return logValueInternal(obj, ctx);
}
} catch (e) {
// noop, we will return the stringified value
}
return logValueInternal(bin2string(val, 512), ctx);
}
if (Array.isArray(val)) {
return new LogValue(() =>
(val as Serialized[]).map((v) => logValue(v, { ...ctx, state: undefined }).value() as Serialized),
);
}
// if (val instanceof Response) {
// // my = my.clone() as unknown as LogValue | Serialized[] | null
// // const rval = my as unknown as Partial;
// // delete rval.clone
// // delete rval.blob
// }
if (val instanceof Headers) {
const headers: Record = {};
val.forEach((v, k) => {
headers[k] = v;
});
return new LogValue(() => headers as unknown as Serialized);
}
if (val instanceof ReadableStream) {
return new LogValue(() => ">Stream<");
}
if (isPromise(val)) {
return new LogValue(() => ">Promise<");
}
// Duplicate reference found, discard key
if (ctx.state?.has(val)) {
return new LogValue(() => "...");
}
ctx.state?.add(val);
try {
if (typeof val.toJSON === "function") {
return new LogValue(() => val.toJSON());
}
} catch (e) {
/* chrome is a strange beast
Failed to read a named property 'toJSON' from 'Window':
Blocked a frame with origin "http://localhost:3001" from accessing a cross-origin frame.
*/
}
const res: Record = {};
const typedVal = val as unknown as Record;
for (const key in typedVal) {
if (ctx.ignoreAttr.IsSome() && ctx.ignoreAttr.unwrap().test(key)) {
continue;
}
const element = typedVal[key];
if (element instanceof LogValue) {
res[key] = element;
} else {
if (typeof element !== "function") {
res[key] = logValueInternal(element, ctx);
}
}
}
// ugly as hell cast but how declare a self-referencing type?
return new LogValue(() => res as unknown as Serialized);
}
default:
if (!val) {
return new LogValue(() => "--Falsy--");
}
throw new Error(`Invalid type:${typeof val}`);
}
}
export interface Sized {
size: number;
}
export interface Lengthed {
length: number;
}
export type SizeOrLength = Sized | Lengthed;
export interface LogFormatter {
format(attr: LogSerializable): Uint8Array;
}
export interface LevelHandler {
enableLevel(level: Level, ...modules: string[]): void;
disableLevel(level: Level, ...modules: string[]): void;
setExposeStack(enable?: boolean): void;
setIgnoreAttr(re?: RegExp): void;
ignoreAttr: Option;
isStackExposed: boolean;
timerStart(key: string): Date; // now
timerEnd(key: string): { now: Date; duration: number }; // returns duration in nanoseconds
setDebug(...modules: (string | string[])[]): void;
isEnabled(ilevel: unknown, module: unknown): boolean;
}
export type HttpType = Response | Result | Request | Result;
export interface LoggerInterface {
readonly levelHandler: LevelHandler;
TxtEnDe(): TxtEnDecoder;
Module(key: string): R;
// if modules is empty, set for all Levels
EnableLevel(level: Level, ...modules: string[]): R;
DisableLevel(level: Level, ...modules: string[]): R;
Attributes(): Record;
SetDebug(...modules: (string | string[])[]): R;
// default is /^_/
SetIgnoreAttribute(re?: RegExp): R;
SetExposeStack(enable?: boolean): R;
SetFormatter(fmt: LogFormatter): R;
Ref(key: string, action: { toString: () => string } | FnSerialized): R;
Result(key: string, res: Result): R;
// default key url
Url(url: CoerceURI, key?: string): R;
// len
Len(value: unknown, key?: string): R;
Hash(value: unknown, key?: string): R;
Str>(key: T, value?: T extends string ? string : undefined): R;
Uint64>(key: T, value?: T extends string ? number : undefined): R;
Int>(key: T, value?: T extends string ? number : undefined): R;
Bool>(key: T, value?: T extends string ? unknown : undefined): R;
Any>(key: T, value?: T extends string ? unknown : undefined): R;
// first string is the key
// first response is Response
// first request is Request
Http(...mix: (HttpType | string)[]): R;
Pair(x: Record): R;
Error(): R;
Warn(): R;
Debug(): R;
Log(): R;
WithLevel(level: Level): R;
Err(err: T | Result | Error): R; // could be Error, or something which coerces to string
Info(): R;
Timestamp(): R;
TimerStart(key: string): R;
TimerEnd(key: string): R;
Dur(key: string, nsec: number): R;
}
export function IsLogger(obj: unknown): obj is Logger {
return (
typeof obj === "object" &&
[
"Module",
"EnableLevel",
"DisableLevel",
"SetDebug",
"Str",
"Error",
"Warn",
"Debug",
"Log",
"WithLevel",
"Err",
"Info",
"Timestamp",
"Any",
"Dur",
"Uint64",
]
.map((fn) => typeof (obj as Record)[fn] === "function")
.reduce((a, b) => a && b, true)
);
}
export interface WithLogger extends LoggerInterface {
Logger(): Logger;
}
export interface AsError {
AsError(): Error;
ResultError(): Result;
}
export interface Logger extends LoggerInterface {
With(): WithLogger;
Msg(...args: string[]): AsError;
Flush(): Promise;
}