document-drive 1.0.0-experimental.10 → 1.0.0-experimental.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/package.json +7 -7
- package/src/queue/base.ts +1 -17
- package/src/queue/redis.ts +12 -22
- package/src/queue/types.ts +0 -3
- package/src/server/index.ts +47 -17
- package/src/server/listener/transmitter/pull-responder.ts +7 -8
- package/src/server/types.ts +3 -1
- package/src/storage/prisma.ts +192 -76
- package/src/storage/sequelize.ts +11 -7
- package/src/storage/types.ts +2 -1
- package/src/queue/manager.ts +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
+
# [1.0.0-alpha.65](https://github.com/powerhouse-inc/document-drive/compare/v1.0.0-alpha.64...v1.0.0-alpha.65) (2024-05-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* fetch resulting state for last unskipped operation ([8297353](https://github.com/powerhouse-inc/document-drive/commit/8297353dc8eaca107d8134580dee67c3265b05b5))
|
|
7
|
+
|
|
8
|
+
# [1.0.0-alpha.64](https://github.com/powerhouse-inc/document-drive/compare/v1.0.0-alpha.63...v1.0.0-alpha.64) (2024-05-30)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* update document-model lib version ([#180](https://github.com/powerhouse-inc/document-drive/issues/180)) ([83cec58](https://github.com/powerhouse-inc/document-drive/commit/83cec58cb02388a3b2a643dd5af6c31cd850242d))
|
|
14
|
+
|
|
15
|
+
# [1.0.0-alpha.63](https://github.com/powerhouse-inc/document-drive/compare/v1.0.0-alpha.62...v1.0.0-alpha.63) (2024-05-30)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* operations query ([8dd11c7](https://github.com/powerhouse-inc/document-drive/commit/8dd11c72058452f3e8febac472d01ad56d959e17))
|
|
21
|
+
|
|
22
|
+
# [1.0.0-alpha.62](https://github.com/powerhouse-inc/document-drive/compare/v1.0.0-alpha.61...v1.0.0-alpha.62) (2024-05-30)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* added drive and documentId filter ([718405f](https://github.com/powerhouse-inc/document-drive/commit/718405febbe8e53a3fd392cdd4acf2b041347eaf))
|
|
28
|
+
|
|
29
|
+
# [1.0.0-alpha.61](https://github.com/powerhouse-inc/document-drive/compare/v1.0.0-alpha.60...v1.0.0-alpha.61) (2024-05-30)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Features
|
|
33
|
+
|
|
34
|
+
* store resulting state as bytes and only retrieve state for last op of each scope ([c6a5004](https://github.com/powerhouse-inc/document-drive/commit/c6a5004a3dd07ee1ce2f5521bb7a6aa5e970465e))
|
|
35
|
+
|
|
36
|
+
# [1.0.0-alpha.60](https://github.com/powerhouse-inc/document-drive/compare/v1.0.0-alpha.59...v1.0.0-alpha.60) (2024-05-29)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Features
|
|
40
|
+
|
|
41
|
+
* avoid duplicated getDocument call ([c0684bc](https://github.com/powerhouse-inc/document-drive/commit/c0684bc0746d8bafb9393cdaeebfb60b38a8a32f))
|
|
42
|
+
|
|
43
|
+
# [1.0.0-alpha.59](https://github.com/powerhouse-inc/document-drive/compare/v1.0.0-alpha.58...v1.0.0-alpha.59) (2024-05-28)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Features
|
|
47
|
+
|
|
48
|
+
* enable operation id ([#174](https://github.com/powerhouse-inc/document-drive/issues/174)) ([1d77fd2](https://github.com/powerhouse-inc/document-drive/commit/1d77fd2f6a4618371c6fc4c072d4eab7d27a662a))
|
|
49
|
+
|
|
50
|
+
# [1.0.0-alpha.58](https://github.com/powerhouse-inc/document-drive/compare/v1.0.0-alpha.57...v1.0.0-alpha.58) (2024-05-28)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Features
|
|
54
|
+
|
|
55
|
+
* don't save queue job result ([efbf239](https://github.com/powerhouse-inc/document-drive/commit/efbf239e5f77592267446d5c11d670a82f1f6e58))
|
|
56
|
+
|
|
57
|
+
# [1.0.0-alpha.57](https://github.com/powerhouse-inc/document-drive/compare/v1.0.0-alpha.56...v1.0.0-alpha.57) (2024-05-22)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
### Features
|
|
61
|
+
|
|
62
|
+
* new release ([17796e8](https://github.com/powerhouse-inc/document-drive/commit/17796e8577d16d3095a8adf3c40e7bd7146b6142))
|
|
63
|
+
|
|
64
|
+
# [1.0.0-alpha.56](https://github.com/powerhouse-inc/document-drive/compare/v1.0.0-alpha.55...v1.0.0-alpha.56) (2024-05-22)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### Features
|
|
68
|
+
|
|
69
|
+
* add queues and append only conflict resolution ([d17abd6](https://github.com/powerhouse-inc/document-drive/commit/d17abd664a7381f80faa6530f83ca9e224282ba1)), closes [#153](https://github.com/powerhouse-inc/document-drive/issues/153)
|
|
70
|
+
|
|
1
71
|
# 1.0.0-experimental.1 (2024-05-15)
|
|
2
72
|
|
|
3
73
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-drive",
|
|
3
|
-
"version": "1.0.0-experimental.
|
|
3
|
+
"version": "1.0.0-experimental.100",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"test:watch": "vitest watch"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"document-model": "1.
|
|
38
|
-
"document-model-libs": "^1.
|
|
37
|
+
"document-model": "1.3.0",
|
|
38
|
+
"document-model-libs": "^1.53.1"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"@prisma/client": "5.
|
|
41
|
+
"@prisma/client": "5.14.0",
|
|
42
42
|
"localforage": "^1.10.0",
|
|
43
43
|
"redis": "^4.6.13",
|
|
44
44
|
"sequelize": "^6.35.2",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
66
66
|
"@typescript-eslint/parser": "^6.21.0",
|
|
67
67
|
"@vitest/coverage-v8": "^1.4.0",
|
|
68
|
-
"document-model": "1.
|
|
69
|
-
"document-model-libs": "1.
|
|
68
|
+
"document-model": "^1.3.0",
|
|
69
|
+
"document-model-libs": "^1.53.1",
|
|
70
70
|
"eslint": "^8.57.0",
|
|
71
71
|
"eslint-config-prettier": "^9.1.0",
|
|
72
72
|
"fake-indexeddb": "^5.0.2",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"msw": "^2.2.13",
|
|
75
75
|
"prettier": "^3.2.5",
|
|
76
76
|
"prettier-plugin-organize-imports": "^3.2.4",
|
|
77
|
-
"prisma": "^5.
|
|
77
|
+
"prisma": "^5.14.0",
|
|
78
78
|
"semantic-release": "^23.0.8",
|
|
79
79
|
"sequelize": "^6.37.2",
|
|
80
80
|
"sqlite3": "^5.1.7",
|
package/src/queue/base.ts
CHANGED
|
@@ -10,7 +10,6 @@ export class MemoryQueue<T, R> implements IQueue<T, R> {
|
|
|
10
10
|
private blocked = false;
|
|
11
11
|
private deleted = false;
|
|
12
12
|
private items: IJob<T>[] = [];
|
|
13
|
-
private results = new Map<JobId, R>();
|
|
14
13
|
private dependencies = new Array<IJob<OperationJob>>();
|
|
15
14
|
|
|
16
15
|
constructor(id: string) {
|
|
@@ -25,15 +24,6 @@ export class MemoryQueue<T, R> implements IQueue<T, R> {
|
|
|
25
24
|
return this.deleted;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
async setResult(jobId: string, result: R): Promise<void> {
|
|
29
|
-
this.results.set(jobId, result);
|
|
30
|
-
return Promise.resolve();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async getResult(jobId: string): Promise<R | undefined> {
|
|
34
|
-
return Promise.resolve(this.results.get(jobId));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
27
|
async addJob(data: IJob<T>) {
|
|
38
28
|
this.items.push(data);
|
|
39
29
|
return Promise.resolve();
|
|
@@ -157,11 +147,6 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
157
147
|
return jobId;
|
|
158
148
|
}
|
|
159
149
|
|
|
160
|
-
async getResult(driveId: string, documentId: string, jobId: JobId): Promise<IOperationResult | undefined> {
|
|
161
|
-
const queue = this.getQueue(driveId, documentId);
|
|
162
|
-
return queue.getResult(jobId);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
150
|
getQueue(driveId: string, documentId?: string) {
|
|
166
151
|
const queueId = this.getQueueId(driveId, documentId);
|
|
167
152
|
let queue = this.queues.find((q) => q.getId() === queueId);
|
|
@@ -237,7 +222,6 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
237
222
|
|
|
238
223
|
try {
|
|
239
224
|
const result = await this.delegate.processOperationJob(nextJob);
|
|
240
|
-
await queue.setResult(nextJob.jobId, result);
|
|
241
225
|
|
|
242
226
|
// unblock the document queues of each add_file operation
|
|
243
227
|
const addFileOperations = nextJob.operations.filter((op) => op.type === "ADD_FILE");
|
|
@@ -252,7 +236,7 @@ export class BaseQueueManager implements IQueueManager {
|
|
|
252
236
|
} catch (e) {
|
|
253
237
|
this.emit("jobFailed", nextJob, e as Error);
|
|
254
238
|
} finally {
|
|
255
|
-
queue.setBlocked(false);
|
|
239
|
+
await queue.setBlocked(false);
|
|
256
240
|
await this.processNextJob();
|
|
257
241
|
}
|
|
258
242
|
}
|
package/src/queue/redis.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RedisClientType } from "redis";
|
|
2
|
-
import { IJob, IQueue, IQueueManager, OperationJob } from "./types";
|
|
2
|
+
import { IJob, IQueue, IQueueManager, IServerDelegate, OperationJob } from "./types";
|
|
3
3
|
import { BaseQueueManager } from "./base";
|
|
4
4
|
|
|
5
5
|
export class RedisQueue<T, R> implements IQueue<T, R> {
|
|
@@ -10,18 +10,7 @@ export class RedisQueue<T, R> implements IQueue<T, R> {
|
|
|
10
10
|
this.client = client;
|
|
11
11
|
this.id = id;
|
|
12
12
|
this.client.hSet("queues", id, "true");
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async setResult(jobId: string, result: any): Promise<void> {
|
|
17
|
-
await this.client.hSet(this.id + "-results", jobId, JSON.stringify(result));
|
|
18
|
-
}
|
|
19
|
-
async getResult(jobId: string): Promise<any> {
|
|
20
|
-
const results = await this.client.hGet(this.id + "-results", jobId);
|
|
21
|
-
if (!results) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
return JSON.parse(results);
|
|
13
|
+
this.client.hSet(this.id, "blocked", "false");
|
|
25
14
|
}
|
|
26
15
|
|
|
27
16
|
async addJob(data: any) {
|
|
@@ -44,13 +33,13 @@ export class RedisQueue<T, R> implements IQueue<T, R> {
|
|
|
44
33
|
if (blocked) {
|
|
45
34
|
await this.client.hSet(this.id, "blocked", "true");
|
|
46
35
|
} else {
|
|
47
|
-
await this.client.
|
|
36
|
+
await this.client.hSet(this.id, "blocked", "false");
|
|
48
37
|
}
|
|
49
38
|
}
|
|
50
39
|
|
|
51
40
|
async isBlocked() {
|
|
52
41
|
const blockedResult = await this.client.hGet(this.id, "blocked");
|
|
53
|
-
if (blockedResult) {
|
|
42
|
+
if (blockedResult === "true") {
|
|
54
43
|
return true;
|
|
55
44
|
}
|
|
56
45
|
|
|
@@ -80,7 +69,6 @@ export class RedisQueue<T, R> implements IQueue<T, R> {
|
|
|
80
69
|
}
|
|
81
70
|
|
|
82
71
|
async removeDependencies(job: IJob<OperationJob>) {
|
|
83
|
-
const allDeps1 = await this.client.lLen(this.id + "-deps");
|
|
84
72
|
await this.client.lRem(this.id + "-deps", 1, JSON.stringify(job));
|
|
85
73
|
const allDeps = await this.client.lLen(this.id + "-deps");
|
|
86
74
|
if (allDeps > 0) {
|
|
@@ -91,15 +79,15 @@ export class RedisQueue<T, R> implements IQueue<T, R> {
|
|
|
91
79
|
}
|
|
92
80
|
|
|
93
81
|
async isDeleted() {
|
|
94
|
-
const
|
|
95
|
-
return
|
|
82
|
+
const active = await this.client.hGet("queues", this.id);
|
|
83
|
+
return active === "false";
|
|
96
84
|
}
|
|
97
85
|
|
|
98
86
|
async setDeleted(deleted: boolean) {
|
|
99
87
|
if (deleted) {
|
|
100
|
-
await this.client.hSet(this.id, "
|
|
88
|
+
await this.client.hSet("queues", this.id, "false");
|
|
101
89
|
} else {
|
|
102
|
-
await this.client.
|
|
90
|
+
await this.client.hSet("queues", this.id, "true");
|
|
103
91
|
}
|
|
104
92
|
}
|
|
105
93
|
}
|
|
@@ -115,10 +103,12 @@ export class RedisQueueManager extends BaseQueueManager implements IQueueManager
|
|
|
115
103
|
|
|
116
104
|
async init(delegate: IServerDelegate, onError: (error: Error) => void): Promise<void> {
|
|
117
105
|
await super.init(delegate, onError);
|
|
118
|
-
// load all queues
|
|
119
106
|
const queues = await this.client.hGetAll("queues");
|
|
120
107
|
for (const queueId in queues) {
|
|
121
|
-
this.
|
|
108
|
+
const active = await this.client.hGet("queues", queueId);
|
|
109
|
+
if (active === "true") {
|
|
110
|
+
this.queues.push(new RedisQueue(queueId, this.client));
|
|
111
|
+
}
|
|
122
112
|
}
|
|
123
113
|
}
|
|
124
114
|
|
package/src/queue/types.ts
CHANGED
|
@@ -23,7 +23,6 @@ export interface IServerDelegate {
|
|
|
23
23
|
|
|
24
24
|
export interface IQueueManager {
|
|
25
25
|
addJob(job: OperationJob): Promise<JobId>;
|
|
26
|
-
getResult(driveId: string, documentId: string, jobId: JobId): Promise<IOperationResult | undefined>;
|
|
27
26
|
getQueue(driveId: string, document?: string): IQueue<OperationJob, IOperationResult>;
|
|
28
27
|
removeQueue(driveId: string, documentId?: string): void;
|
|
29
28
|
getQueueByIndex(index: number): IQueue<OperationJob, IOperationResult> | null;
|
|
@@ -47,8 +46,6 @@ export interface IQueue<T, R> {
|
|
|
47
46
|
isBlocked(): Promise<boolean>;
|
|
48
47
|
isDeleted(): Promise<boolean>;
|
|
49
48
|
setDeleted(deleted: boolean): Promise<void>;
|
|
50
|
-
setResult(jobId: JobId, result: R): Promise<void>;
|
|
51
|
-
getResult(jobId: JobId): Promise<R | undefined>;
|
|
52
49
|
getJobs(): Promise<IJob<T>[]>;
|
|
53
50
|
addDependencies(job: IJob<OperationJob>): Promise<void>;
|
|
54
51
|
removeDependencies(job: IJob<OperationJob>): Promise<void>;
|
package/src/server/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
DocumentModel,
|
|
21
21
|
Operation,
|
|
22
22
|
OperationScope,
|
|
23
|
-
|
|
23
|
+
utils as DocumentUtils
|
|
24
24
|
} from 'document-model/document';
|
|
25
25
|
import { createNanoEvents, Unsubscribe } from 'nanoevents';
|
|
26
26
|
import { ICache } from '../cache';
|
|
@@ -425,7 +425,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
425
425
|
type: operation.type,
|
|
426
426
|
input: operation.input as object,
|
|
427
427
|
skip: operation.skip,
|
|
428
|
-
context: operation.context
|
|
428
|
+
context: operation.context,
|
|
429
|
+
id: operation.id
|
|
429
430
|
}));
|
|
430
431
|
}
|
|
431
432
|
|
|
@@ -630,6 +631,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
630
631
|
|
|
631
632
|
async _processOperations<T extends Document, A extends Action>(
|
|
632
633
|
drive: string,
|
|
634
|
+
documentId: string | undefined,
|
|
633
635
|
storageDocument: DocumentStorage<T>,
|
|
634
636
|
operations: Operation<A | BaseAction>[]
|
|
635
637
|
) {
|
|
@@ -667,7 +669,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
667
669
|
: merge(trunk, invertedTrunk, reshuffleByTimestamp);
|
|
668
670
|
|
|
669
671
|
const newOperations = newHistory.filter(
|
|
670
|
-
(op
|
|
672
|
+
(op) => trunk.length < 1 || precedes(trunk[trunk.length - 1]!, op)
|
|
671
673
|
);
|
|
672
674
|
|
|
673
675
|
for (const nextOperation of newOperations) {
|
|
@@ -676,15 +678,20 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
676
678
|
// when dealing with a merge (tail.length > 0) we have to skip hash validation
|
|
677
679
|
// for the operations that were re-indexed (previous hash becomes invalid due the new position in the history)
|
|
678
680
|
if (tail.length > 0) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
invertedTrunkOp.hash === nextOperation.hash
|
|
681
|
+
const sourceOperation = operations.find(
|
|
682
|
+
op => op.hash === nextOperation.hash
|
|
682
683
|
);
|
|
684
|
+
|
|
685
|
+
skipHashValidation =
|
|
686
|
+
!sourceOperation ||
|
|
687
|
+
sourceOperation.index !== nextOperation.index ||
|
|
688
|
+
sourceOperation.skip !== nextOperation.skip;
|
|
683
689
|
}
|
|
684
690
|
|
|
685
691
|
try {
|
|
686
692
|
const appliedResult = await this._performOperation(
|
|
687
693
|
drive,
|
|
694
|
+
documentId,
|
|
688
695
|
document,
|
|
689
696
|
nextOperation,
|
|
690
697
|
skipHashValidation
|
|
@@ -720,6 +727,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
720
727
|
private _buildDocument<T extends Document>(
|
|
721
728
|
documentStorage: DocumentStorage<T>, options?: GetDocumentOptions
|
|
722
729
|
): T {
|
|
730
|
+
if (documentStorage.state && (!options || options.checkHashes === false)) {
|
|
731
|
+
return documentStorage as T;
|
|
732
|
+
}
|
|
733
|
+
|
|
723
734
|
const documentModel = this._getDocumentModel(
|
|
724
735
|
documentStorage.documentType
|
|
725
736
|
);
|
|
@@ -728,11 +739,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
728
739
|
documentStorage.operations,
|
|
729
740
|
options.revisions
|
|
730
741
|
) : documentStorage.operations;
|
|
731
|
-
const operations = baseUtils.documentHelpers.
|
|
732
|
-
|
|
733
|
-
if (documentStorage.state && (!options || options.checkHashes === false)) {
|
|
734
|
-
return documentStorage as T;
|
|
735
|
-
}
|
|
742
|
+
const operations = baseUtils.documentHelpers.garbageCollectDocumentOperations(revisionOperations);
|
|
736
743
|
|
|
737
744
|
return baseUtils.replayDocument(
|
|
738
745
|
documentStorage.initialState,
|
|
@@ -741,12 +748,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
741
748
|
undefined,
|
|
742
749
|
documentStorage,
|
|
743
750
|
undefined,
|
|
744
|
-
{
|
|
751
|
+
{
|
|
752
|
+
...options,
|
|
753
|
+
checkHashes: options?.checkHashes ?? true,
|
|
754
|
+
reuseOperationResultingState: options?.checkHashes ?? true
|
|
755
|
+
}
|
|
745
756
|
) as T;
|
|
746
757
|
}
|
|
747
758
|
|
|
748
759
|
private async _performOperation<T extends Document>(
|
|
749
760
|
drive: string,
|
|
761
|
+
id: string | undefined,
|
|
750
762
|
document: T,
|
|
751
763
|
operation: Operation,
|
|
752
764
|
skipHashValidation = false
|
|
@@ -756,6 +768,25 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
756
768
|
const signalResults: SignalResult[] = [];
|
|
757
769
|
let newDocument = document;
|
|
758
770
|
|
|
771
|
+
const scope = operation.scope;
|
|
772
|
+
const documentOperations = DocumentUtils.documentHelpers.garbageCollectDocumentOperations(
|
|
773
|
+
{
|
|
774
|
+
...document.operations,
|
|
775
|
+
[scope]: DocumentUtils.documentHelpers.skipHeaderOperations(
|
|
776
|
+
document.operations[scope],
|
|
777
|
+
operation,
|
|
778
|
+
),
|
|
779
|
+
},
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
const lastRemainingOperation = documentOperations[scope].at(-1);
|
|
783
|
+
// if the latest operation doesn't have a resulting state then tries
|
|
784
|
+
// to retrieve it from the db to avoid rerunning all the operations
|
|
785
|
+
if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
|
|
786
|
+
lastRemainingOperation.resultingState = await (id ? this.storage.getOperationResultingState?.(drive, id, lastRemainingOperation.index, lastRemainingOperation.scope, "main") :
|
|
787
|
+
this.storage.getDriveOperationResultingState?.(drive, lastRemainingOperation.index, lastRemainingOperation.scope, "main"))
|
|
788
|
+
}
|
|
789
|
+
|
|
759
790
|
const operationSignals: (() => Promise<SignalResult>)[] = [];
|
|
760
791
|
newDocument = documentModel.reducer(
|
|
761
792
|
newDocument,
|
|
@@ -792,7 +823,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
792
823
|
);
|
|
793
824
|
}
|
|
794
825
|
},
|
|
795
|
-
{ skip: operation.skip }
|
|
826
|
+
{ skip: operation.skip, reuseOperationResultingState: true }
|
|
796
827
|
) as T;
|
|
797
828
|
|
|
798
829
|
const appliedOperation = newDocument.operations[operation.scope].filter(
|
|
@@ -837,7 +868,6 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
837
868
|
callback: (document: DocumentStorage) => Promise<{
|
|
838
869
|
operations: Operation[];
|
|
839
870
|
header: DocumentHeader;
|
|
840
|
-
newState: State<any, any> | undefined;
|
|
841
871
|
}>
|
|
842
872
|
) {
|
|
843
873
|
if (!this.storage.addDocumentOperationsWithTransaction) {
|
|
@@ -884,7 +914,6 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
884
914
|
}
|
|
885
915
|
});
|
|
886
916
|
const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
|
|
887
|
-
console.log("test")
|
|
888
917
|
if (job.jobId === jobId) {
|
|
889
918
|
unsubscribe();
|
|
890
919
|
unsubscribeError();
|
|
@@ -913,6 +942,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
913
942
|
await this._addOperations(drive, id, async documentStorage => {
|
|
914
943
|
const result = await this._processOperations(
|
|
915
944
|
drive,
|
|
945
|
+
id,
|
|
916
946
|
documentStorage,
|
|
917
947
|
operations
|
|
918
948
|
);
|
|
@@ -1098,7 +1128,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1098
1128
|
const result = await this._processOperations<
|
|
1099
1129
|
DocumentDriveDocument,
|
|
1100
1130
|
DocumentDriveAction
|
|
1101
|
-
>(drive, documentStorage, operations.slice());
|
|
1131
|
+
>(drive, undefined, documentStorage, operations.slice());
|
|
1102
1132
|
|
|
1103
1133
|
document = result.document;
|
|
1104
1134
|
operationsApplied.push(...result.operationsApplied);
|
|
@@ -1259,7 +1289,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
|
|
|
1259
1289
|
const document = await this.getDrive(drive);
|
|
1260
1290
|
const operations = this._buildOperations(document, actions);
|
|
1261
1291
|
const result = await this.queueDriveOperations(drive, operations);
|
|
1262
|
-
return result
|
|
1292
|
+
return result;
|
|
1263
1293
|
}
|
|
1264
1294
|
|
|
1265
1295
|
async addInternalListener(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ListenerFilter, Trigger
|
|
1
|
+
import { ListenerFilter, Trigger } from 'document-model-libs/document-drive';
|
|
2
2
|
import { Operation, OperationScope } from 'document-model/document';
|
|
3
3
|
import { PULL_DRIVE_INTERVAL } from '../..';
|
|
4
4
|
import { generateUUID } from '../../../utils';
|
|
@@ -153,6 +153,7 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
153
153
|
scope
|
|
154
154
|
branch
|
|
155
155
|
operations {
|
|
156
|
+
id
|
|
156
157
|
timestamp
|
|
157
158
|
skip
|
|
158
159
|
type
|
|
@@ -237,13 +238,11 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
237
238
|
const listenerRevisions: ListenerRevisionWithError[] = [];
|
|
238
239
|
|
|
239
240
|
for (const strand of strands) {
|
|
240
|
-
const operations: Operation[] = strand.operations.map(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
})
|
|
246
|
-
);
|
|
241
|
+
const operations: Operation[] = strand.operations.map(op => ({
|
|
242
|
+
...op,
|
|
243
|
+
scope: strand.scope,
|
|
244
|
+
branch: strand.branch
|
|
245
|
+
}));
|
|
247
246
|
|
|
248
247
|
let error: Error | undefined = undefined;
|
|
249
248
|
try {
|
package/src/server/types.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
Document,
|
|
15
15
|
Operation,
|
|
16
16
|
OperationScope,
|
|
17
|
+
ReducerOptions,
|
|
17
18
|
Signal,
|
|
18
19
|
State
|
|
19
20
|
} from 'document-model/document';
|
|
@@ -116,6 +117,7 @@ export type OperationUpdate = {
|
|
|
116
117
|
input: object;
|
|
117
118
|
hash: string;
|
|
118
119
|
context?: ActionContext;
|
|
120
|
+
id?: string;
|
|
119
121
|
};
|
|
120
122
|
|
|
121
123
|
export type StrandUpdate = {
|
|
@@ -140,7 +142,7 @@ export type PartialRecord<K extends keyof any, T> = {
|
|
|
140
142
|
|
|
141
143
|
export type RevisionsFilter = PartialRecord<OperationScope, number>;
|
|
142
144
|
|
|
143
|
-
export type GetDocumentOptions = {
|
|
145
|
+
export type GetDocumentOptions = ReducerOptions & {
|
|
144
146
|
revisions?: RevisionsFilter;
|
|
145
147
|
checkHashes?: boolean;
|
|
146
148
|
};
|
package/src/storage/prisma.ts
CHANGED
|
@@ -1,30 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Prisma, PrismaClient } from '@prisma/client';
|
|
2
2
|
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
|
3
|
-
import { backOff, IBackOffOptions } from "exponential-backoff";
|
|
4
3
|
import {
|
|
5
4
|
DocumentDriveAction,
|
|
6
5
|
DocumentDriveLocalState,
|
|
7
6
|
DocumentDriveState
|
|
8
7
|
} from 'document-model-libs/document-drive';
|
|
9
8
|
import type {
|
|
9
|
+
Action,
|
|
10
|
+
AttachmentInput,
|
|
11
|
+
DocumentOperations,
|
|
12
|
+
FileRegistry,
|
|
10
13
|
BaseAction,
|
|
14
|
+
Document,
|
|
11
15
|
DocumentHeader,
|
|
12
16
|
ExtendedState,
|
|
13
17
|
Operation,
|
|
14
18
|
OperationScope,
|
|
15
19
|
State
|
|
16
20
|
} from 'document-model/document';
|
|
21
|
+
import { IBackOffOptions, backOff } from 'exponential-backoff';
|
|
17
22
|
import { ConflictOperationError } from '../server/error';
|
|
18
23
|
import { logger } from '../utils/logger';
|
|
19
24
|
import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
|
|
20
25
|
|
|
21
|
-
type Transaction =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
type Transaction =
|
|
27
|
+
| Omit<
|
|
28
|
+
PrismaClient<Prisma.PrismaClientOptions, never>,
|
|
29
|
+
| '$connect'
|
|
30
|
+
| '$disconnect'
|
|
31
|
+
| '$on'
|
|
32
|
+
| '$transaction'
|
|
33
|
+
| '$use'
|
|
34
|
+
| '$extends'
|
|
35
|
+
>
|
|
36
|
+
| ExtendedPrismaClient;
|
|
25
37
|
|
|
26
38
|
function storageToOperation(
|
|
27
|
-
op: Prisma.$OperationPayload['scalars']
|
|
39
|
+
op: Prisma.$OperationPayload['scalars'] & {
|
|
40
|
+
attachments?: AttachmentInput[];
|
|
41
|
+
}
|
|
28
42
|
): Operation {
|
|
29
43
|
const operation: Operation = {
|
|
30
44
|
skip: op.skip,
|
|
@@ -34,7 +48,10 @@ function storageToOperation(
|
|
|
34
48
|
input: JSON.parse(op.input),
|
|
35
49
|
type: op.type,
|
|
36
50
|
scope: op.scope as OperationScope,
|
|
37
|
-
|
|
51
|
+
resultingState: op.resultingState
|
|
52
|
+
? op.resultingState.toString()
|
|
53
|
+
: undefined,
|
|
54
|
+
attachments: op.attachments
|
|
38
55
|
};
|
|
39
56
|
if (op.context) {
|
|
40
57
|
operation.context = op.context as Prisma.JsonObject;
|
|
@@ -44,27 +61,32 @@ function storageToOperation(
|
|
|
44
61
|
|
|
45
62
|
export type PrismaStorageOptions = {
|
|
46
63
|
transactionRetryBackoff?: IBackOffOptions;
|
|
47
|
-
}
|
|
64
|
+
};
|
|
48
65
|
|
|
49
|
-
function getRetryTransactionsClient<T extends PrismaClient>(
|
|
66
|
+
function getRetryTransactionsClient<T extends PrismaClient>(
|
|
67
|
+
prisma: T,
|
|
68
|
+
backOffOptions?: Partial<IBackOffOptions>
|
|
69
|
+
) {
|
|
50
70
|
return prisma.$extends({
|
|
51
71
|
client: {
|
|
52
|
-
$transaction: (...args: Parameters<T[
|
|
72
|
+
$transaction: (...args: Parameters<T['$transaction']>) => {
|
|
53
73
|
// eslint-disable-next-line prefer-spread
|
|
54
74
|
return backOff(() => prisma.$transaction.apply(prisma, args), {
|
|
55
|
-
retry:
|
|
75
|
+
retry: e => {
|
|
56
76
|
// Retry the transaction only if the error was due to a write conflict or deadlock
|
|
57
77
|
// See: https://www.prisma.io/docs/reference/api-reference/error-reference#p2034
|
|
58
|
-
return (e as { code: string }).code ===
|
|
78
|
+
return (e as { code: string }).code === 'P2034';
|
|
59
79
|
},
|
|
60
|
-
...backOffOptions
|
|
80
|
+
...backOffOptions
|
|
61
81
|
});
|
|
62
82
|
}
|
|
63
83
|
}
|
|
64
84
|
});
|
|
65
85
|
}
|
|
66
86
|
|
|
67
|
-
type ExtendedPrismaClient = ReturnType<
|
|
87
|
+
type ExtendedPrismaClient = ReturnType<
|
|
88
|
+
typeof getRetryTransactionsClient<PrismaClient>
|
|
89
|
+
>;
|
|
68
90
|
|
|
69
91
|
export class PrismaStorage implements IDriveStorage {
|
|
70
92
|
private db: ExtendedPrismaClient;
|
|
@@ -73,9 +95,8 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
73
95
|
const backOffOptions = options?.transactionRetryBackoff;
|
|
74
96
|
this.db = getRetryTransactionsClient(db, {
|
|
75
97
|
...backOffOptions,
|
|
76
|
-
jitter: backOffOptions?.jitter ??
|
|
98
|
+
jitter: backOffOptions?.jitter ?? 'full'
|
|
77
99
|
});
|
|
78
|
-
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
async createDrive(id: string, drive: DocumentDriveStorage): Promise<void> {
|
|
@@ -136,8 +157,7 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
136
157
|
initialState: JSON.stringify(document.initialState),
|
|
137
158
|
lastModified: document.lastModified,
|
|
138
159
|
revision: JSON.stringify(document.revision),
|
|
139
|
-
id
|
|
140
|
-
state: JSON.stringify(document.state)
|
|
160
|
+
id
|
|
141
161
|
}
|
|
142
162
|
});
|
|
143
163
|
}
|
|
@@ -147,14 +167,8 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
147
167
|
drive: string,
|
|
148
168
|
id: string,
|
|
149
169
|
operations: Operation[],
|
|
150
|
-
header: DocumentHeader
|
|
151
|
-
newState: State<any, any> | undefined = undefined
|
|
170
|
+
header: DocumentHeader
|
|
152
171
|
): Promise<void> {
|
|
153
|
-
const document = await this.getDocument(drive, id, tx);
|
|
154
|
-
if (!document) {
|
|
155
|
-
throw new Error(`Document with id ${id} not found`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
172
|
try {
|
|
159
173
|
await tx.operation.createMany({
|
|
160
174
|
data: operations.map(op => ({
|
|
@@ -168,7 +182,10 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
168
182
|
scope: op.scope,
|
|
169
183
|
branch: 'main',
|
|
170
184
|
skip: op.skip,
|
|
171
|
-
context: op.context
|
|
185
|
+
context: op.context,
|
|
186
|
+
resultingState: op.resultingState
|
|
187
|
+
? Buffer.from(JSON.stringify(op.resultingState))
|
|
188
|
+
: undefined
|
|
172
189
|
}))
|
|
173
190
|
});
|
|
174
191
|
|
|
@@ -179,10 +196,34 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
179
196
|
},
|
|
180
197
|
data: {
|
|
181
198
|
lastModified: header.lastModified,
|
|
182
|
-
revision: JSON.stringify(header.revision)
|
|
183
|
-
state: JSON.stringify(newState)
|
|
199
|
+
revision: JSON.stringify(header.revision)
|
|
184
200
|
}
|
|
185
201
|
});
|
|
202
|
+
|
|
203
|
+
await Promise.all(
|
|
204
|
+
operations
|
|
205
|
+
.filter(o => o.attachments?.length)
|
|
206
|
+
.map(op => {
|
|
207
|
+
return tx.operation.update({
|
|
208
|
+
where: {
|
|
209
|
+
unique_operation: {
|
|
210
|
+
driveId: drive,
|
|
211
|
+
documentId: id,
|
|
212
|
+
index: op.index,
|
|
213
|
+
scope: op.scope,
|
|
214
|
+
branch: 'main'
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
data: {
|
|
218
|
+
attachments: {
|
|
219
|
+
createMany: {
|
|
220
|
+
data: op.attachments ?? []
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
})
|
|
226
|
+
);
|
|
186
227
|
} catch (e) {
|
|
187
228
|
// P2002: Unique constraint failed
|
|
188
229
|
// Operation with existing index
|
|
@@ -209,6 +250,7 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
209
250
|
);
|
|
210
251
|
|
|
211
252
|
if (!existingOperation || !conflictOp) {
|
|
253
|
+
console.error(e);
|
|
212
254
|
throw e;
|
|
213
255
|
} else {
|
|
214
256
|
throw new ConflictOperationError(
|
|
@@ -228,7 +270,7 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
228
270
|
callback: (document: DocumentStorage) => Promise<{
|
|
229
271
|
operations: Operation[];
|
|
230
272
|
header: DocumentHeader;
|
|
231
|
-
newState?: State<any, any> | undefined
|
|
273
|
+
newState?: State<any, any> | undefined;
|
|
232
274
|
}>
|
|
233
275
|
) {
|
|
234
276
|
let result: {
|
|
@@ -237,24 +279,25 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
237
279
|
newState?: State<any, any> | undefined;
|
|
238
280
|
} | null = null;
|
|
239
281
|
|
|
240
|
-
await this.db.$transaction(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const { operations, header, newState } = result;
|
|
248
|
-
return this._addDocumentOperations(
|
|
249
|
-
tx,
|
|
250
|
-
drive,
|
|
251
|
-
id,
|
|
252
|
-
operations,
|
|
253
|
-
header,
|
|
254
|
-
newState
|
|
255
|
-
);
|
|
256
|
-
}, { isolationLevel: "Serializable" });
|
|
282
|
+
await this.db.$transaction(
|
|
283
|
+
async tx => {
|
|
284
|
+
const document = await this.getDocument(drive, id, tx);
|
|
285
|
+
if (!document) {
|
|
286
|
+
throw new Error(`Document with id ${id} not found`);
|
|
287
|
+
}
|
|
288
|
+
result = await callback(document);
|
|
257
289
|
|
|
290
|
+
const { operations, header, newState } = result;
|
|
291
|
+
return this._addDocumentOperations(
|
|
292
|
+
tx,
|
|
293
|
+
drive,
|
|
294
|
+
id,
|
|
295
|
+
operations,
|
|
296
|
+
header
|
|
297
|
+
);
|
|
298
|
+
},
|
|
299
|
+
{ isolationLevel: 'Serializable' }
|
|
300
|
+
);
|
|
258
301
|
|
|
259
302
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
260
303
|
if (!result) {
|
|
@@ -268,14 +311,14 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
268
311
|
drive: string,
|
|
269
312
|
id: string,
|
|
270
313
|
operations: Operation[],
|
|
271
|
-
header: DocumentHeader
|
|
314
|
+
header: DocumentHeader
|
|
272
315
|
): Promise<void> {
|
|
273
316
|
return this._addDocumentOperations(
|
|
274
317
|
this.db,
|
|
275
318
|
drive,
|
|
276
319
|
id,
|
|
277
320
|
operations,
|
|
278
|
-
header
|
|
321
|
+
header
|
|
279
322
|
);
|
|
280
323
|
}
|
|
281
324
|
|
|
@@ -299,25 +342,18 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
299
342
|
where: {
|
|
300
343
|
id: id,
|
|
301
344
|
driveId: driveId
|
|
302
|
-
}
|
|
345
|
+
}
|
|
303
346
|
});
|
|
304
347
|
return count > 0;
|
|
305
348
|
}
|
|
306
349
|
|
|
307
350
|
async getDocument(driveId: string, id: string, tx?: Transaction) {
|
|
308
|
-
const
|
|
351
|
+
const prisma = tx ?? this.db;
|
|
352
|
+
const result = await prisma.document.findUnique({
|
|
309
353
|
where: {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
include: {
|
|
314
|
-
operations: {
|
|
315
|
-
orderBy: {
|
|
316
|
-
index: 'asc'
|
|
317
|
-
},
|
|
318
|
-
include: {
|
|
319
|
-
attachments: true
|
|
320
|
-
}
|
|
354
|
+
id_driveId: {
|
|
355
|
+
driveId,
|
|
356
|
+
id
|
|
321
357
|
}
|
|
322
358
|
}
|
|
323
359
|
});
|
|
@@ -326,8 +362,72 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
326
362
|
throw new Error(`Document with id ${id} not found`);
|
|
327
363
|
}
|
|
328
364
|
|
|
365
|
+
// retrieves operations with resulting state
|
|
366
|
+
// for the last operation of each scope
|
|
367
|
+
const operations = await prisma.$queryRaw<
|
|
368
|
+
Prisma.$OperationPayload['scalars'][]
|
|
369
|
+
>`
|
|
370
|
+
WITH ranked_operations AS (
|
|
371
|
+
SELECT
|
|
372
|
+
*,
|
|
373
|
+
ROW_NUMBER() OVER (PARTITION BY scope ORDER BY index DESC) AS rn
|
|
374
|
+
FROM "Operation"
|
|
375
|
+
)
|
|
376
|
+
SELECT
|
|
377
|
+
"id",
|
|
378
|
+
"opId",
|
|
379
|
+
"scope",
|
|
380
|
+
"branch",
|
|
381
|
+
"index",
|
|
382
|
+
"skip",
|
|
383
|
+
"hash",
|
|
384
|
+
"timestamp",
|
|
385
|
+
"input",
|
|
386
|
+
"type",
|
|
387
|
+
"context",
|
|
388
|
+
CASE
|
|
389
|
+
WHEN rn = 1 THEN "resultingState"
|
|
390
|
+
ELSE NULL
|
|
391
|
+
END AS "resultingState"
|
|
392
|
+
FROM ranked_operations
|
|
393
|
+
WHERE "driveId" = ${driveId} AND "documentId" = ${id}
|
|
394
|
+
ORDER BY scope, index;
|
|
395
|
+
`;
|
|
396
|
+
|
|
397
|
+
const operationIds = operations.map(o => o.id);
|
|
398
|
+
const attachments = await prisma.attachment.findMany({
|
|
399
|
+
where: {
|
|
400
|
+
operationId: {
|
|
401
|
+
in: operationIds
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const fileRegistry: FileRegistry = {};
|
|
407
|
+
const operationsByScope = operations.reduce<DocumentOperations<Action>>(
|
|
408
|
+
(acc, operation) => {
|
|
409
|
+
const scope = operation.scope as OperationScope;
|
|
410
|
+
if (!acc[scope]) {
|
|
411
|
+
acc[scope] = [];
|
|
412
|
+
}
|
|
413
|
+
const result = storageToOperation(operation);
|
|
414
|
+
result.attachments = attachments.filter(
|
|
415
|
+
a => a.operationId === operation.id
|
|
416
|
+
);
|
|
417
|
+
result.attachments.forEach(({ hash, ...file }) => {
|
|
418
|
+
fileRegistry[hash] = file;
|
|
419
|
+
});
|
|
420
|
+
acc[scope].push(result);
|
|
421
|
+
return acc;
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
global: [],
|
|
425
|
+
local: []
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
|
|
329
429
|
const dbDoc = result;
|
|
330
|
-
const doc = {
|
|
430
|
+
const doc: Document = {
|
|
331
431
|
created: dbDoc.created.toISOString(),
|
|
332
432
|
name: dbDoc.name ? dbDoc.name : '',
|
|
333
433
|
documentType: dbDoc.documentType,
|
|
@@ -335,22 +435,19 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
335
435
|
DocumentDriveState,
|
|
336
436
|
DocumentDriveLocalState
|
|
337
437
|
>,
|
|
338
|
-
state:
|
|
438
|
+
state: undefined,
|
|
339
439
|
lastModified: new Date(dbDoc.lastModified).toISOString(),
|
|
340
|
-
operations:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
},
|
|
348
|
-
clipboard: dbDoc.operations
|
|
349
|
-
.filter(op => op.clipboard)
|
|
350
|
-
.map(storageToOperation),
|
|
351
|
-
revision: JSON.parse(dbDoc.revision) as Record<OperationScope, number>
|
|
440
|
+
operations: operationsByScope,
|
|
441
|
+
clipboard: [],
|
|
442
|
+
revision: JSON.parse(dbDoc.revision) as Record<
|
|
443
|
+
OperationScope,
|
|
444
|
+
number
|
|
445
|
+
>,
|
|
446
|
+
attachments: {}
|
|
352
447
|
};
|
|
353
448
|
|
|
449
|
+
|
|
450
|
+
|
|
354
451
|
return doc;
|
|
355
452
|
}
|
|
356
453
|
|
|
@@ -408,4 +505,23 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
408
505
|
});
|
|
409
506
|
await this.deleteDocument('drives', id);
|
|
410
507
|
}
|
|
508
|
+
|
|
509
|
+
async getOperationResultingState(driveId: string, documentId: string, index: number, scope: string, branch: string): Promise<unknown> {
|
|
510
|
+
const operation = await this.db.operation.findUnique({
|
|
511
|
+
where: {
|
|
512
|
+
unique_operation: {
|
|
513
|
+
driveId,
|
|
514
|
+
documentId,
|
|
515
|
+
index,
|
|
516
|
+
scope,
|
|
517
|
+
branch
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
return operation?.resultingState?.toString();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
getDriveOperationResultingState(drive: string, index: number, scope: string, branch: string): Promise<unknown> {
|
|
525
|
+
return this.getOperationResultingState("drives", drive, index, scope, branch);
|
|
526
|
+
}
|
|
411
527
|
}
|
package/src/storage/sequelize.ts
CHANGED
|
@@ -25,8 +25,8 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
25
25
|
type: DataTypes.STRING,
|
|
26
26
|
primaryKey: true
|
|
27
27
|
},
|
|
28
|
-
id: DataTypes.STRING
|
|
29
|
-
})
|
|
28
|
+
id: DataTypes.STRING
|
|
29
|
+
});
|
|
30
30
|
const Document = this.db.define('document', {
|
|
31
31
|
id: {
|
|
32
32
|
type: DataTypes.STRING,
|
|
@@ -155,7 +155,7 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
155
155
|
drive: string,
|
|
156
156
|
id: string,
|
|
157
157
|
operations: Operation[],
|
|
158
|
-
header: DocumentHeader
|
|
158
|
+
header: DocumentHeader
|
|
159
159
|
): Promise<void> {
|
|
160
160
|
const document = await this.getDocument(drive, id);
|
|
161
161
|
if (!document) {
|
|
@@ -177,7 +177,8 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
177
177
|
timestamp: op.timestamp,
|
|
178
178
|
type: op.type,
|
|
179
179
|
scope: op.scope,
|
|
180
|
-
branch: 'main'
|
|
180
|
+
branch: 'main',
|
|
181
|
+
opId: op.id
|
|
181
182
|
}))
|
|
182
183
|
);
|
|
183
184
|
|
|
@@ -284,8 +285,8 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
284
285
|
where: {
|
|
285
286
|
id: id,
|
|
286
287
|
driveId: driveId
|
|
287
|
-
}
|
|
288
|
-
})
|
|
288
|
+
}
|
|
289
|
+
});
|
|
289
290
|
|
|
290
291
|
return count > 0;
|
|
291
292
|
}
|
|
@@ -323,6 +324,7 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
323
324
|
input: JSON;
|
|
324
325
|
type: string;
|
|
325
326
|
scope: string;
|
|
327
|
+
opId?: string;
|
|
326
328
|
}
|
|
327
329
|
];
|
|
328
330
|
revision: Required<Record<OperationScope, number>>;
|
|
@@ -348,13 +350,15 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
348
350
|
input: JSON;
|
|
349
351
|
type: string;
|
|
350
352
|
scope: string;
|
|
353
|
+
opId?: string;
|
|
351
354
|
}) => ({
|
|
352
355
|
hash: op.hash,
|
|
353
356
|
index: op.index,
|
|
354
357
|
timestamp: new Date(op.timestamp).toISOString(),
|
|
355
358
|
input: op.input,
|
|
356
359
|
type: op.type,
|
|
357
|
-
scope: op.scope as OperationScope
|
|
360
|
+
scope: op.scope as OperationScope,
|
|
361
|
+
id: op.opId
|
|
358
362
|
// attachments: fileRegistry
|
|
359
363
|
})
|
|
360
364
|
);
|
package/src/storage/types.ts
CHANGED
|
@@ -39,10 +39,10 @@ export interface IStorage {
|
|
|
39
39
|
callback: (document: DocumentStorage) => Promise<{
|
|
40
40
|
operations: Operation[];
|
|
41
41
|
header: DocumentHeader;
|
|
42
|
-
newState: State<any, any> | undefined
|
|
43
42
|
}>
|
|
44
43
|
): Promise<void>;
|
|
45
44
|
deleteDocument(drive: string, id: string): Promise<void>;
|
|
45
|
+
getOperationResultingState?(drive: string, id: string, index: number, scope: string, branch: string): Promise<unknown>;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export interface IDriveStorage extends IStorage {
|
|
@@ -64,4 +64,5 @@ export interface IDriveStorage extends IStorage {
|
|
|
64
64
|
header: DocumentHeader;
|
|
65
65
|
}>
|
|
66
66
|
): Promise<void>;
|
|
67
|
+
getDriveOperationResultingState?(drive: string, index: number, scope: string, branch: string): Promise<unknown>;
|
|
67
68
|
}
|
package/src/queue/manager.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { Unsubscribe } from "nanoevents";
|
|
2
|
-
import { IOperationResult } from "../server";
|
|
3
|
-
import { IQueueManager, QueueEvents } from "./types";
|
|
4
|
-
|
|
5
|
-
export class BaseQueueManager implements IQueueManager {
|
|
6
|
-
|
|
7
|
-
constructor() {
|
|
8
|
-
}
|
|
9
|
-
addJob(job: OperationJob): Promise<string> {
|
|
10
|
-
throw new Error("Method not implemented.");
|
|
11
|
-
}
|
|
12
|
-
getResult(driveId: string, documentId: string, jobId: JobId): Promise<IOperationResult | undefined> {
|
|
13
|
-
throw new Error("Method not implemented.");
|
|
14
|
-
}
|
|
15
|
-
init(processor: OperationJobProcessor, onError: (error: Error) => void): Promise<void> {
|
|
16
|
-
throw new Error("Method not implemented.");
|
|
17
|
-
}
|
|
18
|
-
on<K extends keyof QueueEvents>(this: this, event: K, cb: QueueEvents[K]): Unsubscribe {
|
|
19
|
-
throw new Error("Method not implemented.");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
}
|