Skip to content

Commit c4e8f87

Browse files
authored
Normalize ConnectionThresholds field names + remove disableAutoplay (#33)
* Remove disableAutoplay rendering hint Redundant with deferHeavyComponents — both triggered on identical conditions (isLowEnd || isSlowConnection || isBatteryConstrained). * Normalize ConnectionThresholds field names Rename downlink2gUpperBound/downlink3gUpperBound/downlink4gUpperBound to lowUpperBound/midUpperBound/highUpperBound, matching the pattern used by CpuThresholds and MemoryThresholds.
1 parent f28f1f8 commit c4e8f87

16 files changed

Lines changed: 49 additions & 95 deletions

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
### Breaking Changes
66

7+
- **Normalize `ConnectionThresholds` field names** — Renamed `downlink2gUpperBound``lowUpperBound`, `downlink3gUpperBound``midUpperBound`, `downlink4gUpperBound``highUpperBound` to match the `CpuThresholds`/`MemoryThresholds` naming pattern. Update any custom `connection` threshold objects in your `thresholds` config
78
- **Rename `ConnectionTier` value `'fast'``'high'`** — Aligns connection tier vocabulary with CPU, memory, and GPU tiers which all use `'low' | 'mid' | 'high'`. Update any code comparing `tiers.connection === 'fast'` to use `'high'` instead
89
- **`classify()` and `deriveHints()` now accept `StoredSignals` instead of `RawSignals`** — These functions never used `userAgent` or `viewport` (which are stripped before storage). The narrower type makes the API honest. Existing call sites are unaffected — `RawSignals` is structurally assignable to `StoredSignals`
910
- **`profile:store` event now carries `StoredSignals` instead of `RawSignals`** — The event previously emitted the raw probe payload (including `userAgent`/`viewport`), not what was actually stored. The `signals` field now matches the persisted data. `bot:reject` still carries `RawSignals` (it fires before stripping)
1011
- **Rename default cookie `dr_session``device-router-session`** — Self-documenting name before 1.0 locks the cookie in. If you hardcode `cookieName: 'dr_session'` in your options you are unaffected; if you rely on the default, existing sessions will reset once on deploy
12+
- **Remove `disableAutoplay` rendering hint**`disableAutoplay` triggered on identical conditions to `deferHeavyComponents` (`isLowEnd || isSlowConnection || isBatteryConstrained`). Use `deferHeavyComponents` instead
1113
- **middleware-fastify: normalized return shape**`createDeviceRouter()` now returns raw Fastify hooks instead of a `fastify-plugin` wrapped plugin. Migrate `await app.register(middleware)``app.addHook('preHandler', middleware)`. When using `injectProbe: true`, register the injection hook separately: `app.addHook('onSend', injectionMiddleware)`. Removed `fastify-plugin` dependency
1214

1315
### Features
@@ -60,7 +62,7 @@
6062
### Features
6163

6264
- **GPU detection** — Classify GPU tier from WebGL renderer string: software renderers → `low`, RTX/RX 5000+/Apple M-series → `high`, everything else → `mid`
63-
- **Battery API signal** — Collect battery level and charging status via `navigator.getBattery()` (Chromium-only, silently skipped elsewhere). When unplugged and below 15%, `deferHeavyComponents`, `reduceAnimations`, and `disableAutoplay` are forced on
65+
- **Battery API signal** — Collect battery level and charging status via `navigator.getBattery()` (Chromium-only, silently skipped elsewhere). When unplugged and below 15%, `deferHeavyComponents` and `reduceAnimations` are forced on
6466
- **Signal validation** — New `isValidSignals()` type guard for validating incoming probe payloads
6567
- **Custom GPU thresholds**`softwarePattern` and `highEndPattern` are configurable via `GpuThresholds`
6668

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ A **~1 KB** client probe. One middleware call. Full device awareness on every re
1818
Responsive design adapts layout. DeviceRouter adapts **what you serve**.
1919

2020
- A budget phone on 2G? Skip the heavy animations, defer non-critical components, prefer server-side rendering.
21-
- A flagship on fiber? Go all out — autoplay, full interactivity, rich visuals.
21+
- A flagship on fiber? Go all out — full interactivity, rich visuals.
2222

2323
No user-agent sniffing. No guesswork. Real signals from real devices, classified into actionable tiers and rendering hints your server can act on immediately.
2424

@@ -120,7 +120,6 @@ Based on tiers and user preferences, DeviceRouter derives actionable booleans:
120120
| `serveMinimalCSS` | Low-end device |
121121
| `reduceAnimations` | Low-end device, prefers reduced motion, or low battery |
122122
| `useImagePlaceholders` | Slow connection (2G/3G) |
123-
| `disableAutoplay` | Low-end device, slow connection, or low battery |
124123
| `preferServerRendering` | Low-end device |
125124
| `disable3dEffects` | No GPU or software renderer |
126125

@@ -245,8 +244,8 @@ new RedisStorageAdapter(redisClient, { prefix: 'dr:profile:' });
245244

246245
Each framework has an example app that renders a product landing page adapting in real time to device capabilities:
247246

248-
- **Full experience** (high-end device) — animated gradient hero, SVG icons, inline charts, pulsing CTA, hover transitions, autoplay visualizer
249-
- **Lite experience** (low-end device) — flat solid backgrounds, Unicode icons, placeholder boxes, no animations, autoplay disabled
247+
- **Full experience** (high-end device) — animated gradient hero, SVG icons, inline charts, pulsing CTA, hover transitions
248+
- **Lite experience** (low-end device) — flat solid backgrounds, Unicode icons, placeholder boxes, no animations
250249

251250
Run any example to see it in action:
252251

docs/api/types.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const tiers = classify({
2121

2222
Derives rendering hints from device tiers.
2323

24-
The `battery` signal bypasses tier classification entirely — it's transient state, not a capability. When the device is unplugged and below 15% charge, `deferHeavyComponents`, `reduceAnimations`, and `disableAutoplay` are forced on to conserve power.
24+
The `battery` signal bypasses tier classification entirely — it's transient state, not a capability. When the device is unplugged and below 15% charge, `deferHeavyComponents` and `reduceAnimations` are forced on to conserve power.
2525

2626
```typescript
2727
import { classify, deriveHints } from '@device-router/types';
@@ -54,7 +54,7 @@ Validates custom tier thresholds after merging with defaults. Throws a descripti
5454
**Validation rules:**
5555

5656
- **CPU/Memory**: `lowUpperBound` must be less than `midUpperBound`
57-
- **Connection**: `downlink2gUpperBound` < `downlink3gUpperBound` < `downlink4gUpperBound`
57+
- **Connection**: `lowUpperBound` < `midUpperBound` < `highUpperBound`
5858
- **GPU**: `softwarePattern` and `highEndPattern` must be `RegExp` instances
5959
- All numeric values must be positive (> 0)
6060

@@ -156,11 +156,11 @@ Custom thresholds for tier classification. All fields are optional — unset fie
156156

157157
### ConnectionThresholds
158158

159-
| Field | Type | Default | Description |
160-
| ---------------------- | -------- | ------- | ------------------------------ |
161-
| `downlink2gUpperBound` | `number` | `0.5` | Mbps at or below → `'2g'` tier |
162-
| `downlink3gUpperBound` | `number` | `2` | Mbps at or below → `'3g'` tier |
163-
| `downlink4gUpperBound` | `number` | `5` | Mbps at or below → `'4g'` tier |
159+
| Field | Type | Default | Description |
160+
| ---------------- | -------- | ------- | ------------------------------ |
161+
| `lowUpperBound` | `number` | `0.5` | Mbps at or below → `'2g'` tier |
162+
| `midUpperBound` | `number` | `2` | Mbps at or below → `'3g'` tier |
163+
| `highUpperBound` | `number` | `5` | Mbps at or below → `'4g'` tier |
164164

165165
### GpuThresholds
166166

@@ -177,7 +177,7 @@ import { classify } from '@device-router/types';
177177
const tiers = classify(signals, {
178178
cpu: { lowUpperBound: 4, midUpperBound: 8 },
179179
memory: { midUpperBound: 8 },
180-
connection: { downlink4gUpperBound: 10 },
180+
connection: { highUpperBound: 10 },
181181
gpu: { highEndPattern: /\bRTX\b|\bGTX\b/i },
182182
});
183183
```

docs/getting-started.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ const { middleware, probeEndpoint } = createDeviceRouter({
252252
thresholds: {
253253
cpu: { lowUpperBound: 4, midUpperBound: 8 },
254254
memory: { midUpperBound: 8 },
255-
connection: { downlink4gUpperBound: 10 },
255+
connection: { highUpperBound: 10 },
256256
},
257257
});
258258
```
@@ -367,6 +367,5 @@ Based on device tiers, the middleware provides boolean rendering hints:
367367
- `serveMinimalCSS` — Send reduced stylesheets
368368
- `reduceAnimations` — Disable or simplify animations
369369
- `useImagePlaceholders` — Show placeholders instead of full images
370-
- `disableAutoplay` — Prevent auto-playing media
371370
- `preferServerRendering` — Favor SSR over client-side rendering
372371
- `disable3dEffects` — Disable WebGL/3D content (no GPU or software renderer)

docs/profile-schema.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,5 @@ The full classified result attached to requests by the middleware.
7474
| `serveMinimalCSS` | `boolean` | Low-end device |
7575
| `reduceAnimations` | `boolean` | Low-end device, prefers reduced motion, or low battery |
7676
| `useImagePlaceholders` | `boolean` | Slow connection (2g/3g) |
77-
| `disableAutoplay` | `boolean` | Low-end device, slow connection, or low battery |
7877
| `preferServerRendering` | `boolean` | Low-end device |
7978
| `disable3dEffects` | `boolean` | No GPU or software renderer |

examples/shared/demo-template.ts

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export function renderDemoPage({
1818
serveMinimalCSS: true,
1919
reduceAnimations: true,
2020
useImagePlaceholders: true,
21-
disableAutoplay: true,
2221
preferServerRendering: true,
2322
disable3dEffects: true,
2423
}
@@ -28,18 +27,16 @@ export function renderDemoPage({
2827
serveMinimalCSS: false,
2928
reduceAnimations: false,
3029
useImagePlaceholders: false,
31-
disableAutoplay: false,
3230
preferServerRendering: false,
3331
disable3dEffects: false,
3432
}
3533
: profile?.hints;
3634

37-
// Five booleans derived from hints drive all rendering decisions
35+
// Four booleans derived from hints drive all rendering decisions
3836
const full = !hints?.deferHeavyComponents;
3937
const animate = !hints?.reduceAnimations;
4038
const richCSS = !hints?.serveMinimalCSS;
4139
const showImages = !hints?.useImagePlaceholders;
42-
const autoplay = !hints?.disableAutoplay;
4340

4441
const activeMode = forceParam === 'full' ? 'full' : forceParam === 'lite' ? 'lite' : 'auto';
4542

@@ -97,10 +94,6 @@ export function renderDemoPage({
9794
.gallery-tile{background:#fff;border-radius:12px;padding:1.5rem;text-align:center;border:1px solid #e2e8f0}
9895
.gallery-tile h4{margin-top:0.75rem;font-size:0.8125rem;color:#64748b;font-weight:500}
9996
.placeholder{background:#e2e8f0;border-radius:8px;height:120px;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-size:0.8rem}
100-
.autoplay-section{text-align:center;padding:1.5rem;max-width:960px;margin:0 auto}
101-
.autoplay-bars{display:flex;gap:4px;justify-content:center;align-items:flex-end;height:40px;margin-bottom:0.75rem}
102-
.autoplay-bars span{display:block;width:6px;background:#4f46e5;border-radius:3px}
103-
.autoplay-label{font-size:0.8125rem;color:#64748b}
10497
.profile-panel{max-width:960px;margin:1.5rem auto;padding:0 1.5rem}
10598
.profile-panel summary{cursor:pointer;font-weight:600;padding:0.75rem 0;font-size:1rem}
10699
.profile-panel pre{background:#f1f5f9;padding:1rem;border-radius:8px;overflow-x:auto;font-size:0.8rem;line-height:1.6}
@@ -139,13 +132,6 @@ ${
139132
.gallery-tile:nth-child(2){animation-delay:0.15s}
140133
.gallery-tile:nth-child(3){animation-delay:0.3s}
141134
@keyframes bounce{0%,100%{height:8px}50%{height:100%}}
142-
.autoplay-bars.animated span{animation:bounce 0.8s ease-in-out infinite}
143-
.autoplay-bars.animated span:nth-child(2){animation-delay:0.1s}
144-
.autoplay-bars.animated span:nth-child(3){animation-delay:0.2s}
145-
.autoplay-bars.animated span:nth-child(4){animation-delay:0.3s}
146-
.autoplay-bars.animated span:nth-child(5){animation-delay:0.4s}
147-
.autoplay-bars.animated span:nth-child(6){animation-delay:0.5s}
148-
.autoplay-bars.animated span:nth-child(7){animation-delay:0.6s}
149135
</style>`
150136
: ''
151137
}
@@ -223,19 +209,6 @@ ${
223209
}
224210
</div>
225211
226-
<div class="autoplay-section">
227-
<div class="autoplay-bars${autoplay ? ' animated' : ''}">
228-
<span style="height:12px"></span>
229-
<span style="height:28px"></span>
230-
<span style="height:20px"></span>
231-
<span style="height:36px"></span>
232-
<span style="height:16px"></span>
233-
<span style="height:32px"></span>
234-
<span style="height:24px"></span>
235-
</div>
236-
<div class="autoplay-label">Autoplay: ${autoplay ? 'On' : 'Disabled'}</div>
237-
</div>
238-
239212
<details class="profile-panel">
240213
<summary>Device Profile</summary>
241214
${

packages/types/README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ const hints = deriveHints(tiers, signals);
3636
// serveMinimalCSS: false,
3737
// reduceAnimations: false,
3838
// useImagePlaceholders: false,
39-
// disableAutoplay: false,
4039
// preferServerRendering: false,
4140
// disable3dEffects: false,
4241
// }
@@ -50,7 +49,6 @@ Rendering hints are derived from tiers and transient signals:
5049
| `serveMinimalCSS` | Low-end device |
5150
| `reduceAnimations` | Low-end device, prefers reduced motion, or low battery |
5251
| `useImagePlaceholders` | Slow connection (2G/3G) |
53-
| `disableAutoplay` | Low-end device, slow connection, or low battery |
5452
| `preferServerRendering` | Low-end device |
5553
| `disable3dEffects` | No GPU or software renderer |
5654

@@ -66,7 +64,7 @@ import { classify } from '@device-router/types';
6664
const tiers = classify(signals, {
6765
cpu: { lowUpperBound: 4, midUpperBound: 8 },
6866
memory: { midUpperBound: 8 },
69-
connection: { downlink4gUpperBound: 10 },
67+
connection: { highUpperBound: 10 },
7068
gpu: { highEndPattern: /\bRTX\b|\bGTX\b/i },
7169
});
7270
```

packages/types/src/__tests__/classify.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,18 @@ describe('classifyConnection', () => {
9393

9494
it('uses custom downlink thresholds', () => {
9595
// With wider 2g band (up to 1 Mbps)
96-
expect(classifyConnection(undefined, 0.8, { downlink2gUpperBound: 1 })).toBe('2g');
96+
expect(classifyConnection(undefined, 0.8, { lowUpperBound: 1 })).toBe('2g');
9797
// Default would classify 0.8 as 3g, custom keeps it as 2g
9898
expect(classifyConnection(undefined, 0.8)).toBe('3g');
9999

100100
// Custom 4g upper bound
101-
expect(classifyConnection(undefined, 8, { downlink4gUpperBound: 10 })).toBe('4g');
101+
expect(classifyConnection(undefined, 8, { highUpperBound: 10 })).toBe('4g');
102102
expect(classifyConnection(undefined, 8)).toBe('high');
103103
});
104104

105105
it('effectiveType string matches are not affected by thresholds', () => {
106-
expect(classifyConnection('2g', undefined, { downlink2gUpperBound: 0.1 })).toBe('2g');
107-
expect(classifyConnection('3g', undefined, { downlink3gUpperBound: 0.1 })).toBe('3g');
106+
expect(classifyConnection('2g', undefined, { lowUpperBound: 0.1 })).toBe('2g');
107+
expect(classifyConnection('3g', undefined, { midUpperBound: 0.1 })).toBe('3g');
108108
});
109109
});
110110

@@ -204,7 +204,7 @@ describe('classify', () => {
204204
{
205205
cpu: { lowUpperBound: 4 },
206206
memory: { lowUpperBound: 4 },
207-
connection: { downlink4gUpperBound: 3 },
207+
connection: { highUpperBound: 3 },
208208
},
209209
);
210210
expect(result).toEqual({ cpu: 'low', memory: 'low', connection: 'high', gpu: 'none' });

packages/types/src/__tests__/events.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const sampleEvent: DeviceRouterEvent = {
1111
serveMinimalCSS: false,
1212
reduceAnimations: false,
1313
useImagePlaceholders: false,
14-
disableAutoplay: false,
1514
preferServerRendering: false,
1615
disable3dEffects: false,
1716
},

packages/types/src/__tests__/hints.test.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ describe('deriveHints', () => {
99
serveMinimalCSS: true,
1010
reduceAnimations: true,
1111
useImagePlaceholders: true,
12-
disableAutoplay: true,
1312
preferServerRendering: true,
1413
disable3dEffects: true,
1514
});
@@ -22,7 +21,6 @@ describe('deriveHints', () => {
2221
serveMinimalCSS: false,
2322
reduceAnimations: false,
2423
useImagePlaceholders: false,
25-
disableAutoplay: false,
2624
preferServerRendering: false,
2725
disable3dEffects: false,
2826
});
@@ -40,7 +38,6 @@ describe('deriveHints', () => {
4038
const hints = deriveHints({ cpu: 'high', memory: 'high', connection: '3g', gpu: 'high' });
4139
expect(hints.deferHeavyComponents).toBe(true);
4240
expect(hints.useImagePlaceholders).toBe(true);
43-
expect(hints.disableAutoplay).toBe(true);
4441
expect(hints.serveMinimalCSS).toBe(false);
4542
expect(hints.preferServerRendering).toBe(false);
4643
});
@@ -78,7 +75,6 @@ describe('deriveHints', () => {
7875
);
7976
expect(hints.deferHeavyComponents).toBe(true);
8077
expect(hints.reduceAnimations).toBe(true);
81-
expect(hints.disableAutoplay).toBe(true);
8278
// Capability-based hints stay permissive
8379
expect(hints.serveMinimalCSS).toBe(false);
8480
expect(hints.useImagePlaceholders).toBe(false);
@@ -93,7 +89,6 @@ describe('deriveHints', () => {
9389
);
9490
expect(hints.deferHeavyComponents).toBe(false);
9591
expect(hints.reduceAnimations).toBe(false);
96-
expect(hints.disableAutoplay).toBe(false);
9792
});
9893

9994
it('does not constrain when battery above threshold', () => {
@@ -103,14 +98,12 @@ describe('deriveHints', () => {
10398
);
10499
expect(hints.deferHeavyComponents).toBe(false);
105100
expect(hints.reduceAnimations).toBe(false);
106-
expect(hints.disableAutoplay).toBe(false);
107101
});
108102

109103
it('does not change behavior when battery signal is absent', () => {
110104
const hints = deriveHints({ cpu: 'high', memory: 'high', connection: 'high', gpu: 'high' }, {});
111105
expect(hints.deferHeavyComponents).toBe(false);
112106
expect(hints.reduceAnimations).toBe(false);
113-
expect(hints.disableAutoplay).toBe(false);
114107
});
115108

116109
it('does not constrain at exactly 0.15 (strict less-than)', () => {
@@ -120,6 +113,5 @@ describe('deriveHints', () => {
120113
);
121114
expect(hints.deferHeavyComponents).toBe(false);
122115
expect(hints.reduceAnimations).toBe(false);
123-
expect(hints.disableAutoplay).toBe(false);
124116
});
125117
});

0 commit comments

Comments
 (0)