querysub 0.525.0 → 0.527.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/.claude/settings.local.json +2 -1
- package/appSecrets.ts +23 -0
- package/bin/storageserve.js +8 -0
- package/package.json +4 -3
- package/src/-b-authorities/cdnAuthority.ts +1 -1
- package/src/-b-authorities/cloudflareHelpers.ts +2 -45
- package/src/-b-authorities/dnsAuthority.ts +44 -185
- package/src/-c-identity/IdentityController.ts +4 -2
- package/src/-d-trust/NetworkTrust2.ts +1 -1
- package/src/-f-node-discovery/LatencyTracking.ts +18 -1
- package/src/-f-node-discovery/NodeDiscovery.ts +1 -1
- package/src/-h-path-value-serialize/PathValueSerializer.ts +35 -18
- package/src/0-path-value-core/AuthorityLookup.ts +4 -2
- package/src/0-path-value-core/PathRouter.ts +32 -10
- package/src/1-path-client/RemoteWatcher.ts +46 -2
- package/src/4-querysub/FunctionRunnerTracking.ts +73 -3
- package/src/config.ts +5 -0
- package/src/diagnostics/managementPages.tsx +0 -6
- package/src/diagnostics/misc-pages/RoutingTablePage.tsx +39 -4
- package/src/diagnostics/periodic.ts +2 -1
- package/src/diagnostics/watchdog.ts +11 -0
- package/src/library-components/EdgeNodeSelector.tsx +2 -1
- package/src/library-components/LatencyGraph.tsx +27 -8
- package/src/server.ts +7 -0
- package/src/diagnostics/misc-pages/DNSPage.tsx +0 -354
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"WebSearch",
|
|
30
30
|
"WebFetch(domain:blog.cloudflare.com)",
|
|
31
31
|
"WebFetch(domain:developers.cloudflare.com)",
|
|
32
|
-
"PowerShell(yarn type *)"
|
|
32
|
+
"PowerShell(yarn type *)",
|
|
33
|
+
"PowerShell(Remove-Item -Confirm:$false \"C:\\\\Users\\\\quent\\\\.claude\\\\projects\\\\D--repos-querysub\\\\memory\\\\no-naming-new-bins.md\")"
|
|
33
34
|
]
|
|
34
35
|
}
|
|
35
36
|
}
|
package/appSecrets.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { getBackblazePath } from "./src/-a-archives/archivesBackBlaze";
|
|
3
|
+
import { getCloudflareCreds } from "./src/-b-authorities/cloudflareHelpers";
|
|
4
|
+
import { Querysub } from "./src/4-querysub/Querysub";
|
|
5
|
+
|
|
6
|
+
/** Serves the secret keys sliftutils' getSecret expects, reading them the way querysub already does: backblaze creds off disk (getBackblazePath), cloudflare creds from the keys archives bucket (with its local file fallback). */
|
|
7
|
+
export async function getAppSecret(key: string): Promise<string | undefined> {
|
|
8
|
+
// Use this so some of our default imports get imported
|
|
9
|
+
Querysub;
|
|
10
|
+
if (key.startsWith("backblaze.json.")) {
|
|
11
|
+
let creds = JSON.parse(fs.readFileSync(getBackblazePath(), "utf8")) as { applicationKeyId?: string; applicationKey?: string };
|
|
12
|
+
if (key === "backblaze.json.applicationKeyId") return creds.applicationKeyId;
|
|
13
|
+
if (key === "backblaze.json.applicationKey") return creds.applicationKey;
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
if (key.startsWith("cloudflare.json.")) {
|
|
17
|
+
let creds = await getCloudflareCreds();
|
|
18
|
+
if (key === "cloudflare.json.key") return creds.key;
|
|
19
|
+
if (key === "cloudflare.json.email") return creds.email;
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
require("typenode");
|
|
4
|
+
require("../src/inject.ts");
|
|
5
|
+
const config = require("../src/config");
|
|
6
|
+
let url = `https://storage.${config.getDomain()}:${config.getPort()}`;
|
|
7
|
+
process.argv.push(`--url=${url}`);
|
|
8
|
+
require("sliftutils/storage/remoteStorage/storageServerCli");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.527.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",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"audit-imports": "./bin/audit-imports.js",
|
|
50
50
|
"audit-disk-values": "./bin/audit-disk-values.js",
|
|
51
51
|
"mcp-indexed-logs": "./bin/mcp-indexed-logs.js",
|
|
52
|
-
"autofix": "./bin/autofix.js"
|
|
52
|
+
"autofix": "./bin/autofix.js",
|
|
53
|
+
"storageserve": "./bin/storageserve.js"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
56
|
"@types/fs-ext": "^2.0.3",
|
|
@@ -70,7 +71,7 @@
|
|
|
70
71
|
"pako": "^2.1.0",
|
|
71
72
|
"peggy": "^5.0.6",
|
|
72
73
|
"sliftutils": "^1.7.5",
|
|
73
|
-
"socket-function": "^1.2.
|
|
74
|
+
"socket-function": "^1.2.15",
|
|
74
75
|
"terser": "^5.31.0",
|
|
75
76
|
"typenode": "^6.6.1",
|
|
76
77
|
"typesafecss": "^0.32.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Archives } from "../-a-archives/archives";
|
|
2
2
|
import { ArchivesBackblaze } from "../-a-archives/archivesBackBlaze";
|
|
3
|
-
import { cloudflareGETCall, cloudflarePOSTCall } from "
|
|
3
|
+
import { cloudflareGETCall, cloudflarePOSTCall } from "sliftutils/misc/https/cloudflareHelpers";
|
|
4
4
|
import { setRecord, getZoneId } from "./dnsAuthority";
|
|
5
5
|
import debugbreak from "debugbreak";
|
|
6
6
|
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { httpsRequest } from "socket-function/src/https";
|
|
2
1
|
import { getArchives } from "../-a-archives/archives";
|
|
3
2
|
import { lazy } from "socket-function/src/caching";
|
|
4
|
-
import { getStorageDir
|
|
3
|
+
import { getStorageDir } from "../fs";
|
|
5
4
|
import fs from "fs";
|
|
6
|
-
import { isNodeTrue } from "socket-function/src/misc";
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
const keys = lazy(() => getArchives("keys"));
|
|
9
7
|
|
|
10
8
|
export const getCloudflareCreds = lazy(async (): Promise<{ key: string; email: string }> => {
|
|
11
9
|
let credsJSON = await keys().get("cloudflare.json");
|
|
@@ -20,44 +18,3 @@ export const getCloudflareCreds = lazy(async (): Promise<{ key: string; email: s
|
|
|
20
18
|
}
|
|
21
19
|
return JSON.parse(credsJSON.toString());
|
|
22
20
|
});
|
|
23
|
-
|
|
24
|
-
export async function cloudflareGETCall<T>(path: string, params?: { [key: string]: string }): Promise<T> {
|
|
25
|
-
let url = new URL(`https://api.cloudflare.com/client/v4` + path);
|
|
26
|
-
for (let key in params) {
|
|
27
|
-
url.searchParams.set(key, params[key]);
|
|
28
|
-
}
|
|
29
|
-
let creds = await getCloudflareCreds();
|
|
30
|
-
let result = await httpsRequest(url.toString(), [], "GET", undefined, {
|
|
31
|
-
headers: {
|
|
32
|
-
"Content-Type": "application/json",
|
|
33
|
-
"X-Auth-Email": creds.email,
|
|
34
|
-
"X-Auth-User-Service-Key": creds.key,
|
|
35
|
-
"X-Auth-Key": creds.key,
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
let result2 = JSON.parse(result.toString()) as { result: unknown; success: boolean; errors: { code: number; message: string }[] };
|
|
39
|
-
if (!result2.success) {
|
|
40
|
-
throw new Error(`Cloudflare call failed: ${result2.errors.map(x => x.message).join(", ")}`);
|
|
41
|
-
}
|
|
42
|
-
return result2.result as T;
|
|
43
|
-
}
|
|
44
|
-
export async function cloudflarePOSTCall<T>(path: string, params: { [key: string]: unknown }): Promise<T> {
|
|
45
|
-
return await cloudflareCall(path, Buffer.from(JSON.stringify(params)), "POST");
|
|
46
|
-
}
|
|
47
|
-
export async function cloudflareCall<T>(path: string, payload: Buffer, method: string): Promise<T> {
|
|
48
|
-
let url = new URL(`https://api.cloudflare.com/client/v4` + path);
|
|
49
|
-
let creds = await getCloudflareCreds();
|
|
50
|
-
let result = await httpsRequest(url.toString(), payload, method, undefined, {
|
|
51
|
-
headers: {
|
|
52
|
-
"Content-Type": "application/json",
|
|
53
|
-
"X-Auth-Email": creds.email,
|
|
54
|
-
"X-Auth-User-Service-Key": creds.key,
|
|
55
|
-
"X-Auth-Key": creds.key,
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
let result2 = JSON.parse(result.toString()) as { result: unknown; success: boolean; errors: { code: number; message: string }[] };
|
|
59
|
-
if (!result2.success) {
|
|
60
|
-
throw new Error(`Cloudflare call failed: ${result2.errors.map(x => x.message).join(", ")}`);
|
|
61
|
-
}
|
|
62
|
-
return result2.result as T;
|
|
63
|
-
}
|
|
@@ -1,185 +1,44 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
await getCloudflareCreds();
|
|
47
|
-
return true;
|
|
48
|
-
} catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
export const getZoneId = cache(async (rootDomain: string): Promise<string> => {
|
|
54
|
-
let zones = await cloudflareGETCall<{ id: string; name: string }[]>("/zones", {});
|
|
55
|
-
let selected = zones.find(x => x.name === rootDomain);
|
|
56
|
-
if (!selected) {
|
|
57
|
-
throw new Error(`Could not find zone for ${rootDomain}. Found ${zones.map(x => x.name).join(", ")}`);
|
|
58
|
-
}
|
|
59
|
-
return selected.id;
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
function getRootDomain(key: string) {
|
|
63
|
-
if (key.endsWith(".")) {
|
|
64
|
-
key = key.slice(0, -1);
|
|
65
|
-
}
|
|
66
|
-
return key.split(".").slice(-2).join(".");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export async function getRecordsRaw(type: string, key: string) {
|
|
70
|
-
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
71
|
-
let zoneId = await getZoneId(getRootDomain(key));
|
|
72
|
-
let results = await cloudflareGETCall<{
|
|
73
|
-
id: string;
|
|
74
|
-
type: string;
|
|
75
|
-
name: string;
|
|
76
|
-
content: string;
|
|
77
|
-
proxied: boolean;
|
|
78
|
-
modified_on: string;
|
|
79
|
-
// Omitted by Cloudflare when the record has no comment.
|
|
80
|
-
comment?: string;
|
|
81
|
-
}[]>(`/zones/${zoneId}/dns_records`);
|
|
82
|
-
// DNS names are case-insensitive and Cloudflare returns them lowercased, so a mixed-case key
|
|
83
|
-
// (e.g. a machine id subdomain) would never match an exact-case compare - match lowercased.
|
|
84
|
-
let keyLower = key.toLowerCase();
|
|
85
|
-
return results.filter(x => x.type === type && x.name.toLowerCase() === keyLower);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Cloudflare's batch endpoint applies deletes, then patches, then posts in a single database
|
|
89
|
-
* transaction. We route edits (patches) through here because the standalone PATCH/PUT verbs
|
|
90
|
-
* aren't usable in our setup, and because it lets "remove others + assert target" happen
|
|
91
|
-
* without a window where the name resolves to nothing. */
|
|
92
|
-
export async function batchRecords(zoneId: string, batch: {
|
|
93
|
-
deletes?: { id: string }[];
|
|
94
|
-
patches?: { id: string; comment?: string }[];
|
|
95
|
-
posts?: { type: string; name: string; content: string; ttl: number; proxied: boolean; comment?: string }[];
|
|
96
|
-
}) {
|
|
97
|
-
let payload: { [key: string]: unknown } = {};
|
|
98
|
-
if (batch.deletes && batch.deletes.length > 0) payload.deletes = batch.deletes;
|
|
99
|
-
if (batch.patches && batch.patches.length > 0) payload.patches = batch.patches;
|
|
100
|
-
if (batch.posts && batch.posts.length > 0) payload.posts = batch.posts;
|
|
101
|
-
try {
|
|
102
|
-
await cloudflarePOSTCall(`/zones/${zoneId}/dns_records/batch`, payload);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
console.error(`Error updating DNS records:`, { error: error, batch });
|
|
105
|
-
throw new Error(`Error updating DNS records. ${JSON.stringify(batch)}. Error: ${error}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
export async function getRecords(type: string, key: string) {
|
|
109
|
-
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
110
|
-
let raw = await getRecordsRaw(type, key);
|
|
111
|
-
return raw.map(x => x.content);
|
|
112
|
-
}
|
|
113
|
-
export async function deleteRecord(type: string, key: string, value: string) {
|
|
114
|
-
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
115
|
-
let zoneId = await getZoneId(getRootDomain(key));
|
|
116
|
-
let prevValues = await getRecordsRaw(type, key);
|
|
117
|
-
prevValues = prevValues.filter(x => x.content === value);
|
|
118
|
-
if (prevValues.length === 0) {
|
|
119
|
-
if (!SocketFunction.silent) {
|
|
120
|
-
console.log(`No need to delete record, it was not found. ${JSON.stringify(value)} value was not in ${type} for ${key}, values ${JSON.stringify(prevValues.map(x => x.content))}`);
|
|
121
|
-
}
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
console.log(`Removing records of ${type} for ${key}, values ${JSON.stringify(prevValues.map(x => x.content))}`);
|
|
126
|
-
for (let value of prevValues) {
|
|
127
|
-
await cloudflareCall(`/zones/${zoneId}/dns_records/${value.id}`, Buffer.from([]), "DELETE");
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/** Removes all existing records (unless the record is already present and fresh) */
|
|
131
|
-
export async function setRecord(type: string, key: string, value: string, proxied?: "proxied", staleAfter = DNS_REFRESH_STALE_AFTER) {
|
|
132
|
-
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
133
|
-
let zoneId = await getZoneId(getRootDomain(key));
|
|
134
|
-
let prevValues = await getRecordsRaw(type, key);
|
|
135
|
-
let existing = prevValues.find(x => x.content === value);
|
|
136
|
-
let others = prevValues.filter(x => x.content !== value);
|
|
137
|
-
|
|
138
|
-
// Already correct and recently asserted - a prior run also cleaned up the other records,
|
|
139
|
-
// so there is nothing left to do.
|
|
140
|
-
if (existing && Date.now() - freshnessTime(existing.comment) < staleAfter) return;
|
|
141
|
-
|
|
142
|
-
// A single atomic batch: drop the wrong records, and either refresh the existing record's
|
|
143
|
-
// comment in place or create it - so the name is never left resolving to nothing.
|
|
144
|
-
let ttl = DNS_TTLSeconds[type as "A"] || 60;
|
|
145
|
-
let comment = stampFreshness(existing?.comment);
|
|
146
|
-
console.log(magenta(`Setting ${type} record for ${key} to ${value} (previously had ${JSON.stringify(prevValues.map(x => x.content))})`));
|
|
147
|
-
await batchRecords(zoneId, {
|
|
148
|
-
deletes: others.map(x => ({ id: x.id })),
|
|
149
|
-
patches: existing ? [{ id: existing.id, comment }] : [],
|
|
150
|
-
posts: existing ? [] : [{ type, name: key, content: value, ttl, proxied: proxied === "proxied", comment }],
|
|
151
|
-
});
|
|
152
|
-
// Only a brand new record needs to propagate; an in-place comment refresh doesn't change the answer.
|
|
153
|
-
if (!existing) {
|
|
154
|
-
console.log(`Waiting ${ttl} seconds for DNS to propagate...`);
|
|
155
|
-
for (let ttlLeft = ttl; ttlLeft > 0; ttlLeft--) {
|
|
156
|
-
await delay(1000);
|
|
157
|
-
console.log(`${ttlLeft} seconds left...`);
|
|
158
|
-
}
|
|
159
|
-
console.log(`Done waiting for DNS to update.`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/** Keeps existing records */
|
|
163
|
-
export async function addRecord(type: string, key: string, value: string, proxied?: "proxied", staleAfter = DNS_REFRESH_STALE_AFTER) {
|
|
164
|
-
if (key.endsWith(".")) key = key.slice(0, -1);
|
|
165
|
-
let zoneId = await getZoneId(getRootDomain(key));
|
|
166
|
-
let prevValues = await getRecordsRaw(type, key);
|
|
167
|
-
let existing = prevValues.find(x => x.content === value);
|
|
168
|
-
if (existing && Date.now() - freshnessTime(existing.comment) < staleAfter) return;
|
|
169
|
-
|
|
170
|
-
// Same single-batch flow as setRecord, minus the deletes (we keep sibling records here).
|
|
171
|
-
let ttl = DNS_TTLSeconds[type as "A"] || 60;
|
|
172
|
-
let comment = stampFreshness(existing?.comment);
|
|
173
|
-
console.log(`Adding ${type} record for ${key} to ${value} (previously had ${JSON.stringify(prevValues.map(x => x.content))})`);
|
|
174
|
-
await batchRecords(zoneId, {
|
|
175
|
-
patches: existing ? [{ id: existing.id, comment }] : [],
|
|
176
|
-
posts: existing ? [] : [{ type, name: key, content: value, ttl, proxied: proxied === "proxied", comment }],
|
|
177
|
-
});
|
|
178
|
-
if (existing) return;
|
|
179
|
-
console.log(`Waiting ${ttl} seconds for DNS to propagate...`);
|
|
180
|
-
for (let ttlLeft = ttl; ttlLeft > 0; ttlLeft--) {
|
|
181
|
-
await delay(1000);
|
|
182
|
-
console.log(`${ttlLeft} seconds left...`);
|
|
183
|
-
}
|
|
184
|
-
console.log(`Done waiting for DNS to update.`);
|
|
185
|
-
}
|
|
1
|
+
import { lazy } from "socket-function/src/caching";
|
|
2
|
+
import { isNode } from "socket-function/src/misc";
|
|
3
|
+
import { isClient } from "../config2";
|
|
4
|
+
import {
|
|
5
|
+
addRecord as addRecordBase,
|
|
6
|
+
deleteRecord as deleteRecordBase,
|
|
7
|
+
freshnessTime as freshnessTimeBase,
|
|
8
|
+
getRecords as getRecordsBase,
|
|
9
|
+
getZoneId as getZoneIdBase,
|
|
10
|
+
hasDNSWritePermissions as hasDNSWritePermissionsBase,
|
|
11
|
+
setRecord as setRecordBase,
|
|
12
|
+
} from "sliftutils/misc/https/dns";
|
|
13
|
+
|
|
14
|
+
export function freshnessTime(comment?: string): number {
|
|
15
|
+
return freshnessTimeBase(comment);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const hasDNSWritePermissions = lazy(async () => {
|
|
19
|
+
if (!isNode()) return false;
|
|
20
|
+
if (isClient()) return false;
|
|
21
|
+
return await hasDNSWritePermissionsBase();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export async function getZoneId(rootDomain: string): Promise<string> {
|
|
25
|
+
return await getZoneIdBase(rootDomain);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getRecords(type: string, key: string): Promise<string[]> {
|
|
29
|
+
return await getRecordsBase(type, key);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function deleteRecord(type: string, key: string, value: string): Promise<void> {
|
|
33
|
+
return await deleteRecordBase(type, key, value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Removes all existing records (unless the record is already present and fresh) */
|
|
37
|
+
export async function setRecord(type: string, key: string, value: string, proxied?: "proxied", staleAfter?: number): Promise<void> {
|
|
38
|
+
return await setRecordBase(type, key, value, proxied, staleAfter);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Keeps existing records */
|
|
42
|
+
export async function addRecord(type: string, key: string, value: string, proxied?: "proxied", staleAfter?: number): Promise<void> {
|
|
43
|
+
return await addRecordBase(type, key, value, proxied, staleAfter);
|
|
44
|
+
}
|
|
@@ -251,6 +251,7 @@ const changeIdentityOnce = cacheWeak(async function changeIdentityOnce(connectio
|
|
|
251
251
|
() => new Error(`Timeout calling changeIdentity for ${nodeId}`)
|
|
252
252
|
);
|
|
253
253
|
});
|
|
254
|
+
let startupTime = Date.now();
|
|
254
255
|
SocketFunction.addGlobalClientHook(async function identityHook(context) {
|
|
255
256
|
if (context.call.classGuid === IdentityController._classGuid) return;
|
|
256
257
|
// This is for US to tell them our identity. And if they established the connection the identity will come from their original connection url (that they used to connect to us), and they validated it either being a real certificate, or they added the cert from the trusted backblaze bucket. If it just from a real certificate it means we identified them, but they might not have network trust. But that's fine, as IdentityController is JUST for identification, and if it's a real certificate we know who they are! (Which doesn't mean we trust them).
|
|
@@ -259,8 +260,9 @@ SocketFunction.addGlobalClientHook(async function identityHook(context) {
|
|
|
259
260
|
}
|
|
260
261
|
let time = Date.now();
|
|
261
262
|
await changeIdentityOnce(context.connectionId);
|
|
262
|
-
let
|
|
263
|
-
|
|
263
|
+
let now = Date.now();
|
|
264
|
+
let duration = now - time;
|
|
265
|
+
if (duration > 200 && now - startupTime > timeInMinute) {
|
|
264
266
|
console.log(red(`IdentityHook took ${formatTime(duration)} for ${context.connectionId.nodeId} ${context.call.classGuid}.${context.call.functionName}`));
|
|
265
267
|
}
|
|
266
268
|
});
|
|
@@ -119,7 +119,7 @@ export const loadServerCert = cache(async (machineId: string) => {
|
|
|
119
119
|
console.warn(`Could not find certificate in archives for ${machineId}`);
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
|
-
console.
|
|
122
|
+
console.info(magenta(`Loading certificate for ${machineId}`));
|
|
123
123
|
trustCertificate(certFile);
|
|
124
124
|
});
|
|
125
125
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
11
|
-
import { timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
11
|
+
import { sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
12
12
|
import { lazy } from "socket-function/src/caching";
|
|
13
13
|
import { delay, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
14
14
|
import { isServer } from "../config2";
|
|
@@ -23,6 +23,8 @@ const LATENCY_POLL_INTERVAL = timeInMinute;
|
|
|
23
23
|
const PING_TIMEOUT = timeInSecond * 5;
|
|
24
24
|
// Rolling average size. Small, so latency changes show up quickly.
|
|
25
25
|
const LATENCY_SAMPLE_LIMIT = 20;
|
|
26
|
+
// Raw sample history kept per node, so callers can ask for a median over the last N with a known confidence.
|
|
27
|
+
const LATENCY_HISTORY_LIMIT = 100;
|
|
26
28
|
// Forget nodes we haven't been able to reach for this long.
|
|
27
29
|
const NODE_EXPIRY_TIME = LATENCY_POLL_INTERVAL * 5;
|
|
28
30
|
|
|
@@ -30,6 +32,7 @@ export type NodeLatencyInfo = {
|
|
|
30
32
|
averageLatency: number;
|
|
31
33
|
sampleCount: number;
|
|
32
34
|
lastSeen: number;
|
|
35
|
+
history: number[];
|
|
33
36
|
};
|
|
34
37
|
|
|
35
38
|
// otherNodeId => our rolling latency to it
|
|
@@ -42,6 +45,14 @@ export function getNodeLatencyInfo(nodeId: string): NodeLatencyInfo | undefined
|
|
|
42
45
|
export function getNodeLatency(nodeId: string): number | undefined {
|
|
43
46
|
return getNodeLatencyInfo(nodeId)?.averageLatency;
|
|
44
47
|
}
|
|
48
|
+
// Median of the last historyCount raw samples, plus how many samples were actually available — so callers can scale how much they trust the number.
|
|
49
|
+
export function getNodeLatencyMedian(config: { nodeId: string; historyCount: number }): { latency: number; historyUsed: number } | undefined {
|
|
50
|
+
let history = getNodeLatencyInfo(config.nodeId)?.history;
|
|
51
|
+
if (!history || history.length === 0) return undefined;
|
|
52
|
+
let samples = history.slice(-config.historyCount);
|
|
53
|
+
sort(samples, x => x);
|
|
54
|
+
return { latency: samples[Math.floor(samples.length / 2)], historyUsed: samples.length };
|
|
55
|
+
}
|
|
45
56
|
|
|
46
57
|
export function getCachedNodeLatencyInfoList(): Map<string, NodeLatencyInfo> {
|
|
47
58
|
return latencyByNode;
|
|
@@ -60,10 +71,16 @@ export function getOwnLatencies(): { [nodeId: string]: number } {
|
|
|
60
71
|
function recordLatency(nodeId: string, latency: number) {
|
|
61
72
|
let prev = latencyByNode.get(nodeId);
|
|
62
73
|
let sampleCount = Math.min(prev?.sampleCount || 0, LATENCY_SAMPLE_LIMIT);
|
|
74
|
+
let history = prev?.history || [];
|
|
75
|
+
history.push(latency);
|
|
76
|
+
if (history.length > LATENCY_HISTORY_LIMIT) {
|
|
77
|
+
history.shift();
|
|
78
|
+
}
|
|
63
79
|
latencyByNode.set(nodeId, {
|
|
64
80
|
averageLatency: ((prev?.averageLatency || 0) * sampleCount + latency) / (sampleCount + 1),
|
|
65
81
|
sampleCount: sampleCount + 1,
|
|
66
82
|
lastSeen: Date.now(),
|
|
83
|
+
history,
|
|
67
84
|
});
|
|
68
85
|
}
|
|
69
86
|
|
|
@@ -216,7 +216,7 @@ async function setNodeIds(nodeIds: string[]) {
|
|
|
216
216
|
if (newNodeIds.length === 0 && removedNodeIds.length === 0) return;
|
|
217
217
|
if (logging) {
|
|
218
218
|
for (let nodeId of newNodeIds) {
|
|
219
|
-
console.
|
|
219
|
+
console.info(blue(`Discovered node ${nodeId}`));
|
|
220
220
|
}
|
|
221
221
|
for (let nodeId of removedNodeIds) {
|
|
222
222
|
console.log(red(`Removed node from setNodeIds ${nodeId}`));
|
|
@@ -20,7 +20,7 @@ import { formatNumber, formatPercent, formatTime } from "socket-function/src/for
|
|
|
20
20
|
setFlag(require, "cbor-x", "allowclient", true);
|
|
21
21
|
|
|
22
22
|
import * as pako from "pako";
|
|
23
|
-
import { delay } from "socket-function/src/batching";
|
|
23
|
+
import { delay, safeLoop } from "socket-function/src/batching";
|
|
24
24
|
import { LZ4 } from "../storage/LZ4";
|
|
25
25
|
import { unblockLoop } from "socket-function/src/batching";
|
|
26
26
|
setFlag(require, "pako", "allowclient", true);
|
|
@@ -345,6 +345,7 @@ class PathValueSerializer {
|
|
|
345
345
|
let encodedValues: (Buffer | number)[] = [];
|
|
346
346
|
|
|
347
347
|
let i = 0;
|
|
348
|
+
let valuesSinceYield = 0;
|
|
348
349
|
for (let values of valuesGroups) {
|
|
349
350
|
measureBlock(() => {
|
|
350
351
|
// NOTE: Writing one value at a time is about 1.6X slower. BUT, it allows us to very efficient decode
|
|
@@ -378,21 +379,23 @@ class PathValueSerializer {
|
|
|
378
379
|
i++;
|
|
379
380
|
}
|
|
380
381
|
}, "valuesWriteLoop");
|
|
381
|
-
|
|
382
|
+
valuesSinceYield += values.length;
|
|
383
|
+
if (valuesSinceYield >= 10_000) {
|
|
384
|
+
valuesSinceYield = 0;
|
|
385
|
+
await delay("paintLoop");
|
|
386
|
+
}
|
|
382
387
|
}
|
|
383
388
|
|
|
384
389
|
measureBlock(function valuesWriteTypesBuffer() {
|
|
385
390
|
writer.writeBuffer(types);
|
|
386
391
|
});
|
|
387
|
-
// Break encodeValues into groups of
|
|
388
|
-
// if we are taking too long, but not check it EVERY loop, as that would
|
|
389
|
-
// be too slow.
|
|
392
|
+
// Break encodeValues into groups, and yield based on the amount of values written — NOT by measuring time, as timing every loop is itself too slow.
|
|
390
393
|
let valueGroups: (Buffer | number)[][] = [];
|
|
391
394
|
const VALUE_GROUP_SIZE = 1000;
|
|
392
395
|
for (let i = 0; i < encodedValues.length; i += VALUE_GROUP_SIZE) {
|
|
393
396
|
valueGroups.push(encodedValues.slice(i, i + VALUE_GROUP_SIZE));
|
|
394
397
|
}
|
|
395
|
-
let
|
|
398
|
+
let encodedSinceYield = 0;
|
|
396
399
|
for (let encodedValues of valueGroups) {
|
|
397
400
|
measureBlock(function valuesWriteTypes() {
|
|
398
401
|
for (let value of encodedValues) {
|
|
@@ -403,10 +406,10 @@ class PathValueSerializer {
|
|
|
403
406
|
}
|
|
404
407
|
}
|
|
405
408
|
});
|
|
406
|
-
|
|
407
|
-
if (
|
|
409
|
+
encodedSinceYield += encodedValues.length;
|
|
410
|
+
if (encodedSinceYield >= 10_000) {
|
|
411
|
+
encodedSinceYield = 0;
|
|
408
412
|
await delay("paintLoop");
|
|
409
|
-
prevTime = now;
|
|
410
413
|
}
|
|
411
414
|
}
|
|
412
415
|
}
|
|
@@ -550,9 +553,14 @@ class PathValueSerializer {
|
|
|
550
553
|
}
|
|
551
554
|
|
|
552
555
|
startNewBuffer("pathValues");
|
|
556
|
+
let pathValuesSinceYield = 0;
|
|
553
557
|
for (let values of valueGroups) {
|
|
554
558
|
this.pathValuesWrite(writer, values, settings);
|
|
555
|
-
|
|
559
|
+
pathValuesSinceYield += values.length;
|
|
560
|
+
if (pathValuesSinceYield >= 10_000) {
|
|
561
|
+
pathValuesSinceYield = 0;
|
|
562
|
+
await delay("afterPaint");
|
|
563
|
+
}
|
|
556
564
|
}
|
|
557
565
|
|
|
558
566
|
if (!settings.noLocks) {
|
|
@@ -593,15 +601,20 @@ class PathValueSerializer {
|
|
|
593
601
|
curCount += str.length;
|
|
594
602
|
}
|
|
595
603
|
|
|
604
|
+
let stringBytesSinceYield = 0;
|
|
596
605
|
for (let strings of stringParts) {
|
|
597
606
|
let stringBuffer = StringSerialize.serializeStrings(strings);
|
|
598
607
|
settings.bufferMap!.push("strings");
|
|
599
608
|
outputBuffers.push(stringBuffer);
|
|
600
|
-
|
|
609
|
+
stringBytesSinceYield += stringBuffer.length;
|
|
610
|
+
if (stringBytesSinceYield >= 1_000_000) {
|
|
611
|
+
stringBytesSinceYield = 0;
|
|
612
|
+
await delay("paintLoop");
|
|
613
|
+
}
|
|
601
614
|
}
|
|
602
615
|
|
|
603
616
|
if (settings.compression === "lz4") {
|
|
604
|
-
let compressedBuffers = await
|
|
617
|
+
let compressedBuffers = await measureBlock(() => safeLoop({ data: outputBuffers, name: "PathValueSerializer.serialize|lz4" }, x => LZ4.compress(x)), "PathValueSerializer.serialize|lz4");
|
|
605
618
|
|
|
606
619
|
// If the compress factor is less than a threshold, use the uncompressed buffers
|
|
607
620
|
let uncompressedSize = outputBuffers.reduce((total, x) => total + x.length, 0);
|
|
@@ -723,6 +736,7 @@ class PathValueSerializer {
|
|
|
723
736
|
}
|
|
724
737
|
if (!config?.skipStrings) {
|
|
725
738
|
let stringArrays: string[][] = [];
|
|
739
|
+
let bytesSinceYield = 0;
|
|
726
740
|
for (let stringBuffer of stringBuffers) {
|
|
727
741
|
let obj = StringSerialize.deserializeStringsLazy(stringBuffer);
|
|
728
742
|
while (true) {
|
|
@@ -731,11 +745,14 @@ class PathValueSerializer {
|
|
|
731
745
|
break;
|
|
732
746
|
}
|
|
733
747
|
stringArrays.push(nextStrings);
|
|
734
|
-
|
|
748
|
+
// Each getNextStrings call decodes ~1MB, so on buffers past that we yield per chunk decoded.
|
|
749
|
+
if (stringBuffer.length > 1_000_000) {
|
|
735
750
|
await delay("paintLoop");
|
|
736
751
|
}
|
|
737
752
|
}
|
|
738
|
-
|
|
753
|
+
bytesSinceYield += stringBuffer.length;
|
|
754
|
+
if (bytesSinceYield >= 1_000_000) {
|
|
755
|
+
bytesSinceYield = 0;
|
|
739
756
|
await delay("paintLoop");
|
|
740
757
|
}
|
|
741
758
|
}
|
|
@@ -848,6 +865,7 @@ class PathValueSerializer {
|
|
|
848
865
|
}
|
|
849
866
|
|
|
850
867
|
let stringArrays: string[][] = [];
|
|
868
|
+
let bytesSinceYield = 0;
|
|
851
869
|
for (let bufferIndex of stringBufferIndexes) {
|
|
852
870
|
let stringBuffer = buffers[bufferIndex];
|
|
853
871
|
let obj = StringSerialize.deserializeStringsLazy(stringBuffer);
|
|
@@ -857,13 +875,12 @@ class PathValueSerializer {
|
|
|
857
875
|
break;
|
|
858
876
|
}
|
|
859
877
|
stringArrays.push(nextStrings);
|
|
860
|
-
|
|
878
|
+
bytesSinceYield += stringBuffer.length;
|
|
879
|
+
if (bytesSinceYield >= 1_000_000) {
|
|
880
|
+
bytesSinceYield = 0;
|
|
861
881
|
await delay("paintLoop");
|
|
862
882
|
}
|
|
863
883
|
}
|
|
864
|
-
if (stringBufferIndexes.length > 1) {
|
|
865
|
-
await delay("paintLoop");
|
|
866
|
-
}
|
|
867
884
|
}
|
|
868
885
|
strings = stringArrays.flat();
|
|
869
886
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
1
|
+
import { sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
2
2
|
import { nestArchives } from "../-a-archives/archives";
|
|
3
3
|
import { getArchivesBackblaze } from "../-a-archives/archivesBackBlaze";
|
|
4
4
|
import { archiveJSONT } from "../-a-archives/archivesJSONT";
|
|
@@ -31,7 +31,7 @@ let CALL_TIMEOUT = timeInSecond * 5;
|
|
|
31
31
|
const SYNC_JITTER_WINDOW = timeInSecond * 10;
|
|
32
32
|
|
|
33
33
|
// Nodes this close to their scheduled shutdown are avoided as sources (only used if nothing else can satisfy the request).
|
|
34
|
-
const SHUTDOWN_AVOID_WINDOW = timeInMinute *
|
|
34
|
+
const SHUTDOWN_AVOID_WINDOW = timeInMinute * 5;
|
|
35
35
|
|
|
36
36
|
export type AuthorityEntry = {
|
|
37
37
|
nodeId: string;
|
|
@@ -82,6 +82,8 @@ class AuthorityLookup {
|
|
|
82
82
|
public async setOurSpec(spec: AuthoritySpec) {
|
|
83
83
|
if (!SocketFunction.isMounted()) throw new Error("Cannot call setOurPaths without mounting first (use Querysub.hostService).");
|
|
84
84
|
spec.nodeId = getOwnNodeId();
|
|
85
|
+
// AuthoritySpec.prefixes promises to be sorted, and this is the choke point every published spec goes through.
|
|
86
|
+
sort(spec.prefixes, prefix => prefix.originalPrefix);
|
|
85
87
|
|
|
86
88
|
|
|
87
89
|
if (this.setSpec) {
|