Skip to content
Merged
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
70 changes: 68 additions & 2 deletions packages/host/src/compose-tasks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ interface FakeService extends TasksClient {
sections: TaskSections | undefined;
/** Every change to the sections the service was asked for, in order. */
sectionWrites: TaskSectionWrite[];
/** The transaction a pin's or a section's write answers, where a test sets one; none as a write that changed no row. */
sidebarTxid: string | undefined;
/** The workspaces the account is a member of, as the service lists them. */
memberships: Workspace[];
/** The Luke GitHub App install address a connection answers. */
Expand Down Expand Up @@ -543,6 +545,7 @@ function fakeService(tasks: Task[]): FakeService {
pinsFail: false,
sections: undefined,
sectionWrites: [],
sidebarTxid: undefined,
get memberships() {
return memberships;
},
Expand Down Expand Up @@ -846,13 +849,17 @@ function fakeService(tasks: Task[]): FakeService {
service.pins = pinned
? [standing ?? { repository, pinnedAt: MARKED_AT + service.pins.length }, ...others]
: others;
return { pins: service.pins };
const txid = service.sidebarTxid;
return txid === undefined ? { pins: service.pins } : { pins: service.pins, txid };
}),
// The service answers its sections as they stand, the test having set them to what the write left.
writeTaskSection: (write) =>
Effect.sync(() => {
service.sectionWrites.push(write);
return service.sections;
const txid = service.sidebarTxid;
return service.sections === undefined || txid === undefined
? service.sections
: { ...service.sections, txid };
}),
// The service writes the edit's value into the document's decisions and answers the task whole.
edit: (taskId, edit) =>
Expand Down Expand Up @@ -1672,6 +1679,65 @@ it.effect(
}),
);

it.effect(
"a pin or a section's write answers once the followed sidebar has its transaction, at once where it names none, and at once before the sidebar is followed",
() =>
Effect.gen(function* () {
const service = fakeService([task(INVITES, "Teammate invitations", "# Draft", 10)]);
const { call, last, settled } = yield* subject(service);
const launch: TaskSection = {
id: SECTION_ID,
workspaceId: PERSONAL.id,
title: "Launch",
createdAt: 5,
};
service.sections = { sections: [launch], entries: [] };

// Before the first refresh nothing follows the sidebar, so a write's transaction is not waited on.
service.sidebarTxid = "40";
assert.deepEqual(
yield* call(GATEWAY_METHOD.TASKS_PIN_REPOSITORY, {
repository: "acme/relay",
pinned: true,
}),
{ changed: true },
);
assert.deepEqual(service.realtime.awaitingTxids(), []);

yield* call(GATEWAY_METHOD.TASKS_REFRESH);
yield* settled;

service.sidebarTxid = "41";
const placed = { taskId: INVITES, sectionId: SECTION_ID, addedAt: 6 };
service.sections = { sections: [launch], entries: [placed] };
const place = { kind: TASK_SECTION_WRITE.PLACE, taskId: INVITES, sectionId: SECTION_ID };
const placing = yield* Effect.forkChild(call(GATEWAY_METHOD.TASKS_WRITE_SECTION, place));
yield* settled;
assert.deepEqual(service.realtime.awaitingTxids(), ["41"]);
assert.equal(placing.pollUnsafe(), undefined, "the write waits for its transaction");
service.realtime.arrive("41");
assert.deepEqual(yield* Fiber.join(placing), { changed: true });
assert.deepEqual(last()?.taskSections, { sections: [launch], entries: [placed] });

service.sidebarTxid = "42";
const pinning = yield* Effect.forkChild(
call(GATEWAY_METHOD.TASKS_PIN_REPOSITORY, { repository: "acme/web", pinned: true }),
);
yield* settled;
assert.deepEqual(service.realtime.awaitingTxids(), ["42"]);
service.realtime.arrive("42");
assert.deepEqual(yield* Fiber.join(pinning), { changed: true });

// A write that changed no row names no transaction, so there is nothing to wait for.
service.sidebarTxid = undefined;
assert.deepEqual(
yield* call(GATEWAY_METHOD.TASKS_PIN_REPOSITORY, { repository: "acme/web", pinned: true }),
{ changed: true },
);
assert.deepEqual(service.realtime.awaitingTxids(), []);
}),
);

it.effect("a sign-out drops the view the window drew", () =>
Effect.gen(function* () {
const service = fakeService([task(INVITES, "Teammate invitations", "# Draft", 10)]);
Expand Down
16 changes: 13 additions & 3 deletions packages/host/src/compose-tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,13 @@ import type { AccountComposer } from "./compose-account.js";
import type { Composer } from "./composer.js";
import type { HostKernel } from "./host-kernel.js";
import {
confirmWrite,
LIVE_GONE,
type LiveBroken,
type LiveClient,
openRealtimeClient,
type SealedRead,
type TxWait,
} from "./live-follow.js";
import { pushLive } from "./live-push.js";
import type { RunMode } from "./run-mode.js";
Expand Down Expand Up @@ -852,6 +854,8 @@ export const composeTasks = /* @__PURE__ */ Effect.fn("host/composeTasks")(funct

/** The one follow of the account's sidebar sections and pinned repositories, from the first refresh after a launch or a sign-in until a sign-out. */
const sidebarFollow = yield* FiberHandle.make<void>();
/** The sidebar follow's wait for a write's transaction while it is subscribed over Realtime; none while it polls or is let go. */
let sidebarTxWait: TxWait | undefined;

/**
* Follows the account's sidebar over Realtime where nothing follows it
Expand Down Expand Up @@ -880,6 +884,9 @@ export const composeTasks = /* @__PURE__ */ Effect.fn("host/composeTasks")(funct
poll: (after) => seal(true, after),
openLive: dependencies.openLive ?? openRealtimeClient,
now: kernel.now,
onTxWait: (wait) => {
sidebarTxWait = wait;
},
},
sealed,
);
Expand Down Expand Up @@ -1860,7 +1867,8 @@ export const composeTasks = /* @__PURE__ */ Effect.fn("host/composeTasks")(funct
);
}),
// The service answers the pins as they then stand, so the view takes its
// word rather than the window's guess.
// word rather than the window's guess, once the followed sidebar has the
// pin's transaction, so a delivery from before it draws nothing back.
[GATEWAY_METHOD.TASKS_PIN_REPOSITORY]: (params) =>
Effect.gen(function* () {
const read = readEither(tasksPinRepositoryParamsSchema)(unparsedWire(params));
Expand All @@ -1873,12 +1881,13 @@ export const composeTasks = /* @__PURE__ */ Effect.fn("host/composeTasks")(funct
Effect.gen(function* () {
const answer = yield* served(client.pinRepository(repository, pinned));
if (answer === undefined) return { changed: false };
yield* confirmWrite(sidebarTxWait, answer.txid);
write({ repositoryPins: answer.pins });
return { changed: true };
}),
);
}),
// The service answers the sections as they then stand, as it does the pins.
// The service answers the sections as they then stand, as it does the pins, and waits on the followed sidebar as a pin does.
[GATEWAY_METHOD.TASKS_WRITE_SECTION]: (params) =>
Effect.gen(function* () {
const read = readEither(tasksWriteSectionParamsSchema)(unparsedWire(params));
Expand All @@ -1891,7 +1900,8 @@ export const composeTasks = /* @__PURE__ */ Effect.fn("host/composeTasks")(funct
Effect.gen(function* () {
const written = yield* served(client.writeTaskSection(asked));
if (written === undefined) return { changed: false };
const { txid: _txid, ...sections } = written;
const { txid, ...sections } = written;
yield* confirmWrite(sidebarTxWait, txid);
write({ taskSections: sections });
return { changed: true };
}),
Expand Down
11 changes: 10 additions & 1 deletion packages/host/src/workspaces-follow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ import {
} from "@luke/features/workspace-live";
import type { Workspace } from "@luke/features/workspace-wire";
import { Effect, Option, Schema, Stream } from "effect";
import { LiveBroken, type LiveClient, liveRows, type SealedRead } from "./live-follow.js";
import {
LiveBroken,
type LiveClient,
liveRows,
type SealedRead,
type TxWait,
} from "./live-follow.js";

/**
* workspaces-follow.ts -- the account's workspaces and sidebar, and one workspace's people and pending invitations, followed by the host over Neon Realtime subscriptions.
Expand All @@ -44,6 +50,8 @@ export interface LiveReadDependencies<Answer> {
/** Realtime's client at the address the service named, loaded where it must be. */
readonly openLive: (url: string) => LiveClient | PromiseLike<LiveClient>;
readonly now: () => number;
/** Hears the read's wait for a write's transaction once it is subscribed over Realtime, and none once let go or polled (`live-follow.ts`). */
readonly onTxWait?: (wait: TxWait | undefined) => void;
}

/**
Expand All @@ -65,6 +73,7 @@ function liveMoves<Part extends string, Rows, Built>(
reseal: dependencies.seal,
poll: dependencies.poll,
now: dependencies.now,
...(dependencies.onTxWait === undefined ? undefined : { onTxWait: dependencies.onTxWait }),
},
first,
).pipe(
Expand Down
Loading