@astrale-os/kernel-client 0.6.0-beta.71 → 0.6.0-beta.72
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/credential.d.ts +5 -1
- package/dist/client/client.d.ts +4 -2
- package/dist/client/client.js +13 -2
- package/dist/client/credential.d.ts +4 -2
- package/dist/client/credential.js +7 -2
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/request.d.ts +7 -1
- package/dist/client/request.js +31 -6
- package/dist/index.d.ts +1 -1
- package/dist/session/bound.d.ts +10 -8
- package/dist/session/bound.js +20 -3
- package/dist/session/content.d.ts +30 -0
- package/dist/session/content.js +160 -0
- package/dist/session/context/operation.d.ts +5 -1
- package/dist/session/index.d.ts +3 -2
- package/dist/session/reference.d.ts +6 -3
- package/dist/session/routing/dispatch.d.ts +10 -0
- package/dist/session/routing/dispatch.js +68 -23
- package/dist/session/routing/index.d.ts +1 -1
- package/dist/session/routing/index.js +1 -1
- package/dist/session/routing/request.d.ts +9 -0
- package/dist/session/routing/request.js +17 -0
- package/dist/session/session.d.ts +10 -3
- package/dist/session/session.js +35 -10
- package/dist/session/session.typecheck.js +39 -1
- package/dist/transport/http/byte-stream.d.ts +2 -1
- package/dist/transport/http/byte-stream.js +9 -1
- package/dist/transport/http/transport.d.ts +3 -0
- package/dist/transport/http/transport.js +42 -10
- package/package.json +5 -5
|
@@ -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, dispatchTrace, isAdmittedMissError, isAdmittedStaleRouteError, } from '../../client/index.js';
|
|
3
|
+
import { acceptCall, Client, dispatchTrace, isAdmittedMissError, isAdmittedStaleRouteError, } from '../../client/index.js';
|
|
4
4
|
import { ClientError, ResponseError } from '../../errors/index.js';
|
|
5
5
|
import { runSessionOperation, sessionTransportOptions, allowEndpoint } from '../context/index.js';
|
|
6
6
|
import { resolveAuth } from './auth.js';
|
|
@@ -11,6 +11,13 @@ export class SessionDispatcher {
|
|
|
11
11
|
this.state = state;
|
|
12
12
|
}
|
|
13
13
|
async dispatch(input, options, operation) {
|
|
14
|
+
return this.dispatchWith(input, options, operation);
|
|
15
|
+
}
|
|
16
|
+
/** Reuse Session authority and route recovery while forcing HTTP for streaming calls. */
|
|
17
|
+
async dispatchHttp(input, options, operation, body) {
|
|
18
|
+
return this.dispatchWith(input, options, operation, { body });
|
|
19
|
+
}
|
|
20
|
+
async dispatchWith(input, options, operation, streaming) {
|
|
14
21
|
this.state.requireOpen();
|
|
15
22
|
const call = acceptCall(input);
|
|
16
23
|
// Authority is resolved before warm lookup so changed credentials or delegation cannot reuse a
|
|
@@ -27,7 +34,7 @@ export class SessionDispatcher {
|
|
|
27
34
|
let learned = await this.state.cache.get(cacheKey, auth.credential);
|
|
28
35
|
if (learned !== undefined) {
|
|
29
36
|
try {
|
|
30
|
-
requireCachedRoute(learned, source, call.target, this.state.envelopeTransport, this.state.websocketAvailable, this.state.policy);
|
|
37
|
+
requireCachedRoute(learned, source, call.target, streaming === undefined ? this.state.envelopeTransport : 'http', this.state.websocketAvailable, this.state.policy);
|
|
31
38
|
}
|
|
32
39
|
catch {
|
|
33
40
|
this.state.cache.delete(cacheKey);
|
|
@@ -35,22 +42,31 @@ export class SessionDispatcher {
|
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
if (learned !== undefined) {
|
|
38
|
-
return this.dispatchDestinationWithRecovery(learned, dispatch, options, operation);
|
|
45
|
+
return this.dispatchDestinationWithRecovery(learned, dispatch, options, operation, undefined, streaming);
|
|
39
46
|
}
|
|
40
|
-
const result = await this.dispatchSource(dispatch, undefined, options, operation);
|
|
41
|
-
return this.followSource(result, dispatch, options, operation);
|
|
47
|
+
const result = await this.dispatchSource(dispatch, undefined, options, operation, streaming);
|
|
48
|
+
return this.followSource(result, dispatch, options, operation, streaming);
|
|
42
49
|
}
|
|
43
|
-
async dispatchSource(state, routing, options, operation) {
|
|
50
|
+
async dispatchSource(state, routing, options, operation, streaming) {
|
|
44
51
|
this.state.requireOpen();
|
|
45
52
|
allowEndpoint(this.state.sourceEndpoint, this.state.policy);
|
|
46
|
-
const client =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
const client = streaming === undefined
|
|
54
|
+
? this.state.pool.clientFor(Object.freeze({ transport: 'http', url: this.state.sourceEndpoint }))
|
|
55
|
+
: this.streamingClientFor(this.state.sourceEndpoint);
|
|
56
|
+
return runSessionOperation(operation, () => streaming?.body === undefined
|
|
57
|
+
? authenticated(client, state.auth.credential).dispatch(state.call, {
|
|
58
|
+
...sessionTransportOptions(operation, options.idempotencyKey),
|
|
59
|
+
...(state.auth.delegate === undefined ? {} : { delegate: state.auth.delegate }),
|
|
60
|
+
...(routing === undefined ? {} : { routing }),
|
|
61
|
+
})
|
|
62
|
+
: authenticated(client, state.auth.credential).dispatch(state.call, {
|
|
63
|
+
...sessionTransportOptions(operation),
|
|
64
|
+
...(state.auth.delegate === undefined ? {} : { delegate: state.auth.delegate }),
|
|
65
|
+
...(routing === undefined ? {} : { routing }),
|
|
66
|
+
body: streaming.body,
|
|
67
|
+
}));
|
|
52
68
|
}
|
|
53
|
-
async followSource(result, state, options, operation) {
|
|
69
|
+
async followSource(result, state, options, operation, streaming) {
|
|
54
70
|
if (result.kind !== 'redirect')
|
|
55
71
|
return result;
|
|
56
72
|
const redirect = result.redirect;
|
|
@@ -59,11 +75,11 @@ export class SessionDispatcher {
|
|
|
59
75
|
}
|
|
60
76
|
requireTargetPublication(redirect.route, state.call.target);
|
|
61
77
|
const learned = await this.state.cache.learn(state.cacheKey, redirect.route, redirect.credential, state.auth.credential);
|
|
62
|
-
return this.dispatchDestinationWithRecovery(learned, state, options, operation, redirect.trace);
|
|
78
|
+
return this.dispatchDestinationWithRecovery(learned, state, options, operation, redirect.trace, streaming);
|
|
63
79
|
}
|
|
64
|
-
async dispatchDestinationWithRecovery(learned, state, options, operation, trace) {
|
|
80
|
+
async dispatchDestinationWithRecovery(learned, state, options, operation, trace, streaming) {
|
|
65
81
|
try {
|
|
66
|
-
return await this.dispatchDestination(learned, state.call, options, operation, state.source, trace);
|
|
82
|
+
return await this.dispatchDestination(learned, state.call, options, operation, state.source, trace, streaming);
|
|
67
83
|
}
|
|
68
84
|
catch (cause) {
|
|
69
85
|
const stale = isAdmittedStaleRouteError(cause);
|
|
@@ -83,20 +99,31 @@ export class SessionDispatcher {
|
|
|
83
99
|
stale: learned.route.via.publication.etag,
|
|
84
100
|
})
|
|
85
101
|
: undefined;
|
|
86
|
-
const refreshed = await this.dispatchSource(state, routing, options, operation);
|
|
87
|
-
return this.followSource(refreshed, state, options, operation);
|
|
102
|
+
const refreshed = await this.dispatchSource(state, routing, options, operation, streaming);
|
|
103
|
+
return this.followSource(refreshed, state, options, operation, streaming);
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
|
-
async dispatchDestination(learned, call, options, operation, source, trace) {
|
|
106
|
+
async dispatchDestination(learned, call, options, operation, source, trace, streaming) {
|
|
91
107
|
if (learned.route.via.issuer !== source) {
|
|
92
108
|
throw new ClientError('Learned route issuer differs from the session-pinned source Kernel.');
|
|
93
109
|
}
|
|
94
110
|
requireTargetPublication(learned.route, call.target);
|
|
95
|
-
const
|
|
111
|
+
const destination = streaming === undefined
|
|
112
|
+
? undefined
|
|
113
|
+
: selectEnvelopeTarget(learned.route.endpoint, 'http', this.state.websocketAvailable, this.state.policy);
|
|
114
|
+
const client = streaming === undefined
|
|
115
|
+
? selectEnvelope(learned.route.endpoint, this.state.envelopeTransport, this.state.websocketAvailable, this.state.pool, this.state.policy).client
|
|
116
|
+
: destination?.transport === 'http'
|
|
117
|
+
? this.streamingClientFor(destination.url)
|
|
118
|
+
: invalidStreamingDestination();
|
|
96
119
|
const requestOptions = {
|
|
97
120
|
...sessionTransportOptions(operation, options.idempotencyKey),
|
|
98
121
|
routing: learned.route.via,
|
|
99
122
|
};
|
|
123
|
+
const binaryRequestOptions = {
|
|
124
|
+
...sessionTransportOptions(operation),
|
|
125
|
+
routing: learned.route.via,
|
|
126
|
+
};
|
|
100
127
|
// The source has admitted this fingerprint against the exact Publication carried by Via.
|
|
101
128
|
// Pin that revision at the destination so existing Workers can execute the same admitted
|
|
102
129
|
// callable without understanding the newer fingerprint Target representation. The cache
|
|
@@ -111,14 +138,32 @@ export class SessionDispatcher {
|
|
|
111
138
|
}),
|
|
112
139
|
}
|
|
113
140
|
: call;
|
|
114
|
-
const result = await runSessionOperation(operation, () =>
|
|
115
|
-
?
|
|
116
|
-
|
|
141
|
+
const result = await runSessionOperation(operation, () => streaming?.body !== undefined
|
|
142
|
+
? trace === undefined
|
|
143
|
+
? authenticated(client, learned.credential).dispatch(destinationCall, {
|
|
144
|
+
...binaryRequestOptions,
|
|
145
|
+
body: streaming.body,
|
|
146
|
+
})
|
|
147
|
+
: dispatchTrace(client, learned.credential, destinationCall, { ...binaryRequestOptions, body: streaming.body }, trace)
|
|
148
|
+
: trace === undefined
|
|
149
|
+
? authenticated(client, learned.credential).dispatch(destinationCall, requestOptions)
|
|
150
|
+
: dispatchTrace(client, learned.credential, destinationCall, requestOptions, trace));
|
|
117
151
|
if (result.kind === 'redirect') {
|
|
118
152
|
throw new ClientError('A destination redirect is invalid; only the source Kernel may redirect.');
|
|
119
153
|
}
|
|
120
154
|
return result;
|
|
121
155
|
}
|
|
156
|
+
streamingClientFor(url) {
|
|
157
|
+
return new Client({
|
|
158
|
+
url,
|
|
159
|
+
fetch: this.state.streamingHttp.fetch,
|
|
160
|
+
timeoutMs: this.state.streamingHttp.timeoutMs,
|
|
161
|
+
maximumResponseBytes: this.state.streamingHttp.maximumResponseBytes,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function invalidStreamingDestination() {
|
|
166
|
+
throw new ClientError('Streaming route has no HTTP endpoint.');
|
|
122
167
|
}
|
|
123
168
|
function requireCachedRoute(learned, source, target, envelopeTransport, websocketAvailable, policy) {
|
|
124
169
|
if (learned.route.via.issuer !== source) {
|
|
@@ -2,4 +2,4 @@ export { admitSessionAuth, resolveAuth, type Auth, type EffectiveAuth, type Sess
|
|
|
2
2
|
export { RouteCache, type LearnedRoute, type PersistedRoute, type SessionRouteArtifact, type SessionRouteStore, } from './cache.js';
|
|
3
3
|
export { requireResultKind, SessionDispatcher, type SessionDispatcherOptions } from './dispatch.js';
|
|
4
4
|
export { type ClientPoolCapability, type EnvelopeSelection } from './envelope.js';
|
|
5
|
-
export { admitEnvelopeTransport, admitRequestOptions, type SessionEnvelopeTransport, type SessionRequestOptions, } from './request.js';
|
|
5
|
+
export { admitEnvelopeTransport, admitInvocationOptions, admitRequestOptions, type SessionEnvelopeTransport, type SessionRequestOptions, type SessionRequestOptionsWithBody, } from './request.js';
|
|
@@ -2,4 +2,4 @@ export { admitSessionAuth, resolveAuth, } from './auth.js';
|
|
|
2
2
|
export { RouteCache, } from './cache.js';
|
|
3
3
|
export { requireResultKind, SessionDispatcher } from './dispatch.js';
|
|
4
4
|
export {} from './envelope.js';
|
|
5
|
-
export { admitEnvelopeTransport, admitRequestOptions, } from './request.js';
|
|
5
|
+
export { admitEnvelopeTransport, admitInvocationOptions, admitRequestOptions, } from './request.js';
|
|
@@ -3,6 +3,15 @@ import type { SessionControlOptions } from '../context/index.js';
|
|
|
3
3
|
export type SessionEnvelopeTransport = 'auto' | 'http' | 'websocket';
|
|
4
4
|
export interface SessionRequestOptions extends SessionControlOptions {
|
|
5
5
|
readonly idempotencyKey?: invocation.IdempotencyKey;
|
|
6
|
+
readonly body?: never;
|
|
6
7
|
}
|
|
8
|
+
export type SessionRequestOptionsWithBody = Omit<SessionRequestOptions, 'body' | 'idempotencyKey'> & {
|
|
9
|
+
readonly body: Blob;
|
|
10
|
+
readonly idempotencyKey?: never;
|
|
11
|
+
};
|
|
12
|
+
export declare function admitInvocationOptions(input: SessionRequestOptions | SessionRequestOptionsWithBody | undefined): {
|
|
13
|
+
readonly options: Readonly<SessionRequestOptions>;
|
|
14
|
+
readonly body?: Blob;
|
|
15
|
+
};
|
|
7
16
|
export declare function admitRequestOptions(input?: SessionRequestOptions): Readonly<SessionRequestOptions>;
|
|
8
17
|
export declare function admitEnvelopeTransport(input: SessionEnvelopeTransport): SessionEnvelopeTransport;
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import * as invocationProtocol from '@astrale-os/kernel-protocol/invocation';
|
|
2
2
|
import { admitControlOptions } from '../context/index.js';
|
|
3
|
+
export function admitInvocationOptions(input) {
|
|
4
|
+
if (input === undefined)
|
|
5
|
+
return Object.freeze({ options: admitRequestOptions() });
|
|
6
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
7
|
+
throw new TypeError('Session request options are invalid.');
|
|
8
|
+
}
|
|
9
|
+
if (!Object.hasOwn(input, 'body')) {
|
|
10
|
+
return Object.freeze({ options: admitRequestOptions(input) });
|
|
11
|
+
}
|
|
12
|
+
const { body, ...options } = input;
|
|
13
|
+
if (!(body instanceof Blob))
|
|
14
|
+
throw new TypeError('Binary input must be a Blob or File.');
|
|
15
|
+
if (options.idempotencyKey !== undefined) {
|
|
16
|
+
throw new TypeError('Binary requests cannot carry an idempotency key.');
|
|
17
|
+
}
|
|
18
|
+
return Object.freeze({ options: admitRequestOptions(options), body });
|
|
19
|
+
}
|
|
3
20
|
export function admitRequestOptions(input = {}) {
|
|
4
21
|
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
5
22
|
throw new TypeError('Session request options are invalid.');
|
|
@@ -10,10 +10,11 @@ import type { SchemaApi } from '../schema/index.js';
|
|
|
10
10
|
import type { ClientWarning } from '../transport/auto/index.js';
|
|
11
11
|
import type { Fetch } from '../transport/http/index.js';
|
|
12
12
|
import type { WebSocketConstructor } from '../transport/websocket/index.js';
|
|
13
|
+
import type { ContentApi } from './content.js';
|
|
13
14
|
import type { SessionControlOptions, SessionPolicy } from './context/index.js';
|
|
14
15
|
import type { SessionSnapshot } from './discovery/index.js';
|
|
15
|
-
import type { CallableReference, InputOfReference, OutputOfReference } from './reference.js';
|
|
16
|
-
import type { SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions } from './routing/index.js';
|
|
16
|
+
import type { CallableReference, InputModeOfReference, InputOfReference, OutputOfReference } from './reference.js';
|
|
17
|
+
import type { SessionRouteStore, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, SessionRequestOptionsWithBody } from './routing/index.js';
|
|
17
18
|
import type { ViewCatalog } from './views/index.js';
|
|
18
19
|
export interface ClientSessionOptions {
|
|
19
20
|
/** Exact canonical source Kernel identity; its invocation and discovery endpoints are derived. */
|
|
@@ -44,6 +45,7 @@ export declare class ClientSession {
|
|
|
44
45
|
readonly schema: SchemaApi;
|
|
45
46
|
readonly graph: GraphApi;
|
|
46
47
|
readonly auth: AuthApi;
|
|
48
|
+
readonly content: ContentApi;
|
|
47
49
|
private readonly ownedPool?;
|
|
48
50
|
private readonly discovery;
|
|
49
51
|
private readonly dispatcher;
|
|
@@ -60,8 +62,13 @@ export declare class ClientSession {
|
|
|
60
62
|
snapshot(options?: SessionControlOptions): Promise<SessionSnapshot>;
|
|
61
63
|
viewsFor(target: PathLike, options?: SessionControlOptions): Promise<ViewCatalog>;
|
|
62
64
|
dispatch(call: Call, options?: SessionRequestOptions): Promise<Result>;
|
|
65
|
+
dispatch(call: Call, options: SessionRequestOptionsWithBody): Promise<Result>;
|
|
63
66
|
call(call: Call, options?: SessionRequestOptions): Promise<Input>;
|
|
64
|
-
|
|
67
|
+
call(call: Call, options: SessionRequestOptionsWithBody): Promise<Input>;
|
|
68
|
+
invoke<const Reference extends CallableReference<unknown>>(reference: InputModeOfReference<Reference> extends 'binary' ? never : Reference, input: InputOfReference<Reference>, options?: SessionRequestOptions): Promise<OutputOfReference<Reference>>;
|
|
69
|
+
invoke<const Reference extends CallableReference<unknown, unknown, 'binary'>>(reference: Reference, input: InputOfReference<Reference>, options: SessionRequestOptionsWithBody): Promise<OutputOfReference<Reference>>;
|
|
70
|
+
private dispatchWith;
|
|
71
|
+
private invokeWith;
|
|
65
72
|
stream(call: Call, options?: SessionRequestOptions): Promise<import("../client/result.js").Stream>;
|
|
66
73
|
binary(call: Call, options?: SessionRequestOptions): Promise<import("@astrale-os/kernel-core/callable").BinaryOutput>;
|
|
67
74
|
close(): void;
|
package/dist/session/session.js
CHANGED
|
@@ -11,10 +11,11 @@ import { AutoTransport } from '../transport/auto/index.js';
|
|
|
11
11
|
import { HttpTransport } from '../transport/http/index.js';
|
|
12
12
|
import { WebSocketTransport } from '../transport/websocket/index.js';
|
|
13
13
|
import { admitFunction } from './admission.js';
|
|
14
|
+
import { createContentApi } from './content.js';
|
|
14
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
16
|
import { createSessionDiscovery } from './discovery/index.js';
|
|
16
17
|
import { admitReference } from './reference.js';
|
|
17
|
-
import { admitEnvelopeTransport,
|
|
18
|
+
import { admitEnvelopeTransport, admitInvocationOptions, admitSessionAuth, requireResultKind, RouteCache, SessionDispatcher, } from './routing/index.js';
|
|
18
19
|
import { resolveViewsFor, targetViewCatalog } from './views/index.js';
|
|
19
20
|
const DEFAULT_SESSION_POLICY = Object.freeze({ maximumRouteAgeMs: 60_000 });
|
|
20
21
|
/** Caller-owned discovery and redirect-following conversation with one exact source Kernel. */
|
|
@@ -23,6 +24,7 @@ export class ClientSession {
|
|
|
23
24
|
schema;
|
|
24
25
|
graph;
|
|
25
26
|
auth;
|
|
27
|
+
content;
|
|
26
28
|
ownedPool;
|
|
27
29
|
discovery;
|
|
28
30
|
dispatcher;
|
|
@@ -106,6 +108,16 @@ export class ClientSession {
|
|
|
106
108
|
policy,
|
|
107
109
|
envelopeTransport: admitEnvelopeTransport(options.envelopeTransport ?? 'http'),
|
|
108
110
|
requireOpen: () => this.requireOpen(),
|
|
111
|
+
streamingHttp: {
|
|
112
|
+
fetch: fetchImpl,
|
|
113
|
+
maximumResponseBytes: 100 * 1024 * 1024,
|
|
114
|
+
timeoutMs: 30 * 60 * 1_000,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
this.content = createContentApi({
|
|
118
|
+
operation: (options) => this.operation(options),
|
|
119
|
+
invoke: (reference, input, options, operation) => this.invokeWith(reference, input, options, operation),
|
|
120
|
+
dispatch: (target, input, operation, body) => this.dispatcher.dispatchHttp({ target, input }, {}, operation, body),
|
|
109
121
|
});
|
|
110
122
|
this.schema = createSchema((request, options) => this.dispatch(request, options), {
|
|
111
123
|
maximumPublicationBytes,
|
|
@@ -134,23 +146,32 @@ export class ClientSession {
|
|
|
134
146
|
return resolveViewsFor(async (call, operation) => requireResultKind('value', await this.dispatcher.dispatch(call, {}, operation)).value, this.admitTargetViews, target, this.operation(admitControlOptions(options)));
|
|
135
147
|
}
|
|
136
148
|
dispatch(call, options) {
|
|
137
|
-
|
|
138
|
-
return this.dispatcher.dispatch(call, admitted, this.operation(admitted));
|
|
149
|
+
return this.dispatchWith(call, options);
|
|
139
150
|
}
|
|
140
151
|
async call(call, options) {
|
|
141
|
-
const result = await this.
|
|
152
|
+
const result = await this.dispatchWith(call, options);
|
|
142
153
|
return requireResultKind('value', result).value;
|
|
143
154
|
}
|
|
144
155
|
async invoke(reference, input, options) {
|
|
156
|
+
return this.invokeWith(reference, input, options);
|
|
157
|
+
}
|
|
158
|
+
dispatchWith(call, options, requireHttp = false) {
|
|
159
|
+
const admitted = admitInvocationOptions(options);
|
|
160
|
+
const operation = this.operation(admitted.options);
|
|
161
|
+
return admitted.body === undefined && !requireHttp
|
|
162
|
+
? this.dispatcher.dispatch(call, admitted.options, operation)
|
|
163
|
+
: this.dispatcher.dispatchHttp(call, admitted.options, operation, admitted.body);
|
|
164
|
+
}
|
|
165
|
+
async invokeWith(reference, input, options, sharedOperation) {
|
|
145
166
|
const admittedReference = admitReference(reference);
|
|
146
|
-
const
|
|
167
|
+
const admitted = admitInvocationOptions(options);
|
|
147
168
|
const { anchor, steps } = admittedReference.target.ast;
|
|
148
169
|
const last = steps.at(-1);
|
|
149
170
|
const instance = last?.kind === 'method' && last.dispatch === 'instance'
|
|
150
171
|
? Path.fromAST({ anchor, steps: steps.slice(0, -1) }).raw
|
|
151
172
|
: undefined;
|
|
152
173
|
// A generated reference pins the callable meaning; a hand-written one still pins the revision.
|
|
153
|
-
const
|
|
174
|
+
const call = {
|
|
154
175
|
target: Object.freeze({
|
|
155
176
|
callable: admittedReference.callable,
|
|
156
177
|
...(admittedReference.fingerprint === undefined
|
|
@@ -159,20 +180,24 @@ export class ClientSession {
|
|
|
159
180
|
...(instance === undefined ? {} : { instance }),
|
|
160
181
|
}),
|
|
161
182
|
input,
|
|
162
|
-
}
|
|
183
|
+
};
|
|
184
|
+
const operation = sharedOperation ?? this.operation(admitted.options);
|
|
185
|
+
const result = await (admitted.body === undefined && admittedReference.outputMode !== 'binary'
|
|
186
|
+
? this.dispatcher.dispatch(call, admitted.options, operation)
|
|
187
|
+
: this.dispatcher.dispatchHttp(call, admitted.options, operation, admitted.body));
|
|
163
188
|
const narrowed = requireResultKind(admittedReference.outputMode, result);
|
|
164
|
-
return
|
|
189
|
+
return narrowed.kind === 'value'
|
|
165
190
|
? narrowed.value
|
|
166
191
|
: narrowed.kind === 'stream'
|
|
167
192
|
? narrowed.stream
|
|
168
|
-
: narrowed.value
|
|
193
|
+
: narrowed.value;
|
|
169
194
|
}
|
|
170
195
|
async stream(call, options) {
|
|
171
196
|
const result = await this.dispatch(call, options);
|
|
172
197
|
return requireResultKind('stream', result).stream;
|
|
173
198
|
}
|
|
174
199
|
async binary(call, options) {
|
|
175
|
-
const result = await this.
|
|
200
|
+
const result = await this.dispatchWith(call, options, true);
|
|
176
201
|
return requireResultKind('binary', result).value;
|
|
177
202
|
}
|
|
178
203
|
close() {
|
|
@@ -2,6 +2,8 @@ import { NodeId } from '@astrale-os/kernel-core/graph/node';
|
|
|
2
2
|
import { Path } from '@astrale-os/kernel-core/path';
|
|
3
3
|
import { K, KernelSchema } from '@astrale-os/kernel-core/schema';
|
|
4
4
|
import { defineSchema, func, method, nodeClass, schema, valueSchema, } from '@astrale-os/kernel-dsl/v1';
|
|
5
|
+
import { isBinaryInput } from '@astrale-os/kernel-dsl/v1/language';
|
|
6
|
+
import { call } from '../client/index.js';
|
|
5
7
|
import { ClientSession, createSessionCredentialProvider, reference } from './index.js';
|
|
6
8
|
void new ClientSession(options);
|
|
7
9
|
void auth;
|
|
@@ -40,7 +42,10 @@ const renameInput = valueSchema()({
|
|
|
40
42
|
required: ['name'],
|
|
41
43
|
additionalProperties: false,
|
|
42
44
|
});
|
|
43
|
-
const
|
|
45
|
+
const registerInput = K.functions.register.input;
|
|
46
|
+
if (isBinaryInput(registerInput))
|
|
47
|
+
throw new TypeError('Register input must be a Value.');
|
|
48
|
+
const registrationInput = valueSchema()(registerInput);
|
|
44
49
|
const booleanOutput = valueSchema()({ type: 'boolean' });
|
|
45
50
|
const typedDomain = schema.resolve(defineSchema('client.input-witness.typecheck', {
|
|
46
51
|
dependencies: { kernel: KernelSchema },
|
|
@@ -68,9 +73,42 @@ const typedDomain = schema.resolve(defineSchema('client.input-witness.typecheck'
|
|
|
68
73
|
const registerReference = reference(typedDomain, typedDomain.functions.registerIdentity);
|
|
69
74
|
const staticRegisterReference = reference(typedDomain, typedDomain.classes.Person.static.methods.registerIdentity);
|
|
70
75
|
const renameReference = reference(typedDomain, typedDomain.classes.Person.methods.rename)(Path.id(NodeId('person')));
|
|
76
|
+
const blobReceipt = session.content
|
|
77
|
+
.upload(blobBytes, { mediaType: 'image/png' })
|
|
78
|
+
.then(({ receipt }) => receipt);
|
|
79
|
+
const boundBlobReceipt = boundSession.content
|
|
80
|
+
.upload(blobBytes, { mediaType: 'image/png' })
|
|
81
|
+
.then(({ receipt }) => receipt);
|
|
82
|
+
void blobReceipt;
|
|
83
|
+
void boundBlobReceipt;
|
|
71
84
|
void session.invoke(registerReference, registration);
|
|
72
85
|
void boundSession.invoke(staticRegisterReference, registration);
|
|
73
86
|
void session.invoke(renameReference, renameRequest);
|
|
87
|
+
const binaryUploadReference = reference(K, K.functions.upload);
|
|
88
|
+
void session.invoke(binaryUploadReference, { mediaType: 'image/png' }, { body: new Blob() });
|
|
89
|
+
void boundSession.invoke(binaryUploadReference, { mediaType: 'image/png' }, { body: new Blob() });
|
|
90
|
+
const rawBinaryCall = call(Path.parse('/:example.test:function.write'), { mediaType: 'image/png' });
|
|
91
|
+
void client.dispatch(rawBinaryCall, { body: new Blob() });
|
|
92
|
+
void client.as('credential').call(rawBinaryCall, { body: new Blob() });
|
|
93
|
+
// @ts-expect-error Binary Functions require a byte source.
|
|
94
|
+
void session.invoke(binaryUploadReference, { mediaType: 'image/png' });
|
|
95
|
+
// @ts-expect-error Value Functions reject binary bodies.
|
|
96
|
+
void session.invoke(registerReference, registration, { body: new Blob() });
|
|
97
|
+
// @ts-expect-error Bound binary Functions require a byte source.
|
|
98
|
+
void boundSession.invoke(binaryUploadReference, { mediaType: 'image/png' });
|
|
99
|
+
// @ts-expect-error Bound value Functions reject binary bodies.
|
|
100
|
+
void boundSession.invoke(registerReference, registration, { body: new Blob() });
|
|
101
|
+
void session.invoke(binaryUploadReference, { mediaType: 'image/png' },
|
|
102
|
+
// @ts-expect-error Binary invocations cannot carry an idempotency key.
|
|
103
|
+
{ body: new Blob(), idempotencyKey: 'retry' });
|
|
104
|
+
// @ts-expect-error Raw Session binary calls cannot carry an idempotency key.
|
|
105
|
+
void session.dispatch(rawBinaryCall, { body: new Blob(), idempotencyKey: 'retry' });
|
|
106
|
+
// @ts-expect-error Raw Client binary calls cannot carry an idempotency key.
|
|
107
|
+
void client.dispatch(rawBinaryCall, { body: new Blob(), idempotencyKey: 'retry' });
|
|
108
|
+
// @ts-expect-error Authenticated Client binary calls cannot carry an idempotency key.
|
|
109
|
+
void client.as('credential').call(rawBinaryCall, { body: new Blob(), idempotencyKey: 'retry' });
|
|
110
|
+
void session.dispatch({ target: Path.parse('/:example.test:function.write'), input: {} }, { body: new Blob() });
|
|
111
|
+
void session.call({ target: Path.parse('/:example.test:function.write'), input: {} }, { body: new Blob() });
|
|
74
112
|
const registerInputIsRequest = true;
|
|
75
113
|
const renameInputIsInterface = true;
|
|
76
114
|
const renameOutputIsBoolean = true;
|
|
@@ -3,12 +3,13 @@ import type { ControlledSignal } from '../index.js';
|
|
|
3
3
|
export declare class HttpByteStream implements AsyncIterable<Uint8Array> {
|
|
4
4
|
private readonly maximumBytes;
|
|
5
5
|
private readonly control;
|
|
6
|
+
private readonly expectedBytes?;
|
|
6
7
|
private readonly reader;
|
|
7
8
|
private claimed;
|
|
8
9
|
private terminal;
|
|
9
10
|
private bytes;
|
|
10
11
|
private interruption;
|
|
11
|
-
constructor(body: ReadableStream<Uint8Array>, maximumBytes: number, control: ControlledSignal);
|
|
12
|
+
constructor(body: ReadableStream<Uint8Array>, maximumBytes: number, control: ControlledSignal, expectedBytes?: number | undefined);
|
|
12
13
|
[Symbol.asyncIterator](): AsyncIterator<Uint8Array>;
|
|
13
14
|
cancel(reason?: unknown): Promise<void>;
|
|
14
15
|
private next;
|
|
@@ -4,14 +4,16 @@ import { asError } from '../index.js';
|
|
|
4
4
|
export class HttpByteStream {
|
|
5
5
|
maximumBytes;
|
|
6
6
|
control;
|
|
7
|
+
expectedBytes;
|
|
7
8
|
reader;
|
|
8
9
|
claimed = false;
|
|
9
10
|
terminal = false;
|
|
10
11
|
bytes = 0;
|
|
11
12
|
interruption;
|
|
12
|
-
constructor(body, maximumBytes, control) {
|
|
13
|
+
constructor(body, maximumBytes, control, expectedBytes) {
|
|
13
14
|
this.maximumBytes = maximumBytes;
|
|
14
15
|
this.control = control;
|
|
16
|
+
this.expectedBytes = expectedBytes;
|
|
15
17
|
this.reader = body.getReader();
|
|
16
18
|
const abort = () => void this.interrupt(control.signal.reason);
|
|
17
19
|
if (control.signal.aborted)
|
|
@@ -53,11 +55,17 @@ export class HttpByteStream {
|
|
|
53
55
|
if (this.interruption !== undefined)
|
|
54
56
|
throw this.interruption;
|
|
55
57
|
if (next.done) {
|
|
58
|
+
if (this.expectedBytes !== undefined && this.bytes !== this.expectedBytes) {
|
|
59
|
+
throw new ProtocolError(`streaming binary ended after ${this.bytes} bytes; expected ${this.expectedBytes}.`);
|
|
60
|
+
}
|
|
56
61
|
this.terminal = true;
|
|
57
62
|
this.control.close();
|
|
58
63
|
return { done: true, value: undefined };
|
|
59
64
|
}
|
|
60
65
|
this.bytes += next.value.byteLength;
|
|
66
|
+
if (this.expectedBytes !== undefined && this.bytes > this.expectedBytes) {
|
|
67
|
+
throw new ProtocolError(`streaming binary exceeded declared Content-Length (${this.expectedBytes}).`);
|
|
68
|
+
}
|
|
61
69
|
if (this.bytes > this.maximumBytes) {
|
|
62
70
|
throw new ProtocolError(`streaming binary exceeds maximumResponseBytes (${this.maximumBytes}).`);
|
|
63
71
|
}
|
|
@@ -25,6 +25,9 @@ export declare class HttpTransport implements Transport {
|
|
|
25
25
|
private nextId;
|
|
26
26
|
constructor(options: HttpTransportOptions);
|
|
27
27
|
dispatch(input: TransportRequest, options?: RequestOptions): Promise<TransportResult>;
|
|
28
|
+
/** HTTP-only binary invocation; each call may replay a re-readable Blob. */
|
|
29
|
+
dispatchBinary(input: TransportRequest, body: Blob, options?: RequestOptions): Promise<TransportResult>;
|
|
30
|
+
private dispatchWith;
|
|
28
31
|
private fetchResponse;
|
|
29
32
|
private decodeBinary;
|
|
30
33
|
private nextRequestId;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { canonicalMediaType } from '@astrale-os/kernel-protocol/artifact';
|
|
1
2
|
import * as codecs from '@astrale-os/kernel-protocol/codecs';
|
|
2
3
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
3
4
|
import { ProtocolError, ResponseError, TransportError } from '../../errors/index.js';
|
|
4
5
|
import { invocationFailure } from '../failure.js';
|
|
5
6
|
import { asError, controlledSignal, positiveInteger, requireLive, } from '../index.js';
|
|
6
7
|
import { HttpByteStream } from './byte-stream.js';
|
|
7
|
-
import { binaryHeaders, codecForResponse, decodeProtocol,
|
|
8
|
+
import { binaryHeaders, codecForResponse, decodeProtocol, readBoundedBody, requireId, } from './response.js';
|
|
8
9
|
import { HttpStream } from './stream.js';
|
|
9
10
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
10
11
|
const DEFAULT_MAXIMUM_REQUEST_BYTES = 16 * 1024 * 1024;
|
|
@@ -32,6 +33,18 @@ export class HttpTransport {
|
|
|
32
33
|
this.maximumResponseBytes = positiveInteger(options.maximumResponseBytes ?? DEFAULT_MAXIMUM_RESPONSE_BYTES, 'maximumResponseBytes');
|
|
33
34
|
}
|
|
34
35
|
async dispatch(input, options = {}) {
|
|
36
|
+
return this.dispatchWith(input, options);
|
|
37
|
+
}
|
|
38
|
+
/** HTTP-only binary invocation; each call may replay a re-readable Blob. */
|
|
39
|
+
async dispatchBinary(input, body, options = {}) {
|
|
40
|
+
if (!(body instanceof Blob))
|
|
41
|
+
throw new TypeError('Binary request body must be a Blob.');
|
|
42
|
+
if (options.idempotencyKey !== undefined) {
|
|
43
|
+
throw new TypeError('Binary requests cannot carry an idempotency key.');
|
|
44
|
+
}
|
|
45
|
+
return this.dispatchWith(input, options, body);
|
|
46
|
+
}
|
|
47
|
+
async dispatchWith(input, options, binaryBody) {
|
|
35
48
|
const requestId = this.nextRequestId();
|
|
36
49
|
const timeoutMs = positiveInteger(options.timeoutMs ?? this.timeoutMs, 'timeoutMs');
|
|
37
50
|
const codec = await this.requestCodec();
|
|
@@ -43,19 +56,21 @@ export class HttpTransport {
|
|
|
43
56
|
...invocationInput,
|
|
44
57
|
...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),
|
|
45
58
|
});
|
|
46
|
-
const encoded =
|
|
59
|
+
const encoded = binaryBody === undefined
|
|
60
|
+
? codec.encode(request)
|
|
61
|
+
: protocol.http.encodeBinaryRequestEnvelope(request);
|
|
47
62
|
const control = controlledSignal(options.signal, timeoutMs);
|
|
48
63
|
let handedOff = false;
|
|
49
64
|
let phase = 'connect';
|
|
50
65
|
let delivery = 'not-sent';
|
|
51
66
|
try {
|
|
52
67
|
requireLive(control);
|
|
53
|
-
if (encodedByteLength(encoded) > this.maximumRequestBytes) {
|
|
68
|
+
if (binaryBody === undefined && encodedByteLength(encoded) > this.maximumRequestBytes) {
|
|
54
69
|
throw invocationFailure(`request exceeds maximumRequestBytes (${this.maximumRequestBytes}).`, 'send');
|
|
55
70
|
}
|
|
56
71
|
phase = 'send';
|
|
57
72
|
delivery = 'unknown';
|
|
58
|
-
const response = await this.fetchResponse(codec, encoded, trace, control);
|
|
73
|
+
const response = await this.fetchResponse(codec, encoded, trace, control, binaryBody);
|
|
59
74
|
phase = 'receive';
|
|
60
75
|
requireLive(control);
|
|
61
76
|
if (response.status >= 300 && response.status < 400) {
|
|
@@ -143,20 +158,27 @@ export class HttpTransport {
|
|
|
143
158
|
control.close();
|
|
144
159
|
}
|
|
145
160
|
}
|
|
146
|
-
async fetchResponse(codec, encoded, trace, control) {
|
|
161
|
+
async fetchResponse(codec, encoded, trace, control, binaryBody) {
|
|
147
162
|
const init = {
|
|
148
163
|
method: 'POST',
|
|
149
164
|
redirect: 'manual',
|
|
150
165
|
credentials: 'omit',
|
|
151
166
|
cache: 'no-store',
|
|
152
167
|
referrerPolicy: 'no-referrer',
|
|
168
|
+
// Fetch owns Content-Length; the protocol carries Blob.size across Fetch runtimes.
|
|
153
169
|
headers: {
|
|
154
170
|
...this.headers,
|
|
155
171
|
...protocol.http.encodeTraceHeaders(trace),
|
|
156
|
-
'content-type': codec.contentType,
|
|
172
|
+
'content-type': binaryBody === undefined ? codec.contentType : protocol.http.BINARY_REQUEST_CONTENT_TYPE,
|
|
157
173
|
accept: codec.contentType,
|
|
174
|
+
...(binaryBody === undefined
|
|
175
|
+
? {}
|
|
176
|
+
: {
|
|
177
|
+
[protocol.http.REQUEST_ENVELOPE_FIELD]: encoded,
|
|
178
|
+
[protocol.http.BINARY_LENGTH_FIELD]: String(binaryBody.size),
|
|
179
|
+
}),
|
|
158
180
|
},
|
|
159
|
-
body: toBody(encoded),
|
|
181
|
+
body: binaryBody ?? toBody(encoded),
|
|
160
182
|
signal: control.signal,
|
|
161
183
|
};
|
|
162
184
|
return this.fetchImpl(this.url, init);
|
|
@@ -165,22 +187,32 @@ export class HttpTransport {
|
|
|
165
187
|
if (binaryId !== String(requestId)) {
|
|
166
188
|
throw new ProtocolError(`binary response requestId ${JSON.stringify(binaryId)} does not match request requestId ${requestId}.`);
|
|
167
189
|
}
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
190
|
+
const contentType = response.headers.get('content-type');
|
|
191
|
+
if (contentType === null) {
|
|
170
192
|
throw new ProtocolError('binary response has no media type.');
|
|
171
193
|
}
|
|
194
|
+
const mediaType = decodeProtocol('binary media type', () => canonicalMediaType(contentType));
|
|
172
195
|
const headerNames = decodeProtocol('binary application-header manifest', () => protocol.http.decodeBinaryHeaderNames(response.headers.get(protocol.http.BINARY_HEADERS_FIELD)));
|
|
173
196
|
const headers = binaryHeaders(response.headers, headerNames);
|
|
174
197
|
const invocation = decodeProtocol('binary InvocationId field', () => protocol.http.decodeInvocationId(response.headers.get(protocol.http.INVOCATION_FIELD)));
|
|
175
198
|
if (streaming) {
|
|
176
199
|
if (response.body === null)
|
|
177
200
|
throw new ProtocolError('streaming binary has no body.');
|
|
201
|
+
const length = response.headers.get('content-length');
|
|
202
|
+
const expectedBytes = length === null ? undefined : Number(length);
|
|
203
|
+
if (length !== null &&
|
|
204
|
+
(!/^(?:0|[1-9][0-9]*)$/u.test(length) || !Number.isSafeInteger(expectedBytes))) {
|
|
205
|
+
throw new ProtocolError('streaming binary has an invalid Content-Length.');
|
|
206
|
+
}
|
|
207
|
+
if (expectedBytes !== undefined && expectedBytes > this.maximumResponseBytes) {
|
|
208
|
+
throw new ProtocolError(`streaming binary exceeds maximumResponseBytes (${this.maximumResponseBytes}).`);
|
|
209
|
+
}
|
|
178
210
|
requireLive(control);
|
|
179
211
|
return {
|
|
180
212
|
kind: 'binary',
|
|
181
213
|
invocation,
|
|
182
214
|
value: {
|
|
183
|
-
body: new HttpByteStream(response.body, this.maximumResponseBytes, control),
|
|
215
|
+
body: new HttpByteStream(response.body, this.maximumResponseBytes, control, expectedBytes),
|
|
184
216
|
mediaType,
|
|
185
217
|
...(response.status === 200 ? {} : { status: response.status }),
|
|
186
218
|
...(Object.keys(headers).length === 0 ? {} : { headers }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/kernel-client",
|
|
3
|
-
"version": "0.6.0-beta.
|
|
3
|
+
"version": "0.6.0-beta.72",
|
|
4
4
|
"description": "Universal Client — protocol-first, transport-agnostic",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astrale",
|
|
@@ -65,14 +65,14 @@
|
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@astrale-os/kernel-core": "0.9.0-beta.
|
|
69
|
-
"@astrale-os/kernel-dsl": "0.2.0-beta.
|
|
70
|
-
"@astrale-os/kernel-protocol": "0.5.0-beta.
|
|
68
|
+
"@astrale-os/kernel-core": "0.9.0-beta.55",
|
|
69
|
+
"@astrale-os/kernel-dsl": "0.2.0-beta.40",
|
|
70
|
+
"@astrale-os/kernel-protocol": "0.5.0-beta.61",
|
|
71
71
|
"@tanstack/pacer": "0.20.1",
|
|
72
72
|
"msgpackr": "^2.0.5"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@astrale-os/kernel-server": "0.5.0-beta.
|
|
75
|
+
"@astrale-os/kernel-server": "0.5.0-beta.63",
|
|
76
76
|
"@astrale/typescript-config": "npm:@jsr/astrale__typescript-config@~1.1.0",
|
|
77
77
|
"@types/node": "^22.0.0",
|
|
78
78
|
"typescript": "7.0.2",
|