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 CHANGED
@@ -1,41 +1,71 @@
1
1
  # TerseJSON
2
2
 
3
- **Transparent JSON key compression for Express APIs. Reduce bandwidth by up to 80% with zero code changes.**
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
  [![npm version](https://badge.fury.io/js/tersejson.svg)](https://www.npmjs.com/package/tersejson)
6
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
9
 
8
10
  ## The Problem
9
11
 
10
- Every API response repeats the same keys over and over:
12
+ Your CMS API returns 21 fields per article. Your list view renders 3.
11
13
 
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
- ]
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
- For 1000 objects, you're sending ~50KB of just repeated key names!
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 automatically compresses keys on the server and transparently expands them on the client:
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
- Your code sees (via Proxy magic):
37
- users[0].firstName // "John" - just works!
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 - TerseJSON handles the mapping
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
- 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 │
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
- 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 │
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
- ### Bandwidth Savings
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
- **Without gzip (many servers don't have it enabled):**
134
+ ## Network Savings (Bonus)
109
135
 
110
- | Scenario | Original | With TerseJSON | Savings |
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
- *Many Express apps, serverless functions, and internal APIs don't enable gzip. TerseJSON is often easier to add than configuring compression.*
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
- **With gzip already enabled:**
145
+ **Network speed impact (1000-record payload):**
119
146
 
120
- | Scenario | JSON + gzip | TerseJSON + gzip | Additional Savings |
121
- |----------|-------------|------------------|-------------------|
122
- | 100 users, 10 fields | 8.2 KB | 6.1 KB | **25%** |
123
- | 1000 products, 15 fields | 48 KB | 38 KB | **21%** |
124
- | 10000 logs, 8 fields | 185 KB | 142 KB | **23%** |
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
- *If you already use gzip, TerseJSON stacks on top for additional savings.*
153
+ ## Why Gzip Isn't Enough
127
154
 
128
- **At enterprise scale:**
155
+ **"Just use gzip"** misses two points:
129
156
 
130
- | Traffic | Savings/request | Daily Savings | Monthly Savings |
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
- *At $0.09/GB egress, 10M requests/day = ~$1,000/month saved.*
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, terseQueryParam } from 'tersejson/express';
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
- isCompressibleArray,// Check if data can be compressed
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
- // Manual expansion
212
- const original = expand(compressed);
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, error } = useSWR('/api/users', fetcher);
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
- ## Analytics (Opt-in)
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
- import { terse } from 'tersejson/express';
399
- import { analytics } from 'tersejson/analytics';
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
- // Check your savings anytime
415
- setInterval(() => {
416
- console.log(analytics.getSummary());
417
- // "TerseJSON Stats: 1,234 compressions, 847KB saved (73.2% avg)"
418
- }, 60000);
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
- ### Cloud Analytics (tersejson.com)
320
+ ## TypeScript Support
422
321
 
423
- Get a dashboard with your compression stats at tersejson.com:
322
+ Full type definitions included:
424
323
 
425
324
  ```typescript
426
- app.use(terse({
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
- ### Privacy
327
+ interface User {
328
+ firstName: string;
329
+ lastName: string;
330
+ }
442
331
 
443
- - Analytics are **100% opt-in**
444
- - Endpoint paths are hashed (no sensitive data)
445
- - No request/response content is ever collected
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 work with nested objects?
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
- ### What about non-array responses?
340
+ No! The Proxy is transparent. `JSON.stringify(data)` outputs original key names.
467
341
 
468
- TerseJSON only compresses arrays of objects (where key compression makes sense). Single objects or primitives pass through unchanged.
342
+ ### What about nested objects?
469
343
 
470
- ### Does this break JSON.stringify on the client?
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
- Minimal. Key mapping is O(n) and Proxy access adds negligible overhead. The bandwidth savings far outweigh the processing cost.
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
- // Client (Apollo)
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
- GraphQL queries returning arrays of objects (like `users { firstName lastName }`) benefit from the same key compression.
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 that support:
506
- - `Proxy` (ES6) - Chrome 49+, Firefox 18+, Safari 10+, Edge 12+
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)** | Made with bandwidth in mind
370
+ **[tersejson.com](https://tersejson.com)** | Memory-efficient JSON for high-volume APIs