@kerusiweb/angular 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +336 -0
- package/fesm2022/kerusiweb-angular.mjs +871 -0
- package/fesm2022/kerusiweb-angular.mjs.map +1 -0
- package/package.json +51 -0
- package/types/kerusiweb-angular.d.ts +390 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-2026 Arsyad "Shad Ahm" Ahmad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# @kerusiweb/angular
|
|
2
|
+
|
|
3
|
+
An Angular seat map that renders [Kerusi](https://github.com/ShadAhm/kerusi)
|
|
4
|
+
documents as interactive, accessible inline SVG.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @kerusiweb/core @kerusiweb/angular
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requires Angular 22+. Standalone, signal-based, zoneless-friendly.
|
|
11
|
+
|
|
12
|
+
This package is the Angular binding only — the components and the signal-backed
|
|
13
|
+
`KerusiStateStore`. The document types, the conformance validator, the render
|
|
14
|
+
model and its geometry live in its peer
|
|
15
|
+
[`@kerusiweb/core`](https://www.npmjs.com/package/@kerusiweb/core), which is
|
|
16
|
+
framework-free; import them from there.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## `<kerusi-seatmap>`
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { KerusiSeatmapComponent } from '@kerusiweb/angular';
|
|
24
|
+
|
|
25
|
+
@Component({ imports: [KerusiSeatmapComponent], /* ... */ })
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<kerusi-seatmap
|
|
30
|
+
[map]="map"
|
|
31
|
+
[state]="state"
|
|
32
|
+
[(selection)]="picked"
|
|
33
|
+
[showLegend]="true"
|
|
34
|
+
(seatDisallowed)="explain($event)"
|
|
35
|
+
/>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Inputs
|
|
39
|
+
|
|
40
|
+
**Documents**
|
|
41
|
+
|
|
42
|
+
| Input | Type | Default | |
|
|
43
|
+
| --------- | --------------- | ---------- | --------------------------------------------------------------------------- |
|
|
44
|
+
| `map` | `KerusiMap` | _required_ | The static venue layout. |
|
|
45
|
+
| `state` | `KerusiState` | — | Live availability. Merged by `Seat.id`; an absent seat is available (§5.1). |
|
|
46
|
+
| `session` | `KerusiSession` | — | The optional map↔event join (§5.3). Validated against the map and state. |
|
|
47
|
+
|
|
48
|
+
**Selection**
|
|
49
|
+
|
|
50
|
+
| Input | Type | Default | |
|
|
51
|
+
| -------------------- | ------------------------------- | --------------- | ------------------------------------------------------------------------------- |
|
|
52
|
+
| `selection` | `readonly string[]` | `[]` | Selected seat ids. Two-way — `[(selection)]` — or read-only with `[selection]`. |
|
|
53
|
+
| `selectableStatuses` | `SeatRenderStatus[]` | `['available']` | Which statuses a seat may be picked in. |
|
|
54
|
+
| `companionMode` | `'auto' \| 'independent'` | `'auto'` | `auto` selects a seat's whole companion closure together (§4.6). |
|
|
55
|
+
| `maxSelection` | `number` | — | Cap on selected seats. Counts a companion closure as its full size. |
|
|
56
|
+
| `seatSelectable` | `(seat: RenderSeat) => boolean` | — | A final say, applied after the status test. |
|
|
57
|
+
| `interactive` | `boolean` | `true` | `false` renders read-only. |
|
|
58
|
+
|
|
59
|
+
**Appearance**
|
|
60
|
+
|
|
61
|
+
| Input | Type | Default | |
|
|
62
|
+
| --------------------------------- | --------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
63
|
+
| `colors` | `KerusiSeatmapColors` | theme defaults | Per-status fills, element tones, focus ring, backdrop. See [Theming](#theming) for the CSS custom properties. |
|
|
64
|
+
| `typeColors` | `boolean` | `true` | Let an available seat take its `SeatType.color` (§4.7). |
|
|
65
|
+
| `seatSize` | `number` | `28` | Grid cell edge, in viewBox units. |
|
|
66
|
+
| `seatGap` | `number` | `6` | Gap between grid cells. |
|
|
67
|
+
| `freeformBasis` | `number` | `1000` | Freeform viewBox width; height follows the aspect ratio. |
|
|
68
|
+
| `unitScale` | `number` | `1` | CSS pixels per viewBox unit. Caps each section at its natural size so a narrow section and a wide one draw seats the same size. |
|
|
69
|
+
| `showSectionLabels` | `boolean` | `true` | |
|
|
70
|
+
| `showLegend` / `showLegendPrices` | `boolean` | `false` / `true` | |
|
|
71
|
+
|
|
72
|
+
**Sections**
|
|
73
|
+
|
|
74
|
+
| Input | Type | |
|
|
75
|
+
| ------------------ | -------------------------------------- | ------------------------------------------------------------------- |
|
|
76
|
+
| `sectionIds` | `readonly string[]` | Render only these, in this order. Default: all, by `Section.index`. |
|
|
77
|
+
| `sectionOverrides` | `Record<string, SectionRenderOptions>` | Per-section `hidden`, `aspectRatio`, `seatSize`, `label`. |
|
|
78
|
+
|
|
79
|
+
**Localization, validation, lifecycle**
|
|
80
|
+
|
|
81
|
+
| Input | Type | Default | |
|
|
82
|
+
| ------------------ | ------------------------------- | ---------------------------- | ---------------------------------------------------------------------- |
|
|
83
|
+
| `locale` | `string` | `KerusiMap.locale` ?? `'en'` | BCP-47. Resolves `Section.label` / `SeatType.label` locale maps. |
|
|
84
|
+
| `rtl` | `boolean \| 'auto'` | `'auto'` | Mirrors layout and arrow direction. `auto` derives it from the locale. |
|
|
85
|
+
| `ariaStrings` | `SeatAriaStrings` | English | Every announced phrase, for translation. |
|
|
86
|
+
| `validate` | `'collect' \| 'throw' \| 'off'` | `'collect'` | `collect` reports through `validationIssues` and renders anyway. |
|
|
87
|
+
| `expireHolds` | `boolean` | `false` | Revert a lapsed `holdExpires` to available on a ticker. |
|
|
88
|
+
| `expiryIntervalMs` | `number` | `1000` | |
|
|
89
|
+
|
|
90
|
+
### Outputs
|
|
91
|
+
|
|
92
|
+
| Output | Payload | |
|
|
93
|
+
| ----------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
94
|
+
| `selectionChange` | `readonly string[]` | From the `selection` model. |
|
|
95
|
+
| `seatSelect` / `seatDeselect` | `SeatInteraction` | The `RenderSeat`, the resulting selection, and every seat that changed (companions included). |
|
|
96
|
+
| `seatDisallowed` | `SeatDisallowed` | `reason` is `booked`, `held`, `blocked`, `not-selectable`, `max-selection` or `companion-unavailable`. |
|
|
97
|
+
| `seatFocus` | `RenderSeat` | |
|
|
98
|
+
| `validationIssues` | `readonly KerusiViolation[]` | Emitted whenever the documents change. |
|
|
99
|
+
|
|
100
|
+
### The seat glyph
|
|
101
|
+
|
|
102
|
+
**Colour means seat type; shape means status.** A seat's fill is always its
|
|
103
|
+
`SeatType.color`, or the theme's `availableBg` when the type has none — it
|
|
104
|
+
does not change when the seat is held or booked. Overloading fill with both
|
|
105
|
+
type and status hid the type colour on exactly the seats a busy map has most
|
|
106
|
+
of; see the doc comment on `seatFill` for the full argument. `selected` is the
|
|
107
|
+
one deliberate exception: it still owns the seat's colour outright, so your own picks
|
|
108
|
+
are never ambiguous, and a purple fill on its own would in any case fail WCAG
|
|
109
|
+
1.4.1 (Use of Colour) for anyone who cannot distinguish it — hence the shape
|
|
110
|
+
cues below.
|
|
111
|
+
|
|
112
|
+
The seat body itself carries orientation: square-ish at the front, tapered at
|
|
113
|
+
the back, so which way a seat faces is legible from its outline alone.
|
|
114
|
+
`Seat.rotation` turns the whole group, and the taper turns with it — no
|
|
115
|
+
separate frame is drawn on top. This is what makes a fanned lecture theatre or
|
|
116
|
+
a stadium stand read at a glance. The seat number counter-rotates and stays
|
|
117
|
+
upright regardless.
|
|
118
|
+
|
|
119
|
+
Status is read from two marks layered over the body, never from its colour:
|
|
120
|
+
|
|
121
|
+
| State | Fill | Wash | Occupant figure |
|
|
122
|
+
| --------- | ---------------------------------------------- | ---------- | ---------------------------------------------------- |
|
|
123
|
+
| Available | type colour | — | — |
|
|
124
|
+
| Selected | `selectedBg` **frame** + `selectedFg` **core** | — | solid, tinted `selectedBg` |
|
|
125
|
+
| Held | type colour | `heldBg` | **hollow**, stroked `heldFg` — a hold is provisional |
|
|
126
|
+
| Booked | type colour | `bookedBg` | solid, tinted `bookedFg` — settled, someone else's |
|
|
127
|
+
| Blocked | `blockedBg` | — | — (withheld by the venue; no one is there) |
|
|
128
|
+
|
|
129
|
+
Solid vs. hollow is the real distinction — settled vs. still in progress — and
|
|
130
|
+
every figure leans with the seat rather than staying upright, so it doubles as
|
|
131
|
+
an orientation cue too.
|
|
132
|
+
|
|
133
|
+
Selection is the only state drawn from **two** tones rather than one, and that
|
|
134
|
+
is deliberate. `selectedBg` frames the seat; `selectedFg` fills a core inside
|
|
135
|
+
that frame and is what the number and the figure are read against. Because the
|
|
136
|
+
treatment always holds a light tone and a dark one at once, one of the two
|
|
137
|
+
separates from the page whichever way you have themed it — so the library never
|
|
138
|
+
has to detect a colour scheme, and there is no `prefers-color-scheme` default to
|
|
139
|
+
fight with your own. An earlier revision had this inverted, a near-white rim
|
|
140
|
+
around a mid-purple middle, which put the low-contrast tone on the outside
|
|
141
|
+
boundary: a selected seat dissolved into a light page and sank into a dark one.
|
|
142
|
+
|
|
143
|
+
Both tokens therefore carry real visual weight — override them as a pair. Swapping
|
|
144
|
+
the two gives the inverse treatment, a light frame around a dark core, which works
|
|
145
|
+
just as well.
|
|
146
|
+
|
|
147
|
+
A theme that overrides `heldFg`/`bookedFg` owns the contrast of that mark
|
|
148
|
+
against whatever `SeatType.color` the document supplies — the library cannot
|
|
149
|
+
know the pairing in advance, since the figure tint is fixed while the type
|
|
150
|
+
colour underneath it is not. The shipped defaults are light marks over a
|
|
151
|
+
darkened wash, which reads across the tier colours in the demo fixtures.
|
|
152
|
+
|
|
153
|
+
The geometry is exported, if you want to draw a matching seat elsewhere:
|
|
154
|
+
`seatBodyPath`, `seatSelectedFrame`, `seatOccupantPath`, `seatOccupantStroke`.
|
|
155
|
+
(`seatRingStroke` is the old name for `seatSelectedFrame` and still resolves to
|
|
156
|
+
it.) A seat is always square, which makes `seatBodyPath`'s `inset` an exact
|
|
157
|
+
scaled copy about the centre — that is how the selected core is derived, and why
|
|
158
|
+
marks drawn to the core's box keep every clearance they had against the body.
|
|
159
|
+
|
|
160
|
+
### Theming
|
|
161
|
+
|
|
162
|
+
Colors resolve through three tiers, highest first:
|
|
163
|
+
|
|
164
|
+
1. A `--kerusi-*` custom property in **your** stylesheet.
|
|
165
|
+
2. The **`[colors]` input** — a partial `KerusiSeatmapColors`, merged over the defaults.
|
|
166
|
+
3. The library default.
|
|
167
|
+
|
|
168
|
+
The library never writes a `--kerusi-*` property onto its own host, which is
|
|
169
|
+
what keeps that order true: every fill is emitted as
|
|
170
|
+
`var(--kerusi-selected-bg, <the resolved input value>)`. Use whichever tier
|
|
171
|
+
fits — the input for values known at build time, CSS for anything that has to
|
|
172
|
+
respond to a media query or a theme class.
|
|
173
|
+
|
|
174
|
+
```css
|
|
175
|
+
kerusi-seatmap {
|
|
176
|
+
--kerusi-selected-bg: #8b6ad6;
|
|
177
|
+
--kerusi-available-bg: #2f6b45;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@media (prefers-color-scheme: dark) {
|
|
181
|
+
kerusi-seatmap {
|
|
182
|
+
--kerusi-element-bg: #232a36;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Every key of `KerusiSeatmapColors` has a property, kebab-cased:
|
|
188
|
+
|
|
189
|
+
| Input key | Custom property |
|
|
190
|
+
| ------------------------------------- | ----------------------------------------------------------- |
|
|
191
|
+
| `availableBg` / `availableFg` | `--kerusi-available-bg` / `--kerusi-available-fg` |
|
|
192
|
+
| `selectedBg` / `selectedFg` | `--kerusi-selected-bg` / `--kerusi-selected-fg` |
|
|
193
|
+
| `heldBg` / `heldFg` | `--kerusi-held-bg` / `--kerusi-held-fg` |
|
|
194
|
+
| `bookedBg` / `bookedFg` | `--kerusi-booked-bg` / `--kerusi-booked-fg` |
|
|
195
|
+
| `blockedBg` / `blockedFg` | `--kerusi-blocked-bg` / `--kerusi-blocked-fg` |
|
|
196
|
+
| `elementBg` / `elementFg` | `--kerusi-element-bg` / `--kerusi-element-fg` |
|
|
197
|
+
| `elementAccentBg` / `elementAccentFg` | `--kerusi-element-accent-bg` / `--kerusi-element-accent-fg` |
|
|
198
|
+
| `elementMutedBg` / `elementMutedFg` | `--kerusi-element-muted-bg` / `--kerusi-element-muted-fg` |
|
|
199
|
+
| `focusRing` | `--kerusi-focus-ring` |
|
|
200
|
+
| `backdrop` | `--kerusi-backdrop` |
|
|
201
|
+
|
|
202
|
+
A `SeatType.color` from the document is deliberately **not** themable — it is
|
|
203
|
+
the map's own value under §4.7. Turn it off wholesale with
|
|
204
|
+
`[typeColors]="false"` if you want the theme to own every fill.
|
|
205
|
+
|
|
206
|
+
Overriding `selectedBg` does not cost you the selected cue: the core and the
|
|
207
|
+
figure are shape, not color. Do set `selectedFg` alongside it, though — it is the
|
|
208
|
+
core's fill, so it is most of a selected seat's area rather than just a label
|
|
209
|
+
tint, and the two are read against each other. `heldBg`/`bookedBg` are the wash
|
|
210
|
+
drawn over a taken seat's type colour, not the seat's own fill — see
|
|
211
|
+
[The seat glyph](#the-seat-glyph) for the full state table.
|
|
212
|
+
|
|
213
|
+
If you need the pre-1.1 flat selected seat back, the core is its own node:
|
|
214
|
+
|
|
215
|
+
```css
|
|
216
|
+
kerusi-seatmap .kerusi-seat__core {
|
|
217
|
+
display: none;
|
|
218
|
+
}
|
|
219
|
+
/* Without the core, these would be selectedBg on selectedBg. */
|
|
220
|
+
kerusi-seatmap .kerusi-seat--selected .kerusi-seat__label,
|
|
221
|
+
kerusi-seatmap .kerusi-seat--selected .kerusi-seat__occupant,
|
|
222
|
+
kerusi-seatmap .kerusi-seat--selected .kerusi-seat__wheelchair {
|
|
223
|
+
fill: var(--kerusi-selected-fg, #f3ecff);
|
|
224
|
+
stroke: none;
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
For anything the palette does not cover, each seat group carries class hooks:
|
|
229
|
+
|
|
230
|
+
| Class | |
|
|
231
|
+
| --------------------------------------------------------------- | ------------------------------------------ |
|
|
232
|
+
| `.kerusi-seat` | Every seat group. |
|
|
233
|
+
| `.kerusi-seat--available` / `--held` / `--booked` / `--blocked` | Its `SeatStatus`. |
|
|
234
|
+
| `.kerusi-seat--selected` | Currently picked. |
|
|
235
|
+
| `.kerusi-seat--unselectable` | Fails the status test or `seatSelectable`. |
|
|
236
|
+
| `.kerusi-seat--wheelchair` | `accessibility.wheelchairAccessible`. |
|
|
237
|
+
| `.kerusi-seat--companion` | Has companions (§4.6). |
|
|
238
|
+
|
|
239
|
+
Inside a seat: `.kerusi-seat__box`, `__wash`, `__core`, `__occupant`
|
|
240
|
+
(`__occupant--selected` / `--held` / `--booked`), `__label`, `__wheelchair`.
|
|
241
|
+
|
|
242
|
+
### Keyboard
|
|
243
|
+
|
|
244
|
+
Tab moves between sections; each section keeps its own tab stop. Within a
|
|
245
|
+
section, arrow keys follow the §4.3.1 `col` order — so an aisle, which is a
|
|
246
|
+
column no seat occupies, is stepped across rather than into.
|
|
247
|
+
|
|
248
|
+
| Key | |
|
|
249
|
+
| ------------------------ | ---------------------------------- |
|
|
250
|
+
| `←` `→` | Previous / next seat in the row |
|
|
251
|
+
| `↑` `↓` | Nearest column in the adjacent row |
|
|
252
|
+
| `Home` / `End` | First / last seat of the row |
|
|
253
|
+
| `Ctrl+Home` / `Ctrl+End` | First / last seat of the section |
|
|
254
|
+
| `PageUp` / `PageDown` | First / last seat of the section |
|
|
255
|
+
| `Enter` / `Space` | Toggle |
|
|
256
|
+
| `Escape` | Clear the selection |
|
|
257
|
+
|
|
258
|
+
Each seat is a `role="button"` announcing its position, type, price, status,
|
|
259
|
+
every `Seat.accessibility` property and its attributes. A polite live region
|
|
260
|
+
reports the running selection and any disallowed reason.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## `<kerusi-legend>`
|
|
265
|
+
|
|
266
|
+
Rendered inline by `[showLegend]="true"`, or placed anywhere yourself:
|
|
267
|
+
|
|
268
|
+
```html
|
|
269
|
+
<kerusi-legend [legend]="model.legend" [locale]="'ms'" [showPrices]="true" />
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
It resolves swatches through the same path as the seat fills, so the two cannot
|
|
273
|
+
drift apart. Every availability swatch — not just **Selected** — draws the
|
|
274
|
+
seat glyph itself (wash, ring, occupant, as it applies) rather than a flat
|
|
275
|
+
colour, because shape is the cue the map uses for status.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Working with the format directly
|
|
280
|
+
|
|
281
|
+
All of this lives in `@kerusiweb/core` and is pure — no Angular import — so it
|
|
282
|
+
can run in a test, a build step or on a server.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import {
|
|
286
|
+
buildRenderModel, // KerusiMap + KerusiState -> the resolved render model
|
|
287
|
+
checkKerusiMap, // every violation, no throw
|
|
288
|
+
validateKerusiMap, // throws on the first error
|
|
289
|
+
validateDocumentSet, // map / session / state joins
|
|
290
|
+
resolveSeatPrice, // the §4.9 precedence order
|
|
291
|
+
resolveLocalizedText, // string | Record<string, string> -> string
|
|
292
|
+
formatMoney, // minor units -> "RM 45.00", "¥1,200", "KD 12.500"
|
|
293
|
+
summarizeSelection, // seats, total, unpriced count
|
|
294
|
+
computeSectionLayout, // placed geometry for one section
|
|
295
|
+
buildNavigationGraph, // per-seat keyboard neighbours
|
|
296
|
+
} from '@kerusiweb/core';
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Validation
|
|
300
|
+
|
|
301
|
+
`checkKerusiMap` returns every violation in document order without throwing;
|
|
302
|
+
`validateKerusiMap` throws a `KerusiValidationError` carrying the first error
|
|
303
|
+
and the full list. Each violation has a stable `rule` slug, a `severity`
|
|
304
|
+
(`error` blocks conformance, `warning` is advisory) and a document `path`.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
for (const v of checkKerusiMap(map)) {
|
|
308
|
+
console.warn(`${v.severity} ${v.rule} at ${v.path}: ${v.message}`);
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Live availability
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import { KerusiStateStore } from '@kerusiweb/angular';
|
|
316
|
+
|
|
317
|
+
const store = new KerusiStateStore(initialState);
|
|
318
|
+
socket.onmessage = (e) => {
|
|
319
|
+
const result = store.apply(JSON.parse(e.data));
|
|
320
|
+
if (result.outcome === 'gap') refetch(); // store.needsRefetch() is now true
|
|
321
|
+
};
|
|
322
|
+
const stop = store.startExpiryTicker();
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Deltas that are stale, duplicate or scoped to another session are discarded.
|
|
326
|
+
|
|
327
|
+
> **Gap detection needs a sequence.** §5.2 requires `updatedAt` to be strictly
|
|
328
|
+
> increasing but not contiguous, so it cannot by itself distinguish "a delta was
|
|
329
|
+
> lost" from "nothing happened for a while". The store detects gaps when the
|
|
330
|
+
> transport supplies a monotonic sequence — `metadata.seq` by default, or your
|
|
331
|
+
> own `sequenceOf` reader. A gapped delta is still applied, so the map degrades
|
|
332
|
+
> rather than freezes while you re-fetch.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
MIT © Arsyad "Shad Ahm" Ahmad
|