tersejson 0.1.0 → 0.2.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.
Files changed (38) hide show
  1. package/README.md +47 -4
  2. package/dist/{client-BQAZg7I8.d.mts → client-CFGvusCj.d.mts} +1 -1
  3. package/dist/{client-DOOGwp_p.d.ts → client-hUXNNGcN.d.ts} +1 -1
  4. package/dist/client.d.mts +2 -2
  5. package/dist/client.d.ts +2 -2
  6. package/dist/client.js.map +1 -1
  7. package/dist/client.mjs.map +1 -1
  8. package/dist/{express-LSVylWpN.d.ts → express-Da7WcJtt.d.ts} +1 -1
  9. package/dist/{express-BoL__Ao6.d.mts → express-O5w2NBTf.d.mts} +1 -1
  10. package/dist/express.d.mts +2 -2
  11. package/dist/express.d.ts +2 -2
  12. package/dist/graphql-C3PTnqnZ.d.mts +81 -0
  13. package/dist/graphql-DmtweJgh.d.ts +81 -0
  14. package/dist/graphql-client-2H4FjmRc.d.mts +123 -0
  15. package/dist/graphql-client-BXGtWqe9.d.ts +123 -0
  16. package/dist/graphql-client.d.mts +2 -0
  17. package/dist/graphql-client.d.ts +2 -0
  18. package/dist/graphql-client.js +215 -0
  19. package/dist/graphql-client.js.map +1 -0
  20. package/dist/graphql-client.mjs +207 -0
  21. package/dist/graphql-client.mjs.map +1 -0
  22. package/dist/graphql.d.mts +3 -0
  23. package/dist/graphql.d.ts +3 -0
  24. package/dist/graphql.js +304 -0
  25. package/dist/graphql.js.map +1 -0
  26. package/dist/graphql.mjs +296 -0
  27. package/dist/graphql.mjs.map +1 -0
  28. package/dist/index.d.mts +5 -3
  29. package/dist/index.d.ts +5 -3
  30. package/dist/index.js +400 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +398 -4
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/integrations.js.map +1 -1
  35. package/dist/integrations.mjs.map +1 -1
  36. package/dist/{types-CzaGQaV7.d.mts → types-BTonKlz8.d.mts} +56 -1
  37. package/dist/{types-CzaGQaV7.d.ts → types-BTonKlz8.d.ts} +56 -1
  38. package/package.json +44 -3
package/README.md CHANGED
@@ -105,13 +105,35 @@ console.log(users[0].emailAddress); // Works transparently!
105
105
 
106
106
  ### Bandwidth Savings
107
107
 
108
- | Scenario | Original | Compressed | Savings |
109
- |----------|----------|------------|---------|
108
+ **Without gzip (many servers don't have it enabled):**
109
+
110
+ | Scenario | Original | With TerseJSON | Savings |
111
+ |----------|----------|----------------|---------|
110
112
  | 100 users, 10 fields | 45 KB | 12 KB | **73%** |
111
113
  | 1000 products, 15 fields | 890 KB | 180 KB | **80%** |
112
114
  | 10000 logs, 8 fields | 2.1 MB | 450 KB | **79%** |
113
115
 
114
- *Note: These savings are **before** gzip. Combined with gzip, total reduction can exceed 90%.*
116
+ *Many Express apps, serverless functions, and internal APIs don't enable gzip. TerseJSON is often easier to add than configuring compression.*
117
+
118
+ **With gzip already enabled:**
119
+
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%** |
125
+
126
+ *If you already use gzip, TerseJSON stacks on top for additional savings.*
127
+
128
+ **At enterprise scale:**
129
+
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** |
135
+
136
+ *At $0.09/GB egress, 10M requests/day = ~$1,000/month saved.*
115
137
 
116
138
  ## API Reference
117
139
 
@@ -455,7 +477,28 @@ Minimal. Key mapping is O(n) and Proxy access adds negligible overhead. The band
455
477
 
456
478
  ### Can I use this with GraphQL?
457
479
 
458
- TerseJSON is designed for REST APIs. GraphQL already has efficient query mechanisms.
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
+ })));
491
+
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
+ ```
500
+
501
+ GraphQL queries returning arrays of objects (like `users { firstName lastName }`) benefit from the same key compression.
459
502
 
460
503
  ## Browser Support
461
504
 
@@ -1,4 +1,4 @@
1
- import { C as CompressOptions, T as TersePayload, K as KeyPattern, d as KeyGenerator, b as TerseClientOptions, i as isTersePayload } from './types-CzaGQaV7.mjs';
1
+ import { C as CompressOptions, T as TersePayload, K as KeyPattern, d as KeyGenerator, b as TerseClientOptions, i as isTersePayload } from './types-BTonKlz8.mjs';
2
2
 
3
3
  /**
4
4
  * TerseJSON Core
@@ -1,4 +1,4 @@
1
- import { C as CompressOptions, T as TersePayload, K as KeyPattern, d as KeyGenerator, b as TerseClientOptions, i as isTersePayload } from './types-CzaGQaV7.js';
1
+ import { C as CompressOptions, T as TersePayload, K as KeyPattern, d as KeyGenerator, b as TerseClientOptions, i as isTersePayload } from './types-BTonKlz8.js';
2
2
 
3
3
  /**
4
4
  * TerseJSON Core
package/dist/client.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- export { i as isTersePayload } from './types-CzaGQaV7.mjs';
2
- export { h as axiosInterceptor, f as createFetch, _ as default, e as expand, g as fetch, p as process, w as proxy, u as useTerseFetch } from './client-BQAZg7I8.mjs';
1
+ export { i as isTersePayload } from './types-BTonKlz8.mjs';
2
+ export { h as axiosInterceptor, f as createFetch, _ as default, e as expand, g as fetch, p as process, w as proxy, u as useTerseFetch } from './client-CFGvusCj.mjs';
package/dist/client.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { i as isTersePayload } from './types-CzaGQaV7.js';
2
- export { h as axiosInterceptor, f as createFetch, _ as default, e as expand, g as fetch, p as process, w as proxy, u as useTerseFetch } from './client-DOOGwp_p.js';
1
+ export { i as isTersePayload } from './types-BTonKlz8.js';
2
+ export { h as axiosInterceptor, f as createFetch, _ as default, e as expand, g as fetch, p as process, w as proxy, u as useTerseFetch } from './client-hUXNNGcN.js';
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/core.ts","../src/client.ts"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["/**\n * TerseJSON Types\n *\n * Defines the wire format and configuration options for transparent\n * JSON key compression.\n */\n\n/**\n * The compressed format sent over the wire\n */\nexport interface TersePayload<T = unknown> {\n /** Marker to identify this as a TerseJSON payload */\n __terse__: true;\n /** Version for future compatibility */\n v: 1;\n /** Key mapping: short key -> original key */\n k: Record<string, string>;\n /** The compressed data with short keys */\n d: T;\n /** Pattern used for key generation (for debugging/info) */\n p?: string;\n}\n\n/**\n * Built-in key pattern presets\n */\nexport type KeyPatternPreset =\n | 'alpha' // a, b, c, ... z, aa, ab (default)\n | 'numeric' // 0, 1, 2, ... 9, 10, 11\n | 'alphanumeric' // a1, a2, ... a9, b1, b2\n | 'short' // _, __, ___, a, b (shortest possible)\n | 'prefixed'; // k0, k1, k2 (with 'k' prefix)\n\n/**\n * Custom key generator function\n */\nexport type KeyGenerator = (index: number) => string;\n\n/**\n * Key pattern configuration\n */\nexport type KeyPattern =\n | KeyPatternPreset\n | { prefix: string; style?: 'numeric' | 'alpha' }\n | KeyGenerator;\n\n/**\n * How to handle nested structures\n */\nexport type NestedHandling =\n | 'deep' // Compress all nested objects/arrays (default)\n | 'shallow' // Only compress top-level array\n | 'arrays' // Only compress nested arrays, not single objects\n | number; // Specific depth limit (1 = shallow, Infinity = deep)\n\n/**\n * Compression options\n */\nexport interface CompressOptions {\n /**\n * Minimum key length to consider for compression\n * Keys shorter than this won't be shortened\n * @default 3\n */\n minKeyLength?: number;\n\n /**\n * Maximum depth to traverse for nested objects\n * @default 10\n */\n maxDepth?: number;\n\n /**\n * Key pattern to use for generating short keys\n * @default 'alpha'\n */\n keyPattern?: KeyPattern;\n\n /**\n * How to handle nested objects and arrays\n * @default 'deep'\n */\n nestedHandling?: NestedHandling;\n\n /**\n * Only compress keys that appear in all objects (homogeneous)\n * @default false\n */\n homogeneousOnly?: boolean;\n\n /**\n * Keys to always exclude from compression\n */\n excludeKeys?: string[];\n\n /**\n * Keys to always include in compression (even if short)\n */\n includeKeys?: string[];\n}\n\n/**\n * Configuration options for the Express middleware\n */\nexport interface TerseMiddlewareOptions extends CompressOptions {\n /**\n * Minimum array length to trigger compression\n * @default 2\n */\n minArrayLength?: number;\n\n /**\n * Custom function to determine if a response should be compressed\n * Return false to skip compression for specific responses\n */\n shouldCompress?: (data: unknown, req: unknown) => boolean;\n\n /**\n * Custom header name for signaling terse responses\n * @default 'x-terse-json'\n */\n headerName?: string;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n}\n\n/**\n * Configuration options for the client\n */\nexport interface TerseClientOptions {\n /**\n * Custom header name to check for terse responses\n * @default 'x-terse-json'\n */\n headerName?: string;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n\n /**\n * Automatically expand terse responses\n * @default true\n */\n autoExpand?: boolean;\n}\n\n/**\n * Type helper to preserve original types through compression\n */\nexport type Tersed<T> = T;\n\n/**\n * Check if a value is a TersePayload\n */\nexport function isTersePayload(value: unknown): value is TersePayload {\n return (\n typeof value === 'object' &&\n value !== null &&\n '__terse__' in value &&\n (value as TersePayload).__terse__ === true &&\n 'v' in value &&\n 'k' in value &&\n 'd' in value\n );\n}\n","/**\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 Client\n *\n * Transparent client-side handling of TerseJSON responses.\n * Use the fetch wrapper or manually expand responses.\n */\n\nimport {\n TerseClientOptions,\n isTersePayload,\n} from './types';\nimport { expand, wrapWithProxy } from './core';\n\nconst DEFAULT_OPTIONS: Required<TerseClientOptions> = {\n headerName: 'x-terse-json',\n debug: false,\n autoExpand: true,\n};\n\n/**\n * Expands a TerseJSON payload back to original format\n * Use this if you want full expansion (not proxied)\n */\nexport { expand };\n\n/**\n * Wraps a TerseJSON payload with Proxy for transparent key access\n * More memory efficient than full expansion for large datasets\n */\nexport { wrapWithProxy as proxy };\n\n/**\n * Check if a response/data is a TerseJSON payload\n */\nexport { isTersePayload };\n\n/**\n * Process a potential TerseJSON response\n * Automatically detects and expands/proxies terse payloads\n */\nexport function process<T = unknown>(\n data: unknown,\n options: { useProxy?: boolean } = {}\n): T {\n const { useProxy = true } = options;\n\n if (isTersePayload(data)) {\n return useProxy ? wrapWithProxy<T>(data) : expand<T>(data);\n }\n\n return data as T;\n}\n\n/**\n * Creates a fetch wrapper that automatically handles TerseJSON responses\n *\n * @example\n * ```typescript\n * import { createFetch } from 'tersejson/client';\n *\n * const fetch = createFetch();\n *\n * // Use exactly like regular fetch\n * const users = await fetch('/api/users').then(r => r.json());\n * console.log(users[0].firstName); // Works transparently!\n * ```\n */\nexport function createFetch(options: TerseClientOptions = {}): typeof globalThis.fetch {\n const config = { ...DEFAULT_OPTIONS, ...options };\n\n return async function terseFetch(\n input: RequestInfo | URL,\n init: RequestInit = {}\n ): Promise<Response> {\n // Add header to indicate we accept terse responses\n const headers = new Headers(init.headers);\n headers.set('accept-terse', 'true');\n\n const response = await globalThis.fetch(input, {\n ...init,\n headers,\n });\n\n // Check if response is terse\n const isTerse = response.headers.get(config.headerName) === 'true';\n\n if (!isTerse || !config.autoExpand) {\n return response;\n }\n\n // Clone response and override json() method\n const clonedResponse = response.clone();\n\n // Create a wrapper that intercepts .json()\n return new Proxy(response, {\n get(target, prop) {\n if (prop === 'json') {\n return async function (): Promise<unknown> {\n const data = await clonedResponse.json();\n\n if (isTersePayload(data)) {\n if (config.debug) {\n console.log('[tersejson] Expanding terse response');\n }\n return wrapWithProxy(data);\n }\n\n return data;\n };\n }\n\n const value = Reflect.get(target, prop);\n return typeof value === 'function' ? value.bind(target) : value;\n },\n });\n };\n}\n\n/**\n * Drop-in fetch replacement with TerseJSON support\n *\n * @example\n * ```typescript\n * import { fetch } from 'tersejson/client';\n *\n * const users = await fetch('/api/users').then(r => r.json());\n * console.log(users[0].firstName); // Transparent!\n * ```\n */\nexport const fetch = createFetch();\n\n/**\n * React hook for fetching with TerseJSON support (if using React)\n * Returns a configured fetch function\n *\n * @example\n * ```typescript\n * function MyComponent() {\n * const terseFetch = useTerseFetch();\n * // Use in useEffect, etc.\n * }\n * ```\n */\nexport function useTerseFetch(options: TerseClientOptions = {}): typeof globalThis.fetch {\n // This is a simple hook that just returns the configured fetch\n // Could be extended with caching, SWR integration, etc.\n return createFetch(options);\n}\n\n/**\n * Axios interceptor for TerseJSON support\n *\n * @example\n * ```typescript\n * import axios from 'axios';\n * import { axiosInterceptor } from 'tersejson/client';\n *\n * axios.interceptors.request.use(axiosInterceptor.request);\n * axios.interceptors.response.use(axiosInterceptor.response);\n * ```\n */\nexport const axiosInterceptor = {\n request: (config: { headers?: Record<string, string> }) => {\n config.headers = config.headers || {};\n config.headers['accept-terse'] = 'true';\n return config;\n },\n\n response: (response: { headers?: Record<string, string>; data?: unknown }) => {\n const isTerse = response.headers?.['x-terse-json'] === 'true';\n\n if (isTerse && isTersePayload(response.data)) {\n response.data = wrapWithProxy(response.data);\n }\n\n return response;\n },\n};\n\n// Default export for convenience\nexport default { fetch, createFetch, expand, proxy: wrapWithProxy, process };\n"]}
1
+ {"version":3,"sources":["../src/types.ts","../src/core.ts","../src/client.ts"],"names":[],"mappings":";;;;;AAiKO,SAAS,eAAe,KAAA,EAAuC;AACpE,EAAA,OACE,OAAO,KAAA,KAAU,QAAA,IACjB,KAAA,KAAU,QACV,WAAA,IAAe,KAAA,IACd,KAAA,CAAuB,SAAA,KAAc,IAAA,IACtC,GAAA,IAAO,KAAA,IACP,GAAA,IAAO,SACP,GAAA,IAAO,KAAA;AAEX;;;ACqGA,SAAS,YAAA,CACP,GAAA,EACA,UAAA,EACA,QAAA,EACA,eAAuB,CAAA,EACE;AACzB,EAAA,IAAI,YAAA,IAAgB,UAAU,OAAO,GAAA;AAErC,EAAA,MAAM,WAAoC,EAAC;AAE3C,EAAA,KAAA,MAAW,CAAC,QAAA,EAAU,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAA,EAAG;AACnD,IAAA,MAAM,WAAA,GAAc,UAAA,CAAW,GAAA,CAAI,QAAQ,CAAA,IAAK,QAAA;AAEhD,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,MAAA,QAAA,CAAS,WAAW,CAAA,GAAI,KAAA,CAAM,GAAA,CAAI,CAAA,IAAA,KAAQ;AACxC,QAAA,IAAI,OAAO,SAAS,QAAA,IAAY,IAAA,KAAS,QAAQ,CAAC,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,EAAG;AACrE,UAAA,OAAO,YAAA;AAAA,YACL,IAAA;AAAA,YACA,UAAA;AAAA,YACA,QAAA;AAAA,YACA,YAAA,GAAe;AAAA,WACjB;AAAA,QACF;AACA,QAAA,OAAO,IAAA;AAAA,MACT,CAAC,CAAA;AAAA,IACH,CAAA,MAAA,IAAW,OAAO,KAAA,KAAU,QAAA,IAAY,UAAU,IAAA,EAAM;AACtD,MAAA,QAAA,CAAS,WAAW,CAAA,GAAI,YAAA;AAAA,QACtB,KAAA;AAAA,QACA,UAAA;AAAA,QACA,QAAA;AAAA,QACA,YAAA,GAAe;AAAA,OACjB;AAAA,IACF,CAAA,MAAO;AACL,MAAA,QAAA,CAAS,WAAW,CAAA,GAAI,KAAA;AAAA,IAC1B;AAAA,EACF;AAEA,EAAA,OAAO,QAAA;AACT;AAwEO,SAAS,OAAoB,OAAA,EAA0B;AAC5D,EAAA,MAAM,aAAa,IAAI,GAAA;AAAA,IACrB,MAAA,CAAO,OAAA,CAAQ,OAAA,CAAQ,CAAC,EAAE,GAAA,CAAI,CAAC,CAAC,KAAA,EAAO,QAAQ,CAAA,KAAM,CAAC,KAAA,EAAO,QAAQ,CAAC;AAAA,GACxE;AAEA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,CAAC,CAAA,EAAG;AAC5B,IAAA,OAAO,OAAA,CAAQ,CAAA,CAAE,GAAA,CAAI,CAAA,IAAA,KAAQ;AAC3B,MAAA,IAAI,OAAO,SAAS,QAAA,IAAY,IAAA,KAAS,QAAQ,CAAC,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,EAAG;AACrE,QAAA,OAAO,YAAA,CAAa,IAAA,EAAiC,UAAA,EAAY,EAAE,CAAA;AAAA,MACrE;AACA,MAAA,OAAO,IAAA;AAAA,IACT,CAAC,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,OAAO,OAAA,CAAQ,CAAA,KAAM,QAAA,IAAY,OAAA,CAAQ,MAAM,IAAA,EAAM;AACvD,IAAA,OAAO,YAAA,CAAa,OAAA,CAAQ,CAAA,EAA8B,UAAA,EAAY,EAAE,CAAA;AAAA,EAC1E;AAEA,EAAA,OAAO,OAAA,CAAQ,CAAA;AACjB;AAMO,SAAS,gBAAA,CACd,YACA,MAAA,EACG;AAEH,EAAA,MAAM,kBAAkB,IAAI,GAAA;AAAA,IAC1B,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,CAAE,GAAA,CAAI,CAAC,CAAC,KAAA,EAAO,QAAQ,CAAA,KAAM,CAAC,QAAA,EAAU,KAAK,CAAC;AAAA,GACrE;AAEA,EAAA,MAAM,OAAA,GAAiD;AAAA,IACrD,GAAA,CAAI,QAAQ,IAAA,EAAuB;AACjC,MAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,QAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,MAAA,EAAQ,IAAI,CAAA;AAAA,MACjC;AAGA,MAAA,MAAM,QAAA,GAAW,eAAA,CAAgB,GAAA,CAAI,IAAI,CAAA;AACzC,MAAA,MAAM,YAAY,QAAA,IAAY,IAAA;AAC9B,MAAA,MAAM,KAAA,GAAQ,OAAO,SAAS,CAAA;AAG9B,MAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,QAAA,OAAO,KAAA,CAAM,IAAI,CAAA,IAAA,KAAQ;AACvB,UAAA,IAAI,OAAO,SAAS,QAAA,IAAY,IAAA,KAAS,QAAQ,CAAC,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,EAAG;AACrE,YAAA,OAAO,gBAAA,CAAiB,MAAiC,MAAM,CAAA;AAAA,UACjE;AACA,UAAA,OAAO,IAAA;AAAA,QACT,CAAC,CAAA;AAAA,MACH;AAEA,MAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,IAAA,EAAM;AAC/C,QAAA,OAAO,gBAAA,CAAiB,OAAkC,MAAM,CAAA;AAAA,MAClE;AAEA,MAAA,OAAO,KAAA;AAAA,IACT,CAAA;AAAA,IAEA,GAAA,CAAI,QAAQ,IAAA,EAAuB;AACjC,MAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,QAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,MAAA,EAAQ,IAAI,CAAA;AAAA,MACjC;AACA,MAAA,MAAM,QAAA,GAAW,eAAA,CAAgB,GAAA,CAAI,IAAI,CAAA;AACzC,MAAA,OAAA,CAAQ,YAAY,IAAA,KAAS,MAAA;AAAA,IAC/B,CAAA;AAAA,IAEA,QAAQ,MAAA,EAAQ;AAEd,MAAA,OAAO,MAAA,CAAO,KAAK,MAAM,CAAA,CAAE,IAAI,CAAA,QAAA,KAAY,MAAA,CAAO,QAAQ,CAAA,IAAK,QAAQ,CAAA;AAAA,IACzE,CAAA;AAAA,IAEA,wBAAA,CAAyB,QAAQ,IAAA,EAAuB;AACtD,MAAA,IAAI,OAAO,SAAS,QAAA,EAAU;AAC5B,QAAA,OAAO,OAAA,CAAQ,wBAAA,CAAyB,MAAA,EAAQ,IAAI,CAAA;AAAA,MACtD;AACA,MAAA,MAAM,QAAA,GAAW,eAAA,CAAgB,GAAA,CAAI,IAAI,CAAA;AACzC,MAAA,MAAM,YAAY,QAAA,IAAY,IAAA;AAC9B,MAAA,MAAM,UAAA,GAAa,MAAA,CAAO,wBAAA,CAAyB,MAAA,EAAQ,SAAS,CAAA;AACpE,MAAA,IAAI,UAAA,EAAY;AACd,QAAA,OAAO,EAAE,GAAG,UAAA,EAAY,UAAA,EAAY,IAAA,EAAM,cAAc,IAAA,EAAK;AAAA,MAC/D;AACA,MAAA,OAAO,MAAA;AAAA,IACT;AAAA,GACF;AAEA,EAAA,OAAO,IAAI,KAAA,CAAM,UAAA,EAAY,OAAO,CAAA;AACtC;AAKO,SAAS,cAAiB,OAAA,EAA0B;AACzD,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,CAAC,CAAA,EAAG;AAC5B,IAAA,OAAO,OAAA,CAAQ,CAAA,CAAE,GAAA,CAAI,CAAA,IAAA,KAAQ;AAC3B,MAAA,IAAI,OAAO,SAAS,QAAA,IAAY,IAAA,KAAS,QAAQ,CAAC,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,EAAG;AACrE,QAAA,OAAO,gBAAA,CAAiB,IAAA,EAAiC,OAAA,CAAQ,CAAC,CAAA;AAAA,MACpE;AACA,MAAA,OAAO,IAAA;AAAA,IACT,CAAC,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,OAAO,OAAA,CAAQ,CAAA,KAAM,QAAA,IAAY,OAAA,CAAQ,MAAM,IAAA,EAAM;AACvD,IAAA,OAAO,gBAAA,CAAiB,OAAA,CAAQ,CAAA,EAA8B,OAAA,CAAQ,CAAC,CAAA;AAAA,EACzE;AAEA,EAAA,OAAO,OAAA,CAAQ,CAAA;AACjB;;;AC/dA,IAAM,eAAA,GAAgD;AAAA,EACpD,UAAA,EAAY,cAAA;AAAA,EACZ,KAAA,EAAO,KAAA;AAAA,EACP,UAAA,EAAY;AACd,CAAA;AAuBO,SAAS,OAAA,CACd,IAAA,EACA,OAAA,GAAkC,EAAC,EAChC;AACH,EAAA,MAAM,EAAE,QAAA,GAAW,IAAA,EAAK,GAAI,OAAA;AAE5B,EAAA,IAAI,cAAA,CAAe,IAAI,CAAA,EAAG;AACxB,IAAA,OAAO,QAAA,GAAW,aAAA,CAAiB,IAAI,CAAA,GAAI,OAAU,IAAI,CAAA;AAAA,EAC3D;AAEA,EAAA,OAAO,IAAA;AACT;AAgBO,SAAS,WAAA,CAAY,OAAA,GAA8B,EAAC,EAA4B;AACrF,EAAA,MAAM,MAAA,GAAS,EAAE,GAAG,eAAA,EAAiB,GAAG,OAAA,EAAQ;AAEhD,EAAA,OAAO,eAAe,UAAA,CACpB,KAAA,EACA,IAAA,GAAoB,EAAC,EACF;AAEnB,IAAA,MAAM,OAAA,GAAU,IAAI,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA;AACxC,IAAA,OAAA,CAAQ,GAAA,CAAI,gBAAgB,MAAM,CAAA;AAElC,IAAA,MAAM,QAAA,GAAW,MAAM,UAAA,CAAW,KAAA,CAAM,KAAA,EAAO;AAAA,MAC7C,GAAG,IAAA;AAAA,MACH;AAAA,KACD,CAAA;AAGD,IAAA,MAAM,UAAU,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,MAAA,CAAO,UAAU,CAAA,KAAM,MAAA;AAE5D,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,MAAA,CAAO,UAAA,EAAY;AAClC,MAAA,OAAO,QAAA;AAAA,IACT;AAGA,IAAA,MAAM,cAAA,GAAiB,SAAS,KAAA,EAAM;AAGtC,IAAA,OAAO,IAAI,MAAM,QAAA,EAAU;AAAA,MACzB,GAAA,CAAI,QAAQ,IAAA,EAAM;AAChB,QAAA,IAAI,SAAS,MAAA,EAAQ;AACnB,UAAA,OAAO,iBAAoC;AACzC,YAAA,MAAM,IAAA,GAAO,MAAM,cAAA,CAAe,IAAA,EAAK;AAEvC,YAAA,IAAI,cAAA,CAAe,IAAI,CAAA,EAAG;AACxB,cAAA,IAAI,OAAO,KAAA,EAAO;AAChB,gBAAA,OAAA,CAAQ,IAAI,sCAAsC,CAAA;AAAA,cACpD;AACA,cAAA,OAAO,cAAc,IAAI,CAAA;AAAA,YAC3B;AAEA,YAAA,OAAO,IAAA;AAAA,UACT,CAAA;AAAA,QACF;AAEA,QAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,CAAI,MAAA,EAAQ,IAAI,CAAA;AACtC,QAAA,OAAO,OAAO,KAAA,KAAU,UAAA,GAAa,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA,GAAI,KAAA;AAAA,MAC5D;AAAA,KACD,CAAA;AAAA,EACH,CAAA;AACF;AAaO,IAAM,QAAQ,WAAA;AAcd,SAAS,aAAA,CAAc,OAAA,GAA8B,EAAC,EAA4B;AAGvF,EAAA,OAAO,YAAY,OAAO,CAAA;AAC5B;AAcO,IAAM,gBAAA,GAAmB;AAAA,EAC9B,OAAA,EAAS,CAAC,MAAA,KAAiD;AACzD,IAAA,MAAA,CAAO,OAAA,GAAU,MAAA,CAAO,OAAA,IAAW,EAAC;AACpC,IAAA,MAAA,CAAO,OAAA,CAAQ,cAAc,CAAA,GAAI,MAAA;AACjC,IAAA,OAAO,MAAA;AAAA,EACT,CAAA;AAAA,EAEA,QAAA,EAAU,CAAC,QAAA,KAAmE;AAC5E,IAAA,MAAM,OAAA,GAAU,QAAA,CAAS,OAAA,GAAU,cAAc,CAAA,KAAM,MAAA;AAEvD,IAAA,IAAI,OAAA,IAAW,cAAA,CAAe,QAAA,CAAS,IAAI,CAAA,EAAG;AAC5C,MAAA,QAAA,CAAS,IAAA,GAAO,aAAA,CAAc,QAAA,CAAS,IAAI,CAAA;AAAA,IAC7C;AAEA,IAAA,OAAO,QAAA;AAAA,EACT;AACF;AAGA,IAAO,iBAAQ,EAAE,KAAA,EAAO,aAAa,MAAA,EAAQ,KAAA,EAAO,eAAe,OAAA","file":"client.js","sourcesContent":["/**\n * TerseJSON Types\n *\n * Defines the wire format and configuration options for transparent\n * JSON key compression.\n */\n\n/**\n * The compressed format sent over the wire\n */\nexport interface TersePayload<T = unknown> {\n /** Marker to identify this as a TerseJSON payload */\n __terse__: true;\n /** Version for future compatibility */\n v: 1;\n /** Key mapping: short key -> original key */\n k: Record<string, string>;\n /** The compressed data with short keys */\n d: T;\n /** Pattern used for key generation (for debugging/info) */\n p?: string;\n}\n\n/**\n * Built-in key pattern presets\n */\nexport type KeyPatternPreset =\n | 'alpha' // a, b, c, ... z, aa, ab (default)\n | 'numeric' // 0, 1, 2, ... 9, 10, 11\n | 'alphanumeric' // a1, a2, ... a9, b1, b2\n | 'short' // _, __, ___, a, b (shortest possible)\n | 'prefixed'; // k0, k1, k2 (with 'k' prefix)\n\n/**\n * Custom key generator function\n */\nexport type KeyGenerator = (index: number) => string;\n\n/**\n * Key pattern configuration\n */\nexport type KeyPattern =\n | KeyPatternPreset\n | { prefix: string; style?: 'numeric' | 'alpha' }\n | KeyGenerator;\n\n/**\n * How to handle nested structures\n */\nexport type NestedHandling =\n | 'deep' // Compress all nested objects/arrays (default)\n | 'shallow' // Only compress top-level array\n | 'arrays' // Only compress nested arrays, not single objects\n | number; // Specific depth limit (1 = shallow, Infinity = deep)\n\n/**\n * Compression options\n */\nexport interface CompressOptions {\n /**\n * Minimum key length to consider for compression\n * Keys shorter than this won't be shortened\n * @default 3\n */\n minKeyLength?: number;\n\n /**\n * Maximum depth to traverse for nested objects\n * @default 10\n */\n maxDepth?: number;\n\n /**\n * Key pattern to use for generating short keys\n * @default 'alpha'\n */\n keyPattern?: KeyPattern;\n\n /**\n * How to handle nested objects and arrays\n * @default 'deep'\n */\n nestedHandling?: NestedHandling;\n\n /**\n * Only compress keys that appear in all objects (homogeneous)\n * @default false\n */\n homogeneousOnly?: boolean;\n\n /**\n * Keys to always exclude from compression\n */\n excludeKeys?: string[];\n\n /**\n * Keys to always include in compression (even if short)\n */\n includeKeys?: string[];\n}\n\n/**\n * Configuration options for the Express middleware\n */\nexport interface TerseMiddlewareOptions extends CompressOptions {\n /**\n * Minimum array length to trigger compression\n * @default 2\n */\n minArrayLength?: number;\n\n /**\n * Custom function to determine if a response should be compressed\n * Return false to skip compression for specific responses\n */\n shouldCompress?: (data: unknown, req: unknown) => boolean;\n\n /**\n * Custom header name for signaling terse responses\n * @default 'x-terse-json'\n */\n headerName?: string;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n}\n\n/**\n * Configuration options for the client\n */\nexport interface TerseClientOptions {\n /**\n * Custom header name to check for terse responses\n * @default 'x-terse-json'\n */\n headerName?: string;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n\n /**\n * Automatically expand terse responses\n * @default true\n */\n autoExpand?: boolean;\n}\n\n/**\n * Type helper to preserve original types through compression\n */\nexport type Tersed<T> = T;\n\n/**\n * Check if a value is a TersePayload\n */\nexport function isTersePayload(value: unknown): value is TersePayload {\n return (\n typeof value === 'object' &&\n value !== null &&\n '__terse__' in value &&\n (value as TersePayload).__terse__ === true &&\n 'v' in value &&\n 'k' in value &&\n 'd' in value\n );\n}\n\n/**\n * GraphQL terse metadata (attached to response)\n */\nexport interface GraphQLTerseMetadata {\n /** Version for compatibility */\n v: 1;\n /** Key mapping: short key -> original key */\n k: Record<string, string>;\n /** JSON paths to compressed arrays (e.g., [\"data.users\", \"data.products\"]) */\n paths: string[];\n}\n\n/**\n * GraphQL response with terse compression\n */\nexport interface GraphQLTerseResponse<T = unknown> {\n /** The compressed data with short keys in arrays */\n data: T;\n /** GraphQL errors (untouched) */\n errors?: Array<{ message: string; [key: string]: unknown }>;\n /** GraphQL extensions (untouched) */\n extensions?: Record<string, unknown>;\n /** Terse metadata */\n __terse__: GraphQLTerseMetadata;\n}\n\n/**\n * GraphQL middleware options\n */\nexport interface GraphQLTerseOptions extends CompressOptions {\n /**\n * Minimum array length to trigger compression\n * @default 2\n */\n minArrayLength?: number;\n\n /**\n * Custom function to determine if a path should be compressed\n * Return false to skip compression for specific paths\n */\n shouldCompress?: (data: unknown, path: string) => boolean;\n\n /**\n * Paths to exclude from compression (e.g., [\"data.config\"])\n */\n excludePaths?: string[];\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n}\n\n/**\n * Check if a value is a GraphQL terse response\n */\nexport function isGraphQLTersePayload(value: unknown): value is GraphQLTerseResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'data' in value &&\n '__terse__' in value &&\n typeof (value as GraphQLTerseResponse).__terse__ === 'object' &&\n (value as GraphQLTerseResponse).__terse__ !== null &&\n 'v' in (value as GraphQLTerseResponse).__terse__ &&\n 'k' in (value as GraphQLTerseResponse).__terse__ &&\n 'paths' in (value as GraphQLTerseResponse).__terse__\n );\n}\n","/**\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 Client\n *\n * Transparent client-side handling of TerseJSON responses.\n * Use the fetch wrapper or manually expand responses.\n */\n\nimport {\n TerseClientOptions,\n isTersePayload,\n} from './types';\nimport { expand, wrapWithProxy } from './core';\n\nconst DEFAULT_OPTIONS: Required<TerseClientOptions> = {\n headerName: 'x-terse-json',\n debug: false,\n autoExpand: true,\n};\n\n/**\n * Expands a TerseJSON payload back to original format\n * Use this if you want full expansion (not proxied)\n */\nexport { expand };\n\n/**\n * Wraps a TerseJSON payload with Proxy for transparent key access\n * More memory efficient than full expansion for large datasets\n */\nexport { wrapWithProxy as proxy };\n\n/**\n * Check if a response/data is a TerseJSON payload\n */\nexport { isTersePayload };\n\n/**\n * Process a potential TerseJSON response\n * Automatically detects and expands/proxies terse payloads\n */\nexport function process<T = unknown>(\n data: unknown,\n options: { useProxy?: boolean } = {}\n): T {\n const { useProxy = true } = options;\n\n if (isTersePayload(data)) {\n return useProxy ? wrapWithProxy<T>(data) : expand<T>(data);\n }\n\n return data as T;\n}\n\n/**\n * Creates a fetch wrapper that automatically handles TerseJSON responses\n *\n * @example\n * ```typescript\n * import { createFetch } from 'tersejson/client';\n *\n * const fetch = createFetch();\n *\n * // Use exactly like regular fetch\n * const users = await fetch('/api/users').then(r => r.json());\n * console.log(users[0].firstName); // Works transparently!\n * ```\n */\nexport function createFetch(options: TerseClientOptions = {}): typeof globalThis.fetch {\n const config = { ...DEFAULT_OPTIONS, ...options };\n\n return async function terseFetch(\n input: RequestInfo | URL,\n init: RequestInit = {}\n ): Promise<Response> {\n // Add header to indicate we accept terse responses\n const headers = new Headers(init.headers);\n headers.set('accept-terse', 'true');\n\n const response = await globalThis.fetch(input, {\n ...init,\n headers,\n });\n\n // Check if response is terse\n const isTerse = response.headers.get(config.headerName) === 'true';\n\n if (!isTerse || !config.autoExpand) {\n return response;\n }\n\n // Clone response and override json() method\n const clonedResponse = response.clone();\n\n // Create a wrapper that intercepts .json()\n return new Proxy(response, {\n get(target, prop) {\n if (prop === 'json') {\n return async function (): Promise<unknown> {\n const data = await clonedResponse.json();\n\n if (isTersePayload(data)) {\n if (config.debug) {\n console.log('[tersejson] Expanding terse response');\n }\n return wrapWithProxy(data);\n }\n\n return data;\n };\n }\n\n const value = Reflect.get(target, prop);\n return typeof value === 'function' ? value.bind(target) : value;\n },\n });\n };\n}\n\n/**\n * Drop-in fetch replacement with TerseJSON support\n *\n * @example\n * ```typescript\n * import { fetch } from 'tersejson/client';\n *\n * const users = await fetch('/api/users').then(r => r.json());\n * console.log(users[0].firstName); // Transparent!\n * ```\n */\nexport const fetch = createFetch();\n\n/**\n * React hook for fetching with TerseJSON support (if using React)\n * Returns a configured fetch function\n *\n * @example\n * ```typescript\n * function MyComponent() {\n * const terseFetch = useTerseFetch();\n * // Use in useEffect, etc.\n * }\n * ```\n */\nexport function useTerseFetch(options: TerseClientOptions = {}): typeof globalThis.fetch {\n // This is a simple hook that just returns the configured fetch\n // Could be extended with caching, SWR integration, etc.\n return createFetch(options);\n}\n\n/**\n * Axios interceptor for TerseJSON support\n *\n * @example\n * ```typescript\n * import axios from 'axios';\n * import { axiosInterceptor } from 'tersejson/client';\n *\n * axios.interceptors.request.use(axiosInterceptor.request);\n * axios.interceptors.response.use(axiosInterceptor.response);\n * ```\n */\nexport const axiosInterceptor = {\n request: (config: { headers?: Record<string, string> }) => {\n config.headers = config.headers || {};\n config.headers['accept-terse'] = 'true';\n return config;\n },\n\n response: (response: { headers?: Record<string, string>; data?: unknown }) => {\n const isTerse = response.headers?.['x-terse-json'] === 'true';\n\n if (isTerse && isTersePayload(response.data)) {\n response.data = wrapWithProxy(response.data);\n }\n\n return response;\n },\n};\n\n// Default export for convenience\nexport default { fetch, createFetch, expand, proxy: wrapWithProxy, process };\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/core.ts","../src/client.ts"],"names":[],"mappings":"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","file":"client.mjs","sourcesContent":["/**\n * TerseJSON Types\n *\n * Defines the wire format and configuration options for transparent\n * JSON key compression.\n */\n\n/**\n * The compressed format sent over the wire\n */\nexport interface TersePayload<T = unknown> {\n /** Marker to identify this as a TerseJSON payload */\n __terse__: true;\n /** Version for future compatibility */\n v: 1;\n /** Key mapping: short key -> original key */\n k: Record<string, string>;\n /** The compressed data with short keys */\n d: T;\n /** Pattern used for key generation (for debugging/info) */\n p?: string;\n}\n\n/**\n * Built-in key pattern presets\n */\nexport type KeyPatternPreset =\n | 'alpha' // a, b, c, ... z, aa, ab (default)\n | 'numeric' // 0, 1, 2, ... 9, 10, 11\n | 'alphanumeric' // a1, a2, ... a9, b1, b2\n | 'short' // _, __, ___, a, b (shortest possible)\n | 'prefixed'; // k0, k1, k2 (with 'k' prefix)\n\n/**\n * Custom key generator function\n */\nexport type KeyGenerator = (index: number) => string;\n\n/**\n * Key pattern configuration\n */\nexport type KeyPattern =\n | KeyPatternPreset\n | { prefix: string; style?: 'numeric' | 'alpha' }\n | KeyGenerator;\n\n/**\n * How to handle nested structures\n */\nexport type NestedHandling =\n | 'deep' // Compress all nested objects/arrays (default)\n | 'shallow' // Only compress top-level array\n | 'arrays' // Only compress nested arrays, not single objects\n | number; // Specific depth limit (1 = shallow, Infinity = deep)\n\n/**\n * Compression options\n */\nexport interface CompressOptions {\n /**\n * Minimum key length to consider for compression\n * Keys shorter than this won't be shortened\n * @default 3\n */\n minKeyLength?: number;\n\n /**\n * Maximum depth to traverse for nested objects\n * @default 10\n */\n maxDepth?: number;\n\n /**\n * Key pattern to use for generating short keys\n * @default 'alpha'\n */\n keyPattern?: KeyPattern;\n\n /**\n * How to handle nested objects and arrays\n * @default 'deep'\n */\n nestedHandling?: NestedHandling;\n\n /**\n * Only compress keys that appear in all objects (homogeneous)\n * @default false\n */\n homogeneousOnly?: boolean;\n\n /**\n * Keys to always exclude from compression\n */\n excludeKeys?: string[];\n\n /**\n * Keys to always include in compression (even if short)\n */\n includeKeys?: string[];\n}\n\n/**\n * Configuration options for the Express middleware\n */\nexport interface TerseMiddlewareOptions extends CompressOptions {\n /**\n * Minimum array length to trigger compression\n * @default 2\n */\n minArrayLength?: number;\n\n /**\n * Custom function to determine if a response should be compressed\n * Return false to skip compression for specific responses\n */\n shouldCompress?: (data: unknown, req: unknown) => boolean;\n\n /**\n * Custom header name for signaling terse responses\n * @default 'x-terse-json'\n */\n headerName?: string;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n}\n\n/**\n * Configuration options for the client\n */\nexport interface TerseClientOptions {\n /**\n * Custom header name to check for terse responses\n * @default 'x-terse-json'\n */\n headerName?: string;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n\n /**\n * Automatically expand terse responses\n * @default true\n */\n autoExpand?: boolean;\n}\n\n/**\n * Type helper to preserve original types through compression\n */\nexport type Tersed<T> = T;\n\n/**\n * Check if a value is a TersePayload\n */\nexport function isTersePayload(value: unknown): value is TersePayload {\n return (\n typeof value === 'object' &&\n value !== null &&\n '__terse__' in value &&\n (value as TersePayload).__terse__ === true &&\n 'v' in value &&\n 'k' in value &&\n 'd' in value\n );\n}\n","/**\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 Client\n *\n * Transparent client-side handling of TerseJSON responses.\n * Use the fetch wrapper or manually expand responses.\n */\n\nimport {\n TerseClientOptions,\n isTersePayload,\n} from './types';\nimport { expand, wrapWithProxy } from './core';\n\nconst DEFAULT_OPTIONS: Required<TerseClientOptions> = {\n headerName: 'x-terse-json',\n debug: false,\n autoExpand: true,\n};\n\n/**\n * Expands a TerseJSON payload back to original format\n * Use this if you want full expansion (not proxied)\n */\nexport { expand };\n\n/**\n * Wraps a TerseJSON payload with Proxy for transparent key access\n * More memory efficient than full expansion for large datasets\n */\nexport { wrapWithProxy as proxy };\n\n/**\n * Check if a response/data is a TerseJSON payload\n */\nexport { isTersePayload };\n\n/**\n * Process a potential TerseJSON response\n * Automatically detects and expands/proxies terse payloads\n */\nexport function process<T = unknown>(\n data: unknown,\n options: { useProxy?: boolean } = {}\n): T {\n const { useProxy = true } = options;\n\n if (isTersePayload(data)) {\n return useProxy ? wrapWithProxy<T>(data) : expand<T>(data);\n }\n\n return data as T;\n}\n\n/**\n * Creates a fetch wrapper that automatically handles TerseJSON responses\n *\n * @example\n * ```typescript\n * import { createFetch } from 'tersejson/client';\n *\n * const fetch = createFetch();\n *\n * // Use exactly like regular fetch\n * const users = await fetch('/api/users').then(r => r.json());\n * console.log(users[0].firstName); // Works transparently!\n * ```\n */\nexport function createFetch(options: TerseClientOptions = {}): typeof globalThis.fetch {\n const config = { ...DEFAULT_OPTIONS, ...options };\n\n return async function terseFetch(\n input: RequestInfo | URL,\n init: RequestInit = {}\n ): Promise<Response> {\n // Add header to indicate we accept terse responses\n const headers = new Headers(init.headers);\n headers.set('accept-terse', 'true');\n\n const response = await globalThis.fetch(input, {\n ...init,\n headers,\n });\n\n // Check if response is terse\n const isTerse = response.headers.get(config.headerName) === 'true';\n\n if (!isTerse || !config.autoExpand) {\n return response;\n }\n\n // Clone response and override json() method\n const clonedResponse = response.clone();\n\n // Create a wrapper that intercepts .json()\n return new Proxy(response, {\n get(target, prop) {\n if (prop === 'json') {\n return async function (): Promise<unknown> {\n const data = await clonedResponse.json();\n\n if (isTersePayload(data)) {\n if (config.debug) {\n console.log('[tersejson] Expanding terse response');\n }\n return wrapWithProxy(data);\n }\n\n return data;\n };\n }\n\n const value = Reflect.get(target, prop);\n return typeof value === 'function' ? value.bind(target) : value;\n },\n });\n };\n}\n\n/**\n * Drop-in fetch replacement with TerseJSON support\n *\n * @example\n * ```typescript\n * import { fetch } from 'tersejson/client';\n *\n * const users = await fetch('/api/users').then(r => r.json());\n * console.log(users[0].firstName); // Transparent!\n * ```\n */\nexport const fetch = createFetch();\n\n/**\n * React hook for fetching with TerseJSON support (if using React)\n * Returns a configured fetch function\n *\n * @example\n * ```typescript\n * function MyComponent() {\n * const terseFetch = useTerseFetch();\n * // Use in useEffect, etc.\n * }\n * ```\n */\nexport function useTerseFetch(options: TerseClientOptions = {}): typeof globalThis.fetch {\n // This is a simple hook that just returns the configured fetch\n // Could be extended with caching, SWR integration, etc.\n return createFetch(options);\n}\n\n/**\n * Axios interceptor for TerseJSON support\n *\n * @example\n * ```typescript\n * import axios from 'axios';\n * import { axiosInterceptor } from 'tersejson/client';\n *\n * axios.interceptors.request.use(axiosInterceptor.request);\n * axios.interceptors.response.use(axiosInterceptor.response);\n * ```\n */\nexport const axiosInterceptor = {\n request: (config: { headers?: Record<string, string> }) => {\n config.headers = config.headers || {};\n config.headers['accept-terse'] = 'true';\n return config;\n },\n\n response: (response: { headers?: Record<string, string>; data?: unknown }) => {\n const isTerse = response.headers?.['x-terse-json'] === 'true';\n\n if (isTerse && isTersePayload(response.data)) {\n response.data = wrapWithProxy(response.data);\n }\n\n return response;\n },\n};\n\n// Default export for convenience\nexport default { fetch, createFetch, expand, proxy: wrapWithProxy, process };\n"]}
1
+ {"version":3,"sources":["../src/types.ts","../src/core.ts","../src/client.ts"],"names":[],"mappings":"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","file":"client.mjs","sourcesContent":["/**\n * TerseJSON Types\n *\n * Defines the wire format and configuration options for transparent\n * JSON key compression.\n */\n\n/**\n * The compressed format sent over the wire\n */\nexport interface TersePayload<T = unknown> {\n /** Marker to identify this as a TerseJSON payload */\n __terse__: true;\n /** Version for future compatibility */\n v: 1;\n /** Key mapping: short key -> original key */\n k: Record<string, string>;\n /** The compressed data with short keys */\n d: T;\n /** Pattern used for key generation (for debugging/info) */\n p?: string;\n}\n\n/**\n * Built-in key pattern presets\n */\nexport type KeyPatternPreset =\n | 'alpha' // a, b, c, ... z, aa, ab (default)\n | 'numeric' // 0, 1, 2, ... 9, 10, 11\n | 'alphanumeric' // a1, a2, ... a9, b1, b2\n | 'short' // _, __, ___, a, b (shortest possible)\n | 'prefixed'; // k0, k1, k2 (with 'k' prefix)\n\n/**\n * Custom key generator function\n */\nexport type KeyGenerator = (index: number) => string;\n\n/**\n * Key pattern configuration\n */\nexport type KeyPattern =\n | KeyPatternPreset\n | { prefix: string; style?: 'numeric' | 'alpha' }\n | KeyGenerator;\n\n/**\n * How to handle nested structures\n */\nexport type NestedHandling =\n | 'deep' // Compress all nested objects/arrays (default)\n | 'shallow' // Only compress top-level array\n | 'arrays' // Only compress nested arrays, not single objects\n | number; // Specific depth limit (1 = shallow, Infinity = deep)\n\n/**\n * Compression options\n */\nexport interface CompressOptions {\n /**\n * Minimum key length to consider for compression\n * Keys shorter than this won't be shortened\n * @default 3\n */\n minKeyLength?: number;\n\n /**\n * Maximum depth to traverse for nested objects\n * @default 10\n */\n maxDepth?: number;\n\n /**\n * Key pattern to use for generating short keys\n * @default 'alpha'\n */\n keyPattern?: KeyPattern;\n\n /**\n * How to handle nested objects and arrays\n * @default 'deep'\n */\n nestedHandling?: NestedHandling;\n\n /**\n * Only compress keys that appear in all objects (homogeneous)\n * @default false\n */\n homogeneousOnly?: boolean;\n\n /**\n * Keys to always exclude from compression\n */\n excludeKeys?: string[];\n\n /**\n * Keys to always include in compression (even if short)\n */\n includeKeys?: string[];\n}\n\n/**\n * Configuration options for the Express middleware\n */\nexport interface TerseMiddlewareOptions extends CompressOptions {\n /**\n * Minimum array length to trigger compression\n * @default 2\n */\n minArrayLength?: number;\n\n /**\n * Custom function to determine if a response should be compressed\n * Return false to skip compression for specific responses\n */\n shouldCompress?: (data: unknown, req: unknown) => boolean;\n\n /**\n * Custom header name for signaling terse responses\n * @default 'x-terse-json'\n */\n headerName?: string;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n}\n\n/**\n * Configuration options for the client\n */\nexport interface TerseClientOptions {\n /**\n * Custom header name to check for terse responses\n * @default 'x-terse-json'\n */\n headerName?: string;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n\n /**\n * Automatically expand terse responses\n * @default true\n */\n autoExpand?: boolean;\n}\n\n/**\n * Type helper to preserve original types through compression\n */\nexport type Tersed<T> = T;\n\n/**\n * Check if a value is a TersePayload\n */\nexport function isTersePayload(value: unknown): value is TersePayload {\n return (\n typeof value === 'object' &&\n value !== null &&\n '__terse__' in value &&\n (value as TersePayload).__terse__ === true &&\n 'v' in value &&\n 'k' in value &&\n 'd' in value\n );\n}\n\n/**\n * GraphQL terse metadata (attached to response)\n */\nexport interface GraphQLTerseMetadata {\n /** Version for compatibility */\n v: 1;\n /** Key mapping: short key -> original key */\n k: Record<string, string>;\n /** JSON paths to compressed arrays (e.g., [\"data.users\", \"data.products\"]) */\n paths: string[];\n}\n\n/**\n * GraphQL response with terse compression\n */\nexport interface GraphQLTerseResponse<T = unknown> {\n /** The compressed data with short keys in arrays */\n data: T;\n /** GraphQL errors (untouched) */\n errors?: Array<{ message: string; [key: string]: unknown }>;\n /** GraphQL extensions (untouched) */\n extensions?: Record<string, unknown>;\n /** Terse metadata */\n __terse__: GraphQLTerseMetadata;\n}\n\n/**\n * GraphQL middleware options\n */\nexport interface GraphQLTerseOptions extends CompressOptions {\n /**\n * Minimum array length to trigger compression\n * @default 2\n */\n minArrayLength?: number;\n\n /**\n * Custom function to determine if a path should be compressed\n * Return false to skip compression for specific paths\n */\n shouldCompress?: (data: unknown, path: string) => boolean;\n\n /**\n * Paths to exclude from compression (e.g., [\"data.config\"])\n */\n excludePaths?: string[];\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n}\n\n/**\n * Check if a value is a GraphQL terse response\n */\nexport function isGraphQLTersePayload(value: unknown): value is GraphQLTerseResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'data' in value &&\n '__terse__' in value &&\n typeof (value as GraphQLTerseResponse).__terse__ === 'object' &&\n (value as GraphQLTerseResponse).__terse__ !== null &&\n 'v' in (value as GraphQLTerseResponse).__terse__ &&\n 'k' in (value as GraphQLTerseResponse).__terse__ &&\n 'paths' in (value as GraphQLTerseResponse).__terse__\n );\n}\n","/**\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 Client\n *\n * Transparent client-side handling of TerseJSON responses.\n * Use the fetch wrapper or manually expand responses.\n */\n\nimport {\n TerseClientOptions,\n isTersePayload,\n} from './types';\nimport { expand, wrapWithProxy } from './core';\n\nconst DEFAULT_OPTIONS: Required<TerseClientOptions> = {\n headerName: 'x-terse-json',\n debug: false,\n autoExpand: true,\n};\n\n/**\n * Expands a TerseJSON payload back to original format\n * Use this if you want full expansion (not proxied)\n */\nexport { expand };\n\n/**\n * Wraps a TerseJSON payload with Proxy for transparent key access\n * More memory efficient than full expansion for large datasets\n */\nexport { wrapWithProxy as proxy };\n\n/**\n * Check if a response/data is a TerseJSON payload\n */\nexport { isTersePayload };\n\n/**\n * Process a potential TerseJSON response\n * Automatically detects and expands/proxies terse payloads\n */\nexport function process<T = unknown>(\n data: unknown,\n options: { useProxy?: boolean } = {}\n): T {\n const { useProxy = true } = options;\n\n if (isTersePayload(data)) {\n return useProxy ? wrapWithProxy<T>(data) : expand<T>(data);\n }\n\n return data as T;\n}\n\n/**\n * Creates a fetch wrapper that automatically handles TerseJSON responses\n *\n * @example\n * ```typescript\n * import { createFetch } from 'tersejson/client';\n *\n * const fetch = createFetch();\n *\n * // Use exactly like regular fetch\n * const users = await fetch('/api/users').then(r => r.json());\n * console.log(users[0].firstName); // Works transparently!\n * ```\n */\nexport function createFetch(options: TerseClientOptions = {}): typeof globalThis.fetch {\n const config = { ...DEFAULT_OPTIONS, ...options };\n\n return async function terseFetch(\n input: RequestInfo | URL,\n init: RequestInit = {}\n ): Promise<Response> {\n // Add header to indicate we accept terse responses\n const headers = new Headers(init.headers);\n headers.set('accept-terse', 'true');\n\n const response = await globalThis.fetch(input, {\n ...init,\n headers,\n });\n\n // Check if response is terse\n const isTerse = response.headers.get(config.headerName) === 'true';\n\n if (!isTerse || !config.autoExpand) {\n return response;\n }\n\n // Clone response and override json() method\n const clonedResponse = response.clone();\n\n // Create a wrapper that intercepts .json()\n return new Proxy(response, {\n get(target, prop) {\n if (prop === 'json') {\n return async function (): Promise<unknown> {\n const data = await clonedResponse.json();\n\n if (isTersePayload(data)) {\n if (config.debug) {\n console.log('[tersejson] Expanding terse response');\n }\n return wrapWithProxy(data);\n }\n\n return data;\n };\n }\n\n const value = Reflect.get(target, prop);\n return typeof value === 'function' ? value.bind(target) : value;\n },\n });\n };\n}\n\n/**\n * Drop-in fetch replacement with TerseJSON support\n *\n * @example\n * ```typescript\n * import { fetch } from 'tersejson/client';\n *\n * const users = await fetch('/api/users').then(r => r.json());\n * console.log(users[0].firstName); // Transparent!\n * ```\n */\nexport const fetch = createFetch();\n\n/**\n * React hook for fetching with TerseJSON support (if using React)\n * Returns a configured fetch function\n *\n * @example\n * ```typescript\n * function MyComponent() {\n * const terseFetch = useTerseFetch();\n * // Use in useEffect, etc.\n * }\n * ```\n */\nexport function useTerseFetch(options: TerseClientOptions = {}): typeof globalThis.fetch {\n // This is a simple hook that just returns the configured fetch\n // Could be extended with caching, SWR integration, etc.\n return createFetch(options);\n}\n\n/**\n * Axios interceptor for TerseJSON support\n *\n * @example\n * ```typescript\n * import axios from 'axios';\n * import { axiosInterceptor } from 'tersejson/client';\n *\n * axios.interceptors.request.use(axiosInterceptor.request);\n * axios.interceptors.response.use(axiosInterceptor.response);\n * ```\n */\nexport const axiosInterceptor = {\n request: (config: { headers?: Record<string, string> }) => {\n config.headers = config.headers || {};\n config.headers['accept-terse'] = 'true';\n return config;\n },\n\n response: (response: { headers?: Record<string, string>; data?: unknown }) => {\n const isTerse = response.headers?.['x-terse-json'] === 'true';\n\n if (isTerse && isTersePayload(response.data)) {\n response.data = wrapWithProxy(response.data);\n }\n\n return response;\n },\n};\n\n// Default export for convenience\nexport default { fetch, createFetch, expand, proxy: wrapWithProxy, process };\n"]}
@@ -1,5 +1,5 @@
1
1
  import { RequestHandler } from 'express';
2
- import { a as TerseMiddlewareOptions } from './types-CzaGQaV7.js';
2
+ import { a as TerseMiddlewareOptions } from './types-BTonKlz8.js';
3
3
  import analytics, { AnalyticsConfig, TerseAnalytics, AnalyticsStats, CompressionEvent, getAnalytics, initAnalytics } from './analytics.js';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { RequestHandler } from 'express';
2
- import { a as TerseMiddlewareOptions } from './types-CzaGQaV7.mjs';
2
+ import { a as TerseMiddlewareOptions } from './types-BTonKlz8.mjs';
3
3
  import analytics, { AnalyticsConfig, TerseAnalytics, AnalyticsStats, CompressionEvent, getAnalytics, initAnalytics } from './analytics.mjs';
4
4
 
5
5
  /**
@@ -1,4 +1,4 @@
1
1
  import 'express';
2
- import './types-CzaGQaV7.mjs';
2
+ import './types-BTonKlz8.mjs';
3
3
  export { AnalyticsConfig, AnalyticsStats, CompressionEvent, TerseAnalytics, default as analytics, getAnalytics, initAnalytics } from './analytics.mjs';
4
- export { T as TerseMiddlewareOptionsWithAnalytics, t as default, t as terse, a as terseQueryParam } from './express-BoL__Ao6.mjs';
4
+ export { T as TerseMiddlewareOptionsWithAnalytics, t as default, t as terse, a as terseQueryParam } from './express-O5w2NBTf.mjs';
package/dist/express.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import 'express';
2
- import './types-CzaGQaV7.js';
2
+ import './types-BTonKlz8.js';
3
3
  export { AnalyticsConfig, AnalyticsStats, CompressionEvent, TerseAnalytics, default as analytics, getAnalytics, initAnalytics } from './analytics.js';
4
- export { T as TerseMiddlewareOptionsWithAnalytics, t as default, t as terse, a as terseQueryParam } from './express-LSVylWpN.js';
4
+ export { T as TerseMiddlewareOptionsWithAnalytics, t as default, t as terse, a as terseQueryParam } from './express-Da7WcJtt.js';
@@ -0,0 +1,81 @@
1
+ import { RequestHandler } from 'express';
2
+ import { g as GraphQLTerseOptions, f as GraphQLTerseResponse } from './types-BTonKlz8.mjs';
3
+
4
+ /**
5
+ * TerseJSON GraphQL Middleware
6
+ *
7
+ * Integration for express-graphql that compresses arrays within GraphQL responses.
8
+ */
9
+
10
+ /**
11
+ * Result from finding compressible arrays
12
+ */
13
+ interface CompressibleArrayInfo {
14
+ path: string;
15
+ data: Record<string, unknown>[];
16
+ }
17
+ /**
18
+ * Finds all compressible arrays within a data structure
19
+ */
20
+ declare function findCompressibleArrays(data: unknown, basePath: string | undefined, options: {
21
+ minArrayLength: number;
22
+ excludePaths: string[];
23
+ maxDepth: number;
24
+ }, currentDepth?: number): CompressibleArrayInfo[];
25
+ /**
26
+ * Compresses a GraphQL response in-place
27
+ */
28
+ declare function compressGraphQLResponse<T extends {
29
+ data?: unknown;
30
+ }>(response: T, options?: GraphQLTerseOptions): GraphQLTerseResponse<T['data']> | T;
31
+ /**
32
+ * Creates a format function for express-graphql
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * import { graphqlHTTP } from 'express-graphql';
37
+ * import { createTerseFormatFn } from 'tersejson/graphql';
38
+ *
39
+ * app.use('/graphql', graphqlHTTP({
40
+ * schema: mySchema,
41
+ * formatResult: createTerseFormatFn({ debug: true }),
42
+ * }));
43
+ * ```
44
+ */
45
+ declare function createTerseFormatFn(options?: GraphQLTerseOptions): (result: {
46
+ data?: unknown;
47
+ errors?: unknown[];
48
+ extensions?: Record<string, unknown>;
49
+ }, _context?: unknown, _info?: unknown) => GraphQLTerseResponse<unknown> | {
50
+ data?: unknown;
51
+ errors?: unknown[];
52
+ extensions?: Record<string, unknown>;
53
+ };
54
+ /**
55
+ * Express middleware wrapper for express-graphql
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * import { graphqlHTTP } from 'express-graphql';
60
+ * import { terseGraphQL } from 'tersejson/graphql';
61
+ *
62
+ * app.use('/graphql', terseGraphQL(
63
+ * graphqlHTTP({
64
+ * schema: mySchema,
65
+ * graphiql: true,
66
+ * }),
67
+ * { debug: true }
68
+ * ));
69
+ * ```
70
+ */
71
+ declare function terseGraphQL(graphqlMiddleware: RequestHandler, options?: GraphQLTerseOptions): RequestHandler;
72
+
73
+ declare const graphql_compressGraphQLResponse: typeof compressGraphQLResponse;
74
+ declare const graphql_createTerseFormatFn: typeof createTerseFormatFn;
75
+ declare const graphql_findCompressibleArrays: typeof findCompressibleArrays;
76
+ declare const graphql_terseGraphQL: typeof terseGraphQL;
77
+ declare namespace graphql {
78
+ export { graphql_compressGraphQLResponse as compressGraphQLResponse, graphql_createTerseFormatFn as createTerseFormatFn, terseGraphQL as default, graphql_findCompressibleArrays as findCompressibleArrays, graphql_terseGraphQL as terseGraphQL };
79
+ }
80
+
81
+ export { createTerseFormatFn as a, compressGraphQLResponse as c, findCompressibleArrays as f, graphql as g, terseGraphQL as t };
@@ -0,0 +1,81 @@
1
+ import { RequestHandler } from 'express';
2
+ import { g as GraphQLTerseOptions, f as GraphQLTerseResponse } from './types-BTonKlz8.js';
3
+
4
+ /**
5
+ * TerseJSON GraphQL Middleware
6
+ *
7
+ * Integration for express-graphql that compresses arrays within GraphQL responses.
8
+ */
9
+
10
+ /**
11
+ * Result from finding compressible arrays
12
+ */
13
+ interface CompressibleArrayInfo {
14
+ path: string;
15
+ data: Record<string, unknown>[];
16
+ }
17
+ /**
18
+ * Finds all compressible arrays within a data structure
19
+ */
20
+ declare function findCompressibleArrays(data: unknown, basePath: string | undefined, options: {
21
+ minArrayLength: number;
22
+ excludePaths: string[];
23
+ maxDepth: number;
24
+ }, currentDepth?: number): CompressibleArrayInfo[];
25
+ /**
26
+ * Compresses a GraphQL response in-place
27
+ */
28
+ declare function compressGraphQLResponse<T extends {
29
+ data?: unknown;
30
+ }>(response: T, options?: GraphQLTerseOptions): GraphQLTerseResponse<T['data']> | T;
31
+ /**
32
+ * Creates a format function for express-graphql
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * import { graphqlHTTP } from 'express-graphql';
37
+ * import { createTerseFormatFn } from 'tersejson/graphql';
38
+ *
39
+ * app.use('/graphql', graphqlHTTP({
40
+ * schema: mySchema,
41
+ * formatResult: createTerseFormatFn({ debug: true }),
42
+ * }));
43
+ * ```
44
+ */
45
+ declare function createTerseFormatFn(options?: GraphQLTerseOptions): (result: {
46
+ data?: unknown;
47
+ errors?: unknown[];
48
+ extensions?: Record<string, unknown>;
49
+ }, _context?: unknown, _info?: unknown) => GraphQLTerseResponse<unknown> | {
50
+ data?: unknown;
51
+ errors?: unknown[];
52
+ extensions?: Record<string, unknown>;
53
+ };
54
+ /**
55
+ * Express middleware wrapper for express-graphql
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * import { graphqlHTTP } from 'express-graphql';
60
+ * import { terseGraphQL } from 'tersejson/graphql';
61
+ *
62
+ * app.use('/graphql', terseGraphQL(
63
+ * graphqlHTTP({
64
+ * schema: mySchema,
65
+ * graphiql: true,
66
+ * }),
67
+ * { debug: true }
68
+ * ));
69
+ * ```
70
+ */
71
+ declare function terseGraphQL(graphqlMiddleware: RequestHandler, options?: GraphQLTerseOptions): RequestHandler;
72
+
73
+ declare const graphql_compressGraphQLResponse: typeof compressGraphQLResponse;
74
+ declare const graphql_createTerseFormatFn: typeof createTerseFormatFn;
75
+ declare const graphql_findCompressibleArrays: typeof findCompressibleArrays;
76
+ declare const graphql_terseGraphQL: typeof terseGraphQL;
77
+ declare namespace graphql {
78
+ export { graphql_compressGraphQLResponse as compressGraphQLResponse, graphql_createTerseFormatFn as createTerseFormatFn, terseGraphQL as default, graphql_findCompressibleArrays as findCompressibleArrays, graphql_terseGraphQL as terseGraphQL };
79
+ }
80
+
81
+ export { createTerseFormatFn as a, compressGraphQLResponse as c, findCompressibleArrays as f, graphql as g, terseGraphQL as t };