command.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /**
  2. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import { PluginContext } from '../context';
  7. import { LinkedList } from 'mol-data/generic';
  8. import { RxEventHelper } from 'mol-util/rx-event-helper';
  9. import { UUID } from 'mol-util';
  10. export { PluginCommand }
  11. interface PluginCommand<T = unknown> {
  12. readonly id: UUID,
  13. dispatch(ctx: PluginContext, params: T): Promise<void>,
  14. subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription,
  15. params: { isImmediate: boolean }
  16. }
  17. /** namespace.id must a globally unique identifier */
  18. function PluginCommand<T>(params?: Partial<PluginCommand<T>['params']>): PluginCommand<T> {
  19. return new Impl({ isImmediate: false, ...params });
  20. }
  21. class Impl<T> implements PluginCommand<T> {
  22. dispatch(ctx: PluginContext, params: T): Promise<void> {
  23. return ctx.commands.dispatch(this, params)
  24. }
  25. subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription {
  26. return ctx.commands.subscribe(this, action);
  27. }
  28. id = UUID.create22();
  29. constructor(public params: PluginCommand<T>['params']) {
  30. }
  31. }
  32. namespace PluginCommand {
  33. export type Id = string & { '@type': 'plugin-command-id' }
  34. export interface Subscription {
  35. unsubscribe(): void
  36. }
  37. export type Action<T> = (params: T) => void | Promise<void>
  38. type Instance = { cmd: PluginCommand<any>, params: any, resolve: () => void, reject: (e: any) => void }
  39. export class Manager {
  40. private subs = new Map<string, Action<any>[]>();
  41. private queue = LinkedList<Instance>();
  42. private disposing = false;
  43. private ev = RxEventHelper.create();
  44. readonly behaviour = {
  45. locked: this.ev.behavior<boolean>(false)
  46. };
  47. lock(locked: boolean = true) {
  48. this.behaviour.locked.next(locked);
  49. }
  50. subscribe<T>(cmd: PluginCommand<T>, action: Action<T>): Subscription {
  51. let actions = this.subs.get(cmd.id);
  52. if (!actions) {
  53. actions = [];
  54. this.subs.set(cmd.id, actions);
  55. }
  56. actions.push(action);
  57. return {
  58. unsubscribe: () => {
  59. const actions = this.subs.get(cmd.id);
  60. if (!actions) return;
  61. const idx = actions.indexOf(action);
  62. if (idx < 0) return;
  63. for (let i = idx + 1; i < actions.length; i++) {
  64. actions[i - 1] = actions[i];
  65. }
  66. actions.pop();
  67. }
  68. }
  69. }
  70. /** Resolves after all actions have completed */
  71. dispatch<T>(cmd: PluginCommand<T>, params: T) {
  72. return new Promise<void>((resolve, reject) => {
  73. if (this.disposing) {
  74. reject('disposed');
  75. return;
  76. }
  77. const actions = this.subs.get(cmd.id);
  78. if (!actions) {
  79. resolve();
  80. return;
  81. }
  82. const instance: Instance = { cmd, params, resolve, reject };
  83. if (cmd.params.isImmediate) {
  84. this.resolve(instance);
  85. } else {
  86. this.queue.addLast({ cmd, params, resolve, reject });
  87. this.next();
  88. }
  89. });
  90. }
  91. dispose() {
  92. this.subs.clear();
  93. while (this.queue.count > 0) {
  94. this.queue.removeFirst();
  95. }
  96. }
  97. private async resolve(instance: Instance) {
  98. const actions = this.subs.get(instance.cmd.id);
  99. if (!actions) {
  100. try {
  101. instance.resolve();
  102. } finally {
  103. if (!instance.cmd.params.isImmediate && !this.disposing) this.next();
  104. }
  105. return;
  106. }
  107. try {
  108. if (!instance.cmd.params.isImmediate) this.executing = true;
  109. // TODO: should actions be called "asynchronously" ("setImmediate") instead?
  110. for (const a of actions) {
  111. await a(instance.params);
  112. }
  113. instance.resolve();
  114. } catch (e) {
  115. instance.reject(e);
  116. } finally {
  117. if (!instance.cmd.params.isImmediate) {
  118. this.executing = false;
  119. if (!this.disposing) this.next();
  120. }
  121. }
  122. }
  123. private executing = false;
  124. private async next() {
  125. if (this.queue.count === 0 || this.executing) return;
  126. const instance = this.queue.removeFirst()!;
  127. this.resolve(instance);
  128. }
  129. }
  130. }