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 +21 -0
- package/README.md +476 -0
- package/dist/analytics.d.mts +186 -0
- package/dist/analytics.d.ts +186 -0
- package/dist/analytics.js +226 -0
- package/dist/analytics.js.map +1 -0
- package/dist/analytics.mjs +217 -0
- package/dist/analytics.mjs.map +1 -0
- package/dist/client-BQAZg7I8.d.mts +138 -0
- package/dist/client-DOOGwp_p.d.ts +138 -0
- package/dist/client.d.mts +2 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +200 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +188 -0
- package/dist/client.mjs.map +1 -0
- package/dist/express-BoL__Ao6.d.mts +67 -0
- package/dist/express-LSVylWpN.d.ts +67 -0
- package/dist/express.d.mts +4 -0
- package/dist/express.d.ts +4 -0
- package/dist/express.js +522 -0
- package/dist/express.js.map +1 -0
- package/dist/express.mjs +512 -0
- package/dist/express.mjs.map +1 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1001 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +985 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations-7WeFO1Lk.d.mts +264 -0
- package/dist/integrations-7WeFO1Lk.d.ts +264 -0
- package/dist/integrations.d.mts +1 -0
- package/dist/integrations.d.ts +1 -0
- package/dist/integrations.js +381 -0
- package/dist/integrations.js.map +1 -0
- package/dist/integrations.mjs +367 -0
- package/dist/integrations.mjs.map +1 -0
- package/dist/types-CzaGQaV7.d.mts +134 -0
- package/dist/types-CzaGQaV7.d.ts +134 -0
- package/package.json +87 -0
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
|
+
[](https://www.npmjs.com/package/tersejson)
|
|
6
|
+
[](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 };
|