@linzumi/cli 0.0.19-beta → 0.0.22-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -69
- package/bin/linzumi.js +10 -18
- package/dist/assets/linzumi-logo.svg +1 -0
- package/dist/index.js +9135 -0
- package/package.json +9 -4
- package/src/agentBootstrap.ts +0 -872
- package/src/authCache.ts +0 -157
- package/src/authResolution.ts +0 -77
- package/src/boundedCache.ts +0 -57
- package/src/channelSession.ts +0 -4301
- package/src/channelSessionSupport.ts +0 -308
- package/src/codexAppServer.ts +0 -380
- package/src/codexOutput.ts +0 -846
- package/src/codexRuntimeOptions.ts +0 -80
- package/src/dependencyStatus.ts +0 -198
- package/src/forwardTunnel.ts +0 -859
- package/src/forwardTunnelProtocol.ts +0 -324
- package/src/index.ts +0 -1079
- package/src/json.ts +0 -49
- package/src/kandanQueue.ts +0 -113
- package/src/kandanTls.ts +0 -86
- package/src/localCapabilities.ts +0 -143
- package/src/localCodexMessageState.ts +0 -135
- package/src/localCodexTurnState.ts +0 -108
- package/src/localConfig.ts +0 -99
- package/src/localEditor.ts +0 -1061
- package/src/localEditorRuntime.ts +0 -717
- package/src/localForwarding.ts +0 -523
- package/src/oauth.ts +0 -425
- package/src/pendingKandanMessageQueue.ts +0 -109
- package/src/phoenix.ts +0 -359
- package/src/portForwardApproval.ts +0 -181
- package/src/portForwardWatcher.ts +0 -404
- package/src/protocol.ts +0 -321
- package/src/runner.ts +0 -943
- package/src/runnerConsoleReporter.ts +0 -142
- package/src/runnerLogger.ts +0 -50
- package/src/streamDeltaCoalescing.ts +0 -129
- package/src/streamDeltaQueue.ts +0 -102
package/src/oauth.ts
DELETED
|
@@ -1,425 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
- Date: 2026-04-24
|
|
3
|
-
Spec: plans/2026-04-24-local-codex-channel-thread-binding-spec.md
|
|
4
|
-
Relationship: Implements the runner-side browser authorization flow used
|
|
5
|
-
when the operator does not pass a pre-existing Kandan token.
|
|
6
|
-
*/
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
|
-
import { randomBytes } from "node:crypto";
|
|
9
|
-
|
|
10
|
-
export type LocalRunnerOAuthOptions = {
|
|
11
|
-
readonly kandanUrl: string;
|
|
12
|
-
readonly workspaceSlug?: string | undefined;
|
|
13
|
-
readonly channelSlug?: string | undefined;
|
|
14
|
-
readonly onboarding?: "start" | undefined;
|
|
15
|
-
readonly callbackHost?: string | undefined;
|
|
16
|
-
readonly openAuthorizationUrl?: ((url: string) => Promise<void> | void) | undefined;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type LocalRunnerOAuthToken = {
|
|
20
|
-
readonly accessToken: string;
|
|
21
|
-
readonly expiresInSeconds?: number | undefined;
|
|
22
|
-
readonly workspaceSlug?: string | undefined;
|
|
23
|
-
readonly channelSlug?: string | undefined;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type LocalRunnerStartTarget = {
|
|
27
|
-
readonly workspaceSlug: string;
|
|
28
|
-
readonly channelSlug: string;
|
|
29
|
-
readonly workspaceName: string;
|
|
30
|
-
readonly channelName: string;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export async function acquireLocalRunnerToken(
|
|
34
|
-
options: LocalRunnerOAuthOptions,
|
|
35
|
-
): Promise<string> {
|
|
36
|
-
const token = await acquireLocalRunnerTokenDetails(options);
|
|
37
|
-
return token.accessToken;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function acquireLocalRunnerTokenDetails(
|
|
41
|
-
options: LocalRunnerOAuthOptions,
|
|
42
|
-
): Promise<LocalRunnerOAuthToken> {
|
|
43
|
-
const httpBaseUrl = kandanHttpBaseUrl(options.kandanUrl);
|
|
44
|
-
const state = randomBytes(18).toString("base64url");
|
|
45
|
-
const callback = await startCallbackServer({
|
|
46
|
-
host: oauthCallbackHost(options.kandanUrl, options.callbackHost),
|
|
47
|
-
});
|
|
48
|
-
const authorizeUrl = authorizationUrl({
|
|
49
|
-
httpBaseUrl,
|
|
50
|
-
redirectUri: callback.redirectUri,
|
|
51
|
-
state,
|
|
52
|
-
workspaceSlug: options.workspaceSlug,
|
|
53
|
-
channelSlug: options.channelSlug,
|
|
54
|
-
onboarding: options.onboarding,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
process.stderr.write(`Authorize the local Codex runner:\n${authorizeUrl}\n`);
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
await (options.openAuthorizationUrl ?? openBrowser)(authorizeUrl);
|
|
61
|
-
const result = await callback.waitForCallback();
|
|
62
|
-
|
|
63
|
-
if (result.state !== state) {
|
|
64
|
-
throw new Error("local runner OAuth state mismatch");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return await exchangeCodeForToken({
|
|
68
|
-
httpBaseUrl,
|
|
69
|
-
code: result.code,
|
|
70
|
-
redirectUri: callback.redirectUri,
|
|
71
|
-
});
|
|
72
|
-
} finally {
|
|
73
|
-
callback.close();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export async function validateLocalRunnerToken(args: {
|
|
78
|
-
readonly kandanUrl: string;
|
|
79
|
-
readonly accessToken: string;
|
|
80
|
-
readonly workspaceSlug?: string | undefined;
|
|
81
|
-
readonly channelSlug?: string | undefined;
|
|
82
|
-
}): Promise<boolean> {
|
|
83
|
-
const url = new URL(
|
|
84
|
-
"/api/v2/local-codex-runner/oauth/validate",
|
|
85
|
-
kandanHttpBaseUrl(args.kandanUrl),
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
if (args.workspaceSlug !== undefined) {
|
|
89
|
-
url.searchParams.set("workspace", args.workspaceSlug);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (args.channelSlug !== undefined) {
|
|
93
|
-
url.searchParams.set("channel", args.channelSlug);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const response = await fetch(
|
|
97
|
-
url,
|
|
98
|
-
{
|
|
99
|
-
method: "GET",
|
|
100
|
-
headers: { authorization: `Bearer ${args.accessToken}` },
|
|
101
|
-
},
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
switch (response.status) {
|
|
105
|
-
case 200: {
|
|
106
|
-
const body: unknown = await response.json();
|
|
107
|
-
return typeof body === "object" && body !== null && "ok" in body && body.ok === true;
|
|
108
|
-
}
|
|
109
|
-
case 401:
|
|
110
|
-
case 403:
|
|
111
|
-
return false;
|
|
112
|
-
default:
|
|
113
|
-
throw new Error(`local runner auth validation failed with HTTP ${response.status}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export async function fetchLocalRunnerStartTarget(args: {
|
|
118
|
-
readonly kandanUrl: string;
|
|
119
|
-
readonly accessToken: string;
|
|
120
|
-
}): Promise<LocalRunnerStartTarget> {
|
|
121
|
-
const response = await fetch(
|
|
122
|
-
new URL(
|
|
123
|
-
"/api/v2/local-codex-runner/onboarding/start",
|
|
124
|
-
kandanHttpBaseUrl(args.kandanUrl),
|
|
125
|
-
),
|
|
126
|
-
{
|
|
127
|
-
method: "GET",
|
|
128
|
-
headers: { authorization: `Bearer ${args.accessToken}` },
|
|
129
|
-
},
|
|
130
|
-
);
|
|
131
|
-
const body: unknown = await response.json();
|
|
132
|
-
|
|
133
|
-
if (!response.ok || typeof body !== "object" || body === null) {
|
|
134
|
-
throw new Error(`local runner start target failed with HTTP ${response.status}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const workspace = "workspace" in body ? body.workspace : undefined;
|
|
138
|
-
const channel = "channel" in body ? body.channel : undefined;
|
|
139
|
-
const workspaceName = "workspace_name" in body ? body.workspace_name : undefined;
|
|
140
|
-
const channelName = "channel_name" in body ? body.channel_name : undefined;
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
typeof workspace !== "string" ||
|
|
144
|
-
workspace.trim() === "" ||
|
|
145
|
-
typeof channel !== "string" ||
|
|
146
|
-
channel.trim() === "" ||
|
|
147
|
-
typeof workspaceName !== "string" ||
|
|
148
|
-
workspaceName.trim() === "" ||
|
|
149
|
-
typeof channelName !== "string" ||
|
|
150
|
-
channelName.trim() === ""
|
|
151
|
-
) {
|
|
152
|
-
throw new Error("local runner start target response was incomplete");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
workspaceSlug: workspace,
|
|
157
|
-
channelSlug: channel,
|
|
158
|
-
workspaceName,
|
|
159
|
-
channelName,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function kandanHttpBaseUrl(kandanUrl: string): string {
|
|
164
|
-
const parsed = new URL(kandanUrl);
|
|
165
|
-
|
|
166
|
-
switch (parsed.protocol) {
|
|
167
|
-
case "ws:":
|
|
168
|
-
parsed.protocol = "http:";
|
|
169
|
-
break;
|
|
170
|
-
case "wss:":
|
|
171
|
-
parsed.protocol = "https:";
|
|
172
|
-
break;
|
|
173
|
-
case "http:":
|
|
174
|
-
case "https:":
|
|
175
|
-
break;
|
|
176
|
-
default:
|
|
177
|
-
throw new Error("--kandan-url must be ws://, wss://, http://, or https://");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
parsed.pathname = "";
|
|
181
|
-
parsed.search = "";
|
|
182
|
-
parsed.hash = "";
|
|
183
|
-
return parsed.toString().replace(/\/$/, "");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function oauthCallbackHost(
|
|
187
|
-
kandanUrl: string,
|
|
188
|
-
explicitHost?: string | undefined,
|
|
189
|
-
): string {
|
|
190
|
-
const normalizedExplicitHost = explicitHost?.trim();
|
|
191
|
-
|
|
192
|
-
if (normalizedExplicitHost !== undefined && normalizedExplicitHost !== "") {
|
|
193
|
-
return normalizedExplicitHost;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const host = new URL(kandanUrl).hostname.trim();
|
|
197
|
-
|
|
198
|
-
return isPrivateCallbackHost(host) && host !== "localhost" ? host : "127.0.0.1";
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function isPrivateCallbackHost(host: string): boolean {
|
|
202
|
-
const octets = host.split(".").map((part) => Number.parseInt(part, 10));
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
octets.length !== 4 ||
|
|
206
|
-
octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
|
|
207
|
-
) {
|
|
208
|
-
return host === "localhost";
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const [a, b] = octets;
|
|
212
|
-
|
|
213
|
-
if (a === undefined || b === undefined) {
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return (
|
|
218
|
-
a === 10 ||
|
|
219
|
-
a === 127 ||
|
|
220
|
-
(a === 172 && b >= 16 && b <= 31) ||
|
|
221
|
-
(a === 192 && b === 168) ||
|
|
222
|
-
(a === 169 && b === 254) ||
|
|
223
|
-
(a === 100 && b >= 64 && b <= 127)
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function authorizationUrl(args: {
|
|
228
|
-
readonly httpBaseUrl: string;
|
|
229
|
-
readonly redirectUri: string;
|
|
230
|
-
readonly state: string;
|
|
231
|
-
readonly workspaceSlug?: string | undefined;
|
|
232
|
-
readonly channelSlug?: string | undefined;
|
|
233
|
-
readonly onboarding?: "start" | undefined;
|
|
234
|
-
}): string {
|
|
235
|
-
const url = new URL("/api/v2/local-codex-runner/oauth/authorize", args.httpBaseUrl);
|
|
236
|
-
url.searchParams.set("redirect_uri", args.redirectUri);
|
|
237
|
-
url.searchParams.set("state", args.state);
|
|
238
|
-
|
|
239
|
-
if (args.workspaceSlug !== undefined) {
|
|
240
|
-
url.searchParams.set("workspace", args.workspaceSlug);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (args.channelSlug !== undefined) {
|
|
244
|
-
url.searchParams.set("channel", args.channelSlug);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (args.onboarding !== undefined) {
|
|
248
|
-
url.searchParams.set("onboarding", args.onboarding);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return url.toString();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function exchangeCodeForToken(args: {
|
|
255
|
-
readonly httpBaseUrl: string;
|
|
256
|
-
readonly code: string;
|
|
257
|
-
readonly redirectUri: string;
|
|
258
|
-
}): Promise<LocalRunnerOAuthToken> {
|
|
259
|
-
const response = await fetch(
|
|
260
|
-
new URL("/api/v2/local-codex-runner/oauth/token", args.httpBaseUrl),
|
|
261
|
-
{
|
|
262
|
-
method: "POST",
|
|
263
|
-
headers: { "content-type": "application/json" },
|
|
264
|
-
body: JSON.stringify({
|
|
265
|
-
code: args.code,
|
|
266
|
-
redirect_uri: args.redirectUri,
|
|
267
|
-
}),
|
|
268
|
-
},
|
|
269
|
-
);
|
|
270
|
-
const body: unknown = await response.json();
|
|
271
|
-
|
|
272
|
-
if (!response.ok || typeof body !== "object" || body === null) {
|
|
273
|
-
throw new Error("local runner OAuth token exchange failed");
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const token = "access_token" in body ? body.access_token : undefined;
|
|
277
|
-
|
|
278
|
-
if (typeof token !== "string" || token.trim() === "") {
|
|
279
|
-
throw new Error("local runner OAuth token response did not include access_token");
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const expiresIn = "expires_in" in body ? body.expires_in : undefined;
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
accessToken: token,
|
|
286
|
-
expiresInSeconds: typeof expiresIn === "number" ? expiresIn : undefined,
|
|
287
|
-
workspaceSlug: stringBodyField(body, "workspace"),
|
|
288
|
-
channelSlug: stringBodyField(body, "channel"),
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function stringBodyField(body: object, key: string): string | undefined {
|
|
293
|
-
const value = key in body ? body[key as keyof typeof body] : undefined;
|
|
294
|
-
|
|
295
|
-
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function startCallbackServer(args: {
|
|
299
|
-
readonly host: string;
|
|
300
|
-
}): Promise<{
|
|
301
|
-
readonly redirectUri: string;
|
|
302
|
-
readonly waitForCallback: () => Promise<{ readonly code: string; readonly state: string }>;
|
|
303
|
-
readonly close: () => void;
|
|
304
|
-
}> {
|
|
305
|
-
return new Promise((resolve) => {
|
|
306
|
-
let resolveCallback:
|
|
307
|
-
| ((value: { readonly code: string; readonly state: string }) => void)
|
|
308
|
-
| undefined;
|
|
309
|
-
let rejectCallback: ((reason?: unknown) => void) | undefined;
|
|
310
|
-
const callbackPromise = new Promise<{ readonly code: string; readonly state: string }>(
|
|
311
|
-
(callbackResolve, callbackReject) => {
|
|
312
|
-
resolveCallback = callbackResolve;
|
|
313
|
-
rejectCallback = callbackReject;
|
|
314
|
-
},
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
const server = Bun.serve({
|
|
318
|
-
hostname: args.host,
|
|
319
|
-
port: 0,
|
|
320
|
-
fetch(request) {
|
|
321
|
-
const url = new URL(request.url);
|
|
322
|
-
const code = url.searchParams.get("code");
|
|
323
|
-
const state = url.searchParams.get("state");
|
|
324
|
-
const error = url.searchParams.get("error");
|
|
325
|
-
|
|
326
|
-
if (error !== null && error.trim() !== "") {
|
|
327
|
-
rejectCallback?.(new Error(`local runner OAuth failed: ${error}`));
|
|
328
|
-
return oauthResultHtml({
|
|
329
|
-
title: "Linzumi CLI was not authorized",
|
|
330
|
-
body: "You denied the request. You can close this tab and rerun linzumi start when you are ready.",
|
|
331
|
-
status: 403,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (code === null || state === null || code.trim() === "" || state.trim() === "") {
|
|
336
|
-
return oauthResultHtml({
|
|
337
|
-
title: "Authorization callback was incomplete",
|
|
338
|
-
body: "Kandan did not send the local runner authorization code. Return to your terminal and try again.",
|
|
339
|
-
status: 400,
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
resolveCallback?.({ code, state });
|
|
344
|
-
return oauthResultHtml({
|
|
345
|
-
title: "Linzumi CLI is connected",
|
|
346
|
-
body: "You can close this tab and return to the terminal. Kandan will finish starting the local runner.",
|
|
347
|
-
status: 200,
|
|
348
|
-
});
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
resolve({
|
|
353
|
-
redirectUri: `http://${args.host}:${server.port}/callback`,
|
|
354
|
-
waitForCallback: () => callbackPromise,
|
|
355
|
-
close: () => {
|
|
356
|
-
server.stop(true);
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function oauthResultHtml(args: {
|
|
363
|
-
readonly title: string;
|
|
364
|
-
readonly body: string;
|
|
365
|
-
readonly status: number;
|
|
366
|
-
}): Response {
|
|
367
|
-
return new Response(
|
|
368
|
-
`<!doctype html>
|
|
369
|
-
<html>
|
|
370
|
-
<head>
|
|
371
|
-
<meta charset="utf-8" />
|
|
372
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
373
|
-
<title>${escapeHtml(args.title)}</title>
|
|
374
|
-
<style>
|
|
375
|
-
:root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
376
|
-
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f6f7f9; color: #13161c; }
|
|
377
|
-
main { width: min(520px, calc(100vw - 32px)); border: 1px solid #d9dde5; border-radius: 12px; background: #fff; padding: 30px; box-shadow: 0 24px 70px rgba(18, 25, 38, 0.12); }
|
|
378
|
-
h1 { margin: 0 0 10px; font-size: 24px; line-height: 1.2; }
|
|
379
|
-
p { margin: 0; color: #465160; line-height: 1.55; }
|
|
380
|
-
@media (prefers-color-scheme: dark) {
|
|
381
|
-
body { background: #0f131a; color: #f3f5f8; }
|
|
382
|
-
main { background: #171c25; border-color: #2b3442; box-shadow: 0 24px 70px rgba(0, 0, 0, 0.4); }
|
|
383
|
-
p { color: #aab4c2; }
|
|
384
|
-
}
|
|
385
|
-
</style>
|
|
386
|
-
</head>
|
|
387
|
-
<body>
|
|
388
|
-
<main>
|
|
389
|
-
<h1>${escapeHtml(args.title)}</h1>
|
|
390
|
-
<p>${escapeHtml(args.body)}</p>
|
|
391
|
-
</main>
|
|
392
|
-
</body>
|
|
393
|
-
</html>`,
|
|
394
|
-
{
|
|
395
|
-
status: args.status,
|
|
396
|
-
headers: { "content-type": "text/html; charset=utf-8" },
|
|
397
|
-
},
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function escapeHtml(value: string): string {
|
|
402
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function openBrowser(url: string): Promise<void> {
|
|
406
|
-
const command =
|
|
407
|
-
process.platform === "darwin"
|
|
408
|
-
? "open"
|
|
409
|
-
: process.platform === "win32"
|
|
410
|
-
? "cmd"
|
|
411
|
-
: "xdg-open";
|
|
412
|
-
const args =
|
|
413
|
-
process.platform === "win32"
|
|
414
|
-
? ["/c", "start", "", url]
|
|
415
|
-
: [url];
|
|
416
|
-
|
|
417
|
-
return new Promise((resolve) => {
|
|
418
|
-
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
419
|
-
child.on("error", () => resolve());
|
|
420
|
-
child.on("spawn", () => {
|
|
421
|
-
child.unref();
|
|
422
|
-
resolve();
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
- Date: 2026-04-26
|
|
3
|
-
Spec: kandan/server_v2/plans/2026-04-26-local-codex-driver-worldclass-spec.md
|
|
4
|
-
Relationship: Provides the local Codex channel session with an amortized O(1)
|
|
5
|
-
pending Kandan message queue so long shared sessions avoid repeated
|
|
6
|
-
`Array.shift()` reindexing while preserving interrupt fusion order.
|
|
7
|
-
*/
|
|
8
|
-
import {
|
|
9
|
-
interruptQueuedMessages,
|
|
10
|
-
type QueueInterruptResult,
|
|
11
|
-
type QueuedKandanMessage,
|
|
12
|
-
} from "./kandanQueue";
|
|
13
|
-
|
|
14
|
-
export type PendingKandanMessageQueue = {
|
|
15
|
-
items: QueuedKandanMessage[];
|
|
16
|
-
head: number;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function createPendingKandanMessageQueue(): PendingKandanMessageQueue {
|
|
20
|
-
return {
|
|
21
|
-
items: [],
|
|
22
|
-
head: 0,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function pendingKandanMessageQueueLength(
|
|
27
|
-
queue: PendingKandanMessageQueue,
|
|
28
|
-
): number {
|
|
29
|
-
return queue.items.length - queue.head;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function enqueuePendingKandanMessage(
|
|
33
|
-
queue: PendingKandanMessageQueue,
|
|
34
|
-
message: QueuedKandanMessage,
|
|
35
|
-
): void {
|
|
36
|
-
queue.items.push(message);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function dequeuePendingKandanMessage(
|
|
40
|
-
queue: PendingKandanMessageQueue,
|
|
41
|
-
): QueuedKandanMessage | undefined {
|
|
42
|
-
const message = queue.items[queue.head];
|
|
43
|
-
|
|
44
|
-
if (message === undefined) {
|
|
45
|
-
compactPendingKandanMessageQueue(queue);
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
queue.head = queue.head + 1;
|
|
50
|
-
compactPendingKandanMessageQueue(queue);
|
|
51
|
-
return message;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function requeuePendingKandanMessageFront(
|
|
55
|
-
queue: PendingKandanMessageQueue,
|
|
56
|
-
message: QueuedKandanMessage,
|
|
57
|
-
): void {
|
|
58
|
-
if (queue.head > 0) {
|
|
59
|
-
queue.head = queue.head - 1;
|
|
60
|
-
queue.items[queue.head] = message;
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
queue.items.unshift(message);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function interruptPendingKandanMessages(
|
|
68
|
-
queue: PendingKandanMessageQueue,
|
|
69
|
-
throughSeq: number | undefined,
|
|
70
|
-
): QueueInterruptResult {
|
|
71
|
-
const result = interruptQueuedMessages(pendingKandanMessages(queue), throughSeq);
|
|
72
|
-
|
|
73
|
-
if (result.ok) {
|
|
74
|
-
replacePendingKandanMessages(queue, result.queue);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return result;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function replacePendingKandanMessages(
|
|
81
|
-
queue: PendingKandanMessageQueue,
|
|
82
|
-
messages: QueuedKandanMessage[],
|
|
83
|
-
): void {
|
|
84
|
-
queue.items = messages;
|
|
85
|
-
queue.head = 0;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function pendingKandanMessages(
|
|
89
|
-
queue: PendingKandanMessageQueue,
|
|
90
|
-
): QueuedKandanMessage[] {
|
|
91
|
-
return queue.head === 0 ? queue.items : queue.items.slice(queue.head);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function compactPendingKandanMessageQueue(queue: PendingKandanMessageQueue): void {
|
|
95
|
-
if (queue.head === 0) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (queue.head >= queue.items.length) {
|
|
100
|
-
queue.items = [];
|
|
101
|
-
queue.head = 0;
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (queue.head > queue.items.length / 2) {
|
|
106
|
-
queue.items = queue.items.slice(queue.head);
|
|
107
|
-
queue.head = 0;
|
|
108
|
-
}
|
|
109
|
-
}
|