@graffiti-garden/wrapper-synchronize 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,23 @@
1
- import Ajv from "ajv-draft-04";
1
+ import type Ajv from "ajv";
2
2
  import { Graffiti } from "@graffiti-garden/api";
3
- import type { GraffitiSession, GraffitiObject, JSONSchema4, GraffitiStream } from "@graffiti-garden/api";
3
+ import type { GraffitiSession, GraffitiObject, JSONSchema, GraffitiStream } from "@graffiti-garden/api";
4
4
  import type { GraffitiObjectBase } from "@graffiti-garden/api";
5
+ import type { applyPatch } from "fast-json-patch";
5
6
  export type * from "@graffiti-garden/api";
6
7
  export type GraffitiSynchronizeCallback = (oldObject: GraffitiObjectBase, newObject?: GraffitiObjectBase) => void;
8
+ export interface GraffitiSynchronizeOptions {
9
+ /**
10
+ * Allows synchronize to listen to all objects, not just those
11
+ * that the session is allowed to see. This is useful to get a
12
+ * global view of all Graffiti objects passsing through the system,
13
+ * for example to build a client-side cache. Additional mechanisms
14
+ * should be in place to ensure that users do not see objects or
15
+ * properties they are not allowed to see.
16
+ *
17
+ * Default: `false`
18
+ */
19
+ omniscient?: boolean;
20
+ }
7
21
  /**
8
22
  * Wraps the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)
9
23
  * so that changes made or received in one part of an application
@@ -47,15 +61,19 @@ export type GraffitiSynchronizeCallback = (oldObject: GraffitiObjectBase, newObj
47
61
  * streams appropriate changes to provide a responsive and consistent user experience.
48
62
  */
49
63
  export declare class GraffitiSynchronize extends Graffiti {
50
- protected readonly ajv: Ajv;
64
+ protected ajv_: Promise<Ajv> | undefined;
65
+ protected applyPatch_: Promise<typeof applyPatch> | undefined;
51
66
  protected readonly graffiti: Graffiti;
52
67
  protected readonly callbacks: Set<GraffitiSynchronizeCallback>;
68
+ protected readonly options: GraffitiSynchronizeOptions;
53
69
  channelStats: Graffiti["channelStats"];
54
70
  locationToUri: Graffiti["locationToUri"];
55
71
  uriToLocation: Graffiti["uriToLocation"];
56
72
  login: Graffiti["login"];
57
73
  logout: Graffiti["logout"];
58
74
  sessionEvents: Graffiti["sessionEvents"];
75
+ get ajv(): Promise<Ajv>;
76
+ get applyPatch(): Promise<typeof applyPatch>;
59
77
  /**
60
78
  * Wraps a Graffiti API instance to provide the synchronize methods.
61
79
  * The GraffitiSyncrhonize class rather than the Graffiti class
@@ -66,14 +84,8 @@ export declare class GraffitiSynchronize extends Graffiti {
66
84
  * The [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)
67
85
  * instance to wrap.
68
86
  */
69
- graffiti: Graffiti,
70
- /**
71
- * An optional instance of Ajv to use for validating
72
- * objects before dispatching them to listeners.
73
- * If not provided, a new instance of Ajv will be created.
74
- */
75
- ajv?: Ajv);
76
- protected synchronize<Schema extends JSONSchema4>(matchObject: (object: GraffitiObjectBase) => boolean, channels: string[], schema: Schema, session?: GraffitiSession | null): GraffitiStream<GraffitiObject<Schema>>;
87
+ graffiti: Graffiti, options?: GraffitiSynchronizeOptions);
88
+ protected synchronize<Schema extends JSONSchema>(matchObject: (object: GraffitiObjectBase) => boolean, channels: string[], schema: Schema, session?: GraffitiSession | null): GraffitiStream<GraffitiObject<Schema>>;
77
89
  /**
78
90
  * This method has the same signature as {@link discover} but listens for
79
91
  * changes made via {@link put}, {@link patch}, and {@link delete} or
@@ -87,7 +99,7 @@ export declare class GraffitiSynchronize extends Graffiti {
87
99
  *
88
100
  * @group Synchronize Methods
89
101
  */
90
- synchronizeDiscover<Schema extends JSONSchema4>(...args: Parameters<typeof Graffiti.prototype.discover<Schema>>): GraffitiStream<GraffitiObject<Schema>>;
102
+ synchronizeDiscover<Schema extends JSONSchema>(...args: Parameters<typeof Graffiti.prototype.discover<Schema>>): GraffitiStream<GraffitiObject<Schema>>;
91
103
  /**
92
104
  * This method has the same signature as {@link get} but
93
105
  * listens for changes made via {@link put}, {@link patch}, and {@link delete} or
@@ -99,7 +111,7 @@ export declare class GraffitiSynchronize extends Graffiti {
99
111
  *
100
112
  * @group Synchronize Methods
101
113
  */
102
- synchronizeGet<Schema extends JSONSchema4>(...args: Parameters<typeof Graffiti.prototype.get<Schema>>): GraffitiStream<GraffitiObject<Schema>>;
114
+ synchronizeGet<Schema extends JSONSchema>(...args: Parameters<typeof Graffiti.prototype.get<Schema>>): GraffitiStream<GraffitiObject<Schema>>;
103
115
  /**
104
116
  * This method has the same signature as {@link recoverOrphans} but
105
117
  * listens for changes made via
@@ -113,13 +125,24 @@ export declare class GraffitiSynchronize extends Graffiti {
113
125
  *
114
126
  * @group Synchronize Methods
115
127
  */
116
- synchronizeRecoverOrphans<Schema extends JSONSchema4>(...args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>): GraffitiStream<GraffitiObject<Schema>>;
128
+ synchronizeRecoverOrphans<Schema extends JSONSchema>(...args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>): GraffitiStream<GraffitiObject<Schema>>;
129
+ /**
130
+ * Streams changes made to *any* object in *any* channel
131
+ * and made by *any* user. You may want to use it in conjuction with
132
+ * {@link GraffitiSyncrhonizeOptions.omniscient} to get a global view
133
+ * of all Graffiti objects passing through the system. This is useful
134
+ * for building a client-side cache, for example.
135
+ *
136
+ * Be careful using this method. Without additional filters it can
137
+ * expose the user to content out of context.
138
+ */
139
+ synchronizeAll(schema?: JSONSchema, session?: GraffitiSession | null): GraffitiStream<GraffitiObjectBase>;
117
140
  protected synchronizeDispatch(oldObject: GraffitiObjectBase, newObject?: GraffitiObjectBase, waitForListeners?: boolean): Promise<void>;
118
141
  get: Graffiti["get"];
119
142
  put: Graffiti["put"];
120
143
  patch: Graffiti["patch"];
121
144
  delete: Graffiti["delete"];
122
- protected objectStream<Schema extends JSONSchema4>(iterator: ReturnType<typeof Graffiti.prototype.discover<Schema>>): AsyncGenerator<{
145
+ protected objectStream<Schema extends JSONSchema>(iterator: ReturnType<typeof Graffiti.prototype.discover<Schema>>): AsyncGenerator<{
123
146
  error: Error;
124
147
  source: string;
125
148
  } | {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,UAAU,EACV,cAAc,EACf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AASlD,mBAAmB,sBAAsB,CAAC;AAE1C,MAAM,MAAM,2BAA2B,GAAG,CACxC,SAAS,EAAE,kBAAkB,EAC7B,SAAS,CAAC,EAAE,kBAAkB,KAC3B,IAAI,CAAC;AAEV,MAAM,WAAW,0BAA0B;IACzC;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,qBAAa,mBAAoB,SAAQ,QAAQ;IAC/C,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACzC,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,OAAO,UAAU,CAAC,GAAG,SAAS,CAAC;IAC9D,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IACtC,SAAS,CAAC,QAAQ,CAAC,SAAS,mCAA0C;IACtE,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,0BAA0B,CAAC;IAEvD,YAAY,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC;IACvC,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACzC,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IACzC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IACzB,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC3B,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC,CAAC;IAEzC,IAAI,GAAG,iBAQN;IAED,IAAI,UAAU,+BAQb;IAED;;;;OAIG;;IAED;;;OAGG;IACH,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,0BAA0B;IAatC,SAAS,CAAC,WAAW,CAAC,MAAM,SAAS,UAAU,EAC7C,WAAW,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,OAAO,EACpD,QAAQ,EAAE,MAAM,EAAE,EAClB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI;IAqClC;;;;;;;;;;;;OAYG;IACH,mBAAmB,CAAC,MAAM,SAAS,UAAU,EAC3C,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAC9D,cAAc,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IAQzC;;;;;;;;;;OAUG;IACH,cAAc,CAAC,MAAM,SAAS,UAAU,EACtC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GACzD,cAAc,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IAUzC;;;;;;;;;;;;OAYG;IACH,yBAAyB,CAAC,MAAM,SAAS,UAAU,EACjD,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,SAAS,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,GACpE,cAAc,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IAQzC;;;;;;;;;OASG;IACH,cAAc,CACZ,MAAM,CAAC,EAAE,UAAU,EACnB,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,GAC/B,cAAc,CAAC,kBAAkB,CAAC;cAIrB,mBAAmB,CACjC,SAAS,EAAE,kBAAkB,EAC7B,SAAS,CAAC,EAAE,kBAAkB,EAC9B,gBAAgB,UAAQ;IA8B1B,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAIlB;IAEF,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAYlB;IAEF,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,CAStB;IAEF,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAIxB;IAEF,SAAS,CAAC,YAAY,CAAC,MAAM,SAAS,UAAU,EAC9C,QAAQ,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;;;;;;;;;IAiBlE,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAG5B;IAEF,cAAc,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAGxC;CACH"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@graffiti-garden/wrapper-synchronize",
3
- "version": "0.0.1",
4
- "description": "TBD",
3
+ "version": "0.0.3",
4
+ "description": "Internal synchronization for the Graffiti API",
5
5
  "types": "./dist/index.d.ts",
6
6
  "module": "./dist/esm/index.js",
7
7
  "main": "./dist/cjs/index.js",
@@ -50,14 +50,13 @@
50
50
  "tsx": "^4.19.2",
51
51
  "typedoc": "^0.27.6",
52
52
  "typescript": "^5.7.3",
53
- "vitest": "^2.1.9"
53
+ "vitest": "^3.0.5"
54
54
  },
55
55
  "dependencies": {
56
- "@graffiti-garden/api": "^0.3.0",
57
- "@graffiti-garden/implementation-local": "^0.2.12",
56
+ "@graffiti-garden/api": "^0.4.1",
57
+ "@graffiti-garden/implementation-local": "^0.4.0",
58
58
  "@repeaterjs/repeater": "^3.0.6",
59
59
  "ajv": "^8.17.1",
60
- "ajv-draft-04": "^1.0.0",
61
60
  "fast-json-patch": "^3.1.1"
62
61
  }
63
62
  }
package/src/index.spec.ts CHANGED
@@ -2,8 +2,9 @@ import { it, expect, describe, assert, beforeAll } from "vitest";
2
2
  import type { GraffitiSession } from "@graffiti-garden/api";
3
3
  import { GraffitiLocal } from "@graffiti-garden/implementation-local";
4
4
  import { randomPutObject, randomString } from "@graffiti-garden/api/tests";
5
+ import { GraffitiSynchronize } from "./index";
5
6
 
6
- const useGraffiti = () => new GraffitiLocal();
7
+ const useGraffiti = () => new GraffitiSynchronize(new GraffitiLocal());
7
8
  const graffiti = useGraffiti();
8
9
 
9
10
  const useSession1 = async () => {
@@ -32,7 +33,7 @@ describe.concurrent("synchronizeDiscover", () => {
32
33
 
33
34
  const object = randomPutObject();
34
35
  const channels = object.channels.slice(1);
35
- const putted = await graffiti1.put(object, session);
36
+ const putted = await graffiti1.put<{}>(object, session);
36
37
 
37
38
  const graffiti2 = useGraffiti();
38
39
  const next = graffiti2.synchronizeDiscover(channels, {}).next();
@@ -55,7 +56,7 @@ describe.concurrent("synchronizeDiscover", () => {
55
56
 
56
57
  const oldValue = { hello: "world" };
57
58
  const oldChannels = [beforeChannel, sharedChannel];
58
- const putted = await graffiti.put(
59
+ const putted = await graffiti.put<{}>(
59
60
  {
60
61
  value: oldValue,
61
62
  channels: oldChannels,
@@ -71,7 +72,7 @@ describe.concurrent("synchronizeDiscover", () => {
71
72
  // Replace the object
72
73
  const newValue = { goodbye: "world" };
73
74
  const newChannels = [afterChannel, sharedChannel];
74
- await graffiti.put(
75
+ await graffiti.put<{}>(
75
76
  {
76
77
  ...putted,
77
78
  value: newValue,
@@ -118,7 +119,7 @@ describe.concurrent("synchronizeDiscover", () => {
118
119
 
119
120
  const oldValue = { hello: "world" };
120
121
  const oldChannels = [beforeChannel, sharedChannel];
121
- const putted = await graffiti.put(
122
+ const putted = await graffiti.put<{}>(
122
123
  {
123
124
  value: oldValue,
124
125
  channels: oldChannels,
@@ -194,7 +195,7 @@ describe.concurrent("synchronizeDiscover", () => {
194
195
 
195
196
  const oldValue = { hello: "world" };
196
197
  const oldChannels = [randomString(), ...channels.slice(1)];
197
- const putted = await graffiti.put(
198
+ const putted = await graffiti.put<{}>(
198
199
  {
199
200
  value: oldValue,
200
201
  channels: oldChannels,
@@ -223,7 +224,7 @@ describe.concurrent("synchronizeDiscover", () => {
223
224
 
224
225
  for (let i = 0; i < 10; i++) {
225
226
  const next = iterator.next();
226
- const putted = graffiti.put(object, session);
227
+ const putted = graffiti.put<{}>(object, session);
227
228
 
228
229
  let first: undefined | string = undefined;
229
230
  next.then(() => {
@@ -282,10 +283,13 @@ describe.concurrent("synchronizeDiscover", () => {
282
283
  hello: "world",
283
284
  };
284
285
  const allowed = [randomString(), session2.actor];
285
- await graffiti.put({ value, channels: allChannels, allowed }, session1);
286
+ await graffiti.put<{}>({ value, channels: allChannels, allowed }, session1);
286
287
 
287
288
  // Expect no session to time out!
288
289
  await expect(
290
+ // @ts-ignore - otherwise you might get
291
+ // "Type instantiation is excessively deep
292
+ // and possibly infinite."
289
293
  Promise.race([
290
294
  noSession,
291
295
  new Promise((resolve, rejects) => setTimeout(rejects, 100, "Timeout")),
@@ -327,14 +331,14 @@ describe.concurrent("synchronizeGet", () => {
327
331
 
328
332
  it("replace, delete", async () => {
329
333
  const object = randomPutObject();
330
- const putted = await graffiti.put(object, session);
334
+ const putted = await graffiti.put<{}>(object, session);
331
335
 
332
336
  const iterator = graffiti.synchronizeGet(putted, {});
333
337
  const next = iterator.next();
334
338
 
335
339
  // Change the object
336
340
  const newValue = { goodbye: "world" };
337
- const putted2 = await graffiti.put(
341
+ const putted2 = await graffiti.put<{}>(
338
342
  {
339
343
  ...putted,
340
344
  value: newValue,
@@ -360,8 +364,11 @@ describe.concurrent("synchronizeGet", () => {
360
364
  expect(result2.value.lastModified).toEqual(deleted.lastModified);
361
365
 
362
366
  // Put something else
363
- await graffiti.put(randomPutObject(), session);
367
+ await graffiti.put<{}>(randomPutObject(), session);
364
368
  await expect(
369
+ // @ts-ignore - otherwise you might get
370
+ // "Type instantiation is excessively deep
371
+ // and possibly infinite."
365
372
  Promise.race([
366
373
  iterator.next(),
367
374
  new Promise((resolve, reject) => setTimeout(reject, 100, "Timeout")),
@@ -371,7 +378,7 @@ describe.concurrent("synchronizeGet", () => {
371
378
 
372
379
  it("not allowed", async () => {
373
380
  const object = randomPutObject();
374
- const putted = await graffiti.put(object, session1);
381
+ const putted = await graffiti.put<{}>(object, session1);
375
382
 
376
383
  const iterator1 = graffiti.synchronizeGet(putted, {}, session1);
377
384
  const iterator2 = graffiti.synchronizeGet(putted, {}, session2);
@@ -380,7 +387,7 @@ describe.concurrent("synchronizeGet", () => {
380
387
  const next2 = iterator2.next();
381
388
 
382
389
  const newValue = { goodbye: "world" };
383
- const putted2 = await graffiti.put(
390
+ const putted2 = await graffiti.put<{}>(
384
391
  {
385
392
  ...putted,
386
393
  ...object,
@@ -406,3 +413,59 @@ describe.concurrent("synchronizeGet", () => {
406
413
  expect(result2.value.lastModified).toEqual(putted2.lastModified);
407
414
  });
408
415
  });
416
+
417
+ // can't be concurrent because it gets ALL
418
+ describe("synchronizeAll", () => {
419
+ let session: GraffitiSession;
420
+ let session1: GraffitiSession;
421
+ let session2: GraffitiSession;
422
+ beforeAll(async () => {
423
+ session1 = await useSession1();
424
+ session = session1;
425
+ session2 = await useSession2();
426
+ });
427
+
428
+ it("sync from multiple channels and actors", async () => {
429
+ const object1 = randomPutObject();
430
+ const object2 = randomPutObject();
431
+
432
+ expect(object1.channels).not.toEqual(object2.channels);
433
+
434
+ const iterator = graffiti.synchronizeAll();
435
+
436
+ const next1 = iterator.next();
437
+ const next2 = iterator.next();
438
+
439
+ await graffiti.put<{}>(object1, session1);
440
+ await graffiti.put<{}>(object2, session2);
441
+
442
+ const result1 = (await next1).value;
443
+ const result2 = (await next2).value;
444
+ assert(result1 && !result1.error);
445
+ assert(result2 && !result2.error);
446
+
447
+ expect(result1.value.value).toEqual(object1.value);
448
+ expect(result1.value.channels).toEqual([]);
449
+ expect(result2.value.value).toEqual(object2.value);
450
+ });
451
+
452
+ it("omniscient", async () => {
453
+ const graffiti = new GraffitiSynchronize(new GraffitiLocal(), {
454
+ omniscient: true,
455
+ });
456
+
457
+ const object1 = randomPutObject();
458
+ object1.allowed = [randomString()];
459
+
460
+ const iterator = graffiti.synchronizeAll();
461
+ const next = iterator.next();
462
+
463
+ await graffiti.put<{}>(object1, session1);
464
+
465
+ const result = (await next).value;
466
+ assert(result && !result.error);
467
+ expect(result.value.value).toEqual(object1.value);
468
+ expect(result.value.channels).toEqual(object1.channels);
469
+ expect(result.value.allowed).toEqual(object1.allowed);
470
+ });
471
+ });
package/src/index.ts CHANGED
@@ -1,17 +1,17 @@
1
- import Ajv from "ajv-draft-04";
1
+ import type Ajv from "ajv";
2
2
  import { Graffiti } from "@graffiti-garden/api";
3
3
  import type {
4
4
  GraffitiSession,
5
5
  GraffitiObject,
6
- JSONSchema4,
6
+ JSONSchema,
7
7
  GraffitiStream,
8
8
  } from "@graffiti-garden/api";
9
9
  import type { GraffitiObjectBase } from "@graffiti-garden/api";
10
10
  import { Repeater } from "@repeaterjs/repeater";
11
- import { applyPatch } from "fast-json-patch";
11
+ import type { applyPatch } from "fast-json-patch";
12
12
  import {
13
13
  applyGraffitiPatch,
14
- attemptAjvCompile,
14
+ compileGraffitiObjectSchema,
15
15
  isActorAllowedGraffitiObject,
16
16
  locationToUri,
17
17
  maskGraffitiObject,
@@ -24,6 +24,20 @@ export type GraffitiSynchronizeCallback = (
24
24
  newObject?: GraffitiObjectBase,
25
25
  ) => void;
26
26
 
27
+ export interface GraffitiSynchronizeOptions {
28
+ /**
29
+ * Allows synchronize to listen to all objects, not just those
30
+ * that the session is allowed to see. This is useful to get a
31
+ * global view of all Graffiti objects passsing through the system,
32
+ * for example to build a client-side cache. Additional mechanisms
33
+ * should be in place to ensure that users do not see objects or
34
+ * properties they are not allowed to see.
35
+ *
36
+ * Default: `false`
37
+ */
38
+ omniscient?: boolean;
39
+ }
40
+
27
41
  /**
28
42
  * Wraps the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)
29
43
  * so that changes made or received in one part of an application
@@ -67,9 +81,11 @@ export type GraffitiSynchronizeCallback = (
67
81
  * streams appropriate changes to provide a responsive and consistent user experience.
68
82
  */
69
83
  export class GraffitiSynchronize extends Graffiti {
70
- protected readonly ajv: Ajv;
84
+ protected ajv_: Promise<Ajv> | undefined;
85
+ protected applyPatch_: Promise<typeof applyPatch> | undefined;
71
86
  protected readonly graffiti: Graffiti;
72
87
  protected readonly callbacks = new Set<GraffitiSynchronizeCallback>();
88
+ protected readonly options: GraffitiSynchronizeOptions;
73
89
 
74
90
  channelStats: Graffiti["channelStats"];
75
91
  locationToUri: Graffiti["locationToUri"];
@@ -78,6 +94,26 @@ export class GraffitiSynchronize extends Graffiti {
78
94
  logout: Graffiti["logout"];
79
95
  sessionEvents: Graffiti["sessionEvents"];
80
96
 
97
+ get ajv() {
98
+ if (!this.ajv_) {
99
+ this.ajv_ = (async () => {
100
+ const { default: Ajv } = await import("ajv");
101
+ return new Ajv({ strict: false });
102
+ })();
103
+ }
104
+ return this.ajv_;
105
+ }
106
+
107
+ get applyPatch() {
108
+ if (!this.applyPatch_) {
109
+ this.applyPatch_ = (async () => {
110
+ const { applyPatch } = await import("fast-json-patch");
111
+ return applyPatch;
112
+ })();
113
+ }
114
+ return this.applyPatch_;
115
+ }
116
+
81
117
  /**
82
118
  * Wraps a Graffiti API instance to provide the synchronize methods.
83
119
  * The GraffitiSyncrhonize class rather than the Graffiti class
@@ -89,15 +125,10 @@ export class GraffitiSynchronize extends Graffiti {
89
125
  * instance to wrap.
90
126
  */
91
127
  graffiti: Graffiti,
92
- /**
93
- * An optional instance of Ajv to use for validating
94
- * objects before dispatching them to listeners.
95
- * If not provided, a new instance of Ajv will be created.
96
- */
97
- ajv?: Ajv,
128
+ options?: GraffitiSynchronizeOptions,
98
129
  ) {
99
130
  super();
100
- this.ajv = ajv ?? new Ajv({ strict: false });
131
+ this.options = options ?? {};
101
132
  this.graffiti = graffiti;
102
133
  this.channelStats = graffiti.channelStats.bind(graffiti);
103
134
  this.locationToUri = graffiti.locationToUri.bind(graffiti);
@@ -107,16 +138,15 @@ export class GraffitiSynchronize extends Graffiti {
107
138
  this.sessionEvents = graffiti.sessionEvents;
108
139
  }
109
140
 
110
- protected synchronize<Schema extends JSONSchema4>(
141
+ protected synchronize<Schema extends JSONSchema>(
111
142
  matchObject: (object: GraffitiObjectBase) => boolean,
112
143
  channels: string[],
113
144
  schema: Schema,
114
145
  session?: GraffitiSession | null,
115
146
  ) {
116
- const validate = attemptAjvCompile(this.ajv, schema);
117
-
118
147
  const repeater: GraffitiStream<GraffitiObject<Schema>> = new Repeater(
119
148
  async (push, stop) => {
149
+ const validate = compileGraffitiObjectSchema(await this.ajv, schema);
120
150
  const callback: GraffitiSynchronizeCallback = (
121
151
  oldObjectRaw,
122
152
  newObjectRaw,
@@ -125,10 +155,13 @@ export class GraffitiSynchronize extends Graffiti {
125
155
  if (
126
156
  objectRaw &&
127
157
  matchObject(objectRaw) &&
128
- isActorAllowedGraffitiObject(objectRaw, session)
158
+ (this.options.omniscient ||
159
+ isActorAllowedGraffitiObject(objectRaw, session))
129
160
  ) {
130
161
  const object = { ...objectRaw };
131
- maskGraffitiObject(object, channels, session);
162
+ if (!this.options.omniscient) {
163
+ maskGraffitiObject(object, channels, session);
164
+ }
132
165
  if (validate(object)) {
133
166
  push({ value: object });
134
167
  break;
@@ -159,14 +192,14 @@ export class GraffitiSynchronize extends Graffiti {
159
192
  *
160
193
  * @group Synchronize Methods
161
194
  */
162
- synchronizeDiscover<Schema extends JSONSchema4>(
195
+ synchronizeDiscover<Schema extends JSONSchema>(
163
196
  ...args: Parameters<typeof Graffiti.prototype.discover<Schema>>
164
197
  ): GraffitiStream<GraffitiObject<Schema>> {
165
198
  const [channels, schema, session] = args;
166
199
  function matchObject(object: GraffitiObjectBase) {
167
200
  return object.channels.some((channel) => channels.includes(channel));
168
201
  }
169
- return this.synchronize(matchObject, channels, schema, session);
202
+ return this.synchronize<Schema>(matchObject, channels, schema, session);
170
203
  }
171
204
 
172
205
  /**
@@ -180,7 +213,7 @@ export class GraffitiSynchronize extends Graffiti {
180
213
  *
181
214
  * @group Synchronize Methods
182
215
  */
183
- synchronizeGet<Schema extends JSONSchema4>(
216
+ synchronizeGet<Schema extends JSONSchema>(
184
217
  ...args: Parameters<typeof Graffiti.prototype.get<Schema>>
185
218
  ): GraffitiStream<GraffitiObject<Schema>> {
186
219
  const [locationOrUri, schema, session] = args;
@@ -189,7 +222,7 @@ export class GraffitiSynchronize extends Graffiti {
189
222
  const { uri } = unpackLocationOrUri(locationOrUri);
190
223
  return objectUri === uri;
191
224
  }
192
- return this.synchronize(matchObject, [], schema, session);
225
+ return this.synchronize<Schema>(matchObject, [], schema, session);
193
226
  }
194
227
 
195
228
  /**
@@ -205,14 +238,31 @@ export class GraffitiSynchronize extends Graffiti {
205
238
  *
206
239
  * @group Synchronize Methods
207
240
  */
208
- synchronizeRecoverOrphans<Schema extends JSONSchema4>(
241
+ synchronizeRecoverOrphans<Schema extends JSONSchema>(
209
242
  ...args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>
210
243
  ): GraffitiStream<GraffitiObject<Schema>> {
211
244
  const [schema, session] = args;
212
245
  function matchObject(object: GraffitiObjectBase) {
213
246
  return object.actor === session.actor && object.channels.length === 0;
214
247
  }
215
- return this.synchronize(matchObject, [], schema, session);
248
+ return this.synchronize<Schema>(matchObject, [], schema, session);
249
+ }
250
+
251
+ /**
252
+ * Streams changes made to *any* object in *any* channel
253
+ * and made by *any* user. You may want to use it in conjuction with
254
+ * {@link GraffitiSyncrhonizeOptions.omniscient} to get a global view
255
+ * of all Graffiti objects passing through the system. This is useful
256
+ * for building a client-side cache, for example.
257
+ *
258
+ * Be careful using this method. Without additional filters it can
259
+ * expose the user to content out of context.
260
+ */
261
+ synchronizeAll(
262
+ schema?: JSONSchema,
263
+ session?: GraffitiSession | null,
264
+ ): GraffitiStream<GraffitiObjectBase> {
265
+ return this.synchronize(() => true, [], schema ?? {}, session);
216
266
  }
217
267
 
218
268
  protected async synchronizeDispatch(
@@ -255,7 +305,7 @@ export class GraffitiSynchronize extends Graffiti {
255
305
  };
256
306
 
257
307
  put: Graffiti["put"] = async (...args) => {
258
- const oldObject = await this.graffiti.put(...args);
308
+ const oldObject = await this.graffiti.put<{}>(...args);
259
309
  const partialObject = args[0];
260
310
  const newObject: GraffitiObjectBase = {
261
311
  ...oldObject,
@@ -273,7 +323,7 @@ export class GraffitiSynchronize extends Graffiti {
273
323
  const newObject: GraffitiObjectBase = { ...oldObject };
274
324
  newObject.tombstone = false;
275
325
  for (const prop of ["value", "channels", "allowed"] as const) {
276
- applyGraffitiPatch(applyPatch, prop, args[0], newObject);
326
+ applyGraffitiPatch(await this.applyPatch, prop, args[0], newObject);
277
327
  }
278
328
  await this.synchronizeDispatch(oldObject, newObject, true);
279
329
  return oldObject;
@@ -285,7 +335,7 @@ export class GraffitiSynchronize extends Graffiti {
285
335
  return oldObject;
286
336
  };
287
337
 
288
- protected objectStream<Schema extends JSONSchema4>(
338
+ protected objectStream<Schema extends JSONSchema>(
289
339
  iterator: ReturnType<typeof Graffiti.prototype.discover<Schema>>,
290
340
  ) {
291
341
  const dispatch = this.synchronizeDispatch.bind(this);
@@ -305,11 +355,11 @@ export class GraffitiSynchronize extends Graffiti {
305
355
 
306
356
  discover: Graffiti["discover"] = (...args) => {
307
357
  const iterator = this.graffiti.discover(...args);
308
- return this.objectStream(iterator);
358
+ return this.objectStream<(typeof args)[1]>(iterator);
309
359
  };
310
360
 
311
361
  recoverOrphans: Graffiti["recoverOrphans"] = (...args) => {
312
362
  const iterator = this.graffiti.recoverOrphans(...args);
313
- return this.objectStream(iterator);
363
+ return this.objectStream<(typeof args)[0]>(iterator);
314
364
  };
315
365
  }