@fgv/ts-web-extras 5.0.2 → 5.1.0-1

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 (383) hide show
  1. package/.rush/temp/chunked-rush-logs/ts-web-extras.build.chunks.jsonl +58 -25
  2. package/.rush/temp/chunked-rush-logs/ts-web-extras.test.chunks.jsonl +70 -0
  3. package/.rush/temp/operation/build/all.log +58 -25
  4. package/.rush/temp/operation/build/error.log +18 -0
  5. package/.rush/temp/operation/build/log-chunks.jsonl +58 -25
  6. package/.rush/temp/operation/build/state.json +1 -1
  7. package/.rush/temp/operation/test/all.log +70 -0
  8. package/.rush/temp/operation/test/error.log +16 -0
  9. package/.rush/temp/operation/test/log-chunks.jsonl +70 -0
  10. package/.rush/temp/operation/test/state.json +3 -0
  11. package/.rush/temp/shrinkwrap-deps.json +175 -163
  12. package/config/jest.config.json +4 -1
  13. package/config/typedoc.json +6 -0
  14. package/dist/index.js +2 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/packlets/crypto-utils/browserCryptoProvider.js +254 -0
  17. package/dist/packlets/crypto-utils/browserCryptoProvider.js.map +1 -0
  18. package/dist/packlets/crypto-utils/browserHashProvider.js.map +1 -0
  19. package/dist/packlets/{crypto → crypto-utils}/index.js +1 -0
  20. package/dist/packlets/crypto-utils/index.js.map +1 -0
  21. package/dist/packlets/file-api-types/index.js +27 -3
  22. package/dist/packlets/file-api-types/index.js.map +1 -1
  23. package/dist/packlets/file-tree/directoryHandleStore.js +124 -0
  24. package/dist/packlets/file-tree/directoryHandleStore.js.map +1 -0
  25. package/dist/packlets/file-tree/fileApiTreeAccessors.js +91 -0
  26. package/dist/packlets/file-tree/fileApiTreeAccessors.js.map +1 -1
  27. package/dist/packlets/file-tree/fileSystemAccessTreeAccessors.js +414 -0
  28. package/dist/packlets/file-tree/fileSystemAccessTreeAccessors.js.map +1 -0
  29. package/dist/packlets/file-tree/httpTreeAccessors.js +279 -0
  30. package/dist/packlets/file-tree/httpTreeAccessors.js.map +1 -0
  31. package/dist/packlets/file-tree/index.js +4 -0
  32. package/dist/packlets/file-tree/index.js.map +1 -1
  33. package/dist/packlets/file-tree/localStorageTreeAccessors.js +359 -0
  34. package/dist/packlets/file-tree/localStorageTreeAccessors.js.map +1 -0
  35. package/dist/test/mocks/idb-keyval.js +6 -0
  36. package/dist/test/mocks/idb-keyval.js.map +1 -0
  37. package/dist/test/unit/browserHashProvider.test.js +1 -1
  38. package/dist/test/unit/browserHashProvider.test.js.map +1 -1
  39. package/dist/test/unit/directoryHandleStore.test.js +190 -0
  40. package/dist/test/unit/directoryHandleStore.test.js.map +1 -0
  41. package/dist/test/unit/fileApiTreeAccessors.test.js +51 -0
  42. package/dist/test/unit/fileApiTreeAccessors.test.js.map +1 -1
  43. package/dist/test/unit/fileApiTypes.test.js +30 -0
  44. package/dist/test/unit/fileApiTypes.test.js.map +1 -1
  45. package/dist/test/unit/fileSystemAccessTreeAccessors.test.js +622 -0
  46. package/dist/test/unit/fileSystemAccessTreeAccessors.test.js.map +1 -0
  47. package/dist/test/unit/httpTreeAccessors.test.js +1000 -0
  48. package/dist/test/unit/httpTreeAccessors.test.js.map +1 -0
  49. package/dist/test/unit/localStorageTreeAccessors.test.js +812 -0
  50. package/dist/test/unit/localStorageTreeAccessors.test.js.map +1 -0
  51. package/dist/test/utils/fileSystemAccessMocks.js +271 -0
  52. package/dist/test/utils/fileSystemAccessMocks.js.map +1 -0
  53. package/dist/ts-web-extras.d.ts +584 -1
  54. package/dist/tsdoc-metadata.json +1 -1
  55. package/docs/@fgv/namespaces/CryptoUtils/README.md +18 -0
  56. package/docs/@fgv/namespaces/CryptoUtils/classes/BrowserCryptoProvider.md +203 -0
  57. package/docs/@fgv/namespaces/CryptoUtils/classes/BrowserHashProvider.md +63 -0
  58. package/docs/@fgv/namespaces/CryptoUtils/functions/createBrowserCryptoProvider.md +18 -0
  59. package/docs/@fgv/namespaces/FileTreeHelpers/README.md +19 -0
  60. package/docs/@fgv/namespaces/FileTreeHelpers/functions/extractFileListMetadata.md +23 -0
  61. package/docs/@fgv/namespaces/FileTreeHelpers/functions/extractFileMetadata.md +23 -0
  62. package/docs/@fgv/namespaces/FileTreeHelpers/functions/fromDirectoryUpload.md +33 -0
  63. package/docs/@fgv/namespaces/FileTreeHelpers/functions/fromFileList.md +33 -0
  64. package/docs/@fgv/namespaces/FileTreeHelpers/functions/getOriginalFile.md +25 -0
  65. package/docs/@fgv/namespaces/FileTreeHelpers/variables/defaultFileApiTreeInitParams.md +11 -0
  66. package/docs/README.md +78 -0
  67. package/docs/classes/DirectoryHandleStore.md +116 -0
  68. package/docs/classes/FileApiTreeAccessors.md +286 -0
  69. package/docs/classes/FileSystemAccessTreeAccessors.md +557 -0
  70. package/docs/classes/HttpTreeAccessors.md +508 -0
  71. package/docs/classes/LocalStorageTreeAccessors.md +520 -0
  72. package/docs/functions/exportAsJson.md +23 -0
  73. package/docs/functions/exportUsingFileSystemAPI.md +26 -0
  74. package/docs/functions/extractDirectoryPath.md +23 -0
  75. package/docs/functions/isDirectoryHandle.md +23 -0
  76. package/docs/functions/isFileHandle.md +23 -0
  77. package/docs/functions/isFilePath.md +21 -0
  78. package/docs/functions/parseContextFilter.md +22 -0
  79. package/docs/functions/parseQualifierDefaults.md +22 -0
  80. package/docs/functions/parseResourceTypes.md +22 -0
  81. package/docs/functions/parseUrlParameters.md +15 -0
  82. package/docs/functions/safeShowDirectoryPicker.md +24 -0
  83. package/docs/functions/safeShowOpenFilePicker.md +24 -0
  84. package/docs/functions/safeShowSaveFilePicker.md +24 -0
  85. package/docs/functions/supportsFileSystemAccess.md +23 -0
  86. package/docs/interfaces/FilePickerAcceptType.md +16 -0
  87. package/docs/interfaces/FileSystemCreateWritableOptions.md +15 -0
  88. package/docs/interfaces/FileSystemDirectoryHandle.md +187 -0
  89. package/docs/interfaces/FileSystemFileHandle.md +106 -0
  90. package/docs/interfaces/FileSystemGetDirectoryOptions.md +15 -0
  91. package/docs/interfaces/FileSystemGetFileOptions.md +15 -0
  92. package/docs/interfaces/FileSystemHandle.md +69 -0
  93. package/docs/interfaces/FileSystemHandlePermissionDescriptor.md +15 -0
  94. package/docs/interfaces/FileSystemRemoveOptions.md +15 -0
  95. package/docs/interfaces/FileSystemWritableFileStream.md +127 -0
  96. package/docs/interfaces/IDirectoryHandleTreeInitializer.md +17 -0
  97. package/docs/interfaces/IFileHandleTreeInitializer.md +16 -0
  98. package/docs/interfaces/IFileListTreeInitializer.md +15 -0
  99. package/docs/interfaces/IFileMetadata.md +19 -0
  100. package/docs/interfaces/IFileSystemAccessTreeParams.md +30 -0
  101. package/docs/interfaces/IFsAccessApis.md +57 -0
  102. package/docs/interfaces/IHttpTreeParams.md +32 -0
  103. package/docs/interfaces/ILocalStorageTreeParams.md +30 -0
  104. package/docs/interfaces/IUrlConfigOptions.md +27 -0
  105. package/docs/interfaces/ShowDirectoryPickerOptions.md +17 -0
  106. package/docs/interfaces/ShowOpenFilePickerOptions.md +19 -0
  107. package/docs/interfaces/ShowSaveFilePickerOptions.md +19 -0
  108. package/docs/type-aliases/TreeInitializer.md +11 -0
  109. package/docs/type-aliases/WellKnownDirectory.md +11 -0
  110. package/docs/type-aliases/WindowWithFsAccess.md +11 -0
  111. package/docs/variables/DEFAULT_DIRECTORY_HANDLE_DB.md +11 -0
  112. package/docs/variables/DEFAULT_DIRECTORY_HANDLE_STORE.md +11 -0
  113. package/etc/ts-web-extras.api.md +124 -1
  114. package/lib/index.d.ts +2 -1
  115. package/lib/index.d.ts.map +1 -1
  116. package/lib/index.js +25 -2
  117. package/lib/index.js.map +1 -1
  118. package/lib/packlets/crypto-utils/browserCryptoProvider.d.ts +77 -0
  119. package/lib/packlets/crypto-utils/browserCryptoProvider.d.ts.map +1 -0
  120. package/lib/packlets/crypto-utils/browserCryptoProvider.js +259 -0
  121. package/lib/packlets/crypto-utils/browserCryptoProvider.js.map +1 -0
  122. package/lib/packlets/crypto-utils/browserHashProvider.d.ts.map +1 -0
  123. package/lib/packlets/crypto-utils/browserHashProvider.js.map +1 -0
  124. package/lib/packlets/{crypto → crypto-utils}/index.d.ts +1 -0
  125. package/lib/packlets/crypto-utils/index.d.ts.map +1 -0
  126. package/lib/packlets/{crypto → crypto-utils}/index.js +1 -0
  127. package/lib/packlets/crypto-utils/index.js.map +1 -0
  128. package/lib/packlets/file-api-types/index.d.ts.map +1 -1
  129. package/lib/packlets/file-api-types/index.js +27 -3
  130. package/lib/packlets/file-api-types/index.js.map +1 -1
  131. package/lib/packlets/file-tree/directoryHandleStore.d.ts +59 -0
  132. package/lib/packlets/file-tree/directoryHandleStore.d.ts.map +1 -0
  133. package/lib/packlets/file-tree/directoryHandleStore.js +128 -0
  134. package/lib/packlets/file-tree/directoryHandleStore.js.map +1 -0
  135. package/lib/packlets/file-tree/fileApiTreeAccessors.d.ts +66 -0
  136. package/lib/packlets/file-tree/fileApiTreeAccessors.d.ts.map +1 -1
  137. package/lib/packlets/file-tree/fileApiTreeAccessors.js +91 -0
  138. package/lib/packlets/file-tree/fileApiTreeAccessors.js.map +1 -1
  139. package/lib/packlets/file-tree/fileSystemAccessTreeAccessors.d.ts +152 -0
  140. package/lib/packlets/file-tree/fileSystemAccessTreeAccessors.d.ts.map +1 -0
  141. package/lib/packlets/file-tree/fileSystemAccessTreeAccessors.js +418 -0
  142. package/lib/packlets/file-tree/fileSystemAccessTreeAccessors.js.map +1 -0
  143. package/lib/packlets/file-tree/httpTreeAccessors.d.ts +88 -0
  144. package/lib/packlets/file-tree/httpTreeAccessors.d.ts.map +1 -0
  145. package/lib/packlets/file-tree/httpTreeAccessors.js +283 -0
  146. package/lib/packlets/file-tree/httpTreeAccessors.js.map +1 -0
  147. package/lib/packlets/file-tree/index.d.ts +4 -0
  148. package/lib/packlets/file-tree/index.d.ts.map +1 -1
  149. package/lib/packlets/file-tree/index.js +4 -0
  150. package/lib/packlets/file-tree/index.js.map +1 -1
  151. package/lib/packlets/file-tree/localStorageTreeAccessors.d.ts +141 -0
  152. package/lib/packlets/file-tree/localStorageTreeAccessors.d.ts.map +1 -0
  153. package/lib/packlets/file-tree/localStorageTreeAccessors.js +363 -0
  154. package/lib/packlets/file-tree/localStorageTreeAccessors.js.map +1 -0
  155. package/lib/test/mocks/idb-keyval.d.ts +6 -0
  156. package/lib/test/mocks/idb-keyval.d.ts.map +1 -0
  157. package/lib/test/mocks/idb-keyval.js +9 -0
  158. package/lib/test/mocks/idb-keyval.js.map +1 -0
  159. package/lib/test/unit/browserHashProvider.test.js +21 -21
  160. package/lib/test/unit/browserHashProvider.test.js.map +1 -1
  161. package/lib/test/unit/directoryHandleStore.test.d.ts +2 -0
  162. package/lib/test/unit/directoryHandleStore.test.d.ts.map +1 -0
  163. package/lib/test/unit/directoryHandleStore.test.js +192 -0
  164. package/lib/test/unit/directoryHandleStore.test.js.map +1 -0
  165. package/lib/test/unit/fileApiTreeAccessors.test.js +51 -0
  166. package/lib/test/unit/fileApiTreeAccessors.test.js.map +1 -1
  167. package/lib/test/unit/fileApiTypes.test.js +30 -0
  168. package/lib/test/unit/fileApiTypes.test.js.map +1 -1
  169. package/lib/test/unit/fileSystemAccessTreeAccessors.test.d.ts +2 -0
  170. package/lib/test/unit/fileSystemAccessTreeAccessors.test.d.ts.map +1 -0
  171. package/lib/test/unit/fileSystemAccessTreeAccessors.test.js +624 -0
  172. package/lib/test/unit/fileSystemAccessTreeAccessors.test.js.map +1 -0
  173. package/lib/test/unit/httpTreeAccessors.test.d.ts +2 -0
  174. package/lib/test/unit/httpTreeAccessors.test.d.ts.map +1 -0
  175. package/lib/test/unit/httpTreeAccessors.test.js +1002 -0
  176. package/lib/test/unit/httpTreeAccessors.test.js.map +1 -0
  177. package/lib/test/unit/localStorageTreeAccessors.test.d.ts +2 -0
  178. package/lib/test/unit/localStorageTreeAccessors.test.d.ts.map +1 -0
  179. package/lib/test/unit/localStorageTreeAccessors.test.js +814 -0
  180. package/lib/test/unit/localStorageTreeAccessors.test.js.map +1 -0
  181. package/lib/test/utils/fileSystemAccessMocks.d.ts +53 -0
  182. package/lib/test/utils/fileSystemAccessMocks.d.ts.map +1 -0
  183. package/lib/test/utils/fileSystemAccessMocks.js +277 -0
  184. package/lib/test/utils/fileSystemAccessMocks.js.map +1 -0
  185. package/package.json +41 -34
  186. package/rush-logs/ts-web-extras.build.cache.log +0 -1
  187. package/rush-logs/ts-web-extras.build.error.log +18 -0
  188. package/rush-logs/ts-web-extras.build.log +58 -25
  189. package/rush-logs/ts-web-extras.test.cache.log +1 -0
  190. package/rush-logs/ts-web-extras.test.error.log +16 -0
  191. package/rush-logs/ts-web-extras.test.log +70 -0
  192. package/src/index.ts +2 -2
  193. package/src/packlets/crypto-utils/browserCryptoProvider.ts +311 -0
  194. package/src/packlets/{crypto → crypto-utils}/index.ts +1 -0
  195. package/src/packlets/file-api-types/index.ts +24 -3
  196. package/src/packlets/file-tree/directoryHandleStore.ts +136 -0
  197. package/src/packlets/file-tree/fileApiTreeAccessors.ts +108 -0
  198. package/src/packlets/file-tree/fileSystemAccessTreeAccessors.ts +519 -0
  199. package/src/packlets/file-tree/httpTreeAccessors.ts +381 -0
  200. package/src/packlets/file-tree/index.ts +4 -0
  201. package/src/packlets/file-tree/localStorageTreeAccessors.ts +430 -0
  202. package/src/test/mocks/idb-keyval.ts +5 -0
  203. package/src/test/unit/browserHashProvider.test.ts +1 -1
  204. package/src/test/unit/directoryHandleStore.test.ts +251 -0
  205. package/src/test/unit/fileApiTreeAccessors.test.ts +69 -0
  206. package/src/test/unit/fileApiTypes.test.ts +36 -0
  207. package/src/test/unit/fileSystemAccessTreeAccessors.test.ts +885 -0
  208. package/src/test/unit/httpTreeAccessors.test.ts +1278 -0
  209. package/src/test/unit/localStorageTreeAccessors.test.ts +1014 -0
  210. package/src/test/utils/fileSystemAccessMocks.ts +353 -0
  211. package/temp/build/typescript/ts_8nwakTlr.json +1 -0
  212. package/temp/coverage/crypto/browserHashProvider.ts.html +1 -1
  213. package/temp/coverage/crypto/index.html +1 -1
  214. package/temp/coverage/crypto-utils/browserCryptoProvider.ts.html +1018 -0
  215. package/temp/coverage/crypto-utils/browserHashProvider.ts.html +304 -0
  216. package/temp/coverage/crypto-utils/index.html +131 -0
  217. package/temp/coverage/file-tree/directoryHandleStore.ts.html +493 -0
  218. package/temp/coverage/file-tree/fileApiTreeAccessors.ts.html +330 -6
  219. package/temp/coverage/file-tree/fileSystemAccessTreeAccessors.ts.html +1642 -0
  220. package/temp/coverage/file-tree/httpTreeAccessors.ts.html +1228 -0
  221. package/temp/coverage/file-tree/index.html +69 -9
  222. package/temp/coverage/file-tree/localStorageTreeAccessors.ts.html +1375 -0
  223. package/temp/coverage/helpers/fileTreeHelpers.ts.html +1 -1
  224. package/temp/coverage/helpers/index.html +1 -1
  225. package/temp/coverage/index.html +13 -13
  226. package/temp/coverage/lcov-report/crypto/browserHashProvider.ts.html +1 -1
  227. package/temp/coverage/lcov-report/crypto/index.html +1 -1
  228. package/temp/coverage/lcov-report/crypto-utils/browserCryptoProvider.ts.html +1018 -0
  229. package/temp/coverage/lcov-report/crypto-utils/browserHashProvider.ts.html +304 -0
  230. package/temp/coverage/lcov-report/crypto-utils/index.html +131 -0
  231. package/temp/coverage/lcov-report/file-tree/directoryHandleStore.ts.html +493 -0
  232. package/temp/coverage/lcov-report/file-tree/fileApiTreeAccessors.ts.html +330 -6
  233. package/temp/coverage/lcov-report/file-tree/fileSystemAccessTreeAccessors.ts.html +1642 -0
  234. package/temp/coverage/lcov-report/file-tree/httpTreeAccessors.ts.html +1228 -0
  235. package/temp/coverage/lcov-report/file-tree/index.html +69 -9
  236. package/temp/coverage/lcov-report/file-tree/localStorageTreeAccessors.ts.html +1375 -0
  237. package/temp/coverage/lcov-report/helpers/fileTreeHelpers.ts.html +1 -1
  238. package/temp/coverage/lcov-report/helpers/index.html +1 -1
  239. package/temp/coverage/lcov-report/index.html +13 -13
  240. package/temp/coverage/lcov-report/url-utils/index.html +1 -1
  241. package/temp/coverage/lcov-report/url-utils/urlParams.ts.html +1 -1
  242. package/temp/coverage/lcov.info +2829 -428
  243. package/temp/coverage/url-utils/index.html +1 -1
  244. package/temp/coverage/url-utils/urlParams.ts.html +1 -1
  245. package/temp/test/jest/haste-map-b931e4e63102f86c5bd4949f7dced44f-9d713eb41149188b4e5c0ae3d86d0a57-2ad8e16b24e391b8cdbe50b55c137169 +0 -0
  246. package/temp/test/jest/perf-cache-b931e4e63102f86c5bd4949f7dced44f-da39a3ee5e6b4b0d3255bfef95601890 +1 -0
  247. package/temp/ts-web-extras.api.json +5282 -1472
  248. package/temp/ts-web-extras.api.md +124 -1
  249. package/dist/packlets/crypto/browserHashProvider.js.map +0 -1
  250. package/dist/packlets/crypto/index.js.map +0 -1
  251. package/docs/index.md +0 -34
  252. package/docs/ts-web-extras.browserhashprovider.hashparts.md +0 -88
  253. package/docs/ts-web-extras.browserhashprovider.hashstring.md +0 -72
  254. package/docs/ts-web-extras.browserhashprovider.md +0 -66
  255. package/docs/ts-web-extras.exportasjson.md +0 -70
  256. package/docs/ts-web-extras.exportusingfilesystemapi.md +0 -104
  257. package/docs/ts-web-extras.extractdirectorypath.md +0 -52
  258. package/docs/ts-web-extras.fileapitreeaccessors.create.md +0 -72
  259. package/docs/ts-web-extras.fileapitreeaccessors.extractfilemetadata.md +0 -54
  260. package/docs/ts-web-extras.fileapitreeaccessors.fromdirectoryupload.md +0 -72
  261. package/docs/ts-web-extras.fileapitreeaccessors.fromfilelist.md +0 -72
  262. package/docs/ts-web-extras.fileapitreeaccessors.getoriginalfile.md +0 -72
  263. package/docs/ts-web-extras.fileapitreeaccessors.md +0 -114
  264. package/docs/ts-web-extras.filepickeraccepttype.accept.md +0 -11
  265. package/docs/ts-web-extras.filepickeraccepttype.description.md +0 -11
  266. package/docs/ts-web-extras.filepickeraccepttype.md +0 -75
  267. package/docs/ts-web-extras.filesystemcreatewritableoptions_2.keepexistingdata.md +0 -11
  268. package/docs/ts-web-extras.filesystemcreatewritableoptions_2.md +0 -58
  269. package/docs/ts-web-extras.filesystemdirectoryhandle_2._symbol.asynciterator_.md +0 -15
  270. package/docs/ts-web-extras.filesystemdirectoryhandle_2.entries.md +0 -15
  271. package/docs/ts-web-extras.filesystemdirectoryhandle_2.getdirectoryhandle.md +0 -66
  272. package/docs/ts-web-extras.filesystemdirectoryhandle_2.getfilehandle.md +0 -66
  273. package/docs/ts-web-extras.filesystemdirectoryhandle_2.keys.md +0 -15
  274. package/docs/ts-web-extras.filesystemdirectoryhandle_2.kind.md +0 -11
  275. package/docs/ts-web-extras.filesystemdirectoryhandle_2.md +0 -146
  276. package/docs/ts-web-extras.filesystemdirectoryhandle_2.removeentry.md +0 -66
  277. package/docs/ts-web-extras.filesystemdirectoryhandle_2.resolve.md +0 -50
  278. package/docs/ts-web-extras.filesystemdirectoryhandle_2.values.md +0 -15
  279. package/docs/ts-web-extras.filesystemfilehandle_2.createwritable.md +0 -52
  280. package/docs/ts-web-extras.filesystemfilehandle_2.getfile.md +0 -15
  281. package/docs/ts-web-extras.filesystemfilehandle_2.kind.md +0 -11
  282. package/docs/ts-web-extras.filesystemfilehandle_2.md +0 -92
  283. package/docs/ts-web-extras.filesystemgetdirectoryoptions_2.create.md +0 -11
  284. package/docs/ts-web-extras.filesystemgetdirectoryoptions_2.md +0 -58
  285. package/docs/ts-web-extras.filesystemgetfileoptions_2.create.md +0 -11
  286. package/docs/ts-web-extras.filesystemgetfileoptions_2.md +0 -58
  287. package/docs/ts-web-extras.filesystemhandle_2.issameentry.md +0 -50
  288. package/docs/ts-web-extras.filesystemhandle_2.kind.md +0 -11
  289. package/docs/ts-web-extras.filesystemhandle_2.md +0 -119
  290. package/docs/ts-web-extras.filesystemhandle_2.name.md +0 -11
  291. package/docs/ts-web-extras.filesystemhandle_2.querypermission.md +0 -52
  292. package/docs/ts-web-extras.filesystemhandle_2.requestpermission.md +0 -52
  293. package/docs/ts-web-extras.filesystemhandlepermissiondescriptor.md +0 -58
  294. package/docs/ts-web-extras.filesystemhandlepermissiondescriptor.mode.md +0 -11
  295. package/docs/ts-web-extras.filesystemremoveoptions_2.md +0 -58
  296. package/docs/ts-web-extras.filesystemremoveoptions_2.recursive.md +0 -11
  297. package/docs/ts-web-extras.filesystemwritablefilestream_2.md +0 -57
  298. package/docs/ts-web-extras.filesystemwritablefilestream_2.seek.md +0 -50
  299. package/docs/ts-web-extras.filesystemwritablefilestream_2.truncate.md +0 -50
  300. package/docs/ts-web-extras.filesystemwritablefilestream_2.write.md +0 -50
  301. package/docs/ts-web-extras.filetreehelpers.defaultfileapitreeinitparams.md +0 -13
  302. package/docs/ts-web-extras.filetreehelpers.extractfilelistmetadata.md +0 -56
  303. package/docs/ts-web-extras.filetreehelpers.extractfilemetadata.md +0 -56
  304. package/docs/ts-web-extras.filetreehelpers.fromdirectoryupload.md +0 -76
  305. package/docs/ts-web-extras.filetreehelpers.fromfilelist.md +0 -76
  306. package/docs/ts-web-extras.filetreehelpers.getoriginalfile.md +0 -72
  307. package/docs/ts-web-extras.filetreehelpers.md +0 -102
  308. package/docs/ts-web-extras.idirectoryhandletreeinitializer.dirhandles.md +0 -11
  309. package/docs/ts-web-extras.idirectoryhandletreeinitializer.md +0 -100
  310. package/docs/ts-web-extras.idirectoryhandletreeinitializer.nonrecursive.md +0 -11
  311. package/docs/ts-web-extras.idirectoryhandletreeinitializer.prefix.md +0 -11
  312. package/docs/ts-web-extras.ifilehandletreeinitializer.filehandles.md +0 -11
  313. package/docs/ts-web-extras.ifilehandletreeinitializer.md +0 -79
  314. package/docs/ts-web-extras.ifilehandletreeinitializer.prefix.md +0 -11
  315. package/docs/ts-web-extras.ifilelisttreeinitializer.filelist.md +0 -11
  316. package/docs/ts-web-extras.ifilelisttreeinitializer.md +0 -58
  317. package/docs/ts-web-extras.ifilemetadata.lastmodified.md +0 -11
  318. package/docs/ts-web-extras.ifilemetadata.md +0 -124
  319. package/docs/ts-web-extras.ifilemetadata.name.md +0 -11
  320. package/docs/ts-web-extras.ifilemetadata.path.md +0 -11
  321. package/docs/ts-web-extras.ifilemetadata.size.md +0 -11
  322. package/docs/ts-web-extras.ifilemetadata.type.md +0 -11
  323. package/docs/ts-web-extras.ifsaccessapis.md +0 -56
  324. package/docs/ts-web-extras.ifsaccessapis.showdirectorypicker.md +0 -52
  325. package/docs/ts-web-extras.ifsaccessapis.showopenfilepicker.md +0 -52
  326. package/docs/ts-web-extras.ifsaccessapis.showsavefilepicker.md +0 -52
  327. package/docs/ts-web-extras.isdirectoryhandle.md +0 -56
  328. package/docs/ts-web-extras.isfilehandle.md +0 -56
  329. package/docs/ts-web-extras.isfilepath.md +0 -52
  330. package/docs/ts-web-extras.iurlconfigoptions.config.md +0 -13
  331. package/docs/ts-web-extras.iurlconfigoptions.configstartdir.md +0 -13
  332. package/docs/ts-web-extras.iurlconfigoptions.contextfilter.md +0 -13
  333. package/docs/ts-web-extras.iurlconfigoptions.input.md +0 -13
  334. package/docs/ts-web-extras.iurlconfigoptions.inputstartdir.md +0 -13
  335. package/docs/ts-web-extras.iurlconfigoptions.interactive.md +0 -13
  336. package/docs/ts-web-extras.iurlconfigoptions.loadzip.md +0 -13
  337. package/docs/ts-web-extras.iurlconfigoptions.maxdistance.md +0 -13
  338. package/docs/ts-web-extras.iurlconfigoptions.md +0 -286
  339. package/docs/ts-web-extras.iurlconfigoptions.qualifierdefaults.md +0 -13
  340. package/docs/ts-web-extras.iurlconfigoptions.reducequalifiers.md +0 -13
  341. package/docs/ts-web-extras.iurlconfigoptions.resourcetypes.md +0 -13
  342. package/docs/ts-web-extras.iurlconfigoptions.zipfile.md +0 -13
  343. package/docs/ts-web-extras.iurlconfigoptions.zippath.md +0 -13
  344. package/docs/ts-web-extras.md +0 -512
  345. package/docs/ts-web-extras.parsecontextfilter.md +0 -52
  346. package/docs/ts-web-extras.parsequalifierdefaults.md +0 -52
  347. package/docs/ts-web-extras.parseresourcetypes.md +0 -52
  348. package/docs/ts-web-extras.parseurlparameters.md +0 -17
  349. package/docs/ts-web-extras.safeshowdirectorypicker.md +0 -72
  350. package/docs/ts-web-extras.safeshowopenfilepicker.md +0 -72
  351. package/docs/ts-web-extras.safeshowsavefilepicker.md +0 -72
  352. package/docs/ts-web-extras.showdirectorypickeroptions.id.md +0 -11
  353. package/docs/ts-web-extras.showdirectorypickeroptions.md +0 -96
  354. package/docs/ts-web-extras.showdirectorypickeroptions.mode.md +0 -11
  355. package/docs/ts-web-extras.showdirectorypickeroptions.startin.md +0 -11
  356. package/docs/ts-web-extras.showopenfilepickeroptions.excludeacceptalloption.md +0 -11
  357. package/docs/ts-web-extras.showopenfilepickeroptions.id.md +0 -11
  358. package/docs/ts-web-extras.showopenfilepickeroptions.md +0 -134
  359. package/docs/ts-web-extras.showopenfilepickeroptions.multiple.md +0 -11
  360. package/docs/ts-web-extras.showopenfilepickeroptions.startin.md +0 -11
  361. package/docs/ts-web-extras.showopenfilepickeroptions.types.md +0 -11
  362. package/docs/ts-web-extras.showsavefilepickeroptions.excludeacceptalloption.md +0 -11
  363. package/docs/ts-web-extras.showsavefilepickeroptions.id.md +0 -11
  364. package/docs/ts-web-extras.showsavefilepickeroptions.md +0 -134
  365. package/docs/ts-web-extras.showsavefilepickeroptions.startin.md +0 -11
  366. package/docs/ts-web-extras.showsavefilepickeroptions.suggestedname.md +0 -11
  367. package/docs/ts-web-extras.showsavefilepickeroptions.types.md +0 -11
  368. package/docs/ts-web-extras.supportsfilesystemaccess.md +0 -56
  369. package/docs/ts-web-extras.treeinitializer.md +0 -15
  370. package/docs/ts-web-extras.wellknowndirectory.md +0 -13
  371. package/docs/ts-web-extras.windowwithfsaccess.md +0 -15
  372. package/lib/packlets/crypto/browserHashProvider.d.ts.map +0 -1
  373. package/lib/packlets/crypto/browserHashProvider.js.map +0 -1
  374. package/lib/packlets/crypto/index.d.ts.map +0 -1
  375. package/lib/packlets/crypto/index.js.map +0 -1
  376. package/temp/build/typescript/ts_vnCx6LlY.json +0 -1
  377. package/temp/test/jest/haste-map-7492f1b44480e0cdd1f220078fb3afd8-c8dd6c3430605adeb2f1cadf4f75e791-8c9336785555d572065b28c111982ba4 +0 -0
  378. package/temp/test/jest/perf-cache-7492f1b44480e0cdd1f220078fb3afd8-da39a3ee5e6b4b0d3255bfef95601890 +0 -1
  379. /package/dist/packlets/{crypto → crypto-utils}/browserHashProvider.js +0 -0
  380. /package/lib/packlets/{crypto → crypto-utils}/browserHashProvider.d.ts +0 -0
  381. /package/lib/packlets/{crypto → crypto-utils}/browserHashProvider.js +0 -0
  382. /package/src/packlets/{crypto → crypto-utils}/browserHashProvider.ts +0 -0
  383. /package/temp/test/jest/{jest-transform-cache-7492f1b44480e0cdd1f220078fb3afd8-79ef2876fae7ca75eedb2aa53dc48338/0e/package_0eb6535f5987849d93ea51ef33a14cf6 → jest-transform-cache-b931e4e63102f86c5bd4949f7dced44f-79ef2876fae7ca75eedb2aa53dc48338/b5/package_b5f57afc9ec2c011239b1608ee5bdfa5} +0 -0
@@ -0,0 +1,1002 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright (c) 2026 Erik Fortune
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in all
13
+ * copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ * SOFTWARE.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ require("@fgv/ts-utils-jest");
25
+ const ts_utils_1 = require("@fgv/ts-utils");
26
+ const file_tree_1 = require("../../packlets/file-tree");
27
+ function makeMockResponse(options) {
28
+ const { ok, status = ok ? 200 : 400, jsonValue, textValue, throwOnJson, throwOnText } = options;
29
+ return {
30
+ ok,
31
+ status,
32
+ json: throwOnJson
33
+ ? () => Promise.reject(new Error('JSON parse error'))
34
+ : () => Promise.resolve(jsonValue),
35
+ text: throwOnText
36
+ ? () => Promise.reject(new Error('text error'))
37
+ : () => Promise.resolve(textValue !== null && textValue !== void 0 ? textValue : `HTTP ${status}`)
38
+ };
39
+ }
40
+ /**
41
+ * Creates a mock fetch function that returns responses for each call in order.
42
+ * Each entry maps to one fetch() invocation in call order.
43
+ */
44
+ function makeMockFetch(responses) {
45
+ const calls = [];
46
+ let callIndex = 0;
47
+ const fetchImpl = (url, init) => {
48
+ calls.push({ url: url.toString(), init });
49
+ const response = responses[callIndex++];
50
+ if (response === undefined) {
51
+ return Promise.reject(new Error(`Unexpected fetch call #${callIndex} to ${url.toString()}`));
52
+ }
53
+ return Promise.resolve(makeMockResponse(response));
54
+ };
55
+ return { fetchImpl: fetchImpl, calls };
56
+ }
57
+ /**
58
+ * Creates a mock fetch function that throws a network error on every call.
59
+ */
60
+ function makeThrowingFetch(message) {
61
+ return (_url, _init) => {
62
+ return Promise.reject(new Error(message));
63
+ };
64
+ }
65
+ // ---- Shared test data builders ----
66
+ /** Minimal tree-children response for a single file at root. */
67
+ function rootWithOneFile(fileName = 'data.json') {
68
+ return {
69
+ path: '/',
70
+ children: [{ path: `/${fileName}`, name: fileName, type: 'file' }]
71
+ };
72
+ }
73
+ /** File response for a JSON file. */
74
+ function fileResponse(path, contents, contentType) {
75
+ const response = { path, contents };
76
+ if (contentType !== undefined) {
77
+ response.contentType = contentType;
78
+ }
79
+ return response;
80
+ }
81
+ /** Root children response containing a subdirectory and a file. */
82
+ function rootWithDirAndFile() {
83
+ return {
84
+ path: '/',
85
+ children: [
86
+ { path: '/subdir', name: 'subdir', type: 'directory' },
87
+ { path: '/root.json', name: 'root.json', type: 'file' }
88
+ ]
89
+ };
90
+ }
91
+ /** Children response for /subdir containing one file. */
92
+ function subdirWithOneFile() {
93
+ return {
94
+ path: '/subdir',
95
+ children: [{ path: '/subdir/nested.json', name: 'nested.json', type: 'file' }]
96
+ };
97
+ }
98
+ // ---- Tests ----
99
+ describe('HttpTreeAccessors', () => {
100
+ describe('fromHttp()', () => {
101
+ test('succeeds with an empty directory (no children)', async () => {
102
+ const { fetchImpl } = makeMockFetch([{ ok: true, jsonValue: { path: '/', children: [] } }]);
103
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
104
+ baseUrl: 'http://localhost:3000',
105
+ fetchImpl
106
+ });
107
+ expect(result).toSucceedAndSatisfy((accessors) => {
108
+ expect(accessors).toBeInstanceOf(file_tree_1.HttpTreeAccessors);
109
+ expect(accessors.isDirty()).toBe(false);
110
+ });
111
+ });
112
+ test('succeeds and loads a single file at root', async () => {
113
+ const { fetchImpl } = makeMockFetch([
114
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
115
+ { ok: true, jsonValue: fileResponse('/data.json', '{"items":{}}') }
116
+ ]);
117
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
118
+ baseUrl: 'http://localhost:3000',
119
+ fetchImpl,
120
+ mutable: true
121
+ });
122
+ expect(result).toSucceedAndSatisfy((accessors) => {
123
+ expect(accessors.getFileContents('/data.json')).toSucceedWith('{"items":{}}');
124
+ });
125
+ });
126
+ test('succeeds with multiple files at root', async () => {
127
+ const { fetchImpl } = makeMockFetch([
128
+ {
129
+ ok: true,
130
+ jsonValue: {
131
+ path: '/',
132
+ children: [
133
+ { path: '/alpha.json', name: 'alpha.json', type: 'file' },
134
+ { path: '/beta.json', name: 'beta.json', type: 'file' }
135
+ ]
136
+ }
137
+ },
138
+ { ok: true, jsonValue: fileResponse('/alpha.json', '"alpha"') },
139
+ { ok: true, jsonValue: fileResponse('/beta.json', '"beta"') }
140
+ ]);
141
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
142
+ baseUrl: 'http://localhost:3000',
143
+ fetchImpl,
144
+ mutable: true
145
+ });
146
+ expect(result).toSucceedAndSatisfy((accessors) => {
147
+ expect(accessors.getFileContents('/alpha.json')).toSucceedWith('"alpha"');
148
+ expect(accessors.getFileContents('/beta.json')).toSucceedWith('"beta"');
149
+ });
150
+ });
151
+ test('succeeds and recursively loads files in subdirectories', async () => {
152
+ const { fetchImpl } = makeMockFetch([
153
+ { ok: true, jsonValue: rootWithDirAndFile() },
154
+ { ok: true, jsonValue: subdirWithOneFile() },
155
+ { ok: true, jsonValue: fileResponse('/subdir/nested.json', '"nested"') },
156
+ { ok: true, jsonValue: fileResponse('/root.json', '"root"') }
157
+ ]);
158
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
159
+ baseUrl: 'http://localhost:3000',
160
+ fetchImpl,
161
+ mutable: true
162
+ });
163
+ expect(result).toSucceedAndSatisfy((accessors) => {
164
+ expect(accessors.getFileContents('/subdir/nested.json')).toSucceedWith('"nested"');
165
+ expect(accessors.getFileContents('/root.json')).toSucceedWith('"root"');
166
+ });
167
+ });
168
+ test('strips trailing slash from baseUrl', async () => {
169
+ const { fetchImpl, calls } = makeMockFetch([{ ok: true, jsonValue: { path: '/', children: [] } }]);
170
+ await file_tree_1.HttpTreeAccessors.fromHttp({
171
+ baseUrl: 'http://localhost:3000/',
172
+ fetchImpl
173
+ });
174
+ // The URL should start with the base URL without a trailing slash, then /tree/children
175
+ expect(calls[0].url).toMatch(/^http:\/\/localhost:3000\/tree\/children/);
176
+ // There should be no double slash in the path portion (after the protocol)
177
+ const urlPath = calls[0].url.replace('http://', '');
178
+ expect(urlPath).not.toContain('//');
179
+ });
180
+ test('includes namespace in query parameters when provided', async () => {
181
+ const { fetchImpl, calls } = makeMockFetch([{ ok: true, jsonValue: { path: '/', children: [] } }]);
182
+ await file_tree_1.HttpTreeAccessors.fromHttp({
183
+ baseUrl: 'http://localhost:3000',
184
+ namespace: 'my-namespace',
185
+ fetchImpl
186
+ });
187
+ expect(calls[0].url).toContain('namespace=my-namespace');
188
+ });
189
+ test('omits namespace from query parameters when not provided', async () => {
190
+ const { fetchImpl, calls } = makeMockFetch([{ ok: true, jsonValue: { path: '/', children: [] } }]);
191
+ await file_tree_1.HttpTreeAccessors.fromHttp({
192
+ baseUrl: 'http://localhost:3000',
193
+ fetchImpl
194
+ });
195
+ expect(calls[0].url).not.toContain('namespace');
196
+ });
197
+ test('applies prefix parameter to loaded file paths', async () => {
198
+ const { fetchImpl } = makeMockFetch([
199
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
200
+ { ok: true, jsonValue: fileResponse('/data.json', '{"items":{}}') }
201
+ ]);
202
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
203
+ baseUrl: 'http://localhost:3000',
204
+ prefix: '/app',
205
+ fetchImpl,
206
+ mutable: true
207
+ });
208
+ expect(result).toSucceedAndSatisfy((accessors) => {
209
+ expect(accessors.getFileContents('/app/data.json')).toSucceedWith('{"items":{}}');
210
+ });
211
+ });
212
+ test('fails when the initial children fetch fails with a network error', async () => {
213
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
214
+ baseUrl: 'http://localhost:3000',
215
+ fetchImpl: makeThrowingFetch('Network unreachable')
216
+ });
217
+ expect(result).toFailWith(/network unreachable/i);
218
+ });
219
+ test('fails when the children response is not ok', async () => {
220
+ const { fetchImpl } = makeMockFetch([{ ok: false, status: 404, textValue: 'Not Found' }]);
221
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
222
+ baseUrl: 'http://localhost:3000',
223
+ fetchImpl
224
+ });
225
+ expect(result).toFailWith(/not found/i);
226
+ });
227
+ test('fails when the children response contains invalid JSON', async () => {
228
+ const { fetchImpl } = makeMockFetch([{ ok: true, throwOnJson: true }]);
229
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
230
+ baseUrl: 'http://localhost:3000',
231
+ fetchImpl
232
+ });
233
+ expect(result).toFailWith(/invalid json/i);
234
+ });
235
+ test('fails when a file fetch fails with a network error', async () => {
236
+ let callCount = 0;
237
+ const fetchImpl = (_url, _init) => {
238
+ callCount++;
239
+ if (callCount === 1) {
240
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
241
+ }
242
+ return Promise.reject(new Error('File fetch failed'));
243
+ };
244
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
245
+ baseUrl: 'http://localhost:3000',
246
+ fetchImpl
247
+ });
248
+ expect(result).toFailWith(/file fetch failed/i);
249
+ });
250
+ test('fails when a file response is not ok', async () => {
251
+ const { fetchImpl } = makeMockFetch([
252
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
253
+ { ok: false, status: 403, textValue: 'Forbidden' }
254
+ ]);
255
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
256
+ baseUrl: 'http://localhost:3000',
257
+ fetchImpl
258
+ });
259
+ expect(result).toFailWith(/forbidden/i);
260
+ });
261
+ test('fails when a file response contains invalid JSON', async () => {
262
+ const { fetchImpl } = makeMockFetch([
263
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
264
+ { ok: true, throwOnJson: true }
265
+ ]);
266
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
267
+ baseUrl: 'http://localhost:3000',
268
+ fetchImpl
269
+ });
270
+ expect(result).toFailWith(/invalid json/i);
271
+ });
272
+ test('fails when a nested directory fetch fails', async () => {
273
+ const { fetchImpl } = makeMockFetch([
274
+ { ok: true, jsonValue: rootWithDirAndFile() },
275
+ { ok: false, status: 500, textValue: 'Internal Server Error' }
276
+ ]);
277
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
278
+ baseUrl: 'http://localhost:3000',
279
+ fetchImpl
280
+ });
281
+ expect(result).toFailWith(/internal server error/i);
282
+ });
283
+ });
284
+ describe('isDirty() and getDirtyPaths()', () => {
285
+ test('starts not dirty after loading', async () => {
286
+ const { fetchImpl } = makeMockFetch([{ ok: true, jsonValue: { path: '/', children: [] } }]);
287
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({ baseUrl: 'http://localhost:3000', fetchImpl })).orThrow();
288
+ expect(accessors.isDirty()).toBe(false);
289
+ expect(accessors.getDirtyPaths()).toEqual([]);
290
+ });
291
+ test('becomes dirty after saveFileContents', async () => {
292
+ const { fetchImpl } = makeMockFetch([
293
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
294
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') }
295
+ ]);
296
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
297
+ baseUrl: 'http://localhost:3000',
298
+ fetchImpl,
299
+ mutable: true
300
+ })).orThrow();
301
+ accessors.saveFileContents('/data.json', '{"modified":true}').orThrow();
302
+ expect(accessors.isDirty()).toBe(true);
303
+ expect(accessors.getDirtyPaths()).toContain('/data.json');
304
+ });
305
+ test('tracks multiple dirty files', async () => {
306
+ const { fetchImpl } = makeMockFetch([
307
+ {
308
+ ok: true,
309
+ jsonValue: {
310
+ path: '/',
311
+ children: [
312
+ { path: '/a.json', name: 'a.json', type: 'file' },
313
+ { path: '/b.json', name: 'b.json', type: 'file' }
314
+ ]
315
+ }
316
+ },
317
+ { ok: true, jsonValue: fileResponse('/a.json', '{}') },
318
+ { ok: true, jsonValue: fileResponse('/b.json', '{}') }
319
+ ]);
320
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
321
+ baseUrl: 'http://localhost:3000',
322
+ fetchImpl,
323
+ mutable: true
324
+ })).orThrow();
325
+ accessors.saveFileContents('/a.json', '"a"').orThrow();
326
+ accessors.saveFileContents('/b.json', '"b"').orThrow();
327
+ expect(accessors.getDirtyPaths()).toHaveLength(2);
328
+ expect(accessors.getDirtyPaths()).toContain('/a.json');
329
+ expect(accessors.getDirtyPaths()).toContain('/b.json');
330
+ });
331
+ test('tracks deleted files as pending dirty paths', async () => {
332
+ const { fetchImpl } = makeMockFetch([
333
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
334
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') }
335
+ ]);
336
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
337
+ baseUrl: 'http://localhost:3000',
338
+ fetchImpl,
339
+ mutable: true
340
+ })).orThrow();
341
+ accessors.deleteFile('/data.json').orThrow();
342
+ expect(accessors.isDirty()).toBe(true);
343
+ expect(accessors.getDirtyPaths()).toContain('/data.json');
344
+ });
345
+ });
346
+ describe('saveFileContents()', () => {
347
+ test('fails when the file is not found (immutable tree has no matching path)', async () => {
348
+ const { fetchImpl } = makeMockFetch([{ ok: true, jsonValue: { path: '/', children: [] } }]);
349
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({ baseUrl: 'http://localhost:3000', fetchImpl })).orThrow();
350
+ // With mutable: false (default), saveFileContents should fail
351
+ const result = accessors.saveFileContents('/missing.json', '{}');
352
+ expect(result).toFail();
353
+ });
354
+ test('marks the file as dirty without autoSync', async () => {
355
+ const { fetchImpl } = makeMockFetch([
356
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
357
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') }
358
+ ]);
359
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
360
+ baseUrl: 'http://localhost:3000',
361
+ fetchImpl,
362
+ mutable: true
363
+ })).orThrow();
364
+ const result = accessors.saveFileContents('/data.json', '{"updated":true}');
365
+ expect(result).toSucceedWith('{"updated":true}');
366
+ expect(accessors.isDirty()).toBe(true);
367
+ // No additional fetch calls should have occurred (no autoSync)
368
+ });
369
+ test('triggers fire-and-forget autoSync when autoSync is enabled', async () => {
370
+ const syncResponses = [
371
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
372
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
373
+ // PUT /file response for sync
374
+ { ok: true, jsonValue: { path: '/data.json', contents: '{"auto":true}' } },
375
+ // POST /sync response
376
+ { ok: true, jsonValue: { synced: 1 } }
377
+ ];
378
+ const { fetchImpl, calls } = makeMockFetch(syncResponses);
379
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
380
+ baseUrl: 'http://localhost:3000',
381
+ fetchImpl,
382
+ mutable: true,
383
+ autoSync: true
384
+ })).orThrow();
385
+ const result = accessors.saveFileContents('/data.json', '{"auto":true}');
386
+ expect(result).toSucceedWith('{"auto":true}');
387
+ // autoSync fires-and-forgets; wait for microtasks to drain
388
+ await new Promise((resolve) => setTimeout(resolve, 0));
389
+ // Verify that PUT + POST /sync were called
390
+ const methodCalls = calls.slice(2).map((c) => { var _a; return (_a = c.init) === null || _a === void 0 ? void 0 : _a.method; });
391
+ expect(methodCalls).toContain('PUT');
392
+ expect(methodCalls).toContain('POST');
393
+ });
394
+ test('logs when autoSync returns a failure Result', async () => {
395
+ const logger = {
396
+ detail: jest.fn(),
397
+ info: jest.fn(),
398
+ warn: jest.fn(),
399
+ error: jest.fn()
400
+ };
401
+ const { fetchImpl } = makeMockFetch([
402
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
403
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') }
404
+ ]);
405
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
406
+ baseUrl: 'http://localhost:3000',
407
+ fetchImpl,
408
+ mutable: true,
409
+ autoSync: true,
410
+ logger
411
+ })).orThrow();
412
+ accessors.syncToDisk =
413
+ async () => (0, ts_utils_1.fail)('simulated sync failure');
414
+ accessors.saveFileContents('/data.json', '{"auto":true}').orThrow();
415
+ await new Promise((resolve) => setTimeout(resolve, 0));
416
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('simulated sync failure'));
417
+ });
418
+ test('logs when autoSync throws unexpectedly', async () => {
419
+ const logger = {
420
+ detail: jest.fn(),
421
+ info: jest.fn(),
422
+ warn: jest.fn(),
423
+ error: jest.fn()
424
+ };
425
+ const { fetchImpl } = makeMockFetch([
426
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
427
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') }
428
+ ]);
429
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
430
+ baseUrl: 'http://localhost:3000',
431
+ fetchImpl,
432
+ mutable: true,
433
+ autoSync: true,
434
+ logger
435
+ })).orThrow();
436
+ accessors.syncToDisk = async () => {
437
+ throw new Error('simulated throw');
438
+ };
439
+ accessors.saveFileContents('/data.json', '{"auto":true}').orThrow();
440
+ await new Promise((resolve) => setTimeout(resolve, 0));
441
+ expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('simulated throw'));
442
+ });
443
+ });
444
+ describe('fileIsMutable()', () => {
445
+ test('returns persistent detail when file exists and mutable is true', async () => {
446
+ const { fetchImpl } = makeMockFetch([
447
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
448
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') }
449
+ ]);
450
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
451
+ baseUrl: 'http://localhost:3000',
452
+ fetchImpl,
453
+ mutable: true
454
+ })).orThrow();
455
+ const result = accessors.fileIsMutable('/data.json');
456
+ expect(result.isSuccess()).toBe(true);
457
+ if (result.isSuccess()) {
458
+ expect(result.value).toBe(true);
459
+ expect(result.detail).toBe('persistent');
460
+ }
461
+ });
462
+ test('returns not-mutable detail when mutable is false', async () => {
463
+ const { fetchImpl } = makeMockFetch([
464
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
465
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') }
466
+ ]);
467
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
468
+ baseUrl: 'http://localhost:3000',
469
+ fetchImpl,
470
+ mutable: false
471
+ })).orThrow();
472
+ const result = accessors.fileIsMutable('/data.json');
473
+ expect(result.isFailure()).toBe(true);
474
+ if (result.isFailure()) {
475
+ expect(result.detail).toBe('not-mutable');
476
+ }
477
+ });
478
+ test('returns persistent detail for any path when mutable is true (no path existence check)', async () => {
479
+ // InMemoryTreeAccessors checks the mutable config, not path existence.
480
+ // HttpTreeAccessors layers "persistent" on top of a successful mutable check.
481
+ const { fetchImpl } = makeMockFetch([{ ok: true, jsonValue: { path: '/', children: [] } }]);
482
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
483
+ baseUrl: 'http://localhost:3000',
484
+ fetchImpl,
485
+ mutable: true
486
+ })).orThrow();
487
+ // Any path succeeds as "persistent" when mutable: true
488
+ const result = accessors.fileIsMutable('/nonexistent.json');
489
+ expect(result.isSuccess()).toBe(true);
490
+ if (result.isSuccess()) {
491
+ expect(result.value).toBe(true);
492
+ expect(result.detail).toBe('persistent');
493
+ }
494
+ });
495
+ });
496
+ describe('syncToDisk()', () => {
497
+ test('succeeds immediately with no dirty files', async () => {
498
+ const { fetchImpl } = makeMockFetch([{ ok: true, jsonValue: { path: '/', children: [] } }]);
499
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({ baseUrl: 'http://localhost:3000', fetchImpl })).orThrow();
500
+ const result = await accessors.syncToDisk();
501
+ expect(result).toSucceed();
502
+ });
503
+ test('PUTs each dirty file then POSTs /sync', async () => {
504
+ var _a, _b;
505
+ const { fetchImpl, calls } = makeMockFetch([
506
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
507
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
508
+ { ok: true, jsonValue: fileResponse('/data.json', '{"new":1}') },
509
+ { ok: true, jsonValue: { synced: 1 } }
510
+ ]);
511
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
512
+ baseUrl: 'http://localhost:3000',
513
+ fetchImpl,
514
+ mutable: true
515
+ })).orThrow();
516
+ accessors.saveFileContents('/data.json', '{"new":1}').orThrow();
517
+ const result = await accessors.syncToDisk();
518
+ expect(result).toSucceed();
519
+ const syncCalls = calls.slice(2);
520
+ expect(syncCalls[0].url).toContain('/file');
521
+ expect((_a = syncCalls[0].init) === null || _a === void 0 ? void 0 : _a.method).toBe('PUT');
522
+ expect(syncCalls[1].url).toContain('/sync');
523
+ expect((_b = syncCalls[1].init) === null || _b === void 0 ? void 0 : _b.method).toBe('POST');
524
+ });
525
+ test('clears dirty state after successful sync', async () => {
526
+ const { fetchImpl } = makeMockFetch([
527
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
528
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
529
+ { ok: true, jsonValue: fileResponse('/data.json', '"updated"') },
530
+ { ok: true, jsonValue: { synced: 1 } }
531
+ ]);
532
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
533
+ baseUrl: 'http://localhost:3000',
534
+ fetchImpl,
535
+ mutable: true
536
+ })).orThrow();
537
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
538
+ expect(accessors.isDirty()).toBe(true);
539
+ await accessors.syncToDisk();
540
+ expect(accessors.isDirty()).toBe(false);
541
+ expect(accessors.getDirtyPaths()).toEqual([]);
542
+ });
543
+ test('includes namespace in PUT body and POST /sync body when provided', async () => {
544
+ var _a, _b;
545
+ const { fetchImpl, calls } = makeMockFetch([
546
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
547
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
548
+ { ok: true, jsonValue: fileResponse('/data.json', '"v2"') },
549
+ { ok: true, jsonValue: { synced: 1 } }
550
+ ]);
551
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
552
+ baseUrl: 'http://localhost:3000',
553
+ namespace: 'myns',
554
+ fetchImpl,
555
+ mutable: true
556
+ })).orThrow();
557
+ accessors.saveFileContents('/data.json', '"v2"').orThrow();
558
+ await accessors.syncToDisk();
559
+ const putBody = JSON.parse((_a = calls[2].init) === null || _a === void 0 ? void 0 : _a.body);
560
+ expect(putBody.namespace).toBe('myns');
561
+ const syncBody = JSON.parse((_b = calls[3].init) === null || _b === void 0 ? void 0 : _b.body);
562
+ expect(syncBody.namespace).toBe('myns');
563
+ });
564
+ test('omits namespace from PUT body and POST /sync body when not provided', async () => {
565
+ var _a, _b;
566
+ const { fetchImpl, calls } = makeMockFetch([
567
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
568
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
569
+ { ok: true, jsonValue: fileResponse('/data.json', '"v2"') },
570
+ { ok: true, jsonValue: { synced: 1 } }
571
+ ]);
572
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
573
+ baseUrl: 'http://localhost:3000',
574
+ fetchImpl,
575
+ mutable: true
576
+ })).orThrow();
577
+ accessors.saveFileContents('/data.json', '"v2"').orThrow();
578
+ await accessors.syncToDisk();
579
+ const putBody = JSON.parse((_a = calls[2].init) === null || _a === void 0 ? void 0 : _a.body);
580
+ expect(putBody.namespace).toBeUndefined();
581
+ const syncBody = JSON.parse((_b = calls[3].init) === null || _b === void 0 ? void 0 : _b.body);
582
+ expect(syncBody.namespace).toBeUndefined();
583
+ });
584
+ test('fails when PUT for a dirty file returns a non-ok response', async () => {
585
+ const { fetchImpl } = makeMockFetch([
586
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
587
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
588
+ { ok: false, status: 500, textValue: 'Server Error' }
589
+ ]);
590
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
591
+ baseUrl: 'http://localhost:3000',
592
+ fetchImpl,
593
+ mutable: true
594
+ })).orThrow();
595
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
596
+ const result = await accessors.syncToDisk();
597
+ expect(result).toFailWith(/sync.*data\.json.*server error/i);
598
+ });
599
+ test('fails when PUT for a dirty file encounters a network error', async () => {
600
+ let callCount = 0;
601
+ const fetchImpl = (_url, _init) => {
602
+ callCount++;
603
+ if (callCount === 1) {
604
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
605
+ }
606
+ if (callCount === 2) {
607
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
608
+ }
609
+ return Promise.reject(new Error('PUT network error'));
610
+ };
611
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
612
+ baseUrl: 'http://localhost:3000',
613
+ fetchImpl,
614
+ mutable: true
615
+ })).orThrow();
616
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
617
+ const result = await accessors.syncToDisk();
618
+ expect(result).toFailWith(/put network error/i);
619
+ });
620
+ test('fails when POST /sync returns a non-ok response', async () => {
621
+ const { fetchImpl } = makeMockFetch([
622
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
623
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
624
+ { ok: true, jsonValue: fileResponse('/data.json', '"v2"') },
625
+ { ok: false, status: 503, textValue: 'Service Unavailable' }
626
+ ]);
627
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
628
+ baseUrl: 'http://localhost:3000',
629
+ fetchImpl,
630
+ mutable: true
631
+ })).orThrow();
632
+ accessors.saveFileContents('/data.json', '"v2"').orThrow();
633
+ const result = await accessors.syncToDisk();
634
+ expect(result).toFailWith(/service unavailable/i);
635
+ });
636
+ test('fails when POST /sync encounters a network error', async () => {
637
+ let callCount = 0;
638
+ const fetchImpl = (_url, _init) => {
639
+ callCount++;
640
+ if (callCount === 1) {
641
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
642
+ }
643
+ if (callCount === 2) {
644
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
645
+ }
646
+ if (callCount === 3) {
647
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '"v2"') }));
648
+ }
649
+ return Promise.reject(new Error('POST network error'));
650
+ };
651
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
652
+ baseUrl: 'http://localhost:3000',
653
+ fetchImpl,
654
+ mutable: true
655
+ })).orThrow();
656
+ accessors.saveFileContents('/data.json', '"v2"').orThrow();
657
+ const result = await accessors.syncToDisk();
658
+ expect(result).toFailWith(/post network error/i);
659
+ });
660
+ test('syncs multiple dirty files in order', async () => {
661
+ const { fetchImpl, calls } = makeMockFetch([
662
+ {
663
+ ok: true,
664
+ jsonValue: {
665
+ path: '/',
666
+ children: [
667
+ { path: '/a.json', name: 'a.json', type: 'file' },
668
+ { path: '/b.json', name: 'b.json', type: 'file' }
669
+ ]
670
+ }
671
+ },
672
+ { ok: true, jsonValue: fileResponse('/a.json', '{}') },
673
+ { ok: true, jsonValue: fileResponse('/b.json', '{}') },
674
+ { ok: true, jsonValue: fileResponse('/a.json', '"a-updated"') },
675
+ { ok: true, jsonValue: fileResponse('/b.json', '"b-updated"') },
676
+ { ok: true, jsonValue: { synced: 2 } }
677
+ ]);
678
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
679
+ baseUrl: 'http://localhost:3000',
680
+ fetchImpl,
681
+ mutable: true
682
+ })).orThrow();
683
+ accessors.saveFileContents('/a.json', '"a-updated"').orThrow();
684
+ accessors.saveFileContents('/b.json', '"b-updated"').orThrow();
685
+ const result = await accessors.syncToDisk();
686
+ expect(result).toSucceed();
687
+ // Verify 2 PUTs + 1 POST
688
+ const syncCalls = calls.slice(3);
689
+ const methods = syncCalls.map((c) => { var _a; return (_a = c.init) === null || _a === void 0 ? void 0 : _a.method; });
690
+ expect(methods.filter((m) => m === 'PUT')).toHaveLength(2);
691
+ expect(methods.filter((m) => m === 'POST')).toHaveLength(1);
692
+ });
693
+ test('DELETEs pending deletions before POST /sync', async () => {
694
+ var _a, _b;
695
+ const { fetchImpl, calls } = makeMockFetch([
696
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
697
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
698
+ { ok: true, jsonValue: { deleted: true } },
699
+ { ok: true, jsonValue: { synced: 0 } }
700
+ ]);
701
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
702
+ baseUrl: 'http://localhost:3000',
703
+ fetchImpl,
704
+ mutable: true
705
+ })).orThrow();
706
+ accessors.deleteFile('/data.json').orThrow();
707
+ const result = await accessors.syncToDisk();
708
+ expect(result).toSucceed();
709
+ const syncCalls = calls.slice(2);
710
+ expect((_a = syncCalls[0].init) === null || _a === void 0 ? void 0 : _a.method).toBe('DELETE');
711
+ expect(syncCalls[0].url).toContain('/file?');
712
+ expect((_b = syncCalls[1].init) === null || _b === void 0 ? void 0 : _b.method).toBe('POST');
713
+ expect(accessors.isDirty()).toBe(false);
714
+ });
715
+ test('fails when DELETE for pending deletion returns non-ok response', async () => {
716
+ const { fetchImpl } = makeMockFetch([
717
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
718
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
719
+ { ok: false, status: 500, textValue: 'Delete failed' }
720
+ ]);
721
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
722
+ baseUrl: 'http://localhost:3000',
723
+ fetchImpl,
724
+ mutable: true
725
+ })).orThrow();
726
+ accessors.deleteFile('/data.json').orThrow();
727
+ const result = await accessors.syncToDisk();
728
+ expect(result).toFailWith(/delete.*data\.json.*delete failed/i);
729
+ });
730
+ });
731
+ describe('syncToDisk() - getFileContents failure', () => {
732
+ test('fails when getFileContents returns failure for a dirty file during sync', async () => {
733
+ // This covers lines 112-113: the contentsResult.isFailure() branch in syncToDisk.
734
+ // We load a file, mark it dirty, then sabotage getFileContents to fail.
735
+ const { fetchImpl } = makeMockFetch([
736
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
737
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') }
738
+ ]);
739
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
740
+ baseUrl: 'http://localhost:3000',
741
+ fetchImpl,
742
+ mutable: true
743
+ })).orThrow();
744
+ accessors.saveFileContents('/data.json', '{"new":1}').orThrow();
745
+ // Sabotage the base getFileContents to simulate a failure
746
+ const originalGet = accessors.getFileContents.bind(accessors);
747
+ accessors.getFileContents = (path) => {
748
+ if (path === '/data.json') {
749
+ // eslint-disable-next-line import/no-internal-modules
750
+ const { fail: failResult } = require('@fgv/ts-utils');
751
+ return failResult('simulated get failure');
752
+ }
753
+ return originalGet(path);
754
+ };
755
+ const result = await accessors.syncToDisk();
756
+ expect(result).toFailWith(/simulated get failure/i);
757
+ });
758
+ });
759
+ describe('_request() invalid JSON during sync', () => {
760
+ test('fails when PUT /file returns invalid JSON', async () => {
761
+ // Covers lines 225-226: _request() returns fail('invalid JSON response')
762
+ // when response.json() throws during a sync PUT.
763
+ const { fetchImpl } = makeMockFetch([
764
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
765
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
766
+ // PUT response with ok=true but throwOnJson simulates invalid JSON body
767
+ { ok: true, throwOnJson: true }
768
+ ]);
769
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
770
+ baseUrl: 'http://localhost:3000',
771
+ fetchImpl,
772
+ mutable: true
773
+ })).orThrow();
774
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
775
+ const result = await accessors.syncToDisk();
776
+ expect(result).toFailWith(/invalid json/i);
777
+ });
778
+ });
779
+ describe('fromHttp() with inferContentType', () => {
780
+ test('calls inferContentType when provided and uses the result', async () => {
781
+ // Covers line 270: params.inferContentType?.(...).orDefault()
782
+ const { fetchImpl } = makeMockFetch([
783
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
784
+ { ok: true, jsonValue: fileResponse('/data.json', '{}', 'application/json') }
785
+ ]);
786
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
787
+ baseUrl: 'http://localhost:3000',
788
+ fetchImpl,
789
+ inferContentType: (path, provided) => {
790
+ if (provided === 'application/json') {
791
+ const { succeed: s } = require('@fgv/ts-utils');
792
+ return s('json');
793
+ }
794
+ const { succeed: s } = require('@fgv/ts-utils');
795
+ return s(undefined);
796
+ }
797
+ });
798
+ expect(result).toSucceed();
799
+ });
800
+ });
801
+ describe('_request() error handling', () => {
802
+ test('returns failure with error message for network error (Error instance throw) during sync', async () => {
803
+ // Covers line 214 true branch: response.err instanceof Error -> uses .message
804
+ let callCount = 0;
805
+ const fetchImpl = (_url, _init) => {
806
+ callCount++;
807
+ if (callCount === 1) {
808
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
809
+ }
810
+ if (callCount === 2) {
811
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
812
+ }
813
+ // PUT request throws an Error instance
814
+ return Promise.reject(new Error('real Error instance'));
815
+ };
816
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
817
+ baseUrl: 'http://localhost:3000',
818
+ fetchImpl,
819
+ mutable: true
820
+ })).orThrow();
821
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
822
+ const result = await accessors.syncToDisk();
823
+ expect(result).toFailWith(/real error instance/i);
824
+ });
825
+ test('returns failure with error message for non-Error throw during sync PUT', async () => {
826
+ // Covers line 214 false branch: thrown value is not an Error instance -> uses String(response.err)
827
+ // Must be triggered via syncToDisk() which uses the instance _request() method.
828
+ let callCount = 0;
829
+ const fetchImpl = (_url, _init) => {
830
+ callCount++;
831
+ if (callCount === 1) {
832
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
833
+ }
834
+ if (callCount === 2) {
835
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
836
+ }
837
+ // Non-Error throw (e.g., a plain string)
838
+ return Promise.reject('non-error string rejection');
839
+ };
840
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
841
+ baseUrl: 'http://localhost:3000',
842
+ fetchImpl,
843
+ mutable: true
844
+ })).orThrow();
845
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
846
+ const result = await accessors.syncToDisk();
847
+ expect(result).toFailWith(/non-error string rejection/i);
848
+ });
849
+ test('returns failure with HTTP status fallback when response.text() throws during sync', async () => {
850
+ // Covers the text() catch branch in _request(): uses `HTTP ${status}` fallback
851
+ let callCount = 0;
852
+ const fetchImpl = (_url, _init) => {
853
+ callCount++;
854
+ if (callCount === 1) {
855
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
856
+ }
857
+ if (callCount === 2) {
858
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
859
+ }
860
+ return Promise.resolve(makeMockResponse({ ok: false, status: 502, throwOnText: true }));
861
+ };
862
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
863
+ baseUrl: 'http://localhost:3000',
864
+ fetchImpl,
865
+ mutable: true
866
+ })).orThrow();
867
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
868
+ const result = await accessors.syncToDisk();
869
+ expect(result).toFailWith(/http 502/i);
870
+ });
871
+ });
872
+ describe('_requestWithParams() error handling', () => {
873
+ test('returns failure with error message for non-Error network throw during init', async () => {
874
+ // _requestWithParams is used for GET requests during fromHttp(); test non-Error throw branch
875
+ const fetchImpl = (_url, _init) => {
876
+ return Promise.reject(42);
877
+ };
878
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
879
+ baseUrl: 'http://localhost:3000',
880
+ fetchImpl
881
+ });
882
+ expect(result).toFailWith(/42/);
883
+ });
884
+ test('returns failure with HTTP status fallback when response.text() throws during init', async () => {
885
+ const fetchImpl = (_url, _init) => {
886
+ return Promise.resolve(makeMockResponse({ ok: false, status: 504, throwOnText: true }));
887
+ };
888
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
889
+ baseUrl: 'http://localhost:3000',
890
+ fetchImpl
891
+ });
892
+ expect(result).toFailWith(/http 504/i);
893
+ });
894
+ test('uses globalThis.fetch when no fetchImpl provided', async () => {
895
+ // Covers line 48: the `fetchImpl ?? globalThis.fetch` right-side branch.
896
+ // We temporarily replace globalThis.fetch with a mock that returns an empty tree.
897
+ const originalFetch = globalThis.fetch;
898
+ let fetchCallCount = 0;
899
+ globalThis.fetch = (_url, _init) => {
900
+ fetchCallCount++;
901
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: { path: '/', children: [] } }));
902
+ };
903
+ try {
904
+ const result = await file_tree_1.HttpTreeAccessors.fromHttp({
905
+ baseUrl: 'http://localhost:3000'
906
+ // No fetchImpl - should fall back to globalThis.fetch
907
+ });
908
+ expect(result).toSucceed();
909
+ expect(fetchCallCount).toBeGreaterThan(0);
910
+ }
911
+ finally {
912
+ globalThis.fetch = originalFetch;
913
+ }
914
+ });
915
+ });
916
+ describe('userId header in _request()', () => {
917
+ test('includes X-User-Id header in PUT /file and POST /sync requests when userId is set', async () => {
918
+ var _a;
919
+ // Covers line 259: the this._userId branch in _request()
920
+ const { fetchImpl, calls } = makeMockFetch([
921
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
922
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
923
+ { ok: true, jsonValue: fileResponse('/data.json', '"updated"') },
924
+ { ok: true, jsonValue: { synced: 1 } }
925
+ ]);
926
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
927
+ baseUrl: 'http://localhost:3000',
928
+ fetchImpl,
929
+ mutable: true,
930
+ userId: 'test-user-123'
931
+ })).orThrow();
932
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
933
+ const result = await accessors.syncToDisk();
934
+ expect(result).toSucceed();
935
+ // The PUT and POST requests should include the X-User-Id header
936
+ const syncCalls = calls.slice(2);
937
+ for (const call of syncCalls) {
938
+ const headers = (_a = call.init) === null || _a === void 0 ? void 0 : _a.headers;
939
+ expect(headers === null || headers === void 0 ? void 0 : headers['X-User-Id']).toBe('test-user-123');
940
+ }
941
+ });
942
+ });
943
+ describe('deleteFile()', () => {
944
+ test('fails when the underlying InMemoryTreeAccessors deleteFile fails (file not found)', async () => {
945
+ // Covers lines 200-201: result.isFailure() branch in deleteFile
946
+ const { fetchImpl } = makeMockFetch([{ ok: true, jsonValue: { path: '/', children: [] } }]);
947
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({ baseUrl: 'http://localhost:3000', fetchImpl, mutable: true })).orThrow();
948
+ // Delete a file that does not exist — super.deleteFile should fail
949
+ const result = accessors.deleteFile('/nonexistent.json');
950
+ expect(result).toFail();
951
+ });
952
+ test('includes namespace in DELETE query params when namespace is configured', async () => {
953
+ // Covers lines 131-132: the namespace branch in syncToDisk's pending-deletion loop
954
+ const { fetchImpl, calls } = makeMockFetch([
955
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
956
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
957
+ { ok: true, jsonValue: { deleted: true } },
958
+ { ok: true, jsonValue: { synced: 0 } }
959
+ ]);
960
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
961
+ baseUrl: 'http://localhost:3000',
962
+ namespace: 'my-namespace',
963
+ fetchImpl,
964
+ mutable: true
965
+ })).orThrow();
966
+ accessors.deleteFile('/data.json').orThrow();
967
+ const result = await accessors.syncToDisk();
968
+ expect(result).toSucceed();
969
+ // The DELETE request URL should include namespace as a query parameter
970
+ const deleteCalls = calls.filter((c) => { var _a; return ((_a = c.init) === null || _a === void 0 ? void 0 : _a.method) === 'DELETE'; });
971
+ expect(deleteCalls).toHaveLength(1);
972
+ expect(deleteCalls[0].url).toContain('namespace=my-namespace');
973
+ });
974
+ test('triggers fire-and-forget autoSync after successful delete when autoSync is enabled', async () => {
975
+ // Covers lines 209-212: the autoSync branch in deleteFile
976
+ const syncResponses = [
977
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
978
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
979
+ // DELETE /file response for pending deletion
980
+ { ok: true, jsonValue: { deleted: true } },
981
+ // POST /sync response
982
+ { ok: true, jsonValue: { synced: 0 } }
983
+ ];
984
+ const { fetchImpl, calls } = makeMockFetch(syncResponses);
985
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
986
+ baseUrl: 'http://localhost:3000',
987
+ fetchImpl,
988
+ mutable: true,
989
+ autoSync: true
990
+ })).orThrow();
991
+ const result = accessors.deleteFile('/data.json');
992
+ expect(result).toSucceedWith(true);
993
+ // autoSync fires-and-forgets; wait for microtasks to drain
994
+ await new Promise((resolve) => setTimeout(resolve, 0));
995
+ // Verify that DELETE + POST /sync were called
996
+ const methodCalls = calls.slice(2).map((c) => { var _a; return (_a = c.init) === null || _a === void 0 ? void 0 : _a.method; });
997
+ expect(methodCalls).toContain('DELETE');
998
+ expect(methodCalls).toContain('POST');
999
+ });
1000
+ });
1001
+ });
1002
+ //# sourceMappingURL=httpTreeAccessors.test.js.map