2021-10-20 22:20:43 +03:00
|
|
|
/*
|
|
|
|
* Copyright 2021 Fluence Labs Limited
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
2022-09-05 18:24:19 +03:00
|
|
|
import 'buffer';
|
|
|
|
|
2022-08-24 18:03:06 +03:00
|
|
|
import { RelayConnection } from '@fluencelabs/connection';
|
2023-01-09 15:51:15 +03:00
|
|
|
import { FluenceConnection, IAvmRunner, IMarine } from '@fluencelabs/interfaces';
|
2022-08-24 18:03:06 +03:00
|
|
|
import { KeyPair } from '@fluencelabs/keypair';
|
2022-08-05 16:43:19 +03:00
|
|
|
import type { MultiaddrInput } from 'multiaddr';
|
2021-10-20 22:20:43 +03:00
|
|
|
import { CallServiceData, CallServiceResult, GenericCallServiceHandler, ResultCodes } from './commonTypes';
|
2021-09-08 12:42:30 +03:00
|
|
|
import { PeerIdB58 } from './commonTypes';
|
2021-11-09 14:37:44 +03:00
|
|
|
import { Particle, ParticleExecutionStage, ParticleQueueItem } from './Particle';
|
2023-01-09 15:51:15 +03:00
|
|
|
import { throwIfNotSupported, dataToString, jsonify, isString, ServiceError } from './utils';
|
2021-12-28 20:53:25 +03:00
|
|
|
import { concatMap, filter, pipe, Subject, tap } from 'rxjs';
|
2021-10-20 22:20:43 +03:00
|
|
|
import log from 'loglevel';
|
2022-02-04 22:39:41 +03:00
|
|
|
import { builtInServices } from './builtins/common';
|
|
|
|
import { defaultSigGuard, Sig } from './builtins/Sig';
|
|
|
|
import { registerSig } from './_aqua/services';
|
2022-11-03 21:22:10 +03:00
|
|
|
import { registerSrv } from './_aqua/single-module-srv';
|
2023-01-09 15:51:15 +03:00
|
|
|
import { Buffer } from 'buffer';
|
2022-08-24 18:03:06 +03:00
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
import { JSONValue } from '@fluencelabs/avm';
|
2022-11-03 21:22:10 +03:00
|
|
|
import { NodeUtils, Srv } from './builtins/SingleModuleSrv';
|
|
|
|
import { registerNodeUtils } from './_aqua/node-utils';
|
2023-01-09 15:51:15 +03:00
|
|
|
import { LogLevel } from '@fluencelabs/marine-js';
|
2021-09-08 12:42:30 +03:00
|
|
|
|
|
|
|
/**
|
2021-10-20 22:20:43 +03:00
|
|
|
* Node of the Fluence network specified as a pair of node's multiaddr and it's peer id
|
2021-09-08 12:42:30 +03:00
|
|
|
*/
|
|
|
|
type Node = {
|
|
|
|
peerId: PeerIdB58;
|
|
|
|
multiaddr: string;
|
|
|
|
};
|
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
const DEFAULT_TTL = 7000;
|
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
export type ConnectionOption = string | MultiaddrInput | Node;
|
|
|
|
|
2021-09-08 12:42:30 +03:00
|
|
|
/**
|
|
|
|
* Configuration used when initiating Fluence Peer
|
|
|
|
*/
|
|
|
|
export interface PeerConfig {
|
|
|
|
/**
|
|
|
|
* Node in Fluence network to connect to.
|
|
|
|
* Can be in the form of:
|
|
|
|
* - string: multiaddr in string format
|
|
|
|
* - Multiaddr: multiaddr object, @see https://github.com/multiformats/js-multiaddr
|
|
|
|
* - Node: node structure, @see Node
|
2022-08-05 16:43:19 +03:00
|
|
|
* - Implementation of FluenceConnection class, @see FluenceConnection
|
2021-09-08 12:42:30 +03:00
|
|
|
* If not specified the will work locally and would not be able to send or receive particles.
|
|
|
|
*/
|
2022-08-05 16:43:19 +03:00
|
|
|
connectTo?: ConnectionOption;
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2021-09-10 19:21:45 +03:00
|
|
|
/**
|
2022-04-24 10:49:57 +03:00
|
|
|
* @deprecated. AVM run through marine-js infrastructure.
|
|
|
|
* @see debug.marineLogLevel option to configure logging level of AVM
|
2021-09-10 19:21:45 +03:00
|
|
|
*/
|
2023-01-09 15:51:15 +03:00
|
|
|
avmLogLevel?: LogLevel | 'off';
|
2021-09-08 12:42:30 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Specify the KeyPair to be used to identify the Fluence Peer.
|
|
|
|
* Will be generated randomly if not specified
|
|
|
|
*/
|
|
|
|
KeyPair?: KeyPair;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When the peer established the connection to the network it sends a ping-like message to check if it works correctly.
|
|
|
|
* The options allows to specify the timeout for that message in milliseconds.
|
|
|
|
* If not specified the default timeout will be used
|
|
|
|
*/
|
|
|
|
checkConnectionTimeoutMs?: number;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When the peer established the connection to the network it sends a ping-like message to check if it works correctly.
|
|
|
|
* If set to true, the ping-like message will be skipped
|
|
|
|
* Default: false
|
|
|
|
*/
|
|
|
|
skipCheckConnection?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The dialing timeout in milliseconds
|
|
|
|
*/
|
|
|
|
dialTimeoutMs?: number;
|
2021-10-21 17:56:21 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the default TTL for all particles originating from the peer with no TTL specified.
|
|
|
|
* If the originating particle's TTL is defined then that value will be used
|
|
|
|
* If the option is not set default TTL will be 7000
|
|
|
|
*/
|
|
|
|
defaultTtlMs?: number;
|
2021-12-28 20:53:25 +03:00
|
|
|
|
2022-04-21 14:13:26 +03:00
|
|
|
/**
|
|
|
|
* This option allows to specify the location of various dependencies needed for marine-js.
|
|
|
|
* Each key specifies the location of the corresponding dependency.
|
|
|
|
* If Fluence peer is started inside browser the location is treated as the path to the file relative to origin.
|
|
|
|
* IF Fluence peer is started in nodejs the location is treated as the full path to file on the file system.
|
|
|
|
*/
|
|
|
|
marineJS?: {
|
|
|
|
/**
|
|
|
|
* Configures path to the marine-js worker script.
|
|
|
|
*/
|
|
|
|
workerScriptPath: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Configures the path to marine-js control wasm module
|
|
|
|
*/
|
|
|
|
marineWasmPath: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Configures the path to AVM wasm module
|
|
|
|
*/
|
|
|
|
avmWasmPath: string;
|
|
|
|
};
|
|
|
|
|
2022-04-06 15:16:45 +03:00
|
|
|
/**
|
|
|
|
* Enables\disabled various debugging features
|
|
|
|
*/
|
|
|
|
debug?: {
|
|
|
|
/**
|
|
|
|
* If set to true, newly initiated particle ids will be printed to console.
|
|
|
|
* Useful to see what particle id is responsible for aqua function
|
|
|
|
*/
|
|
|
|
printParticleId?: boolean;
|
2022-04-24 10:49:57 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Log level for marine services. By default logging is turned off.
|
|
|
|
*/
|
|
|
|
marineLogLevel?: LogLevel;
|
2022-04-06 15:16:45 +03:00
|
|
|
};
|
2021-09-08 12:42:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-08-05 16:43:19 +03:00
|
|
|
* Information about Fluence Peer connection.
|
|
|
|
* Represented as object with the following keys:
|
|
|
|
* - `isInitialized`: Is the peer initialized or not.
|
|
|
|
* - `peerId`: Peer Id of the peer. Null if the peer is not initialized
|
|
|
|
* - `isConnected`: Is the peer connected to network or not
|
|
|
|
* - `relayPeerId`: Peer Id of the relay the peer is connected to. If the connection is direct relayPeerId is null
|
|
|
|
* - `isDirect`: True if the peer is connected to the network directly (not through relay)
|
2021-09-08 12:42:30 +03:00
|
|
|
*/
|
2022-05-12 17:14:16 +03:00
|
|
|
export type PeerStatus =
|
|
|
|
| {
|
|
|
|
isInitialized: false;
|
|
|
|
peerId: null;
|
|
|
|
isConnected: false;
|
|
|
|
relayPeerId: null;
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
isInitialized: true;
|
|
|
|
peerId: PeerIdB58;
|
|
|
|
isConnected: false;
|
|
|
|
relayPeerId: null;
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
isInitialized: true;
|
|
|
|
peerId: PeerIdB58;
|
|
|
|
isConnected: true;
|
|
|
|
relayPeerId: PeerIdB58;
|
2022-08-05 16:43:19 +03:00
|
|
|
}
|
|
|
|
| {
|
|
|
|
isInitialized: true;
|
|
|
|
peerId: PeerIdB58;
|
|
|
|
isConnected: true;
|
|
|
|
isDirect: true;
|
|
|
|
relayPeerId: null;
|
2022-05-12 17:14:16 +03:00
|
|
|
};
|
2021-09-08 12:42:30 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* This class implements the Fluence protocol for javascript-based environments.
|
|
|
|
* It provides all the necessary features to communicate with Fluence network
|
|
|
|
*/
|
|
|
|
export class FluencePeer {
|
2023-01-09 15:51:15 +03:00
|
|
|
constructor(private marine: IMarine, private avmRunner: IAvmRunner) {}
|
|
|
|
|
2021-09-08 12:42:30 +03:00
|
|
|
/**
|
2021-09-10 19:21:45 +03:00
|
|
|
* Checks whether the object is instance of FluencePeer class
|
|
|
|
* @param obj - object to check if it is FluencePeer
|
|
|
|
* @returns true if the object is FluencePeer false otherwise
|
|
|
|
*/
|
2022-05-12 17:14:16 +03:00
|
|
|
static isInstance(obj: unknown): obj is FluencePeer {
|
|
|
|
return obj instanceof FluencePeer;
|
2021-09-10 19:21:45 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the peer's status
|
2021-09-08 12:42:30 +03:00
|
|
|
*/
|
2021-09-10 19:21:45 +03:00
|
|
|
getStatus(): PeerStatus {
|
2022-05-12 17:14:16 +03:00
|
|
|
// TODO:: use explicit mechanism for peer's state
|
|
|
|
if (this._keyPair === undefined) {
|
|
|
|
return {
|
|
|
|
isInitialized: false,
|
|
|
|
peerId: null,
|
|
|
|
isConnected: false,
|
|
|
|
relayPeerId: null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
if (this.connection === null) {
|
2022-05-12 17:14:16 +03:00
|
|
|
return {
|
|
|
|
isInitialized: true,
|
|
|
|
peerId: this._keyPair.Libp2pPeerId.toB58String(),
|
|
|
|
isConnected: false,
|
|
|
|
relayPeerId: null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
if (this.connection.relayPeerId === null) {
|
2022-08-05 16:43:19 +03:00
|
|
|
return {
|
|
|
|
isInitialized: true,
|
|
|
|
peerId: this._keyPair.Libp2pPeerId.toB58String(),
|
|
|
|
isConnected: true,
|
|
|
|
isDirect: true,
|
|
|
|
relayPeerId: null,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-09-08 12:42:30 +03:00
|
|
|
return {
|
2022-05-12 17:14:16 +03:00
|
|
|
isInitialized: true,
|
|
|
|
peerId: this._keyPair.Libp2pPeerId.toB58String(),
|
|
|
|
isConnected: true,
|
2023-01-09 15:51:15 +03:00
|
|
|
relayPeerId: this.connection.relayPeerId,
|
2021-09-08 12:42:30 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initializes the peer: starts the Aqua VM, initializes the default call service handlers
|
|
|
|
* and (optionally) connect to the Fluence network
|
|
|
|
* @param config - object specifying peer configuration
|
|
|
|
*/
|
2022-08-05 16:43:19 +03:00
|
|
|
async start(config: PeerConfig = {}): Promise<void> {
|
2022-03-31 23:37:25 +03:00
|
|
|
throwIfNotSupported();
|
2022-08-05 16:43:19 +03:00
|
|
|
const keyPair = config.KeyPair ?? (await KeyPair.randomEd25519());
|
|
|
|
const newConfig = { ...config, KeyPair: keyPair };
|
2022-03-31 23:37:25 +03:00
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
await this.init(newConfig);
|
2021-10-20 22:20:43 +03:00
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
const conn = await configToConnection(newConfig.KeyPair, config?.connectTo, config?.dialTimeoutMs);
|
2023-01-09 15:51:15 +03:00
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
if (conn !== null) {
|
|
|
|
await this.connect(conn);
|
2021-09-08 12:42:30 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-04 22:39:41 +03:00
|
|
|
getServices() {
|
2022-05-12 17:14:16 +03:00
|
|
|
if (this._classServices === undefined) {
|
|
|
|
throw new Error(`Can't get services: peer is not initialized`);
|
|
|
|
}
|
2022-02-04 22:39:41 +03:00
|
|
|
return {
|
|
|
|
...this._classServices,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-04-21 14:13:26 +03:00
|
|
|
/**
|
|
|
|
* Registers marine service within the Fluence peer from wasm file.
|
|
|
|
* Following helper functions can be used to load wasm files:
|
|
|
|
* * loadWasmFromFileSystem
|
|
|
|
* * loadWasmFromNpmPackage
|
|
|
|
* * loadWasmFromServer
|
|
|
|
* @param wasm - buffer with the wasm file for service
|
|
|
|
* @param serviceId - the service id by which the service can be accessed in aqua
|
|
|
|
*/
|
|
|
|
async registerMarineService(wasm: SharedArrayBuffer | Buffer, serviceId: string): Promise<void> {
|
2023-01-09 15:51:15 +03:00
|
|
|
if (!this.marine) {
|
2022-05-12 17:14:16 +03:00
|
|
|
throw new Error("Can't register marine service: peer is not initialized");
|
|
|
|
}
|
2022-04-21 14:13:26 +03:00
|
|
|
if (this._containsService(serviceId)) {
|
|
|
|
throw new Error(`Service with '${serviceId}' id already exists`);
|
|
|
|
}
|
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
await this.marine.createService(wasm, serviceId, this._marineLogLevel);
|
2022-04-21 14:13:26 +03:00
|
|
|
this._marineServices.add(serviceId);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes the specified marine service from the Fluence peer
|
|
|
|
* @param serviceId - the service id to remove
|
|
|
|
*/
|
|
|
|
removeMarineService(serviceId: string): void {
|
|
|
|
this._marineServices.delete(serviceId);
|
|
|
|
}
|
|
|
|
|
2021-09-08 12:42:30 +03:00
|
|
|
/**
|
2021-10-20 22:20:43 +03:00
|
|
|
* Un-initializes the peer: stops all the underlying workflows, stops the Aqua VM
|
2021-09-08 12:42:30 +03:00
|
|
|
* and disconnects from the Fluence network
|
|
|
|
*/
|
2021-09-10 19:21:45 +03:00
|
|
|
async stop() {
|
2021-12-28 20:53:25 +03:00
|
|
|
this._keyPair = undefined; // This will set peer to non-initialized state and stop particle processing
|
2021-10-20 22:20:43 +03:00
|
|
|
this._stopParticleProcessing();
|
2022-08-05 16:43:19 +03:00
|
|
|
await this.disconnect();
|
2023-01-09 15:51:15 +03:00
|
|
|
await this.marine.stop();
|
|
|
|
await this.avmRunner.stop();
|
2022-05-12 17:14:16 +03:00
|
|
|
this._classServices = undefined;
|
2021-10-20 22:20:43 +03:00
|
|
|
|
|
|
|
this._particleSpecificHandlers.clear();
|
|
|
|
this._commonHandlers.clear();
|
2022-04-21 14:13:26 +03:00
|
|
|
this._marineServices.clear();
|
2021-09-08 12:42:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// internal api
|
|
|
|
|
|
|
|
/**
|
2022-08-05 16:43:19 +03:00
|
|
|
* @private Is not intended to be used manually. Subject to change
|
2021-09-08 12:42:30 +03:00
|
|
|
*/
|
|
|
|
get internals() {
|
|
|
|
return {
|
2022-08-04 12:05:31 +03:00
|
|
|
parseAst: async (air: string): Promise<{ success: boolean; data: any }> => {
|
|
|
|
const status = this.getStatus();
|
|
|
|
|
|
|
|
if (!status.isInitialized) {
|
|
|
|
new Error("Can't use avm: peer is not initialized");
|
|
|
|
}
|
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
const res = await this.marine.callService('avm', 'ast', [air], undefined);
|
2022-09-12 13:32:50 +03:00
|
|
|
if (!isString(res)) {
|
|
|
|
throw new Error(`Call to avm:ast expected to return string. Actual return: ${res}`);
|
|
|
|
}
|
|
|
|
|
2022-08-04 12:05:31 +03:00
|
|
|
try {
|
|
|
|
if (res.startsWith('error')) {
|
|
|
|
return {
|
|
|
|
success: false,
|
|
|
|
data: res,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
success: true,
|
|
|
|
data: JSON.parse(res),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
} catch (err) {
|
2022-09-12 13:32:50 +03:00
|
|
|
throw new Error('Failed to call avm. Result: ' + res + '. Error: ' + err);
|
2022-08-04 12:05:31 +03:00
|
|
|
}
|
|
|
|
},
|
2022-05-12 17:14:16 +03:00
|
|
|
createNewParticle: (script: string, ttl: number = this._defaultTTL) => {
|
|
|
|
const status = this.getStatus();
|
|
|
|
|
|
|
|
if (!status.isInitialized) {
|
|
|
|
return new Error("Can't create new particle: peer is not initialized");
|
|
|
|
}
|
|
|
|
|
|
|
|
return Particle.createNew(script, ttl, status.peerId);
|
|
|
|
},
|
2021-10-20 22:20:43 +03:00
|
|
|
/**
|
|
|
|
* Initiates a new particle execution starting from local peer
|
|
|
|
* @param particle - particle to start execution of
|
|
|
|
*/
|
2021-11-09 14:37:44 +03:00
|
|
|
initiateParticle: (particle: Particle, onStageChange: (stage: ParticleExecutionStage) => void): void => {
|
2022-05-12 17:14:16 +03:00
|
|
|
const status = this.getStatus();
|
|
|
|
if (!status.isInitialized) {
|
|
|
|
throw new Error('Cannot initiate new particle: peer is not initialized');
|
2021-11-09 14:37:44 +03:00
|
|
|
}
|
|
|
|
|
2022-04-06 15:16:45 +03:00
|
|
|
if (this._printParticleId) {
|
|
|
|
console.log('Particle id: ', particle.id);
|
|
|
|
}
|
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
if (particle.initPeerId === undefined) {
|
2022-05-12 17:14:16 +03:00
|
|
|
particle.initPeerId = status.peerId;
|
2021-10-20 22:20:43 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (particle.ttl === undefined) {
|
2021-10-21 17:56:21 +03:00
|
|
|
particle.ttl = this._defaultTTL;
|
2021-10-20 22:20:43 +03:00
|
|
|
}
|
|
|
|
|
2021-11-09 14:37:44 +03:00
|
|
|
this._incomingParticles.next({
|
|
|
|
particle: particle,
|
|
|
|
onStageChange: onStageChange,
|
|
|
|
});
|
2021-10-20 22:20:43 +03:00
|
|
|
},
|
2021-11-09 14:37:44 +03:00
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
/**
|
|
|
|
* Register Call Service handler functions
|
|
|
|
*/
|
|
|
|
regHandler: {
|
|
|
|
/**
|
|
|
|
* Register handler for all particles
|
|
|
|
*/
|
|
|
|
common: (
|
|
|
|
// force new line
|
|
|
|
serviceId: string,
|
|
|
|
fnName: string,
|
|
|
|
handler: GenericCallServiceHandler,
|
|
|
|
) => {
|
|
|
|
this._commonHandlers.set(serviceFnKey(serviceId, fnName), handler);
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Register handler which will be called only for particle with the specific id
|
|
|
|
*/
|
|
|
|
forParticle: (
|
|
|
|
particleId: string,
|
|
|
|
serviceId: string,
|
|
|
|
fnName: string,
|
|
|
|
handler: GenericCallServiceHandler,
|
|
|
|
) => {
|
|
|
|
let psh = this._particleSpecificHandlers.get(particleId);
|
|
|
|
if (psh === undefined) {
|
|
|
|
psh = new Map<string, GenericCallServiceHandler>();
|
|
|
|
this._particleSpecificHandlers.set(particleId, psh);
|
|
|
|
}
|
|
|
|
|
|
|
|
psh.set(serviceFnKey(serviceId, fnName), handler);
|
|
|
|
},
|
|
|
|
},
|
2021-09-08 12:42:30 +03:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
/**
|
|
|
|
* @private Subject to change. Do not use this method directly
|
|
|
|
*/
|
|
|
|
async init(config: PeerConfig & Required<Pick<PeerConfig, 'KeyPair'>>) {
|
|
|
|
this._keyPair = config.KeyPair;
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
const peerId = this._keyPair.Libp2pPeerId.toB58String();
|
|
|
|
|
|
|
|
if (config?.debug?.printParticleId) {
|
|
|
|
this._printParticleId = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._defaultTTL = config?.defaultTtlMs ?? DEFAULT_TTL;
|
|
|
|
|
|
|
|
if (config?.debug?.marineLogLevel) {
|
|
|
|
this._marineLogLevel = config.debug.marineLogLevel;
|
|
|
|
}
|
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
await this.marine.start();
|
|
|
|
await this.avmRunner.start();
|
2022-08-05 16:43:19 +03:00
|
|
|
|
|
|
|
registerDefaultServices(this);
|
|
|
|
|
|
|
|
this._classServices = {
|
|
|
|
sig: new Sig(this._keyPair),
|
2022-11-03 21:22:10 +03:00
|
|
|
srv: new Srv(this),
|
2022-08-05 16:43:19 +03:00
|
|
|
};
|
|
|
|
this._classServices.sig.securityGuard = defaultSigGuard(peerId);
|
2023-01-09 15:51:15 +03:00
|
|
|
registerSig(this, 'sig', this._classServices.sig);
|
2022-08-05 16:43:19 +03:00
|
|
|
registerSig(this, peerId, this._classServices.sig);
|
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
registerSrv(this, 'single_module_srv', this._classServices.srv);
|
|
|
|
registerNodeUtils(this, 'node_utils', new NodeUtils(this));
|
2022-11-03 21:22:10 +03:00
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
this._startParticleProcessing();
|
2021-10-20 22:20:43 +03:00
|
|
|
}
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
/**
|
|
|
|
* @private Subject to change. Do not use this method directly
|
|
|
|
*/
|
|
|
|
async connect(connection: FluenceConnection): Promise<void> {
|
2023-01-09 15:51:15 +03:00
|
|
|
if (this.connection) {
|
|
|
|
await this.connection.disconnect();
|
2022-08-05 16:43:19 +03:00
|
|
|
}
|
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
this.connection = connection;
|
|
|
|
await this.connection.connect(this._onIncomingParticle.bind(this));
|
2022-08-05 16:43:19 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @private Subject to change. Do not use this method directly
|
|
|
|
*/
|
|
|
|
async disconnect(): Promise<void> {
|
2023-01-09 15:51:15 +03:00
|
|
|
await this.connection?.disconnect();
|
2021-09-08 12:42:30 +03:00
|
|
|
}
|
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
// private
|
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
// Queues for incoming and outgoing particles
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2021-11-09 14:37:44 +03:00
|
|
|
private _incomingParticles = new Subject<ParticleQueueItem>();
|
2022-08-05 16:43:19 +03:00
|
|
|
private _outgoingParticles = new Subject<ParticleQueueItem & { nextPeerIds: PeerIdB58[] }>();
|
2021-10-20 22:20:43 +03:00
|
|
|
|
|
|
|
// Call service handler
|
|
|
|
|
2022-04-21 14:13:26 +03:00
|
|
|
private _marineServices = new Set<string>();
|
2023-01-09 15:51:15 +03:00
|
|
|
private _marineLogLevel?: LogLevel;
|
2021-10-20 22:20:43 +03:00
|
|
|
private _particleSpecificHandlers = new Map<string, Map<string, GenericCallServiceHandler>>();
|
|
|
|
private _commonHandlers = new Map<string, GenericCallServiceHandler>();
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2022-05-12 17:14:16 +03:00
|
|
|
private _classServices?: {
|
2022-02-04 22:39:41 +03:00
|
|
|
sig: Sig;
|
2022-11-03 21:22:10 +03:00
|
|
|
srv: Srv;
|
2022-02-04 22:39:41 +03:00
|
|
|
};
|
|
|
|
|
2022-04-21 14:13:26 +03:00
|
|
|
private _containsService(serviceId: string): boolean {
|
|
|
|
return this._marineServices.has(serviceId) || this._commonHandlers.has(serviceId);
|
|
|
|
}
|
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
// Internal peer state
|
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
private connection: FluenceConnection | null = null;
|
2022-05-12 17:14:16 +03:00
|
|
|
private _printParticleId = false;
|
|
|
|
private _defaultTTL: number = DEFAULT_TTL;
|
|
|
|
private _keyPair: KeyPair | undefined;
|
2021-10-20 22:20:43 +03:00
|
|
|
private _timeouts: Array<NodeJS.Timeout> = [];
|
2021-11-09 14:37:44 +03:00
|
|
|
private _particleQueues = new Map<string, Subject<ParticleQueueItem>>();
|
2021-10-20 22:20:43 +03:00
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
private _onIncomingParticle(p: string) {
|
|
|
|
const particle = Particle.fromString(p);
|
|
|
|
this._incomingParticles.next({ particle, onStageChange: () => {} });
|
|
|
|
}
|
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
private _startParticleProcessing() {
|
|
|
|
this._incomingParticles
|
|
|
|
.pipe(
|
2021-11-09 14:37:44 +03:00
|
|
|
tap((x) => {
|
|
|
|
x.particle.logTo('debug', 'particle received:');
|
|
|
|
}),
|
2021-10-21 17:56:21 +03:00
|
|
|
filterExpiredParticles(this._expireParticle.bind(this)),
|
2021-10-20 22:20:43 +03:00
|
|
|
)
|
2021-11-09 14:37:44 +03:00
|
|
|
.subscribe((item) => {
|
|
|
|
const p = item.particle;
|
2021-10-21 17:56:21 +03:00
|
|
|
let particlesQueue = this._particleQueues.get(p.id);
|
2021-10-20 22:20:43 +03:00
|
|
|
|
|
|
|
if (!particlesQueue) {
|
|
|
|
particlesQueue = this._createParticlesProcessingQueue();
|
2021-10-21 17:56:21 +03:00
|
|
|
this._particleQueues.set(p.id, particlesQueue);
|
2021-10-20 22:20:43 +03:00
|
|
|
|
|
|
|
const timeout = setTimeout(() => {
|
2021-11-09 14:37:44 +03:00
|
|
|
this._expireParticle(item);
|
2021-10-20 22:20:43 +03:00
|
|
|
}, p.actualTtl());
|
|
|
|
|
|
|
|
this._timeouts.push(timeout);
|
|
|
|
}
|
|
|
|
|
2021-11-09 14:37:44 +03:00
|
|
|
particlesQueue.next(item);
|
2021-10-20 22:20:43 +03:00
|
|
|
});
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2022-05-12 17:14:16 +03:00
|
|
|
this._outgoingParticles.subscribe((item) => {
|
2021-12-28 20:53:25 +03:00
|
|
|
// Do not send particle after the peer has been stopped
|
|
|
|
if (!this.getStatus().isInitialized) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
if (!this.connection) {
|
2021-11-17 09:21:32 +03:00
|
|
|
item.particle.logTo('error', 'cannot send particle, peer is not connected');
|
|
|
|
item.onStageChange({ stage: 'sendingError' });
|
|
|
|
return;
|
|
|
|
}
|
2022-08-05 16:43:19 +03:00
|
|
|
item.particle.logTo('debug', 'sending particle:');
|
2023-01-09 15:51:15 +03:00
|
|
|
this.connection?.sendParticle(item.nextPeerIds, item.particle.toString()).then(
|
2022-05-12 17:14:16 +03:00
|
|
|
() => {
|
|
|
|
item.onStageChange({ stage: 'sent' });
|
|
|
|
},
|
2022-09-12 13:32:50 +03:00
|
|
|
(e: any) => {
|
2022-05-12 17:14:16 +03:00
|
|
|
log.error(e);
|
|
|
|
},
|
|
|
|
);
|
2021-10-20 22:20:43 +03:00
|
|
|
});
|
2021-09-08 12:42:30 +03:00
|
|
|
}
|
|
|
|
|
2021-11-09 14:37:44 +03:00
|
|
|
private _expireParticle(item: ParticleQueueItem) {
|
|
|
|
const particleId = item.particle.id;
|
|
|
|
log.debug(
|
|
|
|
`particle ${particleId} has expired after ${item.particle.ttl}. Deleting particle-related queues and handlers`,
|
|
|
|
);
|
2021-10-21 17:56:21 +03:00
|
|
|
|
|
|
|
this._particleQueues.delete(particleId);
|
|
|
|
this._particleSpecificHandlers.delete(particleId);
|
2021-11-09 14:37:44 +03:00
|
|
|
|
|
|
|
item.onStageChange({ stage: 'expired' });
|
2021-10-21 17:56:21 +03:00
|
|
|
}
|
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
private _createParticlesProcessingQueue() {
|
2022-05-12 17:14:16 +03:00
|
|
|
const particlesQueue = new Subject<ParticleQueueItem>();
|
2021-10-20 22:20:43 +03:00
|
|
|
let prevData: Uint8Array = Buffer.from([]);
|
|
|
|
|
|
|
|
particlesQueue
|
|
|
|
.pipe(
|
2021-10-21 17:56:21 +03:00
|
|
|
filterExpiredParticles(this._expireParticle.bind(this)),
|
2021-12-28 20:53:25 +03:00
|
|
|
|
|
|
|
concatMap(async (item) => {
|
2022-05-12 17:14:16 +03:00
|
|
|
const status = this.getStatus();
|
2023-01-09 15:51:15 +03:00
|
|
|
if (!status.isInitialized || this.marine === undefined) {
|
2022-05-12 17:14:16 +03:00
|
|
|
// If `.stop()` was called return null to stop particle processing immediately
|
2021-12-28 20:53:25 +03:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// IMPORTANT!
|
|
|
|
// AVM runner execution and prevData <-> newData swapping
|
|
|
|
// MUST happen sequentially (in a critical section).
|
|
|
|
// Otherwise the race between runner might occur corrupting the prevData
|
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
item.particle.logTo('debug', 'Sending particle to interpreter');
|
|
|
|
log.debug('prevData: ', dataToString(prevData));
|
|
|
|
|
|
|
|
const avmCallResult = await this.avmRunner.run(
|
2022-09-12 13:32:50 +03:00
|
|
|
{
|
|
|
|
initPeerId: item.particle.initPeerId,
|
|
|
|
currentPeerId: status.peerId,
|
|
|
|
timestamp: item.particle.timestamp,
|
|
|
|
ttl: item.particle.ttl,
|
|
|
|
},
|
|
|
|
item.particle.script,
|
|
|
|
prevData,
|
|
|
|
item.particle.data,
|
|
|
|
item.particle.callResults,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!(avmCallResult instanceof Error) && avmCallResult.retCode === 0) {
|
|
|
|
const newData = Buffer.from(avmCallResult.data);
|
|
|
|
prevData = newData;
|
|
|
|
}
|
2021-12-28 20:53:25 +03:00
|
|
|
|
|
|
|
return {
|
|
|
|
...item,
|
2022-09-12 13:32:50 +03:00
|
|
|
result: avmCallResult,
|
2021-12-28 20:53:25 +03:00
|
|
|
};
|
|
|
|
}),
|
2021-10-20 22:20:43 +03:00
|
|
|
)
|
2022-05-12 17:14:16 +03:00
|
|
|
.subscribe((item) => {
|
|
|
|
// If `.stop()` was called then item will be null and we need to stop particle processing immediately
|
|
|
|
if (item === null || !this.getStatus().isInitialized) {
|
2021-12-28 20:53:25 +03:00
|
|
|
return;
|
|
|
|
}
|
2021-11-09 14:37:44 +03:00
|
|
|
|
2022-12-12 13:44:46 +03:00
|
|
|
// Do not proceed further if the particle is expired
|
|
|
|
if (item.particle.hasExpired()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-11-09 14:37:44 +03:00
|
|
|
// Do not continue if there was an error in particle interpretation
|
2022-09-12 13:32:50 +03:00
|
|
|
if (item.result instanceof Error) {
|
|
|
|
log.error('Interpreter failed: ', jsonify(item.result.message));
|
|
|
|
item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.message });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const toLog = { ...item.result, data: dataToString(item.result.data) };
|
|
|
|
if (item.result.retCode !== 0) {
|
|
|
|
log.error('Interpreter failed: ', jsonify(toLog));
|
2021-12-28 20:53:25 +03:00
|
|
|
item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.errorMessage });
|
2021-11-09 14:37:44 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-09-12 13:32:50 +03:00
|
|
|
log.debug('Interpreter result: ', jsonify(toLog));
|
|
|
|
|
2021-11-09 14:37:44 +03:00
|
|
|
setTimeout(() => {
|
|
|
|
item.onStageChange({ stage: 'interpreted' });
|
|
|
|
}, 0);
|
2021-10-20 22:20:43 +03:00
|
|
|
|
|
|
|
// send particle further if requested
|
2021-12-28 20:53:25 +03:00
|
|
|
if (item.result.nextPeerPks.length > 0) {
|
|
|
|
const newParticle = item.particle.clone();
|
2022-09-12 13:32:50 +03:00
|
|
|
const newData = Buffer.from(item.result.data);
|
|
|
|
newParticle.data = newData;
|
2022-08-05 16:43:19 +03:00
|
|
|
this._outgoingParticles.next({
|
|
|
|
...item,
|
|
|
|
particle: newParticle,
|
|
|
|
nextPeerIds: item.result.nextPeerPks,
|
|
|
|
});
|
2021-10-20 22:20:43 +03:00
|
|
|
}
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
// execute call requests if needed
|
|
|
|
// and put particle with the results back to queue
|
2021-12-28 20:53:25 +03:00
|
|
|
if (item.result.callRequests.length > 0) {
|
2022-05-12 17:14:16 +03:00
|
|
|
for (const [key, cr] of item.result.callRequests) {
|
2021-11-17 09:21:32 +03:00
|
|
|
const req = {
|
|
|
|
fnName: cr.functionName,
|
|
|
|
args: cr.arguments,
|
|
|
|
serviceId: cr.serviceId,
|
|
|
|
tetraplets: cr.tetraplets,
|
2021-12-28 20:53:25 +03:00
|
|
|
particleContext: item.particle.getParticleContext(),
|
2021-11-17 09:21:32 +03:00
|
|
|
};
|
|
|
|
|
2022-12-12 13:44:46 +03:00
|
|
|
if (item.particle.hasExpired()) {
|
|
|
|
// just in case do not call any services if the particle is already expired
|
|
|
|
return;
|
|
|
|
}
|
2021-11-17 09:21:32 +03:00
|
|
|
this._execSingleCallRequest(req)
|
2022-11-03 21:22:10 +03:00
|
|
|
.catch((err): CallServiceResult => {
|
|
|
|
if (err instanceof ServiceError) {
|
|
|
|
return {
|
|
|
|
retCode: ResultCodes.error,
|
|
|
|
result: err.message,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2021-12-10 16:47:58 +03:00
|
|
|
retCode: ResultCodes.error,
|
2021-11-17 09:21:32 +03:00
|
|
|
result: `Handler failed. fnName="${req.fnName}" serviceId="${
|
|
|
|
req.serviceId
|
|
|
|
}" error: ${err.toString()}`,
|
2022-11-03 21:22:10 +03:00
|
|
|
};
|
|
|
|
})
|
2021-11-17 09:21:32 +03:00
|
|
|
.then((res) => {
|
|
|
|
const serviceResult = {
|
2021-12-28 20:53:25 +03:00
|
|
|
result: jsonify(res.result),
|
2021-11-17 09:21:32 +03:00
|
|
|
retCode: res.retCode,
|
|
|
|
};
|
|
|
|
|
2021-12-28 20:53:25 +03:00
|
|
|
const newParticle = item.particle.clone();
|
2021-11-17 09:21:32 +03:00
|
|
|
newParticle.callResults = [[key, serviceResult]];
|
|
|
|
newParticle.data = Buffer.from([]);
|
|
|
|
|
|
|
|
particlesQueue.next({ ...item, particle: newParticle });
|
|
|
|
});
|
|
|
|
}
|
2021-11-09 14:37:44 +03:00
|
|
|
} else {
|
|
|
|
item.onStageChange({ stage: 'localWorkDone' });
|
2021-10-20 22:20:43 +03:00
|
|
|
}
|
|
|
|
});
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
return particlesQueue;
|
2021-09-08 12:42:30 +03:00
|
|
|
}
|
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
private async _execSingleCallRequest(req: CallServiceData): Promise<CallServiceResult> {
|
2021-12-28 20:53:25 +03:00
|
|
|
log.debug('executing call service handler', jsonify(req));
|
2021-10-20 22:20:43 +03:00
|
|
|
const particleId = req.particleContext.particleId;
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2023-01-09 15:51:15 +03:00
|
|
|
if (this.marine && this._marineServices.has(req.serviceId)) {
|
|
|
|
const result = await this.marine.callService(req.serviceId, req.fnName, req.args, undefined);
|
2022-04-21 14:13:26 +03:00
|
|
|
|
2022-09-12 13:32:50 +03:00
|
|
|
return {
|
|
|
|
retCode: ResultCodes.success,
|
|
|
|
result: result as JSONValue,
|
|
|
|
};
|
2022-04-21 14:13:26 +03:00
|
|
|
}
|
|
|
|
|
2022-03-17 07:00:19 +03:00
|
|
|
const key = serviceFnKey(req.serviceId, req.fnName);
|
|
|
|
const psh = this._particleSpecificHandlers.get(particleId);
|
2022-05-12 17:14:16 +03:00
|
|
|
let handler: GenericCallServiceHandler | undefined;
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2022-03-17 07:00:19 +03:00
|
|
|
// we should prioritize handler for this particle if there is one
|
|
|
|
// if particle-specific handlers exist for this particle try getting handler there
|
|
|
|
if (psh !== undefined) {
|
|
|
|
handler = psh.get(key);
|
2021-10-20 22:20:43 +03:00
|
|
|
}
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2022-03-17 07:00:19 +03:00
|
|
|
// then try to find a common handler for all particles with this service-fn key
|
|
|
|
// if there is no particle-specific handler, get one from common map
|
|
|
|
if (handler === undefined) {
|
|
|
|
handler = this._commonHandlers.get(key);
|
|
|
|
}
|
2021-10-20 22:20:43 +03:00
|
|
|
|
2022-03-17 07:00:19 +03:00
|
|
|
// if no handler is found return useful error message to AVM
|
|
|
|
if (handler === undefined) {
|
|
|
|
return {
|
|
|
|
retCode: ResultCodes.error,
|
|
|
|
result: `No handler has been registered for serviceId='${req.serviceId}' fnName='${
|
|
|
|
req.fnName
|
|
|
|
}' args='${jsonify(req.args)}'`,
|
|
|
|
};
|
2021-10-20 22:20:43 +03:00
|
|
|
}
|
|
|
|
|
2022-03-17 07:00:19 +03:00
|
|
|
// if we found a handler, execute it
|
|
|
|
const res = await handler(req);
|
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
if (res.result === undefined) {
|
|
|
|
res.result = null;
|
2021-09-08 12:42:30 +03:00
|
|
|
}
|
|
|
|
|
2021-12-28 20:53:25 +03:00
|
|
|
log.debug('executed call service handler, req and res are: ', jsonify(req), jsonify(res));
|
2021-10-20 22:20:43 +03:00
|
|
|
return res;
|
2021-09-08 12:42:30 +03:00
|
|
|
}
|
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
private _stopParticleProcessing() {
|
|
|
|
// do not hang if the peer has been stopped while some of the timeouts are still being executed
|
2022-05-12 17:14:16 +03:00
|
|
|
this._timeouts.forEach((timeout) => {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
});
|
2021-10-21 17:56:21 +03:00
|
|
|
this._particleQueues.clear();
|
2021-09-08 12:42:30 +03:00
|
|
|
}
|
2021-10-20 22:20:43 +03:00
|
|
|
}
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2022-08-05 16:43:19 +03:00
|
|
|
async function configToConnection(
|
|
|
|
keyPair: KeyPair,
|
|
|
|
connection?: ConnectionOption,
|
|
|
|
dialTimeoutMs?: number,
|
|
|
|
): Promise<FluenceConnection | null> {
|
|
|
|
if (!connection) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (connection instanceof FluenceConnection) {
|
|
|
|
return connection;
|
|
|
|
}
|
|
|
|
|
|
|
|
let connectToMultiAddr: MultiaddrInput;
|
|
|
|
// figuring out what was specified as input
|
|
|
|
const tmp = connection as any;
|
|
|
|
if (tmp.multiaddr !== undefined) {
|
|
|
|
// specified as FluenceNode (object with multiaddr and peerId props)
|
|
|
|
connectToMultiAddr = tmp.multiaddr;
|
|
|
|
} else {
|
|
|
|
// specified as MultiaddrInput
|
|
|
|
connectToMultiAddr = tmp;
|
|
|
|
}
|
|
|
|
|
|
|
|
const res = await RelayConnection.createConnection({
|
|
|
|
peerId: keyPair.Libp2pPeerId,
|
|
|
|
relayAddress: connectToMultiAddr,
|
|
|
|
dialTimeoutMs: dialTimeoutMs,
|
|
|
|
});
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2021-10-20 22:20:43 +03:00
|
|
|
function serviceFnKey(serviceId: string, fnName: string) {
|
|
|
|
return `${serviceId}/${fnName}`;
|
|
|
|
}
|
|
|
|
|
2022-02-04 22:39:41 +03:00
|
|
|
function registerDefaultServices(peer: FluencePeer) {
|
2022-05-12 17:14:16 +03:00
|
|
|
Object.entries(builtInServices).forEach(([serviceId, service]) => {
|
|
|
|
Object.entries(service).forEach(([fnName, fn]) => {
|
|
|
|
peer.internals.regHandler.common(serviceId, fnName, fn);
|
|
|
|
});
|
|
|
|
});
|
2021-10-20 22:20:43 +03:00
|
|
|
}
|
2021-09-08 12:42:30 +03:00
|
|
|
|
2021-11-09 14:37:44 +03:00
|
|
|
function filterExpiredParticles(onParticleExpiration: (item: ParticleQueueItem) => void) {
|
2021-10-20 22:20:43 +03:00
|
|
|
return pipe(
|
2021-11-09 14:37:44 +03:00
|
|
|
tap((item: ParticleQueueItem) => {
|
|
|
|
if (item.particle.hasExpired()) {
|
|
|
|
onParticleExpiration(item);
|
2021-09-08 12:42:30 +03:00
|
|
|
}
|
2021-10-20 22:20:43 +03:00
|
|
|
}),
|
2021-11-09 14:37:44 +03:00
|
|
|
filter((x: ParticleQueueItem) => !x.particle.hasExpired()),
|
2021-10-20 22:20:43 +03:00
|
|
|
);
|
2021-09-08 12:42:30 +03:00
|
|
|
}
|