sliftutils 1.7.88 → 1.7.90

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 (126) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/dist/treeSummary.ts.cache +2 -2
  3. package/index.d.ts +679 -300
  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 +29 -3
  44. package/storage/dist/FileFolderAPI.tsx.cache +2 -2
  45. package/storage/dist/IArchives.ts.cache +17 -53
  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/archiveHelpers.ts.cache +87 -0
  52. package/storage/dist/backblaze.ts.cache +59 -45
  53. package/storage/dist/fileSystemPointer.ts.cache +2 -2
  54. package/storage/dist/remoteFileStorage.ts.cache +2 -2
  55. package/storage/remoteStorage/ArchivesDelayed.d.ts +66 -0
  56. package/storage/remoteStorage/ArchivesDelayed.ts +237 -0
  57. package/storage/remoteStorage/ArchivesRemote.d.ts +5 -6
  58. package/storage/remoteStorage/ArchivesRemote.ts +26 -5
  59. package/storage/remoteStorage/ArchivesUrl.d.ts +2 -5
  60. package/storage/remoteStorage/ArchivesUrl.ts +2 -2
  61. package/storage/remoteStorage/accessPage.tsx +12 -0
  62. package/storage/remoteStorage/blobStore.d.ts +162 -189
  63. package/storage/remoteStorage/blobStore.ts +495 -952
  64. package/storage/remoteStorage/bucketDisk.d.ts +24 -3
  65. package/storage/remoteStorage/bucketDisk.ts +56 -22
  66. package/storage/remoteStorage/certTrustModal.d.ts +1 -0
  67. package/storage/remoteStorage/certTrustModal.tsx +48 -0
  68. package/storage/remoteStorage/cliArgs.d.ts +2 -0
  69. package/storage/remoteStorage/cliArgs.ts +5 -0
  70. package/storage/remoteStorage/createArchives.d.ts +8 -7
  71. package/storage/remoteStorage/createArchives.ts +65 -26
  72. package/storage/remoteStorage/deployTakeover.ts +1 -1
  73. package/storage/remoteStorage/dist/ArchivesDelayed.ts.cache +220 -0
  74. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +26 -6
  75. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +3 -3
  76. package/storage/remoteStorage/dist/accessPage.tsx.cache +13 -3
  77. package/storage/remoteStorage/dist/accessStats.ts.cache +2 -2
  78. package/storage/remoteStorage/dist/blobStore.ts.cache +484 -966
  79. package/storage/remoteStorage/dist/bucketDisk.ts.cache +60 -29
  80. package/storage/remoteStorage/dist/certTrustModal.tsx.cache +55 -0
  81. package/storage/remoteStorage/dist/cliArgs.ts.cache +9 -4
  82. package/storage/remoteStorage/dist/createArchives.ts.cache +64 -27
  83. package/storage/remoteStorage/dist/deployTakeover.ts.cache +5 -5
  84. package/storage/remoteStorage/dist/intermediateManagement.ts.cache +286 -0
  85. package/storage/remoteStorage/dist/intermediateSources.ts.cache +2 -2
  86. package/storage/remoteStorage/dist/remoteConfig.ts.cache +122 -56
  87. package/storage/remoteStorage/dist/serverConfig.ts.cache +4 -4
  88. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +7 -11
  89. package/storage/remoteStorage/dist/sourcesList.ts.cache +3 -6
  90. package/storage/remoteStorage/dist/storageClientController.ts.cache +2 -2
  91. package/storage/remoteStorage/dist/storageController.ts.cache +33 -36
  92. package/storage/remoteStorage/dist/storageServer.ts.cache +27 -7
  93. package/storage/remoteStorage/dist/storageServerCli.ts.cache +7 -3
  94. package/storage/remoteStorage/dist/storageServerState.ts.cache +138 -707
  95. package/storage/remoteStorage/dist/storeConfig.ts.cache +52 -0
  96. package/storage/remoteStorage/dist/storePlan.ts.cache +110 -81
  97. package/storage/remoteStorage/dist/storeSources.ts.cache +51 -0
  98. package/storage/remoteStorage/dist/storeSync.ts.cache +651 -0
  99. package/storage/remoteStorage/dist/validation.ts.cache +14 -4
  100. package/storage/remoteStorage/intermediateManagement.d.ts +12 -0
  101. package/storage/remoteStorage/intermediateManagement.ts +264 -0
  102. package/storage/remoteStorage/remoteConfig.d.ts +32 -0
  103. package/storage/remoteStorage/remoteConfig.ts +115 -50
  104. package/storage/remoteStorage/serverConfig.d.ts +1 -1
  105. package/storage/remoteStorage/serverConfig.ts +1 -1
  106. package/storage/remoteStorage/sourceWrapper.ts +4 -8
  107. package/storage/remoteStorage/sourcesList.d.ts +0 -2
  108. package/storage/remoteStorage/sourcesList.ts +0 -3
  109. package/storage/remoteStorage/spec.md +18 -4
  110. package/storage/remoteStorage/storageController.d.ts +10 -0
  111. package/storage/remoteStorage/storageController.ts +37 -41
  112. package/storage/remoteStorage/storageServer.d.ts +2 -0
  113. package/storage/remoteStorage/storageServer.ts +28 -4
  114. package/storage/remoteStorage/storageServerCli.ts +5 -1
  115. package/storage/remoteStorage/storageServerState.d.ts +39 -26
  116. package/storage/remoteStorage/storageServerState.ts +149 -669
  117. package/storage/remoteStorage/storeConfig.d.ts +30 -0
  118. package/storage/remoteStorage/storeConfig.ts +62 -0
  119. package/storage/remoteStorage/storePlan.d.ts +29 -17
  120. package/storage/remoteStorage/storePlan.ts +112 -85
  121. package/storage/remoteStorage/storeSources.d.ts +11 -0
  122. package/storage/remoteStorage/storeSources.ts +47 -0
  123. package/storage/remoteStorage/storeSync.d.ts +53 -0
  124. package/storage/remoteStorage/storeSync.ts +632 -0
  125. package/storage/remoteStorage/validation.d.ts +2 -0
  126. package/storage/remoteStorage/validation.ts +10 -0
@@ -0,0 +1,30 @@
1
+ import { HostedConfig } from "../IArchives";
2
+ export declare class StoreConfig {
3
+ readonly name: string;
4
+ constructor(name: string, entries: HostedConfig[]);
5
+ private entries;
6
+ /** The routing config changed. Same store either way: its name did not change, so neither did its folder, its index, or the data in it. */
7
+ /** No entries is a real state, not an error: a store exists as soon as its folder does, and it may never have heard of a configuration (see StorePolicy). */
8
+ update(entries: HostedConfig[]): void;
9
+ all(): HostedConfig[];
10
+ /**
11
+ * What this store is configured to be right now: the entry whose window contains this moment;
12
+ * failing that the next one due to start; failing that the last one to have ended.
13
+ *
14
+ * There is always an answer, and it matters that there is: a store between windows still holds
15
+ * data and still has to answer for it, so it needs a route and flags even when nothing is
16
+ * currently pointing writes at it. Which of the three cases produced the answer is deliberately
17
+ * not exposed - the valid window itself is what says whether writes belong here, and every write
18
+ * is checked against it separately.
19
+ */
20
+ current(): StorePolicy;
21
+ }
22
+ /** The parts of a config entry that describe what a store IS - as opposed to which entry said so. Kept separate because a store with no entries still has all of them. */
23
+ export type StorePolicy = {
24
+ validWindow: [number, number];
25
+ route?: [number, number];
26
+ fast?: boolean;
27
+ writeDelay?: number;
28
+ noFullSync?: boolean;
29
+ readerDiskLimit?: number;
30
+ };
@@ -0,0 +1,62 @@
1
+ import { sort } from "socket-function/src/misc";
2
+ import { HostedConfig, FULL_VALID_WINDOW } from "../IArchives";
3
+
4
+ // The configuration of ONE store, kept current. A store is named by its entries (see CommonConfig.name), and at any instant exactly one of them is in effect - the config write path enforces that - so "the configuration of this store right now" is a single entry, and everything the store does with routes, flags and limits reads it from here rather than being handed a snapshot that goes stale.
5
+
6
+ export class StoreConfig {
7
+ constructor(
8
+ public readonly name: string,
9
+ entries: HostedConfig[],
10
+ ) {
11
+ this.update(entries);
12
+ }
13
+
14
+ // Every entry naming this store, oldest window first - past ones included, because a stamped write or a scan can legitimately concern a window that has already ended
15
+ private entries: HostedConfig[] = [];
16
+
17
+ /** The routing config changed. Same store either way: its name did not change, so neither did its folder, its index, or the data in it. */
18
+ /** No entries is a real state, not an error: a store exists as soon as its folder does, and it may never have heard of a configuration (see StorePolicy). */
19
+ public update(entries: HostedConfig[]): void {
20
+ let ordered = [...entries];
21
+ sort(ordered, x => x.validWindow[0]);
22
+ this.entries = ordered;
23
+ }
24
+
25
+ public all(): HostedConfig[] {
26
+ return this.entries;
27
+ }
28
+
29
+ /**
30
+ * What this store is configured to be right now: the entry whose window contains this moment;
31
+ * failing that the next one due to start; failing that the last one to have ended.
32
+ *
33
+ * There is always an answer, and it matters that there is: a store between windows still holds
34
+ * data and still has to answer for it, so it needs a route and flags even when nothing is
35
+ * currently pointing writes at it. Which of the three cases produced the answer is deliberately
36
+ * not exposed - the valid window itself is what says whether writes belong here, and every write
37
+ * is checked against it separately.
38
+ */
39
+ public current(): StorePolicy {
40
+ let now = Date.now();
41
+ let containing = this.entries.find(x => x.validWindow[0] <= now && now < x.validWindow[1]);
42
+ if (containing) return containing;
43
+ let upcoming = this.entries.find(x => x.validWindow[0] > now);
44
+ if (upcoming) return upcoming;
45
+ let last = this.entries[this.entries.length - 1];
46
+ if (last) return last;
47
+ return UNCONFIGURED;
48
+ }
49
+ }
50
+
51
+ /** The parts of a config entry that describe what a store IS - as opposed to which entry said so. Kept separate because a store with no entries still has all of them. */
52
+ export type StorePolicy = {
53
+ validWindow: [number, number];
54
+ route?: [number, number];
55
+ fast?: boolean;
56
+ writeDelay?: number;
57
+ noFullSync?: boolean;
58
+ readerDiskLimit?: number;
59
+ };
60
+
61
+ // A store nobody has configured: always valid, the whole key space, no delays and no limits. Every one of these is the answer that does the least - it holds its own data and nothing else happens to it - which is what makes a store usable before it has ever seen a config.
62
+ const UNCONFIGURED: StorePolicy = { validWindow: FULL_VALID_WINDOW };
@@ -1,6 +1,29 @@
1
1
  import { RemoteConfig, HostedConfig, SourceConfig } from "../IArchives";
2
+ /** Whether a config entry is THIS server's copy of this bucket - the same account and bucket, at an address this process answers on. */
3
+ export declare function isSelfSource(source: SourceConfig, account: string, bucketName: string): boolean;
2
4
  export declare function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: string): number[];
3
5
  export declare function selectEntryAt(entries: HostedConfig[], time: number, route?: number): HostedConfig | undefined;
6
+ /** What one of our stores has to pull in at a valid-window boundary, so the writes that landed just before the handover are not missed. */
7
+ export type BoundaryHandover = {
8
+ name: string;
9
+ route: [number, number];
10
+ scanOwnDisk: boolean;
11
+ remotes: Map<number, [number, number]>;
12
+ };
13
+ /**
14
+ * Who held each slice of our route in the window before windowStart, for every self entry whose
15
+ * window starts exactly then. This is the whole of "who do we take over from": a store taking over a
16
+ * route may be taking it from several previous owners at once (their shards need not line up with
17
+ * ours), and from itself for the parts it already held.
18
+ *
19
+ * A self entry is skipped when an EARLIER entry valid at the boundary already covers its whole route:
20
+ * config order is priority, so that entry is the write target and we are not the one taking over.
21
+ * Owners are then resolved in config order too, each claiming the part of our route still unclaimed -
22
+ * the same first-match-wins rule that picks a write target at any other moment.
23
+ *
24
+ * Pure: config in, plan out. Nothing here reads a store, a clock, or the network.
25
+ */
26
+ export declare function previousWindowOwners(config: RemoteConfig, windowStart: number, selfIndexes: number[]): BoundaryHandover[];
4
27
  /** Our role in a bucket's routing config, summarized across ALL currently-valid self entries. Stored instead of a single representative HostedConfig, so nothing can accidentally use one entry's route or flags where the union is required - the standard config has the same URL twice: a routed write-shard entry plus an unrouted read-everything entry. */
5
28
  export type SelfSummary = {
6
29
  /** The union of the current entries' routes, with overlapping/adjacent ranges combined - which commonly collapses to a single full range, making matching trivial. */
@@ -8,27 +31,16 @@ export type SelfSummary = {
8
31
  public: boolean;
9
32
  immutable: boolean;
10
33
  noFullSync: boolean;
11
- rawDisk: boolean;
12
34
  readerDiskLimit?: number;
13
35
  };
14
- export type StoreSourceSpec = {
15
- sourceConfig?: SourceConfig;
16
- validWindows: [number, number][];
17
- route?: [number, number];
18
- noFullSync?: boolean;
19
- };
20
- export type StorePlanStore = {
21
- routeKey: string;
22
- route?: [number, number];
36
+ /** One store this server runs for a bucket: the name that IS the store, and the entries that name it. What it synchronizes with is NOT decided here - the store reads its own copy of the config and works that out itself (see BlobStore.applyRoutingConfig). */
37
+ export type SelfStore = {
38
+ name: string;
23
39
  entries: HostedConfig[];
24
- rawDisk: boolean;
25
- readerDiskLimit?: number;
26
- sourceSpecs: StoreSourceSpec[];
27
40
  };
28
- export type StorePlan = {
41
+ /** Which stores this server runs for a bucket, by name. Nothing more: a store is found by name, and configures itself. */
42
+ export declare function planSelfStores(account: string, bucketName: string, routing: RemoteConfig): {
29
43
  selfEntries: HostedConfig[];
30
44
  self: SelfSummary | undefined;
31
- stores: StorePlanStore[];
32
- structureKey: string;
45
+ stores: SelfStore[];
33
46
  };
34
- export declare function computeStorePlan(account: string, bucketName: string, routing: RemoteConfig): StorePlan;
@@ -5,14 +5,20 @@ import { isOwnAddress } from "./serverConfig";
5
5
 
6
6
  // Turns a bucket's routing config into this server's store plan: which of the entries are us, what stores (one per route) we run, and which peers each store synchronizes from.
7
7
 
8
+ /** Whether a config entry is THIS server's copy of this bucket - the same account and bucket, at an address this process answers on. */
9
+ export function isSelfSource(source: SourceConfig, account: string, bucketName: string): boolean {
10
+ if (source.type !== "remote") return false;
11
+ let parsed = parseHostedUrl(source.url);
12
+ if (parsed.account !== account || parsed.bucketName !== bucketName) return false;
13
+ return isOwnAddress(parsed.address, parsed.port);
14
+ }
15
+
8
16
  export function findSelfIndexes(routing: RemoteConfig, account: string, bucketName: string): number[] {
9
17
  let indexes: number[] = [];
10
18
  for (let i = 0; i < routing.sources.length; i++) {
11
19
  let source = routing.sources[i];
12
- if (typeof source === "string" || source.type !== "remote") continue;
13
- let parsed = parseHostedUrl(source.url);
14
- if (parsed.account !== account || parsed.bucketName !== bucketName) continue;
15
- if (isOwnAddress(parsed.address, parsed.port)) {
20
+ if (typeof source === "string") continue;
21
+ if (isSelfSource(source, account, bucketName)) {
16
22
  indexes.push(i);
17
23
  }
18
24
  }
@@ -40,6 +46,93 @@ export function selectEntryAt(entries: HostedConfig[], time: number, route?: num
40
46
  return best;
41
47
  }
42
48
 
49
+ /** The parts of `ranges` that `cut` does not cover, plus the single range spanning what it did. Routes are half-open [start, end), so subtracting one from another leaves at most a piece on each side. */
50
+ function subtractRoute(ranges: [number, number][], cut: [number, number]): { remaining: [number, number][]; claimed?: [number, number] } {
51
+ let remaining: [number, number][] = [];
52
+ let claimed: [number, number] | undefined;
53
+ for (let range of ranges) {
54
+ let overlap = routeIntersection(range, cut);
55
+ if (!overlap) {
56
+ remaining.push(range);
57
+ continue;
58
+ }
59
+ claimed = claimed && [Math.min(claimed[0], overlap[0]), Math.max(claimed[1], overlap[1])] as [number, number] || overlap;
60
+ if (range[0] < overlap[0]) remaining.push([range[0], overlap[0]]);
61
+ if (overlap[1] < range[1]) remaining.push([overlap[1], range[1]]);
62
+ }
63
+ return { remaining, claimed };
64
+ }
65
+
66
+ /** What one of our stores has to pull in at a valid-window boundary, so the writes that landed just before the handover are not missed. */
67
+ export type BoundaryHandover = {
68
+ // The store that needs the data: the name of the self entry taking over at the boundary
69
+ name: string;
70
+ // The route it is taking over, which is the slice of the key space the pulls below are limited to
71
+ route: [number, number];
72
+ // We held part of this route in the previous window too, so those writes are already in our own folder and a disk rescan finds them
73
+ scanOwnDisk: boolean;
74
+ // Per source index in the config, the slice of our route THAT source held in the previous window - a boundary scan pulls its recent changes
75
+ remotes: Map<number, [number, number]>;
76
+ };
77
+
78
+ /**
79
+ * Who held each slice of our route in the window before windowStart, for every self entry whose
80
+ * window starts exactly then. This is the whole of "who do we take over from": a store taking over a
81
+ * route may be taking it from several previous owners at once (their shards need not line up with
82
+ * ours), and from itself for the parts it already held.
83
+ *
84
+ * A self entry is skipped when an EARLIER entry valid at the boundary already covers its whole route:
85
+ * config order is priority, so that entry is the write target and we are not the one taking over.
86
+ * Owners are then resolved in config order too, each claiming the part of our route still unclaimed -
87
+ * the same first-match-wins rule that picks a write target at any other moment.
88
+ *
89
+ * Pure: config in, plan out. Nothing here reads a store, a clock, or the network.
90
+ */
91
+ export function previousWindowOwners(config: RemoteConfig, windowStart: number, selfIndexes: number[]): BoundaryHandover[] {
92
+ let selfIndexSet = new Set(selfIndexes);
93
+ let previousTime = windowStart - 1;
94
+ let validAt = (source: SourceConfig, time: number) => source.validWindow[0] <= time && time < source.validWindow[1];
95
+ let byRoute = new Map<string, BoundaryHandover>();
96
+ for (let selfIndex of selfIndexes) {
97
+ let self = config.sources[selfIndex];
98
+ if (typeof self === "string" || self.validWindow[0] !== windowStart) continue;
99
+ let selfRoute = self.route || FULL_ROUTE;
100
+ let shadowed = false;
101
+ for (let i = 0; i < selfIndex; i++) {
102
+ let other = config.sources[i];
103
+ if (typeof other === "string" || !validAt(other, windowStart)) continue;
104
+ let route = other.route || FULL_ROUTE;
105
+ if (route[0] <= selfRoute[0] && selfRoute[1] <= route[1]) {
106
+ shadowed = true;
107
+ break;
108
+ }
109
+ }
110
+ if (shadowed) continue;
111
+ // Keyed by both: one name can take over several routes at the same boundary, and each is pulled separately
112
+ let handoverKey = `${self.name}|${JSON.stringify(selfRoute)}`;
113
+ let handover = byRoute.get(handoverKey);
114
+ if (!handover) {
115
+ handover = { name: self.name, route: selfRoute, scanOwnDisk: false, remotes: new Map() };
116
+ byRoute.set(handoverKey, handover);
117
+ }
118
+ let unclaimed: [number, number][] = [[selfRoute[0], selfRoute[1]]];
119
+ for (let i = 0; i < config.sources.length && unclaimed.length; i++) {
120
+ let other = config.sources[i];
121
+ if (typeof other === "string" || !validAt(other, previousTime)) continue;
122
+ let { remaining, claimed } = subtractRoute(unclaimed, other.route || FULL_ROUTE);
123
+ unclaimed = remaining;
124
+ if (!claimed) continue;
125
+ if (selfIndexSet.has(i)) {
126
+ handover.scanOwnDisk = true;
127
+ continue;
128
+ }
129
+ let existing = handover.remotes.get(i);
130
+ handover.remotes.set(i, existing && [Math.min(existing[0], claimed[0]), Math.max(existing[1], claimed[1])] as [number, number] || claimed);
131
+ }
132
+ }
133
+ return [...byRoute.values()].filter(x => x.scanOwnDisk || x.remotes.size);
134
+ }
135
+
43
136
  /** Our role in a bucket's routing config, summarized across ALL currently-valid self entries. Stored instead of a single representative HostedConfig, so nothing can accidentally use one entry's route or flags where the union is required - the standard config has the same URL twice: a routed write-shard entry plus an unrouted read-everything entry. */
44
137
  export type SelfSummary = {
45
138
  /** The union of the current entries' routes, with overlapping/adjacent ranges combined - which commonly collapses to a single full range, making matching trivial. */
@@ -47,7 +140,6 @@ export type SelfSummary = {
47
140
  public: boolean;
48
141
  immutable: boolean;
49
142
  noFullSync: boolean;
50
- rawDisk: boolean;
51
143
  readerDiskLimit?: number;
52
144
  };
53
145
 
@@ -80,96 +172,31 @@ function summarizeSelf(selfEntries: HostedConfig[], now: number): SelfSummary |
80
172
  public: current.some(x => x.public),
81
173
  immutable: current.some(x => x.immutable),
82
174
  noFullSync: current.some(x => x.noFullSync),
83
- rawDisk: current.some(x => x.rawDisk),
84
175
  readerDiskLimit: current.find(x => x.readerDiskLimit !== undefined)?.readerDiskLimit,
85
176
  };
86
177
  }
87
178
 
88
- export type StoreSourceSpec = { sourceConfig?: SourceConfig; validWindows: [number, number][]; route?: [number, number]; noFullSync?: boolean };
89
- export type StorePlanStore = {
90
- routeKey: string;
91
- route?: [number, number];
179
+ /** One store this server runs for a bucket: the name that IS the store, and the entries that name it. What it synchronizes with is NOT decided here - the store reads its own copy of the config and works that out itself (see BlobStore.applyRoutingConfig). */
180
+ export type SelfStore = {
181
+ name: string;
92
182
  entries: HostedConfig[];
93
- rawDisk: boolean;
94
- readerDiskLimit?: number;
95
- sourceSpecs: StoreSourceSpec[];
96
- };
97
- export type StorePlan = {
98
- selfEntries: HostedConfig[];
99
- self: SelfSummary | undefined;
100
- stores: StorePlanStore[];
101
- structureKey: string;
102
183
  };
103
184
 
104
- export function computeStorePlan(account: string, bucketName: string, routing: RemoteConfig): StorePlan {
185
+ /** Which stores this server runs for a bucket, by name. Nothing more: a store is found by name, and configures itself. */
186
+ export function planSelfStores(account: string, bucketName: string, routing: RemoteConfig): { selfEntries: HostedConfig[]; self: SelfSummary | undefined; stores: SelfStore[] } {
105
187
  let selfIndexes = findSelfIndexes(routing, account, bucketName);
106
188
  let selfEntries = selfIndexes.map(i => routing.sources[i] as HostedConfig);
107
189
  let self = summarizeSelf(selfEntries, Date.now());
108
- let ownIndexes = new Set(selfIndexes);
109
- // One store per distinct route among ALL our entries - past and future windows included, so historical route folders keep serving their data and upcoming routes sync ahead of their window
110
- let groups = new Map<string, { route?: [number, number]; entries: HostedConfig[]; firstIndex: number }>();
111
- for (let i of selfIndexes) {
112
- let entry = routing.sources[i] as HostedConfig;
113
- let routeKey = JSON.stringify(entry.route || FULL_ROUTE);
114
- let group = groups.get(routeKey);
115
- if (!group) {
116
- group = { route: entry.route, entries: [], firstIndex: i };
117
- groups.set(routeKey, group);
190
+ // One store per NAME among ALL our entries - past and future windows included, so a name we no longer write to keeps serving its data and a name we are about to use exists ahead of its window
191
+ let byName = new Map<string, HostedConfig[]>();
192
+ for (let entry of selfEntries) {
193
+ let entries = byName.get(entry.name);
194
+ if (!entries) {
195
+ entries = [];
196
+ byName.set(entry.name, entries);
118
197
  }
119
- group.entries.push(entry);
120
- }
121
- let stores: StorePlanStore[] = [];
122
- for (let [routeKey, group] of groups) {
123
- let anchor = selectEntryAt(group.entries, Date.now());
124
- let diskWindow: [number, number] = [0, 0];
125
- if (anchor) {
126
- let [start, end] = anchor.validWindow;
127
- let merged = true;
128
- while (merged) {
129
- merged = false;
130
- for (let entry of group.entries) {
131
- let [entryStart, entryEnd] = entry.validWindow;
132
- if (entryStart > end || entryEnd < start) continue;
133
- if (entryStart < start || entryEnd > end) {
134
- start = Math.min(start, entryStart);
135
- end = Math.max(end, entryEnd);
136
- merged = true;
137
- }
138
- }
139
- }
140
- diskWindow = [start, end];
141
- }
142
- let sourceSpecs: StoreSourceSpec[] = [{ validWindows: [diskWindow] }];
143
- let noFullSync = group.entries.some(x => x.noFullSync);
144
- // The same peer endpoint (url+route+flags) can appear under several windows at once (a switchover splits one window around an intermediate). It is ONE sync source that carries ALL those windows - never several sources, which would double-scan it and, worse, make matching a request depend on which window the caller happened to hold.
145
- let peerByKey = new Map<string, StoreSourceSpec>();
146
- for (let i = group.firstIndex + 1; i < routing.sources.length; i++) {
147
- let source = routing.sources[i];
148
- if (typeof source === "string" || ownIndexes.has(i)) continue;
149
- let sharedRoute = routeIntersection(group.route, source.route);
150
- if (!sharedRoute) continue;
151
- let key = JSON.stringify({ ...source, validWindow: undefined });
152
- let spec = peerByKey.get(key);
153
- if (!spec) {
154
- spec = { sourceConfig: source, validWindows: [], route: sharedRoute, noFullSync: source.noFullSync || noFullSync };
155
- peerByKey.set(key, spec);
156
- sourceSpecs.push(spec);
157
- }
158
- spec.validWindows.push(source.validWindow);
159
- }
160
- stores.push({
161
- routeKey,
162
- route: group.route,
163
- entries: group.entries,
164
- rawDisk: group.entries.some(x => x.rawDisk),
165
- readerDiskLimit: group.entries.find(x => x.readerDiskLimit !== undefined)?.readerDiskLimit,
166
- sourceSpecs,
167
- });
168
- }
169
- if (!stores.length) {
170
- // Not in the config at all: still serve whatever the plain folder holds, through one inert full-route store
171
- stores.push({ routeKey: JSON.stringify(FULL_ROUTE), route: undefined, entries: [], rawDisk: false, readerDiskLimit: undefined, sourceSpecs: [{ validWindows: [[0, 0]] }] });
198
+ entries.push(entry);
172
199
  }
173
- let structureKey = JSON.stringify(stores.map(s => ({ routeKey: s.routeKey, rawDisk: s.rawDisk, readerDiskLimit: s.readerDiskLimit })));
174
- return { selfEntries, self, stores, structureKey };
200
+ // Not in the config at all: no name means no store here
201
+ return { selfEntries, self, stores: [...byName].map(([name, entries]) => ({ name, entries })) };
175
202
  }
@@ -0,0 +1,11 @@
1
+ import { IArchives, SourceConfig } from "../IArchives";
2
+ /** The client for one configured source: backblaze, or a storage server - including this one. */
3
+ export declare function createApiArchives(source: SourceConfig): IArchives;
4
+ /** The ONE place a store's source is built. Every source a store synchronizes with is one of exactly two things: a configured peer, or the store's own disk folder (no sourceConfig). writeDelay wraps it so its writes are buffered in memory for that long (see ArchivesDelayed) - the whole of "fast writes", per source, decided here. */
5
+ export declare function createStoreSource(config: {
6
+ sourceConfig?: SourceConfig;
7
+ folder: string;
8
+ writeDelay?: number;
9
+ }): IArchives;
10
+ /** Applies a changed config to an ALREADY RUNNING source (same endpoint, see sourceIdentity - only policy moved). Sources that carry their config into every request MUST be updated in place, or they keep sending the old one: the server matches the config it is handed against its own entries, so a source left holding a stale config eventually stops resolving to a store at all. The write delay is policy too, so it moves here as well. */
11
+ export declare function applySourceConfig(source: IArchives, sourceConfig: SourceConfig | undefined, writeDelay?: number): void;
@@ -0,0 +1,47 @@
1
+ import { IArchives, SourceConfig } from "../IArchives";
2
+ import { ArchivesDisk } from "../ArchivesDisk";
3
+ import { ArchivesBackblaze } from "../backblaze";
4
+ import { ArchivesRemote } from "./ArchivesRemote";
5
+ import { parseBackblazeUrl } from "./remoteConfig";
6
+ import { ArchivesDelayed, asDelayed, unwrapDelayed } from "./ArchivesDelayed";
7
+
8
+ // ONE source, talked to directly - the counterpart of createArchives, which resolves a whole routing config into a chain that falls back and re-resolves across many sources. Nothing here picks a source or retries against another one: the caller (a chain, or a store's synchronization) already decided which endpoint it wants, and gets a client bound to exactly that endpoint. Source identity and config updates live here too, because they are the same question - which endpoint is this, and what policy are we applying to it.
9
+
10
+ /** The client for one configured source: backblaze, or a storage server - including this one. */
11
+ export function createApiArchives(source: SourceConfig): IArchives {
12
+ if (source.type === "backblaze") {
13
+ return new ArchivesBackblaze({ bucketName: parseBackblazeUrl(source.url).bucketName, public: source.public, immutable: source.immutable, allowedOrigins: source.allowedOrigins });
14
+ }
15
+ // Including when that server is us: a call to ourselves is a WebSocket to our own port, which authenticates as this machine (so it is always allowed) and costs a round trip on loopback. Not worth a second implementation of every operation to avoid.
16
+ return new ArchivesRemote({ url: source.url, waitForAccess: false, sourceConfig: source });
17
+ }
18
+
19
+ /** The ONE place a store's source is built. Every source a store synchronizes with is one of exactly two things: a configured peer, or the store's own disk folder (no sourceConfig). writeDelay wraps it so its writes are buffered in memory for that long (see ArchivesDelayed) - the whole of "fast writes", per source, decided here. */
20
+ export function createStoreSource(config: { sourceConfig?: SourceConfig; folder: string; writeDelay?: number }): IArchives {
21
+ let source: IArchives;
22
+ if (!config.sourceConfig) {
23
+ source = new ArchivesDisk(config.folder);
24
+ } else {
25
+ source = createApiArchives(config.sourceConfig);
26
+ }
27
+ if (config.writeDelay) {
28
+ source = new ArchivesDelayed(source, config.writeDelay);
29
+ }
30
+ return source;
31
+ }
32
+
33
+ /** A source that holds its own SourceConfig (to send with every request, or to read policy flags off). Duck-typed rather than a base class: the three implementations have nothing else in common, and a source that ignores its config simply doesn't have the method. */
34
+ type ConfigurableSource = { updateSourceConfig(sourceConfig: SourceConfig): void };
35
+
36
+ /** Applies a changed config to an ALREADY RUNNING source (same endpoint, see sourceIdentity - only policy moved). Sources that carry their config into every request MUST be updated in place, or they keep sending the old one: the server matches the config it is handed against its own entries, so a source left holding a stale config eventually stops resolving to a store at all. The write delay is policy too, so it moves here as well. */
37
+ export function applySourceConfig(source: IArchives, sourceConfig: SourceConfig | undefined, writeDelay?: number): void {
38
+ if (writeDelay !== undefined) {
39
+ asDelayed(source)?.setDelay(writeDelay);
40
+ }
41
+ // The store's own disk has no config to apply - only a delay
42
+ if (!sourceConfig) return;
43
+ // The config belongs to the source itself, not to the delay wrapped around it
44
+ let configurable = unwrapDelayed(source) as Partial<ConfigurableSource>;
45
+ if (typeof configurable.updateSourceConfig !== "function") return;
46
+ configurable.updateSourceConfig(sourceConfig);
47
+ }
@@ -0,0 +1,53 @@
1
+ import { IArchives, ArchivesSyncStatus, SyncActivity } from "../IArchives";
2
+ import type { BlobStore } from "./blobStore";
3
+ export declare class StoreSync {
4
+ private store;
5
+ private states;
6
+ private activities;
7
+ private evicting;
8
+ private lastAccess;
9
+ constructor(store: BlobStore);
10
+ /** Starts every live source's synchronization, plus the maintenance loops. Called once, by the store's init - the store's index must already be loaded, since scans write straight into it. */
11
+ start(): void;
12
+ /**
13
+ * Asks every peer for the routing config, and takes it if it is newer than ours. This is the whole
14
+ * of configuration propagation: it rides beside the scans rather than being part of them, because
15
+ * one small read is worth doing every few minutes while a full listing is not, and because a
16
+ * config we are missing is what stops everything else from being right.
17
+ *
18
+ * The copy is stored the same way a scan stores anything it pulls - an internal write into our own
19
+ * store - and the store notices that file landing and re-configures itself. Nothing here knows what
20
+ * a config means; it only knows this one file is worth asking for often.
21
+ */
22
+ private pollRoutingConfig;
23
+ /** Stops every source's loops (the store's own stop token stops the maintenance loops). */
24
+ stop(): void;
25
+ /** A slot the store just appended to its sources array: it starts from nothing, so it gets a full scan. */
26
+ addSource(slot: number): void;
27
+ /** The slot stays in the store's arrays forever (running loops hold slot numbers); it just goes dead - loops stop, and its index entries drop (other sources' scans re-find any copy that's still reachable through the new config). */
28
+ removeSource(slot: number): Promise<void>;
29
+ /** Whether a slot is still configured. Dead slots are never scanned, written, or read. */
30
+ isLive(slot: number): boolean;
31
+ getActivities(): SyncActivity[];
32
+ /** A key was just served, so it goes to the back of the eviction queue. */
33
+ noteAccess(key: string): void;
34
+ private entryUnchanged;
35
+ getStatus(): ArchivesSyncStatus;
36
+ /** Listings come straight from the index, so they must wait for our own base source's initial scan (which might lag minutes) before they are trustworthy. The base (local disk) is implicitly required - remote sources are not, they come and go. */
37
+ waitForRequiredScans(): Promise<void>;
38
+ /** Rescans our own disk's metadata into the index - used around valid window handoffs, where another process wrote files to the shared folder that our index hasn't seen. */
39
+ rescanBase(): Promise<void>;
40
+ /** A boundary scan of the node that owned (part of) our route in the valid window before ours, when that node is different storage (a disk rescan can't see its writes): just its changes since the boundary neighborhood, with matching values pulled onto our own disk. */
41
+ boundaryScanRemote(source: IArchives, config: {
42
+ since: number;
43
+ route?: [number, number];
44
+ }): Promise<void>;
45
+ private runSourceSync;
46
+ private scanSource;
47
+ private reconcileSource;
48
+ private updateScanIndex;
49
+ private pollChanges;
50
+ private copySourceFiles;
51
+ private enforceDiskLimit;
52
+ private cleanupTombstones;
53
+ }