s3db.js 11.3.2 → 12.0.1

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 (83) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36945 -15510
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +66 -1
  5. package/dist/s3db.es.js +36914 -15534
  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 +35 -15
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +79 -49
  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 +97 -47
  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 +544 -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 +354 -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/replicator.plugin.js +2 -1
  55. package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
  56. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  57. package/src/plugins/replicators/index.js +28 -3
  58. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  59. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  60. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  61. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  62. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  63. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  64. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  65. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  66. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  67. package/src/plugins/state-machine.plugin.js +122 -68
  68. package/src/plugins/tfstate/README.md +745 -0
  69. package/src/plugins/tfstate/base-driver.js +80 -0
  70. package/src/plugins/tfstate/errors.js +112 -0
  71. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  72. package/src/plugins/tfstate/index.js +2660 -0
  73. package/src/plugins/tfstate/s3-driver.js +192 -0
  74. package/src/plugins/ttl.plugin.js +536 -0
  75. package/src/resource.class.js +315 -36
  76. package/src/s3db.d.ts +66 -1
  77. package/src/schema.class.js +366 -32
  78. package/SECURITY.md +0 -76
  79. package/src/partition-drivers/base-partition-driver.js +0 -106
  80. package/src/partition-drivers/index.js +0 -66
  81. package/src/partition-drivers/memory-partition-driver.js +0 -289
  82. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  83. 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,45 +202,77 @@ 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(async () => {
207
+ const res = await this.sendCommand(new HeadObjectCommand(options));
208
+
209
+ // Smart decode metadata values (same as getObject)
210
+ if (res.Metadata) {
211
+ const decodedMetadata = {};
212
+ for (const [key, value] of Object.entries(res.Metadata)) {
213
+ decodedMetadata[key] = metadataDecode(value);
214
+ }
215
+ res.Metadata = decodedMetadata;
216
+ }
217
+
218
+ return res;
219
+ });
220
+
221
+ this.emit('headObject', err || response, { key });
222
+
223
+ if (!ok) {
211
224
  throw mapAwsError(err, {
212
225
  bucket: this.config.bucket,
213
226
  key,
214
227
  commandName: 'HeadObjectCommand',
215
228
  commandInput: options,
216
229
  });
217
- } finally {
218
- this.emit('headObject', error || response, { key });
219
230
  }
231
+
232
+ return response;
220
233
  }
221
234
 
222
- async copyObject({ from, to }) {
235
+ async copyObject({ from, to, metadata, metadataDirective, contentType }) {
236
+ const keyPrefix = typeof this.config.keyPrefix === 'string' ? this.config.keyPrefix : '';
223
237
  const options = {
224
238
  Bucket: this.config.bucket,
225
- Key: this.config.keyPrefix ? path.join(this.config.keyPrefix, to) : to,
226
- CopySource: path.join(this.config.bucket, this.config.keyPrefix ? path.join(this.config.keyPrefix, from) : from),
239
+ Key: keyPrefix ? path.join(keyPrefix, to) : to,
240
+ CopySource: path.join(this.config.bucket, keyPrefix ? path.join(keyPrefix, from) : from),
227
241
  };
228
242
 
229
- let response, error;
230
- try {
231
- response = await this.sendCommand(new CopyObjectCommand(options));
232
- return response;
233
- } catch (err) {
234
- error = err;
243
+ // Add metadata directive if specified
244
+ if (metadataDirective) {
245
+ options.MetadataDirective = metadataDirective; // 'COPY' or 'REPLACE'
246
+ }
247
+
248
+ // Add metadata if specified (and encode values)
249
+ if (metadata && typeof metadata === 'object') {
250
+ const encodedMetadata = {};
251
+ for (const [key, value] of Object.entries(metadata)) {
252
+ const { encoded } = metadataEncode(value);
253
+ encodedMetadata[key] = encoded;
254
+ }
255
+ options.Metadata = encodedMetadata;
256
+ }
257
+
258
+ // Add content type if specified
259
+ if (contentType) {
260
+ options.ContentType = contentType;
261
+ }
262
+
263
+ const [ok, err, response] = await tryFn(() => this.sendCommand(new CopyObjectCommand(options)));
264
+ this.emit('copyObject', err || response, { from, to, metadataDirective });
265
+
266
+ if (!ok) {
235
267
  throw mapAwsError(err, {
236
268
  bucket: this.config.bucket,
237
269
  key: to,
238
270
  commandName: 'CopyObjectCommand',
239
271
  commandInput: options,
240
272
  });
241
- } finally {
242
- this.emit('copyObject', error || response, { from, to });
243
273
  }
274
+
275
+ return response;
244
276
  }
245
277
 
246
278
  async exists(key) {
@@ -258,21 +290,19 @@ export class Client extends EventEmitter {
258
290
  Key: keyPrefix ? path.join(keyPrefix, key) : key,
259
291
  };
260
292
 
261
- let response, error;
262
- try {
263
- response = await this.sendCommand(new DeleteObjectCommand(options));
264
- return response;
265
- } catch (err) {
266
- error = err;
293
+ const [ok, err, response] = await tryFn(() => this.sendCommand(new DeleteObjectCommand(options)));
294
+ this.emit('deleteObject', err || response, { key });
295
+
296
+ if (!ok) {
267
297
  throw mapAwsError(err, {
268
298
  bucket: this.config.bucket,
269
299
  key,
270
300
  commandName: 'DeleteObjectCommand',
271
301
  commandInput: options,
272
302
  });
273
- } finally {
274
- this.emit('deleteObject', error || response, { key });
275
303
  }
304
+
305
+ return response;
276
306
  }
277
307
 
278
308
  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
+ };