pyodide.worker.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { loadPyodide, type PyodideInterface } from 'pyodide';
  2. declare global {
  3. interface Window {
  4. stdout: string | null;
  5. stderr: string | null;
  6. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  7. result: any;
  8. pyodide: PyodideInterface;
  9. packages: string[];
  10. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  11. [key: string]: any;
  12. }
  13. }
  14. async function loadPyodideAndPackages(packages: string[] = []) {
  15. self.stdout = null;
  16. self.stderr = null;
  17. self.result = null;
  18. self.pyodide = await loadPyodide({
  19. indexURL: '/pyodide/',
  20. stdout: (text) => {
  21. console.log('Python output:', text);
  22. if (self.stdout) {
  23. self.stdout += `${text}\n`;
  24. } else {
  25. self.stdout = `${text}\n`;
  26. }
  27. },
  28. stderr: (text) => {
  29. console.log('An error occurred:', text);
  30. if (self.stderr) {
  31. self.stderr += `${text}\n`;
  32. } else {
  33. self.stderr = `${text}\n`;
  34. }
  35. },
  36. packages: ['micropip']
  37. });
  38. const mountDir = '/mnt';
  39. self.pyodide.FS.mkdirTree(mountDir);
  40. // self.pyodide.FS.mount(self.pyodide.FS.filesystems.IDBFS, {}, mountDir);
  41. // // Load persisted files from IndexedDB (Initial Sync)
  42. // await new Promise<void>((resolve, reject) => {
  43. // self.pyodide.FS.syncfs(true, (err) => {
  44. // if (err) {
  45. // console.error('Error syncing from IndexedDB:', err);
  46. // reject(err);
  47. // } else {
  48. // console.log('Successfully loaded from IndexedDB.');
  49. // resolve();
  50. // }
  51. // });
  52. // });
  53. const micropip = self.pyodide.pyimport('micropip');
  54. // await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json');
  55. await micropip.install(packages);
  56. }
  57. self.onmessage = async (event) => {
  58. const { id, code, ...context } = event.data;
  59. console.log(event.data);
  60. // The worker copies the context in its own "memory" (an object mapping name to values)
  61. for (const key of Object.keys(context)) {
  62. self[key] = context[key];
  63. }
  64. // make sure loading is done
  65. await loadPyodideAndPackages(self.packages);
  66. try {
  67. // check if matplotlib is imported in the code
  68. if (code.includes('matplotlib')) {
  69. // Override plt.show() to return base64 image
  70. await self.pyodide.runPythonAsync(`import base64
  71. import os
  72. from io import BytesIO
  73. # before importing matplotlib
  74. # to avoid the wasm backend (which needs js.document', not available in worker)
  75. os.environ["MPLBACKEND"] = "AGG"
  76. import matplotlib.pyplot
  77. _old_show = matplotlib.pyplot.show
  78. assert _old_show, "matplotlib.pyplot.show"
  79. def show(*, block=None):
  80. buf = BytesIO()
  81. matplotlib.pyplot.savefig(buf, format="png")
  82. buf.seek(0)
  83. # encode to a base64 str
  84. img_str = base64.b64encode(buf.read()).decode('utf-8')
  85. matplotlib.pyplot.clf()
  86. buf.close()
  87. print(f"data:image/png;base64,{img_str}")
  88. matplotlib.pyplot.show = show`);
  89. }
  90. self.result = await self.pyodide.runPythonAsync(code);
  91. // Safely process and recursively serialize the result
  92. self.result = processResult(self.result);
  93. console.log('Python result:', self.result);
  94. // Persist any changes to IndexedDB
  95. // await new Promise<void>((resolve, reject) => {
  96. // self.pyodide.FS.syncfs(false, (err) => {
  97. // if (err) {
  98. // console.error('Error syncing to IndexedDB:', err);
  99. // reject(err);
  100. // } else {
  101. // console.log('Successfully synced to IndexedDB.');
  102. // resolve();
  103. // }
  104. // });
  105. // });
  106. } catch (error) {
  107. self.stderr = error.toString();
  108. }
  109. self.postMessage({ id, result: self.result, stdout: self.stdout, stderr: self.stderr });
  110. };
  111. function processResult(result: any): any {
  112. // Catch and always return JSON-safe string representations
  113. try {
  114. if (result == null) {
  115. // Handle null and undefined
  116. return null;
  117. }
  118. if (typeof result === 'string' || typeof result === 'number' || typeof result === 'boolean') {
  119. // Handle primitive types directly
  120. return result;
  121. }
  122. if (typeof result === 'bigint') {
  123. // Convert BigInt to a string for JSON-safe representation
  124. return result.toString();
  125. }
  126. if (Array.isArray(result)) {
  127. // If it's an array, recursively process items
  128. return result.map((item) => processResult(item));
  129. }
  130. if (typeof result.toJs === 'function') {
  131. // If it's a Pyodide proxy object (e.g., Pandas DF, Numpy Array), convert to JS and process recursively
  132. return processResult(result.toJs());
  133. }
  134. if (typeof result === 'object') {
  135. // Convert JS objects to a recursively serialized representation
  136. const processedObject: { [key: string]: any } = {};
  137. for (const key in result) {
  138. if (Object.prototype.hasOwnProperty.call(result, key)) {
  139. processedObject[key] = processResult(result[key]);
  140. }
  141. }
  142. return processedObject;
  143. }
  144. // Stringify anything that's left (e.g., Proxy objects that cannot be directly processed)
  145. return JSON.stringify(result);
  146. } catch (err) {
  147. // In case something unexpected happens, we return a stringified fallback
  148. return `[processResult error]: ${err.message || err.toString()}`;
  149. }
  150. }
  151. export default {};