@pond-ts/react 0.11.7 → 0.12.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.
Files changed (2) hide show
  1. package/CHANGELOG.md +187 -1
  2. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -7,10 +7,196 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
7
7
  file covers both packages. Pre-1.0: minor bumps may include new features and
8
8
  type-level changes; patch bumps are strictly additive.
9
9
 
10
- [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.11.7...HEAD
10
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.12.0...HEAD
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.12.0] — 2026-05-01
15
+
16
+ The "triggers" release. Major redesign of how live accumulators
17
+ control emission cadence — `Trigger` is now a first-class concept
18
+ shaped by two converging real-world use cases (synchronised
19
+ partitioned tick aggregation in the gRPC pipeline experiment,
20
+ sequence-sampled rolling in webapp telemetry).
21
+
22
+ Two correctness audits before publish: a Layer 2 Claude review
23
+ (column collision, dispose, late-spawn, peer-dep) and a Codex
24
+ adversarial review (quiet-partition stale samples, pre-existing
25
+ data replay at construction, spawn-listener cleanup). All findings
26
+ fixed and pinned with regression tests. 1039 / 1039 tests pass.
27
+
28
+ ### Added
29
+
30
+ - **Trigger as a first-class concept.** A new `Trigger` factory
31
+ exposed at the package root lets `LiveRollingAggregation` switch
32
+ emission cadence without changing any other shape:
33
+
34
+ ```ts
35
+ import { LiveSeries, Sequence, Trigger } from 'pond-ts';
36
+
37
+ // Webapp telemetry: rolling 1m p95, emit on every 30 s of event-time
38
+ const rolling = timings.rolling(
39
+ '1m',
40
+ { latency: 'p95' },
41
+ { trigger: Trigger.clock(Sequence.every('30s')) },
42
+ );
43
+
44
+ rolling.on('event', (e) =>
45
+ fetch('/api/telemetry', { method: 'POST', body: JSON.stringify(e.data()) }),
46
+ );
47
+ rolling.value(); // current rolling-window snapshot, independent of trigger
48
+ ```
49
+
50
+ Two trigger variants in this release:
51
+ - **`Trigger.event()`** — per-event emission. Default; the historical
52
+ behavior of `LiveRollingAggregation` when no trigger is specified.
53
+ - **`Trigger.clock(sequence)`** — sequence-triggered emission. One
54
+ snapshot fires when a source event crosses an epoch-aligned
55
+ boundary of the (fixed-step) `Sequence`. Output keyed at boundary
56
+ instants. Calendar sequences are rejected upfront.
57
+
58
+ Future variants (`Trigger.count(n)`, custom predicates, compound
59
+ triggers) are reserved but not yet shipped.
60
+
61
+ - **Synchronised partitioned rolling.** `LivePartitionedSeries.rolling`
62
+ now accepts a clock trigger. The output is a
63
+ `LiveSource<RowSchema>` whose schema is `[time, <partitionColumn>,
64
+ ...mappingColumns]`; on every boundary crossing, one event fires
65
+ per known partition, all sharing the same boundary timestamp.
66
+ Synchronised across partitions by construction (the bucket index is
67
+ shared, not per-partition).
68
+
69
+ ```ts
70
+ // Dashboard tick aggregation: 100 hosts, 200ms cadence
71
+ const ticks = live
72
+ .partitionBy('host')
73
+ .rolling(
74
+ '1m',
75
+ { cpu: 'avg' },
76
+ { trigger: Trigger.clock(Sequence.every('200ms')) },
77
+ );
78
+
79
+ ticks.on('event', (e) => {
80
+ // e.begin() === <boundary timestamp>, same for every host this tick
81
+ // e.get('host') === 'api-1' | 'api-2' | …
82
+ // e.get('cpu') === <rolling avg for that host>
83
+ });
84
+ ```
85
+
86
+ Restricted to direct-after-`partitionBy` in this release: chained
87
+ sugar (`partitionBy(c).fill(...).rolling(...)`) rejects clock
88
+ triggers with a clear error. Lifts in a future release once a real
89
+ use case appears.
90
+
91
+ Closes the gRPC experiment's M3.5 dashboard friction note (the
92
+ hand-rolled `HostAggregator` becomes ~10 lines of pond code).
93
+
94
+ ### Removed (breaking — pre-1.0)
95
+
96
+ - **`LiveSequenceRollingAggregation`** class deleted. Its capability
97
+ is preserved as `LiveRollingAggregation` with
98
+ `{ trigger: Trigger.clock(sequence) }`. Migration: replace
99
+ `live.rolling('1m', m).sample(seq)` with
100
+ `live.rolling('1m', m, { trigger: Trigger.clock(seq) })`. Single
101
+ rolling object now serves both backend reporting and direct
102
+ `.value()` reads (no separate sampler reference).
103
+ - **`.sample(sequence)`** method removed from `LiveRollingAggregation`.
104
+ Use the trigger option above.
105
+
106
+ ### Changed
107
+
108
+ - **`LiveRollingOptions`** gains an optional `trigger?: Trigger`
109
+ field. Default behavior (no `trigger` specified) is unchanged from
110
+ v0.11.x — per-event emission. Backward compatible for everyone
111
+ who didn't use `.sample()`.
112
+
113
+ ### Performance
114
+
115
+ - New benchmark `scripts/perf-triggers.mjs` covers both
116
+ non-partitioned and synchronised partitioned cases. Headline numbers
117
+ on a current MacBook Pro:
118
+ - Non-partitioned: clock(30s) ~50% faster than per-event baseline
119
+ (emission is rarer); clock(1s) similar.
120
+ - Synchronised partitioned (100 hosts, 30k events at realistic
121
+ rates): ~300 ns/emission at 200ms cadence; +205% over per-
122
+ partition baseline at the high end. Well within budget for the
123
+ motivating dashboard use case.
124
+
125
+ ### Notes
126
+
127
+ - **`docs/rfcs/triggers.md`** captures the full design rationale,
128
+ the four sign-off questions, and the migration plan. Read this if
129
+ you want the "why this shape" context.
130
+
131
+ ### Known limitations
132
+
133
+ - **Synchronised partitioned rolling output type is loose** —
134
+ `LiveSource<SeriesSchema>` rather than a schema-narrowed shape.
135
+ Runtime schema is correct; only static types widen. Tightening is
136
+ queued for a follow-up release.
137
+ - **Synchronised partitioned rolling rejects column-name collisions**
138
+ between the partition column and any reducer-output column at
139
+ construction (e.g. `partitionBy('cpu').rolling('1m', { cpu: 'avg' }, { trigger })`).
140
+ Rename the reducer output (once `AggregateOutputMap` lands on live
141
+ rolling) or partition by a different column.
142
+ - **Late-spawn partitions only appear in ticks after their first event
143
+ arrives.** A partition unknown to the sync source contributes no
144
+ row to the current tick. Use `partitionBy(col, { groups: [...] })`
145
+ to eagerly include partitions from construction.
146
+
147
+ ## [0.11.8] — 2026-04-30
148
+
149
+ ### Added
150
+
151
+ - **`rolling.sample(sequence)`** on `LiveRollingAggregation` — taps a
152
+ rolling aggregation and emits one snapshot of the rolling state each
153
+ time a source event crosses an epoch-aligned boundary of `sequence`.
154
+ Closes the frontend-telemetry gap: collect high-frequency timing
155
+ events, sample p95 latency to a backend every 30 s, while the same
156
+ rolling drives an in-app live display (no duplicated deque).
157
+
158
+ ```ts
159
+ const rolling = timings.rolling('1m', { latency: 'p95' });
160
+
161
+ // One sampler → backend report every 30 s of event time
162
+ const reported = rolling.sample(Sequence.every('30s'));
163
+ reported.on('event', (e) =>
164
+ fetch('/api/telemetry', { method: 'POST', body: JSON.stringify(e.data()) }),
165
+ );
166
+
167
+ // Same rolling drives the UI live display
168
+ useLiveQuery(timings, () => rolling.value());
169
+ ```
170
+
171
+ `sequence` must be a fixed-step `Sequence`; calendar sequences
172
+ (`Sequence.daily()` etc.) are rejected upfront — boundary indexing
173
+ needs a constant step.
174
+
175
+ Emission is **data-driven**: no `setInterval`. If the source goes
176
+ quiet, no events fire. A single source event spanning multiple
177
+ boundaries fires exactly one event at the new bucket. Snapshot is
178
+ taken after the boundary-crossing event is ingested by the rolling,
179
+ so the emitted value includes that event's contribution.
180
+
181
+ **Independent lifetimes.** `sample.dispose()` only detaches the
182
+ sampler from the rolling; the rolling's lifecycle stays the user's
183
+ responsibility. One rolling can power multiple `.sample()` cadences
184
+ plus direct `rolling.value()` reads without coupling.
185
+
186
+ - **`LiveSequenceRollingAggregation` exported** from package root with
187
+ full `LiveSource<Out>` surface and the same view-transform set as
188
+ `LiveRollingAggregation` (`filter`, `map`, `select`, `window`,
189
+ `diff`, `rate`, `pctChange`, `fill`, `cumulative`, `rolling`,
190
+ `aggregate`).
191
+
192
+ - **Telemetry-reporting recipe** at
193
+ `website/docs/recipes/telemetry-reporting.mdx` — end-to-end
194
+ frontend-collection → backend-summary pattern using `.sample()`,
195
+ plus the React in-app display via `useLiveQuery`.
196
+
197
+ [0.12.0]: https://github.com/pjm17971/pond-ts/compare/v0.11.8...v0.12.0
198
+ [0.11.8]: https://github.com/pjm17971/pond-ts/compare/v0.11.7...v0.11.8
199
+
14
200
  ## [0.11.7] — 2026-04-29
15
201
 
16
202
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/react",
3
- "version": "0.11.7",
3
+ "version": "0.12.0",
4
4
  "description": "React hooks for pond-ts live time series",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "test:runtime": "vitest run"
32
32
  },
33
33
  "peerDependencies": {
34
- "pond-ts": "^0.11.0",
34
+ "pond-ts": "^0.12.0",
35
35
  "react": "^18.0.0 || ^19.0.0"
36
36
  },
37
37
  "devDependencies": {