source: imaps-frontend/node_modules/@discoveryjs/json-ext/src/parse-chunked.js@ 79a0317

main
Last change on this file since 79a0317 was 79a0317, checked in by stefan toskovski <stefantoska84@…>, 3 days ago

F4 Finalna Verzija

  • Property mode set to 100644
File size: 12.0 KB
Line 
1import { isIterable } from './utils.js';
2
3const STACK_OBJECT = 1;
4const STACK_ARRAY = 2;
5const decoder = new TextDecoder();
6
7function adjustPosition(error, parser) {
8 if (error.name === 'SyntaxError' && parser.jsonParseOffset) {
9 error.message = error.message.replace(/at position (\d+)/, (_, pos) =>
10 'at position ' + (Number(pos) + parser.jsonParseOffset)
11 );
12 }
13
14 return error;
15}
16
17function append(array, elements) {
18 // Note: Avoid to use array.push(...elements) since it may lead to
19 // "RangeError: Maximum call stack size exceeded" for a long arrays
20 const initialLength = array.length;
21 array.length += elements.length;
22
23 for (let i = 0; i < elements.length; i++) {
24 array[initialLength + i] = elements[i];
25 }
26}
27
28export async function parseChunked(chunkEmitter) {
29 const iterable = typeof chunkEmitter === 'function'
30 ? chunkEmitter()
31 : chunkEmitter;
32
33 if (isIterable(iterable)) {
34 let parser = new ChunkParser();
35
36 try {
37 for await (const chunk of iterable) {
38 if (typeof chunk !== 'string' && !ArrayBuffer.isView(chunk)) {
39 throw new TypeError('Invalid chunk: Expected string, TypedArray or Buffer');
40 }
41
42 parser.push(chunk);
43 }
44
45 return parser.finish();
46 } catch (e) {
47 throw adjustPosition(e, parser);
48 }
49 }
50
51 throw new TypeError(
52 'Invalid chunk emitter: Expected an Iterable, AsyncIterable, generator, ' +
53 'async generator, or a function returning an Iterable or AsyncIterable'
54 );
55};
56
57class ChunkParser {
58 constructor() {
59 this.value = undefined;
60 this.valueStack = null;
61
62 this.stack = new Array(100);
63 this.lastFlushDepth = 0;
64 this.flushDepth = 0;
65 this.stateString = false;
66 this.stateStringEscape = false;
67 this.pendingByteSeq = null;
68 this.pendingChunk = null;
69 this.chunkOffset = 0;
70 this.jsonParseOffset = 0;
71 }
72
73 parseAndAppend(fragment, wrap) {
74 // Append new entries or elements
75 if (this.stack[this.lastFlushDepth - 1] === STACK_OBJECT) {
76 if (wrap) {
77 this.jsonParseOffset--;
78 fragment = '{' + fragment + '}';
79 }
80
81 Object.assign(this.valueStack.value, JSON.parse(fragment));
82 } else {
83 if (wrap) {
84 this.jsonParseOffset--;
85 fragment = '[' + fragment + ']';
86 }
87
88 append(this.valueStack.value, JSON.parse(fragment));
89 }
90 }
91
92 prepareAddition(fragment) {
93 const { value } = this.valueStack;
94 const expectComma = Array.isArray(value)
95 ? value.length !== 0
96 : Object.keys(value).length !== 0;
97
98 if (expectComma) {
99 // Skip a comma at the beginning of fragment, otherwise it would
100 // fail to parse
101 if (fragment[0] === ',') {
102 this.jsonParseOffset++;
103 return fragment.slice(1);
104 }
105
106 // When value (an object or array) is not empty and a fragment
107 // doesn't start with a comma, a single valid fragment starting
108 // is a closing bracket. If it's not, a prefix is adding to fail
109 // parsing. Otherwise, the sequence of chunks can be successfully
110 // parsed, although it should not, e.g. ["[{}", "{}]"]
111 if (fragment[0] !== '}' && fragment[0] !== ']') {
112 this.jsonParseOffset -= 3;
113 return '[[]' + fragment;
114 }
115 }
116
117 return fragment;
118 }
119
120 flush(chunk, start, end) {
121 let fragment = chunk.slice(start, end);
122
123 // Save position correction an error in JSON.parse() if any
124 this.jsonParseOffset = this.chunkOffset + start;
125
126 // Prepend pending chunk if any
127 if (this.pendingChunk !== null) {
128 fragment = this.pendingChunk + fragment;
129 this.jsonParseOffset -= this.pendingChunk.length;
130 this.pendingChunk = null;
131 }
132
133 if (this.flushDepth === this.lastFlushDepth) {
134 // Depth didn't changed, so it's a root value or entry/element set
135 if (this.flushDepth > 0) {
136 this.parseAndAppend(this.prepareAddition(fragment), true);
137 } else {
138 // That's an entire value on a top level
139 this.value = JSON.parse(fragment);
140 this.valueStack = {
141 value: this.value,
142 prev: null
143 };
144 }
145 } else if (this.flushDepth > this.lastFlushDepth) {
146 // Add missed closing brackets/parentheses
147 for (let i = this.flushDepth - 1; i >= this.lastFlushDepth; i--) {
148 fragment += this.stack[i] === STACK_OBJECT ? '}' : ']';
149 }
150
151 if (this.lastFlushDepth === 0) {
152 // That's a root value
153 this.value = JSON.parse(fragment);
154 this.valueStack = {
155 value: this.value,
156 prev: null
157 };
158 } else {
159 this.parseAndAppend(this.prepareAddition(fragment), true);
160 }
161
162 // Move down to the depths to the last object/array, which is current now
163 for (let i = this.lastFlushDepth || 1; i < this.flushDepth; i++) {
164 let value = this.valueStack.value;
165
166 if (this.stack[i - 1] === STACK_OBJECT) {
167 // find last entry
168 let key;
169 // eslint-disable-next-line curly
170 for (key in value);
171 value = value[key];
172 } else {
173 // last element
174 value = value[value.length - 1];
175 }
176
177 this.valueStack = {
178 value,
179 prev: this.valueStack
180 };
181 }
182 } else /* this.flushDepth < this.lastFlushDepth */ {
183 fragment = this.prepareAddition(fragment);
184
185 // Add missed opening brackets/parentheses
186 for (let i = this.lastFlushDepth - 1; i >= this.flushDepth; i--) {
187 this.jsonParseOffset--;
188 fragment = (this.stack[i] === STACK_OBJECT ? '{' : '[') + fragment;
189 }
190
191 this.parseAndAppend(fragment, false);
192
193 for (let i = this.lastFlushDepth - 1; i >= this.flushDepth; i--) {
194 this.valueStack = this.valueStack.prev;
195 }
196 }
197
198 this.lastFlushDepth = this.flushDepth;
199 }
200
201 push(chunk) {
202 if (typeof chunk !== 'string') {
203 // Suppose chunk is Buffer or Uint8Array
204
205 // Prepend uncompleted byte sequence if any
206 if (this.pendingByteSeq !== null) {
207 const origRawChunk = chunk;
208 chunk = new Uint8Array(this.pendingByteSeq.length + origRawChunk.length);
209 chunk.set(this.pendingByteSeq);
210 chunk.set(origRawChunk, this.pendingByteSeq.length);
211 this.pendingByteSeq = null;
212 }
213
214 // In case Buffer/Uint8Array, an input is encoded in UTF8
215 // Seek for parts of uncompleted UTF8 symbol on the ending
216 // This makes sense only if we expect more chunks and last char is not multi-bytes
217 if (chunk[chunk.length - 1] > 127) {
218 for (let seqLength = 0; seqLength < chunk.length; seqLength++) {
219 const byte = chunk[chunk.length - 1 - seqLength];
220
221 // 10xxxxxx - 2nd, 3rd or 4th byte
222 // 110xxxxx – first byte of 2-byte sequence
223 // 1110xxxx - first byte of 3-byte sequence
224 // 11110xxx - first byte of 4-byte sequence
225 if (byte >> 6 === 3) {
226 seqLength++;
227
228 // If the sequence is really incomplete, then preserve it
229 // for the future chunk and cut off it from the current chunk
230 if ((seqLength !== 4 && byte >> 3 === 0b11110) ||
231 (seqLength !== 3 && byte >> 4 === 0b1110) ||
232 (seqLength !== 2 && byte >> 5 === 0b110)) {
233 this.pendingByteSeq = chunk.slice(chunk.length - seqLength);
234 chunk = chunk.slice(0, -seqLength);
235 }
236
237 break;
238 }
239 }
240 }
241
242 // Convert chunk to a string, since single decode per chunk
243 // is much effective than decode multiple small substrings
244 chunk = decoder.decode(chunk);
245 }
246
247 const chunkLength = chunk.length;
248 let lastFlushPoint = 0;
249 let flushPoint = 0;
250
251 // Main scan loop
252 scan: for (let i = 0; i < chunkLength; i++) {
253 if (this.stateString) {
254 for (; i < chunkLength; i++) {
255 if (this.stateStringEscape) {
256 this.stateStringEscape = false;
257 } else {
258 switch (chunk.charCodeAt(i)) {
259 case 0x22: /* " */
260 this.stateString = false;
261 continue scan;
262
263 case 0x5C: /* \ */
264 this.stateStringEscape = true;
265 }
266 }
267 }
268
269 break;
270 }
271
272 switch (chunk.charCodeAt(i)) {
273 case 0x22: /* " */
274 this.stateString = true;
275 this.stateStringEscape = false;
276 break;
277
278 case 0x2C: /* , */
279 flushPoint = i;
280 break;
281
282 case 0x7B: /* { */
283 // Open an object
284 flushPoint = i + 1;
285 this.stack[this.flushDepth++] = STACK_OBJECT;
286 break;
287
288 case 0x5B: /* [ */
289 // Open an array
290 flushPoint = i + 1;
291 this.stack[this.flushDepth++] = STACK_ARRAY;
292 break;
293
294 case 0x5D: /* ] */
295 case 0x7D: /* } */
296 // Close an object or array
297 flushPoint = i + 1;
298 this.flushDepth--;
299
300 if (this.flushDepth < this.lastFlushDepth) {
301 this.flush(chunk, lastFlushPoint, flushPoint);
302 lastFlushPoint = flushPoint;
303 }
304
305 break;
306
307 case 0x09: /* \t */
308 case 0x0A: /* \n */
309 case 0x0D: /* \r */
310 case 0x20: /* space */
311 // Move points forward when they points on current position and it's a whitespace
312 if (lastFlushPoint === i) {
313 lastFlushPoint++;
314 }
315
316 if (flushPoint === i) {
317 flushPoint++;
318 }
319
320 break;
321 }
322 }
323
324 if (flushPoint > lastFlushPoint) {
325 this.flush(chunk, lastFlushPoint, flushPoint);
326 }
327
328 // Produce pendingChunk if something left
329 if (flushPoint < chunkLength) {
330 if (this.pendingChunk !== null) {
331 // When there is already a pending chunk then no flush happened,
332 // appending entire chunk to pending one
333 this.pendingChunk += chunk;
334 } else {
335 // Create a pending chunk, it will start with non-whitespace since
336 // flushPoint was moved forward away from whitespaces on scan
337 this.pendingChunk = chunk.slice(flushPoint, chunkLength);
338 }
339 }
340
341 this.chunkOffset += chunkLength;
342 }
343
344 finish() {
345 if (this.pendingChunk !== null) {
346 this.flush('', 0, 0);
347 this.pendingChunk = null;
348 }
349
350 return this.value;
351 }
352};
Note: See TracBrowser for help on using the repository browser.