document-drive 0.0.27 → 0.0.29

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.
@@ -0,0 +1,63 @@
1
+ import stringify from 'json-stringify-deterministic';
2
+ import { gql, requestGraphql } from '../../../utils/graphql';
3
+ import {
4
+ BaseDocumentDriveServer,
5
+ Listener,
6
+ ListenerRevision,
7
+ StrandUpdate
8
+ } from '../../types';
9
+ import { ITransmitter } from './types';
10
+
11
+ export class SwitchboardPushTransmitter implements ITransmitter {
12
+ private drive: BaseDocumentDriveServer;
13
+ private listener: Listener;
14
+ private targetURL: string;
15
+
16
+ constructor(listener: Listener, drive: BaseDocumentDriveServer) {
17
+ this.listener = listener;
18
+ this.drive = drive;
19
+ this.targetURL = listener.callInfo!.data!;
20
+ }
21
+
22
+ async transmit(strands: StrandUpdate[]): Promise<ListenerRevision[]> {
23
+ // Send Graphql mutation to switchboard
24
+ try {
25
+ const { pushUpdates } = await requestGraphql<{
26
+ pushUpdates: ListenerRevision[];
27
+ }>(
28
+ this.targetURL,
29
+ gql`
30
+ mutation pushUpdates($strands: [InputStrandUpdate!]) {
31
+ pushUpdates(strands: $strands) {
32
+ driveId
33
+ documentId
34
+ scope
35
+ branch
36
+ status
37
+ revision
38
+ }
39
+ }
40
+ `,
41
+ {
42
+ strands: strands.map(strand => ({
43
+ ...strand,
44
+ operations: strand.operations.map(op => ({
45
+ ...op,
46
+ input: stringify(op.input)
47
+ }))
48
+ }))
49
+ }
50
+ );
51
+
52
+ if (!pushUpdates) {
53
+ throw new Error("Couldn't update listener revision");
54
+ }
55
+
56
+ return pushUpdates;
57
+ } catch (e) {
58
+ console.error(e);
59
+ throw e;
60
+ }
61
+ return [];
62
+ }
63
+ }
@@ -0,0 +1,18 @@
1
+ import {
2
+ PullResponderTriggerData,
3
+ Trigger
4
+ } from 'document-model-libs/document-drive';
5
+ import { ListenerRevision, StrandUpdate } from '../..';
6
+
7
+ export interface ITransmitter {
8
+ transmit(strands: StrandUpdate[]): Promise<ListenerRevision[]>;
9
+ }
10
+
11
+ export interface InternalTransmitterService extends ITransmitter {
12
+ getName(): string;
13
+ }
14
+
15
+ export type PullResponderTrigger = Omit<Trigger, 'data' | 'type'> & {
16
+ data: PullResponderTriggerData;
17
+ type: 'PullResponder';
18
+ };
@@ -2,68 +2,254 @@ import type {
2
2
  DocumentDriveAction,
3
3
  DocumentDriveDocument,
4
4
  DocumentDriveLocalState,
5
- DocumentDriveState
5
+ DocumentDriveState,
6
+ ListenerCallInfo,
7
+ ListenerFilter
6
8
  } from 'document-model-libs/document-drive';
7
9
  import type {
8
10
  BaseAction,
11
+ CreateChildDocumentInput,
9
12
  Document,
10
13
  Operation,
14
+ OperationScope,
11
15
  Signal,
12
16
  State
13
17
  } from 'document-model/document';
18
+ import { OperationError } from './error';
19
+ import { ITransmitter } from './listener/transmitter/types';
14
20
 
15
21
  export type DriveInput = State<
16
- Omit<DocumentDriveState, '__typename' | 'nodes'>,
22
+ Omit<DocumentDriveState, '__typename' | 'id' | 'nodes'> & { id?: string },
17
23
  DocumentDriveLocalState
18
24
  >;
19
25
 
20
- export type CreateDocumentInput = {
21
- id: string;
22
- documentType: string;
23
- document?: Document;
26
+ export type RemoteDriveOptions = DocumentDriveLocalState & {
27
+ // TODO make local state optional
28
+ pullFilter?: ListenerFilter;
29
+ pullInterval?: number;
24
30
  };
25
31
 
32
+ export type CreateDocumentInput = CreateChildDocumentInput;
33
+
26
34
  export type SignalResult = {
27
35
  signal: Signal;
28
36
  result: unknown; // infer from return types on document-model
29
37
  };
30
38
 
31
39
  export type IOperationResult<T extends Document = Document> = {
32
- success: boolean;
33
- error?: Error;
40
+ status: UpdateStatus;
41
+ error?: OperationError;
34
42
  operations: Operation[];
35
43
  document: T | undefined;
36
44
  signals: SignalResult[];
37
45
  };
38
46
 
39
- export interface IDocumentDriveServer {
40
- getDrives(): Promise<string[]>;
41
- addDrive(drive: DriveInput): Promise<void>;
42
- deleteDrive(id: string): Promise<void>;
43
- getDrive(id: string): Promise<DocumentDriveDocument>;
47
+ export type SynchronizationUnit = {
48
+ syncId: string;
49
+ driveId: string;
50
+ documentId: string;
51
+ documentType: string;
52
+ scope: string;
53
+ branch: string;
54
+ lastUpdated: string;
55
+ revision: number;
56
+ };
57
+
58
+ export type Listener = {
59
+ driveId: string;
60
+ listenerId: string;
61
+ label?: string;
62
+ block: boolean;
63
+ system: boolean;
64
+ filter: ListenerFilter;
65
+ callInfo?: ListenerCallInfo;
66
+ };
67
+
68
+ export type CreateListenerInput = {
69
+ driveId: string;
70
+ label?: string;
71
+ block: boolean;
72
+ system: boolean;
73
+ filter: ListenerFilter;
74
+ callInfo?: ListenerCallInfo;
75
+ };
76
+
77
+ export enum TransmitterType {
78
+ Internal,
79
+ SwitchboardPush,
80
+ PullResponder,
81
+ SecureConnect,
82
+ MatrixConnect,
83
+ RESTWebhook
84
+ }
85
+
86
+ export type ListenerRevision = {
87
+ driveId: string;
88
+ documentId: string;
89
+ scope: string;
90
+ branch: string;
91
+ status: UpdateStatus;
92
+ revision: number;
93
+ };
94
+
95
+ export type UpdateStatus = 'SUCCESS' | 'CONFLICT' | 'MISSING' | 'ERROR';
96
+ export type ErrorStatus = Exclude<UpdateStatus, 'SUCCESS'>;
97
+
98
+ export type OperationUpdate = {
99
+ timestamp: string;
100
+ index: number;
101
+ skip: number;
102
+ type: string;
103
+ input: object;
104
+ hash: string;
105
+ };
106
+
107
+ export type StrandUpdate = {
108
+ driveId: string;
109
+ documentId: string;
110
+ scope: OperationScope;
111
+ branch: string;
112
+ operations: OperationUpdate[];
113
+ };
114
+
115
+ export type SyncStatus = 'SYNCING' | UpdateStatus;
116
+
117
+ export abstract class BaseDocumentDriveServer {
118
+ /** Public methods **/
119
+ abstract getDrives(): Promise<string[]>;
120
+ abstract addDrive(drive: DriveInput): Promise<void>;
121
+ abstract addRemoteDrive(
122
+ url: string,
123
+ options: RemoteDriveOptions
124
+ ): Promise<void>;
125
+ abstract deleteDrive(id: string): Promise<void>;
126
+ abstract getDrive(id: string): Promise<DocumentDriveDocument>;
44
127
 
45
- getDocuments: (drive: string) => Promise<string[]>;
46
- getDocument: (drive: string, id: string) => Promise<Document>;
47
- createDocument(drive: string, document: CreateDocumentInput): Promise<void>;
48
- deleteDocument(drive: string, id: string): Promise<void>;
128
+ abstract getDocuments(drive: string): Promise<string[]>;
129
+ abstract getDocument(drive: string, id: string): Promise<Document>;
49
130
 
50
- addOperation(
131
+ abstract addOperation(
51
132
  drive: string,
52
133
  id: string,
53
134
  operation: Operation
54
- ): Promise<IOperationResult>;
55
- addOperations(
135
+ ): Promise<IOperationResult<Document>>;
136
+ abstract addOperations(
56
137
  drive: string,
57
138
  id: string,
58
139
  operations: Operation[]
59
- ): Promise<IOperationResult>;
140
+ ): Promise<IOperationResult<Document>>;
60
141
 
61
- addDriveOperation(
142
+ abstract addDriveOperation(
62
143
  drive: string,
63
144
  operation: Operation<DocumentDriveAction | BaseAction>
64
145
  ): Promise<IOperationResult<DocumentDriveDocument>>;
65
- addDriveOperations(
146
+ abstract addDriveOperations(
66
147
  drive: string,
67
148
  operations: Operation<DocumentDriveAction | BaseAction>[]
68
149
  ): Promise<IOperationResult<DocumentDriveDocument>>;
150
+
151
+ abstract getSyncStatus(drive: string): SyncStatus;
152
+
153
+ /** Synchronization methods */
154
+ abstract getSynchronizationUnits(
155
+ driveId: string,
156
+ documentId?: string[],
157
+ scope?: string[],
158
+ branch?: string[]
159
+ ): Promise<SynchronizationUnit[]>;
160
+
161
+ abstract getSynchronizationUnit(
162
+ driveId: string,
163
+ syncId: string
164
+ ): Promise<SynchronizationUnit>;
165
+
166
+ abstract getOperationData(
167
+ driveId: string,
168
+ syncId: string,
169
+ filter: {
170
+ since?: string;
171
+ fromRevision?: number;
172
+ }
173
+ ): Promise<OperationUpdate[]>;
174
+
175
+ /** Internal methods **/
176
+ protected abstract createDocument(
177
+ drive: string,
178
+ document: CreateDocumentInput
179
+ ): Promise<Document>;
180
+ protected abstract deleteDocument(drive: string, id: string): Promise<void>;
181
+
182
+ abstract getTransmitter(
183
+ driveId: string,
184
+ listenerId: string
185
+ ): Promise<ITransmitter | undefined>;
186
+ }
187
+
188
+ export abstract class BaseListenerManager {
189
+ protected drive: BaseDocumentDriveServer;
190
+ protected listenerState: Map<string, Map<string, ListenerState>> =
191
+ new Map();
192
+ protected transmitters: Record<
193
+ DocumentDriveState['id'],
194
+ Record<Listener['listenerId'], ITransmitter>
195
+ > = {};
196
+
197
+ constructor(
198
+ drive: BaseDocumentDriveServer,
199
+ listenerState: Map<string, Map<string, ListenerState>> = new Map()
200
+ ) {
201
+ this.drive = drive;
202
+ this.listenerState = listenerState;
203
+ }
204
+
205
+ abstract init(): Promise<void>;
206
+ abstract addListener(listener: Listener): Promise<ITransmitter>;
207
+ abstract removeListener(
208
+ driveId: string,
209
+ listenerId: string
210
+ ): Promise<boolean>;
211
+ abstract getTransmitter(
212
+ driveId: string,
213
+ listenerId: string
214
+ ): Promise<ITransmitter | undefined>;
215
+ abstract updateSynchronizationRevision(
216
+ driveId: string,
217
+ syncId: string,
218
+ syncRev: number,
219
+ lastUpdated: string
220
+ ): Promise<void>;
221
+
222
+ abstract updateListenerRevision(
223
+ listenerId: string,
224
+ driveId: string,
225
+ syncId: string,
226
+ listenerRev: number
227
+ ): Promise<void>;
228
+ }
229
+
230
+ export type IDocumentDriveServer = Pick<
231
+ BaseDocumentDriveServer,
232
+ keyof BaseDocumentDriveServer
233
+ >;
234
+
235
+ export type ListenerStatus =
236
+ | 'CREATED'
237
+ | 'PENDING'
238
+ | 'SUCCESS'
239
+ | 'MISSING'
240
+ | 'CONFLICT'
241
+ | 'ERROR';
242
+
243
+ export interface ListenerState {
244
+ driveId: string;
245
+ block: boolean;
246
+ pendingTimeout: string;
247
+ listener: Listener;
248
+ syncUnits: SyncronizationUnitState[];
249
+ listenerStatus: ListenerStatus;
250
+ }
251
+
252
+ export interface SyncronizationUnitState extends SynchronizationUnit {
253
+ listenerRev: number;
254
+ syncRev: number;
69
255
  }
@@ -5,7 +5,7 @@ import {
5
5
  DocumentHeader,
6
6
  Operation
7
7
  } from 'document-model/document';
8
- import { mergeOperations } from '..';
8
+ import { applyUpdatedOperations, mergeOperations } from '..';
9
9
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
10
10
 
11
11
  export class BrowserStorage implements IDriveStorage {
@@ -57,7 +57,8 @@ export class BrowserStorage implements IDriveStorage {
57
57
  drive: string,
58
58
  id: string,
59
59
  operations: Operation[],
60
- header: DocumentHeader
60
+ header: DocumentHeader,
61
+ updatedOperations: Operation[] = []
61
62
  ): Promise<void> {
62
63
  const document = await this.getDocument(drive, id);
63
64
  if (!document) {
@@ -69,12 +70,17 @@ export class BrowserStorage implements IDriveStorage {
69
70
  operations
70
71
  );
71
72
 
73
+ const mergedUpdatedOperations = applyUpdatedOperations(
74
+ mergedOperations,
75
+ updatedOperations
76
+ );
77
+
72
78
  await (
73
79
  await this.db
74
80
  ).setItem(this.buildKey(drive, id), {
75
81
  ...document,
76
82
  ...header,
77
- operations: mergedOperations
83
+ operations: mergedUpdatedOperations
78
84
  });
79
85
  }
80
86
 
@@ -9,9 +9,10 @@ import {
9
9
  writeFileSync
10
10
  } from 'fs';
11
11
  import fs from 'fs/promises';
12
+ import stringify from 'json-stringify-deterministic';
12
13
  import path from 'path';
13
14
  import sanitize from 'sanitize-filename';
14
- import { mergeOperations } from '..';
15
+ import { applyUpdatedOperations, mergeOperations } from '..';
15
16
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
16
17
 
17
18
  type FSError = {
@@ -83,7 +84,6 @@ export class FilesystemStorage implements IDriveStorage {
83
84
  });
84
85
  return JSON.parse(content) as Promise<DocumentStorage>;
85
86
  } catch (error) {
86
- console.error(error);
87
87
  throw new Error(`Document with id ${id} not found`);
88
88
  }
89
89
  }
@@ -91,7 +91,7 @@ export class FilesystemStorage implements IDriveStorage {
91
91
  async createDocument(drive: string, id: string, document: DocumentStorage) {
92
92
  const documentPath = this._buildDocumentPath(drive, id);
93
93
  await ensureDir(path.dirname(documentPath));
94
- await writeFileSync(documentPath, JSON.stringify(document), {
94
+ await writeFileSync(documentPath, stringify(document), {
95
95
  encoding: 'utf-8'
96
96
  });
97
97
  }
@@ -104,7 +104,8 @@ export class FilesystemStorage implements IDriveStorage {
104
104
  drive: string,
105
105
  id: string,
106
106
  operations: Operation[],
107
- header: DocumentHeader
107
+ header: DocumentHeader,
108
+ updatedOperations: Operation[] = []
108
109
  ) {
109
110
  const document = await this.getDocument(drive, id);
110
111
  if (!document) {
@@ -116,10 +117,15 @@ export class FilesystemStorage implements IDriveStorage {
116
117
  operations
117
118
  );
118
119
 
120
+ const mergedUpdatedOperations = applyUpdatedOperations(
121
+ mergedOperations,
122
+ updatedOperations
123
+ );
124
+
119
125
  this.createDocument(drive, id, {
120
126
  ...document,
121
127
  ...header,
122
- operations: mergedOperations
128
+ operations: mergedUpdatedOperations
123
129
  });
124
130
  }
125
131
 
@@ -1,5 +1 @@
1
- export { BrowserStorage } from './browser';
2
- export { FilesystemStorage } from './filesystem';
3
- export { MemoryStorage } from './memory';
4
- export { PrismaStorage } from './prisma';
5
1
  export type * from './types';
@@ -5,7 +5,7 @@ import {
5
5
  DocumentHeader,
6
6
  Operation
7
7
  } from 'document-model/document';
8
- import { mergeOperations } from '..';
8
+ import { applyUpdatedOperations, mergeOperations } from '..';
9
9
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
10
10
 
11
11
  export class MemoryStorage implements IDriveStorage {
@@ -30,6 +30,7 @@ export class MemoryStorage implements IDriveStorage {
30
30
  if (!document) {
31
31
  throw new Error(`Document with id ${id} not found`);
32
32
  }
33
+
33
34
  return document;
34
35
  }
35
36
 
@@ -47,7 +48,8 @@ export class MemoryStorage implements IDriveStorage {
47
48
  revision,
48
49
  documentType,
49
50
  created,
50
- lastModified
51
+ lastModified,
52
+ clipboard
51
53
  } = document;
52
54
  this.documents[drive]![id] = {
53
55
  operations,
@@ -56,7 +58,8 @@ export class MemoryStorage implements IDriveStorage {
56
58
  revision,
57
59
  documentType,
58
60
  created,
59
- lastModified
61
+ lastModified,
62
+ clipboard
60
63
  };
61
64
  }
62
65
 
@@ -64,7 +67,8 @@ export class MemoryStorage implements IDriveStorage {
64
67
  drive: string,
65
68
  id: string,
66
69
  operations: Operation[],
67
- header: DocumentHeader
70
+ header: DocumentHeader,
71
+ updatedOperations: Operation[] = []
68
72
  ): Promise<void> {
69
73
  const document = await this.getDocument(drive, id);
70
74
  if (!document) {
@@ -76,10 +80,15 @@ export class MemoryStorage implements IDriveStorage {
76
80
  operations
77
81
  );
78
82
 
83
+ const mergedUpdatedOperations = applyUpdatedOperations(
84
+ mergedOperations,
85
+ updatedOperations
86
+ );
87
+
79
88
  this.documents[drive]![id] = {
80
89
  ...document,
81
90
  ...header,
82
- operations: mergedOperations
91
+ operations: mergedUpdatedOperations
83
92
  };
84
93
  }
85
94