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.
Files changed (63) hide show
  1. package/README.md +1 -0
  2. package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +316 -0
  3. package/dist/_proto/ankka/protocol/v1/client_pb.js +97 -23
  4. package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +10 -1
  5. package/dist/_proto/ankka/protocol/v1/consumer_pb.js +1 -1
  6. package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +43 -0
  7. package/dist/_proto/ankka/protocol/v1/discovery_pb.js +1 -1
  8. package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +26 -0
  9. package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +15 -10
  10. package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +86 -0
  11. package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +11 -1
  12. package/dist/_proto/ankka/protocol/v1/payload_pb.d.ts +61 -0
  13. package/dist/_proto/ankka/protocol/v1/payload_pb.js +18 -1
  14. package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +7 -1
  15. package/dist/_proto/ankka/protocol/v1/view_pb.js +1 -1
  16. package/dist/_proto/ankka/protocol/v1/wasm_pb.d.ts +12 -0
  17. package/dist/_proto/ankka/protocol/v1/wasm_pb.js +1 -1
  18. package/dist/client.d.ts +96 -3
  19. package/dist/client.js +228 -6
  20. package/dist/consumer.d.ts +18 -1
  21. package/dist/consumer.js +15 -1
  22. package/dist/context.d.ts +25 -1
  23. package/dist/context.js +5 -2
  24. package/dist/contract.d.ts +4 -0
  25. package/dist/contract.js +2 -0
  26. package/dist/effects/common.d.ts +7 -0
  27. package/dist/effects/common.js +9 -1
  28. package/dist/effects/eventSourced.d.ts +26 -1
  29. package/dist/effects/eventSourced.js +58 -5
  30. package/dist/eventSourcedEntity.d.ts +8 -3
  31. package/dist/graph.d.ts +9 -1
  32. package/dist/graph.js +7 -1
  33. package/dist/index.d.ts +5 -4
  34. package/dist/index.js +3 -2
  35. package/dist/keyedView.d.ts +4 -1
  36. package/dist/keyedView.js +7 -1
  37. package/dist/kinds.js +2 -0
  38. package/dist/routes.d.ts +21 -1
  39. package/dist/routes.js +27 -1
  40. package/dist/server/discovery.d.ts +5 -0
  41. package/dist/server/discovery.js +90 -3
  42. package/dist/server/eventSourced.d.ts +4 -1
  43. package/dist/server/eventSourced.js +24 -6
  44. package/dist/server/http.js +2 -0
  45. package/dist/server/stateless.js +5 -4
  46. package/dist/service.d.ts +6 -0
  47. package/dist/service.js +26 -1
  48. package/dist/spec.d.ts +1 -1
  49. package/dist/spec.js +10 -4
  50. package/dist/standing.d.ts +18 -0
  51. package/dist/standing.js +26 -0
  52. package/dist/startFrom.d.ts +4 -0
  53. package/dist/startFrom.js +8 -1
  54. package/dist/testkit/index.d.ts +1 -1
  55. package/dist/testkit/kinds.d.ts +9 -5
  56. package/dist/testkit/kinds.js +17 -14
  57. package/dist/testkit/unit.d.ts +27 -2
  58. package/dist/testkit/unit.js +47 -7
  59. package/dist/version.d.ts +1 -1
  60. package/dist/version.js +1 -1
  61. package/dist/view.d.ts +18 -3
  62. package/dist/view.js +13 -4
  63. 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
- state = entity.applyEvent(state, decodePayload(registered.eventCodec, ev.payload));
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
- const m = materialiseEventSourced(effect, state, (s, e) => entity.applyEvent(s, e));
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
  });
@@ -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
  }
@@ -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
- return Object.freeze({ kind: "event-sourced", id: cls.componentId, cls, stateCodec: codecFor(cls.state), eventCodec: codecFor(cls.events), handlers, snapshotEvery });
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.14";
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.14";
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];
@@ -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];
@@ -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
  }
@@ -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";
@@ -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
- /** A change from source instance `key`: the event, round-tripped through the source's event codec. */
62
- onChange(key: string, event: E, metadata?: Metadata): Promise<ViewEffect<Row>>;
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 the entity `key` of `entity`, round-tripped through that source's codec. */
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
  /**
@@ -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
- /** A change from source instance `key`: the event, round-tripped through the source's event codec. */
221
- onChange(key, event, metadata = {}) {
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 the entity `key` of `entity`, round-tripped through that source's codec. */
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 });
@@ -1,15 +1,29 @@
1
1
  import { ComponentClient } from "../client.ts";
2
2
  import { type Principal } from "../context.ts";
3
- import type { ErrorDetail, Metadata, Retention } from "../effects/common.ts";
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): EventSourcedTestKit<S, E, C>;
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
  }