flatpack-json 8.19.2 → 8.19.4

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.
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert';
2
2
  import { ArrayRefElement, BigIntRefElement, DateRefElement, MapRefElement, NumberRefElement, ObjectRefElement, ObjectWrapperRefElement, PrimitiveRefElement, RegExpRefElement, SetRefElement, StringConcatRefElement, StringPrimitiveRefElement, SubStringRefElement, } from './RefElements.mjs';
3
- import { dataHeader, ElementType, } from './types.mjs';
3
+ import { ElementType, supportedHeaders, } from './types.mjs';
4
4
  export function fromElement(elem, resolve) {
5
5
  function handleArrayElement(element) {
6
6
  switch (element[0]) {
@@ -90,6 +90,6 @@ export class FlatpackedWrapper {
90
90
  }
91
91
  }
92
92
  export function isFlatpacked(value) {
93
- return Array.isArray(value) && value[0] === dataHeader;
93
+ return Array.isArray(value) && typeof value[0] === 'string' && supportedHeaders.has(value[0]);
94
94
  }
95
95
  //# sourceMappingURL=flatpackUtil.mjs.map
@@ -0,0 +1,3 @@
1
+ import { Flatpacked } from './types.mjs';
2
+ export declare function optimizeFlatpacked(data: Flatpacked): Flatpacked;
3
+ //# sourceMappingURL=optimizeFlatpacked.d.mts.map
@@ -0,0 +1,147 @@
1
+ import assert from 'node:assert';
2
+ import { ElementType, supportedHeaders } from './types.mjs';
3
+ export function optimizeFlatpacked(data) {
4
+ const [header] = data;
5
+ if (!supportedHeaders.has(header)) {
6
+ throw new Error('Invalid header');
7
+ }
8
+ const elementRefs = data.map((element, index) => ({ origIndex: index, refCount: 0, index: 0, element }));
9
+ const indexToRefElement = new Map(elementRefs.entries());
10
+ for (const refElement of elementRefs) {
11
+ if (refElement.origIndex === 0) {
12
+ continue;
13
+ }
14
+ const indexes = getRefIndexes(refElement.element);
15
+ for (const index of indexes) {
16
+ const ref = indexToRefElement.get(index);
17
+ assert(ref, `Invalid reference index: ${index}`);
18
+ ref.refCount++;
19
+ }
20
+ }
21
+ const sortedRefElements = elementRefs.slice(2).sort((a, b) => b.refCount - a.refCount || a.origIndex - b.origIndex);
22
+ sortedRefElements.forEach((refElement, index) => {
23
+ refElement.index = index + 2;
24
+ });
25
+ const indexMap = new Map([
26
+ [0, 0],
27
+ [1, 1],
28
+ ]);
29
+ sortedRefElements.forEach((refElement) => {
30
+ indexMap.set(refElement.origIndex, refElement.index);
31
+ });
32
+ const optimizedElements = [
33
+ data[1],
34
+ ...sortedRefElements.map((refElement) => refElement.element),
35
+ ].map((element) => patchIndexes(element, indexMap));
36
+ return [header, ...optimizedElements];
37
+ }
38
+ function patchIndexes(elem, indexMap) {
39
+ function mapIndex(index) {
40
+ const v = indexMap.get(index);
41
+ assert(v !== undefined, `Invalid index: ${index}`);
42
+ return v;
43
+ }
44
+ function mapIndexes(indexes) {
45
+ return indexes.map(mapIndex);
46
+ }
47
+ function handleArrayElement(element) {
48
+ switch (element[0]) {
49
+ case ElementType.Array: {
50
+ return [element[0], ...mapIndexes(element.slice(1))];
51
+ }
52
+ case ElementType.Object: {
53
+ return [element[0], mapIndex(element[1]), mapIndex(element[2])];
54
+ }
55
+ case ElementType.String: {
56
+ return [element[0], ...mapIndexes(element.slice(1))];
57
+ }
58
+ case ElementType.SubString: {
59
+ return element[3] !== undefined
60
+ ? [element[0], mapIndex(element[1]), element[2], element[3]]
61
+ : [element[0], mapIndex(element[1]), element[2]];
62
+ }
63
+ case ElementType.Set: {
64
+ return [element[0], mapIndex(element[1])];
65
+ }
66
+ case ElementType.Map: {
67
+ return [element[0], mapIndex(element[1]), mapIndex(element[2])];
68
+ }
69
+ case ElementType.RegExp: {
70
+ return [element[0], mapIndex(element[1]), mapIndex(element[2])];
71
+ }
72
+ case ElementType.Date: {
73
+ return element;
74
+ }
75
+ case ElementType.BigInt: {
76
+ return [element[0], mapIndex(element[1])];
77
+ }
78
+ }
79
+ assert(false, 'Invalid element type');
80
+ }
81
+ if (Array.isArray(elem)) {
82
+ return handleArrayElement(elem);
83
+ }
84
+ if (typeof elem === 'string') {
85
+ return elem;
86
+ }
87
+ if (typeof elem === 'number') {
88
+ return elem;
89
+ }
90
+ if (typeof elem === 'object') {
91
+ return elem;
92
+ }
93
+ assert(typeof elem === 'boolean');
94
+ return elem;
95
+ }
96
+ function getRefIndexes(elem) {
97
+ function handleArrayElement(element) {
98
+ switch (element[0]) {
99
+ case ElementType.Array: {
100
+ return element.slice(1);
101
+ }
102
+ case ElementType.Object: {
103
+ return element.slice(1).filter((v) => !!v);
104
+ }
105
+ case ElementType.String: {
106
+ return element.slice(1);
107
+ }
108
+ case ElementType.SubString: {
109
+ return [element[1]];
110
+ }
111
+ case ElementType.Set: {
112
+ return element.slice(1);
113
+ }
114
+ case ElementType.Map: {
115
+ return element.slice(1);
116
+ }
117
+ case ElementType.RegExp: {
118
+ return element.slice(1);
119
+ }
120
+ case ElementType.Date: {
121
+ return [];
122
+ }
123
+ case ElementType.BigInt: {
124
+ return [element[1]];
125
+ }
126
+ }
127
+ assert(false, 'Invalid element type');
128
+ }
129
+ if (Array.isArray(elem)) {
130
+ return handleArrayElement(elem);
131
+ }
132
+ if (typeof elem === 'string') {
133
+ return [];
134
+ }
135
+ if (typeof elem === 'number') {
136
+ return [];
137
+ }
138
+ if (typeof elem === 'object') {
139
+ if (elem === null) {
140
+ return [];
141
+ }
142
+ return [];
143
+ }
144
+ assert(typeof elem === 'boolean');
145
+ return [];
146
+ }
147
+ //# sourceMappingURL=optimizeFlatpacked.mjs.map
@@ -19,17 +19,24 @@ export declare class CompactStorage {
19
19
  */
20
20
  private cachedArrays;
21
21
  /**
22
- * Cache of strings that have been deduped and stored in the data array.
22
+ * Cache of strings used for prefix matching.
23
23
  */
24
24
  private knownStrings;
25
+ /**
26
+ * Cache of reversed strings used for suffix matching.
27
+ */
28
+ private knownStringsRev;
25
29
  private cachedElements;
26
30
  constructor(options?: FlatpackOptions | undefined);
27
31
  private primitiveToIdx;
28
32
  private addSubStringRef;
33
+ private estimateSubStringCost;
29
34
  private addKnownString;
30
35
  private addStringPrimitive;
31
36
  private duplicateIndex;
32
37
  private stringToIdx;
38
+ private findPrefix;
39
+ private findSuffix;
33
40
  private objSetToIdx;
34
41
  private createUniqueKeys;
35
42
  private objMapToIdx;
@@ -56,5 +63,5 @@ export declare class CompactStorage {
56
63
  toJSON<V extends Serializable>(json: V): Flatpacked;
57
64
  }
58
65
  export declare function toJSON<V extends Serializable>(json: V, options?: FlatpackOptions): Flatpacked;
59
- export declare function stringify(data: Unpacked, pretty?: boolean): string;
66
+ export declare function stringify(data: Unpacked, pretty?: boolean, options?: FlatpackOptions): string;
60
67
  //# sourceMappingURL=storage.d.mts.map
package/dist/storage.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import assert from 'node:assert';
2
+ import { optimizeFlatpacked } from './optimizeFlatpacked.mjs';
2
3
  import { stringifyFlatpacked } from './stringify.mjs';
3
4
  import { Trie } from './Trie.mjs';
4
5
  import { blockSplitRegex, dataHeader, ElementType } from './types.mjs';
@@ -32,9 +33,13 @@ export class CompactStorage {
32
33
  */
33
34
  cachedArrays = new Map();
34
35
  /**
35
- * Cache of strings that have been deduped and stored in the data array.
36
+ * Cache of strings used for prefix matching.
36
37
  */
37
38
  knownStrings = new Trie();
39
+ /**
40
+ * Cache of reversed strings used for suffix matching.
41
+ */
42
+ knownStringsRev = new Trie();
38
43
  cachedElements = new Map();
39
44
  constructor(options) {
40
45
  this.options = options;
@@ -66,9 +71,23 @@ export class CompactStorage {
66
71
  this.cache.set(value, idx);
67
72
  return idx;
68
73
  }
74
+ estimateSubStringCost(idxString, value, offset) {
75
+ if (this.cache.has(value)) {
76
+ return 0;
77
+ }
78
+ let cost = 5;
79
+ cost += Math.ceil(Math.log10(idxString));
80
+ cost += Math.ceil(Math.log10(value.length));
81
+ if (offset) {
82
+ cost += Math.ceil(Math.log10(offset)) + 1;
83
+ }
84
+ return cost;
85
+ }
69
86
  addKnownString(idx, value) {
70
87
  if (value.length >= minSubStringLen) {
71
- this.knownStrings.add(value.length > 256 ? value.slice(0, 256) : value, { idx });
88
+ const data = { idx, value };
89
+ this.knownStrings.add(value.length > 256 ? [...value].slice(0, 256) : value, data);
90
+ this.knownStringsRev.add(revStr(value, 256), data);
72
91
  }
73
92
  }
74
93
  addStringPrimitive(value) {
@@ -90,20 +109,63 @@ export class CompactStorage {
90
109
  if (forceStringPrimitives || value.length < minSubStringLen || blockSplitRegex.test(value)) {
91
110
  return this.addStringPrimitive(value);
92
111
  }
93
- const trieFound = this.knownStrings.find(value);
94
- if (!trieFound || !trieFound.data || trieFound.found.length < minSubStringLen) {
112
+ const foundPrefix = this.findPrefix(value);
113
+ const foundSuffix = this.findSuffix(value);
114
+ if (!foundPrefix && !foundSuffix) {
95
115
  return this.addStringPrimitive(value);
96
116
  }
97
- const { data: tData, found: subStr } = trieFound;
98
- const sIdx = this.addSubStringRef(tData.idx, subStr, tData.offset);
99
- if (subStr === value)
100
- return sIdx;
101
- const v = [sIdx, this.stringToIdx(value.slice(subStr.length))];
117
+ if (foundPrefix &&
118
+ (!foundSuffix ||
119
+ foundPrefix.subStr.length - foundPrefix.cost >= foundSuffix.subStr.length - foundSuffix.cost)) {
120
+ const { idx: pfIdx, subStr, offset } = foundPrefix;
121
+ const sIdx = this.addSubStringRef(pfIdx, subStr, offset);
122
+ if (subStr === value)
123
+ return sIdx;
124
+ const v = [sIdx, this.stringToIdx(value.slice(subStr.length))];
125
+ const idx = this.data.push([ElementType.String, ...v]) - 1;
126
+ this.cache.set(value, idx);
127
+ this.addKnownString(idx, value);
128
+ return idx;
129
+ }
130
+ if (!foundSuffix) {
131
+ return this.addStringPrimitive(value);
132
+ }
133
+ const { idx: pfIdx, subStr, offset } = foundSuffix;
134
+ const sIdx = this.addSubStringRef(pfIdx, subStr, offset);
135
+ const v = [this.stringToIdx(value.slice(0, -subStr.length)), sIdx];
102
136
  const idx = this.data.push([ElementType.String, ...v]) - 1;
103
137
  this.cache.set(value, idx);
104
138
  this.addKnownString(idx, value);
105
139
  return idx;
106
140
  }
141
+ findPrefix(value) {
142
+ const trieFound = this.knownStrings.find(value);
143
+ if (!trieFound || !trieFound.data || trieFound.found.length < minSubStringLen) {
144
+ return undefined;
145
+ }
146
+ const { data: tData, found: subStr } = trieFound;
147
+ const cost = this.estimateSubStringCost(tData.idx, subStr, undefined);
148
+ if (cost > subStr.length) {
149
+ return undefined;
150
+ }
151
+ return { idx: tData.idx, subStr, offset: undefined, cost };
152
+ }
153
+ findSuffix(value) {
154
+ const rev = revStr(value, 256);
155
+ const trieFound = this.knownStringsRev.find(rev);
156
+ if (!trieFound || !trieFound.data || trieFound.found.length < minSubStringLen) {
157
+ return undefined;
158
+ }
159
+ const { data: tData, found: subStrRev } = trieFound;
160
+ const offset = tData.value.length - subStrRev.length;
161
+ const cost = this.estimateSubStringCost(tData.idx, subStrRev, offset);
162
+ if (cost > subStrRev.length) {
163
+ return undefined;
164
+ }
165
+ const srcStr = tData.value;
166
+ const subStr = srcStr.slice(-subStrRev.length);
167
+ return { idx: tData.idx, subStr, offset, cost };
168
+ }
107
169
  objSetToIdx(value) {
108
170
  const found = this.cache.get(value);
109
171
  if (found !== undefined) {
@@ -328,7 +390,25 @@ export class CompactStorage {
328
390
  toJSON(json) {
329
391
  this.softReset();
330
392
  this.valueToIdx(json);
331
- return this.data;
393
+ return this.options?.optimize ? optimizeFlatpacked(this.data) : this.data;
394
+ }
395
+ }
396
+ function* revStr(str, limit) {
397
+ let i = str.length - 1;
398
+ let n = 0;
399
+ while (i >= 0 && n < limit) {
400
+ // eslint-disable-next-line unicorn/prefer-code-point
401
+ const code = str.charCodeAt(i);
402
+ if (code & 0xfc00) {
403
+ // surrogate pair
404
+ i -= 1;
405
+ yield str.slice(i, i + 2);
406
+ }
407
+ else {
408
+ yield str[i];
409
+ }
410
+ i -= 1;
411
+ n += 1;
332
412
  }
333
413
  }
334
414
  function isEqual(a, b) {
@@ -358,7 +438,8 @@ function isObjectWrapper(value) {
358
438
  export function toJSON(json, options) {
359
439
  return new CompactStorage(options).toJSON(json);
360
440
  }
361
- export function stringify(data, pretty = true) {
362
- return pretty ? stringifyFlatpacked(toJSON(data)) : JSON.stringify(toJSON(data));
441
+ export function stringify(data, pretty = true, options) {
442
+ const json = toJSON(data, options);
443
+ return pretty ? stringifyFlatpacked(json) : JSON.stringify(json);
363
444
  }
364
445
  //# sourceMappingURL=storage.mjs.map
package/dist/types.d.mts CHANGED
@@ -93,7 +93,22 @@ export interface FlatpackOptions {
93
93
  * @default true
94
94
  */
95
95
  dedupe?: boolean;
96
+ /**
97
+ * Try to optimize the size of the output.
98
+ */
99
+ optimize?: boolean;
96
100
  }
97
- export declare const dataHeader: "Dehydrated JSON v1";
101
+ /**
102
+ * Legacy header for Flatpack JSON.
103
+ */
104
+ export declare const dataHeaderV0_1: "Dehydrated JSON v1";
105
+ /**
106
+ * The current header for Flatpack JSON.
107
+ */
108
+ export declare const dataHeader: "Flatpack JSON v1";
109
+ /**
110
+ * The set of supported headers for Flatpack JSON.
111
+ */
112
+ export declare const supportedHeaders: Set<string>;
98
113
  export {};
99
114
  //# sourceMappingURL=types.d.mts.map
package/dist/types.mjs CHANGED
@@ -11,5 +11,16 @@ export var ElementType;
11
11
  ElementType[ElementType["BigInt"] = 8] = "BigInt";
12
12
  })(ElementType || (ElementType = {}));
13
13
  export const blockSplitRegex = /^sha\d/;
14
- export const dataHeader = 'Dehydrated JSON v1';
14
+ /**
15
+ * Legacy header for Flatpack JSON.
16
+ */
17
+ export const dataHeaderV0_1 = 'Dehydrated JSON v1';
18
+ /**
19
+ * The current header for Flatpack JSON.
20
+ */
21
+ export const dataHeader = 'Flatpack JSON v1';
22
+ /**
23
+ * The set of supported headers for Flatpack JSON.
24
+ */
25
+ export const supportedHeaders = new Set([dataHeaderV0_1, dataHeader]);
15
26
  //# sourceMappingURL=types.mjs.map
package/dist/unpack.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import assert from 'node:assert';
2
- import { dataHeader, ElementType, } from './types.mjs';
2
+ import { ElementType, supportedHeaders, } from './types.mjs';
3
3
  export function fromJSON(data) {
4
4
  const [header] = data;
5
- if (header !== dataHeader) {
5
+ if (!supportedHeaders.has(header)) {
6
6
  throw new Error('Invalid header');
7
7
  }
8
8
  const cache = new Map([[0, undefined]]);
package/package.json CHANGED
@@ -4,8 +4,8 @@
4
4
  "access": "public",
5
5
  "provenance": true
6
6
  },
7
- "version": "8.19.2",
8
- "description": "A library to normalize JSON objects to reduce the size.",
7
+ "version": "8.19.4",
8
+ "description": "A library to normalize / flatten JSON objects to reduce the size.",
9
9
  "keywords": [
10
10
  "cspell",
11
11
  "json",
@@ -53,8 +53,9 @@
53
53
  "node": ">=18"
54
54
  },
55
55
  "devDependencies": {
56
- "@cspell/filetypes": "8.19.2",
57
- "diff": "^7.0.0"
56
+ "@cspell/filetypes": "8.19.4",
57
+ "diff": "^7.0.0",
58
+ "flatted": "^3.3.3"
58
59
  },
59
- "gitHead": "f630ececa8cd91420aaaaa81c4ad910039457a06"
60
+ "gitHead": "1bee5f5aa4429a1b1ae0e88934b093c5440b44dc"
60
61
  }