@adecore/database 0.0.1 → 0.16.0-beta.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 (400) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -1
  3. package/dist/DatabaseProvider.d.ts +13 -0
  4. package/dist/DatabaseProvider.js +17 -0
  5. package/dist/actions.d.ts +40 -0
  6. package/dist/actions.js +1 -0
  7. package/dist/client/client.d.ts +5 -0
  8. package/dist/client/client.js +245 -0
  9. package/dist/client/index.d.ts +2 -0
  10. package/dist/client/index.js +2 -0
  11. package/dist/client/types.d.ts +97 -0
  12. package/dist/client/types.js +13 -0
  13. package/dist/client-context.d.ts +16 -0
  14. package/dist/client-context.js +17 -0
  15. package/dist/connections/ConnectionForm.d.ts +10 -0
  16. package/dist/connections/ConnectionForm.js +145 -0
  17. package/dist/connections/ConnectionManager.d.ts +12 -0
  18. package/dist/connections/ConnectionManager.js +93 -0
  19. package/dist/connections/EngineIcon.d.ts +9 -0
  20. package/dist/connections/EngineIcon.js +7 -0
  21. package/dist/connections/connection-config.d.ts +16 -0
  22. package/dist/connections/connection-config.js +107 -0
  23. package/dist/connections/docker.d.ts +9 -0
  24. package/dist/connections/docker.js +38 -0
  25. package/dist/connections/engine-icons.d.ts +3 -0
  26. package/dist/connections/engine-icons.js +3 -0
  27. package/dist/connections/use-docker-containers.d.ts +17 -0
  28. package/dist/connections/use-docker-containers.js +26 -0
  29. package/dist/console/DestructiveDialog.d.ts +9 -0
  30. package/dist/console/DestructiveDialog.js +9 -0
  31. package/dist/console/HistoryPanel.d.ts +10 -0
  32. package/dist/console/HistoryPanel.js +15 -0
  33. package/dist/console/QueryConsole.d.ts +14 -0
  34. package/dist/console/QueryConsole.js +261 -0
  35. package/dist/console/SchemaPicker.d.ts +11 -0
  36. package/dist/console/SchemaPicker.js +8 -0
  37. package/dist/console/SqlEditor.d.ts +12 -0
  38. package/dist/console/SqlEditor.js +61 -0
  39. package/dist/console/StatementResultView.d.ts +18 -0
  40. package/dist/console/StatementResultView.js +32 -0
  41. package/dist/console/TransactionControls.d.ts +13 -0
  42. package/dist/console/TransactionControls.js +12 -0
  43. package/dist/console/destructive.d.ts +7 -0
  44. package/dist/console/destructive.js +43 -0
  45. package/dist/console/editing.d.ts +12 -0
  46. package/dist/console/editing.js +60 -0
  47. package/dist/console/history.d.ts +13 -0
  48. package/dist/console/history.js +43 -0
  49. package/dist/console/labels.d.ts +1 -0
  50. package/dist/console/labels.js +6 -0
  51. package/dist/console/outcome.d.ts +6 -0
  52. package/dist/console/outcome.js +11 -0
  53. package/dist/console/schemas.d.ts +2 -0
  54. package/dist/console/schemas.js +5 -0
  55. package/dist/console/shortcuts.d.ts +2 -0
  56. package/dist/console/shortcuts.js +3 -0
  57. package/dist/console/useConsoleHistory.d.ts +7 -0
  58. package/dist/console/useConsoleHistory.js +22 -0
  59. package/dist/ddl/alter-mysql.d.ts +4 -0
  60. package/dist/ddl/alter-mysql.js +79 -0
  61. package/dist/ddl/alter-sqlite.d.ts +4 -0
  62. package/dist/ddl/alter-sqlite.js +80 -0
  63. package/dist/ddl/alter.d.ts +4 -0
  64. package/dist/ddl/alter.js +24 -0
  65. package/dist/ddl/column.d.ts +9 -0
  66. package/dist/ddl/column.js +62 -0
  67. package/dist/ddl/create.d.ts +12 -0
  68. package/dist/ddl/create.js +74 -0
  69. package/dist/ddl/dialect.d.ts +12 -0
  70. package/dist/ddl/dialect.js +34 -0
  71. package/dist/ddl/diff.d.ts +34 -0
  72. package/dist/ddl/diff.js +111 -0
  73. package/dist/ddl/draft.d.ts +46 -0
  74. package/dist/ddl/draft.js +70 -0
  75. package/dist/ddl/index.d.ts +10 -0
  76. package/dist/ddl/index.js +10 -0
  77. package/dist/ddl/parse.d.ts +17 -0
  78. package/dist/ddl/parse.js +131 -0
  79. package/dist/ddl/statements.d.ts +6 -0
  80. package/dist/ddl/statements.js +12 -0
  81. package/dist/ddl/types.d.ts +6 -0
  82. package/dist/ddl/types.js +52 -0
  83. package/dist/ddl/validate.d.ts +7 -0
  84. package/dist/ddl/validate.js +38 -0
  85. package/dist/designer/ColumnsEditor.d.ts +2 -0
  86. package/dist/designer/ColumnsEditor.js +24 -0
  87. package/dist/designer/DesignerView.d.ts +25 -0
  88. package/dist/designer/DesignerView.js +33 -0
  89. package/dist/designer/ForeignKeysEditor.d.ts +7 -0
  90. package/dist/designer/ForeignKeysEditor.js +24 -0
  91. package/dist/designer/IndexesEditor.d.ts +2 -0
  92. package/dist/designer/IndexesEditor.js +13 -0
  93. package/dist/designer/NamePicker.d.ts +11 -0
  94. package/dist/designer/NamePicker.js +13 -0
  95. package/dist/designer/OptionsEditor.d.ts +2 -0
  96. package/dist/designer/OptionsEditor.js +17 -0
  97. package/dist/designer/SqlPreview.d.ts +12 -0
  98. package/dist/designer/SqlPreview.js +17 -0
  99. package/dist/designer/TableDesigner.d.ts +10 -0
  100. package/dist/designer/TableDesigner.js +103 -0
  101. package/dist/designer/edit.d.ts +17 -0
  102. package/dist/designer/edit.js +115 -0
  103. package/dist/designer/editor-props.d.ts +10 -0
  104. package/dist/designer/editor-props.js +1 -0
  105. package/dist/designer/plan.d.ts +9 -0
  106. package/dist/designer/plan.js +13 -0
  107. package/dist/designer/recover.d.ts +2 -0
  108. package/dist/designer/recover.js +18 -0
  109. package/dist/designer/useReferenceColumns.d.ts +3 -0
  110. package/dist/designer/useReferenceColumns.js +36 -0
  111. package/dist/explorer/DatabaseExplorer.d.ts +13 -0
  112. package/dist/explorer/DatabaseExplorer.js +264 -0
  113. package/dist/explorer/RowMenu.d.ts +11 -0
  114. package/dist/explorer/RowMenu.js +89 -0
  115. package/dist/explorer/TableDialog.d.ts +15 -0
  116. package/dist/explorer/TableDialog.js +50 -0
  117. package/dist/explorer/expansion.d.ts +7 -0
  118. package/dist/explorer/expansion.js +31 -0
  119. package/dist/explorer/table-statements.d.ts +5 -0
  120. package/dist/explorer/table-statements.js +12 -0
  121. package/dist/explorer/tree.d.ts +125 -0
  122. package/dist/explorer/tree.js +295 -0
  123. package/dist/explorer/use-explorer-loads.d.ts +12 -0
  124. package/dist/explorer/use-explorer-loads.js +109 -0
  125. package/dist/explorer/use-stored-expansion.d.ts +9 -0
  126. package/dist/explorer/use-stored-expansion.js +47 -0
  127. package/dist/grid/CellEditor.d.ts +9 -0
  128. package/dist/grid/CellEditor.js +29 -0
  129. package/dist/grid/CopyAsMenu.d.ts +7 -0
  130. package/dist/grid/CopyAsMenu.js +10 -0
  131. package/dist/grid/DataGrid.d.ts +36 -0
  132. package/dist/grid/DataGrid.js +512 -0
  133. package/dist/grid/EnumPicker.d.ts +17 -0
  134. package/dist/grid/EnumPicker.js +53 -0
  135. package/dist/grid/GridHeaderCell.d.ts +36 -0
  136. package/dist/grid/GridHeaderCell.js +69 -0
  137. package/dist/grid/RecordView.d.ts +20 -0
  138. package/dist/grid/RecordView.js +126 -0
  139. package/dist/grid/ValueDock.d.ts +11 -0
  140. package/dist/grid/ValueDock.js +28 -0
  141. package/dist/grid/aggregates.d.ts +22 -0
  142. package/dist/grid/aggregates.js +62 -0
  143. package/dist/grid/cell-range.d.ts +14 -0
  144. package/dist/grid/cell-range.js +11 -0
  145. package/dist/grid/column-layout.d.ts +18 -0
  146. package/dist/grid/column-layout.js +49 -0
  147. package/dist/grid/column-selection.d.ts +11 -0
  148. package/dist/grid/column-selection.js +28 -0
  149. package/dist/grid/column-view.d.ts +10 -0
  150. package/dist/grid/column-view.js +23 -0
  151. package/dist/grid/copy-formats.d.ts +15 -0
  152. package/dist/grid/copy-formats.js +80 -0
  153. package/dist/grid/display.d.ts +20 -0
  154. package/dist/grid/display.js +81 -0
  155. package/dist/grid/edit-value.d.ts +5 -0
  156. package/dist/grid/edit-value.js +55 -0
  157. package/dist/grid/enum-type.d.ts +7 -0
  158. package/dist/grid/enum-type.js +52 -0
  159. package/dist/grid/focused-value.d.ts +4 -0
  160. package/dist/grid/focused-value.js +19 -0
  161. package/dist/grid/layout.d.ts +22 -0
  162. package/dist/grid/layout.js +74 -0
  163. package/dist/grid/navigation.d.ts +15 -0
  164. package/dist/grid/navigation.js +52 -0
  165. package/dist/grid/row-selection.d.ts +9 -0
  166. package/dist/grid/row-selection.js +24 -0
  167. package/dist/grid/sort.d.ts +7 -0
  168. package/dist/grid/sort.js +7 -0
  169. package/dist/grid/types.d.ts +30 -0
  170. package/dist/grid/types.js +9 -0
  171. package/dist/host/helper-path.d.ts +7 -0
  172. package/dist/host/helper-path.js +15 -0
  173. package/dist/host/host.d.ts +2 -0
  174. package/dist/host/host.js +343 -0
  175. package/dist/host/index.d.ts +5 -0
  176. package/dist/host/index.js +4 -0
  177. package/dist/host/spawn.d.ts +7 -0
  178. package/dist/host/spawn.js +98 -0
  179. package/dist/host/types.d.ts +19 -0
  180. package/dist/host/types.js +1 -0
  181. package/dist/host/validate.d.ts +10 -0
  182. package/dist/host/validate.js +339 -0
  183. package/dist/index.d.ts +14 -0
  184. package/dist/index.js +13 -0
  185. package/dist/locales/en.json +577 -0
  186. package/dist/locales/nl.json +577 -0
  187. package/dist/locales.d.ts +4 -0
  188. package/dist/locales.js +14 -0
  189. package/dist/protocol/connection.d.ts +56 -0
  190. package/dist/protocol/connection.js +1 -0
  191. package/dist/protocol/errors.d.ts +7 -0
  192. package/dist/protocol/errors.js +1 -0
  193. package/dist/protocol/index.d.ts +5 -0
  194. package/dist/protocol/index.js +3 -0
  195. package/dist/protocol/messages.d.ts +250 -0
  196. package/dist/protocol/messages.js +2 -0
  197. package/dist/protocol/schema.d.ts +53 -0
  198. package/dist/protocol/schema.js +1 -0
  199. package/dist/protocol/values.d.ts +21 -0
  200. package/dist/protocol/values.js +15 -0
  201. package/dist/sql-split.d.ts +15 -0
  202. package/dist/sql-split.js +201 -0
  203. package/dist/sql.d.ts +29 -0
  204. package/dist/sql.js +104 -0
  205. package/dist/structure/StructureView.d.ts +14 -0
  206. package/dist/structure/StructureView.js +99 -0
  207. package/dist/structure/structure-text.d.ts +2 -0
  208. package/dist/structure/structure-text.js +2 -0
  209. package/dist/table/CommandField.d.ts +14 -0
  210. package/dist/table/CommandField.js +157 -0
  211. package/dist/table/ImportDialog.d.ts +27 -0
  212. package/dist/table/ImportDialog.js +28 -0
  213. package/dist/table/TableStatusBar.d.ts +19 -0
  214. package/dist/table/TableStatusBar.js +22 -0
  215. package/dist/table/TableToolbar.d.ts +43 -0
  216. package/dist/table/TableToolbar.js +16 -0
  217. package/dist/table/TableView.d.ts +13 -0
  218. package/dist/table/TableView.js +508 -0
  219. package/dist/table/command-field.d.ts +57 -0
  220. package/dist/table/command-field.js +203 -0
  221. package/dist/table/grid-rows.d.ts +23 -0
  222. package/dist/table/grid-rows.js +55 -0
  223. package/dist/table/import-mapping.d.ts +7 -0
  224. package/dist/table/import-mapping.js +28 -0
  225. package/dist/table/layout-store.d.ts +13 -0
  226. package/dist/table/layout-store.js +43 -0
  227. package/dist/table/paging.d.ts +12 -0
  228. package/dist/table/paging.js +20 -0
  229. package/dist/table/pending.d.ts +32 -0
  230. package/dist/table/pending.js +94 -0
  231. package/dist/table/references.d.ts +12 -0
  232. package/dist/table/references.js +21 -0
  233. package/dist/table/useLoaded.d.ts +7 -0
  234. package/dist/table/useLoaded.js +36 -0
  235. package/dist/table/useStoredLayout.d.ts +8 -0
  236. package/dist/table/useStoredLayout.js +39 -0
  237. package/dist/table/useTableTransfer.d.ts +51 -0
  238. package/dist/table/useTableTransfer.js +125 -0
  239. package/dist/testing/fake.d.ts +21 -0
  240. package/dist/testing/fake.js +372 -0
  241. package/dist/testing/index.d.ts +1 -0
  242. package/dist/testing/index.js +1 -0
  243. package/dist/use-popup-press.d.ts +1 -0
  244. package/dist/use-popup-press.js +24 -0
  245. package/dist/use-schema-change.d.ts +1 -0
  246. package/dist/use-schema-change.js +13 -0
  247. package/dist/use-stable-callback.d.ts +1 -0
  248. package/dist/use-stable-callback.js +9 -0
  249. package/dist/value/ValuePanel.d.ts +14 -0
  250. package/dist/value/ValuePanel.js +103 -0
  251. package/dist/value/draft.d.ts +33 -0
  252. package/dist/value/draft.js +44 -0
  253. package/dist/value/hex.d.ts +14 -0
  254. package/dist/value/hex.js +46 -0
  255. package/dist/value/json.d.ts +9 -0
  256. package/dist/value/json.js +71 -0
  257. package/dist/value/views.d.ts +22 -0
  258. package/dist/value/views.js +54 -0
  259. package/dist/workbench/DatabaseWorkbench.d.ts +9 -0
  260. package/dist/workbench/DatabaseWorkbench.js +179 -0
  261. package/dist/workbench/DesignerTab.d.ts +10 -0
  262. package/dist/workbench/DesignerTab.js +22 -0
  263. package/dist/workbench/TableTab.d.ts +10 -0
  264. package/dist/workbench/TableTab.js +8 -0
  265. package/dist/workbench/tabs.d.ts +42 -0
  266. package/dist/workbench/tabs.js +139 -0
  267. package/package.json +79 -3
  268. package/src/DatabaseProvider.tsx +37 -0
  269. package/src/actions.ts +34 -0
  270. package/src/client/client.ts +324 -0
  271. package/src/client/index.ts +18 -0
  272. package/src/client/types.ts +161 -0
  273. package/src/client-context.ts +35 -0
  274. package/src/connections/ConnectionForm.tsx +474 -0
  275. package/src/connections/ConnectionManager.tsx +224 -0
  276. package/src/connections/EngineIcon.tsx +18 -0
  277. package/src/connections/connection-config.ts +143 -0
  278. package/src/connections/docker.ts +50 -0
  279. package/src/connections/engine-icons.ts +5 -0
  280. package/src/connections/use-docker-containers.ts +41 -0
  281. package/src/console/DestructiveDialog.tsx +40 -0
  282. package/src/console/HistoryPanel.tsx +67 -0
  283. package/src/console/QueryConsole.tsx +439 -0
  284. package/src/console/SchemaPicker.tsx +32 -0
  285. package/src/console/SqlEditor.tsx +95 -0
  286. package/src/console/StatementResultView.tsx +176 -0
  287. package/src/console/TransactionControls.tsx +53 -0
  288. package/src/console/destructive.ts +60 -0
  289. package/src/console/editing.ts +77 -0
  290. package/src/console/history.ts +62 -0
  291. package/src/console/labels.ts +7 -0
  292. package/src/console/outcome.ts +14 -0
  293. package/src/console/schemas.ts +7 -0
  294. package/src/console/shortcuts.ts +4 -0
  295. package/src/console/useConsoleHistory.ts +39 -0
  296. package/src/ddl/alter-mysql.ts +90 -0
  297. package/src/ddl/alter-sqlite.ts +93 -0
  298. package/src/ddl/alter.ts +29 -0
  299. package/src/ddl/column.ts +74 -0
  300. package/src/ddl/create.ts +92 -0
  301. package/src/ddl/dialect.ts +53 -0
  302. package/src/ddl/diff.ts +164 -0
  303. package/src/ddl/draft.ts +136 -0
  304. package/src/ddl/index.ts +10 -0
  305. package/src/ddl/parse.ts +161 -0
  306. package/src/ddl/statements.ts +20 -0
  307. package/src/ddl/types.ts +60 -0
  308. package/src/ddl/validate.ts +47 -0
  309. package/src/designer/ColumnsEditor.tsx +204 -0
  310. package/src/designer/DesignerView.tsx +184 -0
  311. package/src/designer/ForeignKeysEditor.tsx +132 -0
  312. package/src/designer/IndexesEditor.tsx +63 -0
  313. package/src/designer/NamePicker.tsx +44 -0
  314. package/src/designer/OptionsEditor.tsx +101 -0
  315. package/src/designer/SqlPreview.tsx +56 -0
  316. package/src/designer/TableDesigner.tsx +186 -0
  317. package/src/designer/edit.ts +137 -0
  318. package/src/designer/editor-props.ts +13 -0
  319. package/src/designer/plan.ts +25 -0
  320. package/src/designer/recover.ts +20 -0
  321. package/src/designer/useReferenceColumns.ts +54 -0
  322. package/src/explorer/DatabaseExplorer.tsx +450 -0
  323. package/src/explorer/RowMenu.tsx +134 -0
  324. package/src/explorer/TableDialog.tsx +80 -0
  325. package/src/explorer/expansion.ts +47 -0
  326. package/src/explorer/table-statements.ts +18 -0
  327. package/src/explorer/tree.ts +453 -0
  328. package/src/explorer/use-explorer-loads.ts +157 -0
  329. package/src/explorer/use-stored-expansion.ts +66 -0
  330. package/src/grid/CellEditor.tsx +53 -0
  331. package/src/grid/CopyAsMenu.tsx +33 -0
  332. package/src/grid/DataGrid.tsx +871 -0
  333. package/src/grid/EnumPicker.tsx +108 -0
  334. package/src/grid/GridHeaderCell.tsx +243 -0
  335. package/src/grid/RecordView.tsx +293 -0
  336. package/src/grid/ValueDock.tsx +52 -0
  337. package/src/grid/aggregates.ts +94 -0
  338. package/src/grid/cell-range.ts +26 -0
  339. package/src/grid/column-layout.ts +71 -0
  340. package/src/grid/column-selection.ts +42 -0
  341. package/src/grid/column-view.ts +35 -0
  342. package/src/grid/copy-formats.ts +108 -0
  343. package/src/grid/display.ts +109 -0
  344. package/src/grid/edit-value.ts +60 -0
  345. package/src/grid/enum-type.ts +62 -0
  346. package/src/grid/focused-value.ts +23 -0
  347. package/src/grid/layout.ts +91 -0
  348. package/src/grid/navigation.ts +74 -0
  349. package/src/grid/row-selection.ts +35 -0
  350. package/src/grid/sort.ts +12 -0
  351. package/src/grid/types.ts +59 -0
  352. package/src/host/helper-path.ts +41 -0
  353. package/src/host/host.ts +456 -0
  354. package/src/host/index.ts +5 -0
  355. package/src/host/spawn.ts +124 -0
  356. package/src/host/types.ts +46 -0
  357. package/src/host/validate.ts +402 -0
  358. package/src/index.ts +37 -0
  359. package/src/locales/en.json +577 -0
  360. package/src/locales/nl.json +577 -0
  361. package/src/locales.ts +18 -0
  362. package/src/protocol/connection.ts +86 -0
  363. package/src/protocol/errors.ts +38 -0
  364. package/src/protocol/index.ts +33 -0
  365. package/src/protocol/messages.ts +168 -0
  366. package/src/protocol/schema.ts +76 -0
  367. package/src/protocol/values.ts +52 -0
  368. package/src/sql-split.ts +220 -0
  369. package/src/sql.ts +152 -0
  370. package/src/structure/StructureView.tsx +269 -0
  371. package/src/structure/structure-text.ts +4 -0
  372. package/src/table/CommandField.tsx +318 -0
  373. package/src/table/ImportDialog.tsx +168 -0
  374. package/src/table/TableStatusBar.tsx +91 -0
  375. package/src/table/TableToolbar.tsx +217 -0
  376. package/src/table/TableView.tsx +880 -0
  377. package/src/table/command-field.ts +285 -0
  378. package/src/table/grid-rows.ts +80 -0
  379. package/src/table/import-mapping.ts +37 -0
  380. package/src/table/layout-store.ts +62 -0
  381. package/src/table/paging.ts +33 -0
  382. package/src/table/pending.ts +143 -0
  383. package/src/table/references.ts +38 -0
  384. package/src/table/useLoaded.ts +58 -0
  385. package/src/table/useStoredLayout.ts +53 -0
  386. package/src/table/useTableTransfer.ts +187 -0
  387. package/src/testing/fake.ts +533 -0
  388. package/src/testing/index.ts +1 -0
  389. package/src/use-popup-press.ts +27 -0
  390. package/src/use-schema-change.ts +19 -0
  391. package/src/use-stable-callback.ts +12 -0
  392. package/src/value/ValuePanel.tsx +254 -0
  393. package/src/value/draft.ts +72 -0
  394. package/src/value/hex.ts +62 -0
  395. package/src/value/json.ts +74 -0
  396. package/src/value/views.ts +68 -0
  397. package/src/workbench/DatabaseWorkbench.tsx +348 -0
  398. package/src/workbench/DesignerTab.tsx +37 -0
  399. package/src/workbench/TableTab.tsx +27 -0
  400. package/src/workbench/tabs.ts +184 -0
@@ -0,0 +1,456 @@
1
+ import { PROTOCOL_VERSION, type DatabaseError, type DatabaseErrorCode, type DatabaseRequest, type DatabaseResponse } from '../protocol/index.ts';
2
+ import type { DatabaseHost, DatabaseHostOptions, HelperProcess } from './types.ts';
3
+ import { parseRequest } from './validate.ts';
4
+
5
+ const DEFAULT_READY_TIMEOUT_MS = 10000;
6
+
7
+ /* How long a release or a dispose waits for a helper to answer its `close` requests. */
8
+ const CLOSE_GRACE_MS = 2000;
9
+
10
+ /* A failure with the code the caller gets. */
11
+ class Failure extends Error {
12
+ readonly code: DatabaseErrorCode;
13
+
14
+ constructor(code: DatabaseErrorCode, message: string) {
15
+ super(message);
16
+ this.code = code;
17
+ }
18
+ }
19
+
20
+ /* One helper process, from its start to its end. */
21
+ interface Run {
22
+ readonly process: HelperProcess;
23
+ readonly ready: Promise<void>;
24
+ readonly pending: Map<string, (response: DatabaseResponse) => void>;
25
+ readonly resolveReady: () => void;
26
+ readonly rejectReady: (error: Failure) => void;
27
+ timer: ReturnType<typeof setTimeout> | undefined;
28
+ state: 'starting' | 'ready' | 'dead';
29
+ }
30
+
31
+ /* A request of an owner as the host tracks it, from the moment it arrives until it is answered. */
32
+ interface Entry {
33
+ /* The id toward the helper; `null` while the request waits for the app's check or for the helper. */
34
+ helperId: string | null;
35
+ cancelled: boolean;
36
+ /* The owner is gone, so a session this request opens must close again. */
37
+ released: boolean;
38
+ }
39
+
40
+ const failure = (id: string, code: DatabaseErrorCode, message: string): DatabaseResponse => ({ id, ok: false, error: { code, message } });
41
+
42
+ const messageOf = (error: unknown): string => (error instanceof Error ? error.message : String(error));
43
+
44
+ const isDict = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);
45
+
46
+ /* Settles when every promise has, or after the grace period; a helper that hangs must not hold up an app that quits. */
47
+ const settleWithin = async (promises: readonly Promise<unknown>[]): Promise<void> => {
48
+ let timer: ReturnType<typeof setTimeout> | undefined;
49
+ const grace = new Promise<void>((resolve) => {
50
+ timer = setTimeout(resolve, CLOSE_GRACE_MS);
51
+ });
52
+
53
+ try {
54
+ await Promise.race([Promise.allSettled(promises), grace]);
55
+ } finally {
56
+ clearTimeout(timer);
57
+ }
58
+ };
59
+
60
+ /* Reads what the helper wrote for a request; anything that is not a response of the protocol becomes an `internal` error. */
61
+ const normalize = (id: string, message: Record<string, unknown>): DatabaseResponse => {
62
+ if (message.ok === true) {
63
+ return { id, ok: true, result: message.result ?? null } as DatabaseResponse;
64
+ }
65
+
66
+ const error = message.error;
67
+
68
+ if (message.ok === false && isDict(error) && typeof error.code === 'string' && typeof error.message === 'string') {
69
+ return { id, ok: false, error: error as unknown as DatabaseError };
70
+ }
71
+
72
+ return failure(id, 'internal', 'The helper sent a response that is not part of the protocol.');
73
+ };
74
+
75
+ export const createDatabaseHost = (options: DatabaseHostOptions): DatabaseHost => {
76
+ const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
77
+ // Session id to the owner that opened it.
78
+ const sessions = new Map<string, string>();
79
+ const inflight = new Map<string, Map<string, Entry>>();
80
+ let running: Run | null = null;
81
+ let counter = 0;
82
+ let disposed = false;
83
+ let disposal: Promise<void> | null = null;
84
+
85
+ const retire = (run: Run, message: string): void => {
86
+ if (run.state === 'dead') {
87
+ return;
88
+ }
89
+
90
+ const wasStarting = run.state === 'starting';
91
+ run.state = 'dead';
92
+ clearTimeout(run.timer);
93
+
94
+ if (running === run) {
95
+ running = null;
96
+ }
97
+
98
+ sessions.clear();
99
+
100
+ if (wasStarting) {
101
+ run.rejectReady(new Failure('helper-unavailable', message));
102
+ }
103
+
104
+ const pending = [...run.pending.entries()];
105
+ run.pending.clear();
106
+
107
+ for (const [id, resolve] of pending) {
108
+ resolve(failure(id, 'helper-exited', message));
109
+ }
110
+ };
111
+
112
+ const abort = (run: Run, message: string): void => {
113
+ run.process.kill();
114
+ retire(run, message);
115
+ };
116
+
117
+ const onHandshake = (run: Run, line: string): void => {
118
+ let message: unknown;
119
+
120
+ try {
121
+ message = JSON.parse(line);
122
+ } catch {
123
+ abort(run, 'The helper did not start with a ready line.');
124
+ return;
125
+ }
126
+
127
+ if (!isDict(message) || message.event !== 'ready') {
128
+ abort(run, 'The helper did not start with a ready line.');
129
+ return;
130
+ }
131
+
132
+ if (message.protocol !== PROTOCOL_VERSION) {
133
+ abort(run, `The helper speaks protocol ${String(message.protocol)}, this host speaks ${PROTOCOL_VERSION}.`);
134
+ return;
135
+ }
136
+
137
+ run.state = 'ready';
138
+ clearTimeout(run.timer);
139
+ run.resolveReady();
140
+ };
141
+
142
+ const onResponse = (run: Run, line: string): void => {
143
+ let message: unknown;
144
+
145
+ try {
146
+ message = JSON.parse(line);
147
+ } catch {
148
+ return;
149
+ }
150
+
151
+ if (!isDict(message) || typeof message.id !== 'string') {
152
+ return;
153
+ }
154
+
155
+ const resolve = run.pending.get(message.id);
156
+
157
+ if (resolve === undefined) {
158
+ return;
159
+ }
160
+
161
+ run.pending.delete(message.id);
162
+ resolve(normalize(message.id, message));
163
+ };
164
+
165
+ const startRun = (): Run => {
166
+ let child: HelperProcess;
167
+
168
+ try {
169
+ child = options.start();
170
+ } catch (error) {
171
+ throw new Failure('helper-unavailable', `The helper could not be started: ${messageOf(error)}`);
172
+ }
173
+
174
+ let resolveReady = (): void => {};
175
+ let rejectReady = (_error: Failure): void => {};
176
+ const ready = new Promise<void>((resolve, reject) => {
177
+ resolveReady = resolve;
178
+ rejectReady = reject;
179
+ });
180
+ // A request awaits this; a run nobody waits for must not raise an unhandled rejection.
181
+ ready.catch(() => {});
182
+
183
+ const run: Run = { process: child, ready, pending: new Map(), resolveReady, rejectReady, timer: undefined, state: 'starting' };
184
+ run.timer = setTimeout(() => abort(run, 'The helper did not become ready in time.'), readyTimeoutMs);
185
+
186
+ child.onLine((line) => {
187
+ if (run.state === 'starting') {
188
+ onHandshake(run, line);
189
+ } else if (run.state === 'ready') {
190
+ onResponse(run, line);
191
+ }
192
+ });
193
+ child.onExit((code) => retire(run, code === null ? 'The helper exited.' : `The helper exited with code ${code}.`));
194
+
195
+ return run;
196
+ };
197
+
198
+ const ensureRunning = async (): Promise<Run> => {
199
+ if (running === null || running.state === 'dead') {
200
+ running = startRun();
201
+ }
202
+
203
+ const run = running;
204
+ await run.ready;
205
+
206
+ if (run.state === 'dead') {
207
+ throw new Failure('helper-exited', 'The helper exited.');
208
+ }
209
+
210
+ return run;
211
+ };
212
+
213
+ /* Sends a message under a helper id of the host's own; the promise never rejects. */
214
+ const dispatch = (run: Run, method: string, params: unknown): { readonly id: string; readonly response: Promise<DatabaseResponse> } => {
215
+ const id = `h${++counter}`;
216
+ const response = new Promise<DatabaseResponse>((resolve) => {
217
+ run.pending.set(id, resolve);
218
+
219
+ try {
220
+ run.process.write(JSON.stringify({ id, method, params }));
221
+ } catch (error) {
222
+ run.pending.delete(id);
223
+ resolve(failure(id, 'helper-unavailable', `The helper could not be written to: ${messageOf(error)}`));
224
+ }
225
+ });
226
+
227
+ return { id, response };
228
+ };
229
+
230
+ const liveRun = (): Run | null => (running !== null && running.state === 'ready' ? running : null);
231
+
232
+ const sessionOf = (request: DatabaseRequest): string | null => {
233
+ const session = (request.params as { readonly session?: unknown }).session;
234
+ return typeof session === 'string' ? session : null;
235
+ };
236
+
237
+ const assertSession = (request: DatabaseRequest, owner: string): void => {
238
+ const session = sessionOf(request);
239
+
240
+ if (session !== null && sessions.get(session) !== owner) {
241
+ throw new Failure('unknown-session', `There is no open session "${session}".`);
242
+ }
243
+ };
244
+
245
+ const assertActive = (entry: Entry): void => {
246
+ if (disposed) {
247
+ throw new Failure('helper-unavailable', 'The database host has been disposed.');
248
+ }
249
+
250
+ if (entry.cancelled) {
251
+ throw new Failure('cancelled', 'The request was cancelled.');
252
+ }
253
+ };
254
+
255
+ /* An app's check that throws or rejects counts as a no. */
256
+ const permitted = async (check: () => boolean | Promise<boolean>): Promise<boolean> => {
257
+ try {
258
+ return await check();
259
+ } catch {
260
+ return false;
261
+ }
262
+ };
263
+
264
+ const authorize = async (request: DatabaseRequest, owner: string): Promise<void> => {
265
+ switch (request.method) {
266
+ case 'open':
267
+ case 'test': {
268
+ if (options.authorize !== undefined && !(await permitted(() => options.authorize!(request.params.connection, owner)))) {
269
+ throw new Failure('forbidden', 'This connection is not allowed.');
270
+ }
271
+
272
+ break;
273
+ }
274
+
275
+ case 'export':
276
+ case 'import':
277
+ case 'sample': {
278
+ const access = request.method === 'export' ? 'write' : 'read';
279
+
280
+ if (options.authorizeFile === undefined || !(await permitted(() => options.authorizeFile!(request.params.path, access, owner)))) {
281
+ throw new Failure('forbidden', 'This file is not allowed.');
282
+ }
283
+
284
+ break;
285
+ }
286
+
287
+ case 'discover': {
288
+ if (options.authorizeDiscovery === undefined || !(await permitted(() => options.authorizeDiscovery!(request.params.kind, owner)))) {
289
+ throw new Failure('forbidden', 'Discovery is not allowed.');
290
+ }
291
+
292
+ break;
293
+ }
294
+ }
295
+ };
296
+
297
+ const cancelOwned = async (request: Extract<DatabaseRequest, { method: 'cancel' }>, owner: string): Promise<DatabaseResponse> => {
298
+ const entries = inflight.get(owner);
299
+ const target = request.id === request.params.request ? undefined : entries?.get(request.params.request);
300
+ const answer = (cancelled: boolean): DatabaseResponse => ({ id: request.id, ok: true, result: { cancelled } });
301
+
302
+ if (target === undefined) {
303
+ return answer(false);
304
+ }
305
+
306
+ if (target.helperId === null) {
307
+ target.cancelled = true;
308
+ return answer(true);
309
+ }
310
+
311
+ const run = liveRun();
312
+
313
+ if (run === null) {
314
+ return answer(false);
315
+ }
316
+
317
+ const sent = await dispatch(run, 'cancel', { request: target.helperId }).response;
318
+ return { ...sent, id: request.id } as DatabaseResponse;
319
+ };
320
+
321
+ /* Remembers what a response changed about the sessions, then hands it back under the caller's id. */
322
+ const settle = (request: DatabaseRequest, owner: string, entry: Entry, run: Run, response: DatabaseResponse): DatabaseResponse => {
323
+ if (request.method === 'close') {
324
+ sessions.delete(request.params.session);
325
+ }
326
+
327
+ if (request.method !== 'open' || !response.ok) {
328
+ return { ...response, id: request.id } as DatabaseResponse;
329
+ }
330
+
331
+ const session = (response.result as { readonly session?: unknown } | null)?.session;
332
+
333
+ if (typeof session !== 'string') {
334
+ return failure(request.id, 'internal', 'The helper opened a session without an id.');
335
+ }
336
+
337
+ if (run.state === 'dead') {
338
+ return failure(request.id, 'helper-exited', 'The helper exited.');
339
+ }
340
+
341
+ if (entry.released) {
342
+ void dispatch(run, 'close', { session }).response;
343
+ return failure(request.id, 'cancelled', 'The request was cancelled.');
344
+ }
345
+
346
+ sessions.set(session, owner);
347
+ return { ...response, id: request.id } as DatabaseResponse;
348
+ };
349
+
350
+ const route = async (request: DatabaseRequest, owner: string, entry: Entry): Promise<DatabaseResponse> => {
351
+ assertSession(request, owner);
352
+
353
+ if (request.method === 'cancel') {
354
+ return cancelOwned(request, owner);
355
+ }
356
+
357
+ await authorize(request, owner);
358
+ assertActive(entry);
359
+
360
+ const run = await ensureRunning();
361
+ assertActive(entry);
362
+ assertSession(request, owner);
363
+
364
+ const sent = dispatch(run, request.method, request.params);
365
+ entry.helperId = sent.id;
366
+
367
+ return settle(request, owner, entry, run, await sent.response);
368
+ };
369
+
370
+ const handle = async (input: unknown, owner: string): Promise<DatabaseResponse> => {
371
+ const parsed = parseRequest(input);
372
+
373
+ if (!parsed.ok) {
374
+ return { id: parsed.id, ok: false, error: parsed.error };
375
+ }
376
+
377
+ const request = parsed.request;
378
+
379
+ if (disposed) {
380
+ return failure(request.id, 'helper-unavailable', 'The database host has been disposed.');
381
+ }
382
+
383
+ let entries = inflight.get(owner);
384
+
385
+ if (entries === undefined) {
386
+ entries = new Map();
387
+ inflight.set(owner, entries);
388
+ }
389
+
390
+ if (entries.has(request.id)) {
391
+ return failure(request.id, 'invalid-request', `A request with id "${request.id}" is already running.`);
392
+ }
393
+
394
+ const entry: Entry = { helperId: null, cancelled: false, released: false };
395
+ entries.set(request.id, entry);
396
+
397
+ try {
398
+ return await route(request, owner, entry);
399
+ } catch (error) {
400
+ if (error instanceof Failure) {
401
+ return failure(request.id, error.code, error.message);
402
+ }
403
+
404
+ return failure(request.id, 'internal', messageOf(error));
405
+ } finally {
406
+ entries.delete(request.id);
407
+
408
+ if (entries.size === 0 && inflight.get(owner) === entries) {
409
+ inflight.delete(owner);
410
+ }
411
+ }
412
+ };
413
+
414
+ const release = async (owner: string): Promise<void> => {
415
+ const run = liveRun();
416
+ const closing: Promise<unknown>[] = [];
417
+
418
+ for (const entry of inflight.get(owner)?.values() ?? []) {
419
+ entry.cancelled = true;
420
+ entry.released = true;
421
+
422
+ if (entry.helperId !== null && run !== null) {
423
+ closing.push(dispatch(run, 'cancel', { request: entry.helperId }).response);
424
+ }
425
+ }
426
+
427
+ for (const [session, sessionOwner] of [...sessions]) {
428
+ if (sessionOwner !== owner) {
429
+ continue;
430
+ }
431
+
432
+ sessions.delete(session);
433
+
434
+ if (run !== null) {
435
+ closing.push(dispatch(run, 'close', { session }).response);
436
+ }
437
+ }
438
+
439
+ await settleWithin(closing);
440
+ };
441
+
442
+ const dispose = (): Promise<void> => {
443
+ disposal ??= (async () => {
444
+ disposed = true;
445
+ await Promise.all([...new Set([...inflight.keys(), ...sessions.values()])].map(release));
446
+
447
+ if (running !== null) {
448
+ abort(running, 'The database host has been disposed.');
449
+ }
450
+ })();
451
+
452
+ return disposal;
453
+ };
454
+
455
+ return { handle, release, dispose };
456
+ };
@@ -0,0 +1,5 @@
1
+ export { helperPath, type HelperPathOptions } from './helper-path.ts';
2
+ export { createDatabaseHost } from './host.ts';
3
+ export { spawnHelper, type SpawnHelperOptions } from './spawn.ts';
4
+ export type { DatabaseHost, DatabaseHostOptions, HelperProcess } from './types.ts';
5
+ export { parseRequest, type ParsedRequest } from './validate.ts';
@@ -0,0 +1,124 @@
1
+ import { spawn } from 'node:child_process';
2
+ import type { HelperProcess } from './types.ts';
3
+
4
+ export interface SpawnHelperOptions {
5
+ readonly args?: readonly string[];
6
+ /* Added over the environment of this process. */
7
+ readonly env?: Readonly<Record<string, string>>;
8
+ /* Receives each line the helper writes to stderr. */
9
+ readonly onLog?: (line: string) => void;
10
+ }
11
+
12
+ /*
13
+ * Cuts a stream into lines. A line can be megabytes, so pieces wait in a list and only the new chunk is
14
+ * searched for a break; joining a growing string on every chunk would make a large line quadratic.
15
+ */
16
+ const createLineSplitter = (onLine: (line: string) => void) => {
17
+ let pieces: string[] = [];
18
+
19
+ const emit = (line: string): void => {
20
+ const clean = line.endsWith('\r') ? line.slice(0, -1) : line;
21
+
22
+ if (clean.length > 0) {
23
+ onLine(clean);
24
+ }
25
+ };
26
+
27
+ return {
28
+ push(chunk: string): void {
29
+ let start = 0;
30
+ let end = chunk.indexOf('\n');
31
+
32
+ while (end !== -1) {
33
+ pieces.push(chunk.slice(start, end));
34
+ const line = pieces.join('');
35
+ pieces = [];
36
+ emit(line);
37
+ start = end + 1;
38
+ end = chunk.indexOf('\n', start);
39
+ }
40
+
41
+ if (start < chunk.length) {
42
+ pieces.push(chunk.slice(start));
43
+ }
44
+ },
45
+ flush(): void {
46
+ const line = pieces.join('');
47
+ pieces = [];
48
+ emit(line);
49
+ }
50
+ };
51
+ };
52
+
53
+ /* Runs the helper binary at `path`, speaking one JSON message per line over stdin and stdout. */
54
+ export const spawnHelper = (path: string, options: SpawnHelperOptions = {}): HelperProcess => {
55
+ const lineListeners: ((line: string) => void)[] = [];
56
+ const exitListeners: ((code: number | null) => void)[] = [];
57
+ let finished = false;
58
+ let exitCode: number | null = null;
59
+ let killed = false;
60
+
61
+ const child = spawn(path, [...(options.args ?? [])], {
62
+ stdio: ['pipe', 'pipe', 'pipe'],
63
+ env: options.env ? { ...process.env, ...options.env } : process.env,
64
+ windowsHide: true
65
+ });
66
+
67
+ const lines = createLineSplitter((line) => lineListeners.forEach((listener) => listener(line)));
68
+ const logs = createLineSplitter((line) => options.onLog?.(line));
69
+
70
+ child.stdout.setEncoding('utf8');
71
+ child.stderr.setEncoding('utf8');
72
+ child.stdout.on('data', (chunk: string) => lines.push(chunk));
73
+ child.stderr.on('data', (chunk: string) => logs.push(chunk));
74
+ child.stdin.on('error', () => {});
75
+
76
+ const finish = (code: number | null): void => {
77
+ if (finished) {
78
+ return;
79
+ }
80
+
81
+ finished = true;
82
+ exitCode = code;
83
+ lines.flush();
84
+ logs.flush();
85
+ exitListeners.forEach((listener) => listener(code));
86
+ };
87
+
88
+ child.on('error', (error) => {
89
+ options.onLog?.(error.message);
90
+
91
+ // Without a pid the process never started, and no `close` follows.
92
+ if (child.pid === undefined) {
93
+ finish(null);
94
+ }
95
+ });
96
+ child.on('close', (code) => finish(code));
97
+
98
+ return {
99
+ write(line: string): void {
100
+ if (!finished && child.stdin.writable) {
101
+ child.stdin.write(`${line}\n`);
102
+ }
103
+ },
104
+ onLine(listener): void {
105
+ lineListeners.push(listener);
106
+ },
107
+ onExit(listener): void {
108
+ if (finished) {
109
+ queueMicrotask(() => listener(exitCode));
110
+ return;
111
+ }
112
+
113
+ exitListeners.push(listener);
114
+ },
115
+ kill(): void {
116
+ if (killed || finished) {
117
+ return;
118
+ }
119
+
120
+ killed = true;
121
+ child.kill();
122
+ }
123
+ };
124
+ };
@@ -0,0 +1,46 @@
1
+ import type { ConnectionConfig, DatabaseResponse } from '../protocol/index.ts';
2
+
3
+ /* A running helper as the host sees it: lines in, lines out. `spawnHelper` makes one from a path. */
4
+ export interface HelperProcess {
5
+ /* One message, without its line break. */
6
+ write(line: string): void;
7
+ onLine(listener: (line: string) => void): void;
8
+ onExit(listener: (code: number | null) => void): void;
9
+ kill(): void;
10
+ }
11
+
12
+ export interface DatabaseHostOptions {
13
+ /* Starts a helper; called on the first request, and again on the first request after one exited. */
14
+ start(): HelperProcess;
15
+ /*
16
+ * The app's say over which connections a page may open, asked on every `open` and `test`. A
17
+ * connection it turns down fails with `forbidden`. Every connection is allowed when left out.
18
+ */
19
+ authorize?(connection: ConnectionConfig, owner: string): boolean | Promise<boolean>;
20
+ /*
21
+ * The app's say over the files a page may export to or import from, asked on every `export`,
22
+ * `import` and `sample`. Every file is refused when left out, since the page names the path.
23
+ */
24
+ authorizeFile?(path: string, access: 'read' | 'write', owner: string): boolean | Promise<boolean>;
25
+ /*
26
+ * The app's say over `discover`, which hands container credentials to the page. Refused when left out,
27
+ * as with `authorizeFile`.
28
+ */
29
+ authorizeDiscovery?(kind: 'docker', owner: string): boolean | Promise<boolean>;
30
+ /* How long to wait for the helper's `ready` line. 10000 when left out. */
31
+ readyTimeoutMs?: number;
32
+ }
33
+
34
+ /*
35
+ * The part of the backend between the app's channel and the helper. It registers no channel itself:
36
+ * the app checks the sender, then calls `handle` with an owner that names it (a window, a socket).
37
+ * A session belongs to the owner that opened it, and no other owner can use or cancel it.
38
+ */
39
+ export interface DatabaseHost {
40
+ /* Takes what came over the channel as it is, and checks its shape before anything reaches the helper. */
41
+ handle(request: unknown, owner: string): Promise<DatabaseResponse>;
42
+ /* Closes every session of an owner that went away. */
43
+ release(owner: string): Promise<void>;
44
+ /* Closes every session and stops the helper. */
45
+ dispose(): Promise<void>;
46
+ }