@olane/o-node 0.7.11 → 0.7.12-alpha.10
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/LICENSE +34 -0
- package/dist/src/connection/o-node-connection.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.js +8 -2
- package/dist/src/connection/o-node-connection.manager.d.ts +1 -1
- package/dist/src/connection/o-node-connection.manager.d.ts.map +1 -1
- package/dist/src/connection/o-node-connection.manager.js +39 -19
- package/dist/src/interfaces/i-reconnectable-node.d.ts +45 -0
- package/dist/src/interfaces/i-reconnectable-node.d.ts.map +1 -0
- package/dist/src/interfaces/i-reconnectable-node.js +1 -0
- package/dist/src/interfaces/o-node.config.d.ts +37 -0
- package/dist/src/interfaces/o-node.config.d.ts.map +1 -1
- package/dist/src/managers/o-connection-heartbeat.manager.d.ts +67 -0
- package/dist/src/managers/o-connection-heartbeat.manager.d.ts.map +1 -0
- package/dist/src/managers/o-connection-heartbeat.manager.js +224 -0
- package/dist/src/managers/o-reconnection.manager.d.ts +50 -0
- package/dist/src/managers/o-reconnection.manager.d.ts.map +1 -0
- package/dist/src/managers/o-reconnection.manager.js +255 -0
- package/dist/src/o-node.d.ts +18 -1
- package/dist/src/o-node.d.ts.map +1 -1
- package/dist/src/o-node.js +85 -0
- package/dist/src/o-node.notification-manager.d.ts +52 -0
- package/dist/src/o-node.notification-manager.d.ts.map +1 -0
- package/dist/src/o-node.notification-manager.js +185 -0
- package/dist/src/o-node.tool.d.ts.map +1 -1
- package/dist/src/o-node.tool.js +19 -4
- package/dist/src/router/resolvers/o-node.search-resolver.d.ts.map +1 -1
- package/dist/src/router/resolvers/o-node.search-resolver.js +6 -2
- package/dist/src/utils/leader-request-wrapper.d.ts +45 -0
- package/dist/src/utils/leader-request-wrapper.d.ts.map +1 -0
- package/dist/src/utils/leader-request-wrapper.js +89 -0
- package/dist/test/search-resolver.spec.d.ts +2 -0
- package/dist/test/search-resolver.spec.d.ts.map +1 -0
- package/dist/test/search-resolver.spec.js +693 -0
- package/package.json +7 -10
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { oNotificationManager, NodeConnectedEvent, NodeDisconnectedEvent, NodeDiscoveredEvent, ChildJoinedEvent, ChildLeftEvent, ParentConnectedEvent, ParentDisconnectedEvent, } from '@olane/o-core';
|
|
2
|
+
/**
|
|
3
|
+
* libp2p-specific implementation of oNotificationManager
|
|
4
|
+
* Wraps libp2p events and enriches them with Olane context
|
|
5
|
+
*/
|
|
6
|
+
export class oNodeNotificationManager extends oNotificationManager {
|
|
7
|
+
constructor(p2pNode, hierarchyManager, address) {
|
|
8
|
+
super();
|
|
9
|
+
this.p2pNode = p2pNode;
|
|
10
|
+
this.hierarchyManager = hierarchyManager;
|
|
11
|
+
this.address = address;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Wire up libp2p event listeners
|
|
15
|
+
*/
|
|
16
|
+
setupListeners() {
|
|
17
|
+
this.logger.debug('Setting up libp2p event listeners...');
|
|
18
|
+
// Peer connection events
|
|
19
|
+
this.p2pNode.addEventListener('peer:connect', this.handlePeerConnect.bind(this));
|
|
20
|
+
this.p2pNode.addEventListener('peer:disconnect', this.handlePeerDisconnect.bind(this));
|
|
21
|
+
// Peer discovery events
|
|
22
|
+
this.p2pNode.addEventListener('peer:discovery', this.handlePeerDiscovery.bind(this));
|
|
23
|
+
// Connection events
|
|
24
|
+
this.p2pNode.addEventListener('connection:open', this.handleConnectionOpen.bind(this));
|
|
25
|
+
this.p2pNode.addEventListener('connection:close', this.handleConnectionClose.bind(this));
|
|
26
|
+
this.logger.debug('libp2p event listeners configured');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Handle peer connect event from libp2p
|
|
30
|
+
*/
|
|
31
|
+
handlePeerConnect(evt) {
|
|
32
|
+
const peerId = evt.detail;
|
|
33
|
+
// this.logger.debug(`Peer connected: ${peerId.toString()}`);
|
|
34
|
+
// Try to resolve peer ID to Olane address
|
|
35
|
+
const nodeAddress = this.peerIdToAddress(peerId.toString());
|
|
36
|
+
if (!nodeAddress) {
|
|
37
|
+
// this.logger.debug(
|
|
38
|
+
// `Could not resolve peer ID ${peerId.toString()} to address`,
|
|
39
|
+
// );
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Emit generic node connected event
|
|
43
|
+
this.emit(new NodeConnectedEvent({
|
|
44
|
+
source: this.address,
|
|
45
|
+
nodeAddress,
|
|
46
|
+
connectionMetadata: {
|
|
47
|
+
peerId: peerId.toString(),
|
|
48
|
+
transport: 'libp2p',
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
// Check if this is a child node
|
|
52
|
+
if (this.isChild(nodeAddress)) {
|
|
53
|
+
// this.logger.debug(`Child node connected: ${nodeAddress.toString()}`);
|
|
54
|
+
this.emit(new ChildJoinedEvent({
|
|
55
|
+
source: this.address,
|
|
56
|
+
childAddress: nodeAddress,
|
|
57
|
+
parentAddress: this.address,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
// Check if this is a parent node
|
|
61
|
+
if (this.isParent(nodeAddress)) {
|
|
62
|
+
// this.logger.debug(`Parent node connected: ${nodeAddress.toString()}`);
|
|
63
|
+
this.emit(new ParentConnectedEvent({
|
|
64
|
+
source: this.address,
|
|
65
|
+
parentAddress: nodeAddress,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Handle peer disconnect event from libp2p
|
|
71
|
+
*/
|
|
72
|
+
handlePeerDisconnect(evt) {
|
|
73
|
+
const peerId = evt.detail;
|
|
74
|
+
// this.logger.debug(`Peer disconnected: ${peerId.toString()}`);
|
|
75
|
+
// Try to resolve peer ID to Olane address
|
|
76
|
+
const nodeAddress = this.peerIdToAddress(peerId.toString());
|
|
77
|
+
if (!nodeAddress) {
|
|
78
|
+
// this.logger.debug(
|
|
79
|
+
// `Could not resolve peer ID ${peerId.toString()} to address`,
|
|
80
|
+
// );
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Emit generic node disconnected event
|
|
84
|
+
this.emit(new NodeDisconnectedEvent({
|
|
85
|
+
source: this.address,
|
|
86
|
+
nodeAddress,
|
|
87
|
+
reason: 'peer_disconnected',
|
|
88
|
+
}));
|
|
89
|
+
// Check if this is a child node
|
|
90
|
+
if (this.isChild(nodeAddress)) {
|
|
91
|
+
this.logger.debug(`Child node disconnected: ${nodeAddress.toString()}`);
|
|
92
|
+
this.emit(new ChildLeftEvent({
|
|
93
|
+
source: this.address,
|
|
94
|
+
childAddress: nodeAddress,
|
|
95
|
+
parentAddress: this.address,
|
|
96
|
+
reason: 'peer_disconnected',
|
|
97
|
+
}));
|
|
98
|
+
// Optionally remove from hierarchy (auto-cleanup)
|
|
99
|
+
// this.hierarchyManager.removeChild(nodeAddress);
|
|
100
|
+
}
|
|
101
|
+
// Check if this is a parent node
|
|
102
|
+
if (this.isParent(nodeAddress)) {
|
|
103
|
+
this.logger.debug(`Parent node disconnected: ${nodeAddress.toString()}`);
|
|
104
|
+
this.emit(new ParentDisconnectedEvent({
|
|
105
|
+
source: this.address,
|
|
106
|
+
parentAddress: nodeAddress,
|
|
107
|
+
reason: 'peer_disconnected',
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Handle peer discovery event from libp2p
|
|
113
|
+
*/
|
|
114
|
+
handlePeerDiscovery(evt) {
|
|
115
|
+
const peerInfo = evt.detail;
|
|
116
|
+
// this.logger.debug(`Peer discovered: ${peerInfo.id.toString()}`);
|
|
117
|
+
// Try to resolve peer ID to Olane address
|
|
118
|
+
const nodeAddress = this.peerIdToAddress(peerInfo.id.toString());
|
|
119
|
+
if (!nodeAddress) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.emit(new NodeDiscoveredEvent({
|
|
123
|
+
source: this.address,
|
|
124
|
+
nodeAddress,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Handle connection open event from libp2p
|
|
129
|
+
*/
|
|
130
|
+
handleConnectionOpen(evt) {
|
|
131
|
+
// do nothing for now
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Handle connection close event from libp2p
|
|
135
|
+
*/
|
|
136
|
+
handleConnectionClose(evt) {
|
|
137
|
+
// do nothing for now
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Try to resolve a libp2p peer ID to an Olane address
|
|
141
|
+
* Checks hierarchy manager for known peers
|
|
142
|
+
*/
|
|
143
|
+
peerIdToAddress(peerId) {
|
|
144
|
+
// Check children
|
|
145
|
+
for (const child of this.hierarchyManager.children) {
|
|
146
|
+
const childTransports = child.transports;
|
|
147
|
+
for (const transport of childTransports) {
|
|
148
|
+
if (transport.toString().includes(peerId)) {
|
|
149
|
+
return child;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Check parents
|
|
154
|
+
for (const parent of this.hierarchyManager.parents) {
|
|
155
|
+
const parentTransports = parent.transports;
|
|
156
|
+
for (const transport of parentTransports) {
|
|
157
|
+
if (transport.toString().includes(peerId)) {
|
|
158
|
+
return parent;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Check leaders
|
|
163
|
+
for (const leader of this.hierarchyManager.leaders) {
|
|
164
|
+
const leaderTransports = leader.transports;
|
|
165
|
+
for (const transport of leaderTransports) {
|
|
166
|
+
if (transport.toString().includes(peerId)) {
|
|
167
|
+
return leader;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check if an address is a direct child
|
|
175
|
+
*/
|
|
176
|
+
isChild(address) {
|
|
177
|
+
return this.hierarchyManager.children.some((child) => child.toString() === address.toString());
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Check if an address is a parent
|
|
181
|
+
*/
|
|
182
|
+
isParent(address) {
|
|
183
|
+
return this.hierarchyManager.parents.some((parent) => parent.toString() === address.toString());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EAGR,QAAQ,
|
|
1
|
+
{"version":3,"file":"o-node.tool.d.ts","sourceRoot":"","sources":["../../src/o-node.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EAGR,QAAQ,EAGT,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;;AAIrD;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAAkB;IACzC,cAAc,CAAC,OAAO,EAAE,QAAQ;IAQhC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAW3B,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAiDnE,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC;IAQ9B,oBAAoB,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;CA2B5D"}
|
package/dist/src/o-node.tool.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { CoreUtils,
|
|
1
|
+
import { CoreUtils, oError, oErrorCodes, oRequest, ChildJoinedEvent, } from '@olane/o-core';
|
|
2
2
|
import { oTool } from '@olane/o-tool';
|
|
3
3
|
import { oServerNode } from './nodes/server.node.js';
|
|
4
4
|
import { oNodeTransport } from './router/o-node.transport.js';
|
|
5
|
+
import { oNodeAddress } from './router/o-node.address.js';
|
|
5
6
|
/**
|
|
6
7
|
* oTool is a mixin that extends the base class and implements the oTool interface
|
|
7
8
|
* @param Base - The base class to extend
|
|
@@ -24,7 +25,10 @@ export class oNodeTool extends oTool(oServerNode) {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
async handleStream(stream, connection) {
|
|
27
|
-
|
|
28
|
+
// CRITICAL: Attach message listener immediately to prevent buffer overflow (libp2p v3)
|
|
29
|
+
// Per libp2p migration guide: "If no message event handler is added, streams will
|
|
30
|
+
// buffer incoming data until a pre-configured limit is reached, after which the stream will be reset."
|
|
31
|
+
const messageHandler = async (event) => {
|
|
28
32
|
if (!event.data) {
|
|
29
33
|
this.logger.warn('Malformed event data');
|
|
30
34
|
return;
|
|
@@ -52,7 +56,9 @@ export class oNodeTool extends oTool(oServerNode) {
|
|
|
52
56
|
const response = CoreUtils.buildResponse(request, result, result?.error);
|
|
53
57
|
// add the request method to the response
|
|
54
58
|
await CoreUtils.sendResponse(response, stream);
|
|
55
|
-
}
|
|
59
|
+
};
|
|
60
|
+
// Attach listener synchronously before any async operations
|
|
61
|
+
stream.addEventListener('message', messageHandler);
|
|
56
62
|
}
|
|
57
63
|
async _tool_identify() {
|
|
58
64
|
return {
|
|
@@ -64,8 +70,17 @@ export class oNodeTool extends oTool(oServerNode) {
|
|
|
64
70
|
async _tool_child_register(request) {
|
|
65
71
|
this.logger.debug('Child register: ', request.params);
|
|
66
72
|
const { address, transports } = request.params;
|
|
67
|
-
const childAddress = new
|
|
73
|
+
const childAddress = new oNodeAddress(address, transports.map((t) => new oNodeTransport(t)));
|
|
74
|
+
// Add child to hierarchy
|
|
68
75
|
this.hierarchyManager.addChild(childAddress);
|
|
76
|
+
// Emit child joined event
|
|
77
|
+
if (this.notificationManager) {
|
|
78
|
+
this.notificationManager.emit(new ChildJoinedEvent({
|
|
79
|
+
source: this.address,
|
|
80
|
+
childAddress,
|
|
81
|
+
parentAddress: this.address,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
69
84
|
return {
|
|
70
85
|
message: `Child node registered with parent! ${childAddress.toString()}`,
|
|
71
86
|
parentTransports: this.parentTransports.map((t) => t.toString()),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"o-node.search-resolver.d.ts","sourceRoot":"","sources":["../../../../src/router/resolvers/o-node.search-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,KAAK,EAEL,UAAU,EACV,cAAc,EAEd,aAAa,EAEd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AACH,qBAAa,eAAgB,SAAQ,gBAAgB;IACvC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ;gBAAjB,OAAO,EAAE,QAAQ;IAIhD,IAAI,gBAAgB,IAAI,UAAU,EAAE,CAEnC;IAED;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,IAAI,QAAQ;IAIxC;;;;OAIG;IACH,SAAS,CAAC,eAAe,IAAI,MAAM;IAInC;;;;;OAKG;IACH,SAAS,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,GAAG,GAAG;IAOnD;;;;;;OAMG;IACH,SAAS,CAAC,mBAAmB,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,GAAG,GAAG,EAAE;IASjE;;;;;OAKG;IACH,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI;IAIlD;;;;;OAKG;IACH,SAAS,CAAC,aAAa,CAAC,MAAM,EAAE,GAAG,GAAG,cAAc,EAAE;IAOtD;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,wBAAwB,CAChC,OAAO,EAAE,QAAQ,EACjB,gBAAgB,EAAE,cAAc,EAAE,EAClC,IAAI,EAAE,KAAK,GACV,cAAc,EAAE;IAgBnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyCG;IACH,SAAS,CAAC,gBAAgB,CACxB,IAAI,EAAE,KAAK,EACX,qBAAqB,EAAE,QAAQ,EAC/B,YAAY,EAAE,GAAG,GAChB,QAAQ;IAsBL,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"o-node.search-resolver.d.ts","sourceRoot":"","sources":["../../../../src/router/resolvers/o-node.search-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,KAAK,EAEL,UAAU,EACV,cAAc,EAEd,aAAa,EAEd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AACH,qBAAa,eAAgB,SAAQ,gBAAgB;IACvC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ;gBAAjB,OAAO,EAAE,QAAQ;IAIhD,IAAI,gBAAgB,IAAI,UAAU,EAAE,CAEnC;IAED;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,IAAI,QAAQ;IAIxC;;;;OAIG;IACH,SAAS,CAAC,eAAe,IAAI,MAAM;IAInC;;;;;OAKG;IACH,SAAS,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,GAAG,GAAG;IAOnD;;;;;;OAMG;IACH,SAAS,CAAC,mBAAmB,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,GAAG,GAAG,EAAE;IASjE;;;;;OAKG;IACH,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI;IAIlD;;;;;OAKG;IACH,SAAS,CAAC,aAAa,CAAC,MAAM,EAAE,GAAG,GAAG,cAAc,EAAE;IAOtD;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,wBAAwB,CAChC,OAAO,EAAE,QAAQ,EACjB,gBAAgB,EAAE,cAAc,EAAE,EAClC,IAAI,EAAE,KAAK,GACV,cAAc,EAAE;IAgBnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyCG;IACH,SAAS,CAAC,gBAAgB,CACxB,IAAI,EAAE,KAAK,EACX,qBAAqB,EAAE,QAAQ,EAC/B,YAAY,EAAE,GAAG,GAChB,QAAQ;IAsBL,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;CAoE/D"}
|
|
@@ -228,7 +228,6 @@ export class oSearchResolver extends oAddressResolver {
|
|
|
228
228
|
// Filter and select result
|
|
229
229
|
const filteredResults = this.filterSearchResults(searchResponse.result.data, node);
|
|
230
230
|
const selectedResult = this.selectResult(filteredResults);
|
|
231
|
-
this.logger.debug('Selecting result:', selectedResult?.address || 'no result found');
|
|
232
231
|
// Early return: if no result found, return original address
|
|
233
232
|
if (!selectedResult) {
|
|
234
233
|
return {
|
|
@@ -242,7 +241,12 @@ export class oSearchResolver extends oAddressResolver {
|
|
|
242
241
|
.toString() // o://embeddings-text replace o://embeddings-text = ''
|
|
243
242
|
.replace(address.toRootAddress().toString(), '');
|
|
244
243
|
this.logger.debug('Extra params:', extraParams);
|
|
245
|
-
|
|
244
|
+
// Check if selectedResult.address already contains the complete path
|
|
245
|
+
// This happens when registry finds via staticAddress - the returned address
|
|
246
|
+
// is the canonical hierarchical location, so we shouldn't append extraParams
|
|
247
|
+
const resultAddress = selectedResult.address;
|
|
248
|
+
const shouldAppendParams = extraParams && !resultAddress.endsWith(extraParams);
|
|
249
|
+
const resolvedTargetAddress = new oAddress(shouldAppendParams ? resultAddress + extraParams : resultAddress);
|
|
246
250
|
// Set transports on the target address
|
|
247
251
|
resolvedTargetAddress.setTransports(this.mapTransports(selectedResult));
|
|
248
252
|
// Determine next hop and configure transports
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { oObject, oAddress } from '@olane/o-core';
|
|
2
|
+
export interface LeaderRetryConfig {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
maxAttempts: number;
|
|
5
|
+
baseDelayMs: number;
|
|
6
|
+
maxDelayMs: number;
|
|
7
|
+
timeoutMs: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Leader Request Wrapper
|
|
11
|
+
*
|
|
12
|
+
* Wraps requests to leader/registry with aggressive retry logic.
|
|
13
|
+
* Used when leader may be temporarily unavailable (healing, maintenance).
|
|
14
|
+
*
|
|
15
|
+
* Strategy:
|
|
16
|
+
* 1. Detect if request is to leader or registry
|
|
17
|
+
* 2. Apply retry logic with exponential backoff
|
|
18
|
+
* 3. Timeout individual attempts
|
|
19
|
+
* 4. Log retries for observability
|
|
20
|
+
*/
|
|
21
|
+
export declare class LeaderRequestWrapper extends oObject {
|
|
22
|
+
private config;
|
|
23
|
+
constructor(config: LeaderRetryConfig);
|
|
24
|
+
/**
|
|
25
|
+
* Check if address is a leader-related address that needs retry
|
|
26
|
+
*/
|
|
27
|
+
private isLeaderAddress;
|
|
28
|
+
/**
|
|
29
|
+
* Execute request with retry logic
|
|
30
|
+
*/
|
|
31
|
+
execute<T>(requestFn: () => Promise<T>, address: oAddress, context?: string): Promise<T>;
|
|
32
|
+
/**
|
|
33
|
+
* Calculate exponential backoff delay
|
|
34
|
+
*/
|
|
35
|
+
private calculateBackoffDelay;
|
|
36
|
+
/**
|
|
37
|
+
* Sleep utility
|
|
38
|
+
*/
|
|
39
|
+
private sleep;
|
|
40
|
+
/**
|
|
41
|
+
* Get current configuration
|
|
42
|
+
*/
|
|
43
|
+
getConfig(): LeaderRetryConfig;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=leader-request-wrapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leader-request-wrapper.d.ts","sourceRoot":"","sources":["../../../src/utils/leader-request-wrapper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAuB,MAAM,eAAe,CAAC;AAEvE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,oBAAqB,SAAQ,OAAO;IACnC,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,iBAAiB;IAI7C;;OAEG;IACH,OAAO,CAAC,eAAe;IAKvB;;OAEG;IACG,OAAO,CAAC,CAAC,EACb,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,QAAQ,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,CAAC,CAAC;IAoEb;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAK7B;;OAEG;IACH,OAAO,CAAC,KAAK;IAIb;;OAEG;IACH,SAAS,IAAI,iBAAiB;CAG/B"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { oObject } from '@olane/o-core';
|
|
2
|
+
/**
|
|
3
|
+
* Leader Request Wrapper
|
|
4
|
+
*
|
|
5
|
+
* Wraps requests to leader/registry with aggressive retry logic.
|
|
6
|
+
* Used when leader may be temporarily unavailable (healing, maintenance).
|
|
7
|
+
*
|
|
8
|
+
* Strategy:
|
|
9
|
+
* 1. Detect if request is to leader or registry
|
|
10
|
+
* 2. Apply retry logic with exponential backoff
|
|
11
|
+
* 3. Timeout individual attempts
|
|
12
|
+
* 4. Log retries for observability
|
|
13
|
+
*/
|
|
14
|
+
export class LeaderRequestWrapper extends oObject {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
super();
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if address is a leader-related address that needs retry
|
|
21
|
+
*/
|
|
22
|
+
isLeaderAddress(address) {
|
|
23
|
+
const addressStr = address.toString();
|
|
24
|
+
return addressStr === 'o://leader' || addressStr === 'o://registry';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Execute request with retry logic
|
|
28
|
+
*/
|
|
29
|
+
async execute(requestFn, address, context) {
|
|
30
|
+
// If retry disabled or not a leader address, execute directly
|
|
31
|
+
if (!this.config.enabled || !this.isLeaderAddress(address)) {
|
|
32
|
+
return await requestFn();
|
|
33
|
+
}
|
|
34
|
+
let attempt = 0;
|
|
35
|
+
let lastError;
|
|
36
|
+
while (attempt < this.config.maxAttempts) {
|
|
37
|
+
attempt++;
|
|
38
|
+
try {
|
|
39
|
+
this.logger.debug(`Leader request attempt ${attempt}/${this.config.maxAttempts}` +
|
|
40
|
+
(context ? ` (${context})` : ''));
|
|
41
|
+
// Create timeout promise
|
|
42
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
43
|
+
setTimeout(() => reject(new Error(`Leader request timeout after ${this.config.timeoutMs}ms`)), this.config.timeoutMs);
|
|
44
|
+
});
|
|
45
|
+
// Race between request and timeout
|
|
46
|
+
const result = await Promise.race([requestFn(), timeoutPromise]);
|
|
47
|
+
// Success!
|
|
48
|
+
if (attempt > 1) {
|
|
49
|
+
this.logger.info(`Leader request succeeded after ${attempt} attempts` +
|
|
50
|
+
(context ? ` (${context})` : ''));
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
56
|
+
this.logger.warn(`Leader request attempt ${attempt} failed: ${lastError.message}` +
|
|
57
|
+
(context ? ` (${context})` : ''));
|
|
58
|
+
if (attempt < this.config.maxAttempts) {
|
|
59
|
+
const delay = this.calculateBackoffDelay(attempt);
|
|
60
|
+
this.logger.debug(`Waiting ${delay}ms before next attempt...`);
|
|
61
|
+
await this.sleep(delay);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// All attempts failed
|
|
66
|
+
this.logger.error(`Leader request failed after ${this.config.maxAttempts} attempts` +
|
|
67
|
+
(context ? ` (${context})` : ''));
|
|
68
|
+
throw lastError || new Error('Leader request failed');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Calculate exponential backoff delay
|
|
72
|
+
*/
|
|
73
|
+
calculateBackoffDelay(attempt) {
|
|
74
|
+
const delay = this.config.baseDelayMs * Math.pow(2, attempt - 1);
|
|
75
|
+
return Math.min(delay, this.config.maxDelayMs);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Sleep utility
|
|
79
|
+
*/
|
|
80
|
+
sleep(ms) {
|
|
81
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get current configuration
|
|
85
|
+
*/
|
|
86
|
+
getConfig() {
|
|
87
|
+
return { ...this.config };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-resolver.spec.d.ts","sourceRoot":"","sources":["../../test/search-resolver.spec.ts"],"names":[],"mappings":""}
|