@astrale-os/kernel-client 0.6.0-beta.17 → 0.6.0-beta.18

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.
@@ -1,10 +1,10 @@
1
- import type { BinaryResult, CallableResult } from '@astrale-os/kernel-core/callable';
1
+ import type { BinaryOutput, CallableResult } from '@astrale-os/kernel-core/callable';
2
2
  import type { Value } from '@astrale-os/kernel-dsl/value';
3
3
  import type { invocation as protocol } from '@astrale-os/kernel-protocol';
4
4
  /** Caller-visible value accepted from one admitted remote invocation. */
5
5
  export type Input = Value;
6
- /** Caller-visible finite binary response. */
7
- export type Binary = BinaryResult;
6
+ /** Caller-visible buffered or streaming binary response. */
7
+ export type Binary = BinaryOutput;
8
8
  /** Caller-owned remote stream. */
9
9
  export interface Stream extends AsyncIterable<Value> {
10
10
  cancel(reason?: string): Promise<void>;
@@ -24,5 +24,8 @@ async function rejectMismatch(expected, result) {
24
24
  if (result.kind === 'stream') {
25
25
  await result.stream.cancel(`Expected ${expected} Function result.`).catch(() => undefined);
26
26
  }
27
+ if (result.kind === 'binary' && !(result.value.body instanceof Uint8Array)) {
28
+ await result.value.body[Symbol.asyncIterator]().return?.();
29
+ }
27
30
  mismatch(expected, result.kind);
28
31
  }
@@ -1,11 +1,11 @@
1
1
  import type { installation } from '@astrale-os/kernel-core/schema';
2
- import type { DesiredInstallation as CoreDesiredInstallation, InstallInput, InstallOutput, UninstallInput, UninstallOutput } from '@astrale-os/kernel-core/schema/kernel/syscalls/contracts';
2
+ import type { DomainRequest as CoreDomainRequest, InstallInput, InstallReceipt as CoreInstallReceipt, UninstallInput, UninstallOutput } from '@astrale-os/kernel-core/schema/kernel/syscalls/contracts';
3
3
  export type OperationId = installation.OperationId;
4
4
  /** Canonical caller input accepted by the Kernel install syscall. */
5
- export type DesiredInstallation = CoreDesiredInstallation;
5
+ export type DomainRequest = CoreDomainRequest;
6
6
  export type InstallRequest = InstallInput;
7
7
  /** Admitted terminal install receipt returned by the Kernel. */
8
- export type InstallReceipt = InstallOutput;
8
+ export type InstallReceipt = CoreInstallReceipt;
9
9
  /** Canonical caller input accepted by the Kernel uninstall syscall. */
10
10
  export type UninstallRequest = UninstallInput;
11
11
  /** Admitted terminal uninstall receipt returned by the Kernel. */
@@ -1,3 +1,3 @@
1
- export { createSchema, type SchemaApi, type SchemaCall, type SchemaOptions, type ReadyInstallation, } from './schema.js';
1
+ export { createSchema, type SchemaApi, type SchemaCall, type SchemaOptions, type DomainBundle, type DomainInfo, type InstallResult, } from './schema.js';
2
2
  export { acceptOperationId, createOperationId } from './operation.js';
3
- export type { DesiredInstallation, InstallReceipt, InstallRequest, OperationId, UninstallReceipt, UninstallRequest, } from './contracts.js';
3
+ export type { DomainRequest, InstallReceipt, InstallRequest, OperationId, UninstallReceipt, UninstallRequest, } from './contracts.js';
@@ -11,5 +11,9 @@ export async function introspect(invoke, limits, request, options) {
11
11
  if (result.value.mediaType !== schema.installation.introspection.MEDIA_TYPE) {
12
12
  throw new TypeError('Schema introspection returned an unsupported media type.');
13
13
  }
14
+ if (!(result.value.body instanceof Uint8Array)) {
15
+ await result.value.body[Symbol.asyncIterator]().return?.();
16
+ throw new TypeError('Schema introspection returned a streaming binary result.');
17
+ }
14
18
  return schema.installation.introspection.decode(request, result.value.body, limits);
15
19
  }
@@ -1,19 +1,19 @@
1
- import type { introspection } from '@astrale-os/kernel-core/schema';
2
- import type * as protocolSchema from '@astrale-os/kernel-protocol/schema';
1
+ import type { Key } from '@astrale-os/kernel-dsl/v1';
2
+ import * as protocolSchema from '@astrale-os/kernel-protocol/schema';
3
3
  import type { Call, Result } from '../client/index.js';
4
4
  import type { RequestOptions } from '../transport/index.js';
5
- import type { InstallReceipt, InstallRequest, UninstallReceipt, UninstallRequest } from './contracts.js';
5
+ import type { InstallRequest, UninstallReceipt, UninstallRequest } from './contracts.js';
6
6
  export type SchemaCall = (call: Call, options?: RequestOptions) => Promise<Result>;
7
7
  export interface SchemaOptions extends protocolSchema.installation.introspection.Limits {
8
8
  }
9
- export type ReadyInstallation = protocolSchema.installation.introspection.Installation & {
10
- readonly state: 'ready';
11
- readonly readiness: protocolSchema.installation.introspection.Digest;
12
- };
9
+ export type DomainInfo = protocolSchema.installation.introspection.DomainInfo;
10
+ export type DomainBundle = protocolSchema.installation.introspection.DomainBundle;
11
+ export type InstallResult = protocolSchema.installation.InstallResult;
13
12
  export interface SchemaApi {
14
- install(request: InstallRequest, options?: RequestOptions): Promise<InstallReceipt>;
13
+ install(request: InstallRequest, options?: RequestOptions): Promise<InstallResult>;
15
14
  uninstall(request: UninstallRequest, options?: RequestOptions): Promise<UninstallReceipt>;
16
- introspect<Input extends introspection.Introspection>(request: Input, options?: RequestOptions): Promise<protocolSchema.installation.introspection.ResultFor<Input>>;
15
+ inspect(origin: Key.Origin, options?: RequestOptions): Promise<DomainInfo>;
16
+ bundle(origin: Key.Origin, options?: RequestOptions): Promise<DomainBundle>;
17
17
  }
18
18
  /** Bind the canonical Schema lifecycle syscalls to one caller-scoped value-call capability. */
19
19
  export declare function createSchema(call: SchemaCall, limits: SchemaOptions): SchemaApi;
@@ -1,9 +1,10 @@
1
1
  import { Path } from '@astrale-os/kernel-core/path';
2
- import { installOutput, uninstallOutput, } from '@astrale-os/kernel-core/schema/kernel/syscalls/contracts';
2
+ import { uninstallOutput } from '@astrale-os/kernel-core/schema/kernel/syscalls/contracts';
3
3
  import { install, uninstall } from '@astrale-os/kernel-core/schema/kernel/syscalls/references';
4
+ import * as protocolSchema from '@astrale-os/kernel-protocol/schema';
4
5
  import { call as invocation } from '../client/index.js';
5
6
  import { acceptOutput } from './admission.js';
6
- import { introspect as inspect } from './introspection.js';
7
+ import { introspect } from './introspection.js';
7
8
  const INSTALL_PATH = Path.project(install).raw;
8
9
  const UNINSTALL_PATH = Path.project(uninstall).raw;
9
10
  /** Bind the canonical Schema lifecycle syscalls to one caller-scoped value-call capability. */
@@ -13,18 +14,54 @@ export function createSchema(call, limits) {
13
14
  const admittedLimits = admitLimits(limits);
14
15
  return Object.freeze({
15
16
  async install(request, options) {
16
- const output = value(await call(invocation(INSTALL_PATH, request), options));
17
- return acceptOutput(installOutput, output);
17
+ const result = await call(invocation(INSTALL_PATH, request), options);
18
+ if (result.kind !== 'binary') {
19
+ throw new TypeError('Schema install returned a non-binary result.');
20
+ }
21
+ if (result.value.mediaType !== protocolSchema.installation.MEDIA_TYPE) {
22
+ throw new TypeError('Schema install returned an unsupported media type.');
23
+ }
24
+ if (!(result.value.body instanceof Uint8Array)) {
25
+ await result.value.body[Symbol.asyncIterator]().return?.();
26
+ throw new TypeError('Schema install returned a streaming binary result.');
27
+ }
28
+ const decoded = await protocolSchema.installation.decode(result.value.body, installationResultLimit(admittedLimits.maximumPublicationBytes, request.domains.length));
29
+ proveInstallResult(request, decoded);
30
+ return decoded;
18
31
  },
19
32
  async uninstall(request, options) {
20
33
  const output = value(await call(invocation(UNINSTALL_PATH, request), options));
21
34
  return acceptOutput(uninstallOutput, output);
22
35
  },
23
- async introspect(request, options) {
24
- return inspect(call, admittedLimits, request, options);
36
+ async inspect(origin, options) {
37
+ return introspect(call, admittedLimits, { kind: 'inspect', origin }, options);
38
+ },
39
+ async bundle(origin, options) {
40
+ return introspect(call, admittedLimits, { kind: 'bundle', origin }, options);
25
41
  },
26
42
  });
27
43
  }
44
+ function installationResultLimit(maximumDomainBytes, roots) {
45
+ const perDomain = maximumDomainBytes + 4_096;
46
+ if (roots > Math.floor((Number.MAX_SAFE_INTEGER - 4_096) / perDomain)) {
47
+ return Number.MAX_SAFE_INTEGER;
48
+ }
49
+ return perDomain * roots + 4_096;
50
+ }
51
+ function proveInstallResult(request, result) {
52
+ if (result.changed) {
53
+ if (result.receipt.operation !== request.operation) {
54
+ throw new TypeError('Schema install result does not match the requested operation.');
55
+ }
56
+ if (result.receipt.transitions.length > request.domains.length) {
57
+ throw new TypeError('Schema install result contains more transitions than requested Domains.');
58
+ }
59
+ return;
60
+ }
61
+ if (result.domains.length !== request.domains.length) {
62
+ throw new TypeError('Schema unchanged result does not match the requested Domain count.');
63
+ }
64
+ }
28
65
  function value(result) {
29
66
  if (result.kind !== 'value')
30
67
  throw new TypeError('Schema syscall returned a non-value result.');
@@ -1,5 +1,4 @@
1
1
  import type { IssuerId } from '@astrale-os/kernel-core/auth';
2
- import type { ReadyInstallation } from '../schema/index.js';
3
2
  import type { SessionControlOptions } from './context/index.js';
4
3
  import type { CallableReference, InputOfReference, OutputOfReference } from './reference.js';
5
4
  import type { SessionRequestOptions } from './routing/index.js';
@@ -15,7 +14,6 @@ export interface BoundSessionSource {
15
14
  invoke<const Reference extends CallableReference>(reference: Reference, input: InputOfReference<Reference>, options?: SessionRequestOptions): Promise<OutputOfReference<Reference>>;
16
15
  stream(call: Call, options?: SessionRequestOptions): Promise<Stream>;
17
16
  binary(call: Call, options?: SessionRequestOptions): Promise<Binary>;
18
- installation(origin: string, options?: SessionControlOptions): Promise<ReadyInstallation>;
19
17
  viewsFor(target: Parameters<import('./session.js').ClientSession['viewsFor']>[0], options?: SessionControlOptions): Promise<ViewCatalog>;
20
18
  }
21
19
  export interface BoundSessionControls {
@@ -37,7 +35,6 @@ export interface BoundClientSession extends GraphApi {
37
35
  invoke<const Reference extends CallableReference>(reference: Reference, input: InputOfReference<Reference>, options?: SessionRequestOptions): Promise<OutputOfReference<Reference>>;
38
36
  stream(call: Call, options?: SessionRequestOptions): Promise<Stream>;
39
37
  binary(call: Call, options?: SessionRequestOptions): Promise<Binary>;
40
- installation(origin: string, options?: SessionControlOptions): Promise<ReadyInstallation>;
41
38
  viewsFor: BoundSessionSource['viewsFor'];
42
39
  }
43
40
  /** Capture one Session into the ordinary graph, auth, and invocation Kernel journey. */
@@ -11,14 +11,12 @@ export function bindSession(source, controls) {
11
11
  const invokeSource = capture(source, 'invoke');
12
12
  const streamSource = capture(source, 'stream');
13
13
  const binarySource = capture(source, 'binary');
14
- const installationSource = capture(source, 'installation');
15
14
  const viewsForSource = capture(source, 'viewsFor');
16
15
  const dispatch = (input, options) => dispatchSource(input, requestOptions(options, admittedControls));
17
16
  const call = (input, options) => callSource(input, requestOptions(options, admittedControls));
18
17
  const invoke = (reference, input, options) => invokeSource(reference, input, requestOptions(options, admittedControls));
19
18
  const stream = (input, options) => streamSource(input, requestOptions(options, admittedControls));
20
19
  const binary = (input, options) => binarySource(input, requestOptions(options, admittedControls));
21
- const installation = (origin, options) => installationSource(origin, controlOptions(options, admittedControls));
22
20
  const viewsFor = (target, options) => viewsForSource(target, controlOptions(options, admittedControls));
23
21
  const graph = createGraph(call);
24
22
  const auth = createAuth((target, input, options) => call(createCall(target, input), options));
@@ -31,7 +29,6 @@ export function bindSession(source, controls) {
31
29
  invoke,
32
30
  stream,
33
31
  binary,
34
- installation,
35
32
  viewsFor,
36
33
  });
37
34
  }
@@ -1,6 +1,5 @@
1
1
  export type { SessionControlOptions, SessionPolicy } from './context/index.js';
2
2
  export { bindSession, type BoundClientSession, type BoundSessionControls, type BoundSessionSource, } from './bound.js';
3
- export type { ReadyInstallation } from '../schema/index.js';
4
3
  export type { SessionSnapshot } from './discovery/index.js';
5
4
  export type { Auth, SessionAuth, SessionEnvelopeTransport, SessionRequestOptions, } from './routing/index.js';
6
5
  export { ClientSession, connect, type ClientSessionOptions, type ConnectOptions, } from './session.js';
@@ -2,7 +2,7 @@ import type { Path as PathValue, PathLike } from '@astrale-os/kernel-core/path';
2
2
  import type { CallableInputOf, CallableOutputOf, CallableResultOf, Domain, MethodInheritance, ResolvedFunction, ResolvedMethod } from '@astrale-os/kernel-dsl/v1';
3
3
  import type { Revision } from '@astrale-os/kernel-dsl/v1/addressing';
4
4
  import { type Key as KeyType } from '@astrale-os/kernel-dsl/v1/addressing';
5
- import type { Binary, Input, Result } from '../client/index.js';
5
+ import type { Binary, Input } from '../client/index.js';
6
6
  declare const REFERENCE_INPUT: unique symbol;
7
7
  declare const REFERENCE_OUTPUT: unique symbol;
8
8
  export interface CallableReference<InputValue extends Input = Input, Output = unknown> {
@@ -10,7 +10,7 @@ export interface CallableReference<InputValue extends Input = Input, Output = un
10
10
  readonly revision: Revision;
11
11
  readonly callable: KeyType.Callable;
12
12
  readonly target: PathValue;
13
- readonly outputMode: Exclude<Result['kind'], 'redirect'>;
13
+ readonly outputMode: 'value' | 'stream' | 'binary';
14
14
  readonly [REFERENCE_INPUT]?: InputValue;
15
15
  readonly [REFERENCE_OUTPUT]?: Output;
16
16
  }
@@ -19,7 +19,7 @@ export interface InstanceCallableReference<InputValue extends Input = Input, Out
19
19
  readonly origin: KeyType.Origin;
20
20
  readonly revision: Revision;
21
21
  readonly callable: KeyType.Callable;
22
- readonly outputMode: Exclude<Result['kind'], 'redirect'>;
22
+ readonly outputMode: 'value' | 'stream' | 'binary';
23
23
  }
24
24
  export type InputOfReference<Reference extends CallableReference> = Reference extends CallableReference<infer InputValue, unknown> ? InputValue : never;
25
25
  export type OutputOfReference<Reference extends CallableReference> = Reference extends CallableReference<infer _Input, infer Output> ? Output : never;
@@ -123,6 +123,9 @@ export function requireResultKind(expected, result) {
123
123
  return result;
124
124
  if (result.kind === 'stream')
125
125
  void result.stream.cancel(`Expected ${expected} result.`);
126
+ if (result.kind === 'binary' && !(result.value.body instanceof Uint8Array)) {
127
+ void result.value.body[Symbol.asyncIterator]().return?.();
128
+ }
126
129
  throw new ClientError(`Function returned ${result.kind}; ${expected} was required.`);
127
130
  }
128
131
  function authenticated(client, credential) {
@@ -1,11 +1,10 @@
1
1
  import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
2
2
  import type { PathLike } from '@astrale-os/kernel-core/path';
3
- import type { Key } from '@astrale-os/kernel-dsl/v1';
4
3
  import type { Domain } from '@astrale-os/kernel-dsl/v1/domain';
5
4
  import type { Call, Input, Result } from '../client/index.js';
6
5
  import type { DomainBinding, GraphApi } from '../graph/index.js';
7
6
  import type { ClientFactory, ClientPool as ClientPoolType } from '../pool/index.js';
8
- import type { ReadyInstallation, SchemaApi } from '../schema/index.js';
7
+ import type { SchemaApi } from '../schema/index.js';
9
8
  import type { ClientWarning } from '../transport/auto/index.js';
10
9
  import type { Fetch } from '../transport/http/index.js';
11
10
  import type { WebSocketConstructor } from '../transport/websocket/index.js';
@@ -52,12 +51,11 @@ export declare class ClientSession {
52
51
  bind<const DomainValue extends Domain>(domain: DomainValue): DomainBinding<DomainValue>;
53
52
  snapshot(options?: SessionControlOptions): Promise<SessionSnapshot>;
54
53
  viewsFor(target: PathLike, options?: SessionControlOptions): Promise<ViewCatalog>;
55
- installation(origin: Key.Origin, options?: SessionControlOptions): Promise<ReadyInstallation>;
56
54
  dispatch(call: Call, options?: SessionRequestOptions): Promise<Result>;
57
55
  call(call: Call, options?: SessionRequestOptions): Promise<Input>;
58
56
  invoke<const Reference extends CallableReference>(reference: Reference, input: InputOfReference<Reference>, options?: SessionRequestOptions): Promise<OutputOfReference<Reference>>;
59
57
  stream(call: Call, options?: SessionRequestOptions): Promise<import("../client/result.js").Stream>;
60
- binary(call: Call, options?: SessionRequestOptions): Promise<import("@astrale-os/kernel-core/callable").BinaryResult>;
58
+ binary(call: Call, options?: SessionRequestOptions): Promise<import("@astrale-os/kernel-core/callable").BinaryOutput>;
61
59
  close(): void;
62
60
  private requireOpen;
63
61
  private invokeDomainMethod;
@@ -1,6 +1,6 @@
1
1
  import { Path } from '@astrale-os/kernel-core/path';
2
2
  import { Client } from '../client/index.js';
3
- import { ClientError, SessionError } from '../errors/index.js';
3
+ import { SessionError } from '../errors/index.js';
4
4
  import { createDomainBinding } from '../graph/domain/binding.js';
5
5
  import { createGraph } from '../graph/index.js';
6
6
  import { ClientPool } from '../pool/index.js';
@@ -106,23 +106,6 @@ export class ClientSession {
106
106
  this.requireOpen();
107
107
  return resolveViewsFor(async (call, operation) => requireResultKind('value', await this.dispatcher.dispatch(call, {}, operation)).value, this.admitTargetViews, target, this.operation(admitControlOptions(options)));
108
108
  }
109
- async installation(origin, options) {
110
- const result = await this.schema.introspect({
111
- from: { kind: 'installation', origin },
112
- select: {
113
- state: true,
114
- target: true,
115
- source: true,
116
- bundle: true,
117
- readiness: true,
118
- capabilities: true,
119
- },
120
- }, options);
121
- if (result?.state !== 'ready' || result.readiness === null) {
122
- throw new ClientError(`Domain ${origin} is not ready on this Kernel.`);
123
- }
124
- return result;
125
- }
126
109
  dispatch(call, options) {
127
110
  const admitted = admitRequestOptions(options);
128
111
  return this.dispatcher.dispatch(call, admitted, this.operation(admitted));
@@ -0,0 +1,17 @@
1
+ import type { ControlledSignal } from '../index.js';
2
+ /** Single-consumer HTTP byte stream preserving invocation lifetime and backpressure. */
3
+ export declare class HttpByteStream implements AsyncIterable<Uint8Array> {
4
+ private readonly maximumBytes;
5
+ private readonly control;
6
+ private readonly reader;
7
+ private claimed;
8
+ private terminal;
9
+ private bytes;
10
+ private interruption;
11
+ constructor(body: ReadableStream<Uint8Array>, maximumBytes: number, control: ControlledSignal);
12
+ [Symbol.asyncIterator](): AsyncIterator<Uint8Array>;
13
+ cancel(reason?: unknown): Promise<void>;
14
+ private next;
15
+ private interrupt;
16
+ private cancelReader;
17
+ }
@@ -0,0 +1,86 @@
1
+ import { ProtocolError } from '../../errors/index.js';
2
+ import { asError } from '../index.js';
3
+ /** Single-consumer HTTP byte stream preserving invocation lifetime and backpressure. */
4
+ export class HttpByteStream {
5
+ maximumBytes;
6
+ control;
7
+ reader;
8
+ claimed = false;
9
+ terminal = false;
10
+ bytes = 0;
11
+ interruption;
12
+ constructor(body, maximumBytes, control) {
13
+ this.maximumBytes = maximumBytes;
14
+ this.control = control;
15
+ this.reader = body.getReader();
16
+ const abort = () => void this.interrupt(control.signal.reason);
17
+ if (control.signal.aborted)
18
+ abort();
19
+ else
20
+ control.signal.addEventListener('abort', abort, { once: true });
21
+ }
22
+ [Symbol.asyncIterator]() {
23
+ if (this.claimed)
24
+ throw new TypeError('streaming binary admits exactly one consumer.');
25
+ this.claimed = true;
26
+ return {
27
+ next: () => this.next(),
28
+ return: async () => {
29
+ await this.cancel('streaming binary consumer stopped');
30
+ return { done: true, value: undefined };
31
+ },
32
+ throw: async (error) => {
33
+ await this.cancel('streaming binary consumer failed');
34
+ throw error;
35
+ },
36
+ };
37
+ }
38
+ async cancel(reason = 'streaming binary cancelled') {
39
+ if (this.terminal)
40
+ return;
41
+ this.terminal = true;
42
+ this.control.cancel(reason);
43
+ await this.cancelReader(reason);
44
+ this.control.close();
45
+ }
46
+ async next() {
47
+ if (this.interruption !== undefined)
48
+ throw this.interruption;
49
+ if (this.terminal)
50
+ return { done: true, value: undefined };
51
+ try {
52
+ const next = await this.reader.read();
53
+ if (this.interruption !== undefined)
54
+ throw this.interruption;
55
+ if (next.done) {
56
+ this.terminal = true;
57
+ this.control.close();
58
+ return { done: true, value: undefined };
59
+ }
60
+ this.bytes += next.value.byteLength;
61
+ if (this.bytes > this.maximumBytes) {
62
+ throw new ProtocolError(`streaming binary exceeds maximumResponseBytes (${this.maximumBytes}).`);
63
+ }
64
+ return { done: false, value: new Uint8Array(next.value) };
65
+ }
66
+ catch (error) {
67
+ await this.cancel(error);
68
+ throw error;
69
+ }
70
+ }
71
+ async interrupt(reason) {
72
+ if (this.terminal || this.interruption !== undefined)
73
+ return;
74
+ this.interruption = asError(reason ?? new Error('streaming binary was cancelled.'));
75
+ await this.cancelReader(this.interruption);
76
+ this.control.close();
77
+ }
78
+ async cancelReader(reason) {
79
+ try {
80
+ await this.reader.cancel(reason);
81
+ }
82
+ catch {
83
+ // Cancellation follows an already chosen terminal outcome.
84
+ }
85
+ }
86
+ }
@@ -3,6 +3,7 @@ import * as protocol from '@astrale-os/kernel-protocol/invocation';
3
3
  import { ProtocolError, ResponseError, TransportError } from '../../errors/index.js';
4
4
  import { invocationFailure } from '../failure.js';
5
5
  import { asError, controlledSignal, positiveInteger, requireLive, } from '../index.js';
6
+ import { HttpByteStream } from './byte-stream.js';
6
7
  import { binaryHeaders, codecForResponse, decodeProtocol, normalizeMediaType, readBoundedBody, requireId, } from './response.js';
7
8
  import { HttpStream } from './stream.js';
8
9
  const DEFAULT_TIMEOUT_MS = 30_000;
@@ -64,8 +65,9 @@ export class HttpTransport {
64
65
  const streamFraming = response.headers.get(protocol.http.STREAM_FRAMING_FIELD);
65
66
  const binaryId = response.headers.get(protocol.http.REQUEST_ID_FIELD);
66
67
  const binaryManifest = response.headers.get(protocol.http.BINARY_HEADERS_FIELD);
68
+ const binaryDelivery = response.headers.get(protocol.http.BINARY_DELIVERY_FIELD);
67
69
  const hasStreamSignal = streamFraming !== null;
68
- const hasBinarySignal = binaryId !== null || binaryManifest !== null;
70
+ const hasBinarySignal = binaryId !== null || binaryManifest !== null || binaryDelivery !== null;
69
71
  if (hasStreamSignal && hasBinarySignal) {
70
72
  throw new ProtocolError('server returned conflicting stream and binary response signals.');
71
73
  }
@@ -73,7 +75,13 @@ export class HttpTransport {
73
75
  if (binaryId === null || binaryManifest === null) {
74
76
  throw new ProtocolError('server returned an incomplete binary response signal.');
75
77
  }
76
- return await this.decodeBinary(response, requestId, binaryId, control);
78
+ if (binaryDelivery !== null && binaryDelivery !== protocol.http.STREAMING_BINARY_DELIVERY) {
79
+ throw new ProtocolError('server returned an unsupported binary delivery signal.');
80
+ }
81
+ const binary = await this.decodeBinary(response, requestId, binaryId, binaryDelivery !== null, control);
82
+ if (!(binary.value.body instanceof Uint8Array))
83
+ handedOff = true;
84
+ return binary;
77
85
  }
78
86
  const responseCodec = await codecForResponse(response.headers.get('content-type'));
79
87
  if (responseCodec === undefined) {
@@ -149,10 +157,7 @@ export class HttpTransport {
149
157
  signal: control.signal,
150
158
  });
151
159
  }
152
- async decodeBinary(response, requestId, binaryId, control) {
153
- if (!response.ok) {
154
- throw new ProtocolError('server returned binary data with an error status.');
155
- }
160
+ async decodeBinary(response, requestId, binaryId, streaming, control) {
156
161
  if (binaryId !== String(requestId)) {
157
162
  throw new ProtocolError(`binary response requestId ${JSON.stringify(binaryId)} does not match request requestId ${requestId}.`);
158
163
  }
@@ -160,17 +165,33 @@ export class HttpTransport {
160
165
  if (mediaType === undefined) {
161
166
  throw new ProtocolError('binary response has no media type.');
162
167
  }
163
- const body = await readBoundedBody(response, this.maximumResponseBytes, control.signal);
164
- requireLive(control);
165
168
  const headerNames = decodeProtocol('binary application-header manifest', () => protocol.http.decodeBinaryHeaderNames(response.headers.get(protocol.http.BINARY_HEADERS_FIELD)));
166
169
  const headers = binaryHeaders(response.headers, headerNames);
167
170
  const invocation = decodeProtocol('binary InvocationId field', () => protocol.http.decodeInvocationId(response.headers.get(protocol.http.INVOCATION_FIELD)));
171
+ if (streaming) {
172
+ if (response.body === null)
173
+ throw new ProtocolError('streaming binary has no body.');
174
+ requireLive(control);
175
+ return {
176
+ kind: 'binary',
177
+ invocation,
178
+ value: {
179
+ body: new HttpByteStream(response.body, this.maximumResponseBytes, control),
180
+ mediaType,
181
+ ...(response.status === 200 ? {} : { status: response.status }),
182
+ ...(Object.keys(headers).length === 0 ? {} : { headers }),
183
+ },
184
+ };
185
+ }
186
+ const body = await readBoundedBody(response, this.maximumResponseBytes, control.signal);
187
+ requireLive(control);
168
188
  const binary = decodeProtocol('binary response', () => protocol.decodeBinaryResponse({
169
189
  kind: 'binary',
170
190
  requestId,
171
191
  invocation,
172
192
  body,
173
193
  mediaType,
194
+ status: response.status,
174
195
  ...(Object.keys(headers).length === 0 ? {} : { headers }),
175
196
  }));
176
197
  requireLive(control);
@@ -180,6 +201,7 @@ export class HttpTransport {
180
201
  value: {
181
202
  body: binary.body,
182
203
  mediaType: binary.mediaType,
204
+ ...(binary.status === 200 ? {} : { status: binary.status }),
183
205
  ...(binary.headers === undefined ? {} : { headers: binary.headers }),
184
206
  },
185
207
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/kernel-client",
3
- "version": "0.6.0-beta.17",
3
+ "version": "0.6.0-beta.18",
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.10 <1.0.0",
69
- "@astrale-os/kernel-dsl": ">=0.2.0-beta.8 <1.0.0",
70
- "@astrale-os/kernel-protocol": ">=0.5.0-beta.10 <1.0.0",
71
68
  "@tanstack/pacer": "0.20.1",
72
- "msgpackr": "^2.0.5"
69
+ "msgpackr": "^2.0.5",
70
+ "@astrale-os/kernel-core": "0.9.0-beta.14",
71
+ "@astrale-os/kernel-protocol": "0.5.0-beta.14",
72
+ "@astrale-os/kernel-dsl": "0.2.0-beta.11"
73
73
  },
74
74
  "devDependencies": {
75
- "@astrale-os/kernel-server": ">=0.5.0-beta.11 <1.0.0",
76
75
  "@astrale/typescript-config": "npm:@jsr/astrale__typescript-config@~1.1.0",
77
76
  "@types/node": "^22.0.0",
78
77
  "typescript": "7.0.2",
79
- "vitest": "^4.1.10"
78
+ "vitest": "^4.1.10",
79
+ "@astrale-os/kernel-server": "0.5.0-beta.15"
80
80
  },
81
81
  "scripts": {
82
82
  "build": "tsc",