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
package/src/server/index.ts
DELETED
|
@@ -1,2444 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
actions,
|
|
3
|
-
AddListenerInput,
|
|
4
|
-
DocumentDriveAction,
|
|
5
|
-
DocumentDriveDocument,
|
|
6
|
-
DocumentDriveState,
|
|
7
|
-
FileNode,
|
|
8
|
-
isFileNode,
|
|
9
|
-
ListenerFilter,
|
|
10
|
-
RemoveListenerInput,
|
|
11
|
-
Trigger,
|
|
12
|
-
utils,
|
|
13
|
-
} from "document-model-libs/document-drive";
|
|
14
|
-
import {
|
|
15
|
-
Action,
|
|
16
|
-
BaseAction,
|
|
17
|
-
utils as baseUtils,
|
|
18
|
-
Document,
|
|
19
|
-
DocumentHeader,
|
|
20
|
-
DocumentModel,
|
|
21
|
-
utils as DocumentUtils,
|
|
22
|
-
Operation,
|
|
23
|
-
OperationScope,
|
|
24
|
-
} from "document-model/document";
|
|
25
|
-
import { ClientError } from "graphql-request";
|
|
26
|
-
import { createNanoEvents, Unsubscribe } from "nanoevents";
|
|
27
|
-
import { ICache } from "../cache";
|
|
28
|
-
import InMemoryCache from "../cache/memory";
|
|
29
|
-
import { BaseQueueManager } from "../queue/base";
|
|
30
|
-
import {
|
|
31
|
-
ActionJob,
|
|
32
|
-
IQueueManager,
|
|
33
|
-
isActionJob,
|
|
34
|
-
isOperationJob,
|
|
35
|
-
Job,
|
|
36
|
-
OperationJob,
|
|
37
|
-
} from "../queue/types";
|
|
38
|
-
import { ReadModeServer } from "../read-mode";
|
|
39
|
-
import { MemoryStorage } from "../storage/memory";
|
|
40
|
-
import type {
|
|
41
|
-
DocumentDriveStorage,
|
|
42
|
-
DocumentStorage,
|
|
43
|
-
IDriveStorage,
|
|
44
|
-
} from "../storage/types";
|
|
45
|
-
import {
|
|
46
|
-
generateUUID,
|
|
47
|
-
isBefore,
|
|
48
|
-
isDocumentDrive,
|
|
49
|
-
RunAsap,
|
|
50
|
-
runAsapAsync,
|
|
51
|
-
} from "../utils";
|
|
52
|
-
import { DefaultDrivesManager } from "../utils/default-drives-manager";
|
|
53
|
-
import {
|
|
54
|
-
attachBranch,
|
|
55
|
-
garbageCollect,
|
|
56
|
-
groupOperationsByScope,
|
|
57
|
-
merge,
|
|
58
|
-
precedes,
|
|
59
|
-
removeExistingOperations,
|
|
60
|
-
reshuffleByTimestamp,
|
|
61
|
-
sortOperations,
|
|
62
|
-
} from "../utils/document-helpers";
|
|
63
|
-
import { requestPublicDrive } from "../utils/graphql";
|
|
64
|
-
import { logger } from "../utils/logger";
|
|
65
|
-
import {
|
|
66
|
-
ConflictOperationError,
|
|
67
|
-
DriveAlreadyExistsError,
|
|
68
|
-
OperationError,
|
|
69
|
-
SynchronizationUnitNotFoundError,
|
|
70
|
-
} from "./error";
|
|
71
|
-
import { ListenerManager } from "./listener";
|
|
72
|
-
import {
|
|
73
|
-
CancelPullLoop,
|
|
74
|
-
InternalTransmitter,
|
|
75
|
-
IReceiver,
|
|
76
|
-
ITransmitter,
|
|
77
|
-
PullResponderTransmitter,
|
|
78
|
-
StrandUpdateSource,
|
|
79
|
-
SwitchboardPushTransmitter,
|
|
80
|
-
} from "./listener/transmitter";
|
|
81
|
-
import {
|
|
82
|
-
AddOperationOptions,
|
|
83
|
-
DefaultListenerManagerOptions,
|
|
84
|
-
DocumentDriveServerOptions,
|
|
85
|
-
DriveEvents,
|
|
86
|
-
GetDocumentOptions,
|
|
87
|
-
GetStrandsOptions,
|
|
88
|
-
IBaseDocumentDriveServer,
|
|
89
|
-
IListenerManager,
|
|
90
|
-
IOperationResult,
|
|
91
|
-
Listener,
|
|
92
|
-
ListenerState,
|
|
93
|
-
RemoteDriveAccessLevel,
|
|
94
|
-
RemoteDriveOptions,
|
|
95
|
-
StrandUpdate,
|
|
96
|
-
SynchronizationUnitQuery,
|
|
97
|
-
SyncStatus,
|
|
98
|
-
SyncUnitStatusObject,
|
|
99
|
-
type CreateDocumentInput,
|
|
100
|
-
type DriveInput,
|
|
101
|
-
type OperationUpdate,
|
|
102
|
-
type SignalResult,
|
|
103
|
-
type SynchronizationUnit,
|
|
104
|
-
} from "./types";
|
|
105
|
-
import { filterOperationsByRevision, isAtRevision } from "./utils";
|
|
106
|
-
|
|
107
|
-
export * from "./listener";
|
|
108
|
-
export type * from "./types";
|
|
109
|
-
|
|
110
|
-
export * from "../read-mode";
|
|
111
|
-
|
|
112
|
-
export const PULL_DRIVE_INTERVAL = 5000;
|
|
113
|
-
|
|
114
|
-
interface ITransmitterFactory {
|
|
115
|
-
instance(
|
|
116
|
-
transmitterType: string,
|
|
117
|
-
listener: Listener,
|
|
118
|
-
driveServer: IBaseDocumentDriveServer,
|
|
119
|
-
): ITransmitter;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export class TransmitterFactory implements ITransmitterFactory {
|
|
123
|
-
private readonly listenerManager: IListenerManager;
|
|
124
|
-
|
|
125
|
-
constructor(listenerManager: IListenerManager) {
|
|
126
|
-
this.listenerManager = listenerManager;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
instance(
|
|
130
|
-
transmitterType: string,
|
|
131
|
-
listener: Listener,
|
|
132
|
-
driveServer: IBaseDocumentDriveServer,
|
|
133
|
-
): ITransmitter {
|
|
134
|
-
switch (transmitterType) {
|
|
135
|
-
case "SwitchboardPush": {
|
|
136
|
-
return new SwitchboardPushTransmitter(listener.callInfo!.data!);
|
|
137
|
-
}
|
|
138
|
-
case "Internal": {
|
|
139
|
-
return new InternalTransmitter(listener, driveServer);
|
|
140
|
-
}
|
|
141
|
-
default: {
|
|
142
|
-
return new PullResponderTransmitter(listener, this.listenerManager);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export class BaseDocumentDriveServer implements IBaseDocumentDriveServer {
|
|
149
|
-
private emitter = createNanoEvents<DriveEvents>();
|
|
150
|
-
private cache: ICache;
|
|
151
|
-
private documentModels: DocumentModel[];
|
|
152
|
-
private storage: IDriveStorage;
|
|
153
|
-
private transmitterFactory: ITransmitterFactory;
|
|
154
|
-
private listenerManager: IListenerManager;
|
|
155
|
-
private triggerMap = new Map<
|
|
156
|
-
DocumentDriveState["id"],
|
|
157
|
-
Map<Trigger["id"], CancelPullLoop>
|
|
158
|
-
>();
|
|
159
|
-
private syncStatus = new Map<string, SyncUnitStatusObject>();
|
|
160
|
-
|
|
161
|
-
private queueManager: IQueueManager;
|
|
162
|
-
private initializePromise: Promise<Error[] | null>;
|
|
163
|
-
|
|
164
|
-
private defaultDrivesManager: DefaultDrivesManager;
|
|
165
|
-
|
|
166
|
-
protected options: Required<DocumentDriveServerOptions>;
|
|
167
|
-
|
|
168
|
-
constructor(
|
|
169
|
-
documentModels: DocumentModel[],
|
|
170
|
-
storage: IDriveStorage = new MemoryStorage(),
|
|
171
|
-
cache: ICache = new InMemoryCache(),
|
|
172
|
-
queueManager: IQueueManager = new BaseQueueManager(),
|
|
173
|
-
options?: DocumentDriveServerOptions,
|
|
174
|
-
) {
|
|
175
|
-
this.options = {
|
|
176
|
-
...options,
|
|
177
|
-
defaultDrives: {
|
|
178
|
-
...options?.defaultDrives,
|
|
179
|
-
},
|
|
180
|
-
listenerManager: {
|
|
181
|
-
...DefaultListenerManagerOptions,
|
|
182
|
-
...options?.listenerManager,
|
|
183
|
-
},
|
|
184
|
-
taskQueueMethod:
|
|
185
|
-
options?.taskQueueMethod === undefined
|
|
186
|
-
? RunAsap.runAsap
|
|
187
|
-
: options.taskQueueMethod,
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// todo: pull this into the constructor -- there is a circular dependency right now
|
|
191
|
-
this.listenerManager = new ListenerManager(
|
|
192
|
-
this,
|
|
193
|
-
undefined,
|
|
194
|
-
this.options.listenerManager,
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
// todo: pull this into the constructor, depends on listenerManager
|
|
198
|
-
this.transmitterFactory = new TransmitterFactory(this.listenerManager);
|
|
199
|
-
|
|
200
|
-
this.documentModels = documentModels;
|
|
201
|
-
this.storage = storage;
|
|
202
|
-
this.cache = cache;
|
|
203
|
-
this.queueManager = queueManager;
|
|
204
|
-
this.defaultDrivesManager = new DefaultDrivesManager(
|
|
205
|
-
this,
|
|
206
|
-
this.defaultDrivesManagerDelegate,
|
|
207
|
-
options,
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
this.storage.setStorageDelegate?.({
|
|
211
|
-
getCachedOperations: async (drive, id) => {
|
|
212
|
-
try {
|
|
213
|
-
const document = await this.cache.getDocument(drive, id);
|
|
214
|
-
return document?.operations;
|
|
215
|
-
} catch (error) {
|
|
216
|
-
logger.error(error);
|
|
217
|
-
return undefined;
|
|
218
|
-
}
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
this.initializePromise = this._initialize();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
setDocumentModels(models: DocumentModel[]): void {
|
|
226
|
-
this.documentModels = [...models];
|
|
227
|
-
this.emit("documentModels", [...models]);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
initializeDefaultRemoteDrives() {
|
|
231
|
-
return this.defaultDrivesManager.initializeDefaultRemoteDrives();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
getDefaultRemoteDrives() {
|
|
235
|
-
return this.defaultDrivesManager.getDefaultRemoteDrives();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
setDefaultDriveAccessLevel(url: string, level: RemoteDriveAccessLevel) {
|
|
239
|
-
return this.defaultDrivesManager.setDefaultDriveAccessLevel(url, level);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
setAllDefaultDrivesAccessLevel(level: RemoteDriveAccessLevel) {
|
|
243
|
-
return this.defaultDrivesManager.setAllDefaultDrivesAccessLevel(level);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
private getOperationSource(source: StrandUpdateSource) {
|
|
247
|
-
return source.type === "local" ? "push" : "pull";
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private getCombinedSyncUnitStatus(
|
|
251
|
-
syncUnitStatus: SyncUnitStatusObject,
|
|
252
|
-
): SyncStatus {
|
|
253
|
-
if (!syncUnitStatus.pull && !syncUnitStatus.push) return "INITIAL_SYNC";
|
|
254
|
-
if (syncUnitStatus.pull === "INITIAL_SYNC") return "INITIAL_SYNC";
|
|
255
|
-
if (syncUnitStatus.push === "INITIAL_SYNC")
|
|
256
|
-
return syncUnitStatus.pull || "INITIAL_SYNC";
|
|
257
|
-
|
|
258
|
-
const order: Array<SyncStatus> = [
|
|
259
|
-
"ERROR",
|
|
260
|
-
"MISSING",
|
|
261
|
-
"CONFLICT",
|
|
262
|
-
"SYNCING",
|
|
263
|
-
"SUCCESS",
|
|
264
|
-
];
|
|
265
|
-
const sortedStatus = Object.values(syncUnitStatus).sort(
|
|
266
|
-
(a, b) => order.indexOf(a) - order.indexOf(b),
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
270
|
-
return sortedStatus[0]!;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
private initSyncStatus(
|
|
274
|
-
syncUnitId: string,
|
|
275
|
-
status: Partial<SyncUnitStatusObject>,
|
|
276
|
-
) {
|
|
277
|
-
const defaultSyncUnitStatus: SyncUnitStatusObject = Object.entries(
|
|
278
|
-
status,
|
|
279
|
-
).reduce((acc, [key, _status]) => {
|
|
280
|
-
return {
|
|
281
|
-
...acc,
|
|
282
|
-
[key]: _status !== "SYNCING" ? _status : "INITIAL_SYNC",
|
|
283
|
-
};
|
|
284
|
-
}, {});
|
|
285
|
-
|
|
286
|
-
this.syncStatus.set(syncUnitId, defaultSyncUnitStatus);
|
|
287
|
-
this.emit(
|
|
288
|
-
"syncStatus",
|
|
289
|
-
syncUnitId,
|
|
290
|
-
this.getCombinedSyncUnitStatus(defaultSyncUnitStatus),
|
|
291
|
-
undefined,
|
|
292
|
-
defaultSyncUnitStatus,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
private async initializeDriveSyncStatus(
|
|
297
|
-
driveId: string,
|
|
298
|
-
drive: DocumentDriveDocument,
|
|
299
|
-
) {
|
|
300
|
-
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
301
|
-
const syncStatus: SyncUnitStatusObject = {
|
|
302
|
-
pull: drive.state.local.triggers.length > 0 ? "INITIAL_SYNC" : undefined,
|
|
303
|
-
push: drive.state.local.listeners.length > 0 ? "SUCCESS" : undefined,
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
if (!syncStatus.pull && !syncStatus.push) return;
|
|
307
|
-
|
|
308
|
-
const syncUnitsIds = [driveId, ...syncUnits.map((s) => s.syncId)];
|
|
309
|
-
|
|
310
|
-
for (const syncUnitId of syncUnitsIds) {
|
|
311
|
-
this.initSyncStatus(syncUnitId, syncStatus);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private updateSyncUnitStatus(
|
|
316
|
-
syncUnitId: string,
|
|
317
|
-
status: Partial<SyncUnitStatusObject> | null,
|
|
318
|
-
error?: Error,
|
|
319
|
-
) {
|
|
320
|
-
if (status === null) {
|
|
321
|
-
this.syncStatus.delete(syncUnitId);
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const syncUnitStatus = this.syncStatus.get(syncUnitId);
|
|
326
|
-
|
|
327
|
-
if (!syncUnitStatus) {
|
|
328
|
-
this.initSyncStatus(syncUnitId, status);
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const shouldUpdateStatus = Object.entries(status).some(
|
|
333
|
-
([key, _status]) =>
|
|
334
|
-
syncUnitStatus[key as keyof SyncUnitStatusObject] !== _status,
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
if (shouldUpdateStatus) {
|
|
338
|
-
const newstatus = Object.entries(status).reduce((acc, [key, _status]) => {
|
|
339
|
-
return {
|
|
340
|
-
...acc,
|
|
341
|
-
// do not replace initial_syncing if it has not finished yet
|
|
342
|
-
[key]:
|
|
343
|
-
acc[key as keyof SyncUnitStatusObject] === "INITIAL_SYNC" &&
|
|
344
|
-
_status === "SYNCING"
|
|
345
|
-
? "INITIAL_SYNC"
|
|
346
|
-
: _status,
|
|
347
|
-
};
|
|
348
|
-
}, syncUnitStatus);
|
|
349
|
-
|
|
350
|
-
const previousCombinedStatus =
|
|
351
|
-
this.getCombinedSyncUnitStatus(syncUnitStatus);
|
|
352
|
-
const newCombinedStatus = this.getCombinedSyncUnitStatus(newstatus);
|
|
353
|
-
|
|
354
|
-
this.syncStatus.set(syncUnitId, newstatus);
|
|
355
|
-
|
|
356
|
-
if (previousCombinedStatus !== newCombinedStatus) {
|
|
357
|
-
this.emit(
|
|
358
|
-
"syncStatus",
|
|
359
|
-
syncUnitId,
|
|
360
|
-
this.getCombinedSyncUnitStatus(newstatus),
|
|
361
|
-
error,
|
|
362
|
-
newstatus,
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
private async saveStrand(strand: StrandUpdate, source: StrandUpdateSource) {
|
|
369
|
-
const operations: Operation[] = strand.operations.map((op) => ({
|
|
370
|
-
...op,
|
|
371
|
-
scope: strand.scope,
|
|
372
|
-
branch: strand.branch,
|
|
373
|
-
}));
|
|
374
|
-
|
|
375
|
-
const result = await (!strand.documentId
|
|
376
|
-
? this.queueDriveOperations(
|
|
377
|
-
strand.driveId,
|
|
378
|
-
operations as Operation<DocumentDriveAction | BaseAction>[],
|
|
379
|
-
{ source },
|
|
380
|
-
)
|
|
381
|
-
: this.queueOperations(strand.driveId, strand.documentId, operations, {
|
|
382
|
-
source,
|
|
383
|
-
}));
|
|
384
|
-
|
|
385
|
-
if (result.status === "ERROR") {
|
|
386
|
-
const syncUnits =
|
|
387
|
-
strand.documentId !== ""
|
|
388
|
-
? (
|
|
389
|
-
await this.getSynchronizationUnitsIds(
|
|
390
|
-
strand.driveId,
|
|
391
|
-
[strand.documentId],
|
|
392
|
-
[strand.scope],
|
|
393
|
-
[strand.branch],
|
|
394
|
-
)
|
|
395
|
-
).map((s) => s.syncId)
|
|
396
|
-
: [strand.driveId];
|
|
397
|
-
|
|
398
|
-
const operationSource = this.getOperationSource(source);
|
|
399
|
-
|
|
400
|
-
for (const syncUnit of syncUnits) {
|
|
401
|
-
this.updateSyncUnitStatus(
|
|
402
|
-
syncUnit,
|
|
403
|
-
{ [operationSource]: result.status },
|
|
404
|
-
result.error,
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
this.emit("strandUpdate", strand);
|
|
409
|
-
return result;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
private handleListenerError(
|
|
413
|
-
error: Error,
|
|
414
|
-
driveId: string,
|
|
415
|
-
listener: ListenerState,
|
|
416
|
-
) {
|
|
417
|
-
logger.error(
|
|
418
|
-
`Listener ${listener.listener.label ?? listener.listener.listenerId} error:`,
|
|
419
|
-
error,
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
const status = error instanceof OperationError ? error.status : "ERROR";
|
|
423
|
-
|
|
424
|
-
this.updateSyncUnitStatus(driveId, { push: status }, error);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private shouldSyncRemoteDrive(drive: DocumentDriveDocument) {
|
|
428
|
-
return (
|
|
429
|
-
drive.state.local.availableOffline &&
|
|
430
|
-
drive.state.local.triggers.length > 0
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
private async startSyncRemoteDrive(driveId: string) {
|
|
435
|
-
const drive = await this.getDrive(driveId);
|
|
436
|
-
let driveTriggers = this.triggerMap.get(driveId);
|
|
437
|
-
|
|
438
|
-
const syncUnits = await this.getSynchronizationUnitsIds(
|
|
439
|
-
driveId,
|
|
440
|
-
undefined,
|
|
441
|
-
undefined,
|
|
442
|
-
undefined,
|
|
443
|
-
undefined,
|
|
444
|
-
drive,
|
|
445
|
-
);
|
|
446
|
-
|
|
447
|
-
for (const trigger of drive.state.local.triggers) {
|
|
448
|
-
if (driveTriggers?.get(trigger.id)) {
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (!driveTriggers) {
|
|
453
|
-
driveTriggers = new Map();
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
this.updateSyncUnitStatus(driveId, { pull: "SYNCING" });
|
|
457
|
-
|
|
458
|
-
for (const syncUnit of syncUnits) {
|
|
459
|
-
this.updateSyncUnitStatus(syncUnit.syncId, { pull: "SYNCING" });
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
|
|
463
|
-
let firstPull = true;
|
|
464
|
-
const cancelPullLoop = PullResponderTransmitter.setupPull(
|
|
465
|
-
driveId,
|
|
466
|
-
trigger,
|
|
467
|
-
this.saveStrand.bind(this),
|
|
468
|
-
(error) => {
|
|
469
|
-
const statusError =
|
|
470
|
-
error instanceof OperationError ? error.status : "ERROR";
|
|
471
|
-
|
|
472
|
-
this.updateSyncUnitStatus(driveId, { pull: statusError }, error);
|
|
473
|
-
|
|
474
|
-
if (error instanceof ClientError) {
|
|
475
|
-
this.emit(
|
|
476
|
-
"clientStrandsError",
|
|
477
|
-
driveId,
|
|
478
|
-
trigger,
|
|
479
|
-
error.response.status,
|
|
480
|
-
error.message,
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
},
|
|
484
|
-
(revisions) => {
|
|
485
|
-
const errorRevision = revisions.filter(
|
|
486
|
-
(r) => r.status !== "SUCCESS",
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
if (errorRevision.length < 1) {
|
|
490
|
-
this.updateSyncUnitStatus(driveId, {
|
|
491
|
-
pull: "SUCCESS",
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const documentIdsFromRevision = revisions
|
|
496
|
-
.filter((rev) => rev.documentId !== "")
|
|
497
|
-
.map((rev) => rev.documentId);
|
|
498
|
-
|
|
499
|
-
this.getSynchronizationUnitsIds(driveId, documentIdsFromRevision)
|
|
500
|
-
.then((revSyncUnits) => {
|
|
501
|
-
for (const syncUnit of revSyncUnits) {
|
|
502
|
-
const fileErrorRevision = errorRevision.find(
|
|
503
|
-
(r) => r.documentId === syncUnit.documentId,
|
|
504
|
-
);
|
|
505
|
-
|
|
506
|
-
if (fileErrorRevision) {
|
|
507
|
-
this.updateSyncUnitStatus(
|
|
508
|
-
syncUnit.syncId,
|
|
509
|
-
{ pull: fileErrorRevision.status },
|
|
510
|
-
fileErrorRevision.error,
|
|
511
|
-
);
|
|
512
|
-
} else {
|
|
513
|
-
this.updateSyncUnitStatus(syncUnit.syncId, {
|
|
514
|
-
pull: "SUCCESS",
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
})
|
|
519
|
-
.catch(console.error);
|
|
520
|
-
|
|
521
|
-
// if it is the first pull and returns empty
|
|
522
|
-
// then updates corresponding push transmitter
|
|
523
|
-
if (firstPull) {
|
|
524
|
-
firstPull = false;
|
|
525
|
-
const pushListener = drive.state.local.listeners.find(
|
|
526
|
-
(listener) => trigger.data.url === listener.callInfo?.data,
|
|
527
|
-
);
|
|
528
|
-
if (pushListener) {
|
|
529
|
-
this.getSynchronizationUnitsRevision(driveId, syncUnits)
|
|
530
|
-
.then((syncUnitRevisions) => {
|
|
531
|
-
for (const revision of syncUnitRevisions) {
|
|
532
|
-
this.listenerManager
|
|
533
|
-
.updateListenerRevision(
|
|
534
|
-
pushListener.listenerId,
|
|
535
|
-
driveId,
|
|
536
|
-
revision.syncId,
|
|
537
|
-
revision.revision,
|
|
538
|
-
)
|
|
539
|
-
.catch(logger.error);
|
|
540
|
-
}
|
|
541
|
-
})
|
|
542
|
-
.catch(logger.error);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
},
|
|
546
|
-
);
|
|
547
|
-
driveTriggers.set(trigger.id, cancelPullLoop);
|
|
548
|
-
this.triggerMap.set(driveId, driveTriggers);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
private async stopSyncRemoteDrive(driveId: string) {
|
|
554
|
-
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
555
|
-
const filesNodeSyncId = syncUnits
|
|
556
|
-
.filter((syncUnit) => syncUnit.documentId !== "")
|
|
557
|
-
.map((syncUnit) => syncUnit.syncId);
|
|
558
|
-
|
|
559
|
-
const triggers = this.triggerMap.get(driveId);
|
|
560
|
-
triggers?.forEach((cancel) => cancel());
|
|
561
|
-
this.updateSyncUnitStatus(driveId, null);
|
|
562
|
-
|
|
563
|
-
for (const fileNodeSyncId of filesNodeSyncId) {
|
|
564
|
-
this.updateSyncUnitStatus(fileNodeSyncId, null);
|
|
565
|
-
}
|
|
566
|
-
return this.triggerMap.delete(driveId);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
private defaultDrivesManagerDelegate = {
|
|
570
|
-
detachDrive: this.detachDrive.bind(this),
|
|
571
|
-
emit: (...args: Parameters<DriveEvents["defaultRemoteDrive"]>) =>
|
|
572
|
-
this.emit("defaultRemoteDrive", ...args),
|
|
573
|
-
};
|
|
574
|
-
|
|
575
|
-
private queueDelegate = {
|
|
576
|
-
checkDocumentExists: (
|
|
577
|
-
driveId: string,
|
|
578
|
-
documentId: string,
|
|
579
|
-
): Promise<boolean> =>
|
|
580
|
-
this.storage.checkDocumentExists(driveId, documentId),
|
|
581
|
-
processOperationJob: async ({
|
|
582
|
-
driveId,
|
|
583
|
-
documentId,
|
|
584
|
-
operations,
|
|
585
|
-
options,
|
|
586
|
-
}: OperationJob) => {
|
|
587
|
-
return documentId
|
|
588
|
-
? this.addOperations(driveId, documentId, operations, options)
|
|
589
|
-
: this.addDriveOperations(
|
|
590
|
-
driveId,
|
|
591
|
-
operations as Operation<DocumentDriveAction | BaseAction>[],
|
|
592
|
-
options,
|
|
593
|
-
);
|
|
594
|
-
},
|
|
595
|
-
processActionJob: async ({
|
|
596
|
-
driveId,
|
|
597
|
-
documentId,
|
|
598
|
-
actions,
|
|
599
|
-
options,
|
|
600
|
-
}: ActionJob) => {
|
|
601
|
-
return documentId
|
|
602
|
-
? this.addActions(driveId, documentId, actions, options)
|
|
603
|
-
: this.addDriveActions(
|
|
604
|
-
driveId,
|
|
605
|
-
actions as Operation<DocumentDriveAction | BaseAction>[],
|
|
606
|
-
options,
|
|
607
|
-
);
|
|
608
|
-
},
|
|
609
|
-
processJob: async (job: Job) => {
|
|
610
|
-
if (isOperationJob(job)) {
|
|
611
|
-
return this.queueDelegate.processOperationJob(job);
|
|
612
|
-
} else if (isActionJob(job)) {
|
|
613
|
-
return this.queueDelegate.processActionJob(job);
|
|
614
|
-
} else {
|
|
615
|
-
throw new Error("Unknown job type", job);
|
|
616
|
-
}
|
|
617
|
-
},
|
|
618
|
-
};
|
|
619
|
-
|
|
620
|
-
initialize() {
|
|
621
|
-
return this.initializePromise;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
private async _initialize() {
|
|
625
|
-
await this.listenerManager.initialize(this.handleListenerError);
|
|
626
|
-
|
|
627
|
-
await this.queueManager.init(this.queueDelegate, (error) => {
|
|
628
|
-
logger.error(`Error initializing queue manager`, error);
|
|
629
|
-
errors.push(error);
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
try {
|
|
633
|
-
await this.defaultDrivesManager.removeOldremoteDrives();
|
|
634
|
-
} catch (error) {
|
|
635
|
-
logger.error(error);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const errors: Error[] = [];
|
|
639
|
-
const drives = await this.getDrives();
|
|
640
|
-
for (const drive of drives) {
|
|
641
|
-
await this._initializeDrive(drive).catch((error) => {
|
|
642
|
-
logger.error(`Error initializing drive ${drive}`, error);
|
|
643
|
-
errors.push(error as Error);
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
if (this.options.defaultDrives.loadOnInit !== false) {
|
|
648
|
-
await this.defaultDrivesManager.initializeDefaultRemoteDrives();
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
return errors.length === 0 ? null : errors;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
private async _initializeDrive(driveId: string) {
|
|
655
|
-
const drive = await this.getDrive(driveId);
|
|
656
|
-
await this.initializeDriveSyncStatus(driveId, drive);
|
|
657
|
-
|
|
658
|
-
if (this.shouldSyncRemoteDrive(drive)) {
|
|
659
|
-
await this.startSyncRemoteDrive(driveId);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
for (const zodListener of drive.state.local.listeners) {
|
|
663
|
-
const transmitter = this.transmitterFactory.instance(
|
|
664
|
-
zodListener.callInfo?.transmitterType ?? "",
|
|
665
|
-
{
|
|
666
|
-
driveId,
|
|
667
|
-
listenerId: zodListener.listenerId,
|
|
668
|
-
block: zodListener.block,
|
|
669
|
-
filter: zodListener.filter,
|
|
670
|
-
system: zodListener.system,
|
|
671
|
-
label: zodListener.label || undefined,
|
|
672
|
-
callInfo: zodListener.callInfo || undefined,
|
|
673
|
-
},
|
|
674
|
-
this,
|
|
675
|
-
);
|
|
676
|
-
|
|
677
|
-
await this.listenerManager.setListener(driveId, {
|
|
678
|
-
block: zodListener.block,
|
|
679
|
-
driveId: drive.state.global.id,
|
|
680
|
-
filter: {
|
|
681
|
-
branch: zodListener.filter.branch ?? [],
|
|
682
|
-
documentId: zodListener.filter.documentId ?? [],
|
|
683
|
-
documentType: zodListener.filter.documentType,
|
|
684
|
-
scope: zodListener.filter.scope ?? [],
|
|
685
|
-
},
|
|
686
|
-
listenerId: zodListener.listenerId,
|
|
687
|
-
system: zodListener.system,
|
|
688
|
-
label: zodListener.label ?? "",
|
|
689
|
-
transmitter,
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
public async getSynchronizationUnits(
|
|
695
|
-
driveId: string,
|
|
696
|
-
documentId?: string[],
|
|
697
|
-
scope?: string[],
|
|
698
|
-
branch?: string[],
|
|
699
|
-
documentType?: string[],
|
|
700
|
-
loadedDrive?: DocumentDriveDocument,
|
|
701
|
-
) {
|
|
702
|
-
const drive = loadedDrive || (await this.getDrive(driveId));
|
|
703
|
-
|
|
704
|
-
const synchronizationUnitsQuery = await this.getSynchronizationUnitsIds(
|
|
705
|
-
driveId,
|
|
706
|
-
documentId,
|
|
707
|
-
scope,
|
|
708
|
-
branch,
|
|
709
|
-
documentType,
|
|
710
|
-
drive,
|
|
711
|
-
);
|
|
712
|
-
return this.getSynchronizationUnitsRevision(
|
|
713
|
-
driveId,
|
|
714
|
-
synchronizationUnitsQuery,
|
|
715
|
-
drive,
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
public async getSynchronizationUnitsRevision(
|
|
720
|
-
driveId: string,
|
|
721
|
-
syncUnitsQuery: SynchronizationUnitQuery[],
|
|
722
|
-
loadedDrive?: DocumentDriveDocument,
|
|
723
|
-
): Promise<SynchronizationUnit[]> {
|
|
724
|
-
const drive = loadedDrive || (await this.getDrive(driveId));
|
|
725
|
-
|
|
726
|
-
const revisions =
|
|
727
|
-
await this.storage.getSynchronizationUnitsRevision(syncUnitsQuery);
|
|
728
|
-
|
|
729
|
-
const synchronizationUnits: SynchronizationUnit[] = syncUnitsQuery.map(
|
|
730
|
-
(s) => ({
|
|
731
|
-
...s,
|
|
732
|
-
lastUpdated: drive.created,
|
|
733
|
-
revision: -1,
|
|
734
|
-
}),
|
|
735
|
-
);
|
|
736
|
-
for (const revision of revisions) {
|
|
737
|
-
const syncUnit = synchronizationUnits.find(
|
|
738
|
-
(s) =>
|
|
739
|
-
revision.driveId === s.driveId &&
|
|
740
|
-
revision.documentId === s.documentId &&
|
|
741
|
-
revision.scope === s.scope &&
|
|
742
|
-
revision.branch === s.branch,
|
|
743
|
-
);
|
|
744
|
-
if (syncUnit) {
|
|
745
|
-
syncUnit.revision = revision.revision;
|
|
746
|
-
syncUnit.lastUpdated = revision.lastUpdated;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
return synchronizationUnits;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
public async getSynchronizationUnitsIds(
|
|
753
|
-
driveId: string,
|
|
754
|
-
documentId?: string[],
|
|
755
|
-
scope?: string[],
|
|
756
|
-
branch?: string[],
|
|
757
|
-
documentType?: string[],
|
|
758
|
-
loadedDrive?: DocumentDriveDocument,
|
|
759
|
-
): Promise<SynchronizationUnitQuery[]> {
|
|
760
|
-
const drive = loadedDrive ?? (await this.getDrive(driveId));
|
|
761
|
-
const nodes = drive.state.global.nodes.filter(
|
|
762
|
-
(node) =>
|
|
763
|
-
isFileNode(node) &&
|
|
764
|
-
(!documentId?.length ||
|
|
765
|
-
documentId.includes(node.id) ||
|
|
766
|
-
documentId.includes("*")) &&
|
|
767
|
-
(!documentType?.length ||
|
|
768
|
-
documentType.includes(node.documentType) ||
|
|
769
|
-
documentType.includes("*")),
|
|
770
|
-
) as Pick<FileNode, "id" | "documentType" | "synchronizationUnits">[];
|
|
771
|
-
|
|
772
|
-
// checks if document drive synchronization unit should be added
|
|
773
|
-
if (
|
|
774
|
-
(!documentId || documentId.includes("*") || documentId.includes("")) &&
|
|
775
|
-
(!documentType?.length ||
|
|
776
|
-
documentType.includes("powerhouse/document-drive") ||
|
|
777
|
-
documentType.includes("*"))
|
|
778
|
-
) {
|
|
779
|
-
nodes.unshift({
|
|
780
|
-
id: "",
|
|
781
|
-
documentType: "powerhouse/document-drive",
|
|
782
|
-
synchronizationUnits: [
|
|
783
|
-
{
|
|
784
|
-
syncId: "0",
|
|
785
|
-
scope: "global",
|
|
786
|
-
branch: "main",
|
|
787
|
-
},
|
|
788
|
-
],
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
const synchronizationUnitsQuery: Omit<
|
|
793
|
-
SynchronizationUnit,
|
|
794
|
-
"revision" | "lastUpdated"
|
|
795
|
-
>[] = [];
|
|
796
|
-
for (const node of nodes) {
|
|
797
|
-
const nodeUnits =
|
|
798
|
-
scope?.length || branch?.length
|
|
799
|
-
? node.synchronizationUnits.filter(
|
|
800
|
-
(unit) =>
|
|
801
|
-
(!scope?.length ||
|
|
802
|
-
scope.includes(unit.scope) ||
|
|
803
|
-
scope.includes("*")) &&
|
|
804
|
-
(!branch?.length ||
|
|
805
|
-
branch.includes(unit.branch) ||
|
|
806
|
-
branch.includes("*")),
|
|
807
|
-
)
|
|
808
|
-
: node.synchronizationUnits;
|
|
809
|
-
if (!nodeUnits.length) {
|
|
810
|
-
continue;
|
|
811
|
-
}
|
|
812
|
-
synchronizationUnitsQuery.push(
|
|
813
|
-
...nodeUnits.map((n) => ({
|
|
814
|
-
driveId,
|
|
815
|
-
documentId: node.id,
|
|
816
|
-
syncId: n.syncId,
|
|
817
|
-
documentType: node.documentType,
|
|
818
|
-
scope: n.scope,
|
|
819
|
-
branch: n.branch,
|
|
820
|
-
})),
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
return synchronizationUnitsQuery;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
public async getSynchronizationUnitIdInfo(
|
|
827
|
-
driveId: string,
|
|
828
|
-
syncId: string,
|
|
829
|
-
loadedDrive?: DocumentDriveDocument,
|
|
830
|
-
): Promise<SynchronizationUnitQuery | undefined> {
|
|
831
|
-
const drive = loadedDrive || (await this.getDrive(driveId));
|
|
832
|
-
const node = drive.state.global.nodes.find(
|
|
833
|
-
(node) =>
|
|
834
|
-
isFileNode(node) &&
|
|
835
|
-
node.synchronizationUnits.find((unit) => unit.syncId === syncId),
|
|
836
|
-
);
|
|
837
|
-
|
|
838
|
-
if (!node || !isFileNode(node)) {
|
|
839
|
-
return undefined;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
const syncUnit = node.synchronizationUnits.find(
|
|
843
|
-
(unit) => unit.syncId === syncId,
|
|
844
|
-
);
|
|
845
|
-
if (!syncUnit) {
|
|
846
|
-
return undefined;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
return {
|
|
850
|
-
syncId,
|
|
851
|
-
scope: syncUnit.scope,
|
|
852
|
-
branch: syncUnit.branch,
|
|
853
|
-
driveId,
|
|
854
|
-
documentId: node.id,
|
|
855
|
-
documentType: node.documentType,
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
public async getSynchronizationUnit(
|
|
860
|
-
driveId: string,
|
|
861
|
-
syncId: string,
|
|
862
|
-
loadedDrive?: DocumentDriveDocument,
|
|
863
|
-
): Promise<SynchronizationUnit | undefined> {
|
|
864
|
-
const syncUnit = await this.getSynchronizationUnitIdInfo(
|
|
865
|
-
driveId,
|
|
866
|
-
syncId,
|
|
867
|
-
loadedDrive,
|
|
868
|
-
);
|
|
869
|
-
|
|
870
|
-
if (!syncUnit) {
|
|
871
|
-
return undefined;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
const { scope, branch, documentId, documentType } = syncUnit;
|
|
875
|
-
|
|
876
|
-
// TODO: REPLACE WITH GET DOCUMENT OPERATIONS
|
|
877
|
-
const document = await this.getDocument(driveId, documentId);
|
|
878
|
-
const operations = document.operations[scope as OperationScope] ?? [];
|
|
879
|
-
const lastOperation = operations[operations.length - 1];
|
|
880
|
-
|
|
881
|
-
return {
|
|
882
|
-
syncId,
|
|
883
|
-
scope,
|
|
884
|
-
branch,
|
|
885
|
-
driveId,
|
|
886
|
-
documentId,
|
|
887
|
-
documentType,
|
|
888
|
-
lastUpdated: lastOperation?.timestamp ?? document.lastModified,
|
|
889
|
-
revision: lastOperation?.index ?? 0,
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
async getOperationData(
|
|
894
|
-
driveId: string,
|
|
895
|
-
syncId: string,
|
|
896
|
-
filter: GetStrandsOptions,
|
|
897
|
-
loadedDrive?: DocumentDriveDocument,
|
|
898
|
-
): Promise<OperationUpdate[]> {
|
|
899
|
-
const syncUnit =
|
|
900
|
-
syncId === "0"
|
|
901
|
-
? { documentId: "", scope: "global" }
|
|
902
|
-
: await this.getSynchronizationUnitIdInfo(driveId, syncId, loadedDrive);
|
|
903
|
-
|
|
904
|
-
if (!syncUnit) {
|
|
905
|
-
throw new Error(`Invalid Sync Id ${syncId} in drive ${driveId}`);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
const document =
|
|
909
|
-
syncId === "0"
|
|
910
|
-
? loadedDrive || (await this.getDrive(driveId))
|
|
911
|
-
: await this.getDocument(driveId, syncUnit.documentId); // TODO replace with getDocumentOperations
|
|
912
|
-
|
|
913
|
-
const operations =
|
|
914
|
-
document.operations[syncUnit.scope as OperationScope] ?? []; // TODO filter by branch also
|
|
915
|
-
|
|
916
|
-
const filteredOperations = operations.filter(
|
|
917
|
-
(operation) =>
|
|
918
|
-
Object.keys(filter).length === 0 ||
|
|
919
|
-
((filter.since === undefined ||
|
|
920
|
-
isBefore(filter.since, operation.timestamp)) &&
|
|
921
|
-
(filter.fromRevision === undefined ||
|
|
922
|
-
operation.index > filter.fromRevision)),
|
|
923
|
-
);
|
|
924
|
-
|
|
925
|
-
const limitedOperations = filter.limit
|
|
926
|
-
? filteredOperations.slice(0, filter.limit)
|
|
927
|
-
: filteredOperations;
|
|
928
|
-
|
|
929
|
-
return limitedOperations.map((operation) => ({
|
|
930
|
-
hash: operation.hash,
|
|
931
|
-
index: operation.index,
|
|
932
|
-
timestamp: operation.timestamp,
|
|
933
|
-
type: operation.type,
|
|
934
|
-
input: operation.input as object,
|
|
935
|
-
skip: operation.skip,
|
|
936
|
-
context: operation.context,
|
|
937
|
-
id: operation.id,
|
|
938
|
-
}));
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
protected getDocumentModel(documentType: string) {
|
|
942
|
-
const documentModel = this.documentModels.find(
|
|
943
|
-
(model) => model.documentModel.id === documentType,
|
|
944
|
-
);
|
|
945
|
-
if (!documentModel) {
|
|
946
|
-
throw new Error(`Document type ${documentType} not supported`);
|
|
947
|
-
}
|
|
948
|
-
return documentModel;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
getDocumentModels() {
|
|
952
|
-
return [...this.documentModels];
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
async addDrive(input: DriveInput): Promise<DocumentDriveDocument> {
|
|
956
|
-
const id = input.global.id || generateUUID();
|
|
957
|
-
if (!id) {
|
|
958
|
-
throw new Error("Invalid Drive Id");
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
const drives = await this.storage.getDrives();
|
|
962
|
-
if (drives.includes(id)) {
|
|
963
|
-
throw new DriveAlreadyExistsError(id);
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
const document = utils.createDocument({
|
|
967
|
-
state: input,
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
await this.storage.createDrive(id, document);
|
|
971
|
-
|
|
972
|
-
if (input.global.slug) {
|
|
973
|
-
await this.cache.deleteDocument("drives-slug", input.global.slug);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
await this._initializeDrive(id);
|
|
977
|
-
|
|
978
|
-
this.emit("driveAdded", document);
|
|
979
|
-
|
|
980
|
-
return document;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
async addRemoteDrive(
|
|
984
|
-
url: string,
|
|
985
|
-
options: RemoteDriveOptions,
|
|
986
|
-
): Promise<DocumentDriveDocument> {
|
|
987
|
-
const { id, name, slug, icon } =
|
|
988
|
-
options.expectedDriveInfo || (await requestPublicDrive(url));
|
|
989
|
-
|
|
990
|
-
const {
|
|
991
|
-
pullFilter,
|
|
992
|
-
pullInterval,
|
|
993
|
-
availableOffline,
|
|
994
|
-
sharingType,
|
|
995
|
-
listeners,
|
|
996
|
-
triggers,
|
|
997
|
-
} = options;
|
|
998
|
-
|
|
999
|
-
const pullTrigger =
|
|
1000
|
-
await PullResponderTransmitter.createPullResponderTrigger(id, url, {
|
|
1001
|
-
pullFilter,
|
|
1002
|
-
pullInterval,
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
return await this.addDrive({
|
|
1006
|
-
global: {
|
|
1007
|
-
id: id,
|
|
1008
|
-
name,
|
|
1009
|
-
slug,
|
|
1010
|
-
icon: icon ?? null,
|
|
1011
|
-
},
|
|
1012
|
-
local: {
|
|
1013
|
-
triggers: [...triggers, pullTrigger],
|
|
1014
|
-
listeners: listeners,
|
|
1015
|
-
availableOffline,
|
|
1016
|
-
sharingType,
|
|
1017
|
-
},
|
|
1018
|
-
});
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
public async registerPullResponderTrigger(
|
|
1022
|
-
driveId: string,
|
|
1023
|
-
url: string,
|
|
1024
|
-
options: Pick<RemoteDriveOptions, "pullFilter" | "pullInterval">,
|
|
1025
|
-
) {
|
|
1026
|
-
const pullTrigger =
|
|
1027
|
-
await PullResponderTransmitter.createPullResponderTrigger(
|
|
1028
|
-
driveId,
|
|
1029
|
-
url,
|
|
1030
|
-
options,
|
|
1031
|
-
);
|
|
1032
|
-
|
|
1033
|
-
return pullTrigger;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
async deleteDrive(driveId: string) {
|
|
1037
|
-
const result = await Promise.allSettled([
|
|
1038
|
-
this.stopSyncRemoteDrive(driveId),
|
|
1039
|
-
this.listenerManager.removeDrive(driveId),
|
|
1040
|
-
this.cache.deleteDocument("drives", driveId),
|
|
1041
|
-
this.storage.deleteDrive(driveId),
|
|
1042
|
-
]);
|
|
1043
|
-
|
|
1044
|
-
result.forEach((r) => {
|
|
1045
|
-
if (r.status === "rejected") {
|
|
1046
|
-
throw r.reason;
|
|
1047
|
-
}
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
getDrives() {
|
|
1052
|
-
return this.storage.getDrives();
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
async getDrive(driveId: string, options?: GetDocumentOptions) {
|
|
1056
|
-
let document: DocumentDriveDocument | undefined;
|
|
1057
|
-
try {
|
|
1058
|
-
const cachedDocument = await this.cache.getDocument("drives", driveId); // TODO support GetDocumentOptions
|
|
1059
|
-
if (cachedDocument && isDocumentDrive(cachedDocument)) {
|
|
1060
|
-
document = cachedDocument;
|
|
1061
|
-
if (isAtRevision(document, options?.revisions)) {
|
|
1062
|
-
return document;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
} catch (e) {
|
|
1066
|
-
logger.error("Error getting drive from cache", e);
|
|
1067
|
-
}
|
|
1068
|
-
const driveStorage = document ?? (await this.storage.getDrive(driveId));
|
|
1069
|
-
const result = this._buildDocument(driveStorage, options);
|
|
1070
|
-
if (!isDocumentDrive(result)) {
|
|
1071
|
-
throw new Error(`Document with id ${driveId} is not a Document Drive`);
|
|
1072
|
-
} else {
|
|
1073
|
-
if (!options?.revisions) {
|
|
1074
|
-
this.cache.setDocument("drives", driveId, result).catch(logger.error);
|
|
1075
|
-
}
|
|
1076
|
-
return result;
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
async getDriveBySlug(slug: string, options?: GetDocumentOptions) {
|
|
1081
|
-
try {
|
|
1082
|
-
const document = await this.cache.getDocument("drives-slug", slug);
|
|
1083
|
-
if (document && isDocumentDrive(document)) {
|
|
1084
|
-
return document;
|
|
1085
|
-
}
|
|
1086
|
-
} catch (e) {
|
|
1087
|
-
logger.error("Error getting drive from cache", e);
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
const driveStorage = await this.storage.getDriveBySlug(slug);
|
|
1091
|
-
const document = this._buildDocument(driveStorage, options);
|
|
1092
|
-
if (!isDocumentDrive(document)) {
|
|
1093
|
-
throw new Error(`Document with slug ${slug} is not a Document Drive`);
|
|
1094
|
-
} else {
|
|
1095
|
-
this.cache.setDocument("drives-slug", slug, document).catch(logger.error);
|
|
1096
|
-
return document;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
async getDocument(
|
|
1101
|
-
driveId: string,
|
|
1102
|
-
documentId: string,
|
|
1103
|
-
options?: GetDocumentOptions,
|
|
1104
|
-
) {
|
|
1105
|
-
let cachedDocument: Document | undefined;
|
|
1106
|
-
try {
|
|
1107
|
-
cachedDocument = await this.cache.getDocument(driveId, documentId); // TODO support GetDocumentOptions
|
|
1108
|
-
if (cachedDocument && isAtRevision(cachedDocument, options?.revisions)) {
|
|
1109
|
-
return cachedDocument;
|
|
1110
|
-
}
|
|
1111
|
-
} catch (e) {
|
|
1112
|
-
logger.error("Error getting document from cache", e);
|
|
1113
|
-
}
|
|
1114
|
-
const documentStorage =
|
|
1115
|
-
cachedDocument ?? (await this.storage.getDocument(driveId, documentId));
|
|
1116
|
-
const document = this._buildDocument(documentStorage, options);
|
|
1117
|
-
|
|
1118
|
-
if (!options?.revisions) {
|
|
1119
|
-
this.cache.setDocument(driveId, documentId, document).catch(logger.error);
|
|
1120
|
-
}
|
|
1121
|
-
return document;
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
getDocuments(driveId: string) {
|
|
1125
|
-
return this.storage.getDocuments(driveId);
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
protected async createDocument(driveId: string, input: CreateDocumentInput) {
|
|
1129
|
-
// if a document was provided then checks if it's valid
|
|
1130
|
-
let state = undefined;
|
|
1131
|
-
if (input.document) {
|
|
1132
|
-
if (input.documentType !== input.document.documentType) {
|
|
1133
|
-
throw new Error(`Provided document is not ${input.documentType}`);
|
|
1134
|
-
}
|
|
1135
|
-
const doc = this._buildDocument(input.document);
|
|
1136
|
-
state = doc.state;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// if no document was provided then create a new one
|
|
1140
|
-
const document =
|
|
1141
|
-
input.document ??
|
|
1142
|
-
this.getDocumentModel(input.documentType).utils.createDocument();
|
|
1143
|
-
|
|
1144
|
-
// stores document information
|
|
1145
|
-
const documentStorage: DocumentStorage = {
|
|
1146
|
-
name: document.name,
|
|
1147
|
-
revision: document.revision,
|
|
1148
|
-
documentType: document.documentType,
|
|
1149
|
-
created: document.created,
|
|
1150
|
-
lastModified: document.lastModified,
|
|
1151
|
-
operations: { global: [], local: [] },
|
|
1152
|
-
initialState: document.initialState,
|
|
1153
|
-
clipboard: [],
|
|
1154
|
-
state: state ?? document.state,
|
|
1155
|
-
};
|
|
1156
|
-
await this.storage.createDocument(driveId, input.id, documentStorage);
|
|
1157
|
-
|
|
1158
|
-
// set initial state for new syncUnits
|
|
1159
|
-
for (const syncUnit of input.synchronizationUnits) {
|
|
1160
|
-
this.initSyncStatus(syncUnit.syncId, {
|
|
1161
|
-
pull: this.triggerMap.get(driveId) ? "INITIAL_SYNC" : undefined,
|
|
1162
|
-
push: this.listenerManager.driveHasListeners(driveId)
|
|
1163
|
-
? "SUCCESS"
|
|
1164
|
-
: undefined,
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// if the document contains operations then
|
|
1169
|
-
// stores the operations in the storage
|
|
1170
|
-
const operations = Object.values(document.operations).flat();
|
|
1171
|
-
if (operations.length) {
|
|
1172
|
-
if (isDocumentDrive(document)) {
|
|
1173
|
-
await this.storage.addDriveOperations(
|
|
1174
|
-
driveId,
|
|
1175
|
-
operations as Operation<DocumentDriveAction>[],
|
|
1176
|
-
document,
|
|
1177
|
-
);
|
|
1178
|
-
} else {
|
|
1179
|
-
await this.storage.addDocumentOperations(
|
|
1180
|
-
driveId,
|
|
1181
|
-
input.id,
|
|
1182
|
-
operations,
|
|
1183
|
-
document,
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
return document;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
async deleteDocument(driveId: string, documentId: string) {
|
|
1192
|
-
try {
|
|
1193
|
-
const syncUnits = await this.getSynchronizationUnitsIds(driveId, [
|
|
1194
|
-
documentId,
|
|
1195
|
-
]);
|
|
1196
|
-
|
|
1197
|
-
// remove document sync units status when a document is deleted
|
|
1198
|
-
for (const syncUnit of syncUnits) {
|
|
1199
|
-
this.updateSyncUnitStatus(syncUnit.syncId, null);
|
|
1200
|
-
}
|
|
1201
|
-
await this.listenerManager.removeSyncUnits(driveId, syncUnits);
|
|
1202
|
-
} catch (error) {
|
|
1203
|
-
logger.warn("Error deleting document", error);
|
|
1204
|
-
}
|
|
1205
|
-
await this.cache.deleteDocument(driveId, documentId);
|
|
1206
|
-
return this.storage.deleteDocument(driveId, documentId);
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
async _processOperations<T extends Document, A extends Action>(
|
|
1210
|
-
driveId: string,
|
|
1211
|
-
documentId: string | undefined,
|
|
1212
|
-
documentStorage: DocumentStorage<T>,
|
|
1213
|
-
operations: Operation<A | BaseAction>[],
|
|
1214
|
-
) {
|
|
1215
|
-
const operationsApplied: Operation<A | BaseAction>[] = [];
|
|
1216
|
-
const signals: SignalResult[] = [];
|
|
1217
|
-
|
|
1218
|
-
const documentStorageWithState = await this._addDocumentResultingStage(
|
|
1219
|
-
documentStorage,
|
|
1220
|
-
driveId,
|
|
1221
|
-
documentId,
|
|
1222
|
-
);
|
|
1223
|
-
|
|
1224
|
-
let document: T = this._buildDocument(documentStorageWithState);
|
|
1225
|
-
let error: OperationError | undefined; // TODO: replace with an array of errors/consistency issues
|
|
1226
|
-
const operationsByScope = groupOperationsByScope(operations);
|
|
1227
|
-
|
|
1228
|
-
for (const scope of Object.keys(operationsByScope)) {
|
|
1229
|
-
const storageDocumentOperations =
|
|
1230
|
-
documentStorage.operations[scope as OperationScope];
|
|
1231
|
-
|
|
1232
|
-
// TODO two equal operations done by two clients will be considered the same, ie: { type: "INCREMENT" }
|
|
1233
|
-
const branch = removeExistingOperations(
|
|
1234
|
-
operationsByScope[scope as OperationScope] || [],
|
|
1235
|
-
storageDocumentOperations,
|
|
1236
|
-
);
|
|
1237
|
-
|
|
1238
|
-
// No operations to apply
|
|
1239
|
-
if (branch.length < 1) {
|
|
1240
|
-
continue;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
const trunk = garbageCollect(sortOperations(storageDocumentOperations));
|
|
1244
|
-
|
|
1245
|
-
const [invertedTrunk, tail] = attachBranch(trunk, branch);
|
|
1246
|
-
|
|
1247
|
-
const newHistory =
|
|
1248
|
-
tail.length < 1
|
|
1249
|
-
? invertedTrunk
|
|
1250
|
-
: merge(trunk, invertedTrunk, reshuffleByTimestamp);
|
|
1251
|
-
|
|
1252
|
-
const newOperations = newHistory.filter(
|
|
1253
|
-
(op) => trunk.length < 1 || precedes(trunk[trunk.length - 1]!, op),
|
|
1254
|
-
);
|
|
1255
|
-
|
|
1256
|
-
for (const nextOperation of newOperations) {
|
|
1257
|
-
let skipHashValidation = false;
|
|
1258
|
-
|
|
1259
|
-
// when dealing with a merge (tail.length > 0) we have to skip hash validation
|
|
1260
|
-
// for the operations that were re-indexed (previous hash becomes invalid due the new position in the history)
|
|
1261
|
-
if (tail.length > 0) {
|
|
1262
|
-
const sourceOperation = operations.find(
|
|
1263
|
-
(op) => op.hash === nextOperation.hash,
|
|
1264
|
-
);
|
|
1265
|
-
|
|
1266
|
-
skipHashValidation =
|
|
1267
|
-
!sourceOperation ||
|
|
1268
|
-
sourceOperation.index !== nextOperation.index ||
|
|
1269
|
-
sourceOperation.skip !== nextOperation.skip;
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
try {
|
|
1273
|
-
// runs operation on next available tick, to avoid blocking the main thread
|
|
1274
|
-
const taskQueueMethod = this.options.taskQueueMethod;
|
|
1275
|
-
const task = () =>
|
|
1276
|
-
this._performOperation(
|
|
1277
|
-
driveId,
|
|
1278
|
-
documentId,
|
|
1279
|
-
document,
|
|
1280
|
-
nextOperation,
|
|
1281
|
-
skipHashValidation,
|
|
1282
|
-
);
|
|
1283
|
-
const appliedResult = await (taskQueueMethod
|
|
1284
|
-
? runAsapAsync(task, taskQueueMethod)
|
|
1285
|
-
: task());
|
|
1286
|
-
document = appliedResult.document;
|
|
1287
|
-
signals.push(...appliedResult.signals);
|
|
1288
|
-
operationsApplied.push(appliedResult.operation);
|
|
1289
|
-
|
|
1290
|
-
// TODO what to do if one of the applied operations has an error?
|
|
1291
|
-
} catch (e) {
|
|
1292
|
-
error =
|
|
1293
|
-
e instanceof OperationError
|
|
1294
|
-
? e
|
|
1295
|
-
: new OperationError(
|
|
1296
|
-
"ERROR",
|
|
1297
|
-
nextOperation,
|
|
1298
|
-
(e as Error).message,
|
|
1299
|
-
(e as Error).cause,
|
|
1300
|
-
);
|
|
1301
|
-
|
|
1302
|
-
// TODO: don't break on errors...
|
|
1303
|
-
break;
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
return {
|
|
1309
|
-
document,
|
|
1310
|
-
operationsApplied,
|
|
1311
|
-
signals,
|
|
1312
|
-
error,
|
|
1313
|
-
} as const;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
private async _addDocumentResultingStage<T extends Document>(
|
|
1317
|
-
document: DocumentStorage<T>,
|
|
1318
|
-
driveId: string,
|
|
1319
|
-
documentId?: string,
|
|
1320
|
-
options?: GetDocumentOptions,
|
|
1321
|
-
): Promise<DocumentStorage<T>> {
|
|
1322
|
-
// apply skip header operations to all scopes
|
|
1323
|
-
const operations =
|
|
1324
|
-
options?.revisions !== undefined
|
|
1325
|
-
? filterOperationsByRevision(document.operations, options.revisions)
|
|
1326
|
-
: document.operations;
|
|
1327
|
-
const documentOperations =
|
|
1328
|
-
DocumentUtils.documentHelpers.garbageCollectDocumentOperations(
|
|
1329
|
-
operations,
|
|
1330
|
-
);
|
|
1331
|
-
|
|
1332
|
-
for (const scope of Object.keys(documentOperations)) {
|
|
1333
|
-
const lastRemainingOperation =
|
|
1334
|
-
documentOperations[scope as OperationScope].at(-1);
|
|
1335
|
-
// if the latest operation doesn't have a resulting state then tries
|
|
1336
|
-
// to retrieve it from the db to avoid rerunning all the operations
|
|
1337
|
-
if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
|
|
1338
|
-
lastRemainingOperation.resultingState = await (documentId
|
|
1339
|
-
? this.storage.getOperationResultingState?.(
|
|
1340
|
-
driveId,
|
|
1341
|
-
documentId,
|
|
1342
|
-
lastRemainingOperation.index,
|
|
1343
|
-
lastRemainingOperation.scope,
|
|
1344
|
-
"main",
|
|
1345
|
-
)
|
|
1346
|
-
: this.storage.getDriveOperationResultingState?.(
|
|
1347
|
-
driveId,
|
|
1348
|
-
lastRemainingOperation.index,
|
|
1349
|
-
lastRemainingOperation.scope,
|
|
1350
|
-
"main",
|
|
1351
|
-
));
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
return {
|
|
1356
|
-
...document,
|
|
1357
|
-
operations: documentOperations,
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
private _buildDocument<T extends Document>(
|
|
1362
|
-
documentStorage: DocumentStorage<T>,
|
|
1363
|
-
options?: GetDocumentOptions,
|
|
1364
|
-
): T {
|
|
1365
|
-
if (
|
|
1366
|
-
documentStorage.state &&
|
|
1367
|
-
(!options || options.checkHashes === false) &&
|
|
1368
|
-
isAtRevision(documentStorage as unknown as Document, options?.revisions)
|
|
1369
|
-
) {
|
|
1370
|
-
return documentStorage as T;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
const documentModel = this.getDocumentModel(documentStorage.documentType);
|
|
1374
|
-
|
|
1375
|
-
const revisionOperations =
|
|
1376
|
-
options?.revisions !== undefined
|
|
1377
|
-
? filterOperationsByRevision(
|
|
1378
|
-
documentStorage.operations,
|
|
1379
|
-
options.revisions,
|
|
1380
|
-
)
|
|
1381
|
-
: documentStorage.operations;
|
|
1382
|
-
const operations =
|
|
1383
|
-
baseUtils.documentHelpers.garbageCollectDocumentOperations(
|
|
1384
|
-
revisionOperations,
|
|
1385
|
-
);
|
|
1386
|
-
|
|
1387
|
-
return baseUtils.replayDocument(
|
|
1388
|
-
documentStorage.initialState,
|
|
1389
|
-
operations,
|
|
1390
|
-
documentModel.reducer,
|
|
1391
|
-
undefined,
|
|
1392
|
-
documentStorage,
|
|
1393
|
-
undefined,
|
|
1394
|
-
{
|
|
1395
|
-
...options,
|
|
1396
|
-
checkHashes: options?.checkHashes ?? true,
|
|
1397
|
-
reuseOperationResultingState: options?.checkHashes ?? true,
|
|
1398
|
-
},
|
|
1399
|
-
) as T;
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
private async _performOperation<T extends Document>(
|
|
1403
|
-
driveId: string,
|
|
1404
|
-
documentId: string | undefined,
|
|
1405
|
-
document: T,
|
|
1406
|
-
operation: Operation,
|
|
1407
|
-
skipHashValidation = false,
|
|
1408
|
-
) {
|
|
1409
|
-
const documentModel = this.getDocumentModel(document.documentType);
|
|
1410
|
-
|
|
1411
|
-
const signalResults: SignalResult[] = [];
|
|
1412
|
-
let newDocument = document;
|
|
1413
|
-
|
|
1414
|
-
const scope = operation.scope;
|
|
1415
|
-
const documentOperations =
|
|
1416
|
-
DocumentUtils.documentHelpers.garbageCollectDocumentOperations({
|
|
1417
|
-
...document.operations,
|
|
1418
|
-
[scope]: DocumentUtils.documentHelpers.skipHeaderOperations(
|
|
1419
|
-
document.operations[scope],
|
|
1420
|
-
operation,
|
|
1421
|
-
),
|
|
1422
|
-
});
|
|
1423
|
-
|
|
1424
|
-
const lastRemainingOperation = documentOperations[scope].at(-1);
|
|
1425
|
-
// if the latest operation doesn't have a resulting state then tries
|
|
1426
|
-
// to retrieve it from the db to avoid rerunning all the operations
|
|
1427
|
-
if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
|
|
1428
|
-
lastRemainingOperation.resultingState = await (documentId
|
|
1429
|
-
? this.storage.getOperationResultingState?.(
|
|
1430
|
-
driveId,
|
|
1431
|
-
documentId,
|
|
1432
|
-
lastRemainingOperation.index,
|
|
1433
|
-
lastRemainingOperation.scope,
|
|
1434
|
-
"main",
|
|
1435
|
-
)
|
|
1436
|
-
: this.storage.getDriveOperationResultingState?.(
|
|
1437
|
-
driveId,
|
|
1438
|
-
lastRemainingOperation.index,
|
|
1439
|
-
lastRemainingOperation.scope,
|
|
1440
|
-
"main",
|
|
1441
|
-
));
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
const operationSignals: (() => Promise<SignalResult>)[] = [];
|
|
1445
|
-
newDocument = documentModel.reducer(
|
|
1446
|
-
newDocument,
|
|
1447
|
-
operation,
|
|
1448
|
-
(signal) => {
|
|
1449
|
-
let handler: (() => Promise<unknown>) | undefined = undefined;
|
|
1450
|
-
switch (signal.type) {
|
|
1451
|
-
case "CREATE_CHILD_DOCUMENT":
|
|
1452
|
-
handler = () => this.createDocument(driveId, signal.input);
|
|
1453
|
-
break;
|
|
1454
|
-
case "DELETE_CHILD_DOCUMENT":
|
|
1455
|
-
handler = () => this.deleteDocument(driveId, signal.input.id);
|
|
1456
|
-
break;
|
|
1457
|
-
case "COPY_CHILD_DOCUMENT":
|
|
1458
|
-
handler = () =>
|
|
1459
|
-
this.getDocument(driveId, signal.input.id).then(
|
|
1460
|
-
(documentToCopy) =>
|
|
1461
|
-
this.createDocument(driveId, {
|
|
1462
|
-
id: signal.input.newId,
|
|
1463
|
-
documentType: documentToCopy.documentType,
|
|
1464
|
-
document: documentToCopy,
|
|
1465
|
-
synchronizationUnits: signal.input.synchronizationUnits,
|
|
1466
|
-
}),
|
|
1467
|
-
);
|
|
1468
|
-
break;
|
|
1469
|
-
}
|
|
1470
|
-
if (handler) {
|
|
1471
|
-
operationSignals.push(() =>
|
|
1472
|
-
handler().then((result) => ({ signal, result })),
|
|
1473
|
-
);
|
|
1474
|
-
}
|
|
1475
|
-
},
|
|
1476
|
-
{ skip: operation.skip, reuseOperationResultingState: true },
|
|
1477
|
-
) as T;
|
|
1478
|
-
|
|
1479
|
-
const appliedOperations = newDocument.operations[operation.scope].filter(
|
|
1480
|
-
(op) => op.index == operation.index && op.skip == operation.skip,
|
|
1481
|
-
);
|
|
1482
|
-
const appliedOperation = appliedOperations.at(0);
|
|
1483
|
-
|
|
1484
|
-
if (!appliedOperation) {
|
|
1485
|
-
throw new OperationError(
|
|
1486
|
-
"ERROR",
|
|
1487
|
-
operation,
|
|
1488
|
-
`Operation with index ${operation.index}:${operation.skip} was not applied.`,
|
|
1489
|
-
);
|
|
1490
|
-
}
|
|
1491
|
-
if (
|
|
1492
|
-
!appliedOperation.error &&
|
|
1493
|
-
appliedOperation.hash !== operation.hash &&
|
|
1494
|
-
!skipHashValidation
|
|
1495
|
-
) {
|
|
1496
|
-
throw new ConflictOperationError(operation, appliedOperation);
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
for (const signalHandler of operationSignals) {
|
|
1500
|
-
const result = await signalHandler();
|
|
1501
|
-
signalResults.push(result);
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
return {
|
|
1505
|
-
document: newDocument,
|
|
1506
|
-
signals: signalResults,
|
|
1507
|
-
operation: appliedOperation,
|
|
1508
|
-
};
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
addOperation(
|
|
1512
|
-
driveId: string,
|
|
1513
|
-
documentId: string,
|
|
1514
|
-
operation: Operation,
|
|
1515
|
-
options?: AddOperationOptions,
|
|
1516
|
-
): Promise<IOperationResult> {
|
|
1517
|
-
return this.addOperations(driveId, documentId, [operation], options);
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
private async _addOperations(
|
|
1521
|
-
driveId: string,
|
|
1522
|
-
documentId: string,
|
|
1523
|
-
callback: (document: DocumentStorage) => Promise<{
|
|
1524
|
-
operations: Operation[];
|
|
1525
|
-
header: DocumentHeader;
|
|
1526
|
-
}>,
|
|
1527
|
-
) {
|
|
1528
|
-
if (!this.storage.addDocumentOperationsWithTransaction) {
|
|
1529
|
-
const documentStorage = await this.storage.getDocument(
|
|
1530
|
-
driveId,
|
|
1531
|
-
documentId,
|
|
1532
|
-
);
|
|
1533
|
-
const result = await callback(documentStorage);
|
|
1534
|
-
// saves the applied operations to storage
|
|
1535
|
-
if (result.operations.length > 0) {
|
|
1536
|
-
await this.storage.addDocumentOperations(
|
|
1537
|
-
driveId,
|
|
1538
|
-
documentId,
|
|
1539
|
-
result.operations,
|
|
1540
|
-
result.header,
|
|
1541
|
-
);
|
|
1542
|
-
}
|
|
1543
|
-
} else {
|
|
1544
|
-
await this.storage.addDocumentOperationsWithTransaction(
|
|
1545
|
-
driveId,
|
|
1546
|
-
documentId,
|
|
1547
|
-
callback,
|
|
1548
|
-
);
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
queueOperation(
|
|
1553
|
-
driveId: string,
|
|
1554
|
-
documentId: string,
|
|
1555
|
-
operation: Operation,
|
|
1556
|
-
options?: AddOperationOptions,
|
|
1557
|
-
): Promise<IOperationResult> {
|
|
1558
|
-
return this.queueOperations(driveId, documentId, [operation], options);
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
private async resultIfExistingOperations(
|
|
1562
|
-
drive: string,
|
|
1563
|
-
id: string,
|
|
1564
|
-
operations: Operation[],
|
|
1565
|
-
): Promise<IOperationResult | undefined> {
|
|
1566
|
-
try {
|
|
1567
|
-
const document = await this.getDocument(drive, id);
|
|
1568
|
-
const newOperation = operations.find(
|
|
1569
|
-
(op) =>
|
|
1570
|
-
!op.id ||
|
|
1571
|
-
!document.operations[op.scope].find(
|
|
1572
|
-
(existingOp) =>
|
|
1573
|
-
existingOp.id === op.id &&
|
|
1574
|
-
existingOp.index === op.index &&
|
|
1575
|
-
existingOp.type === op.type &&
|
|
1576
|
-
existingOp.hash === op.hash,
|
|
1577
|
-
),
|
|
1578
|
-
);
|
|
1579
|
-
if (!newOperation) {
|
|
1580
|
-
return {
|
|
1581
|
-
status: "SUCCESS",
|
|
1582
|
-
document,
|
|
1583
|
-
operations,
|
|
1584
|
-
signals: [],
|
|
1585
|
-
};
|
|
1586
|
-
} else {
|
|
1587
|
-
return undefined;
|
|
1588
|
-
}
|
|
1589
|
-
} catch (error) {
|
|
1590
|
-
if (
|
|
1591
|
-
!(error as Error).message.includes(`Document with id ${id} not found`)
|
|
1592
|
-
) {
|
|
1593
|
-
console.error(error);
|
|
1594
|
-
}
|
|
1595
|
-
return undefined;
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
async queueOperations(
|
|
1600
|
-
driveId: string,
|
|
1601
|
-
documentId: string,
|
|
1602
|
-
operations: Operation[],
|
|
1603
|
-
options?: AddOperationOptions,
|
|
1604
|
-
) {
|
|
1605
|
-
// if operations are already stored then returns cached document
|
|
1606
|
-
const result = await this.resultIfExistingOperations(
|
|
1607
|
-
driveId,
|
|
1608
|
-
documentId,
|
|
1609
|
-
operations,
|
|
1610
|
-
);
|
|
1611
|
-
if (result) {
|
|
1612
|
-
return result;
|
|
1613
|
-
}
|
|
1614
|
-
try {
|
|
1615
|
-
const jobId = await this.queueManager.addJob({
|
|
1616
|
-
driveId: driveId,
|
|
1617
|
-
documentId: documentId,
|
|
1618
|
-
operations,
|
|
1619
|
-
options,
|
|
1620
|
-
});
|
|
1621
|
-
|
|
1622
|
-
return new Promise<IOperationResult>((resolve, reject) => {
|
|
1623
|
-
const unsubscribe = this.queueManager.on(
|
|
1624
|
-
"jobCompleted",
|
|
1625
|
-
(job, result) => {
|
|
1626
|
-
if (job.jobId === jobId) {
|
|
1627
|
-
unsubscribe();
|
|
1628
|
-
unsubscribeError();
|
|
1629
|
-
resolve(result);
|
|
1630
|
-
}
|
|
1631
|
-
},
|
|
1632
|
-
);
|
|
1633
|
-
const unsubscribeError = this.queueManager.on(
|
|
1634
|
-
"jobFailed",
|
|
1635
|
-
(job, error) => {
|
|
1636
|
-
if (job.jobId === jobId) {
|
|
1637
|
-
unsubscribe();
|
|
1638
|
-
unsubscribeError();
|
|
1639
|
-
reject(error);
|
|
1640
|
-
}
|
|
1641
|
-
},
|
|
1642
|
-
);
|
|
1643
|
-
});
|
|
1644
|
-
} catch (error) {
|
|
1645
|
-
logger.error("Error adding job", error);
|
|
1646
|
-
throw error;
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
async queueAction(
|
|
1651
|
-
driveId: string,
|
|
1652
|
-
documentId: string,
|
|
1653
|
-
action: Action,
|
|
1654
|
-
options?: AddOperationOptions,
|
|
1655
|
-
): Promise<IOperationResult> {
|
|
1656
|
-
return this.queueActions(driveId, documentId, [action], options);
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
async queueActions(
|
|
1660
|
-
driveId: string,
|
|
1661
|
-
documentId: string,
|
|
1662
|
-
actions: Action[],
|
|
1663
|
-
options?: AddOperationOptions,
|
|
1664
|
-
): Promise<IOperationResult> {
|
|
1665
|
-
try {
|
|
1666
|
-
const jobId = await this.queueManager.addJob({
|
|
1667
|
-
driveId: driveId,
|
|
1668
|
-
documentId: documentId,
|
|
1669
|
-
actions,
|
|
1670
|
-
options,
|
|
1671
|
-
});
|
|
1672
|
-
|
|
1673
|
-
return new Promise<IOperationResult>((resolve, reject) => {
|
|
1674
|
-
const unsubscribe = this.queueManager.on(
|
|
1675
|
-
"jobCompleted",
|
|
1676
|
-
(job, result) => {
|
|
1677
|
-
if (job.jobId === jobId) {
|
|
1678
|
-
unsubscribe();
|
|
1679
|
-
unsubscribeError();
|
|
1680
|
-
resolve(result);
|
|
1681
|
-
}
|
|
1682
|
-
},
|
|
1683
|
-
);
|
|
1684
|
-
const unsubscribeError = this.queueManager.on(
|
|
1685
|
-
"jobFailed",
|
|
1686
|
-
(job, error) => {
|
|
1687
|
-
if (job.jobId === jobId) {
|
|
1688
|
-
unsubscribe();
|
|
1689
|
-
unsubscribeError();
|
|
1690
|
-
reject(error);
|
|
1691
|
-
}
|
|
1692
|
-
},
|
|
1693
|
-
);
|
|
1694
|
-
});
|
|
1695
|
-
} catch (error) {
|
|
1696
|
-
logger.error("Error adding job", error);
|
|
1697
|
-
throw error;
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
async queueDriveAction(
|
|
1702
|
-
driveId: string,
|
|
1703
|
-
action: DocumentDriveAction | BaseAction,
|
|
1704
|
-
options?: AddOperationOptions,
|
|
1705
|
-
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1706
|
-
return this.queueDriveActions(driveId, [action], options);
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
async queueDriveActions(
|
|
1710
|
-
driveId: string,
|
|
1711
|
-
actions: (DocumentDriveAction | BaseAction)[],
|
|
1712
|
-
options?: AddOperationOptions,
|
|
1713
|
-
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1714
|
-
try {
|
|
1715
|
-
const jobId = await this.queueManager.addJob({
|
|
1716
|
-
driveId: driveId,
|
|
1717
|
-
actions,
|
|
1718
|
-
options,
|
|
1719
|
-
});
|
|
1720
|
-
return new Promise<IOperationResult<DocumentDriveDocument>>(
|
|
1721
|
-
(resolve, reject) => {
|
|
1722
|
-
const unsubscribe = this.queueManager.on(
|
|
1723
|
-
"jobCompleted",
|
|
1724
|
-
(job, result) => {
|
|
1725
|
-
if (job.jobId === jobId) {
|
|
1726
|
-
unsubscribe();
|
|
1727
|
-
unsubscribeError();
|
|
1728
|
-
resolve(result as IOperationResult<DocumentDriveDocument>);
|
|
1729
|
-
}
|
|
1730
|
-
},
|
|
1731
|
-
);
|
|
1732
|
-
const unsubscribeError = this.queueManager.on(
|
|
1733
|
-
"jobFailed",
|
|
1734
|
-
(job, error) => {
|
|
1735
|
-
if (job.jobId === jobId) {
|
|
1736
|
-
unsubscribe();
|
|
1737
|
-
unsubscribeError();
|
|
1738
|
-
reject(error);
|
|
1739
|
-
}
|
|
1740
|
-
},
|
|
1741
|
-
);
|
|
1742
|
-
},
|
|
1743
|
-
);
|
|
1744
|
-
} catch (error) {
|
|
1745
|
-
logger.error("Error adding drive job", error);
|
|
1746
|
-
throw error;
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
async addOperations(
|
|
1751
|
-
driveId: string,
|
|
1752
|
-
documentId: string,
|
|
1753
|
-
operations: Operation[],
|
|
1754
|
-
options?: AddOperationOptions,
|
|
1755
|
-
) {
|
|
1756
|
-
// if operations are already stored then returns the result
|
|
1757
|
-
const result = await this.resultIfExistingOperations(
|
|
1758
|
-
driveId,
|
|
1759
|
-
documentId,
|
|
1760
|
-
operations,
|
|
1761
|
-
);
|
|
1762
|
-
if (result) {
|
|
1763
|
-
return result;
|
|
1764
|
-
}
|
|
1765
|
-
let document: Document | undefined;
|
|
1766
|
-
const operationsApplied: Operation[] = [];
|
|
1767
|
-
const signals: SignalResult[] = [];
|
|
1768
|
-
let error: Error | undefined;
|
|
1769
|
-
|
|
1770
|
-
try {
|
|
1771
|
-
await this._addOperations(
|
|
1772
|
-
driveId,
|
|
1773
|
-
documentId,
|
|
1774
|
-
async (documentStorage) => {
|
|
1775
|
-
const result = await this._processOperations(
|
|
1776
|
-
driveId,
|
|
1777
|
-
documentId,
|
|
1778
|
-
documentStorage,
|
|
1779
|
-
operations,
|
|
1780
|
-
);
|
|
1781
|
-
|
|
1782
|
-
if (!result.document) {
|
|
1783
|
-
logger.error("Invalid document");
|
|
1784
|
-
throw result.error ?? new Error("Invalid document");
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
document = result.document;
|
|
1788
|
-
error = result.error;
|
|
1789
|
-
signals.push(...result.signals);
|
|
1790
|
-
operationsApplied.push(...result.operationsApplied);
|
|
1791
|
-
|
|
1792
|
-
return {
|
|
1793
|
-
operations: result.operationsApplied,
|
|
1794
|
-
header: result.document,
|
|
1795
|
-
newState: document.state,
|
|
1796
|
-
};
|
|
1797
|
-
},
|
|
1798
|
-
);
|
|
1799
|
-
|
|
1800
|
-
if (document) {
|
|
1801
|
-
this.cache
|
|
1802
|
-
.setDocument(driveId, documentId, document)
|
|
1803
|
-
.catch(logger.error);
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
// gets all the different scopes and branches combinations from the operations
|
|
1807
|
-
const { scopes, branches } = operationsApplied.reduce(
|
|
1808
|
-
(acc, operation) => {
|
|
1809
|
-
if (!acc.scopes.includes(operation.scope)) {
|
|
1810
|
-
acc.scopes.push(operation.scope);
|
|
1811
|
-
}
|
|
1812
|
-
return acc;
|
|
1813
|
-
},
|
|
1814
|
-
{ scopes: [] as string[], branches: ["main"] },
|
|
1815
|
-
);
|
|
1816
|
-
|
|
1817
|
-
const syncUnits = await this.getSynchronizationUnits(
|
|
1818
|
-
driveId,
|
|
1819
|
-
[documentId],
|
|
1820
|
-
scopes,
|
|
1821
|
-
branches,
|
|
1822
|
-
);
|
|
1823
|
-
|
|
1824
|
-
// checks if any of the provided operations where reshufled
|
|
1825
|
-
const newOp = operationsApplied.find(
|
|
1826
|
-
(appliedOp) =>
|
|
1827
|
-
!operations.find(
|
|
1828
|
-
(o) =>
|
|
1829
|
-
o.id === appliedOp.id &&
|
|
1830
|
-
o.index === appliedOp.index &&
|
|
1831
|
-
o.skip === appliedOp.skip &&
|
|
1832
|
-
o.hash === appliedOp.hash,
|
|
1833
|
-
),
|
|
1834
|
-
);
|
|
1835
|
-
|
|
1836
|
-
// if there are no new operations then reuses the provided source
|
|
1837
|
-
// otherwise sets it to local so listeners know that there were
|
|
1838
|
-
// new changes originating from this document drive server
|
|
1839
|
-
const source: StrandUpdateSource = newOp
|
|
1840
|
-
? { type: "local" }
|
|
1841
|
-
: (options?.source ?? { type: "local" });
|
|
1842
|
-
|
|
1843
|
-
// update listener cache
|
|
1844
|
-
|
|
1845
|
-
const operationSource = this.getOperationSource(source);
|
|
1846
|
-
|
|
1847
|
-
this.listenerManager
|
|
1848
|
-
.updateSynchronizationRevisions(
|
|
1849
|
-
driveId,
|
|
1850
|
-
syncUnits,
|
|
1851
|
-
source,
|
|
1852
|
-
() => {
|
|
1853
|
-
this.updateSyncUnitStatus(driveId, {
|
|
1854
|
-
[operationSource]: "SYNCING",
|
|
1855
|
-
});
|
|
1856
|
-
|
|
1857
|
-
for (const syncUnit of syncUnits) {
|
|
1858
|
-
this.updateSyncUnitStatus(syncUnit.syncId, {
|
|
1859
|
-
[operationSource]: "SYNCING",
|
|
1860
|
-
});
|
|
1861
|
-
}
|
|
1862
|
-
},
|
|
1863
|
-
this.handleListenerError.bind(this),
|
|
1864
|
-
options?.forceSync ?? source.type === "local",
|
|
1865
|
-
)
|
|
1866
|
-
.then((updates) => {
|
|
1867
|
-
if (updates.length) {
|
|
1868
|
-
this.updateSyncUnitStatus(driveId, {
|
|
1869
|
-
[operationSource]: "SUCCESS",
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
for (const syncUnit of syncUnits) {
|
|
1874
|
-
this.updateSyncUnitStatus(syncUnit.syncId, {
|
|
1875
|
-
[operationSource]: "SUCCESS",
|
|
1876
|
-
});
|
|
1877
|
-
}
|
|
1878
|
-
})
|
|
1879
|
-
.catch((error) => {
|
|
1880
|
-
logger.error("Non handled error updating sync revision", error);
|
|
1881
|
-
this.updateSyncUnitStatus(
|
|
1882
|
-
driveId,
|
|
1883
|
-
{
|
|
1884
|
-
[operationSource]: "ERROR",
|
|
1885
|
-
},
|
|
1886
|
-
error as Error,
|
|
1887
|
-
);
|
|
1888
|
-
|
|
1889
|
-
for (const syncUnit of syncUnits) {
|
|
1890
|
-
this.updateSyncUnitStatus(
|
|
1891
|
-
syncUnit.syncId,
|
|
1892
|
-
{
|
|
1893
|
-
[operationSource]: "ERROR",
|
|
1894
|
-
},
|
|
1895
|
-
error as Error,
|
|
1896
|
-
);
|
|
1897
|
-
}
|
|
1898
|
-
});
|
|
1899
|
-
|
|
1900
|
-
// after applying all the valid operations,throws
|
|
1901
|
-
// an error if there was an invalid operation
|
|
1902
|
-
if (error) {
|
|
1903
|
-
throw error;
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
return {
|
|
1907
|
-
status: "SUCCESS",
|
|
1908
|
-
document,
|
|
1909
|
-
operations: operationsApplied,
|
|
1910
|
-
signals,
|
|
1911
|
-
} satisfies IOperationResult;
|
|
1912
|
-
} catch (error) {
|
|
1913
|
-
const operationError =
|
|
1914
|
-
error instanceof OperationError
|
|
1915
|
-
? error
|
|
1916
|
-
: new OperationError(
|
|
1917
|
-
"ERROR",
|
|
1918
|
-
undefined,
|
|
1919
|
-
(error as Error).message,
|
|
1920
|
-
(error as Error).cause,
|
|
1921
|
-
);
|
|
1922
|
-
|
|
1923
|
-
return {
|
|
1924
|
-
status: operationError.status,
|
|
1925
|
-
error: operationError,
|
|
1926
|
-
document,
|
|
1927
|
-
operations: operationsApplied,
|
|
1928
|
-
signals,
|
|
1929
|
-
} satisfies IOperationResult;
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
addDriveOperation(
|
|
1934
|
-
driveId: string,
|
|
1935
|
-
operation: Operation<DocumentDriveAction | BaseAction>,
|
|
1936
|
-
options?: AddOperationOptions,
|
|
1937
|
-
) {
|
|
1938
|
-
return this.addDriveOperations(driveId, [operation], options);
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
async clearStorage() {
|
|
1942
|
-
for (const drive of await this.getDrives()) {
|
|
1943
|
-
await this.deleteDrive(drive);
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
await this.storage.clearStorage?.();
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
private async _addDriveOperations(
|
|
1950
|
-
driveId: string,
|
|
1951
|
-
callback: (document: DocumentDriveStorage) => Promise<{
|
|
1952
|
-
operations: Operation<DocumentDriveAction | BaseAction>[];
|
|
1953
|
-
header: DocumentHeader;
|
|
1954
|
-
}>,
|
|
1955
|
-
) {
|
|
1956
|
-
if (!this.storage.addDriveOperationsWithTransaction) {
|
|
1957
|
-
const documentStorage = await this.storage.getDrive(driveId);
|
|
1958
|
-
const result = await callback(documentStorage);
|
|
1959
|
-
// saves the applied operations to storage
|
|
1960
|
-
if (result.operations.length > 0) {
|
|
1961
|
-
await this.storage.addDriveOperations(
|
|
1962
|
-
driveId,
|
|
1963
|
-
result.operations,
|
|
1964
|
-
result.header,
|
|
1965
|
-
);
|
|
1966
|
-
}
|
|
1967
|
-
return result;
|
|
1968
|
-
} else {
|
|
1969
|
-
return this.storage.addDriveOperationsWithTransaction(driveId, callback);
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
queueDriveOperation(
|
|
1974
|
-
driveId: string,
|
|
1975
|
-
operation: Operation<DocumentDriveAction | BaseAction>,
|
|
1976
|
-
options?: AddOperationOptions,
|
|
1977
|
-
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
1978
|
-
return this.queueDriveOperations(driveId, [operation], options);
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
private async resultIfExistingDriveOperations(
|
|
1982
|
-
driveId: string,
|
|
1983
|
-
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
1984
|
-
): Promise<IOperationResult<DocumentDriveDocument> | undefined> {
|
|
1985
|
-
try {
|
|
1986
|
-
const drive = await this.getDrive(driveId);
|
|
1987
|
-
const newOperation = operations.find(
|
|
1988
|
-
(op) =>
|
|
1989
|
-
!op.id ||
|
|
1990
|
-
!drive.operations[op.scope].find(
|
|
1991
|
-
(existingOp) =>
|
|
1992
|
-
existingOp.id === op.id &&
|
|
1993
|
-
existingOp.index === op.index &&
|
|
1994
|
-
existingOp.type === op.type &&
|
|
1995
|
-
existingOp.hash === op.hash,
|
|
1996
|
-
),
|
|
1997
|
-
);
|
|
1998
|
-
if (!newOperation) {
|
|
1999
|
-
return {
|
|
2000
|
-
status: "SUCCESS",
|
|
2001
|
-
document: drive,
|
|
2002
|
-
operations: operations,
|
|
2003
|
-
signals: [],
|
|
2004
|
-
} as IOperationResult<DocumentDriveDocument>;
|
|
2005
|
-
} else {
|
|
2006
|
-
return undefined;
|
|
2007
|
-
}
|
|
2008
|
-
} catch (error) {
|
|
2009
|
-
console.error(error); // TODO error
|
|
2010
|
-
return undefined;
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
async queueDriveOperations(
|
|
2015
|
-
driveId: string,
|
|
2016
|
-
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
2017
|
-
options?: AddOperationOptions,
|
|
2018
|
-
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
2019
|
-
// if operations are already stored then returns cached document
|
|
2020
|
-
const result = await this.resultIfExistingDriveOperations(
|
|
2021
|
-
driveId,
|
|
2022
|
-
operations,
|
|
2023
|
-
);
|
|
2024
|
-
if (result) {
|
|
2025
|
-
return result;
|
|
2026
|
-
}
|
|
2027
|
-
try {
|
|
2028
|
-
const jobId = await this.queueManager.addJob({
|
|
2029
|
-
driveId: driveId,
|
|
2030
|
-
operations,
|
|
2031
|
-
options,
|
|
2032
|
-
});
|
|
2033
|
-
return new Promise<IOperationResult<DocumentDriveDocument>>(
|
|
2034
|
-
(resolve, reject) => {
|
|
2035
|
-
const unsubscribe = this.queueManager.on(
|
|
2036
|
-
"jobCompleted",
|
|
2037
|
-
(job, result) => {
|
|
2038
|
-
if (job.jobId === jobId) {
|
|
2039
|
-
unsubscribe();
|
|
2040
|
-
unsubscribeError();
|
|
2041
|
-
resolve(result as IOperationResult<DocumentDriveDocument>);
|
|
2042
|
-
}
|
|
2043
|
-
},
|
|
2044
|
-
);
|
|
2045
|
-
const unsubscribeError = this.queueManager.on(
|
|
2046
|
-
"jobFailed",
|
|
2047
|
-
(job, error) => {
|
|
2048
|
-
if (job.jobId === jobId) {
|
|
2049
|
-
unsubscribe();
|
|
2050
|
-
unsubscribeError();
|
|
2051
|
-
reject(error);
|
|
2052
|
-
}
|
|
2053
|
-
},
|
|
2054
|
-
);
|
|
2055
|
-
},
|
|
2056
|
-
);
|
|
2057
|
-
} catch (error) {
|
|
2058
|
-
logger.error("Error adding drive job", error);
|
|
2059
|
-
throw error;
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
async addDriveOperations(
|
|
2064
|
-
driveId: string,
|
|
2065
|
-
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
2066
|
-
options?: AddOperationOptions,
|
|
2067
|
-
) {
|
|
2068
|
-
let document: DocumentDriveDocument | undefined;
|
|
2069
|
-
const operationsApplied: Operation<DocumentDriveAction | BaseAction>[] = [];
|
|
2070
|
-
const signals: SignalResult[] = [];
|
|
2071
|
-
let error: Error | undefined;
|
|
2072
|
-
|
|
2073
|
-
// if operations are already stored then returns cached drive
|
|
2074
|
-
const result = await this.resultIfExistingDriveOperations(
|
|
2075
|
-
driveId,
|
|
2076
|
-
operations,
|
|
2077
|
-
);
|
|
2078
|
-
if (result) {
|
|
2079
|
-
return result;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
try {
|
|
2083
|
-
await this._addDriveOperations(driveId, async (documentStorage) => {
|
|
2084
|
-
const result = await this._processOperations<
|
|
2085
|
-
DocumentDriveDocument,
|
|
2086
|
-
DocumentDriveAction
|
|
2087
|
-
>(driveId, undefined, documentStorage, operations.slice());
|
|
2088
|
-
document = result.document;
|
|
2089
|
-
operationsApplied.push(...result.operationsApplied);
|
|
2090
|
-
signals.push(...result.signals);
|
|
2091
|
-
error = result.error;
|
|
2092
|
-
|
|
2093
|
-
return {
|
|
2094
|
-
operations: result.operationsApplied,
|
|
2095
|
-
header: result.document,
|
|
2096
|
-
};
|
|
2097
|
-
});
|
|
2098
|
-
|
|
2099
|
-
if (!document || !isDocumentDrive(document)) {
|
|
2100
|
-
throw error ?? new Error("Invalid Document Drive document");
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
this.cache.setDocument("drives", driveId, document).catch(logger.error);
|
|
2104
|
-
|
|
2105
|
-
for (const operation of operationsApplied) {
|
|
2106
|
-
switch (operation.type) {
|
|
2107
|
-
case "ADD_LISTENER": {
|
|
2108
|
-
const zodListener = operation.input.listener;
|
|
2109
|
-
|
|
2110
|
-
// create the transmitter
|
|
2111
|
-
const transmitter = this.transmitterFactory.instance(
|
|
2112
|
-
zodListener.callInfo?.transmitterType ?? "",
|
|
2113
|
-
{
|
|
2114
|
-
driveId,
|
|
2115
|
-
listenerId: zodListener.listenerId,
|
|
2116
|
-
block: zodListener.block,
|
|
2117
|
-
filter: zodListener.filter,
|
|
2118
|
-
system: zodListener.system,
|
|
2119
|
-
label: zodListener.label || undefined,
|
|
2120
|
-
callInfo: zodListener.callInfo || undefined,
|
|
2121
|
-
},
|
|
2122
|
-
this,
|
|
2123
|
-
);
|
|
2124
|
-
|
|
2125
|
-
// create the listener
|
|
2126
|
-
const listener = {
|
|
2127
|
-
...zodListener,
|
|
2128
|
-
driveId: driveId,
|
|
2129
|
-
label: zodListener.label ?? "",
|
|
2130
|
-
system: zodListener.system ?? false,
|
|
2131
|
-
filter: {
|
|
2132
|
-
branch: zodListener.filter.branch ?? [],
|
|
2133
|
-
documentId: zodListener.filter.documentId ?? [],
|
|
2134
|
-
documentType: zodListener.filter.documentType ?? [],
|
|
2135
|
-
scope: zodListener.filter.scope ?? [],
|
|
2136
|
-
},
|
|
2137
|
-
callInfo: {
|
|
2138
|
-
data: zodListener.callInfo?.data ?? "",
|
|
2139
|
-
name: zodListener.callInfo?.name ?? "PullResponder",
|
|
2140
|
-
transmitterType:
|
|
2141
|
-
zodListener.callInfo?.transmitterType ?? "PullResponder",
|
|
2142
|
-
},
|
|
2143
|
-
transmitter,
|
|
2144
|
-
};
|
|
2145
|
-
|
|
2146
|
-
await this.addListener(driveId, listener);
|
|
2147
|
-
break;
|
|
2148
|
-
}
|
|
2149
|
-
case "REMOVE_LISTENER": {
|
|
2150
|
-
await this.removeListener(driveId, operation);
|
|
2151
|
-
break;
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
// update listener cache
|
|
2157
|
-
const lastOperation = operationsApplied
|
|
2158
|
-
.filter((op) => op.scope === "global")
|
|
2159
|
-
.slice()
|
|
2160
|
-
.pop();
|
|
2161
|
-
|
|
2162
|
-
if (lastOperation) {
|
|
2163
|
-
// checks if any of the provided operations where reshufled
|
|
2164
|
-
const newOp = operationsApplied.find(
|
|
2165
|
-
(appliedOp) =>
|
|
2166
|
-
!operations.find(
|
|
2167
|
-
(o) =>
|
|
2168
|
-
o.id === appliedOp.id &&
|
|
2169
|
-
o.index === appliedOp.index &&
|
|
2170
|
-
o.skip === appliedOp.skip &&
|
|
2171
|
-
o.hash === appliedOp.hash,
|
|
2172
|
-
),
|
|
2173
|
-
);
|
|
2174
|
-
|
|
2175
|
-
// if there are no new operations then reuses the provided source
|
|
2176
|
-
// otherwise sets it to local so listeners know that there were
|
|
2177
|
-
// new changes originating from this document drive server
|
|
2178
|
-
const source: StrandUpdateSource = newOp
|
|
2179
|
-
? { type: "local" }
|
|
2180
|
-
: (options?.source ?? { type: "local" });
|
|
2181
|
-
|
|
2182
|
-
const operationSource = this.getOperationSource(source);
|
|
2183
|
-
|
|
2184
|
-
this.listenerManager
|
|
2185
|
-
.updateSynchronizationRevisions(
|
|
2186
|
-
driveId,
|
|
2187
|
-
[
|
|
2188
|
-
{
|
|
2189
|
-
syncId: "0",
|
|
2190
|
-
driveId: driveId,
|
|
2191
|
-
documentId: "",
|
|
2192
|
-
scope: "global",
|
|
2193
|
-
branch: "main",
|
|
2194
|
-
documentType: "powerhouse/document-drive",
|
|
2195
|
-
lastUpdated: lastOperation.timestamp,
|
|
2196
|
-
revision: lastOperation.index,
|
|
2197
|
-
},
|
|
2198
|
-
],
|
|
2199
|
-
source,
|
|
2200
|
-
() => {
|
|
2201
|
-
this.updateSyncUnitStatus(driveId, {
|
|
2202
|
-
[operationSource]: "SYNCING",
|
|
2203
|
-
});
|
|
2204
|
-
},
|
|
2205
|
-
this.handleListenerError.bind(this),
|
|
2206
|
-
options?.forceSync ?? source.type === "local",
|
|
2207
|
-
)
|
|
2208
|
-
.then((updates) => {
|
|
2209
|
-
if (updates.length) {
|
|
2210
|
-
this.updateSyncUnitStatus(driveId, {
|
|
2211
|
-
[operationSource]: "SUCCESS",
|
|
2212
|
-
});
|
|
2213
|
-
}
|
|
2214
|
-
})
|
|
2215
|
-
.catch((error) => {
|
|
2216
|
-
logger.error("Non handled error updating sync revision", error);
|
|
2217
|
-
this.updateSyncUnitStatus(
|
|
2218
|
-
driveId,
|
|
2219
|
-
{ [operationSource]: "ERROR" },
|
|
2220
|
-
error as Error,
|
|
2221
|
-
);
|
|
2222
|
-
});
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
if (this.shouldSyncRemoteDrive(document)) {
|
|
2226
|
-
this.startSyncRemoteDrive(document.state.global.id);
|
|
2227
|
-
} else {
|
|
2228
|
-
this.stopSyncRemoteDrive(document.state.global.id);
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
// after applying all the valid operations,throws
|
|
2232
|
-
// an error if there was an invalid operation
|
|
2233
|
-
if (error) {
|
|
2234
|
-
throw error;
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
return {
|
|
2238
|
-
status: "SUCCESS",
|
|
2239
|
-
document,
|
|
2240
|
-
operations: operationsApplied,
|
|
2241
|
-
signals,
|
|
2242
|
-
} satisfies IOperationResult;
|
|
2243
|
-
} catch (error) {
|
|
2244
|
-
const operationError =
|
|
2245
|
-
error instanceof OperationError
|
|
2246
|
-
? error
|
|
2247
|
-
: new OperationError(
|
|
2248
|
-
"ERROR",
|
|
2249
|
-
undefined,
|
|
2250
|
-
(error as Error).message,
|
|
2251
|
-
(error as Error).cause,
|
|
2252
|
-
);
|
|
2253
|
-
|
|
2254
|
-
return {
|
|
2255
|
-
status: operationError.status,
|
|
2256
|
-
error: operationError,
|
|
2257
|
-
document,
|
|
2258
|
-
operations: operationsApplied,
|
|
2259
|
-
signals,
|
|
2260
|
-
} satisfies IOperationResult;
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
private _buildOperations<T extends Action>(
|
|
2265
|
-
documentId: Document,
|
|
2266
|
-
actions: (T | BaseAction)[],
|
|
2267
|
-
): Operation<T | BaseAction>[] {
|
|
2268
|
-
const operations: Operation<T | BaseAction>[] = [];
|
|
2269
|
-
const { reducer } = this.getDocumentModel(documentId.documentType);
|
|
2270
|
-
for (const action of actions) {
|
|
2271
|
-
documentId = reducer(documentId, action);
|
|
2272
|
-
const operation = documentId.operations[action.scope].slice().pop();
|
|
2273
|
-
if (!operation) {
|
|
2274
|
-
throw new Error("Error creating operations");
|
|
2275
|
-
}
|
|
2276
|
-
operations.push(operation);
|
|
2277
|
-
}
|
|
2278
|
-
return operations;
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
async addAction(
|
|
2282
|
-
driveId: string,
|
|
2283
|
-
documentId: string,
|
|
2284
|
-
action: Action,
|
|
2285
|
-
options?: AddOperationOptions,
|
|
2286
|
-
): Promise<IOperationResult> {
|
|
2287
|
-
return this.addActions(driveId, documentId, [action], options);
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
async addActions(
|
|
2291
|
-
driveId: string,
|
|
2292
|
-
documentId: string,
|
|
2293
|
-
actions: Action[],
|
|
2294
|
-
options?: AddOperationOptions,
|
|
2295
|
-
): Promise<IOperationResult> {
|
|
2296
|
-
const document = await this.getDocument(driveId, documentId);
|
|
2297
|
-
const operations = this._buildOperations(document, actions);
|
|
2298
|
-
return this.addOperations(driveId, documentId, operations, options);
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
async addDriveAction(
|
|
2302
|
-
driveId: string,
|
|
2303
|
-
action: DocumentDriveAction | BaseAction,
|
|
2304
|
-
options?: AddOperationOptions,
|
|
2305
|
-
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
2306
|
-
return this.addDriveActions(driveId, [action], options);
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
async addDriveActions(
|
|
2310
|
-
driveId: string,
|
|
2311
|
-
actions: (DocumentDriveAction | BaseAction)[],
|
|
2312
|
-
options?: AddOperationOptions,
|
|
2313
|
-
): Promise<IOperationResult<DocumentDriveDocument>> {
|
|
2314
|
-
const document = await this.getDrive(driveId);
|
|
2315
|
-
const operations = this._buildOperations(document, actions);
|
|
2316
|
-
const result = await this.addDriveOperations(driveId, operations, options);
|
|
2317
|
-
return result;
|
|
2318
|
-
}
|
|
2319
|
-
|
|
2320
|
-
async detachDrive(driveId: string) {
|
|
2321
|
-
const documentDrive = await this.getDrive(driveId);
|
|
2322
|
-
const listeners = documentDrive.state.local.listeners || [];
|
|
2323
|
-
const triggers = documentDrive.state.local.triggers || [];
|
|
2324
|
-
|
|
2325
|
-
for (const listener of listeners) {
|
|
2326
|
-
await this.addDriveAction(
|
|
2327
|
-
driveId,
|
|
2328
|
-
actions.removeListener({ listenerId: listener.listenerId }),
|
|
2329
|
-
);
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
for (const trigger of triggers) {
|
|
2333
|
-
await this.addDriveAction(
|
|
2334
|
-
driveId,
|
|
2335
|
-
actions.removeTrigger({ triggerId: trigger.id }),
|
|
2336
|
-
);
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
await this.addDriveAction(
|
|
2340
|
-
driveId,
|
|
2341
|
-
actions.setSharingType({ type: "LOCAL" }),
|
|
2342
|
-
);
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
async addListener(driveId: string, listener: Listener) {
|
|
2346
|
-
await this.listenerManager.setListener(driveId, listener);
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
|
-
async addInternalListener(
|
|
2350
|
-
driveId: string,
|
|
2351
|
-
receiver: IReceiver,
|
|
2352
|
-
options: {
|
|
2353
|
-
listenerId: string;
|
|
2354
|
-
label: string;
|
|
2355
|
-
block: boolean;
|
|
2356
|
-
filter: ListenerFilter;
|
|
2357
|
-
},
|
|
2358
|
-
) {
|
|
2359
|
-
const listener: AddListenerInput["listener"] = {
|
|
2360
|
-
callInfo: {
|
|
2361
|
-
data: "",
|
|
2362
|
-
name: "Interal",
|
|
2363
|
-
transmitterType: "Internal",
|
|
2364
|
-
},
|
|
2365
|
-
system: true,
|
|
2366
|
-
...options,
|
|
2367
|
-
};
|
|
2368
|
-
await this.addDriveAction(driveId, actions.addListener({ listener }));
|
|
2369
|
-
const transmitter = await this.getTransmitter(driveId, options.listenerId);
|
|
2370
|
-
if (!transmitter) {
|
|
2371
|
-
logger.error("Internal listener not found");
|
|
2372
|
-
throw new Error("Internal listener not found");
|
|
2373
|
-
}
|
|
2374
|
-
if (!(transmitter instanceof InternalTransmitter)) {
|
|
2375
|
-
logger.error("Listener is not an internal transmitter");
|
|
2376
|
-
throw new Error("Listener is not an internal transmitter");
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
|
-
transmitter.setReceiver(receiver);
|
|
2380
|
-
return transmitter;
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
private async removeListener(
|
|
2384
|
-
driveId: string,
|
|
2385
|
-
operation: Operation<Action<"REMOVE_LISTENER", RemoveListenerInput>>,
|
|
2386
|
-
) {
|
|
2387
|
-
const { listenerId } = operation.input;
|
|
2388
|
-
await this.listenerManager.removeListener(driveId, listenerId);
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
async getTransmitter(
|
|
2392
|
-
driveId: string,
|
|
2393
|
-
listenerId: string,
|
|
2394
|
-
): Promise<ITransmitter | undefined> {
|
|
2395
|
-
const listener = await this.listenerManager.getListenerState(
|
|
2396
|
-
driveId,
|
|
2397
|
-
listenerId,
|
|
2398
|
-
);
|
|
2399
|
-
return listener?.listener.transmitter;
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
getListener(
|
|
2403
|
-
driveId: string,
|
|
2404
|
-
listenerId: string,
|
|
2405
|
-
): Promise<ListenerState | undefined> {
|
|
2406
|
-
let listenerState;
|
|
2407
|
-
try {
|
|
2408
|
-
listenerState = this.listenerManager.getListenerState(
|
|
2409
|
-
driveId,
|
|
2410
|
-
listenerId,
|
|
2411
|
-
);
|
|
2412
|
-
} catch {
|
|
2413
|
-
return Promise.resolve(undefined);
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
return Promise.resolve(listenerState);
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
getSyncStatus(
|
|
2420
|
-
syncUnitId: string,
|
|
2421
|
-
): SyncStatus | SynchronizationUnitNotFoundError {
|
|
2422
|
-
const status = this.syncStatus.get(syncUnitId);
|
|
2423
|
-
if (!status) {
|
|
2424
|
-
return new SynchronizationUnitNotFoundError(
|
|
2425
|
-
`Sync status not found for syncUnitId: ${syncUnitId}`,
|
|
2426
|
-
syncUnitId,
|
|
2427
|
-
);
|
|
2428
|
-
}
|
|
2429
|
-
return this.getCombinedSyncUnitStatus(status);
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
|
-
on<K extends keyof DriveEvents>(event: K, cb: DriveEvents[K]): Unsubscribe {
|
|
2433
|
-
return this.emitter.on(event, cb);
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
protected emit<K extends keyof DriveEvents>(
|
|
2437
|
-
event: K,
|
|
2438
|
-
...args: Parameters<DriveEvents[K]>
|
|
2439
|
-
): void {
|
|
2440
|
-
return this.emitter.emit(event, ...args);
|
|
2441
|
-
}
|
|
2442
|
-
}
|
|
2443
|
-
|
|
2444
|
-
export const DocumentDriveServer = ReadModeServer(BaseDocumentDriveServer);
|