@powerhousedao/reactor-api 1.20.2 → 1.21.0
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/CHANGELOG.md +0 -20
- package/dist/index.d.ts +63 -76
- package/dist/index.js +87 -64
- package/dist/index.js.map +1 -1
- package/package.json +9 -6
- package/src/subgraphs/drive/index.ts +55 -74
- package/src/sync/utils.ts +85 -0
- package/test/benchmarks/load.bench.ts +78 -0
- package/test/benchmarks/sync.bench.ts +151 -0
- package/test/data/BlocktowerAndromeda.zip +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powerhousedao/reactor-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"license": "AGPL-3.0-only",
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@powerhousedao/analytics-engine-graphql": "^0.2.2",
|
|
17
|
+
"@sky-ph/atlas": "^1.0.14",
|
|
17
18
|
"@types/body-parser": "^1.19.5",
|
|
18
19
|
"@types/cors": "^2.8.17",
|
|
19
20
|
"@types/express": "^5.0.0",
|
|
@@ -23,9 +24,10 @@
|
|
|
23
24
|
"@types/pg": "^8.11.10",
|
|
24
25
|
"esbuild": "^0.24.0",
|
|
25
26
|
"graphql-tag": "^2.12.6",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"document-
|
|
27
|
+
"tinybench": "^3.1.1",
|
|
28
|
+
"@powerhousedao/scalars": "1.23.0",
|
|
29
|
+
"document-drive": "1.18.0",
|
|
30
|
+
"document-model": "2.20.0"
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
33
|
"@apollo/server": "^4.11.0",
|
|
@@ -52,11 +54,12 @@
|
|
|
52
54
|
"uuid": "^9.0.1",
|
|
53
55
|
"wildcard-match": "^5.1.3",
|
|
54
56
|
"zod": "^3.24.1",
|
|
55
|
-
"document-model-libs": "1.
|
|
57
|
+
"document-model-libs": "1.132.0"
|
|
56
58
|
},
|
|
57
59
|
"scripts": {
|
|
58
60
|
"build": "tsup",
|
|
59
61
|
"test": "vitest run",
|
|
60
|
-
"lint": "eslint"
|
|
62
|
+
"lint": "eslint",
|
|
63
|
+
"bench": "vitest bench"
|
|
61
64
|
}
|
|
62
65
|
}
|
|
@@ -3,23 +3,27 @@ import { pascalCase } from "change-case";
|
|
|
3
3
|
import {
|
|
4
4
|
generateUUID,
|
|
5
5
|
ListenerRevision,
|
|
6
|
-
PullResponderTransmitter,
|
|
7
6
|
StrandUpdateGraphQL,
|
|
8
7
|
} from "document-drive";
|
|
9
8
|
import {
|
|
10
9
|
actions,
|
|
11
|
-
DocumentDriveAction,
|
|
12
10
|
FileNode,
|
|
13
11
|
Listener,
|
|
14
12
|
ListenerFilter,
|
|
15
13
|
TransmitterType,
|
|
16
14
|
} from "document-model-libs/document-drive";
|
|
17
|
-
import {
|
|
15
|
+
import { Document, Operation } from "document-model/document";
|
|
18
16
|
import {
|
|
19
17
|
DocumentModelInput,
|
|
20
18
|
DocumentModelState,
|
|
21
19
|
} from "document-model/document-model";
|
|
22
20
|
import { gql } from "graphql-tag";
|
|
21
|
+
import {
|
|
22
|
+
InternalStrandUpdate,
|
|
23
|
+
processAcknowledge,
|
|
24
|
+
processGetStrands,
|
|
25
|
+
processPushUpdate,
|
|
26
|
+
} from "src/sync/utils";
|
|
23
27
|
import { Subgraph } from "../base";
|
|
24
28
|
import { Context } from "../types";
|
|
25
29
|
import { Asset } from "./temp-hack-rwa-type-defs";
|
|
@@ -276,58 +280,32 @@ export class DriveSubgraph extends Subgraph {
|
|
|
276
280
|
},
|
|
277
281
|
pushUpdates: async (
|
|
278
282
|
_: unknown,
|
|
279
|
-
{ strands }: { strands: StrandUpdateGraphQL[] },
|
|
283
|
+
{ strands: strandsGql }: { strands: StrandUpdateGraphQL[] },
|
|
280
284
|
ctx: Context,
|
|
281
285
|
) => {
|
|
282
286
|
if (!ctx.driveId) throw new Error("Drive ID is required");
|
|
283
|
-
const listenerRevisions: ListenerRevision[] = await Promise.all(
|
|
284
|
-
strands.map(async (s) => {
|
|
285
|
-
const operations =
|
|
286
|
-
s.operations.map((o) => ({
|
|
287
|
-
...o,
|
|
288
|
-
input: JSON.parse(o.input) as DocumentModelInput,
|
|
289
|
-
skip: o.skip ?? 0,
|
|
290
|
-
scope: s.scope,
|
|
291
|
-
branch: "main",
|
|
292
|
-
})) ?? [];
|
|
293
|
-
|
|
294
|
-
const result = await (s.documentId !== undefined
|
|
295
|
-
? this.reactor.queueOperations(
|
|
296
|
-
s.driveId,
|
|
297
|
-
s.documentId,
|
|
298
|
-
operations,
|
|
299
|
-
)
|
|
300
|
-
: this.reactor.queueDriveOperations(
|
|
301
|
-
s.driveId,
|
|
302
|
-
operations as Operation<DocumentDriveAction | BaseAction>[],
|
|
303
|
-
));
|
|
304
287
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
288
|
+
// translate data types
|
|
289
|
+
const strands: InternalStrandUpdate[] = strandsGql.map((strandGql) => {
|
|
290
|
+
return {
|
|
291
|
+
operations: strandGql.operations.map((op) => ({
|
|
292
|
+
...op,
|
|
293
|
+
input: JSON.parse(op.input) as DocumentModelInput,
|
|
294
|
+
skip: op.skip ?? 0,
|
|
295
|
+
scope: strandGql.scope,
|
|
296
|
+
branch: "main",
|
|
297
|
+
})) as Operation[],
|
|
298
|
+
documentId: strandGql.documentId,
|
|
299
|
+
driveId: strandGql.driveId,
|
|
300
|
+
scope: strandGql.scope,
|
|
301
|
+
branch: strandGql.branch,
|
|
302
|
+
};
|
|
303
|
+
});
|
|
316
304
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
branch: s.branch,
|
|
321
|
-
documentId: s.documentId ?? "",
|
|
322
|
-
driveId: s.driveId,
|
|
323
|
-
scope: s.scope,
|
|
324
|
-
status: result.status,
|
|
325
|
-
error: result.error?.message || undefined,
|
|
326
|
-
};
|
|
327
|
-
}),
|
|
305
|
+
// return a list of listener revisions
|
|
306
|
+
return await Promise.all(
|
|
307
|
+
strands.map((strand) => processPushUpdate(this.reactor, strand)),
|
|
328
308
|
);
|
|
329
|
-
|
|
330
|
-
return listenerRevisions;
|
|
331
309
|
},
|
|
332
310
|
acknowledge: async (
|
|
333
311
|
_: unknown,
|
|
@@ -339,6 +317,8 @@ export class DriveSubgraph extends Subgraph {
|
|
|
339
317
|
) => {
|
|
340
318
|
if (!listenerId || !revisions) return false;
|
|
341
319
|
if (!ctx.driveId) throw new Error("Drive ID is required");
|
|
320
|
+
|
|
321
|
+
// translate data types
|
|
342
322
|
const validEntries = revisions
|
|
343
323
|
.filter((r) => r !== null)
|
|
344
324
|
.map((e) => ({
|
|
@@ -350,17 +330,13 @@ export class DriveSubgraph extends Subgraph {
|
|
|
350
330
|
status: e.status,
|
|
351
331
|
}));
|
|
352
332
|
|
|
353
|
-
|
|
333
|
+
// return a boolean indicating if the acknowledge was successful
|
|
334
|
+
return await processAcknowledge(
|
|
335
|
+
this.reactor,
|
|
354
336
|
ctx.driveId,
|
|
355
337
|
listenerId,
|
|
356
|
-
)) as PullResponderTransmitter;
|
|
357
|
-
const result = await transmitter.processAcknowledge(
|
|
358
|
-
ctx.driveId ?? "1",
|
|
359
|
-
listenerId,
|
|
360
338
|
validEntries,
|
|
361
339
|
);
|
|
362
|
-
|
|
363
|
-
return result;
|
|
364
340
|
},
|
|
365
341
|
},
|
|
366
342
|
System: {},
|
|
@@ -374,26 +350,31 @@ export class DriveSubgraph extends Subgraph {
|
|
|
374
350
|
ctx: Context,
|
|
375
351
|
) => {
|
|
376
352
|
if (!ctx.driveId) throw new Error("Drive ID is required");
|
|
377
|
-
|
|
353
|
+
|
|
354
|
+
// get the requested strand updates
|
|
355
|
+
const strands = await processGetStrands(
|
|
356
|
+
this.reactor,
|
|
378
357
|
ctx.driveId,
|
|
379
358
|
listenerId,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
359
|
+
since,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
// translate data types
|
|
363
|
+
return strands.map((update) => ({
|
|
364
|
+
driveId: update.driveId,
|
|
365
|
+
documentId: update.documentId,
|
|
366
|
+
scope: update.scope,
|
|
367
|
+
branch: update.branch,
|
|
368
|
+
operations: update.operations.map((op) => ({
|
|
369
|
+
index: op.index,
|
|
370
|
+
skip: op.skip,
|
|
371
|
+
name: op.type,
|
|
372
|
+
input: JSON.stringify(op.input),
|
|
373
|
+
hash: op.hash,
|
|
374
|
+
timestamp: op.timestamp,
|
|
375
|
+
type: op.type,
|
|
376
|
+
context: op.context,
|
|
377
|
+
id: op.id,
|
|
397
378
|
})),
|
|
398
379
|
}));
|
|
399
380
|
},
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IDocumentDriveServer,
|
|
3
|
+
ListenerRevision,
|
|
4
|
+
PullResponderTransmitter,
|
|
5
|
+
StrandUpdate,
|
|
6
|
+
} from "document-drive";
|
|
7
|
+
import { DocumentDriveAction } from "document-model-libs/document-drive";
|
|
8
|
+
import { BaseAction, Operation, OperationScope } from "document-model/document";
|
|
9
|
+
|
|
10
|
+
// define types
|
|
11
|
+
export type InternalStrandUpdate = {
|
|
12
|
+
operations: Operation[];
|
|
13
|
+
documentId: string;
|
|
14
|
+
driveId: string;
|
|
15
|
+
scope: OperationScope;
|
|
16
|
+
branch: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// processes a strand update and returns a listener revision
|
|
20
|
+
export const processPushUpdate = async (
|
|
21
|
+
reactor: IDocumentDriveServer,
|
|
22
|
+
strand: InternalStrandUpdate
|
|
23
|
+
): Promise<ListenerRevision> => {
|
|
24
|
+
const result = await (strand.documentId !== undefined
|
|
25
|
+
? reactor.queueOperations(
|
|
26
|
+
strand.driveId,
|
|
27
|
+
strand.documentId,
|
|
28
|
+
strand.operations
|
|
29
|
+
)
|
|
30
|
+
: reactor.queueDriveOperations(
|
|
31
|
+
strand.driveId,
|
|
32
|
+
strand.operations as Operation<DocumentDriveAction | BaseAction>[]
|
|
33
|
+
));
|
|
34
|
+
|
|
35
|
+
const scopeOperations = result.document?.operations[strand.scope] ?? [];
|
|
36
|
+
if (scopeOperations.length === 0) {
|
|
37
|
+
return {
|
|
38
|
+
revision: -1,
|
|
39
|
+
branch: strand.branch,
|
|
40
|
+
documentId: strand.documentId ?? "",
|
|
41
|
+
driveId: strand.driveId,
|
|
42
|
+
scope: strand.scope,
|
|
43
|
+
status: result.status,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const revision = scopeOperations.slice().pop()?.index ?? -1;
|
|
48
|
+
return {
|
|
49
|
+
revision,
|
|
50
|
+
branch: strand.branch,
|
|
51
|
+
documentId: strand.documentId ?? "",
|
|
52
|
+
driveId: strand.driveId,
|
|
53
|
+
scope: strand.scope,
|
|
54
|
+
status: result.status,
|
|
55
|
+
error: result.error?.message || undefined,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// processes an acknowledge request and returns a boolean
|
|
60
|
+
export const processAcknowledge = async (
|
|
61
|
+
reactor: IDocumentDriveServer,
|
|
62
|
+
driveId: string,
|
|
63
|
+
listenerId: string,
|
|
64
|
+
revisions: ListenerRevision[]
|
|
65
|
+
): Promise<boolean> => {
|
|
66
|
+
const transmitter = (await reactor.getTransmitter(
|
|
67
|
+
driveId,
|
|
68
|
+
listenerId
|
|
69
|
+
)) as PullResponderTransmitter;
|
|
70
|
+
return transmitter.processAcknowledge(driveId, listenerId, revisions);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// processes a get strands request and returns a list of strand updates
|
|
74
|
+
export const processGetStrands = async (
|
|
75
|
+
reactor: IDocumentDriveServer,
|
|
76
|
+
driveId: string,
|
|
77
|
+
listenerId: string,
|
|
78
|
+
since: string | undefined
|
|
79
|
+
): Promise<StrandUpdate[]> => {
|
|
80
|
+
const transmitter = (await reactor.getTransmitter(
|
|
81
|
+
driveId,
|
|
82
|
+
listenerId
|
|
83
|
+
)) as PullResponderTransmitter;
|
|
84
|
+
return transmitter.getStrands({ since });
|
|
85
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { RealWorldAssets } from "@sky-ph/atlas/document-models";
|
|
2
|
+
import { DocumentDriveServer, generateUUID } from "document-drive";
|
|
3
|
+
import { MemoryStorage } from "document-drive/storage/memory";
|
|
4
|
+
import {
|
|
5
|
+
module as DocumentDrive,
|
|
6
|
+
generateAddNodeAction,
|
|
7
|
+
} from "document-model-libs/document-drive";
|
|
8
|
+
import { DocumentModel } from "document-model/document";
|
|
9
|
+
|
|
10
|
+
import { bench, describe } from "vitest";
|
|
11
|
+
|
|
12
|
+
describe("Document Drive", async () => {
|
|
13
|
+
const documentModels = [DocumentDrive, RealWorldAssets] as DocumentModel[];
|
|
14
|
+
const document = await RealWorldAssets.utils.loadFromFile(
|
|
15
|
+
"test/data/BlocktowerAndromeda.zip",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
bench(
|
|
19
|
+
"Load PHDM into Document Drive",
|
|
20
|
+
async () => {
|
|
21
|
+
const server = new DocumentDriveServer(
|
|
22
|
+
documentModels,
|
|
23
|
+
new MemoryStorage(),
|
|
24
|
+
);
|
|
25
|
+
await server.initialize();
|
|
26
|
+
|
|
27
|
+
const driveId = generateUUID();
|
|
28
|
+
const documentId = generateUUID();
|
|
29
|
+
|
|
30
|
+
const drive = await server.addDrive({
|
|
31
|
+
global: {
|
|
32
|
+
id: driveId,
|
|
33
|
+
name: "Test Drive",
|
|
34
|
+
icon: null,
|
|
35
|
+
slug: null,
|
|
36
|
+
},
|
|
37
|
+
local: {
|
|
38
|
+
availableOffline: false,
|
|
39
|
+
sharingType: "PRIVATE",
|
|
40
|
+
listeners: [],
|
|
41
|
+
triggers: [],
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// adds file node for document
|
|
46
|
+
const addFileAction = generateAddNodeAction(
|
|
47
|
+
drive.state.global,
|
|
48
|
+
{
|
|
49
|
+
documentType: document.documentType,
|
|
50
|
+
id: documentId,
|
|
51
|
+
name: "BlocktowerAndromeda",
|
|
52
|
+
},
|
|
53
|
+
["global"],
|
|
54
|
+
);
|
|
55
|
+
await server.addDriveAction(driveId, addFileAction);
|
|
56
|
+
|
|
57
|
+
// adds document operations
|
|
58
|
+
const result = await server.addOperations(
|
|
59
|
+
driveId,
|
|
60
|
+
documentId,
|
|
61
|
+
document.operations.global,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (result.error) {
|
|
65
|
+
throw result.error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const lastOperation = document.operations.global.at(-1);
|
|
69
|
+
const lastLoadedOperation = result.operations.at(-1);
|
|
70
|
+
if (
|
|
71
|
+
JSON.stringify(lastOperation) !== JSON.stringify(lastLoadedOperation)
|
|
72
|
+
) {
|
|
73
|
+
throw new Error("Document operations mismatch");
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{ throws: true },
|
|
77
|
+
);
|
|
78
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { RealWorldAssets } from "@sky-ph/atlas/document-models";
|
|
2
|
+
import {
|
|
3
|
+
DocumentDriveServer,
|
|
4
|
+
generateUUID,
|
|
5
|
+
InternalTransmitterUpdate,
|
|
6
|
+
IReceiver,
|
|
7
|
+
} from "document-drive";
|
|
8
|
+
import { MemoryStorage } from "document-drive/storage/memory";
|
|
9
|
+
import {
|
|
10
|
+
module as DocumentDrive,
|
|
11
|
+
generateAddNodeAction,
|
|
12
|
+
ListenerFilter,
|
|
13
|
+
} from "document-model-libs/document-drive";
|
|
14
|
+
import {
|
|
15
|
+
Document,
|
|
16
|
+
DocumentModel,
|
|
17
|
+
OperationScope,
|
|
18
|
+
} from "document-model/document";
|
|
19
|
+
|
|
20
|
+
import { beforeAll, bench, describe } from "vitest";
|
|
21
|
+
|
|
22
|
+
class TestReceiver<
|
|
23
|
+
T extends Document = Document,
|
|
24
|
+
S extends OperationScope = OperationScope,
|
|
25
|
+
> implements IReceiver<T, S>
|
|
26
|
+
{
|
|
27
|
+
async onStrands(strands: InternalTransmitterUpdate<T, S>[]) {
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async onDisconnect() {
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {});
|
|
37
|
+
|
|
38
|
+
describe("Document Drive", async () => {
|
|
39
|
+
const documentModels = Object.values([
|
|
40
|
+
DocumentDrive,
|
|
41
|
+
RealWorldAssets,
|
|
42
|
+
]) as DocumentModel[];
|
|
43
|
+
const document = await RealWorldAssets.utils.loadFromFile(
|
|
44
|
+
"test/data/BlocktowerAndromeda.zip",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
bench(
|
|
48
|
+
"Load PHDM into Document Drive",
|
|
49
|
+
async () => {
|
|
50
|
+
const serverA = new DocumentDriveServer(
|
|
51
|
+
documentModels,
|
|
52
|
+
new MemoryStorage(),
|
|
53
|
+
);
|
|
54
|
+
await serverA.initialize();
|
|
55
|
+
|
|
56
|
+
const serverB = new DocumentDriveServer(
|
|
57
|
+
documentModels,
|
|
58
|
+
new MemoryStorage(),
|
|
59
|
+
);
|
|
60
|
+
await serverB.initialize();
|
|
61
|
+
|
|
62
|
+
const driveAId = generateUUID();
|
|
63
|
+
const documentId = generateUUID();
|
|
64
|
+
|
|
65
|
+
const driveA = await serverA.addDrive({
|
|
66
|
+
global: {
|
|
67
|
+
id: driveAId,
|
|
68
|
+
name: "Test Drive",
|
|
69
|
+
icon: null,
|
|
70
|
+
slug: null,
|
|
71
|
+
},
|
|
72
|
+
local: {
|
|
73
|
+
availableOffline: false,
|
|
74
|
+
sharingType: "PRIVATE",
|
|
75
|
+
listeners: [],
|
|
76
|
+
triggers: [],
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const driveBId = generateUUID();
|
|
81
|
+
const driveB = await serverB.addDrive({
|
|
82
|
+
global: {
|
|
83
|
+
id: driveBId,
|
|
84
|
+
name: "Test Drive",
|
|
85
|
+
icon: null,
|
|
86
|
+
slug: null,
|
|
87
|
+
},
|
|
88
|
+
local: {
|
|
89
|
+
availableOffline: false,
|
|
90
|
+
sharingType: "PRIVATE",
|
|
91
|
+
listeners: [],
|
|
92
|
+
triggers: [],
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// listener!
|
|
97
|
+
const filter: ListenerFilter = {
|
|
98
|
+
branch: ["*"],
|
|
99
|
+
documentId: ["*"],
|
|
100
|
+
documentType: ["*"],
|
|
101
|
+
scope: ["*"],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const receiver = new TestReceiver();
|
|
105
|
+
await serverA.addInternalListener(driveAId, receiver, {
|
|
106
|
+
listenerId: generateUUID(),
|
|
107
|
+
label: "Test Listener",
|
|
108
|
+
block: false,
|
|
109
|
+
filter,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await serverB.addInternalListener(driveBId, receiver, {
|
|
113
|
+
listenerId: generateUUID(),
|
|
114
|
+
label: "Test Listener",
|
|
115
|
+
block: false,
|
|
116
|
+
filter,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// loads document in drive A
|
|
120
|
+
const addFileAction = generateAddNodeAction(
|
|
121
|
+
driveA.state.global,
|
|
122
|
+
{
|
|
123
|
+
documentType: document.documentType,
|
|
124
|
+
id: documentId,
|
|
125
|
+
name: "BlocktowerAndromeda",
|
|
126
|
+
},
|
|
127
|
+
["global"],
|
|
128
|
+
);
|
|
129
|
+
await serverA.addDriveAction(driveAId, addFileAction);
|
|
130
|
+
|
|
131
|
+
const result = await serverA.addOperations(
|
|
132
|
+
driveAId,
|
|
133
|
+
documentId,
|
|
134
|
+
document.operations.global,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (result.error) {
|
|
138
|
+
throw result.error;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const lastOperation = document.operations.global.at(-1);
|
|
142
|
+
const lastLoadedOperation = result.operations.at(-1);
|
|
143
|
+
if (
|
|
144
|
+
JSON.stringify(lastOperation) !== JSON.stringify(lastLoadedOperation)
|
|
145
|
+
) {
|
|
146
|
+
throw new Error("Document operations mismatch");
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{ throws: true },
|
|
150
|
+
);
|
|
151
|
+
});
|
|
Binary file
|