ankka 0.11.0 → 0.13.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 (92) hide show
  1. package/README.md +1 -0
  2. package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +910 -0
  3. package/dist/_proto/ankka/protocol/v1/client_pb.js +239 -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 +177 -0
  7. package/dist/_proto/ankka/protocol/v1/discovery_pb.js +51 -36
  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/mcp_pb.d.ts +120 -0
  13. package/dist/_proto/ankka/protocol/v1/mcp_pb.js +29 -0
  14. package/dist/_proto/ankka/protocol/v1/payload_pb.d.ts +61 -0
  15. package/dist/_proto/ankka/protocol/v1/payload_pb.js +18 -1
  16. package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +7 -1
  17. package/dist/_proto/ankka/protocol/v1/view_pb.js +1 -1
  18. package/dist/_proto/ankka/protocol/v1/wasm_pb.d.ts +12 -0
  19. package/dist/_proto/ankka/protocol/v1/wasm_pb.js +1 -1
  20. package/dist/agent.d.ts +10 -0
  21. package/dist/agent.js +20 -0
  22. package/dist/autonomous.d.ts +5 -0
  23. package/dist/autonomous.js +10 -0
  24. package/dist/client-keys.d.ts +89 -0
  25. package/dist/client-keys.js +182 -0
  26. package/dist/client.d.ts +96 -3
  27. package/dist/client.js +228 -6
  28. package/dist/consumer.d.ts +23 -1
  29. package/dist/consumer.js +25 -1
  30. package/dist/context.d.ts +25 -1
  31. package/dist/context.js +5 -2
  32. package/dist/contract.d.ts +4 -0
  33. package/dist/contract.js +2 -0
  34. package/dist/effects/common.d.ts +7 -0
  35. package/dist/effects/common.js +9 -1
  36. package/dist/effects/eventSourced.d.ts +26 -1
  37. package/dist/effects/eventSourced.js +58 -5
  38. package/dist/endpoint.d.ts +10 -0
  39. package/dist/endpoint.js +20 -0
  40. package/dist/eventSourcedEntity.d.ts +8 -3
  41. package/dist/graph.d.ts +14 -1
  42. package/dist/graph.js +17 -1
  43. package/dist/index.d.ts +10 -4
  44. package/dist/index.js +8 -2
  45. package/dist/keyedView.d.ts +4 -1
  46. package/dist/keyedView.js +7 -1
  47. package/dist/kinds.js +2 -0
  48. package/dist/mcp-endpoint.d.ts +144 -0
  49. package/dist/mcp-endpoint.js +256 -0
  50. package/dist/routes.d.ts +46 -3
  51. package/dist/routes.js +56 -3
  52. package/dist/server/discovery.d.ts +12 -0
  53. package/dist/server/discovery.js +115 -4
  54. package/dist/server/eventSourced.d.ts +4 -1
  55. package/dist/server/eventSourced.js +24 -6
  56. package/dist/server/http.d.ts +8 -0
  57. package/dist/server/http.js +12 -4
  58. package/dist/server/mcp.d.ts +17 -0
  59. package/dist/server/mcp.js +157 -0
  60. package/dist/server/server.d.ts +11 -1
  61. package/dist/server/server.js +19 -0
  62. package/dist/server/stateless.js +5 -4
  63. package/dist/server/storageEndpoint.d.ts +14 -0
  64. package/dist/server/storageEndpoint.js +82 -0
  65. package/dist/service.d.ts +12 -1
  66. package/dist/service.js +73 -5
  67. package/dist/sigv4.d.ts +47 -0
  68. package/dist/sigv4.js +149 -0
  69. package/dist/spec.d.ts +8 -2
  70. package/dist/spec.js +62 -6
  71. package/dist/standing.d.ts +18 -0
  72. package/dist/standing.js +26 -0
  73. package/dist/startFrom.d.ts +4 -0
  74. package/dist/startFrom.js +8 -1
  75. package/dist/storage.d.ts +288 -0
  76. package/dist/storage.js +1081 -0
  77. package/dist/testkit/index.d.ts +1 -1
  78. package/dist/testkit/integration.d.ts +9 -0
  79. package/dist/testkit/integration.js +5 -1
  80. package/dist/testkit/kinds.d.ts +25 -9
  81. package/dist/testkit/kinds.js +53 -28
  82. package/dist/testkit/unit.d.ts +31 -3
  83. package/dist/testkit/unit.js +55 -11
  84. package/dist/timedAction.d.ts +10 -0
  85. package/dist/timedAction.js +20 -0
  86. package/dist/version.d.ts +1 -1
  87. package/dist/version.js +1 -1
  88. package/dist/view.d.ts +18 -3
  89. package/dist/view.js +13 -4
  90. package/dist/workflow.d.ts +8 -0
  91. package/dist/workflow.js +15 -0
  92. package/package.json +1 -1
@@ -3,8 +3,10 @@ import type { Contract, Publication } from "./contract.ts";
3
3
  import type { Shape } from "./codec.ts";
4
4
  import type { Metadata } from "./effects/common.ts";
5
5
  import { type ComponentClient, type ComponentRef, type Secrets } from "./client.ts";
6
+ import { type Buckets } from "./storage.ts";
6
7
  import { ConsumerEffects, type ConsumerEffect } from "./effects/stateless.ts";
7
8
  import { type Services } from "./services.ts";
9
+ import type { Standing } from "./standing.ts";
8
10
  type MaybePromise<T> = T | Promise<T>;
9
11
  /** The metadata entry a runtime sets on a consumer's request to say what it speaks: its protocol version. */
10
12
  export declare const PROTOCOL_KEY = "ankka.protocol";
@@ -26,13 +28,23 @@ export declare abstract class Consumer<M, Out = never> {
26
28
  /** The message's metadata: `ce-subject` is the source instance's id, `ankka.sequence` its sequence number. */
27
29
  get metadata(): Metadata;
28
30
  get subject(): string;
31
+ /** 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. */
32
+ get standing(): Standing | undefined;
29
33
  /** The change's sequence number: an event's, or a key value state's revision. `0n` for a topic's message. */
30
34
  get sequenceNumber(): bigint | undefined;
35
+ /** Of a replicated entity's change (1.19): the installation that wrote it. `undefined` otherwise. */
36
+ get installation(): string | undefined;
37
+ /** Whether the change was written at this installation: `false` only for a replicated entity's mirrored change. */
38
+ get localOrigin(): boolean;
31
39
  get client(): ComponentClient;
32
40
  /** The service's secret store: values kept encrypted in the service's own database, never in a journal or a view. */
33
41
  get secrets(): Secrets;
34
42
  /** A unit test's store in place of the runtime's: `component.secrets = new InMemorySecrets()`. */
35
43
  set secrets(store: Secrets);
44
+ /** The service's buckets: `this.storage.default`, or `this.storage.bucket("photos")`. */
45
+ get storage(): Buckets;
46
+ /** A unit test's store in place of the runtime's: `component.storage = new InMemoryBuckets()`. */
47
+ set storage(buckets: Buckets);
36
48
  /** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
37
49
  get services(): Services;
38
50
  /** A unit test's in place of the runtime's: `component.services = new ScriptedServices()`. */
@@ -43,7 +55,7 @@ export declare abstract class Consumer<M, Out = never> {
43
55
  /** The source was deleted. Ignored unless this says otherwise. */
44
56
  onDelete(): MaybePromise<ConsumerEffect<Out>>;
45
57
  /** @internal */
46
- _bind(metadata: Metadata, client: ComponentClient): void;
58
+ _bind(metadata: Metadata, client: ComponentClient, standing?: Standing): void;
47
59
  }
48
60
  export interface ConsumerClass<M = unknown, Out = unknown, C extends Consumer<M, Out> = Consumer<M, Out>> {
49
61
  new (): C;
@@ -58,6 +70,11 @@ export interface ConsumerClass<M = unknown, Out = unknown, C extends Consumer<M,
58
70
  readonly contract?: Contract;
59
71
  /** The declared broker the topic read is on; absent is the installation's. */
60
72
  readonly broker?: string;
73
+ /**
74
+ * Another project's topic (1.15): the id of the project whose topic `topic` is. The broker serves it
75
+ * while that project grants this service consume on it.
76
+ */
77
+ readonly project?: string;
61
78
  /** Whether the partitions an instance holds are handled at once, each in order. */
62
79
  readonly parallel?: boolean;
63
80
  readonly message: Shape<M>;
@@ -65,5 +82,10 @@ export interface ConsumerClass<M = unknown, Out = unknown, C extends Consumer<M,
65
82
  readonly out?: Shape<Out>;
66
83
  /** The topic produced to: its name, or a `Publication` with the contract stated for it and its broker. */
67
84
  readonly producesTo?: string | Publication;
85
+ /**
86
+ * A consumer that publishes is given a replicated entity's changes written at other installations
87
+ * too only when this says so (1.19); otherwise each is published once, where it was written.
88
+ */
89
+ readonly mirroredChanges?: boolean;
68
90
  }
69
91
  export {};
package/dist/consumer.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // A consumer: reacts to a source's changes, optionally producing onward to a topic.
2
2
  import { secretsFor } from "./client.js";
3
+ import { processStorage } from "./storage.js";
3
4
  import { ConsumerEffects } from "./effects/stateless.js";
4
5
  import { servicesFor } from "./services.js";
5
6
  /** The metadata entry a runtime sets on a consumer's request to say what it speaks: its protocol version. */
@@ -31,6 +32,7 @@ export function sequenceNumberOf(metadata) {
31
32
  export class Consumer {
32
33
  effects = new ConsumerEffects();
33
34
  #metadata = {};
35
+ #standing = undefined;
34
36
  #client;
35
37
  /** The message's metadata: `ce-subject` is the source instance's id, `ankka.sequence` its sequence number. */
36
38
  get metadata() {
@@ -39,10 +41,22 @@ export class Consumer {
39
41
  get subject() {
40
42
  return this.#metadata["ce-subject"] ?? "";
41
43
  }
44
+ /** 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. */
45
+ get standing() {
46
+ return this.#standing;
47
+ }
42
48
  /** The change's sequence number: an event's, or a key value state's revision. `0n` for a topic's message. */
43
49
  get sequenceNumber() {
44
50
  return sequenceNumberOf(this.#metadata);
45
51
  }
52
+ /** Of a replicated entity's change (1.19): the installation that wrote it. `undefined` otherwise. */
53
+ get installation() {
54
+ return this.#metadata["ankka.installation"];
55
+ }
56
+ /** Whether the change was written at this installation: `false` only for a replicated entity's mirrored change. */
57
+ get localOrigin() {
58
+ return this.#metadata["ankka.local"] !== "false";
59
+ }
46
60
  get client() {
47
61
  if (!this.#client)
48
62
  throw new Error("client is only available inside onMessage or onDelete");
@@ -57,6 +71,15 @@ export class Consumer {
57
71
  set secrets(store) {
58
72
  this.#secrets = store;
59
73
  }
74
+ #storage;
75
+ /** The service's buckets: `this.storage.default`, or `this.storage.bucket("photos")`. */
76
+ get storage() {
77
+ return this.#storage ?? processStorage();
78
+ }
79
+ /** A unit test's store in place of the runtime's: `component.storage = new InMemoryBuckets()`. */
80
+ set storage(buckets) {
81
+ this.#storage = buckets;
82
+ }
60
83
  #services;
61
84
  /** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
62
85
  get services() {
@@ -75,8 +98,9 @@ export class Consumer {
75
98
  return this.effects.ignore();
76
99
  }
77
100
  /** @internal */
78
- _bind(metadata, client) {
101
+ _bind(metadata, client, standing) {
79
102
  this.#metadata = metadata;
103
+ this.#standing = standing;
80
104
  this.#client = client;
81
105
  }
82
106
  }
package/dist/context.d.ts CHANGED
@@ -10,8 +10,26 @@ export interface CommandContext {
10
10
  readonly metadata: Metadata;
11
11
  /** The current instant; a seam a test can replace. */
12
12
  now(): Instant;
13
+ /** The installation handling the command, for a replicated entity (1.19); `null` when none is named. */
14
+ readonly installation: string | null;
15
+ /**
16
+ * Whether this installation is the replicated entity's home. A command that may persist runs only at
17
+ * the home, so this is `false` only for a query a follower answers; `true` for an entity not replicated.
18
+ */
19
+ readonly isHome: boolean;
13
20
  }
14
- export declare function commandContext(componentId: string, entityId: string, sequenceNumber: bigint, metadata: Metadata, now?: () => Instant): CommandContext;
21
+ export declare function commandContext(componentId: string, entityId: string, sequenceNumber: bigint, metadata: Metadata, now?: () => Instant, installation?: string | null, isHome?: boolean): CommandContext;
22
+ /**
23
+ * Where the event `applyEvent` is applying was written (1.19): the installation (`null` when none is
24
+ * named; an event from before replication reads as written at the home), whether that was this
25
+ * installation, and its sequence number. The third argument of `applyEvent`.
26
+ */
27
+ export interface EventContext {
28
+ readonly installation: string | null;
29
+ readonly isLocal: boolean;
30
+ readonly sequence: bigint;
31
+ }
32
+ export declare function eventContext(installation: string | null, isLocal: boolean, sequence: bigint): EventContext;
15
33
  export declare function metadataFromProto(proto: ProtoMetadata | undefined): Metadata;
16
34
  export declare function metadataToProto(metadata: Metadata | undefined): {
17
35
  entries: {
@@ -41,6 +59,12 @@ export type Caller = {
41
59
  readonly kind: "service";
42
60
  readonly project: string;
43
61
  readonly name: string;
62
+ }
63
+ /** A machine registered on an organization, proven by a token the control plane issued (protocol 1.15). */
64
+ | {
65
+ readonly kind: "machine";
66
+ readonly organization: string;
67
+ readonly name: string;
44
68
  } | {
45
69
  readonly kind: "local";
46
70
  };
package/dist/context.js CHANGED
@@ -4,8 +4,11 @@
4
4
  // request per asynchronous context, and work handed to another context cannot see it.
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { Instant } from "./time.js";
7
- export function commandContext(componentId, entityId, sequenceNumber, metadata, now = Instant.now) {
8
- return Object.freeze({ componentId, entityId, sequenceNumber, metadata, now });
7
+ export function commandContext(componentId, entityId, sequenceNumber, metadata, now = Instant.now, installation = null, isHome = true) {
8
+ return Object.freeze({ componentId, entityId, sequenceNumber, metadata, now, installation, isHome });
9
+ }
10
+ export function eventContext(installation, isLocal, sequence) {
11
+ return Object.freeze({ installation, isLocal, sequence });
9
12
  }
10
13
  export function metadataFromProto(proto) {
11
14
  const out = {};
@@ -31,6 +31,10 @@ export interface Publication {
31
31
  readonly topic: string;
32
32
  readonly contract?: Contract;
33
33
  readonly broker?: string;
34
+ /** Another project's topic (1.15), which that project must grant this service produce on. */
35
+ readonly project?: string;
34
36
  }
37
+ /** The first protocol in which a process can read or publish to another project's topic. */
38
+ export declare const GRANTS_PROTOCOL: readonly [number, number];
35
39
  /** The first protocol in which a process can declare a contract, a broker or parallel reading. */
36
40
  export declare const CONTRACT_PROTOCOL: readonly [number, number];
package/dist/contract.js CHANGED
@@ -54,5 +54,7 @@ export function fingerprintOf(document) {
54
54
  throw new TypeError("a schema document must be JSON");
55
55
  return "sha256:" + createHash("sha256").update(canonical, "utf8").digest("hex");
56
56
  }
57
+ /** The first protocol in which a process can read or publish to another project's topic. */
58
+ export const GRANTS_PROTOCOL = [1, 15];
57
59
  /** The first protocol in which a process can declare a contract, a broker or parallel reading. */
58
60
  export const CONTRACT_PROTOCOL = [1, 14];
@@ -9,6 +9,10 @@ export declare const ErrorCode: Readonly<{
9
9
  readonly Conflict: "CONFLICT";
10
10
  readonly Timeout: "TIMEOUT";
11
11
  readonly Unavailable: "UNAVAILABLE";
12
+ /** A workflow a caller waited for failed or was deleted (protocol 1.18); the step and the reason are in `details`. */
13
+ readonly WorkflowFailed: "WORKFLOW_FAILED";
14
+ /** A replicated entity's copy here cannot answer (1.19): its home is unreachable, or the read is outside the replication set. */
15
+ readonly NotHome: "NOT_HOME";
12
16
  }>;
13
17
  export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
14
18
  export declare function httpStatusOf(code: ErrorCode): number;
@@ -16,10 +20,13 @@ export declare function httpStatusOf(code: ErrorCode): number;
16
20
  export interface ErrorDetail {
17
21
  readonly message: string;
18
22
  readonly code: ErrorCode;
23
+ /** What a caller reads without parsing the message: a failed workflow's `step`, `reason` and `deleted`. */
24
+ readonly details?: Readonly<Record<string, string>>;
19
25
  }
20
26
  /** Thrown by the component client when the callee refused. */
21
27
  export declare class CommandError extends Error {
22
28
  readonly code: ErrorCode;
29
+ readonly details: Readonly<Record<string, string>>;
23
30
  constructor(detail: ErrorDetail);
24
31
  get detail(): ErrorDetail;
25
32
  }
@@ -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,4 +1,6 @@
1
+ import { type ClientKeys } from "./client-keys.ts";
1
2
  import { type ComponentClient, type Secrets } from "./client.ts";
3
+ import { type Buckets } from "./storage.ts";
2
4
  import { type RequestContext } from "./context.ts";
3
5
  import type { Acl, RouteTable } from "./routes.ts";
4
6
  import { type Services } from "./services.ts";
@@ -12,10 +14,18 @@ export declare abstract class Endpoint {
12
14
  get secrets(): Secrets;
13
15
  /** A unit test's store in place of the runtime's: `component.secrets = new InMemorySecrets()`. */
14
16
  set secrets(store: Secrets);
17
+ /** The service's buckets: `this.storage.default`, or `this.storage.bucket("photos")`. */
18
+ get storage(): Buckets;
19
+ /** A unit test's store in place of the runtime's: `component.storage = new InMemoryBuckets()`. */
20
+ set storage(buckets: Buckets);
15
21
  /** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
16
22
  get services(): Services;
17
23
  /** A unit test's in place of the runtime's: `component.services = new ScriptedServices()`. */
18
24
  set services(services: Services);
25
+ /** The service's client keys (1.20): issued for its MCP endpoint, kept by the runtime, each shown once. */
26
+ get clientKeys(): ClientKeys;
27
+ /** A unit test's in place of the runtime's: `component.clientKeys = new InMemoryClientKeys()`. */
28
+ set clientKeys(keys: ClientKeys);
19
29
  /** @internal */
20
30
  _bindClient(client: ComponentClient | undefined): void;
21
31
  }
package/dist/endpoint.js CHANGED
@@ -8,7 +8,9 @@
8
8
  // static readonly routes = { getCart: get("/{cartId}", ShoppingCart, (ep: ShoppingCartEndpoint, req) => ep.getCart(req.params.cartId)) }
9
9
  // getCart(cartId: string) { return this.client.of(ShoppingCartEntity, cartId).call(ShoppingCartEntity.handlers.getCart).invoke() }
10
10
  // }
11
+ import { clientKeysFor } from "./client-keys.js";
11
12
  import { secretsFor } from "./client.js";
13
+ import { processStorage } from "./storage.js";
12
14
  import { currentRequest, requestIfAny } from "./context.js";
13
15
  import { servicesFor } from "./services.js";
14
16
  export class Endpoint {
@@ -33,6 +35,15 @@ export class Endpoint {
33
35
  set secrets(store) {
34
36
  this.#secrets = store;
35
37
  }
38
+ #storage;
39
+ /** The service's buckets: `this.storage.default`, or `this.storage.bucket("photos")`. */
40
+ get storage() {
41
+ return this.#storage ?? processStorage();
42
+ }
43
+ /** A unit test's store in place of the runtime's: `component.storage = new InMemoryBuckets()`. */
44
+ set storage(buckets) {
45
+ this.#storage = buckets;
46
+ }
36
47
  #services;
37
48
  /** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
38
49
  get services() {
@@ -42,6 +53,15 @@ export class Endpoint {
42
53
  set services(services) {
43
54
  this.#services = services;
44
55
  }
56
+ #clientKeys;
57
+ /** The service's client keys (1.20): issued for its MCP endpoint, kept by the runtime, each shown once. */
58
+ get clientKeys() {
59
+ return this.#clientKeys ?? clientKeysFor(this.client);
60
+ }
61
+ /** A unit test's in place of the runtime's: `component.clientKeys = new InMemoryClientKeys()`. */
62
+ set clientKeys(keys) {
63
+ this.#clientKeys = keys;
64
+ }
45
65
  /** @internal */
46
66
  _bindClient(client) {
47
67
  this.#client = client;
@@ -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
@@ -2,7 +2,9 @@ import type { StartFrom } from "./startFrom.ts";
2
2
  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
+ import { type Buckets } from "./storage.ts";
5
6
  import { type Services } from "./services.ts";
7
+ import type { Standing } from "./standing.ts";
6
8
  type MaybePromise<T> = T | Promise<T>;
7
9
  /** The contract's name: the manifest and the `ce-type` of every delta. */
8
10
  export declare const GRAPH_DELTA_SCHEMA = "ankka.graph-delta.v1";
@@ -136,6 +138,8 @@ export declare abstract class GraphConsumer<M> {
136
138
  /** The change's metadata: `ce-subject` is the source instance's id, `ankka.sequence` its sequence number. */
137
139
  get metadata(): Metadata;
138
140
  get subject(): string;
141
+ /** 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. */
142
+ get standing(): Standing | undefined;
139
143
  /** The change's sequence number, which is the version of every element that states none. */
140
144
  get sequenceNumber(): bigint | undefined;
141
145
  get client(): ComponentClient;
@@ -143,6 +147,10 @@ export declare abstract class GraphConsumer<M> {
143
147
  get secrets(): Secrets;
144
148
  /** A unit test's store in place of the runtime's: `component.secrets = new InMemorySecrets()`. */
145
149
  set secrets(store: Secrets);
150
+ /** The service's buckets: `this.storage.default`, or `this.storage.bucket("photos")`. */
151
+ get storage(): Buckets;
152
+ /** A unit test's store in place of the runtime's: `component.storage = new InMemoryBuckets()`. */
153
+ set storage(buckets: Buckets);
146
154
  /** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
147
155
  get services(): Services;
148
156
  /** A unit test's in place of the runtime's: `component.services = new ScriptedServices()`. */
@@ -153,7 +161,7 @@ export declare abstract class GraphConsumer<M> {
153
161
  /** The source was deleted. Ignored unless this says otherwise; publish tombstones here. */
154
162
  onDelete(): MaybePromise<GraphEffect>;
155
163
  /** @internal */
156
- _bind(metadata: Metadata, client: ComponentClient): void;
164
+ _bind(metadata: Metadata, client: ComponentClient, standing?: Standing): void;
157
165
  }
158
166
  export interface GraphConsumerClass<M = unknown, C extends GraphConsumer<M> = GraphConsumer<M>> {
159
167
  new (): C;
@@ -167,5 +175,10 @@ export interface GraphConsumerClass<M = unknown, C extends GraphConsumer<M> = Gr
167
175
  readonly message: Shape<M>;
168
176
  /** The delta topic. It must be compacted to hold the graph; the pipeline that reads it creates it so. */
169
177
  readonly producesTo: string;
178
+ /**
179
+ * Given a replicated entity's changes written at other installations too (1.19); otherwise each delta
180
+ * is published once, from the installation that wrote the change.
181
+ */
182
+ readonly mirroredChanges?: boolean;
170
183
  }
171
184
  export {};
package/dist/graph.js CHANGED
@@ -16,6 +16,7 @@
16
16
  // }
17
17
  import { JSON_CONTENT } from "./codec.js";
18
18
  import { secretsFor } from "./client.js";
19
+ import { processStorage } from "./storage.js";
19
20
  import { sequenceNumberOf } from "./consumer.js";
20
21
  import { renderDouble, reviver } from "./json.js";
21
22
  import { servicesFor } from "./services.js";
@@ -395,6 +396,7 @@ export class GraphConsumer {
395
396
  graph = new Graph();
396
397
  effects = new GraphEffects();
397
398
  #metadata = {};
399
+ #standing = undefined;
398
400
  #client;
399
401
  /** The change's metadata: `ce-subject` is the source instance's id, `ankka.sequence` its sequence number. */
400
402
  get metadata() {
@@ -403,6 +405,10 @@ export class GraphConsumer {
403
405
  get subject() {
404
406
  return this.#metadata["ce-subject"] ?? "";
405
407
  }
408
+ /** 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. */
409
+ get standing() {
410
+ return this.#standing;
411
+ }
406
412
  /** The change's sequence number, which is the version of every element that states none. */
407
413
  get sequenceNumber() {
408
414
  return sequenceNumberOf(this.#metadata);
@@ -421,6 +427,15 @@ export class GraphConsumer {
421
427
  set secrets(store) {
422
428
  this.#secrets = store;
423
429
  }
430
+ #storage;
431
+ /** The service's buckets: `this.storage.default`, or `this.storage.bucket("photos")`. */
432
+ get storage() {
433
+ return this.#storage ?? processStorage();
434
+ }
435
+ /** A unit test's store in place of the runtime's: `component.storage = new InMemoryBuckets()`. */
436
+ set storage(buckets) {
437
+ this.#storage = buckets;
438
+ }
424
439
  #services;
425
440
  /** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
426
441
  get services() {
@@ -439,8 +454,9 @@ export class GraphConsumer {
439
454
  return this.effects.ignore();
440
455
  }
441
456
  /** @internal */
442
- _bind(metadata, client) {
457
+ _bind(metadata, client, standing) {
443
458
  this.#metadata = metadata;
459
+ this.#standing = standing;
444
460
  this.#client = client;
445
461
  }
446
462
  }