ankka 0.11.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/client_pb.d.ts +316 -0
- package/dist/_proto/ankka/protocol/v1/client_pb.js +97 -23
- 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 +43 -0
- package/dist/_proto/ankka/protocol/v1/discovery_pb.js +1 -1
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +26 -0
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +15 -10
- 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 +7 -1
- package/dist/_proto/ankka/protocol/v1/view_pb.js +1 -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/client.d.ts +96 -3
- package/dist/client.js +228 -6
- package/dist/consumer.d.ts +18 -1
- package/dist/consumer.js +15 -1
- package/dist/context.d.ts +25 -1
- package/dist/context.js +5 -2
- package/dist/contract.d.ts +4 -0
- package/dist/contract.js +2 -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/eventSourcedEntity.d.ts +8 -3
- package/dist/graph.d.ts +9 -1
- package/dist/graph.js +7 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/keyedView.d.ts +4 -1
- package/dist/keyedView.js +7 -1
- package/dist/kinds.js +2 -0
- package/dist/routes.d.ts +21 -1
- package/dist/routes.js +27 -1
- package/dist/server/discovery.d.ts +5 -0
- package/dist/server/discovery.js +90 -3
- package/dist/server/eventSourced.d.ts +4 -1
- package/dist/server/eventSourced.js +24 -6
- package/dist/server/http.js +2 -0
- package/dist/server/stateless.js +5 -4
- package/dist/service.d.ts +6 -0
- package/dist/service.js +26 -1
- package/dist/spec.d.ts +1 -1
- package/dist/spec.js +10 -4
- package/dist/standing.d.ts +18 -0
- package/dist/standing.js +26 -0
- package/dist/startFrom.d.ts +4 -0
- package/dist/startFrom.js +8 -1
- package/dist/testkit/index.d.ts +1 -1
- package/dist/testkit/kinds.d.ts +9 -5
- package/dist/testkit/kinds.js +17 -14
- package/dist/testkit/unit.d.ts +27 -2
- package/dist/testkit/unit.js +47 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/view.d.ts +18 -3
- package/dist/view.js +13 -4
- package/package.json +1 -1
package/dist/effects/common.js
CHANGED
|
@@ -10,6 +10,10 @@ export const ErrorCode = Object.freeze({
|
|
|
10
10
|
Conflict: "CONFLICT",
|
|
11
11
|
Timeout: "TIMEOUT",
|
|
12
12
|
Unavailable: "UNAVAILABLE",
|
|
13
|
+
/** A workflow a caller waited for failed or was deleted (protocol 1.18); the step and the reason are in `details`. */
|
|
14
|
+
WorkflowFailed: "WORKFLOW_FAILED",
|
|
15
|
+
/** A replicated entity's copy here cannot answer (1.19): its home is unreachable, or the read is outside the replication set. */
|
|
16
|
+
NotHome: "NOT_HOME",
|
|
13
17
|
});
|
|
14
18
|
const HTTP_STATUS = Object.freeze({
|
|
15
19
|
INTERNAL: 500,
|
|
@@ -20,6 +24,8 @@ const HTTP_STATUS = Object.freeze({
|
|
|
20
24
|
CONFLICT: 409,
|
|
21
25
|
TIMEOUT: 504,
|
|
22
26
|
UNAVAILABLE: 503,
|
|
27
|
+
WORKFLOW_FAILED: 424,
|
|
28
|
+
NOT_HOME: 421,
|
|
23
29
|
});
|
|
24
30
|
export function httpStatusOf(code) {
|
|
25
31
|
return HTTP_STATUS[code];
|
|
@@ -27,13 +33,15 @@ export function httpStatusOf(code) {
|
|
|
27
33
|
/** Thrown by the component client when the callee refused. */
|
|
28
34
|
export class CommandError extends Error {
|
|
29
35
|
code;
|
|
36
|
+
details;
|
|
30
37
|
constructor(detail) {
|
|
31
38
|
super(detail.message);
|
|
32
39
|
this.name = "CommandError";
|
|
33
40
|
this.code = detail.code;
|
|
41
|
+
this.details = detail.details ?? {};
|
|
34
42
|
}
|
|
35
43
|
get detail() {
|
|
36
|
-
return { message: this.message, code: this.code };
|
|
44
|
+
return { message: this.message, code: this.code, details: this.details };
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
export const Outcome = Object.freeze({
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import type { Duration } from "../time.ts";
|
|
2
2
|
import { done, type Done } from "../schema.ts";
|
|
3
3
|
import { ErrorCode, Outcome, Retention, type EffectLike, type Metadata, type ReadOnlyLike } from "./common.ts";
|
|
4
|
+
/** What a replicated entity's handler asks of its home beside the events it persists (1.19). */
|
|
5
|
+
export type ReplicationChange = {
|
|
6
|
+
readonly kind: "hand-over";
|
|
7
|
+
readonly to: string;
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: "set";
|
|
10
|
+
readonly add: readonly string[];
|
|
11
|
+
readonly remove: readonly string[];
|
|
12
|
+
};
|
|
4
13
|
export interface PersistEffect<S, E, R> extends EffectLike<R> {
|
|
5
14
|
readonly kind: "persist";
|
|
6
15
|
readonly events: readonly E[];
|
|
7
16
|
readonly retention: Retention | null;
|
|
8
17
|
readonly outcome: Outcome<S, R>;
|
|
18
|
+
readonly replication?: ReplicationChange;
|
|
9
19
|
}
|
|
10
20
|
export interface ReadOnlyEffect<S, E, R> extends ReadOnlyLike<R> {
|
|
11
21
|
readonly kind: "read-only";
|
|
@@ -18,11 +28,20 @@ export type EventSourcedEffect<S, E, R> = PersistEffect<S, E, R> | ReadOnlyEffec
|
|
|
18
28
|
/** `effects.persist(e)` returns one of these; finish it with `thenReply`, `thenReplyState` or `thenNoReply`. */
|
|
19
29
|
export declare class PersistBuilder<S, E> {
|
|
20
30
|
#private;
|
|
21
|
-
constructor(events: readonly E[], retention?: Retention | null);
|
|
31
|
+
constructor(events: readonly E[], retention?: Retention | null, replication?: ReplicationChange);
|
|
22
32
|
/** Delete the entity once these events are persisted. */
|
|
23
33
|
deleteEntity(): PersistBuilder<S, E>;
|
|
24
34
|
/** Expire the entity a while after these events are persisted. */
|
|
25
35
|
expireAfter(after: Duration): PersistBuilder<S, E>;
|
|
36
|
+
/**
|
|
37
|
+
* Persist these events, then hand a replicated entity's home to `installation`, which must be in its
|
|
38
|
+
* replication set. Recorded once the new home holds everything before it.
|
|
39
|
+
*/
|
|
40
|
+
handOverTo(installation: string): PersistBuilder<S, E>;
|
|
41
|
+
/** Persist these events, then add `installation` to a replicated entity's replication set. */
|
|
42
|
+
replicateTo(installation: string): PersistBuilder<S, E>;
|
|
43
|
+
/** Persist these events, then take `installation` out of a replicated entity's replication set; it keeps its copy. */
|
|
44
|
+
stopReplicatingTo(installation: string): PersistBuilder<S, E>;
|
|
26
45
|
/** Reply with a value computed from the state *after* the events. */
|
|
27
46
|
thenReply<R>(compute: (state: S) => R, metadata?: Metadata): PersistEffect<S, E, R>;
|
|
28
47
|
/** Reply with the state after the events. */
|
|
@@ -46,5 +65,11 @@ export declare class EventSourcedEffects<S, E> {
|
|
|
46
65
|
deleteEntity(): PersistBuilder<S, E>;
|
|
47
66
|
/** Expire the entity after a while, persisting nothing more. */
|
|
48
67
|
expireAfter(after: Duration): PersistBuilder<S, E>;
|
|
68
|
+
/** Hand a replicated entity's home to `installation`, with no event of its own. */
|
|
69
|
+
handOverTo(installation: string): PersistBuilder<S, E>;
|
|
70
|
+
/** Add `installation` to a replicated entity's replication set, with no event of its own. */
|
|
71
|
+
replicateTo(installation: string): PersistBuilder<S, E>;
|
|
72
|
+
/** Take `installation` out of a replicated entity's replication set; it keeps its copy. */
|
|
73
|
+
stopReplicatingTo(installation: string): PersistBuilder<S, E>;
|
|
49
74
|
}
|
|
50
75
|
export { done, type Done };
|
|
@@ -1,27 +1,67 @@
|
|
|
1
1
|
// The effects an event sourced entity's handlers return: persist these events and then reply from the
|
|
2
2
|
// state after them, or answer without persisting. Inert values. `ReadOnlyEffect` is a distinct type
|
|
3
3
|
// with `kind: "read-only"`, which is what lets `query` refuse a persisting handler at compile time.
|
|
4
|
+
var _a;
|
|
4
5
|
import { done } from "../schema.js";
|
|
5
6
|
import { ErrorCode, Outcome, Retention } from "./common.js";
|
|
6
7
|
/** `effects.persist(e)` returns one of these; finish it with `thenReply`, `thenReplyState` or `thenNoReply`. */
|
|
7
8
|
export class PersistBuilder {
|
|
8
9
|
#events;
|
|
9
10
|
#retention;
|
|
10
|
-
|
|
11
|
+
#replication;
|
|
12
|
+
constructor(events, retention = null, replication) {
|
|
11
13
|
this.#events = events;
|
|
12
14
|
this.#retention = retention;
|
|
15
|
+
this.#replication = replication;
|
|
13
16
|
}
|
|
14
17
|
/** Delete the entity once these events are persisted. */
|
|
15
18
|
deleteEntity() {
|
|
16
|
-
return new
|
|
19
|
+
return new _a(this.#events, Retention.deleteNow, this.#replication);
|
|
17
20
|
}
|
|
18
21
|
/** Expire the entity a while after these events are persisted. */
|
|
19
22
|
expireAfter(after) {
|
|
20
|
-
return new
|
|
23
|
+
return new _a(this.#events, Retention.expireAfter(after), this.#replication);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Persist these events, then hand a replicated entity's home to `installation`, which must be in its
|
|
27
|
+
* replication set. Recorded once the new home holds everything before it.
|
|
28
|
+
*/
|
|
29
|
+
handOverTo(installation) {
|
|
30
|
+
if (this.#replication)
|
|
31
|
+
throw new Error(`a command asks for one handover or one change to the replication set; it asked for ${JSON.stringify(this.#replication)}`);
|
|
32
|
+
return new _a(this.#events, this.#retention, Object.freeze({ kind: "hand-over", to: installation }));
|
|
33
|
+
}
|
|
34
|
+
/** Persist these events, then add `installation` to a replicated entity's replication set. */
|
|
35
|
+
replicateTo(installation) {
|
|
36
|
+
return this.#setChange([installation], []);
|
|
37
|
+
}
|
|
38
|
+
/** Persist these events, then take `installation` out of a replicated entity's replication set; it keeps its copy. */
|
|
39
|
+
stopReplicatingTo(installation) {
|
|
40
|
+
return this.#setChange([], [installation]);
|
|
41
|
+
}
|
|
42
|
+
#setChange(add, remove) {
|
|
43
|
+
const current = this.#replication;
|
|
44
|
+
if (current?.kind === "hand-over")
|
|
45
|
+
throw new Error(`a command asks for one handover or one change to the replication set; it asked for ${JSON.stringify(current)}`);
|
|
46
|
+
const adding = [...new Set([...(current?.add ?? []), ...add])].sort();
|
|
47
|
+
const removing = [...new Set([...(current?.remove ?? []), ...remove])].sort();
|
|
48
|
+
const both = adding.filter((name) => removing.includes(name));
|
|
49
|
+
if (both.length > 0)
|
|
50
|
+
throw new Error(`a command both adds and removes ${both.join(", ")} in the replication set`);
|
|
51
|
+
return new _a(this.#events, this.#retention, Object.freeze({ kind: "set", add: Object.freeze(adding), remove: Object.freeze(removing) }));
|
|
52
|
+
}
|
|
53
|
+
#effect(outcome) {
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
kind: "persist",
|
|
56
|
+
events: this.#events,
|
|
57
|
+
retention: this.#retention,
|
|
58
|
+
outcome,
|
|
59
|
+
...(this.#replication ? { replication: this.#replication } : {}),
|
|
60
|
+
});
|
|
21
61
|
}
|
|
22
62
|
/** Reply with a value computed from the state *after* the events. */
|
|
23
63
|
thenReply(compute, metadata) {
|
|
24
|
-
return
|
|
64
|
+
return this.#effect(Outcome.reply(compute, metadata));
|
|
25
65
|
}
|
|
26
66
|
/** Reply with the state after the events. */
|
|
27
67
|
thenReplyState() {
|
|
@@ -29,9 +69,10 @@ export class PersistBuilder {
|
|
|
29
69
|
}
|
|
30
70
|
/** Persist and answer nothing. */
|
|
31
71
|
thenNoReply() {
|
|
32
|
-
return
|
|
72
|
+
return this.#effect(Outcome.noReply());
|
|
33
73
|
}
|
|
34
74
|
}
|
|
75
|
+
_a = PersistBuilder;
|
|
35
76
|
/** The factory a handler reaches as `this.effects`. */
|
|
36
77
|
export class EventSourcedEffects {
|
|
37
78
|
/** Persist one or more events. */
|
|
@@ -62,5 +103,17 @@ export class EventSourcedEffects {
|
|
|
62
103
|
expireAfter(after) {
|
|
63
104
|
return new PersistBuilder(Object.freeze([]), Retention.expireAfter(after));
|
|
64
105
|
}
|
|
106
|
+
/** Hand a replicated entity's home to `installation`, with no event of its own. */
|
|
107
|
+
handOverTo(installation) {
|
|
108
|
+
return new PersistBuilder(Object.freeze([])).handOverTo(installation);
|
|
109
|
+
}
|
|
110
|
+
/** Add `installation` to a replicated entity's replication set, with no event of its own. */
|
|
111
|
+
replicateTo(installation) {
|
|
112
|
+
return new PersistBuilder(Object.freeze([])).replicateTo(installation);
|
|
113
|
+
}
|
|
114
|
+
/** Take `installation` out of a replicated entity's replication set; it keeps its copy. */
|
|
115
|
+
stopReplicatingTo(installation) {
|
|
116
|
+
return new PersistBuilder(Object.freeze([])).stopReplicatingTo(installation);
|
|
117
|
+
}
|
|
65
118
|
}
|
|
66
119
|
export { done };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Shape } from "./codec.ts";
|
|
2
|
-
import type { CommandContext } from "./context.ts";
|
|
2
|
+
import type { CommandContext, EventContext } from "./context.ts";
|
|
3
3
|
import type { HandlerTable } from "./handlers.ts";
|
|
4
4
|
import type { ComponentClient } from "./client.ts";
|
|
5
5
|
import { EventSourcedEffects } from "./effects/eventSourced.ts";
|
|
@@ -19,8 +19,11 @@ export declare abstract class EventSourcedEntity<S, E> {
|
|
|
19
19
|
get _kind(): "event-sourced";
|
|
20
20
|
/** The state of an instance with no events yet. `this.entityId` is available here. */
|
|
21
21
|
abstract emptyState(): S;
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The fold: the state after `event`, given the state before it. Must not read `this.state`. The third
|
|
24
|
+
* argument, which a two-argument fold may leave out, says where the event was written (1.19).
|
|
25
|
+
*/
|
|
26
|
+
abstract applyEvent(state: S, event: E, context?: EventContext): S;
|
|
24
27
|
/** @internal Bound by the runtime and the testkit before `emptyState`. */
|
|
25
28
|
_bindInstance(entityId: string): void;
|
|
26
29
|
/** @internal Bound by the runtime and the testkit for the duration of one handler call. */
|
|
@@ -37,4 +40,6 @@ export interface EventSourcedEntityClass<S = unknown, E = unknown, C extends Eve
|
|
|
37
40
|
readonly handlers: HandlerTable<C>;
|
|
38
41
|
/** Ask for a snapshot every N events; absent or 0: never. */
|
|
39
42
|
readonly snapshotEvery?: number;
|
|
43
|
+
/** Held at every installation in its replication set and written only at its home (1.19). */
|
|
44
|
+
readonly replicated?: boolean;
|
|
40
45
|
}
|
package/dist/graph.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type Codec, type Shape } from "./codec.ts";
|
|
|
3
3
|
import type { EffectLike, Metadata } from "./effects/common.ts";
|
|
4
4
|
import { type ComponentClient, type ComponentRef, type Secrets } from "./client.ts";
|
|
5
5
|
import { type Services } from "./services.ts";
|
|
6
|
+
import type { Standing } from "./standing.ts";
|
|
6
7
|
type MaybePromise<T> = T | Promise<T>;
|
|
7
8
|
/** The contract's name: the manifest and the `ce-type` of every delta. */
|
|
8
9
|
export declare const GRAPH_DELTA_SCHEMA = "ankka.graph-delta.v1";
|
|
@@ -136,6 +137,8 @@ export declare abstract class GraphConsumer<M> {
|
|
|
136
137
|
/** The change's metadata: `ce-subject` is the source instance's id, `ankka.sequence` its sequence number. */
|
|
137
138
|
get metadata(): Metadata;
|
|
138
139
|
get subject(): string;
|
|
140
|
+
/** Of a change from a workflow: where it stood once the effect that recorded the state was applied. `undefined` for a change from an entity or a topic. */
|
|
141
|
+
get standing(): Standing | undefined;
|
|
139
142
|
/** The change's sequence number, which is the version of every element that states none. */
|
|
140
143
|
get sequenceNumber(): bigint | undefined;
|
|
141
144
|
get client(): ComponentClient;
|
|
@@ -153,7 +156,7 @@ export declare abstract class GraphConsumer<M> {
|
|
|
153
156
|
/** The source was deleted. Ignored unless this says otherwise; publish tombstones here. */
|
|
154
157
|
onDelete(): MaybePromise<GraphEffect>;
|
|
155
158
|
/** @internal */
|
|
156
|
-
_bind(metadata: Metadata, client: ComponentClient): void;
|
|
159
|
+
_bind(metadata: Metadata, client: ComponentClient, standing?: Standing): void;
|
|
157
160
|
}
|
|
158
161
|
export interface GraphConsumerClass<M = unknown, C extends GraphConsumer<M> = GraphConsumer<M>> {
|
|
159
162
|
new (): C;
|
|
@@ -167,5 +170,10 @@ export interface GraphConsumerClass<M = unknown, C extends GraphConsumer<M> = Gr
|
|
|
167
170
|
readonly message: Shape<M>;
|
|
168
171
|
/** The delta topic. It must be compacted to hold the graph; the pipeline that reads it creates it so. */
|
|
169
172
|
readonly producesTo: string;
|
|
173
|
+
/**
|
|
174
|
+
* Given a replicated entity's changes written at other installations too (1.19); otherwise each delta
|
|
175
|
+
* is published once, from the installation that wrote the change.
|
|
176
|
+
*/
|
|
177
|
+
readonly mirroredChanges?: boolean;
|
|
170
178
|
}
|
|
171
179
|
export {};
|
package/dist/graph.js
CHANGED
|
@@ -395,6 +395,7 @@ export class GraphConsumer {
|
|
|
395
395
|
graph = new Graph();
|
|
396
396
|
effects = new GraphEffects();
|
|
397
397
|
#metadata = {};
|
|
398
|
+
#standing = undefined;
|
|
398
399
|
#client;
|
|
399
400
|
/** The change's metadata: `ce-subject` is the source instance's id, `ankka.sequence` its sequence number. */
|
|
400
401
|
get metadata() {
|
|
@@ -403,6 +404,10 @@ export class GraphConsumer {
|
|
|
403
404
|
get subject() {
|
|
404
405
|
return this.#metadata["ce-subject"] ?? "";
|
|
405
406
|
}
|
|
407
|
+
/** Of a change from a workflow: where it stood once the effect that recorded the state was applied. `undefined` for a change from an entity or a topic. */
|
|
408
|
+
get standing() {
|
|
409
|
+
return this.#standing;
|
|
410
|
+
}
|
|
406
411
|
/** The change's sequence number, which is the version of every element that states none. */
|
|
407
412
|
get sequenceNumber() {
|
|
408
413
|
return sequenceNumberOf(this.#metadata);
|
|
@@ -439,8 +444,9 @@ export class GraphConsumer {
|
|
|
439
444
|
return this.effects.ignore();
|
|
440
445
|
}
|
|
441
446
|
/** @internal */
|
|
442
|
-
_bind(metadata, client) {
|
|
447
|
+
_bind(metadata, client, standing) {
|
|
443
448
|
this.#metadata = metadata;
|
|
449
|
+
this.#standing = standing;
|
|
444
450
|
this.#client = client;
|
|
445
451
|
}
|
|
446
452
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { EncodingError, DecodingError, renderDouble } from "./json.ts";
|
|
|
5
5
|
export { Instant, Duration, LocalDate, LocalDateTime } from "./time.ts";
|
|
6
6
|
export { ErrorCode, CommandError, Outcome, Retention, httpStatusOf, type ErrorDetail, type Metadata, type EffectLike, type ReadOnlyLike } from "./effects/common.ts";
|
|
7
7
|
export { command, query, stream, step, action, tool, guardrail, type HandlerRef, type ToolRef, type GuardrailRef, type GuardrailStage, type HandlerTable } from "./handlers.ts";
|
|
8
|
-
export { PersistBuilder, EventSourcedEffects, type PersistEffect, type ReadOnlyEffect, type EventSourcedEffect } from "./effects/eventSourced.ts";
|
|
8
|
+
export { PersistBuilder, EventSourcedEffects, type PersistEffect, type ReadOnlyEffect, type EventSourcedEffect, type ReplicationChange } from "./effects/eventSourced.ts";
|
|
9
9
|
export { EventSourcedEntity, type EventSourcedEntityClass } from "./eventSourcedEntity.ts";
|
|
10
10
|
export { UpdateBuilder, KeyValueEffects, type UpdateEffect, type KeyValueReadOnlyEffect, type KeyValueEffect } from "./effects/keyValue.ts";
|
|
11
11
|
export { KeyValueEntity, type KeyValueEntityClass } from "./keyValueEntity.ts";
|
|
@@ -26,14 +26,15 @@ export { mcpServer, resultGuardrail, toolName, type McpServerOptions, type McpSe
|
|
|
26
26
|
export { AutonomousAgent, AutonomousAgentCalls, TaskCalls, Tasks, accepted, rejected, taskAcceptance, taskRule, taskType, type Accepted, type AgentState, type Attachment, type AutonomousAgentClass, type AutonomousSettings, type Notification, type Rejected, type TaskAcceptance, type TaskRule, type TaskSnapshot, type TaskType, } from "./autonomous.ts";
|
|
27
27
|
export { Endpoint, type EndpointClass } from "./endpoint.ts";
|
|
28
28
|
export { type Socket, SocketClosed } from "./socket.ts";
|
|
29
|
-
export { Acl, Callers, type CallerMatcher, type CallersAcl, HttpProblem, get, post, put, patch, del, sse, sseEvent, type SseEvent, socket, type RouteRef, type RouteOptions, type RouteTable, type Params, type ParamNames, type HttpMethod } from "./routes.ts";
|
|
30
|
-
export { type CommandContext, type RequestContext, type Principal, type Caller, Query, Headers } from "./context.ts";
|
|
31
|
-
export { ComponentClient, Calls, TypedCalls, Invocation, Views, Timers, Secrets, InMemorySecrets, noSecrets, type TimerTarget, type TimedActionTarget, type ComponentRef } from "./client.ts";
|
|
29
|
+
export { Acl, Callers, type CallerMatcher, type CallersAcl, HttpProblem, get, post, put, patch, del, sse, sseEvent, sseEvents, type SseEvent, socket, type RouteRef, type RouteOptions, type RouteTable, type Params, type ParamNames, type HttpMethod } from "./routes.ts";
|
|
30
|
+
export { type CommandContext, type EventContext, type RequestContext, type Principal, type Caller, Query, Headers } from "./context.ts";
|
|
31
|
+
export { ComponentClient, Calls, TypedCalls, Invocation, AwaitingInvocation, AWAIT_SINCE, awaitPartJson, Views, Timers, Secrets, InMemorySecrets, noSecrets, WatchEnded, VIEW_STREAMS_SINCE, type AwaitPart, type WatchEvent, type WatchOptions, type Overflow, type TimerTarget, type TimedActionTarget, type ComponentRef } from "./client.ts";
|
|
32
32
|
export { Services, ServiceClient, ScriptedServices, noServices, ServiceError, ServiceUnresolvable, ServiceIdentityMismatch, ServiceUnanswered, ServiceCallFailed, type ServiceResponse, type ServiceRequest, type ServiceHeaders, } from "./services.ts";
|
|
33
33
|
export { Ankka, ServiceBuilder, Registry, RegistrationError, type ServiceOptions, type RegisteredComponent, type RegisteredEndpoint, type Source, type RegisteredEventSourced, type RegisteredKeyValue, type RegisteredWorkflow, type RegisteredView, type RegisteredKeyedView, type RegisteredKeyedSource, type RegisteredConsumer, type RegisteredTimedAction, type RegisteredAgent, } from "./service.ts";
|
|
34
34
|
export { Server, type ServerOptions } from "./server/server.ts";
|
|
35
35
|
export { problems as sidecarProblems } from "./server/discovery.ts";
|
|
36
36
|
export { PROTOCOL_VERSION, SDK_NAME } from "./spec.ts";
|
|
37
37
|
export { StartFrom } from "./startFrom.ts";
|
|
38
|
+
export { isTerminal, isUnknown, type Standing, type StandingStatus } from "./standing.ts";
|
|
38
39
|
export { materialiseEventSourced, materialiseKeyValue, materialiseWorkflowCommand, materialiseStep, type Materialised, type MaterialisedKeyValue, type MaterialisedWorkflowCommand, type MaterialisedStep, } from "./materialise.ts";
|
|
39
40
|
export type { ComponentKind } from "./kinds.ts";
|
package/dist/index.js
CHANGED
|
@@ -37,13 +37,14 @@ export { mcpServer, resultGuardrail, toolName } from "./mcp.js";
|
|
|
37
37
|
export { AutonomousAgent, AutonomousAgentCalls, TaskCalls, Tasks, accepted, rejected, taskAcceptance, taskRule, taskType, } from "./autonomous.js";
|
|
38
38
|
export { Endpoint } from "./endpoint.js";
|
|
39
39
|
export { SocketClosed } from "./socket.js";
|
|
40
|
-
export { Acl, Callers, HttpProblem, get, post, put, patch, del, sse, sseEvent, socket } from "./routes.js";
|
|
40
|
+
export { Acl, Callers, HttpProblem, get, post, put, patch, del, sse, sseEvent, sseEvents, socket } from "./routes.js";
|
|
41
41
|
export { Query, Headers } from "./context.js";
|
|
42
|
-
export { ComponentClient, Calls, TypedCalls, Invocation, Views, Timers, Secrets, InMemorySecrets, noSecrets } from "./client.js";
|
|
42
|
+
export { ComponentClient, Calls, TypedCalls, Invocation, AwaitingInvocation, AWAIT_SINCE, awaitPartJson, Views, Timers, Secrets, InMemorySecrets, noSecrets, WatchEnded, VIEW_STREAMS_SINCE } from "./client.js";
|
|
43
43
|
export { Services, ServiceClient, ScriptedServices, noServices, ServiceError, ServiceUnresolvable, ServiceIdentityMismatch, ServiceUnanswered, ServiceCallFailed, } from "./services.js";
|
|
44
44
|
export { Ankka, ServiceBuilder, Registry, RegistrationError, } from "./service.js";
|
|
45
45
|
export { Server } from "./server/server.js";
|
|
46
46
|
export { problems as sidecarProblems } from "./server/discovery.js";
|
|
47
47
|
export { PROTOCOL_VERSION, SDK_NAME } from "./spec.js";
|
|
48
48
|
export { StartFrom } from "./startFrom.js";
|
|
49
|
+
export { isTerminal, isUnknown } from "./standing.js";
|
|
49
50
|
export { materialiseEventSourced, materialiseKeyValue, materialiseWorkflowCommand, materialiseStep, } from "./materialise.js";
|
package/dist/keyedView.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Metadata } from "./effects/common.ts";
|
|
|
3
3
|
import type { ComponentClient, ComponentRef } from "./client.ts";
|
|
4
4
|
import { KeyedViewEffects, type KeyedViewEffect } from "./effects/keyed.ts";
|
|
5
5
|
import type { DeclaredQuery } from "./view.ts";
|
|
6
|
+
import type { Standing } from "./standing.ts";
|
|
6
7
|
type MaybePromise<T> = T | Promise<T>;
|
|
7
8
|
/** A keyed view's own rows, for the length of one change. It reaches no other view. */
|
|
8
9
|
export interface ViewRows<Row> {
|
|
@@ -32,13 +33,15 @@ export declare abstract class KeyedView<Row> {
|
|
|
32
33
|
get metadata(): Metadata;
|
|
33
34
|
/** The id of the entity the change came from. */
|
|
34
35
|
get subject(): string;
|
|
36
|
+
/** Of a change from a workflow: where it stood once the effect that recorded the state was applied. `undefined` for a change from an entity or a topic. */
|
|
37
|
+
get standing(): Standing | undefined;
|
|
35
38
|
/** This view's own rows, by key or by one of its declared queries. */
|
|
36
39
|
get rows(): ViewRows<Row>;
|
|
37
40
|
get client(): ComponentClient;
|
|
38
41
|
/** @internal */
|
|
39
42
|
get _kind(): "view";
|
|
40
43
|
/** @internal */
|
|
41
|
-
_bind(metadata: Metadata, client: ComponentClient | undefined, rows: ViewRows<Row
|
|
44
|
+
_bind(metadata: Metadata, client: ComponentClient | undefined, rows: ViewRows<Row>, standing?: Standing): void;
|
|
42
45
|
}
|
|
43
46
|
export interface KeyedViewClass<Row = unknown, C extends KeyedView<Row> = KeyedView<Row>> {
|
|
44
47
|
new (): C;
|
package/dist/keyedView.js
CHANGED
|
@@ -26,6 +26,7 @@ export function on(source, events, onChange, options = {}) {
|
|
|
26
26
|
export class KeyedView {
|
|
27
27
|
effects = new KeyedViewEffects();
|
|
28
28
|
#metadata = {};
|
|
29
|
+
#standing = undefined;
|
|
29
30
|
#client;
|
|
30
31
|
#rows;
|
|
31
32
|
/** The change's metadata: `ce-subject` is the source entity's id, `ankka.sequence` its sequence number. */
|
|
@@ -36,6 +37,10 @@ export class KeyedView {
|
|
|
36
37
|
get subject() {
|
|
37
38
|
return this.#metadata["ce-subject"] ?? "";
|
|
38
39
|
}
|
|
40
|
+
/** Of a change from a workflow: where it stood once the effect that recorded the state was applied. `undefined` for a change from an entity or a topic. */
|
|
41
|
+
get standing() {
|
|
42
|
+
return this.#standing;
|
|
43
|
+
}
|
|
39
44
|
/** This view's own rows, by key or by one of its declared queries. */
|
|
40
45
|
get rows() {
|
|
41
46
|
if (!this.#rows)
|
|
@@ -52,8 +57,9 @@ export class KeyedView {
|
|
|
52
57
|
return "view";
|
|
53
58
|
}
|
|
54
59
|
/** @internal */
|
|
55
|
-
_bind(metadata, client, rows) {
|
|
60
|
+
_bind(metadata, client, rows, standing) {
|
|
56
61
|
this.#metadata = metadata;
|
|
62
|
+
this.#standing = standing;
|
|
57
63
|
this.#client = client;
|
|
58
64
|
this.#rows = rows;
|
|
59
65
|
}
|
package/dist/kinds.js
CHANGED
|
@@ -30,6 +30,8 @@ const CODE_TO_PROTO = Object.freeze({
|
|
|
30
30
|
CONFLICT: ProtoErrorCode.CONFLICT,
|
|
31
31
|
TIMEOUT: ProtoErrorCode.TIMEOUT,
|
|
32
32
|
UNAVAILABLE: ProtoErrorCode.UNAVAILABLE,
|
|
33
|
+
WORKFLOW_FAILED: ProtoErrorCode.WORKFLOW_FAILED,
|
|
34
|
+
NOT_HOME: ProtoErrorCode.NOT_HOME,
|
|
33
35
|
});
|
|
34
36
|
export function errorCodeToProto(code) {
|
|
35
37
|
return CODE_TO_PROTO[code];
|
package/dist/routes.d.ts
CHANGED
|
@@ -13,15 +13,20 @@ export type CallerMatcher = {
|
|
|
13
13
|
readonly kind: "anyInProject";
|
|
14
14
|
} | {
|
|
15
15
|
readonly kind: "self";
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: "granted";
|
|
16
18
|
};
|
|
17
19
|
/**
|
|
18
20
|
* The callers an endpoint can name: `Acl.allowCallers(Callers.internet, Callers.service("orders"))`. A named
|
|
19
|
-
* 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.
|
|
20
24
|
*/
|
|
21
25
|
export declare const Callers: Readonly<{
|
|
22
26
|
internet: CallerMatcher;
|
|
23
27
|
anyInProject: CallerMatcher;
|
|
24
28
|
self: CallerMatcher;
|
|
29
|
+
granted: CallerMatcher;
|
|
25
30
|
service(name: string, options?: {
|
|
26
31
|
readonly project?: string;
|
|
27
32
|
}): CallerMatcher;
|
|
@@ -113,6 +118,21 @@ export interface SseEvent {
|
|
|
113
118
|
readonly name: string;
|
|
114
119
|
readonly value: unknown;
|
|
115
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>;
|
|
116
136
|
export declare function sseEvent(name: string, value: unknown): SseEvent;
|
|
117
137
|
/** A server-sent-events route: the handler returns an `AsyncIterable<string | SseEvent>`, one frame per string, and may yield an `SseEvent` among them. */
|
|
118
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>;
|
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
|
},
|
|
@@ -96,6 +99,29 @@ 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");
|
|
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
|
+
}
|
|
99
125
|
export function sseEvent(name, value) {
|
|
100
126
|
if (typeof name !== "string" || name === "" || /[\r\n]/.test(name))
|
|
101
127
|
throw new TypeError(`an event name must be one non-empty line, not ${JSON.stringify(name)}`);
|
|
@@ -14,3 +14,8 @@ export declare const SOCKETS_SINCE: readonly [number, number];
|
|
|
14
14
|
/** Why `spec` cannot be declared to a runtime speaking `runtime`, or undefined. A runtime before 1.9
|
|
15
15
|
* does not know a socket route, and would serve it as a plain GET. */
|
|
16
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
|
@@ -6,14 +6,59 @@ import { create } from "@bufbuild/protobuf";
|
|
|
6
6
|
import { Discovery } from "../_proto/ankka/protocol/v1/discovery_pb.js";
|
|
7
7
|
import { EmptySchema } from "../_proto/ankka/protocol/v1/payload_pb.js";
|
|
8
8
|
import { PROTOCOL_VERSION } from "../spec.js";
|
|
9
|
-
import {
|
|
9
|
+
import { Kind } from "../_proto/ankka/protocol/v1/discovery_pb.js";
|
|
10
|
+
import { olderThan, olderThanContracts, olderThanDeclaredQueries, olderThanStartPositions, olderThanViewStreams, olderThanWorkflowSources } from "../startFrom.js";
|
|
10
11
|
/**
|
|
11
12
|
* A sidecar older than 1.7 would ignore where a topic source starts and its version: a consumer
|
|
12
13
|
* declared `latest` would read everything, and a raised version would rebuild nothing. Refused,
|
|
13
14
|
* naming what declares them, rather than served wrong.
|
|
14
15
|
*/
|
|
15
16
|
export function refusal(spec, sidecarProtocol) {
|
|
16
|
-
return
|
|
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.`);
|
|
17
62
|
}
|
|
18
63
|
/**
|
|
19
64
|
* A sidecar older than 1.14 would ignore a topic's contract, broker and parallel reading and a
|
|
@@ -39,6 +84,21 @@ function contractRefusal(spec, sidecarProtocol) {
|
|
|
39
84
|
return (`${declaring.join(", ")} declare a topic's contract, broker or parallel reading, which the sidecar ignores: ` +
|
|
40
85
|
`it speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.14 or later.`);
|
|
41
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
|
+
}
|
|
42
102
|
/**
|
|
43
103
|
* A sidecar older than 1.13 would ignore a view's declared queries, would read a keyed view as having
|
|
44
104
|
* no source, and would refuse a version on a view that reads an entity as applying only to a topic.
|
|
@@ -84,7 +144,9 @@ export function discoveryRoutes(router, spec, log = console.error) {
|
|
|
84
144
|
async discover(info) {
|
|
85
145
|
log(`ankka: discovery from sidecar (protocol ${info.protocolVersion}, runtime ${info.runtimeVersion})`);
|
|
86
146
|
const answer = spec();
|
|
87
|
-
const why = refusal(answer, info.protocolVersion) ??
|
|
147
|
+
const why = refusal(answer, info.protocolVersion) ??
|
|
148
|
+
socketRefusal(answer, info.protocolVersion) ??
|
|
149
|
+
grantsRefusal(answer, info.protocolVersion);
|
|
88
150
|
if (why !== undefined) {
|
|
89
151
|
log(`ankka: ${why}`);
|
|
90
152
|
problems.push(why);
|
|
@@ -113,3 +175,28 @@ export function socketRefusal(spec, runtime) {
|
|
|
113
175
|
return undefined;
|
|
114
176
|
return `this runtime speaks protocol ${runtime || "unknown"}; a socket route needs 1.9 (${routes.join(", ")})`;
|
|
115
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;
|