@powerhousedao/reactor-local 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/dist/chunk-VFTWNJCU.js +3935 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +30 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +1 -0
- package/drizzle.config.ts +11 -0
- package/package.json +52 -0
- package/src/cli.ts +56 -0
- package/src/server.ts +85 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +45 -0
- package/types.d.ts +5 -0
- package/types.ts +10 -0
|
@@ -0,0 +1,3935 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url';
|
|
2
|
+
import path2 from 'path';
|
|
3
|
+
import { startAPI, setAdditionalContextFields, registerInternalListener, addSubgraph, createSchema } from '@powerhousedao/reactor-api';
|
|
4
|
+
import * as DocumentDrive from 'document-model-libs/document-drive';
|
|
5
|
+
import { isFileNode, utils as utils$1, actions, documentModel } from 'document-model-libs/document-drive';
|
|
6
|
+
import { utils } from 'document-model/document';
|
|
7
|
+
import { ClientError, gql, GraphQLClient } from 'graphql-request';
|
|
8
|
+
import { createNanoEvents } from 'nanoevents';
|
|
9
|
+
import { v4 } from 'uuid';
|
|
10
|
+
import { pascalCase } from 'change-case';
|
|
11
|
+
import { GraphQLError, buildSchema, GraphQLObjectType, GraphQLNonNull, GraphQLUnionType, GraphQLList, GraphQLScalarType } from 'graphql';
|
|
12
|
+
import stringify from 'json-stringify-deterministic';
|
|
13
|
+
import { readdirSync, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
14
|
+
import fs from 'fs/promises';
|
|
15
|
+
import sanitize from 'sanitize-filename';
|
|
16
|
+
import * as DocumentModelsLibs from 'document-model-libs/document-models';
|
|
17
|
+
import { module } from 'document-model/document-model';
|
|
18
|
+
import dotenv from 'dotenv';
|
|
19
|
+
import { drizzle } from 'drizzle-orm/connect';
|
|
20
|
+
import * as searchListener from '@powerhousedao/general-document-indexer';
|
|
21
|
+
|
|
22
|
+
// ../../node_modules/.pnpm/tsup@8.3.0_@swc+core@1.5.29_postcss@8.4.47_typescript@5.6.3/node_modules/tsup/assets/esm_shims.js
|
|
23
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
24
|
+
var getDirname = () => path2.dirname(getFilename());
|
|
25
|
+
var __dirname = /* @__PURE__ */ getDirname();
|
|
26
|
+
|
|
27
|
+
// ../document-drive/src/cache/memory.ts
|
|
28
|
+
var InMemoryCache = class {
|
|
29
|
+
cache = /* @__PURE__ */ new Map();
|
|
30
|
+
async setDocument(drive, id, document) {
|
|
31
|
+
const global2 = document.operations.global.map((e) => {
|
|
32
|
+
delete e.resultingState;
|
|
33
|
+
return e;
|
|
34
|
+
});
|
|
35
|
+
const local = document.operations.local.map((e) => {
|
|
36
|
+
delete e.resultingState;
|
|
37
|
+
return e;
|
|
38
|
+
});
|
|
39
|
+
const doc = { ...document, operations: { global: global2, local } };
|
|
40
|
+
if (!this.cache.has(drive)) {
|
|
41
|
+
this.cache.set(drive, /* @__PURE__ */ new Map());
|
|
42
|
+
}
|
|
43
|
+
this.cache.get(drive)?.set(id, doc);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
async deleteDocument(drive, id) {
|
|
47
|
+
return this.cache.get(drive)?.delete(id) ?? false;
|
|
48
|
+
}
|
|
49
|
+
async getDocument(drive, id) {
|
|
50
|
+
return this.cache.get(drive)?.get(id);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var memory_default = InMemoryCache;
|
|
54
|
+
|
|
55
|
+
// ../document-drive/src/server/error.ts
|
|
56
|
+
var DocumentModelNotFoundError = class extends Error {
|
|
57
|
+
constructor(id, cause) {
|
|
58
|
+
super(`Document model "${id}" not found`, { cause });
|
|
59
|
+
this.id = id;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var OperationError = class extends Error {
|
|
63
|
+
status;
|
|
64
|
+
operation;
|
|
65
|
+
constructor(status, operation, message, cause) {
|
|
66
|
+
super(message, { cause: cause ?? operation });
|
|
67
|
+
this.status = status;
|
|
68
|
+
this.operation = operation;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var ConflictOperationError = class extends OperationError {
|
|
72
|
+
constructor(existingOperation, newOperation) {
|
|
73
|
+
super(
|
|
74
|
+
"CONFLICT",
|
|
75
|
+
newOperation,
|
|
76
|
+
`Conflicting operation on index ${newOperation.index}`,
|
|
77
|
+
{ existingOperation, newOperation }
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var DriveAlreadyExistsError = class extends Error {
|
|
82
|
+
driveId;
|
|
83
|
+
constructor(driveId) {
|
|
84
|
+
super(`Drive already exists. ID: ${driveId}`);
|
|
85
|
+
this.driveId = driveId;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var DriveNotFoundError = class extends Error {
|
|
89
|
+
driveId;
|
|
90
|
+
constructor(driveId) {
|
|
91
|
+
super(`Drive with id ${driveId} not found`);
|
|
92
|
+
this.driveId = driveId;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var SynchronizationUnitNotFoundError = class extends Error {
|
|
96
|
+
syncUnitId;
|
|
97
|
+
constructor(message, syncUnitId) {
|
|
98
|
+
super(message);
|
|
99
|
+
this.syncUnitId = syncUnitId;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ../document-drive/src/utils/run-asap.ts
|
|
104
|
+
var RunAsap;
|
|
105
|
+
((RunAsap2) => {
|
|
106
|
+
RunAsap2.useMessageChannel = (() => {
|
|
107
|
+
if (typeof MessageChannel === "undefined") {
|
|
108
|
+
return new Error("MessageChannel is not supported");
|
|
109
|
+
}
|
|
110
|
+
return (task) => {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const signal = controller.signal;
|
|
113
|
+
const mc = new MessageChannel();
|
|
114
|
+
mc.port1.postMessage(null);
|
|
115
|
+
mc.port2.addEventListener(
|
|
116
|
+
"message",
|
|
117
|
+
() => {
|
|
118
|
+
task();
|
|
119
|
+
mc.port1.close();
|
|
120
|
+
mc.port2.close();
|
|
121
|
+
},
|
|
122
|
+
{ once: true, signal }
|
|
123
|
+
);
|
|
124
|
+
mc.port2.start();
|
|
125
|
+
return () => controller.abort();
|
|
126
|
+
};
|
|
127
|
+
})();
|
|
128
|
+
RunAsap2.usePostMessage = (() => {
|
|
129
|
+
const _main = (
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
131
|
+
typeof window === "object" && window || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
132
|
+
typeof global === "object" && global || typeof self === "object" && self
|
|
133
|
+
);
|
|
134
|
+
if (!_main) {
|
|
135
|
+
return new Error("No global object found");
|
|
136
|
+
}
|
|
137
|
+
const main = _main;
|
|
138
|
+
if (
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
140
|
+
!main.postMessage || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
141
|
+
!main.addEventListener || main.importScripts
|
|
142
|
+
) {
|
|
143
|
+
return new Error("postMessage is not supported");
|
|
144
|
+
}
|
|
145
|
+
let index = 0;
|
|
146
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
147
|
+
function getNewIndex() {
|
|
148
|
+
if (index === 9007199254740991) {
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
return ++index;
|
|
152
|
+
}
|
|
153
|
+
const MESSAGE_PREFIX = "com.usePostMessage" + Math.random();
|
|
154
|
+
main.addEventListener(
|
|
155
|
+
"message",
|
|
156
|
+
(e) => {
|
|
157
|
+
const event = e;
|
|
158
|
+
if (typeof event.data !== "string") {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (event.source !== main || !event.data.startsWith(MESSAGE_PREFIX)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const index2 = event.data.split(":").at(1);
|
|
165
|
+
if (index2 === void 0) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const i = +index2;
|
|
169
|
+
const task = tasks.get(i);
|
|
170
|
+
if (task) {
|
|
171
|
+
task();
|
|
172
|
+
tasks.delete(i);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
false
|
|
176
|
+
);
|
|
177
|
+
return (task) => {
|
|
178
|
+
const i = getNewIndex();
|
|
179
|
+
tasks.set(i, task);
|
|
180
|
+
main.postMessage(MESSAGE_PREFIX + ":" + i, { targetOrigin: "*" });
|
|
181
|
+
return () => {
|
|
182
|
+
tasks.delete(i);
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
})();
|
|
186
|
+
RunAsap2.useSetImmediate = (() => {
|
|
187
|
+
if (typeof window !== "undefined") {
|
|
188
|
+
return new Error("setImmediate is not supported on the browser");
|
|
189
|
+
}
|
|
190
|
+
if (typeof setImmediate === "undefined") {
|
|
191
|
+
return new Error("setImmediate is not supported");
|
|
192
|
+
}
|
|
193
|
+
return (task) => {
|
|
194
|
+
const id = setImmediate(task);
|
|
195
|
+
return () => clearImmediate(id);
|
|
196
|
+
};
|
|
197
|
+
})();
|
|
198
|
+
RunAsap2.useSetTimeout = /* @__PURE__ */ (() => {
|
|
199
|
+
return (task) => {
|
|
200
|
+
const id = setTimeout(task, 0);
|
|
201
|
+
return () => clearTimeout(id);
|
|
202
|
+
};
|
|
203
|
+
})();
|
|
204
|
+
function runAsap2(task) {
|
|
205
|
+
if (!(RunAsap2.useSetImmediate instanceof Error)) {
|
|
206
|
+
return (0, RunAsap2.useSetImmediate)(task);
|
|
207
|
+
} else if (!(RunAsap2.useMessageChannel instanceof Error)) {
|
|
208
|
+
return (0, RunAsap2.useMessageChannel)(task);
|
|
209
|
+
} else if (!(RunAsap2.usePostMessage instanceof Error)) {
|
|
210
|
+
return (0, RunAsap2.usePostMessage)(task);
|
|
211
|
+
} else {
|
|
212
|
+
return (0, RunAsap2.useSetTimeout)(task);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
RunAsap2.runAsap = runAsap2;
|
|
216
|
+
function runAsapAsync2(task, queueMethod = runAsap2) {
|
|
217
|
+
if (queueMethod instanceof Error) {
|
|
218
|
+
throw new Error("queueMethod is not supported", {
|
|
219
|
+
cause: queueMethod
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
queueMethod(() => {
|
|
224
|
+
task().then(resolve).catch(reject);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
RunAsap2.runAsapAsync = runAsapAsync2;
|
|
229
|
+
})(RunAsap || (RunAsap = {}));
|
|
230
|
+
|
|
231
|
+
// ../document-drive/src/utils/index.ts
|
|
232
|
+
var runAsap = RunAsap.runAsap;
|
|
233
|
+
var runAsapAsync = RunAsap.runAsapAsync;
|
|
234
|
+
function isDocumentDrive(document) {
|
|
235
|
+
return document.documentType === documentModel.id;
|
|
236
|
+
}
|
|
237
|
+
function mergeOperations(currentOperations, newOperations) {
|
|
238
|
+
const minIndexByScope = Object.keys(currentOperations).reduce((acc, curr) => {
|
|
239
|
+
const scope = curr;
|
|
240
|
+
acc[scope] = currentOperations[scope].at(-1)?.index ?? 0;
|
|
241
|
+
return acc;
|
|
242
|
+
}, {});
|
|
243
|
+
const conflictOp = newOperations.find(
|
|
244
|
+
(op) => op.index < (minIndexByScope[op.scope] ?? 0)
|
|
245
|
+
);
|
|
246
|
+
if (conflictOp) {
|
|
247
|
+
throw new OperationError(
|
|
248
|
+
"ERROR",
|
|
249
|
+
conflictOp,
|
|
250
|
+
`Tried to add operation with index ${conflictOp.index} and document is at index ${minIndexByScope[conflictOp.scope]}`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return newOperations.sort((a, b) => a.index - b.index).reduce((acc, curr) => {
|
|
254
|
+
const existingOperations = acc[curr.scope] || [];
|
|
255
|
+
return { ...acc, [curr.scope]: [...existingOperations, curr] };
|
|
256
|
+
}, currentOperations);
|
|
257
|
+
}
|
|
258
|
+
function generateUUID() {
|
|
259
|
+
return v4();
|
|
260
|
+
}
|
|
261
|
+
function isBefore(dateA, dateB) {
|
|
262
|
+
return new Date(dateA) < new Date(dateB);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ../document-drive/src/utils/logger.ts
|
|
266
|
+
var Logger = class {
|
|
267
|
+
#logger = console;
|
|
268
|
+
set logger(logger2) {
|
|
269
|
+
this.#logger = logger2;
|
|
270
|
+
}
|
|
271
|
+
log(...data) {
|
|
272
|
+
return this.#logger.log(...data);
|
|
273
|
+
}
|
|
274
|
+
info(...data) {
|
|
275
|
+
return this.#logger.info(...data);
|
|
276
|
+
}
|
|
277
|
+
warn(...data) {
|
|
278
|
+
return this.#logger.warn(...data);
|
|
279
|
+
}
|
|
280
|
+
error(...data) {
|
|
281
|
+
return this.#logger.error(...data);
|
|
282
|
+
}
|
|
283
|
+
debug(...data) {
|
|
284
|
+
return this.#logger.debug(...data);
|
|
285
|
+
}
|
|
286
|
+
trace(...data) {
|
|
287
|
+
return this.#logger.trace(...data);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
var loggerInstance = new Logger();
|
|
291
|
+
var logger = loggerInstance;
|
|
292
|
+
|
|
293
|
+
// ../document-drive/src/queue/types.ts
|
|
294
|
+
function isOperationJob(job) {
|
|
295
|
+
return "operations" in job;
|
|
296
|
+
}
|
|
297
|
+
function isActionJob(job) {
|
|
298
|
+
return "actions" in job;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ../document-drive/src/queue/base.ts
|
|
302
|
+
var MemoryQueue = class {
|
|
303
|
+
id;
|
|
304
|
+
blocked = false;
|
|
305
|
+
deleted = false;
|
|
306
|
+
items = [];
|
|
307
|
+
dependencies = new Array();
|
|
308
|
+
constructor(id) {
|
|
309
|
+
this.id = id;
|
|
310
|
+
}
|
|
311
|
+
async setDeleted(deleted) {
|
|
312
|
+
this.deleted = deleted;
|
|
313
|
+
}
|
|
314
|
+
async isDeleted() {
|
|
315
|
+
return this.deleted;
|
|
316
|
+
}
|
|
317
|
+
async addJob(data) {
|
|
318
|
+
this.items.push(data);
|
|
319
|
+
return Promise.resolve();
|
|
320
|
+
}
|
|
321
|
+
async getNextJob() {
|
|
322
|
+
const job = this.items.shift();
|
|
323
|
+
return Promise.resolve(job);
|
|
324
|
+
}
|
|
325
|
+
async amountOfJobs() {
|
|
326
|
+
return Promise.resolve(this.items.length);
|
|
327
|
+
}
|
|
328
|
+
getId() {
|
|
329
|
+
return this.id;
|
|
330
|
+
}
|
|
331
|
+
async setBlocked(blocked) {
|
|
332
|
+
this.blocked = blocked;
|
|
333
|
+
}
|
|
334
|
+
async isBlocked() {
|
|
335
|
+
return this.blocked;
|
|
336
|
+
}
|
|
337
|
+
async getJobs() {
|
|
338
|
+
return this.items;
|
|
339
|
+
}
|
|
340
|
+
async addDependencies(job) {
|
|
341
|
+
if (!this.dependencies.find((j) => j.jobId === job.jobId)) {
|
|
342
|
+
this.dependencies.push(job);
|
|
343
|
+
}
|
|
344
|
+
if (!this.isBlocked()) {
|
|
345
|
+
this.setBlocked(true);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async removeDependencies(job) {
|
|
349
|
+
this.dependencies = this.dependencies.filter(
|
|
350
|
+
(j) => j.jobId !== job.jobId && j.driveId !== job.driveId
|
|
351
|
+
);
|
|
352
|
+
if (this.dependencies.length === 0) {
|
|
353
|
+
await this.setBlocked(false);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
var BaseQueueManager = class {
|
|
358
|
+
emitter = createNanoEvents();
|
|
359
|
+
ticker = 0;
|
|
360
|
+
queues = [];
|
|
361
|
+
workers;
|
|
362
|
+
timeout;
|
|
363
|
+
delegate;
|
|
364
|
+
constructor(workers = 3, timeout = 0) {
|
|
365
|
+
this.workers = workers;
|
|
366
|
+
this.timeout = timeout;
|
|
367
|
+
}
|
|
368
|
+
async init(delegate, onError) {
|
|
369
|
+
this.delegate = delegate;
|
|
370
|
+
for (let i = 0; i < this.workers; i++) {
|
|
371
|
+
setTimeout(
|
|
372
|
+
() => this.processNextJob.bind(this)().catch(onError),
|
|
373
|
+
100 * i
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return Promise.resolve();
|
|
377
|
+
}
|
|
378
|
+
async addJob(job) {
|
|
379
|
+
if (!this.delegate) {
|
|
380
|
+
throw new Error("No server delegate defined");
|
|
381
|
+
}
|
|
382
|
+
const jobId = generateUUID();
|
|
383
|
+
const queue = this.getQueue(job.driveId, job.documentId);
|
|
384
|
+
if (await queue.isDeleted()) {
|
|
385
|
+
throw new Error("Queue is deleted");
|
|
386
|
+
}
|
|
387
|
+
const newDocument = job.documentId && !await this.delegate.checkDocumentExists(job.driveId, job.documentId);
|
|
388
|
+
if (newDocument && !await queue.isBlocked()) {
|
|
389
|
+
await queue.setBlocked(true);
|
|
390
|
+
const driveQueue = this.getQueue(job.driveId);
|
|
391
|
+
const jobs = await driveQueue.getJobs();
|
|
392
|
+
for (const driveJob of jobs) {
|
|
393
|
+
const actions3 = isOperationJob(driveJob) ? driveJob.operations : driveJob.actions;
|
|
394
|
+
const op = actions3.find((j) => {
|
|
395
|
+
const input = j.input;
|
|
396
|
+
return j.type === "ADD_FILE" && input.id === job.documentId;
|
|
397
|
+
});
|
|
398
|
+
if (op) {
|
|
399
|
+
await queue.addDependencies(driveJob);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const actions2 = isOperationJob(job) ? job.operations : job.actions;
|
|
404
|
+
const addFileOps = actions2.filter((j) => j.type === "ADD_FILE");
|
|
405
|
+
for (const addFileOp of addFileOps) {
|
|
406
|
+
const input = addFileOp.input;
|
|
407
|
+
const q = this.getQueue(job.driveId, input.id);
|
|
408
|
+
await q.addDependencies({ jobId, ...job });
|
|
409
|
+
}
|
|
410
|
+
const removeFileOps = actions2.filter(
|
|
411
|
+
(j) => j.type === "DELETE_NODE"
|
|
412
|
+
);
|
|
413
|
+
for (const removeFileOp of removeFileOps) {
|
|
414
|
+
const input = removeFileOp.input;
|
|
415
|
+
const queue2 = this.getQueue(job.driveId, input.id);
|
|
416
|
+
await queue2.setDeleted(true);
|
|
417
|
+
}
|
|
418
|
+
await queue.addJob({ jobId, ...job });
|
|
419
|
+
return jobId;
|
|
420
|
+
}
|
|
421
|
+
getQueue(driveId, documentId) {
|
|
422
|
+
const queueId = this.getQueueId(driveId, documentId);
|
|
423
|
+
let queue = this.queues.find((q) => q.getId() === queueId);
|
|
424
|
+
if (!queue) {
|
|
425
|
+
queue = new MemoryQueue(queueId);
|
|
426
|
+
this.queues.push(queue);
|
|
427
|
+
}
|
|
428
|
+
return queue;
|
|
429
|
+
}
|
|
430
|
+
removeQueue(driveId, documentId) {
|
|
431
|
+
const queueId = this.getQueueId(driveId, documentId);
|
|
432
|
+
this.queues = this.queues.filter((q) => q.getId() !== queueId);
|
|
433
|
+
this.emit("queueRemoved", queueId);
|
|
434
|
+
}
|
|
435
|
+
getQueueByIndex(index) {
|
|
436
|
+
const queue = this.queues[index];
|
|
437
|
+
if (queue) {
|
|
438
|
+
return queue;
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
getQueues() {
|
|
443
|
+
return this.queues.map((q) => q.getId());
|
|
444
|
+
}
|
|
445
|
+
retryNextJob(timeout) {
|
|
446
|
+
const _timeout = timeout !== void 0 ? timeout : this.timeout;
|
|
447
|
+
const retry = _timeout > 0 ? (fn) => setTimeout(fn, _timeout) : runAsap;
|
|
448
|
+
retry(() => this.processNextJob());
|
|
449
|
+
}
|
|
450
|
+
async findFirstNonEmptyQueue(ticker) {
|
|
451
|
+
const numQueues = this.queues.length;
|
|
452
|
+
for (let i = 0; i < numQueues; i++) {
|
|
453
|
+
const index = (ticker + i) % numQueues;
|
|
454
|
+
const queue = this.queues[index];
|
|
455
|
+
if (queue && await queue.amountOfJobs() > 0) {
|
|
456
|
+
return index;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
async processNextJob() {
|
|
462
|
+
if (!this.delegate) {
|
|
463
|
+
throw new Error("No server delegate defined");
|
|
464
|
+
}
|
|
465
|
+
if (this.queues.length === 0) {
|
|
466
|
+
this.retryNextJob();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const queue = this.queues[this.ticker];
|
|
470
|
+
if (!queue) {
|
|
471
|
+
this.ticker = 0;
|
|
472
|
+
this.retryNextJob();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const amountOfJobs = await queue.amountOfJobs();
|
|
476
|
+
if (amountOfJobs === 0) {
|
|
477
|
+
const nextTicker = await this.findFirstNonEmptyQueue(this.ticker);
|
|
478
|
+
if (nextTicker !== null) {
|
|
479
|
+
this.ticker = nextTicker;
|
|
480
|
+
this.retryNextJob(0);
|
|
481
|
+
} else {
|
|
482
|
+
this.retryNextJob();
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
this.ticker = this.ticker === this.queues.length - 1 ? 0 : this.ticker + 1;
|
|
487
|
+
const isBlocked = await queue.isBlocked();
|
|
488
|
+
if (isBlocked) {
|
|
489
|
+
this.retryNextJob();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
await queue.setBlocked(true);
|
|
493
|
+
const nextJob = await queue.getNextJob();
|
|
494
|
+
if (!nextJob) {
|
|
495
|
+
this.retryNextJob();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
const result = await this.delegate.processJob(nextJob);
|
|
500
|
+
const actions2 = isOperationJob(nextJob) ? nextJob.operations : nextJob.actions;
|
|
501
|
+
const addFileActions = actions2.filter((op) => op.type === "ADD_FILE");
|
|
502
|
+
if (addFileActions.length > 0) {
|
|
503
|
+
for (const addFile of addFileActions) {
|
|
504
|
+
const documentQueue = this.getQueue(
|
|
505
|
+
nextJob.driveId,
|
|
506
|
+
addFile.input.id
|
|
507
|
+
);
|
|
508
|
+
await documentQueue.removeDependencies(nextJob);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
this.emit("jobCompleted", nextJob, result);
|
|
512
|
+
} catch (e) {
|
|
513
|
+
logger.error(`job failed`, e);
|
|
514
|
+
this.emit("jobFailed", nextJob, e);
|
|
515
|
+
} finally {
|
|
516
|
+
await queue.setBlocked(false);
|
|
517
|
+
this.retryNextJob(0);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
emit(event, ...args) {
|
|
521
|
+
this.emitter.emit(event, ...args);
|
|
522
|
+
}
|
|
523
|
+
on(event, cb) {
|
|
524
|
+
return this.emitter.on(event, cb);
|
|
525
|
+
}
|
|
526
|
+
getQueueId(driveId, documentId) {
|
|
527
|
+
return `queue:${driveId}${documentId ? `:${documentId}` : ""}`;
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
async function requestGraphql(...args) {
|
|
531
|
+
const [url, ...requestArgs] = args;
|
|
532
|
+
const client = new GraphQLClient(url, { fetch });
|
|
533
|
+
const { errors, ...response } = await client.request(...requestArgs);
|
|
534
|
+
const result = { ...response };
|
|
535
|
+
if (errors?.length) {
|
|
536
|
+
result.errors = errors.map(
|
|
537
|
+
({ message, ...options2 }) => new GraphQLError(message, options2)
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
return result;
|
|
541
|
+
}
|
|
542
|
+
function getFields(type) {
|
|
543
|
+
if (type instanceof GraphQLObjectType) {
|
|
544
|
+
return Object.entries(type.getFields()).map(([fieldName, field]) => {
|
|
545
|
+
const fieldType = field.type instanceof GraphQLNonNull ? field.type.ofType : field.type;
|
|
546
|
+
if (fieldType instanceof GraphQLObjectType || fieldType instanceof GraphQLUnionType) {
|
|
547
|
+
return `${fieldName} { ${getFields(fieldType)} }`;
|
|
548
|
+
}
|
|
549
|
+
if (fieldType instanceof GraphQLList) {
|
|
550
|
+
const listItemType = fieldType.ofType instanceof GraphQLNonNull ? fieldType.ofType.ofType : fieldType.ofType;
|
|
551
|
+
if (listItemType instanceof GraphQLScalarType) {
|
|
552
|
+
return fieldName;
|
|
553
|
+
} else if (listItemType instanceof GraphQLObjectType || listItemType instanceof GraphQLUnionType) {
|
|
554
|
+
return `${fieldName} { ${getFields(listItemType)} }`;
|
|
555
|
+
} else {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`List item type ${listItemType.toString()} is not handled`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return fieldName;
|
|
562
|
+
}).join(" ");
|
|
563
|
+
} else if (type instanceof GraphQLUnionType) {
|
|
564
|
+
return type.getTypes().map((unionType) => {
|
|
565
|
+
return `... on ${unionType.name} { ${getFields(unionType)} }`;
|
|
566
|
+
}).join(" ");
|
|
567
|
+
}
|
|
568
|
+
return "";
|
|
569
|
+
}
|
|
570
|
+
function generateDocumentStateQueryFields(documentModel2, options2) {
|
|
571
|
+
const name = pascalCase(documentModel2.name);
|
|
572
|
+
const spec = documentModel2.specifications.at(-1);
|
|
573
|
+
if (!spec) {
|
|
574
|
+
throw new Error("No document model specification found");
|
|
575
|
+
}
|
|
576
|
+
const source = `${spec.state.global.schema} type Query { ${name}: ${name}State }`;
|
|
577
|
+
const schema = buildSchema(source, options2);
|
|
578
|
+
const queryType = schema.getQueryType();
|
|
579
|
+
if (!queryType) {
|
|
580
|
+
throw new Error("No query type found");
|
|
581
|
+
}
|
|
582
|
+
const fields = queryType.getFields();
|
|
583
|
+
const stateQuery = fields[name];
|
|
584
|
+
if (!stateQuery) {
|
|
585
|
+
throw new Error("No state query found");
|
|
586
|
+
}
|
|
587
|
+
const queryFields = getFields(stateQuery.type);
|
|
588
|
+
return queryFields;
|
|
589
|
+
}
|
|
590
|
+
async function requestPublicDrive(url) {
|
|
591
|
+
let drive;
|
|
592
|
+
try {
|
|
593
|
+
const result = await requestGraphql(
|
|
594
|
+
url,
|
|
595
|
+
gql`
|
|
596
|
+
query getDrive {
|
|
597
|
+
drive {
|
|
598
|
+
id
|
|
599
|
+
name
|
|
600
|
+
icon
|
|
601
|
+
slug
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
`
|
|
605
|
+
);
|
|
606
|
+
if (result.errors?.length || !result.drive) {
|
|
607
|
+
throw result.errors?.at(0) ?? new Error("Drive not found");
|
|
608
|
+
}
|
|
609
|
+
drive = result.drive;
|
|
610
|
+
} catch (e) {
|
|
611
|
+
logger.error(e);
|
|
612
|
+
throw new Error("Couldn't find drive info");
|
|
613
|
+
}
|
|
614
|
+
return drive;
|
|
615
|
+
}
|
|
616
|
+
async function fetchDocument(url, documentId, documentModelLib) {
|
|
617
|
+
const { documentModel: documentModel2, utils: utils3 } = documentModelLib;
|
|
618
|
+
const stateFields = generateDocumentStateQueryFields(documentModel2);
|
|
619
|
+
const name = pascalCase(documentModel2.name);
|
|
620
|
+
const result = await requestGraphql(
|
|
621
|
+
url,
|
|
622
|
+
gql`
|
|
623
|
+
query ($id: String!) {
|
|
624
|
+
document(id: $id) {
|
|
625
|
+
id
|
|
626
|
+
name
|
|
627
|
+
created
|
|
628
|
+
documentType
|
|
629
|
+
lastModified
|
|
630
|
+
revision
|
|
631
|
+
operations {
|
|
632
|
+
id
|
|
633
|
+
error
|
|
634
|
+
hash
|
|
635
|
+
index
|
|
636
|
+
skip
|
|
637
|
+
timestamp
|
|
638
|
+
type
|
|
639
|
+
inputText
|
|
640
|
+
context {
|
|
641
|
+
signer {
|
|
642
|
+
user {
|
|
643
|
+
address
|
|
644
|
+
networkId
|
|
645
|
+
chainId
|
|
646
|
+
}
|
|
647
|
+
app {
|
|
648
|
+
name
|
|
649
|
+
key
|
|
650
|
+
}
|
|
651
|
+
signatures
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
... on ${name} {
|
|
656
|
+
state {
|
|
657
|
+
${stateFields}
|
|
658
|
+
}
|
|
659
|
+
initialState {
|
|
660
|
+
${stateFields}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
`,
|
|
666
|
+
{ id: documentId }
|
|
667
|
+
);
|
|
668
|
+
const document = result.document ? {
|
|
669
|
+
...result.document,
|
|
670
|
+
revision: {
|
|
671
|
+
global: result.document.revision,
|
|
672
|
+
local: 0
|
|
673
|
+
},
|
|
674
|
+
state: utils3.createState({ global: result.document.state }),
|
|
675
|
+
operations: {
|
|
676
|
+
global: result.document.operations.map(({ inputText, ...o }) => ({
|
|
677
|
+
...o,
|
|
678
|
+
error: o.error ?? void 0,
|
|
679
|
+
scope: "global",
|
|
680
|
+
input: JSON.parse(inputText)
|
|
681
|
+
})),
|
|
682
|
+
local: []
|
|
683
|
+
},
|
|
684
|
+
attachments: {},
|
|
685
|
+
initialState: utils3.createExtendedState({
|
|
686
|
+
// TODO: getDocument should return all the initial state fields
|
|
687
|
+
created: result.document.created,
|
|
688
|
+
lastModified: result.document.created,
|
|
689
|
+
state: utils3.createState({
|
|
690
|
+
global: result.document.initialState
|
|
691
|
+
})
|
|
692
|
+
}),
|
|
693
|
+
clipboard: []
|
|
694
|
+
} : null;
|
|
695
|
+
return {
|
|
696
|
+
...result,
|
|
697
|
+
document
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ../document-drive/src/read-mode/errors.ts
|
|
702
|
+
var ReadDriveError = class extends Error {
|
|
703
|
+
};
|
|
704
|
+
var ReadDriveNotFoundError = class extends ReadDriveError {
|
|
705
|
+
constructor(driveId) {
|
|
706
|
+
super(`Read drive ${driveId} not found.`);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
var ReadDriveSlugNotFoundError = class extends ReadDriveError {
|
|
710
|
+
constructor(slug) {
|
|
711
|
+
super(`Read drive with slug ${slug} not found.`);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
var ReadDocumentNotFoundError = class extends ReadDriveError {
|
|
715
|
+
constructor(drive, id) {
|
|
716
|
+
super(`Document with id ${id} not found on read drive ${drive}.`);
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
// ../document-drive/src/read-mode/service.ts
|
|
721
|
+
var ReadModeService = class {
|
|
722
|
+
#getDocumentModel;
|
|
723
|
+
#drives = /* @__PURE__ */ new Map();
|
|
724
|
+
constructor(getDocumentModel) {
|
|
725
|
+
this.#getDocumentModel = getDocumentModel;
|
|
726
|
+
}
|
|
727
|
+
#parseGraphQLErrors(errors, driveId, documentId) {
|
|
728
|
+
for (const error of errors) {
|
|
729
|
+
if (error.message === `Drive with id ${driveId} not found`) {
|
|
730
|
+
return new ReadDriveNotFoundError(driveId);
|
|
731
|
+
} else if (documentId && error.message === `Document with id ${documentId} not found`) {
|
|
732
|
+
return new ReadDocumentNotFoundError(driveId, documentId);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const firstError = errors.at(0);
|
|
736
|
+
if (firstError) {
|
|
737
|
+
return firstError;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async #fetchDrive(id, url) {
|
|
741
|
+
const { errors, document } = await fetchDocument(
|
|
742
|
+
url,
|
|
743
|
+
id,
|
|
744
|
+
DocumentDrive
|
|
745
|
+
);
|
|
746
|
+
const error = errors ? this.#parseGraphQLErrors(errors, id) : void 0;
|
|
747
|
+
return error || document;
|
|
748
|
+
}
|
|
749
|
+
async fetchDrive(id) {
|
|
750
|
+
const drive = this.#drives.get(id);
|
|
751
|
+
if (!drive) {
|
|
752
|
+
return new ReadDriveNotFoundError(id);
|
|
753
|
+
}
|
|
754
|
+
const document = await this.fetchDocument(
|
|
755
|
+
id,
|
|
756
|
+
id,
|
|
757
|
+
DocumentDrive.documentModel.id
|
|
758
|
+
);
|
|
759
|
+
if (document instanceof Error) {
|
|
760
|
+
return document;
|
|
761
|
+
}
|
|
762
|
+
const result = { ...document, readContext: drive.context };
|
|
763
|
+
drive.drive = result;
|
|
764
|
+
return result;
|
|
765
|
+
}
|
|
766
|
+
async fetchDocument(driveId, documentId, documentType) {
|
|
767
|
+
const drive = this.#drives.get(driveId);
|
|
768
|
+
if (!drive) {
|
|
769
|
+
return new ReadDriveNotFoundError(driveId);
|
|
770
|
+
}
|
|
771
|
+
let documentModel2 = void 0;
|
|
772
|
+
try {
|
|
773
|
+
documentModel2 = this.#getDocumentModel(
|
|
774
|
+
documentType
|
|
775
|
+
);
|
|
776
|
+
} catch (error) {
|
|
777
|
+
return new DocumentModelNotFoundError(documentType, error);
|
|
778
|
+
}
|
|
779
|
+
const { url } = drive.context;
|
|
780
|
+
const { errors, document } = await fetchDocument(
|
|
781
|
+
url,
|
|
782
|
+
documentId,
|
|
783
|
+
documentModel2
|
|
784
|
+
);
|
|
785
|
+
if (errors) {
|
|
786
|
+
const error = this.#parseGraphQLErrors(errors, driveId, documentId);
|
|
787
|
+
if (error instanceof ReadDriveError) {
|
|
788
|
+
return error;
|
|
789
|
+
} else if (error) {
|
|
790
|
+
throw error;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!document) {
|
|
794
|
+
return new ReadDocumentNotFoundError(driveId, documentId);
|
|
795
|
+
}
|
|
796
|
+
return document;
|
|
797
|
+
}
|
|
798
|
+
async addReadDrive(url, options2) {
|
|
799
|
+
const { id } = options2?.expectedDriveInfo ?? await requestPublicDrive(url);
|
|
800
|
+
const result = await this.#fetchDrive(id, url);
|
|
801
|
+
if (result instanceof Error) {
|
|
802
|
+
throw result;
|
|
803
|
+
} else if (!result) {
|
|
804
|
+
throw new Error(`Drive "${id}" not found at ${url}`);
|
|
805
|
+
}
|
|
806
|
+
this.#drives.set(id, {
|
|
807
|
+
drive: result,
|
|
808
|
+
context: {
|
|
809
|
+
...options2,
|
|
810
|
+
url
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async getReadDrives() {
|
|
815
|
+
return Promise.resolve([...this.#drives.keys()]);
|
|
816
|
+
}
|
|
817
|
+
async getReadDrive(id) {
|
|
818
|
+
const result = this.#drives.get(id);
|
|
819
|
+
return Promise.resolve(
|
|
820
|
+
result ? { ...result.drive, readContext: result.context } : new ReadDriveNotFoundError(id)
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
async getReadDriveBySlug(slug) {
|
|
824
|
+
const readDrive = [...this.#drives.values()].find(
|
|
825
|
+
({ drive }) => drive.state.global.slug === slug
|
|
826
|
+
);
|
|
827
|
+
return Promise.resolve(
|
|
828
|
+
readDrive ? { ...readDrive.drive, readContext: readDrive.context } : new ReadDriveSlugNotFoundError(slug)
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
getReadDriveContext(id) {
|
|
832
|
+
return Promise.resolve(
|
|
833
|
+
this.#drives.get(id)?.context ?? new ReadDriveNotFoundError(id)
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
deleteReadDrive(id) {
|
|
837
|
+
const deleted = this.#drives.delete(id);
|
|
838
|
+
return Promise.resolve(
|
|
839
|
+
deleted ? void 0 : new ReadDriveNotFoundError(id)
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
// ../document-drive/src/read-mode/index.ts
|
|
845
|
+
function ReadModeServer(Base) {
|
|
846
|
+
return class ReadMode extends Base {
|
|
847
|
+
#readModeStorage;
|
|
848
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
849
|
+
constructor(...args) {
|
|
850
|
+
super(...args);
|
|
851
|
+
this.#readModeStorage = new ReadModeService(
|
|
852
|
+
this.getDocumentModel.bind(this)
|
|
853
|
+
);
|
|
854
|
+
this.#buildDrives().then((drives) => {
|
|
855
|
+
if (drives.length) {
|
|
856
|
+
this.#notifyListeners(drives, "add");
|
|
857
|
+
}
|
|
858
|
+
}).catch(logger.error);
|
|
859
|
+
}
|
|
860
|
+
async #buildDrives() {
|
|
861
|
+
const driveIds = await this.getReadDrives();
|
|
862
|
+
const drives = (await Promise.all(driveIds.map((driveId) => this.getReadDrive(driveId)))).filter((drive) => !(drive instanceof Error));
|
|
863
|
+
return drives;
|
|
864
|
+
}
|
|
865
|
+
#notifyListeners(drives, operation) {
|
|
866
|
+
this.#listeners.forEach((listener) => listener(drives, operation));
|
|
867
|
+
}
|
|
868
|
+
getReadDrives() {
|
|
869
|
+
return this.#readModeStorage.getReadDrives();
|
|
870
|
+
}
|
|
871
|
+
getReadDrive(id) {
|
|
872
|
+
return this.#readModeStorage.getReadDrive(id);
|
|
873
|
+
}
|
|
874
|
+
getReadDriveBySlug(slug) {
|
|
875
|
+
return this.#readModeStorage.getReadDriveBySlug(slug);
|
|
876
|
+
}
|
|
877
|
+
getReadDriveContext(id) {
|
|
878
|
+
return this.#readModeStorage.getReadDriveContext(id);
|
|
879
|
+
}
|
|
880
|
+
async addReadDrive(url, options2) {
|
|
881
|
+
await this.#readModeStorage.addReadDrive(url, options2);
|
|
882
|
+
this.#notifyListeners(await this.#buildDrives(), "add");
|
|
883
|
+
}
|
|
884
|
+
fetchDrive(id) {
|
|
885
|
+
return this.#readModeStorage.fetchDrive(id);
|
|
886
|
+
}
|
|
887
|
+
fetchDocument(driveId, documentId, documentType) {
|
|
888
|
+
return this.#readModeStorage.fetchDocument(
|
|
889
|
+
driveId,
|
|
890
|
+
documentId,
|
|
891
|
+
documentType
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
async deleteReadDrive(id) {
|
|
895
|
+
const error = await this.#readModeStorage.deleteReadDrive(id);
|
|
896
|
+
if (error) {
|
|
897
|
+
return error;
|
|
898
|
+
}
|
|
899
|
+
this.#notifyListeners(await this.#buildDrives(), "delete");
|
|
900
|
+
}
|
|
901
|
+
async migrateReadDrive(id, options2) {
|
|
902
|
+
const result = await this.getReadDriveContext(id);
|
|
903
|
+
if (result instanceof Error) {
|
|
904
|
+
return result;
|
|
905
|
+
}
|
|
906
|
+
const { url, ...readOptions } = result;
|
|
907
|
+
try {
|
|
908
|
+
const newDrive = await this.addRemoteDrive(url, options2);
|
|
909
|
+
return newDrive;
|
|
910
|
+
} catch (error) {
|
|
911
|
+
logger.error(error);
|
|
912
|
+
await this.addReadDrive(result.url, readOptions);
|
|
913
|
+
throw error;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
onReadDrivesUpdate(listener) {
|
|
917
|
+
this.#listeners.add(listener);
|
|
918
|
+
return Promise.resolve(() => this.#listeners.delete(listener));
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// ../document-drive/src/storage/memory.ts
|
|
924
|
+
var MemoryStorage = class {
|
|
925
|
+
documents;
|
|
926
|
+
drives;
|
|
927
|
+
slugToDriveId = {};
|
|
928
|
+
constructor() {
|
|
929
|
+
this.documents = {};
|
|
930
|
+
this.drives = {};
|
|
931
|
+
}
|
|
932
|
+
checkDocumentExists(drive, id) {
|
|
933
|
+
return Promise.resolve(this.documents[drive]?.[id] !== void 0);
|
|
934
|
+
}
|
|
935
|
+
async getDocuments(drive) {
|
|
936
|
+
return Object.keys(this.documents[drive] ?? {});
|
|
937
|
+
}
|
|
938
|
+
async getDocument(driveId, id) {
|
|
939
|
+
const drive = this.documents[driveId];
|
|
940
|
+
if (!drive) {
|
|
941
|
+
throw new DriveNotFoundError(driveId);
|
|
942
|
+
}
|
|
943
|
+
const document = drive[id];
|
|
944
|
+
if (!document) {
|
|
945
|
+
throw new Error(`Document with id ${id} not found`);
|
|
946
|
+
}
|
|
947
|
+
return document;
|
|
948
|
+
}
|
|
949
|
+
async saveDocument(drive, id, document) {
|
|
950
|
+
this.documents[drive] = this.documents[drive] ?? {};
|
|
951
|
+
this.documents[drive][id] = document;
|
|
952
|
+
}
|
|
953
|
+
async clearStorage() {
|
|
954
|
+
this.documents = {};
|
|
955
|
+
this.drives = {};
|
|
956
|
+
}
|
|
957
|
+
async createDocument(drive, id, document) {
|
|
958
|
+
this.documents[drive] = this.documents[drive] ?? {};
|
|
959
|
+
const {
|
|
960
|
+
operations,
|
|
961
|
+
initialState,
|
|
962
|
+
name,
|
|
963
|
+
revision,
|
|
964
|
+
documentType,
|
|
965
|
+
created,
|
|
966
|
+
lastModified,
|
|
967
|
+
clipboard,
|
|
968
|
+
state
|
|
969
|
+
} = document;
|
|
970
|
+
this.documents[drive][id] = {
|
|
971
|
+
operations,
|
|
972
|
+
initialState,
|
|
973
|
+
name,
|
|
974
|
+
revision,
|
|
975
|
+
documentType,
|
|
976
|
+
created,
|
|
977
|
+
lastModified,
|
|
978
|
+
clipboard,
|
|
979
|
+
state
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
async addDocumentOperations(drive, id, operations, header) {
|
|
983
|
+
const document = await this.getDocument(drive, id);
|
|
984
|
+
if (!document) {
|
|
985
|
+
throw new Error(`Document with id ${id} not found`);
|
|
986
|
+
}
|
|
987
|
+
const mergedOperations = mergeOperations(document.operations, operations);
|
|
988
|
+
this.documents[drive][id] = {
|
|
989
|
+
...document,
|
|
990
|
+
...header,
|
|
991
|
+
operations: mergedOperations
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
async deleteDocument(drive, id) {
|
|
995
|
+
if (!this.documents[drive]) {
|
|
996
|
+
throw new DriveNotFoundError(drive);
|
|
997
|
+
}
|
|
998
|
+
delete this.documents[drive][id];
|
|
999
|
+
}
|
|
1000
|
+
async getDrives() {
|
|
1001
|
+
return Object.keys(this.drives);
|
|
1002
|
+
}
|
|
1003
|
+
async getDrive(id) {
|
|
1004
|
+
const drive = this.drives[id];
|
|
1005
|
+
if (!drive) {
|
|
1006
|
+
throw new DriveNotFoundError(id);
|
|
1007
|
+
}
|
|
1008
|
+
return drive;
|
|
1009
|
+
}
|
|
1010
|
+
async getDriveBySlug(slug) {
|
|
1011
|
+
const driveId = this.slugToDriveId[slug];
|
|
1012
|
+
if (!driveId) {
|
|
1013
|
+
throw new Error(`Drive with slug ${slug} not found`);
|
|
1014
|
+
}
|
|
1015
|
+
return this.getDrive(driveId);
|
|
1016
|
+
}
|
|
1017
|
+
async createDrive(id, drive) {
|
|
1018
|
+
this.drives[id] = drive;
|
|
1019
|
+
this.documents[id] = {};
|
|
1020
|
+
const { slug } = drive.initialState.state.global;
|
|
1021
|
+
if (slug) {
|
|
1022
|
+
this.slugToDriveId[slug] = id;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
async addDriveOperations(id, operations, header) {
|
|
1026
|
+
const drive = await this.getDrive(id);
|
|
1027
|
+
const mergedOperations = mergeOperations(drive.operations, operations);
|
|
1028
|
+
this.drives[id] = {
|
|
1029
|
+
...drive,
|
|
1030
|
+
...header,
|
|
1031
|
+
operations: mergedOperations
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
async deleteDrive(id) {
|
|
1035
|
+
delete this.documents[id];
|
|
1036
|
+
delete this.drives[id];
|
|
1037
|
+
}
|
|
1038
|
+
async getSynchronizationUnitsRevision(units) {
|
|
1039
|
+
const results = await Promise.allSettled(
|
|
1040
|
+
units.map(async (unit) => {
|
|
1041
|
+
try {
|
|
1042
|
+
const document = await (unit.documentId ? this.getDocument(unit.driveId, unit.documentId) : this.getDrive(unit.driveId));
|
|
1043
|
+
if (!document) {
|
|
1044
|
+
return void 0;
|
|
1045
|
+
}
|
|
1046
|
+
const operation = document.operations[unit.scope].at(-1);
|
|
1047
|
+
if (operation) {
|
|
1048
|
+
return {
|
|
1049
|
+
driveId: unit.driveId,
|
|
1050
|
+
documentId: unit.documentId,
|
|
1051
|
+
scope: unit.scope,
|
|
1052
|
+
branch: unit.branch,
|
|
1053
|
+
lastUpdated: operation.timestamp,
|
|
1054
|
+
revision: operation.index
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
} catch {
|
|
1058
|
+
return void 0;
|
|
1059
|
+
}
|
|
1060
|
+
})
|
|
1061
|
+
);
|
|
1062
|
+
return results.reduce((acc, curr) => {
|
|
1063
|
+
if (curr.status === "fulfilled" && curr.value !== void 0) {
|
|
1064
|
+
acc.push(curr.value);
|
|
1065
|
+
}
|
|
1066
|
+
return acc;
|
|
1067
|
+
}, []);
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
// ../document-drive/src/utils/default-drives-manager.ts
|
|
1072
|
+
function isReadModeDriveServer(obj) {
|
|
1073
|
+
return typeof obj.getReadDrives === "function";
|
|
1074
|
+
}
|
|
1075
|
+
var DefaultDrivesManager = class {
|
|
1076
|
+
constructor(server, delegate, options2) {
|
|
1077
|
+
this.server = server;
|
|
1078
|
+
this.delegate = delegate;
|
|
1079
|
+
if (options2?.defaultDrives.remoteDrives) {
|
|
1080
|
+
for (const defaultDrive of options2.defaultDrives.remoteDrives) {
|
|
1081
|
+
this.defaultRemoteDrives.set(defaultDrive.url, {
|
|
1082
|
+
...defaultDrive,
|
|
1083
|
+
status: "PENDING"
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
this.removeOldRemoteDrivesConfig = options2?.defaultDrives.removeOldRemoteDrives || {
|
|
1088
|
+
strategy: "preserve-all"
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
defaultRemoteDrives = /* @__PURE__ */ new Map();
|
|
1092
|
+
removeOldRemoteDrivesConfig;
|
|
1093
|
+
getDefaultRemoteDrives() {
|
|
1094
|
+
return new Map(
|
|
1095
|
+
JSON.parse(
|
|
1096
|
+
JSON.stringify(Array.from(this.defaultRemoteDrives))
|
|
1097
|
+
)
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
async deleteDriveById(driveId) {
|
|
1101
|
+
try {
|
|
1102
|
+
await this.server.deleteDrive(driveId);
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
if (!(error instanceof DriveNotFoundError)) {
|
|
1105
|
+
logger.error(error);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
async preserveDrivesById(driveIdsToPreserve, drives, removeStrategy = "detach") {
|
|
1110
|
+
const getAllDrives = drives.map((driveId) => this.server.getDrive(driveId));
|
|
1111
|
+
const drivesToRemove = (await Promise.all(getAllDrives)).filter(
|
|
1112
|
+
(drive) => drive.state.local.listeners.length > 0 || drive.state.local.triggers.length > 0
|
|
1113
|
+
).filter((drive) => !driveIdsToPreserve.includes(drive.state.global.id));
|
|
1114
|
+
const driveIds = drivesToRemove.map((drive) => drive.state.global.id);
|
|
1115
|
+
if (removeStrategy === "detach") {
|
|
1116
|
+
await this.detachDrivesById(driveIds);
|
|
1117
|
+
} else {
|
|
1118
|
+
await this.removeDrivesById(driveIds);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
async removeDrivesById(driveIds) {
|
|
1122
|
+
for (const driveId of driveIds) {
|
|
1123
|
+
await this.deleteDriveById(driveId);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
async detachDrivesById(driveIds) {
|
|
1127
|
+
const detachDrivesPromises = driveIds.map(
|
|
1128
|
+
(driveId) => this.delegate.detachDrive(driveId)
|
|
1129
|
+
);
|
|
1130
|
+
await Promise.all(detachDrivesPromises);
|
|
1131
|
+
}
|
|
1132
|
+
async removeOldremoteDrives() {
|
|
1133
|
+
const driveids = await this.server.getDrives();
|
|
1134
|
+
switch (this.removeOldRemoteDrivesConfig.strategy) {
|
|
1135
|
+
case "preserve-by-id-and-detach":
|
|
1136
|
+
case "preserve-by-id": {
|
|
1137
|
+
const detach = this.removeOldRemoteDrivesConfig.strategy === "preserve-by-id-and-detach" ? "detach" : "remove";
|
|
1138
|
+
await this.preserveDrivesById(
|
|
1139
|
+
this.removeOldRemoteDrivesConfig.ids,
|
|
1140
|
+
driveids,
|
|
1141
|
+
detach
|
|
1142
|
+
);
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
case "preserve-by-url-and-detach":
|
|
1146
|
+
case "preserve-by-url": {
|
|
1147
|
+
const detach = this.removeOldRemoteDrivesConfig.strategy === "preserve-by-url-and-detach" ? "detach" : "remove";
|
|
1148
|
+
const getDrivesInfo = this.removeOldRemoteDrivesConfig.urls.map(
|
|
1149
|
+
(url) => requestPublicDrive(url)
|
|
1150
|
+
);
|
|
1151
|
+
const drivesIdsToPreserve = (await Promise.all(getDrivesInfo)).map(
|
|
1152
|
+
(driveInfo) => driveInfo.id
|
|
1153
|
+
);
|
|
1154
|
+
await this.preserveDrivesById(drivesIdsToPreserve, driveids, detach);
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
case "remove-by-id": {
|
|
1158
|
+
const drivesIdsToRemove = this.removeOldRemoteDrivesConfig.ids.filter(
|
|
1159
|
+
(driveId) => driveids.includes(driveId)
|
|
1160
|
+
);
|
|
1161
|
+
await this.removeDrivesById(drivesIdsToRemove);
|
|
1162
|
+
break;
|
|
1163
|
+
}
|
|
1164
|
+
case "remove-by-url": {
|
|
1165
|
+
const getDrivesInfo = this.removeOldRemoteDrivesConfig.urls.map(
|
|
1166
|
+
(driveUrl) => requestPublicDrive(driveUrl)
|
|
1167
|
+
);
|
|
1168
|
+
const drivesInfo = await Promise.all(getDrivesInfo);
|
|
1169
|
+
const drivesIdsToRemove = drivesInfo.map((driveInfo) => driveInfo.id).filter((driveId) => driveids.includes(driveId));
|
|
1170
|
+
await this.removeDrivesById(drivesIdsToRemove);
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
case "remove-all": {
|
|
1174
|
+
const getDrives = driveids.map(
|
|
1175
|
+
(driveId) => this.server.getDrive(driveId)
|
|
1176
|
+
);
|
|
1177
|
+
const drives = await Promise.all(getDrives);
|
|
1178
|
+
const drivesToRemove = drives.filter(
|
|
1179
|
+
(drive) => drive.state.local.listeners.length > 0 || drive.state.local.triggers.length > 0
|
|
1180
|
+
).map((drive) => drive.state.global.id);
|
|
1181
|
+
await this.removeDrivesById(drivesToRemove);
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
case "detach-by-id": {
|
|
1185
|
+
const drivesIdsToRemove = this.removeOldRemoteDrivesConfig.ids.filter(
|
|
1186
|
+
(driveId) => driveids.includes(driveId)
|
|
1187
|
+
);
|
|
1188
|
+
const detachDrivesPromises = drivesIdsToRemove.map(
|
|
1189
|
+
(driveId) => this.delegate.detachDrive(driveId)
|
|
1190
|
+
);
|
|
1191
|
+
await Promise.all(detachDrivesPromises);
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
case "detach-by-url": {
|
|
1195
|
+
const getDrivesInfo = this.removeOldRemoteDrivesConfig.urls.map(
|
|
1196
|
+
(driveUrl) => requestPublicDrive(driveUrl)
|
|
1197
|
+
);
|
|
1198
|
+
const drivesInfo = await Promise.all(getDrivesInfo);
|
|
1199
|
+
const drivesIdsToRemove = drivesInfo.map((driveInfo) => driveInfo.id).filter((driveId) => driveids.includes(driveId));
|
|
1200
|
+
const detachDrivesPromises = drivesIdsToRemove.map(
|
|
1201
|
+
(driveId) => this.delegate.detachDrive(driveId)
|
|
1202
|
+
);
|
|
1203
|
+
await Promise.all(detachDrivesPromises);
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async setAllDefaultDrivesAccessLevel(level) {
|
|
1209
|
+
const drives = this.defaultRemoteDrives.values();
|
|
1210
|
+
for (const drive of drives) {
|
|
1211
|
+
await this.setDefaultDriveAccessLevel(drive.url, level);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
async setDefaultDriveAccessLevel(url, level) {
|
|
1215
|
+
const drive = this.defaultRemoteDrives.get(url);
|
|
1216
|
+
if (drive && drive.options.accessLevel !== level) {
|
|
1217
|
+
const newDriveValue = {
|
|
1218
|
+
...drive,
|
|
1219
|
+
options: { ...drive.options, accessLevel: level }
|
|
1220
|
+
};
|
|
1221
|
+
this.defaultRemoteDrives.set(url, newDriveValue);
|
|
1222
|
+
await this.initializeDefaultRemoteDrives([newDriveValue]);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async initializeDefaultRemoteDrives(defaultDrives = Array.from(
|
|
1226
|
+
this.defaultRemoteDrives.values()
|
|
1227
|
+
)) {
|
|
1228
|
+
const drives = await this.server.getDrives();
|
|
1229
|
+
const readServer = isReadModeDriveServer(this.server) ? this.server : void 0;
|
|
1230
|
+
const readDrives = await readServer?.getReadDrives();
|
|
1231
|
+
for (const remoteDrive of defaultDrives) {
|
|
1232
|
+
let remoteDriveInfo = { ...remoteDrive };
|
|
1233
|
+
try {
|
|
1234
|
+
const driveInfo = remoteDrive.metadata ?? await requestPublicDrive(remoteDrive.url);
|
|
1235
|
+
remoteDriveInfo = { ...remoteDrive, metadata: driveInfo };
|
|
1236
|
+
this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
|
|
1237
|
+
const driveIsAdded = drives.includes(driveInfo.id);
|
|
1238
|
+
const readDriveIsAdded = readDrives?.includes(driveInfo.id);
|
|
1239
|
+
const hasAccessLevel = remoteDrive.options.accessLevel !== void 0;
|
|
1240
|
+
const readMode = readServer && remoteDrive.options.accessLevel === "READ";
|
|
1241
|
+
const isAdded = readMode ? readDriveIsAdded : driveIsAdded;
|
|
1242
|
+
const driveToDelete = hasAccessLevel && (readMode ? driveIsAdded : readDriveIsAdded);
|
|
1243
|
+
if (driveToDelete) {
|
|
1244
|
+
try {
|
|
1245
|
+
await (readMode ? this.server.deleteDrive(driveInfo.id) : readServer?.deleteReadDrive(driveInfo.id));
|
|
1246
|
+
} catch (e) {
|
|
1247
|
+
logger.error(e);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (isAdded) {
|
|
1251
|
+
remoteDriveInfo.status = "ALREADY_ADDED";
|
|
1252
|
+
this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
|
|
1253
|
+
this.delegate.emit(
|
|
1254
|
+
"ALREADY_ADDED",
|
|
1255
|
+
this.defaultRemoteDrives,
|
|
1256
|
+
remoteDriveInfo,
|
|
1257
|
+
driveInfo.id,
|
|
1258
|
+
driveInfo.name
|
|
1259
|
+
);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
remoteDriveInfo.status = "ADDING";
|
|
1263
|
+
this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
|
|
1264
|
+
this.delegate.emit("ADDING", this.defaultRemoteDrives, remoteDriveInfo);
|
|
1265
|
+
if (!hasAccessLevel && readServer || readMode) {
|
|
1266
|
+
await readServer.addReadDrive(remoteDrive.url, {
|
|
1267
|
+
...remoteDrive.options,
|
|
1268
|
+
expectedDriveInfo: driveInfo
|
|
1269
|
+
});
|
|
1270
|
+
} else {
|
|
1271
|
+
await this.server.addRemoteDrive(remoteDrive.url, {
|
|
1272
|
+
...remoteDrive.options,
|
|
1273
|
+
expectedDriveInfo: driveInfo
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
remoteDriveInfo.status = "SUCCESS";
|
|
1277
|
+
this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
|
|
1278
|
+
this.delegate.emit(
|
|
1279
|
+
"SUCCESS",
|
|
1280
|
+
this.defaultRemoteDrives,
|
|
1281
|
+
remoteDriveInfo,
|
|
1282
|
+
driveInfo.id,
|
|
1283
|
+
driveInfo.name
|
|
1284
|
+
);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
remoteDriveInfo.status = "ERROR";
|
|
1287
|
+
this.defaultRemoteDrives.set(remoteDrive.url, remoteDriveInfo);
|
|
1288
|
+
this.delegate.emit(
|
|
1289
|
+
"ERROR",
|
|
1290
|
+
this.defaultRemoteDrives,
|
|
1291
|
+
remoteDriveInfo,
|
|
1292
|
+
void 0,
|
|
1293
|
+
void 0,
|
|
1294
|
+
error
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
var {
|
|
1301
|
+
attachBranch,
|
|
1302
|
+
garbageCollect,
|
|
1303
|
+
groupOperationsByScope,
|
|
1304
|
+
merge,
|
|
1305
|
+
split,
|
|
1306
|
+
precedes,
|
|
1307
|
+
removeExistingOperations,
|
|
1308
|
+
reshuffleByTimestamp,
|
|
1309
|
+
sortOperations,
|
|
1310
|
+
addUndo,
|
|
1311
|
+
checkOperationsIntegrity,
|
|
1312
|
+
checkCleanedOperationsIntegrity,
|
|
1313
|
+
reshuffleByTimestampAndIndex,
|
|
1314
|
+
nextSkipNumber,
|
|
1315
|
+
prepareOperations,
|
|
1316
|
+
IntegrityIssueSubType,
|
|
1317
|
+
IntegrityIssueType
|
|
1318
|
+
} = utils.documentHelpers;
|
|
1319
|
+
|
|
1320
|
+
// ../document-drive/src/server/types.ts
|
|
1321
|
+
var AbstractDocumentDriveServer = class {
|
|
1322
|
+
};
|
|
1323
|
+
var DefaultListenerManagerOptions = {
|
|
1324
|
+
sequentialUpdates: true
|
|
1325
|
+
};
|
|
1326
|
+
var BaseListenerManager = class {
|
|
1327
|
+
drive;
|
|
1328
|
+
listenerState = /* @__PURE__ */ new Map();
|
|
1329
|
+
options;
|
|
1330
|
+
transmitters = {};
|
|
1331
|
+
constructor(drive, listenerState = /* @__PURE__ */ new Map(), options2 = DefaultListenerManagerOptions) {
|
|
1332
|
+
this.drive = drive;
|
|
1333
|
+
this.listenerState = listenerState;
|
|
1334
|
+
this.options = { ...DefaultListenerManagerOptions, ...options2 };
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
// ../document-drive/src/server/utils.ts
|
|
1339
|
+
function buildRevisionsFilter(strands, driveId, documentId) {
|
|
1340
|
+
return strands.reduce((acc, s) => {
|
|
1341
|
+
if (!(s.driveId === driveId && s.documentId === documentId)) {
|
|
1342
|
+
return acc;
|
|
1343
|
+
}
|
|
1344
|
+
acc[s.scope] = s.operations[s.operations.length - 1]?.index ?? -1;
|
|
1345
|
+
return acc;
|
|
1346
|
+
}, {});
|
|
1347
|
+
}
|
|
1348
|
+
function filterOperationsByRevision(operations, revisions) {
|
|
1349
|
+
if (!revisions) {
|
|
1350
|
+
return operations;
|
|
1351
|
+
}
|
|
1352
|
+
return Object.keys(operations).reduce((acc, scope) => {
|
|
1353
|
+
const revision = revisions[scope];
|
|
1354
|
+
if (revision !== void 0) {
|
|
1355
|
+
acc[scope] = operations[scope].filter((op) => op.index <= revision);
|
|
1356
|
+
}
|
|
1357
|
+
return acc;
|
|
1358
|
+
}, operations);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// ../document-drive/src/server/listener/transmitter/internal.ts
|
|
1362
|
+
var InternalTransmitter = class {
|
|
1363
|
+
drive;
|
|
1364
|
+
listener;
|
|
1365
|
+
receiver;
|
|
1366
|
+
constructor(listener, drive) {
|
|
1367
|
+
this.listener = listener;
|
|
1368
|
+
this.drive = drive;
|
|
1369
|
+
}
|
|
1370
|
+
async transmit(strands) {
|
|
1371
|
+
if (!this.receiver) {
|
|
1372
|
+
return [];
|
|
1373
|
+
}
|
|
1374
|
+
const retrievedDocuments = /* @__PURE__ */ new Map();
|
|
1375
|
+
const updates = [];
|
|
1376
|
+
for (const strand of strands) {
|
|
1377
|
+
let document = retrievedDocuments.get(
|
|
1378
|
+
`${strand.driveId}:${strand.documentId}`
|
|
1379
|
+
);
|
|
1380
|
+
if (!document) {
|
|
1381
|
+
const revisions = buildRevisionsFilter(
|
|
1382
|
+
strands,
|
|
1383
|
+
strand.driveId,
|
|
1384
|
+
strand.documentId
|
|
1385
|
+
);
|
|
1386
|
+
document = await (strand.documentId ? this.drive.getDocument(strand.driveId, strand.documentId, {
|
|
1387
|
+
revisions
|
|
1388
|
+
}) : this.drive.getDrive(strand.driveId, { revisions }));
|
|
1389
|
+
retrievedDocuments.set(
|
|
1390
|
+
`${strand.driveId}:${strand.documentId}`,
|
|
1391
|
+
document
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
updates.push({ ...strand, state: document.state[strand.scope] });
|
|
1395
|
+
}
|
|
1396
|
+
try {
|
|
1397
|
+
await this.receiver.transmit(updates);
|
|
1398
|
+
return strands.map(({ operations, ...s }) => ({
|
|
1399
|
+
...s,
|
|
1400
|
+
status: "SUCCESS",
|
|
1401
|
+
revision: operations[operations.length - 1]?.index ?? -1
|
|
1402
|
+
}));
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
logger.error(error);
|
|
1405
|
+
return strands.map(({ operations, ...s }) => ({
|
|
1406
|
+
...s,
|
|
1407
|
+
status: "ERROR",
|
|
1408
|
+
revision: (operations[0]?.index ?? 0) - 1
|
|
1409
|
+
}));
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
setReceiver(receiver) {
|
|
1413
|
+
this.receiver = receiver;
|
|
1414
|
+
}
|
|
1415
|
+
async disconnect() {
|
|
1416
|
+
await this.receiver?.disconnect();
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
// ../document-drive/src/server/listener/transmitter/pull-responder.ts
|
|
1421
|
+
var PullResponderTransmitter = class _PullResponderTransmitter {
|
|
1422
|
+
drive;
|
|
1423
|
+
listener;
|
|
1424
|
+
manager;
|
|
1425
|
+
constructor(listener, drive, manager) {
|
|
1426
|
+
this.listener = listener;
|
|
1427
|
+
this.drive = drive;
|
|
1428
|
+
this.manager = manager;
|
|
1429
|
+
}
|
|
1430
|
+
getStrands(options2) {
|
|
1431
|
+
return this.manager.getStrands(
|
|
1432
|
+
this.listener.driveId,
|
|
1433
|
+
this.listener.listenerId,
|
|
1434
|
+
options2
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
disconnect() {
|
|
1438
|
+
return Promise.resolve();
|
|
1439
|
+
}
|
|
1440
|
+
async processAcknowledge(driveId, listenerId, revisions) {
|
|
1441
|
+
const syncUnits = await this.manager.getListenerSyncUnitIds(
|
|
1442
|
+
driveId,
|
|
1443
|
+
listenerId
|
|
1444
|
+
);
|
|
1445
|
+
let success = true;
|
|
1446
|
+
for (const revision of revisions) {
|
|
1447
|
+
const syncUnit = syncUnits.find(
|
|
1448
|
+
(s) => s.scope === revision.scope && s.branch === revision.branch && s.driveId === revision.driveId && s.documentId == revision.documentId
|
|
1449
|
+
);
|
|
1450
|
+
if (!syncUnit) {
|
|
1451
|
+
logger.warn("Unknown sync unit was acknowledged", revision);
|
|
1452
|
+
success = false;
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
await this.manager.updateListenerRevision(
|
|
1456
|
+
listenerId,
|
|
1457
|
+
driveId,
|
|
1458
|
+
syncUnit.syncId,
|
|
1459
|
+
revision.revision
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
return success;
|
|
1463
|
+
}
|
|
1464
|
+
static async registerPullResponder(driveId, url, filter) {
|
|
1465
|
+
const result = await requestGraphql(
|
|
1466
|
+
url,
|
|
1467
|
+
gql`
|
|
1468
|
+
mutation registerPullResponderListener($filter: InputListenerFilter!) {
|
|
1469
|
+
registerPullResponderListener(filter: $filter) {
|
|
1470
|
+
listenerId
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
`,
|
|
1474
|
+
{ filter }
|
|
1475
|
+
);
|
|
1476
|
+
const error = result.errors?.at(0);
|
|
1477
|
+
if (error) {
|
|
1478
|
+
throw error;
|
|
1479
|
+
}
|
|
1480
|
+
if (!result.registerPullResponderListener) {
|
|
1481
|
+
throw new Error("Error registering listener");
|
|
1482
|
+
}
|
|
1483
|
+
return result.registerPullResponderListener.listenerId;
|
|
1484
|
+
}
|
|
1485
|
+
static async pullStrands(driveId, url, listenerId, options2) {
|
|
1486
|
+
const result = await requestGraphql(
|
|
1487
|
+
url,
|
|
1488
|
+
gql`
|
|
1489
|
+
query strands($listenerId: ID!) {
|
|
1490
|
+
system {
|
|
1491
|
+
sync {
|
|
1492
|
+
strands(listenerId: $listenerId) {
|
|
1493
|
+
driveId
|
|
1494
|
+
documentId
|
|
1495
|
+
scope
|
|
1496
|
+
branch
|
|
1497
|
+
operations {
|
|
1498
|
+
id
|
|
1499
|
+
timestamp
|
|
1500
|
+
skip
|
|
1501
|
+
type
|
|
1502
|
+
input
|
|
1503
|
+
hash
|
|
1504
|
+
index
|
|
1505
|
+
context {
|
|
1506
|
+
signer {
|
|
1507
|
+
user {
|
|
1508
|
+
address
|
|
1509
|
+
networkId
|
|
1510
|
+
chainId
|
|
1511
|
+
}
|
|
1512
|
+
app {
|
|
1513
|
+
name
|
|
1514
|
+
key
|
|
1515
|
+
}
|
|
1516
|
+
signatures
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
`,
|
|
1525
|
+
{ listenerId }
|
|
1526
|
+
);
|
|
1527
|
+
const error = result.errors?.at(0);
|
|
1528
|
+
if (error) {
|
|
1529
|
+
throw error;
|
|
1530
|
+
}
|
|
1531
|
+
if (!result.system) {
|
|
1532
|
+
return [];
|
|
1533
|
+
}
|
|
1534
|
+
return result.system.sync.strands.map((s) => ({
|
|
1535
|
+
...s,
|
|
1536
|
+
operations: s.operations.map((o) => ({
|
|
1537
|
+
...o,
|
|
1538
|
+
input: JSON.parse(o.input)
|
|
1539
|
+
}))
|
|
1540
|
+
}));
|
|
1541
|
+
}
|
|
1542
|
+
static async acknowledgeStrands(driveId, url, listenerId, revisions) {
|
|
1543
|
+
const result = await requestGraphql(
|
|
1544
|
+
url,
|
|
1545
|
+
gql`
|
|
1546
|
+
mutation acknowledge(
|
|
1547
|
+
$listenerId: String!
|
|
1548
|
+
$revisions: [ListenerRevisionInput]
|
|
1549
|
+
) {
|
|
1550
|
+
acknowledge(listenerId: $listenerId, revisions: $revisions)
|
|
1551
|
+
}
|
|
1552
|
+
`,
|
|
1553
|
+
{ listenerId, revisions }
|
|
1554
|
+
);
|
|
1555
|
+
const error = result.errors?.at(0);
|
|
1556
|
+
if (error) {
|
|
1557
|
+
throw error;
|
|
1558
|
+
}
|
|
1559
|
+
if (result.acknowledge === null) {
|
|
1560
|
+
throw new Error("Error acknowledging strands");
|
|
1561
|
+
}
|
|
1562
|
+
return result.acknowledge;
|
|
1563
|
+
}
|
|
1564
|
+
static async executePull(driveId, trigger, onStrandUpdate, onError, onRevisions, onAcknowledge) {
|
|
1565
|
+
try {
|
|
1566
|
+
const { url, listenerId } = trigger.data;
|
|
1567
|
+
const strands = await _PullResponderTransmitter.pullStrands(
|
|
1568
|
+
driveId,
|
|
1569
|
+
url,
|
|
1570
|
+
listenerId
|
|
1571
|
+
// since ?
|
|
1572
|
+
);
|
|
1573
|
+
if (!strands.length) {
|
|
1574
|
+
onRevisions?.([]);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const listenerRevisions = [];
|
|
1578
|
+
for (const strand of strands) {
|
|
1579
|
+
const operations = strand.operations.map((op) => ({
|
|
1580
|
+
...op,
|
|
1581
|
+
scope: strand.scope,
|
|
1582
|
+
branch: strand.branch
|
|
1583
|
+
}));
|
|
1584
|
+
let error = void 0;
|
|
1585
|
+
try {
|
|
1586
|
+
const result = await onStrandUpdate(strand, {
|
|
1587
|
+
type: "trigger",
|
|
1588
|
+
trigger
|
|
1589
|
+
});
|
|
1590
|
+
if (result.error) {
|
|
1591
|
+
throw result.error;
|
|
1592
|
+
}
|
|
1593
|
+
} catch (e) {
|
|
1594
|
+
error = e;
|
|
1595
|
+
onError(error);
|
|
1596
|
+
}
|
|
1597
|
+
listenerRevisions.push({
|
|
1598
|
+
branch: strand.branch,
|
|
1599
|
+
documentId: strand.documentId || "",
|
|
1600
|
+
driveId: strand.driveId,
|
|
1601
|
+
revision: operations.pop()?.index ?? -1,
|
|
1602
|
+
scope: strand.scope,
|
|
1603
|
+
status: error ? error instanceof OperationError ? error.status : "ERROR" : "SUCCESS",
|
|
1604
|
+
error
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
onRevisions?.(listenerRevisions);
|
|
1608
|
+
await _PullResponderTransmitter.acknowledgeStrands(
|
|
1609
|
+
driveId,
|
|
1610
|
+
url,
|
|
1611
|
+
listenerId,
|
|
1612
|
+
listenerRevisions.map((revision) => {
|
|
1613
|
+
const { error, ...rest } = revision;
|
|
1614
|
+
return rest;
|
|
1615
|
+
})
|
|
1616
|
+
).then((result) => onAcknowledge?.(result)).catch((error) => logger.error("ACK error", error));
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
onError(error);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
static setupPull(driveId, trigger, onStrandUpdate, onError, onRevisions, onAcknowledge) {
|
|
1622
|
+
const { interval } = trigger.data;
|
|
1623
|
+
let loopInterval = PULL_DRIVE_INTERVAL;
|
|
1624
|
+
if (interval) {
|
|
1625
|
+
try {
|
|
1626
|
+
const intervalNumber = parseInt(interval);
|
|
1627
|
+
if (intervalNumber) {
|
|
1628
|
+
loopInterval = intervalNumber;
|
|
1629
|
+
}
|
|
1630
|
+
} catch {
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
let isCancelled = false;
|
|
1634
|
+
let timeout;
|
|
1635
|
+
const executeLoop = async () => {
|
|
1636
|
+
while (!isCancelled) {
|
|
1637
|
+
await this.executePull(
|
|
1638
|
+
driveId,
|
|
1639
|
+
trigger,
|
|
1640
|
+
onStrandUpdate,
|
|
1641
|
+
onError,
|
|
1642
|
+
onRevisions,
|
|
1643
|
+
onAcknowledge
|
|
1644
|
+
);
|
|
1645
|
+
await new Promise((resolve) => {
|
|
1646
|
+
timeout = setTimeout(resolve, loopInterval);
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
executeLoop().catch(logger.error);
|
|
1651
|
+
return () => {
|
|
1652
|
+
isCancelled = true;
|
|
1653
|
+
if (timeout !== void 0) {
|
|
1654
|
+
clearTimeout(timeout);
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
static async createPullResponderTrigger(driveId, url, options2) {
|
|
1659
|
+
const { pullFilter, pullInterval } = options2;
|
|
1660
|
+
const listenerId = await _PullResponderTransmitter.registerPullResponder(
|
|
1661
|
+
driveId,
|
|
1662
|
+
url,
|
|
1663
|
+
pullFilter ?? {
|
|
1664
|
+
documentId: ["*"],
|
|
1665
|
+
documentType: ["*"],
|
|
1666
|
+
branch: ["*"],
|
|
1667
|
+
scope: ["*"]
|
|
1668
|
+
}
|
|
1669
|
+
);
|
|
1670
|
+
const pullTrigger = {
|
|
1671
|
+
id: generateUUID(),
|
|
1672
|
+
type: "PullResponder",
|
|
1673
|
+
data: {
|
|
1674
|
+
url,
|
|
1675
|
+
listenerId,
|
|
1676
|
+
interval: pullInterval?.toString() ?? ""
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
return pullTrigger;
|
|
1680
|
+
}
|
|
1681
|
+
static isPullResponderTrigger(trigger) {
|
|
1682
|
+
return trigger.type === "PullResponder";
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
var SwitchboardPushTransmitter = class {
|
|
1686
|
+
drive;
|
|
1687
|
+
listener;
|
|
1688
|
+
targetURL;
|
|
1689
|
+
constructor(listener, drive) {
|
|
1690
|
+
this.listener = listener;
|
|
1691
|
+
this.drive = drive;
|
|
1692
|
+
this.targetURL = listener.callInfo.data;
|
|
1693
|
+
}
|
|
1694
|
+
async transmit(strands, source) {
|
|
1695
|
+
if (source.type === "trigger" && source.trigger.data?.url === this.targetURL) {
|
|
1696
|
+
return strands.map((strand) => ({
|
|
1697
|
+
driveId: strand.driveId,
|
|
1698
|
+
documentId: strand.documentId,
|
|
1699
|
+
scope: strand.scope,
|
|
1700
|
+
branch: strand.branch,
|
|
1701
|
+
status: "SUCCESS",
|
|
1702
|
+
revision: strand.operations.at(-1)?.index ?? -1
|
|
1703
|
+
}));
|
|
1704
|
+
}
|
|
1705
|
+
try {
|
|
1706
|
+
const { pushUpdates } = await requestGraphql(
|
|
1707
|
+
this.targetURL,
|
|
1708
|
+
gql`
|
|
1709
|
+
mutation pushUpdates($strands: [InputStrandUpdate!]) {
|
|
1710
|
+
pushUpdates(strands: $strands) {
|
|
1711
|
+
driveId
|
|
1712
|
+
documentId
|
|
1713
|
+
scope
|
|
1714
|
+
branch
|
|
1715
|
+
status
|
|
1716
|
+
revision
|
|
1717
|
+
error
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
`,
|
|
1721
|
+
{
|
|
1722
|
+
strands: strands.map((strand) => ({
|
|
1723
|
+
...strand,
|
|
1724
|
+
operations: strand.operations.map((op) => ({
|
|
1725
|
+
...op,
|
|
1726
|
+
input: stringify(op.input)
|
|
1727
|
+
}))
|
|
1728
|
+
}))
|
|
1729
|
+
}
|
|
1730
|
+
);
|
|
1731
|
+
if (!pushUpdates) {
|
|
1732
|
+
throw new Error("Couldn't update listener revision");
|
|
1733
|
+
}
|
|
1734
|
+
return pushUpdates;
|
|
1735
|
+
} catch (e) {
|
|
1736
|
+
logger.error(e);
|
|
1737
|
+
throw e;
|
|
1738
|
+
}
|
|
1739
|
+
return [];
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
// ../document-drive/src/server/listener/manager.ts
|
|
1744
|
+
function debounce(func, delay = 250) {
|
|
1745
|
+
let timer;
|
|
1746
|
+
return (immediate, ...args) => {
|
|
1747
|
+
if (timer) {
|
|
1748
|
+
clearTimeout(timer);
|
|
1749
|
+
}
|
|
1750
|
+
return new Promise((resolve, reject) => {
|
|
1751
|
+
if (immediate) {
|
|
1752
|
+
func(...args).then(resolve).catch(reject);
|
|
1753
|
+
} else {
|
|
1754
|
+
timer = setTimeout(() => {
|
|
1755
|
+
func(...args).then(resolve).catch(reject);
|
|
1756
|
+
}, delay);
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
var ListenerManager = class _ListenerManager extends BaseListenerManager {
|
|
1762
|
+
static LISTENER_UPDATE_DELAY = 250;
|
|
1763
|
+
async getTransmitter(driveId, listenerId) {
|
|
1764
|
+
return Promise.resolve(this.transmitters[driveId]?.[listenerId]);
|
|
1765
|
+
}
|
|
1766
|
+
driveHasListeners(driveId) {
|
|
1767
|
+
return this.listenerState.has(driveId);
|
|
1768
|
+
}
|
|
1769
|
+
async addListener(listener) {
|
|
1770
|
+
const drive = listener.driveId;
|
|
1771
|
+
if (!this.listenerState.has(drive)) {
|
|
1772
|
+
this.listenerState.set(drive, /* @__PURE__ */ new Map());
|
|
1773
|
+
}
|
|
1774
|
+
const driveMap = this.listenerState.get(drive);
|
|
1775
|
+
driveMap.set(listener.listenerId, {
|
|
1776
|
+
block: listener.block,
|
|
1777
|
+
driveId: listener.driveId,
|
|
1778
|
+
pendingTimeout: "0",
|
|
1779
|
+
listener,
|
|
1780
|
+
listenerStatus: "CREATED",
|
|
1781
|
+
syncUnits: /* @__PURE__ */ new Map()
|
|
1782
|
+
});
|
|
1783
|
+
let transmitter;
|
|
1784
|
+
switch (listener.callInfo?.transmitterType) {
|
|
1785
|
+
case "SwitchboardPush": {
|
|
1786
|
+
transmitter = new SwitchboardPushTransmitter(listener, this.drive);
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
case "PullResponder": {
|
|
1790
|
+
transmitter = new PullResponderTransmitter(listener, this.drive, this);
|
|
1791
|
+
break;
|
|
1792
|
+
}
|
|
1793
|
+
case "Internal": {
|
|
1794
|
+
transmitter = new InternalTransmitter(listener, this.drive);
|
|
1795
|
+
break;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (!transmitter) {
|
|
1799
|
+
throw new Error("Transmitter not found");
|
|
1800
|
+
}
|
|
1801
|
+
const driveTransmitters = this.transmitters[drive] || {};
|
|
1802
|
+
driveTransmitters[listener.listenerId] = transmitter;
|
|
1803
|
+
this.transmitters[drive] = driveTransmitters;
|
|
1804
|
+
return Promise.resolve(transmitter);
|
|
1805
|
+
}
|
|
1806
|
+
async removeListener(driveId, listenerId) {
|
|
1807
|
+
const driveMap = this.listenerState.get(driveId);
|
|
1808
|
+
if (!driveMap) {
|
|
1809
|
+
return false;
|
|
1810
|
+
}
|
|
1811
|
+
return Promise.resolve(driveMap.delete(listenerId));
|
|
1812
|
+
}
|
|
1813
|
+
async removeSyncUnits(driveId, syncUnits) {
|
|
1814
|
+
const listeners = this.listenerState.get(driveId);
|
|
1815
|
+
if (!listeners) {
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
for (const [, listener] of listeners) {
|
|
1819
|
+
for (const syncUnit of syncUnits) {
|
|
1820
|
+
listener.syncUnits.delete(syncUnit.syncId);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return Promise.resolve();
|
|
1824
|
+
}
|
|
1825
|
+
async updateSynchronizationRevisions(driveId, syncUnits, source, willUpdate, onError, forceSync = false) {
|
|
1826
|
+
const drive = this.listenerState.get(driveId);
|
|
1827
|
+
if (!drive) {
|
|
1828
|
+
return [];
|
|
1829
|
+
}
|
|
1830
|
+
const outdatedListeners = [];
|
|
1831
|
+
for (const [, listener] of drive) {
|
|
1832
|
+
if (outdatedListeners.find(
|
|
1833
|
+
(l) => l.listenerId === listener.listener.listenerId
|
|
1834
|
+
)) {
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
const transmitter = await this.getTransmitter(
|
|
1838
|
+
driveId,
|
|
1839
|
+
listener.listener.listenerId
|
|
1840
|
+
);
|
|
1841
|
+
if (!transmitter?.transmit) {
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
for (const syncUnit of syncUnits) {
|
|
1845
|
+
if (!this._checkFilter(listener.listener.filter, syncUnit)) {
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
const listenerRev = listener.syncUnits.get(syncUnit.syncId);
|
|
1849
|
+
if (!listenerRev || listenerRev.listenerRev < syncUnit.revision) {
|
|
1850
|
+
outdatedListeners.push(listener.listener);
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (outdatedListeners.length) {
|
|
1856
|
+
willUpdate?.(outdatedListeners);
|
|
1857
|
+
return this.triggerUpdate(forceSync, source, onError);
|
|
1858
|
+
}
|
|
1859
|
+
return [];
|
|
1860
|
+
}
|
|
1861
|
+
async updateListenerRevision(listenerId, driveId, syncId, listenerRev) {
|
|
1862
|
+
const drive = this.listenerState.get(driveId);
|
|
1863
|
+
if (!drive) {
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
const listener = drive.get(listenerId);
|
|
1867
|
+
if (!listener) {
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
const lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1871
|
+
const entry = listener.syncUnits.get(syncId);
|
|
1872
|
+
if (entry) {
|
|
1873
|
+
entry.listenerRev = listenerRev;
|
|
1874
|
+
entry.lastUpdated = lastUpdated;
|
|
1875
|
+
} else {
|
|
1876
|
+
listener.syncUnits.set(syncId, { listenerRev, lastUpdated });
|
|
1877
|
+
}
|
|
1878
|
+
return Promise.resolve();
|
|
1879
|
+
}
|
|
1880
|
+
triggerUpdate = debounce(
|
|
1881
|
+
this._triggerUpdate.bind(this),
|
|
1882
|
+
_ListenerManager.LISTENER_UPDATE_DELAY
|
|
1883
|
+
);
|
|
1884
|
+
async _triggerUpdate(source, onError) {
|
|
1885
|
+
const listenerUpdates = [];
|
|
1886
|
+
for (const [driveId, drive] of this.listenerState) {
|
|
1887
|
+
for (const [id, listener] of drive) {
|
|
1888
|
+
const transmitter = await this.getTransmitter(driveId, id);
|
|
1889
|
+
if (!transmitter?.transmit) {
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
const syncUnits = await this.getListenerSyncUnits(
|
|
1893
|
+
driveId,
|
|
1894
|
+
listener.listener.listenerId
|
|
1895
|
+
);
|
|
1896
|
+
const strandUpdates = [];
|
|
1897
|
+
const tasks = syncUnits.map((syncUnit) => async () => {
|
|
1898
|
+
const unitState = listener.syncUnits.get(syncUnit.syncId);
|
|
1899
|
+
if (unitState && unitState.listenerRev >= syncUnit.revision) {
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
const opData = [];
|
|
1903
|
+
try {
|
|
1904
|
+
const data = await this.drive.getOperationData(
|
|
1905
|
+
// TODO - join queries, DEAL WITH INVALID SYNC ID ERROR
|
|
1906
|
+
driveId,
|
|
1907
|
+
syncUnit.syncId,
|
|
1908
|
+
{
|
|
1909
|
+
fromRevision: unitState?.listenerRev
|
|
1910
|
+
}
|
|
1911
|
+
);
|
|
1912
|
+
opData.push(...data);
|
|
1913
|
+
} catch (e) {
|
|
1914
|
+
logger.error(e);
|
|
1915
|
+
}
|
|
1916
|
+
if (!opData.length) {
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
strandUpdates.push({
|
|
1920
|
+
driveId,
|
|
1921
|
+
documentId: syncUnit.documentId,
|
|
1922
|
+
branch: syncUnit.branch,
|
|
1923
|
+
operations: opData,
|
|
1924
|
+
scope: syncUnit.scope
|
|
1925
|
+
});
|
|
1926
|
+
});
|
|
1927
|
+
if (this.options.sequentialUpdates) {
|
|
1928
|
+
for (const task of tasks) {
|
|
1929
|
+
await task();
|
|
1930
|
+
}
|
|
1931
|
+
} else {
|
|
1932
|
+
await Promise.all(tasks.map((task) => task()));
|
|
1933
|
+
}
|
|
1934
|
+
if (strandUpdates.length == 0) {
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
listener.pendingTimeout = new Date(
|
|
1938
|
+
(/* @__PURE__ */ new Date()).getTime() / 1e3 + 300
|
|
1939
|
+
).toISOString();
|
|
1940
|
+
listener.listenerStatus = "PENDING";
|
|
1941
|
+
try {
|
|
1942
|
+
const listenerRevisions = await transmitter.transmit(
|
|
1943
|
+
strandUpdates,
|
|
1944
|
+
source
|
|
1945
|
+
);
|
|
1946
|
+
listener.pendingTimeout = "0";
|
|
1947
|
+
listener.listenerStatus = "PENDING";
|
|
1948
|
+
const lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1949
|
+
for (const revision of listenerRevisions) {
|
|
1950
|
+
const syncUnit = syncUnits.find(
|
|
1951
|
+
(unit) => revision.documentId === unit.documentId && revision.scope === unit.scope && revision.branch === unit.branch
|
|
1952
|
+
);
|
|
1953
|
+
if (syncUnit) {
|
|
1954
|
+
listener.syncUnits.set(syncUnit.syncId, {
|
|
1955
|
+
lastUpdated,
|
|
1956
|
+
listenerRev: revision.revision
|
|
1957
|
+
});
|
|
1958
|
+
} else {
|
|
1959
|
+
logger.warn(
|
|
1960
|
+
`Received revision for untracked unit for listener ${listener.listener.listenerId}`,
|
|
1961
|
+
revision
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
for (const revision of listenerRevisions) {
|
|
1966
|
+
const error = revision.status === "ERROR";
|
|
1967
|
+
if (revision.error?.includes("Missing operations")) {
|
|
1968
|
+
const updates = await this._triggerUpdate(source, onError);
|
|
1969
|
+
listenerUpdates.push(...updates);
|
|
1970
|
+
} else {
|
|
1971
|
+
listenerUpdates.push({
|
|
1972
|
+
listenerId: listener.listener.listenerId,
|
|
1973
|
+
listenerRevisions
|
|
1974
|
+
});
|
|
1975
|
+
if (error) {
|
|
1976
|
+
throw new OperationError(
|
|
1977
|
+
revision.status,
|
|
1978
|
+
void 0,
|
|
1979
|
+
revision.error,
|
|
1980
|
+
revision.error
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
listener.listenerStatus = "SUCCESS";
|
|
1986
|
+
} catch (e) {
|
|
1987
|
+
onError?.(e, driveId, listener);
|
|
1988
|
+
listener.listenerStatus = e instanceof OperationError ? e.status : "ERROR";
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return listenerUpdates;
|
|
1993
|
+
}
|
|
1994
|
+
_checkFilter(filter, syncUnit) {
|
|
1995
|
+
const { branch, documentId, scope, documentType } = syncUnit;
|
|
1996
|
+
if ((!filter.branch || filter.branch.includes(branch) || filter.branch.includes("*")) && (!filter.documentId || filter.documentId.includes(documentId) || filter.documentId.includes("*")) && (!filter.scope || filter.scope.includes(scope) || filter.scope.includes("*")) && (!filter.documentType || filter.documentType.includes(documentType) || filter.documentType.includes("*"))) {
|
|
1997
|
+
return true;
|
|
1998
|
+
}
|
|
1999
|
+
return false;
|
|
2000
|
+
}
|
|
2001
|
+
getListenerSyncUnits(driveId, listenerId, loadedDrive) {
|
|
2002
|
+
const listener = this.listenerState.get(driveId)?.get(listenerId);
|
|
2003
|
+
if (!listener) {
|
|
2004
|
+
return [];
|
|
2005
|
+
}
|
|
2006
|
+
const filter = listener.listener.filter;
|
|
2007
|
+
return this.drive.getSynchronizationUnits(
|
|
2008
|
+
driveId,
|
|
2009
|
+
filter.documentId ?? ["*"],
|
|
2010
|
+
filter.scope ?? ["*"],
|
|
2011
|
+
filter.branch ?? ["*"],
|
|
2012
|
+
filter.documentType ?? ["*"],
|
|
2013
|
+
loadedDrive
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
getListenerSyncUnitIds(driveId, listenerId) {
|
|
2017
|
+
const listener = this.listenerState.get(driveId)?.get(listenerId);
|
|
2018
|
+
if (!listener) {
|
|
2019
|
+
return [];
|
|
2020
|
+
}
|
|
2021
|
+
const filter = listener.listener.filter;
|
|
2022
|
+
return this.drive.getSynchronizationUnitsIds(
|
|
2023
|
+
driveId,
|
|
2024
|
+
filter.documentId ?? ["*"],
|
|
2025
|
+
filter.scope ?? ["*"],
|
|
2026
|
+
filter.branch ?? ["*"],
|
|
2027
|
+
filter.documentType ?? ["*"]
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
async initDrive(drive) {
|
|
2031
|
+
const {
|
|
2032
|
+
state: {
|
|
2033
|
+
local: { listeners }
|
|
2034
|
+
}
|
|
2035
|
+
} = drive;
|
|
2036
|
+
for (const listener of listeners) {
|
|
2037
|
+
await this.addListener({
|
|
2038
|
+
block: listener.block,
|
|
2039
|
+
driveId: drive.state.global.id,
|
|
2040
|
+
filter: {
|
|
2041
|
+
branch: listener.filter.branch ?? [],
|
|
2042
|
+
documentId: listener.filter.documentId ?? [],
|
|
2043
|
+
documentType: listener.filter.documentType,
|
|
2044
|
+
scope: listener.filter.scope ?? []
|
|
2045
|
+
},
|
|
2046
|
+
listenerId: listener.listenerId,
|
|
2047
|
+
system: listener.system,
|
|
2048
|
+
callInfo: listener.callInfo ?? void 0,
|
|
2049
|
+
label: listener.label ?? ""
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
async removeDrive(driveId) {
|
|
2054
|
+
this.listenerState.delete(driveId);
|
|
2055
|
+
const transmitters = this.transmitters[driveId];
|
|
2056
|
+
if (transmitters) {
|
|
2057
|
+
await Promise.all(
|
|
2058
|
+
Object.values(transmitters).map((t) => t.disconnect?.())
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
getListener(driveId, listenerId) {
|
|
2063
|
+
const drive = this.listenerState.get(driveId);
|
|
2064
|
+
if (!drive) throw new Error("Drive not found");
|
|
2065
|
+
const listener = drive.get(listenerId);
|
|
2066
|
+
if (!listener) throw new Error("Listener not found");
|
|
2067
|
+
return Promise.resolve(listener);
|
|
2068
|
+
}
|
|
2069
|
+
async getStrands(driveId, listenerId, options2) {
|
|
2070
|
+
const listener = await this.getListener(driveId, listenerId);
|
|
2071
|
+
const strands = [];
|
|
2072
|
+
const drive = await this.drive.getDrive(driveId);
|
|
2073
|
+
const syncUnits = await this.getListenerSyncUnits(
|
|
2074
|
+
driveId,
|
|
2075
|
+
listenerId,
|
|
2076
|
+
drive
|
|
2077
|
+
);
|
|
2078
|
+
const limit = options2?.limit;
|
|
2079
|
+
let operationsCount = 0;
|
|
2080
|
+
const tasks = syncUnits.map((syncUnit) => async () => {
|
|
2081
|
+
if (limit && operationsCount >= limit) {
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
if (syncUnit.revision < 0) {
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
const entry = listener.syncUnits.get(syncUnit.syncId);
|
|
2088
|
+
if (entry && entry.listenerRev >= syncUnit.revision) {
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const { documentId, driveId: driveId2, scope, branch } = syncUnit;
|
|
2092
|
+
try {
|
|
2093
|
+
const operations = await this.drive.getOperationData(
|
|
2094
|
+
// DEAL WITH INVALID SYNC ID ERROR
|
|
2095
|
+
driveId2,
|
|
2096
|
+
syncUnit.syncId,
|
|
2097
|
+
{
|
|
2098
|
+
since: options2?.since,
|
|
2099
|
+
fromRevision: options2?.fromRevision ?? entry?.listenerRev,
|
|
2100
|
+
limit: limit ? limit - operationsCount : void 0
|
|
2101
|
+
},
|
|
2102
|
+
drive
|
|
2103
|
+
);
|
|
2104
|
+
if (!operations.length) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
operationsCount += operations.length;
|
|
2108
|
+
strands.push({
|
|
2109
|
+
driveId: driveId2,
|
|
2110
|
+
documentId,
|
|
2111
|
+
scope,
|
|
2112
|
+
branch,
|
|
2113
|
+
operations
|
|
2114
|
+
});
|
|
2115
|
+
} catch (error) {
|
|
2116
|
+
logger.error(error);
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
if (this.options.sequentialUpdates) {
|
|
2121
|
+
for (const task of tasks) {
|
|
2122
|
+
await task();
|
|
2123
|
+
}
|
|
2124
|
+
} else {
|
|
2125
|
+
await Promise.all(tasks.map((task) => task()));
|
|
2126
|
+
}
|
|
2127
|
+
return strands;
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2131
|
+
// ../document-drive/src/server/index.ts
|
|
2132
|
+
var PULL_DRIVE_INTERVAL = 5e3;
|
|
2133
|
+
var BaseDocumentDriveServer = class extends AbstractDocumentDriveServer {
|
|
2134
|
+
emitter = createNanoEvents();
|
|
2135
|
+
cache;
|
|
2136
|
+
documentModels;
|
|
2137
|
+
storage;
|
|
2138
|
+
listenerStateManager;
|
|
2139
|
+
triggerMap = /* @__PURE__ */ new Map();
|
|
2140
|
+
syncStatus = /* @__PURE__ */ new Map();
|
|
2141
|
+
queueManager;
|
|
2142
|
+
initializePromise;
|
|
2143
|
+
defaultDrivesManager;
|
|
2144
|
+
options;
|
|
2145
|
+
constructor(documentModels, storage = new MemoryStorage(), cache = new memory_default(), queueManager = new BaseQueueManager(), options2) {
|
|
2146
|
+
super();
|
|
2147
|
+
this.options = {
|
|
2148
|
+
...options2,
|
|
2149
|
+
defaultDrives: {
|
|
2150
|
+
...options2?.defaultDrives
|
|
2151
|
+
},
|
|
2152
|
+
listenerManager: {
|
|
2153
|
+
...DefaultListenerManagerOptions,
|
|
2154
|
+
...options2?.listenerManager
|
|
2155
|
+
},
|
|
2156
|
+
taskQueueMethod: options2?.taskQueueMethod === void 0 ? RunAsap.runAsap : options2.taskQueueMethod
|
|
2157
|
+
};
|
|
2158
|
+
this.listenerStateManager = new ListenerManager(
|
|
2159
|
+
this,
|
|
2160
|
+
void 0,
|
|
2161
|
+
options2?.listenerManager
|
|
2162
|
+
);
|
|
2163
|
+
this.documentModels = documentModels;
|
|
2164
|
+
this.storage = storage;
|
|
2165
|
+
this.cache = cache;
|
|
2166
|
+
this.queueManager = queueManager;
|
|
2167
|
+
this.defaultDrivesManager = new DefaultDrivesManager(
|
|
2168
|
+
this,
|
|
2169
|
+
this.defaultDrivesManagerDelegate,
|
|
2170
|
+
options2
|
|
2171
|
+
);
|
|
2172
|
+
this.storage.setStorageDelegate?.({
|
|
2173
|
+
getCachedOperations: async (drive, id) => {
|
|
2174
|
+
try {
|
|
2175
|
+
const document = await this.cache.getDocument(drive, id);
|
|
2176
|
+
return document?.operations;
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
logger.error(error);
|
|
2179
|
+
return void 0;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
this.initializePromise = this._initialize();
|
|
2184
|
+
}
|
|
2185
|
+
setDocumentModels(models) {
|
|
2186
|
+
this.documentModels = [...models];
|
|
2187
|
+
this.emit("documentModels", [...models]);
|
|
2188
|
+
}
|
|
2189
|
+
initializeDefaultRemoteDrives() {
|
|
2190
|
+
return this.defaultDrivesManager.initializeDefaultRemoteDrives();
|
|
2191
|
+
}
|
|
2192
|
+
getDefaultRemoteDrives() {
|
|
2193
|
+
return this.defaultDrivesManager.getDefaultRemoteDrives();
|
|
2194
|
+
}
|
|
2195
|
+
setDefaultDriveAccessLevel(url, level) {
|
|
2196
|
+
return this.defaultDrivesManager.setDefaultDriveAccessLevel(url, level);
|
|
2197
|
+
}
|
|
2198
|
+
setAllDefaultDrivesAccessLevel(level) {
|
|
2199
|
+
return this.defaultDrivesManager.setAllDefaultDrivesAccessLevel(level);
|
|
2200
|
+
}
|
|
2201
|
+
getOperationSource(source) {
|
|
2202
|
+
return source.type === "local" ? "push" : "pull";
|
|
2203
|
+
}
|
|
2204
|
+
getCombinedSyncUnitStatus(syncUnitStatus) {
|
|
2205
|
+
if (!syncUnitStatus.pull && !syncUnitStatus.push) return "INITIAL_SYNC";
|
|
2206
|
+
if (syncUnitStatus.pull === "INITIAL_SYNC") return "INITIAL_SYNC";
|
|
2207
|
+
if (syncUnitStatus.push === "INITIAL_SYNC")
|
|
2208
|
+
return syncUnitStatus.pull || "INITIAL_SYNC";
|
|
2209
|
+
const order = [
|
|
2210
|
+
"ERROR",
|
|
2211
|
+
"MISSING",
|
|
2212
|
+
"CONFLICT",
|
|
2213
|
+
"SYNCING",
|
|
2214
|
+
"SUCCESS"
|
|
2215
|
+
];
|
|
2216
|
+
const sortedStatus = Object.values(syncUnitStatus).sort(
|
|
2217
|
+
(a, b) => order.indexOf(a) - order.indexOf(b)
|
|
2218
|
+
);
|
|
2219
|
+
return sortedStatus[0];
|
|
2220
|
+
}
|
|
2221
|
+
initSyncStatus(syncUnitId, status) {
|
|
2222
|
+
const defaultSyncUnitStatus = Object.entries(
|
|
2223
|
+
status
|
|
2224
|
+
).reduce((acc, [key, _status]) => {
|
|
2225
|
+
return {
|
|
2226
|
+
...acc,
|
|
2227
|
+
[key]: _status !== "SYNCING" ? _status : "INITIAL_SYNC"
|
|
2228
|
+
};
|
|
2229
|
+
}, {});
|
|
2230
|
+
this.syncStatus.set(syncUnitId, defaultSyncUnitStatus);
|
|
2231
|
+
this.emit(
|
|
2232
|
+
"syncStatus",
|
|
2233
|
+
syncUnitId,
|
|
2234
|
+
this.getCombinedSyncUnitStatus(defaultSyncUnitStatus),
|
|
2235
|
+
void 0,
|
|
2236
|
+
defaultSyncUnitStatus
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
async initializeDriveSyncStatus(driveId, drive) {
|
|
2240
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
2241
|
+
const syncStatus = {
|
|
2242
|
+
pull: drive.state.local.triggers.length > 0 ? "INITIAL_SYNC" : void 0,
|
|
2243
|
+
push: drive.state.local.listeners.length > 0 ? "SUCCESS" : void 0
|
|
2244
|
+
};
|
|
2245
|
+
if (!syncStatus.pull && !syncStatus.push) return;
|
|
2246
|
+
const syncUnitsIds = [driveId, ...syncUnits.map((s) => s.syncId)];
|
|
2247
|
+
for (const syncUnitId of syncUnitsIds) {
|
|
2248
|
+
this.initSyncStatus(syncUnitId, syncStatus);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
updateSyncUnitStatus(syncUnitId, status, error) {
|
|
2252
|
+
if (status === null) {
|
|
2253
|
+
this.syncStatus.delete(syncUnitId);
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
const syncUnitStatus = this.syncStatus.get(syncUnitId);
|
|
2257
|
+
if (!syncUnitStatus) {
|
|
2258
|
+
this.initSyncStatus(syncUnitId, status);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
const shouldUpdateStatus = Object.entries(status).some(
|
|
2262
|
+
([key, _status]) => syncUnitStatus[key] !== _status
|
|
2263
|
+
);
|
|
2264
|
+
if (shouldUpdateStatus) {
|
|
2265
|
+
const newstatus = Object.entries(status).reduce((acc, [key, _status]) => {
|
|
2266
|
+
return {
|
|
2267
|
+
...acc,
|
|
2268
|
+
// do not replace initial_syncing if it has not finished yet
|
|
2269
|
+
[key]: acc[key] === "INITIAL_SYNC" && _status === "SYNCING" ? "INITIAL_SYNC" : _status
|
|
2270
|
+
};
|
|
2271
|
+
}, syncUnitStatus);
|
|
2272
|
+
const previousCombinedStatus = this.getCombinedSyncUnitStatus(syncUnitStatus);
|
|
2273
|
+
const newCombinedStatus = this.getCombinedSyncUnitStatus(newstatus);
|
|
2274
|
+
this.syncStatus.set(syncUnitId, newstatus);
|
|
2275
|
+
if (previousCombinedStatus !== newCombinedStatus) {
|
|
2276
|
+
this.emit(
|
|
2277
|
+
"syncStatus",
|
|
2278
|
+
syncUnitId,
|
|
2279
|
+
this.getCombinedSyncUnitStatus(newstatus),
|
|
2280
|
+
error,
|
|
2281
|
+
newstatus
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
async saveStrand(strand, source) {
|
|
2287
|
+
const operations = strand.operations.map((op) => ({
|
|
2288
|
+
...op,
|
|
2289
|
+
scope: strand.scope,
|
|
2290
|
+
branch: strand.branch
|
|
2291
|
+
}));
|
|
2292
|
+
const result = await (!strand.documentId ? this.queueDriveOperations(
|
|
2293
|
+
strand.driveId,
|
|
2294
|
+
operations,
|
|
2295
|
+
{ source }
|
|
2296
|
+
) : this.queueOperations(strand.driveId, strand.documentId, operations, {
|
|
2297
|
+
source
|
|
2298
|
+
}));
|
|
2299
|
+
if (result.status === "ERROR") {
|
|
2300
|
+
const syncUnits = strand.documentId !== "" ? (await this.getSynchronizationUnitsIds(
|
|
2301
|
+
strand.driveId,
|
|
2302
|
+
[strand.documentId],
|
|
2303
|
+
[strand.scope],
|
|
2304
|
+
[strand.branch]
|
|
2305
|
+
)).map((s) => s.syncId) : [strand.driveId];
|
|
2306
|
+
const operationSource = this.getOperationSource(source);
|
|
2307
|
+
for (const syncUnit of syncUnits) {
|
|
2308
|
+
this.updateSyncUnitStatus(
|
|
2309
|
+
syncUnit,
|
|
2310
|
+
{ [operationSource]: result.status },
|
|
2311
|
+
result.error
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
this.emit("strandUpdate", strand);
|
|
2316
|
+
return result;
|
|
2317
|
+
}
|
|
2318
|
+
handleListenerError(error, driveId, listener) {
|
|
2319
|
+
logger.error(
|
|
2320
|
+
`Listener ${listener.listener.label ?? listener.listener.listenerId} error:`,
|
|
2321
|
+
error
|
|
2322
|
+
);
|
|
2323
|
+
const status = error instanceof OperationError ? error.status : "ERROR";
|
|
2324
|
+
this.updateSyncUnitStatus(driveId, { push: status }, error);
|
|
2325
|
+
}
|
|
2326
|
+
shouldSyncRemoteDrive(drive) {
|
|
2327
|
+
return drive.state.local.availableOffline && drive.state.local.triggers.length > 0;
|
|
2328
|
+
}
|
|
2329
|
+
async startSyncRemoteDrive(driveId) {
|
|
2330
|
+
const drive = await this.getDrive(driveId);
|
|
2331
|
+
let driveTriggers = this.triggerMap.get(driveId);
|
|
2332
|
+
const syncUnits = await this.getSynchronizationUnitsIds(
|
|
2333
|
+
driveId,
|
|
2334
|
+
void 0,
|
|
2335
|
+
void 0,
|
|
2336
|
+
void 0,
|
|
2337
|
+
void 0,
|
|
2338
|
+
drive
|
|
2339
|
+
);
|
|
2340
|
+
for (const trigger of drive.state.local.triggers) {
|
|
2341
|
+
if (driveTriggers?.get(trigger.id)) {
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
if (!driveTriggers) {
|
|
2345
|
+
driveTriggers = /* @__PURE__ */ new Map();
|
|
2346
|
+
}
|
|
2347
|
+
this.updateSyncUnitStatus(driveId, { pull: "SYNCING" });
|
|
2348
|
+
for (const syncUnit of syncUnits) {
|
|
2349
|
+
this.updateSyncUnitStatus(syncUnit.syncId, { pull: "SYNCING" });
|
|
2350
|
+
}
|
|
2351
|
+
if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
|
|
2352
|
+
let firstPull = true;
|
|
2353
|
+
const cancelPullLoop = PullResponderTransmitter.setupPull(
|
|
2354
|
+
driveId,
|
|
2355
|
+
trigger,
|
|
2356
|
+
this.saveStrand.bind(this),
|
|
2357
|
+
(error) => {
|
|
2358
|
+
const statusError = error instanceof OperationError ? error.status : "ERROR";
|
|
2359
|
+
this.updateSyncUnitStatus(driveId, { pull: statusError }, error);
|
|
2360
|
+
if (error instanceof ClientError) {
|
|
2361
|
+
this.emit(
|
|
2362
|
+
"clientStrandsError",
|
|
2363
|
+
driveId,
|
|
2364
|
+
trigger,
|
|
2365
|
+
error.response.status,
|
|
2366
|
+
error.message
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
},
|
|
2370
|
+
(revisions) => {
|
|
2371
|
+
const errorRevision = revisions.filter(
|
|
2372
|
+
(r) => r.status !== "SUCCESS"
|
|
2373
|
+
);
|
|
2374
|
+
if (errorRevision.length < 1) {
|
|
2375
|
+
this.updateSyncUnitStatus(driveId, {
|
|
2376
|
+
pull: "SUCCESS"
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
const documentIdsFromRevision = revisions.filter((rev) => rev.documentId !== "").map((rev) => rev.documentId);
|
|
2380
|
+
this.getSynchronizationUnitsIds(driveId, documentIdsFromRevision).then((revSyncUnits) => {
|
|
2381
|
+
for (const syncUnit of revSyncUnits) {
|
|
2382
|
+
const fileErrorRevision = errorRevision.find(
|
|
2383
|
+
(r) => r.documentId === syncUnit.documentId
|
|
2384
|
+
);
|
|
2385
|
+
if (fileErrorRevision) {
|
|
2386
|
+
this.updateSyncUnitStatus(
|
|
2387
|
+
syncUnit.syncId,
|
|
2388
|
+
{ pull: fileErrorRevision.status },
|
|
2389
|
+
fileErrorRevision.error
|
|
2390
|
+
);
|
|
2391
|
+
} else {
|
|
2392
|
+
this.updateSyncUnitStatus(syncUnit.syncId, {
|
|
2393
|
+
pull: "SUCCESS"
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}).catch(console.error);
|
|
2398
|
+
if (firstPull) {
|
|
2399
|
+
firstPull = false;
|
|
2400
|
+
const pushListener = drive.state.local.listeners.find(
|
|
2401
|
+
(listener) => trigger.data.url === listener.callInfo?.data
|
|
2402
|
+
);
|
|
2403
|
+
if (pushListener) {
|
|
2404
|
+
this.getSynchronizationUnitsRevision(driveId, syncUnits).then((syncUnitRevisions) => {
|
|
2405
|
+
for (const revision of syncUnitRevisions) {
|
|
2406
|
+
this.listenerStateManager.updateListenerRevision(
|
|
2407
|
+
pushListener.listenerId,
|
|
2408
|
+
driveId,
|
|
2409
|
+
revision.syncId,
|
|
2410
|
+
revision.revision
|
|
2411
|
+
).catch(logger.error);
|
|
2412
|
+
}
|
|
2413
|
+
}).catch(logger.error);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
);
|
|
2418
|
+
driveTriggers.set(trigger.id, cancelPullLoop);
|
|
2419
|
+
this.triggerMap.set(driveId, driveTriggers);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
async stopSyncRemoteDrive(driveId) {
|
|
2424
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId);
|
|
2425
|
+
const filesNodeSyncId = syncUnits.filter((syncUnit) => syncUnit.documentId !== "").map((syncUnit) => syncUnit.syncId);
|
|
2426
|
+
const triggers = this.triggerMap.get(driveId);
|
|
2427
|
+
triggers?.forEach((cancel) => cancel());
|
|
2428
|
+
this.updateSyncUnitStatus(driveId, null);
|
|
2429
|
+
for (const fileNodeSyncId of filesNodeSyncId) {
|
|
2430
|
+
this.updateSyncUnitStatus(fileNodeSyncId, null);
|
|
2431
|
+
}
|
|
2432
|
+
return this.triggerMap.delete(driveId);
|
|
2433
|
+
}
|
|
2434
|
+
defaultDrivesManagerDelegate = {
|
|
2435
|
+
detachDrive: this.detachDrive.bind(this),
|
|
2436
|
+
emit: (...args) => this.emit("defaultRemoteDrive", ...args)
|
|
2437
|
+
};
|
|
2438
|
+
queueDelegate = {
|
|
2439
|
+
checkDocumentExists: (driveId, documentId) => this.storage.checkDocumentExists(driveId, documentId),
|
|
2440
|
+
processOperationJob: async ({
|
|
2441
|
+
driveId,
|
|
2442
|
+
documentId,
|
|
2443
|
+
operations,
|
|
2444
|
+
options: options2
|
|
2445
|
+
}) => {
|
|
2446
|
+
return documentId ? this.addOperations(driveId, documentId, operations, options2) : this.addDriveOperations(
|
|
2447
|
+
driveId,
|
|
2448
|
+
operations,
|
|
2449
|
+
options2
|
|
2450
|
+
);
|
|
2451
|
+
},
|
|
2452
|
+
processActionJob: async ({
|
|
2453
|
+
driveId,
|
|
2454
|
+
documentId,
|
|
2455
|
+
actions: actions2,
|
|
2456
|
+
options: options2
|
|
2457
|
+
}) => {
|
|
2458
|
+
return documentId ? this.addActions(driveId, documentId, actions2, options2) : this.addDriveActions(
|
|
2459
|
+
driveId,
|
|
2460
|
+
actions2,
|
|
2461
|
+
options2
|
|
2462
|
+
);
|
|
2463
|
+
},
|
|
2464
|
+
processJob: async (job) => {
|
|
2465
|
+
if (isOperationJob(job)) {
|
|
2466
|
+
return this.queueDelegate.processOperationJob(job);
|
|
2467
|
+
} else if (isActionJob(job)) {
|
|
2468
|
+
return this.queueDelegate.processActionJob(job);
|
|
2469
|
+
} else {
|
|
2470
|
+
throw new Error("Unknown job type", job);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
initialize() {
|
|
2475
|
+
return this.initializePromise;
|
|
2476
|
+
}
|
|
2477
|
+
async _initialize() {
|
|
2478
|
+
await this.queueManager.init(this.queueDelegate, (error) => {
|
|
2479
|
+
logger.error(`Error initializing queue manager`, error);
|
|
2480
|
+
errors.push(error);
|
|
2481
|
+
});
|
|
2482
|
+
try {
|
|
2483
|
+
await this.defaultDrivesManager.removeOldremoteDrives();
|
|
2484
|
+
} catch (error) {
|
|
2485
|
+
logger.error(error);
|
|
2486
|
+
}
|
|
2487
|
+
const errors = [];
|
|
2488
|
+
const drives = await this.getDrives();
|
|
2489
|
+
for (const drive of drives) {
|
|
2490
|
+
await this._initializeDrive(drive).catch((error) => {
|
|
2491
|
+
logger.error(`Error initializing drive ${drive}`, error);
|
|
2492
|
+
errors.push(error);
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
if (this.options.defaultDrives.loadOnInit !== false) {
|
|
2496
|
+
await this.defaultDrivesManager.initializeDefaultRemoteDrives();
|
|
2497
|
+
}
|
|
2498
|
+
if (typeof window !== "undefined") {
|
|
2499
|
+
window.addEventListener("online", () => {
|
|
2500
|
+
this.listenerStateManager.triggerUpdate(
|
|
2501
|
+
false,
|
|
2502
|
+
{ type: "local" },
|
|
2503
|
+
this.handleListenerError.bind(this)
|
|
2504
|
+
).catch((error) => {
|
|
2505
|
+
logger.error("Non handled error updating listeners", error);
|
|
2506
|
+
});
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
return errors.length === 0 ? null : errors;
|
|
2510
|
+
}
|
|
2511
|
+
async _initializeDrive(driveId) {
|
|
2512
|
+
const drive = await this.getDrive(driveId);
|
|
2513
|
+
await this.initializeDriveSyncStatus(driveId, drive);
|
|
2514
|
+
if (this.shouldSyncRemoteDrive(drive)) {
|
|
2515
|
+
await this.startSyncRemoteDrive(driveId);
|
|
2516
|
+
}
|
|
2517
|
+
await this.listenerStateManager.initDrive(drive);
|
|
2518
|
+
}
|
|
2519
|
+
async getSynchronizationUnits(driveId, documentId, scope, branch, documentType, loadedDrive) {
|
|
2520
|
+
const drive = loadedDrive || await this.getDrive(driveId);
|
|
2521
|
+
const synchronizationUnitsQuery = await this.getSynchronizationUnitsIds(
|
|
2522
|
+
driveId,
|
|
2523
|
+
documentId,
|
|
2524
|
+
scope,
|
|
2525
|
+
branch,
|
|
2526
|
+
documentType,
|
|
2527
|
+
drive
|
|
2528
|
+
);
|
|
2529
|
+
return this.getSynchronizationUnitsRevision(
|
|
2530
|
+
driveId,
|
|
2531
|
+
synchronizationUnitsQuery,
|
|
2532
|
+
drive
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
async getSynchronizationUnitsRevision(driveId, syncUnitsQuery, loadedDrive) {
|
|
2536
|
+
const drive = loadedDrive || await this.getDrive(driveId);
|
|
2537
|
+
const revisions = await this.storage.getSynchronizationUnitsRevision(syncUnitsQuery);
|
|
2538
|
+
const synchronizationUnits = syncUnitsQuery.map(
|
|
2539
|
+
(s) => ({
|
|
2540
|
+
...s,
|
|
2541
|
+
lastUpdated: drive.created,
|
|
2542
|
+
revision: -1
|
|
2543
|
+
})
|
|
2544
|
+
);
|
|
2545
|
+
for (const revision of revisions) {
|
|
2546
|
+
const syncUnit = synchronizationUnits.find(
|
|
2547
|
+
(s) => revision.driveId === s.driveId && revision.documentId === s.documentId && revision.scope === s.scope && revision.branch === s.branch
|
|
2548
|
+
);
|
|
2549
|
+
if (syncUnit) {
|
|
2550
|
+
syncUnit.revision = revision.revision;
|
|
2551
|
+
syncUnit.lastUpdated = revision.lastUpdated;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
return synchronizationUnits;
|
|
2555
|
+
}
|
|
2556
|
+
async getSynchronizationUnitsIds(driveId, documentId, scope, branch, documentType, loadedDrive) {
|
|
2557
|
+
const drive = loadedDrive ?? await this.getDrive(driveId);
|
|
2558
|
+
const nodes = drive.state.global.nodes.filter(
|
|
2559
|
+
(node) => isFileNode(node) && (!documentId?.length || documentId.includes(node.id) || documentId.includes("*")) && (!documentType?.length || documentType.includes(node.documentType) || documentType.includes("*"))
|
|
2560
|
+
);
|
|
2561
|
+
if ((!documentId || documentId.includes("*") || documentId.includes("")) && (!documentType?.length || documentType.includes("powerhouse/document-drive") || documentType.includes("*"))) {
|
|
2562
|
+
nodes.unshift({
|
|
2563
|
+
id: "",
|
|
2564
|
+
documentType: "powerhouse/document-drive",
|
|
2565
|
+
synchronizationUnits: [
|
|
2566
|
+
{
|
|
2567
|
+
syncId: "0",
|
|
2568
|
+
scope: "global",
|
|
2569
|
+
branch: "main"
|
|
2570
|
+
}
|
|
2571
|
+
]
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
const synchronizationUnitsQuery = [];
|
|
2575
|
+
for (const node of nodes) {
|
|
2576
|
+
const nodeUnits = scope?.length || branch?.length ? node.synchronizationUnits.filter(
|
|
2577
|
+
(unit) => (!scope?.length || scope.includes(unit.scope) || scope.includes("*")) && (!branch?.length || branch.includes(unit.branch) || branch.includes("*"))
|
|
2578
|
+
) : node.synchronizationUnits;
|
|
2579
|
+
if (!nodeUnits.length) {
|
|
2580
|
+
continue;
|
|
2581
|
+
}
|
|
2582
|
+
synchronizationUnitsQuery.push(
|
|
2583
|
+
...nodeUnits.map((n) => ({
|
|
2584
|
+
driveId,
|
|
2585
|
+
documentId: node.id,
|
|
2586
|
+
syncId: n.syncId,
|
|
2587
|
+
documentType: node.documentType,
|
|
2588
|
+
scope: n.scope,
|
|
2589
|
+
branch: n.branch
|
|
2590
|
+
}))
|
|
2591
|
+
);
|
|
2592
|
+
}
|
|
2593
|
+
return synchronizationUnitsQuery;
|
|
2594
|
+
}
|
|
2595
|
+
async getSynchronizationUnitIdInfo(driveId, syncId, loadedDrive) {
|
|
2596
|
+
const drive = loadedDrive || await this.getDrive(driveId);
|
|
2597
|
+
const node = drive.state.global.nodes.find(
|
|
2598
|
+
(node2) => isFileNode(node2) && node2.synchronizationUnits.find((unit) => unit.syncId === syncId)
|
|
2599
|
+
);
|
|
2600
|
+
if (!node || !isFileNode(node)) {
|
|
2601
|
+
return void 0;
|
|
2602
|
+
}
|
|
2603
|
+
const syncUnit = node.synchronizationUnits.find(
|
|
2604
|
+
(unit) => unit.syncId === syncId
|
|
2605
|
+
);
|
|
2606
|
+
if (!syncUnit) {
|
|
2607
|
+
return void 0;
|
|
2608
|
+
}
|
|
2609
|
+
return {
|
|
2610
|
+
syncId,
|
|
2611
|
+
scope: syncUnit.scope,
|
|
2612
|
+
branch: syncUnit.branch,
|
|
2613
|
+
driveId,
|
|
2614
|
+
documentId: node.id,
|
|
2615
|
+
documentType: node.documentType
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
async getSynchronizationUnit(driveId, syncId, loadedDrive) {
|
|
2619
|
+
const syncUnit = await this.getSynchronizationUnitIdInfo(
|
|
2620
|
+
driveId,
|
|
2621
|
+
syncId,
|
|
2622
|
+
loadedDrive
|
|
2623
|
+
);
|
|
2624
|
+
if (!syncUnit) {
|
|
2625
|
+
return void 0;
|
|
2626
|
+
}
|
|
2627
|
+
const { scope, branch, documentId, documentType } = syncUnit;
|
|
2628
|
+
const document = await this.getDocument(driveId, documentId);
|
|
2629
|
+
const operations = document.operations[scope] ?? [];
|
|
2630
|
+
const lastOperation = operations[operations.length - 1];
|
|
2631
|
+
return {
|
|
2632
|
+
syncId,
|
|
2633
|
+
scope,
|
|
2634
|
+
branch,
|
|
2635
|
+
driveId,
|
|
2636
|
+
documentId,
|
|
2637
|
+
documentType,
|
|
2638
|
+
lastUpdated: lastOperation?.timestamp ?? document.lastModified,
|
|
2639
|
+
revision: lastOperation?.index ?? 0
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
async getOperationData(driveId, syncId, filter, loadedDrive) {
|
|
2643
|
+
const syncUnit = syncId === "0" ? { documentId: "", scope: "global" } : await this.getSynchronizationUnitIdInfo(driveId, syncId, loadedDrive);
|
|
2644
|
+
if (!syncUnit) {
|
|
2645
|
+
throw new Error(`Invalid Sync Id ${syncId} in drive ${driveId}`);
|
|
2646
|
+
}
|
|
2647
|
+
const document = syncId === "0" ? loadedDrive || await this.getDrive(driveId) : await this.getDocument(driveId, syncUnit.documentId);
|
|
2648
|
+
const operations = document.operations[syncUnit.scope] ?? [];
|
|
2649
|
+
const filteredOperations = operations.filter(
|
|
2650
|
+
(operation) => Object.keys(filter).length === 0 || (filter.since === void 0 || isBefore(filter.since, operation.timestamp)) && (filter.fromRevision === void 0 || operation.index > filter.fromRevision)
|
|
2651
|
+
);
|
|
2652
|
+
const limitedOperations = filter.limit ? filteredOperations.slice(0, filter.limit) : filteredOperations;
|
|
2653
|
+
return limitedOperations.map((operation) => ({
|
|
2654
|
+
hash: operation.hash,
|
|
2655
|
+
index: operation.index,
|
|
2656
|
+
timestamp: operation.timestamp,
|
|
2657
|
+
type: operation.type,
|
|
2658
|
+
input: operation.input,
|
|
2659
|
+
skip: operation.skip,
|
|
2660
|
+
context: operation.context,
|
|
2661
|
+
id: operation.id
|
|
2662
|
+
}));
|
|
2663
|
+
}
|
|
2664
|
+
getDocumentModel(documentType) {
|
|
2665
|
+
const documentModel2 = this.documentModels.find(
|
|
2666
|
+
(model) => model.documentModel.id === documentType
|
|
2667
|
+
);
|
|
2668
|
+
if (!documentModel2) {
|
|
2669
|
+
throw new Error(`Document type ${documentType} not supported`);
|
|
2670
|
+
}
|
|
2671
|
+
return documentModel2;
|
|
2672
|
+
}
|
|
2673
|
+
getDocumentModels() {
|
|
2674
|
+
return [...this.documentModels];
|
|
2675
|
+
}
|
|
2676
|
+
async addDrive(drive) {
|
|
2677
|
+
const id = drive.global.id || generateUUID();
|
|
2678
|
+
if (!id) {
|
|
2679
|
+
throw new Error("Invalid Drive Id");
|
|
2680
|
+
}
|
|
2681
|
+
const drives = await this.storage.getDrives();
|
|
2682
|
+
if (drives.includes(id)) {
|
|
2683
|
+
throw new DriveAlreadyExistsError(id);
|
|
2684
|
+
}
|
|
2685
|
+
const document = utils$1.createDocument({
|
|
2686
|
+
state: drive
|
|
2687
|
+
});
|
|
2688
|
+
await this.storage.createDrive(id, document);
|
|
2689
|
+
if (drive.global.slug) {
|
|
2690
|
+
await this.cache.deleteDocument("drives-slug", drive.global.slug);
|
|
2691
|
+
}
|
|
2692
|
+
await this._initializeDrive(id);
|
|
2693
|
+
this.emit("driveAdded", document);
|
|
2694
|
+
return document;
|
|
2695
|
+
}
|
|
2696
|
+
async addRemoteDrive(url, options2) {
|
|
2697
|
+
const { id, name, slug, icon } = options2.expectedDriveInfo || await requestPublicDrive(url);
|
|
2698
|
+
const {
|
|
2699
|
+
pullFilter,
|
|
2700
|
+
pullInterval,
|
|
2701
|
+
availableOffline,
|
|
2702
|
+
sharingType,
|
|
2703
|
+
listeners,
|
|
2704
|
+
triggers
|
|
2705
|
+
} = options2;
|
|
2706
|
+
const pullTrigger = await PullResponderTransmitter.createPullResponderTrigger(id, url, {
|
|
2707
|
+
pullFilter,
|
|
2708
|
+
pullInterval
|
|
2709
|
+
});
|
|
2710
|
+
return await this.addDrive({
|
|
2711
|
+
global: {
|
|
2712
|
+
id,
|
|
2713
|
+
name,
|
|
2714
|
+
slug,
|
|
2715
|
+
icon: icon ?? null
|
|
2716
|
+
},
|
|
2717
|
+
local: {
|
|
2718
|
+
triggers: [...triggers, pullTrigger],
|
|
2719
|
+
listeners,
|
|
2720
|
+
availableOffline,
|
|
2721
|
+
sharingType
|
|
2722
|
+
}
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
async registerPullResponderTrigger(id, url, options2) {
|
|
2726
|
+
const pullTrigger = await PullResponderTransmitter.createPullResponderTrigger(
|
|
2727
|
+
id,
|
|
2728
|
+
url,
|
|
2729
|
+
options2
|
|
2730
|
+
);
|
|
2731
|
+
return pullTrigger;
|
|
2732
|
+
}
|
|
2733
|
+
async deleteDrive(id) {
|
|
2734
|
+
const result = await Promise.allSettled([
|
|
2735
|
+
this.stopSyncRemoteDrive(id),
|
|
2736
|
+
this.listenerStateManager.removeDrive(id),
|
|
2737
|
+
this.cache.deleteDocument("drives", id),
|
|
2738
|
+
this.storage.deleteDrive(id)
|
|
2739
|
+
]);
|
|
2740
|
+
result.forEach((r) => {
|
|
2741
|
+
if (r.status === "rejected") {
|
|
2742
|
+
throw r.reason;
|
|
2743
|
+
}
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
getDrives() {
|
|
2747
|
+
return this.storage.getDrives();
|
|
2748
|
+
}
|
|
2749
|
+
async getDrive(drive, options2) {
|
|
2750
|
+
try {
|
|
2751
|
+
const document2 = await this.cache.getDocument("drives", drive);
|
|
2752
|
+
if (document2 && isDocumentDrive(document2)) {
|
|
2753
|
+
return document2;
|
|
2754
|
+
}
|
|
2755
|
+
} catch (e) {
|
|
2756
|
+
logger.error("Error getting drive from cache", e);
|
|
2757
|
+
}
|
|
2758
|
+
const driveStorage = await this.storage.getDrive(drive);
|
|
2759
|
+
const document = this._buildDocument(driveStorage, options2);
|
|
2760
|
+
if (!isDocumentDrive(document)) {
|
|
2761
|
+
throw new Error(`Document with id ${drive} is not a Document Drive`);
|
|
2762
|
+
} else {
|
|
2763
|
+
this.cache.setDocument("drives", drive, document).catch(logger.error);
|
|
2764
|
+
return document;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
async getDriveBySlug(slug, options2) {
|
|
2768
|
+
try {
|
|
2769
|
+
const document2 = await this.cache.getDocument("drives-slug", slug);
|
|
2770
|
+
if (document2 && isDocumentDrive(document2)) {
|
|
2771
|
+
return document2;
|
|
2772
|
+
}
|
|
2773
|
+
} catch (e) {
|
|
2774
|
+
logger.error("Error getting drive from cache", e);
|
|
2775
|
+
}
|
|
2776
|
+
const driveStorage = await this.storage.getDriveBySlug(slug);
|
|
2777
|
+
const document = this._buildDocument(driveStorage, options2);
|
|
2778
|
+
if (!isDocumentDrive(document)) {
|
|
2779
|
+
throw new Error(`Document with slug ${slug} is not a Document Drive`);
|
|
2780
|
+
} else {
|
|
2781
|
+
this.cache.setDocument("drives-slug", slug, document).catch(logger.error);
|
|
2782
|
+
return document;
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
async getDocument(drive, id, options2) {
|
|
2786
|
+
try {
|
|
2787
|
+
const document2 = await this.cache.getDocument(drive, id);
|
|
2788
|
+
if (document2) {
|
|
2789
|
+
return document2;
|
|
2790
|
+
}
|
|
2791
|
+
} catch (e) {
|
|
2792
|
+
logger.error("Error getting document from cache", e);
|
|
2793
|
+
}
|
|
2794
|
+
const documentStorage = await this.storage.getDocument(drive, id);
|
|
2795
|
+
const document = this._buildDocument(documentStorage, options2);
|
|
2796
|
+
this.cache.setDocument(drive, id, document).catch(logger.error);
|
|
2797
|
+
return document;
|
|
2798
|
+
}
|
|
2799
|
+
getDocuments(drive) {
|
|
2800
|
+
return this.storage.getDocuments(drive);
|
|
2801
|
+
}
|
|
2802
|
+
async createDocument(driveId, input) {
|
|
2803
|
+
let state = void 0;
|
|
2804
|
+
if (input.document) {
|
|
2805
|
+
if (input.documentType !== input.document.documentType) {
|
|
2806
|
+
throw new Error(`Provided document is not ${input.documentType}`);
|
|
2807
|
+
}
|
|
2808
|
+
const doc = this._buildDocument(input.document);
|
|
2809
|
+
state = doc.state;
|
|
2810
|
+
}
|
|
2811
|
+
const document = input.document ?? this.getDocumentModel(input.documentType).utils.createDocument();
|
|
2812
|
+
const documentStorage = {
|
|
2813
|
+
name: document.name,
|
|
2814
|
+
revision: document.revision,
|
|
2815
|
+
documentType: document.documentType,
|
|
2816
|
+
created: document.created,
|
|
2817
|
+
lastModified: document.lastModified,
|
|
2818
|
+
operations: { global: [], local: [] },
|
|
2819
|
+
initialState: document.initialState,
|
|
2820
|
+
clipboard: [],
|
|
2821
|
+
state: state ?? document.state
|
|
2822
|
+
};
|
|
2823
|
+
await this.storage.createDocument(driveId, input.id, documentStorage);
|
|
2824
|
+
for (const syncUnit of input.synchronizationUnits) {
|
|
2825
|
+
this.initSyncStatus(syncUnit.syncId, {
|
|
2826
|
+
pull: this.triggerMap.get(driveId) ? "INITIAL_SYNC" : void 0,
|
|
2827
|
+
push: this.listenerStateManager.driveHasListeners(driveId) ? "SUCCESS" : void 0
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
const operations = Object.values(document.operations).flat();
|
|
2831
|
+
if (operations.length) {
|
|
2832
|
+
if (isDocumentDrive(document)) {
|
|
2833
|
+
await this.storage.addDriveOperations(
|
|
2834
|
+
driveId,
|
|
2835
|
+
operations,
|
|
2836
|
+
document
|
|
2837
|
+
);
|
|
2838
|
+
} else {
|
|
2839
|
+
await this.storage.addDocumentOperations(
|
|
2840
|
+
driveId,
|
|
2841
|
+
input.id,
|
|
2842
|
+
operations,
|
|
2843
|
+
document
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
return document;
|
|
2848
|
+
}
|
|
2849
|
+
async deleteDocument(driveId, id) {
|
|
2850
|
+
try {
|
|
2851
|
+
const syncUnits = await this.getSynchronizationUnitsIds(driveId, [id]);
|
|
2852
|
+
for (const syncUnit of syncUnits) {
|
|
2853
|
+
this.updateSyncUnitStatus(syncUnit.syncId, null);
|
|
2854
|
+
}
|
|
2855
|
+
await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
|
|
2856
|
+
} catch (error) {
|
|
2857
|
+
logger.warn("Error deleting document", error);
|
|
2858
|
+
}
|
|
2859
|
+
await this.cache.deleteDocument(driveId, id);
|
|
2860
|
+
return this.storage.deleteDocument(driveId, id);
|
|
2861
|
+
}
|
|
2862
|
+
async _processOperations(drive, documentId, documentStorage, operations) {
|
|
2863
|
+
const operationsApplied = [];
|
|
2864
|
+
const signals = [];
|
|
2865
|
+
const documentStorageWithState = await this._addDocumentResultingStage(
|
|
2866
|
+
documentStorage,
|
|
2867
|
+
drive,
|
|
2868
|
+
documentId
|
|
2869
|
+
);
|
|
2870
|
+
let document = this._buildDocument(documentStorageWithState);
|
|
2871
|
+
let error;
|
|
2872
|
+
const operationsByScope = groupOperationsByScope(operations);
|
|
2873
|
+
for (const scope of Object.keys(operationsByScope)) {
|
|
2874
|
+
const storageDocumentOperations = documentStorage.operations[scope];
|
|
2875
|
+
const branch = removeExistingOperations(
|
|
2876
|
+
operationsByScope[scope] || [],
|
|
2877
|
+
storageDocumentOperations
|
|
2878
|
+
);
|
|
2879
|
+
if (branch.length < 1) {
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
const trunk = garbageCollect(sortOperations(storageDocumentOperations));
|
|
2883
|
+
const [invertedTrunk, tail] = attachBranch(trunk, branch);
|
|
2884
|
+
const newHistory = tail.length < 1 ? invertedTrunk : merge(trunk, invertedTrunk, reshuffleByTimestamp);
|
|
2885
|
+
const newOperations = newHistory.filter(
|
|
2886
|
+
(op) => trunk.length < 1 || precedes(trunk[trunk.length - 1], op)
|
|
2887
|
+
);
|
|
2888
|
+
for (const nextOperation of newOperations) {
|
|
2889
|
+
let skipHashValidation = false;
|
|
2890
|
+
if (tail.length > 0) {
|
|
2891
|
+
const sourceOperation = operations.find(
|
|
2892
|
+
(op) => op.hash === nextOperation.hash
|
|
2893
|
+
);
|
|
2894
|
+
skipHashValidation = !sourceOperation || sourceOperation.index !== nextOperation.index || sourceOperation.skip !== nextOperation.skip;
|
|
2895
|
+
}
|
|
2896
|
+
try {
|
|
2897
|
+
const taskQueueMethod = this.options.taskQueueMethod;
|
|
2898
|
+
const task = () => this._performOperation(
|
|
2899
|
+
drive,
|
|
2900
|
+
documentId,
|
|
2901
|
+
document,
|
|
2902
|
+
nextOperation,
|
|
2903
|
+
skipHashValidation
|
|
2904
|
+
);
|
|
2905
|
+
const appliedResult = await (taskQueueMethod ? runAsapAsync(task, taskQueueMethod) : task());
|
|
2906
|
+
document = appliedResult.document;
|
|
2907
|
+
signals.push(...appliedResult.signals);
|
|
2908
|
+
operationsApplied.push(appliedResult.operation);
|
|
2909
|
+
} catch (e) {
|
|
2910
|
+
error = e instanceof OperationError ? e : new OperationError(
|
|
2911
|
+
"ERROR",
|
|
2912
|
+
nextOperation,
|
|
2913
|
+
e.message,
|
|
2914
|
+
e.cause
|
|
2915
|
+
);
|
|
2916
|
+
break;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
return {
|
|
2921
|
+
document,
|
|
2922
|
+
operationsApplied,
|
|
2923
|
+
signals,
|
|
2924
|
+
error
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
async _addDocumentResultingStage(document, drive, documentId, options2) {
|
|
2928
|
+
const operations = options2?.revisions !== void 0 ? filterOperationsByRevision(document.operations, options2.revisions) : document.operations;
|
|
2929
|
+
const documentOperations = utils.documentHelpers.garbageCollectDocumentOperations(
|
|
2930
|
+
operations
|
|
2931
|
+
);
|
|
2932
|
+
for (const scope of Object.keys(documentOperations)) {
|
|
2933
|
+
const lastRemainingOperation = documentOperations[scope].at(-1);
|
|
2934
|
+
if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
|
|
2935
|
+
lastRemainingOperation.resultingState = await (documentId ? this.storage.getOperationResultingState?.(
|
|
2936
|
+
drive,
|
|
2937
|
+
documentId,
|
|
2938
|
+
lastRemainingOperation.index,
|
|
2939
|
+
lastRemainingOperation.scope,
|
|
2940
|
+
"main"
|
|
2941
|
+
) : this.storage.getDriveOperationResultingState?.(
|
|
2942
|
+
drive,
|
|
2943
|
+
lastRemainingOperation.index,
|
|
2944
|
+
lastRemainingOperation.scope,
|
|
2945
|
+
"main"
|
|
2946
|
+
));
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
return {
|
|
2950
|
+
...document,
|
|
2951
|
+
operations: documentOperations
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
_buildDocument(documentStorage, options2) {
|
|
2955
|
+
if (documentStorage.state && (!options2 || options2.checkHashes === false)) {
|
|
2956
|
+
return documentStorage;
|
|
2957
|
+
}
|
|
2958
|
+
const documentModel2 = this.getDocumentModel(documentStorage.documentType);
|
|
2959
|
+
const revisionOperations = options2?.revisions !== void 0 ? filterOperationsByRevision(
|
|
2960
|
+
documentStorage.operations,
|
|
2961
|
+
options2.revisions
|
|
2962
|
+
) : documentStorage.operations;
|
|
2963
|
+
const operations = utils.documentHelpers.garbageCollectDocumentOperations(
|
|
2964
|
+
revisionOperations
|
|
2965
|
+
);
|
|
2966
|
+
return utils.replayDocument(
|
|
2967
|
+
documentStorage.initialState,
|
|
2968
|
+
operations,
|
|
2969
|
+
documentModel2.reducer,
|
|
2970
|
+
void 0,
|
|
2971
|
+
documentStorage,
|
|
2972
|
+
void 0,
|
|
2973
|
+
{
|
|
2974
|
+
...options2,
|
|
2975
|
+
checkHashes: options2?.checkHashes ?? true,
|
|
2976
|
+
reuseOperationResultingState: options2?.checkHashes ?? true
|
|
2977
|
+
}
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
async _performOperation(drive, id, document, operation, skipHashValidation = false) {
|
|
2981
|
+
const documentModel2 = this.getDocumentModel(document.documentType);
|
|
2982
|
+
const signalResults = [];
|
|
2983
|
+
let newDocument = document;
|
|
2984
|
+
const scope = operation.scope;
|
|
2985
|
+
const documentOperations = utils.documentHelpers.garbageCollectDocumentOperations({
|
|
2986
|
+
...document.operations,
|
|
2987
|
+
[scope]: utils.documentHelpers.skipHeaderOperations(
|
|
2988
|
+
document.operations[scope],
|
|
2989
|
+
operation
|
|
2990
|
+
)
|
|
2991
|
+
});
|
|
2992
|
+
const lastRemainingOperation = documentOperations[scope].at(-1);
|
|
2993
|
+
if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
|
|
2994
|
+
lastRemainingOperation.resultingState = await (id ? this.storage.getOperationResultingState?.(
|
|
2995
|
+
drive,
|
|
2996
|
+
id,
|
|
2997
|
+
lastRemainingOperation.index,
|
|
2998
|
+
lastRemainingOperation.scope,
|
|
2999
|
+
"main"
|
|
3000
|
+
) : this.storage.getDriveOperationResultingState?.(
|
|
3001
|
+
drive,
|
|
3002
|
+
lastRemainingOperation.index,
|
|
3003
|
+
lastRemainingOperation.scope,
|
|
3004
|
+
"main"
|
|
3005
|
+
));
|
|
3006
|
+
}
|
|
3007
|
+
const operationSignals = [];
|
|
3008
|
+
newDocument = documentModel2.reducer(
|
|
3009
|
+
newDocument,
|
|
3010
|
+
operation,
|
|
3011
|
+
(signal) => {
|
|
3012
|
+
let handler = void 0;
|
|
3013
|
+
switch (signal.type) {
|
|
3014
|
+
case "CREATE_CHILD_DOCUMENT":
|
|
3015
|
+
handler = () => this.createDocument(drive, signal.input);
|
|
3016
|
+
break;
|
|
3017
|
+
case "DELETE_CHILD_DOCUMENT":
|
|
3018
|
+
handler = () => this.deleteDocument(drive, signal.input.id);
|
|
3019
|
+
break;
|
|
3020
|
+
case "COPY_CHILD_DOCUMENT":
|
|
3021
|
+
handler = () => this.getDocument(drive, signal.input.id).then(
|
|
3022
|
+
(documentToCopy) => this.createDocument(drive, {
|
|
3023
|
+
id: signal.input.newId,
|
|
3024
|
+
documentType: documentToCopy.documentType,
|
|
3025
|
+
document: documentToCopy,
|
|
3026
|
+
synchronizationUnits: signal.input.synchronizationUnits
|
|
3027
|
+
})
|
|
3028
|
+
);
|
|
3029
|
+
break;
|
|
3030
|
+
}
|
|
3031
|
+
if (handler) {
|
|
3032
|
+
operationSignals.push(
|
|
3033
|
+
() => handler().then((result) => ({ signal, result }))
|
|
3034
|
+
);
|
|
3035
|
+
}
|
|
3036
|
+
},
|
|
3037
|
+
{ skip: operation.skip, reuseOperationResultingState: true }
|
|
3038
|
+
);
|
|
3039
|
+
const appliedOperations = newDocument.operations[operation.scope].filter(
|
|
3040
|
+
(op) => op.index == operation.index && op.skip == operation.skip
|
|
3041
|
+
);
|
|
3042
|
+
const appliedOperation = appliedOperations.at(0);
|
|
3043
|
+
if (!appliedOperation) {
|
|
3044
|
+
throw new OperationError(
|
|
3045
|
+
"ERROR",
|
|
3046
|
+
operation,
|
|
3047
|
+
`Operation with index ${operation.index}:${operation.skip} was not applied.`
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
if (!appliedOperation.error && appliedOperation.hash !== operation.hash && !skipHashValidation) {
|
|
3051
|
+
throw new ConflictOperationError(operation, appliedOperation);
|
|
3052
|
+
}
|
|
3053
|
+
for (const signalHandler of operationSignals) {
|
|
3054
|
+
const result = await signalHandler();
|
|
3055
|
+
signalResults.push(result);
|
|
3056
|
+
}
|
|
3057
|
+
return {
|
|
3058
|
+
document: newDocument,
|
|
3059
|
+
signals: signalResults,
|
|
3060
|
+
operation: appliedOperation
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
addOperation(drive, id, operation, options2) {
|
|
3064
|
+
return this.addOperations(drive, id, [operation], options2);
|
|
3065
|
+
}
|
|
3066
|
+
async _addOperations(drive, id, callback) {
|
|
3067
|
+
if (!this.storage.addDocumentOperationsWithTransaction) {
|
|
3068
|
+
const documentStorage = await this.storage.getDocument(drive, id);
|
|
3069
|
+
const result = await callback(documentStorage);
|
|
3070
|
+
if (result.operations.length > 0) {
|
|
3071
|
+
await this.storage.addDocumentOperations(
|
|
3072
|
+
drive,
|
|
3073
|
+
id,
|
|
3074
|
+
result.operations,
|
|
3075
|
+
result.header
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
} else {
|
|
3079
|
+
await this.storage.addDocumentOperationsWithTransaction(
|
|
3080
|
+
drive,
|
|
3081
|
+
id,
|
|
3082
|
+
callback
|
|
3083
|
+
);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
queueOperation(drive, id, operation, options2) {
|
|
3087
|
+
return this.queueOperations(drive, id, [operation], options2);
|
|
3088
|
+
}
|
|
3089
|
+
async resultIfExistingOperations(drive, id, operations) {
|
|
3090
|
+
try {
|
|
3091
|
+
const document = await this.getDocument(drive, id);
|
|
3092
|
+
const newOperation = operations.find(
|
|
3093
|
+
(op) => !op.id || !document.operations[op.scope].find(
|
|
3094
|
+
(existingOp) => existingOp.id === op.id && existingOp.index === op.index && existingOp.type === op.type && existingOp.hash === op.hash
|
|
3095
|
+
)
|
|
3096
|
+
);
|
|
3097
|
+
if (!newOperation) {
|
|
3098
|
+
return {
|
|
3099
|
+
status: "SUCCESS",
|
|
3100
|
+
document,
|
|
3101
|
+
operations,
|
|
3102
|
+
signals: []
|
|
3103
|
+
};
|
|
3104
|
+
} else {
|
|
3105
|
+
return void 0;
|
|
3106
|
+
}
|
|
3107
|
+
} catch (error) {
|
|
3108
|
+
if (!error.message.includes(`Document with id ${id} not found`)) {
|
|
3109
|
+
console.error(error);
|
|
3110
|
+
}
|
|
3111
|
+
return void 0;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
async queueOperations(drive, id, operations, options2) {
|
|
3115
|
+
const result = await this.resultIfExistingOperations(drive, id, operations);
|
|
3116
|
+
if (result) {
|
|
3117
|
+
return result;
|
|
3118
|
+
}
|
|
3119
|
+
try {
|
|
3120
|
+
const jobId = await this.queueManager.addJob({
|
|
3121
|
+
driveId: drive,
|
|
3122
|
+
documentId: id,
|
|
3123
|
+
operations,
|
|
3124
|
+
options: options2
|
|
3125
|
+
});
|
|
3126
|
+
return new Promise((resolve, reject) => {
|
|
3127
|
+
const unsubscribe = this.queueManager.on(
|
|
3128
|
+
"jobCompleted",
|
|
3129
|
+
(job, result2) => {
|
|
3130
|
+
if (job.jobId === jobId) {
|
|
3131
|
+
unsubscribe();
|
|
3132
|
+
unsubscribeError();
|
|
3133
|
+
resolve(result2);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
);
|
|
3137
|
+
const unsubscribeError = this.queueManager.on(
|
|
3138
|
+
"jobFailed",
|
|
3139
|
+
(job, error) => {
|
|
3140
|
+
if (job.jobId === jobId) {
|
|
3141
|
+
unsubscribe();
|
|
3142
|
+
unsubscribeError();
|
|
3143
|
+
reject(error);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
);
|
|
3147
|
+
});
|
|
3148
|
+
} catch (error) {
|
|
3149
|
+
logger.error("Error adding job", error);
|
|
3150
|
+
throw error;
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
async queueAction(drive, id, action, options2) {
|
|
3154
|
+
return this.queueActions(drive, id, [action], options2);
|
|
3155
|
+
}
|
|
3156
|
+
async queueActions(drive, id, actions2, options2) {
|
|
3157
|
+
try {
|
|
3158
|
+
const jobId = await this.queueManager.addJob({
|
|
3159
|
+
driveId: drive,
|
|
3160
|
+
documentId: id,
|
|
3161
|
+
actions: actions2,
|
|
3162
|
+
options: options2
|
|
3163
|
+
});
|
|
3164
|
+
return new Promise((resolve, reject) => {
|
|
3165
|
+
const unsubscribe = this.queueManager.on(
|
|
3166
|
+
"jobCompleted",
|
|
3167
|
+
(job, result) => {
|
|
3168
|
+
if (job.jobId === jobId) {
|
|
3169
|
+
unsubscribe();
|
|
3170
|
+
unsubscribeError();
|
|
3171
|
+
resolve(result);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
);
|
|
3175
|
+
const unsubscribeError = this.queueManager.on(
|
|
3176
|
+
"jobFailed",
|
|
3177
|
+
(job, error) => {
|
|
3178
|
+
if (job.jobId === jobId) {
|
|
3179
|
+
unsubscribe();
|
|
3180
|
+
unsubscribeError();
|
|
3181
|
+
reject(error);
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
);
|
|
3185
|
+
});
|
|
3186
|
+
} catch (error) {
|
|
3187
|
+
logger.error("Error adding job", error);
|
|
3188
|
+
throw error;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
async queueDriveAction(drive, action, options2) {
|
|
3192
|
+
return this.queueDriveActions(drive, [action], options2);
|
|
3193
|
+
}
|
|
3194
|
+
async queueDriveActions(drive, actions2, options2) {
|
|
3195
|
+
try {
|
|
3196
|
+
const jobId = await this.queueManager.addJob({
|
|
3197
|
+
driveId: drive,
|
|
3198
|
+
actions: actions2,
|
|
3199
|
+
options: options2
|
|
3200
|
+
});
|
|
3201
|
+
return new Promise(
|
|
3202
|
+
(resolve, reject) => {
|
|
3203
|
+
const unsubscribe = this.queueManager.on(
|
|
3204
|
+
"jobCompleted",
|
|
3205
|
+
(job, result) => {
|
|
3206
|
+
if (job.jobId === jobId) {
|
|
3207
|
+
unsubscribe();
|
|
3208
|
+
unsubscribeError();
|
|
3209
|
+
resolve(result);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
);
|
|
3213
|
+
const unsubscribeError = this.queueManager.on(
|
|
3214
|
+
"jobFailed",
|
|
3215
|
+
(job, error) => {
|
|
3216
|
+
if (job.jobId === jobId) {
|
|
3217
|
+
unsubscribe();
|
|
3218
|
+
unsubscribeError();
|
|
3219
|
+
reject(error);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
);
|
|
3225
|
+
} catch (error) {
|
|
3226
|
+
logger.error("Error adding drive job", error);
|
|
3227
|
+
throw error;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
async addOperations(drive, id, operations, options2) {
|
|
3231
|
+
const result = await this.resultIfExistingOperations(drive, id, operations);
|
|
3232
|
+
if (result) {
|
|
3233
|
+
return result;
|
|
3234
|
+
}
|
|
3235
|
+
let document;
|
|
3236
|
+
const operationsApplied = [];
|
|
3237
|
+
const signals = [];
|
|
3238
|
+
let error;
|
|
3239
|
+
try {
|
|
3240
|
+
await this._addOperations(drive, id, async (documentStorage) => {
|
|
3241
|
+
const result2 = await this._processOperations(
|
|
3242
|
+
drive,
|
|
3243
|
+
id,
|
|
3244
|
+
documentStorage,
|
|
3245
|
+
operations
|
|
3246
|
+
);
|
|
3247
|
+
if (!result2.document) {
|
|
3248
|
+
logger.error("Invalid document");
|
|
3249
|
+
throw result2.error ?? new Error("Invalid document");
|
|
3250
|
+
}
|
|
3251
|
+
document = result2.document;
|
|
3252
|
+
error = result2.error;
|
|
3253
|
+
signals.push(...result2.signals);
|
|
3254
|
+
operationsApplied.push(...result2.operationsApplied);
|
|
3255
|
+
return {
|
|
3256
|
+
operations: result2.operationsApplied,
|
|
3257
|
+
header: result2.document,
|
|
3258
|
+
newState: document.state
|
|
3259
|
+
};
|
|
3260
|
+
});
|
|
3261
|
+
if (document) {
|
|
3262
|
+
this.cache.setDocument(drive, id, document).catch(logger.error);
|
|
3263
|
+
}
|
|
3264
|
+
const { scopes, branches } = operationsApplied.reduce(
|
|
3265
|
+
(acc, operation) => {
|
|
3266
|
+
if (!acc.scopes.includes(operation.scope)) {
|
|
3267
|
+
acc.scopes.push(operation.scope);
|
|
3268
|
+
}
|
|
3269
|
+
return acc;
|
|
3270
|
+
},
|
|
3271
|
+
{ scopes: [], branches: ["main"] }
|
|
3272
|
+
);
|
|
3273
|
+
const syncUnits = await this.getSynchronizationUnits(
|
|
3274
|
+
drive,
|
|
3275
|
+
[id],
|
|
3276
|
+
scopes,
|
|
3277
|
+
branches
|
|
3278
|
+
);
|
|
3279
|
+
const newOp = operationsApplied.find(
|
|
3280
|
+
(appliedOp) => !operations.find(
|
|
3281
|
+
(o) => o.id === appliedOp.id && o.index === appliedOp.index && o.skip === appliedOp.skip && o.hash === appliedOp.hash
|
|
3282
|
+
)
|
|
3283
|
+
);
|
|
3284
|
+
const source = newOp ? { type: "local" } : options2?.source ?? { type: "local" };
|
|
3285
|
+
const operationSource = this.getOperationSource(source);
|
|
3286
|
+
this.listenerStateManager.updateSynchronizationRevisions(
|
|
3287
|
+
drive,
|
|
3288
|
+
syncUnits,
|
|
3289
|
+
source,
|
|
3290
|
+
() => {
|
|
3291
|
+
this.updateSyncUnitStatus(drive, {
|
|
3292
|
+
[operationSource]: "SYNCING"
|
|
3293
|
+
});
|
|
3294
|
+
for (const syncUnit of syncUnits) {
|
|
3295
|
+
this.updateSyncUnitStatus(syncUnit.syncId, {
|
|
3296
|
+
[operationSource]: "SYNCING"
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
},
|
|
3300
|
+
this.handleListenerError.bind(this),
|
|
3301
|
+
options2?.forceSync ?? source.type === "local"
|
|
3302
|
+
).then((updates) => {
|
|
3303
|
+
if (updates.length) {
|
|
3304
|
+
this.updateSyncUnitStatus(drive, {
|
|
3305
|
+
[operationSource]: "SUCCESS"
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
for (const syncUnit of syncUnits) {
|
|
3309
|
+
this.updateSyncUnitStatus(syncUnit.syncId, {
|
|
3310
|
+
[operationSource]: "SUCCESS"
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
}).catch((error2) => {
|
|
3314
|
+
logger.error("Non handled error updating sync revision", error2);
|
|
3315
|
+
this.updateSyncUnitStatus(
|
|
3316
|
+
drive,
|
|
3317
|
+
{
|
|
3318
|
+
[operationSource]: "ERROR"
|
|
3319
|
+
},
|
|
3320
|
+
error2
|
|
3321
|
+
);
|
|
3322
|
+
for (const syncUnit of syncUnits) {
|
|
3323
|
+
this.updateSyncUnitStatus(
|
|
3324
|
+
syncUnit.syncId,
|
|
3325
|
+
{
|
|
3326
|
+
[operationSource]: "ERROR"
|
|
3327
|
+
},
|
|
3328
|
+
error2
|
|
3329
|
+
);
|
|
3330
|
+
}
|
|
3331
|
+
});
|
|
3332
|
+
if (error) {
|
|
3333
|
+
throw error;
|
|
3334
|
+
}
|
|
3335
|
+
return {
|
|
3336
|
+
status: "SUCCESS",
|
|
3337
|
+
document,
|
|
3338
|
+
operations: operationsApplied,
|
|
3339
|
+
signals
|
|
3340
|
+
};
|
|
3341
|
+
} catch (error2) {
|
|
3342
|
+
const operationError = error2 instanceof OperationError ? error2 : new OperationError(
|
|
3343
|
+
"ERROR",
|
|
3344
|
+
void 0,
|
|
3345
|
+
error2.message,
|
|
3346
|
+
error2.cause
|
|
3347
|
+
);
|
|
3348
|
+
return {
|
|
3349
|
+
status: operationError.status,
|
|
3350
|
+
error: operationError,
|
|
3351
|
+
document,
|
|
3352
|
+
operations: operationsApplied,
|
|
3353
|
+
signals
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
addDriveOperation(drive, operation, options2) {
|
|
3358
|
+
return this.addDriveOperations(drive, [operation], options2);
|
|
3359
|
+
}
|
|
3360
|
+
async clearStorage() {
|
|
3361
|
+
for (const drive of await this.getDrives()) {
|
|
3362
|
+
await this.deleteDrive(drive);
|
|
3363
|
+
}
|
|
3364
|
+
await this.storage.clearStorage?.();
|
|
3365
|
+
}
|
|
3366
|
+
async _addDriveOperations(drive, callback) {
|
|
3367
|
+
if (!this.storage.addDriveOperationsWithTransaction) {
|
|
3368
|
+
const documentStorage = await this.storage.getDrive(drive);
|
|
3369
|
+
const result = await callback(documentStorage);
|
|
3370
|
+
if (result.operations.length > 0) {
|
|
3371
|
+
await this.storage.addDriveOperations(
|
|
3372
|
+
drive,
|
|
3373
|
+
result.operations,
|
|
3374
|
+
result.header
|
|
3375
|
+
);
|
|
3376
|
+
}
|
|
3377
|
+
return result;
|
|
3378
|
+
} else {
|
|
3379
|
+
return this.storage.addDriveOperationsWithTransaction(drive, callback);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
queueDriveOperation(drive, operation, options2) {
|
|
3383
|
+
return this.queueDriveOperations(drive, [operation], options2);
|
|
3384
|
+
}
|
|
3385
|
+
async resultIfExistingDriveOperations(driveId, operations) {
|
|
3386
|
+
try {
|
|
3387
|
+
const drive = await this.getDrive(driveId);
|
|
3388
|
+
const newOperation = operations.find(
|
|
3389
|
+
(op) => !op.id || !drive.operations[op.scope].find(
|
|
3390
|
+
(existingOp) => existingOp.id === op.id && existingOp.index === op.index && existingOp.type === op.type && existingOp.hash === op.hash
|
|
3391
|
+
)
|
|
3392
|
+
);
|
|
3393
|
+
if (!newOperation) {
|
|
3394
|
+
return {
|
|
3395
|
+
status: "SUCCESS",
|
|
3396
|
+
document: drive,
|
|
3397
|
+
operations,
|
|
3398
|
+
signals: []
|
|
3399
|
+
};
|
|
3400
|
+
} else {
|
|
3401
|
+
return void 0;
|
|
3402
|
+
}
|
|
3403
|
+
} catch (error) {
|
|
3404
|
+
console.error(error);
|
|
3405
|
+
return void 0;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
async queueDriveOperations(drive, operations, options2) {
|
|
3409
|
+
const result = await this.resultIfExistingDriveOperations(
|
|
3410
|
+
drive,
|
|
3411
|
+
operations
|
|
3412
|
+
);
|
|
3413
|
+
if (result) {
|
|
3414
|
+
return result;
|
|
3415
|
+
}
|
|
3416
|
+
try {
|
|
3417
|
+
const jobId = await this.queueManager.addJob({
|
|
3418
|
+
driveId: drive,
|
|
3419
|
+
operations,
|
|
3420
|
+
options: options2
|
|
3421
|
+
});
|
|
3422
|
+
return new Promise(
|
|
3423
|
+
(resolve, reject) => {
|
|
3424
|
+
const unsubscribe = this.queueManager.on(
|
|
3425
|
+
"jobCompleted",
|
|
3426
|
+
(job, result2) => {
|
|
3427
|
+
if (job.jobId === jobId) {
|
|
3428
|
+
unsubscribe();
|
|
3429
|
+
unsubscribeError();
|
|
3430
|
+
resolve(result2);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
);
|
|
3434
|
+
const unsubscribeError = this.queueManager.on(
|
|
3435
|
+
"jobFailed",
|
|
3436
|
+
(job, error) => {
|
|
3437
|
+
if (job.jobId === jobId) {
|
|
3438
|
+
unsubscribe();
|
|
3439
|
+
unsubscribeError();
|
|
3440
|
+
reject(error);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
);
|
|
3444
|
+
}
|
|
3445
|
+
);
|
|
3446
|
+
} catch (error) {
|
|
3447
|
+
logger.error("Error adding drive job", error);
|
|
3448
|
+
throw error;
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
async addDriveOperations(drive, operations, options2) {
|
|
3452
|
+
let document;
|
|
3453
|
+
const operationsApplied = [];
|
|
3454
|
+
const signals = [];
|
|
3455
|
+
let error;
|
|
3456
|
+
const result = await this.resultIfExistingDriveOperations(
|
|
3457
|
+
drive,
|
|
3458
|
+
operations
|
|
3459
|
+
);
|
|
3460
|
+
if (result) {
|
|
3461
|
+
return result;
|
|
3462
|
+
}
|
|
3463
|
+
try {
|
|
3464
|
+
await this._addDriveOperations(drive, async (documentStorage) => {
|
|
3465
|
+
const result2 = await this._processOperations(drive, void 0, documentStorage, operations.slice());
|
|
3466
|
+
document = result2.document;
|
|
3467
|
+
operationsApplied.push(...result2.operationsApplied);
|
|
3468
|
+
signals.push(...result2.signals);
|
|
3469
|
+
error = result2.error;
|
|
3470
|
+
return {
|
|
3471
|
+
operations: result2.operationsApplied,
|
|
3472
|
+
header: result2.document
|
|
3473
|
+
};
|
|
3474
|
+
});
|
|
3475
|
+
if (!document || !isDocumentDrive(document)) {
|
|
3476
|
+
throw error ?? new Error("Invalid Document Drive document");
|
|
3477
|
+
}
|
|
3478
|
+
this.cache.setDocument("drives", drive, document).catch(logger.error);
|
|
3479
|
+
for (const operation of operationsApplied) {
|
|
3480
|
+
switch (operation.type) {
|
|
3481
|
+
case "ADD_LISTENER": {
|
|
3482
|
+
await this.addListener(drive, operation);
|
|
3483
|
+
break;
|
|
3484
|
+
}
|
|
3485
|
+
case "REMOVE_LISTENER": {
|
|
3486
|
+
await this.removeListener(drive, operation);
|
|
3487
|
+
break;
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
const lastOperation = operationsApplied.filter((op) => op.scope === "global").slice().pop();
|
|
3492
|
+
if (lastOperation) {
|
|
3493
|
+
const newOp = operationsApplied.find(
|
|
3494
|
+
(appliedOp) => !operations.find(
|
|
3495
|
+
(o) => o.id === appliedOp.id && o.index === appliedOp.index && o.skip === appliedOp.skip && o.hash === appliedOp.hash
|
|
3496
|
+
)
|
|
3497
|
+
);
|
|
3498
|
+
const source = newOp ? { type: "local" } : options2?.source ?? { type: "local" };
|
|
3499
|
+
const operationSource = this.getOperationSource(source);
|
|
3500
|
+
this.listenerStateManager.updateSynchronizationRevisions(
|
|
3501
|
+
drive,
|
|
3502
|
+
[
|
|
3503
|
+
{
|
|
3504
|
+
syncId: "0",
|
|
3505
|
+
driveId: drive,
|
|
3506
|
+
documentId: "",
|
|
3507
|
+
scope: "global",
|
|
3508
|
+
branch: "main",
|
|
3509
|
+
documentType: "powerhouse/document-drive",
|
|
3510
|
+
lastUpdated: lastOperation.timestamp,
|
|
3511
|
+
revision: lastOperation.index
|
|
3512
|
+
}
|
|
3513
|
+
],
|
|
3514
|
+
source,
|
|
3515
|
+
() => {
|
|
3516
|
+
this.updateSyncUnitStatus(drive, {
|
|
3517
|
+
[operationSource]: "SYNCING"
|
|
3518
|
+
});
|
|
3519
|
+
},
|
|
3520
|
+
this.handleListenerError.bind(this),
|
|
3521
|
+
options2?.forceSync ?? source.type === "local"
|
|
3522
|
+
).then((updates) => {
|
|
3523
|
+
if (updates.length) {
|
|
3524
|
+
this.updateSyncUnitStatus(drive, {
|
|
3525
|
+
[operationSource]: "SUCCESS"
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
}).catch((error2) => {
|
|
3529
|
+
logger.error("Non handled error updating sync revision", error2);
|
|
3530
|
+
this.updateSyncUnitStatus(
|
|
3531
|
+
drive,
|
|
3532
|
+
{ [operationSource]: "ERROR" },
|
|
3533
|
+
error2
|
|
3534
|
+
);
|
|
3535
|
+
});
|
|
3536
|
+
}
|
|
3537
|
+
if (this.shouldSyncRemoteDrive(document)) {
|
|
3538
|
+
this.startSyncRemoteDrive(document.state.global.id);
|
|
3539
|
+
} else {
|
|
3540
|
+
this.stopSyncRemoteDrive(document.state.global.id);
|
|
3541
|
+
}
|
|
3542
|
+
if (error) {
|
|
3543
|
+
throw error;
|
|
3544
|
+
}
|
|
3545
|
+
return {
|
|
3546
|
+
status: "SUCCESS",
|
|
3547
|
+
document,
|
|
3548
|
+
operations: operationsApplied,
|
|
3549
|
+
signals
|
|
3550
|
+
};
|
|
3551
|
+
} catch (error2) {
|
|
3552
|
+
const operationError = error2 instanceof OperationError ? error2 : new OperationError(
|
|
3553
|
+
"ERROR",
|
|
3554
|
+
void 0,
|
|
3555
|
+
error2.message,
|
|
3556
|
+
error2.cause
|
|
3557
|
+
);
|
|
3558
|
+
return {
|
|
3559
|
+
status: operationError.status,
|
|
3560
|
+
error: operationError,
|
|
3561
|
+
document,
|
|
3562
|
+
operations: operationsApplied,
|
|
3563
|
+
signals
|
|
3564
|
+
};
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
_buildOperations(document, actions2) {
|
|
3568
|
+
const operations = [];
|
|
3569
|
+
const { reducer } = this.getDocumentModel(document.documentType);
|
|
3570
|
+
for (const action of actions2) {
|
|
3571
|
+
document = reducer(document, action);
|
|
3572
|
+
const operation = document.operations[action.scope].slice().pop();
|
|
3573
|
+
if (!operation) {
|
|
3574
|
+
throw new Error("Error creating operations");
|
|
3575
|
+
}
|
|
3576
|
+
operations.push(operation);
|
|
3577
|
+
}
|
|
3578
|
+
return operations;
|
|
3579
|
+
}
|
|
3580
|
+
async addAction(drive, id, action, options2) {
|
|
3581
|
+
return this.addActions(drive, id, [action], options2);
|
|
3582
|
+
}
|
|
3583
|
+
async addActions(drive, id, actions2, options2) {
|
|
3584
|
+
const document = await this.getDocument(drive, id);
|
|
3585
|
+
const operations = this._buildOperations(document, actions2);
|
|
3586
|
+
return this.addOperations(drive, id, operations, options2);
|
|
3587
|
+
}
|
|
3588
|
+
async addDriveAction(drive, action, options2) {
|
|
3589
|
+
return this.addDriveActions(drive, [action], options2);
|
|
3590
|
+
}
|
|
3591
|
+
async addDriveActions(drive, actions2, options2) {
|
|
3592
|
+
const document = await this.getDrive(drive);
|
|
3593
|
+
const operations = this._buildOperations(document, actions2);
|
|
3594
|
+
const result = await this.addDriveOperations(drive, operations, options2);
|
|
3595
|
+
return result;
|
|
3596
|
+
}
|
|
3597
|
+
async addInternalListener(driveId, receiver, options2) {
|
|
3598
|
+
const listener = {
|
|
3599
|
+
callInfo: {
|
|
3600
|
+
data: "",
|
|
3601
|
+
name: "Interal",
|
|
3602
|
+
transmitterType: "Internal"
|
|
3603
|
+
},
|
|
3604
|
+
system: true,
|
|
3605
|
+
...options2
|
|
3606
|
+
};
|
|
3607
|
+
await this.addDriveAction(driveId, actions.addListener({ listener }));
|
|
3608
|
+
const transmitter = await this.getTransmitter(driveId, options2.listenerId);
|
|
3609
|
+
if (!transmitter) {
|
|
3610
|
+
logger.error("Internal listener not found");
|
|
3611
|
+
throw new Error("Internal listener not found");
|
|
3612
|
+
}
|
|
3613
|
+
if (!(transmitter instanceof InternalTransmitter)) {
|
|
3614
|
+
logger.error("Listener is not an internal transmitter");
|
|
3615
|
+
throw new Error("Listener is not an internal transmitter");
|
|
3616
|
+
}
|
|
3617
|
+
transmitter.setReceiver(receiver);
|
|
3618
|
+
return transmitter;
|
|
3619
|
+
}
|
|
3620
|
+
async detachDrive(driveId) {
|
|
3621
|
+
const documentDrive = await this.getDrive(driveId);
|
|
3622
|
+
const listeners = documentDrive.state.local.listeners || [];
|
|
3623
|
+
const triggers = documentDrive.state.local.triggers || [];
|
|
3624
|
+
for (const listener of listeners) {
|
|
3625
|
+
await this.addDriveAction(
|
|
3626
|
+
driveId,
|
|
3627
|
+
actions.removeListener({ listenerId: listener.listenerId })
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
for (const trigger of triggers) {
|
|
3631
|
+
await this.addDriveAction(
|
|
3632
|
+
driveId,
|
|
3633
|
+
actions.removeTrigger({ triggerId: trigger.id })
|
|
3634
|
+
);
|
|
3635
|
+
}
|
|
3636
|
+
await this.addDriveAction(
|
|
3637
|
+
driveId,
|
|
3638
|
+
actions.setSharingType({ type: "LOCAL" })
|
|
3639
|
+
);
|
|
3640
|
+
}
|
|
3641
|
+
async addListener(driveId, operation) {
|
|
3642
|
+
const { listener } = operation.input;
|
|
3643
|
+
await this.listenerStateManager.addListener({
|
|
3644
|
+
...listener,
|
|
3645
|
+
driveId,
|
|
3646
|
+
label: listener.label ?? "",
|
|
3647
|
+
system: listener.system ?? false,
|
|
3648
|
+
filter: {
|
|
3649
|
+
branch: listener.filter.branch ?? [],
|
|
3650
|
+
documentId: listener.filter.documentId ?? [],
|
|
3651
|
+
documentType: listener.filter.documentType ?? [],
|
|
3652
|
+
scope: listener.filter.scope ?? []
|
|
3653
|
+
},
|
|
3654
|
+
callInfo: {
|
|
3655
|
+
data: listener.callInfo?.data ?? "",
|
|
3656
|
+
name: listener.callInfo?.name ?? "PullResponder",
|
|
3657
|
+
transmitterType: listener.callInfo?.transmitterType ?? "PullResponder"
|
|
3658
|
+
}
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3661
|
+
async removeListener(driveId, operation) {
|
|
3662
|
+
const { listenerId } = operation.input;
|
|
3663
|
+
await this.listenerStateManager.removeListener(driveId, listenerId);
|
|
3664
|
+
}
|
|
3665
|
+
getTransmitter(driveId, listenerId) {
|
|
3666
|
+
return this.listenerStateManager.getTransmitter(driveId, listenerId);
|
|
3667
|
+
}
|
|
3668
|
+
getListener(driveId, listenerId) {
|
|
3669
|
+
return this.listenerStateManager.getListener(driveId, listenerId);
|
|
3670
|
+
}
|
|
3671
|
+
getSyncStatus(syncUnitId) {
|
|
3672
|
+
const status = this.syncStatus.get(syncUnitId);
|
|
3673
|
+
if (!status) {
|
|
3674
|
+
return new SynchronizationUnitNotFoundError(
|
|
3675
|
+
`Sync status not found for syncUnitId: ${syncUnitId}`,
|
|
3676
|
+
syncUnitId
|
|
3677
|
+
);
|
|
3678
|
+
}
|
|
3679
|
+
return this.getCombinedSyncUnitStatus(status);
|
|
3680
|
+
}
|
|
3681
|
+
on(event, cb) {
|
|
3682
|
+
return this.emitter.on(event, cb);
|
|
3683
|
+
}
|
|
3684
|
+
emit(event, ...args) {
|
|
3685
|
+
return this.emitter.emit(event, ...args);
|
|
3686
|
+
}
|
|
3687
|
+
};
|
|
3688
|
+
var DocumentDriveServer = ReadModeServer(BaseDocumentDriveServer);
|
|
3689
|
+
function ensureDir(dir) {
|
|
3690
|
+
if (!existsSync(dir)) {
|
|
3691
|
+
mkdirSync(dir, { recursive: true });
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
var FilesystemStorage = class _FilesystemStorage {
|
|
3695
|
+
basePath;
|
|
3696
|
+
drivesPath;
|
|
3697
|
+
static DRIVES_DIR = "drives";
|
|
3698
|
+
constructor(basePath) {
|
|
3699
|
+
this.basePath = basePath;
|
|
3700
|
+
ensureDir(this.basePath);
|
|
3701
|
+
this.drivesPath = path2.join(this.basePath, _FilesystemStorage.DRIVES_DIR);
|
|
3702
|
+
ensureDir(this.drivesPath);
|
|
3703
|
+
}
|
|
3704
|
+
_buildDocumentPath(...args) {
|
|
3705
|
+
return `${path2.join(
|
|
3706
|
+
this.basePath,
|
|
3707
|
+
...args.map((arg) => sanitize(arg))
|
|
3708
|
+
)}.json`;
|
|
3709
|
+
}
|
|
3710
|
+
async getDocuments(drive) {
|
|
3711
|
+
let files = [];
|
|
3712
|
+
try {
|
|
3713
|
+
files = readdirSync(path2.join(this.basePath, drive), {
|
|
3714
|
+
withFileTypes: true
|
|
3715
|
+
});
|
|
3716
|
+
} catch (error) {
|
|
3717
|
+
if (error.code !== "ENOENT") {
|
|
3718
|
+
throw error;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
const documents = [];
|
|
3722
|
+
for (const file of files.filter((file2) => file2.isFile())) {
|
|
3723
|
+
try {
|
|
3724
|
+
const documentId = path2.parse(file.name).name;
|
|
3725
|
+
await this.getDocument(drive, documentId);
|
|
3726
|
+
documents.push(documentId);
|
|
3727
|
+
} catch {
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
return documents;
|
|
3731
|
+
}
|
|
3732
|
+
checkDocumentExists(drive, id) {
|
|
3733
|
+
const documentExists = existsSync(this._buildDocumentPath(drive, id));
|
|
3734
|
+
return Promise.resolve(documentExists);
|
|
3735
|
+
}
|
|
3736
|
+
async getDocument(drive, id) {
|
|
3737
|
+
try {
|
|
3738
|
+
const content = readFileSync(this._buildDocumentPath(drive, id), {
|
|
3739
|
+
encoding: "utf-8"
|
|
3740
|
+
});
|
|
3741
|
+
return JSON.parse(content);
|
|
3742
|
+
} catch (error) {
|
|
3743
|
+
throw new Error(`Document with id ${id} not found`);
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
async createDocument(drive, id, document) {
|
|
3747
|
+
const documentPath = this._buildDocumentPath(drive, id);
|
|
3748
|
+
ensureDir(path2.dirname(documentPath));
|
|
3749
|
+
writeFileSync(documentPath, stringify(document), {
|
|
3750
|
+
encoding: "utf-8"
|
|
3751
|
+
});
|
|
3752
|
+
return Promise.resolve();
|
|
3753
|
+
}
|
|
3754
|
+
async clearStorage() {
|
|
3755
|
+
const drivesPath = path2.join(this.basePath, _FilesystemStorage.DRIVES_DIR);
|
|
3756
|
+
const drives = (await fs.readdir(drivesPath, {
|
|
3757
|
+
withFileTypes: true,
|
|
3758
|
+
recursive: true
|
|
3759
|
+
})).filter((dirent) => !!dirent.name);
|
|
3760
|
+
await Promise.all(
|
|
3761
|
+
drives.map(async (dirent) => {
|
|
3762
|
+
await fs.rm(path2.join(drivesPath, dirent.name), {
|
|
3763
|
+
recursive: true
|
|
3764
|
+
});
|
|
3765
|
+
})
|
|
3766
|
+
);
|
|
3767
|
+
const files = (await fs.readdir(this.basePath, { withFileTypes: true })).filter(
|
|
3768
|
+
(file) => file.name !== _FilesystemStorage.DRIVES_DIR && !!file.name
|
|
3769
|
+
);
|
|
3770
|
+
await Promise.all(
|
|
3771
|
+
files.map(async (dirent) => {
|
|
3772
|
+
await fs.rm(path2.join(this.basePath, dirent.name), {
|
|
3773
|
+
recursive: true
|
|
3774
|
+
});
|
|
3775
|
+
})
|
|
3776
|
+
);
|
|
3777
|
+
}
|
|
3778
|
+
async deleteDocument(drive, id) {
|
|
3779
|
+
return fs.rm(this._buildDocumentPath(drive, id));
|
|
3780
|
+
}
|
|
3781
|
+
async addDocumentOperations(drive, id, operations, header) {
|
|
3782
|
+
const document = await this.getDocument(drive, id);
|
|
3783
|
+
if (!document) {
|
|
3784
|
+
throw new Error(`Document with id ${id} not found`);
|
|
3785
|
+
}
|
|
3786
|
+
const mergedOperations = mergeOperations(document.operations, operations);
|
|
3787
|
+
await this.createDocument(drive, id, {
|
|
3788
|
+
...document,
|
|
3789
|
+
...header,
|
|
3790
|
+
operations: mergedOperations
|
|
3791
|
+
});
|
|
3792
|
+
}
|
|
3793
|
+
async getDrives() {
|
|
3794
|
+
const files = readdirSync(this.drivesPath, {
|
|
3795
|
+
withFileTypes: true
|
|
3796
|
+
});
|
|
3797
|
+
const drives = [];
|
|
3798
|
+
for (const file of files.filter((file2) => file2.isFile())) {
|
|
3799
|
+
try {
|
|
3800
|
+
const driveId = path2.parse(file.name).name;
|
|
3801
|
+
await this.getDrive(driveId);
|
|
3802
|
+
drives.push(driveId);
|
|
3803
|
+
} catch {
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
return drives;
|
|
3807
|
+
}
|
|
3808
|
+
async getDrive(id) {
|
|
3809
|
+
try {
|
|
3810
|
+
return await this.getDocument(
|
|
3811
|
+
_FilesystemStorage.DRIVES_DIR,
|
|
3812
|
+
id
|
|
3813
|
+
);
|
|
3814
|
+
} catch {
|
|
3815
|
+
throw new DriveNotFoundError(id);
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
async getDriveBySlug(slug) {
|
|
3819
|
+
const drives = (await this.getDrives()).reverse();
|
|
3820
|
+
for (const drive of drives) {
|
|
3821
|
+
const {
|
|
3822
|
+
initialState: {
|
|
3823
|
+
state: {
|
|
3824
|
+
global: { slug: driveSlug }
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
} = await this.getDrive(drive);
|
|
3828
|
+
if (driveSlug === slug) {
|
|
3829
|
+
return this.getDrive(drive);
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
throw new Error(`Drive with slug ${slug} not found`);
|
|
3833
|
+
}
|
|
3834
|
+
createDrive(id, drive) {
|
|
3835
|
+
return this.createDocument(_FilesystemStorage.DRIVES_DIR, id, drive);
|
|
3836
|
+
}
|
|
3837
|
+
async deleteDrive(id) {
|
|
3838
|
+
const documents = await this.getDocuments(id);
|
|
3839
|
+
await this.deleteDocument(_FilesystemStorage.DRIVES_DIR, id);
|
|
3840
|
+
await Promise.all(
|
|
3841
|
+
documents.map((document) => this.deleteDocument(id, document))
|
|
3842
|
+
);
|
|
3843
|
+
}
|
|
3844
|
+
async addDriveOperations(id, operations, header) {
|
|
3845
|
+
const drive = await this.getDrive(id);
|
|
3846
|
+
const mergedOperations = mergeOperations(drive.operations, operations);
|
|
3847
|
+
await this.createDrive(id, {
|
|
3848
|
+
...drive,
|
|
3849
|
+
...header,
|
|
3850
|
+
operations: mergedOperations
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
async getSynchronizationUnitsRevision(units) {
|
|
3854
|
+
const results = await Promise.allSettled(
|
|
3855
|
+
units.map(async (unit) => {
|
|
3856
|
+
try {
|
|
3857
|
+
const document = await (unit.documentId ? this.getDocument(unit.driveId, unit.documentId) : this.getDrive(unit.driveId));
|
|
3858
|
+
if (!document) {
|
|
3859
|
+
return void 0;
|
|
3860
|
+
}
|
|
3861
|
+
const operation = document.operations[unit.scope].at(-1);
|
|
3862
|
+
if (operation) {
|
|
3863
|
+
return {
|
|
3864
|
+
driveId: unit.driveId,
|
|
3865
|
+
documentId: unit.documentId,
|
|
3866
|
+
scope: unit.scope,
|
|
3867
|
+
branch: unit.branch,
|
|
3868
|
+
lastUpdated: operation.timestamp,
|
|
3869
|
+
revision: operation.index
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
} catch {
|
|
3873
|
+
return void 0;
|
|
3874
|
+
}
|
|
3875
|
+
})
|
|
3876
|
+
);
|
|
3877
|
+
return results.reduce((acc, curr) => {
|
|
3878
|
+
if (curr.status === "fulfilled" && curr.value !== void 0) {
|
|
3879
|
+
acc.push(curr.value);
|
|
3880
|
+
}
|
|
3881
|
+
return acc;
|
|
3882
|
+
}, []);
|
|
3883
|
+
}
|
|
3884
|
+
};
|
|
3885
|
+
dotenv.config();
|
|
3886
|
+
var driveServer = new DocumentDriveServer(
|
|
3887
|
+
[module, ...Object.values(DocumentModelsLibs)],
|
|
3888
|
+
new FilesystemStorage(path2.join(__dirname, "../file-storage"))
|
|
3889
|
+
);
|
|
3890
|
+
var serverPort = process.env.PORT ? Number(process.env.PORT) : 4001;
|
|
3891
|
+
var startServer = async () => {
|
|
3892
|
+
const db = await drizzle("pglite", "./dev.db");
|
|
3893
|
+
await driveServer.initialize();
|
|
3894
|
+
try {
|
|
3895
|
+
await driveServer.addDrive({
|
|
3896
|
+
global: {
|
|
3897
|
+
id: "powerhouse",
|
|
3898
|
+
name: "Powerhouse",
|
|
3899
|
+
icon: "powerhouse",
|
|
3900
|
+
slug: "powerhouse"
|
|
3901
|
+
},
|
|
3902
|
+
local: {
|
|
3903
|
+
availableOffline: true,
|
|
3904
|
+
listeners: [],
|
|
3905
|
+
sharingType: "public",
|
|
3906
|
+
triggers: []
|
|
3907
|
+
}
|
|
3908
|
+
});
|
|
3909
|
+
} catch (e) {
|
|
3910
|
+
console.info("Default drive already exists. Skipping...");
|
|
3911
|
+
}
|
|
3912
|
+
try {
|
|
3913
|
+
await startAPI(driveServer, {
|
|
3914
|
+
port: serverPort
|
|
3915
|
+
});
|
|
3916
|
+
setAdditionalContextFields({ db });
|
|
3917
|
+
await registerInternalListener({
|
|
3918
|
+
name: "search",
|
|
3919
|
+
options: searchListener.options,
|
|
3920
|
+
transmit: (strands) => searchListener.transmit(strands, db)
|
|
3921
|
+
});
|
|
3922
|
+
await addSubgraph({
|
|
3923
|
+
getSchema: () => createSchema(
|
|
3924
|
+
driveServer,
|
|
3925
|
+
searchListener.resolvers,
|
|
3926
|
+
searchListener.typeDefs
|
|
3927
|
+
),
|
|
3928
|
+
name: "search/:drive"
|
|
3929
|
+
});
|
|
3930
|
+
} catch (e) {
|
|
3931
|
+
console.error("App crashed", e);
|
|
3932
|
+
}
|
|
3933
|
+
};
|
|
3934
|
+
|
|
3935
|
+
export { startServer };
|