opencode-gateway 0.2.3 → 0.2.4
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/dist/cli.js +0 -0
- package/dist/index.js +20907 -52
- package/package.json +1 -1
- package/dist/binding/execution.js +0 -1
- package/dist/binding/gateway.js +0 -1
- package/dist/binding/index.js +0 -4
- package/dist/binding/opencode.js +0 -1
- package/dist/cli/args.js +0 -53
- package/dist/cli/doctor.js +0 -49
- package/dist/cli/init.js +0 -40
- package/dist/cli/opencode-config-file.js +0 -18
- package/dist/cli/opencode-config.js +0 -194
- package/dist/cli/paths.js +0 -22
- package/dist/cli/templates.js +0 -41
- package/dist/config/cron.js +0 -52
- package/dist/config/gateway.js +0 -148
- package/dist/config/memory.js +0 -105
- package/dist/config/paths.js +0 -39
- package/dist/config/telegram.js +0 -91
- package/dist/cron/runtime.js +0 -402
- package/dist/delivery/telegram.js +0 -75
- package/dist/delivery/text.js +0 -175
- package/dist/gateway.js +0 -117
- package/dist/host/file-sender.js +0 -59
- package/dist/host/logger.js +0 -53
- package/dist/host/transport.js +0 -35
- package/dist/mailbox/router.js +0 -16
- package/dist/media/mime.js +0 -45
- package/dist/memory/prompt.js +0 -122
- package/dist/opencode/adapter.js +0 -340
- package/dist/opencode/driver-hub.js +0 -82
- package/dist/opencode/event-normalize.js +0 -48
- package/dist/opencode/event-stream.js +0 -65
- package/dist/opencode/events.js +0 -1
- package/dist/questions/client.js +0 -36
- package/dist/questions/format.js +0 -36
- package/dist/questions/normalize.js +0 -45
- package/dist/questions/parser.js +0 -96
- package/dist/questions/runtime.js +0 -195
- package/dist/questions/types.js +0 -1
- package/dist/runtime/attachments.js +0 -12
- package/dist/runtime/conversation-coordinator.js +0 -22
- package/dist/runtime/executor.js +0 -407
- package/dist/runtime/mailbox.js +0 -112
- package/dist/runtime/opencode-runner.js +0 -79
- package/dist/runtime/runtime-singleton.js +0 -28
- package/dist/session/context.js +0 -23
- package/dist/session/conversation-key.js +0 -3
- package/dist/session/switcher.js +0 -59
- package/dist/session/system-prompt.js +0 -52
- package/dist/store/migrations.js +0 -197
- package/dist/store/sqlite.js +0 -777
- package/dist/telegram/client.js +0 -180
- package/dist/telegram/media.js +0 -65
- package/dist/telegram/normalize.js +0 -119
- package/dist/telegram/poller.js +0 -166
- package/dist/telegram/runtime.js +0 -157
- package/dist/telegram/state.js +0 -149
- package/dist/telegram/types.js +0 -1
- package/dist/tools/channel-new-session.js +0 -27
- package/dist/tools/channel-send-file.js +0 -27
- package/dist/tools/channel-target.js +0 -34
- package/dist/tools/cron-run.js +0 -20
- package/dist/tools/cron-upsert.js +0 -51
- package/dist/tools/gateway-dispatch-cron.js +0 -33
- package/dist/tools/gateway-status.js +0 -25
- package/dist/tools/schedule-cancel.js +0 -12
- package/dist/tools/schedule-format.js +0 -48
- package/dist/tools/schedule-list.js +0 -17
- package/dist/tools/schedule-once.js +0 -43
- package/dist/tools/schedule-status.js +0 -23
- package/dist/tools/telegram-send-test.js +0 -26
- package/dist/tools/telegram-status.js +0 -49
- package/dist/tools/time.js +0 -25
- package/dist/utils/error.js +0 -57
package/dist/questions/format.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export function formatPlainTextQuestion(request) {
|
|
2
|
-
return [
|
|
3
|
-
"OpenCode needs additional input before it can continue.",
|
|
4
|
-
"",
|
|
5
|
-
...request.questions.flatMap((question, index) => formatQuestionBlock(question, index)),
|
|
6
|
-
formatReplyInstructions(request.questions),
|
|
7
|
-
].join("\n");
|
|
8
|
-
}
|
|
9
|
-
export function formatQuestionReplyError(request, message) {
|
|
10
|
-
return [message, "", formatReplyInstructions(request.questions)].join("\n");
|
|
11
|
-
}
|
|
12
|
-
function formatQuestionBlock(question, index) {
|
|
13
|
-
const label = `Question ${index + 1}: ${question.header}`;
|
|
14
|
-
const options = question.options.length === 0
|
|
15
|
-
? []
|
|
16
|
-
: [
|
|
17
|
-
"Options:",
|
|
18
|
-
...question.options.map((option, optionIndex) => `${optionIndex + 1}. ${option.label} - ${option.description}`),
|
|
19
|
-
];
|
|
20
|
-
return [label, question.question, ...options, ""];
|
|
21
|
-
}
|
|
22
|
-
function formatReplyInstructions(questions) {
|
|
23
|
-
if (questions.length === 1) {
|
|
24
|
-
const question = questions[0];
|
|
25
|
-
const selectionHint = question.multiple
|
|
26
|
-
? "Reply with one line. You may send option numbers or labels separated by commas."
|
|
27
|
-
: "Reply with one line. You may send an option number, an option label, or custom text.";
|
|
28
|
-
return ["How to reply:", `- ${selectionHint}`, "- Reply /cancel to reject this question."].join("\n");
|
|
29
|
-
}
|
|
30
|
-
return [
|
|
31
|
-
"How to reply:",
|
|
32
|
-
"- Reply with one non-empty line per question, in order.",
|
|
33
|
-
"- Each line may use option numbers, option labels, or custom text when allowed.",
|
|
34
|
-
"- Reply /cancel to reject this question.",
|
|
35
|
-
].join("\n");
|
|
36
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export function normalizeQuestionEvent(event) {
|
|
2
|
-
if (isQuestionAskedEvent(event)) {
|
|
3
|
-
return {
|
|
4
|
-
kind: "asked",
|
|
5
|
-
request: {
|
|
6
|
-
requestId: event.properties.id,
|
|
7
|
-
sessionId: event.properties.sessionID,
|
|
8
|
-
questions: event.properties.questions.map((question) => ({
|
|
9
|
-
header: question.header,
|
|
10
|
-
question: question.question,
|
|
11
|
-
options: question.options.map((option) => ({
|
|
12
|
-
label: option.label,
|
|
13
|
-
description: option.description,
|
|
14
|
-
})),
|
|
15
|
-
multiple: question.multiple === true,
|
|
16
|
-
custom: question.custom !== false,
|
|
17
|
-
})),
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
if (isQuestionResolvedEvent(event)) {
|
|
22
|
-
return {
|
|
23
|
-
kind: "resolved",
|
|
24
|
-
requestId: event.properties.requestID,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
function isQuestionAskedEvent(event) {
|
|
30
|
-
if (event.type !== "question.asked" || typeof event.properties !== "object" || event.properties === null) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
const properties = event.properties;
|
|
34
|
-
return (typeof properties.id === "string" &&
|
|
35
|
-
typeof properties.sessionID === "string" &&
|
|
36
|
-
Array.isArray(properties.questions));
|
|
37
|
-
}
|
|
38
|
-
function isQuestionResolvedEvent(event) {
|
|
39
|
-
if ((event.type !== "question.replied" && event.type !== "question.rejected") ||
|
|
40
|
-
typeof event.properties !== "object" ||
|
|
41
|
-
event.properties === null) {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
return typeof event.properties.requestID === "string";
|
|
45
|
-
}
|
package/dist/questions/parser.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
const CANCEL_WORDS = new Set(["/cancel", "cancel", "/reject", "reject"]);
|
|
2
|
-
export function parseQuestionReply(request, text) {
|
|
3
|
-
if (text === null) {
|
|
4
|
-
return {
|
|
5
|
-
kind: "invalid",
|
|
6
|
-
message: "This question currently accepts text replies only.",
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
const trimmed = text.trim();
|
|
10
|
-
if (trimmed.length === 0) {
|
|
11
|
-
return {
|
|
12
|
-
kind: "invalid",
|
|
13
|
-
message: "Reply text must not be empty.",
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
if (CANCEL_WORDS.has(trimmed.toLowerCase())) {
|
|
17
|
-
return {
|
|
18
|
-
kind: "reject",
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
if (request.questions.length === 1) {
|
|
22
|
-
const parsedAnswer = parseQuestionLine(request.questions[0], trimmed);
|
|
23
|
-
return parsedAnswer.kind === "invalid"
|
|
24
|
-
? parsedAnswer
|
|
25
|
-
: {
|
|
26
|
-
kind: "reply",
|
|
27
|
-
answers: [parsedAnswer.answer],
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
const lines = trimmed
|
|
31
|
-
.split(/\r?\n/u)
|
|
32
|
-
.map((line) => line.trim())
|
|
33
|
-
.filter((line) => line.length > 0);
|
|
34
|
-
if (lines.length !== request.questions.length) {
|
|
35
|
-
return {
|
|
36
|
-
kind: "invalid",
|
|
37
|
-
message: `Expected ${request.questions.length} non-empty lines, but received ${lines.length}.`,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
const answers = [];
|
|
41
|
-
for (const [index, question] of request.questions.entries()) {
|
|
42
|
-
const parsedAnswer = parseQuestionLine(question, lines[index]);
|
|
43
|
-
if (parsedAnswer.kind === "invalid") {
|
|
44
|
-
return parsedAnswer;
|
|
45
|
-
}
|
|
46
|
-
answers.push(parsedAnswer.answer);
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
kind: "reply",
|
|
50
|
-
answers,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function parseQuestionLine(question, line) {
|
|
54
|
-
const rawSelections = question.multiple
|
|
55
|
-
? line
|
|
56
|
-
.split(/[,\n]/u)
|
|
57
|
-
.map((token) => token.trim())
|
|
58
|
-
.filter((token) => token.length > 0)
|
|
59
|
-
: [line];
|
|
60
|
-
if (rawSelections.length === 0) {
|
|
61
|
-
return {
|
|
62
|
-
kind: "invalid",
|
|
63
|
-
message: "At least one answer is required.",
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
const answers = [];
|
|
67
|
-
for (const rawSelection of rawSelections) {
|
|
68
|
-
const option = resolveOptionSelection(question, rawSelection);
|
|
69
|
-
if (option !== null) {
|
|
70
|
-
answers.push(option);
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (!question.custom) {
|
|
74
|
-
return {
|
|
75
|
-
kind: "invalid",
|
|
76
|
-
message: `Answer "${rawSelection}" does not match any allowed option.`,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
answers.push(rawSelection);
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
kind: "answer",
|
|
83
|
-
answer: answers,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function resolveOptionSelection(question, selection) {
|
|
87
|
-
if (question.options.length === 0) {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
const numericIndex = Number.parseInt(selection, 10);
|
|
91
|
-
if (Number.isSafeInteger(numericIndex) && String(numericIndex) === selection && numericIndex >= 1) {
|
|
92
|
-
return question.options[numericIndex - 1]?.label ?? null;
|
|
93
|
-
}
|
|
94
|
-
const normalized = selection.toLowerCase();
|
|
95
|
-
return question.options.find((option) => option.label.toLowerCase() === normalized)?.label ?? null;
|
|
96
|
-
}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { recordTelegramSendFailure, recordTelegramSendSuccess } from "../telegram/state";
|
|
2
|
-
import { formatError } from "../utils/error";
|
|
3
|
-
import { formatPlainTextQuestion, formatQuestionReplyError } from "./format";
|
|
4
|
-
import { normalizeQuestionEvent } from "./normalize";
|
|
5
|
-
import { parseQuestionReply } from "./parser";
|
|
6
|
-
export class GatewayQuestionRuntime {
|
|
7
|
-
client;
|
|
8
|
-
directory;
|
|
9
|
-
store;
|
|
10
|
-
sessions;
|
|
11
|
-
transport;
|
|
12
|
-
telegramClient;
|
|
13
|
-
logger;
|
|
14
|
-
constructor(client, directory, store, sessions, transport, telegramClient, logger) {
|
|
15
|
-
this.client = client;
|
|
16
|
-
this.directory = directory;
|
|
17
|
-
this.store = store;
|
|
18
|
-
this.sessions = sessions;
|
|
19
|
-
this.transport = transport;
|
|
20
|
-
this.telegramClient = telegramClient;
|
|
21
|
-
this.logger = logger;
|
|
22
|
-
}
|
|
23
|
-
handleEvent(event) {
|
|
24
|
-
const normalized = normalizeQuestionEvent(event);
|
|
25
|
-
if (normalized === null) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
void this.processEvent(normalized).catch((error) => {
|
|
29
|
-
this.logger.log("warn", `question bridge failed: ${formatError(error)}`);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
async tryHandleInboundMessage(message) {
|
|
33
|
-
const pending = this.store.getPendingQuestionForTarget(message.deliveryTarget);
|
|
34
|
-
if (pending === null) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
const parsed = parseQuestionReply(pending, message.text);
|
|
38
|
-
switch (parsed.kind) {
|
|
39
|
-
case "invalid":
|
|
40
|
-
await this.sendPlainText(pending.deliveryTarget, formatQuestionReplyError(pending, parsed.message));
|
|
41
|
-
return true;
|
|
42
|
-
case "reject":
|
|
43
|
-
await this.rejectQuestion(pending.requestId);
|
|
44
|
-
this.store.deletePendingQuestion(pending.requestId);
|
|
45
|
-
return true;
|
|
46
|
-
case "reply":
|
|
47
|
-
await this.replyQuestion(pending.requestId, parsed.answers);
|
|
48
|
-
this.store.deletePendingQuestion(pending.requestId);
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
async handleTelegramCallbackQuery(query) {
|
|
53
|
-
if (this.telegramClient === null) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
const pending = this.store.getPendingQuestionForTelegramMessage(query.deliveryTarget, query.messageId);
|
|
57
|
-
if (pending === null) {
|
|
58
|
-
await this.telegramClient.answerCallbackQuery(query.callbackQueryId, "This question is no longer pending.");
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
const answer = resolveCallbackAnswer(query.data, pending);
|
|
62
|
-
if (answer === null) {
|
|
63
|
-
await this.telegramClient.answerCallbackQuery(query.callbackQueryId, "This button is no longer valid.");
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
await this.replyQuestion(pending.requestId, [[answer]]);
|
|
67
|
-
this.store.deletePendingQuestion(pending.requestId);
|
|
68
|
-
await this.telegramClient.answerCallbackQuery(query.callbackQueryId, `Sent: ${answer}`);
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
async processEvent(event) {
|
|
72
|
-
switch (event.kind) {
|
|
73
|
-
case "asked":
|
|
74
|
-
await this.handleQuestionAsked(event.request);
|
|
75
|
-
return;
|
|
76
|
-
case "resolved":
|
|
77
|
-
this.store.deletePendingQuestion(event.requestId);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
async handleQuestionAsked(request) {
|
|
82
|
-
const targets = this.sessions.listReplyTargets(request.sessionId);
|
|
83
|
-
if (targets.length === 0) {
|
|
84
|
-
this.logger.log("warn", `question ${request.requestId} has no reply target for session ${request.sessionId}`);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const deliveredTargets = [];
|
|
88
|
-
for (const target of targets) {
|
|
89
|
-
try {
|
|
90
|
-
deliveredTargets.push(await this.sendQuestion(target, request));
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
this.logger.log("warn", `question ${request.requestId} delivery failed for ${target.channel}:${target.target}: ${formatError(error)}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (deliveredTargets.length === 0) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
this.store.replacePendingQuestion({
|
|
100
|
-
requestId: request.requestId,
|
|
101
|
-
sessionId: request.sessionId,
|
|
102
|
-
questions: request.questions,
|
|
103
|
-
targets: deliveredTargets,
|
|
104
|
-
recordedAtMs: Date.now(),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
async sendQuestion(target, request) {
|
|
108
|
-
const nativeKeyboard = buildTelegramInlineKeyboard(request);
|
|
109
|
-
if (target.channel === "telegram" && nativeKeyboard !== null && this.telegramClient !== null) {
|
|
110
|
-
try {
|
|
111
|
-
const sent = await this.telegramClient.sendInteractiveMessage(target.target, formatTelegramNativeQuestion(request), target.topic, nativeKeyboard);
|
|
112
|
-
recordTelegramSendSuccess(this.store, Date.now());
|
|
113
|
-
return {
|
|
114
|
-
deliveryTarget: target,
|
|
115
|
-
telegramMessageId: sent.message_id,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
recordTelegramSendFailure(this.store, formatError(error), Date.now());
|
|
120
|
-
throw error;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
await this.sendPlainText(target, formatPlainTextQuestion(request));
|
|
124
|
-
return {
|
|
125
|
-
deliveryTarget: target,
|
|
126
|
-
telegramMessageId: null,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
async sendPlainText(target, body) {
|
|
130
|
-
const ack = await this.transport.sendMessage({
|
|
131
|
-
deliveryTarget: target,
|
|
132
|
-
body,
|
|
133
|
-
});
|
|
134
|
-
if (ack.errorMessage !== null) {
|
|
135
|
-
throw new Error(ack.errorMessage);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
async replyQuestion(requestId, answers) {
|
|
139
|
-
await this.client.question.reply({
|
|
140
|
-
requestID: requestId,
|
|
141
|
-
directory: this.directory,
|
|
142
|
-
answers,
|
|
143
|
-
}, {
|
|
144
|
-
responseStyle: "data",
|
|
145
|
-
throwOnError: true,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
async rejectQuestion(requestId) {
|
|
149
|
-
await this.client.question.reject({
|
|
150
|
-
requestID: requestId,
|
|
151
|
-
directory: this.directory,
|
|
152
|
-
}, {
|
|
153
|
-
responseStyle: "data",
|
|
154
|
-
throwOnError: true,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
function buildTelegramInlineKeyboard(request) {
|
|
159
|
-
if (request.questions.length !== 1) {
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
const [question] = request.questions;
|
|
163
|
-
if (question.multiple || question.options.length === 0) {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
return {
|
|
167
|
-
inline_keyboard: question.options.map((option, index) => [
|
|
168
|
-
{
|
|
169
|
-
text: option.label,
|
|
170
|
-
callback_data: `q:${index}`,
|
|
171
|
-
},
|
|
172
|
-
]),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
function formatTelegramNativeQuestion(request) {
|
|
176
|
-
const [question] = request.questions;
|
|
177
|
-
return [
|
|
178
|
-
"OpenCode needs additional input before it can continue.",
|
|
179
|
-
"",
|
|
180
|
-
`${question.header}: ${question.question}`,
|
|
181
|
-
"",
|
|
182
|
-
"Tap a button below or reply with text.",
|
|
183
|
-
].join("\n");
|
|
184
|
-
}
|
|
185
|
-
function resolveCallbackAnswer(data, pending) {
|
|
186
|
-
if (data === null || !data.startsWith("q:") || pending.questions.length !== 1) {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
const indexText = data.slice(2);
|
|
190
|
-
const index = Number.parseInt(indexText, 10);
|
|
191
|
-
if (!Number.isSafeInteger(index) || index < 0) {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
return pending.questions[0]?.options[index]?.label ?? null;
|
|
195
|
-
}
|
package/dist/questions/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { rm } from "node:fs/promises";
|
|
2
|
-
export async function deleteInboundAttachmentFiles(entries, logger) {
|
|
3
|
-
const paths = new Set(entries.flatMap((entry) => entry.attachments.map((attachment) => attachment.localPath)));
|
|
4
|
-
await Promise.all([...paths].map(async (path) => {
|
|
5
|
-
try {
|
|
6
|
-
await rm(path, { force: true });
|
|
7
|
-
}
|
|
8
|
-
catch (error) {
|
|
9
|
-
logger.log("warn", `failed to remove cached inbound attachment ${path}: ${String(error)}`);
|
|
10
|
-
}
|
|
11
|
-
}));
|
|
12
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export class ConversationCoordinator {
|
|
2
|
-
tails = new Map();
|
|
3
|
-
async runExclusive(conversationKey, operation) {
|
|
4
|
-
const previous = this.tails.get(conversationKey) ?? Promise.resolve();
|
|
5
|
-
let release;
|
|
6
|
-
const current = new Promise((resolve) => {
|
|
7
|
-
release = resolve;
|
|
8
|
-
});
|
|
9
|
-
const tail = previous.then(() => current, () => current);
|
|
10
|
-
this.tails.set(conversationKey, tail);
|
|
11
|
-
await previous;
|
|
12
|
-
try {
|
|
13
|
-
return await operation();
|
|
14
|
-
}
|
|
15
|
-
finally {
|
|
16
|
-
release();
|
|
17
|
-
if (this.tails.get(conversationKey) === tail) {
|
|
18
|
-
this.tails.delete(conversationKey);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|