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
71 changes: 71 additions & 0 deletions apps/web/src/cloud/cloudLinkState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useAtomValue } from "@effect/atom-react";
import {
AuthRelayReadScope,
type EnvironmentCloudLinkStateResult,
type EnvironmentId,
} from "@t3tools/contracts";
import * as Cause from "effect/Cause";
import { AsyncResult, Atom } from "effect/reactivity";

import { connectionAtomRuntime } from "../connection/runtime";
import { usePrimaryEnvironmentId } from "../state/environments";
import { appAtomRegistry } from "../rpc/atomRegistry";
import { readEnvironmentScope, useEnvironmentScope } from "../state/session";
import { readCloudLinkState, type CloudLinkTarget } from "./linkEnvironment";
import { hasCloudPublicConfig } from "./publicConfig";

const cloudLinkStateAtom = Atom.family((environmentId: EnvironmentId) =>
connectionAtomRuntime
.atom(readCloudLinkState({ target: { environmentId } }))
.pipe(
Atom.swr({ staleTime: 5_000, revalidateOnMount: true }),
Atom.setIdleTTL(5 * 60_000),
Atom.withLabel(`cloud-link:${environmentId}`),
),
);

const EMPTY_CLOUD_LINK_STATE_ATOM = Atom.make(
AsyncResult.success<EnvironmentCloudLinkStateResult | null>(null),
).pipe(Atom.keepAlive, Atom.withLabel("cloud-link:null"));

function refreshCloudLinkState(target: CloudLinkTarget | null): void {
if (target && readEnvironmentScope(target.environmentId, AuthRelayReadScope)) {
appAtomRegistry.refresh(cloudLinkStateAtom(target.environmentId));
}
}

export function readCachedCloudLinkState(target: CloudLinkTarget) {
if (!readEnvironmentScope(target.environmentId, AuthRelayReadScope)) return null;
const result = appAtomRegistry.get(cloudLinkStateAtom(target.environmentId));
return result._tag === "Success" ? result.value : null;
}

export function useCloudLinkState(environmentId: EnvironmentId | null) {
const canReadRelay = useEnvironmentScope(environmentId, AuthRelayReadScope);
const target: CloudLinkTarget | null = environmentId === null ? null : { environmentId };
const atom =
environmentId !== null && hasCloudPublicConfig() && canReadRelay
? cloudLinkStateAtom(environmentId)
: EMPTY_CLOUD_LINK_STATE_ATOM;
const result = useAtomValue(atom);
const refresh = () => {
refreshCloudLinkState(target);
};
let error: string | null = null;
if (result._tag === "Failure") {
const cause = Cause.squash(result.cause);
error = cause instanceof Error ? cause.message : "Could not read T3 Connect link state.";
}

return {
data: result._tag === "Failure" ? null : AsyncResult.getOrElse(result, () => null),
error,
isPending: result.waiting,
refresh,
target,
};
}

export function usePrimaryCloudLinkState() {
return useCloudLinkState(usePrimaryEnvironmentId());
}
119 changes: 93 additions & 26 deletions apps/web/src/cloud/linkEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { afterEach, beforeEach, vi } from "vite-plus/test";
import {
AVAILABLE_CONNECTION_STATE,
type PreparedConnection,
BearerConnectionTarget,
PrimaryConnectionTarget,
EnvironmentSupervisor,
} from "@t3tools/client-runtime/connection";
Expand All @@ -25,15 +26,22 @@ import { ManagedRelay } from "@t3tools/client-runtime/relay";
import { layerRemoteHttpClient } from "@t3tools/client-runtime/rpc";
import { __resetDesktopPrimaryAuthForTests } from "../environments/primary/desktopAuth";

import { readPrimaryCloudLinkState, type CloudLinkTarget } from "./linkEnvironment";
import { readCloudLinkState } from "./linkEnvironment";

const TARGET: CloudLinkTarget = {
const TARGET = {
environmentId: "environment-1",
label: "Desktop",
httpBaseUrl: "http://127.0.0.1:3000",
wsBaseUrl: "ws://127.0.0.1:3000",
};

const SAVED_SERVICE_TARGET = {
environmentId: "saved-service",
label: "Service",
httpBaseUrl: "http://127.0.0.1:3773",
wsBaseUrl: "ws://127.0.0.1:3773",
};

const relayClientInstallDialog = vi.hoisted(() => ({
requestConfirmation: vi.fn(),
reportProgress: vi.fn(),
Expand Down Expand Up @@ -88,26 +96,73 @@ function layerRegistry(options?: {
inboundFrames: Effect.succeed(0),
closed: Effect.never,
};
const target = new PrimaryConnectionTarget({
environmentId: EnvironmentId.make(TARGET.environmentId),
label: TARGET.label,
httpBaseUrl: TARGET.httpBaseUrl,
wsBaseUrl: TARGET.wsBaseUrl,
const makeSupervisor = Effect.fn(function* (prepared: Option.Option<PreparedConnection>) {
return EnvironmentSupervisor.EnvironmentSupervisor.of({
target: new PrimaryConnectionTarget({
environmentId: EnvironmentId.make(TARGET.environmentId),
label: TARGET.label,
httpBaseUrl: TARGET.httpBaseUrl,
wsBaseUrl: TARGET.wsBaseUrl,
}),
state: yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE),
session: yield* SubscriptionRef.make(Option.some(session)),
prepared: yield* SubscriptionRef.make(prepared),
connect: Effect.void,
disconnect: Effect.void,
retryNow: Effect.void,
} satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
});
const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({
target,
state: yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE),
session: yield* SubscriptionRef.make(Option.some(session)),
prepared: yield* SubscriptionRef.make(Option.none<PreparedConnection>()),
connect: Effect.void,
disconnect: Effect.void,
retryNow: Effect.void,
} satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]);
const supervisors = new Map([
[
TARGET.environmentId,
yield* makeSupervisor(
Option.some({
environmentId: EnvironmentId.make(TARGET.environmentId),
label: TARGET.label,
httpBaseUrl: TARGET.httpBaseUrl,
socketUrl: `${TARGET.wsBaseUrl}/ws`,
httpAuthorization: { _tag: "Bearer", token: "desktop-bearer-token" },
target: new PrimaryConnectionTarget({
environmentId: EnvironmentId.make(TARGET.environmentId),
label: TARGET.label,
httpBaseUrl: TARGET.httpBaseUrl,
wsBaseUrl: TARGET.wsBaseUrl,
}),
}),
),
],
[
SAVED_SERVICE_TARGET.environmentId,
yield* makeSupervisor(
Option.some({
environmentId: EnvironmentId.make(SAVED_SERVICE_TARGET.environmentId),
label: SAVED_SERVICE_TARGET.label,
httpBaseUrl: SAVED_SERVICE_TARGET.httpBaseUrl,
socketUrl: `${SAVED_SERVICE_TARGET.wsBaseUrl}/ws`,
httpAuthorization: { _tag: "Bearer", token: "saved-service-token" },
target: new BearerConnectionTarget({
environmentId: EnvironmentId.make(SAVED_SERVICE_TARGET.environmentId),
label: SAVED_SERVICE_TARGET.label,
connectionId: "saved-service-connection",
}),
}),
),
],
]);
const supervisorFor = (environmentId: EnvironmentId) => supervisors.get(environmentId)!;
const registry = {
run: <A, E, R>(_environmentId: EnvironmentId, effect: Effect.Effect<A, E, R>) =>
Effect.provideService(effect, EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
runStream: <A, E, R>(_environmentId: EnvironmentId, stream: Stream.Stream<A, E, R>) =>
Stream.provideService(stream, EnvironmentSupervisor.EnvironmentSupervisor, supervisor),
run: <A, E, R>(environmentId: EnvironmentId, effect: Effect.Effect<A, E, R>) =>
Effect.provideService(
effect,
EnvironmentSupervisor.EnvironmentSupervisor,
supervisorFor(environmentId),
),
runStream: <A, E, R>(environmentId: EnvironmentId, stream: Stream.Stream<A, E, R>) =>
Stream.provideService(
stream,
EnvironmentSupervisor.EnvironmentSupervisor,
supervisorFor(environmentId),
),
} as unknown as EnvironmentRegistry.EnvironmentRegistry["Service"];
return EnvironmentRegistry.EnvironmentRegistry.of(registry);
}),
Expand Down Expand Up @@ -145,9 +200,9 @@ afterEach(() => {
});

describe("web cloud link environment client", () => {
it.effect("uses desktop bearer auth for primary cloud link state", () =>
it.effect("reads link state from the chosen environment with its own credentials", () =>
Effect.gen(function* () {
const fetchMock = vi.fn().mockResolvedValue(
const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) =>
Response.json({
linked: true,
cloudUserId: "user-1",
Expand All @@ -165,11 +220,23 @@ describe("web cloud link environment client", () => {
} as unknown as DesktopBridge,
});

yield* withServices(readPrimaryCloudLinkState({ target: TARGET }));
for (const [target, token] of [
[TARGET, "desktop-bearer-token"],
[SAVED_SERVICE_TARGET, "saved-service-token"],
] as const) {
fetchMock.mockClear();
yield* withServices(
readCloudLinkState({
target: { environmentId: EnvironmentId.make(target.environmentId) },
}),
);

const request = new Request(fetchMock.mock.calls[0]?.[0], fetchMock.mock.calls[0]?.[1]);
expect(request.credentials).not.toBe("include");
expect(request.headers.get("authorization")).toBe("Bearer desktop-bearer-token");
const [input, init] = fetchMock.mock.calls[0]!;
const request = new Request(input, init);
expect(new URL(request.url).origin).toBe(target.httpBaseUrl);
expect(request.credentials).not.toBe("include");
expect(request.headers.get("authorization")).toBe(`Bearer ${token}`);
}
}),
);
});
Loading
Loading