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.
Files changed (43) hide show
  1. package/README.md +1 -0
  2. package/package.json +74 -88
  3. package/src/cache/index.ts +2 -2
  4. package/src/cache/memory.ts +22 -13
  5. package/src/cache/redis.ts +43 -16
  6. package/src/cache/types.ts +4 -4
  7. package/src/index.ts +6 -3
  8. package/src/queue/base.ts +276 -214
  9. package/src/queue/index.ts +2 -2
  10. package/src/queue/redis.ts +138 -127
  11. package/src/queue/types.ts +44 -38
  12. package/src/read-mode/errors.ts +19 -0
  13. package/src/read-mode/index.ts +125 -0
  14. package/src/read-mode/service.ts +207 -0
  15. package/src/read-mode/types.ts +108 -0
  16. package/src/server/error.ts +61 -26
  17. package/src/server/index.ts +2160 -1785
  18. package/src/server/listener/index.ts +2 -2
  19. package/src/server/listener/manager.ts +475 -437
  20. package/src/server/listener/transmitter/index.ts +4 -5
  21. package/src/server/listener/transmitter/internal.ts +77 -79
  22. package/src/server/listener/transmitter/pull-responder.ts +363 -329
  23. package/src/server/listener/transmitter/switchboard-push.ts +72 -55
  24. package/src/server/listener/transmitter/types.ts +19 -25
  25. package/src/server/types.ts +536 -349
  26. package/src/server/utils.ts +26 -27
  27. package/src/storage/base.ts +81 -0
  28. package/src/storage/browser.ts +233 -216
  29. package/src/storage/filesystem.ts +257 -256
  30. package/src/storage/index.ts +2 -1
  31. package/src/storage/memory.ts +206 -214
  32. package/src/storage/prisma.ts +575 -568
  33. package/src/storage/sequelize.ts +460 -471
  34. package/src/storage/types.ts +83 -67
  35. package/src/utils/default-drives-manager.ts +341 -0
  36. package/src/utils/document-helpers.ts +19 -18
  37. package/src/utils/graphql.ts +288 -34
  38. package/src/utils/index.ts +61 -59
  39. package/src/utils/logger.ts +39 -37
  40. package/src/utils/migrations.ts +58 -0
  41. package/src/utils/run-asap.ts +156 -0
  42. package/CHANGELOG.md +0 -818
  43. 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
- }