document-drive 1.0.0-websockets.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +74 -88
- package/src/cache/index.ts +2 -2
- package/src/cache/memory.ts +22 -13
- package/src/cache/redis.ts +43 -16
- package/src/cache/types.ts +4 -4
- package/src/index.ts +6 -3
- package/src/queue/base.ts +276 -214
- package/src/queue/index.ts +2 -2
- package/src/queue/redis.ts +138 -127
- package/src/queue/types.ts +44 -38
- package/src/read-mode/errors.ts +19 -0
- package/src/read-mode/index.ts +125 -0
- package/src/read-mode/service.ts +207 -0
- package/src/read-mode/types.ts +108 -0
- package/src/server/error.ts +61 -26
- package/src/server/index.ts +2160 -1785
- package/src/server/listener/index.ts +2 -2
- package/src/server/listener/manager.ts +475 -437
- package/src/server/listener/transmitter/index.ts +4 -5
- package/src/server/listener/transmitter/internal.ts +77 -79
- package/src/server/listener/transmitter/pull-responder.ts +363 -329
- package/src/server/listener/transmitter/switchboard-push.ts +72 -55
- package/src/server/listener/transmitter/types.ts +19 -25
- package/src/server/types.ts +536 -349
- package/src/server/utils.ts +26 -27
- package/src/storage/base.ts +81 -0
- package/src/storage/browser.ts +233 -216
- package/src/storage/filesystem.ts +257 -256
- package/src/storage/index.ts +2 -1
- package/src/storage/memory.ts +206 -214
- package/src/storage/prisma.ts +575 -568
- package/src/storage/sequelize.ts +460 -471
- package/src/storage/types.ts +83 -67
- package/src/utils/default-drives-manager.ts +341 -0
- package/src/utils/document-helpers.ts +19 -18
- package/src/utils/graphql.ts +288 -34
- package/src/utils/index.ts +61 -59
- package/src/utils/logger.ts +39 -37
- package/src/utils/migrations.ts +58 -0
- package/src/utils/run-asap.ts +156 -0
- package/CHANGELOG.md +0 -818
- package/src/server/listener/transmitter/subscription.ts +0 -364
|
@@ -1,364 +0,0 @@
|
|
|
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
|
-
}
|