sliftutils 1.7.87 → 1.7.89

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 (127) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/dist/treeSummary.ts.cache +2 -2
  3. package/index.d.ts +675 -296
  4. package/misc/dist/environment.ts.cache +2 -2
  5. package/misc/dist/getSecret.ts.cache +2 -2
  6. package/misc/dist/strings.ts.cache +2 -2
  7. package/misc/dist/zip.ts.cache +2 -2
  8. package/misc/https/dist/certs.ts.cache +2 -2
  9. package/misc/https/dist/cloudflareHelpers.ts.cache +2 -2
  10. package/misc/https/dist/dns.ts.cache +2 -2
  11. package/misc/https/dist/hostServer.ts.cache +49 -21
  12. package/misc/https/dist/hostsFile.ts.cache +82 -0
  13. package/misc/https/dist/httpsCerts.ts.cache +2 -2
  14. package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
  15. package/misc/https/hostServer.d.ts +6 -2
  16. package/misc/https/hostServer.ts +52 -19
  17. package/misc/https/hostsFile.d.ts +7 -0
  18. package/misc/https/hostsFile.ts +66 -0
  19. package/package.json +1 -1
  20. package/render-utils/dist/observer.tsx.cache +2 -2
  21. package/storage/ArchivesDisk.d.ts +3 -6
  22. package/storage/ArchivesDisk.ts +26 -2
  23. package/storage/BulkDatabase2/BulkDatabaseReader.ts +1 -1
  24. package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +2 -2
  25. package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +2 -2
  26. package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +2 -2
  27. package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +3 -3
  28. package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +2 -2
  29. package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +2 -2
  30. package/storage/BulkDatabase2/dist/blockCache.ts.cache +2 -2
  31. package/storage/BulkDatabase2/dist/mergeLock.ts.cache +2 -2
  32. package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +2 -2
  33. package/storage/BulkDatabase2/dist/streamLog.ts.cache +2 -2
  34. package/storage/BulkDatabase2/dist/syncClient.ts.cache +2 -2
  35. package/storage/IArchives.d.ts +37 -24
  36. package/storage/IArchives.ts +47 -62
  37. package/storage/LogMap.d.ts +56 -0
  38. package/storage/LogMap.ts +220 -0
  39. package/storage/archiveHelpers.d.ts +36 -0
  40. package/storage/archiveHelpers.ts +103 -0
  41. package/storage/backblaze.d.ts +7 -12
  42. package/storage/backblaze.ts +53 -48
  43. package/storage/dist/ArchivesDisk.ts.cache +28 -2
  44. package/storage/dist/FileFolderAPI.tsx.cache +2 -2
  45. package/storage/dist/IArchives.ts.cache +16 -52
  46. package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
  47. package/storage/dist/JSONStorage.ts.cache +2 -2
  48. package/storage/dist/LogMap.ts.cache +212 -0
  49. package/storage/dist/PendingManager.tsx.cache +2 -2
  50. package/storage/dist/TransactionStorage.ts.cache +2 -2
  51. package/storage/dist/backblaze.ts.cache +58 -44
  52. package/storage/dist/fileSystemPointer.ts.cache +2 -2
  53. package/storage/dist/remoteFileStorage.ts.cache +2 -2
  54. package/storage/remoteStorage/ArchivesDelayed.d.ts +66 -0
  55. package/storage/remoteStorage/ArchivesDelayed.ts +237 -0
  56. package/storage/remoteStorage/ArchivesRemote.d.ts +5 -6
  57. package/storage/remoteStorage/ArchivesRemote.ts +26 -5
  58. package/storage/remoteStorage/ArchivesUrl.d.ts +2 -5
  59. package/storage/remoteStorage/ArchivesUrl.ts +2 -2
  60. package/storage/remoteStorage/accessPage.tsx +12 -0
  61. package/storage/remoteStorage/accessStats.d.ts +1 -1
  62. package/storage/remoteStorage/accessStats.ts +3 -3
  63. package/storage/remoteStorage/blobStore.d.ts +162 -189
  64. package/storage/remoteStorage/blobStore.ts +495 -952
  65. package/storage/remoteStorage/bucketDisk.d.ts +24 -3
  66. package/storage/remoteStorage/bucketDisk.ts +56 -22
  67. package/storage/remoteStorage/certTrustModal.d.ts +1 -0
  68. package/storage/remoteStorage/certTrustModal.tsx +48 -0
  69. package/storage/remoteStorage/cliArgs.d.ts +2 -0
  70. package/storage/remoteStorage/cliArgs.ts +5 -0
  71. package/storage/remoteStorage/createArchives.d.ts +8 -7
  72. package/storage/remoteStorage/createArchives.ts +65 -26
  73. package/storage/remoteStorage/deployTakeover.ts +1 -1
  74. package/storage/remoteStorage/dist/ArchivesDelayed.ts.cache +220 -0
  75. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +25 -5
  76. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +3 -3
  77. package/storage/remoteStorage/dist/accessPage.tsx.cache +13 -3
  78. package/storage/remoteStorage/dist/accessStats.ts.cache +6 -6
  79. package/storage/remoteStorage/dist/blobStore.ts.cache +483 -965
  80. package/storage/remoteStorage/dist/bucketDisk.ts.cache +60 -29
  81. package/storage/remoteStorage/dist/certTrustModal.tsx.cache +55 -0
  82. package/storage/remoteStorage/dist/cliArgs.ts.cache +9 -4
  83. package/storage/remoteStorage/dist/createArchives.ts.cache +63 -26
  84. package/storage/remoteStorage/dist/deployTakeover.ts.cache +5 -5
  85. package/storage/remoteStorage/dist/intermediateManagement.ts.cache +286 -0
  86. package/storage/remoteStorage/dist/intermediateSources.ts.cache +2 -2
  87. package/storage/remoteStorage/dist/remoteConfig.ts.cache +122 -56
  88. package/storage/remoteStorage/dist/serverConfig.ts.cache +4 -4
  89. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +7 -11
  90. package/storage/remoteStorage/dist/sourcesList.ts.cache +3 -6
  91. package/storage/remoteStorage/dist/storageClientController.ts.cache +2 -2
  92. package/storage/remoteStorage/dist/storageController.ts.cache +27 -12
  93. package/storage/remoteStorage/dist/storageServer.ts.cache +27 -7
  94. package/storage/remoteStorage/dist/storageServerCli.ts.cache +7 -3
  95. package/storage/remoteStorage/dist/storageServerState.ts.cache +133 -696
  96. package/storage/remoteStorage/dist/storeConfig.ts.cache +52 -0
  97. package/storage/remoteStorage/dist/storePlan.ts.cache +110 -81
  98. package/storage/remoteStorage/dist/storeSources.ts.cache +51 -0
  99. package/storage/remoteStorage/dist/storeSync.ts.cache +650 -0
  100. package/storage/remoteStorage/dist/validation.ts.cache +14 -4
  101. package/storage/remoteStorage/intermediateManagement.d.ts +12 -0
  102. package/storage/remoteStorage/intermediateManagement.ts +264 -0
  103. package/storage/remoteStorage/remoteConfig.d.ts +32 -0
  104. package/storage/remoteStorage/remoteConfig.ts +115 -50
  105. package/storage/remoteStorage/serverConfig.d.ts +1 -1
  106. package/storage/remoteStorage/serverConfig.ts +1 -1
  107. package/storage/remoteStorage/sourceWrapper.ts +4 -8
  108. package/storage/remoteStorage/sourcesList.d.ts +0 -2
  109. package/storage/remoteStorage/sourcesList.ts +0 -3
  110. package/storage/remoteStorage/spec.md +18 -4
  111. package/storage/remoteStorage/storageController.d.ts +10 -0
  112. package/storage/remoteStorage/storageController.ts +29 -16
  113. package/storage/remoteStorage/storageServer.d.ts +2 -0
  114. package/storage/remoteStorage/storageServer.ts +28 -4
  115. package/storage/remoteStorage/storageServerCli.ts +5 -1
  116. package/storage/remoteStorage/storageServerState.d.ts +34 -21
  117. package/storage/remoteStorage/storageServerState.ts +147 -662
  118. package/storage/remoteStorage/storeConfig.d.ts +30 -0
  119. package/storage/remoteStorage/storeConfig.ts +62 -0
  120. package/storage/remoteStorage/storePlan.d.ts +29 -17
  121. package/storage/remoteStorage/storePlan.ts +112 -85
  122. package/storage/remoteStorage/storeSources.d.ts +11 -0
  123. package/storage/remoteStorage/storeSources.ts +47 -0
  124. package/storage/remoteStorage/storeSync.d.ts +53 -0
  125. package/storage/remoteStorage/storeSync.ts +632 -0
  126. package/storage/remoteStorage/validation.d.ts +2 -0
  127. package/storage/remoteStorage/validation.ts +10 -0
@@ -1,12 +1,22 @@
1
- "use strict"; var console = (function shimConsole() { let baseConsole = globalThis.console; if (typeof window !== "undefined") return baseConsole; let name = __filename.replaceAll("\\", "/").split("/").pop(); let dirName = __dirname.replaceAll("\\", "/").split("/").pop(); let context = { __FILE__: __filename, __DIR__: __dirname, __NAME__: name, __DIRNAME__: dirName }; let newConsole = { ...baseConsole }; for (let shim of ["log", "info", "warn", "error"]) { newConsole[shim] = (...args) => { args.push(context); baseConsole[shim](...args); }; } newConsole.__context = context; return newConsole; })(); "use strict";
1
+ "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
3
- //exports.assertValidArgs = exports.assertValidPath = exports.assertValidName = void 0;
3
+ //exports.assertValidArgs = exports.assertValidPath = exports.assertValidSourceName = exports.assertValidName = void 0;
4
4
  function assertValidName(value, kind) {
5
5
  if (!/^[\w-]{1,64}$/.test(value)) {
6
6
  throw new Error(`Invalid ${kind} ${JSON.stringify(value)}, expected 1-64 characters of letters/numbers/underscore/dash`);
7
7
  }
8
8
  }
9
9
  exports.assertValidName = assertValidName;
10
+ /** A store's name (see CommonConfig.name), which also allows dots - a name is often a host or a version, and both read wrong without them. It is one path segment of the store's folder, so the two names that would mean a different folder entirely are rejected: everything else containing dots is just a name. */
11
+ function assertValidSourceName(value) {
12
+ if (!/^[\w.-]{1,64}$/.test(value)) {
13
+ throw new Error(`Invalid source name ${JSON.stringify(value)}, expected 1-64 characters of letters/numbers/underscore/dash/dot`);
14
+ }
15
+ if (/^\.+$/.test(value)) {
16
+ throw new Error(`Invalid source name ${JSON.stringify(value)}: a name of only dots refers to a directory rather than naming one`);
17
+ }
18
+ }
19
+ exports.assertValidSourceName = assertValidSourceName;
10
20
  function assertValidPath(path) {
11
21
  if (Buffer.from(path, "utf8").length > 1000) {
12
22
  throw new Error(`Path too long: ${path.length} characters > 1000. Path: ${path.slice(0, 200)}`);
@@ -36,5 +46,5 @@ function assertValidArgs(target, key, descriptor) {
36
46
  };
37
47
  }
38
48
  exports.assertValidArgs = assertValidArgs;
39
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInZhbGlkYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsU0FBZ0IsZUFBZSxDQUFDLEtBQWEsRUFBRSxJQUFZO0lBQ3ZELElBQUksQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDL0IsTUFBTSxJQUFJLEtBQUssQ0FBQyxXQUFXLElBQUksSUFBSSxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQywrREFBK0QsQ0FBQyxDQUFDO0lBQzdILENBQUM7QUFDTCxDQUFDO0FBSkQsMENBSUM7QUFFRCxTQUFnQixlQUFlLENBQUMsSUFBWTtJQUN4QyxJQUFJLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLE1BQU0sQ0FBQyxDQUFDLE1BQU0sR0FBRyxJQUFJLEVBQUUsQ0FBQztRQUMxQyxNQUFNLElBQUksS0FBSyxDQUFDLGtCQUFrQixJQUFJLENBQUMsTUFBTSw2QkFBNkIsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ3BHLENBQUM7SUFDRCxJQUFJLENBQUMsSUFBSSxJQUFJLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsSUFBSSxJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQzdILE1BQU0sSUFBSSxLQUFLLENBQUMsZ0JBQWdCLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsOEZBQThGLENBQUMsQ0FBQztJQUN0SyxDQUFDO0lBQ0QsSUFBSSxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLElBQUksS0FBSyxHQUFHLElBQUksSUFBSSxLQUFLLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDOUQsTUFBTSxJQUFJLEtBQUssQ0FBQyxnQkFBZ0IsSUFBSSxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyx5Q0FBeUMsQ0FBQyxDQUFDO0lBQ2pILENBQUM7QUFDTCxDQUFDO0FBVkQsMENBVUM7QUFFRCw0V0FBNFc7QUFDNVcsU0FBZ0IsZUFBZSxDQUFDLE1BQWUsRUFBRSxHQUFXLEVBQUUsVUFBOEI7SUFDeEYsSUFBSSxRQUFRLEdBQUcsVUFBVSxDQUFDLEtBQXdDLENBQUM7SUFDbkUsVUFBVSxDQUFDLEtBQUssR0FBRyxVQUFVLEdBQUcsSUFBZTtRQUMzQyxJQUFJLE1BQU0sR0FBRyxJQUFJLENBQUMsQ0FBQyxDQUF5RSxDQUFDO1FBQzdGLElBQUksTUFBTSxJQUFJLE9BQU8sTUFBTSxLQUFLLFFBQVEsRUFBRSxDQUFDO1lBQ3ZDLElBQUksU0FBUyxJQUFJLE1BQU07Z0JBQUUsZUFBZSxDQUFDLE1BQU0sQ0FBQyxPQUFpQixFQUFFLFNBQVMsQ0FBQyxDQUFDO1lBQzlFLElBQUksWUFBWSxJQUFJLE1BQU07Z0JBQUUsZUFBZSxDQUFDLE1BQU0sQ0FBQyxVQUFvQixFQUFFLGFBQWEsQ0FBQyxDQUFDO1lBQ3hGLElBQUksTUFBTSxJQUFJLE1BQU07Z0JBQUUsZUFBZSxDQUFDLE1BQU0sQ0FBQyxJQUFjLENBQUMsQ0FBQztRQUNqRSxDQUFDO1FBQ0QsT0FBTyxRQUFRLENBQUMsS0FBSyxDQUFDLElBQUksRUFBRSxJQUFJLENBQUMsQ0FBQztJQUN0QyxDQUFDLENBQUM7QUFDTixDQUFDO0FBWEQsMENBV0MiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gYXNzZXJ0VmFsaWROYW1lKHZhbHVlOiBzdHJpbmcsIGtpbmQ6IHN0cmluZyk6IHZvaWQge1xuICAgIGlmICghL15bXFx3LV17MSw2NH0kLy50ZXN0KHZhbHVlKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgJHtraW5kfSAke0pTT04uc3RyaW5naWZ5KHZhbHVlKX0sIGV4cGVjdGVkIDEtNjQgY2hhcmFjdGVycyBvZiBsZXR0ZXJzL251bWJlcnMvdW5kZXJzY29yZS9kYXNoYCk7XG4gICAgfVxufVxuXG5leHBvcnQgZnVuY3Rpb24gYXNzZXJ0VmFsaWRQYXRoKHBhdGg6IHN0cmluZyk6IHZvaWQge1xuICAgIGlmIChCdWZmZXIuZnJvbShwYXRoLCBcInV0ZjhcIikubGVuZ3RoID4gMTAwMCkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFBhdGggdG9vIGxvbmc6ICR7cGF0aC5sZW5ndGh9IGNoYXJhY3RlcnMgPiAxMDAwLiBQYXRoOiAke3BhdGguc2xpY2UoMCwgMjAwKX1gKTtcbiAgICB9XG4gICAgaWYgKCFwYXRoIHx8IHBhdGguc3RhcnRzV2l0aChcIi9cIikgfHwgcGF0aC5lbmRzV2l0aChcIi9cIikgfHwgcGF0aC5pbmNsdWRlcyhcIi8vXCIpIHx8IHBhdGguaW5jbHVkZXMoXCJcXFxcXCIpIHx8IHBhdGguaW5jbHVkZXMoXCJcXHgwMFwiKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgcGF0aCAke0pTT04uc3RyaW5naWZ5KHBhdGguc2xpY2UoMCwgMjAwKSl9LCBwYXRocyBjYW5ub3QgYmUgZW1wdHksIHN0YXJ0IG9yIGVuZCB3aXRoIC8sIG9yIGNvbnRhaW4gLy8sIGJhY2tzbGFzaGVzLCBvciBudWxsIGNoYXJhY3RlcnNgKTtcbiAgICB9XG4gICAgaWYgKHBhdGguc3BsaXQoXCIvXCIpLnNvbWUocGFydCA9PiBwYXJ0ID09PSBcIi5cIiB8fCBwYXJ0ID09PSBcIi4uXCIpKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgSW52YWxpZCBwYXRoICR7SlNPTi5zdHJpbmdpZnkocGF0aC5zbGljZSgwLCAyMDApKX0sIHBhdGhzIGNhbm5vdCBjb250YWluIC4gb3IgLi4gc2VnbWVudHNgKTtcbiAgICB9XG59XG5cbi8qKiBNZXRob2QgZGVjb3JhdG9yOiB2YWxpZGF0ZXMgdGhlIHdlbGwta25vd24gZmllbGRzIG9mIHRoZSBtZXRob2QncyBzaW5nbGUgY29uZmlnLW9iamVjdCBhcmd1bWVudCAtIGFjY291bnQvYnVja2V0TmFtZSBhcyBuYW1lcywgcGF0aCBhcyBhIHBhdGggLSBiZWZvcmUgdGhlIG1ldGhvZCBydW5zLiBGaWVsZHMgdGhlIGNvbmZpZyBkb2Vzbid0IGhhdmUgYXJlIHNraXBwZWQsIHNvIGl0IGFwcGxpZXMgdG8gZXZlcnkgQVBJIG1ldGhvZCB1bmlmb3JtbHkuIHByZWZpeCBpcyBkZWxpYmVyYXRlbHkgTk9UIHZhbGlkYXRlZDogcHJlZml4ZXMgbWF5IGJlIGVtcHR5IG9yIGVuZCB3aXRoIFwiL1wiLCBib3RoIGludmFsaWQgZm9yIHBhdGhzLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGFzc2VydFZhbGlkQXJncyh0YXJnZXQ6IHVua25vd24sIGtleTogc3RyaW5nLCBkZXNjcmlwdG9yOiBQcm9wZXJ0eURlc2NyaXB0b3IpOiB2b2lkIHtcbiAgICBsZXQgb3JpZ2luYWwgPSBkZXNjcmlwdG9yLnZhbHVlIGFzICguLi5hcmdzOiB1bmtub3duW10pID0+IHVua25vd247XG4gICAgZGVzY3JpcHRvci52YWx1ZSA9IGZ1bmN0aW9uICguLi5hcmdzOiB1bmtub3duW10pOiB1bmtub3duIHtcbiAgICAgICAgbGV0IGNvbmZpZyA9IGFyZ3NbMF0gYXMgeyBhY2NvdW50Pzogc3RyaW5nOyBidWNrZXROYW1lPzogc3RyaW5nOyBwYXRoPzogc3RyaW5nIH0gfCB1bmRlZmluZWQ7XG4gICAgICAgIGlmIChjb25maWcgJiYgdHlwZW9mIGNvbmZpZyA9PT0gXCJvYmplY3RcIikge1xuICAgICAgICAgICAgaWYgKFwiYWNjb3VudFwiIGluIGNvbmZpZykgYXNzZXJ0VmFsaWROYW1lKGNvbmZpZy5hY2NvdW50IGFzIHN0cmluZywgXCJhY2NvdW50XCIpO1xuICAgICAgICAgICAgaWYgKFwiYnVja2V0TmFtZVwiIGluIGNvbmZpZykgYXNzZXJ0VmFsaWROYW1lKGNvbmZpZy5idWNrZXROYW1lIGFzIHN0cmluZywgXCJidWNrZXQgbmFtZVwiKTtcbiAgICAgICAgICAgIGlmIChcInBhdGhcIiBpbiBjb25maWcpIGFzc2VydFZhbGlkUGF0aChjb25maWcucGF0aCBhcyBzdHJpbmcpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBvcmlnaW5hbC5hcHBseSh0aGlzLCBhcmdzKTtcbiAgICB9O1xufVxuIl19
40
- /* _JS_SOURCE_HASH = "05503760cddd9cddffc575307a41c7effd2a063244c8cff7064c8aeb7b0aa0f9"; */
49
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInZhbGlkYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsU0FBZ0IsZUFBZSxDQUFDLEtBQWEsRUFBRSxJQUFZO0lBQ3ZELElBQUksQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDL0IsTUFBTSxJQUFJLEtBQUssQ0FBQyxXQUFXLElBQUksSUFBSSxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQywrREFBK0QsQ0FBQyxDQUFDO0lBQzdILENBQUM7QUFDTCxDQUFDO0FBSkQsMENBSUM7QUFFRCx1VEFBdVQ7QUFDdlQsU0FBZ0IscUJBQXFCLENBQUMsS0FBYTtJQUMvQyxJQUFJLENBQUMsZ0JBQWdCLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDaEMsTUFBTSxJQUFJLEtBQUssQ0FBQyx1QkFBdUIsSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsbUVBQW1FLENBQUMsQ0FBQztJQUNySSxDQUFDO0lBQ0QsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDdEIsTUFBTSxJQUFJLEtBQUssQ0FBQyx1QkFBdUIsSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsb0VBQW9FLENBQUMsQ0FBQztJQUN0SSxDQUFDO0FBQ0wsQ0FBQztBQVBELHNEQU9DO0FBRUQsU0FBZ0IsZUFBZSxDQUFDLElBQVk7SUFDeEMsSUFBSSxNQUFNLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxNQUFNLENBQUMsQ0FBQyxNQUFNLEdBQUcsSUFBSSxFQUFFLENBQUM7UUFDMUMsTUFBTSxJQUFJLEtBQUssQ0FBQyxrQkFBa0IsSUFBSSxDQUFDLE1BQU0sNkJBQTZCLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUNwRyxDQUFDO0lBQ0QsSUFBSSxDQUFDLElBQUksSUFBSSxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsSUFBSSxJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztRQUM3SCxNQUFNLElBQUksS0FBSyxDQUFDLGdCQUFnQixJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxDQUFDLDhGQUE4RixDQUFDLENBQUM7SUFDdEssQ0FBQztJQUNELElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLEtBQUssR0FBRyxJQUFJLElBQUksS0FBSyxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQzlELE1BQU0sSUFBSSxLQUFLLENBQUMsZ0JBQWdCLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMseUNBQXlDLENBQUMsQ0FBQztJQUNqSCxDQUFDO0FBQ0wsQ0FBQztBQVZELDBDQVVDO0FBRUQsNFdBQTRXO0FBQzVXLFNBQWdCLGVBQWUsQ0FBQyxNQUFlLEVBQUUsR0FBVyxFQUFFLFVBQThCO0lBQ3hGLElBQUksUUFBUSxHQUFHLFVBQVUsQ0FBQyxLQUF3QyxDQUFDO0lBQ25FLFVBQVUsQ0FBQyxLQUFLLEdBQUcsVUFBVSxHQUFHLElBQWU7UUFDM0MsSUFBSSxNQUFNLEdBQUcsSUFBSSxDQUFDLENBQUMsQ0FBeUUsQ0FBQztRQUM3RixJQUFJLE1BQU0sSUFBSSxPQUFPLE1BQU0sS0FBSyxRQUFRLEVBQUUsQ0FBQztZQUN2QyxJQUFJLFNBQVMsSUFBSSxNQUFNO2dCQUFFLGVBQWUsQ0FBQyxNQUFNLENBQUMsT0FBaUIsRUFBRSxTQUFTLENBQUMsQ0FBQztZQUM5RSxJQUFJLFlBQVksSUFBSSxNQUFNO2dCQUFFLGVBQWUsQ0FBQyxNQUFNLENBQUMsVUFBb0IsRUFBRSxhQUFhLENBQUMsQ0FBQztZQUN4RixJQUFJLE1BQU0sSUFBSSxNQUFNO2dCQUFFLGVBQWUsQ0FBQyxNQUFNLENBQUMsSUFBYyxDQUFDLENBQUM7UUFDakUsQ0FBQztRQUNELE9BQU8sUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLENBQUM7SUFDdEMsQ0FBQyxDQUFDO0FBQ04sQ0FBQztBQVhELDBDQVdDIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIGFzc2VydFZhbGlkTmFtZSh2YWx1ZTogc3RyaW5nLCBraW5kOiBzdHJpbmcpOiB2b2lkIHtcbiAgICBpZiAoIS9eW1xcdy1dezEsNjR9JC8udGVzdCh2YWx1ZSkpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBJbnZhbGlkICR7a2luZH0gJHtKU09OLnN0cmluZ2lmeSh2YWx1ZSl9LCBleHBlY3RlZCAxLTY0IGNoYXJhY3RlcnMgb2YgbGV0dGVycy9udW1iZXJzL3VuZGVyc2NvcmUvZGFzaGApO1xuICAgIH1cbn1cblxuLyoqIEEgc3RvcmUncyBuYW1lIChzZWUgQ29tbW9uQ29uZmlnLm5hbWUpLCB3aGljaCBhbHNvIGFsbG93cyBkb3RzIC0gYSBuYW1lIGlzIG9mdGVuIGEgaG9zdCBvciBhIHZlcnNpb24sIGFuZCBib3RoIHJlYWQgd3Jvbmcgd2l0aG91dCB0aGVtLiBJdCBpcyBvbmUgcGF0aCBzZWdtZW50IG9mIHRoZSBzdG9yZSdzIGZvbGRlciwgc28gdGhlIHR3byBuYW1lcyB0aGF0IHdvdWxkIG1lYW4gYSBkaWZmZXJlbnQgZm9sZGVyIGVudGlyZWx5IGFyZSByZWplY3RlZDogZXZlcnl0aGluZyBlbHNlIGNvbnRhaW5pbmcgZG90cyBpcyBqdXN0IGEgbmFtZS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnRWYWxpZFNvdXJjZU5hbWUodmFsdWU6IHN0cmluZyk6IHZvaWQge1xuICAgIGlmICghL15bXFx3Li1dezEsNjR9JC8udGVzdCh2YWx1ZSkpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBJbnZhbGlkIHNvdXJjZSBuYW1lICR7SlNPTi5zdHJpbmdpZnkodmFsdWUpfSwgZXhwZWN0ZWQgMS02NCBjaGFyYWN0ZXJzIG9mIGxldHRlcnMvbnVtYmVycy91bmRlcnNjb3JlL2Rhc2gvZG90YCk7XG4gICAgfVxuICAgIGlmICgvXlxcLiskLy50ZXN0KHZhbHVlKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgc291cmNlIG5hbWUgJHtKU09OLnN0cmluZ2lmeSh2YWx1ZSl9OiBhIG5hbWUgb2Ygb25seSBkb3RzIHJlZmVycyB0byBhIGRpcmVjdG9yeSByYXRoZXIgdGhhbiBuYW1pbmcgb25lYCk7XG4gICAgfVxufVxuXG5leHBvcnQgZnVuY3Rpb24gYXNzZXJ0VmFsaWRQYXRoKHBhdGg6IHN0cmluZyk6IHZvaWQge1xuICAgIGlmIChCdWZmZXIuZnJvbShwYXRoLCBcInV0ZjhcIikubGVuZ3RoID4gMTAwMCkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFBhdGggdG9vIGxvbmc6ICR7cGF0aC5sZW5ndGh9IGNoYXJhY3RlcnMgPiAxMDAwLiBQYXRoOiAke3BhdGguc2xpY2UoMCwgMjAwKX1gKTtcbiAgICB9XG4gICAgaWYgKCFwYXRoIHx8IHBhdGguc3RhcnRzV2l0aChcIi9cIikgfHwgcGF0aC5lbmRzV2l0aChcIi9cIikgfHwgcGF0aC5pbmNsdWRlcyhcIi8vXCIpIHx8IHBhdGguaW5jbHVkZXMoXCJcXFxcXCIpIHx8IHBhdGguaW5jbHVkZXMoXCJcXHgwMFwiKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgcGF0aCAke0pTT04uc3RyaW5naWZ5KHBhdGguc2xpY2UoMCwgMjAwKSl9LCBwYXRocyBjYW5ub3QgYmUgZW1wdHksIHN0YXJ0IG9yIGVuZCB3aXRoIC8sIG9yIGNvbnRhaW4gLy8sIGJhY2tzbGFzaGVzLCBvciBudWxsIGNoYXJhY3RlcnNgKTtcbiAgICB9XG4gICAgaWYgKHBhdGguc3BsaXQoXCIvXCIpLnNvbWUocGFydCA9PiBwYXJ0ID09PSBcIi5cIiB8fCBwYXJ0ID09PSBcIi4uXCIpKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgSW52YWxpZCBwYXRoICR7SlNPTi5zdHJpbmdpZnkocGF0aC5zbGljZSgwLCAyMDApKX0sIHBhdGhzIGNhbm5vdCBjb250YWluIC4gb3IgLi4gc2VnbWVudHNgKTtcbiAgICB9XG59XG5cbi8qKiBNZXRob2QgZGVjb3JhdG9yOiB2YWxpZGF0ZXMgdGhlIHdlbGwta25vd24gZmllbGRzIG9mIHRoZSBtZXRob2QncyBzaW5nbGUgY29uZmlnLW9iamVjdCBhcmd1bWVudCAtIGFjY291bnQvYnVja2V0TmFtZSBhcyBuYW1lcywgcGF0aCBhcyBhIHBhdGggLSBiZWZvcmUgdGhlIG1ldGhvZCBydW5zLiBGaWVsZHMgdGhlIGNvbmZpZyBkb2Vzbid0IGhhdmUgYXJlIHNraXBwZWQsIHNvIGl0IGFwcGxpZXMgdG8gZXZlcnkgQVBJIG1ldGhvZCB1bmlmb3JtbHkuIHByZWZpeCBpcyBkZWxpYmVyYXRlbHkgTk9UIHZhbGlkYXRlZDogcHJlZml4ZXMgbWF5IGJlIGVtcHR5IG9yIGVuZCB3aXRoIFwiL1wiLCBib3RoIGludmFsaWQgZm9yIHBhdGhzLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGFzc2VydFZhbGlkQXJncyh0YXJnZXQ6IHVua25vd24sIGtleTogc3RyaW5nLCBkZXNjcmlwdG9yOiBQcm9wZXJ0eURlc2NyaXB0b3IpOiB2b2lkIHtcbiAgICBsZXQgb3JpZ2luYWwgPSBkZXNjcmlwdG9yLnZhbHVlIGFzICguLi5hcmdzOiB1bmtub3duW10pID0+IHVua25vd247XG4gICAgZGVzY3JpcHRvci52YWx1ZSA9IGZ1bmN0aW9uICguLi5hcmdzOiB1bmtub3duW10pOiB1bmtub3duIHtcbiAgICAgICAgbGV0IGNvbmZpZyA9IGFyZ3NbMF0gYXMgeyBhY2NvdW50Pzogc3RyaW5nOyBidWNrZXROYW1lPzogc3RyaW5nOyBwYXRoPzogc3RyaW5nIH0gfCB1bmRlZmluZWQ7XG4gICAgICAgIGlmIChjb25maWcgJiYgdHlwZW9mIGNvbmZpZyA9PT0gXCJvYmplY3RcIikge1xuICAgICAgICAgICAgaWYgKFwiYWNjb3VudFwiIGluIGNvbmZpZykgYXNzZXJ0VmFsaWROYW1lKGNvbmZpZy5hY2NvdW50IGFzIHN0cmluZywgXCJhY2NvdW50XCIpO1xuICAgICAgICAgICAgaWYgKFwiYnVja2V0TmFtZVwiIGluIGNvbmZpZykgYXNzZXJ0VmFsaWROYW1lKGNvbmZpZy5idWNrZXROYW1lIGFzIHN0cmluZywgXCJidWNrZXQgbmFtZVwiKTtcbiAgICAgICAgICAgIGlmIChcInBhdGhcIiBpbiBjb25maWcpIGFzc2VydFZhbGlkUGF0aChjb25maWcucGF0aCBhcyBzdHJpbmcpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBvcmlnaW5hbC5hcHBseSh0aGlzLCBhcmdzKTtcbiAgICB9O1xufVxuIl19
50
+ /* _JS_SOURCE_HASH = "52f73d0ef7e2f65848acc68373c50e34bd39c017c3ff908220fc66a6f3d0296f"; */
@@ -0,0 +1,12 @@
1
+ import { RemoteConfig } from "../IArchives";
2
+ import { BucketState } from "./storageServerState";
3
+ /** Called whenever a bucket's state is (re)built: arms the scans its upcoming window boundaries need. Each scan is scheduled once - the key includes the boundary it is for - so re-arming on every config change is harmless. */
4
+ export declare function scheduleBoundaryWork(loaded: BucketState): void;
5
+ /** An operator's config knows nothing about a switchover that is in flight right now, so writing it as-is would cancel one mid-handover. The in-flight windows are put back into it first. */
6
+ export declare function reinjectIntermediates(current: RemoteConfig | undefined, incoming: RemoteConfig): RemoteConfig;
7
+ /** Started by deployTakeover once we are actually a deploy successor listening on an alternate port. Until then there are no switchover windows to write or expire, so nothing polls. */
8
+ export declare const startIntermediateMaintenance: {
9
+ (): void;
10
+ reset(): void;
11
+ set(newValue: void): void;
12
+ };
@@ -0,0 +1,264 @@
1
+ import { lazy } from "socket-function/src/caching";
2
+ import { runInfinitePoll } from "socket-function/src/batching";
3
+ import { RemoteConfig, SourceConfig, HostedConfig } from "../IArchives";
4
+ import { ROUTING_FILE, getConfigVersion, serializeRemoteConfig, parseHostedUrl, replaceHostedUrlPort } from "./remoteConfig";
5
+ import { injectIntermediateSource, expireIntermediateSources, getIntermediateSources, nextIntermediateVersion, INTERMEDIATE_EXPIRE_GRACE } from "./intermediateSources";
6
+ import { getTakeoverIntermediate } from "./deployTakeover";
7
+ import { getStorageServerConfig } from "./serverConfig";
8
+ import { createApiArchives, listServerActiveBucketKeys } from "./createArchives";
9
+ import { findSelfIndexes, previousWindowOwners } from "./storePlan";
10
+ import { BucketState, getBucketStates, getLoadedBucket, writeRoutingConfig } from "./storageServerState";
11
+ import { DEFAULT_FAST_WRITE_DELAY } from "./ArchivesDelayed";
12
+ import { WINDOW_END_FLUSH_MARGIN } from "./blobStore";
13
+
14
+ // Deploy switchovers, and the handovers they cause. A switchover is a source moving to a new port for a few minutes: we give it a virtual valid window on that port (an "intermediate"), which means the write target changes twice in quick succession, which means the store taking over has to go and collect the writes that landed just before it did. None of that is storage - it is scheduling - so it lives here rather than in the store or in the server's bucket state.
15
+
16
+ const MAX_TIMER_DELAY = 2 ** 31 - 1;
17
+ const BOUNDARY_BUFFER = 1000;
18
+ // A handover is scanned just before the boundary, just after, and again a little later - the last writes to the old target can land after the boundary itself
19
+ const BOUNDARY_SCAN_OFFSETS = [-30 * 1000, 2 * 1000, 30 * 1000];
20
+ // How far back a boundary scan asks for changes: everything that could still have been buffered when the window ended
21
+ const BOUNDARY_SCAN_LOOKBACK = DEFAULT_FAST_WRITE_DELAY + WINDOW_END_FLUSH_MARGIN;
22
+ const STARTUP_BOUNDARY_SCAN_DELAY = 30 * 1000;
23
+ const OWN_CONFIG_WRITE_THROTTLE = 30 * 1000;
24
+ const INTERMEDIATE_MAINTAIN_INTERVAL = 60 * 1000;
25
+
26
+ const scheduledBoundaryScans = new Set<string>();
27
+ const boundaryScansRunning = new Set<string>();
28
+
29
+ /** Called whenever a bucket's state is (re)built: arms the scans its upcoming window boundaries need. Each scan is scheduled once - the key includes the boundary it is for - so re-arming on every config change is harmless. */
30
+ export function scheduleBoundaryWork(loaded: BucketState): void {
31
+ let key = `${loaded.account}/${loaded.bucketName}`;
32
+ let now = Date.now();
33
+ let starts = new Set<number>();
34
+ for (let self of loaded.selfEntries) {
35
+ let start = self.validWindow[0];
36
+ if (start > now && start < Number.MAX_SAFE_INTEGER) {
37
+ starts.add(start);
38
+ }
39
+ }
40
+ // A window we have only just taken over, on a server that has only just started: the boundary happened while we were not running, so its scan still has to happen
41
+ for (let self of loaded.selfEntries) {
42
+ let [start, end] = self.validWindow;
43
+ if (!(start <= now && now < end) || start <= 0) continue;
44
+ if (now - start > BOUNDARY_SCAN_LOOKBACK) continue;
45
+ let scheduleKey = `${key}|${start}|startup`;
46
+ if (scheduledBoundaryScans.has(scheduleKey)) continue;
47
+ scheduledBoundaryScans.add(scheduleKey);
48
+ let timer = setTimeout(() => {
49
+ void runBoundaryScan(loaded, start, STARTUP_BOUNDARY_SCAN_DELAY, "startup+30s").catch((e: Error) => console.error(`Boundary scan for bucket ${key} failed: ${e.stack ?? e}`));
50
+ }, STARTUP_BOUNDARY_SCAN_DELAY);
51
+ (timer as { unref?: () => void }).unref?.();
52
+ }
53
+ for (let start of starts) {
54
+ for (let offset of BOUNDARY_SCAN_OFFSETS) {
55
+ let at = start + offset;
56
+ if (at <= now) continue;
57
+ let scheduleKey = `${key}|${start}|${offset}`;
58
+ if (scheduledBoundaryScans.has(scheduleKey)) continue;
59
+ scheduledBoundaryScans.add(scheduleKey);
60
+ // Re-armed rather than trusted: a long timer can fire early after a suspend, and firing early would scan for a handover that has not happened
61
+ let arm = () => {
62
+ let timer = setTimeout(() => {
63
+ if (Date.now() < at) {
64
+ arm();
65
+ return;
66
+ }
67
+ void runBoundaryScan(loaded, start, offset).catch((e: Error) => console.error(`Boundary scan for bucket ${key} failed: ${e.stack ?? e}`));
68
+ }, Math.min(at - Date.now(), MAX_TIMER_DELAY));
69
+ (timer as { unref?: () => void }).unref?.();
70
+ };
71
+ arm();
72
+ }
73
+ }
74
+ }
75
+
76
+ /** Collects the writes that landed on the previous window's owners just before a boundary, into the store that has just taken over. Who owned what is worked out purely from the config (see previousWindowOwners); this only does the pulling. */
77
+ async function runBoundaryScan(loaded: BucketState, windowStart: number, offset: number, offsetLabel?: string): Promise<void> {
78
+ let key = `${loaded.account}/${loaded.bucketName}`;
79
+ let label = `bucket ${key}, window starting ${new Date(windowStart).toISOString()}, offset ${offsetLabel || `${offset / 1000}s`}`;
80
+ if (boundaryScansRunning.has(key)) {
81
+ console.log(`Skipping boundary scan (${label}): the previous boundary scan is still running`);
82
+ return;
83
+ }
84
+ boundaryScansRunning.add(key);
85
+ try {
86
+ let effective = loaded.routing;
87
+ if (!effective) return;
88
+ let handovers = previousWindowOwners(effective, windowStart, findSelfIndexes(effective, loaded.account, loaded.bucketName));
89
+ if (!handovers.length) return;
90
+ console.log(`Boundary scan (${label}): ${handovers.length} route(s) taking over: ${handovers.map(x => `${JSON.stringify(x.route)} from ${x.scanOwnDisk && "our own disk" || ""}${x.scanOwnDisk && x.remotes.size && " + " || ""}${[...x.remotes.keys()].map(i => `source ${i}`).join(", ")}`).join("; ")}`);
91
+ let since = windowStart - BOUNDARY_SCAN_LOOKBACK;
92
+ for (let handover of handovers) {
93
+ let selfStore = loaded.stores.find(x => x.name === handover.name);
94
+ if (!selfStore) continue;
95
+ if (handover.scanOwnDisk) {
96
+ await selfStore.store.rescanBase();
97
+ }
98
+ for (let [sourceIndex, route] of handover.remotes) {
99
+ let ownerSource = effective.sources[sourceIndex];
100
+ if (typeof ownerSource === "string") continue;
101
+ try {
102
+ await selfStore.store.boundaryScanRemote(createApiArchives(ownerSource), { since, route });
103
+ } catch (e) {
104
+ console.error(`Boundary scan (${label}) of previous-window owner (source index ${sourceIndex}) failed: ${(e as Error).stack ?? e}`);
105
+ }
106
+ }
107
+ }
108
+ } finally {
109
+ boundaryScansRunning.delete(key);
110
+ }
111
+ }
112
+
113
+ /** An operator's config knows nothing about a switchover that is in flight right now, so writing it as-is would cancel one mid-handover. The in-flight windows are put back into it first. */
114
+ export function reinjectIntermediates(current: RemoteConfig | undefined, incoming: RemoteConfig): RemoteConfig {
115
+ if (!current) return incoming;
116
+ let stored = incoming;
117
+ let reinjected = 0;
118
+ for (let intermediate of getIntermediateSources(expireIntermediateSources(current, Date.now()))) {
119
+ stored = injectIntermediateSource(stored, {
120
+ splitUrl: intermediate.intermediate || intermediate.url,
121
+ intermediateUrl: intermediate.url,
122
+ start: intermediate.validWindow[0],
123
+ end: intermediate.validWindow[1],
124
+ });
125
+ reinjected++;
126
+ }
127
+ if (reinjected) {
128
+ console.log(`Re-injected ${reinjected} in-flight switchover window(s) into an incoming routing config (version ${getConfigVersion(incoming)})`);
129
+ }
130
+ return stored;
131
+ }
132
+
133
+ // Per bucket, not global: a switchover has to write every bucket's windows at once, and a shared throttle would let only one bucket through per interval
134
+ const lastOwnConfigWrite = new Map<string, number>();
135
+
136
+ async function writeOwnRoutingConfig(loaded: BucketState, updated: RemoteConfig, reason: string): Promise<void> {
137
+ let key = `${loaded.account}/${loaded.bucketName}`;
138
+ let sinceLast = Date.now() - (lastOwnConfigWrite.get(key) || 0);
139
+ if (sinceLast < OWN_CONFIG_WRITE_THROTTLE) {
140
+ console.log(`Not writing our own routing config update for bucket ${key} (${reason}): the last one was ${sinceLast}ms ago, under the ${OWN_CONFIG_WRITE_THROTTLE}ms throttle. Retrying on the next maintenance pass.`);
141
+ return;
142
+ }
143
+ lastOwnConfigWrite.set(key, Date.now());
144
+ // A switchover writes on top of whatever the bucket is running; with no config at all there is nothing to switch over
145
+ if (!loaded.routing) return;
146
+ let currentVersion = getConfigVersion(loaded.routing);
147
+ let version = nextIntermediateVersion(currentVersion);
148
+ let next: RemoteConfig = { ...updated, version };
149
+ console.log(`Writing ${ROUTING_FILE} for bucket ${key} (${reason}), version ${currentVersion} -> ${version}: ${JSON.stringify(next)}`);
150
+ let data = Buffer.from(serializeRemoteConfig(next));
151
+ // Into one of our own stores - it applies it and its peers pull it - and directly to the other servers, which is what makes a switchover take effect in seconds rather than at the next poll
152
+ let ourStore = loaded.stores[0];
153
+ if (ourStore) {
154
+ await writeRoutingConfig(loaded.account, loaded.bucketName, ourStore.name, data);
155
+ }
156
+ await propagateRoutingConfig(loaded, next, data);
157
+ }
158
+
159
+ async function propagateRoutingConfig(loaded: BucketState, next: RemoteConfig, data: Buffer): Promise<void> {
160
+ let targets = new Map<string, SourceConfig>();
161
+ for (let source of [...loaded.routing?.sources || [], ...next.sources]) {
162
+ if (typeof source === "string" || source.intermediate) continue;
163
+ if (source.type === "remote") {
164
+ let parsed = parseHostedUrl(source.url);
165
+ if (parsed.account !== loaded.account || parsed.bucketName !== loaded.bucketName) continue;
166
+ }
167
+ if (targets.has(source.url)) continue;
168
+ targets.set(source.url, source);
169
+ }
170
+ for (let [url, source] of targets) {
171
+ try {
172
+ await createApiArchives(source).set(ROUTING_FILE, data);
173
+ console.log(`Wrote ${ROUTING_FILE} for bucket ${loaded.account}/${loaded.bucketName} to ${url}`);
174
+ } catch (e) {
175
+ console.error(`Propagating our routing config update to ${url} failed: ${(e as Error).stack ?? e}`);
176
+ }
177
+ }
178
+ }
179
+
180
+ /** Keeps every loaded bucket's switchover windows current: gives our alternate port its window while a takeover is in flight, and takes expired ones back out. */
181
+ async function maintainIntermediates(): Promise<void> {
182
+ let { domain, port } = getStorageServerConfig();
183
+ let takeover = getTakeoverIntermediate();
184
+ let written: string[] = [];
185
+ let alreadyCorrect: string[] = [];
186
+ let notOurs: string[] = [];
187
+ // ONLY buckets already in memory. A switchover must never load a bucket: loading starts its synchronization, and buckets nothing has touched (legacy ones especially) have no writes to hand off in the first place. One that does get used mid-switchover loads on that access, and the next pass gives it its window.
188
+ let states = getBucketStates();
189
+ for (let loaded of states) {
190
+ let key = `${loaded.account}/${loaded.bucketName}`;
191
+ let routing = loaded.routing;
192
+ if (!routing) continue;
193
+ let expired = expireIntermediateSources(routing, Date.now());
194
+ if (JSON.stringify(expired) !== JSON.stringify(routing)) {
195
+ await writeOwnRoutingConfig(loaded, expired, `switchover windows expired more than ${INTERMEDIATE_EXPIRE_GRACE / 1000}s ago`);
196
+ written.push(key);
197
+ continue;
198
+ }
199
+ if (!takeover) continue;
200
+ let mainUrl = routing.sources.find(x => {
201
+ if (typeof x === "string" || x.type !== "remote" || x.intermediate) return false;
202
+ let parsed = parseHostedUrl(x.url);
203
+ return parsed.address === domain && parsed.port === port;
204
+ }) as HostedConfig | undefined;
205
+ if (!mainUrl) {
206
+ notOurs.push(key);
207
+ continue;
208
+ }
209
+ let injected = injectIntermediateSource(routing, {
210
+ splitUrl: mainUrl.url,
211
+ intermediateUrl: replaceHostedUrlPort(mainUrl.url, takeover.altPort),
212
+ start: takeover.start,
213
+ end: takeover.end,
214
+ });
215
+ if (JSON.stringify(injected) === JSON.stringify(routing)) {
216
+ alreadyCorrect.push(key);
217
+ continue;
218
+ }
219
+ await writeOwnRoutingConfig(loaded, injected, `we are a deploy successor: writes route to our alternate port ${takeover.altPort} from ${new Date(takeover.start).toISOString()} until our predecessor is killed at ${new Date(takeover.end).toISOString()}`);
220
+ written.push(key);
221
+ }
222
+ if (!takeover) return;
223
+ if (!states.length) {
224
+ console.log(`No active buckets, so no config files to write the intermediate into (a bucket is only active once something uses it)`);
225
+ return;
226
+ }
227
+ if (!written.length) {
228
+ console.log(`Wrote no config files: of ${states.length} active buckets, ${alreadyCorrect.length} already contain the intermediate and ${notOurs.length} are not hosted by us`);
229
+ }
230
+ }
231
+
232
+ /** Our predecessor still holds the main port and has been serving all along, so it - not us - knows which buckets are in use. We activate exactly those, which is what makes their config files get the intermediate; nothing else on disk is touched. */
233
+ async function activatePredecessorBuckets(): Promise<void> {
234
+ let { domain, port } = getStorageServerConfig();
235
+ let url = `https://${domain}:${port}`;
236
+ let keys: { account: string; bucketName: string }[];
237
+ try {
238
+ keys = await listServerActiveBucketKeys({ url });
239
+ } catch (e) {
240
+ console.error(`Could not ask our predecessor on ${url} which buckets are active, so only buckets used on this process get the intermediate: ${(e as Error).stack ?? e}`);
241
+ return;
242
+ }
243
+ if (!keys.length) {
244
+ console.log(`Our predecessor on ${url} has no active buckets, so there are no config files to write the intermediate into`);
245
+ return;
246
+ }
247
+ console.log(`Our predecessor on ${url} has ${keys.length} active buckets, activating them here so their config files get the intermediate: ${keys.map(x => `${x.account}/${x.bucketName}`).join(", ")}`);
248
+ for (let { account, bucketName } of keys) {
249
+ try {
250
+ await getLoadedBucket(account, bucketName);
251
+ } catch (e) {
252
+ console.error(`Activating bucket ${account}/${bucketName} (active on our predecessor) failed: ${(e as Error).stack ?? e}`);
253
+ }
254
+ }
255
+ }
256
+
257
+ /** Started by deployTakeover once we are actually a deploy successor listening on an alternate port. Until then there are no switchover windows to write or expire, so nothing polls. */
258
+ export const startIntermediateMaintenance = lazy(() => {
259
+ void (async () => {
260
+ await activatePredecessorBuckets();
261
+ await maintainIntermediates();
262
+ })().catch((e: Error) => console.error(`Writing the intermediate into the config files failed: ${e.stack ?? e}`));
263
+ runInfinitePoll(INTERMEDIATE_MAINTAIN_INTERVAL, maintainIntermediates);
264
+ });
@@ -26,7 +26,39 @@ export declare function parseBackblazeUrl(url: string): {
26
26
  bucketName: string;
27
27
  };
28
28
  export declare function replaceHostedUrlPort(url: string, port: number): string;
29
+ /**
30
+ * Puts a source into the shape the code expects. It does NOT judge it: this runs every time a config
31
+ * is READ, and a config that is already on disk has to keep working - a server that cannot parse its
32
+ * own routing file is a server that cannot serve, and it would stay that way forever. Anything
33
+ * missing or unusable is filled in with the safest equivalent instead, loudly where it matters.
34
+ *
35
+ * Judging happens on the way IN, in assertValidRemoteConfig.
36
+ */
29
37
  export declare function normalizeSource(source: RemoteConfigBase): SourceConfig;
38
+ /** Puts a whole config into the shape the code expects, without judging it - see normalizeSource, and see assertValidRemoteConfig for the judging. */
30
39
  export declare function normalizeRemoteConfig(config: RemoteConfig | RemoteConfigBase): RemoteConfig;
40
+ /**
41
+ * Whether a config may be WRITTEN. Everything here is a rule about the config as a whole, which is
42
+ * exactly why it cannot run on read: a config that is already stored somewhere has to keep being
43
+ * readable, or a server that once accepted a bad one could never start again. Rejecting it at the
44
+ * point it is introduced is what keeps a bad one from ever being stored in the first place.
45
+ */
46
+ export declare function assertValidRemoteConfig(config: RemoteConfig): void;
47
+ /**
48
+ * The identity of one of a store's SOURCE SLOTS - which is the endpoint it talks to, and not the same
49
+ * question as which store this is (that is CommonConfig.name). A switchover's alternate port is a
50
+ * distinct slot even though it names the same storage, because a slot holds a connection to a port.
51
+ *
52
+ * ONLY the type, the url, and the intermediate's alternate port are part of it: everything else is
53
+ * policy about how we USE the endpoint, and changing policy must never make a store believe it is
54
+ * looking at a NEW source - that would drop every index entry the old one held and rescan it from
55
+ * scratch, so the files it holds go missing from listings until the rescan finishes, for a flag flip.
56
+ *
57
+ * Built by hand rather than by serializing the config, so it cannot change just because the routing
58
+ * file was written with its keys in a different order.
59
+ */
60
+ export declare function sourceIdentity(sourceConfig: SourceConfig | undefined): string;
61
+ /** What an index entry records as the holder of its bytes (see ArchivesSource.url), so it must name the endpoint FOREVER. An intermediate is a switchover's temporary alternate port onto another source, and that port is gone for good once its window passes - so it is recorded as the source it was split out of, which holds the same bucket and outlives it. */
62
+ export declare function sourcePersistentUrl(sourceConfig: SourceConfig | undefined, folder: string): string;
31
63
  export declare function parseRoutingData(data: Buffer): RemoteConfig;
32
64
  export declare function serializeRemoteConfig(config: RemoteConfig): Buffer;
@@ -3,6 +3,7 @@ import { getBufferInt } from "socket-function/src/bits";
3
3
  import { setFlag } from "socket-function/require/compileFlags";
4
4
  import jsSha256 from "js-sha256";
5
5
  import { RemoteConfig, RemoteConfigBase, SourceConfig, ArchiveFileInfo, ChangesAfterConfig, FULL_VALID_WINDOW, FULL_ROUTE, VARIABLE_SHARD } from "../IArchives";
6
+ import { assertValidSourceName } from "./validation";
6
7
 
7
8
  setFlag(require, "js-sha256", "allowclient", true);
8
9
 
@@ -109,38 +110,86 @@ export function replaceHostedUrlPort(url: string, port: number): string {
109
110
  return u.toString();
110
111
  }
111
112
 
113
+ /**
114
+ * Puts a source into the shape the code expects. It does NOT judge it: this runs every time a config
115
+ * is READ, and a config that is already on disk has to keep working - a server that cannot parse its
116
+ * own routing file is a server that cannot serve, and it would stay that way forever. Anything
117
+ * missing or unusable is filled in with the safest equivalent instead, loudly where it matters.
118
+ *
119
+ * Judging happens on the way IN, in assertValidRemoteConfig.
120
+ */
112
121
  export function normalizeSource(source: RemoteConfigBase): SourceConfig {
113
122
  if (typeof source !== "string") {
114
- let window = source.validWindow;
115
- if (!Array.isArray(window) || window.length !== 2 || !window.every(x => typeof x === "number")) {
116
- throw new Error(`Object source configs must specify validWindow ([startMs, endMs] of the write times the source is valid for) - configuration changes must be scheduled, not flipped instantly. Use FULL_VALID_WINDOW when the source is always valid. Was ${JSON.stringify(window)} on ${JSON.stringify(source).slice(0, 500)}`);
117
- }
118
- let route = source.route;
119
- if (route !== undefined && (!Array.isArray(route) || route.length !== 2 || !route.every(x => typeof x === "number") || route[0] < 0 || route[1] > 1 || route[0] >= route[1])) {
120
- throw new Error(`Source route must be a [start, end) fraction range within [0, 1] with start < end, was ${JSON.stringify(route)} on ${JSON.stringify(source).slice(0, 500)}`);
121
- }
122
- if (source.readerDiskLimit !== undefined) {
123
- if (typeof source.readerDiskLimit !== "number" || source.readerDiskLimit <= 0) {
124
- throw new Error(`readerDiskLimit must be a positive byte count, was ${JSON.stringify(source.readerDiskLimit)} on ${JSON.stringify(source).slice(0, 500)}`);
125
- }
126
- if (!source.noFullSync) {
127
- throw new Error(`readerDiskLimit requires noFullSync: a full copy cannot be bounded, only a read cache can. On ${JSON.stringify(source).slice(0, 500)}`);
123
+ let normalized = { ...source };
124
+ // Configs written before names existed have none, and a name that could not be a folder cannot be used as one. Both get the derived name: deterministic, so every server picks the same one for the same entry.
125
+ if (!isUsableSourceName(normalized.name)) {
126
+ let derived = derivedSourceName(normalized);
127
+ if (normalized.name !== undefined) {
128
+ console.error(`Source name ${JSON.stringify(normalized.name)} in a routing config cannot be used as a folder name, so this entry is being read as ${JSON.stringify(derived)}: ${JSON.stringify(source).slice(0, 500)}`);
128
129
  }
130
+ normalized.name = derived;
129
131
  }
130
- if (source.type === "remote") {
131
- // Throws if the URL is malformed, so bad configs are rejected before they're stored
132
- parseHostedUrl(source.url);
132
+ let window = normalized.validWindow;
133
+ if (!Array.isArray(window) || window.length !== 2 || !window.every(x => typeof x === "number")) {
134
+ console.error(`Source ${JSON.stringify(normalized.name)} has no usable validWindow (${JSON.stringify(window)}), reading it as always valid: ${JSON.stringify(source).slice(0, 500)}`);
135
+ normalized.validWindow = FULL_VALID_WINDOW;
136
+ }
137
+ let route = normalized.route;
138
+ if (route !== undefined && !isUsableRoute(route)) {
139
+ console.error(`Source ${JSON.stringify(normalized.name)} has an unusable route (${JSON.stringify(route)}), reading it as the whole key space: ${JSON.stringify(source).slice(0, 500)}`);
140
+ normalized.route = undefined;
133
141
  }
134
- return source;
142
+ return normalized;
135
143
  }
136
144
  let hostname = new URL(source).hostname;
137
145
  if (hostname.endsWith(".backblazeb2.com")) {
138
146
  // Validates the URL (throws on malformed) before it's stored; the bucket name is read back out of the URL at use sites, never stored on the config.
139
- parseBackblazeUrl(source);
140
- return { type: "backblaze", url: source, validWindow: FULL_VALID_WINDOW, public: true };
147
+ let parsed = parseBackblazeUrl(source);
148
+ return { type: "backblaze", url: source, name: sanitizeSourceName(parsed.bucketName), validWindow: FULL_VALID_WINDOW, public: true };
149
+ }
150
+ let parsed = parseHostedUrl(source);
151
+ return { type: "remote", url: source, name: sanitizeSourceName(`${parsed.address}-${parsed.port}`), validWindow: FULL_VALID_WINDOW, public: true };
152
+ }
153
+
154
+ function isUsableSourceName(name: string | undefined): boolean {
155
+ if (!name) return false;
156
+ try {
157
+ assertValidSourceName(name);
158
+ return true;
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+
164
+ function isUsableRoute(route: [number, number]): boolean {
165
+ if (!Array.isArray(route) || route.length !== 2 || !route.every(x => typeof x === "number")) return false;
166
+ return route[0] >= 0 && route[1] <= 1 && route[0] < route[1];
167
+ }
168
+
169
+ /** The name an entry gets when it has no usable one of its own: the endpoint it points at, plus its route when it has one - so entries that were distinct storage before names existed stay distinct now. */
170
+ function derivedSourceName(source: SourceConfig): string {
171
+ let endpoint = source.url;
172
+ if (source.type === "remote") {
173
+ let parsed = parseHostedUrl(source.url);
174
+ endpoint = `${parsed.address}-${parsed.port}`;
175
+ } else {
176
+ endpoint = parseBackblazeUrl(source.url).bucketName;
177
+ }
178
+ let route = source.route;
179
+ if (route && isUsableRoute(route) && !(route[0] === FULL_ROUTE[0] && route[1] === FULL_ROUTE[1])) {
180
+ endpoint += `-route-${route[0]}-${route[1]}`;
181
+ }
182
+ return sanitizeSourceName(endpoint);
183
+ }
184
+
185
+ /** A name built from something that was never meant to be one (a host, a bucket), made usable as a folder. Periods survive, so it still reads as what it came from. */
186
+ function sanitizeSourceName(from: string): string {
187
+ let name = from.replace(/[^\w.-]/g, "-").slice(0, 64);
188
+ if (!isUsableSourceName(name)) {
189
+ // Only reachable from something with no usable characters at all; the name still has to exist
190
+ return "source";
141
191
  }
142
- parseHostedUrl(source);
143
- return { type: "remote", url: source, validWindow: FULL_VALID_WINDOW, public: true };
192
+ return name;
144
193
  }
145
194
 
146
195
  /** How far up from 0 the sources' routes reach without a gap (1 means the whole key space). */
@@ -155,15 +204,23 @@ function getRouteCoverage(sources: SourceConfig[]): number {
155
204
  return covered;
156
205
  }
157
206
 
207
+ /** Puts a whole config into the shape the code expects, without judging it - see normalizeSource, and see assertValidRemoteConfig for the judging. */
158
208
  export function normalizeRemoteConfig(config: RemoteConfig | RemoteConfigBase): RemoteConfig {
159
- let result: RemoteConfig;
160
209
  if (typeof config !== "string" && "sources" in config) {
161
- result = { version: config.version, sources: config.sources.map(normalizeSource) };
162
- } else {
163
- result = { sources: [normalizeSource(config)] };
210
+ return { version: config.version, sources: config.sources.map(normalizeSource) };
164
211
  }
212
+ return { sources: [normalizeSource(config)] };
213
+ }
214
+
215
+ /**
216
+ * Whether a config may be WRITTEN. Everything here is a rule about the config as a whole, which is
217
+ * exactly why it cannot run on read: a config that is already stored somewhere has to keep being
218
+ * readable, or a server that once accepted a bad one could never start again. Rejecting it at the
219
+ * point it is introduced is what keeps a bad one from ever being stored in the first place.
220
+ */
221
+ export function assertValidRemoteConfig(config: RemoteConfig): void {
222
+ let sources = config.sources.map(normalizeSource);
165
223
  // Mixed immutability makes no sense AMONG sources valid at the same time: a mutable source would accept overwrites that its immutable peers refuse to synchronize, forking their contents. Sources are grouped by transitively overlapping valid windows (absorb everything overlapping the group, extend the group's end, repeat) - a soft check that is correct as long as windows have clean breaks (a group ends exactly where the next begins).
166
- let sources = result.sources.map(normalizeSource);
167
224
  let sorted = [...sources];
168
225
  sort(sorted, x => x.validWindow[0]);
169
226
  let group: typeof sources = [];
@@ -178,22 +235,7 @@ export function normalizeRemoteConfig(config: RemoteConfig | RemoteConfigBase):
178
235
  if (covered < 1) {
179
236
  throw new Error(`Sources with overlapping valid windows must cover the full route space [0, 1); coverage stops at ${covered}. Sources: ${JSON.stringify(group.map(x => ({ url: x.url, validWindow: x.validWindow, route: x.route || FULL_ROUTE })))}`);
180
237
  }
181
- // Every bucket must ALSO cover the full route space across its own entries, because all of a bucket's entries share one disk. A bucket that only ever synchronizes part of the key space never learns about deletions in the rest of it - and tombstones expire, so files it still holds from a route it no longer serves eventually look like live files again, and get resurrected into the chain by its next scan. An entry with no route (implicitly the full space) satisfies this on its own, which is why the usual configuration lists each bucket once per shard AND once unsharded.
182
- let byUrl = new Map<string, typeof group>();
183
- for (let source of group) {
184
- let entries = byUrl.get(source.url);
185
- if (!entries) {
186
- entries = [];
187
- byUrl.set(source.url, entries);
188
- }
189
- entries.push(source);
190
- }
191
- for (let [url, entries] of byUrl) {
192
- let urlCovered = getRouteCoverage(entries);
193
- if (urlCovered < 1) {
194
- throw new Error(`Every bucket must cover the full route space [0, 1) across its own entries with overlapping valid windows, but ${url} only covers up to ${urlCovered} (its routes: ${JSON.stringify(entries.map(x => x.route || FULL_ROUTE))}). Add an entry for it with no route (or entries whose routes span the rest), alongside its sharded entries. This is enforced because a bucket's entries all share one disk: if it only ever synchronizes part of the key space, it never sees deletions in the rest, and once those tombstones expire the files it still holds are resurrected into the chain by its next scan.`);
195
- }
196
- }
238
+ // A store is NOT required to cover the whole key space on its own. It once was, because a store that holds keys outside the route it synchronizes never learns of their deletions and eventually resurrects them - but a store's data now lives under its name rather than its route, and a scan only ever indexes keys inside the route the store is configured for, so keys outside it are never taken into the index that could resurrect them. What replaces that rule is the naming discipline: a name is one storage, and pointing a name at a different route later is what would hand it keys it never synchronized.
197
239
  }
198
240
  for (let source of sorted) {
199
241
  if (group.length && source.validWindow[0] >= groupEnd) {
@@ -204,23 +246,46 @@ export function normalizeRemoteConfig(config: RemoteConfig | RemoteConfigBase):
204
246
  groupEnd = Math.max(groupEnd, source.validWindow[1]);
205
247
  }
206
248
  checkGroup();
207
- // Two entries with the EXACT same URL and the SAME route may never be valid at the same time: on that server the route maps to exactly one on-disk folder and one store, so simultaneous duplicates would collide on it. A different port is a different server. Non-overlapping windows are fine; that is how transitions between windows work.
249
+ // ONE entry per store at any instant. A name is a store - one folder, one index - so two entries naming it at the same time would be two answers to "what is this store configured to be right now", and every route and flag it reads would depend on which entry you happened to look at. Non-overlapping windows are fine, and are how a store's configuration changes over time.
208
250
  for (let i = 0; i < sources.length; i++) {
209
251
  for (let j = i + 1; j < sources.length; j++) {
210
252
  let a = sources[i];
211
253
  let b = sources[j];
212
- if (sourceRouteIdentity(a) !== sourceRouteIdentity(b)) continue;
254
+ if (storeIdentity(a) !== storeIdentity(b)) continue;
213
255
  if (a.validWindow[0] < b.validWindow[1] && b.validWindow[0] < a.validWindow[1]) {
214
- throw new Error(`Two sources with the same URL have the same route with overlapping valid windows - on that server the route maps to one on-disk folder, so these would collide. Give same-route entries non-overlapping windows instead. Conflicting: ${JSON.stringify(a)} and ${JSON.stringify(b)}`);
256
+ throw new Error(`The name ${JSON.stringify(a.name)} is reused with multiple source configurations at the same time:\n ${JSON.stringify(a)}\n ${JSON.stringify(b)}\nA name is one store - one folder, one index - so it can only be configured one way at a time. Give them non-overlapping valid windows, or different names if they are meant to be different storage.`);
215
257
  }
216
258
  }
217
259
  }
218
- return result;
219
260
  }
220
261
 
221
- /** The per-store identity of a source: its exact URL plus its route. Entries sharing this identity map to the same store/folder on the same server, so they may never overlap in time (see normalizeRemoteConfig). */
222
- function sourceRouteIdentity(source: SourceConfig): string {
223
- return `${source.url}|${JSON.stringify(source.route || FULL_ROUTE)}`;
262
+ /**
263
+ * The identity of one of a store's SOURCE SLOTS - which is the endpoint it talks to, and not the same
264
+ * question as which store this is (that is CommonConfig.name). A switchover's alternate port is a
265
+ * distinct slot even though it names the same storage, because a slot holds a connection to a port.
266
+ *
267
+ * ONLY the type, the url, and the intermediate's alternate port are part of it: everything else is
268
+ * policy about how we USE the endpoint, and changing policy must never make a store believe it is
269
+ * looking at a NEW source - that would drop every index entry the old one held and rescan it from
270
+ * scratch, so the files it holds go missing from listings until the rescan finishes, for a flag flip.
271
+ *
272
+ * Built by hand rather than by serializing the config, so it cannot change just because the routing
273
+ * file was written with its keys in a different order.
274
+ */
275
+ export function sourceIdentity(sourceConfig: SourceConfig | undefined): string {
276
+ if (!sourceConfig) return "disk";
277
+ return `${sourceConfig.type}|${sourceConfig.url}|${sourceConfig.intermediate || ""}`;
278
+ }
279
+
280
+ /** What an index entry records as the holder of its bytes (see ArchivesSource.url), so it must name the endpoint FOREVER. An intermediate is a switchover's temporary alternate port onto another source, and that port is gone for good once its window passes - so it is recorded as the source it was split out of, which holds the same bucket and outlives it. */
281
+ export function sourcePersistentUrl(sourceConfig: SourceConfig | undefined, folder: string): string {
282
+ if (!sourceConfig) return folder;
283
+ return sourceConfig.intermediate || sourceConfig.url;
284
+ }
285
+
286
+ /** Which store an entry configures: its name, on the server it points at. A switchover's alternate port is the same server (so it is compared against the entry it was split out of, whose window it must not overlap); a genuinely different server with the same name is a different store, on a different machine. */
287
+ function storeIdentity(source: SourceConfig): string {
288
+ return `${source.intermediate || source.url}|${source.name}`;
224
289
  }
225
290
 
226
291
  export function parseRoutingData(data: Buffer): RemoteConfig {
@@ -23,5 +23,5 @@ export declare function setTrustedMachines(config: {
23
23
  }): Promise<void>;
24
24
  export declare function addExtraListenPort(port: number): void;
25
25
  export declare function removeExtraListenPort(port: number): void;
26
- /** Whether address:port is this server process. The ONE self test - findSelfIndexes, createApiArchives, and SourceWrapper all consult it, so "is this me" cannot disagree between the routing plan and connection building: a URL that is us on an extra listen port must never become a network client to ourselves, which is how infinite self-request loops form. */
26
+ /** Whether address:port is this server process, including its extra listen ports (a deploy switchover's alternate port is still us). Used to tell which config entries are OUR copy of a bucket - the stores we run - as opposed to peers we synchronize with. Talking to ourselves is not one of the things it prevents: a source that happens to be us is reached over the API like any other. */
27
27
  export declare function isOwnAddress(address: string, port: number): boolean;