Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/cyberstrike/src/cli/cmd/tui/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { useExit } from "./exit"
import { useArgs } from "./args"
import { batch, onMount } from "solid-js"
import { Log } from "@/util/log"
import { singleflight } from "@/util/singleflight"
import type { Path } from "@cyberstrike-io/sdk"

export const { use: useSync, provider: SyncProvider } = createSimpleContext({
Expand Down Expand Up @@ -577,7 +578,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const exit = useExit()
const args = useArgs()

async function bootstrap() {
const bootstrap = singleflight(async function () {
console.log("bootstrapping")
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
const sessionListPromise = sdk.client.session
Expand Down Expand Up @@ -664,7 +665,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
})
await exit(e)
})
}
})

onMount(() => {
bootstrap()
Expand Down
10 changes: 10 additions & 0 deletions packages/cyberstrike/src/util/singleflight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function singleflight<T>(fn: () => Promise<T>) {
let inflight: Promise<T> | undefined
return () => {
if (inflight) return inflight
inflight = fn().finally(() => {
inflight = undefined
})
return inflight
}
}
69 changes: 69 additions & 0 deletions packages/cyberstrike/test/util/singleflight.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, test } from "bun:test"
import { singleflight } from "../../src/util/singleflight"

function tick() {
return new Promise<void>((r) => queueMicrotask(r))
}

describe("util.singleflight", () => {
test("concurrent calls share one execution", async () => {
let count = 0
const run = singleflight(async () => {
count++
await tick()
return { value: Math.random() }
})

const [a, b] = await Promise.all([run(), run()])
expect(count).toBe(1)
expect(a).toBe(b)
})

test("executes again after the promise settles", async () => {
let count = 0
const run = singleflight(async () => {
count++
await tick()
})

await run()
await run()
expect(count).toBe(2)
})

test("rejection propagates to all callers and the guard clears", async () => {
let fail = true
let count = 0
const run = singleflight(async () => {
count++
await tick()
if (fail) throw new Error("boom")
return "ok"
})

const results = await Promise.allSettled([run(), run(), run()])
expect(count).toBe(1)
for (const result of results) {
expect(result.status).toBe("rejected")
if (result.status === "rejected") expect(result.reason.message).toBe("boom")
}

fail = false
expect(await run()).toBe("ok")
expect(count).toBe(2)
})

test("sequential non-overlapping calls each execute", async () => {
let count = 0
const run = singleflight(async () => {
count++
await tick()
return count
})

expect(await run()).toBe(1)
expect(await run()).toBe(2)
expect(await run()).toBe(3)
expect(count).toBe(3)
})
})
Loading