@reventlessdev/trait-attachments 1.0.0-alpha.5 → 1.0.0-alpha.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/trait-attachments",
3
- "version": "1.0.0-alpha.5",
3
+ "version": "1.0.0-alpha.7",
4
4
  "description": "Attachments domain trait: rules, host contract, conformance suite and emitter for an ordered set of stored files on an entity, with a primary and captions",
5
5
  "license": "Apache-2.0",
6
6
  "files": [
@@ -13,8 +13,8 @@
13
13
  "dependencies": {
14
14
  "sury": "11.0.0-rc.2",
15
15
  "sury-ppx": "11.0.0-rc.2",
16
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.203",
17
- "@reventlessdev/reventless-spec": "3.0.0-alpha.129"
16
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.131",
17
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.205"
18
18
  },
19
19
  "devDependencies": {
20
20
  "rescript": "12.3.0"
@@ -43,13 +43,27 @@ module type Binding = {
43
43
  module Spec: ReventlessGwt.Behavior_GWT.BehaviorSpec
44
44
  module Behavior: ReventlessGwt.Behavior_GWT.Behavior with module Spec = Spec
45
45
 
46
- /** History that brings the entity into existence with an empty set. */
47
- let created: array<Spec.consumedEvent>
48
- /** The set's own facts, as the slice consumes them. */
49
- let attachedC: ref => Spec.consumedEvent
50
- let removedC: ref => Spec.consumedEvent
51
- let primarySetC: ref => Spec.consumedEvent
52
- let altTextSetC: (ref, string) => Spec.consumedEvent
46
+ /** The same facts as the slice **consumes** them, for building history.
47
+
48
+ A slice declares `consumedEvent` and `event` separately and they differ
49
+ the consumed form carries no entity id, because the partition already
50
+ says which entity it belongs to. So a suite needs both spellings of the
51
+ same fact: one to lay down history, one to assert what was emitted.
52
+
53
+ Grouped rather than suffixed. The pairs used to read `attachedC` against
54
+ `attached`, which said "consumed" only to someone who already knew — and
55
+ in a codebase where every binding also carries commands, a bare `C` reads
56
+ just as naturally as one. */
57
+ module Consumed: {
58
+ /** History that brings the entity into existence with an empty set. */
59
+ let created: array<Spec.consumedEvent>
60
+ let attached: ref => Spec.consumedEvent
61
+ let removed: ref => Spec.consumedEvent
62
+ let primarySet: ref => Spec.consumedEvent
63
+ /** The conclusion — see `Attachments_Rules.fact`. */
64
+ let effectiveChanged: option<ref> => Spec.consumedEvent
65
+ let altTextSet: (ref, string) => Spec.consumedEvent
66
+ }
53
67
 
54
68
  let attach: ref => Spec.command
55
69
  let remove: ref => Spec.command
@@ -60,6 +74,10 @@ module type Binding = {
60
74
  let attached: ref => Spec.event
61
75
  let removed: ref => Spec.event
62
76
  let primarySet: ref => Spec.event
77
+ /** The member a reader should now show, or `None` where the set has none.
78
+ Every graft declares it, whatever its cardinality: a subscriber reading it
79
+ must not have to know how many members the host allows. */
80
+ let effectiveChanged: option<ref> => Spec.event
63
81
  let altTextSet: (ref, string) => Spec.event
64
82
  /** The refusal for a primary or caption on a ref that is not in the set. */
65
83
  let notAttached: Spec.error
@@ -89,13 +107,19 @@ module type SingleBinding = {
89
107
  module Spec: ReventlessGwt.Behavior_GWT.BehaviorSpec
90
108
  module Behavior: ReventlessGwt.Behavior_GWT.Behavior with module Spec = Spec
91
109
 
92
- /** History that brings the entity into existence with an empty set. */
93
- let created: array<Spec.consumedEvent>
94
- /** The set's own facts, as the slice consumes them. No `primarySetC`: nothing
95
- emits one. */
96
- let attachedC: ref => Spec.consumedEvent
97
- let removedC: ref => Spec.consumedEvent
98
- let altTextSetC: (ref, string) => Spec.consumedEvent
110
+ /** The facts as the slice **consumes** them see `Binding.Consumed`. No
111
+ `primarySet`: at this cardinality nothing emits one. */
112
+ module Consumed: {
113
+ /** History that brings the entity into existence with an empty set. */
114
+ let created: array<Spec.consumedEvent>
115
+ let attached: ref => Spec.consumedEvent
116
+ let removed: ref => Spec.consumedEvent
117
+ let altTextSet: (ref, string) => Spec.consumedEvent
118
+ /** Declared at this cardinality too even though `attached`/`removed` imply
119
+ it here: a subscriber must not have to know how many members a host
120
+ allows to know what to read. */
121
+ let effectiveChanged: option<ref> => Spec.consumedEvent
122
+ }
99
123
 
100
124
  let attach: ref => Spec.command
101
125
  /** Remove whatever is held. Takes no ref — there is only one, and asking the
@@ -111,6 +135,8 @@ module type SingleBinding = {
111
135
  let attached: ref => Spec.event
112
136
  let removed: ref => Spec.event
113
137
  let altTextSet: (ref, string) => Spec.event
138
+ /** The member a reader should now show, or `None` where nothing is held. */
139
+ let effectiveChanged: option<ref> => Spec.event
114
140
  /** The refusal for a caption on a ref that is not held. */
115
141
  let notAttached: Spec.error
116
142
  }
@@ -17,13 +17,15 @@ let suiteName = (host: string) => `${host} conforms to the attachments trait`
17
17
  module Make = (B: Attachments.Binding) => {
18
18
  module G = ReventlessGwt.Behavior_GWT.Make(B.Spec, B.Behavior)
19
19
 
20
- let withA = Array.concat(B.created, [B.attachedC(B.refA)])
21
- let withAB = Array.concat(withA, [B.attachedC(B.refB)])
20
+ let withA = Array.concat(B.Consumed.created, [B.Consumed.attached(B.refA)])
21
+ let withAB = Array.concat(withA, [B.Consumed.attached(B.refB)])
22
22
 
23
23
  let register = () =>
24
24
  G.describe(suiteName(B.Spec.name), () => {
25
25
  G.test("the first attachment is appended", () =>
26
- G.givenEvents(B.created)->G.whenCmd(B.attach(B.refA))->G.thenEvent(B.attached(B.refA))
26
+ G.givenEvents(B.Consumed.created)
27
+ ->G.whenCmd(B.attach(B.refA))
28
+ ->G.thenEvents([B.attached(B.refA), B.effectiveChanged(Some(B.refA))])
27
29
  )
28
30
 
29
31
  G.test("attaching a ref already in the set is a no-op", () =>
@@ -35,7 +37,9 @@ module Make = (B: Attachments.Binding) => {
35
37
  )
36
38
 
37
39
  G.test("removing an attached ref is appended", () =>
38
- G.givenEvents(withA)->G.whenCmd(B.remove(B.refA))->G.thenEvent(B.removed(B.refA))
40
+ G.givenEvents(withA)
41
+ ->G.whenCmd(B.remove(B.refA))
42
+ ->G.thenEvents([B.removed(B.refA), B.effectiveChanged(None)])
39
43
  )
40
44
 
41
45
  G.test("removing a ref not in the set is a no-op", () =>
@@ -43,9 +47,9 @@ module Make = (B: Attachments.Binding) => {
43
47
  )
44
48
 
45
49
  G.test("a removed ref can be attached again", () =>
46
- G.givenEvents(Array.concat(withA, [B.removedC(B.refA)]))
50
+ G.givenEvents(Array.concat(withA, [B.Consumed.removed(B.refA)]))
47
51
  ->G.whenCmd(B.attach(B.refA))
48
- ->G.thenEvent(B.attached(B.refA))
52
+ ->G.thenEvents([B.attached(B.refA), B.effectiveChanged(Some(B.refA))])
49
53
  )
50
54
 
51
55
  G.test("the primary must be in the set", () =>
@@ -57,27 +61,48 @@ module Make = (B: Attachments.Binding) => {
57
61
  )
58
62
 
59
63
  G.test("choosing another primary is appended", () =>
60
- G.givenEvents(withAB)->G.whenCmd(B.setPrimary(B.refB))->G.thenEvent(B.primarySet(B.refB))
64
+ G.givenEvents(withAB)
65
+ ->G.whenCmd(B.setPrimary(B.refB))
66
+ ->G.thenEvents([B.primarySet(B.refB), B.effectiveChanged(Some(B.refB))])
61
67
  )
62
68
 
63
69
  G.test("choosing the current primary is a no-op", () =>
64
- G.givenEvents(Array.concat(withAB, [B.primarySetC(B.refB)]))
70
+ G.givenEvents(Array.concat(withAB, [B.Consumed.primarySet(B.refB)]))
65
71
  ->G.whenCmd(B.setPrimary(B.refB))
66
72
  ->G.thenNoEvent
67
73
  )
68
74
 
69
75
  G.test("removing the chosen primary lets the first remaining stand in", () =>
70
- G.givenEvents(Array.concat(withAB, [B.primarySetC(B.refB), B.removedC(B.refB)]))
76
+ G.givenEvents(Array.concat(withAB, [B.Consumed.primarySet(B.refB), B.Consumed.removed(B.refB)]))
71
77
  ->G.whenCmd(B.setPrimary(B.refA))
72
78
  ->G.thenNoEvent
73
79
  )
74
80
 
81
+ // The two moves nobody decides. Neither produces a fact of its own — the
82
+ // set simply resolves differently afterwards — which is why anything
83
+ // outside it could not follow the primary before this was announced.
84
+ G.test("removing the member that stood in promotes the next, and says so", () =>
85
+ G.givenEvents(withAB)
86
+ ->G.whenCmd(B.remove(B.refA))
87
+ ->G.thenEvents([B.removed(B.refA), B.effectiveChanged(Some(B.refB))])
88
+ )
89
+
90
+ G.test("removing the chosen primary announces the one that stands in", () =>
91
+ G.givenEvents(Array.concat(withAB, [B.Consumed.primarySet(B.refB)]))
92
+ ->G.whenCmd(B.remove(B.refB))
93
+ ->G.thenEvents([B.removed(B.refB), B.effectiveChanged(Some(B.refA))])
94
+ )
95
+
75
96
  G.test("a caption needs its ref in the set", () =>
76
97
  G.givenEvents(withA)
77
98
  ->G.whenCmd(B.setAltText(B.refB, "side"))
78
99
  ->G.thenError(B.notAttached)
79
100
  )
80
101
 
102
+ // One fact and no second one: a caption moves no file, so the conclusion
103
+ // is unchanged and must not be announced. This is what stops an
104
+ // implementation appending it after every command rather than after the
105
+ // ones that move it.
81
106
  G.test("a caption is appended", () =>
82
107
  G.givenEvents(withA)
83
108
  ->G.whenCmd(B.setAltText(B.refA, "front"))
@@ -85,7 +110,7 @@ module Make = (B: Attachments.Binding) => {
85
110
  )
86
111
 
87
112
  G.test("repeating the caption is a no-op", () =>
88
- G.givenEvents(Array.concat(withA, [B.altTextSetC(B.refA, "front")]))
113
+ G.givenEvents(Array.concat(withA, [B.Consumed.altTextSet(B.refA, "front")]))
89
114
  ->G.whenCmd(B.setAltText(B.refA, "front"))
90
115
  ->G.thenNoEvent
91
116
  )
@@ -108,12 +133,14 @@ single image wants exactly as much as a gallery member does.
108
133
  module MakeSingle = (B: Attachments.SingleBinding) => {
109
134
  module G = ReventlessGwt.Behavior_GWT.Make(B.Spec, B.Behavior)
110
135
 
111
- let withA = Array.concat(B.created, [B.attachedC(B.refA)])
136
+ let withA = Array.concat(B.Consumed.created, [B.Consumed.attached(B.refA)])
112
137
 
113
138
  let register = () =>
114
139
  G.describe(suiteName(B.Spec.name), () => {
115
140
  G.test("the first attachment is appended", () =>
116
- G.givenEvents(B.created)->G.whenCmd(B.attach(B.refA))->G.thenEvent(B.attached(B.refA))
141
+ G.givenEvents(B.Consumed.created)
142
+ ->G.whenCmd(B.attach(B.refA))
143
+ ->G.thenEvents([B.attached(B.refA), B.effectiveChanged(Some(B.refA))])
117
144
  )
118
145
 
119
146
  G.test("attaching the ref already held is a no-op", () =>
@@ -126,21 +153,31 @@ module MakeSingle = (B: Attachments.SingleBinding) => {
126
153
  G.test("a second ref replaces the first rather than joining it", () =>
127
154
  G.givenEvents(withA)
128
155
  ->G.whenCmd(B.attach(B.refB))
129
- ->G.thenEvents([B.removed(B.refA), B.attached(B.refB)])
156
+ ->G.thenEvents([
157
+ B.removed(B.refA),
158
+ B.attached(B.refB),
159
+ B.effectiveChanged(Some(B.refB)),
160
+ ])
130
161
  )
131
162
 
132
163
  G.test("clearing a held set removes what it holds", () =>
133
- G.givenEvents(withA)->G.whenCmd(B.clear)->G.thenEvent(B.removed(B.refA))
164
+ G.givenEvents(withA)
165
+ ->G.whenCmd(B.clear)
166
+ ->G.thenEvents([B.removed(B.refA), B.effectiveChanged(None)])
134
167
  )
135
168
 
136
169
  G.test("clearing an empty set is a no-op", () =>
137
- G.givenEvents(B.created)->G.whenCmd(B.clear)->G.thenNoEvent
170
+ G.givenEvents(B.Consumed.created)->G.whenCmd(B.clear)->G.thenNoEvent
138
171
  )
139
172
 
140
173
  G.test("a replaced ref can be attached again", () =>
141
- G.givenEvents(Array.concat(withA, [B.removedC(B.refA), B.attachedC(B.refB)]))
174
+ G.givenEvents(Array.concat(withA, [B.Consumed.removed(B.refA), B.Consumed.attached(B.refB)]))
142
175
  ->G.whenCmd(B.attach(B.refA))
143
- ->G.thenEvents([B.removed(B.refB), B.attached(B.refA)])
176
+ ->G.thenEvents([
177
+ B.removed(B.refB),
178
+ B.attached(B.refA),
179
+ B.effectiveChanged(Some(B.refA)),
180
+ ])
144
181
  )
145
182
 
146
183
  // The caption names no ref either, so what it lands on is whatever is
@@ -153,17 +190,17 @@ module MakeSingle = (B: Attachments.SingleBinding) => {
153
190
  )
154
191
 
155
192
  G.test("a caption follows a replacement onto the new ref", () =>
156
- G.givenEvents(Array.concat(withA, [B.removedC(B.refA), B.attachedC(B.refB)]))
193
+ G.givenEvents(Array.concat(withA, [B.Consumed.removed(B.refA), B.Consumed.attached(B.refB)]))
157
194
  ->G.whenCmd(B.setAltText("side"))
158
195
  ->G.thenEvent(B.altTextSet(B.refB, "side"))
159
196
  )
160
197
 
161
198
  G.test("captioning an empty set is refused", () =>
162
- G.givenEvents(B.created)->G.whenCmd(B.setAltText("front"))->G.thenError(B.notAttached)
199
+ G.givenEvents(B.Consumed.created)->G.whenCmd(B.setAltText("front"))->G.thenError(B.notAttached)
163
200
  )
164
201
 
165
202
  G.test("repeating the caption is a no-op", () =>
166
- G.givenEvents(Array.concat(withA, [B.altTextSetC(B.refA, "front")]))
203
+ G.givenEvents(Array.concat(withA, [B.Consumed.altTextSet(B.refA, "front")]))
167
204
  ->G.whenCmd(B.setAltText("front"))
168
205
  ->G.thenNoEvent
169
206
  )
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
3
4
  import * as Behavior_GWT$ReventlessGwt from "@reventlessdev/reventless-gwt/src/Behavior_GWT.res.mjs";
4
5
 
5
6
  function suiteName(host) {
@@ -13,26 +14,46 @@ function Make(B) {
13
14
  evolve: $$let.evolve,
14
15
  decide: $$let.decide
15
16
  });
16
- let withA = B.created.concat([B.attachedC(B.refA)]);
17
- let withAB = withA.concat([B.attachedC(B.refB)]);
17
+ let withA = B.Consumed.created.concat([B.Consumed.attached(B.refA)]);
18
+ let withAB = withA.concat([B.Consumed.attached(B.refB)]);
18
19
  let register = () => G.describe(suiteName(B.Spec.name), () => {
19
- G.test("the first attachment is appended", () => G.thenEvent(G.whenCmd(G.givenEvents(B.created), B.attach(B.refA)), B.attached(B.refA)));
20
+ G.test("the first attachment is appended", () => G.thenEvents(G.whenCmd(G.givenEvents(B.Consumed.created), B.attach(B.refA)), [
21
+ B.attached(B.refA),
22
+ B.effectiveChanged(Primitive_option.some(B.refA))
23
+ ]));
20
24
  G.test("attaching a ref already in the set is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withA), B.attach(B.refA))));
21
25
  G.test("a second ref extends the set rather than replacing it", () => G.thenEvent(G.whenCmd(G.givenEvents(withA), B.attach(B.refB)), B.attached(B.refB)));
22
- G.test("removing an attached ref is appended", () => G.thenEvent(G.whenCmd(G.givenEvents(withA), B.remove(B.refA)), B.removed(B.refA)));
26
+ G.test("removing an attached ref is appended", () => G.thenEvents(G.whenCmd(G.givenEvents(withA), B.remove(B.refA)), [
27
+ B.removed(B.refA),
28
+ B.effectiveChanged(undefined)
29
+ ]));
23
30
  G.test("removing a ref not in the set is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withA), B.remove(B.refB))));
24
- G.test("a removed ref can be attached again", () => G.thenEvent(G.whenCmd(G.givenEvents(withA.concat([B.removedC(B.refA)])), B.attach(B.refA)), B.attached(B.refA)));
31
+ G.test("a removed ref can be attached again", () => G.thenEvents(G.whenCmd(G.givenEvents(withA.concat([B.Consumed.removed(B.refA)])), B.attach(B.refA)), [
32
+ B.attached(B.refA),
33
+ B.effectiveChanged(Primitive_option.some(B.refA))
34
+ ]));
25
35
  G.test("the primary must be in the set", () => G.thenError(G.whenCmd(G.givenEvents(withA), B.setPrimary(B.refB)), B.notAttached));
26
36
  G.test("the first attached is the primary until one is chosen", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withAB), B.setPrimary(B.refA))));
27
- G.test("choosing another primary is appended", () => G.thenEvent(G.whenCmd(G.givenEvents(withAB), B.setPrimary(B.refB)), B.primarySet(B.refB)));
28
- G.test("choosing the current primary is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withAB.concat([B.primarySetC(B.refB)])), B.setPrimary(B.refB))));
37
+ G.test("choosing another primary is appended", () => G.thenEvents(G.whenCmd(G.givenEvents(withAB), B.setPrimary(B.refB)), [
38
+ B.primarySet(B.refB),
39
+ B.effectiveChanged(Primitive_option.some(B.refB))
40
+ ]));
41
+ G.test("choosing the current primary is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withAB.concat([B.Consumed.primarySet(B.refB)])), B.setPrimary(B.refB))));
29
42
  G.test("removing the chosen primary lets the first remaining stand in", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withAB.concat([
30
- B.primarySetC(B.refB),
31
- B.removedC(B.refB)
43
+ B.Consumed.primarySet(B.refB),
44
+ B.Consumed.removed(B.refB)
32
45
  ])), B.setPrimary(B.refA))));
46
+ G.test("removing the member that stood in promotes the next, and says so", () => G.thenEvents(G.whenCmd(G.givenEvents(withAB), B.remove(B.refA)), [
47
+ B.removed(B.refA),
48
+ B.effectiveChanged(Primitive_option.some(B.refB))
49
+ ]));
50
+ G.test("removing the chosen primary announces the one that stands in", () => G.thenEvents(G.whenCmd(G.givenEvents(withAB.concat([B.Consumed.primarySet(B.refB)])), B.remove(B.refB)), [
51
+ B.removed(B.refB),
52
+ B.effectiveChanged(Primitive_option.some(B.refA))
53
+ ]));
33
54
  G.test("a caption needs its ref in the set", () => G.thenError(G.whenCmd(G.givenEvents(withA), B.setAltText(B.refB, "side")), B.notAttached));
34
55
  G.test("a caption is appended", () => G.thenEvent(G.whenCmd(G.givenEvents(withA), B.setAltText(B.refA, "front")), B.altTextSet(B.refA, "front")));
35
- G.test("repeating the caption is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withA.concat([B.altTextSetC(B.refA, "front")])), B.setAltText(B.refA, "front"))));
56
+ G.test("repeating the caption is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withA.concat([B.Consumed.altTextSet(B.refA, "front")])), B.setAltText(B.refA, "front"))));
36
57
  });
37
58
  return {
38
59
  G: G,
@@ -49,30 +70,38 @@ function MakeSingle(B) {
49
70
  evolve: $$let.evolve,
50
71
  decide: $$let.decide
51
72
  });
52
- let withA = B.created.concat([B.attachedC(B.refA)]);
73
+ let withA = B.Consumed.created.concat([B.Consumed.attached(B.refA)]);
53
74
  let register = () => G.describe(suiteName(B.Spec.name), () => {
54
- G.test("the first attachment is appended", () => G.thenEvent(G.whenCmd(G.givenEvents(B.created), B.attach(B.refA)), B.attached(B.refA)));
75
+ G.test("the first attachment is appended", () => G.thenEvents(G.whenCmd(G.givenEvents(B.Consumed.created), B.attach(B.refA)), [
76
+ B.attached(B.refA),
77
+ B.effectiveChanged(Primitive_option.some(B.refA))
78
+ ]));
55
79
  G.test("attaching the ref already held is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withA), B.attach(B.refA))));
56
80
  G.test("a second ref replaces the first rather than joining it", () => G.thenEvents(G.whenCmd(G.givenEvents(withA), B.attach(B.refB)), [
57
81
  B.removed(B.refA),
58
- B.attached(B.refB)
82
+ B.attached(B.refB),
83
+ B.effectiveChanged(Primitive_option.some(B.refB))
84
+ ]));
85
+ G.test("clearing a held set removes what it holds", () => G.thenEvents(G.whenCmd(G.givenEvents(withA), B.clear), [
86
+ B.removed(B.refA),
87
+ B.effectiveChanged(undefined)
59
88
  ]));
60
- G.test("clearing a held set removes what it holds", () => G.thenEvent(G.whenCmd(G.givenEvents(withA), B.clear), B.removed(B.refA)));
61
- G.test("clearing an empty set is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(B.created), B.clear)));
89
+ G.test("clearing an empty set is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(B.Consumed.created), B.clear)));
62
90
  G.test("a replaced ref can be attached again", () => G.thenEvents(G.whenCmd(G.givenEvents(withA.concat([
63
- B.removedC(B.refA),
64
- B.attachedC(B.refB)
91
+ B.Consumed.removed(B.refA),
92
+ B.Consumed.attached(B.refB)
65
93
  ])), B.attach(B.refA)), [
66
94
  B.removed(B.refB),
67
- B.attached(B.refA)
95
+ B.attached(B.refA),
96
+ B.effectiveChanged(Primitive_option.some(B.refA))
68
97
  ]));
69
98
  G.test("a caption lands on the ref that is held", () => G.thenEvent(G.whenCmd(G.givenEvents(withA), B.setAltText("front")), B.altTextSet(B.refA, "front")));
70
99
  G.test("a caption follows a replacement onto the new ref", () => G.thenEvent(G.whenCmd(G.givenEvents(withA.concat([
71
- B.removedC(B.refA),
72
- B.attachedC(B.refB)
100
+ B.Consumed.removed(B.refA),
101
+ B.Consumed.attached(B.refB)
73
102
  ])), B.setAltText("side")), B.altTextSet(B.refB, "side")));
74
- G.test("captioning an empty set is refused", () => G.thenError(G.whenCmd(G.givenEvents(B.created), B.setAltText("front")), B.notAttached));
75
- G.test("repeating the caption is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withA.concat([B.altTextSetC(B.refA, "front")])), B.setAltText("front"))));
103
+ G.test("captioning an empty set is refused", () => G.thenError(G.whenCmd(G.givenEvents(B.Consumed.created), B.setAltText("front")), B.notAttached));
104
+ G.test("repeating the caption is a no-op", () => G.thenNoEvent(G.whenCmd(G.givenEvents(withA.concat([B.Consumed.altTextSet(B.refA, "front")])), B.setAltText("front"))));
76
105
  });
77
106
  return {
78
107
  G: G,
@@ -77,6 +77,25 @@ type fact =
77
77
  | Removed({ref: ref})
78
78
  | PrimarySet({ref: ref})
79
79
  | AltTextSet({ref: ref, altText: string})
80
+ | /** The member a reader should now show, or `None` where the set has none.
81
+
82
+ **A conclusion, not a decision.** `PrimarySet` records that somebody
83
+ chose; this records what the set resolved to, however it got there —
84
+ which is a different fact, and the one anything outside the set can act
85
+ on. The two coincide on an explicit choice and diverge everywhere else:
86
+ the first attachment stands in with nobody choosing, and removing the
87
+ one that stood in promotes the next.
88
+
89
+ Emitted at **both** cardinalities even though a `Single` host could
90
+ derive it from its own `Attached`/`Removed` pair, for the reason
91
+ `decide` gives below for not collapsing those two: a fact that only some
92
+ grafts emit makes a subscriber ask how many members the host allows,
93
+ which is not its business.
94
+
95
+ Optional because "there is no longer one to show" is as much a change as
96
+ any other, and a subscriber that cannot be told it keeps showing a file
97
+ the set no longer holds. */
98
+ EffectiveChanged({ref: option<ref>})
80
99
 
81
100
  /** The primary a reader should show: the one chosen, else the first attached, so
82
101
  a set never shows no file while it holds one. The read model applies the same
@@ -137,6 +156,10 @@ let evolve = (t, fact) =>
137
156
  altTexts: t.altTexts->Array.filter(((r, _)) => r != ref),
138
157
  }
139
158
  | PrimarySet({ref}) => {...t, primary: Some(ref)}
159
+ // Carries no state of its own: it is what `effectivePrimary` already answers,
160
+ // said out loud. Folding it would be storing a derivation beside the values it
161
+ // derives from, which is the one way the two could disagree.
162
+ | EffectiveChanged(_) => t
140
163
  | AltTextSet({ref, altText}) => {
141
164
  ...t,
142
165
  altTexts: t.altTexts->Array.filter(((r, _)) => r != ref)->Array.concat([(ref, altText)]),
@@ -167,7 +190,7 @@ and an event log records both. Collapsing them into a single "replaced" fact
167
190
  would make a host declare an event that only bounded hosts have, which is one
168
191
  more way the two cardinalities' emitted surfaces could diverge.
169
192
  */
170
- let decide = (t, ~cardinality: cardinality=Many, op): result<array<fact>, [#NotAttached]> =>
193
+ let decideFacts = (t, ~cardinality: cardinality=Many, op): result<array<fact>, [#NotAttached]> =>
171
194
  switch op {
172
195
  | Attach({ref, altText}) =>
173
196
  if t.attached->Array.includes(ref) {
@@ -206,3 +229,26 @@ let decide = (t, ~cardinality: cardinality=Many, op): result<array<fact>, [#NotA
206
229
  | None => Error(#NotAttached)
207
230
  }
208
231
  }
232
+
233
+ /**
234
+ The set's own facts, followed by `EffectiveChanged` when those facts moved which
235
+ member a reader should show.
236
+
237
+ Appended rather than woven into each arm because the question is the same one
238
+ after every op: fold what was decided, ask `effectivePrimary` again, and say so
239
+ if the answer differs. An arm that had to remember to announce would eventually
240
+ be an arm that forgot — and the ops where it moves without anybody choosing (a
241
+ first attachment, a removal that promotes the next) are exactly the ones where
242
+ forgetting is easiest.
243
+ */
244
+ let decide = (t, ~cardinality: cardinality=Many, op): result<array<fact>, [#NotAttached]> =>
245
+ switch decideFacts(t, ~cardinality, op) {
246
+ | Error(_) as e => e
247
+ | Ok(facts) =>
248
+ let after = facts->Array.reduce(t, evolve)
249
+ Ok(
250
+ effectivePrimary(t) == effectivePrimary(after)
251
+ ? facts
252
+ : facts->Array.concat([EffectiveChanged({ref: effectivePrimary(after)})]),
253
+ )
254
+ }
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Sury from "sury";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
7
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
@@ -84,6 +85,8 @@ function evolve(t, fact) {
84
85
  fact.altText
85
86
  ]])
86
87
  };
88
+ case "EffectiveChanged" :
89
+ return t;
87
90
  }
88
91
  }
89
92
 
@@ -112,7 +115,7 @@ function setAltText(t, ref, altText) {
112
115
  }
113
116
  }
114
117
 
115
- function decide(t, cardinalityOpt, op) {
118
+ function decideFacts(t, cardinalityOpt, op) {
116
119
  let cardinality = cardinalityOpt !== undefined ? cardinalityOpt : "Many";
117
120
  if (typeof op !== "object") {
118
121
  return {
@@ -209,6 +212,23 @@ function decide(t, cardinalityOpt, op) {
209
212
  }
210
213
  }
211
214
 
215
+ function decide(t, cardinalityOpt, op) {
216
+ let cardinality = cardinalityOpt !== undefined ? cardinalityOpt : "Many";
217
+ let e = decideFacts(t, cardinality, op);
218
+ if (e.TAG !== "Ok") {
219
+ return e;
220
+ }
221
+ let facts = e._0;
222
+ let after = Stdlib_Array.reduce(facts, t, evolve);
223
+ return {
224
+ TAG: "Ok",
225
+ _0: Primitive_object.equal(effectivePrimary(t), effectivePrimary(after)) ? facts : facts.concat([{
226
+ TAG: "EffectiveChanged",
227
+ ref: effectivePrimary(after)
228
+ }])
229
+ };
230
+ }
231
+
212
232
  export {
213
233
  cardinalitySchema,
214
234
  empty,
@@ -218,6 +238,7 @@ export {
218
238
  primaryFirst,
219
239
  evolve,
220
240
  setAltText,
241
+ decideFacts,
221
242
  decide,
222
243
  }
223
244
  /* cardinalitySchema Not a pure module */
@@ -126,6 +126,7 @@ type names = {
126
126
  attached: string,
127
127
  removed: string,
128
128
  primarySet: string,
129
+ effectiveChanged: string,
129
130
  altTextSet: string,
130
131
  notFound: string,
131
132
  notAttached: string,
@@ -151,6 +152,9 @@ let namesOf = (c: config): names => {
151
152
  attached: subject ++ "Attached",
152
153
  removed: subject ++ "Removed",
153
154
  primarySet: c.entity ++ "Primary" ++ c.noun ++ "Set",
155
+ // Named for what it states — the one that now stands — rather than for the
156
+ // command that moved it, because most of the time no command did.
157
+ effectiveChanged: c.entity ++ "Effective" ++ c.noun ++ "Changed",
154
158
  altTextSet: subject ++ "AltTextSet",
155
159
  notFound: c.entity ++ "NotFound",
156
160
  notAttached: subject ++ "NotAttached",
@@ -335,6 +339,7 @@ let sliceSpec = (c: config): string => {
335
339
  ` | ${n.removed}({ ${c.file}: ${ref}})`,
336
340
  ...primaryArm(""),
337
341
  ` | ${n.altTextSet}({ ${c.file}: ${ref}, altText: string})`,
342
+ ` | ${n.effectiveChanged}({ ${c.file}?: ${ref}})`,
338
343
  ` // TODO(graft): add the events this host's own refusal turns on — whatever`,
339
344
  ` // moves it into a state where attachments may not be changed.`,
340
345
  ``,
@@ -385,6 +390,10 @@ let sliceSpec = (c: config): string => {
385
390
  ` | ${n.removed}({ ${c.entityId}: string, ${c.file}: ${ref}})`,
386
391
  ...primaryArm(`${c.entityId}: string, `),
387
392
  ` | ${n.altTextSet}({ ${c.entityId}: string, ${c.file}: ${ref}, altText: string})`,
393
+ ` // The member a reader should now show, or none. A conclusion rather than a`,
394
+ ` // decision: most of the moves that change it — a first attachment, a removal`,
395
+ ` // promoting the next — are nobody's choice, so nothing else announces them.`,
396
+ ` | ${n.effectiveChanged}({ ${c.entityId}: string, ${c.file}?: ${ref}})`,
388
397
  ``,
389
398
  ...commandTransitionBinding(c),
390
399
  `// The graft's own record of itself. Nothing else survives into a deployed`,
@@ -426,6 +435,7 @@ let sliceBehavior = (c: config): string => {
426
435
  ? []
427
436
  : [` | ${n.primarySet}({${c.file}}) => fold(PrimarySet({ref: ${c.file}}))`]),
428
437
  ` | ${n.altTextSet}({${c.file}, altText}) => fold(AltTextSet({ref: ${c.file}, altText}))`,
438
+ ` | ${n.effectiveChanged}({${c.file}: ?ref}) => fold(EffectiveChanged({ref: ref}))`,
429
439
  ` // TODO(graft): fold this host's own events into its own state.`,
430
440
  ` }`,
431
441
  `}`,
@@ -492,6 +502,10 @@ let sliceBehavior = (c: config): string => {
492
502
  ` ${single ? "Some(" : ""}${n.altTextSet}({${c.entityId}, ${c.file}: ref, altText})${single
493
503
  ? ")"
494
504
  : ""}`,
505
+ ` | Attachments.EffectiveChanged({ref}) =>`,
506
+ ` ${single ? "Some(" : ""}${n.effectiveChanged}({${c.entityId}, ${c.file}: ?ref})${single
507
+ ? ")"
508
+ : ""}`,
495
509
  ` }`,
496
510
  ``,
497
511
  `let decide = (state, command) =>`,
@@ -544,16 +558,22 @@ let conformanceBinding = (c: config): string => {
544
558
  ` module Behavior = ${n.slice}_Behavior`,
545
559
  ``,
546
560
  ` // Annotated: the slice consumes and emits same-named constructors.`,
547
- ` let created: array<${n.slice}.consumedEvent> = [${createdValue}]`,
548
- ` let attachedC = (ref): ${n.slice}.consumedEvent => ${n.attached}({ ${c.file}: ref})`,
549
- ` let removedC = (ref): ${n.slice}.consumedEvent => ${n.removed}({ ${c.file}: ref})`,
561
+ ` // The same facts as the slice consumes them: no ${c.entityId}, because the`,
562
+ ` // partition already says which entity they belong to.`,
563
+ ` module Consumed = {`,
564
+ ` let created: array<${n.slice}.consumedEvent> = [${createdValue}]`,
565
+ ` let attached = (ref): ${n.slice}.consumedEvent => ${n.attached}({ ${c.file}: ref})`,
566
+ ` let removed = (ref): ${n.slice}.consumedEvent => ${n.removed}({ ${c.file}: ref})`,
550
567
  ...(single
551
568
  ? []
552
569
  : [
553
- ` let primarySetC = (ref): ${n.slice}.consumedEvent => ${n.primarySet}({ ${c.file}: ref})`,
570
+ ` let primarySet = (ref): ${n.slice}.consumedEvent => ${n.primarySet}({ ${c.file}: ref})`,
554
571
  ]),
555
- ` let altTextSetC = (ref, altText): ${n.slice}.consumedEvent =>`,
556
- ` ${n.altTextSet}({ ${c.file}: ref, altText})`,
572
+ ` let altTextSet = (ref, altText): ${n.slice}.consumedEvent =>`,
573
+ ` ${n.altTextSet}({ ${c.file}: ref, altText})`,
574
+ ` let effectiveChanged = (ref): ${n.slice}.consumedEvent =>`,
575
+ ` ${n.effectiveChanged}({ ${c.file}: ?ref})`,
576
+ ` }`,
557
577
  ``,
558
578
  ` let attach = ref => ${n.slice}.${n.attachCmd}({ ${c.entityId}: "${id}", ${c.file}: ref})`,
559
579
  ...(single
@@ -577,6 +597,8 @@ let conformanceBinding = (c: config): string => {
577
597
  ]),
578
598
  ` let altTextSet = (ref, altText) =>`,
579
599
  ` ${n.slice}.${n.altTextSet}({ ${c.entityId}: "${id}", ${c.file}: ref, altText})`,
600
+ ` let effectiveChanged = ref =>`,
601
+ ` ${n.slice}.${n.effectiveChanged}({ ${c.entityId}: "${id}", ${c.file}: ?ref})`,
580
602
  ` let notAttached = ${n.slice}.${n.notAttached}`,
581
603
  `}`,
582
604
  ``,
@@ -43,6 +43,7 @@ function namesOf(c) {
43
43
  attached: subject + "Attached",
44
44
  removed: subject + "Removed",
45
45
  primarySet: c.entity + "Primary" + c.noun + "Set",
46
+ effectiveChanged: c.entity + "Effective" + c.noun + "Changed",
46
47
  altTextSet: subject + "AltTextSet",
47
48
  notFound: c.entity + "NotFound",
48
49
  notAttached: subject + "NotAttached"
@@ -189,6 +190,7 @@ function sliceSpec(c) {
189
190
  primaryArm(""),
190
191
  [
191
192
  ` | ` + n.altTextSet + `({ ` + c.file + `: ` + ref + `, altText: string})`,
193
+ ` | ` + n.effectiveChanged + `({ ` + c.file + `?: ` + ref + `})`,
192
194
  ` // TODO(graft): add the events this host's own refusal turns on — whatever`,
193
195
  ` // moves it into a state where attachments may not be changed.`,
194
196
  ``
@@ -235,6 +237,10 @@ function sliceSpec(c) {
235
237
  primaryArm(c.entityId + `: string, `),
236
238
  [
237
239
  ` | ` + n.altTextSet + `({ ` + c.entityId + `: string, ` + c.file + `: ` + ref + `, altText: string})`,
240
+ ` // The member a reader should now show, or none. A conclusion rather than a`,
241
+ ` // decision: most of the moves that change it — a first attachment, a removal`,
242
+ ` // promoting the next — are nobody's choice, so nothing else announces them.`,
243
+ ` | ` + n.effectiveChanged + `({ ` + c.entityId + `: string, ` + c.file + `?: ` + ref + `})`,
238
244
  ``
239
245
  ],
240
246
  commandTransitionBinding(c),
@@ -273,6 +279,7 @@ function sliceBehavior(c) {
273
279
  single ? [] : [` | ` + n.primarySet + `({` + c.file + `}) => fold(PrimarySet({ref: ` + c.file + `}))`],
274
280
  [
275
281
  ` | ` + n.altTextSet + `({` + c.file + `, altText}) => fold(AltTextSet({ref: ` + c.file + `, altText}))`,
282
+ ` | ` + n.effectiveChanged + `({` + c.file + `: ?ref}) => fold(EffectiveChanged({ref: ref}))`,
276
283
  ` // TODO(graft): fold this host's own events into its own state.`,
277
284
  ` }`,
278
285
  `}`,
@@ -332,6 +339,12 @@ function sliceBehavior(c) {
332
339
  ) + n.altTextSet + `({` + c.entityId + `, ` + c.file + `: ref, altText})` + (
333
340
  single ? ")" : ""
334
341
  ),
342
+ ` | Attachments.EffectiveChanged({ref}) =>`,
343
+ ` ` + (
344
+ single ? "Some(" : ""
345
+ ) + n.effectiveChanged + `({` + c.entityId + `, ` + c.file + `: ?ref})` + (
346
+ single ? ")" : ""
347
+ ),
335
348
  ` }`,
336
349
  ``,
337
350
  `let decide = (state, command) =>`,
@@ -373,14 +386,20 @@ function conformanceBinding(c) {
373
386
  ` module Behavior = ` + n.slice + `_Behavior`,
374
387
  ``,
375
388
  ` // Annotated: the slice consumes and emits same-named constructors.`,
376
- ` let created: array<` + n.slice + `.consumedEvent> = [` + createdValue + `]`,
377
- ` let attachedC = (ref): ` + n.slice + `.consumedEvent => ` + n.attached + `({ ` + c.file + `: ref})`,
378
- ` let removedC = (ref): ` + n.slice + `.consumedEvent => ` + n.removed + `({ ` + c.file + `: ref})`
389
+ ` // The same facts as the slice consumes them: no ` + c.entityId + `, because the`,
390
+ ` // partition already says which entity they belong to.`,
391
+ ` module Consumed = {`,
392
+ ` let created: array<` + n.slice + `.consumedEvent> = [` + createdValue + `]`,
393
+ ` let attached = (ref): ` + n.slice + `.consumedEvent => ` + n.attached + `({ ` + c.file + `: ref})`,
394
+ ` let removed = (ref): ` + n.slice + `.consumedEvent => ` + n.removed + `({ ` + c.file + `: ref})`
379
395
  ],
380
- single ? [] : [` let primarySetC = (ref): ` + n.slice + `.consumedEvent => ` + n.primarySet + `({ ` + c.file + `: ref})`],
396
+ single ? [] : [` let primarySet = (ref): ` + n.slice + `.consumedEvent => ` + n.primarySet + `({ ` + c.file + `: ref})`],
381
397
  [
382
- ` let altTextSetC = (ref, altText): ` + n.slice + `.consumedEvent =>`,
383
- ` ` + n.altTextSet + `({ ` + c.file + `: ref, altText})`,
398
+ ` let altTextSet = (ref, altText): ` + n.slice + `.consumedEvent =>`,
399
+ ` ` + n.altTextSet + `({ ` + c.file + `: ref, altText})`,
400
+ ` let effectiveChanged = (ref): ` + n.slice + `.consumedEvent =>`,
401
+ ` ` + n.effectiveChanged + `({ ` + c.file + `: ?ref})`,
402
+ ` }`,
384
403
  ``,
385
404
  ` let attach = ref => ` + n.slice + `.` + n.attachCmd + `({ ` + c.entityId + `: "` + id + `", ` + c.file + `: ref})`
386
405
  ],
@@ -402,6 +421,8 @@ function conformanceBinding(c) {
402
421
  [
403
422
  ` let altTextSet = (ref, altText) =>`,
404
423
  ` ` + n.slice + `.` + n.altTextSet + `({ ` + c.entityId + `: "` + id + `", ` + c.file + `: ref, altText})`,
424
+ ` let effectiveChanged = ref =>`,
425
+ ` ` + n.slice + `.` + n.effectiveChanged + `({ ` + c.entityId + `: "` + id + `", ` + c.file + `: ?ref})`,
405
426
  ` let notAttached = ` + n.slice + `.` + n.notAttached,
406
427
  `}`,
407
428
  ``,