@pond-ts/react 0.11.8 → 0.12.1

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 +184 -1
  2. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -7,10 +7,192 @@ 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.8...HEAD
10
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.12.1...HEAD
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.12.1] — 2026-05-01
15
+
16
+ Strictly additive over v0.12.0. Closes the chained-view restriction
17
+ on synchronised partitioned rolling. The trigger option now applies
18
+ consistently across the entire `rolling()` surface — chained sugar
19
+ methods on the partitioned surface (`fill`, `diff`, `rate`,
20
+ `pctChange`, `cumulative`) no longer break it.
21
+
22
+ ### Changed
23
+
24
+ - **`partitionBy(col).<chained>().rolling(window, m, { trigger: Trigger.clock(seq) })` now works.**
25
+ Previously this threw a clear-but-restrictive error. The chain
26
+ factory runs per partition; the sync rolling subscribes to each
27
+ chain output instead of the raw partition events. Output schema is
28
+ unchanged (`[time, <partitionColumn>, ...mappingColumns]`); the
29
+ partition tag is set from the routing key, so chains that drop the
30
+ partition column still emit correctly.
31
+
32
+ Motivating example — per-host gap-filling before synchronised
33
+ ticks:
34
+
35
+ ```ts
36
+ const ticks = live
37
+ .partitionBy('host')
38
+ .fill({ cpu: 'hold' })
39
+ .rolling(
40
+ '1m',
41
+ { cpu: 'avg' },
42
+ { trigger: Trigger.clock(Sequence.every('200ms')) },
43
+ );
44
+ ```
45
+
46
+ Coherence-of-feature fix: the trigger concept now applies wherever
47
+ `rolling()` appears in the partitioned chain, not just in the
48
+ one-step case. Captured in the RFC's post-implementation notes
49
+ alongside the deferred-and-now-shipped section.
50
+
51
+ ### Tests
52
+
53
+ - 4 new tests in `test/Triggers.test.ts` covering chained-view sync
54
+ rolling: `fill().rolling(.., trigger)`, output schema, cross-
55
+ partition synchronisation through the chain, dispose semantics
56
+ through the chain, and replay-on-construction with the chain factory.
57
+ - 1 test removed (the throw-on-chained-view assertion that no longer
58
+ applies).
59
+ - Test count: 34 (was 30). Total core tests: 1043 (was 1039).
60
+
61
+ [0.12.1]: https://github.com/pjm17971/pond-ts/compare/v0.12.0...v0.12.1
62
+
63
+ ## [0.12.0] — 2026-05-01
64
+
65
+ The "triggers" release. Major redesign of how live accumulators
66
+ control emission cadence — `Trigger` is now a first-class concept
67
+ shaped by two converging real-world use cases (synchronised
68
+ partitioned tick aggregation in the gRPC pipeline experiment,
69
+ sequence-sampled rolling in webapp telemetry).
70
+
71
+ Two correctness audits before publish: a Layer 2 Claude review
72
+ (column collision, dispose, late-spawn, peer-dep) and a Codex
73
+ adversarial review (quiet-partition stale samples, pre-existing
74
+ data replay at construction, spawn-listener cleanup). All findings
75
+ fixed and pinned with regression tests. 1039 / 1039 tests pass.
76
+
77
+ ### Added
78
+
79
+ - **Trigger as a first-class concept.** A new `Trigger` factory
80
+ exposed at the package root lets `LiveRollingAggregation` switch
81
+ emission cadence without changing any other shape:
82
+
83
+ ```ts
84
+ import { LiveSeries, Sequence, Trigger } from 'pond-ts';
85
+
86
+ // Webapp telemetry: rolling 1m p95, emit on every 30 s of event-time
87
+ const rolling = timings.rolling(
88
+ '1m',
89
+ { latency: 'p95' },
90
+ { trigger: Trigger.clock(Sequence.every('30s')) },
91
+ );
92
+
93
+ rolling.on('event', (e) =>
94
+ fetch('/api/telemetry', { method: 'POST', body: JSON.stringify(e.data()) }),
95
+ );
96
+ rolling.value(); // current rolling-window snapshot, independent of trigger
97
+ ```
98
+
99
+ Two trigger variants in this release:
100
+ - **`Trigger.event()`** — per-event emission. Default; the historical
101
+ behavior of `LiveRollingAggregation` when no trigger is specified.
102
+ - **`Trigger.clock(sequence)`** — sequence-triggered emission. One
103
+ snapshot fires when a source event crosses an epoch-aligned
104
+ boundary of the (fixed-step) `Sequence`. Output keyed at boundary
105
+ instants. Calendar sequences are rejected upfront.
106
+
107
+ Future variants (`Trigger.count(n)`, custom predicates, compound
108
+ triggers) are reserved but not yet shipped.
109
+
110
+ - **Synchronised partitioned rolling.** `LivePartitionedSeries.rolling`
111
+ now accepts a clock trigger. The output is a
112
+ `LiveSource<RowSchema>` whose schema is `[time, <partitionColumn>,
113
+ ...mappingColumns]`; on every boundary crossing, one event fires
114
+ per known partition, all sharing the same boundary timestamp.
115
+ Synchronised across partitions by construction (the bucket index is
116
+ shared, not per-partition).
117
+
118
+ ```ts
119
+ // Dashboard tick aggregation: 100 hosts, 200ms cadence
120
+ const ticks = live
121
+ .partitionBy('host')
122
+ .rolling(
123
+ '1m',
124
+ { cpu: 'avg' },
125
+ { trigger: Trigger.clock(Sequence.every('200ms')) },
126
+ );
127
+
128
+ ticks.on('event', (e) => {
129
+ // e.begin() === <boundary timestamp>, same for every host this tick
130
+ // e.get('host') === 'api-1' | 'api-2' | …
131
+ // e.get('cpu') === <rolling avg for that host>
132
+ });
133
+ ```
134
+
135
+ Restricted to direct-after-`partitionBy` in this release: chained
136
+ sugar (`partitionBy(c).fill(...).rolling(...)`) rejects clock
137
+ triggers with a clear error. Lifts in a future release once a real
138
+ use case appears.
139
+
140
+ Closes the gRPC experiment's M3.5 dashboard friction note (the
141
+ hand-rolled `HostAggregator` becomes ~10 lines of pond code).
142
+
143
+ ### Removed (breaking — pre-1.0)
144
+
145
+ - **`LiveSequenceRollingAggregation`** class deleted. Its capability
146
+ is preserved as `LiveRollingAggregation` with
147
+ `{ trigger: Trigger.clock(sequence) }`. Migration: replace
148
+ `live.rolling('1m', m).sample(seq)` with
149
+ `live.rolling('1m', m, { trigger: Trigger.clock(seq) })`. Single
150
+ rolling object now serves both backend reporting and direct
151
+ `.value()` reads (no separate sampler reference).
152
+ - **`.sample(sequence)`** method removed from `LiveRollingAggregation`.
153
+ Use the trigger option above.
154
+
155
+ ### Changed
156
+
157
+ - **`LiveRollingOptions`** gains an optional `trigger?: Trigger`
158
+ field. Default behavior (no `trigger` specified) is unchanged from
159
+ v0.11.x — per-event emission. Backward compatible for everyone
160
+ who didn't use `.sample()`.
161
+
162
+ ### Performance
163
+
164
+ - New benchmark `scripts/perf-triggers.mjs` covers both
165
+ non-partitioned and synchronised partitioned cases. Headline numbers
166
+ on a current MacBook Pro:
167
+ - Non-partitioned: clock(30s) ~50% faster than per-event baseline
168
+ (emission is rarer); clock(1s) similar.
169
+ - Synchronised partitioned (100 hosts, 30k events at realistic
170
+ rates): ~300 ns/emission at 200ms cadence; +205% over per-
171
+ partition baseline at the high end. Well within budget for the
172
+ motivating dashboard use case.
173
+
174
+ ### Notes
175
+
176
+ - **`docs/rfcs/triggers.md`** captures the full design rationale,
177
+ the four sign-off questions, and the migration plan. Read this if
178
+ you want the "why this shape" context.
179
+
180
+ ### Known limitations
181
+
182
+ - **Synchronised partitioned rolling output type is loose** —
183
+ `LiveSource<SeriesSchema>` rather than a schema-narrowed shape.
184
+ Runtime schema is correct; only static types widen. Tightening is
185
+ queued for a follow-up release.
186
+ - **Synchronised partitioned rolling rejects column-name collisions**
187
+ between the partition column and any reducer-output column at
188
+ construction (e.g. `partitionBy('cpu').rolling('1m', { cpu: 'avg' }, { trigger })`).
189
+ Rename the reducer output (once `AggregateOutputMap` lands on live
190
+ rolling) or partition by a different column.
191
+ - **Late-spawn partitions only appear in ticks after their first event
192
+ arrives.** A partition unknown to the sync source contributes no
193
+ row to the current tick. Use `partitionBy(col, { groups: [...] })`
194
+ to eagerly include partitions from construction.
195
+
14
196
  ## [0.11.8] — 2026-04-30
15
197
 
16
198
  ### Added
@@ -61,6 +243,7 @@ type-level changes; patch bumps are strictly additive.
61
243
  frontend-collection → backend-summary pattern using `.sample()`,
62
244
  plus the React in-app display via `useLiveQuery`.
63
245
 
246
+ [0.12.0]: https://github.com/pjm17971/pond-ts/compare/v0.11.8...v0.12.0
64
247
  [0.11.8]: https://github.com/pjm17971/pond-ts/compare/v0.11.7...v0.11.8
65
248
 
66
249
  ## [0.11.7] — 2026-04-29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/react",
3
- "version": "0.11.8",
3
+ "version": "0.12.1",
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": {