@olane/o-node 0.7.39 → 0.7.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/connection/index.d.ts +2 -0
- package/dist/src/connection/index.d.ts.map +1 -1
- package/dist/src/connection/index.js +2 -0
- package/dist/src/connection/o-managed-stream.d.ts +57 -0
- package/dist/src/connection/o-managed-stream.d.ts.map +1 -0
- package/dist/src/connection/o-managed-stream.js +76 -0
- package/dist/src/connection/o-node-connection.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.js +9 -1
- package/dist/src/connection/stream-handler.config.d.ts +11 -0
- package/dist/src/connection/stream-handler.config.d.ts.map +1 -1
- package/dist/src/connection/stream-handler.d.ts +54 -2
- package/dist/src/connection/stream-handler.d.ts.map +1 -1
- package/dist/src/connection/stream-handler.js +130 -5
- package/dist/src/o-node.tool.d.ts.map +1 -1
- package/dist/src/o-node.tool.js +5 -0
- package/dist/test/o-managed-stream.spec.d.ts +2 -0
- package/dist/test/o-managed-stream.spec.d.ts.map +1 -0
- package/dist/test/o-managed-stream.spec.js +122 -0
- package/dist/test/stream-handler-caching.spec.d.ts +2 -0
- package/dist/test/stream-handler-caching.spec.d.ts.map +1 -0
- package/dist/test/stream-handler-caching.spec.js +261 -0
- package/package.json +7 -7
|
@@ -2,4 +2,6 @@ export * from './o-node-connection.js';
|
|
|
2
2
|
export * from './o-node-connection.manager.js';
|
|
3
3
|
export * from './interfaces/o-node-connection.config.js';
|
|
4
4
|
export * from './interfaces/o-node-connection-manager.config.js';
|
|
5
|
+
export * from './o-managed-stream.js';
|
|
6
|
+
export * from './stream-handler.config.js';
|
|
5
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/connection/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0CAA0C,CAAC;AACzD,cAAc,kDAAkD,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/connection/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0CAA0C,CAAC;AACzD,cAAc,kDAAkD,CAAC;AACjE,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC"}
|
|
@@ -2,3 +2,5 @@ export * from './o-node-connection.js';
|
|
|
2
2
|
export * from './o-node-connection.manager.js';
|
|
3
3
|
export * from './interfaces/o-node-connection.config.js';
|
|
4
4
|
export * from './interfaces/o-node-connection-manager.config.js';
|
|
5
|
+
export * from './o-managed-stream.js';
|
|
6
|
+
export * from './stream-handler.config.js';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Stream } from '@libp2p/interface';
|
|
2
|
+
import type { oAddress } from '@olane/o-core';
|
|
3
|
+
/**
|
|
4
|
+
* oManagedStream wraps a libp2p Stream with caller/receiver address metadata
|
|
5
|
+
* to enable proper stream reuse based on address pairs rather than protocol only.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Bidirectional cache keys: A↔B === B↔A
|
|
9
|
+
* - Automatic reusability checking
|
|
10
|
+
* - Idle time tracking for cleanup
|
|
11
|
+
*/
|
|
12
|
+
export declare class oManagedStream {
|
|
13
|
+
readonly stream: Stream;
|
|
14
|
+
readonly callerAddress: oAddress;
|
|
15
|
+
readonly receiverAddress: oAddress;
|
|
16
|
+
readonly direction: 'inbound' | 'outbound';
|
|
17
|
+
readonly createdAt: number;
|
|
18
|
+
private _lastUsedAt;
|
|
19
|
+
constructor(stream: Stream, callerAddress: oAddress, receiverAddress: oAddress, direction: 'inbound' | 'outbound');
|
|
20
|
+
/**
|
|
21
|
+
* Generates a bidirectional cache key from caller and receiver addresses.
|
|
22
|
+
* The key is symmetric: A↔B === B↔A
|
|
23
|
+
*
|
|
24
|
+
* @returns Cache key string in format "address1↔address2" (sorted)
|
|
25
|
+
*/
|
|
26
|
+
get cacheKey(): string;
|
|
27
|
+
/**
|
|
28
|
+
* Checks if the stream is in a reusable state:
|
|
29
|
+
* - Stream status is 'open'
|
|
30
|
+
* - Write status is 'writable'
|
|
31
|
+
* - Remote read status is 'readable'
|
|
32
|
+
*
|
|
33
|
+
* @returns true if stream can be reused
|
|
34
|
+
*/
|
|
35
|
+
get isReusable(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Gets the last used timestamp
|
|
38
|
+
*/
|
|
39
|
+
get lastUsedAt(): number;
|
|
40
|
+
/**
|
|
41
|
+
* Gets the age of the stream in milliseconds
|
|
42
|
+
*/
|
|
43
|
+
get age(): number;
|
|
44
|
+
/**
|
|
45
|
+
* Gets the idle time in milliseconds since last use
|
|
46
|
+
*/
|
|
47
|
+
get idleTime(): number;
|
|
48
|
+
/**
|
|
49
|
+
* Updates the last used timestamp to now
|
|
50
|
+
*/
|
|
51
|
+
updateLastUsed(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Gets a string representation of the managed stream for debugging
|
|
54
|
+
*/
|
|
55
|
+
toString(): string;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=o-managed-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"o-managed-stream.d.ts","sourceRoot":"","sources":["../../../src/connection/o-managed-stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C;;;;;;;;GAQG;AACH,qBAAa,cAAc;aAKP,MAAM,EAAE,MAAM;aACd,aAAa,EAAE,QAAQ;aACvB,eAAe,EAAE,QAAQ;aACzB,SAAS,EAAE,SAAS,GAAG,UAAU;IAPnD,SAAgB,SAAS,EAAE,MAAM,CAAC;IAClC,OAAO,CAAC,WAAW,CAAS;gBAGV,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,QAAQ,EACvB,eAAe,EAAE,QAAQ,EACzB,SAAS,EAAE,SAAS,GAAG,UAAU;IAMnD;;;;;OAKG;IACH,IAAI,QAAQ,IAAI,MAAM,CAQrB;IAED;;;;;;;OAOG;IACH,IAAI,UAAU,IAAI,OAAO,CAMxB;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;OAEG;IACH,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;OAEG;IACH,cAAc,IAAI,IAAI;IAItB;;OAEG;IACH,QAAQ,IAAI,MAAM;CAGnB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oManagedStream wraps a libp2p Stream with caller/receiver address metadata
|
|
3
|
+
* to enable proper stream reuse based on address pairs rather than protocol only.
|
|
4
|
+
*
|
|
5
|
+
* Key features:
|
|
6
|
+
* - Bidirectional cache keys: A↔B === B↔A
|
|
7
|
+
* - Automatic reusability checking
|
|
8
|
+
* - Idle time tracking for cleanup
|
|
9
|
+
*/
|
|
10
|
+
export class oManagedStream {
|
|
11
|
+
constructor(stream, callerAddress, receiverAddress, direction) {
|
|
12
|
+
this.stream = stream;
|
|
13
|
+
this.callerAddress = callerAddress;
|
|
14
|
+
this.receiverAddress = receiverAddress;
|
|
15
|
+
this.direction = direction;
|
|
16
|
+
this.createdAt = Date.now();
|
|
17
|
+
this._lastUsedAt = this.createdAt;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generates a bidirectional cache key from caller and receiver addresses.
|
|
21
|
+
* The key is symmetric: A↔B === B↔A
|
|
22
|
+
*
|
|
23
|
+
* @returns Cache key string in format "address1↔address2" (sorted)
|
|
24
|
+
*/
|
|
25
|
+
get cacheKey() {
|
|
26
|
+
// Sort addresses to ensure bidirectionality
|
|
27
|
+
const addresses = [
|
|
28
|
+
this.callerAddress.value,
|
|
29
|
+
this.receiverAddress.value,
|
|
30
|
+
].sort();
|
|
31
|
+
return `${addresses[0]}↔${addresses[1]}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Checks if the stream is in a reusable state:
|
|
35
|
+
* - Stream status is 'open'
|
|
36
|
+
* - Write status is 'writable'
|
|
37
|
+
* - Remote read status is 'readable'
|
|
38
|
+
*
|
|
39
|
+
* @returns true if stream can be reused
|
|
40
|
+
*/
|
|
41
|
+
get isReusable() {
|
|
42
|
+
return (this.stream.status === 'open' &&
|
|
43
|
+
this.stream.writeStatus === 'writable' &&
|
|
44
|
+
this.stream.remoteReadStatus === 'readable');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Gets the last used timestamp
|
|
48
|
+
*/
|
|
49
|
+
get lastUsedAt() {
|
|
50
|
+
return this._lastUsedAt;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Gets the age of the stream in milliseconds
|
|
54
|
+
*/
|
|
55
|
+
get age() {
|
|
56
|
+
return Date.now() - this.createdAt;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Gets the idle time in milliseconds since last use
|
|
60
|
+
*/
|
|
61
|
+
get idleTime() {
|
|
62
|
+
return Date.now() - this._lastUsedAt;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Updates the last used timestamp to now
|
|
66
|
+
*/
|
|
67
|
+
updateLastUsed() {
|
|
68
|
+
this._lastUsedAt = Date.now();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Gets a string representation of the managed stream for debugging
|
|
72
|
+
*/
|
|
73
|
+
toString() {
|
|
74
|
+
return `oManagedStream(${this.cacheKey}, ${this.direction}, ${this.stream.status})`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"o-node-connection.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAEL,WAAW,EAGX,QAAQ,EACR,SAAS,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAEV,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AAEpC,qBAAa,eAAgB,SAAQ,WAAW;IAKlC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,qBAAqB;IAJrD,aAAa,EAAE,UAAU,CAAC;IACjC,SAAS,CAAC,aAAa,EAAE,aAAa,CAAC;IACvC,SAAS,CAAC,WAAW,EAAE,iBAAiB,CAAC;gBAEV,MAAM,EAAE,qBAAqB;IAO5D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IAmBlB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"o-node-connection.d.ts","sourceRoot":"","sources":["../../../src/connection/o-node-connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAEL,WAAW,EAGX,QAAQ,EACR,SAAS,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAEV,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AAEpC,qBAAa,eAAgB,SAAQ,WAAW;IAKlC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,qBAAqB;IAJrD,aAAa,EAAE,UAAU,CAAC;IACjC,SAAS,CAAC,aAAa,EAAE,aAAa,CAAC;IACvC,SAAS,CAAC,WAAW,EAAE,iBAAiB,CAAC;gBAEV,MAAM,EAAE,qBAAqB;IAO5D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IAmBlB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;IA6BpC,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC;IA2C/C,YAAY,CAAC,MAAM,EAAE,MAAM;IAQ3B,KAAK,CAAC,KAAK,EAAE,KAAK;IAMlB,KAAK;CAKZ"}
|
|
@@ -30,7 +30,15 @@ export class oNodeConnection extends oConnection {
|
|
|
30
30
|
reusePolicy: this.reusePolicy,
|
|
31
31
|
drainTimeoutMs: this.config.drainTimeoutMs,
|
|
32
32
|
};
|
|
33
|
-
|
|
33
|
+
// Build stream addresses for address-based caching
|
|
34
|
+
const streamAddresses = this.callerAddress && this.nextHopAddress
|
|
35
|
+
? {
|
|
36
|
+
callerAddress: this.callerAddress,
|
|
37
|
+
receiverAddress: this.nextHopAddress,
|
|
38
|
+
direction: 'outbound',
|
|
39
|
+
}
|
|
40
|
+
: undefined;
|
|
41
|
+
return this.streamHandler.getOrCreateStream(this.p2pConnection, this.nextHopAddress.protocol, streamConfig, streamAddresses);
|
|
34
42
|
}
|
|
35
43
|
async transmit(request) {
|
|
36
44
|
try {
|
|
@@ -34,6 +34,17 @@ export interface StreamHandlerConfig {
|
|
|
34
34
|
* AbortSignal for cancellation
|
|
35
35
|
*/
|
|
36
36
|
signal?: AbortSignal;
|
|
37
|
+
/**
|
|
38
|
+
* Maximum number of streams to cache
|
|
39
|
+
* Older streams will be evicted when limit is reached
|
|
40
|
+
* @default unlimited
|
|
41
|
+
*/
|
|
42
|
+
maxCachedStreams?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Time in milliseconds after which idle cached streams are closed
|
|
45
|
+
* @default unlimited (streams remain cached until closed by peer)
|
|
46
|
+
*/
|
|
47
|
+
streamIdleTimeout?: number;
|
|
37
48
|
}
|
|
38
49
|
/**
|
|
39
50
|
* Context for stream lifecycle operations
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-handler.config.d.ts","sourceRoot":"","sources":["../../../src/connection/stream-handler.config.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAEhC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"stream-handler.config.d.ts","sourceRoot":"","sources":["../../../src/connection/stream-handler.config.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAEhC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAEjC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5B;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,mBAAmB,CAAC;CAC7B"}
|
|
@@ -4,8 +4,9 @@ import { EventEmitter } from 'events';
|
|
|
4
4
|
import { oRequest, oResponse, Logger } from '@olane/o-core';
|
|
5
5
|
import type { oRouterRequest } from '@olane/o-core';
|
|
6
6
|
import type { oConnection } from '@olane/o-core';
|
|
7
|
+
import type { oAddress } from '@olane/o-core';
|
|
7
8
|
import type { RunResult } from '@olane/o-tool';
|
|
8
|
-
import type { StreamHandlerConfig } from './stream-handler.config.js';
|
|
9
|
+
import type { StreamHandlerConfig, StreamReusePolicy } from './stream-handler.config.js';
|
|
9
10
|
/**
|
|
10
11
|
* StreamHandler centralizes all stream-related functionality including:
|
|
11
12
|
* - Message type detection (request vs response)
|
|
@@ -16,6 +17,7 @@ import type { StreamHandlerConfig } from './stream-handler.config.js';
|
|
|
16
17
|
*/
|
|
17
18
|
export declare class StreamHandler {
|
|
18
19
|
private logger;
|
|
20
|
+
private streamCache;
|
|
19
21
|
constructor(logger?: Logger);
|
|
20
22
|
/**
|
|
21
23
|
* Detects if a decoded message is a request
|
|
@@ -40,14 +42,64 @@ export declare class StreamHandler {
|
|
|
40
42
|
* @throws Error if JSON parsing fails even with JSON5 fallback
|
|
41
43
|
*/
|
|
42
44
|
private extractAndParseJSON;
|
|
45
|
+
/**
|
|
46
|
+
* Builds a bidirectional cache key from caller and receiver addresses
|
|
47
|
+
* The key is symmetric: A↔B === B↔A
|
|
48
|
+
*
|
|
49
|
+
* @param callerAddress - The caller's address
|
|
50
|
+
* @param receiverAddress - The receiver's address
|
|
51
|
+
* @returns Cache key string
|
|
52
|
+
*/
|
|
53
|
+
private buildCacheKey;
|
|
54
|
+
/**
|
|
55
|
+
* Caches a managed stream for reuse
|
|
56
|
+
*
|
|
57
|
+
* @param managedStream - The managed stream to cache
|
|
58
|
+
*/
|
|
59
|
+
private cacheStream;
|
|
60
|
+
/**
|
|
61
|
+
* Removes a stream from the cache
|
|
62
|
+
*
|
|
63
|
+
* @param stream - The stream to remove
|
|
64
|
+
*/
|
|
65
|
+
private removeStreamFromCache;
|
|
66
|
+
/**
|
|
67
|
+
* Sets up cleanup listener for stream close events
|
|
68
|
+
*
|
|
69
|
+
* @param stream - The stream to monitor
|
|
70
|
+
*/
|
|
71
|
+
private setupStreamCleanup;
|
|
72
|
+
/**
|
|
73
|
+
* Extracts the remote peer's address from a connection
|
|
74
|
+
* Falls back to creating an address from the peer ID if no address metadata is available
|
|
75
|
+
*
|
|
76
|
+
* @param connection - The libp2p connection
|
|
77
|
+
* @returns The remote peer's oAddress
|
|
78
|
+
*/
|
|
79
|
+
extractRemotePeerAddress(connection: Connection): oAddress;
|
|
80
|
+
/**
|
|
81
|
+
* Caches an inbound stream for bidirectional reuse
|
|
82
|
+
* This allows the same stream to be reused for responses
|
|
83
|
+
*
|
|
84
|
+
* @param stream - The inbound stream to cache
|
|
85
|
+
* @param callerAddress - The address of the caller
|
|
86
|
+
* @param receiverAddress - The address of the receiver (local node)
|
|
87
|
+
* @param reusePolicy - Whether to enable caching
|
|
88
|
+
*/
|
|
89
|
+
cacheInboundStream(stream: Stream, callerAddress: oAddress, receiverAddress: oAddress, reusePolicy?: StreamReusePolicy): void;
|
|
43
90
|
/**
|
|
44
91
|
* Gets an existing open stream or creates a new one based on reuse policy
|
|
45
92
|
*
|
|
46
93
|
* @param connection - The libp2p connection
|
|
47
94
|
* @param protocol - The protocol to use for the stream
|
|
48
95
|
* @param config - Stream handler configuration
|
|
96
|
+
* @param streamAddresses - Optional addresses for address-based stream reuse
|
|
49
97
|
*/
|
|
50
|
-
getOrCreateStream(connection: Connection, protocol: string, config?: StreamHandlerConfig
|
|
98
|
+
getOrCreateStream(connection: Connection, protocol: string, config?: StreamHandlerConfig, streamAddresses?: {
|
|
99
|
+
callerAddress: oAddress;
|
|
100
|
+
receiverAddress: oAddress;
|
|
101
|
+
direction: 'inbound' | 'outbound';
|
|
102
|
+
}): Promise<Stream>;
|
|
51
103
|
/**
|
|
52
104
|
* Sends data through a stream using length-prefixed encoding (libp2p v3 best practice)
|
|
53
105
|
* Each message is automatically prefixed with a varint indicating the message length
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-handler.d.ts","sourceRoot":"","sources":["../../../src/connection/stream-handler.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EACL,QAAQ,EACR,SAAS,EAIT,MAAM,EAEP,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"stream-handler.d.ts","sourceRoot":"","sources":["../../../src/connection/stream-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { oRequest, oResponse, CoreUtils, oError, oErrorCodes, Logger, ResponseBuilder, } from '@olane/o-core';
|
|
2
2
|
import { lpStream } from '@olane/o-config';
|
|
3
3
|
import JSON5 from 'json5';
|
|
4
|
+
import { oManagedStream } from './o-managed-stream.js';
|
|
4
5
|
/**
|
|
5
6
|
* StreamHandler centralizes all stream-related functionality including:
|
|
6
7
|
* - Message type detection (request vs response)
|
|
@@ -11,6 +12,7 @@ import JSON5 from 'json5';
|
|
|
11
12
|
*/
|
|
12
13
|
export class StreamHandler {
|
|
13
14
|
constructor(logger) {
|
|
15
|
+
this.streamCache = new Map();
|
|
14
16
|
this.logger = logger ?? new Logger('StreamHandler');
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
@@ -71,48 +73,171 @@ export class StreamHandler {
|
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Builds a bidirectional cache key from caller and receiver addresses
|
|
78
|
+
* The key is symmetric: A↔B === B↔A
|
|
79
|
+
*
|
|
80
|
+
* @param callerAddress - The caller's address
|
|
81
|
+
* @param receiverAddress - The receiver's address
|
|
82
|
+
* @returns Cache key string
|
|
83
|
+
*/
|
|
84
|
+
buildCacheKey(callerAddress, receiverAddress) {
|
|
85
|
+
const addresses = [callerAddress.value, receiverAddress.value].sort();
|
|
86
|
+
return `${addresses[0]}↔${addresses[1]}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Caches a managed stream for reuse
|
|
90
|
+
*
|
|
91
|
+
* @param managedStream - The managed stream to cache
|
|
92
|
+
*/
|
|
93
|
+
cacheStream(managedStream) {
|
|
94
|
+
this.streamCache.set(managedStream.cacheKey, managedStream);
|
|
95
|
+
this.logger.debug('Cached stream', {
|
|
96
|
+
cacheKey: managedStream.cacheKey,
|
|
97
|
+
direction: managedStream.direction,
|
|
98
|
+
streamId: managedStream.stream.id,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Removes a stream from the cache
|
|
103
|
+
*
|
|
104
|
+
* @param stream - The stream to remove
|
|
105
|
+
*/
|
|
106
|
+
removeStreamFromCache(stream) {
|
|
107
|
+
// Find and remove the stream from cache
|
|
108
|
+
for (const [key, managedStream] of this.streamCache.entries()) {
|
|
109
|
+
if (managedStream.stream.id === stream.id) {
|
|
110
|
+
this.streamCache.delete(key);
|
|
111
|
+
this.logger.debug('Removed stream from cache', {
|
|
112
|
+
cacheKey: key,
|
|
113
|
+
streamId: stream.id,
|
|
114
|
+
});
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Sets up cleanup listener for stream close events
|
|
121
|
+
*
|
|
122
|
+
* @param stream - The stream to monitor
|
|
123
|
+
*/
|
|
124
|
+
setupStreamCleanup(stream) {
|
|
125
|
+
// Listen for stream close event
|
|
126
|
+
stream.addEventListener('close', () => {
|
|
127
|
+
this.removeStreamFromCache(stream);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Extracts the remote peer's address from a connection
|
|
132
|
+
* Falls back to creating an address from the peer ID if no address metadata is available
|
|
133
|
+
*
|
|
134
|
+
* @param connection - The libp2p connection
|
|
135
|
+
* @returns The remote peer's oAddress
|
|
136
|
+
*/
|
|
137
|
+
extractRemotePeerAddress(connection) {
|
|
138
|
+
// Try to get address from connection metadata if available
|
|
139
|
+
// For now, create address from peer ID as fallback
|
|
140
|
+
const peerId = connection.remotePeer.toString();
|
|
141
|
+
// Import oAddress at runtime to avoid circular dependency
|
|
142
|
+
const { oAddress: oAddressClass } = require('@olane/o-core');
|
|
143
|
+
return new oAddressClass(`o://peer/${peerId}`);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Caches an inbound stream for bidirectional reuse
|
|
147
|
+
* This allows the same stream to be reused for responses
|
|
148
|
+
*
|
|
149
|
+
* @param stream - The inbound stream to cache
|
|
150
|
+
* @param callerAddress - The address of the caller
|
|
151
|
+
* @param receiverAddress - The address of the receiver (local node)
|
|
152
|
+
* @param reusePolicy - Whether to enable caching
|
|
153
|
+
*/
|
|
154
|
+
cacheInboundStream(stream, callerAddress, receiverAddress, reusePolicy = 'none') {
|
|
155
|
+
if (reusePolicy !== 'reuse') {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const cacheKey = this.buildCacheKey(callerAddress, receiverAddress);
|
|
159
|
+
// Check if already cached
|
|
160
|
+
if (this.streamCache.has(cacheKey)) {
|
|
161
|
+
this.logger.debug('Stream already cached', { cacheKey });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const managedStream = new oManagedStream(stream, callerAddress, receiverAddress, 'inbound');
|
|
165
|
+
this.cacheStream(managedStream);
|
|
166
|
+
this.setupStreamCleanup(stream);
|
|
167
|
+
}
|
|
74
168
|
/**
|
|
75
169
|
* Gets an existing open stream or creates a new one based on reuse policy
|
|
76
170
|
*
|
|
77
171
|
* @param connection - The libp2p connection
|
|
78
172
|
* @param protocol - The protocol to use for the stream
|
|
79
173
|
* @param config - Stream handler configuration
|
|
174
|
+
* @param streamAddresses - Optional addresses for address-based stream reuse
|
|
80
175
|
*/
|
|
81
|
-
async getOrCreateStream(connection, protocol, config = {}) {
|
|
176
|
+
async getOrCreateStream(connection, protocol, config = {}, streamAddresses) {
|
|
82
177
|
this.logger.debug(`Getting or creating stream for protocol: ${protocol}, connection`, {
|
|
83
178
|
status: connection.status,
|
|
84
179
|
remoteAddr: connection.remoteAddr.toString(),
|
|
85
180
|
streamCount: connection.streams.length,
|
|
86
181
|
reusePolicy: config.reusePolicy ?? 'none',
|
|
182
|
+
hasAddresses: !!streamAddresses,
|
|
87
183
|
});
|
|
88
184
|
if (connection.status !== 'open') {
|
|
89
185
|
throw new oError(oErrorCodes.INVALID_STATE, 'Connection not open');
|
|
90
186
|
}
|
|
91
187
|
const reusePolicy = config.reusePolicy ?? 'none';
|
|
92
|
-
|
|
188
|
+
// Check address-based cache if reuse is enabled and addresses provided
|
|
189
|
+
if (reusePolicy === 'reuse' && streamAddresses) {
|
|
190
|
+
const cacheKey = this.buildCacheKey(streamAddresses.callerAddress, streamAddresses.receiverAddress);
|
|
191
|
+
const cachedStream = this.streamCache.get(cacheKey);
|
|
192
|
+
if (cachedStream?.isReusable) {
|
|
193
|
+
this.logger.debug('Reusing cached stream by address', {
|
|
194
|
+
cacheKey,
|
|
195
|
+
streamId: cachedStream.stream.id,
|
|
196
|
+
direction: cachedStream.direction,
|
|
197
|
+
});
|
|
198
|
+
cachedStream.updateLastUsed();
|
|
199
|
+
return cachedStream.stream;
|
|
200
|
+
}
|
|
201
|
+
else if (cachedStream) {
|
|
202
|
+
// Stream exists but not reusable, remove from cache
|
|
203
|
+
this.logger.debug('Removing non-reusable stream from cache', {
|
|
204
|
+
cacheKey,
|
|
205
|
+
status: cachedStream.stream.status,
|
|
206
|
+
});
|
|
207
|
+
this.streamCache.delete(cacheKey);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
this.logger.debug('No reusable cached stream found, checking connection streams', connection.streams.map((s) => ({
|
|
93
211
|
status: s.status,
|
|
94
212
|
protocol: s.protocol,
|
|
95
213
|
writeStatus: s.writeStatus,
|
|
96
214
|
remoteReadStatus: s.remoteReadStatus,
|
|
97
215
|
id: s.id,
|
|
98
216
|
})));
|
|
99
|
-
//
|
|
100
|
-
if (reusePolicy === 'reuse') {
|
|
217
|
+
// Fallback to protocol-based check (legacy behavior)
|
|
218
|
+
if (reusePolicy === 'reuse' && !streamAddresses) {
|
|
101
219
|
const existingStream = connection.streams.find((stream) => stream.status === 'open' &&
|
|
102
220
|
stream.protocol === protocol &&
|
|
103
221
|
stream.writeStatus === 'writable' &&
|
|
104
222
|
stream.remoteReadStatus === 'readable');
|
|
105
223
|
if (existingStream) {
|
|
106
|
-
this.logger.debug('Reusing existing stream', existingStream.id, existingStream.direction);
|
|
224
|
+
this.logger.debug('Reusing existing stream by protocol (legacy)', existingStream.id, existingStream.direction);
|
|
107
225
|
return existingStream;
|
|
108
226
|
}
|
|
109
227
|
}
|
|
110
228
|
// Create new stream
|
|
229
|
+
this.logger.debug('Creating new stream', { protocol });
|
|
111
230
|
const stream = await connection.newStream(protocol, {
|
|
112
231
|
signal: config.signal,
|
|
113
232
|
maxOutboundStreams: config.maxOutboundStreams ?? 1000,
|
|
114
233
|
runOnLimitedConnection: config.runOnLimitedConnection ?? false,
|
|
115
234
|
});
|
|
235
|
+
// Cache the stream if reuse is enabled and addresses are provided
|
|
236
|
+
if (reusePolicy === 'reuse' && streamAddresses) {
|
|
237
|
+
const managedStream = new oManagedStream(stream, streamAddresses.callerAddress, streamAddresses.receiverAddress, streamAddresses.direction);
|
|
238
|
+
this.cacheStream(managedStream);
|
|
239
|
+
this.setupStreamCleanup(stream);
|
|
240
|
+
}
|
|
116
241
|
return stream;
|
|
117
242
|
}
|
|
118
243
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAoB,MAAM,eAAe,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAKrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IAC/C,OAAO,CAAC,aAAa,CAAiB;IAEhC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAehC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAoB,MAAM,eAAe,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAKrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IAC/C,OAAO,CAAC,aAAa,CAAiB;IAEhC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAehC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAoCnE,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CAiC5D"}
|
package/dist/src/o-node.tool.js
CHANGED
|
@@ -34,6 +34,11 @@ export class oNodeTool extends oTool(oServerNode) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
async handleStream(stream, connection) {
|
|
37
|
+
// Extract caller address from connection
|
|
38
|
+
const callerAddress = this.streamHandler.extractRemotePeerAddress(connection);
|
|
39
|
+
// Cache inbound stream for bidirectional reuse (if reuse policy is enabled)
|
|
40
|
+
// The cacheInboundStream method will check the reuse policy
|
|
41
|
+
this.streamHandler.cacheInboundStream(stream, callerAddress, this.address, 'reuse');
|
|
37
42
|
// Use StreamHandler for consistent stream handling
|
|
38
43
|
// This follows libp2p v3 best practices for length-prefixed streaming
|
|
39
44
|
await this.streamHandler.handleIncomingStream(stream, connection, async (request, stream) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"o-managed-stream.spec.d.ts","sourceRoot":"","sources":["../../test/o-managed-stream.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { oManagedStream } from '../src/connection/o-managed-stream.js';
|
|
3
|
+
import { oAddress } from '@olane/o-core';
|
|
4
|
+
describe('oManagedStream', () => {
|
|
5
|
+
let mockStream;
|
|
6
|
+
let callerAddress;
|
|
7
|
+
let receiverAddress;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Create mock stream
|
|
10
|
+
mockStream = {
|
|
11
|
+
id: 'test-stream-id',
|
|
12
|
+
status: 'open',
|
|
13
|
+
writeStatus: 'writable',
|
|
14
|
+
remoteReadStatus: 'readable',
|
|
15
|
+
direction: 'outbound',
|
|
16
|
+
protocol: '/o/test',
|
|
17
|
+
addEventListener: () => { },
|
|
18
|
+
};
|
|
19
|
+
callerAddress = new oAddress('o://caller');
|
|
20
|
+
receiverAddress = new oAddress('o://receiver');
|
|
21
|
+
});
|
|
22
|
+
describe('Construction', () => {
|
|
23
|
+
it('should create managed stream with correct properties', () => {
|
|
24
|
+
const managedStream = new oManagedStream(mockStream, callerAddress, receiverAddress, 'outbound');
|
|
25
|
+
expect(managedStream.stream).to.equal(mockStream);
|
|
26
|
+
expect(managedStream.callerAddress).to.equal(callerAddress);
|
|
27
|
+
expect(managedStream.receiverAddress).to.equal(receiverAddress);
|
|
28
|
+
expect(managedStream.direction).to.equal('outbound');
|
|
29
|
+
expect(managedStream.createdAt).to.be.a('number');
|
|
30
|
+
expect(managedStream.lastUsedAt).to.equal(managedStream.createdAt);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('Cache Key Generation', () => {
|
|
34
|
+
it('should generate bidirectional cache key (A→B === B→A)', () => {
|
|
35
|
+
const streamAtoB = new oManagedStream(mockStream, callerAddress, receiverAddress, 'outbound');
|
|
36
|
+
const streamBtoA = new oManagedStream(mockStream, receiverAddress, callerAddress, 'inbound');
|
|
37
|
+
// Cache keys should be identical regardless of direction
|
|
38
|
+
expect(streamAtoB.cacheKey).to.equal(streamBtoA.cacheKey);
|
|
39
|
+
});
|
|
40
|
+
it('should generate different keys for different address pairs', () => {
|
|
41
|
+
const address1 = new oAddress('o://node-1');
|
|
42
|
+
const address2 = new oAddress('o://node-2');
|
|
43
|
+
const address3 = new oAddress('o://node-3');
|
|
44
|
+
const stream1 = new oManagedStream(mockStream, address1, address2, 'outbound');
|
|
45
|
+
const stream2 = new oManagedStream(mockStream, address1, address3, 'outbound');
|
|
46
|
+
expect(stream1.cacheKey).to.not.equal(stream2.cacheKey);
|
|
47
|
+
});
|
|
48
|
+
it('should create stable, sorted cache keys', () => {
|
|
49
|
+
const managedStream = new oManagedStream(mockStream, callerAddress, receiverAddress, 'outbound');
|
|
50
|
+
const expectedKey = [callerAddress.value, receiverAddress.value]
|
|
51
|
+
.sort()
|
|
52
|
+
.join('↔');
|
|
53
|
+
expect(managedStream.cacheKey).to.equal(expectedKey);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('Reusability Check', () => {
|
|
57
|
+
it('should be reusable when stream is open, writable, and readable', () => {
|
|
58
|
+
mockStream.status = 'open';
|
|
59
|
+
mockStream.writeStatus = 'writable';
|
|
60
|
+
mockStream.remoteReadStatus = 'readable';
|
|
61
|
+
const managedStream = new oManagedStream(mockStream, callerAddress, receiverAddress, 'outbound');
|
|
62
|
+
expect(managedStream.isReusable).to.be.true;
|
|
63
|
+
});
|
|
64
|
+
it('should not be reusable when stream is closed', () => {
|
|
65
|
+
mockStream.status = 'closed';
|
|
66
|
+
mockStream.writeStatus = 'writable';
|
|
67
|
+
mockStream.remoteReadStatus = 'readable';
|
|
68
|
+
const managedStream = new oManagedStream(mockStream, callerAddress, receiverAddress, 'outbound');
|
|
69
|
+
expect(managedStream.isReusable).to.be.false;
|
|
70
|
+
});
|
|
71
|
+
it('should not be reusable when stream is not writable', () => {
|
|
72
|
+
mockStream.status = 'open';
|
|
73
|
+
mockStream.writeStatus = 'closed';
|
|
74
|
+
mockStream.remoteReadStatus = 'readable';
|
|
75
|
+
const managedStream = new oManagedStream(mockStream, callerAddress, receiverAddress, 'outbound');
|
|
76
|
+
expect(managedStream.isReusable).to.be.false;
|
|
77
|
+
});
|
|
78
|
+
it('should not be reusable when remote is not readable', () => {
|
|
79
|
+
mockStream.status = 'open';
|
|
80
|
+
mockStream.writeStatus = 'writable';
|
|
81
|
+
mockStream.remoteReadStatus = 'closed';
|
|
82
|
+
const managedStream = new oManagedStream(mockStream, callerAddress, receiverAddress, 'outbound');
|
|
83
|
+
expect(managedStream.isReusable).to.be.false;
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('Timestamp Management', () => {
|
|
87
|
+
it('should update lastUsedAt when updateLastUsed is called', async () => {
|
|
88
|
+
const managedStream = new oManagedStream(mockStream, callerAddress, receiverAddress, 'outbound');
|
|
89
|
+
const initialLastUsed = managedStream.lastUsedAt;
|
|
90
|
+
// Wait a bit to ensure timestamp difference
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
92
|
+
managedStream.updateLastUsed();
|
|
93
|
+
expect(managedStream.lastUsedAt).to.be.greaterThan(initialLastUsed);
|
|
94
|
+
});
|
|
95
|
+
it('should calculate age correctly', async () => {
|
|
96
|
+
const managedStream = new oManagedStream(mockStream, callerAddress, receiverAddress, 'outbound');
|
|
97
|
+
// Wait a bit
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
99
|
+
expect(managedStream.age).to.be.greaterThan(0);
|
|
100
|
+
expect(managedStream.age).to.be.lessThan(1000); // Should be less than 1 second
|
|
101
|
+
});
|
|
102
|
+
it('should calculate idle time correctly', async () => {
|
|
103
|
+
const managedStream = new oManagedStream(mockStream, callerAddress, receiverAddress, 'outbound');
|
|
104
|
+
// Wait a bit without using
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
106
|
+
expect(managedStream.idleTime).to.be.greaterThan(0);
|
|
107
|
+
// Update and check idle time resets
|
|
108
|
+
managedStream.updateLastUsed();
|
|
109
|
+
expect(managedStream.idleTime).to.be.lessThan(5);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('String Representation', () => {
|
|
113
|
+
it('should provide meaningful toString output', () => {
|
|
114
|
+
const managedStream = new oManagedStream(mockStream, callerAddress, receiverAddress, 'outbound');
|
|
115
|
+
const str = managedStream.toString();
|
|
116
|
+
expect(str).to.include('oManagedStream');
|
|
117
|
+
expect(str).to.include(managedStream.cacheKey);
|
|
118
|
+
expect(str).to.include('outbound');
|
|
119
|
+
expect(str).to.include('open');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-handler-caching.spec.d.ts","sourceRoot":"","sources":["../../test/stream-handler-caching.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { StreamHandler } from '../src/connection/stream-handler.js';
|
|
3
|
+
import { oAddress } from '@olane/o-core';
|
|
4
|
+
describe('StreamHandler Address-Based Caching', () => {
|
|
5
|
+
let streamHandler;
|
|
6
|
+
let mockConnection;
|
|
7
|
+
let mockStream;
|
|
8
|
+
let callerAddress;
|
|
9
|
+
let receiverAddress;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
streamHandler = new StreamHandler();
|
|
12
|
+
// Create mock connection
|
|
13
|
+
mockConnection = {
|
|
14
|
+
status: 'open',
|
|
15
|
+
remotePeer: {
|
|
16
|
+
toString: () => 'test-peer-id',
|
|
17
|
+
},
|
|
18
|
+
remoteAddr: {
|
|
19
|
+
toString: () => '/memory/test',
|
|
20
|
+
},
|
|
21
|
+
streams: [],
|
|
22
|
+
newStream: async () => mockStream,
|
|
23
|
+
};
|
|
24
|
+
// Create mock stream
|
|
25
|
+
mockStream = {
|
|
26
|
+
id: 'test-stream-id',
|
|
27
|
+
status: 'open',
|
|
28
|
+
writeStatus: 'writable',
|
|
29
|
+
remoteReadStatus: 'readable',
|
|
30
|
+
direction: 'outbound',
|
|
31
|
+
protocol: '/o/test',
|
|
32
|
+
addEventListener: () => { },
|
|
33
|
+
};
|
|
34
|
+
callerAddress = new oAddress('o://caller');
|
|
35
|
+
receiverAddress = new oAddress('o://receiver');
|
|
36
|
+
});
|
|
37
|
+
describe('getOrCreateStream with Address-Based Caching', () => {
|
|
38
|
+
it('should create new stream when cache is empty', async () => {
|
|
39
|
+
const config = {
|
|
40
|
+
reusePolicy: 'reuse',
|
|
41
|
+
};
|
|
42
|
+
const streamAddresses = {
|
|
43
|
+
callerAddress,
|
|
44
|
+
receiverAddress,
|
|
45
|
+
direction: 'outbound',
|
|
46
|
+
};
|
|
47
|
+
const stream = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamAddresses);
|
|
48
|
+
expect(stream).to.equal(mockStream);
|
|
49
|
+
});
|
|
50
|
+
it('should reuse cached stream with same caller-receiver pair', async () => {
|
|
51
|
+
const config = {
|
|
52
|
+
reusePolicy: 'reuse',
|
|
53
|
+
};
|
|
54
|
+
const streamAddresses = {
|
|
55
|
+
callerAddress,
|
|
56
|
+
receiverAddress,
|
|
57
|
+
direction: 'outbound',
|
|
58
|
+
};
|
|
59
|
+
// First call creates and caches
|
|
60
|
+
const stream1 = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamAddresses);
|
|
61
|
+
// Second call should reuse
|
|
62
|
+
const stream2 = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamAddresses);
|
|
63
|
+
expect(stream1).to.equal(stream2);
|
|
64
|
+
});
|
|
65
|
+
it('should reuse stream bidirectionally (A→B same as B→A)', async () => {
|
|
66
|
+
const config = {
|
|
67
|
+
reusePolicy: 'reuse',
|
|
68
|
+
};
|
|
69
|
+
// First: caller → receiver
|
|
70
|
+
const streamAtoB = {
|
|
71
|
+
callerAddress,
|
|
72
|
+
receiverAddress,
|
|
73
|
+
direction: 'outbound',
|
|
74
|
+
};
|
|
75
|
+
const stream1 = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamAtoB);
|
|
76
|
+
// Second: receiver → caller (reversed)
|
|
77
|
+
const streamBtoA = {
|
|
78
|
+
callerAddress: receiverAddress,
|
|
79
|
+
receiverAddress: callerAddress,
|
|
80
|
+
direction: 'inbound',
|
|
81
|
+
};
|
|
82
|
+
const stream2 = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamBtoA);
|
|
83
|
+
// Should be the same stream due to bidirectional caching
|
|
84
|
+
expect(stream1).to.equal(stream2);
|
|
85
|
+
});
|
|
86
|
+
it('should create new stream for different address pairs', async () => {
|
|
87
|
+
const config = {
|
|
88
|
+
reusePolicy: 'reuse',
|
|
89
|
+
};
|
|
90
|
+
const otherReceiver = new oAddress('o://other-receiver');
|
|
91
|
+
// Create new mock stream for second call
|
|
92
|
+
const mockStream2 = {
|
|
93
|
+
...mockStream,
|
|
94
|
+
id: 'test-stream-id-2',
|
|
95
|
+
};
|
|
96
|
+
let streamCount = 0;
|
|
97
|
+
mockConnection.newStream = async () => {
|
|
98
|
+
streamCount++;
|
|
99
|
+
return (streamCount === 1 ? mockStream : mockStream2);
|
|
100
|
+
};
|
|
101
|
+
// First pair
|
|
102
|
+
const stream1 = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, {
|
|
103
|
+
callerAddress,
|
|
104
|
+
receiverAddress,
|
|
105
|
+
direction: 'outbound',
|
|
106
|
+
});
|
|
107
|
+
// Different pair
|
|
108
|
+
const stream2 = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, {
|
|
109
|
+
callerAddress,
|
|
110
|
+
receiverAddress: otherReceiver,
|
|
111
|
+
direction: 'outbound',
|
|
112
|
+
});
|
|
113
|
+
// Should be different streams
|
|
114
|
+
expect(stream1.id).to.not.equal(stream2.id);
|
|
115
|
+
});
|
|
116
|
+
it('should not cache when reusePolicy is none', async () => {
|
|
117
|
+
const config = {
|
|
118
|
+
reusePolicy: 'none',
|
|
119
|
+
};
|
|
120
|
+
const streamAddresses = {
|
|
121
|
+
callerAddress,
|
|
122
|
+
receiverAddress,
|
|
123
|
+
direction: 'outbound',
|
|
124
|
+
};
|
|
125
|
+
let callCount = 0;
|
|
126
|
+
mockConnection.newStream = async () => {
|
|
127
|
+
callCount++;
|
|
128
|
+
return {
|
|
129
|
+
...mockStream,
|
|
130
|
+
id: `stream-${callCount}`,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
const stream1 = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamAddresses);
|
|
134
|
+
const stream2 = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamAddresses);
|
|
135
|
+
// Should be different streams (not cached)
|
|
136
|
+
expect(stream1.id).to.not.equal(stream2.id);
|
|
137
|
+
});
|
|
138
|
+
it('should remove non-reusable streams from cache', async () => {
|
|
139
|
+
const config = {
|
|
140
|
+
reusePolicy: 'reuse',
|
|
141
|
+
};
|
|
142
|
+
const streamAddresses = {
|
|
143
|
+
callerAddress,
|
|
144
|
+
receiverAddress,
|
|
145
|
+
direction: 'outbound',
|
|
146
|
+
};
|
|
147
|
+
// First call creates and caches
|
|
148
|
+
const stream1 = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamAddresses);
|
|
149
|
+
// Make stream non-reusable
|
|
150
|
+
mockStream.status = 'closed';
|
|
151
|
+
// Create new mock stream for second call
|
|
152
|
+
const mockStream2 = {
|
|
153
|
+
...mockStream,
|
|
154
|
+
id: 'test-stream-id-2',
|
|
155
|
+
status: 'open',
|
|
156
|
+
};
|
|
157
|
+
mockConnection.newStream = async () => mockStream2;
|
|
158
|
+
// Second call should create new stream (old one not reusable)
|
|
159
|
+
const stream2 = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamAddresses);
|
|
160
|
+
expect(stream1.id).to.not.equal(stream2.id);
|
|
161
|
+
});
|
|
162
|
+
it('should fall back to protocol-based check when addresses not provided', async () => {
|
|
163
|
+
const config = {
|
|
164
|
+
reusePolicy: 'reuse',
|
|
165
|
+
};
|
|
166
|
+
// Add mock stream to connection streams
|
|
167
|
+
mockConnection.streams = [mockStream];
|
|
168
|
+
// Call without streamAddresses
|
|
169
|
+
const stream = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config);
|
|
170
|
+
// Should find and return the existing stream by protocol
|
|
171
|
+
expect(stream).to.equal(mockStream);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe('extractRemotePeerAddress', () => {
|
|
175
|
+
it('should extract address from connection peer ID', () => {
|
|
176
|
+
const address = streamHandler.extractRemotePeerAddress(mockConnection);
|
|
177
|
+
expect(address.value).to.include('o://peer/');
|
|
178
|
+
expect(address.value).to.include('test-peer-id');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('cacheInboundStream', () => {
|
|
182
|
+
it('should cache inbound stream when reuse is enabled', async () => {
|
|
183
|
+
streamHandler.cacheInboundStream(mockStream, callerAddress, receiverAddress, 'reuse');
|
|
184
|
+
// Try to get the cached stream
|
|
185
|
+
const config = {
|
|
186
|
+
reusePolicy: 'reuse',
|
|
187
|
+
};
|
|
188
|
+
const streamAddresses = {
|
|
189
|
+
callerAddress,
|
|
190
|
+
receiverAddress,
|
|
191
|
+
direction: 'outbound',
|
|
192
|
+
};
|
|
193
|
+
const stream = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamAddresses);
|
|
194
|
+
// Should reuse the cached inbound stream
|
|
195
|
+
expect(stream).to.equal(mockStream);
|
|
196
|
+
});
|
|
197
|
+
it('should not cache when reusePolicy is none', async () => {
|
|
198
|
+
streamHandler.cacheInboundStream(mockStream, callerAddress, receiverAddress, 'none');
|
|
199
|
+
// Try to get stream - should create new one
|
|
200
|
+
const config = {
|
|
201
|
+
reusePolicy: 'reuse',
|
|
202
|
+
};
|
|
203
|
+
const newMockStream = { ...mockStream, id: 'new-stream-id' };
|
|
204
|
+
mockConnection.newStream = async () => newMockStream;
|
|
205
|
+
const streamAddresses = {
|
|
206
|
+
callerAddress,
|
|
207
|
+
receiverAddress,
|
|
208
|
+
direction: 'outbound',
|
|
209
|
+
};
|
|
210
|
+
const stream = await streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamAddresses);
|
|
211
|
+
// Should not reuse (was not cached)
|
|
212
|
+
expect(stream.id).to.equal('new-stream-id');
|
|
213
|
+
});
|
|
214
|
+
it('should not cache duplicate streams', () => {
|
|
215
|
+
// Cache first time
|
|
216
|
+
streamHandler.cacheInboundStream(mockStream, callerAddress, receiverAddress, 'reuse');
|
|
217
|
+
// Try to cache again - should be no-op
|
|
218
|
+
streamHandler.cacheInboundStream(mockStream, callerAddress, receiverAddress, 'reuse');
|
|
219
|
+
// No error should occur, and stream should still be cached
|
|
220
|
+
// (This test mainly ensures no exception is thrown)
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
describe('Stream Cleanup', () => {
|
|
224
|
+
it('should remove stream from cache on close event', (done) => {
|
|
225
|
+
const config = {
|
|
226
|
+
reusePolicy: 'reuse',
|
|
227
|
+
};
|
|
228
|
+
let closeListener;
|
|
229
|
+
// Override addEventListener to capture the close listener
|
|
230
|
+
mockStream.addEventListener = (event, listener) => {
|
|
231
|
+
if (event === 'close') {
|
|
232
|
+
closeListener = listener;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const streamAddresses = {
|
|
236
|
+
callerAddress,
|
|
237
|
+
receiverAddress,
|
|
238
|
+
direction: 'outbound',
|
|
239
|
+
};
|
|
240
|
+
streamHandler
|
|
241
|
+
.getOrCreateStream(mockConnection, '/o/test', config, streamAddresses)
|
|
242
|
+
.then(() => {
|
|
243
|
+
expect(closeListener).to.not.be.undefined;
|
|
244
|
+
// Simulate close event
|
|
245
|
+
if (closeListener) {
|
|
246
|
+
closeListener();
|
|
247
|
+
}
|
|
248
|
+
// Create new stream for next call
|
|
249
|
+
const newMockStream = { ...mockStream, id: 'new-stream-after-close' };
|
|
250
|
+
mockConnection.newStream = async () => newMockStream;
|
|
251
|
+
// Try to get stream again - should create new one since old one was removed
|
|
252
|
+
return streamHandler.getOrCreateStream(mockConnection, '/o/test', config, streamAddresses);
|
|
253
|
+
})
|
|
254
|
+
.then((stream) => {
|
|
255
|
+
expect(stream.id).to.equal('new-stream-after-close');
|
|
256
|
+
done();
|
|
257
|
+
})
|
|
258
|
+
.catch(done);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olane/o-node",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.40",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@eslint/eslintrc": "^3.3.1",
|
|
42
42
|
"@eslint/js": "^9.29.0",
|
|
43
|
-
"@olane/o-test": "0.7.
|
|
43
|
+
"@olane/o-test": "0.7.40",
|
|
44
44
|
"@tsconfig/node20": "^20.1.6",
|
|
45
45
|
"@types/jest": "^30.0.0",
|
|
46
46
|
"@types/json5": "^2.2.0",
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
"typescript": "5.4.5"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@olane/o-config": "0.7.
|
|
64
|
-
"@olane/o-core": "0.7.
|
|
65
|
-
"@olane/o-protocol": "0.7.
|
|
66
|
-
"@olane/o-tool": "0.7.
|
|
63
|
+
"@olane/o-config": "0.7.40",
|
|
64
|
+
"@olane/o-core": "0.7.40",
|
|
65
|
+
"@olane/o-protocol": "0.7.40",
|
|
66
|
+
"@olane/o-tool": "0.7.40",
|
|
67
67
|
"debug": "^4.4.1",
|
|
68
68
|
"dotenv": "^16.5.0",
|
|
69
69
|
"json5": "^2.2.3"
|
|
70
70
|
},
|
|
71
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "d2d7cb62d69249fe63d4cb293dbbdfc44d250f17"
|
|
72
72
|
}
|