lowdata 0.1.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 +21 -0
- package/README.md +267 -0
- package/dist/client-BvdWSYIM.d.ts +187 -0
- package/dist/client-CqO3Y1J5.d.cts +187 -0
- package/dist/forms.cjs +1090 -0
- package/dist/forms.cjs.map +1 -0
- package/dist/forms.d.cts +29 -0
- package/dist/forms.d.ts +29 -0
- package/dist/forms.js +1088 -0
- package/dist/forms.js.map +1 -0
- package/dist/index.cjs +1109 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1099 -0
- package/dist/index.js.map +1 -0
- package/dist/media.cjs +278 -0
- package/dist/media.cjs.map +1 -0
- package/dist/media.d.cts +35 -0
- package/dist/media.d.ts +35 -0
- package/dist/media.js +274 -0
- package/dist/media.js.map +1 -0
- package/dist/network.cjs +923 -0
- package/dist/network.cjs.map +1 -0
- package/dist/network.d.cts +46 -0
- package/dist/network.d.ts +46 -0
- package/dist/network.js +912 -0
- package/dist/network.js.map +1 -0
- package/dist/progressiveImage-BWNdewbi.d.cts +23 -0
- package/dist/progressiveImage-BhY_K8Dj.d.ts +23 -0
- package/dist/react.cjs +1195 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +33 -0
- package/dist/react.d.ts +33 -0
- package/dist/react.js +1190 -0
- package/dist/react.js.map +1 -0
- package/dist/retry-C3zL9T5Z.d.cts +22 -0
- package/dist/retry-D6DfKGOi.d.ts +22 -0
- package/dist/types--FRrBa-i.d.cts +39 -0
- package/dist/types--FRrBa-i.d.ts +39 -0
- package/dist/types-Bn1BAcch.d.ts +33 -0
- package/dist/types-CZDYS-fB.d.cts +33 -0
- package/package.json +97 -0
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { b as ConnectionQuality } from './types--FRrBa-i.js';
|
|
2
|
+
export { P as ProgressiveImageLoader, a as ProgressiveImageLoaderOptions, b as ProgressiveImageState, c as createProgressiveImageLoader } from './progressiveImage-BhY_K8Dj.js';
|
|
3
|
+
|
|
4
|
+
interface ImageCompressionOptions {
|
|
5
|
+
/** Longest edge in pixels; the image is downscaled proportionally to fit. Default 1280. */
|
|
6
|
+
maxWidth?: number;
|
|
7
|
+
maxHeight?: number;
|
|
8
|
+
/** 0..1. Default 0.6, or a connection-aware preset when `connectionAware` is set. */
|
|
9
|
+
quality?: number;
|
|
10
|
+
mimeType?: 'image/jpeg' | 'image/webp';
|
|
11
|
+
/** If set, quality is iteratively lowered (down to a floor) until the blob fits this budget. */
|
|
12
|
+
targetSizeKB?: number;
|
|
13
|
+
/** Pull `maxWidth`/`quality`/`targetSizeKB` defaults from the current connection quality. */
|
|
14
|
+
connectionAware?: boolean;
|
|
15
|
+
}
|
|
16
|
+
interface CompressedImageResult {
|
|
17
|
+
blob: Blob;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
sizeBytes: number;
|
|
21
|
+
quality: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Client-side image compression via the Canvas API: downscales to a max dimension and re-encodes
|
|
26
|
+
* at a target quality, optionally iterating quality further down to hit a byte budget. No
|
|
27
|
+
* dependency — avoids pulling in a native/wasm image library, keeping the `lowdata/media` subpath
|
|
28
|
+
* tiny. Known v1 limitation: EXIF orientation can be lost on re-encode in some browsers.
|
|
29
|
+
*/
|
|
30
|
+
declare function compressImage(file: File | Blob, options?: ImageCompressionOptions): Promise<CompressedImageResult>;
|
|
31
|
+
|
|
32
|
+
type CompressionPreset = Required<Pick<ImageCompressionOptions, 'maxWidth' | 'quality'>> & Pick<ImageCompressionOptions, 'targetSizeKB'>;
|
|
33
|
+
declare function presetForQuality(quality: ConnectionQuality): CompressionPreset;
|
|
34
|
+
|
|
35
|
+
export { type CompressedImageResult, type CompressionPreset, type ImageCompressionOptions, compressImage, presetForQuality };
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// src/core/events.ts
|
|
2
|
+
var Emitter = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
5
|
+
}
|
|
6
|
+
subscribe(listener) {
|
|
7
|
+
this.listeners.add(listener);
|
|
8
|
+
return () => {
|
|
9
|
+
this.listeners.delete(listener);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
emit(value) {
|
|
13
|
+
for (const listener of this.listeners) {
|
|
14
|
+
listener(value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
clear() {
|
|
18
|
+
this.listeners.clear();
|
|
19
|
+
}
|
|
20
|
+
get size() {
|
|
21
|
+
return this.listeners.size;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/core/connection.ts
|
|
26
|
+
var DEFAULT_SLOW_RTT_MS = 600;
|
|
27
|
+
var DEFAULT_SLOW_DOWNLINK_MBPS = 0.5;
|
|
28
|
+
var DEFAULT_PING_TIMEOUT_MS = 5e3;
|
|
29
|
+
function hasNavigator() {
|
|
30
|
+
return typeof navigator !== "undefined";
|
|
31
|
+
}
|
|
32
|
+
var ConnectionMonitor = class {
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.emitter = new Emitter();
|
|
35
|
+
this.disposed = false;
|
|
36
|
+
var _a, _b, _c;
|
|
37
|
+
this.options = {
|
|
38
|
+
pingUrl: options.pingUrl,
|
|
39
|
+
slowRttThresholdMs: (_a = options.slowRttThresholdMs) != null ? _a : DEFAULT_SLOW_RTT_MS,
|
|
40
|
+
slowDownlinkMbps: (_b = options.slowDownlinkMbps) != null ? _b : DEFAULT_SLOW_DOWNLINK_MBPS,
|
|
41
|
+
pingTimeoutMs: (_c = options.pingTimeoutMs) != null ? _c : DEFAULT_PING_TIMEOUT_MS
|
|
42
|
+
};
|
|
43
|
+
this.current = this.computeInfo();
|
|
44
|
+
if (hasNavigator() && typeof window !== "undefined") {
|
|
45
|
+
this.onlineHandler = () => {
|
|
46
|
+
this.refresh();
|
|
47
|
+
void this.probeNow();
|
|
48
|
+
};
|
|
49
|
+
this.offlineHandler = () => this.refresh();
|
|
50
|
+
window.addEventListener("online", this.onlineHandler);
|
|
51
|
+
window.addEventListener("offline", this.offlineHandler);
|
|
52
|
+
const conn = navigator.connection;
|
|
53
|
+
if (conn == null ? void 0 : conn.addEventListener) {
|
|
54
|
+
this.connectionChangeHandler = () => this.refresh();
|
|
55
|
+
conn.addEventListener("change", this.connectionChangeHandler);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (this.options.pingUrl) {
|
|
59
|
+
void this.probeNow();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
getStatus() {
|
|
63
|
+
return this.current;
|
|
64
|
+
}
|
|
65
|
+
subscribe(listener) {
|
|
66
|
+
return this.emitter.subscribe(listener);
|
|
67
|
+
}
|
|
68
|
+
/** Manually re-run the opt-in ping probe (no-op if no `pingUrl` was configured). */
|
|
69
|
+
async probeNow() {
|
|
70
|
+
if (!this.options.pingUrl || !hasNavigator() || navigator.onLine === false) {
|
|
71
|
+
return this.current;
|
|
72
|
+
}
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timer = setTimeout(() => controller.abort(), this.options.pingTimeoutMs);
|
|
76
|
+
try {
|
|
77
|
+
await fetch(this.options.pingUrl, {
|
|
78
|
+
method: "HEAD",
|
|
79
|
+
cache: "no-store",
|
|
80
|
+
signal: controller.signal
|
|
81
|
+
});
|
|
82
|
+
this.probedRttMs = Date.now() - start;
|
|
83
|
+
} catch {
|
|
84
|
+
} finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
return this.refresh();
|
|
88
|
+
}
|
|
89
|
+
destroy() {
|
|
90
|
+
var _a;
|
|
91
|
+
if (this.disposed) return;
|
|
92
|
+
this.disposed = true;
|
|
93
|
+
if (typeof window !== "undefined") {
|
|
94
|
+
if (this.onlineHandler) window.removeEventListener("online", this.onlineHandler);
|
|
95
|
+
if (this.offlineHandler) window.removeEventListener("offline", this.offlineHandler);
|
|
96
|
+
}
|
|
97
|
+
if (hasNavigator() && this.connectionChangeHandler) {
|
|
98
|
+
const conn = navigator.connection;
|
|
99
|
+
(_a = conn == null ? void 0 : conn.removeEventListener) == null ? void 0 : _a.call(conn, "change", this.connectionChangeHandler);
|
|
100
|
+
}
|
|
101
|
+
this.emitter.clear();
|
|
102
|
+
}
|
|
103
|
+
refresh() {
|
|
104
|
+
this.current = this.computeInfo();
|
|
105
|
+
this.emitter.emit(this.current);
|
|
106
|
+
return this.current;
|
|
107
|
+
}
|
|
108
|
+
computeInfo() {
|
|
109
|
+
if (!hasNavigator()) {
|
|
110
|
+
return { quality: "online", online: true };
|
|
111
|
+
}
|
|
112
|
+
const online = navigator.onLine !== false;
|
|
113
|
+
if (!online) {
|
|
114
|
+
return { quality: "offline", online: false };
|
|
115
|
+
}
|
|
116
|
+
const conn = navigator.connection;
|
|
117
|
+
if (conn) {
|
|
118
|
+
const info = {
|
|
119
|
+
quality: "online",
|
|
120
|
+
online: true,
|
|
121
|
+
effectiveType: conn.effectiveType,
|
|
122
|
+
downlinkMbps: conn.downlink,
|
|
123
|
+
rttMs: conn.rtt,
|
|
124
|
+
saveData: conn.saveData
|
|
125
|
+
};
|
|
126
|
+
const isSlow = conn.saveData === true || conn.effectiveType === "2g" || conn.effectiveType === "slow-2g" || typeof conn.downlink === "number" && conn.downlink < this.options.slowDownlinkMbps || typeof conn.rtt === "number" && conn.rtt > this.options.slowRttThresholdMs;
|
|
127
|
+
return { ...info, quality: isSlow ? "slow" : "online" };
|
|
128
|
+
}
|
|
129
|
+
const quality = typeof this.probedRttMs === "number" && this.probedRttMs > this.options.slowRttThresholdMs ? "slow" : "online";
|
|
130
|
+
return {
|
|
131
|
+
quality,
|
|
132
|
+
online: true,
|
|
133
|
+
rttMs: this.probedRttMs
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
var sharedMonitor;
|
|
138
|
+
function getSharedMonitor() {
|
|
139
|
+
if (!sharedMonitor) sharedMonitor = new ConnectionMonitor();
|
|
140
|
+
return sharedMonitor;
|
|
141
|
+
}
|
|
142
|
+
function getConnectionQuality() {
|
|
143
|
+
return getSharedMonitor().getStatus();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/media/qualityPolicy.ts
|
|
147
|
+
var PRESETS = {
|
|
148
|
+
online: { maxWidth: 1600, quality: 0.72 },
|
|
149
|
+
slow: { maxWidth: 1e3, quality: 0.5, targetSizeKB: 150 },
|
|
150
|
+
offline: { maxWidth: 800, quality: 0.4, targetSizeKB: 80 }
|
|
151
|
+
};
|
|
152
|
+
function presetForQuality(quality) {
|
|
153
|
+
return PRESETS[quality];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/media/compressImage.ts
|
|
157
|
+
var DEFAULT_MAX_WIDTH = 1280;
|
|
158
|
+
var DEFAULT_QUALITY = 0.6;
|
|
159
|
+
var DEFAULT_MIME = "image/jpeg";
|
|
160
|
+
var MIN_QUALITY = 0.35;
|
|
161
|
+
var QUALITY_STEP = 0.1;
|
|
162
|
+
var MAX_QUALITY_ITERATIONS = 5;
|
|
163
|
+
async function loadImageSource(file) {
|
|
164
|
+
if (typeof createImageBitmap === "function") {
|
|
165
|
+
return createImageBitmap(file);
|
|
166
|
+
}
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const url = URL.createObjectURL(file);
|
|
169
|
+
const img = new Image();
|
|
170
|
+
img.onload = () => {
|
|
171
|
+
URL.revokeObjectURL(url);
|
|
172
|
+
resolve(img);
|
|
173
|
+
};
|
|
174
|
+
img.onerror = () => {
|
|
175
|
+
URL.revokeObjectURL(url);
|
|
176
|
+
reject(new Error("lowdata: failed to load image for compression"));
|
|
177
|
+
};
|
|
178
|
+
img.src = url;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function computeTargetDimensions(sourceWidth, sourceHeight, maxWidth, maxHeight) {
|
|
182
|
+
let width = sourceWidth;
|
|
183
|
+
let height = sourceHeight;
|
|
184
|
+
if (width > maxWidth) {
|
|
185
|
+
height = Math.round(height * maxWidth / width);
|
|
186
|
+
width = maxWidth;
|
|
187
|
+
}
|
|
188
|
+
if (maxHeight && height > maxHeight) {
|
|
189
|
+
width = Math.round(width * maxHeight / height);
|
|
190
|
+
height = maxHeight;
|
|
191
|
+
}
|
|
192
|
+
return { width, height };
|
|
193
|
+
}
|
|
194
|
+
function drawToCanvas(source, width, height) {
|
|
195
|
+
const canvas = document.createElement("canvas");
|
|
196
|
+
canvas.width = width;
|
|
197
|
+
canvas.height = height;
|
|
198
|
+
const ctx = canvas.getContext("2d");
|
|
199
|
+
if (!ctx) throw new Error("lowdata: canvas 2D context is not available in this environment");
|
|
200
|
+
ctx.drawImage(source, 0, 0, width, height);
|
|
201
|
+
return canvas;
|
|
202
|
+
}
|
|
203
|
+
function canvasToBlob(canvas, mimeType, quality) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
canvas.toBlob(
|
|
206
|
+
(blob) => {
|
|
207
|
+
if (blob) resolve(blob);
|
|
208
|
+
else reject(new Error("lowdata: canvas failed to produce an image blob"));
|
|
209
|
+
},
|
|
210
|
+
mimeType,
|
|
211
|
+
quality
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async function compressImage(file, options = {}) {
|
|
216
|
+
var _a, _b, _c, _d, _e, _f;
|
|
217
|
+
const preset = options.connectionAware ? presetForQuality(getConnectionQuality().quality) : void 0;
|
|
218
|
+
const maxWidth = (_b = (_a = options.maxWidth) != null ? _a : preset == null ? void 0 : preset.maxWidth) != null ? _b : DEFAULT_MAX_WIDTH;
|
|
219
|
+
const maxHeight = options.maxHeight;
|
|
220
|
+
const mimeType = (_c = options.mimeType) != null ? _c : DEFAULT_MIME;
|
|
221
|
+
const targetSizeKB = (_d = options.targetSizeKB) != null ? _d : preset == null ? void 0 : preset.targetSizeKB;
|
|
222
|
+
let quality = (_f = (_e = options.quality) != null ? _e : preset == null ? void 0 : preset.quality) != null ? _f : DEFAULT_QUALITY;
|
|
223
|
+
const source = await loadImageSource(file);
|
|
224
|
+
const { width, height } = computeTargetDimensions(
|
|
225
|
+
source.width,
|
|
226
|
+
source.height,
|
|
227
|
+
maxWidth,
|
|
228
|
+
maxHeight
|
|
229
|
+
);
|
|
230
|
+
const canvas = drawToCanvas(source, width, height);
|
|
231
|
+
if ("close" in source && typeof source.close === "function") source.close();
|
|
232
|
+
let blob = await canvasToBlob(canvas, mimeType, quality);
|
|
233
|
+
if (targetSizeKB) {
|
|
234
|
+
const targetBytes = targetSizeKB * 1024;
|
|
235
|
+
let iterations = 0;
|
|
236
|
+
while (blob.size > targetBytes && quality > MIN_QUALITY && iterations < MAX_QUALITY_ITERATIONS) {
|
|
237
|
+
quality = Math.max(MIN_QUALITY, quality - QUALITY_STEP);
|
|
238
|
+
blob = await canvasToBlob(canvas, mimeType, quality);
|
|
239
|
+
iterations++;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { blob, width, height, sizeBytes: blob.size, quality };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/media/progressiveImage.ts
|
|
246
|
+
function createProgressiveImageLoader(options) {
|
|
247
|
+
const emitter = new Emitter();
|
|
248
|
+
let state = { src: options.placeholder, isLoaded: false };
|
|
249
|
+
let disposed = false;
|
|
250
|
+
const img = typeof Image !== "undefined" ? new Image() : void 0;
|
|
251
|
+
if (img) {
|
|
252
|
+
img.onload = () => {
|
|
253
|
+
if (disposed) return;
|
|
254
|
+
state = { src: options.src, isLoaded: true };
|
|
255
|
+
emitter.emit(state);
|
|
256
|
+
};
|
|
257
|
+
img.src = options.src;
|
|
258
|
+
} else {
|
|
259
|
+
state = { src: options.src, isLoaded: true };
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
subscribe: (callback) => emitter.subscribe(callback),
|
|
263
|
+
getState: () => state,
|
|
264
|
+
destroy: () => {
|
|
265
|
+
disposed = true;
|
|
266
|
+
if (img) img.onload = null;
|
|
267
|
+
emitter.clear();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export { compressImage, createProgressiveImageLoader, presetForQuality };
|
|
273
|
+
//# sourceMappingURL=media.js.map
|
|
274
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/events.ts","../src/core/connection.ts","../src/media/qualityPolicy.ts","../src/media/compressImage.ts","../src/media/progressiveImage.ts"],"names":[],"mappings":"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","file":"media.js","sourcesContent":["/** Tiny typed pub-sub. No Node `EventEmitter` dependency, works in any JS runtime. */\nexport class Emitter<T> {\n private listeners = new Set<(value: T) => void>();\n\n subscribe(listener: (value: T) => void): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n emit(value: T): void {\n for (const listener of this.listeners) {\n listener(value);\n }\n }\n\n clear(): void {\n this.listeners.clear();\n }\n\n get size(): number {\n return this.listeners.size;\n }\n}\n","import { Emitter } from './events.js';\nimport type {\n ConnectionInfo,\n ConnectionListener,\n ConnectionQuality,\n Unsubscribe,\n} from './types.js';\n\n/**\n * The Network Information API (`navigator.connection`) is Chromium-only and not part of the\n * standard DOM lib types, hence this local shape instead of a global augmentation.\n */\ninterface NetworkInformationLike {\n effectiveType?: '4g' | '3g' | '2g' | 'slow-2g';\n downlink?: number;\n rtt?: number;\n saveData?: boolean;\n addEventListener?: (type: 'change', listener: () => void) => void;\n removeEventListener?: (type: 'change', listener: () => void) => void;\n}\n\ntype NavigatorWithConnection = Navigator & { connection?: NetworkInformationLike };\n\nexport interface ConnectionMonitorOptions {\n /**\n * Opt-in latency probe URL used to detect 'slow' connections on browsers without the Network\n * Information API (Safari, Firefox). Never probed automatically on a timer — only once on\n * startup and once per reconnect — because every probe costs a little data.\n */\n pingUrl?: string;\n /** Round-trip time above which the ping probe classifies the connection as 'slow'. Default 600ms. */\n slowRttThresholdMs?: number;\n /** downlink (Mbps) below which the Network Information API classifies as 'slow'. Default 0.5. */\n slowDownlinkMbps?: number;\n /** Timeout for the ping probe itself, in ms. Default 5000. */\n pingTimeoutMs?: number;\n}\n\nconst DEFAULT_SLOW_RTT_MS = 600;\nconst DEFAULT_SLOW_DOWNLINK_MBPS = 0.5;\nconst DEFAULT_PING_TIMEOUT_MS = 5000;\n\nfunction hasNavigator(): boolean {\n return typeof navigator !== 'undefined';\n}\n\n/**\n * Tracks connection quality (online / slow / offline) using the best signal available:\n * `navigator.onLine` + online/offline events as the universal baseline, `navigator.connection`\n * where present, and an optional opt-in ping probe as a cross-browser fallback for 'slow'.\n */\nexport class ConnectionMonitor {\n private emitter = new Emitter<ConnectionInfo>();\n private current: ConnectionInfo;\n private probedRttMs: number | undefined;\n private readonly options: Required<\n Pick<ConnectionMonitorOptions, 'slowRttThresholdMs' | 'slowDownlinkMbps' | 'pingTimeoutMs'>\n > &\n Pick<ConnectionMonitorOptions, 'pingUrl'>;\n private disposed = false;\n private onlineHandler?: () => void;\n private offlineHandler?: () => void;\n private connectionChangeHandler?: () => void;\n\n constructor(options: ConnectionMonitorOptions = {}) {\n this.options = {\n pingUrl: options.pingUrl,\n slowRttThresholdMs: options.slowRttThresholdMs ?? DEFAULT_SLOW_RTT_MS,\n slowDownlinkMbps: options.slowDownlinkMbps ?? DEFAULT_SLOW_DOWNLINK_MBPS,\n pingTimeoutMs: options.pingTimeoutMs ?? DEFAULT_PING_TIMEOUT_MS,\n };\n this.current = this.computeInfo();\n\n if (hasNavigator() && typeof window !== 'undefined') {\n this.onlineHandler = () => {\n this.refresh();\n void this.probeNow();\n };\n this.offlineHandler = () => this.refresh();\n window.addEventListener('online', this.onlineHandler);\n window.addEventListener('offline', this.offlineHandler);\n\n const conn = (navigator as NavigatorWithConnection).connection;\n if (conn?.addEventListener) {\n this.connectionChangeHandler = () => this.refresh();\n conn.addEventListener('change', this.connectionChangeHandler);\n }\n }\n\n if (this.options.pingUrl) {\n void this.probeNow();\n }\n }\n\n getStatus(): ConnectionInfo {\n return this.current;\n }\n\n subscribe(listener: ConnectionListener): Unsubscribe {\n return this.emitter.subscribe(listener);\n }\n\n /** Manually re-run the opt-in ping probe (no-op if no `pingUrl` was configured). */\n async probeNow(): Promise<ConnectionInfo> {\n if (!this.options.pingUrl || !hasNavigator() || navigator.onLine === false) {\n return this.current;\n }\n const start = Date.now();\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.options.pingTimeoutMs);\n try {\n await fetch(this.options.pingUrl, {\n method: 'HEAD',\n cache: 'no-store',\n signal: controller.signal,\n });\n this.probedRttMs = Date.now() - start;\n } catch {\n // A failed probe tells us nothing reliable about latency; leave prior measurement as-is.\n } finally {\n clearTimeout(timer);\n }\n return this.refresh();\n }\n\n destroy(): void {\n if (this.disposed) return;\n this.disposed = true;\n if (typeof window !== 'undefined') {\n if (this.onlineHandler) window.removeEventListener('online', this.onlineHandler);\n if (this.offlineHandler) window.removeEventListener('offline', this.offlineHandler);\n }\n if (hasNavigator() && this.connectionChangeHandler) {\n const conn = (navigator as NavigatorWithConnection).connection;\n conn?.removeEventListener?.('change', this.connectionChangeHandler);\n }\n this.emitter.clear();\n }\n\n private refresh(): ConnectionInfo {\n this.current = this.computeInfo();\n this.emitter.emit(this.current);\n return this.current;\n }\n\n private computeInfo(): ConnectionInfo {\n if (!hasNavigator()) {\n // Server/SSR context: assume the best case rather than block anything.\n return { quality: 'online', online: true };\n }\n\n const online = navigator.onLine !== false;\n if (!online) {\n return { quality: 'offline', online: false };\n }\n\n const conn = (navigator as NavigatorWithConnection).connection;\n if (conn) {\n const info: ConnectionInfo = {\n quality: 'online',\n online: true,\n effectiveType: conn.effectiveType,\n downlinkMbps: conn.downlink,\n rttMs: conn.rtt,\n saveData: conn.saveData,\n };\n const isSlow =\n conn.saveData === true ||\n conn.effectiveType === '2g' ||\n conn.effectiveType === 'slow-2g' ||\n (typeof conn.downlink === 'number' && conn.downlink < this.options.slowDownlinkMbps) ||\n (typeof conn.rtt === 'number' && conn.rtt > this.options.slowRttThresholdMs);\n return { ...info, quality: isSlow ? 'slow' : 'online' };\n }\n\n // No Network Information API: fall back to the opt-in ping probe measurement, if any.\n const quality: ConnectionQuality =\n typeof this.probedRttMs === 'number' && this.probedRttMs > this.options.slowRttThresholdMs\n ? 'slow'\n : 'online';\n return {\n quality,\n online: true,\n rttMs: this.probedRttMs,\n };\n }\n}\n\nlet sharedMonitor: ConnectionMonitor | undefined;\n\nfunction getSharedMonitor(): ConnectionMonitor {\n if (!sharedMonitor) sharedMonitor = new ConnectionMonitor();\n return sharedMonitor;\n}\n\n/** Convenience one-shot read of connection quality, without owning a `LowdataClient`. */\nexport function getConnectionQuality(): ConnectionInfo {\n return getSharedMonitor().getStatus();\n}\n\n/** Convenience subscription to connection changes, without owning a `LowdataClient`. */\nexport function onConnectionChange(listener: ConnectionListener): Unsubscribe {\n return getSharedMonitor().subscribe(listener);\n}\n","import type { ConnectionQuality } from '../core/types.js';\nimport type { ImageCompressionOptions } from './types.js';\n\nexport type CompressionPreset = Required<Pick<ImageCompressionOptions, 'maxWidth' | 'quality'>> &\n Pick<ImageCompressionOptions, 'targetSizeKB'>;\n\n/**\n * Aggressive-but-usable presets by connection quality. 'offline' is deliberately the smallest —\n * an image compressed while offline is headed straight into the persistent queue, where every\n * extra KB counts against storage quota and, eventually, the user's data bundle.\n */\nconst PRESETS: Record<ConnectionQuality, CompressionPreset> = {\n online: { maxWidth: 1600, quality: 0.72 },\n slow: { maxWidth: 1000, quality: 0.5, targetSizeKB: 150 },\n offline: { maxWidth: 800, quality: 0.4, targetSizeKB: 80 },\n};\n\nexport function presetForQuality(quality: ConnectionQuality): CompressionPreset {\n return PRESETS[quality];\n}\n","import { getConnectionQuality } from '../core/connection.js';\nimport { presetForQuality } from './qualityPolicy.js';\nimport type { CompressedImageResult, ImageCompressionOptions } from './types.js';\n\nconst DEFAULT_MAX_WIDTH = 1280;\nconst DEFAULT_QUALITY = 0.6;\nconst DEFAULT_MIME: NonNullable<ImageCompressionOptions['mimeType']> = 'image/jpeg';\nconst MIN_QUALITY = 0.35;\nconst QUALITY_STEP = 0.1;\nconst MAX_QUALITY_ITERATIONS = 5;\n\ntype ImageSource = ImageBitmap | HTMLImageElement;\n\nasync function loadImageSource(file: File | Blob): Promise<ImageSource> {\n if (typeof createImageBitmap === 'function') {\n return createImageBitmap(file);\n }\n // Fallback for environments without createImageBitmap (older Safari).\n return new Promise<HTMLImageElement>((resolve, reject) => {\n const url = URL.createObjectURL(file);\n const img = new Image();\n img.onload = () => {\n URL.revokeObjectURL(url);\n resolve(img);\n };\n img.onerror = () => {\n URL.revokeObjectURL(url);\n reject(new Error('lowdata: failed to load image for compression'));\n };\n img.src = url;\n });\n}\n\nfunction computeTargetDimensions(\n sourceWidth: number,\n sourceHeight: number,\n maxWidth: number,\n maxHeight?: number,\n): { width: number; height: number } {\n let width = sourceWidth;\n let height = sourceHeight;\n\n if (width > maxWidth) {\n height = Math.round((height * maxWidth) / width);\n width = maxWidth;\n }\n if (maxHeight && height > maxHeight) {\n width = Math.round((width * maxHeight) / height);\n height = maxHeight;\n }\n return { width, height };\n}\n\nfunction drawToCanvas(source: ImageSource, width: number, height: number): HTMLCanvasElement {\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('lowdata: canvas 2D context is not available in this environment');\n ctx.drawImage(source, 0, 0, width, height);\n return canvas;\n}\n\nfunction canvasToBlob(canvas: HTMLCanvasElement, mimeType: string, quality: number): Promise<Blob> {\n return new Promise((resolve, reject) => {\n canvas.toBlob(\n (blob) => {\n if (blob) resolve(blob);\n else reject(new Error('lowdata: canvas failed to produce an image blob'));\n },\n mimeType,\n quality,\n );\n });\n}\n\n/**\n * Client-side image compression via the Canvas API: downscales to a max dimension and re-encodes\n * at a target quality, optionally iterating quality further down to hit a byte budget. No\n * dependency — avoids pulling in a native/wasm image library, keeping the `lowdata/media` subpath\n * tiny. Known v1 limitation: EXIF orientation can be lost on re-encode in some browsers.\n */\nexport async function compressImage(\n file: File | Blob,\n options: ImageCompressionOptions = {},\n): Promise<CompressedImageResult> {\n const preset = options.connectionAware\n ? presetForQuality(getConnectionQuality().quality)\n : undefined;\n const maxWidth = options.maxWidth ?? preset?.maxWidth ?? DEFAULT_MAX_WIDTH;\n const maxHeight = options.maxHeight;\n const mimeType = options.mimeType ?? DEFAULT_MIME;\n const targetSizeKB = options.targetSizeKB ?? preset?.targetSizeKB;\n let quality = options.quality ?? preset?.quality ?? DEFAULT_QUALITY;\n\n const source = await loadImageSource(file);\n const { width, height } = computeTargetDimensions(\n source.width,\n source.height,\n maxWidth,\n maxHeight,\n );\n const canvas = drawToCanvas(source, width, height);\n if ('close' in source && typeof source.close === 'function') source.close();\n\n let blob = await canvasToBlob(canvas, mimeType, quality);\n\n if (targetSizeKB) {\n const targetBytes = targetSizeKB * 1024;\n let iterations = 0;\n while (\n blob.size > targetBytes &&\n quality > MIN_QUALITY &&\n iterations < MAX_QUALITY_ITERATIONS\n ) {\n quality = Math.max(MIN_QUALITY, quality - QUALITY_STEP);\n blob = await canvasToBlob(canvas, mimeType, quality);\n iterations++;\n }\n }\n\n return { blob, width, height, sizeBytes: blob.size, quality };\n}\n","import { Emitter } from '../core/events.js';\nimport type { Unsubscribe } from '../core/types.js';\n\nexport interface ProgressiveImageState {\n src: string;\n isLoaded: boolean;\n}\n\nexport interface ProgressiveImageLoaderOptions {\n src: string;\n placeholder: string;\n}\n\nexport interface ProgressiveImageLoader {\n subscribe(callback: (state: ProgressiveImageState) => void): Unsubscribe;\n getState(): ProgressiveImageState;\n destroy(): void;\n}\n\n/**\n * Blur-up / placeholder helper: reports the (typically tiny, inlined) placeholder immediately,\n * preloads the full-size image in the background, and reports the swap once it's ready.\n * Framework-agnostic — `lowdata/react`'s `useProgressiveImage` is a thin hook over this.\n */\nexport function createProgressiveImageLoader(\n options: ProgressiveImageLoaderOptions,\n): ProgressiveImageLoader {\n const emitter = new Emitter<ProgressiveImageState>();\n let state: ProgressiveImageState = { src: options.placeholder, isLoaded: false };\n let disposed = false;\n\n const img = typeof Image !== 'undefined' ? new Image() : undefined;\n if (img) {\n img.onload = () => {\n if (disposed) return;\n state = { src: options.src, isLoaded: true };\n emitter.emit(state);\n };\n img.src = options.src;\n } else {\n // No Image constructor available (SSR) — nothing to preload; report the target src directly.\n state = { src: options.src, isLoaded: true };\n }\n\n return {\n subscribe: (callback) => emitter.subscribe(callback),\n getState: () => state,\n destroy: () => {\n disposed = true;\n if (img) img.onload = null;\n emitter.clear();\n },\n };\n}\n"]}
|