@danypops/pi-lector 0.1.11 → 0.1.12
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/extension/src/lector-client.ts +16 -42
- package/package.json +2 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dirname, parse } from "node:path";
|
|
2
|
+
import { createRetryingClient, type RetryingClient } from "@danypops/daemon-kit/pi-client";
|
|
2
3
|
import {
|
|
3
4
|
connectLectorClient,
|
|
4
5
|
type LectorClient,
|
|
@@ -20,60 +21,33 @@ import { nearestGitRoot } from "./nearest-workspace-root.ts";
|
|
|
20
21
|
*
|
|
21
22
|
* The daemon binds a new random port on every restart. A client resolved
|
|
22
23
|
* once and cached for the rest of the session would otherwise point at a
|
|
23
|
-
* dead port after any later restart --
|
|
24
|
+
* dead port after any later restart -- daemon-kit's createRetryingClient
|
|
24
25
|
* detects that on the failing call itself (not just the first connection
|
|
25
|
-
* attempt) and retries once against a freshly re-resolved client,
|
|
26
|
-
*
|
|
26
|
+
* attempt) and retries once against a freshly re-resolved client, the same
|
|
27
|
+
* policy this file used to hand-roll and now shares with web-spider's
|
|
28
|
+
* callWebSpider(), papyrus's callService(), and pi-packed's createNatives().
|
|
27
29
|
*/
|
|
28
30
|
|
|
29
31
|
type ClientConnector = () => Promise<LectorClient>;
|
|
30
32
|
|
|
31
33
|
let connector: ClientConnector = () => connectLectorClient();
|
|
32
|
-
|
|
34
|
+
// Wraps `() => connector()` rather than `connector` itself, so a test's
|
|
35
|
+
// setLectorClientConnectorForTests still takes effect after this retrying
|
|
36
|
+
// client is constructed once at module load.
|
|
37
|
+
const retryingClient: RetryingClient<LectorClient> = createRetryingClient(() => connector(), { label: "Lector" });
|
|
33
38
|
const workspaceIdByRoot = new Map<string, WorkspaceId>();
|
|
34
39
|
|
|
35
|
-
async function resolveClient(): Promise<LectorClient> {
|
|
36
|
-
if (!cachedClient) {
|
|
37
|
-
cachedClient = connector().catch((error: unknown) => {
|
|
38
|
-
cachedClient = undefined;
|
|
39
|
-
throw error;
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return cachedClient;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* True when `error` means the connection itself is bad (the daemon
|
|
47
|
-
* restarted on a new port since this client was cached, or died outright)
|
|
48
|
-
* -- worth invalidating the cache and retrying once. False for a genuine
|
|
49
|
-
* domain-level rejection (e.g. UnknownWorkspace), which a retry cannot fix
|
|
50
|
-
* and would only mask.
|
|
51
|
-
*/
|
|
52
|
-
function isStaleConnectionError(error: unknown): boolean {
|
|
53
|
-
if (error instanceof TypeError) return true; // fetch()'s own connection-refused/DNS-failure shape
|
|
54
|
-
if (!(error instanceof Error)) return false;
|
|
55
|
-
if (error.name === "AbortError" || error.name === "TimeoutError") return true;
|
|
56
|
-
return /fetch failed|unable to connect|network|socket|ECONNRESET|ECONNREFUSED|connection refused/i.test(error.message);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
40
|
export interface RetryingLectorClient {
|
|
60
41
|
call<Name extends OperationName>(operation: Name, input: OperationInputs[Name]): Promise<OperationOutputs[Name]>;
|
|
61
42
|
}
|
|
62
43
|
|
|
44
|
+
// Kept async even though its own body has no await: every call site across this package does
|
|
45
|
+
// `await lectorClient()`, and dropping async here (just to satisfy require-await) would turn an
|
|
46
|
+
// internal implementation detail into a signature change rippling through every one of them.
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
63
48
|
export async function lectorClient(): Promise<RetryingLectorClient> {
|
|
64
49
|
return {
|
|
65
|
-
|
|
66
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
67
|
-
const client = await resolveClient();
|
|
68
|
-
try {
|
|
69
|
-
return await client.call(operation, input);
|
|
70
|
-
} catch (error) {
|
|
71
|
-
cachedClient = undefined;
|
|
72
|
-
if (attempt === 1 || !isStaleConnectionError(error)) throw error;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
throw new Error("Lector daemon client retry exhausted");
|
|
76
|
-
},
|
|
50
|
+
call: (operation, input) => retryingClient.call((client) => client.call(operation, input)),
|
|
77
51
|
};
|
|
78
52
|
}
|
|
79
53
|
|
|
@@ -163,13 +137,13 @@ export async function withWorkspace<T>(resolve: () => Promise<ResolvedWorkspace>
|
|
|
163
137
|
}
|
|
164
138
|
|
|
165
139
|
export function setLectorClientConnectorForTests(value: ClientConnector): void {
|
|
166
|
-
|
|
140
|
+
retryingClient.reset();
|
|
167
141
|
workspaceIdByRoot.clear();
|
|
168
142
|
connector = value;
|
|
169
143
|
}
|
|
170
144
|
|
|
171
145
|
export function resetLectorClientForTests(): void {
|
|
172
|
-
|
|
146
|
+
retryingClient.reset();
|
|
173
147
|
workspaceIdByRoot.clear();
|
|
174
148
|
connector = () => connectLectorClient();
|
|
175
149
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Pi host adapter for Lector: overrides read/write/edit with a daemon-backed, hash-guarded filesystem",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"typebox": "*"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"@danypops/daemon-kit": "^0.4.0",
|
|
21
22
|
"@danypops/lector": "^0.1.10"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|