@graffiti-garden/api 0.2.0 → 0.2.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.
@@ -14,6 +14,7 @@ import type { JSONSchema4 } from "json-schema";
14
14
  *
15
15
  * There are several different implementations of this Graffiti API available,
16
16
  * including a [federated implementation](https://github.com/graffiti-garden/implementation-federated),
17
+ * that lets users choose where their data is stored,
17
18
  * and a [local implementation](https://github.com/graffiti-garden/implementation-local)
18
19
  * that can be used for testing and development. In our design of Graffiti, this API is our
19
20
  * primary focus as it is the layer that shapes the experience
@@ -27,22 +28,28 @@ import type { JSONSchema4 } from "json-schema";
27
28
  *
28
29
  * ## Overview
29
30
  *
30
- * This API tries to draw from well-known concepts and standards wherever possible.
31
- * JSON objects, representing social artifacts (e.g. posts, profiles) and activities
32
- * (e.g. likes, follows) can be interacted with through standard CRUD operations:
31
+ * Graffiti provides applications with methods to create and store data
32
+ * on behalf of their users using standard CRUD operations:
33
33
  * {@link put}, {@link get}, {@link patch}, and {@link delete}.
34
- * Objects can be typed with [JSON Schema](https://json-schema.org/) and patches
35
- * can be applied with [JSON Patch](https://jsonpatch.com).
36
- * For interoperability between Graffiti applications, we recommend using established properties from the
37
- * [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) when available.
34
+ * This data can represent both social artifacts (e.g. posts, profiles) and
35
+ * activities (e.g. likes, follows) and is stored as JSON.
38
36
  *
39
- * The social aspect of Graffiti comes from the {@link discover} operation
37
+ * The social aspect of Graffiti comes from the {@link discover} method
40
38
  * which allows applications to find objects that other users made.
41
39
  * It is a lot like a traditional query operation, but it only
42
40
  * returns objects that have been placed in particular
43
41
  * {@link GraffitiObjectBase.channels | `channels`}
44
42
  * specified by the discovering application.
45
43
  *
44
+ * Graffiti builds on well known concepts and standards wherever possible.
45
+ * JSON Objects can be typed with [JSON Schema](https://json-schema.org/) and patches
46
+ * can be applied with [JSON Patch](https://jsonpatch.com).
47
+ * For interoperability between Graffiti applications, we recommend that
48
+ * objects use established properties from the
49
+ * [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) when available,
50
+ * however it is always possible to create additional properties, contributing
51
+ * to the broader [folksonomy](https://en.wikipedia.org/wiki/Folksonomy).
52
+ *
46
53
  * {@link GraffitiObjectBase.channels | `channels`} are one of the major concepts
47
54
  * unique to Graffiti along with *interaction relativity*.
48
55
  * Channels create boundaries between public spaces and work to prevent
@@ -222,19 +229,17 @@ import type { JSONSchema4 } from "json-schema";
222
229
  *
223
230
  * ## TODO
224
231
  *
225
- * - Test for listChannels and listOrphans,
226
232
  * - Implement scope.
227
233
  *
228
234
  * @groupDescription CRUD Methods
229
235
  * Methods for {@link put | creating}, {@link get | reading}, {@link patch | updating},
230
236
  * and {@link delete | deleting} {@link GraffitiObjectBase | Graffiti objects}.
231
237
  * @groupDescription Query Methods
232
- * Methods for retrieving multiple {@link GraffitiObjectBase | Graffiti objects} at a time.
238
+ * Methods that retrieve or accumulate information about multiple {@link GraffitiObjectBase | Graffiti objects} at a time.
233
239
  * @groupDescription Session Management
234
240
  * Methods and properties for logging in and out of a Graffiti implementation.
235
241
  * @groupDescription Utilities
236
- * Methods for for converting Graffiti objects to and from URIs
237
- * and for finding lost objects.
242
+ * Methods for for converting Graffiti objects to and from URIs.
238
243
  */
239
244
  export declare abstract class Graffiti {
240
245
  /**
@@ -297,14 +302,26 @@ export declare abstract class Graffiti {
297
302
  session: GraffitiSession): Promise<GraffitiObjectBase>;
298
303
  /**
299
304
  * Retrieves an object from a given location.
300
- * If no object exists at that location or if the retrieving
301
- * {@link GraffitiObjectBase.actor | `actor`} is not the creator or included in
302
- * the object's {@link GraffitiObjectBase.allowed | `allowed`} property,
303
- * a {@link GraffitiErrorNotFound} is thrown.
304
305
  *
305
- * The retrieved object is also type-checked against the provided [JSON schema](https://json-schema.org/)
306
+ * The retrieved object is type-checked against the provided [JSON schema](https://json-schema.org/)
306
307
  * otherwise a {@link GraffitiErrorSchemaMismatch} is thrown.
307
308
  *
309
+ * If the object existed but has since been deleted,
310
+ * or the retrieving {@link GraffitiObjectBase.actor | `actor`}
311
+ * was {@link GraffitiObjectBase.allowed | `allowed`} to access
312
+ * the object but now isn't, this method will return the latest
313
+ * version of the object that the {@link GraffitiObjectBase.actor | `actor`}
314
+ * was allowed to access with its {@link GraffitiObjectBase.tombstone | `tombstone`}
315
+ * set to `true`, so long as that version is still cached.
316
+ *
317
+ * Otherwise, if the object never existed, or the
318
+ * retrieving {@link GraffitiObjectBase.actor | `actor`} was never
319
+ * {@link GraffitiObjectBase.allowed | `allowed`} to access it, or if
320
+ * the object was changed long enough ago that its history has been
321
+ * purged from the cache, a {@link GraffitiErrorNotFound} is thrown.
322
+ * The rate at which the cache is purged is implementation dependent.
323
+ * See the `tombstoneReturn` property returned by {@link discover}.
324
+ *
308
325
  * @group CRUD Methods
309
326
  */
310
327
  abstract get<Schema extends JSONSchema4>(
@@ -395,7 +412,7 @@ export declare abstract class Graffiti {
395
412
  * not specified by the `discover` method will not be revealed. This masking happens
396
413
  * before the supplied schema is applied.
397
414
  *
398
- * {@link discover} can be used in conjunction with {@link synchronize}
415
+ * {@link discover} can be used in conjunction with {@link synchronizeDiscover}
399
416
  * to provide a responsive and consistent user experience.
400
417
  *
401
418
  * Since different implementations may fetch data from multiple sources there is
@@ -460,121 +477,60 @@ export declare abstract class Graffiti {
460
477
  tombstoneRetention: number;
461
478
  }>;
462
479
  /**
463
- * This method has the same signature as {@link discover} but listens for
464
- * changes made via {@link put}, {@link patch}, and {@link delete} or
465
- * fetched from {@link get} or {@link discover} and then streams appropriate
466
- * changes to provide a responsive and consistent user experience.
467
- *
468
- * Unlike {@link discover}, this method continuously listens for changes
469
- * and will not terminate unless the user calls the `return` method on the iterator
470
- * or `break`s out of the loop.
471
- *
472
- * Example 1: Suppose a user publishes a post using {@link put}. If the feed
473
- * displaying that user's posts is using {@link synchronize} to listen for changes,
474
- * then the user's new post will instantly appear in their feed, giving the UI a
475
- * responsive feel.
476
- *
477
- * Example 2: Suppose one of a user's friends changes their name. As soon as the
478
- * user's application receives one notice of that change (using {@link get}
479
- * or {@link discover}), then {@link synchronize} listeners can be used to update
480
- * all instance's of that friend's name in the user's application instantly,
481
- * providing a consistent user experience.
480
+ * Discovers objects **not** contained in any
481
+ * {@link GraffitiObjectBase.channels | `channels`}
482
+ * that were created by the querying {@link GraffitiObjectBase.actor | `actor`}
483
+ * and match the given [JSON Schema](https://json-schema.org).
484
+ * Unlike {@link discover}, this method will not return objects created by other users.
482
485
  *
483
- * @group Synchronize Methods
484
- */
485
- abstract synchronizeDiscover<Schema extends JSONSchema4>(
486
- /**
487
- * The {@link GraffitiObjectBase.channels | `channels`} that the objects must be associated with.
488
- */
489
- channels: string[],
490
- /**
491
- * A [JSON Schema](https://json-schema.org) that objects must satisfy.
492
- */
493
- schema: Schema,
494
- /**
495
- * An implementation-specific object with information to authenticate the
496
- * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
497
- * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
498
- * property will be returned.
499
- */
500
- session?: GraffitiSession | null): GraffitiStream<GraffitiObject<Schema>>;
501
- /**
502
- * This method has the same signature as {@link get} but, like {@link synchronizeDiscover},
503
- * it listens for changes made via {@link put}, {@link patch}, and {@link delete} or
504
- * fetched from {@link get} or {@link discover} and then streams appropriate
505
- * changes to provide a responsive and consistent user experience.
486
+ * This method is not useful for most applications, but necessary for
487
+ * getting a global view of all a user's Graffiti data or debugging
488
+ * channel usage.
506
489
  *
507
- * Unlike {@link get}, which returns a single result, this method continuously
508
- * listens for changes which are output as an asynchronous {@link GraffitiStream}.
490
+ * It's return value is the same as {@link discover}.
509
491
  *
510
- * @group Synchronize Methods
511
- */
512
- abstract synchronizeGet<Schema extends JSONSchema4>(
513
- /**
514
- * The location of the object to get.
492
+ * @group Query Methods
515
493
  */
516
- locationOrUri: GraffitiLocation | string,
494
+ abstract recoverOrphans<Schema extends JSONSchema4>(
517
495
  /**
518
- * The JSON schema to validate the retrieved object against.
496
+ * A [JSON Schema](https://json-schema.org) that orphaned objects must satisfy.
519
497
  */
520
498
  schema: Schema,
521
499
  /**
522
500
  * An implementation-specific object with information to authenticate the
523
- * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
524
- * the retrieved object's {@link GraffitiObjectBase.allowed | `allowed`}
525
- * property must be `undefined`.
501
+ * {@link GraffitiObjectBase.actor | `actor`}.
526
502
  */
527
- session?: GraffitiSession | null): GraffitiStream<GraffitiObject<Schema>>;
503
+ session: GraffitiSession): GraffitiStream<GraffitiObject<Schema>, {
504
+ tombstoneRetention: number;
505
+ }>;
528
506
  /**
529
- * Returns a list of all {@link GraffitiObjectBase.channels | `channels`}
507
+ * Returns statistics about all the {@link GraffitiObjectBase.channels | `channels`}
530
508
  * that an {@link GraffitiObjectBase.actor | `actor`} has posted to.
531
509
  * This is not very useful for most applications, but
532
510
  * necessary for certain applications where a user wants a
533
511
  * global view of all their Graffiti data or to debug
534
512
  * channel usage.
535
513
  *
536
- * @group Utilities
514
+ * @group Query Methods
537
515
  *
538
- * @returns A stream the {@link GraffitiObjectBase.channels | `channel`}s
516
+ * @returns A stream of all {@link GraffitiObjectBase.channels | `channel`}s
539
517
  * that the {@link GraffitiObjectBase.actor | `actor`} has posted to.
540
518
  * The `lastModified` field is the time that the user last modified an
541
519
  * object in that channel. The `count` field is the number of objects
542
520
  * that the user has posted to that channel.
521
+ * {@link GraffitiObjectBase.tombstone | `tombstone`}d objects are not included
522
+ * in either the `count` or `lastModified` fields.
543
523
  */
544
- abstract listChannels(
524
+ abstract channelStats(
545
525
  /**
546
526
  * An implementation-specific object with information to authenticate the
547
527
  * {@link GraffitiObjectBase.actor | `actor`}.
548
528
  */
549
529
  session: GraffitiSession): GraffitiStream<{
550
530
  channel: string;
551
- lastModified: number;
552
531
  count: number;
532
+ lastModified: number;
553
533
  }>;
554
- /**
555
- * Returns a list of all {@link GraffitiObjectBase | objects} a user has posted that are
556
- * not associated with any {@link GraffitiObjectBase.channels | `channel`}, i.e. orphaned objects.
557
- * This is not very useful for most applications, but
558
- * necessary for certain applications where a user wants a
559
- * global view of all their Graffiti data or to debug
560
- * channel usage.
561
- *
562
- * @group Utilities
563
- *
564
- * @returns A stream of the {@link GraffitiObjectBase.name | `name`}
565
- * and {@link GraffitiObjectBase.source | `source`} of the orphaned objects
566
- * that the {@link GraffitiObjectBase.actor | `actor`} has posted to.
567
- * The {@link GraffitiObjectBase.lastModified | lastModified} field is the
568
- * time that the user last modified the orphan.
569
- */
570
- abstract listOrphans(session: GraffitiSession): GraffitiStream<{
571
- name: string;
572
- source: string;
573
- lastModified: string;
574
- }>;
575
- /**
576
- * The age at which a query for a session will be considered expired.
577
- */
578
534
  /**
579
535
  * Begins the login process. Depending on the implementation, this may
580
536
  * involve redirecting the user to a login page or opening a popup,
@@ -642,6 +598,96 @@ export declare abstract class Graffiti {
642
598
  * @group Session Management
643
599
  */
644
600
  abstract readonly sessionEvents: EventTarget;
601
+ /**
602
+ * This method has the same signature as {@link discover} but listens for
603
+ * changes made via {@link put}, {@link patch}, and {@link delete} or
604
+ * fetched from {@link get}, {@link discover}, and {@link recoverOrphans}
605
+ * and then streams appropriate changes to provide a responsive and
606
+ * consistent user experience.
607
+ *
608
+ * Unlike {@link discover}, this method continuously listens for changes
609
+ * and will not terminate unless the user calls the `return` method on the iterator
610
+ * or `break`s out of the loop.
611
+ *
612
+ * Example 1: Suppose a user publishes a post using {@link put}. If the feed
613
+ * displaying that user's posts is using {@link synchronizeDiscover} to listen for changes,
614
+ * then the user's new post will instantly appear in their feed, giving the UI a
615
+ * responsive feel.
616
+ *
617
+ * Example 2: Suppose one of a user's friends changes their name. As soon as the
618
+ * user's application receives one notice of that change (using {@link get}
619
+ * or {@link discover}), then {@link synchronizeDiscover} listeners can be used to update
620
+ * all instance's of that friend's name in the user's application instantly,
621
+ * providing a consistent user experience.
622
+ *
623
+ * @group Synchronize Methods
624
+ */
625
+ abstract synchronizeDiscover<Schema extends JSONSchema4>(
626
+ /**
627
+ * The {@link GraffitiObjectBase.channels | `channels`} that the objects must be associated with.
628
+ */
629
+ channels: string[],
630
+ /**
631
+ * A [JSON Schema](https://json-schema.org) that objects must satisfy.
632
+ */
633
+ schema: Schema,
634
+ /**
635
+ * An implementation-specific object with information to authenticate the
636
+ * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
637
+ * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
638
+ * property will be returned.
639
+ */
640
+ session?: GraffitiSession | null): GraffitiStream<GraffitiObject<Schema>>;
641
+ /**
642
+ * This method has the same signature as {@link get} but, like {@link synchronizeDiscover},
643
+ * it listens for changes made via {@link put}, {@link patch}, and {@link delete} or
644
+ * fetched from {@link get}, {@link discover}, and {@link recoverOrphans} and then
645
+ * streams appropriate changes to provide a responsive and consistent user experience.
646
+ *
647
+ * Unlike {@link get}, which returns a single result, this method continuously
648
+ * listens for changes which are output as an asynchronous {@link GraffitiStream}.
649
+ *
650
+ * @group Synchronize Methods
651
+ */
652
+ abstract synchronizeGet<Schema extends JSONSchema4>(
653
+ /**
654
+ * The location of the object to get.
655
+ */
656
+ locationOrUri: GraffitiLocation | string,
657
+ /**
658
+ * The JSON schema to validate the retrieved object against.
659
+ */
660
+ schema: Schema,
661
+ /**
662
+ * An implementation-specific object with information to authenticate the
663
+ * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
664
+ * the retrieved object's {@link GraffitiObjectBase.allowed | `allowed`}
665
+ * property must be `undefined`.
666
+ */
667
+ session?: GraffitiSession | null): GraffitiStream<GraffitiObject<Schema>>;
668
+ /**
669
+ * This method has the same signature as {@link recoverOrphans} but,
670
+ * like {@link synchronizeDiscover}, it listens for changes made via
671
+ * {@link put}, {@link patch}, and {@link delete} or fetched from
672
+ * {@link get}, {@link discover}, and {@link recoverOrphans} and then
673
+ * streams appropriate changes to provide a responsive and consistent user experience.
674
+ *
675
+ * Unlike {@link recoverOrphans}, this method continuously listens for changes
676
+ * and will not terminate unless the user calls the `return` method on the iterator
677
+ * or `break`s out of the loop.
678
+ *
679
+ * @group Synchronize Methods
680
+ */
681
+ abstract synchronizeRecoverOrphans<Schema extends JSONSchema4>(
682
+ /**
683
+ * A [JSON Schema](https://json-schema.org) that orphaned objects must satisfy.
684
+ */
685
+ schema: Schema,
686
+ /**
687
+ * An implementation-specific object with information to authenticate the
688
+ * {@link GraffitiObjectBase.actor | `actor`}.
689
+ */
690
+ session: GraffitiSession): GraffitiStream<GraffitiObject<Schema>>;
645
691
  }
646
692
  /**
647
693
  * This is a factory function that produces an instance of
@@ -1 +1 @@
1
- {"version":3,"file":"1-api.d.ts","sourceRoot":"","sources":["../../src/1-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"1-api.d.ts","sourceRoot":"","sources":["../../src/1-api.ts"],"names":[],"mappings":"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"}
@@ -221,7 +221,7 @@ export interface GraffitiPatch {
221
221
  /**
222
222
  * This type represents a stream of data that are
223
223
  * returned by Graffiti's query-like operations such as
224
- * {@link Graffiti.discover} and {@link Graffiti.listChannels}.
224
+ * {@link Graffiti.discover} and {@link Graffiti.recoverOrphans}.
225
225
  *
226
226
  * Errors are returned within the stream rather than as
227
227
  * exceptions that would halt the entire stream. This is because
@@ -273,8 +273,11 @@ export type GraffitiLogoutEvent = CustomEvent<{
273
273
  * and restore any previously active sessions.
274
274
  * Successful session restores will be returned in parallel as
275
275
  * their own {@link GraffitiLoginEvent} events.
276
- * This event optionally return an `href` property
277
- * if there were any redirects during the restoration process.
276
+ *
277
+ * This event optionally returns an `href` property
278
+ * representing the URL the user originated a login request
279
+ * from, which may be useful for redirecting the user back to
280
+ * the page they were on after login.
278
281
  * The event name to listen for is `initialized`.
279
282
  */
280
283
  export type GraffitiSessionInitializedEvent = CustomEvent<{
@@ -1 +1 @@
1
- {"version":3,"file":"2-types.d.ts","sourceRoot":"","sources":["../../src/2-types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"2-types.d.ts","sourceRoot":"","sources":["../../src/2-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,IAAI,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;OAKG;IACH,KAAK,EAAE,EAAE,CAAC;IAEV;;;;;;;;;;;;OAYG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAC;IAEnB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAE1B;;;;;;;;;;;;OAYG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;;;;;;;OASG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,CAAC,MAAM,IAAI,kBAAkB,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;AAE9E;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,kBAAkB,EAClB,OAAO,GAAG,MAAM,GAAG,QAAQ,CAC5B,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,iBAAiB,CAAC,MAAM,IAAI,IAAI,CAC1C,kBAAkB,EAClB,OAAO,GAAG,UAAU,GAAG,SAAS,CACjC,GACC,OAAO,CAAC,gBAAgB,CAAC,GACzB,WAAW,CAAC,MAAM,CAAC,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,KAAK,CAAC,EAAE,EAAE,CAAC;CACZ;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAE7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAEhC;;;;OAIG;IACH,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAChC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,IAAI,cAAc,CAC/D;IACE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IACE,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,EACH,OAAO,CACR,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,WAAW,CACxC;IACE,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB,GACD;IACE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,eAAe,CAAC;CAC1B,CACJ,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,WAAW,CACzC;IACE,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACD;IACE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CACJ,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,+BAA+B,GAAG,WAAW,CACrD;IACE,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GACD,IAAI,GACJ,SAAS,CACZ,CAAC"}
@@ -1,2 +1,2 @@
1
- import{assert as e,describe as a,it as t,expect as o}from"vitest";import{GraffitiErrorInvalidUri as n,GraffitiErrorNotFound as l,GraffitiErrorForbidden as s,GraffitiErrorInvalidSchema as i,GraffitiErrorSchemaMismatch as r,GraffitiErrorPatchTestFailed as c,GraffitiErrorPatchError as u}from"@graffiti-garden/api";function d(){const e=new Uint8Array(16);return crypto.getRandomValues(e),Array.from(e).map((e=>e.toString(16).padStart(2,"0"))).join("")}function v(){return{value:{[d()]:d()},channels:[d(),d()]}}async function h(a){const t=await a.next();return e(!t.done&&!t.value.error,"result has no value"),t.value.value}const w=e=>{a("URI and location conversion",(()=>{t("location to uri and back",(async()=>{const a=e(),t={name:d(),actor:d(),source:d()},n=a.locationToUri(t),l=a.uriToLocation(n);o(t).toEqual(l)})),t("collision resistance",(async()=>{const a=e(),t={name:d(),actor:d(),source:d()};for(const e of["name","actor","source"]){const n={...t,[e]:d()},l=a.locationToUri(t),s=a.locationToUri(n);o(l).not.toEqual(s)}})),t("random URI should not be a valid location",(async()=>{const a=e();o((()=>a.uriToLocation(""))).toThrow(n)}))}))},p=(e,n,h)=>{a("CRUD",{timeout:2e4},(()=>{t("put, get, delete",(async()=>{const a=e(),t=n(),s={something:"hello, world~ c:"},i=[d(),d()],r=await a.put({value:s,channels:i},t);o(r.value).toEqual({}),o(r.channels).toEqual([]),o(r.allowed).toBeUndefined(),o(r.actor).toEqual(t.actor);const c=await a.get(r,{});o(c.value).toEqual(s),o(c.channels).toEqual([]),o(c.allowed).toBeUndefined(),o(c.name).toEqual(r.name),o(c.actor).toEqual(r.actor),o(c.source).toEqual(r.source),o(c.lastModified).toEqual(r.lastModified);const u={something:"goodbye, world~ :c"},v=await a.put({...r,value:u,channels:[]},t);o(v.value).toEqual(s),o(v.tombstone).toEqual(!0),o(v.name).toEqual(r.name),o(v.actor).toEqual(r.actor),o(v.source).toEqual(r.source),o(v.lastModified).toBeGreaterThanOrEqual(c.lastModified);const h=await a.get(r,{});o(h.value).toEqual(u),o(h.lastModified).toEqual(v.lastModified),o(h.tombstone).toEqual(!1);const w=await a.delete(h,t);o(w.tombstone).toEqual(!0),o(w.value).toEqual(u),o(w.lastModified).toBeGreaterThanOrEqual(v.lastModified),await o(a.get(h,{})).rejects.toThrow(l)})),t("put, get, delete with wrong actor",(async()=>{const a=e(),t=n(),l=h();await o(a.put({value:{},channels:[],actor:l.actor},t)).rejects.toThrow(s);const i=await a.put({value:{},channels:[]},l);await o(a.delete(i,t)).rejects.toThrow(s),await o(a.patch({},i,t)).rejects.toThrow(s)})),t("put and get with schema",(async()=>{const a=e(),t=n(),l={something:"hello",another:42},s=await a.put({value:l,channels:[]},t),i=await a.get(s,{properties:{value:{properties:{something:{type:"string"},another:{type:"integer"}}}}});o(i.value.something).toEqual(l.something),o(i.value.another).toEqual(l.another)})),t("put and get with invalid schema",(async()=>{const a=e(),t=n(),l=await a.put({value:{},channels:[]},t);await o(a.get(l,{properties:{value:{type:"asdf"}}})).rejects.toThrow(i)})),t("put and get with wrong schema",(async()=>{const a=e(),t=n(),l=await a.put({value:{hello:"world"},channels:[]},t);await o(a.get(l,{properties:{value:{properties:{hello:{type:"number"}}}}})).rejects.toThrow(r)})),t("put and get with empty access control",(async()=>{const a=e(),t=n(),l=h(),s={um:"hi"},i=[d()],r=[d()],c=await a.put({value:s,allowed:i,channels:r},t),u=await a.get(c,{},t);o(u.value).toEqual(s),o(u.allowed).toEqual(i),o(u.channels).toEqual(r),await o(a.get(c,{})).rejects.toThrow(),await o(a.get(c,{},l)).rejects.toThrow()})),t("put and get with specific access control",(async()=>{const a=e(),t=n(),l=h(),s={um:"hi"},i=[d(),l.actor,d()],r=[d()],c=await a.put({value:s,allowed:i,channels:r},t),u=await a.get(c,{},t);o(u.value).toEqual(s),o(u.allowed).toEqual(i),o(u.channels).toEqual(r),await o(a.get(c,{})).rejects.toThrow();const v=await a.get(c,{},l);o(v.value).toEqual(s),o(v.allowed).toEqual([l.actor]),o(v.channels).toEqual([])})),t("patch value",(async()=>{const a=e(),t=n(),l={something:"hello, world~ c:"},s=await a.put({value:l,channels:[]},t),i=await a.patch({value:[{op:"replace",path:"/something",value:"goodbye, world~ :c"}]},s,t);o(i.value).toEqual(l),o(i.tombstone).toBe(!0);const r=await a.get(s,{});o(r.value).toEqual({something:"goodbye, world~ :c"}),o(i.lastModified).toBe(r.lastModified),await a.delete(s,t)})),t("deep patch",(async()=>{const a=e(),t=n(),l={something:{another:{somethingElse:"hello"}}},s=await a.put({value:l,channels:[]},t),i=await a.patch({value:[{op:"replace",path:"/something/another/somethingElse",value:"goodbye"}]},s,t),r=await a.get(s,{});o(i.value).toEqual(l),o(r.value).toEqual({something:{another:{somethingElse:"goodbye"}}})})),t("patch channels",(async()=>{const a=e(),t=n(),l=[d()],s=[d()],i=await a.put({value:{},channels:l},t),r={channels:[{op:"replace",path:"/0",value:s[0]}]},c=await a.patch(r,i,t);o(c.channels).toEqual(l);const u=await a.get(i,{},t);o(u.channels).toEqual(s),await a.delete(i,t)})),t("patch 'increment' with test",(async()=>{const a=e(),t=n(),l=await a.put({value:{counter:1},channels:[]},t),s=await a.patch({value:[{op:"test",path:"/counter",value:1},{op:"replace",path:"/counter",value:2}]},l,t);o(s.value).toEqual({counter:1});const i=await a.get(s,{properties:{value:{properties:{counter:{type:"integer"}}}}});o(i.value.counter).toEqual(2),await o(a.patch({value:[{op:"test",path:"/counter",value:1},{op:"replace",path:"/counter",value:3}]},l,t)).rejects.toThrow(c)})),t("invalid patch",(async()=>{const a=e(),t=n(),l=v(),s=await a.put(l,t);await o(a.patch({value:[{op:"add",path:"/root",value:[]},{op:"add",path:"/root/2",value:2}]},s,t)).rejects.toThrow(u)})),t("patch channels to be wrong",(async()=>{const a=e(),t=n(),l=v();l.allowed=[d()];const s=await a.put(l,t),i=[{channels:[{op:"replace",path:"",value:null}]},{channels:[{op:"replace",path:"",value:{}}]},{channels:[{op:"replace",path:"",value:["hello",["hi"]]}]},{channels:[{op:"add",path:"/0",value:1}]},{value:[{op:"replace",path:"",value:"not an object"}]},{value:[{op:"replace",path:"",value:null}]},{value:[{op:"replace",path:"",value:[]}]},{allowed:[{op:"replace",path:"",value:{}}]},{allowed:[{op:"replace",path:"",value:["hello",["hi"]]}]}];for(const e of i)await o(a.patch(e,s,t)).rejects.toThrow(u);const r=await a.get(s,{},t);o(r.value).toEqual(l.value),o(r.channels).toEqual(l.channels),o(r.allowed).toEqual(l.allowed),o(r.lastModified).toEqual(s.lastModified)}))}))},m=(n,l,s)=>{a("synchronizeDiscover",(()=>{t("get",(async()=>{const e=n(),a=l(),t=v(),s=t.channels.slice(1),i=await e.put(t,a),r=n(),c=r.synchronizeDiscover(s,{}).next(),u=await r.get(i,{},a),d=(await c).value;if(!d||d.error)throw new Error("Error in synchronize");o(d.value.value).toEqual(t.value),o(d.value.channels).toEqual(s),o(d.value.tombstone).toBe(!1),o(d.value.lastModified).toEqual(u.lastModified)})),t("put",(async()=>{const e=n(),a=l(),t=d(),s=d(),i=d(),r={hello:"world"},c=[t,i],u=await e.put({value:r,channels:c},a),v=e.synchronizeDiscover([t],{}).next(),h=e.synchronizeDiscover([s],{}).next(),w=e.synchronizeDiscover([i],{}).next(),p={goodbye:"world"},m=[s,i];await e.put({...u,value:p,channels:m},a);const E=(await v).value,q=(await h).value,y=(await w).value;if(!E||E.error||!q||q.error||!y||y.error)throw new Error("Error in synchronize");o(E.value.value).toEqual(r),o(E.value.channels).toEqual([t]),o(E.value.tombstone).toBe(!0),o(q.value.value).toEqual(p),o(q.value.channels).toEqual([s]),o(q.value.tombstone).toBe(!1),o(y.value.value).toEqual(p),o(y.value.channels).toEqual([i]),o(y.value.tombstone).toBe(!1),o(E.value.lastModified).toEqual(q.value.lastModified),o(y.value.lastModified).toEqual(q.value.lastModified)})),t("patch",(async()=>{const e=n(),a=l(),t=d(),s=d(),i=d(),r={hello:"world"},c=[t,i],u=await e.put({value:r,channels:c},a),v=e.synchronizeDiscover([t],{}).next(),h=e.synchronizeDiscover([s],{}).next(),w=e.synchronizeDiscover([i],{}).next();await e.patch({value:[{op:"add",path:"/something",value:"new value"}],channels:[{op:"add",path:"/-",value:s},{op:"remove",path:`/${c.indexOf(t)}`}]},u,a);const p=(await v).value,m=(await h).value,E=(await w).value;if(!p||p.error||!m||m.error||!E||E.error)throw new Error("Error in synchronize");const q={...r,something:"new value"};o(p.value.value).toEqual(r),o(p.value.channels).toEqual([t]),o(p.value.tombstone).toBe(!0),o(m.value.value).toEqual(q),o(m.value.channels).toEqual([s]),o(m.value.tombstone).toBe(!1),o(E.value.value).toEqual(q),o(E.value.channels).toEqual([i]),o(E.value.tombstone).toBe(!1),o(p.value.lastModified).toEqual(m.value.lastModified),o(E.value.lastModified).toEqual(m.value.lastModified)})),t("delete",(async()=>{const e=n(),a=l(),t=[d(),d(),d()],s={hello:"world"},i=[d(),...t.slice(1)],r=await e.put({value:s,channels:i},a),c=e.synchronizeDiscover(t,{}).next();e.delete(r,a);const u=(await c).value;if(!u||u.error)throw new Error("Error in synchronize");o(u.value.tombstone).toBe(!0),o(u.value.value).toEqual(s),o(u.value.channels).toEqual(t.filter((e=>i.includes(e))))})),t("not allowed",(async()=>{const e=n(),a=l(),t=s(),i=[d(),d(),d()],r=i.slice(1),c=e.synchronizeDiscover(r,{},a).next(),u=e.synchronizeDiscover(r,{},t).next(),v=e.synchronizeDiscover(r,{}).next(),h={hello:"world"},w=[d(),t.actor];await e.put({value:h,channels:i,allowed:w},a),await o(Promise.race([v,new Promise(((e,a)=>setTimeout(a,100,"Timeout")))])).rejects.toThrow("Timeout");const p=(await c).value,m=(await u).value;if(!p||p.error||!m||m.error)throw new Error("Error in synchronize");o(p.value.value).toEqual(h),o(p.value.allowed).toEqual(w),o(p.value.channels).toEqual(i),o(m.value.value).toEqual(h),o(m.value.allowed).toEqual([t.actor]),o(m.value.channels).toEqual(r)}))})),a("synchronizeGet",(()=>{t("replace, delete",(async()=>{const a=n(),t=l(),s=v(),i=await a.put(s,t),r=a.synchronizeGet(i,{}),c=r.next(),u={goodbye:"world"},d=await a.put({...i,value:u},t),h=(await c).value;e(h&&!h.error),o(h.value.value).toEqual(u),o(h.value.actor).toEqual(t.actor),o(h.value.channels).toEqual([]),o(h.value.tombstone).toBe(!1),o(h.value.lastModified).toEqual(d.lastModified),o(h.value.allowed).toBeUndefined();const w=await a.delete(d,t),p=(await r.next()).value;e(p&&!p.error),o(p.value.tombstone).toBe(!0),o(p.value.lastModified).toEqual(w.lastModified),await a.put(v(),t),await o(Promise.race([r.next(),new Promise(((e,a)=>setTimeout(a,100,"Timeout")))])).rejects.toThrow("Timeout")})),t("not allowed",(async()=>{const a=n(),t=l(),i=s(),r=v(),c=await a.put(r,t),u=a.synchronizeGet(c,{},t),d=a.synchronizeGet(c,{},i),h=u.next(),w=d.next(),p={goodbye:"world"},m=await a.put({...c,...r,allowed:[],value:p},t),E=(await h).value,q=(await w).value;e(E&&!E.error),e(q&&!q.error),o(E.value.value).toEqual(p),o(q.value.value).toEqual(r.value),o(E.value.actor).toEqual(t.actor),o(q.value.actor).toEqual(t.actor),o(E.value.channels).toEqual(r.channels),o(q.value.channels).toEqual([]),o(E.value.tombstone).toBe(!1),o(q.value.tombstone).toBe(!0),o(E.value.lastModified).toEqual(m.lastModified),o(q.value.lastModified).toEqual(m.lastModified)}))}))},E=(n,l,s)=>{a("discover",{timeout:2e4},(()=>{t("discover nothing",(async()=>{const e=n().discover([],{});o(await e.next()).toHaveProperty("done",!0)})),t("discover single",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a),i=[d(),t.channels[0]],r=e.discover(i,{}),c=await h(r);o(c.value).toEqual(t.value),o(c.channels).toEqual([t.channels[0]]),o(c.allowed).toBeUndefined(),o(c.actor).toEqual(a.actor),o(c.tombstone).toBe(!1),o(c.lastModified).toEqual(s.lastModified);const u=await r.next();o(u.done).toBe(!0)})),t("discover wrong channel",(async()=>{const e=n(),a=l(),t=v();await e.put(t,a);const s=e.discover([d()],{});await o(s.next()).resolves.toHaveProperty("done",!0)})),t("discover not allowed",(async()=>{const e=n(),a=l(),t=s(),i=v();i.allowed=[d(),d()];const r=await e.put(i,a),c=e.discover(i.channels,{},a),u=await h(c);o(u.value).toEqual(i.value),o(u.channels).toEqual(i.channels),o(u.allowed).toEqual(i.allowed),o(u.actor).toEqual(a.actor),o(u.tombstone).toBe(!1),o(u.lastModified).toEqual(r.lastModified);const w=e.discover(i.channels,{},t);o(await w.next()).toHaveProperty("done",!0);const p=e.discover(i.channels,{});o(await p.next()).toHaveProperty("done",!0)})),t("discover allowed",(async()=>{const e=n(),a=l(),t=s(),i=v();i.allowed=[d(),t.actor,d()];const r=await e.put(i,a),c=e.discover(i.channels,{},t),u=await h(c);o(u.value).toEqual(i.value),o(u.allowed).toEqual([t.actor]),o(u.channels).toEqual(i.channels),o(u.actor).toEqual(a.actor),o(u.tombstone).toBe(!1),o(u.lastModified).toEqual(r.lastModified)}));for(const e of["name","actor","lastModified"])t(`discover for ${e}`,(async()=>{const a=n(),t=l(),i=s(),r=v(),c=await a.put(r,t),u=v();u.channels=r.channels,await new Promise((e=>setTimeout(e,20)));const d=await a.put(u,i),w=a.discover(r.channels,{properties:{[e]:{enum:[c[e]]}}}),p=await h(w);o(p.name).toEqual(c.name),o(p.name).not.toEqual(d.name),o(p.value).toEqual(r.value),await o(w.next()).resolves.toHaveProperty("done",!0)}));t("discover with lastModified range",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a);await new Promise((e=>setTimeout(e,20)));const i=await e.put(t,a);o(s.name).not.toEqual(i.name),o(s.lastModified).toBeLessThan(i.lastModified);const r=e.discover([t.channels[0]],{properties:{lastModified:{minimum:i.lastModified,exclusiveMinimum:!0}}});o(await r.next()).toHaveProperty("done",!0);const c=e.discover([t.channels[0]],{properties:{lastModified:{minimum:i.lastModified-.1,exclusiveMinimum:!0}}}),u=await h(c);o(u.name).toEqual(i.name),o(await c.next()).toHaveProperty("done",!0);const d=e.discover(t.channels,{properties:{value:{},lastModified:{minimum:i.lastModified}}}),w=await h(d);o(w.name).toEqual(i.name),o(await d.next()).toHaveProperty("done",!0);const p=e.discover(t.channels,{properties:{lastModified:{minimum:i.lastModified+.1}}});o(await p.next()).toHaveProperty("done",!0);const m=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified,exclusiveMaximum:!0}}});o(await m.next()).toHaveProperty("done",!0);const E=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified+.1,exclusiveMaximum:!0}}}),q=await h(E);o(q.name).toEqual(s.name),o(await E.next()).toHaveProperty("done",!0);const y=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified}}}),f=await h(y);o(f.name).toEqual(s.name),o(await y.next()).toHaveProperty("done",!0);const g=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified-.1}}});o(await g.next()).toHaveProperty("done",!0)})),t("discover schema allowed, as and not as owner",(async()=>{const e=n(),a=l(),t=s(),i=v();i.allowed=[d(),t.actor,d()],await e.put(i,a);const r=e.discover(i.channels,{properties:{allowed:{minItems:3,not:{items:{not:{enum:[t.actor]}}}}}},a),c=await h(r);o(c.value).toEqual(i.value),await o(r.next()).resolves.toHaveProperty("done",!0);const u=e.discover(i.channels,{properties:{allowed:{minItems:3}}},t);await o(u.next()).resolves.toHaveProperty("done",!0);const w=e.discover(i.channels,{properties:{allowed:{not:{items:{not:{enum:[i.channels[0]]}}}}}},t);await o(w.next()).resolves.toHaveProperty("done",!0);const p=e.discover(i.channels,{properties:{allowed:{maxItems:1,not:{items:{not:{enum:[t.actor]}}}}}},t),m=await h(p);o(m.value).toEqual(i.value),await o(p.next()).resolves.toHaveProperty("done",!0)})),t("discover schema channels, as and not as owner",(async()=>{const e=n(),a=l(),t=s(),i=v();i.channels=[d(),d(),d()],await e.put(i,a);const r=e.discover([i.channels[0],i.channels[2]],{properties:{channels:{minItems:3,not:{items:{not:{enum:[i.channels[1]]}}}}}},a),c=await h(r);o(c.value).toEqual(i.value),await o(r.next()).resolves.toHaveProperty("done",!0);const u=e.discover([i.channels[0],i.channels[2]],{properties:{channels:{minItems:3}}},t);await o(u.next()).resolves.toHaveProperty("done",!0);const w=e.discover([i.channels[0],i.channels[2]],{properties:{channels:{not:{items:{not:{enum:[i.channels[1]]}}}}}},t);await o(w.next()).resolves.toHaveProperty("done",!0);const p=e.discover([i.channels[0],i.channels[2]],{properties:{allowed:{maxItems:2,not:{items:{not:{enum:[i.channels[2]]}}}}}},t),m=await h(p);o(m.value).toEqual(i.value),await o(p.next()).resolves.toHaveProperty("done",!0)})),t("discover query for empty allowed",(async()=>{const e=n(),a=l(),t=v(),s={not:{required:["allowed"]}};await e.put(t,a);const i=e.discover(t.channels,s,a),r=await h(i);o(r.value).toEqual(t.value),o(r.allowed).toBeUndefined(),await o(i.next()).resolves.toHaveProperty("done",!0);const c=v();c.allowed=[],await e.put(c,a);const u=e.discover(c.channels,s,a);await o(u.next()).resolves.toHaveProperty("done",!0)})),t("discover query for values",(async()=>{const a=n(),t=l(),s=v();s.value={test:d()},await a.put(s,t);const i=v();i.channels=s.channels,i.value={test:d(),something:d()},await a.put(i,t);const r=v();r.channels=s.channels,r.value={other:d(),something:d()},await a.put(r,t);const c=new Map;for(const t of["test","something","other"]){let o=0;for await(const n of a.discover(s.channels,{properties:{value:{required:[t]}}}))e(!n.error,"result has error"),t in n.value.value&&o++;c.set(t,o)}o(c.get("test")).toBe(2),o(c.get("something")).toBe(2),o(c.get("other")).toBe(1)})),t("discover for deleted content",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a),i=await e.delete(s,a),r=e.discover(t.channels,{}),c=await h(r);o(c.tombstone).toBe(!0),o(c.value).toEqual(t.value),o(c.channels).toEqual(t.channels),o(c.actor).toEqual(a.actor),o(c.lastModified).toEqual(i.lastModified),await o(r.next()).resolves.toHaveProperty("done",!0)})),t("discover for replaced channels",(async()=>{for(let e=0;e<10;e++){const e=n(),a=l(),t=v(),s=await e.put(t,a),i=v(),r=await e.put({...s,...i},a),c=e.discover(t.channels,{}),u=await h(c);await o(c.next()).resolves.toHaveProperty("done",!0);const d=e.discover(i.channels,{}),w=await h(d);await o(d.next()).resolves.toHaveProperty("done",!0),s.lastModified===r.lastModified?(o(u.tombstone||w.tombstone).toBe(!0),o(u.tombstone&&w.tombstone).toBe(!1)):(o(u.tombstone).toBe(!0),o(u.value).toEqual(t.value),o(u.channels).toEqual(t.channels),o(u.lastModified).toEqual(r.lastModified),o(w.tombstone).toBe(!1),o(w.value).toEqual(i.value),o(w.channels).toEqual(i.channels),o(w.lastModified).toEqual(r.lastModified))}})),t("discover for patched allowed",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a);await e.patch({allowed:[{op:"add",path:"",value:[]}]},s,a);const i=e.discover(t.channels,{}),r=await h(i);o(r.tombstone).toBe(!0),o(r.value).toEqual(t.value),o(r.channels).toEqual(t.channels),o(r.allowed).toBeUndefined(),await o(i.next()).resolves.toHaveProperty("done",!0)})),t("put concurrently and discover one",(async()=>{const a=n(),t=l(),s=v();s.name=d();const i=Array(100).fill(0).map((()=>a.put(s,t)));await Promise.all(i);const r=a.discover(s.channels,{});let c=0,u=0;for await(const a of r)e(!a.error,"result has error"),a.value.tombstone?c++:u++;o(c).toBe(99),o(u).toBe(1)}))}))};export{p as graffitiCRUDTests,E as graffitiDiscoverTests,w as graffitiLocationTests,m as graffitiSynchronizeTests};
1
+ import{assert as e,describe as a,it as t,expect as o}from"vitest";import{GraffitiErrorInvalidUri as n,GraffitiErrorNotFound as l,GraffitiErrorForbidden as s,GraffitiErrorInvalidSchema as i,GraffitiErrorSchemaMismatch as c,GraffitiErrorPatchTestFailed as r,GraffitiErrorPatchError as u}from"@graffiti-garden/api";function d(){const e=new Uint8Array(16);crypto.getRandomValues(e);return Array.from(e).map((e=>e.toString(16).padStart(2,"0"))).join("")+"👩🏽‍❤️‍💋‍👩🏻🫱🏼‍🫲🏿"}function v(){return{value:{[d()]:d()},channels:[d(),d()]}}async function h(a){const t=await a.next();return e(!t.done&&!t.value.error,"result has no value"),t.value.value}const w=e=>{a("URI and location conversion",(()=>{t("location to uri and back",(async()=>{const a=e(),t={name:d(),actor:d(),source:d()},n=a.locationToUri(t),l=a.uriToLocation(n);o(t).toEqual(l)})),t("collision resistance",(async()=>{const a=e(),t={name:d(),actor:d(),source:d()};for(const e of["name","actor","source"]){const n={...t,[e]:d()},l=a.locationToUri(t),s=a.locationToUri(n);o(l).not.toEqual(s)}})),t("random URI should not be a valid location",(async()=>{const a=e();o((()=>a.uriToLocation(""))).toThrow(n)}))}))},p=(e,n,h)=>{a("CRUD",{timeout:2e4},(()=>{t("put, get, delete",(async()=>{const a=e(),t=n(),l={something:"hello, world~ c:"},s=[d(),d()],i=await a.put({value:l,channels:s},t);o(i.value).toEqual({}),o(i.channels).toEqual([]),o(i.allowed).toBeUndefined(),o(i.actor).toEqual(t.actor);const c=await a.get(i,{});o(c.value).toEqual(l),o(c.channels).toEqual([]),o(c.allowed).toBeUndefined(),o(c.name).toEqual(i.name),o(c.actor).toEqual(i.actor),o(c.source).toEqual(i.source),o(c.lastModified).toEqual(i.lastModified);const r={something:"goodbye, world~ :c"},u=await a.put({...i,value:r,channels:[]},t);o(u.value).toEqual(l),o(u.tombstone).toEqual(!0),o(u.name).toEqual(i.name),o(u.actor).toEqual(i.actor),o(u.source).toEqual(i.source),o(u.lastModified).toBeGreaterThan(c.lastModified);const v=await a.get(i,{});o(v.value).toEqual(r),o(v.lastModified).toEqual(u.lastModified),o(v.tombstone).toEqual(!1);const h=await a.delete(v,t);o(h.tombstone).toEqual(!0),o(h.value).toEqual(r),o(h.lastModified).toBeGreaterThan(u.lastModified);const w=await a.get(v,{});o(w).toEqual(h)})),t("get non-existant",(async()=>{const a=e(),t=n(),s=await a.put(v(),t);await o(a.get({...s,name:d()},{})).rejects.toBeInstanceOf(l)})),t("put, get, delete with wrong actor",(async()=>{const a=e(),t=n(),l=h();await o(a.put({value:{},channels:[],actor:l.actor},t)).rejects.toThrow(s);const i=await a.put({value:{},channels:[]},l);await o(a.delete(i,t)).rejects.toThrow(s),await o(a.patch({},i,t)).rejects.toThrow(s)})),t("put and get with schema",(async()=>{const a=e(),t=n(),l={something:"hello",another:42},s=await a.put({value:l,channels:[]},t),i=await a.get(s,{properties:{value:{properties:{something:{type:"string"},another:{type:"integer"}}}}});o(i.value.something).toEqual(l.something),o(i.value.another).toEqual(l.another)})),t("put and get with invalid schema",(async()=>{const a=e(),t=n(),l=await a.put({value:{},channels:[]},t);await o(a.get(l,{properties:{value:{type:"asdf"}}})).rejects.toThrow(i)})),t("put and get with wrong schema",(async()=>{const a=e(),t=n(),l=await a.put({value:{hello:"world"},channels:[]},t);await o(a.get(l,{properties:{value:{properties:{hello:{type:"number"}}}}})).rejects.toThrow(c)})),t("put and get with empty access control",(async()=>{const a=e(),t=n(),s=h(),i={um:"hi"},c=[d()],r=[d()],u=await a.put({value:i,allowed:c,channels:r},t),v=await a.get(u,{},t);o(v.value).toEqual(i),o(v.allowed).toEqual(c),o(v.channels).toEqual(r),await o(a.get(u,{})).rejects.toBeInstanceOf(l),await o(a.get(u,{},s)).rejects.toBeInstanceOf(l)})),t("put and get with specific access control",(async()=>{const a=e(),t=n(),s=h(),i={um:"hi"},c=[d(),s.actor,d()],r=[d()],u=await a.put({value:i,allowed:c,channels:r},t),v=await a.get(u,{},t);o(v.value).toEqual(i),o(v.allowed).toEqual(c),o(v.channels).toEqual(r),await o(a.get(u,{})).rejects.toBeInstanceOf(l);const w=await a.get(u,{},s);o(w.value).toEqual(i),o(w.allowed).toEqual([s.actor]),o(w.channels).toEqual([])})),t("patch value",(async()=>{const a=e(),t=n(),l={something:"hello, world~ c:"},s=await a.put({value:l,channels:[]},t),i=await a.patch({value:[{op:"replace",path:"/something",value:"goodbye, world~ :c"}]},s,t);o(i.value).toEqual(l),o(i.tombstone).toBe(!0);const c=await a.get(s,{});o(c.value).toEqual({something:"goodbye, world~ :c"}),o(i.lastModified).toBe(c.lastModified),await a.delete(s,t)})),t("patch deleted object",(async()=>{const a=e(),t=n(),s=await a.put(v(),t);await a.delete(s,t),await o(a.patch({},s,t)).rejects.toBeInstanceOf(l)})),t("deep patch",(async()=>{const a=e(),t=n(),l={something:{another:{somethingElse:"hello"}}},s=await a.put({value:l,channels:[]},t),i=await a.patch({value:[{op:"replace",path:"/something/another/somethingElse",value:"goodbye"}]},s,t),c=await a.get(s,{});o(i.value).toEqual(l),o(c.value).toEqual({something:{another:{somethingElse:"goodbye"}}})})),t("patch channels",(async()=>{const a=e(),t=n(),l=[d()],s=[d()],i=await a.put({value:{},channels:l},t),c={channels:[{op:"replace",path:"/0",value:s[0]}]},r=await a.patch(c,i,t);o(r.channels).toEqual(l);const u=await a.get(i,{},t);o(u.channels).toEqual(s),await a.delete(i,t)})),t("patch 'increment' with test",(async()=>{const a=e(),t=n(),l=await a.put({value:{counter:1},channels:[]},t),s=await a.patch({value:[{op:"test",path:"/counter",value:1},{op:"replace",path:"/counter",value:2}]},l,t);o(s.value).toEqual({counter:1});const i=await a.get(s,{properties:{value:{properties:{counter:{type:"integer"}}}}});o(i.value.counter).toEqual(2),await o(a.patch({value:[{op:"test",path:"/counter",value:1},{op:"replace",path:"/counter",value:3}]},l,t)).rejects.toThrow(r)})),t("invalid patch",(async()=>{const a=e(),t=n(),l=v(),s=await a.put(l,t);await o(a.patch({value:[{op:"add",path:"/root",value:[]},{op:"add",path:"/root/2",value:2}]},s,t)).rejects.toThrow(u)})),t("patch channels to be wrong",(async()=>{const a=e(),t=n(),l=v();l.allowed=[d()];const s=await a.put(l,t),i=[{channels:[{op:"replace",path:"",value:null}]},{channels:[{op:"replace",path:"",value:{}}]},{channels:[{op:"replace",path:"",value:["hello",["hi"]]}]},{channels:[{op:"add",path:"/0",value:1}]},{value:[{op:"replace",path:"",value:"not an object"}]},{value:[{op:"replace",path:"",value:null}]},{value:[{op:"replace",path:"",value:[]}]},{allowed:[{op:"replace",path:"",value:{}}]},{allowed:[{op:"replace",path:"",value:["hello",["hi"]]}]}];for(const e of i)await o(a.patch(e,s,t)).rejects.toThrow(u);const c=await a.get(s,{},t);o(c.value).toEqual(l.value),o(c.channels).toEqual(l.channels),o(c.allowed).toEqual(l.allowed),o(c.lastModified).toEqual(s.lastModified)}))}))},m=(n,l,s)=>{a("synchronizeDiscover",(()=>{t("get",(async()=>{const e=n(),a=l(),t=v(),s=t.channels.slice(1),i=await e.put(t,a),c=n(),r=c.synchronizeDiscover(s,{}).next(),u=await c.get(i,{},a),d=(await r).value;if(!d||d.error)throw new Error("Error in synchronize");o(d.value.value).toEqual(t.value),o(d.value.channels).toEqual(s),o(d.value.tombstone).toBe(!1),o(d.value.lastModified).toEqual(u.lastModified)})),t("put",(async()=>{const e=n(),a=l(),t=d(),s=d(),i=d(),c={hello:"world"},r=[t,i],u=await e.put({value:c,channels:r},a),v=e.synchronizeDiscover([t],{}).next(),h=e.synchronizeDiscover([s],{}).next(),w=e.synchronizeDiscover([i],{}).next(),p={goodbye:"world"},m=[s,i];await e.put({...u,value:p,channels:m},a);const E=(await v).value,f=(await h).value,q=(await w).value;if(!E||E.error||!f||f.error||!q||q.error)throw new Error("Error in synchronize");o(E.value.value).toEqual(c),o(E.value.channels).toEqual([t]),o(E.value.tombstone).toBe(!0),o(f.value.value).toEqual(p),o(f.value.channels).toEqual([s]),o(f.value.tombstone).toBe(!1),o(q.value.value).toEqual(p),o(q.value.channels).toEqual([i]),o(q.value.tombstone).toBe(!1),o(E.value.lastModified).toEqual(f.value.lastModified),o(q.value.lastModified).toEqual(f.value.lastModified)})),t("patch",(async()=>{const e=n(),a=l(),t=d(),s=d(),i=d(),c={hello:"world"},r=[t,i],u=await e.put({value:c,channels:r},a),v=e.synchronizeDiscover([t],{}).next(),h=e.synchronizeDiscover([s],{}).next(),w=e.synchronizeDiscover([i],{}).next();await e.patch({value:[{op:"add",path:"/something",value:"new value"}],channels:[{op:"add",path:"/-",value:s},{op:"remove",path:`/${r.indexOf(t)}`}]},u,a);const p=(await v).value,m=(await h).value,E=(await w).value;if(!p||p.error||!m||m.error||!E||E.error)throw new Error("Error in synchronize");const f={...c,something:"new value"};o(p.value.value).toEqual(c),o(p.value.channels).toEqual([t]),o(p.value.tombstone).toBe(!0),o(m.value.value).toEqual(f),o(m.value.channels).toEqual([s]),o(m.value.tombstone).toBe(!1),o(E.value.value).toEqual(f),o(E.value.channels).toEqual([i]),o(E.value.tombstone).toBe(!1),o(p.value.lastModified).toEqual(m.value.lastModified),o(E.value.lastModified).toEqual(m.value.lastModified)})),t("delete",(async()=>{const e=n(),a=l(),t=[d(),d(),d()],s={hello:"world"},i=[d(),...t.slice(1)],c=await e.put({value:s,channels:i},a),r=e.synchronizeDiscover(t,{}).next();e.delete(c,a);const u=(await r).value;if(!u||u.error)throw new Error("Error in synchronize");o(u.value.tombstone).toBe(!0),o(u.value.value).toEqual(s),o(u.value.channels).toEqual(t.filter((e=>i.includes(e))))})),t("not allowed",(async()=>{const e=n(),a=l(),t=s(),i=[d(),d(),d()],c=i.slice(1),r=e.synchronizeDiscover(c,{},a).next(),u=e.synchronizeDiscover(c,{},t).next(),v=e.synchronizeDiscover(c,{}).next(),h={hello:"world"},w=[d(),t.actor];await e.put({value:h,channels:i,allowed:w},a),await o(Promise.race([v,new Promise(((e,a)=>setTimeout(a,100,"Timeout")))])).rejects.toThrow("Timeout");const p=(await r).value,m=(await u).value;if(!p||p.error||!m||m.error)throw new Error("Error in synchronize");o(p.value.value).toEqual(h),o(p.value.allowed).toEqual(w),o(p.value.channels).toEqual(i),o(m.value.value).toEqual(h),o(m.value.allowed).toEqual([t.actor]),o(m.value.channels).toEqual(c)}))})),a("synchronizeGet",(()=>{t("replace, delete",(async()=>{const a=n(),t=l(),s=v(),i=await a.put(s,t),c=a.synchronizeGet(i,{}),r=c.next(),u={goodbye:"world"},d=await a.put({...i,value:u},t),h=(await r).value;e(h&&!h.error),o(h.value.value).toEqual(u),o(h.value.actor).toEqual(t.actor),o(h.value.channels).toEqual([]),o(h.value.tombstone).toBe(!1),o(h.value.lastModified).toEqual(d.lastModified),o(h.value.allowed).toBeUndefined();const w=await a.delete(d,t),p=(await c.next()).value;e(p&&!p.error),o(p.value.tombstone).toBe(!0),o(p.value.lastModified).toEqual(w.lastModified),await a.put(v(),t),await o(Promise.race([c.next(),new Promise(((e,a)=>setTimeout(a,100,"Timeout")))])).rejects.toThrow("Timeout")})),t("not allowed",(async()=>{const a=n(),t=l(),i=s(),c=v(),r=await a.put(c,t),u=a.synchronizeGet(r,{},t),d=a.synchronizeGet(r,{},i),h=u.next(),w=d.next(),p={goodbye:"world"},m=await a.put({...r,...c,allowed:[],value:p},t),E=(await h).value,f=(await w).value;e(E&&!E.error),e(f&&!f.error),o(E.value.value).toEqual(p),o(f.value.value).toEqual(c.value),o(E.value.actor).toEqual(t.actor),o(f.value.actor).toEqual(t.actor),o(E.value.channels).toEqual(c.channels),o(f.value.channels).toEqual([]),o(E.value.tombstone).toBe(!1),o(f.value.tombstone).toBe(!0),o(E.value.lastModified).toEqual(m.lastModified),o(f.value.lastModified).toEqual(m.lastModified)}))}))},E=(n,l,s)=>{a("discover",{timeout:2e4},(()=>{t("discover nothing",(async()=>{const e=n().discover([],{});o(await e.next()).toHaveProperty("done",!0)})),t("discover single",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a),i=[d(),t.channels[0]],c=e.discover(i,{}),r=await h(c);o(r.value).toEqual(t.value),o(r.channels).toEqual([t.channels[0]]),o(r.allowed).toBeUndefined(),o(r.actor).toEqual(a.actor),o(r.tombstone).toBe(!1),o(r.lastModified).toEqual(s.lastModified);const u=await c.next();o(u.done).toBe(!0)})),t("discover wrong channel",(async()=>{const e=n(),a=l(),t=v();await e.put(t,a);const s=e.discover([d()],{});await o(s.next()).resolves.toHaveProperty("done",!0)})),t("discover not allowed",(async()=>{const e=n(),a=l(),t=s(),i=v();i.allowed=[d(),d()];const c=await e.put(i,a),r=e.discover(i.channels,{},a),u=await h(r);o(u.value).toEqual(i.value),o(u.channels).toEqual(i.channels),o(u.allowed).toEqual(i.allowed),o(u.actor).toEqual(a.actor),o(u.tombstone).toBe(!1),o(u.lastModified).toEqual(c.lastModified);const w=e.discover(i.channels,{},t);o(await w.next()).toHaveProperty("done",!0);const p=e.discover(i.channels,{});o(await p.next()).toHaveProperty("done",!0)})),t("discover allowed",(async()=>{const e=n(),a=l(),t=s(),i=v();i.allowed=[d(),t.actor,d()];const c=await e.put(i,a),r=e.discover(i.channels,{},t),u=await h(r);o(u.value).toEqual(i.value),o(u.allowed).toEqual([t.actor]),o(u.channels).toEqual(i.channels),o(u.actor).toEqual(a.actor),o(u.tombstone).toBe(!1),o(u.lastModified).toEqual(c.lastModified)}));for(const e of["name","actor","lastModified"])t(`discover for ${e}`,(async()=>{const a=n(),t=l(),i=s(),c=v(),r=await a.put(c,t),u=v();u.channels=c.channels,await new Promise((e=>setTimeout(e,20)));const d=await a.put(u,i),w=a.discover(c.channels,{properties:{[e]:{enum:[r[e]]}}}),p=await h(w);o(p.name).toEqual(r.name),o(p.name).not.toEqual(d.name),o(p.value).toEqual(c.value),await o(w.next()).resolves.toHaveProperty("done",!0)}));t("discover with lastModified range",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a);await new Promise((e=>setTimeout(e,20)));const i=await e.put(t,a);o(s.name).not.toEqual(i.name),o(s.lastModified).toBeLessThan(i.lastModified);const c=e.discover([t.channels[0]],{properties:{lastModified:{minimum:i.lastModified,exclusiveMinimum:!0}}});o(await c.next()).toHaveProperty("done",!0);const r=e.discover([t.channels[0]],{properties:{lastModified:{minimum:i.lastModified-.1,exclusiveMinimum:!0}}}),u=await h(r);o(u.name).toEqual(i.name),o(await r.next()).toHaveProperty("done",!0);const d=e.discover(t.channels,{properties:{value:{},lastModified:{minimum:i.lastModified}}}),w=await h(d);o(w.name).toEqual(i.name),o(await d.next()).toHaveProperty("done",!0);const p=e.discover(t.channels,{properties:{lastModified:{minimum:i.lastModified+.1}}});o(await p.next()).toHaveProperty("done",!0);const m=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified,exclusiveMaximum:!0}}});o(await m.next()).toHaveProperty("done",!0);const E=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified+.1,exclusiveMaximum:!0}}}),f=await h(E);o(f.name).toEqual(s.name),o(await E.next()).toHaveProperty("done",!0);const q=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified}}}),y=await h(q);o(y.name).toEqual(s.name),o(await q.next()).toHaveProperty("done",!0);const M=e.discover(t.channels,{properties:{lastModified:{maximum:s.lastModified-.1}}});o(await M.next()).toHaveProperty("done",!0)})),t("discover schema allowed, as and not as owner",(async()=>{const e=n(),a=l(),t=s(),i=v();i.allowed=[d(),t.actor,d()],await e.put(i,a);const c=e.discover(i.channels,{properties:{allowed:{minItems:3,not:{items:{not:{enum:[t.actor]}}}}}},a),r=await h(c);o(r.value).toEqual(i.value),await o(c.next()).resolves.toHaveProperty("done",!0);const u=e.discover(i.channels,{properties:{allowed:{minItems:3}}},t);await o(u.next()).resolves.toHaveProperty("done",!0);const w=e.discover(i.channels,{properties:{allowed:{not:{items:{not:{enum:[i.channels[0]]}}}}}},t);await o(w.next()).resolves.toHaveProperty("done",!0);const p=e.discover(i.channels,{properties:{allowed:{maxItems:1,not:{items:{not:{enum:[t.actor]}}}}}},t),m=await h(p);o(m.value).toEqual(i.value),await o(p.next()).resolves.toHaveProperty("done",!0)})),t("discover schema channels, as and not as owner",(async()=>{const e=n(),a=l(),t=s(),i=v();i.channels=[d(),d(),d()],await e.put(i,a);const c=e.discover([i.channels[0],i.channels[2]],{properties:{channels:{minItems:3,not:{items:{not:{enum:[i.channels[1]]}}}}}},a),r=await h(c);o(r.value).toEqual(i.value),await o(c.next()).resolves.toHaveProperty("done",!0);const u=e.discover([i.channels[0],i.channels[2]],{properties:{channels:{minItems:3}}},t);await o(u.next()).resolves.toHaveProperty("done",!0);const w=e.discover([i.channels[0],i.channels[2]],{properties:{channels:{not:{items:{not:{enum:[i.channels[1]]}}}}}},t);await o(w.next()).resolves.toHaveProperty("done",!0);const p=e.discover([i.channels[0],i.channels[2]],{properties:{allowed:{maxItems:2,not:{items:{not:{enum:[i.channels[2]]}}}}}},t),m=await h(p);o(m.value).toEqual(i.value),await o(p.next()).resolves.toHaveProperty("done",!0)})),t("discover query for empty allowed",(async()=>{const e=n(),a=l(),t=v(),s={not:{required:["allowed"]}};await e.put(t,a);const i=e.discover(t.channels,s,a),c=await h(i);o(c.value).toEqual(t.value),o(c.allowed).toBeUndefined(),await o(i.next()).resolves.toHaveProperty("done",!0);const r=v();r.allowed=[],await e.put(r,a);const u=e.discover(r.channels,s,a);await o(u.next()).resolves.toHaveProperty("done",!0)})),t("discover query for values",(async()=>{const a=n(),t=l(),s=v();s.value={test:d()},await a.put(s,t);const i=v();i.channels=s.channels,i.value={test:d(),something:d()},await a.put(i,t);const c=v();c.channels=s.channels,c.value={other:d(),something:d()},await a.put(c,t);const r=new Map;for(const t of["test","something","other"]){let o=0;for await(const n of a.discover(s.channels,{properties:{value:{required:[t]}}}))e(!n.error,"result has error"),t in n.value.value&&o++;r.set(t,o)}o(r.get("test")).toBe(2),o(r.get("something")).toBe(2),o(r.get("other")).toBe(1)})),t("discover for deleted content",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a),i=await e.delete(s,a),c=e.discover(t.channels,{}),r=await h(c);o(r.tombstone).toBe(!0),o(r.value).toEqual(t.value),o(r.channels).toEqual(t.channels),o(r.actor).toEqual(a.actor),o(r.lastModified).toEqual(i.lastModified),await o(c.next()).resolves.toHaveProperty("done",!0)})),t("discover for replaced channels",(async()=>{for(let e=0;e<10;e++){const e=n(),a=l(),t=v(),s=await e.put(t,a),i=v(),c=await e.put({...s,...i},a),r=e.discover(t.channels,{}),u=await h(r);await o(r.next()).resolves.toHaveProperty("done",!0);const d=e.discover(i.channels,{}),w=await h(d);await o(d.next()).resolves.toHaveProperty("done",!0),s.lastModified===c.lastModified?(o(u.tombstone||w.tombstone).toBe(!0),o(u.tombstone&&w.tombstone).toBe(!1)):(o(u.tombstone).toBe(!0),o(u.value).toEqual(t.value),o(u.channels).toEqual(t.channels),o(u.lastModified).toEqual(c.lastModified),o(w.tombstone).toBe(!1),o(w.value).toEqual(i.value),o(w.channels).toEqual(i.channels),o(w.lastModified).toEqual(c.lastModified))}})),t("discover for patched allowed",(async()=>{const e=n(),a=l(),t=v(),s=await e.put(t,a);await e.patch({allowed:[{op:"add",path:"",value:[]}]},s,a);const i=e.discover(t.channels,{}),c=await h(i);o(c.tombstone).toBe(!0),o(c.value).toEqual(t.value),o(c.channels).toEqual(t.channels),o(c.allowed).toBeUndefined(),await o(i.next()).resolves.toHaveProperty("done",!0)})),t("put concurrently and discover one",(async()=>{const a=n(),t=l(),s=v();s.name=d();const i=Array(100).fill(0).map((()=>a.put(s,t)));await Promise.all(i);const c=a.discover(s.channels,{});let r=0,u=0;for await(const a of c)e(!a.error,"result has error"),a.value.tombstone?r++:u++;o(r).toBe(99),o(u).toBe(1)}))}))},f=(e,n,l)=>{a("recoverOrphans",(()=>{t("list orphans",(async()=>{const a=e(),t=n(),l=[],s=a.recoverOrphans({},t);for await(const e of s)e.error||l.push(e.value.name);const i=v();i.channels=[];const c=await a.put(i,t),r=a.recoverOrphans({},t);let u=0;for await(const e of r)e.error||e.value.name===c.name&&(u++,o(e.value.source).toBe(c.source),o(e.value.lastModified).toBe(c.lastModified));o(u).toBe(1)})),t("replaced orphan, no longer",(async()=>{const a=e(),t=n(),l=v();l.channels=[];const s=await a.put(l,t),i=await a.put({...s,...l,channels:[d()]},t);o(i.name).toBe(s.name);const c=a.recoverOrphans({},t);let r=0;for await(const e of c)e.error||e.value.name===s.name&&(r++,o(e.value.tombstone).toBe(!0),o(e.value.lastModified).toBe(i.lastModified),o(e.value.channels).toEqual([]));o(r).toBe(1)}))}))},q=(n,l,s)=>{a("channel stats",(()=>{t("list channels",(async()=>{const e=n(),a=l(),t=new Map,s=e.channelStats(a);for await(const e of s)e.error||t.set(e.value.channel,e.value.count);const i=[d(),d(),d()];for(let t=0;t<3;t++)for(let o=0;o<t+1;o++)await e.put({value:{index:o},channels:i.slice(0,t+1)},a);await e.put({value:{index:3},channels:[i[2]]},a);const c=e.channelStats(a);let r=new Map;for await(const e of c)e.error||r.set(e.value.channel,e.value.count);r=new Map(Array.from(r).filter((([e,a])=>!t.has(e)))),o(r.size).toBe(3),o(r.get(i[0])).toBe(6),o(r.get(i[1])).toBe(5),o(r.get(i[2])).toBe(4)})),t("list channels with deleted channel",(async()=>{const a=n(),t=l(),s=[d(),d(),d()],i=await a.put({value:{index:2},channels:s.slice(1)},t),c=await a.put({value:{index:0},channels:s},t);await a.delete(c,t);const r=await a.put({value:{index:1},channels:s.slice(2)},t),u=a.channelStats(t);let v=0,h=0;for await(const a of u){if(a.error)continue;const{channel:t,count:n,lastModified:l}=a.value;e(t!==s[0],"There should not be an object in channel[0]"),t===s[1]?(o(n).toBe(1),o(l).toBe(i.lastModified),v++):t===s[2]&&(o(n).toBe(2),o(l).toBe(r.lastModified),h++)}o(v).toBe(1),o(h).toBe(1)}))}))};export{p as graffitiCRUDTests,q as graffitiChannelStatsTests,E as graffitiDiscoverTests,w as graffitiLocationTests,f as graffitiOrphanTests,m as graffitiSynchronizeTests};
2
2
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { GraffitiFactory, GraffitiSession } from "@graffiti-garden/api";
2
+ export declare const graffitiChannelStatsTests: (useGraffiti: GraffitiFactory, useSession1: () => GraffitiSession, useSession2: () => GraffitiSession) => void;
3
+ //# sourceMappingURL=channel-stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-stats.d.ts","sourceRoot":"","sources":["../../../tests/src/channel-stats.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG7E,eAAO,MAAM,yBAAyB,gBACvB,eAAe,eACf,MAAM,eAAe,eACrB,MAAM,eAAe,SAiHnC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"crud.d.ts","sourceRoot":"","sources":["../../../tests/src/crud.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,QAAQ,EACR,eAAe,EAEhB,MAAM,sBAAsB,CAAC;AAW9B,eAAO,MAAM,iBAAiB,gBACf,MAAM,IAAI,CAAC,QAAQ,EAAE,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC,eACxD,MAAM,eAAe,eACrB,MAAM,eAAe,SAwcnC,CAAC"}
1
+ {"version":3,"file":"crud.d.ts","sourceRoot":"","sources":["../../../tests/src/crud.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,QAAQ,EACR,eAAe,EAEhB,MAAM,sBAAsB,CAAC;AAW9B,eAAO,MAAM,iBAAiB,gBACf,MAAM,IAAI,CAAC,QAAQ,EAAE,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC,eACxD,MAAM,eAAe,eACrB,MAAM,eAAe,SAwenC,CAAC"}
@@ -2,4 +2,6 @@ export * from "./location";
2
2
  export * from "./crud";
3
3
  export * from "./synchronize";
4
4
  export * from "./discover";
5
+ export * from "./orphans";
6
+ export * from "./channel-stats";
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../tests/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../tests/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { GraffitiFactory, GraffitiSession } from "@graffiti-garden/api";
2
+ export declare const graffitiListTests: (useGraffiti: GraffitiFactory, useSession1: () => GraffitiSession, useSession2: () => GraffitiSession) => void;
3
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../tests/src/list.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG7E,eAAO,MAAM,iBAAiB,gBACf,eAAe,eACf,MAAM,eAAe,eACrB,MAAM,eAAe,SAgBnC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { GraffitiFactory, GraffitiSession } from "@graffiti-garden/api";
2
+ export declare const graffitiOrphanTests: (useGraffiti: GraffitiFactory, useSession1: () => GraffitiSession, useSession2: () => GraffitiSession) => void;
3
+ //# sourceMappingURL=orphans.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orphans.d.ts","sourceRoot":"","sources":["../../../tests/src/orphans.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG7E,eAAO,MAAM,mBAAmB,gBACjB,eAAe,eACf,MAAM,eAAe,eACrB,MAAM,eAAe,SA8DnC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../tests/src/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE9E,wBAAgB,YAAY,IAAI,MAAM,CAMrC;AAED,wBAAgB,WAAW;;EAI1B;AAED,wBAAgB,eAAe,IAAI,iBAAiB,CAAC,EAAE,CAAC,CAKvD;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,cAIzE"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../tests/src/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE9E,wBAAgB,YAAY,IAAI,MAAM,CASrC;AAED,wBAAgB,WAAW;;EAI1B;AAED,wBAAgB,eAAe,IAAI,iBAAiB,CAAC,EAAE,CAAC,CAKvD;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,cAIzE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graffiti-garden/api",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "The heart of Graffiti",
5
5
  "types": "./dist/src/index.d.ts",
6
6
  "module": "./dist/index.js",
package/src/1-api.ts CHANGED
@@ -23,6 +23,7 @@ import type { JSONSchema4 } from "json-schema";
23
23
  *
24
24
  * There are several different implementations of this Graffiti API available,
25
25
  * including a [federated implementation](https://github.com/graffiti-garden/implementation-federated),
26
+ * that lets users choose where their data is stored,
26
27
  * and a [local implementation](https://github.com/graffiti-garden/implementation-local)
27
28
  * that can be used for testing and development. In our design of Graffiti, this API is our
28
29
  * primary focus as it is the layer that shapes the experience
@@ -36,22 +37,28 @@ import type { JSONSchema4 } from "json-schema";
36
37
  *
37
38
  * ## Overview
38
39
  *
39
- * This API tries to draw from well-known concepts and standards wherever possible.
40
- * JSON objects, representing social artifacts (e.g. posts, profiles) and activities
41
- * (e.g. likes, follows) can be interacted with through standard CRUD operations:
40
+ * Graffiti provides applications with methods to create and store data
41
+ * on behalf of their users using standard CRUD operations:
42
42
  * {@link put}, {@link get}, {@link patch}, and {@link delete}.
43
- * Objects can be typed with [JSON Schema](https://json-schema.org/) and patches
44
- * can be applied with [JSON Patch](https://jsonpatch.com).
45
- * For interoperability between Graffiti applications, we recommend using established properties from the
46
- * [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) when available.
43
+ * This data can represent both social artifacts (e.g. posts, profiles) and
44
+ * activities (e.g. likes, follows) and is stored as JSON.
47
45
  *
48
- * The social aspect of Graffiti comes from the {@link discover} operation
46
+ * The social aspect of Graffiti comes from the {@link discover} method
49
47
  * which allows applications to find objects that other users made.
50
48
  * It is a lot like a traditional query operation, but it only
51
49
  * returns objects that have been placed in particular
52
50
  * {@link GraffitiObjectBase.channels | `channels`}
53
51
  * specified by the discovering application.
54
52
  *
53
+ * Graffiti builds on well known concepts and standards wherever possible.
54
+ * JSON Objects can be typed with [JSON Schema](https://json-schema.org/) and patches
55
+ * can be applied with [JSON Patch](https://jsonpatch.com).
56
+ * For interoperability between Graffiti applications, we recommend that
57
+ * objects use established properties from the
58
+ * [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) when available,
59
+ * however it is always possible to create additional properties, contributing
60
+ * to the broader [folksonomy](https://en.wikipedia.org/wiki/Folksonomy).
61
+ *
55
62
  * {@link GraffitiObjectBase.channels | `channels`} are one of the major concepts
56
63
  * unique to Graffiti along with *interaction relativity*.
57
64
  * Channels create boundaries between public spaces and work to prevent
@@ -231,19 +238,17 @@ import type { JSONSchema4 } from "json-schema";
231
238
  *
232
239
  * ## TODO
233
240
  *
234
- * - Test for listChannels and listOrphans,
235
241
  * - Implement scope.
236
242
  *
237
243
  * @groupDescription CRUD Methods
238
244
  * Methods for {@link put | creating}, {@link get | reading}, {@link patch | updating},
239
245
  * and {@link delete | deleting} {@link GraffitiObjectBase | Graffiti objects}.
240
246
  * @groupDescription Query Methods
241
- * Methods for retrieving multiple {@link GraffitiObjectBase | Graffiti objects} at a time.
247
+ * Methods that retrieve or accumulate information about multiple {@link GraffitiObjectBase | Graffiti objects} at a time.
242
248
  * @groupDescription Session Management
243
249
  * Methods and properties for logging in and out of a Graffiti implementation.
244
250
  * @groupDescription Utilities
245
- * Methods for for converting Graffiti objects to and from URIs
246
- * and for finding lost objects.
251
+ * Methods for for converting Graffiti objects to and from URIs.
247
252
  */
248
253
  export abstract class Graffiti {
249
254
  /**
@@ -313,14 +318,26 @@ export abstract class Graffiti {
313
318
 
314
319
  /**
315
320
  * Retrieves an object from a given location.
316
- * If no object exists at that location or if the retrieving
317
- * {@link GraffitiObjectBase.actor | `actor`} is not the creator or included in
318
- * the object's {@link GraffitiObjectBase.allowed | `allowed`} property,
319
- * a {@link GraffitiErrorNotFound} is thrown.
320
321
  *
321
- * The retrieved object is also type-checked against the provided [JSON schema](https://json-schema.org/)
322
+ * The retrieved object is type-checked against the provided [JSON schema](https://json-schema.org/)
322
323
  * otherwise a {@link GraffitiErrorSchemaMismatch} is thrown.
323
324
  *
325
+ * If the object existed but has since been deleted,
326
+ * or the retrieving {@link GraffitiObjectBase.actor | `actor`}
327
+ * was {@link GraffitiObjectBase.allowed | `allowed`} to access
328
+ * the object but now isn't, this method will return the latest
329
+ * version of the object that the {@link GraffitiObjectBase.actor | `actor`}
330
+ * was allowed to access with its {@link GraffitiObjectBase.tombstone | `tombstone`}
331
+ * set to `true`, so long as that version is still cached.
332
+ *
333
+ * Otherwise, if the object never existed, or the
334
+ * retrieving {@link GraffitiObjectBase.actor | `actor`} was never
335
+ * {@link GraffitiObjectBase.allowed | `allowed`} to access it, or if
336
+ * the object was changed long enough ago that its history has been
337
+ * purged from the cache, a {@link GraffitiErrorNotFound} is thrown.
338
+ * The rate at which the cache is purged is implementation dependent.
339
+ * See the `tombstoneReturn` property returned by {@link discover}.
340
+ *
324
341
  * @group CRUD Methods
325
342
  */
326
343
  abstract get<Schema extends JSONSchema4>(
@@ -417,7 +434,7 @@ export abstract class Graffiti {
417
434
  * not specified by the `discover` method will not be revealed. This masking happens
418
435
  * before the supplied schema is applied.
419
436
  *
420
- * {@link discover} can be used in conjunction with {@link synchronize}
437
+ * {@link discover} can be used in conjunction with {@link synchronizeDiscover}
421
438
  * to provide a responsive and consistent user experience.
422
439
  *
423
440
  * Since different implementations may fetch data from multiple sources there is
@@ -487,92 +504,56 @@ export abstract class Graffiti {
487
504
  >;
488
505
 
489
506
  /**
490
- * This method has the same signature as {@link discover} but listens for
491
- * changes made via {@link put}, {@link patch}, and {@link delete} or
492
- * fetched from {@link get} or {@link discover} and then streams appropriate
493
- * changes to provide a responsive and consistent user experience.
494
- *
495
- * Unlike {@link discover}, this method continuously listens for changes
496
- * and will not terminate unless the user calls the `return` method on the iterator
497
- * or `break`s out of the loop.
498
- *
499
- * Example 1: Suppose a user publishes a post using {@link put}. If the feed
500
- * displaying that user's posts is using {@link synchronize} to listen for changes,
501
- * then the user's new post will instantly appear in their feed, giving the UI a
502
- * responsive feel.
503
- *
504
- * Example 2: Suppose one of a user's friends changes their name. As soon as the
505
- * user's application receives one notice of that change (using {@link get}
506
- * or {@link discover}), then {@link synchronize} listeners can be used to update
507
- * all instance's of that friend's name in the user's application instantly,
508
- * providing a consistent user experience.
507
+ * Discovers objects **not** contained in any
508
+ * {@link GraffitiObjectBase.channels | `channels`}
509
+ * that were created by the querying {@link GraffitiObjectBase.actor | `actor`}
510
+ * and match the given [JSON Schema](https://json-schema.org).
511
+ * Unlike {@link discover}, this method will not return objects created by other users.
509
512
  *
510
- * @group Synchronize Methods
511
- */
512
- abstract synchronizeDiscover<Schema extends JSONSchema4>(
513
- /**
514
- * The {@link GraffitiObjectBase.channels | `channels`} that the objects must be associated with.
515
- */
516
- channels: string[],
517
- /**
518
- * A [JSON Schema](https://json-schema.org) that objects must satisfy.
519
- */
520
- schema: Schema,
521
- /**
522
- * An implementation-specific object with information to authenticate the
523
- * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
524
- * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
525
- * property will be returned.
526
- */
527
- session?: GraffitiSession | null,
528
- ): GraffitiStream<GraffitiObject<Schema>>;
529
-
530
- /**
531
- * This method has the same signature as {@link get} but, like {@link synchronizeDiscover},
532
- * it listens for changes made via {@link put}, {@link patch}, and {@link delete} or
533
- * fetched from {@link get} or {@link discover} and then streams appropriate
534
- * changes to provide a responsive and consistent user experience.
513
+ * This method is not useful for most applications, but necessary for
514
+ * getting a global view of all a user's Graffiti data or debugging
515
+ * channel usage.
535
516
  *
536
- * Unlike {@link get}, which returns a single result, this method continuously
537
- * listens for changes which are output as an asynchronous {@link GraffitiStream}.
517
+ * It's return value is the same as {@link discover}.
538
518
  *
539
- * @group Synchronize Methods
519
+ * @group Query Methods
540
520
  */
541
- abstract synchronizeGet<Schema extends JSONSchema4>(
521
+ abstract recoverOrphans<Schema extends JSONSchema4>(
542
522
  /**
543
- * The location of the object to get.
544
- */
545
- locationOrUri: GraffitiLocation | string,
546
- /**
547
- * The JSON schema to validate the retrieved object against.
523
+ * A [JSON Schema](https://json-schema.org) that orphaned objects must satisfy.
548
524
  */
549
525
  schema: Schema,
550
526
  /**
551
527
  * An implementation-specific object with information to authenticate the
552
- * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
553
- * the retrieved object's {@link GraffitiObjectBase.allowed | `allowed`}
554
- * property must be `undefined`.
528
+ * {@link GraffitiObjectBase.actor | `actor`}.
555
529
  */
556
- session?: GraffitiSession | null,
557
- ): GraffitiStream<GraffitiObject<Schema>>;
530
+ session: GraffitiSession,
531
+ ): GraffitiStream<
532
+ GraffitiObject<Schema>,
533
+ {
534
+ tombstoneRetention: number;
535
+ }
536
+ >;
558
537
 
559
538
  /**
560
- * Returns a list of all {@link GraffitiObjectBase.channels | `channels`}
539
+ * Returns statistics about all the {@link GraffitiObjectBase.channels | `channels`}
561
540
  * that an {@link GraffitiObjectBase.actor | `actor`} has posted to.
562
541
  * This is not very useful for most applications, but
563
542
  * necessary for certain applications where a user wants a
564
543
  * global view of all their Graffiti data or to debug
565
544
  * channel usage.
566
545
  *
567
- * @group Utilities
546
+ * @group Query Methods
568
547
  *
569
- * @returns A stream the {@link GraffitiObjectBase.channels | `channel`}s
548
+ * @returns A stream of all {@link GraffitiObjectBase.channels | `channel`}s
570
549
  * that the {@link GraffitiObjectBase.actor | `actor`} has posted to.
571
550
  * The `lastModified` field is the time that the user last modified an
572
551
  * object in that channel. The `count` field is the number of objects
573
552
  * that the user has posted to that channel.
553
+ * {@link GraffitiObjectBase.tombstone | `tombstone`}d objects are not included
554
+ * in either the `count` or `lastModified` fields.
574
555
  */
575
- abstract listChannels(
556
+ abstract channelStats(
576
557
  /**
577
558
  * An implementation-specific object with information to authenticate the
578
559
  * {@link GraffitiObjectBase.actor | `actor`}.
@@ -580,37 +561,10 @@ export abstract class Graffiti {
580
561
  session: GraffitiSession,
581
562
  ): GraffitiStream<{
582
563
  channel: string;
583
- lastModified: number;
584
564
  count: number;
565
+ lastModified: number;
585
566
  }>;
586
567
 
587
- /**
588
- * Returns a list of all {@link GraffitiObjectBase | objects} a user has posted that are
589
- * not associated with any {@link GraffitiObjectBase.channels | `channel`}, i.e. orphaned objects.
590
- * This is not very useful for most applications, but
591
- * necessary for certain applications where a user wants a
592
- * global view of all their Graffiti data or to debug
593
- * channel usage.
594
- *
595
- * @group Utilities
596
- *
597
- * @returns A stream of the {@link GraffitiObjectBase.name | `name`}
598
- * and {@link GraffitiObjectBase.source | `source`} of the orphaned objects
599
- * that the {@link GraffitiObjectBase.actor | `actor`} has posted to.
600
- * The {@link GraffitiObjectBase.lastModified | lastModified} field is the
601
- * time that the user last modified the orphan.
602
- */
603
- abstract listOrphans(session: GraffitiSession): GraffitiStream<{
604
- name: string;
605
- source: string;
606
- lastModified: string;
607
- }>;
608
-
609
- /**
610
- * The age at which a query for a session will be considered expired.
611
- */
612
- // abstract readonly maxAge: number;
613
-
614
568
  /**
615
569
  * Begins the login process. Depending on the implementation, this may
616
570
  * involve redirecting the user to a login page or opening a popup,
@@ -682,6 +636,102 @@ export abstract class Graffiti {
682
636
  * @group Session Management
683
637
  */
684
638
  abstract readonly sessionEvents: EventTarget;
639
+
640
+ /**
641
+ * This method has the same signature as {@link discover} but listens for
642
+ * changes made via {@link put}, {@link patch}, and {@link delete} or
643
+ * fetched from {@link get}, {@link discover}, and {@link recoverOrphans}
644
+ * and then streams appropriate changes to provide a responsive and
645
+ * consistent user experience.
646
+ *
647
+ * Unlike {@link discover}, this method continuously listens for changes
648
+ * and will not terminate unless the user calls the `return` method on the iterator
649
+ * or `break`s out of the loop.
650
+ *
651
+ * Example 1: Suppose a user publishes a post using {@link put}. If the feed
652
+ * displaying that user's posts is using {@link synchronizeDiscover} to listen for changes,
653
+ * then the user's new post will instantly appear in their feed, giving the UI a
654
+ * responsive feel.
655
+ *
656
+ * Example 2: Suppose one of a user's friends changes their name. As soon as the
657
+ * user's application receives one notice of that change (using {@link get}
658
+ * or {@link discover}), then {@link synchronizeDiscover} listeners can be used to update
659
+ * all instance's of that friend's name in the user's application instantly,
660
+ * providing a consistent user experience.
661
+ *
662
+ * @group Synchronize Methods
663
+ */
664
+ abstract synchronizeDiscover<Schema extends JSONSchema4>(
665
+ /**
666
+ * The {@link GraffitiObjectBase.channels | `channels`} that the objects must be associated with.
667
+ */
668
+ channels: string[],
669
+ /**
670
+ * A [JSON Schema](https://json-schema.org) that objects must satisfy.
671
+ */
672
+ schema: Schema,
673
+ /**
674
+ * An implementation-specific object with information to authenticate the
675
+ * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
676
+ * only objects that have no {@link GraffitiObjectBase.allowed | `allowed`}
677
+ * property will be returned.
678
+ */
679
+ session?: GraffitiSession | null,
680
+ ): GraffitiStream<GraffitiObject<Schema>>;
681
+
682
+ /**
683
+ * This method has the same signature as {@link get} but, like {@link synchronizeDiscover},
684
+ * it listens for changes made via {@link put}, {@link patch}, and {@link delete} or
685
+ * fetched from {@link get}, {@link discover}, and {@link recoverOrphans} and then
686
+ * streams appropriate changes to provide a responsive and consistent user experience.
687
+ *
688
+ * Unlike {@link get}, which returns a single result, this method continuously
689
+ * listens for changes which are output as an asynchronous {@link GraffitiStream}.
690
+ *
691
+ * @group Synchronize Methods
692
+ */
693
+ abstract synchronizeGet<Schema extends JSONSchema4>(
694
+ /**
695
+ * The location of the object to get.
696
+ */
697
+ locationOrUri: GraffitiLocation | string,
698
+ /**
699
+ * The JSON schema to validate the retrieved object against.
700
+ */
701
+ schema: Schema,
702
+ /**
703
+ * An implementation-specific object with information to authenticate the
704
+ * {@link GraffitiObjectBase.actor | `actor`}. If no `session` is provided,
705
+ * the retrieved object's {@link GraffitiObjectBase.allowed | `allowed`}
706
+ * property must be `undefined`.
707
+ */
708
+ session?: GraffitiSession | null,
709
+ ): GraffitiStream<GraffitiObject<Schema>>;
710
+
711
+ /**
712
+ * This method has the same signature as {@link recoverOrphans} but,
713
+ * like {@link synchronizeDiscover}, it listens for changes made via
714
+ * {@link put}, {@link patch}, and {@link delete} or fetched from
715
+ * {@link get}, {@link discover}, and {@link recoverOrphans} and then
716
+ * streams appropriate changes to provide a responsive and consistent user experience.
717
+ *
718
+ * Unlike {@link recoverOrphans}, this method continuously listens for changes
719
+ * and will not terminate unless the user calls the `return` method on the iterator
720
+ * or `break`s out of the loop.
721
+ *
722
+ * @group Synchronize Methods
723
+ */
724
+ abstract synchronizeRecoverOrphans<Schema extends JSONSchema4>(
725
+ /**
726
+ * A [JSON Schema](https://json-schema.org) that orphaned objects must satisfy.
727
+ */
728
+ schema: Schema,
729
+ /**
730
+ * An implementation-specific object with information to authenticate the
731
+ * {@link GraffitiObjectBase.actor | `actor`}.
732
+ */
733
+ session: GraffitiSession,
734
+ ): GraffitiStream<GraffitiObject<Schema>>;
685
735
  }
686
736
 
687
737
  /**
package/src/2-types.ts CHANGED
@@ -245,7 +245,7 @@ export interface GraffitiPatch {
245
245
  /**
246
246
  * This type represents a stream of data that are
247
247
  * returned by Graffiti's query-like operations such as
248
- * {@link Graffiti.discover} and {@link Graffiti.listChannels}.
248
+ * {@link Graffiti.discover} and {@link Graffiti.recoverOrphans}.
249
249
  *
250
250
  * Errors are returned within the stream rather than as
251
251
  * exceptions that would halt the entire stream. This is because
@@ -310,8 +310,11 @@ export type GraffitiLogoutEvent = CustomEvent<
310
310
  * and restore any previously active sessions.
311
311
  * Successful session restores will be returned in parallel as
312
312
  * their own {@link GraffitiLoginEvent} events.
313
- * This event optionally return an `href` property
314
- * if there were any redirects during the restoration process.
313
+ *
314
+ * This event optionally returns an `href` property
315
+ * representing the URL the user originated a login request
316
+ * from, which may be useful for redirecting the user back to
317
+ * the page they were on after login.
315
318
  * The event name to listen for is `initialized`.
316
319
  */
317
320
  export type GraffitiSessionInitializedEvent = CustomEvent<
@@ -0,0 +1,121 @@
1
+ import { it, expect, describe, assert } from "vitest";
2
+ import type { GraffitiFactory, GraffitiSession } from "@graffiti-garden/api";
3
+ import { randomPutObject, randomString } from "./utils";
4
+
5
+ export const graffitiChannelStatsTests = (
6
+ useGraffiti: GraffitiFactory,
7
+ useSession1: () => GraffitiSession,
8
+ useSession2: () => GraffitiSession,
9
+ ) => {
10
+ describe("channel stats", () => {
11
+ it("list channels", async () => {
12
+ const graffiti = useGraffiti();
13
+ const session = useSession1();
14
+
15
+ const existingChannels: Map<string, number> = new Map();
16
+ const channelIterator1 = graffiti.channelStats(session);
17
+ for await (const channel of channelIterator1) {
18
+ if (channel.error) continue;
19
+ existingChannels.set(channel.value.channel, channel.value.count);
20
+ }
21
+
22
+ const channels = [randomString(), randomString(), randomString()];
23
+
24
+ // Add one value to channels[0],
25
+ // two values to both channels[0] and channels[1],
26
+ // three values to all channels
27
+ // one value to channels[2]
28
+ for (let i = 0; i < 3; i++) {
29
+ for (let j = 0; j < i + 1; j++) {
30
+ await graffiti.put(
31
+ {
32
+ value: {
33
+ index: j,
34
+ },
35
+ channels: channels.slice(0, i + 1),
36
+ },
37
+ session,
38
+ );
39
+ }
40
+ }
41
+ await graffiti.put(
42
+ { value: { index: 3 }, channels: [channels[2]] },
43
+ session,
44
+ );
45
+
46
+ const channelIterator2 = graffiti.channelStats(session);
47
+ let newChannels: Map<string, number> = new Map();
48
+ for await (const channel of channelIterator2) {
49
+ if (channel.error) continue;
50
+ newChannels.set(channel.value.channel, channel.value.count);
51
+ }
52
+ // Filter out existing channels
53
+ newChannels = new Map(
54
+ Array.from(newChannels).filter(
55
+ ([channel, count]) => !existingChannels.has(channel),
56
+ ),
57
+ );
58
+ expect(newChannels.size).toBe(3);
59
+ expect(newChannels.get(channels[0])).toBe(6);
60
+ expect(newChannels.get(channels[1])).toBe(5);
61
+ expect(newChannels.get(channels[2])).toBe(4);
62
+ });
63
+
64
+ it("list channels with deleted channel", async () => {
65
+ const graffiti = useGraffiti();
66
+ const session = useSession1();
67
+
68
+ const channels = [randomString(), randomString(), randomString()];
69
+
70
+ // Add an item with two channels
71
+ const before = await graffiti.put(
72
+ {
73
+ value: { index: 2 },
74
+ channels: channels.slice(1),
75
+ },
76
+ session,
77
+ );
78
+
79
+ // Add an item with all channels
80
+ const first = await graffiti.put(
81
+ { value: { index: 0 }, channels },
82
+ session,
83
+ );
84
+ // But then delete it
85
+ await graffiti.delete(first, session);
86
+
87
+ // Create a new object with only one channel
88
+ const second = await graffiti.put(
89
+ {
90
+ value: { index: 1 },
91
+ channels: channels.slice(2),
92
+ },
93
+ session,
94
+ );
95
+
96
+ const channelIterator = graffiti.channelStats(session);
97
+
98
+ let got1 = 0;
99
+ let got2 = 0;
100
+ for await (const result of channelIterator) {
101
+ if (result.error) continue;
102
+ const { channel, count, lastModified } = result.value;
103
+ assert(
104
+ channel !== channels[0],
105
+ "There should not be an object in channel[0]",
106
+ );
107
+ if (channel === channels[1]) {
108
+ expect(count).toBe(1);
109
+ expect(lastModified).toBe(before.lastModified);
110
+ got1++;
111
+ } else if (channel === channels[2]) {
112
+ expect(count).toBe(2);
113
+ expect(lastModified).toBe(second.lastModified);
114
+ got2++;
115
+ }
116
+ }
117
+ expect(got1).toBe(1);
118
+ expect(got2).toBe(1);
119
+ });
120
+ });
121
+ };
package/tests/src/crud.ts CHANGED
@@ -63,7 +63,7 @@ export const graffitiCRUDTests = (
63
63
  expect(beforeReplaced.name).toEqual(previous.name);
64
64
  expect(beforeReplaced.actor).toEqual(previous.actor);
65
65
  expect(beforeReplaced.source).toEqual(previous.source);
66
- expect(beforeReplaced.lastModified).toBeGreaterThanOrEqual(
66
+ expect(beforeReplaced.lastModified).toBeGreaterThan(
67
67
  gotten.lastModified,
68
68
  );
69
69
 
@@ -77,14 +77,29 @@ export const graffitiCRUDTests = (
77
77
  const beforeDeleted = await graffiti.delete(afterReplaced, session);
78
78
  expect(beforeDeleted.tombstone).toEqual(true);
79
79
  expect(beforeDeleted.value).toEqual(newValue);
80
- expect(beforeDeleted.lastModified).toBeGreaterThanOrEqual(
80
+ expect(beforeDeleted.lastModified).toBeGreaterThan(
81
81
  beforeReplaced.lastModified,
82
82
  );
83
83
 
84
- // Try to get it and fail
85
- await expect(graffiti.get(afterReplaced, {})).rejects.toThrow(
86
- GraffitiErrorNotFound,
87
- );
84
+ // Get a tombstone
85
+ const final = await graffiti.get(afterReplaced, {});
86
+ expect(final).toEqual(beforeDeleted);
87
+ });
88
+
89
+ it("get non-existant", async () => {
90
+ const graffiti = useGraffiti();
91
+ const session = useSession1();
92
+
93
+ const putted = await graffiti.put(randomPutObject(), session);
94
+ await expect(
95
+ graffiti.get(
96
+ {
97
+ ...putted,
98
+ name: randomString(),
99
+ },
100
+ {},
101
+ ),
102
+ ).rejects.toBeInstanceOf(GraffitiErrorNotFound);
88
103
  });
89
104
 
90
105
  it("put, get, delete with wrong actor", async () => {
@@ -218,10 +233,14 @@ export const graffitiCRUDTests = (
218
233
  expect(gotten.channels).toEqual(channels);
219
234
 
220
235
  // But not without session
221
- await expect(graffiti.get(putted, {})).rejects.toThrow();
236
+ await expect(graffiti.get(putted, {})).rejects.toBeInstanceOf(
237
+ GraffitiErrorNotFound,
238
+ );
222
239
 
223
240
  // Or the wrong session
224
- await expect(graffiti.get(putted, {}, session2)).rejects.toThrow();
241
+ await expect(graffiti.get(putted, {}, session2)).rejects.toBeInstanceOf(
242
+ GraffitiErrorNotFound,
243
+ );
225
244
  });
226
245
 
227
246
  it("put and get with specific access control", async () => {
@@ -250,7 +269,9 @@ export const graffitiCRUDTests = (
250
269
  expect(gotten.channels).toEqual(channels);
251
270
 
252
271
  // But not without session
253
- await expect(graffiti.get(putted, {})).rejects.toThrow();
272
+ await expect(graffiti.get(putted, {})).rejects.toBeInstanceOf(
273
+ GraffitiErrorNotFound,
274
+ );
254
275
 
255
276
  const gotten2 = await graffiti.get(putted, {}, session2);
256
277
  expect(gotten2.value).toEqual(value);
@@ -287,6 +308,17 @@ export const graffitiCRUDTests = (
287
308
  await graffiti.delete(putted, session);
288
309
  });
289
310
 
311
+ it("patch deleted object", async () => {
312
+ const graffiti = useGraffiti();
313
+ const session = useSession1();
314
+
315
+ const putted = await graffiti.put(randomPutObject(), session);
316
+ const deleted = await graffiti.delete(putted, session);
317
+ await expect(
318
+ graffiti.patch({}, putted, session),
319
+ ).rejects.toBeInstanceOf(GraffitiErrorNotFound);
320
+ });
321
+
290
322
  it("deep patch", async () => {
291
323
  const graffiti = useGraffiti();
292
324
  const session = useSession1();
@@ -2,3 +2,5 @@ export * from "./location";
2
2
  export * from "./crud";
3
3
  export * from "./synchronize";
4
4
  export * from "./discover";
5
+ export * from "./orphans";
6
+ export * from "./channel-stats";
@@ -0,0 +1,70 @@
1
+ import { it, expect, describe, assert } from "vitest";
2
+ import type { GraffitiFactory, GraffitiSession } from "@graffiti-garden/api";
3
+ import { randomPutObject, randomString } from "./utils";
4
+
5
+ export const graffitiOrphanTests = (
6
+ useGraffiti: GraffitiFactory,
7
+ useSession1: () => GraffitiSession,
8
+ useSession2: () => GraffitiSession,
9
+ ) => {
10
+ describe("recoverOrphans", () => {
11
+ it("list orphans", async () => {
12
+ const graffiti = useGraffiti();
13
+ const session = useSession1();
14
+
15
+ const existingOrphans: string[] = [];
16
+ const orphanIterator1 = graffiti.recoverOrphans({}, session);
17
+ for await (const orphan of orphanIterator1) {
18
+ if (orphan.error) continue;
19
+ existingOrphans.push(orphan.value.name);
20
+ }
21
+
22
+ const object = randomPutObject();
23
+ object.channels = [];
24
+ const putted = await graffiti.put(object, session);
25
+ const orphanIterator2 = graffiti.recoverOrphans({}, session);
26
+ let numResults = 0;
27
+ for await (const orphan of orphanIterator2) {
28
+ if (orphan.error) continue;
29
+ if (orphan.value.name === putted.name) {
30
+ numResults++;
31
+ expect(orphan.value.source).toBe(putted.source);
32
+ expect(orphan.value.lastModified).toBe(putted.lastModified);
33
+ }
34
+ }
35
+ expect(numResults).toBe(1);
36
+ });
37
+
38
+ it("replaced orphan, no longer", async () => {
39
+ const graffiti = useGraffiti();
40
+ const session = useSession1();
41
+
42
+ const object = randomPutObject();
43
+ object.channels = [];
44
+ const putOrphan = await graffiti.put(object, session);
45
+
46
+ const putNotOrphan = await graffiti.put(
47
+ {
48
+ ...putOrphan,
49
+ ...object,
50
+ channels: [randomString()],
51
+ },
52
+ session,
53
+ );
54
+ expect(putNotOrphan.name).toBe(putOrphan.name);
55
+
56
+ const orphanIterator = graffiti.recoverOrphans({}, session);
57
+ let numResults = 0;
58
+ for await (const orphan of orphanIterator) {
59
+ if (orphan.error) continue;
60
+ if (orphan.value.name === putOrphan.name) {
61
+ numResults++;
62
+ expect(orphan.value.tombstone).toBe(true);
63
+ expect(orphan.value.lastModified).toBe(putNotOrphan.lastModified);
64
+ expect(orphan.value.channels).toEqual([]);
65
+ }
66
+ }
67
+ expect(numResults).toBe(1);
68
+ });
69
+ });
70
+ };
@@ -4,9 +4,12 @@ import type { GraffitiPutObject, GraffitiStream } from "@graffiti-garden/api";
4
4
  export function randomString(): string {
5
5
  const array = new Uint8Array(16);
6
6
  crypto.getRandomValues(array);
7
- return Array.from(array)
7
+ const str = Array.from(array)
8
8
  .map((b) => b.toString(16).padStart(2, "0"))
9
9
  .join("");
10
+
11
+ // check for unicode support
12
+ return str + "👩🏽‍❤️‍💋‍👩🏻🫱🏼‍🫲🏿";
10
13
  }
11
14
 
12
15
  export function randomValue() {