document-drive 0.0.26 → 0.0.28
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/package.json +18 -10
- package/src/server/error.ts +18 -0
- package/src/server/index.ts +678 -82
- package/src/server/listener/index.ts +2 -0
- package/src/server/listener/manager.ts +382 -0
- package/src/server/listener/transmitter/index.ts +3 -0
- package/src/server/listener/transmitter/pull-responder.ts +308 -0
- package/src/server/listener/transmitter/switchboard-push.ts +63 -0
- package/src/server/listener/transmitter/types.ts +18 -0
- package/src/server/types.ts +209 -23
- package/src/storage/filesystem.ts +2 -2
- package/src/storage/index.ts +0 -4
- package/src/storage/memory.ts +5 -2
- package/src/storage/prisma.ts +65 -18
- package/src/utils/graphql.ts +46 -0
- package/src/{utils.ts → utils/index.ts} +8 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ListenerCallInfo,
|
|
3
|
+
ListenerFilter
|
|
4
|
+
} from 'document-model-libs/document-drive';
|
|
5
|
+
import { OperationScope } from 'document-model/document';
|
|
6
|
+
import { OperationError } from '../error';
|
|
7
|
+
import {
|
|
8
|
+
BaseListenerManager,
|
|
9
|
+
ErrorStatus,
|
|
10
|
+
Listener,
|
|
11
|
+
ListenerState,
|
|
12
|
+
StrandUpdate,
|
|
13
|
+
SynchronizationUnit
|
|
14
|
+
} from '../types';
|
|
15
|
+
import { PullResponderTransmitter } from './transmitter';
|
|
16
|
+
import { SwitchboardPushTransmitter } from './transmitter/switchboard-push';
|
|
17
|
+
import { ITransmitter } from './transmitter/types';
|
|
18
|
+
|
|
19
|
+
export class ListenerManager extends BaseListenerManager {
|
|
20
|
+
async getTransmitter(
|
|
21
|
+
driveId: string,
|
|
22
|
+
listenerId: string
|
|
23
|
+
): Promise<ITransmitter | undefined> {
|
|
24
|
+
return this.transmitters[driveId]?.[listenerId];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async addListener(listener: Listener) {
|
|
28
|
+
const drive = listener.driveId;
|
|
29
|
+
|
|
30
|
+
const syncUnits = await this.drive.getSynchronizationUnits(drive);
|
|
31
|
+
const filteredSyncUnits = [];
|
|
32
|
+
for (const syncUnit of syncUnits) {
|
|
33
|
+
if (this._checkFilter(listener.filter, syncUnit)) {
|
|
34
|
+
filteredSyncUnits.push(syncUnit);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!this.listenerState.has(drive)) {
|
|
39
|
+
this.listenerState.set(drive, new Map());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const driveMap = this.listenerState.get(drive)!;
|
|
43
|
+
|
|
44
|
+
const driveDocument = await this.drive.getDrive(drive);
|
|
45
|
+
|
|
46
|
+
const lastDriveOperation = driveDocument.operations.global
|
|
47
|
+
.slice()
|
|
48
|
+
.pop();
|
|
49
|
+
|
|
50
|
+
driveMap.set(listener.listenerId, {
|
|
51
|
+
block: listener.block,
|
|
52
|
+
driveId: listener.driveId,
|
|
53
|
+
pendingTimeout: '0',
|
|
54
|
+
listener,
|
|
55
|
+
listenerStatus: 'CREATED',
|
|
56
|
+
syncUnits: [
|
|
57
|
+
{
|
|
58
|
+
syncId: '0',
|
|
59
|
+
driveId: listener.driveId,
|
|
60
|
+
documentId: '',
|
|
61
|
+
documentType: driveDocument.documentType,
|
|
62
|
+
scope: 'global',
|
|
63
|
+
branch: 'main',
|
|
64
|
+
lastUpdated:
|
|
65
|
+
lastDriveOperation?.timestamp ??
|
|
66
|
+
driveDocument.lastModified,
|
|
67
|
+
revision: lastDriveOperation?.index ?? 0,
|
|
68
|
+
listenerRev: -1,
|
|
69
|
+
syncRev: lastDriveOperation?.index ?? 0
|
|
70
|
+
}
|
|
71
|
+
].concat(
|
|
72
|
+
filteredSyncUnits.map(e => ({
|
|
73
|
+
...e,
|
|
74
|
+
listenerRev: -1,
|
|
75
|
+
syncRev: e.revision
|
|
76
|
+
}))
|
|
77
|
+
)
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let transmitter: ITransmitter | undefined;
|
|
81
|
+
|
|
82
|
+
switch (listener.callInfo?.transmitterType) {
|
|
83
|
+
case 'SwitchboardPush': {
|
|
84
|
+
transmitter = new SwitchboardPushTransmitter(
|
|
85
|
+
listener,
|
|
86
|
+
this.drive
|
|
87
|
+
);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case 'PullResponder': {
|
|
92
|
+
transmitter = new PullResponderTransmitter(
|
|
93
|
+
listener,
|
|
94
|
+
this.drive,
|
|
95
|
+
this
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!transmitter) {
|
|
101
|
+
throw new Error('Transmitter not found');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const driveTransmitters = this.transmitters[drive] || {};
|
|
105
|
+
driveTransmitters[listener.listenerId] = transmitter;
|
|
106
|
+
this.transmitters[drive] = driveTransmitters;
|
|
107
|
+
return transmitter;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async removeListener(driveId: string, listenerId: string) {
|
|
111
|
+
const driveMap = this.listenerState.get(driveId);
|
|
112
|
+
if (!driveMap) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return driveMap.delete(listenerId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async updateSynchronizationRevision(
|
|
120
|
+
driveId: string,
|
|
121
|
+
syncId: string,
|
|
122
|
+
syncRev: number,
|
|
123
|
+
lastUpdated: string
|
|
124
|
+
) {
|
|
125
|
+
const drive = this.listenerState.get(driveId);
|
|
126
|
+
if (!drive) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let newRevision = false;
|
|
131
|
+
for (const [, listener] of drive) {
|
|
132
|
+
const syncUnits = listener.syncUnits.filter(
|
|
133
|
+
e => e.syncId === syncId
|
|
134
|
+
);
|
|
135
|
+
if (listener.driveId !== driveId) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const syncUnit of syncUnits) {
|
|
140
|
+
if (syncUnit.syncId !== syncId) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
syncUnit.syncRev = syncRev;
|
|
145
|
+
syncUnit.lastUpdated = lastUpdated;
|
|
146
|
+
newRevision = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (newRevision) {
|
|
151
|
+
return this.triggerUpdate();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async addSyncUnits(syncUnits: SynchronizationUnit[]) {
|
|
156
|
+
for (const [driveId, drive] of this.listenerState) {
|
|
157
|
+
for (const [id, listenerState] of drive) {
|
|
158
|
+
const transmitter = await this.getTransmitter(driveId, id);
|
|
159
|
+
if (!transmitter) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const filteredSyncUnits = [];
|
|
163
|
+
const { listener } = listenerState;
|
|
164
|
+
for (const syncUnit of syncUnits) {
|
|
165
|
+
if (!this._checkFilter(listener.filter, syncUnit)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const existingSyncUnit = listenerState.syncUnits.find(
|
|
169
|
+
unit => unit.syncId === syncUnit.syncId
|
|
170
|
+
);
|
|
171
|
+
if (existingSyncUnit) {
|
|
172
|
+
existingSyncUnit.syncRev = syncUnit.revision;
|
|
173
|
+
existingSyncUnit.lastUpdated = syncUnit.lastUpdated;
|
|
174
|
+
} else {
|
|
175
|
+
filteredSyncUnits.push(syncUnit);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// TODO is this possible?
|
|
180
|
+
if (!this.listenerState.has(driveId)) {
|
|
181
|
+
this.listenerState.set(driveId, new Map());
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const driveMap = this.listenerState.get(driveId)!;
|
|
185
|
+
|
|
186
|
+
// TODO reuse existing state
|
|
187
|
+
driveMap.set(listener.listenerId, {
|
|
188
|
+
block: listener.block,
|
|
189
|
+
driveId: listener.driveId,
|
|
190
|
+
pendingTimeout: '0',
|
|
191
|
+
listener,
|
|
192
|
+
listenerStatus: 'CREATED',
|
|
193
|
+
syncUnits: listenerState.syncUnits.concat(
|
|
194
|
+
filteredSyncUnits.map(e => ({
|
|
195
|
+
...e,
|
|
196
|
+
listenerRev: -1,
|
|
197
|
+
syncRev: e.revision
|
|
198
|
+
}))
|
|
199
|
+
)
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async updateListenerRevision(
|
|
206
|
+
listenerId: string,
|
|
207
|
+
driveId: string,
|
|
208
|
+
syncId: string,
|
|
209
|
+
listenerRev: number
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
const drive = this.listenerState.get(driveId);
|
|
212
|
+
if (!drive) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const listener = drive.get(listenerId);
|
|
217
|
+
if (!listener) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const entry = listener.syncUnits.find(s => s.syncId === syncId);
|
|
222
|
+
if (entry) {
|
|
223
|
+
entry.listenerRev = listenerRev;
|
|
224
|
+
entry.lastUpdated = new Date().toISOString();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async triggerUpdate() {
|
|
229
|
+
for (const [driveId, drive] of this.listenerState) {
|
|
230
|
+
for (const [id, listener] of drive) {
|
|
231
|
+
const transmitter = await this.getTransmitter(driveId, id);
|
|
232
|
+
if (!transmitter) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const strandUpdates: StrandUpdate[] = [];
|
|
237
|
+
for (const unit of listener.syncUnits) {
|
|
238
|
+
const {
|
|
239
|
+
syncRev,
|
|
240
|
+
syncId,
|
|
241
|
+
listenerRev,
|
|
242
|
+
driveId,
|
|
243
|
+
documentId,
|
|
244
|
+
scope,
|
|
245
|
+
branch
|
|
246
|
+
} = unit;
|
|
247
|
+
if (listenerRev >= syncRev) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const opData = await this.drive.getOperationData(
|
|
252
|
+
driveId,
|
|
253
|
+
syncId,
|
|
254
|
+
{
|
|
255
|
+
fromRevision: listenerRev
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (!opData.length) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
strandUpdates.push({
|
|
264
|
+
driveId,
|
|
265
|
+
documentId,
|
|
266
|
+
branch,
|
|
267
|
+
operations: opData,
|
|
268
|
+
scope: scope as OperationScope
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (strandUpdates.length == 0) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
listener.pendingTimeout = new Date(
|
|
277
|
+
new Date().getTime() / 1000 + 300
|
|
278
|
+
).toISOString();
|
|
279
|
+
listener.listenerStatus = 'PENDING';
|
|
280
|
+
|
|
281
|
+
// TODO update listeners in parallel, blocking for listeners with block=true
|
|
282
|
+
try {
|
|
283
|
+
const listenerRevisions =
|
|
284
|
+
await transmitter?.transmit(strandUpdates);
|
|
285
|
+
|
|
286
|
+
listener.pendingTimeout = '0';
|
|
287
|
+
listener.listenerStatus = 'PENDING';
|
|
288
|
+
|
|
289
|
+
for (const unit of listener.syncUnits) {
|
|
290
|
+
const revision = listenerRevisions.find(
|
|
291
|
+
e =>
|
|
292
|
+
e.documentId === unit.documentId &&
|
|
293
|
+
e.scope === unit.scope &&
|
|
294
|
+
e.branch === unit.branch
|
|
295
|
+
);
|
|
296
|
+
if (revision) {
|
|
297
|
+
unit.listenerRev = revision.revision;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const revisionError = listenerRevisions.find(
|
|
301
|
+
l => l.status !== 'SUCCESS'
|
|
302
|
+
);
|
|
303
|
+
if (revisionError) {
|
|
304
|
+
throw new OperationError(
|
|
305
|
+
revisionError.status as ErrorStatus,
|
|
306
|
+
undefined
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
listener.listenerStatus = 'SUCCESS';
|
|
310
|
+
} catch (e) {
|
|
311
|
+
// TODO: Handle error based on listener params (blocking, retry, etc)
|
|
312
|
+
listener.listenerStatus =
|
|
313
|
+
e instanceof OperationError ? e.status : 'ERROR';
|
|
314
|
+
throw e;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private _checkFilter(
|
|
321
|
+
filter: ListenerFilter,
|
|
322
|
+
syncUnit: SynchronizationUnit
|
|
323
|
+
) {
|
|
324
|
+
const { branch, documentId, scope, documentType } = syncUnit;
|
|
325
|
+
// TODO: Needs to be optimized
|
|
326
|
+
if (
|
|
327
|
+
(!filter.branch ||
|
|
328
|
+
filter.branch.includes(branch) ||
|
|
329
|
+
filter.branch.includes('*')) &&
|
|
330
|
+
(!filter.documentId ||
|
|
331
|
+
filter.documentId.includes(documentId) ||
|
|
332
|
+
filter.documentId.includes('*')) &&
|
|
333
|
+
(!filter.scope ||
|
|
334
|
+
filter.scope.includes(scope) ||
|
|
335
|
+
filter.scope.includes('*')) &&
|
|
336
|
+
(!filter.documentType ||
|
|
337
|
+
filter.documentType.includes(documentType) ||
|
|
338
|
+
filter.documentType.includes('*'))
|
|
339
|
+
) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async init() {
|
|
346
|
+
const drives = await this.drive.getDrives();
|
|
347
|
+
for (const driveId of drives) {
|
|
348
|
+
const drive = await this.drive.getDrive(driveId);
|
|
349
|
+
const {
|
|
350
|
+
state: {
|
|
351
|
+
local: { listeners }
|
|
352
|
+
}
|
|
353
|
+
} = drive;
|
|
354
|
+
|
|
355
|
+
for (const listener of listeners) {
|
|
356
|
+
this.addListener({
|
|
357
|
+
block: listener.block,
|
|
358
|
+
driveId,
|
|
359
|
+
filter: {
|
|
360
|
+
branch: listener.filter.branch ?? [],
|
|
361
|
+
documentId: listener.filter.documentId ?? [],
|
|
362
|
+
documentType: listener.filter.documentType,
|
|
363
|
+
scope: listener.filter.scope ?? []
|
|
364
|
+
},
|
|
365
|
+
listenerId: listener.listenerId,
|
|
366
|
+
system: listener.system,
|
|
367
|
+
callInfo:
|
|
368
|
+
(listener.callInfo as ListenerCallInfo) ?? undefined,
|
|
369
|
+
label: listener.label ?? ''
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
getListener(driveId: string, listenerId: string): ListenerState {
|
|
376
|
+
const drive = this.listenerState.get(driveId);
|
|
377
|
+
if (!drive) throw new Error('Drive not found');
|
|
378
|
+
const listener = drive.get(listenerId);
|
|
379
|
+
if (!listener) throw new Error('Listener not found');
|
|
380
|
+
return listener;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { ListenerFilter, Trigger, z } from 'document-model-libs/document-drive';
|
|
2
|
+
import { Operation, OperationScope } from 'document-model/document';
|
|
3
|
+
import { PULL_DRIVE_INTERVAL } from '../..';
|
|
4
|
+
import { gql, requestGraphql } from '../../../utils/graphql';
|
|
5
|
+
import { OperationError } from '../../error';
|
|
6
|
+
import {
|
|
7
|
+
BaseDocumentDriveServer,
|
|
8
|
+
IOperationResult,
|
|
9
|
+
Listener,
|
|
10
|
+
ListenerRevision,
|
|
11
|
+
OperationUpdate,
|
|
12
|
+
StrandUpdate
|
|
13
|
+
} from '../../types';
|
|
14
|
+
import { ListenerManager } from '../manager';
|
|
15
|
+
import { ITransmitter, PullResponderTrigger } from './types';
|
|
16
|
+
|
|
17
|
+
export type OperationUpdateGraphQL = Omit<OperationUpdate, 'input'> & {
|
|
18
|
+
input: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type PullStrandsGraphQL = {
|
|
22
|
+
system: {
|
|
23
|
+
sync: {
|
|
24
|
+
strands: StrandUpdateGraphQL[];
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type StrandUpdateGraphQL = Omit<StrandUpdate, 'operations'> & {
|
|
30
|
+
operations: OperationUpdateGraphQL[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class PullResponderTransmitter implements ITransmitter {
|
|
34
|
+
private drive: BaseDocumentDriveServer;
|
|
35
|
+
private listener: Listener;
|
|
36
|
+
private manager: ListenerManager;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
listener: Listener,
|
|
40
|
+
drive: BaseDocumentDriveServer,
|
|
41
|
+
manager: ListenerManager
|
|
42
|
+
) {
|
|
43
|
+
this.listener = listener;
|
|
44
|
+
this.drive = drive;
|
|
45
|
+
this.manager = manager;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async transmit(): Promise<ListenerRevision[]> {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getStrands(
|
|
53
|
+
listenerId: string,
|
|
54
|
+
since?: string
|
|
55
|
+
): Promise<StrandUpdate[]> {
|
|
56
|
+
// fetch listenerState from listenerManager
|
|
57
|
+
const entries = this.manager.getListener(
|
|
58
|
+
this.listener.driveId,
|
|
59
|
+
listenerId
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// fetch operations from drive and prepare strands
|
|
63
|
+
const strands: StrandUpdate[] = [];
|
|
64
|
+
|
|
65
|
+
for (const entry of entries.syncUnits) {
|
|
66
|
+
if (entry.listenerRev >= entry.syncRev) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { documentId, driveId, scope, branch } = entry;
|
|
71
|
+
const operations = await this.drive.getOperationData(
|
|
72
|
+
entry.driveId,
|
|
73
|
+
entry.syncId,
|
|
74
|
+
{
|
|
75
|
+
since,
|
|
76
|
+
fromRevision: entry.listenerRev
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
strands.push({
|
|
80
|
+
driveId,
|
|
81
|
+
documentId,
|
|
82
|
+
scope: scope as OperationScope,
|
|
83
|
+
branch,
|
|
84
|
+
operations
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return strands;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async processAcknowledge(
|
|
92
|
+
driveId: string,
|
|
93
|
+
listenerId: string,
|
|
94
|
+
revisions: ListenerRevision[]
|
|
95
|
+
): Promise<boolean> {
|
|
96
|
+
const listener = this.manager.getListener(driveId, listenerId);
|
|
97
|
+
let success = true;
|
|
98
|
+
for (const revision of revisions) {
|
|
99
|
+
const syncId = listener.syncUnits.find(
|
|
100
|
+
s => s.scope === revision.scope && s.branch === revision.branch && s.documentId === revision.documentId && s.driveId === driveId
|
|
101
|
+
)?.syncId;
|
|
102
|
+
if (!syncId) {
|
|
103
|
+
success = false;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await this.manager.updateListenerRevision(
|
|
108
|
+
listenerId,
|
|
109
|
+
driveId,
|
|
110
|
+
syncId,
|
|
111
|
+
revision.revision
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return success;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static async registerPullResponder(
|
|
119
|
+
driveId: string,
|
|
120
|
+
url: string,
|
|
121
|
+
filter: ListenerFilter
|
|
122
|
+
): Promise<Listener['listenerId']> {
|
|
123
|
+
// graphql request to switchboard
|
|
124
|
+
const { registerPullResponderListener } = await requestGraphql<{
|
|
125
|
+
registerPullResponderListener: {
|
|
126
|
+
listenerId: Listener['listenerId'];
|
|
127
|
+
};
|
|
128
|
+
}>(
|
|
129
|
+
url,
|
|
130
|
+
gql`
|
|
131
|
+
mutation registerPullResponderListener(
|
|
132
|
+
$filter: InputListenerFilter!
|
|
133
|
+
) {
|
|
134
|
+
registerPullResponderListener(filter: $filter) {
|
|
135
|
+
listenerId
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
`,
|
|
139
|
+
{ filter }
|
|
140
|
+
);
|
|
141
|
+
return registerPullResponderListener.listenerId;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static async pullStrands(
|
|
145
|
+
driveId: string,
|
|
146
|
+
url: string,
|
|
147
|
+
listenerId: string,
|
|
148
|
+
since?: string // TODO add support for since
|
|
149
|
+
): Promise<StrandUpdate[]> {
|
|
150
|
+
const {
|
|
151
|
+
system: {
|
|
152
|
+
sync: { strands }
|
|
153
|
+
}
|
|
154
|
+
} = await requestGraphql<PullStrandsGraphQL>(
|
|
155
|
+
url,
|
|
156
|
+
gql`
|
|
157
|
+
query strands($listenerId: ID!) {
|
|
158
|
+
system {
|
|
159
|
+
sync {
|
|
160
|
+
strands(listenerId: $listenerId) {
|
|
161
|
+
driveId
|
|
162
|
+
documentId
|
|
163
|
+
scope
|
|
164
|
+
branch
|
|
165
|
+
operations {
|
|
166
|
+
timestamp
|
|
167
|
+
skip
|
|
168
|
+
type
|
|
169
|
+
input
|
|
170
|
+
hash
|
|
171
|
+
index
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
`,
|
|
178
|
+
{ listenerId }
|
|
179
|
+
);
|
|
180
|
+
return strands.map(s => ({
|
|
181
|
+
...s,
|
|
182
|
+
operations: s.operations.map(o => ({
|
|
183
|
+
...o,
|
|
184
|
+
input: JSON.parse(o.input) as object
|
|
185
|
+
}))
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static async acknowledgeStrands(
|
|
190
|
+
driveId: string,
|
|
191
|
+
url: string,
|
|
192
|
+
listenerId: string,
|
|
193
|
+
revisions: ListenerRevision[]
|
|
194
|
+
): Promise<boolean> {
|
|
195
|
+
const result = await requestGraphql<{ acknowledge: boolean }>(
|
|
196
|
+
url,
|
|
197
|
+
gql`
|
|
198
|
+
mutation acknowledge(
|
|
199
|
+
$listenerId: String!
|
|
200
|
+
$revisions: [ListenerRevisionInput]
|
|
201
|
+
) {
|
|
202
|
+
acknowledge(listenerId: $listenerId, revisions: $revisions)
|
|
203
|
+
}
|
|
204
|
+
`,
|
|
205
|
+
{ listenerId, revisions }
|
|
206
|
+
);
|
|
207
|
+
return result.acknowledge;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static setupPull(
|
|
211
|
+
driveId: string,
|
|
212
|
+
trigger: PullResponderTrigger,
|
|
213
|
+
onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
|
|
214
|
+
onError: (error: Error) => void,
|
|
215
|
+
onAcknowledge?: (success: boolean) => void
|
|
216
|
+
): number {
|
|
217
|
+
const { url, listenerId, interval } = trigger.data;
|
|
218
|
+
let loopInterval = PULL_DRIVE_INTERVAL;
|
|
219
|
+
if (interval) {
|
|
220
|
+
try {
|
|
221
|
+
const intervalNumber = parseInt(interval);
|
|
222
|
+
if (intervalNumber) {
|
|
223
|
+
loopInterval = intervalNumber;
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// ignore invalid interval
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const timeout = setInterval(async () => {
|
|
231
|
+
try {
|
|
232
|
+
const strands = await PullResponderTransmitter.pullStrands(
|
|
233
|
+
driveId,
|
|
234
|
+
url,
|
|
235
|
+
listenerId
|
|
236
|
+
// since ?
|
|
237
|
+
);
|
|
238
|
+
const listenerRevisions: ListenerRevision[] = [];
|
|
239
|
+
|
|
240
|
+
for (const strand of strands) {
|
|
241
|
+
const operations: Operation[] = strand.operations.map(
|
|
242
|
+
({ index, type, hash, input, skip, timestamp }) => ({
|
|
243
|
+
index,
|
|
244
|
+
type,
|
|
245
|
+
hash,
|
|
246
|
+
input,
|
|
247
|
+
skip,
|
|
248
|
+
timestamp,
|
|
249
|
+
scope: strand.scope,
|
|
250
|
+
branch: strand.branch
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
let error: Error | undefined = undefined;
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const result = await onStrandUpdate(strand);
|
|
258
|
+
if (result.error) {
|
|
259
|
+
throw result.error;
|
|
260
|
+
}
|
|
261
|
+
} catch (e) {
|
|
262
|
+
error = e as Error;
|
|
263
|
+
onError?.(error);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
listenerRevisions.push({
|
|
267
|
+
branch: strand.branch,
|
|
268
|
+
documentId: strand.documentId ?? '',
|
|
269
|
+
driveId: strand.driveId,
|
|
270
|
+
revision: operations.pop()?.index ?? -1,
|
|
271
|
+
scope: strand.scope as OperationScope,
|
|
272
|
+
status: error
|
|
273
|
+
? error instanceof OperationError
|
|
274
|
+
? error.status
|
|
275
|
+
: 'ERROR'
|
|
276
|
+
: 'SUCCESS'
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// TODO: Should try to parse remaining strands?
|
|
280
|
+
if (error) {
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await PullResponderTransmitter.acknowledgeStrands(
|
|
286
|
+
driveId,
|
|
287
|
+
url,
|
|
288
|
+
listenerId,
|
|
289
|
+
listenerRevisions
|
|
290
|
+
)
|
|
291
|
+
.then(result => onAcknowledge?.(result))
|
|
292
|
+
.catch(error => console.error('ACK error', error));
|
|
293
|
+
} catch (error) {
|
|
294
|
+
onError(error as Error);
|
|
295
|
+
}
|
|
296
|
+
}, loopInterval);
|
|
297
|
+
return timeout as unknown as number;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
static isPullResponderTrigger(
|
|
301
|
+
trigger: Trigger
|
|
302
|
+
): trigger is PullResponderTrigger {
|
|
303
|
+
return (
|
|
304
|
+
trigger.type === 'PullResponder' &&
|
|
305
|
+
z.PullResponderTriggerDataSchema().safeParse(trigger.data).success
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|