@graffiti-garden/wrapper-synchronize 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/index.browser.js +42 -0
- package/dist/index.browser.js.map +7 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.d.ts.map +1 -0
- package/package.json +63 -0
- package/src/index.spec.ts +408 -0
- package/src/index.ts +315 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import Ajv from "ajv-draft-04";
|
|
2
|
+
import { Graffiti } from "@graffiti-garden/api";
|
|
3
|
+
import type { GraffitiSession, GraffitiObject, JSONSchema4, GraffitiStream } from "@graffiti-garden/api";
|
|
4
|
+
import type { GraffitiObjectBase } from "@graffiti-garden/api";
|
|
5
|
+
export type * from "@graffiti-garden/api";
|
|
6
|
+
export type GraffitiSynchronizeCallback = (oldObject: GraffitiObjectBase, newObject?: GraffitiObjectBase) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Wraps the [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)
|
|
9
|
+
* so that changes made or received in one part of an application
|
|
10
|
+
* are automatically routed to other parts of the application.
|
|
11
|
+
* This is an important tool for building responsive
|
|
12
|
+
* and consistent user interfaces, and is built upon to make
|
|
13
|
+
* the [Graffiti Vue Plugin](https://vue.graffiti.garden/variables/GraffitiPlugin.html)
|
|
14
|
+
* and possibly other front-end libraries in the future.
|
|
15
|
+
*
|
|
16
|
+
* Specifically, it provides the following *synchronize*
|
|
17
|
+
* methods for each of the following API methods:
|
|
18
|
+
*
|
|
19
|
+
* | API Method | Synchronize Method |
|
|
20
|
+
* |------------|--------------------|
|
|
21
|
+
* | {@link get} | {@link synchronizeGet} |
|
|
22
|
+
* | {@link discover} | {@link synchronizeDiscover} |
|
|
23
|
+
* | {@link recoverOrphans} | {@link synchronizeRecoverOrphans} |
|
|
24
|
+
*
|
|
25
|
+
* Whenever a change is made via {@link put}, {@link patch}, and {@link delete} or
|
|
26
|
+
* received from {@link get}, {@link discover}, and {@link recoverOrphans},
|
|
27
|
+
* those changes are forwarded to the appropriate synchronize method.
|
|
28
|
+
* Each synchronize method returns an iterator that streams these changes
|
|
29
|
+
* continually until the user calls `return` on the iterator or `break`s out of the loop,
|
|
30
|
+
* allowing for live updates without additional polling.
|
|
31
|
+
*
|
|
32
|
+
* Example 1: Suppose a user publishes a post using {@link put}. If the feed
|
|
33
|
+
* displaying that user's posts is using {@link synchronizeDiscover} to listen for changes,
|
|
34
|
+
* then the user's new post will instantly appear in their feed, giving the UI a
|
|
35
|
+
* responsive feel.
|
|
36
|
+
*
|
|
37
|
+
* Example 2: Suppose one of a user's friends changes their name. As soon as the
|
|
38
|
+
* user's application receives one notice of that change (using {@link get}
|
|
39
|
+
* or {@link discover}), then {@link synchronizeDiscover} listeners can be used to update
|
|
40
|
+
* all instance's of that friend's name in the user's application instantly,
|
|
41
|
+
* providing a consistent user experience.
|
|
42
|
+
*
|
|
43
|
+
* @groupDescription Synchronize Methods
|
|
44
|
+
* This group contains methods that listen for changes made via
|
|
45
|
+
* {@link put}, {@link patch}, and {@link delete} or fetched from
|
|
46
|
+
* {@link get}, {@link discover}, and {@link recoverOrphans} and then
|
|
47
|
+
* streams appropriate changes to provide a responsive and consistent user experience.
|
|
48
|
+
*/
|
|
49
|
+
export declare class GraffitiSynchronize extends Graffiti {
|
|
50
|
+
protected readonly ajv: Ajv;
|
|
51
|
+
protected readonly graffiti: Graffiti;
|
|
52
|
+
protected readonly callbacks: Set<GraffitiSynchronizeCallback>;
|
|
53
|
+
channelStats: Graffiti["channelStats"];
|
|
54
|
+
locationToUri: Graffiti["locationToUri"];
|
|
55
|
+
uriToLocation: Graffiti["uriToLocation"];
|
|
56
|
+
login: Graffiti["login"];
|
|
57
|
+
logout: Graffiti["logout"];
|
|
58
|
+
sessionEvents: Graffiti["sessionEvents"];
|
|
59
|
+
/**
|
|
60
|
+
* Wraps a Graffiti API instance to provide the synchronize methods.
|
|
61
|
+
* The GraffitiSyncrhonize class rather than the Graffiti class
|
|
62
|
+
* must be used for all functions for the synchronize methods to work.
|
|
63
|
+
*/
|
|
64
|
+
constructor(
|
|
65
|
+
/**
|
|
66
|
+
* The [Graffiti API](https://api.graffiti.garden/classes/Graffiti.html)
|
|
67
|
+
* instance to wrap.
|
|
68
|
+
*/
|
|
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>>;
|
|
77
|
+
/**
|
|
78
|
+
* This method has the same signature as {@link discover} but listens for
|
|
79
|
+
* changes made via {@link put}, {@link patch}, and {@link delete} or
|
|
80
|
+
* fetched from {@link get}, {@link discover}, and {@link recoverOrphans}
|
|
81
|
+
* and then streams appropriate changes to provide a responsive and
|
|
82
|
+
* consistent user experience.
|
|
83
|
+
*
|
|
84
|
+
* Unlike {@link discover}, this method continuously listens for changes
|
|
85
|
+
* and will not terminate unless the user calls the `return` method on the iterator
|
|
86
|
+
* or `break`s out of the loop.
|
|
87
|
+
*
|
|
88
|
+
* @group Synchronize Methods
|
|
89
|
+
*/
|
|
90
|
+
synchronizeDiscover<Schema extends JSONSchema4>(...args: Parameters<typeof Graffiti.prototype.discover<Schema>>): GraffitiStream<GraffitiObject<Schema>>;
|
|
91
|
+
/**
|
|
92
|
+
* This method has the same signature as {@link get} but
|
|
93
|
+
* listens for changes made via {@link put}, {@link patch}, and {@link delete} or
|
|
94
|
+
* fetched from {@link get}, {@link discover}, and {@link recoverOrphans} and then
|
|
95
|
+
* streams appropriate changes to provide a responsive and consistent user experience.
|
|
96
|
+
*
|
|
97
|
+
* Unlike {@link get}, which returns a single result, this method continuously
|
|
98
|
+
* listens for changes which are output as an asynchronous {@link GraffitiStream}.
|
|
99
|
+
*
|
|
100
|
+
* @group Synchronize Methods
|
|
101
|
+
*/
|
|
102
|
+
synchronizeGet<Schema extends JSONSchema4>(...args: Parameters<typeof Graffiti.prototype.get<Schema>>): GraffitiStream<GraffitiObject<Schema>>;
|
|
103
|
+
/**
|
|
104
|
+
* This method has the same signature as {@link recoverOrphans} but
|
|
105
|
+
* listens for changes made via
|
|
106
|
+
* {@link put}, {@link patch}, and {@link delete} or fetched from
|
|
107
|
+
* {@link get}, {@link discover}, and {@link recoverOrphans} and then
|
|
108
|
+
* streams appropriate changes to provide a responsive and consistent user experience.
|
|
109
|
+
*
|
|
110
|
+
* Unlike {@link recoverOrphans}, this method continuously listens for changes
|
|
111
|
+
* and will not terminate unless the user calls the `return` method on the iterator
|
|
112
|
+
* or `break`s out of the loop.
|
|
113
|
+
*
|
|
114
|
+
* @group Synchronize Methods
|
|
115
|
+
*/
|
|
116
|
+
synchronizeRecoverOrphans<Schema extends JSONSchema4>(...args: Parameters<typeof Graffiti.prototype.recoverOrphans<Schema>>): GraffitiStream<GraffitiObject<Schema>>;
|
|
117
|
+
protected synchronizeDispatch(oldObject: GraffitiObjectBase, newObject?: GraffitiObjectBase, waitForListeners?: boolean): Promise<void>;
|
|
118
|
+
get: Graffiti["get"];
|
|
119
|
+
put: Graffiti["put"];
|
|
120
|
+
patch: Graffiti["patch"];
|
|
121
|
+
delete: Graffiti["delete"];
|
|
122
|
+
protected objectStream<Schema extends JSONSchema4>(iterator: ReturnType<typeof Graffiti.prototype.discover<Schema>>): AsyncGenerator<{
|
|
123
|
+
error: Error;
|
|
124
|
+
source: string;
|
|
125
|
+
} | {
|
|
126
|
+
error?: undefined;
|
|
127
|
+
value: GraffitiObject<Schema>;
|
|
128
|
+
}, {
|
|
129
|
+
tombstoneRetention: number;
|
|
130
|
+
}, unknown>;
|
|
131
|
+
discover: Graffiti["discover"];
|
|
132
|
+
recoverOrphans: Graffiti["recoverOrphans"];
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graffiti-garden/wrapper-synchronize",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "TBD",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"module": "./dist/esm/index.js",
|
|
7
|
+
"main": "./dist/cjs/index.js",
|
|
8
|
+
"browser": "./dist/index.browser.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/esm/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/cjs/index.js"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test:coverage": "vitest --coverage",
|
|
25
|
+
"build:types": "tsc --declaration --emitDeclarationOnly",
|
|
26
|
+
"build:js": "tsx esbuild.config.mts",
|
|
27
|
+
"build:docs": "typedoc --options typedoc.json",
|
|
28
|
+
"build": "rm -rf dist && npm run build:types && npm run build:js && npm run build:docs",
|
|
29
|
+
"prepublishOnly": "npm update && npm test && npm run build"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src",
|
|
33
|
+
"dist",
|
|
34
|
+
"tests",
|
|
35
|
+
"package.json",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"author": "Theia Henderson",
|
|
39
|
+
"license": "GPL-3.0-or-later",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/graffiti-garden/wrapper-synchronize.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/graffiti-garden/wrapper-synchronize/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://sync.graffiti.garden/classes/GraffitiSynchronize.html",
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.13.1",
|
|
50
|
+
"tsx": "^4.19.2",
|
|
51
|
+
"typedoc": "^0.27.6",
|
|
52
|
+
"typescript": "^5.7.3",
|
|
53
|
+
"vitest": "^2.1.9"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@graffiti-garden/api": "^0.3.0",
|
|
57
|
+
"@graffiti-garden/implementation-local": "^0.2.12",
|
|
58
|
+
"@repeaterjs/repeater": "^3.0.6",
|
|
59
|
+
"ajv": "^8.17.1",
|
|
60
|
+
"ajv-draft-04": "^1.0.0",
|
|
61
|
+
"fast-json-patch": "^3.1.1"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { it, expect, describe, assert, beforeAll } from "vitest";
|
|
2
|
+
import type { GraffitiSession } from "@graffiti-garden/api";
|
|
3
|
+
import { GraffitiLocal } from "@graffiti-garden/implementation-local";
|
|
4
|
+
import { randomPutObject, randomString } from "@graffiti-garden/api/tests";
|
|
5
|
+
|
|
6
|
+
const useGraffiti = () => new GraffitiLocal();
|
|
7
|
+
const graffiti = useGraffiti();
|
|
8
|
+
|
|
9
|
+
const useSession1 = async () => {
|
|
10
|
+
return {
|
|
11
|
+
actor: randomString(),
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
const useSession2 = async () => {
|
|
15
|
+
return {
|
|
16
|
+
actor: randomString(),
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe.concurrent("synchronizeDiscover", () => {
|
|
21
|
+
let session: GraffitiSession;
|
|
22
|
+
let session1: GraffitiSession;
|
|
23
|
+
let session2: GraffitiSession;
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
session1 = await useSession1();
|
|
26
|
+
session = session1;
|
|
27
|
+
session2 = await useSession2();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("get", async () => {
|
|
31
|
+
const graffiti1 = useGraffiti();
|
|
32
|
+
|
|
33
|
+
const object = randomPutObject();
|
|
34
|
+
const channels = object.channels.slice(1);
|
|
35
|
+
const putted = await graffiti1.put(object, session);
|
|
36
|
+
|
|
37
|
+
const graffiti2 = useGraffiti();
|
|
38
|
+
const next = graffiti2.synchronizeDiscover(channels, {}).next();
|
|
39
|
+
const gotten = await graffiti2.get(putted, {}, session);
|
|
40
|
+
|
|
41
|
+
const result = (await next).value;
|
|
42
|
+
if (!result || result.error) {
|
|
43
|
+
throw new Error("Error in synchronize");
|
|
44
|
+
}
|
|
45
|
+
expect(result.value.value).toEqual(object.value);
|
|
46
|
+
expect(result.value.channels).toEqual(channels);
|
|
47
|
+
expect(result.value.tombstone).toBe(false);
|
|
48
|
+
expect(result.value.lastModified).toEqual(gotten.lastModified);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("put", async () => {
|
|
52
|
+
const beforeChannel = randomString();
|
|
53
|
+
const afterChannel = randomString();
|
|
54
|
+
const sharedChannel = randomString();
|
|
55
|
+
|
|
56
|
+
const oldValue = { hello: "world" };
|
|
57
|
+
const oldChannels = [beforeChannel, sharedChannel];
|
|
58
|
+
const putted = await graffiti.put(
|
|
59
|
+
{
|
|
60
|
+
value: oldValue,
|
|
61
|
+
channels: oldChannels,
|
|
62
|
+
},
|
|
63
|
+
session,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Start listening for changes...
|
|
67
|
+
const before = graffiti.synchronizeDiscover([beforeChannel], {}).next();
|
|
68
|
+
const after = graffiti.synchronizeDiscover([afterChannel], {}).next();
|
|
69
|
+
const shared = graffiti.synchronizeDiscover([sharedChannel], {}).next();
|
|
70
|
+
|
|
71
|
+
// Replace the object
|
|
72
|
+
const newValue = { goodbye: "world" };
|
|
73
|
+
const newChannels = [afterChannel, sharedChannel];
|
|
74
|
+
await graffiti.put(
|
|
75
|
+
{
|
|
76
|
+
...putted,
|
|
77
|
+
value: newValue,
|
|
78
|
+
channels: newChannels,
|
|
79
|
+
},
|
|
80
|
+
session,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const beforeResult = (await before).value;
|
|
84
|
+
const afterResult = (await after).value;
|
|
85
|
+
const sharedResult = (await shared).value;
|
|
86
|
+
if (
|
|
87
|
+
!beforeResult ||
|
|
88
|
+
beforeResult.error ||
|
|
89
|
+
!afterResult ||
|
|
90
|
+
afterResult.error ||
|
|
91
|
+
!sharedResult ||
|
|
92
|
+
sharedResult.error
|
|
93
|
+
) {
|
|
94
|
+
throw new Error("Error in synchronize");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
expect(beforeResult.value.value).toEqual(oldValue);
|
|
98
|
+
expect(beforeResult.value.channels).toEqual([beforeChannel]);
|
|
99
|
+
expect(beforeResult.value.tombstone).toBe(true);
|
|
100
|
+
expect(afterResult.value.value).toEqual(newValue);
|
|
101
|
+
expect(afterResult.value.channels).toEqual([afterChannel]);
|
|
102
|
+
expect(afterResult.value.tombstone).toBe(false);
|
|
103
|
+
expect(sharedResult.value.value).toEqual(newValue);
|
|
104
|
+
expect(sharedResult.value.channels).toEqual([sharedChannel]);
|
|
105
|
+
expect(sharedResult.value.tombstone).toBe(false);
|
|
106
|
+
expect(beforeResult.value.lastModified).toEqual(
|
|
107
|
+
afterResult.value.lastModified,
|
|
108
|
+
);
|
|
109
|
+
expect(sharedResult.value.lastModified).toEqual(
|
|
110
|
+
afterResult.value.lastModified,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("patch", async () => {
|
|
115
|
+
const beforeChannel = randomString();
|
|
116
|
+
const afterChannel = randomString();
|
|
117
|
+
const sharedChannel = randomString();
|
|
118
|
+
|
|
119
|
+
const oldValue = { hello: "world" };
|
|
120
|
+
const oldChannels = [beforeChannel, sharedChannel];
|
|
121
|
+
const putted = await graffiti.put(
|
|
122
|
+
{
|
|
123
|
+
value: oldValue,
|
|
124
|
+
channels: oldChannels,
|
|
125
|
+
},
|
|
126
|
+
session,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Start listening for changes...
|
|
130
|
+
const before = graffiti.synchronizeDiscover([beforeChannel], {}).next();
|
|
131
|
+
const after = graffiti.synchronizeDiscover([afterChannel], {}).next();
|
|
132
|
+
const shared = graffiti.synchronizeDiscover([sharedChannel], {}).next();
|
|
133
|
+
|
|
134
|
+
await graffiti.patch(
|
|
135
|
+
{
|
|
136
|
+
value: [
|
|
137
|
+
{
|
|
138
|
+
op: "add",
|
|
139
|
+
path: "/something",
|
|
140
|
+
value: "new value",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
channels: [
|
|
144
|
+
{
|
|
145
|
+
op: "add",
|
|
146
|
+
path: "/-",
|
|
147
|
+
value: afterChannel,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
op: "remove",
|
|
151
|
+
path: `/${oldChannels.indexOf(beforeChannel)}`,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
putted,
|
|
156
|
+
session,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const beforeResult = (await before).value;
|
|
160
|
+
const afterResult = (await after).value;
|
|
161
|
+
const sharedResult = (await shared).value;
|
|
162
|
+
if (
|
|
163
|
+
!beforeResult ||
|
|
164
|
+
beforeResult.error ||
|
|
165
|
+
!afterResult ||
|
|
166
|
+
afterResult.error ||
|
|
167
|
+
!sharedResult ||
|
|
168
|
+
sharedResult.error
|
|
169
|
+
) {
|
|
170
|
+
throw new Error("Error in synchronize");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const newValue = { ...oldValue, something: "new value" };
|
|
174
|
+
const newChannels = [sharedChannel, afterChannel];
|
|
175
|
+
expect(beforeResult.value.value).toEqual(oldValue);
|
|
176
|
+
expect(beforeResult.value.channels).toEqual([beforeChannel]);
|
|
177
|
+
expect(beforeResult.value.tombstone).toBe(true);
|
|
178
|
+
expect(afterResult.value.value).toEqual(newValue);
|
|
179
|
+
expect(afterResult.value.channels).toEqual([afterChannel]);
|
|
180
|
+
expect(afterResult.value.tombstone).toBe(false);
|
|
181
|
+
expect(sharedResult.value.value).toEqual(newValue);
|
|
182
|
+
expect(sharedResult.value.channels).toEqual([sharedChannel]);
|
|
183
|
+
expect(sharedResult.value.tombstone).toBe(false);
|
|
184
|
+
expect(beforeResult.value.lastModified).toEqual(
|
|
185
|
+
afterResult.value.lastModified,
|
|
186
|
+
);
|
|
187
|
+
expect(sharedResult.value.lastModified).toEqual(
|
|
188
|
+
afterResult.value.lastModified,
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("delete", async () => {
|
|
193
|
+
const channels = [randomString(), randomString(), randomString()];
|
|
194
|
+
|
|
195
|
+
const oldValue = { hello: "world" };
|
|
196
|
+
const oldChannels = [randomString(), ...channels.slice(1)];
|
|
197
|
+
const putted = await graffiti.put(
|
|
198
|
+
{
|
|
199
|
+
value: oldValue,
|
|
200
|
+
channels: oldChannels,
|
|
201
|
+
},
|
|
202
|
+
session,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const next = graffiti.synchronizeDiscover(channels, {}).next();
|
|
206
|
+
|
|
207
|
+
graffiti.delete(putted, session);
|
|
208
|
+
|
|
209
|
+
const result = (await next).value;
|
|
210
|
+
if (!result || result.error) {
|
|
211
|
+
throw new Error("Error in synchronize");
|
|
212
|
+
}
|
|
213
|
+
expect(result.value.tombstone).toBe(true);
|
|
214
|
+
expect(result.value.value).toEqual(oldValue);
|
|
215
|
+
expect(result.value.channels).toEqual(
|
|
216
|
+
channels.filter((c) => oldChannels.includes(c)),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("synchronize happens before putters", async () => {
|
|
221
|
+
const object = randomPutObject();
|
|
222
|
+
const iterator = graffiti.synchronizeDiscover(object.channels, {});
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < 10; i++) {
|
|
225
|
+
const next = iterator.next();
|
|
226
|
+
const putted = graffiti.put(object, session);
|
|
227
|
+
|
|
228
|
+
let first: undefined | string = undefined;
|
|
229
|
+
next.then(() => {
|
|
230
|
+
if (!first) first = "synchronize";
|
|
231
|
+
});
|
|
232
|
+
putted.then(() => {
|
|
233
|
+
if (!first) first = "put";
|
|
234
|
+
});
|
|
235
|
+
await putted;
|
|
236
|
+
|
|
237
|
+
expect(first).toBe("synchronize");
|
|
238
|
+
|
|
239
|
+
const patched = graffiti.patch({}, await putted, session);
|
|
240
|
+
const next2 = iterator.next();
|
|
241
|
+
|
|
242
|
+
let second: undefined | string = undefined;
|
|
243
|
+
next2.then(() => {
|
|
244
|
+
if (!second) second = "synchronize";
|
|
245
|
+
});
|
|
246
|
+
patched.then(() => {
|
|
247
|
+
if (!second) second = "patch";
|
|
248
|
+
});
|
|
249
|
+
await patched;
|
|
250
|
+
|
|
251
|
+
expect(second).toBe("synchronize");
|
|
252
|
+
|
|
253
|
+
const deleted = graffiti.delete(await putted, session);
|
|
254
|
+
const next3 = iterator.next();
|
|
255
|
+
|
|
256
|
+
let third: undefined | string = undefined;
|
|
257
|
+
next3.then(() => {
|
|
258
|
+
if (!third) third = "synchronize";
|
|
259
|
+
});
|
|
260
|
+
deleted.then(() => {
|
|
261
|
+
if (!third) third = "delete";
|
|
262
|
+
});
|
|
263
|
+
await deleted;
|
|
264
|
+
|
|
265
|
+
expect(third).toBe("synchronize");
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("not allowed", async () => {
|
|
270
|
+
const allChannels = [randomString(), randomString(), randomString()];
|
|
271
|
+
const channels = allChannels.slice(1);
|
|
272
|
+
|
|
273
|
+
const creatorNext = graffiti
|
|
274
|
+
.synchronizeDiscover(channels, {}, session1)
|
|
275
|
+
.next();
|
|
276
|
+
const allowedNext = graffiti
|
|
277
|
+
.synchronizeDiscover(channels, {}, session2)
|
|
278
|
+
.next();
|
|
279
|
+
const noSession = graffiti.synchronizeDiscover(channels, {}).next();
|
|
280
|
+
|
|
281
|
+
const value = {
|
|
282
|
+
hello: "world",
|
|
283
|
+
};
|
|
284
|
+
const allowed = [randomString(), session2.actor];
|
|
285
|
+
await graffiti.put({ value, channels: allChannels, allowed }, session1);
|
|
286
|
+
|
|
287
|
+
// Expect no session to time out!
|
|
288
|
+
await expect(
|
|
289
|
+
Promise.race([
|
|
290
|
+
noSession,
|
|
291
|
+
new Promise((resolve, rejects) => setTimeout(rejects, 100, "Timeout")),
|
|
292
|
+
]),
|
|
293
|
+
).rejects.toThrow("Timeout");
|
|
294
|
+
|
|
295
|
+
const creatorResult = (await creatorNext).value;
|
|
296
|
+
const allowedResult = (await allowedNext).value;
|
|
297
|
+
|
|
298
|
+
if (
|
|
299
|
+
!creatorResult ||
|
|
300
|
+
creatorResult.error ||
|
|
301
|
+
!allowedResult ||
|
|
302
|
+
allowedResult.error
|
|
303
|
+
) {
|
|
304
|
+
throw new Error("Error in synchronize");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
expect(creatorResult.value.value).toEqual(value);
|
|
308
|
+
expect(creatorResult.value.allowed).toEqual(allowed);
|
|
309
|
+
expect(creatorResult.value.channels).toEqual(allChannels);
|
|
310
|
+
expect(allowedResult.value.value).toEqual(value);
|
|
311
|
+
expect(allowedResult.value.allowed).toEqual([session2.actor]);
|
|
312
|
+
expect(allowedResult.value.channels).toEqual(channels);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe.concurrent("synchronizeGet", () => {
|
|
317
|
+
let graffiti: ReturnType<typeof useGraffiti>;
|
|
318
|
+
let session: GraffitiSession;
|
|
319
|
+
let session1: GraffitiSession;
|
|
320
|
+
let session2: GraffitiSession;
|
|
321
|
+
beforeAll(async () => {
|
|
322
|
+
graffiti = useGraffiti();
|
|
323
|
+
session1 = await useSession1();
|
|
324
|
+
session = session1;
|
|
325
|
+
session2 = await useSession2();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("replace, delete", async () => {
|
|
329
|
+
const object = randomPutObject();
|
|
330
|
+
const putted = await graffiti.put(object, session);
|
|
331
|
+
|
|
332
|
+
const iterator = graffiti.synchronizeGet(putted, {});
|
|
333
|
+
const next = iterator.next();
|
|
334
|
+
|
|
335
|
+
// Change the object
|
|
336
|
+
const newValue = { goodbye: "world" };
|
|
337
|
+
const putted2 = await graffiti.put(
|
|
338
|
+
{
|
|
339
|
+
...putted,
|
|
340
|
+
value: newValue,
|
|
341
|
+
},
|
|
342
|
+
session,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const result = (await next).value;
|
|
346
|
+
assert(result && !result.error);
|
|
347
|
+
|
|
348
|
+
expect(result.value.value).toEqual(newValue);
|
|
349
|
+
expect(result.value.actor).toEqual(session.actor);
|
|
350
|
+
expect(result.value.channels).toEqual([]);
|
|
351
|
+
expect(result.value.tombstone).toBe(false);
|
|
352
|
+
expect(result.value.lastModified).toEqual(putted2.lastModified);
|
|
353
|
+
expect(result.value.allowed).toBeUndefined();
|
|
354
|
+
|
|
355
|
+
// Delete the object
|
|
356
|
+
const deleted = await graffiti.delete(putted2, session);
|
|
357
|
+
const result2 = (await iterator.next()).value;
|
|
358
|
+
assert(result2 && !result2.error);
|
|
359
|
+
expect(result2.value.tombstone).toBe(true);
|
|
360
|
+
expect(result2.value.lastModified).toEqual(deleted.lastModified);
|
|
361
|
+
|
|
362
|
+
// Put something else
|
|
363
|
+
await graffiti.put(randomPutObject(), session);
|
|
364
|
+
await expect(
|
|
365
|
+
Promise.race([
|
|
366
|
+
iterator.next(),
|
|
367
|
+
new Promise((resolve, reject) => setTimeout(reject, 100, "Timeout")),
|
|
368
|
+
]),
|
|
369
|
+
).rejects.toThrow("Timeout");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("not allowed", async () => {
|
|
373
|
+
const object = randomPutObject();
|
|
374
|
+
const putted = await graffiti.put(object, session1);
|
|
375
|
+
|
|
376
|
+
const iterator1 = graffiti.synchronizeGet(putted, {}, session1);
|
|
377
|
+
const iterator2 = graffiti.synchronizeGet(putted, {}, session2);
|
|
378
|
+
|
|
379
|
+
const next1 = iterator1.next();
|
|
380
|
+
const next2 = iterator2.next();
|
|
381
|
+
|
|
382
|
+
const newValue = { goodbye: "world" };
|
|
383
|
+
const putted2 = await graffiti.put(
|
|
384
|
+
{
|
|
385
|
+
...putted,
|
|
386
|
+
...object,
|
|
387
|
+
allowed: [],
|
|
388
|
+
value: newValue,
|
|
389
|
+
},
|
|
390
|
+
session1,
|
|
391
|
+
);
|
|
392
|
+
const result1 = (await next1).value;
|
|
393
|
+
const result2 = (await next2).value;
|
|
394
|
+
assert(result1 && !result1.error);
|
|
395
|
+
assert(result2 && !result2.error);
|
|
396
|
+
|
|
397
|
+
expect(result1.value.value).toEqual(newValue);
|
|
398
|
+
expect(result2.value.value).toEqual(object.value);
|
|
399
|
+
expect(result1.value.actor).toEqual(session1.actor);
|
|
400
|
+
expect(result2.value.actor).toEqual(session1.actor);
|
|
401
|
+
expect(result1.value.channels).toEqual(object.channels);
|
|
402
|
+
expect(result2.value.channels).toEqual([]);
|
|
403
|
+
expect(result1.value.tombstone).toBe(false);
|
|
404
|
+
expect(result2.value.tombstone).toBe(true);
|
|
405
|
+
expect(result1.value.lastModified).toEqual(putted2.lastModified);
|
|
406
|
+
expect(result2.value.lastModified).toEqual(putted2.lastModified);
|
|
407
|
+
});
|
|
408
|
+
});
|