@anterior/brrr 0.1.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.
@@ -0,0 +1,705 @@
1
+ import { beforeEach, suite, test } from "node:test";
2
+ import { strictEqual } from "node:assert";
3
+ import {
4
+ type ActiveWorker,
5
+ AppConsumer,
6
+ AppWorker,
7
+ type Handlers,
8
+ taskFn,
9
+ } from "./app.ts";
10
+ import {
11
+ type Connection,
12
+ Defer,
13
+ type Request,
14
+ type Response,
15
+ Server,
16
+ SubscriberServer,
17
+ } from "./connection.ts";
18
+ import {
19
+ InMemoryCache,
20
+ InMemoryEmitter,
21
+ InMemoryStore,
22
+ } from "./backends/in-memory.ts";
23
+ import { NaiveJsonCodec } from "./naive-json-codec.ts";
24
+ import type { Call } from "./call.ts";
25
+ import { NotFoundError, SpawnLimitError } from "./errors.ts";
26
+ import { deepStrictEqual, ok, rejects } from "node:assert/strict";
27
+ import { LocalApp, LocalBrrr } from "./local-app.ts";
28
+ import type { Cache, Store } from "./store.ts";
29
+ import type { Publisher, Subscriber } from "./emitter.ts";
30
+ import { BrrrShutdownSymbol, BrrrTaskDoneEventSymbol } from "./symbol.ts";
31
+
32
+ const codec = new NaiveJsonCodec();
33
+ const topic = "brrr-test";
34
+ const subtopics = {
35
+ t1: "t1",
36
+ t2: "t2",
37
+ t3: "t3",
38
+ } as const;
39
+
40
+ let store: Store;
41
+ let cache: Cache;
42
+ let emitter: Publisher & Subscriber;
43
+ let server: SubscriberServer;
44
+
45
+ // Test tasks
46
+ function bar(a: number) {
47
+ return 456;
48
+ }
49
+
50
+ async function foo(app: ActiveWorker, a: number) {
51
+ return (await app.call(bar, topic)(a + 1)) + 1;
52
+ }
53
+
54
+ function one(a: number): number {
55
+ return a + 5;
56
+ }
57
+
58
+ async function two(app: ActiveWorker, a: number): Promise<void> {
59
+ const result = await app.call("one", subtopics.t1)(a + 3);
60
+ strictEqual(result, 15);
61
+ }
62
+
63
+ const handlers: Handlers = {
64
+ bar: taskFn(bar),
65
+ foo,
66
+ };
67
+
68
+ await suite(import.meta.filename, async () => {
69
+ function waitFor(call: Call, predicate?: () => Promise<void>): Promise<void> {
70
+ return new Promise((resolve) => {
71
+ emitter.onEventSymbol?.(
72
+ BrrrTaskDoneEventSymbol,
73
+ async ({ callHash }: Call) => {
74
+ if (callHash === call.callHash) {
75
+ await predicate?.();
76
+ resolve();
77
+ }
78
+ },
79
+ );
80
+ });
81
+ }
82
+
83
+ beforeEach(() => {
84
+ store = new InMemoryStore();
85
+ cache = new InMemoryCache();
86
+ emitter = new InMemoryEmitter();
87
+ server = new SubscriberServer(store, cache, emitter);
88
+ });
89
+
90
+ await test(AppWorker.name, async () => {
91
+ const app = new AppWorker(codec, server, handlers);
92
+ server.listen(topic, app.handle);
93
+
94
+ const call = await codec.encodeCall(foo.name, [122]);
95
+
96
+ const done = waitFor(call, async () => {
97
+ strictEqual(await app.read(foo)(122), 457);
98
+ strictEqual(await app.read("foo")(122), 457);
99
+ strictEqual(await app.read(foo)(122), 457);
100
+ strictEqual(await app.read("bar")(123), 456);
101
+ strictEqual(await app.read(bar)(123), 456);
102
+ });
103
+
104
+ await app.schedule(foo, topic)(122);
105
+ return done;
106
+ });
107
+
108
+ await test(AppConsumer.name, async () => {
109
+ function foo(n: number) {
110
+ return n * n;
111
+ }
112
+
113
+ const workerServer = new SubscriberServer(store, cache, emitter);
114
+ const appWorker = new AppWorker(codec, workerServer, {
115
+ foo: taskFn(foo),
116
+ });
117
+ workerServer.listen(topic, appWorker.handle);
118
+
119
+ const appConsumer = new AppConsumer(codec, workerServer);
120
+ const call = await codec.encodeCall(foo.name, [5]);
121
+
122
+ const done = waitFor(call, async () => {
123
+ strictEqual(await appConsumer.read("foo")(5), 25);
124
+ await rejects(appConsumer.read("foo")(3), NotFoundError);
125
+ await rejects(appConsumer.read("bar")(5), NotFoundError);
126
+ });
127
+
128
+ await appWorker.schedule("foo", topic)(5);
129
+ return done;
130
+ });
131
+
132
+ await test(LocalBrrr.name, async () => {
133
+ const brrr = new LocalBrrr(topic, handlers, codec);
134
+ strictEqual(await brrr.run(foo)(122), 457);
135
+ });
136
+
137
+ await suite("gather", async () => {
138
+ async function callNestedGather(useBrrGather = true): Promise<string[]> {
139
+ const calls: string[] = [];
140
+
141
+ function foo(a: number): number {
142
+ calls.push(`foo(${a})`);
143
+ return a * 2;
144
+ }
145
+
146
+ function bar(a: number): number {
147
+ calls.push(`bar(${a})`);
148
+ return a - 1;
149
+ }
150
+
151
+ async function notBrrrTask(
152
+ app: ActiveWorker,
153
+ a: number,
154
+ ): Promise<number> {
155
+ const b = await app.call(foo)(a);
156
+ return app.call(bar)(b);
157
+ }
158
+
159
+ async function top(app: ActiveWorker, xs: number[]) {
160
+ calls.push(`top(${xs})`);
161
+ if (useBrrGather) {
162
+ return app.gather(...xs.map((x) => notBrrrTask(app, x)));
163
+ }
164
+ return Promise.all(xs.map((x) => notBrrrTask(app, x)));
165
+ }
166
+
167
+ const localBrrr = new LocalBrrr(
168
+ topic,
169
+ {
170
+ foo: taskFn(foo),
171
+ bar: taskFn(bar),
172
+ top,
173
+ },
174
+ codec,
175
+ );
176
+ await localBrrr.run(top)([3, 4]);
177
+ return calls;
178
+ }
179
+
180
+ await test("app gather", async () => {
181
+ const brrrCalls = await callNestedGather();
182
+ strictEqual(brrrCalls.filter((it) => it.startsWith("top")).length, 5);
183
+ const foo3 = brrrCalls.indexOf("foo(3)");
184
+ const foo4 = brrrCalls.indexOf("foo(4)");
185
+ const bar6 = brrrCalls.indexOf("bar(6)");
186
+ const bar8 = brrrCalls.indexOf("bar(8)");
187
+ ok(foo3 < bar6);
188
+ ok(foo3 < bar8);
189
+ ok(foo4 < bar6);
190
+ ok(foo4 < bar8);
191
+ });
192
+
193
+ await test("Promise.all gather", async () => {
194
+ const promises = await callNestedGather(false);
195
+ strictEqual(promises.filter((it) => it.startsWith("top")).length, 5);
196
+ const foo3 = promises.indexOf("foo(3)");
197
+ const foo4 = promises.indexOf("foo(4)");
198
+ const bar6 = promises.indexOf("bar(6)");
199
+ const bar8 = promises.indexOf("bar(8)");
200
+ ok(foo3 < bar6);
201
+ ok(foo4 < bar8);
202
+ });
203
+ });
204
+
205
+ await test("topics separate app same connection", async () => {
206
+ const app1 = new AppWorker(codec, server, {
207
+ one: taskFn(one),
208
+ });
209
+ const app2 = new AppWorker(codec, server, { two });
210
+
211
+ const call = await codec.encodeCall("two", [7]);
212
+
213
+ const done = waitFor(call);
214
+
215
+ server.listen(subtopics.t1, app1.handle);
216
+ server.listen(subtopics.t2, app2.handle);
217
+
218
+ await app2.schedule(two, subtopics.t2)(7);
219
+
220
+ return done;
221
+ });
222
+
223
+ await test("topics separate app separate connection", async () => {
224
+ const server1 = new SubscriberServer(store, cache, emitter);
225
+ const server2 = new SubscriberServer(store, cache, emitter);
226
+ const app1 = new AppWorker(codec, server1, {
227
+ one: taskFn(one),
228
+ });
229
+ const app2 = new AppWorker(codec, server2, { two });
230
+
231
+ server1.listen(subtopics.t1, app1.handle);
232
+ server2.listen(subtopics.t2, app2.handle);
233
+
234
+ const call = await codec.encodeCall("two", [7]);
235
+
236
+ const done = waitFor(call);
237
+
238
+ await app2.schedule(two, subtopics.t2)(7);
239
+ return done;
240
+ });
241
+
242
+ await test("topics same app", async () => {
243
+ const app = new AppWorker(codec, server, {
244
+ one: taskFn(one),
245
+ two,
246
+ });
247
+ server.listen(subtopics.t1, app.handle);
248
+ server.listen(subtopics.t2, app.handle);
249
+ await app.schedule(two, subtopics.t2)(7);
250
+ });
251
+
252
+ await test("stress parallel", async () => {
253
+ async function fib(app: ActiveWorker, n: bigint): Promise<bigint> {
254
+ if (n < 2) {
255
+ return n;
256
+ }
257
+ const [a, b] = await app.gather(
258
+ app.call(fib)(n - 1n),
259
+ app.call(fib)(n - 2n),
260
+ );
261
+ return a + b;
262
+ }
263
+
264
+ async function top(app: ActiveWorker): Promise<void> {
265
+ const n = await app.call(fib)(1000n);
266
+ deepStrictEqual(
267
+ n,
268
+ 43466557686937456435688527675040625802564660517371780402481729089536555417949051890403879840079255169295922593080322634775209689623239873322471161642996440906533187938298969649928516003704476137795166849228875n,
269
+ );
270
+ }
271
+
272
+ const app = new AppWorker(codec, server, { fib, top });
273
+ await app.schedule(top, topic)();
274
+
275
+ await Promise.all(
276
+ new Array(10).keys().map(() => server.listen(topic, app.handle)),
277
+ );
278
+ });
279
+
280
+ await test("weird names", async () => {
281
+ const topic = "//':\"~`\\";
282
+ const taskName = "`'\"\\/~$!@:";
283
+
284
+ function double(x: number): number {
285
+ return x * 2;
286
+ }
287
+
288
+ const app = new AppWorker(codec, server, {
289
+ [taskName]: taskFn(double),
290
+ });
291
+ server.listen(topic, app.handle);
292
+
293
+ const call = await codec.encodeCall(taskName, [7]);
294
+
295
+ await app.schedule(taskName, topic)(7);
296
+
297
+ await waitFor(call);
298
+ strictEqual(await app.read(taskName)(7), 14);
299
+ });
300
+
301
+ await test("debounce child", async () => {
302
+ const calls = new Map<number, number>();
303
+
304
+ async function foo(app: ActiveWorker, a: number): Promise<number> {
305
+ calls.set(a, (calls.get(a) || 0) + 1);
306
+ if (a === 0) {
307
+ return a;
308
+ }
309
+ const results = await app.gather(
310
+ ...Array(50)
311
+ .keys()
312
+ .map(() => app.call(foo)(a - 1)),
313
+ );
314
+ return results.reduce((sum, val) => sum + val);
315
+ }
316
+
317
+ const brrr = new LocalBrrr(topic, { foo }, codec);
318
+ await brrr.run(foo)(3);
319
+
320
+ deepStrictEqual(Object.fromEntries(calls), { 0: 1, 1: 2, 2: 2, 3: 2 });
321
+ });
322
+
323
+ await test("no debounce parent", async () => {
324
+ const calls = new Map<string, number>();
325
+
326
+ function one(_: number): number {
327
+ calls.set("one", (calls.get("one") || 0) + 1);
328
+ return 1;
329
+ }
330
+
331
+ async function foo(app: ActiveWorker, a: number): Promise<number> {
332
+ calls.set("foo", (calls.get("foo") || 0) + 1);
333
+ const results = await app.gather(
334
+ ...new Array(a).keys().map((i) => app.call(one)(i)),
335
+ );
336
+ return results.reduce((sum, val) => sum + val);
337
+ }
338
+
339
+ const brrr = new LocalBrrr(
340
+ topic,
341
+ {
342
+ one: taskFn(one),
343
+ foo,
344
+ },
345
+ codec,
346
+ );
347
+ await brrr.run(foo)(50);
348
+
349
+ deepStrictEqual(Object.fromEntries(calls), { one: 50, foo: 51 });
350
+ });
351
+
352
+ await test("app handler names", async () => {
353
+ function foo(a: number): number {
354
+ return a * a;
355
+ }
356
+
357
+ async function bar(app: ActiveWorker, a: number): Promise<number> {
358
+ return (
359
+ (await app.call(foo)(a)) *
360
+ (await app.call<[number], number>("quux/zim")(a))
361
+ );
362
+ }
363
+
364
+ const worker = new AppWorker(codec, server, {
365
+ "quux/zim": taskFn(foo),
366
+ "quux/bar": bar,
367
+ });
368
+ const localApp = new LocalApp(topic, server, worker);
369
+ localApp.run();
370
+
371
+ const call = await codec.encodeCall("quux/bar", [4]);
372
+ const done = waitFor(call, async () => {
373
+ strictEqual(await localApp.read("quux/zim")(4), 16);
374
+ strictEqual(await localApp.read(foo)(4), 16);
375
+ });
376
+
377
+ await localApp.schedule("quux/bar")(4);
378
+ return done;
379
+ });
380
+
381
+ await suite("loop mode", async () => {
382
+ let queues: Record<string, (string | typeof BrrrShutdownSymbol)[]>;
383
+ let server: Server;
384
+
385
+ const publisher: Publisher = {
386
+ async emit(topic: string, callId: string | Call): Promise<void> {
387
+ queues[topic]?.push(callId as string);
388
+ },
389
+ };
390
+
391
+ async function flusher() {
392
+ const item = queues[topic]?.shift();
393
+ if (!item) {
394
+ return BrrrShutdownSymbol;
395
+ }
396
+ return item;
397
+ }
398
+
399
+ beforeEach(() => {
400
+ queues = {
401
+ [topic]: [],
402
+ };
403
+ server = new Server(store, cache, publisher);
404
+ });
405
+
406
+ await test("basic loop", async () => {
407
+ async function foo(app: ActiveWorker, a: number) {
408
+ return (await app.call(bar, topic)(a + 1)) + 1;
409
+ }
410
+
411
+ const server = new Server(store, cache, publisher);
412
+ const app = new AppWorker(codec, server, { ...handlers, foo });
413
+
414
+ await app.schedule(foo, topic)(122);
415
+
416
+ await server.loop(topic, app.handle, flusher);
417
+
418
+ strictEqual(await app.read(foo)(122), 457);
419
+ });
420
+
421
+ await test("loop with no tasks", async () => {
422
+ const app = new AppWorker(codec, server, handlers);
423
+
424
+ let looped = false;
425
+ await server.loop(topic, app.handle, async () => {
426
+ if (looped) {
427
+ return BrrrShutdownSymbol;
428
+ }
429
+ looped = true;
430
+ return undefined;
431
+ });
432
+
433
+ ok(looped);
434
+ });
435
+
436
+ await test("resumable loop", async () => {
437
+ class MyError extends Error {}
438
+
439
+ let errors = 5;
440
+
441
+ async function foo(a: number): Promise<number> {
442
+ if (errors) {
443
+ errors--;
444
+ throw new MyError();
445
+ }
446
+ queues[topic]?.push(BrrrShutdownSymbol);
447
+ return a;
448
+ }
449
+
450
+ const app = new AppWorker(codec, server, {
451
+ ...handlers,
452
+ foo: taskFn(foo),
453
+ });
454
+
455
+ while (true) {
456
+ try {
457
+ await app.schedule(foo, topic)(3);
458
+ await server.loop(topic, app.handle, async () => {
459
+ return queues[topic]?.pop();
460
+ });
461
+ break;
462
+ } catch (err) {
463
+ if (err instanceof MyError) {
464
+ continue;
465
+ }
466
+ throw err;
467
+ }
468
+ }
469
+ strictEqual(errors, 0);
470
+ });
471
+
472
+ await test("resumable loop nested", async () => {
473
+ class MyError extends Error {}
474
+
475
+ let errors = 5;
476
+
477
+ function bar(a: number): number {
478
+ if (errors) {
479
+ errors--;
480
+ throw new MyError();
481
+ }
482
+ return a;
483
+ }
484
+
485
+ async function foo(app: ActiveWorker, a: number): Promise<number> {
486
+ return app.call(bar)(a);
487
+ }
488
+
489
+ const app = new AppWorker(codec, server, {
490
+ ...handlers,
491
+ foo,
492
+ bar: taskFn(bar),
493
+ });
494
+
495
+ while (true) {
496
+ try {
497
+ await app.schedule(foo, topic)(3);
498
+ await server.loop(topic, app.handle, flusher);
499
+ break;
500
+ } catch (err) {
501
+ if (err instanceof MyError) {
502
+ continue;
503
+ }
504
+ throw err;
505
+ }
506
+ }
507
+ strictEqual(errors, 0);
508
+ });
509
+
510
+ await test("app subclass", async () => {
511
+ function bar(a: number): number {
512
+ return a + 1;
513
+ }
514
+
515
+ function baz(a: number) {
516
+ return a + 10;
517
+ }
518
+
519
+ async function foo(app: ActiveWorker, a: number): Promise<number> {
520
+ return app.call(bar)(a);
521
+ }
522
+
523
+ class MyAppWorker extends AppWorker {
524
+ public readonly myHandle = async (
525
+ request: Request,
526
+ connection: Connection,
527
+ ): Promise<Response | Defer> => {
528
+ const response = await this.handle(request, connection);
529
+ if (response instanceof Defer) {
530
+ for (const deferredCall of response.calls) {
531
+ Object.defineProperty(deferredCall.call, "taskName", {
532
+ value: "baz",
533
+ });
534
+ }
535
+ return new Defer(...response.calls);
536
+ }
537
+ return response;
538
+ };
539
+ }
540
+
541
+ const app = new MyAppWorker(codec, server, {
542
+ foo,
543
+ bar: taskFn(bar),
544
+ baz: taskFn(baz),
545
+ });
546
+ await app.schedule(foo, topic)(4);
547
+ await server.loop(topic, app.myHandle, flusher);
548
+ strictEqual(await app.read(foo)(4), 14);
549
+ });
550
+
551
+ await suite("spawn limit", async () => {
552
+ await test("spawn limit depth", async () => {
553
+ let n = 0;
554
+
555
+ async function foo(app: ActiveWorker, a: number): Promise<number> {
556
+ n++;
557
+ if (a === 0) {
558
+ return 0;
559
+ }
560
+ return app.call(foo)(a - 1);
561
+ }
562
+
563
+ const server = new Server(store, cache, publisher);
564
+ // override for test
565
+ Object.defineProperty(server, "spawnLimit", {
566
+ value: 100,
567
+ });
568
+
569
+ const app = new AppWorker(codec, server, { foo });
570
+ await app.schedule(foo, topic)(server.spawnLimit + 3);
571
+
572
+ await rejects(server.loop(topic, app.handle, flusher));
573
+ strictEqual(n, server.spawnLimit);
574
+ });
575
+
576
+ await test("spawn limit recoverable", async () => {
577
+ function one(_: number): number {
578
+ return 1;
579
+ }
580
+
581
+ async function foo(app: ActiveWorker, a: number): Promise<number> {
582
+ const results = await app.gather(
583
+ ...new Array(a).keys().map((i) => app.call(one)(i)),
584
+ );
585
+ return results.reduce((sum, val) => sum + val);
586
+ }
587
+
588
+ const server = new Server(store, cache, publisher);
589
+ // override for test
590
+ Object.defineProperty(server, "spawnLimit", {
591
+ value: 100,
592
+ });
593
+ const n = server.spawnLimit + 1;
594
+ let spawnLimitEncountered = false;
595
+ const app = new AppWorker(codec, server, { one: taskFn(one), foo });
596
+ while (true) {
597
+ // reset cache
598
+ Object.defineProperty(cache, "cache", {
599
+ value: new Map(),
600
+ });
601
+ try {
602
+ await app.schedule(foo, topic)(n);
603
+ await server.loop(topic, app.handle, flusher);
604
+ break;
605
+ } catch (err) {
606
+ if (err instanceof SpawnLimitError) {
607
+ spawnLimitEncountered = true;
608
+ continue;
609
+ }
610
+ throw err;
611
+ }
612
+ }
613
+ ok(spawnLimitEncountered);
614
+ strictEqual(await app.read(foo)(n), n);
615
+ });
616
+
617
+ await test("spawn limit breadth mapped", async () => {
618
+ const calls = new Map<string, number>();
619
+
620
+ function one(_: number): number {
621
+ calls.set("one", (calls.get("one") || 0) + 1);
622
+ return 1;
623
+ }
624
+
625
+ async function foo(app: ActiveWorker, a: number): Promise<number> {
626
+ calls.set("foo", (calls.get("foo") || 0) + 1);
627
+ const results = await app.gather(
628
+ ...new Array(a).keys().map((i) => app.call(one)(i)),
629
+ );
630
+ return results.reduce((sum, val) => sum + val);
631
+ }
632
+
633
+ const app = new AppWorker(codec, server, { one: taskFn(one), foo });
634
+ await app.schedule(foo, topic)(server.spawnLimit + 4);
635
+
636
+ await rejects(server.loop(topic, app.handle, flusher), SpawnLimitError);
637
+ strictEqual(calls.get(foo.name), 1);
638
+ });
639
+
640
+ await test("spawn limit breadth manual", async () => {
641
+ const calls = new Map<string, number>();
642
+
643
+ function one(_: number): number {
644
+ calls.set("one", (calls.get("one") || 0) + 1);
645
+ return 1;
646
+ }
647
+
648
+ async function foo(app: ActiveWorker, a: number): Promise<number> {
649
+ calls.set("foo", (calls.get("foo") || 0) + 1);
650
+ let total = 0;
651
+ for (let i = 0; i < a; i++) {
652
+ total += await app.call(one)(i);
653
+ }
654
+ return total;
655
+ }
656
+
657
+ // override for test
658
+ const server = new Server(store, cache, publisher);
659
+ Object.defineProperty(server, "spawnLimit", {
660
+ value: 100,
661
+ });
662
+
663
+ const app = new AppWorker(codec, server, { foo, one: taskFn(one) });
664
+ await app.schedule(foo, topic)(server.spawnLimit + 3);
665
+
666
+ await rejects(server.loop(topic, app.handle, flusher));
667
+ deepStrictEqual(Object.fromEntries(calls), {
668
+ one: server.spawnLimit / 2,
669
+ foo: server.spawnLimit / 2,
670
+ });
671
+ });
672
+
673
+ await test("spawn limit cached", async () => {
674
+ let n = 0;
675
+ let final = undefined;
676
+
677
+ function same(a: number): number {
678
+ n++;
679
+ return a;
680
+ }
681
+
682
+ async function foo(app: ActiveWorker, a: number): Promise<number> {
683
+ const results = await app.gather(
684
+ ...new Array(a).fill(1).map((i) => app.call(same)(i)),
685
+ );
686
+ const val = results.reduce((sum, val) => sum + val);
687
+ final = val;
688
+ return val;
689
+ }
690
+
691
+ const server = new Server(store, cache, publisher);
692
+ Object.defineProperty(server, "spawnLimit", {
693
+ value: 100,
694
+ });
695
+
696
+ const app = new AppWorker(codec, server, { foo, same: taskFn(same) });
697
+ await app.schedule(foo, topic)(server.spawnLimit + 5);
698
+
699
+ await server.loop(topic, app.handle, flusher);
700
+ strictEqual(n, 1);
701
+ strictEqual(final, server.spawnLimit + 5);
702
+ });
703
+ });
704
+ });
705
+ });