@effect.healthcare/messenger 0.5.0 → 0.6.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.
@@ -1,686 +0,0 @@
1
- # Event
2
-
3
- This document defines an implementation policy for **component events**, then illustrates how specifications are implemented by applying this policy.
4
-
5
- This document is intentionally **events** only, but part of a set of implementation policies which together defines the policies which should be followed to transform specifications into an implementation which is predictable, maintainable, and production-ready at scale.
6
-
7
- ---
8
-
9
- # How to Read This Policy
10
-
11
- Rules in this policy fall into three strengths. Read each rule with its strength in mind:
12
-
13
- - **Principles** are non-negotiable. They derive from framework correctness or TDD discipline and hold for every component. A conflict with a principle is a defect.
14
- - **Templates** are invariant skeletons with explicit variation points. The skeleton applies to every component; specific steps activate or deactivate based on the component's declared classification (e.g., attribute-backed vs non-attribute, readonly vs writable, payload-bearing vs payload-free). Variation is part of the template, not a deviation from it.
15
- - **Conventions** are canonical defaults that keep implementations readable across components. A second component may deviate when its shape genuinely warrants it; the deviation should be explained at review time, not forbidden by the policy.
16
-
17
- When applying this policy to a new component, classify each rule before reconciling conflicts. A conflict with a principle is a defect; a conflict with a template means a variation point needs to be made explicit; a conflict with a convention is a documented deviation.
18
-
19
- ---
20
-
21
- # 1) Event Contract Must Be Declared in the Specification
22
-
23
- Each component specification must declare its public event contract under `Events`.
24
-
25
- `Events` must be an object whose property names are the canonical event names.
26
-
27
- Canonical event names must use lowerCamelCase and must begin with `on`.
28
-
29
- Each event definition must include:
30
-
31
- - `description`
32
-
33
- An event definition may also include:
34
-
35
- - `parameters`
36
-
37
- Interpretation:
38
-
39
- - `description` defines the semantic meaning of the event
40
- - `parameters` defines the public runtime payload contract for the event
41
- - if `parameters` is present, runtime payload must be expressed through a `detail` object
42
- - if `parameters` is absent, the event emits no declared runtime payload
43
-
44
- Example specification:
45
-
46
- ```json
47
- "onpin": {
48
- "description": "Dispatched when the component becomes pinned.",
49
- "parameters": [
50
- {
51
- "name": "detail",
52
- "type": "object",
53
- "properties": {
54
- "status": {
55
- "type": "Status",
56
- "values": ["pinned"]
57
- }
58
- }
59
- }
60
- ]
61
- }
62
- ```
63
-
64
- This defines the public event contract that the implementation must honor.
65
-
66
- # Template Variation Points
67
-
68
- Events vary by payload presence:
69
-
70
- - **Payload-bearing events** — declared with `parameters` in the specification. Runtime payload is delivered through `event.detail` and must match the declared parameter contract (Section 7). Verification under the testing policy requires three behavioral commit pairs: setter existence, handler trigger, and payload contract.
71
- - **Payload-free events** — declared without `parameters`. They emit no runtime payload. Verification requires two behavioral commit pairs: setter existence and handler trigger.
72
-
73
- The framework dispatch path (Section 6), the listener-replacement semantics (Section 5), and the accepted-outcome dispatch rule (Section 8) are invariant across both variants.
74
-
75
- # 2) Events Must Represent Semantic Outcomes
76
-
77
- This framework distinguishes:
78
-
79
- - `Events`: the set of observable semantic outcomes exposed by the component
80
- - `Subscription Properties`: the public component API surface used to assign event handlers
81
- - `Runtime Dispatch`: the framework event path used to emit public events
82
- - `Gestures`: the set of low-level user or runtime interactions that may trigger behavior
83
-
84
- Policy:
85
-
86
- - events describe what happened as a meaningful component outcome
87
- - gestures describe what the user did or what low-level interaction occurred
88
- - subscription properties provide the public assignment surface for handling declared events
89
- - runtime dispatch provides the authoritative emission path for declared public events
90
- - events must not be used to declare gestures
91
- - events must not be used to declare state
92
- - events must not be used to declare operations
93
-
94
- Examples of valid semantic events:
95
-
96
- - `onpin`
97
- - `onunpin`
98
- - `onshow`
99
- - `onhide`
100
-
101
- Examples that must not normally be modeled as component events:
102
-
103
- - `onclick`
104
- - `onhover`
105
- - `onmousedown`
106
-
107
- These belong to gesture-level behavior, not semantic outcome reporting.
108
-
109
- # 3) Public Event Names Must Be Declared in Metadata
110
-
111
- For every event declared in the specification, the implementation must define corresponding metadata in `<component>.meta.ts`.
112
-
113
- This metadata defines the canonical event vocabulary for the component.
114
-
115
- Policy:
116
-
117
- - every declared public event must have a key in `Event`
118
- - `Event` contains only declared public event names
119
- - the implementation must use event metadata consistently as the canonical event vocabulary
120
-
121
- Example pattern:
122
-
123
- ```typescript
124
- export const Event = {
125
- ON_HIDE: "onhide",
126
- ON_SHOW: "onshow",
127
- ON_PIN: "onpin",
128
- ON_UNPIN: "onunpin",
129
- } as const;
130
-
131
- export type Event = (typeof Event)[keyof typeof Event];
132
- ```
133
-
134
- This gives the implementation a stable, centralized event vocabulary that prevents string duplication and keeps naming consistent across metadata, subscription properties, and dispatch.
135
-
136
- # 4) Event Subscription Must Be Exposed Through Properties
137
-
138
- For every public event declared in the specification, the component class must expose a corresponding public subscription property.
139
-
140
- Policy:
141
-
142
- - every declared public event must have a corresponding public subscription property on the component class
143
- - the public subscription property name must match the canonical event name
144
- - the subscription property must store its assigned handler in a private handler slot
145
- - assigning a subscription property must replace the previously assigned handler for that property
146
- - assigning `null` must remove the current handler
147
-
148
- Example pattern:
149
-
150
- ```typescript
151
- private _onpin: EventListener | null = null;
152
-
153
- public set onpin(handler: EventListener | null) {
154
- this._onpin && this.removeEventListener(Event.ON_PIN, this._onpin);
155
-
156
- this._onpin = handler;
157
-
158
- this._onpin && this.addEventListener(Event.ON_PIN, this._onpin);
159
- }
160
- ```
161
-
162
- This gives the component a stable event subscription API:
163
-
164
- ```typescript
165
- pin.onpin = (event) => console.log(event.detail.status);
166
- pin.onpin = null;
167
- ```
168
-
169
- # 5) Subscription Properties Must Replace Existing Listeners
170
-
171
- Event subscription properties must behave as single-slot listener assignments.
172
-
173
- Assignment replaces the previous handler.
174
-
175
- Required sequence:
176
-
177
- 1. remove the previous listener
178
- 2. store the new handler
179
- 3. attach the new listener if non-null
180
-
181
- Policy:
182
-
183
- - a subscription property must manage at most one currently assigned handler
184
- - reassigning a subscription property must replace the previous handler rather than accumulate listeners
185
- - assigning `null` must leave no active listener for that subscription property
186
- - listener replacement behavior must be implemented through normal DOM listener registration and removal
187
-
188
- Example:
189
-
190
- ```typescript
191
- public set onunpin(handler: EventListener | null) {
192
- this._onunpin && this.removeEventListener(Event.ON_UNPIN, this._onunpin);
193
-
194
- this._onunpin = handler;
195
-
196
- this._onunpin && this.addEventListener(Event.ON_UNPIN, this._onunpin);
197
- }
198
- ```
199
-
200
- To remove the listener:
201
-
202
- ```typescript
203
- pin.onunpin = null;
204
- ```
205
-
206
- # 6) Events Must Be Dispatched Through the Framework Dispatch Path
207
-
208
- Events must not manually call subscribed handlers.
209
-
210
- They must be emitted through the framework dispatch helper so the event is dispatched through the component's standard event path.
211
-
212
- Policy:
213
-
214
- - public events must be dispatched through `_dispatchEvent(...)`
215
- - the implementation must not manually invoke subscribed handlers as a substitute for event dispatch
216
- - the dispatch helper is the authoritative runtime path for public event emission
217
- - event name metadata from `Event` must be used as the dispatch vocabulary
218
-
219
- Example:
220
-
221
- ```typescript
222
- const event = { detail: { status } };
223
-
224
- status === Status.PINNED && this._dispatchEvent(Event.ON_PIN, event);
225
- status === Status.UNPINNED && this._dispatchEvent(Event.ON_UNPIN, event);
226
- ```
227
-
228
- This ensures the implementation stays aligned with the DOM event model and keeps public event emission consistent across the component.
229
-
230
- # 7) Event Payload Must Match the Specification Contract
231
-
232
- For every event declared with `parameters` in the specification, the implementation must emit a runtime payload that matches that declared contract.
233
-
234
- Event payload must be delivered through `detail`.
235
-
236
- Policy:
237
-
238
- - if an event declares `parameters`, the emitted runtime payload must match the declared parameter contract
239
- - when runtime payload is emitted, it must be delivered through `detail`
240
- - emitted payload values must conform to the values and types declared in the specification
241
- - emitted payload must describe the accepted semantic outcome represented by the event
242
- - the implementation must not emit undeclared runtime payload for a declared public event
243
-
244
- Example dispatch:
245
-
246
- ```typescript
247
- const event = { detail: { visibility } };
248
-
249
- this._dispatchEvent(Event.ON_SHOW, event);
250
- ```
251
-
252
- Consumer usage:
253
-
254
- ```typescript
255
- pin.onshow = (event) => console.log(event.detail.visibility);
256
- ```
257
-
258
- In this example, the emitted `detail.visibility` value must match the event contract declared in the specification for `onshow`.
259
-
260
- # 8) Events Must Only Be Emitted After Accepted Transitions
261
-
262
- Public events that represent semantic outcomes must be emitted only after the component has accepted the outcome they describe.
263
-
264
- Policy:
265
-
266
- - no event must be emitted for a no-op assignment
267
- - no event must be emitted for a rejected transition
268
- - no event must be emitted before the component accepts the semantic outcome
269
- - when an event represents a state outcome, dispatch must occur after the authoritative mutation path has accepted and applied the transition
270
-
271
- Example pattern:
272
-
273
- ```typescript
274
- public set status(status: Status) {
275
- status = Validate.status(status);
276
-
277
- if (this._status === status) return;
278
-
279
- this._status = status;
280
- this.setAttribute(Attributes.STATUS, status);
281
-
282
- const event = { detail: { status } };
283
-
284
- status === Status.PINNED && this._dispatchEvent(Event.ON_PIN, event);
285
- status === Status.UNPINNED && this._dispatchEvent(Event.ON_UNPIN, event);
286
- }
287
- ```
288
-
289
- In this example, the event is emitted only after validation succeeds, the guard allows the transition, and the canonical state has been updated to the accepted outcome.
290
-
291
- # 9) Naming Must Be Predictably Transformed Across Layers
292
-
293
- Public event naming must remain consistent across the specification, metadata, component API, and runtime dispatch.
294
-
295
- Policy:
296
-
297
- - the specification defines the canonical public event name
298
- - metadata must define a corresponding `Event` key whose value is that canonical event name
299
- - the component class must expose a public subscription property using that same canonical event name
300
- - runtime dispatch must use the corresponding metadata value from `Event`
301
- - naming must remain predictably traceable across all implementation layers
302
-
303
- Transformation pattern:
304
-
305
- | Layer | Example |
306
- |---|---|
307
- | Specification | `onpin` |
308
- | Metadata | `Event.ON_PIN = "onpin"` |
309
- | Component API | `set onpin(handler)` |
310
- | Dispatch | `_dispatchEvent(Event.ON_PIN, ...)` |
311
-
312
- This predictable transformation keeps the implementation directly traceable back to the specification and consistent with the framework's metadata-driven design.
313
-
314
- ---
315
-
316
- # Policy Checklist
317
-
318
- A compliant component must:
319
-
320
- 1. Declare all public events under `Events` in the specification.
321
- 2. Define each event using the required specification contract.
322
- 3. Use `description` for every declared event.
323
- 4. Use `parameters` only when the event emits runtime payload.
324
- 5. Express runtime payload through a `detail` object when payload is emitted.
325
- 6. Ensure events represent semantic outcomes rather than low-level gestures.
326
- 7. Avoid modeling gestures, state, or operations as events.
327
- 8. Define all declared public event names in `<component>.meta.ts`.
328
- 9. Use `Event` metadata as the canonical event vocabulary.
329
- 10. Expose a public subscription property for every declared public event.
330
- 11. Keep each subscription property name aligned with the canonical event name from the specification.
331
- 12. Type subscription property setters as `EventListener | null` to formally support handler removal.
332
- 13. Store each assigned subscription handler in a private handler slot typed `EventListener | null`.
333
- 14. Replace the previous listener when a subscription property is reassigned.
334
- 15. Remove the active listener when a subscription property is assigned `null`.
335
- 16. Dispatch public events through `_dispatchEvent(...)`.
336
- 17. Never manually invoke subscribed handlers as a substitute for event dispatch.
337
- 18. Emit runtime payload through `event.detail`.
338
- 19. Ensure emitted payload matches the specification contract for the event.
339
- 20. Emit only values and shapes declared by the specification.
340
- 21. Emit events only after the semantic outcome has been accepted.
341
- 22. Emit no event for no-op assignments.
342
- 23. Emit no event for rejected transitions.
343
- 24. Keep naming predictably aligned across specification, metadata, component API, and runtime dispatch.
344
-
345
- ---
346
-
347
- # Policy Applied to Specification
348
-
349
- Specification fragment:
350
-
351
- ```json
352
- {
353
- "Events": {
354
- "onhide": {
355
- "description": "Dispatched when pin 'visibility' state changes from 'visible' to 'hidden'.",
356
- "parameters": [
357
- {
358
- "name": "detail",
359
- "type": "object",
360
- "description": "Contains the new visibility state of the component.",
361
- "properties": {
362
- "visibility": {
363
- "type": "Visibility",
364
- "values": ["hidden"]
365
- }
366
- }
367
- }
368
- ]
369
- },
370
- "onshow": {
371
- "description": "Dispatched when pin 'visibility' state changes from 'hidden' to 'visible'.",
372
- "parameters": [
373
- {
374
- "name": "detail",
375
- "type": "object",
376
- "description": "Contains the new visibility state of the component.",
377
- "properties": {
378
- "visibility": {
379
- "type": "Visibility",
380
- "values": ["visible"]
381
- }
382
- }
383
- }
384
- ]
385
- },
386
- "onpin": {
387
- "description": "Dispatched when pin 'status' changes from 'unpinned' to 'pinned'.",
388
- "parameters": [
389
- {
390
- "name": "detail",
391
- "type": "object",
392
- "description": "Contains the new status of the component.",
393
- "properties": {
394
- "status": {
395
- "type": "Status",
396
- "values": ["pinned"]
397
- }
398
- }
399
- }
400
- ]
401
- },
402
- "onunpin": {
403
- "description": "Dispatched when pin 'status' changes from 'pinned' to 'unpinned'.",
404
- "parameters": [
405
- {
406
- "name": "detail",
407
- "type": "object",
408
- "description": "Contains the new status of the component.",
409
- "properties": {
410
- "status": {
411
- "type": "Status",
412
- "values": ["unpinned"]
413
- }
414
- }
415
- }
416
- ]
417
- }
418
- }
419
- }
420
- ```
421
-
422
- How this specification is implemented:
423
-
424
- - every declared event is represented in metadata as part of the canonical `Event` vocabulary
425
- - every declared event is exposed through a corresponding public subscription property
426
- - `onhide`
427
- - subscription property typed `EventListener | null`
428
- - dispatched by the `visibility` setter after accepting a transition to `Visibility.HIDDEN`
429
- - payload: `detail.visibility = "hidden"`
430
- - `onshow`
431
- - subscription property typed `EventListener | null`
432
- - dispatched by the `visibility` setter after accepting a transition to `Visibility.VISIBLE`
433
- - payload: `detail.visibility = "visible"`
434
- - `onpin`
435
- - subscription property typed `EventListener | null`
436
- - dispatched by the `status` setter after accepting a transition to `Status.PINNED`
437
- - payload: `detail.status = "pinned"`
438
- - `onunpin`
439
- - subscription property typed `EventListener | null`
440
- - dispatched by the `status` setter after accepting a transition to `Status.UNPINNED`
441
- - payload: `detail.status = "unpinned"`
442
-
443
- Implementation:
444
-
445
- ```typescript
446
- private _onhide: EventListener | null = null;
447
- private _onshow: EventListener | null = null;
448
- private _onpin: EventListener | null = null;
449
- private _onunpin: EventListener | null = null;
450
-
451
- public set onhide(handler: EventListener | null) {
452
- this._onhide && this.removeEventListener(Event.ON_HIDE, this._onhide);
453
- this._onhide = handler;
454
- this._onhide && this.addEventListener(Event.ON_HIDE, this._onhide);
455
- }
456
-
457
- public set onshow(handler: EventListener | null) {
458
- this._onshow && this.removeEventListener(Event.ON_SHOW, this._onshow);
459
- this._onshow = handler;
460
- this._onshow && this.addEventListener(Event.ON_SHOW, this._onshow);
461
- }
462
-
463
- public set onpin(handler: EventListener | null) {
464
- this._onpin && this.removeEventListener(Event.ON_PIN, this._onpin);
465
- this._onpin = handler;
466
- this._onpin && this.addEventListener(Event.ON_PIN, this._onpin);
467
- }
468
-
469
- public set onunpin(handler: EventListener | null) {
470
- this._onunpin && this.removeEventListener(Event.ON_UNPIN, this._onunpin);
471
- this._onunpin = handler;
472
- this._onunpin && this.addEventListener(Event.ON_UNPIN, this._onunpin);
473
- }
474
- ```
475
-
476
- Dispatch from within the authoritative state setters:
477
-
478
- ```typescript
479
- public set visibility(visibility: Visibility) {
480
- visibility = visibility ?? Visibility.VISIBLE;
481
- visibility = Validate.visibility(visibility);
482
-
483
- if (this._visibility === visibility) return;
484
-
485
- this._visibility = visibility;
486
- visibility === Visibility.VISIBLE && this.removeAttribute(Attributes.VISIBILITY);
487
- visibility === Visibility.HIDDEN && this.setAttribute(Attributes.VISIBILITY, visibility);
488
-
489
- const event = { detail: { visibility } };
490
-
491
- visibility === Visibility.HIDDEN && this._dispatchEvent(Event.ON_HIDE, event);
492
- visibility === Visibility.VISIBLE && this._dispatchEvent(Event.ON_SHOW, event);
493
- }
494
-
495
- public set status(status: Status) {
496
- status = Validate.status(status);
497
-
498
- if (this._status === status) return;
499
-
500
- this._status = status;
501
- this.setAttribute(Attributes.STATUS, status);
502
-
503
- const event = { detail: { status } };
504
-
505
- status === Status.PINNED && this._dispatchEvent(Event.ON_PIN, event);
506
- status === Status.UNPINNED && this._dispatchEvent(Event.ON_UNPIN, event);
507
- }
508
- ```
509
-
510
- How the implementation applies the policy:
511
-
512
- - the specification defines the public event contract under `Events`
513
- - metadata defines the canonical event vocabulary
514
- - the component class exposes each declared event as a public subscription property with the same canonical name
515
- - subscription properties are typed `EventListener | null` to formally support both handler assignment and handler removal
516
- - subscription properties manage a single handler slot — reassignment replaces, `null` removes
517
- - events are dispatched through `_dispatchEvent(...)`, not by manually invoking handlers
518
- - payload is delivered through `detail` and matches the values declared in the specification
519
- - events are emitted only after the authoritative state setter has validated, guarded, mutated, and reflected the accepted transition
520
- - naming is predictably traceable: specification → `Event` metadata → subscription property → `_dispatchEvent` call
521
-
522
- ---
523
-
524
- # Component Event Specification Schema
525
-
526
- JSON Schema:
527
-
528
- ```json
529
- {
530
- "$schema": "https://json-schema.org/draft/2020-12/schema",
531
- "$id": "https://scalable.software/schema/component-events.schema.json",
532
- "title": "Component Events",
533
- "description": "Schema for the Events object in a component specification.",
534
- "type": "object",
535
- "propertyNames": {
536
- "type": "string",
537
- "pattern": "^on[a-z][a-zA-Z0-9]*$",
538
- "description": "Canonical event name in lowerCamelCase beginning with 'on', for example: onpin, onunpin, onshow, onhide."
539
- },
540
- "additionalProperties": {
541
- "type": "object",
542
- "description": "An event definition.",
543
- "required": ["description"],
544
- "additionalProperties": false,
545
- "properties": {
546
- "description": {
547
- "type": "string",
548
- "minLength": 1,
549
- "description": "Human-readable description of the semantic outcome represented by the event."
550
- },
551
- "parameters": {
552
- "type": "array",
553
- "description": "Ordered list of runtime payload parameters for the event. When present, runtime payload must be expressed through a detail object.",
554
- "minItems": 1,
555
- "items": {
556
- "type": "object",
557
- "required": ["name", "type"],
558
- "additionalProperties": false,
559
- "properties": {
560
- "name": {
561
- "type": "string",
562
- "const": "detail",
563
- "description": "The public runtime payload channel for component events. Event payload must be exposed through detail."
564
- },
565
- "type": {
566
- "type": "string",
567
- "const": "object",
568
- "description": "Event payload type. Runtime payload for component events must be expressed as an object."
569
- },
570
- "description": {
571
- "type": "string",
572
- "minLength": 1,
573
- "description": "Human-readable description of the detail payload."
574
- },
575
- "properties": {
576
- "type": "object",
577
- "description": "The object properties emitted through event.detail.",
578
- "minProperties": 1,
579
- "propertyNames": {
580
- "type": "string",
581
- "pattern": "^[a-z][a-zA-Z0-9]*$",
582
- "description": "Canonical property name in lowerCamelCase, for example: status, visibility."
583
- },
584
- "additionalProperties": {
585
- "type": "object",
586
- "required": ["type"],
587
- "additionalProperties": false,
588
- "properties": {
589
- "type": {
590
- "type": "string",
591
- "minLength": 1,
592
- "description": "Declared type of the detail property, for example: Status, Visibility, string, boolean."
593
- },
594
- "values": {
595
- "type": "array",
596
- "minItems": 1,
597
- "description": "Allowed values for this emitted property when the event contract constrains the outcome to a subset of the value domain."
598
- },
599
- "description": {
600
- "type": "string",
601
- "minLength": 1,
602
- "description": "Human-readable description of the emitted property."
603
- }
604
- }
605
- }
606
- }
607
- }
608
- }
609
- }
610
- }
611
- }
612
- }
613
- ```
614
-
615
- Example JSON:
616
-
617
- ```json
618
- {
619
- "Events": {
620
- "onhide": {
621
- "description": "Dispatched when pin 'visibility' state changes from 'visible' to 'hidden'.",
622
- "parameters": [
623
- {
624
- "name": "detail",
625
- "type": "object",
626
- "description": "Contains the new visibility state of the component.",
627
- "properties": {
628
- "visibility": {
629
- "type": "Visibility",
630
- "values": ["hidden"]
631
- }
632
- }
633
- }
634
- ]
635
- },
636
- "onshow": {
637
- "description": "Dispatched when pin 'visibility' state changes from 'hidden' to 'visible'.",
638
- "parameters": [
639
- {
640
- "name": "detail",
641
- "type": "object",
642
- "description": "Contains the new visibility state of the component.",
643
- "properties": {
644
- "visibility": {
645
- "type": "Visibility",
646
- "values": ["visible"]
647
- }
648
- }
649
- }
650
- ]
651
- },
652
- "onpin": {
653
- "description": "Dispatched when pin 'status' changes from 'unpinned' to 'pinned'.",
654
- "parameters": [
655
- {
656
- "name": "detail",
657
- "type": "object",
658
- "description": "Contains the new status of the component.",
659
- "properties": {
660
- "status": {
661
- "type": "Status",
662
- "values": ["pinned"]
663
- }
664
- }
665
- }
666
- ]
667
- },
668
- "onunpin": {
669
- "description": "Dispatched when pin 'status' changes from 'pinned' to 'unpinned'.",
670
- "parameters": [
671
- {
672
- "name": "detail",
673
- "type": "object",
674
- "description": "Contains the new status of the component.",
675
- "properties": {
676
- "status": {
677
- "type": "Status",
678
- "values": ["unpinned"]
679
- }
680
- }
681
- }
682
- ]
683
- }
684
- }
685
- }
686
- ```