document-drive 1.0.0-websockets.1 → 1.0.0

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,133 +1,144 @@
1
1
  import { RedisClientType } from "redis";
2
- import { IJob, IQueue, IQueueManager, IServerDelegate, OperationJob } from "./types";
3
2
  import { BaseQueueManager } from "./base";
3
+ import {
4
+ IJob,
5
+ IQueue,
6
+ IQueueManager,
7
+ IServerDelegate,
8
+ OperationJob,
9
+ } from "./types";
4
10
 
5
11
  export class RedisQueue<T, R> implements IQueue<T, R> {
6
- private id: string;
7
- private client: RedisClientType;
8
-
9
- constructor(id: string, client: RedisClientType) {
10
- this.client = client;
11
- this.id = id;
12
- this.client.hSet("queues", id, "true");
13
- this.client.hSet(this.id, "blocked", "false");
14
- }
15
-
16
- async addJob(data: any) {
17
- await this.client.lPush(this.id + "-jobs", JSON.stringify(data));
18
- }
19
-
20
- async getNextJob() {
21
- const job = await this.client.rPop(this.id + "-jobs");
22
- if (!job) {
23
- return undefined;
24
- }
25
- return JSON.parse(job) as IJob<T>;
26
- }
27
-
28
- async amountOfJobs() {
29
- return this.client.lLen(this.id + "-jobs");
30
- }
31
-
32
- async setBlocked(blocked: boolean) {
33
- if (blocked) {
34
- await this.client.hSet(this.id, "blocked", "true");
35
- } else {
36
- await this.client.hSet(this.id, "blocked", "false");
37
- }
38
- }
39
-
40
- async isBlocked() {
41
- const blockedResult = await this.client.hGet(this.id, "blocked");
42
- if (blockedResult === "true") {
43
- return true;
44
- }
45
-
46
- return false;
47
- }
48
-
49
- getId() {
50
- return this.id;
51
- }
52
-
53
- async getJobs() {
54
- const entries = await this.client.lRange(this.id + "-jobs", 0, -1)
55
- return entries.map(e => JSON.parse(e) as IJob<T>);
56
- }
57
-
58
- async addDependencies(job: IJob<OperationJob>) {
59
- if (await this.hasDependency(job)) {
60
- return;
61
- }
62
- await this.client.lPush(this.id + "-deps", JSON.stringify(job));
63
- await this.setBlocked(true);
64
- }
65
-
66
- async hasDependency(job: IJob<OperationJob>) {
67
- const deps = await this.client.lRange(this.id + "-deps", 0, -1);
68
- return deps.some(d => d === JSON.stringify(job));
69
- }
70
-
71
- async removeDependencies(job: IJob<OperationJob>) {
72
- await this.client.lRem(this.id + "-deps", 1, JSON.stringify(job));
73
- const allDeps = await this.client.lLen(this.id + "-deps");
74
- if (allDeps > 0) {
75
- await this.setBlocked(true);
76
- } else {
77
- await this.setBlocked(false);
78
- }
79
- }
80
-
81
- async isDeleted() {
82
- const active = await this.client.hGet("queues", this.id);
83
- return active === "false";
84
- }
85
-
86
- async setDeleted(deleted: boolean) {
87
- if (deleted) {
88
- await this.client.hSet("queues", this.id, "false");
89
- } else {
90
- await this.client.hSet("queues", this.id, "true");
91
- }
92
- }
12
+ private id: string;
13
+ private client: RedisClientType;
14
+
15
+ constructor(id: string, client: RedisClientType) {
16
+ this.client = client;
17
+ this.id = id;
18
+ this.client.hSet("queues", id, "true");
19
+ this.client.hSet(this.id, "blocked", "false");
20
+ }
21
+
22
+ async addJob(data: any) {
23
+ await this.client.lPush(this.id + "-jobs", JSON.stringify(data));
24
+ }
25
+
26
+ async getNextJob() {
27
+ const job = await this.client.rPop(this.id + "-jobs");
28
+ if (!job) {
29
+ return undefined;
30
+ }
31
+ return JSON.parse(job) as IJob<T>;
32
+ }
33
+
34
+ async amountOfJobs() {
35
+ return this.client.lLen(this.id + "-jobs");
36
+ }
37
+
38
+ async setBlocked(blocked: boolean) {
39
+ if (blocked) {
40
+ await this.client.hSet(this.id, "blocked", "true");
41
+ } else {
42
+ await this.client.hSet(this.id, "blocked", "false");
43
+ }
44
+ }
45
+
46
+ async isBlocked() {
47
+ const blockedResult = await this.client.hGet(this.id, "blocked");
48
+ if (blockedResult === "true") {
49
+ return true;
50
+ }
51
+
52
+ return false;
53
+ }
54
+
55
+ getId() {
56
+ return this.id;
57
+ }
58
+
59
+ async getJobs() {
60
+ const entries = await this.client.lRange(this.id + "-jobs", 0, -1);
61
+ return entries.map((e) => JSON.parse(e) as IJob<T>);
62
+ }
63
+
64
+ async addDependencies(job: IJob<OperationJob>) {
65
+ if (await this.hasDependency(job)) {
66
+ return;
67
+ }
68
+ await this.client.lPush(this.id + "-deps", JSON.stringify(job));
69
+ await this.setBlocked(true);
70
+ }
71
+
72
+ async hasDependency(job: IJob<OperationJob>) {
73
+ const deps = await this.client.lRange(this.id + "-deps", 0, -1);
74
+ return deps.some((d) => d === JSON.stringify(job));
75
+ }
76
+
77
+ async removeDependencies(job: IJob<OperationJob>) {
78
+ await this.client.lRem(this.id + "-deps", 1, JSON.stringify(job));
79
+ const allDeps = await this.client.lLen(this.id + "-deps");
80
+ if (allDeps > 0) {
81
+ await this.setBlocked(true);
82
+ } else {
83
+ await this.setBlocked(false);
84
+ }
85
+ }
86
+
87
+ async isDeleted() {
88
+ const active = await this.client.hGet("queues", this.id);
89
+ return active === "false";
90
+ }
91
+
92
+ async setDeleted(deleted: boolean) {
93
+ if (deleted) {
94
+ await this.client.hSet("queues", this.id, "false");
95
+ } else {
96
+ await this.client.hSet("queues", this.id, "true");
97
+ }
98
+ }
93
99
  }
94
100
 
95
- export class RedisQueueManager extends BaseQueueManager implements IQueueManager {
96
-
97
- private client: RedisClientType;
98
-
99
- constructor(workers = 3, timeout = 0, client: RedisClientType) {
100
- super(workers, timeout);
101
- this.client = client;
102
- }
103
-
104
- async init(delegate: IServerDelegate, onError: (error: Error) => void): Promise<void> {
105
- await super.init(delegate, onError);
106
- const queues = await this.client.hGetAll("queues");
107
- for (const queueId in queues) {
108
- const active = await this.client.hGet("queues", queueId);
109
- if (active === "true") {
110
- this.queues.push(new RedisQueue(queueId, this.client));
111
- }
112
- }
113
- }
114
-
115
- getQueue(driveId: string, documentId?: string) {
116
- const queueId = this.getQueueId(driveId, documentId);
117
- let queue = this.queues.find((q) => q.getId() === queueId);
118
-
119
- if (!queue) {
120
- queue = new RedisQueue(queueId, this.client);
121
- this.queues.push(queue);
122
- }
123
-
124
- return queue;
125
- }
126
-
127
- removeQueue(driveId: string, documentId?: string | undefined): void {
128
- super.removeQueue(driveId, documentId);
129
-
130
- const queueId = this.getQueueId(driveId, documentId);
131
- this.client.hDel("queues", queueId);
132
- }
133
- }
101
+ export class RedisQueueManager
102
+ extends BaseQueueManager
103
+ implements IQueueManager
104
+ {
105
+ private client: RedisClientType;
106
+
107
+ constructor(workers = 3, timeout = 0, client: RedisClientType) {
108
+ super(workers, timeout);
109
+ this.client = client;
110
+ }
111
+
112
+ async init(
113
+ delegate: IServerDelegate,
114
+ onError: (error: Error) => void,
115
+ ): Promise<void> {
116
+ await super.init(delegate, onError);
117
+ const queues = await this.client.hGetAll("queues");
118
+ for (const queueId in queues) {
119
+ const active = await this.client.hGet("queues", queueId);
120
+ if (active === "true") {
121
+ this.queues.push(new RedisQueue(queueId, this.client));
122
+ }
123
+ }
124
+ }
125
+
126
+ getQueue(driveId: string, documentId?: string) {
127
+ const queueId = this.getQueueId(driveId, documentId);
128
+ let queue = this.queues.find((q) => q.getId() === queueId);
129
+
130
+ if (!queue) {
131
+ queue = new RedisQueue(queueId, this.client);
132
+ this.queues.push(queue);
133
+ }
134
+
135
+ return queue;
136
+ }
137
+
138
+ removeQueue(driveId: string, documentId?: string | undefined): void {
139
+ super.removeQueue(driveId, documentId);
140
+
141
+ const queueId = this.getQueueId(driveId, documentId);
142
+ this.client.hDel("queues", queueId);
143
+ }
144
+ }
@@ -1,20 +1,20 @@
1
1
  import { Action, Operation } from "document-model/document";
2
- import { IOperationResult } from "../server";
3
2
  import type { Unsubscribe } from "nanoevents";
3
+ import { AddOperationOptions, IOperationResult } from "../server";
4
4
 
5
5
  export interface BaseJob {
6
- driveId: string;
7
- documentId?: string
8
- actions?: Action[]
9
- forceSync?: boolean
6
+ driveId: string;
7
+ documentId?: string;
8
+ actions?: Action[];
9
+ options?: AddOperationOptions;
10
10
  }
11
11
 
12
12
  export interface OperationJob extends BaseJob {
13
- operations: Operation[]
13
+ operations: Operation[];
14
14
  }
15
15
 
16
16
  export interface ActionJob extends BaseJob {
17
- actions: Action[]
17
+ actions: Action[];
18
18
  }
19
19
 
20
20
  export type Job = OperationJob | ActionJob;
@@ -22,52 +22,58 @@ export type Job = OperationJob | ActionJob;
22
22
  export type JobId = string;
23
23
 
24
24
  export interface QueueEvents {
25
- jobCompleted: (job: IJob<Job>, result: IOperationResult) => void;
26
- jobFailed: (job: IJob<Job>, error: Error) => void;
27
- queueRemoved: (queueId: string) => void;
25
+ jobCompleted: (job: IJob<Job>, result: IOperationResult) => void;
26
+ jobFailed: (job: IJob<Job>, error: Error) => void;
27
+ queueRemoved: (queueId: string) => void;
28
28
  }
29
29
 
30
30
  export interface IServerDelegate {
31
- checkDocumentExists: (driveId: string, documentId: string) => Promise<boolean>;
32
- processJob: (job: Job) => Promise<IOperationResult>;
33
- };
31
+ checkDocumentExists: (
32
+ driveId: string,
33
+ documentId: string,
34
+ ) => Promise<boolean>;
35
+ processJob: (job: Job) => Promise<IOperationResult>;
36
+ }
34
37
 
35
38
  export interface IQueueManager {
36
- addJob(job: Job): Promise<JobId>;
37
- getQueue(driveId: string, document?: string): IQueue<Job, IOperationResult>;
38
- removeQueue(driveId: string, documentId?: string): void;
39
- getQueueByIndex(index: number): IQueue<Job, IOperationResult> | null;
40
- getQueues(): string[];
41
- init(delegate: IServerDelegate, onError: (error: Error) => void): Promise<void>;
42
- on<K extends keyof QueueEvents>(
43
- this: this,
44
- event: K,
45
- cb: QueueEvents[K]
46
- ): Unsubscribe;
39
+ addJob(job: Job): Promise<JobId>;
40
+ getQueue(driveId: string, document?: string): IQueue<Job, IOperationResult>;
41
+ removeQueue(driveId: string, documentId?: string): void;
42
+ getQueueByIndex(index: number): IQueue<Job, IOperationResult> | null;
43
+ getQueues(): string[];
44
+ init(
45
+ delegate: IServerDelegate,
46
+ onError: (error: Error) => void,
47
+ ): Promise<void>;
48
+ on<K extends keyof QueueEvents>(
49
+ this: this,
50
+ event: K,
51
+ cb: QueueEvents[K],
52
+ ): Unsubscribe;
47
53
  }
48
54
 
49
55
  export type IJob<T> = { jobId: JobId } & T;
50
56
 
51
57
  export interface IQueue<T, R> {
52
- addJob(data: IJob<T>): Promise<void>;
53
- getNextJob(): Promise<IJob<T> | undefined>;
54
- amountOfJobs(): Promise<number>;
55
- getId(): string;
56
- setBlocked(blocked: boolean): Promise<void>;
57
- isBlocked(): Promise<boolean>;
58
- isDeleted(): Promise<boolean>;
59
- setDeleted(deleted: boolean): Promise<void>;
60
- getJobs(): Promise<IJob<T>[]>;
61
- addDependencies(job: IJob<Job>): Promise<void>;
62
- removeDependencies(job: IJob<Job>): Promise<void>;
58
+ addJob(data: IJob<T>): Promise<void>;
59
+ getNextJob(): Promise<IJob<T> | undefined>;
60
+ amountOfJobs(): Promise<number>;
61
+ getId(): string;
62
+ setBlocked(blocked: boolean): Promise<void>;
63
+ isBlocked(): Promise<boolean>;
64
+ isDeleted(): Promise<boolean>;
65
+ setDeleted(deleted: boolean): Promise<void>;
66
+ getJobs(): Promise<IJob<T>[]>;
67
+ addDependencies(job: IJob<Job>): Promise<void>;
68
+ removeDependencies(job: IJob<Job>): Promise<void>;
63
69
  }
64
70
 
65
71
  export type IJobQueue = IQueue<Job, IOperationResult>;
66
72
 
67
73
  export function isOperationJob(job: Job): job is OperationJob {
68
- return "operations" in job;
74
+ return "operations" in job;
69
75
  }
70
76
 
71
77
  export function isActionJob(job: Job): job is ActionJob {
72
- return "actions" in job;
73
- }
78
+ return "actions" in job;
79
+ }
@@ -0,0 +1,19 @@
1
+ export abstract class ReadDriveError extends Error {}
2
+
3
+ export class ReadDriveNotFoundError extends ReadDriveError {
4
+ constructor(driveId: string) {
5
+ super(`Read drive ${driveId} not found.`);
6
+ }
7
+ }
8
+
9
+ export class ReadDriveSlugNotFoundError extends ReadDriveError {
10
+ constructor(slug: string) {
11
+ super(`Read drive with slug ${slug} not found.`);
12
+ }
13
+ }
14
+
15
+ export class ReadDocumentNotFoundError extends ReadDriveError {
16
+ constructor(drive: string, id: string) {
17
+ super(`Document with id ${id} not found on read drive ${drive}.`);
18
+ }
19
+ }
@@ -0,0 +1,125 @@
1
+ import { Document } from "document-model/document";
2
+ import { DocumentDriveServerConstructor, RemoteDriveOptions } from "../server";
3
+ import { logger } from "../utils/logger";
4
+ import { ReadDriveSlugNotFoundError } from "./errors";
5
+ import { ReadModeService } from "./service";
6
+ import {
7
+ IReadModeDriveServer,
8
+ IReadModeDriveService,
9
+ ReadDrive,
10
+ ReadDriveOptions,
11
+ ReadDrivesListener,
12
+ ReadModeDriveServerMixin,
13
+ } from "./types";
14
+
15
+ export * from "./errors";
16
+ export * from "./types";
17
+
18
+ export function ReadModeServer<TBase extends DocumentDriveServerConstructor>(
19
+ Base: TBase,
20
+ ): ReadModeDriveServerMixin {
21
+ return class ReadMode extends Base implements IReadModeDriveServer {
22
+ #readModeStorage: IReadModeDriveService;
23
+ #listeners = new Set<ReadDrivesListener>();
24
+
25
+ constructor(...args: any[]) {
26
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
27
+ super(...args);
28
+
29
+ this.#readModeStorage = new ReadModeService(
30
+ this.getDocumentModel.bind(this),
31
+ );
32
+
33
+ this.#buildDrives()
34
+ .then((drives) => {
35
+ if (drives.length) {
36
+ this.#notifyListeners(drives, "add");
37
+ }
38
+ })
39
+ .catch(logger.error);
40
+ }
41
+
42
+ async #buildDrives() {
43
+ const driveIds = await this.getReadDrives();
44
+ const drives = (
45
+ await Promise.all(driveIds.map((driveId) => this.getReadDrive(driveId)))
46
+ ).filter((drive) => !(drive instanceof Error)) as ReadDrive[];
47
+ return drives;
48
+ }
49
+
50
+ #notifyListeners(drives: ReadDrive[], operation: "add" | "delete") {
51
+ this.#listeners.forEach((listener) => listener(drives, operation));
52
+ }
53
+
54
+ getReadDrives(): Promise<string[]> {
55
+ return this.#readModeStorage.getReadDrives();
56
+ }
57
+
58
+ getReadDrive(id: string) {
59
+ return this.#readModeStorage.getReadDrive(id);
60
+ }
61
+
62
+ getReadDriveBySlug(
63
+ slug: string,
64
+ ): Promise<ReadDrive | ReadDriveSlugNotFoundError> {
65
+ return this.#readModeStorage.getReadDriveBySlug(slug);
66
+ }
67
+
68
+ getReadDriveContext(id: string) {
69
+ return this.#readModeStorage.getReadDriveContext(id);
70
+ }
71
+
72
+ async addReadDrive(url: string, options?: ReadDriveOptions) {
73
+ await this.#readModeStorage.addReadDrive(url, options);
74
+ this.#notifyListeners(await this.#buildDrives(), "add");
75
+ }
76
+
77
+ fetchDrive(id: string) {
78
+ return this.#readModeStorage.fetchDrive(id);
79
+ }
80
+
81
+ fetchDocument<D extends Document>(
82
+ driveId: string,
83
+ documentId: string,
84
+ documentType: string,
85
+ ) {
86
+ return this.#readModeStorage.fetchDocument<D>(
87
+ driveId,
88
+ documentId,
89
+ documentType,
90
+ );
91
+ }
92
+
93
+ async deleteReadDrive(id: string) {
94
+ const error = await this.#readModeStorage.deleteReadDrive(id);
95
+ if (error) {
96
+ return error;
97
+ }
98
+
99
+ this.#notifyListeners(await this.#buildDrives(), "delete");
100
+ }
101
+
102
+ async migrateReadDrive(id: string, options: RemoteDriveOptions) {
103
+ const result = await this.getReadDriveContext(id);
104
+ if (result instanceof Error) {
105
+ return result;
106
+ }
107
+
108
+ const { url, ...readOptions } = result;
109
+ try {
110
+ const newDrive = await this.addRemoteDrive(url, options);
111
+ return newDrive;
112
+ } catch (error) {
113
+ // if an error is thrown, then add the read drive again
114
+ logger.error(error);
115
+ await this.addReadDrive(result.url, readOptions);
116
+ throw error;
117
+ }
118
+ }
119
+
120
+ onReadDrivesUpdate(listener: ReadDrivesListener) {
121
+ this.#listeners.add(listener);
122
+ return Promise.resolve(() => this.#listeners.delete(listener));
123
+ }
124
+ };
125
+ }