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
package/dist/json.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// The JSON encoding, schema-directed, byte for byte what the Scala codecs write (protocol/ENCODING.md).
|
|
2
|
+
//
|
|
3
|
+
// Writing is the SDK's own: fields in declaration order, every field written, `null` for an absent
|
|
4
|
+
// option, doubles rendered as `Double.toString` does (`1.0`, `1.0E10`), a `long` as digits however
|
|
5
|
+
// large. `JSON.stringify` could do none of that. Reading is `JSON.parse` with the reviver's source-text
|
|
6
|
+
// access (Node 21+) so an integer past 2⁵³ becomes a `BigInt` instead of a rounded number, followed by a
|
|
7
|
+
// schema-directed decode that converts, checks and names the path of anything it refuses.
|
|
8
|
+
import { Duration, Instant, LocalDate, LocalDateTime } from "./time.js";
|
|
9
|
+
import { done, resolve, describe } from "./schema.js";
|
|
10
|
+
export class EncodingError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "EncodingError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class DecodingError extends Error {
|
|
17
|
+
/** Where in the document the refusal is, like `items[2].quantity`; empty at the top level. */
|
|
18
|
+
path;
|
|
19
|
+
constructor(message, path) {
|
|
20
|
+
super(path ? `${path}: ${message}` : message);
|
|
21
|
+
this.name = "DecodingError";
|
|
22
|
+
this.path = path;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** `Double.toString` as Scala prints it: plain between 1e-3 and 1e7 with at least one fractional digit, scientific outside. */
|
|
26
|
+
export function renderDouble(d) {
|
|
27
|
+
if (!Number.isFinite(d))
|
|
28
|
+
throw new EncodingError("NaN and infinities are not JSON");
|
|
29
|
+
if (d === 0)
|
|
30
|
+
return Object.is(d, -0) ? "-0.0" : "0.0";
|
|
31
|
+
const magnitude = Math.abs(d);
|
|
32
|
+
if (magnitude >= 1e-3 && magnitude < 1e7) {
|
|
33
|
+
const plain = String(d);
|
|
34
|
+
if (plain.includes("e"))
|
|
35
|
+
return fromExponential(d); // not reachable in this range for finite doubles, kept for safety
|
|
36
|
+
return plain.includes(".") ? plain : plain + ".0";
|
|
37
|
+
}
|
|
38
|
+
return fromExponential(d);
|
|
39
|
+
}
|
|
40
|
+
function fromExponential(d) {
|
|
41
|
+
const [mantissa, exponent] = d.toExponential().split("e");
|
|
42
|
+
const m = mantissa.includes(".") ? mantissa : mantissa + ".0";
|
|
43
|
+
return `${m}E${Number(exponent)}`;
|
|
44
|
+
}
|
|
45
|
+
function base64Encode(bytes) {
|
|
46
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
|
|
47
|
+
}
|
|
48
|
+
function base64Decode(text) {
|
|
49
|
+
return new Uint8Array(Buffer.from(text, "base64"));
|
|
50
|
+
}
|
|
51
|
+
/** The JSON text for `value` under `schema`. */
|
|
52
|
+
export function writeJson(schema, value) {
|
|
53
|
+
const out = [];
|
|
54
|
+
write(schema, value, out, "");
|
|
55
|
+
return out.join("");
|
|
56
|
+
}
|
|
57
|
+
function write(schema, value, out, path) {
|
|
58
|
+
const r = resolve(schema);
|
|
59
|
+
switch (r.kind) {
|
|
60
|
+
case "string":
|
|
61
|
+
if (typeof value !== "string")
|
|
62
|
+
throw new EncodingError(`${at(path)}expected a string, got ${kindOf(value)}`);
|
|
63
|
+
out.push(JSON.stringify(value));
|
|
64
|
+
return;
|
|
65
|
+
case "int":
|
|
66
|
+
if (typeof value === "bigint" && value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
67
|
+
out.push(value.toString());
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
|
|
71
|
+
throw new EncodingError(`${at(path)}expected a whole number within ±2^53 for int, got ${kindOf(value)}`);
|
|
72
|
+
}
|
|
73
|
+
out.push(String(value));
|
|
74
|
+
return;
|
|
75
|
+
case "long":
|
|
76
|
+
if (typeof value === "bigint") {
|
|
77
|
+
out.push(value.toString());
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
81
|
+
out.push(String(value));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
throw new EncodingError(`${at(path)}expected a bigint (or a safe integer) for long, got ${kindOf(value)}`);
|
|
85
|
+
case "double":
|
|
86
|
+
if (typeof value === "bigint") {
|
|
87
|
+
out.push(renderDouble(Number(value)));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (typeof value !== "number")
|
|
91
|
+
throw new EncodingError(`${at(path)}expected a number for double, got ${kindOf(value)}`);
|
|
92
|
+
out.push(renderDouble(value));
|
|
93
|
+
return;
|
|
94
|
+
case "boolean":
|
|
95
|
+
if (typeof value !== "boolean")
|
|
96
|
+
throw new EncodingError(`${at(path)}expected a boolean, got ${kindOf(value)}`);
|
|
97
|
+
out.push(value ? "true" : "false");
|
|
98
|
+
return;
|
|
99
|
+
case "instant": {
|
|
100
|
+
const instant = value instanceof Instant ? value : value instanceof Date ? Instant.fromDate(value) : undefined;
|
|
101
|
+
if (!instant)
|
|
102
|
+
throw new EncodingError(`${at(path)}expected an Instant (or a Date), got ${kindOf(value)}`);
|
|
103
|
+
out.push(JSON.stringify(instant.toString()));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
case "duration":
|
|
107
|
+
if (!(value instanceof Duration))
|
|
108
|
+
throw new EncodingError(`${at(path)}expected a Duration, got ${kindOf(value)}`);
|
|
109
|
+
out.push(JSON.stringify(value.toString()));
|
|
110
|
+
return;
|
|
111
|
+
case "localDate":
|
|
112
|
+
if (!(value instanceof LocalDate))
|
|
113
|
+
throw new EncodingError(`${at(path)}expected a LocalDate, got ${kindOf(value)}`);
|
|
114
|
+
out.push(JSON.stringify(value.toString()));
|
|
115
|
+
return;
|
|
116
|
+
case "localDateTime":
|
|
117
|
+
if (!(value instanceof LocalDateTime))
|
|
118
|
+
throw new EncodingError(`${at(path)}expected a LocalDateTime, got ${kindOf(value)}`);
|
|
119
|
+
out.push(JSON.stringify(value.toString()));
|
|
120
|
+
return;
|
|
121
|
+
case "bytes":
|
|
122
|
+
if (!(value instanceof Uint8Array))
|
|
123
|
+
throw new EncodingError(`${at(path)}expected a Uint8Array, got ${kindOf(value)}`);
|
|
124
|
+
out.push(JSON.stringify(base64Encode(value)));
|
|
125
|
+
return;
|
|
126
|
+
case "unit":
|
|
127
|
+
case "done":
|
|
128
|
+
out.push("null");
|
|
129
|
+
return;
|
|
130
|
+
case "option":
|
|
131
|
+
if (value === null || value === undefined)
|
|
132
|
+
out.push("null");
|
|
133
|
+
else
|
|
134
|
+
write(r.inner, value, out, path);
|
|
135
|
+
return;
|
|
136
|
+
case "list": {
|
|
137
|
+
if (!Array.isArray(value))
|
|
138
|
+
throw new EncodingError(`${at(path)}expected an array, got ${kindOf(value)}`);
|
|
139
|
+
out.push("[");
|
|
140
|
+
value.forEach((item, i) => {
|
|
141
|
+
if (i > 0)
|
|
142
|
+
out.push(",");
|
|
143
|
+
write(r.inner, item, out, `${path}[${i}]`);
|
|
144
|
+
});
|
|
145
|
+
out.push("]");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
case "stringMap": {
|
|
149
|
+
if (!isPlainObject(value))
|
|
150
|
+
throw new EncodingError(`${at(path)}expected an object for a string-keyed map, got ${kindOf(value)}`);
|
|
151
|
+
out.push("{");
|
|
152
|
+
let first = true;
|
|
153
|
+
for (const [k, v] of Object.entries(value)) {
|
|
154
|
+
if (!first)
|
|
155
|
+
out.push(",");
|
|
156
|
+
first = false;
|
|
157
|
+
out.push(JSON.stringify(k), ":");
|
|
158
|
+
write(r.inner, v, out, path ? `${path}.${k}` : k);
|
|
159
|
+
}
|
|
160
|
+
out.push("}");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
case "record": {
|
|
164
|
+
if (!isPlainObject(value))
|
|
165
|
+
throw new EncodingError(`${at(path)}expected an object for ${describe(r)}, got ${kindOf(value)}`);
|
|
166
|
+
out.push("{");
|
|
167
|
+
writeFields(r.fields, value, out, path, false);
|
|
168
|
+
out.push("}");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
case "sumType": {
|
|
172
|
+
if (!isPlainObject(value) || typeof value["type"] !== "string") {
|
|
173
|
+
throw new EncodingError(`${at(path)}expected an object with a "type" for ${describe(r)}, got ${kindOf(value)}`);
|
|
174
|
+
}
|
|
175
|
+
const caseName = value["type"];
|
|
176
|
+
const fields = r.cases[caseName];
|
|
177
|
+
if (!fields)
|
|
178
|
+
throw new EncodingError(`${at(path)}${describe(r)} has no case ${JSON.stringify(caseName)}`);
|
|
179
|
+
out.push("{", JSON.stringify("type"), ":", JSON.stringify(caseName));
|
|
180
|
+
writeFields(fields, value, out, path, true);
|
|
181
|
+
out.push("}");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
case "enumeration": {
|
|
185
|
+
if (typeof value !== "string" || !r.values.includes(value)) {
|
|
186
|
+
throw new EncodingError(`${at(path)}expected one of ${r.values.map((v) => JSON.stringify(v)).join(", ")} for ${describe(r)}, got ${kindOf(value)}`);
|
|
187
|
+
}
|
|
188
|
+
out.push(`{"type":${JSON.stringify(value)}}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function writeFields(fields, value, out, path, continuing) {
|
|
194
|
+
let first = !continuing;
|
|
195
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
196
|
+
if (!first)
|
|
197
|
+
out.push(",");
|
|
198
|
+
first = false;
|
|
199
|
+
out.push(JSON.stringify(name), ":");
|
|
200
|
+
const v = value[name];
|
|
201
|
+
if (v === undefined && resolve(field).kind !== "option") {
|
|
202
|
+
throw new EncodingError(`${at(path ? `${path}.${name}` : name)}missing required field`);
|
|
203
|
+
}
|
|
204
|
+
write(field, v, out, path ? `${path}.${name}` : name);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Decodes `text` under `schema`. Lenient on unknown fields and field order; strict on what is required. */
|
|
208
|
+
export function readJson(schema, text) {
|
|
209
|
+
let raw;
|
|
210
|
+
try {
|
|
211
|
+
raw = JSON.parse(text, reviver);
|
|
212
|
+
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
throw new DecodingError(`not JSON: ${e.message}`, "");
|
|
215
|
+
}
|
|
216
|
+
return decode(schema, raw, "");
|
|
217
|
+
}
|
|
218
|
+
/** Applies the schema to a value `JSON.parse` produced (with the same reviver), for callers that hold parsed JSON. */
|
|
219
|
+
export function decodeJsonValue(schema, raw) {
|
|
220
|
+
return decode(schema, raw, "");
|
|
221
|
+
}
|
|
222
|
+
/** The reviver: an integer past 2⁵³ arrives as a `BigInt` made from its source text, everything else as usual. */
|
|
223
|
+
export function reviver(_key, value, context) {
|
|
224
|
+
if (typeof value === "number" && Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
225
|
+
const source = context?.source;
|
|
226
|
+
if (source && /^-?\d+$/.test(source))
|
|
227
|
+
return BigInt(source);
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
function decode(schema, raw, path) {
|
|
232
|
+
const r = resolve(schema);
|
|
233
|
+
switch (r.kind) {
|
|
234
|
+
case "string":
|
|
235
|
+
if (typeof raw !== "string")
|
|
236
|
+
throw new DecodingError(`expected a string, got ${kindOf(raw)}`, path);
|
|
237
|
+
return raw;
|
|
238
|
+
case "int":
|
|
239
|
+
if (typeof raw === "bigint") {
|
|
240
|
+
if (raw < BigInt(Number.MIN_SAFE_INTEGER) || raw > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
241
|
+
throw new DecodingError(`${raw} does not fit an int (declare the field as long)`, path);
|
|
242
|
+
}
|
|
243
|
+
return Number(raw);
|
|
244
|
+
}
|
|
245
|
+
if (typeof raw !== "number" || !Number.isInteger(raw))
|
|
246
|
+
throw new DecodingError(`expected a whole number, got ${kindOf(raw)}`, path);
|
|
247
|
+
return raw;
|
|
248
|
+
case "long":
|
|
249
|
+
if (typeof raw === "bigint")
|
|
250
|
+
return raw;
|
|
251
|
+
if (typeof raw === "number" && Number.isInteger(raw))
|
|
252
|
+
return BigInt(raw);
|
|
253
|
+
throw new DecodingError(`expected a whole number, got ${kindOf(raw)}`, path);
|
|
254
|
+
case "double":
|
|
255
|
+
if (typeof raw === "bigint")
|
|
256
|
+
return Number(raw);
|
|
257
|
+
if (typeof raw !== "number")
|
|
258
|
+
throw new DecodingError(`expected a number, got ${kindOf(raw)}`, path);
|
|
259
|
+
return raw;
|
|
260
|
+
case "boolean":
|
|
261
|
+
if (typeof raw !== "boolean")
|
|
262
|
+
throw new DecodingError(`expected a boolean, got ${kindOf(raw)}`, path);
|
|
263
|
+
return raw;
|
|
264
|
+
case "instant":
|
|
265
|
+
return parseWith(raw, path, "an ISO-8601 instant", Instant.parse);
|
|
266
|
+
case "duration":
|
|
267
|
+
return parseWith(raw, path, "an ISO-8601 duration", Duration.parse);
|
|
268
|
+
case "localDate":
|
|
269
|
+
return parseWith(raw, path, "an ISO-8601 date", LocalDate.parse);
|
|
270
|
+
case "localDateTime":
|
|
271
|
+
return parseWith(raw, path, "an ISO-8601 date-time", LocalDateTime.parse);
|
|
272
|
+
case "bytes":
|
|
273
|
+
if (typeof raw !== "string")
|
|
274
|
+
throw new DecodingError(`expected base64 text, got ${kindOf(raw)}`, path);
|
|
275
|
+
return base64Decode(raw);
|
|
276
|
+
case "unit":
|
|
277
|
+
return undefined;
|
|
278
|
+
case "done":
|
|
279
|
+
return done;
|
|
280
|
+
case "option":
|
|
281
|
+
return raw === null || raw === undefined ? null : decode(r.inner, raw, path);
|
|
282
|
+
case "list":
|
|
283
|
+
if (!Array.isArray(raw))
|
|
284
|
+
throw new DecodingError(`expected an array, got ${kindOf(raw)}`, path);
|
|
285
|
+
return raw.map((item, i) => decode(r.inner, item, `${path}[${i}]`));
|
|
286
|
+
case "stringMap": {
|
|
287
|
+
if (!isPlainObject(raw))
|
|
288
|
+
throw new DecodingError(`expected an object, got ${kindOf(raw)}`, path);
|
|
289
|
+
const out = {};
|
|
290
|
+
for (const [k, v] of Object.entries(raw))
|
|
291
|
+
out[k] = decode(r.inner, v, path ? `${path}.${k}` : k);
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
case "record": {
|
|
295
|
+
if (!isPlainObject(raw))
|
|
296
|
+
throw new DecodingError(`expected an object for ${describe(r)}, got ${kindOf(raw)}`, path);
|
|
297
|
+
return decodeFields(r.fields, raw, path);
|
|
298
|
+
}
|
|
299
|
+
case "sumType": {
|
|
300
|
+
if (!isPlainObject(raw))
|
|
301
|
+
throw new DecodingError(`expected an object for ${describe(r)}, got ${kindOf(raw)}`, path);
|
|
302
|
+
const caseName = raw["type"];
|
|
303
|
+
if (typeof caseName !== "string")
|
|
304
|
+
throw new DecodingError(`${describe(r)} needs a "type"`, path);
|
|
305
|
+
const fields = r.cases[caseName];
|
|
306
|
+
if (!fields)
|
|
307
|
+
throw new DecodingError(`${describe(r)} has no case ${JSON.stringify(caseName)}`, path);
|
|
308
|
+
return { type: caseName, ...decodeFields(fields, raw, path) };
|
|
309
|
+
}
|
|
310
|
+
case "enumeration": {
|
|
311
|
+
const name = typeof raw === "string" ? raw : isPlainObject(raw) ? raw["type"] : undefined;
|
|
312
|
+
if (typeof name !== "string" || !r.values.includes(name)) {
|
|
313
|
+
throw new DecodingError(`expected one of ${r.values.map((v) => JSON.stringify(v)).join(", ")} for ${describe(r)}, got ${JSON.stringify(raw)}`, path);
|
|
314
|
+
}
|
|
315
|
+
return name;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function decodeFields(fields, raw, path) {
|
|
320
|
+
const out = {};
|
|
321
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
322
|
+
const fieldPath = path ? `${path}.${name}` : name;
|
|
323
|
+
const v = raw[name];
|
|
324
|
+
if (v === undefined) {
|
|
325
|
+
if (resolve(field).kind === "option")
|
|
326
|
+
out[name] = null;
|
|
327
|
+
else
|
|
328
|
+
throw new DecodingError("missing required field", fieldPath);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
out[name] = decode(field, v, fieldPath);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
function parseWith(raw, path, what, parse) {
|
|
337
|
+
if (typeof raw !== "string")
|
|
338
|
+
throw new DecodingError(`expected ${what}, got ${kindOf(raw)}`, path);
|
|
339
|
+
try {
|
|
340
|
+
return parse(raw);
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
throw new DecodingError(e.message, path);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function isPlainObject(value) {
|
|
347
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
348
|
+
}
|
|
349
|
+
function kindOf(value) {
|
|
350
|
+
if (value === null)
|
|
351
|
+
return "null";
|
|
352
|
+
if (Array.isArray(value))
|
|
353
|
+
return "an array";
|
|
354
|
+
if (value instanceof Uint8Array)
|
|
355
|
+
return "bytes";
|
|
356
|
+
if (typeof value === "object")
|
|
357
|
+
return "an object";
|
|
358
|
+
return typeof value === "bigint" ? "a bigint" : `${/^[aeiou]/.test(typeof value) ? "an" : "a"} ${typeof value}`;
|
|
359
|
+
}
|
|
360
|
+
function at(path) {
|
|
361
|
+
return path ? `${path}: ` : "";
|
|
362
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 { KeyValueEffects } from "./effects/keyValue.ts";
|
|
6
|
+
export declare abstract class KeyValueEntity<S> {
|
|
7
|
+
#private;
|
|
8
|
+
readonly effects: KeyValueEffects<S>;
|
|
9
|
+
get state(): S;
|
|
10
|
+
get entityId(): string;
|
|
11
|
+
get context(): CommandContext;
|
|
12
|
+
get client(): ComponentClient;
|
|
13
|
+
/** @internal */
|
|
14
|
+
get _kind(): "key-value";
|
|
15
|
+
/** The state of an instance that has never been written. `this.entityId` is available here. */
|
|
16
|
+
abstract emptyState(): S;
|
|
17
|
+
/** @internal */
|
|
18
|
+
_bindInstance(entityId: string): void;
|
|
19
|
+
/** @internal */
|
|
20
|
+
_bindCommand(state: S, context: CommandContext, client: ComponentClient): void;
|
|
21
|
+
/** @internal */
|
|
22
|
+
_unbindCommand(): void;
|
|
23
|
+
}
|
|
24
|
+
export interface KeyValueEntityClass<S = unknown, C extends KeyValueEntity<S> = KeyValueEntity<S>> {
|
|
25
|
+
new (): C;
|
|
26
|
+
readonly componentId: string;
|
|
27
|
+
readonly state: Shape<S>;
|
|
28
|
+
readonly handlers: HandlerTable<C>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// A key value entity: one durable state per instance, replaced whole by `updateState`.
|
|
2
|
+
import { KeyValueEffects } from "./effects/keyValue.js";
|
|
3
|
+
export class KeyValueEntity {
|
|
4
|
+
effects = new KeyValueEffects();
|
|
5
|
+
#state;
|
|
6
|
+
#entityId;
|
|
7
|
+
#context;
|
|
8
|
+
#client;
|
|
9
|
+
#bound = false;
|
|
10
|
+
get state() {
|
|
11
|
+
if (!this.#bound)
|
|
12
|
+
throw new Error("state is only available inside a command handler");
|
|
13
|
+
return this.#state;
|
|
14
|
+
}
|
|
15
|
+
get entityId() {
|
|
16
|
+
if (this.#entityId === undefined)
|
|
17
|
+
throw new Error("entityId is only available once the entity is bound to an instance");
|
|
18
|
+
return this.#entityId;
|
|
19
|
+
}
|
|
20
|
+
get context() {
|
|
21
|
+
if (!this.#context)
|
|
22
|
+
throw new Error("context is only available inside a command handler");
|
|
23
|
+
return this.#context;
|
|
24
|
+
}
|
|
25
|
+
get client() {
|
|
26
|
+
if (!this.#client)
|
|
27
|
+
throw new Error("client is only available inside a command handler");
|
|
28
|
+
return this.#client;
|
|
29
|
+
}
|
|
30
|
+
/** @internal */
|
|
31
|
+
get _kind() {
|
|
32
|
+
return "key-value";
|
|
33
|
+
}
|
|
34
|
+
/** @internal */
|
|
35
|
+
_bindInstance(entityId) {
|
|
36
|
+
this.#entityId = entityId;
|
|
37
|
+
}
|
|
38
|
+
/** @internal */
|
|
39
|
+
_bindCommand(state, context, client) {
|
|
40
|
+
this.#state = state;
|
|
41
|
+
this.#context = context;
|
|
42
|
+
this.#client = client;
|
|
43
|
+
this.#bound = true;
|
|
44
|
+
}
|
|
45
|
+
/** @internal */
|
|
46
|
+
_unbindCommand() {
|
|
47
|
+
this.#state = undefined;
|
|
48
|
+
this.#context = undefined;
|
|
49
|
+
this.#client = undefined;
|
|
50
|
+
this.#bound = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/kinds.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Kind as ProtoKind } from "./_proto/ankka/protocol/v1/discovery_pb.ts";
|
|
2
|
+
import { ErrorCode as ProtoErrorCode } from "./_proto/ankka/protocol/v1/payload_pb.ts";
|
|
3
|
+
import { ErrorCode } from "./effects/common.ts";
|
|
4
|
+
export type ComponentKind = "event-sourced" | "key-value" | "workflow" | "view" | "consumer" | "timed-action" | "agent";
|
|
5
|
+
export declare function kindToProto(kind: ComponentKind): ProtoKind;
|
|
6
|
+
export declare function kindFromProto(kind: ProtoKind): ComponentKind;
|
|
7
|
+
export declare function errorCodeToProto(code: ErrorCode): ProtoErrorCode;
|
|
8
|
+
export declare function errorCodeFromProto(code: ProtoErrorCode): ErrorCode;
|
package/dist/kinds.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The component kinds, and the mappings between the SDK's words and the protocol's numbers.
|
|
2
|
+
import { Kind as ProtoKind } from "./_proto/ankka/protocol/v1/discovery_pb.js";
|
|
3
|
+
import { ErrorCode as ProtoErrorCode } from "./_proto/ankka/protocol/v1/payload_pb.js";
|
|
4
|
+
import { ErrorCode } from "./effects/common.js";
|
|
5
|
+
const KIND_TO_PROTO = Object.freeze({
|
|
6
|
+
"event-sourced": ProtoKind.EVENT_SOURCED_ENTITY,
|
|
7
|
+
"key-value": ProtoKind.KEY_VALUE_ENTITY,
|
|
8
|
+
workflow: ProtoKind.WORKFLOW,
|
|
9
|
+
view: ProtoKind.VIEW,
|
|
10
|
+
consumer: ProtoKind.CONSUMER,
|
|
11
|
+
"timed-action": ProtoKind.TIMED_ACTION,
|
|
12
|
+
agent: ProtoKind.AGENT,
|
|
13
|
+
});
|
|
14
|
+
export function kindToProto(kind) {
|
|
15
|
+
return KIND_TO_PROTO[kind];
|
|
16
|
+
}
|
|
17
|
+
export function kindFromProto(kind) {
|
|
18
|
+
for (const [k, v] of Object.entries(KIND_TO_PROTO))
|
|
19
|
+
if (v === kind)
|
|
20
|
+
return k;
|
|
21
|
+
throw new RangeError(`unknown component kind ${kind}`);
|
|
22
|
+
}
|
|
23
|
+
const CODE_TO_PROTO = Object.freeze({
|
|
24
|
+
INTERNAL: ProtoErrorCode.INTERNAL,
|
|
25
|
+
BAD_REQUEST: ProtoErrorCode.BAD_REQUEST,
|
|
26
|
+
UNAUTHORIZED: ProtoErrorCode.UNAUTHORIZED,
|
|
27
|
+
FORBIDDEN: ProtoErrorCode.FORBIDDEN,
|
|
28
|
+
NOT_FOUND: ProtoErrorCode.NOT_FOUND,
|
|
29
|
+
CONFLICT: ProtoErrorCode.CONFLICT,
|
|
30
|
+
TIMEOUT: ProtoErrorCode.TIMEOUT,
|
|
31
|
+
UNAVAILABLE: ProtoErrorCode.UNAVAILABLE,
|
|
32
|
+
});
|
|
33
|
+
export function errorCodeToProto(code) {
|
|
34
|
+
return CODE_TO_PROTO[code];
|
|
35
|
+
}
|
|
36
|
+
export function errorCodeFromProto(code) {
|
|
37
|
+
for (const [k, v] of Object.entries(CODE_TO_PROTO))
|
|
38
|
+
if (v === code)
|
|
39
|
+
return k;
|
|
40
|
+
return ErrorCode.Internal;
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ErrorDetail, Retention } from "./effects/common.ts";
|
|
2
|
+
import type { EventSourcedEffect } from "./effects/eventSourced.ts";
|
|
3
|
+
import type { KeyValueEffect } from "./effects/keyValue.ts";
|
|
4
|
+
import type { StepEffect, StepOutcome, StepRef, WorkflowCommandEffect } from "./effects/workflow.ts";
|
|
5
|
+
/** What an effect amounts to, as values a test can assert on and the server can encode. */
|
|
6
|
+
export interface Materialised<S, E, R> {
|
|
7
|
+
readonly events: readonly E[];
|
|
8
|
+
readonly newState: S;
|
|
9
|
+
readonly retention: Retention | null;
|
|
10
|
+
/** The reply, when the outcome was a reply. */
|
|
11
|
+
readonly reply: R | undefined;
|
|
12
|
+
/** The refusal, when the outcome was one. */
|
|
13
|
+
readonly error: ErrorDetail | undefined;
|
|
14
|
+
/** True when the handler answered nothing. */
|
|
15
|
+
readonly noReply: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function materialiseEventSourced<S, E, R>(effect: EventSourcedEffect<S, E, R>, state: S, applyEvent: (state: S, event: E) => S): Materialised<S, E, R>;
|
|
18
|
+
/** A key value effect: `newState` is the state to keep (unchanged when the effect only replied or deleted). `changed` says whether to send it. */
|
|
19
|
+
export interface MaterialisedKeyValue<S, R> extends Materialised<S, never, R> {
|
|
20
|
+
readonly changed: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function materialiseKeyValue<S, R>(effect: KeyValueEffect<S, R>, state: S): MaterialisedKeyValue<S, R>;
|
|
23
|
+
export interface MaterialisedWorkflowCommand<S, R> extends Materialised<S, never, R> {
|
|
24
|
+
readonly changed: boolean;
|
|
25
|
+
readonly transition: StepRef | null;
|
|
26
|
+
}
|
|
27
|
+
export declare function materialiseWorkflowCommand<S, R>(effect: WorkflowCommandEffect<S, R>, state: S): MaterialisedWorkflowCommand<S, R>;
|
|
28
|
+
export interface MaterialisedStep<S> {
|
|
29
|
+
readonly newState: S;
|
|
30
|
+
readonly changed: boolean;
|
|
31
|
+
readonly next: StepOutcome;
|
|
32
|
+
}
|
|
33
|
+
export declare function materialiseStep<S>(effect: StepEffect<S>, state: S): MaterialisedStep<S>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// One reduction per kind, shared by the server and the unit testkit, so a test and a deployment cannot
|
|
2
|
+
// disagree about what an effect means. This is the SDK's copy of the rule that puts
|
|
3
|
+
// `EventSourcedEffect.materialise` in `modules/core`: the reply is computed from the state *after* the
|
|
4
|
+
// events, and a refusal leaves everything as it was.
|
|
5
|
+
function outcomeOf(outcome, state) {
|
|
6
|
+
switch (outcome.kind) {
|
|
7
|
+
case "reply":
|
|
8
|
+
return { reply: outcome.compute(state), error: undefined, noReply: false };
|
|
9
|
+
case "no-reply":
|
|
10
|
+
return { reply: undefined, error: undefined, noReply: true };
|
|
11
|
+
case "fail":
|
|
12
|
+
return { reply: undefined, error: outcome.error, noReply: false };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function materialiseEventSourced(effect, state, applyEvent) {
|
|
16
|
+
switch (effect.kind) {
|
|
17
|
+
case "persist": {
|
|
18
|
+
const after = effect.events.reduce(applyEvent, state);
|
|
19
|
+
return Object.freeze({ events: effect.events, newState: after, retention: effect.retention, ...outcomeOf(effect.outcome, after) });
|
|
20
|
+
}
|
|
21
|
+
case "read-only":
|
|
22
|
+
return Object.freeze({ events: [], newState: state, retention: effect.retention, ...outcomeOf(effect.outcome, state) });
|
|
23
|
+
default:
|
|
24
|
+
throw new TypeError(`not an event sourced effect: ${JSON.stringify(effect.kind)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function materialiseKeyValue(effect, state) {
|
|
28
|
+
switch (effect.kind) {
|
|
29
|
+
case "update": {
|
|
30
|
+
const after = effect.newState === null ? state : effect.newState;
|
|
31
|
+
return Object.freeze({ events: [], newState: after, changed: effect.newState !== null, retention: effect.retention, ...outcomeOf(effect.outcome, after) });
|
|
32
|
+
}
|
|
33
|
+
case "read-only":
|
|
34
|
+
return Object.freeze({ events: [], newState: state, changed: false, retention: null, ...outcomeOf(effect.outcome, state) });
|
|
35
|
+
default:
|
|
36
|
+
throw new TypeError(`not a key value effect: ${JSON.stringify(effect.kind)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function materialiseWorkflowCommand(effect, state) {
|
|
40
|
+
switch (effect.kind) {
|
|
41
|
+
case "workflow": {
|
|
42
|
+
const after = effect.newState === null ? state : effect.newState;
|
|
43
|
+
return Object.freeze({ events: [], newState: after, changed: effect.newState !== null, transition: effect.transition, retention: null, ...outcomeOf(effect.outcome, after) });
|
|
44
|
+
}
|
|
45
|
+
case "read-only":
|
|
46
|
+
return Object.freeze({ events: [], newState: state, changed: false, transition: null, retention: null, ...outcomeOf(effect.outcome, state) });
|
|
47
|
+
default:
|
|
48
|
+
throw new TypeError(`not a workflow effect: ${JSON.stringify(effect.kind)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function materialiseStep(effect, state) {
|
|
52
|
+
if (effect.kind !== "step")
|
|
53
|
+
throw new TypeError(`not a step effect: ${JSON.stringify(effect.kind)}`);
|
|
54
|
+
return Object.freeze({ newState: effect.newState === null ? state : effect.newState, changed: effect.newState !== null, next: effect.next });
|
|
55
|
+
}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Shape } from "./codec.ts";
|
|
2
|
+
import type { RequestContext } from "./context.ts";
|
|
3
|
+
import type { Infer, Schema } from "./schema.ts";
|
|
4
|
+
/** Who may call: everyone, no one, or authenticated callers only. There is no default. */
|
|
5
|
+
export declare const Acl: Readonly<{
|
|
6
|
+
readonly allowAll: "allow-all";
|
|
7
|
+
readonly denyAll: "deny-all";
|
|
8
|
+
readonly authenticated: "authenticated";
|
|
9
|
+
}>;
|
|
10
|
+
export type Acl = (typeof Acl)[keyof typeof Acl];
|
|
11
|
+
/** Thrown from a route handler to answer a status other than 200 with a plain-text message. */
|
|
12
|
+
export declare class HttpProblem extends Error {
|
|
13
|
+
readonly status: number;
|
|
14
|
+
constructor(status: number, message: string);
|
|
15
|
+
}
|
|
16
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
17
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
18
|
+
/** The parameter names in a template: `ParamNames<"/{cartId}/items/{productId}">` is `"cartId" | "productId"`. */
|
|
19
|
+
export type ParamNames<T extends string> = T extends `${string}{${infer P}}${infer Rest}` ? P | ParamNames<Rest> : never;
|
|
20
|
+
/** The typed parameters of a template, each a `string` unless `params` narrows it with a schema. */
|
|
21
|
+
export type Params<T extends string, PS> = {
|
|
22
|
+
readonly [K in ParamNames<T>]: K extends keyof PS ? Infer<PS[K]> : string;
|
|
23
|
+
};
|
|
24
|
+
/** Options on a route: an access rule for this route alone, and schemas narrowing path parameters. */
|
|
25
|
+
export interface RouteOptions<PS extends Readonly<Record<string, Schema>> = {}> {
|
|
26
|
+
readonly acl?: Acl;
|
|
27
|
+
readonly params?: PS;
|
|
28
|
+
}
|
|
29
|
+
/** One entry of an endpoint's `routes` table. */
|
|
30
|
+
export interface RouteRef<Ep = unknown, P = unknown, B = unknown, R = unknown> {
|
|
31
|
+
readonly method: HttpMethod;
|
|
32
|
+
/** As declared: `/{cartId}/items`, relative to the endpoint's prefix. */
|
|
33
|
+
readonly template: string;
|
|
34
|
+
/** Parameter names in template order. */
|
|
35
|
+
readonly paramNames: readonly string[];
|
|
36
|
+
/** Schemas narrowing parameters; a parameter not named here is a string. */
|
|
37
|
+
readonly paramShapes: Readonly<Record<string, Schema>>;
|
|
38
|
+
readonly body: Shape<B> | undefined;
|
|
39
|
+
readonly reply: Shape<R> | undefined;
|
|
40
|
+
readonly streaming: boolean;
|
|
41
|
+
readonly acl: Acl | undefined;
|
|
42
|
+
readonly run: (self: Ep, request: RequestContext<P>, body: B) => MaybePromise<R> | AsyncIterable<string>;
|
|
43
|
+
}
|
|
44
|
+
export declare function parseTemplate(template: string): string[];
|
|
45
|
+
type NoBodyRun<Ep, T extends string, PS, R> = (self: Ep, request: RequestContext<Params<T, PS>>) => MaybePromise<R>;
|
|
46
|
+
type BodyRun<Ep, T extends string, PS, B, R> = (self: Ep, request: RequestContext<Params<T, PS>>, body: B) => MaybePromise<R>;
|
|
47
|
+
/** A GET route: `get("/{cartId}", ShoppingCart, (ep: CartEndpoint, req) => ep.getCart(req.params.cartId))`. */
|
|
48
|
+
export declare function get<Ep, T extends string, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, reply: Shape<R>, run: NoBodyRun<Ep, T, PS, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, R>;
|
|
49
|
+
/** A POST route, with a body (`post(template, Body, Reply, run)`) or without (`post(template, Reply, run)`). */
|
|
50
|
+
export declare const post: {
|
|
51
|
+
<Ep, T extends string, B, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, body: Shape<B>, reply: Shape<R>, run: BodyRun<Ep, T, PS, B, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, B, R>;
|
|
52
|
+
<Ep, T extends string, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, reply: Shape<R>, run: NoBodyRun<Ep, T, PS, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, R>;
|
|
53
|
+
};
|
|
54
|
+
export declare const put: {
|
|
55
|
+
<Ep, T extends string, B, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, body: Shape<B>, reply: Shape<R>, run: BodyRun<Ep, T, PS, B, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, B, R>;
|
|
56
|
+
<Ep, T extends string, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, reply: Shape<R>, run: NoBodyRun<Ep, T, PS, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, R>;
|
|
57
|
+
};
|
|
58
|
+
export declare const patch: {
|
|
59
|
+
<Ep, T extends string, B, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, body: Shape<B>, reply: Shape<R>, run: BodyRun<Ep, T, PS, B, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, B, R>;
|
|
60
|
+
<Ep, T extends string, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, reply: Shape<R>, run: NoBodyRun<Ep, T, PS, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, R>;
|
|
61
|
+
};
|
|
62
|
+
/** A DELETE route; `delete` is a reserved word, so `del`. */
|
|
63
|
+
export declare const del: {
|
|
64
|
+
<Ep, T extends string, B, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, body: Shape<B>, reply: Shape<R>, run: BodyRun<Ep, T, PS, B, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, B, R>;
|
|
65
|
+
<Ep, T extends string, R, PS extends Readonly<Record<string, Schema>> = {}>(template: T, reply: Shape<R>, run: NoBodyRun<Ep, T, PS, R>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, R>;
|
|
66
|
+
};
|
|
67
|
+
/** A server-sent-events route: the handler returns an `AsyncIterable<string>`, one frame per string. */
|
|
68
|
+
export declare function sse<Ep, T extends string, PS extends Readonly<Record<string, Schema>> = {}>(template: T, run: (self: Ep, request: RequestContext<Params<T, PS>>) => AsyncIterable<string> | Promise<AsyncIterable<string>>, options?: RouteOptions<PS>): RouteRef<Ep, Params<T, PS>, undefined, never>;
|
|
69
|
+
/** The routes table type an endpoint declares. */
|
|
70
|
+
export type RouteTable<Ep> = Readonly<Record<string, RouteRef<Ep, any, any, any>>>;
|
|
71
|
+
export {};
|