bunja 2.1.1 → 3.0.0-alpha.4

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/test.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { assertEquals } from "@std/assert";
1
+ import { assertEquals, assertThrows } from "@std/assert";
2
2
  import { assertSpyCalls, spy } from "@std/testing/mock";
3
3
 
4
4
  import { bunja, createBunjaStore, createScope } from "./bunja.ts";
5
+ import type { Bunja, BunjaRef } from "./bunja.ts";
5
6
 
6
7
  const readNull = <T>() => (null as T);
7
8
 
@@ -207,7 +208,33 @@ Deno.test({
207
208
  });
208
209
 
209
210
  Deno.test({
210
- name: "fork",
211
+ name: "root scope value pairs are included in store.get deps",
212
+ fn() {
213
+ const store = createBunjaStore();
214
+ const myScope = createScope<string>();
215
+ const myBunja = bunja(() => {
216
+ const scopeValue = bunja.use(myScope);
217
+ return { scopeValue };
218
+ });
219
+
220
+ const first = store.get(
221
+ { bunja: myBunja, with: [myScope.bind("foo")] },
222
+ readNull,
223
+ );
224
+ const second = store.get(
225
+ { bunja: myBunja, with: [myScope.bind("bar")] },
226
+ readNull,
227
+ );
228
+
229
+ assertEquals(first.value.scopeValue, "foo");
230
+ assertEquals(second.value.scopeValue, "bar");
231
+ assertEquals(first.deps, ["foo"]);
232
+ assertEquals(second.deps, ["bar"]);
233
+ },
234
+ });
235
+
236
+ Deno.test({
237
+ name: "bunja.use can override scope value pairs inside a bunja init",
211
238
  fn() {
212
239
  const store = createBunjaStore();
213
240
  const aaScope = createScope<string>();
@@ -227,8 +254,8 @@ Deno.test({
227
254
  return { a, scopeValue };
228
255
  });
229
256
  const cBunja = bunja(() => {
230
- const foo = bunja.fork(bBunja, [aaScope.bind("foo")]);
231
- const bar = bunja.fork(bBunja, [aaScope.bind("bar")]);
257
+ const foo = bunja.use(bBunja, [aaScope.bind("foo")]);
258
+ const bar = bunja.use(bBunja, [aaScope.bind("bar")]);
232
259
  bunja.effect(() => (cMountSpy(), cUnmountSpy));
233
260
  return { foo, bar };
234
261
  });
@@ -270,3 +297,505 @@ Deno.test({
270
297
  assertSpyCalls(cUnmountSpy, 1);
271
298
  },
272
299
  });
300
+
301
+ Deno.test({
302
+ name: "bunja.use with scope value pairs propagates unbound required scopes",
303
+ fn() {
304
+ const createGraph = () => {
305
+ const injectedScope = createScope<string>();
306
+ const outerScope = createScope<string>();
307
+ const dependencyBunja = bunja(() => {
308
+ const injected = bunja.use(injectedScope);
309
+ const outer = bunja.use(outerScope);
310
+ return { injected, outer };
311
+ });
312
+ const consumerBunja = bunja(() =>
313
+ bunja.use(dependencyBunja, [injectedScope.bind("injected")])
314
+ );
315
+ return { consumerBunja, injectedScope, outerScope };
316
+ };
317
+ const readScope = <T>() => "outer" as T;
318
+
319
+ const prebakeGraph = createGraph();
320
+ const { requiredScopes } = createBunjaStore().prebake(
321
+ prebakeGraph.consumerBunja,
322
+ readScope,
323
+ );
324
+ assertEquals(requiredScopes.length, 1);
325
+ assertEquals(requiredScopes[0] === prebakeGraph.outerScope, true);
326
+ assertEquals(
327
+ requiredScopes.some((scope) => scope === prebakeGraph.injectedScope),
328
+ false,
329
+ );
330
+
331
+ const getGraph = createGraph();
332
+ const { value, deps } = createBunjaStore().get(
333
+ getGraph.consumerBunja,
334
+ readScope,
335
+ );
336
+ const requiredScopes2 = getGraph.consumerBunja.requiredScopes;
337
+ assertEquals(value, { injected: "injected", outer: "outer" });
338
+ assertEquals(deps, ["outer"]);
339
+ assertEquals(requiredScopes2.length, 1);
340
+ assertEquals(
341
+ requiredScopes2[0] === getGraph.outerScope,
342
+ true,
343
+ );
344
+ assertEquals(
345
+ requiredScopes2.some((scope) => scope === getGraph.injectedScope),
346
+ false,
347
+ );
348
+ },
349
+ });
350
+
351
+ Deno.test({
352
+ name: "seed is used only when creating a bunja instance",
353
+ fn() {
354
+ const store = createBunjaStore();
355
+ const myBunja = bunja.withSeed({ value: "default" }, (seed) => ({ seed }));
356
+
357
+ const { value: firstValue, mount: firstMount } = store.get(
358
+ { bunja: myBunja, seed: { value: "first" } },
359
+ readNull,
360
+ );
361
+ const firstCleanup = firstMount();
362
+
363
+ const { value: secondValue, mount: secondMount } = store.get(
364
+ { bunja: myBunja, seed: { value: "second" } },
365
+ readNull,
366
+ );
367
+ const secondCleanup = secondMount();
368
+
369
+ assertEquals(firstValue.seed.value, "first");
370
+ assertEquals(secondValue, firstValue);
371
+
372
+ firstCleanup();
373
+ secondCleanup();
374
+
375
+ const { value: thirdValue, mount: thirdMount } = store.get(
376
+ { bunja: myBunja, seed: { value: "third" } },
377
+ readNull,
378
+ );
379
+ const thirdCleanup = thirdMount();
380
+
381
+ assertEquals(thirdValue.seed.value, "third");
382
+ thirdCleanup();
383
+ },
384
+ });
385
+
386
+ Deno.test({
387
+ name: "bunja.use can provide seed without storing it in graph refs",
388
+ fn() {
389
+ const store = createBunjaStore();
390
+ const seededDependencyBunja = bunja.withSeed(
391
+ { value: "default" },
392
+ (seed) => seed.value,
393
+ );
394
+ const seededDependencyRef: BunjaRef<string, { value: string }> = {
395
+ bunja: seededDependencyBunja,
396
+ seed: { value: "dependency" },
397
+ };
398
+ const consumerBunja = bunja(() => {
399
+ return bunja.use(seededDependencyRef);
400
+ });
401
+
402
+ const { value, mount } = store.get(consumerBunja, readNull);
403
+ const cleanup = mount();
404
+
405
+ assertEquals(value, "dependency");
406
+ assertEquals(consumerBunja.requiredBunjaRefs.length, 1);
407
+ assertEquals("seed" in consumerBunja.requiredBunjaRefs[0], false);
408
+
409
+ cleanup();
410
+ },
411
+ });
412
+
413
+ Deno.test({
414
+ name: "prebake rejects root seed and initializes with default seed",
415
+ fn() {
416
+ const store = createBunjaStore();
417
+ const usedSeeds: string[] = [];
418
+ const myBunja = bunja.withSeed({ value: "default" }, (seed) => {
419
+ usedSeeds.push(seed.value);
420
+ return seed.value;
421
+ });
422
+ const myRef: BunjaRef<string, { value: string }> = {
423
+ bunja: myBunja,
424
+ seed: { value: "custom" },
425
+ };
426
+ const consumerBunja = bunja(() => bunja.use(myRef));
427
+
428
+ store.prebake(myBunja, readNull);
429
+ store.prebake(consumerBunja, readNull);
430
+
431
+ assertEquals(usedSeeds, ["default", "default"]);
432
+ assertEquals("seed" in consumerBunja.requiredBunjaRefs[0], false);
433
+ assertThrows(
434
+ () => {
435
+ // @ts-expect-error Prebake collects deterministic graph data only.
436
+ store.prebake(myRef, readNull);
437
+ },
438
+ Error,
439
+ "seed cannot be provided to `store.prebake`",
440
+ );
441
+ },
442
+ });
443
+
444
+ Deno.test({
445
+ name: "bunja.will mounts only the selected dependency",
446
+ fn() {
447
+ const store = createBunjaStore();
448
+ const condScope = createScope<"a" | "b">();
449
+ const [aMountSpy, aUnmountSpy] = [spy(), spy()];
450
+ const [bMountSpy, bUnmountSpy] = [spy(), spy()];
451
+ const aBunja = bunja(() => {
452
+ bunja.effect(() => (aMountSpy(), aUnmountSpy));
453
+ return "a";
454
+ });
455
+ const bBunja = bunja(() => {
456
+ bunja.effect(() => (bMountSpy(), bUnmountSpy));
457
+ return "b";
458
+ });
459
+ const consumerBunja = bunja(() => {
460
+ const cond = bunja.use(condScope);
461
+ const getA = bunja.will(aBunja);
462
+ const getB = bunja.will(bBunja);
463
+ return cond === "a" ? getA() : getB();
464
+ });
465
+
466
+ const { value, mount } = store.get(
467
+ consumerBunja,
468
+ <T>() => "a" as T,
469
+ );
470
+ assertEquals(value, "a");
471
+ assertEquals(consumerBunja.relatedBunjas, [aBunja, bBunja]);
472
+
473
+ const cleanup = mount();
474
+ assertSpyCalls(aMountSpy, 1);
475
+ assertSpyCalls(bMountSpy, 0);
476
+
477
+ cleanup();
478
+ assertSpyCalls(aUnmountSpy, 1);
479
+ assertSpyCalls(bUnmountSpy, 0);
480
+ },
481
+ });
482
+
483
+ Deno.test({
484
+ name: "bunja.will records optional refs once when thunk is used",
485
+ fn() {
486
+ const createConsumer = () => {
487
+ const dependencyBunja = bunja(() => "dependency");
488
+ const consumerBunja = bunja(() => {
489
+ const getDependency = bunja.will(dependencyBunja);
490
+ return getDependency();
491
+ });
492
+ return { consumerBunja };
493
+ };
494
+
495
+ const getGraph = createConsumer();
496
+ createBunjaStore().get(getGraph.consumerBunja, readNull);
497
+
498
+ const prebakeGraph = createConsumer();
499
+ createBunjaStore().prebake(prebakeGraph.consumerBunja, readNull);
500
+
501
+ assertEquals(getGraph.consumerBunja.optionalBunjaRefs.length, 1);
502
+ assertEquals(prebakeGraph.consumerBunja.optionalBunjaRefs.length, 1);
503
+ },
504
+ });
505
+
506
+ Deno.test({
507
+ name: "relatedBunjas includes nested bunja.will dependencies",
508
+ fn() {
509
+ const store = createBunjaStore();
510
+ const grandparentDependencyBunja = bunja(() => "grandparent");
511
+ const parentDependencyBunja = bunja(() => {
512
+ bunja.will(grandparentDependencyBunja);
513
+ return "parent";
514
+ });
515
+ const consumerBunja = bunja(() => {
516
+ const getParent = bunja.will(parentDependencyBunja);
517
+ return getParent();
518
+ });
519
+
520
+ const { mount } = store.get(consumerBunja, readNull);
521
+ const cleanup = mount();
522
+
523
+ assertEquals(consumerBunja.relatedBunjas, [
524
+ grandparentDependencyBunja,
525
+ parentDependencyBunja,
526
+ ]);
527
+
528
+ cleanup();
529
+ },
530
+ });
531
+
532
+ Deno.test({
533
+ name: "prebake expands inactive bunja.will dependency graph",
534
+ fn() {
535
+ const store = createBunjaStore();
536
+ const grandparentDependencyMountSpy = spy();
537
+ const grandparentDependencyBunja = bunja(() => {
538
+ bunja.effect(() => {
539
+ grandparentDependencyMountSpy();
540
+ });
541
+ return "grandparent";
542
+ });
543
+ const parentDependencyBunja = bunja(() => {
544
+ bunja.will(grandparentDependencyBunja);
545
+ return "parent";
546
+ });
547
+ const consumerBunja = bunja(() => {
548
+ bunja.will(parentDependencyBunja);
549
+ return "consumer";
550
+ });
551
+
552
+ const { value, mount } = store.get(consumerBunja, readNull);
553
+ const cleanup = mount();
554
+
555
+ assertEquals(value, "consumer");
556
+ assertEquals(consumerBunja.relatedBunjas, [parentDependencyBunja]);
557
+ assertSpyCalls(grandparentDependencyMountSpy, 0);
558
+
559
+ const prebaked = store.prebake(consumerBunja, readNull);
560
+
561
+ assertEquals(prebaked.relatedBunjas, [
562
+ grandparentDependencyBunja,
563
+ parentDependencyBunja,
564
+ ]);
565
+ assertEquals(consumerBunja.relatedBunjas, [
566
+ grandparentDependencyBunja,
567
+ parentDependencyBunja,
568
+ ]);
569
+ assertSpyCalls(grandparentDependencyMountSpy, 0);
570
+
571
+ cleanup();
572
+ },
573
+ });
574
+
575
+ Deno.test({
576
+ name: "prebake runs each dependency once per traversal",
577
+ fn() {
578
+ const store = createBunjaStore();
579
+ const requiredInitSpy = spy();
580
+ const optionalInitSpy = spy();
581
+ const requiredDependencyBunja = bunja(() => {
582
+ requiredInitSpy();
583
+ return "required";
584
+ });
585
+ const optionalDependencyBunja = bunja(() => {
586
+ optionalInitSpy();
587
+ return "optional";
588
+ });
589
+ const consumerBunja = bunja(() => {
590
+ const required = bunja.use(requiredDependencyBunja);
591
+ const getOptional = bunja.will(optionalDependencyBunja);
592
+ return `${required}:${getOptional()}`;
593
+ });
594
+
595
+ store.prebake(consumerBunja, readNull);
596
+
597
+ assertSpyCalls(requiredInitSpy, 1);
598
+ assertSpyCalls(optionalInitSpy, 1);
599
+
600
+ store.prebake(consumerBunja, readNull);
601
+
602
+ assertSpyCalls(requiredInitSpy, 2);
603
+ assertSpyCalls(optionalInitSpy, 2);
604
+ },
605
+ });
606
+
607
+ Deno.test({
608
+ name: "prebake disposes the dry-run wrapper",
609
+ fn() {
610
+ const disposeSpy = spy();
611
+ let wrapCalls = 0;
612
+ const store = createBunjaStore({
613
+ wrapInstance: (fn) => {
614
+ wrapCalls++;
615
+ return fn(disposeSpy);
616
+ },
617
+ });
618
+ const myBunja = bunja(() => "value");
619
+
620
+ store.prebake(myBunja, readNull);
621
+
622
+ assertEquals(wrapCalls, 1);
623
+ assertSpyCalls(disposeSpy, 1);
624
+ },
625
+ });
626
+
627
+ Deno.test({
628
+ name: "bunja.will thunk can only be called during the same bunja init",
629
+ fn() {
630
+ const store = createBunjaStore();
631
+ const parentDependencyBunja = bunja(() => "parent");
632
+ let getParent!: () => string;
633
+ const consumerBunja = bunja(() => {
634
+ getParent = bunja.will(parentDependencyBunja);
635
+ return {};
636
+ });
637
+
638
+ const { mount } = store.get(consumerBunja, readNull);
639
+ const cleanup = mount();
640
+
641
+ assertThrows(
642
+ () => getParent(),
643
+ Error,
644
+ "same bunja init function",
645
+ );
646
+
647
+ cleanup();
648
+ },
649
+ });
650
+
651
+ Deno.test({
652
+ name: "store.get rejects circular bunja dependencies",
653
+ fn() {
654
+ const disposeSpy = spy();
655
+ const store = createBunjaStore({
656
+ wrapInstance: (fn) => fn(disposeSpy),
657
+ });
658
+ let bBunja!: Bunja<string>;
659
+ const aBunja = bunja(() => bunja.use(bBunja));
660
+ bBunja = bunja(() => bunja.use(aBunja));
661
+
662
+ assertThrows(
663
+ () => store.get(aBunja, readNull),
664
+ Error,
665
+ "Circular bunja dependency detected.",
666
+ );
667
+ assertSpyCalls(disposeSpy, 2);
668
+ },
669
+ });
670
+
671
+ Deno.test({
672
+ name: "prebake rejects circular bunja dependencies",
673
+ fn() {
674
+ const store = createBunjaStore();
675
+ let bBunja!: Bunja<string>;
676
+ const aBunja = bunja(() => bunja.use(bBunja));
677
+ bBunja = bunja(() => bunja.use(aBunja));
678
+
679
+ assertThrows(
680
+ () => store.prebake(aBunja, readNull),
681
+ Error,
682
+ "Circular bunja dependency detected.",
683
+ );
684
+ },
685
+ });
686
+
687
+ Deno.test({
688
+ name: "active optional dependency scopes are part of bunja instance identity",
689
+ fn() {
690
+ const store = createBunjaStore();
691
+ const condScope = createScope<boolean>();
692
+ const resourceScope = createScope<string>();
693
+ const mounted: string[] = [];
694
+ const unmounted: string[] = [];
695
+ const parentDependencyBunja = bunja(() => {
696
+ const resource = bunja.use(resourceScope);
697
+ bunja.effect(() => {
698
+ mounted.push(resource);
699
+ return () => unmounted.push(resource);
700
+ });
701
+ return { resource };
702
+ });
703
+ const consumerBunja = bunja(() => {
704
+ const cond = bunja.use(condScope);
705
+ const getParent = bunja.will(parentDependencyBunja);
706
+ return { parent: cond ? getParent() : null };
707
+ });
708
+ const readScope = (resource: string) => <T>(scope: unknown) =>
709
+ (scope === condScope ? true : resource) as T;
710
+
711
+ const first = store.get(consumerBunja, readScope("a"));
712
+ const firstCleanup = first.mount();
713
+ const second = store.get(consumerBunja, readScope("b"));
714
+ const secondCleanup = second.mount();
715
+
716
+ assertEquals(consumerBunja.requiredScopes.length, 1);
717
+ assertEquals(consumerBunja.requiredScopes[0] === condScope, true);
718
+ assertEquals(first.value.parent?.resource, "a");
719
+ assertEquals(second.value.parent?.resource, "b");
720
+ assertEquals(first.value === second.value, false);
721
+ assertEquals(first.deps, [true, "a"]);
722
+ assertEquals(second.deps, [true, "b"]);
723
+ assertEquals(mounted, ["a", "b"]);
724
+
725
+ firstCleanup();
726
+ assertEquals(unmounted, ["a"]);
727
+ secondCleanup();
728
+ assertEquals(unmounted, ["a", "b"]);
729
+ },
730
+ });
731
+
732
+ Deno.test({
733
+ name:
734
+ "inactive optional dependency scopes do not change bunja instance identity",
735
+ fn() {
736
+ const store = createBunjaStore();
737
+ const condScope = createScope<boolean>();
738
+ const resourceScope = createScope<string>();
739
+ const parentDependencyMountSpy = spy();
740
+ const parentDependencyBunja = bunja(() => {
741
+ bunja.use(resourceScope);
742
+ bunja.effect(() => {
743
+ parentDependencyMountSpy();
744
+ });
745
+ return {};
746
+ });
747
+ const consumerBunja = bunja(() => {
748
+ const cond = bunja.use(condScope);
749
+ const getParent = bunja.will(parentDependencyBunja);
750
+ return { parent: cond ? getParent() : null };
751
+ });
752
+ const readScope = (resource: string) => <T>(scope: unknown) =>
753
+ (scope === condScope ? false : resource) as T;
754
+
755
+ const first = store.get(consumerBunja, readScope("a"));
756
+ const second = store.get(consumerBunja, readScope("b"));
757
+ const firstCleanup = first.mount();
758
+ const secondCleanup = second.mount();
759
+
760
+ assertEquals(consumerBunja.requiredScopes.length, 1);
761
+ assertEquals(consumerBunja.requiredScopes[0] === condScope, true);
762
+ assertEquals(first.value, second.value);
763
+ assertEquals(first.deps, [false]);
764
+ assertEquals(second.deps, [false]);
765
+ assertSpyCalls(parentDependencyMountSpy, 0);
766
+
767
+ firstCleanup();
768
+ secondCleanup();
769
+ },
770
+ });
771
+
772
+ Deno.test({
773
+ name: "duplicate optional dependency calls are mounted once",
774
+ fn() {
775
+ const store = createBunjaStore();
776
+ const mountSpy = spy();
777
+ const parentDependencyValue = {};
778
+ const parentDependencyBunja = bunja(() => {
779
+ bunja.effect(() => {
780
+ mountSpy();
781
+ });
782
+ return parentDependencyValue;
783
+ });
784
+ const consumerBunja = bunja(() => {
785
+ const getParent = bunja.will(parentDependencyBunja);
786
+ return {
787
+ first: getParent(),
788
+ second: getParent(),
789
+ };
790
+ });
791
+
792
+ const { value, mount } = store.get(consumerBunja, readNull);
793
+ const cleanup = mount();
794
+
795
+ assertEquals(value.first, parentDependencyValue);
796
+ assertEquals(value.second, parentDependencyValue);
797
+ assertSpyCalls(mountSpy, 1);
798
+
799
+ cleanup();
800
+ },
801
+ });