document-drive 1.19.1 → 1.20.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 +4 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/src/cache/memory.d.ts +10 -0
- package/dist/src/cache/memory.d.ts.map +1 -0
- package/dist/src/cache/memory.js +26 -0
- package/dist/src/cache/redis.d.ts +14 -0
- package/dist/src/cache/redis.d.ts.map +1 -0
- package/dist/src/cache/redis.js +40 -0
- package/dist/src/cache/types.d.ts +7 -0
- package/dist/src/cache/types.d.ts.map +1 -0
- package/dist/src/cache/types.js +1 -0
- package/dist/src/drive-document-model/constants.d.ts +2 -0
- package/dist/src/drive-document-model/constants.d.ts.map +1 -0
- package/dist/src/drive-document-model/constants.js +1 -0
- package/dist/src/drive-document-model/gen/actions.d.ts +7 -0
- package/dist/src/drive-document-model/gen/actions.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/actions.js +2 -0
- package/dist/src/drive-document-model/gen/constants.d.ts +7 -0
- package/dist/src/drive-document-model/gen/constants.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/constants.js +16 -0
- package/dist/src/drive-document-model/gen/creators.d.ts +3 -0
- package/dist/src/drive-document-model/gen/creators.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/creators.js +2 -0
- package/dist/src/drive-document-model/gen/document-model.d.ts +3 -0
- package/dist/src/drive-document-model/gen/document-model.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/document-model.js +210 -0
- package/dist/src/drive-document-model/gen/drive/actions.d.ts +12 -0
- package/dist/src/drive-document-model/gen/drive/actions.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/drive/actions.js +1 -0
- package/dist/src/drive-document-model/gen/drive/creators.d.ts +11 -0
- package/dist/src/drive-document-model/gen/drive/creators.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/drive/creators.js +10 -0
- package/dist/src/drive-document-model/gen/drive/error.d.ts +2 -0
- package/dist/src/drive-document-model/gen/drive/error.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/drive/error.js +1 -0
- package/dist/src/drive-document-model/gen/drive/object.d.ts +14 -0
- package/dist/src/drive-document-model/gen/drive/object.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/drive/object.js +28 -0
- package/dist/src/drive-document-model/gen/drive/operations.d.ts +14 -0
- package/dist/src/drive-document-model/gen/drive/operations.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/drive/operations.js +1 -0
- package/dist/src/drive-document-model/gen/node/actions.d.ts +11 -0
- package/dist/src/drive-document-model/gen/node/actions.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/node/actions.js +1 -0
- package/dist/src/drive-document-model/gen/node/creators.d.ts +10 -0
- package/dist/src/drive-document-model/gen/node/creators.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/node/creators.js +9 -0
- package/dist/src/drive-document-model/gen/node/error.d.ts +2 -0
- package/dist/src/drive-document-model/gen/node/error.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/node/error.js +1 -0
- package/dist/src/drive-document-model/gen/node/object.d.ts +13 -0
- package/dist/src/drive-document-model/gen/node/object.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/node/object.js +25 -0
- package/dist/src/drive-document-model/gen/node/operations.d.ts +13 -0
- package/dist/src/drive-document-model/gen/node/operations.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/node/operations.js +1 -0
- package/dist/src/drive-document-model/gen/object.d.ts +21 -0
- package/dist/src/drive-document-model/gen/object.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/object.js +28 -0
- package/dist/src/drive-document-model/gen/reducer.d.ts +4 -0
- package/dist/src/drive-document-model/gen/reducer.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/reducer.js +74 -0
- package/dist/src/drive-document-model/gen/schema/types.d.ts +176 -0
- package/dist/src/drive-document-model/gen/schema/types.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/schema/types.js +1 -0
- package/dist/src/drive-document-model/gen/schema/zod.d.ts +87 -0
- package/dist/src/drive-document-model/gen/schema/zod.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/schema/zod.js +203 -0
- package/dist/src/drive-document-model/gen/types.d.ts +9 -0
- package/dist/src/drive-document-model/gen/types.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/types.js +1 -0
- package/dist/src/drive-document-model/gen/utils.d.ts +10 -0
- package/dist/src/drive-document-model/gen/utils.d.ts.map +1 -0
- package/dist/src/drive-document-model/gen/utils.js +27 -0
- package/dist/src/drive-document-model/index.d.ts +2 -0
- package/dist/src/drive-document-model/index.d.ts.map +1 -0
- package/dist/src/drive-document-model/index.js +1 -0
- package/dist/src/drive-document-model/module.d.ts +3 -0
- package/dist/src/drive-document-model/module.d.ts.map +1 -0
- package/dist/src/drive-document-model/module.js +12 -0
- package/dist/src/drive-document-model/src/reducers/drive.d.ts +8 -0
- package/dist/src/drive-document-model/src/reducers/drive.d.ts.map +1 -0
- package/dist/src/drive-document-model/src/reducers/drive.js +37 -0
- package/dist/src/drive-document-model/src/reducers/node.d.ts +8 -0
- package/dist/src/drive-document-model/src/reducers/node.d.ts.map +1 -0
- package/dist/src/drive-document-model/src/reducers/node.js +185 -0
- package/dist/src/drive-document-model/src/utils.d.ts +34 -0
- package/dist/src/drive-document-model/src/utils.d.ts.map +1 -0
- package/dist/src/drive-document-model/src/utils.js +146 -0
- package/dist/src/queue/base.d.ts +43 -0
- package/dist/src/queue/base.d.ts.map +1 -0
- package/dist/src/queue/base.js +241 -0
- package/dist/src/queue/redis.d.ts +28 -0
- package/dist/src/queue/redis.d.ts.map +1 -0
- package/dist/src/queue/redis.js +110 -0
- package/dist/src/queue/types.d.ts +55 -0
- package/dist/src/queue/types.d.ts.map +1 -0
- package/dist/src/queue/types.js +6 -0
- package/dist/src/read-mode/errors.d.ts +12 -0
- package/dist/src/read-mode/errors.d.ts.map +1 -0
- package/dist/src/read-mode/errors.js +17 -0
- package/dist/src/read-mode/server.d.ts +4 -0
- package/dist/src/read-mode/server.d.ts.map +1 -0
- package/dist/src/read-mode/server.js +78 -0
- package/dist/src/read-mode/service.d.ts +18 -0
- package/dist/src/read-mode/service.d.ts.map +1 -0
- package/dist/src/read-mode/service.js +112 -0
- package/dist/src/read-mode/types.d.ts +35 -0
- package/dist/src/read-mode/types.d.ts.map +1 -0
- package/dist/src/read-mode/types.js +1 -0
- package/dist/src/server/base-server.d.ts +112 -0
- package/dist/src/server/base-server.d.ts.map +1 -0
- package/dist/src/server/base-server.js +1280 -0
- package/dist/src/server/builder.d.ts +30 -0
- package/dist/src/server/builder.d.ts.map +1 -0
- package/dist/src/server/builder.js +89 -0
- package/dist/src/server/constants.d.ts +2 -0
- package/dist/src/server/constants.d.ts.map +1 -0
- package/dist/src/server/constants.js +1 -0
- package/dist/src/server/error.d.ts +30 -0
- package/dist/src/server/error.d.ts.map +1 -0
- package/dist/src/server/error.js +47 -0
- package/dist/src/server/event-emitter.d.ts +8 -0
- package/dist/src/server/event-emitter.d.ts.map +1 -0
- package/dist/src/server/event-emitter.js +10 -0
- package/dist/src/server/listener/index.d.ts +2 -0
- package/dist/src/server/listener/index.d.ts.map +1 -0
- package/dist/src/server/listener/index.js +1 -0
- package/dist/src/server/listener/listener-manager.d.ts +27 -0
- package/dist/src/server/listener/listener-manager.d.ts.map +1 -0
- package/dist/src/server/listener/listener-manager.js +401 -0
- package/dist/src/server/listener/transmitter/factory.d.ts +8 -0
- package/dist/src/server/listener/transmitter/factory.d.ts.map +1 -0
- package/dist/src/server/listener/transmitter/factory.js +25 -0
- package/dist/src/server/listener/transmitter/internal.d.ts +34 -0
- package/dist/src/server/listener/transmitter/internal.d.ts.map +1 -0
- package/dist/src/server/listener/transmitter/internal.js +87 -0
- package/dist/src/server/listener/transmitter/pull-responder.d.ts +38 -0
- package/dist/src/server/listener/transmitter/pull-responder.d.ts.map +1 -0
- package/dist/src/server/listener/transmitter/pull-responder.js +256 -0
- package/dist/src/server/listener/transmitter/switchboard-push.d.ts +9 -0
- package/dist/src/server/listener/transmitter/switchboard-push.d.ts.map +1 -0
- package/dist/src/server/listener/transmitter/switchboard-push.js +77 -0
- package/dist/src/server/listener/transmitter/types.d.ts +20 -0
- package/dist/src/server/listener/transmitter/types.d.ts.map +1 -0
- package/dist/src/server/listener/transmitter/types.js +1 -0
- package/dist/src/server/listener/util.d.ts +2 -0
- package/dist/src/server/listener/util.d.ts.map +1 -0
- package/dist/src/server/listener/util.js +22 -0
- package/dist/src/server/sync-manager.d.ts +30 -0
- package/dist/src/server/sync-manager.d.ts.map +1 -0
- package/dist/src/server/sync-manager.js +287 -0
- package/dist/src/server/types.d.ts +308 -0
- package/dist/src/server/types.d.ts.map +1 -0
- package/dist/src/server/types.js +12 -0
- package/dist/src/server/utils.d.ts +8 -0
- package/dist/src/server/utils.d.ts.map +1 -0
- package/dist/src/server/utils.js +47 -0
- package/dist/src/storage/base.d.ts +36 -0
- package/dist/src/storage/base.d.ts.map +1 -0
- package/dist/src/storage/base.js +4 -0
- package/dist/src/storage/browser.d.ts +36 -0
- package/dist/src/storage/browser.d.ts.map +1 -0
- package/dist/src/storage/browser.js +155 -0
- package/dist/src/storage/filesystem.d.ts +33 -0
- package/dist/src/storage/filesystem.d.ts.map +1 -0
- package/dist/src/storage/filesystem.js +197 -0
- package/dist/src/storage/memory.d.ts +33 -0
- package/dist/src/storage/memory.d.ts.map +1 -0
- package/dist/src/storage/memory.js +139 -0
- package/dist/src/storage/prisma.d.ts +67 -0
- package/dist/src/storage/prisma.d.ts.map +1 -0
- package/dist/src/storage/prisma.js +445 -0
- package/dist/src/storage/sequelize.d.ts +32 -0
- package/dist/src/storage/sequelize.d.ts.map +1 -0
- package/dist/src/storage/sequelize.js +373 -0
- package/dist/src/storage/types.d.ts +43 -0
- package/dist/src/storage/types.d.ts.map +1 -0
- package/dist/src/storage/types.js +1 -0
- package/dist/src/utils/default-drives-manager.d.ts +29 -0
- package/dist/src/utils/default-drives-manager.d.ts.map +1 -0
- package/dist/src/utils/default-drives-manager.js +208 -0
- package/dist/src/utils/graphql.d.ts +34 -0
- package/dist/src/utils/graphql.d.ts.map +1 -0
- package/dist/src/utils/graphql.js +183 -0
- package/dist/src/utils/logger.d.ts +27 -0
- package/dist/src/utils/logger.d.ts.map +1 -0
- package/dist/src/utils/logger.js +105 -0
- package/dist/src/utils/migrations.d.ts +4 -0
- package/dist/src/utils/migrations.d.ts.map +1 -0
- package/dist/src/utils/migrations.js +41 -0
- package/dist/src/utils/misc.d.ts +11 -0
- package/dist/src/utils/misc.d.ts.map +1 -0
- package/dist/src/utils/misc.js +43 -0
- package/dist/src/utils/run-asap.d.ts +12 -0
- package/dist/src/utils/run-asap.d.ts.map +1 -0
- package/dist/src/utils/run-asap.js +131 -0
- package/dist/test/document-helpers/utils.d.ts +8 -0
- package/dist/test/document-helpers/utils.d.ts.map +1 -0
- package/dist/test/document-helpers/utils.js +21 -0
- package/dist/test/utils.d.ts +48 -0
- package/dist/test/utils.d.ts.map +1 -0
- package/dist/test/utils.js +132 -0
- package/dist/test/vitest-setup.d.ts +2 -0
- package/dist/test/vitest-setup.d.ts.map +1 -0
- package/dist/test/vitest-setup.js +4 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +20 -0
- package/package.json +20 -38
- package/src/cache/index.ts +0 -2
- package/src/cache/memory.ts +0 -33
- package/src/cache/redis.ts +0 -56
- package/src/cache/types.ts +0 -9
- package/src/index.ts +0 -4
- package/src/queue/base.ts +0 -320
- package/src/queue/index.ts +0 -2
- package/src/queue/redis.ts +0 -144
- package/src/queue/types.ts +0 -79
- package/src/read-mode/errors.ts +0 -19
- package/src/read-mode/index.ts +0 -125
- package/src/read-mode/service.ts +0 -207
- package/src/read-mode/types.ts +0 -108
- package/src/server/error.ts +0 -70
- package/src/server/index.ts +0 -2444
- package/src/server/listener/index.ts +0 -2
- package/src/server/listener/manager.ts +0 -652
- package/src/server/listener/transmitter/index.ts +0 -4
- package/src/server/listener/transmitter/internal.ts +0 -143
- package/src/server/listener/transmitter/pull-responder.ts +0 -462
- package/src/server/listener/transmitter/switchboard-push.ts +0 -125
- package/src/server/listener/transmitter/types.ts +0 -27
- package/src/server/types.ts +0 -596
- package/src/server/utils.ts +0 -82
- package/src/storage/base.ts +0 -81
- package/src/storage/browser.ts +0 -238
- package/src/storage/filesystem.ts +0 -297
- package/src/storage/index.ts +0 -2
- package/src/storage/memory.ts +0 -211
- package/src/storage/prisma.ts +0 -653
- package/src/storage/sequelize.ts +0 -498
- package/src/storage/types.ts +0 -97
- package/src/utils/default-drives-manager.ts +0 -341
- package/src/utils/document-helpers.ts +0 -21
- package/src/utils/graphql.ts +0 -301
- package/src/utils/index.ts +0 -90
- package/src/utils/logger.ts +0 -38
- package/src/utils/migrations.ts +0 -58
- package/src/utils/run-asap.ts +0 -156
|
@@ -0,0 +1,1280 @@
|
|
|
1
|
+
import { addListener, removeListener, removeTrigger, setSharingType, } from "#drive-document-model/gen/creators";
|
|
2
|
+
import { createDocument } from "#drive-document-model/gen/utils";
|
|
3
|
+
import { isActionJob, isOperationJob, } from "#queue/types";
|
|
4
|
+
import { ReadModeServer } from "#read-mode/server";
|
|
5
|
+
import { DefaultDrivesManager, } from "#utils/default-drives-manager";
|
|
6
|
+
import { requestPublicDrive } from "#utils/graphql";
|
|
7
|
+
import { logger } from "#utils/logger";
|
|
8
|
+
import { generateUUID, isDocumentDrive, runAsapAsync } from "#utils/misc";
|
|
9
|
+
import { RunAsap } from "#utils/run-asap";
|
|
10
|
+
import { attachBranch, garbageCollect, garbageCollectDocumentOperations, groupOperationsByScope, merge, precedes, removeExistingOperations, replayDocument, reshuffleByTimestamp, skipHeaderOperations, sortOperations } from "document-model";
|
|
11
|
+
import { ClientError } from "graphql-request";
|
|
12
|
+
import { ConflictOperationError, DriveAlreadyExistsError, OperationError, } from "./error.js";
|
|
13
|
+
import { InternalTransmitter, } from "./listener/transmitter/internal.js";
|
|
14
|
+
import { PullResponderTransmitter, } from "./listener/transmitter/pull-responder.js";
|
|
15
|
+
import { DefaultListenerManagerOptions, } from "./types.js";
|
|
16
|
+
import { filterOperationsByRevision, isAtRevision } from "./utils.js";
|
|
17
|
+
export class BaseDocumentDriveServer {
|
|
18
|
+
// external dependencies
|
|
19
|
+
documentModelModules;
|
|
20
|
+
storage;
|
|
21
|
+
cache;
|
|
22
|
+
queueManager;
|
|
23
|
+
eventEmitter;
|
|
24
|
+
options;
|
|
25
|
+
// waiting to move to external dependencies
|
|
26
|
+
listenerManager;
|
|
27
|
+
transmitterFactory;
|
|
28
|
+
synchronizationManager;
|
|
29
|
+
// internal dependencies
|
|
30
|
+
defaultDrivesManager;
|
|
31
|
+
// internal state
|
|
32
|
+
triggerMap = new Map();
|
|
33
|
+
initializePromise;
|
|
34
|
+
constructor(documentModelModules, storage, cache, queueManager, eventEmitter, synchronizationManager, listenerManager, transmitterFactory, options) {
|
|
35
|
+
this.documentModelModules = documentModelModules;
|
|
36
|
+
this.storage = storage;
|
|
37
|
+
this.cache = cache;
|
|
38
|
+
this.queueManager = queueManager;
|
|
39
|
+
this.eventEmitter = eventEmitter;
|
|
40
|
+
this.synchronizationManager = synchronizationManager;
|
|
41
|
+
this.listenerManager = listenerManager;
|
|
42
|
+
this.transmitterFactory = transmitterFactory;
|
|
43
|
+
this.options = {
|
|
44
|
+
...options,
|
|
45
|
+
defaultDrives: {
|
|
46
|
+
...options?.defaultDrives,
|
|
47
|
+
},
|
|
48
|
+
listenerManager: {
|
|
49
|
+
...DefaultListenerManagerOptions,
|
|
50
|
+
...options?.listenerManager,
|
|
51
|
+
},
|
|
52
|
+
taskQueueMethod: options?.taskQueueMethod === undefined
|
|
53
|
+
? RunAsap.runAsap
|
|
54
|
+
: options.taskQueueMethod,
|
|
55
|
+
};
|
|
56
|
+
this.defaultDrivesManager = new DefaultDrivesManager(this, this.defaultDrivesManagerDelegate, options);
|
|
57
|
+
this.storage.setStorageDelegate?.({
|
|
58
|
+
getCachedOperations: async (drive, id) => {
|
|
59
|
+
try {
|
|
60
|
+
const document = await this.cache.getDocument(drive, id);
|
|
61
|
+
return document?.operations;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
logger.error(error);
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
this.initializePromise = this._initialize();
|
|
70
|
+
}
|
|
71
|
+
initialize() {
|
|
72
|
+
return this.initializePromise;
|
|
73
|
+
}
|
|
74
|
+
async _initialize() {
|
|
75
|
+
await this.listenerManager.initialize(this.handleListenerError);
|
|
76
|
+
await this.queueManager.init(this.queueDelegate, (error) => {
|
|
77
|
+
logger.error(`Error initializing queue manager`, error);
|
|
78
|
+
errors.push(error);
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
await this.defaultDrivesManager.removeOldremoteDrives();
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
logger.error(error);
|
|
85
|
+
}
|
|
86
|
+
const errors = [];
|
|
87
|
+
const drives = await this.getDrives();
|
|
88
|
+
for (const drive of drives) {
|
|
89
|
+
await this._initializeDrive(drive).catch((error) => {
|
|
90
|
+
logger.error(`Error initializing drive ${drive}`, error);
|
|
91
|
+
errors.push(error);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (this.options.defaultDrives.loadOnInit !== false) {
|
|
95
|
+
await this.defaultDrivesManager.initializeDefaultRemoteDrives();
|
|
96
|
+
}
|
|
97
|
+
return errors.length === 0 ? null : errors;
|
|
98
|
+
}
|
|
99
|
+
setDocumentModelModules(modules) {
|
|
100
|
+
this.documentModelModules = [...modules];
|
|
101
|
+
this.eventEmitter.emit("documentModelModules", [...modules]);
|
|
102
|
+
}
|
|
103
|
+
initializeDefaultRemoteDrives() {
|
|
104
|
+
return this.defaultDrivesManager.initializeDefaultRemoteDrives();
|
|
105
|
+
}
|
|
106
|
+
getDefaultRemoteDrives() {
|
|
107
|
+
return this.defaultDrivesManager.getDefaultRemoteDrives();
|
|
108
|
+
}
|
|
109
|
+
setDefaultDriveAccessLevel(url, level) {
|
|
110
|
+
return this.defaultDrivesManager.setDefaultDriveAccessLevel(url, level);
|
|
111
|
+
}
|
|
112
|
+
setAllDefaultDrivesAccessLevel(level) {
|
|
113
|
+
return this.defaultDrivesManager.setAllDefaultDrivesAccessLevel(level);
|
|
114
|
+
}
|
|
115
|
+
getOperationSource(source) {
|
|
116
|
+
return source.type === "local" ? "push" : "pull";
|
|
117
|
+
}
|
|
118
|
+
handleListenerError(error, driveId, listener) {
|
|
119
|
+
logger.error(`Listener ${listener.listener.label ?? listener.listener.listenerId} error:`, error);
|
|
120
|
+
const status = error instanceof OperationError ? error.status : "ERROR";
|
|
121
|
+
this.synchronizationManager.updateSyncStatus(driveId, { push: status }, error);
|
|
122
|
+
}
|
|
123
|
+
shouldSyncRemoteDrive(drive) {
|
|
124
|
+
return (drive.state.local.availableOffline &&
|
|
125
|
+
drive.state.local.triggers.length > 0);
|
|
126
|
+
}
|
|
127
|
+
async startSyncRemoteDrive(driveId) {
|
|
128
|
+
let driveTriggers = this.triggerMap.get(driveId);
|
|
129
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
130
|
+
const drive = await this.getDrive(driveId);
|
|
131
|
+
for (const trigger of drive.state.local.triggers) {
|
|
132
|
+
if (driveTriggers?.get(trigger.id)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!driveTriggers) {
|
|
136
|
+
driveTriggers = new Map();
|
|
137
|
+
}
|
|
138
|
+
this.synchronizationManager.updateSyncStatus(driveId, {
|
|
139
|
+
pull: "SYNCING",
|
|
140
|
+
});
|
|
141
|
+
for (const syncUnit of syncUnits) {
|
|
142
|
+
this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
|
|
143
|
+
pull: "SYNCING",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
|
|
147
|
+
let firstPull = true;
|
|
148
|
+
const cancelPullLoop = PullResponderTransmitter.setupPull(driveId, trigger, this.saveStrand.bind(this), (error) => {
|
|
149
|
+
const statusError = error instanceof OperationError ? error.status : "ERROR";
|
|
150
|
+
this.synchronizationManager.updateSyncStatus(driveId, { pull: statusError }, error);
|
|
151
|
+
if (error instanceof ClientError) {
|
|
152
|
+
this.eventEmitter.emit("clientStrandsError", driveId, trigger, error.response.status, error.message);
|
|
153
|
+
}
|
|
154
|
+
}, (revisions) => {
|
|
155
|
+
const errorRevision = revisions.filter((r) => r.status !== "SUCCESS");
|
|
156
|
+
if (errorRevision.length < 1) {
|
|
157
|
+
this.synchronizationManager.updateSyncStatus(driveId, {
|
|
158
|
+
pull: "SUCCESS",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const documentIdsFromRevision = revisions
|
|
162
|
+
.filter((rev) => rev.documentId !== "")
|
|
163
|
+
.map((rev) => rev.documentId);
|
|
164
|
+
this.getSynchronizationUnitsIds(driveId, documentIdsFromRevision)
|
|
165
|
+
.then((revSyncUnits) => {
|
|
166
|
+
for (const syncUnit of revSyncUnits) {
|
|
167
|
+
const fileErrorRevision = errorRevision.find((r) => r.documentId === syncUnit.documentId);
|
|
168
|
+
if (fileErrorRevision) {
|
|
169
|
+
this.synchronizationManager.updateSyncStatus(syncUnit.syncId, { pull: fileErrorRevision.status }, fileErrorRevision.error);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
|
|
173
|
+
pull: "SUCCESS",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
.catch(console.error);
|
|
179
|
+
// if it is the first pull and returns empty
|
|
180
|
+
// then updates corresponding push transmitter
|
|
181
|
+
if (firstPull) {
|
|
182
|
+
firstPull = false;
|
|
183
|
+
const pushListener = drive.state.local.listeners.find((listener) => trigger.data.url === listener.callInfo?.data);
|
|
184
|
+
if (pushListener) {
|
|
185
|
+
this.getSynchronizationUnitsRevision(driveId, syncUnits)
|
|
186
|
+
.then((syncUnitRevisions) => {
|
|
187
|
+
for (const revision of syncUnitRevisions) {
|
|
188
|
+
this.listenerManager
|
|
189
|
+
.updateListenerRevision(pushListener.listenerId, driveId, revision.syncId, revision.revision)
|
|
190
|
+
.catch(logger.error);
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
.catch(logger.error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
driveTriggers.set(trigger.id, cancelPullLoop);
|
|
198
|
+
this.triggerMap.set(driveId, driveTriggers);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async stopSyncRemoteDrive(driveId) {
|
|
203
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
204
|
+
const filesNodeSyncId = syncUnits
|
|
205
|
+
.filter((syncUnit) => syncUnit.documentId !== "")
|
|
206
|
+
.map((syncUnit) => syncUnit.syncId);
|
|
207
|
+
const triggers = this.triggerMap.get(driveId);
|
|
208
|
+
triggers?.forEach((cancel) => cancel());
|
|
209
|
+
this.synchronizationManager.updateSyncStatus(driveId, null);
|
|
210
|
+
for (const fileNodeSyncId of filesNodeSyncId) {
|
|
211
|
+
this.synchronizationManager.updateSyncStatus(fileNodeSyncId, null);
|
|
212
|
+
}
|
|
213
|
+
return this.triggerMap.delete(driveId);
|
|
214
|
+
}
|
|
215
|
+
defaultDrivesManagerDelegate = {
|
|
216
|
+
detachDrive: this.detachDrive.bind(this),
|
|
217
|
+
emit: (...args) => this.eventEmitter.emit("defaultRemoteDrive", ...args),
|
|
218
|
+
};
|
|
219
|
+
queueDelegate = {
|
|
220
|
+
checkDocumentExists: (driveId, documentId) => this.storage.checkDocumentExists(driveId, documentId),
|
|
221
|
+
processOperationJob: async ({ driveId, documentId, operations, options, }) => {
|
|
222
|
+
return documentId
|
|
223
|
+
? this.addOperations(driveId, documentId, operations, options)
|
|
224
|
+
: this.addDriveOperations(driveId, operations, options);
|
|
225
|
+
},
|
|
226
|
+
processActionJob: async ({ driveId, documentId, actions, options, }) => {
|
|
227
|
+
return documentId
|
|
228
|
+
? this.addActions(driveId, documentId, actions, options)
|
|
229
|
+
: this.addDriveActions(driveId, actions, options);
|
|
230
|
+
},
|
|
231
|
+
processJob: async (job) => {
|
|
232
|
+
if (isOperationJob(job)) {
|
|
233
|
+
return this.queueDelegate.processOperationJob(job);
|
|
234
|
+
}
|
|
235
|
+
else if (isActionJob(job)) {
|
|
236
|
+
return this.queueDelegate.processActionJob(job);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
throw new Error("Unknown job type", job);
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
async _initializeDrive(driveId) {
|
|
244
|
+
const drive = await this.getDrive(driveId);
|
|
245
|
+
await this.synchronizationManager.initializeDriveSyncStatus(driveId, drive);
|
|
246
|
+
if (this.shouldSyncRemoteDrive(drive)) {
|
|
247
|
+
await this.startSyncRemoteDrive(driveId);
|
|
248
|
+
}
|
|
249
|
+
for (const zodListener of drive.state.local.listeners) {
|
|
250
|
+
const transmitter = this.transmitterFactory.instance(zodListener.callInfo?.transmitterType ?? "", {
|
|
251
|
+
driveId,
|
|
252
|
+
listenerId: zodListener.listenerId,
|
|
253
|
+
block: zodListener.block,
|
|
254
|
+
filter: zodListener.filter,
|
|
255
|
+
system: zodListener.system,
|
|
256
|
+
label: zodListener.label ?? "",
|
|
257
|
+
callInfo: zodListener.callInfo ?? undefined,
|
|
258
|
+
}, this);
|
|
259
|
+
await this.listenerManager.setListener(driveId, {
|
|
260
|
+
block: zodListener.block,
|
|
261
|
+
driveId: drive.state.global.id,
|
|
262
|
+
filter: {
|
|
263
|
+
branch: zodListener.filter.branch ?? [],
|
|
264
|
+
documentId: zodListener.filter.documentId ?? [],
|
|
265
|
+
documentType: zodListener.filter.documentType,
|
|
266
|
+
scope: zodListener.filter.scope ?? [],
|
|
267
|
+
},
|
|
268
|
+
listenerId: zodListener.listenerId,
|
|
269
|
+
callInfo: zodListener.callInfo ?? undefined,
|
|
270
|
+
system: zodListener.system,
|
|
271
|
+
label: zodListener.label ?? "",
|
|
272
|
+
transmitter,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Delegate synchronization methods to synchronizationManager
|
|
277
|
+
getSynchronizationUnits(driveId, documentId, scope, branch, documentType) {
|
|
278
|
+
return this.synchronizationManager.getSynchronizationUnits(driveId, documentId, scope, branch, documentType);
|
|
279
|
+
}
|
|
280
|
+
getSynchronizationUnitsIds(driveId, documentId, scope, branch, documentType) {
|
|
281
|
+
return this.synchronizationManager.getSynchronizationUnitsIds(driveId, documentId, scope, branch, documentType);
|
|
282
|
+
}
|
|
283
|
+
getOperationData(driveId, syncId, filter) {
|
|
284
|
+
return this.synchronizationManager.getOperationData(driveId, syncId, filter);
|
|
285
|
+
}
|
|
286
|
+
getSynchronizationUnitsRevision(driveId, syncUnitsQuery) {
|
|
287
|
+
return this.synchronizationManager.getSynchronizationUnitsRevision(driveId, syncUnitsQuery);
|
|
288
|
+
}
|
|
289
|
+
getDocumentModelModule(documentType) {
|
|
290
|
+
const documentModelModule = this.documentModelModules.find((module) => module.documentModel.id === documentType);
|
|
291
|
+
if (!documentModelModule) {
|
|
292
|
+
throw new Error(`Document type ${documentType} not supported`);
|
|
293
|
+
}
|
|
294
|
+
return documentModelModule;
|
|
295
|
+
}
|
|
296
|
+
getDocumentModelModules() {
|
|
297
|
+
return [...this.documentModelModules];
|
|
298
|
+
}
|
|
299
|
+
async addDrive(input, preferredEditor) {
|
|
300
|
+
const id = input.global.id || generateUUID();
|
|
301
|
+
if (!id) {
|
|
302
|
+
throw new Error("Invalid Drive Id");
|
|
303
|
+
}
|
|
304
|
+
const drives = await this.storage.getDrives();
|
|
305
|
+
if (drives.includes(id)) {
|
|
306
|
+
throw new DriveAlreadyExistsError(id);
|
|
307
|
+
}
|
|
308
|
+
const document = createDocument({
|
|
309
|
+
state: input,
|
|
310
|
+
});
|
|
311
|
+
document.meta = {
|
|
312
|
+
preferredEditor: preferredEditor,
|
|
313
|
+
};
|
|
314
|
+
await this.storage.createDrive(id, document);
|
|
315
|
+
if (input.global.slug) {
|
|
316
|
+
await this.cache.deleteDocument("drives-slug", input.global.slug);
|
|
317
|
+
}
|
|
318
|
+
await this._initializeDrive(id);
|
|
319
|
+
this.eventEmitter.emit("driveAdded", document);
|
|
320
|
+
return document;
|
|
321
|
+
}
|
|
322
|
+
async addRemoteDrive(url, options) {
|
|
323
|
+
const { id, name, slug, icon, meta } = options.expectedDriveInfo || (await requestPublicDrive(url));
|
|
324
|
+
const { pullFilter, pullInterval, availableOffline, sharingType, listeners, triggers, } = options;
|
|
325
|
+
const pullTrigger = await PullResponderTransmitter.createPullResponderTrigger(id, url, {
|
|
326
|
+
pullFilter,
|
|
327
|
+
pullInterval,
|
|
328
|
+
});
|
|
329
|
+
return await this.addDrive({
|
|
330
|
+
global: {
|
|
331
|
+
id: id,
|
|
332
|
+
name,
|
|
333
|
+
slug,
|
|
334
|
+
icon: icon ?? null,
|
|
335
|
+
},
|
|
336
|
+
local: {
|
|
337
|
+
triggers: [...triggers, pullTrigger],
|
|
338
|
+
listeners: listeners,
|
|
339
|
+
availableOffline,
|
|
340
|
+
sharingType,
|
|
341
|
+
},
|
|
342
|
+
}, meta?.preferredEditor);
|
|
343
|
+
}
|
|
344
|
+
async registerPullResponderTrigger(driveId, url, options) {
|
|
345
|
+
const pullTrigger = await PullResponderTransmitter.createPullResponderTrigger(driveId, url, options);
|
|
346
|
+
return pullTrigger;
|
|
347
|
+
}
|
|
348
|
+
async deleteDrive(driveId) {
|
|
349
|
+
const result = await Promise.allSettled([
|
|
350
|
+
this.stopSyncRemoteDrive(driveId),
|
|
351
|
+
this.listenerManager.removeDrive(driveId),
|
|
352
|
+
this.cache.deleteDocument("drives", driveId),
|
|
353
|
+
this.storage.deleteDrive(driveId),
|
|
354
|
+
]);
|
|
355
|
+
result.forEach((r) => {
|
|
356
|
+
if (r.status === "rejected") {
|
|
357
|
+
throw r.reason;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
getDrives() {
|
|
362
|
+
return this.storage.getDrives();
|
|
363
|
+
}
|
|
364
|
+
async getDrive(driveId, options) {
|
|
365
|
+
let document;
|
|
366
|
+
try {
|
|
367
|
+
const cachedDocument = await this.cache.getDocument("drives", driveId); // TODO support GetDocumentOptions
|
|
368
|
+
if (cachedDocument && isDocumentDrive(cachedDocument)) {
|
|
369
|
+
document = cachedDocument;
|
|
370
|
+
if (isAtRevision(document, options?.revisions)) {
|
|
371
|
+
return document;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch (e) {
|
|
376
|
+
logger.error("Error getting drive from cache", e);
|
|
377
|
+
}
|
|
378
|
+
const driveStorage = document ?? (await this.storage.getDrive(driveId));
|
|
379
|
+
const result = this._buildDocument(driveStorage, options);
|
|
380
|
+
if (!isDocumentDrive(result)) {
|
|
381
|
+
throw new Error(`Document with id ${driveId} is not a Document Drive`);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
if (!options?.revisions) {
|
|
385
|
+
this.cache.setDocument("drives", driveId, result).catch(logger.error);
|
|
386
|
+
}
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async getDriveBySlug(slug, options) {
|
|
391
|
+
try {
|
|
392
|
+
const document = await this.cache.getDocument("drives-slug", slug);
|
|
393
|
+
if (document && isDocumentDrive(document)) {
|
|
394
|
+
return document;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (e) {
|
|
398
|
+
logger.error("Error getting drive from cache", e);
|
|
399
|
+
}
|
|
400
|
+
const driveStorage = await this.storage.getDriveBySlug(slug);
|
|
401
|
+
const document = this._buildDocument(driveStorage, options);
|
|
402
|
+
if (!isDocumentDrive(document)) {
|
|
403
|
+
throw new Error(`Document with slug ${slug} is not a Document Drive`);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
this.cache.setDocument("drives-slug", slug, document).catch(logger.error);
|
|
407
|
+
return document;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async getDocument(driveId, documentId, options) {
|
|
411
|
+
let cachedDocument;
|
|
412
|
+
try {
|
|
413
|
+
cachedDocument = await this.cache.getDocument(driveId, documentId); // TODO support GetDocumentOptions
|
|
414
|
+
if (cachedDocument && isAtRevision(cachedDocument, options?.revisions)) {
|
|
415
|
+
return cachedDocument;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (e) {
|
|
419
|
+
logger.error("Error getting document from cache", e);
|
|
420
|
+
}
|
|
421
|
+
const documentStorage = cachedDocument ??
|
|
422
|
+
(await this.storage.getDocument(driveId, documentId));
|
|
423
|
+
const document = this._buildDocument(documentStorage, options);
|
|
424
|
+
if (!options?.revisions) {
|
|
425
|
+
this.cache.setDocument(driveId, documentId, document).catch(logger.error);
|
|
426
|
+
}
|
|
427
|
+
return document;
|
|
428
|
+
}
|
|
429
|
+
getDocuments(driveId) {
|
|
430
|
+
return this.storage.getDocuments(driveId);
|
|
431
|
+
}
|
|
432
|
+
async createDocument(driveId, input) {
|
|
433
|
+
// if a document was provided then checks if it's valid
|
|
434
|
+
let state = undefined;
|
|
435
|
+
if (input.document) {
|
|
436
|
+
if (input.documentType !== input.document.documentType) {
|
|
437
|
+
throw new Error(`Provided document is not ${input.documentType}`);
|
|
438
|
+
}
|
|
439
|
+
const doc = this._buildDocument(input.document);
|
|
440
|
+
state = doc.state;
|
|
441
|
+
}
|
|
442
|
+
// if no document was provided then create a new one
|
|
443
|
+
const document = input.document ??
|
|
444
|
+
this.getDocumentModelModule(input.documentType).utils.createDocument();
|
|
445
|
+
// stores document information
|
|
446
|
+
const documentStorage = {
|
|
447
|
+
name: document.name,
|
|
448
|
+
revision: document.revision,
|
|
449
|
+
documentType: document.documentType,
|
|
450
|
+
created: document.created,
|
|
451
|
+
lastModified: document.lastModified,
|
|
452
|
+
operations: { global: [], local: [] },
|
|
453
|
+
initialState: document.initialState,
|
|
454
|
+
clipboard: [],
|
|
455
|
+
state: state ?? document.state,
|
|
456
|
+
};
|
|
457
|
+
await this.storage.createDocument(driveId, input.id, documentStorage);
|
|
458
|
+
// set initial state for new syncUnits
|
|
459
|
+
for (const syncUnit of input.synchronizationUnits) {
|
|
460
|
+
this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
|
|
461
|
+
pull: this.triggerMap.get(driveId) ? "INITIAL_SYNC" : undefined,
|
|
462
|
+
push: this.listenerManager.driveHasListeners(driveId)
|
|
463
|
+
? "SUCCESS"
|
|
464
|
+
: undefined,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
// if the document contains operations then
|
|
468
|
+
// stores the operations in the storage
|
|
469
|
+
const operations = Object.values(document.operations).flat();
|
|
470
|
+
if (operations.length) {
|
|
471
|
+
if (isDocumentDrive(document)) {
|
|
472
|
+
await this.storage.addDriveOperations(driveId, operations, document);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
await this.storage.addDocumentOperations(driveId, input.id, operations, document);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return document;
|
|
479
|
+
}
|
|
480
|
+
async deleteDocument(driveId, documentId) {
|
|
481
|
+
try {
|
|
482
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId, [
|
|
483
|
+
documentId,
|
|
484
|
+
]);
|
|
485
|
+
// remove document sync units status when a document is deleted
|
|
486
|
+
for (const syncUnit of syncUnits) {
|
|
487
|
+
this.synchronizationManager.updateSyncStatus(syncUnit.syncId, null);
|
|
488
|
+
}
|
|
489
|
+
await this.listenerManager.removeSyncUnits(driveId, syncUnits);
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
logger.warn("Error deleting document", error);
|
|
493
|
+
}
|
|
494
|
+
await this.cache.deleteDocument(driveId, documentId);
|
|
495
|
+
return this.storage.deleteDocument(driveId, documentId);
|
|
496
|
+
}
|
|
497
|
+
async _processOperations(driveId, documentId, documentStorage, operations) {
|
|
498
|
+
const operationsApplied = [];
|
|
499
|
+
const signals = [];
|
|
500
|
+
const documentStorageWithState = await this._addDocumentResultingStage(documentStorage, driveId, documentId);
|
|
501
|
+
let document = this._buildDocument(documentStorageWithState);
|
|
502
|
+
let error; // TODO: replace with an array of errors/consistency issues
|
|
503
|
+
const operationsByScope = groupOperationsByScope(operations);
|
|
504
|
+
for (const scope of Object.keys(operationsByScope)) {
|
|
505
|
+
const storageDocumentOperations = documentStorage.operations[scope];
|
|
506
|
+
// TODO two equal operations done by two clients will be considered the same, ie: { type: "INCREMENT" }
|
|
507
|
+
const branch = removeExistingOperations(operationsByScope[scope] || [], storageDocumentOperations);
|
|
508
|
+
// No operations to apply
|
|
509
|
+
if (branch.length < 1) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const trunk = garbageCollect(sortOperations(storageDocumentOperations));
|
|
513
|
+
const [invertedTrunk, tail] = attachBranch(trunk, branch);
|
|
514
|
+
const newHistory = tail.length < 1
|
|
515
|
+
? invertedTrunk
|
|
516
|
+
: merge(trunk, invertedTrunk, reshuffleByTimestamp);
|
|
517
|
+
const newOperations = newHistory.filter((op) => trunk.length < 1 || precedes(trunk[trunk.length - 1], op));
|
|
518
|
+
for (const nextOperation of newOperations) {
|
|
519
|
+
let skipHashValidation = false;
|
|
520
|
+
// when dealing with a merge (tail.length > 0) we have to skip hash validation
|
|
521
|
+
// for the operations that were re-indexed (previous hash becomes invalid due the new position in the history)
|
|
522
|
+
if (tail.length > 0) {
|
|
523
|
+
const sourceOperation = operations.find((op) => op.hash === nextOperation.hash);
|
|
524
|
+
skipHashValidation =
|
|
525
|
+
!sourceOperation ||
|
|
526
|
+
sourceOperation.index !== nextOperation.index ||
|
|
527
|
+
sourceOperation.skip !== nextOperation.skip;
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
// runs operation on next available tick, to avoid blocking the main thread
|
|
531
|
+
const taskQueueMethod = this.options.taskQueueMethod;
|
|
532
|
+
const task = () => this._performOperation(driveId, documentId, document, nextOperation, skipHashValidation);
|
|
533
|
+
const appliedResult = await (taskQueueMethod
|
|
534
|
+
? runAsapAsync(task, taskQueueMethod)
|
|
535
|
+
: task());
|
|
536
|
+
document = appliedResult.document;
|
|
537
|
+
signals.push(...appliedResult.signals);
|
|
538
|
+
operationsApplied.push(appliedResult.operation);
|
|
539
|
+
// TODO what to do if one of the applied operations has an error?
|
|
540
|
+
}
|
|
541
|
+
catch (e) {
|
|
542
|
+
error =
|
|
543
|
+
e instanceof OperationError
|
|
544
|
+
? e
|
|
545
|
+
: new OperationError("ERROR", nextOperation, e.message, e.cause);
|
|
546
|
+
// TODO: don't break on errors...
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
document,
|
|
553
|
+
operationsApplied,
|
|
554
|
+
signals,
|
|
555
|
+
error,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
async _addDocumentResultingStage(document, driveId, documentId, options) {
|
|
559
|
+
// apply skip header operations to all scopes
|
|
560
|
+
const operations = options?.revisions !== undefined
|
|
561
|
+
? filterOperationsByRevision(document.operations, options.revisions)
|
|
562
|
+
: document.operations;
|
|
563
|
+
const documentOperations = garbageCollectDocumentOperations(operations);
|
|
564
|
+
for (const scope of Object.keys(documentOperations)) {
|
|
565
|
+
const lastRemainingOperation = documentOperations[scope].at(-1);
|
|
566
|
+
// if the latest operation doesn't have a resulting state then tries
|
|
567
|
+
// to retrieve it from the db to avoid rerunning all the operations
|
|
568
|
+
if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
|
|
569
|
+
lastRemainingOperation.resultingState = await (documentId
|
|
570
|
+
? this.storage.getOperationResultingState?.(driveId, documentId, lastRemainingOperation.index, lastRemainingOperation.scope, "main")
|
|
571
|
+
: this.storage.getDriveOperationResultingState?.(driveId, lastRemainingOperation.index, lastRemainingOperation.scope, "main"));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
...document,
|
|
576
|
+
operations: documentOperations,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
_buildDocument(documentStorage, options) {
|
|
580
|
+
if (documentStorage.state &&
|
|
581
|
+
(!options || options.checkHashes === false) &&
|
|
582
|
+
isAtRevision(documentStorage, options?.revisions)) {
|
|
583
|
+
return documentStorage;
|
|
584
|
+
}
|
|
585
|
+
const documentModelModule = this.getDocumentModelModule(documentStorage.documentType);
|
|
586
|
+
const revisionOperations = options?.revisions !== undefined
|
|
587
|
+
? filterOperationsByRevision(documentStorage.operations, options.revisions)
|
|
588
|
+
: documentStorage.operations;
|
|
589
|
+
const operations = garbageCollectDocumentOperations(revisionOperations);
|
|
590
|
+
return replayDocument(documentStorage.initialState, operations, documentModelModule.reducer, undefined, documentStorage, undefined, {
|
|
591
|
+
...options,
|
|
592
|
+
checkHashes: options?.checkHashes ?? true,
|
|
593
|
+
reuseOperationResultingState: options?.checkHashes ?? true,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
async _performOperation(driveId, documentId, document, operation, skipHashValidation = false) {
|
|
597
|
+
const documentModelModule = this.getDocumentModelModule(document.documentType);
|
|
598
|
+
const signalResults = [];
|
|
599
|
+
let newDocument = document;
|
|
600
|
+
const scope = operation.scope;
|
|
601
|
+
const documentOperations = garbageCollectDocumentOperations({
|
|
602
|
+
...document.operations,
|
|
603
|
+
[scope]: skipHeaderOperations(document.operations[scope], operation),
|
|
604
|
+
});
|
|
605
|
+
const lastRemainingOperation = documentOperations[scope].at(-1);
|
|
606
|
+
// if the latest operation doesn't have a resulting state then tries
|
|
607
|
+
// to retrieve it from the db to avoid rerunning all the operations
|
|
608
|
+
if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
|
|
609
|
+
lastRemainingOperation.resultingState = await (documentId
|
|
610
|
+
? this.storage.getOperationResultingState?.(driveId, documentId, lastRemainingOperation.index, lastRemainingOperation.scope, "main")
|
|
611
|
+
: this.storage.getDriveOperationResultingState?.(driveId, lastRemainingOperation.index, lastRemainingOperation.scope, "main"));
|
|
612
|
+
}
|
|
613
|
+
const operationSignals = [];
|
|
614
|
+
newDocument = documentModelModule.reducer(newDocument, operation, (signal) => {
|
|
615
|
+
let handler = undefined;
|
|
616
|
+
switch (signal.type) {
|
|
617
|
+
case "CREATE_CHILD_DOCUMENT":
|
|
618
|
+
handler = () => this.createDocument(driveId, signal.input);
|
|
619
|
+
break;
|
|
620
|
+
case "DELETE_CHILD_DOCUMENT":
|
|
621
|
+
handler = () => this.deleteDocument(driveId, signal.input.id);
|
|
622
|
+
break;
|
|
623
|
+
case "COPY_CHILD_DOCUMENT":
|
|
624
|
+
handler = () => this.getDocument(driveId, signal.input.id).then((documentToCopy) => this.createDocument(driveId, {
|
|
625
|
+
id: signal.input.newId,
|
|
626
|
+
documentType: documentToCopy.documentType,
|
|
627
|
+
document: documentToCopy,
|
|
628
|
+
synchronizationUnits: signal.input.synchronizationUnits,
|
|
629
|
+
}));
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
if (handler) {
|
|
633
|
+
operationSignals.push(() => handler().then((result) => ({ signal, result })));
|
|
634
|
+
}
|
|
635
|
+
}, { skip: operation.skip, reuseOperationResultingState: true });
|
|
636
|
+
const appliedOperations = newDocument.operations[operation.scope].filter((op) => op.index == operation.index && op.skip == operation.skip);
|
|
637
|
+
const appliedOperation = appliedOperations.at(0);
|
|
638
|
+
if (!appliedOperation) {
|
|
639
|
+
throw new OperationError("ERROR", operation, `Operation with index ${operation.index}:${operation.skip} was not applied.`);
|
|
640
|
+
}
|
|
641
|
+
if (!appliedOperation.error &&
|
|
642
|
+
appliedOperation.hash !== operation.hash &&
|
|
643
|
+
!skipHashValidation) {
|
|
644
|
+
throw new ConflictOperationError(operation, appliedOperation);
|
|
645
|
+
}
|
|
646
|
+
for (const signalHandler of operationSignals) {
|
|
647
|
+
const result = await signalHandler();
|
|
648
|
+
signalResults.push(result);
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
document: newDocument,
|
|
652
|
+
signals: signalResults,
|
|
653
|
+
operation: appliedOperation,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
addOperation(driveId, documentId, operation, options) {
|
|
657
|
+
return this.addOperations(driveId, documentId, [operation], options);
|
|
658
|
+
}
|
|
659
|
+
async _addOperations(driveId, documentId, callback) {
|
|
660
|
+
if (!this.storage.addDocumentOperationsWithTransaction) {
|
|
661
|
+
const documentStorage = await this.storage.getDocument(driveId, documentId);
|
|
662
|
+
const result = await callback(documentStorage);
|
|
663
|
+
// saves the applied operations to storage
|
|
664
|
+
if (result.operations.length > 0) {
|
|
665
|
+
await this.storage.addDocumentOperations(driveId, documentId, result.operations, result.header);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
await this.storage.addDocumentOperationsWithTransaction(driveId, documentId, callback);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
queueOperation(driveId, documentId, operation, options) {
|
|
673
|
+
return this.queueOperations(driveId, documentId, [operation], options);
|
|
674
|
+
}
|
|
675
|
+
async resultIfExistingOperations(drive, id, operations) {
|
|
676
|
+
try {
|
|
677
|
+
const document = await this.getDocument(drive, id);
|
|
678
|
+
const newOperation = operations.find((op) => !op.id ||
|
|
679
|
+
!document.operations[op.scope].find((existingOp) => existingOp.id === op.id &&
|
|
680
|
+
existingOp.index === op.index &&
|
|
681
|
+
existingOp.type === op.type &&
|
|
682
|
+
existingOp.hash === op.hash));
|
|
683
|
+
if (!newOperation) {
|
|
684
|
+
return {
|
|
685
|
+
status: "SUCCESS",
|
|
686
|
+
document,
|
|
687
|
+
operations,
|
|
688
|
+
signals: [],
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
if (!error.message.includes(`Document with id ${id} not found`)) {
|
|
697
|
+
console.error(error);
|
|
698
|
+
}
|
|
699
|
+
return undefined;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
async queueOperations(driveId, documentId, operations, options) {
|
|
703
|
+
// if operations are already stored then returns cached document
|
|
704
|
+
const result = await this.resultIfExistingOperations(driveId, documentId, operations);
|
|
705
|
+
if (result) {
|
|
706
|
+
return result;
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
const jobId = await this.queueManager.addJob({
|
|
710
|
+
driveId: driveId,
|
|
711
|
+
documentId: documentId,
|
|
712
|
+
operations,
|
|
713
|
+
options,
|
|
714
|
+
});
|
|
715
|
+
return new Promise((resolve, reject) => {
|
|
716
|
+
const unsubscribe = this.queueManager.on("jobCompleted", (job, result) => {
|
|
717
|
+
if (job.jobId === jobId) {
|
|
718
|
+
unsubscribe();
|
|
719
|
+
unsubscribeError();
|
|
720
|
+
resolve(result);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
const unsubscribeError = this.queueManager.on("jobFailed", (job, error) => {
|
|
724
|
+
if (job.jobId === jobId) {
|
|
725
|
+
unsubscribe();
|
|
726
|
+
unsubscribeError();
|
|
727
|
+
reject(error);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
logger.error("Error adding job", error);
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
async queueAction(driveId, documentId, action, options) {
|
|
738
|
+
return this.queueActions(driveId, documentId, [action], options);
|
|
739
|
+
}
|
|
740
|
+
async queueActions(driveId, documentId, actions, options) {
|
|
741
|
+
try {
|
|
742
|
+
const jobId = await this.queueManager.addJob({
|
|
743
|
+
driveId: driveId,
|
|
744
|
+
documentId: documentId,
|
|
745
|
+
actions,
|
|
746
|
+
options,
|
|
747
|
+
});
|
|
748
|
+
return new Promise((resolve, reject) => {
|
|
749
|
+
const unsubscribe = this.queueManager.on("jobCompleted", (job, result) => {
|
|
750
|
+
if (job.jobId === jobId) {
|
|
751
|
+
unsubscribe();
|
|
752
|
+
unsubscribeError();
|
|
753
|
+
resolve(result);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
const unsubscribeError = this.queueManager.on("jobFailed", (job, error) => {
|
|
757
|
+
if (job.jobId === jobId) {
|
|
758
|
+
unsubscribe();
|
|
759
|
+
unsubscribeError();
|
|
760
|
+
reject(error);
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
logger.error("Error adding job", error);
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
async queueDriveAction(driveId, action, options) {
|
|
771
|
+
return this.queueDriveActions(driveId, [action], options);
|
|
772
|
+
}
|
|
773
|
+
async queueDriveActions(driveId, actions, options) {
|
|
774
|
+
try {
|
|
775
|
+
const jobId = await this.queueManager.addJob({
|
|
776
|
+
driveId: driveId,
|
|
777
|
+
actions,
|
|
778
|
+
options,
|
|
779
|
+
});
|
|
780
|
+
return new Promise((resolve, reject) => {
|
|
781
|
+
const unsubscribe = this.queueManager.on("jobCompleted", (job, result) => {
|
|
782
|
+
if (job.jobId === jobId) {
|
|
783
|
+
unsubscribe();
|
|
784
|
+
unsubscribeError();
|
|
785
|
+
resolve(result);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
const unsubscribeError = this.queueManager.on("jobFailed", (job, error) => {
|
|
789
|
+
if (job.jobId === jobId) {
|
|
790
|
+
unsubscribe();
|
|
791
|
+
unsubscribeError();
|
|
792
|
+
reject(error);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
logger.error("Error adding drive job", error);
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async addOperations(driveId, documentId, operations, options) {
|
|
803
|
+
// if operations are already stored then returns the result
|
|
804
|
+
const result = await this.resultIfExistingOperations(driveId, documentId, operations);
|
|
805
|
+
if (result) {
|
|
806
|
+
return result;
|
|
807
|
+
}
|
|
808
|
+
let document;
|
|
809
|
+
const operationsApplied = [];
|
|
810
|
+
const signals = [];
|
|
811
|
+
let error;
|
|
812
|
+
try {
|
|
813
|
+
await this._addOperations(driveId, documentId, async (documentStorage) => {
|
|
814
|
+
const result = await this._processOperations(driveId, documentId, documentStorage, operations);
|
|
815
|
+
if (!result.document) {
|
|
816
|
+
logger.error("Invalid document");
|
|
817
|
+
throw result.error ?? new Error("Invalid document");
|
|
818
|
+
}
|
|
819
|
+
document = result.document;
|
|
820
|
+
error = result.error;
|
|
821
|
+
signals.push(...result.signals);
|
|
822
|
+
operationsApplied.push(...result.operationsApplied);
|
|
823
|
+
return {
|
|
824
|
+
operations: result.operationsApplied,
|
|
825
|
+
header: result.document,
|
|
826
|
+
newState: document.state,
|
|
827
|
+
};
|
|
828
|
+
});
|
|
829
|
+
if (document) {
|
|
830
|
+
this.cache
|
|
831
|
+
.setDocument(driveId, documentId, document)
|
|
832
|
+
.catch(logger.error);
|
|
833
|
+
}
|
|
834
|
+
// gets all the different scopes and branches combinations from the operations
|
|
835
|
+
const { scopes, branches } = operationsApplied.reduce((acc, operation) => {
|
|
836
|
+
if (!acc.scopes.includes(operation.scope)) {
|
|
837
|
+
acc.scopes.push(operation.scope);
|
|
838
|
+
}
|
|
839
|
+
return acc;
|
|
840
|
+
}, { scopes: [], branches: ["main"] });
|
|
841
|
+
const syncUnits = await this.getSynchronizationUnits(driveId, [documentId], scopes, branches);
|
|
842
|
+
// checks if any of the provided operations where reshufled
|
|
843
|
+
const newOp = operationsApplied.find((appliedOp) => !operations.find((o) => o.id === appliedOp.id &&
|
|
844
|
+
o.index === appliedOp.index &&
|
|
845
|
+
o.skip === appliedOp.skip &&
|
|
846
|
+
o.hash === appliedOp.hash));
|
|
847
|
+
// if there are no new operations then reuses the provided source
|
|
848
|
+
// otherwise sets it to local so listeners know that there were
|
|
849
|
+
// new changes originating from this document drive server
|
|
850
|
+
const source = newOp
|
|
851
|
+
? { type: "local" }
|
|
852
|
+
: (options?.source ?? { type: "local" });
|
|
853
|
+
// update listener cache
|
|
854
|
+
const operationSource = this.getOperationSource(source);
|
|
855
|
+
this.listenerManager
|
|
856
|
+
.updateSynchronizationRevisions(driveId, syncUnits, source, () => {
|
|
857
|
+
this.synchronizationManager.updateSyncStatus(driveId, {
|
|
858
|
+
[operationSource]: "SYNCING",
|
|
859
|
+
});
|
|
860
|
+
for (const syncUnit of syncUnits) {
|
|
861
|
+
this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
|
|
862
|
+
[operationSource]: "SYNCING",
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}, this.handleListenerError.bind(this), options?.forceSync ?? source.type === "local")
|
|
866
|
+
.then((updates) => {
|
|
867
|
+
if (updates.length) {
|
|
868
|
+
this.synchronizationManager.updateSyncStatus(driveId, {
|
|
869
|
+
[operationSource]: "SUCCESS",
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
for (const syncUnit of syncUnits) {
|
|
873
|
+
this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
|
|
874
|
+
[operationSource]: "SUCCESS",
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
})
|
|
878
|
+
.catch((error) => {
|
|
879
|
+
logger.error("Non handled error updating sync revision", error);
|
|
880
|
+
this.synchronizationManager.updateSyncStatus(driveId, {
|
|
881
|
+
[operationSource]: "ERROR",
|
|
882
|
+
}, error);
|
|
883
|
+
for (const syncUnit of syncUnits) {
|
|
884
|
+
this.synchronizationManager.updateSyncStatus(syncUnit.syncId, {
|
|
885
|
+
[operationSource]: "ERROR",
|
|
886
|
+
}, error);
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
// after applying all the valid operations,throws
|
|
890
|
+
// an error if there was an invalid operation
|
|
891
|
+
if (error) {
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
status: "SUCCESS",
|
|
896
|
+
document,
|
|
897
|
+
operations: operationsApplied,
|
|
898
|
+
signals,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
const operationError = error instanceof OperationError
|
|
903
|
+
? error
|
|
904
|
+
: new OperationError("ERROR", undefined, error.message, error.cause);
|
|
905
|
+
return {
|
|
906
|
+
status: operationError.status,
|
|
907
|
+
error: operationError,
|
|
908
|
+
document,
|
|
909
|
+
operations: operationsApplied,
|
|
910
|
+
signals,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
addDriveOperation(driveId, operation, options) {
|
|
915
|
+
return this.addDriveOperations(driveId, [operation], options);
|
|
916
|
+
}
|
|
917
|
+
async clearStorage() {
|
|
918
|
+
for (const drive of await this.getDrives()) {
|
|
919
|
+
await this.deleteDrive(drive);
|
|
920
|
+
}
|
|
921
|
+
await this.storage.clearStorage?.();
|
|
922
|
+
}
|
|
923
|
+
async _addDriveOperations(driveId, callback) {
|
|
924
|
+
if (!this.storage.addDriveOperationsWithTransaction) {
|
|
925
|
+
const documentStorage = await this.storage.getDrive(driveId);
|
|
926
|
+
const result = await callback(documentStorage);
|
|
927
|
+
// saves the applied operations to storage
|
|
928
|
+
if (result.operations.length > 0) {
|
|
929
|
+
await this.storage.addDriveOperations(driveId, result.operations, result.header);
|
|
930
|
+
}
|
|
931
|
+
return result;
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
return this.storage.addDriveOperationsWithTransaction(driveId, callback);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
queueDriveOperation(driveId, operation, options) {
|
|
938
|
+
return this.queueDriveOperations(driveId, [operation], options);
|
|
939
|
+
}
|
|
940
|
+
async resultIfExistingDriveOperations(driveId, operations) {
|
|
941
|
+
try {
|
|
942
|
+
const drive = await this.getDrive(driveId);
|
|
943
|
+
const newOperation = operations.find((op) => !op.id ||
|
|
944
|
+
!drive.operations[op.scope].find((existingOp) => existingOp.id === op.id &&
|
|
945
|
+
existingOp.index === op.index &&
|
|
946
|
+
existingOp.type === op.type &&
|
|
947
|
+
existingOp.hash === op.hash));
|
|
948
|
+
if (!newOperation) {
|
|
949
|
+
return {
|
|
950
|
+
status: "SUCCESS",
|
|
951
|
+
document: drive,
|
|
952
|
+
operations: operations,
|
|
953
|
+
signals: [],
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
return undefined;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
catch (error) {
|
|
961
|
+
console.error(error); // TODO error
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
async queueDriveOperations(driveId, operations, options) {
|
|
966
|
+
// if operations are already stored then returns cached document
|
|
967
|
+
const result = await this.resultIfExistingDriveOperations(driveId, operations);
|
|
968
|
+
if (result) {
|
|
969
|
+
return result;
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
const jobId = await this.queueManager.addJob({
|
|
973
|
+
driveId: driveId,
|
|
974
|
+
operations,
|
|
975
|
+
options,
|
|
976
|
+
});
|
|
977
|
+
return new Promise((resolve, reject) => {
|
|
978
|
+
const unsubscribe = this.queueManager.on("jobCompleted", (job, result) => {
|
|
979
|
+
if (job.jobId === jobId) {
|
|
980
|
+
unsubscribe();
|
|
981
|
+
unsubscribeError();
|
|
982
|
+
resolve(result);
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
const unsubscribeError = this.queueManager.on("jobFailed", (job, error) => {
|
|
986
|
+
if (job.jobId === jobId) {
|
|
987
|
+
unsubscribe();
|
|
988
|
+
unsubscribeError();
|
|
989
|
+
reject(error);
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
logger.error("Error adding drive job", error);
|
|
996
|
+
throw error;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async addDriveOperations(driveId, operations, options) {
|
|
1000
|
+
let document;
|
|
1001
|
+
const operationsApplied = [];
|
|
1002
|
+
const signals = [];
|
|
1003
|
+
let error;
|
|
1004
|
+
// if operations are already stored then returns cached drive
|
|
1005
|
+
const result = await this.resultIfExistingDriveOperations(driveId, operations);
|
|
1006
|
+
if (result) {
|
|
1007
|
+
return result;
|
|
1008
|
+
}
|
|
1009
|
+
try {
|
|
1010
|
+
await this._addDriveOperations(driveId, async (documentStorage) => {
|
|
1011
|
+
const result = await this._processOperations(driveId, undefined, documentStorage, operations.slice());
|
|
1012
|
+
document = result.document;
|
|
1013
|
+
operationsApplied.push(...result.operationsApplied);
|
|
1014
|
+
signals.push(...result.signals);
|
|
1015
|
+
error = result.error;
|
|
1016
|
+
return {
|
|
1017
|
+
operations: result.operationsApplied,
|
|
1018
|
+
header: result.document,
|
|
1019
|
+
};
|
|
1020
|
+
});
|
|
1021
|
+
if (!document || !isDocumentDrive(document)) {
|
|
1022
|
+
throw error ?? new Error("Invalid Document Drive document");
|
|
1023
|
+
}
|
|
1024
|
+
this.cache.setDocument("drives", driveId, document).catch(logger.error);
|
|
1025
|
+
for (const operation of operationsApplied) {
|
|
1026
|
+
switch (operation.type) {
|
|
1027
|
+
case "ADD_LISTENER": {
|
|
1028
|
+
const zodListener = operation.input.listener;
|
|
1029
|
+
// create the transmitter
|
|
1030
|
+
const transmitter = this.transmitterFactory.instance(zodListener.callInfo?.transmitterType ?? "", {
|
|
1031
|
+
driveId,
|
|
1032
|
+
listenerId: zodListener.listenerId,
|
|
1033
|
+
block: zodListener.block,
|
|
1034
|
+
filter: zodListener.filter,
|
|
1035
|
+
system: zodListener.system,
|
|
1036
|
+
label: zodListener.label ?? "",
|
|
1037
|
+
callInfo: zodListener.callInfo ?? undefined,
|
|
1038
|
+
}, this);
|
|
1039
|
+
// create the listener
|
|
1040
|
+
const listener = {
|
|
1041
|
+
...zodListener,
|
|
1042
|
+
driveId: driveId,
|
|
1043
|
+
label: zodListener.label ?? "",
|
|
1044
|
+
system: zodListener.system ?? false,
|
|
1045
|
+
filter: {
|
|
1046
|
+
branch: zodListener.filter.branch ?? [],
|
|
1047
|
+
documentId: zodListener.filter.documentId ?? [],
|
|
1048
|
+
documentType: zodListener.filter.documentType ?? [],
|
|
1049
|
+
scope: zodListener.filter.scope ?? [],
|
|
1050
|
+
},
|
|
1051
|
+
callInfo: {
|
|
1052
|
+
data: zodListener.callInfo?.data ?? "",
|
|
1053
|
+
name: zodListener.callInfo?.name ?? "PullResponder",
|
|
1054
|
+
transmitterType: zodListener.callInfo?.transmitterType ?? "PullResponder",
|
|
1055
|
+
},
|
|
1056
|
+
transmitter,
|
|
1057
|
+
};
|
|
1058
|
+
await this.addListener(driveId, listener);
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
case "REMOVE_LISTENER": {
|
|
1062
|
+
await this.removeListener(driveId, operation);
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
// update listener cache
|
|
1068
|
+
const lastOperation = operationsApplied
|
|
1069
|
+
.filter((op) => op.scope === "global")
|
|
1070
|
+
.slice()
|
|
1071
|
+
.pop();
|
|
1072
|
+
if (lastOperation) {
|
|
1073
|
+
// checks if any of the provided operations where reshufled
|
|
1074
|
+
const newOp = operationsApplied.find((appliedOp) => !operations.find((o) => o.id === appliedOp.id &&
|
|
1075
|
+
o.index === appliedOp.index &&
|
|
1076
|
+
o.skip === appliedOp.skip &&
|
|
1077
|
+
o.hash === appliedOp.hash));
|
|
1078
|
+
// if there are no new operations then reuses the provided source
|
|
1079
|
+
// otherwise sets it to local so listeners know that there were
|
|
1080
|
+
// new changes originating from this document drive server
|
|
1081
|
+
const source = newOp
|
|
1082
|
+
? { type: "local" }
|
|
1083
|
+
: (options?.source ?? { type: "local" });
|
|
1084
|
+
const operationSource = this.getOperationSource(source);
|
|
1085
|
+
this.listenerManager
|
|
1086
|
+
.updateSynchronizationRevisions(driveId, [
|
|
1087
|
+
{
|
|
1088
|
+
syncId: "0",
|
|
1089
|
+
driveId: driveId,
|
|
1090
|
+
documentId: "",
|
|
1091
|
+
scope: "global",
|
|
1092
|
+
branch: "main",
|
|
1093
|
+
documentType: "powerhouse/document-drive",
|
|
1094
|
+
lastUpdated: lastOperation.timestamp,
|
|
1095
|
+
revision: lastOperation.index,
|
|
1096
|
+
},
|
|
1097
|
+
], source, () => {
|
|
1098
|
+
this.synchronizationManager.updateSyncStatus(driveId, {
|
|
1099
|
+
[operationSource]: "SYNCING",
|
|
1100
|
+
});
|
|
1101
|
+
}, this.handleListenerError.bind(this), options?.forceSync ?? source.type === "local")
|
|
1102
|
+
.then((updates) => {
|
|
1103
|
+
if (updates.length) {
|
|
1104
|
+
this.synchronizationManager.updateSyncStatus(driveId, {
|
|
1105
|
+
[operationSource]: "SUCCESS",
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
})
|
|
1109
|
+
.catch((error) => {
|
|
1110
|
+
logger.error("Non handled error updating sync revision", error);
|
|
1111
|
+
this.synchronizationManager.updateSyncStatus(driveId, {
|
|
1112
|
+
[operationSource]: "ERROR",
|
|
1113
|
+
}, error);
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
if (this.shouldSyncRemoteDrive(document)) {
|
|
1117
|
+
this.startSyncRemoteDrive(driveId);
|
|
1118
|
+
}
|
|
1119
|
+
else {
|
|
1120
|
+
this.stopSyncRemoteDrive(driveId);
|
|
1121
|
+
}
|
|
1122
|
+
// after applying all the valid operations,throws
|
|
1123
|
+
// an error if there was an invalid operation
|
|
1124
|
+
if (error) {
|
|
1125
|
+
throw error;
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
status: "SUCCESS",
|
|
1129
|
+
document,
|
|
1130
|
+
operations: operationsApplied,
|
|
1131
|
+
signals,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
catch (error) {
|
|
1135
|
+
const operationError = error instanceof OperationError
|
|
1136
|
+
? error
|
|
1137
|
+
: new OperationError("ERROR", undefined, error.message, error.cause);
|
|
1138
|
+
return {
|
|
1139
|
+
status: operationError.status,
|
|
1140
|
+
error: operationError,
|
|
1141
|
+
document,
|
|
1142
|
+
operations: operationsApplied,
|
|
1143
|
+
signals,
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
_buildOperations(documentId, actions) {
|
|
1148
|
+
const operations = [];
|
|
1149
|
+
const { reducer } = this.getDocumentModelModule(documentId.documentType);
|
|
1150
|
+
for (const action of actions) {
|
|
1151
|
+
documentId = reducer(documentId, action);
|
|
1152
|
+
const operation = documentId.operations[action.scope].slice().pop();
|
|
1153
|
+
if (!operation) {
|
|
1154
|
+
throw new Error("Error creating operations");
|
|
1155
|
+
}
|
|
1156
|
+
operations.push(operation);
|
|
1157
|
+
}
|
|
1158
|
+
return operations;
|
|
1159
|
+
}
|
|
1160
|
+
async addAction(driveId, documentId, action, options) {
|
|
1161
|
+
return this.addActions(driveId, documentId, [action], options);
|
|
1162
|
+
}
|
|
1163
|
+
async addActions(driveId, documentId, actions, options) {
|
|
1164
|
+
const document = await this.getDocument(driveId, documentId);
|
|
1165
|
+
const operations = this._buildOperations(document, actions);
|
|
1166
|
+
return this.addOperations(driveId, documentId, operations, options);
|
|
1167
|
+
}
|
|
1168
|
+
async addDriveAction(driveId, action, options) {
|
|
1169
|
+
return this.addDriveActions(driveId, [action], options);
|
|
1170
|
+
}
|
|
1171
|
+
async addDriveActions(driveId, actions, options) {
|
|
1172
|
+
const document = await this.getDrive(driveId);
|
|
1173
|
+
const operations = this._buildOperations(document, actions);
|
|
1174
|
+
const result = await this.addDriveOperations(driveId, operations, options);
|
|
1175
|
+
return result;
|
|
1176
|
+
}
|
|
1177
|
+
async detachDrive(driveId) {
|
|
1178
|
+
const documentDrive = await this.getDrive(driveId);
|
|
1179
|
+
const listeners = documentDrive.state.local.listeners || [];
|
|
1180
|
+
const triggers = documentDrive.state.local.triggers || [];
|
|
1181
|
+
for (const listener of listeners) {
|
|
1182
|
+
await this.addDriveAction(driveId, removeListener({ listenerId: listener.listenerId }));
|
|
1183
|
+
}
|
|
1184
|
+
for (const trigger of triggers) {
|
|
1185
|
+
await this.addDriveAction(driveId, removeTrigger({ triggerId: trigger.id }));
|
|
1186
|
+
}
|
|
1187
|
+
await this.addDriveAction(driveId, setSharingType({ type: "LOCAL" }));
|
|
1188
|
+
}
|
|
1189
|
+
async addListener(driveId, listener) {
|
|
1190
|
+
await this.listenerManager.setListener(driveId, listener);
|
|
1191
|
+
}
|
|
1192
|
+
async addInternalListener(driveId, receiver, options) {
|
|
1193
|
+
const listener = {
|
|
1194
|
+
callInfo: {
|
|
1195
|
+
data: "",
|
|
1196
|
+
name: "Interal",
|
|
1197
|
+
transmitterType: "Internal",
|
|
1198
|
+
},
|
|
1199
|
+
system: true,
|
|
1200
|
+
...options,
|
|
1201
|
+
};
|
|
1202
|
+
await this.addDriveAction(driveId, addListener({ listener }));
|
|
1203
|
+
const transmitter = await this.getTransmitter(driveId, options.listenerId);
|
|
1204
|
+
if (!transmitter) {
|
|
1205
|
+
logger.error("Internal listener not found");
|
|
1206
|
+
throw new Error("Internal listener not found");
|
|
1207
|
+
}
|
|
1208
|
+
if (!(transmitter instanceof InternalTransmitter)) {
|
|
1209
|
+
logger.error("Listener is not an internal transmitter");
|
|
1210
|
+
throw new Error("Listener is not an internal transmitter");
|
|
1211
|
+
}
|
|
1212
|
+
transmitter.setReceiver(receiver);
|
|
1213
|
+
return transmitter;
|
|
1214
|
+
}
|
|
1215
|
+
async removeListener(driveId, operation) {
|
|
1216
|
+
const { listenerId } = operation.input;
|
|
1217
|
+
await this.listenerManager.removeListener(driveId, listenerId);
|
|
1218
|
+
}
|
|
1219
|
+
async getTransmitter(driveId, listenerId) {
|
|
1220
|
+
const listener = this.listenerManager.getListenerState(driveId, listenerId);
|
|
1221
|
+
return listener.listener.transmitter;
|
|
1222
|
+
}
|
|
1223
|
+
getListener(driveId, listenerId) {
|
|
1224
|
+
let listenerState;
|
|
1225
|
+
try {
|
|
1226
|
+
listenerState = this.listenerManager.getListenerState(driveId, listenerId);
|
|
1227
|
+
}
|
|
1228
|
+
catch {
|
|
1229
|
+
return Promise.resolve(undefined);
|
|
1230
|
+
}
|
|
1231
|
+
return Promise.resolve(listenerState);
|
|
1232
|
+
}
|
|
1233
|
+
getSyncStatus(syncUnitId) {
|
|
1234
|
+
return this.synchronizationManager.getSyncStatus(syncUnitId);
|
|
1235
|
+
}
|
|
1236
|
+
on(event, cb) {
|
|
1237
|
+
return this.eventEmitter.on(event, cb);
|
|
1238
|
+
}
|
|
1239
|
+
emit(event, ...args) {
|
|
1240
|
+
return this.eventEmitter.emit(event, ...args);
|
|
1241
|
+
}
|
|
1242
|
+
getSynchronizationUnit(driveId, syncId) {
|
|
1243
|
+
return this.synchronizationManager.getSynchronizationUnit(driveId, syncId);
|
|
1244
|
+
}
|
|
1245
|
+
// Add delegated methods to properly implement ISynchronizationManager
|
|
1246
|
+
updateSyncStatus(syncUnitId, status, error) {
|
|
1247
|
+
this.synchronizationManager.updateSyncStatus(syncUnitId, status, error);
|
|
1248
|
+
}
|
|
1249
|
+
initializeDriveSyncStatus(driveId, drive) {
|
|
1250
|
+
return this.synchronizationManager.initializeDriveSyncStatus(driveId, drive);
|
|
1251
|
+
}
|
|
1252
|
+
getCombinedSyncUnitStatus(syncUnitStatus) {
|
|
1253
|
+
return this.synchronizationManager.getCombinedSyncUnitStatus(syncUnitStatus);
|
|
1254
|
+
}
|
|
1255
|
+
// Add back the saveStrand method that was accidentally removed
|
|
1256
|
+
async saveStrand(strand, source) {
|
|
1257
|
+
const operations = strand.operations.map((op) => ({
|
|
1258
|
+
...op,
|
|
1259
|
+
scope: strand.scope,
|
|
1260
|
+
branch: strand.branch,
|
|
1261
|
+
}));
|
|
1262
|
+
const result = await (!strand.documentId
|
|
1263
|
+
? this.queueDriveOperations(strand.driveId, operations, { source })
|
|
1264
|
+
: this.queueOperations(strand.driveId, strand.documentId, operations, {
|
|
1265
|
+
source,
|
|
1266
|
+
}));
|
|
1267
|
+
if (result.status === "ERROR") {
|
|
1268
|
+
const syncUnits = strand.documentId !== ""
|
|
1269
|
+
? (await this.getSynchronizationUnitsIds(strand.driveId, [strand.documentId], [strand.scope], [strand.branch])).map((s) => s.syncId)
|
|
1270
|
+
: [strand.driveId];
|
|
1271
|
+
const operationSource = this.getOperationSource(source);
|
|
1272
|
+
for (const syncUnit of syncUnits) {
|
|
1273
|
+
this.synchronizationManager.updateSyncStatus(syncUnit, { [operationSource]: result.status }, result.error);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
this.eventEmitter.emit("strandUpdate", strand);
|
|
1277
|
+
return result;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
export const DocumentDriveServer = ReadModeServer(BaseDocumentDriveServer);
|