ankka 0.6.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 (108) hide show
  1. package/README.md +30 -0
  2. package/dist/_proto/ankka/protocol/v1/agent_pb.d.ts +350 -0
  3. package/dist/_proto/ankka/protocol/v1/agent_pb.js +90 -0
  4. package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +254 -0
  5. package/dist/_proto/ankka/protocol/v1/client_pb.js +47 -0
  6. package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +105 -0
  7. package/dist/_proto/ankka/protocol/v1/consumer_pb.js +28 -0
  8. package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +598 -0
  9. package/dist/_proto/ankka/protocol/v1/discovery_pb.js +175 -0
  10. package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +224 -0
  11. package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +44 -0
  12. package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +212 -0
  13. package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +47 -0
  14. package/dist/_proto/ankka/protocol/v1/key_value_pb.d.ts +154 -0
  15. package/dist/_proto/ankka/protocol/v1/key_value_pb.js +36 -0
  16. package/dist/_proto/ankka/protocol/v1/payload_pb.d.ts +276 -0
  17. package/dist/_proto/ankka/protocol/v1/payload_pb.js +110 -0
  18. package/dist/_proto/ankka/protocol/v1/timed_action_pb.d.ts +75 -0
  19. package/dist/_proto/ankka/protocol/v1/timed_action_pb.js +23 -0
  20. package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +93 -0
  21. package/dist/_proto/ankka/protocol/v1/view_pb.js +23 -0
  22. package/dist/_proto/ankka/protocol/v1/workflow_pb.d.ts +298 -0
  23. package/dist/_proto/ankka/protocol/v1/workflow_pb.js +67 -0
  24. package/dist/agent.d.ts +27 -0
  25. package/dist/agent.js +36 -0
  26. package/dist/client.d.ts +92 -0
  27. package/dist/client.js +239 -0
  28. package/dist/codec.d.ts +44 -0
  29. package/dist/codec.js +215 -0
  30. package/dist/consumer.d.ts +31 -0
  31. package/dist/consumer.js +32 -0
  32. package/dist/context.d.ts +61 -0
  33. package/dist/context.js +75 -0
  34. package/dist/effects/agent.d.ts +37 -0
  35. package/dist/effects/agent.js +48 -0
  36. package/dist/effects/common.d.ts +68 -0
  37. package/dist/effects/common.js +55 -0
  38. package/dist/effects/eventSourced.d.ts +50 -0
  39. package/dist/effects/eventSourced.js +66 -0
  40. package/dist/effects/keyValue.d.ts +36 -0
  41. package/dist/effects/keyValue.js +47 -0
  42. package/dist/effects/stateless.d.ts +40 -0
  43. package/dist/effects/stateless.js +35 -0
  44. package/dist/effects/workflow.d.ts +99 -0
  45. package/dist/effects/workflow.js +83 -0
  46. package/dist/endpoint.d.ts +21 -0
  47. package/dist/endpoint.js +29 -0
  48. package/dist/eventSourcedEntity.d.ts +40 -0
  49. package/dist/eventSourcedEntity.js +66 -0
  50. package/dist/handlers.d.ts +55 -0
  51. package/dist/handlers.js +64 -0
  52. package/dist/index.d.ts +29 -0
  53. package/dist/index.js +39 -0
  54. package/dist/json.d.ts +21 -0
  55. package/dist/json.js +362 -0
  56. package/dist/keyValueEntity.d.ts +29 -0
  57. package/dist/keyValueEntity.js +52 -0
  58. package/dist/kinds.d.ts +8 -0
  59. package/dist/kinds.js +41 -0
  60. package/dist/materialise.d.ts +33 -0
  61. package/dist/materialise.js +55 -0
  62. package/dist/routes.d.ts +71 -0
  63. package/dist/routes.js +79 -0
  64. package/dist/schema.d.ts +112 -0
  65. package/dist/schema.js +200 -0
  66. package/dist/server/agent.d.ts +14 -0
  67. package/dist/server/agent.js +113 -0
  68. package/dist/server/discovery.d.ts +5 -0
  69. package/dist/server/discovery.js +21 -0
  70. package/dist/server/eventSourced.d.ts +12 -0
  71. package/dist/server/eventSourced.js +168 -0
  72. package/dist/server/http.d.ts +10 -0
  73. package/dist/server/http.js +153 -0
  74. package/dist/server/keyValue.d.ts +5 -0
  75. package/dist/server/keyValue.js +122 -0
  76. package/dist/server/payloads.d.ts +6 -0
  77. package/dist/server/payloads.js +11 -0
  78. package/dist/server/queue.d.ts +10 -0
  79. package/dist/server/queue.js +67 -0
  80. package/dist/server/server.d.ts +34 -0
  81. package/dist/server/server.js +101 -0
  82. package/dist/server/stateless.d.ts +9 -0
  83. package/dist/server/stateless.js +108 -0
  84. package/dist/server/workflow.d.ts +5 -0
  85. package/dist/server/workflow.js +244 -0
  86. package/dist/service.d.ts +147 -0
  87. package/dist/service.js +384 -0
  88. package/dist/spec.d.ts +7 -0
  89. package/dist/spec.js +106 -0
  90. package/dist/testkit/index.d.ts +3 -0
  91. package/dist/testkit/index.js +6 -0
  92. package/dist/testkit/integration.d.ts +60 -0
  93. package/dist/testkit/integration.js +284 -0
  94. package/dist/testkit/kinds.d.ts +133 -0
  95. package/dist/testkit/kinds.js +398 -0
  96. package/dist/testkit/unit.d.ts +54 -0
  97. package/dist/testkit/unit.js +224 -0
  98. package/dist/time.d.ts +73 -0
  99. package/dist/time.js +275 -0
  100. package/dist/timedAction.d.ts +20 -0
  101. package/dist/timedAction.js +28 -0
  102. package/dist/version.d.ts +1 -0
  103. package/dist/version.js +2 -0
  104. package/dist/view.d.ts +37 -0
  105. package/dist/view.js +52 -0
  106. package/dist/workflow.d.ts +33 -0
  107. package/dist/workflow.js +61 -0
  108. package/package.json +75 -0
@@ -0,0 +1,61 @@
1
+ import { Instant } from "./time.ts";
2
+ import type { Metadata } from "./effects/common.ts";
3
+ import type { Metadata as ProtoMetadata } from "./_proto/ankka/protocol/v1/payload_pb.ts";
4
+ /** The context of one command on a stateful component. */
5
+ export interface CommandContext {
6
+ readonly componentId: string;
7
+ readonly entityId: string;
8
+ /** The sequence number of the last event applied before this command; `0n` for a fresh entity. */
9
+ readonly sequenceNumber: bigint;
10
+ readonly metadata: Metadata;
11
+ /** The current instant; a seam a test can replace. */
12
+ now(): Instant;
13
+ }
14
+ export declare function commandContext(componentId: string, entityId: string, sequenceNumber: bigint, metadata: Metadata, now?: () => Instant): CommandContext;
15
+ export declare function metadataFromProto(proto: ProtoMetadata | undefined): Metadata;
16
+ export declare function metadataToProto(metadata: Metadata | undefined): {
17
+ entries: {
18
+ key: string;
19
+ value: string;
20
+ }[];
21
+ };
22
+ /** The caller an authenticated route was invoked by. */
23
+ export interface Principal {
24
+ readonly subject: string;
25
+ readonly name: string | null;
26
+ readonly email: string | null;
27
+ readonly emailVerified: boolean;
28
+ readonly roles: readonly string[];
29
+ }
30
+ /** Query parameters in request order; a name may repeat. */
31
+ export declare class Query {
32
+ private readonly pairs;
33
+ constructor(pairs: readonly (readonly [string, string])[]);
34
+ get(name: string): string | null;
35
+ getAll(name: string): string[];
36
+ has(name: string): boolean;
37
+ entries(): readonly (readonly [string, string])[];
38
+ }
39
+ /** Request headers; names are matched case-insensitively. */
40
+ export declare class Headers {
41
+ private readonly pairs;
42
+ constructor(pairs: readonly (readonly [string, string])[]);
43
+ get(name: string): string | null;
44
+ getAll(name: string): string[];
45
+ entries(): readonly (readonly [string, string])[];
46
+ }
47
+ /** What an endpoint handler sees of its request: `this.request` on the endpoint. `P` is the typed path parameters. */
48
+ export interface RequestContext<P = Readonly<Record<string, unknown>>> {
49
+ readonly params: P;
50
+ readonly query: Query;
51
+ readonly headers: Headers;
52
+ /** The caller, when the route's access rule is `authenticated`; `null` otherwise. */
53
+ readonly principal: Principal | null;
54
+ readonly metadata: Metadata;
55
+ }
56
+ /** Runs `fn` with `request` as the current request, for the asynchronous work it starts. */
57
+ export declare function withRequest<T>(request: RequestContext, fn: () => T): T;
58
+ /** The current request, or `undefined` outside one. */
59
+ export declare function requestIfAny(): RequestContext | undefined;
60
+ /** The current request, or a clear error when called outside one. */
61
+ export declare function currentRequest(): RequestContext;
@@ -0,0 +1,75 @@
1
+ // What a handler can see about the request it is running for. A `CommandContext` is bound on the
2
+ // component instance for one call; an HTTP `RequestContext` lives in an `AsyncLocalStorage`, the Node
3
+ // equivalent of the thread-local the Scala SDK uses and the `ContextVar` the Python SDK uses: one
4
+ // request per asynchronous context, and work handed to another context cannot see it.
5
+ import { AsyncLocalStorage } from "node:async_hooks";
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 });
9
+ }
10
+ export function metadataFromProto(proto) {
11
+ const out = {};
12
+ for (const e of proto?.entries ?? [])
13
+ out[e.key] = e.value;
14
+ return Object.freeze(out);
15
+ }
16
+ export function metadataToProto(metadata) {
17
+ return { entries: Object.entries(metadata ?? {}).map(([key, value]) => ({ key, value })) };
18
+ }
19
+ /** Query parameters in request order; a name may repeat. */
20
+ export class Query {
21
+ pairs;
22
+ constructor(pairs) {
23
+ this.pairs = pairs;
24
+ Object.freeze(this);
25
+ }
26
+ get(name) {
27
+ const hit = this.pairs.find(([n]) => n === name);
28
+ return hit ? hit[1] : null;
29
+ }
30
+ getAll(name) {
31
+ return this.pairs.filter(([n]) => n === name).map(([, v]) => v);
32
+ }
33
+ has(name) {
34
+ return this.pairs.some(([n]) => n === name);
35
+ }
36
+ entries() {
37
+ return this.pairs;
38
+ }
39
+ }
40
+ /** Request headers; names are matched case-insensitively. */
41
+ export class Headers {
42
+ pairs;
43
+ constructor(pairs) {
44
+ this.pairs = pairs;
45
+ Object.freeze(this);
46
+ }
47
+ get(name) {
48
+ const lower = name.toLowerCase();
49
+ const hit = this.pairs.find(([n]) => n.toLowerCase() === lower);
50
+ return hit ? hit[1] : null;
51
+ }
52
+ getAll(name) {
53
+ const lower = name.toLowerCase();
54
+ return this.pairs.filter(([n]) => n.toLowerCase() === lower).map(([, v]) => v);
55
+ }
56
+ entries() {
57
+ return this.pairs;
58
+ }
59
+ }
60
+ const requestStorage = new AsyncLocalStorage();
61
+ /** Runs `fn` with `request` as the current request, for the asynchronous work it starts. */
62
+ export function withRequest(request, fn) {
63
+ return requestStorage.run(request, fn);
64
+ }
65
+ /** The current request, or `undefined` outside one. */
66
+ export function requestIfAny() {
67
+ return requestStorage.getStore();
68
+ }
69
+ /** The current request, or a clear error when called outside one. */
70
+ export function currentRequest() {
71
+ const r = requestStorage.getStore();
72
+ if (!r)
73
+ throw new Error("request is only available inside an endpoint handler");
74
+ return r;
75
+ }
@@ -0,0 +1,37 @@
1
+ import { ErrorCode, type EffectLike, type ErrorDetail } from "./common.ts";
2
+ export interface AgentEffect<R = string> extends EffectLike<R> {
3
+ readonly kind: "agent";
4
+ /** A model by the name the sidecar configured; absent, the sidecar's default. */
5
+ readonly model: string | null;
6
+ readonly system: string | null;
7
+ readonly user: string | null;
8
+ readonly context: readonly string[];
9
+ readonly sessionMemory: boolean;
10
+ readonly toolNames: readonly string[];
11
+ readonly guardrailNames: readonly string[];
12
+ readonly jsonReply: boolean;
13
+ readonly schemaHint: string;
14
+ /** `effects.error(...)`: a refusal before any model call. */
15
+ readonly failure: ErrorDetail | null;
16
+ withModel(name: string): AgentEffect<R>;
17
+ systemMessage(text: string): AgentEffect<R>;
18
+ userMessage(text: string): AgentEffect<R>;
19
+ /** Context the user did not type — retrieved documents, an entity's state — kept apart from the user message so memory records what the user said. */
20
+ withContext(text: string): AgentEffect<R>;
21
+ /** `false`: no memory at all, for a one-shot classification. */
22
+ memory(session: boolean): AgentEffect<R>;
23
+ tools(...names: string[]): AgentEffect<R>;
24
+ guardrails(...names: string[]): AgentEffect<R>;
25
+ /** Reply with the model's text. */
26
+ thenReply(): AgentEffect<string>;
27
+ /** Reply with the model's JSON, decoded by the caller's reply shape `R`. The schema is not sent to the model — say what you want in the system message. */
28
+ thenReplyJson<J = unknown>(schemaHint?: string): AgentEffect<J>;
29
+ }
30
+ /** Inside an agent's handler: `this.effects`. */
31
+ export declare class AgentEffects {
32
+ model(name: string): AgentEffect<string>;
33
+ systemMessage(text: string): AgentEffect<string>;
34
+ userMessage(text: string): AgentEffect<string>;
35
+ /** Reject the request without calling a model. */
36
+ error(message: string, code?: ErrorCode): AgentEffect<never>;
37
+ }
@@ -0,0 +1,48 @@
1
+ // Effects for agents: a description of one interaction with a model — which model, what instructions,
2
+ // which tools and guardrails, what memory — as data. Building one calls no model; the sidecar's loop
3
+ // interprets it, calling back for tools and guardrails. The model's key never lives in this process.
4
+ import { ErrorCode } from "./common.js";
5
+ const EMPTY = {
6
+ model: null,
7
+ system: null,
8
+ user: null,
9
+ context: [],
10
+ sessionMemory: true,
11
+ toolNames: [],
12
+ guardrailNames: [],
13
+ jsonReply: false,
14
+ schemaHint: "",
15
+ failure: null,
16
+ };
17
+ function make(fields) {
18
+ const f = { ...fields, context: Object.freeze([...fields.context]), toolNames: Object.freeze([...fields.toolNames]), guardrailNames: Object.freeze([...fields.guardrailNames]) };
19
+ return Object.freeze({
20
+ kind: "agent",
21
+ ...f,
22
+ withModel: (name) => make({ ...f, model: name }),
23
+ systemMessage: (text) => make({ ...f, system: text }),
24
+ userMessage: (text) => make({ ...f, user: text }),
25
+ withContext: (text) => make({ ...f, context: [...f.context, text] }),
26
+ memory: (session) => make({ ...f, sessionMemory: session }),
27
+ tools: (...names) => make({ ...f, toolNames: [...f.toolNames, ...names] }),
28
+ guardrails: (...names) => make({ ...f, guardrailNames: [...f.guardrailNames, ...names] }),
29
+ thenReply: () => make({ ...f, jsonReply: false }),
30
+ thenReplyJson: (schemaHint = "JSON") => make({ ...f, jsonReply: true, schemaHint }),
31
+ });
32
+ }
33
+ /** Inside an agent's handler: `this.effects`. */
34
+ export class AgentEffects {
35
+ model(name) {
36
+ return make({ ...EMPTY, model: name });
37
+ }
38
+ systemMessage(text) {
39
+ return make({ ...EMPTY, system: text });
40
+ }
41
+ userMessage(text) {
42
+ return make({ ...EMPTY, user: text });
43
+ }
44
+ /** Reject the request without calling a model. */
45
+ error(message, code = ErrorCode.BadRequest) {
46
+ return make({ ...EMPTY, failure: { message, code } });
47
+ }
48
+ }
@@ -0,0 +1,68 @@
1
+ import type { Duration } from "../time.ts";
2
+ /** The refusal codes a handler may answer with, and the HTTP status each becomes at an endpoint. */
3
+ export declare const ErrorCode: Readonly<{
4
+ readonly Internal: "INTERNAL";
5
+ readonly BadRequest: "BAD_REQUEST";
6
+ readonly Unauthorized: "UNAUTHORIZED";
7
+ readonly Forbidden: "FORBIDDEN";
8
+ readonly NotFound: "NOT_FOUND";
9
+ readonly Conflict: "CONFLICT";
10
+ readonly Timeout: "TIMEOUT";
11
+ readonly Unavailable: "UNAVAILABLE";
12
+ }>;
13
+ export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
14
+ export declare function httpStatusOf(code: ErrorCode): number;
15
+ /** A refusal, as a value: what `effects.error(message, code)` carries and what a caller's `CommandError` holds. */
16
+ export interface ErrorDetail {
17
+ readonly message: string;
18
+ readonly code: ErrorCode;
19
+ }
20
+ /** Thrown by the component client when the callee refused. */
21
+ export declare class CommandError extends Error {
22
+ readonly code: ErrorCode;
23
+ constructor(detail: ErrorDetail);
24
+ get detail(): ErrorDetail;
25
+ }
26
+ /** Metadata on a request or a reply: trace and span ids, the source's id, a timer's name. */
27
+ export type Metadata = Readonly<Record<string, string>>;
28
+ /**
29
+ * The three cases of an effect's outcome. A `Reply` computes its value from the state *after* the
30
+ * effect's events, which is why it is a function and not a value.
31
+ */
32
+ export type Outcome<S, R> = {
33
+ readonly kind: "reply";
34
+ readonly compute: (state: S) => R;
35
+ readonly metadata?: Metadata;
36
+ } | {
37
+ readonly kind: "no-reply";
38
+ } | {
39
+ readonly kind: "fail";
40
+ readonly error: ErrorDetail;
41
+ };
42
+ export declare const Outcome: Readonly<{
43
+ reply<S, R>(compute: (state: S) => R, metadata?: Metadata): Outcome<S, R>;
44
+ noReply<S>(): Outcome<S, never>;
45
+ fail<S>(message: string, code?: ErrorCode): Outcome<S, never>;
46
+ }>;
47
+ /** What an entity asks the runtime to do with it after this effect: delete now, or expire after a while. */
48
+ export type Retention = {
49
+ readonly kind: "delete-now";
50
+ } | {
51
+ readonly kind: "expire-after";
52
+ readonly after: Duration;
53
+ };
54
+ export declare const Retention: Readonly<{
55
+ deleteNow: Retention;
56
+ expireAfter(after: Duration): Retention;
57
+ }>;
58
+ /**
59
+ * Every effect has a `kind`; read-only effects have `kind: "read-only"`, which is what lets `query`
60
+ * refuse a persisting handler at compile time. `_reply` is a phantom carrying the reply type.
61
+ */
62
+ export interface EffectLike<R = unknown> {
63
+ readonly kind: string;
64
+ readonly _reply?: R;
65
+ }
66
+ export interface ReadOnlyLike<R = unknown> extends EffectLike<R> {
67
+ readonly kind: "read-only";
68
+ }
@@ -0,0 +1,55 @@
1
+ // What every kind's effects share: the outcome of a request (a reply, no reply, or a refusal), the
2
+ // retention a handler may ask for, and the error codes. Inert values; nothing here performs I/O.
3
+ /** The refusal codes a handler may answer with, and the HTTP status each becomes at an endpoint. */
4
+ export const ErrorCode = Object.freeze({
5
+ Internal: "INTERNAL",
6
+ BadRequest: "BAD_REQUEST",
7
+ Unauthorized: "UNAUTHORIZED",
8
+ Forbidden: "FORBIDDEN",
9
+ NotFound: "NOT_FOUND",
10
+ Conflict: "CONFLICT",
11
+ Timeout: "TIMEOUT",
12
+ Unavailable: "UNAVAILABLE",
13
+ });
14
+ const HTTP_STATUS = Object.freeze({
15
+ INTERNAL: 500,
16
+ BAD_REQUEST: 400,
17
+ UNAUTHORIZED: 401,
18
+ FORBIDDEN: 403,
19
+ NOT_FOUND: 404,
20
+ CONFLICT: 409,
21
+ TIMEOUT: 504,
22
+ UNAVAILABLE: 503,
23
+ });
24
+ export function httpStatusOf(code) {
25
+ return HTTP_STATUS[code];
26
+ }
27
+ /** Thrown by the component client when the callee refused. */
28
+ export class CommandError extends Error {
29
+ code;
30
+ constructor(detail) {
31
+ super(detail.message);
32
+ this.name = "CommandError";
33
+ this.code = detail.code;
34
+ }
35
+ get detail() {
36
+ return { message: this.message, code: this.code };
37
+ }
38
+ }
39
+ export const Outcome = Object.freeze({
40
+ reply(compute, metadata) {
41
+ return Object.freeze({ kind: "reply", compute, ...(metadata ? { metadata } : {}) });
42
+ },
43
+ noReply() {
44
+ return Object.freeze({ kind: "no-reply" });
45
+ },
46
+ fail(message, code = ErrorCode.Internal) {
47
+ return Object.freeze({ kind: "fail", error: Object.freeze({ message, code }) });
48
+ },
49
+ });
50
+ export const Retention = Object.freeze({
51
+ deleteNow: Object.freeze({ kind: "delete-now" }),
52
+ expireAfter(after) {
53
+ return Object.freeze({ kind: "expire-after", after });
54
+ },
55
+ });
@@ -0,0 +1,50 @@
1
+ import type { Duration } from "../time.ts";
2
+ import { done, type Done } from "../schema.ts";
3
+ import { ErrorCode, Outcome, Retention, type EffectLike, type Metadata, type ReadOnlyLike } from "./common.ts";
4
+ export interface PersistEffect<S, E, R> extends EffectLike<R> {
5
+ readonly kind: "persist";
6
+ readonly events: readonly E[];
7
+ readonly retention: Retention | null;
8
+ readonly outcome: Outcome<S, R>;
9
+ }
10
+ export interface ReadOnlyEffect<S, E, R> extends ReadOnlyLike<R> {
11
+ readonly kind: "read-only";
12
+ readonly retention: Retention | null;
13
+ readonly outcome: Outcome<S, R>;
14
+ /** Phantom: the entity's event type, so a read-only effect is tied to its entity. */
15
+ readonly _events?: E;
16
+ }
17
+ export type EventSourcedEffect<S, E, R> = PersistEffect<S, E, R> | ReadOnlyEffect<S, E, R>;
18
+ /** `effects.persist(e)` returns one of these; finish it with `thenReply`, `thenReplyState` or `thenNoReply`. */
19
+ export declare class PersistBuilder<S, E> {
20
+ #private;
21
+ constructor(events: readonly E[], retention?: Retention | null);
22
+ /** Delete the entity once these events are persisted. */
23
+ deleteEntity(): PersistBuilder<S, E>;
24
+ /** Expire the entity a while after these events are persisted. */
25
+ expireAfter(after: Duration): PersistBuilder<S, E>;
26
+ /** Reply with a value computed from the state *after* the events. */
27
+ thenReply<R>(compute: (state: S) => R, metadata?: Metadata): PersistEffect<S, E, R>;
28
+ /** Reply with the state after the events. */
29
+ thenReplyState(): PersistEffect<S, E, S>;
30
+ /** Persist and answer nothing. */
31
+ thenNoReply(): PersistEffect<S, E, never>;
32
+ }
33
+ /** The factory a handler reaches as `this.effects`. */
34
+ export declare class EventSourcedEffects<S, E> {
35
+ /** Persist one or more events. */
36
+ persist(event: E, ...more: E[]): PersistBuilder<S, E>;
37
+ /** Persist a list of events; an empty list is allowed and persists nothing. */
38
+ persistAll(events: readonly E[]): PersistBuilder<S, E>;
39
+ /** Answer without persisting. */
40
+ reply<R>(value: R, metadata?: Metadata): ReadOnlyEffect<S, E, R>;
41
+ /** Refuse the command: nothing is persisted and the caller sees the code. */
42
+ error(message: string, code?: ErrorCode): ReadOnlyEffect<S, E, never>;
43
+ /** Answer nothing and persist nothing. */
44
+ noReply(): ReadOnlyEffect<S, E, never>;
45
+ /** Delete the entity, persisting nothing more; finish with `thenReply(() => done)` or `thenNoReply()`. */
46
+ deleteEntity(): PersistBuilder<S, E>;
47
+ /** Expire the entity after a while, persisting nothing more. */
48
+ expireAfter(after: Duration): PersistBuilder<S, E>;
49
+ }
50
+ export { done, type Done };
@@ -0,0 +1,66 @@
1
+ // The effects an event sourced entity's handlers return: persist these events and then reply from the
2
+ // state after them, or answer without persisting. Inert values. `ReadOnlyEffect` is a distinct type
3
+ // with `kind: "read-only"`, which is what lets `query` refuse a persisting handler at compile time.
4
+ import { done } from "../schema.js";
5
+ import { ErrorCode, Outcome, Retention } from "./common.js";
6
+ /** `effects.persist(e)` returns one of these; finish it with `thenReply`, `thenReplyState` or `thenNoReply`. */
7
+ export class PersistBuilder {
8
+ #events;
9
+ #retention;
10
+ constructor(events, retention = null) {
11
+ this.#events = events;
12
+ this.#retention = retention;
13
+ }
14
+ /** Delete the entity once these events are persisted. */
15
+ deleteEntity() {
16
+ return new PersistBuilder(this.#events, Retention.deleteNow);
17
+ }
18
+ /** Expire the entity a while after these events are persisted. */
19
+ expireAfter(after) {
20
+ return new PersistBuilder(this.#events, Retention.expireAfter(after));
21
+ }
22
+ /** Reply with a value computed from the state *after* the events. */
23
+ thenReply(compute, metadata) {
24
+ return Object.freeze({ kind: "persist", events: this.#events, retention: this.#retention, outcome: Outcome.reply(compute, metadata) });
25
+ }
26
+ /** Reply with the state after the events. */
27
+ thenReplyState() {
28
+ return this.thenReply((state) => state);
29
+ }
30
+ /** Persist and answer nothing. */
31
+ thenNoReply() {
32
+ return Object.freeze({ kind: "persist", events: this.#events, retention: this.#retention, outcome: Outcome.noReply() });
33
+ }
34
+ }
35
+ /** The factory a handler reaches as `this.effects`. */
36
+ export class EventSourcedEffects {
37
+ /** Persist one or more events. */
38
+ persist(event, ...more) {
39
+ return new PersistBuilder(Object.freeze([event, ...more]));
40
+ }
41
+ /** Persist a list of events; an empty list is allowed and persists nothing. */
42
+ persistAll(events) {
43
+ return new PersistBuilder(Object.freeze([...events]));
44
+ }
45
+ /** Answer without persisting. */
46
+ reply(value, metadata) {
47
+ return Object.freeze({ kind: "read-only", retention: null, outcome: Outcome.reply(() => value, metadata) });
48
+ }
49
+ /** Refuse the command: nothing is persisted and the caller sees the code. */
50
+ error(message, code = ErrorCode.BadRequest) {
51
+ return Object.freeze({ kind: "read-only", retention: null, outcome: Outcome.fail(message, code) });
52
+ }
53
+ /** Answer nothing and persist nothing. */
54
+ noReply() {
55
+ return Object.freeze({ kind: "read-only", retention: null, outcome: Outcome.noReply() });
56
+ }
57
+ /** Delete the entity, persisting nothing more; finish with `thenReply(() => done)` or `thenNoReply()`. */
58
+ deleteEntity() {
59
+ return new PersistBuilder(Object.freeze([]), Retention.deleteNow);
60
+ }
61
+ /** Expire the entity after a while, persisting nothing more. */
62
+ expireAfter(after) {
63
+ return new PersistBuilder(Object.freeze([]), Retention.expireAfter(after));
64
+ }
65
+ }
66
+ export { done };
@@ -0,0 +1,36 @@
1
+ import type { Duration } from "../time.ts";
2
+ import { done, type Done } from "../schema.ts";
3
+ import { ErrorCode, Outcome, Retention, type EffectLike, type Metadata, type ReadOnlyLike } from "./common.ts";
4
+ export interface UpdateEffect<S, R> extends EffectLike<R> {
5
+ readonly kind: "update";
6
+ /** The state to store; `null` when the effect only deletes. */
7
+ readonly newState: S | null;
8
+ readonly retention: Retention | null;
9
+ readonly outcome: Outcome<S, R>;
10
+ }
11
+ export interface KeyValueReadOnlyEffect<S, R> extends ReadOnlyLike<R> {
12
+ readonly kind: "read-only";
13
+ readonly retention: null;
14
+ readonly outcome: Outcome<S, R>;
15
+ }
16
+ export type KeyValueEffect<S, R> = UpdateEffect<S, R> | KeyValueReadOnlyEffect<S, R>;
17
+ export declare class UpdateBuilder<S> {
18
+ #private;
19
+ constructor(newState: S | null, retention?: Retention | null);
20
+ deleteEntity(): UpdateBuilder<S>;
21
+ expireAfter(after: Duration): UpdateBuilder<S>;
22
+ /** Reply with a value computed from the new state. */
23
+ thenReply<R>(compute: (state: S) => R, metadata?: Metadata): UpdateEffect<S, R>;
24
+ thenReplyState(): UpdateEffect<S, S>;
25
+ thenNoReply(): UpdateEffect<S, never>;
26
+ }
27
+ /** The factory a key value entity's handler reaches as `this.effects`. */
28
+ export declare class KeyValueEffects<S> {
29
+ updateState(state: S): UpdateBuilder<S>;
30
+ /** Delete the entity, keeping the state as it is until then. */
31
+ deleteEntity(): UpdateBuilder<S>;
32
+ reply<R>(value: R, metadata?: Metadata): KeyValueReadOnlyEffect<S, R>;
33
+ error(message: string, code?: ErrorCode): KeyValueReadOnlyEffect<S, never>;
34
+ noReply(): KeyValueReadOnlyEffect<S, never>;
35
+ }
36
+ export { done, type Done };
@@ -0,0 +1,47 @@
1
+ // Effects for key value entities: replace the state, then decide what to reply.
2
+ import { done } from "../schema.js";
3
+ import { ErrorCode, Outcome, Retention } from "./common.js";
4
+ export class UpdateBuilder {
5
+ #newState;
6
+ #retention;
7
+ constructor(newState, retention = null) {
8
+ this.#newState = newState;
9
+ this.#retention = retention;
10
+ }
11
+ deleteEntity() {
12
+ return new UpdateBuilder(this.#newState, Retention.deleteNow);
13
+ }
14
+ expireAfter(after) {
15
+ return new UpdateBuilder(this.#newState, Retention.expireAfter(after));
16
+ }
17
+ /** Reply with a value computed from the new state. */
18
+ thenReply(compute, metadata) {
19
+ return Object.freeze({ kind: "update", newState: this.#newState, retention: this.#retention, outcome: Outcome.reply(compute, metadata) });
20
+ }
21
+ thenReplyState() {
22
+ return this.thenReply((state) => state);
23
+ }
24
+ thenNoReply() {
25
+ return Object.freeze({ kind: "update", newState: this.#newState, retention: this.#retention, outcome: Outcome.noReply() });
26
+ }
27
+ }
28
+ /** The factory a key value entity's handler reaches as `this.effects`. */
29
+ export class KeyValueEffects {
30
+ updateState(state) {
31
+ return new UpdateBuilder(state);
32
+ }
33
+ /** Delete the entity, keeping the state as it is until then. */
34
+ deleteEntity() {
35
+ return new UpdateBuilder(null, Retention.deleteNow);
36
+ }
37
+ reply(value, metadata) {
38
+ return Object.freeze({ kind: "read-only", retention: null, outcome: Outcome.reply(() => value, metadata) });
39
+ }
40
+ error(message, code = ErrorCode.BadRequest) {
41
+ return Object.freeze({ kind: "read-only", retention: null, outcome: Outcome.fail(message, code) });
42
+ }
43
+ noReply() {
44
+ return Object.freeze({ kind: "read-only", retention: null, outcome: Outcome.noReply() });
45
+ }
46
+ }
47
+ export { done };
@@ -0,0 +1,40 @@
1
+ import { ErrorCode, type EffectLike, type ErrorDetail, type Metadata } from "./common.ts";
2
+ export type ViewEffect<Row> = ({
3
+ readonly kind: "update-row";
4
+ readonly row: Row;
5
+ } & EffectLike<never>) | ({
6
+ readonly kind: "delete-row";
7
+ } & EffectLike<never>) | ({
8
+ readonly kind: "ignore";
9
+ } & EffectLike<never>);
10
+ export declare class ViewEffects<Row> {
11
+ updateRow(row: Row): ViewEffect<Row>;
12
+ deleteRow(): ViewEffect<Row>;
13
+ ignore(): ViewEffect<Row>;
14
+ }
15
+ export type ConsumerEffect<Out> = ({
16
+ readonly kind: "produce";
17
+ readonly payload: Out;
18
+ readonly metadata: Metadata;
19
+ } & EffectLike<never>) | ({
20
+ readonly kind: "done";
21
+ } & EffectLike<never>) | ({
22
+ readonly kind: "ignore";
23
+ } & EffectLike<never>);
24
+ export declare class ConsumerEffects<Out> {
25
+ /** Produce onward to the topic the consumer declares in `producesTo`. */
26
+ produce(payload: Out, metadata?: Metadata): ConsumerEffect<Out>;
27
+ done(): ConsumerEffect<Out>;
28
+ ignore(): ConsumerEffect<Out>;
29
+ }
30
+ export type TimedActionEffect = ({
31
+ readonly kind: "done";
32
+ } & EffectLike<never>) | ({
33
+ readonly kind: "fail";
34
+ readonly error: ErrorDetail;
35
+ } & EffectLike<never>);
36
+ export declare class TimedActionEffects {
37
+ done(): TimedActionEffect;
38
+ /** Failed: the sweeper retries on its schedule with the attempt count incremented. */
39
+ fail(message: string, code?: ErrorCode): TimedActionEffect;
40
+ }
@@ -0,0 +1,35 @@
1
+ // Effects for the stateless kinds: a view says what an event does to a row, a consumer acknowledges
2
+ // or produces onward, a timed action is done or failed (and retried by the sweeper).
3
+ import { ErrorCode } from "./common.js";
4
+ export class ViewEffects {
5
+ updateRow(row) {
6
+ return Object.freeze({ kind: "update-row", row });
7
+ }
8
+ deleteRow() {
9
+ return Object.freeze({ kind: "delete-row" });
10
+ }
11
+ ignore() {
12
+ return Object.freeze({ kind: "ignore" });
13
+ }
14
+ }
15
+ export class ConsumerEffects {
16
+ /** Produce onward to the topic the consumer declares in `producesTo`. */
17
+ produce(payload, metadata = {}) {
18
+ return Object.freeze({ kind: "produce", payload, metadata });
19
+ }
20
+ done() {
21
+ return Object.freeze({ kind: "done" });
22
+ }
23
+ ignore() {
24
+ return Object.freeze({ kind: "ignore" });
25
+ }
26
+ }
27
+ export class TimedActionEffects {
28
+ done() {
29
+ return Object.freeze({ kind: "done" });
30
+ }
31
+ /** Failed: the sweeper retries on its schedule with the attempt count incremented. */
32
+ fail(message, code = ErrorCode.Internal) {
33
+ return Object.freeze({ kind: "fail", error: { message, code } });
34
+ }
35
+ }