@saga-a2ui/core 0.5.6-rc.1 → 0.7.6
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 -1
- package/package.json +1 -1
- package/src/actions/binding.ts +23 -5
- package/src/actions/category.ts +13 -4
- package/src/actions/definition.ts +250 -77
- package/src/actions/index.ts +14 -2
- package/src/actions/instance-host.ts +73 -21
- package/src/actions/invocation.ts +295 -76
- package/src/actions/limits.ts +23 -6
- package/src/actions/outcome.ts +57 -12
- package/src/actions/prerequisite.ts +33 -10
- package/src/actions/scope.ts +50 -14
- package/src/actions/server.ts +45 -12
- package/src/actions/thenable.ts +3 -1
- package/src/delivered-result.ts +31 -0
- package/src/host-links.ts +49 -0
- package/src/protocols/a2ui/v0_9/decoder.ts +48 -19
- package/src/protocols/a2ui/v0_9/deep-freeze.ts +8 -0
- package/src/protocols/a2ui/v0_9/index.ts +14 -1
- package/src/protocols/a2ui/v0_9/session.ts +26 -9
- package/src/protocols/a2ui/v0_9/surface-layout.ts +256 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @saga-a2ui/core
|
|
2
2
|
|
|
3
|
-
Renderer-independent actions and A2UI v0.9 decoding. Entry points: `/actions`, `/actions/action-button-schema`, `/protocols/a2ui/v0_9`, `/protocols/a2ui/zod3`.
|
|
3
|
+
Renderer-independent actions and A2UI v0.9 decoding. Entry points: `/actions`, `/actions/action-button-schema`, `/delivered-result` (the neutral whole-result type a delivery hands a view), `/host-links`, `/protocols/a2ui/v0_9`, `/protocols/a2ui/zod3`.
|
|
4
4
|
|
|
5
5
|
Requires Node >=22.12.0 and a TypeScript-aware bundler; exports are TypeScript source. Apache-2.0, with dependency licenses retained separately.
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@saga-a2ui/core","private":false,"description":"Renderer-independent SAGA A2UI processing: version-specific decoding, validated message cases and the message session, and the renderer-independent action runtime (local and server actions, prerequisites, bounded hooks, resource ownership) at /actions","license":"Apache-2.0","type":"module","exports":{"./actions":"./src/actions/index.ts","./actions/action-button-schema":"./src/actions/action-button-schema.ts","./protocols/a2ui/v0_9":"./src/protocols/a2ui/v0_9/index.ts","./protocols/a2ui/zod3":"./src/protocols/a2ui/zod3.ts"},"files":["src","!src/**/*.test.ts","!src/**/*.test-support.ts"],"scripts":{"typecheck":"tsc --noEmit && tsc --noEmit -p tsconfig.test.json"},"dependencies":{"@a2ui/web_core":"0.13.0","zod3":"npm:zod@3.25.76"},"devDependencies":{"@types/node":"22.20.5","typescript":"^5.9.0","vitest":"^5.0.4"},"version":"0.
|
|
1
|
+
{"name":"@saga-a2ui/core","private":false,"description":"Renderer-independent SAGA A2UI processing: version-specific decoding, validated message cases and the message session, and the renderer-independent action runtime (local and server actions, prerequisites, bounded hooks, resource ownership) at /actions","license":"Apache-2.0","type":"module","exports":{"./actions":"./src/actions/index.ts","./actions/action-button-schema":"./src/actions/action-button-schema.ts","./delivered-result":"./src/delivered-result.ts","./host-links":"./src/host-links.ts","./protocols/a2ui/v0_9":"./src/protocols/a2ui/v0_9/index.ts","./protocols/a2ui/zod3":"./src/protocols/a2ui/zod3.ts"},"files":["src","!src/**/*.test.ts","!src/**/*.test-support.ts"],"scripts":{"typecheck":"tsc --noEmit && tsc --noEmit -p tsconfig.test.json"},"dependencies":{"@a2ui/web_core":"0.13.0","zod3":"npm:zod@3.25.76"},"devDependencies":{"@types/node":"22.20.5","typescript":"^5.9.0","vitest":"^5.0.4"},"version":"0.7.6","engines":{"node":">=22.12.0"},"repository":{"type":"git","url":"https://github.com/MachineWisdomAI/saga-a2ui.git","directory":"packages/core"},"publishConfig":{"access":"public","tag":"next"}}
|
package/src/actions/binding.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { planOf, type ActionDefinition } from './definition.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
emitObservation,
|
|
4
|
+
Invocation,
|
|
5
|
+
type InvocationOwner,
|
|
6
|
+
type Seam,
|
|
7
|
+
} from './invocation.js';
|
|
3
8
|
import { resolveLimits, type Limits } from './limits.js';
|
|
4
9
|
import { discardOutcome, isThenable } from './thenable.js';
|
|
5
10
|
|
|
@@ -63,7 +68,10 @@ let bindingCount = 0;
|
|
|
63
68
|
export function createBinding<Event>(
|
|
64
69
|
definition: ActionDefinition<Event>,
|
|
65
70
|
seam: BindingSeam,
|
|
66
|
-
): {
|
|
71
|
+
): {
|
|
72
|
+
readonly binding: ActionBinding;
|
|
73
|
+
readonly activate: (event: Event) => void;
|
|
74
|
+
} {
|
|
67
75
|
const plan = planOf(definition);
|
|
68
76
|
// Resolved here, once, from two immutable layers: an invocation's clocks cannot change afterwards.
|
|
69
77
|
const limits = resolveLimits(plan.limits, seam.limits ?? {});
|
|
@@ -100,7 +108,8 @@ export function createBinding<Event>(
|
|
|
100
108
|
bindingId: id,
|
|
101
109
|
isDisposed,
|
|
102
110
|
decided(invocation) {
|
|
103
|
-
if (owner === invocation && !isDisposed() && phase() === 'running')
|
|
111
|
+
if (owner === invocation && !isDisposed() && phase() === 'running')
|
|
112
|
+
setPhase('settling');
|
|
104
113
|
},
|
|
105
114
|
settled(invocation) {
|
|
106
115
|
unsettled.delete(invocation);
|
|
@@ -137,7 +146,11 @@ export function createBinding<Event>(
|
|
|
137
146
|
};
|
|
138
147
|
|
|
139
148
|
const ignore = (busy: Invocation<Event>): void => {
|
|
140
|
-
emitObservation(plan, id, Date.now(), {
|
|
149
|
+
emitObservation(plan, id, Date.now(), {
|
|
150
|
+
phase: 'ignored',
|
|
151
|
+
ignoredReason: 'busy',
|
|
152
|
+
busyInvocationId: busy.id,
|
|
153
|
+
});
|
|
141
154
|
};
|
|
142
155
|
|
|
143
156
|
const activate = (event: Event): void => {
|
|
@@ -152,7 +165,12 @@ export function createBinding<Event>(
|
|
|
152
165
|
ignore(owner);
|
|
153
166
|
return;
|
|
154
167
|
}
|
|
155
|
-
const invocation = new Invocation<Event>(
|
|
168
|
+
const invocation = new Invocation<Event>(
|
|
169
|
+
plan,
|
|
170
|
+
invocationOwner,
|
|
171
|
+
seam,
|
|
172
|
+
limits,
|
|
173
|
+
);
|
|
156
174
|
const replaced = owner;
|
|
157
175
|
owner = invocation;
|
|
158
176
|
replaced?.replaced();
|
package/src/actions/category.ts
CHANGED
|
@@ -17,7 +17,8 @@ export type KnownErrorCategory =
|
|
|
17
17
|
* Open on purpose: a consumer must handle a category it does not know, and `'unknown'` is the
|
|
18
18
|
* fallback when nothing established a cause. `(string & {})` keeps the known names in editors.
|
|
19
19
|
*/
|
|
20
|
-
export type ErrorCategory =
|
|
20
|
+
export type ErrorCategory =
|
|
21
|
+
KnownErrorCategory | (string & Record<never, never>);
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* A cause that says what happened. An application's handler, prerequisite or hook throws it when
|
|
@@ -30,7 +31,11 @@ export type ErrorCategory = KnownErrorCategory | (string & Record<never, never>)
|
|
|
30
31
|
export class ActionError extends Error {
|
|
31
32
|
readonly category: ErrorCategory;
|
|
32
33
|
|
|
33
|
-
constructor(
|
|
34
|
+
constructor(
|
|
35
|
+
category: ErrorCategory,
|
|
36
|
+
message: string,
|
|
37
|
+
options?: { readonly cause?: unknown },
|
|
38
|
+
) {
|
|
34
39
|
super(message, options);
|
|
35
40
|
if (typeof category !== 'string' || category === '') {
|
|
36
41
|
throw new TypeError('An ActionError needs a non-empty category.');
|
|
@@ -45,12 +50,16 @@ export class ActionError extends Error {
|
|
|
45
50
|
* failure the runtime itself decided on (for example input that could not be read) is that
|
|
46
51
|
* evidence and is passed in as the fallback.
|
|
47
52
|
*/
|
|
48
|
-
export function categorize(
|
|
53
|
+
export function categorize(
|
|
54
|
+
error: unknown,
|
|
55
|
+
fallback: ErrorCategory = 'unknown',
|
|
56
|
+
): ErrorCategory {
|
|
49
57
|
if (error instanceof ActionError) return error.category;
|
|
50
58
|
// The platform's own typed errors (what `AbortSignal.timeout` and `fetch` reject with) are
|
|
51
59
|
// DOMExceptions with these exact names. A plain Error that merely carries the same `name` is
|
|
52
60
|
// not typed evidence.
|
|
53
|
-
const platform: unknown = (globalThis as { DOMException?: unknown })
|
|
61
|
+
const platform: unknown = (globalThis as { DOMException?: unknown })
|
|
62
|
+
.DOMException;
|
|
54
63
|
if (typeof platform === 'function' && error instanceof platform) {
|
|
55
64
|
const { name } = error as { name?: unknown };
|
|
56
65
|
if (name === 'TimeoutError') return 'timeout';
|
|
@@ -11,7 +11,12 @@ import type {
|
|
|
11
11
|
import { readLimits, type Limits } from './limits.js';
|
|
12
12
|
import type { Prerequisite } from './prerequisite.js';
|
|
13
13
|
import type { ActivationScope, InvocationScope } from './scope.js';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
checkTransport,
|
|
16
|
+
type ServerCalls,
|
|
17
|
+
type ServerReply,
|
|
18
|
+
type ServerTransport,
|
|
19
|
+
} from './server.js';
|
|
15
20
|
import type { Synchronous } from './thenable.js';
|
|
16
21
|
|
|
17
22
|
/** What `atActivation` receives, in the event's own call stack. */
|
|
@@ -22,7 +27,11 @@ export interface ActivationContext<Event> {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/** What the handler receives for one activation. */
|
|
25
|
-
export interface HandlerContext<
|
|
30
|
+
export interface HandlerContext<
|
|
31
|
+
Params,
|
|
32
|
+
Activation,
|
|
33
|
+
Provided = unknown,
|
|
34
|
+
> extends Freshness {
|
|
26
35
|
/** What `input` copied out of the event, or `undefined` for an action without `input`. */
|
|
27
36
|
readonly params: Params;
|
|
28
37
|
/** What `atActivation` returned, or `undefined` for an action without it. */
|
|
@@ -51,7 +60,12 @@ interface HookContext extends Freshness {
|
|
|
51
60
|
// the resource this invocation took, whatever stage the outcome was decided in. A failure of the
|
|
52
61
|
// prefix itself has none: `atActivation` did not return one.
|
|
53
62
|
|
|
54
|
-
export interface SuccessContext<
|
|
63
|
+
export interface SuccessContext<
|
|
64
|
+
Params,
|
|
65
|
+
Activation,
|
|
66
|
+
Provided,
|
|
67
|
+
Result,
|
|
68
|
+
> extends HookContext {
|
|
55
69
|
readonly params: Params;
|
|
56
70
|
readonly activation: Activation;
|
|
57
71
|
readonly provided: Provided;
|
|
@@ -76,30 +90,31 @@ export type FailureContext<Params, Activation, Provided, Result> = HookContext &
|
|
|
76
90
|
}
|
|
77
91
|
);
|
|
78
92
|
|
|
79
|
-
export type CanceledContext<Params, Activation, Provided, Result> =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
93
|
+
export type CanceledContext<Params, Activation, Provided, Result> =
|
|
94
|
+
HookContext &
|
|
95
|
+
(
|
|
96
|
+
| {
|
|
97
|
+
readonly stage: 'prerequisites';
|
|
98
|
+
readonly cause: CancelCause;
|
|
99
|
+
readonly params: Params;
|
|
100
|
+
readonly activation: Activation;
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
readonly stage: 'running';
|
|
104
|
+
readonly cause: CancelCause;
|
|
105
|
+
readonly params: Params;
|
|
106
|
+
readonly activation: Activation;
|
|
107
|
+
readonly provided: Provided;
|
|
108
|
+
}
|
|
109
|
+
| {
|
|
110
|
+
readonly stage: 'completing';
|
|
111
|
+
readonly cause: CancelCause;
|
|
112
|
+
readonly params: Params;
|
|
113
|
+
readonly activation: Activation;
|
|
114
|
+
readonly provided: Provided;
|
|
115
|
+
readonly result: Result;
|
|
116
|
+
}
|
|
117
|
+
);
|
|
103
118
|
|
|
104
119
|
/**
|
|
105
120
|
* What final reporting can know: the immutable primary outcome as earlier hooks received it, plus
|
|
@@ -131,7 +146,10 @@ export interface ActionDefinition<Event> {
|
|
|
131
146
|
* any number of `before`, then `run`. `Calls` is what `run` also receives: nothing for a local
|
|
132
147
|
* action, the server call for a server action.
|
|
133
148
|
*/
|
|
134
|
-
export interface ActionDraft<
|
|
149
|
+
export interface ActionDraft<
|
|
150
|
+
Event,
|
|
151
|
+
Calls = unknown,
|
|
152
|
+
> extends ActionDraftAfterInput<Event, undefined, Calls> {
|
|
135
153
|
/**
|
|
136
154
|
* Copies what the handler needs out of the event, once, when it occurs. It runs synchronously
|
|
137
155
|
* in the event's own call stack and must not perform effects. A thrown error, or a returned
|
|
@@ -143,7 +161,11 @@ export interface ActionDraft<Event, Calls = unknown> extends ActionDraftAfterInp
|
|
|
143
161
|
): ActionDraftAfterInput<Event, Params, Calls>;
|
|
144
162
|
}
|
|
145
163
|
|
|
146
|
-
export interface ActionDraftAfterInput<
|
|
164
|
+
export interface ActionDraftAfterInput<
|
|
165
|
+
Event,
|
|
166
|
+
Params,
|
|
167
|
+
Calls = unknown,
|
|
168
|
+
> extends ActionDraftChain<Event, Params, undefined, unknown, Calls> {
|
|
147
169
|
/**
|
|
148
170
|
* Work that an API requires inside the event's own call stack, such as taking a lease before
|
|
149
171
|
* anything is awaited. It runs synchronously after `input`, in the environment and event stack
|
|
@@ -159,7 +181,13 @@ export interface ActionDraftAfterInput<Event, Params, Calls = unknown> extends A
|
|
|
159
181
|
): ActionDraftChain<Event, Params, Activation, unknown, Calls>;
|
|
160
182
|
}
|
|
161
183
|
|
|
162
|
-
export interface ActionDraftChain<
|
|
184
|
+
export interface ActionDraftChain<
|
|
185
|
+
Event,
|
|
186
|
+
Params,
|
|
187
|
+
Activation,
|
|
188
|
+
Provided = unknown,
|
|
189
|
+
Calls = unknown,
|
|
190
|
+
> {
|
|
163
191
|
/**
|
|
164
192
|
* A prerequisite: application work that must finish before the handler and may stop it. They
|
|
165
193
|
* run strictly in the order written, each after the previous one proceeded. The compiler
|
|
@@ -174,14 +202,32 @@ export interface ActionDraftChain<Event, Params, Activation, Provided = unknown,
|
|
|
174
202
|
* server action's handler also receives `call`, which sends its request through the transport.
|
|
175
203
|
*/
|
|
176
204
|
run<Result>(
|
|
177
|
-
handler: (
|
|
178
|
-
|
|
205
|
+
handler: (
|
|
206
|
+
context: HandlerContext<Params, Activation, Provided> & Calls,
|
|
207
|
+
) => Result | PromiseLike<Result>,
|
|
208
|
+
): ActionDraftWithHandler<
|
|
209
|
+
Event,
|
|
210
|
+
Params,
|
|
211
|
+
Activation,
|
|
212
|
+
Provided,
|
|
213
|
+
Result,
|
|
214
|
+
never,
|
|
215
|
+
Calls
|
|
216
|
+
>;
|
|
179
217
|
}
|
|
180
218
|
|
|
181
219
|
type HookName = 'onSuccess' | 'onFailure' | 'onCanceled' | 'onFinally';
|
|
182
220
|
type TailName = 'limits' | 'whenBusy' | 'observe' | HookName;
|
|
183
221
|
|
|
184
|
-
interface TailMembers<
|
|
222
|
+
interface TailMembers<
|
|
223
|
+
Event,
|
|
224
|
+
Params,
|
|
225
|
+
Activation,
|
|
226
|
+
Provided,
|
|
227
|
+
Result,
|
|
228
|
+
Used extends TailName,
|
|
229
|
+
Calls,
|
|
230
|
+
> {
|
|
185
231
|
/**
|
|
186
232
|
* Overrides the bounds for this action alone, for a local and a server action alike. Each
|
|
187
233
|
* setting is optional and resolves on its own: this override, then the owning SDK instance's
|
|
@@ -192,27 +238,89 @@ interface TailMembers<Event, Params, Activation, Provided, Result, Used extends
|
|
|
192
238
|
*/
|
|
193
239
|
limits(
|
|
194
240
|
limits: Limits,
|
|
195
|
-
): ActionDraftWithHandler<
|
|
241
|
+
): ActionDraftWithHandler<
|
|
242
|
+
Event,
|
|
243
|
+
Params,
|
|
244
|
+
Activation,
|
|
245
|
+
Provided,
|
|
246
|
+
Result,
|
|
247
|
+
Used | 'limits',
|
|
248
|
+
Calls
|
|
249
|
+
>;
|
|
196
250
|
/** `ignore` (the default) drops an activation while busy; `replace` cancels the running invocation. */
|
|
197
251
|
whenBusy(
|
|
198
252
|
policy: BusyPolicy,
|
|
199
|
-
): ActionDraftWithHandler<
|
|
253
|
+
): ActionDraftWithHandler<
|
|
254
|
+
Event,
|
|
255
|
+
Params,
|
|
256
|
+
Activation,
|
|
257
|
+
Provided,
|
|
258
|
+
Result,
|
|
259
|
+
Used | 'whenBusy',
|
|
260
|
+
Calls
|
|
261
|
+
>;
|
|
200
262
|
observe(
|
|
201
263
|
observer: ActionObserver,
|
|
202
|
-
): ActionDraftWithHandler<
|
|
264
|
+
): ActionDraftWithHandler<
|
|
265
|
+
Event,
|
|
266
|
+
Params,
|
|
267
|
+
Activation,
|
|
268
|
+
Provided,
|
|
269
|
+
Result,
|
|
270
|
+
Used | 'observe',
|
|
271
|
+
Calls
|
|
272
|
+
>;
|
|
203
273
|
/** Required client work that decides success. Its exception or overrun fails the invocation. */
|
|
204
274
|
onSuccess(
|
|
205
|
-
hook: (
|
|
206
|
-
|
|
275
|
+
hook: (
|
|
276
|
+
context: SuccessContext<Params, Activation, Provided, Result>,
|
|
277
|
+
) => void | PromiseLike<void>,
|
|
278
|
+
): ActionDraftWithHandler<
|
|
279
|
+
Event,
|
|
280
|
+
Params,
|
|
281
|
+
Activation,
|
|
282
|
+
Provided,
|
|
283
|
+
Result,
|
|
284
|
+
Used | 'onSuccess',
|
|
285
|
+
Calls
|
|
286
|
+
>;
|
|
207
287
|
onFailure(
|
|
208
|
-
hook: (
|
|
209
|
-
|
|
288
|
+
hook: (
|
|
289
|
+
context: FailureContext<Params, Activation, Provided, Result>,
|
|
290
|
+
) => void | PromiseLike<void>,
|
|
291
|
+
): ActionDraftWithHandler<
|
|
292
|
+
Event,
|
|
293
|
+
Params,
|
|
294
|
+
Activation,
|
|
295
|
+
Provided,
|
|
296
|
+
Result,
|
|
297
|
+
Used | 'onFailure',
|
|
298
|
+
Calls
|
|
299
|
+
>;
|
|
210
300
|
onCanceled(
|
|
211
|
-
hook: (
|
|
212
|
-
|
|
301
|
+
hook: (
|
|
302
|
+
context: CanceledContext<Params, Activation, Provided, Result>,
|
|
303
|
+
) => void | PromiseLike<void>,
|
|
304
|
+
): ActionDraftWithHandler<
|
|
305
|
+
Event,
|
|
306
|
+
Params,
|
|
307
|
+
Activation,
|
|
308
|
+
Provided,
|
|
309
|
+
Result,
|
|
310
|
+
Used | 'onCanceled',
|
|
311
|
+
Calls
|
|
312
|
+
>;
|
|
213
313
|
onFinally(
|
|
214
314
|
hook: (context: FinallyContext<Result>) => void | PromiseLike<void>,
|
|
215
|
-
): ActionDraftWithHandler<
|
|
315
|
+
): ActionDraftWithHandler<
|
|
316
|
+
Event,
|
|
317
|
+
Params,
|
|
318
|
+
Activation,
|
|
319
|
+
Provided,
|
|
320
|
+
Result,
|
|
321
|
+
Used | 'onFinally',
|
|
322
|
+
Calls
|
|
323
|
+
>;
|
|
216
324
|
build(): ActionDefinition<Event>;
|
|
217
325
|
}
|
|
218
326
|
|
|
@@ -225,7 +333,10 @@ export type ActionDraftWithHandler<
|
|
|
225
333
|
Result = unknown,
|
|
226
334
|
Used extends TailName = never,
|
|
227
335
|
Calls = unknown,
|
|
228
|
-
> = Omit<
|
|
336
|
+
> = Omit<
|
|
337
|
+
TailMembers<Event, Params, Activation, Provided, Result, Used, Calls>,
|
|
338
|
+
Used
|
|
339
|
+
>;
|
|
229
340
|
|
|
230
341
|
// ---------------------------------------------------------------------------------------------
|
|
231
342
|
// What the runtime reads. Internal: authors only see the definition.
|
|
@@ -233,9 +344,15 @@ export type ActionDraftWithHandler<
|
|
|
233
344
|
|
|
234
345
|
/** Hooks with their types erased: the builder's types are what tie them to a result. */
|
|
235
346
|
export interface ErasedHooks {
|
|
236
|
-
readonly onSuccess?: (
|
|
237
|
-
|
|
238
|
-
|
|
347
|
+
readonly onSuccess?: (
|
|
348
|
+
context: SuccessContext<unknown, unknown, unknown, unknown>,
|
|
349
|
+
) => unknown;
|
|
350
|
+
readonly onFailure?: (
|
|
351
|
+
context: FailureContext<unknown, unknown, unknown, unknown>,
|
|
352
|
+
) => unknown;
|
|
353
|
+
readonly onCanceled?: (
|
|
354
|
+
context: CanceledContext<unknown, unknown, unknown, unknown>,
|
|
355
|
+
) => unknown;
|
|
239
356
|
readonly onFinally?: (context: FinallyContext<unknown>) => unknown;
|
|
240
357
|
}
|
|
241
358
|
|
|
@@ -243,9 +360,12 @@ export interface ErasedHooks {
|
|
|
243
360
|
export interface ActionPlan<Event> {
|
|
244
361
|
readonly label: string;
|
|
245
362
|
readonly extract: ((event: Event) => unknown) | undefined;
|
|
246
|
-
readonly activate:
|
|
363
|
+
readonly activate:
|
|
364
|
+
((context: ActivationContext<Event>) => unknown) | undefined;
|
|
247
365
|
readonly prerequisites: readonly Prerequisite<object, unknown, unknown>[];
|
|
248
|
-
readonly handler: (
|
|
366
|
+
readonly handler: (
|
|
367
|
+
context: HandlerContext<unknown, unknown, unknown>,
|
|
368
|
+
) => unknown;
|
|
249
369
|
/** This action's own overrides: what the definition declared, validated and copied. Possibly empty. */
|
|
250
370
|
readonly limits: Limits;
|
|
251
371
|
readonly hooks: ErasedHooks;
|
|
@@ -265,9 +385,13 @@ class BuiltAction<Event> implements ActionDefinition<Event> {
|
|
|
265
385
|
}
|
|
266
386
|
|
|
267
387
|
/** The plan behind a definition. Refuses a value that `Action` did not build. */
|
|
268
|
-
export function planOf<Event>(
|
|
388
|
+
export function planOf<Event>(
|
|
389
|
+
definition: ActionDefinition<Event>,
|
|
390
|
+
): ActionPlan<Event> {
|
|
269
391
|
if (definition instanceof BuiltAction) return definition.plan;
|
|
270
|
-
throw new TypeError(
|
|
392
|
+
throw new TypeError(
|
|
393
|
+
'This is not an action definition. Build one with Action.local(...).run(...).build().',
|
|
394
|
+
);
|
|
271
395
|
}
|
|
272
396
|
|
|
273
397
|
type HookKey = keyof ErasedHooks;
|
|
@@ -304,7 +428,8 @@ class Draft {
|
|
|
304
428
|
}
|
|
305
429
|
|
|
306
430
|
private mustFollowRun(step: string): void {
|
|
307
|
-
if (this.declared.handler === undefined)
|
|
431
|
+
if (this.declared.handler === undefined)
|
|
432
|
+
throw new Error(`Action '${this.label}': ${step} can only follow run.`);
|
|
308
433
|
}
|
|
309
434
|
|
|
310
435
|
input(extract: unknown): Draft {
|
|
@@ -315,66 +440,106 @@ class Draft {
|
|
|
315
440
|
declared.prerequisites.length > 0 ||
|
|
316
441
|
declared.handler !== undefined
|
|
317
442
|
) {
|
|
318
|
-
throw new Error(
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Action '${this.label}': input can be declared once, and only before atActivation, before and run.`,
|
|
445
|
+
);
|
|
319
446
|
}
|
|
320
|
-
if (typeof extract !== 'function')
|
|
447
|
+
if (typeof extract !== 'function')
|
|
448
|
+
throw new TypeError(`Action '${this.label}': input needs a function.`);
|
|
321
449
|
return this.next({ extract: extract as Declared['extract'] });
|
|
322
450
|
}
|
|
323
451
|
|
|
324
452
|
atActivation(perform: unknown): Draft {
|
|
325
453
|
const { declared } = this;
|
|
326
|
-
if (
|
|
327
|
-
|
|
454
|
+
if (
|
|
455
|
+
declared.activate !== undefined ||
|
|
456
|
+
declared.prerequisites.length > 0 ||
|
|
457
|
+
declared.handler !== undefined
|
|
458
|
+
) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`Action '${this.label}': atActivation can be declared once, and only before before and run.`,
|
|
461
|
+
);
|
|
328
462
|
}
|
|
329
|
-
if (typeof perform !== 'function')
|
|
463
|
+
if (typeof perform !== 'function')
|
|
464
|
+
throw new TypeError(
|
|
465
|
+
`Action '${this.label}': atActivation needs a function.`,
|
|
466
|
+
);
|
|
330
467
|
return this.next({ activate: perform as Declared['activate'] });
|
|
331
468
|
}
|
|
332
469
|
|
|
333
470
|
before(candidate: unknown): Draft {
|
|
334
471
|
const { declared } = this;
|
|
335
|
-
if (declared.handler !== undefined)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
472
|
+
if (declared.handler !== undefined)
|
|
473
|
+
throw new Error(`Action '${this.label}': before can only precede run.`);
|
|
474
|
+
const { label, run } = (candidate ?? {}) as {
|
|
475
|
+
label?: unknown;
|
|
476
|
+
run?: unknown;
|
|
477
|
+
};
|
|
478
|
+
if (
|
|
479
|
+
typeof label !== 'string' ||
|
|
480
|
+
label === '' ||
|
|
481
|
+
typeof run !== 'function'
|
|
482
|
+
) {
|
|
483
|
+
throw new TypeError(
|
|
484
|
+
`Action '${this.label}': before needs a prerequisite with a label and a run function.`,
|
|
485
|
+
);
|
|
339
486
|
}
|
|
340
|
-
const kept = Object.freeze({ label, run }) as Prerequisite<
|
|
341
|
-
|
|
487
|
+
const kept = Object.freeze({ label, run }) as Prerequisite<
|
|
488
|
+
object,
|
|
489
|
+
unknown,
|
|
490
|
+
unknown
|
|
491
|
+
>;
|
|
492
|
+
return this.next({
|
|
493
|
+
prerequisites: Object.freeze([...declared.prerequisites, kept]),
|
|
494
|
+
});
|
|
342
495
|
}
|
|
343
496
|
|
|
344
497
|
run(handler: unknown): Draft {
|
|
345
|
-
if (this.declared.handler !== undefined)
|
|
346
|
-
|
|
498
|
+
if (this.declared.handler !== undefined)
|
|
499
|
+
throw new Error(`Action '${this.label}': run can be declared once.`);
|
|
500
|
+
if (typeof handler !== 'function')
|
|
501
|
+
throw new TypeError(`Action '${this.label}': run needs a function.`);
|
|
347
502
|
return this.next({ handler: handler as Declared['handler'] });
|
|
348
503
|
}
|
|
349
504
|
|
|
350
505
|
limits(limits: unknown): Draft {
|
|
351
506
|
this.mustFollowRun('limits');
|
|
352
|
-
if (this.declared.limits !== undefined)
|
|
507
|
+
if (this.declared.limits !== undefined)
|
|
508
|
+
throw new Error(`Action '${this.label}': limits can be declared once.`);
|
|
353
509
|
// Checked now, copied, and kept apart from the caller's object.
|
|
354
510
|
return this.next({ limits: readLimits(`Action '${this.label}'`, limits) });
|
|
355
511
|
}
|
|
356
512
|
|
|
357
513
|
whenBusy(policy: unknown): Draft {
|
|
358
514
|
this.mustFollowRun('whenBusy');
|
|
359
|
-
if (this.declared.busy !== undefined)
|
|
515
|
+
if (this.declared.busy !== undefined)
|
|
516
|
+
throw new Error(`Action '${this.label}': whenBusy can be declared once.`);
|
|
360
517
|
if (policy !== 'ignore' && policy !== 'replace') {
|
|
361
|
-
throw new TypeError(
|
|
518
|
+
throw new TypeError(
|
|
519
|
+
`Action '${this.label}': whenBusy takes 'ignore' or 'replace'.`,
|
|
520
|
+
);
|
|
362
521
|
}
|
|
363
522
|
return this.next({ busy: policy });
|
|
364
523
|
}
|
|
365
524
|
|
|
366
525
|
observe(observer: unknown): Draft {
|
|
367
526
|
this.mustFollowRun('observe');
|
|
368
|
-
if (this.declared.observer !== undefined)
|
|
369
|
-
|
|
527
|
+
if (this.declared.observer !== undefined)
|
|
528
|
+
throw new Error(`Action '${this.label}': observe can be declared once.`);
|
|
529
|
+
if (typeof observer !== 'function')
|
|
530
|
+
throw new TypeError(`Action '${this.label}': observe needs a function.`);
|
|
370
531
|
return this.next({ observer: observer as ActionObserver });
|
|
371
532
|
}
|
|
372
533
|
|
|
373
534
|
private withHook(key: HookKey, hook: unknown): Draft {
|
|
374
535
|
this.mustFollowRun(key);
|
|
375
|
-
if (this.declared.hooks[key] !== undefined)
|
|
376
|
-
|
|
377
|
-
|
|
536
|
+
if (this.declared.hooks[key] !== undefined)
|
|
537
|
+
throw new Error(`Action '${this.label}': ${key} can be declared once.`);
|
|
538
|
+
if (typeof hook !== 'function')
|
|
539
|
+
throw new TypeError(`Action '${this.label}': ${key} needs a function.`);
|
|
540
|
+
return this.next({
|
|
541
|
+
hooks: Object.freeze({ ...this.declared.hooks, [key]: hook }),
|
|
542
|
+
});
|
|
378
543
|
}
|
|
379
544
|
|
|
380
545
|
onSuccess(hook: unknown): Draft {
|
|
@@ -393,7 +558,8 @@ class Draft {
|
|
|
393
558
|
build(): ActionDefinition<never> {
|
|
394
559
|
const { label, declared } = this;
|
|
395
560
|
const { handler } = declared;
|
|
396
|
-
if (handler === undefined)
|
|
561
|
+
if (handler === undefined)
|
|
562
|
+
throw new Error(`Action '${label}' cannot be built before run.`);
|
|
397
563
|
return new BuiltAction<never>(label, {
|
|
398
564
|
label,
|
|
399
565
|
// Interop assertions, the only ones in this module. The typed interfaces above are what tie
|
|
@@ -413,8 +579,12 @@ class Draft {
|
|
|
413
579
|
}
|
|
414
580
|
}
|
|
415
581
|
|
|
416
|
-
function startDraft(
|
|
417
|
-
|
|
582
|
+
function startDraft(
|
|
583
|
+
label: string,
|
|
584
|
+
transport: ServerTransport | undefined,
|
|
585
|
+
): Draft {
|
|
586
|
+
if (typeof label !== 'string' || label === '')
|
|
587
|
+
throw new TypeError('An action needs a non-empty label.');
|
|
418
588
|
return new Draft(label, {
|
|
419
589
|
extract: undefined,
|
|
420
590
|
activate: undefined,
|
|
@@ -449,6 +619,9 @@ export const Action = {
|
|
|
449
619
|
transport: ServerTransport<Reply>,
|
|
450
620
|
): ActionDraft<Event, ServerCalls<Reply>> {
|
|
451
621
|
// Interop assertion, as for `local`.
|
|
452
|
-
return startDraft(
|
|
622
|
+
return startDraft(
|
|
623
|
+
label,
|
|
624
|
+
checkTransport(label, transport),
|
|
625
|
+
) as unknown as ActionDraft<Event, ServerCalls<Reply>>;
|
|
453
626
|
},
|
|
454
627
|
};
|
package/src/actions/index.ts
CHANGED
|
@@ -42,9 +42,21 @@ export type {
|
|
|
42
42
|
} from './server.js';
|
|
43
43
|
export type { Synchronous } from './thenable.js';
|
|
44
44
|
export { InvocationEndedError } from './scope.js';
|
|
45
|
-
export type {
|
|
45
|
+
export type {
|
|
46
|
+
ActivationScope,
|
|
47
|
+
Held,
|
|
48
|
+
InvocationScope,
|
|
49
|
+
Release,
|
|
50
|
+
ReleaseContext,
|
|
51
|
+
} from './scope.js';
|
|
46
52
|
export { prerequisite, proceed, refuse } from './prerequisite.js';
|
|
47
|
-
export type {
|
|
53
|
+
export type {
|
|
54
|
+
Prerequisite,
|
|
55
|
+
PrerequisiteContext,
|
|
56
|
+
PrerequisiteResult,
|
|
57
|
+
Proceed,
|
|
58
|
+
Refuse,
|
|
59
|
+
} from './prerequisite.js';
|
|
48
60
|
export type {
|
|
49
61
|
ActionObserver,
|
|
50
62
|
CancelCause,
|