@jarenjs/josl 0.49.2 → 0.66.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/src/write.js CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  formatSection,
18
18
  isPlainTable,
19
19
  } from './stringify.js';
20
+ import { closeIterator, abortedError } from './pull.js';
20
21
 
21
22
  class JoslStreamWriter {
22
23
  constructor(options = {}) {
@@ -26,7 +27,14 @@ class JoslStreamWriter {
26
27
  onRegExp: options.onRegExp,
27
28
  };
28
29
  this.onChunk = options.onChunk ?? null;
30
+ // `buffer: false` hands every chunk to `onChunk` and keeps none: the
31
+ // caller chose its sink, so the writer retains no second copy of the
32
+ // document; `text()`/`end()` then answer '' and say so
33
+ this.buffered = options.buffer !== false;
34
+ if (!this.buffered && this.onChunk === null)
35
+ throw new JoslStringifyError('buffer: false needs an onChunk sink to deliver the chunks to');
29
36
  this.chunks = [];
37
+ this.emitted = false;
30
38
  this.ended = false;
31
39
  this.rootIsArray = false;
32
40
  this.rootHasPairs = false;
@@ -37,7 +45,9 @@ class JoslStreamWriter {
37
45
  }
38
46
 
39
47
  emit(chunk) {
40
- this.chunks.push(chunk);
48
+ this.emitted = true;
49
+ if (this.buffered)
50
+ this.chunks.push(chunk);
41
51
  if (this.onChunk !== null)
42
52
  this.onChunk(chunk);
43
53
  }
@@ -48,7 +58,7 @@ class JoslStreamWriter {
48
58
  }
49
59
 
50
60
  blank() {
51
- if (this.chunks.length !== 0)
61
+ if (this.emitted)
52
62
  this.emit('\n');
53
63
  }
54
64
 
@@ -159,7 +169,8 @@ class JoslStreamWriter {
159
169
  }
160
170
 
161
171
  /**
162
- * The document text emitted so far.
172
+ * The document text emitted so far; `''` under `buffer: false`, whose
173
+ * chunks went to the sink and nowhere else.
163
174
  * @returns {string} Concatenated chunks
164
175
  */
165
176
  text() {
@@ -179,12 +190,15 @@ class JoslStreamWriter {
179
190
  /**
180
191
  * Create a streaming JOSL/TOML writer - the write-side mirror of
181
192
  * `createStreamReader`. Chunks are delivered through `onChunk` as they
182
- * are produced and also accumulate for `text()` / `end()`.
193
+ * are produced and also accumulate for `text()` / `end()` — unless
194
+ * `buffer: false`, which keeps no copy: the sink is the only holder.
183
195
  * @param {object} [options] - Writer options
184
196
  * @param {'josl'|'toml'} [options.mode] - 'toml' emits strict TOML 1.0
185
197
  * @param {'error'|'omit'} [options.onNull] - See `stringifyJosl`
186
198
  * @param {'error'|'string'} [options.onRegExp] - See `stringifyJosl`
187
199
  * @param {(chunk: string) => void} [options.onChunk] - Chunk sink
200
+ * @param {boolean} [options.buffer] - `false` retains no emitted text
201
+ * (needs `onChunk`); `text()` and `end()` then answer `''`
188
202
  * @returns {JoslStreamWriter} The writer
189
203
  */
190
204
  export function createStreamWriter(options = undefined) {
@@ -203,15 +217,8 @@ export function* stringifyJoslChunks(value, options = {}) {
203
217
  if (Array.isArray(value)) {
204
218
  if (options.mode === 'toml')
205
219
  throw new JoslStringifyError('a TOML root must be a table; root arrays are a JOSL extension');
206
- for (let i = 0; i < value.length; ++i) {
207
- if (!isPlainTable(value[i]))
208
- throw new JoslStringifyError('root array elements must be tables', [i]);
209
- const body = formatSection(value[i], options);
210
- // keep byte-identical with stringifyJosl: a body that opens with a
211
- // section header gets a blank line after the [[]] header
212
- yield (i === 0 ? '' : '\n') + '[[]]\n'
213
- + (body.startsWith('[') ? '\n' : '') + body;
214
- }
220
+ for (let i = 0; i < value.length; ++i)
221
+ yield rootItemChunk(value[i], i, options);
215
222
  return;
216
223
  }
217
224
  if (!isPlainTable(value))
@@ -221,6 +228,64 @@ export function* stringifyJoslChunks(value, options = {}) {
221
228
  yield text;
222
229
  }
223
230
 
231
+ /**
232
+ * One `[[]]` record as a chunk — the text both `stringifyJoslChunks` and
233
+ * `stringifyJoslStream` yield for record `index`, so the two are
234
+ * byte-identical by construction (and identical to `stringifyJosl`: a
235
+ * body that opens with a section header gets a blank line after the
236
+ * `[[]]` header).
237
+ * @param {*} record - A plain table
238
+ * @param {number} index - The record's position in the root array
239
+ * @param {object} options - Writer options
240
+ * @returns {string} The chunk
241
+ * @throws {JoslStringifyError} When the record is not a table
242
+ */
243
+ function rootItemChunk(record, index, options) {
244
+ if (!isPlainTable(record))
245
+ throw new JoslStringifyError('root array elements must be tables', [index]);
246
+ const body = formatSection(record, options);
247
+ return (index === 0 ? '' : '\n') + '[[]]\n' + (body.startsWith('[') ? '\n' : '') + body;
248
+ }
249
+
250
+ /**
251
+ * Serialize an async iterable of table records as an async iterable of
252
+ * `[[]]` chunks — the pull form of `stringifyJoslChunks` over a root
253
+ * array, byte-identical to it for the same records. Pull is the
254
+ * backpressure: the next record is requested only when the consumer
255
+ * asks for the next chunk. `options.signal` aborts between pulls (the
256
+ * rejection is the signal's reason); an abort, a consumer that stops
257
+ * early or a throw closes the record source exactly once.
258
+ * @param {AsyncIterable<object>|Iterable<object>} records - Table records
259
+ * @param {object} [options] - Writer options; see `stringifyJosl`, plus `signal`
260
+ * @yields {string} One `[[]]` record per chunk
261
+ * @example
262
+ * response.body = stringifyJoslStream(store.collection('rows').query(doc), { signal });
263
+ */
264
+ export async function* stringifyJoslStream(records, options = {}) {
265
+ if (options.mode === 'toml')
266
+ throw new JoslStringifyError('a TOML root must be a table; root arrays are a JOSL extension');
267
+ const signal = options.signal ?? null;
268
+ const iterator = records[Symbol.asyncIterator]?.() ?? records[Symbol.iterator]();
269
+ let index = 0;
270
+ let finished = false;
271
+ try {
272
+ for (;;) {
273
+ if (signal !== null && signal.aborted)
274
+ throw abortedError(signal);
275
+ const step = await iterator.next();
276
+ if (step.done) {
277
+ finished = true;
278
+ break;
279
+ }
280
+ yield rootItemChunk(step.value, index++, options);
281
+ }
282
+ }
283
+ finally {
284
+ if (!finished)
285
+ await closeIterator(iterator);
286
+ }
287
+ }
288
+
224
289
  export { JoslStringifyError } from './errors.js';
225
290
 
226
291
  //#endregion