@f0rbit/overview 0.1.0 → 0.2.1
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/bin/overview +10 -0
- package/dist/overview.js +10361 -0
- package/package.json +22 -15
- package/bunfig.toml +0 -7
- package/packages/core/__tests__/concurrency.test.ts +0 -111
- package/packages/core/__tests__/helpers.ts +0 -60
- package/packages/core/__tests__/integration/git-status.test.ts +0 -62
- package/packages/core/__tests__/integration/scanner.test.ts +0 -140
- package/packages/core/__tests__/ocn.test.ts +0 -164
- package/packages/core/package.json +0 -13
- package/packages/core/src/cache.ts +0 -31
- package/packages/core/src/concurrency.ts +0 -44
- package/packages/core/src/devpad.ts +0 -61
- package/packages/core/src/git-graph.ts +0 -54
- package/packages/core/src/git-stats.ts +0 -201
- package/packages/core/src/git-status.ts +0 -316
- package/packages/core/src/github.ts +0 -286
- package/packages/core/src/index.ts +0 -58
- package/packages/core/src/ocn.ts +0 -74
- package/packages/core/src/scanner.ts +0 -118
- package/packages/core/src/types.ts +0 -199
- package/packages/core/src/watcher.ts +0 -128
- package/packages/core/src/worktree.ts +0 -80
- package/packages/core/tsconfig.json +0 -5
- package/packages/render/bunfig.toml +0 -8
- package/packages/render/jsx-runtime.d.ts +0 -3
- package/packages/render/package.json +0 -18
- package/packages/render/src/components/__tests__/scrollbox-height.test.tsx +0 -780
- package/packages/render/src/components/__tests__/widget-container.integration.test.tsx +0 -304
- package/packages/render/src/components/git-graph.tsx +0 -127
- package/packages/render/src/components/help-overlay.tsx +0 -108
- package/packages/render/src/components/index.ts +0 -7
- package/packages/render/src/components/repo-list.tsx +0 -127
- package/packages/render/src/components/stats-panel.tsx +0 -116
- package/packages/render/src/components/status-badge.tsx +0 -70
- package/packages/render/src/components/status-bar.tsx +0 -56
- package/packages/render/src/components/widget-container.tsx +0 -286
- package/packages/render/src/components/widgets/__tests__/widget-rendering.test.tsx +0 -326
- package/packages/render/src/components/widgets/branch-list.tsx +0 -93
- package/packages/render/src/components/widgets/commit-activity.tsx +0 -112
- package/packages/render/src/components/widgets/devpad-milestones.tsx +0 -88
- package/packages/render/src/components/widgets/devpad-tasks.tsx +0 -81
- package/packages/render/src/components/widgets/file-changes.tsx +0 -78
- package/packages/render/src/components/widgets/git-status.tsx +0 -125
- package/packages/render/src/components/widgets/github-ci.tsx +0 -98
- package/packages/render/src/components/widgets/github-issues.tsx +0 -101
- package/packages/render/src/components/widgets/github-prs.tsx +0 -119
- package/packages/render/src/components/widgets/github-release.tsx +0 -73
- package/packages/render/src/components/widgets/index.ts +0 -12
- package/packages/render/src/components/widgets/recent-commits.tsx +0 -64
- package/packages/render/src/components/widgets/registry.ts +0 -23
- package/packages/render/src/components/widgets/repo-meta.tsx +0 -80
- package/packages/render/src/config/index.ts +0 -104
- package/packages/render/src/lib/__tests__/fetch-context.test.ts +0 -200
- package/packages/render/src/lib/__tests__/widget-grid.test.ts +0 -665
- package/packages/render/src/lib/actions.ts +0 -68
- package/packages/render/src/lib/fetch-context.ts +0 -102
- package/packages/render/src/lib/filter.ts +0 -94
- package/packages/render/src/lib/format.ts +0 -36
- package/packages/render/src/lib/use-devpad.ts +0 -167
- package/packages/render/src/lib/use-github.ts +0 -75
- package/packages/render/src/lib/widget-grid.ts +0 -204
- package/packages/render/src/lib/widget-state.ts +0 -96
- package/packages/render/src/overview.tsx +0 -16
- package/packages/render/src/screens/index.ts +0 -1
- package/packages/render/src/screens/main-screen.tsx +0 -410
- package/packages/render/src/theme/index.ts +0 -37
- package/packages/render/tsconfig.json +0 -9
- package/tsconfig.json +0 -23
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { For, Show } from "solid-js";
|
|
2
|
-
import type { WidgetRenderProps, RepoStatus } from "@overview/core";
|
|
3
|
-
import { registerWidget } from "./registry";
|
|
4
|
-
import { theme } from "../../theme";
|
|
5
|
-
import { truncate, formatRelativeTime } from "../../lib/format";
|
|
6
|
-
|
|
7
|
-
const size_hint = { span: "half" as const, min_height: 2 };
|
|
8
|
-
const MAX_VISIBLE = 8;
|
|
9
|
-
|
|
10
|
-
function RecentCommitsWidget(props: WidgetRenderProps & { status: RepoStatus | null }) {
|
|
11
|
-
const all_commits = () => props.status?.recent_commits ?? [];
|
|
12
|
-
const visible_commits = () => all_commits().slice(0, MAX_VISIBLE);
|
|
13
|
-
const overflow = () => Math.max(0, all_commits().length - MAX_VISIBLE);
|
|
14
|
-
|
|
15
|
-
const format_line = (commit: { hash: string; message: string; time: number }) => {
|
|
16
|
-
const hash_short = commit.hash.slice(0, 7);
|
|
17
|
-
const time_str = " " + formatRelativeTime(commit.time);
|
|
18
|
-
const available = props.width - hash_short.length - 1 - time_str.length;
|
|
19
|
-
const msg = truncate(commit.message, Math.max(1, available));
|
|
20
|
-
return { hash_short, msg, time_str };
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<box flexDirection="column">
|
|
25
|
-
<box height={1}>
|
|
26
|
-
<text fg={theme.fg_dark} content="Recent Commits" />
|
|
27
|
-
</box>
|
|
28
|
-
<Show
|
|
29
|
-
when={(props.status?.recent_commits?.length ?? 0) > 0}
|
|
30
|
-
fallback={
|
|
31
|
-
<box height={1}>
|
|
32
|
-
<text fg={theme.fg_dim} content="(no commits)" />
|
|
33
|
-
</box>
|
|
34
|
-
}
|
|
35
|
-
>
|
|
36
|
-
<For each={visible_commits()}>
|
|
37
|
-
{(commit) => {
|
|
38
|
-
const line = () => format_line(commit);
|
|
39
|
-
return (
|
|
40
|
-
<box flexDirection="row" height={1}>
|
|
41
|
-
<text fg={theme.yellow} content={line().hash_short} />
|
|
42
|
-
<text content={" "} />
|
|
43
|
-
<text content={line().msg} />
|
|
44
|
-
<text fg={theme.fg_dim} content={line().time_str} />
|
|
45
|
-
</box>
|
|
46
|
-
);
|
|
47
|
-
}}
|
|
48
|
-
</For>
|
|
49
|
-
<Show when={overflow() > 0}>
|
|
50
|
-
<text fg={theme.fg_dim} content={`+${overflow()} more`} />
|
|
51
|
-
</Show>
|
|
52
|
-
</Show>
|
|
53
|
-
</box>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
registerWidget({
|
|
58
|
-
id: "recent-commits",
|
|
59
|
-
label: "Recent Commits",
|
|
60
|
-
size_hint,
|
|
61
|
-
component: RecentCommitsWidget,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
export { RecentCommitsWidget };
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { Component } from "solid-js";
|
|
2
|
-
import type { WidgetId, WidgetSizeHint, WidgetRenderProps, RepoStatus } from "@overview/core";
|
|
3
|
-
|
|
4
|
-
export interface WidgetDefinition {
|
|
5
|
-
id: WidgetId;
|
|
6
|
-
label: string;
|
|
7
|
-
size_hint: WidgetSizeHint;
|
|
8
|
-
component: Component<WidgetRenderProps & { status: RepoStatus | null }>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const registry = new Map<WidgetId, WidgetDefinition>();
|
|
12
|
-
|
|
13
|
-
export function registerWidget(def: WidgetDefinition): void {
|
|
14
|
-
registry.set(def.id, def);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function getWidget(id: WidgetId): WidgetDefinition | undefined {
|
|
18
|
-
return registry.get(id);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function getAllWidgets(): WidgetDefinition[] {
|
|
22
|
-
return Array.from(registry.values());
|
|
23
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Show } from "solid-js";
|
|
2
|
-
import type { WidgetRenderProps, RepoStatus } from "@overview/core";
|
|
3
|
-
import { registerWidget } from "./registry";
|
|
4
|
-
import { theme } from "../../theme";
|
|
5
|
-
import { formatBytes } from "../../lib/format";
|
|
6
|
-
|
|
7
|
-
const size_hint = { span: "third" as const, min_height: 2 };
|
|
8
|
-
|
|
9
|
-
const SEMVER_RE = /^v?\d+\.\d+/;
|
|
10
|
-
|
|
11
|
-
function RepoMetaWidget(props: WidgetRenderProps & { status: RepoStatus | null }) {
|
|
12
|
-
const s = () => props.status;
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<box flexDirection="column">
|
|
16
|
-
<Show
|
|
17
|
-
when={s()}
|
|
18
|
-
fallback={
|
|
19
|
-
<text fg={theme.fg_dim} content="(no data)" />
|
|
20
|
-
}
|
|
21
|
-
>
|
|
22
|
-
{(status) => {
|
|
23
|
-
const latest_tag = () => status().tags[0] ?? null;
|
|
24
|
-
const tag_is_semver = () => {
|
|
25
|
-
const tag = latest_tag();
|
|
26
|
-
return tag !== null && SEMVER_RE.test(tag);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<>
|
|
31
|
-
{/* Row 1: commits + contributors */}
|
|
32
|
-
<box flexDirection="row" height={1} gap={2}>
|
|
33
|
-
<box flexDirection="row" gap={1}>
|
|
34
|
-
<text fg={theme.yellow} content={`${status().total_commits}`} />
|
|
35
|
-
<text fg={theme.fg_dim} content="commits" />
|
|
36
|
-
</box>
|
|
37
|
-
<box flexDirection="row" gap={1}>
|
|
38
|
-
<text fg={theme.yellow} content={`${status().contributor_count}`} />
|
|
39
|
-
<text fg={theme.fg_dim} content="contributors" />
|
|
40
|
-
</box>
|
|
41
|
-
</box>
|
|
42
|
-
|
|
43
|
-
{/* Row 2: repo size + tag count */}
|
|
44
|
-
<box flexDirection="row" height={1} gap={2}>
|
|
45
|
-
<box flexDirection="row" gap={1}>
|
|
46
|
-
<text fg={theme.yellow} content={formatBytes(status().repo_size_bytes)} />
|
|
47
|
-
<text fg={theme.fg_dim} content="on disk" />
|
|
48
|
-
</box>
|
|
49
|
-
<box flexDirection="row" gap={1}>
|
|
50
|
-
<text fg={theme.yellow} content={`${status().tags.length}`} />
|
|
51
|
-
<text fg={theme.fg_dim} content="tags" />
|
|
52
|
-
</box>
|
|
53
|
-
</box>
|
|
54
|
-
|
|
55
|
-
{/* Row 3: latest tag */}
|
|
56
|
-
<Show when={latest_tag()}>
|
|
57
|
-
<box flexDirection="row" height={1} gap={1}>
|
|
58
|
-
<text fg={theme.fg_dim} content="latest:" />
|
|
59
|
-
<text
|
|
60
|
-
fg={tag_is_semver() ? theme.green : theme.fg}
|
|
61
|
-
content={latest_tag()!}
|
|
62
|
-
/>
|
|
63
|
-
</box>
|
|
64
|
-
</Show>
|
|
65
|
-
</>
|
|
66
|
-
);
|
|
67
|
-
}}
|
|
68
|
-
</Show>
|
|
69
|
-
</box>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
registerWidget({
|
|
74
|
-
id: "repo-meta",
|
|
75
|
-
label: "Repo Meta",
|
|
76
|
-
size_hint,
|
|
77
|
-
component: RepoMetaWidget,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
export { RepoMetaWidget };
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { ok, err, merge_deep, type Result, type DeepPartial } from "@f0rbit/corpus";
|
|
2
|
-
import { type OverviewConfig, defaultConfig } from "@overview/core";
|
|
3
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
|
|
7
|
-
export type ConfigError =
|
|
8
|
-
| { kind: "parse_error"; path: string; cause: string }
|
|
9
|
-
| { kind: "write_error"; path: string; cause: string };
|
|
10
|
-
|
|
11
|
-
const CONFIG_DIR = join(homedir(), ".config", "overview");
|
|
12
|
-
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
13
|
-
|
|
14
|
-
const expandTilde = (p: string): string =>
|
|
15
|
-
p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
|
|
16
|
-
|
|
17
|
-
const expandPaths = (config: OverviewConfig): OverviewConfig => ({
|
|
18
|
-
...config,
|
|
19
|
-
scan_dirs: config.scan_dirs.map(expandTilde),
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const isEnoent = (e: unknown): boolean =>
|
|
23
|
-
e instanceof Error && "code" in e && (e as NodeJS.ErrnoException).code === "ENOENT";
|
|
24
|
-
|
|
25
|
-
export async function loadConfig(): Promise<Result<OverviewConfig, ConfigError>> {
|
|
26
|
-
let raw: string;
|
|
27
|
-
try {
|
|
28
|
-
raw = await readFile(CONFIG_PATH, "utf-8");
|
|
29
|
-
} catch (e) {
|
|
30
|
-
if (isEnoent(e)) return ok(expandPaths(defaultConfig()));
|
|
31
|
-
return err({ kind: "parse_error", path: CONFIG_PATH, cause: String(e) });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
let parsed: DeepPartial<OverviewConfig>;
|
|
35
|
-
try {
|
|
36
|
-
parsed = JSON.parse(raw);
|
|
37
|
-
} catch (e) {
|
|
38
|
-
return err({ kind: "parse_error", path: CONFIG_PATH, cause: `Invalid JSON: ${e}` });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const merged = merge_deep(defaultConfig() as unknown as Record<string, unknown>, parsed as unknown as Record<string, unknown>) as unknown as OverviewConfig;
|
|
42
|
-
return ok(expandPaths(merged));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function writeDefaultConfig(): Promise<Result<void, ConfigError>> {
|
|
46
|
-
try {
|
|
47
|
-
await readFile(CONFIG_PATH, "utf-8");
|
|
48
|
-
return ok(undefined);
|
|
49
|
-
} catch (e) {
|
|
50
|
-
if (!isEnoent(e)) return err({ kind: "write_error", path: CONFIG_PATH, cause: String(e) });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
await mkdir(CONFIG_DIR, { recursive: true });
|
|
55
|
-
await writeFile(CONFIG_PATH, JSON.stringify(defaultConfig(), null, 2) + "\n", "utf-8");
|
|
56
|
-
return ok(undefined);
|
|
57
|
-
} catch (e) {
|
|
58
|
-
return err({ kind: "write_error", path: CONFIG_PATH, cause: String(e) });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface CliArgs {
|
|
63
|
-
dir?: string;
|
|
64
|
-
depth?: number;
|
|
65
|
-
sort?: "name" | "status" | "last-commit";
|
|
66
|
-
filter?: "all" | "dirty" | "clean" | "ahead" | "behind";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function parseCliArgs(argv: string[]): CliArgs {
|
|
70
|
-
const args = argv.slice(2);
|
|
71
|
-
const result: CliArgs = {};
|
|
72
|
-
|
|
73
|
-
for (let i = 0; i < args.length; i++) {
|
|
74
|
-
const flag = args[i];
|
|
75
|
-
const next = args[i + 1];
|
|
76
|
-
|
|
77
|
-
if ((flag === "--dir" || flag === "-d") && next) {
|
|
78
|
-
result.dir = next;
|
|
79
|
-
i++;
|
|
80
|
-
} else if (flag === "--depth" && next) {
|
|
81
|
-
result.depth = parseInt(next, 10);
|
|
82
|
-
i++;
|
|
83
|
-
} else if (flag === "--sort" && next) {
|
|
84
|
-
result.sort = next as CliArgs["sort"];
|
|
85
|
-
i++;
|
|
86
|
-
} else if (flag === "--filter" && next) {
|
|
87
|
-
result.filter = next as CliArgs["filter"];
|
|
88
|
-
i++;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return result;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function mergeCliArgs(config: OverviewConfig, args: CliArgs): OverviewConfig {
|
|
96
|
-
const result = { ...config };
|
|
97
|
-
|
|
98
|
-
if (args.dir !== undefined) result.scan_dirs = [expandTilde(args.dir)];
|
|
99
|
-
if (args.depth !== undefined) result.depth = args.depth;
|
|
100
|
-
if (args.sort !== undefined) result.sort = args.sort;
|
|
101
|
-
if (args.filter !== undefined) result.filter = args.filter;
|
|
102
|
-
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { createFetchContext, InFlightDedup } from "../fetch-context";
|
|
3
|
-
|
|
4
|
-
describe("createFetchContext", () => {
|
|
5
|
-
test("trigger fires after delay", async () => {
|
|
6
|
-
let result: string | null = null;
|
|
7
|
-
const ctx = createFetchContext<string>(50, (value) => {
|
|
8
|
-
result = value;
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
ctx.trigger(() => Promise.resolve("hello"));
|
|
12
|
-
|
|
13
|
-
// Not yet fired
|
|
14
|
-
expect(result).toBeNull();
|
|
15
|
-
|
|
16
|
-
// Wait for debounce
|
|
17
|
-
await Bun.sleep(80);
|
|
18
|
-
|
|
19
|
-
expect(result).toBe("hello");
|
|
20
|
-
ctx.dispose();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("rapid triggers only fire the last one", async () => {
|
|
24
|
-
const results: string[] = [];
|
|
25
|
-
const ctx = createFetchContext<string>(50, (value) => {
|
|
26
|
-
results.push(value);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
ctx.trigger(() => Promise.resolve("first"));
|
|
30
|
-
ctx.trigger(() => Promise.resolve("second"));
|
|
31
|
-
ctx.trigger(() => Promise.resolve("third"));
|
|
32
|
-
|
|
33
|
-
await Bun.sleep(80);
|
|
34
|
-
|
|
35
|
-
expect(results).toEqual(["third"]);
|
|
36
|
-
ctx.dispose();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("immediate bypasses debounce", async () => {
|
|
40
|
-
let result: string | null = null;
|
|
41
|
-
const ctx = createFetchContext<string>(500, (value) => {
|
|
42
|
-
result = value;
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
ctx.immediate(() => Promise.resolve("now"));
|
|
46
|
-
|
|
47
|
-
await Bun.sleep(10);
|
|
48
|
-
|
|
49
|
-
expect(result).toBe("now");
|
|
50
|
-
ctx.dispose();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("immediate cancels pending debounced trigger", async () => {
|
|
54
|
-
const results: string[] = [];
|
|
55
|
-
const ctx = createFetchContext<string>(50, (value) => {
|
|
56
|
-
results.push(value);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
ctx.trigger(() => Promise.resolve("debounced"));
|
|
60
|
-
ctx.immediate(() => Promise.resolve("immediate"));
|
|
61
|
-
|
|
62
|
-
await Bun.sleep(80);
|
|
63
|
-
|
|
64
|
-
// Only "immediate" should have fired, "debounced" was cancelled
|
|
65
|
-
expect(results).toEqual(["immediate"]);
|
|
66
|
-
ctx.dispose();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("stale results are discarded", async () => {
|
|
70
|
-
const results: string[] = [];
|
|
71
|
-
const ctx = createFetchContext<string>(10, (value) => {
|
|
72
|
-
results.push(value);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Start a slow fetch
|
|
76
|
-
ctx.immediate(async () => {
|
|
77
|
-
await Bun.sleep(100);
|
|
78
|
-
return "slow";
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// Before the slow fetch completes, trigger a fast one
|
|
82
|
-
await Bun.sleep(20);
|
|
83
|
-
ctx.immediate(() => Promise.resolve("fast"));
|
|
84
|
-
|
|
85
|
-
// Wait for both to complete
|
|
86
|
-
await Bun.sleep(150);
|
|
87
|
-
|
|
88
|
-
// Only "fast" should be in results — "slow" was stale
|
|
89
|
-
expect(results).toEqual(["fast"]);
|
|
90
|
-
ctx.dispose();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("cancel prevents pending execution", async () => {
|
|
94
|
-
const results: string[] = [];
|
|
95
|
-
const ctx = createFetchContext<string>(50, (value) => {
|
|
96
|
-
results.push(value);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
ctx.trigger(() => Promise.resolve("cancelled"));
|
|
100
|
-
ctx.cancel();
|
|
101
|
-
|
|
102
|
-
await Bun.sleep(80);
|
|
103
|
-
|
|
104
|
-
expect(results).toEqual([]);
|
|
105
|
-
ctx.dispose();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("request_id increments on each trigger/cancel/immediate", () => {
|
|
109
|
-
const ctx = createFetchContext<string>(50, () => {});
|
|
110
|
-
|
|
111
|
-
const id1 = ctx.request_id;
|
|
112
|
-
ctx.trigger(() => Promise.resolve("a"));
|
|
113
|
-
expect(ctx.request_id).toBe(id1 + 1);
|
|
114
|
-
|
|
115
|
-
ctx.cancel();
|
|
116
|
-
expect(ctx.request_id).toBe(id1 + 2);
|
|
117
|
-
|
|
118
|
-
ctx.immediate(() => Promise.resolve("b"));
|
|
119
|
-
expect(ctx.request_id).toBe(id1 + 3);
|
|
120
|
-
|
|
121
|
-
ctx.dispose();
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe("InFlightDedup", () => {
|
|
126
|
-
test("deduplicates concurrent calls for the same key", async () => {
|
|
127
|
-
const dedup = new InFlightDedup<string>();
|
|
128
|
-
let call_count = 0;
|
|
129
|
-
|
|
130
|
-
const fn = async () => {
|
|
131
|
-
call_count++;
|
|
132
|
-
await Bun.sleep(50);
|
|
133
|
-
return "result";
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// Fire 3 concurrent requests for the same key
|
|
137
|
-
const [r1, r2, r3] = await Promise.all([
|
|
138
|
-
dedup.run("key1", fn),
|
|
139
|
-
dedup.run("key1", fn),
|
|
140
|
-
dedup.run("key1", fn),
|
|
141
|
-
]);
|
|
142
|
-
|
|
143
|
-
// All get the same result
|
|
144
|
-
expect(r1).toBe("result");
|
|
145
|
-
expect(r2).toBe("result");
|
|
146
|
-
expect(r3).toBe("result");
|
|
147
|
-
|
|
148
|
-
// But fn was only called once
|
|
149
|
-
expect(call_count).toBe(1);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("different keys run independently", async () => {
|
|
153
|
-
const dedup = new InFlightDedup<string>();
|
|
154
|
-
let call_count = 0;
|
|
155
|
-
|
|
156
|
-
const fn = async (val: string) => {
|
|
157
|
-
call_count++;
|
|
158
|
-
await Bun.sleep(20);
|
|
159
|
-
return val;
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const [r1, r2] = await Promise.all([
|
|
163
|
-
dedup.run("a", () => fn("alpha")),
|
|
164
|
-
dedup.run("b", () => fn("beta")),
|
|
165
|
-
]);
|
|
166
|
-
|
|
167
|
-
expect(r1).toBe("alpha");
|
|
168
|
-
expect(r2).toBe("beta");
|
|
169
|
-
expect(call_count).toBe(2);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("after completion, next call starts a new fetch", async () => {
|
|
173
|
-
const dedup = new InFlightDedup<number>();
|
|
174
|
-
let call_count = 0;
|
|
175
|
-
|
|
176
|
-
const fn = async () => {
|
|
177
|
-
call_count++;
|
|
178
|
-
return call_count;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const r1 = await dedup.run("k", fn);
|
|
182
|
-
expect(r1).toBe(1);
|
|
183
|
-
expect(dedup.has("k")).toBe(false);
|
|
184
|
-
|
|
185
|
-
const r2 = await dedup.run("k", fn);
|
|
186
|
-
expect(r2).toBe(2);
|
|
187
|
-
expect(call_count).toBe(2);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test("error propagates and cleans up in-flight entry", async () => {
|
|
191
|
-
const dedup = new InFlightDedup<string>();
|
|
192
|
-
|
|
193
|
-
const fn = async () => {
|
|
194
|
-
throw new Error("boom");
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
await expect(dedup.run("k", fn)).rejects.toThrow("boom");
|
|
198
|
-
expect(dedup.has("k")).toBe(false);
|
|
199
|
-
});
|
|
200
|
-
});
|