document-drive 1.0.0-experimental.8 → 1.0.0-websockets
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 +148 -0
- package/package.json +15 -10
- package/src/queue/base.ts +19 -34
- package/src/queue/redis.ts +15 -25
- package/src/queue/types.ts +32 -16
- package/src/server/index.ts +773 -197
- package/src/server/listener/manager.ts +60 -22
- package/src/server/listener/transmitter/index.ts +1 -0
- package/src/server/listener/transmitter/internal.ts +5 -0
- package/src/server/listener/transmitter/pull-responder.ts +11 -15
- package/src/server/listener/transmitter/subscription.ts +364 -0
- package/src/server/listener/transmitter/types.ts +15 -0
- package/src/server/types.ts +55 -6
- package/src/storage/browser.ts +69 -5
- package/src/storage/filesystem.ts +60 -2
- package/src/storage/memory.ts +67 -4
- package/src/storage/prisma.ts +316 -81
- package/src/storage/sequelize.ts +69 -7
- package/src/storage/types.ts +19 -5
- package/src/queue/manager.ts +0 -22
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
StrandUpdate,
|
|
15
15
|
SynchronizationUnit
|
|
16
16
|
} from '../types';
|
|
17
|
-
import { PullResponderTransmitter } from './transmitter';
|
|
17
|
+
import { PullResponderTransmitter, SubscriptionTransmitter } from './transmitter';
|
|
18
18
|
import { InternalTransmitter } from './transmitter/internal';
|
|
19
19
|
import { SwitchboardPushTransmitter } from './transmitter/switchboard-push';
|
|
20
20
|
import { ITransmitter } from './transmitter/types';
|
|
@@ -53,7 +53,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
53
53
|
driveId: string,
|
|
54
54
|
listenerId: string
|
|
55
55
|
): Promise<ITransmitter | undefined> {
|
|
56
|
-
return this.transmitters[driveId]?.[listenerId];
|
|
56
|
+
return Promise.resolve(this.transmitters[driveId]?.[listenerId]);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async addListener(listener: Listener) {
|
|
@@ -93,6 +93,10 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
93
93
|
);
|
|
94
94
|
break;
|
|
95
95
|
}
|
|
96
|
+
case "Subscription": {
|
|
97
|
+
transmitter = new SubscriptionTransmitter(listener, this);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
96
100
|
case 'Internal': {
|
|
97
101
|
transmitter = new InternalTransmitter(listener, this.drive);
|
|
98
102
|
break;
|
|
@@ -106,7 +110,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
106
110
|
const driveTransmitters = this.transmitters[drive] || {};
|
|
107
111
|
driveTransmitters[listener.listenerId] = transmitter;
|
|
108
112
|
this.transmitters[drive] = driveTransmitters;
|
|
109
|
-
return transmitter;
|
|
113
|
+
return Promise.resolve(transmitter);
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
async removeListener(driveId: string, listenerId: string) {
|
|
@@ -115,10 +119,10 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
115
119
|
return false;
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
return driveMap.delete(listenerId);
|
|
122
|
+
return Promise.resolve(driveMap.delete(listenerId));
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
async removeSyncUnits(driveId: string, syncUnits: SynchronizationUnit[]) {
|
|
125
|
+
async removeSyncUnits(driveId: string, syncUnits: Pick<SynchronizationUnit, "syncId">[]) {
|
|
122
126
|
const listeners = this.listenerState.get(driveId);
|
|
123
127
|
if (!listeners) {
|
|
124
128
|
return;
|
|
@@ -128,6 +132,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
128
132
|
listener.syncUnits.delete(syncUnit.syncId);
|
|
129
133
|
}
|
|
130
134
|
}
|
|
135
|
+
return Promise.resolve();
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
async updateSynchronizationRevisions(
|
|
@@ -203,6 +208,8 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
203
208
|
} else {
|
|
204
209
|
listener.syncUnits.set(syncId, { listenerRev, lastUpdated });
|
|
205
210
|
}
|
|
211
|
+
|
|
212
|
+
return Promise.resolve();
|
|
206
213
|
}
|
|
207
214
|
|
|
208
215
|
triggerUpdate = debounce(
|
|
@@ -243,7 +250,7 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
243
250
|
|
|
244
251
|
const opData: OperationUpdate[] = [];
|
|
245
252
|
try {
|
|
246
|
-
const data = await this.drive.getOperationData(
|
|
253
|
+
const data = await this.drive.getOperationData( // TODO - join queries, DEAL WITH INVALID SYNC ID ERROR
|
|
247
254
|
driveId,
|
|
248
255
|
syncUnit.syncId,
|
|
249
256
|
{
|
|
@@ -373,6 +380,21 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
373
380
|
);
|
|
374
381
|
}
|
|
375
382
|
|
|
383
|
+
getListenerSyncUnitIds(driveId: string, listenerId: string) {
|
|
384
|
+
const listener = this.listenerState.get(driveId)?.get(listenerId);
|
|
385
|
+
if (!listener) {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
const filter = listener.listener.filter;
|
|
389
|
+
return this.drive.getSynchronizationUnitsIds(
|
|
390
|
+
driveId,
|
|
391
|
+
filter.documentId ?? ['*'],
|
|
392
|
+
filter.scope ?? ['*'],
|
|
393
|
+
filter.branch ?? ['*'],
|
|
394
|
+
filter.documentType ?? ['*']
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
376
398
|
async initDrive(drive: DocumentDriveDocument) {
|
|
377
399
|
const {
|
|
378
400
|
state: {
|
|
@@ -398,6 +420,14 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
398
420
|
}
|
|
399
421
|
}
|
|
400
422
|
|
|
423
|
+
async removeDrive(driveId: string): Promise<void> {
|
|
424
|
+
this.listenerState.delete(driveId);
|
|
425
|
+
const transmitters = this.transmitters[driveId];
|
|
426
|
+
if (transmitters) {
|
|
427
|
+
await Promise.all(Object.values(transmitters).map(t => t.disconnect?.()));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
401
431
|
getListener(driveId: string, listenerId: string): Promise<ListenerState> {
|
|
402
432
|
const drive = this.listenerState.get(driveId);
|
|
403
433
|
if (!drive) throw new Error('Drive not found');
|
|
@@ -420,32 +450,40 @@ export class ListenerManager extends BaseListenerManager {
|
|
|
420
450
|
const syncUnits = await this.getListenerSyncUnits(driveId, listenerId);
|
|
421
451
|
|
|
422
452
|
for (const syncUnit of syncUnits) {
|
|
453
|
+
if (syncUnit.revision < 0) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
423
456
|
const entry = listener.syncUnits.get(syncUnit.syncId);
|
|
424
457
|
if (entry && entry.listenerRev >= syncUnit.revision) {
|
|
425
458
|
continue;
|
|
426
459
|
}
|
|
427
460
|
|
|
428
461
|
const { documentId, driveId, scope, branch } = syncUnit;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
462
|
+
try {
|
|
463
|
+
const operations = await this.drive.getOperationData( // DEAL WITH INVALID SYNC ID ERROR
|
|
464
|
+
driveId,
|
|
465
|
+
syncUnit.syncId,
|
|
466
|
+
{
|
|
467
|
+
since,
|
|
468
|
+
fromRevision: entry?.listenerRev
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
if (!operations.length) {
|
|
473
|
+
continue;
|
|
435
474
|
}
|
|
436
|
-
);
|
|
437
475
|
|
|
438
|
-
|
|
476
|
+
strands.push({
|
|
477
|
+
driveId,
|
|
478
|
+
documentId,
|
|
479
|
+
scope: scope as OperationScope,
|
|
480
|
+
branch,
|
|
481
|
+
operations
|
|
482
|
+
});
|
|
483
|
+
} catch (error) {
|
|
484
|
+
logger.error(error);
|
|
439
485
|
continue;
|
|
440
486
|
}
|
|
441
|
-
|
|
442
|
-
strands.push({
|
|
443
|
-
driveId,
|
|
444
|
-
documentId,
|
|
445
|
-
scope: scope as OperationScope,
|
|
446
|
-
branch,
|
|
447
|
-
operations
|
|
448
|
-
});
|
|
449
487
|
}
|
|
450
488
|
|
|
451
489
|
return strands;
|
|
@@ -12,6 +12,7 @@ import { logger } from '../../../utils/logger';
|
|
|
12
12
|
|
|
13
13
|
export interface IReceiver {
|
|
14
14
|
transmit: (strands: InternalTransmitterUpdate[]) => Promise<void>;
|
|
15
|
+
disconnect: () => Promise<void>;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export type InternalTransmitterUpdate<
|
|
@@ -88,4 +89,8 @@ export class InternalTransmitter implements ITransmitter {
|
|
|
88
89
|
setReceiver(receiver: IReceiver) {
|
|
89
90
|
this.receiver = receiver;
|
|
90
91
|
}
|
|
92
|
+
|
|
93
|
+
async disconnect(): Promise<void> {
|
|
94
|
+
await this.receiver?.disconnect();
|
|
95
|
+
}
|
|
91
96
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ListenerFilter, Trigger
|
|
2
|
-
import {
|
|
1
|
+
import { ListenerFilter, Trigger } from 'document-model-libs/document-drive';
|
|
2
|
+
import { OperationScope } from 'document-model/document';
|
|
3
3
|
import { PULL_DRIVE_INTERVAL } from '../..';
|
|
4
4
|
import { generateUUID } from '../../../utils';
|
|
5
5
|
import { gql, requestGraphql } from '../../../utils/graphql';
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
StrandUpdate
|
|
17
17
|
} from '../../types';
|
|
18
18
|
import { ListenerManager } from '../manager';
|
|
19
|
-
import {
|
|
19
|
+
import { ITriggerTransmitter, PullResponderTrigger } from './types';
|
|
20
20
|
|
|
21
21
|
export type OperationUpdateGraphQL = Omit<OperationUpdate, 'input'> & {
|
|
22
22
|
input: string;
|
|
@@ -36,7 +36,7 @@ export type StrandUpdateGraphQL = Omit<StrandUpdate, 'operations'> & {
|
|
|
36
36
|
operations: OperationUpdateGraphQL[];
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
export interface IPullResponderTransmitter extends
|
|
39
|
+
export interface IPullResponderTransmitter extends ITriggerTransmitter {
|
|
40
40
|
getStrands(since?: string): Promise<StrandUpdate[]>;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -72,7 +72,7 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
72
72
|
listenerId: string,
|
|
73
73
|
revisions: ListenerRevision[]
|
|
74
74
|
): Promise<boolean> {
|
|
75
|
-
const syncUnits = await this.manager.
|
|
75
|
+
const syncUnits = await this.manager.getListenerSyncUnitIds(
|
|
76
76
|
driveId,
|
|
77
77
|
listenerId
|
|
78
78
|
);
|
|
@@ -153,6 +153,7 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
153
153
|
scope
|
|
154
154
|
branch
|
|
155
155
|
operations {
|
|
156
|
+
id
|
|
156
157
|
timestamp
|
|
157
158
|
skip
|
|
158
159
|
type
|
|
@@ -237,17 +238,10 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
237
238
|
const listenerRevisions: ListenerRevisionWithError[] = [];
|
|
238
239
|
|
|
239
240
|
for (const strand of strands) {
|
|
240
|
-
|
|
241
|
-
(op) => ({
|
|
242
|
-
...op,
|
|
243
|
-
scope: strand.scope,
|
|
244
|
-
branch: strand.branch
|
|
245
|
-
})
|
|
246
|
-
);
|
|
247
|
-
|
|
241
|
+
let result: IOperationResult | undefined = undefined;
|
|
248
242
|
let error: Error | undefined = undefined;
|
|
249
243
|
try {
|
|
250
|
-
|
|
244
|
+
result = await onStrandUpdate(strand);
|
|
251
245
|
if (result.error) {
|
|
252
246
|
throw result.error;
|
|
253
247
|
}
|
|
@@ -260,7 +254,9 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
|
|
|
260
254
|
branch: strand.branch,
|
|
261
255
|
documentId: strand.documentId || '',
|
|
262
256
|
driveId: strand.driveId,
|
|
263
|
-
revision:
|
|
257
|
+
revision:
|
|
258
|
+
result?.document?.operations[strand.scope]?.at(-1)
|
|
259
|
+
?.index ?? -1,
|
|
264
260
|
scope: strand.scope as OperationScope,
|
|
265
261
|
status: error
|
|
266
262
|
? error instanceof OperationError
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { Client, createClient } from 'graphql-ws';
|
|
2
|
+
import WebSocket from 'isomorphic-ws';
|
|
3
|
+
import { logger } from '../../../utils/logger';
|
|
4
|
+
import {
|
|
5
|
+
IOperationResult,
|
|
6
|
+
Listener,
|
|
7
|
+
ListenerRevision,
|
|
8
|
+
ListenerRevisionWithError,
|
|
9
|
+
RemoteDriveOptions,
|
|
10
|
+
StrandUpdate
|
|
11
|
+
} from '../../types';
|
|
12
|
+
import { ListenerManager } from '../manager';
|
|
13
|
+
import { ITriggerTransmitter, SubscriptionTrigger } from './types';
|
|
14
|
+
import { ListenerFilter, Trigger, z } from 'document-model-libs/document-drive';
|
|
15
|
+
import { gql, requestGraphql } from '../../../utils/graphql';
|
|
16
|
+
import { generateUUID } from '../../../utils';
|
|
17
|
+
import { OperationScope } from 'document-model/document';
|
|
18
|
+
import { OperationError } from '../../error';
|
|
19
|
+
import { StrandUpdateGraphQL } from './pull-responder';
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
export class SubscriptionTransmitter implements ITriggerTransmitter {
|
|
23
|
+
private _strands: StrandUpdate[] = [];
|
|
24
|
+
private listener: Listener;
|
|
25
|
+
private manager: ListenerManager;
|
|
26
|
+
private _init: Promise<StrandUpdate[]> | null = null;
|
|
27
|
+
private handler: ((strands: StrandUpdate[]) => void) | null = null;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
listener: Listener,
|
|
31
|
+
manager: ListenerManager,
|
|
32
|
+
) {
|
|
33
|
+
this.listener = listener;
|
|
34
|
+
this.manager = manager;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async init() {
|
|
38
|
+
if (this._init) {
|
|
39
|
+
return this._init;
|
|
40
|
+
}
|
|
41
|
+
this._init = this.#refreshStrands();
|
|
42
|
+
return this._init;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#updateStrands(strands: StrandUpdate[]): void {
|
|
46
|
+
this._strands = strands;
|
|
47
|
+
this.handler?.(strands);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async #refreshStrands() {
|
|
51
|
+
const strands = await this.manager.getStrands(
|
|
52
|
+
this.listener.driveId,
|
|
53
|
+
this.listener.listenerId
|
|
54
|
+
);
|
|
55
|
+
this.#updateStrands(strands);
|
|
56
|
+
return Promise.resolve(strands);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async * strandsGenerator(since?: number): AsyncGenerator<StrandUpdate[]> {
|
|
60
|
+
await this.init();
|
|
61
|
+
let waitHandler = null;
|
|
62
|
+
let firstTime = true;
|
|
63
|
+
// eslint-disable-next-line
|
|
64
|
+
while (true) {
|
|
65
|
+
if (waitHandler) {
|
|
66
|
+
await waitHandler;
|
|
67
|
+
}
|
|
68
|
+
waitHandler = new Promise<StrandUpdate[]>(resolve => {
|
|
69
|
+
this.handler = resolve;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// only return empty array on first call
|
|
73
|
+
if (this._strands.length || firstTime) {
|
|
74
|
+
firstTime = false;
|
|
75
|
+
// TODO add support for 'since' parameter
|
|
76
|
+
yield this._strands.slice();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async transmit(strands: StrandUpdate[]): Promise<ListenerRevision[]> {
|
|
82
|
+
// if subscription has not been initiated by
|
|
83
|
+
// the client then ignores new strands
|
|
84
|
+
if (!this._init) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
console.log("TRANSMIT", this.listener.listenerId, strands.map(s => s.operations.length))
|
|
88
|
+
this.#updateStrands([...this._strands, ...strands]);
|
|
89
|
+
return Promise.resolve([]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async processAcknowledge(
|
|
93
|
+
driveId: string,
|
|
94
|
+
listenerId: string,
|
|
95
|
+
revisions: ListenerRevision[]
|
|
96
|
+
): Promise<boolean> {
|
|
97
|
+
const syncUnits = await this.manager.getListenerSyncUnits(
|
|
98
|
+
driveId,
|
|
99
|
+
listenerId
|
|
100
|
+
);
|
|
101
|
+
let success = true;
|
|
102
|
+
let acknowledged = false;
|
|
103
|
+
for (const revision of revisions) {
|
|
104
|
+
const syncUnit = syncUnits.find(
|
|
105
|
+
s =>
|
|
106
|
+
s.scope === revision.scope &&
|
|
107
|
+
s.branch === revision.branch &&
|
|
108
|
+
s.driveId === revision.driveId &&
|
|
109
|
+
s.documentId == revision.documentId
|
|
110
|
+
);
|
|
111
|
+
if (!syncUnit) {
|
|
112
|
+
logger.warn(
|
|
113
|
+
'Unknown sync unit was acknowledged',
|
|
114
|
+
revision
|
|
115
|
+
);
|
|
116
|
+
success = false;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await this.manager.updateListenerRevision(
|
|
121
|
+
listenerId,
|
|
122
|
+
driveId,
|
|
123
|
+
syncUnit.syncId,
|
|
124
|
+
revision.revision
|
|
125
|
+
);
|
|
126
|
+
acknowledged = true;
|
|
127
|
+
}
|
|
128
|
+
if (acknowledged) {
|
|
129
|
+
await this.#refreshStrands();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return success;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
static async registerSubscription(
|
|
136
|
+
driveId: string,
|
|
137
|
+
url: string,
|
|
138
|
+
filter: ListenerFilter
|
|
139
|
+
): Promise<Listener['listenerId']> {
|
|
140
|
+
// graphql request to switchboard
|
|
141
|
+
const { registerListener } = await requestGraphql<{
|
|
142
|
+
registerListener: {
|
|
143
|
+
listenerId: Listener['listenerId'];
|
|
144
|
+
};
|
|
145
|
+
}>(
|
|
146
|
+
url,
|
|
147
|
+
gql`
|
|
148
|
+
mutation registerListener(
|
|
149
|
+
$filter: InputListenerFilter!
|
|
150
|
+
$type: TransmitterType!
|
|
151
|
+
) {
|
|
152
|
+
registerListener(filter: $filter, type: $type) {
|
|
153
|
+
listenerId
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
`,
|
|
157
|
+
{ filter, type: 'Subscription' }
|
|
158
|
+
);
|
|
159
|
+
return registerListener.listenerId;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
static async createTrigger(
|
|
163
|
+
driveId: string,
|
|
164
|
+
url: string,
|
|
165
|
+
options: Pick<RemoteDriveOptions, 'pullFilter'>
|
|
166
|
+
): Promise<Trigger> {
|
|
167
|
+
const { pullFilter } = options;
|
|
168
|
+
const listenerId = await SubscriptionTransmitter.registerSubscription(
|
|
169
|
+
driveId,
|
|
170
|
+
url,
|
|
171
|
+
pullFilter ?? {
|
|
172
|
+
documentId: ['*'],
|
|
173
|
+
documentType: ['*'],
|
|
174
|
+
branch: ['*'],
|
|
175
|
+
scope: ['*']
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const trigger: Trigger = {
|
|
180
|
+
id: generateUUID(),
|
|
181
|
+
type: 'Subscription',
|
|
182
|
+
data: {
|
|
183
|
+
url,
|
|
184
|
+
listenerId,
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
return trigger;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
static isTrigger(
|
|
191
|
+
trigger: Trigger
|
|
192
|
+
): trigger is SubscriptionTrigger {
|
|
193
|
+
return (
|
|
194
|
+
trigger.type === 'Subscription' &&
|
|
195
|
+
z.SubscriptionTriggerDataSchema().safeParse(trigger.data).success
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static setup(driveId: string,
|
|
200
|
+
trigger: SubscriptionTrigger,
|
|
201
|
+
onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
|
|
202
|
+
onError: (error: Error) => void,
|
|
203
|
+
onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
|
|
204
|
+
onAcknowledge?: (success: boolean) => void) {
|
|
205
|
+
|
|
206
|
+
const { url } = trigger.data;
|
|
207
|
+
let subscriptionUrl = url.replace("http", "ws")
|
|
208
|
+
subscriptionUrl += subscriptionUrl.endsWith("/") ? "ws" : "/ws";
|
|
209
|
+
|
|
210
|
+
const client = createClient({
|
|
211
|
+
url: subscriptionUrl,
|
|
212
|
+
webSocketImpl: WebSocket,
|
|
213
|
+
});
|
|
214
|
+
try {
|
|
215
|
+
SubscriptionTransmitter.subscribeStrands(client, trigger, onStrandUpdate, onError, onRevisions, onAcknowledge).catch(onError);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
onError(error as Error);
|
|
218
|
+
}
|
|
219
|
+
return () => { return client.dispose(); };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private static async subscribeStrands(client: Client, trigger: SubscriptionTrigger,
|
|
223
|
+
onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
|
|
224
|
+
onError: (error: Error) => void,
|
|
225
|
+
onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
|
|
226
|
+
onAcknowledge?: (success: boolean) => void) {
|
|
227
|
+
const { listenerId } = trigger.data;
|
|
228
|
+
const subscription = client.iterate<{ subscribeStrands: StrandUpdateGraphQL[] }>({
|
|
229
|
+
query: `
|
|
230
|
+
subscription($listenerId: ID) {
|
|
231
|
+
subscribeStrands(listenerId: $listenerId) {
|
|
232
|
+
branch
|
|
233
|
+
documentId
|
|
234
|
+
driveId
|
|
235
|
+
operations {
|
|
236
|
+
timestamp
|
|
237
|
+
skip
|
|
238
|
+
type
|
|
239
|
+
input
|
|
240
|
+
hash
|
|
241
|
+
index
|
|
242
|
+
context {
|
|
243
|
+
signer {
|
|
244
|
+
user {
|
|
245
|
+
address
|
|
246
|
+
networkId
|
|
247
|
+
chainId
|
|
248
|
+
}
|
|
249
|
+
app {
|
|
250
|
+
name
|
|
251
|
+
key
|
|
252
|
+
}
|
|
253
|
+
signature
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
scope
|
|
258
|
+
}
|
|
259
|
+
}`,
|
|
260
|
+
variables: {
|
|
261
|
+
listenerId,
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
for await (const { errors, data } of subscription) {
|
|
265
|
+
const error = errors?.at(0);
|
|
266
|
+
if (error) {
|
|
267
|
+
onError(error);
|
|
268
|
+
} else {
|
|
269
|
+
const strands = data?.subscribeStrands ?? [];
|
|
270
|
+
console.log(listenerId, "Save strands", strands);
|
|
271
|
+
SubscriptionTransmitter.saveStrands(trigger, client, strands, onStrandUpdate, onError, onRevisions, onAcknowledge).catch(onError);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
static async saveStrands(
|
|
277
|
+
trigger: SubscriptionTrigger,
|
|
278
|
+
client: Client,
|
|
279
|
+
strandsQL: StrandUpdateGraphQL[],
|
|
280
|
+
onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
|
|
281
|
+
onError: (error: Error) => void,
|
|
282
|
+
onRevisions?: (revisions: ListenerRevisionWithError[]) => void,
|
|
283
|
+
onAcknowledge?: (success: boolean) => void) {
|
|
284
|
+
const strands = strandsQL.map(s => ({
|
|
285
|
+
...s,
|
|
286
|
+
operations: s.operations.map(o => ({
|
|
287
|
+
...o,
|
|
288
|
+
scope: s.scope,
|
|
289
|
+
branch: s.branch,
|
|
290
|
+
input: JSON.parse(o.input) as object
|
|
291
|
+
}))
|
|
292
|
+
}));
|
|
293
|
+
// if there are no new strands then do nothing
|
|
294
|
+
if (!strands.length) {
|
|
295
|
+
onRevisions?.([]);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const listenerRevisions: ListenerRevisionWithError[] = [];
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
for (const strand of strands) {
|
|
303
|
+
let error: Error | undefined = undefined;
|
|
304
|
+
let result: IOperationResult | undefined = undefined;
|
|
305
|
+
try {
|
|
306
|
+
result = await onStrandUpdate(strand);
|
|
307
|
+
if (result.error) {
|
|
308
|
+
throw result.error;
|
|
309
|
+
}
|
|
310
|
+
} catch (e) {
|
|
311
|
+
error = e as Error;
|
|
312
|
+
onError(error);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
listenerRevisions.push({
|
|
316
|
+
branch: strand.branch,
|
|
317
|
+
documentId: strand.documentId || '',
|
|
318
|
+
driveId: strand.driveId,
|
|
319
|
+
revision: result?.document?.operations[strand.scope]?.at(-1)?.index ?? -1,
|
|
320
|
+
scope: strand.scope as OperationScope,
|
|
321
|
+
status: error
|
|
322
|
+
? error instanceof OperationError
|
|
323
|
+
? error.status
|
|
324
|
+
: 'ERROR'
|
|
325
|
+
: 'SUCCESS',
|
|
326
|
+
error
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
onRevisions?.(listenerRevisions);
|
|
331
|
+
|
|
332
|
+
await SubscriptionTransmitter.acknowledgeStrands(
|
|
333
|
+
client,
|
|
334
|
+
trigger.data.listenerId,
|
|
335
|
+
listenerRevisions.map(revision => {
|
|
336
|
+
const { error, ...rest } = revision;
|
|
337
|
+
return rest;
|
|
338
|
+
})
|
|
339
|
+
)
|
|
340
|
+
.then(result => onAcknowledge?.(result))
|
|
341
|
+
.catch(error => logger.error('ACK error', error));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
static async acknowledgeStrands(
|
|
345
|
+
client: Client,
|
|
346
|
+
listenerId: string,
|
|
347
|
+
revisions: ListenerRevision[]
|
|
348
|
+
): Promise<boolean> {
|
|
349
|
+
const subscription = client.iterate<{ acknowledge: boolean }>({
|
|
350
|
+
query: `
|
|
351
|
+
mutation acknowledge(
|
|
352
|
+
$listenerId: String!
|
|
353
|
+
$revisions: [ListenerRevisionInput]
|
|
354
|
+
) {
|
|
355
|
+
acknowledge(listenerId: $listenerId, revisions: $revisions)
|
|
356
|
+
}
|
|
357
|
+
`,
|
|
358
|
+
variables: { listenerId, revisions }
|
|
359
|
+
});
|
|
360
|
+
const result = await subscription.next();
|
|
361
|
+
|
|
362
|
+
return (result.value as { acknowledge: boolean }).acknowledge as boolean;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PullResponderTriggerData,
|
|
3
|
+
SubscriptionTriggerData,
|
|
3
4
|
Trigger
|
|
4
5
|
} from 'document-model-libs/document-drive';
|
|
5
6
|
import { ListenerRevision, StrandUpdate } from '../..';
|
|
6
7
|
|
|
7
8
|
export interface ITransmitter {
|
|
8
9
|
transmit(strands: StrandUpdate[]): Promise<ListenerRevision[]>;
|
|
10
|
+
disconnect?(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ITriggerTransmitter extends ITransmitter {
|
|
14
|
+
processAcknowledge(
|
|
15
|
+
driveId: string,
|
|
16
|
+
listenerId: string,
|
|
17
|
+
revisions: ListenerRevision[]
|
|
18
|
+
): Promise<boolean>
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
export interface InternalTransmitterService extends ITransmitter {
|
|
@@ -16,3 +26,8 @@ export type PullResponderTrigger = Omit<Trigger, 'data' | 'type'> & {
|
|
|
16
26
|
data: PullResponderTriggerData;
|
|
17
27
|
type: 'PullResponder';
|
|
18
28
|
};
|
|
29
|
+
|
|
30
|
+
export type SubscriptionTrigger = Omit<Trigger, 'data' | 'type'> & {
|
|
31
|
+
data: SubscriptionTriggerData;
|
|
32
|
+
type: 'Subscription';
|
|
33
|
+
};
|