@ivogt/rsc-router 0.0.0-experimental.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.
Files changed (123) hide show
  1. package/README.md +19 -0
  2. package/package.json +131 -0
  3. package/src/__mocks__/version.ts +6 -0
  4. package/src/__tests__/route-definition.test.ts +63 -0
  5. package/src/browser/event-controller.ts +876 -0
  6. package/src/browser/index.ts +18 -0
  7. package/src/browser/link-interceptor.ts +121 -0
  8. package/src/browser/lru-cache.ts +69 -0
  9. package/src/browser/merge-segment-loaders.ts +126 -0
  10. package/src/browser/navigation-bridge.ts +891 -0
  11. package/src/browser/navigation-client.ts +155 -0
  12. package/src/browser/navigation-store.ts +823 -0
  13. package/src/browser/partial-update.ts +545 -0
  14. package/src/browser/react/Link.tsx +248 -0
  15. package/src/browser/react/NavigationProvider.tsx +228 -0
  16. package/src/browser/react/ScrollRestoration.tsx +94 -0
  17. package/src/browser/react/context.ts +53 -0
  18. package/src/browser/react/index.ts +52 -0
  19. package/src/browser/react/location-state-shared.ts +120 -0
  20. package/src/browser/react/location-state.ts +62 -0
  21. package/src/browser/react/use-action.ts +240 -0
  22. package/src/browser/react/use-client-cache.ts +56 -0
  23. package/src/browser/react/use-handle.ts +178 -0
  24. package/src/browser/react/use-link-status.ts +134 -0
  25. package/src/browser/react/use-navigation.ts +150 -0
  26. package/src/browser/react/use-segments.ts +188 -0
  27. package/src/browser/request-controller.ts +149 -0
  28. package/src/browser/rsc-router.tsx +310 -0
  29. package/src/browser/scroll-restoration.ts +324 -0
  30. package/src/browser/server-action-bridge.ts +747 -0
  31. package/src/browser/shallow.ts +35 -0
  32. package/src/browser/types.ts +443 -0
  33. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  34. package/src/cache/__tests__/memory-store.test.ts +484 -0
  35. package/src/cache/cache-scope.ts +565 -0
  36. package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
  37. package/src/cache/cf/cf-cache-store.ts +274 -0
  38. package/src/cache/cf/index.ts +19 -0
  39. package/src/cache/index.ts +52 -0
  40. package/src/cache/memory-segment-store.ts +150 -0
  41. package/src/cache/memory-store.ts +253 -0
  42. package/src/cache/types.ts +366 -0
  43. package/src/client.rsc.tsx +88 -0
  44. package/src/client.tsx +609 -0
  45. package/src/components/DefaultDocument.tsx +20 -0
  46. package/src/default-error-boundary.tsx +88 -0
  47. package/src/deps/browser.ts +8 -0
  48. package/src/deps/html-stream-client.ts +2 -0
  49. package/src/deps/html-stream-server.ts +2 -0
  50. package/src/deps/rsc.ts +10 -0
  51. package/src/deps/ssr.ts +2 -0
  52. package/src/errors.ts +259 -0
  53. package/src/handle.ts +120 -0
  54. package/src/handles/MetaTags.tsx +178 -0
  55. package/src/handles/index.ts +6 -0
  56. package/src/handles/meta.ts +247 -0
  57. package/src/href-client.ts +128 -0
  58. package/src/href.ts +139 -0
  59. package/src/index.rsc.ts +69 -0
  60. package/src/index.ts +84 -0
  61. package/src/loader.rsc.ts +204 -0
  62. package/src/loader.ts +47 -0
  63. package/src/network-error-thrower.tsx +21 -0
  64. package/src/outlet-context.ts +15 -0
  65. package/src/root-error-boundary.tsx +277 -0
  66. package/src/route-content-wrapper.tsx +198 -0
  67. package/src/route-definition.ts +1333 -0
  68. package/src/route-map-builder.ts +140 -0
  69. package/src/route-types.ts +148 -0
  70. package/src/route-utils.ts +89 -0
  71. package/src/router/__tests__/match-context.test.ts +104 -0
  72. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  73. package/src/router/__tests__/match-result.test.ts +566 -0
  74. package/src/router/__tests__/on-error.test.ts +935 -0
  75. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  76. package/src/router/error-handling.ts +287 -0
  77. package/src/router/handler-context.ts +60 -0
  78. package/src/router/loader-resolution.ts +326 -0
  79. package/src/router/manifest.ts +116 -0
  80. package/src/router/match-context.ts +261 -0
  81. package/src/router/match-middleware/background-revalidation.ts +236 -0
  82. package/src/router/match-middleware/cache-lookup.ts +261 -0
  83. package/src/router/match-middleware/cache-store.ts +250 -0
  84. package/src/router/match-middleware/index.ts +81 -0
  85. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  86. package/src/router/match-middleware/segment-resolution.ts +174 -0
  87. package/src/router/match-pipelines.ts +214 -0
  88. package/src/router/match-result.ts +212 -0
  89. package/src/router/metrics.ts +62 -0
  90. package/src/router/middleware.test.ts +1355 -0
  91. package/src/router/middleware.ts +748 -0
  92. package/src/router/pattern-matching.ts +271 -0
  93. package/src/router/revalidation.ts +190 -0
  94. package/src/router/router-context.ts +299 -0
  95. package/src/router/types.ts +96 -0
  96. package/src/router.ts +3484 -0
  97. package/src/rsc/__tests__/helpers.test.ts +175 -0
  98. package/src/rsc/handler.ts +942 -0
  99. package/src/rsc/helpers.ts +64 -0
  100. package/src/rsc/index.ts +56 -0
  101. package/src/rsc/nonce.ts +18 -0
  102. package/src/rsc/types.ts +225 -0
  103. package/src/segment-system.tsx +405 -0
  104. package/src/server/__tests__/request-context.test.ts +171 -0
  105. package/src/server/context.ts +340 -0
  106. package/src/server/handle-store.ts +230 -0
  107. package/src/server/loader-registry.ts +174 -0
  108. package/src/server/request-context.ts +470 -0
  109. package/src/server/root-layout.tsx +10 -0
  110. package/src/server/tsconfig.json +14 -0
  111. package/src/server.ts +126 -0
  112. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  113. package/src/ssr/index.tsx +215 -0
  114. package/src/types.ts +1473 -0
  115. package/src/use-loader.tsx +346 -0
  116. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  117. package/src/vite/expose-action-id.ts +344 -0
  118. package/src/vite/expose-handle-id.ts +209 -0
  119. package/src/vite/expose-loader-id.ts +357 -0
  120. package/src/vite/expose-location-state-id.ts +177 -0
  121. package/src/vite/index.ts +608 -0
  122. package/src/vite/version.d.ts +12 -0
  123. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,537 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { compose, empty } from "../match-pipelines";
3
+ import type { GeneratorMiddleware } from "../match-middleware/cache-lookup";
4
+
5
+ // Helper to collect all values from an async generator
6
+ async function collect<T>(gen: AsyncGenerator<T>): Promise<T[]> {
7
+ const results: T[] = [];
8
+ for await (const item of gen) {
9
+ results.push(item);
10
+ }
11
+ return results;
12
+ }
13
+
14
+ // Helper to create async generator from array
15
+ async function* fromArray<T>(items: T[]): AsyncGenerator<T> {
16
+ for (const item of items) {
17
+ yield item;
18
+ }
19
+ }
20
+
21
+ describe("match-pipelines", () => {
22
+ describe("empty()", () => {
23
+ it("should yield no items", async () => {
24
+ const result = await collect(empty<number>());
25
+ expect(result).toEqual([]);
26
+ });
27
+
28
+ it("should complete immediately", async () => {
29
+ const gen = empty<string>();
30
+ const { done } = await gen.next();
31
+ expect(done).toBe(true);
32
+ });
33
+ });
34
+
35
+ describe("compose()", () => {
36
+ it("should return identity middleware when no middleware provided", async () => {
37
+ const pipeline = compose<number>();
38
+ const source = fromArray([1, 2, 3]);
39
+ const result = await collect(pipeline(source));
40
+
41
+ expect(result).toEqual([1, 2, 3]);
42
+ });
43
+
44
+ it("should return single middleware when one provided", async () => {
45
+ const double: GeneratorMiddleware<number> = async function* (source) {
46
+ for await (const n of source) {
47
+ yield n * 2;
48
+ }
49
+ };
50
+
51
+ const pipeline = compose(double);
52
+ const source = fromArray([1, 2, 3]);
53
+ const result = await collect(pipeline(source));
54
+
55
+ expect(result).toEqual([2, 4, 6]);
56
+ });
57
+
58
+ it("should compose middleware in reverse order (rightmost runs first)", async () => {
59
+ const order: string[] = [];
60
+
61
+ const addA: GeneratorMiddleware<string> = async function* (source) {
62
+ order.push("A-start");
63
+ for await (const s of source) {
64
+ yield s + "A";
65
+ }
66
+ order.push("A-end");
67
+ };
68
+
69
+ const addB: GeneratorMiddleware<string> = async function* (source) {
70
+ order.push("B-start");
71
+ for await (const s of source) {
72
+ yield s + "B";
73
+ }
74
+ order.push("B-end");
75
+ };
76
+
77
+ const addC: GeneratorMiddleware<string> = async function* (source) {
78
+ order.push("C-start");
79
+ for await (const s of source) {
80
+ yield s + "C";
81
+ }
82
+ order.push("C-end");
83
+ };
84
+
85
+ // compose(A, B, C) means C runs first (innermost), then B, then A (outermost)
86
+ const pipeline = compose(addA, addB, addC);
87
+ const source = fromArray(["x"]);
88
+ const result = await collect(pipeline(source));
89
+
90
+ // C transforms first: "x" -> "xC"
91
+ // B transforms second: "xC" -> "xCB"
92
+ // A transforms last: "xCB" -> "xCBA"
93
+ expect(result).toEqual(["xCBA"]);
94
+ expect(order).toEqual([
95
+ "A-start",
96
+ "B-start",
97
+ "C-start",
98
+ "C-end",
99
+ "B-end",
100
+ "A-end",
101
+ ]);
102
+ });
103
+
104
+ it("should handle middleware that yields multiple items", async () => {
105
+ const duplicate: GeneratorMiddleware<number> = async function* (source) {
106
+ for await (const n of source) {
107
+ yield n;
108
+ yield n;
109
+ }
110
+ };
111
+
112
+ const pipeline = compose(duplicate);
113
+ const source = fromArray([1, 2]);
114
+ const result = await collect(pipeline(source));
115
+
116
+ expect(result).toEqual([1, 1, 2, 2]);
117
+ });
118
+
119
+ it("should handle middleware that filters items", async () => {
120
+ const evensOnly: GeneratorMiddleware<number> = async function* (source) {
121
+ for await (const n of source) {
122
+ if (n % 2 === 0) {
123
+ yield n;
124
+ }
125
+ }
126
+ };
127
+
128
+ const pipeline = compose(evensOnly);
129
+ const source = fromArray([1, 2, 3, 4, 5]);
130
+ const result = await collect(pipeline(source));
131
+
132
+ expect(result).toEqual([2, 4]);
133
+ });
134
+
135
+ it("should handle middleware that adds items at beginning", async () => {
136
+ const prepend: GeneratorMiddleware<number> = async function* (source) {
137
+ yield 0;
138
+ yield* source;
139
+ };
140
+
141
+ const pipeline = compose(prepend);
142
+ const source = fromArray([1, 2, 3]);
143
+ const result = await collect(pipeline(source));
144
+
145
+ expect(result).toEqual([0, 1, 2, 3]);
146
+ });
147
+
148
+ it("should handle middleware that adds items at end", async () => {
149
+ const append: GeneratorMiddleware<number> = async function* (source) {
150
+ yield* source;
151
+ yield 99;
152
+ };
153
+
154
+ const pipeline = compose(append);
155
+ const source = fromArray([1, 2, 3]);
156
+ const result = await collect(pipeline(source));
157
+
158
+ expect(result).toEqual([1, 2, 3, 99]);
159
+ });
160
+
161
+ it("should work with empty source", async () => {
162
+ const double: GeneratorMiddleware<number> = async function* (source) {
163
+ for await (const n of source) {
164
+ yield n * 2;
165
+ }
166
+ };
167
+
168
+ const pipeline = compose(double);
169
+ const source = fromArray<number>([]);
170
+ const result = await collect(pipeline(source));
171
+
172
+ expect(result).toEqual([]);
173
+ });
174
+
175
+ it("should compose three middleware correctly", async () => {
176
+ const add1: GeneratorMiddleware<number> = async function* (source) {
177
+ for await (const n of source) {
178
+ yield n + 1;
179
+ }
180
+ };
181
+
182
+ const multiply2: GeneratorMiddleware<number> = async function* (source) {
183
+ for await (const n of source) {
184
+ yield n * 2;
185
+ }
186
+ };
187
+
188
+ const add10: GeneratorMiddleware<number> = async function* (source) {
189
+ for await (const n of source) {
190
+ yield n + 10;
191
+ }
192
+ };
193
+
194
+ // compose(add1, multiply2, add10)(source)
195
+ // Flow: source -> add10 -> multiply2 -> add1 -> output
196
+ // 5 -> 15 -> 30 -> 31
197
+ const pipeline = compose(add1, multiply2, add10);
198
+ const source = fromArray([5]);
199
+ const result = await collect(pipeline(source));
200
+
201
+ expect(result).toEqual([31]);
202
+ });
203
+
204
+ it("should handle async operations in middleware", async () => {
205
+ const asyncDouble: GeneratorMiddleware<number> = async function* (
206
+ source
207
+ ) {
208
+ for await (const n of source) {
209
+ await new Promise((r) => setTimeout(r, 1));
210
+ yield n * 2;
211
+ }
212
+ };
213
+
214
+ const pipeline = compose(asyncDouble);
215
+ const source = fromArray([1, 2, 3]);
216
+ const result = await collect(pipeline(source));
217
+
218
+ expect(result).toEqual([2, 4, 6]);
219
+ });
220
+
221
+ it("should propagate errors from source", async () => {
222
+ const identity: GeneratorMiddleware<number> = async function* (source) {
223
+ yield* source;
224
+ };
225
+
226
+ async function* errorSource(): AsyncGenerator<number> {
227
+ yield 1;
228
+ throw new Error("source error");
229
+ }
230
+
231
+ const pipeline = compose(identity);
232
+
233
+ await expect(collect(pipeline(errorSource()))).rejects.toThrow(
234
+ "source error"
235
+ );
236
+ });
237
+
238
+ it("should propagate errors from middleware", async () => {
239
+ const throwingMiddleware: GeneratorMiddleware<number> =
240
+ async function* () {
241
+ throw new Error("middleware error");
242
+ };
243
+
244
+ const pipeline = compose(throwingMiddleware);
245
+ const source = fromArray([1, 2, 3]);
246
+
247
+ await expect(collect(pipeline(source))).rejects.toThrow(
248
+ "middleware error"
249
+ );
250
+ });
251
+
252
+ it("should handle middleware that conditionally yields based on source", async () => {
253
+ let sourceWasEmpty = true;
254
+
255
+ const checkEmpty: GeneratorMiddleware<number> = async function* (
256
+ source
257
+ ) {
258
+ for await (const n of source) {
259
+ sourceWasEmpty = false;
260
+ yield n;
261
+ }
262
+ if (sourceWasEmpty) {
263
+ yield -1; // Sentinel value for empty source
264
+ }
265
+ };
266
+
267
+ const pipeline = compose(checkEmpty);
268
+
269
+ // Test with non-empty source
270
+ sourceWasEmpty = true;
271
+ const result1 = await collect(pipeline(fromArray([1, 2])));
272
+ expect(result1).toEqual([1, 2]);
273
+
274
+ // Test with empty source
275
+ sourceWasEmpty = true;
276
+ const result2 = await collect(pipeline(fromArray([])));
277
+ expect(result2).toEqual([-1]);
278
+ });
279
+ });
280
+
281
+ describe("compose() edge cases", () => {
282
+ it("should handle middleware that does not consume source (early return)", async () => {
283
+ const earlyReturn: GeneratorMiddleware<number> = async function* () {
284
+ yield 42;
285
+ // Does not iterate source at all
286
+ };
287
+
288
+ const pipeline = compose(earlyReturn);
289
+ const source = fromArray([1, 2, 3]);
290
+ const result = await collect(pipeline(source));
291
+
292
+ expect(result).toEqual([42]);
293
+ });
294
+
295
+ it("should handle middleware that yields before consuming source", async () => {
296
+ const yieldFirst: GeneratorMiddleware<number> = async function* (
297
+ source
298
+ ) {
299
+ yield 0; // Yield before consuming
300
+ for await (const n of source) {
301
+ yield n;
302
+ }
303
+ };
304
+
305
+ const pipeline = compose(yieldFirst);
306
+ const source = fromArray([1, 2, 3]);
307
+ const result = await collect(pipeline(source));
308
+
309
+ expect(result).toEqual([0, 1, 2, 3]);
310
+ });
311
+
312
+ it("should handle middleware that yields both before and after source", async () => {
313
+ const wrap: GeneratorMiddleware<number> = async function* (source) {
314
+ yield -1; // Before
315
+ for await (const n of source) {
316
+ yield n;
317
+ }
318
+ yield 99; // After
319
+ };
320
+
321
+ const pipeline = compose(wrap);
322
+ const source = fromArray([1, 2, 3]);
323
+ const result = await collect(pipeline(source));
324
+
325
+ expect(result).toEqual([-1, 1, 2, 3, 99]);
326
+ });
327
+
328
+ it("should handle error thrown partway through source iteration", async () => {
329
+ let count = 0;
330
+ const countAndPass: GeneratorMiddleware<number> = async function* (
331
+ source
332
+ ) {
333
+ for await (const n of source) {
334
+ count++;
335
+ yield n;
336
+ }
337
+ };
338
+
339
+ async function* partialErrorSource(): AsyncGenerator<number> {
340
+ yield 1;
341
+ yield 2;
342
+ throw new Error("error after 2 items");
343
+ }
344
+
345
+ const pipeline = compose(countAndPass);
346
+
347
+ await expect(collect(pipeline(partialErrorSource()))).rejects.toThrow(
348
+ "error after 2 items"
349
+ );
350
+ expect(count).toBe(2); // Should have processed 2 items before error
351
+ });
352
+
353
+ it("should handle long middleware chains (10 middleware)", async () => {
354
+ const makeAdder = (n: number): GeneratorMiddleware<number> =>
355
+ async function* (source) {
356
+ for await (const x of source) {
357
+ yield x + n;
358
+ }
359
+ };
360
+
361
+ // Create chain of 10 middleware, each adding 1
362
+ const middleware = Array.from({ length: 10 }, () => makeAdder(1));
363
+ const pipeline = compose(...middleware);
364
+
365
+ const source = fromArray([0]);
366
+ const result = await collect(pipeline(source));
367
+
368
+ expect(result).toEqual([10]); // 0 + 1*10 = 10
369
+ });
370
+
371
+ it("should handle middleware that accumulates state across iterations", async () => {
372
+ const runningSum: GeneratorMiddleware<number> = async function* (
373
+ source
374
+ ) {
375
+ let sum = 0;
376
+ for await (const n of source) {
377
+ sum += n;
378
+ yield sum;
379
+ }
380
+ };
381
+
382
+ const pipeline = compose(runningSum);
383
+ const source = fromArray([1, 2, 3, 4]);
384
+ const result = await collect(pipeline(source));
385
+
386
+ expect(result).toEqual([1, 3, 6, 10]); // Running sums
387
+ });
388
+
389
+ it("should handle middleware that batches items", async () => {
390
+ const batchBy2: GeneratorMiddleware<number> = async function* (source) {
391
+ let batch: number[] = [];
392
+ for await (const n of source) {
393
+ batch.push(n);
394
+ if (batch.length === 2) {
395
+ yield batch.reduce((a, b) => a + b, 0); // Yield sum of batch
396
+ batch = [];
397
+ }
398
+ }
399
+ if (batch.length > 0) {
400
+ yield batch.reduce((a, b) => a + b, 0); // Yield remaining
401
+ }
402
+ };
403
+
404
+ const pipeline = compose(batchBy2);
405
+
406
+ // Even number of items
407
+ const result1 = await collect(pipeline(fromArray([1, 2, 3, 4])));
408
+ expect(result1).toEqual([3, 7]); // (1+2), (3+4)
409
+
410
+ // Odd number of items
411
+ const result2 = await collect(pipeline(fromArray([1, 2, 3])));
412
+ expect(result2).toEqual([3, 3]); // (1+2), (3)
413
+ });
414
+
415
+ it("should handle middleware that skips first N items", async () => {
416
+ const skipFirst2: GeneratorMiddleware<number> = async function* (
417
+ source
418
+ ) {
419
+ let count = 0;
420
+ for await (const n of source) {
421
+ if (count >= 2) {
422
+ yield n;
423
+ }
424
+ count++;
425
+ }
426
+ };
427
+
428
+ const pipeline = compose(skipFirst2);
429
+ const source = fromArray([1, 2, 3, 4, 5]);
430
+ const result = await collect(pipeline(source));
431
+
432
+ expect(result).toEqual([3, 4, 5]);
433
+ });
434
+
435
+ it("should handle middleware that takes first N items only", async () => {
436
+ const takeFirst2: GeneratorMiddleware<number> = async function* (
437
+ source
438
+ ) {
439
+ let count = 0;
440
+ for await (const n of source) {
441
+ if (count >= 2) break;
442
+ yield n;
443
+ count++;
444
+ }
445
+ };
446
+
447
+ const pipeline = compose(takeFirst2);
448
+ const source = fromArray([1, 2, 3, 4, 5]);
449
+ const result = await collect(pipeline(source));
450
+
451
+ expect(result).toEqual([1, 2]);
452
+ });
453
+
454
+ it("should handle composed middleware where inner produces more than outer consumes", async () => {
455
+ const duplicate: GeneratorMiddleware<number> = async function* (source) {
456
+ for await (const n of source) {
457
+ yield n;
458
+ yield n;
459
+ }
460
+ };
461
+
462
+ const takeFirst3: GeneratorMiddleware<number> = async function* (
463
+ source
464
+ ) {
465
+ let count = 0;
466
+ for await (const n of source) {
467
+ if (count >= 3) break;
468
+ yield n;
469
+ count++;
470
+ }
471
+ };
472
+
473
+ // takeFirst3 is outer (runs last), duplicate is inner (runs first)
474
+ const pipeline = compose(takeFirst3, duplicate);
475
+ const source = fromArray([1, 2, 3]);
476
+ const result = await collect(pipeline(source));
477
+
478
+ // duplicate produces: 1, 1, 2, 2, 3, 3
479
+ // takeFirst3 takes: 1, 1, 2
480
+ expect(result).toEqual([1, 1, 2]);
481
+ });
482
+
483
+ it("should handle middleware with async delays between yields", async () => {
484
+ const delayedYield: GeneratorMiddleware<number> = async function* (
485
+ source
486
+ ) {
487
+ for await (const n of source) {
488
+ await new Promise((r) => setTimeout(r, 5));
489
+ yield n * 2;
490
+ await new Promise((r) => setTimeout(r, 5));
491
+ }
492
+ };
493
+
494
+ const pipeline = compose(delayedYield);
495
+ const source = fromArray([1, 2]);
496
+ const result = await collect(pipeline(source));
497
+
498
+ expect(result).toEqual([2, 4]);
499
+ });
500
+
501
+ it("should handle middleware that transforms type", async () => {
502
+ // Note: This tests type transformation within the same generic constraint
503
+ const stringify: GeneratorMiddleware<number | string> = async function* (
504
+ source
505
+ ) {
506
+ for await (const n of source) {
507
+ yield `num:${n}`;
508
+ }
509
+ };
510
+
511
+ const pipeline = compose(stringify);
512
+ const source = fromArray([1, 2, 3] as (number | string)[]);
513
+ const result = await collect(pipeline(source));
514
+
515
+ expect(result).toEqual(["num:1", "num:2", "num:3"]);
516
+ });
517
+
518
+ it("should handle re-using the same composed pipeline multiple times", async () => {
519
+ const double: GeneratorMiddleware<number> = async function* (source) {
520
+ for await (const n of source) {
521
+ yield n * 2;
522
+ }
523
+ };
524
+
525
+ const pipeline = compose(double);
526
+
527
+ // Use the same pipeline multiple times
528
+ const result1 = await collect(pipeline(fromArray([1, 2])));
529
+ const result2 = await collect(pipeline(fromArray([3, 4])));
530
+ const result3 = await collect(pipeline(fromArray([5])));
531
+
532
+ expect(result1).toEqual([2, 4]);
533
+ expect(result2).toEqual([6, 8]);
534
+ expect(result3).toEqual([10]);
535
+ });
536
+ });
537
+ });