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.
@@ -0,0 +1,522 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ // src/core.ts
6
+ function alphaGenerator(index) {
7
+ let key = "";
8
+ let remaining = index;
9
+ do {
10
+ key = String.fromCharCode(97 + remaining % 26) + key;
11
+ remaining = Math.floor(remaining / 26) - 1;
12
+ } while (remaining >= 0);
13
+ return key;
14
+ }
15
+ function numericGenerator(index) {
16
+ return String(index);
17
+ }
18
+ function alphanumericGenerator(index) {
19
+ const letterIndex = Math.floor(index / 9);
20
+ const numIndex = index % 9 + 1;
21
+ return alphaGenerator(letterIndex) + numIndex;
22
+ }
23
+ function shortGenerator(index) {
24
+ if (index === 0) return "_";
25
+ return alphaGenerator(index - 1);
26
+ }
27
+ function prefixedGenerator(prefix, style = "numeric") {
28
+ return (index) => {
29
+ if (style === "alpha") {
30
+ return prefix + alphaGenerator(index);
31
+ }
32
+ return prefix + index;
33
+ };
34
+ }
35
+ function createKeyGenerator(pattern) {
36
+ if (typeof pattern === "function") {
37
+ return { generator: pattern, name: "custom" };
38
+ }
39
+ if (typeof pattern === "string") {
40
+ switch (pattern) {
41
+ case "alpha":
42
+ return { generator: alphaGenerator, name: "alpha" };
43
+ case "numeric":
44
+ return { generator: numericGenerator, name: "numeric" };
45
+ case "alphanumeric":
46
+ return { generator: alphanumericGenerator, name: "alphanumeric" };
47
+ case "short":
48
+ return { generator: shortGenerator, name: "short" };
49
+ case "prefixed":
50
+ return { generator: prefixedGenerator("k"), name: "prefixed:k" };
51
+ default:
52
+ return { generator: alphaGenerator, name: "alpha" };
53
+ }
54
+ }
55
+ if (typeof pattern === "object" && "prefix" in pattern) {
56
+ return {
57
+ generator: prefixedGenerator(pattern.prefix, pattern.style || "numeric"),
58
+ name: `prefixed:${pattern.prefix}`
59
+ };
60
+ }
61
+ return { generator: alphaGenerator, name: "alpha" };
62
+ }
63
+ function resolveNestedDepth(handling, maxDepth) {
64
+ if (handling === void 0 || handling === "deep") {
65
+ return maxDepth;
66
+ }
67
+ if (handling === "shallow") {
68
+ return 1;
69
+ }
70
+ if (handling === "arrays") {
71
+ return maxDepth;
72
+ }
73
+ if (typeof handling === "number") {
74
+ return handling;
75
+ }
76
+ return maxDepth;
77
+ }
78
+ function collectKeys(data, options, currentDepth = 0) {
79
+ const keys = /* @__PURE__ */ new Set();
80
+ const { minKeyLength, maxDepth, nestedHandling, excludeKeys, includeKeys } = options;
81
+ if (currentDepth >= maxDepth) return keys;
82
+ const keyCounts = /* @__PURE__ */ new Map();
83
+ for (const item of data) {
84
+ if (typeof item !== "object" || item === null) continue;
85
+ for (const key of Object.keys(item)) {
86
+ if (excludeKeys?.includes(key)) continue;
87
+ const shouldInclude = includeKeys?.includes(key) || key.length >= minKeyLength;
88
+ if (shouldInclude) {
89
+ keys.add(key);
90
+ keyCounts.set(key, (keyCounts.get(key) || 0) + 1);
91
+ }
92
+ const value = item[key];
93
+ if (nestedHandling === "shallow") {
94
+ continue;
95
+ }
96
+ if (Array.isArray(value) && value.length > 0 && isCompressibleArray(value)) {
97
+ const nestedKeys = collectKeys(
98
+ value,
99
+ options,
100
+ currentDepth + 1
101
+ );
102
+ nestedKeys.forEach((k) => keys.add(k));
103
+ } else if (nestedHandling !== "arrays" && typeof value === "object" && value !== null && !Array.isArray(value)) {
104
+ const nestedKeys = collectKeys(
105
+ [value],
106
+ options,
107
+ currentDepth + 1
108
+ );
109
+ nestedKeys.forEach((k) => keys.add(k));
110
+ }
111
+ }
112
+ }
113
+ if (options.homogeneousOnly && data.length > 0) {
114
+ for (const [key, count] of keyCounts) {
115
+ if (count < data.length) {
116
+ keys.delete(key);
117
+ }
118
+ }
119
+ }
120
+ return keys;
121
+ }
122
+ function isCompressibleArray(data) {
123
+ if (!Array.isArray(data) || data.length === 0) return false;
124
+ return data.every(
125
+ (item) => typeof item === "object" && item !== null && !Array.isArray(item)
126
+ );
127
+ }
128
+ function compressObject(obj, keyToShort, maxDepth, currentDepth = 0) {
129
+ if (currentDepth >= maxDepth) return obj;
130
+ const compressed = {};
131
+ for (const [key, value] of Object.entries(obj)) {
132
+ const shortKey = keyToShort.get(key) ?? key;
133
+ if (Array.isArray(value) && isCompressibleArray(value)) {
134
+ compressed[shortKey] = value.map(
135
+ (item) => compressObject(
136
+ item,
137
+ keyToShort,
138
+ maxDepth,
139
+ currentDepth + 1
140
+ )
141
+ );
142
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
143
+ compressed[shortKey] = compressObject(
144
+ value,
145
+ keyToShort,
146
+ maxDepth,
147
+ currentDepth + 1
148
+ );
149
+ } else {
150
+ compressed[shortKey] = value;
151
+ }
152
+ }
153
+ return compressed;
154
+ }
155
+ function compress(data, options = {}) {
156
+ const {
157
+ minKeyLength = 3,
158
+ maxDepth = 10,
159
+ keyPattern = "alpha",
160
+ nestedHandling = "deep",
161
+ homogeneousOnly = false,
162
+ excludeKeys,
163
+ includeKeys
164
+ } = options;
165
+ const { generator, name: patternName } = createKeyGenerator(keyPattern);
166
+ const effectiveDepth = resolveNestedDepth(nestedHandling, maxDepth);
167
+ const allKeys = collectKeys(data, {
168
+ minKeyLength,
169
+ maxDepth: effectiveDepth,
170
+ nestedHandling,
171
+ excludeKeys,
172
+ includeKeys,
173
+ homogeneousOnly
174
+ });
175
+ const sortedKeys = Array.from(allKeys).sort();
176
+ const keyToShort = /* @__PURE__ */ new Map();
177
+ const keyMap = {};
178
+ sortedKeys.forEach((key, index) => {
179
+ const shortKey = generator(index);
180
+ if (shortKey.length < key.length) {
181
+ keyToShort.set(key, shortKey);
182
+ keyMap[shortKey] = key;
183
+ }
184
+ });
185
+ const compressed = data.map(
186
+ (item) => compressObject(item, keyToShort, effectiveDepth)
187
+ );
188
+ return {
189
+ __terse__: true,
190
+ v: 1,
191
+ k: keyMap,
192
+ d: compressed,
193
+ p: patternName
194
+ };
195
+ }
196
+
197
+ // src/analytics.ts
198
+ var DEFAULT_CONFIG = {
199
+ enabled: false,
200
+ reportToCloud: false,
201
+ reportInterval: 6e4,
202
+ trackEndpoints: false,
203
+ endpoint: "https://api.tersejson.com/v1/analytics",
204
+ debug: false
205
+ };
206
+ var TerseAnalytics = class {
207
+ constructor(config = {}) {
208
+ this.events = [];
209
+ this.config = { ...DEFAULT_CONFIG, ...config };
210
+ this.isNode = typeof window === "undefined";
211
+ this.stats = this.createEmptyStats();
212
+ if (this.config.enabled && this.config.reportToCloud) {
213
+ this.startReporting();
214
+ }
215
+ }
216
+ /**
217
+ * Create empty stats object
218
+ */
219
+ createEmptyStats() {
220
+ return {
221
+ totalEvents: 0,
222
+ totalOriginalBytes: 0,
223
+ totalCompressedBytes: 0,
224
+ totalBytesSaved: 0,
225
+ averageRatio: 0,
226
+ totalObjects: 0,
227
+ sessionStart: Date.now(),
228
+ lastEvent: Date.now()
229
+ };
230
+ }
231
+ /**
232
+ * Record a compression event
233
+ */
234
+ record(event) {
235
+ if (!this.config.enabled) return;
236
+ const fullEvent = {
237
+ ...event,
238
+ timestamp: Date.now(),
239
+ // Hash endpoint for privacy if tracking is enabled
240
+ endpoint: this.config.trackEndpoints && event.endpoint ? this.hashEndpoint(event.endpoint) : void 0
241
+ };
242
+ this.stats.totalEvents++;
243
+ this.stats.totalOriginalBytes += event.originalSize;
244
+ this.stats.totalCompressedBytes += event.compressedSize;
245
+ this.stats.totalBytesSaved += event.originalSize - event.compressedSize;
246
+ this.stats.totalObjects += event.objectCount;
247
+ this.stats.lastEvent = fullEvent.timestamp;
248
+ this.stats.averageRatio = this.stats.totalCompressedBytes / this.stats.totalOriginalBytes;
249
+ this.events.push(fullEvent);
250
+ if (this.config.onEvent) {
251
+ this.config.onEvent(fullEvent);
252
+ }
253
+ if (this.config.debug) {
254
+ const savings = ((1 - event.compressedSize / event.originalSize) * 100).toFixed(1);
255
+ console.log(`[tersejson:analytics] ${event.originalSize} \u2192 ${event.compressedSize} bytes (${savings}% saved)`);
256
+ }
257
+ }
258
+ /**
259
+ * Get current stats
260
+ */
261
+ getStats() {
262
+ return { ...this.stats };
263
+ }
264
+ /**
265
+ * Get formatted stats summary
266
+ */
267
+ getSummary() {
268
+ const stats = this.stats;
269
+ const savedKB = (stats.totalBytesSaved / 1024).toFixed(2);
270
+ const savedPercent = stats.totalOriginalBytes > 0 ? ((1 - stats.averageRatio) * 100).toFixed(1) : "0";
271
+ return `TerseJSON Stats: ${stats.totalEvents} compressions, ${savedKB}KB saved (${savedPercent}% avg)`;
272
+ }
273
+ /**
274
+ * Reset stats
275
+ */
276
+ reset() {
277
+ this.events = [];
278
+ this.stats = this.createEmptyStats();
279
+ }
280
+ /**
281
+ * Start periodic reporting to cloud
282
+ */
283
+ startReporting() {
284
+ if (this.reportTimer) return;
285
+ this.reportTimer = setInterval(() => {
286
+ this.reportToCloud();
287
+ }, this.config.reportInterval);
288
+ if (this.isNode && typeof process !== "undefined") {
289
+ process.on("beforeExit", () => this.reportToCloud());
290
+ }
291
+ }
292
+ /**
293
+ * Stop reporting
294
+ */
295
+ stop() {
296
+ if (this.reportTimer) {
297
+ clearInterval(this.reportTimer);
298
+ this.reportTimer = void 0;
299
+ }
300
+ }
301
+ /**
302
+ * Report stats to tersejson.com
303
+ */
304
+ async reportToCloud() {
305
+ if (!this.config.reportToCloud || this.events.length === 0) return;
306
+ const payload = {
307
+ apiKey: this.config.apiKey,
308
+ projectId: this.config.projectId,
309
+ stats: this.stats,
310
+ events: this.events.slice(-100),
311
+ // Last 100 events only
312
+ meta: {
313
+ version: "0.1.0",
314
+ runtime: this.isNode ? "node" : "browser"
315
+ }
316
+ };
317
+ try {
318
+ const fetchFn = this.isNode ? (await import('https')).request : globalThis.fetch;
319
+ if (this.isNode) {
320
+ const url = new URL(this.config.endpoint);
321
+ const req = fetchFn({
322
+ hostname: url.hostname,
323
+ port: url.port || 443,
324
+ path: url.pathname,
325
+ method: "POST",
326
+ headers: {
327
+ "Content-Type": "application/json"
328
+ }
329
+ });
330
+ req.write(JSON.stringify(payload));
331
+ req.end();
332
+ } else {
333
+ fetchFn(this.config.endpoint, {
334
+ method: "POST",
335
+ headers: { "Content-Type": "application/json" },
336
+ body: JSON.stringify(payload),
337
+ keepalive: true
338
+ // Allow sending on page unload
339
+ }).catch(() => {
340
+ });
341
+ }
342
+ this.events = [];
343
+ if (this.config.onStats) {
344
+ this.config.onStats(this.stats);
345
+ }
346
+ if (this.config.debug) {
347
+ console.log("[tersejson:analytics] Reported stats to cloud");
348
+ }
349
+ } catch {
350
+ if (this.config.debug) {
351
+ console.log("[tersejson:analytics] Failed to report stats");
352
+ }
353
+ }
354
+ }
355
+ /**
356
+ * Hash endpoint for privacy
357
+ */
358
+ hashEndpoint(endpoint) {
359
+ return endpoint.replace(/\/\d+/g, "/:id").replace(/\/[a-f0-9-]{36}/gi, "/:uuid").replace(/\?.*$/, "");
360
+ }
361
+ };
362
+ var globalAnalytics = null;
363
+ function initAnalytics(config) {
364
+ globalAnalytics = new TerseAnalytics(config);
365
+ return globalAnalytics;
366
+ }
367
+ function getAnalytics() {
368
+ return globalAnalytics;
369
+ }
370
+ function recordEvent(event) {
371
+ globalAnalytics?.record(event);
372
+ }
373
+ var analytics = {
374
+ /**
375
+ * Enable local-only analytics (no cloud reporting)
376
+ */
377
+ local(options = {}) {
378
+ return initAnalytics({
379
+ enabled: true,
380
+ reportToCloud: false,
381
+ debug: options.debug,
382
+ onEvent: options.onEvent
383
+ });
384
+ },
385
+ /**
386
+ * Enable cloud analytics with API key
387
+ */
388
+ cloud(apiKey, options = {}) {
389
+ return initAnalytics({
390
+ ...options,
391
+ enabled: true,
392
+ reportToCloud: true,
393
+ apiKey
394
+ });
395
+ },
396
+ /**
397
+ * Get current stats
398
+ */
399
+ getStats() {
400
+ return globalAnalytics?.getStats() ?? null;
401
+ },
402
+ /**
403
+ * Get formatted summary
404
+ */
405
+ getSummary() {
406
+ return globalAnalytics?.getSummary() ?? "Analytics not initialized";
407
+ }
408
+ };
409
+
410
+ // src/express.ts
411
+ var DEFAULT_OPTIONS = {
412
+ // Middleware-specific options
413
+ minArrayLength: 2,
414
+ shouldCompress: () => true,
415
+ headerName: "x-terse-json",
416
+ debug: false,
417
+ // CompressOptions
418
+ minKeyLength: 3,
419
+ maxDepth: 10,
420
+ keyPattern: "alpha",
421
+ nestedHandling: "deep",
422
+ homogeneousOnly: false,
423
+ excludeKeys: [],
424
+ includeKeys: []
425
+ };
426
+ function terse(options = {}) {
427
+ const { analytics: analyticsOption, ...restOptions } = options;
428
+ const config = { ...DEFAULT_OPTIONS, ...restOptions };
429
+ let analyticsInstance = null;
430
+ if (analyticsOption) {
431
+ if (analyticsOption instanceof TerseAnalytics) {
432
+ analyticsInstance = analyticsOption;
433
+ } else if (analyticsOption === true) {
434
+ analyticsInstance = new TerseAnalytics({ enabled: true, debug: config.debug });
435
+ } else if (typeof analyticsOption === "object") {
436
+ analyticsInstance = new TerseAnalytics({ enabled: true, ...analyticsOption });
437
+ }
438
+ }
439
+ return function terseMiddleware(req, res, next) {
440
+ const acceptsTerse = req.headers["accept-terse"] === "true" || req.headers["x-accept-terse"] === "true";
441
+ const originalJson = res.json.bind(res);
442
+ res.json = function terseJson(data) {
443
+ if (!acceptsTerse) {
444
+ return originalJson(data);
445
+ }
446
+ if (!isCompressibleArray(data)) {
447
+ return originalJson(data);
448
+ }
449
+ if (data.length < config.minArrayLength) {
450
+ return originalJson(data);
451
+ }
452
+ if (!config.shouldCompress(data, req)) {
453
+ return originalJson(data);
454
+ }
455
+ try {
456
+ const dataArray = data;
457
+ const compressed = compress(dataArray, {
458
+ minKeyLength: config.minKeyLength,
459
+ maxDepth: config.maxDepth,
460
+ keyPattern: config.keyPattern,
461
+ nestedHandling: config.nestedHandling,
462
+ homogeneousOnly: config.homogeneousOnly,
463
+ excludeKeys: config.excludeKeys,
464
+ includeKeys: config.includeKeys
465
+ });
466
+ const originalSize = JSON.stringify(data).length;
467
+ const compressedSize = JSON.stringify(compressed).length;
468
+ if (analyticsInstance) {
469
+ analyticsInstance.record({
470
+ originalSize,
471
+ compressedSize,
472
+ objectCount: dataArray.length,
473
+ keysCompressed: Object.keys(compressed.k).length,
474
+ endpoint: req.path,
475
+ keyPattern: compressed.p || "alpha"
476
+ });
477
+ }
478
+ recordEvent({
479
+ originalSize,
480
+ compressedSize,
481
+ objectCount: dataArray.length,
482
+ keysCompressed: Object.keys(compressed.k).length,
483
+ endpoint: req.path,
484
+ keyPattern: compressed.p || "alpha"
485
+ });
486
+ if (config.debug) {
487
+ const savings = ((1 - compressedSize / originalSize) * 100).toFixed(1);
488
+ console.log(
489
+ `[tersejson] Compressed ${originalSize} -> ${compressedSize} bytes (${savings}% savings)`
490
+ );
491
+ }
492
+ res.setHeader(config.headerName, "true");
493
+ return originalJson(compressed);
494
+ } catch (error) {
495
+ if (config.debug) {
496
+ console.error("[tersejson] Compression failed:", error);
497
+ }
498
+ return originalJson(data);
499
+ }
500
+ };
501
+ next();
502
+ };
503
+ }
504
+ function terseQueryParam(paramName = "terse") {
505
+ return function terseQueryMiddleware(req, _res, next) {
506
+ if (req.query[paramName] === "true") {
507
+ req.headers["accept-terse"] = "true";
508
+ }
509
+ next();
510
+ };
511
+ }
512
+ var express_default = terse;
513
+
514
+ exports.TerseAnalytics = TerseAnalytics;
515
+ exports.analytics = analytics;
516
+ exports.default = express_default;
517
+ exports.getAnalytics = getAnalytics;
518
+ exports.initAnalytics = initAnalytics;
519
+ exports.terse = terse;
520
+ exports.terseQueryParam = terseQueryParam;
521
+ //# sourceMappingURL=express.js.map
522
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core.ts","../src/analytics.ts","../src/express.ts"],"names":[],"mappings":"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","file":"express.js","sourcesContent":["/**\n * TerseJSON Core\n *\n * The core compression and expansion algorithms.\n */\n\nimport {\n TersePayload,\n isTersePayload,\n KeyPattern,\n KeyGenerator,\n NestedHandling,\n CompressOptions,\n} from './types';\n\n// ============================================================================\n// KEY PATTERN GENERATORS\n// ============================================================================\n\n/**\n * Alpha pattern: a, b, c, ... z, aa, ab, ...\n */\nfunction alphaGenerator(index: number): string {\n let key = '';\n let remaining = index;\n\n do {\n key = String.fromCharCode(97 + (remaining % 26)) + key;\n remaining = Math.floor(remaining / 26) - 1;\n } while (remaining >= 0);\n\n return key;\n}\n\n/**\n * Numeric pattern: 0, 1, 2, ... 9, 10, 11, ...\n */\nfunction numericGenerator(index: number): string {\n return String(index);\n}\n\n/**\n * Alphanumeric pattern: a1, a2, ... a9, b1, b2, ...\n */\nfunction alphanumericGenerator(index: number): string {\n const letterIndex = Math.floor(index / 9);\n const numIndex = (index % 9) + 1;\n return alphaGenerator(letterIndex) + numIndex;\n}\n\n/**\n * Short pattern: uses shortest possible keys\n * _, a, b, ..., z, aa, ab, ...\n */\nfunction shortGenerator(index: number): string {\n if (index === 0) return '_';\n return alphaGenerator(index - 1);\n}\n\n/**\n * Prefixed pattern: k0, k1, k2, ...\n */\nfunction prefixedGenerator(prefix: string, style: 'numeric' | 'alpha' = 'numeric'): KeyGenerator {\n return (index: number) => {\n if (style === 'alpha') {\n return prefix + alphaGenerator(index);\n }\n return prefix + index;\n };\n}\n\n/**\n * Creates a key generator from a pattern configuration\n */\nexport function createKeyGenerator(pattern: KeyPattern): { generator: KeyGenerator; name: string } {\n // If it's already a function, use it directly\n if (typeof pattern === 'function') {\n return { generator: pattern, name: 'custom' };\n }\n\n // If it's a preset string\n if (typeof pattern === 'string') {\n switch (pattern) {\n case 'alpha':\n return { generator: alphaGenerator, name: 'alpha' };\n case 'numeric':\n return { generator: numericGenerator, name: 'numeric' };\n case 'alphanumeric':\n return { generator: alphanumericGenerator, name: 'alphanumeric' };\n case 'short':\n return { generator: shortGenerator, name: 'short' };\n case 'prefixed':\n return { generator: prefixedGenerator('k'), name: 'prefixed:k' };\n default:\n return { generator: alphaGenerator, name: 'alpha' };\n }\n }\n\n // If it's a prefix config object\n if (typeof pattern === 'object' && 'prefix' in pattern) {\n return {\n generator: prefixedGenerator(pattern.prefix, pattern.style || 'numeric'),\n name: `prefixed:${pattern.prefix}`,\n };\n }\n\n return { generator: alphaGenerator, name: 'alpha' };\n}\n\n/**\n * Resolves nested handling to a numeric depth\n */\nfunction resolveNestedDepth(handling: NestedHandling | undefined, maxDepth: number): number {\n if (handling === undefined || handling === 'deep') {\n return maxDepth;\n }\n if (handling === 'shallow') {\n return 1;\n }\n if (handling === 'arrays') {\n return maxDepth; // Special handling in collect/compress functions\n }\n if (typeof handling === 'number') {\n return handling;\n }\n return maxDepth;\n}\n\n// Legacy generateShortKey removed - use createKeyGenerator instead\n\ninterface CollectKeysOptions {\n minKeyLength: number;\n maxDepth: number;\n nestedHandling: NestedHandling;\n excludeKeys?: string[];\n includeKeys?: string[];\n homogeneousOnly?: boolean;\n}\n\n/**\n * Collects all unique keys from an array of objects\n */\nfunction collectKeys(\n data: Record<string, unknown>[],\n options: CollectKeysOptions,\n currentDepth: number = 0\n): Set<string> {\n const keys = new Set<string>();\n const { minKeyLength, maxDepth, nestedHandling, excludeKeys, includeKeys } = options;\n\n if (currentDepth >= maxDepth) return keys;\n\n // For homogeneous mode, track key counts\n const keyCounts = new Map<string, number>();\n\n for (const item of data) {\n if (typeof item !== 'object' || item === null) continue;\n\n for (const key of Object.keys(item)) {\n // Skip excluded keys\n if (excludeKeys?.includes(key)) continue;\n\n // Include if in includeKeys, or if meets minKeyLength\n const shouldInclude = includeKeys?.includes(key) || key.length >= minKeyLength;\n\n if (shouldInclude) {\n keys.add(key);\n keyCounts.set(key, (keyCounts.get(key) || 0) + 1);\n }\n\n // Handle nested structures based on nestedHandling option\n const value = item[key];\n\n if (nestedHandling === 'shallow') {\n // Don't process nested structures\n continue;\n }\n\n if (Array.isArray(value) && value.length > 0 && isCompressibleArray(value)) {\n // Nested array of objects - always process\n const nestedKeys = collectKeys(\n value as Record<string, unknown>[],\n options,\n currentDepth + 1\n );\n nestedKeys.forEach(k => keys.add(k));\n } else if (\n nestedHandling !== 'arrays' &&\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value)\n ) {\n // Single nested object - skip if nestedHandling is 'arrays'\n const nestedKeys = collectKeys(\n [value as Record<string, unknown>],\n options,\n currentDepth + 1\n );\n nestedKeys.forEach(k => keys.add(k));\n }\n }\n }\n\n // If homogeneous mode, only keep keys that appear in ALL objects\n if (options.homogeneousOnly && data.length > 0) {\n for (const [key, count] of keyCounts) {\n if (count < data.length) {\n keys.delete(key);\n }\n }\n }\n\n return keys;\n}\n\n/**\n * Checks if an array is compressible (array of objects with consistent structure)\n */\nexport function isCompressibleArray(data: unknown): data is Record<string, unknown>[] {\n if (!Array.isArray(data) || data.length === 0) return false;\n\n // Check if all items are objects\n return data.every(\n item => typeof item === 'object' && item !== null && !Array.isArray(item)\n );\n}\n\n/**\n * Compresses an object using the key mapping\n */\nfunction compressObject(\n obj: Record<string, unknown>,\n keyToShort: Map<string, string>,\n maxDepth: number,\n currentDepth: number = 0\n): Record<string, unknown> {\n if (currentDepth >= maxDepth) return obj;\n\n const compressed: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(obj)) {\n const shortKey = keyToShort.get(key) ?? key;\n\n if (Array.isArray(value) && isCompressibleArray(value)) {\n // Recursively compress nested arrays\n compressed[shortKey] = value.map(item =>\n compressObject(\n item as Record<string, unknown>,\n keyToShort,\n maxDepth,\n currentDepth + 1\n )\n );\n } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n // Compress single nested objects too\n compressed[shortKey] = compressObject(\n value as Record<string, unknown>,\n keyToShort,\n maxDepth,\n currentDepth + 1\n );\n } else {\n compressed[shortKey] = value;\n }\n }\n\n return compressed;\n}\n\n/**\n * Expands an object using the key mapping\n */\nfunction expandObject(\n obj: Record<string, unknown>,\n shortToKey: Map<string, string>,\n maxDepth: number,\n currentDepth: number = 0\n): Record<string, unknown> {\n if (currentDepth >= maxDepth) return obj;\n\n const expanded: Record<string, unknown> = {};\n\n for (const [shortKey, value] of Object.entries(obj)) {\n const originalKey = shortToKey.get(shortKey) ?? shortKey;\n\n if (Array.isArray(value)) {\n expanded[originalKey] = value.map(item => {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n return expandObject(\n item as Record<string, unknown>,\n shortToKey,\n maxDepth,\n currentDepth + 1\n );\n }\n return item;\n });\n } else if (typeof value === 'object' && value !== null) {\n expanded[originalKey] = expandObject(\n value as Record<string, unknown>,\n shortToKey,\n maxDepth,\n currentDepth + 1\n );\n } else {\n expanded[originalKey] = value;\n }\n }\n\n return expanded;\n}\n\n// Re-export CompressOptions from types for backwards compatibility\nexport type { CompressOptions } from './types';\n\n/**\n * Compresses an array of objects by replacing keys with short aliases\n */\nexport function compress<T extends Record<string, unknown>[]>(\n data: T,\n options: CompressOptions = {}\n): TersePayload<unknown[]> {\n const {\n minKeyLength = 3,\n maxDepth = 10,\n keyPattern = 'alpha',\n nestedHandling = 'deep',\n homogeneousOnly = false,\n excludeKeys,\n includeKeys,\n } = options;\n\n // Create key generator\n const { generator, name: patternName } = createKeyGenerator(keyPattern);\n\n // Resolve nested depth\n const effectiveDepth = resolveNestedDepth(nestedHandling, maxDepth);\n\n // Collect all unique keys\n const allKeys = collectKeys(data, {\n minKeyLength,\n maxDepth: effectiveDepth,\n nestedHandling,\n excludeKeys,\n includeKeys,\n homogeneousOnly,\n });\n\n // Sort keys by frequency of use (most used first) for optimal compression\n // For now, just sort alphabetically for deterministic output\n const sortedKeys = Array.from(allKeys).sort();\n\n // Create bidirectional mapping\n const keyToShort = new Map<string, string>();\n const keyMap: Record<string, string> = {};\n\n sortedKeys.forEach((key, index) => {\n const shortKey = generator(index);\n // Only use short key if it's actually shorter\n if (shortKey.length < key.length) {\n keyToShort.set(key, shortKey);\n keyMap[shortKey] = key;\n }\n });\n\n // Compress the data\n const compressed = data.map(item =>\n compressObject(item, keyToShort, effectiveDepth)\n );\n\n return {\n __terse__: true,\n v: 1,\n k: keyMap,\n d: compressed,\n p: patternName,\n };\n}\n\n/**\n * Expands a TersePayload back to its original form\n */\nexport function expand<T = unknown>(payload: TersePayload): T {\n const shortToKey = new Map(\n Object.entries(payload.k).map(([short, original]) => [short, original])\n );\n\n if (Array.isArray(payload.d)) {\n return payload.d.map(item => {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n return expandObject(item as Record<string, unknown>, shortToKey, 10);\n }\n return item;\n }) as T;\n }\n\n if (typeof payload.d === 'object' && payload.d !== null) {\n return expandObject(payload.d as Record<string, unknown>, shortToKey, 10) as T;\n }\n\n return payload.d as T;\n}\n\n/**\n * Creates a Proxy that transparently maps original keys to short keys\n * This is the magic that makes client-side access seamless\n */\nexport function createTerseProxy<T extends Record<string, unknown>>(\n compressed: Record<string, unknown>,\n keyMap: Record<string, string> // short -> original\n): T {\n // Create reverse map: original -> short\n const originalToShort = new Map(\n Object.entries(keyMap).map(([short, original]) => [original, short])\n );\n\n const handler: ProxyHandler<Record<string, unknown>> = {\n get(target, prop: string | symbol) {\n if (typeof prop === 'symbol') {\n return Reflect.get(target, prop);\n }\n\n // Check if accessing by original key name\n const shortKey = originalToShort.get(prop);\n const actualKey = shortKey ?? prop;\n const value = target[actualKey];\n\n // Recursively proxy nested objects/arrays\n if (Array.isArray(value)) {\n return value.map(item => {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n return createTerseProxy(item as Record<string, unknown>, keyMap);\n }\n return item;\n });\n }\n\n if (typeof value === 'object' && value !== null) {\n return createTerseProxy(value as Record<string, unknown>, keyMap);\n }\n\n return value;\n },\n\n has(target, prop: string | symbol) {\n if (typeof prop === 'symbol') {\n return Reflect.has(target, prop);\n }\n const shortKey = originalToShort.get(prop);\n return (shortKey ?? prop) in target;\n },\n\n ownKeys(target) {\n // Return original key names\n return Object.keys(target).map(shortKey => keyMap[shortKey] ?? shortKey);\n },\n\n getOwnPropertyDescriptor(target, prop: string | symbol) {\n if (typeof prop === 'symbol') {\n return Reflect.getOwnPropertyDescriptor(target, prop);\n }\n const shortKey = originalToShort.get(prop);\n const actualKey = shortKey ?? prop;\n const descriptor = Object.getOwnPropertyDescriptor(target, actualKey);\n if (descriptor) {\n return { ...descriptor, enumerable: true, configurable: true };\n }\n return undefined;\n },\n };\n\n return new Proxy(compressed, handler) as T;\n}\n\n/**\n * Wraps TersePayload data with Proxies for transparent access\n */\nexport function wrapWithProxy<T>(payload: TersePayload): T {\n if (Array.isArray(payload.d)) {\n return payload.d.map(item => {\n if (typeof item === 'object' && item !== null && !Array.isArray(item)) {\n return createTerseProxy(item as Record<string, unknown>, payload.k);\n }\n return item;\n }) as T;\n }\n\n if (typeof payload.d === 'object' && payload.d !== null) {\n return createTerseProxy(payload.d as Record<string, unknown>, payload.k) as T;\n }\n\n return payload.d as T;\n}\n\n// Re-export for convenience\nexport { isTersePayload };\n","/**\n * TerseJSON Analytics\n *\n * Opt-in analytics to track compression savings.\n * Data is anonymous and helps improve the library.\n */\n\n/**\n * Compression event data\n */\nexport interface CompressionEvent {\n /** Timestamp of the compression */\n timestamp: number;\n /** Original payload size in bytes */\n originalSize: number;\n /** Compressed payload size in bytes */\n compressedSize: number;\n /** Number of objects in the array */\n objectCount: number;\n /** Number of keys compressed */\n keysCompressed: number;\n /** Route/endpoint (optional, anonymized) */\n endpoint?: string;\n /** Key pattern used */\n keyPattern: string;\n}\n\n/**\n * Aggregated stats for reporting\n */\nexport interface AnalyticsStats {\n /** Total compression events */\n totalEvents: number;\n /** Total bytes before compression */\n totalOriginalBytes: number;\n /** Total bytes after compression */\n totalCompressedBytes: number;\n /** Total bytes saved */\n totalBytesSaved: number;\n /** Average compression ratio (0-1) */\n averageRatio: number;\n /** Total objects processed */\n totalObjects: number;\n /** Session start time */\n sessionStart: number;\n /** Last event time */\n lastEvent: number;\n}\n\n/**\n * Analytics configuration\n */\nexport interface AnalyticsConfig {\n /**\n * Enable analytics collection\n * @default false\n */\n enabled: boolean;\n\n /**\n * Send anonymous stats to tersejson.com\n * Helps improve the library\n * @default false\n */\n reportToCloud: boolean;\n\n /**\n * API key for tersejson.com (optional)\n * Get one at tersejson.com/dashboard\n */\n apiKey?: string;\n\n /**\n * Project/site identifier (optional)\n */\n projectId?: string;\n\n /**\n * Callback for each compression event\n * Use for custom logging/monitoring\n */\n onEvent?: (event: CompressionEvent) => void;\n\n /**\n * Callback for periodic stats summary\n */\n onStats?: (stats: AnalyticsStats) => void;\n\n /**\n * How often to report stats (ms)\n * @default 60000 (1 minute)\n */\n reportInterval?: number;\n\n /**\n * Include endpoint paths in analytics\n * Paths are hashed for privacy\n * @default false\n */\n trackEndpoints?: boolean;\n\n /**\n * Cloud reporting endpoint\n * @default 'https://api.tersejson.com/v1/analytics'\n */\n endpoint?: string;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n}\n\n/**\n * Default configuration\n */\nconst DEFAULT_CONFIG: Required<Omit<AnalyticsConfig, 'apiKey' | 'projectId' | 'onEvent' | 'onStats'>> = {\n enabled: false,\n reportToCloud: false,\n reportInterval: 60000,\n trackEndpoints: false,\n endpoint: 'https://api.tersejson.com/v1/analytics',\n debug: false,\n};\n\n/**\n * Analytics collector class\n */\nexport class TerseAnalytics {\n private config: Required<Omit<AnalyticsConfig, 'apiKey' | 'projectId' | 'onEvent' | 'onStats'>> &\n Pick<AnalyticsConfig, 'apiKey' | 'projectId' | 'onEvent' | 'onStats'>;\n private events: CompressionEvent[] = [];\n private stats: AnalyticsStats;\n private reportTimer?: ReturnType<typeof setInterval>;\n private isNode: boolean;\n\n constructor(config: Partial<AnalyticsConfig> = {}) {\n this.config = { ...DEFAULT_CONFIG, ...config };\n this.isNode = typeof window === 'undefined';\n this.stats = this.createEmptyStats();\n\n if (this.config.enabled && this.config.reportToCloud) {\n this.startReporting();\n }\n }\n\n /**\n * Create empty stats object\n */\n private createEmptyStats(): AnalyticsStats {\n return {\n totalEvents: 0,\n totalOriginalBytes: 0,\n totalCompressedBytes: 0,\n totalBytesSaved: 0,\n averageRatio: 0,\n totalObjects: 0,\n sessionStart: Date.now(),\n lastEvent: Date.now(),\n };\n }\n\n /**\n * Record a compression event\n */\n record(event: Omit<CompressionEvent, 'timestamp'>): void {\n if (!this.config.enabled) return;\n\n const fullEvent: CompressionEvent = {\n ...event,\n timestamp: Date.now(),\n // Hash endpoint for privacy if tracking is enabled\n endpoint: this.config.trackEndpoints && event.endpoint\n ? this.hashEndpoint(event.endpoint)\n : undefined,\n };\n\n // Update stats\n this.stats.totalEvents++;\n this.stats.totalOriginalBytes += event.originalSize;\n this.stats.totalCompressedBytes += event.compressedSize;\n this.stats.totalBytesSaved += (event.originalSize - event.compressedSize);\n this.stats.totalObjects += event.objectCount;\n this.stats.lastEvent = fullEvent.timestamp;\n this.stats.averageRatio = this.stats.totalCompressedBytes / this.stats.totalOriginalBytes;\n\n // Store event for batch reporting\n this.events.push(fullEvent);\n\n // Call event callback\n if (this.config.onEvent) {\n this.config.onEvent(fullEvent);\n }\n\n // Debug logging\n if (this.config.debug) {\n const savings = ((1 - event.compressedSize / event.originalSize) * 100).toFixed(1);\n console.log(`[tersejson:analytics] ${event.originalSize} → ${event.compressedSize} bytes (${savings}% saved)`);\n }\n }\n\n /**\n * Get current stats\n */\n getStats(): AnalyticsStats {\n return { ...this.stats };\n }\n\n /**\n * Get formatted stats summary\n */\n getSummary(): string {\n const stats = this.stats;\n const savedKB = (stats.totalBytesSaved / 1024).toFixed(2);\n const savedPercent = stats.totalOriginalBytes > 0\n ? ((1 - stats.averageRatio) * 100).toFixed(1)\n : '0';\n\n return `TerseJSON Stats: ${stats.totalEvents} compressions, ${savedKB}KB saved (${savedPercent}% avg)`;\n }\n\n /**\n * Reset stats\n */\n reset(): void {\n this.events = [];\n this.stats = this.createEmptyStats();\n }\n\n /**\n * Start periodic reporting to cloud\n */\n private startReporting(): void {\n if (this.reportTimer) return;\n\n this.reportTimer = setInterval(() => {\n this.reportToCloud();\n }, this.config.reportInterval);\n\n // Report on exit (Node.js only)\n if (this.isNode && typeof process !== 'undefined') {\n process.on('beforeExit', () => this.reportToCloud());\n }\n }\n\n /**\n * Stop reporting\n */\n stop(): void {\n if (this.reportTimer) {\n clearInterval(this.reportTimer);\n this.reportTimer = undefined;\n }\n }\n\n /**\n * Report stats to tersejson.com\n */\n private async reportToCloud(): Promise<void> {\n if (!this.config.reportToCloud || this.events.length === 0) return;\n\n const payload = {\n apiKey: this.config.apiKey,\n projectId: this.config.projectId,\n stats: this.stats,\n events: this.events.slice(-100), // Last 100 events only\n meta: {\n version: '0.1.0',\n runtime: this.isNode ? 'node' : 'browser',\n },\n };\n\n try {\n // Use appropriate fetch based on environment\n const fetchFn = this.isNode\n ? (await import('node:https')).request\n : globalThis.fetch;\n\n if (this.isNode) {\n // Node.js - fire and forget\n const url = new URL(this.config.endpoint);\n const req = (fetchFn as typeof import('node:https').request)({\n hostname: url.hostname,\n port: url.port || 443,\n path: url.pathname,\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n req.write(JSON.stringify(payload));\n req.end();\n } else {\n // Browser\n (fetchFn as typeof fetch)(this.config.endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true, // Allow sending on page unload\n }).catch(() => {}); // Ignore errors\n }\n\n // Clear reported events\n this.events = [];\n\n // Call stats callback\n if (this.config.onStats) {\n this.config.onStats(this.stats);\n }\n\n if (this.config.debug) {\n console.log('[tersejson:analytics] Reported stats to cloud');\n }\n } catch {\n // Silently fail - analytics should never break the app\n if (this.config.debug) {\n console.log('[tersejson:analytics] Failed to report stats');\n }\n }\n }\n\n /**\n * Hash endpoint for privacy\n */\n private hashEndpoint(endpoint: string): string {\n // Simple hash - just keeps route structure without specifics\n return endpoint\n .replace(/\\/\\d+/g, '/:id')\n .replace(/\\/[a-f0-9-]{36}/gi, '/:uuid')\n .replace(/\\?.*$/, '');\n }\n}\n\n/**\n * Global analytics instance (singleton)\n */\nlet globalAnalytics: TerseAnalytics | null = null;\n\n/**\n * Initialize global analytics\n */\nexport function initAnalytics(config: Partial<AnalyticsConfig>): TerseAnalytics {\n globalAnalytics = new TerseAnalytics(config);\n return globalAnalytics;\n}\n\n/**\n * Get global analytics instance\n */\nexport function getAnalytics(): TerseAnalytics | null {\n return globalAnalytics;\n}\n\n/**\n * Record an event to global analytics (if initialized)\n */\nexport function recordEvent(event: Omit<CompressionEvent, 'timestamp'>): void {\n globalAnalytics?.record(event);\n}\n\n/**\n * Quick setup for common use cases\n */\nexport const analytics = {\n /**\n * Enable local-only analytics (no cloud reporting)\n */\n local(options: { debug?: boolean; onEvent?: AnalyticsConfig['onEvent'] } = {}) {\n return initAnalytics({\n enabled: true,\n reportToCloud: false,\n debug: options.debug,\n onEvent: options.onEvent,\n });\n },\n\n /**\n * Enable cloud analytics with API key\n */\n cloud(apiKey: string, options: Partial<AnalyticsConfig> = {}) {\n return initAnalytics({\n ...options,\n enabled: true,\n reportToCloud: true,\n apiKey,\n });\n },\n\n /**\n * Get current stats\n */\n getStats() {\n return globalAnalytics?.getStats() ?? null;\n },\n\n /**\n * Get formatted summary\n */\n getSummary() {\n return globalAnalytics?.getSummary() ?? 'Analytics not initialized';\n },\n};\n\nexport default analytics;\n","/**\n * TerseJSON Express Middleware\n *\n * Automatically compresses JSON responses with repeated object structures.\n * Zero configuration required - just add the middleware.\n */\n\nimport type { Request, Response, NextFunction, RequestHandler } from 'express';\nimport { TerseMiddlewareOptions } from './types';\nimport { compress, isCompressibleArray } from './core';\nimport { recordEvent, TerseAnalytics, AnalyticsConfig } from './analytics';\n\n/**\n * Extended middleware options with analytics\n */\nexport interface TerseMiddlewareOptionsWithAnalytics extends TerseMiddlewareOptions {\n /**\n * Analytics configuration\n * Set to true for local-only analytics\n * Set to { apiKey: 'xxx' } for cloud reporting\n */\n analytics?: boolean | Partial<AnalyticsConfig> | TerseAnalytics;\n}\n\nconst DEFAULT_OPTIONS: Required<TerseMiddlewareOptions> = {\n // Middleware-specific options\n minArrayLength: 2,\n shouldCompress: () => true,\n headerName: 'x-terse-json',\n debug: false,\n // CompressOptions\n minKeyLength: 3,\n maxDepth: 10,\n keyPattern: 'alpha',\n nestedHandling: 'deep',\n homogeneousOnly: false,\n excludeKeys: [],\n includeKeys: [],\n};\n\n/**\n * Creates the TerseJSON Express middleware\n *\n * @example\n * ```typescript\n * import express from 'express';\n * import { terse } from 'tersejson/express';\n *\n * const app = express();\n * app.use(terse());\n *\n * app.get('/users', (req, res) => {\n * // Just send data as normal - compression is automatic\n * res.json(users);\n * });\n * ```\n */\nexport function terse(options: TerseMiddlewareOptionsWithAnalytics = {}): RequestHandler {\n const { analytics: analyticsOption, ...restOptions } = options;\n const config = { ...DEFAULT_OPTIONS, ...restOptions };\n\n // Setup analytics if enabled\n let analyticsInstance: TerseAnalytics | null = null;\n if (analyticsOption) {\n if (analyticsOption instanceof TerseAnalytics) {\n analyticsInstance = analyticsOption;\n } else if (analyticsOption === true) {\n // Local-only analytics\n analyticsInstance = new TerseAnalytics({ enabled: true, debug: config.debug });\n } else if (typeof analyticsOption === 'object') {\n analyticsInstance = new TerseAnalytics({ enabled: true, ...analyticsOption });\n }\n }\n\n return function terseMiddleware(\n req: Request,\n res: Response,\n next: NextFunction\n ): void {\n // Check if client supports terse responses\n const acceptsTerse = req.headers['accept-terse'] === 'true' ||\n req.headers['x-accept-terse'] === 'true';\n\n // Store original json method\n const originalJson = res.json.bind(res);\n\n // Override res.json\n res.json = function terseJson(data: unknown): Response {\n // Skip if client doesn't accept terse or compression is disabled\n if (!acceptsTerse) {\n return originalJson(data);\n }\n\n // Check if data is compressible\n if (!isCompressibleArray(data)) {\n return originalJson(data);\n }\n\n // Check minimum array length\n if ((data as unknown[]).length < config.minArrayLength) {\n return originalJson(data);\n }\n\n // Check custom shouldCompress function\n if (!config.shouldCompress(data, req)) {\n return originalJson(data);\n }\n\n try {\n const dataArray = data as Record<string, unknown>[];\n\n // Compress the data\n const compressed = compress(dataArray, {\n minKeyLength: config.minKeyLength,\n maxDepth: config.maxDepth,\n keyPattern: config.keyPattern,\n nestedHandling: config.nestedHandling,\n homogeneousOnly: config.homogeneousOnly,\n excludeKeys: config.excludeKeys,\n includeKeys: config.includeKeys,\n });\n\n // Calculate sizes\n const originalSize = JSON.stringify(data).length;\n const compressedSize = JSON.stringify(compressed).length;\n\n // Record analytics\n if (analyticsInstance) {\n analyticsInstance.record({\n originalSize,\n compressedSize,\n objectCount: dataArray.length,\n keysCompressed: Object.keys(compressed.k).length,\n endpoint: req.path,\n keyPattern: compressed.p || 'alpha',\n });\n }\n\n // Also record to global analytics if initialized\n recordEvent({\n originalSize,\n compressedSize,\n objectCount: dataArray.length,\n keysCompressed: Object.keys(compressed.k).length,\n endpoint: req.path,\n keyPattern: compressed.p || 'alpha',\n });\n\n // Debug logging\n if (config.debug) {\n const savings = ((1 - compressedSize / originalSize) * 100).toFixed(1);\n console.log(\n `[tersejson] Compressed ${originalSize} -> ${compressedSize} bytes (${savings}% savings)`\n );\n }\n\n // Set header to indicate terse response\n res.setHeader(config.headerName, 'true');\n\n return originalJson(compressed);\n } catch (error) {\n // If compression fails, fall back to original\n if (config.debug) {\n console.error('[tersejson] Compression failed:', error);\n }\n return originalJson(data);\n }\n };\n\n next();\n };\n}\n\n/**\n * Middleware to automatically add x-accept-terse header based on query param\n * Useful for testing or when you can't control client headers\n *\n * @example\n * ```typescript\n * app.use(terseQueryParam());\n * // Now ?terse=true will enable compression\n * ```\n */\nexport function terseQueryParam(paramName: string = 'terse'): RequestHandler {\n return function terseQueryMiddleware(\n req: Request,\n _res: Response,\n next: NextFunction\n ): void {\n if (req.query[paramName] === 'true') {\n req.headers['accept-terse'] = 'true';\n }\n next();\n };\n}\n\n// Re-export analytics for convenience\nexport { TerseAnalytics, analytics, initAnalytics, getAnalytics } from './analytics';\nexport type { AnalyticsConfig, AnalyticsStats, CompressionEvent } from './analytics';\n\n// Default export for convenience\nexport default terse;\n"]}