@cratis/scene.engine 3.6.0 → 4.0.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/ActionDispatcher.ts +87 -0
- package/buildStarterProfile.ts +2 -2
- package/dist/cjs/buildStarterProfile.js.map +1 -1
- package/dist/cjs/index.js +9 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/resolveBehaviors.js +104 -0
- package/dist/cjs/resolveBehaviors.js.map +1 -0
- package/dist/cjs/runActions.js +219 -0
- package/dist/cjs/runActions.js.map +1 -0
- package/dist/esm/ActionDispatcher.d.ts +27 -0
- package/dist/esm/ActionDispatcher.d.ts.map +1 -0
- package/dist/esm/ActionDispatcher.js +2 -0
- package/dist/esm/ActionDispatcher.js.map +1 -0
- package/dist/esm/buildStarterProfile.d.ts +2 -2
- package/dist/esm/buildStarterProfile.d.ts.map +1 -1
- package/dist/esm/buildStarterProfile.js.map +1 -1
- package/dist/esm/for_resolveBehaviors/when_resolving_additive_attachments.d.ts +2 -0
- package/dist/esm/for_resolveBehaviors/when_resolving_additive_attachments.d.ts.map +1 -0
- package/dist/esm/for_resolveBehaviors/when_resolving_additive_attachments.js +68 -0
- package/dist/esm/for_resolveBehaviors/when_resolving_additive_attachments.js.map +1 -0
- package/dist/esm/for_runActions/given/a_recording_dispatcher.d.ts +21 -0
- package/dist/esm/for_runActions/given/a_recording_dispatcher.d.ts.map +1 -0
- package/dist/esm/for_runActions/given/a_recording_dispatcher.js +45 -0
- package/dist/esm/for_runActions/given/a_recording_dispatcher.js.map +1 -0
- package/dist/esm/for_runActions/when_running_a_sequence_of_actions.d.ts +2 -0
- package/dist/esm/for_runActions/when_running_a_sequence_of_actions.d.ts.map +1 -0
- package/dist/esm/for_runActions/when_running_a_sequence_of_actions.js +126 -0
- package/dist/esm/for_runActions/when_running_a_sequence_of_actions.js.map +1 -0
- package/dist/esm/for_runActions/when_running_against_the_shared_fixture_corpus.d.ts +2 -0
- package/dist/esm/for_runActions/when_running_against_the_shared_fixture_corpus.d.ts.map +1 -0
- package/dist/esm/for_runActions/when_running_against_the_shared_fixture_corpus.js +61 -0
- package/dist/esm/for_runActions/when_running_against_the_shared_fixture_corpus.js.map +1 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/resolveBehaviors.d.ts +16 -0
- package/dist/esm/resolveBehaviors.d.ts.map +1 -0
- package/dist/esm/resolveBehaviors.js +99 -0
- package/dist/esm/resolveBehaviors.js.map +1 -0
- package/dist/esm/runActions.d.ts +26 -0
- package/dist/esm/runActions.d.ts.map +1 -0
- package/dist/esm/runActions.js +215 -0
- package/dist/esm/runActions.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/for_resolveBehaviors/when_resolving_additive_attachments.ts +100 -0
- package/for_runActions/given/a_recording_dispatcher.ts +72 -0
- package/for_runActions/when_running_a_sequence_of_actions.ts +226 -0
- package/for_runActions/when_running_against_the_shared_fixture_corpus.ts +124 -0
- package/index.ts +3 -0
- package/package.json +3 -3
- package/resolveBehaviors.ts +137 -0
- package/runActions.ts +284 -0
package/runActions.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { InteractionBinding } from '@cratis/scene.model';
|
|
5
|
+
import { ResolvedBinding } from './resolveBehaviors';
|
|
6
|
+
import {
|
|
7
|
+
ConfirmAction,
|
|
8
|
+
ExecuteCommandAction,
|
|
9
|
+
InteractionAction,
|
|
10
|
+
InteractionActionKind,
|
|
11
|
+
InteractionArgument,
|
|
12
|
+
InteractionMessage,
|
|
13
|
+
NavigateAction,
|
|
14
|
+
NotifyAction,
|
|
15
|
+
OpenDialogAction,
|
|
16
|
+
RaiseTriggerAction,
|
|
17
|
+
RefreshQueryAction,
|
|
18
|
+
SetStateAction,
|
|
19
|
+
} from '@cratis/scene.model';
|
|
20
|
+
|
|
21
|
+
import { ActionDispatcher } from './ActionDispatcher';
|
|
22
|
+
import { BindingResolver } from './BindingResolver';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Why an interaction stopped before running everything it was given.
|
|
26
|
+
*/
|
|
27
|
+
export enum InteractionStop {
|
|
28
|
+
/** Everything ran. */
|
|
29
|
+
Completed = 'Completed',
|
|
30
|
+
|
|
31
|
+
/** A `confirm` was declined, so the actions it gates did not run. */
|
|
32
|
+
Declined = 'Declined',
|
|
33
|
+
|
|
34
|
+
/** A navigation happened, so anything after it belonged to a screen that is gone. */
|
|
35
|
+
Navigated = 'Navigated',
|
|
36
|
+
|
|
37
|
+
/** An action could not be realized and was reported. */
|
|
38
|
+
Reported = 'Reported',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Something an interaction could not do.
|
|
43
|
+
*
|
|
44
|
+
* Findings are collected rather than thrown: one unresolvable action must not take down the rest of the
|
|
45
|
+
* application, and 'this application produced no findings' has to be checkable rather than a matter of opinion.
|
|
46
|
+
*/
|
|
47
|
+
export interface InteractionFinding {
|
|
48
|
+
kind: 'unknownAction' | 'unresolvedMessage' | 'unresolvedCondition';
|
|
49
|
+
detail: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What an interaction did.
|
|
54
|
+
*/
|
|
55
|
+
export interface InteractionRun {
|
|
56
|
+
stop: InteractionStop;
|
|
57
|
+
findings: InteractionFinding[];
|
|
58
|
+
/** The actions that actually ran, in order - the thing a spec asserts on. */
|
|
59
|
+
ran: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How to resolve the values an action needs.
|
|
64
|
+
*/
|
|
65
|
+
export interface InteractionContext {
|
|
66
|
+
resolve: BindingResolver;
|
|
67
|
+
/** Looks up a localization key. Returns the key itself when the host has no catalogue for it. */
|
|
68
|
+
localize?: (key: string) => string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function describe(action: InteractionAction): string {
|
|
72
|
+
switch (action.kind) {
|
|
73
|
+
case InteractionActionKind.ExecuteCommand: return `execute ${(action as ExecuteCommandAction).command}`;
|
|
74
|
+
case InteractionActionKind.Navigate: return `navigate to ${(action as NavigateAction).screen}`;
|
|
75
|
+
case InteractionActionKind.NavigateBack: return 'navigate back';
|
|
76
|
+
case InteractionActionKind.OpenDialog: return `open dialog ${(action as OpenDialogAction).dialogTemplate}`;
|
|
77
|
+
case InteractionActionKind.CloseDialog: return 'close dialog';
|
|
78
|
+
case InteractionActionKind.RefreshQuery: return `refresh ${(action as RefreshQueryAction).query}`;
|
|
79
|
+
case InteractionActionKind.SetState: return `set ${(action as SetStateAction).target}`;
|
|
80
|
+
case InteractionActionKind.Notify: return 'notify';
|
|
81
|
+
case InteractionActionKind.Confirm: return 'confirm';
|
|
82
|
+
case InteractionActionKind.RaiseTrigger: return `raise ${(action as RaiseTriggerAction).trigger}`;
|
|
83
|
+
default: return String(action.kind);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function argumentsOf(action: InteractionAction, context: InteractionContext): Record<string, unknown> {
|
|
88
|
+
const args: Record<string, unknown> = {};
|
|
89
|
+
for (const argument of (action.arguments ?? []) as InteractionArgument[]) {
|
|
90
|
+
args[argument.name] = context.resolve(argument.value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return args;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function messageOf(message: InteractionMessage, context: InteractionContext, findings: InteractionFinding[]): string {
|
|
97
|
+
if (message.text !== undefined) return message.text;
|
|
98
|
+
if (message.stringsKey !== undefined) return context.localize ? context.localize(message.stringsKey) : message.stringsKey;
|
|
99
|
+
if (message.binding !== undefined) {
|
|
100
|
+
const resolved = context.resolve(message.binding);
|
|
101
|
+
if (resolved === undefined || resolved === null) {
|
|
102
|
+
findings.push({ kind: 'unresolvedMessage', detail: `The message binding '${message.binding.path}' resolved to nothing` });
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return String(resolved);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
findings.push({ kind: 'unresolvedMessage', detail: 'The action carries no message' });
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Runs a sequence of actions, in order, handling continuations.
|
|
115
|
+
*
|
|
116
|
+
* The rules, all of which live here rather than in a renderer so they can be asserted without a DOM:
|
|
117
|
+
*
|
|
118
|
+
* - Actions run in the order they were declared.
|
|
119
|
+
* - A declined `confirm` stops the sequence. Gating what follows is the whole point of it.
|
|
120
|
+
* - A navigation stops the sequence, because the screen the remaining actions were written for is gone. The
|
|
121
|
+
* compiler reports that as unreachable, so reaching it at runtime means the document changed underneath.
|
|
122
|
+
* - An action that can fail branches into `onSuccess` or `onFailure`; a dialog also runs `onResult`.
|
|
123
|
+
* - An action kind the engine does not know is **reported and the sequence abandoned**, never silently skipped.
|
|
124
|
+
* Continuing would half-apply a sequence whose gate may well have been the action this renderer could not
|
|
125
|
+
* understand. This is what lets a newer document be opened by an older renderer without pretending it worked.
|
|
126
|
+
*
|
|
127
|
+
* @param actions The actions to run.
|
|
128
|
+
* @param dispatcher The seam that performs the effects.
|
|
129
|
+
* @param context How to resolve bindings and localization.
|
|
130
|
+
* @returns What happened.
|
|
131
|
+
*/
|
|
132
|
+
export async function runActions(
|
|
133
|
+
actions: InteractionAction[],
|
|
134
|
+
dispatcher: ActionDispatcher,
|
|
135
|
+
context: InteractionContext
|
|
136
|
+
): Promise<InteractionRun> {
|
|
137
|
+
const run: InteractionRun = { stop: InteractionStop.Completed, findings: [], ran: [] };
|
|
138
|
+
await runSequence(actions, dispatcher, context, run);
|
|
139
|
+
return run;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function runSequence(
|
|
143
|
+
actions: InteractionAction[],
|
|
144
|
+
dispatcher: ActionDispatcher,
|
|
145
|
+
context: InteractionContext,
|
|
146
|
+
run: InteractionRun
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
for (const action of actions) {
|
|
149
|
+
if (await runOne(action, dispatcher, context, run)) return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function runOne(
|
|
154
|
+
action: InteractionAction,
|
|
155
|
+
dispatcher: ActionDispatcher,
|
|
156
|
+
context: InteractionContext,
|
|
157
|
+
run: InteractionRun
|
|
158
|
+
): Promise<boolean> {
|
|
159
|
+
const args = argumentsOf(action, context);
|
|
160
|
+
|
|
161
|
+
switch (action.kind) {
|
|
162
|
+
case InteractionActionKind.ExecuteCommand: {
|
|
163
|
+
run.ran.push(describe(action));
|
|
164
|
+
const outcome = await dispatcher.executeCommand((action as ExecuteCommandAction).command, args);
|
|
165
|
+
await runSequence(outcome.isSuccess ? (action.onSuccess ?? []) : (action.onFailure ?? []), dispatcher, context, run);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case InteractionActionKind.Navigate:
|
|
170
|
+
run.ran.push(describe(action));
|
|
171
|
+
await dispatcher.navigate((action as NavigateAction).screen, args);
|
|
172
|
+
run.stop = InteractionStop.Navigated;
|
|
173
|
+
return true;
|
|
174
|
+
|
|
175
|
+
case InteractionActionKind.NavigateBack:
|
|
176
|
+
run.ran.push(describe(action));
|
|
177
|
+
await dispatcher.navigateBack();
|
|
178
|
+
run.stop = InteractionStop.Navigated;
|
|
179
|
+
return true;
|
|
180
|
+
|
|
181
|
+
case InteractionActionKind.OpenDialog: {
|
|
182
|
+
run.ran.push(describe(action));
|
|
183
|
+
const outcome = await dispatcher.openDialog((action as OpenDialogAction).dialogTemplate, args);
|
|
184
|
+
await runSequence(outcome.isConfirmed ? (action.onSuccess ?? []) : (action.onFailure ?? []), dispatcher, context, run);
|
|
185
|
+
await runSequence((action.onResult ?? []), dispatcher, context, run);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case InteractionActionKind.CloseDialog:
|
|
190
|
+
run.ran.push(describe(action));
|
|
191
|
+
await dispatcher.closeDialog();
|
|
192
|
+
return false;
|
|
193
|
+
|
|
194
|
+
case InteractionActionKind.RefreshQuery: {
|
|
195
|
+
run.ran.push(describe(action));
|
|
196
|
+
await dispatcher.refreshQuery((action as RefreshQueryAction).query);
|
|
197
|
+
await runSequence((action.onSuccess ?? []), dispatcher, context, run);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case InteractionActionKind.SetState:
|
|
202
|
+
run.ran.push(describe(action));
|
|
203
|
+
dispatcher.setState((action as SetStateAction).target, context.resolve((action as SetStateAction).value));
|
|
204
|
+
return false;
|
|
205
|
+
|
|
206
|
+
case InteractionActionKind.Notify:
|
|
207
|
+
run.ran.push(describe(action));
|
|
208
|
+
dispatcher.notify((action as NotifyAction).level, messageOf((action as NotifyAction).message, context, run.findings));
|
|
209
|
+
return false;
|
|
210
|
+
|
|
211
|
+
case InteractionActionKind.Confirm: {
|
|
212
|
+
run.ran.push(describe(action));
|
|
213
|
+
const confirmed = await dispatcher.confirm(messageOf((action as ConfirmAction).message, context, run.findings));
|
|
214
|
+
if (!confirmed) {
|
|
215
|
+
await runSequence((action.onFailure ?? []), dispatcher, context, run);
|
|
216
|
+
run.stop = InteractionStop.Declined;
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
await runSequence((action.onSuccess ?? []), dispatcher, context, run);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case InteractionActionKind.RaiseTrigger: {
|
|
225
|
+
run.ran.push(describe(action));
|
|
226
|
+
await dispatcher.raise((action as RaiseTriggerAction).trigger, args);
|
|
227
|
+
await runSequence((action.onSuccess ?? []), dispatcher, context, run);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
default:
|
|
232
|
+
run.findings.push({ kind: 'unknownAction', detail: `The renderer does not know the action kind '${String(action.kind)}'` });
|
|
233
|
+
run.stop = InteractionStop.Reported;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Runs the bindings a trigger resolved to, in order, honouring each one's guard.
|
|
240
|
+
*
|
|
241
|
+
* The `where` guard belongs here rather than in a renderer for the same reason the sequencing does: whether a
|
|
242
|
+
* binding applies is a property of the document, not of the event that arrived. A guard that resolves to
|
|
243
|
+
* nothing is treated as false and reported - an interaction that silently does nothing is indistinguishable
|
|
244
|
+
* from one that is broken, and the whole point of a finding is to tell those apart.
|
|
245
|
+
*
|
|
246
|
+
* @param bindings The bindings to run, already resolved and ordered.
|
|
247
|
+
* @param dispatcher The seam that performs the effects.
|
|
248
|
+
* @param context How to resolve bindings and localization.
|
|
249
|
+
* @returns What happened across all of them.
|
|
250
|
+
*/
|
|
251
|
+
export async function runBindings(
|
|
252
|
+
bindings: ResolvedBinding[],
|
|
253
|
+
dispatcher: ActionDispatcher,
|
|
254
|
+
context: InteractionContext
|
|
255
|
+
): Promise<InteractionRun> {
|
|
256
|
+
const run: InteractionRun = { stop: InteractionStop.Completed, findings: [], ran: [] };
|
|
257
|
+
|
|
258
|
+
for (const candidate of bindings) {
|
|
259
|
+
if (!applies(candidate.binding, context, run)) continue;
|
|
260
|
+
|
|
261
|
+
await runSequence(candidate.binding.actions ?? [], dispatcher, context, run);
|
|
262
|
+
|
|
263
|
+
// A navigation or a declined confirm ends the interaction, not merely the binding that caused it.
|
|
264
|
+
// The screen the later bindings were attached to is gone, or the gate they sat behind was refused.
|
|
265
|
+
if (run.stop !== InteractionStop.Completed) break;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return run;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function applies(binding: InteractionBinding, context: InteractionContext, run: InteractionRun): boolean {
|
|
272
|
+
if (!binding.condition) return true;
|
|
273
|
+
|
|
274
|
+
const value = context.resolve(binding.condition);
|
|
275
|
+
if (value === undefined || value === null) {
|
|
276
|
+
run.findings.push({
|
|
277
|
+
kind: 'unresolvedCondition',
|
|
278
|
+
detail: `The guard '${binding.condition.path}' resolved to nothing, so the binding did not run`,
|
|
279
|
+
});
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return Boolean(value);
|
|
284
|
+
}
|