querysub 0.565.0 → 0.567.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.565.0",
3
+ "version": "0.567.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -19,6 +19,9 @@ function createSourceWindows(
19
19
  let timeRanges: [number, number][] = [];
20
20
  for (let windowIndex = 0; windowIndex < sourceWindows.length; windowIndex++) {
21
21
  let startTime = sourceWindows[windowIndex].startTime;
22
+ if (windowIndex === 0) {
23
+ startTime = 0;
24
+ }
22
25
  let endTime = sourceWindows[windowIndex + 1]?.startTime ?? Number.MAX_SAFE_INTEGER;
23
26
  if (startTime > endTime) throw new Error(`Source window ${formatDateTime(startTime)} starts after end of ${formatDateTime(endTime)}`);
24
27
  timeRanges.push([startTime, endTime]);
@@ -40,29 +43,29 @@ function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
40
43
  const BACKBLAZE = `https://f002.backblazeb2.com/file/${bucket}/storage/storagerouting.json`;
41
44
  let sources = createSourceWindows(overrides, [
42
45
  {
43
- startTime: 0,
46
+ startTime: +new Date("2026-07-20 18:00:00-04:00"),
44
47
  sources: [
45
- { type: "remote", url: ONTARIO, route: [0, 0.5], },
46
- { type: "remote", url: HETZNER, route: [0.5, 1], },
48
+ { type: "remote", url: ONTARIO, route: [0.5, 1], },
49
+ { type: "remote", url: HETZNER, route: [0, 0.5], },
47
50
  { type: "remote", url: ONTARIO, },
48
51
  { type: "remote", url: HETZNER, },
49
52
  { type: "backblaze", url: BACKBLAZE, },
50
53
  ]
51
54
  },
52
55
  {
53
- startTime: +new Date("2026-07-20 18:00:00-04:00"),
56
+ startTime: +new Date("2026-07-21 02:59:00-04:00"),
54
57
  sources: [
55
- { type: "remote", url: ONTARIO, route: [0.5, 1], },
56
- { type: "remote", url: HETZNER, route: [0, 0.5], },
58
+ { type: "remote", url: ONTARIO, route: [0, 0.5], },
59
+ { type: "remote", url: HETZNER, route: [0.5, 1], },
57
60
  { type: "remote", url: ONTARIO, },
58
61
  { type: "remote", url: HETZNER, },
59
62
  { type: "backblaze", url: BACKBLAZE, },
60
63
  ]
61
- }
64
+ },
62
65
  ]);
63
66
 
64
67
  return createArchives({
65
- version: 14,
68
+ version: 15,
66
69
  sources,
67
70
  });
68
71
  }
@@ -12,7 +12,7 @@ import type { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig } fr
12
12
  import { showModal } from "../../5-diagnostics/Modal";
13
13
  import { FullscreenModal } from "../../5-diagnostics/FullscreenModal";
14
14
  import { Button } from "../../library-components/Button";
15
- import { Tag, TAG_FONT_SIZE, TAG_WARNING_COLOR, NO_FULL_SYNC_TAG } from "./Tag";
15
+ import { Tag, TAG_FONT_SIZE, TAG_WARNING_COLOR, getSourceTags } from "./Tag";
16
16
  import { StorageSynced, type StorageServerBuckets } from "./StoragePage";
17
17
 
18
18
  module.hotreload = true;
@@ -343,21 +343,6 @@ function showConfigModal(bucketName: string, rawConfig: RemoteConfig): void {
343
343
  });
344
344
  }
345
345
 
346
- function getSourceTags(source: Source): { icon: string; text: string }[] {
347
- let tags: { icon: string; text: string }[] = [];
348
- if (source.public) tags.push({ icon: "🌐", text: "public" });
349
- if (source.immutable) tags.push({ icon: "🔒", text: "immutable" });
350
- if (source.noFullSync) tags.push(NO_FULL_SYNC_TAG);
351
- if (source.intermediate) tags.push({ icon: "⏳", text: "intermediate" });
352
- if (source.type === "remote") {
353
- if (source.fast) tags.push({ icon: "⚡", text: "fast" });
354
- if (source.rawDisk) tags.push({ icon: "💽", text: "rawDisk" });
355
- if (source.writeDelay !== undefined) tags.push({ icon: "⏱", text: `writeDelay ${formatTime(source.writeDelay)}` });
356
- }
357
- if (source.readerDiskLimit !== undefined) tags.push({ icon: "📦", text: `readCache ${formatNumber(source.readerDiskLimit)}B` });
358
- return tags;
359
- }
360
-
361
346
  /** Watching a source pins that server's own live view of the bucket to the top of the page, so disagreements between servers are visible directly. */
362
347
  class WatchButton extends qreact.Component<{ source: Source; bucketName: string }> {
363
348
  render() {
@@ -17,7 +17,7 @@ import { isDefined } from "../../misc";
17
17
  import { STORAGE_ACCOUNT } from "../../-a-archives/archives2";
18
18
  import { Table } from "../../5-diagnostics/Table";
19
19
  import { RouteConfigView } from "./RouteConfigView";
20
- import { Tag, NO_FULL_SYNC_TAG } from "./Tag";
20
+ import { Tag, SourceTag, getSourceTags } from "./Tag";
21
21
 
22
22
  module.hotreload = true;
23
23
 
@@ -134,7 +134,7 @@ type BucketRow = {
134
134
  state: string;
135
135
  files: string;
136
136
  bytes: string;
137
- noFullSync: boolean;
137
+ flags: SourceTag[];
138
138
  indexSources: ArchivesConfig["indexSources"];
139
139
  readerDiskLimit: string;
140
140
  writes: string;
@@ -152,13 +152,13 @@ const DISK_BAR_TYPE = "DISK";
152
152
  const ACTIVE_STATE = "active";
153
153
  const INACTIVE_STATE = "inactive";
154
154
 
155
- /** Whether this server only caches what is read of the bucket instead of keeping a full copy, read off its own entry in the bucket's routing config. */
156
- function getNoFullSync(serverUrl: string, bucketName: string, remoteConfig: RemoteConfig | undefined): boolean {
155
+ /** The flags this server's own entries in the bucket's routing config set - how it holds the bucket, which is not something the bucket's own state reports. */
156
+ function getBucketFlags(serverUrl: string, bucketName: string, remoteConfig: RemoteConfig | undefined): SourceTag[] {
157
157
  let host: URL;
158
158
  try {
159
159
  host = new URL(serverUrl);
160
160
  } catch {
161
- return false;
161
+ return [];
162
162
  }
163
163
  let selfEntries: HostedConfig[] = [];
164
164
  for (let source of remoteConfig?.sources || []) {
@@ -173,7 +173,15 @@ function getNoFullSync(serverUrl: string, bucketName: string, remoteConfig: Remo
173
173
  // A malformed url in the config is not this column's problem
174
174
  }
175
175
  }
176
- return selfEntries.some(x => x.noFullSync);
176
+ // A server can hold several entries for one bucket (different windows or route shards), so the flags are the union of theirs, deduped by what they say
177
+ let flags = new Map<string, SourceTag>();
178
+ for (let entry of selfEntries) {
179
+ for (let tag of getSourceTags(entry)) {
180
+ if (flags.has(tag.text)) continue;
181
+ flags.set(tag.text, tag);
182
+ }
183
+ }
184
+ return [...flags.values()];
177
185
  }
178
186
 
179
187
  /** How much fast-mode coalescing saved: every accepted write over the ones that actually reached a source. */
@@ -195,7 +203,7 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
195
203
  state: "",
196
204
  files: "",
197
205
  bytes: "",
198
- noFullSync: false,
206
+ flags: [],
199
207
  indexSources: undefined,
200
208
  readerDiskLimit: "",
201
209
  writes: "",
@@ -225,7 +233,7 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
225
233
  state: bucket.active && ACTIVE_STATE || INACTIVE_STATE,
226
234
  files: config?.index && formatNumber(config.index.fileCount) || "",
227
235
  bytes: config?.index && formatNumber(config.index.byteCount) + "B" || "",
228
- noFullSync: getNoFullSync(server.url, bucket.bucketName, config?.remoteConfig),
236
+ flags: getBucketFlags(server.url, bucket.bucketName, config?.remoteConfig),
229
237
  indexSources: config?.indexSources,
230
238
  readerDiskLimit: config?.readerDiskLimit && formatNumber(config.readerDiskLimit) + "B" || "",
231
239
  writes: bucket.writeStats && formatNumber(bucket.writeStats.originalWrites) || "",
@@ -581,10 +589,12 @@ export class StoragePage extends qreact.Component {
581
589
  },
582
590
  files: { title: "Files" },
583
591
  bytes: { title: "Bytes" },
584
- noFullSync: {
585
- title: "Sync",
586
- // Nothing to show when a full copy is kept - that is the default, and the tag is the flag
587
- formatter: noFullSync => noFullSync && <Tag icon={NO_FULL_SYNC_TAG.icon} text={NO_FULL_SYNC_TAG.text} /> || ""
592
+ flags: {
593
+ title: "Flags",
594
+ // Only flags that are set produce a tag, so an empty cell means every default
595
+ formatter: flags => <div className={css.hbox(4).wrap}>
596
+ {(flags || []).map(tag => <Tag key={tag.text} icon={tag.icon} text={tag.text} />)}
597
+ </div>
588
598
  },
589
599
  indexSources: {
590
600
  title: "Index sources",
@@ -1,5 +1,7 @@
1
1
  import { qreact } from "../../4-dom/qreact";
2
2
  import { css } from "typesafecss";
3
+ import { formatNumber, formatTime } from "socket-function/src/formatting/format";
4
+ import type { HostedConfig, BackblazeConfig } from "sliftutils/storage/IArchives";
3
5
 
4
6
  module.hotreload = true;
5
7
 
@@ -9,8 +11,23 @@ export const TAG_COLOR: TagColor = { h: 210, s: 45, l: 88 };
9
11
  export const TAG_WARNING_COLOR: TagColor = { h: 35, s: 90, l: 85 };
10
12
  export const TAG_FONT_SIZE = 11;
11
13
 
12
- /** A source flag, shown wherever that flag matters. The tag is only ever rendered when the flag is on - an absent tag is the default, so there is nothing to show for it. */
13
- export const NO_FULL_SYNC_TAG = { icon: "⇣", text: "noFullSync" };
14
+ export type SourceTag = { icon: string; text: string };
15
+
16
+ /** Every flag a source has set. Only flags that are on produce a tag - an absent tag is the default, so there is nothing to show for it. */
17
+ export function getSourceTags(source: HostedConfig | BackblazeConfig): SourceTag[] {
18
+ let tags: SourceTag[] = [];
19
+ if (source.public) tags.push({ icon: "🌐", text: "public" });
20
+ if (source.immutable) tags.push({ icon: "🔒", text: "immutable" });
21
+ if (source.noFullSync) tags.push({ icon: "⇣", text: "noFullSync" });
22
+ if (source.intermediate) tags.push({ icon: "⏳", text: "intermediate" });
23
+ if (source.type === "remote") {
24
+ if (source.fast) tags.push({ icon: "⚡", text: "fast" });
25
+ if (source.rawDisk) tags.push({ icon: "💽", text: "rawDisk" });
26
+ if (source.writeDelay !== undefined) tags.push({ icon: "⏱", text: `writeDelay ${formatTime(source.writeDelay)}` });
27
+ }
28
+ if (source.readerDiskLimit !== undefined) tags.push({ icon: "📦", text: `readCache ${formatNumber(source.readerDiskLimit)}B` });
29
+ return tags;
30
+ }
14
31
 
15
32
  export class Tag extends qreact.Component<{ icon: string; text: string; warning?: boolean; color?: TagColor; title?: string; }> {
16
33
  render() {