tersejson 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tim Carter
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.
package/README.md ADDED
@@ -0,0 +1,476 @@
1
+ # TerseJSON
2
+
3
+ **Transparent JSON key compression for Express APIs. Reduce bandwidth by up to 80% with zero code changes.**
4
+
5
+ [![npm version](https://badge.fury.io/js/tersejson.svg)](https://www.npmjs.com/package/tersejson)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## The Problem
9
+
10
+ Every API response repeats the same keys over and over:
11
+
12
+ ```json
13
+ [
14
+ { "firstName": "John", "lastName": "Doe", "emailAddress": "john@example.com" },
15
+ { "firstName": "Jane", "lastName": "Doe", "emailAddress": "jane@example.com" },
16
+ // ... 1000 more objects with the same keys
17
+ ]
18
+ ```
19
+
20
+ For 1000 objects, you're sending ~50KB of just repeated key names!
21
+
22
+ ## The Solution
23
+
24
+ TerseJSON automatically compresses keys on the server and transparently expands them on the client:
25
+
26
+ ```
27
+ Over the wire (compressed):
28
+ {
29
+ "k": { "a": "firstName", "b": "lastName", "c": "emailAddress" },
30
+ "d": [
31
+ { "a": "John", "b": "Doe", "c": "john@example.com" },
32
+ { "a": "Jane", "b": "Doe", "c": "jane@example.com" }
33
+ ]
34
+ }
35
+
36
+ Your code sees (via Proxy magic):
37
+ users[0].firstName // "John" - just works!
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ### Installation
43
+
44
+ ```bash
45
+ npm install tersejson
46
+ ```
47
+
48
+ ### Backend (Express)
49
+
50
+ ```typescript
51
+ import express from 'express';
52
+ import { terse } from 'tersejson/express';
53
+
54
+ const app = express();
55
+ app.use(terse());
56
+
57
+ app.get('/api/users', (req, res) => {
58
+ // Just send data as normal - compression is automatic!
59
+ res.json(users);
60
+ });
61
+ ```
62
+
63
+ ### Frontend
64
+
65
+ ```typescript
66
+ import { fetch } from 'tersejson/client';
67
+
68
+ // Use exactly like regular fetch
69
+ const users = await fetch('/api/users').then(r => r.json());
70
+
71
+ // Access properties normally - TerseJSON handles the mapping
72
+ console.log(users[0].firstName); // Works transparently!
73
+ console.log(users[0].emailAddress); // Works transparently!
74
+ ```
75
+
76
+ ## How It Works
77
+
78
+ ### Compression Flow
79
+
80
+ ```
81
+ ┌─────────────────────────────────────────────────────────────┐
82
+ │ 1. Your Express route calls res.json(data) │
83
+ │ ↓ │
84
+ │ 2. TerseJSON middleware intercepts the response │
85
+ │ ↓ │
86
+ │ 3. Detects array of objects with repeated keys │
87
+ │ ↓ │
88
+ │ 4. Creates key map: { "a": "firstName", "b": "lastName" } │
89
+ │ ↓ │
90
+ │ 5. Replaces keys in data with short aliases │
91
+ │ ↓ │
92
+ │ 6. Sends compressed payload + header │
93
+ └─────────────────────────────────────────────────────────────┘
94
+
95
+ ┌─────────────────────────────────────────────────────────────┐
96
+ │ 7. Client fetch() receives response │
97
+ │ ↓ │
98
+ │ 8. Detects terse header, parses payload │
99
+ │ ↓ │
100
+ │ 9. Wraps data in Proxy for transparent key access │
101
+ │ ↓ │
102
+ │ 10. Your code accesses data.firstName → mapped to data.a │
103
+ └─────────────────────────────────────────────────────────────┘
104
+ ```
105
+
106
+ ### Bandwidth Savings
107
+
108
+ | Scenario | Original | Compressed | Savings |
109
+ |----------|----------|------------|---------|
110
+ | 100 users, 10 fields | 45 KB | 12 KB | **73%** |
111
+ | 1000 products, 15 fields | 890 KB | 180 KB | **80%** |
112
+ | 10000 logs, 8 fields | 2.1 MB | 450 KB | **79%** |
113
+
114
+ *Note: These savings are **before** gzip. Combined with gzip, total reduction can exceed 90%.*
115
+
116
+ ## API Reference
117
+
118
+ ### Express Middleware
119
+
120
+ ```typescript
121
+ import { terse, terseQueryParam } from 'tersejson/express';
122
+
123
+ // Basic usage
124
+ app.use(terse());
125
+
126
+ // With options
127
+ app.use(terse({
128
+ minArrayLength: 5, // Only compress arrays with 5+ items
129
+ minKeyLength: 4, // Only compress keys with 4+ characters
130
+ maxDepth: 5, // Max nesting depth to traverse
131
+ debug: true, // Log compression stats
132
+ headerName: 'x-terse', // Custom header name
133
+ shouldCompress: (data, req) => {
134
+ // Custom logic to skip compression
135
+ return !req.path.includes('/admin');
136
+ },
137
+ }));
138
+
139
+ // Enable via query parameter (?terse=true)
140
+ app.use(terseQueryParam());
141
+ ```
142
+
143
+ ### Client Library
144
+
145
+ ```typescript
146
+ import {
147
+ fetch, // Drop-in fetch replacement
148
+ createFetch, // Create configured fetch instance
149
+ expand, // Fully expand a terse payload
150
+ proxy, // Wrap payload with Proxy (default)
151
+ process, // Auto-detect and expand/proxy
152
+ axiosInterceptor // Axios support
153
+ } from 'tersejson/client';
154
+
155
+ // Drop-in fetch replacement
156
+ const data = await fetch('/api/users').then(r => r.json());
157
+
158
+ // Custom fetch instance
159
+ const customFetch = createFetch({
160
+ debug: true,
161
+ autoExpand: true,
162
+ });
163
+
164
+ // Axios integration
165
+ import axios from 'axios';
166
+ axios.interceptors.request.use(axiosInterceptor.request);
167
+ axios.interceptors.response.use(axiosInterceptor.response);
168
+
169
+ // Manual processing
170
+ import { process } from 'tersejson/client';
171
+ const response = await regularFetch('/api/users');
172
+ const data = process(await response.json());
173
+ ```
174
+
175
+ ### Core Functions
176
+
177
+ ```typescript
178
+ import {
179
+ compress, // Compress an array of objects
180
+ expand, // Expand a terse payload
181
+ isCompressibleArray,// Check if data can be compressed
182
+ isTersePayload, // Check if data is a terse payload
183
+ createTerseProxy, // Create a Proxy for transparent access
184
+ } from 'tersejson';
185
+
186
+ // Manual compression
187
+ const compressed = compress(users, { minKeyLength: 3 });
188
+
189
+ // Manual expansion
190
+ const original = expand(compressed);
191
+
192
+ // Type checking
193
+ if (isTersePayload(data)) {
194
+ const expanded = expand(data);
195
+ }
196
+ ```
197
+
198
+ ## TypeScript Support
199
+
200
+ TerseJSON is written in TypeScript and provides full type definitions:
201
+
202
+ ```typescript
203
+ import type {
204
+ TersePayload,
205
+ TerseMiddlewareOptions,
206
+ TerseClientOptions,
207
+ Tersed,
208
+ } from 'tersejson';
209
+
210
+ interface User {
211
+ firstName: string;
212
+ lastName: string;
213
+ email: string;
214
+ }
215
+
216
+ // Types flow through compression
217
+ const users: User[] = await fetch('/api/users').then(r => r.json());
218
+ users[0].firstName; // TypeScript knows this is a string
219
+ ```
220
+
221
+ ## Framework Integrations
222
+
223
+ TerseJSON provides ready-to-use integrations for popular HTTP clients and frameworks.
224
+
225
+ ### Axios
226
+
227
+ ```typescript
228
+ import axios from 'axios';
229
+ import { createAxiosInterceptors } from 'tersejson/integrations';
230
+
231
+ const { request, response } = createAxiosInterceptors();
232
+ axios.interceptors.request.use(request);
233
+ axios.interceptors.response.use(response);
234
+
235
+ // Now all axios requests automatically handle TerseJSON!
236
+ const { data } = await axios.get('/api/users');
237
+ console.log(data[0].firstName); // Works transparently!
238
+ ```
239
+
240
+ ### Angular (HttpClient)
241
+
242
+ ```typescript
243
+ // terse.interceptor.ts
244
+ import { Injectable } from '@angular/core';
245
+ import {
246
+ HttpInterceptor,
247
+ HttpRequest,
248
+ HttpHandler,
249
+ HttpEvent,
250
+ HttpResponse
251
+ } from '@angular/common/http';
252
+ import { Observable } from 'rxjs';
253
+ import { map } from 'rxjs/operators';
254
+ import { isTersePayload, wrapWithProxy } from 'tersejson';
255
+
256
+ @Injectable()
257
+ export class TerseInterceptor implements HttpInterceptor {
258
+ intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
259
+ // Add accept-terse header
260
+ const terseReq = req.clone({
261
+ setHeaders: { 'accept-terse': 'true' }
262
+ });
263
+
264
+ return next.handle(terseReq).pipe(
265
+ map(event => {
266
+ if (event instanceof HttpResponse && event.body) {
267
+ const isTerse = event.headers.get('x-terse-json') === 'true';
268
+ if (isTerse && isTersePayload(event.body)) {
269
+ return event.clone({ body: wrapWithProxy(event.body) });
270
+ }
271
+ }
272
+ return event;
273
+ })
274
+ );
275
+ }
276
+ }
277
+
278
+ // app.module.ts
279
+ @NgModule({
280
+ providers: [
281
+ { provide: HTTP_INTERCEPTORS, useClass: TerseInterceptor, multi: true }
282
+ ]
283
+ })
284
+ ```
285
+
286
+ ### AngularJS (1.x)
287
+
288
+ ```javascript
289
+ angular.module('myApp', [])
290
+ .factory('terseInterceptor', function() {
291
+ return {
292
+ request: function(config) {
293
+ config.headers = config.headers || {};
294
+ config.headers['accept-terse'] = 'true';
295
+ return config;
296
+ },
297
+ response: function(response) {
298
+ var isTerse = response.headers('x-terse-json') === 'true';
299
+ if (isTerse && response.data && response.data.__terse__) {
300
+ response.data = tersejson.process(response.data);
301
+ }
302
+ return response;
303
+ }
304
+ };
305
+ })
306
+ .config(['$httpProvider', function($httpProvider) {
307
+ $httpProvider.interceptors.push('terseInterceptor');
308
+ }]);
309
+ ```
310
+
311
+ ### jQuery
312
+
313
+ ```javascript
314
+ import { setupJQueryAjax } from 'tersejson/integrations';
315
+
316
+ // One-time setup
317
+ setupJQueryAjax($);
318
+
319
+ // All jQuery AJAX calls now support TerseJSON
320
+ $.get('/api/users', function(data) {
321
+ console.log(data[0].firstName); // Works!
322
+ });
323
+ ```
324
+
325
+ ### SWR (React)
326
+
327
+ ```typescript
328
+ import useSWR from 'swr';
329
+ import { createSWRFetcher } from 'tersejson/integrations';
330
+
331
+ const fetcher = createSWRFetcher();
332
+
333
+ function UserList() {
334
+ const { data, error } = useSWR('/api/users', fetcher);
335
+
336
+ if (error) return <div>Error loading</div>;
337
+ if (!data) return <div>Loading...</div>;
338
+
339
+ return (
340
+ <ul>
341
+ {data.map(user => (
342
+ <li key={user.id}>{user.firstName}</li>
343
+ ))}
344
+ </ul>
345
+ );
346
+ }
347
+ ```
348
+
349
+ ### React Query / TanStack Query
350
+
351
+ ```typescript
352
+ import { useQuery } from '@tanstack/react-query';
353
+ import { createQueryFn } from 'tersejson/integrations';
354
+
355
+ const queryFn = createQueryFn();
356
+
357
+ function UserList() {
358
+ const { data } = useQuery({
359
+ queryKey: ['users'],
360
+ queryFn: () => queryFn('/api/users')
361
+ });
362
+
363
+ return <div>{data?.[0].firstName}</div>;
364
+ }
365
+ ```
366
+
367
+ ## Analytics (Opt-in)
368
+
369
+ TerseJSON includes optional analytics to track your compression savings.
370
+
371
+ ### Local Analytics
372
+
373
+ Track compression stats without sending data anywhere:
374
+
375
+ ```typescript
376
+ import { terse } from 'tersejson/express';
377
+ import { analytics } from 'tersejson/analytics';
378
+
379
+ // Enable local-only analytics
380
+ app.use(terse({ analytics: true }));
381
+
382
+ // Or with custom callbacks
383
+ app.use(terse({
384
+ analytics: {
385
+ enabled: true,
386
+ onEvent: (event) => {
387
+ console.log(`Saved ${event.originalSize - event.compressedSize} bytes`);
388
+ },
389
+ },
390
+ }));
391
+
392
+ // Check your savings anytime
393
+ setInterval(() => {
394
+ console.log(analytics.getSummary());
395
+ // "TerseJSON Stats: 1,234 compressions, 847KB saved (73.2% avg)"
396
+ }, 60000);
397
+ ```
398
+
399
+ ### Cloud Analytics (tersejson.com)
400
+
401
+ Get a dashboard with your compression stats at tersejson.com:
402
+
403
+ ```typescript
404
+ app.use(terse({
405
+ analytics: {
406
+ apiKey: 'your-api-key', // Get one at tersejson.com/dashboard
407
+ projectId: 'my-app',
408
+ reportToCloud: true,
409
+ },
410
+ }));
411
+ ```
412
+
413
+ Dashboard features:
414
+ - Real-time compression stats
415
+ - Bandwidth savings over time
416
+ - Per-endpoint analytics
417
+ - Team sharing
418
+
419
+ ### Privacy
420
+
421
+ - Analytics are **100% opt-in**
422
+ - Endpoint paths are hashed (no sensitive data)
423
+ - No request/response content is ever collected
424
+ - Only aggregate stats are reported
425
+
426
+ ## FAQ
427
+
428
+ ### Does this work with nested objects?
429
+
430
+ Yes! TerseJSON recursively compresses nested objects and arrays:
431
+
432
+ ```javascript
433
+ // This works
434
+ const data = [
435
+ {
436
+ user: { firstName: "John", lastName: "Doe" },
437
+ orders: [
438
+ { productName: "Widget", quantity: 5 }
439
+ ]
440
+ }
441
+ ];
442
+ ```
443
+
444
+ ### What about non-array responses?
445
+
446
+ TerseJSON only compresses arrays of objects (where key compression makes sense). Single objects or primitives pass through unchanged.
447
+
448
+ ### Does this break JSON.stringify on the client?
449
+
450
+ No! The Proxy is transparent. `JSON.stringify(data)` works and outputs the original key names.
451
+
452
+ ### What's the performance overhead?
453
+
454
+ Minimal. Key mapping is O(n) and Proxy access adds negligible overhead. The bandwidth savings far outweigh the processing cost.
455
+
456
+ ### Can I use this with GraphQL?
457
+
458
+ TerseJSON is designed for REST APIs. GraphQL already has efficient query mechanisms.
459
+
460
+ ## Browser Support
461
+
462
+ Works in all modern browsers that support:
463
+ - `Proxy` (ES6) - Chrome 49+, Firefox 18+, Safari 10+, Edge 12+
464
+ - `fetch` - Or use a polyfill
465
+
466
+ ## Contributing
467
+
468
+ Contributions welcome! Please read our [contributing guidelines](CONTRIBUTING.md).
469
+
470
+ ## License
471
+
472
+ MIT - see [LICENSE](LICENSE)
473
+
474
+ ---
475
+
476
+ **[tersejson.com](https://tersejson.com)** | Made with bandwidth in mind
@@ -0,0 +1,186 @@
1
+ /**
2
+ * TerseJSON Analytics
3
+ *
4
+ * Opt-in analytics to track compression savings.
5
+ * Data is anonymous and helps improve the library.
6
+ */
7
+ /**
8
+ * Compression event data
9
+ */
10
+ interface CompressionEvent {
11
+ /** Timestamp of the compression */
12
+ timestamp: number;
13
+ /** Original payload size in bytes */
14
+ originalSize: number;
15
+ /** Compressed payload size in bytes */
16
+ compressedSize: number;
17
+ /** Number of objects in the array */
18
+ objectCount: number;
19
+ /** Number of keys compressed */
20
+ keysCompressed: number;
21
+ /** Route/endpoint (optional, anonymized) */
22
+ endpoint?: string;
23
+ /** Key pattern used */
24
+ keyPattern: string;
25
+ }
26
+ /**
27
+ * Aggregated stats for reporting
28
+ */
29
+ interface AnalyticsStats {
30
+ /** Total compression events */
31
+ totalEvents: number;
32
+ /** Total bytes before compression */
33
+ totalOriginalBytes: number;
34
+ /** Total bytes after compression */
35
+ totalCompressedBytes: number;
36
+ /** Total bytes saved */
37
+ totalBytesSaved: number;
38
+ /** Average compression ratio (0-1) */
39
+ averageRatio: number;
40
+ /** Total objects processed */
41
+ totalObjects: number;
42
+ /** Session start time */
43
+ sessionStart: number;
44
+ /** Last event time */
45
+ lastEvent: number;
46
+ }
47
+ /**
48
+ * Analytics configuration
49
+ */
50
+ interface AnalyticsConfig {
51
+ /**
52
+ * Enable analytics collection
53
+ * @default false
54
+ */
55
+ enabled: boolean;
56
+ /**
57
+ * Send anonymous stats to tersejson.com
58
+ * Helps improve the library
59
+ * @default false
60
+ */
61
+ reportToCloud: boolean;
62
+ /**
63
+ * API key for tersejson.com (optional)
64
+ * Get one at tersejson.com/dashboard
65
+ */
66
+ apiKey?: string;
67
+ /**
68
+ * Project/site identifier (optional)
69
+ */
70
+ projectId?: string;
71
+ /**
72
+ * Callback for each compression event
73
+ * Use for custom logging/monitoring
74
+ */
75
+ onEvent?: (event: CompressionEvent) => void;
76
+ /**
77
+ * Callback for periodic stats summary
78
+ */
79
+ onStats?: (stats: AnalyticsStats) => void;
80
+ /**
81
+ * How often to report stats (ms)
82
+ * @default 60000 (1 minute)
83
+ */
84
+ reportInterval?: number;
85
+ /**
86
+ * Include endpoint paths in analytics
87
+ * Paths are hashed for privacy
88
+ * @default false
89
+ */
90
+ trackEndpoints?: boolean;
91
+ /**
92
+ * Cloud reporting endpoint
93
+ * @default 'https://api.tersejson.com/v1/analytics'
94
+ */
95
+ endpoint?: string;
96
+ /**
97
+ * Enable debug logging
98
+ * @default false
99
+ */
100
+ debug?: boolean;
101
+ }
102
+ /**
103
+ * Analytics collector class
104
+ */
105
+ declare class TerseAnalytics {
106
+ private config;
107
+ private events;
108
+ private stats;
109
+ private reportTimer?;
110
+ private isNode;
111
+ constructor(config?: Partial<AnalyticsConfig>);
112
+ /**
113
+ * Create empty stats object
114
+ */
115
+ private createEmptyStats;
116
+ /**
117
+ * Record a compression event
118
+ */
119
+ record(event: Omit<CompressionEvent, 'timestamp'>): void;
120
+ /**
121
+ * Get current stats
122
+ */
123
+ getStats(): AnalyticsStats;
124
+ /**
125
+ * Get formatted stats summary
126
+ */
127
+ getSummary(): string;
128
+ /**
129
+ * Reset stats
130
+ */
131
+ reset(): void;
132
+ /**
133
+ * Start periodic reporting to cloud
134
+ */
135
+ private startReporting;
136
+ /**
137
+ * Stop reporting
138
+ */
139
+ stop(): void;
140
+ /**
141
+ * Report stats to tersejson.com
142
+ */
143
+ private reportToCloud;
144
+ /**
145
+ * Hash endpoint for privacy
146
+ */
147
+ private hashEndpoint;
148
+ }
149
+ /**
150
+ * Initialize global analytics
151
+ */
152
+ declare function initAnalytics(config: Partial<AnalyticsConfig>): TerseAnalytics;
153
+ /**
154
+ * Get global analytics instance
155
+ */
156
+ declare function getAnalytics(): TerseAnalytics | null;
157
+ /**
158
+ * Record an event to global analytics (if initialized)
159
+ */
160
+ declare function recordEvent(event: Omit<CompressionEvent, 'timestamp'>): void;
161
+ /**
162
+ * Quick setup for common use cases
163
+ */
164
+ declare const analytics: {
165
+ /**
166
+ * Enable local-only analytics (no cloud reporting)
167
+ */
168
+ local(options?: {
169
+ debug?: boolean;
170
+ onEvent?: AnalyticsConfig["onEvent"];
171
+ }): TerseAnalytics;
172
+ /**
173
+ * Enable cloud analytics with API key
174
+ */
175
+ cloud(apiKey: string, options?: Partial<AnalyticsConfig>): TerseAnalytics;
176
+ /**
177
+ * Get current stats
178
+ */
179
+ getStats(): AnalyticsStats | null;
180
+ /**
181
+ * Get formatted summary
182
+ */
183
+ getSummary(): string;
184
+ };
185
+
186
+ export { type AnalyticsConfig, type AnalyticsStats, type CompressionEvent, TerseAnalytics, analytics, analytics as default, getAnalytics, initAnalytics, recordEvent };