I'm trying to use rxjs in conjunction with babeljs to create an async generator function that yields when next is called, throws when error is called, and finishes when complete is called. The problem I have with this is that I can't yield from a callback.
I can await a Promise to handle the return/throw requirement.
async function *getData( observable ) {
await new Promise( ( resolve, reject ) => {
observable.subscribe( {
next( data ) {
yield data; // can't yield here
},
error( err ) {
reject( err );
},
complete() {
resolve();
}
} );
} );
}
( async function example() {
for await( const data of getData( foo ) ) {
console.log( 'data received' );
}
console.log( 'done' );
}() );
Is this possible?
I asked the rubber duck, then I wrote the following code which does what I wanted:
function defer() {
const properties = {},
promise = new Promise( ( resolve, reject ) => {
Object.assign( properties, { resolve, reject } );
} );
return Object.assign( promise, properties );
}
async function *getData( observable ) {
let nextData = defer();
const sub = observable.subscribe( {
next( data ) {
const n = nextData;
nextData = defer();
n.resolve( data );
},
error( err ) {
nextData.reject( err );
},
complete() {
const n = nextData;
nextData = null;
n.resolve();
}
} );
try {
for(;;) {
const value = await nextData;
if( !nextData ) break;
yield value;
}
} finally {
sub.unsubscribe();
}
}
I think a problem with this solution is that the observable could generate several values in one batch (without deferring). This is my proposal:
const defer = () => new Promise (resolve =>
setTimeout (resolve, 0));
async function* getData (observable)
{
let values = [];
let error = null;
let done = false;
observable.subscribe (
data => values.push (data),
err => error = err,
() => done = true);
for (;;)
{
if (values.length)
{
for (const value of values)
yield value;
values = [];
}
if (error)
throw error;
if (done)
return;
await defer ();
}
}
Related
I am new to Angular and i am facing some difficulties with a task. I have an array of IDs that i want to execute the same GET Call over. And for every GET call result i have to do some operations and then add the result of every operation to some arrays. I managed to find a way to do it correctly. But my problem is, i can't manage to wait for the final result to be ready (after all the GET calls are done and the operations too) before giving it as an argument to another method that will send it with a POST call.
the method where i do the GET calls and the operations over every call's result (the problem occurs when i am in the rollBackSPN condition).
async getComponentIds(taskName: String, selectedComponents: IComponent[]) {
const componentsId: number[] = [];
const componentsWithoutParams: IComponent[] = [];
let sendPortaPrecedente : boolean;
if(taskName == "rollBackSPN"){
from(selectedComponents).pipe(
concatMap(component =>{
return this.http.get<any>("Url"+component.idComponent).pipe(
tap(val => {
sendPortaPrecedente = true;
for(const obj of val){
if((obj.name == "z0bpqPrevious" && obj.value == null) || (obj.name == "datePortaPrevious" && obj.value == null) || (obj.name == "typePortaPrevious" && obj.value == null)){
sendPortaPrecedente = false;
}
}
if(sendPortaPrecedente){
componentsId.push(component.idComponent);
}else{
componentsWithoutParams.push(component);
}
}),
catchError(err => {
return of(err);
})
)
})
).subscribe(val => {
return { componentsId : componentsId, componentsWithoutParams : componentsWithoutParams, sendPortaPrecedente : sendPortaPrecedente};
});
}else{
for (const component of selectedComponents) {
componentsId.push(component.idComponent)
return { componentsId : componentsId, componentsWithoutParams : componentsWithoutParams, sendPortaPrecedente : sendPortaPrecedente};
}
}
}
The method where i pass the getComponentIds(taskName: String, selectedComponents: IComponent[]) result so it can be send with a POST call (again when i am in the rollBackSPN condition)
executeTask(serviceIdSi: string, actionIdSi: string, actionClassName: string, componentName: string, taskName: string,
componentsId: number[], componentsWithoutParams: IComponent[], sendPortaPrecedente: boolean): Observable<any> {
const url = this.taskUrl + `?serviceId=${serviceIdSi}` + `&actionId=${actionIdSi}` + `&actionClassName=${actionClassName}`
+ `&componentName=${componentName}` + `&taskName=${taskName}`;
if(taskName == "rollBackSPN"){
if(sendPortaPrecedente && componentsWithoutParams.length == 0){
return this.http.post<any>(url, componentsId);
}else{
let errMessage = "Some Error Message"
for(const component of componentsWithoutParams){
errMessage = errMessage + component.idComponent +"\n";
}
throw throwError(errMessage);
}
}else{
return this.http.post<any>(url, componentsId);
}
}
Both these methods are defined in a service called TaskService.
And the service is called like this in a component UnitTaskButtonsComponent.
async launchUnitTask() {
this.isLoading = true;
this.isClosed = false;
this.appComponent.currentComponentIndex = this.componentIndex;
let res = await this.taskService.getComponentIds(this.unitTaskLabel, this.selectedComponents);
this.taskService.executeTask(this.appComponent.currentService.identifiantSi,
this.appComponent.currentAction.identifiantSi,
this.appComponent.currentAction.className,
this.selectedComponents[0].name,
this.unitTaskLabel,
res.componentsId,
res.componentsWithoutParams,
res.sendPortaPrecedente).subscribe(
data => this.executeTaskSuccess(),
error => this.executeTaskError());
}
"res" properties are always undefined when it's a rollBackSPN task.
The main issue here is that getComponentIds does not return a Promise. So awaiting does not work. I would suggest to change getComponentIds so that it returns an Observable instead.
getComponentIds(taskName: string, selectedComponents: IComponent[]) {
// ^^^^^^ use string instead of String
return forkJoin(
selectedComponents.map((component) => {
return this.http.get<any>("Url" + component.idComponent).pipe(
map((val) => {
let sendPortaPrecedente = true;
for (const obj of val) {
if (
(obj.name == "z0bpqPrevious" && obj.value == null) ||
(obj.name == "datePortaPrevious" && obj.value == null) ||
(obj.name == "typePortaPrevious" && obj.value == null)
) {
sendPortaPrecedente = false;
}
}
return { component, sendPortaPrecedente }
}),
catchError((err) => of(err))
);
})
).pipe(
map((result) => {
const componentsId: number[] = [];
const componentsWithoutParams: IComponent[] = [];
for (const val of result) {
if (val.sendPortaPrecedente) {
componentsId.push(val.component.idComponent);
} else {
componentsWithoutParams.push(val.component);
}
}
return { componentsId, componentsWithoutParams };
})
);
}
Instead of using concatMap, let's use a forkJoin. The forkJoin allows sending all requests in parallel and returns the result in an array. But we have to pass in an array of Observables. That's why we map over the selectedComponents.
In the lower map, we can now get the complete result of the http calls in the result parameter. Here we do the processing of the data. I was not really sure how to handle the sendPortaPrecedente. You will have to fill that in.
We simply return the whole Observable
async launchUnitTask() {
this.taskService
.getComponentIds(this.unitTaskLabel, this.selectedComponents)
.pipe(
switchMap((res) => {
this.taskService
.executeTask(
this.appComponent.currentService.identifiantSi,
this.appComponent.currentAction.identifiantSi,
this.appComponent.currentAction.className,
this.selectedComponents[0].name,
this.unitTaskLabel,
res.componentsId,
res.componentsWithoutParams,
res.sendPortaPrecedente
)
})
).subscribe(
(data) => this.executeTaskSuccess(),
(error) => this.executeTaskError()
);
}
In the launchUnitTask method, we don't use await anymore. Instead, we call getComponentIds and chain the call of executeTask with a switchMap.
Configured my store this way with redux toolkit for sure
const rootReducer = combineReducers({
someReducer,
systemsConfigs
});
const store = return configureStore({
devTools: true,
reducer: rootReducer ,
// middleware: [middleware, logger],
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(middleware),
});
middleware.run(sagaRoot)
And thats my channel i am connecting to it
export function createSocketChannel(
productId: ProductId,
pair: string,
createSocket = () => new WebSocket('wss://somewebsocket')
) {
return eventChannel<SocketEvent>((emitter) => {
const socket_OrderBook = createSocket();
socket_OrderBook.addEventListener('open', () => {
emitter({
type: 'connection-established',
payload: true,
});
socket_OrderBook.send(
`subscribe-asdqwe`
);
});
socket_OrderBook.addEventListener('message', (event) => {
if (event.data?.includes('bids')) {
emitter({
type: 'message',
payload: JSON.parse(event.data),
});
//
}
});
socket_OrderBook.addEventListener('close', (event: any) => {
emitter(new SocketClosedByServer());
});
return () => {
if (socket_OrderBook.readyState === WebSocket.OPEN) {
socket_OrderBook.send(
`unsubscribe-order-book-${pair}`
);
}
if (socket_OrderBook.readyState === WebSocket.OPEN || socket_OrderBook.readyState === WebSocket.CONNECTING) {
socket_OrderBook.close();
}
};
}, buffers.expanding<SocketEvent>());
}
And here's how my saga connecting handlers looks like
export function* handleConnectingSocket(ctx: SagaContext) {
try {
const productId = yield select((state: State) => state.productId);
const requested_pair = yield select((state: State) => state.requested_pair);
if (ctx.socketChannel === null) {
ctx.socketChannel = yield call(createSocketChannel, productId, requested_pair);
}
//
const message: SocketEvent = yield take(ctx.socketChannel!);
if (message.type !== 'connection-established') {
throw new SocketUnexpectedResponseError();
}
yield put(connectedSocket());
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.BAD_CONNECTION,
})
);
}
}
export function* handleConnectedSocket(ctx: SagaContext) {
try {
while (true) {
if (ctx.socketChannel === null) {
break;
}
const events = yield flush(ctx.socketChannel);
const startedExecutingAt = performance.now();
if (Array.isArray(events)) {
const deltas = events.reduce(
(patch, event) => {
if (event.type === 'message') {
patch.bids.push(...event.payload.data?.bids);
patch.asks.push(...event.payload.data?.asks);
//
}
//
return patch;
},
{ bids: [], asks: [] } as SocketMessage
);
if (deltas.bids.length || deltas.asks.length) {
yield putResolve(receivedDeltas(deltas));
}
}
yield call(delayNextDispatch, startedExecutingAt);
}
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.UNKNOWN,
})
);
}
}
After Debugging I got the following:
The Thing is that when I Provide one Reducer to my store the channel works well and data is fetched where as when providing combinedReducers I am getting
an established connection from my handleConnectingSocket generator function
and an empty event array [] from
const events = yield flush(ctx.socketChannel) written in handleConnectedSocket
Tried to clarify as much as possible
ok so I start refactoring my typescript by changing the types, then saw all the places that break, there was a problem in my sagas.tsx.
Ping me if someone faced such an issue in the future
Here's my code first
const [getData, setGetData] = useState();
const [ref, setRef] = useState();
const initializeData = async() => {
const userToken = await AsyncStorage.getItem('user_id');
setGetData(JSON.parse(userToken));
}
useEffect(() => {
return initializeData();
},[])
useEffect(() => {
let interval;
if(getData != null)
{
interval = setInterval(() => {
setRef(firestore().collection('**********').where("SendersNo", "==", getData.number));
}, 2000);
}
return () => clearInterval(interval);
},[getData])
useEffect(() => {
if(ref != null)
{
return ref.onSnapshot(querySnapshot => {
const list = [];
querySnapshot.forEach(doc => {
const {
id,driverName,driverContactNumber,driverRating,driverPlateNumber,driverTrackingNumber,userPlaceName,
destinationPlaceName,PaymentMethod,Fare
} = doc.data();
list.push({id: doc.id,driverName,driverContactNumber,driverRating,
driverPlateNumber,driverTrackingNumber,userPlaceName,destinationPlaceName,PaymentMethod,Fare});
});
setUserBookingData(list);
console.log("HEY!");
});
}
},[])
const CurrentTransaction = () => {
if(ref == null)
{
return (
<View>
<Text>You don't have a Current Transaction</Text>
</View>
)
}
else
{
return userBookingData.map((element) => {
return (
<View key={element.id}>
<View>
<Text>{element.name}</Text>
</View>
</View>
)
});
}
}
So currently right now what I am trying to is if there's a data on my firestore it will update on the screen but before updating it I need to get the data from the setGetData so that I can query it but the problem is that when I refresh the whole simulator/page it doesn't get the data but instead just a blank page . But when i edit and save my code without refreshing the page/simulator it can get the data . Can someone help me what I am doing wrong .
EDIT
if I do this
useEffect(() => {
if(ref != null)
{
return ref.onSnapshot(querySnapshot => {
const list = [];
querySnapshot.forEach(doc => {
const {
id,driverName,driverContactNumber,driverRating,driverPlateNumber,driverTrackingNumber,userPlaceName,
destinationPlaceName,PaymentMethod,Fare
} = doc.data();
list.push({id: doc.id,driverName,driverContactNumber,driverRating,
driverPlateNumber,driverTrackingNumber,userPlaceName,destinationPlaceName,PaymentMethod,Fare});
});
setUserBookingData(list);
console.log("HEY!");
});
}
else
{
return null;
}
},[ref])
it keeps looping the console.log('hey') but it can get the data and display it . but it loops so its bad.
i believe snapshot from firebase realtime database is a listener so its doesn't need setinterval
useEffect(() => {
if(getData != null)
{
const ref = firestore().collection('**********').where("SendersNo", "==", getData.number);
ref.onSnapshot(querySnapshot => {
const list = [];
querySnapshot.forEach(doc => {
const {
id,driverName,driverContactNumber,driverRating,driverPlateNumber,driverTrackingNumber,userPlaceName,
destinationPlaceName,PaymentMethod,Fare
} = doc.data();
list.push({id: doc.id,driverName,driverContactNumber,driverRating,
driverPlateNumber,driverTrackingNumber,userPlaceName,destinationPlaceName,PaymentMethod,Fare});
});
setUserBookingData(list);
console.log("HEY!");
});
}
return () => {
//clear your ref listener here
}
},[getData])
if you put a return on use effect it will be called after the screen is no longer used.
useEffect(()=>{
//inside this will be called when the screen complete render
const someListener = DeviceEventEmitter('listentosomething',()=>{
//do something
});
return ()=>{
//inside this will be called after the screen no longer be used
//example go to other screen
someListener.remove();
}
},)
I'm using the Google Storage NodeJS client library to list GCS Bucket paths.
Here's the code to the Firebase Function:
import * as functions from 'firebase-functions';
import { Storage } from '#google-cloud/storage';
import { globVars } from '../admin/admin';
const projectId = process.env.GCLOUD_PROJECT;
// shared global variables setup
const { keyFilename } = globVars;
// Storage set up
const storage = new Storage({
projectId,
keyFilename,
});
export const gcsListPath = functions
.region('europe-west2')
.runWith({ timeoutSeconds: 540, memory: '256MB' })
.https.onCall(async (data, context) => {
if (context.auth?.token.email_verified) {
const { bucketName, prefix, pathList = false, fileList = false } = data;
let list;
const options = {
autoPaginate: false,
delimiter: '',
prefix,
};
if (pathList) {
options.delimiter = '/';
let test: any[] = [];
const callback = (_err: any, _files: any, nextQuery: any, apiResponse: any) => {
test = test.concat(apiResponse.prefixes);
console.log('test : ', test);
console.log('nextQuery : ', nextQuery);
if (nextQuery) {
storage.bucket(bucketName).getFiles(nextQuery, callback);
} else {
// prefixes = The finished array of prefixes.
list = test;
}
}
storage.bucket(bucketName).getFiles(options, callback);
}
if (fileList) {
const [files] = await storage
.bucket(bucketName)
.getFiles(options);
list = files.map((file) => file.name);
}
return { list }; //returning null as it exec before callback fns finish
} else {
return {
error: { message: 'Bad Request', status: 'INVALID_ARGUMENT' },
};
}
});
My problem is that my Firebase function returns the list (null) before all the callback functions finish execution.
Could someone spot and point out what needs to be changed/added to make the function wait for all the callback functions to finish. I've tried adding async/await but can't seem to get it right.
The reason for your error is that you use a callback. It's not awaited in the code. I would recommend to turn the callback code to a promise. Something like this.
import * as functions from "firebase-functions";
import { Storage } from "#google-cloud/storage";
import { globVars } from "../admin/admin";
const projectId = process.env.GCLOUD_PROJECT;
// shared global variables setup
const { keyFilename } = globVars;
// Storage set up
const storage = new Storage({
projectId,
keyFilename,
});
const getList = (bucketName, options) => {
return new Promise((resolve, reject) => {
let list;
let test: any[] = [];
const callback = (
_err: any,
_files: any,
nextQuery: any,
apiResponse: any
) => {
test = test.concat(apiResponse.prefixes);
console.log("test : ", test);
console.log("nextQuery : ", nextQuery);
if (nextQuery) {
storage.bucket(bucketName).getFiles(nextQuery, callback);
} else {
// prefixes = The finished array of prefixes.
list = test;
}
resolve(list);
};
try {
storage.bucket(bucketName).getFiles(options, callback);
} catch (error) {
reject(eror);
}
});
};
export const gcsListPath = functions
.region("europe-west2")
.runWith({ timeoutSeconds: 540, memory: "256MB" })
.https.onCall(async (data, context) => {
if (context.auth?.token.email_verified) {
const { bucketName, prefix, pathList = false, fileList = false } = data;
let list;
const options = {
autoPaginate: false,
delimiter: "",
prefix,
};
if (pathList) {
options.delimiter = "/";
list = await getList(bucketName, options);
}
if (fileList) {
const [files] = await storage.bucket(bucketName).getFiles(options);
list = files.map((file) => file.name);
}
return { list }; //returning null as it exec before callback fns finish
} else {
return {
error: { message: "Bad Request", status: "INVALID_ARGUMENT" },
};
}
});
I'm not sure if the part with fileList will work as expectedt. It looks like the API doesn't support await but only callbacks.
import * as functions from "firebase-functions";
import { GetFilesOptions, Storage } from "#google-cloud/storage";
import { globVars } from "../admin/admin";
const projectId = process.env.GCLOUD_PROJECT;
// shared global variables setup
const { keyFilename } = globVars;
// Storage set up
const storage = new Storage({
projectId,
keyFilename,
});
const getList = (bucketName: string, options: GetFilesOptions) => {
return new Promise((resolve, reject) => {
// let test: any[] = [];
let list: any[] = [];
const callback = (
_err: any,
_files: any,
nextQuery: any,
apiResponse: any
) => {
list = list.concat(apiResponse.prefixes);
console.log("list : ", list);
console.log("nextQuery : ", nextQuery);
if (nextQuery) {
storage.bucket(bucketName).getFiles(nextQuery, callback);
} else {
// prefixes = The finished array of prefixes.
resolve(list);
}
};
try {
storage.bucket(bucketName).getFiles(options, callback);
} catch (error) {
reject(error);
}
});
};
export const gcsListPath = functions
.region("europe-west2")
.runWith({ timeoutSeconds: 540, memory: "256MB" })
.https.onCall(async (data, context) => {
if (context.auth?.token.email_verified) {
const { bucketName, prefix, pathList = false, fileList = false } = data;
let list;
const options = {
autoPaginate: false,
delimiter: "",
prefix,
};
if (pathList) {
options.delimiter = "/";
list = await getList(bucketName, options);
}
if (fileList) {
const [files] = await storage.bucket(bucketName).getFiles(options);
list = files.map((file) => file.name);
}
return { list }; //returning null as it exec before callback fns finish
} else {
return {
error: { message: "Bad Request", status: "INVALID_ARGUMENT" },
};
}
});
I have a function that wraps observable with error handling, but to do so I need some code to run once it's inner observable is subscribed.
I also need that cancelling the higher Observable cancels the inner one, as it is doing HTTP call.
Context
slideshow: string[] = [];
currentIndex = 0;
private is = {
loading: new BehaviorSubject(false),
}
private loadImage(src: string): Observable;
private loadNextImage(index = this.currentIndex, preload = false): Observable<number> {
const nextIndex = (index + 1) % this.slideshow.length;
if (this.currentIndex == nextIndex) {
if (!preload) {
this.is.loading.next(false);
}
throw new Error('No other images are valid');
}
return ( possible code below )
}
Defer - This worked nicely until I realised this will create a new instance for every subscriber.
defer(() => {
if (!preload) {
this.is.loading.next(true);
}
return this.loadImage(this.slideshow[nextIndex]).pipe(
finalize(() => {
if (!preload) {
this.is.loading.next(false);
}
}),
map(() => nextIndex),
catchError(err => this.loadNextImage(nextIndex)),
);
});
Of(void 0).pipe(mergeMap(...)) - This does what is should, but it is really ugly
of(void 0).pipe(
mergeMap(() => {
if (!preload) {
this.is.loading.next(true);
}
return this.loadImage(this.slideshow[nextIndex]).pipe(
finalize(() => {
if (!preload) {
this.is.loading.next(false);
}
}),
map(() => nextIndex),
catchError(err => this.loadNextImage(nextIndex)),
);
}),
)
new Observable - I think there should be a solution that I am missing