@astrale-os/kernel-client 0.6.0-beta.70 → 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/auth.d.ts +17 -5
- package/dist/auth/auth.js +3 -4
- package/dist/auth/credential.d.ts +5 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +0 -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/admission.d.ts +18 -0
- package/dist/session/admission.js +117 -0
- package/dist/session/bound.d.ts +12 -9
- package/dist/session/bound.js +24 -5
- 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/discovery/index.d.ts +1 -0
- package/dist/session/discovery/index.js +1 -0
- package/dist/session/index.d.ts +3 -2
- package/dist/session/reference.d.ts +7 -3
- package/dist/session/reference.js +1 -1
- 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 +12 -3
- package/dist/session/session.js +58 -13
- package/dist/session/session.typecheck.js +46 -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 +6 -6
package/dist/session/session.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Path } from '@astrale-os/kernel-core/path';
|
|
2
2
|
import { callableFingerprint } from '@astrale-os/kernel-dsl/v1/schema/fingerprint';
|
|
3
|
-
import {
|
|
3
|
+
import { createAuth } from '../auth/auth.js';
|
|
4
|
+
import { Client, acceptCall } from '../client/index.js';
|
|
4
5
|
import { SessionError } from '../errors/index.js';
|
|
5
6
|
import { createDomainBinding } from '../graph/domain/binding.js';
|
|
6
7
|
import { createGraph } from '../graph/index.js';
|
|
@@ -9,10 +10,12 @@ import { createSchema, DEFAULT_SCHEMA_LIFECYCLE_TIMEOUT_MS } from '../schema/ind
|
|
|
9
10
|
import { AutoTransport } from '../transport/auto/index.js';
|
|
10
11
|
import { HttpTransport } from '../transport/http/index.js';
|
|
11
12
|
import { WebSocketTransport } from '../transport/websocket/index.js';
|
|
12
|
-
import {
|
|
13
|
+
import { admitFunction } from './admission.js';
|
|
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';
|
|
13
16
|
import { createSessionDiscovery } from './discovery/index.js';
|
|
14
17
|
import { admitReference } from './reference.js';
|
|
15
|
-
import { admitEnvelopeTransport,
|
|
18
|
+
import { admitEnvelopeTransport, admitInvocationOptions, admitSessionAuth, requireResultKind, RouteCache, SessionDispatcher, } from './routing/index.js';
|
|
16
19
|
import { resolveViewsFor, targetViewCatalog } from './views/index.js';
|
|
17
20
|
const DEFAULT_SESSION_POLICY = Object.freeze({ maximumRouteAgeMs: 60_000 });
|
|
18
21
|
/** Caller-owned discovery and redirect-following conversation with one exact source Kernel. */
|
|
@@ -20,6 +23,8 @@ export class ClientSession {
|
|
|
20
23
|
kernel;
|
|
21
24
|
schema;
|
|
22
25
|
graph;
|
|
26
|
+
auth;
|
|
27
|
+
content;
|
|
23
28
|
ownedPool;
|
|
24
29
|
discovery;
|
|
25
30
|
dispatcher;
|
|
@@ -80,16 +85,39 @@ export class ClientSession {
|
|
|
80
85
|
acquisitionOperation: () => this.operation(undefined),
|
|
81
86
|
});
|
|
82
87
|
this.routes = new RouteCache(policy.maximumRouteAgeMs, undefined, options.routeStore);
|
|
88
|
+
const auth = admitSessionAuth(options.auth);
|
|
89
|
+
const admission = {
|
|
90
|
+
kernel: this.kernel,
|
|
91
|
+
auth,
|
|
92
|
+
source: () => {
|
|
93
|
+
allowEndpoint(sourceEndpoint, policy);
|
|
94
|
+
return pool.clientFor({ transport: 'http', url: sourceEndpoint });
|
|
95
|
+
},
|
|
96
|
+
fetch: fetchImpl,
|
|
97
|
+
policy,
|
|
98
|
+
limits: { maximumPublicationBytes, maximumBundleBytes },
|
|
99
|
+
maximumResponseBytes,
|
|
100
|
+
};
|
|
83
101
|
this.dispatcher = new SessionDispatcher({
|
|
84
102
|
kernel: this.kernel,
|
|
85
103
|
sourceEndpoint,
|
|
86
104
|
pool,
|
|
87
105
|
websocketAvailable,
|
|
88
|
-
auth
|
|
106
|
+
auth,
|
|
89
107
|
cache: this.routes,
|
|
90
108
|
policy,
|
|
91
109
|
envelopeTransport: admitEnvelopeTransport(options.envelopeTransport ?? 'http'),
|
|
92
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),
|
|
93
121
|
});
|
|
94
122
|
this.schema = createSchema((request, options) => this.dispatch(request, options), {
|
|
95
123
|
maximumPublicationBytes,
|
|
@@ -97,6 +125,10 @@ export class ClientSession {
|
|
|
97
125
|
lifecycleTimeoutMs: schemaLifecycleTimeoutMs,
|
|
98
126
|
});
|
|
99
127
|
this.graph = createGraph((call, options) => this.call(call, options));
|
|
128
|
+
this.auth = createAuth((target, input, options) => this.call({ target, input }, options), (call, options) => {
|
|
129
|
+
const operation = this.operation(admitControlOptions(options));
|
|
130
|
+
return admitFunction(admission, acceptCall(call), options, operation);
|
|
131
|
+
});
|
|
100
132
|
}
|
|
101
133
|
/** Bind one admitted DSL Domain to authenticated graph projection and Method dispatch. */
|
|
102
134
|
bind(domain) {
|
|
@@ -114,23 +146,32 @@ export class ClientSession {
|
|
|
114
146
|
return resolveViewsFor(async (call, operation) => requireResultKind('value', await this.dispatcher.dispatch(call, {}, operation)).value, this.admitTargetViews, target, this.operation(admitControlOptions(options)));
|
|
115
147
|
}
|
|
116
148
|
dispatch(call, options) {
|
|
117
|
-
|
|
118
|
-
return this.dispatcher.dispatch(call, admitted, this.operation(admitted));
|
|
149
|
+
return this.dispatchWith(call, options);
|
|
119
150
|
}
|
|
120
151
|
async call(call, options) {
|
|
121
|
-
const result = await this.
|
|
152
|
+
const result = await this.dispatchWith(call, options);
|
|
122
153
|
return requireResultKind('value', result).value;
|
|
123
154
|
}
|
|
124
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) {
|
|
125
166
|
const admittedReference = admitReference(reference);
|
|
126
|
-
const
|
|
167
|
+
const admitted = admitInvocationOptions(options);
|
|
127
168
|
const { anchor, steps } = admittedReference.target.ast;
|
|
128
169
|
const last = steps.at(-1);
|
|
129
170
|
const instance = last?.kind === 'method' && last.dispatch === 'instance'
|
|
130
171
|
? Path.fromAST({ anchor, steps: steps.slice(0, -1) }).raw
|
|
131
172
|
: undefined;
|
|
132
173
|
// A generated reference pins the callable meaning; a hand-written one still pins the revision.
|
|
133
|
-
const
|
|
174
|
+
const call = {
|
|
134
175
|
target: Object.freeze({
|
|
135
176
|
callable: admittedReference.callable,
|
|
136
177
|
...(admittedReference.fingerprint === undefined
|
|
@@ -139,20 +180,24 @@ export class ClientSession {
|
|
|
139
180
|
...(instance === undefined ? {} : { instance }),
|
|
140
181
|
}),
|
|
141
182
|
input,
|
|
142
|
-
}
|
|
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));
|
|
143
188
|
const narrowed = requireResultKind(admittedReference.outputMode, result);
|
|
144
|
-
return
|
|
189
|
+
return narrowed.kind === 'value'
|
|
145
190
|
? narrowed.value
|
|
146
191
|
: narrowed.kind === 'stream'
|
|
147
192
|
? narrowed.stream
|
|
148
|
-
: narrowed.value
|
|
193
|
+
: narrowed.value;
|
|
149
194
|
}
|
|
150
195
|
async stream(call, options) {
|
|
151
196
|
const result = await this.dispatch(call, options);
|
|
152
197
|
return requireResultKind('stream', result).stream;
|
|
153
198
|
}
|
|
154
199
|
async binary(call, options) {
|
|
155
|
-
const result = await this.
|
|
200
|
+
const result = await this.dispatchWith(call, options, true);
|
|
156
201
|
return requireResultKind('binary', result).value;
|
|
157
202
|
}
|
|
158
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;
|
|
@@ -12,6 +14,13 @@ void request;
|
|
|
12
14
|
void snapshot;
|
|
13
15
|
const observedPublication = session.publication(control);
|
|
14
16
|
void observedPublication;
|
|
17
|
+
const admitted = session.auth.admit({
|
|
18
|
+
target: Path.parse('/:example.test:function.write'),
|
|
19
|
+
input: {},
|
|
20
|
+
}, { attenuation: { kind: 'identity', self: true }, ...control });
|
|
21
|
+
void admitted;
|
|
22
|
+
// @ts-expect-error Admission belongs to Auth, not the top-level Session.
|
|
23
|
+
void session.admit({ target: Path.parse('/:example.test:function.write'), input: {} });
|
|
15
24
|
const credentials = createSessionCredentialProvider({
|
|
16
25
|
ttlSeconds: 60,
|
|
17
26
|
mint: async () => minted,
|
|
@@ -33,7 +42,10 @@ const renameInput = valueSchema()({
|
|
|
33
42
|
required: ['name'],
|
|
34
43
|
additionalProperties: false,
|
|
35
44
|
});
|
|
36
|
-
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);
|
|
37
49
|
const booleanOutput = valueSchema()({ type: 'boolean' });
|
|
38
50
|
const typedDomain = schema.resolve(defineSchema('client.input-witness.typecheck', {
|
|
39
51
|
dependencies: { kernel: KernelSchema },
|
|
@@ -61,9 +73,42 @@ const typedDomain = schema.resolve(defineSchema('client.input-witness.typecheck'
|
|
|
61
73
|
const registerReference = reference(typedDomain, typedDomain.functions.registerIdentity);
|
|
62
74
|
const staticRegisterReference = reference(typedDomain, typedDomain.classes.Person.static.methods.registerIdentity);
|
|
63
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;
|
|
64
84
|
void session.invoke(registerReference, registration);
|
|
65
85
|
void boundSession.invoke(staticRegisterReference, registration);
|
|
66
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() });
|
|
67
112
|
const registerInputIsRequest = true;
|
|
68
113
|
const renameInputIsInterface = true;
|
|
69
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,18 +65,18 @@
|
|
|
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",
|
|
79
|
-
"vitest": "^
|
|
79
|
+
"vitest": "^5.0.1"
|
|
80
80
|
},
|
|
81
81
|
"scripts": {
|
|
82
82
|
"build": "tsc",
|