@serve.zone/interfaces 32.56.0 → 32.58.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/changelog.md +54 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/appstore/catalog.d.ts +1 -1
- package/dist_ts/appstore/catalog.js +2 -1
- package/dist_ts/appstore/types.d.ts +43 -0
- package/dist_ts/appstore/types.js +15 -1
- package/dist_ts/appstore/versionconfig.d.ts +12 -3
- package/dist_ts/appstore/versionconfig.js +131 -17
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/data/runtimesessionrefusal.d.ts +97 -0
- package/dist_ts/data/runtimesessionrefusal.js +127 -0
- package/package.json +3 -3
- package/readme.md +82 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/catalog.ts +1 -0
- package/ts/appstore/types.ts +46 -0
- package/ts/appstore/versionconfig.ts +144 -13
- package/ts/data/index.ts +1 -0
- package/ts/data/runtimesessionrefusal.ts +153 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readExactDataRecord } from '../private/dataproperties.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The refusal a controller states when it does not take a node's runtime session request.
|
|
5
|
+
*
|
|
6
|
+
* It travels as `TypedResponseError.errorData` on `registerPalletRuntimeSession` and on every
|
|
7
|
+
* session-scoped read and report a node sends its controller. `code` is a frozen wire value.
|
|
8
|
+
* `momentaryReasons` are the controller's own momentary state, not a judgement of the node: a
|
|
9
|
+
* registering node waits and offers the same registration again, keeping everything it runs.
|
|
10
|
+
* Every other reason, and an error that carries no such payload, is a verdict — on a registration
|
|
11
|
+
* it ends the node's session. Momentary against verdict governs only whether a node defers a
|
|
12
|
+
* registration: a node retries its session reads and reports regardless of the reason, so the
|
|
13
|
+
* reasons a report is refused with (`state-conflict`, `*-write-conflict`, a capacity limit such as
|
|
14
|
+
* `log-intake-saturated`) are unchanged by this contract.
|
|
15
|
+
*
|
|
16
|
+
* Who states which reason today, on a registration (Cloudly ≥33.19.1, `isBudgetRefusal`):
|
|
17
|
+
* - Cloudly states `controller-deadline` when the request ran out of
|
|
18
|
+
* `runtimeSessionRequestBounds.controllerBudgetMs`, and `controller-unavailable` when it is
|
|
19
|
+
* stopping. Every other failure, a transient one of its own included, is answered without a
|
|
20
|
+
* payload and so reads as a verdict.
|
|
21
|
+
* - No relay states a refusal of its own yet.
|
|
22
|
+
*
|
|
23
|
+
* The rule producers adopt:
|
|
24
|
+
* - Cloudly states `controller-unavailable` on a registration for every transient failure of its
|
|
25
|
+
* own: its database connection lost or unavailable, a registration write conflict, a race with a
|
|
26
|
+
* credential being applied.
|
|
27
|
+
* - A relay states only `controller-unavailable`, and only when it cannot carry the request or its
|
|
28
|
+
* answer: no Cloudly link, or its own bound (`runtimeSessionRequestBounds`) ran out. Every
|
|
29
|
+
* answer Cloudly gave it forwards untouched; it never states a verdict or `controller-deadline`.
|
|
30
|
+
* - A verdict names the check that refused: a revoked or inactive credential, a deleted, retiring
|
|
31
|
+
* or foreign node, an invalid body. It never carries a momentary reason.
|
|
32
|
+
*
|
|
33
|
+
* The payload is exactly `{ code, reason }`. Pallet 36.3.14 reads the two members and tolerates
|
|
34
|
+
* others, but every reader built on `readRuntimeSessionRefusal` refuses a payload with any other
|
|
35
|
+
* member, and a refused payload reads as no refusal — a momentary refusal would turn into a
|
|
36
|
+
* verdict. A producer therefore never adds a member without gating it on the peer's spoken
|
|
37
|
+
* interfaces version.
|
|
38
|
+
*/
|
|
39
|
+
export const runtimeSessionRefusalContract = Object.freeze({
|
|
40
|
+
code: 'runtime-session-report-refused',
|
|
41
|
+
momentaryReasons: Object.freeze(['controller-deadline', 'controller-unavailable'] as const),
|
|
42
|
+
/**
|
|
43
|
+
* A reason is one or more words of lowercase ASCII letters joined by single hyphens, at most
|
|
44
|
+
* this many characters. It names a check, never a value: no digit, case, dot, colon or other
|
|
45
|
+
* separator can carry an identifier, address, digest, count or credential across.
|
|
46
|
+
*/
|
|
47
|
+
maximumReasonLength: 64,
|
|
48
|
+
} as const);
|
|
49
|
+
|
|
50
|
+
export type TRuntimeSessionMomentaryRefusalReason =
|
|
51
|
+
typeof runtimeSessionRefusalContract.momentaryReasons[number];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `TypedResponseError.errorData` of a refused runtime session request. `reason` is a word naming
|
|
55
|
+
* the check that refused (see `runtimeSessionRefusalContract`), never a value.
|
|
56
|
+
*/
|
|
57
|
+
export interface IRuntimeSessionRefusal {
|
|
58
|
+
code: typeof runtimeSessionRefusalContract.code;
|
|
59
|
+
reason: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const isRefusalReason = (valueArg: unknown): valueArg is string => (
|
|
63
|
+
typeof valueArg === 'string'
|
|
64
|
+
&& valueArg.length <= runtimeSessionRefusalContract.maximumReasonLength
|
|
65
|
+
&& /^[a-z]+(?:-[a-z]+)*$/.test(valueArg)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const reasonRule = 'must be lowercase letter words joined by single hyphens, at most '
|
|
69
|
+
+ `${runtimeSessionRefusalContract.maximumReasonLength} characters`;
|
|
70
|
+
|
|
71
|
+
/** Validates one runtime session refusal and names every reason it is refused. */
|
|
72
|
+
export const validateRuntimeSessionRefusal = (
|
|
73
|
+
valueArg: unknown,
|
|
74
|
+
pathArg = 'runtime session refusal',
|
|
75
|
+
): string[] => {
|
|
76
|
+
const refusal = readExactDataRecord(valueArg, ['code', 'reason']);
|
|
77
|
+
if (refusal === null) {
|
|
78
|
+
return [`${pathArg} must use its exact schema`];
|
|
79
|
+
}
|
|
80
|
+
const errors: string[] = [];
|
|
81
|
+
if (refusal.code !== runtimeSessionRefusalContract.code) {
|
|
82
|
+
errors.push(`${pathArg} code must be ${runtimeSessionRefusalContract.code}`);
|
|
83
|
+
}
|
|
84
|
+
if (!isRefusalReason(refusal.reason)) {
|
|
85
|
+
errors.push(`${pathArg} reason ${reasonRule}`);
|
|
86
|
+
}
|
|
87
|
+
return errors;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** The payload a controller or relay answers a refused request with; throws on a non-word reason. */
|
|
91
|
+
export const buildRuntimeSessionRefusal = (reasonArg: string): IRuntimeSessionRefusal => {
|
|
92
|
+
if (!isRefusalReason(reasonArg)) {
|
|
93
|
+
throw new Error(`runtime session refusal reason ${reasonRule}`);
|
|
94
|
+
}
|
|
95
|
+
return Object.freeze({ code: runtimeSessionRefusalContract.code, reason: reasonArg });
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The refusal inside an error payload, or `null` when the value is not exactly one.
|
|
100
|
+
*
|
|
101
|
+
* It reads a value that survived serialization, the way `protocol.readProtocolRefusal` does: own
|
|
102
|
+
* data properties only, exactly `code` and `reason`, nothing that runs.
|
|
103
|
+
*/
|
|
104
|
+
export const readRuntimeSessionRefusal = (valueArg: unknown): IRuntimeSessionRefusal | null => {
|
|
105
|
+
const refusal = readExactDataRecord(valueArg, ['code', 'reason']);
|
|
106
|
+
if (refusal === null || validateRuntimeSessionRefusal(refusal).length > 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze({ code: runtimeSessionRefusalContract.code, reason: refusal.reason as string });
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The momentary reason an error payload states, or `null` when it is a verdict: any other reason,
|
|
114
|
+
* a payload that is not exactly a refusal, or no payload at all.
|
|
115
|
+
*/
|
|
116
|
+
export const readMomentaryRuntimeSessionRefusal = (
|
|
117
|
+
valueArg: unknown,
|
|
118
|
+
): TRuntimeSessionMomentaryRefusalReason | null => {
|
|
119
|
+
const refusal = readRuntimeSessionRefusal(valueArg);
|
|
120
|
+
if (refusal === null) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const momentary = runtimeSessionRefusalContract.momentaryReasons
|
|
124
|
+
.find((reasonArg) => reasonArg === refusal.reason);
|
|
125
|
+
return momentary ?? null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* How long each hop may hold a node's runtime session request, so the hop nearest the decision
|
|
130
|
+
* answers first and the node always reads a stated refusal rather than its own timeout.
|
|
131
|
+
*/
|
|
132
|
+
export const runtimeSessionRequestBounds = Object.freeze({
|
|
133
|
+
/**
|
|
134
|
+
* Cloudly's budget for every node request, registration included. When it runs out Cloudly
|
|
135
|
+
* answers `controller-deadline`.
|
|
136
|
+
*/
|
|
137
|
+
controllerBudgetMs: 6_000,
|
|
138
|
+
/**
|
|
139
|
+
* A relay's bound on a registration, from its arrival at the relay to the answer, including any
|
|
140
|
+
* wait for the relay's own Cloudly link. It exceeds `controllerBudgetMs` by more than a second so
|
|
141
|
+
* Cloudly's own `controller-deadline` reaches the node through the relay, and stays below
|
|
142
|
+
* TypedSocket's 10 s connection-restore deadline (`TYPEDSOCKET_MAX_CONNECTION_RESTORE_TIMEOUT_MS`,
|
|
143
|
+
* which a client can only lower) so the node reads an answer before its restore hook times out.
|
|
144
|
+
* When it runs out the relay answers `controller-unavailable`.
|
|
145
|
+
*/
|
|
146
|
+
relayRegistrationBudgetMs: 9_000,
|
|
147
|
+
/**
|
|
148
|
+
* A relay's bound on forwarding a session read or report over its established Cloudly link.
|
|
149
|
+
* It exceeds `controllerBudgetMs` so Cloudly's answer arrives first; when it runs out the relay
|
|
150
|
+
* answers `controller-unavailable`.
|
|
151
|
+
*/
|
|
152
|
+
relaySessionForwardMs: 8_000,
|
|
153
|
+
} as const);
|