document-drive 1.0.0-alpha.90 → 1.0.0-alpha.92

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.90",
3
+ "version": "1.0.0-alpha.92",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -35,7 +35,7 @@
35
35
  "test:watch": "vitest watch"
36
36
  },
37
37
  "peerDependencies": {
38
- "document-model": "^1.7.0",
38
+ "document-model": "^1.8.0",
39
39
  "document-model-libs": "^1.57.0"
40
40
  },
41
41
  "optionalDependencies": {
@@ -65,9 +65,10 @@
65
65
  "@types/uuid": "^9.0.8",
66
66
  "@typescript-eslint/eslint-plugin": "^6.21.0",
67
67
  "@typescript-eslint/parser": "^6.21.0",
68
+ "@vitest/browser": "^2.0.5",
68
69
  "@vitest/coverage-v8": "^2.0.5",
69
70
  "document-model": "^1.7.0",
70
- "document-model-libs": "^1.70.0",
71
+ "document-model-libs": "^1.83.0",
71
72
  "eslint": "^8.57.0",
72
73
  "eslint-config-prettier": "^9.1.0",
73
74
  "fake-indexeddb": "^5.0.2",
@@ -80,7 +81,8 @@
80
81
  "sequelize": "^6.37.2",
81
82
  "sqlite3": "^5.1.7",
82
83
  "typescript": "^5.5.3",
83
- "vitest": "^2.0.5"
84
+ "vitest": "^2.0.5",
85
+ "webdriverio": "^9.0.9"
84
86
  },
85
87
  "packageManager": "pnpm@9.1.4+sha256.30a1801ac4e723779efed13a21f4c39f9eb6c9fbb4ced101bce06b422593d7c9"
86
88
  }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './server';
2
+ export * from './server/error';
2
3
  export * from './storage';
3
4
  export * from './utils';
package/src/queue/base.ts CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from 'document-model-libs/document-drive';
5
5
  import { Action } from 'document-model/document';
6
6
  import { Unsubscribe, createNanoEvents } from 'nanoevents';
7
- import { generateUUID } from '../utils';
7
+ import { generateUUID, runAsap } from '../utils';
8
8
  import { logger } from '../utils/logger';
9
9
  import {
10
10
  IJob,
@@ -212,10 +212,10 @@ export class BaseQueueManager implements IQueueManager {
212
212
  private retryNextJob(timeout?: number) {
213
213
  const _timeout = timeout !== undefined ? timeout : this.timeout;
214
214
  const retry =
215
- _timeout === 0 && typeof setImmediate !== 'undefined'
216
- ? setImmediate
217
- : (fn: () => void) => setTimeout(fn, _timeout);
218
- return retry(() => this.processNextJob());
215
+ _timeout > 0
216
+ ? (fn: () => void) => setTimeout(fn, _timeout)
217
+ : runAsap;
218
+ retry(() => this.processNextJob());
219
219
  }
220
220
 
221
221
  private async findFirstNonEmptyQueue(
@@ -51,3 +51,12 @@ export class DriveNotFoundError extends Error {
51
51
  this.driveId = driveId;
52
52
  }
53
53
  }
54
+
55
+ export class SynchronizationUnitNotFoundError extends Error {
56
+ syncUnitId: string;
57
+
58
+ constructor(message: string, syncUnitId: string) {
59
+ super(message);
60
+ this.syncUnitId = syncUnitId;
61
+ }
62
+ }
@@ -41,7 +41,13 @@ import type {
41
41
  DocumentStorage,
42
42
  IDriveStorage
43
43
  } from '../storage/types';
44
- import { generateUUID, isBefore, isDocumentDrive } from '../utils';
44
+ import {
45
+ generateUUID,
46
+ isBefore,
47
+ isDocumentDrive,
48
+ RunAsap,
49
+ runAsapAsync
50
+ } from '../utils';
45
51
  import { DefaultDrivesManager } from '../utils/default-drives-manager';
46
52
  import {
47
53
  attachBranch,
@@ -58,7 +64,8 @@ import { logger } from '../utils/logger';
58
64
  import {
59
65
  ConflictOperationError,
60
66
  DriveAlreadyExistsError,
61
- OperationError
67
+ OperationError,
68
+ SynchronizationUnitNotFoundError
62
69
  } from './error';
63
70
  import { ListenerManager } from './listener/manager';
64
71
  import {
@@ -72,9 +79,11 @@ import {
72
79
  import {
73
80
  AddOperationOptions,
74
81
  BaseDocumentDriveServer,
82
+ DefaultListenerManagerOptions,
75
83
  DocumentDriveServerOptions,
76
84
  DriveEvents,
77
85
  GetDocumentOptions,
86
+ GetStrandsOptions,
78
87
  IOperationResult,
79
88
  ListenerState,
80
89
  RemoteDriveOptions,
@@ -111,6 +120,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
111
120
 
112
121
  private defaultDrivesManager: DefaultDrivesManager;
113
122
 
123
+ protected options: Required<DocumentDriveServerOptions>;
124
+
114
125
  constructor(
115
126
  documentModels: DocumentModel[],
116
127
  storage: IDriveStorage = new MemoryStorage(),
@@ -119,7 +130,27 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
119
130
  options?: DocumentDriveServerOptions
120
131
  ) {
121
132
  super();
122
- this.listenerStateManager = new ListenerManager(this);
133
+ this.options = {
134
+ defaultRemoteDrives: [],
135
+ removeOldRemoteDrives: {
136
+ strategy: 'preserve-all'
137
+ },
138
+ ...options,
139
+ listenerManager: {
140
+ ...DefaultListenerManagerOptions,
141
+ ...options?.listenerManager
142
+ },
143
+ taskQueueMethod:
144
+ options?.taskQueueMethod === undefined
145
+ ? RunAsap.runAsap
146
+ : options.taskQueueMethod
147
+ };
148
+
149
+ this.listenerStateManager = new ListenerManager(
150
+ this,
151
+ undefined,
152
+ options?.listenerManager
153
+ );
123
154
  this.documentModels = documentModels;
124
155
  this.storage = storage;
125
156
  this.cache = cache;
@@ -559,6 +590,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
559
590
  }
560
591
 
561
592
  private async _initialize() {
593
+ await this.queueManager.init(this.queueDelegate, error => {
594
+ logger.error(`Error initializing queue manager`, error);
595
+ errors.push(error);
596
+ });
597
+
562
598
  try {
563
599
  await this.defaultDrivesManager.removeOldremoteDrives();
564
600
  } catch (error) {
@@ -574,11 +610,6 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
574
610
  });
575
611
  }
576
612
 
577
- await this.queueManager.init(this.queueDelegate, error => {
578
- logger.error(`Error initializing queue manager`, error);
579
- errors.push(error);
580
- });
581
-
582
613
  await this.defaultDrivesManager.initializeDefaultRemoteDrives();
583
614
 
584
615
  // if network connect comes back online
@@ -819,10 +850,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
819
850
  async getOperationData(
820
851
  driveId: string,
821
852
  syncId: string,
822
- filter: {
823
- since?: string | undefined;
824
- fromRevision?: number | undefined;
825
- },
853
+ filter: GetStrandsOptions,
826
854
  loadedDrive?: DocumentDriveDocument
827
855
  ): Promise<OperationUpdate[]> {
828
856
  const syncUnit =
@@ -845,6 +873,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
845
873
 
846
874
  const operations =
847
875
  document.operations[syncUnit.scope as OperationScope] ?? []; // TODO filter by branch also
876
+
848
877
  const filteredOperations = operations.filter(
849
878
  operation =>
850
879
  Object.keys(filter).length === 0 ||
@@ -854,7 +883,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
854
883
  operation.index > filter.fromRevision))
855
884
  );
856
885
 
857
- return filteredOperations.map(operation => ({
886
+ const limitedOperations = filter.limit
887
+ ? filteredOperations.slice(0, filter.limit)
888
+ : filteredOperations;
889
+
890
+ return limitedOperations.map(operation => ({
858
891
  hash: operation.hash,
859
892
  index: operation.index,
860
893
  timestamp: operation.timestamp,
@@ -1135,6 +1168,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1135
1168
  ) {
1136
1169
  const operationsApplied: Operation<A | BaseAction>[] = [];
1137
1170
  const signals: SignalResult[] = [];
1171
+
1138
1172
  const documentStorageWithState = await this._addDocumentResultingStage(
1139
1173
  documentStorage,
1140
1174
  drive,
@@ -1192,13 +1226,19 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1192
1226
  }
1193
1227
 
1194
1228
  try {
1195
- const appliedResult = await this._performOperation(
1196
- drive,
1197
- documentId,
1198
- document,
1199
- nextOperation,
1200
- skipHashValidation
1201
- );
1229
+ // runs operation on next available tick, to avoid blocking the main thread
1230
+ const taskQueueMethod = this.options.taskQueueMethod;
1231
+ const task = () =>
1232
+ this._performOperation(
1233
+ drive,
1234
+ documentId,
1235
+ document,
1236
+ nextOperation,
1237
+ skipHashValidation
1238
+ );
1239
+ const appliedResult = await (taskQueueMethod
1240
+ ? runAsapAsync(task, taskQueueMethod)
1241
+ : task());
1202
1242
  document = appliedResult.document;
1203
1243
  signals.push(...appliedResult.signals);
1204
1244
  operationsApplied.push(appliedResult.operation);
@@ -2304,11 +2344,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
2304
2344
  return this.listenerStateManager.getListener(driveId, listenerId);
2305
2345
  }
2306
2346
 
2307
- getSyncStatus(drive: string): SyncStatus {
2308
- const status = this.syncStatus.get(drive);
2347
+ getSyncStatus(
2348
+ syncUnitId: string
2349
+ ): SyncStatus | SynchronizationUnitNotFoundError {
2350
+ const status = this.syncStatus.get(syncUnitId);
2309
2351
  if (!status) {
2310
- logger.error(`Sync status not found for drive ${drive}`);
2311
- throw new Error(`Sync status not found for drive ${drive}`);
2352
+ return new SynchronizationUnitNotFoundError(
2353
+ `Sync status not found for syncUnitId: ${syncUnitId}`,
2354
+ syncUnitId
2355
+ );
2312
2356
  }
2313
2357
  return this.getCombinedSyncUnitStatus(status);
2314
2358
  }
@@ -8,6 +8,7 @@ import { OperationError } from '../error';
8
8
  import {
9
9
  BaseListenerManager,
10
10
  ErrorStatus,
11
+ GetStrandsOptions,
11
12
  Listener,
12
13
  ListenerState,
13
14
  ListenerUpdate,
@@ -45,7 +46,6 @@ function debounce<T extends unknown[], R>(
45
46
  });
46
47
  };
47
48
  }
48
-
49
49
  export class ListenerManager extends BaseListenerManager {
50
50
  static LISTENER_UPDATE_DELAY = 250;
51
51
 
@@ -252,49 +252,51 @@ export class ListenerManager extends BaseListenerManager {
252
252
  );
253
253
 
254
254
  const strandUpdates: StrandUpdate[] = [];
255
-
256
255
  // TODO change to push one after the other, reusing operation data
257
- await Promise.all(
258
- syncUnits.map(async syncUnit => {
259
- const unitState = listener.syncUnits.get(
260
- syncUnit.syncId
261
- );
256
+ const tasks = syncUnits.map(syncUnit => async () => {
257
+ const unitState = listener.syncUnits.get(syncUnit.syncId);
262
258
 
263
- if (
264
- unitState &&
265
- unitState.listenerRev >= syncUnit.revision
266
- ) {
267
- return;
268
- }
259
+ if (
260
+ unitState &&
261
+ unitState.listenerRev >= syncUnit.revision
262
+ ) {
263
+ return;
264
+ }
269
265
 
270
- const opData: OperationUpdate[] = [];
271
- try {
272
- const data = await this.drive.getOperationData(
273
- // TODO - join queries, DEAL WITH INVALID SYNC ID ERROR
274
- driveId,
275
- syncUnit.syncId,
276
- {
277
- fromRevision: unitState?.listenerRev
278
- }
279
- );
280
- opData.push(...data);
281
- } catch (e) {
282
- logger.error(e);
283
- }
266
+ const opData: OperationUpdate[] = [];
267
+ try {
268
+ const data = await this.drive.getOperationData(
269
+ // TODO - join queries, DEAL WITH INVALID SYNC ID ERROR
270
+ driveId,
271
+ syncUnit.syncId,
272
+ {
273
+ fromRevision: unitState?.listenerRev
274
+ }
275
+ );
276
+ opData.push(...data);
277
+ } catch (e) {
278
+ logger.error(e);
279
+ }
284
280
 
285
- if (!opData.length) {
286
- return;
287
- }
281
+ if (!opData.length) {
282
+ return;
283
+ }
288
284
 
289
- strandUpdates.push({
290
- driveId,
291
- documentId: syncUnit.documentId,
292
- branch: syncUnit.branch,
293
- operations: opData,
294
- scope: syncUnit.scope as OperationScope
295
- });
296
- })
297
- );
285
+ strandUpdates.push({
286
+ driveId,
287
+ documentId: syncUnit.documentId,
288
+ branch: syncUnit.branch,
289
+ operations: opData,
290
+ scope: syncUnit.scope as OperationScope
291
+ });
292
+ });
293
+ if (this.options.sequentialUpdates) {
294
+ for (const task of tasks) {
295
+ await task();
296
+ }
297
+ } else {
298
+ await Promise.all(tasks.map(task => task()));
299
+ }
298
300
 
299
301
  if (strandUpdates.length == 0) {
300
302
  continue;
@@ -478,7 +480,7 @@ export class ListenerManager extends BaseListenerManager {
478
480
  async getStrands(
479
481
  driveId: string,
480
482
  listenerId: string,
481
- since?: string
483
+ options?: GetStrandsOptions
482
484
  ): Promise<StrandUpdate[]> {
483
485
  // fetch listenerState from listenerManager
484
486
  const listener = await this.getListener(driveId, listenerId);
@@ -493,46 +495,63 @@ export class ListenerManager extends BaseListenerManager {
493
495
  drive
494
496
  );
495
497
 
496
- await Promise.all(
497
- syncUnits.map(async syncUnit => {
498
- if (syncUnit.revision < 0) {
499
- return;
500
- }
501
- const entry = listener.syncUnits.get(syncUnit.syncId);
502
- if (entry && entry.listenerRev >= syncUnit.revision) {
503
- return;
504
- }
498
+ const limit = options?.limit; // maximum number of operations to send across all sync units
499
+ let operationsCount = 0; // total amount of operations that have been retrieved
505
500
 
506
- const { documentId, driveId, scope, branch } = syncUnit;
507
- try {
508
- const operations = await this.drive.getOperationData(
509
- // DEAL WITH INVALID SYNC ID ERROR
510
- driveId,
511
- syncUnit.syncId,
512
- {
513
- since,
514
- fromRevision: entry?.listenerRev
515
- },
516
- drive
517
- );
501
+ const tasks = syncUnits.map(syncUnit => async () => {
502
+ if (limit && operationsCount >= limit) {
503
+ // break;
504
+ return;
505
+ }
506
+ if (syncUnit.revision < 0) {
507
+ return;
508
+ }
509
+ const entry = listener.syncUnits.get(syncUnit.syncId);
510
+ if (entry && entry.listenerRev >= syncUnit.revision) {
511
+ return;
512
+ }
518
513
 
519
- if (!operations.length) {
520
- return;
521
- }
514
+ const { documentId, driveId, scope, branch } = syncUnit;
515
+ try {
516
+ const operations = await this.drive.getOperationData(
517
+ // DEAL WITH INVALID SYNC ID ERROR
518
+ driveId,
519
+ syncUnit.syncId,
520
+ {
521
+ since: options?.since,
522
+ fromRevision:
523
+ options?.fromRevision ?? entry?.listenerRev,
524
+ limit: limit ? limit - operationsCount : undefined
525
+ },
526
+ drive
527
+ );
522
528
 
523
- strands.push({
524
- driveId,
525
- documentId,
526
- scope: scope as OperationScope,
527
- branch,
528
- operations
529
- });
530
- } catch (error) {
531
- logger.error(error);
529
+ if (!operations.length) {
532
530
  return;
533
531
  }
534
- })
535
- );
532
+
533
+ operationsCount += operations.length;
534
+
535
+ strands.push({
536
+ driveId,
537
+ documentId,
538
+ scope: scope as OperationScope,
539
+ branch,
540
+ operations
541
+ });
542
+ } catch (error) {
543
+ logger.error(error);
544
+ return;
545
+ }
546
+ });
547
+
548
+ if (this.options.sequentialUpdates) {
549
+ for (const task of tasks) {
550
+ await task();
551
+ }
552
+ } else {
553
+ await Promise.all(tasks.map(task => task()));
554
+ }
536
555
 
537
556
  return strands;
538
557
  }
@@ -7,6 +7,7 @@ import { logger as defaultLogger } from '../../../utils/logger';
7
7
  import { OperationError } from '../../error';
8
8
  import {
9
9
  BaseDocumentDriveServer,
10
+ GetStrandsOptions,
10
11
  IOperationResult,
11
12
  Listener,
12
13
  ListenerRevision,
@@ -41,7 +42,7 @@ export type StrandUpdateGraphQL = Omit<StrandUpdate, 'operations'> & {
41
42
  };
42
43
 
43
44
  export interface IPullResponderTransmitter extends ITransmitter {
44
- getStrands(since?: string): Promise<StrandUpdate[]>;
45
+ getStrands(options?: GetStrandsOptions): Promise<StrandUpdate[]>;
45
46
  }
46
47
 
47
48
  export class PullResponderTransmitter implements IPullResponderTransmitter {
@@ -59,11 +60,11 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
59
60
  this.manager = manager;
60
61
  }
61
62
 
62
- getStrands(since?: string | undefined): Promise<StrandUpdate[]> {
63
+ getStrands(options?: GetStrandsOptions): Promise<StrandUpdate[]> {
63
64
  return this.manager.getStrands(
64
65
  this.listener.driveId,
65
66
  this.listener.listenerId,
66
- since
67
+ options
67
68
  );
68
69
  }
69
70
 
@@ -135,7 +136,7 @@ export class PullResponderTransmitter implements IPullResponderTransmitter {
135
136
  driveId: string,
136
137
  url: string,
137
138
  listenerId: string,
138
- since?: string // TODO add support for since
139
+ options?: GetStrandsOptions // TODO add support for since
139
140
  ): Promise<StrandUpdate[]> {
140
141
  const {
141
142
  system: {
@@ -20,8 +20,9 @@ import type {
20
20
  State
21
21
  } from 'document-model/document';
22
22
  import { Unsubscribe } from 'nanoevents';
23
+ import { RunAsap } from '../utils';
23
24
  import { DriveInfo } from '../utils/graphql';
24
- import { OperationError } from './error';
25
+ import { OperationError, SynchronizationUnitNotFoundError } from './error';
25
26
  import {
26
27
  ITransmitter,
27
28
  PullResponderTrigger,
@@ -235,6 +236,18 @@ export type RemoveOldRemoteDrivesOption =
235
236
  export type DocumentDriveServerOptions = {
236
237
  defaultRemoteDrives?: Array<DefaultRemoteDriveInput>;
237
238
  removeOldRemoteDrives?: RemoveOldRemoteDrivesOption;
239
+ /* method to queue heavy tasks that might block the event loop.
240
+ * If set to null then it will queued as micro task.
241
+ * Defaults to the most appropriate method according to the system
242
+ */
243
+ taskQueueMethod?: RunAsap.RunAsap<unknown> | null;
244
+ listenerManager?: ListenerManagerOptions;
245
+ };
246
+
247
+ export type GetStrandsOptions = {
248
+ limit?: number;
249
+ since?: string;
250
+ fromRevision?: number;
238
251
  };
239
252
 
240
253
  export abstract class BaseDocumentDriveServer {
@@ -361,7 +374,9 @@ export abstract class BaseDocumentDriveServer {
361
374
  options?: AddOperationOptions
362
375
  ): Promise<IOperationResult<DocumentDriveDocument>>;
363
376
 
364
- abstract getSyncStatus(drive: string): SyncStatus;
377
+ abstract getSyncStatus(
378
+ syncUnitId: string
379
+ ): SyncStatus | SynchronizationUnitNotFoundError;
365
380
 
366
381
  /** Synchronization methods */
367
382
  abstract getSynchronizationUnits(
@@ -390,10 +405,7 @@ export abstract class BaseDocumentDriveServer {
390
405
  abstract getOperationData(
391
406
  driveId: string,
392
407
  syncId: string,
393
- filter: {
394
- since?: string;
395
- fromRevision?: number;
396
- },
408
+ filter: GetStrandsOptions,
397
409
  loadedDrive?: DocumentDriveDocument
398
410
  ): Promise<OperationUpdate[]>;
399
411
 
@@ -430,8 +442,17 @@ export abstract class BaseDocumentDriveServer {
430
442
  ): Promise<PullResponderTrigger>;
431
443
  }
432
444
 
445
+ export type ListenerManagerOptions = {
446
+ sequentialUpdates?: boolean;
447
+ };
448
+
449
+ export const DefaultListenerManagerOptions = {
450
+ sequentialUpdates: true
451
+ };
452
+
433
453
  export abstract class BaseListenerManager {
434
454
  protected drive: BaseDocumentDriveServer;
455
+ protected options: ListenerManagerOptions;
435
456
  protected listenerState = new Map<string, Map<string, ListenerState>>();
436
457
  protected transmitters: Record<
437
458
  DocumentDriveState['id'],
@@ -440,10 +461,12 @@ export abstract class BaseListenerManager {
440
461
 
441
462
  constructor(
442
463
  drive: BaseDocumentDriveServer,
443
- listenerState = new Map<string, Map<string, ListenerState>>()
464
+ listenerState = new Map<string, Map<string, ListenerState>>(),
465
+ options: ListenerManagerOptions = DefaultListenerManagerOptions
444
466
  ) {
445
467
  this.drive = drive;
446
468
  this.listenerState = listenerState;
469
+ this.options = { ...DefaultListenerManagerOptions, ...options };
447
470
  }
448
471
 
449
472
  abstract initDrive(drive: DocumentDriveDocument): Promise<void>;
@@ -466,8 +489,9 @@ export abstract class BaseListenerManager {
466
489
  ): Promise<ITransmitter | undefined>;
467
490
 
468
491
  abstract getStrands(
492
+ driveId: string,
469
493
  listenerId: string,
470
- since?: string
494
+ options?: GetStrandsOptions
471
495
  ): Promise<StrandUpdate[]>;
472
496
 
473
497
  abstract updateSynchronizationRevisions(
@@ -10,9 +10,15 @@ import {
10
10
  Operation,
11
11
  OperationScope
12
12
  } from 'document-model/document';
13
+ // import setAsap from 'setasap';
13
14
  import { v4 as uuidv4 } from 'uuid';
14
15
  import { OperationError } from '../server/error';
15
16
  import { DocumentDriveStorage, DocumentStorage } from '../storage';
17
+ import { RunAsap } from './run-asap';
18
+ export * from './run-asap';
19
+
20
+ export const runAsap = RunAsap.runAsap;
21
+ export const runAsapAsync = RunAsap.runAsapAsync;
16
22
 
17
23
  export function isDocumentDriveStorage(
18
24
  document: DocumentStorage
@@ -0,0 +1,159 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-namespace
2
+ export namespace RunAsap {
3
+ export type Task<T = void> = () => T;
4
+ export type AbortTask = () => void;
5
+ export type RunAsap<T> = (task: Task<T>) => AbortTask;
6
+
7
+ export const useMessageChannel = (() => {
8
+ if (typeof MessageChannel === 'undefined') {
9
+ return new Error('MessageChannel is not supported');
10
+ }
11
+
12
+ return (task: Task) => {
13
+ const controller = new AbortController();
14
+ const signal = controller.signal;
15
+ const mc = new MessageChannel();
16
+ mc.port1.postMessage(null);
17
+ mc.port2.addEventListener(
18
+ 'message',
19
+ () => {
20
+ task();
21
+ mc.port1.close();
22
+ mc.port2.close();
23
+ },
24
+ { once: true, signal: signal }
25
+ );
26
+ mc.port2.start();
27
+ return () => controller.abort();
28
+ };
29
+ })();
30
+
31
+ export const usePostMessage = (() => {
32
+ const _main: unknown =
33
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
34
+ (typeof window === 'object' && window) ||
35
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
36
+ (typeof global === 'object' && global) ||
37
+ (typeof self === 'object' && self);
38
+ if (!_main) {
39
+ return new Error('No global object found');
40
+ }
41
+
42
+ const main = _main as Window;
43
+ if (
44
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
45
+ !main.postMessage ||
46
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
47
+ !main.addEventListener ||
48
+ (main as { importScripts?: unknown }).importScripts // web workers can't this method
49
+ ) {
50
+ return new Error('postMessage is not supported');
51
+ }
52
+
53
+ let index = 0;
54
+ const tasks = new Map<number, Task>();
55
+
56
+ function getNewIndex() {
57
+ if (index === 9007199254740991) {
58
+ return 0;
59
+ }
60
+ return ++index;
61
+ }
62
+
63
+ const MESSAGE_PREFIX = 'com.usePostMessage' + Math.random();
64
+
65
+ main.addEventListener(
66
+ 'message',
67
+ e => {
68
+ const event = e as MessageEvent<string>;
69
+ if (typeof event.data !== 'string') {
70
+ return;
71
+ }
72
+ if (
73
+ event.source !== main ||
74
+ !event.data.startsWith(MESSAGE_PREFIX)
75
+ ) {
76
+ return;
77
+ }
78
+ const index = event.data.split(':').at(1);
79
+ if (index === undefined) {
80
+ return;
81
+ }
82
+ const i = +index;
83
+ const task = tasks.get(i);
84
+ if (task) {
85
+ task();
86
+ tasks.delete(i);
87
+ }
88
+ },
89
+ false
90
+ );
91
+
92
+ return (task: Task) => {
93
+ const i = getNewIndex();
94
+ tasks.set(i, task);
95
+ main.postMessage(MESSAGE_PREFIX + ':' + i, { targetOrigin: '*' });
96
+ return () => {
97
+ tasks.delete(i);
98
+ };
99
+ };
100
+ })();
101
+
102
+ export const useSetImmediate = (() => {
103
+ if (typeof window !== 'undefined') {
104
+ return new Error('setImmediate is not supported on the browser');
105
+ }
106
+ if (typeof setImmediate === 'undefined') {
107
+ return new Error('setImmediate is not supported');
108
+ }
109
+
110
+ return (task: Task) => {
111
+ const id = setImmediate(task);
112
+ return () => clearImmediate(id);
113
+ };
114
+ })();
115
+
116
+ export const useSetTimeout = (() => {
117
+ return (task: Task) => {
118
+ const id = setTimeout(task, 0);
119
+ return () => clearTimeout(id);
120
+ };
121
+ })();
122
+
123
+ // queues the task in the macro tasks queue, so it doesn't
124
+ // prevent the event loop from movin on the next tick
125
+ export function runAsap<T = void>(task: Task<T>): AbortTask {
126
+ // if on node use setImmediate
127
+ if (!(useSetImmediate instanceof Error)) {
128
+ return useSetImmediate(task);
129
+ }
130
+ // on browser use MessageChannel if available
131
+ else if (!(useMessageChannel instanceof Error)) {
132
+ return useMessageChannel(task);
133
+ }
134
+ // otherwise use window.postMessage
135
+ else if (!(usePostMessage instanceof Error)) {
136
+ return usePostMessage(task);
137
+ }
138
+ // fallback to setTimeout with 0 delay
139
+ else {
140
+ return useSetTimeout(task);
141
+ }
142
+ }
143
+
144
+ export function runAsapAsync<T = void>(
145
+ task: RunAsap.Task<Promise<T>>,
146
+ queueMethod: RunAsap<void> = runAsap
147
+ ): Promise<T> {
148
+ if (queueMethod instanceof Error) {
149
+ throw new Error('queueMethod is not supported', {
150
+ cause: queueMethod
151
+ });
152
+ }
153
+ return new Promise((resolve, reject) => {
154
+ queueMethod(() => {
155
+ task().then(resolve).catch(reject);
156
+ });
157
+ });
158
+ }
159
+ }