tywrap 0.1.0
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/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +263 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +41 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +234 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/analyzer.d.ts +63 -0
- package/dist/core/analyzer.d.ts.map +1 -0
- package/dist/core/analyzer.js +676 -0
- package/dist/core/analyzer.js.map +1 -0
- package/dist/core/discovery.d.ts +85 -0
- package/dist/core/discovery.d.ts.map +1 -0
- package/dist/core/discovery.js +344 -0
- package/dist/core/discovery.js.map +1 -0
- package/dist/core/generator.d.ts +37 -0
- package/dist/core/generator.d.ts.map +1 -0
- package/dist/core/generator.js +368 -0
- package/dist/core/generator.js.map +1 -0
- package/dist/core/mapper.d.ts +40 -0
- package/dist/core/mapper.d.ts.map +1 -0
- package/dist/core/mapper.js +401 -0
- package/dist/core/mapper.js.map +1 -0
- package/dist/core/validation.d.ts +79 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +486 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/base.d.ts +12 -0
- package/dist/runtime/base.d.ts.map +1 -0
- package/dist/runtime/base.js +6 -0
- package/dist/runtime/base.js.map +1 -0
- package/dist/runtime/errors.d.ts +17 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +21 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/http.d.ts +22 -0
- package/dist/runtime/http.d.ts.map +1 -0
- package/dist/runtime/http.js +79 -0
- package/dist/runtime/http.js.map +1 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/node.d.ts +48 -0
- package/dist/runtime/node.d.ts.map +1 -0
- package/dist/runtime/node.js +328 -0
- package/dist/runtime/node.js.map +1 -0
- package/dist/runtime/optimized-node.d.ts +131 -0
- package/dist/runtime/optimized-node.d.ts.map +1 -0
- package/dist/runtime/optimized-node.js +622 -0
- package/dist/runtime/optimized-node.js.map +1 -0
- package/dist/runtime/protocol.d.ts +3 -0
- package/dist/runtime/protocol.d.ts.map +1 -0
- package/dist/runtime/protocol.js +3 -0
- package/dist/runtime/protocol.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +27 -0
- package/dist/runtime/pyodide.d.ts.map +1 -0
- package/dist/runtime/pyodide.js +191 -0
- package/dist/runtime/pyodide.js.map +1 -0
- package/dist/tools/python_suite.js +173 -0
- package/dist/tools/python_suite.js.map +1 -0
- package/dist/types/index.d.ts +299 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/tywrap.d.ts +29 -0
- package/dist/tywrap.d.ts.map +1 -0
- package/dist/tywrap.js +589 -0
- package/dist/tywrap.js.map +1 -0
- package/dist/utils/bundle-optimizer.d.ts +182 -0
- package/dist/utils/bundle-optimizer.d.ts.map +1 -0
- package/dist/utils/bundle-optimizer.js +680 -0
- package/dist/utils/bundle-optimizer.js.map +1 -0
- package/dist/utils/cache.d.ts +149 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +492 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/codec.d.ts +94 -0
- package/dist/utils/codec.d.ts.map +1 -0
- package/dist/utils/codec.js +150 -0
- package/dist/utils/codec.js.map +1 -0
- package/dist/utils/logger.d.ts +42 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +140 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/memory-profiler.d.ts +123 -0
- package/dist/utils/memory-profiler.d.ts.map +1 -0
- package/dist/utils/memory-profiler.js +426 -0
- package/dist/utils/memory-profiler.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +146 -0
- package/dist/utils/parallel-processor.d.ts.map +1 -0
- package/dist/utils/parallel-processor.js +636 -0
- package/dist/utils/parallel-processor.js.map +1 -0
- package/dist/utils/python.d.ts +8 -0
- package/dist/utils/python.d.ts.map +1 -0
- package/dist/utils/python.js +56 -0
- package/dist/utils/python.js.map +1 -0
- package/dist/utils/runtime.d.ts +153 -0
- package/dist/utils/runtime.d.ts.map +1 -0
- package/dist/utils/runtime.js +519 -0
- package/dist/utils/runtime.js.map +1 -0
- package/package.json +136 -0
- package/runtime/python_bridge.py +476 -0
- package/src/cli.ts +331 -0
- package/src/config/index.ts +289 -0
- package/src/core/analyzer.ts +788 -0
- package/src/core/discovery.ts +415 -0
- package/src/core/generator.ts +447 -0
- package/src/core/mapper.ts +504 -0
- package/src/core/validation.ts +693 -0
- package/src/index.ts +83 -0
- package/src/runtime/base.ts +32 -0
- package/src/runtime/errors.ts +19 -0
- package/src/runtime/http.ts +132 -0
- package/src/runtime/index.ts +24 -0
- package/src/runtime/node.ts +431 -0
- package/src/runtime/optimized-node.ts +838 -0
- package/src/runtime/protocol.ts +2 -0
- package/src/runtime/pyodide.ts +228 -0
- package/src/types/global.d.ts +60 -0
- package/src/types/index.ts +410 -0
- package/src/tywrap.ts +669 -0
- package/src/utils/bundle-optimizer.ts +876 -0
- package/src/utils/cache.ts +634 -0
- package/src/utils/codec.ts +275 -0
- package/src/utils/logger.ts +201 -0
- package/src/utils/memory-profiler.ts +582 -0
- package/src/utils/parallel-processor.ts +879 -0
- package/src/utils/python.ts +79 -0
- package/src/utils/runtime.ts +616 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codec utilities for transporting rich Python values over JSON
|
|
3
|
+
*
|
|
4
|
+
* Python bridge emits envelopes like:
|
|
5
|
+
* { "__tywrap__": "dataframe", "encoding": "arrow", "b64": "..." }
|
|
6
|
+
* Optionally with ndarray fallback: { "__tywrap__": "ndarray", "encoding": "json", ... }
|
|
7
|
+
* SciPy sparse: { "__tywrap__": "scipy.sparse", "encoding": "json", "format": "csr", ... }
|
|
8
|
+
* Torch tensors: { "__tywrap__": "torch.tensor", "encoding": "ndarray", "value": { ... } }
|
|
9
|
+
* Sklearn estimators: { "__tywrap__": "sklearn.estimator", "encoding": "json", ... }
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Avoid hard dependency on apache-arrow types at compile time to keep install optional.
|
|
13
|
+
export type ArrowTable = { readonly numCols?: number; readonly numRows?: number } & Record<
|
|
14
|
+
string,
|
|
15
|
+
unknown
|
|
16
|
+
>;
|
|
17
|
+
|
|
18
|
+
export interface SparseMatrix {
|
|
19
|
+
format: 'csr' | 'csc' | 'coo';
|
|
20
|
+
shape: readonly number[];
|
|
21
|
+
data: readonly unknown[];
|
|
22
|
+
indices?: readonly number[];
|
|
23
|
+
indptr?: readonly number[];
|
|
24
|
+
row?: readonly number[];
|
|
25
|
+
col?: readonly number[];
|
|
26
|
+
dtype?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TorchTensor {
|
|
30
|
+
data: unknown;
|
|
31
|
+
shape?: readonly number[];
|
|
32
|
+
dtype?: string;
|
|
33
|
+
device?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SklearnEstimator {
|
|
37
|
+
className: string;
|
|
38
|
+
module: string;
|
|
39
|
+
version?: string;
|
|
40
|
+
params: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type CodecEnvelope =
|
|
44
|
+
| {
|
|
45
|
+
readonly __tywrap__: 'dataframe';
|
|
46
|
+
readonly encoding: 'arrow';
|
|
47
|
+
readonly b64: string;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
readonly __tywrap__: 'series';
|
|
51
|
+
readonly encoding: 'arrow' | 'json';
|
|
52
|
+
readonly b64?: string;
|
|
53
|
+
readonly data?: unknown;
|
|
54
|
+
readonly name?: string | null;
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
readonly __tywrap__: 'ndarray';
|
|
58
|
+
readonly encoding: 'arrow' | 'json';
|
|
59
|
+
readonly b64?: string; // when encoding=arrow
|
|
60
|
+
readonly data?: unknown; // when encoding=json
|
|
61
|
+
readonly shape?: readonly number[];
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
readonly __tywrap__: 'scipy.sparse';
|
|
65
|
+
readonly encoding: 'json';
|
|
66
|
+
readonly format: 'csr' | 'csc' | 'coo';
|
|
67
|
+
readonly shape: readonly number[];
|
|
68
|
+
readonly data: readonly unknown[];
|
|
69
|
+
readonly indices?: readonly number[];
|
|
70
|
+
readonly indptr?: readonly number[];
|
|
71
|
+
readonly row?: readonly number[];
|
|
72
|
+
readonly col?: readonly number[];
|
|
73
|
+
readonly dtype?: string;
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
readonly __tywrap__: 'torch.tensor';
|
|
77
|
+
readonly encoding: 'ndarray';
|
|
78
|
+
readonly value: unknown;
|
|
79
|
+
readonly shape?: readonly number[];
|
|
80
|
+
readonly dtype?: string;
|
|
81
|
+
readonly device?: string;
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
readonly __tywrap__: 'sklearn.estimator';
|
|
85
|
+
readonly encoding: 'json';
|
|
86
|
+
readonly className: string;
|
|
87
|
+
readonly module: string;
|
|
88
|
+
readonly version?: string;
|
|
89
|
+
readonly params: Record<string, unknown>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type DecodedValue = ArrowTable | Uint8Array | SparseMatrix | TorchTensor | SklearnEstimator | unknown;
|
|
93
|
+
|
|
94
|
+
let arrowTableFrom: ((bytes: Uint8Array) => ArrowTable | Uint8Array) | undefined;
|
|
95
|
+
|
|
96
|
+
export function registerArrowDecoder(
|
|
97
|
+
decoder: (bytes: Uint8Array) => ArrowTable | Uint8Array
|
|
98
|
+
): void {
|
|
99
|
+
arrowTableFrom = decoder;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function clearArrowDecoder(): void {
|
|
103
|
+
arrowTableFrom = undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether an Arrow decoder has been registered for this process.
|
|
108
|
+
*/
|
|
109
|
+
export function hasArrowDecoder(): boolean {
|
|
110
|
+
return typeof arrowTableFrom === 'function';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isObject(value: unknown): value is { [k: string]: unknown } {
|
|
114
|
+
return typeof value === 'object' && value !== null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function fromBase64(b64: string): Uint8Array {
|
|
118
|
+
if (typeof Buffer !== 'undefined') {
|
|
119
|
+
const buf = Buffer.from(b64, 'base64');
|
|
120
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.length);
|
|
121
|
+
}
|
|
122
|
+
if (globalThis.atob) {
|
|
123
|
+
const bin = globalThis.atob(b64);
|
|
124
|
+
const arr = Array.from(bin, c => c.charCodeAt(0));
|
|
125
|
+
return new Uint8Array(arr);
|
|
126
|
+
}
|
|
127
|
+
throw new Error('Base64 decoding is not available in this runtime');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function requireArrowDecoder(): (bytes: Uint8Array) => ArrowTable | Uint8Array {
|
|
131
|
+
if (!arrowTableFrom) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'Arrow decoder not registered. Call registerArrowDecoder(...) or set TYWRAP_CODEC_FALLBACK=json in Python.'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return arrowTableFrom;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function tryDecodeArrowTable(bytes: Uint8Array): Promise<ArrowTable | Uint8Array> {
|
|
140
|
+
const decoder = requireArrowDecoder();
|
|
141
|
+
try {
|
|
142
|
+
return decoder(bytes);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
throw new Error(`Arrow decode failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function decodeEnvelope<T>(value: unknown, decodeArrow: (bytes: Uint8Array) => T): T | unknown {
|
|
149
|
+
if (!isObject(value)) {
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
const marker = (value as { __tywrap__?: unknown }).__tywrap__;
|
|
153
|
+
if (
|
|
154
|
+
(marker === 'dataframe' || marker === 'series') &&
|
|
155
|
+
(value as { encoding?: unknown }).encoding === 'arrow' &&
|
|
156
|
+
typeof (value as { b64?: unknown }).b64 === 'string'
|
|
157
|
+
) {
|
|
158
|
+
const bytes = fromBase64(String((value as { b64: string }).b64));
|
|
159
|
+
return decodeArrow(bytes);
|
|
160
|
+
}
|
|
161
|
+
if (
|
|
162
|
+
marker === 'dataframe' &&
|
|
163
|
+
(value as { encoding?: unknown }).encoding === 'json' &&
|
|
164
|
+
'data' in (value as object)
|
|
165
|
+
) {
|
|
166
|
+
return (value as { data: unknown }).data;
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
marker === 'series' &&
|
|
170
|
+
(value as { encoding?: unknown }).encoding === 'json' &&
|
|
171
|
+
'data' in (value as object)
|
|
172
|
+
) {
|
|
173
|
+
return (value as { data: unknown }).data;
|
|
174
|
+
}
|
|
175
|
+
if (marker === 'ndarray') {
|
|
176
|
+
if (
|
|
177
|
+
(value as { encoding?: unknown }).encoding === 'arrow' &&
|
|
178
|
+
typeof (value as { b64?: unknown }).b64 === 'string'
|
|
179
|
+
) {
|
|
180
|
+
const bytes = fromBase64(String((value as { b64: string }).b64));
|
|
181
|
+
return decodeArrow(bytes);
|
|
182
|
+
}
|
|
183
|
+
if ((value as { encoding?: unknown }).encoding === 'json' && 'data' in (value as object)) {
|
|
184
|
+
return (value as { data: unknown }).data;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (
|
|
188
|
+
marker === 'scipy.sparse' &&
|
|
189
|
+
(value as { encoding?: unknown }).encoding === 'json' &&
|
|
190
|
+
typeof (value as { format?: unknown }).format === 'string' &&
|
|
191
|
+
Array.isArray((value as { shape?: unknown }).shape) &&
|
|
192
|
+
Array.isArray((value as { data?: unknown }).data)
|
|
193
|
+
) {
|
|
194
|
+
const sparse = value as {
|
|
195
|
+
format: 'csr' | 'csc' | 'coo';
|
|
196
|
+
shape: readonly number[];
|
|
197
|
+
data: readonly unknown[];
|
|
198
|
+
indices?: readonly number[];
|
|
199
|
+
indptr?: readonly number[];
|
|
200
|
+
row?: readonly number[];
|
|
201
|
+
col?: readonly number[];
|
|
202
|
+
dtype?: string;
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
format: sparse.format,
|
|
206
|
+
shape: sparse.shape,
|
|
207
|
+
data: sparse.data,
|
|
208
|
+
indices: sparse.indices,
|
|
209
|
+
indptr: sparse.indptr,
|
|
210
|
+
row: sparse.row,
|
|
211
|
+
col: sparse.col,
|
|
212
|
+
dtype: sparse.dtype,
|
|
213
|
+
} satisfies SparseMatrix;
|
|
214
|
+
}
|
|
215
|
+
if (marker === 'torch.tensor' && (value as { encoding?: unknown }).encoding === 'ndarray') {
|
|
216
|
+
const torchValue = value as {
|
|
217
|
+
value?: unknown;
|
|
218
|
+
shape?: readonly number[];
|
|
219
|
+
dtype?: string;
|
|
220
|
+
device?: string;
|
|
221
|
+
};
|
|
222
|
+
if ('value' in (torchValue as object)) {
|
|
223
|
+
const decoded = decodeEnvelope(torchValue.value, decodeArrow);
|
|
224
|
+
return {
|
|
225
|
+
data: decoded,
|
|
226
|
+
shape: torchValue.shape,
|
|
227
|
+
dtype: torchValue.dtype,
|
|
228
|
+
device: torchValue.device,
|
|
229
|
+
} satisfies TorchTensor;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (
|
|
233
|
+
marker === 'sklearn.estimator' &&
|
|
234
|
+
(value as { encoding?: unknown }).encoding === 'json' &&
|
|
235
|
+
typeof (value as { className?: unknown }).className === 'string' &&
|
|
236
|
+
typeof (value as { module?: unknown }).module === 'string' &&
|
|
237
|
+
isObject((value as { params?: unknown }).params)
|
|
238
|
+
) {
|
|
239
|
+
const estimator = value as {
|
|
240
|
+
className: string;
|
|
241
|
+
module: string;
|
|
242
|
+
version?: string;
|
|
243
|
+
params: Record<string, unknown>;
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
className: estimator.className,
|
|
247
|
+
module: estimator.module,
|
|
248
|
+
version: estimator.version,
|
|
249
|
+
params: estimator.params,
|
|
250
|
+
} satisfies SklearnEstimator;
|
|
251
|
+
}
|
|
252
|
+
return value as unknown;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Decode values produced by the Python bridge.
|
|
257
|
+
*/
|
|
258
|
+
export async function decodeValueAsync(value: unknown): Promise<DecodedValue> {
|
|
259
|
+
return await decodeEnvelope(value, tryDecodeArrowTable);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Synchronous decode. Arrow decoding requires a registered decoder.
|
|
264
|
+
*/
|
|
265
|
+
export function decodeValue(value: unknown): DecodedValue {
|
|
266
|
+
const decodeArrow = (bytes: Uint8Array): DecodedValue => {
|
|
267
|
+
const decoder = requireArrowDecoder();
|
|
268
|
+
try {
|
|
269
|
+
return decoder(bytes);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
throw new Error(`Arrow decode failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
return decodeEnvelope(value, decodeArrow);
|
|
275
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Logger for tywrap
|
|
3
|
+
* Provides log levels, optional JSON output, and component context
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'SILENT';
|
|
7
|
+
|
|
8
|
+
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
9
|
+
DEBUG: 0,
|
|
10
|
+
INFO: 1,
|
|
11
|
+
WARN: 2,
|
|
12
|
+
ERROR: 3,
|
|
13
|
+
SILENT: 4,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface LogContext {
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LoggerOptions {
|
|
21
|
+
/** Minimum log level to output. Default: 'WARN' */
|
|
22
|
+
level?: LogLevel;
|
|
23
|
+
/** Component name for context (e.g., 'Cache', 'Worker'). */
|
|
24
|
+
component?: string;
|
|
25
|
+
/** Output logs as JSON. Default: false */
|
|
26
|
+
jsonOutput?: boolean;
|
|
27
|
+
/** Enable/disable logging. Default: true */
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
/** Output stream. Default: process.stderr */
|
|
30
|
+
output?: NodeJS.WritableStream;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Logger {
|
|
34
|
+
debug(message: string, context?: LogContext): void;
|
|
35
|
+
info(message: string, context?: LogContext): void;
|
|
36
|
+
warn(message: string, context?: LogContext): void;
|
|
37
|
+
error(message: string, context?: LogContext): void;
|
|
38
|
+
/** Create a child logger with a component prefix */
|
|
39
|
+
child(component: string): Logger;
|
|
40
|
+
/** Check if a log level is enabled */
|
|
41
|
+
isLevelEnabled(level: LogLevel): boolean;
|
|
42
|
+
/** Update logger options */
|
|
43
|
+
configure(options: Partial<LoggerOptions>): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface LogEntry {
|
|
47
|
+
timestamp: string;
|
|
48
|
+
level: LogLevel;
|
|
49
|
+
component?: string;
|
|
50
|
+
message: string;
|
|
51
|
+
context?: LogContext;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getEnvLogLevel(): LogLevel | undefined {
|
|
55
|
+
if (typeof process === 'undefined' || !process.env) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const envLevel = process.env.TYWRAP_LOG_LEVEL?.toUpperCase();
|
|
59
|
+
if (envLevel && envLevel in LOG_LEVEL_PRIORITY) {
|
|
60
|
+
return envLevel as LogLevel;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getEnvJsonOutput(): boolean {
|
|
66
|
+
if (typeof process === 'undefined' || !process.env) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return process.env.TYWRAP_LOG_JSON === 'true' || process.env.TYWRAP_LOG_JSON === '1';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatHumanReadable(entry: LogEntry): string {
|
|
73
|
+
const componentPart = entry.component ? ` [${entry.component}]` : '';
|
|
74
|
+
const contextPart = entry.context && Object.keys(entry.context).length > 0
|
|
75
|
+
? ` ${JSON.stringify(entry.context)}`
|
|
76
|
+
: '';
|
|
77
|
+
return `${entry.timestamp} [${entry.level}]${componentPart} ${entry.message}${contextPart}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatJson(entry: LogEntry): string {
|
|
81
|
+
const output: Record<string, unknown> = {
|
|
82
|
+
ts: entry.timestamp,
|
|
83
|
+
level: entry.level,
|
|
84
|
+
};
|
|
85
|
+
if (entry.component) {
|
|
86
|
+
output.component = entry.component;
|
|
87
|
+
}
|
|
88
|
+
output.msg = entry.message;
|
|
89
|
+
if (entry.context && Object.keys(entry.context).length > 0) {
|
|
90
|
+
Object.assign(output, entry.context);
|
|
91
|
+
}
|
|
92
|
+
return JSON.stringify(output);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class LoggerImpl implements Logger {
|
|
96
|
+
private level: LogLevel;
|
|
97
|
+
private component?: string;
|
|
98
|
+
private jsonOutput: boolean;
|
|
99
|
+
private enabled: boolean;
|
|
100
|
+
private output: NodeJS.WritableStream;
|
|
101
|
+
|
|
102
|
+
constructor(options: LoggerOptions = {}) {
|
|
103
|
+
this.level = options.level ?? getEnvLogLevel() ?? 'WARN';
|
|
104
|
+
this.component = options.component;
|
|
105
|
+
this.jsonOutput = options.jsonOutput ?? getEnvJsonOutput();
|
|
106
|
+
this.enabled = options.enabled ?? true;
|
|
107
|
+
this.output = options.output ?? (typeof process !== 'undefined' ? process.stderr : undefined as unknown as NodeJS.WritableStream);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
configure(options: Partial<LoggerOptions>): void {
|
|
111
|
+
if (options.level !== undefined) {
|
|
112
|
+
this.level = options.level;
|
|
113
|
+
}
|
|
114
|
+
if (options.component !== undefined) {
|
|
115
|
+
this.component = options.component;
|
|
116
|
+
}
|
|
117
|
+
if (options.jsonOutput !== undefined) {
|
|
118
|
+
this.jsonOutput = options.jsonOutput;
|
|
119
|
+
}
|
|
120
|
+
if (options.enabled !== undefined) {
|
|
121
|
+
this.enabled = options.enabled;
|
|
122
|
+
}
|
|
123
|
+
if (options.output !== undefined) {
|
|
124
|
+
this.output = options.output;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
isLevelEnabled(level: LogLevel): boolean {
|
|
129
|
+
if (!this.enabled) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.level];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private log(level: LogLevel, message: string, context?: LogContext): void {
|
|
136
|
+
// Early exit for performance - avoid string operations when disabled
|
|
137
|
+
if (!this.isLevelEnabled(level)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const entry: LogEntry = {
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
level,
|
|
144
|
+
component: this.component,
|
|
145
|
+
message,
|
|
146
|
+
context,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const formatted = this.jsonOutput ? formatJson(entry) : formatHumanReadable(entry);
|
|
150
|
+
|
|
151
|
+
if (this.output) {
|
|
152
|
+
this.output.write(`${formatted}\n`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
debug(message: string, context?: LogContext): void {
|
|
157
|
+
this.log('DEBUG', message, context);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
info(message: string, context?: LogContext): void {
|
|
161
|
+
this.log('INFO', message, context);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
warn(message: string, context?: LogContext): void {
|
|
165
|
+
this.log('WARN', message, context);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
error(message: string, context?: LogContext): void {
|
|
169
|
+
this.log('ERROR', message, context);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
child(component: string): Logger {
|
|
173
|
+
const childComponent = this.component
|
|
174
|
+
? `${this.component}:${component}`
|
|
175
|
+
: component;
|
|
176
|
+
|
|
177
|
+
return new LoggerImpl({
|
|
178
|
+
level: this.level,
|
|
179
|
+
component: childComponent,
|
|
180
|
+
jsonOutput: this.jsonOutput,
|
|
181
|
+
enabled: this.enabled,
|
|
182
|
+
output: this.output,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Global logger instance */
|
|
188
|
+
export const logger: Logger = new LoggerImpl();
|
|
189
|
+
|
|
190
|
+
/** Create a new logger with custom options */
|
|
191
|
+
export function createLogger(options: LoggerOptions = {}): Logger {
|
|
192
|
+
return new LoggerImpl(options);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create component-specific loggers
|
|
197
|
+
* Usage: const log = getComponentLogger('Cache');
|
|
198
|
+
*/
|
|
199
|
+
export function getComponentLogger(component: string): Logger {
|
|
200
|
+
return logger.child(component);
|
|
201
|
+
}
|