@dronedeploy/rocos-js-sdk 3.0.0-alpha.19 → 3.0.0-alpha.20
Sign up to get free protection for your applications and to get access to all the features.
- package/helpers/callerMessageHelpers.d.ts +14 -0
- package/helpers/callerMessageHelpers.js +86 -0
- package/models/caller/IRocosCallerMessageChunk.d.ts +6 -1
- package/models/caller/index.d.ts +1 -0
- package/models/caller/index.js +1 -0
- package/models/params/ICallerParams.d.ts +10 -0
- package/package.json +1 -1
- package/services/CallerService.d.ts +33 -1
- package/services/CallerService.js +66 -2
- package/services/CallerService.spec.d.ts +1 -0
- package/services/CallerService.spec.js +227 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
import { IRocosCallerMessage, IRocosCallerMessageChunk, IRocosCallerMessageResponse } from '../models';
|
2
|
+
import { OperatorFunction } from 'rxjs';
|
3
|
+
/** Given a caller message, return the responses contained within it.
|
4
|
+
*
|
5
|
+
* Chunked responses with acks and results will return the intermediate acks and results as Responses along with the chunk itself
|
6
|
+
*/
|
7
|
+
export declare function getResponses(res: IRocosCallerMessage): (IRocosCallerMessageResponse | IRocosCallerMessageChunk)[];
|
8
|
+
/** A pipeable operator that will convert a stream of chunks into a stream of responses.
|
9
|
+
*
|
10
|
+
* Any non-chunk messages will be passed through unchanged.
|
11
|
+
*
|
12
|
+
* Chunked responses will be buffered until all chunks are received, then merged into a single response.
|
13
|
+
*/
|
14
|
+
export declare function handleChunkedMessages(): OperatorFunction<IRocosCallerMessageChunk | IRocosCallerMessageResponse, IRocosCallerMessageResponse>;
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import { map, pipe } from 'rxjs';
|
2
|
+
import { filter } from 'rxjs/operators';
|
3
|
+
/** Given a caller message, return the responses contained within it.
|
4
|
+
*
|
5
|
+
* Chunked responses with acks and results will return the intermediate acks and results as Responses along with the chunk itself
|
6
|
+
*/
|
7
|
+
export function getResponses(res) {
|
8
|
+
if (res.chunks?.chunks) {
|
9
|
+
return getIntermediateResponsesFromChunks(res.chunks.chunks);
|
10
|
+
}
|
11
|
+
if (res.responses?.responses) {
|
12
|
+
return res.responses.responses;
|
13
|
+
}
|
14
|
+
return [];
|
15
|
+
}
|
16
|
+
/** Given a list of chunks, convert any intermediate acks and results into responses. */
|
17
|
+
function getIntermediateResponsesFromChunks(chunks) {
|
18
|
+
const responses = [];
|
19
|
+
for (const chunk of chunks) {
|
20
|
+
// chunk needs to be pushed first so that the final response can be constructed from the chunks
|
21
|
+
// before any result message can close the observable
|
22
|
+
responses.push(chunk);
|
23
|
+
if (chunk.uid && (chunk.ack || chunk.result)) {
|
24
|
+
responses.push({
|
25
|
+
uid: chunk.uid,
|
26
|
+
ack: chunk.ack,
|
27
|
+
result: chunk.result,
|
28
|
+
});
|
29
|
+
}
|
30
|
+
}
|
31
|
+
return responses;
|
32
|
+
}
|
33
|
+
/** A pipeable operator that will convert a stream of chunks into a stream of responses.
|
34
|
+
*
|
35
|
+
* Any non-chunk messages will be passed through unchanged.
|
36
|
+
*
|
37
|
+
* Chunked responses will be buffered until all chunks are received, then merged into a single response.
|
38
|
+
*/
|
39
|
+
export function handleChunkedMessages() {
|
40
|
+
let chunkBuffer = null;
|
41
|
+
return pipe(map((message) => {
|
42
|
+
if ('chunkIndex' in message) {
|
43
|
+
if (chunkBuffer === null) {
|
44
|
+
chunkBuffer = new Array(message.chunkCount);
|
45
|
+
}
|
46
|
+
chunkBuffer[message.chunkIndex] = message;
|
47
|
+
// have to check with Object.keys because the array is buffered and the length is not accurate
|
48
|
+
if (Object.keys(chunkBuffer).length !== message.chunkCount) {
|
49
|
+
return;
|
50
|
+
}
|
51
|
+
return buildResponseFromChunks(chunkBuffer);
|
52
|
+
}
|
53
|
+
return message;
|
54
|
+
}), filter(Boolean));
|
55
|
+
}
|
56
|
+
/** Given a complete set of chunks, build a single response. */
|
57
|
+
function buildResponseFromChunks(chunks) {
|
58
|
+
const chunkPayloads = chunks.map((x) => x.payload).filter((x) => x !== undefined);
|
59
|
+
const payloadLength = chunkPayloads.reduce((acc, x) => acc + x.length, 0);
|
60
|
+
const mergedPayload = new Uint8Array(payloadLength);
|
61
|
+
for (let i = 0, offset = 0; i < chunkPayloads.length; i++) {
|
62
|
+
mergedPayload.set(chunkPayloads[i], offset);
|
63
|
+
offset += chunkPayloads[i].length;
|
64
|
+
}
|
65
|
+
const [header] = chunks.map((x) => x.header).filter((x) => x !== undefined);
|
66
|
+
if (header === undefined || Array.isArray(header)) {
|
67
|
+
throw new Error('No header found in chunks');
|
68
|
+
}
|
69
|
+
const [uid] = chunks.map((x) => x.uid).filter((x) => x !== undefined);
|
70
|
+
if (uid === undefined || Array.isArray(uid)) {
|
71
|
+
throw new Error('No uid found in chunks');
|
72
|
+
}
|
73
|
+
const createdAt = new Date(Number(header.created) * 1000);
|
74
|
+
const createdNs = (BigInt(header.created) * BigInt(1e9)).toString();
|
75
|
+
return {
|
76
|
+
uid,
|
77
|
+
return: {
|
78
|
+
header: {
|
79
|
+
createdNs,
|
80
|
+
createdAt,
|
81
|
+
meta: header.meta,
|
82
|
+
},
|
83
|
+
payload: mergedPayload,
|
84
|
+
},
|
85
|
+
};
|
86
|
+
}
|
@@ -2,9 +2,14 @@ import { IRocosCallerMessageResponseAck } from './IRocosCallerMessageResponseAck
|
|
2
2
|
import { IRocosCallerMessageResponseResult } from './IRocosCallerMessageResponseResult';
|
3
3
|
import { IRocosCallerMessageResponseUid } from './IRocosCallerMessageResponseUid';
|
4
4
|
export interface IRocosCallerMessageChunk {
|
5
|
+
uid?: IRocosCallerMessageResponseUid;
|
5
6
|
chunkIndex: number;
|
6
7
|
chunkCount: number;
|
7
|
-
|
8
|
+
header?: {
|
9
|
+
created: string;
|
10
|
+
meta: Record<string, string>;
|
11
|
+
};
|
8
12
|
ack?: IRocosCallerMessageResponseAck;
|
9
13
|
result?: IRocosCallerMessageResponseResult;
|
14
|
+
payload?: Uint8Array;
|
10
15
|
}
|
package/models/caller/index.d.ts
CHANGED
@@ -4,6 +4,7 @@ export * from './IRocosCallerMessageHeartbeat';
|
|
4
4
|
export * from './IRocosCallerMessageResponse';
|
5
5
|
export * from './IRocosCallerMessageResponseAck';
|
6
6
|
export * from './IRocosCallerMessageResponseResult';
|
7
|
+
export * from './IRocosCallerMessageResponseReturn';
|
7
8
|
export * from './IRocosCallerMessageResponses';
|
8
9
|
export * from './IRocosCallerMessageResponseUid';
|
9
10
|
export * from './RocosCallerResultStatus';
|
package/models/caller/index.js
CHANGED
@@ -4,6 +4,7 @@ export * from './IRocosCallerMessageHeartbeat';
|
|
4
4
|
export * from './IRocosCallerMessageResponse';
|
5
5
|
export * from './IRocosCallerMessageResponseAck';
|
6
6
|
export * from './IRocosCallerMessageResponseResult';
|
7
|
+
export * from './IRocosCallerMessageResponseReturn';
|
7
8
|
export * from './IRocosCallerMessageResponses';
|
8
9
|
export * from './IRocosCallerMessageResponseUid';
|
9
10
|
export * from './RocosCallerResultStatus';
|
@@ -14,3 +14,13 @@ export interface ICallerInvokeParams extends ICallerParams {
|
|
14
14
|
payload: string;
|
15
15
|
query?: Record<string, string[]>;
|
16
16
|
}
|
17
|
+
export interface ICallerCallParams {
|
18
|
+
projectId: string;
|
19
|
+
callsign: string;
|
20
|
+
source: string;
|
21
|
+
payload?: unknown;
|
22
|
+
options?: {
|
23
|
+
uid?: string;
|
24
|
+
query?: Record<string, string[]>;
|
25
|
+
};
|
26
|
+
}
|
package/package.json
CHANGED
@@ -1,10 +1,42 @@
|
|
1
|
-
import { ICallerInvokeParams, ICallerParams, ICallerStream, IRocosCallerMessage, IRocosSDKConfig, IStreamConfig } from '../models';
|
1
|
+
import { ICallerCallParams, ICallerInvokeParams, ICallerParams, ICallerStream, IRocosCallerMessage, IRocosCallerMessageResponseAck, IRocosCallerMessageResponseResult, IRocosCallerMessageResponseReturn, IRocosSDKConfig, IStreamConfig } from '../models';
|
2
2
|
import { Observable } from 'rxjs';
|
3
3
|
import { BaseStreamService } from './BaseStreamService';
|
4
4
|
export declare class CallerService extends BaseStreamService<ICallerStream> {
|
5
5
|
constructor(config: IRocosSDKConfig);
|
6
6
|
invokeRequest(params: ICallerInvokeParams): Observable<IRocosCallerMessage>;
|
7
7
|
cancelRequest(params: ICallerParams): Observable<IRocosCallerMessage>;
|
8
|
+
/** Call a service and return the response(s).
|
9
|
+
*
|
10
|
+
* It will complete once a result message is received with a `COMPLETE_SUCCESS` status,
|
11
|
+
* or throw an error if the status is not `COMPLETE_SUCCESS`.
|
12
|
+
*
|
13
|
+
* This is a high level method that wraps the lower level `invokeRequest` method.
|
14
|
+
*
|
15
|
+
* @see invokeRequest
|
16
|
+
* @see call
|
17
|
+
*/
|
18
|
+
callRaw(params: ICallerCallParams): {
|
19
|
+
return$: Observable<IRocosCallerMessageResponseReturn>;
|
20
|
+
result$: Observable<IRocosCallerMessageResponseResult>;
|
21
|
+
ack$: Observable<IRocosCallerMessageResponseAck>;
|
22
|
+
};
|
23
|
+
/** Call a service and return the response(s) as UTF-8 encoded JSON.
|
24
|
+
*
|
25
|
+
* It will complete once a result message is received with a `COMPLETE_SUCCESS` status,
|
26
|
+
* or throw an error if the status is not `COMPLETE_SUCCESS`.
|
27
|
+
*
|
28
|
+
* This is a high level method that wraps the lower level `invokeRequest` method.
|
29
|
+
*
|
30
|
+
* Equivalent to calling `callRaw` and then parsing the return payload as UTF-8 encoded JSON.
|
31
|
+
*
|
32
|
+
* @see callRaw
|
33
|
+
* @see invokeRequest
|
34
|
+
*/
|
35
|
+
call<T = unknown>(params: ICallerCallParams): {
|
36
|
+
return$: Observable<T>;
|
37
|
+
result$: Observable<IRocosCallerMessageResponseResult>;
|
38
|
+
ack$: Observable<IRocosCallerMessageResponseAck>;
|
39
|
+
};
|
8
40
|
protected createStream(): Promise<ICallerStream>;
|
9
41
|
protected getStream(config: IStreamConfig): ICallerStream;
|
10
42
|
}
|
@@ -1,8 +1,11 @@
|
|
1
|
-
import {
|
1
|
+
import { ResultStatus, RocosResponseLevel, } from '../models';
|
2
|
+
import { Subject, map, mergeMap, take, takeUntil } from 'rxjs';
|
3
|
+
import { filter, finalize } from 'rxjs/operators';
|
4
|
+
import { getResponses, handleChunkedMessages } from '../helpers/callerMessageHelpers';
|
2
5
|
import { BaseStreamService } from './BaseStreamService';
|
3
6
|
import { CallerStream } from '../api/streams/caller/CallerStream';
|
4
7
|
import { IDENTIFIER_NAME_CALLER } from '../constants/identifier';
|
5
|
-
import {
|
8
|
+
import { v4 } from 'uuid';
|
6
9
|
export class CallerService extends BaseStreamService {
|
7
10
|
constructor(config) {
|
8
11
|
super('CallerService', config);
|
@@ -37,6 +40,67 @@ export class CallerService extends BaseStreamService {
|
|
37
40
|
subscription?.unsubscribe();
|
38
41
|
}));
|
39
42
|
}
|
43
|
+
/** Call a service and return the response(s).
|
44
|
+
*
|
45
|
+
* It will complete once a result message is received with a `COMPLETE_SUCCESS` status,
|
46
|
+
* or throw an error if the status is not `COMPLETE_SUCCESS`.
|
47
|
+
*
|
48
|
+
* This is a high level method that wraps the lower level `invokeRequest` method.
|
49
|
+
*
|
50
|
+
* @see invokeRequest
|
51
|
+
* @see call
|
52
|
+
*/
|
53
|
+
callRaw(params) {
|
54
|
+
const [component, ...topicItems] = params.source.replace(/^\/+/, '').split('/');
|
55
|
+
const topic = topicItems.join('/');
|
56
|
+
const payloadString = JSON.stringify(params.payload ?? {});
|
57
|
+
const source$ = this.invokeRequest({
|
58
|
+
uid: params.options?.uid ?? v4(),
|
59
|
+
projectId: params.projectId,
|
60
|
+
callsign: params.callsign,
|
61
|
+
subSystem: '',
|
62
|
+
component,
|
63
|
+
topic,
|
64
|
+
responseLevelNumber: RocosResponseLevel.ALL,
|
65
|
+
payload: payloadString,
|
66
|
+
query: params.options?.query,
|
67
|
+
}).pipe(mergeMap(getResponses), handleChunkedMessages());
|
68
|
+
const result$ = source$.pipe(filter((x) => x.result !== undefined), map((x) => x.result), take(1));
|
69
|
+
const resultNotifier$ = result$.pipe(map((x) => {
|
70
|
+
if (x.status !== ResultStatus.COMPLETE_SUCCESS)
|
71
|
+
throw x;
|
72
|
+
return x;
|
73
|
+
}));
|
74
|
+
const return$ = source$.pipe(filter((x) => x.return !== undefined), map((x) => x.return), takeUntil(resultNotifier$));
|
75
|
+
const ack$ = source$.pipe(filter((x) => x.ack !== undefined), map((x) => x.ack), takeUntil(resultNotifier$));
|
76
|
+
return {
|
77
|
+
return$,
|
78
|
+
result$,
|
79
|
+
ack$,
|
80
|
+
};
|
81
|
+
}
|
82
|
+
/** Call a service and return the response(s) as UTF-8 encoded JSON.
|
83
|
+
*
|
84
|
+
* It will complete once a result message is received with a `COMPLETE_SUCCESS` status,
|
85
|
+
* or throw an error if the status is not `COMPLETE_SUCCESS`.
|
86
|
+
*
|
87
|
+
* This is a high level method that wraps the lower level `invokeRequest` method.
|
88
|
+
*
|
89
|
+
* Equivalent to calling `callRaw` and then parsing the return payload as UTF-8 encoded JSON.
|
90
|
+
*
|
91
|
+
* @see callRaw
|
92
|
+
* @see invokeRequest
|
93
|
+
*/
|
94
|
+
call(params) {
|
95
|
+
const stream = this.callRaw(params);
|
96
|
+
return {
|
97
|
+
...stream,
|
98
|
+
return$: stream.return$.pipe(map((x) => {
|
99
|
+
const decoded = new TextDecoder().decode(x.payload);
|
100
|
+
return JSON.parse(decoded);
|
101
|
+
})),
|
102
|
+
};
|
103
|
+
}
|
40
104
|
async createStream() {
|
41
105
|
return (await this.createStreamFromConfig(IDENTIFIER_NAME_CALLER, {
|
42
106
|
url: this.config.url,
|
@@ -0,0 +1 @@
|
|
1
|
+
export {};
|
@@ -0,0 +1,227 @@
|
|
1
|
+
import { AckStatus, ResultStatus, Stage, UIDVersion, } from '../models';
|
2
|
+
import { concatMap, delay, from, of } from 'rxjs';
|
3
|
+
import { CallerService } from './CallerService';
|
4
|
+
import { TestScheduler } from 'rxjs/testing';
|
5
|
+
const makeCallerMessage = (responses) => ({
|
6
|
+
responses: {
|
7
|
+
responses: responses ?? [],
|
8
|
+
},
|
9
|
+
});
|
10
|
+
const makeCallerChunkedMessage = (responses) => {
|
11
|
+
return responses.map((response, index) => {
|
12
|
+
if ('return' in response) {
|
13
|
+
throw new Error('Cannot create chunked message with return');
|
14
|
+
}
|
15
|
+
const chunk = response;
|
16
|
+
chunk.chunkIndex = index;
|
17
|
+
chunk.chunkCount = responses.length;
|
18
|
+
chunk.header = {
|
19
|
+
created: '0',
|
20
|
+
meta: {},
|
21
|
+
};
|
22
|
+
return {
|
23
|
+
chunks: {
|
24
|
+
chunks: [chunk],
|
25
|
+
},
|
26
|
+
};
|
27
|
+
});
|
28
|
+
};
|
29
|
+
const uid = {
|
30
|
+
hash: 'hash',
|
31
|
+
version: UIDVersion.HEADER_HASH_RAND,
|
32
|
+
};
|
33
|
+
const ack = (id) => ({
|
34
|
+
uid,
|
35
|
+
ack: {
|
36
|
+
status: AckStatus.PROGRESS,
|
37
|
+
message: id,
|
38
|
+
stage: Stage.AGENT,
|
39
|
+
},
|
40
|
+
});
|
41
|
+
const ret = (id) => {
|
42
|
+
const encoder = new TextEncoder();
|
43
|
+
const payload = encoder.encode(JSON.stringify({ id }));
|
44
|
+
return {
|
45
|
+
uid,
|
46
|
+
return: {
|
47
|
+
payload,
|
48
|
+
},
|
49
|
+
};
|
50
|
+
};
|
51
|
+
const chunkRet = (payload) => {
|
52
|
+
const array = new TextEncoder().encode(payload);
|
53
|
+
return {
|
54
|
+
uid,
|
55
|
+
payload: array,
|
56
|
+
chunkIndex: 0,
|
57
|
+
chunkCount: 1,
|
58
|
+
header: {
|
59
|
+
created: '0',
|
60
|
+
meta: {},
|
61
|
+
},
|
62
|
+
};
|
63
|
+
};
|
64
|
+
const res = (ok) => ({
|
65
|
+
uid,
|
66
|
+
result: {
|
67
|
+
status: ok ? ResultStatus.COMPLETE_SUCCESS : ResultStatus.FATAL,
|
68
|
+
message: ok ? 'everything is fine' : 'nothing is fine',
|
69
|
+
},
|
70
|
+
});
|
71
|
+
let testScheduler;
|
72
|
+
let service;
|
73
|
+
describe('CallerService', () => {
|
74
|
+
beforeEach(() => {
|
75
|
+
service = new CallerService({
|
76
|
+
url: 'http://localhost:8080',
|
77
|
+
token: 'token',
|
78
|
+
});
|
79
|
+
testScheduler = new TestScheduler((actual, expected) => {
|
80
|
+
expect(actual).toEqual(expected);
|
81
|
+
});
|
82
|
+
});
|
83
|
+
describe('callRaw', () => {
|
84
|
+
it('should handle a successful call', () => {
|
85
|
+
// Arrange
|
86
|
+
jest.spyOn(service, 'invokeRequest').mockImplementation(() => {
|
87
|
+
return from([
|
88
|
+
makeCallerMessage([ack('a')]),
|
89
|
+
makeCallerMessage([ack('b')]),
|
90
|
+
makeCallerMessage([ack('c'), ret(1)]),
|
91
|
+
makeCallerMessage([ret(2)]),
|
92
|
+
makeCallerMessage([ret(3), ret(4)]),
|
93
|
+
makeCallerMessage([ack('d')]),
|
94
|
+
makeCallerMessage([ack('e')]),
|
95
|
+
makeCallerMessage([ack('f')]),
|
96
|
+
makeCallerMessage([ack('g'), res(true)]),
|
97
|
+
makeCallerMessage([ack('h')]),
|
98
|
+
]).pipe(concatMap((item) => of(item).pipe(delay(1))));
|
99
|
+
});
|
100
|
+
testScheduler.run(({ expectObservable }) => {
|
101
|
+
// Act
|
102
|
+
const call = service.call({
|
103
|
+
callsign: 'callsign',
|
104
|
+
projectId: 'project',
|
105
|
+
source: '/test',
|
106
|
+
});
|
107
|
+
// Assert
|
108
|
+
const expectedAckMarbles = '-abc--def|';
|
109
|
+
const expectedRetMarbles = '---ab(cd)|';
|
110
|
+
const expectedResMarbles = '---------(a|)';
|
111
|
+
const expectedAckValues = {
|
112
|
+
a: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'a' },
|
113
|
+
b: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'b' },
|
114
|
+
c: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'c' },
|
115
|
+
d: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'd' },
|
116
|
+
e: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'e' },
|
117
|
+
f: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'f' },
|
118
|
+
};
|
119
|
+
const expectedReturnValues = {
|
120
|
+
a: { id: 1 },
|
121
|
+
b: { id: 2 },
|
122
|
+
c: { id: 3 },
|
123
|
+
d: { id: 4 },
|
124
|
+
};
|
125
|
+
const expectedResultValues = {
|
126
|
+
a: { status: ResultStatus.COMPLETE_SUCCESS, message: 'everything is fine' },
|
127
|
+
};
|
128
|
+
expectObservable(call.ack$).toBe(expectedAckMarbles, expectedAckValues);
|
129
|
+
expectObservable(call.return$).toBe(expectedRetMarbles, expectedReturnValues);
|
130
|
+
expectObservable(call.result$).toBe(expectedResMarbles, expectedResultValues);
|
131
|
+
});
|
132
|
+
});
|
133
|
+
it('should handle a failed call', () => {
|
134
|
+
// Arrange
|
135
|
+
jest.spyOn(service, 'invokeRequest').mockImplementation(() => {
|
136
|
+
return from([
|
137
|
+
makeCallerMessage([ack('a')]),
|
138
|
+
makeCallerMessage([ack('b')]),
|
139
|
+
makeCallerMessage([ack('c'), ret(1)]),
|
140
|
+
makeCallerMessage([ret(2)]),
|
141
|
+
makeCallerMessage([ret(3), ret(4)]),
|
142
|
+
makeCallerMessage([ack('d')]),
|
143
|
+
makeCallerMessage([ack('e')]),
|
144
|
+
makeCallerMessage([ack('f')]),
|
145
|
+
makeCallerMessage([ack('g'), res(false)]),
|
146
|
+
makeCallerMessage([ack('h')]),
|
147
|
+
]).pipe(concatMap((item) => of(item).pipe(delay(1))));
|
148
|
+
});
|
149
|
+
testScheduler.run(({ expectObservable }) => {
|
150
|
+
// Act
|
151
|
+
const call = service.call({
|
152
|
+
callsign: 'callsign',
|
153
|
+
projectId: 'project',
|
154
|
+
source: '/test',
|
155
|
+
});
|
156
|
+
// Assert
|
157
|
+
const expectedAckMarbles = '-abc--def#';
|
158
|
+
const expectedRetMarbles = '---ab(cd)#';
|
159
|
+
const expectedResMarbles = '---------(a|)';
|
160
|
+
const expectedAckValues = {
|
161
|
+
a: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'a' },
|
162
|
+
b: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'b' },
|
163
|
+
c: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'c' },
|
164
|
+
d: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'd' },
|
165
|
+
e: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'e' },
|
166
|
+
f: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'f' },
|
167
|
+
};
|
168
|
+
const expectedReturnValues = {
|
169
|
+
a: { id: 1 },
|
170
|
+
b: { id: 2 },
|
171
|
+
c: { id: 3 },
|
172
|
+
d: { id: 4 },
|
173
|
+
};
|
174
|
+
const expectedResultValues = {
|
175
|
+
a: { status: ResultStatus.FATAL, message: 'nothing is fine' },
|
176
|
+
};
|
177
|
+
expectObservable(call.ack$).toBe(expectedAckMarbles, expectedAckValues, res(false).result);
|
178
|
+
expectObservable(call.return$).toBe(expectedRetMarbles, expectedReturnValues, res(false).result);
|
179
|
+
expectObservable(call.result$).toBe(expectedResMarbles, expectedResultValues);
|
180
|
+
});
|
181
|
+
});
|
182
|
+
it('should handle a successful chunked call', () => {
|
183
|
+
// Arrange
|
184
|
+
jest.spyOn(service, 'invokeRequest').mockImplementation(() => {
|
185
|
+
return from([
|
186
|
+
...makeCallerChunkedMessage([
|
187
|
+
chunkRet('[{"'),
|
188
|
+
chunkRet('id":1},'),
|
189
|
+
ack('a'),
|
190
|
+
chunkRet('{"id":2}'),
|
191
|
+
ack('b'),
|
192
|
+
chunkRet(']'),
|
193
|
+
]),
|
194
|
+
makeCallerMessage([ack('c'), ret(3)]),
|
195
|
+
makeCallerMessage([res(true)]),
|
196
|
+
]).pipe(concatMap((item) => of(item).pipe(delay(1))));
|
197
|
+
});
|
198
|
+
testScheduler.run(({ expectObservable }) => {
|
199
|
+
// Act
|
200
|
+
const call = service.call({
|
201
|
+
callsign: 'callsign',
|
202
|
+
projectId: 'project',
|
203
|
+
source: '/test',
|
204
|
+
});
|
205
|
+
// Assert
|
206
|
+
const expectedAckMarbles = '---a-b-c|';
|
207
|
+
const expectedRetMarbles = '------ab|';
|
208
|
+
const expectedResMarbles = '--------(a|)';
|
209
|
+
const expectedAckValues = {
|
210
|
+
a: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'a' },
|
211
|
+
b: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'b' },
|
212
|
+
c: { stage: Stage.AGENT, status: AckStatus.PROGRESS, message: 'c' },
|
213
|
+
};
|
214
|
+
const expectedReturnValues = {
|
215
|
+
a: [{ id: 1 }, { id: 2 }],
|
216
|
+
b: { id: 3 },
|
217
|
+
};
|
218
|
+
const expectedResultValues = {
|
219
|
+
a: { status: ResultStatus.COMPLETE_SUCCESS, message: 'everything is fine' },
|
220
|
+
};
|
221
|
+
expectObservable(call.ack$).toBe(expectedAckMarbles, expectedAckValues);
|
222
|
+
expectObservable(call.return$).toBe(expectedRetMarbles, expectedReturnValues);
|
223
|
+
expectObservable(call.result$).toBe(expectedResMarbles, expectedResultValues);
|
224
|
+
});
|
225
|
+
});
|
226
|
+
});
|
227
|
+
});
|