@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/dist/browser/index.js +2 -20
- package/dist/browser/index.js.map +4 -4
- package/dist/cjs/database.js +130 -139
- package/dist/cjs/database.js.map +3 -3
- package/dist/cjs/index.js +11 -2
- package/dist/cjs/index.js.map +2 -2
- package/dist/database.d.ts +8 -4
- package/dist/database.d.ts.map +1 -1
- package/dist/esm/database.js +130 -139
- package/dist/esm/database.js.map +3 -3
- package/dist/esm/index.js +11 -2
- package/dist/esm/index.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -2
- package/src/database.ts +200 -202
- package/src/index.ts +16 -3
package/src/database.ts
CHANGED
|
@@ -4,9 +4,8 @@ import type {
|
|
|
4
4
|
GraffitiObjectUrl,
|
|
5
5
|
JSONSchema,
|
|
6
6
|
GraffitiSession,
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
601
|
+
const result = await (
|
|
602
|
+
await this.db
|
|
603
|
+
).query<GraffitiObjectWithTombstone>(index, {
|
|
604
|
+
startkey,
|
|
605
|
+
endkey,
|
|
606
|
+
include_docs: true,
|
|
607
|
+
});
|
|
608
608
|
|
|
609
|
-
|
|
609
|
+
for (const row of result.rows) {
|
|
610
|
+
const doc = row.doc;
|
|
611
|
+
if (!doc) continue;
|
|
610
612
|
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
624
|
-
const doc = row.doc;
|
|
625
|
-
if (!doc) continue;
|
|
618
|
+
const object = this.extractGraffitiObject(doc);
|
|
626
619
|
|
|
627
|
-
|
|
620
|
+
if (channels) {
|
|
621
|
+
if (!isActorAllowedGraffitiObject(object, session)) continue;
|
|
622
|
+
maskGraffitiObject(object, channels, session);
|
|
623
|
+
}
|
|
628
624
|
|
|
629
|
-
|
|
625
|
+
if (!validate(object)) continue;
|
|
630
626
|
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
641
|
-
if (!isActorAllowedGraffitiObject(doc, session)) continue;
|
|
653
|
+
const processedIds = new Set<string>();
|
|
642
654
|
|
|
643
|
-
|
|
644
|
-
// if the user is not the owner
|
|
645
|
-
maskGraffitiObject(object, channels, session);
|
|
655
|
+
const startTime = new Date().getTime();
|
|
646
656
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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
|
-
|
|
679
|
-
|
|
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
|
-
|
|
684
|
-
session: GraffitiSession,
|
|
680
|
+
protected async *recoverOrphansMeta<Schema extends JSONSchema>(
|
|
681
|
+
args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>,
|
|
685
682
|
ifModifiedSince?: number,
|
|
686
|
-
):
|
|
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
|
-
|
|
696
|
-
const showTombstones = ifModifiedSince !== undefined;
|
|
696
|
+
const validate = compileGraffitiObjectSchema(await this.ajv, schema);
|
|
697
697
|
|
|
698
|
-
const
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
700
|
+
const iterator = this.streamObjects<Schema>(
|
|
701
|
+
"indexes/orphansPerActorAndLastModified",
|
|
702
|
+
startkey,
|
|
703
|
+
endkey,
|
|
704
|
+
validate,
|
|
705
|
+
session,
|
|
706
|
+
ifModifiedSince,
|
|
707
|
+
);
|
|
717
708
|
|
|
718
|
-
|
|
709
|
+
for await (const result of iterator) yield result;
|
|
719
710
|
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
725
|
-
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
745
|
-
|
|
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
|
-
|
|
768
|
+
}
|
|
769
|
+
yield result.value;
|
|
770
|
+
}
|
|
753
771
|
}
|
|
754
772
|
|
|
755
773
|
recoverOrphans: Graffiti["recoverOrphans"] = (...args) => {
|
|
756
|
-
|
|
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
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
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.
|
|
47
|
-
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
|