@astrale-os/kernel-client 0.6.0-beta.16 → 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.
- package/dist/client/result.d.ts +3 -3
- package/dist/client/result.js +3 -0
- package/dist/schema/contracts.d.ts +3 -3
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/introspection.js +4 -0
- package/dist/schema/schema.d.ts +9 -9
- package/dist/schema/schema.js +43 -6
- package/dist/session/bound.d.ts +0 -3
- package/dist/session/bound.js +0 -3
- package/dist/session/index.d.ts +0 -1
- package/dist/session/reference.d.ts +3 -3
- package/dist/session/routing/dispatch.d.ts +0 -3
- package/dist/session/routing/dispatch.js +9 -13
- package/dist/session/session.d.ts +2 -4
- package/dist/session/session.js +1 -19
- package/dist/transport/http/byte-stream.d.ts +17 -0
- package/dist/transport/http/byte-stream.js +86 -0
- package/dist/transport/http/transport.js +30 -8
- package/package.json +7 -7
package/dist/client/result.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
7
|
-
export type Binary =
|
|
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>;
|
package/dist/client/result.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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 =
|
|
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. */
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { createSchema, type SchemaApi, type SchemaCall, type SchemaOptions, type
|
|
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 {
|
|
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
|
}
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import
|
|
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 {
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
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<
|
|
13
|
+
install(request: InstallRequest, options?: RequestOptions): Promise<InstallResult>;
|
|
15
14
|
uninstall(request: UninstallRequest, options?: RequestOptions): Promise<UninstallReceipt>;
|
|
16
|
-
|
|
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;
|
package/dist/schema/schema.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Path } from '@astrale-os/kernel-core/path';
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
17
|
-
|
|
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
|
|
24
|
-
return
|
|
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.');
|
package/dist/session/bound.d.ts
CHANGED
|
@@ -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. */
|
package/dist/session/bound.js
CHANGED
|
@@ -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
|
}
|
package/dist/session/index.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { IssuerId } from '@astrale-os/kernel-core/auth/issuer';
|
|
2
2
|
import type { Call, Result } from '../../client/index.js';
|
|
3
3
|
import type { SessionOperation, SessionPolicy } from '../context/index.js';
|
|
4
|
-
import type { SessionDiscovery } from '../discovery/index.js';
|
|
5
4
|
import type { SessionAuth } from './auth.js';
|
|
6
5
|
import type { RouteCache } from './cache.js';
|
|
7
6
|
import type { ClientPoolCapability } from './envelope.js';
|
|
@@ -16,7 +15,6 @@ export interface SessionDispatcherOptions {
|
|
|
16
15
|
readonly cache: RouteCache;
|
|
17
16
|
readonly policy: Readonly<SessionPolicy>;
|
|
18
17
|
readonly envelopeTransport: SessionEnvelopeTransport;
|
|
19
|
-
readonly discovery: SessionDiscovery;
|
|
20
18
|
readonly requireOpen: () => void;
|
|
21
19
|
}
|
|
22
20
|
export declare class SessionDispatcher {
|
|
@@ -27,7 +25,6 @@ export declare class SessionDispatcher {
|
|
|
27
25
|
private followSource;
|
|
28
26
|
private dispatchDestinationWithRecovery;
|
|
29
27
|
private dispatchDestination;
|
|
30
|
-
private sourceKernel;
|
|
31
28
|
}
|
|
32
29
|
export declare function requireResultKind<Kind extends 'value' | 'stream' | 'binary'>(expected: Kind, result: Result): Extract<Result, {
|
|
33
30
|
kind: Kind;
|
|
@@ -15,8 +15,8 @@ export class SessionDispatcher {
|
|
|
15
15
|
// Authority is resolved before warm lookup so changed credentials or delegation cannot reuse a
|
|
16
16
|
// carrier minted for a different caller partition.
|
|
17
17
|
const auth = await resolveAuth(this.state.auth, call, operation);
|
|
18
|
-
const source =
|
|
19
|
-
const cacheKey = await this.state.cache.key(source
|
|
18
|
+
const source = this.state.kernel;
|
|
19
|
+
const cacheKey = await this.state.cache.key(source, call, auth, expected);
|
|
20
20
|
const dispatch = Object.freeze({
|
|
21
21
|
call,
|
|
22
22
|
auth,
|
|
@@ -46,7 +46,7 @@ export class SessionDispatcher {
|
|
|
46
46
|
if (result.kind !== 'redirect')
|
|
47
47
|
return result;
|
|
48
48
|
const redirect = result.redirect;
|
|
49
|
-
if (redirect.route.via.issuer !== state.source
|
|
49
|
+
if (redirect.route.via.issuer !== state.source) {
|
|
50
50
|
throw new ClientError('Redirect issuer differs from the session-pinned source Kernel identity.');
|
|
51
51
|
}
|
|
52
52
|
requireExpectedPublication(redirect.route, state.expected);
|
|
@@ -55,7 +55,7 @@ export class SessionDispatcher {
|
|
|
55
55
|
}
|
|
56
56
|
async dispatchDestinationWithRecovery(learned, state, options, operation, trace) {
|
|
57
57
|
try {
|
|
58
|
-
return await this.dispatchDestination(learned, state.call, options, operation, state.expected, trace);
|
|
58
|
+
return await this.dispatchDestination(learned, state.call, options, operation, state.source, state.expected, trace);
|
|
59
59
|
}
|
|
60
60
|
catch (cause) {
|
|
61
61
|
const stale = isAdmittedStaleRouteError(cause);
|
|
@@ -79,8 +79,8 @@ export class SessionDispatcher {
|
|
|
79
79
|
return this.followSource(refreshed, state, options, operation);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
async dispatchDestination(learned, call, options, operation, expected, trace) {
|
|
83
|
-
if (learned.route.via.issuer !==
|
|
82
|
+
async dispatchDestination(learned, call, options, operation, source, expected, trace) {
|
|
83
|
+
if (learned.route.via.issuer !== source) {
|
|
84
84
|
throw new ClientError('Learned route issuer differs from the session-pinned source Kernel.');
|
|
85
85
|
}
|
|
86
86
|
requireExpectedPublication(learned.route, expected);
|
|
@@ -97,13 +97,6 @@ export class SessionDispatcher {
|
|
|
97
97
|
}
|
|
98
98
|
return result;
|
|
99
99
|
}
|
|
100
|
-
async sourceKernel(operation) {
|
|
101
|
-
const snapshot = await this.state.discovery.snapshot(operation);
|
|
102
|
-
if (snapshot.kernel !== this.state.kernel) {
|
|
103
|
-
throw new ClientError('Session discovery returned another Kernel identity.');
|
|
104
|
-
}
|
|
105
|
-
return Object.freeze({ issuer: snapshot.publication.identity.issuer });
|
|
106
|
-
}
|
|
107
100
|
}
|
|
108
101
|
function isRouteCompatibilityConflict(input) {
|
|
109
102
|
if (!(input instanceof ResponseError))
|
|
@@ -130,6 +123,9 @@ export function requireResultKind(expected, result) {
|
|
|
130
123
|
return result;
|
|
131
124
|
if (result.kind === 'stream')
|
|
132
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
|
+
}
|
|
133
129
|
throw new ClientError(`Function returned ${result.kind}; ${expected} was required.`);
|
|
134
130
|
}
|
|
135
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 {
|
|
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").
|
|
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;
|
package/dist/session/session.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Path } from '@astrale-os/kernel-core/path';
|
|
2
2
|
import { Client } from '../client/index.js';
|
|
3
|
-
import {
|
|
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';
|
|
@@ -87,7 +87,6 @@ export class ClientSession {
|
|
|
87
87
|
cache: this.routes,
|
|
88
88
|
policy,
|
|
89
89
|
envelopeTransport: admitEnvelopeTransport(options.envelopeTransport ?? 'http'),
|
|
90
|
-
discovery: this.discovery,
|
|
91
90
|
requireOpen: () => this.requireOpen(),
|
|
92
91
|
});
|
|
93
92
|
this.schema = createSchema((request, options) => this.dispatch(request, options), {
|
|
@@ -107,23 +106,6 @@ export class ClientSession {
|
|
|
107
106
|
this.requireOpen();
|
|
108
107
|
return resolveViewsFor(async (call, operation) => requireResultKind('value', await this.dispatcher.dispatch(call, {}, operation)).value, this.admitTargetViews, target, this.operation(admitControlOptions(options)));
|
|
109
108
|
}
|
|
110
|
-
async installation(origin, options) {
|
|
111
|
-
const result = await this.schema.introspect({
|
|
112
|
-
from: { kind: 'installation', origin },
|
|
113
|
-
select: {
|
|
114
|
-
state: true,
|
|
115
|
-
target: true,
|
|
116
|
-
source: true,
|
|
117
|
-
bundle: true,
|
|
118
|
-
readiness: true,
|
|
119
|
-
capabilities: true,
|
|
120
|
-
},
|
|
121
|
-
}, options);
|
|
122
|
-
if (result?.state !== 'ready' || result.readiness === null) {
|
|
123
|
-
throw new ClientError(`Domain ${origin} is not ready on this Kernel.`);
|
|
124
|
-
}
|
|
125
|
-
return result;
|
|
126
|
-
}
|
|
127
109
|
dispatch(call, options) {
|
|
128
110
|
const admitted = admitRequestOptions(options);
|
|
129
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
|
-
|
|
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.
|
|
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",
|