Skip to content
Source
Sign inSign up

DesktopWebLinks.ts

86 lines3.1 KB

1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5​
6import * as DesktopWindow from "../window/DesktopWindow.ts";
7import * as ElectronWindow from "../electron/ElectronWindow.ts";
8import { WEB_LINK_OPEN_CHANNEL } from "../ipc/channels.ts";
9​
10export const isWebLink = (value: string) => {
11 if (!URL.canParse(value)) return false;
12 const { protocol } = new URL(value);
13 return protocol === "http:" || protocol === "https:";
14};
15​
16export const isWebPageFile = (path: string) => /\.x?html?$/i.test(path);
17​
18export class DesktopWebLinks extends Context.Service<
19 DesktopWebLinks,
20 {
21 readonly receive: (url: string) => Effect.Effect<void>;
22 readonly setRendererReady: (ready: boolean) => Effect.Effect<void>;
23 }
24>()("@t3tools/desktop/app/DesktopWebLinks") {}
25​
26const make = Effect.gen(function* () {
27 const electronWindow = yield* ElectronWindow.ElectronWindow;
28 const desktopWindow = yield* DesktopWindow.DesktopWindow;
29 const pending: Array<string> = [];
30 let listening: Electron.WebContents | null = null;
31 let detachListening: (() => void) | null = null;
32​
33 const stopListening = () => {
34 detachListening?.();
35 detachListening = null;
36 listening = null;
37 };
38​
39 const flush = Effect.gen(function* () {
40 if (listening === null || pending.length === 0) return;
41 const window = yield* electronWindow.currentMainOrFirst;
42 if (Option.isNone(window) || window.value.webContents !== listening) return;
43 if (listening.isDestroyed()) {
44 stopListening();
45 return;
46 }
47 for (const url of pending.splice(0)) listening.send(WEB_LINK_OPEN_CHANNEL, url);
48 yield* desktopWindow.activate.pipe(Effect.ignore);
49 });
50​
51 const startListening = Effect.gen(function* () {
52 const window = yield* electronWindow.currentMainOrFirst;
53 if (Option.isNone(window) || window.value.webContents.isDestroyed()) return;
54 const webContents = window.value.webContents;
55 if (listening === webContents) return;
56 stopListening();
57 listening = webContents;
58 const onNavigation = (
59 event: Electron.Event<Electron.WebContentsDidStartNavigationEventParams>,
60 ) => {
61 if (event.isMainFrame && !event.isSameDocument) stopListening();
62 };
63 webContents.on("did-start-navigation", onNavigation);
64 webContents.on("render-process-gone", stopListening);
65 webContents.once("destroyed", stopListening);
66 detachListening = () => {
67 webContents.removeListener("did-start-navigation", onNavigation);
68 webContents.removeListener("render-process-gone", stopListening);
69 webContents.removeListener("destroyed", stopListening);
70 };
71 });
72​
73 return DesktopWebLinks.of({
74 receive: Effect.fn("DesktopWebLinks.receive")(function (url) {
75 return Effect.suspend(() => {
76 pending.push(url);
77 return flush;
78 });
79 }),
80 setRendererReady: Effect.fn("DesktopWebLinks.setRendererReady")(function (ready) {
81 return ready ? startListening : Effect.sync(stopListening);
82 }, Effect.andThen(flush)),
83 });
84});
85​
86export const layer = Layer.effect(DesktopWebLinks, make);