@dench.com/cli 2.8.5 → 2.8.7-staging.189

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,35 @@
1
+ /**
2
+ * Serialize pull-queue drains without dropping notifications that arrive
3
+ * while a drain is already in progress.
4
+ */
5
+ export function createPullQueueDrain(args: {
6
+ processRequests: () => Promise<void>;
7
+ onError: (error: unknown) => void;
8
+ }): () => Promise<void> {
9
+ let inFlight: Promise<void> | null = null;
10
+ let rerunRequested = false;
11
+
12
+ return function drain(): Promise<void> {
13
+ if (inFlight) {
14
+ rerunRequested = true;
15
+ return inFlight;
16
+ }
17
+
18
+ inFlight = (async () => {
19
+ try {
20
+ do {
21
+ rerunRequested = false;
22
+ try {
23
+ await args.processRequests();
24
+ } catch (error) {
25
+ args.onError(error);
26
+ }
27
+ } while (rerunRequested);
28
+ } finally {
29
+ inFlight = null;
30
+ }
31
+ })();
32
+
33
+ return inFlight;
34
+ };
35
+ }
package/fs-daemon.ts CHANGED
@@ -56,6 +56,7 @@ import {
56
56
  mkdir,
57
57
  readdir,
58
58
  readFile,
59
+ rename,
59
60
  stat,
60
61
  unlink,
61
62
  writeFile,
@@ -76,6 +77,7 @@ import {
76
77
  type MountInfo,
77
78
  readMountInfo,
78
79
  } from "./fs-daemon-mount";
80
+ import { createPullQueueDrain } from "./fs-daemon-pull-queue";
79
81
 
80
82
  type FileTreeSyncRow = {
81
83
  path: string;
@@ -731,7 +733,34 @@ async function runDaemon(daemonArgs: Args): Promise<void> {
731
733
  status.recordBreakerEvent(breakerState);
732
734
  void status.flush();
733
735
  });
736
+
737
+ // Register the pull signal before publishing the daemon status. `fs pull`
738
+ // treats that status as readiness; publishing first left a startup window
739
+ // where SIGUSR2 was lost (or terminated the daemon) and the request timed
740
+ // out. A queued rerun also prevents signals received mid-drain from being
741
+ // coalesced away.
742
+ const drainPullRequests = createPullQueueDrain({
743
+ processRequests: () =>
744
+ processPullRequests({
745
+ client,
746
+ tracker,
747
+ workspace: daemonArgs.workspace,
748
+ }),
749
+ onError: (error) =>
750
+ console.error(
751
+ `[dench-fs-daemon] pull queue failed: ${
752
+ error instanceof Error ? error.message : String(error)
753
+ }`,
754
+ ),
755
+ });
756
+ process.on("SIGUSR2", () => {
757
+ console.log(`[dench-fs-daemon] SIGUSR2 received — processing pull queue`);
758
+ void drainPullRequests();
759
+ });
734
760
  await status.flush();
761
+ // Recover requests left behind if a previous daemon exited after the
762
+ // request file was written but before it could process the signal.
763
+ void drainPullRequests();
735
764
 
736
765
  let chokidar: typeof import("chokidar");
737
766
  try {
@@ -940,21 +969,6 @@ async function runDaemon(daemonArgs: Args): Promise<void> {
940
969
  .catch(() => undefined);
941
970
  });
942
971
 
943
- process.on("SIGUSR2", () => {
944
- console.log(`[dench-fs-daemon] SIGUSR2 received — processing pull queue`);
945
- void processPullRequests({
946
- client,
947
- tracker,
948
- workspace: daemonArgs.workspace,
949
- }).catch((error) =>
950
- console.error(
951
- `[dench-fs-daemon] pull queue failed: ${
952
- error instanceof Error ? error.message : String(error)
953
- }`,
954
- ),
955
- );
956
- });
957
-
958
972
  process.on("SIGTERM", () => {
959
973
  reconciler.stop();
960
974
  unsubscribe();
@@ -1505,11 +1519,15 @@ async function runPull(argv: string[]): Promise<void> {
1505
1519
  await mkdir(PULL_REQUEST_DIR, { recursive: true });
1506
1520
  const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1507
1521
  const request: PullRequest = { id, paths, requestedAt: Date.now() };
1508
- await writeFile(
1509
- `${PULL_REQUEST_DIR}/request-${id}.json`,
1510
- JSON.stringify(request),
1511
- "utf-8",
1512
- );
1522
+ const requestPath = `${PULL_REQUEST_DIR}/request-${id}.json`;
1523
+ const temporaryRequestPath = `${requestPath}.${process.pid}.tmp`;
1524
+ try {
1525
+ await writeFile(temporaryRequestPath, JSON.stringify(request), "utf-8");
1526
+ await rename(temporaryRequestPath, requestPath);
1527
+ } catch (error) {
1528
+ await unlink(temporaryRequestPath).catch(() => undefined);
1529
+ throw error;
1530
+ }
1513
1531
  process.kill(status.pid, "SIGUSR2");
1514
1532
  if (!wait) {
1515
1533
  const response = { ok: true, id, queued: paths };
@@ -127,6 +127,14 @@ export type SearchJobsBody = {
127
127
  pageToken?: string;
128
128
  };
129
129
 
130
+ /**
131
+ * FullEnrich hard cases run 40–120s end to end; a 90s budget produced
132
+ * spurious "No data returned" failures for jobs that later completed (and
133
+ * charged). 150s covers the observed tail while the 2s interval keeps
134
+ * fast results snappy.
135
+ */
136
+ const DEFAULT_POLL_TIMEOUT_MS = 150000;
137
+
130
138
  export type EnrichmentGatewayOptions = {
131
139
  apiKey?: string;
132
140
  baseUrl?: string;
@@ -134,7 +142,7 @@ export type EnrichmentGatewayOptions = {
134
142
  fetchImpl?: FetchLike;
135
143
  /** Override the poll interval (ms) for testing. Default: 2000. */
136
144
  pollIntervalMs?: number;
137
- /** Override the poll timeout (ms) for testing. Default: 30000. */
145
+ /** Override the poll timeout (ms) for testing. Default: 150000. */
138
146
  pollTimeoutMs?: number;
139
147
  /**
140
148
  * Optional abort signal — propagated to fetch and short-circuits the polling
@@ -448,9 +456,7 @@ async function pollForPersonResult(
448
456
  options: EnrichmentGatewayOptions,
449
457
  ): Promise<Record<string, unknown>> {
450
458
  const pollIntervalMs = options.pollIntervalMs ?? 2000;
451
- // Default 90s: FullEnrich jobs for hard-to-find contacts can take 40–120s.
452
- // 30s was too short and caused spurious "No data returned" FatalErrors.
453
- const pollTimeoutMs = options.pollTimeoutMs ?? 90000;
459
+ const pollTimeoutMs = options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
454
460
  const deadline = Date.now() + pollTimeoutMs;
455
461
 
456
462
  while (Date.now() < deadline) {
@@ -640,7 +646,7 @@ async function pollForBulkPersonResults(
640
646
  options: EnrichmentGatewayOptions,
641
647
  ): Promise<void> {
642
648
  const pollIntervalMs = options.pollIntervalMs ?? 2000;
643
- const pollTimeoutMs = options.pollTimeoutMs ?? 90000;
649
+ const pollTimeoutMs = options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
644
650
  const deadline = Date.now() + pollTimeoutMs;
645
651
 
646
652
  // Slots that this chunk is responsible for — anything still missing after
@@ -882,7 +888,7 @@ async function pollForReverseEmailResults(
882
888
  options: EnrichmentGatewayOptions,
883
889
  ): Promise<void> {
884
890
  const pollIntervalMs = options.pollIntervalMs ?? 2000;
885
- const pollTimeoutMs = options.pollTimeoutMs ?? 90000;
891
+ const pollTimeoutMs = options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
886
892
  const deadline = Date.now() + pollTimeoutMs;
887
893
 
888
894
  const ownedIndexes = emails
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dench.com/cli",
3
- "version": "2.8.5",
3
+ "version": "2.8.7-staging.189",
4
4
  "description": "Dench agent workspace CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  "fs-daemon-binary.ts",
18
18
  "fs-daemon-flush.ts",
19
19
  "fs-daemon-mount.ts",
20
+ "fs-daemon-pull-queue.ts",
20
21
  "home-sync",
21
22
  "home-sync.ts",
22
23
  "home-sync-lib.ts",