tokmon 0.23.5 → 0.24.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.
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ TOKMON_WS_METHODS,
4
+ TOKMON_WS_PATH,
5
+ TokmonRpcGroup
6
+ } from "./chunk-O27I2XFN.js";
7
+
8
+ // src/client/daemon-rpc-client.ts
9
+ import { Cause, Context, Duration, Effect, Exit, Fiber, Layer, ManagedRuntime, Schedule, Stream } from "effect";
10
+ import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
11
+ import * as Socket from "effect/unstable/socket/Socket";
12
+ var TokmonRpcClient = class extends Context.Service()(
13
+ "tokmon/client/DaemonRpcClient/TokmonRpcClient"
14
+ ) {
15
+ };
16
+ var dynamicImport = new Function("specifier", "return import(specifier)");
17
+ function toWsUrl(baseUrl) {
18
+ const base = typeof window === "undefined" ? void 0 : window.location.origin;
19
+ const url = new URL(baseUrl, base);
20
+ if (url.protocol === "http:") url.protocol = "ws:";
21
+ else if (url.protocol === "https:") url.protocol = "wss:";
22
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") {
23
+ throw new Error(`unsupported daemon RPC protocol: ${url.protocol}`);
24
+ }
25
+ url.hash = "";
26
+ url.searchParams.delete("tokmonToken");
27
+ url.searchParams.delete("wsToken");
28
+ url.pathname = TOKMON_WS_PATH;
29
+ return url.toString();
30
+ }
31
+ function shouldUseNodeTransport(transport) {
32
+ if (transport === "node") return true;
33
+ if (transport === "browser") return false;
34
+ return typeof window === "undefined";
35
+ }
36
+ async function socketLayerFor(url, transport) {
37
+ if (shouldUseNodeTransport(transport)) {
38
+ const NodeSocket = await dynamicImport("@effect/platform-node/NodeSocket");
39
+ return NodeSocket.layerWebSocket(url);
40
+ }
41
+ return Socket.layerWebSocket(url).pipe(
42
+ Layer.provide(Socket.layerWebSocketConstructorGlobal)
43
+ );
44
+ }
45
+ function retryPolicy(options) {
46
+ const baseDelay = options.reconnectBaseDelayMs ?? 250;
47
+ const policy = Schedule.exponential(Duration.millis(baseDelay), 1.5).pipe(
48
+ Schedule.either(Schedule.spaced(Duration.millis(2500)))
49
+ );
50
+ return typeof options.reconnectAttempts === "number" ? policy.pipe(Schedule.both(Schedule.recurs(options.reconnectAttempts))) : policy;
51
+ }
52
+ function createDaemonRpcClient(baseUrl, options = {}) {
53
+ const url = toWsUrl(baseUrl);
54
+ const fibers = /* @__PURE__ */ new Set();
55
+ let session = null;
56
+ let sessionPromise = null;
57
+ let closed = false;
58
+ const setConn = (state, error) => {
59
+ options.onConn?.(state, error);
60
+ };
61
+ const resetSession = (active) => {
62
+ if (active !== void 0 && active !== null && active !== session) {
63
+ void active.runtime.dispose().catch(() => {
64
+ });
65
+ return;
66
+ }
67
+ const dead = active ?? session;
68
+ session = null;
69
+ sessionPromise = null;
70
+ if (dead) void dead.runtime.dispose().catch(() => {
71
+ });
72
+ };
73
+ const makeProtocolLayer = async () => {
74
+ const socketLayer = await socketLayerFor(url, options.transport);
75
+ const connectionHooksLayer = Layer.succeed(
76
+ RpcClient.ConnectionHooks,
77
+ RpcClient.ConnectionHooks.of({
78
+ onConnect: Effect.sync(() => {
79
+ setConn("live");
80
+ }),
81
+ onDisconnect: Effect.sync(() => {
82
+ if (!closed) {
83
+ setConn("reconnecting");
84
+ }
85
+ })
86
+ })
87
+ );
88
+ return Layer.effect(
89
+ RpcClient.Protocol,
90
+ RpcClient.makeProtocolSocket({
91
+ retryPolicy: retryPolicy(options),
92
+ retryTransientErrors: true
93
+ })
94
+ ).pipe(
95
+ Layer.provide(Layer.mergeAll(socketLayer, RpcSerialization.layerJson, connectionHooksLayer))
96
+ );
97
+ };
98
+ const ensureSession = async () => {
99
+ if (closed) throw new Error("daemon RPC client is closed");
100
+ if (session) return session;
101
+ if (sessionPromise) return sessionPromise;
102
+ setConn("connecting");
103
+ sessionPromise = (async () => {
104
+ let runtime;
105
+ try {
106
+ const protocolLayer = await makeProtocolLayer();
107
+ const clientLayer = Layer.effect(
108
+ TokmonRpcClient,
109
+ RpcClient.make(TokmonRpcGroup)
110
+ ).pipe(
111
+ Layer.provide(protocolLayer)
112
+ );
113
+ runtime = ManagedRuntime.make(clientLayer);
114
+ await runtime.runPromise(TokmonRpcClient.asEffect());
115
+ if (closed) {
116
+ await runtime.dispose();
117
+ throw new Error("daemon RPC client is closed");
118
+ }
119
+ session = { runtime };
120
+ return session;
121
+ } catch (error) {
122
+ sessionPromise = null;
123
+ await runtime?.dispose().catch(() => {
124
+ });
125
+ if (!closed) setConn("error", error);
126
+ throw error;
127
+ }
128
+ })();
129
+ return sessionPromise;
130
+ };
131
+ const run = async (effectFor) => {
132
+ const active = await ensureSession();
133
+ try {
134
+ return await active.runtime.runPromise(
135
+ TokmonRpcClient.use((client) => effectFor(client))
136
+ );
137
+ } catch (error) {
138
+ if (!closed) {
139
+ resetSession(active);
140
+ setConn("error", error);
141
+ }
142
+ throw error;
143
+ }
144
+ };
145
+ const subscribe = (streamFor, onValue, staleAfterFor) => {
146
+ if (closed) return () => {
147
+ };
148
+ let fiber = null;
149
+ let unsubscribed = false;
150
+ let retryTimer = null;
151
+ let watchdogTimer = null;
152
+ let lastValueAt = 0;
153
+ let staleAfterMs = Number.POSITIVE_INFINITY;
154
+ let watchdogRestart = false;
155
+ const stopFiber = () => {
156
+ if (!fiber) return;
157
+ const current = fiber;
158
+ fiber = null;
159
+ fibers.delete(current);
160
+ void (session?.runtime.runPromise(Fiber.interrupt(current)) ?? Effect.runPromise(Fiber.interrupt(current))).catch(() => {
161
+ });
162
+ };
163
+ const scheduleRetry = () => {
164
+ if (closed || unsubscribed || retryTimer) return;
165
+ retryTimer = setTimeout(() => {
166
+ retryTimer = null;
167
+ start();
168
+ }, options.reconnectBaseDelayMs ?? 250);
169
+ retryTimer.unref?.();
170
+ };
171
+ if (staleAfterFor) {
172
+ const checkEveryMs = Math.min(5e3, Math.max(10, (options.snapshotStaleFloorMs ?? 9e4) / 2));
173
+ watchdogTimer = setInterval(() => {
174
+ if (!fiber || lastValueAt === 0 || Date.now() - lastValueAt <= staleAfterMs) return;
175
+ watchdogRestart = true;
176
+ lastValueAt = Date.now();
177
+ setConn("reconnecting");
178
+ stopFiber();
179
+ }, checkEveryMs);
180
+ watchdogTimer.unref?.();
181
+ }
182
+ const start = () => {
183
+ void (async () => {
184
+ try {
185
+ const active = await ensureSession();
186
+ if (closed || unsubscribed) return;
187
+ lastValueAt = Date.now();
188
+ staleAfterMs = options.snapshotStaleFloorMs ?? 9e4;
189
+ const currentFiber = active.runtime.runFork(
190
+ TokmonRpcClient.use(
191
+ (client) => streamFor(client).pipe(
192
+ Stream.runForEach(
193
+ (value) => Effect.sync(() => {
194
+ lastValueAt = Date.now();
195
+ if (staleAfterFor) staleAfterMs = staleAfterFor(value);
196
+ try {
197
+ onValue(value);
198
+ } catch {
199
+ }
200
+ })
201
+ )
202
+ )
203
+ )
204
+ );
205
+ fiber = currentFiber;
206
+ fibers.add(currentFiber);
207
+ currentFiber.addObserver((exit) => {
208
+ fibers.delete(currentFiber);
209
+ if (fiber === currentFiber) fiber = null;
210
+ if (closed || unsubscribed) return;
211
+ const stale = watchdogRestart;
212
+ watchdogRestart = false;
213
+ resetSession(active);
214
+ if (stale || Exit.isSuccess(exit)) setConn("reconnecting");
215
+ else setConn("error", Cause.squash(exit.cause));
216
+ scheduleRetry();
217
+ });
218
+ } catch (error) {
219
+ if (!closed && !unsubscribed) {
220
+ resetSession();
221
+ setConn("error", error);
222
+ scheduleRetry();
223
+ }
224
+ }
225
+ })();
226
+ };
227
+ start();
228
+ return () => {
229
+ unsubscribed = true;
230
+ if (retryTimer) {
231
+ clearTimeout(retryTimer);
232
+ retryTimer = null;
233
+ }
234
+ if (watchdogTimer) {
235
+ clearInterval(watchdogTimer);
236
+ watchdogTimer = null;
237
+ }
238
+ stopFiber();
239
+ };
240
+ };
241
+ return {
242
+ getConfig: () => run((client) => client[TOKMON_WS_METHODS.getConfig]({})).then((state) => state),
243
+ setConfig: (config) => run((client) => client[TOKMON_WS_METHODS.setConfig](config)).then((state) => state),
244
+ refresh: (scope = "all") => run((client) => client[TOKMON_WS_METHODS.refresh]({ scope })),
245
+ browseFs: (path) => run((client) => client[TOKMON_WS_METHODS.browseFs]({ path })),
246
+ subscribeSnapshot: (onSnapshot) => subscribe(
247
+ (client) => client[TOKMON_WS_METHODS.snapshot]({}).pipe(Stream.map((value) => value)),
248
+ onSnapshot,
249
+ (snapshot) => Math.max(options.snapshotStaleFloorMs ?? 9e4, snapshot.intervalMs * 3)
250
+ ),
251
+ subscribeConfig: (onConfig) => subscribe((client) => client[TOKMON_WS_METHODS.config]({}).pipe(Stream.map((value) => value)), onConfig),
252
+ async close() {
253
+ if (closed) return;
254
+ closed = true;
255
+ setConn("closed");
256
+ const active = [...fibers];
257
+ fibers.clear();
258
+ const activeSession = session ?? await sessionPromise?.catch(() => null) ?? null;
259
+ await Promise.all(active.map(
260
+ (fiber) => (activeSession?.runtime.runPromise(Fiber.interrupt(fiber)) ?? Effect.runPromise(Fiber.interrupt(fiber))).catch(() => {
261
+ })
262
+ ));
263
+ session = null;
264
+ sessionPromise = null;
265
+ if (activeSession) {
266
+ await activeSession.runtime.dispose().catch(() => {
267
+ });
268
+ }
269
+ }
270
+ };
271
+ }
272
+
273
+ export {
274
+ createDaemonRpcClient
275
+ };