service-bridge 1.1.0-dev.27 → 1.1.1-dev.30
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/README.md +192 -0
- package/dist/index.js +546 -137
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -953,6 +953,198 @@ try {
|
|
|
953
953
|
|
|
954
954
|
---
|
|
955
955
|
|
|
956
|
+
## v2 Session API
|
|
957
|
+
|
|
958
|
+
`session_v2.ts` реализует новый Enterprise Session Protocol — Channel-based bidi stream с 8-состоянийным FSM, адаптивным heartbeat и кредитным управлением потоком. Симметричен с Go и Python SDK.
|
|
959
|
+
|
|
960
|
+
### Жизненный цикл сессии (8 состояний FSM)
|
|
961
|
+
|
|
962
|
+
```
|
|
963
|
+
connecting → handshaking → ready ↔ active
|
|
964
|
+
↘ suspended → (reconnect)
|
|
965
|
+
↘ draining → closed
|
|
966
|
+
↘ fenced (permanent)
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
| Состояние | Описание |
|
|
970
|
+
|-----------|----------|
|
|
971
|
+
| `connecting` | Устанавливается TCP/TLS соединение |
|
|
972
|
+
| `handshaking` | Отправлен Hello, ждём HelloAck |
|
|
973
|
+
| `ready` | HelloAck получен, команды не выполняются |
|
|
974
|
+
| `active` | Есть активные команды |
|
|
975
|
+
| `suspended` | Heartbeat пропущен 2+ раза |
|
|
976
|
+
| `draining` | Инициирован graceful shutdown |
|
|
977
|
+
| `fenced` | Сервер прислал GOAWAY_FENCED — сессия закрыта навсегда |
|
|
978
|
+
| `closed` | Соединение закрыто |
|
|
979
|
+
|
|
980
|
+
### Быстрый старт
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
import { V2SessionClient, validateV2Config } from 'service-bridge';
|
|
984
|
+
|
|
985
|
+
const cfg = {
|
|
986
|
+
serverAddress: 'localhost:9090',
|
|
987
|
+
serviceName: 'my-worker',
|
|
988
|
+
instanceId: 'worker-1',
|
|
989
|
+
zone: 'us-east-1a',
|
|
990
|
+
transportMode: 'direct' as const,
|
|
991
|
+
maxInflight: 64,
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
validateV2Config(cfg);
|
|
995
|
+
const session = new V2SessionClient(cfg);
|
|
996
|
+
|
|
997
|
+
// Отправить Hello при подключении
|
|
998
|
+
const hello = session.getHelloFields();
|
|
999
|
+
|
|
1000
|
+
// Обработать HelloAck от сервера
|
|
1001
|
+
session.onHelloAck({
|
|
1002
|
+
sessionId: 'sess-abc',
|
|
1003
|
+
resumeToken: 'token-xyz',
|
|
1004
|
+
epoch: 1n,
|
|
1005
|
+
resumed: false,
|
|
1006
|
+
resumeFromSeq: 0n,
|
|
1007
|
+
replayedCommands: 0,
|
|
1008
|
+
reconciledResults: 0,
|
|
1009
|
+
heartbeatIntervalMs: 10_000,
|
|
1010
|
+
heartbeatTimeoutMs: 30_000,
|
|
1011
|
+
initialPermits: 64,
|
|
1012
|
+
maxPermits: 128,
|
|
1013
|
+
effectiveTransportMode: 'direct',
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
console.log(session.state); // 'ready'
|
|
1017
|
+
|
|
1018
|
+
// Входящая команда
|
|
1019
|
+
const accepted = session.onCommandReceived(1n, 'cmd-001');
|
|
1020
|
+
if (!accepted) {
|
|
1021
|
+
// backpressure — permits = 0
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Команда выполнена
|
|
1025
|
+
session.onCommandCompleted(1n, 'cmd-001');
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
### Адаптивный heartbeat (EWMA RTT)
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1031
|
+
import { AdaptiveHeartbeatV2 } from 'service-bridge';
|
|
1032
|
+
|
|
1033
|
+
const hb = new AdaptiveHeartbeatV2(10_000, 30_000);
|
|
1034
|
+
|
|
1035
|
+
// Получен pong
|
|
1036
|
+
hb.onPong(25); // rttMs
|
|
1037
|
+
|
|
1038
|
+
// Следующий интервал (адаптируется по EWMA RTT)
|
|
1039
|
+
const nextMs = hb.nextIntervalMs();
|
|
1040
|
+
|
|
1041
|
+
// Пропуск — ускоряем пинги
|
|
1042
|
+
const missCount = hb.onMiss();
|
|
1043
|
+
if (missCount >= 2) {
|
|
1044
|
+
// reconnect
|
|
1045
|
+
}
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
Алгоритм: базовый интервал `intervalMs / 3`; при пропусках делится на `2^miss` (min 2s); при стабильном RTT < 50ms удваивается (max 30s).
|
|
1049
|
+
|
|
1050
|
+
### Кредитное управление потоком
|
|
1051
|
+
|
|
1052
|
+
```typescript
|
|
1053
|
+
import { FlowControlStateV2 } from 'service-bridge';
|
|
1054
|
+
|
|
1055
|
+
const fc = new FlowControlStateV2(64, 1, 128);
|
|
1056
|
+
|
|
1057
|
+
if (fc.tryConsume()) {
|
|
1058
|
+
// dispatch command
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Команда завершена — вернуть permit
|
|
1062
|
+
fc.release(1);
|
|
1063
|
+
|
|
1064
|
+
// Сервер прислал FlowControlUpdate
|
|
1065
|
+
fc.setWindow(32);
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
### Reconnect и resume
|
|
1069
|
+
|
|
1070
|
+
`BackoffV2` реализует экспоненциальный backoff с full jitter (base=100ms, max=30s). При переподключении `getHelloFields()` автоматически включает `resumeToken`, `epoch`, `lastReceivedSeq`, `lastSentSeq`, `completedCommandIds` — сервер продолжит сессию с нужной позиции.
|
|
1071
|
+
|
|
1072
|
+
```typescript
|
|
1073
|
+
import { BackoffV2 } from 'service-bridge';
|
|
1074
|
+
|
|
1075
|
+
const backoff = new BackoffV2();
|
|
1076
|
+
|
|
1077
|
+
while (true) {
|
|
1078
|
+
if (backoff.isCircuitOpen()) break; // 10+ сбоев подряд
|
|
1079
|
+
|
|
1080
|
+
const delayMs = backoff.next();
|
|
1081
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
1082
|
+
|
|
1083
|
+
try {
|
|
1084
|
+
// reconnect...
|
|
1085
|
+
backoff.reset();
|
|
1086
|
+
} catch {
|
|
1087
|
+
backoff.recordFail();
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
### ConfigPush — динамическая конфигурация транспорта
|
|
1093
|
+
|
|
1094
|
+
Сервер может в любой момент прислать `ConfigPush` с новыми правилами маршрутизации:
|
|
1095
|
+
|
|
1096
|
+
```typescript
|
|
1097
|
+
session.onConfigPush({
|
|
1098
|
+
defaultMode: 'direct',
|
|
1099
|
+
serviceOverrides: {
|
|
1100
|
+
'payment-svc': { mode: 'proxy', fallbackPolicy: 'fallback_to_direct' },
|
|
1101
|
+
},
|
|
1102
|
+
functionOverrides: {
|
|
1103
|
+
'payment-svc/charge': { mode: 'proxy', timeoutMs: 5000 },
|
|
1104
|
+
},
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// Разрешить транспорт для функции
|
|
1108
|
+
const mode = session.resolveTransportMode('payment-svc/charge'); // 'proxy'
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
### Все события сессии
|
|
1112
|
+
|
|
1113
|
+
| Метод | Описание |
|
|
1114
|
+
|-------|----------|
|
|
1115
|
+
| `getHelloFields()` | Поля для отправки Hello (первый + resume) |
|
|
1116
|
+
| `onHelloAck(ack)` | Обработка HelloAck от сервера |
|
|
1117
|
+
| `onCommandReceived(seq, id)` | Входящая команда; возвращает `false` при backpressure |
|
|
1118
|
+
| `onCommandCompleted(seq, id)` | Команда выполнена; освобождает permit |
|
|
1119
|
+
| `onPermitGrant(n)` | Сервер добавил `n` permits |
|
|
1120
|
+
| `onFlowControlUpdate(size, reason)` | Сервер изменил размер окна |
|
|
1121
|
+
| `onPong(rttMs)` | Получен pong; обновляет EWMA |
|
|
1122
|
+
| `onHeartbeatMiss()` | Таймаут pong; возвращает `true` → `suspended` |
|
|
1123
|
+
| `onDrain(reason, deadlineMs)` | Инициировать graceful drain |
|
|
1124
|
+
| `onGoaway(code, reason)` | GoawaySignal от сервера |
|
|
1125
|
+
| `onConfigPush(config)` | Применить новую конфигурацию транспорта |
|
|
1126
|
+
| `resolveTransportMode(fnName)` | Получить режим транспорта для функции |
|
|
1127
|
+
| `stop()` | Немедленно закрыть сессию |
|
|
1128
|
+
|
|
1129
|
+
### Экспортируемые классы и типы
|
|
1130
|
+
|
|
1131
|
+
| Символ | Тип | Описание |
|
|
1132
|
+
|--------|-----|----------|
|
|
1133
|
+
| `V2SessionClient` | class | Главный клиент сессии |
|
|
1134
|
+
| `AdaptiveHeartbeatV2` | class | EWMA RTT heartbeat controller |
|
|
1135
|
+
| `FlowControlStateV2` | class | Кредитное управление потоком |
|
|
1136
|
+
| `BackoffV2` | class | Exponential backoff + circuit |
|
|
1137
|
+
| `PositionTrackerV2` | class | Трекер seq/completed IDs |
|
|
1138
|
+
| `ConfigPushStateV2` | class | Менеджер динамической конфигурации |
|
|
1139
|
+
| `validateV2Config` | function | Валидация конфига; бросает `Error` |
|
|
1140
|
+
| `V2Config` | interface | Конфигурация сессии |
|
|
1141
|
+
| `SessionStateV2` | type | Союз 8 состояний FSM |
|
|
1142
|
+
| `TransportMode` | type | `'direct' \| 'proxy'` |
|
|
1143
|
+
| `HelloAckV2` | interface | Данные HelloAck от сервера |
|
|
1144
|
+
| `TransportConfigV2` | interface | ConfigPush payload |
|
|
1145
|
+
|
|
1146
|
+
---
|
|
1147
|
+
|
|
956
1148
|
## FAQ
|
|
957
1149
|
|
|
958
1150
|
**How does ServiceBridge handle service failures?**
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,312 @@ import * as protobuf from "protobufjs";
|
|
|
12
12
|
// sdk/src/generated/servicebridge-package-definition.ts
|
|
13
13
|
var DESCRIPTOR_SET_BASE64 = "Cuk9ChNzZXJ2aWNlYnJpZGdlLnByb3RvEg1zZXJ2aWNlYnJpZGdlIk8KDUhhbmRsZVJlcXVlc3QSCgoCZm4YASABKAkSDwoHcGF5bG9hZBgCIAEoDBIQCgh0cmFjZV9pZBgDIAEoCRIPCgdzcGFuX2lkGAQgASgJIkAKDkhhbmRsZVJlc3BvbnNlEg4KBm91dHB1dBgBIAEoDBIPCgdzdWNjZXNzGAIgASgIEg0KBWVycm9yGAMgASgJIt8BChVEZWxpdmVyTWVzc2FnZVJlcXVlc3QSEgoKbWVzc2FnZV9pZBgBIAEoCRISCgpncm91cF9uYW1lGAIgASgJEg0KBXRvcGljGAMgASgJEg8KB3BheWxvYWQYBCABKAwSGAoHaGVhZGVycxgFIAMoCzIHSGVhZGVycxIQCgh0cmFjZV9pZBgGIAEoCRIWCg5wYXJlbnRfc3Bhbl9pZBgHIAEoCRIPCgdhdHRlbXB0GAggASgFGikKB0hlYWRlcnMSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLSAQoOUHVibGlzaFJlcXVlc3QSDQoFdG9waWMYASABKAkSDwoHcGF5bG9hZBgCIAEoDBIYCgdoZWFkZXJzGAMgAygLMgdIZWFkZXJzEhAKCHRyYWNlX2lkGAQgASgJEhYKDnBhcmVudF9zcGFuX2lkGAUgASgJEhgKEHByb2R1Y2VyX3NlcnZpY2UYBiABKAkSFwoPaWRlbXBvdGVuY3lfa2V5GAcgASgJGikKB0hlYWRlcnMSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIlCg9QdWJsaXNoUmVzcG9uc2USEgoKbWVzc2FnZV9pZBgBIAEoCSJYCgtEZWxpdmVyeUFjaxILCgNhY2sYASABKAgSFgoOcmV0cnlfYWZ0ZXJfbXMYAiABKAUSFQoNcmVqZWN0X3JlYXNvbhgDIAEoCRINCgVlcnJvchgEIAEoCSIuChRXYXRjaFJlZ2lzdHJ5UmVxdWVzdBIWCg5mdW5jdGlvbl9uYW1lcxgBIAMoCSIoChVMb29rdXBGdW5jdGlvblJlcXVlc3QSDwoHZm5fbmFtZRgBIAEoCSJgChZMb29rdXBGdW5jdGlvblJlc3BvbnNlEg0KBWZvdW5kGAEgASgIEhYKDmNhbm9uaWNhbF9uYW1lGAIgASgJEh8KCWVuZHBvaW50cxgDIAEoCzIMRW5kcG9pbnRMaXN0IoQBCg9SZWdpc3RyeU1lc3NhZ2USIQoEa2luZBgBIAEoDjITUmVnaXN0cnlNZXNzYWdlS2luZBIQCghyZXZpc2lvbhgCIAEoAxIeCgRmdWxsGAMgASgLMhBSZWdpc3RyeVNuYXBzaG90EhwKBWRlbHRhGAQgASgLMg1SZWdpc3RyeURlbHRhInQKDVJlZ2lzdHJ5RGVsdGESGAoHdXBzZXJ0cxgBIAMoCzIHVXBzZXJ0cxIQCghyZW1vdmFscxgCIAMoCRo3CgdVcHNlcnRzEgsKA2tleRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMRW5kcG9pbnRMaXN0OgI4ASJaCg5Xb3JrZXJFbmRwb2ludBIQCghlbmRwb2ludBgBIAEoCRIRCgl0cmFuc3BvcnQYAiABKAkSEwoLaW5zdGFuY2VfaWQYAyABKAkSDgoGd2VpZ2h0GAQgASgFIlkKEFJlZ2lzdHJ5U25hcHNob3QSEAoDcnBjGAEgAygLMgNScGMaMwoDUnBjEgsKA2tleRgBIAEoCRIbCgV2YWx1ZRgCIAEoCzIMRW5kcG9pbnRMaXN0OgI4ASKHAQoMRW5kcG9pbnRMaXN0EiEKCWVuZHBvaW50cxgBIAMoCzIOV29ya2VyRW5kcG9pbnQSGQoRaW5wdXRfc2NoZW1hX2pzb24YAiABKAkSGgoSb3V0cHV0X3NjaGVtYV9qc29uGAMgASgJEhcKD2FsbG93ZWRfY2FsbGVycxgEIAMoCUoECAUQByK+AQoWUmVwb3J0Q2FsbFN0YXJ0UmVxdWVzdBIQCgh0cmFjZV9pZBgBIAEoCRIPCgdzcGFuX2lkGAIgASgJEgoKAmZuGAMgASgJEhQKDHNlcnZpY2VfbmFtZRgEIAEoCRISCgpzdGFydGVkX2F0GAUgASgDEg0KBWlucHV0GAYgASgMEg8KB2F0dGVtcHQYByABKAUSFgoOcGFyZW50X3NwYW5faWQYCCABKAkSEwoLaW5zdGFuY2VfaWQYCSABKAki5gEKEVJlcG9ydENhbGxSZXF1ZXN0EhAKCHRyYWNlX2lkGAEgASgJEg8KB3NwYW5faWQYAiABKAkSCgoCZm4YAyABKAkSFAoMc2VydmljZV9uYW1lGAQgASgJEhIKCnN0YXJ0ZWRfYXQYBSABKAMSEwoLZHVyYXRpb25fbXMYBiABKAMSDwoHc3VjY2VzcxgHIAEoCBINCgVlcnJvchgIIAEoCRINCgVpbnB1dBgJIAEoDBIOCgZvdXRwdXQYCiABKAwSDwoHYXR0ZW1wdBgLIAEoBRITCgtpbnN0YW5jZV9pZBgMIAEoCSIUChJSZXBvcnRDYWxsUmVzcG9uc2Ui3AEKCExvZ0VudHJ5EhAKCHRyYWNlX2lkGAEgASgJEg8KB3NwYW5faWQYAiABKAkSFAoMc2VydmljZV9uYW1lGAMgASgJEg0KBWxldmVsGAQgASgJEg8KB21lc3NhZ2UYBSABKAkSFAoMdGltZXN0YW1wX25zGAYgASgDEh4KCmF0dHJpYnV0ZXMYByADKAsyCkF0dHJpYnV0ZXMSEwoLaW5zdGFuY2VfaWQYCCABKAkaLAoKQXR0cmlidXRlcxILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIi0KEFJlcG9ydExvZ1JlcXVlc3QSGQoHZW50cmllcxgBIAMoCzIITG9nRW50cnkiEwoRUmVwb3J0TG9nUmVzcG9uc2UiwQEKF1JlZ2lzdGVyRnVuY3Rpb25SZXF1ZXN0Eg8KB2ZuX25hbWUYASABKAkSFAoMc2VydmljZV9uYW1lGAIgASgJEhMKC2luc3RhbmNlX2lkGAMgASgJEhAKCGVuZHBvaW50GAQgASgJEg4KBndlaWdodBgFIAEoBRIZChFpbnB1dF9zY2hlbWFfanNvbhgGIAEoCRIaChJvdXRwdXRfc2NoZW1hX2pzb24YByABKAkSEQoJdHJhbnNwb3J0GAggASgJIhoKGFJlZ2lzdGVyRnVuY3Rpb25SZXNwb25zZSKRAQocUmVnaXN0ZXJDb25zdW1lckdyb3VwUmVxdWVzdBIMCgRuYW1lGAEgASgJEg8KB3BhdHRlcm4YAiABKAkSDAoEbW9kZRgDIAEoCRIZChFyZXRyeV9wb2xpY3lfanNvbhgEIAEoCRIOCgZhY3RpdmUYBiABKAgSEwoLZmlsdGVyX2V4cHIYByABKAlKBAgFEAUiHwodUmVnaXN0ZXJDb25zdW1lckdyb3VwUmVzcG9uc2UikAEKGlJlZ2lzdGVyR3JvdXBNZW1iZXJSZXF1ZXN0EhIKCmdyb3VwX25hbWUYASABKAkSFAoMc2VydmljZV9uYW1lGAIgASgJEhMKC2luc3RhbmNlX2lkGAMgASgJEhAKCGVuZHBvaW50GAQgASgJEg4KBndlaWdodBgFIAEoBRIRCgl0cmFuc3BvcnQYBiABKAkiHQobUmVnaXN0ZXJHcm91cE1lbWJlclJlc3BvbnNlItUBChBIZWFydGJlYXRSZXF1ZXN0EhQKDHNlcnZpY2VfbmFtZRgBIAEoCRITCgtpbnN0YW5jZV9pZBgCIAEoCRIQCghlbmRwb2ludBgDIAEoCRITCgtncm91cF9uYW1lcxgEIAMoCRIWCg5mdW5jdGlvbl9uYW1lcxgFIAMoCRIRCgl0cmFuc3BvcnQYBiABKAkSFwoLY3B1X3BlcmNlbnQYByABKAJCAEgAEhIKBnJhbV9tYhgIIAEoBUIASAFCDQoLX2NwdVBlcmNlbnRCCAoGX3JhbU1iIhMKEUhlYXJ0YmVhdFJlc3BvbnNlIugBChtSZWdpc3Rlckh0dHBFbmRwb2ludFJlcXVlc3QSFAoMc2VydmljZV9uYW1lGAEgASgJEg4KBm1ldGhvZBgCIAEoCRIVCg1yb3V0ZV9wYXR0ZXJuGAMgASgJEhsKE3JlcXVlc3Rfc2NoZW1hX2pzb24YBCABKAkSHAoUcmVzcG9uc2Vfc2NoZW1hX2pzb24YBSABKAkSEwoLaW5zdGFuY2VfaWQYBiABKAkSEAoIZW5kcG9pbnQYByABKAkSEQoJdHJhbnNwb3J0GAggASgJEhcKD2FsbG93ZWRfY2FsbGVycxgJIAMoCSJCChxSZWdpc3Rlckh0dHBFbmRwb2ludFJlc3BvbnNlEgoKAm9rGAEgASgIEhYKDmNhbm9uaWNhbF9uYW1lGAIgASgJIkkKHEhlYXJ0YmVhdEh0dHBFbmRwb2ludFJlcXVlc3QSFAoMc2VydmljZV9uYW1lGAEgASgJEhMKC2luc3RhbmNlX2lkGAIgASgJIisKHUhlYXJ0YmVhdEh0dHBFbmRwb2ludFJlc3BvbnNlEgoKAm9rGAEgASgIImQKIVByb3Zpc2lvbldvcmtlckNlcnRpZmljYXRlUmVxdWVzdBIUCgxzZXJ2aWNlX25hbWUYASABKAkSFgoOcHVibGljX2tleV9wZW0YAiABKAkSEQoJZXh0cmFfaXBzGAMgAygJIkYKIlByb3Zpc2lvbldvcmtlckNlcnRpZmljYXRlUmVzcG9uc2USEAoIY2VydF9wZW0YASABKAkSDgoGY2FfcGVtGAIgASgJIlUKEldvcmtlclNlc3Npb25IZWxsbxIUCgxzZXJ2aWNlX25hbWUYASABKAkSEwoLaW5zdGFuY2VfaWQYAiABKAkSFAoMbWF4X2luZmxpZ2h0GAMgASgFImIKEFdvcmtlclJQQ0NvbW1hbmQSCgoCZm4YASABKAkSDwoHcGF5bG9hZBgCIAEoDBIQCgh0cmFjZV9pZBgDIAEoCRIPCgdzcGFuX2lkGAQgASgJEg4KBnJ1bl9pZBgFIAEoCSLsAQoSV29ya2VyRXZlbnRDb21tYW5kEhIKCm1lc3NhZ2VfaWQYASABKAkSEgoKZ3JvdXBfbmFtZRgCIAEoCRINCgV0b3BpYxgDIAEoCRIPCgdwYXlsb2FkGAQgASgMEhgKB2hlYWRlcnMYBSADKAsyB0hlYWRlcnMSEAoIdHJhY2VfaWQYBiABKAkSFgoOcGFyZW50X3NwYW5faWQYByABKAkSDwoHYXR0ZW1wdBgIIAEoBRIOCgZydW5faWQYCSABKAkaKQoHSGVhZGVycxILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIqcBChRXb3JrZXJTZXNzaW9uQ29tbWFuZBISCgpjb21tYW5kX2lkGAEgASgJEh8KBGtpbmQYAiABKA4yEVdvcmtlckNvbW1hbmRLaW5kEh0KA3JwYxgDIAEoCzIQV29ya2VyUlBDQ29tbWFuZBIhCgVldmVudBgEIAEoCzISV29ya2VyRXZlbnRDb21tYW5kEhgKEGRlYWRsaW5lX3VuaXhfbXMYBSABKAMinAEKGldvcmtlclNlc3Npb25Db21tYW5kUmVzdWx0EhIKCmNvbW1hbmRfaWQYASABKAkSDwoHc3VjY2VzcxgCIAEoCBIOCgZvdXRwdXQYAyABKAwSDQoFZXJyb3IYBCABKAkSCwoDYWNrGAUgASgIEhYKDnJldHJ5X2FmdGVyX21zGAYgASgFEhUKDXJlamVjdF9yZWFzb24YByABKAkiLgoRV29ya2VyU2Vzc2lvblBpbmcSGQoRdGltZXN0YW1wX3VuaXhfbXMYASABKAMiLgoRV29ya2VyU2Vzc2lvblBvbmcSGQoRdGltZXN0YW1wX3VuaXhfbXMYASABKAMinwEKFFdvcmtlclNlc3Npb25SZXF1ZXN0EiMKBWhlbGxvGAEgASgLMhJXb3JrZXJTZXNzaW9uSGVsbG9IABI0Cg5jb21tYW5kX3Jlc3VsdBgCIAEoCzIaV29ya2VyU2Vzc2lvbkNvbW1hbmRSZXN1bHRIABIhCgRwaW5nGAMgASgLMhFXb3JrZXJTZXNzaW9uUGluZ0gAQgkKB3BheWxvYWQibgoVV29ya2VyU2Vzc2lvblJlc3BvbnNlEicKB2NvbW1hbmQYASABKAsyFFdvcmtlclNlc3Npb25Db21tYW5kSAASIQoEcG9uZxgCIAEoCzIRV29ya2VyU2Vzc2lvblBvbmdIAEIJCgdwYXlsb2FkIl8KF1JlZ2lzdGVyV29ya2Zsb3dSZXF1ZXN0EgwKBG5hbWUYASABKAkSEgoKZGVmaW5pdGlvbhgCIAEoCRIMCgRvcHRzGAMgASgJEhQKDHNlcnZpY2VfbmFtZRgEIAEoCSImChhSZWdpc3RlcldvcmtmbG93UmVzcG9uc2USCgoCaWQYASABKAkiRwoSUnVuV29ya2Zsb3dSZXF1ZXN0EgwKBG5hbWUYASABKAkSDQoFaW5wdXQYAiABKAwSFAoMc2VydmljZV9uYW1lGAMgASgJIjcKE1J1bldvcmtmbG93UmVzcG9uc2USDgoGcnVuX2lkGAEgASgJEhAKCHRyYWNlX2lkGAIgASgJIioKGENhbmNlbFdvcmtmbG93UnVuUmVxdWVzdBIOCgZydW5faWQYASABKAkiPgoZQ2FuY2VsV29ya2Zsb3dSdW5SZXNwb25zZRIRCgljYW5jZWxsZWQYASABKAgSDgoGcnVuX2lkGAIgASgJIr0BChJSZWdpc3RlckpvYlJlcXVlc3QSEQoJY3Jvbl9leHByGAEgASgJEhAKCHRpbWV6b25lGAIgASgJEhYKDm1pc2ZpcmVfcG9saWN5GAMgASgJEhMKC3RhcmdldF90eXBlGAQgASgJEhIKCnRhcmdldF9yZWYYBSABKAkSEAoIZGVsYXlfbXMYBiABKAUSFAoMc2VydmljZV9uYW1lGAcgASgJEhkKEXJldHJ5X3BvbGljeV9qc29uGAggASgJIiEKE1JlZ2lzdGVySm9iUmVzcG9uc2USCgoCaWQYASABKAkiQAoTQXBwZW5kU3RyZWFtUmVxdWVzdBIOCgZydW5faWQYASABKAkSCwoDa2V5GAIgASgJEgwKBGRhdGEYAyABKAwiFgoUQXBwZW5kU3RyZWFtUmVzcG9uc2UiRQoPV2F0Y2hSdW5SZXF1ZXN0Eg4KBnJ1bl9pZBgBIAEoCRILCgNrZXkYAiABKAkSFQoNZnJvbV9zZXF1ZW5jZRgDIAEoAyJlCg5SdW5TdHJlYW1DaHVuaxIMCgR0eXBlGAEgASgJEgsKA2tleRgDIAEoCRIMCgRkYXRhGAQgASgMEhAKCHNlcXVlbmNlGAUgASgDEhIKCnJ1bl9zdGF0dXMYBiABKAlKBAgCEAIqKgoTUmVnaXN0cnlNZXNzYWdlS2luZBIICgRGVUxMEAASCQoFREVMVEEQASp0ChFXb3JrZXJDb21tYW5kS2luZBIjCh9XT1JLRVJfQ09NTUFORF9LSU5EX1VOU1BFQ0lGSUVEEAASGwoXV09SS0VSX0NPTU1BTkRfS0lORF9SUEMQARIdChlXT1JLRVJfQ09NTUFORF9LSU5EX0VWRU5UEAIykw8KDVNlcnZpY2VCcmlkZ2USYwoQUmVnaXN0ZXJGdW5jdGlvbhImLnNlcnZpY2VicmlkZ2UuUmVnaXN0ZXJGdW5jdGlvblJlcXVlc3QaJy5zZXJ2aWNlYnJpZGdlLlJlZ2lzdGVyRnVuY3Rpb25SZXNwb25zZRJyChVSZWdpc3RlckNvbnN1bWVyR3JvdXASKy5zZXJ2aWNlYnJpZGdlLlJlZ2lzdGVyQ29uc3VtZXJHcm91cFJlcXVlc3QaLC5zZXJ2aWNlYnJpZGdlLlJlZ2lzdGVyQ29uc3VtZXJHcm91cFJlc3BvbnNlEmwKE1JlZ2lzdGVyR3JvdXBNZW1iZXISKS5zZXJ2aWNlYnJpZGdlLlJlZ2lzdGVyR3JvdXBNZW1iZXJSZXF1ZXN0Giouc2VydmljZWJyaWRnZS5SZWdpc3Rlckdyb3VwTWVtYmVyUmVzcG9uc2USTgoJSGVhcnRiZWF0Eh8uc2VydmljZWJyaWRnZS5IZWFydGJlYXRSZXF1ZXN0GiAuc2VydmljZWJyaWRnZS5IZWFydGJlYXRSZXNwb25zZRJvChRSZWdpc3Rlckh0dHBFbmRwb2ludBIqLnNlcnZpY2VicmlkZ2UuUmVnaXN0ZXJIdHRwRW5kcG9pbnRSZXF1ZXN0Gisuc2VydmljZWJyaWRnZS5SZWdpc3Rlckh0dHBFbmRwb2ludFJlc3BvbnNlEnIKFUhlYXJ0YmVhdEh0dHBFbmRwb2ludBIrLnNlcnZpY2VicmlkZ2UuSGVhcnRiZWF0SHR0cEVuZHBvaW50UmVxdWVzdBosLnNlcnZpY2VicmlkZ2UuSGVhcnRiZWF0SHR0cEVuZHBvaW50UmVzcG9uc2USgQEKGlByb3Zpc2lvbldvcmtlckNlcnRpZmljYXRlEjAuc2VydmljZWJyaWRnZS5Qcm92aXNpb25Xb3JrZXJDZXJ0aWZpY2F0ZVJlcXVlc3QaMS5zZXJ2aWNlYnJpZGdlLlByb3Zpc2lvbldvcmtlckNlcnRpZmljYXRlUmVzcG9uc2USYgoRT3BlbldvcmtlclNlc3Npb24SIy5zZXJ2aWNlYnJpZGdlLldvcmtlclNlc3Npb25SZXF1ZXN0GiQuc2VydmljZWJyaWRnZS5Xb3JrZXJTZXNzaW9uUmVzcG9uc2UoATABEmMKEFJlZ2lzdGVyV29ya2Zsb3cSJi5zZXJ2aWNlYnJpZGdlLlJlZ2lzdGVyV29ya2Zsb3dSZXF1ZXN0Gicuc2VydmljZWJyaWRnZS5SZWdpc3RlcldvcmtmbG93UmVzcG9uc2USVAoLUnVuV29ya2Zsb3cSIS5zZXJ2aWNlYnJpZGdlLlJ1bldvcmtmbG93UmVxdWVzdBoiLnNlcnZpY2VicmlkZ2UuUnVuV29ya2Zsb3dSZXNwb25zZRJmChFDYW5jZWxXb3JrZmxvd1J1bhInLnNlcnZpY2VicmlkZ2UuQ2FuY2VsV29ya2Zsb3dSdW5SZXF1ZXN0Giguc2VydmljZWJyaWRnZS5DYW5jZWxXb3JrZmxvd1J1blJlc3BvbnNlElQKC1JlZ2lzdGVySm9iEiEuc2VydmljZWJyaWRnZS5SZWdpc3RlckpvYlJlcXVlc3QaIi5zZXJ2aWNlYnJpZGdlLlJlZ2lzdGVySm9iUmVzcG9uc2USVgoNV2F0Y2hSZWdpc3RyeRIjLnNlcnZpY2VicmlkZ2UuV2F0Y2hSZWdpc3RyeVJlcXVlc3QaHi5zZXJ2aWNlYnJpZGdlLlJlZ2lzdHJ5TWVzc2FnZTABEl0KDkxvb2t1cEZ1bmN0aW9uEiQuc2VydmljZWJyaWRnZS5Mb29rdXBGdW5jdGlvblJlcXVlc3QaJS5zZXJ2aWNlYnJpZGdlLkxvb2t1cEZ1bmN0aW9uUmVzcG9uc2USWwoPUmVwb3J0Q2FsbFN0YXJ0EiUuc2VydmljZWJyaWRnZS5SZXBvcnRDYWxsU3RhcnRSZXF1ZXN0GiEuc2VydmljZWJyaWRnZS5SZXBvcnRDYWxsUmVzcG9uc2USUQoKUmVwb3J0Q2FsbBIgLnNlcnZpY2VicmlkZ2UuUmVwb3J0Q2FsbFJlcXVlc3QaIS5zZXJ2aWNlYnJpZGdlLlJlcG9ydENhbGxSZXNwb25zZRJOCglSZXBvcnRMb2cSHy5zZXJ2aWNlYnJpZGdlLlJlcG9ydExvZ1JlcXVlc3QaIC5zZXJ2aWNlYnJpZGdlLlJlcG9ydExvZ1Jlc3BvbnNlEkgKB1B1Ymxpc2gSHS5zZXJ2aWNlYnJpZGdlLlB1Ymxpc2hSZXF1ZXN0Gh4uc2VydmljZWJyaWRnZS5QdWJsaXNoUmVzcG9uc2USVwoMQXBwZW5kU3RyZWFtEiIuc2VydmljZWJyaWRnZS5BcHBlbmRTdHJlYW1SZXF1ZXN0GiMuc2VydmljZWJyaWRnZS5BcHBlbmRTdHJlYW1SZXNwb25zZRJLCghXYXRjaFJ1bhIeLnNlcnZpY2VicmlkZ2UuV2F0Y2hSdW5SZXF1ZXN0Gh0uc2VydmljZWJyaWRnZS5SdW5TdHJlYW1DaHVuazABMlwKE1NlcnZpY2VCcmlkZ2VXb3JrZXISRQoGSGFuZGxlEhwuc2VydmljZWJyaWRnZS5IYW5kbGVSZXF1ZXN0Gh0uc2VydmljZWJyaWRnZS5IYW5kbGVSZXNwb25zZUItWitnaXRodWIuY29tL3NlcnZpY2UtYnJpZGdlL2dvL2ludGVybmFsL3BiO3BiYgZwcm90bzM=";
|
|
14
14
|
|
|
15
|
+
// sdk/src/session_v2.ts
|
|
16
|
+
class AdaptiveHeartbeatV2 {
|
|
17
|
+
intervalMs;
|
|
18
|
+
timeoutMs;
|
|
19
|
+
missCount = 0;
|
|
20
|
+
rttEwma = 0;
|
|
21
|
+
alpha = 0.3;
|
|
22
|
+
constructor(intervalMs, timeoutMs) {
|
|
23
|
+
this.intervalMs = intervalMs;
|
|
24
|
+
this.timeoutMs = timeoutMs;
|
|
25
|
+
}
|
|
26
|
+
nextIntervalMs() {
|
|
27
|
+
let base = this.intervalMs / 3;
|
|
28
|
+
if (this.missCount > 0) {
|
|
29
|
+
const shift = Math.min(this.missCount, 4);
|
|
30
|
+
base = base / 2 ** shift;
|
|
31
|
+
} else if (this.rttEwma > 0 && this.rttEwma < 50) {
|
|
32
|
+
base = Math.min(base * 2, 30000);
|
|
33
|
+
}
|
|
34
|
+
return Math.max(2000, Math.min(30000, base));
|
|
35
|
+
}
|
|
36
|
+
onPong(rttMs) {
|
|
37
|
+
this.rttEwma = this.alpha * rttMs + (1 - this.alpha) * this.rttEwma;
|
|
38
|
+
this.missCount = 0;
|
|
39
|
+
}
|
|
40
|
+
onMiss() {
|
|
41
|
+
return ++this.missCount;
|
|
42
|
+
}
|
|
43
|
+
resetMiss() {
|
|
44
|
+
this.missCount = 0;
|
|
45
|
+
}
|
|
46
|
+
getTimeoutMs() {
|
|
47
|
+
return this.timeoutMs;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class FlowControlStateV2 {
|
|
52
|
+
permits;
|
|
53
|
+
window;
|
|
54
|
+
minW;
|
|
55
|
+
maxW;
|
|
56
|
+
constructor(initial, min, max) {
|
|
57
|
+
this.permits = initial;
|
|
58
|
+
this.window = initial;
|
|
59
|
+
this.minW = min;
|
|
60
|
+
this.maxW = max;
|
|
61
|
+
}
|
|
62
|
+
tryConsume() {
|
|
63
|
+
if (this.permits <= 0)
|
|
64
|
+
return false;
|
|
65
|
+
this.permits--;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
release(n) {
|
|
69
|
+
this.permits += n;
|
|
70
|
+
}
|
|
71
|
+
available() {
|
|
72
|
+
return this.permits;
|
|
73
|
+
}
|
|
74
|
+
setWindow(n) {
|
|
75
|
+
this.window = Math.max(this.minW, Math.min(this.maxW, n));
|
|
76
|
+
this.permits = this.window;
|
|
77
|
+
}
|
|
78
|
+
getWindow() {
|
|
79
|
+
return this.window;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class BackoffV2 {
|
|
84
|
+
attempt = 0;
|
|
85
|
+
baseMs = 100;
|
|
86
|
+
maxMs = 30000;
|
|
87
|
+
circuitFails = 0;
|
|
88
|
+
next() {
|
|
89
|
+
this.attempt++;
|
|
90
|
+
const calculated = this.baseMs * 2 ** Math.min(this.attempt, 10);
|
|
91
|
+
const capped = Math.min(calculated, this.maxMs);
|
|
92
|
+
const jittered = Math.random() * capped;
|
|
93
|
+
return Math.max(this.baseMs, jittered);
|
|
94
|
+
}
|
|
95
|
+
reset() {
|
|
96
|
+
this.attempt = 0;
|
|
97
|
+
this.circuitFails = 0;
|
|
98
|
+
}
|
|
99
|
+
recordFail() {
|
|
100
|
+
this.circuitFails++;
|
|
101
|
+
}
|
|
102
|
+
isCircuitOpen() {
|
|
103
|
+
return this.circuitFails >= 10;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class PositionTrackerV2 {
|
|
108
|
+
lastReceivedSeq = BigInt(0);
|
|
109
|
+
lastSentSeq = BigInt(0);
|
|
110
|
+
completedIds = new Set;
|
|
111
|
+
maxCompleted = 512;
|
|
112
|
+
recordReceived(seq) {
|
|
113
|
+
if (seq > this.lastReceivedSeq)
|
|
114
|
+
this.lastReceivedSeq = seq;
|
|
115
|
+
}
|
|
116
|
+
recordSent(seq) {
|
|
117
|
+
if (seq > this.lastSentSeq)
|
|
118
|
+
this.lastSentSeq = seq;
|
|
119
|
+
}
|
|
120
|
+
markCompleted(commandId) {
|
|
121
|
+
this.completedIds.add(commandId);
|
|
122
|
+
if (this.completedIds.size > this.maxCompleted) {
|
|
123
|
+
const first = this.completedIds.values().next().value;
|
|
124
|
+
if (first !== undefined)
|
|
125
|
+
this.completedIds.delete(first);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
getResumeState(token, epoch) {
|
|
129
|
+
return {
|
|
130
|
+
resumeToken: token,
|
|
131
|
+
epoch,
|
|
132
|
+
lastReceivedSeq: this.lastReceivedSeq,
|
|
133
|
+
lastSentSeq: this.lastSentSeq,
|
|
134
|
+
completedCommandIds: Array.from(this.completedIds)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class ConfigPushStateV2 {
|
|
140
|
+
defaultMode = "direct";
|
|
141
|
+
serviceOverrides = {};
|
|
142
|
+
functionOverrides = {};
|
|
143
|
+
apply(config) {
|
|
144
|
+
if (config.defaultMode)
|
|
145
|
+
this.defaultMode = config.defaultMode;
|
|
146
|
+
if (config.serviceOverrides) {
|
|
147
|
+
Object.assign(this.serviceOverrides, config.serviceOverrides);
|
|
148
|
+
}
|
|
149
|
+
if (config.functionOverrides) {
|
|
150
|
+
Object.assign(this.functionOverrides, config.functionOverrides);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
resolveMode(fnName) {
|
|
154
|
+
const fnOverride = this.functionOverrides[fnName];
|
|
155
|
+
if (fnOverride?.mode)
|
|
156
|
+
return fnOverride.mode;
|
|
157
|
+
const svcName = fnName.includes("/") ? fnName.split("/")[0] : fnName;
|
|
158
|
+
const svcOverride = this.serviceOverrides[svcName];
|
|
159
|
+
if (svcOverride?.mode)
|
|
160
|
+
return svcOverride.mode;
|
|
161
|
+
return this.defaultMode;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
class V2SessionClient {
|
|
166
|
+
config;
|
|
167
|
+
_state = "connecting";
|
|
168
|
+
resumeToken = "";
|
|
169
|
+
epoch = BigInt(0);
|
|
170
|
+
position;
|
|
171
|
+
flowCtrl;
|
|
172
|
+
heartbeat;
|
|
173
|
+
backoff;
|
|
174
|
+
configPush;
|
|
175
|
+
_draining = false;
|
|
176
|
+
_stopped = false;
|
|
177
|
+
constructor(config) {
|
|
178
|
+
this.config = {
|
|
179
|
+
serverAddress: config.serverAddress,
|
|
180
|
+
serviceName: config.serviceName,
|
|
181
|
+
instanceId: config.instanceId,
|
|
182
|
+
zone: config.zone ?? "",
|
|
183
|
+
transportMode: config.transportMode ?? "direct",
|
|
184
|
+
maxInflight: config.maxInflight ?? 128,
|
|
185
|
+
minInflight: config.minInflight ?? 1,
|
|
186
|
+
sdkVersion: config.sdkVersion ?? "node-v2.0.0",
|
|
187
|
+
capabilities: config.capabilities ?? [],
|
|
188
|
+
tlsEnabled: config.tlsEnabled ?? false,
|
|
189
|
+
certPem: config.certPem ?? "",
|
|
190
|
+
keyPem: config.keyPem ?? "",
|
|
191
|
+
caPem: config.caPem ?? ""
|
|
192
|
+
};
|
|
193
|
+
this.position = new PositionTrackerV2;
|
|
194
|
+
this.flowCtrl = new FlowControlStateV2(this.config.maxInflight, this.config.minInflight, this.config.maxInflight);
|
|
195
|
+
this.heartbeat = new AdaptiveHeartbeatV2(1e4, 30000);
|
|
196
|
+
this.backoff = new BackoffV2;
|
|
197
|
+
this.configPush = new ConfigPushStateV2;
|
|
198
|
+
}
|
|
199
|
+
get state() {
|
|
200
|
+
return this._state;
|
|
201
|
+
}
|
|
202
|
+
get isDraining() {
|
|
203
|
+
return this._draining;
|
|
204
|
+
}
|
|
205
|
+
get isStopped() {
|
|
206
|
+
return this._stopped;
|
|
207
|
+
}
|
|
208
|
+
getHelloFields() {
|
|
209
|
+
const rs = this.position.getResumeState(this.resumeToken, this.epoch);
|
|
210
|
+
return {
|
|
211
|
+
identity: {
|
|
212
|
+
serviceName: this.config.serviceName,
|
|
213
|
+
instanceId: this.config.instanceId,
|
|
214
|
+
transport: this.config.transportMode
|
|
215
|
+
},
|
|
216
|
+
maxInflight: this.config.maxInflight,
|
|
217
|
+
minInflight: this.config.minInflight,
|
|
218
|
+
resumeToken: rs.resumeToken,
|
|
219
|
+
epoch: rs.epoch,
|
|
220
|
+
lastReceivedSeq: rs.lastReceivedSeq,
|
|
221
|
+
lastSentSeq: rs.lastSentSeq,
|
|
222
|
+
completedCommandIds: rs.completedCommandIds,
|
|
223
|
+
sdkVersion: this.config.sdkVersion,
|
|
224
|
+
capabilities: this.config.capabilities,
|
|
225
|
+
zone: this.config.zone,
|
|
226
|
+
transportMode: this.config.transportMode
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
onHelloAck(ack) {
|
|
230
|
+
this.resumeToken = ack.resumeToken;
|
|
231
|
+
this.epoch = ack.epoch;
|
|
232
|
+
this._state = "ready";
|
|
233
|
+
this.flowCtrl.setWindow(ack.initialPermits);
|
|
234
|
+
this.heartbeat = new AdaptiveHeartbeatV2(ack.heartbeatIntervalMs, ack.heartbeatTimeoutMs);
|
|
235
|
+
this.backoff.reset();
|
|
236
|
+
console.log(`[session-v2] hello_ack session=${ack.sessionId} resumed=${ack.resumed} epoch=${ack.epoch}`);
|
|
237
|
+
}
|
|
238
|
+
onCommandReceived(seq, commandId) {
|
|
239
|
+
this.position.recordReceived(seq);
|
|
240
|
+
if (!this.flowCtrl.tryConsume()) {
|
|
241
|
+
console.warn(`[session-v2] no permits for command ${commandId}, backpressure`);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (this._state === "ready")
|
|
245
|
+
this._state = "active";
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
onCommandCompleted(seq, commandId) {
|
|
249
|
+
this.position.markCompleted(commandId);
|
|
250
|
+
this.position.recordSent(seq);
|
|
251
|
+
this.flowCtrl.release(1);
|
|
252
|
+
}
|
|
253
|
+
onPermitGrant(additional) {
|
|
254
|
+
this.flowCtrl.release(additional);
|
|
255
|
+
}
|
|
256
|
+
onFlowControlUpdate(newWindowSize, reason) {
|
|
257
|
+
this.flowCtrl.setWindow(newWindowSize);
|
|
258
|
+
console.log(`[session-v2] flow_control_update window=${newWindowSize} reason=${reason}`);
|
|
259
|
+
}
|
|
260
|
+
onPong(rttMs) {
|
|
261
|
+
this.heartbeat.onPong(rttMs);
|
|
262
|
+
if (this._state === "suspended") {
|
|
263
|
+
this._state = "ready";
|
|
264
|
+
console.log("[session-v2] heartbeat resumed, state→ready");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
onHeartbeatMiss() {
|
|
268
|
+
const count = this.heartbeat.onMiss();
|
|
269
|
+
if (count >= 2 && (this._state === "ready" || this._state === "active")) {
|
|
270
|
+
this._state = "suspended";
|
|
271
|
+
console.warn(`[session-v2] heartbeat miss ${count}, state→suspended`);
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
onDrain(reason, deadlineMs) {
|
|
277
|
+
if (!this._draining) {
|
|
278
|
+
this._draining = true;
|
|
279
|
+
this._state = "draining";
|
|
280
|
+
console.log(`[session-v2] drain initiated reason=${reason} deadline=${deadlineMs}ms`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
onGoaway(code, reason) {
|
|
284
|
+
console.log(`[session-v2] goaway code=${code} reason=${reason}`);
|
|
285
|
+
switch (code) {
|
|
286
|
+
case "GOAWAY_FENCED":
|
|
287
|
+
this._state = "fenced";
|
|
288
|
+
this.stop();
|
|
289
|
+
break;
|
|
290
|
+
case "GOAWAY_GRACEFUL_SHUTDOWN":
|
|
291
|
+
this._state = "closed";
|
|
292
|
+
break;
|
|
293
|
+
default:
|
|
294
|
+
this._state = "closed";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
onConfigPush(config) {
|
|
298
|
+
this.configPush.apply(config);
|
|
299
|
+
console.log("[session-v2] config push applied");
|
|
300
|
+
}
|
|
301
|
+
resolveTransportMode(fnName) {
|
|
302
|
+
return this.configPush.resolveMode(fnName);
|
|
303
|
+
}
|
|
304
|
+
stop() {
|
|
305
|
+
this._stopped = true;
|
|
306
|
+
this._state = "closed";
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function validateV2Config(cfg) {
|
|
310
|
+
if (!cfg.serverAddress)
|
|
311
|
+
throw new Error("servicebridge: serverAddress is required");
|
|
312
|
+
if (!cfg.serviceName)
|
|
313
|
+
throw new Error("servicebridge: serviceName is required");
|
|
314
|
+
if (!cfg.instanceId)
|
|
315
|
+
throw new Error("servicebridge: instanceId is required");
|
|
316
|
+
if (cfg.transportMode && cfg.transportMode !== "direct" && cfg.transportMode !== "proxy") {
|
|
317
|
+
throw new Error(`servicebridge: transportMode must be 'direct' or 'proxy', got '${cfg.transportMode}'`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
15
321
|
// sdk/src/grpc-client.ts
|
|
16
322
|
function outboundIP() {
|
|
17
323
|
return new Promise((resolve) => {
|
|
@@ -591,13 +897,16 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
591
897
|
const fnAliasMap = new Map;
|
|
592
898
|
const functionChannels = new Map;
|
|
593
899
|
let isOnline = false;
|
|
900
|
+
let isFlushing = false;
|
|
594
901
|
let stopped = false;
|
|
595
|
-
let
|
|
902
|
+
let isWatchingChannel = false;
|
|
596
903
|
const offlineQueue = [];
|
|
597
904
|
let workerServer = null;
|
|
598
905
|
let workerSessionStream = null;
|
|
599
906
|
let workerSessionReconnectTimer = null;
|
|
600
907
|
let workerSessionPingTimer = null;
|
|
908
|
+
let workerSessionPositionTimer = null;
|
|
909
|
+
let v2Session = null;
|
|
601
910
|
let serveState = null;
|
|
602
911
|
let heartbeatTimer = null;
|
|
603
912
|
let registrationSyncPromise = null;
|
|
@@ -656,7 +965,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
656
965
|
span_id: op.spanId,
|
|
657
966
|
parent_span_id: op.parentSpanId,
|
|
658
967
|
fn: op.fn,
|
|
659
|
-
started_at:
|
|
968
|
+
started_at: op.startedAt,
|
|
660
969
|
input: op.inputBuf,
|
|
661
970
|
attempt: op.attempt
|
|
662
971
|
}
|
|
@@ -670,7 +979,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
670
979
|
parent_span_id: op.parentSpanId,
|
|
671
980
|
fn: op.fn,
|
|
672
981
|
service_name: service,
|
|
673
|
-
started_at:
|
|
982
|
+
started_at: op.startedAt,
|
|
674
983
|
input: op.inputBuf,
|
|
675
984
|
attempt: op.attempt,
|
|
676
985
|
instance_id: serveState?.instanceId ?? ""
|
|
@@ -683,8 +992,8 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
683
992
|
trace_id: op.traceId,
|
|
684
993
|
span_id: op.spanId,
|
|
685
994
|
fn: op.fn,
|
|
686
|
-
started_at:
|
|
687
|
-
duration_ms:
|
|
995
|
+
started_at: op.startedAt,
|
|
996
|
+
duration_ms: op.durationMs,
|
|
688
997
|
success: op.success,
|
|
689
998
|
error: op.error ?? "",
|
|
690
999
|
input: op.inputBuf,
|
|
@@ -700,8 +1009,8 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
700
1009
|
span_id: op.spanId,
|
|
701
1010
|
fn: op.fn,
|
|
702
1011
|
service_name: service,
|
|
703
|
-
started_at:
|
|
704
|
-
duration_ms:
|
|
1012
|
+
started_at: op.startedAt,
|
|
1013
|
+
duration_ms: op.durationMs,
|
|
705
1014
|
success: op.success,
|
|
706
1015
|
error: op.error ?? "",
|
|
707
1016
|
input: op.inputBuf,
|
|
@@ -900,36 +1209,43 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
900
1209
|
}
|
|
901
1210
|
}
|
|
902
1211
|
_controlReady.then(() => {
|
|
903
|
-
|
|
904
|
-
flushQueue().catch((err) => {
|
|
905
|
-
if (isConnectionError(err))
|
|
906
|
-
scheduleOnlineRestore();
|
|
907
|
-
else
|
|
908
|
-
reportSDKError("flush-on-ready", err);
|
|
909
|
-
});
|
|
1212
|
+
watchChannelConnectivity();
|
|
910
1213
|
}).catch(() => {});
|
|
911
|
-
function
|
|
912
|
-
|
|
1214
|
+
function watchChannelConnectivity() {
|
|
1215
|
+
isWatchingChannel = false;
|
|
1216
|
+
if (stopped)
|
|
913
1217
|
return;
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1218
|
+
const channel = stub.getChannel();
|
|
1219
|
+
const state = channel.getConnectivityState(true);
|
|
1220
|
+
if (state === grpc.connectivityState.READY) {
|
|
1221
|
+
if (!isOnline) {
|
|
917
1222
|
isOnline = true;
|
|
918
1223
|
const queueLen = offlineQueue.length;
|
|
919
1224
|
console.info(`[servicebridge] reconnected to runtime${queueLen > 0 ? ` — flushing ${queueLen} queued operation(s)` : ""}`);
|
|
920
|
-
flushQueue().
|
|
921
|
-
if (
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1225
|
+
flushQueue().then(() => {
|
|
1226
|
+
if (isOnline)
|
|
1227
|
+
scheduleNextHeartbeat(100);
|
|
1228
|
+
}).catch((err) => {
|
|
1229
|
+
reportSDKError("flush-on-restore", err);
|
|
925
1230
|
});
|
|
926
|
-
scheduleNextHeartbeat(100);
|
|
927
1231
|
}
|
|
928
|
-
}
|
|
1232
|
+
} else if (state === grpc.connectivityState.TRANSIENT_FAILURE) {
|
|
1233
|
+
if (isOnline) {
|
|
1234
|
+
isOnline = false;
|
|
1235
|
+
console.warn("[servicebridge] lost connection to runtime — entering offline mode");
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
isWatchingChannel = true;
|
|
1239
|
+
channel.watchConnectivityState(state, Infinity, watchChannelConnectivity);
|
|
1240
|
+
}
|
|
1241
|
+
function ensureChannelWatch() {
|
|
1242
|
+
if (!isWatchingChannel && !stopped) {
|
|
1243
|
+
watchChannelConnectivity();
|
|
1244
|
+
}
|
|
929
1245
|
}
|
|
930
1246
|
function isConnectionError(e) {
|
|
931
1247
|
const code = e?.code;
|
|
932
|
-
return code === grpc.status.UNAVAILABLE || code === grpc.status.UNKNOWN
|
|
1248
|
+
return code === grpc.status.UNAVAILABLE || code === grpc.status.UNKNOWN;
|
|
933
1249
|
}
|
|
934
1250
|
function normalizeUnknownErrorMessage(error) {
|
|
935
1251
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -962,7 +1278,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
962
1278
|
console.warn("[servicebridge] lost connection to runtime — entering offline mode");
|
|
963
1279
|
}
|
|
964
1280
|
isOnline = false;
|
|
965
|
-
|
|
1281
|
+
ensureChannelWatch();
|
|
966
1282
|
enqueueOffline({
|
|
967
1283
|
type: "reportCallStart",
|
|
968
1284
|
traceId,
|
|
@@ -1013,7 +1329,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1013
1329
|
console.warn("[servicebridge] lost connection to runtime — entering offline mode");
|
|
1014
1330
|
}
|
|
1015
1331
|
isOnline = false;
|
|
1016
|
-
|
|
1332
|
+
ensureChannelWatch();
|
|
1017
1333
|
enqueueOffline({
|
|
1018
1334
|
type: "reportCall",
|
|
1019
1335
|
traceId,
|
|
@@ -1033,66 +1349,71 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1033
1349
|
});
|
|
1034
1350
|
}
|
|
1035
1351
|
async function flushQueue() {
|
|
1036
|
-
if (offlineQueue.length === 0)
|
|
1352
|
+
if (isFlushing || offlineQueue.length === 0)
|
|
1037
1353
|
return;
|
|
1354
|
+
isFlushing = true;
|
|
1038
1355
|
const snapshot = offlineQueue.slice();
|
|
1039
1356
|
let flushed = 0;
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
if (
|
|
1087
|
-
|
|
1357
|
+
try {
|
|
1358
|
+
while (offlineQueue.length > 0 && isOnline) {
|
|
1359
|
+
const op = offlineQueue[0];
|
|
1360
|
+
try {
|
|
1361
|
+
if (op.type === "event") {
|
|
1362
|
+
await new Promise((res, rej) => {
|
|
1363
|
+
stub.Publish({
|
|
1364
|
+
topic: op.topic,
|
|
1365
|
+
payload: toJsonBuffer(op.payload),
|
|
1366
|
+
headers: toWireStringMap(op.opts?.headers),
|
|
1367
|
+
trace_id: op.opts?.traceId ?? "",
|
|
1368
|
+
parent_span_id: op.opts?.parentSpanId ?? "",
|
|
1369
|
+
producer_service: service,
|
|
1370
|
+
idempotency_key: op.opts?.idempotencyKey ?? ""
|
|
1371
|
+
}, meta, unaryDeadlineOptions(), (err) => err ? rej(err) : res());
|
|
1372
|
+
});
|
|
1373
|
+
} else if (op.type === "job") {
|
|
1374
|
+
await new Promise((res, rej) => {
|
|
1375
|
+
stub.RegisterJob({
|
|
1376
|
+
cron_expr: op.opts.cron ?? "",
|
|
1377
|
+
timezone: op.opts.timezone ?? "UTC",
|
|
1378
|
+
misfire_policy: op.opts.misfire ?? "fire_now",
|
|
1379
|
+
target_type: op.opts.via ?? "rpc",
|
|
1380
|
+
target_ref: op.target,
|
|
1381
|
+
delay_ms: op.opts.delay ?? 0,
|
|
1382
|
+
service_name: service,
|
|
1383
|
+
retry_policy_json: op.opts.retryPolicyJson ?? "{}"
|
|
1384
|
+
}, meta, unaryDeadlineOptions(), (err) => err ? rej(err) : res());
|
|
1385
|
+
});
|
|
1386
|
+
} else if (op.type === "workflow") {
|
|
1387
|
+
await new Promise((res, rej) => {
|
|
1388
|
+
stub.RegisterWorkflow({
|
|
1389
|
+
name: op.name,
|
|
1390
|
+
definition: JSON.stringify(op.steps),
|
|
1391
|
+
opts: op.opts ? JSON.stringify(op.opts) : "{}",
|
|
1392
|
+
service_name: service
|
|
1393
|
+
}, meta, unaryDeadlineOptions(), (err) => err ? rej(err) : res());
|
|
1394
|
+
});
|
|
1395
|
+
} else if (op.type === "reportCallStart") {
|
|
1396
|
+
await sendReportCallStart({ ...op });
|
|
1397
|
+
} else if (op.type === "reportCall") {
|
|
1398
|
+
await sendReportCall(op);
|
|
1399
|
+
}
|
|
1400
|
+
offlineQueue.shift();
|
|
1401
|
+
flushed++;
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
if (isConnectionError(err)) {
|
|
1404
|
+
if (isOnline) {
|
|
1405
|
+
console.warn("[servicebridge] lost connection to runtime — entering offline mode");
|
|
1406
|
+
}
|
|
1407
|
+
isOnline = false;
|
|
1408
|
+
ensureChannelWatch();
|
|
1409
|
+
break;
|
|
1088
1410
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
break;
|
|
1411
|
+
reportSDKError("flush-offline-queue", err);
|
|
1412
|
+
offlineQueue.shift();
|
|
1092
1413
|
}
|
|
1093
|
-
reportSDKError("flush-offline-queue", err);
|
|
1094
|
-
break;
|
|
1095
1414
|
}
|
|
1415
|
+
} finally {
|
|
1416
|
+
isFlushing = false;
|
|
1096
1417
|
}
|
|
1097
1418
|
if (flushed > 0) {
|
|
1098
1419
|
const remaining = offlineQueue.length;
|
|
@@ -1174,7 +1495,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1174
1495
|
});
|
|
1175
1496
|
});
|
|
1176
1497
|
}
|
|
1177
|
-
async function
|
|
1498
|
+
async function reconcileRegistrations(reason) {
|
|
1178
1499
|
if (!serveState)
|
|
1179
1500
|
return;
|
|
1180
1501
|
if (registrationSyncPromise) {
|
|
@@ -1182,50 +1503,44 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1182
1503
|
}
|
|
1183
1504
|
const state = serveState;
|
|
1184
1505
|
registrationSyncPromise = (async () => {
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
name: groupName,
|
|
1205
|
-
pattern: entry.pattern,
|
|
1206
|
-
mode: "shared",
|
|
1207
|
-
retry_policy_json: entry.opts.retryPolicyJson ?? "{}",
|
|
1208
|
-
active: true,
|
|
1209
|
-
filter_expr: entry.opts.filterExpr ?? ""
|
|
1210
|
-
}, meta, unaryDeadlineOptions(), (err) => err ? reject(normalizeServiceError(err, `register-group:${reason}:${groupName}`)) : resolve());
|
|
1211
|
-
});
|
|
1212
|
-
await new Promise((resolve, reject) => {
|
|
1213
|
-
stub.RegisterGroupMember({
|
|
1214
|
-
group_name: groupName,
|
|
1506
|
+
const functions = Array.from(fnHandlers.entries()).map(([fn, fnEntry]) => ({
|
|
1507
|
+
fn_name: fn,
|
|
1508
|
+
weight: Math.max(1, state.opts.weight ?? 1),
|
|
1509
|
+
input_schema_json: fnEntry.opts.schema?.input ? JSON.stringify(fnEntry.opts.schema.input) : "",
|
|
1510
|
+
output_schema_json: fnEntry.opts.schema?.output ? JSON.stringify(fnEntry.opts.schema.output) : "",
|
|
1511
|
+
allowed_callers: fnEntry.opts.allowedCallers ?? []
|
|
1512
|
+
}));
|
|
1513
|
+
const consumerGroups = Array.from(eventHandlers.values()).map((entry) => ({
|
|
1514
|
+
name: entry.groupName,
|
|
1515
|
+
pattern: entry.pattern,
|
|
1516
|
+
mode: "shared",
|
|
1517
|
+
retry_policy_json: entry.opts.retryPolicyJson ?? "{}",
|
|
1518
|
+
active: true,
|
|
1519
|
+
filter_expr: entry.opts.filterExpr ?? ""
|
|
1520
|
+
}));
|
|
1521
|
+
const metrics = getProcessMetrics();
|
|
1522
|
+
await new Promise((resolve, reject) => {
|
|
1523
|
+
stub.Reconcile({
|
|
1524
|
+
identity: {
|
|
1215
1525
|
service_name: service,
|
|
1216
1526
|
instance_id: state.instanceId,
|
|
1217
1527
|
endpoint: state.endpoint,
|
|
1218
|
-
transport: state.transport
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1528
|
+
transport: state.transport
|
|
1529
|
+
},
|
|
1530
|
+
functions,
|
|
1531
|
+
consumer_groups: consumerGroups,
|
|
1532
|
+
transport_mode: "TRANSPORT_MODE_DIRECT",
|
|
1533
|
+
zone: "",
|
|
1534
|
+
metrics: {
|
|
1535
|
+
cpu_percent: metrics.cpuPercent ?? null,
|
|
1536
|
+
ram_mb: metrics.ramMb ?? null
|
|
1537
|
+
}
|
|
1538
|
+
}, meta, unaryDeadlineOptions(), (err) => err ? reject(normalizeServiceError(err, `reconcile:${reason}`)) : resolve());
|
|
1539
|
+
});
|
|
1224
1540
|
registeredGroups.clear();
|
|
1225
|
-
for (const
|
|
1226
|
-
registeredGroups.add(groupName);
|
|
1541
|
+
for (const entry of eventHandlers.values()) {
|
|
1542
|
+
registeredGroups.add(entry.groupName);
|
|
1227
1543
|
}
|
|
1228
|
-
await sendHeartbeat();
|
|
1229
1544
|
})().finally(() => {
|
|
1230
1545
|
registrationSyncPromise = null;
|
|
1231
1546
|
});
|
|
@@ -1256,9 +1571,9 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1256
1571
|
console.warn("[servicebridge] lost connection to runtime — entering offline mode");
|
|
1257
1572
|
}
|
|
1258
1573
|
isOnline = false;
|
|
1259
|
-
|
|
1574
|
+
ensureChannelWatch();
|
|
1260
1575
|
} else if (isRegistryResyncRequiredError(err)) {
|
|
1261
|
-
await
|
|
1576
|
+
await reconcileRegistrations("heartbeat-resync");
|
|
1262
1577
|
} else {
|
|
1263
1578
|
reportSDKError("heartbeat", err);
|
|
1264
1579
|
}
|
|
@@ -1424,6 +1739,10 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1424
1739
|
clearInterval(workerSessionPingTimer);
|
|
1425
1740
|
workerSessionPingTimer = null;
|
|
1426
1741
|
}
|
|
1742
|
+
if (workerSessionPositionTimer) {
|
|
1743
|
+
clearInterval(workerSessionPositionTimer);
|
|
1744
|
+
workerSessionPositionTimer = null;
|
|
1745
|
+
}
|
|
1427
1746
|
if (workerSessionReconnectTimer) {
|
|
1428
1747
|
clearTimeout(workerSessionReconnectTimer);
|
|
1429
1748
|
workerSessionReconnectTimer = null;
|
|
@@ -1492,6 +1811,10 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1492
1811
|
result.error = normalizeUnknownErrorMessage(err);
|
|
1493
1812
|
}
|
|
1494
1813
|
stream.write({ command_result: result });
|
|
1814
|
+
if (v2Session) {
|
|
1815
|
+
v2Session.onCommandCompleted(BigInt(0), commandId);
|
|
1816
|
+
stream.write({ permit_release: { released: 1 } });
|
|
1817
|
+
}
|
|
1495
1818
|
}
|
|
1496
1819
|
function scheduleWorkerSessionReconnect(delayMs = 1000) {
|
|
1497
1820
|
if (stopped || !serveState || workerSessionReconnectTimer)
|
|
@@ -1504,22 +1827,57 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1504
1827
|
});
|
|
1505
1828
|
}, delayMs);
|
|
1506
1829
|
}
|
|
1830
|
+
function adaptConfigPush(cp) {
|
|
1831
|
+
const tc = cp?.transport_config;
|
|
1832
|
+
if (!tc)
|
|
1833
|
+
return {};
|
|
1834
|
+
const defaultModeRaw = tc.default_mode;
|
|
1835
|
+
return {
|
|
1836
|
+
defaultMode: defaultModeRaw === "TRANSPORT_MODE_PROXY" ? "proxy" : "direct",
|
|
1837
|
+
serviceOverrides: tc.service_overrides,
|
|
1838
|
+
functionOverrides: tc.function_overrides
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1507
1841
|
async function openWorkerSession(initial) {
|
|
1508
1842
|
if (!serveState) {
|
|
1509
1843
|
throw new Error("serve() state is not initialized");
|
|
1510
1844
|
}
|
|
1511
1845
|
if (workerSessionStream)
|
|
1512
1846
|
return;
|
|
1847
|
+
if (!v2Session) {
|
|
1848
|
+
v2Session = new V2SessionClient({
|
|
1849
|
+
serverAddress: target,
|
|
1850
|
+
serviceName: service,
|
|
1851
|
+
instanceId: serveState.instanceId,
|
|
1852
|
+
transportMode: "direct",
|
|
1853
|
+
maxInflight: serveState.maxInFlight,
|
|
1854
|
+
minInflight: 1
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1513
1857
|
const stream = stub.OpenWorkerSession(meta);
|
|
1514
1858
|
workerSessionStream = stream;
|
|
1515
|
-
const
|
|
1859
|
+
const hf = v2Session.getHelloFields();
|
|
1860
|
+
stream.write({
|
|
1516
1861
|
hello: {
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1862
|
+
identity: {
|
|
1863
|
+
service_name: hf.identity.serviceName,
|
|
1864
|
+
instance_id: hf.identity.instanceId,
|
|
1865
|
+
endpoint: serveState.endpoint,
|
|
1866
|
+
transport: serveState.transport
|
|
1867
|
+
},
|
|
1868
|
+
max_inflight: hf.maxInflight,
|
|
1869
|
+
min_inflight: hf.minInflight,
|
|
1870
|
+
resume_token: hf.resumeToken,
|
|
1871
|
+
epoch: String(hf.epoch),
|
|
1872
|
+
last_received_seq: String(hf.lastReceivedSeq),
|
|
1873
|
+
last_sent_seq: String(hf.lastSentSeq),
|
|
1874
|
+
completed_command_ids: hf.completedCommandIds,
|
|
1875
|
+
sdk_version: hf.sdkVersion,
|
|
1876
|
+
capabilities: hf.capabilities,
|
|
1877
|
+
zone: hf.zone,
|
|
1878
|
+
transport_mode: hf.transportMode === "proxy" ? "TRANSPORT_MODE_PROXY" : "TRANSPORT_MODE_DIRECT"
|
|
1520
1879
|
}
|
|
1521
|
-
};
|
|
1522
|
-
stream.write(hello);
|
|
1880
|
+
});
|
|
1523
1881
|
workerSessionPingTimer = setInterval(() => {
|
|
1524
1882
|
if (!workerSessionStream)
|
|
1525
1883
|
return;
|
|
@@ -1527,7 +1885,50 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1527
1885
|
ping: { timestamp_unix_ms: Date.now() }
|
|
1528
1886
|
});
|
|
1529
1887
|
}, 1e4);
|
|
1888
|
+
workerSessionPositionTimer = setInterval(() => {
|
|
1889
|
+
if (!workerSessionStream || !v2Session)
|
|
1890
|
+
return;
|
|
1891
|
+
workerSessionStream.write({
|
|
1892
|
+
position_update: {
|
|
1893
|
+
last_received_seq: String(v2Session.position.lastReceivedSeq)
|
|
1894
|
+
}
|
|
1895
|
+
});
|
|
1896
|
+
}, 5000);
|
|
1530
1897
|
stream.on("data", (msg) => {
|
|
1898
|
+
if (msg.seq !== undefined && v2Session) {
|
|
1899
|
+
v2Session.position.recordReceived(BigInt(msg.seq));
|
|
1900
|
+
}
|
|
1901
|
+
if (msg.hello_ack && v2Session) {
|
|
1902
|
+
const ack = msg.hello_ack;
|
|
1903
|
+
const helloAck = {
|
|
1904
|
+
sessionId: ack.session_id ?? "",
|
|
1905
|
+
resumeToken: ack.resume_token ?? "",
|
|
1906
|
+
epoch: BigInt(ack.epoch ?? "0"),
|
|
1907
|
+
resumed: ack.resumed ?? false,
|
|
1908
|
+
resumeFromSeq: BigInt(ack.resume_from_seq ?? "0"),
|
|
1909
|
+
replayedCommands: ack.replayed_commands ?? 0,
|
|
1910
|
+
reconciledResults: ack.reconciled_results ?? 0,
|
|
1911
|
+
heartbeatIntervalMs: ack.heartbeat_interval_ms ?? 1e4,
|
|
1912
|
+
heartbeatTimeoutMs: ack.heartbeat_timeout_ms ?? 30000,
|
|
1913
|
+
initialPermits: ack.initial_permits ?? 0,
|
|
1914
|
+
maxPermits: ack.max_permits ?? 0,
|
|
1915
|
+
effectiveTransportMode: ack.effective_transport_mode === "TRANSPORT_MODE_PROXY" ? "proxy" : "direct",
|
|
1916
|
+
resumeFailReason: ack.resume_fail_reason
|
|
1917
|
+
};
|
|
1918
|
+
v2Session.onHelloAck(helloAck);
|
|
1919
|
+
}
|
|
1920
|
+
if (msg.goaway && v2Session) {
|
|
1921
|
+
v2Session.onGoaway(msg.goaway.code ?? "", msg.goaway.reason ?? "");
|
|
1922
|
+
}
|
|
1923
|
+
if (msg.config_push && v2Session) {
|
|
1924
|
+
v2Session.onConfigPush(adaptConfigPush(msg.config_push));
|
|
1925
|
+
}
|
|
1926
|
+
if (msg.permit_grant && v2Session) {
|
|
1927
|
+
v2Session.onPermitGrant(msg.permit_grant.additional_permits ?? 0);
|
|
1928
|
+
}
|
|
1929
|
+
if (msg.flow_update && v2Session) {
|
|
1930
|
+
v2Session.onFlowControlUpdate(msg.flow_update.new_window_size ?? 0, msg.flow_update.reason ?? "");
|
|
1931
|
+
}
|
|
1531
1932
|
if (msg.command) {
|
|
1532
1933
|
processWorkerSessionCommand(msg.command).catch((err) => {
|
|
1533
1934
|
reportSDKError("worker-session-command", err);
|
|
@@ -1541,6 +1942,10 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1541
1942
|
clearInterval(workerSessionPingTimer);
|
|
1542
1943
|
workerSessionPingTimer = null;
|
|
1543
1944
|
}
|
|
1945
|
+
if (workerSessionPositionTimer) {
|
|
1946
|
+
clearInterval(workerSessionPositionTimer);
|
|
1947
|
+
workerSessionPositionTimer = null;
|
|
1948
|
+
}
|
|
1544
1949
|
workerSessionStream = null;
|
|
1545
1950
|
if (err != null) {
|
|
1546
1951
|
reportSDKError(operation, err);
|
|
@@ -1751,7 +2156,6 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1751
2156
|
}
|
|
1752
2157
|
server.bindAsync(`${host}:0`, serverCreds, (err, p) => err ? reject(err) : resolve(p));
|
|
1753
2158
|
});
|
|
1754
|
-
workerServer.start();
|
|
1755
2159
|
const endpoint = `${advertiseHost}:${port}`;
|
|
1756
2160
|
const instanceId = opts.instanceId || Array.from(crypto.getRandomValues(new Uint8Array(3))).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1757
2161
|
serveState = {
|
|
@@ -1768,7 +2172,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1768
2172
|
serverName: tlsOpts?.serverName
|
|
1769
2173
|
};
|
|
1770
2174
|
await openWorkerSession(true);
|
|
1771
|
-
await
|
|
2175
|
+
await reconcileRegistrations("initial");
|
|
1772
2176
|
scheduleNextHeartbeat();
|
|
1773
2177
|
await _controlReady;
|
|
1774
2178
|
} catch (err) {
|
|
@@ -1787,11 +2191,9 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
|
|
|
1787
2191
|
stop() {
|
|
1788
2192
|
stopped = true;
|
|
1789
2193
|
isOnline = false;
|
|
1790
|
-
|
|
1791
|
-
clearTimeout(onlineRestoreTimer);
|
|
2194
|
+
isWatchingChannel = false;
|
|
1792
2195
|
if (heartbeatTimer)
|
|
1793
2196
|
clearTimeout(heartbeatTimer);
|
|
1794
|
-
onlineRestoreTimer = null;
|
|
1795
2197
|
heartbeatTimer = null;
|
|
1796
2198
|
registeredGroups.clear();
|
|
1797
2199
|
closeWorkerSession();
|
|
@@ -2227,8 +2629,15 @@ function captureConsole(svc) {
|
|
|
2227
2629
|
}
|
|
2228
2630
|
}
|
|
2229
2631
|
export {
|
|
2632
|
+
validateV2Config,
|
|
2230
2633
|
servicebridge,
|
|
2231
2634
|
runWithTraceContext,
|
|
2232
2635
|
getTraceContext,
|
|
2233
|
-
|
|
2636
|
+
V2SessionClient,
|
|
2637
|
+
ServiceBridgeError,
|
|
2638
|
+
PositionTrackerV2,
|
|
2639
|
+
FlowControlStateV2,
|
|
2640
|
+
ConfigPushStateV2,
|
|
2641
|
+
BackoffV2,
|
|
2642
|
+
AdaptiveHeartbeatV2
|
|
2234
2643
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "service-bridge",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1-dev.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ServiceBridge SDK for Node.js — production-ready RPC, durable events, workflows, jobs, and distributed tracing. One Go runtime + PostgreSQL replaces Istio, RabbitMQ, Temporal, and Jaeger.",
|
|
6
6
|
"keywords": [
|