ankka 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/_proto/ankka/protocol/v1/client_pb.d.ts +594 -0
  2. package/dist/_proto/ankka/protocol/v1/client_pb.js +172 -30
  3. package/dist/_proto/ankka/protocol/v1/consumer_pb.js +1 -1
  4. package/dist/_proto/ankka/protocol/v1/discovery_pb.d.ts +134 -0
  5. package/dist/_proto/ankka/protocol/v1/discovery_pb.js +51 -36
  6. package/dist/_proto/ankka/protocol/v1/event_sourced_pb.d.ts +24 -0
  7. package/dist/_proto/ankka/protocol/v1/event_sourced_pb.js +1 -1
  8. package/dist/_proto/ankka/protocol/v1/mcp_pb.d.ts +120 -0
  9. package/dist/_proto/ankka/protocol/v1/mcp_pb.js +29 -0
  10. package/dist/_proto/ankka/protocol/v1/timed_action_pb.js +1 -1
  11. package/dist/_proto/ankka/protocol/v1/view_pb.js +1 -1
  12. package/dist/_proto/ankka/protocol/v1/wasm_pb.d.ts +12 -0
  13. package/dist/_proto/ankka/protocol/v1/wasm_pb.js +1 -1
  14. package/dist/agent.d.ts +10 -0
  15. package/dist/agent.js +20 -0
  16. package/dist/autonomous.d.ts +5 -0
  17. package/dist/autonomous.js +10 -0
  18. package/dist/client-keys.d.ts +89 -0
  19. package/dist/client-keys.js +182 -0
  20. package/dist/consumer.d.ts +5 -0
  21. package/dist/consumer.js +10 -0
  22. package/dist/context.d.ts +11 -3
  23. package/dist/context.js +20 -4
  24. package/dist/endpoint.d.ts +10 -0
  25. package/dist/endpoint.js +20 -0
  26. package/dist/graph.d.ts +5 -0
  27. package/dist/graph.js +10 -0
  28. package/dist/index.d.ts +6 -1
  29. package/dist/index.js +5 -0
  30. package/dist/mcp-endpoint.d.ts +144 -0
  31. package/dist/mcp-endpoint.js +256 -0
  32. package/dist/routes.d.ts +25 -2
  33. package/dist/routes.js +29 -2
  34. package/dist/server/discovery.d.ts +7 -0
  35. package/dist/server/discovery.js +26 -2
  36. package/dist/server/eventSourced.js +3 -3
  37. package/dist/server/http.d.ts +8 -0
  38. package/dist/server/http.js +10 -4
  39. package/dist/server/mcp.d.ts +17 -0
  40. package/dist/server/mcp.js +157 -0
  41. package/dist/server/server.d.ts +11 -1
  42. package/dist/server/server.js +19 -0
  43. package/dist/server/storageEndpoint.d.ts +14 -0
  44. package/dist/server/storageEndpoint.js +82 -0
  45. package/dist/service.d.ts +6 -1
  46. package/dist/service.js +47 -4
  47. package/dist/sigv4.d.ts +47 -0
  48. package/dist/sigv4.js +149 -0
  49. package/dist/spec.d.ts +8 -2
  50. package/dist/spec.js +53 -3
  51. package/dist/storage.d.ts +288 -0
  52. package/dist/storage.js +1081 -0
  53. package/dist/testkit/integration.d.ts +9 -0
  54. package/dist/testkit/integration.js +5 -1
  55. package/dist/testkit/kinds.d.ts +16 -4
  56. package/dist/testkit/kinds.js +36 -14
  57. package/dist/testkit/unit.d.ts +4 -1
  58. package/dist/testkit/unit.js +8 -4
  59. package/dist/timedAction.d.ts +10 -0
  60. package/dist/timedAction.js +20 -0
  61. package/dist/version.d.ts +1 -1
  62. package/dist/version.js +1 -1
  63. package/dist/workflow.d.ts +8 -0
  64. package/dist/workflow.js +15 -0
  65. package/package.json +2 -2
@@ -1,5 +1,6 @@
1
1
  import type { ComponentClient } from "../client.ts";
2
2
  import type { ServiceBuilder } from "../service.ts";
3
+ import { type Buckets } from "../storage.ts";
3
4
  export interface AnkkaTestKitOptions {
4
5
  /** The sidecar image; `sidecarImage()` by default. */
5
6
  readonly image?: string;
@@ -9,6 +10,12 @@ export interface AnkkaTestKitOptions {
9
10
  /** How long to wait for the sidecar to report healthy; 120s by default. */
10
11
  readonly readyTimeoutMs?: number;
11
12
  readonly log?: (message: string) => void;
13
+ /**
14
+ * The service's buckets. By default the store `ANKKA_STORAGE_BACKEND=memory` gives this process: in
15
+ * memory, with the buckets `service.json` in the working directory declares (the unnamed one, and no
16
+ * named one, when there is no such file). `kit.storage` is the same store.
17
+ */
18
+ readonly storage?: Buckets;
12
19
  }
13
20
  /** What `http` answers with. */
14
21
  export declare class HttpResponse {
@@ -47,6 +54,8 @@ export declare class AnkkaTestKit {
47
54
  #private;
48
55
  /** The component client every handler in `service` uses, pointed at the sidecar's mapped callback port. */
49
56
  readonly client: ComponentClient;
57
+ /** The buckets the service holds: what a handler kept, a test reads here, and what a test puts here a handler reads. */
58
+ readonly storage: Buckets;
50
59
  private constructor();
51
60
  /** Starts Postgres, this process's server and the sidecar; a start that fails half-way stops what it started. */
52
61
  static start(service: ServiceBuilder, options?: AnkkaTestKitOptions): Promise<AnkkaTestKit>;
@@ -16,6 +16,7 @@ import { randomBytes } from "node:crypto";
16
16
  import { chmodSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
17
17
  import { tmpdir } from "node:os";
18
18
  import { join } from "node:path";
19
+ import { chooseStorage } from "../storage.js";
19
20
  import { VERSION } from "../version.js";
20
21
  const POSTGRES_IMAGE = "postgres:17-alpine";
21
22
  const HTTP_PORT = 9000;
@@ -149,7 +150,10 @@ export class AnkkaTestKit {
149
150
  #http;
150
151
  /** The component client every handler in `service` uses, pointed at the sidecar's mapped callback port. */
151
152
  client;
153
+ /** The buckets the service holds: what a handler kept, a test reads here, and what a test puts here a handler reads. */
154
+ storage;
152
155
  constructor(service, options) {
156
+ this.storage = options.storage ?? chooseStorage({ ...process.env, ANKKA_STORAGE_BACKEND: "memory" }).buckets;
153
157
  this.#service = service;
154
158
  this.#image = options.image ?? sidecarImage();
155
159
  this.#env = options.env ?? {};
@@ -203,7 +207,7 @@ export class AnkkaTestKit {
203
207
  .withCopyDirectoriesToContainer([{ source: this.#ddlDir, target: "/docker-entrypoint-initdb.d", mode: 0o755 }])
204
208
  .start();
205
209
  // This process's server, on all interfaces: the sidecar is in a container and dials in.
206
- this.#server = this.#service.server({ host: "0.0.0.0", port: 0, log: this.#log });
210
+ this.#server = this.#service.server({ host: "0.0.0.0", port: 0, log: this.#log, storage: this.storage });
207
211
  this.#processPort = (await this.#server.start()).port;
208
212
  await this.#startSidecar();
209
213
  }
@@ -1,4 +1,5 @@
1
1
  import { ComponentClient } from "../client.ts";
2
+ import { type Buckets } from "../storage.ts";
2
3
  import { type ErrorDetail, type Metadata } from "../effects/common.ts";
3
4
  import type { StepRef } from "../effects/workflow.ts";
4
5
  import type { ConsumerEffect, TimedActionEffect, ViewEffect } from "../effects/stateless.ts";
@@ -42,8 +43,10 @@ export declare class WorkflowTestKit<S, C extends Workflow<S>> {
42
43
  #private;
43
44
  readonly entityId: string;
44
45
  state: S;
46
+ /** The buckets the workflow's steps are given; the test reads what they kept here. */
47
+ readonly storage: Buckets;
45
48
  private constructor();
46
- static of<S, C extends Workflow<S>>(cls: WorkflowClass<S, C>, entityId?: string, client?: ComponentClient): WorkflowTestKit<S, C>;
49
+ static of<S, C extends Workflow<S>>(cls: WorkflowClass<S, C>, entityId?: string, client?: ComponentClient, storage?: Buckets): WorkflowTestKit<S, C>;
47
50
  get progress(): WorkflowProgress;
48
51
  call<I, R>(handler: HandlerRef<C, I, R, any> | string, input?: I, metadata?: Metadata): Promise<MaterialisedWorkflowCommand<S, R>>;
49
52
  /** Runs one step, by name or declaration, with an input (or the pending transition's). */
@@ -102,8 +105,10 @@ export declare class ConsumerTestKit<M, Out, C extends Consumer<M, Out>> {
102
105
  readonly metadata: Metadata;
103
106
  readonly key?: string;
104
107
  }[];
108
+ /** The buckets the consumer is given; the test reads what it kept here. */
109
+ readonly storage: Buckets;
105
110
  private constructor();
106
- static of<M, Out, C extends Consumer<M, Out>>(cls: ConsumerClass<M, Out, C>, client?: ComponentClient): ConsumerTestKit<M, Out, C>;
111
+ static of<M, Out, C extends Consumer<M, Out>>(cls: ConsumerClass<M, Out, C>, client?: ComponentClient, storage?: Buckets): ConsumerTestKit<M, Out, C>;
107
112
  /**
108
113
  * A message from source instance `subject`. The metadata says the runtime speaks this SDK's protocol
109
114
  * version; pass `"ankka.protocol"` or `"ankka.sequence"` to say otherwise.
@@ -118,8 +123,10 @@ export declare class ConsumerTestKit<M, Out, C extends Consumer<M, Out>> {
118
123
  */
119
124
  export declare class GraphConsumerTestKit<M> {
120
125
  #private;
126
+ /** The buckets the graph consumer is given; the test reads what it kept here. */
127
+ readonly storage: Buckets;
121
128
  private constructor();
122
- static of<M>(cls: GraphConsumerClass<M>, client?: ComponentClient): GraphConsumerTestKit<M>;
129
+ static of<M>(cls: GraphConsumerClass<M>, client?: ComponentClient, storage?: Buckets): GraphConsumerTestKit<M>;
123
130
  /** A change from source instance `subject` (default `"test"`) at `sequence` (default 1). */
124
131
  onMessage(message: M, options?: GraphChange): Promise<Delta[]>;
125
132
  /** The source instance was deleted, at `sequence`. */
@@ -135,8 +142,10 @@ export interface GraphChange {
135
142
  /** Invokes a timed action's handlers as the sweeper would. */
136
143
  export declare class TimedActionTestKit<C extends TimedAction> {
137
144
  #private;
145
+ /** The buckets the timed action is given; the test reads what it kept here. */
146
+ readonly storage: Buckets;
138
147
  private constructor();
139
- static of<C extends TimedAction>(cls: TimedActionClass<C>, client?: ComponentClient): TimedActionTestKit<C>;
148
+ static of<C extends TimedAction>(cls: TimedActionClass<C>, client?: ComponentClient, storage?: Buckets): TimedActionTestKit<C>;
140
149
  invoke<I>(action: HandlerRef<C, I, any, any> | string, input?: I, metadata?: Metadata): Promise<TimedActionEffect>;
141
150
  }
142
151
  /** What a scripted model answers next. */
@@ -189,9 +198,12 @@ export declare class AgentTestKit<C extends Agent> {
189
198
  role: "user" | "assistant";
190
199
  text: string;
191
200
  }[];
201
+ /** The buckets the agent's tools are given; the test reads what they kept here. */
202
+ readonly storage: Buckets;
192
203
  private constructor();
193
204
  static of<C extends Agent>(cls: AgentClass<C>, sessionId: string, model: ScriptedModel, client?: ComponentClient, options?: {
194
205
  readonly mcp?: ScriptedMcp;
206
+ readonly storage?: Buckets;
195
207
  }): AgentTestKit<C>;
196
208
  /** One turn: returns the reply text (JSON text for a `thenReplyJson` plan). A refusal or a blocked guardrail rejects with `CommandError`; a turn that waits, with `ApprovalAwaited`. */
197
209
  ask<I>(handler: HandlerRef<C, I, any, any> | string, input?: I): Promise<string>;
@@ -2,6 +2,7 @@
2
2
  // agents. Same rules as the event sourced kit: no sidecar, effects as values, every value round-tripped
3
3
  // through the component's codecs, the server's reduction.
4
4
  import { ComponentClient, noClient } from "../client.js";
5
+ import { InMemoryBuckets } from "../storage.js";
5
6
  import { binaryCodecs, codecFor } from "../codec.js";
6
7
  import { commandContext } from "../context.js";
7
8
  import { CommandError, ErrorCode } from "../effects/common.js";
@@ -78,17 +79,20 @@ export class WorkflowTestKit {
78
79
  entityId;
79
80
  state;
80
81
  #progress = { pending: null, paused: null, ended: false, failed: null };
81
- constructor(cls, entityId, client) {
82
+ /** The buckets the workflow's steps are given; the test reads what they kept here. */
83
+ storage;
84
+ constructor(cls, entityId, client, storage) {
82
85
  this.#registered = registryFor(cls, client).component(cls.componentId);
83
86
  this.#cls = cls;
84
87
  this.#client = client;
88
+ this.storage = storage;
85
89
  this.entityId = entityId;
86
90
  const instance = new cls();
87
91
  instance._bindInstance(entityId);
88
92
  this.state = instance.emptyState();
89
93
  }
90
- static of(cls, entityId = "test", client = noClient()) {
91
- return new WorkflowTestKit(cls, entityId, client);
94
+ static of(cls, entityId = "test", client = noClient(), storage = new InMemoryBuckets()) {
95
+ return new WorkflowTestKit(cls, entityId, client, storage);
92
96
  }
93
97
  get progress() {
94
98
  return this.#progress;
@@ -96,6 +100,7 @@ export class WorkflowTestKit {
96
100
  #instance() {
97
101
  const w = new this.#cls();
98
102
  w._bindInstance(this.entityId);
103
+ w.storage = this.storage;
99
104
  return w;
100
105
  }
101
106
  async call(handler, input, metadata = {}) {
@@ -135,6 +140,7 @@ export class WorkflowTestKit {
135
140
  const wireInput = ref.input ? roundTrip(codecFor(ref.input), input) : undefined;
136
141
  const w = this.#instance();
137
142
  w._bindCommand(this.state, commandContext(this.#registered.id, this.entityId, 0n, {}), this.#client);
143
+ w._enterStep();
138
144
  let effect;
139
145
  try {
140
146
  effect = (await ref.run(w, wireInput));
@@ -323,17 +329,21 @@ export class ConsumerTestKit {
323
329
  * `key` is the record key a message of `produceAll` named; absent, the message is keyed by its subject.
324
330
  */
325
331
  produced = [];
326
- constructor(cls, client) {
332
+ /** The buckets the consumer is given; the test reads what it kept here. */
333
+ storage;
334
+ constructor(cls, client, storage) {
327
335
  this.#registered = registryFor(cls, client).component(cls.componentId);
328
336
  this.#cls = cls;
329
337
  this.#client = client;
338
+ this.storage = storage;
330
339
  }
331
- static of(cls, client = noClient()) {
332
- return new ConsumerTestKit(cls, client);
340
+ static of(cls, client = noClient(), storage = new InMemoryBuckets()) {
341
+ return new ConsumerTestKit(cls, client, storage);
333
342
  }
334
343
  async #apply(fn, metadata, standing) {
335
344
  const consumer = new this.#cls();
336
345
  consumer._bind(metadata, this.#client.withMetadata(metadata), standing);
346
+ consumer.storage = this.storage;
337
347
  const effect = await fn(consumer);
338
348
  const outCodec = () => {
339
349
  const codec = this.#registered.outCodec;
@@ -374,13 +384,16 @@ export class GraphConsumerTestKit {
374
384
  #registered;
375
385
  #cls;
376
386
  #client;
377
- constructor(cls, client) {
387
+ /** The buckets the graph consumer is given; the test reads what it kept here. */
388
+ storage;
389
+ constructor(cls, client, storage) {
378
390
  this.#registered = registryFor(cls, client).component(cls.componentId);
379
391
  this.#cls = cls;
380
392
  this.#client = client;
393
+ this.storage = storage;
381
394
  }
382
- static of(cls, client = noClient()) {
383
- return new GraphConsumerTestKit(cls, client);
395
+ static of(cls, client = noClient(), storage = new InMemoryBuckets()) {
396
+ return new GraphConsumerTestKit(cls, client, storage);
384
397
  }
385
398
  async #apply(fn, options) {
386
399
  const metadata = {
@@ -391,6 +404,7 @@ export class GraphConsumerTestKit {
391
404
  };
392
405
  const consumer = new this.#cls();
393
406
  consumer._bind(metadata, this.#client.withMetadata(metadata));
407
+ consumer.storage = this.storage;
394
408
  const records = deltaRecords(await fn(consumer), metadata);
395
409
  if (records.length > 0)
396
410
  requireSeveralMessages(metadata);
@@ -411,19 +425,23 @@ export class TimedActionTestKit {
411
425
  #registered;
412
426
  #cls;
413
427
  #client;
414
- constructor(cls, client) {
428
+ /** The buckets the timed action is given; the test reads what it kept here. */
429
+ storage;
430
+ constructor(cls, client, storage) {
415
431
  this.#registered = registryFor(cls, client).component(cls.componentId);
416
432
  this.#cls = cls;
417
433
  this.#client = client;
434
+ this.storage = storage;
418
435
  }
419
- static of(cls, client = noClient()) {
420
- return new TimedActionTestKit(cls, client);
436
+ static of(cls, client = noClient(), storage = new InMemoryBuckets()) {
437
+ return new TimedActionTestKit(cls, client, storage);
421
438
  }
422
439
  async invoke(action, input, metadata = {}) {
423
440
  const ref = findHandler(this.#registered.actions, action, this.#registered.cls.name);
424
441
  const wireInput = ref.input ? roundTrip(codecFor(ref.input), input) : undefined;
425
442
  const instance = new this.#cls();
426
443
  instance._bind({ "ankka.timer": "test", "ankka.attempts": "1", ...metadata }, this.#client.withMetadata(metadata));
444
+ instance.storage = this.storage;
427
445
  return (await ref.run(instance, wireInput));
428
446
  }
429
447
  }
@@ -475,20 +493,24 @@ export class AgentTestKit {
475
493
  #turn;
476
494
  #decided = new Set();
477
495
  #nextId = 0;
478
- constructor(cls, sessionId, model, client, mcp) {
496
+ /** The buckets the agent's tools are given; the test reads what they kept here. */
497
+ storage;
498
+ constructor(cls, sessionId, model, client, mcp, storage) {
479
499
  this.#registered = registryFor(cls, client).component(cls.componentId);
480
500
  this.#cls = cls;
481
501
  this.#client = client;
502
+ this.storage = storage;
482
503
  this.#model = model;
483
504
  this.#mcp = mcp;
484
505
  this.sessionId = sessionId;
485
506
  }
486
507
  static of(cls, sessionId, model, client = noClient(), options = {}) {
487
- return new AgentTestKit(cls, sessionId, model, client, options.mcp ?? {});
508
+ return new AgentTestKit(cls, sessionId, model, client, options.mcp ?? {}, options.storage ?? new InMemoryBuckets());
488
509
  }
489
510
  #agent() {
490
511
  const a = new this.#cls();
491
512
  a._bind(this.sessionId, {}, this.#client);
513
+ a.storage = this.storage;
492
514
  return a;
493
515
  }
494
516
  /** One turn: returns the reply text (JSON text for a `thenReplyJson` plan). A refusal or a blocked guardrail rejects with `CommandError`; a turn that waits, with `ApprovalAwaited`. */
@@ -1,4 +1,5 @@
1
1
  import { ComponentClient } from "../client.ts";
2
+ import { type Buckets } from "../storage.ts";
2
3
  import { type Principal } from "../context.ts";
3
4
  import { type ErrorDetail, type Metadata, type Retention } from "../effects/common.ts";
4
5
  import type { ReplicationChange } from "../effects/eventSourced.ts";
@@ -72,8 +73,10 @@ export interface SocketRun {
72
73
  /** Calls an endpoint's routes by path, matching them as the sidecar's router does: literal segments outrank parameters. */
73
74
  export declare class EndpointTestKit<C extends Endpoint> {
74
75
  #private;
76
+ /** The buckets the endpoint is given; the test reads what its routes kept here. */
77
+ readonly storage: Buckets;
75
78
  private constructor();
76
- static of<C extends Endpoint>(cls: EndpointClass<C>, client?: ComponentClient): EndpointTestKit<C>;
79
+ static of<C extends Endpoint>(cls: EndpointClass<C>, client?: ComponentClient, storage?: Buckets): EndpointTestKit<C>;
77
80
  request(method: string, path: string, body?: unknown, options?: RequestOptions): Promise<Response>;
78
81
  /**
79
82
  * Opens a socket route with no runtime: the handler is given `frames` one at a time and then told the
@@ -6,6 +6,7 @@
6
6
  import { create } from "@bufbuild/protobuf";
7
7
  import { HttpRequestSchema, SocketInSchema } from "../_proto/ankka/protocol/v1/endpoint_pb.js";
8
8
  import { ComponentClient, noClient } from "../client.js";
9
+ import { InMemoryBuckets } from "../storage.js";
9
10
  import { binaryCodecs, codecFor } from "../codec.js";
10
11
  import { commandContext, eventContext } from "../context.js";
11
12
  import { ErrorCode } from "../effects/common.js";
@@ -143,15 +144,18 @@ const PLACEHOLDER = /\{[A-Za-z_][A-Za-z0-9_]*\}/g;
143
144
  export class EndpointTestKit {
144
145
  #cls;
145
146
  #dispatcher;
146
- constructor(cls, client) {
147
+ /** The buckets the endpoint is given; the test reads what its routes kept here. */
148
+ storage;
149
+ constructor(cls, client, storage) {
147
150
  this.#cls = cls;
151
+ this.storage = storage;
148
152
  const registry = Ankka.service({ client, log: () => { } })
149
153
  .register(cls)
150
154
  .validate();
151
- this.#dispatcher = createHttpDispatcher({ registry, client, log: () => { } });
155
+ this.#dispatcher = createHttpDispatcher({ registry, client, log: () => { }, storage });
152
156
  }
153
- static of(cls, client = noClient()) {
154
- return new EndpointTestKit(cls, client);
157
+ static of(cls, client = noClient(), storage = new InMemoryBuckets()) {
158
+ return new EndpointTestKit(cls, client, storage);
155
159
  }
156
160
  #match(method, path) {
157
161
  const prefix = this.#cls.prefix;
@@ -1,5 +1,7 @@
1
1
  import type { Metadata } from "./effects/common.ts";
2
+ import { type ClientKeys } from "./client-keys.ts";
2
3
  import { type ComponentClient, type Secrets } from "./client.ts";
4
+ import { type Buckets } from "./storage.ts";
3
5
  import type { HandlerTable } from "./handlers.ts";
4
6
  import { TimedActionEffects } from "./effects/stateless.ts";
5
7
  import { type Services } from "./services.ts";
@@ -19,6 +21,14 @@ export declare abstract class TimedAction {
19
21
  get secrets(): Secrets;
20
22
  /** A unit test's store in place of the runtime's: `component.secrets = new InMemorySecrets()`. */
21
23
  set secrets(store: Secrets);
24
+ /** The service's client keys (1.20): issued for its MCP endpoint, kept by the runtime, each shown once. */
25
+ get clientKeys(): ClientKeys;
26
+ /** A unit test's in place of the runtime's: `component.clientKeys = new InMemoryClientKeys()`. */
27
+ set clientKeys(keys: ClientKeys);
28
+ /** The service's buckets: `this.storage.default`, or `this.storage.bucket("photos")`. */
29
+ get storage(): Buckets;
30
+ /** A unit test's store in place of the runtime's: `component.storage = new InMemoryBuckets()`. */
31
+ set storage(buckets: Buckets);
22
32
  /** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
23
33
  get services(): Services;
24
34
  /** A unit test's in place of the runtime's: `component.services = new ScriptedServices()`. */
@@ -2,7 +2,9 @@
2
2
  // sweeper delivers it here, retrying a failure with backoff.
3
3
  //
4
4
  // static readonly actions = { remind: action("remind", s.string, (t: Reminder, id) => t.remind(id)) }
5
+ import { clientKeysFor } from "./client-keys.js";
5
6
  import { secretsFor } from "./client.js";
7
+ import { processStorage } from "./storage.js";
6
8
  import { TimedActionEffects } from "./effects/stateless.js";
7
9
  import { servicesFor } from "./services.js";
8
10
  export class TimedAction {
@@ -39,6 +41,24 @@ export class TimedAction {
39
41
  set secrets(store) {
40
42
  this.#secrets = store;
41
43
  }
44
+ #clientKeys;
45
+ /** The service's client keys (1.20): issued for its MCP endpoint, kept by the runtime, each shown once. */
46
+ get clientKeys() {
47
+ return this.#clientKeys ?? clientKeysFor(this.client);
48
+ }
49
+ /** A unit test's in place of the runtime's: `component.clientKeys = new InMemoryClientKeys()`. */
50
+ set clientKeys(keys) {
51
+ this.#clientKeys = keys;
52
+ }
53
+ #storage;
54
+ /** The service's buckets: `this.storage.default`, or `this.storage.bucket("photos")`. */
55
+ get storage() {
56
+ return this.#storage ?? processStorage();
57
+ }
58
+ /** A unit test's store in place of the runtime's: `component.storage = new InMemoryBuckets()`. */
59
+ set storage(buckets) {
60
+ this.#storage = buckets;
61
+ }
42
62
  #services;
43
63
  /** Other services, called as this one, through the runtime: `this.services.service("orders")`. */
44
64
  get services() {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.12.0";
1
+ export declare const VERSION = "0.14.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Written by scripts/proto.ts from package.json; do not edit. 0.0.0 in the tree, the tag's version in a release.
2
- export const VERSION = "0.12.0";
2
+ export const VERSION = "0.14.0";
@@ -4,6 +4,7 @@ import type { HandlerTable } from "./handlers.ts";
4
4
  import { type ComponentClient, type Secrets } from "./client.ts";
5
5
  import { StepEffects, WorkflowEffects, type WorkflowSettings } from "./effects/workflow.ts";
6
6
  import { type Services } from "./services.ts";
7
+ import { type Buckets } from "./storage.ts";
7
8
  export declare abstract class Workflow<S> {
8
9
  #private;
9
10
  /** Inside a command handler. */
@@ -21,6 +22,13 @@ export declare abstract class Workflow<S> {
21
22
  get secrets(): Secrets;
22
23
  /** A unit test's store in place of the runtime's. */
23
24
  set secrets(store: Secrets);
25
+ /**
26
+ * The service's buckets, in a step. A command handler is refused: a wait on a store would hold the
27
+ * workflow's other commands behind it.
28
+ */
29
+ get storage(): Buckets;
30
+ /** A unit test's store in place of the runtime's. */
31
+ set storage(buckets: Buckets);
24
32
  /**
25
33
  * Other services, called as this one, in a step. A command handler is refused, as for the secret
26
34
  * store: it would block the workflow's other commands behind another service.
package/dist/workflow.js CHANGED
@@ -9,6 +9,7 @@ import { secretsFor } from "./client.js";
9
9
  import { CommandError } from "./effects/common.js";
10
10
  import { StepEffects, WorkflowEffects } from "./effects/workflow.js";
11
11
  import { servicesFor } from "./services.js";
12
+ import { processStorage, StorageRefusals } from "./storage.js";
12
13
  export class Workflow {
13
14
  /** Inside a command handler. */
14
15
  effects = new WorkflowEffects();
@@ -55,6 +56,20 @@ export class Workflow {
55
56
  set secrets(store) {
56
57
  this.#secrets = store;
57
58
  }
59
+ #storage;
60
+ /**
61
+ * The service's buckets, in a step. A command handler is refused: a wait on a store would hold the
62
+ * workflow's other commands behind it.
63
+ */
64
+ get storage() {
65
+ if (!this.#inStep)
66
+ throw StorageRefusals.stepsOnly();
67
+ return this.#storage ?? processStorage();
68
+ }
69
+ /** A unit test's store in place of the runtime's. */
70
+ set storage(buckets) {
71
+ this.#storage = buckets;
72
+ }
58
73
  #services;
59
74
  /**
60
75
  * Other services, called as this one, in a step. A command handler is refused, as for the secret
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ankka",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Build services on ankka in TypeScript: entities, views, workflows, agents and endpoints hosted by the ankka sidecar.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -51,7 +51,7 @@
51
51
  "@bufbuild/protobuf": "^2.15.0",
52
52
  "@connectrpc/connect": "^2.2.0",
53
53
  "@connectrpc/connect-node": "^2.2.0",
54
- "canonicalize": "^2.1.0"
54
+ "canonicalize": "^5.1.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@testcontainers/postgresql": "^12.1.0",