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.
- package/CHANGELOG.md +56 -0
- package/dist/agent-runtime/run-acquisition.d.ts +9 -2
- package/dist/agent-runtime/run-acquisition.d.ts.map +1 -1
- package/dist/agent-runtime/run-execution.d.ts +3 -0
- package/dist/agent-runtime/run-execution.d.ts.map +1 -1
- package/dist/agent-runtime/run-failure.d.ts +4 -1
- package/dist/agent-runtime/run-failure.d.ts.map +1 -1
- package/dist/agent-runtime/run-store-retry.d.ts +8 -0
- package/dist/agent-runtime/run-store-retry.d.ts.map +1 -0
- package/dist/agent-runtime/run-terminal.d.ts.map +1 -1
- package/dist/agent-runtime/run-unacquired.d.ts +19 -0
- package/dist/agent-runtime/run-unacquired.d.ts.map +1 -0
- package/dist/agent-runtime/store-conformance.d.ts +8 -0
- package/dist/agent-runtime/store-conformance.d.ts.map +1 -1
- package/dist/agent-runtime/store-driver-contract.d.ts +5 -0
- package/dist/agent-runtime/store-driver-contract.d.ts.map +1 -1
- package/dist/agent-runtime/terminal-commit.d.ts +34 -4
- package/dist/agent-runtime/terminal-commit.d.ts.map +1 -1
- package/dist/entrypoints/agent-runtime/harness.js +2 -2
- package/dist/entrypoints/agent-runtime/sqlite/bun.js +3 -3
- package/dist/entrypoints/agent-runtime/sqlite/node.js +3 -3
- package/dist/entrypoints/agent-runtime/testing.js +22 -4
- package/dist/entrypoints/agent-runtime.d.ts +1 -1
- package/dist/entrypoints/agent-runtime.d.ts.map +1 -1
- package/dist/entrypoints/agent-runtime.js +13 -11
- package/dist/entrypoints/application/diagnostic-journal.js +1 -1
- package/dist/entrypoints/application/directory-inbox.js +3 -3
- package/dist/entrypoints/cli/publish.js +2 -2
- package/dist/entrypoints/files.js +2 -2
- package/dist/entrypoints/primitives.js +51 -154
- package/dist/entrypoints/server.js +6 -6
- package/dist/entrypoints/telegram.d.ts +1 -1
- package/dist/entrypoints/telegram.d.ts.map +1 -1
- package/dist/entrypoints/telegram.js +352 -137
- package/dist/entrypoints/tracking/contract.d.ts +11 -0
- package/dist/entrypoints/tracking/contract.d.ts.map +1 -0
- package/dist/entrypoints/tracking/contract.js +162 -0
- package/dist/entrypoints/tracking/performance.d.ts +8 -0
- package/dist/entrypoints/tracking/performance.d.ts.map +1 -0
- package/dist/entrypoints/tracking/performance.js +181 -0
- package/dist/entrypoints/tracking/server.d.ts +1 -0
- package/dist/entrypoints/tracking/server.d.ts.map +1 -1
- package/dist/entrypoints/tracking/server.js +40 -12
- package/dist/entrypoints/tracking.d.ts +4 -3
- package/dist/entrypoints/tracking.d.ts.map +1 -1
- package/dist/entrypoints/tracking.js +43 -143
- package/dist/entrypoints/xlsx.d.ts +4 -0
- package/dist/entrypoints/xlsx.d.ts.map +1 -0
- package/dist/entrypoints/xlsx.js +406 -0
- package/dist/{index-fa435yq2.js → index-073nw9qr.js} +8 -8
- package/dist/index-1f40z584.js +7 -0
- package/dist/{index-fdk4r4r6.js → index-75ns2ybj.js} +1 -1
- package/dist/{index-jq2ky8fw.js → index-cycp3r3e.js} +124 -181
- package/dist/{index-4g6qmgdx.js → index-ghtpfz4z.js} +9 -9
- package/dist/{index-n3v695sb.js → index-hdjmdwsj.js} +277 -96
- package/dist/{index-zqkz4af9.js → index-nqxrm176.js} +1 -1
- package/dist/index-qezhmp51.js +242 -0
- package/dist/index-vv1sch8a.js +94 -0
- package/dist/{index-fggxgbrk.js → index-xx1j310k.js} +165 -102
- package/dist/internal/exclusive-lock.d.ts.map +1 -1
- package/dist/primitives/decimal.d.ts +10 -0
- package/dist/primitives/decimal.d.ts.map +1 -1
- package/dist/primitives/index.d.ts +3 -1
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/money.d.ts +20 -0
- package/dist/primitives/money.d.ts.map +1 -1
- package/dist/primitives/quantity.d.ts.map +1 -1
- package/dist/primitives/rounding.d.ts +40 -0
- package/dist/primitives/rounding.d.ts.map +1 -0
- package/dist/telegram/budget-transformer.d.ts +2 -2
- package/dist/telegram/not-dispatched.d.ts +2 -2
- package/dist/telegram/not-dispatched.d.ts.map +1 -1
- package/dist/telegram/outbound-budget.d.ts +53 -16
- package/dist/telegram/outbound-budget.d.ts.map +1 -1
- package/dist/telegram/outbound-cost.d.ts +5 -0
- package/dist/telegram/outbound-cost.d.ts.map +1 -0
- package/dist/telegram/outbound-pauses.d.ts +17 -0
- package/dist/telegram/outbound-pauses.d.ts.map +1 -0
- package/dist/telegram/outbound-queue.d.ts +17 -0
- package/dist/telegram/outbound-queue.d.ts.map +1 -0
- package/dist/telegram/update-intake-work.d.ts +20 -1
- package/dist/telegram/update-intake-work.d.ts.map +1 -1
- package/dist/telegram/update-intake.d.ts +12 -3
- package/dist/telegram/update-intake.d.ts.map +1 -1
- package/dist/telegram/update-store-check.d.ts +1 -1
- package/dist/telegram/update-store-check.d.ts.map +1 -1
- package/dist/telegram/update-store-postgres.d.ts.map +1 -1
- package/dist/telegram/update-store-sqlite.d.ts.map +1 -1
- package/dist/telegram/update-store.d.ts +5 -1
- package/dist/telegram/update-store.d.ts.map +1 -1
- package/dist/tracking/attribution.d.ts +1 -1
- package/dist/tracking/attribution.d.ts.map +1 -1
- package/dist/tracking/client-visit.d.ts +3 -1
- package/dist/tracking/client-visit.d.ts.map +1 -1
- package/dist/tracking/client.d.ts.map +1 -1
- package/dist/tracking/performance/collector.d.ts +61 -0
- package/dist/tracking/performance/collector.d.ts.map +1 -0
- package/dist/tracking/performance/schema.d.ts +45 -0
- package/dist/tracking/performance/schema.d.ts.map +1 -0
- package/dist/tracking/performance/summary.d.ts +12 -0
- package/dist/tracking/performance/summary.d.ts.map +1 -0
- package/dist/tracking/privacy.d.ts +5 -0
- package/dist/tracking/privacy.d.ts.map +1 -0
- package/dist/tracking/server/telemetry-health.d.ts +31 -0
- package/dist/tracking/server/telemetry-health.d.ts.map +1 -0
- package/dist/xlsx/cells.d.ts +17 -0
- package/dist/xlsx/cells.d.ts.map +1 -0
- package/dist/xlsx/render.d.ts +11 -0
- package/dist/xlsx/render.d.ts.map +1 -0
- package/dist/xlsx/schema.d.ts +119 -0
- package/dist/xlsx/schema.d.ts.map +1 -0
- package/dist/xlsx/zip.d.ts +13 -0
- package/dist/xlsx/zip.d.ts.map +1 -0
- package/llms/agent-runtime.2.txt +40 -16
- package/llms/agent-runtime.3.txt +17 -0
- package/llms/agent-runtime.txt +4 -1
- package/llms/primitives.txt +21 -0
- package/llms/stitchkit.txt +9 -6
- package/llms/telegram.txt +46 -15
- package/llms/tracking-contract.txt +32 -0
- package/llms/tracking-performance.txt +21 -0
- package/llms/tracking-server.txt +1 -0
- package/llms/tracking.txt +75 -12
- package/llms/{upgrading-0.105.0-0.116.0.txt → upgrading-0.105.0-0.118.0.txt} +73 -3
- package/llms/upgrading.txt +1 -1
- package/llms/xlsx.txt +107 -0
- package/llms-full.txt +392 -34
- package/llms.txt +11 -7
- package/package.json +27 -9
- 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-
|
|
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
|
-
|
|
912
|
-
|
|
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
|
|
963
|
-
|
|
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
|
|
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
|
|
982
|
-
const current =
|
|
983
|
-
|
|
1032
|
+
const wake = () => {
|
|
1033
|
+
const current = waking;
|
|
1034
|
+
waking = undefined;
|
|
984
1035
|
current?.abort();
|
|
985
1036
|
};
|
|
986
|
-
const nextWaiter = () =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1027
|
-
|
|
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
|
-
|
|
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
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1237
|
+
return {
|
|
1119
1238
|
run,
|
|
1120
|
-
acquire,
|
|
1121
|
-
pause:
|
|
1239
|
+
acquire: queue.acquire,
|
|
1240
|
+
pause: pauses.pauseToken,
|
|
1122
1241
|
pauseAfter,
|
|
1123
1242
|
waitUntilResumed,
|
|
1124
1243
|
stats: () => ({
|
|
1125
|
-
|
|
1126
|
-
messages,
|
|
1127
|
-
queued: queue.length,
|
|
1128
|
-
queuedBulk: queue.filter((waiter) => waiter.lane === "bulk").length,
|
|
1244
|
+
...queue.counts(),
|
|
1129
1245
|
messagesPerSecond: rate,
|
|
1130
|
-
pausedForMs:
|
|
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
|
|
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
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
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
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
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
|
|
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
|
|
1833
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1902
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|