tywrap 0.6.0 → 0.7.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 (118) hide show
  1. package/dist/core/annotation-parser.d.ts.map +1 -1
  2. package/dist/core/annotation-parser.js +19 -14
  3. package/dist/core/annotation-parser.js.map +1 -1
  4. package/dist/core/discovery.d.ts +17 -0
  5. package/dist/core/discovery.d.ts.map +1 -1
  6. package/dist/core/discovery.js +59 -49
  7. package/dist/core/discovery.js.map +1 -1
  8. package/dist/core/generator.d.ts.map +1 -1
  9. package/dist/core/generator.js +40 -9
  10. package/dist/core/generator.js.map +1 -1
  11. package/dist/core/validation.d.ts +23 -0
  12. package/dist/core/validation.d.ts.map +1 -1
  13. package/dist/core/validation.js +52 -48
  14. package/dist/core/validation.js.map +1 -1
  15. package/dist/dev.d.ts.map +1 -1
  16. package/dist/dev.js +175 -133
  17. package/dist/dev.js.map +1 -1
  18. package/dist/runtime/base-bridge.d.ts +57 -0
  19. package/dist/runtime/base-bridge.d.ts.map +1 -0
  20. package/dist/runtime/base-bridge.js +72 -0
  21. package/dist/runtime/base-bridge.js.map +1 -0
  22. package/dist/runtime/bridge-codec.d.ts +15 -0
  23. package/dist/runtime/bridge-codec.d.ts.map +1 -1
  24. package/dist/runtime/bridge-codec.js +45 -48
  25. package/dist/runtime/bridge-codec.js.map +1 -1
  26. package/dist/runtime/http-transport.d.ts +11 -1
  27. package/dist/runtime/http-transport.d.ts.map +1 -1
  28. package/dist/runtime/http-transport.js +19 -0
  29. package/dist/runtime/http-transport.js.map +1 -1
  30. package/dist/runtime/http.d.ts +5 -12
  31. package/dist/runtime/http.d.ts.map +1 -1
  32. package/dist/runtime/http.js +6 -29
  33. package/dist/runtime/http.js.map +1 -1
  34. package/dist/runtime/index.d.ts +1 -1
  35. package/dist/runtime/index.d.ts.map +1 -1
  36. package/dist/runtime/index.js.map +1 -1
  37. package/dist/runtime/node.d.ts +12 -19
  38. package/dist/runtime/node.d.ts.map +1 -1
  39. package/dist/runtime/node.js +14 -34
  40. package/dist/runtime/node.js.map +1 -1
  41. package/dist/runtime/pooled-transport.d.ts +21 -2
  42. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  43. package/dist/runtime/pooled-transport.js +16 -0
  44. package/dist/runtime/pooled-transport.js.map +1 -1
  45. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  46. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  47. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  48. package/dist/runtime/pyodide-transport.d.ts +12 -1
  49. package/dist/runtime/pyodide-transport.d.ts.map +1 -1
  50. package/dist/runtime/pyodide-transport.js +20 -0
  51. package/dist/runtime/pyodide-transport.js.map +1 -1
  52. package/dist/runtime/pyodide.d.ts +5 -12
  53. package/dist/runtime/pyodide.d.ts.map +1 -1
  54. package/dist/runtime/pyodide.js +6 -29
  55. package/dist/runtime/pyodide.js.map +1 -1
  56. package/dist/runtime/rpc-client.d.ts +32 -1
  57. package/dist/runtime/rpc-client.d.ts.map +1 -1
  58. package/dist/runtime/rpc-client.js +45 -24
  59. package/dist/runtime/rpc-client.js.map +1 -1
  60. package/dist/runtime/subprocess-transport.d.ts +26 -1
  61. package/dist/runtime/subprocess-transport.d.ts.map +1 -1
  62. package/dist/runtime/subprocess-transport.js +72 -35
  63. package/dist/runtime/subprocess-transport.js.map +1 -1
  64. package/dist/runtime/transport.d.ts +59 -0
  65. package/dist/runtime/transport.d.ts.map +1 -1
  66. package/dist/runtime/transport.js +1 -0
  67. package/dist/runtime/transport.js.map +1 -1
  68. package/dist/types/index.d.ts +35 -0
  69. package/dist/types/index.d.ts.map +1 -1
  70. package/dist/tywrap.d.ts +0 -5
  71. package/dist/tywrap.d.ts.map +1 -1
  72. package/dist/tywrap.js +212 -169
  73. package/dist/tywrap.js.map +1 -1
  74. package/dist/utils/cache.d.ts +11 -0
  75. package/dist/utils/cache.d.ts.map +1 -1
  76. package/dist/utils/cache.js +50 -58
  77. package/dist/utils/cache.js.map +1 -1
  78. package/dist/utils/codec.d.ts +2 -0
  79. package/dist/utils/codec.d.ts.map +1 -1
  80. package/dist/utils/codec.js +53 -2
  81. package/dist/utils/codec.js.map +1 -1
  82. package/dist/utils/python.d.ts.map +1 -1
  83. package/dist/utils/python.js +28 -17
  84. package/dist/utils/python.js.map +1 -1
  85. package/dist/utils/runtime.d.ts.map +1 -1
  86. package/dist/utils/runtime.js +22 -16
  87. package/dist/utils/runtime.js.map +1 -1
  88. package/dist/version.js +1 -1
  89. package/package.json +7 -1
  90. package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
  91. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  92. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  93. package/runtime/tywrap_bridge_core.py +55 -3
  94. package/src/core/annotation-parser.ts +23 -18
  95. package/src/core/discovery.ts +70 -54
  96. package/src/core/generator.ts +50 -11
  97. package/src/core/validation.ts +84 -48
  98. package/src/dev.ts +237 -153
  99. package/src/runtime/base-bridge.ts +106 -0
  100. package/src/runtime/bridge-codec.ts +58 -70
  101. package/src/runtime/http-transport.ts +21 -1
  102. package/src/runtime/http.ts +7 -51
  103. package/src/runtime/index.ts +1 -0
  104. package/src/runtime/node.ts +17 -52
  105. package/src/runtime/pooled-transport.ts +25 -2
  106. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  107. package/src/runtime/pyodide-transport.ts +22 -0
  108. package/src/runtime/pyodide.ts +7 -52
  109. package/src/runtime/rpc-client.ts +56 -32
  110. package/src/runtime/subprocess-transport.ts +89 -40
  111. package/src/runtime/transport.ts +71 -0
  112. package/src/types/index.ts +37 -0
  113. package/src/tywrap.ts +274 -187
  114. package/src/utils/cache.ts +59 -61
  115. package/src/utils/codec.ts +61 -4
  116. package/src/utils/python.ts +33 -20
  117. package/src/utils/runtime.ts +24 -13
  118. package/src/version.ts +1 -1
@@ -111,6 +111,24 @@ export type DecodedValue =
111
111
 
112
112
  let arrowTableFrom: ((bytes: Uint8Array) => ArrowTable | Uint8Array) | undefined;
113
113
 
114
+ // Why: lazy auto-registration (on first Arrow decode) imports apache-arrow at most
115
+ // once per process. We cache the in-flight/settled attempt so concurrent decodes
116
+ // share a single dynamic import, and a missing module is not re-probed on every call.
117
+ let lazyRegistration: Promise<boolean> | undefined;
118
+
119
+ // Why: the lazy decode path imports apache-arrow through the default Node loader, which
120
+ // is hard to simulate-as-absent in a test env where the dependency IS installed. This
121
+ // internal seam lets the unit suite exercise the "apache-arrow missing" clear-failure
122
+ // branch deterministically. It is intentionally NOT re-exported from the package root
123
+ // (see test/api_surface.test.ts) and is reset by clearArrowDecoder().
124
+ let lazyArrowLoaderOverride: ArrowModuleLoader | undefined;
125
+
126
+ /** @internal Test-only: override the loader used by lazy auto-registration. */
127
+ export const _setLazyArrowLoaderForTesting = (loader: ArrowModuleLoader | undefined): void => {
128
+ lazyArrowLoaderOverride = loader;
129
+ lazyRegistration = undefined;
130
+ };
131
+
114
132
  export function registerArrowDecoder(
115
133
  decoder: (bytes: Uint8Array) => ArrowTable | Uint8Array
116
134
  ): void {
@@ -119,6 +137,10 @@ export function registerArrowDecoder(
119
137
 
120
138
  export function clearArrowDecoder(): void {
121
139
  arrowTableFrom = undefined;
140
+ // Why: reset the cached import attempt so tests (and reload helpers) can exercise
141
+ // the auto-registration path again from a clean slate.
142
+ lazyRegistration = undefined;
143
+ lazyArrowLoaderOverride = undefined;
122
144
  }
123
145
 
124
146
  /**
@@ -186,6 +208,12 @@ export async function autoRegisterArrowDecoder(
186
208
  }
187
209
  try {
188
210
  const arrowModule = await loader();
211
+ // Another path may have registered a decoder while the import was in flight
212
+ // (e.g. an explicit registerArrowDecoder() during concurrent startup/reload).
213
+ // Don't clobber it — the explicit registration wins.
214
+ if (hasArrowDecoder()) {
215
+ return true;
216
+ }
189
217
  registerArrowDecoderFromModule(arrowModule as { tableFromIPC?: unknown });
190
218
  return true;
191
219
  } catch {
@@ -214,17 +242,46 @@ function fromBase64(b64: string): Uint8Array {
214
242
  throw new Error('Base64 decoding is not available in this runtime');
215
243
  }
216
244
 
245
+ // Why: a single, actionable message for both decode paths so users always know the two
246
+ // supported remedies — install the optional dependency, or opt into the lossy JSON fallback.
247
+ const ARROW_MISSING_MESSAGE =
248
+ 'Received an Arrow-encoded payload but no Arrow decoder is available. ' +
249
+ 'Install the optional dependency with `npm install apache-arrow`, or set ' +
250
+ 'TYWRAP_CODEC_FALLBACK=json on the Python side to receive JSON instead ' +
251
+ '(lossy for dtype/NA fidelity). tywrap never silently downgrades Arrow payloads.';
252
+
217
253
  function requireArrowDecoder(): (bytes: Uint8Array) => ArrowTable | Uint8Array {
218
254
  if (!arrowTableFrom) {
219
- throw new Error(
220
- 'Arrow decoder not registered. Call registerArrowDecoder(...) or set TYWRAP_CODEC_FALLBACK=json in Python.'
221
- );
255
+ throw new Error(ARROW_MISSING_MESSAGE);
256
+ }
257
+ return arrowTableFrom;
258
+ }
259
+
260
+ /**
261
+ * Ensure an Arrow decoder is registered, lazily importing apache-arrow on first use.
262
+ *
263
+ * Why: keep apache-arrow optional and zero-config. The first Arrow-encoded payload
264
+ * triggers a single best-effort dynamic import; if it succeeds the decoder is cached
265
+ * for the rest of the process. If apache-arrow is absent we throw a clear, actionable
266
+ * error rather than silently producing wrong data.
267
+ */
268
+ async function ensureArrowDecoder(): Promise<(bytes: Uint8Array) => ArrowTable | Uint8Array> {
269
+ if (arrowTableFrom) {
270
+ return arrowTableFrom;
271
+ }
272
+ // Reuse a single import attempt across concurrent decodes.
273
+ lazyRegistration ??= autoRegisterArrowDecoder(
274
+ lazyArrowLoaderOverride ? { loader: lazyArrowLoaderOverride } : {}
275
+ );
276
+ await lazyRegistration;
277
+ if (!arrowTableFrom) {
278
+ throw new Error(ARROW_MISSING_MESSAGE);
222
279
  }
223
280
  return arrowTableFrom;
224
281
  }
225
282
 
226
283
  async function tryDecodeArrowTable(bytes: Uint8Array): Promise<ArrowTable | Uint8Array> {
227
- const decoder = requireArrowDecoder();
284
+ const decoder = await ensureArrowDecoder();
228
285
  try {
229
286
  return decoder(bytes);
230
287
  } catch (err) {
@@ -44,30 +44,43 @@ export function getDefaultPythonPath(): string {
44
44
  return getPythonExecutableName();
45
45
  }
46
46
 
47
+ /**
48
+ * Whether the configured pythonPath is the default interpreter (or unset),
49
+ * meaning a virtual environment lookup should take precedence.
50
+ */
51
+ function usesDefaultPython(pythonPath: string | undefined): boolean {
52
+ return !pythonPath || pythonPath === 'python3' || pythonPath === 'python';
53
+ }
54
+
55
+ /**
56
+ * Resolve the Python executable inside a virtual environment, preferring the
57
+ * Node path module when available and falling back to cross-runtime pathUtils.
58
+ */
59
+ async function resolveVenvPython(virtualEnv: string, cwd: string): Promise<string> {
60
+ const binDir = getVenvBinDir();
61
+ const exe = getVenvPythonExe();
62
+ const pathMod = await loadNodePathModule();
63
+
64
+ if (pathMod) {
65
+ const venvRoot = pathMod.resolve(cwd, virtualEnv);
66
+ return pathMod.join(venvRoot, binDir, exe);
67
+ }
68
+
69
+ const venvRoot = isAbsolutePath(virtualEnv)
70
+ ? pathUtils.join(virtualEnv)
71
+ : pathUtils.join(cwd, virtualEnv);
72
+ return pathUtils.join(venvRoot, binDir, exe);
73
+ }
74
+
47
75
  export async function resolvePythonExecutable(options: PythonResolveOptions = {}): Promise<string> {
48
76
  const pythonPath = options.pythonPath?.trim();
49
77
  const virtualEnv = options.virtualEnv?.trim();
50
78
 
51
- if (virtualEnv) {
52
- const usesDefaultPython = !pythonPath || pythonPath === 'python3' || pythonPath === 'python';
53
- if (usesDefaultPython) {
54
- const cwd =
55
- options.cwd ??
56
- (typeof process !== 'undefined' && typeof process.cwd === 'function' ? process.cwd() : '.');
57
- const binDir = getVenvBinDir();
58
- const exe = getVenvPythonExe();
59
- const pathMod = await loadNodePathModule();
60
-
61
- if (pathMod) {
62
- const venvRoot = pathMod.resolve(cwd, virtualEnv);
63
- return pathMod.join(venvRoot, binDir, exe);
64
- }
65
-
66
- const venvRoot = isAbsolutePath(virtualEnv)
67
- ? pathUtils.join(virtualEnv)
68
- : pathUtils.join(cwd, virtualEnv);
69
- return pathUtils.join(venvRoot, binDir, exe);
70
- }
79
+ if (virtualEnv && usesDefaultPython(pythonPath)) {
80
+ const cwd =
81
+ options.cwd ??
82
+ (typeof process !== 'undefined' && typeof process.cwd === 'function' ? process.cwd() : '.');
83
+ return resolveVenvPython(virtualEnv, cwd);
71
84
  }
72
85
 
73
86
  if (pythonPath) {
@@ -229,26 +229,37 @@ async function loadPathModule(): Promise<PathModule | null> {
229
229
  return null;
230
230
  }
231
231
 
232
+ /**
233
+ * Apply a single path segment to the accumulated normalized segments,
234
+ * resolving '..' and dropping '.'/empty segments in place.
235
+ */
236
+ function applyPathSegment(normalized: string[], segment: string, isAbsolute: boolean): void {
237
+ if (segment === '.' || segment === '') {
238
+ return; // Skip current directory and empty segments
239
+ }
240
+
241
+ if (segment !== '..') {
242
+ normalized.push(segment);
243
+ return;
244
+ }
245
+
246
+ // '..' segment: go up one directory when possible.
247
+ if (normalized.length > 0 && normalized[normalized.length - 1] !== '..') {
248
+ normalized.pop();
249
+ } else if (!isAbsolute) {
250
+ normalized.push(segment); // Keep '..' if not absolute and at root
251
+ }
252
+ }
253
+
232
254
  /**
233
255
  * Normalize path by stripping '.' and resolving '..' components
234
256
  */
235
257
  function normalizePath(path: string): string {
236
258
  const isAbsolute = path.startsWith('/');
237
- const segments = path.split('/');
238
259
  const normalized: string[] = [];
239
260
 
240
- for (const segment of segments) {
241
- if (segment === '.' || segment === '') {
242
- continue; // Skip current directory and empty segments
243
- } else if (segment === '..') {
244
- if (normalized.length > 0 && normalized[normalized.length - 1] !== '..') {
245
- normalized.pop(); // Go up one directory
246
- } else if (!isAbsolute) {
247
- normalized.push(segment); // Keep '..' if not absolute and at root
248
- }
249
- } else {
250
- normalized.push(segment);
251
- }
261
+ for (const segment of path.split('/')) {
262
+ applyPathSegment(normalized, segment, isAbsolute);
252
263
  }
253
264
 
254
265
  const result = normalized.join('/');
package/src/version.ts CHANGED
@@ -9,4 +9,4 @@
9
9
  * Regenerate with: node scripts/generate-version.mjs
10
10
  */
11
11
 
12
- export const VERSION: string = "0.6.0";
12
+ export const VERSION: string = "0.7.0";