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.
- package/README.md +30 -0
- package/dist/_proto/ankka/protocol/v1/agent_pb.d.ts +350 -0
- package/dist/_proto/ankka/protocol/v1/agent_pb.js +90 -0
- package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +254 -0
- package/dist/_proto/ankka/protocol/v1/client_pb.js +47 -0
- package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +105 -0
- package/dist/_proto/ankka/protocol/v1/consumer_pb.js +28 -0
- package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +598 -0
- package/dist/_proto/ankka/protocol/v1/discovery_pb.js +175 -0
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +224 -0
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +44 -0
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +212 -0
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +47 -0
- package/dist/_proto/ankka/protocol/v1/key_value_pb.d.ts +154 -0
- package/dist/_proto/ankka/protocol/v1/key_value_pb.js +36 -0
- package/dist/_proto/ankka/protocol/v1/payload_pb.d.ts +276 -0
- package/dist/_proto/ankka/protocol/v1/payload_pb.js +110 -0
- package/dist/_proto/ankka/protocol/v1/timed_action_pb.d.ts +75 -0
- package/dist/_proto/ankka/protocol/v1/timed_action_pb.js +23 -0
- package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +93 -0
- package/dist/_proto/ankka/protocol/v1/view_pb.js +23 -0
- package/dist/_proto/ankka/protocol/v1/workflow_pb.d.ts +298 -0
- package/dist/_proto/ankka/protocol/v1/workflow_pb.js +67 -0
- package/dist/agent.d.ts +27 -0
- package/dist/agent.js +36 -0
- package/dist/client.d.ts +92 -0
- package/dist/client.js +239 -0
- package/dist/codec.d.ts +44 -0
- package/dist/codec.js +215 -0
- package/dist/consumer.d.ts +31 -0
- package/dist/consumer.js +32 -0
- package/dist/context.d.ts +61 -0
- package/dist/context.js +75 -0
- package/dist/effects/agent.d.ts +37 -0
- package/dist/effects/agent.js +48 -0
- package/dist/effects/common.d.ts +68 -0
- package/dist/effects/common.js +55 -0
- package/dist/effects/eventSourced.d.ts +50 -0
- package/dist/effects/eventSourced.js +66 -0
- package/dist/effects/keyValue.d.ts +36 -0
- package/dist/effects/keyValue.js +47 -0
- package/dist/effects/stateless.d.ts +40 -0
- package/dist/effects/stateless.js +35 -0
- package/dist/effects/workflow.d.ts +99 -0
- package/dist/effects/workflow.js +83 -0
- package/dist/endpoint.d.ts +21 -0
- package/dist/endpoint.js +29 -0
- package/dist/eventSourcedEntity.d.ts +40 -0
- package/dist/eventSourcedEntity.js +66 -0
- package/dist/handlers.d.ts +55 -0
- package/dist/handlers.js +64 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +39 -0
- package/dist/json.d.ts +21 -0
- package/dist/json.js +362 -0
- package/dist/keyValueEntity.d.ts +29 -0
- package/dist/keyValueEntity.js +52 -0
- package/dist/kinds.d.ts +8 -0
- package/dist/kinds.js +41 -0
- package/dist/materialise.d.ts +33 -0
- package/dist/materialise.js +55 -0
- package/dist/routes.d.ts +71 -0
- package/dist/routes.js +79 -0
- package/dist/schema.d.ts +112 -0
- package/dist/schema.js +200 -0
- package/dist/server/agent.d.ts +14 -0
- package/dist/server/agent.js +113 -0
- package/dist/server/discovery.d.ts +5 -0
- package/dist/server/discovery.js +21 -0
- package/dist/server/eventSourced.d.ts +12 -0
- package/dist/server/eventSourced.js +168 -0
- package/dist/server/http.d.ts +10 -0
- package/dist/server/http.js +153 -0
- package/dist/server/keyValue.d.ts +5 -0
- package/dist/server/keyValue.js +122 -0
- package/dist/server/payloads.d.ts +6 -0
- package/dist/server/payloads.js +11 -0
- package/dist/server/queue.d.ts +10 -0
- package/dist/server/queue.js +67 -0
- package/dist/server/server.d.ts +34 -0
- package/dist/server/server.js +101 -0
- package/dist/server/stateless.d.ts +9 -0
- package/dist/server/stateless.js +108 -0
- package/dist/server/workflow.d.ts +5 -0
- package/dist/server/workflow.js +244 -0
- package/dist/service.d.ts +147 -0
- package/dist/service.js +384 -0
- package/dist/spec.d.ts +7 -0
- package/dist/spec.js +106 -0
- package/dist/testkit/index.d.ts +3 -0
- package/dist/testkit/index.js +6 -0
- package/dist/testkit/integration.d.ts +60 -0
- package/dist/testkit/integration.js +284 -0
- package/dist/testkit/kinds.d.ts +133 -0
- package/dist/testkit/kinds.js +398 -0
- package/dist/testkit/unit.d.ts +54 -0
- package/dist/testkit/unit.js +224 -0
- package/dist/time.d.ts +73 -0
- package/dist/time.js +275 -0
- package/dist/timedAction.d.ts +20 -0
- package/dist/timedAction.js +28 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/view.d.ts +37 -0
- package/dist/view.js +52 -0
- package/dist/workflow.d.ts +33 -0
- package/dist/workflow.js +61 -0
- package/package.json +75 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Duration } from "../time.ts";
|
|
2
|
+
import { ErrorCode, Outcome, type EffectLike, type ErrorDetail, type Metadata, type ReadOnlyLike } from "./common.ts";
|
|
3
|
+
/** A step to run, by wire name, with its input if it takes one. The step's declared shape encodes the input. */
|
|
4
|
+
export interface StepRef {
|
|
5
|
+
readonly step: string;
|
|
6
|
+
readonly input: unknown;
|
|
7
|
+
}
|
|
8
|
+
export type StepOutcome = {
|
|
9
|
+
readonly kind: "transition";
|
|
10
|
+
readonly ref: StepRef;
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: "pause";
|
|
13
|
+
readonly after: Duration | null;
|
|
14
|
+
readonly onTimeout: StepRef | null;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: "end";
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "fail";
|
|
19
|
+
readonly error: ErrorDetail;
|
|
20
|
+
};
|
|
21
|
+
/** A command handler's effect: a new state, a transition, and a reply. */
|
|
22
|
+
export interface WorkflowEffect<S, R> extends EffectLike<R> {
|
|
23
|
+
readonly kind: "workflow";
|
|
24
|
+
readonly newState: S | null;
|
|
25
|
+
readonly transition: StepRef | null;
|
|
26
|
+
readonly outcome: Outcome<S, R>;
|
|
27
|
+
}
|
|
28
|
+
export interface WorkflowReadOnlyEffect<S, R> extends ReadOnlyLike<R> {
|
|
29
|
+
readonly kind: "read-only";
|
|
30
|
+
readonly newState: null;
|
|
31
|
+
readonly transition: null;
|
|
32
|
+
readonly outcome: Outcome<S, R>;
|
|
33
|
+
}
|
|
34
|
+
export type WorkflowCommandEffect<S, R> = WorkflowEffect<S, R> | WorkflowReadOnlyEffect<S, R>;
|
|
35
|
+
/** A step's effect: a new state and what happens next. */
|
|
36
|
+
export interface StepEffect<S> extends EffectLike<never> {
|
|
37
|
+
readonly kind: "step";
|
|
38
|
+
readonly newState: S | null;
|
|
39
|
+
readonly next: StepOutcome;
|
|
40
|
+
}
|
|
41
|
+
export interface PauseOptions {
|
|
42
|
+
/** How long to pause before `onTimeout` runs; absent, the pause waits for a command. */
|
|
43
|
+
readonly after?: Duration;
|
|
44
|
+
/** The step to run when the pause times out. */
|
|
45
|
+
readonly onTimeout?: string;
|
|
46
|
+
readonly onTimeoutInput?: unknown;
|
|
47
|
+
}
|
|
48
|
+
export declare class WorkflowUpdateBuilder<S> {
|
|
49
|
+
#private;
|
|
50
|
+
constructor(newState: S | null, transition?: StepRef | null);
|
|
51
|
+
/** Start `step` once this command's state is journaled. */
|
|
52
|
+
thenTransitionTo(step: string, input?: unknown): WorkflowUpdateBuilder<S>;
|
|
53
|
+
thenReply<R>(compute: (state: S) => R, metadata?: Metadata): WorkflowEffect<S, R>;
|
|
54
|
+
thenReplyState(): WorkflowEffect<S, S>;
|
|
55
|
+
thenNoReply(): WorkflowEffect<S, never>;
|
|
56
|
+
}
|
|
57
|
+
/** Inside a command handler: `this.effects`. */
|
|
58
|
+
export declare class WorkflowEffects<S> {
|
|
59
|
+
updateState(state: S): WorkflowUpdateBuilder<S>;
|
|
60
|
+
transitionTo(step: string, input?: unknown): WorkflowUpdateBuilder<S>;
|
|
61
|
+
reply<R>(value: R, metadata?: Metadata): WorkflowReadOnlyEffect<S, R>;
|
|
62
|
+
error(message: string, code?: ErrorCode): WorkflowReadOnlyEffect<S, never>;
|
|
63
|
+
}
|
|
64
|
+
export declare class StepUpdateBuilder<S> {
|
|
65
|
+
#private;
|
|
66
|
+
constructor(newState: S | null);
|
|
67
|
+
thenTransitionTo(step: string, input?: unknown): StepEffect<S>;
|
|
68
|
+
/** Pause until a command moves the workflow on, or `after` passes and `onTimeout` runs. */
|
|
69
|
+
thenPause(options?: PauseOptions): StepEffect<S>;
|
|
70
|
+
thenEnd(): StepEffect<S>;
|
|
71
|
+
/** End the workflow as failed. This is what the step answered on purpose; a *thrown* error is retried and failed over as declared. */
|
|
72
|
+
thenFail(message: string, code?: ErrorCode): StepEffect<S>;
|
|
73
|
+
}
|
|
74
|
+
/** Inside a step: `this.stepEffects`. */
|
|
75
|
+
export declare class StepEffects<S> {
|
|
76
|
+
updateState(state: S): StepUpdateBuilder<S>;
|
|
77
|
+
transitionTo(step: string, input?: unknown): StepEffect<S>;
|
|
78
|
+
pause(options?: PauseOptions): StepEffect<S>;
|
|
79
|
+
end(): StepEffect<S>;
|
|
80
|
+
fail(message: string, code?: ErrorCode): StepEffect<S>;
|
|
81
|
+
}
|
|
82
|
+
/** What the sidecar does when a step throws or times out: retry it, then fail over to a step that takes no input, or fail the workflow. */
|
|
83
|
+
export interface Recovery {
|
|
84
|
+
readonly maxRetries: number;
|
|
85
|
+
readonly failoverTo?: string;
|
|
86
|
+
}
|
|
87
|
+
export interface StepSettings {
|
|
88
|
+
readonly timeout?: Duration;
|
|
89
|
+
readonly recovery?: Recovery;
|
|
90
|
+
}
|
|
91
|
+
/** Timeouts and recovery, enforced by the sidecar's engine. Absent values are the engine's defaults: no overall limit, 30 seconds a step, a failed step fails the workflow. */
|
|
92
|
+
export interface WorkflowSettings {
|
|
93
|
+
readonly timeout?: Duration;
|
|
94
|
+
readonly defaultStepTimeout?: Duration;
|
|
95
|
+
readonly defaultRecovery?: Recovery;
|
|
96
|
+
readonly steps?: Readonly<Record<string, StepSettings>>;
|
|
97
|
+
}
|
|
98
|
+
/** Declares a workflow's settings; the registry checks every step it names against the declared steps. */
|
|
99
|
+
export declare function workflowSettings(settings: WorkflowSettings): WorkflowSettings;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Effects for workflows: a command may change state and start a step; a step changes state and says
|
|
2
|
+
// what happens next. Timeouts and recovery are declared in `settings` and enforced by the sidecar's
|
|
3
|
+
// engine, since the process cannot enforce them.
|
|
4
|
+
import { ErrorCode, Outcome } from "./common.js";
|
|
5
|
+
export class WorkflowUpdateBuilder {
|
|
6
|
+
#newState;
|
|
7
|
+
#transition;
|
|
8
|
+
constructor(newState, transition = null) {
|
|
9
|
+
this.#newState = newState;
|
|
10
|
+
this.#transition = transition;
|
|
11
|
+
}
|
|
12
|
+
/** Start `step` once this command's state is journaled. */
|
|
13
|
+
thenTransitionTo(step, input) {
|
|
14
|
+
return new WorkflowUpdateBuilder(this.#newState, { step, input });
|
|
15
|
+
}
|
|
16
|
+
thenReply(compute, metadata) {
|
|
17
|
+
return Object.freeze({ kind: "workflow", newState: this.#newState, transition: this.#transition, outcome: Outcome.reply(compute, metadata) });
|
|
18
|
+
}
|
|
19
|
+
thenReplyState() {
|
|
20
|
+
return this.thenReply((state) => state);
|
|
21
|
+
}
|
|
22
|
+
thenNoReply() {
|
|
23
|
+
return Object.freeze({ kind: "workflow", newState: this.#newState, transition: this.#transition, outcome: Outcome.noReply() });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Inside a command handler: `this.effects`. */
|
|
27
|
+
export class WorkflowEffects {
|
|
28
|
+
updateState(state) {
|
|
29
|
+
return new WorkflowUpdateBuilder(state);
|
|
30
|
+
}
|
|
31
|
+
transitionTo(step, input) {
|
|
32
|
+
return new WorkflowUpdateBuilder(null, { step, input });
|
|
33
|
+
}
|
|
34
|
+
reply(value, metadata) {
|
|
35
|
+
return Object.freeze({ kind: "read-only", newState: null, transition: null, outcome: Outcome.reply(() => value, metadata) });
|
|
36
|
+
}
|
|
37
|
+
error(message, code = ErrorCode.BadRequest) {
|
|
38
|
+
return Object.freeze({ kind: "read-only", newState: null, transition: null, outcome: Outcome.fail(message, code) });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class StepUpdateBuilder {
|
|
42
|
+
#newState;
|
|
43
|
+
constructor(newState) {
|
|
44
|
+
this.#newState = newState;
|
|
45
|
+
}
|
|
46
|
+
thenTransitionTo(step, input) {
|
|
47
|
+
return Object.freeze({ kind: "step", newState: this.#newState, next: { kind: "transition", ref: { step, input } } });
|
|
48
|
+
}
|
|
49
|
+
/** Pause until a command moves the workflow on, or `after` passes and `onTimeout` runs. */
|
|
50
|
+
thenPause(options = {}) {
|
|
51
|
+
const onTimeout = options.onTimeout ? { step: options.onTimeout, input: options.onTimeoutInput } : null;
|
|
52
|
+
return Object.freeze({ kind: "step", newState: this.#newState, next: { kind: "pause", after: options.after ?? null, onTimeout } });
|
|
53
|
+
}
|
|
54
|
+
thenEnd() {
|
|
55
|
+
return Object.freeze({ kind: "step", newState: this.#newState, next: { kind: "end" } });
|
|
56
|
+
}
|
|
57
|
+
/** End the workflow as failed. This is what the step answered on purpose; a *thrown* error is retried and failed over as declared. */
|
|
58
|
+
thenFail(message, code = ErrorCode.Internal) {
|
|
59
|
+
return Object.freeze({ kind: "step", newState: this.#newState, next: { kind: "fail", error: { message, code } } });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Inside a step: `this.stepEffects`. */
|
|
63
|
+
export class StepEffects {
|
|
64
|
+
updateState(state) {
|
|
65
|
+
return new StepUpdateBuilder(state);
|
|
66
|
+
}
|
|
67
|
+
transitionTo(step, input) {
|
|
68
|
+
return new StepUpdateBuilder(null).thenTransitionTo(step, input);
|
|
69
|
+
}
|
|
70
|
+
pause(options = {}) {
|
|
71
|
+
return new StepUpdateBuilder(null).thenPause(options);
|
|
72
|
+
}
|
|
73
|
+
end() {
|
|
74
|
+
return new StepUpdateBuilder(null).thenEnd();
|
|
75
|
+
}
|
|
76
|
+
fail(message, code = ErrorCode.Internal) {
|
|
77
|
+
return new StepUpdateBuilder(null).thenFail(message, code);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Declares a workflow's settings; the registry checks every step it names against the declared steps. */
|
|
81
|
+
export function workflowSettings(settings) {
|
|
82
|
+
return Object.freeze({ ...settings, steps: settings.steps ? Object.freeze({ ...settings.steps }) : undefined });
|
|
83
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComponentClient } from "./client.ts";
|
|
2
|
+
import { type RequestContext } from "./context.ts";
|
|
3
|
+
import type { Acl, RouteTable } from "./routes.ts";
|
|
4
|
+
export declare abstract class Endpoint {
|
|
5
|
+
#private;
|
|
6
|
+
/** The request being handled: path parameters, query, headers, principal. Request-scoped. */
|
|
7
|
+
get request(): RequestContext;
|
|
8
|
+
/** The component client, scoped to the current request's trace so the sidecar records child spans. */
|
|
9
|
+
get client(): ComponentClient;
|
|
10
|
+
/** @internal */
|
|
11
|
+
_bindClient(client: ComponentClient | undefined): void;
|
|
12
|
+
}
|
|
13
|
+
/** The statics an endpoint class must declare; `register` constrains on this type, so a missing `acl` is a compile error. */
|
|
14
|
+
export interface EndpointClass<C extends Endpoint = Endpoint> {
|
|
15
|
+
new (): C;
|
|
16
|
+
/** `/carts`: every route is relative to it. */
|
|
17
|
+
readonly prefix: string;
|
|
18
|
+
/** Who may call, for every route unless a route states its own. */
|
|
19
|
+
readonly acl: Acl;
|
|
20
|
+
readonly routes: RouteTable<C>;
|
|
21
|
+
}
|
package/dist/endpoint.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// An HTTP endpoint: a prefix, an access rule and a table of routes. The sidecar serves the routes on
|
|
2
|
+
// the service's HTTP port and forwards each request here; the process never binds an HTTP port. One
|
|
3
|
+
// instance serves every request, so per-request state lives on `this.request`, never on a field.
|
|
4
|
+
//
|
|
5
|
+
// export class ShoppingCartEndpoint extends Endpoint {
|
|
6
|
+
// static readonly prefix = "/carts"
|
|
7
|
+
// static readonly acl = Acl.allowAll // required; there is no default
|
|
8
|
+
// static readonly routes = { getCart: get("/{cartId}", ShoppingCart, (ep: ShoppingCartEndpoint, req) => ep.getCart(req.params.cartId)) }
|
|
9
|
+
// getCart(cartId: string) { return this.client.of(ShoppingCartEntity, cartId).call(ShoppingCartEntity.handlers.getCart).invoke() }
|
|
10
|
+
// }
|
|
11
|
+
import { currentRequest, requestIfAny } from "./context.js";
|
|
12
|
+
export class Endpoint {
|
|
13
|
+
#client;
|
|
14
|
+
/** The request being handled: path parameters, query, headers, principal. Request-scoped. */
|
|
15
|
+
get request() {
|
|
16
|
+
return currentRequest();
|
|
17
|
+
}
|
|
18
|
+
/** The component client, scoped to the current request's trace so the sidecar records child spans. */
|
|
19
|
+
get client() {
|
|
20
|
+
if (!this.#client)
|
|
21
|
+
throw new Error("client is only available inside a route handler");
|
|
22
|
+
const request = requestIfAny();
|
|
23
|
+
return request ? this.#client.withMetadata(request.metadata) : this.#client;
|
|
24
|
+
}
|
|
25
|
+
/** @internal */
|
|
26
|
+
_bindClient(client) {
|
|
27
|
+
this.#client = client;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Shape } from "./codec.ts";
|
|
2
|
+
import type { CommandContext } from "./context.ts";
|
|
3
|
+
import type { HandlerTable } from "./handlers.ts";
|
|
4
|
+
import type { ComponentClient } from "./client.ts";
|
|
5
|
+
import { EventSourcedEffects } from "./effects/eventSourced.ts";
|
|
6
|
+
export declare abstract class EventSourcedEntity<S, E> {
|
|
7
|
+
#private;
|
|
8
|
+
/** The effect builders: `this.effects.persist(...)`, `.reply(...)`, `.error(...)`. */
|
|
9
|
+
readonly effects: EventSourcedEffects<S, E>;
|
|
10
|
+
/** The state the entity has now, inside a handler. */
|
|
11
|
+
get state(): S;
|
|
12
|
+
/** The id of the instance this object stands for; available in `emptyState` and in handlers. */
|
|
13
|
+
get entityId(): string;
|
|
14
|
+
/** The command's context: component id, sequence number, metadata, the clock. */
|
|
15
|
+
get context(): CommandContext;
|
|
16
|
+
/** The component client, scoped to the command's trace. */
|
|
17
|
+
get client(): ComponentClient;
|
|
18
|
+
/** @internal The kind, read off the prototype by the typed client and the timers. */
|
|
19
|
+
get _kind(): "event-sourced";
|
|
20
|
+
/** The state of an instance with no events yet. `this.entityId` is available here. */
|
|
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;
|
|
24
|
+
/** @internal Bound by the runtime and the testkit before `emptyState`. */
|
|
25
|
+
_bindInstance(entityId: string): void;
|
|
26
|
+
/** @internal Bound by the runtime and the testkit for the duration of one handler call. */
|
|
27
|
+
_bindCommand(state: S, context: CommandContext, client: ComponentClient): void;
|
|
28
|
+
/** @internal */
|
|
29
|
+
_unbindCommand(): void;
|
|
30
|
+
}
|
|
31
|
+
/** The statics an event sourced entity class must declare; `register` constrains on this type. */
|
|
32
|
+
export interface EventSourcedEntityClass<S = unknown, E = unknown, C extends EventSourcedEntity<S, E> = EventSourcedEntity<S, E>> {
|
|
33
|
+
new (): C;
|
|
34
|
+
readonly componentId: string;
|
|
35
|
+
readonly state: Shape<S>;
|
|
36
|
+
readonly events: Shape<E>;
|
|
37
|
+
readonly handlers: HandlerTable<C>;
|
|
38
|
+
/** Ask for a snapshot every N events; absent or 0: never. */
|
|
39
|
+
readonly snapshotEvery?: number;
|
|
40
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// An event sourced entity: state derived from a journal of events. The class declares its identity and
|
|
2
|
+
// codecs as statics and its handlers in a static table; the instance holds the state for one command.
|
|
3
|
+
//
|
|
4
|
+
// export class ShoppingCartEntity extends EventSourcedEntity<ShoppingCart, ShoppingCartEvent> {
|
|
5
|
+
// static readonly componentId = "shopping-cart"
|
|
6
|
+
// static readonly state = jsonCodec(ShoppingCart, "shopping-cart")
|
|
7
|
+
// static readonly events = jsonCodec(ShoppingCartEvent, "shopping-cart-event")
|
|
8
|
+
// static readonly handlers = { addItem: command("add-item", LineItem, Done, (cart: ShoppingCartEntity, item) => cart.addItem(item)) }
|
|
9
|
+
// emptyState() { ... } applyEvent(state, event) { ... } addItem(item) { return this.effects.persist(...).thenReply(() => done) }
|
|
10
|
+
// }
|
|
11
|
+
import { EventSourcedEffects } from "./effects/eventSourced.js";
|
|
12
|
+
export class EventSourcedEntity {
|
|
13
|
+
/** The effect builders: `this.effects.persist(...)`, `.reply(...)`, `.error(...)`. */
|
|
14
|
+
effects = new EventSourcedEffects();
|
|
15
|
+
#state;
|
|
16
|
+
#entityId;
|
|
17
|
+
#context;
|
|
18
|
+
#client;
|
|
19
|
+
#bound = false;
|
|
20
|
+
/** The state the entity has now, inside a handler. */
|
|
21
|
+
get state() {
|
|
22
|
+
if (!this.#bound)
|
|
23
|
+
throw new Error("state is only available inside a command handler");
|
|
24
|
+
return this.#state;
|
|
25
|
+
}
|
|
26
|
+
/** The id of the instance this object stands for; available in `emptyState` and in handlers. */
|
|
27
|
+
get entityId() {
|
|
28
|
+
if (this.#entityId === undefined)
|
|
29
|
+
throw new Error("entityId is only available once the entity is bound to an instance");
|
|
30
|
+
return this.#entityId;
|
|
31
|
+
}
|
|
32
|
+
/** The command's context: component id, sequence number, metadata, the clock. */
|
|
33
|
+
get context() {
|
|
34
|
+
if (!this.#context)
|
|
35
|
+
throw new Error("context is only available inside a command handler");
|
|
36
|
+
return this.#context;
|
|
37
|
+
}
|
|
38
|
+
/** The component client, scoped to the command's trace. */
|
|
39
|
+
get client() {
|
|
40
|
+
if (!this.#client)
|
|
41
|
+
throw new Error("client is only available inside a command handler");
|
|
42
|
+
return this.#client;
|
|
43
|
+
}
|
|
44
|
+
/** @internal The kind, read off the prototype by the typed client and the timers. */
|
|
45
|
+
get _kind() {
|
|
46
|
+
return "event-sourced";
|
|
47
|
+
}
|
|
48
|
+
/** @internal Bound by the runtime and the testkit before `emptyState`. */
|
|
49
|
+
_bindInstance(entityId) {
|
|
50
|
+
this.#entityId = entityId;
|
|
51
|
+
}
|
|
52
|
+
/** @internal Bound by the runtime and the testkit for the duration of one handler call. */
|
|
53
|
+
_bindCommand(state, context, client) {
|
|
54
|
+
this.#state = state;
|
|
55
|
+
this.#context = context;
|
|
56
|
+
this.#client = client;
|
|
57
|
+
this.#bound = true;
|
|
58
|
+
}
|
|
59
|
+
/** @internal */
|
|
60
|
+
_unbindCommand() {
|
|
61
|
+
this.#state = undefined;
|
|
62
|
+
this.#context = undefined;
|
|
63
|
+
this.#client = undefined;
|
|
64
|
+
this.#bound = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Shape } from "./codec.ts";
|
|
2
|
+
import type { EffectLike, ReadOnlyLike } from "./effects/common.ts";
|
|
3
|
+
export type HandlerKind = "command" | "query" | "stream" | "step" | "action";
|
|
4
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
5
|
+
/** One entry of a handler table. `C` is the component class, `I` the input, `R` the reply. */
|
|
6
|
+
export interface HandlerRef<C = unknown, I = unknown, R = unknown, E extends EffectLike = EffectLike> {
|
|
7
|
+
readonly kind: HandlerKind;
|
|
8
|
+
/** The wire name. */
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly input: Shape<I> | undefined;
|
|
11
|
+
readonly reply: Shape<R> | undefined;
|
|
12
|
+
readonly readOnly: boolean;
|
|
13
|
+
readonly streaming: boolean;
|
|
14
|
+
readonly run: (self: C, input: I) => MaybePromise<E>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A command handler: may persist, update, transition or refuse. `command("add-item", LineItem, Done, (e, item) => e.addItem(item))`,
|
|
18
|
+
* or without an input: `command("checkout", Done, (e) => e.checkout())`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function command<C, I, R, E extends EffectLike<R>>(name: string, input: Shape<I>, reply: Shape<R>, run: (self: C, input: I) => MaybePromise<E>): HandlerRef<C, I, R, E>;
|
|
21
|
+
export declare function command<C, R, E extends EffectLike<R>>(name: string, reply: Shape<R>, run: (self: C) => MaybePromise<E>): HandlerRef<C, undefined, R, E>;
|
|
22
|
+
/**
|
|
23
|
+
* A query handler: its function must return a read-only effect, so a query that persists does not
|
|
24
|
+
* compile. Registration checks the effect's `kind` again at runtime, and the sidecar refuses events
|
|
25
|
+
* from a read-only handler regardless.
|
|
26
|
+
*/
|
|
27
|
+
export declare function query<C, I, R, E extends ReadOnlyLike<R>>(name: string, input: Shape<I>, reply: Shape<R>, run: (self: C, input: I) => MaybePromise<E>): HandlerRef<C, I, R, E>;
|
|
28
|
+
export declare function query<C, R, E extends ReadOnlyLike<R>>(name: string, reply: Shape<R>, run: (self: C) => MaybePromise<E>): HandlerRef<C, undefined, R, E>;
|
|
29
|
+
/** An agent handler whose reply streams as tokens: `stream("stream", s.string, (a, q) => a.ask(q))`. */
|
|
30
|
+
export declare function stream<C, I, E extends EffectLike>(name: string, input: Shape<I>, run: (self: C, input: I) => MaybePromise<E>): HandlerRef<C, I, string, E>;
|
|
31
|
+
export declare function stream<C, E extends EffectLike>(name: string, run: (self: C) => MaybePromise<E>): HandlerRef<C, undefined, string, E>;
|
|
32
|
+
/** A workflow step: `step("charge", s.int, (w, quantity) => w.charge(quantity))` or `step("reserve", (w) => w.reserve())`. */
|
|
33
|
+
export declare function step<C, I, E extends EffectLike>(name: string, input: Shape<I>, run: (self: C, input: I) => MaybePromise<E>): HandlerRef<C, I, undefined, E>;
|
|
34
|
+
export declare function step<C, E extends EffectLike>(name: string, run: (self: C) => MaybePromise<E>): HandlerRef<C, undefined, undefined, E>;
|
|
35
|
+
/** A timed action: `action("remind", s.string, (t, id) => t.remind(id))`. */
|
|
36
|
+
export declare function action<C, I, E extends EffectLike>(name: string, input: Shape<I>, run: (self: C, input: I) => MaybePromise<E>): HandlerRef<C, I, undefined, E>;
|
|
37
|
+
export declare function action<C, E extends EffectLike>(name: string, run: (self: C) => MaybePromise<E>): HandlerRef<C, undefined, undefined, E>;
|
|
38
|
+
/** An agent tool: the model sees `description` and the JSON Schema of `input`; the process runs `run`. */
|
|
39
|
+
export interface ToolRef<C = unknown, I = unknown> {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly description: string;
|
|
42
|
+
readonly input: Shape<I>;
|
|
43
|
+
readonly run: (self: C, input: I) => MaybePromise<unknown>;
|
|
44
|
+
}
|
|
45
|
+
export declare function tool<C, I>(name: string, description: string, input: Shape<I>, run: (self: C, input: I) => MaybePromise<unknown>): ToolRef<C, I>;
|
|
46
|
+
export type GuardrailStage = "input" | "output";
|
|
47
|
+
/** An agent guardrail: returns a reason to block, or `null` to let the text through. */
|
|
48
|
+
export interface GuardrailRef {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly check: (stage: GuardrailStage, text: string) => MaybePromise<string | null>;
|
|
51
|
+
}
|
|
52
|
+
export declare function guardrail(name: string, check: (stage: GuardrailStage, text: string) => MaybePromise<string | null>): GuardrailRef;
|
|
53
|
+
/** The handler table type a component declares: property names are the developer's, `name` is the wire's. */
|
|
54
|
+
export type HandlerTable<C> = Readonly<Record<string, HandlerRef<C, any, any, any>>>;
|
|
55
|
+
export {};
|
package/dist/handlers.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Handler declarations: the entries of a component's static `handlers`, `steps`, `actions`, `tools`
|
|
2
|
+
// and `guardrails` tables. Each carries the wire name, the input and reply schemas, and the function
|
|
3
|
+
// that runs it. The wire name is declared here and nowhere else; renaming the method it calls changes
|
|
4
|
+
// nothing on the wire. These are plain values, so the registry can read them without reflection.
|
|
5
|
+
function ref(kind, name, input, reply, run, flags) {
|
|
6
|
+
if (typeof name !== "string" || name.trim() === "")
|
|
7
|
+
throw new TypeError(`a ${kind} needs a wire name`);
|
|
8
|
+
if (typeof run !== "function")
|
|
9
|
+
throw new TypeError(`${kind} ${name}: the handler is not a function`);
|
|
10
|
+
return Object.freeze({ kind, name, input, reply, run, ...flags });
|
|
11
|
+
}
|
|
12
|
+
export function command(name, ...rest) {
|
|
13
|
+
if (rest.length === 3) {
|
|
14
|
+
const [input, reply, run] = rest;
|
|
15
|
+
return ref("command", name, input, reply, run, { readOnly: false, streaming: false });
|
|
16
|
+
}
|
|
17
|
+
const [reply, run] = rest;
|
|
18
|
+
return ref("command", name, undefined, reply, run, { readOnly: false, streaming: false });
|
|
19
|
+
}
|
|
20
|
+
export function query(name, ...rest) {
|
|
21
|
+
if (rest.length === 3) {
|
|
22
|
+
const [input, reply, run] = rest;
|
|
23
|
+
return ref("query", name, input, reply, run, { readOnly: true, streaming: false });
|
|
24
|
+
}
|
|
25
|
+
const [reply, run] = rest;
|
|
26
|
+
return ref("query", name, undefined, reply, run, { readOnly: true, streaming: false });
|
|
27
|
+
}
|
|
28
|
+
export function stream(name, ...rest) {
|
|
29
|
+
if (rest.length === 2) {
|
|
30
|
+
const [input, run] = rest;
|
|
31
|
+
return ref("stream", name, input, undefined, run, { readOnly: false, streaming: true });
|
|
32
|
+
}
|
|
33
|
+
const [run] = rest;
|
|
34
|
+
return ref("stream", name, undefined, undefined, run, { readOnly: false, streaming: true });
|
|
35
|
+
}
|
|
36
|
+
export function step(name, ...rest) {
|
|
37
|
+
if (rest.length === 2) {
|
|
38
|
+
const [input, run] = rest;
|
|
39
|
+
return ref("step", name, input, undefined, run, { readOnly: false, streaming: false });
|
|
40
|
+
}
|
|
41
|
+
const [run] = rest;
|
|
42
|
+
return ref("step", name, undefined, undefined, run, { readOnly: false, streaming: false });
|
|
43
|
+
}
|
|
44
|
+
export function action(name, ...rest) {
|
|
45
|
+
if (rest.length === 2) {
|
|
46
|
+
const [input, run] = rest;
|
|
47
|
+
return ref("action", name, input, undefined, run, { readOnly: false, streaming: false });
|
|
48
|
+
}
|
|
49
|
+
const [run] = rest;
|
|
50
|
+
return ref("action", name, undefined, undefined, run, { readOnly: false, streaming: false });
|
|
51
|
+
}
|
|
52
|
+
export function tool(name, description, input, run) {
|
|
53
|
+
if (typeof name !== "string" || name.trim() === "")
|
|
54
|
+
throw new TypeError("a tool needs a name");
|
|
55
|
+
if (typeof description !== "string" || description.trim() === "") {
|
|
56
|
+
throw new TypeError(`tool ${name}: a tool needs a description, or the model cannot choose it`);
|
|
57
|
+
}
|
|
58
|
+
return Object.freeze({ name, description, input, run });
|
|
59
|
+
}
|
|
60
|
+
export function guardrail(name, check) {
|
|
61
|
+
if (typeof name !== "string" || name.trim() === "")
|
|
62
|
+
throw new TypeError("a guardrail needs a name");
|
|
63
|
+
return Object.freeze({ name, check });
|
|
64
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export { VERSION } from "./version.ts";
|
|
2
|
+
export { s, Done, done, SchemaError, toJsonSchema, defaultManifest, type Schema, type Infer } from "./schema.ts";
|
|
3
|
+
export { jsonCodec, defaultCodecFor, codecForManifest, textCodecs, binaryCodecs, type Codec, type Shape, type ContentType } from "./codec.ts";
|
|
4
|
+
export { EncodingError, DecodingError, renderDouble } from "./json.ts";
|
|
5
|
+
export { Instant, Duration, LocalDate, LocalDateTime } from "./time.ts";
|
|
6
|
+
export { ErrorCode, CommandError, Outcome, Retention, httpStatusOf, type ErrorDetail, type Metadata, type EffectLike, type ReadOnlyLike } from "./effects/common.ts";
|
|
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";
|
|
9
|
+
export { EventSourcedEntity, type EventSourcedEntityClass } from "./eventSourcedEntity.ts";
|
|
10
|
+
export { UpdateBuilder, KeyValueEffects, type UpdateEffect, type KeyValueReadOnlyEffect, type KeyValueEffect } from "./effects/keyValue.ts";
|
|
11
|
+
export { KeyValueEntity, type KeyValueEntityClass } from "./keyValueEntity.ts";
|
|
12
|
+
export { WorkflowEffects, StepEffects, WorkflowUpdateBuilder, StepUpdateBuilder, workflowSettings, type WorkflowEffect, type WorkflowReadOnlyEffect, type WorkflowCommandEffect, type StepEffect, type StepOutcome, type StepRef, type PauseOptions, type WorkflowSettings, type StepSettings, type Recovery, } from "./effects/workflow.ts";
|
|
13
|
+
export { Workflow, type WorkflowClass } from "./workflow.ts";
|
|
14
|
+
export { ViewEffects, ConsumerEffects, TimedActionEffects, type ViewEffect, type ConsumerEffect, type TimedActionEffect } from "./effects/stateless.ts";
|
|
15
|
+
export { View, type ViewClass } from "./view.ts";
|
|
16
|
+
export { Consumer, type ConsumerClass } from "./consumer.ts";
|
|
17
|
+
export { TimedAction, type TimedActionClass } from "./timedAction.ts";
|
|
18
|
+
export { AgentEffects, type AgentEffect } from "./effects/agent.ts";
|
|
19
|
+
export { Agent, type AgentClass } from "./agent.ts";
|
|
20
|
+
export { Endpoint, type EndpointClass } from "./endpoint.ts";
|
|
21
|
+
export { Acl, HttpProblem, get, post, put, patch, del, sse, type RouteRef, type RouteOptions, type RouteTable, type Params, type ParamNames, type HttpMethod } from "./routes.ts";
|
|
22
|
+
export { type CommandContext, type RequestContext, type Principal, Query, Headers } from "./context.ts";
|
|
23
|
+
export { ComponentClient, Calls, TypedCalls, Invocation, Views, Timers, type TimerTarget, type ComponentRef } from "./client.ts";
|
|
24
|
+
export { Ankka, ServiceBuilder, Registry, RegistrationError, type ServiceOptions, type RegisteredComponent, type RegisteredEndpoint, type Source, type RegisteredEventSourced, type RegisteredKeyValue, type RegisteredWorkflow, type RegisteredView, type RegisteredConsumer, type RegisteredTimedAction, type RegisteredAgent, } from "./service.ts";
|
|
25
|
+
export { Server, type ServerOptions } from "./server/server.ts";
|
|
26
|
+
export { problems as sidecarProblems } from "./server/discovery.ts";
|
|
27
|
+
export { PROTOCOL_VERSION, SDK_NAME } from "./spec.ts";
|
|
28
|
+
export { materialiseEventSourced, materialiseKeyValue, materialiseWorkflowCommand, materialiseStep, type Materialised, type MaterialisedKeyValue, type MaterialisedWorkflowCommand, type MaterialisedStep, } from "./materialise.ts";
|
|
29
|
+
export type { ComponentKind } from "./kinds.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The ankka SDK for TypeScript on Node.js. See https://docs.ankka.cloud/reference/typescript-sdk/.
|
|
2
|
+
// The Node floor, checked before anything else is evaluated, so an old Node fails with a message that
|
|
3
|
+
// names the version rather than a syntax error from a construct it lacks.
|
|
4
|
+
{
|
|
5
|
+
const required = "22.22.0";
|
|
6
|
+
const [major, minor, patch] = process.versions.node.split(".").map(Number);
|
|
7
|
+
const [rMajor, rMinor, rPatch] = required.split(".").map(Number);
|
|
8
|
+
const ok = major > rMajor || (major === rMajor && (minor > rMinor || (minor === rMinor && patch >= rPatch)));
|
|
9
|
+
if (!ok)
|
|
10
|
+
throw new Error(`ankka needs Node.js ${required} or later; this is ${process.versions.node}`);
|
|
11
|
+
}
|
|
12
|
+
export { VERSION } from "./version.js";
|
|
13
|
+
export { s, Done, done, SchemaError, toJsonSchema, defaultManifest } from "./schema.js";
|
|
14
|
+
export { jsonCodec, defaultCodecFor, codecForManifest, textCodecs, binaryCodecs } from "./codec.js";
|
|
15
|
+
export { EncodingError, DecodingError, renderDouble } from "./json.js";
|
|
16
|
+
export { Instant, Duration, LocalDate, LocalDateTime } from "./time.js";
|
|
17
|
+
export { ErrorCode, CommandError, Outcome, Retention, httpStatusOf } from "./effects/common.js";
|
|
18
|
+
export { command, query, stream, step, action, tool, guardrail } from "./handlers.js";
|
|
19
|
+
export { PersistBuilder, EventSourcedEffects } from "./effects/eventSourced.js";
|
|
20
|
+
export { EventSourcedEntity } from "./eventSourcedEntity.js";
|
|
21
|
+
export { UpdateBuilder, KeyValueEffects } from "./effects/keyValue.js";
|
|
22
|
+
export { KeyValueEntity } from "./keyValueEntity.js";
|
|
23
|
+
export { WorkflowEffects, StepEffects, WorkflowUpdateBuilder, StepUpdateBuilder, workflowSettings, } from "./effects/workflow.js";
|
|
24
|
+
export { Workflow } from "./workflow.js";
|
|
25
|
+
export { ViewEffects, ConsumerEffects, TimedActionEffects } from "./effects/stateless.js";
|
|
26
|
+
export { View } from "./view.js";
|
|
27
|
+
export { Consumer } from "./consumer.js";
|
|
28
|
+
export { TimedAction } from "./timedAction.js";
|
|
29
|
+
export { AgentEffects } from "./effects/agent.js";
|
|
30
|
+
export { Agent } from "./agent.js";
|
|
31
|
+
export { Endpoint } from "./endpoint.js";
|
|
32
|
+
export { Acl, HttpProblem, get, post, put, patch, del, sse } from "./routes.js";
|
|
33
|
+
export { Query, Headers } from "./context.js";
|
|
34
|
+
export { ComponentClient, Calls, TypedCalls, Invocation, Views, Timers } from "./client.js";
|
|
35
|
+
export { Ankka, ServiceBuilder, Registry, RegistrationError, } from "./service.js";
|
|
36
|
+
export { Server } from "./server/server.js";
|
|
37
|
+
export { problems as sidecarProblems } from "./server/discovery.js";
|
|
38
|
+
export { PROTOCOL_VERSION, SDK_NAME } from "./spec.js";
|
|
39
|
+
export { materialiseEventSourced, materialiseKeyValue, materialiseWorkflowCommand, materialiseStep, } from "./materialise.js";
|
package/dist/json.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Schema } from "./schema.ts";
|
|
2
|
+
export declare class EncodingError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class DecodingError extends Error {
|
|
6
|
+
/** Where in the document the refusal is, like `items[2].quantity`; empty at the top level. */
|
|
7
|
+
readonly path: string;
|
|
8
|
+
constructor(message: string, path: string);
|
|
9
|
+
}
|
|
10
|
+
/** `Double.toString` as Scala prints it: plain between 1e-3 and 1e7 with at least one fractional digit, scientific outside. */
|
|
11
|
+
export declare function renderDouble(d: number): string;
|
|
12
|
+
/** The JSON text for `value` under `schema`. */
|
|
13
|
+
export declare function writeJson(schema: Schema, value: unknown): string;
|
|
14
|
+
/** Decodes `text` under `schema`. Lenient on unknown fields and field order; strict on what is required. */
|
|
15
|
+
export declare function readJson<T>(schema: Schema<T>, text: string): T;
|
|
16
|
+
/** Applies the schema to a value `JSON.parse` produced (with the same reviver), for callers that hold parsed JSON. */
|
|
17
|
+
export declare function decodeJsonValue<T>(schema: Schema<T>, raw: unknown): T;
|
|
18
|
+
/** The reviver: an integer past 2⁵³ arrives as a `BigInt` made from its source text, everything else as usual. */
|
|
19
|
+
export declare function reviver(this: unknown, _key: string, value: unknown, context?: {
|
|
20
|
+
source?: string;
|
|
21
|
+
}): unknown;
|