@getjack/jack 0.1.30 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib/picker.ts CHANGED
@@ -86,7 +86,9 @@ export function requireTTY(): void {
86
86
  * Interactive project picker using @clack/core primitives
87
87
  * @param options.cloudOnly - If true, only shows cloud-only projects (for linking)
88
88
  */
89
- export async function pickProject(options?: PickProjectOptions): Promise<PickerResult | PickerCancelResult> {
89
+ export async function pickProject(
90
+ options?: PickProjectOptions,
91
+ ): Promise<PickerResult | PickerCancelResult> {
90
92
  // Fetch all projects
91
93
  let allProjects: ProjectListItem[];
92
94
  try {
@@ -47,7 +47,7 @@ import { debug, isDebug, printTimingSummary, timerEnd, timerStart } from "./debu
47
47
  import { ensureWranglerInstalled, validateModeAvailability } from "./deploy-mode.ts";
48
48
  import { detectSecrets, generateEnvFile, generateSecretsJson } from "./env-parser.ts";
49
49
  import { JackError, JackErrorCode } from "./errors.ts";
50
- import { type HookOutput, runHook } from "./hooks.ts";
50
+ import { type HookOutput, promptSelect, runHook } from "./hooks.ts";
51
51
  import { loadTemplateKeywords, matchTemplateByIntent } from "./intent.ts";
52
52
  import {
53
53
  type ManagedCreateResult,
@@ -1605,13 +1605,36 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1605
1605
  // User is logged into Jack Cloud - create managed project
1606
1606
  const orphanedProjectName = await getProjectNameFromDir(projectPath);
1607
1607
 
1608
- reporter.info(`Linking "${orphanedProjectName}" to jack cloud...`);
1609
-
1610
- // Get username for URL construction
1608
+ // Get username for confirmation prompt and URL construction
1611
1609
  const { getCurrentUserProfile } = await import("./control-plane.ts");
1612
1610
  const profile = await getCurrentUserProfile();
1613
1611
  const ownerUsername = profile?.username ?? undefined;
1614
1612
 
1613
+ // Confirm before creating new project
1614
+ if (interactive) {
1615
+ reporter.info("This project isn't linked to jack cloud.");
1616
+ const choice = await promptSelect(
1617
+ ["Yes", "No"],
1618
+ `Create new project "${orphanedProjectName}" under @${ownerUsername ?? "unknown"}?`,
1619
+ );
1620
+ if (choice !== 0) {
1621
+ reporter.info("Cancelled. To link to an existing project, use: jack link <project-id>");
1622
+ return {
1623
+ projectName: orphanedProjectName,
1624
+ workerUrl: null,
1625
+ deployMode: "managed" as DeployMode,
1626
+ };
1627
+ }
1628
+ } else {
1629
+ throw new JackError(
1630
+ JackErrorCode.PROJECT_NOT_FOUND,
1631
+ "Project not linked to jack cloud (non-interactive mode)",
1632
+ "Run interactively or use: jack link <project-id>",
1633
+ );
1634
+ }
1635
+
1636
+ reporter.info(`Linking "${orphanedProjectName}" to jack cloud...`);
1637
+
1615
1638
  // Create managed project on jack cloud
1616
1639
  const remoteResult = await createManagedProjectRemote(orphanedProjectName, reporter, {
1617
1640
  usePrebuilt: false,
@@ -1716,6 +1739,17 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1716
1739
  );
1717
1740
  }
1718
1741
 
1742
+ // Show current identity for visibility (managed mode only, not dry run)
1743
+ if (!dryRun) {
1744
+ const { getCurrentUserProfile } = await import("./control-plane.ts");
1745
+ const profile = await getCurrentUserProfile();
1746
+ if (profile?.username) {
1747
+ reporter.info(`Deploying as @${profile.username}...`);
1748
+ } else if (profile?.email) {
1749
+ reporter.info(`Deploying as ${profile.email}...`);
1750
+ }
1751
+ }
1752
+
1719
1753
  // Dry run: build for validation then stop before actual deployment
1720
1754
  // (deployToManagedProject handles its own build, so only build here for dry-run)
1721
1755
  if (dryRun) {
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Cron schedule creation logic for jack services cron create
3
+ *
4
+ * Validates cron expression, checks minimum interval, and calls control plane API.
5
+ * Only supported for managed (Jack Cloud) projects.
6
+ */
7
+
8
+ import { createCronSchedule as createCronScheduleApi } from "../control-plane.ts";
9
+ import { readProjectLink } from "../project-link.ts";
10
+ import { checkMinimumInterval, validateCronExpression } from "./cron-utils.ts";
11
+
12
+ // Minimum interval between cron runs (in minutes)
13
+ const MIN_INTERVAL_MINUTES = 15;
14
+
15
+ export interface CreateCronScheduleOptions {
16
+ interactive?: boolean;
17
+ }
18
+
19
+ export interface CreateCronScheduleResult {
20
+ id: string;
21
+ expression: string;
22
+ description: string;
23
+ nextRunAt: string;
24
+ created: boolean;
25
+ }
26
+
27
+ /**
28
+ * Create a cron schedule for the current project.
29
+ *
30
+ * For managed projects: calls control plane POST /v1/projects/:id/crons
31
+ * For BYO projects: throws error (not supported)
32
+ */
33
+ export async function createCronSchedule(
34
+ projectDir: string,
35
+ expression: string,
36
+ options: CreateCronScheduleOptions = {},
37
+ ): Promise<CreateCronScheduleResult> {
38
+ // Validate expression
39
+ const validation = validateCronExpression(expression);
40
+ if (!validation.valid) {
41
+ throw new Error(`Invalid cron expression: ${validation.error}`);
42
+ }
43
+
44
+ const normalizedExpression = validation.normalized!;
45
+
46
+ // Check minimum interval (15 minutes)
47
+ if (!checkMinimumInterval(normalizedExpression, MIN_INTERVAL_MINUTES)) {
48
+ throw new Error(
49
+ `Cron schedules must run at least ${MIN_INTERVAL_MINUTES} minutes apart. ` +
50
+ "This limit helps ensure reliable execution.",
51
+ );
52
+ }
53
+
54
+ // Must be managed mode
55
+ const link = await readProjectLink(projectDir);
56
+ if (!link || link.deploy_mode !== "managed") {
57
+ throw new Error(
58
+ "Cron schedules are only supported for Jack Cloud (managed) projects.\n" +
59
+ "BYO projects can use native Cloudflare cron triggers in wrangler.toml.",
60
+ );
61
+ }
62
+
63
+ // Create via control plane
64
+ const result = await createCronScheduleApi(link.project_id, normalizedExpression);
65
+
66
+ return {
67
+ id: result.id,
68
+ expression: result.expression,
69
+ description: result.description,
70
+ nextRunAt: result.next_run_at,
71
+ created: true,
72
+ };
73
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Cron schedule deletion logic for jack services cron delete
3
+ *
4
+ * Finds schedule by expression and deletes via control plane API.
5
+ * Only supported for managed (Jack Cloud) projects.
6
+ */
7
+
8
+ import {
9
+ deleteCronSchedule as deleteCronScheduleApi,
10
+ listCronSchedules as listCronSchedulesApi,
11
+ } from "../control-plane.ts";
12
+ import { readProjectLink } from "../project-link.ts";
13
+ import { normalizeCronExpression } from "./cron-utils.ts";
14
+
15
+ export interface DeleteCronScheduleOptions {
16
+ interactive?: boolean;
17
+ }
18
+
19
+ export interface DeleteCronScheduleResult {
20
+ expression: string;
21
+ deleted: boolean;
22
+ }
23
+
24
+ /**
25
+ * Delete a cron schedule by its expression.
26
+ *
27
+ * For managed projects: finds the schedule by expression and calls DELETE API
28
+ * For BYO projects: throws error (not supported)
29
+ */
30
+ export async function deleteCronSchedule(
31
+ projectDir: string,
32
+ expression: string,
33
+ options: DeleteCronScheduleOptions = {},
34
+ ): Promise<DeleteCronScheduleResult> {
35
+ const normalizedExpression = normalizeCronExpression(expression);
36
+
37
+ // Must be managed mode
38
+ const link = await readProjectLink(projectDir);
39
+ if (!link || link.deploy_mode !== "managed") {
40
+ throw new Error(
41
+ "Cron schedules are only supported for Jack Cloud (managed) projects.\n" +
42
+ "BYO projects can use native Cloudflare cron triggers in wrangler.toml.",
43
+ );
44
+ }
45
+
46
+ // Find the schedule by expression
47
+ const schedules = await listCronSchedulesApi(link.project_id);
48
+ const schedule = schedules.find(
49
+ (s) => normalizeCronExpression(s.expression) === normalizedExpression,
50
+ );
51
+
52
+ if (!schedule) {
53
+ throw new Error(
54
+ `No cron schedule found with expression "${normalizedExpression}".\n` +
55
+ "Use 'jack services cron list' to see all schedules.",
56
+ );
57
+ }
58
+
59
+ // Delete via control plane
60
+ await deleteCronScheduleApi(link.project_id, schedule.id);
61
+
62
+ return {
63
+ expression: normalizedExpression,
64
+ deleted: true,
65
+ };
66
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Cron schedule listing logic for jack services cron list
3
+ *
4
+ * Fetches cron schedules from control plane API.
5
+ * Only supported for managed (Jack Cloud) projects.
6
+ */
7
+
8
+ import {
9
+ type CronScheduleInfo,
10
+ listCronSchedules as listCronSchedulesApi,
11
+ } from "../control-plane.ts";
12
+ import { readProjectLink } from "../project-link.ts";
13
+
14
+ export interface CronScheduleListEntry {
15
+ id: string;
16
+ expression: string;
17
+ description: string;
18
+ enabled: boolean;
19
+ nextRunAt: string;
20
+ lastRunAt: string | null;
21
+ lastRunStatus: string | null;
22
+ lastRunDurationMs: number | null;
23
+ consecutiveFailures: number;
24
+ createdAt: string;
25
+ }
26
+
27
+ /**
28
+ * List all cron schedules for the current project.
29
+ *
30
+ * For managed projects: calls control plane GET /v1/projects/:id/crons
31
+ * For BYO projects: throws error (not supported)
32
+ */
33
+ export async function listCronSchedules(projectDir: string): Promise<CronScheduleListEntry[]> {
34
+ // Must be managed mode
35
+ const link = await readProjectLink(projectDir);
36
+ if (!link || link.deploy_mode !== "managed") {
37
+ throw new Error(
38
+ "Cron schedules are only supported for Jack Cloud (managed) projects.\n" +
39
+ "BYO projects can use native Cloudflare cron triggers in wrangler.toml.",
40
+ );
41
+ }
42
+
43
+ // Fetch from control plane
44
+ const schedules = await listCronSchedulesApi(link.project_id);
45
+
46
+ // Map to our format
47
+ return schedules.map((s: CronScheduleInfo) => ({
48
+ id: s.id,
49
+ expression: s.expression,
50
+ description: s.description,
51
+ enabled: s.enabled,
52
+ nextRunAt: s.next_run_at,
53
+ lastRunAt: s.last_run_at,
54
+ lastRunStatus: s.last_run_status,
55
+ lastRunDurationMs: s.last_run_duration_ms,
56
+ consecutiveFailures: s.consecutive_failures,
57
+ createdAt: s.created_at,
58
+ }));
59
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Cron schedule testing logic for jack services cron test
3
+ *
4
+ * Validates expression, shows human-readable description and next times.
5
+ * Optionally triggers the schedule on production (managed projects only).
6
+ */
7
+
8
+ import { triggerCronSchedule as triggerCronScheduleApi } from "../control-plane.ts";
9
+ import { readProjectLink } from "../project-link.ts";
10
+ import {
11
+ describeCronExpression,
12
+ getNextScheduledTimes,
13
+ validateCronExpression,
14
+ } from "./cron-utils.ts";
15
+
16
+ export interface CronTestOptions {
17
+ triggerProduction?: boolean;
18
+ interactive?: boolean;
19
+ }
20
+
21
+ export interface CronTestResult {
22
+ valid: boolean;
23
+ error?: string;
24
+ expression?: string;
25
+ description?: string;
26
+ nextTimes?: Date[];
27
+ triggerResult?: {
28
+ triggered: boolean;
29
+ status: string;
30
+ durationMs: number;
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Test a cron expression: validate, describe, and show next run times.
36
+ * Optionally trigger the schedule handler on production.
37
+ */
38
+ export async function testCronExpression(
39
+ projectDir: string,
40
+ expression: string,
41
+ options: CronTestOptions = {},
42
+ ): Promise<CronTestResult> {
43
+ // Validate expression
44
+ const validation = validateCronExpression(expression);
45
+ if (!validation.valid) {
46
+ return {
47
+ valid: false,
48
+ error: validation.error,
49
+ };
50
+ }
51
+
52
+ const normalizedExpression = validation.normalized!;
53
+
54
+ // Get description and next times
55
+ const description = describeCronExpression(normalizedExpression);
56
+ const nextTimes = getNextScheduledTimes(normalizedExpression, 5);
57
+
58
+ const result: CronTestResult = {
59
+ valid: true,
60
+ expression: normalizedExpression,
61
+ description,
62
+ nextTimes,
63
+ };
64
+
65
+ // Optionally trigger production
66
+ if (options.triggerProduction) {
67
+ const link = await readProjectLink(projectDir);
68
+ if (!link || link.deploy_mode !== "managed") {
69
+ throw new Error("Production trigger is only available for Jack Cloud (managed) projects.");
70
+ }
71
+
72
+ const triggerResponse = await triggerCronScheduleApi(link.project_id, normalizedExpression);
73
+ result.triggerResult = {
74
+ triggered: triggerResponse.triggered,
75
+ status: triggerResponse.status,
76
+ durationMs: triggerResponse.duration_ms,
77
+ };
78
+ }
79
+
80
+ return result;
81
+ }
82
+
83
+ /**
84
+ * Common cron expression patterns for help output.
85
+ */
86
+ export const COMMON_CRON_PATTERNS = [
87
+ { expression: "*/15 * * * *", description: "Every 15 minutes" },
88
+ { expression: "0 * * * *", description: "Every hour" },
89
+ { expression: "0 0 * * *", description: "Daily at midnight" },
90
+ { expression: "0 9 * * *", description: "Daily at 9am" },
91
+ { expression: "0 9 * * 1", description: "Every Monday at 9am" },
92
+ { expression: "0 0 1 * *", description: "First day of every month" },
93
+ ];
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Cron expression utilities for validation, parsing, and human-readable descriptions.
3
+ */
4
+
5
+ import parser from "cron-parser";
6
+ import cronstrue from "cronstrue";
7
+
8
+ /**
9
+ * Normalize a cron expression by collapsing whitespace.
10
+ * "0 * * * *" -> "0 * * * *"
11
+ */
12
+ export function normalizeCronExpression(expression: string): string {
13
+ return expression.trim().replace(/\s+/g, " ");
14
+ }
15
+
16
+ /**
17
+ * Validate a cron expression.
18
+ * Returns normalized expression if valid, error message if invalid.
19
+ */
20
+ export function validateCronExpression(expression: string): {
21
+ valid: boolean;
22
+ error?: string;
23
+ normalized?: string;
24
+ } {
25
+ try {
26
+ const normalized = normalizeCronExpression(expression);
27
+ parser.parseExpression(normalized);
28
+ return { valid: true, normalized };
29
+ } catch (e) {
30
+ return { valid: false, error: e instanceof Error ? e.message : String(e) };
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Check if a cron expression has at least the minimum interval between runs.
36
+ * Returns true if the interval is >= minMinutes.
37
+ */
38
+ export function checkMinimumInterval(expression: string, minMinutes: number): boolean {
39
+ try {
40
+ const interval = parser.parseExpression(expression);
41
+ const first = interval.next().toDate();
42
+ const second = interval.next().toDate();
43
+ return (second.getTime() - first.getTime()) / 1000 / 60 >= minMinutes;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get the next N scheduled times for a cron expression.
51
+ */
52
+ export function getNextScheduledTimes(expression: string, count: number): Date[] {
53
+ const interval = parser.parseExpression(expression);
54
+ const times: Date[] = [];
55
+ for (let i = 0; i < count; i++) {
56
+ times.push(interval.next().toDate());
57
+ }
58
+ return times;
59
+ }
60
+
61
+ /**
62
+ * Get a human-readable description of a cron expression.
63
+ * e.g., "0 * * * *" -> "At minute 0"
64
+ */
65
+ export function describeCronExpression(expression: string): string {
66
+ try {
67
+ return cronstrue.toString(expression);
68
+ } catch {
69
+ return expression;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Compute the next run time for a cron expression as ISO string.
75
+ */
76
+ export function computeNextRun(expression: string): string {
77
+ return parser.parseExpression(expression).next().toDate().toISOString();
78
+ }
@@ -15,14 +15,18 @@ import { JackError, JackErrorCode } from "../errors.ts";
15
15
 
16
16
  export type DomainStatus =
17
17
  | "claimed"
18
+ | "unassigned"
18
19
  | "pending"
20
+ | "pending_dns"
19
21
  | "pending_owner"
20
22
  | "pending_ssl"
21
23
  | "active"
22
24
  | "blocked"
23
25
  | "moved"
24
26
  | "failed"
25
- | "deleting";
27
+ | "deleting"
28
+ | "expired"
29
+ | "deleted";
26
30
 
27
31
  export interface DomainVerification {
28
32
  type: "cname";
@@ -36,6 +40,22 @@ export interface DomainOwnershipVerification {
36
40
  value: string;
37
41
  }
38
42
 
43
+ export interface DomainDns {
44
+ verified: boolean;
45
+ checked_at: string | null;
46
+ current_target: string | null;
47
+ expected_target: string | null;
48
+ error: string | null;
49
+ }
50
+
51
+ export interface DomainNextStep {
52
+ action: string;
53
+ record_type?: string;
54
+ record_name?: string;
55
+ record_value?: string;
56
+ message?: string;
57
+ }
58
+
39
59
  export interface DomainInfo {
40
60
  id: string;
41
61
  hostname: string;
@@ -45,6 +65,8 @@ export interface DomainInfo {
45
65
  project_slug: string | null;
46
66
  verification?: DomainVerification;
47
67
  ownership_verification?: DomainOwnershipVerification;
68
+ dns?: DomainDns;
69
+ next_step?: DomainNextStep;
48
70
  created_at: string;
49
71
  }
50
72
 
@@ -96,16 +118,20 @@ interface ListDomainsApiResponse {
96
118
  }
97
119
 
98
120
  interface ConnectDomainApiResponse {
99
- id: string;
100
- hostname: string;
101
- status: string;
121
+ domain: {
122
+ id: string;
123
+ hostname: string;
124
+ status: string;
125
+ };
102
126
  }
103
127
 
104
128
  interface AssignDomainApiResponse {
105
- id: string;
106
- hostname: string;
107
- status: string;
108
- ssl_status: string | null;
129
+ domain: {
130
+ id: string;
131
+ hostname: string;
132
+ status: string;
133
+ ssl_status: string | null;
134
+ };
109
135
  verification?: DomainVerification;
110
136
  ownership_verification?: DomainOwnershipVerification;
111
137
  }
@@ -215,9 +241,9 @@ export async function connectDomain(hostname: string): Promise<ConnectDomainResu
215
241
 
216
242
  const data = (await response.json()) as ConnectDomainApiResponse;
217
243
  return {
218
- id: data.id,
219
- hostname: data.hostname,
220
- status: data.status as DomainStatus,
244
+ id: data.domain.id,
245
+ hostname: data.domain.hostname,
246
+ status: data.domain.status as DomainStatus,
221
247
  };
222
248
  }
223
249
 
@@ -282,10 +308,10 @@ export async function assignDomain(
282
308
 
283
309
  const data = (await response.json()) as AssignDomainApiResponse;
284
310
  return {
285
- id: data.id,
286
- hostname: data.hostname,
287
- status: data.status as DomainStatus,
288
- ssl_status: data.ssl_status,
311
+ id: data.domain.id,
312
+ hostname: data.domain.hostname,
313
+ status: data.domain.status as DomainStatus,
314
+ ssl_status: data.domain.ssl_status,
289
315
  project_id: project.id,
290
316
  project_slug: projectSlug,
291
317
  verification: data.verification,
@@ -334,10 +360,13 @@ export async function unassignDomain(hostname: string): Promise<UnassignDomainRe
334
360
  );
335
361
  }
336
362
 
363
+ const data = (await response.json()) as {
364
+ domain: { id: string; hostname: string; status: string };
365
+ };
337
366
  return {
338
- id: domain.id,
339
- hostname: domain.hostname,
340
- status: "claimed" as DomainStatus,
367
+ id: data.domain.id,
368
+ hostname: data.domain.hostname,
369
+ status: data.domain.status as DomainStatus,
341
370
  };
342
371
  }
343
372
 
@@ -377,3 +406,45 @@ export async function disconnectDomain(hostname: string): Promise<DisconnectDoma
377
406
  hostname: domain.hostname,
378
407
  };
379
408
  }
409
+
410
+ /**
411
+ * Verify DNS configuration for a domain.
412
+ *
413
+ * @throws JackError with RESOURCE_NOT_FOUND if domain not found
414
+ */
415
+ export interface VerifyDomainResult {
416
+ domain: DomainInfo;
417
+ dns_check?: {
418
+ verified: boolean;
419
+ target: string | null;
420
+ error: string | null;
421
+ };
422
+ }
423
+
424
+ export async function verifyDomain(hostname: string): Promise<VerifyDomainResult> {
425
+ const domain = await getDomainByHostname(hostname);
426
+ if (!domain) {
427
+ throw new JackError(
428
+ JackErrorCode.PROJECT_NOT_FOUND,
429
+ `Domain not found: ${hostname}`,
430
+ "Run 'jack domain' to see all domains",
431
+ { exitCode: 1 },
432
+ );
433
+ }
434
+
435
+ const response = await authFetch(`${getControlApiUrl()}/v1/domains/${domain.id}/verify`, {
436
+ method: "POST",
437
+ });
438
+
439
+ if (!response.ok) {
440
+ const err = (await response
441
+ .json()
442
+ .catch(() => ({ message: "Unknown error" }))) as ApiErrorResponse;
443
+ throw new JackError(
444
+ JackErrorCode.INTERNAL_ERROR,
445
+ err.message || `Failed to verify domain: ${response.status}`,
446
+ );
447
+ }
448
+
449
+ return (await response.json()) as VerifyDomainResult;
450
+ }
package/src/mcp/server.ts CHANGED
@@ -49,6 +49,20 @@ export async function startMcpServer(options: McpServerOptions = {}) {
49
49
 
50
50
  debug("Starting MCP server on stdio transport");
51
51
 
52
+ // Process-level error handlers to prevent silent crashes
53
+ process.on("uncaughtException", (error) => {
54
+ console.error(`[jack-mcp] Uncaught exception: ${error.message}`);
55
+ debug("Uncaught exception", { error: error.stack });
56
+ process.exit(1);
57
+ });
58
+
59
+ process.on("unhandledRejection", (reason) => {
60
+ const message = reason instanceof Error ? reason.message : String(reason);
61
+ console.error(`[jack-mcp] Unhandled rejection: ${message}`);
62
+ debug("Unhandled rejection", { reason });
63
+ process.exit(1);
64
+ });
65
+
52
66
  // Always log startup to stderr so user knows it's running
53
67
  console.error(
54
68
  `[jack-mcp] Server started (v${pkg.version})${options.debug ? " [debug mode]" : ""}`,
@@ -57,4 +71,10 @@ export async function startMcpServer(options: McpServerOptions = {}) {
57
71
  await server.connect(transport);
58
72
 
59
73
  debug("MCP server connected and ready");
74
+
75
+ // Keep the server running indefinitely.
76
+ // This blocks the async function from returning, preventing the caller from
77
+ // falling through to any cleanup/exit code (like process.exit(0) in index.ts).
78
+ // The process will stay alive via stdin event listeners in the transport.
79
+ await new Promise(() => {});
60
80
  }