tersejson 0.2.0 → 0.3.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/README.md +173 -322
- package/dist/server-memory.d.mts +167 -0
- package/dist/server-memory.d.ts +167 -0
- package/dist/server-memory.js +455 -0
- package/dist/server-memory.js.map +1 -0
- package/dist/server-memory.mjs +451 -0
- package/dist/server-memory.mjs.map +1 -0
- package/package.json +13 -9
package/README.md
CHANGED
|
@@ -1,41 +1,71 @@
|
|
|
1
1
|
# TerseJSON
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Memory-efficient JSON processing. Lazy Proxy expansion uses 70% less RAM than JSON.parse.**
|
|
4
|
+
|
|
5
|
+
> TerseJSON does **LESS work** than JSON.parse, not more. The Proxy skips full deserialization - only accessed fields allocate memory. Plus 30-80% smaller payloads.
|
|
4
6
|
|
|
5
7
|
[](https://www.npmjs.com/package/tersejson)
|
|
6
8
|
[](https://opensource.org/licenses/MIT)
|
|
7
9
|
|
|
8
10
|
## The Problem
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
Your CMS API returns 21 fields per article. Your list view renders 3.
|
|
11
13
|
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
```javascript
|
|
15
|
+
// Standard JSON.parse workflow:
|
|
16
|
+
const articles = await fetch('/api/articles').then(r => r.json());
|
|
17
|
+
// Result: 1000 objects x 21 fields = 21,000 properties allocated in memory
|
|
18
|
+
// You use: title, slug, excerpt (3 fields)
|
|
19
|
+
// Wasted: 18,000 properties that need garbage collection
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
**Full deserialization wastes memory.** Every field gets allocated whether you access it or not. Binary formats (Protobuf, MessagePack) have the same problem - they require complete deserialization.
|
|
21
23
|
|
|
22
24
|
## The Solution
|
|
23
25
|
|
|
24
|
-
TerseJSON
|
|
26
|
+
TerseJSON's Proxy wraps compressed data and translates keys **on-demand**:
|
|
25
27
|
|
|
28
|
+
```javascript
|
|
29
|
+
// TerseJSON workflow:
|
|
30
|
+
const articles = await terseFetch('/api/articles');
|
|
31
|
+
// Result: Compressed payload + Proxy wrapper
|
|
32
|
+
// Access: article.title → translates key, returns value
|
|
33
|
+
// Never accessed: 18 other fields stay compressed, never allocate
|
|
26
34
|
```
|
|
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
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
**Memory Benchmarks (1000 records, 21 fields each):**
|
|
37
|
+
|
|
38
|
+
| Fields Accessed | Normal JSON | TerseJSON Proxy | Memory Saved |
|
|
39
|
+
|-----------------|-------------|-----------------|--------------|
|
|
40
|
+
| 1 field | 6.35 MB | 4.40 MB | **31%** |
|
|
41
|
+
| 3 fields (list view) | 3.07 MB | ~0 MB | **~100%** |
|
|
42
|
+
| 6 fields (card view) | 3.07 MB | ~0 MB | **~100%** |
|
|
43
|
+
| All 21 fields | 4.53 MB | 1.36 MB | **70%** |
|
|
44
|
+
|
|
45
|
+
*Run the benchmark yourself: `node --expose-gc demo/memory-analysis.js`*
|
|
46
|
+
|
|
47
|
+
## "Doesn't This Add Overhead?"
|
|
48
|
+
|
|
49
|
+
This is the most common misconception. Let's trace the actual operations:
|
|
50
|
+
|
|
51
|
+
**Standard JSON.parse workflow:**
|
|
52
|
+
1. Parse 890KB string → allocate 1000 objects x 21 fields = **21,000 properties**
|
|
53
|
+
2. Access 3 fields per object
|
|
54
|
+
3. GC eventually collects 18,000 unused properties
|
|
55
|
+
|
|
56
|
+
**TerseJSON workflow:**
|
|
57
|
+
1. Parse 180KB string (smaller = faster) → allocate 1000 objects x 21 SHORT keys
|
|
58
|
+
2. Wrap in Proxy (O(1), ~0.1ms, no allocation)
|
|
59
|
+
3. Access 3 fields → **3,000 properties CREATED**
|
|
60
|
+
4. 18,000 properties **NEVER EXIST**
|
|
61
|
+
|
|
62
|
+
**The math:**
|
|
63
|
+
- Parse time: Smaller string (180KB vs 890KB) = **faster**
|
|
64
|
+
- Allocations: 3,000 vs 21,000 = **86% fewer**
|
|
65
|
+
- GC pressure: Only 3,000 objects to collect vs 21,000
|
|
66
|
+
- Proxy lookup: O(1) Map access, ~0.001ms per field
|
|
67
|
+
|
|
68
|
+
**Result:** LESS total work, not more. The Proxy doesn't add overhead - it **skips** work.
|
|
39
69
|
|
|
40
70
|
## Quick Start
|
|
41
71
|
|
|
@@ -68,98 +98,126 @@ import { fetch } from 'tersejson/client';
|
|
|
68
98
|
// Use exactly like regular fetch
|
|
69
99
|
const users = await fetch('/api/users').then(r => r.json());
|
|
70
100
|
|
|
71
|
-
// Access properties normally -
|
|
101
|
+
// Access properties normally - Proxy handles key translation
|
|
72
102
|
console.log(users[0].firstName); // Works transparently!
|
|
73
103
|
console.log(users[0].emailAddress); // Works transparently!
|
|
74
104
|
```
|
|
75
105
|
|
|
76
106
|
## How It Works
|
|
77
107
|
|
|
78
|
-
### Compression Flow
|
|
79
|
-
|
|
80
108
|
```
|
|
81
109
|
┌─────────────────────────────────────────────────────────────┐
|
|
110
|
+
│ SERVER │
|
|
82
111
|
│ 1. Your Express route calls res.json(data) │
|
|
83
|
-
│
|
|
84
|
-
│
|
|
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 │
|
|
112
|
+
│ 2. TerseJSON middleware intercepts │
|
|
113
|
+
│ 3. Compresses keys: { "a": "firstName", "b": "lastName" } │
|
|
114
|
+
│ 4. Sends smaller payload (180KB vs 890KB) │
|
|
93
115
|
└─────────────────────────────────────────────────────────────┘
|
|
94
|
-
↓
|
|
116
|
+
↓ Network (smaller, faster)
|
|
95
117
|
┌─────────────────────────────────────────────────────────────┐
|
|
96
|
-
│
|
|
97
|
-
│
|
|
98
|
-
│
|
|
99
|
-
│
|
|
100
|
-
│
|
|
101
|
-
│ ↓ │
|
|
102
|
-
│ 10. Your code accesses data.firstName → mapped to data.a │
|
|
118
|
+
│ CLIENT │
|
|
119
|
+
│ 5. JSON.parse smaller string (faster) │
|
|
120
|
+
│ 6. Wrap in Proxy (O(1), near-zero cost) │
|
|
121
|
+
│ 7. Access data.firstName → Proxy translates to data.a │
|
|
122
|
+
│ 8. Unused fields never materialize in memory │
|
|
103
123
|
└─────────────────────────────────────────────────────────────┘
|
|
104
124
|
```
|
|
105
125
|
|
|
106
|
-
|
|
126
|
+
## Perfect For
|
|
127
|
+
|
|
128
|
+
- **CMS list views** - title + slug + excerpt from 20+ field objects
|
|
129
|
+
- **Dashboards** - large datasets, aggregate calculations on subsets
|
|
130
|
+
- **Mobile apps** - memory constrained, infinite scroll
|
|
131
|
+
- **E-commerce** - product grids (name + price + image from 30+ field objects)
|
|
132
|
+
- **Long-running SPAs** - memory accumulation over hours (support tools, dashboards)
|
|
107
133
|
|
|
108
|
-
|
|
134
|
+
## Network Savings (Bonus)
|
|
109
135
|
|
|
110
|
-
|
|
111
|
-
|----------|----------|----------------|---------|
|
|
112
|
-
| 100 users, 10 fields | 45 KB | 12 KB | **73%** |
|
|
113
|
-
| 1000 products, 15 fields | 890 KB | 180 KB | **80%** |
|
|
114
|
-
| 10000 logs, 8 fields | 2.1 MB | 450 KB | **79%** |
|
|
136
|
+
Memory efficiency is the headline. Smaller payloads are the bonus:
|
|
115
137
|
|
|
116
|
-
|
|
138
|
+
| Compression Method | Reduction | Use Case |
|
|
139
|
+
|--------------------|-----------|----------|
|
|
140
|
+
| TerseJSON alone | **30-39%** | Sites without Gzip (68% of web) |
|
|
141
|
+
| Gzip alone | ~75% | Large payloads (>32KB) |
|
|
142
|
+
| **TerseJSON + Gzip** | **~85%** | Recommended for production |
|
|
143
|
+
| **TerseJSON + Brotli** | **~93%** | Maximum compression |
|
|
117
144
|
|
|
118
|
-
**
|
|
145
|
+
**Network speed impact (1000-record payload):**
|
|
119
146
|
|
|
120
|
-
|
|
|
121
|
-
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
|
|
|
147
|
+
| Network | Normal JSON | TerseJSON + Gzip | Saved |
|
|
148
|
+
|---------|-------------|------------------|-------|
|
|
149
|
+
| 4G (20 Mbps) | 200ms | 30ms | **170ms** |
|
|
150
|
+
| 3G (2 Mbps) | 2,000ms | 300ms | **1,700ms** |
|
|
151
|
+
| Slow 3G | 10,000ms | 1,500ms | **8,500ms** |
|
|
125
152
|
|
|
126
|
-
|
|
153
|
+
## Why Gzip Isn't Enough
|
|
127
154
|
|
|
128
|
-
**
|
|
155
|
+
**"Just use gzip"** misses two points:
|
|
129
156
|
|
|
130
|
-
|
|
131
|
-
|---------|-----------------|---------------|-----------------|
|
|
132
|
-
| 1M requests/day | 40 KB | **40 GB** | **1.2 TB** |
|
|
133
|
-
| 10M requests/day | 40 KB | **400 GB** | **12 TB** |
|
|
134
|
-
| 100M requests/day | 40 KB | **4 TB** | **120 TB** |
|
|
157
|
+
1. **68% of websites don't have Gzip enabled** ([W3Techs](https://w3techs.com/technologies/details/ce-gzipcompression)). Proxy defaults are hostile - nginx, Traefik, Kubernetes all ship with compression off.
|
|
135
158
|
|
|
136
|
-
|
|
159
|
+
2. **Gzip doesn't help memory.** Even with perfect compression over the wire, JSON.parse still allocates every field. TerseJSON's Proxy keeps unused fields compressed in memory.
|
|
160
|
+
|
|
161
|
+
**TerseJSON works at the application layer:**
|
|
162
|
+
- No proxy config needed
|
|
163
|
+
- No DevOps tickets
|
|
164
|
+
- Stacks with gzip/brotli for maximum savings
|
|
165
|
+
- **Plus** memory benefits that gzip can't provide
|
|
166
|
+
|
|
167
|
+
## vs Binary Formats (Protobuf, MessagePack)
|
|
168
|
+
|
|
169
|
+
| | TerseJSON | Protobuf/MessagePack |
|
|
170
|
+
|---|-----------|---------------------|
|
|
171
|
+
| Wire compression | 30-80% | 80-90% |
|
|
172
|
+
| **Memory on partial access** | **Only accessed fields** | Full deserialization required |
|
|
173
|
+
| Schema required | No | Yes |
|
|
174
|
+
| Human-readable | Yes (JSON in DevTools) | No (binary) |
|
|
175
|
+
| Migration effort | 2 minutes | Days/weeks |
|
|
176
|
+
| Debugging | Easy | Need special tools |
|
|
177
|
+
|
|
178
|
+
**Binary formats win on wire size. TerseJSON wins on memory.**
|
|
179
|
+
|
|
180
|
+
If you access 3 fields from a 21-field object:
|
|
181
|
+
- Protobuf: All 21 fields deserialized into memory
|
|
182
|
+
- TerseJSON: Only 3 fields materialize
|
|
183
|
+
|
|
184
|
+
## Server-Side Memory Optimization
|
|
185
|
+
|
|
186
|
+
TerseJSON includes utilities for memory-efficient server-side data handling:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { TerseCache, compressStream } from 'tersejson/server-memory';
|
|
190
|
+
|
|
191
|
+
// Memory-efficient caching - stores compressed, expands on access
|
|
192
|
+
const cache = new TerseCache();
|
|
193
|
+
cache.set('users', largeUserArray);
|
|
194
|
+
const users = cache.get('users'); // Returns Proxy-wrapped data
|
|
195
|
+
|
|
196
|
+
// Streaming compression for database cursors
|
|
197
|
+
const cursor = db.collection('users').find().stream();
|
|
198
|
+
for await (const batch of compressStream(cursor, { batchSize: 100 })) {
|
|
199
|
+
// Process compressed batches without loading entire result set
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Inter-service communication - pass compressed data without intermediate expansion
|
|
203
|
+
import { createTerseServiceClient } from 'tersejson/server-memory';
|
|
204
|
+
const serviceB = createTerseServiceClient({ baseUrl: 'http://service-b' });
|
|
205
|
+
const data = await serviceB.get('/api/users'); // Returns Proxy-wrapped
|
|
206
|
+
```
|
|
137
207
|
|
|
138
208
|
## API Reference
|
|
139
209
|
|
|
140
210
|
### Express Middleware
|
|
141
211
|
|
|
142
212
|
```typescript
|
|
143
|
-
import { terse
|
|
144
|
-
|
|
145
|
-
// Basic usage
|
|
146
|
-
app.use(terse());
|
|
213
|
+
import { terse } from 'tersejson/express';
|
|
147
214
|
|
|
148
|
-
// With options
|
|
149
215
|
app.use(terse({
|
|
150
216
|
minArrayLength: 5, // Only compress arrays with 5+ items
|
|
151
217
|
minKeyLength: 4, // Only compress keys with 4+ characters
|
|
152
218
|
maxDepth: 5, // Max nesting depth to traverse
|
|
153
219
|
debug: true, // Log compression stats
|
|
154
|
-
headerName: 'x-terse', // Custom header name
|
|
155
|
-
shouldCompress: (data, req) => {
|
|
156
|
-
// Custom logic to skip compression
|
|
157
|
-
return !req.path.includes('/admin');
|
|
158
|
-
},
|
|
159
220
|
}));
|
|
160
|
-
|
|
161
|
-
// Enable via query parameter (?terse=true)
|
|
162
|
-
app.use(terseQueryParam());
|
|
163
221
|
```
|
|
164
222
|
|
|
165
223
|
### Client Library
|
|
@@ -171,23 +229,11 @@ import {
|
|
|
171
229
|
expand, // Fully expand a terse payload
|
|
172
230
|
proxy, // Wrap payload with Proxy (default)
|
|
173
231
|
process, // Auto-detect and expand/proxy
|
|
174
|
-
axiosInterceptor // Axios support
|
|
175
232
|
} from 'tersejson/client';
|
|
176
233
|
|
|
177
234
|
// Drop-in fetch replacement
|
|
178
235
|
const data = await fetch('/api/users').then(r => r.json());
|
|
179
236
|
|
|
180
|
-
// Custom fetch instance
|
|
181
|
-
const customFetch = createFetch({
|
|
182
|
-
debug: true,
|
|
183
|
-
autoExpand: true,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Axios integration
|
|
187
|
-
import axios from 'axios';
|
|
188
|
-
axios.interceptors.request.use(axiosInterceptor.request);
|
|
189
|
-
axios.interceptors.response.use(axiosInterceptor.response);
|
|
190
|
-
|
|
191
237
|
// Manual processing
|
|
192
238
|
import { process } from 'tersejson/client';
|
|
193
239
|
const response = await regularFetch('/api/users');
|
|
@@ -199,51 +245,21 @@ const data = process(await response.json());
|
|
|
199
245
|
```typescript
|
|
200
246
|
import {
|
|
201
247
|
compress, // Compress an array of objects
|
|
202
|
-
expand, // Expand a terse payload
|
|
203
|
-
|
|
248
|
+
expand, // Expand a terse payload (full deserialization)
|
|
249
|
+
wrapWithProxy, // Wrap payload with Proxy (lazy expansion - recommended)
|
|
204
250
|
isTersePayload, // Check if data is a terse payload
|
|
205
|
-
createTerseProxy, // Create a Proxy for transparent access
|
|
206
251
|
} from 'tersejson';
|
|
207
252
|
|
|
208
253
|
// Manual compression
|
|
209
254
|
const compressed = compress(users, { minKeyLength: 3 });
|
|
210
255
|
|
|
211
|
-
//
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
// Type checking
|
|
215
|
-
if (isTersePayload(data)) {
|
|
216
|
-
const expanded = expand(data);
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
## TypeScript Support
|
|
221
|
-
|
|
222
|
-
TerseJSON is written in TypeScript and provides full type definitions:
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
import type {
|
|
226
|
-
TersePayload,
|
|
227
|
-
TerseMiddlewareOptions,
|
|
228
|
-
TerseClientOptions,
|
|
229
|
-
Tersed,
|
|
230
|
-
} from 'tersejson';
|
|
231
|
-
|
|
232
|
-
interface User {
|
|
233
|
-
firstName: string;
|
|
234
|
-
lastName: string;
|
|
235
|
-
email: string;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Types flow through compression
|
|
239
|
-
const users: User[] = await fetch('/api/users').then(r => r.json());
|
|
240
|
-
users[0].firstName; // TypeScript knows this is a string
|
|
256
|
+
// Two expansion strategies:
|
|
257
|
+
const expanded = expand(compressed); // Full expansion - all fields allocated
|
|
258
|
+
const proxied = wrapWithProxy(compressed); // Lazy expansion - only accessed fields
|
|
241
259
|
```
|
|
242
260
|
|
|
243
261
|
## Framework Integrations
|
|
244
262
|
|
|
245
|
-
TerseJSON provides ready-to-use integrations for popular HTTP clients and frameworks.
|
|
246
|
-
|
|
247
263
|
### Axios
|
|
248
264
|
|
|
249
265
|
```typescript
|
|
@@ -253,95 +269,6 @@ import { createAxiosInterceptors } from 'tersejson/integrations';
|
|
|
253
269
|
const { request, response } = createAxiosInterceptors();
|
|
254
270
|
axios.interceptors.request.use(request);
|
|
255
271
|
axios.interceptors.response.use(response);
|
|
256
|
-
|
|
257
|
-
// Now all axios requests automatically handle TerseJSON!
|
|
258
|
-
const { data } = await axios.get('/api/users');
|
|
259
|
-
console.log(data[0].firstName); // Works transparently!
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### Angular (HttpClient)
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
// terse.interceptor.ts
|
|
266
|
-
import { Injectable } from '@angular/core';
|
|
267
|
-
import {
|
|
268
|
-
HttpInterceptor,
|
|
269
|
-
HttpRequest,
|
|
270
|
-
HttpHandler,
|
|
271
|
-
HttpEvent,
|
|
272
|
-
HttpResponse
|
|
273
|
-
} from '@angular/common/http';
|
|
274
|
-
import { Observable } from 'rxjs';
|
|
275
|
-
import { map } from 'rxjs/operators';
|
|
276
|
-
import { isTersePayload, wrapWithProxy } from 'tersejson';
|
|
277
|
-
|
|
278
|
-
@Injectable()
|
|
279
|
-
export class TerseInterceptor implements HttpInterceptor {
|
|
280
|
-
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
|
281
|
-
// Add accept-terse header
|
|
282
|
-
const terseReq = req.clone({
|
|
283
|
-
setHeaders: { 'accept-terse': 'true' }
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
return next.handle(terseReq).pipe(
|
|
287
|
-
map(event => {
|
|
288
|
-
if (event instanceof HttpResponse && event.body) {
|
|
289
|
-
const isTerse = event.headers.get('x-terse-json') === 'true';
|
|
290
|
-
if (isTerse && isTersePayload(event.body)) {
|
|
291
|
-
return event.clone({ body: wrapWithProxy(event.body) });
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return event;
|
|
295
|
-
})
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// app.module.ts
|
|
301
|
-
@NgModule({
|
|
302
|
-
providers: [
|
|
303
|
-
{ provide: HTTP_INTERCEPTORS, useClass: TerseInterceptor, multi: true }
|
|
304
|
-
]
|
|
305
|
-
})
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
### AngularJS (1.x)
|
|
309
|
-
|
|
310
|
-
```javascript
|
|
311
|
-
angular.module('myApp', [])
|
|
312
|
-
.factory('terseInterceptor', function() {
|
|
313
|
-
return {
|
|
314
|
-
request: function(config) {
|
|
315
|
-
config.headers = config.headers || {};
|
|
316
|
-
config.headers['accept-terse'] = 'true';
|
|
317
|
-
return config;
|
|
318
|
-
},
|
|
319
|
-
response: function(response) {
|
|
320
|
-
var isTerse = response.headers('x-terse-json') === 'true';
|
|
321
|
-
if (isTerse && response.data && response.data.__terse__) {
|
|
322
|
-
response.data = tersejson.process(response.data);
|
|
323
|
-
}
|
|
324
|
-
return response;
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
})
|
|
328
|
-
.config(['$httpProvider', function($httpProvider) {
|
|
329
|
-
$httpProvider.interceptors.push('terseInterceptor');
|
|
330
|
-
}]);
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
### jQuery
|
|
334
|
-
|
|
335
|
-
```javascript
|
|
336
|
-
import { setupJQueryAjax } from 'tersejson/integrations';
|
|
337
|
-
|
|
338
|
-
// One-time setup
|
|
339
|
-
setupJQueryAjax($);
|
|
340
|
-
|
|
341
|
-
// All jQuery AJAX calls now support TerseJSON
|
|
342
|
-
$.get('/api/users', function(data) {
|
|
343
|
-
console.log(data[0].firstName); // Works!
|
|
344
|
-
});
|
|
345
272
|
```
|
|
346
273
|
|
|
347
274
|
### SWR (React)
|
|
@@ -353,18 +280,8 @@ import { createSWRFetcher } from 'tersejson/integrations';
|
|
|
353
280
|
const fetcher = createSWRFetcher();
|
|
354
281
|
|
|
355
282
|
function UserList() {
|
|
356
|
-
const { data
|
|
357
|
-
|
|
358
|
-
if (error) return <div>Error loading</div>;
|
|
359
|
-
if (!data) return <div>Loading...</div>;
|
|
360
|
-
|
|
361
|
-
return (
|
|
362
|
-
<ul>
|
|
363
|
-
{data.map(user => (
|
|
364
|
-
<li key={user.id}>{user.firstName}</li>
|
|
365
|
-
))}
|
|
366
|
-
</ul>
|
|
367
|
-
);
|
|
283
|
+
const { data } = useSWR('/api/users', fetcher);
|
|
284
|
+
return <ul>{data?.map(user => <li>{user.firstName}</li>)}</ul>;
|
|
368
285
|
}
|
|
369
286
|
```
|
|
370
287
|
|
|
@@ -381,130 +298,64 @@ function UserList() {
|
|
|
381
298
|
queryKey: ['users'],
|
|
382
299
|
queryFn: () => queryFn('/api/users')
|
|
383
300
|
});
|
|
384
|
-
|
|
385
301
|
return <div>{data?.[0].firstName}</div>;
|
|
386
302
|
}
|
|
387
303
|
```
|
|
388
304
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
TerseJSON includes optional analytics to track your compression savings.
|
|
392
|
-
|
|
393
|
-
### Local Analytics
|
|
394
|
-
|
|
395
|
-
Track compression stats without sending data anywhere:
|
|
305
|
+
### GraphQL (Apollo)
|
|
396
306
|
|
|
397
307
|
```typescript
|
|
398
|
-
|
|
399
|
-
import {
|
|
400
|
-
|
|
401
|
-
// Enable local-only analytics
|
|
402
|
-
app.use(terse({ analytics: true }));
|
|
403
|
-
|
|
404
|
-
// Or with custom callbacks
|
|
405
|
-
app.use(terse({
|
|
406
|
-
analytics: {
|
|
407
|
-
enabled: true,
|
|
408
|
-
onEvent: (event) => {
|
|
409
|
-
console.log(`Saved ${event.originalSize - event.compressedSize} bytes`);
|
|
410
|
-
},
|
|
411
|
-
},
|
|
412
|
-
}));
|
|
308
|
+
// Server
|
|
309
|
+
import { terseGraphQL } from 'tersejson/graphql';
|
|
310
|
+
app.use('/graphql', terseGraphQL(graphqlHTTP({ schema })));
|
|
413
311
|
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
312
|
+
// Client
|
|
313
|
+
import { createTerseLink } from 'tersejson/graphql-client';
|
|
314
|
+
const client = new ApolloClient({
|
|
315
|
+
link: from([createTerseLink(), httpLink]),
|
|
316
|
+
cache: new InMemoryCache(),
|
|
317
|
+
});
|
|
419
318
|
```
|
|
420
319
|
|
|
421
|
-
|
|
320
|
+
## TypeScript Support
|
|
422
321
|
|
|
423
|
-
|
|
322
|
+
Full type definitions included:
|
|
424
323
|
|
|
425
324
|
```typescript
|
|
426
|
-
|
|
427
|
-
analytics: {
|
|
428
|
-
apiKey: 'your-api-key', // Get one at tersejson.com/dashboard
|
|
429
|
-
projectId: 'my-app',
|
|
430
|
-
reportToCloud: true,
|
|
431
|
-
},
|
|
432
|
-
}));
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
Dashboard features:
|
|
436
|
-
- Real-time compression stats
|
|
437
|
-
- Bandwidth savings over time
|
|
438
|
-
- Per-endpoint analytics
|
|
439
|
-
- Team sharing
|
|
325
|
+
import type { TersePayload, Tersed } from 'tersejson';
|
|
440
326
|
|
|
441
|
-
|
|
327
|
+
interface User {
|
|
328
|
+
firstName: string;
|
|
329
|
+
lastName: string;
|
|
330
|
+
}
|
|
442
331
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
- Only aggregate stats are reported
|
|
332
|
+
const users: User[] = await fetch('/api/users').then(r => r.json());
|
|
333
|
+
users[0].firstName; // TypeScript knows this is a string
|
|
334
|
+
```
|
|
447
335
|
|
|
448
336
|
## FAQ
|
|
449
337
|
|
|
450
|
-
### Does this
|
|
451
|
-
|
|
452
|
-
Yes! TerseJSON recursively compresses nested objects and arrays:
|
|
453
|
-
|
|
454
|
-
```javascript
|
|
455
|
-
// This works
|
|
456
|
-
const data = [
|
|
457
|
-
{
|
|
458
|
-
user: { firstName: "John", lastName: "Doe" },
|
|
459
|
-
orders: [
|
|
460
|
-
{ productName: "Widget", quantity: 5 }
|
|
461
|
-
]
|
|
462
|
-
}
|
|
463
|
-
];
|
|
464
|
-
```
|
|
338
|
+
### Does this break JSON.stringify?
|
|
465
339
|
|
|
466
|
-
|
|
340
|
+
No! The Proxy is transparent. `JSON.stringify(data)` outputs original key names.
|
|
467
341
|
|
|
468
|
-
|
|
342
|
+
### What about nested objects?
|
|
469
343
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
No! The Proxy is transparent. `JSON.stringify(data)` works and outputs the original key names.
|
|
344
|
+
Fully supported. TerseJSON recursively compresses nested objects and arrays.
|
|
473
345
|
|
|
474
346
|
### What's the performance overhead?
|
|
475
347
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
### Can I use this with GraphQL?
|
|
479
|
-
|
|
480
|
-
Yes! TerseJSON supports GraphQL via `express-graphql` and Apollo Client:
|
|
481
|
-
|
|
482
|
-
```typescript
|
|
483
|
-
// Server (express-graphql)
|
|
484
|
-
import { graphqlHTTP } from 'express-graphql';
|
|
485
|
-
import { terseGraphQL } from 'tersejson/graphql';
|
|
486
|
-
|
|
487
|
-
app.use('/graphql', terseGraphQL(graphqlHTTP({
|
|
488
|
-
schema: mySchema,
|
|
489
|
-
graphiql: true,
|
|
490
|
-
})));
|
|
348
|
+
Proxy mode adds **<5% CPU overhead** vs JSON.parse(). But with smaller payloads and fewer allocations, **net total work is LESS**. Memory is significantly lower.
|
|
491
349
|
|
|
492
|
-
|
|
493
|
-
import { createTerseLink } from 'tersejson/graphql-client';
|
|
494
|
-
|
|
495
|
-
const client = new ApolloClient({
|
|
496
|
-
link: from([createTerseLink(), httpLink]),
|
|
497
|
-
cache: new InMemoryCache(),
|
|
498
|
-
});
|
|
499
|
-
```
|
|
350
|
+
### When should I use expand() vs wrapWithProxy()?
|
|
500
351
|
|
|
501
|
-
|
|
352
|
+
- **wrapWithProxy()** (default): Best for most cases. Lazy expansion, lower memory.
|
|
353
|
+
- **expand()**: When you need a plain object (serialization to storage, passing to libraries that don't support Proxy).
|
|
502
354
|
|
|
503
355
|
## Browser Support
|
|
504
356
|
|
|
505
|
-
Works in all modern browsers
|
|
506
|
-
-
|
|
507
|
-
- `fetch` - Or use a polyfill
|
|
357
|
+
Works in all modern browsers supporting `Proxy` (ES6):
|
|
358
|
+
- Chrome 49+, Firefox 18+, Safari 10+, Edge 12+
|
|
508
359
|
|
|
509
360
|
## Contributing
|
|
510
361
|
|
|
@@ -516,4 +367,4 @@ MIT - see [LICENSE](LICENSE)
|
|
|
516
367
|
|
|
517
368
|
---
|
|
518
369
|
|
|
519
|
-
**[tersejson.com](https://tersejson.com)** |
|
|
370
|
+
**[tersejson.com](https://tersejson.com)** | Memory-efficient JSON for high-volume APIs
|