@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.
- package/README.md +19 -0
- package/package.json +131 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +891 -0
- package/src/browser/navigation-client.ts +155 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +545 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +228 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +149 -0
- package/src/browser/rsc-router.tsx +310 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +443 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
- package/src/cache/cf/cf-cache-store.ts +274 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/index.ts +52 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +366 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +609 -0
- package/src/components/DefaultDocument.tsx +20 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +178 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href.ts +139 -0
- package/src/index.rsc.ts +69 -0
- package/src/index.ts +84 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1333 -0
- package/src/route-map-builder.ts +140 -0
- package/src/route-types.ts +148 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +60 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +116 -0
- package/src/router/match-context.ts +261 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +250 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +212 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +271 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3484 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +942 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +225 -0
- package/src/segment-system.tsx +405 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +340 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +470 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +126 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +215 -0
- package/src/types.ts +1473 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +608 -0
- package/src/vite/version.d.ts +12 -0
- 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
|
+
});
|