t3code-cli 0.1.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/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/bin.js +17842 -0
- package/dist/index.js +2 -0
- package/dist/runtime-DU_hs0MM.js +61631 -0
- package/package.json +59 -0
- package/src/application/error.ts +4 -0
- package/src/application/layer.ts +20 -0
- package/src/application/model-selection.ts +109 -0
- package/src/application/models.ts +26 -0
- package/src/application/project-commands.ts +28 -0
- package/src/application/projects.ts +50 -0
- package/src/application/service.ts +93 -0
- package/src/application/shell-sequence.ts +29 -0
- package/src/application/thread-commands.ts +89 -0
- package/src/application/thread-wait.ts +86 -0
- package/src/application/threads.ts +172 -0
- package/src/auth/error.ts +42 -0
- package/src/auth/layer.ts +114 -0
- package/src/auth/local.ts +241 -0
- package/src/auth/pairing.ts +54 -0
- package/src/auth/schema.ts +55 -0
- package/src/auth/service.ts +16 -0
- package/src/auth/transport.ts +132 -0
- package/src/auth/type.ts +31 -0
- package/src/bin.ts +47 -0
- package/src/cli/app.ts +18 -0
- package/src/cli/auth-format.ts +43 -0
- package/src/cli/auth.ts +99 -0
- package/src/cli/error.ts +16 -0
- package/src/cli/input/error.ts +11 -0
- package/src/cli/input/layer.ts +31 -0
- package/src/cli/input/service.ts +11 -0
- package/src/cli/message-input.ts +25 -0
- package/src/cli/model-format.ts +18 -0
- package/src/cli/model-options.ts +56 -0
- package/src/cli/models.ts +45 -0
- package/src/cli/output/error.ts +11 -0
- package/src/cli/output/layer.ts +53 -0
- package/src/cli/output/service.ts +15 -0
- package/src/cli/output-format.ts +41 -0
- package/src/cli/project-format.ts +11 -0
- package/src/cli/projects.ts +62 -0
- package/src/cli/thread-format.ts +74 -0
- package/src/cli/threads/archive.ts +28 -0
- package/src/cli/threads/list.ts +29 -0
- package/src/cli/threads/messages.ts +36 -0
- package/src/cli/threads/send.ts +91 -0
- package/src/cli/threads/start.ts +136 -0
- package/src/cli/threads/wait.ts +35 -0
- package/src/cli/threads.ts +22 -0
- package/src/cli/wait-events.ts +112 -0
- package/src/config/error.ts +21 -0
- package/src/config/layer.ts +103 -0
- package/src/config/service.ts +24 -0
- package/src/config/url.ts +55 -0
- package/src/domain/command-schema.ts +82 -0
- package/src/domain/error.ts +46 -0
- package/src/domain/helpers.ts +23 -0
- package/src/domain/model-config.ts +40 -0
- package/src/domain/schema.ts +162 -0
- package/src/domain/thread-lifecycle.ts +97 -0
- package/src/environment/layer.ts +12 -0
- package/src/environment/service.ts +13 -0
- package/src/index.ts +18 -0
- package/src/orchestration/layer.ts +193 -0
- package/src/orchestration/service.ts +39 -0
- package/src/protocol/schema.ts +105 -0
- package/src/rpc/error.ts +31 -0
- package/src/rpc/layer.ts +99 -0
- package/src/rpc/service.ts +16 -0
- package/src/runtime.ts +28 -0
- package/src/version/layer.ts +25 -0
- package/src/version/service.ts +8 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { T3Application } from "./application/service.ts";
|
|
2
|
+
export type {
|
|
3
|
+
SendThreadInput,
|
|
4
|
+
StartThreadInput,
|
|
5
|
+
StartThreadPolicy,
|
|
6
|
+
WaitEvent,
|
|
7
|
+
} from "./application/service.ts";
|
|
8
|
+
export type { ApplicationError } from "./application/error.ts";
|
|
9
|
+
export type {
|
|
10
|
+
ProjectShell,
|
|
11
|
+
ServerProvider,
|
|
12
|
+
ShellSnapshot,
|
|
13
|
+
ThreadDetail,
|
|
14
|
+
ThreadMessage,
|
|
15
|
+
ThreadShell,
|
|
16
|
+
} from "./domain/schema.ts";
|
|
17
|
+
export { NodeEnvironmentLive } from "./environment/layer.ts";
|
|
18
|
+
export { AppLayer, AuthAppLayer } from "./runtime.ts";
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as Schedule from "effect/Schedule";
|
|
5
|
+
import * as Sink from "effect/Sink";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import { RpcClientError } from "effect/unstable/rpc";
|
|
8
|
+
|
|
9
|
+
import { type ClientOrchestrationCommand } from "../domain/command-schema.ts";
|
|
10
|
+
import {
|
|
11
|
+
ORCHESTRATION_WS_METHODS,
|
|
12
|
+
type ShellStreamItem,
|
|
13
|
+
type ThreadStreamItem,
|
|
14
|
+
WS_METHODS,
|
|
15
|
+
} from "../protocol/schema.ts";
|
|
16
|
+
import { RpcError } from "../rpc/error.ts";
|
|
17
|
+
import { T3Rpc, type WsClient } from "../rpc/service.ts";
|
|
18
|
+
import { T3Orchestration, type OpenThread } from "./service.ts";
|
|
19
|
+
|
|
20
|
+
type ReconnectableRpcError = RpcClientError.RpcClientError | RpcError;
|
|
21
|
+
|
|
22
|
+
const rpcRetrySchedule = Schedule.exponential("100 millis").pipe(
|
|
23
|
+
Schedule.take(4),
|
|
24
|
+
Schedule.collectWhile(
|
|
25
|
+
(metadata: Schedule.Metadata<unknown, ReconnectableRpcError>) =>
|
|
26
|
+
metadata.input instanceof RpcClientError.RpcClientError,
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function runRpc<A>(
|
|
31
|
+
rpc: T3Rpc["Service"],
|
|
32
|
+
method: string,
|
|
33
|
+
f: (client: WsClient) => Effect.Effect<A, ReconnectableRpcError>,
|
|
34
|
+
) {
|
|
35
|
+
return Effect.gen(function* () {
|
|
36
|
+
const client = yield* rpc.getClient;
|
|
37
|
+
return yield* f(client);
|
|
38
|
+
}).pipe(
|
|
39
|
+
Effect.tapError((error) =>
|
|
40
|
+
error instanceof RpcClientError.RpcClientError ? rpc.disconnect : Effect.void,
|
|
41
|
+
),
|
|
42
|
+
Effect.retry(rpcRetrySchedule),
|
|
43
|
+
Effect.catchTags({
|
|
44
|
+
RpcClientError: (error) =>
|
|
45
|
+
Effect.fail(
|
|
46
|
+
new RpcError({
|
|
47
|
+
message: error.message,
|
|
48
|
+
method,
|
|
49
|
+
cause: error,
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
RpcError: (error) => Effect.fail(error),
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function subscribeRpc<A>(
|
|
58
|
+
rpc: T3Rpc["Service"],
|
|
59
|
+
method: string,
|
|
60
|
+
f: (client: WsClient) => Stream.Stream<A, ReconnectableRpcError>,
|
|
61
|
+
) {
|
|
62
|
+
return Stream.unwrap(Effect.map(rpc.getClient, f)).pipe(
|
|
63
|
+
Stream.tapError((error) =>
|
|
64
|
+
error instanceof RpcClientError.RpcClientError ? rpc.disconnect : Effect.void,
|
|
65
|
+
),
|
|
66
|
+
Stream.retry(rpcRetrySchedule),
|
|
67
|
+
Stream.catchTags({
|
|
68
|
+
RpcClientError: (error) =>
|
|
69
|
+
Stream.fail(
|
|
70
|
+
new RpcError({
|
|
71
|
+
message: error.message,
|
|
72
|
+
method,
|
|
73
|
+
cause: error,
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
RpcError: (error) => Stream.fail(error),
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const makeT3Orchestration = Effect.fn("makeT3Orchestration")(function* () {
|
|
82
|
+
const rpc = yield* T3Rpc;
|
|
83
|
+
const dispatch = Effect.fn("T3OrchestrationLive.dispatch")(function* (
|
|
84
|
+
command: ClientOrchestrationCommand,
|
|
85
|
+
) {
|
|
86
|
+
const client = yield* rpc.getClient;
|
|
87
|
+
return yield* client[ORCHESTRATION_WS_METHODS.dispatchCommand](command).pipe(
|
|
88
|
+
Effect.tapErrorTag("RpcClientError", () => rpc.disconnect),
|
|
89
|
+
Effect.catchTags({
|
|
90
|
+
RpcClientError: (error) =>
|
|
91
|
+
Effect.fail(
|
|
92
|
+
new RpcError({
|
|
93
|
+
message: error.message,
|
|
94
|
+
method: ORCHESTRATION_WS_METHODS.dispatchCommand,
|
|
95
|
+
cause: error,
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
RpcError: (error) => Effect.fail(error),
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
const getServerConfig = Effect.fn("T3OrchestrationLive.getServerConfig")(function* () {
|
|
103
|
+
return yield* runRpc(rpc, WS_METHODS.serverGetConfig, (client) =>
|
|
104
|
+
client[WS_METHODS.serverGetConfig]({}),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
const getShellSnapshot = Effect.fn("T3OrchestrationLive.getShellSnapshot")(function* () {
|
|
108
|
+
const item = yield* Stream.runHead(
|
|
109
|
+
subscribeRpc(rpc, ORCHESTRATION_WS_METHODS.subscribeShell, (client) =>
|
|
110
|
+
client[ORCHESTRATION_WS_METHODS.subscribeShell]({}),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
const value = Option.getOrUndefined(item);
|
|
114
|
+
if (value === undefined || value.kind !== "snapshot") {
|
|
115
|
+
return yield* Effect.fail(
|
|
116
|
+
new RpcError({
|
|
117
|
+
message: "server did not return shell snapshot",
|
|
118
|
+
method: ORCHESTRATION_WS_METHODS.subscribeShell,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return value.snapshot;
|
|
123
|
+
});
|
|
124
|
+
const getThreadSnapshot = Effect.fn("T3OrchestrationLive.getThreadSnapshot")(function* (
|
|
125
|
+
threadId: string,
|
|
126
|
+
) {
|
|
127
|
+
const item = yield* Stream.runHead(
|
|
128
|
+
subscribeRpc(rpc, ORCHESTRATION_WS_METHODS.subscribeThread, (client) =>
|
|
129
|
+
client[ORCHESTRATION_WS_METHODS.subscribeThread]({ threadId }),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
const value = Option.getOrUndefined(item);
|
|
133
|
+
if (value === undefined || value.kind !== "snapshot") {
|
|
134
|
+
return yield* Effect.fail(
|
|
135
|
+
new RpcError({
|
|
136
|
+
message: `thread ${threadId} not found`,
|
|
137
|
+
method: ORCHESTRATION_WS_METHODS.subscribeThread,
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return value.snapshot.thread;
|
|
142
|
+
});
|
|
143
|
+
const watchShellSequence = () =>
|
|
144
|
+
subscribeRpc(rpc, ORCHESTRATION_WS_METHODS.subscribeShell, (client) =>
|
|
145
|
+
client[ORCHESTRATION_WS_METHODS.subscribeShell]({}),
|
|
146
|
+
).pipe(
|
|
147
|
+
Stream.map((item: ShellStreamItem) =>
|
|
148
|
+
item.kind === "snapshot" ? item.snapshot.snapshotSequence : item.sequence,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
const watchThreadItems = (threadId: string) =>
|
|
152
|
+
subscribeRpc(rpc, ORCHESTRATION_WS_METHODS.subscribeThread, (client) =>
|
|
153
|
+
client[ORCHESTRATION_WS_METHODS.subscribeThread]({ threadId }),
|
|
154
|
+
);
|
|
155
|
+
const openThread = Effect.fn("T3OrchestrationLive.openThread")(function* (threadId: string) {
|
|
156
|
+
return yield* watchThreadItems(threadId).pipe(
|
|
157
|
+
Stream.peel(Sink.head<ThreadStreamItem>()),
|
|
158
|
+
Effect.flatMap(([item, rest]) => {
|
|
159
|
+
const value = Option.getOrUndefined(item);
|
|
160
|
+
if (value === undefined || value.kind !== "snapshot") {
|
|
161
|
+
return Effect.fail(
|
|
162
|
+
new RpcError({
|
|
163
|
+
message: `thread ${threadId} not found`,
|
|
164
|
+
method: ORCHESTRATION_WS_METHODS.subscribeThread,
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return Effect.succeed({
|
|
169
|
+
snapshot: value.snapshot.thread,
|
|
170
|
+
events: rest.pipe(
|
|
171
|
+
Stream.filter(
|
|
172
|
+
(next): next is Extract<ThreadStreamItem, { readonly kind: "event" }> =>
|
|
173
|
+
next.kind === "event",
|
|
174
|
+
),
|
|
175
|
+
Stream.map((next) => next.event),
|
|
176
|
+
),
|
|
177
|
+
} satisfies OpenThread);
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
dispatch,
|
|
184
|
+
getServerConfig,
|
|
185
|
+
getShellSnapshot,
|
|
186
|
+
getThreadSnapshot,
|
|
187
|
+
watchShellSequence,
|
|
188
|
+
watchThreadItems,
|
|
189
|
+
openThread,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
export const T3OrchestrationLive = Layer.effect(T3Orchestration, makeT3Orchestration());
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
import type * as Scope from "effect/Scope";
|
|
4
|
+
import type * as Stream from "effect/Stream";
|
|
5
|
+
|
|
6
|
+
import type { ClientOrchestrationCommand, DispatchResult } from "../domain/command-schema.ts";
|
|
7
|
+
import type { ServerConfig, ShellSnapshot, ThreadDetail, ThreadEvent } from "../domain/schema.ts";
|
|
8
|
+
import type { RpcError } from "../rpc/error.ts";
|
|
9
|
+
|
|
10
|
+
export type OrchestrationError = RpcError;
|
|
11
|
+
|
|
12
|
+
export type OpenThread = {
|
|
13
|
+
readonly snapshot: ThreadDetail;
|
|
14
|
+
readonly events: Stream.Stream<ThreadEvent, OrchestrationError>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ThreadStreamItem =
|
|
18
|
+
| { readonly kind: "snapshot"; readonly snapshot: { readonly thread: ThreadDetail } }
|
|
19
|
+
| { readonly kind: "event"; readonly event: ThreadEvent };
|
|
20
|
+
|
|
21
|
+
export type Orchestration = {
|
|
22
|
+
readonly dispatch: (
|
|
23
|
+
command: ClientOrchestrationCommand,
|
|
24
|
+
) => Effect.Effect<DispatchResult, OrchestrationError>;
|
|
25
|
+
readonly getServerConfig: () => Effect.Effect<ServerConfig, OrchestrationError>;
|
|
26
|
+
readonly getShellSnapshot: () => Effect.Effect<ShellSnapshot, OrchestrationError>;
|
|
27
|
+
readonly getThreadSnapshot: (threadId: string) => Effect.Effect<ThreadDetail, OrchestrationError>;
|
|
28
|
+
readonly watchShellSequence: () => Stream.Stream<number, OrchestrationError, Scope.Scope>;
|
|
29
|
+
readonly watchThreadItems: (
|
|
30
|
+
threadId: string,
|
|
31
|
+
) => Stream.Stream<ThreadStreamItem, OrchestrationError, Scope.Scope>;
|
|
32
|
+
readonly openThread: (
|
|
33
|
+
threadId: string,
|
|
34
|
+
) => Effect.Effect<OpenThread, OrchestrationError, Scope.Scope>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class T3Orchestration extends Context.Service<T3Orchestration, Orchestration>()(
|
|
38
|
+
"t3cli/T3Orchestration",
|
|
39
|
+
) {}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { Rpc, RpcGroup } from "effect/unstable/rpc";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ClientOrchestrationCommandSchema,
|
|
6
|
+
DispatchResultSchema,
|
|
7
|
+
} from "../domain/command-schema.ts";
|
|
8
|
+
import {
|
|
9
|
+
ProjectShellSchema,
|
|
10
|
+
ServerConfigSchema,
|
|
11
|
+
ShellSnapshotSchema,
|
|
12
|
+
ThreadDetailSchema,
|
|
13
|
+
ThreadEventSchema,
|
|
14
|
+
ThreadShellSchema,
|
|
15
|
+
} from "../domain/schema.ts";
|
|
16
|
+
import { RpcError } from "../rpc/error.ts";
|
|
17
|
+
|
|
18
|
+
export const ORCHESTRATION_WS_METHODS = {
|
|
19
|
+
dispatchCommand: "orchestration.dispatchCommand",
|
|
20
|
+
subscribeShell: "orchestration.subscribeShell",
|
|
21
|
+
subscribeThread: "orchestration.subscribeThread",
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export const WS_METHODS = {
|
|
25
|
+
serverGetConfig: "server.getConfig",
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export const ThreadStreamItemSchema = Schema.Union([
|
|
29
|
+
Schema.Struct({
|
|
30
|
+
kind: Schema.Literal("snapshot"),
|
|
31
|
+
snapshot: Schema.Struct({ thread: ThreadDetailSchema }),
|
|
32
|
+
}),
|
|
33
|
+
Schema.Struct({
|
|
34
|
+
kind: Schema.Literal("event"),
|
|
35
|
+
event: ThreadEventSchema,
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
export type ThreadStreamItem = typeof ThreadStreamItemSchema.Type;
|
|
39
|
+
|
|
40
|
+
export const ShellStreamItemSchema = Schema.Union([
|
|
41
|
+
Schema.Struct({
|
|
42
|
+
kind: Schema.Literal("snapshot"),
|
|
43
|
+
snapshot: ShellSnapshotSchema,
|
|
44
|
+
}),
|
|
45
|
+
Schema.Struct({
|
|
46
|
+
kind: Schema.Literal("project-upserted"),
|
|
47
|
+
sequence: Schema.Number,
|
|
48
|
+
project: ProjectShellSchema,
|
|
49
|
+
}),
|
|
50
|
+
Schema.Struct({
|
|
51
|
+
kind: Schema.Literal("project-removed"),
|
|
52
|
+
sequence: Schema.Number,
|
|
53
|
+
projectId: Schema.String,
|
|
54
|
+
}),
|
|
55
|
+
Schema.Struct({
|
|
56
|
+
kind: Schema.Literal("thread-upserted"),
|
|
57
|
+
sequence: Schema.Number,
|
|
58
|
+
thread: ThreadShellSchema,
|
|
59
|
+
}),
|
|
60
|
+
Schema.Struct({
|
|
61
|
+
kind: Schema.Literal("thread-removed"),
|
|
62
|
+
sequence: Schema.Number,
|
|
63
|
+
threadId: Schema.String,
|
|
64
|
+
}),
|
|
65
|
+
]);
|
|
66
|
+
export type ShellStreamItem = typeof ShellStreamItemSchema.Type;
|
|
67
|
+
|
|
68
|
+
export const WsOrchestrationDispatchCommandRpc = Rpc.make(
|
|
69
|
+
ORCHESTRATION_WS_METHODS.dispatchCommand,
|
|
70
|
+
{
|
|
71
|
+
payload: ClientOrchestrationCommandSchema,
|
|
72
|
+
success: DispatchResultSchema,
|
|
73
|
+
error: RpcError,
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export const WsOrchestrationSubscribeShellRpc = Rpc.make(ORCHESTRATION_WS_METHODS.subscribeShell, {
|
|
78
|
+
payload: Schema.Struct({}),
|
|
79
|
+
success: ShellStreamItemSchema,
|
|
80
|
+
error: RpcError,
|
|
81
|
+
stream: true,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const WsOrchestrationSubscribeThreadRpc = Rpc.make(
|
|
85
|
+
ORCHESTRATION_WS_METHODS.subscribeThread,
|
|
86
|
+
{
|
|
87
|
+
payload: Schema.Struct({ threadId: Schema.String }),
|
|
88
|
+
success: ThreadStreamItemSchema,
|
|
89
|
+
error: RpcError,
|
|
90
|
+
stream: true,
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
export const WsServerGetConfigRpc = Rpc.make(WS_METHODS.serverGetConfig, {
|
|
95
|
+
payload: Schema.Struct({}),
|
|
96
|
+
success: ServerConfigSchema,
|
|
97
|
+
error: RpcError,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export const WsRpcGroup = RpcGroup.make(
|
|
101
|
+
WsOrchestrationDispatchCommandRpc,
|
|
102
|
+
WsOrchestrationSubscribeShellRpc,
|
|
103
|
+
WsOrchestrationSubscribeThreadRpc,
|
|
104
|
+
WsServerGetConfigRpc,
|
|
105
|
+
);
|
package/src/rpc/error.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { HttpClientError } from "effect/unstable/http";
|
|
3
|
+
import { RpcClientError } from "effect/unstable/rpc";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AuthConfigError,
|
|
7
|
+
AuthLocalError,
|
|
8
|
+
AuthPairingUrlError,
|
|
9
|
+
AuthTransportError,
|
|
10
|
+
} from "../auth/error.ts";
|
|
11
|
+
import { ConfigError, UrlError } from "../config/error.ts";
|
|
12
|
+
|
|
13
|
+
const RpcErrorCauseSchema = Schema.Union([
|
|
14
|
+
RpcClientError.RpcClientError,
|
|
15
|
+
AuthConfigError,
|
|
16
|
+
AuthLocalError,
|
|
17
|
+
AuthPairingUrlError,
|
|
18
|
+
AuthTransportError,
|
|
19
|
+
ConfigError,
|
|
20
|
+
HttpClientError.HttpClientErrorSchema,
|
|
21
|
+
UrlError,
|
|
22
|
+
Schema.instanceOf(Schema.SchemaError),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export class RpcError extends Schema.TaggedErrorClass<RpcError>()("RpcError", {
|
|
26
|
+
message: Schema.String,
|
|
27
|
+
method: Schema.optionalKey(Schema.String),
|
|
28
|
+
cause: Schema.optionalKey(RpcErrorCauseSchema),
|
|
29
|
+
}) {}
|
|
30
|
+
|
|
31
|
+
export type OrchestrationError = RpcError;
|
package/src/rpc/layer.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Exit from "effect/Exit";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as Option from "effect/Option";
|
|
6
|
+
import * as Schedule from "effect/Schedule";
|
|
7
|
+
import * as Scope from "effect/Scope";
|
|
8
|
+
import * as SynchronizedRef from "effect/SynchronizedRef";
|
|
9
|
+
import * as NodeSocket from "@effect/platform-node/NodeSocket";
|
|
10
|
+
import { HttpClientRequest } from "effect/unstable/http";
|
|
11
|
+
import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
|
|
12
|
+
import * as Socket from "effect/unstable/socket/Socket";
|
|
13
|
+
|
|
14
|
+
import { T3Auth } from "../auth/service.ts";
|
|
15
|
+
import { T3Config } from "../config/service.ts";
|
|
16
|
+
import { toWebSocketBaseUrl } from "../config/url.ts";
|
|
17
|
+
import { WsRpcGroup } from "../protocol/schema.ts";
|
|
18
|
+
import { RpcError } from "./error.ts";
|
|
19
|
+
import { T3Rpc, type WsClient } from "./service.ts";
|
|
20
|
+
|
|
21
|
+
const makeClient = RpcClient.make(WsRpcGroup);
|
|
22
|
+
const connectionRetrySchedule = Schedule.exponential("100 millis").pipe(Schedule.take(4));
|
|
23
|
+
|
|
24
|
+
type Connection = {
|
|
25
|
+
readonly scope: Scope.Closeable;
|
|
26
|
+
readonly client: WsClient;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const makeT3RpcLayer = Effect.fn("makeT3RpcLayer")(function* () {
|
|
30
|
+
const config = yield* T3Config;
|
|
31
|
+
const auth = yield* T3Auth;
|
|
32
|
+
const parentScope = yield* Scope.Scope;
|
|
33
|
+
const connection = yield* SynchronizedRef.make(Option.none<Connection>());
|
|
34
|
+
const openConnection = Effect.fn("T3RpcLive.openConnection")(function* () {
|
|
35
|
+
const scope = yield* Scope.fork(parentScope);
|
|
36
|
+
return yield* Effect.gen(function* () {
|
|
37
|
+
const url = yield* makeWsUrl({ auth, config });
|
|
38
|
+
const protocol = yield* Layer.buildWithScope(makeProtocolLayer(url), scope);
|
|
39
|
+
const client = yield* makeClient.pipe(
|
|
40
|
+
Effect.provide(protocol),
|
|
41
|
+
Effect.provideService(Scope.Scope, scope),
|
|
42
|
+
);
|
|
43
|
+
return { scope, client } satisfies Connection;
|
|
44
|
+
}).pipe(
|
|
45
|
+
Effect.retry(connectionRetrySchedule),
|
|
46
|
+
Effect.onError(() => Scope.close(scope, Exit.void)),
|
|
47
|
+
Effect.mapError((error) => new RpcError({ message: error.message, cause: error })),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const disconnect = Effect.fn("T3RpcLive.disconnect")(function* () {
|
|
52
|
+
const current = yield* SynchronizedRef.getAndSet(connection, Option.none<Connection>());
|
|
53
|
+
if (Option.isSome(current)) {
|
|
54
|
+
yield* Scope.close(current.value.scope, Exit.void);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const getClient = Effect.fn("T3RpcLive.getClient")(function* () {
|
|
59
|
+
return yield* SynchronizedRef.modifyEffect(connection, (current) => {
|
|
60
|
+
if (Option.isSome(current)) {
|
|
61
|
+
return Effect.succeed([current.value.client, current] as const);
|
|
62
|
+
}
|
|
63
|
+
return openConnection().pipe(Effect.map((next) => [next.client, Option.some(next)] as const));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
getClient: getClient(),
|
|
69
|
+
disconnect: disconnect(),
|
|
70
|
+
reconnect: disconnect().pipe(Effect.andThen(getClient())),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const makeWsUrl = Effect.fn("makeWsUrl")(function* (input: {
|
|
75
|
+
readonly config: T3Config["Service"];
|
|
76
|
+
readonly auth: T3Auth["Service"];
|
|
77
|
+
}) {
|
|
78
|
+
const resolved = yield* input.config.resolve();
|
|
79
|
+
const wsToken = yield* input.auth.issueWebSocketToken();
|
|
80
|
+
const wsUrl = yield* toWebSocketBaseUrl(resolved.url);
|
|
81
|
+
const request = HttpClientRequest.get(wsUrl).pipe(
|
|
82
|
+
HttpClientRequest.setUrlParam("wsToken", wsToken.token),
|
|
83
|
+
);
|
|
84
|
+
return Option.getOrThrow(HttpClientRequest.toUrl(request)).toString();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function makeProtocolLayer(url: string) {
|
|
88
|
+
const socketLayer = Socket.layerWebSocket(url).pipe(
|
|
89
|
+
Layer.provide(NodeSocket.layerWebSocketConstructor),
|
|
90
|
+
);
|
|
91
|
+
return RpcClient.layerProtocolSocket({ retryTransientErrors: true }).pipe(
|
|
92
|
+
Layer.provide(socketLayer),
|
|
93
|
+
Layer.provide(RpcSerialization.layerJson),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const T3RpcLive = Layer.effectContext(
|
|
98
|
+
makeT3RpcLayer().pipe(Effect.map((rpc) => Context.make(T3Rpc, rpc))),
|
|
99
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
import type { RpcClient, RpcClientError } from "effect/unstable/rpc";
|
|
4
|
+
|
|
5
|
+
import type { WsRpcGroup } from "../protocol/schema.ts";
|
|
6
|
+
import type { RpcError } from "./error.ts";
|
|
7
|
+
|
|
8
|
+
export type WsClient = RpcClient.FromGroup<typeof WsRpcGroup, RpcClientError.RpcClientError>;
|
|
9
|
+
|
|
10
|
+
export type T3RpcService = {
|
|
11
|
+
readonly getClient: Effect.Effect<WsClient, RpcError>;
|
|
12
|
+
readonly reconnect: Effect.Effect<WsClient, RpcError>;
|
|
13
|
+
readonly disconnect: Effect.Effect<void>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class T3Rpc extends Context.Service<T3Rpc, T3RpcService>()("t3cli/T3Rpc") {}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as Layer from "effect/Layer";
|
|
2
|
+
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
|
|
3
|
+
import * as NodeSocket from "@effect/platform-node/NodeSocket";
|
|
4
|
+
|
|
5
|
+
import { T3AuthLive } from "./auth/layer.ts";
|
|
6
|
+
import { T3ConfigLive } from "./config/layer.ts";
|
|
7
|
+
import { T3ApplicationLive } from "./application/layer.ts";
|
|
8
|
+
import { T3OrchestrationLive } from "./orchestration/layer.ts";
|
|
9
|
+
import { T3RpcLive } from "./rpc/layer.ts";
|
|
10
|
+
|
|
11
|
+
const T3AuthLayer = T3AuthLive.pipe(
|
|
12
|
+
Layer.provide(Layer.mergeAll(T3ConfigLive, NodeHttpClient.layerUndici)),
|
|
13
|
+
);
|
|
14
|
+
const T3RpcLayer = T3RpcLive.pipe(
|
|
15
|
+
Layer.provide(Layer.mergeAll(T3ConfigLive, T3AuthLayer, NodeSocket.layerWebSocketConstructor)),
|
|
16
|
+
);
|
|
17
|
+
const T3OrchestrationLayer = T3OrchestrationLive.pipe(Layer.provide(T3RpcLayer));
|
|
18
|
+
const T3ApplicationLayer = T3ApplicationLive.pipe(Layer.provide(T3OrchestrationLayer));
|
|
19
|
+
|
|
20
|
+
export const AuthAppLayer = Layer.mergeAll(T3ConfigLive, T3AuthLayer);
|
|
21
|
+
|
|
22
|
+
export const AppLayer = Layer.mergeAll(
|
|
23
|
+
T3ConfigLive,
|
|
24
|
+
T3AuthLayer,
|
|
25
|
+
T3RpcLayer,
|
|
26
|
+
T3OrchestrationLayer,
|
|
27
|
+
T3ApplicationLayer,
|
|
28
|
+
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as FileSystem from "effect/FileSystem";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
|
|
7
|
+
import { T3Version } from "./service.ts";
|
|
8
|
+
|
|
9
|
+
declare const T3CLI_VERSION: string;
|
|
10
|
+
|
|
11
|
+
const PackageJsonSchema = Schema.fromJsonString(Schema.Struct({ version: Schema.String }));
|
|
12
|
+
|
|
13
|
+
export const T3VersionBundledLive = Layer.sync(T3Version, () => ({ version: T3CLI_VERSION }));
|
|
14
|
+
|
|
15
|
+
export const T3VersionPackageJsonLive = Layer.effect(
|
|
16
|
+
T3Version,
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const fs = yield* FileSystem.FileSystem;
|
|
19
|
+
const packageJson = yield* fs.readFileString(
|
|
20
|
+
fileURLToPath(new URL("../../package.json", import.meta.url)),
|
|
21
|
+
);
|
|
22
|
+
const decoded = yield* Schema.decodeUnknownEffect(PackageJsonSchema)(packageJson);
|
|
23
|
+
return { version: decoded.version };
|
|
24
|
+
}),
|
|
25
|
+
);
|