tywrap 0.1.1 → 0.2.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.
Files changed (63) hide show
  1. package/README.md +32 -5
  2. package/dist/cli.js +70 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +47 -11
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/core/generator.d.ts.map +1 -1
  8. package/dist/core/generator.js +6 -2
  9. package/dist/core/generator.js.map +1 -1
  10. package/dist/core/mapper.d.ts.map +1 -1
  11. package/dist/core/mapper.js +16 -4
  12. package/dist/core/mapper.js.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/runtime/bridge-core.d.ts +64 -0
  18. package/dist/runtime/bridge-core.d.ts.map +1 -0
  19. package/dist/runtime/bridge-core.js +344 -0
  20. package/dist/runtime/bridge-core.js.map +1 -0
  21. package/dist/runtime/node.d.ts +161 -15
  22. package/dist/runtime/node.d.ts.map +1 -1
  23. package/dist/runtime/node.js +542 -218
  24. package/dist/runtime/node.js.map +1 -1
  25. package/dist/runtime/optimized-node.d.ts +19 -128
  26. package/dist/runtime/optimized-node.d.ts.map +1 -1
  27. package/dist/runtime/optimized-node.js +19 -627
  28. package/dist/runtime/optimized-node.js.map +1 -1
  29. package/dist/runtime/timed-out-request-tracker.d.ts +20 -0
  30. package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -0
  31. package/dist/runtime/timed-out-request-tracker.js +48 -0
  32. package/dist/runtime/timed-out-request-tracker.js.map +1 -0
  33. package/dist/types/index.d.ts +3 -0
  34. package/dist/types/index.d.ts.map +1 -1
  35. package/dist/tywrap.d.ts +17 -4
  36. package/dist/tywrap.d.ts.map +1 -1
  37. package/dist/tywrap.js +68 -35
  38. package/dist/tywrap.js.map +1 -1
  39. package/dist/utils/codec.d.ts +22 -0
  40. package/dist/utils/codec.d.ts.map +1 -1
  41. package/dist/utils/codec.js +245 -55
  42. package/dist/utils/codec.js.map +1 -1
  43. package/dist/utils/logger.d.ts.map +1 -1
  44. package/dist/utils/logger.js +1 -0
  45. package/dist/utils/logger.js.map +1 -1
  46. package/dist/utils/python.d.ts.map +1 -1
  47. package/dist/utils/python.js.map +1 -1
  48. package/package.json +11 -2
  49. package/runtime/python_bridge.py +450 -69
  50. package/src/cli.ts +75 -3
  51. package/src/config/index.ts +58 -15
  52. package/src/core/generator.ts +6 -2
  53. package/src/core/mapper.ts +16 -4
  54. package/src/index.ts +2 -1
  55. package/src/runtime/bridge-core.ts +454 -0
  56. package/src/runtime/node.ts +725 -253
  57. package/src/runtime/optimized-node.ts +19 -844
  58. package/src/runtime/timed-out-request-tracker.ts +58 -0
  59. package/src/types/index.ts +3 -0
  60. package/src/tywrap.ts +91 -36
  61. package/src/utils/codec.ts +299 -82
  62. package/src/utils/logger.ts +1 -0
  63. package/src/utils/python.ts +0 -1
@@ -0,0 +1,58 @@
1
+ export interface TimedOutRequestTrackerOptions {
2
+ ttlMs: number;
3
+ maxSize?: number;
4
+ }
5
+
6
+ /**
7
+ * Track request IDs that timed out on the JS side so late Python responses can be
8
+ * safely ignored instead of treated as protocol errors.
9
+ */
10
+ export class TimedOutRequestTracker {
11
+ private readonly ttlMs: number;
12
+ private readonly maxSize: number;
13
+ private readonly items = new Map<number, number>();
14
+
15
+ constructor(options: TimedOutRequestTrackerOptions) {
16
+ this.ttlMs = options.ttlMs;
17
+ this.maxSize = options.maxSize ?? 1000;
18
+ }
19
+
20
+ clear(): void {
21
+ this.items.clear();
22
+ }
23
+
24
+ mark(id: number): void {
25
+ const now = Date.now();
26
+ this.pruneOld(now);
27
+ this.items.set(id, now);
28
+ this.pruneMax();
29
+ }
30
+
31
+ consume(id: number): boolean {
32
+ if (!this.items.has(id)) {
33
+ return false;
34
+ }
35
+ this.items.delete(id);
36
+ return true;
37
+ }
38
+
39
+ private pruneOld(now: number): void {
40
+ const cutoff = now - this.ttlMs;
41
+ for (const [key, ts] of this.items) {
42
+ if (ts >= cutoff) {
43
+ break;
44
+ }
45
+ this.items.delete(key);
46
+ }
47
+ }
48
+
49
+ private pruneMax(): void {
50
+ while (this.items.size > this.maxSize) {
51
+ const oldest = this.items.keys().next();
52
+ if (oldest.done) {
53
+ break;
54
+ }
55
+ this.items.delete(oldest.value);
56
+ }
57
+ }
58
+ }
@@ -317,6 +317,9 @@ export interface BridgeInfo {
317
317
  pid: number;
318
318
  codecFallback: 'json' | 'none';
319
319
  arrowAvailable: boolean;
320
+ scipyAvailable: boolean;
321
+ torchAvailable: boolean;
322
+ sklearnAvailable: boolean;
320
323
  instances: number;
321
324
  }
322
325
 
package/src/tywrap.ts CHANGED
@@ -72,16 +72,50 @@ expectType<Record<string, unknown>>(mod);
72
72
  await writeFile(filePath, content, 'utf-8');
73
73
  }
74
74
 
75
+ export interface GenerateRunOptions {
76
+ /**
77
+ * If true, do not write files; instead compare generated output to what's on disk.
78
+ * Intended for CI to ensure wrappers are checked in and up to date.
79
+ */
80
+ check?: boolean;
81
+ }
82
+
83
+ export interface GenerateResult {
84
+ written: string[];
85
+ warnings: string[];
86
+ /**
87
+ * Only set when `GenerateRunOptions.check === true`.
88
+ * Lists files that are missing or differ from what would be generated.
89
+ */
90
+ outOfDate?: string[];
91
+ }
92
+
93
+ function normalizeForComparison(text: string): string {
94
+ // Avoid false positives from CRLF vs LF (e.g. Windows checkouts)
95
+ return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
96
+ }
97
+
98
+ async function safeReadFile(path: string): Promise<string | null> {
99
+ try {
100
+ return await fsUtils.readFile(path);
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
75
106
  /**
76
107
  * Generate TypeScript wrappers for configured Python modules.
77
108
  * Minimal MVP implementation: resolves module file, analyzes, generates TS, writes to output.dir
78
109
  */
79
110
  export async function generate(
80
- options: Partial<TywrapOptions>
81
- ): Promise<{ written: string[]; warnings: string[] }> {
111
+ options: Partial<TywrapOptions>,
112
+ runOptions: GenerateRunOptions = {}
113
+ ): Promise<GenerateResult> {
114
+ const checkMode = runOptions.check === true;
82
115
  const resolvedOptions = createConfig(options);
83
116
  const instance = await tywrap(resolvedOptions);
84
117
  const written: string[] = [];
118
+ const outOfDate: string[] = [];
85
119
  const warnings: string[] = [];
86
120
  const outputDir = resolvedOptions.output.dir;
87
121
  const caching = resolvedOptions.performance.caching;
@@ -92,11 +126,13 @@ export async function generate(
92
126
  });
93
127
 
94
128
  // Ensure directory exists (Node-only best-effort)
95
- try {
96
- const modFs = await import('fs/promises');
97
- await modFs.mkdir(outputDir, { recursive: true });
98
- } catch {
99
- // ignore in non-node or if already exists
129
+ if (!checkMode) {
130
+ try {
131
+ const modFs = await import('fs/promises');
132
+ await modFs.mkdir(outputDir, { recursive: true });
133
+ } catch {
134
+ // ignore in non-node or if already exists
135
+ }
100
136
  }
101
137
 
102
138
  const modules = resolvedOptions.pythonModules ?? {};
@@ -109,10 +145,6 @@ export async function generate(
109
145
  let ir: unknown | null = null;
110
146
  let irError: string | undefined;
111
147
  if (caching && fsUtils.isAvailable()) {
112
- try {
113
- const modFs = await import('fs/promises');
114
- await modFs.mkdir(cacheDir, { recursive: true });
115
- } catch {}
116
148
  try {
117
149
  const cached = await fsUtils.readFile(pathUtils.join(cacheDir, cacheKey));
118
150
  ir = JSON.parse(cached);
@@ -124,7 +156,7 @@ export async function generate(
124
156
  const fetchResult = await fetchPythonIr(moduleKey, pythonPath);
125
157
  ir = fetchResult.ir;
126
158
  irError = fetchResult.error;
127
- if (ir && caching && fsUtils.isAvailable()) {
159
+ if (ir && caching && fsUtils.isAvailable() && !checkMode) {
128
160
  try {
129
161
  await fsUtils.writeFile(pathUtils.join(cacheDir, cacheKey), JSON.stringify(ir));
130
162
  } catch {}
@@ -141,26 +173,43 @@ export async function generate(
141
173
  const annotatedJSDoc = Boolean(resolvedOptions.output?.annotatedJSDoc);
142
174
  const gen = instance.generator.generateModuleDefinition(moduleModel, annotatedJSDoc);
143
175
 
144
- // Write file
145
176
  const baseName = moduleModel.name || 'module';
146
- const outPath = pathUtils.join(outputDir, `${baseName}.generated.ts`);
147
- await fsUtils.writeFile(outPath, gen.typescript);
148
- written.push(outPath);
177
+ const filesToEmit: Array<{ path: string; content: string }> = [
178
+ { path: pathUtils.join(outputDir, `${baseName}.generated.ts`), content: gen.typescript },
179
+ ];
149
180
 
150
181
  // Optional .d.ts emission (header-only declarations mirroring exports)
151
182
  if (resolvedOptions.output?.declaration) {
152
- const dtsPath = pathUtils.join(outputDir, `${baseName}.generated.d.ts`);
153
- const dts = renderDts(gen.typescript);
154
- await fsUtils.writeFile(dtsPath, dts);
155
- written.push(dtsPath);
183
+ filesToEmit.push({
184
+ path: pathUtils.join(outputDir, `${baseName}.generated.d.ts`),
185
+ content: renderDts(gen.typescript),
186
+ });
156
187
  }
157
188
 
158
189
  // Optional source map emission (placeholder mapping for now)
159
190
  if (resolvedOptions.output?.sourceMap) {
160
- const mapPath = pathUtils.join(outputDir, `${baseName}.generated.ts.map`);
161
- const map = renderSourceMapPlaceholder(moduleModel.name);
162
- await fsUtils.writeFile(mapPath, map);
163
- written.push(mapPath);
191
+ filesToEmit.push({
192
+ path: pathUtils.join(outputDir, `${baseName}.generated.ts.map`),
193
+ content: renderSourceMapPlaceholder(moduleModel.name),
194
+ });
195
+ }
196
+
197
+ if (checkMode) {
198
+ for (const file of filesToEmit) {
199
+ const existing = await safeReadFile(file.path);
200
+ if (existing === null) {
201
+ outOfDate.push(file.path);
202
+ continue;
203
+ }
204
+ if (normalizeForComparison(existing) !== normalizeForComparison(file.content)) {
205
+ outOfDate.push(file.path);
206
+ }
207
+ }
208
+ } else {
209
+ for (const file of filesToEmit) {
210
+ await fsUtils.writeFile(file.path, file.content);
211
+ written.push(file.path);
212
+ }
164
213
  }
165
214
 
166
215
  // Emit warning summary of unknown typing constructs (best-effort)
@@ -174,22 +223,28 @@ export async function generate(
174
223
  `Module ${baseName}: unknown typing constructs encountered: ${unkList}${entries.length > 25 ? '…' : ''}`
175
224
  );
176
225
  // Write JSON report
177
- try {
178
- const reportsDir = pathUtils.join('.tywrap', 'reports');
179
- await fsUtils.writeFile(
180
- pathUtils.join(reportsDir, `${baseName}.json`),
181
- JSON.stringify({
182
- module: baseName,
183
- unknowns: Object.fromEntries(entries),
184
- generatedAt: new Date().toISOString(),
185
- })
186
- );
187
- } catch {
188
- // ignore
226
+ if (!checkMode) {
227
+ try {
228
+ const reportsDir = pathUtils.join('.tywrap', 'reports');
229
+ await fsUtils.writeFile(
230
+ pathUtils.join(reportsDir, `${baseName}.json`),
231
+ JSON.stringify({
232
+ module: baseName,
233
+ unknowns: Object.fromEntries(entries),
234
+ generatedAt: new Date().toISOString(),
235
+ })
236
+ );
237
+ } catch {
238
+ // ignore
239
+ }
189
240
  }
190
241
  }
191
242
  }
192
243
 
244
+ if (checkMode) {
245
+ return { written: [], warnings, outOfDate };
246
+ }
247
+
193
248
  return { written, warnings };
194
249
  }
195
250