stitchkit 0.116.0 → 0.118.0

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 (130) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/agent-runtime/run-acquisition.d.ts +9 -2
  3. package/dist/agent-runtime/run-acquisition.d.ts.map +1 -1
  4. package/dist/agent-runtime/run-execution.d.ts +3 -0
  5. package/dist/agent-runtime/run-execution.d.ts.map +1 -1
  6. package/dist/agent-runtime/run-failure.d.ts +4 -1
  7. package/dist/agent-runtime/run-failure.d.ts.map +1 -1
  8. package/dist/agent-runtime/run-store-retry.d.ts +8 -0
  9. package/dist/agent-runtime/run-store-retry.d.ts.map +1 -0
  10. package/dist/agent-runtime/run-terminal.d.ts.map +1 -1
  11. package/dist/agent-runtime/run-unacquired.d.ts +19 -0
  12. package/dist/agent-runtime/run-unacquired.d.ts.map +1 -0
  13. package/dist/agent-runtime/store-conformance.d.ts +8 -0
  14. package/dist/agent-runtime/store-conformance.d.ts.map +1 -1
  15. package/dist/agent-runtime/store-driver-contract.d.ts +5 -0
  16. package/dist/agent-runtime/store-driver-contract.d.ts.map +1 -1
  17. package/dist/agent-runtime/terminal-commit.d.ts +34 -4
  18. package/dist/agent-runtime/terminal-commit.d.ts.map +1 -1
  19. package/dist/entrypoints/agent-runtime/harness.js +2 -2
  20. package/dist/entrypoints/agent-runtime/sqlite/bun.js +3 -3
  21. package/dist/entrypoints/agent-runtime/sqlite/node.js +3 -3
  22. package/dist/entrypoints/agent-runtime/testing.js +22 -4
  23. package/dist/entrypoints/agent-runtime.d.ts +1 -1
  24. package/dist/entrypoints/agent-runtime.d.ts.map +1 -1
  25. package/dist/entrypoints/agent-runtime.js +13 -11
  26. package/dist/entrypoints/application/diagnostic-journal.js +1 -1
  27. package/dist/entrypoints/application/directory-inbox.js +3 -3
  28. package/dist/entrypoints/cli/publish.js +2 -2
  29. package/dist/entrypoints/files.js +2 -2
  30. package/dist/entrypoints/primitives.js +51 -154
  31. package/dist/entrypoints/server.js +6 -6
  32. package/dist/entrypoints/telegram.d.ts +1 -1
  33. package/dist/entrypoints/telegram.d.ts.map +1 -1
  34. package/dist/entrypoints/telegram.js +352 -137
  35. package/dist/entrypoints/tracking/contract.d.ts +11 -0
  36. package/dist/entrypoints/tracking/contract.d.ts.map +1 -0
  37. package/dist/entrypoints/tracking/contract.js +162 -0
  38. package/dist/entrypoints/tracking/performance.d.ts +8 -0
  39. package/dist/entrypoints/tracking/performance.d.ts.map +1 -0
  40. package/dist/entrypoints/tracking/performance.js +181 -0
  41. package/dist/entrypoints/tracking/server.d.ts +1 -0
  42. package/dist/entrypoints/tracking/server.d.ts.map +1 -1
  43. package/dist/entrypoints/tracking/server.js +40 -12
  44. package/dist/entrypoints/tracking.d.ts +4 -3
  45. package/dist/entrypoints/tracking.d.ts.map +1 -1
  46. package/dist/entrypoints/tracking.js +43 -143
  47. package/dist/entrypoints/xlsx.d.ts +4 -0
  48. package/dist/entrypoints/xlsx.d.ts.map +1 -0
  49. package/dist/entrypoints/xlsx.js +406 -0
  50. package/dist/{index-fa435yq2.js → index-073nw9qr.js} +8 -8
  51. package/dist/index-1f40z584.js +7 -0
  52. package/dist/{index-fdk4r4r6.js → index-75ns2ybj.js} +1 -1
  53. package/dist/{index-jq2ky8fw.js → index-cycp3r3e.js} +124 -181
  54. package/dist/{index-4g6qmgdx.js → index-ghtpfz4z.js} +9 -9
  55. package/dist/{index-n3v695sb.js → index-hdjmdwsj.js} +277 -96
  56. package/dist/{index-zqkz4af9.js → index-nqxrm176.js} +1 -1
  57. package/dist/index-qezhmp51.js +242 -0
  58. package/dist/index-vv1sch8a.js +94 -0
  59. package/dist/{index-fggxgbrk.js → index-xx1j310k.js} +165 -102
  60. package/dist/internal/exclusive-lock.d.ts.map +1 -1
  61. package/dist/primitives/decimal.d.ts +10 -0
  62. package/dist/primitives/decimal.d.ts.map +1 -1
  63. package/dist/primitives/index.d.ts +3 -1
  64. package/dist/primitives/index.d.ts.map +1 -1
  65. package/dist/primitives/money.d.ts +20 -0
  66. package/dist/primitives/money.d.ts.map +1 -1
  67. package/dist/primitives/quantity.d.ts.map +1 -1
  68. package/dist/primitives/rounding.d.ts +40 -0
  69. package/dist/primitives/rounding.d.ts.map +1 -0
  70. package/dist/telegram/budget-transformer.d.ts +2 -2
  71. package/dist/telegram/not-dispatched.d.ts +2 -2
  72. package/dist/telegram/not-dispatched.d.ts.map +1 -1
  73. package/dist/telegram/outbound-budget.d.ts +53 -16
  74. package/dist/telegram/outbound-budget.d.ts.map +1 -1
  75. package/dist/telegram/outbound-cost.d.ts +5 -0
  76. package/dist/telegram/outbound-cost.d.ts.map +1 -0
  77. package/dist/telegram/outbound-pauses.d.ts +17 -0
  78. package/dist/telegram/outbound-pauses.d.ts.map +1 -0
  79. package/dist/telegram/outbound-queue.d.ts +17 -0
  80. package/dist/telegram/outbound-queue.d.ts.map +1 -0
  81. package/dist/telegram/update-intake-work.d.ts +20 -1
  82. package/dist/telegram/update-intake-work.d.ts.map +1 -1
  83. package/dist/telegram/update-intake.d.ts +12 -3
  84. package/dist/telegram/update-intake.d.ts.map +1 -1
  85. package/dist/telegram/update-store-check.d.ts +1 -1
  86. package/dist/telegram/update-store-check.d.ts.map +1 -1
  87. package/dist/telegram/update-store-postgres.d.ts.map +1 -1
  88. package/dist/telegram/update-store-sqlite.d.ts.map +1 -1
  89. package/dist/telegram/update-store.d.ts +5 -1
  90. package/dist/telegram/update-store.d.ts.map +1 -1
  91. package/dist/tracking/attribution.d.ts +1 -1
  92. package/dist/tracking/attribution.d.ts.map +1 -1
  93. package/dist/tracking/client-visit.d.ts +3 -1
  94. package/dist/tracking/client-visit.d.ts.map +1 -1
  95. package/dist/tracking/client.d.ts.map +1 -1
  96. package/dist/tracking/performance/collector.d.ts +61 -0
  97. package/dist/tracking/performance/collector.d.ts.map +1 -0
  98. package/dist/tracking/performance/schema.d.ts +45 -0
  99. package/dist/tracking/performance/schema.d.ts.map +1 -0
  100. package/dist/tracking/performance/summary.d.ts +12 -0
  101. package/dist/tracking/performance/summary.d.ts.map +1 -0
  102. package/dist/tracking/privacy.d.ts +5 -0
  103. package/dist/tracking/privacy.d.ts.map +1 -0
  104. package/dist/tracking/server/telemetry-health.d.ts +31 -0
  105. package/dist/tracking/server/telemetry-health.d.ts.map +1 -0
  106. package/dist/xlsx/cells.d.ts +17 -0
  107. package/dist/xlsx/cells.d.ts.map +1 -0
  108. package/dist/xlsx/render.d.ts +11 -0
  109. package/dist/xlsx/render.d.ts.map +1 -0
  110. package/dist/xlsx/schema.d.ts +119 -0
  111. package/dist/xlsx/schema.d.ts.map +1 -0
  112. package/dist/xlsx/zip.d.ts +13 -0
  113. package/dist/xlsx/zip.d.ts.map +1 -0
  114. package/llms/agent-runtime.2.txt +40 -16
  115. package/llms/agent-runtime.3.txt +17 -0
  116. package/llms/agent-runtime.txt +4 -1
  117. package/llms/primitives.txt +21 -0
  118. package/llms/stitchkit.txt +9 -6
  119. package/llms/telegram.txt +46 -15
  120. package/llms/tracking-contract.txt +32 -0
  121. package/llms/tracking-performance.txt +21 -0
  122. package/llms/tracking-server.txt +1 -0
  123. package/llms/tracking.txt +75 -12
  124. package/llms/{upgrading-0.105.0-0.116.0.txt → upgrading-0.105.0-0.118.0.txt} +73 -3
  125. package/llms/upgrading.txt +1 -1
  126. package/llms/xlsx.txt +107 -0
  127. package/llms-full.txt +392 -34
  128. package/llms.txt +11 -7
  129. package/package.json +27 -9
  130. package/dist/{index-z3d6smer.js → index-tdpmfks8.js} +5 -5
@@ -5,7 +5,7 @@ import"../index-2kbysk9s.js";
5
5
  import"../index-n8z3rvzw.js";
6
6
  import {
7
7
  attemptExclusiveLock
8
- } from "../index-z3d6smer.js";
8
+ } from "../index-tdpmfks8.js";
9
9
  import {
10
10
  TELEGRAM_SEND_FAILURE_REASONS2,
11
11
  classifyTelegramSendFailure2
@@ -908,11 +908,8 @@ function telegramOperatorSender(config) {
908
908
  }
909
909
  // src/telegram/outbound-budget.ts
910
910
  import { z as z5 } from "zod";
911
- var DEFAULT_MESSAGES_PER_SECOND = 24;
912
- var DEFAULT_RETRY_AFTER_FALLBACK_MS = 1000;
913
- var TOKEN = z5.string().min(1, "token must not be empty");
914
- var MESSAGES_PER_SECOND = z5.number().positive().max(1000);
915
- var RETRY_AFTER_FALLBACK_MS = z5.number().int().nonnegative().max(MAX_TIMER_MS);
911
+
912
+ // src/telegram/outbound-cost.ts
916
913
  var SEND_METHODS = new Set([
917
914
  "sendMessage",
918
915
  "sendPhoto",
@@ -959,15 +956,69 @@ function sendCost(method, payload) {
959
956
  const value = payload[field];
960
957
  return Array.isArray(value) ? Math.max(1, value.length) : 1;
961
958
  }
962
- function createBudget(rate, fallback, now, sleepFor) {
963
- const interval = 1000 / rate;
959
+ function chatOf(payload) {
960
+ if (!isRecord(payload))
961
+ return;
962
+ const chat = payload.chat_id;
963
+ return typeof chat === "number" || typeof chat === "string" ? String(chat) : undefined;
964
+ }
965
+
966
+ // src/telegram/outbound-pauses.ts
967
+ function createOutboundPauses({ tokenPauseChats, tokenPauseWindowMs }, now, onPause) {
968
+ const chatPauses = new Map;
969
+ const refusedChats = new Map;
970
+ let pausedUntil = Number.NEGATIVE_INFINITY;
971
+ const pauseEnd = (milliseconds) => {
972
+ if (Number.isNaN(milliseconds) || milliseconds < 0) {
973
+ throw new RangeError("pause must be a non-negative number of milliseconds");
974
+ }
975
+ return now() + (Number.isFinite(milliseconds) ? milliseconds : Number.MAX_SAFE_INTEGER);
976
+ };
977
+ const pauseToken = (milliseconds) => {
978
+ pausedUntil = Math.max(pausedUntil, pauseEnd(milliseconds));
979
+ onPause();
980
+ };
981
+ const forgetExpired = (current) => {
982
+ for (const [chat, until] of chatPauses)
983
+ if (until <= current)
984
+ chatPauses.delete(chat);
985
+ for (const [chat, at] of refusedChats) {
986
+ if (current - at > tokenPauseWindowMs)
987
+ refusedChats.delete(chat);
988
+ }
989
+ };
990
+ return {
991
+ resumesAt: (chat) => Math.max(pausedUntil, (chat === undefined ? undefined : chatPauses.get(chat)) ?? Number.NEGATIVE_INFINITY),
992
+ pauseToken,
993
+ pauseChat(chat, milliseconds) {
994
+ const until = pauseEnd(milliseconds);
995
+ const current = now();
996
+ forgetExpired(current);
997
+ chatPauses.set(chat, Math.max(chatPauses.get(chat) ?? until, until));
998
+ refusedChats.set(chat, current);
999
+ onPause();
1000
+ if (refusedChats.size < tokenPauseChats)
1001
+ return;
1002
+ refusedChats.clear();
1003
+ pauseToken(milliseconds);
1004
+ },
1005
+ pausedForMs: () => Math.max(0, pausedUntil - now()),
1006
+ pausedChats() {
1007
+ forgetExpired(now());
1008
+ return chatPauses.size;
1009
+ }
1010
+ };
1011
+ }
1012
+
1013
+ // src/telegram/outbound-queue.ts
1014
+ function createOutboundQueue(interval, now, sleepFor, pauses) {
964
1015
  const queue = [];
965
1016
  let nextGrantAt = now();
966
- let pausedUntil = Number.NEGATIVE_INFINITY;
967
1017
  let granted = 0;
968
1018
  let messages = 0;
969
1019
  let draining = false;
970
- let wake;
1020
+ let waking;
1021
+ let wakingAt = Number.POSITIVE_INFINITY;
971
1022
  const wait = async (milliseconds, signal) => {
972
1023
  const delay = Math.min(MAX_TIMER_MS, Math.max(0, milliseconds));
973
1024
  if (delay <= 0)
@@ -978,20 +1029,40 @@ function createBudget(rate, fallback, now, sleepFor) {
978
1029
  else
979
1030
  await pending;
980
1031
  };
981
- const wakeWaiting = () => {
982
- const current = wake;
983
- wake = undefined;
1032
+ const wake = () => {
1033
+ const current = waking;
1034
+ waking = undefined;
984
1035
  current?.abort();
985
1036
  };
986
- const nextWaiter = () => queue.find((waiter) => waiter.lane === "interactive") ?? queue[0];
1037
+ const nextWaiter = (current) => {
1038
+ let bulk;
1039
+ let held;
1040
+ for (const waiter of queue) {
1041
+ if (pauses.resumesAt(waiter.chat) > current) {
1042
+ if (held === undefined || pauses.resumesAt(waiter.chat) < pauses.resumesAt(held.chat)) {
1043
+ held = waiter;
1044
+ }
1045
+ } else if (waiter.lane === "interactive") {
1046
+ return waiter;
1047
+ } else {
1048
+ bulk ??= waiter;
1049
+ }
1050
+ }
1051
+ return bulk ?? held;
1052
+ };
1053
+ const detach = (waiter) => {
1054
+ if (waiter.onAbort)
1055
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
1056
+ waiter.expiry?.abort();
1057
+ };
987
1058
  const remove = (waiter) => {
988
1059
  const index = queue.indexOf(waiter);
989
1060
  if (index === -1)
990
1061
  return;
991
- const next = nextWaiter() === waiter;
1062
+ const next = nextWaiter(now()) === waiter;
992
1063
  queue.splice(index, 1);
993
1064
  if (next)
994
- wakeWaiting();
1065
+ wake();
995
1066
  };
996
1067
  const drain = async () => {
997
1068
  if (draining)
@@ -999,38 +1070,38 @@ function createBudget(rate, fallback, now, sleepFor) {
999
1070
  draining = true;
1000
1071
  try {
1001
1072
  while (queue.length > 0) {
1002
- const head = nextWaiter();
1073
+ const current = now();
1074
+ const head = nextWaiter(current);
1003
1075
  if (head === undefined)
1004
1076
  break;
1005
1077
  if (head.signal?.aborted) {
1006
1078
  remove(head);
1007
1079
  continue;
1008
1080
  }
1009
- const current = now();
1010
- const at = Math.max(current, nextGrantAt, pausedUntil);
1081
+ const at = Math.max(current, nextGrantAt, pauses.resumesAt(head.chat));
1011
1082
  if (at > current) {
1012
1083
  const controller = new AbortController;
1013
- wake = controller;
1084
+ waking = controller;
1085
+ wakingAt = at;
1014
1086
  try {
1015
1087
  await wait(at - current, controller.signal);
1016
1088
  } catch (error) {
1017
1089
  if (!controller.signal.aborted) {
1018
1090
  for (const waiter of queue.splice(0)) {
1019
- if (waiter.onAbort)
1020
- waiter.signal?.removeEventListener("abort", waiter.onAbort);
1091
+ detach(waiter);
1021
1092
  waiter.reject(error);
1022
1093
  }
1023
1094
  return;
1024
1095
  }
1025
1096
  } finally {
1026
- if (wake === controller)
1027
- wake = undefined;
1097
+ if (waking === controller)
1098
+ waking = undefined;
1099
+ wakingAt = Number.POSITIVE_INFINITY;
1028
1100
  }
1029
1101
  continue;
1030
1102
  }
1031
1103
  queue.splice(queue.indexOf(head), 1);
1032
- if (head.onAbort)
1033
- head.signal?.removeEventListener("abort", head.onAbort);
1104
+ detach(head);
1034
1105
  nextGrantAt = at + head.cost * interval;
1035
1106
  granted += 1;
1036
1107
  messages += head.cost;
@@ -1042,67 +1113,115 @@ function createBudget(rate, fallback, now, sleepFor) {
1042
1113
  drain();
1043
1114
  }
1044
1115
  };
1045
- const acquire = (cost, signal, options) => {
1046
- if (!Number.isSafeInteger(cost) || cost < 1) {
1047
- return Promise.reject(new RangeError("cost must be an integer of at least one message"));
1048
- }
1049
- if (signal?.aborted)
1050
- return Promise.reject(signal.reason);
1051
- return new Promise((resolve, reject) => {
1052
- const waiter = {
1053
- cost,
1054
- lane: options?.lane ?? "interactive",
1055
- resolve,
1056
- reject,
1057
- ...signal && { signal }
1058
- };
1059
- if (signal) {
1060
- const onAbort = () => {
1061
- remove(waiter);
1062
- reject(signal.reason);
1063
- };
1064
- waiter.onAbort = onAbort;
1065
- signal.addEventListener("abort", onAbort, { once: true });
1066
- }
1067
- queue.push(waiter);
1068
- drain();
1116
+ const expireAfter = (waiter, maxWaitMs, reject) => {
1117
+ waiter.expiry = new AbortController;
1118
+ wait(maxWaitMs, waiter.expiry.signal).then(() => {
1119
+ if (!queue.includes(waiter))
1120
+ return;
1121
+ remove(waiter);
1122
+ detach(waiter);
1123
+ reject(new TelegramNotDispatchedError2("queue", "the Telegram request waited longer than maxWaitMs for its turn"));
1124
+ }, () => {
1125
+ return;
1069
1126
  });
1070
1127
  };
1071
- const pauseFor = (milliseconds) => {
1072
- if (Number.isNaN(milliseconds) || milliseconds < 0) {
1073
- throw new RangeError("pause must be a non-negative number of milliseconds");
1074
- }
1075
- const duration = Number.isFinite(milliseconds) ? milliseconds : Number.MAX_SAFE_INTEGER;
1076
- pausedUntil = Math.max(pausedUntil, now() + duration);
1077
- wakeWaiting();
1128
+ return {
1129
+ acquire(cost, signal, options) {
1130
+ if (!Number.isSafeInteger(cost) || cost < 1) {
1131
+ return Promise.reject(new RangeError("cost must be an integer of at least one message"));
1132
+ }
1133
+ if (signal?.aborted)
1134
+ return Promise.reject(signal.reason);
1135
+ const maxWaitMs = options?.maxWaitMs;
1136
+ if (maxWaitMs !== undefined && (!Number.isSafeInteger(maxWaitMs) || maxWaitMs < 1 || maxWaitMs > MAX_TIMER_MS)) {
1137
+ return Promise.reject(new RangeError(`maxWaitMs must be an integer from 1 to ${MAX_TIMER_MS}`));
1138
+ }
1139
+ const chat = options?.chatId === undefined ? undefined : String(options.chatId);
1140
+ return new Promise((resolve, reject) => {
1141
+ const waiter = {
1142
+ cost,
1143
+ lane: options?.lane ?? "interactive",
1144
+ chat,
1145
+ resolve,
1146
+ reject,
1147
+ ...signal && { signal }
1148
+ };
1149
+ if (signal) {
1150
+ const onAbort = () => {
1151
+ remove(waiter);
1152
+ detach(waiter);
1153
+ reject(signal.reason);
1154
+ };
1155
+ waiter.onAbort = onAbort;
1156
+ signal.addEventListener("abort", onAbort, { once: true });
1157
+ }
1158
+ if (maxWaitMs !== undefined)
1159
+ expireAfter(waiter, maxWaitMs, reject);
1160
+ queue.push(waiter);
1161
+ if (Math.max(now(), nextGrantAt, pauses.resumesAt(chat)) < wakingAt)
1162
+ wake();
1163
+ drain();
1164
+ });
1165
+ },
1166
+ wake,
1167
+ wait,
1168
+ counts: () => ({
1169
+ granted,
1170
+ messages,
1171
+ queued: queue.length,
1172
+ queuedBulk: queue.filter((waiter) => waiter.lane === "bulk").length
1173
+ })
1078
1174
  };
1079
- const pauseAfter = (error) => {
1175
+ }
1176
+
1177
+ // src/telegram/outbound-budget.ts
1178
+ var DEFAULT_MESSAGES_PER_SECOND = 24;
1179
+ var DEFAULT_RETRY_AFTER_FALLBACK_MS = 1000;
1180
+ var DEFAULT_TOKEN_PAUSE_CHATS = 3;
1181
+ var DEFAULT_TOKEN_PAUSE_WINDOW_MS = 5000;
1182
+ var TOKEN = z5.string().min(1, "token must not be empty");
1183
+ var MESSAGES_PER_SECOND = z5.number().positive().max(1000);
1184
+ var RETRY_AFTER_FALLBACK_MS = z5.number().int().nonnegative().max(MAX_TIMER_MS);
1185
+ var TOKEN_PAUSE_CHATS = z5.number().int().min(2).max(1000);
1186
+ var TOKEN_PAUSE_WINDOW_MS = z5.number().int().positive().max(MAX_TIMER_MS);
1187
+ function createBudget(policy, now, sleepFor) {
1188
+ const { rate, fallback } = policy;
1189
+ const pauses = createOutboundPauses(policy, now, () => queue.wake());
1190
+ const queue = createOutboundQueue(1000 / rate, now, sleepFor, pauses);
1191
+ const pauseAfter = (error, options) => {
1080
1192
  const failure = classifyTelegramSendFailure2(error);
1081
1193
  if (failure.reason !== "rate-limited")
1082
1194
  return;
1083
- pauseFor(failure.retryAfterSeconds === undefined ? fallback : failure.retryAfterSeconds * 1000);
1195
+ const milliseconds = failure.retryAfterSeconds === undefined ? fallback : failure.retryAfterSeconds * 1000;
1196
+ if (options?.chatId === undefined)
1197
+ pauses.pauseToken(milliseconds);
1198
+ else
1199
+ pauses.pauseChat(String(options.chatId), milliseconds);
1084
1200
  };
1085
- const waitUntilResumed = async (signal) => {
1201
+ const waitUntilResumed = async (signal, options) => {
1202
+ const chat = options?.chatId === undefined ? undefined : String(options.chatId);
1086
1203
  while (true) {
1087
1204
  if (signal?.aborted)
1088
1205
  throw signal.reason;
1089
- const remaining = pausedUntil - now();
1206
+ const remaining = pauses.resumesAt(chat) - now();
1090
1207
  if (remaining <= 0)
1091
1208
  return;
1092
- await wait(Math.min(remaining, MAX_TIMER_MS), signal);
1209
+ await queue.wait(Math.min(remaining, MAX_TIMER_MS), signal);
1093
1210
  }
1094
1211
  };
1095
1212
  const run = async (method, payload, signal, send, options) => {
1096
1213
  if (signal?.aborted)
1097
1214
  throw signal.reason;
1098
1215
  const cost = sendCost(method, payload);
1216
+ const chatId = options?.chatId ?? chatOf(payload);
1217
+ const call = chatId === undefined ? {} : { chatId };
1099
1218
  if (cost > 0)
1100
- await acquire(cost, signal, options);
1219
+ await queue.acquire(cost, signal, { ...options, ...call });
1101
1220
  if (signal?.aborted)
1102
1221
  throw signal.reason;
1103
1222
  const pauseOnRefusal = (refusal) => {
1104
- if (cost > 0)
1105
- pauseAfter(refusal);
1223
+ if (cost > 0 && options?.refusal !== "call")
1224
+ pauseAfter(refusal, call);
1106
1225
  };
1107
1226
  let result;
1108
1227
  try {
@@ -1115,22 +1234,19 @@ function createBudget(rate, fallback, now, sleepFor) {
1115
1234
  pauseOnRefusal(result);
1116
1235
  return result;
1117
1236
  };
1118
- const budget = {
1237
+ return {
1119
1238
  run,
1120
- acquire,
1121
- pause: pauseFor,
1239
+ acquire: queue.acquire,
1240
+ pause: pauses.pauseToken,
1122
1241
  pauseAfter,
1123
1242
  waitUntilResumed,
1124
1243
  stats: () => ({
1125
- granted,
1126
- messages,
1127
- queued: queue.length,
1128
- queuedBulk: queue.filter((waiter) => waiter.lane === "bulk").length,
1244
+ ...queue.counts(),
1129
1245
  messagesPerSecond: rate,
1130
- pausedForMs: Math.max(0, pausedUntil - now())
1246
+ pausedForMs: pauses.pausedForMs(),
1247
+ pausedChats: pauses.pausedChats()
1131
1248
  })
1132
1249
  };
1133
- return budget;
1134
1250
  }
1135
1251
  var budgets = new Map;
1136
1252
  function keyOf(token) {
@@ -1140,20 +1256,24 @@ function telegramOutboundBudget(config) {
1140
1256
  const token = TOKEN.parse(config.token);
1141
1257
  const rate = MESSAGES_PER_SECOND.parse(config.messagesPerSecond ?? DEFAULT_MESSAGES_PER_SECOND);
1142
1258
  const fallback = RETRY_AFTER_FALLBACK_MS.parse(config.retryAfterFallbackMs ?? DEFAULT_RETRY_AFTER_FALLBACK_MS);
1259
+ const tokenPauseChats = TOKEN_PAUSE_CHATS.parse(config.tokenPauseChats ?? DEFAULT_TOKEN_PAUSE_CHATS);
1260
+ const tokenPauseWindowMs = TOKEN_PAUSE_WINDOW_MS.parse(config.tokenPauseWindowMs ?? DEFAULT_TOKEN_PAUSE_WINDOW_MS);
1143
1261
  const key = keyOf(token);
1144
1262
  const existing = budgets.get(key);
1145
1263
  if (existing) {
1146
- const same = existing.rate === rate && existing.fallback === fallback && existing.nowOption === config.now && existing.sleepOption === config.sleep;
1264
+ const same = existing.rate === rate && existing.fallback === fallback && existing.tokenPauseChats === tokenPauseChats && existing.tokenPauseWindowMs === tokenPauseWindowMs && existing.nowOption === config.now && existing.sleepOption === config.sleep;
1147
1265
  if (!same) {
1148
1266
  throw new Error("a Telegram outbound budget already exists for this token with different options");
1149
1267
  }
1150
1268
  return existing.budget;
1151
1269
  }
1152
- const budget = createBudget(rate, fallback, config.now ?? (() => performance.now()), config.sleep ?? sleep);
1270
+ const budget = createBudget({ rate, fallback, tokenPauseChats, tokenPauseWindowMs }, config.now ?? (() => performance.now()), config.sleep ?? sleep);
1153
1271
  budgets.set(key, {
1154
1272
  budget,
1155
1273
  rate,
1156
1274
  fallback,
1275
+ tokenPauseChats,
1276
+ tokenPauseWindowMs,
1157
1277
  nowOption: config.now,
1158
1278
  sleepOption: config.sleep
1159
1279
  });
@@ -1502,6 +1622,72 @@ function createTelegramUpdateWorkSlots(maxConcurrent) {
1502
1622
  }
1503
1623
  };
1504
1624
  }
1625
+ function lineOf(update) {
1626
+ for (const [key, payload] of Object.entries(update)) {
1627
+ if (key === "update_id" || !isRecord(payload))
1628
+ continue;
1629
+ const message = isRecord(payload.message) ? payload.message : undefined;
1630
+ for (const holder of [
1631
+ payload.chat,
1632
+ message?.chat,
1633
+ payload.from,
1634
+ payload.user,
1635
+ payload.voter_chat
1636
+ ]) {
1637
+ if (isRecord(holder) && (typeof holder.id === "number" || typeof holder.id === "string")) {
1638
+ return `chat:${holder.id}`;
1639
+ }
1640
+ }
1641
+ }
1642
+ return `update:${String(update.update_id)}`;
1643
+ }
1644
+ function createTelegramUpdateLines(run) {
1645
+ const lines = new Map;
1646
+ const scheduled = new Set;
1647
+ return {
1648
+ has: (updateId) => scheduled.has(updateId),
1649
+ schedule(update) {
1650
+ const updateId = Number(update.update_id);
1651
+ if (scheduled.has(updateId))
1652
+ return false;
1653
+ scheduled.add(updateId);
1654
+ const line = lineOf(update);
1655
+ const next = (lines.get(line) ?? Promise.resolve()).then(() => run(update, updateId)).finally(() => {
1656
+ scheduled.delete(updateId);
1657
+ if (lines.get(line) === next)
1658
+ lines.delete(line);
1659
+ });
1660
+ lines.set(line, next);
1661
+ return true;
1662
+ },
1663
+ pending: () => [...lines.values()]
1664
+ };
1665
+ }
1666
+ async function readTelegramUpdatesDue(store, read) {
1667
+ const { skip, parse, reading, ...query } = read;
1668
+ const rows = [];
1669
+ let afterUpdateId;
1670
+ for (;; ) {
1671
+ const due = await store.due({
1672
+ ...query,
1673
+ ...afterUpdateId !== undefined && { afterUpdateId }
1674
+ });
1675
+ for (const row of due) {
1676
+ if (afterUpdateId !== undefined && row.updateId <= afterUpdateId) {
1677
+ throw new TypeError("[stitchkit] telegram update store: due returned a row at or before afterUpdateId");
1678
+ }
1679
+ if (skip(row.updateId))
1680
+ continue;
1681
+ const update = parse(row.body);
1682
+ if (update)
1683
+ rows.push(update);
1684
+ }
1685
+ afterUpdateId = due.at(-1)?.updateId;
1686
+ if (due.length === 0 || !reading())
1687
+ return rows;
1688
+ await new Promise((resolve) => setTimeout(resolve, 0));
1689
+ }
1690
+ }
1505
1691
  function createTelegramUpdateExhaustionScheduler(options) {
1506
1692
  const { config, store, now, slots, parse, reportStoreError } = options;
1507
1693
  const handler = config.handleExhaustion;
@@ -1709,25 +1895,6 @@ function defaultRetry(error, attempt) {
1709
1895
  const backoff = Math.min(5000 * 4 ** (attempt - 1), 15 * 60000);
1710
1896
  return Math.max(backoff, (failure.retryAfterSeconds ?? 0) * 1000);
1711
1897
  }
1712
- function lineOf(update) {
1713
- for (const [key, payload] of Object.entries(update)) {
1714
- if (key === "update_id" || !isRecord(payload))
1715
- continue;
1716
- const message = isRecord(payload.message) ? payload.message : undefined;
1717
- for (const holder of [
1718
- payload.chat,
1719
- message?.chat,
1720
- payload.from,
1721
- payload.user,
1722
- payload.voter_chat
1723
- ]) {
1724
- if (isRecord(holder) && (typeof holder.id === "number" || typeof holder.id === "string")) {
1725
- return `chat:${holder.id}`;
1726
- }
1727
- }
1728
- }
1729
- return `update:${String(update.update_id)}`;
1730
- }
1731
1898
  function parsed(body) {
1732
1899
  try {
1733
1900
  const value = JSON.parse(body);
@@ -1748,7 +1915,7 @@ function isTelegramUpdateFencedStore(store) {
1748
1915
  function isTelegramUpdateDurableStore(store) {
1749
1916
  return isTelegramUpdateFencedStore(store) && "exhaust" in store && typeof store.exhaust === "function" && "dueExhaustions" in store && typeof store.dueExhaustions === "function" && "acknowledgeExhaustion" in store && typeof store.acknowledgeExhaustion === "function";
1750
1917
  }
1751
- function createTelegramUpdateIntake(config) {
1918
+ function storeCapabilities(config) {
1752
1919
  const { store } = config;
1753
1920
  const exhaustionHandler = config.handleExhaustion;
1754
1921
  const attemptAware = "handleAttempt" in config;
@@ -1760,6 +1927,11 @@ function createTelegramUpdateIntake(config) {
1760
1927
  if (exhaustionHandler && !durable) {
1761
1928
  throw new TypeError("[stitchkit] telegram update intake: handleExhaustion requires a TelegramUpdateDurableStore");
1762
1929
  }
1930
+ return { fenced, durable };
1931
+ }
1932
+ function createTelegramUpdateIntake(config) {
1933
+ const { store } = config;
1934
+ const { fenced, durable } = storeCapabilities(config);
1763
1935
  const now = config.now ?? Date.now;
1764
1936
  const retry = config.retry ?? defaultRetry;
1765
1937
  const maxAttempts = positive("maxAttempts", config.maxAttempts ?? 5);
@@ -1768,13 +1940,12 @@ function createTelegramUpdateIntake(config) {
1768
1940
  const sweepEveryMs = positive("sweepEveryMs", config.sweepEveryMs ?? 30000);
1769
1941
  const retainMs = config.retainMs ?? 86400000;
1770
1942
  const maxConcurrent = positive("maxConcurrent", config.maxConcurrent ?? 32);
1771
- const lines = new Map;
1772
- const scheduled = new Set;
1773
1943
  let started = false;
1774
1944
  let starting;
1775
1945
  let lifecycleGeneration = 0;
1776
1946
  let timer;
1777
1947
  let exhaustionCursor;
1948
+ let arrivals;
1778
1949
  const storeError = (error, step) => {
1779
1950
  try {
1780
1951
  config.onStoreError?.(error, step);
@@ -1804,34 +1975,38 @@ function createTelegramUpdateIntake(config) {
1804
1975
  },
1805
1976
  ownershipLostReason: (identity) => new TelegramUpdateOwnershipLostError(identity)
1806
1977
  });
1807
- const schedule = (update) => {
1808
- const updateId = Number(update.update_id);
1809
- if (!started || scheduled.has(updateId))
1810
- return false;
1811
- scheduled.add(updateId);
1812
- const line = lineOf(update);
1813
- const next = (lines.get(line) ?? Promise.resolve()).then(async () => {
1814
- await slots.take();
1815
- try {
1978
+ const lines = createTelegramUpdateLines(async (update, updateId) => {
1979
+ await slots.take();
1980
+ try {
1981
+ if (started)
1816
1982
  await attempt(update, updateId);
1817
- } finally {
1818
- slots.release();
1819
- }
1820
- }).finally(() => {
1821
- scheduled.delete(updateId);
1822
- if (lines.get(line) === next)
1823
- lines.delete(line);
1824
- });
1825
- lines.set(line, next);
1826
- return true;
1827
- };
1828
- const sweep = async (pendingBefore) => {
1983
+ } catch (error) {
1984
+ storeError(error, "settle");
1985
+ } finally {
1986
+ slots.release();
1987
+ }
1988
+ });
1989
+ const schedule = (update) => started && lines.schedule(update);
1990
+ const sweep = async (pendingBefore, waiting = []) => {
1829
1991
  const at = now();
1830
- const due = await store.due({ now: at, pendingBefore, limit: SWEEP_BATCH });
1992
+ const generation = lifecycleGeneration;
1993
+ const rows = await readTelegramUpdatesDue(store, {
1994
+ now: at,
1995
+ pendingBefore,
1996
+ limit: SWEEP_BATCH,
1997
+ skip: lines.has,
1998
+ parse: parsed,
1999
+ reading: () => started
2000
+ });
2001
+ if (generation !== lifecycleGeneration)
2002
+ return 0;
2003
+ if (arrivals === waiting)
2004
+ arrivals = undefined;
2005
+ const waited = new Set(waiting);
2006
+ const queue = [...rows, ...waiting].sort((left, right) => Number(left.update_id) - Number(right.update_id));
1831
2007
  let count = 0;
1832
- for (const row of due) {
1833
- const update = parsed(row.body);
1834
- if (update && schedule(update))
2008
+ for (const update of queue) {
2009
+ if (schedule(update) && !waited.has(update))
1835
2010
  count += 1;
1836
2011
  }
1837
2012
  if (durable) {
@@ -1852,6 +2027,11 @@ function createTelegramUpdateIntake(config) {
1852
2027
  await store.prune(at - retainMs);
1853
2028
  return count;
1854
2029
  };
2030
+ const drained = async () => {
2031
+ while (lines.pending().length > 0 || exhaustionScheduler.pending().length > 0) {
2032
+ await Promise.all([...lines.pending(), ...exhaustionScheduler.pending()]);
2033
+ }
2034
+ };
1855
2035
  return {
1856
2036
  async accept(body) {
1857
2037
  const update = parsed(body);
@@ -1861,7 +2041,10 @@ function createTelegramUpdateIntake(config) {
1861
2041
  const added = await store.add({ updateId, body, receivedAt: now() });
1862
2042
  if (!added)
1863
2043
  return "duplicate";
1864
- schedule(update);
2044
+ if (arrivals)
2045
+ arrivals.push(update);
2046
+ else
2047
+ schedule(update);
1865
2048
  return "accepted";
1866
2049
  },
1867
2050
  start() {
@@ -1871,13 +2054,18 @@ function createTelegramUpdateIntake(config) {
1871
2054
  return Promise.resolve();
1872
2055
  started = true;
1873
2056
  const generation = ++lifecycleGeneration;
2057
+ const waiting = [];
2058
+ arrivals = waiting;
1874
2059
  const operation = (async () => {
1875
2060
  try {
1876
- await sweep(now());
2061
+ await sweep(now(), waiting);
1877
2062
  } catch (error) {
1878
2063
  if (started && lifecycleGeneration === generation)
1879
2064
  started = false;
1880
2065
  throw error;
2066
+ } finally {
2067
+ if (arrivals === waiting)
2068
+ arrivals = undefined;
1881
2069
  }
1882
2070
  if (!started || lifecycleGeneration !== generation)
1883
2071
  return;
@@ -1898,9 +2086,10 @@ function createTelegramUpdateIntake(config) {
1898
2086
  },
1899
2087
  sweep: () => sweep(now() - pendingGraceMs),
1900
2088
  async idle() {
1901
- while (lines.size > 0 || exhaustionScheduler.pending().length > 0) {
1902
- await Promise.all([...lines.values(), ...exhaustionScheduler.pending()]);
1903
- }
2089
+ await starting?.catch(() => {
2090
+ return;
2091
+ });
2092
+ await drained();
1904
2093
  },
1905
2094
  async close() {
1906
2095
  started = false;
@@ -1916,9 +2105,7 @@ function createTelegramUpdateIntake(config) {
1916
2105
  return;
1917
2106
  });
1918
2107
  }
1919
- while (lines.size > 0 || exhaustionScheduler.pending().length > 0) {
1920
- await Promise.all([...lines.values(), ...exhaustionScheduler.pending()]);
1921
- }
2108
+ await drained();
1922
2109
  }
1923
2110
  };
1924
2111
  }
@@ -2001,7 +2188,7 @@ function memoryTelegramUpdateStore() {
2001
2188
  settleCurrent(updateId, attempt, settlement);
2002
2189
  },
2003
2190
  async due(query) {
2004
- return [...records.values()].filter((record) => record.state === "pending" ? record.receivedAt <= query.pendingBefore : isClaimable(record, query.now)).sort((left, right) => left.updateId - right.updateId).slice(0, query.limit).map(({ updateId, body }) => ({ updateId, body }));
2191
+ return [...records.values()].filter((record) => record.state === "pending" ? record.receivedAt <= query.pendingBefore : isClaimable(record, query.now)).filter((record) => query.afterUpdateId === undefined || record.updateId > query.afterUpdateId).sort((left, right) => left.updateId - right.updateId).slice(0, query.limit).map(({ updateId, body }) => ({ updateId, body }));
2005
2192
  },
2006
2193
  async owns(identity, at) {
2007
2194
  const record = holding(identity.updateId, identity.attempt, identity.claimId);
@@ -2134,6 +2321,27 @@ var RULES = {
2134
2321
  if (!same(await store.due({ now: 100, pendingBefore: 0, limit: 10 }), []))
2135
2322
  violated("an update past maxAttempts was not abandoned");
2136
2323
  },
2324
+ "due continues strictly after its cursor": async (store, violated) => {
2325
+ for (const updateId of [1, 2, 3]) {
2326
+ await store.add({ updateId, body: "b", receivedAt: 0 });
2327
+ }
2328
+ await store.claim(2, { ...claim, now: 0, leaseUntil: 10 });
2329
+ await store.settle(2, 1, { state: "failed", at: 1, retryAt: 5, error: "x" });
2330
+ const page = async (afterUpdateId) => (await store.due({
2331
+ now: 5,
2332
+ pendingBefore: 0,
2333
+ limit: 2,
2334
+ ...afterUpdateId !== undefined && { afterUpdateId }
2335
+ })).map(({ updateId }) => updateId);
2336
+ if (!same(await page(), [1, 2]))
2337
+ violated("due did not begin at the first row");
2338
+ if (!same(await page(1), [2, 3]))
2339
+ violated("due lost a retry after its cursor");
2340
+ if (!same(await page(2), [3]))
2341
+ violated("due did not continue after its cursor");
2342
+ if (!same(await page(3), []))
2343
+ violated("due listed a row at or before its cursor");
2344
+ },
2137
2345
  "settled updates are forgotten after they are pruned": async (store, violated) => {
2138
2346
  await store.add({ updateId: 3, body: "b", receivedAt: 0 });
2139
2347
  await store.claim(3, { ...claim, now: 0, leaseUntil: 100 });
@@ -2512,9 +2720,15 @@ function postgresTelegramUpdateStore(config) {
2512
2720
  },
2513
2721
  async due(options) {
2514
2722
  const rows = await run(`SELECT update_id, body FROM ${table}
2515
- WHERE (state = 'pending' AND received_at <= $2::bigint)
2516
- OR (state <> 'pending' AND ${CLAIMABLE})
2517
- ORDER BY update_id LIMIT $3::integer`, [options.now, options.pendingBefore, options.limit]);
2723
+ WHERE ((state = 'pending' AND received_at <= $2::bigint)
2724
+ OR (state <> 'pending' AND ${CLAIMABLE}))
2725
+ AND update_id > $4::bigint
2726
+ ORDER BY update_id LIMIT $3::integer`, [
2727
+ options.now,
2728
+ options.pendingBefore,
2729
+ options.limit,
2730
+ options.afterUpdateId ?? Number.MIN_SAFE_INTEGER
2731
+ ]);
2518
2732
  return rows.flatMap((row) => {
2519
2733
  const updateId = integer(field(row, "update_id"));
2520
2734
  const body = field(row, "body");
@@ -2623,7 +2837,8 @@ function prepareTelegramSqliteUpdateStatements(database, table) {
2623
2837
  WHERE update_id = ?5 AND attempts = ?6 AND state = 'processing' AND due_at >= ?7
2624
2838
  AND (?8 IS NULL OR claim_id = ?8)`),
2625
2839
  due: database.prepare(`SELECT update_id, body FROM ${table}
2626
- WHERE (state = 'pending' AND received_at <= ?2) OR (state <> 'pending' AND ${CLAIMABLE2})
2840
+ WHERE ((state = 'pending' AND received_at <= ?2) OR (state <> 'pending' AND ${CLAIMABLE2}))
2841
+ AND update_id > ?4
2627
2842
  ORDER BY update_id LIMIT ?3`),
2628
2843
  prune: database.prepare(`DELETE FROM ${table} WHERE state IN ('completed', 'abandoned') AND settled_at < ?1`),
2629
2844
  owns: database.prepare(`SELECT update_id FROM ${table}
@@ -2707,7 +2922,7 @@ function sqliteTelegramUpdateStore(config) {
2707
2922
  settleCurrent(updateId, attempt, settlement);
2708
2923
  },
2709
2924
  async due(query) {
2710
- return due.all(query.now, query.pendingBefore, query.limit).flatMap((row) => rowOf(row) ?? []);
2925
+ return due.all(query.now, query.pendingBefore, query.limit, query.afterUpdateId ?? Number.MIN_SAFE_INTEGER).flatMap((row) => rowOf(row) ?? []);
2711
2926
  },
2712
2927
  async owns(identity, at) {
2713
2928
  return owns.all(identity.updateId, identity.attempt, at, identity.claimId).length > 0;