@powerhousedao/reactor-api 1.2.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 +21 -0
- package/LICENSE +661 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +16403 -0
- package/dist/index.js.map +1 -0
- package/dist/schema-2TGHGOBQ.graphql +18 -0
- package/dist/schema-GBN2TKQG.graphql +142 -0
- package/package.json +42 -0
- package/src/index.ts +3 -0
- package/src/internal-listener-manager.ts +111 -0
- package/src/router.ts +122 -0
- package/src/server.ts +21 -0
- package/src/subgraphs/drive/resolvers.ts +223 -0
- package/src/subgraphs/drive/schema.graphql +142 -0
- package/src/subgraphs/drive/subgraph.ts +16 -0
- package/src/subgraphs/index.ts +13 -0
- package/src/subgraphs/system/resolvers.ts +22 -0
- package/src/subgraphs/system/schema.graphql +18 -0
- package/src/subgraphs/system/subgraph.ts +16 -0
- package/src/types.ts +8 -0
- package/src/utils/create-schema.ts +81 -0
- package/test/router.test.ts +55 -0
- package/tsconfig.json +18 -0
- package/tsdoc.json +3 -0
- package/tsup.config.ts +16 -0
- package/types.d.ts +5 -0
- package/vitest.config.ts +28 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type Query {
|
|
2
|
+
drives: [String!]!
|
|
3
|
+
driveIdBySlug(slug: String!): String
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type Mutation {
|
|
7
|
+
addDrive(global: DocumentDriveStateInput!): DocumentDriveState
|
|
8
|
+
deleteDrive(id: ID!): Boolean
|
|
9
|
+
setDriveIcon(id: String!, icon: String!): Boolean
|
|
10
|
+
setDriveName(id: String!, name: String!): Boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
input DocumentDriveStateInput {
|
|
14
|
+
name: String
|
|
15
|
+
id: String
|
|
16
|
+
slug: String
|
|
17
|
+
icon: String
|
|
18
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
type Query {
|
|
2
|
+
system: System
|
|
3
|
+
drive: DocumentDriveState
|
|
4
|
+
document(id: ID!): IDocument
|
|
5
|
+
documents: [String!]!
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type Mutation {
|
|
9
|
+
registerPullResponderListener(filter: InputListenerFilter!): Listener
|
|
10
|
+
pushUpdates(strands: [InputStrandUpdate!]): [ListenerRevision!]!
|
|
11
|
+
acknowledge(listenerId: String!, revisions: [ListenerRevisionInput]): Boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
input InputOperationSignerUser {
|
|
15
|
+
address: String!
|
|
16
|
+
networkId: String!
|
|
17
|
+
chainId: Int!
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type OperationSignerUser {
|
|
21
|
+
address: String!
|
|
22
|
+
networkId: String!
|
|
23
|
+
chainId: Int!
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
input InputOperationSignerApp {
|
|
27
|
+
name: String!
|
|
28
|
+
key: String!
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type OperationSignerApp {
|
|
32
|
+
name: String!
|
|
33
|
+
key: String!
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
input InputListenerFilter {
|
|
37
|
+
documentType: [String!]
|
|
38
|
+
documentId: [String!]
|
|
39
|
+
scope: [String!]
|
|
40
|
+
branch: [String!]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type OperationSigner {
|
|
44
|
+
app: OperationSignerApp
|
|
45
|
+
user: OperationSignerUser
|
|
46
|
+
signatures: [String!]!
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
input InputOperationSigner {
|
|
50
|
+
app: InputOperationSignerApp
|
|
51
|
+
user: InputOperationSignerUser
|
|
52
|
+
signatures: [String!]!
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type OperationContext {
|
|
56
|
+
signer: OperationSigner
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
input InputOperationContext {
|
|
60
|
+
signer: InputOperationSigner
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
input InputOperationUpdate {
|
|
64
|
+
index: Int!
|
|
65
|
+
skip: Int
|
|
66
|
+
type: String!
|
|
67
|
+
id: String!
|
|
68
|
+
input: String!
|
|
69
|
+
hash: String!
|
|
70
|
+
timestamp: String!
|
|
71
|
+
error: String
|
|
72
|
+
context: InputOperationContext
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type OperationUpdate {
|
|
76
|
+
index: Int!
|
|
77
|
+
skip: Int
|
|
78
|
+
type: String!
|
|
79
|
+
id: String!
|
|
80
|
+
input: String!
|
|
81
|
+
hash: String!
|
|
82
|
+
timestamp: String!
|
|
83
|
+
error: String
|
|
84
|
+
context: OperationContext
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type StrandUpdate {
|
|
88
|
+
driveId: String!
|
|
89
|
+
documentId: String!
|
|
90
|
+
scope: String!
|
|
91
|
+
branch: String!
|
|
92
|
+
operations: [OperationUpdate!]!
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
input InputStrandUpdate {
|
|
96
|
+
driveId: String!
|
|
97
|
+
documentId: String!
|
|
98
|
+
scope: String!
|
|
99
|
+
branch: String!
|
|
100
|
+
operations: [InputOperationUpdate!]!
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
input ListenerFilterInput {
|
|
104
|
+
documentType: [String!]
|
|
105
|
+
documentId: [String!]
|
|
106
|
+
scope: [String!]
|
|
107
|
+
branch: [String!]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
enum UpdateStatus {
|
|
111
|
+
SUCCESS
|
|
112
|
+
MISSING
|
|
113
|
+
CONFLICT
|
|
114
|
+
ERROR
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
input ListenerRevisionInput {
|
|
118
|
+
driveId: String!
|
|
119
|
+
documentId: String!
|
|
120
|
+
scope: String!
|
|
121
|
+
branch: String!
|
|
122
|
+
status: UpdateStatus!
|
|
123
|
+
revision: Int!
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type ListenerRevision {
|
|
127
|
+
driveId: String!
|
|
128
|
+
documentId: String!
|
|
129
|
+
scope: String!
|
|
130
|
+
branch: String!
|
|
131
|
+
status: UpdateStatus!
|
|
132
|
+
revision: Int!
|
|
133
|
+
error: String
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type System {
|
|
137
|
+
sync: Sync
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type Sync {
|
|
141
|
+
strands(listenerId: ID!, since: String): [StrandUpdate!]!
|
|
142
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@powerhousedao/reactor-api",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "AGPL-3.0-only",
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/body-parser": "^1.19.5",
|
|
17
|
+
"@types/cors": "^2.8.17",
|
|
18
|
+
"@types/express": "^5.0.0",
|
|
19
|
+
"document-drive": "1.2.0",
|
|
20
|
+
"esbuild": "^0.24.0",
|
|
21
|
+
"graphql": "^16.9.0",
|
|
22
|
+
"graphql-tag": "^2.12.6",
|
|
23
|
+
"tsup": "^8.3.0",
|
|
24
|
+
"document-model": "2.3.1"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"document-drive": "^1.0.1",
|
|
28
|
+
"document-model": "^2.2.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@apollo/server": "^4.11.0",
|
|
32
|
+
"@apollo/subgraph": "^2.9.2",
|
|
33
|
+
"body-parser": "^1.20.3",
|
|
34
|
+
"cors": "^2.8.5",
|
|
35
|
+
"document-model-libs": "1.93.2",
|
|
36
|
+
"express": "^4.21.1"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"test": "vitest"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseDocumentDriveServer,
|
|
3
|
+
InternalTransmitter,
|
|
4
|
+
InternalTransmitterUpdate,
|
|
5
|
+
Listener,
|
|
6
|
+
} from "document-drive";
|
|
7
|
+
import { DocumentDriveDocument } from "document-model-libs/document-drive";
|
|
8
|
+
|
|
9
|
+
export type InternalListenerModule = {
|
|
10
|
+
name: string;
|
|
11
|
+
options: Omit<Listener, "driveId">;
|
|
12
|
+
transmit: (strands: InternalTransmitterUpdate[]) => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class InternalListenerManager {
|
|
16
|
+
private driveServer: BaseDocumentDriveServer;
|
|
17
|
+
private modules: InternalListenerModule[] = [];
|
|
18
|
+
|
|
19
|
+
constructor(driveServer: BaseDocumentDriveServer) {
|
|
20
|
+
this.driveServer = driveServer;
|
|
21
|
+
driveServer.on("driveAdded", this.#onDriveAdded.bind(this));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async #onDriveAdded(drive: DocumentDriveDocument) {
|
|
25
|
+
await Promise.all(
|
|
26
|
+
this.modules.map((module) =>
|
|
27
|
+
this.driveServer.addInternalListener(
|
|
28
|
+
drive.state.global.id,
|
|
29
|
+
{
|
|
30
|
+
transmit: (strands) => module.transmit(strands),
|
|
31
|
+
disconnect: async () => {
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{ ...module.options, label: module.options.label ?? "" }
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async init() {
|
|
42
|
+
const drives = await this.driveServer.getDrives();
|
|
43
|
+
|
|
44
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
45
|
+
for (const { options, transmit } of this.modules) {
|
|
46
|
+
console.log(options, transmit);
|
|
47
|
+
if (!options || !transmit) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
52
|
+
for (const driveId of drives) {
|
|
53
|
+
try {
|
|
54
|
+
const { listenerId } = options;
|
|
55
|
+
const drive = await this.driveServer.getDrive(driveId);
|
|
56
|
+
const moduleRegistered =
|
|
57
|
+
drive.state.local.listeners.filter(
|
|
58
|
+
(l) => l.listenerId === listenerId
|
|
59
|
+
).length > 0;
|
|
60
|
+
if (!moduleRegistered) {
|
|
61
|
+
await this.driveServer.addInternalListener(
|
|
62
|
+
driveId,
|
|
63
|
+
{
|
|
64
|
+
transmit: async (strands) => transmit(strands),
|
|
65
|
+
disconnect: async () => Promise.resolve(),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
block: false,
|
|
69
|
+
filter: options.filter,
|
|
70
|
+
label: options.label!,
|
|
71
|
+
listenerId,
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const transmitter = await this.driveServer.getTransmitter(
|
|
79
|
+
driveId,
|
|
80
|
+
listenerId
|
|
81
|
+
);
|
|
82
|
+
if (transmitter instanceof InternalTransmitter) {
|
|
83
|
+
transmitter.setReceiver({
|
|
84
|
+
transmit: async (strands: InternalTransmitterUpdate[]) => {
|
|
85
|
+
await transmit(strands);
|
|
86
|
+
return Promise.resolve();
|
|
87
|
+
},
|
|
88
|
+
disconnect: () => {
|
|
89
|
+
console.log(`Disconnecting listener ${options.listenerId}`);
|
|
90
|
+
return Promise.resolve();
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error(
|
|
96
|
+
`Error while initializing listener ${options.listenerId} for drive ${driveId}`,
|
|
97
|
+
e
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async registerInternalListener(module: InternalListenerModule) {
|
|
105
|
+
if (this.modules.find((m) => m.name === module.name)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.modules.push(module);
|
|
109
|
+
await this.init();
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ApolloServer } from "@apollo/server";
|
|
2
|
+
import { expressMiddleware } from "@apollo/server/express4";
|
|
3
|
+
import { ApolloServerPluginInlineTraceDisabled } from "@apollo/server/plugin/disabled";
|
|
4
|
+
import bodyParser from "body-parser";
|
|
5
|
+
import cors from "cors";
|
|
6
|
+
import { BaseDocumentDriveServer } from "document-drive";
|
|
7
|
+
import express, { IRouter, Router } from "express";
|
|
8
|
+
import {
|
|
9
|
+
InternalListenerManager,
|
|
10
|
+
InternalListenerModule,
|
|
11
|
+
} from "./internal-listener-manager";
|
|
12
|
+
import { SUBGRAPH_REGISTRY } from "./subgraphs";
|
|
13
|
+
import { Context } from "./types";
|
|
14
|
+
export let reactorRouter: IRouter = Router();
|
|
15
|
+
|
|
16
|
+
const getLocalSubgraphConfig = (subgraphName: string) =>
|
|
17
|
+
SUBGRAPH_REGISTRY.find((it) => it.name === subgraphName);
|
|
18
|
+
|
|
19
|
+
let listenerManager: InternalListenerManager | undefined;
|
|
20
|
+
|
|
21
|
+
export const getListenerManager = async (
|
|
22
|
+
driveServer: BaseDocumentDriveServer
|
|
23
|
+
) => {
|
|
24
|
+
if (!listenerManager) {
|
|
25
|
+
listenerManager = new InternalListenerManager(driveServer);
|
|
26
|
+
await listenerManager.init();
|
|
27
|
+
}
|
|
28
|
+
return listenerManager;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const updateRouter = async (driveServer: BaseDocumentDriveServer) => {
|
|
32
|
+
const newRouter = Router();
|
|
33
|
+
newRouter.use(cors());
|
|
34
|
+
newRouter.use(bodyParser.json());
|
|
35
|
+
// Run each subgraph on the same http server, but at different paths
|
|
36
|
+
for (const subgraph of SUBGRAPH_REGISTRY) {
|
|
37
|
+
const subgraphConfig = getLocalSubgraphConfig(subgraph.name);
|
|
38
|
+
if (!subgraphConfig) continue;
|
|
39
|
+
|
|
40
|
+
// get schema
|
|
41
|
+
const schema = subgraphConfig.getSchema(driveServer);
|
|
42
|
+
|
|
43
|
+
// create apollo server
|
|
44
|
+
const server = new ApolloServer({
|
|
45
|
+
schema,
|
|
46
|
+
introspection: true,
|
|
47
|
+
plugins: [ApolloServerPluginInlineTraceDisabled()],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// start apollo server
|
|
51
|
+
await server.start();
|
|
52
|
+
|
|
53
|
+
// setup path
|
|
54
|
+
const path = `/${subgraphConfig.name}`;
|
|
55
|
+
newRouter.use(
|
|
56
|
+
path,
|
|
57
|
+
expressMiddleware(server, {
|
|
58
|
+
context: ({ req }): Promise<Context> =>
|
|
59
|
+
Promise.resolve({
|
|
60
|
+
headers: req.headers,
|
|
61
|
+
driveId: req.params.drive ?? undefined,
|
|
62
|
+
driveServer,
|
|
63
|
+
...getAdditionalContextFields(),
|
|
64
|
+
}),
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
console.log(`Setting up [${subgraphConfig.name}] subgraph at ${path}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
listenerManager = await getListenerManager(driveServer);
|
|
71
|
+
reactorRouter = newRouter;
|
|
72
|
+
console.log("All subgraphs started.");
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
let docDriveServer: BaseDocumentDriveServer;
|
|
76
|
+
export const initReactorRouter = async (
|
|
77
|
+
path: string,
|
|
78
|
+
app: express.Express,
|
|
79
|
+
driveServer: BaseDocumentDriveServer
|
|
80
|
+
) => {
|
|
81
|
+
docDriveServer = driveServer;
|
|
82
|
+
const models = driveServer.getDocumentModels();
|
|
83
|
+
const driveModel = models.find(
|
|
84
|
+
(it) => it.documentModel.name === "DocumentDrive"
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!driveModel) {
|
|
88
|
+
throw new Error("DocumentDrive model required");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await updateRouter(driveServer);
|
|
92
|
+
driveServer.on("documentModels", () => {
|
|
93
|
+
updateRouter(driveServer);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
app.use(path, (req, res, next) => reactorRouter(req, res, next));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const addSubgraph = async (
|
|
100
|
+
subgraph: (typeof SUBGRAPH_REGISTRY)[number]
|
|
101
|
+
) => {
|
|
102
|
+
SUBGRAPH_REGISTRY.unshift(subgraph);
|
|
103
|
+
await updateRouter(docDriveServer);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const registerInternalListener = async (
|
|
107
|
+
module: InternalListenerModule
|
|
108
|
+
) => {
|
|
109
|
+
if (!listenerManager) {
|
|
110
|
+
throw new Error("Listener manager not initialized");
|
|
111
|
+
}
|
|
112
|
+
await listenerManager.registerInternalListener(module);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
let contextFields = {};
|
|
116
|
+
export const getAdditionalContextFields = () => {
|
|
117
|
+
return contextFields;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const setAdditionalContextFields = (fields: Record<string, any>) => {
|
|
121
|
+
contextFields = { ...contextFields, ...fields };
|
|
122
|
+
};
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaseDocumentDriveServer } from "document-drive";
|
|
2
|
+
import express, { Express } from "express";
|
|
3
|
+
import { initReactorRouter } from "./router";
|
|
4
|
+
type Options = {
|
|
5
|
+
express?: Express;
|
|
6
|
+
port?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PORT = 4000;
|
|
10
|
+
|
|
11
|
+
export async function startAPI(
|
|
12
|
+
reactor: BaseDocumentDriveServer,
|
|
13
|
+
options: Options
|
|
14
|
+
) {
|
|
15
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
16
|
+
const app = options.express ?? express();
|
|
17
|
+
|
|
18
|
+
await initReactorRouter("/", app, reactor);
|
|
19
|
+
|
|
20
|
+
app.listen(port);
|
|
21
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateUUID,
|
|
3
|
+
ListenerRevision,
|
|
4
|
+
PullResponderTransmitter,
|
|
5
|
+
StrandUpdateGraphQL,
|
|
6
|
+
} from "document-drive";
|
|
7
|
+
import {
|
|
8
|
+
actions,
|
|
9
|
+
DocumentDriveAction,
|
|
10
|
+
Listener,
|
|
11
|
+
ListenerFilter,
|
|
12
|
+
TransmitterType,
|
|
13
|
+
} from "document-model-libs/document-drive";
|
|
14
|
+
import { BaseAction, Operation } from "document-model/document";
|
|
15
|
+
import {
|
|
16
|
+
DocumentModelInput,
|
|
17
|
+
DocumentModelState,
|
|
18
|
+
} from "document-model/document-model";
|
|
19
|
+
import { Context } from "../../../../../../apps/switchboard/types";
|
|
20
|
+
|
|
21
|
+
export const resolvers = {
|
|
22
|
+
Query: {
|
|
23
|
+
drive: async (_: unknown, args: unknown, ctx: Context) => {
|
|
24
|
+
if (!ctx.driveId) throw new Error("Drive ID is required");
|
|
25
|
+
const drive = await ctx.driveServer.getDrive(ctx.driveId);
|
|
26
|
+
return drive.state.global;
|
|
27
|
+
},
|
|
28
|
+
documents: async (_: unknown, args: unknown, ctx: Context) => {
|
|
29
|
+
if (!ctx.driveId) throw new Error("Drive ID is required");
|
|
30
|
+
const documents = await ctx.driveServer.getDocuments(ctx.driveId);
|
|
31
|
+
return documents;
|
|
32
|
+
},
|
|
33
|
+
document: async (_: unknown, { id }: { id: string }, ctx: Context) => {
|
|
34
|
+
if (!ctx.driveId) throw new Error("Drive ID is required");
|
|
35
|
+
const document = await ctx.driveServer.getDocument(ctx.driveId, id);
|
|
36
|
+
|
|
37
|
+
const dms = ctx.driveServer.getDocumentModels();
|
|
38
|
+
const dm = dms.find(
|
|
39
|
+
({ documentModel }: { documentModel: DocumentModelState }) =>
|
|
40
|
+
documentModel.id === document.documentType
|
|
41
|
+
);
|
|
42
|
+
const globalState = document.state.global;
|
|
43
|
+
if (!globalState) throw new Error("Document not found");
|
|
44
|
+
const response = {
|
|
45
|
+
...document,
|
|
46
|
+
id,
|
|
47
|
+
revision: document.revision.global,
|
|
48
|
+
state: document.state.global,
|
|
49
|
+
operations: document.operations.global.map((op: Operation) => ({
|
|
50
|
+
...op,
|
|
51
|
+
inputText:
|
|
52
|
+
typeof op.input === "string" ? op.input : JSON.stringify(op.input),
|
|
53
|
+
})),
|
|
54
|
+
initialState: document.initialState.state.global,
|
|
55
|
+
__typename: dm?.documentModel.name,
|
|
56
|
+
};
|
|
57
|
+
console.log(response);
|
|
58
|
+
return response;
|
|
59
|
+
},
|
|
60
|
+
system: () => ({ sync: {} }),
|
|
61
|
+
},
|
|
62
|
+
Mutation: {
|
|
63
|
+
registerPullResponderListener: async (
|
|
64
|
+
_: unknown,
|
|
65
|
+
{ filter }: { filter: ListenerFilter },
|
|
66
|
+
ctx: Context
|
|
67
|
+
) => {
|
|
68
|
+
if (!ctx.driveId) throw new Error("Drive ID is required");
|
|
69
|
+
const uuid = generateUUID();
|
|
70
|
+
const listener: Listener = {
|
|
71
|
+
block: false,
|
|
72
|
+
callInfo: {
|
|
73
|
+
data: "",
|
|
74
|
+
name: "PullResponder",
|
|
75
|
+
transmitterType: "PullResponder" as TransmitterType,
|
|
76
|
+
},
|
|
77
|
+
filter: {
|
|
78
|
+
branch: filter.branch ?? [],
|
|
79
|
+
documentId: filter.documentId ?? [],
|
|
80
|
+
documentType: filter.documentType ?? [],
|
|
81
|
+
scope: filter.scope ?? [],
|
|
82
|
+
},
|
|
83
|
+
label: `Pullresponder #${uuid}`,
|
|
84
|
+
listenerId: uuid,
|
|
85
|
+
system: false,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const result = await ctx.driveServer.queueDriveAction(
|
|
89
|
+
ctx.driveId,
|
|
90
|
+
actions.addListener({ listener })
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
console.log(result);
|
|
94
|
+
if (result.status !== "SUCCESS" && result.error) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Listener couldn't be registered: ${result.error.message}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return listener;
|
|
101
|
+
},
|
|
102
|
+
pushUpdates: async (
|
|
103
|
+
_: unknown,
|
|
104
|
+
{ strands }: { strands: StrandUpdateGraphQL[] },
|
|
105
|
+
ctx: Context
|
|
106
|
+
) => {
|
|
107
|
+
if (!ctx.driveId) throw new Error("Drive ID is required");
|
|
108
|
+
const listenerRevisions: ListenerRevision[] = await Promise.all(
|
|
109
|
+
strands.map(async (s) => {
|
|
110
|
+
const operations =
|
|
111
|
+
s.operations.map((o) => ({
|
|
112
|
+
...o,
|
|
113
|
+
input: JSON.parse(o.input) as DocumentModelInput,
|
|
114
|
+
skip: o.skip ?? 0,
|
|
115
|
+
scope: s.scope,
|
|
116
|
+
branch: "main",
|
|
117
|
+
})) ?? [];
|
|
118
|
+
|
|
119
|
+
const result = await (s.documentId !== undefined
|
|
120
|
+
? ctx.driveServer.queueOperations(
|
|
121
|
+
s.driveId,
|
|
122
|
+
s.documentId,
|
|
123
|
+
operations
|
|
124
|
+
)
|
|
125
|
+
: ctx.driveServer.queueDriveOperations(
|
|
126
|
+
s.driveId,
|
|
127
|
+
operations as Operation<DocumentDriveAction | BaseAction>[]
|
|
128
|
+
));
|
|
129
|
+
|
|
130
|
+
const scopeOperations = result.document?.operations[s.scope] ?? [];
|
|
131
|
+
if (scopeOperations.length === 0) {
|
|
132
|
+
return {
|
|
133
|
+
revision: -1,
|
|
134
|
+
branch: s.branch,
|
|
135
|
+
documentId: s.documentId ?? "",
|
|
136
|
+
driveId: s.driveId,
|
|
137
|
+
scope: s.scope,
|
|
138
|
+
status: result.status,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const revision = scopeOperations.slice().pop()?.index ?? -1;
|
|
143
|
+
return {
|
|
144
|
+
revision,
|
|
145
|
+
branch: s.branch,
|
|
146
|
+
documentId: s.documentId ?? "",
|
|
147
|
+
driveId: s.driveId,
|
|
148
|
+
scope: s.scope,
|
|
149
|
+
status: result.status,
|
|
150
|
+
error: result.error?.message || undefined,
|
|
151
|
+
};
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return listenerRevisions;
|
|
156
|
+
},
|
|
157
|
+
acknowledge: async (
|
|
158
|
+
_: unknown,
|
|
159
|
+
{
|
|
160
|
+
listenerId,
|
|
161
|
+
revisions,
|
|
162
|
+
}: { listenerId: string; revisions: ListenerRevision[] },
|
|
163
|
+
ctx: Context
|
|
164
|
+
) => {
|
|
165
|
+
if (!listenerId || !revisions) return false;
|
|
166
|
+
if (!ctx.driveId) throw new Error("Drive ID is required");
|
|
167
|
+
const validEntries = revisions
|
|
168
|
+
.filter((r) => r !== null)
|
|
169
|
+
.map((e) => ({
|
|
170
|
+
driveId: e.driveId,
|
|
171
|
+
documentId: e.documentId,
|
|
172
|
+
scope: e.scope,
|
|
173
|
+
branch: e.branch,
|
|
174
|
+
revision: e.revision,
|
|
175
|
+
status: e.status,
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
const transmitter = (await ctx.driveServer.getTransmitter(
|
|
179
|
+
ctx.driveId,
|
|
180
|
+
listenerId
|
|
181
|
+
)) as PullResponderTransmitter;
|
|
182
|
+
const result = await transmitter.processAcknowledge(
|
|
183
|
+
ctx.driveId ?? "1",
|
|
184
|
+
listenerId,
|
|
185
|
+
validEntries
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
return result;
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
System: {},
|
|
192
|
+
Sync: {
|
|
193
|
+
strands: async (
|
|
194
|
+
_: unknown,
|
|
195
|
+
{ listenerId, since }: { listenerId: string; since: string | undefined },
|
|
196
|
+
ctx: Context
|
|
197
|
+
) => {
|
|
198
|
+
if (!ctx.driveId) throw new Error("Drive ID is required");
|
|
199
|
+
const listener = (await ctx.driveServer.getTransmitter(
|
|
200
|
+
ctx.driveId,
|
|
201
|
+
listenerId
|
|
202
|
+
)) as PullResponderTransmitter;
|
|
203
|
+
const strands = await listener.getStrands({ since });
|
|
204
|
+
return strands.map((e) => ({
|
|
205
|
+
driveId: e.driveId,
|
|
206
|
+
documentId: e.documentId,
|
|
207
|
+
scope: e.scope,
|
|
208
|
+
branch: e.branch,
|
|
209
|
+
operations: e.operations.map((o) => ({
|
|
210
|
+
index: o.index,
|
|
211
|
+
skip: o.skip,
|
|
212
|
+
name: o.type,
|
|
213
|
+
input: JSON.stringify(o.input),
|
|
214
|
+
hash: o.hash,
|
|
215
|
+
timestamp: o.timestamp,
|
|
216
|
+
type: o.type,
|
|
217
|
+
context: o.context,
|
|
218
|
+
id: o.id,
|
|
219
|
+
})),
|
|
220
|
+
}));
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
};
|