diff --git a/src/QueueLink.ts b/src/QueueLink.ts index 55064d8..2fea904 100644 --- a/src/QueueLink.ts +++ b/src/QueueLink.ts @@ -1,56 +1,153 @@ import { - ApolloLink, - Operation, - FetchResult, - NextLink, + ApolloLink, + Operation, + FetchResult, + NextLink, } from '@apollo/client/link/core'; -import { - Observable, - Observer, -} from '@apollo/client/utilities'; +import { Observable, Observer } from '@apollo/client/utilities'; +import { createGuid } from './Utils'; interface OperationQueueEntry { - operation: Operation; - forward: NextLink; - observer: Observer; - subscription?: { unsubscribe: () => void }; + operation: Operation; + forward: NextLink; + observer: Observer; + subscription?: { unsubscribe: () => void }; +} + +type event = 'dequeue' | 'enqueue' | 'change'; + +interface Listener { + id: string; + callback: (entry: any) => void; } export default class QueueLink extends ApolloLink { - private opQueue: OperationQueueEntry[] = []; - private isOpen = true; - - public open() { - this.isOpen = true; - this.opQueue.forEach(({ operation, forward, observer }) => { - forward(operation).subscribe(observer); - }); - this.opQueue = []; - } + static listeners: Record = {}; + private opQueue: OperationQueueEntry[] = []; + private isOpen = true; - public close() { - this.isOpen = false; - } + public clear() { + this.opQueue = []; + QueueLink.listeners = {}; + } - public request(operation: Operation, forward: NextLink) { - if (this.isOpen) { - return forward(operation); - } - if (operation.getContext().skipQueue) { - return forward(operation); + public open() { + this.isOpen = true; + + const first: OperationQueueEntry | undefined = this.opQueue.shift(); + + if (first !== undefined) { + const { operation, forward, observer } = first; + + this.triggerListeners(first, 'dequeue'); + + forward(operation).subscribe( + (value) => { + if (observer && observer.next) { + observer?.next(value); + } + }, + (error) => { + if (observer && observer.error) { + observer?.error(error); + } + this.open(); + }, + () => { + if (observer && observer.complete) { + observer?.complete(); + } + this.open(); } - return new Observable((observer: Observer) => { - const operationEntry = { operation, forward, observer }; - this.enqueue(operationEntry); - return () => this.cancelOperation(operationEntry); - }); + ); } + } + + public static addLinkQueueEventListener = ( + opName: string, + event: event, + callback: (entry: any) => void + ) => { + if (event === 'change') opName = ''; + const key: string = QueueLink.key(opName, event); + + const newGuid = createGuid(); + + const newListener = { + [key]: [ + ...(key in QueueLink.listeners ? QueueLink.listeners[key] : []), + ...[{ id: newGuid, callback }], + ], + }; - private cancelOperation(entry: OperationQueueEntry) { - this.opQueue = this.opQueue.filter(e => e !== entry); + QueueLink.listeners = { ...QueueLink.listeners, ...newListener }; + + return newGuid; + }; + + public static removeLinkQueueEventListener = ( + opName: string, + event: event, + id: string + ) => { + if (event === 'change') opName = ''; + const key: string = QueueLink.key(opName, event); + + if (QueueLink.listeners[key] !== undefined) { + QueueLink.listeners[key] = QueueLink.listeners[key].filter( + (listener) => listener.id !== id + ); + + if (QueueLink.listeners[key].length === 0) { + delete QueueLink.listeners[key]; + } } + }; - private enqueue(entry: OperationQueueEntry) { - this.opQueue.push(entry); + public close() { + this.isOpen = false; + } + + public request(operation: Operation, forward: NextLink) { + if (this.isOpen) { + return forward(operation); + } + if (operation.getContext().skipQueue) { + return forward(operation); + } + return new Observable((observer: Observer) => { + const operationEntry = { operation, forward, observer }; + this.enqueue(operationEntry); + return () => this.cancelOperation(operationEntry); + }); + } + + private static key(op: string, ev: string) { + return `${op}${ev}`.toLocaleLowerCase(); + } + + private cancelOperation(entry: OperationQueueEntry) { + this.opQueue = this.opQueue.filter((e) => e !== entry); + } + + private enqueue(entry: OperationQueueEntry) { + this.opQueue.push(entry); + + this.triggerListeners(entry, 'enqueue'); + } + + private triggerListeners(entry: OperationQueueEntry, event: string) { + let key: string = QueueLink.key(entry.operation.operationName, event); + if (key in QueueLink.listeners) { + QueueLink.listeners[key].forEach((listener) => { + listener.callback(entry); + }); + } + key = QueueLink.key('', 'change'); + if (key in QueueLink.listeners) { + QueueLink.listeners[key].forEach((listener) => { + listener.callback(this.opQueue); + }); } + } } diff --git a/src/Utils.ts b/src/Utils.ts new file mode 100644 index 0000000..e2a3963 --- /dev/null +++ b/src/Utils.ts @@ -0,0 +1,7 @@ +export const createGuid = () => { + function _p8(s: boolean) { + const p = (Math.random().toString(16) + '000000000').substr(2, 8); + return s ? '-' + p.substr(0, 4) + '-' + p.substr(4, 4) : p; + } + return _p8(false) + _p8(true) + _p8(true) + _p8(false); +}; \ No newline at end of file diff --git a/tslint.json b/tslint.json index 3703cb6..3841866 100644 --- a/tslint.json +++ b/tslint.json @@ -1,8 +1,6 @@ { "extends": "tslint:latest", "rules": { - "no-unnecessary-type-assertion": true, - "array-type": [true, "array"], "ban-types": { "options": [ @@ -13,13 +11,11 @@ ["String", "Avoid using the `String` type. Did you mean `string`?"] ] }, - "boolean-trivia": true, "class-name": true, "comment-format": [true, "check-space" ], "curly":[true, "ignore-same-line"], - "debug-assert": true, "indent": [true, "spaces" ], @@ -27,24 +23,13 @@ "interface-over-type-literal": true, "jsdoc-format": true, "linebreak-style": [true, "LF"], - "next-line": [true, - "check-catch", - "check-else" - ], - "no-bom": true, - "no-double-space": true, - "no-in-operator": true, - "no-increment-decrement": true, "no-inferrable-types": true, "no-internal-module": true, "no-null-keyword": true, "no-switch-case-fall-through": true, "no-trailing-whitespace": [true, "ignore-template-strings"], - "no-type-assertion-whitespace": true, - "no-unnecessary-qualifier": true, "no-var-keyword": true, "object-literal-shorthand": true, - "object-literal-surrounding-space": true, "one-line": [true, "check-open-brace", "check-whitespace" @@ -57,7 +42,6 @@ "semicolon": [true, "always", "ignore-bound-class-methods"], "space-within-parens": true, "triple-equals": true, - "type-operator-spacing": true, "typedef-whitespace": [ true, { @@ -90,7 +74,6 @@ "arrow-parens": false, "arrow-return-shorthand": false, - "ban-types": false, "forin": false, "member-access": false, "no-conditional-assignment": false, @@ -106,13 +89,11 @@ "prefer-conditional-expression": false, "radix": false, "trailing-comma": false, - "align": false, "eofline": false, "max-line-length": false, "no-consecutive-blank-lines": false, "space-before-function-paren": false, - "ban-comma-operator": false, "max-classes-per-file": false, "member-ordering": false, @@ -122,5 +103,5 @@ "no-reference": false, "object-literal-sort-keys": false, "one-variable-per-declaration": false - } -} + } +} \ No newline at end of file