@procedures/peer 0.22.1 → 0.24.0
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/CHANGELOG.md +76 -0
- package/README.md +9 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/host/index.d.ts +1 -0
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +1 -0
- package/dist/host/index.js.map +1 -1
- package/dist/host/local-face.d.ts +42 -0
- package/dist/host/local-face.d.ts.map +1 -0
- package/dist/host/local-face.js +160 -0
- package/dist/host/local-face.js.map +1 -0
- package/dist/host/tracking.d.ts.map +1 -1
- package/dist/host/tracking.js +2 -0
- package/dist/host/tracking.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/host/index.ts +1 -0
- package/src/host/local-face.ts +183 -0
- package/src/host/tracking.ts +2 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local face of the procedures host (CLI-26): the calls of this process, through the request
|
|
3
|
+
* tracking of the service.
|
|
4
|
+
*
|
|
5
|
+
* `localFace()` gives a face factory for the option `faces` of `createProcedureHost()`, and a
|
|
6
|
+
* transport of `@procedures/client` for a `Client`. Each call of the client goes to the handler of
|
|
7
|
+
* the face, as a call of the HTTP face does. Thus the service admits the call, counts it, drains
|
|
8
|
+
* it and cuts it. The cold mode of `mark` calls its host in this process with this face.
|
|
9
|
+
*
|
|
10
|
+
* - Each call has `LOCAL_CREDENTIALS`: it is a call of this application (CLI-1). A token of the
|
|
11
|
+
* policy does not apply to it.
|
|
12
|
+
* - The signal of the call goes with the request. The cooperative abort of the drain also aborts the call.
|
|
13
|
+
* - When the signal aborts, the caller stops its wait at once, as over HTTP. The call gets the
|
|
14
|
+
* abort, and the drain of the service waits for its end.
|
|
15
|
+
* - A value is one response item. A stream gives one item for each value. An error ends the
|
|
16
|
+
* stream with an error item, with the code, the flag `retryable` and the reason of the error.
|
|
17
|
+
* - The face mounts nothing on a listener, and it has no stop parts. Before `createProcedureHost()`
|
|
18
|
+
* uses the factory, each call gets `UNAVAILABLE`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
LOCAL_CREDENTIALS,
|
|
23
|
+
type Message,
|
|
24
|
+
type ResponseItem,
|
|
25
|
+
type ServerRequest,
|
|
26
|
+
type ServerResponse,
|
|
27
|
+
type Status,
|
|
28
|
+
type Transport,
|
|
29
|
+
} from "@procedures/client";
|
|
30
|
+
import type { HostFaceFactory } from "./faces.js";
|
|
31
|
+
import type { FaceHandler } from "./tracking.js";
|
|
32
|
+
|
|
33
|
+
/** The local face: the factory for the host, and the transport for a `Client` of this process. */
|
|
34
|
+
export interface LocalFace {
|
|
35
|
+
/** The face factory. Give it to the option `faces` of `createProcedureHost()`, for example as `local`. */
|
|
36
|
+
readonly face: HostFaceFactory;
|
|
37
|
+
/** The transport of a `Client`: each call goes to the host through the request tracking of the face. */
|
|
38
|
+
readonly transport: Transport;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The value of `untilAborted` when the signal aborts first. */
|
|
42
|
+
const ABORTED: unique symbol = Symbol("aborted");
|
|
43
|
+
|
|
44
|
+
/** This function gives the value of a promise, or `ABORTED` when the signal aborts first. The promise continues. */
|
|
45
|
+
function untilAborted<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T | typeof ABORTED> {
|
|
46
|
+
if (!signal) return promise;
|
|
47
|
+
if (signal.aborted) {
|
|
48
|
+
promise.catch(() => undefined);
|
|
49
|
+
return Promise.resolve(ABORTED);
|
|
50
|
+
}
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const onAbort = (): void => {
|
|
53
|
+
promise.catch(() => undefined);
|
|
54
|
+
resolve(ABORTED);
|
|
55
|
+
};
|
|
56
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
57
|
+
promise.then(
|
|
58
|
+
(value) => {
|
|
59
|
+
signal.removeEventListener("abort", onAbort);
|
|
60
|
+
resolve(value);
|
|
61
|
+
},
|
|
62
|
+
(error: unknown) => {
|
|
63
|
+
signal.removeEventListener("abort", onAbort);
|
|
64
|
+
reject(error);
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** This function gives a response item of a status. */
|
|
71
|
+
function item<T>(id: string, status: Status, payload: unknown, metadata: Record<string, unknown> = {}): ResponseItem<T> {
|
|
72
|
+
return { id, status, payload: payload as T, metadata };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** This function gives the error item of an abort of the caller: the item of the local transport. */
|
|
76
|
+
function abortedItem<T>(id: string): ResponseItem<T> {
|
|
77
|
+
return item<T>(id, { type: "error", code: "ABORTED", message: "Request was aborted", retryable: false }, null);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** This function gives the error item of an error that ends a stream: its code, its flag `retryable` and its reason. */
|
|
81
|
+
function errorItem<T>(id: string, error: unknown): ResponseItem<T> {
|
|
82
|
+
const fields = (error ?? {}) as { code?: unknown; retryable?: unknown; reason?: unknown };
|
|
83
|
+
const code = typeof fields.code === "string" ? fields.code : "HANDLER_ERROR";
|
|
84
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
|
+
const retryable = typeof fields.retryable === "boolean" ? fields.retryable : false;
|
|
86
|
+
return item<T>(id, { type: "error", code, message, retryable }, null, typeof fields.reason === "string" ? { reason: fields.reason } : {});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** This function reads a stream that nobody reads to its end, so its request ends. The call has its abort, thus it ends soon. */
|
|
90
|
+
async function discard(response: ServerResponse): Promise<void> {
|
|
91
|
+
const values = response.stream?.[Symbol.asyncIterator]();
|
|
92
|
+
if (!values) return;
|
|
93
|
+
try {
|
|
94
|
+
while (!(await values.next()).done) {
|
|
95
|
+
// Each value goes nowhere
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// The end of the call after its abort
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The response items of a response of the face: one item for a value or an error, and one item for each value of a stream. */
|
|
103
|
+
async function* itemsOf<T>(id: string, response: ServerResponse, signal: AbortSignal | undefined): AsyncGenerator<ResponseItem<T>, void, undefined> {
|
|
104
|
+
if (response.status.type === "error" || response.stream === undefined) {
|
|
105
|
+
yield item<T>(id, response.status, response.status.type === "error" ? null : response.payload, response.metadata ?? {});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const values = response.stream[Symbol.asyncIterator]();
|
|
109
|
+
let ended = false;
|
|
110
|
+
try {
|
|
111
|
+
for (;;) {
|
|
112
|
+
const next = await untilAborted(values.next(), signal);
|
|
113
|
+
if (next === ABORTED) {
|
|
114
|
+
yield abortedItem<T>(id);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (next.done) {
|
|
118
|
+
ended = true;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
yield item<T>(id, { type: "success", code: 200 }, next.value);
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
ended = true;
|
|
125
|
+
yield errorItem<T>(id, error);
|
|
126
|
+
} finally {
|
|
127
|
+
// A reader that stops early stops the call: the work gets its abort
|
|
128
|
+
if (!ended) void values.return?.(undefined)?.catch(() => undefined);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* This function gives the local face of a host (CLI-26). One face serves one host.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```typescript
|
|
137
|
+
* const local = localFace();
|
|
138
|
+
* const host = createProcedureHost({ registry, faces: { local: local.face } });
|
|
139
|
+
* const life = await host.declare(service("mark").profile(ephemeral)).run();
|
|
140
|
+
* const client = new Client({ transport: local.transport });
|
|
141
|
+
* for await (const item of client.stream({ service: "git", operation: "status" }, {}, { signal })) print(item);
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export function localFace(): LocalFace {
|
|
145
|
+
let handler: FaceHandler | undefined;
|
|
146
|
+
const face: HostFaceFactory = (context) => {
|
|
147
|
+
handler = context.handler;
|
|
148
|
+
return {};
|
|
149
|
+
};
|
|
150
|
+
const transport: Transport = {
|
|
151
|
+
name: "local-face",
|
|
152
|
+
async *send<TReq, TRes>(message: Message<TReq>): AsyncIterable<ResponseItem<TRes>> {
|
|
153
|
+
const target = handler;
|
|
154
|
+
if (!target) {
|
|
155
|
+
yield item<TRes>(message.id, { type: "error", code: "UNAVAILABLE", message: "The local face has no host: give its factory to createProcedureHost()", retryable: false }, null);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (message.signal?.aborted) {
|
|
159
|
+
yield abortedItem<TRes>(message.id);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const request: ServerRequest = {
|
|
163
|
+
id: message.id,
|
|
164
|
+
method: message.method,
|
|
165
|
+
payload: message.payload,
|
|
166
|
+
metadata: { ...message.metadata },
|
|
167
|
+
credentials: LOCAL_CREDENTIALS,
|
|
168
|
+
...(message.signal ? { signal: message.signal } : {}),
|
|
169
|
+
};
|
|
170
|
+
const pending = target.handle(request);
|
|
171
|
+
const response = await untilAborted(pending, message.signal);
|
|
172
|
+
if (response === ABORTED) {
|
|
173
|
+
// A stream that comes after the abort still holds its request: it is read to its end
|
|
174
|
+
void pending.then(discard, () => undefined);
|
|
175
|
+
yield abortedItem<TRes>(message.id);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
yield* itemsOf<TRes>(message.id, response, message.signal);
|
|
179
|
+
},
|
|
180
|
+
close: async () => undefined,
|
|
181
|
+
};
|
|
182
|
+
return { face, transport };
|
|
183
|
+
}
|
package/src/host/tracking.ts
CHANGED
|
@@ -220,6 +220,8 @@ export function faceHandler(options: FaceHandlerOptions): FaceHandler {
|
|
|
220
220
|
return {
|
|
221
221
|
ambientCredentials: server.ambientCredentials,
|
|
222
222
|
getRegistry: () => server.getRegistry(),
|
|
223
|
+
// The principal before a call (CLI-69): the MCP face keys the fair share of its tasks with it
|
|
224
|
+
identify: (credentials) => server.identify(credentials),
|
|
223
225
|
handle: (request: ServerRequest): Promise<ServerResponse> => {
|
|
224
226
|
const life = options.life();
|
|
225
227
|
return life ? trackedCall(life, options, request) : server.handle(request);
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export {
|
|
|
31
31
|
createProcedureHost,
|
|
32
32
|
httpFace,
|
|
33
33
|
wsFace,
|
|
34
|
+
localFace,
|
|
34
35
|
LOCAL_LISTENER_KNOBS,
|
|
35
36
|
HOST_PROBE_PREFIX,
|
|
36
37
|
callOperations,
|
|
@@ -60,6 +61,7 @@ export type {
|
|
|
60
61
|
HostFaceEndpoint,
|
|
61
62
|
HostFaceFactory,
|
|
62
63
|
HostFaceUpgrade,
|
|
64
|
+
LocalFace,
|
|
63
65
|
CallDetail,
|
|
64
66
|
CallOperation,
|
|
65
67
|
FaceHandler,
|