service-bridge 1.1.1-dev.29 → 1.1.1-dev.31

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.
Files changed (3) hide show
  1. package/README.md +192 -0
  2. package/dist/index.js +454 -56
  3. 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) => {
@@ -599,6 +905,8 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
599
905
  let workerSessionStream = null;
600
906
  let workerSessionReconnectTimer = null;
601
907
  let workerSessionPingTimer = null;
908
+ let workerSessionPositionTimer = null;
909
+ let v2Session = null;
602
910
  let serveState = null;
603
911
  let heartbeatTimer = null;
604
912
  let registrationSyncPromise = null;
@@ -657,7 +965,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
657
965
  span_id: op.spanId,
658
966
  parent_span_id: op.parentSpanId,
659
967
  fn: op.fn,
660
- started_at: String(op.startedAt),
968
+ started_at: op.startedAt,
661
969
  input: op.inputBuf,
662
970
  attempt: op.attempt
663
971
  }
@@ -671,7 +979,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
671
979
  parent_span_id: op.parentSpanId,
672
980
  fn: op.fn,
673
981
  service_name: service,
674
- started_at: String(op.startedAt),
982
+ started_at: op.startedAt,
675
983
  input: op.inputBuf,
676
984
  attempt: op.attempt,
677
985
  instance_id: serveState?.instanceId ?? ""
@@ -684,8 +992,8 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
684
992
  trace_id: op.traceId,
685
993
  span_id: op.spanId,
686
994
  fn: op.fn,
687
- started_at: String(op.startedAt),
688
- duration_ms: String(op.durationMs),
995
+ started_at: op.startedAt,
996
+ duration_ms: op.durationMs,
689
997
  success: op.success,
690
998
  error: op.error ?? "",
691
999
  input: op.inputBuf,
@@ -701,8 +1009,8 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
701
1009
  span_id: op.spanId,
702
1010
  fn: op.fn,
703
1011
  service_name: service,
704
- started_at: String(op.startedAt),
705
- duration_ms: String(op.durationMs),
1012
+ started_at: op.startedAt,
1013
+ duration_ms: op.durationMs,
706
1014
  success: op.success,
707
1015
  error: op.error ?? "",
708
1016
  input: op.inputBuf,
@@ -1187,7 +1495,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1187
1495
  });
1188
1496
  });
1189
1497
  }
1190
- async function syncRegistrations(reason) {
1498
+ async function reconcileRegistrations(reason) {
1191
1499
  if (!serveState)
1192
1500
  return;
1193
1501
  if (registrationSyncPromise) {
@@ -1195,50 +1503,44 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1195
1503
  }
1196
1504
  const state = serveState;
1197
1505
  registrationSyncPromise = (async () => {
1198
- const nextGroups = new Set;
1199
- for (const [fn, fnEntry] of fnHandlers.entries()) {
1200
- await new Promise((resolve, reject) => {
1201
- stub.RegisterFunction({
1202
- fn_name: fn,
1203
- service_name: service,
1204
- instance_id: state.instanceId,
1205
- endpoint: state.endpoint,
1206
- transport: state.transport,
1207
- weight: Math.max(1, state.opts.weight ?? 1),
1208
- input_schema_json: fnEntry.opts.schema?.input ? JSON.stringify(fnEntry.opts.schema.input) : "",
1209
- output_schema_json: fnEntry.opts.schema?.output ? JSON.stringify(fnEntry.opts.schema.output) : ""
1210
- }, meta, unaryDeadlineOptions(), (err) => err ? reject(normalizeServiceError(err, `register-function:${reason}:${fn}`)) : resolve());
1211
- });
1212
- }
1213
- for (const entry of eventHandlers.values()) {
1214
- const groupName = entry.groupName;
1215
- await new Promise((resolve, reject) => {
1216
- stub.RegisterConsumerGroup({
1217
- name: groupName,
1218
- pattern: entry.pattern,
1219
- mode: "shared",
1220
- retry_policy_json: entry.opts.retryPolicyJson ?? "{}",
1221
- active: true,
1222
- filter_expr: entry.opts.filterExpr ?? ""
1223
- }, meta, unaryDeadlineOptions(), (err) => err ? reject(normalizeServiceError(err, `register-group:${reason}:${groupName}`)) : resolve());
1224
- });
1225
- await new Promise((resolve, reject) => {
1226
- stub.RegisterGroupMember({
1227
- 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: {
1228
1525
  service_name: service,
1229
1526
  instance_id: state.instanceId,
1230
1527
  endpoint: state.endpoint,
1231
- transport: state.transport,
1232
- weight: Math.max(1, state.opts.weight ?? 1)
1233
- }, meta, unaryDeadlineOptions(), (err) => err ? reject(normalizeServiceError(err, `register-member:${reason}:${groupName}`)) : resolve());
1234
- });
1235
- nextGroups.add(groupName);
1236
- }
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
+ });
1237
1540
  registeredGroups.clear();
1238
- for (const groupName of nextGroups) {
1239
- registeredGroups.add(groupName);
1541
+ for (const entry of eventHandlers.values()) {
1542
+ registeredGroups.add(entry.groupName);
1240
1543
  }
1241
- await sendHeartbeat();
1242
1544
  })().finally(() => {
1243
1545
  registrationSyncPromise = null;
1244
1546
  });
@@ -1271,7 +1573,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1271
1573
  isOnline = false;
1272
1574
  ensureChannelWatch();
1273
1575
  } else if (isRegistryResyncRequiredError(err)) {
1274
- await syncRegistrations("heartbeat-resync");
1576
+ await reconcileRegistrations("heartbeat-resync");
1275
1577
  } else {
1276
1578
  reportSDKError("heartbeat", err);
1277
1579
  }
@@ -1437,6 +1739,10 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1437
1739
  clearInterval(workerSessionPingTimer);
1438
1740
  workerSessionPingTimer = null;
1439
1741
  }
1742
+ if (workerSessionPositionTimer) {
1743
+ clearInterval(workerSessionPositionTimer);
1744
+ workerSessionPositionTimer = null;
1745
+ }
1440
1746
  if (workerSessionReconnectTimer) {
1441
1747
  clearTimeout(workerSessionReconnectTimer);
1442
1748
  workerSessionReconnectTimer = null;
@@ -1505,6 +1811,10 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1505
1811
  result.error = normalizeUnknownErrorMessage(err);
1506
1812
  }
1507
1813
  stream.write({ command_result: result });
1814
+ if (v2Session) {
1815
+ v2Session.onCommandCompleted(BigInt(0), commandId);
1816
+ stream.write({ permit_release: { released: 1 } });
1817
+ }
1508
1818
  }
1509
1819
  function scheduleWorkerSessionReconnect(delayMs = 1000) {
1510
1820
  if (stopped || !serveState || workerSessionReconnectTimer)
@@ -1517,22 +1827,57 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1517
1827
  });
1518
1828
  }, delayMs);
1519
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
+ }
1520
1841
  async function openWorkerSession(initial) {
1521
1842
  if (!serveState) {
1522
1843
  throw new Error("serve() state is not initialized");
1523
1844
  }
1524
1845
  if (workerSessionStream)
1525
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
+ }
1526
1857
  const stream = stub.OpenWorkerSession(meta);
1527
1858
  workerSessionStream = stream;
1528
- const hello = {
1859
+ const hf = v2Session.getHelloFields();
1860
+ stream.write({
1529
1861
  hello: {
1530
- service_name: service,
1531
- instance_id: serveState.instanceId,
1532
- max_inflight: serveState.maxInFlight
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"
1533
1879
  }
1534
- };
1535
- stream.write(hello);
1880
+ });
1536
1881
  workerSessionPingTimer = setInterval(() => {
1537
1882
  if (!workerSessionStream)
1538
1883
  return;
@@ -1540,7 +1885,50 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1540
1885
  ping: { timestamp_unix_ms: Date.now() }
1541
1886
  });
1542
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);
1543
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
+ }
1544
1932
  if (msg.command) {
1545
1933
  processWorkerSessionCommand(msg.command).catch((err) => {
1546
1934
  reportSDKError("worker-session-command", err);
@@ -1554,6 +1942,10 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1554
1942
  clearInterval(workerSessionPingTimer);
1555
1943
  workerSessionPingTimer = null;
1556
1944
  }
1945
+ if (workerSessionPositionTimer) {
1946
+ clearInterval(workerSessionPositionTimer);
1947
+ workerSessionPositionTimer = null;
1948
+ }
1557
1949
  workerSessionStream = null;
1558
1950
  if (err != null) {
1559
1951
  reportSDKError(operation, err);
@@ -1764,7 +2156,6 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1764
2156
  }
1765
2157
  server.bindAsync(`${host}:0`, serverCreds, (err, p) => err ? reject(err) : resolve(p));
1766
2158
  });
1767
- workerServer.start();
1768
2159
  const endpoint = `${advertiseHost}:${port}`;
1769
2160
  const instanceId = opts.instanceId || Array.from(crypto.getRandomValues(new Uint8Array(3))).map((b) => b.toString(16).padStart(2, "0")).join("");
1770
2161
  serveState = {
@@ -1781,7 +2172,7 @@ function servicebridge(url, serviceKey, serviceOrOpts = {}, maybeGlobalOpts = {}
1781
2172
  serverName: tlsOpts?.serverName
1782
2173
  };
1783
2174
  await openWorkerSession(true);
1784
- await syncRegistrations("initial");
2175
+ await reconcileRegistrations("initial");
1785
2176
  scheduleNextHeartbeat();
1786
2177
  await _controlReady;
1787
2178
  } catch (err) {
@@ -2238,8 +2629,15 @@ function captureConsole(svc) {
2238
2629
  }
2239
2630
  }
2240
2631
  export {
2632
+ validateV2Config,
2241
2633
  servicebridge,
2242
2634
  runWithTraceContext,
2243
2635
  getTraceContext,
2244
- ServiceBridgeError
2636
+ V2SessionClient,
2637
+ ServiceBridgeError,
2638
+ PositionTrackerV2,
2639
+ FlowControlStateV2,
2640
+ ConfigPushStateV2,
2641
+ BackoffV2,
2642
+ AdaptiveHeartbeatV2
2245
2643
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "service-bridge",
3
- "version": "1.1.1-dev.29",
3
+ "version": "1.1.1-dev.31",
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": [