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
|
@@ -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.js
CHANGED
|
@@ -47,6 +47,8 @@ function callerOf(req) {
|
|
|
47
47
|
return Object.freeze({ kind: "gateway" });
|
|
48
48
|
case "service":
|
|
49
49
|
return Object.freeze({ kind: "service", project: kind.value.project, name: kind.value.name });
|
|
50
|
+
case "machine":
|
|
51
|
+
return Object.freeze({ kind: "machine", organization: kind.value.organization, name: kind.value.name });
|
|
50
52
|
default:
|
|
51
53
|
return Object.freeze({ kind: "local" });
|
|
52
54
|
}
|
package/dist/server/stateless.js
CHANGED
|
@@ -15,6 +15,7 @@ import { JSON_CONTENT } from "../codec.js";
|
|
|
15
15
|
import { errorCodeToProto } from "../kinds.js";
|
|
16
16
|
import { decodePayload, encodePayload } from "./payloads.js";
|
|
17
17
|
import { PayloadSchema } from "../_proto/ankka/protocol/v1/payload_pb.js";
|
|
18
|
+
import { standingFromProto } from "../standing.js";
|
|
18
19
|
function messageOf(e) {
|
|
19
20
|
return e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
|
20
21
|
}
|
|
@@ -33,7 +34,7 @@ async function handleKeyedView(req, sourceId, ctx) {
|
|
|
33
34
|
const view = new registered.cls();
|
|
34
35
|
try {
|
|
35
36
|
const client = ctx.client.withMetadata(metadata);
|
|
36
|
-
view._bind(metadata, client, clientRows(client, registered.id, registered.rowCodec));
|
|
37
|
+
view._bind(metadata, client, clientRows(client, registered.id, registered.rowCodec), standingFromProto(req.standing));
|
|
37
38
|
const effect = (req.deleted
|
|
38
39
|
? source.deleted
|
|
39
40
|
? await source.deleted(view)
|
|
@@ -63,7 +64,7 @@ export async function handleView(req, ctx) {
|
|
|
63
64
|
const view = new registered.cls();
|
|
64
65
|
try {
|
|
65
66
|
const row = req.row ? decodePayload(registered.rowCodec, req.row) : null;
|
|
66
|
-
view._bind(row, metadata, ctx.client.withMetadata(metadata));
|
|
67
|
+
view._bind(row, metadata, ctx.client.withMetadata(metadata), standingFromProto(req.standing));
|
|
67
68
|
const effect = req.deleted ? await view.onDelete() : await view.onChange(decodePayload(registered.eventCodec, req.event));
|
|
68
69
|
switch (effect.kind) {
|
|
69
70
|
case "update-row":
|
|
@@ -102,7 +103,7 @@ function produceAll(request, messages) {
|
|
|
102
103
|
}
|
|
103
104
|
async function graphEffect(registered, req, metadata, ctx) {
|
|
104
105
|
const consumer = new registered.cls();
|
|
105
|
-
consumer._bind(metadata, ctx.client.withMetadata(metadata));
|
|
106
|
+
consumer._bind(metadata, ctx.client.withMetadata(metadata), standingFromProto(req.standing));
|
|
106
107
|
const effect = req.deleted ? await consumer.onDelete() : await consumer.onMessage(decodePayload(registered.messageCodec, req.message));
|
|
107
108
|
switch (effect.kind) {
|
|
108
109
|
case "publish":
|
|
@@ -121,7 +122,7 @@ async function graphEffect(registered, req, metadata, ctx) {
|
|
|
121
122
|
}
|
|
122
123
|
async function consumerEffect(registered, req, metadata, ctx) {
|
|
123
124
|
const consumer = new registered.cls();
|
|
124
|
-
consumer._bind(metadata, ctx.client.withMetadata(metadata));
|
|
125
|
+
consumer._bind(metadata, ctx.client.withMetadata(metadata), standingFromProto(req.standing));
|
|
125
126
|
const effect = req.deleted ? await consumer.onDelete() : await consumer.onMessage(decodePayload(registered.messageCodec, req.message));
|
|
126
127
|
const outCodec = () => {
|
|
127
128
|
if (!registered.outCodec)
|
package/dist/service.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface RegisteredEventSourced {
|
|
|
28
28
|
readonly eventCodec: Codec<any>;
|
|
29
29
|
readonly handlers: ReadonlyMap<string, HandlerRef<any, any, any, any>>;
|
|
30
30
|
readonly snapshotEvery: number;
|
|
31
|
+
/** Declared replicated (1.19). */
|
|
32
|
+
readonly replicated: boolean;
|
|
31
33
|
}
|
|
32
34
|
export interface RegisteredKeyValue {
|
|
33
35
|
readonly kind: "key-value";
|
|
@@ -58,6 +60,8 @@ export type Source = {
|
|
|
58
60
|
readonly contract?: Contract;
|
|
59
61
|
/** The declared broker the topic is on; absent is the installation's. */
|
|
60
62
|
readonly broker?: string;
|
|
63
|
+
/** Another project's topic: the project whose topic it is (1.15). */
|
|
64
|
+
readonly project?: string;
|
|
61
65
|
/** Partitions handled at once, each in order. */
|
|
62
66
|
readonly parallel?: boolean;
|
|
63
67
|
};
|
|
@@ -102,6 +106,8 @@ export interface RegisteredConsumer {
|
|
|
102
106
|
/** A graph consumer: its handlers return elements, each published as a delta under its element key. */
|
|
103
107
|
readonly graph: boolean;
|
|
104
108
|
readonly version: number | undefined;
|
|
109
|
+
/** Given a replicated entity's mirrored changes too (1.19). */
|
|
110
|
+
readonly mirroredChanges: boolean;
|
|
105
111
|
}
|
|
106
112
|
export interface RegisteredTimedAction {
|
|
107
113
|
readonly kind: "timed-action";
|
package/dist/service.js
CHANGED
|
@@ -191,7 +191,18 @@ function registerEventSourced(cls, problems) {
|
|
|
191
191
|
const handlers = collectHandlers(cls.handlers, "handlers", ["command", "query"], fail);
|
|
192
192
|
if (!ok())
|
|
193
193
|
return undefined;
|
|
194
|
-
|
|
194
|
+
if (cls.replicated !== undefined && typeof cls.replicated !== "boolean")
|
|
195
|
+
fail(`replicated must be true or false, not ${String(cls.replicated)}`);
|
|
196
|
+
return Object.freeze({
|
|
197
|
+
kind: "event-sourced",
|
|
198
|
+
id: cls.componentId,
|
|
199
|
+
cls,
|
|
200
|
+
stateCodec: codecFor(cls.state),
|
|
201
|
+
eventCodec: codecFor(cls.events),
|
|
202
|
+
handlers,
|
|
203
|
+
snapshotEvery,
|
|
204
|
+
replicated: cls.replicated === true,
|
|
205
|
+
});
|
|
195
206
|
}
|
|
196
207
|
function registerKeyValue(cls, problems) {
|
|
197
208
|
const { fail, ok } = checker(cls, problems);
|
|
@@ -261,6 +272,12 @@ function topicProblems(cls, consumer, fail) {
|
|
|
261
272
|
if ((cls.contract !== undefined || cls.broker !== undefined || cls.parallel !== undefined) && !readsTopic) {
|
|
262
273
|
fail("declares a contract, a broker or parallel, which apply to a topic; it reads a component");
|
|
263
274
|
}
|
|
275
|
+
if (cls.project !== undefined && (typeof cls.project !== "string" || cls.project.trim() === ""))
|
|
276
|
+
fail("project must be the id of the project whose topic this is");
|
|
277
|
+
if (cls.project !== undefined && !readsTopic)
|
|
278
|
+
fail("names a project, which applies to a topic; it reads a component");
|
|
279
|
+
if (cls.project !== undefined && cls.broker !== undefined)
|
|
280
|
+
fail("names a project and a broker; another project's topic is on the installation's broker");
|
|
264
281
|
}
|
|
265
282
|
function sourceOf(cls, fail, consumer) {
|
|
266
283
|
const hasSource = cls.source !== undefined;
|
|
@@ -281,6 +298,7 @@ function sourceOf(cls, fail, consumer) {
|
|
|
281
298
|
...(cls.contract instanceof Contract ? { contract: cls.contract } : {}),
|
|
282
299
|
...(typeof cls.broker === "string" ? { broker: cls.broker } : {}),
|
|
283
300
|
...(cls.parallel === true ? { parallel: true } : {}),
|
|
301
|
+
...(typeof cls.project === "string" ? { project: cls.project } : {}),
|
|
284
302
|
};
|
|
285
303
|
}
|
|
286
304
|
const src = cls.source;
|
|
@@ -413,6 +431,7 @@ function registerConsumer(cls, problems) {
|
|
|
413
431
|
produces,
|
|
414
432
|
graph: false,
|
|
415
433
|
version: cls.version,
|
|
434
|
+
mirroredChanges: cls.mirroredChanges === true,
|
|
416
435
|
});
|
|
417
436
|
}
|
|
418
437
|
/**
|
|
@@ -436,10 +455,15 @@ function publicationOf(declared, fail) {
|
|
|
436
455
|
fail("producesTo.contract must be a Contract: Contract.fromFile(path, name)");
|
|
437
456
|
if (p.broker !== undefined && (typeof p.broker !== "string" || p.broker.trim() === ""))
|
|
438
457
|
fail("producesTo.broker must be the name of a broker the project declares");
|
|
458
|
+
if (p.project !== undefined && (typeof p.project !== "string" || p.project.trim() === ""))
|
|
459
|
+
fail("producesTo.project must be the id of the project whose topic this is");
|
|
460
|
+
if (p.project !== undefined && p.broker !== undefined)
|
|
461
|
+
fail("producesTo names a project and a broker; another project's topic is on the installation's broker");
|
|
439
462
|
return {
|
|
440
463
|
topic: p.topic,
|
|
441
464
|
...(p.contract instanceof Contract ? { contract: p.contract } : {}),
|
|
442
465
|
...(typeof p.broker === "string" ? { broker: p.broker } : {}),
|
|
466
|
+
...(typeof p.project === "string" ? { project: p.project } : {}),
|
|
443
467
|
};
|
|
444
468
|
}
|
|
445
469
|
/** A graph consumer is a consumer to discovery and to the runtime: one that produces deltas to its topic. */
|
|
@@ -465,6 +489,7 @@ function registerGraphConsumer(cls, problems) {
|
|
|
465
489
|
produces: undefined,
|
|
466
490
|
graph: true,
|
|
467
491
|
version: cls.version,
|
|
492
|
+
mirroredChanges: cls.mirroredChanges === true,
|
|
468
493
|
});
|
|
469
494
|
}
|
|
470
495
|
function registerTimedAction(cls, problems) {
|
package/dist/spec.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type MessageInitShape } from "@bufbuild/protobuf";
|
|
|
2
2
|
import { Endpoint_Acl, type CallerMatcherSchema, type Spec } from "./_proto/ankka/protocol/v1/discovery_pb.ts";
|
|
3
3
|
import type { Registry } from "./service.ts";
|
|
4
4
|
import type { Acl } from "./routes.ts";
|
|
5
|
-
export declare const PROTOCOL_VERSION = "1.
|
|
5
|
+
export declare const PROTOCOL_VERSION = "1.19";
|
|
6
6
|
export declare const SDK_NAME = "ankka-typescript";
|
|
7
7
|
export declare function aclToProto(acl: Acl): Endpoint_Acl;
|
|
8
8
|
/** The callers a CALLERS acl names, as discovery carries them; empty for every other rule. */
|
package/dist/spec.js
CHANGED
|
@@ -17,7 +17,7 @@ function toolInit(t) {
|
|
|
17
17
|
...(approval !== undefined ? { approval } : {}),
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
export const PROTOCOL_VERSION = "1.
|
|
20
|
+
export const PROTOCOL_VERSION = "1.19";
|
|
21
21
|
export const SDK_NAME = "ankka-typescript";
|
|
22
22
|
export function aclToProto(acl) {
|
|
23
23
|
if (typeof acl === "object")
|
|
@@ -43,6 +43,8 @@ export function callersToProto(acl) {
|
|
|
43
43
|
return { kind: { case: "anyInProject", value: {} } };
|
|
44
44
|
case "self":
|
|
45
45
|
return { kind: { case: "self", value: {} } };
|
|
46
|
+
case "granted":
|
|
47
|
+
return { kind: { case: "granted", value: {} } };
|
|
46
48
|
case "service":
|
|
47
49
|
return { kind: { case: "service", value: m.project !== undefined ? { name: m.name, project: m.project } : { name: m.name } } };
|
|
48
50
|
}
|
|
@@ -62,6 +64,8 @@ function sourceInit(source) {
|
|
|
62
64
|
...(source.contract !== undefined ? { contract: { name: source.contract.name, fingerprint: source.contract.fingerprint } } : {}),
|
|
63
65
|
...(source.broker !== undefined ? { broker: source.broker } : {}),
|
|
64
66
|
...(source.parallel === true ? { parallel: true } : {}),
|
|
67
|
+
// 1.15: another project's topic.
|
|
68
|
+
...(source.project !== undefined ? { project: source.project } : {}),
|
|
65
69
|
};
|
|
66
70
|
if (start === undefined)
|
|
67
71
|
return { source: { case: "topic", value: source.topic }, ...options };
|
|
@@ -93,7 +97,7 @@ function componentInit(c) {
|
|
|
93
97
|
case "autonomous-agent":
|
|
94
98
|
return autonomousInit(c);
|
|
95
99
|
case "event-sourced":
|
|
96
|
-
return { ...base, handlers: handlerInits(c.handlers), detail: { case: "eventSourced", value: { snapshotEvery: c.snapshotEvery } } };
|
|
100
|
+
return { ...base, handlers: handlerInits(c.handlers), detail: { case: "eventSourced", value: { snapshotEvery: c.snapshotEvery, replicated: c.replicated } } };
|
|
97
101
|
case "key-value":
|
|
98
102
|
return { ...base, handlers: handlerInits(c.handlers), detail: { case: "keyValue", value: {} } };
|
|
99
103
|
case "workflow":
|
|
@@ -113,7 +117,7 @@ function componentInit(c) {
|
|
|
113
117
|
rowManifest: c.rowCodec.manifest,
|
|
114
118
|
queries: [...c.queries],
|
|
115
119
|
...(c.version !== undefined ? { version: c.version } : {}),
|
|
116
|
-
declaredQueries: c.declared.map((q) => ({ name: q.name, statement: q.statement })),
|
|
120
|
+
declaredQueries: c.declared.map((q) => ({ name: q.name, statement: q.statement, watched: q.watched ?? false })),
|
|
117
121
|
},
|
|
118
122
|
},
|
|
119
123
|
};
|
|
@@ -128,7 +132,7 @@ function componentInit(c) {
|
|
|
128
132
|
queries: [],
|
|
129
133
|
...(c.version !== undefined ? { version: c.version } : {}),
|
|
130
134
|
sources: [...c.sources.values()].map((s) => sourceInit({ component: { kind: s.kind, id: s.id } })),
|
|
131
|
-
declaredQueries: c.declared.map((q) => ({ name: q.name, statement: q.statement })),
|
|
135
|
+
declaredQueries: c.declared.map((q) => ({ name: q.name, statement: q.statement, watched: q.watched ?? false })),
|
|
132
136
|
},
|
|
133
137
|
},
|
|
134
138
|
};
|
|
@@ -142,6 +146,7 @@ function componentInit(c) {
|
|
|
142
146
|
source: sourceInit(c.source),
|
|
143
147
|
...(c.producesTo !== undefined ? { producesTo: c.producesTo } : {}),
|
|
144
148
|
...(c.version !== undefined ? { version: c.version } : {}),
|
|
149
|
+
mirroredChanges: c.mirroredChanges,
|
|
145
150
|
// 1.14: the publication with its contract and broker; `producesTo` stays for an older sidecar.
|
|
146
151
|
...(c.produces !== undefined
|
|
147
152
|
? {
|
|
@@ -149,6 +154,7 @@ function componentInit(c) {
|
|
|
149
154
|
topic: c.produces.topic,
|
|
150
155
|
...(c.produces.contract !== undefined ? { contract: { name: c.produces.contract.name, fingerprint: c.produces.contract.fingerprint } } : {}),
|
|
151
156
|
...(c.produces.broker !== undefined ? { broker: c.produces.broker } : {}),
|
|
157
|
+
...(c.produces.project !== undefined ? { project: c.produces.project } : {}),
|
|
152
158
|
},
|
|
153
159
|
}
|
|
154
160
|
: {}),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WorkflowStanding } from "./_proto/ankka/protocol/v1/payload_pb.ts";
|
|
2
|
+
export type StandingStatus = "NotStarted" | "Running" | "Paused" | "Completed" | "Failed" | "Unknown";
|
|
3
|
+
export interface Standing {
|
|
4
|
+
readonly status: StandingStatus;
|
|
5
|
+
/** The step the workflow is on, or the step a pause names or follows. */
|
|
6
|
+
readonly step?: string;
|
|
7
|
+
/** Retries so far, per step. */
|
|
8
|
+
readonly retries: Readonly<Record<string, number>>;
|
|
9
|
+
/** Why it failed, when it did. */
|
|
10
|
+
readonly failure?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function isTerminal(standing: Standing): boolean;
|
|
13
|
+
/** The state was recorded before the platform stamped standings. */
|
|
14
|
+
export declare function isUnknown(standing: Standing): boolean;
|
|
15
|
+
/** @internal The standing a request carries, or `undefined` for a change from an entity or a topic. */
|
|
16
|
+
export declare function standingFromProto(standing: WorkflowStanding | undefined): Standing | undefined;
|
|
17
|
+
/** @internal The version whose runtimes read a workflow as a source. */
|
|
18
|
+
export declare const WORKFLOW_SOURCE_PROTOCOL: readonly [number, number];
|
package/dist/standing.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Where a workflow stood once the effect that recorded a state was applied (protocol 1.16).
|
|
2
|
+
//
|
|
3
|
+
// A view or a consumer whose source is a workflow is handed, with each state the workflow records,
|
|
4
|
+
// its standing: running, paused, completed or failed, the step it is on or waits after, the retries
|
|
5
|
+
// of each step and, when it failed, why. `Unknown` is the standing of a state recorded before the
|
|
6
|
+
// platform stamped standings. A change from an entity or a topic has none.
|
|
7
|
+
export function isTerminal(standing) {
|
|
8
|
+
return standing.status === "Completed" || standing.status === "Failed";
|
|
9
|
+
}
|
|
10
|
+
/** The state was recorded before the platform stamped standings. */
|
|
11
|
+
export function isUnknown(standing) {
|
|
12
|
+
return standing.status === "Unknown";
|
|
13
|
+
}
|
|
14
|
+
/** @internal The standing a request carries, or `undefined` for a change from an entity or a topic. */
|
|
15
|
+
export function standingFromProto(standing) {
|
|
16
|
+
if (standing === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
status: standing.status,
|
|
20
|
+
...(standing.step ? { step: standing.step } : {}),
|
|
21
|
+
retries: Object.freeze({ ...standing.retries }),
|
|
22
|
+
...(standing.failure ? { failure: standing.failure } : {}),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** @internal The version whose runtimes read a workflow as a source. */
|
|
26
|
+
export const WORKFLOW_SOURCE_PROTOCOL = [1, 16];
|
package/dist/startFrom.d.ts
CHANGED
|
@@ -24,9 +24,13 @@ export declare const START_POSITION_PROTOCOL: readonly [number, number];
|
|
|
24
24
|
export declare function isStartFrom(value: unknown): value is StartFrom;
|
|
25
25
|
/** The first protocol in which a process can declare a view's queries. */
|
|
26
26
|
export declare const DECLARED_QUERY_PROTOCOL: readonly [number, number];
|
|
27
|
+
export declare function olderThan(protocolVersion: string, [wantMajor, wantMinor]: readonly [number, number]): boolean;
|
|
27
28
|
/** Whether a sidecar speaking `protocolVersion` would ignore a start position and a version. */
|
|
28
29
|
export declare function olderThanStartPositions(protocolVersion: string): boolean;
|
|
29
30
|
/** Whether a sidecar speaking `protocolVersion` would ignore a view's declared queries. */
|
|
30
31
|
export declare function olderThanDeclaredQueries(protocolVersion: string): boolean;
|
|
32
|
+
/** Whether a sidecar speaking `protocolVersion` would not know a watched query. */
|
|
33
|
+
export declare function olderThanViewStreams(protocolVersion: string): boolean;
|
|
31
34
|
/** Whether a sidecar speaking `protocolVersion` would ignore a topic's contract, broker and parallel reading. */
|
|
35
|
+
export declare function olderThanWorkflowSources(protocolVersion: string): boolean;
|
|
32
36
|
export declare function olderThanContracts(protocolVersion: string): boolean;
|
package/dist/startFrom.js
CHANGED
|
@@ -24,7 +24,7 @@ export function isStartFrom(value) {
|
|
|
24
24
|
}
|
|
25
25
|
/** The first protocol in which a process can declare a view's queries. */
|
|
26
26
|
export const DECLARED_QUERY_PROTOCOL = [1, 13];
|
|
27
|
-
function olderThan(protocolVersion, [wantMajor, wantMinor]) {
|
|
27
|
+
export function olderThan(protocolVersion, [wantMajor, wantMinor]) {
|
|
28
28
|
const [major, minor] = protocolVersion.split(".").map((part) => Number.parseInt(part, 10));
|
|
29
29
|
if (!Number.isFinite(major) || !Number.isFinite(minor))
|
|
30
30
|
return false;
|
|
@@ -38,7 +38,14 @@ export function olderThanStartPositions(protocolVersion) {
|
|
|
38
38
|
export function olderThanDeclaredQueries(protocolVersion) {
|
|
39
39
|
return olderThan(protocolVersion, DECLARED_QUERY_PROTOCOL);
|
|
40
40
|
}
|
|
41
|
+
/** Whether a sidecar speaking `protocolVersion` would not know a watched query. */
|
|
42
|
+
export function olderThanViewStreams(protocolVersion) {
|
|
43
|
+
return olderThan(protocolVersion, [1, 17]);
|
|
44
|
+
}
|
|
41
45
|
/** Whether a sidecar speaking `protocolVersion` would ignore a topic's contract, broker and parallel reading. */
|
|
46
|
+
export function olderThanWorkflowSources(protocolVersion) {
|
|
47
|
+
return olderThan(protocolVersion, [1, 16]);
|
|
48
|
+
}
|
|
42
49
|
export function olderThanContracts(protocolVersion) {
|
|
43
50
|
return olderThan(protocolVersion, [1, 14]);
|
|
44
51
|
}
|
package/dist/testkit/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { EventSourcedTestKit, EndpointTestKit, Response, type RequestOptions, type Materialised, type SocketRun } from "./unit.ts";
|
|
1
|
+
export { EventSourcedTestKit, EndpointTestKit, Response, type RequestOptions, type Materialised, type SocketRun, type ReplicationMode } from "./unit.ts";
|
|
2
2
|
export { KeyValueTestKit, WorkflowTestKit, ViewTestKit, KeyedViewTestKit, ConsumerTestKit, GraphConsumerTestKit, TimedActionTestKit, AgentTestKit, ScriptedModel, type WorkflowProgress, type ModelResponse, type ModelCall, type GraphChange, } from "./kinds.ts";
|
|
3
3
|
export { AnkkaTestKit, PUBLISHED_SIDECAR, sidecarImage, type AnkkaTestKitOptions, type HttpResponse, type Http, type Beside } from "./integration.ts";
|
package/dist/testkit/kinds.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { Agent, AgentClass } from "../agent.ts";
|
|
|
15
15
|
import type { HandlerRef } from "../handlers.ts";
|
|
16
16
|
import { type MaterialisedKeyValue, type MaterialisedStep, type MaterialisedWorkflowCommand } from "../materialise.ts";
|
|
17
17
|
import { type AgentOutcome, type DecisionInput } from "../approvals.ts";
|
|
18
|
+
import type { Standing } from "../standing.ts";
|
|
18
19
|
/** Drives one key value entity instance through its handlers. */
|
|
19
20
|
export declare class KeyValueTestKit<S, C extends KeyValueEntity<S>> {
|
|
20
21
|
#private;
|
|
@@ -58,8 +59,11 @@ export declare class ViewTestKit<E, Row, C extends View<E, Row>> {
|
|
|
58
59
|
readonly rows: Map<string, Row>;
|
|
59
60
|
private constructor();
|
|
60
61
|
static of<E, Row, C extends View<E, Row>>(cls: ViewClass<E, Row, C>, client?: ComponentClient): ViewTestKit<E, Row, C>;
|
|
61
|
-
/**
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* A change from source instance `key`: the event, round-tripped through the source's event codec.
|
|
64
|
+
* `standing` is a workflow's, for a view whose source is one.
|
|
65
|
+
*/
|
|
66
|
+
onChange(key: string, event: E, metadata?: Metadata, standing?: Standing): Promise<ViewEffect<Row>>;
|
|
63
67
|
/** The source instance `key` was deleted. */
|
|
64
68
|
onDelete(key: string, metadata?: Metadata): Promise<ViewEffect<Row>>;
|
|
65
69
|
get(key: string): Row | null;
|
|
@@ -80,8 +84,8 @@ export declare class KeyedViewTestKit<Row, C extends KeyedView<Row>> {
|
|
|
80
84
|
static of<Row, C extends KeyedView<Row>>(cls: KeyedViewClass<Row, C>, client?: ComponentClient): KeyedViewTestKit<Row, C>;
|
|
81
85
|
/** What the declared query `name` answers, given the values it is asked with. */
|
|
82
86
|
answering(name: string, answer: (values: Readonly<Record<string, string>>) => Row[]): this;
|
|
83
|
-
/** A change of
|
|
84
|
-
change<E>(entity: ComponentRef, key: string, event: E): Promise<KeyedViewEffect<Row>>;
|
|
87
|
+
/** A change of `key` of `entity`, round-tripped through that source's codec; `standing` a workflow's. */
|
|
88
|
+
change<E>(entity: ComponentRef, key: string, event: E, standing?: Standing): Promise<KeyedViewEffect<Row>>;
|
|
85
89
|
/** The entity `key` of `entity` was deleted. */
|
|
86
90
|
deleted(entity: ComponentRef, key: string): Promise<KeyedViewEffect<Row>>;
|
|
87
91
|
get(key: string): Row | null;
|
|
@@ -104,7 +108,7 @@ export declare class ConsumerTestKit<M, Out, C extends Consumer<M, Out>> {
|
|
|
104
108
|
* A message from source instance `subject`. The metadata says the runtime speaks this SDK's protocol
|
|
105
109
|
* version; pass `"ankka.protocol"` or `"ankka.sequence"` to say otherwise.
|
|
106
110
|
*/
|
|
107
|
-
onMessage(message: M, subject?: string, metadata?: Metadata): Promise<ConsumerEffect<Out>>;
|
|
111
|
+
onMessage(message: M, subject?: string, metadata?: Metadata, standing?: Standing): Promise<ConsumerEffect<Out>>;
|
|
108
112
|
onDelete(subject?: string, metadata?: Metadata): Promise<ConsumerEffect<Out>>;
|
|
109
113
|
}
|
|
110
114
|
/**
|
package/dist/testkit/kinds.js
CHANGED
|
@@ -199,11 +199,11 @@ export class ViewTestKit {
|
|
|
199
199
|
static of(cls, client = noClient()) {
|
|
200
200
|
return new ViewTestKit(cls, client);
|
|
201
201
|
}
|
|
202
|
-
async #apply(key, fn, metadata) {
|
|
202
|
+
async #apply(key, fn, metadata, standing) {
|
|
203
203
|
const view = new this.#cls();
|
|
204
204
|
const rowCodec = this.#registered.rowCodec;
|
|
205
205
|
const current = this.rows.get(key);
|
|
206
|
-
view._bind(current === undefined ? null : roundTrip(rowCodec, current), { "ce-subject": key, ...metadata }, this.#client.withMetadata(metadata));
|
|
206
|
+
view._bind(current === undefined ? null : roundTrip(rowCodec, current), { "ce-subject": key, ...metadata }, this.#client.withMetadata(metadata), standing);
|
|
207
207
|
const effect = await fn(view);
|
|
208
208
|
switch (effect.kind) {
|
|
209
209
|
case "update-row":
|
|
@@ -217,10 +217,13 @@ export class ViewTestKit {
|
|
|
217
217
|
}
|
|
218
218
|
return effect;
|
|
219
219
|
}
|
|
220
|
-
/**
|
|
221
|
-
|
|
220
|
+
/**
|
|
221
|
+
* A change from source instance `key`: the event, round-tripped through the source's event codec.
|
|
222
|
+
* `standing` is a workflow's, for a view whose source is one.
|
|
223
|
+
*/
|
|
224
|
+
onChange(key, event, metadata = {}, standing) {
|
|
222
225
|
const wire = roundTrip(this.#registered.eventCodec, event);
|
|
223
|
-
return this.#apply(key, (v) => v.onChange(wire), metadata);
|
|
226
|
+
return this.#apply(key, (v) => v.onChange(wire), metadata, standing);
|
|
224
227
|
}
|
|
225
228
|
/** The source instance `key` was deleted. */
|
|
226
229
|
onDelete(key, metadata = {}) {
|
|
@@ -265,7 +268,7 @@ export class KeyedViewTestKit {
|
|
|
265
268
|
throw new Error(`${this.#registered.id} reads no ${JSON.stringify(entity.componentId)}`);
|
|
266
269
|
return source;
|
|
267
270
|
}
|
|
268
|
-
async #apply(key, run) {
|
|
271
|
+
async #apply(key, run, standing) {
|
|
269
272
|
const view = new this.#cls();
|
|
270
273
|
const rowCodec = this.#registered.rowCodec;
|
|
271
274
|
const id = this.#registered.id;
|
|
@@ -283,7 +286,7 @@ export class KeyedViewTestKit {
|
|
|
283
286
|
};
|
|
284
287
|
this.#sequence += 1;
|
|
285
288
|
const metadata = { "ce-subject": key, "ankka.sequence": String(this.#sequence) };
|
|
286
|
-
view._bind(metadata, this.#client.withMetadata(metadata), rows);
|
|
289
|
+
view._bind(metadata, this.#client.withMetadata(metadata), rows, standing);
|
|
287
290
|
const effect = ((await run(view)) ?? { kind: "rows", changes: [] });
|
|
288
291
|
if (effect.kind !== "rows")
|
|
289
292
|
throw new TypeError(`${id}'s handler returned something that is not a keyed view effect`);
|
|
@@ -295,11 +298,11 @@ export class KeyedViewTestKit {
|
|
|
295
298
|
}
|
|
296
299
|
return effect;
|
|
297
300
|
}
|
|
298
|
-
/** A change of
|
|
299
|
-
change(entity, key, event) {
|
|
301
|
+
/** A change of `key` of `entity`, round-tripped through that source's codec; `standing` a workflow's. */
|
|
302
|
+
change(entity, key, event, standing) {
|
|
300
303
|
const source = this.#source(entity);
|
|
301
304
|
const wire = roundTrip(source.eventCodec, event);
|
|
302
|
-
return this.#apply(key, (v) => source.onChange(v, wire));
|
|
305
|
+
return this.#apply(key, (v) => source.onChange(v, wire), standing);
|
|
303
306
|
}
|
|
304
307
|
/** The entity `key` of `entity` was deleted. */
|
|
305
308
|
deleted(entity, key) {
|
|
@@ -328,9 +331,9 @@ export class ConsumerTestKit {
|
|
|
328
331
|
static of(cls, client = noClient()) {
|
|
329
332
|
return new ConsumerTestKit(cls, client);
|
|
330
333
|
}
|
|
331
|
-
async #apply(fn, metadata) {
|
|
334
|
+
async #apply(fn, metadata, standing) {
|
|
332
335
|
const consumer = new this.#cls();
|
|
333
|
-
consumer._bind(metadata, this.#client.withMetadata(metadata));
|
|
336
|
+
consumer._bind(metadata, this.#client.withMetadata(metadata), standing);
|
|
334
337
|
const effect = await fn(consumer);
|
|
335
338
|
const outCodec = () => {
|
|
336
339
|
const codec = this.#registered.outCodec;
|
|
@@ -354,9 +357,9 @@ export class ConsumerTestKit {
|
|
|
354
357
|
* A message from source instance `subject`. The metadata says the runtime speaks this SDK's protocol
|
|
355
358
|
* version; pass `"ankka.protocol"` or `"ankka.sequence"` to say otherwise.
|
|
356
359
|
*/
|
|
357
|
-
onMessage(message, subject = "test", metadata = {}) {
|
|
360
|
+
onMessage(message, subject = "test", metadata = {}, standing) {
|
|
358
361
|
const wire = roundTrip(this.#registered.messageCodec, message);
|
|
359
|
-
return this.#apply((c) => c.onMessage(wire), { "ce-subject": subject, [PROTOCOL_KEY]: PROTOCOL_VERSION, ...metadata });
|
|
362
|
+
return this.#apply((c) => c.onMessage(wire), { "ce-subject": subject, [PROTOCOL_KEY]: PROTOCOL_VERSION, ...metadata }, standing);
|
|
360
363
|
}
|
|
361
364
|
onDelete(subject = "test", metadata = {}) {
|
|
362
365
|
return this.#apply((c) => c.onDelete(), { "ce-subject": subject, [PROTOCOL_KEY]: PROTOCOL_VERSION, ...metadata });
|
package/dist/testkit/unit.d.ts
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { ComponentClient } from "../client.ts";
|
|
2
2
|
import { type Principal } from "../context.ts";
|
|
3
|
-
import type
|
|
3
|
+
import { type ErrorDetail, type Metadata, type Retention } from "../effects/common.ts";
|
|
4
|
+
import type { ReplicationChange } from "../effects/eventSourced.ts";
|
|
4
5
|
import type { Endpoint, EndpointClass } from "../endpoint.ts";
|
|
5
6
|
import { EventSourcedEntity, type EventSourcedEntityClass } from "../eventSourcedEntity.ts";
|
|
6
7
|
import type { HandlerRef } from "../handlers.ts";
|
|
7
8
|
import { type Materialised } from "../materialise.ts";
|
|
8
9
|
export type { Materialised };
|
|
9
10
|
/** Drives one event sourced entity instance through its handlers, folding events as it goes. */
|
|
11
|
+
/** How a replicated entity's copy stands in the test kit (1.19); `installation` is the copy's own. */
|
|
12
|
+
export type ReplicationMode = {
|
|
13
|
+
readonly kind: "unreplicated";
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "home";
|
|
16
|
+
readonly installation?: string;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "follower";
|
|
19
|
+
readonly home: string;
|
|
20
|
+
readonly installation?: string;
|
|
21
|
+
};
|
|
10
22
|
export declare class EventSourcedTestKit<S, E, C extends EventSourcedEntity<S, E>> {
|
|
11
23
|
#private;
|
|
12
24
|
readonly entityId: string;
|
|
25
|
+
/** The handover or change to the replication set the last call asked for, if any. */
|
|
26
|
+
lastReplication: ReplicationChange | undefined;
|
|
13
27
|
/** The state now, after every call so far. */
|
|
14
28
|
state: S;
|
|
15
29
|
/** The sequence number of the last event, `0n` before any. */
|
|
@@ -18,7 +32,18 @@ export declare class EventSourcedTestKit<S, E, C extends EventSourcedEntity<S, E
|
|
|
18
32
|
readonly allEvents: E[];
|
|
19
33
|
private constructor();
|
|
20
34
|
/** A kit for `cls` as instance `entityId`. Calls to other components reach `client`, which by default refuses them. */
|
|
21
|
-
static of<S, E, C extends EventSourcedEntity<S, E>>(cls: EventSourcedEntityClass<S, E, C>, entityId?: string, client?: ComponentClient
|
|
35
|
+
static of<S, E, C extends EventSourcedEntity<S, E>>(cls: EventSourcedEntityClass<S, E, C>, entityId?: string, client?: ComponentClient,
|
|
36
|
+
/**
|
|
37
|
+
* How a replicated entity's copy stands (1.19): `{ kind: "home" }`, where every command runs, or
|
|
38
|
+
* `{ kind: "follower", home }`, where a command that may persist is refused `NOT_HOME` and a query is
|
|
39
|
+
* answered from what `replay` gave it. Not replicated by default.
|
|
40
|
+
*/
|
|
41
|
+
mode?: ReplicationMode): EventSourcedTestKit<S, E, C>;
|
|
42
|
+
/**
|
|
43
|
+
* Applies `events` as history the copy already holds, written at `writtenAt` (this installation by
|
|
44
|
+
* default): for a follower, what it has mirrored from its home.
|
|
45
|
+
*/
|
|
46
|
+
replay(events: readonly E[], writtenAt?: string | null): void;
|
|
22
47
|
/** Sends a command or query and returns what its effect amounts to. */
|
|
23
48
|
call<I, R>(handler: HandlerRef<C, I, R, any> | string, input?: I, metadata?: Metadata): Promise<Materialised<S, E, R>>;
|
|
24
49
|
}
|