@player-ui/player 0.13.0-next.1 → 0.13.0-next.2
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/dist/Player.native.js +34 -88
- package/dist/Player.native.js.map +1 -1
- package/dist/cjs/index.cjs +16 -25
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.legacy-esm.js +18 -27
- package/dist/index.mjs +18 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/flow.test.ts +165 -1
- package/src/__tests__/player.test.ts +51 -1
- package/src/controllers/flow/controller.ts +9 -2
- package/src/controllers/flow/flow.ts +54 -16
- package/src/controllers/view/controller.ts +1 -2
- package/src/expressions/__tests__/evaluator.test.ts +10 -0
- package/src/player.ts +21 -22
- package/types/controllers/flow/controller.d.ts +4 -3
- package/types/controllers/flow/flow.d.ts +31 -18
package/package.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"types"
|
|
7
7
|
],
|
|
8
8
|
"name": "@player-ui/player",
|
|
9
|
-
"version": "0.13.0-next.
|
|
9
|
+
"version": "0.13.0-next.2",
|
|
10
10
|
"main": "dist/cjs/index.cjs",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@player-ui/partial-match-registry": "0.13.0-next.
|
|
13
|
-
"@player-ui/make-flow": "0.13.0-next.
|
|
14
|
-
"@player-ui/types": "0.13.0-next.
|
|
12
|
+
"@player-ui/partial-match-registry": "0.13.0-next.2",
|
|
13
|
+
"@player-ui/make-flow": "0.13.0-next.2",
|
|
14
|
+
"@player-ui/types": "0.13.0-next.2",
|
|
15
15
|
"@types/dlv": "^1.1.4",
|
|
16
16
|
"dequal": "^2.0.2",
|
|
17
17
|
"dlv": "^1.1.3",
|
|
@@ -4,6 +4,7 @@ import type { DataController } from "..";
|
|
|
4
4
|
import { Player } from "..";
|
|
5
5
|
import type { InProgressState } from "../types";
|
|
6
6
|
import { waitFor } from "@testing-library/react";
|
|
7
|
+
import { describe } from "node:test";
|
|
7
8
|
|
|
8
9
|
test("transitions on action nodes", async () => {
|
|
9
10
|
const player = new Player();
|
|
@@ -314,9 +315,10 @@ test("works with iffe flows", async () => {
|
|
|
314
315
|
|
|
315
316
|
test("awaited async transitions", async () => {
|
|
316
317
|
const player = new Player();
|
|
317
|
-
|
|
318
|
+
let counter = 0;
|
|
318
319
|
player.hooks.expressionEvaluator.tap("test", (expEval) => {
|
|
319
320
|
expEval.addExpressionFunction("testAsync", async (ctx, name) => {
|
|
321
|
+
counter += 1;
|
|
320
322
|
return new Promise((resolve) => {
|
|
321
323
|
setTimeout(() => {
|
|
322
324
|
resolve(name);
|
|
@@ -377,6 +379,8 @@ test("awaited async transitions", async () => {
|
|
|
377
379
|
},
|
|
378
380
|
transitions: {},
|
|
379
381
|
});
|
|
382
|
+
|
|
383
|
+
expect(counter).toEqual(1);
|
|
380
384
|
});
|
|
381
385
|
|
|
382
386
|
test("unawaited async transitions", async () => {
|
|
@@ -445,3 +449,163 @@ test("unawaited async transitions", async () => {
|
|
|
445
449
|
expect(mockFn2).not.toHaveBeenCalled();
|
|
446
450
|
});
|
|
447
451
|
});
|
|
452
|
+
|
|
453
|
+
describe("edge cases", () => {
|
|
454
|
+
test("async action nodes firing async expressions more than once after a regular action node", async () => {
|
|
455
|
+
const player = new Player();
|
|
456
|
+
let asyncCounter = 0;
|
|
457
|
+
let syncCounter = 0;
|
|
458
|
+
player.hooks.expressionEvaluator.tap("test", (expEval) => {
|
|
459
|
+
expEval.addExpressionFunction("testAsync", async (ctx, name) => {
|
|
460
|
+
asyncCounter += 1;
|
|
461
|
+
return new Promise((resolve) => {
|
|
462
|
+
setTimeout(() => {
|
|
463
|
+
resolve(name);
|
|
464
|
+
}, 10);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
expEval.addExpressionFunction("testSync", (ctx) => {
|
|
468
|
+
syncCounter += 1;
|
|
469
|
+
return "foo";
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
player.start({
|
|
474
|
+
id: "test-flow",
|
|
475
|
+
data: {
|
|
476
|
+
my: {
|
|
477
|
+
puppy: "Ginger",
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
navigation: {
|
|
481
|
+
BEGIN: "FLOW_1",
|
|
482
|
+
FLOW_1: {
|
|
483
|
+
startState: "ACTION_1",
|
|
484
|
+
ACTION_1: {
|
|
485
|
+
state_type: "ACTION",
|
|
486
|
+
exp: "{{something}} = testSync()",
|
|
487
|
+
transitions: {
|
|
488
|
+
"*": "ACTION_2",
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
ACTION_2: {
|
|
492
|
+
state_type: "ACTION",
|
|
493
|
+
exp: "{{something}} = testSync()",
|
|
494
|
+
transitions: {
|
|
495
|
+
"*": "ACTION_3",
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
ACTION_3: {
|
|
499
|
+
state_type: "ASYNC_ACTION",
|
|
500
|
+
exp: "{{my.puppy}} = await(testAsync('Daisy'))",
|
|
501
|
+
transitions: {
|
|
502
|
+
Daisy: "EXTERNAL_1",
|
|
503
|
+
},
|
|
504
|
+
await: true,
|
|
505
|
+
},
|
|
506
|
+
EXTERNAL_1: {
|
|
507
|
+
state_type: "EXTERNAL",
|
|
508
|
+
ref: "view_1",
|
|
509
|
+
param: {
|
|
510
|
+
best: "{{my.puppy}}",
|
|
511
|
+
},
|
|
512
|
+
transitions: {},
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
await vitest.waitFor(() =>
|
|
519
|
+
expect(player.getState().status).toBe("in-progress"),
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
let currentState: NamedState | undefined;
|
|
523
|
+
|
|
524
|
+
await waitFor(() => {
|
|
525
|
+
const state = player.getState();
|
|
526
|
+
currentState = (state as InProgressState).controllers.flow.current
|
|
527
|
+
?.currentState;
|
|
528
|
+
expect(currentState?.name).toBe("EXTERNAL_1");
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
expect(currentState?.value.ref).toStrictEqual("view_1");
|
|
532
|
+
|
|
533
|
+
expect(asyncCounter).toEqual(1);
|
|
534
|
+
expect(syncCounter).toEqual(2);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("async action nodes only transition when the async action is resolved", async () => {
|
|
538
|
+
const player = new Player();
|
|
539
|
+
let expressionResolve: (val: any) => void;
|
|
540
|
+
|
|
541
|
+
player.hooks.expressionEvaluator.tap("test", (expEval) => {
|
|
542
|
+
expEval.addExpressionFunction("testAsync", async (ctx, name) => {
|
|
543
|
+
return new Promise((resolve) => {
|
|
544
|
+
expressionResolve = resolve;
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
player.start({
|
|
550
|
+
id: "test-flow",
|
|
551
|
+
views: [
|
|
552
|
+
{
|
|
553
|
+
id: "test",
|
|
554
|
+
type: "test",
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
data: {
|
|
558
|
+
my: {
|
|
559
|
+
puppy: "Ginger",
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
navigation: {
|
|
563
|
+
BEGIN: "FLOW_1",
|
|
564
|
+
FLOW_1: {
|
|
565
|
+
startState: "ACTION_1",
|
|
566
|
+
ACTION_1: {
|
|
567
|
+
state_type: "ASYNC_ACTION",
|
|
568
|
+
exp: "{{my.puppy}} = await(testAsync('Daisy'))",
|
|
569
|
+
transitions: {
|
|
570
|
+
"*": "VIEW_1",
|
|
571
|
+
},
|
|
572
|
+
await: true,
|
|
573
|
+
},
|
|
574
|
+
VIEW_1: {
|
|
575
|
+
state_type: "VIEW",
|
|
576
|
+
ref: "test",
|
|
577
|
+
transitions: {
|
|
578
|
+
"*": "END",
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
END: {
|
|
582
|
+
state_type: "END",
|
|
583
|
+
outcome: "done",
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
await vitest.waitFor(() =>
|
|
590
|
+
expect(player.getState().status).toBe("in-progress"),
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
let currentState: NamedState | undefined;
|
|
594
|
+
|
|
595
|
+
await waitFor(() => {
|
|
596
|
+
const state = player.getState();
|
|
597
|
+
expect(state.status).toBe("in-progress");
|
|
598
|
+
currentState = (state as InProgressState).controllers.flow.current
|
|
599
|
+
?.currentState;
|
|
600
|
+
expect(currentState?.name).toBe("ACTION_1");
|
|
601
|
+
expressionResolve("foo");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
await waitFor(() => {
|
|
605
|
+
const state = player.getState();
|
|
606
|
+
currentState = (state as InProgressState).controllers.flow.current
|
|
607
|
+
?.currentState;
|
|
608
|
+
expect(currentState?.name).toBe("VIEW_1");
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
});
|
|
@@ -543,7 +543,7 @@ describe("failure cases", () => {
|
|
|
543
543
|
);
|
|
544
544
|
});
|
|
545
545
|
|
|
546
|
-
it("fails gracefully when states after an ACTION state have failures", async () => {
|
|
546
|
+
it("fails gracefully when states after an ACTION state have failures (sync)", async () => {
|
|
547
547
|
const player = new Player();
|
|
548
548
|
|
|
549
549
|
const payload = {
|
|
@@ -581,6 +581,56 @@ describe("failure cases", () => {
|
|
|
581
581
|
);
|
|
582
582
|
});
|
|
583
583
|
|
|
584
|
+
it("fails gracefully when states after an ACTION state have failures (async)", async () => {
|
|
585
|
+
const player = new Player();
|
|
586
|
+
|
|
587
|
+
player.hooks.expressionEvaluator.tap("test", (expEval) => {
|
|
588
|
+
expEval.addExpressionFunction("testAsync", async (ctx, name) => {
|
|
589
|
+
return new Promise((resolve) => {
|
|
590
|
+
setTimeout(() => {
|
|
591
|
+
resolve(name);
|
|
592
|
+
}, 1);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const payload = {
|
|
598
|
+
id: "test",
|
|
599
|
+
views: [
|
|
600
|
+
{
|
|
601
|
+
id: "view",
|
|
602
|
+
type: "text",
|
|
603
|
+
value: "Some text",
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
data: {},
|
|
607
|
+
navigation: {
|
|
608
|
+
BEGIN: "Flow",
|
|
609
|
+
Flow: {
|
|
610
|
+
startState: "ActionState",
|
|
611
|
+
ActionState: {
|
|
612
|
+
state_type: "ASYNC_ACTION",
|
|
613
|
+
exp: "await(testAsync('test'))",
|
|
614
|
+
transitions: {
|
|
615
|
+
test: "ViewState",
|
|
616
|
+
},
|
|
617
|
+
await: true,
|
|
618
|
+
},
|
|
619
|
+
ViewState: {
|
|
620
|
+
state_type: "VIEW",
|
|
621
|
+
ref: "non-existing-view-async",
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const response = player.start(makeFlow(payload));
|
|
628
|
+
|
|
629
|
+
await expect(response).rejects.toThrowError(
|
|
630
|
+
"No view with id non-existing-view-async",
|
|
631
|
+
);
|
|
632
|
+
});
|
|
633
|
+
|
|
584
634
|
it("can be failed from other places", async () => {
|
|
585
635
|
const player = new Player();
|
|
586
636
|
|
|
@@ -4,9 +4,13 @@ import type { Logger } from "../../logger";
|
|
|
4
4
|
import type { TransitionOptions } from "./flow";
|
|
5
5
|
import { FlowInstance } from "./flow";
|
|
6
6
|
|
|
7
|
+
export interface FlowControllerHooks {
|
|
8
|
+
flow: SyncHook<[FlowInstance], Record<string, any>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
/** A manager for the navigation section of a Content blob */
|
|
8
12
|
export class FlowController {
|
|
9
|
-
public readonly hooks = {
|
|
13
|
+
public readonly hooks: FlowControllerHooks = {
|
|
10
14
|
flow: new SyncHook<[FlowInstance]>(),
|
|
11
15
|
};
|
|
12
16
|
|
|
@@ -33,7 +37,10 @@ export class FlowController {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
/** Navigate to another state in the state-machine */
|
|
36
|
-
public transition(
|
|
40
|
+
public transition(
|
|
41
|
+
stateTransition: string,
|
|
42
|
+
options?: TransitionOptions,
|
|
43
|
+
): void {
|
|
37
44
|
if (this.current === undefined) {
|
|
38
45
|
throw new Error("Not currently in a flow. Cannot transition.");
|
|
39
46
|
}
|
|
@@ -5,6 +5,11 @@ import type {
|
|
|
5
5
|
NavigationFlow,
|
|
6
6
|
NavigationFlowState,
|
|
7
7
|
NavigationFlowEndState,
|
|
8
|
+
NavigationFlowActionState,
|
|
9
|
+
NavigationFlowAsyncActionState,
|
|
10
|
+
NavigationFlowExternalState,
|
|
11
|
+
NavigationFlowFlowState,
|
|
12
|
+
NavigationFlowViewState,
|
|
8
13
|
} from "@player-ui/types";
|
|
9
14
|
import type { Logger } from "../../logger";
|
|
10
15
|
|
|
@@ -25,6 +30,50 @@ export type TransitionFunction = (
|
|
|
25
30
|
options?: TransitionOptions,
|
|
26
31
|
) => void;
|
|
27
32
|
|
|
33
|
+
export interface FlowInstanceHooks {
|
|
34
|
+
beforeStart: SyncBailHook<
|
|
35
|
+
[NavigationFlow],
|
|
36
|
+
NavigationFlow,
|
|
37
|
+
Record<string, any>
|
|
38
|
+
>;
|
|
39
|
+
/** A callback when the onStart node was present */
|
|
40
|
+
onStart: SyncHook<[any], Record<string, any>>;
|
|
41
|
+
/** A callback when the onEnd node was present */
|
|
42
|
+
onEnd: SyncHook<[any], Record<string, any>>;
|
|
43
|
+
/** A hook to intercept and block a transition */
|
|
44
|
+
skipTransition: SyncBailHook<
|
|
45
|
+
[NamedState | undefined],
|
|
46
|
+
boolean | undefined,
|
|
47
|
+
Record<string, any>
|
|
48
|
+
>;
|
|
49
|
+
/** A chance to manipulate the flow-node used to calculate the given transition used */
|
|
50
|
+
beforeTransition: SyncWaterfallHook<
|
|
51
|
+
[
|
|
52
|
+
(
|
|
53
|
+
| NavigationFlowViewState
|
|
54
|
+
| NavigationFlowFlowState
|
|
55
|
+
| NavigationFlowActionState
|
|
56
|
+
| NavigationFlowAsyncActionState
|
|
57
|
+
| NavigationFlowExternalState
|
|
58
|
+
),
|
|
59
|
+
string,
|
|
60
|
+
],
|
|
61
|
+
Record<string, any>
|
|
62
|
+
>;
|
|
63
|
+
/** A chance to manipulate the flow-node calculated after a transition */
|
|
64
|
+
resolveTransitionNode: SyncWaterfallHook<
|
|
65
|
+
[NavigationFlowState],
|
|
66
|
+
Record<string, any>
|
|
67
|
+
>;
|
|
68
|
+
/** A callback when a transition from 1 state to another was made */
|
|
69
|
+
transition: SyncHook<
|
|
70
|
+
[NamedState | undefined, NamedState],
|
|
71
|
+
Record<string, any>
|
|
72
|
+
>;
|
|
73
|
+
/** A callback to run actions after a transition occurs */
|
|
74
|
+
afterTransition: SyncHook<[FlowInstance], Record<string, any>>;
|
|
75
|
+
}
|
|
76
|
+
|
|
28
77
|
/** The Content navigation state machine */
|
|
29
78
|
export class FlowInstance {
|
|
30
79
|
private flow: NavigationFlow;
|
|
@@ -34,33 +83,19 @@ export class FlowInstance {
|
|
|
34
83
|
private flowPromise?: DeferredPromise<NavigationFlowEndState>;
|
|
35
84
|
public readonly id: string;
|
|
36
85
|
public currentState?: NamedState;
|
|
37
|
-
public readonly hooks = {
|
|
86
|
+
public readonly hooks: FlowInstanceHooks = {
|
|
38
87
|
beforeStart: new SyncBailHook<[NavigationFlow], NavigationFlow>(),
|
|
39
|
-
|
|
40
|
-
/** A callback when the onStart node was present */
|
|
41
88
|
onStart: new SyncHook<[any]>(),
|
|
42
|
-
|
|
43
|
-
/** A callback when the onEnd node was present */
|
|
44
89
|
onEnd: new SyncHook<[any]>(),
|
|
45
|
-
|
|
46
|
-
/** A hook to intercept and block a transition */
|
|
47
90
|
skipTransition: new SyncBailHook<
|
|
48
91
|
[NamedState | undefined],
|
|
49
92
|
boolean | undefined
|
|
50
93
|
>(),
|
|
51
|
-
|
|
52
|
-
/** A chance to manipulate the flow-node used to calculate the given transition used */
|
|
53
94
|
beforeTransition: new SyncWaterfallHook<
|
|
54
95
|
[Exclude<NavigationFlowState, NavigationFlowEndState>, string]
|
|
55
96
|
>(),
|
|
56
|
-
|
|
57
|
-
/** A chance to manipulate the flow-node calculated after a transition */
|
|
58
97
|
resolveTransitionNode: new SyncWaterfallHook<[NavigationFlowState]>(),
|
|
59
|
-
|
|
60
|
-
/** A callback when a transition from 1 state to another was made */
|
|
61
98
|
transition: new SyncHook<[NamedState | undefined, NamedState]>(),
|
|
62
|
-
|
|
63
|
-
/** A callback to run actions after a transition occurs */
|
|
64
99
|
afterTransition: new SyncHook<[FlowInstance]>(),
|
|
65
100
|
};
|
|
66
101
|
|
|
@@ -115,7 +150,10 @@ export class FlowInstance {
|
|
|
115
150
|
return this.flowPromise.promise;
|
|
116
151
|
}
|
|
117
152
|
|
|
118
|
-
public transition(
|
|
153
|
+
public transition(
|
|
154
|
+
transitionValue: string,
|
|
155
|
+
options?: TransitionOptions,
|
|
156
|
+
): void {
|
|
119
157
|
if (this.isTransitioning) {
|
|
120
158
|
throw new Error(
|
|
121
159
|
`Transitioning while ongoing transition from ${this.currentState?.name} is in progress is not supported`,
|
|
@@ -71,7 +71,6 @@ export class ViewController {
|
|
|
71
71
|
this.viewOptions = options;
|
|
72
72
|
this.viewMap = initialViews.reduce<Record<string, View>>(
|
|
73
73
|
(viewMap, view) => {
|
|
74
|
-
// eslint-disable-next-line no-param-reassign
|
|
75
74
|
viewMap[view.id] = view;
|
|
76
75
|
return viewMap;
|
|
77
76
|
},
|
|
@@ -171,7 +170,7 @@ export class ViewController {
|
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
172
|
|
|
174
|
-
public onView(state: NavigationFlowViewState) {
|
|
173
|
+
public onView(state: NavigationFlowViewState): void {
|
|
175
174
|
const viewId = state.ref;
|
|
176
175
|
|
|
177
176
|
const source = this.hooks.resolveView.call(
|
|
@@ -470,6 +470,16 @@ describe("async evaluator", () => {
|
|
|
470
470
|
expect(await result).toBe("truthy");
|
|
471
471
|
});
|
|
472
472
|
|
|
473
|
+
test("Async functions are only called once", async () => {
|
|
474
|
+
const mockHandler = vitest.fn().mockReturnValue(Promise.resolve(true));
|
|
475
|
+
evaluator.addExpressionFunction("asyncTest", mockHandler);
|
|
476
|
+
|
|
477
|
+
const result = evaluator.evaluateAsync("await(asyncTest())");
|
|
478
|
+
expect(result).toBeInstanceOf(Promise);
|
|
479
|
+
expect(await result).toBe(true);
|
|
480
|
+
expect(mockHandler).toBeCalledTimes(1);
|
|
481
|
+
});
|
|
482
|
+
|
|
473
483
|
test("logical operators with async values", async () => {
|
|
474
484
|
evaluator.addExpressionFunction("asyncTrue", async () => {
|
|
475
485
|
return Promise.resolve(true);
|
package/src/player.ts
CHANGED
|
@@ -346,47 +346,46 @@ export class Player {
|
|
|
346
346
|
}
|
|
347
347
|
});
|
|
348
348
|
|
|
349
|
-
// Tap for
|
|
350
|
-
flow.hooks.afterTransition.tap("player", (flowInstance) => {
|
|
351
|
-
const value = flowInstance.currentState?.value;
|
|
352
|
-
if (value && value.state_type === "ACTION") {
|
|
353
|
-
const { exp } = value;
|
|
354
|
-
const result = expressionEvaluator.evaluate(exp);
|
|
355
|
-
if (isPromiseLike(result)) {
|
|
356
|
-
this.logger.warn(
|
|
357
|
-
"Async expression used as return value in in non-async context, transitioning with '*' value",
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
flowController?.transition(String(result));
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
expressionEvaluator.reset();
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// Tap for async action states
|
|
367
|
-
flow.hooks.afterTransition.tap("player", async (flowInstance) => {
|
|
349
|
+
// Tap for action states
|
|
350
|
+
flow.hooks.afterTransition.tap("player-action-states", (flowInstance) => {
|
|
368
351
|
const value = flowInstance.currentState?.value;
|
|
369
352
|
if (value && value.state_type === "ASYNC_ACTION") {
|
|
370
353
|
const { exp } = value;
|
|
354
|
+
// defer async execution to next tick to allow transition to settle
|
|
371
355
|
try {
|
|
372
|
-
|
|
356
|
+
const result = expressionEvaluator.evaluateAsync(exp);
|
|
373
357
|
if (isPromiseLike(result)) {
|
|
374
358
|
if (value.await) {
|
|
375
|
-
|
|
359
|
+
queueMicrotask(() => {
|
|
360
|
+
result
|
|
361
|
+
.then((r) => flowController?.transition(String(r)))
|
|
362
|
+
.catch(flowResultDeferred.reject);
|
|
363
|
+
});
|
|
376
364
|
} else {
|
|
377
365
|
this.logger.warn(
|
|
378
366
|
"Unawaited promise used as return value in in non-async context, transitioning with '*' value",
|
|
379
367
|
);
|
|
368
|
+
flowController?.transition(String(result));
|
|
380
369
|
}
|
|
381
370
|
} else {
|
|
382
371
|
this.logger.warn(
|
|
383
372
|
"Non async expression used in async action node",
|
|
384
373
|
);
|
|
374
|
+
flowController?.transition(String(result));
|
|
385
375
|
}
|
|
386
|
-
flowController?.transition(String(result));
|
|
387
376
|
} catch (e) {
|
|
388
377
|
flowResultDeferred.reject(e);
|
|
389
378
|
}
|
|
379
|
+
} else if (value && value.state_type === "ACTION") {
|
|
380
|
+
// handle sync actions
|
|
381
|
+
const { exp } = value;
|
|
382
|
+
const result = expressionEvaluator.evaluate(exp);
|
|
383
|
+
if (isPromiseLike(result)) {
|
|
384
|
+
this.logger.warn(
|
|
385
|
+
"Async expression used as return value in in non-async context, transitioning with '*' value",
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
flowController?.transition(String(result));
|
|
390
389
|
}
|
|
391
390
|
|
|
392
391
|
expressionEvaluator.reset();
|
|
@@ -3,11 +3,12 @@ import type { Navigation, NavigationFlowEndState } from "@player-ui/types";
|
|
|
3
3
|
import type { Logger } from "../../logger";
|
|
4
4
|
import type { TransitionOptions } from "./flow";
|
|
5
5
|
import { FlowInstance } from "./flow";
|
|
6
|
+
export interface FlowControllerHooks {
|
|
7
|
+
flow: SyncHook<[FlowInstance], Record<string, any>>;
|
|
8
|
+
}
|
|
6
9
|
/** A manager for the navigation section of a Content blob */
|
|
7
10
|
export declare class FlowController {
|
|
8
|
-
readonly hooks:
|
|
9
|
-
flow: SyncHook<[FlowInstance], Record<string, any>>;
|
|
10
|
-
};
|
|
11
|
+
readonly hooks: FlowControllerHooks;
|
|
11
12
|
private readonly log?;
|
|
12
13
|
private navigation;
|
|
13
14
|
private navStack;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SyncBailHook, SyncHook, SyncWaterfallHook } from "tapable-ts";
|
|
2
|
-
import type { NavigationFlow, NavigationFlowState, NavigationFlowEndState } from "@player-ui/types";
|
|
2
|
+
import type { NavigationFlow, NavigationFlowState, NavigationFlowEndState, NavigationFlowActionState, NavigationFlowAsyncActionState, NavigationFlowExternalState, NavigationFlowFlowState, NavigationFlowViewState } from "@player-ui/types";
|
|
3
3
|
import type { Logger } from "../../logger";
|
|
4
4
|
export interface NamedState {
|
|
5
5
|
/** The name of the navigation node */
|
|
@@ -12,6 +12,35 @@ export interface TransitionOptions {
|
|
|
12
12
|
force?: boolean;
|
|
13
13
|
}
|
|
14
14
|
export type TransitionFunction = (name: string, options?: TransitionOptions) => void;
|
|
15
|
+
export interface FlowInstanceHooks {
|
|
16
|
+
beforeStart: SyncBailHook<[
|
|
17
|
+
NavigationFlow
|
|
18
|
+
], NavigationFlow, Record<string, any>>;
|
|
19
|
+
/** A callback when the onStart node was present */
|
|
20
|
+
onStart: SyncHook<[any], Record<string, any>>;
|
|
21
|
+
/** A callback when the onEnd node was present */
|
|
22
|
+
onEnd: SyncHook<[any], Record<string, any>>;
|
|
23
|
+
/** A hook to intercept and block a transition */
|
|
24
|
+
skipTransition: SyncBailHook<[
|
|
25
|
+
NamedState | undefined
|
|
26
|
+
], boolean | undefined, Record<string, any>>;
|
|
27
|
+
/** A chance to manipulate the flow-node used to calculate the given transition used */
|
|
28
|
+
beforeTransition: SyncWaterfallHook<[
|
|
29
|
+
(NavigationFlowViewState | NavigationFlowFlowState | NavigationFlowActionState | NavigationFlowAsyncActionState | NavigationFlowExternalState),
|
|
30
|
+
string
|
|
31
|
+
], Record<string, any>>;
|
|
32
|
+
/** A chance to manipulate the flow-node calculated after a transition */
|
|
33
|
+
resolveTransitionNode: SyncWaterfallHook<[
|
|
34
|
+
NavigationFlowState
|
|
35
|
+
], Record<string, any>>;
|
|
36
|
+
/** A callback when a transition from 1 state to another was made */
|
|
37
|
+
transition: SyncHook<[
|
|
38
|
+
NamedState | undefined,
|
|
39
|
+
NamedState
|
|
40
|
+
], Record<string, any>>;
|
|
41
|
+
/** A callback to run actions after a transition occurs */
|
|
42
|
+
afterTransition: SyncHook<[FlowInstance], Record<string, any>>;
|
|
43
|
+
}
|
|
15
44
|
/** The Content navigation state machine */
|
|
16
45
|
export declare class FlowInstance {
|
|
17
46
|
private flow;
|
|
@@ -21,23 +50,7 @@ export declare class FlowInstance {
|
|
|
21
50
|
private flowPromise?;
|
|
22
51
|
readonly id: string;
|
|
23
52
|
currentState?: NamedState;
|
|
24
|
-
readonly hooks:
|
|
25
|
-
beforeStart: SyncBailHook<[NavigationFlow], NavigationFlow, Record<string, any>>;
|
|
26
|
-
/** A callback when the onStart node was present */
|
|
27
|
-
onStart: SyncHook<[any], Record<string, any>>;
|
|
28
|
-
/** A callback when the onEnd node was present */
|
|
29
|
-
onEnd: SyncHook<[any], Record<string, any>>;
|
|
30
|
-
/** A hook to intercept and block a transition */
|
|
31
|
-
skipTransition: SyncBailHook<[NamedState | undefined], boolean | undefined, Record<string, any>>;
|
|
32
|
-
/** A chance to manipulate the flow-node used to calculate the given transition used */
|
|
33
|
-
beforeTransition: SyncWaterfallHook<[import("@player-ui/types").NavigationFlowViewState | import("@player-ui/types").NavigationFlowFlowState | import("@player-ui/types").NavigationFlowActionState | import("@player-ui/types").NavigationFlowAsyncActionState | import("@player-ui/types").NavigationFlowExternalState, string], Record<string, any>>;
|
|
34
|
-
/** A chance to manipulate the flow-node calculated after a transition */
|
|
35
|
-
resolveTransitionNode: SyncWaterfallHook<[NavigationFlowState], Record<string, any>>;
|
|
36
|
-
/** A callback when a transition from 1 state to another was made */
|
|
37
|
-
transition: SyncHook<[NamedState | undefined, NamedState], Record<string, any>>;
|
|
38
|
-
/** A callback to run actions after a transition occurs */
|
|
39
|
-
afterTransition: SyncHook<[FlowInstance], Record<string, any>>;
|
|
40
|
-
};
|
|
53
|
+
readonly hooks: FlowInstanceHooks;
|
|
41
54
|
constructor(id: string, flow: NavigationFlow, options?: {
|
|
42
55
|
/** Logger instance to use */
|
|
43
56
|
logger?: Logger;
|