@cotal-ai/core 0.54.0 → 0.55.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/dist/auth-provider.d.ts +24 -1
- package/dist/auth-provider.d.ts.map +1 -1
- package/dist/auth-provider.js.map +1 -1
- package/dist/connector.d.ts +7 -1
- package/dist/connector.d.ts.map +1 -1
- package/dist/connector.js.map +1 -1
- package/dist/endpoint-invoke.js +1 -1
- package/dist/endpoint-invoke.js.map +1 -1
- package/dist/endpoint.d.ts +36 -2
- package/dist/endpoint.d.ts.map +1 -1
- package/dist/endpoint.js +74 -5
- package/dist/endpoint.js.map +1 -1
- package/dist/provision.d.ts +23 -0
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +71 -10
- package/dist/provision.js.map +1 -1
- package/dist/remote-manager-authority.d.ts +111 -0
- package/dist/remote-manager-authority.d.ts.map +1 -1
- package/dist/remote-manager-authority.js +175 -1
- package/dist/remote-manager-authority.js.map +1 -1
- package/dist/resolve.d.ts +13 -0
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +16 -0
- package/dist/resolve.js.map +1 -1
- package/dist/schema-profile.d.ts.map +1 -1
- package/dist/schema-profile.js +87 -8
- package/dist/schema-profile.js.map +1 -1
- package/dist/streams.d.ts +21 -21
- package/dist/streams.d.ts.map +1 -1
- package/dist/streams.js +52 -29
- package/dist/streams.js.map +1 -1
- package/dist/subjects.d.ts +18 -0
- package/dist/subjects.d.ts.map +1 -1
- package/dist/subjects.js +21 -0
- package/dist/subjects.js.map +1 -1
- package/package.json +1 -1
package/dist/endpoint.js
CHANGED
|
@@ -48,6 +48,27 @@ export const MULTI_FILTER_BATCH = 1_000;
|
|
|
48
48
|
* issuing one read per channel: a space large enough to need batches must not get the fan-out back
|
|
49
49
|
* under another name. */
|
|
50
50
|
export const MULTI_FILTER_READ_CONCURRENCY = 4;
|
|
51
|
+
/** A presence bucket that has refused consecutive writes for at least one full presence TTL.
|
|
52
|
+
* The condition is non-transient: ordinary heartbeat retry has already failed for the whole
|
|
53
|
+
* liveness window, so callers should report the roster as last-known until a write succeeds. */
|
|
54
|
+
export class PresenceWriteStuckError extends Error {
|
|
55
|
+
bucket;
|
|
56
|
+
since;
|
|
57
|
+
consecutiveFailures;
|
|
58
|
+
ttlMs;
|
|
59
|
+
lastError;
|
|
60
|
+
code = "presence-write-stuck";
|
|
61
|
+
transient = false;
|
|
62
|
+
constructor(bucket, since, consecutiveFailures, ttlMs, lastError) {
|
|
63
|
+
super(`presence writes to bucket ${JSON.stringify(bucket)} have failed ${consecutiveFailures} consecutive times for at least one TTL (${ttlMs}ms); the presence view is not live until a write succeeds or the broker store is repaired${lastError ? ` (last refusal: ${lastError})` : ""}`);
|
|
64
|
+
this.bucket = bucket;
|
|
65
|
+
this.since = since;
|
|
66
|
+
this.consecutiveFailures = consecutiveFailures;
|
|
67
|
+
this.ttlMs = ttlMs;
|
|
68
|
+
this.lastError = lastError;
|
|
69
|
+
this.name = "PresenceWriteStuckError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
51
72
|
/**
|
|
52
73
|
* Events: "message" (CotalMessage), "presence" (PresenceEvent), "roster" (Presence[]), "error" (Error),
|
|
53
74
|
* "connection" ({ connected: boolean }) — true on every successful (re)bind (initial start, manual
|
|
@@ -225,6 +246,10 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
225
246
|
presenceWriteFailingSince;
|
|
226
247
|
/** #1356: the broker's last refusal message, kept alongside the start time for diagnosis. */
|
|
227
248
|
lastPresenceWriteError;
|
|
249
|
+
/** Consecutive latest-evidence refusals on the current connection epoch. A success resets it. */
|
|
250
|
+
presenceWriteFailures = 0;
|
|
251
|
+
/** One named non-transient warning per failed run. A success re-arms it. */
|
|
252
|
+
presenceWriteEscalated = false;
|
|
228
253
|
/** #1461: monotonic per-put generation on the presence path, so a settle can tell whether it is
|
|
229
254
|
* the latest evidence on its epoch. {@link publishPresence} snapshots it at put start and every
|
|
230
255
|
* settle records its generation here: a settle is the latest evidence only while no put that
|
|
@@ -1949,11 +1974,18 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
1949
1974
|
const caller = this.serviceCaller();
|
|
1950
1975
|
const resolve = async (signal = opts.signal) => {
|
|
1951
1976
|
signal?.throwIfAborted();
|
|
1952
|
-
const cached = this.resolvedServices.get(endpoint);
|
|
1977
|
+
const cached = opts.instanceId === undefined ? this.resolvedServices.get(endpoint) : undefined;
|
|
1953
1978
|
if (cached)
|
|
1954
1979
|
return cached;
|
|
1955
|
-
const svc = await resolveService(nc, this.space, endpoint, caller, {
|
|
1956
|
-
|
|
1980
|
+
const svc = await resolveService(nc, this.space, endpoint, caller, {
|
|
1981
|
+
deadlineMs: opts.deadlineMs ?? 10_000,
|
|
1982
|
+
signal,
|
|
1983
|
+
...(opts.instanceId !== undefined ? { instanceId: opts.instanceId } : {}),
|
|
1984
|
+
});
|
|
1985
|
+
// A pinned resolve never enters the endpoint-only class cache. Otherwise a later unpinned call
|
|
1986
|
+
// could silently inherit that instance, or a different pin could reuse the wrong manager.
|
|
1987
|
+
if (opts.instanceId === undefined)
|
|
1988
|
+
this.resolvedServices.set(endpoint, svc);
|
|
1957
1989
|
return svc;
|
|
1958
1990
|
};
|
|
1959
1991
|
const invokeOpts = { ...(opts.target ? { target: opts.target } : {}), ...(opts.deadlineMs !== undefined ? { deadlineMs: opts.deadlineMs } : {}) };
|
|
@@ -2591,7 +2623,7 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
2591
2623
|
}
|
|
2592
2624
|
else {
|
|
2593
2625
|
const closedEpoch = err.name === "ClosedConnectionError" || /^closed connection$/i.test(err.message);
|
|
2594
|
-
const timeout =
|
|
2626
|
+
const timeout = isTimeoutError(err);
|
|
2595
2627
|
const dyingEpochTimeout = timeout && (this.reconnecting || !this.nc || this.nc.isClosed());
|
|
2596
2628
|
// Cleanup of an ordered consumer: a delete timeout means the broker did not answer in time,
|
|
2597
2629
|
// not that the endpoint is unusable. The broker reaps an idle/ephemeral consumer anyway.
|
|
@@ -4673,15 +4705,20 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
4673
4705
|
return;
|
|
4674
4706
|
if (!this.ownLifecycleUid)
|
|
4675
4707
|
return; // no lifecycle uid — never provisioned for Plane-3 (its durable is lifecycle-keyed)
|
|
4708
|
+
const durable = dlvDurable(this.owner, this.actor, this.ownLifecycleUid);
|
|
4676
4709
|
let consumer;
|
|
4677
4710
|
try {
|
|
4678
|
-
consumer = await this.js.consumers.get(dlvStream(this.space),
|
|
4711
|
+
consumer = await this.js.consumers.get(dlvStream(this.space), durable);
|
|
4679
4712
|
}
|
|
4680
4713
|
catch (e) {
|
|
4681
4714
|
if (isJetStreamMissing(e, JetStreamApiCodes.ConsumerNotFound))
|
|
4682
4715
|
return;
|
|
4683
4716
|
throw e; // a denied bind is not proof Plane-3 is absent
|
|
4684
4717
|
}
|
|
4718
|
+
const lease = await this.readDeliveryLease(0);
|
|
4719
|
+
const liveDeliveryPlane = lease?.ready === true;
|
|
4720
|
+
if (!liveDeliveryPlane)
|
|
4721
|
+
this.emit("warning", new Error(`delivery durable "${durable}" for space "${this.space}" bound while the plane this connection reaches has no ready delivery lease, so the durable delivers nothing until a delivery daemon serves this plane. If a daemon serves this space on another plane, reconnect against it, which re-binds the durable there.`));
|
|
4685
4722
|
const msgs = await consumer.consume();
|
|
4686
4723
|
this.streamMsgs.push(msgs);
|
|
4687
4724
|
void (async () => {
|
|
@@ -5433,6 +5470,8 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
5433
5470
|
if (epoch === this.presenceEpoch && !this.stopped && !superseded) {
|
|
5434
5471
|
this.presenceWriteFailingSince ??= Date.now();
|
|
5435
5472
|
this.lastPresenceWriteError = e?.message ?? String(e);
|
|
5473
|
+
this.presenceWriteFailures++;
|
|
5474
|
+
this.escalatePresenceWriteFailureIfStuck();
|
|
5436
5475
|
}
|
|
5437
5476
|
throw e;
|
|
5438
5477
|
}
|
|
@@ -5471,6 +5510,21 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
5471
5510
|
clearPresenceWriteFailure() {
|
|
5472
5511
|
this.presenceWriteFailingSince = undefined;
|
|
5473
5512
|
this.lastPresenceWriteError = undefined;
|
|
5513
|
+
this.presenceWriteFailures = 0;
|
|
5514
|
+
this.presenceWriteEscalated = false;
|
|
5515
|
+
}
|
|
5516
|
+
/** A heartbeat retry remains recoverable inside one TTL. Once two or more consecutive writes have
|
|
5517
|
+
* failed across the full window, ordinary retry has exhausted the roster's liveness budget: raise
|
|
5518
|
+
* one named, non-transient warning. Later failures keep the duration/count current without flooding
|
|
5519
|
+
* the operator log; the next successful write clears and re-arms the condition. */
|
|
5520
|
+
escalatePresenceWriteFailureIfStuck() {
|
|
5521
|
+
if (this.presenceWriteEscalated ||
|
|
5522
|
+
this.presenceWriteFailingSince === undefined ||
|
|
5523
|
+
this.presenceWriteFailures < 2 ||
|
|
5524
|
+
Date.now() - this.presenceWriteFailingSince < this.ttlMs)
|
|
5525
|
+
return;
|
|
5526
|
+
this.presenceWriteEscalated = true;
|
|
5527
|
+
this.emitRecoverable(new PresenceWriteStuckError(presenceBucket(this.space), this.presenceWriteFailingSince, this.presenceWriteFailures, this.ttlMs, this.lastPresenceWriteError));
|
|
5474
5528
|
}
|
|
5475
5529
|
/** #1356: the presence bucket has been refusing writes since this time, or `undefined` when the
|
|
5476
5530
|
* last publish succeeded. Cleared by a successful write that is the latest evidence on its
|
|
@@ -5488,6 +5542,8 @@ export class CotalEndpoint extends EventEmitter {
|
|
|
5488
5542
|
forMs: Date.now() - this.presenceWriteFailingSince,
|
|
5489
5543
|
error: this.lastPresenceWriteError,
|
|
5490
5544
|
bucket: presenceBucket(this.space),
|
|
5545
|
+
consecutiveFailures: this.presenceWriteFailures,
|
|
5546
|
+
stuck: this.presenceWriteEscalated,
|
|
5491
5547
|
};
|
|
5492
5548
|
}
|
|
5493
5549
|
/** Bind a presence watch on the current connection. Resolves true when the watch was
|
|
@@ -6447,6 +6503,14 @@ export async function isReachable(servers = DEFAULT_SERVER, opts = {}) {
|
|
|
6447
6503
|
return e instanceof AuthorizationError || e instanceof UserAuthenticationExpiredError;
|
|
6448
6504
|
}
|
|
6449
6505
|
}
|
|
6506
|
+
/** True when `err` is a dial/consumer-op timeout rather than a real refusal — the one shared test
|
|
6507
|
+
* for "the operation ran out of its own budget", used both by {@link classifyProbeFailure} (#851:
|
|
6508
|
+
* a probe timeout must never collapse into `unreachable`, which a TLS-required target then
|
|
6509
|
+
* misreads as a trust failure) and by {@link Endpoint#disarmMembershipWatch}'s consumer-delete
|
|
6510
|
+
* cleanup, which predates it. */
|
|
6511
|
+
function isTimeoutError(err) {
|
|
6512
|
+
return err instanceof Error && (err.name === "TimeoutError" || /timeout/i.test(err.message));
|
|
6513
|
+
}
|
|
6450
6514
|
/** Like {@link isReachable}, but distinguishes "up but won't take these creds" from "nothing there".
|
|
6451
6515
|
* `spawn` needs the difference: auth-required → name the trust dir + next step; unreachable → the
|
|
6452
6516
|
* mesh is down (prune the stale entry, tell the user to `cotal up`). Pass `creds` to confirm a
|
|
@@ -6504,6 +6568,11 @@ function classifyProbeFailure(e, opts) {
|
|
|
6504
6568
|
// The broker answered but rejected these creds (so it IS up) — auth-required, not stale-auth.
|
|
6505
6569
|
if (e instanceof AuthorizationError)
|
|
6506
6570
|
return { ok: false, reason: "auth-required" };
|
|
6571
|
+
// A dial that ran out of its own budget is neither a refusal nor a dead broker — it is latency.
|
|
6572
|
+
// `e` is undefined when the tcpDialable gate refused before any connect() attempt; that path has
|
|
6573
|
+
// no timeout to inspect and must stay `unreachable` (nothing answered at all).
|
|
6574
|
+
if (e !== undefined && isTimeoutError(e))
|
|
6575
|
+
return { ok: false, reason: "timeout" };
|
|
6507
6576
|
return { ok: false, reason: "unreachable" };
|
|
6508
6577
|
}
|
|
6509
6578
|
//# sourceMappingURL=endpoint.js.map
|