ankka 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +316 -0
- package/dist/_proto/ankka/protocol/v1/client_pb.js +97 -23
- package/dist/_proto/ankka/protocol/v1/consumer_pb.d.ts +10 -1
- package/dist/_proto/ankka/protocol/v1/consumer_pb.js +1 -1
- package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +43 -0
- package/dist/_proto/ankka/protocol/v1/discovery_pb.js +1 -1
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.d.ts +26 -0
- package/dist/_proto/ankka/protocol/v1/endpoint_pb.js +15 -10
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +86 -0
- package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +11 -1
- package/dist/_proto/ankka/protocol/v1/payload_pb.d.ts +61 -0
- package/dist/_proto/ankka/protocol/v1/payload_pb.js +18 -1
- package/dist/_proto/ankka/protocol/v1/view_pb.d.ts +7 -1
- package/dist/_proto/ankka/protocol/v1/view_pb.js +1 -1
- package/dist/_proto/ankka/protocol/v1/wasm_pb.d.ts +12 -0
- package/dist/_proto/ankka/protocol/v1/wasm_pb.js +1 -1
- package/dist/client.d.ts +96 -3
- package/dist/client.js +228 -6
- package/dist/consumer.d.ts +18 -1
- package/dist/consumer.js +15 -1
- package/dist/context.d.ts +25 -1
- package/dist/context.js +5 -2
- package/dist/contract.d.ts +4 -0
- package/dist/contract.js +2 -0
- package/dist/effects/common.d.ts +7 -0
- package/dist/effects/common.js +9 -1
- package/dist/effects/eventSourced.d.ts +26 -1
- package/dist/effects/eventSourced.js +58 -5
- package/dist/eventSourcedEntity.d.ts +8 -3
- package/dist/graph.d.ts +9 -1
- package/dist/graph.js +7 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/keyedView.d.ts +4 -1
- package/dist/keyedView.js +7 -1
- package/dist/kinds.js +2 -0
- package/dist/routes.d.ts +21 -1
- package/dist/routes.js +27 -1
- package/dist/server/discovery.d.ts +5 -0
- package/dist/server/discovery.js +90 -3
- package/dist/server/eventSourced.d.ts +4 -1
- package/dist/server/eventSourced.js +24 -6
- package/dist/server/http.js +2 -0
- package/dist/server/stateless.js +5 -4
- package/dist/service.d.ts +6 -0
- package/dist/service.js +26 -1
- package/dist/spec.d.ts +1 -1
- package/dist/spec.js +10 -4
- package/dist/standing.d.ts +18 -0
- package/dist/standing.js +26 -0
- package/dist/startFrom.d.ts +4 -0
- package/dist/startFrom.js +8 -1
- package/dist/testkit/index.d.ts +1 -1
- package/dist/testkit/kinds.d.ts +9 -5
- package/dist/testkit/kinds.js +17 -14
- package/dist/testkit/unit.d.ts +27 -2
- package/dist/testkit/unit.js +47 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/view.d.ts +18 -3
- package/dist/view.js +13 -4
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Client as ConnectClient, type Transport } from "@connectrpc/connect";
|
|
2
2
|
import { Client } from "./_proto/ankka/protocol/v1/client_pb.ts";
|
|
3
3
|
import { jsonCodec, type Shape } from "./codec.ts";
|
|
4
|
-
import { type Metadata } from "./effects/common.ts";
|
|
4
|
+
import { CommandError, type Metadata } from "./effects/common.ts";
|
|
5
5
|
import type { HandlerRef } from "./handlers.ts";
|
|
6
6
|
import { type ComponentKind } from "./kinds.ts";
|
|
7
7
|
import { Duration } from "./time.ts";
|
|
@@ -19,15 +19,38 @@ interface Connection {
|
|
|
19
19
|
transport: Transport | undefined;
|
|
20
20
|
stub: ConnectClient<typeof Client> | undefined;
|
|
21
21
|
}
|
|
22
|
+
/** The first protocol whose runtime can wait for a workflow's end; an older one answers `UNIMPLEMENTED`. */
|
|
23
|
+
export declare const AWAIT_SINCE = "1.18";
|
|
24
|
+
/** One part of a wait served as a stream: the wait goes on, or how it ended. */
|
|
25
|
+
export type AwaitPart<S> = {
|
|
26
|
+
readonly kind: "heartbeat";
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: "ended";
|
|
29
|
+
readonly state: S;
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: "failed";
|
|
32
|
+
readonly error: CommandError;
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: "timed-out";
|
|
35
|
+
readonly message: string;
|
|
36
|
+
};
|
|
37
|
+
/** A part as one line of JSON, which is what an `sse` route yields for it. */
|
|
38
|
+
export declare function awaitPartJson<S>(part: AwaitPart<S>): string;
|
|
22
39
|
/** One call, ready to invoke or stream. */
|
|
23
40
|
export declare class Invocation<I, R> {
|
|
24
41
|
#private;
|
|
25
|
-
constructor(connection: Connection, metadata: Metadata, kind: ComponentKind, componentId: string, entityId: string, name: string, input: Shape<I> | undefined, reply: Shape<R> | undefined);
|
|
42
|
+
constructor(connection: Connection, metadata: Metadata, kind: ComponentKind, componentId: string, entityId: string, name: string, input: Shape<I> | undefined, reply: Shape<R> | undefined, state?: Shape<unknown>);
|
|
26
43
|
/**
|
|
27
44
|
* Invokes and decodes the reply; a refusal rejects with `CommandError`. An agent's turn that waits for a
|
|
28
45
|
* person rejects with `ApprovalAwaited`; `ask` answers it instead.
|
|
29
46
|
*/
|
|
30
47
|
invoke(input?: I): Promise<R>;
|
|
48
|
+
/**
|
|
49
|
+
* Sends this command to the workflow and then waits for its end, as one call: answered with the state it
|
|
50
|
+
* ended with, within `timeoutMillis` of the command being sent. A refusal of the command rejects at once and
|
|
51
|
+
* no wait begins; the command's own reply is not kept. `state` decodes it, unless the workflow class did.
|
|
52
|
+
*/
|
|
53
|
+
thenAwaitEnd<S>(timeoutMillis: number, state?: Shape<S>): AwaitingInvocation<I, S>;
|
|
31
54
|
/** Calls an agent's handler for its outcome: the model's answer, or the approval requests the turn waits on. */
|
|
32
55
|
ask(input?: I): Promise<AgentOutcome<R>>;
|
|
33
56
|
/**
|
|
@@ -43,12 +66,31 @@ export declare class Invocation<I, R> {
|
|
|
43
66
|
kind: "awaiting-approval";
|
|
44
67
|
}>>;
|
|
45
68
|
}
|
|
69
|
+
/** A command to a workflow followed by a wait for its end, with one deadline from the send. */
|
|
70
|
+
export declare class AwaitingInvocation<I, S> {
|
|
71
|
+
#private;
|
|
72
|
+
constructor(timeoutMillis: number, send: (input: I | undefined) => Promise<unknown>, wait: (leftMillis: number) => Promise<S>);
|
|
73
|
+
/** Sends the command and waits for the end; rejects as `Calls.awaitEnd` does. */
|
|
74
|
+
invoke(input?: I): Promise<S>;
|
|
75
|
+
}
|
|
46
76
|
/** Calls on one component instance, by wire name. */
|
|
47
77
|
export declare class Calls {
|
|
48
78
|
#private;
|
|
49
|
-
constructor(connection: Connection, metadata: Metadata, kind: ComponentKind, componentId: string, entityId: string);
|
|
79
|
+
constructor(connection: Connection, metadata: Metadata, kind: ComponentKind, componentId: string, entityId: string, state?: Shape<unknown>);
|
|
50
80
|
/** `call("add-item", LineItem, Done)`: the wire name, the input shape (omit for none) and the reply shape (omit for `done`). */
|
|
51
81
|
call<I = undefined, R = unknown>(name: string, input?: Shape<I>, reply?: Shape<R>): Invocation<I, R>;
|
|
82
|
+
/**
|
|
83
|
+
* Waits for this workflow's end, for at most `timeoutMillis`, and resolves with the state it ended with — at
|
|
84
|
+
* once when it already has. A workflow that failed or was deleted rejects with `CommandError` whose code is
|
|
85
|
+
* `WORKFLOW_FAILED` and whose `details` hold `step`, `reason` and `deleted`; a wait not answered in time
|
|
86
|
+
* rejects with `TIMEOUT` and the workflow runs on. A paused workflow has not ended. There is no default.
|
|
87
|
+
*/
|
|
88
|
+
awaitEnd<S>(timeoutMillis: number, state?: Shape<S>): Promise<S>;
|
|
89
|
+
/**
|
|
90
|
+
* The same wait as a stream, for an `sse` route that must outlast the idle timeout: a heartbeat while it goes
|
|
91
|
+
* on, then exactly one `ended`, `failed` or `timed-out` part. `awaitPartJson` renders a part as a line.
|
|
92
|
+
*/
|
|
93
|
+
awaitEndParts<S>(timeoutMillis: number, state?: Shape<S>): AsyncIterable<AwaitPart<S>>;
|
|
52
94
|
}
|
|
53
95
|
/** Calls on one component instance through its class's handler table, fully typed. */
|
|
54
96
|
export declare class TypedCalls<C> {
|
|
@@ -56,7 +98,41 @@ export declare class TypedCalls<C> {
|
|
|
56
98
|
constructor(calls: Calls);
|
|
57
99
|
/** `call(ShoppingCartEntity.handlers.addItem)`: the handler's wire name and shapes come from the declaration. */
|
|
58
100
|
call<I, R>(handler: HandlerRef<C, I, R, any>): Invocation<I, R>;
|
|
101
|
+
/** For a workflow: waits for its end and resolves with its state, decoded as the class declares it. */
|
|
102
|
+
awaitEnd<S = StateOf<C>>(timeoutMillis: number): Promise<S>;
|
|
103
|
+
/** For a workflow: the same wait as a stream of parts. */
|
|
104
|
+
awaitEndParts<S = StateOf<C>>(timeoutMillis: number): AsyncIterable<AwaitPart<S>>;
|
|
105
|
+
}
|
|
106
|
+
/** The first protocol in which a view's query can be streamed and watched. */
|
|
107
|
+
export declare const VIEW_STREAMS_SINCE = "1.17";
|
|
108
|
+
/** What a watch of a view gives: a row as the query sees it now, a removal of one it gave, or the caught-up marker. */
|
|
109
|
+
export type WatchEvent<Row> = {
|
|
110
|
+
readonly kind: "row";
|
|
111
|
+
readonly key: string;
|
|
112
|
+
readonly row: Row;
|
|
113
|
+
} | {
|
|
114
|
+
readonly kind: "removed";
|
|
115
|
+
readonly key: string;
|
|
116
|
+
} | {
|
|
117
|
+
readonly kind: "caughtUp";
|
|
118
|
+
};
|
|
119
|
+
/** What a watch does past its unread bound: none slows the view. */
|
|
120
|
+
export type Overflow = "dropHead" | "dropTail" | "dropNew" | "dropAll" | "fail";
|
|
121
|
+
export interface WatchOptions {
|
|
122
|
+
/** How many unread rows the watch holds, one per key; the instance's own bound when absent. */
|
|
123
|
+
readonly unread?: number;
|
|
124
|
+
/** What it does past that bound; `dropHead`, the row changed longest ago, when absent. */
|
|
125
|
+
readonly overflow?: Overflow;
|
|
59
126
|
}
|
|
127
|
+
/** A watch ended for `reason`: `rebuilt`, `instance-stopping`, `listener-lost` or `unread`. Watch again for the rows now. */
|
|
128
|
+
export declare class WatchEnded extends Error {
|
|
129
|
+
readonly reason: string;
|
|
130
|
+
constructor(reason: string);
|
|
131
|
+
}
|
|
132
|
+
/** A workflow instance's state type, read from its `state` getter. */
|
|
133
|
+
type StateOf<C> = C extends {
|
|
134
|
+
readonly state: infer S;
|
|
135
|
+
} ? S : unknown;
|
|
60
136
|
/** View queries: rows come back as a JSON array decoded with the row shape. */
|
|
61
137
|
export declare class Views {
|
|
62
138
|
#private;
|
|
@@ -69,6 +145,23 @@ export declare class Views {
|
|
|
69
145
|
* `limit` rows, 1000 when it is not given, in the statement's own order.
|
|
70
146
|
*/
|
|
71
147
|
ask<Row>(viewId: string, name: string, values: Readonly<Record<string, string>>, row: Shape<Row>, limit?: number): Promise<Row[]>;
|
|
148
|
+
/**
|
|
149
|
+
* The rows of `all` or one of a view's declared queries as the database yields them: nothing
|
|
150
|
+
* collected, and no limit unless one is given. A statement the database ends, or a reader that
|
|
151
|
+
* takes nothing for its timeout, throws.
|
|
152
|
+
*/
|
|
153
|
+
stream<Row>(viewId: string, name: string, values: Readonly<Record<string, string>>, row: Shape<Row>, options?: {
|
|
154
|
+
readonly limit?: number;
|
|
155
|
+
}): AsyncIterable<Row>;
|
|
156
|
+
/**
|
|
157
|
+
* A watch of one of a view's declared queries, declared `{ watched: true }`: every row it matches
|
|
158
|
+
* now, then `caughtUp`, then each row as it is written and matches and a `removed` for each given row
|
|
159
|
+
* that stops matching. Throws `WatchEnded` when it ends with a reason. `unread` bounds the rows held
|
|
160
|
+
* for a reader that has not read them, and `overflow` says what happens past that bound.
|
|
161
|
+
*/
|
|
162
|
+
watch<Row>(viewId: string, name: string, values: Readonly<Record<string, string>>, row: Shape<Row>, options?: WatchOptions): AsyncIterable<WatchEvent<Row>>;
|
|
163
|
+
/** A watch of one row by its key: the row now, then `caughtUp`, then each version and a `removed` when it is deleted. */
|
|
164
|
+
watchRow<Row>(viewId: string, key: string, row: Shape<Row>, options?: WatchOptions): AsyncIterable<WatchEvent<Row>>;
|
|
72
165
|
}
|
|
73
166
|
/** What a timer fires: a component class and one of its handlers, or the same by name. */
|
|
74
167
|
export type TimerTarget = {
|
package/dist/client.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// so the sidecar records the call as a child span.
|
|
6
6
|
import { Code, ConnectError, createClient } from "@connectrpc/connect";
|
|
7
7
|
import { createGrpcTransport } from "@connectrpc/connect-node";
|
|
8
|
-
import { Client } from "./_proto/ankka/protocol/v1/client_pb.js";
|
|
8
|
+
import { Client, Overflow as ProtoOverflow } from "./_proto/ankka/protocol/v1/client_pb.js";
|
|
9
9
|
import { codecFor, jsonCodec, isCodec, textCodecs, binaryCodecs, codecForManifest } from "./codec.js";
|
|
10
10
|
import { CommandError } from "./effects/common.js";
|
|
11
11
|
import { decodeJsonValue, reviver } from "./json.js";
|
|
@@ -24,7 +24,28 @@ function stubOf(connection) {
|
|
|
24
24
|
return connection.stub;
|
|
25
25
|
}
|
|
26
26
|
function errorOf(error) {
|
|
27
|
-
return { message: error.message, code: errorCodeFromProto(error.code) };
|
|
27
|
+
return { message: error.message, code: errorCodeFromProto(error.code), details: { ...(error.details ?? {}) } };
|
|
28
|
+
}
|
|
29
|
+
/** The first protocol whose runtime can wait for a workflow's end; an older one answers `UNIMPLEMENTED`. */
|
|
30
|
+
export const AWAIT_SINCE = "1.18";
|
|
31
|
+
/** A part as one line of JSON, which is what an `sse` route yields for it. */
|
|
32
|
+
export function awaitPartJson(part) {
|
|
33
|
+
switch (part.kind) {
|
|
34
|
+
case "heartbeat":
|
|
35
|
+
return JSON.stringify({ heartbeat: true });
|
|
36
|
+
case "ended":
|
|
37
|
+
return JSON.stringify({ ended: part.state });
|
|
38
|
+
case "failed":
|
|
39
|
+
return JSON.stringify({ failed: { code: part.error.code, message: part.error.message, ...part.error.details } });
|
|
40
|
+
case "timed-out":
|
|
41
|
+
return JSON.stringify({ timedOut: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function timeoutOf(timeoutMillis) {
|
|
45
|
+
if (!(timeoutMillis > 0)) {
|
|
46
|
+
throw new CommandError({ message: `a wait for a workflow's end needs a timeout of more than zero, not ${timeoutMillis}`, code: "BAD_REQUEST" });
|
|
47
|
+
}
|
|
48
|
+
return Math.max(1, Math.floor(timeoutMillis));
|
|
28
49
|
}
|
|
29
50
|
function inputPayload(shape, input) {
|
|
30
51
|
if (shape === undefined) {
|
|
@@ -88,7 +109,9 @@ export class Invocation {
|
|
|
88
109
|
#name;
|
|
89
110
|
#input;
|
|
90
111
|
#reply;
|
|
91
|
-
|
|
112
|
+
#state;
|
|
113
|
+
constructor(connection, metadata, kind, componentId, entityId, name, input, reply, state) {
|
|
114
|
+
this.#state = state;
|
|
92
115
|
this.#connection = connection;
|
|
93
116
|
this.#metadata = metadata;
|
|
94
117
|
this.#kind = kind;
|
|
@@ -118,6 +141,16 @@ export class Invocation {
|
|
|
118
141
|
throw new ApprovalAwaited(outcome.requests);
|
|
119
142
|
return outcome.value;
|
|
120
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Sends this command to the workflow and then waits for its end, as one call: answered with the state it
|
|
146
|
+
* ended with, within `timeoutMillis` of the command being sent. A refusal of the command rejects at once and
|
|
147
|
+
* no wait begins; the command's own reply is not kept. `state` decodes it, unless the workflow class did.
|
|
148
|
+
*/
|
|
149
|
+
thenAwaitEnd(timeoutMillis, state) {
|
|
150
|
+
const shape = (state ?? this.#state);
|
|
151
|
+
const calls = new Calls(this.#connection, this.#metadata, this.#kind, this.#componentId, this.#entityId, shape);
|
|
152
|
+
return new AwaitingInvocation(timeoutMillis, (input) => this.invoke(input), (left) => calls.awaitEnd(left));
|
|
153
|
+
}
|
|
121
154
|
/** Calls an agent's handler for its outcome: the model's answer, or the approval requests the turn waits on. */
|
|
122
155
|
async ask(input) {
|
|
123
156
|
return outcomeOf(this.#reply, await stubOf(this.#connection).invoke(this.#request(input)));
|
|
@@ -156,6 +189,32 @@ export class Invocation {
|
|
|
156
189
|
}
|
|
157
190
|
}
|
|
158
191
|
}
|
|
192
|
+
/** A command to a workflow followed by a wait for its end, with one deadline from the send. */
|
|
193
|
+
export class AwaitingInvocation {
|
|
194
|
+
#timeoutMillis;
|
|
195
|
+
#send;
|
|
196
|
+
#await;
|
|
197
|
+
constructor(timeoutMillis, send, wait) {
|
|
198
|
+
this.#timeoutMillis = timeoutMillis;
|
|
199
|
+
this.#send = send;
|
|
200
|
+
this.#await = wait;
|
|
201
|
+
}
|
|
202
|
+
/** Sends the command and waits for the end; rejects as `Calls.awaitEnd` does. */
|
|
203
|
+
async invoke(input) {
|
|
204
|
+
const deadline = Date.now() + timeoutOf(this.#timeoutMillis);
|
|
205
|
+
await this.#send(input);
|
|
206
|
+
return await this.#await(Math.max(1, deadline - Date.now()));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const awaitTooOld = (failure) => {
|
|
210
|
+
if (failure instanceof ConnectError && failure.code === Code.Unimplemented) {
|
|
211
|
+
throw new CommandError({
|
|
212
|
+
message: `the runtime beside this process is too old for waiting for a workflow's end, which needs protocol ${AWAIT_SINCE}; this process speaks ${PROTOCOL_VERSION}: ${failure.rawMessage}`,
|
|
213
|
+
code: "UNAVAILABLE",
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
throw failure;
|
|
217
|
+
};
|
|
159
218
|
/** Calls on one component instance, by wire name. */
|
|
160
219
|
export class Calls {
|
|
161
220
|
#connection;
|
|
@@ -163,16 +222,67 @@ export class Calls {
|
|
|
163
222
|
#kind;
|
|
164
223
|
#componentId;
|
|
165
224
|
#entityId;
|
|
166
|
-
|
|
225
|
+
#state;
|
|
226
|
+
constructor(connection, metadata, kind, componentId, entityId, state) {
|
|
167
227
|
this.#connection = connection;
|
|
168
228
|
this.#metadata = metadata;
|
|
169
229
|
this.#kind = kind;
|
|
170
230
|
this.#componentId = componentId;
|
|
171
231
|
this.#entityId = entityId;
|
|
232
|
+
this.#state = state;
|
|
172
233
|
}
|
|
173
234
|
/** `call("add-item", LineItem, Done)`: the wire name, the input shape (omit for none) and the reply shape (omit for `done`). */
|
|
174
235
|
call(name, input, reply) {
|
|
175
|
-
return new Invocation(this.#connection, this.#metadata, this.#kind, this.#componentId, this.#entityId, name, input, reply);
|
|
236
|
+
return new Invocation(this.#connection, this.#metadata, this.#kind, this.#componentId, this.#entityId, name, input, reply, this.#state);
|
|
237
|
+
}
|
|
238
|
+
#awaitRequest(timeoutMillis) {
|
|
239
|
+
return {
|
|
240
|
+
componentId: this.#componentId,
|
|
241
|
+
entityId: this.#entityId,
|
|
242
|
+
timeoutMillis: BigInt(timeoutOf(timeoutMillis)),
|
|
243
|
+
metadata: metadataToProto(this.#metadata),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Waits for this workflow's end, for at most `timeoutMillis`, and resolves with the state it ended with — at
|
|
248
|
+
* once when it already has. A workflow that failed or was deleted rejects with `CommandError` whose code is
|
|
249
|
+
* `WORKFLOW_FAILED` and whose `details` hold `step`, `reason` and `deleted`; a wait not answered in time
|
|
250
|
+
* rejects with `TIMEOUT` and the workflow runs on. A paused workflow has not ended. There is no default.
|
|
251
|
+
*/
|
|
252
|
+
async awaitEnd(timeoutMillis, state) {
|
|
253
|
+
const shape = (state ?? this.#state);
|
|
254
|
+
const answer = await stubOf(this.#connection).awaitEnd(this.#awaitRequest(timeoutMillis)).catch(awaitTooOld);
|
|
255
|
+
const outcome = outcomeOf(shape, answer);
|
|
256
|
+
if (outcome.kind !== "answered")
|
|
257
|
+
throw new CommandError({ message: "a workflow answered a wait as an agent", code: "INTERNAL" });
|
|
258
|
+
return outcome.value;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The same wait as a stream, for an `sse` route that must outlast the idle timeout: a heartbeat while it goes
|
|
262
|
+
* on, then exactly one `ended`, `failed` or `timed-out` part. `awaitPartJson` renders a part as a line.
|
|
263
|
+
*/
|
|
264
|
+
async *awaitEndParts(timeoutMillis, state) {
|
|
265
|
+
const shape = (state ?? this.#state);
|
|
266
|
+
try {
|
|
267
|
+
for await (const token of stubOf(this.#connection).awaitEndStream(this.#awaitRequest(timeoutMillis))) {
|
|
268
|
+
switch (token.token.case) {
|
|
269
|
+
case "heartbeat":
|
|
270
|
+
yield { kind: "heartbeat" };
|
|
271
|
+
break;
|
|
272
|
+
case "ended":
|
|
273
|
+
yield { kind: "ended", state: decodeReply(shape, token.token.value) };
|
|
274
|
+
return;
|
|
275
|
+
case "failed": {
|
|
276
|
+
const error = new CommandError(errorOf(token.token.value));
|
|
277
|
+
yield error.code === "TIMEOUT" ? { kind: "timed-out", message: error.message } : { kind: "failed", error };
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (failure) {
|
|
284
|
+
awaitTooOld(failure);
|
|
285
|
+
}
|
|
176
286
|
}
|
|
177
287
|
}
|
|
178
288
|
/** Calls on one component instance through its class's handler table, fully typed. */
|
|
@@ -185,6 +295,39 @@ export class TypedCalls {
|
|
|
185
295
|
call(handler) {
|
|
186
296
|
return this.#calls.call(handler.name, handler.input, handler.reply);
|
|
187
297
|
}
|
|
298
|
+
/** For a workflow: waits for its end and resolves with its state, decoded as the class declares it. */
|
|
299
|
+
awaitEnd(timeoutMillis) {
|
|
300
|
+
return this.#calls.awaitEnd(timeoutMillis);
|
|
301
|
+
}
|
|
302
|
+
/** For a workflow: the same wait as a stream of parts. */
|
|
303
|
+
awaitEndParts(timeoutMillis) {
|
|
304
|
+
return this.#calls.awaitEndParts(timeoutMillis);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** The first protocol in which a view's query can be streamed and watched. */
|
|
308
|
+
export const VIEW_STREAMS_SINCE = "1.17";
|
|
309
|
+
/** A watch ended for `reason`: `rebuilt`, `instance-stopping`, `listener-lost` or `unread`. Watch again for the rows now. */
|
|
310
|
+
export class WatchEnded extends Error {
|
|
311
|
+
reason;
|
|
312
|
+
constructor(reason) {
|
|
313
|
+
super(`the watch ended: ${reason}`);
|
|
314
|
+
this.name = "WatchEnded";
|
|
315
|
+
this.reason = reason;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function overflowToProto(overflow) {
|
|
319
|
+
switch (overflow) {
|
|
320
|
+
case "dropHead":
|
|
321
|
+
return ProtoOverflow.DROP_HEAD;
|
|
322
|
+
case "dropTail":
|
|
323
|
+
return ProtoOverflow.DROP_TAIL;
|
|
324
|
+
case "dropNew":
|
|
325
|
+
return ProtoOverflow.DROP_NEW;
|
|
326
|
+
case "dropAll":
|
|
327
|
+
return ProtoOverflow.DROP_ALL;
|
|
328
|
+
case "fail":
|
|
329
|
+
return ProtoOverflow.FAIL;
|
|
330
|
+
}
|
|
188
331
|
}
|
|
189
332
|
/** View queries: rows come back as a JSON array decoded with the row shape. */
|
|
190
333
|
export class Views {
|
|
@@ -219,6 +362,83 @@ export class Views {
|
|
|
219
362
|
});
|
|
220
363
|
return Views.#rows(answer, row);
|
|
221
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* The rows of `all` or one of a view's declared queries as the database yields them: nothing
|
|
367
|
+
* collected, and no limit unless one is given. A statement the database ends, or a reader that
|
|
368
|
+
* takes nothing for its timeout, throws.
|
|
369
|
+
*/
|
|
370
|
+
async *stream(viewId, name, values, row, options = {}) {
|
|
371
|
+
const frames = stubOf(this.#connection).queryStream({
|
|
372
|
+
viewId,
|
|
373
|
+
name,
|
|
374
|
+
metadata: metadataToProto(this.#metadata),
|
|
375
|
+
values: { ...values },
|
|
376
|
+
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
377
|
+
});
|
|
378
|
+
for await (const event of Views.#frames(frames, row))
|
|
379
|
+
if (event.kind === "row")
|
|
380
|
+
yield event.row;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* A watch of one of a view's declared queries, declared `{ watched: true }`: every row it matches
|
|
384
|
+
* now, then `caughtUp`, then each row as it is written and matches and a `removed` for each given row
|
|
385
|
+
* that stops matching. Throws `WatchEnded` when it ends with a reason. `unread` bounds the rows held
|
|
386
|
+
* for a reader that has not read them, and `overflow` says what happens past that bound.
|
|
387
|
+
*/
|
|
388
|
+
async *watch(viewId, name, values, row, options = {}) {
|
|
389
|
+
const frames = stubOf(this.#connection).watch({
|
|
390
|
+
viewId,
|
|
391
|
+
metadata: metadataToProto(this.#metadata),
|
|
392
|
+
target: { case: "named", value: { name, values: { ...values } } },
|
|
393
|
+
...(options.unread !== undefined ? { unreadBound: options.unread } : {}),
|
|
394
|
+
overflow: overflowToProto(options.overflow ?? "dropHead"),
|
|
395
|
+
});
|
|
396
|
+
yield* Views.#frames(frames, row);
|
|
397
|
+
}
|
|
398
|
+
/** A watch of one row by its key: the row now, then `caughtUp`, then each version and a `removed` when it is deleted. */
|
|
399
|
+
async *watchRow(viewId, key, row, options = {}) {
|
|
400
|
+
const frames = stubOf(this.#connection).watch({
|
|
401
|
+
viewId,
|
|
402
|
+
metadata: metadataToProto(this.#metadata),
|
|
403
|
+
target: { case: "key", value: key },
|
|
404
|
+
...(options.unread !== undefined ? { unreadBound: options.unread } : {}),
|
|
405
|
+
overflow: overflowToProto(options.overflow ?? "dropHead"),
|
|
406
|
+
});
|
|
407
|
+
yield* Views.#frames(frames, row);
|
|
408
|
+
}
|
|
409
|
+
static async *#frames(frames, row) {
|
|
410
|
+
const codec = isCodec(row) ? row : codecFor(row);
|
|
411
|
+
try {
|
|
412
|
+
for await (const frame of frames) {
|
|
413
|
+
switch (frame.frame.case) {
|
|
414
|
+
case "row":
|
|
415
|
+
yield { kind: "row", key: frame.frame.value.key, row: codec.decode(frame.frame.value.payload?.data ?? new Uint8Array()) };
|
|
416
|
+
break;
|
|
417
|
+
case "removed":
|
|
418
|
+
yield { kind: "removed", key: frame.frame.value.key };
|
|
419
|
+
break;
|
|
420
|
+
case "caughtUp":
|
|
421
|
+
yield { kind: "caughtUp" };
|
|
422
|
+
break;
|
|
423
|
+
case "ended":
|
|
424
|
+
throw new WatchEnded(frame.frame.value.reason);
|
|
425
|
+
case "failed":
|
|
426
|
+
throw new CommandError(errorOf(frame.frame.value));
|
|
427
|
+
default:
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (failure) {
|
|
433
|
+
if (failure instanceof ConnectError && failure.code === Code.Unimplemented) {
|
|
434
|
+
throw new CommandError({
|
|
435
|
+
message: `the runtime beside this process does not offer view streams, which need protocol ${VIEW_STREAMS_SINCE} (this SDK speaks ${PROTOCOL_VERSION}): ${failure.rawMessage}`,
|
|
436
|
+
code: "INTERNAL",
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
throw failure;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
222
442
|
static #rows(answer, row) {
|
|
223
443
|
switch (answer.result.case) {
|
|
224
444
|
case "rows": {
|
|
@@ -517,7 +737,9 @@ export class ComponentClient {
|
|
|
517
737
|
}
|
|
518
738
|
/** Typed calls through a component class's handler table: `client.of(ShoppingCartEntity, cartId).call(ShoppingCartEntity.handlers.addItem).invoke(item)`. */
|
|
519
739
|
of(component, entityId) {
|
|
520
|
-
|
|
740
|
+
// A workflow class declares its state's shape: what a wait for its end decodes the state with.
|
|
741
|
+
const state = component.state;
|
|
742
|
+
return new TypedCalls(new Calls(this.#connection, this.#metadata, component.prototype._kind, component.componentId, entityId, state));
|
|
521
743
|
}
|
|
522
744
|
}
|
|
523
745
|
/** A client for unit tests: every call throws, naming what was attempted. */
|
package/dist/consumer.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Metadata } from "./effects/common.ts";
|
|
|
5
5
|
import { type ComponentClient, type ComponentRef, type Secrets } from "./client.ts";
|
|
6
6
|
import { ConsumerEffects, type ConsumerEffect } from "./effects/stateless.ts";
|
|
7
7
|
import { type Services } from "./services.ts";
|
|
8
|
+
import type { Standing } from "./standing.ts";
|
|
8
9
|
type MaybePromise<T> = T | Promise<T>;
|
|
9
10
|
/** The metadata entry a runtime sets on a consumer's request to say what it speaks: its protocol version. */
|
|
10
11
|
export declare const PROTOCOL_KEY = "ankka.protocol";
|
|
@@ -26,8 +27,14 @@ export declare abstract class Consumer<M, Out = never> {
|
|
|
26
27
|
/** The message's metadata: `ce-subject` is the source instance's id, `ankka.sequence` its sequence number. */
|
|
27
28
|
get metadata(): Metadata;
|
|
28
29
|
get subject(): string;
|
|
30
|
+
/** 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. */
|
|
31
|
+
get standing(): Standing | undefined;
|
|
29
32
|
/** The change's sequence number: an event's, or a key value state's revision. `0n` for a topic's message. */
|
|
30
33
|
get sequenceNumber(): bigint | undefined;
|
|
34
|
+
/** Of a replicated entity's change (1.19): the installation that wrote it. `undefined` otherwise. */
|
|
35
|
+
get installation(): string | undefined;
|
|
36
|
+
/** Whether the change was written at this installation: `false` only for a replicated entity's mirrored change. */
|
|
37
|
+
get localOrigin(): boolean;
|
|
31
38
|
get client(): ComponentClient;
|
|
32
39
|
/** The service's secret store: values kept encrypted in the service's own database, never in a journal or a view. */
|
|
33
40
|
get secrets(): Secrets;
|
|
@@ -43,7 +50,7 @@ export declare abstract class Consumer<M, Out = never> {
|
|
|
43
50
|
/** The source was deleted. Ignored unless this says otherwise. */
|
|
44
51
|
onDelete(): MaybePromise<ConsumerEffect<Out>>;
|
|
45
52
|
/** @internal */
|
|
46
|
-
_bind(metadata: Metadata, client: ComponentClient): void;
|
|
53
|
+
_bind(metadata: Metadata, client: ComponentClient, standing?: Standing): void;
|
|
47
54
|
}
|
|
48
55
|
export interface ConsumerClass<M = unknown, Out = unknown, C extends Consumer<M, Out> = Consumer<M, Out>> {
|
|
49
56
|
new (): C;
|
|
@@ -58,6 +65,11 @@ export interface ConsumerClass<M = unknown, Out = unknown, C extends Consumer<M,
|
|
|
58
65
|
readonly contract?: Contract;
|
|
59
66
|
/** The declared broker the topic read is on; absent is the installation's. */
|
|
60
67
|
readonly broker?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Another project's topic (1.15): the id of the project whose topic `topic` is. The broker serves it
|
|
70
|
+
* while that project grants this service consume on it.
|
|
71
|
+
*/
|
|
72
|
+
readonly project?: string;
|
|
61
73
|
/** Whether the partitions an instance holds are handled at once, each in order. */
|
|
62
74
|
readonly parallel?: boolean;
|
|
63
75
|
readonly message: Shape<M>;
|
|
@@ -65,5 +77,10 @@ export interface ConsumerClass<M = unknown, Out = unknown, C extends Consumer<M,
|
|
|
65
77
|
readonly out?: Shape<Out>;
|
|
66
78
|
/** The topic produced to: its name, or a `Publication` with the contract stated for it and its broker. */
|
|
67
79
|
readonly producesTo?: string | Publication;
|
|
80
|
+
/**
|
|
81
|
+
* A consumer that publishes is given a replicated entity's changes written at other installations
|
|
82
|
+
* too only when this says so (1.19); otherwise each is published once, where it was written.
|
|
83
|
+
*/
|
|
84
|
+
readonly mirroredChanges?: boolean;
|
|
68
85
|
}
|
|
69
86
|
export {};
|
package/dist/consumer.js
CHANGED
|
@@ -31,6 +31,7 @@ export function sequenceNumberOf(metadata) {
|
|
|
31
31
|
export class Consumer {
|
|
32
32
|
effects = new ConsumerEffects();
|
|
33
33
|
#metadata = {};
|
|
34
|
+
#standing = undefined;
|
|
34
35
|
#client;
|
|
35
36
|
/** The message's metadata: `ce-subject` is the source instance's id, `ankka.sequence` its sequence number. */
|
|
36
37
|
get metadata() {
|
|
@@ -39,10 +40,22 @@ export class Consumer {
|
|
|
39
40
|
get subject() {
|
|
40
41
|
return this.#metadata["ce-subject"] ?? "";
|
|
41
42
|
}
|
|
43
|
+
/** 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. */
|
|
44
|
+
get standing() {
|
|
45
|
+
return this.#standing;
|
|
46
|
+
}
|
|
42
47
|
/** The change's sequence number: an event's, or a key value state's revision. `0n` for a topic's message. */
|
|
43
48
|
get sequenceNumber() {
|
|
44
49
|
return sequenceNumberOf(this.#metadata);
|
|
45
50
|
}
|
|
51
|
+
/** Of a replicated entity's change (1.19): the installation that wrote it. `undefined` otherwise. */
|
|
52
|
+
get installation() {
|
|
53
|
+
return this.#metadata["ankka.installation"];
|
|
54
|
+
}
|
|
55
|
+
/** Whether the change was written at this installation: `false` only for a replicated entity's mirrored change. */
|
|
56
|
+
get localOrigin() {
|
|
57
|
+
return this.#metadata["ankka.local"] !== "false";
|
|
58
|
+
}
|
|
46
59
|
get client() {
|
|
47
60
|
if (!this.#client)
|
|
48
61
|
throw new Error("client is only available inside onMessage or onDelete");
|
|
@@ -75,8 +88,9 @@ export class Consumer {
|
|
|
75
88
|
return this.effects.ignore();
|
|
76
89
|
}
|
|
77
90
|
/** @internal */
|
|
78
|
-
_bind(metadata, client) {
|
|
91
|
+
_bind(metadata, client, standing) {
|
|
79
92
|
this.#metadata = metadata;
|
|
93
|
+
this.#standing = standing;
|
|
80
94
|
this.#client = client;
|
|
81
95
|
}
|
|
82
96
|
}
|
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 = {};
|
package/dist/contract.d.ts
CHANGED
|
@@ -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];
|
package/dist/effects/common.d.ts
CHANGED
|
@@ -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
|
}
|