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
@@ -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
- constructor(events, retention = null) {
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 PersistBuilder(this.#events, Retention.deleteNow);
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 PersistBuilder(this.#events, Retention.expireAfter(after));
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 Object.freeze({ kind: "persist", events: this.#events, retention: this.#retention, outcome: Outcome.reply(compute, metadata) });
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 Object.freeze({ kind: "persist", events: this.#events, retention: this.#retention, outcome: Outcome.noReply() });
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
- /** The fold: the state after `event`, given the state before it. Must not read `this.state`. */
23
- abstract applyEvent(state: S, event: E): S;
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";
@@ -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>): void;
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;
@@ -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 { olderThanContracts, olderThanDeclaredQueries, olderThanStartPositions } from "../startFrom.js";
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 startPositionRefusal(spec, sidecarProtocol) ?? declaredQueryRefusal(spec, sidecarProtocol) ?? contractRefusal(spec, sidecarProtocol);
17
+ return (workflowSourceRefusal(spec, sidecarProtocol) ??
18
+ startPositionRefusal(spec, sidecarProtocol) ??
19
+ declaredQueryRefusal(spec, sidecarProtocol) ??
20
+ contractRefusal(spec, sidecarProtocol) ??
21
+ watchedQueryRefusal(spec, sidecarProtocol) ??
22
+ // Last: a reason an older sidecar has too is named first, by the version it needs.
23
+ replicationRefusal(spec, sidecarProtocol));
24
+ }
25
+ /**
26
+ * A sidecar older than 1.19 would host a replicated entity at one installation and say nothing, and
27
+ * would hand a consumer given mirrored changes only the changes written here. Refused at discovery,
28
+ * naming the components and the version.
29
+ */
30
+ function replicationRefusal(spec, sidecarProtocol) {
31
+ if (!olderThan(sidecarProtocol, [1, 19]))
32
+ return undefined;
33
+ const replicated = spec.components
34
+ .filter((c) => (c.detail.case === "eventSourced" && c.detail.value.replicated) ||
35
+ (c.detail.case === "consumer" && c.detail.value.mirroredChanges))
36
+ .map((c) => c.id);
37
+ if (replicated.length === 0)
38
+ return undefined;
39
+ return (`${replicated.join(", ")} declare replication, which needs protocol 1.19: ` +
40
+ `the sidecar speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.19 or later.`);
41
+ }
42
+ /**
43
+ * A sidecar older than 1.16 would not read a workflow as a source: a view or consumer that does would
44
+ * be handed nothing. Refused at discovery, naming them.
45
+ */
46
+ function workflowSourceRefusal(spec, sidecarProtocol) {
47
+ if (!olderThanWorkflowSources(sidecarProtocol))
48
+ return undefined;
49
+ const reading = spec.components
50
+ .filter((c) => {
51
+ const d = c.detail;
52
+ if (d.case !== "view" && d.case !== "consumer")
53
+ return false;
54
+ const sources = d.case === "view" ? [d.value.source, ...d.value.sources] : [d.value.source];
55
+ return sources.some((s) => s?.source.case === "component" && s.source.value.kind === Kind.WORKFLOW);
56
+ })
57
+ .map((c) => c.id);
58
+ if (reading.length === 0)
59
+ return undefined;
60
+ return (`${reading.join(", ")} read a workflow, which the sidecar does not know: ` +
61
+ `it speaks protocol ${sidecarProtocol}, and this SDK ${PROTOCOL_VERSION}. Run a sidecar speaking 1.16 or later.`);
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) ?? socketRefusal(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;