document-drive 0.0.30 → 1.0.0-alpha.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-drive",
3
- "version": "0.0.30",
3
+ "version": "1.0.0-alpha.1",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -24,6 +24,7 @@
24
24
  "lint": "eslint src --ext .js,.jsx,.ts,.tsx && yarn check-types",
25
25
  "lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
26
26
  "format": "prettier . --write",
27
+ "release": "semantic-release",
27
28
  "test": "vitest run --coverage",
28
29
  "test:watch": "vitest watch"
29
30
  },
@@ -43,7 +44,11 @@
43
44
  "sanitize-filename": "^1.6.3"
44
45
  },
45
46
  "devDependencies": {
47
+ "@commitlint/cli": "^18.6.1",
48
+ "@commitlint/config-conventional": "^18.6.2",
46
49
  "@prisma/client": "5.8.1",
50
+ "@semantic-release/changelog": "^6.0.3",
51
+ "@semantic-release/git": "^10.0.1",
47
52
  "@total-typescript/ts-reset": "^0.5.1",
48
53
  "@types/node": "^20.11.16",
49
54
  "@typescript-eslint/eslint-plugin": "^6.18.1",
@@ -58,6 +63,7 @@
58
63
  "msw": "^2.1.2",
59
64
  "prettier": "^3.1.1",
60
65
  "prettier-plugin-organize-imports": "^3.2.4",
66
+ "semantic-release": "^23.0.2",
61
67
  "sequelize": "^6.35.2",
62
68
  "sqlite3": "^5.1.7",
63
69
  "typescript": "^5.3.2",
@@ -138,9 +138,9 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
138
138
 
139
139
  if (!driveTriggers) {
140
140
  driveTriggers = new Map();
141
- this.updateSyncStatus(driveId, 'SYNCING');
142
141
  }
143
142
 
143
+ this.updateSyncStatus(driveId, 'SYNCING');
144
144
  if (PullResponderTransmitter.isPullResponderTrigger(trigger)) {
145
145
  const intervalId = PullResponderTransmitter.setupPull(
146
146
  driveId,
@@ -786,8 +786,14 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
786
786
  syncUnit.syncId,
787
787
  syncUnit.revision,
788
788
  syncUnit.lastUpdated,
789
+ () => this.updateSyncStatus(drive, 'SYNCING'),
789
790
  this.handleListenerError.bind(this)
790
791
  )
792
+ .then(
793
+ updates =>
794
+ updates.length &&
795
+ this.updateSyncStatus(drive, 'SUCCESS')
796
+ )
791
797
  .catch(error => {
792
798
  console.error(
793
799
  'Non handled error updating sync revision',
@@ -917,8 +923,14 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
917
923
  '0',
918
924
  lastOperation.index,
919
925
  lastOperation.timestamp,
926
+ () => this.updateSyncStatus(drive, 'SYNCING'),
920
927
  this.handleListenerError.bind(this)
921
928
  )
929
+ .then(
930
+ updates =>
931
+ updates.length &&
932
+ this.updateSyncStatus(drive, 'SUCCESS')
933
+ )
922
934
  .catch(error => {
923
935
  console.error(
924
936
  'Non handled error updating sync revision',
@@ -9,6 +9,7 @@ import {
9
9
  ErrorStatus,
10
10
  Listener,
11
11
  ListenerState,
12
+ ListenerUpdate,
12
13
  OperationUpdate,
13
14
  StrandUpdate,
14
15
  SynchronizationUnit
@@ -122,6 +123,7 @@ export class ListenerManager extends BaseListenerManager {
122
123
  syncId: string,
123
124
  syncRev: number,
124
125
  lastUpdated: string,
126
+ willUpdate?: (listeners: Listener[]) => void,
125
127
  onError?: (
126
128
  error: Error,
127
129
  driveId: string,
@@ -130,10 +132,10 @@ export class ListenerManager extends BaseListenerManager {
130
132
  ) {
131
133
  const drive = this.listenerState.get(driveId);
132
134
  if (!drive) {
133
- return;
135
+ return [];
134
136
  }
135
137
 
136
- let newRevision = false;
138
+ const outdatedListeners: Listener[] = [];
137
139
  for (const [, listener] of drive) {
138
140
  const syncUnits = listener.syncUnits.filter(
139
141
  e => e.syncId === syncId
@@ -149,13 +151,21 @@ export class ListenerManager extends BaseListenerManager {
149
151
 
150
152
  syncUnit.syncRev = syncRev;
151
153
  syncUnit.lastUpdated = lastUpdated;
152
- newRevision = true;
154
+ if (
155
+ !outdatedListeners.find(
156
+ l => l.listenerId === listener.listener.listenerId
157
+ )
158
+ ) {
159
+ outdatedListeners.push(listener.listener);
160
+ }
153
161
  }
154
162
  }
155
163
 
156
- if (newRevision) {
164
+ if (outdatedListeners.length) {
165
+ willUpdate?.(outdatedListeners);
157
166
  return this.triggerUpdate(onError);
158
167
  }
168
+ return [];
159
169
  }
160
170
 
161
171
  async addSyncUnits(syncUnits: SynchronizationUnit[]) {
@@ -251,6 +261,7 @@ export class ListenerManager extends BaseListenerManager {
251
261
  listener: ListenerState
252
262
  ) => void
253
263
  ) {
264
+ const listenerUpdates: ListenerUpdate[] = [];
254
265
  for (const [driveId, drive] of this.listenerState) {
255
266
  for (const [id, listener] of drive) {
256
267
  const transmitter = await this.getTransmitter(driveId, id);
@@ -338,6 +349,10 @@ export class ListenerManager extends BaseListenerManager {
338
349
  );
339
350
  }
340
351
  listener.listenerStatus = 'SUCCESS';
352
+ listenerUpdates.push({
353
+ listenerId: listener.listener.listenerId,
354
+ listenerRevisions
355
+ });
341
356
  } catch (e) {
342
357
  // TODO: Handle error based on listener params (blocking, retry, etc)
343
358
  onError?.(e as Error, driveId, listener);
@@ -346,6 +361,7 @@ export class ListenerManager extends BaseListenerManager {
346
361
  }
347
362
  }
348
363
  }
364
+ return listenerUpdates;
349
365
  }
350
366
 
351
367
  private _checkFilter(
@@ -218,6 +218,87 @@ export class PullResponderTransmitter implements ITransmitter {
218
218
  return result.acknowledge;
219
219
  }
220
220
 
221
+ private static async executePull(
222
+ driveId: string,
223
+ trigger: PullResponderTrigger,
224
+ onStrandUpdate: (strand: StrandUpdate) => Promise<IOperationResult>,
225
+ onError: (error: Error) => void,
226
+ onAcknowledge?: (success: boolean) => void
227
+ ) {
228
+ try {
229
+ const { url, listenerId } = trigger.data;
230
+ const strands = await PullResponderTransmitter.pullStrands(
231
+ driveId,
232
+ url,
233
+ listenerId
234
+ // since ?
235
+ );
236
+
237
+ // if there are no new strands then do nothing
238
+ if (!strands.length) {
239
+ return;
240
+ }
241
+
242
+ const listenerRevisions: ListenerRevision[] = [];
243
+
244
+ for (const strand of strands) {
245
+ const operations: Operation[] = strand.operations.map(
246
+ ({ index, type, hash, input, skip, timestamp }) => ({
247
+ index,
248
+ type,
249
+ hash,
250
+ input,
251
+ skip,
252
+ timestamp,
253
+ scope: strand.scope,
254
+ branch: strand.branch
255
+ })
256
+ );
257
+
258
+ let error: Error | undefined = undefined;
259
+
260
+ try {
261
+ const result = await onStrandUpdate(strand);
262
+ if (result.error) {
263
+ throw result.error;
264
+ }
265
+ } catch (e) {
266
+ error = e as Error;
267
+ onError(error);
268
+ }
269
+
270
+ listenerRevisions.push({
271
+ branch: strand.branch,
272
+ documentId: strand.documentId || '',
273
+ driveId: strand.driveId,
274
+ revision: operations.pop()?.index ?? -1,
275
+ scope: strand.scope as OperationScope,
276
+ status: error
277
+ ? error instanceof OperationError
278
+ ? error.status
279
+ : 'ERROR'
280
+ : 'SUCCESS'
281
+ });
282
+
283
+ // TODO: Should try to parse remaining strands?
284
+ if (error) {
285
+ break;
286
+ }
287
+ }
288
+
289
+ await PullResponderTransmitter.acknowledgeStrands(
290
+ driveId,
291
+ url,
292
+ listenerId,
293
+ listenerRevisions
294
+ )
295
+ .then(result => onAcknowledge?.(result))
296
+ .catch(error => console.error('ACK error', error));
297
+ } catch (error) {
298
+ onError(error as Error);
299
+ }
300
+ }
301
+
221
302
  static setupPull(
222
303
  driveId: string,
223
304
  trigger: PullResponderTrigger,
@@ -225,7 +306,7 @@ export class PullResponderTransmitter implements ITransmitter {
225
306
  onError: (error: Error) => void,
226
307
  onAcknowledge?: (success: boolean) => void
227
308
  ): number {
228
- const { url, listenerId, interval } = trigger.data;
309
+ const { interval } = trigger.data;
229
310
  let loopInterval = PULL_DRIVE_INTERVAL;
230
311
  if (interval) {
231
312
  try {
@@ -238,79 +319,25 @@ export class PullResponderTransmitter implements ITransmitter {
238
319
  }
239
320
  }
240
321
 
241
- const timeout = setInterval(async () => {
242
- try {
243
- const strands = await PullResponderTransmitter.pullStrands(
244
- driveId,
245
- url,
246
- listenerId
247
- // since ?
248
- );
249
-
250
- // if there are no new strands then do nothing
251
- if (!strands.length) {
252
- return;
253
- }
254
-
255
- const listenerRevisions: ListenerRevision[] = [];
256
-
257
- for (const strand of strands) {
258
- const operations: Operation[] = strand.operations.map(
259
- ({ index, type, hash, input, skip, timestamp }) => ({
260
- index,
261
- type,
262
- hash,
263
- input,
264
- skip,
265
- timestamp,
266
- scope: strand.scope,
267
- branch: strand.branch
268
- })
269
- );
270
-
271
- let error: Error | undefined = undefined;
272
-
273
- try {
274
- const result = await onStrandUpdate(strand);
275
- if (result.error) {
276
- throw result.error;
277
- }
278
- } catch (e) {
279
- error = e as Error;
280
- onError?.(error);
281
- }
282
-
283
- listenerRevisions.push({
284
- branch: strand.branch,
285
- documentId: strand.documentId ?? '',
286
- driveId: strand.driveId,
287
- revision: operations.pop()?.index ?? -1,
288
- scope: strand.scope as OperationScope,
289
- status: error
290
- ? error instanceof OperationError
291
- ? error.status
292
- : 'ERROR'
293
- : 'SUCCESS'
294
- });
295
-
296
- // TODO: Should try to parse remaining strands?
297
- if (error) {
298
- break;
299
- }
300
- }
322
+ this.executePull(
323
+ driveId,
324
+ trigger,
325
+ onStrandUpdate,
326
+ onError,
327
+ onAcknowledge
328
+ );
301
329
 
302
- await PullResponderTransmitter.acknowledgeStrands(
330
+ const timeout = setInterval(
331
+ async () =>
332
+ this.executePull(
303
333
  driveId,
304
- url,
305
- listenerId,
306
- listenerRevisions
307
- )
308
- .then(result => onAcknowledge?.(result))
309
- .catch(error => console.error('ACK error', error));
310
- } catch (error) {
311
- onError(error as Error);
312
- }
313
- }, loopInterval);
334
+ trigger,
335
+ onStrandUpdate,
336
+ onError,
337
+ onAcknowledge
338
+ ),
339
+ loopInterval
340
+ );
314
341
  return timeout as unknown as number;
315
342
  }
316
343
 
@@ -93,6 +93,11 @@ export type ListenerRevision = {
93
93
  revision: number;
94
94
  };
95
95
 
96
+ export type ListenerUpdate = {
97
+ listenerId: string;
98
+ listenerRevisions: ListenerRevision[];
99
+ };
100
+
96
101
  export type UpdateStatus = 'SUCCESS' | 'CONFLICT' | 'MISSING' | 'ERROR';
97
102
  export type ErrorStatus = Exclude<UpdateStatus, 'SUCCESS'>;
98
103
 
@@ -233,8 +238,14 @@ export abstract class BaseListenerManager {
233
238
  driveId: string,
234
239
  syncId: string,
235
240
  syncRev: number,
236
- lastUpdated: string
237
- ): Promise<void>;
241
+ lastUpdated: string,
242
+ willUpdate?: (listeners: Listener[]) => void,
243
+ onError?: (
244
+ error: Error,
245
+ driveId: string,
246
+ listener: ListenerState
247
+ ) => void
248
+ ): Promise<ListenerUpdate[]>;
238
249
 
239
250
  abstract updateListenerRevision(
240
251
  listenerId: string,
@@ -119,28 +119,6 @@ export class PrismaStorage implements IDriveStorage {
119
119
  } catch (e) {
120
120
  console.log(e);
121
121
  }
122
-
123
- await this.db.document.upsert({
124
- where: {
125
- id_driveId: {
126
- id: 'drives',
127
- driveId: id
128
- }
129
- },
130
- create: {
131
- id: 'drives',
132
- driveId: id,
133
- documentType: header.documentType,
134
- initialState: document.initialState,
135
- lastModified: header.lastModified,
136
- revision: header.revision,
137
- created: header.created
138
- },
139
- update: {
140
- lastModified: header.lastModified,
141
- revision: header.revision
142
- }
143
- });
144
122
  }
145
123
 
146
124
  async getDocuments(drive: string) {