turbo-stream 2.3.0 → 2.4.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/dist/flatten.js CHANGED
@@ -26,7 +26,7 @@ function flatten(input) {
26
26
  }
27
27
  exports.flatten = flatten;
28
28
  function stringify(input, index) {
29
- const { deferred, plugins } = this;
29
+ const { deferred, plugins, postPlugins } = this;
30
30
  const str = this.stringified;
31
31
  const stack = [[input, index]];
32
32
  while (stack.length > 0) {
@@ -34,6 +34,7 @@ function stringify(input, index) {
34
34
  const partsForObj = (obj) => Object.keys(obj)
35
35
  .map((k) => `"_${flatten.call(this, k)}":${flatten.call(this, obj[k])}`)
36
36
  .join(",");
37
+ let error = null;
37
38
  switch (typeof input) {
38
39
  case "boolean":
39
40
  case "number":
@@ -45,9 +46,12 @@ function stringify(input, index) {
45
46
  break;
46
47
  case "symbol": {
47
48
  const keyFor = Symbol.keyFor(input);
48
- if (!keyFor)
49
- throw new Error("Cannot encode symbol unless created with Symbol.for()");
50
- str[index] = `["${utils_js_1.TYPE_SYMBOL}",${JSON.stringify(keyFor)}]`;
49
+ if (!keyFor) {
50
+ error = new Error("Cannot encode symbol unless created with Symbol.for()");
51
+ }
52
+ else {
53
+ str[index] = `["${utils_js_1.TYPE_SYMBOL}",${JSON.stringify(keyFor)}]`;
54
+ }
51
55
  break;
52
56
  }
53
57
  case "object": {
@@ -133,7 +137,7 @@ function stringify(input, index) {
133
137
  str[index] = `{${partsForObj(input)}}`;
134
138
  }
135
139
  else {
136
- throw new Error("Cannot encode object with prototype");
140
+ error = new Error("Cannot encode object with prototype");
137
141
  }
138
142
  }
139
143
  break;
@@ -159,10 +163,33 @@ function stringify(input, index) {
159
163
  }
160
164
  }
161
165
  if (!pluginHandled) {
162
- throw new Error("Cannot encode function or unexpected type");
166
+ error = new Error("Cannot encode function or unexpected type");
163
167
  }
164
168
  }
165
169
  }
170
+ if (error) {
171
+ let pluginHandled = false;
172
+ if (postPlugins) {
173
+ for (const plugin of postPlugins) {
174
+ const pluginResult = plugin(input);
175
+ if (Array.isArray(pluginResult)) {
176
+ pluginHandled = true;
177
+ const [pluginIdentifier, ...rest] = pluginResult;
178
+ str[index] = `[${JSON.stringify(pluginIdentifier)}`;
179
+ if (rest.length > 0) {
180
+ str[index] += `,${rest
181
+ .map((v) => flatten.call(this, v))
182
+ .join(",")}`;
183
+ }
184
+ str[index] += "]";
185
+ break;
186
+ }
187
+ }
188
+ }
189
+ if (!pluginHandled) {
190
+ throw error;
191
+ }
192
+ }
166
193
  }
167
194
  }
168
195
  const objectProtoNames = Object.getOwnPropertyNames(Object.prototype)
@@ -8,5 +8,6 @@ export declare function decode(readable: ReadableStream<Uint8Array>, options?: {
8
8
  }>;
9
9
  export declare function encode(input: unknown, options?: {
10
10
  plugins?: EncodePlugin[];
11
+ postPlugins?: EncodePlugin[];
11
12
  signal?: AbortSignal;
12
13
  }): ReadableStream<Uint8Array>;
@@ -107,13 +107,14 @@ async function decodeDeferred(reader) {
107
107
  }
108
108
  }
109
109
  function encode(input, options) {
110
- const { plugins, signal } = options ?? {};
110
+ const { plugins, postPlugins, signal } = options ?? {};
111
111
  const encoder = {
112
112
  deferred: {},
113
113
  index: 0,
114
114
  indices: new Map(),
115
115
  stringified: [],
116
116
  plugins,
117
+ postPlugins,
117
118
  signal,
118
119
  };
119
120
  const textEncoder = new TextEncoder();
@@ -132,56 +133,79 @@ function encode(input, options) {
132
133
  lastSentIndex = encoder.stringified.length - 1;
133
134
  }
134
135
  const seenPromises = new WeakSet();
135
- while (Object.keys(encoder.deferred).length > 0) {
136
- for (const [deferredId, deferred] of Object.entries(encoder.deferred)) {
137
- if (seenPromises.has(deferred))
138
- continue;
139
- seenPromises.add((encoder.deferred[Number(deferredId)] = raceSignal(deferred, encoder.signal)
140
- .then((resolved) => {
141
- const id = flatten_js_1.flatten.call(encoder, resolved);
142
- if (Array.isArray(id)) {
143
- controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${deferredId}:[["${utils_js_1.TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`));
144
- encoder.index++;
145
- lastSentIndex++;
146
- }
147
- else if (id < 0) {
148
- controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${deferredId}:${id}\n`));
136
+ if (Object.keys(encoder.deferred).length) {
137
+ let raceDone;
138
+ const racePromise = new Promise((resolve, reject) => {
139
+ raceDone = resolve;
140
+ if (signal) {
141
+ const rejectPromise = () => reject(signal.reason || new Error("Signal was aborted."));
142
+ if (signal.aborted) {
143
+ rejectPromise();
149
144
  }
150
145
  else {
151
- const values = encoder.stringified
152
- .slice(lastSentIndex + 1)
153
- .join(",");
154
- controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${deferredId}:[${values}]\n`));
155
- lastSentIndex = encoder.stringified.length - 1;
156
- }
157
- }, (reason) => {
158
- if (!reason ||
159
- typeof reason !== "object" ||
160
- !(reason instanceof Error)) {
161
- reason = new Error("An unknown error occurred");
162
- }
163
- const id = flatten_js_1.flatten.call(encoder, reason);
164
- if (Array.isArray(id)) {
165
- controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_ERROR}${deferredId}:[["${utils_js_1.TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`));
166
- encoder.index++;
167
- lastSentIndex++;
146
+ signal.addEventListener("abort", (event) => {
147
+ rejectPromise();
148
+ });
168
149
  }
169
- else if (id < 0) {
170
- controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_ERROR}${deferredId}:${id}\n`));
171
- }
172
- else {
173
- const values = encoder.stringified
174
- .slice(lastSentIndex + 1)
175
- .join(",");
176
- controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_ERROR}${deferredId}:[${values}]\n`));
177
- lastSentIndex = encoder.stringified.length - 1;
178
- }
179
- })
180
- .finally(() => {
181
- delete encoder.deferred[Number(deferredId)];
182
- })));
150
+ }
151
+ });
152
+ while (Object.keys(encoder.deferred).length > 0) {
153
+ for (const [deferredId, deferred] of Object.entries(encoder.deferred)) {
154
+ if (seenPromises.has(deferred))
155
+ continue;
156
+ seenPromises.add(
157
+ // biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
158
+ (encoder.deferred[Number(deferredId)] = Promise.race([
159
+ racePromise,
160
+ deferred,
161
+ ])
162
+ .then((resolved) => {
163
+ const id = flatten_js_1.flatten.call(encoder, resolved);
164
+ if (Array.isArray(id)) {
165
+ controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${deferredId}:[["${utils_js_1.TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`));
166
+ encoder.index++;
167
+ lastSentIndex++;
168
+ }
169
+ else if (id < 0) {
170
+ controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${deferredId}:${id}\n`));
171
+ }
172
+ else {
173
+ const values = encoder.stringified
174
+ .slice(lastSentIndex + 1)
175
+ .join(",");
176
+ controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_PROMISE}${deferredId}:[${values}]\n`));
177
+ lastSentIndex = encoder.stringified.length - 1;
178
+ }
179
+ }, (reason) => {
180
+ if (!reason ||
181
+ typeof reason !== "object" ||
182
+ !(reason instanceof Error)) {
183
+ reason = new Error("An unknown error occurred");
184
+ }
185
+ const id = flatten_js_1.flatten.call(encoder, reason);
186
+ if (Array.isArray(id)) {
187
+ controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_ERROR}${deferredId}:[["${utils_js_1.TYPE_PREVIOUS_RESOLVED}",${id[0]}]]\n`));
188
+ encoder.index++;
189
+ lastSentIndex++;
190
+ }
191
+ else if (id < 0) {
192
+ controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_ERROR}${deferredId}:${id}\n`));
193
+ }
194
+ else {
195
+ const values = encoder.stringified
196
+ .slice(lastSentIndex + 1)
197
+ .join(",");
198
+ controller.enqueue(textEncoder.encode(`${utils_js_1.TYPE_ERROR}${deferredId}:[${values}]\n`));
199
+ lastSentIndex = encoder.stringified.length - 1;
200
+ }
201
+ })
202
+ .finally(() => {
203
+ delete encoder.deferred[Number(deferredId)];
204
+ })));
205
+ }
206
+ await Promise.race(Object.values(encoder.deferred));
183
207
  }
184
- await Promise.race(Object.values(encoder.deferred));
208
+ raceDone();
185
209
  }
186
210
  await Promise.all(Object.values(encoder.deferred));
187
211
  controller.close();
@@ -190,17 +214,3 @@ function encode(input, options) {
190
214
  return readable;
191
215
  }
192
216
  exports.encode = encode;
193
- function raceSignal(promise, signal) {
194
- if (!signal)
195
- return promise;
196
- if (signal.aborted)
197
- return Promise.reject(signal.reason || new Error("Signal was aborted."));
198
- const abort = new Promise((resolve, reject) => {
199
- signal.addEventListener("abort", (event) => {
200
- reject(signal.reason || new Error("Signal was aborted."));
201
- });
202
- promise.then(resolve).catch(reject);
203
- });
204
- abort.catch(() => { });
205
- return Promise.race([abort, promise]);
206
- }
@@ -72,12 +72,13 @@ function flatten(input) {
72
72
  return index;
73
73
  }
74
74
  function stringify(input, index) {
75
- const { deferred, plugins } = this;
75
+ const { deferred, plugins, postPlugins } = this;
76
76
  const str = this.stringified;
77
77
  const stack = [[input, index]];
78
78
  while (stack.length > 0) {
79
79
  const [input2, index2] = stack.pop();
80
80
  const partsForObj = (obj) => Object.keys(obj).map((k) => `"_${flatten.call(this, k)}":${flatten.call(this, obj[k])}`).join(",");
81
+ let error = null;
81
82
  switch (typeof input2) {
82
83
  case "boolean":
83
84
  case "number":
@@ -89,11 +90,13 @@ function stringify(input, index) {
89
90
  break;
90
91
  case "symbol": {
91
92
  const keyFor = Symbol.keyFor(input2);
92
- if (!keyFor)
93
- throw new Error(
93
+ if (!keyFor) {
94
+ error = new Error(
94
95
  "Cannot encode symbol unless created with Symbol.for()"
95
96
  );
96
- str[index2] = `["${TYPE_SYMBOL}",${JSON.stringify(keyFor)}]`;
97
+ } else {
98
+ str[index2] = `["${TYPE_SYMBOL}",${JSON.stringify(keyFor)}]`;
99
+ }
97
100
  break;
98
101
  }
99
102
  case "object": {
@@ -161,7 +164,7 @@ function stringify(input, index) {
161
164
  } else if (isPlainObject(input2)) {
162
165
  str[index2] = `{${partsForObj(input2)}}`;
163
166
  } else {
164
- throw new Error("Cannot encode object with prototype");
167
+ error = new Error("Cannot encode object with prototype");
165
168
  }
166
169
  }
167
170
  break;
@@ -185,9 +188,30 @@ function stringify(input, index) {
185
188
  }
186
189
  }
187
190
  if (!pluginHandled) {
188
- throw new Error("Cannot encode function or unexpected type");
191
+ error = new Error("Cannot encode function or unexpected type");
192
+ }
193
+ }
194
+ }
195
+ if (error) {
196
+ let pluginHandled = false;
197
+ if (postPlugins) {
198
+ for (const plugin of postPlugins) {
199
+ const pluginResult = plugin(input2);
200
+ if (Array.isArray(pluginResult)) {
201
+ pluginHandled = true;
202
+ const [pluginIdentifier, ...rest] = pluginResult;
203
+ str[index2] = `[${JSON.stringify(pluginIdentifier)}`;
204
+ if (rest.length > 0) {
205
+ str[index2] += `,${rest.map((v) => flatten.call(this, v)).join(",")}`;
206
+ }
207
+ str[index2] += "]";
208
+ break;
209
+ }
189
210
  }
190
211
  }
212
+ if (!pluginHandled) {
213
+ throw error;
214
+ }
191
215
  }
192
216
  }
193
217
  }
@@ -520,13 +544,14 @@ async function decodeDeferred(reader) {
520
544
  }
521
545
  }
522
546
  function encode(input, options) {
523
- const { plugins, signal } = options ?? {};
547
+ const { plugins, postPlugins, signal } = options ?? {};
524
548
  const encoder = {
525
549
  deferred: {},
526
550
  index: 0,
527
551
  indices: /* @__PURE__ */ new Map(),
528
552
  stringified: [],
529
553
  plugins,
554
+ postPlugins,
530
555
  signal
531
556
  };
532
557
  const textEncoder = new TextEncoder();
@@ -548,78 +573,100 @@ function encode(input, options) {
548
573
  lastSentIndex = encoder.stringified.length - 1;
549
574
  }
550
575
  const seenPromises = /* @__PURE__ */ new WeakSet();
551
- while (Object.keys(encoder.deferred).length > 0) {
552
- for (const [deferredId, deferred] of Object.entries(encoder.deferred)) {
553
- if (seenPromises.has(deferred))
554
- continue;
555
- seenPromises.add(
556
- encoder.deferred[Number(deferredId)] = raceSignal(
557
- deferred,
558
- encoder.signal
559
- ).then(
560
- (resolved) => {
561
- const id2 = flatten.call(encoder, resolved);
562
- if (Array.isArray(id2)) {
563
- controller.enqueue(
564
- textEncoder.encode(
565
- `${TYPE_PROMISE}${deferredId}:[["${TYPE_PREVIOUS_RESOLVED}",${id2[0]}]]
576
+ if (Object.keys(encoder.deferred).length) {
577
+ let raceDone;
578
+ const racePromise = new Promise((resolve, reject) => {
579
+ raceDone = resolve;
580
+ if (signal) {
581
+ const rejectPromise = () => reject(signal.reason || new Error("Signal was aborted."));
582
+ if (signal.aborted) {
583
+ rejectPromise();
584
+ } else {
585
+ signal.addEventListener("abort", (event) => {
586
+ rejectPromise();
587
+ });
588
+ }
589
+ }
590
+ });
591
+ while (Object.keys(encoder.deferred).length > 0) {
592
+ for (const [deferredId, deferred] of Object.entries(
593
+ encoder.deferred
594
+ )) {
595
+ if (seenPromises.has(deferred))
596
+ continue;
597
+ seenPromises.add(
598
+ // biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
599
+ encoder.deferred[Number(deferredId)] = Promise.race([
600
+ racePromise,
601
+ deferred
602
+ ]).then(
603
+ (resolved) => {
604
+ const id2 = flatten.call(encoder, resolved);
605
+ if (Array.isArray(id2)) {
606
+ controller.enqueue(
607
+ textEncoder.encode(
608
+ `${TYPE_PROMISE}${deferredId}:[["${TYPE_PREVIOUS_RESOLVED}",${id2[0]}]]
566
609
  `
567
- )
568
- );
569
- encoder.index++;
570
- lastSentIndex++;
571
- } else if (id2 < 0) {
572
- controller.enqueue(
573
- textEncoder.encode(`${TYPE_PROMISE}${deferredId}:${id2}
574
- `)
575
- );
576
- } else {
577
- const values = encoder.stringified.slice(lastSentIndex + 1).join(",");
578
- controller.enqueue(
579
- textEncoder.encode(
580
- `${TYPE_PROMISE}${deferredId}:[${values}]
610
+ )
611
+ );
612
+ encoder.index++;
613
+ lastSentIndex++;
614
+ } else if (id2 < 0) {
615
+ controller.enqueue(
616
+ textEncoder.encode(
617
+ `${TYPE_PROMISE}${deferredId}:${id2}
581
618
  `
582
- )
583
- );
584
- lastSentIndex = encoder.stringified.length - 1;
585
- }
586
- },
587
- (reason) => {
588
- if (!reason || typeof reason !== "object" || !(reason instanceof Error)) {
589
- reason = new Error("An unknown error occurred");
590
- }
591
- const id2 = flatten.call(encoder, reason);
592
- if (Array.isArray(id2)) {
593
- controller.enqueue(
594
- textEncoder.encode(
595
- `${TYPE_ERROR}${deferredId}:[["${TYPE_PREVIOUS_RESOLVED}",${id2[0]}]]
619
+ )
620
+ );
621
+ } else {
622
+ const values = encoder.stringified.slice(lastSentIndex + 1).join(",");
623
+ controller.enqueue(
624
+ textEncoder.encode(
625
+ `${TYPE_PROMISE}${deferredId}:[${values}]
596
626
  `
597
- )
598
- );
599
- encoder.index++;
600
- lastSentIndex++;
601
- } else if (id2 < 0) {
602
- controller.enqueue(
603
- textEncoder.encode(`${TYPE_ERROR}${deferredId}:${id2}
627
+ )
628
+ );
629
+ lastSentIndex = encoder.stringified.length - 1;
630
+ }
631
+ },
632
+ (reason) => {
633
+ if (!reason || typeof reason !== "object" || !(reason instanceof Error)) {
634
+ reason = new Error("An unknown error occurred");
635
+ }
636
+ const id2 = flatten.call(encoder, reason);
637
+ if (Array.isArray(id2)) {
638
+ controller.enqueue(
639
+ textEncoder.encode(
640
+ `${TYPE_ERROR}${deferredId}:[["${TYPE_PREVIOUS_RESOLVED}",${id2[0]}]]
641
+ `
642
+ )
643
+ );
644
+ encoder.index++;
645
+ lastSentIndex++;
646
+ } else if (id2 < 0) {
647
+ controller.enqueue(
648
+ textEncoder.encode(`${TYPE_ERROR}${deferredId}:${id2}
604
649
  `)
605
- );
606
- } else {
607
- const values = encoder.stringified.slice(lastSentIndex + 1).join(",");
608
- controller.enqueue(
609
- textEncoder.encode(
610
- `${TYPE_ERROR}${deferredId}:[${values}]
650
+ );
651
+ } else {
652
+ const values = encoder.stringified.slice(lastSentIndex + 1).join(",");
653
+ controller.enqueue(
654
+ textEncoder.encode(
655
+ `${TYPE_ERROR}${deferredId}:[${values}]
611
656
  `
612
- )
613
- );
614
- lastSentIndex = encoder.stringified.length - 1;
657
+ )
658
+ );
659
+ lastSentIndex = encoder.stringified.length - 1;
660
+ }
615
661
  }
616
- }
617
- ).finally(() => {
618
- delete encoder.deferred[Number(deferredId)];
619
- })
620
- );
662
+ ).finally(() => {
663
+ delete encoder.deferred[Number(deferredId)];
664
+ })
665
+ );
666
+ }
667
+ await Promise.race(Object.values(encoder.deferred));
621
668
  }
622
- await Promise.race(Object.values(encoder.deferred));
669
+ raceDone();
623
670
  }
624
671
  await Promise.all(Object.values(encoder.deferred));
625
672
  controller.close();
@@ -627,21 +674,6 @@ function encode(input, options) {
627
674
  });
628
675
  return readable;
629
676
  }
630
- function raceSignal(promise, signal) {
631
- if (!signal)
632
- return promise;
633
- if (signal.aborted)
634
- return Promise.reject(signal.reason || new Error("Signal was aborted."));
635
- const abort = new Promise((resolve, reject) => {
636
- signal.addEventListener("abort", (event) => {
637
- reject(signal.reason || new Error("Signal was aborted."));
638
- });
639
- promise.then(resolve).catch(reject);
640
- });
641
- abort.catch(() => {
642
- });
643
- return Promise.race([abort, promise]);
644
- }
645
677
  export {
646
678
  decode,
647
679
  encode
package/dist/utils.d.ts CHANGED
@@ -32,6 +32,7 @@ export interface ThisEncode {
32
32
  stringified: string[];
33
33
  deferred: Record<number, Promise<unknown>>;
34
34
  plugins?: EncodePlugin[];
35
+ postPlugins?: EncodePlugin[];
35
36
  signal?: AbortSignal;
36
37
  }
37
38
  export declare class Deferred<T = unknown> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbo-stream",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "description": "A streaming data transport format that aims to support built-in features such as Promises, Dates, RegExps, Maps, Sets and more.",
5
5
  "files": [
6
6
  "dist",
@@ -18,7 +18,7 @@
18
18
  "./package.json": "./package.json"
19
19
  },
20
20
  "scripts": {
21
- "build": "pnpm build:esm && pnpm build:cjs",
21
+ "build": "pnpm build:esm && pnpm build:cjs && cp ./dist/turbo-stream.mjs ./viewer/scripts/turbo-stream.js",
22
22
  "build:cjs": "tsc --outDir dist --project tsconfig.lib.json",
23
23
  "build:esm": "esbuild --bundle --platform=node --target=node16 --format=esm --outfile=dist/turbo-stream.mjs src/turbo-stream.ts",
24
24
  "test": "node --no-warnings --loader tsm --enable-source-maps --test-reporter tap --test src/*.spec.ts",