@graffiti-garden/implementation-local 0.6.0 → 0.6.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/src/database.ts CHANGED
@@ -4,9 +4,8 @@ import type {
4
4
  GraffitiObjectUrl,
5
5
  JSONSchema,
6
6
  GraffitiSession,
7
- GraffitiObject,
8
- GraffitiStream,
9
- ChannelStats,
7
+ GraffitiObjectStreamContinue,
8
+ GraffitiObjectStreamContinueEntry,
10
9
  } from "@graffiti-garden/api";
11
10
  import {
12
11
  GraffitiErrorNotFound,
@@ -22,7 +21,6 @@ import {
22
21
  compileGraffitiObjectSchema,
23
22
  unpackObjectUrl,
24
23
  } from "./utilities.js";
25
- import { Repeater } from "@repeaterjs/repeater";
26
24
  import type Ajv from "ajv";
27
25
  import type { applyPatch } from "fast-json-patch";
28
26
 
@@ -68,6 +66,7 @@ export interface GraffitiLocalOptions {
68
66
  }
69
67
 
70
68
  const DEFAULT_ORIGIN = "graffiti:local:";
69
+ const LAST_MODIFIED_BUFFER = 60000;
71
70
 
72
71
  type GraffitiObjectWithTombstone = GraffitiObjectBase & { tombstone: boolean };
73
72
 
@@ -587,103 +586,105 @@ export class GraffitiLocalDatabase
587
586
  };
588
587
  }
589
588
 
590
- protected discoverMeta<Schema extends JSONSchema>(
591
- channels: string[],
592
- schema: Schema,
593
- session?: GraffitiSession | null,
594
- ifModifiedSince?: number,
595
- ): GraffitiStream<GraffitiObject<Schema>> {
596
- const { startKeySuffix, endKeySuffix } = this.queryLastModifiedSuffixes(
597
- schema,
598
- ifModifiedSince,
599
- );
600
-
601
- // Don't return tombstones on the first pass
589
+ protected async *streamObjects<Schema extends JSONSchema>(
590
+ index: string,
591
+ startkey: string,
592
+ endkey: string,
593
+ validate: ReturnType<typeof compileGraffitiObjectSchema<Schema>>,
594
+ session: GraffitiSession | undefined | null,
595
+ ifModifiedSince: number | undefined,
596
+ channels?: string[],
597
+ processedIds?: Set<string>,
598
+ ): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>> {
602
599
  const showTombstones = ifModifiedSince !== undefined;
603
600
 
604
- const repeater: GraffitiStream<GraffitiObject<typeof schema>> =
605
- // @ts-ignore
606
- new Repeater(async (push, stop) => {
607
- const validate = compileGraffitiObjectSchema(await this.ajv, schema);
601
+ const result = await (
602
+ await this.db
603
+ ).query<GraffitiObjectWithTombstone>(index, {
604
+ startkey,
605
+ endkey,
606
+ include_docs: true,
607
+ });
608
608
 
609
- const processedIds = new Set<string>();
609
+ for (const row of result.rows) {
610
+ const doc = row.doc;
611
+ if (!doc) continue;
610
612
 
611
- for (const channel of channels) {
612
- const keyPrefix = encodeURIComponent(channel) + "/";
613
- const startkey = keyPrefix + startKeySuffix;
614
- const endkey = keyPrefix + endKeySuffix;
613
+ if (processedIds?.has(doc._id)) continue;
614
+ processedIds?.add(doc._id);
615
615
 
616
- const result = await (
617
- await this.db
618
- ).query<GraffitiObjectWithTombstone>(
619
- "indexes/objectsPerChannelAndLastModified",
620
- { startkey, endkey, include_docs: true },
621
- );
616
+ if (!showTombstones && doc.tombstone) continue;
622
617
 
623
- for (const row of result.rows) {
624
- const doc = row.doc;
625
- if (!doc) continue;
618
+ const object = this.extractGraffitiObject(doc);
626
619
 
627
- if (!showTombstones && doc.tombstone) continue;
620
+ if (channels) {
621
+ if (!isActorAllowedGraffitiObject(object, session)) continue;
622
+ maskGraffitiObject(object, channels, session);
623
+ }
628
624
 
629
- const object = this.extractGraffitiObject(doc);
625
+ if (!validate(object)) continue;
630
626
 
631
- if (!ifModifiedSince || object.lastModified > ifModifiedSince) {
632
- ifModifiedSince = object.lastModified;
633
- }
627
+ yield doc.tombstone
628
+ ? {
629
+ tombstone: true,
630
+ object: {
631
+ url: object.url,
632
+ lastModified: object.lastModified,
633
+ },
634
+ }
635
+ : { object };
636
+ }
637
+ }
634
638
 
635
- // Don't double return the same object
636
- // (which can happen if it's in multiple channels)
637
- if (processedIds.has(doc._id)) continue;
638
- processedIds.add(doc._id);
639
+ protected async *discoverMeta<Schema extends JSONSchema>(
640
+ args: Parameters<typeof Graffiti.prototype.discover<Schema>>,
641
+ ifModifiedSince?: number,
642
+ ): AsyncGenerator<
643
+ GraffitiObjectStreamContinueEntry<Schema>,
644
+ number | undefined
645
+ > {
646
+ const [channels, schema, session] = args;
647
+ const validate = compileGraffitiObjectSchema(await this.ajv, schema);
648
+ const { startKeySuffix, endKeySuffix } = this.queryLastModifiedSuffixes(
649
+ schema,
650
+ ifModifiedSince,
651
+ );
639
652
 
640
- // Make sure the user is allowed to see it
641
- if (!isActorAllowedGraffitiObject(doc, session)) continue;
653
+ const processedIds = new Set<string>();
642
654
 
643
- // Mask out the allowed list and channels
644
- // if the user is not the owner
645
- maskGraffitiObject(object, channels, session);
655
+ const startTime = new Date().getTime();
646
656
 
647
- // Check that it matches the schema
648
- if (validate(object)) {
649
- await push({
650
- value: object,
651
- ...(doc.tombstone ? { tombstone: true } : {}),
652
- });
653
- }
654
- }
655
- }
656
- stop();
657
-
658
- const cursor: string =
659
- "discover:" +
660
- JSON.stringify({
661
- channels,
662
- schema,
663
- ifModifiedSince,
664
- actor: session?.actor,
665
- });
666
- return {
667
- cursor,
668
- continue: () =>
669
- this.continueStream(cursor, session) as GraffitiStream<
670
- GraffitiObject<Schema>
671
- >,
672
- };
673
- });
657
+ for (const channel of channels) {
658
+ const keyPrefix = encodeURIComponent(channel) + "/";
659
+ const startkey = keyPrefix + startKeySuffix;
660
+ const endkey = keyPrefix + endKeySuffix;
674
661
 
675
- return repeater;
676
- }
662
+ const iterator = this.streamObjects<Schema>(
663
+ "indexes/objectsPerChannelAndLastModified",
664
+ startkey,
665
+ endkey,
666
+ validate,
667
+ session,
668
+ ifModifiedSince,
669
+ channels,
670
+ processedIds,
671
+ );
677
672
 
678
- discover: Graffiti["discover"] = (...args) => {
679
- return this.discoverMeta<(typeof args)[1]>(...args);
680
- };
673
+ for await (const result of iterator) yield result;
674
+ }
675
+
676
+ // Subtract a minute to make sure we don't miss any objects
677
+ return startTime - LAST_MODIFIED_BUFFER;
678
+ }
681
679
 
682
- protected recoverOrphansMeta<Schema extends JSONSchema>(
683
- schema: Schema,
684
- session: GraffitiSession,
680
+ protected async *recoverOrphansMeta<Schema extends JSONSchema>(
681
+ args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>,
685
682
  ifModifiedSince?: number,
686
- ): GraffitiStream<GraffitiObject<Schema>> {
683
+ ): AsyncGenerator<
684
+ GraffitiObjectStreamContinueEntry<Schema>,
685
+ number | undefined
686
+ > {
687
+ const [schema, session] = args;
687
688
  const { startKeySuffix, endKeySuffix } = this.queryLastModifiedSuffixes(
688
689
  schema,
689
690
  ifModifiedSince,
@@ -692,141 +693,138 @@ export class GraffitiLocalDatabase
692
693
  const startkey = keyPrefix + startKeySuffix;
693
694
  const endkey = keyPrefix + endKeySuffix;
694
695
 
695
- // Don't return tombstones on the first pass
696
- const showTombstones = ifModifiedSince !== undefined;
696
+ const validate = compileGraffitiObjectSchema(await this.ajv, schema);
697
697
 
698
- const repeater: GraffitiStream<GraffitiObject<Schema>> =
699
- // @ts-ignore
700
- new Repeater(async (push, stop) => {
701
- const validate = compileGraffitiObjectSchema(await this.ajv, schema);
702
-
703
- const result = await (
704
- await this.db
705
- ).query<GraffitiObjectWithTombstone>(
706
- "indexes/orphansPerActorAndLastModified",
707
- {
708
- startkey,
709
- endkey,
710
- include_docs: true,
711
- },
712
- );
698
+ const startTime = new Date().getTime();
713
699
 
714
- for (const row of result.rows) {
715
- const doc = row.doc;
716
- if (!doc) continue;
700
+ const iterator = this.streamObjects<Schema>(
701
+ "indexes/orphansPerActorAndLastModified",
702
+ startkey,
703
+ endkey,
704
+ validate,
705
+ session,
706
+ ifModifiedSince,
707
+ );
717
708
 
718
- if (!showTombstones && doc.tombstone) continue;
709
+ for await (const result of iterator) yield result;
719
710
 
720
- if (!ifModifiedSince || doc.lastModified > ifModifiedSince) {
721
- ifModifiedSince = doc.lastModified;
722
- }
711
+ return startTime - LAST_MODIFIED_BUFFER;
712
+ }
713
+
714
+ protected async *discoverContinue<Schema extends JSONSchema>(
715
+ args: Parameters<typeof Graffiti.prototype.discover<Schema>>,
716
+ ifModifiedSince?: number,
717
+ ): GraffitiObjectStreamContinue<Schema> {
718
+ const iterator = this.discoverMeta(args, ifModifiedSince);
723
719
 
724
- // No masking/access necessary because
725
- // the objects are all owned by the querier
720
+ while (true) {
721
+ const result = await iterator.next();
722
+ if (result.done) {
723
+ const ifModifiedSince = result.value;
724
+ return {
725
+ continue: () => this.discoverContinue<Schema>(args, ifModifiedSince),
726
+ cursor: "",
727
+ };
728
+ }
729
+ yield result.value;
730
+ }
731
+ }
726
732
 
727
- const object = this.extractGraffitiObject(doc);
728
- if (validate(object)) {
729
- await push({
730
- value: object,
731
- ...(doc.tombstone ? { tombstone: true } : {}),
732
- });
733
- }
733
+ discover: Graffiti["discover"] = (...args) => {
734
+ const iterator = this.discoverMeta(args);
735
+
736
+ const this_ = this;
737
+ return (async function* () {
738
+ while (true) {
739
+ const result = await iterator.next();
740
+ if (result.done) {
741
+ return {
742
+ continue: () =>
743
+ this_.discoverContinue<(typeof args)[1]>(args, result.value),
744
+ cursor: "",
745
+ };
734
746
  }
735
- stop();
736
- const cursor: string =
737
- "recover-orphans:" +
738
- JSON.stringify({
739
- schema,
740
- actor: session.actor,
741
- ifModifiedSince,
742
- });
747
+ // Make sure to filter out tombstones
748
+ if (result.value.tombstone) continue;
749
+ yield result.value;
750
+ }
751
+ })();
752
+ };
753
+
754
+ protected async *recoverContinue<Schema extends JSONSchema>(
755
+ args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>,
756
+ ifModifiedSince?: number,
757
+ ): GraffitiObjectStreamContinue<Schema> {
758
+ const iterator = this.recoverOrphansMeta(args, ifModifiedSince);
759
+
760
+ while (true) {
761
+ const result = await iterator.next();
762
+ if (result.done) {
763
+ const ifModifiedSince = result.value;
743
764
  return {
744
- cursor,
745
- continue: () =>
746
- this.continueStream(cursor, session) as GraffitiStream<
747
- GraffitiObject<Schema>
748
- >,
765
+ continue: () => this.recoverContinue<Schema>(args, ifModifiedSince),
766
+ cursor: "",
749
767
  };
750
- });
751
-
752
- return repeater;
768
+ }
769
+ yield result.value;
770
+ }
753
771
  }
754
772
 
755
773
  recoverOrphans: Graffiti["recoverOrphans"] = (...args) => {
756
- return this.recoverOrphansMeta<(typeof args)[0]>(...args);
774
+ const iterator = this.recoverOrphansMeta(args);
775
+
776
+ const this_ = this;
777
+ return (async function* () {
778
+ while (true) {
779
+ const result = await iterator.next();
780
+ if (result.done) {
781
+ return {
782
+ continue: () =>
783
+ this_.recoverContinue<(typeof args)[0]>(args, result.value),
784
+ cursor: "",
785
+ };
786
+ }
787
+ // Make sure to filter out tombstones
788
+ if (result.value.tombstone) continue;
789
+ yield result.value;
790
+ }
791
+ })();
757
792
  };
758
793
 
759
794
  channelStats: Graffiti["channelStats"] = (session) => {
760
- const repeater: ReturnType<typeof Graffiti.prototype.channelStats> =
761
- new Repeater(async (push, stop) => {
762
- const keyPrefix = encodeURIComponent(session.actor) + "/";
763
- const result = await (
764
- await this.db
765
- ).query("indexes/channelStatsPerActor", {
766
- startkey: keyPrefix,
767
- endkey: keyPrefix + "\uffff",
768
- reduce: true,
769
- group: true,
770
- });
771
- for (const row of result.rows) {
772
- const channelEncoded = row.key.split("/")[1];
773
- if (typeof channelEncoded !== "string") continue;
774
- const { count, max: lastModified } = row.value;
775
- if (typeof count !== "number" || typeof lastModified !== "number")
776
- continue;
777
- await push({
778
- value: {
779
- channel: decodeURIComponent(channelEncoded),
780
- count,
781
- lastModified,
782
- },
783
- });
784
- }
785
- stop();
786
- const cursor = "channel-stats";
787
- return {
788
- cursor,
789
- continue: () =>
790
- this.continueStream(
791
- cursor,
792
- session,
793
- ) as GraffitiStream<ChannelStats>,
794
- };
795
+ const this_ = this;
796
+ return (async function* () {
797
+ const keyPrefix = encodeURIComponent(session.actor) + "/";
798
+ const result = await (
799
+ await this_.db
800
+ ).query("indexes/channelStatsPerActor", {
801
+ startkey: keyPrefix,
802
+ endkey: keyPrefix + "\uffff",
803
+ reduce: true,
804
+ group: true,
795
805
  });
796
-
797
- return repeater;
806
+ for (const row of result.rows) {
807
+ const channelEncoded = row.key.split("/")[1];
808
+ if (typeof channelEncoded !== "string") continue;
809
+ const { count, max: lastModified } = row.value;
810
+ if (typeof count !== "number" || typeof lastModified !== "number")
811
+ continue;
812
+ yield {
813
+ value: {
814
+ channel: decodeURIComponent(channelEncoded),
815
+ count,
816
+ lastModified,
817
+ },
818
+ };
819
+ }
820
+ })();
798
821
  };
799
822
 
800
- continueStream: Graffiti["continueStream"] = (cursor, session) => {
801
- if (cursor === "channel-stats") {
802
- if (!session) {
803
- throw new GraffitiErrorForbidden(
804
- "You must be logged in to continue the stream",
805
- );
806
- }
807
- return this.channelStats(session);
808
- } else if (cursor.startsWith("recover-orphans:")) {
809
- const { schema, actor, ifModifiedSince } = JSON.parse(
810
- cursor.slice("recover-orphans:".length),
811
- );
812
- if (!session || session.actor !== actor) {
813
- throw new GraffitiErrorForbidden(
814
- "You must be logged in as the actor same actor who started the stream",
815
- );
816
- }
817
- return this.recoverOrphansMeta(schema, session, ifModifiedSince);
818
- } else if (cursor.startsWith("discover:")) {
819
- const { channels, schema, actor, ifModifiedSince } = JSON.parse(
820
- cursor.slice("discover:".length),
821
- );
822
- if (session?.actor !== actor) {
823
- throw new GraffitiErrorForbidden(
824
- "You must be logged in as the actor same actor who started the stream",
825
- );
826
- }
827
- return this.discoverMeta(channels, schema, session, ifModifiedSince);
828
- } else {
829
- throw new GraffitiErrorNotFound("Cursor not found");
830
- }
823
+ continueObjectStream: Graffiti["continueObjectStream"] = (
824
+ cursor,
825
+ session,
826
+ ) => {
827
+ // TODO: Implement this
828
+ throw new GraffitiErrorNotFound("Cursor not found");
831
829
  };
832
830
  }
package/src/index.ts CHANGED
@@ -27,7 +27,7 @@ export class GraffitiLocal extends Graffiti {
27
27
  discover: Graffiti["discover"];
28
28
  recoverOrphans: Graffiti["recoverOrphans"];
29
29
  channelStats: Graffiti["channelStats"];
30
- continueStream: Graffiti["continueStream"];
30
+ continueObjectStream: Graffiti["continueObjectStream"];
31
31
 
32
32
  constructor(options?: GraffitiLocalOptions) {
33
33
  super();
@@ -43,7 +43,20 @@ export class GraffitiLocal extends Graffiti {
43
43
  graffitiPouchDbBase.recoverOrphans.bind(graffitiPouchDbBase);
44
44
  this.channelStats =
45
45
  graffitiPouchDbBase.channelStats.bind(graffitiPouchDbBase);
46
- this.continueStream =
47
- graffitiPouchDbBase.continueStream.bind(graffitiPouchDbBase);
46
+ this.continueObjectStream =
47
+ graffitiPouchDbBase.continueObjectStream.bind(graffitiPouchDbBase);
48
48
  }
49
49
  }
50
+
51
+ function myFunction<T extends boolean>(
52
+ flag: T,
53
+ ): T extends true ? string : number {
54
+ if (!flag) {
55
+ return "Hello" as T extends true ? string : number;
56
+ } else {
57
+ return 42 as T extends true ? string : number;
58
+ }
59
+ }
60
+ // Usage
61
+ const a = myFunction(false); // Type is number
62
+ const b = myFunction(true); // Type is string