airfoil 0.4.4 → 0.4.6

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.
@@ -1,279 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import { Arrow, ClusterClient } from "@useairfoil/wings";
3
- import { printTable } from "console-table-printer";
4
- import { Effect, Option } from "effect";
5
- import { Command, Flag } from "effect/unstable/cli";
6
- import * as fs from "node:fs";
7
-
8
- import { makeClusterClientLayer } from "../../../utils/client";
9
- import { hostOption, portOption } from "../../../utils/options";
10
-
11
- const SUPPORTED_INLINE_TYPES = [
12
- "Int8",
13
- "Int16",
14
- "Int32",
15
- "Int64",
16
- "Uint8",
17
- "Uint16",
18
- "Uint32",
19
- "Uint64",
20
- "Bool",
21
- "Utf8",
22
- "Binary",
23
- "Null",
24
- "Float16",
25
- "Float32",
26
- "Float64",
27
- "DateDay",
28
- "DateMillisecond",
29
- "TimestampSecond",
30
- "TimestampMillisecond",
31
- "TimestampMicrosecond",
32
- "TimestampNanosecond",
33
- "DurationSecond",
34
- "DurationMillisecond",
35
- "DurationMicrosecond",
36
- "DurationNanosecond",
37
- ] as const;
38
-
39
- type SupportedInlineType = (typeof SUPPORTED_INLINE_TYPES)[number];
40
- type FieldConfig = Arrow.FieldConfig;
41
-
42
- const parentOption = Flag.string("parent").pipe(
43
- Flag.withDescription("Parent namespace in format: namespaces/{namespace}"),
44
- );
45
-
46
- const tableIdOption = Flag.string("table-id").pipe(
47
- Flag.withDescription("Unique identifier for the table"),
48
- );
49
-
50
- const descriptionOption = Flag.string("description").pipe(
51
- Flag.withDescription("Table description"),
52
- Flag.optional,
53
- );
54
-
55
- const fieldsOption = Flag.string("fields").pipe(
56
- Flag.withDescription('Field definitions in format "name:Type" or "name:Type?" for nullable'),
57
- Flag.atLeast(0),
58
- );
59
-
60
- const schemaFileOption = Flag.string("schema-file").pipe(
61
- Flag.withDescription("Path to JSON file containing FieldConfig[] (for complex types)"),
62
- Flag.optional,
63
- );
64
-
65
- const keyFieldOption = Flag.string("key-field").pipe(
66
- Flag.withDescription("Name of the field used as the primary key"),
67
- );
68
-
69
- const versionFieldOption = Flag.string("version-field").pipe(
70
- Flag.withDescription("Name of the field used as the version/timestamp"),
71
- );
72
-
73
- const partitionFieldOption = Flag.string("partition-field").pipe(
74
- Flag.withDescription("Name of the field used for partitioning"),
75
- Flag.optional,
76
- );
77
-
78
- const freshnessSecondsOption = Flag.integer("freshness-seconds").pipe(
79
- Flag.withDescription("Target freshness interval for compaction (seconds)"),
80
- Flag.withDefault(60),
81
- );
82
-
83
- function parseFieldString(fieldStr: string, index: number): FieldConfig {
84
- const match = fieldStr.match(/^([^:]+):([^?]+)(\?)?$/);
85
- if (!match) {
86
- throw new Error(
87
- `Invalid field format: "${fieldStr}". Expected format: "name:Type" or "name:Type?"`,
88
- );
89
- }
90
-
91
- const name = match[1];
92
- const dataType = match[2];
93
- const nullableMarker = match[3];
94
-
95
- if (!name || !dataType) {
96
- throw new Error(
97
- `Invalid field format: "${fieldStr}". Expected format: "name:Type" or "name:Type?"`,
98
- );
99
- }
100
-
101
- const nullable = nullableMarker === "?";
102
-
103
- if (!SUPPORTED_INLINE_TYPES.includes(dataType as SupportedInlineType)) {
104
- throw new Error(
105
- `Unsupported type: "${dataType}". Supported types: ${SUPPORTED_INLINE_TYPES.join(", ")}. For complex types, use --schema-file.`,
106
- );
107
- }
108
-
109
- return {
110
- name: name.trim(),
111
- dataType: dataType as SupportedInlineType,
112
- nullable,
113
- id: BigInt(index + 1),
114
- };
115
- }
116
-
117
- function parseFieldsFromArgs(fields: ReadonlyArray<string>): FieldConfig[] {
118
- return fields.map((field, index) => parseFieldString(field, index));
119
- }
120
-
121
- function loadFieldsFromFile(filePath: string): FieldConfig[] {
122
- if (!fs.existsSync(filePath)) {
123
- throw new Error(`Schema file not found: "${filePath}"`);
124
- }
125
-
126
- const content = fs.readFileSync(filePath, "utf-8");
127
-
128
- let parsed: unknown;
129
- try {
130
- parsed = JSON.parse(content);
131
- } catch {
132
- throw new Error(`Invalid JSON in schema file: "${filePath}"`);
133
- }
134
-
135
- if (!Array.isArray(parsed)) {
136
- throw new Error(`Schema file must contain an array of fields. Got: ${typeof parsed}`);
137
- }
138
-
139
- if (parsed.length === 0) {
140
- throw new Error("Schema file contains an empty array. At least one field is required.");
141
- }
142
-
143
- return (parsed as FieldConfig[]).map((field, index) => {
144
- const fieldId = (field as { id?: bigint | number }).id;
145
- return {
146
- ...field,
147
- id: typeof fieldId === "bigint" ? fieldId : BigInt(fieldId ?? index + 1),
148
- };
149
- });
150
- }
151
-
152
- export const createTableCommand = Command.make(
153
- "create-table",
154
- {
155
- parent: parentOption,
156
- tableId: tableIdOption,
157
- description: descriptionOption,
158
- fields: fieldsOption,
159
- schemaFile: schemaFileOption,
160
- keyField: keyFieldOption,
161
- versionField: versionFieldOption,
162
- partitionField: partitionFieldOption,
163
- freshnessSeconds: freshnessSecondsOption,
164
- host: hostOption,
165
- port: portOption,
166
- },
167
- ({
168
- parent,
169
- tableId,
170
- description,
171
- fields,
172
- schemaFile,
173
- keyField,
174
- versionField,
175
- partitionField,
176
- freshnessSeconds,
177
- }) =>
178
- Effect.gen(function* () {
179
- p.intro("📋 Create Table");
180
-
181
- const schemaFilePath = Option.getOrUndefined(schemaFile);
182
- const hasFields = fields.length > 0;
183
-
184
- if (!hasFields && !schemaFilePath) {
185
- return yield* Effect.fail(new Error("Either --fields or --schema-file must be provided"));
186
- }
187
-
188
- if (hasFields && schemaFilePath) {
189
- return yield* Effect.fail(
190
- new Error("Cannot use both --fields and --schema-file. Choose one."),
191
- );
192
- }
193
-
194
- const tableFields = yield* Effect.try({
195
- try: () =>
196
- schemaFilePath ? loadFieldsFromFile(schemaFilePath) : parseFieldsFromArgs(fields),
197
- catch: (error) => (error instanceof Error ? error : new Error("Failed to parse fields")),
198
- });
199
-
200
- if (tableFields.length === 0) {
201
- return yield* Effect.fail(new Error("At least one field is required"));
202
- }
203
-
204
- const resolveField = (fieldName: string, role: string): bigint => {
205
- const field = tableFields.find((f) => f.name === fieldName);
206
- if (!field) {
207
- throw new Error(
208
- `${role} field "${fieldName}" not found. Available fields: ${tableFields.map((f) => f.name).join(", ")}`,
209
- );
210
- }
211
- return field.id;
212
- };
213
-
214
- const keyFieldId = yield* Effect.try({
215
- try: () => resolveField(keyField, "Key"),
216
- catch: (e) => (e instanceof Error ? e : new Error(String(e))),
217
- });
218
-
219
- const versionFieldId = yield* Effect.try({
220
- try: () => resolveField(versionField, "Version"),
221
- catch: (e) => (e instanceof Error ? e : new Error(String(e))),
222
- });
223
-
224
- const partitionFieldName = Option.getOrUndefined(partitionField);
225
- const partitionFieldId = partitionFieldName
226
- ? yield* Effect.try({
227
- try: () => resolveField(partitionFieldName, "Partition"),
228
- catch: (e) => (e instanceof Error ? e : new Error(String(e))),
229
- })
230
- : undefined;
231
-
232
- const s = p.spinner();
233
- s.start("Creating table...");
234
-
235
- const table = yield* ClusterClient.createTable({
236
- parent,
237
- tableId,
238
- description: Option.getOrUndefined(description),
239
- fields: tableFields,
240
- keyFieldId,
241
- versionFieldId,
242
- partitionFieldId,
243
- targetFreshnessSeconds: BigInt(freshnessSeconds),
244
- }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to create table"))));
245
-
246
- s.stop("Table created successfully");
247
-
248
- yield* Effect.sync(() => {
249
- printTable([
250
- {
251
- name: table.name,
252
- description: table.description || "-",
253
- key_field_id: table.keyFieldId.toString(),
254
- version_field_id: table.versionFieldId.toString(),
255
- partition_field_id: table.partitionFieldId?.toString() || "-",
256
- freshness_seconds: table.targetFreshnessSeconds.toString(),
257
- fields_count: table.schema.fields.length.toString(),
258
- },
259
- ]);
260
-
261
- if (table.schema.fields.length > 0) {
262
- console.log("\nFields:");
263
- printTable(
264
- table.schema.fields.map((field) => ({
265
- name: field.name,
266
- id: field.id.toString(),
267
- type: field.arrowType?._tag ?? "unknown",
268
- nullable: field.nullable ? "yes" : "no",
269
- })),
270
- );
271
- }
272
-
273
- p.outro("✓ Done");
274
- });
275
- }),
276
- ).pipe(
277
- Command.withDescription("Create a new table belonging to a namespace"),
278
- Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
279
- );
@@ -1,54 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import { ClusterClient } from "@useairfoil/wings";
3
- import { Effect } from "effect";
4
- import { Command, Flag } from "effect/unstable/cli";
5
-
6
- import { makeClusterClientLayer } from "../../../utils/client";
7
- import { forceOption, hostOption, portOption } from "../../../utils/options";
8
-
9
- const nameOption = Flag.string("name").pipe(
10
- Flag.withDescription("Table name in format: namespaces/{namespace}/tables/{table}"),
11
- );
12
-
13
- export const deleteTableCommand = Command.make(
14
- "delete-table",
15
- {
16
- name: nameOption,
17
- force: forceOption,
18
- host: hostOption,
19
- port: portOption,
20
- },
21
- ({ name, force }) =>
22
- Effect.gen(function* () {
23
- p.intro("🗑️ Delete Table");
24
-
25
- if (!force) {
26
- const confirm = yield* Effect.tryPromise({
27
- try: () =>
28
- p.confirm({
29
- message: `Are you sure you want to delete table ${name}?`,
30
- initialValue: false,
31
- }),
32
- catch: () => new Error("Failed to confirm deletion"),
33
- });
34
-
35
- if (p.isCancel(confirm) || !confirm) {
36
- p.cancel("Deletion cancelled");
37
- return;
38
- }
39
- }
40
-
41
- const s = p.spinner();
42
- s.start("Deleting table...");
43
-
44
- yield* ClusterClient.deleteTable({ name }).pipe(
45
- Effect.tapError(() => Effect.sync(() => s.stop("Failed to delete table"))),
46
- );
47
-
48
- s.stop("Table deleted successfully");
49
- p.outro("✓ Done");
50
- }),
51
- ).pipe(
52
- Command.withDescription("Delete a table from the cluster"),
53
- Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
54
- );
@@ -1,62 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import { ClusterClient } from "@useairfoil/wings";
3
- import { printTable } from "console-table-printer";
4
- import { Effect } from "effect";
5
- import { Command, Flag } from "effect/unstable/cli";
6
-
7
- import { makeClusterClientLayer } from "../../../utils/client";
8
- import { hostOption, portOption } from "../../../utils/options";
9
-
10
- const nameOption = Flag.string("name").pipe(
11
- Flag.withDescription("Table name in format: namespaces/{namespace}/tables/{table}"),
12
- );
13
-
14
- export const getTableCommand = Command.make(
15
- "get-table",
16
- {
17
- name: nameOption,
18
- host: hostOption,
19
- port: portOption,
20
- },
21
- ({ name }) =>
22
- Effect.gen(function* () {
23
- const s = p.spinner();
24
- s.start("Fetching table...");
25
-
26
- const table = yield* ClusterClient.getTable({ name }).pipe(
27
- Effect.tapError(() => Effect.sync(() => s.stop("Failed to get table"))),
28
- );
29
-
30
- s.stop("Table retrieved");
31
-
32
- yield* Effect.sync(() => {
33
- printTable([
34
- {
35
- name: table.name,
36
- description: table.description || "-",
37
- key_field_id: table.keyFieldId.toString(),
38
- version_field_id: table.versionFieldId.toString(),
39
- partition_field_id: table.partitionFieldId?.toString() || "-",
40
- freshness_seconds: table.targetFreshnessSeconds.toString(),
41
- },
42
- ]);
43
-
44
- if (table.schema.fields.length > 0) {
45
- console.log("\nFields:");
46
- printTable(
47
- table.schema.fields.map((field) => ({
48
- name: field.name,
49
- id: field.id.toString(),
50
- type: field.arrowType?._tag ?? "unknown",
51
- nullable: field.nullable ? "yes" : "no",
52
- })),
53
- );
54
- }
55
-
56
- p.outro("✓ Done");
57
- });
58
- }),
59
- ).pipe(
60
- Command.withDescription("Get details of a specific table"),
61
- Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
62
- );
@@ -1,61 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import { ClusterClient } from "@useairfoil/wings";
3
- import { printTable } from "console-table-printer";
4
- import { Effect, Option } from "effect";
5
- import { Command, Flag } from "effect/unstable/cli";
6
-
7
- import { makeClusterClientLayer } from "../../../utils/client";
8
- import { hostOption, pageSizeOption, pageTokenOption, portOption } from "../../../utils/options";
9
-
10
- const parentOption = Flag.string("parent").pipe(
11
- Flag.withDescription("Parent namespace in format: namespaces/{namespace}"),
12
- );
13
-
14
- export const listTablesCommand = Command.make(
15
- "list-tables",
16
- {
17
- parent: parentOption,
18
- pageSize: pageSizeOption,
19
- pageToken: pageTokenOption,
20
- host: hostOption,
21
- port: portOption,
22
- },
23
- ({ parent, pageSize, pageToken }) =>
24
- Effect.gen(function* () {
25
- const s = p.spinner();
26
- s.start("Fetching tables...");
27
-
28
- const response = yield* ClusterClient.listTables({
29
- parent,
30
- pageSize,
31
- pageToken: Option.getOrUndefined(pageToken),
32
- }).pipe(Effect.tapError(() => Effect.sync(() => s.stop("Failed to list tables"))));
33
-
34
- s.stop(`Found ${response.tables.length} table(s)`);
35
-
36
- yield* Effect.sync(() => {
37
- if (response.tables.length === 0) {
38
- p.log.warn("No tables found");
39
- } else {
40
- printTable(
41
- response.tables.map((table) => ({
42
- name: table.name,
43
- description: table.description || "-",
44
- key_field_id: table.keyFieldId.toString(),
45
- partition_field_id: table.partitionFieldId?.toString() || "-",
46
- freshness_seconds: table.targetFreshnessSeconds.toString(),
47
- })),
48
- );
49
- }
50
-
51
- if (response.nextPageToken) {
52
- p.log.info(`Next page token: ${response.nextPageToken}`);
53
- }
54
-
55
- p.outro("✓ Done");
56
- });
57
- }),
58
- ).pipe(
59
- Command.withDescription("List all tables belonging to a namespace"),
60
- Command.provide(({ host, port }) => makeClusterClientLayer(host, port)),
61
- );
@@ -1,136 +0,0 @@
1
- import { Effect, FileSystem } from "effect";
2
- import { Command, Flag, Prompt } from "effect/unstable/cli";
3
- import { ChildProcess } from "effect/unstable/process";
4
-
5
- import {
6
- checkDockerVersion,
7
- createDockerVolume,
8
- pullDockerImage,
9
- runDockerContainer,
10
- } from "../utils/docker";
11
- import { downloadWings, getWingsPath } from "../utils/wings";
12
-
13
- const dockerOption = Flag.boolean("docker").pipe(
14
- Flag.withDescription("Run Wings using Docker (recommended)"),
15
- Flag.withDefault(false),
16
- );
17
-
18
- const versionOption = Flag.string("version").pipe(
19
- Flag.withDescription("Specify Wings version (e.g., v0.1.0-alpha.14 or 'latest')"),
20
- Flag.withDefault("latest"),
21
- );
22
-
23
- const tagOption = Flag.string("tag").pipe(
24
- Flag.withDescription("Docker image tag (only with --docker)"),
25
- Flag.withDefault("latest"),
26
- );
27
-
28
- const forcePullOption = Flag.boolean("force-pull").pipe(
29
- Flag.withDescription("Force pull Docker image even if it exists locally"),
30
- Flag.withDefault(false),
31
- );
32
-
33
- const stressOption = Flag.boolean("stress").pipe(
34
- Flag.withDescription("Use Wings stress binary variant"),
35
- Flag.withDefault(false),
36
- );
37
-
38
- const yesOption = Flag.boolean("yes").pipe(
39
- Flag.withAlias("y"),
40
- Flag.withDescription("Skip confirmation prompts"),
41
- Flag.withDefault(false),
42
- );
43
-
44
- export const devCommand = Command.make(
45
- "dev",
46
- {
47
- docker: dockerOption,
48
- version: versionOption,
49
- tag: tagOption,
50
- forcePull: forcePullOption,
51
- stress: stressOption,
52
- yes: yesOption,
53
- },
54
- (options) => (options.docker ? runWithDocker(options) : runWithBinary(options)),
55
- ).pipe(
56
- Command.withDescription(
57
- "Download and run Wings dev server locally (Docker recommended for portability)",
58
- ),
59
- );
60
-
61
- const runWithBinary = (options: { version: string; yes: boolean; stress: boolean }) =>
62
- Effect.gen(function* () {
63
- const fs = yield* FileSystem.FileSystem;
64
-
65
- yield* Effect.logInfo("🪽 Airfoil Dev");
66
-
67
- const version = options.version;
68
- const isStress = options.stress;
69
- const wingsPath = yield* getWingsPath(version, isStress);
70
- const exists = yield* fs.exists(wingsPath);
71
-
72
- if (!exists) {
73
- yield* Effect.logWarning(
74
- `Wings${isStress ? " stress" : ""} binary not found for version ${version}`,
75
- );
76
-
77
- if (!options.yes) {
78
- const confirm = yield* Prompt.confirm({
79
- message: `Download Wings${isStress ? " stress" : ""} ${version} binary? (~100MB)`,
80
- initial: true,
81
- });
82
-
83
- if (!confirm) {
84
- return yield* Effect.fail(new Error("Download cancelled"));
85
- }
86
- }
87
-
88
- yield* Effect.logInfo(`Downloading Wings${isStress ? " stress" : ""} ${version}...`);
89
-
90
- yield* downloadWings(version, wingsPath, isStress);
91
- } else {
92
- yield* Effect.logInfo(`Using cached Wings${isStress ? " stress" : ""} binary (${version})`);
93
- }
94
-
95
- yield* Effect.logInfo(`Starting Wings dev server...`);
96
-
97
- const handle = yield* ChildProcess.make(wingsPath, ["dev"], {
98
- stdin: "inherit",
99
- stdout: "inherit",
100
- });
101
-
102
- const exitCode = yield* handle.exitCode;
103
-
104
- if (exitCode !== 0) {
105
- return yield* Effect.fail(new Error(`Process exited with code ${exitCode}`));
106
- }
107
- });
108
-
109
- const runWithDocker = (options: { tag: string; forcePull: boolean }) =>
110
- Effect.gen(function* () {
111
- yield* Effect.logInfo("🐳 Airfoil Dev (Docker)");
112
-
113
- let tag = options.tag;
114
-
115
- if (tag !== "latest" && !tag.includes("aarch64") && !tag.includes("x86_64")) {
116
- const arch = process.arch === "arm64" ? "aarch64" : "x86_64";
117
- const cleanTag = tag.startsWith("v") ? tag.substring(1) : tag;
118
- tag = `${cleanTag}-${arch}`;
119
- }
120
-
121
- const image = `docker.useairfoil.com/airfoil/wings:${tag}`;
122
-
123
- yield* Effect.logInfo(`Using Docker image: ${image}`);
124
- yield* Effect.logInfo("Ports: 7777 (gRPC)");
125
-
126
- yield* checkDockerVersion();
127
-
128
- if (options.forcePull) {
129
- yield* Effect.logInfo(`Pulling Docker image: ${image}`);
130
- yield* pullDockerImage(image);
131
- }
132
-
133
- yield* createDockerVolume("wings-data");
134
-
135
- yield* runDockerContainer(image, "wings-data");
136
- });
@@ -1,90 +0,0 @@
1
- import { ArrowFlightSqlClient } from "@useairfoil/flight";
2
- import { printTable } from "console-table-printer";
3
- import { Effect, FileSystem, Option, Stream } from "effect";
4
- import { Argument, Command, Flag } from "effect/unstable/cli";
5
- import { Metadata } from "nice-grpc-common";
6
-
7
- import { hostOption, portOption } from "../utils/options";
8
-
9
- const queryArg = Argument.string("query").pipe(
10
- Argument.withDescription("SQL query to execute"),
11
- Argument.optional,
12
- );
13
-
14
- const fileOption = Flag.string("file").pipe(
15
- Flag.withAlias("f"),
16
- Flag.withDescription("Execute SQL from file"),
17
- Flag.optional,
18
- );
19
-
20
- const namespaceOption = Flag.string("namespace").pipe(
21
- Flag.withAlias("n"),
22
- Flag.withDescription("Wings namespace"),
23
- Flag.withDefault("namespaces/default"),
24
- );
25
-
26
- const jsonOption = Flag.boolean("json").pipe(
27
- Flag.withDescription("Output results as JSON"),
28
- Flag.withDefault(false),
29
- );
30
-
31
- export const sqlCommand = Command.make(
32
- "sql",
33
- {
34
- query: queryArg,
35
- host: hostOption,
36
- port: portOption,
37
- file: fileOption,
38
- namespace: namespaceOption,
39
- json: jsonOption,
40
- },
41
- ({ query, host, port, file, namespace, json }) =>
42
- Effect.gen(function* () {
43
- const fs = yield* FileSystem.FileSystem;
44
- let sqlQuery = Option.getOrUndefined(query);
45
- const filePath = Option.getOrUndefined(file);
46
-
47
- if (filePath) {
48
- sqlQuery = yield* fs.readFileString(filePath);
49
- }
50
-
51
- if (!sqlQuery) {
52
- return yield* Effect.fail(
53
- new Error("No query provided. Use a query argument or --file option."),
54
- );
55
- }
56
-
57
- const hostPort = `${host}:${port}`;
58
-
59
- yield* Effect.logInfo(`Namespace: ${namespace}`);
60
-
61
- const client = yield* ArrowFlightSqlClient.make({
62
- host: hostPort,
63
- defaultCallOptions: {
64
- "*": {
65
- metadata: Metadata({
66
- "x-wings-namespace": namespace,
67
- }),
68
- },
69
- },
70
- });
71
-
72
- const flightInfo = yield* client.executeQuery({ query: sqlQuery });
73
-
74
- const batches = yield* client.executeFlightInfo(flightInfo).pipe(Stream.runCollect);
75
-
76
- const data = Array.from(batches).flatMap(({ batch }) => batch.toArray());
77
-
78
- if (json) {
79
- yield* Effect.log(JSON.stringify(data, null, 2));
80
- } else if (data.length === 0) {
81
- yield* Effect.logWarning("No results returned");
82
- } else {
83
- printTable(data, {
84
- defaultColumnOptions: {
85
- alignment: "left",
86
- },
87
- });
88
- }
89
- }),
90
- ).pipe(Command.withDescription("Execute SQL queries using Arrow Flight SQL"));
@@ -1,6 +0,0 @@
1
- import { ClusterClient } from "@useairfoil/wings";
2
-
3
- export const makeClusterClientLayer = (host: string, port: number) =>
4
- ClusterClient.layer({
5
- host: `${host}:${port}`,
6
- });