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

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 (286) hide show
  1. package/.rush/temp/63a2c58f11944fdb592459d835a6483c79343b3f.tar.log +237 -0
  2. package/.rush/temp/chunked-rush-logs/ts-web-extras.build.chunks.jsonl +19 -39
  3. package/.rush/temp/operation/build/all.log +19 -39
  4. package/.rush/temp/operation/build/log-chunks.jsonl +19 -39
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/.rush/temp/shrinkwrap-deps.json +690 -633
  7. package/config/typedoc.json +2 -1
  8. package/dist/packlets/file-tree/httpTreeAccessors.js +128 -41
  9. package/dist/packlets/file-tree/httpTreeAccessors.js.map +1 -1
  10. package/dist/test/unit/httpTreeAccessors.test.js +438 -74
  11. package/dist/test/unit/httpTreeAccessors.test.js.map +1 -1
  12. package/dist/ts-web-extras.d.ts +20 -0
  13. package/dist/tsdoc-metadata.json +1 -1
  14. package/docs/CryptoUtils/README.md +60 -0
  15. package/docs/CryptoUtils/classes/BrowserCryptoProvider.decrypt.md +27 -0
  16. package/docs/CryptoUtils/classes/BrowserCryptoProvider.deriveKey.md +26 -0
  17. package/docs/CryptoUtils/classes/BrowserCryptoProvider.encrypt.md +25 -0
  18. package/docs/CryptoUtils/classes/BrowserCryptoProvider.fromBase64.md +24 -0
  19. package/docs/CryptoUtils/classes/BrowserCryptoProvider.generateKey.md +17 -0
  20. package/docs/CryptoUtils/classes/BrowserCryptoProvider.generateRandomBytes.md +24 -0
  21. package/docs/CryptoUtils/classes/BrowserCryptoProvider.md +151 -0
  22. package/docs/CryptoUtils/classes/BrowserCryptoProvider.toBase64.md +24 -0
  23. package/docs/CryptoUtils/classes/BrowserHashProvider.hashParts.md +26 -0
  24. package/docs/CryptoUtils/classes/BrowserHashProvider.hashString.md +25 -0
  25. package/docs/CryptoUtils/classes/BrowserHashProvider.md +81 -0
  26. package/docs/CryptoUtils/functions/createBrowserCryptoProvider.md +12 -0
  27. package/docs/FileTreeHelpers/README.md +85 -0
  28. package/docs/FileTreeHelpers/functions/extractFileListMetadata.md +11 -0
  29. package/docs/FileTreeHelpers/functions/extractFileMetadata.md +11 -0
  30. package/docs/FileTreeHelpers/functions/fromDirectoryUpload.md +12 -0
  31. package/docs/FileTreeHelpers/functions/fromFileList.md +12 -0
  32. package/docs/FileTreeHelpers/functions/getOriginalFile.md +11 -0
  33. package/docs/FileTreeHelpers/variables/defaultFileApiTreeInitParams.md +9 -0
  34. package/docs/README.md +499 -53
  35. package/docs/classes/BrowserCryptoProvider.decrypt.md +27 -0
  36. package/docs/classes/BrowserCryptoProvider.deriveKey.md +26 -0
  37. package/docs/classes/BrowserCryptoProvider.encrypt.md +25 -0
  38. package/docs/classes/BrowserCryptoProvider.fromBase64.md +24 -0
  39. package/docs/classes/BrowserCryptoProvider.generateKey.md +17 -0
  40. package/docs/classes/BrowserCryptoProvider.generateRandomBytes.md +24 -0
  41. package/docs/classes/BrowserCryptoProvider.md +151 -0
  42. package/docs/classes/BrowserCryptoProvider.toBase64.md +24 -0
  43. package/docs/classes/BrowserHashProvider.hashParts.md +26 -0
  44. package/docs/classes/BrowserHashProvider.hashString.md +25 -0
  45. package/docs/classes/BrowserHashProvider.md +81 -0
  46. package/docs/classes/DirectoryHandleStore.getAll.md +17 -0
  47. package/docs/classes/DirectoryHandleStore.getAllLabels.md +17 -0
  48. package/docs/classes/DirectoryHandleStore.load.md +24 -0
  49. package/docs/classes/DirectoryHandleStore.md +65 -61
  50. package/docs/classes/DirectoryHandleStore.remove.md +24 -0
  51. package/docs/classes/DirectoryHandleStore.save.md +25 -0
  52. package/docs/classes/FileApiTreeAccessors.create.md +25 -0
  53. package/docs/classes/FileApiTreeAccessors.createFromHttp.md +24 -0
  54. package/docs/classes/FileApiTreeAccessors.createFromLocalStorage.md +25 -0
  55. package/docs/classes/FileApiTreeAccessors.createPersistent.md +26 -0
  56. package/docs/classes/FileApiTreeAccessors.createPersistentFromFile.md +27 -0
  57. package/docs/classes/FileApiTreeAccessors.extractFileMetadata.md +24 -0
  58. package/docs/classes/FileApiTreeAccessors.fromDirectoryUpload.md +25 -0
  59. package/docs/classes/FileApiTreeAccessors.fromFileList.md +25 -0
  60. package/docs/classes/FileApiTreeAccessors.getOriginalFile.md +27 -0
  61. package/docs/classes/FileApiTreeAccessors.md +87 -201
  62. package/docs/classes/FileSystemAccessTreeAccessors.deleteFile.md +22 -0
  63. package/docs/classes/FileSystemAccessTreeAccessors.fileIsMutable.md +22 -0
  64. package/docs/classes/FileSystemAccessTreeAccessors.fromDirectoryHandle.md +25 -0
  65. package/docs/classes/FileSystemAccessTreeAccessors.fromFileHandle.md +29 -0
  66. package/docs/classes/FileSystemAccessTreeAccessors.getDirtyPaths.md +15 -0
  67. package/docs/classes/FileSystemAccessTreeAccessors.isDirty.md +15 -0
  68. package/docs/classes/FileSystemAccessTreeAccessors.md +128 -410
  69. package/docs/classes/FileSystemAccessTreeAccessors.saveFileContents.md +23 -0
  70. package/docs/classes/FileSystemAccessTreeAccessors.syncToDisk.md +15 -0
  71. package/docs/classes/HttpTreeAccessors.deleteFile.md +22 -0
  72. package/docs/classes/HttpTreeAccessors.fileIsMutable.md +24 -0
  73. package/docs/classes/HttpTreeAccessors.fromHttp.md +24 -0
  74. package/docs/classes/HttpTreeAccessors.getDirtyPaths.md +17 -0
  75. package/docs/classes/HttpTreeAccessors.isDirty.md +17 -0
  76. package/docs/classes/HttpTreeAccessors.md +121 -368
  77. package/docs/classes/HttpTreeAccessors.saveFileContents.md +25 -0
  78. package/docs/classes/HttpTreeAccessors.syncToDisk.md +22 -0
  79. package/docs/classes/LocalStorageTreeAccessors.deleteFile.md +24 -0
  80. package/docs/classes/LocalStorageTreeAccessors.fileIsMutable.md +24 -0
  81. package/docs/classes/LocalStorageTreeAccessors.fromStorage.md +25 -0
  82. package/docs/classes/LocalStorageTreeAccessors.getDirtyPaths.md +17 -0
  83. package/docs/classes/LocalStorageTreeAccessors.isDirty.md +17 -0
  84. package/docs/classes/LocalStorageTreeAccessors.md +121 -371
  85. package/docs/classes/LocalStorageTreeAccessors.saveFileContents.md +25 -0
  86. package/docs/classes/LocalStorageTreeAccessors.syncToDisk.md +17 -0
  87. package/docs/functions/createBrowserCryptoProvider.md +12 -0
  88. package/docs/functions/exportAsJson.md +6 -17
  89. package/docs/functions/exportUsingFileSystemAPI.md +6 -21
  90. package/docs/functions/extractDirectoryPath.md +6 -16
  91. package/docs/functions/extractFileListMetadata.md +11 -0
  92. package/docs/functions/extractFileMetadata.md +11 -0
  93. package/docs/functions/fromDirectoryUpload.md +12 -0
  94. package/docs/functions/fromFileList.md +12 -0
  95. package/docs/functions/getOriginalFile.md +11 -0
  96. package/docs/functions/isDirectoryHandle.md +6 -18
  97. package/docs/functions/isFileHandle.md +6 -18
  98. package/docs/functions/isFilePath.md +6 -16
  99. package/docs/functions/parseContextFilter.md +6 -16
  100. package/docs/functions/parseQualifierDefaults.md +6 -16
  101. package/docs/functions/parseResourceTypes.md +6 -16
  102. package/docs/functions/parseUrlParameters.md +6 -10
  103. package/docs/functions/safeShowDirectoryPicker.md +6 -19
  104. package/docs/functions/safeShowOpenFilePicker.md +6 -19
  105. package/docs/functions/safeShowSaveFilePicker.md +6 -19
  106. package/docs/functions/supportsFileSystemAccess.md +6 -18
  107. package/docs/interfaces/FilePickerAcceptType.accept.md +9 -0
  108. package/docs/interfaces/FilePickerAcceptType.description.md +9 -0
  109. package/docs/interfaces/FilePickerAcceptType.md +54 -9
  110. package/docs/interfaces/FileSystemCreateWritableOptions.keepExistingData.md +9 -0
  111. package/docs/interfaces/FileSystemCreateWritableOptions.md +37 -8
  112. package/docs/interfaces/FileSystemDirectoryHandle._asyncIterator_.md +13 -0
  113. package/docs/interfaces/FileSystemDirectoryHandle.entries.md +13 -0
  114. package/docs/interfaces/FileSystemDirectoryHandle.getDirectoryHandle.md +21 -0
  115. package/docs/interfaces/FileSystemDirectoryHandle.getFileHandle.md +21 -0
  116. package/docs/interfaces/FileSystemDirectoryHandle.keys.md +13 -0
  117. package/docs/interfaces/FileSystemDirectoryHandle.kind.md +9 -0
  118. package/docs/interfaces/FileSystemDirectoryHandle.md +140 -103
  119. package/docs/interfaces/FileSystemDirectoryHandle.removeEntry.md +21 -0
  120. package/docs/interfaces/FileSystemDirectoryHandle.resolve.md +20 -0
  121. package/docs/interfaces/FileSystemDirectoryHandle.values.md +13 -0
  122. package/docs/interfaces/FileSystemFileHandle.createWritable.md +20 -0
  123. package/docs/interfaces/FileSystemFileHandle.getFile.md +13 -0
  124. package/docs/interfaces/FileSystemFileHandle.kind.md +9 -0
  125. package/docs/interfaces/FileSystemFileHandle.md +98 -58
  126. package/docs/interfaces/FileSystemGetDirectoryOptions.create.md +9 -0
  127. package/docs/interfaces/FileSystemGetDirectoryOptions.md +37 -8
  128. package/docs/interfaces/FileSystemGetFileOptions.create.md +9 -0
  129. package/docs/interfaces/FileSystemGetFileOptions.md +37 -8
  130. package/docs/interfaces/FileSystemHandle.isSameEntry.md +20 -0
  131. package/docs/interfaces/FileSystemHandle.kind.md +9 -0
  132. package/docs/interfaces/FileSystemHandle.md +88 -39
  133. package/docs/interfaces/FileSystemHandle.name.md +9 -0
  134. package/docs/interfaces/FileSystemHandle.queryPermission.md +20 -0
  135. package/docs/interfaces/FileSystemHandle.requestPermission.md +20 -0
  136. package/docs/interfaces/FileSystemHandlePermissionDescriptor.md +37 -8
  137. package/docs/interfaces/FileSystemHandlePermissionDescriptor.mode.md +9 -0
  138. package/docs/interfaces/FileSystemRemoveOptions.md +37 -8
  139. package/docs/interfaces/FileSystemRemoveOptions.recursive.md +9 -0
  140. package/docs/interfaces/FileSystemWritableFileStream.md +83 -68
  141. package/docs/interfaces/FileSystemWritableFileStream.seek.md +20 -0
  142. package/docs/interfaces/FileSystemWritableFileStream.truncate.md +20 -0
  143. package/docs/interfaces/FileSystemWritableFileStream.write.md +20 -0
  144. package/docs/interfaces/IDirectoryHandleTreeInitializer.dirHandles.md +9 -0
  145. package/docs/interfaces/IDirectoryHandleTreeInitializer.md +71 -10
  146. package/docs/interfaces/IDirectoryHandleTreeInitializer.nonRecursive.md +9 -0
  147. package/docs/interfaces/IDirectoryHandleTreeInitializer.prefix.md +9 -0
  148. package/docs/interfaces/IFileHandleTreeInitializer.fileHandles.md +9 -0
  149. package/docs/interfaces/IFileHandleTreeInitializer.md +54 -9
  150. package/docs/interfaces/IFileHandleTreeInitializer.prefix.md +9 -0
  151. package/docs/interfaces/IFileListTreeInitializer.fileList.md +9 -0
  152. package/docs/interfaces/IFileListTreeInitializer.md +37 -8
  153. package/docs/interfaces/IFileMetadata.lastModified.md +9 -0
  154. package/docs/interfaces/IFileMetadata.md +105 -12
  155. package/docs/interfaces/IFileMetadata.name.md +9 -0
  156. package/docs/interfaces/IFileMetadata.path.md +9 -0
  157. package/docs/interfaces/IFileMetadata.size.md +9 -0
  158. package/docs/interfaces/IFileMetadata.type.md +9 -0
  159. package/docs/interfaces/IFileSystemAccessTreeParams.autoSync.md +12 -0
  160. package/docs/interfaces/IFileSystemAccessTreeParams.filePath.md +13 -0
  161. package/docs/interfaces/IFileSystemAccessTreeParams.logger.md +11 -0
  162. package/docs/interfaces/IFileSystemAccessTreeParams.md +138 -20
  163. package/docs/interfaces/IFileSystemAccessTreeParams.requireWritePermission.md +13 -0
  164. package/docs/interfaces/IFsAccessApis.md +36 -31
  165. package/docs/interfaces/IFsAccessApis.showDirectoryPicker.md +20 -0
  166. package/docs/interfaces/IFsAccessApis.showOpenFilePicker.md +20 -0
  167. package/docs/interfaces/IFsAccessApis.showSaveFilePicker.md +20 -0
  168. package/docs/interfaces/IHttpTreeParams.autoSync.md +9 -0
  169. package/docs/interfaces/IHttpTreeParams.baseUrl.md +9 -0
  170. package/docs/interfaces/IHttpTreeParams.fetchImpl.md +9 -0
  171. package/docs/interfaces/IHttpTreeParams.logger.md +9 -0
  172. package/docs/interfaces/IHttpTreeParams.md +172 -22
  173. package/docs/interfaces/IHttpTreeParams.namespace.md +9 -0
  174. package/docs/interfaces/IHttpTreeParams.userId.md +9 -0
  175. package/docs/interfaces/ILocalStorageTreeParams.autoSync.md +12 -0
  176. package/docs/interfaces/ILocalStorageTreeParams.md +121 -20
  177. package/docs/interfaces/ILocalStorageTreeParams.pathToKeyMap.md +13 -0
  178. package/docs/interfaces/ILocalStorageTreeParams.storage.md +12 -0
  179. package/docs/interfaces/IUrlConfigOptions.config.md +11 -0
  180. package/docs/interfaces/IUrlConfigOptions.configStartDir.md +11 -0
  181. package/docs/interfaces/IUrlConfigOptions.contextFilter.md +11 -0
  182. package/docs/interfaces/IUrlConfigOptions.input.md +11 -0
  183. package/docs/interfaces/IUrlConfigOptions.inputStartDir.md +11 -0
  184. package/docs/interfaces/IUrlConfigOptions.interactive.md +11 -0
  185. package/docs/interfaces/IUrlConfigOptions.loadZip.md +11 -0
  186. package/docs/interfaces/IUrlConfigOptions.maxDistance.md +11 -0
  187. package/docs/interfaces/IUrlConfigOptions.md +241 -20
  188. package/docs/interfaces/IUrlConfigOptions.qualifierDefaults.md +11 -0
  189. package/docs/interfaces/IUrlConfigOptions.reduceQualifiers.md +11 -0
  190. package/docs/interfaces/IUrlConfigOptions.resourceTypes.md +11 -0
  191. package/docs/interfaces/IUrlConfigOptions.zipFile.md +11 -0
  192. package/docs/interfaces/IUrlConfigOptions.zipPath.md +11 -0
  193. package/docs/interfaces/ShowDirectoryPickerOptions.id.md +9 -0
  194. package/docs/interfaces/ShowDirectoryPickerOptions.md +71 -10
  195. package/docs/interfaces/ShowDirectoryPickerOptions.mode.md +9 -0
  196. package/docs/interfaces/ShowDirectoryPickerOptions.startIn.md +9 -0
  197. package/docs/interfaces/ShowOpenFilePickerOptions.excludeAcceptAllOption.md +9 -0
  198. package/docs/interfaces/ShowOpenFilePickerOptions.id.md +9 -0
  199. package/docs/interfaces/ShowOpenFilePickerOptions.md +105 -12
  200. package/docs/interfaces/ShowOpenFilePickerOptions.multiple.md +9 -0
  201. package/docs/interfaces/ShowOpenFilePickerOptions.startIn.md +9 -0
  202. package/docs/interfaces/ShowOpenFilePickerOptions.types.md +9 -0
  203. package/docs/interfaces/ShowSaveFilePickerOptions.excludeAcceptAllOption.md +9 -0
  204. package/docs/interfaces/ShowSaveFilePickerOptions.id.md +9 -0
  205. package/docs/interfaces/ShowSaveFilePickerOptions.md +105 -12
  206. package/docs/interfaces/ShowSaveFilePickerOptions.startIn.md +9 -0
  207. package/docs/interfaces/ShowSaveFilePickerOptions.suggestedName.md +9 -0
  208. package/docs/interfaces/ShowSaveFilePickerOptions.types.md +9 -0
  209. package/docs/type-aliases/TreeInitializer.md +7 -7
  210. package/docs/type-aliases/WellKnownDirectory.md +7 -7
  211. package/docs/type-aliases/WindowWithFsAccess.md +7 -7
  212. package/docs/variables/DEFAULT_DIRECTORY_HANDLE_DB.md +5 -7
  213. package/docs/variables/DEFAULT_DIRECTORY_HANDLE_STORE.md +5 -7
  214. package/docs/variables/defaultFileApiTreeInitParams.md +9 -0
  215. package/lib/packlets/file-tree/httpTreeAccessors.d.ts +20 -0
  216. package/lib/packlets/file-tree/httpTreeAccessors.d.ts.map +1 -1
  217. package/lib/packlets/file-tree/httpTreeAccessors.js +128 -41
  218. package/lib/packlets/file-tree/httpTreeAccessors.js.map +1 -1
  219. package/lib/test/unit/httpTreeAccessors.test.js +438 -74
  220. package/lib/test/unit/httpTreeAccessors.test.js.map +1 -1
  221. package/package.json +27 -26
  222. package/rush-logs/ts-web-extras.build.cache.log +3 -0
  223. package/rush-logs/ts-web-extras.build.log +19 -39
  224. package/src/packlets/file-tree/httpTreeAccessors.ts +142 -45
  225. package/src/test/unit/httpTreeAccessors.test.ts +556 -84
  226. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  227. package/temp/coverage/crypto-utils/browserCryptoProvider.ts.html +1 -1
  228. package/temp/coverage/crypto-utils/browserHashProvider.ts.html +1 -1
  229. package/temp/coverage/crypto-utils/index.html +1 -1
  230. package/temp/coverage/file-tree/directoryHandleStore.ts.html +1 -1
  231. package/temp/coverage/file-tree/fileApiTreeAccessors.ts.html +1 -1
  232. package/temp/coverage/file-tree/fileSystemAccessTreeAccessors.ts.html +1 -1
  233. package/temp/coverage/file-tree/httpTreeAccessors.ts.html +507 -216
  234. package/temp/coverage/file-tree/index.html +11 -11
  235. package/temp/coverage/file-tree/localStorageTreeAccessors.ts.html +1 -1
  236. package/temp/coverage/helpers/fileTreeHelpers.ts.html +1 -1
  237. package/temp/coverage/helpers/index.html +1 -1
  238. package/temp/coverage/index.html +11 -11
  239. package/temp/coverage/lcov-report/crypto-utils/browserCryptoProvider.ts.html +1 -1
  240. package/temp/coverage/lcov-report/crypto-utils/browserHashProvider.ts.html +1 -1
  241. package/temp/coverage/lcov-report/crypto-utils/index.html +1 -1
  242. package/temp/coverage/lcov-report/file-tree/directoryHandleStore.ts.html +1 -1
  243. package/temp/coverage/lcov-report/file-tree/fileApiTreeAccessors.ts.html +1 -1
  244. package/temp/coverage/lcov-report/file-tree/fileSystemAccessTreeAccessors.ts.html +1 -1
  245. package/temp/coverage/lcov-report/file-tree/httpTreeAccessors.ts.html +507 -216
  246. package/temp/coverage/lcov-report/file-tree/index.html +11 -11
  247. package/temp/coverage/lcov-report/file-tree/localStorageTreeAccessors.ts.html +1 -1
  248. package/temp/coverage/lcov-report/helpers/fileTreeHelpers.ts.html +1 -1
  249. package/temp/coverage/lcov-report/helpers/index.html +1 -1
  250. package/temp/coverage/lcov-report/index.html +11 -11
  251. package/temp/coverage/lcov-report/url-utils/index.html +1 -1
  252. package/temp/coverage/lcov-report/url-utils/urlParams.ts.html +1 -1
  253. package/temp/coverage/lcov.info +520 -387
  254. package/temp/coverage/url-utils/index.html +1 -1
  255. package/temp/coverage/url-utils/urlParams.ts.html +1 -1
  256. package/temp/test/jest/haste-map-7492f1b44480e0cdd1f220078fb3afd8-c8dd6c3430605adeb2f1cadf4f75e791-8c9336785555d572065b28c111982ba4 +0 -0
  257. package/temp/test/jest/perf-cache-7492f1b44480e0cdd1f220078fb3afd8-da39a3ee5e6b4b0d3255bfef95601890 +1 -0
  258. package/temp/ts-web-extras.api.json +2 -2
  259. package/.rush/temp/chunked-rush-logs/ts-web-extras.test.chunks.jsonl +0 -70
  260. package/.rush/temp/operation/build/error.log +0 -18
  261. package/.rush/temp/operation/test/all.log +0 -70
  262. package/.rush/temp/operation/test/error.log +0 -16
  263. package/.rush/temp/operation/test/log-chunks.jsonl +0 -70
  264. package/.rush/temp/operation/test/state.json +0 -3
  265. package/docs/@fgv/namespaces/CryptoUtils/README.md +0 -18
  266. package/docs/@fgv/namespaces/CryptoUtils/classes/BrowserCryptoProvider.md +0 -203
  267. package/docs/@fgv/namespaces/CryptoUtils/classes/BrowserHashProvider.md +0 -63
  268. package/docs/@fgv/namespaces/CryptoUtils/functions/createBrowserCryptoProvider.md +0 -18
  269. package/docs/@fgv/namespaces/FileTreeHelpers/README.md +0 -19
  270. package/docs/@fgv/namespaces/FileTreeHelpers/functions/extractFileListMetadata.md +0 -23
  271. package/docs/@fgv/namespaces/FileTreeHelpers/functions/extractFileMetadata.md +0 -23
  272. package/docs/@fgv/namespaces/FileTreeHelpers/functions/fromDirectoryUpload.md +0 -33
  273. package/docs/@fgv/namespaces/FileTreeHelpers/functions/fromFileList.md +0 -33
  274. package/docs/@fgv/namespaces/FileTreeHelpers/functions/getOriginalFile.md +0 -25
  275. package/docs/@fgv/namespaces/FileTreeHelpers/variables/defaultFileApiTreeInitParams.md +0 -11
  276. package/rush-logs/ts-web-extras.build.error.log +0 -18
  277. package/rush-logs/ts-web-extras.test.cache.log +0 -1
  278. package/rush-logs/ts-web-extras.test.error.log +0 -16
  279. package/rush-logs/ts-web-extras.test.log +0 -70
  280. package/temp/coverage/crypto/browserHashProvider.ts.html +0 -304
  281. package/temp/coverage/crypto/index.html +0 -116
  282. package/temp/coverage/lcov-report/crypto/browserHashProvider.ts.html +0 -304
  283. package/temp/coverage/lcov-report/crypto/index.html +0 -116
  284. package/temp/test/jest/haste-map-b931e4e63102f86c5bd4949f7dced44f-9d713eb41149188b4e5c0ae3d86d0a57-2ad8e16b24e391b8cdbe50b55c137169 +0 -0
  285. package/temp/test/jest/perf-cache-b931e4e63102f86c5bd4949f7dced44f-da39a3ee5e6b4b0d3255bfef95601890 +0 -1
  286. /package/temp/test/jest/{jest-transform-cache-b931e4e63102f86c5bd4949f7dced44f-79ef2876fae7ca75eedb2aa53dc48338/b5/package_b5f57afc9ec2c011239b1608ee5bdfa5 → jest-transform-cache-7492f1b44480e0cdd1f220078fb3afd8-79ef2876fae7ca75eedb2aa53dc48338/7c/package_7c16afc8299e635d80273763175c7a50} +0 -0
@@ -29,6 +29,7 @@ function makeMockResponse(options) {
29
29
  return {
30
30
  ok,
31
31
  status,
32
+ statusText: ok ? 'OK' : `Error ${status}`,
32
33
  json: throwOnJson
33
34
  ? () => Promise.reject(new Error('JSON parse error'))
34
35
  : () => Promise.resolve(jsonValue),
@@ -597,65 +598,99 @@ describe('HttpTreeAccessors', () => {
597
598
  expect(result).toFailWith(/sync.*data\.json.*server error/i);
598
599
  });
599
600
  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);
601
+ jest.useFakeTimers();
602
+ try {
603
+ let callCount = 0;
604
+ const fetchImpl = (_url, _init) => {
605
+ callCount++;
606
+ if (callCount === 1) {
607
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
608
+ }
609
+ if (callCount === 2) {
610
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
611
+ }
612
+ return Promise.reject(new Error('PUT network error'));
613
+ };
614
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
615
+ baseUrl: 'http://localhost:3000',
616
+ fetchImpl,
617
+ mutable: true
618
+ })).orThrow();
619
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
620
+ const syncPromise = accessors.syncToDisk();
621
+ await jest.advanceTimersByTimeAsync(1500);
622
+ const result = await syncPromise;
623
+ expect(result).toFailWith(/put network error/i);
624
+ }
625
+ finally {
626
+ jest.useRealTimers();
627
+ }
619
628
  });
620
629
  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);
630
+ jest.useFakeTimers();
631
+ try {
632
+ let callCount = 0;
633
+ const fetchImpl = (_url, _init) => {
634
+ callCount++;
635
+ if (callCount === 1) {
636
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
637
+ }
638
+ if (callCount === 2) {
639
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
640
+ }
641
+ if (callCount === 3) {
642
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '"v2"') }));
643
+ }
644
+ // All /sync attempts: persistent 503
645
+ return Promise.resolve(makeMockResponse({ ok: false, status: 503, textValue: 'Service Unavailable' }));
646
+ };
647
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
648
+ baseUrl: 'http://localhost:3000',
649
+ fetchImpl,
650
+ mutable: true
651
+ })).orThrow();
652
+ accessors.saveFileContents('/data.json', '"v2"').orThrow();
653
+ const syncPromise = accessors.syncToDisk();
654
+ // Advance past both backoff delays: 500ms + 1000ms
655
+ await jest.advanceTimersByTimeAsync(1500);
656
+ const result = await syncPromise;
657
+ expect(result).toFailWith(/service unavailable/i);
658
+ }
659
+ finally {
660
+ jest.useRealTimers();
661
+ }
635
662
  });
636
663
  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);
664
+ jest.useFakeTimers();
665
+ try {
666
+ let callCount = 0;
667
+ const fetchImpl = (_url, _init) => {
668
+ callCount++;
669
+ if (callCount === 1) {
670
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
671
+ }
672
+ if (callCount === 2) {
673
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
674
+ }
675
+ if (callCount === 3) {
676
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '"v2"') }));
677
+ }
678
+ return Promise.reject(new Error('POST network error'));
679
+ };
680
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
681
+ baseUrl: 'http://localhost:3000',
682
+ fetchImpl,
683
+ mutable: true
684
+ })).orThrow();
685
+ accessors.saveFileContents('/data.json', '"v2"').orThrow();
686
+ const syncPromise = accessors.syncToDisk();
687
+ await jest.advanceTimersByTimeAsync(1500);
688
+ const result = await syncPromise;
689
+ expect(result).toFailWith(/post network error/i);
690
+ }
691
+ finally {
692
+ jest.useRealTimers();
693
+ }
659
694
  });
660
695
  test('syncs multiple dirty files in order', async () => {
661
696
  const { fetchImpl, calls } = makeMockFetch([
@@ -848,25 +883,34 @@ describe('HttpTreeAccessors', () => {
848
883
  });
849
884
  test('returns failure with HTTP status fallback when response.text() throws during sync', async () => {
850
885
  // 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);
886
+ // 502 is transient so retries will be attempted; use fake timers to avoid real delays
887
+ jest.useFakeTimers();
888
+ try {
889
+ let callCount = 0;
890
+ const fetchImpl = (_url, _init) => {
891
+ callCount++;
892
+ if (callCount === 1) {
893
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
894
+ }
895
+ if (callCount === 2) {
896
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
897
+ }
898
+ return Promise.resolve(makeMockResponse({ ok: false, status: 502, throwOnText: true }));
899
+ };
900
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
901
+ baseUrl: 'http://localhost:3000',
902
+ fetchImpl,
903
+ mutable: true
904
+ })).orThrow();
905
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
906
+ const syncPromise = accessors.syncToDisk();
907
+ await jest.advanceTimersByTimeAsync(1500);
908
+ const result = await syncPromise;
909
+ expect(result).toFailWith(/http 502/i);
910
+ }
911
+ finally {
912
+ jest.useRealTimers();
913
+ }
870
914
  });
871
915
  });
872
916
  describe('_requestWithParams() error handling', () => {
@@ -998,5 +1042,325 @@ describe('HttpTreeAccessors', () => {
998
1042
  expect(methodCalls).toContain('POST');
999
1043
  });
1000
1044
  });
1045
+ describe('_requestWithRetry()', () => {
1046
+ test('succeeds on second attempt after a transient 503 error', async () => {
1047
+ jest.useFakeTimers();
1048
+ try {
1049
+ // Responses: init (GET tree/children), then PUT fails with 503, then PUT succeeds, then POST /sync succeeds
1050
+ let callCount = 0;
1051
+ const fetchImpl = (_url, _init) => {
1052
+ callCount++;
1053
+ if (callCount === 1) {
1054
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
1055
+ }
1056
+ if (callCount === 2) {
1057
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
1058
+ }
1059
+ // First PUT attempt: transient 503
1060
+ if (callCount === 3) {
1061
+ return Promise.resolve(makeMockResponse({ ok: false, status: 503, textValue: '503 Service Unavailable' }));
1062
+ }
1063
+ // Second PUT attempt: success
1064
+ if (callCount === 4) {
1065
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '"updated"') }));
1066
+ }
1067
+ // POST /sync
1068
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: { synced: 1 } }));
1069
+ };
1070
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
1071
+ baseUrl: 'http://localhost:3000',
1072
+ fetchImpl,
1073
+ mutable: true
1074
+ })).orThrow();
1075
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
1076
+ const syncPromise = accessors.syncToDisk();
1077
+ // Advance past the 500ms backoff for the first retry
1078
+ await jest.advanceTimersByTimeAsync(500);
1079
+ const result = await syncPromise;
1080
+ expect(result).toSucceed();
1081
+ // Three PUT-related calls: init (2) + first PUT attempt (1) + retry PUT (1) + POST sync (1)
1082
+ expect(callCount).toBe(5);
1083
+ }
1084
+ finally {
1085
+ jest.useRealTimers();
1086
+ }
1087
+ });
1088
+ test('does not retry on a non-transient error (e.g. 404)', async () => {
1089
+ jest.useFakeTimers();
1090
+ try {
1091
+ let callCount = 0;
1092
+ const fetchImpl = (_url, _init) => {
1093
+ callCount++;
1094
+ if (callCount === 1) {
1095
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
1096
+ }
1097
+ if (callCount === 2) {
1098
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
1099
+ }
1100
+ // PUT attempt: non-transient 404
1101
+ return Promise.resolve(makeMockResponse({ ok: false, status: 404, textValue: 'Not Found' }));
1102
+ };
1103
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
1104
+ baseUrl: 'http://localhost:3000',
1105
+ fetchImpl,
1106
+ mutable: true
1107
+ })).orThrow();
1108
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
1109
+ const result = await accessors.syncToDisk();
1110
+ // No timers should need advancing - non-transient errors return immediately
1111
+ expect(result).toFailWith(/not found/i);
1112
+ // Only 3 calls: 2 init + 1 PUT (no retries)
1113
+ expect(callCount).toBe(3);
1114
+ }
1115
+ finally {
1116
+ jest.useRealTimers();
1117
+ }
1118
+ });
1119
+ test('returns the last error after exhausting all retries on persistent transient errors', async () => {
1120
+ jest.useFakeTimers();
1121
+ try {
1122
+ let callCount = 0;
1123
+ const fetchImpl = (_url, _init) => {
1124
+ callCount++;
1125
+ if (callCount === 1) {
1126
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
1127
+ }
1128
+ if (callCount === 2) {
1129
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
1130
+ }
1131
+ // All PUT attempts: transient 502 every time
1132
+ return Promise.resolve(makeMockResponse({ ok: false, status: 502, textValue: '502 Bad Gateway' }));
1133
+ };
1134
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
1135
+ baseUrl: 'http://localhost:3000',
1136
+ fetchImpl,
1137
+ mutable: true
1138
+ })).orThrow();
1139
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
1140
+ const syncPromise = accessors.syncToDisk();
1141
+ // Advance past both backoff delays: 500ms (attempt 1) + 1000ms (attempt 2)
1142
+ await jest.advanceTimersByTimeAsync(1500);
1143
+ const result = await syncPromise;
1144
+ expect(result).toFailWith(/502 bad gateway/i);
1145
+ // 2 init + 3 PUT attempts (maxAttempts = 3)
1146
+ expect(callCount).toBe(5);
1147
+ }
1148
+ finally {
1149
+ jest.useRealTimers();
1150
+ }
1151
+ });
1152
+ test('logs retry message with method name from init when retrying', async () => {
1153
+ jest.useFakeTimers();
1154
+ try {
1155
+ const logger = {
1156
+ detail: jest.fn(),
1157
+ info: jest.fn(),
1158
+ warn: jest.fn(),
1159
+ error: jest.fn()
1160
+ };
1161
+ let callCount = 0;
1162
+ const fetchImpl = (_url, _init) => {
1163
+ callCount++;
1164
+ if (callCount === 1) {
1165
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
1166
+ }
1167
+ if (callCount === 2) {
1168
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
1169
+ }
1170
+ // First PUT attempt: transient 503
1171
+ if (callCount === 3) {
1172
+ return Promise.resolve(makeMockResponse({ ok: false, status: 503, textValue: '503 Service Unavailable' }));
1173
+ }
1174
+ // Second PUT attempt: success
1175
+ if (callCount === 4) {
1176
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '"updated"') }));
1177
+ }
1178
+ // POST /sync
1179
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: { synced: 1 } }));
1180
+ };
1181
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
1182
+ baseUrl: 'http://localhost:3000',
1183
+ fetchImpl,
1184
+ mutable: true,
1185
+ logger
1186
+ })).orThrow();
1187
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
1188
+ const syncPromise = accessors.syncToDisk();
1189
+ await jest.advanceTimersByTimeAsync(500);
1190
+ await syncPromise;
1191
+ // The retry log message should include the method name (PUT) from init
1192
+ expect(logger.detail).toHaveBeenCalledWith(expect.stringContaining('PUT'));
1193
+ }
1194
+ finally {
1195
+ jest.useRealTimers();
1196
+ }
1197
+ });
1198
+ });
1199
+ describe('syncToDisk() concurrency guard', () => {
1200
+ test('concurrent syncToDisk calls share the same promise and only make one set of network calls', async () => {
1201
+ const { fetchImpl, calls } = makeMockFetch([
1202
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
1203
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
1204
+ // PUT /file for the single sync
1205
+ { ok: true, jsonValue: fileResponse('/data.json', '"updated"') },
1206
+ // POST /sync
1207
+ { ok: true, jsonValue: { synced: 1 } }
1208
+ ]);
1209
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
1210
+ baseUrl: 'http://localhost:3000',
1211
+ fetchImpl,
1212
+ mutable: true
1213
+ })).orThrow();
1214
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
1215
+ // Start two concurrent syncToDisk calls without awaiting the first
1216
+ const promise1 = accessors.syncToDisk();
1217
+ const promise2 = accessors.syncToDisk();
1218
+ const [result1, result2] = await Promise.all([promise1, promise2]);
1219
+ expect(result1).toSucceed();
1220
+ expect(result2).toSucceed();
1221
+ // Both promises should have resolved to the same underlying promise result.
1222
+ // Only one PUT and one POST /sync should have been issued (not doubled).
1223
+ const syncCalls = calls.slice(2);
1224
+ const putCalls = syncCalls.filter((c) => { var _a; return ((_a = c.init) === null || _a === void 0 ? void 0 : _a.method) === 'PUT'; });
1225
+ const postCalls = syncCalls.filter((c) => { var _a; return ((_a = c.init) === null || _a === void 0 ? void 0 : _a.method) === 'POST'; });
1226
+ expect(putCalls).toHaveLength(1);
1227
+ expect(postCalls).toHaveLength(1);
1228
+ });
1229
+ test('drains items written during an in-flight sync within the same _doSync call', async () => {
1230
+ // This tests the drain loop fix for the race where:
1231
+ // 1. sync starts, snapshots dirty set {a.json}
1232
+ // 2. during the PUT for a.json, a new write arrives adding a.json back
1233
+ // 3. _doSync loops back, snapshots the new item, and syncs it too
1234
+ // 4. Only one POST /sync is sent at the end, after all items are drained
1235
+ let callCount = 0;
1236
+ let saveHook;
1237
+ const fetchImpl = (url, init) => {
1238
+ callCount++;
1239
+ const urlStr = url.toString();
1240
+ // Calls 1-2: fromHttp load
1241
+ if (callCount <= 2) {
1242
+ if (callCount === 1) {
1243
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('a.json') }));
1244
+ }
1245
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/a.json', '"orig"') }));
1246
+ }
1247
+ // Call 3: first PUT /file for a.json — trigger a write mid-sync
1248
+ if (callCount === 3 && (init === null || init === void 0 ? void 0 : init.method) === 'PUT') {
1249
+ saveHook === null || saveHook === void 0 ? void 0 : saveHook();
1250
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/a.json', '"v1"') }));
1251
+ }
1252
+ // Call 4: second PUT /file for a.json (drain loop iteration)
1253
+ if (callCount === 4 && (init === null || init === void 0 ? void 0 : init.method) === 'PUT') {
1254
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/a.json', '"v2"') }));
1255
+ }
1256
+ // Call 5: single POST /sync after drain loop exits
1257
+ if (callCount === 5 && urlStr.includes('/sync')) {
1258
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: { synced: 1 } }));
1259
+ }
1260
+ return Promise.reject(new Error(`Unexpected call #${callCount} to ${urlStr}`));
1261
+ };
1262
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
1263
+ baseUrl: 'http://localhost:3000',
1264
+ fetchImpl: fetchImpl,
1265
+ mutable: true
1266
+ })).orThrow();
1267
+ // Write a.json
1268
+ accessors.saveFileContents('/a.json', '"v1"').orThrow();
1269
+ // Set up the hook BEFORE starting sync — the mock fetch for the PUT
1270
+ // runs synchronously within syncToDisk(), so the hook must be in place.
1271
+ saveHook = () => {
1272
+ accessors.saveFileContents('/a.json', '"v2"').orThrow();
1273
+ };
1274
+ // Start sync — during the first PUT, saveHook fires and writes v2.
1275
+ // The drain loop in _doSync picks this up and PUTs again before
1276
+ // sending the final POST /sync.
1277
+ const result = await accessors.syncToDisk();
1278
+ expect(result).toSucceed();
1279
+ expect(accessors.isDirty()).toBe(false);
1280
+ // 5 calls: 2 load + 2 PUTs (v1, v2) + 1 POST /sync
1281
+ expect(callCount).toBe(5);
1282
+ });
1283
+ });
1284
+ describe('syncToDisk() error recovery', () => {
1285
+ test('restores dirty files when PUT fails so they can be retried', async () => {
1286
+ const { fetchImpl } = makeMockFetch([
1287
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
1288
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
1289
+ // PUT /file fails with non-transient error
1290
+ { ok: false, status: 500, textValue: 'Server Error' }
1291
+ ]);
1292
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
1293
+ baseUrl: 'http://localhost:3000',
1294
+ fetchImpl,
1295
+ mutable: true
1296
+ })).orThrow();
1297
+ accessors.saveFileContents('/data.json', '"updated"').orThrow();
1298
+ expect(accessors.isDirty()).toBe(true);
1299
+ const result = await accessors.syncToDisk();
1300
+ expect(result).toFailWith(/sync.*data\.json.*server error/i);
1301
+ // The file should still be dirty so a retry is possible
1302
+ expect(accessors.isDirty()).toBe(true);
1303
+ expect(accessors.getDirtyPaths()).toContain('/data.json');
1304
+ });
1305
+ test('restores pending deletions when DELETE fails so they can be retried', async () => {
1306
+ const { fetchImpl } = makeMockFetch([
1307
+ { ok: true, jsonValue: rootWithOneFile('data.json') },
1308
+ { ok: true, jsonValue: fileResponse('/data.json', '{}') },
1309
+ // DELETE fails with non-transient error
1310
+ { ok: false, status: 500, textValue: 'Server Error' }
1311
+ ]);
1312
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
1313
+ baseUrl: 'http://localhost:3000',
1314
+ fetchImpl,
1315
+ mutable: true
1316
+ })).orThrow();
1317
+ accessors.deleteFile('/data.json').orThrow();
1318
+ expect(accessors.isDirty()).toBe(true);
1319
+ const result = await accessors.syncToDisk();
1320
+ expect(result).toFailWith(/delete.*data\.json.*server error/i);
1321
+ // The deletion should still be pending so a retry is possible
1322
+ expect(accessors.isDirty()).toBe(true);
1323
+ expect(accessors.getDirtyPaths()).toContain('/data.json');
1324
+ });
1325
+ test('restores dirty files when POST /sync fails so they can be retried', async () => {
1326
+ jest.useFakeTimers();
1327
+ try {
1328
+ let callCount = 0;
1329
+ const fetchImpl = (_url, _init) => {
1330
+ callCount++;
1331
+ if (callCount === 1) {
1332
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: rootWithOneFile('data.json') }));
1333
+ }
1334
+ if (callCount === 2) {
1335
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '{}') }));
1336
+ }
1337
+ if (callCount === 3) {
1338
+ // PUT succeeds
1339
+ return Promise.resolve(makeMockResponse({ ok: true, jsonValue: fileResponse('/data.json', '"v2"') }));
1340
+ }
1341
+ // All /sync attempts: persistent 503
1342
+ return Promise.resolve(makeMockResponse({ ok: false, status: 503, textValue: 'Service Unavailable' }));
1343
+ };
1344
+ const accessors = (await file_tree_1.HttpTreeAccessors.fromHttp({
1345
+ baseUrl: 'http://localhost:3000',
1346
+ fetchImpl,
1347
+ mutable: true
1348
+ })).orThrow();
1349
+ accessors.saveFileContents('/data.json', '"v2"').orThrow();
1350
+ const syncPromise = accessors.syncToDisk();
1351
+ await jest.advanceTimersByTimeAsync(1500);
1352
+ const result = await syncPromise;
1353
+ expect(result).toFailWith(/service unavailable/i);
1354
+ // The PUT succeeded — data is on the server — so the file is no
1355
+ // longer dirty. Only the server-side /sync acknowledgement failed,
1356
+ // which is not something the client can meaningfully retry via
1357
+ // re-sending the file contents.
1358
+ expect(accessors.isDirty()).toBe(false);
1359
+ }
1360
+ finally {
1361
+ jest.useRealTimers();
1362
+ }
1363
+ });
1364
+ });
1001
1365
  });
1002
1366
  //# sourceMappingURL=httpTreeAccessors.test.js.map