mycomesh-consumer 0.9.4 → 0.9.5
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 +1 -1
- package/src/consumer.mjs +62 -14
package/package.json
CHANGED
package/src/consumer.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// V11 Consumer runtime: one custodied deposit, any Provider, relay-blind requests.
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { createServer, request as httpRequest } from "node:http";
|
|
4
|
-
import { request as httpsRequest } from "node:https";
|
|
3
|
+
import { Agent as HttpAgent, createServer, request as httpRequest } from "node:http";
|
|
4
|
+
import { Agent as HttpsAgent, request as httpsRequest } from "node:https";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
6
|
import { rootCertificates } from "node:tls";
|
|
7
7
|
import { addressOf, encodeCall } from "./eip712.mjs";
|
|
@@ -28,10 +28,19 @@ export function loadNetwork(path) {
|
|
|
28
28
|
return network;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* A fresh connection per request, through an agent of its own: a reused keep-alive socket that the Relay's
|
|
33
|
+
* edge has just closed fails with ECONNRESET after the request may have been sent, which can only be
|
|
34
|
+
* reported as outcome unknown. (Only an agent's own createConnection is honoured, hence the pinned one here.)
|
|
35
|
+
*/
|
|
31
36
|
async function tlsOptions(target, ca, pin) {
|
|
32
|
-
if (target.protocol !== "https:") return {};
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
if (target.protocol !== "https:") return { agent: new HttpAgent({ keepAlive: false }) };
|
|
38
|
+
const agent = new HttpsAgent({ keepAlive: false });
|
|
39
|
+
if (pin) {
|
|
40
|
+
agent.createConnection = pinnedOptions(target.href, pin).createConnection;
|
|
41
|
+
return { agent };
|
|
42
|
+
}
|
|
43
|
+
return ca ? { agent, ca: [...rootCertificates, ca] } : { agent };
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export async function httpJson(url, { method = "GET", body, ca, pin, timeoutMs = 330_000 } = {}) {
|
|
@@ -56,7 +65,10 @@ export async function httpJson(url, { method = "GET", body, ca, pin, timeoutMs =
|
|
|
56
65
|
catch (error) { reject(error); }
|
|
57
66
|
});
|
|
58
67
|
});
|
|
59
|
-
|
|
68
|
+
// A wall-clock deadline, not a socket idle timeout: keep-alive sockets are reused, and an idle timeout
|
|
69
|
+
// left on one by an earlier short request (a 10 s /providers call) would later cut a long answer.
|
|
70
|
+
const timer = setTimeout(() => req.destroy(new Error(`request to ${target.host}${target.pathname} timed out after ${timeoutMs / 1000} s`)), timeoutMs);
|
|
71
|
+
req.on("close", () => clearTimeout(timer));
|
|
60
72
|
req.on("error", reject);
|
|
61
73
|
if (data) req.write(data);
|
|
62
74
|
req.end();
|
|
@@ -96,7 +108,10 @@ export async function httpNdjson(url, { body, ca, pin, timeoutMs = 340_000, onLi
|
|
|
96
108
|
try { resolvePromise({ status: res.statusCode, body: JSON.parse(chunks.join("") || "{}") }); } catch (error) { reject(error); }
|
|
97
109
|
});
|
|
98
110
|
});
|
|
99
|
-
|
|
111
|
+
// A wall-clock deadline, not a socket idle timeout: keep-alive sockets are reused, and an idle timeout
|
|
112
|
+
// left on one by an earlier short request (a 10 s /providers call) would later cut a long answer.
|
|
113
|
+
const timer = setTimeout(() => req.destroy(new Error(`request to ${target.host}${target.pathname} timed out after ${timeoutMs / 1000} s`)), timeoutMs);
|
|
114
|
+
req.on("close", () => clearTimeout(timer));
|
|
100
115
|
req.on("error", reject);
|
|
101
116
|
req.end(data);
|
|
102
117
|
});
|
|
@@ -129,6 +144,7 @@ export class Consumer {
|
|
|
129
144
|
constructor({ network, keyPrivate, maxFee, journalDir, fetchJson = httpJson, streamJson = httpNdjson, now = () => Math.floor(Date.now() / 1000) }) {
|
|
130
145
|
this.streamJson = streamJson;
|
|
131
146
|
this.journalDir = journalDir;
|
|
147
|
+
this.inFlight = new Map(); // Provider signer -> requests this Consumer has with it now
|
|
132
148
|
this.network = network;
|
|
133
149
|
this.keyPrivate = keyPrivate;
|
|
134
150
|
this.key = addressOf(keyPrivate);
|
|
@@ -213,8 +229,24 @@ export class Consumer {
|
|
|
213
229
|
return [...names].sort();
|
|
214
230
|
}
|
|
215
231
|
|
|
216
|
-
/**
|
|
217
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Try Relays and Providers in order; fail over only while the request provably was not dispatched. When
|
|
234
|
+
* every Provider is busy, wait and try again (a short queue) rather than failing a burst of requests.
|
|
235
|
+
*/
|
|
236
|
+
async request(args) {
|
|
237
|
+
const deadline = Date.now() + (this.queueMs ?? 120_000);
|
|
238
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
239
|
+
try {
|
|
240
|
+
return await this.attempt(args);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const busy = error.dispatched === false && [502, 503].includes(error.status);
|
|
243
|
+
if (!busy || Date.now() > deadline) throw error;
|
|
244
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(4_000, 500 * 2 ** attempt) * (0.5 + Math.random())));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async attempt({ endpoint, model, content, maxOutputTokens = 4096, options = {}, provider, onDelta }) {
|
|
218
250
|
let lastError;
|
|
219
251
|
for (const relay of await this.relays()) {
|
|
220
252
|
let candidates;
|
|
@@ -223,9 +255,18 @@ export class Consumer {
|
|
|
223
255
|
&& (!provider || descriptor.provider_signer === provider.toLowerCase()));
|
|
224
256
|
}
|
|
225
257
|
catch (error) { lastError = error; continue; }
|
|
226
|
-
// What the chain proves ranks first
|
|
258
|
+
// What the chain proves ranks first (recent fraud, a suspected downgrade, verified probe failures); then
|
|
259
|
+
// Providers this Consumer is not already keeping busy; then reputation in coarse bands, shuffled within a
|
|
260
|
+
// band so that many Consumers spread over the network instead of all queueing at its single best Provider.
|
|
227
261
|
const records = await this.reputations().catch(() => ({}));
|
|
228
|
-
|
|
262
|
+
const key = (descriptor) => {
|
|
263
|
+
const [fraud, downgrade, failure, volume] = rankKey(descriptor, records[descriptor.provider_signer]);
|
|
264
|
+
const busy = (this.inFlight.get(descriptor.provider_signer) || 0) >= Math.max(1, Number(descriptor.capacity) || 1) ? 1 : 0;
|
|
265
|
+
return [fraud, downgrade, busy, failure, -Math.floor(Math.log10(1 - volume)), Math.random()];
|
|
266
|
+
};
|
|
267
|
+
const keyed = candidates.map((descriptor) => [key(descriptor), descriptor]);
|
|
268
|
+
keyed.sort((a, b) => compareRank(a[0], b[0]));
|
|
269
|
+
candidates = keyed.map(([, descriptor]) => descriptor);
|
|
229
270
|
for (const descriptor of candidates) {
|
|
230
271
|
const prepared = prepareRequest({
|
|
231
272
|
descriptor, deployment: this.deployment, keyPrivate: this.keyPrivate, relaySigner: relay.signer,
|
|
@@ -234,6 +275,8 @@ export class Consumer {
|
|
|
234
275
|
let reply;
|
|
235
276
|
const streamed = [];
|
|
236
277
|
let final = null;
|
|
278
|
+
const signer = descriptor.provider_signer;
|
|
279
|
+
this.inFlight.set(signer, (this.inFlight.get(signer) || 0) + 1);
|
|
237
280
|
try {
|
|
238
281
|
reply = onDelta
|
|
239
282
|
? await this.streamJson(`${relay.url}/v11/requests`, { body: prepared.payload, ca: this.network.tls_ca, pin: relay.pin, onLine: (line) => {
|
|
@@ -248,11 +291,15 @@ export class Consumer {
|
|
|
248
291
|
} catch (error) {
|
|
249
292
|
// The request may have reached the Relay: never replay it elsewhere.
|
|
250
293
|
throw Object.assign(new Error(`request outcome unknown: ${error.message}`), { code: "outcome_unknown", requestId: prepared.authorization.request_id });
|
|
294
|
+
} finally {
|
|
295
|
+
this.inFlight.set(signer, (this.inFlight.get(signer) || 1) - 1);
|
|
251
296
|
}
|
|
252
297
|
if (reply.streamed) {
|
|
253
298
|
if (!final) throw Object.assign(new Error("stream ended without a result"), { code: "outcome_unknown" });
|
|
254
|
-
|
|
255
|
-
reply =
|
|
299
|
+
// An error before dispatch (a busy Provider, say) is safe to fail over, exactly as without streaming.
|
|
300
|
+
reply = final.type === "error"
|
|
301
|
+
? { status: final.status || 502, body: { error: final.error, dispatched: final.dispatched } }
|
|
302
|
+
: { status: 200, body: final };
|
|
256
303
|
}
|
|
257
304
|
if (reply.status === 200) {
|
|
258
305
|
const opened = openResponse(prepared, reply.body, this.deployment, this.now());
|
|
@@ -263,7 +310,8 @@ export class Consumer {
|
|
|
263
310
|
if (this.journalDir) recordRequest(this.journalDir, prepared, opened, relay.url);
|
|
264
311
|
return opened;
|
|
265
312
|
}
|
|
266
|
-
lastError = Object.assign(new Error(reply.body?.error || `Relay returned ${reply.status}`),
|
|
313
|
+
lastError = Object.assign(new Error(reply.body?.error || `Relay returned ${reply.status}`),
|
|
314
|
+
{ status: reply.status, dispatched: reply.body?.dispatched });
|
|
267
315
|
if (reply.body?.dispatched !== false || ![502, 503].includes(reply.status)) throw lastError;
|
|
268
316
|
}
|
|
269
317
|
}
|