@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powerhousedao/reactor-api",
3
- "version": "1.20.2",
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
- "@powerhousedao/scalars": "1.22.0",
27
- "document-drive": "1.17.2",
28
- "document-model": "2.19.0"
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.131.2"
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 { BaseAction, Document, Operation } from "document-model/document";
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
- const scopeOperations = result.document?.operations[s.scope] ?? [];
306
- if (scopeOperations.length === 0) {
307
- return {
308
- revision: -1,
309
- branch: s.branch,
310
- documentId: s.documentId ?? "",
311
- driveId: s.driveId,
312
- scope: s.scope,
313
- status: result.status,
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
- const revision = scopeOperations.slice().pop()?.index ?? -1;
318
- return {
319
- revision,
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
- const transmitter = (await this.reactor.getTransmitter(
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
- const listener = (await this.reactor.getTransmitter(
353
+
354
+ // get the requested strand updates
355
+ const strands = await processGetStrands(
356
+ this.reactor,
378
357
  ctx.driveId,
379
358
  listenerId,
380
- )) as PullResponderTransmitter;
381
- const strands = await listener.getStrands({ since });
382
- return strands.map((e) => ({
383
- driveId: e.driveId,
384
- documentId: e.documentId,
385
- scope: e.scope,
386
- branch: e.branch,
387
- operations: e.operations.map((o) => ({
388
- index: o.index,
389
- skip: o.skip,
390
- name: o.type,
391
- input: JSON.stringify(o.input),
392
- hash: o.hash,
393
- timestamp: o.timestamp,
394
- type: o.type,
395
- context: o.context,
396
- id: o.id,
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
+ });