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