@milaboratories/pl-tree 1.15.7 → 1.16.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/accessors.cjs +6 -2
- package/dist/accessors.cjs.map +1 -1
- package/dist/accessors.d.ts.map +1 -1
- package/dist/accessors.js +6 -2
- package/dist/accessors.js.map +1 -1
- package/dist/delta_sync.cjs +18 -8
- package/dist/delta_sync.cjs.map +1 -1
- package/dist/delta_sync.js +18 -8
- package/dist/delta_sync.js.map +1 -1
- package/dist/state.cjs +108 -30
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.ts +27 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +108 -30
- package/dist/state.js.map +1 -1
- package/dist/sync.cjs +2 -1
- package/dist/sync.cjs.map +1 -1
- package/dist/sync.d.ts +2 -0
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +2 -1
- package/dist/sync.js.map +1 -1
- package/dist/synchronized_tree.cjs +46 -9
- package/dist/synchronized_tree.cjs.map +1 -1
- package/dist/synchronized_tree.d.ts +6 -0
- package/dist/synchronized_tree.d.ts.map +1 -1
- package/dist/synchronized_tree.js +47 -10
- package/dist/synchronized_tree.js.map +1 -1
- package/package.json +4 -4
- package/src/accessors.test.ts +108 -0
- package/src/accessors.ts +8 -3
- package/src/delta_sync.test.ts +88 -3
- package/src/delta_sync.ts +19 -8
- package/src/state.test.ts +397 -2
- package/src/state.ts +137 -46
- package/src/state_notifications.model.test.ts +264 -0
- package/src/sync.ts +6 -3
- package/src/synchronized_tree.test.ts +550 -1
- package/src/synchronized_tree.ts +102 -20
package/src/state.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expect, test } from "vitest";
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
2
|
import { Computable } from "@milaboratories/computable";
|
|
3
3
|
import type { Watcher } from "@milaboratories/computable";
|
|
4
4
|
import type { FieldData } from "@milaboratories/pl-client";
|
|
@@ -8,10 +8,14 @@ import {
|
|
|
8
8
|
NullSignedResourceId,
|
|
9
9
|
} from "@milaboratories/pl-client";
|
|
10
10
|
import { isPlTreeEntry, isPlTreeEntryAccessor, isPlTreeNodeAccessor } from "./accessors";
|
|
11
|
-
import {
|
|
11
|
+
import type { ExtendedResourceData } from "./state";
|
|
12
|
+
import { PlTreeState, TreeStateUpdateError } from "./state";
|
|
13
|
+
import { constructTreeLoadingRequest, initialTreeLoadingStat } from "./sync";
|
|
12
14
|
import {
|
|
13
15
|
dField,
|
|
16
|
+
field,
|
|
14
17
|
iField,
|
|
18
|
+
InitialStructuralResourceState,
|
|
15
19
|
ResourceReady,
|
|
16
20
|
TestDynamicRootId1,
|
|
17
21
|
TestDynamicRootState1,
|
|
@@ -474,3 +478,394 @@ test("removal of a typed field still throws after a reorder", () => {
|
|
|
474
478
|
/removal of Input field b/,
|
|
475
479
|
);
|
|
476
480
|
});
|
|
481
|
+
|
|
482
|
+
const R1 = rid(10n);
|
|
483
|
+
const V1 = rid(20n);
|
|
484
|
+
|
|
485
|
+
function res(
|
|
486
|
+
id: ReturnType<typeof rid>,
|
|
487
|
+
typeName: string,
|
|
488
|
+
patch: Partial<ExtendedResourceData> = {},
|
|
489
|
+
fields: FieldData[] = [],
|
|
490
|
+
): ExtendedResourceData {
|
|
491
|
+
return {
|
|
492
|
+
...InitialStructuralResourceState,
|
|
493
|
+
id,
|
|
494
|
+
type: { name: typeName, version: "1" },
|
|
495
|
+
fields,
|
|
496
|
+
...patch,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** A tree whose root holds R1 through a dynamic field, R1 built from `r1`. */
|
|
501
|
+
function treeWith(r1: ExtendedResourceData, ...others: ExtendedResourceData[]): PlTreeState {
|
|
502
|
+
const t = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
503
|
+
t.updateFromResourceData([
|
|
504
|
+
{ ...TestDynamicRootState1, fields: [dField("a", R1)] },
|
|
505
|
+
...others,
|
|
506
|
+
r1,
|
|
507
|
+
]);
|
|
508
|
+
return t;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
describe("becoming final notifies the readers of every source it retires", () => {
|
|
512
|
+
test("a missing-field reader is re-run when the resource becomes final", () => {
|
|
513
|
+
const t = treeWith(res(R1, "StdMap", { inputsLocked: true }));
|
|
514
|
+
const reader = w();
|
|
515
|
+
const unstable: string[] = [];
|
|
516
|
+
expect(t.get(w(), R1).getField(reader, "x", (m) => unstable.push(m))).toBeUndefined();
|
|
517
|
+
expect(unstable).toEqual(["field_not_found:x"]);
|
|
518
|
+
|
|
519
|
+
// ready makes a StdMap final; the field list itself does not change
|
|
520
|
+
t.updateFromResourceData([res(R1, "StdMap", { inputsLocked: true, resourceReady: true })]);
|
|
521
|
+
expect(t.get(w(), R1).finalState).toBe(true);
|
|
522
|
+
expect(reader.isChanged).toBe(true);
|
|
523
|
+
|
|
524
|
+
// and the re-run reads the absent field as stable
|
|
525
|
+
const rerun: string[] = [];
|
|
526
|
+
expect(t.get(w(), R1).getField(w(), "x", (m) => rerun.push(m))).toBeUndefined();
|
|
527
|
+
expect(rerun).toEqual([]);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("a KV reader of an absent key is re-run when the resource becomes final", () => {
|
|
531
|
+
const t = treeWith(res(R1, "StdMap", { inputsLocked: true }));
|
|
532
|
+
const reader = w();
|
|
533
|
+
expect(t.get(w(), R1).getKeyValue(reader, "k")).toBeUndefined();
|
|
534
|
+
t.updateFromResourceData([res(R1, "StdMap", { inputsLocked: true, resourceReady: true })]);
|
|
535
|
+
expect(reader.isChanged).toBe(true);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
describe("any error inside an update invalidates the tree and surfaces as a TreeStateUpdateError", () => {
|
|
540
|
+
test("a throwing final predicate invalidates the tree and notifies readers of the mutated state", () => {
|
|
541
|
+
const A = rid(30n);
|
|
542
|
+
const B = rid(31n);
|
|
543
|
+
const C = rid(32n);
|
|
544
|
+
const S = rid(33n);
|
|
545
|
+
const t = new PlTreeState(TestDynamicRootId1, (r) => {
|
|
546
|
+
if (r.type.name === "Boom") throw new Error("predicate failure");
|
|
547
|
+
return DefaultFinalResourceDataPredicate(r);
|
|
548
|
+
});
|
|
549
|
+
t.updateFromResourceData([
|
|
550
|
+
{ ...TestDynamicRootState1, fields: [dField("a", A)] },
|
|
551
|
+
res(A, "UserProject", {}, [dField("f", B)]),
|
|
552
|
+
res(B, "UserProject"),
|
|
553
|
+
]);
|
|
554
|
+
const reader = w();
|
|
555
|
+
t.get(reader, B);
|
|
556
|
+
// A is repointed B -> C, then the predicate throws a plain Error on S.
|
|
557
|
+
expect(() =>
|
|
558
|
+
t.updateFromResourceData(
|
|
559
|
+
[res(A, "UserProject", {}, [dField("f", C)]), res(C, "UserProject"), res(S, "Boom")],
|
|
560
|
+
{ allowOrphanInputs: true },
|
|
561
|
+
),
|
|
562
|
+
).toThrow(TreeStateUpdateError);
|
|
563
|
+
expect(t.isValid).toBe(false);
|
|
564
|
+
expect(reader.isChanged).toBe(true);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("the original error is kept as the cause", () => {
|
|
568
|
+
const t = new PlTreeState(TestDynamicRootId1, DefaultFinalResourceDataPredicate);
|
|
569
|
+
let thrown: unknown;
|
|
570
|
+
try {
|
|
571
|
+
// ready with inputs unlocked: verifyReadyState throws a plain Error
|
|
572
|
+
t.updateFromResourceData([{ ...TestDynamicRootState1, inputsLocked: false, fields: [] }]);
|
|
573
|
+
} catch (e: unknown) {
|
|
574
|
+
thrown = e;
|
|
575
|
+
}
|
|
576
|
+
expect(thrown).toBeInstanceOf(TreeStateUpdateError);
|
|
577
|
+
expect(thrown instanceof Error && thrown.cause instanceof Error).toBe(true);
|
|
578
|
+
expect(t.isValid).toBe(false);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe("a lock transition notifies lock readers", () => {
|
|
583
|
+
test("inputs", () => {
|
|
584
|
+
const t = treeWith(res(R1, "UserProject"));
|
|
585
|
+
const reader = w();
|
|
586
|
+
expect(t.get(w(), R1).getInputsLocked(reader)).toBe(false);
|
|
587
|
+
t.updateFromResourceData([res(R1, "UserProject", { inputsLocked: true })]);
|
|
588
|
+
expect(reader.isChanged).toBe(true);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("outputs", () => {
|
|
592
|
+
const t = treeWith(res(R1, "UserProject", { inputsLocked: true, resourceReady: true }));
|
|
593
|
+
const reader = w();
|
|
594
|
+
expect(t.get(w(), R1).getOutputsLocked(reader)).toBe(false);
|
|
595
|
+
t.updateFromResourceData([
|
|
596
|
+
res(R1, "UserProject", { inputsLocked: true, resourceReady: true, outputsLocked: true }),
|
|
597
|
+
]);
|
|
598
|
+
expect(reader.isChanged).toBe(true);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("a lock alone does not notify a ready reader", () => {
|
|
602
|
+
const t = treeWith(res(R1, "UserProject"));
|
|
603
|
+
const reader = w();
|
|
604
|
+
expect(t.get(w(), R1).getIsReadyOrError(reader)).toBe(false);
|
|
605
|
+
t.updateFromResourceData([res(R1, "UserProject", { inputsLocked: true })]);
|
|
606
|
+
expect(reader.isChanged).toBe(false);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
describe("a required-field read is re-run when the field appears", () => {
|
|
611
|
+
for (const opt of ["errorIfFieldNotFound", "errorIfFieldNotSet"] as const) {
|
|
612
|
+
test(opt, () => {
|
|
613
|
+
const t = treeWith(res(R1, "UserProject"));
|
|
614
|
+
const reader = w();
|
|
615
|
+
expect(() => t.get(w(), R1).getField(reader, { field: "x", [opt]: true }, () => {})).toThrow(
|
|
616
|
+
/not found/,
|
|
617
|
+
);
|
|
618
|
+
t.updateFromResourceData([res(R1, "UserProject", {}, [dField("x")])]);
|
|
619
|
+
expect(reader.isChanged).toBe(true);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
describe("removing a dynamic field re-evaluates finality", () => {
|
|
625
|
+
test("the resource becomes final and leaves the seed set", () => {
|
|
626
|
+
const upload = (fields: FieldData[]) =>
|
|
627
|
+
res(
|
|
628
|
+
R1,
|
|
629
|
+
"BlobUpload/x",
|
|
630
|
+
{ inputsLocked: true, outputsLocked: true, resourceReady: true },
|
|
631
|
+
fields,
|
|
632
|
+
);
|
|
633
|
+
const output = field("Output", "o", V1, NullSignedResourceId, true);
|
|
634
|
+
const t = treeWith(upload([output, dField("d")]), {
|
|
635
|
+
...TestValueResourceState1,
|
|
636
|
+
id: V1,
|
|
637
|
+
data: Buffer.from("x"),
|
|
638
|
+
});
|
|
639
|
+
expect(t.get(w(), R1).finalState).toBe(false);
|
|
640
|
+
|
|
641
|
+
t.updateFromResourceData([upload([output])]);
|
|
642
|
+
expect(t.get(w(), R1).finalState).toBe(true);
|
|
643
|
+
expect(constructTreeLoadingRequest(t).seedResources).not.toContain(R1);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test("a removal alone counts as a changed resource", () => {
|
|
647
|
+
const t = treeWith(res(R1, "UserProject", {}, [dField("d")]));
|
|
648
|
+
const stat = initialTreeLoadingStat();
|
|
649
|
+
t.updateFromResourceData([res(R1, "UserProject")], { stat });
|
|
650
|
+
expect(stat.resourcesChanged).toBe(1);
|
|
651
|
+
expect(stat.fieldsRemoved).toBe(1);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
describe("listDynamicFields lists no Service field", () => {
|
|
656
|
+
test("a Service field is an input field, not a dynamic one", () => {
|
|
657
|
+
const t = treeWith(res(R1, "UserProject"));
|
|
658
|
+
const dynamicReader = w();
|
|
659
|
+
const inputReader = w();
|
|
660
|
+
expect(t.get(w(), R1).listDynamicFields(dynamicReader)).toEqual([]);
|
|
661
|
+
expect(t.get(w(), R1).listInputFields(inputReader)).toEqual([]);
|
|
662
|
+
|
|
663
|
+
t.updateFromResourceData([res(R1, "UserProject", {}, [field("Service", "s"), dField("d")])]);
|
|
664
|
+
expect(t.get(w(), R1).listDynamicFields(w())).toEqual(["d"]);
|
|
665
|
+
expect(t.get(w(), R1).listInputFields(w())).toEqual(["s"]);
|
|
666
|
+
expect(inputReader.isChanged).toBe(true);
|
|
667
|
+
expect(dynamicReader.isChanged).toBe(true);
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
describe("a Dynamic or MTW field recreated under another type", () => {
|
|
672
|
+
for (const from of ["Dynamic", "MTW"] as const) {
|
|
673
|
+
test(`${from} -> Input notifies both lists`, () => {
|
|
674
|
+
const t = treeWith(res(R1, "UserProject", {}, [field(from, "f")]));
|
|
675
|
+
const inputReader = w();
|
|
676
|
+
const dynamicReader = w();
|
|
677
|
+
expect(t.get(w(), R1).listInputFields(inputReader)).toEqual([]);
|
|
678
|
+
expect(t.get(w(), R1).listDynamicFields(dynamicReader)).toEqual(["f"]);
|
|
679
|
+
|
|
680
|
+
t.updateFromResourceData([res(R1, "UserProject", {}, [field("Input", "f")])]);
|
|
681
|
+
expect(t.isValid).toBe(true);
|
|
682
|
+
expect(t.get(w(), R1).listInputFields(w())).toEqual(["f"]);
|
|
683
|
+
expect(t.get(w(), R1).listDynamicFields(w())).toEqual([]);
|
|
684
|
+
expect(inputReader.isChanged).toBe(true);
|
|
685
|
+
expect(dynamicReader.isChanged).toBe(true);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test(`${from} -> Output notifies the output list`, () => {
|
|
689
|
+
const t = treeWith(res(R1, "UserProject", {}, [field(from, "f")]));
|
|
690
|
+
const outputReader = w();
|
|
691
|
+
expect(t.get(w(), R1).listOutputFields(outputReader)).toEqual([]);
|
|
692
|
+
t.updateFromResourceData([res(R1, "UserProject", {}, [field("Output", "f")])]);
|
|
693
|
+
expect(t.get(w(), R1).listOutputFields(w())).toEqual(["f"]);
|
|
694
|
+
expect(outputReader.isChanged).toBe(true);
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
test("a reader refused on the old type is re-run when the field changes type", () => {
|
|
699
|
+
const t = treeWith(res(R1, "UserProject", {}, [dField("f")]));
|
|
700
|
+
const reader = w();
|
|
701
|
+
expect(() =>
|
|
702
|
+
t.get(w(), R1).getField(reader, { field: "f", assertFieldType: "Output" }, () => {}),
|
|
703
|
+
).toThrow(/Unexpected field type/);
|
|
704
|
+
t.updateFromResourceData([res(R1, "UserProject", {}, [field("Output", "f")])]);
|
|
705
|
+
expect(reader.isChanged).toBe(true);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("MTW -> Dynamic is accepted", () => {
|
|
709
|
+
const t = treeWith(res(R1, "UserProject", {}, [field("MTW", "f")]));
|
|
710
|
+
t.updateFromResourceData([res(R1, "UserProject", {}, [dField("f")])]);
|
|
711
|
+
expect(t.get(w(), R1).fieldsMap.get("f")?.type).toBe("Dynamic");
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test("-> Input while inputs are locked is rejected", () => {
|
|
715
|
+
const t = treeWith(res(R1, "UserProject", { inputsLocked: true }, [dField("f")]));
|
|
716
|
+
expect(() =>
|
|
717
|
+
t.updateFromResourceData([
|
|
718
|
+
res(R1, "UserProject", { inputsLocked: true }, [field("Input", "f")]),
|
|
719
|
+
]),
|
|
720
|
+
).toThrow(TreeStateUpdateError);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
test("-> Service while inputs are locked is rejected", () => {
|
|
724
|
+
const t = treeWith(res(R1, "UserProject", { inputsLocked: true }, [dField("f")]));
|
|
725
|
+
expect(() =>
|
|
726
|
+
t.updateFromResourceData([
|
|
727
|
+
res(R1, "UserProject", { inputsLocked: true }, [field("Service", "f")]),
|
|
728
|
+
]),
|
|
729
|
+
).toThrow(TreeStateUpdateError);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test("-> Output while outputs are locked is rejected", () => {
|
|
733
|
+
const locked = { inputsLocked: true, outputsLocked: true, resourceReady: true };
|
|
734
|
+
const t = treeWith(res(R1, "UserProject", locked, [dField("f")]));
|
|
735
|
+
expect(() =>
|
|
736
|
+
t.updateFromResourceData([res(R1, "UserProject", locked, [field("Output", "f")])]),
|
|
737
|
+
).toThrow(TreeStateUpdateError);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("a typed field cannot change type", () => {
|
|
741
|
+
const t = treeWith(res(R1, "UserProject", {}, [field("Input", "f")]));
|
|
742
|
+
expect(() =>
|
|
743
|
+
t.updateFromResourceData([res(R1, "UserProject", {}, [field("Output", "f")])]),
|
|
744
|
+
).toThrow(TreeStateUpdateError);
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
describe("the backend final flag follows updates", () => {
|
|
749
|
+
test("copied on update, and counted as a change", () => {
|
|
750
|
+
const t = treeWith(res(R1, "UserProject"));
|
|
751
|
+
const stat = initialTreeLoadingStat();
|
|
752
|
+
t.updateFromResourceData([res(R1, "UserProject", { final: true })], { stat });
|
|
753
|
+
expect(t.get(w(), R1).final).toBe(true);
|
|
754
|
+
expect(stat.resourcesChanged).toBe(1);
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
describe("invalidation reaches every reader of the invalidated state", () => {
|
|
759
|
+
test("a reader waiting for a resource the tree does not hold", () => {
|
|
760
|
+
const t = treeWith(res(R1, "UserProject"));
|
|
761
|
+
const reader = w();
|
|
762
|
+
expect(() => t.get(reader, rid(11n))).toThrow(/not found/);
|
|
763
|
+
t.invalidateTree("rebuild");
|
|
764
|
+
expect(reader.isChanged).toBe(true);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("a reader of a dropped root", () => {
|
|
768
|
+
const t = new PlTreeState(new Set([TestDynamicRootId1]), DefaultFinalResourceDataPredicate);
|
|
769
|
+
t.updateFromResourceData([{ ...TestDynamicRootState1, fields: [] }]);
|
|
770
|
+
expect(t.dropDeletedRoot(TestDynamicRootId1)).toBe(true);
|
|
771
|
+
const reader = w();
|
|
772
|
+
expect(() => t.get(reader, TestDynamicRootId1)).toThrow(/not found/);
|
|
773
|
+
t.invalidateTree("synchronization terminated for the tree");
|
|
774
|
+
expect(reader.isChanged).toBe(true);
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
describe("a resource becoming final settles readers of an unresolved field", () => {
|
|
779
|
+
test("the reader is re-run, and reads the field as permanently unresolved", () => {
|
|
780
|
+
const map = (ready: boolean) =>
|
|
781
|
+
res(R1, "StdMap", { inputsLocked: true, resourceReady: ready }, [dField("x")]);
|
|
782
|
+
const t = treeWith(map(false));
|
|
783
|
+
const reader = w();
|
|
784
|
+
const before: string[] = [];
|
|
785
|
+
t.get(w(), R1).getField(reader, "x", (m) => before.push(m));
|
|
786
|
+
expect(before).toEqual(["field_not_resolved:x"]);
|
|
787
|
+
|
|
788
|
+
t.updateFromResourceData([map(true)]);
|
|
789
|
+
expect(t.get(w(), R1).finalState).toBe(true);
|
|
790
|
+
expect(reader.isChanged).toBe(true);
|
|
791
|
+
const after: string[] = [];
|
|
792
|
+
t.get(w(), R1).getField(w(), "x", (m) => after.push(m));
|
|
793
|
+
expect(after).toEqual([]);
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
describe("a lock alone settles an asserted absence", () => {
|
|
798
|
+
for (const [assertFieldType, lock] of [
|
|
799
|
+
["Output", { inputsLocked: true, resourceReady: true, outputsLocked: true }],
|
|
800
|
+
["Input", { inputsLocked: true }],
|
|
801
|
+
] as const)
|
|
802
|
+
test(assertFieldType, () => {
|
|
803
|
+
const unlocked =
|
|
804
|
+
assertFieldType === "Output" ? { inputsLocked: true, resourceReady: true } : {};
|
|
805
|
+
const t = treeWith(res(R1, "UserProject", unlocked));
|
|
806
|
+
const step = { field: "f", assertFieldType, allowPermanentAbsence: true };
|
|
807
|
+
const reader = w();
|
|
808
|
+
const before: string[] = [];
|
|
809
|
+
expect(t.get(w(), R1).getField(reader, step, (m) => before.push(m))).toBeUndefined();
|
|
810
|
+
expect(before).toEqual(["field_not_found:f"]);
|
|
811
|
+
t.updateFromResourceData([res(R1, "UserProject", lock)]);
|
|
812
|
+
expect(reader.isChanged).toBe(true);
|
|
813
|
+
const after: string[] = [];
|
|
814
|
+
expect(t.get(w(), R1).getField(w(), step, (m) => after.push(m))).toBeUndefined();
|
|
815
|
+
expect(after).toEqual([]);
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
describe("a field retyped in the same body that locks its list is accepted", () => {
|
|
820
|
+
// the backend may recreate a field and lock its list in one transaction between two polls
|
|
821
|
+
test("Dynamic -> Input with inputs locking in the same body", () => {
|
|
822
|
+
const t = treeWith(res(R1, "UserProject", {}, [dField("f")]));
|
|
823
|
+
const inputReader = w();
|
|
824
|
+
const lockReader = w();
|
|
825
|
+
expect(t.get(w(), R1).listInputFields(inputReader)).toEqual([]);
|
|
826
|
+
expect(t.get(w(), R1).getInputsLocked(lockReader)).toBe(false);
|
|
827
|
+
t.updateFromResourceData([
|
|
828
|
+
res(R1, "UserProject", { inputsLocked: true }, [field("Input", "f")]),
|
|
829
|
+
]);
|
|
830
|
+
expect(t.isValid).toBe(true);
|
|
831
|
+
expect(t.get(w(), R1).listInputFields(w())).toEqual(["f"]);
|
|
832
|
+
expect(inputReader.isChanged).toBe(true);
|
|
833
|
+
expect(lockReader.isChanged).toBe(true);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test("MTW -> Output with outputs locking in the same body", () => {
|
|
837
|
+
const t = treeWith(res(R1, "UserProject", { inputsLocked: true }, [field("MTW", "f")]));
|
|
838
|
+
const outputReader = w();
|
|
839
|
+
expect(t.get(w(), R1).listOutputFields(outputReader)).toEqual([]);
|
|
840
|
+
t.updateFromResourceData([
|
|
841
|
+
res(R1, "UserProject", { inputsLocked: true, outputsLocked: true, resourceReady: true }, [
|
|
842
|
+
field("Output", "f"),
|
|
843
|
+
]),
|
|
844
|
+
]);
|
|
845
|
+
expect(t.isValid).toBe(true);
|
|
846
|
+
expect(outputReader.isChanged).toBe(true);
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
test("dropping a root keeps a child that is another root, and a child shared with one", () => {
|
|
851
|
+
const A = TestDynamicRootId1;
|
|
852
|
+
const B = rid(40n);
|
|
853
|
+
const C = rid(41n);
|
|
854
|
+
const S = rid(42n);
|
|
855
|
+
const t = new PlTreeState(new Set([A, B, C]), DefaultFinalResourceDataPredicate);
|
|
856
|
+
t.updateFromResourceData([
|
|
857
|
+
{ ...TestDynamicRootState1, fields: [dField("b", B), dField("s", S)] },
|
|
858
|
+
res(B, "UserProject"),
|
|
859
|
+
res(C, "UserProject", {}, [dField("s", S)]),
|
|
860
|
+
res(S, "UserProject"),
|
|
861
|
+
]);
|
|
862
|
+
expect(t.dropDeletedRoot(A)).toBe(true);
|
|
863
|
+
const ids = t.dumpState().map((r) => r.id);
|
|
864
|
+
expect(ids).not.toContain(A);
|
|
865
|
+
expect(ids).toContain(B);
|
|
866
|
+
expect(ids).toContain(S);
|
|
867
|
+
// with A dropped, only C holds S
|
|
868
|
+
expect(t.dropDeletedRoot(C)).toBe(true);
|
|
869
|
+
expect(t.dumpState().map((r) => r.id)).not.toContain(S);
|
|
870
|
+
expect(t.isValid).toBe(true);
|
|
871
|
+
});
|