@graffiti-garden/api 0.6.1 → 0.6.3

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/tests/discover.ts CHANGED
@@ -2,14 +2,21 @@ import { it, expect, describe, assert, beforeAll } from "vitest";
2
2
  import type {
3
3
  Graffiti,
4
4
  GraffitiObjectBase,
5
- GraffitiObjectStreamEntry,
6
5
  GraffitiSession,
7
6
  JSONSchema,
8
7
  } from "@graffiti-garden/api";
9
- import { randomString, nextStreamValue, randomPutObject } from "./utils";
8
+ import {
9
+ randomString,
10
+ nextStreamValue,
11
+ randomPutObject,
12
+ continueStream,
13
+ } from "./utils";
10
14
 
11
15
  export const graffitiDiscoverTests = (
12
- useGraffiti: () => Pick<Graffiti, "discover" | "put" | "delete" | "patch">,
16
+ useGraffiti: () => Pick<
17
+ Graffiti,
18
+ "discover" | "put" | "delete" | "patch" | "continueObjectStream"
19
+ >,
13
20
  useSession1: () => GraffitiSession | Promise<GraffitiSession>,
14
21
  useSession2: () => GraffitiSession | Promise<GraffitiSession>,
15
22
  ) => {
@@ -469,197 +476,230 @@ export const graffitiDiscoverTests = (
469
476
  expect(counts.get("other")).toBe(1);
470
477
  });
471
478
 
472
- it("discover for deleted content", async () => {
473
- const object = randomPutObject();
474
-
475
- const putted = await graffiti.put<{}>(object, session);
476
-
477
- const iterator1 = graffiti.discover<{}>(object.channels, {});
478
- const value1 = await nextStreamValue<{}>(iterator1);
479
- expect(value1.value).toEqual(object.value);
480
- const returnValue = await iterator1.next();
481
- assert(returnValue.done, "value2 is not done");
482
-
483
- const deleted = await graffiti.delete(putted, session);
484
-
485
- const iterator = graffiti.discover(object.channels, {});
486
- await expect(iterator.next()).resolves.toHaveProperty("done", true);
479
+ for (const continueType of ["cursor", "continue"] as const) {
480
+ describe(`continue discover with ${continueType}`, () => {
481
+ it("discover for deleted content", async () => {
482
+ const object = randomPutObject();
483
+
484
+ const putted = await graffiti.put<{}>(object, session);
485
+
486
+ const iterator1 = graffiti.discover<{}>(object.channels, {});
487
+ const value1 = await nextStreamValue<{}>(iterator1);
488
+ expect(value1.value).toEqual(object.value);
489
+ const returnValue = await iterator1.next();
490
+ assert(returnValue.done, "value2 is not done");
491
+
492
+ const deleted = await graffiti.delete(putted, session);
493
+
494
+ const iterator = graffiti.discover(object.channels, {});
495
+ await expect(iterator.next()).resolves.toHaveProperty("done", true);
496
+
497
+ const tombIterator = continueStream<{}>(
498
+ graffiti,
499
+ returnValue.value,
500
+ continueType,
501
+ );
502
+ const value = await tombIterator.next();
503
+ assert(!value.done && !value.value.error, "value is done");
504
+ assert(value.value.tombstone, "value is not tombstone");
505
+ expect(value.value.object.url).toEqual(putted.url);
506
+ await expect(tombIterator.next()).resolves.toHaveProperty(
507
+ "done",
508
+ true,
509
+ );
510
+ });
487
511
 
488
- const tombIterator = returnValue.value.continue();
489
- const value = await tombIterator.next();
490
- assert(!value.done && !value.value.error, "value is done");
491
- assert(value.value.tombstone, "value is not tombstone");
492
- expect(value.value.object.url).toEqual(putted.url);
493
- await expect(tombIterator.next()).resolves.toHaveProperty("done", true);
494
- });
512
+ it("discover for replaced channels", async () => {
513
+ // Do this a bunch to check for concurrency issues
514
+ async function runTest() {
515
+ const object1 = randomPutObject();
516
+ const putted = await graffiti.put<{}>(object1, session);
517
+
518
+ const iterator3 = graffiti.discover<{}>(object1.channels, {});
519
+ const value3 = await nextStreamValue<{}>(iterator3);
520
+ expect(value3.value).toEqual(object1.value);
521
+ const returnValue = await iterator3.next();
522
+ assert(returnValue.done, "value2 is not done");
523
+
524
+ const object2 = randomPutObject();
525
+ const replaced = await graffiti.put<{}>(
526
+ {
527
+ ...object2,
528
+ url: putted.url,
529
+ },
530
+ session,
531
+ );
532
+
533
+ const iterator1 = graffiti.discover<{}>(object1.channels, {});
534
+ const iterator2 = graffiti.discover<{}>(object2.channels, {});
535
+ const tombIterator = continueStream<{}>(
536
+ graffiti,
537
+ returnValue.value,
538
+ continueType,
539
+ );
540
+
541
+ if (putted.lastModified === replaced.lastModified) {
542
+ const value1 = await iterator1.next();
543
+ const value2 = await iterator2.next();
544
+ const value3 = await tombIterator.next();
545
+
546
+ // Only one should be done
547
+ expect(value1.done || value2.done).toBe(true);
548
+ expect(value1.done && value2.done).toBe(false);
549
+
550
+ assert(!value3.done && !value3.value.error, "value is done");
551
+ expect(value3.value.tombstone || value2.done).toBe(true);
552
+ expect(value3.value.tombstone && value2.done).toBe(false);
553
+ return;
554
+ }
495
555
 
496
- it("discover for replaced channels", async () => {
497
- // Do this a bunch to check for concurrency issues
498
- for (let i = 0; i < 20; i++) {
499
- const object1 = randomPutObject();
500
- const putted = await graffiti.put<{}>(object1, session);
556
+ // Otherwise 1 should be done and 2 should not
557
+ const value5 = await iterator1.next();
558
+ assert(value5.done, "value5 is not done");
559
+
560
+ const value4 = await tombIterator.next();
561
+ assert(!value4.done && !value4.value.error, "value is done");
562
+
563
+ assert(value4.value.tombstone, "value is not tombstone");
564
+ expect(value4.value.object.url).toEqual(putted.url);
565
+ expect(value4.value.object.lastModified).toEqual(
566
+ replaced.lastModified,
567
+ );
568
+
569
+ const value2 = await nextStreamValue<{}>(iterator2);
570
+ await expect(iterator2.next()).resolves.toHaveProperty(
571
+ "done",
572
+ true,
573
+ );
574
+
575
+ expect(value2.value).toEqual(object2.value);
576
+ expect(value2.channels).toEqual(object2.channels);
577
+ expect(value2.lastModified).toEqual(replaced.lastModified);
578
+
579
+ // Replace the channels back
580
+ const patched = await graffiti.patch(
581
+ {
582
+ channels: [
583
+ { op: "replace", path: "", value: object1.channels },
584
+ ],
585
+ },
586
+ replaced,
587
+ session,
588
+ );
589
+
590
+ const tombIterator2 = continueStream<{}>(
591
+ graffiti,
592
+ value5.value,
593
+ continueType,
594
+ );
595
+
596
+ let result:
597
+ | {
598
+ tombstone: true;
599
+ object: {
600
+ url: string;
601
+ lastModified: number;
602
+ };
603
+ }
604
+ | {
605
+ tombstone?: undefined;
606
+ object: GraffitiObjectBase;
607
+ }
608
+ | undefined;
609
+ for await (const value of tombIterator2) {
610
+ if (value.error) continue;
611
+ if (!result) {
612
+ result = value;
613
+ continue;
614
+ }
615
+ if (
616
+ value.object.lastModified > result.object.lastModified ||
617
+ (value.object.lastModified === result.object.lastModified &&
618
+ !value.tombstone &&
619
+ result.tombstone)
620
+ ) {
621
+ result = value;
622
+ }
623
+ }
501
624
 
502
- const iterator3 = graffiti.discover<{}>(object1.channels, {});
503
- const value3 = await nextStreamValue<{}>(iterator3);
504
- expect(value3.value).toEqual(object1.value);
505
- const returnValue = await iterator3.next();
506
- assert(returnValue.done, "value2 is not done");
625
+ assert(result, "result is not defined");
626
+ assert(!result.tombstone, "result is tombstone");
627
+ expect(result.object.url).toEqual(replaced.url);
628
+ expect(result.object.lastModified).toEqual(patched.lastModified);
629
+ expect(result.object.channels).toEqual(object1.channels);
630
+ expect(result.object.value).toEqual(object2.value);
631
+ }
507
632
 
508
- const object2 = randomPutObject();
509
- const replaced = await graffiti.put<{}>(
510
- {
511
- ...object2,
512
- url: putted.url,
513
- },
514
- session,
515
- );
516
-
517
- const iterator1 = graffiti.discover(object1.channels, {});
518
- const iterator2 = graffiti.discover<{}>(object2.channels, {});
519
- const tombIterator = returnValue.value.continue();
520
-
521
- if (putted.lastModified === replaced.lastModified) {
522
- const value1 = await iterator1.next();
523
- const value2 = await iterator2.next();
524
- const value3 = await tombIterator.next();
525
-
526
- // Only one should be done
527
- expect(value1.done || value2.done).toBe(true);
528
- expect(value1.done && value2.done).toBe(false);
529
-
530
- assert(!value3.done && !value3.value.error, "value is done");
531
- expect(value3.value.tombstone || value2.done).toBe(true);
532
- expect(value3.value.tombstone && value2.done).toBe(false);
533
- continue;
534
- }
633
+ // Run the test 20 times in parallel to check for concurrency issues
634
+ await Promise.allSettled(Array.from({ length: 20 }, () => runTest()));
635
+ });
535
636
 
536
- // Otherwise 1 should be done and 2 should not
537
- const value5 = await iterator1.next();
538
- assert(value5.done, "value5 is not done");
637
+ it("discover for patched allowed", async () => {
638
+ const object = randomPutObject();
639
+ const putted = await graffiti.put<{}>(object, session);
539
640
 
540
- const value4 = await tombIterator.next();
541
- assert(!value4.done && !value4.value.error, "value is done");
641
+ const iterator1 = graffiti.discover<{}>(object.channels, {});
642
+ const value1 = await nextStreamValue<{}>(iterator1);
643
+ expect(value1.value).toEqual(object.value);
644
+ const returnValue = await iterator1.next();
645
+ assert(returnValue.done, "value2 is not done");
542
646
 
543
- assert(value4.value.tombstone, "value is not tombstone");
544
- expect(value4.value.object.url).toEqual(putted.url);
545
- expect(value4.value.object.lastModified).toEqual(replaced.lastModified);
647
+ await graffiti.patch(
648
+ {
649
+ allowed: [{ op: "add", path: "", value: [] }],
650
+ },
651
+ putted,
652
+ session,
653
+ );
654
+ const iterator2 = graffiti.discover(object.channels, {});
655
+ expect(await iterator2.next()).toHaveProperty("done", true);
656
+
657
+ const iterator = continueStream<{}>(
658
+ graffiti,
659
+ returnValue.value,
660
+ continueType,
661
+ );
662
+ const value = await iterator.next();
663
+ assert(!value.done && !value.value.error, "value is done");
664
+ assert(value.value.tombstone, "value is not tombstone");
665
+ expect(value.value.object.url).toEqual(putted.url);
666
+ await expect(iterator.next()).resolves.toHaveProperty("done", true);
667
+ });
546
668
 
547
- const value2 = await nextStreamValue<{}>(iterator2);
548
- await expect(iterator2.next()).resolves.toHaveProperty("done", true);
669
+ it("put concurrently and discover one", async () => {
670
+ const object = randomPutObject();
549
671
 
550
- expect(value2.value).toEqual(object2.value);
551
- expect(value2.channels).toEqual(object2.channels);
552
- expect(value2.lastModified).toEqual(replaced.lastModified);
672
+ // Put a first one to get a URI
673
+ const putted = await graffiti.put<{}>(object, session);
553
674
 
554
- // Replace the channels back
555
- const patched = await graffiti.patch(
556
- {
557
- channels: [{ op: "replace", path: "", value: object1.channels }],
558
- },
559
- replaced,
560
- session,
561
- );
562
-
563
- const tombIterator2 = value5.value.continue();
564
-
565
- let result:
566
- | {
567
- tombstone: true;
568
- object: {
569
- url: string;
570
- lastModified: number;
571
- };
572
- }
573
- | {
574
- tombstone?: undefined;
575
- object: GraffitiObjectBase;
675
+ const putPromises = Array(99)
676
+ .fill(0)
677
+ .map(() =>
678
+ graffiti.put<{}>(
679
+ {
680
+ ...object,
681
+ url: putted.url,
682
+ },
683
+ session,
684
+ ),
685
+ );
686
+ await Promise.all(putPromises);
687
+
688
+ const iterator = graffiti.discover(object.channels, {});
689
+ let tombstoneCount = 0;
690
+ let valueCount = 0;
691
+ for await (const result of iterator) {
692
+ assert(!result.error, "result has error");
693
+ if (result.tombstone) {
694
+ tombstoneCount++;
695
+ } else {
696
+ valueCount++;
576
697
  }
577
- | undefined;
578
- for await (const value of tombIterator2) {
579
- if (value.error) continue;
580
- if (!result) {
581
- result = value;
582
- continue;
583
- }
584
- if (
585
- value.object.lastModified > result.object.lastModified ||
586
- (value.object.lastModified === result.object.lastModified &&
587
- !value.tombstone &&
588
- result.tombstone)
589
- ) {
590
- result = value;
591
698
  }
592
- }
593
-
594
- assert(result, "result is not defined");
595
- assert(!result.tombstone, "result is tombstone");
596
- expect(result.object.url).toEqual(replaced.url);
597
- expect(result.object.lastModified).toEqual(patched.lastModified);
598
- expect(result.object.channels).toEqual(object1.channels);
599
- expect(result.object.value).toEqual(object2.value);
600
- }
601
- });
602
-
603
- it("discover for patched allowed", async () => {
604
- const object = randomPutObject();
605
- const putted = await graffiti.put<{}>(object, session);
606
-
607
- const iterator1 = graffiti.discover<{}>(object.channels, {});
608
- const value1 = await nextStreamValue<{}>(iterator1);
609
- expect(value1.value).toEqual(object.value);
610
- const returnValue = await iterator1.next();
611
- assert(returnValue.done, "value2 is not done");
612
-
613
- await graffiti.patch(
614
- {
615
- allowed: [{ op: "add", path: "", value: [] }],
616
- },
617
- putted,
618
- session,
619
- );
620
- const iterator2 = graffiti.discover(object.channels, {});
621
- expect(await iterator2.next()).toHaveProperty("done", true);
622
-
623
- const iterator = returnValue.value.continue();
624
- const value = await iterator.next();
625
- assert(!value.done && !value.value.error, "value is done");
626
- assert(value.value.tombstone, "value is not tombstone");
627
- expect(value.value.object.url).toEqual(putted.url);
628
- await expect(iterator.next()).resolves.toHaveProperty("done", true);
629
- });
630
-
631
- it("put concurrently and discover one", async () => {
632
- const object = randomPutObject();
633
-
634
- // Put a first one to get a URI
635
- const putted = await graffiti.put<{}>(object, session);
636
-
637
- const putPromises = Array(99)
638
- .fill(0)
639
- .map(() =>
640
- graffiti.put<{}>(
641
- {
642
- ...object,
643
- url: putted.url,
644
- },
645
- session,
646
- ),
647
- );
648
- await Promise.all(putPromises);
649
-
650
- const iterator = graffiti.discover(object.channels, {});
651
- let tombstoneCount = 0;
652
- let valueCount = 0;
653
- for await (const result of iterator) {
654
- assert(!result.error, "result has error");
655
- if (result.tombstone) {
656
- tombstoneCount++;
657
- } else {
658
- valueCount++;
659
- }
660
- }
661
- expect(tombstoneCount).toBe(0);
662
- expect(valueCount).toBe(1);
663
- });
699
+ expect(tombstoneCount).toBe(0);
700
+ expect(valueCount).toBe(1);
701
+ });
702
+ });
703
+ }
664
704
  });
665
705
  };
package/tests/orphans.ts CHANGED
@@ -1,11 +1,16 @@
1
1
  import { it, expect, describe, assert, beforeAll } from "vitest";
2
2
  import type { Graffiti, GraffitiSession } from "@graffiti-garden/api";
3
- import { randomPutObject, randomString, nextStreamValue } from "./utils";
3
+ import {
4
+ randomPutObject,
5
+ randomString,
6
+ nextStreamValue,
7
+ continueStream,
8
+ } from "./utils";
4
9
 
5
10
  export const graffitiOrphanTests = (
6
11
  useGraffiti: () => Pick<
7
12
  Graffiti,
8
- "recoverOrphans" | "put" | "delete" | "patch"
13
+ "recoverOrphans" | "put" | "delete" | "patch" | "continueObjectStream"
9
14
  >,
10
15
  useSession1: () => GraffitiSession | Promise<GraffitiSession>,
11
16
  useSession2: () => GraffitiSession | Promise<GraffitiSession>,
@@ -46,67 +51,78 @@ export const graffitiOrphanTests = (
46
51
  expect(numResults).toBe(1);
47
52
  });
48
53
 
49
- it("replaced orphan, no longer", async () => {
50
- const object = randomPutObject();
51
- object.channels = [];
52
- const putOrphan = await graffiti.put<{}>(object, session);
54
+ for (const continueType of ["continue", "cursor"] as const) {
55
+ describe(`continue orphans with ${continueType}`, () => {
56
+ it("replaced orphan, no longer", async () => {
57
+ const object = randomPutObject();
58
+ object.channels = [];
59
+ const putOrphan = await graffiti.put<{}>(object, session);
53
60
 
54
- // Wait for the put to be processed
55
- await new Promise((resolve) => setTimeout(resolve, 10));
61
+ // Wait for the put to be processed
62
+ await new Promise((resolve) => setTimeout(resolve, 10));
56
63
 
57
- expect(Object.keys(object.value).length).toBeGreaterThanOrEqual(1);
58
- expect(Object.keys(object.value)[0]).toBeTypeOf("string");
59
- const iterator1 = graffiti.recoverOrphans<{}>(
60
- {
61
- properties: {
62
- value: {
64
+ expect(Object.keys(object.value).length).toBeGreaterThanOrEqual(1);
65
+ expect(Object.keys(object.value)[0]).toBeTypeOf("string");
66
+ const iterator1 = graffiti.recoverOrphans<{}>(
67
+ {
63
68
  properties: {
64
- [Object.keys(object.value)[0]]: {
65
- type: "string",
69
+ value: {
70
+ properties: {
71
+ [Object.keys(object.value)[0]]: {
72
+ type: "string",
73
+ },
74
+ },
75
+ required: [Object.keys(object.value)[0]],
66
76
  },
67
77
  },
68
- required: [Object.keys(object.value)[0]],
69
78
  },
70
- },
71
- },
72
- session,
73
- );
74
- const value1 = await nextStreamValue<{}>(iterator1);
75
- expect(value1.value).toEqual(object.value);
76
- const returnValue = await iterator1.next();
77
- assert(returnValue.done, "value2 is not done");
79
+ session,
80
+ );
81
+ const value1 = await nextStreamValue<{}>(iterator1);
82
+ expect(value1.value).toEqual(object.value);
83
+ const returnValue = await iterator1.next();
84
+ assert(returnValue.done, "value2 is not done");
78
85
 
79
- const putNotOrphan = await graffiti.put<{}>(
80
- {
81
- ...putOrphan,
82
- ...object,
83
- channels: [randomString()],
84
- },
85
- session,
86
- );
87
- expect(putNotOrphan.url).toBe(putOrphan.url);
88
- expect(putNotOrphan.lastModified).toBeGreaterThan(putOrphan.lastModified);
86
+ const putNotOrphan = await graffiti.put<{}>(
87
+ {
88
+ ...putOrphan,
89
+ ...object,
90
+ channels: [randomString()],
91
+ },
92
+ session,
93
+ );
94
+ expect(putNotOrphan.url).toBe(putOrphan.url);
95
+ expect(putNotOrphan.lastModified).toBeGreaterThan(
96
+ putOrphan.lastModified,
97
+ );
89
98
 
90
- // The tombstone will not appear to a fresh iterator
91
- const orphanIterator = graffiti.recoverOrphans({}, session);
92
- let numResults = 0;
93
- for await (const orphan of orphanIterator) {
94
- if (orphan.error) continue;
95
- if (orphan.object.url === putOrphan.url) {
96
- numResults++;
97
- }
98
- }
99
- expect(numResults).toBe(0);
99
+ // The tombstone will not appear to a fresh iterator
100
+ const orphanIterator = graffiti.recoverOrphans({}, session);
101
+ let numResults = 0;
102
+ for await (const orphan of orphanIterator) {
103
+ if (orphan.error) continue;
104
+ if (orphan.object.url === putOrphan.url) {
105
+ numResults++;
106
+ }
107
+ }
108
+ expect(numResults).toBe(0);
100
109
 
101
- const iterator2 = returnValue.value.continue();
102
- const value2 = await iterator2.next();
103
- assert(
104
- !value2.done && !value2.value.error,
105
- "value2 is done or has error",
106
- );
107
- assert(value2.value.tombstone, "value2 is not tombstone");
108
- expect(value2.value.object.url).toBe(putOrphan.url);
109
- await expect(iterator2.next()).resolves.toHaveProperty("done", true);
110
- });
110
+ const iterator2 = continueStream<{}>(
111
+ graffiti,
112
+ returnValue.value,
113
+ continueType,
114
+ session,
115
+ );
116
+ const value2 = await iterator2.next();
117
+ assert(
118
+ !value2.done && !value2.value.error,
119
+ "value2 is done or has error",
120
+ );
121
+ assert(value2.value.tombstone, "value2 is not tombstone");
122
+ expect(value2.value.object.url).toBe(putOrphan.url);
123
+ await expect(iterator2.next()).resolves.toHaveProperty("done", true);
124
+ });
125
+ });
126
+ }
111
127
  });
112
128
  };
package/tests/utils.ts CHANGED
@@ -4,6 +4,10 @@ import type {
4
4
  GraffitiObjectStream,
5
5
  JSONSchema,
6
6
  GraffitiObject,
7
+ GraffitiObjectStreamReturn,
8
+ GraffitiObjectStreamContinue,
9
+ Graffiti,
10
+ GraffitiSession,
7
11
  } from "@graffiti-garden/api";
8
12
 
9
13
  export function randomString(): string {
@@ -38,3 +42,19 @@ export async function nextStreamValue<Schema extends JSONSchema>(
38
42
  assert(!result.value.tombstone, "result has been deleted!");
39
43
  return result.value.object;
40
44
  }
45
+
46
+ export function continueStream<Schema extends JSONSchema>(
47
+ graffiti: Pick<Graffiti, "continueObjectStream">,
48
+ streamReturn: GraffitiObjectStreamReturn<Schema>,
49
+ type: "cursor" | "continue",
50
+ session?: GraffitiSession | null,
51
+ ): GraffitiObjectStreamContinue<Schema> {
52
+ if (type === "cursor") {
53
+ return graffiti.continueObjectStream(
54
+ streamReturn.cursor,
55
+ session,
56
+ ) as unknown as GraffitiObjectStreamContinue<Schema>;
57
+ } else {
58
+ return streamReturn.continue();
59
+ }
60
+ }