@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/dist/browser/index.js +2 -20
- package/dist/browser/index.js.map +4 -4
- package/dist/cjs/database.js +181 -90
- package/dist/cjs/database.js.map +3 -3
- package/dist/cjs/index.js +11 -0
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/utilities.js +3 -7
- package/dist/cjs/utilities.js.map +2 -2
- package/dist/database.d.ts +28 -20
- package/dist/database.d.ts.map +1 -1
- package/dist/esm/database.js +182 -92
- package/dist/esm/database.js.map +3 -3
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +2 -2
- package/dist/esm/utilities.js +3 -7
- package/dist/esm/utilities.js.map +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/utilities.d.ts +1 -2
- package/dist/utilities.d.ts.map +1 -1
- package/package.json +2 -3
- package/src/database.ts +285 -164
- package/src/index.ts +17 -1
- package/src/utilities.ts +2 -14
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
|
-
|
|
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
|
|
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
|
|
46
|
-
*
|
|
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
|
-
|
|
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_:
|
|
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<
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
212
|
-
const
|
|
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:
|
|
217
|
-
endkey:
|
|
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
|
-
|
|
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(
|
|
235
|
-
return
|
|
236
|
+
protected docId(objectUrl: GraffitiObjectUrl) {
|
|
237
|
+
return objectUrl.url + "/" + randomBase64();
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
get: Graffiti["get"] = async (...args) => {
|
|
239
|
-
const [
|
|
241
|
+
const [urlObject, schema, session] = args;
|
|
240
242
|
|
|
241
|
-
const docsAll = await this.allDocsAtLocation(
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
...
|
|
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 [
|
|
372
|
-
const deletedObject = await this.deleteAtLocation(
|
|
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.
|
|
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:
|
|
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,
|
|
461
|
+
const [patch, url, session] = args;
|
|
452
462
|
let originalObject: GraffitiObjectBase;
|
|
453
463
|
try {
|
|
454
|
-
originalObject = await this.get(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
578
|
-
|
|
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
|
|
581
|
-
|
|
609
|
+
for (const row of result.rows) {
|
|
610
|
+
const doc = row.doc;
|
|
611
|
+
if (!doc) continue;
|
|
582
612
|
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
616
|
+
if (!showTombstones && doc.tombstone) continue;
|
|
589
617
|
|
|
590
|
-
|
|
591
|
-
const keyPrefix = encodeURIComponent(channel) + "/";
|
|
592
|
-
const startkey = keyPrefix + startKeySuffix;
|
|
593
|
-
const endkey = keyPrefix + endKeySuffix;
|
|
618
|
+
const object = this.extractGraffitiObject(doc);
|
|
594
619
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
)
|
|
598
|
-
|
|
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
|
-
|
|
603
|
-
const doc = row.doc;
|
|
604
|
-
if (!doc) continue;
|
|
625
|
+
if (!validate(object)) continue;
|
|
605
626
|
|
|
606
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
614
|
-
if (!isActorAllowedGraffitiObject(doc, session)) continue;
|
|
653
|
+
const processedIds = new Set<string>();
|
|
615
654
|
|
|
616
|
-
|
|
617
|
-
// if the user is not the owner
|
|
618
|
-
maskGraffitiObject(object, channels, session);
|
|
655
|
+
const startTime = new Date().getTime();
|
|
619
656
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
661
|
-
|
|
711
|
+
return startTime - LAST_MODIFIED_BUFFER;
|
|
712
|
+
}
|
|
662
713
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
tombstoneRetention:
|
|
671
|
-
this.options.tombstoneRetention ?? DEFAULT_TOMBSTONE_RETENTION,
|
|
672
|
-
};
|
|
673
|
-
});
|
|
751
|
+
})();
|
|
752
|
+
};
|
|
674
753
|
|
|
675
|
-
|
|
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
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
823
|
+
continueObjectStream: Graffiti["continueObjectStream"] = (
|
|
824
|
+
cursor,
|
|
825
|
+
session,
|
|
826
|
+
) => {
|
|
827
|
+
// TODO: Implement this
|
|
828
|
+
throw new GraffitiErrorNotFound("Cursor not found");
|
|
708
829
|
};
|
|
709
830
|
}
|