document-drive 1.0.0-alpha.65 → 1.0.0-alpha.67

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": "1.0.0-alpha.65",
3
+ "version": "1.0.0-alpha.67",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -34,8 +34,8 @@
34
34
  "test:watch": "vitest watch"
35
35
  },
36
36
  "peerDependencies": {
37
- "document-model": "1.3.0",
38
- "document-model-libs": "^1.53.1"
37
+ "document-model": "^1.4.0",
38
+ "document-model-libs": "^1.57.0"
39
39
  },
40
40
  "optionalDependencies": {
41
41
  "@prisma/client": "5.14.0",
@@ -65,8 +65,8 @@
65
65
  "@typescript-eslint/eslint-plugin": "^6.21.0",
66
66
  "@typescript-eslint/parser": "^6.21.0",
67
67
  "@vitest/coverage-v8": "^1.4.0",
68
- "document-model": "^1.3.0",
69
- "document-model-libs": "^1.53.1",
68
+ "document-model": "^1.4.0",
69
+ "document-model-libs": "^1.57.0",
70
70
  "eslint": "^8.57.0",
71
71
  "eslint-config-prettier": "^9.1.0",
72
72
  "fake-indexeddb": "^5.0.2",
@@ -79,6 +79,7 @@
79
79
  "sequelize": "^6.37.2",
80
80
  "sqlite3": "^5.1.7",
81
81
  "typescript": "^5.4.4",
82
- "vitest": "^1.5.0"
83
- }
82
+ "vitest": "^1.6.0"
83
+ },
84
+ "packageManager": "pnpm@9.1.4+sha256.30a1801ac4e723779efed13a21f4c39f9eb6c9fbb4ced101bce06b422593d7c9"
84
85
  }
@@ -103,6 +103,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
103
103
  this.storage = storage;
104
104
  this.cache = cache;
105
105
  this.queueManager = queueManager;
106
+
107
+ this.storage.setStorageDelegate?.({
108
+ getCachedOperations: async (drive, id) => {
109
+ try {
110
+ const document = await this.cache.getDocument(drive, id);
111
+ return document?.operations;
112
+ } catch (error) {
113
+ logger.error(error);
114
+ return undefined;
115
+ }
116
+ }
117
+ })
106
118
  }
107
119
 
108
120
  private updateSyncStatus(
@@ -21,7 +21,7 @@ import type {
21
21
  import { IBackOffOptions, backOff } from 'exponential-backoff';
22
22
  import { ConflictOperationError } from '../server/error';
23
23
  import { logger } from '../utils/logger';
24
- import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
24
+ import { DocumentDriveStorage, DocumentStorage, IDriveStorage, IStorageDelegate } from './types';
25
25
 
26
26
  type Transaction =
27
27
  | Omit<
@@ -90,6 +90,7 @@ type ExtendedPrismaClient = ReturnType<
90
90
 
91
91
  export class PrismaStorage implements IDriveStorage {
92
92
  private db: ExtendedPrismaClient;
93
+ private delegate: IStorageDelegate | undefined;
93
94
 
94
95
  constructor(db: PrismaClient, options?: PrismaStorageOptions) {
95
96
  const backOffOptions = options?.transactionRetryBackoff;
@@ -99,6 +100,10 @@ export class PrismaStorage implements IDriveStorage {
99
100
  });
100
101
  }
101
102
 
103
+ setStorageDelegate(delegate: IStorageDelegate): void {
104
+ this.delegate = delegate;
105
+ }
106
+
102
107
  async createDrive(id: string, drive: DocumentDriveStorage): Promise<void> {
103
108
  // drive for all drive documents
104
109
  await this.createDocument('drives', id, drive as DocumentStorage);
@@ -362,12 +367,26 @@ export class PrismaStorage implements IDriveStorage {
362
367
  throw new Error(`Document with id ${id} not found`);
363
368
  }
364
369
 
370
+ const cachedOperations = await this.delegate?.getCachedOperations(driveId, id) ?? {
371
+ global: [],
372
+ local: []
373
+ };
374
+ const scopeIndex = Object.keys(cachedOperations).reduceRight<Record<OperationScope, number>>((acc, value) => {
375
+ const scope = value as OperationScope;
376
+ const lastIndex = cachedOperations[scope]?.at(-1)?.index ?? -1;
377
+ acc[scope] = lastIndex;
378
+ return acc;
379
+ }, { global: -1, local: -1 });
380
+
381
+ const conditions = Object.entries(scopeIndex).map(([scope, index]) => `("scope" = '${scope}' AND "index" > ${index})`);
382
+ conditions.push(`("scope" NOT IN (${Object.keys(cachedOperations).map(s => `'${s}'`).join(", ")}))`);
383
+
365
384
  // retrieves operations with resulting state
366
385
  // for the last operation of each scope
367
- const operations = await prisma.$queryRaw<
386
+ // TODO prevent SQL injection
387
+ const queryOperations = await prisma.$queryRawUnsafe<
368
388
  Prisma.$OperationPayload['scalars'][]
369
- >`
370
- WITH ranked_operations AS (
389
+ >(`WITH ranked_operations AS (
371
390
  SELECT
372
391
  *,
373
392
  ROW_NUMBER() OVER (PARTITION BY scope ORDER BY index DESC) AS rn
@@ -390,21 +409,23 @@ export class PrismaStorage implements IDriveStorage {
390
409
  ELSE NULL
391
410
  END AS "resultingState"
392
411
  FROM ranked_operations
393
- WHERE "driveId" = ${driveId} AND "documentId" = ${id}
412
+ WHERE "driveId" = $1 AND "documentId" = $2
413
+ AND (${conditions.join(' OR ')})
394
414
  ORDER BY scope, index;
395
- `;
396
-
397
- const operationIds = operations.map(o => o.id);
415
+ `, driveId, id);
416
+ const operationIds = queryOperations.map(o => o.id)
398
417
  const attachments = await prisma.attachment.findMany({
399
418
  where: {
400
419
  operationId: {
401
420
  in: operationIds
402
421
  }
403
- }
422
+ },
404
423
  });
405
424
 
425
+ // TODO add attachments from cached operations
406
426
  const fileRegistry: FileRegistry = {};
407
- const operationsByScope = operations.reduce<DocumentOperations<Action>>(
427
+
428
+ const operationsByScope = queryOperations.reduce<DocumentOperations<Action>>(
408
429
  (acc, operation) => {
409
430
  const scope = operation.scope as OperationScope;
410
431
  if (!acc[scope]) {
@@ -420,10 +441,7 @@ export class PrismaStorage implements IDriveStorage {
420
441
  acc[scope].push(result);
421
442
  return acc;
422
443
  },
423
- {
424
- global: [],
425
- local: []
426
- }
444
+ cachedOperations
427
445
  );
428
446
 
429
447
  const dbDoc = result;
@@ -445,9 +463,6 @@ export class PrismaStorage implements IDriveStorage {
445
463
  >,
446
464
  attachments: {}
447
465
  };
448
-
449
-
450
-
451
466
  return doc;
452
467
  }
453
468
 
@@ -3,12 +3,12 @@ import type {
3
3
  DocumentDriveDocument,
4
4
  } from 'document-model-libs/document-drive';
5
5
  import type {
6
+ Action,
6
7
  BaseAction,
7
8
  Document,
8
9
  DocumentHeader,
9
- ExtendedState,
10
+ DocumentOperations,
10
11
  Operation,
11
- State
12
12
  } from 'document-model/document';
13
13
 
14
14
  export type DocumentStorage<D extends Document = Document> = Omit<
@@ -18,6 +18,10 @@ export type DocumentStorage<D extends Document = Document> = Omit<
18
18
 
19
19
  export type DocumentDriveStorage = DocumentStorage<DocumentDriveDocument>;
20
20
 
21
+ export interface IStorageDelegate {
22
+ getCachedOperations(drive: string, id: string): Promise<DocumentOperations<Action> | undefined>;
23
+ }
24
+
21
25
  export interface IStorage {
22
26
  checkDocumentExists(drive: string, id: string): Promise<boolean>;
23
27
  getDocuments: (drive: string) => Promise<string[]>;
@@ -43,8 +47,8 @@ export interface IStorage {
43
47
  ): Promise<void>;
44
48
  deleteDocument(drive: string, id: string): Promise<void>;
45
49
  getOperationResultingState?(drive: string, id: string, index: number, scope: string, branch: string): Promise<unknown>;
50
+ setStorageDelegate?(delegate: IStorageDelegate): void;
46
51
  }
47
-
48
52
  export interface IDriveStorage extends IStorage {
49
53
  getDrives(): Promise<string[]>;
50
54
  getDrive(id: string): Promise<DocumentDriveStorage>;