densing 0.1.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/LICENSE +21 -0
- package/README.md +438 -0
- package/dist/api.d.ts +69 -0
- package/dist/densing.d.ts +39 -0
- package/dist/helpers.d.ts +39 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +870 -0
- package/dist/schema/builder.d.ts +13 -0
- package/dist/schema/default-data.d.ts +7 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/recursive-builder-helper.d.ts +6 -0
- package/dist/schema/type-generator.d.ts +13 -0
- package/dist/schema/validation.d.ts +10 -0
- package/dist/schema-type.d.ts +63 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonas Ward
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
**Densing** is a TypeScript library for ultra-compact data serialization. It uses bit-level packing to encode structured data into the smallest possible representation, then converts it to character based encodings like urlSafeBase64, or QRBase45-safe strings.
|
|
2
|
+
|
|
3
|
+
Perfect for embedding complex data in URLs, QR codes, or any scenario where every character counts!
|
|
4
|
+
|
|
5
|
+
## 🎯 Why Densing?
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// Traditional JSON: 87 bytes
|
|
9
|
+
const json = '{"deviceId":42,"enabled":true,"temperature":23.5,"mode":"performance"}';
|
|
10
|
+
|
|
11
|
+
// Densing: base64 -> 4 characters (4 bytes) - **94% smaller!**
|
|
12
|
+
const densed = 'Cqnu';
|
|
13
|
+
|
|
14
|
+
// note with:
|
|
15
|
+
// deviceId: value form 0 to 1000 -> 1001 states -> 10 bits
|
|
16
|
+
// enabled: boolean value -> 2 states -> 1 bit
|
|
17
|
+
// temperature: value from -40.0 to 125.0 (one decimal precision) -> 1650 states -> 11 bits
|
|
18
|
+
// mode: 'eco' | 'normal' | 'performance' -> 3 states -> 2 bits
|
|
19
|
+
// --> 24 bits (3 bytes)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Key Features
|
|
23
|
+
|
|
24
|
+
- 🔬 **Bit-level precision** - Only uses the exact bits needed for your data
|
|
25
|
+
- 📦 **Type-safe schemas** - Define your data structure with full TypeScript support
|
|
26
|
+
- 🔐 **URL-safe encoding** - Base64url by default, custom bases supported
|
|
27
|
+
- ✅ **Built-in validation method** - Ensure data integrity before encoding
|
|
28
|
+
- 📊 **Size analysis** - See exactly how many bits each field uses
|
|
29
|
+
- 🔄 **Lossless compression** - Perfect round-trip encoding/decoding
|
|
30
|
+
- 🚀 **Zero dependencies** - Lightweight and fast, only uses base javascript types
|
|
31
|
+
|
|
32
|
+
## 📦 Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install densing
|
|
36
|
+
# or
|
|
37
|
+
bun add densing
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 🚀 Quick Start
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { schema, int, bool, fixed, enumeration, densing, undensing } from 'densing';
|
|
44
|
+
|
|
45
|
+
// 1. Define your schema
|
|
46
|
+
const DeviceSchema = schema(
|
|
47
|
+
int('deviceId', 0, 1000), // 10 bits (0-1000)
|
|
48
|
+
bool('enabled'), // 1 bit
|
|
49
|
+
fixed('temperature', -40, 125, 0.1), // 11 bits (-40 to 125, precision 0.1)
|
|
50
|
+
enumeration('mode', ['eco', 'normal', 'performance']) // 2 bits (3 options)
|
|
51
|
+
);
|
|
52
|
+
// total of 24 bits
|
|
53
|
+
|
|
54
|
+
// 2. Encode your data
|
|
55
|
+
const data = {
|
|
56
|
+
deviceId: 42,
|
|
57
|
+
enabled: true,
|
|
58
|
+
temperature: 23.5,
|
|
59
|
+
mode: 'performance'
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const encoded = densing(DeviceSchema, data);
|
|
63
|
+
console.log(encoded); // "Cqnu" (24 bits, 4 base64 chars vs JSON 70 chars, -94%)
|
|
64
|
+
|
|
65
|
+
// 3. Decode it back
|
|
66
|
+
const decoded = undensing(DeviceSchema, encoded);
|
|
67
|
+
console.log(decoded); // { deviceId: 42, enabled: true, temperature: 23.5, mode: 'performance' }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 📚 Core Concepts
|
|
71
|
+
|
|
72
|
+
### Schema Definition
|
|
73
|
+
|
|
74
|
+
Schemas define the structure and constraints of your data. Densing uses this to calculate the minimum bits needed.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { schema, int, fixed, bool, enumeration } from 'densing';
|
|
78
|
+
|
|
79
|
+
const MySchema = schema(
|
|
80
|
+
// Integers: specify min and max range
|
|
81
|
+
int('id', 0, 1000), // 10 bits for 1001 possible values
|
|
82
|
+
|
|
83
|
+
// Fixed-point numbers: specify range and precision
|
|
84
|
+
fixed('price', 0, 100, 0.01), // 14 bits for $0.00 to $100.00
|
|
85
|
+
|
|
86
|
+
// Booleans: just 1 bit
|
|
87
|
+
bool('active'),
|
|
88
|
+
|
|
89
|
+
// Enums: bits based on number of options
|
|
90
|
+
enumeration('status', ['pending', 'active', 'done']) // 2 bits for 3 options
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Field Types
|
|
95
|
+
|
|
96
|
+
| Type | Description | Bits Used | Example |
|
|
97
|
+
| ----------- | ------------------- | --------------------------------- | ----------------------------------------------------------- |
|
|
98
|
+
| `int` | Integer range | `log2(max - min + 1)` | `int('age', 0, 120)` → 7 bits |
|
|
99
|
+
| `fixed` | Fixed-point decimal | `log2((max-min) / precision + 1)` | `fixed('temp', 0, 50, 0.1)` → 9 bits |
|
|
100
|
+
| `bool` | Boolean | 1 bit | `bool('enabled')` |
|
|
101
|
+
| `enum` | Enumeration | `log2(options.length)` | `enumeration('color', ['R', 'G', 'B'])` → 2 bits |
|
|
102
|
+
| `optional` | Optional field | 1 + field bits | `optional('metadata', int('version', 0, 10))` |
|
|
103
|
+
| `array` | Array of fields | length bits + content | `array('items', 0, 10, int('value', 0, 100))` |
|
|
104
|
+
| `enumArray` | Packed enum array | length + packed content | `enumArray('tags', enum, 0, 5)` |
|
|
105
|
+
| `object` | Nested object | sum of field bits | `object('config', bool('debug'), int('port', 1024, 65535))` |
|
|
106
|
+
| `union` | Discriminated union | discriminator + variant | `union('action', discriminator, variants)` |
|
|
107
|
+
|
|
108
|
+
## 🎨 Examples
|
|
109
|
+
|
|
110
|
+
### Optional Fields
|
|
111
|
+
|
|
112
|
+
Optional fields add a single presence bit:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { schema, int, optional } from 'densing';
|
|
116
|
+
|
|
117
|
+
const UserSchema = schema(
|
|
118
|
+
int('id', 0, 10000), // 10001 states -> 14 bits
|
|
119
|
+
optional('age', int('ageValue', 0, 120)) // 2 + 121 states -> 1 bit presence + 7 bits if present
|
|
120
|
+
); // 14 + 1 (+ 7 bits) -> 15 or 22 bits
|
|
121
|
+
|
|
122
|
+
// With age
|
|
123
|
+
densing(UserSchema, { id: 100, age: 25 }); // "AZJk" (22 bits, 4 base64 chars vs JSON 19 chars, -79%)
|
|
124
|
+
|
|
125
|
+
// Without age
|
|
126
|
+
densing(UserSchema, { id: 100, age: null }); // "AZA" (15 bits, 3 base64 chars vs JSON 21 chars, -86%)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Nested Objects
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { schema, int, object, bool } from 'densing';
|
|
133
|
+
|
|
134
|
+
const ConfigSchema = schema(int('version', 1, 10), object('settings', bool('darkMode'), int('fontSize', 8, 24)));
|
|
135
|
+
|
|
136
|
+
const data = {
|
|
137
|
+
version: 2,
|
|
138
|
+
settings: {
|
|
139
|
+
darkMode: true,
|
|
140
|
+
fontSize: 14
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
densing(ConfigSchema, data); // "GY" (10 bits, 2 base64 chars vs JSON 56 chars, -96%)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Arrays
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { schema, array, int } from 'densing';
|
|
151
|
+
|
|
152
|
+
const ListSchema = schema(
|
|
153
|
+
array('scores', 0, 10, int('score', 0, 100)) // 0-10 scores, each 0-100 -> 4 bits + 0-10 x 7 bits
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// In readme-examples.test.ts, you can add:
|
|
157
|
+
const data1 = { scores: [95] }; // 4 + 7 bits -> 11 bits -> 2 characters vs 13 (-85%) => "G-"
|
|
158
|
+
const data2 = { scores: [95, 87, 92, 88] }; // 4 + 4 * 7 bits -> 32 bits -> 6 characters vs 22 (-73%) => "S_XuWA"
|
|
159
|
+
const data3 = { scores: [95, 87, 92, 88, 10, 12, 13, 15, 16, 99] }; // 4 + 10 * 7 bits -> 74 bits -> 13 characters vs 40 (-68%) => "q_XuWBQwaPIYw"
|
|
160
|
+
|
|
161
|
+
densing(ListSchema, data1); // "G-" (11 bits, 2 base64 chars vs JSON 15 chars, -87%)
|
|
162
|
+
densing(ListSchema, data2); // "S_XuWA" (32 bits, 6 base64 chars vs JSON 24 chars, -75%)
|
|
163
|
+
densing(ListSchema, data3); // "q_XuWBQwaPIYw" (74 bits, 13 base64 chars vs JSON 42 chars, -69%)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Unions (Polymorphic Types)
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { schema, union, enumeration, int, bool } from 'densing';
|
|
170
|
+
|
|
171
|
+
const ActionSchema = schema(
|
|
172
|
+
union('action', enumeration('type', ['start', 'stop', 'pause']), {
|
|
173
|
+
start: [int('delay', 0, 60)],
|
|
174
|
+
stop: [bool('force')],
|
|
175
|
+
pause: [int('duration', 0, 3600)]
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Start action
|
|
180
|
+
densing(ActionSchema, { action: { type: 'start', delay: 5 } }); // "BQ" (8 bits, 2 base64 chars vs JSON 37 chars, -95%)
|
|
181
|
+
|
|
182
|
+
// Stop action
|
|
183
|
+
densing(ActionSchema, { action: { type: 'stop', force: true } }); // "Y" (3 bits, 1 base64 char vs JSON 39 chars, -97%)
|
|
184
|
+
|
|
185
|
+
// Pause action
|
|
186
|
+
densing(ActionSchema, { action: { type: 'pause', duration: 1234 } }); // "k0g" (14 bits, 3 base64 chars vs JSON 43 chars, -93%)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Enum Arrays (Packed)
|
|
190
|
+
|
|
191
|
+
Enum arrays are packed into minimal bits using base-N encoding:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { schema, enumArray, enumeration } from 'densing';
|
|
195
|
+
|
|
196
|
+
const ColorSchema = schema(enumArray('palette', enumeration('color', ['R', 'G', 'B']), 0, 10));
|
|
197
|
+
|
|
198
|
+
const data = { palette: ['R', 'G', 'B', 'R', 'R'] };
|
|
199
|
+
const encoded = densing(ColorSchema, data);
|
|
200
|
+
// "Ut" (12 bits, 2 base64 chars vs JSON 33 chars, -94%)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 🔧 Advanced Features
|
|
204
|
+
|
|
205
|
+
### Validation
|
|
206
|
+
|
|
207
|
+
Validate data before encoding:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { validate } from 'densing';
|
|
211
|
+
|
|
212
|
+
const result = validate(MySchema, data);
|
|
213
|
+
|
|
214
|
+
if (!result.valid) {
|
|
215
|
+
console.error('Validation errors:', result.errors);
|
|
216
|
+
// [{ path: 'age', message: 'value 150 out of range [0, 120]' }]
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Size Analysis
|
|
221
|
+
|
|
222
|
+
See exactly how your data will be encoded:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { analyzeDenseSchemaSize, calculateDenseDataSize } from 'densing';
|
|
226
|
+
|
|
227
|
+
// Static analysis (without data)
|
|
228
|
+
const schemaSize = analyzeDenseSchemaSize(MySchema);
|
|
229
|
+
console.log(schemaSize.staticRange);
|
|
230
|
+
// { minBits: 18, maxBits: 45, minBase64Chars: 3, maxBase64Chars: 8 }
|
|
231
|
+
|
|
232
|
+
// Actual size for specific data
|
|
233
|
+
const dataSize = calculateDenseDataSize(MySchema, myData);
|
|
234
|
+
console.log(dataSize);
|
|
235
|
+
// {
|
|
236
|
+
// totalBits: 32,
|
|
237
|
+
// base64Length: 6,
|
|
238
|
+
// fieldSizes: { deviceId: 10, enabled: 1, temperature: 11, mode: 2 },
|
|
239
|
+
// efficiency: { utilizationPercent: 51.8 }
|
|
240
|
+
// }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Default Values
|
|
244
|
+
|
|
245
|
+
Generate default data for your schema:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { getDefaultData } from 'densing';
|
|
249
|
+
|
|
250
|
+
const defaultData = getDefaultData(MySchema);
|
|
251
|
+
// { deviceId: 0, enabled: false, temperature: -40, mode: 'eco' }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Type Generation
|
|
255
|
+
|
|
256
|
+
Generate TypeScript types from your schema:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { generateTypes } from 'densing';
|
|
260
|
+
|
|
261
|
+
const types = generateTypes(MySchema, 'MyData');
|
|
262
|
+
console.log(types);
|
|
263
|
+
// export interface MyData {
|
|
264
|
+
// deviceId: number;
|
|
265
|
+
// enabled: boolean;
|
|
266
|
+
// temperature: number;
|
|
267
|
+
// mode: 'eco' | 'normal' | 'performance';
|
|
268
|
+
// }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Custom Bases
|
|
272
|
+
|
|
273
|
+
Use any character set for encoding:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// Default: base64url (URL-safe)
|
|
277
|
+
densing(schema, data); // "VA" (example)
|
|
278
|
+
|
|
279
|
+
// Binary string
|
|
280
|
+
densing(schema, data, 'binary'); // "0101010"
|
|
281
|
+
|
|
282
|
+
// Custom base (hexadecimal)
|
|
283
|
+
densing(schema, data, '0123456789ABCDEF'); // "54"
|
|
284
|
+
|
|
285
|
+
// Decode with same base
|
|
286
|
+
undensing(schema, encoded, 'binary');
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Recursive Structures
|
|
290
|
+
|
|
291
|
+
Define recursive data structures with `createRecursiveUnion`:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { schema, createRecursiveUnion, int, enumeration } from 'densing';
|
|
295
|
+
|
|
296
|
+
const ExpressionSchema = schema(
|
|
297
|
+
createRecursiveUnion(
|
|
298
|
+
'expr',
|
|
299
|
+
['number', 'add', 'multiply'],
|
|
300
|
+
(recurse) => ({
|
|
301
|
+
number: [int('value', 0, 1000)],
|
|
302
|
+
add: [recurse('left'), recurse('right')],
|
|
303
|
+
multiply: [recurse('left'), recurse('right')]
|
|
304
|
+
}),
|
|
305
|
+
5 // max depth
|
|
306
|
+
)
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Encode: (5 + 3) * 2
|
|
310
|
+
const data = {
|
|
311
|
+
expr: {
|
|
312
|
+
type: 'multiply',
|
|
313
|
+
left: {
|
|
314
|
+
type: 'add',
|
|
315
|
+
left: { type: 'number', value: 5 },
|
|
316
|
+
right: { type: 'number', value: 3 }
|
|
317
|
+
},
|
|
318
|
+
right: { type: 'number', value: 2 }
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
densing(ExpressionSchema, data); // "kAUAMAI" (190 bits, 7 base64 chars vs JSON 157 chars, -96%)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## 📊 Use Cases
|
|
326
|
+
|
|
327
|
+
### URL Parameters
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// Embed complex state in URLs
|
|
331
|
+
const state = { page: 5, sortBy: 'date', filters: [1, 3] };
|
|
332
|
+
const url = `https://app.com/search?state=${densing(StateSchema, state)}`;
|
|
333
|
+
// https://app.com/search?state=CCEw (4 base64 chars, 20 bits vs JSON 42 chars, -90%)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### QR Codes
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// Fit more data in QR codes
|
|
340
|
+
const deviceConfig = { id: 123, settings: {...} };
|
|
341
|
+
const qrData = densing(ConfigSchema, deviceConfig);
|
|
342
|
+
// Compact encoding means lower error correction level or more data capacity
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### IoT & Embedded Systems
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// Minimize bandwidth for sensor data
|
|
349
|
+
const sensorData = { temp: 23.5, humidity: 65.2, battery: 87 };
|
|
350
|
+
const payload = densing(SensorSchema, sensorData);
|
|
351
|
+
// "T3Rlc" (28 bits, 5 base64 chars vs JSON 42 chars, -88%)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Local Storage
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// Reduce storage footprint
|
|
358
|
+
localStorage.setItem('userPrefs', densing(PrefsSchema, preferences));
|
|
359
|
+
// Store 10 preferences in compact form instead of verbose JSON
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## 🧪 Testing
|
|
363
|
+
|
|
364
|
+
The library includes comprehensive tests:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
bun test
|
|
368
|
+
# 348 tests pass
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## 📖 API Reference
|
|
372
|
+
|
|
373
|
+
For detailed API documentation, see [API.md](./API.md).
|
|
374
|
+
|
|
375
|
+
### Core Functions
|
|
376
|
+
|
|
377
|
+
- `schema(...fields)` - Define a schema
|
|
378
|
+
- `densing(schema, data, base?)` - Encode data
|
|
379
|
+
- `undensing(schema, encoded, base?)` - Decode data
|
|
380
|
+
- `validate(schema, data)` - Validate data
|
|
381
|
+
- `getDefaultData(schema)` - Generate default values
|
|
382
|
+
|
|
383
|
+
### Field Builders
|
|
384
|
+
|
|
385
|
+
- `int(name, min, max, default?)` - Integer field
|
|
386
|
+
- `fixed(name, min, max, precision, default?)` - Fixed-point number
|
|
387
|
+
- `bool(name, default?)` - Boolean field
|
|
388
|
+
- `enumeration(name, options, default?)` - Enum field
|
|
389
|
+
- `optional(name, field, default?)` - Optional field
|
|
390
|
+
- `array(name, minLength, maxLength, itemField)` - Array field
|
|
391
|
+
- `enumArray(name, enumField, minLength, maxLength)` - Packed enum array
|
|
392
|
+
- `object(name, ...fields)` - Nested object
|
|
393
|
+
- `union(name, discriminator, variants)` - Discriminated union
|
|
394
|
+
|
|
395
|
+
### Utility Functions
|
|
396
|
+
|
|
397
|
+
- `analyzeDenseSchemaSize(schema)` - Static size analysis
|
|
398
|
+
- `calculateDenseDataSize(schema, data)` - Actual size calculation
|
|
399
|
+
- `getDenseFieldBitWidthRange(field)` - Min/max bits for field
|
|
400
|
+
- `calculateDenseFieldBitWidth(field, value)` - Actual bits used
|
|
401
|
+
- `getFieldByPath(schema, path)` - Get field by path
|
|
402
|
+
- `walkDenseSchema(schema, callback)` - Visit all fields
|
|
403
|
+
- `getAllDenseSchemaPaths(schema)` - Get all field paths
|
|
404
|
+
- `generateTypes(schema, typeName?)` - Generate TypeScript types
|
|
405
|
+
|
|
406
|
+
## 🎯 Performance
|
|
407
|
+
|
|
408
|
+
Densing is designed for efficiency (benchmarked on Bun runtime on m4 macbookAir):
|
|
409
|
+
|
|
410
|
+
**Simple Schema (4 fields: int, bool, fixed, enum):**
|
|
411
|
+
- Encoding: **~1,330,000 ops/sec** (0.75µs per operation)
|
|
412
|
+
- Decoding: **~1,516,000 ops/sec** (0.66µs per operation)
|
|
413
|
+
- Round-trip: **~753,000 ops/sec** (1.33µs per operation)
|
|
414
|
+
|
|
415
|
+
**Complex Schema (nested objects, arrays, optionals):**
|
|
416
|
+
- Encoding: **~484,000 ops/sec** (2.06µs per operation)
|
|
417
|
+
- Decoding: **~394,000 ops/sec** (2.54µs per operation)
|
|
418
|
+
|
|
419
|
+
**Large Arrays (50 integer elements):**
|
|
420
|
+
- Encoding: **~98,000 ops/sec** (10.23µs per operation)
|
|
421
|
+
- Decoding: **~91,000 ops/sec** (11.02µs per operation)
|
|
422
|
+
|
|
423
|
+
Run your own benchmarks:
|
|
424
|
+
```bash
|
|
425
|
+
bun run benchmark.ts
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## 🤝 Contributing
|
|
429
|
+
|
|
430
|
+
Contributions are welcome! Please feel free to submit issues or pull requests.
|
|
431
|
+
|
|
432
|
+
## 🙏 Acknowledgments
|
|
433
|
+
|
|
434
|
+
Built with TypeScript and tested with Bun.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
**Made with ❤️ for applications where every character matters**
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { DenseSchema, DenseField } from './schema-type';
|
|
2
|
+
/**
|
|
3
|
+
* Calculate the bit width RANGE for a field (min and max possible bits)
|
|
4
|
+
* Returns the minimum and maximum number of bits that can be used to encode this field
|
|
5
|
+
*/
|
|
6
|
+
export declare const getDenseFieldBitWidthRange: (field: DenseField) => {
|
|
7
|
+
min: number;
|
|
8
|
+
max: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Calculate the ACTUAL bit width for a field with a specific value
|
|
12
|
+
* Returns the exact number of bits that will be used when encoding this value
|
|
13
|
+
*/
|
|
14
|
+
export declare const calculateDenseFieldBitWidth: (field: DenseField, value: any) => number;
|
|
15
|
+
/**
|
|
16
|
+
* Schema-level size information (static analysis without data)
|
|
17
|
+
*/
|
|
18
|
+
export interface SchemaSizeInfo {
|
|
19
|
+
staticRange: {
|
|
20
|
+
minBits: number;
|
|
21
|
+
maxBits: number;
|
|
22
|
+
minBytes: number;
|
|
23
|
+
maxBytes: number;
|
|
24
|
+
minBase64Chars: number;
|
|
25
|
+
maxBase64Chars: number;
|
|
26
|
+
};
|
|
27
|
+
fieldRanges: Record<string, {
|
|
28
|
+
min: number;
|
|
29
|
+
max: number;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Analyze a schema to get static size information (min/max possible encoding sizes)
|
|
34
|
+
*/
|
|
35
|
+
export declare const analyzeDenseSchemaSize: (schema: DenseSchema) => SchemaSizeInfo;
|
|
36
|
+
/**
|
|
37
|
+
* Data-specific size information (actual encoding size for given data)
|
|
38
|
+
*/
|
|
39
|
+
export interface DataSizeInfo {
|
|
40
|
+
totalBits: number;
|
|
41
|
+
totalBytes: number;
|
|
42
|
+
base64Length: number;
|
|
43
|
+
fieldSizes: Record<string, number>;
|
|
44
|
+
efficiency: {
|
|
45
|
+
usedBits: number;
|
|
46
|
+
minPossibleBits: number;
|
|
47
|
+
maxPossibleBits: number;
|
|
48
|
+
utilizationPercent: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Calculate the actual encoding size for specific data
|
|
53
|
+
*/
|
|
54
|
+
export declare const calculateDenseDataSize: (schema: DenseSchema, data: any) => DataSizeInfo;
|
|
55
|
+
/**
|
|
56
|
+
* Get a field definition by path
|
|
57
|
+
* @example getFieldByPath(schema, 'network.port') -> IntField
|
|
58
|
+
*/
|
|
59
|
+
export declare const getFieldByPath: (schema: DenseSchema, path: string) => DenseField | null;
|
|
60
|
+
/**
|
|
61
|
+
* Walk all fields in schema (including nested)
|
|
62
|
+
* @example walkDenseSchema(schema, (field, path) => console.log(path, field))
|
|
63
|
+
*/
|
|
64
|
+
export declare const walkDenseSchema: (schema: DenseSchema, callback: (field: DenseField, path: string, parent?: DenseField) => void, prefix?: string) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Get all paths in schema
|
|
67
|
+
* @example getAllPaths(schema) -> ['deviceId', 'deviceType', 'network.ssid', 'network.port', ...]
|
|
68
|
+
*/
|
|
69
|
+
export declare const getAllDenseSchemaPaths: (schema: DenseSchema) => string[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { BitWriter, BitReader, BaseType } from './helpers';
|
|
2
|
+
import { DenseSchema, DenseField, ConstantBitWidthField } from './schema-type';
|
|
3
|
+
export declare const bitsForRange: (range: number) => number;
|
|
4
|
+
export declare const bitsForMinMaxLength: (minLength: number, maxLength: number) => number;
|
|
5
|
+
export declare const lengthForUIntMinMaxLength: (uInt: number, minLength: number) => number;
|
|
6
|
+
export declare const bitsForOptions: (options: readonly string[]) => number;
|
|
7
|
+
/**
|
|
8
|
+
* Densing method - key method to dense (pack into encoded string) the data for a given schema into a string of a specific base
|
|
9
|
+
* @param denseSchema - The schema to dense the data for
|
|
10
|
+
* @param data - The data to dense
|
|
11
|
+
* @param base - The base as string of characters, where every symbol is interpreted as a specific value
|
|
12
|
+
* @returns The dense string in the given base
|
|
13
|
+
*/
|
|
14
|
+
export declare const densing: (denseSchema: DenseSchema, data: any, base?: BaseType | string) => string;
|
|
15
|
+
export declare const getUIntForConstantBitWidthField: (field: ConstantBitWidthField, value: any) => number;
|
|
16
|
+
export declare const getBitWidthForContantBitWidthFields: (field: ConstantBitWidthField) => number;
|
|
17
|
+
/**
|
|
18
|
+
* Helper method to dense a single field of the schema into the given bit writer
|
|
19
|
+
* @param w - The bit writer to write the dense data to
|
|
20
|
+
* @param field - The field used as the template to dense the value with
|
|
21
|
+
* @param value - The value to dense, should match the type of the field! This method doesn't do any validation of data!
|
|
22
|
+
*/
|
|
23
|
+
export declare const densingField: (w: BitWriter, field: DenseField, value: any) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Undensing method - key method to undense (unpack from encoded string) the data for a given schema from a string of a specific base
|
|
26
|
+
* @param denseSchema - The schema to undense the data for
|
|
27
|
+
* @param baseString - The dense string to undense
|
|
28
|
+
* @param base - The base as string of characters, where every symbol is interpreted as a specific value
|
|
29
|
+
* @returns The undense data
|
|
30
|
+
*/
|
|
31
|
+
export declare const undensing: (denseSchema: DenseSchema, baseString: string, base?: BaseType | string) => any;
|
|
32
|
+
export declare const undensingDataForConstantBitWidthField: (field: ConstantBitWidthField, unsignedInt: number) => any;
|
|
33
|
+
/**
|
|
34
|
+
* Internal Helper method to undense a single field of the schema from the given bit reader
|
|
35
|
+
* @param r - The bit reader to read the dense data from
|
|
36
|
+
* @param denseField - The field used as the template to undense the value with
|
|
37
|
+
* @returns The undense value, should match the type of the field! This method doesn't do any validation of data!
|
|
38
|
+
*/
|
|
39
|
+
export declare const undensingField: (r: BitReader, denseField: DenseField) => any;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In Densing defined base types
|
|
3
|
+
* You can provide your own as well
|
|
4
|
+
*/
|
|
5
|
+
export declare const BaseTypes: readonly ["base64url", "baseQRCode45UrlSafe", "binary"];
|
|
6
|
+
/**
|
|
7
|
+
* Predefined base types
|
|
8
|
+
*/
|
|
9
|
+
export type BaseType = (typeof BaseTypes)[number];
|
|
10
|
+
export declare const base64url = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
11
|
+
export declare const baseQRCode45UrlSafe = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-.";
|
|
12
|
+
export declare const binary = "01";
|
|
13
|
+
export declare const getBigIntFromBase64: (base64: string) => bigint;
|
|
14
|
+
export declare const getBase64FromBigInt: (bigInt: bigint, bitWidth?: number) => string;
|
|
15
|
+
export declare const getbaseQRCode45UrlSafeFromBigInt: (bigInt: bigint, bitWidth?: number) => string;
|
|
16
|
+
export declare const getBigIntFrombaseQRCode45UrlSafe: (baseQRCode45UrlSafe: string) => bigint;
|
|
17
|
+
/**
|
|
18
|
+
* Helper class for writing the uInt numeric value of a field in the schema into the bigint representing the densed data
|
|
19
|
+
*/
|
|
20
|
+
export declare class BitWriter {
|
|
21
|
+
private buffer;
|
|
22
|
+
private bitsWritten;
|
|
23
|
+
constructor();
|
|
24
|
+
writeUInt: (value: number | bigint, bitWidth: number) => void;
|
|
25
|
+
getBigInt: () => bigint;
|
|
26
|
+
getBitLength: () => number;
|
|
27
|
+
getFromBase: (base?: BaseType | string) => string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Helper class for reading the uInt numeric value of a field in the schema from the bigint representing the densed data
|
|
31
|
+
*/
|
|
32
|
+
export declare class BitReader {
|
|
33
|
+
private buffer;
|
|
34
|
+
private bitsLeft;
|
|
35
|
+
private constructor();
|
|
36
|
+
readUInt: (bitWidth: number) => number;
|
|
37
|
+
readUBigInt: (bitWidth: number) => bigint;
|
|
38
|
+
static getFromBase: (baseString: string, base: BaseType | string) => BitReader;
|
|
39
|
+
}
|