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.
- package/README.md +102 -8
- package/dist/s3db.cjs.js +36945 -15510
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +66 -1
- package/dist/s3db.es.js +36914 -15534
- 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 +35 -15
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +79 -49
- 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 +97 -47
- 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 +544 -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 +354 -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/replicator.plugin.js +2 -1
- 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 +315 -36
- package/src/s3db.d.ts +66 -1
- 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,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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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:
|
|
226
|
-
CopySource: path.join(this.config.bucket,
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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) {
|
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
|
+
};
|