@deepseek-ai/dsh-client-connection 0.1.2-alpha.5 → 0.1.3-alpha.2
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/README.i18n.yaml +2 -2
- package/README.md +9 -4
- package/README.zh.md +9 -4
- package/lib/client.js +1562 -403
- package/lib/index.js +122 -58
- package/lib/types/client/connection.d.ts +8 -14
- package/lib/types/client/fixture.d.ts +31 -0
- package/lib/types/client/index.d.ts +8 -12
- package/lib/types/http-bridge.d.ts +4 -12
- package/lib/types/index.d.ts +5 -2
- package/lib/types/recovery-config.d.ts +27 -0
- package/lib/types/rpc.d.ts +14 -1
- package/package.json +13 -12
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
2
3
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
4
|
import { credentialKey } from "@deepseek-ai/dsh-credentials";
|
|
4
5
|
import { Service } from "@deepseek-ai/cordis";
|
|
@@ -24,48 +25,69 @@ const DEFAULT_MAX_REQUEST_BODY_BYTES = 300 * 1024 * 1024;
|
|
|
24
25
|
/**
|
|
25
26
|
* Bridge one node:http request to the fetch-shaped handler (client close
|
|
26
27
|
* aborts; response bodies stream out chunk by chunk).
|
|
27
|
-
* @param req - incoming node:http request
|
|
28
|
+
* @param req - incoming node:http request.
|
|
28
29
|
* @param res - node:http response the bridge writes and owns to completion.
|
|
29
30
|
* @param apiHandler - fetch-shaped API carrier the request is dispatched to.
|
|
30
|
-
* @param maxRequestBodyBytes - maximum
|
|
31
|
+
* @param maxRequestBodyBytes - maximum bytes buffered for a buffered route.
|
|
31
32
|
*/
|
|
32
33
|
async function bridge(req, res, apiHandler, maxRequestBodyBytes = DEFAULT_MAX_REQUEST_BODY_BYTES) {
|
|
33
34
|
const abort = new AbortController();
|
|
34
35
|
res.on("close", () => {
|
|
35
36
|
if (!res.writableEnded) abort.abort();
|
|
36
37
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
let
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
if (received > maxRequestBodyBytes) {
|
|
38
|
+
/* v8 ignore next 2 -- node:http always sets url/method on server requests. */
|
|
39
|
+
const url = new URL(req.url ?? "/", "http://dsh.internal");
|
|
40
|
+
const method = req.method ?? "GET";
|
|
41
|
+
const headers = Object.fromEntries(Object.entries(req.headers).filter(([, value]) => typeof value === "string"));
|
|
42
|
+
const bodyMode = apiHandler.requestBodyMode({
|
|
43
|
+
method,
|
|
44
|
+
url
|
|
45
|
+
});
|
|
46
|
+
let request;
|
|
47
|
+
if (bodyMode === "buffered") {
|
|
48
|
+
const declaredLength = req.headers["content-length"];
|
|
49
|
+
if (declaredLength !== void 0 && Number(declaredLength) > maxRequestBodyBytes) {
|
|
50
50
|
res.writeHead(413, { connection: "close" });
|
|
51
51
|
res.end();
|
|
52
52
|
req.destroy();
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
-
chunks
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
const chunks = [];
|
|
56
|
+
let received = 0;
|
|
57
|
+
for await (const chunk of req) {
|
|
58
|
+
const buffer = chunk;
|
|
59
|
+
received += buffer.byteLength;
|
|
60
|
+
if (received > maxRequestBodyBytes) {
|
|
61
|
+
res.writeHead(413, { connection: "close" });
|
|
62
|
+
res.end();
|
|
63
|
+
req.destroy();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
chunks.push(buffer);
|
|
67
|
+
}
|
|
68
|
+
request = new Request(url, {
|
|
69
|
+
method,
|
|
70
|
+
headers,
|
|
71
|
+
...chunks.length > 0 ? { body: Buffer.concat(chunks) } : {},
|
|
72
|
+
signal: abort.signal
|
|
73
|
+
});
|
|
74
|
+
} else request = new Request(url, {
|
|
75
|
+
method,
|
|
76
|
+
headers,
|
|
77
|
+
body: Readable.toWeb(req),
|
|
78
|
+
signal: abort.signal,
|
|
79
|
+
duplex: "half"
|
|
64
80
|
});
|
|
65
81
|
const response = await apiHandler.fetch(request);
|
|
66
|
-
|
|
82
|
+
const requestUnread = bodyMode === "streaming" && !req.readableEnded;
|
|
83
|
+
const responseHeaders = Object.fromEntries(response.headers.entries());
|
|
84
|
+
res.writeHead(response.status, requestUnread ? {
|
|
85
|
+
...responseHeaders,
|
|
86
|
+
connection: "close"
|
|
87
|
+
} : responseHeaders);
|
|
67
88
|
if (response.body === null) {
|
|
68
89
|
res.end();
|
|
90
|
+
if (requestUnread) req.destroy();
|
|
69
91
|
return;
|
|
70
92
|
}
|
|
71
93
|
for await (const chunk of response.body) if (!res.write(chunk)) await new Promise((resolve) => {
|
|
@@ -78,6 +100,7 @@ async function bridge(req, res, apiHandler, maxRequestBodyBytes = DEFAULT_MAX_RE
|
|
|
78
100
|
res.once("close", done);
|
|
79
101
|
});
|
|
80
102
|
res.end();
|
|
103
|
+
if (requestUnread) req.destroy();
|
|
81
104
|
}
|
|
82
105
|
//#endregion
|
|
83
106
|
//#region lib/types/loopback-hostname.js
|
|
@@ -545,20 +568,27 @@ var HostConnectionService = class extends Service {
|
|
|
545
568
|
* @returns Fetch handler that selects one owner or returns 404.
|
|
546
569
|
*/
|
|
547
570
|
createSharedFetchHandler(channel) {
|
|
548
|
-
return {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
571
|
+
return {
|
|
572
|
+
requestBodyMode: ({ method, url }) => {
|
|
573
|
+
const route = this.fetchRoutes.get(url.pathname);
|
|
574
|
+
return route?.methods.has(method) === true ? route.requestBody : "buffered";
|
|
575
|
+
},
|
|
576
|
+
fetch: (request) => {
|
|
577
|
+
const pathname = new URL(request.url).pathname;
|
|
578
|
+
const route = this.fetchRoutes.get(pathname);
|
|
579
|
+
if (route?.methods.has(request.method) === true) return route.fetch(request);
|
|
580
|
+
const endpoint = endpointFromPath(channel, pathname);
|
|
581
|
+
const interceptor = this.interceptors.get(channel);
|
|
582
|
+
if (endpoint === void 0 || interceptor === void 0 || !interceptor.matches(endpoint)) return Promise.resolve(new Response("not found", { status: 404 }));
|
|
583
|
+
return interceptor.fetchHandler.fetch(request);
|
|
584
|
+
}
|
|
585
|
+
};
|
|
557
586
|
}
|
|
558
587
|
registerFetchRoute(owner, route) {
|
|
559
588
|
assertFetchRoute(route);
|
|
560
589
|
const registered = {
|
|
561
590
|
methods: new Set(route.methods),
|
|
591
|
+
requestBody: route.requestBody,
|
|
562
592
|
fetch: route.fetch
|
|
563
593
|
};
|
|
564
594
|
return owner.effect(() => {
|
|
@@ -603,31 +633,34 @@ var HostConnectionService = class extends Service {
|
|
|
603
633
|
}
|
|
604
634
|
};
|
|
605
635
|
function rpcFetchHandler(channel, handler) {
|
|
606
|
-
return {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
body
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
636
|
+
return {
|
|
637
|
+
requestBodyMode: () => "buffered",
|
|
638
|
+
async fetch(request) {
|
|
639
|
+
const endpoint = endpointFromPath(channel, new URL(request.url).pathname);
|
|
640
|
+
if (request.method !== "POST" || endpoint === void 0) return new Response("not found", { status: 404 });
|
|
641
|
+
if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response("content type must be application/json", { status: 415 });
|
|
642
|
+
let body;
|
|
643
|
+
try {
|
|
644
|
+
body = await request.json();
|
|
645
|
+
} catch {
|
|
646
|
+
return new Response("body is not JSON", { status: 400 });
|
|
647
|
+
}
|
|
648
|
+
const envelope = clientRequestSchema.safeParse(body);
|
|
649
|
+
if (!envelope.success) return invalidEnvelopeResponse(body, envelope.error.issues);
|
|
650
|
+
const message = envelope.data;
|
|
651
|
+
if (message.method !== endpoint) return errorResponse(message.rpcId, {
|
|
652
|
+
code: "gateway/bad-request",
|
|
653
|
+
message: `method ${JSON.stringify(message.method)} does not match endpoint ${JSON.stringify(endpoint)}`,
|
|
654
|
+
details: { issues: [] }
|
|
655
|
+
});
|
|
656
|
+
try {
|
|
657
|
+
const result = await handler(endpoint, message.payload, request.signal);
|
|
658
|
+
return fullResponse(message.rpcId, result);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
return new Response(`handler failure: ${String(error)}`, { status: 500 });
|
|
661
|
+
}
|
|
629
662
|
}
|
|
630
|
-
}
|
|
663
|
+
};
|
|
631
664
|
}
|
|
632
665
|
function invalidEnvelopeResponse(body, issues) {
|
|
633
666
|
const rawId = body?.rpcId;
|
|
@@ -666,6 +699,28 @@ function assertFetchRoute(route) {
|
|
|
666
699
|
if (new Set(route.methods).size !== route.methods.length) throw new Error(`connection: exact Fetch route ${JSON.stringify(route.path)} repeats a method`);
|
|
667
700
|
}
|
|
668
701
|
//#endregion
|
|
702
|
+
//#region lib/types/recovery-config.js
|
|
703
|
+
/** Shared validation for Host-configured and browser-local connection recovery. */
|
|
704
|
+
const MAX_TIMER_MS = 2147483647;
|
|
705
|
+
/** Schema shared by the Host plugin and the Client's recovery input parser. */
|
|
706
|
+
const ConnectionRecoveryConfigSchema = z.object({
|
|
707
|
+
backoffBaseMs: z.natural().min(1).max(MAX_TIMER_MS).default(500),
|
|
708
|
+
backoffFactor: z.number().min(1).max(Number.MAX_VALUE).default(2),
|
|
709
|
+
backoffMaxMs: z.natural().min(1).max(MAX_TIMER_MS).default(1e4),
|
|
710
|
+
generationReadyWarnMs: z.natural().min(1).max(MAX_TIMER_MS).default(3e3),
|
|
711
|
+
generationReadyTimeoutMs: z.natural().min(1).max(MAX_TIMER_MS).default(15e3)
|
|
712
|
+
});
|
|
713
|
+
/**
|
|
714
|
+
* Validate recovery input and supply every timing default before starting work.
|
|
715
|
+
* @param config - Host configuration, page bootstrap data, or direct loop options.
|
|
716
|
+
* @returns validated, complete recovery timing.
|
|
717
|
+
*/
|
|
718
|
+
function resolveConnectionConfig(config = {}) {
|
|
719
|
+
const resolved = ConnectionRecoveryConfigSchema(config);
|
|
720
|
+
if (!Number.isFinite(resolved.backoffFactor)) throw new RangeError("connection recovery backoffFactor must be finite");
|
|
721
|
+
return resolved;
|
|
722
|
+
}
|
|
723
|
+
//#endregion
|
|
669
724
|
//#region lib/types/index.js
|
|
670
725
|
/** Stable Cordis plugin name. */
|
|
671
726
|
const name = "client-connection";
|
|
@@ -680,6 +735,7 @@ function assertImageBodyCapacity(ctx, maxRequestBodyBytes) {
|
|
|
680
735
|
/** Services required before providing Connection. */
|
|
681
736
|
const inject = ["webServer", "credentials"];
|
|
682
737
|
const Config = z.object({
|
|
738
|
+
recovery: ConnectionRecoveryConfigSchema.default({}),
|
|
683
739
|
trustedHosts: z.array(String).default([]),
|
|
684
740
|
cookieMaxAgeDays: z.natural().min(1).default(30),
|
|
685
741
|
maxRequestBodyBytes: z.natural().min(1).default(DEFAULT_MAX_REQUEST_BODY_BYTES)
|
|
@@ -692,12 +748,20 @@ const Config = z.object({
|
|
|
692
748
|
* @param config - resolved plugin config (schema defaults applied).
|
|
693
749
|
*/
|
|
694
750
|
async function apply(ctx, config) {
|
|
751
|
+
const recovery = resolveConnectionConfig(config?.recovery);
|
|
695
752
|
const trustedHosts = config?.trustedHosts ?? [];
|
|
696
753
|
const cookieMaxAgeDays = config?.cookieMaxAgeDays ?? 30;
|
|
697
754
|
const maxRequestBodyBytes = config?.maxRequestBodyBytes ?? 314572800;
|
|
698
755
|
for (const entry of trustedHosts) assertTrustedAuthority(entry);
|
|
699
756
|
assertImageBodyCapacity(ctx, maxRequestBodyBytes);
|
|
700
757
|
const connection = new HostConnectionService(ctx, trustedHosts, await BrowserAuth.create(ctx.root, ctx.credentials, cookieMaxAgeDays));
|
|
758
|
+
ctx.on("webserver/index-inject", (table) => {
|
|
759
|
+
table.push({
|
|
760
|
+
kind: "global",
|
|
761
|
+
name: "__DSH_CONNECTION_RECOVERY__",
|
|
762
|
+
value: recovery
|
|
763
|
+
});
|
|
764
|
+
});
|
|
701
765
|
const fetchHandler = connection.createSharedFetchHandler(API_PATH);
|
|
702
766
|
const route = {
|
|
703
767
|
kind: "prefix",
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** Connection generation readiness, cancellation, and continuous recovery. */
|
|
2
|
+
import { type ConnectionRecoveryConfig } from '../recovery-config.ts';
|
|
3
|
+
export type { ConnectionRecoveryConfig } from '../recovery-config.ts';
|
|
1
4
|
/** Stable Host facts delivered by one established Remote event generation. */
|
|
2
5
|
export interface ConnectionHostInfo {
|
|
3
6
|
/** Host account home used only to abbreviate displayed filesystem paths. */
|
|
@@ -10,17 +13,6 @@ export interface ConnectionGeneration {
|
|
|
10
13
|
/** Host facts carried by this generation's opening frame. */
|
|
11
14
|
readonly host: ConnectionHostInfo;
|
|
12
15
|
}
|
|
13
|
-
/** Reconnect/backoff tunables. All fields are optional; defaults are below. */
|
|
14
|
-
export interface ConnectionConfig {
|
|
15
|
-
/** First-retry backoff cap in ms (jittered: actual delay is cap/2..cap). */
|
|
16
|
-
backoffBaseMs?: number;
|
|
17
|
-
/** Exponential growth factor per failed attempt; values at or below 1 make the base tier final. */
|
|
18
|
-
backoffFactor?: number;
|
|
19
|
-
/** Upper bound for the backoff cap in ms. */
|
|
20
|
-
backoffMaxMs?: number;
|
|
21
|
-
/** Maximum wait for the registered generation source's ready signal. */
|
|
22
|
-
generationReadyTimeoutMs?: number;
|
|
23
|
-
}
|
|
24
16
|
/** Connection lifecycle state published after the first attempt has an outcome. */
|
|
25
17
|
export type ConnectionState = 'connected' | 'disconnected' | 'connecting';
|
|
26
18
|
/** Connection-generation callbacks owned by API Gateway. */
|
|
@@ -35,7 +27,8 @@ export interface ConnectionSinks {
|
|
|
35
27
|
/**
|
|
36
28
|
* One long-lived source defining a Connection generation. The source must
|
|
37
29
|
* attach its incremental listeners before calling `ready`, then remain pending
|
|
38
|
-
* until the generation is lost or `signal` aborts.
|
|
30
|
+
* until the generation is lost or `signal` aborts. On abort it must stop
|
|
31
|
+
* delivery, release its resources, and settle before a replacement can start.
|
|
39
32
|
* @param signal - cancellation for the current generation.
|
|
40
33
|
* @param ready - one-shot report that incremental delivery is attached.
|
|
41
34
|
* @returns a promise settling only when this generation ends or fails.
|
|
@@ -58,7 +51,7 @@ export declare class ConnectionController {
|
|
|
58
51
|
private networkAvailable;
|
|
59
52
|
private lastState;
|
|
60
53
|
private readonly config;
|
|
61
|
-
constructor(source: ConnectionGenerationSource, sinks?: ConnectionSinks, config?:
|
|
54
|
+
constructor(source: ConnectionGenerationSource, sinks?: ConnectionSinks, config?: ConnectionRecoveryConfig);
|
|
62
55
|
/** Idempotent: begin the connect/pump/reconnect loop. */
|
|
63
56
|
start(): void;
|
|
64
57
|
/** Stop the loop and abort the current generation source. */
|
|
@@ -72,7 +65,8 @@ export declare class ConnectionController {
|
|
|
72
65
|
setNetworkAvailable(available: boolean): void;
|
|
73
66
|
private backoffCap;
|
|
74
67
|
private backoffDelay;
|
|
75
|
-
|
|
68
|
+
/** Re-read retry inputs after a potentially reentrant state sink. */
|
|
69
|
+
private isRetryInterrupted;
|
|
76
70
|
/** Read through a method: stop() flips the flag across awaits, so narrowing from the loop condition must not stick. */
|
|
77
71
|
private isRunning;
|
|
78
72
|
/** Re-read both mutable liveness guards after a potentially reentrant sink. */
|
|
@@ -1,4 +1,35 @@
|
|
|
1
|
+
import { LlmAttemptId } from '@deepseek-ai/dsh-llm/brand';
|
|
2
|
+
import type { SessionSeqCursor } from '@deepseek-ai/dsh-session/types';
|
|
3
|
+
import type { JsonValue } from '@deepseek-ai/dsh-util-values';
|
|
1
4
|
import type { ClientConnectionRpc } from '../rpc.ts';
|
|
5
|
+
/** Assistant frame emitted by the standalone fixture; tests pin it to the controller wire type. */
|
|
6
|
+
export type FixtureAssistantStreamFrame = {
|
|
7
|
+
readonly type: 'start';
|
|
8
|
+
readonly attemptId: ReturnType<typeof LlmAttemptId>;
|
|
9
|
+
readonly revision: number;
|
|
10
|
+
readonly startedAfterSeq: SessionSeqCursor;
|
|
11
|
+
readonly turn: number;
|
|
12
|
+
readonly step: number;
|
|
13
|
+
} | {
|
|
14
|
+
readonly type: 'chunk';
|
|
15
|
+
readonly attemptId: ReturnType<typeof LlmAttemptId>;
|
|
16
|
+
readonly revision: number;
|
|
17
|
+
readonly index: number;
|
|
18
|
+
readonly time: number;
|
|
19
|
+
readonly chunk: JsonValue;
|
|
20
|
+
} | {
|
|
21
|
+
readonly type: 'end';
|
|
22
|
+
readonly attemptId: ReturnType<typeof LlmAttemptId>;
|
|
23
|
+
readonly revision: number;
|
|
24
|
+
readonly index: number;
|
|
25
|
+
readonly outcome: {
|
|
26
|
+
readonly kind: 'committed';
|
|
27
|
+
readonly eventType: 'assistant/message' | 'assistant/attempt';
|
|
28
|
+
readonly seq: number;
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: 'abandoned';
|
|
31
|
+
};
|
|
32
|
+
};
|
|
2
33
|
/** Deterministic fixture branches used by keyless Web assembly tests. */
|
|
3
34
|
export interface FixtureOptions {
|
|
4
35
|
/** Start with no real Workspace or Session. */
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser wire client. The plugin selects fixture or HTTP transport, provides
|
|
3
|
-
* the shared API client, and lets API Gateway own the connection loop.
|
|
4
|
-
*/
|
|
1
|
+
/** Browser wire client: Remote transport and connection generations. */
|
|
5
2
|
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
-
import { type
|
|
3
|
+
import { type ConnectionRecoveryConfig, type ConnectionGeneration, type ConnectionGenerationSource, type ConnectionSinks, type ConnectionState } from './connection.ts';
|
|
7
4
|
import { type RpcFetch, type RpcStreamOpen } from './rpc.ts';
|
|
8
5
|
import type { ClientConnectionRpc } from '../rpc.ts';
|
|
9
6
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -18,7 +15,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
18
15
|
}
|
|
19
16
|
export type { MessageId, RpcRequest, RpcResponse, RpcResult, ClientRequest, ServerResponse, RpcMessage, SessionId, SessionEvent, ContentBlock, StreamChunk, } from './api.ts';
|
|
20
17
|
export { RpcId, transportError, } from './api.ts';
|
|
21
|
-
export type {
|
|
18
|
+
export type { ConnectionRecoveryConfig, ConnectionGeneration, ConnectionGenerationSource, ConnectionHostInfo, ConnectionSinks, ConnectionState, } from './connection.ts';
|
|
22
19
|
export type { ClientConnectionRpc, ConnectionRpcFailure, ConnectionRpcResult, } from '../rpc.ts';
|
|
23
20
|
export type { RpcFetch } from './rpc.ts';
|
|
24
21
|
/** Observable identity and Host facts for the active connection generation. */
|
|
@@ -65,9 +62,8 @@ export interface ClientTransportHooks {
|
|
|
65
62
|
ownsHost?: boolean;
|
|
66
63
|
}
|
|
67
64
|
/**
|
|
68
|
-
* The ctx.connection service API
|
|
69
|
-
*
|
|
70
|
-
* Connection stays independent of downstream domain state.
|
|
65
|
+
* The ctx.connection service API. API Gateway supplies generation readiness
|
|
66
|
+
* and reset callbacks; Connection stays independent of downstream domain state.
|
|
71
67
|
*/
|
|
72
68
|
export interface ConnectionHandle {
|
|
73
69
|
/**
|
|
@@ -95,10 +91,10 @@ export interface ConnectionHandle {
|
|
|
95
91
|
* Start the connect/reconnect loop with the consumer's state callbacks.
|
|
96
92
|
* API Gateway owns the loop; a second call throws.
|
|
97
93
|
* @param sinks - connection-state callbacks.
|
|
98
|
-
* @param config -
|
|
94
|
+
* @param config - explicit timing overrides; omitted fields use Host bootstrap timing.
|
|
99
95
|
* @returns lifecycle controls for the loop.
|
|
100
96
|
*/
|
|
101
|
-
start(sinks: ConnectionSinks, config?:
|
|
97
|
+
start(sinks: ConnectionSinks, config?: ConnectionRecoveryConfig): ConnectionLoop;
|
|
102
98
|
}
|
|
103
99
|
/** Controls retained by the sole owner of a running connection loop. */
|
|
104
100
|
export interface ConnectionLoop {
|
|
@@ -106,7 +102,7 @@ export interface ConnectionLoop {
|
|
|
106
102
|
stop(): void;
|
|
107
103
|
}
|
|
108
104
|
/**
|
|
109
|
-
* Client plugin body: pick
|
|
105
|
+
* Client plugin body: pick physical carriers by page mode and provide ctx.connection.
|
|
110
106
|
* @param ctx - client cordis context.
|
|
111
107
|
*/
|
|
112
108
|
export declare function apply(ctx: Context): void;
|
|
@@ -3,27 +3,19 @@
|
|
|
3
3
|
* web carrier; the fetch-shaped handler itself is transport-agnostic).
|
|
4
4
|
*/
|
|
5
5
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
6
|
+
import type { ConnectionFetchHandler } from './rpc.ts';
|
|
6
7
|
/** Default carrier cap for all HTTP RPC bodies: sized for the default
|
|
7
8
|
* aggregate image limit (200 MiB) after base64 expansion plus envelope
|
|
8
9
|
* headroom (~267.7 MiB required), rounded up for slack. The bridge buffers
|
|
9
10
|
* each body in memory, so this cap is also the per-request resident bound. */
|
|
10
11
|
export declare const DEFAULT_MAX_REQUEST_BODY_BYTES: number;
|
|
11
|
-
/** Transport-independent request handler consumed by the Host HTTP bridge. */
|
|
12
|
-
export interface FetchHandler {
|
|
13
|
-
/**
|
|
14
|
-
* Handle one standard Fetch request.
|
|
15
|
-
* @param request - request produced by the active transport bridge.
|
|
16
|
-
* @returns complete or streaming Fetch response.
|
|
17
|
-
*/
|
|
18
|
-
fetch(request: Request): Promise<Response>;
|
|
19
|
-
}
|
|
20
12
|
/**
|
|
21
13
|
* Bridge one node:http request to the fetch-shaped handler (client close
|
|
22
14
|
* aborts; response bodies stream out chunk by chunk).
|
|
23
|
-
* @param req - incoming node:http request
|
|
15
|
+
* @param req - incoming node:http request.
|
|
24
16
|
* @param res - node:http response the bridge writes and owns to completion.
|
|
25
17
|
* @param apiHandler - fetch-shaped API carrier the request is dispatched to.
|
|
26
|
-
* @param maxRequestBodyBytes - maximum
|
|
18
|
+
* @param maxRequestBodyBytes - maximum bytes buffered for a buffered route.
|
|
27
19
|
*/
|
|
28
|
-
export declare function bridge(req: IncomingMessage, res: ServerResponse, apiHandler:
|
|
20
|
+
export declare function bridge(req: IncomingMessage, res: ServerResponse, apiHandler: ConnectionFetchHandler, maxRequestBodyBytes?: number): Promise<void>;
|
|
29
21
|
//# sourceMappingURL=http-bridge.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Host HTTP bridge for browser-client RPC. */
|
|
2
2
|
import type { Context } from '@deepseek-ai/cordis';
|
|
3
3
|
import z from '@deepseek-ai/schemastery';
|
|
4
|
-
|
|
4
|
+
import { type ConnectionRecoveryConfig } from './recovery-config.ts';
|
|
5
|
+
export type { ConnectionFetchMethod, ConnectionFetchHandler, ConnectionFetchRoute, ConnectionIndexRequest, ConnectionIndexResponse, ConnectionRpcEndpointMatcher, ConnectionRpcFailure, ConnectionRpcHandler, ConnectionRequestRejection, ConnectionRpcResult, ConnectionRequestBodyMode, ConnectionTrustRequest, ClientRequest, HostConnectionHandle, HostConnectionFetch, HostConnectionRpc, RpcMessage, ServerResponse, } from './rpc.ts';
|
|
5
6
|
export { RpcId, transportError } from './rpc.ts';
|
|
6
7
|
export { clientRequestSchema, rpcErrorSchema, rpcIdSchema, rpcMessageSchema, rpcResultSchema, serverResponseSchema, } from './rpc-schema.ts';
|
|
7
8
|
export { HostConnectionService } from './rpc-host.ts';
|
|
@@ -10,8 +11,10 @@ export { API_PATH } from './api-path.ts';
|
|
|
10
11
|
export declare const name = "client-connection";
|
|
11
12
|
/** Services required before providing Connection. */
|
|
12
13
|
export declare const inject: string[];
|
|
13
|
-
/**
|
|
14
|
+
/** Browser authentication, request limits, and connection recovery configuration. */
|
|
14
15
|
export interface ConnectionConfig {
|
|
16
|
+
/** Browser recovery timing, injected into each served page. */
|
|
17
|
+
recovery?: ConnectionRecoveryConfig;
|
|
15
18
|
/**
|
|
16
19
|
* Authorities this deployment serves beyond loopback: exact `host:port`, or
|
|
17
20
|
* port-less `host` matching any port. The /api trust fence refuses any
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Shared validation for Host-configured and browser-local connection recovery. */
|
|
2
|
+
import z from '@deepseek-ai/schemastery';
|
|
3
|
+
/** Timing for generation readiness and automatic reconnection. */
|
|
4
|
+
export interface ConnectionRecoveryConfig {
|
|
5
|
+
/** First-retry delay cap in ms; actual delay is 50–100% of the cap. Default: 500. */
|
|
6
|
+
backoffBaseMs?: number;
|
|
7
|
+
/** Finite growth factor of at least 1 per failed attempt; 1 keeps a fixed cap. Default: 2. */
|
|
8
|
+
backoffFactor?: number;
|
|
9
|
+
/** Maximum retry delay cap in ms; retries continue at this cap. Default: 10000. */
|
|
10
|
+
backoffMaxMs?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Delay before reporting a slow handshake, without cancelling it. Default: 3000.
|
|
13
|
+
* Omitted when readiness, failure, cancellation, or the hard deadline occurs first.
|
|
14
|
+
*/
|
|
15
|
+
generationReadyWarnMs?: number;
|
|
16
|
+
/** Deadline in ms for readiness, including physical connection setup. Default: 15000. */
|
|
17
|
+
generationReadyTimeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Schema shared by the Host plugin and the Client's recovery input parser. */
|
|
20
|
+
export declare const ConnectionRecoveryConfigSchema: z<ConnectionRecoveryConfig>;
|
|
21
|
+
/**
|
|
22
|
+
* Validate recovery input and supply every timing default before starting work.
|
|
23
|
+
* @param config - Host configuration, page bootstrap data, or direct loop options.
|
|
24
|
+
* @returns validated, complete recovery timing.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveConnectionConfig(config?: unknown): Required<ConnectionRecoveryConfig>;
|
|
27
|
+
//# sourceMappingURL=recovery-config.d.ts.map
|
package/lib/types/rpc.d.ts
CHANGED
|
@@ -77,13 +77,17 @@ export type ConnectionRpcHandler = (endpoint: string, payload: unknown, signal:
|
|
|
77
77
|
/** Synchronous ownership test for one endpoint on a shared RPC channel. */
|
|
78
78
|
export type ConnectionRpcEndpointMatcher = (endpoint: string) => boolean;
|
|
79
79
|
/** HTTP methods supported by exact Fetch routes on the shared API channel. */
|
|
80
|
-
export type ConnectionFetchMethod = 'GET' | 'HEAD';
|
|
80
|
+
export type ConnectionFetchMethod = 'GET' | 'HEAD' | 'POST';
|
|
81
|
+
/** How the node:http bridge presents one request body to its Fetch route. */
|
|
82
|
+
export type ConnectionRequestBodyMode = 'buffered' | 'streaming';
|
|
81
83
|
/** One exact, transport-independent Fetch route owned by a Host feature. */
|
|
82
84
|
export interface ConnectionFetchRoute {
|
|
83
85
|
/** Absolute path below `/api`; query parameters remain available on the request URL. */
|
|
84
86
|
readonly path: string;
|
|
85
87
|
/** Methods this route owns. Other methods continue through normal shared-channel dispatch. */
|
|
86
88
|
readonly methods: readonly ConnectionFetchMethod[];
|
|
89
|
+
/** Buffered requests obey the configured JSON cap; streaming requests arrive with backpressure and no aggregate cap. */
|
|
90
|
+
readonly requestBody: ConnectionRequestBodyMode;
|
|
87
91
|
/** Handle one request after the physical carrier has applied its trust and authentication policy. */
|
|
88
92
|
readonly fetch: (request: Request) => Promise<Response>;
|
|
89
93
|
}
|
|
@@ -149,6 +153,15 @@ export interface HostConnectionHandle {
|
|
|
149
153
|
}
|
|
150
154
|
/** Transport-independent Fetch handler used by HTTP and worker carriers. */
|
|
151
155
|
export interface ConnectionFetchHandler {
|
|
156
|
+
/**
|
|
157
|
+
* Resolve body handling before the bridge reads any request bytes.
|
|
158
|
+
* @param request - request method and URL available from node:http headers.
|
|
159
|
+
* @returns the registered route's body handling mode.
|
|
160
|
+
*/
|
|
161
|
+
requestBodyMode(request: {
|
|
162
|
+
readonly method: string;
|
|
163
|
+
readonly url: URL;
|
|
164
|
+
}): ConnectionRequestBodyMode;
|
|
152
165
|
/**
|
|
153
166
|
* Dispatch one already-authenticated request.
|
|
154
167
|
* @param request - Fetch request below the shared channel.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-connection",
|
|
3
3
|
"description": "Authenticated RPC transport, generation lifecycle, and browser fixture",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"zod": "^4.4.3",
|
|
38
|
-
"@deepseek-ai/dsh-credentials": "^0.1.
|
|
38
|
+
"@deepseek-ai/dsh-credentials": "^0.1.3-alpha.2",
|
|
39
39
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
@@ -48,15 +48,16 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/dsh-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-host-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-
|
|
51
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.3-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-attachment": "^0.1.3-alpha.2",
|
|
53
|
+
"@deepseek-ai/dsh-brand": "^0.1.3-alpha.2",
|
|
54
|
+
"@deepseek-ai/dsh-commands": "^0.1.3-alpha.2",
|
|
55
|
+
"@deepseek-ai/dsh-host-directory-picker": "^0.1.3-alpha.2",
|
|
56
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.3-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
|
|
58
|
+
"@deepseek-ai/dsh-settings": "^0.1.3-alpha.2",
|
|
59
|
+
"@deepseek-ai/dsh-tool-todo": "^0.1.3-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-util-values": "^0.1.3-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-session": "^0.1.3-alpha.2"
|
|
61
62
|
}
|
|
62
63
|
}
|