1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Schema from "effect/Schema";
5import * as Scope from "effect/Scope";
6
7import { autoUpdater } from "electron-updater";
8
9type AutoUpdater = typeof autoUpdater;
10
11export type ElectronUpdaterFeedUrl = Parameters<AutoUpdater["setFeedURL"]>[0];
12
13export class ElectronUpdaterCheckForUpdatesError extends Schema.TaggedError<ElectronUpdaterCheckForUpdatesError>()(
14 "ElectronUpdaterCheckForUpdatesError",
15 {
16 channel: Schema.NullOr(Schema.String),
17 cause: Schema.Defect(),
18 },
19) {
20 override get message(): string {
21 return `Electron updater failed to check for updates on channel ${this.channel ?? "default"}.`;
22 }
23}
24
25export class ElectronUpdaterDownloadUpdateError extends Schema.TaggedError<ElectronUpdaterDownloadUpdateError>()(
26 "ElectronUpdaterDownloadUpdateError",
27 {
28 channel: Schema.NullOr(Schema.String),
29 cause: Schema.Defect(),
30 },
31) {
32 override get message(): string {
33 return `Electron updater failed to download the update on channel ${this.channel ?? "default"}.`;
34 }
35}
36
37export class ElectronUpdaterQuitAndInstallError extends Schema.TaggedError<ElectronUpdaterQuitAndInstallError>()(
38 "ElectronUpdaterQuitAndInstallError",
39 {
40 channel: Schema.NullOr(Schema.String),
41 isSilent: Schema.Boolean,
42 isForceRunAfter: Schema.Boolean,
43 cause: Schema.Defect(),
44 },
45) {
46 override get message(): string {
47 return `Electron updater failed to quit and install the update on channel ${this.channel ?? "default"} (silent: ${this.isSilent}, force run after: ${this.isForceRunAfter}).`;
48 }
49}
50
51export const ElectronUpdaterError = Schema.Union([
52 ElectronUpdaterCheckForUpdatesError,
53 ElectronUpdaterDownloadUpdateError,
54 ElectronUpdaterQuitAndInstallError,
55]);
56export type ElectronUpdaterError = typeof ElectronUpdaterError.Type;
57
58export class ElectronUpdater extends Context.Service<
59 ElectronUpdater,
60 {
61 readonly setFeedURL: (options: ElectronUpdaterFeedUrl) => Effect.Effect<void>;
62 readonly setAutoDownload: (value: boolean) => Effect.Effect<void>;
63 readonly setAutoInstallOnAppQuit: (value: boolean) => Effect.Effect<void>;
64 readonly setChannel: (channel: string) => Effect.Effect<void>;
65 readonly setAllowPrerelease: (value: boolean) => Effect.Effect<void>;
66 readonly allowDowngrade: Effect.Effect<boolean>;
67 readonly setAllowDowngrade: (value: boolean) => Effect.Effect<void>;
68 readonly setFullChangelog: (value: boolean) => Effect.Effect<void>;
69 readonly setDisableDifferentialDownload: (value: boolean) => Effect.Effect<void>;
70 readonly checkForUpdates: Effect.Effect<void, ElectronUpdaterCheckForUpdatesError>;
71 readonly downloadUpdate: Effect.Effect<
72 ReadonlyArray<string>,
73 ElectronUpdaterDownloadUpdateError
74 >;
75 readonly quitAndInstall: (options: {
76 readonly isSilent: boolean;
77 readonly isForceRunAfter: boolean;
78 }) => Effect.Effect<void, ElectronUpdaterQuitAndInstallError>;
79 readonly on: <Args extends ReadonlyArray<unknown>>(
80 eventName: string,
81 listener: (...args: Args) => void,
82 ) => Effect.Effect<void, never, Scope.Scope>;
83 }
84>()("@t3tools/desktop/electron/ElectronUpdater") {}
85
86/** @public Service construction is part of the canonical Effect module API. */
87export const make = ElectronUpdater.of({
88 setFeedURL: (options) =>
89 Effect.suspend(() => {
90 autoUpdater.setFeedURL(options);
91 return Effect.void;
92 }),
93 setAutoDownload: (value) =>
94 Effect.suspend(() => {
95 autoUpdater.autoDownload = value;
96 return Effect.void;
97 }),
98 setAutoInstallOnAppQuit: (value) =>
99 Effect.suspend(() => {
100 autoUpdater.autoInstallOnAppQuit = value;
101 return Effect.void;
102 }),
103 setChannel: (channel) =>
104 Effect.suspend(() => {
105 autoUpdater.channel = channel;
106 return Effect.void;
107 }),
108 setAllowPrerelease: (value) =>
109 Effect.suspend(() => {
110 autoUpdater.allowPrerelease = value;
111 return Effect.void;
112 }),
113 allowDowngrade: Effect.sync(() => autoUpdater.allowDowngrade),
114 setAllowDowngrade: (value) =>
115 Effect.suspend(() => {
116 autoUpdater.allowDowngrade = value;
117 return Effect.void;
118 }),
119 setFullChangelog: (value) =>
120 Effect.suspend(() => {
121 autoUpdater.fullChangelog = value;
122 return Effect.void;
123 }),
124 setDisableDifferentialDownload: (value) =>
125 Effect.suspend(() => {
126 autoUpdater.disableDifferentialDownload = value;
127 return Effect.void;
128 }),
129 checkForUpdates: Effect.suspend(() => {
130 const channel = autoUpdater.channel;
131 return Effect.tryPromise({
132 try: () => autoUpdater.checkForUpdates(),
133 catch: (cause) => new ElectronUpdaterCheckForUpdatesError({ channel, cause }),
134 }).pipe(Effect.asVoid);
135 }),
136 downloadUpdate: Effect.suspend(() => {
137 const channel = autoUpdater.channel;
138 return Effect.tryPromise({
139 try: () => autoUpdater.downloadUpdate(),
140 catch: (cause) => new ElectronUpdaterDownloadUpdateError({ channel, cause }),
141 });
142 }),
143 quitAndInstall: ({ isSilent, isForceRunAfter }) =>
144 Effect.suspend(() => {
145 const channel = autoUpdater.channel;
146 return Effect.try({
147 try: () => autoUpdater.quitAndInstall(isSilent, isForceRunAfter),
148 catch: (cause) =>
149 new ElectronUpdaterQuitAndInstallError({
150 channel,
151 isSilent,
152 isForceRunAfter,
153 cause,
154 }),
155 });
156 }),
157 on: (eventName, listener) => {
158 const eventTarget = autoUpdater as unknown as {
159 on: (eventName: string, listener: (...args: Array<unknown>) => void) => void;
160 removeListener: (eventName: string, listener: (...args: Array<unknown>) => void) => void;
161 };
162 const untypedListener = listener as unknown as (...args: Array<unknown>) => void;
163 return Effect.acquireRelease(
164 Effect.sync(() => {
165 eventTarget.on(eventName, untypedListener);
166 }),
167 () =>
168 Effect.sync(() => {
169 eventTarget.removeListener(eventName, untypedListener);
170 }),
171 ).pipe(Effect.asVoid);
172 },
173});
174
175export const layer = Layer.succeed(ElectronUpdater, make);