ankka 0.10.0 → 0.12.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/README.md +1 -0
- package/dist/_proto/ankka/protocol/v1/agent_pb.d.ts +35 -0
- package/dist/_proto/ankka/protocol/v1/agent_pb.js +8 -1
- package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +858 -2
- package/dist/_proto/ankka/protocol/v1/client_pb.js +184 -9
- package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +10 -1
- package/dist/_proto/ankka/protocol/v1/consumer_pb.js +1 -1
- package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +373 -0
- package/dist/_proto/ankka/protocol/v1/discovery_pb.js +81 -16
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +210 -0
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +35 -5
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +86 -0
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +11 -1
- package/dist/_proto/ankka/protocol/v1/payload_pb.d.ts +61 -0
- package/dist/_proto/ankka/protocol/v1/payload_pb.js +18 -1
- package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +70 -1
- package/dist/_proto/ankka/protocol/v1/view_pb.js +11 -1
- package/dist/_proto/ankka/protocol/v1/wasm_pb.d.ts +12 -0
- package/dist/_proto/ankka/protocol/v1/wasm_pb.js +1 -1
- package/dist/_proto/ankka/protocol/v1/workflow_pb.d.ts +7 -0
- package/dist/_proto/ankka/protocol/v1/workflow_pb.js +1 -1
- package/dist/agent.d.ts +15 -1
- package/dist/agent.js +20 -0
- package/dist/approvals.d.ts +47 -0
- package/dist/approvals.js +57 -0
- package/dist/autonomous.d.ts +23 -1
- package/dist/autonomous.js +30 -1
- package/dist/client.d.ts +188 -7
- package/dist/client.js +490 -21
- package/dist/consumer.d.ts +42 -3
- package/dist/consumer.js +35 -1
- package/dist/context.d.ts +29 -1
- package/dist/context.js +5 -2
- package/dist/contract.d.ts +40 -0
- package/dist/contract.js +60 -0
- package/dist/effects/common.d.ts +7 -0
- package/dist/effects/common.js +9 -1
- package/dist/effects/eventSourced.d.ts +26 -1
- package/dist/effects/eventSourced.js +58 -5
- package/dist/effects/keyed.d.ts +33 -0
- package/dist/effects/keyed.js +41 -0
- package/dist/endpoint.d.ts +10 -1
- package/dist/endpoint.js +20 -0
- package/dist/eventSourcedEntity.d.ts +8 -3
- package/dist/graph.d.ts +24 -2
- package/dist/graph.js +27 -1
- package/dist/handlers.d.ts +10 -2
- package/dist/handlers.js +6 -2
- package/dist/index.d.ts +15 -6
- package/dist/index.js +12 -3
- package/dist/keyedView.d.ts +59 -0
- package/dist/keyedView.js +73 -0
- package/dist/kinds.js +2 -0
- package/dist/mcp.d.ts +57 -0
- package/dist/mcp.js +64 -0
- package/dist/routes.d.ts +44 -4
- package/dist/routes.js +43 -4
- package/dist/server/agent.js +12 -4
- package/dist/server/discovery.d.ts +17 -1
- package/dist/server/discovery.js +182 -1
- package/dist/server/eventSourced.d.ts +4 -1
- package/dist/server/eventSourced.js +24 -6
- package/dist/server/http.d.ts +2 -1
- package/dist/server/http.js +80 -4
- package/dist/server/stateless.js +42 -3
- package/dist/server/workflow.js +5 -1
- package/dist/service.d.ts +46 -2
- package/dist/service.js +235 -15
- package/dist/services.d.ts +137 -0
- package/dist/services.js +259 -0
- package/dist/socket.d.ts +15 -0
- package/dist/socket.js +25 -0
- package/dist/spec.d.ts +1 -1
- package/dist/spec.js +94 -15
- package/dist/standing.d.ts +18 -0
- package/dist/standing.js +26 -0
- package/dist/startFrom.d.ts +36 -0
- package/dist/startFrom.js +51 -0
- package/dist/testkit/index.d.ts +2 -2
- package/dist/testkit/index.js +1 -1
- package/dist/testkit/integration.js +5 -0
- package/dist/testkit/kinds.d.ts +50 -5
- package/dist/testkit/kinds.js +208 -35
- package/dist/testkit/unit.d.ts +41 -3
- package/dist/testkit/unit.js +87 -10
- package/dist/timedAction.d.ts +17 -2
- package/dist/timedAction.js +33 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/view.d.ts +48 -1
- package/dist/view.js +18 -1
- package/dist/workflow.d.ts +18 -1
- package/dist/workflow.js +39 -0
- package/package.json +3 -2
package/dist/routes.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Socket } from "./socket.ts";
|
|
1
2
|
import type { Shape } from "./codec.ts";
|
|
2
3
|
import type { RequestContext } from "./context.ts";
|
|
3
4
|
import type { Infer, Schema } from "./schema.ts";
|
|
@@ -12,15 +13,20 @@ export type CallerMatcher = {
|
|
|
12
13
|
readonly kind: "anyInProject";
|
|
13
14
|
} | {
|
|
14
15
|
readonly kind: "self";
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: "granted";
|
|
15
18
|
};
|
|
16
19
|
/**
|
|
17
20
|
* The callers an endpoint can name: `Acl.allowCallers(Callers.internet, Callers.service("orders"))`. A named
|
|
18
|
-
* service is in this service's own project unless `project` names another.
|
|
21
|
+
* service is in this service's own project unless `project` names another. `Callers.granted` is whoever this
|
|
22
|
+
* service's project has granted the route to — a service of another project, or a registered machine
|
|
23
|
+
* (protocol 1.15); a route that does not name it can never be granted.
|
|
19
24
|
*/
|
|
20
25
|
export declare const Callers: Readonly<{
|
|
21
26
|
internet: CallerMatcher;
|
|
22
27
|
anyInProject: CallerMatcher;
|
|
23
28
|
self: CallerMatcher;
|
|
29
|
+
granted: CallerMatcher;
|
|
24
30
|
service(name: string, options?: {
|
|
25
31
|
readonly project?: string;
|
|
26
32
|
}): CallerMatcher;
|
|
@@ -74,8 +80,10 @@ export interface RouteRef<Ep = unknown, P = unknown, B = unknown, R = unknown> {
|
|
|
74
80
|
readonly body: Shape<B> | undefined;
|
|
75
81
|
readonly reply: Shape<R> | undefined;
|
|
76
82
|
readonly streaming: boolean;
|
|
83
|
+
/** Answered by opening a socket (protocol 1.9); `run`'s third argument is then the `Socket`. */
|
|
84
|
+
readonly socket: boolean;
|
|
77
85
|
readonly acl: Acl | undefined;
|
|
78
|
-
readonly run: (self: Ep, request: RequestContext<P>, body: B) => MaybePromise<R> | AsyncIterable<string>;
|
|
86
|
+
readonly run: (self: Ep, request: RequestContext<P>, body: B) => MaybePromise<R> | AsyncIterable<string | SseEvent>;
|
|
79
87
|
}
|
|
80
88
|
export declare function parseTemplate(template: string): string[];
|
|
81
89
|
type NoBodyRun<Ep, T extends string, PS, R> = (self: Ep, request: RequestContext<Params<T, PS>>) => MaybePromise<R>;
|
|
@@ -100,8 +108,40 @@ export declare const del: {
|
|
|
100
108
|
<Ep, T extends string, B, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, body: Shape<B>, reply: Shape<R>, run: BodyRun<Ep, T, PS, B, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, B, R>;
|
|
101
109
|
<Ep, T extends string, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, reply: Shape<R>, run: NoBodyRun<Ep, T, PS, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, R>;
|
|
102
110
|
};
|
|
103
|
-
/**
|
|
104
|
-
|
|
111
|
+
/**
|
|
112
|
+
* A server-sent event under a name of its own, which an `sse` handler may yield beside text: an agent
|
|
113
|
+
* turn that stops for approval ends its stream with `sseEvent("approval", requests)`. `value` is sent as
|
|
114
|
+
* JSON. Needs protocol 1.11 of the runtime.
|
|
115
|
+
*/
|
|
116
|
+
export interface SseEvent {
|
|
117
|
+
readonly kind: "sse-event";
|
|
118
|
+
readonly name: string;
|
|
119
|
+
readonly value: unknown;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A watch as server-sent events, for an `sse` route: each row as `row` carrying the key and the row,
|
|
123
|
+
* each removal as `removed`, the marker as `caught-up`, and a watch that ended with a reason as a last
|
|
124
|
+
* event `ended` carrying it, so a page is told why.
|
|
125
|
+
*/
|
|
126
|
+
export declare function sseEvents<Row>(watch: AsyncIterable<{
|
|
127
|
+
readonly kind: "row";
|
|
128
|
+
readonly key: string;
|
|
129
|
+
readonly row: Row;
|
|
130
|
+
} | {
|
|
131
|
+
readonly kind: "removed";
|
|
132
|
+
readonly key: string;
|
|
133
|
+
} | {
|
|
134
|
+
readonly kind: "caughtUp";
|
|
135
|
+
}>): AsyncIterable<SseEvent>;
|
|
136
|
+
export declare function sseEvent(name: string, value: unknown): SseEvent;
|
|
137
|
+
/** A server-sent-events route: the handler returns an `AsyncIterable<string | SseEvent>`, one frame per string, and may yield an `SseEvent` among them. */
|
|
138
|
+
export declare function sse<Ep, T extends string, PS extends Readonly<Record<string, Schema>> = {}>(template: T, run: (self: Ep, request: RequestContext<Params<T, PS>>) => AsyncIterable<string | SseEvent> | Promise<AsyncIterable<string | SseEvent>>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, never>;
|
|
139
|
+
/**
|
|
140
|
+
* A socket route: `socket("/{room}", async (ep: Rooms, req, socket) => { for await (const t of socket) await socket.send(t) })`.
|
|
141
|
+
* The handler runs for as long as the socket is open, with the opening request as `req` throughout.
|
|
142
|
+
* Needs protocol 1.9 of the runtime.
|
|
143
|
+
*/
|
|
144
|
+
export declare function socket<Ep, T extends string, PS extends Readonly<Record<string, Schema>> = {}>(template: T, run: (self: Ep, request: RequestContext<Params<T, PS>>, socket: Socket) => Promise<void>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, Socket, void>;
|
|
105
145
|
/** The routes table type an endpoint declares. */
|
|
106
146
|
export type RouteTable<Ep> = Readonly<Record<string, RouteRef<Ep, any, any, any>>>;
|
|
107
147
|
export {};
|
package/dist/routes.js
CHANGED
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
import { isCodec } from "./codec.js";
|
|
12
12
|
/**
|
|
13
13
|
* The callers an endpoint can name: `Acl.allowCallers(Callers.internet, Callers.service("orders"))`. A named
|
|
14
|
-
* service is in this service's own project unless `project` names another.
|
|
14
|
+
* service is in this service's own project unless `project` names another. `Callers.granted` is whoever this
|
|
15
|
+
* service's project has granted the route to — a service of another project, or a registered machine
|
|
16
|
+
* (protocol 1.15); a route that does not name it can never be granted.
|
|
15
17
|
*/
|
|
16
18
|
export const Callers = Object.freeze({
|
|
17
19
|
internet: Object.freeze({ kind: "internet" }),
|
|
18
20
|
anyInProject: Object.freeze({ kind: "anyInProject" }),
|
|
19
21
|
self: Object.freeze({ kind: "self" }),
|
|
22
|
+
granted: Object.freeze({ kind: "granted" }),
|
|
20
23
|
service(name, options) {
|
|
21
24
|
return Object.freeze(options?.project !== undefined ? { kind: "service", name, project: options.project } : { kind: "service", name });
|
|
22
25
|
},
|
|
@@ -57,7 +60,7 @@ export function parseTemplate(template) {
|
|
|
57
60
|
throw new TypeError(`route template ${template} repeats a parameter`);
|
|
58
61
|
return names;
|
|
59
62
|
}
|
|
60
|
-
function route(method, template, body, reply, run, options, streaming) {
|
|
63
|
+
function route(method, template, body, reply, run, options, streaming, socket = false) {
|
|
61
64
|
const paramNames = parseTemplate(template);
|
|
62
65
|
if (typeof run !== "function")
|
|
63
66
|
throw new TypeError(`route ${method} ${template}: the handler is not a function`);
|
|
@@ -68,7 +71,7 @@ function route(method, template, body, reply, run, options, streaming) {
|
|
|
68
71
|
if (!paramNames.includes(name))
|
|
69
72
|
throw new TypeError(`route ${method} ${template}: params names ${name}, which is not in the template`);
|
|
70
73
|
}
|
|
71
|
-
return Object.freeze({ method, template, paramNames, paramShapes, body, reply, streaming, acl: options?.acl, run });
|
|
74
|
+
return Object.freeze({ method, template, paramNames, paramShapes, body, reply, streaming, socket, acl: options?.acl, run });
|
|
72
75
|
}
|
|
73
76
|
function isShape(x) {
|
|
74
77
|
return typeof x === "object" && x !== null && (typeof x.kind === "string" || isCodec(x));
|
|
@@ -96,7 +99,43 @@ export const put = withBody("PUT");
|
|
|
96
99
|
export const patch = withBody("PATCH");
|
|
97
100
|
/** A DELETE route; `delete` is a reserved word, so `del`. */
|
|
98
101
|
export const del = withBody("DELETE");
|
|
99
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* A watch as server-sent events, for an `sse` route: each row as `row` carrying the key and the row,
|
|
104
|
+
* each removal as `removed`, the marker as `caught-up`, and a watch that ended with a reason as a last
|
|
105
|
+
* event `ended` carrying it, so a page is told why.
|
|
106
|
+
*/
|
|
107
|
+
export async function* sseEvents(watch) {
|
|
108
|
+
try {
|
|
109
|
+
for await (const event of watch) {
|
|
110
|
+
if (event.kind === "row")
|
|
111
|
+
yield sseEvent("row", { key: event.key, row: event.row });
|
|
112
|
+
else if (event.kind === "removed")
|
|
113
|
+
yield sseEvent("removed", { key: event.key });
|
|
114
|
+
else
|
|
115
|
+
yield sseEvent("caught-up", {});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (failure) {
|
|
119
|
+
if (failure instanceof Error && failure.name === "WatchEnded")
|
|
120
|
+
yield sseEvent("ended", { reason: failure.reason });
|
|
121
|
+
else
|
|
122
|
+
throw failure;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function sseEvent(name, value) {
|
|
126
|
+
if (typeof name !== "string" || name === "" || /[\r\n]/.test(name))
|
|
127
|
+
throw new TypeError(`an event name must be one non-empty line, not ${JSON.stringify(name)}`);
|
|
128
|
+
return Object.freeze({ kind: "sse-event", name, value });
|
|
129
|
+
}
|
|
130
|
+
/** A server-sent-events route: the handler returns an `AsyncIterable<string | SseEvent>`, one frame per string, and may yield an `SseEvent` among them. */
|
|
100
131
|
export function sse(template, run, options) {
|
|
101
132
|
return route("GET", template, undefined, undefined, run, options, true);
|
|
102
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* A socket route: `socket("/{room}", async (ep: Rooms, req, socket) => { for await (const t of socket) await socket.send(t) })`.
|
|
136
|
+
* The handler runs for as long as the socket is open, with the opening request as `req` throughout.
|
|
137
|
+
* Needs protocol 1.9 of the runtime.
|
|
138
|
+
*/
|
|
139
|
+
export function socket(template, run, options) {
|
|
140
|
+
return route("GET", template, undefined, undefined, run, options, false, true);
|
|
141
|
+
}
|
package/dist/server/agent.js
CHANGED
|
@@ -84,7 +84,10 @@ export async function handleTool(req, ctx) {
|
|
|
84
84
|
if (!registered)
|
|
85
85
|
return create(ToolResultSchema, { result: { case: "error", value: `no agent ${JSON.stringify(req.componentId)} is registered` } });
|
|
86
86
|
const agent = new registered.cls();
|
|
87
|
-
|
|
87
|
+
// What the sidecar says about the tool's run: its trace, and the agent's handler as the caller of
|
|
88
|
+
// whatever the tool calls. A sidecar before 1.3 sends none.
|
|
89
|
+
const metadata = metadataFromProto(req.metadata);
|
|
90
|
+
agent._bind(req.sessionId, metadata, ctx.client.withMetadata(metadata));
|
|
88
91
|
try {
|
|
89
92
|
return create(ToolResultSchema, { result: { case: "ok", value: await runTool(registered, agent, req.tool, req.argumentsJson) } });
|
|
90
93
|
}
|
|
@@ -95,11 +98,16 @@ export async function handleTool(req, ctx) {
|
|
|
95
98
|
}
|
|
96
99
|
export async function handleGuardrail(req, ctx) {
|
|
97
100
|
const registered = ctx.registry.of("agent", req.componentId) ?? ctx.registry.of("autonomous-agent", req.componentId);
|
|
98
|
-
|
|
101
|
+
// RESULT (1.11): what an MCP server's tool answered, checked by one of the agent's result guardrails,
|
|
102
|
+
// which are declared apart from its other guardrails.
|
|
103
|
+
const result = req.stage === GuardrailRequest_Stage.RESULT;
|
|
104
|
+
const guardrail = result ? registered?.resultGuardrails.get(req.guardrail) : registered?.guardrails.get(req.guardrail);
|
|
99
105
|
if (!registered || !guardrail)
|
|
100
106
|
return create(GuardrailResultSchema, { result: { case: "block", value: `no guardrail ${JSON.stringify(req.guardrail)} on ${req.componentId}` } });
|
|
101
107
|
try {
|
|
102
|
-
const reason =
|
|
108
|
+
const reason = result
|
|
109
|
+
? await guardrail.check(req.tool ?? "", req.text)
|
|
110
|
+
: await guardrail.check(req.stage === GuardrailRequest_Stage.OUTPUT ? "output" : "input", req.text);
|
|
103
111
|
return reason === null || reason === undefined
|
|
104
112
|
? create(GuardrailResultSchema, { result: { case: "pass", value: {} } })
|
|
105
113
|
: create(GuardrailResultSchema, { result: { case: "block", value: reason } });
|
|
@@ -114,7 +122,7 @@ async function handleAutonomousTool(registered, req, ctx) {
|
|
|
114
122
|
if (!tool)
|
|
115
123
|
return create(ToolResultSchema, { result: { case: "error", value: `no tool ${JSON.stringify(req.tool)} on ${registered.id}` } });
|
|
116
124
|
const agent = new registered.cls();
|
|
117
|
-
agent._bind(req.sessionId, ctx.client);
|
|
125
|
+
agent._bind(req.sessionId, ctx.client.withMetadata(metadataFromProto(req.metadata)));
|
|
118
126
|
try {
|
|
119
127
|
let input;
|
|
120
128
|
if (isCodec(tool.input))
|
|
@@ -1,5 +1,21 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ConnectRouter } from "@connectrpc/connect";
|
|
2
2
|
import { type Spec } from "../_proto/ankka/protocol/v1/discovery_pb.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A sidecar older than 1.7 would ignore where a topic source starts and its version: a consumer
|
|
5
|
+
* declared `latest` would read everything, and a raised version would rebuild nothing. Refused,
|
|
6
|
+
* naming what declares them, rather than served wrong.
|
|
7
|
+
*/
|
|
8
|
+
export declare function refusal(spec: Spec, sidecarProtocol: string): string | undefined;
|
|
3
9
|
/** Every problem the sidecar has reported to this process, in order. */
|
|
4
10
|
export declare const problems: string[];
|
|
5
11
|
export declare function discoveryRoutes(router: ConnectRouter, spec: () => Spec, log?: (message: string) => void): void;
|
|
12
|
+
/** The protocol version whose runtimes serve socket routes. */
|
|
13
|
+
export declare const SOCKETS_SINCE: readonly [number, number];
|
|
14
|
+
/** Why `spec` cannot be declared to a runtime speaking `runtime`, or undefined. A runtime before 1.9
|
|
15
|
+
* does not know a socket route, and would serve it as a plain GET. */
|
|
16
|
+
export declare function socketRefusal(spec: Spec, runtime: string): string | undefined;
|
|
17
|
+
/** The protocol version whose runtimes admit granted callers. */
|
|
18
|
+
export declare const GRANTS_SINCE: readonly [number, number];
|
|
19
|
+
/** Why `spec` cannot be declared to a runtime speaking `runtime`, or undefined. A runtime before 1.15
|
|
20
|
+
* does not know `Callers.granted`, and would admit nobody by it. */
|
|
21
|
+
export declare function grantsRefusal(spec: Spec, runtime: string): string | undefined;
|
package/dist/server/discovery.js
CHANGED
|
@@ -1,16 +1,158 @@
|
|
|
1
1
|
// Discovery: the first conversation. The sidecar asks what we host; we answer with the Spec the
|
|
2
2
|
// registry renders. `ReportError` is how the sidecar tells us why it refused to start — logged, and kept
|
|
3
3
|
// so a service can expose it (the conformance reference does, at GET /conformance/problems).
|
|
4
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
4
5
|
import { create } from "@bufbuild/protobuf";
|
|
5
6
|
import { Discovery } from "../_proto/ankka/protocol/v1/discovery_pb.js";
|
|
6
7
|
import { EmptySchema } from "../_proto/ankka/protocol/v1/payload_pb.js";
|
|
8
|
+
import { PROTOCOL_VERSION } from "../spec.js";
|
|
9
|
+
import { Kind } from "../_proto/ankka/protocol/v1/discovery_pb.js";
|
|
10
|
+
import { olderThan, olderThanContracts, olderThanDeclaredQueries, olderThanStartPositions, olderThanViewStreams, olderThanWorkflowSources } from "../startFrom.js";
|
|
11
|
+
/**
|
|
12
|
+
* A sidecar older than 1.7 would ignore where a topic source starts and its version: a consumer
|
|
13
|
+
* declared `latest` would read everything, and a raised version would rebuild nothing. Refused,
|
|
14
|
+
* naming what declares them, rather than served wrong.
|
|
15
|
+
*/
|
|
16
|
+
export function refusal(spec, sidecarProtocol) {
|
|
17
|
+
return (workflowSourceRefusal(spec, sidecarProtocol) ??
|
|
18
|
+
startPositionRefusal(spec, sidecarProtocol) ??
|
|
19
|
+
declaredQueryRefusal(spec, sidecarProtocol) ??
|
|
20
|
+
contractRefusal(spec, sidecarProtocol) ??
|
|
21
|
+
watchedQueryRefusal(spec, sidecarProtocol) ??
|
|
22
|
+
// Last: a reason an older sidecar has too is named first, by the version it needs.
|
|
23
|
+
replicationRefusal(spec, sidecarProtocol));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A sidecar older than 1.19 would host a replicated entity at one installation and say nothing, and
|
|
27
|
+
* would hand a consumer given mirrored changes only the changes written here. Refused at discovery,
|
|
28
|
+
* naming the components and the version.
|
|
29
|
+
*/
|
|
30
|
+
function replicationRefusal(spec, sidecarProtocol) {
|
|
31
|
+
if (!olderThan(sidecarProtocol, [1, 19]))
|
|
32
|
+
return undefined;
|
|
33
|
+
const replicated = spec.components
|
|
34
|
+
.filter((c) => (c.detail.case === "eventSourced" && c.detail.value.replicated) ||
|
|
35
|
+
(c.detail.case === "consumer" && c.detail.value.mirroredChanges))
|
|
36
|
+
.map((c) => c.id);
|
|
37
|
+
if (replicated.length === 0)
|
|
38
|
+
return undefined;
|
|
39
|
+
return (`${replicated.join(", ")} declare replication, which needs protocol 1.19: ` +
|
|
40
|
+
`the sidecar speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.19 or later.`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A sidecar older than 1.16 would not read a workflow as a source: a view or consumer that does would
|
|
44
|
+
* be handed nothing. Refused at discovery, naming them.
|
|
45
|
+
*/
|
|
46
|
+
function workflowSourceRefusal(spec, sidecarProtocol) {
|
|
47
|
+
if (!olderThanWorkflowSources(sidecarProtocol))
|
|
48
|
+
return undefined;
|
|
49
|
+
const reading = spec.components
|
|
50
|
+
.filter((c) => {
|
|
51
|
+
const d = c.detail;
|
|
52
|
+
if (d.case !== "view" && d.case !== "consumer")
|
|
53
|
+
return false;
|
|
54
|
+
const sources = d.case === "view" ? [d.value.source, ...d.value.sources] : [d.value.source];
|
|
55
|
+
return sources.some((s) => s?.source.case === "component" && s.source.value.kind === Kind.WORKFLOW);
|
|
56
|
+
})
|
|
57
|
+
.map((c) => c.id);
|
|
58
|
+
if (reading.length === 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
return (`${reading.join(", ")} read a workflow, which the sidecar does not know: ` +
|
|
61
|
+
`it speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.16 or later.`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A sidecar older than 1.14 would ignore a topic's contract, broker and parallel reading and a
|
|
65
|
+
* publication's contract and broker: a contract would go unchecked, a topic on another broker would be
|
|
66
|
+
* read from the installation's. Refused at discovery, naming the components, rather than served wrong.
|
|
67
|
+
*/
|
|
68
|
+
function contractRefusal(spec, sidecarProtocol) {
|
|
69
|
+
if (!olderThanContracts(sidecarProtocol))
|
|
70
|
+
return undefined;
|
|
71
|
+
const declaring = spec.components
|
|
72
|
+
.filter((c) => {
|
|
73
|
+
const d = c.detail;
|
|
74
|
+
if (d.case !== "view" && d.case !== "consumer")
|
|
75
|
+
return false;
|
|
76
|
+
const src = d.value.source;
|
|
77
|
+
const onSource = src !== undefined && (src.contract !== undefined || src.broker !== undefined || src.parallel === true);
|
|
78
|
+
const onPublication = d.case === "consumer" && d.value.produces !== undefined && (d.value.produces.contract !== undefined || d.value.produces.broker !== undefined);
|
|
79
|
+
return onSource || onPublication;
|
|
80
|
+
})
|
|
81
|
+
.map((c) => c.id);
|
|
82
|
+
if (declaring.length === 0)
|
|
83
|
+
return undefined;
|
|
84
|
+
return (`${declaring.join(", ")} declare a topic's contract, broker or parallel reading, which the sidecar ignores: ` +
|
|
85
|
+
`it speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.14 or later.`);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A sidecar older than 1.17 would not know that a view's query may be watched, and a watch of it would
|
|
89
|
+
* be refused at its first asking. Refused at discovery, naming the views.
|
|
90
|
+
*/
|
|
91
|
+
function watchedQueryRefusal(spec, sidecarProtocol) {
|
|
92
|
+
if (!olderThanViewStreams(sidecarProtocol))
|
|
93
|
+
return undefined;
|
|
94
|
+
const watching = spec.components
|
|
95
|
+
.filter((c) => c.detail.case === "view" && c.detail.value.declaredQueries.some((q) => q.watched))
|
|
96
|
+
.map((c) => c.id);
|
|
97
|
+
if (watching.length === 0)
|
|
98
|
+
return undefined;
|
|
99
|
+
return (`${watching.join(", ")} declare a watched query, which the sidecar does not know: ` +
|
|
100
|
+
`it speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.17 or later.`);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A sidecar older than 1.13 would ignore a view's declared queries, would read a keyed view as having
|
|
104
|
+
* no source, and would refuse a version on a view that reads an entity as applying only to a topic.
|
|
105
|
+
* Refused at discovery, naming the views, rather than found later or misreported.
|
|
106
|
+
*/
|
|
107
|
+
function declaredQueryRefusal(spec, sidecarProtocol) {
|
|
108
|
+
if (!olderThanDeclaredQueries(sidecarProtocol))
|
|
109
|
+
return undefined;
|
|
110
|
+
const declaring = spec.components
|
|
111
|
+
.filter((c) => {
|
|
112
|
+
if (c.detail.case !== "view")
|
|
113
|
+
return false;
|
|
114
|
+
const v = c.detail.value;
|
|
115
|
+
// A declared query, a keyed view, or a version on a view that reads an entity.
|
|
116
|
+
return v.declaredQueries.length > 0 || v.sources.length > 0 || (v.version !== undefined && v.source?.source.case === "component");
|
|
117
|
+
})
|
|
118
|
+
.map((c) => c.id);
|
|
119
|
+
if (declaring.length === 0)
|
|
120
|
+
return undefined;
|
|
121
|
+
return (`${declaring.join(", ")} declare queries, several sources, or a version on a view that reads an entity, which the sidecar ignores: ` +
|
|
122
|
+
`it speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.13 or later.`);
|
|
123
|
+
}
|
|
124
|
+
function startPositionRefusal(spec, sidecarProtocol) {
|
|
125
|
+
if (!olderThanStartPositions(sidecarProtocol))
|
|
126
|
+
return undefined;
|
|
127
|
+
const declaring = spec.components
|
|
128
|
+
.filter((c) => {
|
|
129
|
+
const d = c.detail;
|
|
130
|
+
if (d.case !== "view" && d.case !== "consumer")
|
|
131
|
+
return false;
|
|
132
|
+
return d.value.source?.startFrom !== undefined || d.value.version !== undefined;
|
|
133
|
+
})
|
|
134
|
+
.map((c) => c.id);
|
|
135
|
+
if (declaring.length === 0)
|
|
136
|
+
return undefined;
|
|
137
|
+
return (`${declaring.join(", ")} declare where a topic source starts or its version, which the sidecar ignores: ` +
|
|
138
|
+
`it speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.7 or later.`);
|
|
139
|
+
}
|
|
7
140
|
/** Every problem the sidecar has reported to this process, in order. */
|
|
8
141
|
export const problems = [];
|
|
9
142
|
export function discoveryRoutes(router, spec, log = console.error) {
|
|
10
143
|
router.service(Discovery, {
|
|
11
144
|
async discover(info) {
|
|
12
145
|
log(`ankka: discovery from sidecar (protocol ${info.protocolVersion}, runtime ${info.runtimeVersion})`);
|
|
13
|
-
|
|
146
|
+
const answer = spec();
|
|
147
|
+
const why = refusal(answer, info.protocolVersion) ??
|
|
148
|
+
socketRefusal(answer, info.protocolVersion) ??
|
|
149
|
+
grantsRefusal(answer, info.protocolVersion);
|
|
150
|
+
if (why !== undefined) {
|
|
151
|
+
log(`ankka: ${why}`);
|
|
152
|
+
problems.push(why);
|
|
153
|
+
throw new ConnectError(why, Code.FailedPrecondition);
|
|
154
|
+
}
|
|
155
|
+
return answer;
|
|
14
156
|
},
|
|
15
157
|
async reportError(problem) {
|
|
16
158
|
log(`ankka: the sidecar refused to start: ${problem.message}`);
|
|
@@ -19,3 +161,42 @@ export function discoveryRoutes(router, spec, log = console.error) {
|
|
|
19
161
|
},
|
|
20
162
|
});
|
|
21
163
|
}
|
|
164
|
+
/** The protocol version whose runtimes serve socket routes. */
|
|
165
|
+
export const SOCKETS_SINCE = [1, 9];
|
|
166
|
+
/** Why `spec` cannot be declared to a runtime speaking `runtime`, or undefined. A runtime before 1.9
|
|
167
|
+
* does not know a socket route, and would serve it as a plain GET. */
|
|
168
|
+
export function socketRefusal(spec, runtime) {
|
|
169
|
+
const routes = spec.endpoints.flatMap((e) => e.routes.filter((r) => r.socket).map((r) => `${e.id}.${r.id}`));
|
|
170
|
+
if (routes.length === 0)
|
|
171
|
+
return undefined;
|
|
172
|
+
const [major, minor] = runtime.split(".").map((part) => Number.parseInt(part, 10));
|
|
173
|
+
const speaks = Number.isFinite(major) && Number.isFinite(minor) ? [major, minor] : [0, 0];
|
|
174
|
+
if (speaks[0] > SOCKETS_SINCE[0] || (speaks[0] === SOCKETS_SINCE[0] && speaks[1] >= SOCKETS_SINCE[1]))
|
|
175
|
+
return undefined;
|
|
176
|
+
return `this runtime speaks protocol ${runtime || "unknown"}; a socket route needs 1.9 (${routes.join(", ")})`;
|
|
177
|
+
}
|
|
178
|
+
/** The protocol version whose runtimes admit granted callers. */
|
|
179
|
+
export const GRANTS_SINCE = [1, 15];
|
|
180
|
+
/** Why `spec` cannot be declared to a runtime speaking `runtime`, or undefined. A runtime before 1.15
|
|
181
|
+
* does not know `Callers.granted`, and would admit nobody by it. */
|
|
182
|
+
export function grantsRefusal(spec, runtime) {
|
|
183
|
+
const grants = (callers) => callers.some((m) => m.kind.case === "granted");
|
|
184
|
+
const endpoints = spec.endpoints
|
|
185
|
+
.filter((e) => grants(e.allowCallers) || e.routes.some((r) => grants(r.allowCallers)))
|
|
186
|
+
.map((e) => e.id);
|
|
187
|
+
// Another project's topic is 1.15's too: an older runtime would read it as this project's.
|
|
188
|
+
for (const c of spec.components) {
|
|
189
|
+
const d = c.detail;
|
|
190
|
+
const sources = d.case === "view" ? [d.value.source, ...d.value.sources] : d.case === "consumer" ? [d.value.source] : [];
|
|
191
|
+
const publishes = d.case === "consumer" && d.value.produces?.project !== undefined;
|
|
192
|
+
if (sources.some((s) => s?.project !== undefined) || publishes)
|
|
193
|
+
endpoints.push(c.id);
|
|
194
|
+
}
|
|
195
|
+
if (endpoints.length === 0)
|
|
196
|
+
return undefined;
|
|
197
|
+
const [major, minor] = runtime.split(".").map((part) => Number.parseInt(part, 10));
|
|
198
|
+
const speaks = Number.isFinite(major) && Number.isFinite(minor) ? [major, minor] : [0, 0];
|
|
199
|
+
if (speaks[0] > GRANTS_SINCE[0] || (speaks[0] === GRANTS_SINCE[0] && speaks[1] >= GRANTS_SINCE[1]))
|
|
200
|
+
return undefined;
|
|
201
|
+
return `this runtime speaks protocol ${runtime || "unknown"}; granted callers and another project's topics need 1.15 (${endpoints.join(", ")})`;
|
|
202
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { ConnectRouter } from "@connectrpc/connect";
|
|
2
2
|
import { type MessageInitShape } from "@bufbuild/protobuf";
|
|
3
|
-
import { type EventSourcedIn, type EventSourcedOut } from "../_proto/ankka/protocol/v1/event_sourced_pb.ts";
|
|
3
|
+
import { type ReplicationChangeSchema, type EventSourcedIn, type EventSourcedOut } from "../_proto/ankka/protocol/v1/event_sourced_pb.ts";
|
|
4
4
|
import type { RetentionSchema } from "../_proto/ankka/protocol/v1/payload_pb.ts";
|
|
5
5
|
import { type Retention } from "../effects/common.ts";
|
|
6
|
+
import type { ReplicationChange } from "../effects/eventSourced.ts";
|
|
6
7
|
import type { ServerContext } from "./server.ts";
|
|
7
8
|
type RetentionInit = MessageInitShape<typeof RetentionSchema>;
|
|
9
|
+
/** A replicated entity's handover or change to its replication set, as the reply carries it (1.19). */
|
|
10
|
+
export declare function replicationToProto(change: ReplicationChange | undefined): MessageInitShape<typeof ReplicationChangeSchema> | undefined;
|
|
8
11
|
/** Retention as the protocol carries it; `undefined` when the effect asked for none. */
|
|
9
12
|
export declare function retentionToProto(retention: Retention | null): RetentionInit | undefined;
|
|
10
13
|
export declare function eventSourcedRoutes(router: ConnectRouter, ctx: ServerContext): void;
|
|
@@ -9,12 +9,20 @@
|
|
|
9
9
|
import { create } from "@bufbuild/protobuf";
|
|
10
10
|
import { EventSourced, EventSourcedOutSchema, } from "../_proto/ankka/protocol/v1/event_sourced_pb.js";
|
|
11
11
|
import { codecFor } from "../codec.js";
|
|
12
|
-
import { commandContext, metadataFromProto } from "../context.js";
|
|
12
|
+
import { commandContext, eventContext, metadataFromProto } from "../context.js";
|
|
13
13
|
import { ErrorCode } from "../effects/common.js";
|
|
14
14
|
import { binaryCodecs } from "../codec.js";
|
|
15
15
|
import { errorCodeToProto } from "../kinds.js";
|
|
16
16
|
import { materialiseEventSourced } from "../materialise.js";
|
|
17
17
|
import { decodePayload, encodePayload } from "./payloads.js";
|
|
18
|
+
/** A replicated entity's handover or change to its replication set, as the reply carries it (1.19). */
|
|
19
|
+
export function replicationToProto(change) {
|
|
20
|
+
if (!change)
|
|
21
|
+
return undefined;
|
|
22
|
+
return change.kind === "hand-over"
|
|
23
|
+
? { change: { case: "handOverTo", value: change.to } }
|
|
24
|
+
: { change: { case: "set", value: { add: [...change.add], remove: [...change.remove] } } };
|
|
25
|
+
}
|
|
18
26
|
/** Retention as the protocol carries it; `undefined` when the effect asked for none. */
|
|
19
27
|
export function retentionToProto(retention) {
|
|
20
28
|
if (!retention)
|
|
@@ -43,10 +51,13 @@ export async function* handleEventSourced(requests, ctx) {
|
|
|
43
51
|
let entityId = "";
|
|
44
52
|
let state;
|
|
45
53
|
let sequence = 0n;
|
|
54
|
+
// This installation, for a replicated entity (1.19): null on a service that names none.
|
|
55
|
+
let here = null;
|
|
46
56
|
for await (const m of requests) {
|
|
47
57
|
switch (m.message.case) {
|
|
48
58
|
case "init": {
|
|
49
59
|
const init = m.message.value;
|
|
60
|
+
here = init.installation || null;
|
|
50
61
|
const found = ctx.registry.component(init.componentId);
|
|
51
62
|
if (!found || found.kind !== "event-sourced") {
|
|
52
63
|
yield failure(0n, { message: `no event sourced entity ${JSON.stringify(init.componentId)} is registered`, code: ErrorCode.NotFound });
|
|
@@ -80,7 +91,8 @@ export async function* handleEventSourced(requests, ctx) {
|
|
|
80
91
|
}
|
|
81
92
|
const ev = m.message.value;
|
|
82
93
|
try {
|
|
83
|
-
|
|
94
|
+
const written = ev.installation || here;
|
|
95
|
+
state = entity.applyEvent(state, decodePayload(registered.eventCodec, ev.payload), eventContext(written, written === here, ev.sequence));
|
|
84
96
|
sequence = ev.sequence;
|
|
85
97
|
}
|
|
86
98
|
catch (e) {
|
|
@@ -95,7 +107,7 @@ export async function* handleEventSourced(requests, ctx) {
|
|
|
95
107
|
yield failure(m.message.value.id, { message: "a command arrived before Init", code: ErrorCode.Internal });
|
|
96
108
|
return;
|
|
97
109
|
}
|
|
98
|
-
const result = await runCommand(registered, entity, entityId, state, sequence, m.message.value, ctx);
|
|
110
|
+
const result = await runCommand(registered, entity, entityId, state, sequence, m.message.value, ctx, here);
|
|
99
111
|
yield result.out;
|
|
100
112
|
if (result.newState !== undefined) {
|
|
101
113
|
state = result.newState.state;
|
|
@@ -108,7 +120,7 @@ export async function* handleEventSourced(requests, ctx) {
|
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
|
-
async function runCommand(registered, entity, entityId, state, sequence, cmd, ctx) {
|
|
123
|
+
async function runCommand(registered, entity, entityId, state, sequence, cmd, ctx, here = null) {
|
|
112
124
|
const handler = registered.handlers.get(cmd.name);
|
|
113
125
|
if (!handler)
|
|
114
126
|
return { out: failure(cmd.id, { message: `no handler ${JSON.stringify(cmd.name)} on ${registered.id}`, code: ErrorCode.NotFound }) };
|
|
@@ -120,7 +132,7 @@ async function runCommand(registered, entity, entityId, state, sequence, cmd, ct
|
|
|
120
132
|
return { out: failure(cmd.id, { message: `${registered.id}/${cmd.name}: the input could not be decoded: ${messageOf(e)}`, code: ErrorCode.BadRequest }) };
|
|
121
133
|
}
|
|
122
134
|
const metadata = metadataFromProto(cmd.metadata);
|
|
123
|
-
const context = commandContext(registered.id, entityId, sequence, metadata);
|
|
135
|
+
const context = commandContext(registered.id, entityId, sequence, metadata, undefined, cmd.installation || null, cmd.atHome);
|
|
124
136
|
let effect;
|
|
125
137
|
entity._bindCommand(state, context, ctx.client.withMetadata(metadata));
|
|
126
138
|
try {
|
|
@@ -137,7 +149,12 @@ async function runCommand(registered, entity, entityId, state, sequence, cmd, ct
|
|
|
137
149
|
return { out: failure(cmd.id, { message: `${registered.id}/${cmd.name} is a query and returned a persisting effect`, code: ErrorCode.Internal }) };
|
|
138
150
|
}
|
|
139
151
|
try {
|
|
140
|
-
|
|
152
|
+
let applied = sequence;
|
|
153
|
+
const m = materialiseEventSourced(effect, state, (s, e) => {
|
|
154
|
+
applied += 1n;
|
|
155
|
+
return entity.applyEvent(s, e, eventContext(here, true, applied));
|
|
156
|
+
});
|
|
157
|
+
const replication = !m.error && effect.kind === "persist" ? replicationToProto(effect.replication) : undefined;
|
|
141
158
|
const events = m.events.map((e) => encodePayload(registered.eventCodec, e));
|
|
142
159
|
const replyCodec = handler.reply ? codecFor(handler.reply) : binaryCodecs.done;
|
|
143
160
|
const outcome = m.error
|
|
@@ -154,6 +171,7 @@ async function runCommand(registered, entity, entityId, state, sequence, cmd, ct
|
|
|
154
171
|
retention: retentionToProto(m.retention),
|
|
155
172
|
outcome,
|
|
156
173
|
snapshot: cmd.snapshotRequested ? encodePayload(registered.stateCodec, m.newState) : undefined,
|
|
174
|
+
replication,
|
|
157
175
|
},
|
|
158
176
|
},
|
|
159
177
|
});
|
package/dist/server/http.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ConnectRouter } from "@connectrpc/connect";
|
|
2
|
-
import { type HttpRequest, type HttpReply, type StreamFrame } from "../_proto/ankka/protocol/v1/endpoint_pb.ts";
|
|
2
|
+
import { type HttpRequest, type HttpReply, type SocketIn, type SocketOut, type StreamFrame } from "../_proto/ankka/protocol/v1/endpoint_pb.ts";
|
|
3
3
|
import type { ServerContext } from "./server.ts";
|
|
4
4
|
export interface HttpDispatcher {
|
|
5
5
|
handle(req: HttpRequest): Promise<HttpReply>;
|
|
6
6
|
handleStream(req: HttpRequest): AsyncIterable<StreamFrame>;
|
|
7
|
+
handleSocket(requests: AsyncIterable<SocketIn>): AsyncIterable<SocketOut>;
|
|
7
8
|
}
|
|
8
9
|
/** The behaviour behind `Http.Handle` and `Http.HandleStream`, over the registry's endpoints. */
|
|
9
10
|
export declare function createHttpDispatcher(ctx: ServerContext): HttpDispatcher;
|