@astrale-os/kernel-client 0.6.0-beta.82 → 0.6.0-beta.84
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/client/admission.js +1 -9
- package/dist/client/client.js +3 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/routing-control.d.ts +11 -3
- package/dist/client/routing-control.js +18 -3
- package/dist/errors/stale-route-error.d.ts +14 -3
- package/dist/errors/stale-route-error.js +15 -3
- package/dist/errors/transport-error.d.ts +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/introspection.js +10 -6
- package/dist/schema/schema.d.ts +3 -0
- package/dist/schema/schema.js +13 -2
- package/dist/session/bound.js +10 -1
- package/dist/session/content.js +4 -27
- package/dist/session/context/index.d.ts +1 -1
- package/dist/session/context/index.js +1 -1
- package/dist/session/context/limits.d.ts +1 -0
- package/dist/session/context/limits.js +1 -0
- package/dist/session/discovery/bundle.js +1 -1
- package/dist/session/discovery/discovery.d.ts +12 -4
- package/dist/session/discovery/discovery.js +28 -19
- package/dist/session/discovery/index.d.ts +8 -1
- package/dist/session/discovery/index.js +5 -8
- package/dist/session/discovery/legacy/index.d.ts +38 -0
- package/dist/session/discovery/legacy/index.js +38 -0
- package/dist/session/discovery/legacy/intrinsic.d.ts +33 -0
- package/dist/session/discovery/legacy/intrinsic.js +67 -0
- package/dist/session/discovery/legacy/projection.d.ts +8 -0
- package/dist/session/discovery/legacy/projection.js +17 -0
- package/dist/session/discovery/{publication.d.ts → legacy/publication.d.ts} +8 -5
- package/dist/session/discovery/{publication.js → legacy/publication.js} +13 -60
- package/dist/session/discovery/release.d.ts +29 -0
- package/dist/session/discovery/release.js +81 -0
- package/dist/session/discovery/response.d.ts +3 -0
- package/dist/session/discovery/response.js +59 -0
- package/dist/session/discovery/snapshot.d.ts +9 -2
- package/dist/session/discovery/snapshot.js +2 -2
- package/dist/session/discovery/transport.d.ts +1 -3
- package/dist/session/exchange/options.d.ts +13 -10
- package/dist/session/exchange/options.js +6 -5
- package/dist/session/exchange/presentation.d.ts +3 -2
- package/dist/session/index.d.ts +1 -1
- package/dist/session/reference.d.ts +3 -6
- package/dist/session/reference.js +0 -4
- package/dist/session/routing/dispatch.js +36 -37
- package/dist/session/session.d.ts +18 -3
- package/dist/session/session.js +21 -10
- package/dist/session/session.typecheck.js +14 -3
- package/dist/transport/auto/transport.d.ts +0 -1
- package/dist/transport/auto/transport.js +0 -2
- package/dist/transport/http/response.d.ts +1 -1
- package/dist/transport/http/response.js +2 -6
- package/dist/transport/http/transport.d.ts +5 -3
- package/dist/transport/http/transport.js +40 -19
- package/dist/transport/index.d.ts +1 -1
- package/dist/transport/transport.d.ts +10 -2
- package/dist/transport/websocket/transport.d.ts +0 -3
- package/dist/transport/websocket/transport.js +1 -16
- package/package.json +6 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Path as PathValue, PathLike } from '@astrale-os/kernel-core/path';
|
|
2
2
|
import type { CallableInputOf, CallableInputModeOf, CallableOutputOf, CallableResultOf, Domain, ResolvedFunction, ExecutableMethod } from '@astrale-os/kernel-dsl/v1';
|
|
3
|
-
import type { MethodAddress
|
|
3
|
+
import type { MethodAddress } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
4
4
|
import type { MeaningFingerprint } from '@astrale-os/kernel-dsl/v1/schema/fingerprint';
|
|
5
5
|
import { type Key as KeyType } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
6
6
|
import type { Binary, Input } from '../client/index.js';
|
|
@@ -10,8 +10,6 @@ declare const REFERENCE_INPUT_MODE: unique symbol;
|
|
|
10
10
|
declare const REFERENCE_OUTPUT: unique symbol;
|
|
11
11
|
export interface CallableReference<InputValue = Input, Output = unknown, InputMode extends 'value' | 'binary' = 'value' | 'binary'> {
|
|
12
12
|
readonly origin: KeyType.Origin;
|
|
13
|
-
/** @deprecated Informational only; dispatch pins `fingerprint` and revision pinning will be removed. */
|
|
14
|
-
readonly revision?: Revision;
|
|
15
13
|
/** Meaning fingerprint of the callable; a generated reference always carries one. */
|
|
16
14
|
readonly fingerprint?: MeaningFingerprint;
|
|
17
15
|
readonly callable: KeyType.Callable;
|
|
@@ -24,7 +22,6 @@ export interface CallableReference<InputValue = Input, Output = unknown, InputMo
|
|
|
24
22
|
export interface InstanceCallableReference<InputValue = Input, Output = unknown> {
|
|
25
23
|
(receiver: PathLike): CallableReference<InputValue, Output>;
|
|
26
24
|
readonly origin: KeyType.Origin;
|
|
27
|
-
readonly revision?: Revision;
|
|
28
25
|
readonly fingerprint?: MeaningFingerprint;
|
|
29
26
|
readonly callable: KeyType.Callable;
|
|
30
27
|
readonly outputMode: 'value' | 'stream' | 'binary';
|
|
@@ -42,9 +39,9 @@ type ReferenceOutput<Callable> = Callable extends {
|
|
|
42
39
|
type ExecutableStaticMethod = ExecutableMethod<unknown, unknown, true>;
|
|
43
40
|
type ExecutableInstanceMethod = ExecutableMethod<unknown, unknown, false>;
|
|
44
41
|
/** Project one exact resolved Function or static Method into a typed Client reference. */
|
|
45
|
-
export declare function reference<const Callable extends ResolvedFunction | ExecutableStaticMethod>(domain: Pick<Domain, 'origin'
|
|
42
|
+
export declare function reference<const Callable extends ResolvedFunction | ExecutableStaticMethod>(domain: Pick<Domain, 'origin'>, callable: Callable): CallableReference<ReferenceInput<Callable>, ReferenceOutput<Callable>, CallableInputModeOf<Callable>>;
|
|
46
43
|
/** Project one exact resolved instance Method into a receiver-bound typed Client reference. */
|
|
47
|
-
export declare function reference<const Callable extends ExecutableInstanceMethod>(domain: Pick<Domain, 'origin'
|
|
44
|
+
export declare function reference<const Callable extends ExecutableInstanceMethod>(domain: Pick<Domain, 'origin'>, callable: Callable): InstanceCallableReference<ReferenceInput<Callable>, ReferenceOutput<Callable>>;
|
|
48
45
|
/**
|
|
49
46
|
* Reference a Kernel instance Method by its Core coordinate, without a meaning pin, as the Session
|
|
50
47
|
* addresses every Kernel syscall. Loading the Session therefore never resolves the Kernel Schema.
|
|
@@ -9,7 +9,6 @@ export function reference(domain, callable) {
|
|
|
9
9
|
invalid();
|
|
10
10
|
const common = Object.freeze({
|
|
11
11
|
origin: domain.origin,
|
|
12
|
-
revision: domain.revision,
|
|
13
12
|
fingerprint: callableFingerprint(callable),
|
|
14
13
|
callable: callable.key,
|
|
15
14
|
outputMode: callable.output.mode,
|
|
@@ -45,8 +44,6 @@ export function admitReference(input) {
|
|
|
45
44
|
invalid();
|
|
46
45
|
if (typeof input.origin !== 'string' ||
|
|
47
46
|
!patterns.origin.test(input.origin) ||
|
|
48
|
-
(input.revision !== undefined &&
|
|
49
|
-
(typeof input.revision !== 'string' || !patterns.revision.test(input.revision))) ||
|
|
50
47
|
typeof input.callable !== 'string' ||
|
|
51
48
|
!Key.is(input.callable) ||
|
|
52
49
|
Key.origin(input.callable) !== input.origin ||
|
|
@@ -59,7 +56,6 @@ export function admitReference(input) {
|
|
|
59
56
|
invalid();
|
|
60
57
|
return Object.freeze({
|
|
61
58
|
origin: input.origin,
|
|
62
|
-
...(input.revision === undefined ? {} : { revision: input.revision }),
|
|
63
59
|
...(input.fingerprint === undefined ? {} : { fingerprint: input.fingerprint }),
|
|
64
60
|
callable: input.callable,
|
|
65
61
|
target,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Key } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
2
|
import * as invocationProtocol from '@astrale-os/kernel-protocol/invocation';
|
|
3
|
-
import { acceptCall, Client, dispatchTrace, isAdmittedMissError, isAdmittedStaleRouteError, } from '../../client/index.js';
|
|
3
|
+
import { acceptCall, Client, dispatchTrace, isAdmittedDeploymentRetiredError, isAdmittedMissError, isAdmittedStaleRouteError, } from '../../client/index.js';
|
|
4
4
|
import { ClientError, ResponseError } from '../../errors/index.js';
|
|
5
5
|
import { runSessionOperation, remainingSessionTime, sessionTransportOptions, allowEndpoint, } from '../context/index.js';
|
|
6
6
|
import { resolveAuth } from './auth.js';
|
|
@@ -86,23 +86,23 @@ export class SessionDispatcher {
|
|
|
86
86
|
return await this.dispatchDestination(learned, state.call, options, operation, state.source, trace, streaming);
|
|
87
87
|
}
|
|
88
88
|
catch (cause) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
const control = routingControl(cause);
|
|
90
|
+
if (control === undefined) {
|
|
91
|
+
if (isRouteCompatibilityConflict(cause))
|
|
92
|
+
this.state.cache.delete(state.cacheKey);
|
|
93
93
|
throw cause;
|
|
94
94
|
}
|
|
95
|
-
if (!stale && !miss)
|
|
96
|
-
throw cause;
|
|
97
95
|
this.state.cache.delete(state.cacheKey);
|
|
98
96
|
if (!operation.claimRecovery())
|
|
99
97
|
throw cause;
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
// A stale or retired route names the release the destination no longer serves; a miss
|
|
99
|
+
// names a Path, so the source receives no digest for it.
|
|
100
|
+
const routing = control === 'miss'
|
|
101
|
+
? undefined
|
|
102
|
+
: Object.freeze({
|
|
102
103
|
kind: 'refresh',
|
|
103
|
-
stale: learned.route.via
|
|
104
|
-
})
|
|
105
|
-
: undefined;
|
|
104
|
+
stale: invocationProtocol.routedDigest(learned.route.via),
|
|
105
|
+
});
|
|
106
106
|
const refreshed = await this.dispatchSource(state, routing, options, operation, streaming);
|
|
107
107
|
return this.followSource(refreshed, state, options, operation, streaming);
|
|
108
108
|
}
|
|
@@ -129,30 +129,16 @@ export class SessionDispatcher {
|
|
|
129
129
|
...sessionTransportOptions(operation),
|
|
130
130
|
routing: learned.route.via,
|
|
131
131
|
};
|
|
132
|
-
// The source has admitted this fingerprint against the exact Publication carried by Via.
|
|
133
|
-
// Pin that revision at the destination so existing Workers can execute the same admitted
|
|
134
|
-
// callable without understanding the newer fingerprint Target representation. The cache
|
|
135
|
-
// remains partitioned by the original fingerprint, and stale recovery re-admits it at source.
|
|
136
|
-
const destinationCall = typeof call.target !== 'string' && call.target.fingerprint !== undefined
|
|
137
|
-
? {
|
|
138
|
-
...call,
|
|
139
|
-
target: invocationProtocol.Target({
|
|
140
|
-
callable: call.target.callable,
|
|
141
|
-
revision: learned.route.via.publication.revision,
|
|
142
|
-
...(call.target.instance === undefined ? {} : { instance: call.target.instance }),
|
|
143
|
-
}),
|
|
144
|
-
}
|
|
145
|
-
: call;
|
|
146
132
|
const result = await runSessionOperation(operation, () => streaming?.body !== undefined
|
|
147
133
|
? trace === undefined
|
|
148
|
-
? authenticated(client, learned.credential).dispatch(
|
|
134
|
+
? authenticated(client, learned.credential).dispatch(call, {
|
|
149
135
|
...binaryRequestOptions,
|
|
150
136
|
body: streaming.body,
|
|
151
137
|
})
|
|
152
|
-
: dispatchTrace(client, learned.credential,
|
|
138
|
+
: dispatchTrace(client, learned.credential, call, { ...binaryRequestOptions, body: streaming.body }, trace)
|
|
153
139
|
: trace === undefined
|
|
154
|
-
? authenticated(client, learned.credential).dispatch(
|
|
155
|
-
: dispatchTrace(client, learned.credential,
|
|
140
|
+
? authenticated(client, learned.credential).dispatch(call, requestOptions)
|
|
141
|
+
: dispatchTrace(client, learned.credential, call, requestOptions, trace));
|
|
156
142
|
if (result.kind === 'redirect') {
|
|
157
143
|
throw new ClientError('A destination redirect is invalid; only the source Kernel may redirect.');
|
|
158
144
|
}
|
|
@@ -177,6 +163,20 @@ function requireCachedRoute(learned, source, target, envelopeTransport, websocke
|
|
|
177
163
|
requireTargetPublication(learned.route, target);
|
|
178
164
|
selectEnvelopeTarget(learned.route.endpoint, envelopeTransport, websocketAvailable, policy);
|
|
179
165
|
}
|
|
166
|
+
/** The privately witnessed pre-handler control a destination failure carries, if any. */
|
|
167
|
+
function routingControl(input) {
|
|
168
|
+
if (isAdmittedStaleRouteError(input))
|
|
169
|
+
return 'stale';
|
|
170
|
+
if (isAdmittedMissError(input))
|
|
171
|
+
return 'miss';
|
|
172
|
+
if (isAdmittedDeploymentRetiredError(input))
|
|
173
|
+
return 'retired';
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A failure that proves the warm route can no longer serve this call without witnessing a
|
|
178
|
+
* pre-handler control: the route is evicted for later calls, but the invocation is never replayed.
|
|
179
|
+
*/
|
|
180
180
|
function isRouteCompatibilityConflict(input) {
|
|
181
181
|
if (!(input instanceof ResponseError))
|
|
182
182
|
return false;
|
|
@@ -185,17 +185,16 @@ function isRouteCompatibilityConflict(input) {
|
|
|
185
185
|
message: input.message,
|
|
186
186
|
...(input.data === undefined ? {} : { data: input.data }),
|
|
187
187
|
});
|
|
188
|
-
return (failure?.data.code === invocationProtocol.
|
|
189
|
-
failure?.data.code === invocationProtocol.
|
|
190
|
-
failure?.data.code === invocationProtocol.
|
|
188
|
+
return (failure?.data.code === invocationProtocol.CALLABLE_FINGERPRINT_MISMATCH ||
|
|
189
|
+
failure?.data.code === invocationProtocol.SERVICE_TRUST_REQUIRED ||
|
|
190
|
+
failure?.data.code === invocationProtocol.DEPLOYMENT_RETIRED);
|
|
191
191
|
}
|
|
192
192
|
function requireTargetPublication(route, target) {
|
|
193
193
|
if (typeof target === 'string')
|
|
194
194
|
return;
|
|
195
|
-
const actual = route.via.publication;
|
|
196
|
-
if (actual.origin !== Key.origin(target.callable)
|
|
197
|
-
(
|
|
198
|
-
throw new ClientError('Resolved route does not match the expected Schema revision.');
|
|
195
|
+
const actual = 'release' in route.via ? route.via.release : route.via.publication;
|
|
196
|
+
if (actual.origin !== Key.origin(target.callable)) {
|
|
197
|
+
throw new ClientError('Resolved route does not match the Target origin.');
|
|
199
198
|
}
|
|
200
199
|
}
|
|
201
200
|
export function requireResultKind(expected, result) {
|
|
@@ -13,7 +13,7 @@ import type { WebSocketConstructor } from '../transport/websocket/index.js';
|
|
|
13
13
|
import type { ContentApi } from './content.js';
|
|
14
14
|
import type { SessionControlOptions, SessionPolicy } from './context/index.js';
|
|
15
15
|
import type { SessionCredential } from './credential/index.js';
|
|
16
|
-
import type { SessionSnapshot } from './discovery/index.js';
|
|
16
|
+
import type { IntrinsicRelease, SessionSnapshot } from './discovery/index.js';
|
|
17
17
|
import type { SessionExchangeOptions, SessionExchangeRequest } from './exchange/index.js';
|
|
18
18
|
import type { CallableReference, InputModeOfReference, InputOfReference, OutputOfReference } from './reference.js';
|
|
19
19
|
import type { SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, SessionRequestOptionsWithBody } from './routing/index.js';
|
|
@@ -31,7 +31,6 @@ export interface ClientSessionOptions {
|
|
|
31
31
|
* caller, so the Domain is the principal and the caller is `auth`. Kernel and local callables keep
|
|
32
32
|
* the `auth` credential. `connect` defaults to `auto` when `auth` is present; `new ClientSession`
|
|
33
33
|
* keeps source authority when this option is omitted. `{ mode: 'source' }` keeps it for every Call.
|
|
34
|
-
* Legacy `{}` and `{ ttlSeconds }` options remain equivalent to `{ mode: 'auto' }`.
|
|
35
34
|
*/
|
|
36
35
|
readonly exchange?: SessionExchangeOptions;
|
|
37
36
|
/** Optional caller-owned persistence for bounded confidential route artifacts. */
|
|
@@ -39,6 +38,12 @@ export interface ClientSessionOptions {
|
|
|
39
38
|
readonly policy: SessionPolicy;
|
|
40
39
|
readonly envelopeTransport?: SessionEnvelopeTransport;
|
|
41
40
|
readonly onWarning?: (warning: ClientWarning) => void;
|
|
41
|
+
/** Bounds the source Kernel's own `release.json`. */
|
|
42
|
+
readonly maximumReleaseBytes?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Bounds every Publication the Session reads: the version 3 `domain.json` a Host that predates
|
|
45
|
+
* `release.json` serves, and the Publications Schema results carry.
|
|
46
|
+
*/
|
|
42
47
|
readonly maximumPublicationBytes?: number;
|
|
43
48
|
readonly maximumBundleBytes?: number;
|
|
44
49
|
readonly maximumResponseBytes?: number;
|
|
@@ -75,7 +80,17 @@ export declare class ClientSession {
|
|
|
75
80
|
* `createSessionCredentialProvider`, for as long as it covers the calls it carries.
|
|
76
81
|
*/
|
|
77
82
|
exchange(domain: string, options?: SessionExchangeRequest): Promise<SessionCredential>;
|
|
78
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Revalidate the source Kernel's own release (`release.json`) without acquiring its bundle or
|
|
85
|
+
* Views; a Host that predates intrinsic `release.json` yields its version 3 Publication instead.
|
|
86
|
+
*/
|
|
87
|
+
release(options?: SessionControlOptions): Promise<IntrinsicRelease>;
|
|
88
|
+
/**
|
|
89
|
+
* Revalidate the intrinsic release and return its v3 document: the Host's own version 3
|
|
90
|
+
* Publication, or the frozen v3 projection of its `DomainRelease`.
|
|
91
|
+
*
|
|
92
|
+
* @deprecated Use `release()`. Deleted when no supported Host predates intrinsic `release.json`.
|
|
93
|
+
*/
|
|
79
94
|
publication(options?: SessionControlOptions): Promise<publication.Publication>;
|
|
80
95
|
snapshot(options?: SessionControlOptions): Promise<SessionSnapshot>;
|
|
81
96
|
viewsFor(target: PathLike, options?: SessionControlOptions): Promise<ViewCatalog>;
|
package/dist/session/session.js
CHANGED
|
@@ -12,7 +12,7 @@ import { HttpTransport } from '../transport/http/index.js';
|
|
|
12
12
|
import { WebSocketTransport } from '../transport/websocket/index.js';
|
|
13
13
|
import { admitFunction } from './admission.js';
|
|
14
14
|
import { createContentApi } from './content.js';
|
|
15
|
-
import { admitControlOptions, admitPolicy, createSessionOperation, DEFAULT_MAXIMUM_BUNDLE_BYTES, DEFAULT_MAXIMUM_PUBLICATION_BYTES, DEFAULT_MAXIMUM_RESPONSE_BYTES, DEFAULT_TIMEOUT_MS, admitKernel, allowEndpoint, kernelInvocationEndpoint, platformDuration, positive, } from './context/index.js';
|
|
15
|
+
import { admitControlOptions, admitPolicy, createSessionOperation, DEFAULT_MAXIMUM_BUNDLE_BYTES, DEFAULT_MAXIMUM_PUBLICATION_BYTES, DEFAULT_MAXIMUM_RELEASE_BYTES, DEFAULT_MAXIMUM_RESPONSE_BYTES, DEFAULT_TIMEOUT_MS, admitKernel, allowEndpoint, kernelInvocationEndpoint, platformDuration, positive, } from './context/index.js';
|
|
16
16
|
import { createSessionDiscovery } from './discovery/index.js';
|
|
17
17
|
import { acquireDomainCredential, admitExchangeOptions, admitExchangeRequest, createSessionExchange, } from './exchange/index.js';
|
|
18
18
|
import { admitReference } from './reference.js';
|
|
@@ -51,6 +51,7 @@ export class ClientSession {
|
|
|
51
51
|
if (options.client !== undefined && options.pool !== undefined) {
|
|
52
52
|
throw new TypeError('ClientSession accepts either client or pool, not both.');
|
|
53
53
|
}
|
|
54
|
+
const maximumReleaseBytes = positive(options.maximumReleaseBytes ?? DEFAULT_MAXIMUM_RELEASE_BYTES, 'maximumReleaseBytes');
|
|
54
55
|
const maximumPublicationBytes = positive(options.maximumPublicationBytes ?? DEFAULT_MAXIMUM_PUBLICATION_BYTES, 'maximumPublicationBytes');
|
|
55
56
|
const maximumBundleBytes = positive(options.maximumBundleBytes ?? DEFAULT_MAXIMUM_BUNDLE_BYTES, 'maximumBundleBytes');
|
|
56
57
|
const maximumResponseBytes = positive(options.maximumResponseBytes ?? DEFAULT_MAXIMUM_RESPONSE_BYTES, 'maximumResponseBytes');
|
|
@@ -82,6 +83,7 @@ export class ClientSession {
|
|
|
82
83
|
kernel: this.kernel,
|
|
83
84
|
fetch: fetchImpl,
|
|
84
85
|
policy,
|
|
86
|
+
maximumReleaseBytes,
|
|
85
87
|
maximumPublicationBytes,
|
|
86
88
|
maximumBundleBytes,
|
|
87
89
|
requireOpen: () => this.requireOpen(),
|
|
@@ -152,7 +154,19 @@ export class ClientSession {
|
|
|
152
154
|
const request = admitExchangeRequest(domain, options);
|
|
153
155
|
return acquireDomainCredential(this.exchangeDependencies, request, this.operation(request.control));
|
|
154
156
|
}
|
|
155
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* Revalidate the source Kernel's own release (`release.json`) without acquiring its bundle or
|
|
159
|
+
* Views; a Host that predates intrinsic `release.json` yields its version 3 Publication instead.
|
|
160
|
+
*/
|
|
161
|
+
release(options) {
|
|
162
|
+
return this.discovery.release(this.operation(admitControlOptions(options)));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Revalidate the intrinsic release and return its v3 document: the Host's own version 3
|
|
166
|
+
* Publication, or the frozen v3 projection of its `DomainRelease`.
|
|
167
|
+
*
|
|
168
|
+
* @deprecated Use `release()`. Deleted when no supported Host predates intrinsic `release.json`.
|
|
169
|
+
*/
|
|
156
170
|
publication(options) {
|
|
157
171
|
return this.discovery.publication(this.operation(admitControlOptions(options)));
|
|
158
172
|
}
|
|
@@ -178,16 +192,14 @@ export class ClientSession {
|
|
|
178
192
|
const instance = last?.kind === 'method' && last.dispatch === 'instance'
|
|
179
193
|
? Path.fromAST({ anchor, steps: steps.slice(0, -1) }).raw
|
|
180
194
|
: undefined;
|
|
181
|
-
// A generated reference pins the callable meaning
|
|
182
|
-
//
|
|
195
|
+
// A generated reference pins the callable meaning; a Kernel reference pins nothing, like every
|
|
196
|
+
// other syscall the Session addresses.
|
|
183
197
|
const call = {
|
|
184
198
|
target: Object.freeze({
|
|
185
199
|
callable: admittedReference.callable,
|
|
186
|
-
...(admittedReference.fingerprint
|
|
187
|
-
? {
|
|
188
|
-
: admittedReference.
|
|
189
|
-
? { revision: admittedReference.revision }
|
|
190
|
-
: {}),
|
|
200
|
+
...(admittedReference.fingerprint === undefined
|
|
201
|
+
? {}
|
|
202
|
+
: { fingerprint: admittedReference.fingerprint }),
|
|
191
203
|
...(instance === undefined ? {} : { instance }),
|
|
192
204
|
}),
|
|
193
205
|
input,
|
|
@@ -241,7 +253,6 @@ export class ClientSession {
|
|
|
241
253
|
throw new TypeError(`Method owner ${method.owner.ref.origin} is absent.`);
|
|
242
254
|
return this.invoke({
|
|
243
255
|
origin: owner.origin,
|
|
244
|
-
revision: owner.revision,
|
|
245
256
|
fingerprint: callableFingerprint(method),
|
|
246
257
|
callable: method.key,
|
|
247
258
|
target: Path.instanceMethod(Path.id(node.id), method.key),
|
|
@@ -12,6 +12,9 @@ void envelope;
|
|
|
12
12
|
void policy;
|
|
13
13
|
void request;
|
|
14
14
|
void snapshot;
|
|
15
|
+
const observedRelease = session.release(control);
|
|
16
|
+
void observedRelease;
|
|
17
|
+
void observedRelease.then((current) => current.version === 4 ? current.build.digest : current.etag);
|
|
15
18
|
const observedPublication = session.publication(control);
|
|
16
19
|
void observedPublication;
|
|
17
20
|
const admitted = session.auth.admit({
|
|
@@ -28,11 +31,19 @@ const credentials = createSessionCredentialProvider({
|
|
|
28
31
|
// A credential provider is a Session authentication resolver; no adapter stands between them.
|
|
29
32
|
const credentialAuth = credentials;
|
|
30
33
|
void new ClientSession({ ...options, auth: credentialAuth });
|
|
31
|
-
// Automatic exchange remains compatible with the original empty and TTL-only options.
|
|
32
|
-
void new ClientSession({ ...options, auth: credentialAuth, exchange: {} });
|
|
33
|
-
void new ClientSession({ ...options, auth: credentialAuth, exchange: { ttlSeconds: 300 } });
|
|
34
34
|
void new ClientSession({ ...options, auth: credentialAuth, exchange: { mode: 'auto' } });
|
|
35
|
+
void new ClientSession({
|
|
36
|
+
...options,
|
|
37
|
+
auth: credentialAuth,
|
|
38
|
+
exchange: { mode: 'auto', ttlSeconds: 300 },
|
|
39
|
+
});
|
|
35
40
|
void new ClientSession({ ...options, auth: credentialAuth, exchange: { mode: 'source' } });
|
|
41
|
+
// @ts-expect-error Exchange options always name their mode.
|
|
42
|
+
void new ClientSession({ ...options, auth: credentialAuth, exchange: {} });
|
|
43
|
+
// @ts-expect-error Exchange options always name their mode.
|
|
44
|
+
void new ClientSession({ ...options, auth: credentialAuth, exchange: { ttlSeconds: 300 } });
|
|
45
|
+
// @ts-expect-error Source mode requests no Domain credential lifetime.
|
|
46
|
+
void new ClientSession({ ...options, exchange: { mode: 'source', ttlSeconds: 300 } });
|
|
36
47
|
// @ts-expect-error Exchange accepts named modes, not a boolean switch.
|
|
37
48
|
void new ClientSession({ ...options, exchange: true });
|
|
38
49
|
// @ts-expect-error Unknown exchange modes are not admitted.
|
|
@@ -7,7 +7,6 @@ export interface AutoTransportOptions {
|
|
|
7
7
|
readonly http: string;
|
|
8
8
|
readonly websocket: string;
|
|
9
9
|
};
|
|
10
|
-
readonly codec?: 'json' | 'msgpack';
|
|
11
10
|
readonly fetch?: Fetch;
|
|
12
11
|
readonly websocket?: WebSocketConstructor;
|
|
13
12
|
readonly headers?: Readonly<Record<string, string>>;
|
|
@@ -26,7 +26,6 @@ export class AutoTransport {
|
|
|
26
26
|
this.onWarning = options.onWarning;
|
|
27
27
|
this.http = new HttpTransport({
|
|
28
28
|
url: options.endpoints.http,
|
|
29
|
-
...(options.codec === undefined ? {} : { codec: options.codec }),
|
|
30
29
|
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
31
30
|
...(options.headers === undefined ? {} : { headers: options.headers }),
|
|
32
31
|
timeoutMs: this.timeoutMs,
|
|
@@ -39,7 +38,6 @@ export class AutoTransport {
|
|
|
39
38
|
});
|
|
40
39
|
this.websocket = new WebSocketTransport({
|
|
41
40
|
url: options.endpoints.websocket,
|
|
42
|
-
...(options.codec === undefined ? {} : { codec: options.codec }),
|
|
43
41
|
...(options.websocket === undefined ? {} : { websocket: options.websocket }),
|
|
44
42
|
timeoutMs: this.timeoutMs,
|
|
45
43
|
...(options.maximumRequestBytes === undefined
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as codecs from '@astrale-os/kernel-protocol/codecs';
|
|
2
2
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
3
|
-
export declare function codecForResponse(contentType: string | null):
|
|
3
|
+
export declare function codecForResponse(contentType: string | null): codecs.Codec | undefined;
|
|
4
4
|
export declare function readBoundedBody(response: Response, maximumBytes: number, signal: AbortSignal): Promise<Uint8Array>;
|
|
5
5
|
export declare function requireId(expected: protocol.RequestId, received: protocol.RequestId): void;
|
|
6
6
|
export declare function decodeProtocol<T>(label: string, decode: () => T): T;
|
|
@@ -2,12 +2,8 @@ import * as codecs from '@astrale-os/kernel-protocol/codecs';
|
|
|
2
2
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
3
3
|
import { ProtocolError, ResponseError } from '../../errors/index.js';
|
|
4
4
|
import { asError } from '../index.js';
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
const supported = mediaType === codecs.CONTENT_TYPE_MSGPACK
|
|
8
|
-
? [codecs.json, await codecs.loadMsgpack()]
|
|
9
|
-
: [codecs.json];
|
|
10
|
-
return protocol.http.codecForContentType(contentType, supported);
|
|
5
|
+
export function codecForResponse(contentType) {
|
|
6
|
+
return protocol.http.codecForContentType(contentType, [codecs.json]);
|
|
11
7
|
}
|
|
12
8
|
export async function readBoundedBody(response, maximumBytes, signal) {
|
|
13
9
|
const declared = response.headers.get('content-length');
|
|
@@ -3,7 +3,6 @@ import type { RequestOptions, Transport, TransportRequest, TransportResult } fro
|
|
|
3
3
|
export type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
4
4
|
export interface HttpTransportOptions {
|
|
5
5
|
readonly url: string;
|
|
6
|
-
readonly codec?: 'json' | 'msgpack';
|
|
7
6
|
/** Inject this capability when the host does not provide global Fetch. */
|
|
8
7
|
readonly fetch?: Fetch;
|
|
9
8
|
readonly headers?: Readonly<Record<string, string>>;
|
|
@@ -16,7 +15,6 @@ export interface HttpTransportOptions {
|
|
|
16
15
|
/** HTTP implementation of the universal invocation protocol. */
|
|
17
16
|
export declare class HttpTransport implements Transport {
|
|
18
17
|
readonly url: string;
|
|
19
|
-
private readonly codecName;
|
|
20
18
|
private readonly fetchImpl;
|
|
21
19
|
private readonly headers;
|
|
22
20
|
private readonly timeoutMs;
|
|
@@ -29,7 +27,11 @@ export declare class HttpTransport implements Transport {
|
|
|
29
27
|
dispatchBinary(input: TransportRequest, body: Blob, options?: RequestOptions): Promise<TransportResult>;
|
|
30
28
|
private dispatchWith;
|
|
31
29
|
private fetchResponse;
|
|
30
|
+
/**
|
|
31
|
+
* Admit the reserved retirement control only as the exact 410, header and envelope triple; the
|
|
32
|
+
* header beside any other status, value or body is an invalid response, never an ordinary error.
|
|
33
|
+
*/
|
|
34
|
+
private decodeDeploymentRetired;
|
|
32
35
|
private decodeBinary;
|
|
33
36
|
private nextRequestId;
|
|
34
|
-
private requestCodec;
|
|
35
37
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { canonicalMediaType } from '@astrale-os/kernel-protocol/artifact';
|
|
2
2
|
import * as codecs from '@astrale-os/kernel-protocol/codecs';
|
|
3
3
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
4
|
+
import * as retirement from '@astrale-os/kernel-protocol/routes/deployment-retired';
|
|
4
5
|
import { ProtocolError, ResponseError, TransportError } from '../../errors/index.js';
|
|
5
6
|
import { invocationFailure } from '../failure.js';
|
|
6
7
|
import { asError, controlledSignal, positiveInteger, requireLive, } from '../index.js';
|
|
@@ -13,7 +14,6 @@ const DEFAULT_MAXIMUM_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
|
13
14
|
/** HTTP implementation of the universal invocation protocol. */
|
|
14
15
|
export class HttpTransport {
|
|
15
16
|
url;
|
|
16
|
-
codecName;
|
|
17
17
|
fetchImpl;
|
|
18
18
|
headers;
|
|
19
19
|
timeoutMs;
|
|
@@ -25,7 +25,6 @@ export class HttpTransport {
|
|
|
25
25
|
throw new TypeError('HttpTransport options must be an object.');
|
|
26
26
|
}
|
|
27
27
|
this.url = normalizeUrl(options.url);
|
|
28
|
-
this.codecName = admitCodecName(options.codec);
|
|
29
28
|
this.fetchImpl = admitFetch(options.fetch);
|
|
30
29
|
this.headers = admitHeaders(options.headers ?? {});
|
|
31
30
|
this.timeoutMs = positiveInteger(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, 'timeoutMs');
|
|
@@ -47,7 +46,7 @@ export class HttpTransport {
|
|
|
47
46
|
async dispatchWith(input, options, binaryBody) {
|
|
48
47
|
const requestId = this.nextRequestId();
|
|
49
48
|
const timeoutMs = positiveInteger(options.timeoutMs ?? this.timeoutMs, 'timeoutMs');
|
|
50
|
-
const codec =
|
|
49
|
+
const codec = codecs.json;
|
|
51
50
|
const { trace: traceInput, ...invocationInput } = input;
|
|
52
51
|
const trace = traceInput === undefined ? undefined : protocol.admitTrace(traceInput);
|
|
53
52
|
const request = protocol.decodeRequest({
|
|
@@ -82,7 +81,15 @@ export class HttpTransport {
|
|
|
82
81
|
const binaryDelivery = response.headers.get(protocol.http.BINARY_DELIVERY_FIELD);
|
|
83
82
|
const hasStreamSignal = streamFraming !== null;
|
|
84
83
|
const hasBinarySignal = binaryId !== null || binaryManifest !== null || binaryDelivery !== null;
|
|
85
|
-
const responseCodec =
|
|
84
|
+
const responseCodec = codecForResponse(response.headers.get('content-type'));
|
|
85
|
+
if (response.headers.has(retirement.HTTP_DEPLOYMENT_RETIRED)) {
|
|
86
|
+
if (hasStreamSignal || hasBinarySignal) {
|
|
87
|
+
throw new ProtocolError('server returned a deployment-retired control with a stream or binary signal.');
|
|
88
|
+
}
|
|
89
|
+
const retired = await this.decodeDeploymentRetired(response, responseCodec, control);
|
|
90
|
+
requireLive(control);
|
|
91
|
+
return retired;
|
|
92
|
+
}
|
|
86
93
|
if (!response.ok && responseCodec === undefined && !hasStreamSignal && !hasBinarySignal) {
|
|
87
94
|
throw invocationFailure(`server returned HTTP ${response.status} without an invocation response.`, 'receive', { delivery: 'unknown' });
|
|
88
95
|
}
|
|
@@ -182,6 +189,29 @@ export class HttpTransport {
|
|
|
182
189
|
};
|
|
183
190
|
return this.fetchImpl(this.url, init);
|
|
184
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Admit the reserved retirement control only as the exact 410, header and envelope triple; the
|
|
194
|
+
* header beside any other status, value or body is an invalid response, never an ordinary error.
|
|
195
|
+
*/
|
|
196
|
+
async decodeDeploymentRetired(response, codec, control) {
|
|
197
|
+
if (codec === undefined) {
|
|
198
|
+
throw new ProtocolError('server returned a deployment-retired control without an envelope.');
|
|
199
|
+
}
|
|
200
|
+
requireNoStore(response);
|
|
201
|
+
const body = await readBoundedBody(response, this.maximumResponseBytes, control.signal);
|
|
202
|
+
const decoded = decodeProtocol('deployment-retired body', () => codec.decode(body));
|
|
203
|
+
const retired = decodeProtocol('deployment-retired control', () => retirement.decodeDeploymentRetired(response.status, response.headers, decoded));
|
|
204
|
+
const envelope = decodeProtocol('deployment-retired envelope', () => protocol.decodeResponse(decoded));
|
|
205
|
+
if (retired === undefined || !('error' in envelope)) {
|
|
206
|
+
throw new ProtocolError('server returned an invalid deployment-retired control.');
|
|
207
|
+
}
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
kind: 'deployment-retired',
|
|
210
|
+
reason: retired.reason,
|
|
211
|
+
message: envelope.error.message,
|
|
212
|
+
invocation: envelope.invocation,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
185
215
|
async decodeBinary(response, requestId, binaryId, streaming, control) {
|
|
186
216
|
if (binaryId !== String(requestId)) {
|
|
187
217
|
throw new ProtocolError(`binary response requestId ${JSON.stringify(binaryId)} does not match request requestId ${requestId}.`);
|
|
@@ -197,14 +227,13 @@ export class HttpTransport {
|
|
|
197
227
|
if (streaming) {
|
|
198
228
|
if (response.body === null)
|
|
199
229
|
throw new ProtocolError('streaming binary has no body.');
|
|
200
|
-
// An intermediary may rewrite Content-Length (for example when it compresses the body), so
|
|
201
|
-
// protocol length
|
|
202
|
-
const
|
|
203
|
-
const length = declared ?? response.headers.get('content-length');
|
|
230
|
+
// An intermediary may rewrite Content-Length (for example when it compresses the body), so
|
|
231
|
+
// only the protocol length counts.
|
|
232
|
+
const length = response.headers.get(protocol.http.BINARY_LENGTH_FIELD);
|
|
204
233
|
const expectedBytes = length === null ? undefined : Number(length);
|
|
205
234
|
if (length !== null &&
|
|
206
235
|
(!/^(?:0|[1-9][0-9]*)$/u.test(length) || !Number.isSafeInteger(expectedBytes))) {
|
|
207
|
-
throw new ProtocolError(`streaming binary has an invalid ${
|
|
236
|
+
throw new ProtocolError(`streaming binary has an invalid ${protocol.http.BINARY_LENGTH_FIELD}.`);
|
|
208
237
|
}
|
|
209
238
|
if (expectedBytes !== undefined && expectedBytes > this.maximumResponseBytes) {
|
|
210
239
|
throw new ProtocolError(`streaming binary exceeds maximumResponseBytes (${this.maximumResponseBytes}).`);
|
|
@@ -215,6 +244,8 @@ export class HttpTransport {
|
|
|
215
244
|
invocation,
|
|
216
245
|
value: {
|
|
217
246
|
body: new HttpByteStream(response.body, this.maximumResponseBytes, control, expectedBytes),
|
|
247
|
+
// The byte stream enforces this exact count through its final read.
|
|
248
|
+
...(expectedBytes === undefined ? {} : { length: expectedBytes }),
|
|
218
249
|
mediaType,
|
|
219
250
|
...(response.status === 200 ? {} : { status: response.status }),
|
|
220
251
|
...(Object.keys(headers).length === 0 ? {} : { headers }),
|
|
@@ -249,9 +280,6 @@ export class HttpTransport {
|
|
|
249
280
|
this.nextId = requestId === Number.MAX_SAFE_INTEGER ? 1 : requestId + 1;
|
|
250
281
|
return requestId;
|
|
251
282
|
}
|
|
252
|
-
requestCodec() {
|
|
253
|
-
return this.codecName === 'msgpack' ? codecs.loadMsgpack() : Promise.resolve(codecs.json);
|
|
254
|
-
}
|
|
255
283
|
}
|
|
256
284
|
function redirectResult(response) {
|
|
257
285
|
return Object.freeze({
|
|
@@ -340,13 +368,6 @@ function transportFailure(input, control, parent, timeoutMs, url, phase, deliver
|
|
|
340
368
|
delivery,
|
|
341
369
|
});
|
|
342
370
|
}
|
|
343
|
-
function admitCodecName(input) {
|
|
344
|
-
if (input === undefined || input === 'json')
|
|
345
|
-
return 'json';
|
|
346
|
-
if (input === 'msgpack')
|
|
347
|
-
return input;
|
|
348
|
-
throw new TypeError('codec must be "json" or "msgpack".');
|
|
349
|
-
}
|
|
350
371
|
function admitFetch(input) {
|
|
351
372
|
const implementation = input ?? globalThis.fetch;
|
|
352
373
|
if (typeof implementation !== 'function') {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { RequestOptions, MissControl, StaleControl, Transport, TransportRequest, TransportResult, } from './transport.js';
|
|
1
|
+
export type { RequestOptions, DeploymentRetired, MissControl, StaleControl, Transport, TransportRequest, TransportResult, } from './transport.js';
|
|
2
2
|
export { asError, controlledSignal, positiveInteger, requireLive, type ControlledSignal, } from './deadline.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { invocation as protocol } from '@astrale-os/kernel-protocol';
|
|
1
|
+
import type { invocation as protocol, routes } from '@astrale-os/kernel-protocol';
|
|
2
2
|
import type { Result } from '../client/result.js';
|
|
3
3
|
export interface RequestOptions {
|
|
4
4
|
readonly signal?: AbortSignal;
|
|
@@ -25,7 +25,15 @@ export interface MissControl {
|
|
|
25
25
|
readonly kind: 'miss';
|
|
26
26
|
readonly invocation: protocol.InvocationId;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* A retired deployment's reserved 410 control: its typed `DEPLOYMENT_RETIRED` reason with the
|
|
30
|
+
* message and invocation of the envelope it carried, which Client raises unchanged.
|
|
31
|
+
*/
|
|
32
|
+
export interface DeploymentRetired extends routes.DeploymentRetired {
|
|
33
|
+
readonly message: string;
|
|
34
|
+
readonly invocation: protocol.InvocationId;
|
|
35
|
+
}
|
|
36
|
+
export type TransportResult = Result | StaleControl | MissControl | DeploymentRetired;
|
|
29
37
|
export interface Transport {
|
|
30
38
|
/** Primary embedding-environment adapter for a complete invocation boundary. */
|
|
31
39
|
dispatch(input: TransportRequest, options?: RequestOptions): Promise<TransportResult>;
|
|
@@ -3,7 +3,6 @@ import type { RequestOptions, Transport, TransportRequest, TransportResult } fro
|
|
|
3
3
|
export type WebSocketConstructor = new (url: string | URL, protocols?: string | string[]) => WebSocket;
|
|
4
4
|
export interface WebSocketTransportOptions {
|
|
5
5
|
readonly url: string;
|
|
6
|
-
readonly codec?: 'json' | 'msgpack';
|
|
7
6
|
/** Inject this capability when the host does not provide a global WebSocket. */
|
|
8
7
|
readonly websocket?: WebSocketConstructor;
|
|
9
8
|
readonly timeoutMs?: number;
|
|
@@ -22,7 +21,6 @@ export interface WebSocketTransportOptions {
|
|
|
22
21
|
export declare class WebSocketTransport implements Transport {
|
|
23
22
|
readonly url: string;
|
|
24
23
|
private readonly connectionUrl;
|
|
25
|
-
private readonly codecName;
|
|
26
24
|
private readonly WebSocketConstructor;
|
|
27
25
|
private readonly timeoutMs;
|
|
28
26
|
private readonly maximumRequestBytes;
|
|
@@ -42,5 +40,4 @@ export declare class WebSocketTransport implements Transport {
|
|
|
42
40
|
private connectionClosed;
|
|
43
41
|
private acquire;
|
|
44
42
|
private nextRequestId;
|
|
45
|
-
private codec;
|
|
46
43
|
}
|