@graffiti-garden/implementation-local 0.5.1 → 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,6 +4,8 @@ import type {
4
4
  GraffitiObjectUrl,
5
5
  JSONSchema,
6
6
  GraffitiSession,
7
+ GraffitiObjectStreamContinue,
8
+ GraffitiObjectStreamContinueEntry,
7
9
  } from "@graffiti-garden/api";
8
10
  import {
9
11
  GraffitiErrorNotFound,
@@ -16,11 +18,9 @@ import {
16
18
  applyGraffitiPatch,
17
19
  maskGraffitiObject,
18
20
  isActorAllowedGraffitiObject,
19
- isObjectNewer,
20
21
  compileGraffitiObjectSchema,
21
- unpackLocationOrUri,
22
+ unpackObjectUrl,
22
23
  } from "./utilities.js";
23
- import { Repeater } from "@repeaterjs/repeater";
24
24
  import type Ajv from "ajv";
25
25
  import type { applyPatch } from "fast-json-patch";
26
26
 
@@ -38,18 +38,18 @@ export interface GraffitiLocalOptions {
38
38
  pouchDBOptions?: PouchDB.Configuration.DatabaseConfiguration;
39
39
  /**
40
40
  * Includes the scheme and other information (possibly domain name)
41
- * to prefix prefixes all URIs put in the system. Defaults to `graffiti:local`.
41
+ * to prefix prefixes all URLs put in the system. Defaults to `graffiti:local`.
42
42
  */
43
43
  origin?: string;
44
44
  /**
45
- * Whether to allow putting objects at arbtirary URIs, i.e.
46
- * URIs that are *not* prefixed with the origin or not generated
45
+ * Whether to allow putting objects at arbtirary URLs, i.e.
46
+ * URLs that are *not* prefixed with the origin or not generated
47
47
  * by the system. Defaults to `false`.
48
48
  *
49
49
  * Allows this implementation to be used as a client-side cache
50
50
  * for remote sources.
51
51
  */
52
- allowSettingArbitraryUris?: boolean;
52
+ allowSettingArbitraryUrls?: boolean;
53
53
  /**
54
54
  * Whether to allow the user to set the lastModified field
55
55
  * when putting objects. Defaults to `false`.
@@ -58,12 +58,6 @@ export interface GraffitiLocalOptions {
58
58
  * for remote sources.
59
59
  */
60
60
  allowSettinngLastModified?: boolean;
61
- /**
62
- * The time in milliseconds to keep tombstones before deleting them.
63
- * See the {@link https://api.graffiti.garden/classes/Graffiti.html#discover | `discover` }
64
- * documentation for more information.
65
- */
66
- tombstoneRetention?: number;
67
61
  /**
68
62
  * An optional Ajv instance to use for schema validation.
69
63
  * If not provided, an internal instance will be created.
@@ -71,27 +65,21 @@ export interface GraffitiLocalOptions {
71
65
  ajv?: Ajv;
72
66
  }
73
67
 
74
- const DEFAULT_TOMBSTONE_RETENTION = 86400000; // 1 day in milliseconds
75
68
  const DEFAULT_ORIGIN = "graffiti:local:";
69
+ const LAST_MODIFIED_BUFFER = 60000;
70
+
71
+ type GraffitiObjectWithTombstone = GraffitiObjectBase & { tombstone: boolean };
76
72
 
77
73
  /**
78
74
  * An implementation of only the database operations of the
79
75
  * GraffitiAPI without synchronization or session management.
80
76
  */
81
77
  export class GraffitiLocalDatabase
82
- implements
83
- Pick<
84
- Graffiti,
85
- | "get"
86
- | "put"
87
- | "patch"
88
- | "delete"
89
- | "discover"
90
- | "recoverOrphans"
91
- | "channelStats"
92
- >
78
+ implements Omit<Graffiti, "login" | "logout" | "sessionEvents">
93
79
  {
94
- protected db_: Promise<PouchDB.Database<GraffitiObjectBase>> | undefined;
80
+ protected db_:
81
+ | Promise<PouchDB.Database<GraffitiObjectWithTombstone>>
82
+ | undefined;
95
83
  protected applyPatch_: Promise<typeof applyPatch> | undefined;
96
84
  protected ajv_: Promise<Ajv> | undefined;
97
85
  protected readonly options: GraffitiLocalOptions;
@@ -105,7 +93,7 @@ export class GraffitiLocalDatabase
105
93
  name: "graffitiDb",
106
94
  ...this.options.pouchDBOptions,
107
95
  };
108
- const db = new PouchDB<GraffitiObjectBase>(
96
+ const db = new PouchDB<GraffitiObjectWithTombstone>(
109
97
  pouchDbOptions.name,
110
98
  pouchDbOptions,
111
99
  );
@@ -115,7 +103,7 @@ export class GraffitiLocalDatabase
115
103
  _id: "_design/indexes",
116
104
  views: {
117
105
  objectsPerChannelAndLastModified: {
118
- map: function (object: GraffitiObjectBase) {
106
+ map: function (object: GraffitiObjectWithTombstone) {
119
107
  const paddedLastModified = object.lastModified
120
108
  .toString()
121
109
  .padStart(15, "0");
@@ -128,7 +116,7 @@ export class GraffitiLocalDatabase
128
116
  }.toString(),
129
117
  },
130
118
  orphansPerActorAndLastModified: {
131
- map: function (object: GraffitiObjectBase) {
119
+ map: function (object: GraffitiObjectWithTombstone) {
132
120
  if (object.channels.length === 0) {
133
121
  const paddedLastModified = object.lastModified
134
122
  .toString()
@@ -143,7 +131,7 @@ export class GraffitiLocalDatabase
143
131
  }.toString(),
144
132
  },
145
133
  channelStatsPerActor: {
146
- map: function (object: GraffitiObjectBase) {
134
+ map: function (object: GraffitiObjectWithTombstone) {
147
135
  if (object.tombstone) return;
148
136
  object.channels.forEach(function (channel) {
149
137
  const id =
@@ -178,7 +166,7 @@ export class GraffitiLocalDatabase
178
166
  return this.db_;
179
167
  }
180
168
 
181
- get applyPatch() {
169
+ protected get applyPatch() {
182
170
  if (!this.applyPatch_) {
183
171
  this.applyPatch_ = (async () => {
184
172
  const { applyPatch } = await import("fast-json-patch");
@@ -188,7 +176,7 @@ export class GraffitiLocalDatabase
188
176
  return this.applyPatch_;
189
177
  }
190
178
 
191
- get ajv() {
179
+ protected get ajv() {
192
180
  if (!this.ajv_) {
193
181
  this.ajv_ = this.options.ajv
194
182
  ? Promise.resolve(this.options.ajv)
@@ -200,6 +188,20 @@ export class GraffitiLocalDatabase
200
188
  return this.ajv_;
201
189
  }
202
190
 
191
+ protected extractGraffitiObject(
192
+ object: GraffitiObjectWithTombstone,
193
+ ): GraffitiObjectBase {
194
+ const { value, channels, allowed, url, actor, lastModified } = object;
195
+ return {
196
+ value,
197
+ channels,
198
+ allowed,
199
+ url,
200
+ actor,
201
+ lastModified,
202
+ };
203
+ }
204
+
203
205
  constructor(options?: GraffitiLocalOptions) {
204
206
  this.options = options ?? {};
205
207
  this.origin = this.options.origin ?? DEFAULT_ORIGIN;
@@ -208,13 +210,13 @@ export class GraffitiLocalDatabase
208
210
  }
209
211
  }
210
212
 
211
- protected async allDocsAtLocation(locationOrUri: GraffitiObjectUrl | string) {
212
- const uri = unpackLocationOrUri(locationOrUri) + "/";
213
+ protected async allDocsAtLocation(objectUrl: string | GraffitiObjectUrl) {
214
+ const url = unpackObjectUrl(objectUrl) + "/";
213
215
  const results = await (
214
216
  await this.db
215
217
  ).allDocs({
216
- startkey: uri,
217
- endkey: uri + "\uffff", // \uffff is the last unicode character
218
+ startkey: url,
219
+ endkey: url + "\uffff", // \uffff is the last unicode character
218
220
  include_docs: true,
219
221
  });
220
222
  const docs = results.rows
@@ -222,7 +224,7 @@ export class GraffitiLocalDatabase
222
224
  // Remove undefined docs
223
225
  .reduce<
224
226
  PouchDB.Core.ExistingDocument<
225
- GraffitiObjectBase & PouchDB.Core.AllDocsMeta
227
+ GraffitiObjectWithTombstone & PouchDB.Core.AllDocsMeta
226
228
  >[]
227
229
  >((acc, doc) => {
228
230
  if (doc) acc.push(doc);
@@ -231,14 +233,14 @@ export class GraffitiLocalDatabase
231
233
  return docs;
232
234
  }
233
235
 
234
- protected docId(location: GraffitiObjectUrl) {
235
- return location.url + "/" + randomBase64();
236
+ protected docId(objectUrl: GraffitiObjectUrl) {
237
+ return objectUrl.url + "/" + randomBase64();
236
238
  }
237
239
 
238
240
  get: Graffiti["get"] = async (...args) => {
239
- const [locationOrUri, schema, session] = args;
241
+ const [urlObject, schema, session] = args;
240
242
 
241
- const docsAll = await this.allDocsAtLocation(locationOrUri);
243
+ const docsAll = await this.allDocsAtLocation(urlObject);
242
244
 
243
245
  // Filter out ones not allowed
244
246
  const docs = docsAll.filter((doc) =>
@@ -250,10 +252,20 @@ export class GraffitiLocalDatabase
250
252
  );
251
253
 
252
254
  // Get the most recent document
253
- const doc = docs.reduce((a, b) => (isObjectNewer(a, b) ? a : b));
255
+ const doc = docs.reduce((a, b) =>
256
+ a.lastModified > b.lastModified ||
257
+ (a.lastModified === b.lastModified && !a.tombstone && b.tombstone)
258
+ ? a
259
+ : b,
260
+ );
261
+
262
+ if (doc.tombstone) {
263
+ throw new GraffitiErrorNotFound(
264
+ "The object you are trying to get either does not exist or you are not allowed to see it",
265
+ );
266
+ }
254
267
 
255
- // Strip out the _id and _rev
256
- const { _id, _rev, _conflicts, _attachments, ...object } = doc;
268
+ const object = this.extractGraffitiObject(doc);
257
269
 
258
270
  // Mask out the allowed list and channels
259
271
  // if the user is not the owner
@@ -275,7 +287,7 @@ export class GraffitiLocalDatabase
275
287
  * spared.
276
288
  */
277
289
  protected async deleteAtLocation(
278
- locationOrUri: GraffitiObjectUrl | string,
290
+ url: GraffitiObjectUrl | string,
279
291
  options: {
280
292
  keepLatest?: boolean;
281
293
  session?: GraffitiSession;
@@ -283,7 +295,7 @@ export class GraffitiLocalDatabase
283
295
  keepLatest: false,
284
296
  },
285
297
  ) {
286
- const docsAtLocationAll = await this.allDocsAtLocation(locationOrUri);
298
+ const docsAtLocationAll = await this.allDocsAtLocation(url);
287
299
  const docsAtLocationAllowed = options.session
288
300
  ? docsAtLocationAll.filter((doc) =>
289
301
  isActorAllowedGraffitiObject(doc, options.session),
@@ -353,10 +365,8 @@ export class GraffitiLocalDatabase
353
365
  const { id } = resultOrError;
354
366
  const deletedDoc = docsToDelete.find((doc) => doc._id === id);
355
367
  if (deletedDoc) {
356
- const { _id, _rev, _conflicts, _attachments, ...object } = deletedDoc;
357
368
  deletedObject = {
358
- ...object,
359
- tombstone: true,
369
+ ...this.extractGraffitiObject(deletedDoc),
360
370
  lastModified,
361
371
  };
362
372
  break;
@@ -368,8 +378,8 @@ export class GraffitiLocalDatabase
368
378
  }
369
379
 
370
380
  delete: Graffiti["delete"] = async (...args) => {
371
- const [locationOrUri, session] = args;
372
- const deletedObject = await this.deleteAtLocation(locationOrUri, {
381
+ const [url, session] = args;
382
+ const deletedObject = await this.deleteAtLocation(url, {
373
383
  session,
374
384
  });
375
385
  if (!deletedObject) {
@@ -392,7 +402,7 @@ export class GraffitiLocalDatabase
392
402
  oldObject = await this.get(objectPartial.url, {}, session);
393
403
  } catch (e) {
394
404
  if (e instanceof GraffitiErrorNotFound) {
395
- if (!this.options.allowSettingArbitraryUris) {
405
+ if (!this.options.allowSettingArbitraryUrls) {
396
406
  throw new GraffitiErrorNotFound(
397
407
  "The object you are trying to replace does not exist or you are not allowed to see it",
398
408
  );
@@ -413,7 +423,7 @@ export class GraffitiLocalDatabase
413
423
  objectPartial.lastModified) ||
414
424
  new Date().getTime();
415
425
 
416
- const object: GraffitiObjectBase = {
426
+ const object: GraffitiObjectWithTombstone = {
417
427
  value: objectPartial.value,
418
428
  channels: objectPartial.channels,
419
429
  allowed: objectPartial.allowed,
@@ -448,10 +458,10 @@ export class GraffitiLocalDatabase
448
458
  };
449
459
 
450
460
  patch: Graffiti["patch"] = async (...args) => {
451
- const [patch, locationOrUri, session] = args;
461
+ const [patch, url, session] = args;
452
462
  let originalObject: GraffitiObjectBase;
453
463
  try {
454
- originalObject = await this.get(locationOrUri, {}, session);
464
+ originalObject = await this.get(url, {}, session);
455
465
  } catch (e) {
456
466
  if (e instanceof GraffitiErrorNotFound) {
457
467
  throw new GraffitiErrorNotFound(
@@ -465,10 +475,6 @@ export class GraffitiLocalDatabase
465
475
  throw new GraffitiErrorForbidden(
466
476
  "The object you are trying to patch is owned by another actor",
467
477
  );
468
- } else if (originalObject.tombstone) {
469
- throw new GraffitiErrorNotFound(
470
- "The object you are trying to patch has been deleted",
471
- );
472
478
  }
473
479
 
474
480
  // Patch it outside of the database
@@ -512,6 +518,7 @@ export class GraffitiLocalDatabase
512
518
  await this.db
513
519
  ).put({
514
520
  ...patchObject,
521
+ tombstone: false,
515
522
  _id: this.docId(patchObject),
516
523
  });
517
524
 
@@ -522,12 +529,14 @@ export class GraffitiLocalDatabase
522
529
 
523
530
  return {
524
531
  ...originalObject,
525
- tombstone: true,
526
532
  lastModified: patchObject.lastModified,
527
533
  };
528
534
  };
529
535
 
530
- protected queryLastModifiedSuffixes(schema: JSONSchema) {
536
+ protected queryLastModifiedSuffixes(
537
+ schema: JSONSchema,
538
+ lastModified?: number,
539
+ ) {
531
540
  // Use the index for queries over ranges of lastModified
532
541
  let startKeySuffix = "";
533
542
  let endKeySuffix = "\uffff";
@@ -538,7 +547,10 @@ export class GraffitiLocalDatabase
538
547
  ) {
539
548
  const lastModifiedSchema = schema.properties.lastModified;
540
549
 
541
- const minimum = lastModifiedSchema.minimum;
550
+ const minimum =
551
+ lastModified && lastModifiedSchema.minimum
552
+ ? Math.max(lastModified, lastModifiedSchema.minimum)
553
+ : (lastModified ?? lastModifiedSchema.minimum);
542
554
  const exclusiveMinimum = lastModifiedSchema.exclusiveMinimum;
543
555
 
544
556
  let intMinimum: number | undefined;
@@ -574,136 +586,245 @@ export class GraffitiLocalDatabase
574
586
  };
575
587
  }
576
588
 
577
- discover: Graffiti["discover"] = (...args) => {
578
- const [channels, schema, session] = args;
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>> {
599
+ const showTombstones = ifModifiedSince !== undefined;
600
+
601
+ const result = await (
602
+ await this.db
603
+ ).query<GraffitiObjectWithTombstone>(index, {
604
+ startkey,
605
+ endkey,
606
+ include_docs: true,
607
+ });
579
608
 
580
- const { startKeySuffix, endKeySuffix } =
581
- this.queryLastModifiedSuffixes(schema);
609
+ for (const row of result.rows) {
610
+ const doc = row.doc;
611
+ if (!doc) continue;
582
612
 
583
- const repeater: ReturnType<
584
- typeof Graffiti.prototype.discover<typeof schema>
585
- > = new Repeater(async (push, stop) => {
586
- const validate = compileGraffitiObjectSchema(await this.ajv, schema);
613
+ if (processedIds?.has(doc._id)) continue;
614
+ processedIds?.add(doc._id);
587
615
 
588
- const processedIds = new Set<string>();
616
+ if (!showTombstones && doc.tombstone) continue;
589
617
 
590
- for (const channel of channels) {
591
- const keyPrefix = encodeURIComponent(channel) + "/";
592
- const startkey = keyPrefix + startKeySuffix;
593
- const endkey = keyPrefix + endKeySuffix;
618
+ const object = this.extractGraffitiObject(doc);
594
619
 
595
- const result = await (
596
- await this.db
597
- ).query<GraffitiObjectBase>(
598
- "indexes/objectsPerChannelAndLastModified",
599
- { startkey, endkey, include_docs: true },
600
- );
620
+ if (channels) {
621
+ if (!isActorAllowedGraffitiObject(object, session)) continue;
622
+ maskGraffitiObject(object, channels, session);
623
+ }
601
624
 
602
- for (const row of result.rows) {
603
- const doc = row.doc;
604
- if (!doc) continue;
625
+ if (!validate(object)) continue;
605
626
 
606
- const { _id, _rev, ...object } = doc;
627
+ yield doc.tombstone
628
+ ? {
629
+ tombstone: true,
630
+ object: {
631
+ url: object.url,
632
+ lastModified: object.lastModified,
633
+ },
634
+ }
635
+ : { object };
636
+ }
637
+ }
607
638
 
608
- // Don't double return the same object
609
- // (which can happen if it's in multiple channels)
610
- if (processedIds.has(_id)) continue;
611
- processedIds.add(_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
+ );
612
652
 
613
- // Make sure the user is allowed to see it
614
- if (!isActorAllowedGraffitiObject(doc, session)) continue;
653
+ const processedIds = new Set<string>();
615
654
 
616
- // Mask out the allowed list and channels
617
- // if the user is not the owner
618
- maskGraffitiObject(object, channels, session);
655
+ const startTime = new Date().getTime();
619
656
 
620
- // Check that it matches the schema
621
- if (validate(object)) {
622
- await push({ value: object });
623
- }
624
- }
625
- }
626
- stop();
627
- return {
628
- tombstoneRetention:
629
- this.options.tombstoneRetention ?? DEFAULT_TOMBSTONE_RETENTION,
630
- };
631
- });
657
+ for (const channel of channels) {
658
+ const keyPrefix = encodeURIComponent(channel) + "/";
659
+ const startkey = keyPrefix + startKeySuffix;
660
+ const endkey = keyPrefix + endKeySuffix;
632
661
 
633
- return repeater;
634
- };
662
+ const iterator = this.streamObjects<Schema>(
663
+ "indexes/objectsPerChannelAndLastModified",
664
+ startkey,
665
+ endkey,
666
+ validate,
667
+ session,
668
+ ifModifiedSince,
669
+ channels,
670
+ processedIds,
671
+ );
635
672
 
636
- recoverOrphans: Graffiti["recoverOrphans"] = (schema, session) => {
637
- const { startKeySuffix, endKeySuffix } =
638
- this.queryLastModifiedSuffixes(schema);
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
+ }
679
+
680
+ protected async *recoverOrphansMeta<Schema extends JSONSchema>(
681
+ args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>,
682
+ ifModifiedSince?: number,
683
+ ): AsyncGenerator<
684
+ GraffitiObjectStreamContinueEntry<Schema>,
685
+ number | undefined
686
+ > {
687
+ const [schema, session] = args;
688
+ const { startKeySuffix, endKeySuffix } = this.queryLastModifiedSuffixes(
689
+ schema,
690
+ ifModifiedSince,
691
+ );
639
692
  const keyPrefix = encodeURIComponent(session.actor) + "/";
640
693
  const startkey = keyPrefix + startKeySuffix;
641
694
  const endkey = keyPrefix + endKeySuffix;
642
695
 
643
- const repeater: ReturnType<
644
- typeof Graffiti.prototype.recoverOrphans<typeof schema>
645
- > = new Repeater(async (push, stop) => {
646
- const validate = compileGraffitiObjectSchema(await this.ajv, schema);
696
+ const validate = compileGraffitiObjectSchema(await this.ajv, schema);
647
697
 
648
- const result = await (
649
- await this.db
650
- ).query<GraffitiObjectBase>("indexes/orphansPerActorAndLastModified", {
651
- startkey,
652
- endkey,
653
- include_docs: true,
654
- });
698
+ const startTime = new Date().getTime();
655
699
 
656
- for (const row of result.rows) {
657
- const doc = row.doc;
658
- if (!doc) continue;
700
+ const iterator = this.streamObjects<Schema>(
701
+ "indexes/orphansPerActorAndLastModified",
702
+ startkey,
703
+ endkey,
704
+ validate,
705
+ session,
706
+ ifModifiedSince,
707
+ );
708
+
709
+ for await (const result of iterator) yield result;
659
710
 
660
- // No masking/access necessary because
661
- // the objects are all owned by the querier
711
+ return startTime - LAST_MODIFIED_BUFFER;
712
+ }
662
713
 
663
- const { _id, _rev, ...object } = doc;
664
- if (validate(object)) {
665
- await push({ value: object });
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);
719
+
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
+ }
732
+
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
+ };
666
746
  }
747
+ // Make sure to filter out tombstones
748
+ if (result.value.tombstone) continue;
749
+ yield result.value;
667
750
  }
668
- stop();
669
- return {
670
- tombstoneRetention:
671
- this.options.tombstoneRetention ?? DEFAULT_TOMBSTONE_RETENTION,
672
- };
673
- });
751
+ })();
752
+ };
674
753
 
675
- return repeater;
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;
764
+ return {
765
+ continue: () => this.recoverContinue<Schema>(args, ifModifiedSince),
766
+ cursor: "",
767
+ };
768
+ }
769
+ yield result.value;
770
+ }
771
+ }
772
+
773
+ recoverOrphans: Graffiti["recoverOrphans"] = (...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
+ })();
676
792
  };
677
793
 
678
794
  channelStats: Graffiti["channelStats"] = (session) => {
679
- const repeater: ReturnType<typeof Graffiti.prototype.channelStats> =
680
- new Repeater(async (push, stop) => {
681
- const keyPrefix = encodeURIComponent(session.actor) + "/";
682
- const result = await (
683
- await this.db
684
- ).query("indexes/channelStatsPerActor", {
685
- startkey: keyPrefix,
686
- endkey: keyPrefix + "\uffff",
687
- reduce: true,
688
- group: true,
689
- });
690
- for (const row of result.rows) {
691
- const channelEncoded = row.key.split("/")[1];
692
- if (typeof channelEncoded !== "string") continue;
693
- const { count, max: lastModified } = row.value;
694
- if (typeof count !== "number" || typeof lastModified !== "number")
695
- continue;
696
- await push({
697
- value: {
698
- channel: decodeURIComponent(channelEncoded),
699
- count,
700
- lastModified,
701
- },
702
- });
703
- }
704
- stop();
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,
705
805
  });
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
+ })();
821
+ };
706
822
 
707
- return repeater;
823
+ continueObjectStream: Graffiti["continueObjectStream"] = (
824
+ cursor,
825
+ session,
826
+ ) => {
827
+ // TODO: Implement this
828
+ throw new GraffitiErrorNotFound("Cursor not found");
708
829
  };
709
830
  }