@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/browser/ajv-LKZCCH33.js +9 -0
- package/dist/browser/ajv-LKZCCH33.js.map +7 -0
- package/dist/browser/chunk-JSDGZ4NY.js +2 -0
- package/dist/browser/chunk-JSDGZ4NY.js.map +7 -0
- package/dist/browser/fast-json-patch-HPEBNS53.js +19 -0
- package/dist/browser/fast-json-patch-HPEBNS53.js.map +7 -0
- package/dist/browser/index.js +20 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +3 -3
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +3 -3
- package/dist/index.d.ts +38 -15
- package/dist/index.d.ts.map +1 -1
- package/package.json +5 -6
- package/src/index.spec.ts +76 -13
- package/src/index.ts +78 -28
- package/dist/index.browser.js +0 -42
- package/dist/index.browser.js.map +0 -7
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import Ajv from "ajv
|
|
1
|
+
import type Ajv from "ajv";
|
|
2
2
|
import { Graffiti } from "@graffiti-garden/api";
|
|
3
|
-
import type { GraffitiSession, GraffitiObject,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
145
|
+
protected objectStream<Schema extends JSONSchema>(iterator: ReturnType<typeof Graffiti.prototype.discover<Schema>>): AsyncGenerator<{
|
|
123
146
|
error: Error;
|
|
124
147
|
source: string;
|
|
125
148
|
} | {
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graffiti-garden/wrapper-synchronize",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
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": "^
|
|
53
|
+
"vitest": "^3.0.5"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@graffiti-garden/api": "^0.
|
|
57
|
-
"@graffiti-garden/implementation-local": "^0.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
158
|
+
(this.options.omniscient ||
|
|
159
|
+
isActorAllowedGraffitiObject(objectRaw, session))
|
|
129
160
|
) {
|
|
130
161
|
const object = { ...objectRaw };
|
|
131
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|