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.
- package/README.md +102 -8
- package/dist/s3db.cjs.js +36664 -15480
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +57 -0
- package/dist/s3db.es.js +36661 -15531
- package/dist/s3db.es.js.map +1 -1
- package/mcp/entrypoint.js +58 -0
- package/mcp/tools/documentation.js +434 -0
- package/mcp/tools/index.js +4 -0
- package/package.json +27 -6
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +41 -46
- package/src/concerns/base62.js +85 -0
- package/src/concerns/dictionary-encoding.js +294 -0
- package/src/concerns/geo-encoding.js +256 -0
- package/src/concerns/high-performance-inserter.js +34 -30
- package/src/concerns/ip.js +325 -0
- package/src/concerns/metadata-encoding.js +345 -66
- package/src/concerns/money.js +193 -0
- package/src/concerns/partition-queue.js +7 -4
- package/src/concerns/plugin-storage.js +39 -19
- package/src/database.class.js +76 -74
- package/src/errors.js +0 -4
- package/src/plugins/api/auth/api-key-auth.js +88 -0
- package/src/plugins/api/auth/basic-auth.js +154 -0
- package/src/plugins/api/auth/index.js +112 -0
- package/src/plugins/api/auth/jwt-auth.js +169 -0
- package/src/plugins/api/index.js +539 -0
- package/src/plugins/api/middlewares/index.js +15 -0
- package/src/plugins/api/middlewares/validator.js +185 -0
- package/src/plugins/api/routes/auth-routes.js +241 -0
- package/src/plugins/api/routes/resource-routes.js +304 -0
- package/src/plugins/api/server.js +350 -0
- package/src/plugins/api/utils/error-handler.js +147 -0
- package/src/plugins/api/utils/openapi-generator.js +1240 -0
- package/src/plugins/api/utils/response-formatter.js +218 -0
- package/src/plugins/backup/streaming-exporter.js +132 -0
- package/src/plugins/backup.plugin.js +103 -50
- package/src/plugins/cache/s3-cache.class.js +95 -47
- package/src/plugins/cache.plugin.js +107 -9
- package/src/plugins/concerns/plugin-dependencies.js +313 -0
- package/src/plugins/concerns/prometheus-formatter.js +255 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
- package/src/plugins/consumers/sqs-consumer.js +4 -0
- package/src/plugins/costs.plugin.js +255 -39
- package/src/plugins/eventual-consistency/helpers.js +15 -1
- package/src/plugins/geo.plugin.js +873 -0
- package/src/plugins/importer/index.js +1020 -0
- package/src/plugins/index.js +11 -0
- package/src/plugins/metrics.plugin.js +163 -4
- package/src/plugins/queue-consumer.plugin.js +6 -27
- package/src/plugins/relation.errors.js +139 -0
- package/src/plugins/relation.plugin.js +1242 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
- package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
- package/src/plugins/replicators/index.js +28 -3
- package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
- package/src/plugins/replicators/mysql-replicator.class.js +558 -0
- package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
- package/src/plugins/replicators/postgres-replicator.class.js +182 -7
- package/src/plugins/replicators/s3db-replicator.class.js +1 -12
- package/src/plugins/replicators/schema-sync.helper.js +601 -0
- package/src/plugins/replicators/sqs-replicator.class.js +11 -9
- package/src/plugins/replicators/turso-replicator.class.js +416 -0
- package/src/plugins/replicators/webhook-replicator.class.js +612 -0
- package/src/plugins/state-machine.plugin.js +122 -68
- package/src/plugins/tfstate/README.md +745 -0
- package/src/plugins/tfstate/base-driver.js +80 -0
- package/src/plugins/tfstate/errors.js +112 -0
- package/src/plugins/tfstate/filesystem-driver.js +129 -0
- package/src/plugins/tfstate/index.js +2660 -0
- package/src/plugins/tfstate/s3-driver.js +192 -0
- package/src/plugins/ttl.plugin.js +536 -0
- package/src/resource.class.js +14 -10
- package/src/s3db.d.ts +57 -0
- package/src/schema.class.js +366 -32
- package/SECURITY.md +0 -76
- package/src/partition-drivers/base-partition-driver.js +0 -106
- package/src/partition-drivers/index.js +0 -66
- package/src/partition-drivers/memory-partition-driver.js +0 -289
- package/src/partition-drivers/sqs-partition-driver.js +0 -337
- package/src/partition-drivers/sync-partition-driver.js +0 -38
package/src/client.class.js
CHANGED
|
@@ -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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 (
|
|
174
|
+
if (res.Metadata) {
|
|
178
175
|
const decodedMetadata = {};
|
|
179
|
-
for (const [key, value] of Object.entries(
|
|
176
|
+
for (const [key, value] of Object.entries(res.Metadata)) {
|
|
180
177
|
decodedMetadata[key] = metadataDecode(value);
|
|
181
178
|
}
|
|
182
|
-
|
|
179
|
+
res.Metadata = decodedMetadata;
|
|
183
180
|
}
|
|
184
|
-
|
|
185
|
-
return
|
|
186
|
-
}
|
|
187
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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) {
|
package/src/concerns/base62.js
CHANGED
|
@@ -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
|
+
};
|