آلات حالات تتفاعل مع الأحداث، وتتحقق من الشروط، وتنفذ التأثيرات، وترسم الشاشة.
السمة آلة حالات مرتبطة بكيان. تسرد حالات، وكل حالة تسرد الأحداث التي تقبلها، وإلى أين يقود كل منها، وما يحدث في الطريق.
1trait Approval -> Expense [interaction, instance] {
2 initial: draft
3
4 state draft {
5 SUBMIT -> submitted when (> @entity.amount 0)
6 (render-ui main { type: typography, content: "Submitted", variant: h4 })
7 }
8
9 state submitted {
10 APPROVE -> approved when (<= @entity.amount 500)
11 (set @entity.note "Approved")
12 REJECT -> rejected
13 (set @entity.note "Rejected")
14 }
15}EVENT -> target انتقال، وتتبعه التأثيرات كل منها في سطر وتُنفَّذ بالترتيب.when (…) شرط: إن كان خاطئًا يُرفض الحدث ولا يتغير شيء.initial: غيرها.يحمل قوس الترويسة فئة (interaction وlifecycle وintegration…) ومعها instance (آلة حالات لكل صف من الكيان، وهو الافتراضي) أو collection (آلة واحدة للكيان كله).
هذا مسار الموافقة كاملًا يعمل الآن. جرّب تقديم مطالبة تتجاوز 500 يورو ثم الموافقة عليها، وسيرفضها الشرط:
Approval
on Expense · states: 4 · transitions: 7السمة نفسها كآلة حالات: كل مربع حالة، وكل سهم حدث ينقلها، والأيقونات على السهم هي التأثيرات التي يشغّلها ذلك الحدث. مرّر المؤشر فوق أي رمز لترى ما يفعله. كيف تقرأ AVL
1app std-orb-docex-expense "1.0.0"
2"std-orb-docex-expense — an approval flow: four states, a guard on the amount, and a rule the shape of the program enforces."
3orbital ExpenseOrbital {
4 entity Expense [runtime] {
5 id : string!
6 amount : number = 120
7 note : string = ""
8 }
9
10 trait Approval -> Expense [interaction, instance] {
11 initial: draft
12
13 state draft {
14 INIT -> draft
15 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
16 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 },
17 { type: stack, direction: horizontal, gap: sm, children: [
18 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost },
19 { type: button, label: "Submit", action: SUBMIT, variant: primary }
20 ] }
21 ] })
22 RAISE -> draft
23 (emit MOVED { event: "RAISE" })
24 (set @entity.amount (+ @entity.amount 50))
25 SUBMIT -> submitted when (> @entity.amount 0)
26 (emit MOVED { event: "SUBMIT" })
27 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
28 { type: typography, content: (str/concat "Submitted: " @entity.amount " EUR"), variant: h4 },
29 { type: typography, content: "Claims over 500 EUR cannot be approved here.", variant: small, color: muted },
30 { type: stack, direction: horizontal, gap: sm, children: [
31 { type: button, label: "Approve", action: APPROVE, variant: primary },
32 { type: button, label: "Reject", action: REJECT, variant: secondary }
33 ] }
34 ] })
35 }
36
37 state submitted {
38 APPROVE -> approved when (<= @entity.amount 500)
39 (emit MOVED { event: "APPROVE" })
40 (set @entity.note "Approved")
41 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
42 { type: badge, label: "Approved", variant: success },
43 { type: button, label: "New claim", action: NEW_CLAIM, variant: ghost }
44 ] })
45 REJECT -> rejected
46 (emit MOVED { event: "REJECT" })
47 (set @entity.note "Rejected")
48 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
49 { type: badge, label: "Rejected", variant: error },
50 { type: button, label: "Revise", action: REVISE, variant: primary }
51 ] })
52 }
53
54 state approved {
55 NEW_CLAIM -> draft
56 (emit MOVED { event: "NEW_CLAIM" })
57 (set @entity.amount 120)
58 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
59 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 },
60 { type: stack, direction: horizontal, gap: sm, children: [
61 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost },
62 { type: button, label: "Submit", action: SUBMIT, variant: primary }
63 ] }
64 ] })
65 }
66
67 state rejected {
68 REVISE -> draft
69 (emit MOVED { event: "REVISE" })
70 (render-ui main { type: stack, direction: vertical, gap: sm, children: [
71 { type: typography, content: (str/concat "Draft claim: " @entity.amount " EUR"), variant: h4 },
72 { type: stack, direction: horizontal, gap: sm, children: [
73 { type: button, label: "Add 50 EUR", action: RAISE, variant: ghost },
74 { type: button, label: "Submit", action: SUBMIT, variant: primary }
75 ] }
76 ] })
77 }
78
79 emits {
80 MOVED -> external {
81 event : string!
82 }
83 @description "Each move names the event it took, so the diagram beside the demo can follow it."
84 }
85 }
86
87 page "/expenses" -> Approval
88}ما الذي يحدث عندما يقع الحدث APPROVE:
(<= @entity.amount 500)(set @entity.note "Approved")| التأثير | عمله |
|---|---|
(set @entity.f v) | يعيّن حقلًا. صيغة التعيين الوحيدة. |
(fetch E { … }) / (persist create|update|delete E data) | يقرأ الصفوف المخزّنة ويكتبها. |
(emit EVENT { … }) | يبث حدثًا معلَنًا. |
(render-ui slot { … }) | يرسم شجرة أنماط في خانة (main وmodal وdrawer وtoast…)، وnull يفرغها. |
(navigate "/path") / (navigate-back) | ينتقل إلى صفحة أخرى. |
(notify info "Saved") | يعرض إشعارًا. |
(call-service provider action { … }) | يستدعي تكاملًا (نموذج لغوي، بريد، دفع…). |
العوامل تحسب القيم: (+ a b) و(str/concat …) و(array/map xs (fn x …)). كلها بادئة، ولا توجد صيغة وسطية. يسرد مرجع العوامل كل واحد منها.
| الرمز | المعنى |
|---|---|
@entity.field | حقل من الكيان المرتبط |
?field | حقل من حمولة الحدث الوارد |
@config.key | قيمة من config الخاص بالسمة |
@user.field | الزائر المسجّل |
now / state | الطابع الزمني لهذه المعالجة / الحالة الحالية |
تعلن السمة ما تبثه، وتعلن سمة أخرى ما تتفاعل معه. هنا يبث القرص TARGET_CHANGED، وتستمع إليه الحالة وتحوّله إلى حدثها الخاص SHOW:
Dial
on Thermostat · states: 1 · transitions: 3Status
on Thermostat · states: 1 · transitions: 2Set a temperature between 16 and 28 °C.
1app std-orb-docex-thermostat "1.0.0"
2"std-orb-docex-thermostat — two traits in one orbital: one guards and emits, the other listens and reacts."
3orbital ThermostatOrbital {
4 entity Thermostat [runtime, shared] {
5 id : string!
6 target : number = 21
7 }
8
9 trait Dial -> Thermostat [interaction, instance] {
10 initial: idle
11
12 state idle {
13 INIT -> idle
14 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [
15 { type: button, label: "Colder", action: DOWN, variant: secondary },
16 { type: typography, content: (str/concat @entity.target " °C"), variant: h2 },
17 { type: button, label: "Warmer", action: UP, variant: secondary }
18 ] })
19 UP -> idle when (< @entity.target 28)
20 (emit MOVED { event: "UP" })
21 (set @entity.target (+ @entity.target 1))
22 (emit TARGET_CHANGED { target: @entity.target })
23 DOWN -> idle when (> @entity.target 16)
24 (emit MOVED { event: "DOWN" })
25 (set @entity.target (- @entity.target 1))
26 (emit TARGET_CHANGED { target: @entity.target })
27 }
28
29 emits {
30 TARGET_CHANGED {
31 target : number!
32 }
33 @description "The target temperature moved."
34 MOVED -> external {
35 event : string!
36 }
37 @description "Each move names the event it took, so the diagram beside the demo can follow it."
38 }
39 }
40
41 trait Status -> Thermostat [interaction, instance] {
42 initial: watching
43
44 state watching {
45 INIT -> watching
46 (render-ui main { type: typography, content: "Set a temperature between 16 and 28 °C.", variant: body, color: muted })
47 SHOW -> watching
48 (emit MOVED { event: "SHOW" })
49 (render-ui main { type: badge, label: (if (> ?target 23) "Heating up" (if (< ?target 19) "Cooling down" "Comfortable")), variant: (if (> ?target 23) warning (if (< ?target 19) info success)) })
50 }
51
52 emits {
53 MOVED -> external {
54 event : string!
55 }
56 @description "Each move names the event it took, so the diagram beside the demo can follow it."
57 }
58
59 listens {
60 Dial.TARGET_CHANGED -> SHOW
61 SHOW {
62 target : number!
63 }
64 }
65 }
66
67 page "/thermostat" -> Dial, Status
68}ما الذي يحدث عندما يقع الحدث UP:
(< @entity.target 28)(set @entity.target (+ @entity.target 1))(emit "TARGET_CHANGED" { target: @entity.target })1emits {
2 TARGET_CHANGED {
3 target : number!
4 }
5}
6
7listens {
8 Dial.TARGET_CHANGED -> SHOW
9 SHOW {
10 target : number!
11 }
12}الأحداث internal (الافتراضية) تبقى داخل المداريّ، والأحداث external تعبر إلى مداريّات أخرى ويجب أن تعلن حمولتها. يُحَل Trait.EVENT داخل مداريّ المستمع نفسه، وعبر المداريّات يُكتب Orbital.Trait.EVENT. يعرض درس الأحداث بين المداريّات الحالتين.
النبضة تأثير دوري، لا يعمل إلا ما دام شرط when متحققًا، فالنبضة المرتبطة بحالة تتوقف لحظة خروج السمة منها:
Timing
on Stopwatch · states: 2 · transitions: 41app std-orb-docex-stopwatch "1.0.0"
2"std-orb-docex-stopwatch — a tick: a periodic effect that runs only while the trait is in the running state."
3orbital StopwatchOrbital {
4 entity Stopwatch [runtime] {
5 id : string!
6 seconds : number = 0
7 }
8
9 trait Timing -> Stopwatch [interaction, instance] {
10 initial: stopped
11
12 state stopped {
13 INIT -> stopped
14 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [
15 { type: typography, content: (str/concat @entity.seconds " s"), variant: h2 },
16 { type: button, label: "Start", action: START, variant: primary },
17 { type: button, label: "Reset", action: RESET, variant: ghost }
18 ] })
19 START -> running
20 (emit MOVED { event: "START" })
21 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [
22 { type: typography, content: (str/concat @entity.seconds " s"), variant: h2 },
23 { type: button, label: "Stop", action: STOP, variant: secondary }
24 ] })
25 RESET -> stopped
26 (emit MOVED { event: "RESET" })
27 (set @entity.seconds 0)
28 }
29
30 state running {
31 STOP -> stopped
32 (emit MOVED { event: "STOP" })
33 (render-ui main { type: stack, direction: horizontal, gap: md, align: center, children: [
34 { type: typography, content: (str/concat @entity.seconds " s"), variant: h2 },
35 { type: button, label: "Start", action: START, variant: primary },
36 { type: button, label: "Reset", action: RESET, variant: ghost }
37 ] })
38 }
39
40 emits {
41 MOVED -> external {
42 event : string!
43 }
44 @description "Each move names the event it took, so the diagram beside the demo can follow it."
45 }
46
47 ticks {
48 second every 1s when (= state "running")
49 (set @entity.seconds (+ @entity.seconds 1))
50 }
51 }
52
53 page "/stopwatch" -> Timing
54}ما الذي يحدث عندما يقع الحدث START:
▼ticks {
second every 1s when (= state "running")
(set @entity.seconds (+ @entity.seconds 1))
}الفواصل بالمللي ثانية، أو مدة (5s و1m و1h و1d)، أو نص cron. النبضة على كيان مخزّن تعمل على الخادم ما دامت صفحة ما تحمل السمة، ووسمها [background] يجعلها تعمل دون أن يراقبها أحد.
config { pageSize : int = 10 @label "Items per page" } يعلن مفتاحًا تقرؤه السمة بـ @config.pageSize. المفاتيح هي ما يحق لموضع الاستدعاء تغييره عند استيراد السمة، راجع التركيب.
الصفحات تضع السمات على الشاشة.