s3db.js 11.3.2 → 12.0.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 (82) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36664 -15480
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +57 -0
  5. package/dist/s3db.es.js +36661 -15531
  6. package/dist/s3db.es.js.map +1 -1
  7. package/mcp/entrypoint.js +58 -0
  8. package/mcp/tools/documentation.js +434 -0
  9. package/mcp/tools/index.js +4 -0
  10. package/package.json +27 -6
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +41 -46
  13. package/src/concerns/base62.js +85 -0
  14. package/src/concerns/dictionary-encoding.js +294 -0
  15. package/src/concerns/geo-encoding.js +256 -0
  16. package/src/concerns/high-performance-inserter.js +34 -30
  17. package/src/concerns/ip.js +325 -0
  18. package/src/concerns/metadata-encoding.js +345 -66
  19. package/src/concerns/money.js +193 -0
  20. package/src/concerns/partition-queue.js +7 -4
  21. package/src/concerns/plugin-storage.js +39 -19
  22. package/src/database.class.js +76 -74
  23. package/src/errors.js +0 -4
  24. package/src/plugins/api/auth/api-key-auth.js +88 -0
  25. package/src/plugins/api/auth/basic-auth.js +154 -0
  26. package/src/plugins/api/auth/index.js +112 -0
  27. package/src/plugins/api/auth/jwt-auth.js +169 -0
  28. package/src/plugins/api/index.js +539 -0
  29. package/src/plugins/api/middlewares/index.js +15 -0
  30. package/src/plugins/api/middlewares/validator.js +185 -0
  31. package/src/plugins/api/routes/auth-routes.js +241 -0
  32. package/src/plugins/api/routes/resource-routes.js +304 -0
  33. package/src/plugins/api/server.js +350 -0
  34. package/src/plugins/api/utils/error-handler.js +147 -0
  35. package/src/plugins/api/utils/openapi-generator.js +1240 -0
  36. package/src/plugins/api/utils/response-formatter.js +218 -0
  37. package/src/plugins/backup/streaming-exporter.js +132 -0
  38. package/src/plugins/backup.plugin.js +103 -50
  39. package/src/plugins/cache/s3-cache.class.js +95 -47
  40. package/src/plugins/cache.plugin.js +107 -9
  41. package/src/plugins/concerns/plugin-dependencies.js +313 -0
  42. package/src/plugins/concerns/prometheus-formatter.js +255 -0
  43. package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
  44. package/src/plugins/consumers/sqs-consumer.js +4 -0
  45. package/src/plugins/costs.plugin.js +255 -39
  46. package/src/plugins/eventual-consistency/helpers.js +15 -1
  47. package/src/plugins/geo.plugin.js +873 -0
  48. package/src/plugins/importer/index.js +1020 -0
  49. package/src/plugins/index.js +11 -0
  50. package/src/plugins/metrics.plugin.js +163 -4
  51. package/src/plugins/queue-consumer.plugin.js +6 -27
  52. package/src/plugins/relation.errors.js +139 -0
  53. package/src/plugins/relation.plugin.js +1242 -0
  54. package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
  55. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  56. package/src/plugins/replicators/index.js +28 -3
  57. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  58. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  59. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  60. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  61. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  62. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  63. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  64. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  65. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  66. package/src/plugins/state-machine.plugin.js +122 -68
  67. package/src/plugins/tfstate/README.md +745 -0
  68. package/src/plugins/tfstate/base-driver.js +80 -0
  69. package/src/plugins/tfstate/errors.js +112 -0
  70. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  71. package/src/plugins/tfstate/index.js +2660 -0
  72. package/src/plugins/tfstate/s3-driver.js +192 -0
  73. package/src/plugins/ttl.plugin.js +536 -0
  74. package/src/resource.class.js +14 -10
  75. package/src/s3db.d.ts +57 -0
  76. package/src/schema.class.js +366 -32
  77. package/SECURITY.md +0 -76
  78. package/src/partition-drivers/base-partition-driver.js +0 -106
  79. package/src/partition-drivers/index.js +0 -66
  80. package/src/partition-drivers/memory-partition-driver.js +0 -289
  81. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  82. package/src/partition-drivers/sync-partition-driver.js +0 -38
@@ -145,21 +145,19 @@ export class Client extends EventEmitter {
145
145
  if (contentLength !== undefined) options.ContentLength = contentLength
146
146
  if (ifMatch !== undefined) options.IfMatch = ifMatch
147
147
 
148
- let response, error;
149
- try {
150
- response = await this.sendCommand(new PutObjectCommand(options));
151
- return response;
152
- } catch (err) {
153
- error = err;
148
+ const [ok, err, response] = await tryFn(() => this.sendCommand(new PutObjectCommand(options)));
149
+ this.emit('putObject', err || response, { key, metadata, contentType, body, contentEncoding, contentLength });
150
+
151
+ if (!ok) {
154
152
  throw mapAwsError(err, {
155
153
  bucket: this.config.bucket,
156
154
  key,
157
155
  commandName: 'PutObjectCommand',
158
156
  commandInput: options,
159
157
  });
160
- } finally {
161
- this.emit('putObject', error || response, { key, metadata, contentType, body, contentEncoding, contentLength });
162
158
  }
159
+
160
+ return response;
163
161
  }
164
162
 
165
163
  async getObject(key) {
@@ -168,32 +166,34 @@ export class Client extends EventEmitter {
168
166
  Bucket: this.config.bucket,
169
167
  Key: keyPrefix ? path.join(keyPrefix, key) : key,
170
168
  };
171
-
172
- let response, error;
173
- try {
174
- response = await this.sendCommand(new GetObjectCommand(options));
175
-
169
+
170
+ const [ok, err, response] = await tryFn(async () => {
171
+ const res = await this.sendCommand(new GetObjectCommand(options));
172
+
176
173
  // Smart decode metadata values
177
- if (response.Metadata) {
174
+ if (res.Metadata) {
178
175
  const decodedMetadata = {};
179
- for (const [key, value] of Object.entries(response.Metadata)) {
176
+ for (const [key, value] of Object.entries(res.Metadata)) {
180
177
  decodedMetadata[key] = metadataDecode(value);
181
178
  }
182
- response.Metadata = decodedMetadata;
179
+ res.Metadata = decodedMetadata;
183
180
  }
184
-
185
- return response;
186
- } catch (err) {
187
- error = err;
181
+
182
+ return res;
183
+ });
184
+
185
+ this.emit('getObject', err || response, { key });
186
+
187
+ if (!ok) {
188
188
  throw mapAwsError(err, {
189
189
  bucket: this.config.bucket,
190
190
  key,
191
191
  commandName: 'GetObjectCommand',
192
192
  commandInput: options,
193
193
  });
194
- } finally {
195
- this.emit('getObject', error || response, { key });
196
194
  }
195
+
196
+ return response;
197
197
  }
198
198
 
199
199
  async headObject(key) {
@@ -202,21 +202,20 @@ export class Client extends EventEmitter {
202
202
  Bucket: this.config.bucket,
203
203
  Key: keyPrefix ? path.join(keyPrefix, key) : key,
204
204
  };
205
- let response, error;
206
- try {
207
- response = await this.sendCommand(new HeadObjectCommand(options));
208
- return response;
209
- } catch (err) {
210
- error = err;
205
+
206
+ const [ok, err, response] = await tryFn(() => this.sendCommand(new HeadObjectCommand(options)));
207
+ this.emit('headObject', err || response, { key });
208
+
209
+ if (!ok) {
211
210
  throw mapAwsError(err, {
212
211
  bucket: this.config.bucket,
213
212
  key,
214
213
  commandName: 'HeadObjectCommand',
215
214
  commandInput: options,
216
215
  });
217
- } finally {
218
- this.emit('headObject', error || response, { key });
219
216
  }
217
+
218
+ return response;
220
219
  }
221
220
 
222
221
  async copyObject({ from, to }) {
@@ -226,21 +225,19 @@ export class Client extends EventEmitter {
226
225
  CopySource: path.join(this.config.bucket, this.config.keyPrefix ? path.join(this.config.keyPrefix, from) : from),
227
226
  };
228
227
 
229
- let response, error;
230
- try {
231
- response = await this.sendCommand(new CopyObjectCommand(options));
232
- return response;
233
- } catch (err) {
234
- error = err;
228
+ const [ok, err, response] = await tryFn(() => this.sendCommand(new CopyObjectCommand(options)));
229
+ this.emit('copyObject', err || response, { from, to });
230
+
231
+ if (!ok) {
235
232
  throw mapAwsError(err, {
236
233
  bucket: this.config.bucket,
237
234
  key: to,
238
235
  commandName: 'CopyObjectCommand',
239
236
  commandInput: options,
240
237
  });
241
- } finally {
242
- this.emit('copyObject', error || response, { from, to });
243
238
  }
239
+
240
+ return response;
244
241
  }
245
242
 
246
243
  async exists(key) {
@@ -258,21 +255,19 @@ export class Client extends EventEmitter {
258
255
  Key: keyPrefix ? path.join(keyPrefix, key) : key,
259
256
  };
260
257
 
261
- let response, error;
262
- try {
263
- response = await this.sendCommand(new DeleteObjectCommand(options));
264
- return response;
265
- } catch (err) {
266
- error = err;
258
+ const [ok, err, response] = await tryFn(() => this.sendCommand(new DeleteObjectCommand(options)));
259
+ this.emit('deleteObject', err || response, { key });
260
+
261
+ if (!ok) {
267
262
  throw mapAwsError(err, {
268
263
  bucket: this.config.bucket,
269
264
  key,
270
265
  commandName: 'DeleteObjectCommand',
271
266
  commandInput: options,
272
267
  });
273
- } finally {
274
- this.emit('deleteObject', error || response, { key });
275
268
  }
269
+
270
+ return response;
276
271
  }
277
272
 
278
273
  async deleteObjects(keys) {
@@ -129,3 +129,88 @@ export const decodeFixedPoint = (s, precision = 6) => {
129
129
  const scaled = negative ? -r : r;
130
130
  return scaled / scale;
131
131
  };
132
+
133
+ /**
134
+ * Batch encoding for arrays of fixed-point numbers (optimized for embeddings)
135
+ *
136
+ * Achieves ~17% additional compression vs individual encodeFixedPoint by using
137
+ * a single prefix for the entire array instead of one prefix per value.
138
+ *
139
+ * For 1536-dim embedding: ~1533 bytes saved (17.4%)
140
+ * For 3072-dim embedding: ~3069 bytes saved (17.5%)
141
+ *
142
+ * @param {number[]} values - Array of numbers to encode
143
+ * @param {number} precision - Decimal places to preserve (default: 6)
144
+ * @returns {string} Batch-encoded string with format: ^[val1,val2,val3,...]
145
+ *
146
+ * Examples:
147
+ * [0.123, -0.456, 0.789] → "^[w7f,-3sdz,oHb]"
148
+ * [] → "^[]"
149
+ */
150
+ export const encodeFixedPointBatch = (values, precision = 6) => {
151
+ if (!Array.isArray(values)) return '';
152
+ if (values.length === 0) return '^[]';
153
+
154
+ const scale = Math.pow(10, precision);
155
+
156
+ const encoded = values.map(n => {
157
+ if (typeof n !== 'number' || isNaN(n) || !isFinite(n)) return '';
158
+
159
+ const scaled = Math.round(n * scale);
160
+ if (scaled === 0) return '0';
161
+
162
+ const negative = scaled < 0;
163
+ let num = Math.abs(scaled);
164
+ let s = '';
165
+
166
+ while (num > 0) {
167
+ s = alphabet[num % base] + s;
168
+ num = Math.floor(num / base);
169
+ }
170
+
171
+ return (negative ? '-' : '') + s;
172
+ });
173
+
174
+ // Single prefix for entire batch, comma-separated
175
+ return '^[' + encoded.join(',') + ']';
176
+ };
177
+
178
+ /**
179
+ * Decodes batch-encoded fixed-point arrays
180
+ *
181
+ * @param {string} s - Batch-encoded string (format: ^[val1,val2,...])
182
+ * @param {number} precision - Decimal places used in encoding (default: 6)
183
+ * @returns {number[]} Decoded array of numbers
184
+ */
185
+ export const decodeFixedPointBatch = (s, precision = 6) => {
186
+ if (typeof s !== 'string') return [];
187
+ if (!s.startsWith('^[')) return [];
188
+
189
+ s = s.slice(2, -1); // Remove ^[ and ]
190
+
191
+ if (s === '') return [];
192
+
193
+ const parts = s.split(',');
194
+ const scale = Math.pow(10, precision);
195
+
196
+ return parts.map(part => {
197
+ if (part === '0') return 0;
198
+ if (part === '') return NaN;
199
+
200
+ let negative = false;
201
+ if (part[0] === '-') {
202
+ negative = true;
203
+ part = part.slice(1);
204
+ }
205
+
206
+ let r = 0;
207
+ for (let i = 0; i < part.length; i++) {
208
+ const idx = charToValue[part[i]];
209
+ if (idx === undefined) return NaN;
210
+ r = r * base + idx;
211
+ }
212
+
213
+ const scaled = negative ? -r : r;
214
+ return scaled / scale;
215
+ });
216
+ };
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Dictionary Encoding for Common Metadata Values
3
+ *
4
+ * Provides massive compression for frequently-used long strings:
5
+ * - Content-Types: application/json (16B) → j (1B) = -93.75%
6
+ * - URL Prefixes: https://api.example.com/ (24B) → @a (2B) = -91.7%
7
+ * - Status Messages: processing (10B) → p (1B) = -90%
8
+ *
9
+ * Encoding format: 'd:{code}' where {code} is 1-2 characters
10
+ * Example: 'd:j' = application/json (3B vs 16B = -81% with prefix!)
11
+ */
12
+
13
+ /**
14
+ * Content-Type Dictionary
15
+ * Most common MIME types with massive savings potential
16
+ * Format: 'original_value' → 'single_char_code'
17
+ */
18
+ const CONTENT_TYPE_DICT = {
19
+ // JSON/XML (most common, highest savings)
20
+ 'application/json': 'j', // 16B → 1B = -93.75%
21
+ 'application/xml': 'X', // 15B → 1B = -93.3% (changed from 'x' to avoid conflict)
22
+ 'application/ld+json': 'J', // 20B → 1B = -95%
23
+
24
+ // Text types
25
+ 'text/html': 'H', // 9B → 1B = -88.9% (changed from 'h' to avoid conflict)
26
+ 'text/plain': 'T', // 10B → 1B = -90% (changed from 'p' to avoid conflict)
27
+ 'text/css': 'C', // 8B → 1B = -87.5% (changed from 'c' to avoid conflict)
28
+ 'text/javascript': 'V', // 15B → 1B = -93.3% (changed from 's' to avoid conflict)
29
+ 'text/csv': 'v', // 8B → 1B = -87.5%
30
+
31
+ // Images
32
+ 'image/png': 'P', // 9B → 1B = -88.9%
33
+ 'image/jpeg': 'I', // 10B → 1B = -90%
34
+ 'image/gif': 'G', // 9B → 1B = -88.9%
35
+ 'image/svg+xml': 'S', // 13B → 1B = -92.3%
36
+ 'image/webp': 'W', // 10B → 1B = -90%
37
+
38
+ // Application types
39
+ 'application/pdf': 'Q', // 15B → 1B = -93.3% (changed from 'd' to avoid conflict)
40
+ 'application/zip': 'z', // 15B → 1B = -93.3%
41
+ 'application/octet-stream': 'o', // 24B → 1B = -95.8%
42
+ 'application/x-www-form-urlencoded': 'u', // 33B → 1B = -97%
43
+ 'multipart/form-data': 'F', // 19B → 1B = -94.7% (changed from 'f' to avoid conflict)
44
+
45
+ // Font types
46
+ 'font/woff': 'w', // 9B → 1B = -88.9%
47
+ 'font/woff2': 'f' // 10B → 1B = -90% (changed from 'F')
48
+ };
49
+
50
+ /**
51
+ * URL Prefix Dictionary
52
+ * Common URL prefixes that appear in paths, webhooks, API endpoints
53
+ * Format: 'prefix' → '@{code}'
54
+ */
55
+ const URL_PREFIX_DICT = {
56
+ // API endpoints (very common)
57
+ '/api/v1/': '@1', // 8B → 2B = -75%
58
+ '/api/v2/': '@2', // 8B → 2B = -75%
59
+ '/api/v3/': '@3', // 8B → 2B = -75%
60
+ '/api/': '@a', // 5B → 2B = -60%
61
+
62
+ // HTTPS prefixes
63
+ 'https://api.example.com/': '@A', // 24B → 2B = -91.7%
64
+ 'https://api.': '@H', // 11B → 2B = -81.8%
65
+ 'https://www.': '@W', // 12B → 2B = -83.3%
66
+ 'https://': '@h', // 8B → 2B = -75%
67
+ 'http://': '@t', // 7B → 2B = -71.4%
68
+
69
+ // AWS/S3 (common in s3db.js context)
70
+ 'https://s3.amazonaws.com/': '@s', // 26B → 2B = -92.3%
71
+ 'https://s3-': '@S', // 10B → 2B = -80%
72
+
73
+ // Localhost (development)
74
+ 'http://localhost:': '@L', // 17B → 2B = -88.2%
75
+ 'http://localhost': '@l', // 16B → 2B = -87.5%
76
+
77
+ // Common paths
78
+ '/v1/': '@v', // 4B → 2B = -50%
79
+ '/users/': '@u', // 7B → 2B = -71.4%
80
+ '/products/': '@p' // 10B → 2B = -80%
81
+ };
82
+
83
+ /**
84
+ * Status Message Dictionary
85
+ * Common status/state strings
86
+ * Format: 'status' → 'code'
87
+ */
88
+ const STATUS_MESSAGE_DICT = {
89
+ // Processing states (very common, good savings)
90
+ 'processing': 'p', // 10B → 1B = -90%
91
+ 'completed': 'c', // 9B → 1B = -88.9%
92
+ 'succeeded': 's', // 9B → 1B = -88.9%
93
+ 'failed': 'f', // 6B → 1B = -83.3%
94
+ 'cancelled': 'x', // 9B → 1B = -88.9%
95
+ 'timeout': 't', // 7B → 1B = -85.7%
96
+ 'retrying': 'r', // 8B → 1B = -87.5%
97
+
98
+ // Payment states
99
+ 'authorized': 'a', // 10B → 1B = -90%
100
+ 'captured': 'K', // 8B → 1B = -87.5% (changed from C to avoid conflict)
101
+ 'refunded': 'R', // 8B → 1B = -87.5%
102
+ 'declined': 'd', // 8B → 1B = -87.5%
103
+
104
+ // Order/delivery states
105
+ 'shipped': 'h', // 7B → 1B = -85.7% (changed from S to avoid conflict)
106
+ 'delivered': 'D', // 9B → 1B = -88.9%
107
+ 'returned': 'e', // 8B → 1B = -87.5% (changed from T to avoid conflict)
108
+ 'in_transit': 'i', // 10B → 1B = -90%
109
+
110
+ // Generic states
111
+ 'initialized': 'n', // 11B → 1B = -90.9% (changed from I to avoid conflict)
112
+ 'terminated': 'm' // 10B → 1B = -90% (changed from X to avoid conflict)
113
+ };
114
+
115
+ /**
116
+ * Reverse dictionaries for decoding
117
+ * Built automatically from forward dictionaries
118
+ */
119
+ const CONTENT_TYPE_REVERSE = Object.fromEntries(
120
+ Object.entries(CONTENT_TYPE_DICT).map(([k, v]) => [v, k])
121
+ );
122
+
123
+ const URL_PREFIX_REVERSE = Object.fromEntries(
124
+ Object.entries(URL_PREFIX_DICT).map(([k, v]) => [v, k])
125
+ );
126
+
127
+ const STATUS_MESSAGE_REVERSE = Object.fromEntries(
128
+ Object.entries(STATUS_MESSAGE_DICT).map(([k, v]) => [v, k])
129
+ );
130
+
131
+ /**
132
+ * Combined dictionaries for easier lookup
133
+ * All dictionaries merged into one for encoding
134
+ */
135
+ const COMBINED_DICT = {
136
+ ...CONTENT_TYPE_DICT,
137
+ ...STATUS_MESSAGE_DICT
138
+ // URL prefixes handled separately (prefix matching)
139
+ };
140
+
141
+ const COMBINED_REVERSE = {
142
+ ...CONTENT_TYPE_REVERSE,
143
+ ...STATUS_MESSAGE_REVERSE
144
+ // URL prefixes handled separately
145
+ };
146
+
147
+ /**
148
+ * Encode a value using dictionary if available
149
+ * @param {string} value - Value to encode
150
+ * @returns {Object|null} Encoded result or null if not in dictionary
151
+ */
152
+ export function dictionaryEncode(value) {
153
+ if (typeof value !== 'string' || !value) {
154
+ return null;
155
+ }
156
+
157
+ // Check exact match first (content-types, status messages)
158
+ if (COMBINED_DICT[value]) {
159
+ return {
160
+ encoded: 'd:' + COMBINED_DICT[value],
161
+ encoding: 'dictionary',
162
+ originalLength: value.length,
163
+ encodedLength: 2 + COMBINED_DICT[value].length,
164
+ dictionaryType: 'exact',
165
+ savings: value.length - (2 + COMBINED_DICT[value].length)
166
+ };
167
+ }
168
+
169
+ // Check URL prefix matching (for paths, URLs)
170
+ // Sort prefixes by length (longest first) to prioritize specific matches
171
+ const sortedPrefixes = Object.entries(URL_PREFIX_DICT)
172
+ .sort(([a], [b]) => b.length - a.length);
173
+
174
+ for (const [prefix, code] of sortedPrefixes) {
175
+ if (value.startsWith(prefix)) {
176
+ const remainder = value.substring(prefix.length);
177
+ const encoded = 'd:' + code + remainder;
178
+
179
+ return {
180
+ encoded,
181
+ encoding: 'dictionary',
182
+ originalLength: value.length,
183
+ encodedLength: encoded.length,
184
+ dictionaryType: 'prefix',
185
+ prefix,
186
+ remainder,
187
+ savings: value.length - encoded.length
188
+ };
189
+ }
190
+ }
191
+
192
+ // Not in dictionary
193
+ return null;
194
+ }
195
+
196
+ /**
197
+ * Decode a dictionary-encoded value
198
+ * @param {string} encoded - Encoded value (starts with 'd:')
199
+ * @returns {string|null} Decoded value or null if not dictionary-encoded
200
+ */
201
+ export function dictionaryDecode(encoded) {
202
+ if (typeof encoded !== 'string' || !encoded.startsWith('d:')) {
203
+ return null;
204
+ }
205
+
206
+ const payload = encoded.substring(2); // Remove 'd:' prefix
207
+
208
+ if (payload.length === 0) {
209
+ return null;
210
+ }
211
+
212
+ // Try exact match first (single character codes)
213
+ if (payload.length === 1) {
214
+ const decoded = COMBINED_REVERSE[payload];
215
+ if (decoded) {
216
+ return decoded;
217
+ }
218
+ }
219
+
220
+ // Try URL prefix match (starts with @)
221
+ if (payload.startsWith('@')) {
222
+ // Extract prefix code (1-2 chars after @)
223
+ const prefixCode = payload.substring(0, 2); // '@' + 1 char
224
+ const remainder = payload.substring(2);
225
+
226
+ const prefix = URL_PREFIX_REVERSE[prefixCode];
227
+ if (prefix) {
228
+ return prefix + remainder;
229
+ }
230
+ }
231
+
232
+ // Unknown dictionary code - return null (fall back to original)
233
+ return null;
234
+ }
235
+
236
+ /**
237
+ * Calculate compression ratio for a value
238
+ * @param {string} value - Original value
239
+ * @returns {Object} Compression statistics
240
+ */
241
+ export function calculateDictionaryCompression(value) {
242
+ const result = dictionaryEncode(value);
243
+
244
+ if (!result) {
245
+ return {
246
+ compressible: false,
247
+ original: value.length,
248
+ encoded: value.length,
249
+ savings: 0,
250
+ ratio: 1.0
251
+ };
252
+ }
253
+
254
+ return {
255
+ compressible: true,
256
+ original: result.originalLength,
257
+ encoded: result.encodedLength,
258
+ savings: result.savings,
259
+ ratio: result.encodedLength / result.originalLength,
260
+ savingsPercent: ((result.savings / result.originalLength) * 100).toFixed(1) + '%'
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Get dictionary statistics (for debugging/monitoring)
266
+ * @returns {Object} Statistics about dictionaries
267
+ */
268
+ export function getDictionaryStats() {
269
+ return {
270
+ contentTypes: Object.keys(CONTENT_TYPE_DICT).length,
271
+ urlPrefixes: Object.keys(URL_PREFIX_DICT).length,
272
+ statusMessages: Object.keys(STATUS_MESSAGE_DICT).length,
273
+ total: Object.keys(COMBINED_DICT).length + Object.keys(URL_PREFIX_DICT).length,
274
+ avgSavingsContentType:
275
+ Object.keys(CONTENT_TYPE_DICT).reduce((sum, key) =>
276
+ sum + (key.length - (2 + CONTENT_TYPE_DICT[key].length)), 0
277
+ ) / Object.keys(CONTENT_TYPE_DICT).length,
278
+ avgSavingsStatus:
279
+ Object.keys(STATUS_MESSAGE_DICT).reduce((sum, key) =>
280
+ sum + (key.length - (2 + STATUS_MESSAGE_DICT[key].length)), 0
281
+ ) / Object.keys(STATUS_MESSAGE_DICT).length
282
+ };
283
+ }
284
+
285
+ export default {
286
+ dictionaryEncode,
287
+ dictionaryDecode,
288
+ calculateDictionaryCompression,
289
+ getDictionaryStats,
290
+ // Export dictionaries for testing
291
+ CONTENT_TYPE_DICT,
292
+ URL_PREFIX_DICT,
293
+ STATUS_MESSAGE_DICT
294
+ };