@astralyn/sash 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +22 -7
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/api.js +14 -45
- package/dist/app-state.js +99 -0
- package/dist/autostart/command.js +24 -0
- package/dist/autostart/context.js +43 -0
- package/dist/autostart/files.js +60 -0
- package/dist/autostart/installation.js +48 -0
- package/dist/autostart/start.js +47 -0
- package/dist/autostart/windows-registry.js +88 -0
- package/dist/autostart/windows.js +59 -0
- package/dist/autostart-contract.js +31 -0
- package/dist/autostart-entry.js +10 -0
- package/dist/autostart.js +69 -0
- package/dist/cli.js +11 -11
- package/dist/commands/auto.js +15 -0
- package/dist/commands/lifecycle.js +5 -31
- package/dist/commands/logs.js +11 -4
- package/dist/commands/shared.js +2 -4
- package/dist/commands/status.js +6 -7
- package/dist/commands/update.js +7 -3
- package/dist/commands/web.js +19 -27
- package/dist/contracts.js +171 -335
- package/dist/core-config-validation.js +22 -38
- package/dist/core-update.js +126 -762
- package/dist/core.js +20 -46
- package/dist/daemon/app.js +132 -113
- package/dist/daemon/context.js +13 -7
- package/dist/daemon/entry.js +21 -38
- package/dist/daemon/errors.js +6 -1
- package/dist/daemon/handlers/autostart.js +18 -0
- package/dist/daemon/handlers/core.js +32 -9
- package/dist/daemon/handlers/daemon.js +32 -6
- package/dist/daemon/handlers/profiles.js +12 -2
- package/dist/daemon/handlers/settings.js +9 -36
- package/dist/daemon/router.js +40 -27
- package/dist/daemon/scheduler.js +3 -1
- package/dist/daemon/server.js +6 -3
- package/dist/daemon/web-auth.js +52 -0
- package/dist/daemon-auth.js +2 -2
- package/dist/daemon-client.js +18 -2
- package/dist/daemon-http.js +7 -0
- package/dist/daemon-lifecycle.js +19 -196
- package/dist/github.js +7 -2
- package/dist/http.js +7 -5
- package/dist/log-follow.js +2 -1
- package/dist/mihomo-config.js +7 -9
- package/dist/paths.js +5 -7
- package/dist/profile-model.js +87 -0
- package/dist/profile-service.js +250 -587
- package/dist/profiles.js +43 -171
- package/dist/runtime-lifecycle.js +112 -194
- package/dist/runtime-owner.js +37 -76
- package/dist/sash-client.js +63 -21
- package/dist/settings-service.js +29 -228
- package/dist/settings.js +61 -285
- package/dist/status.js +24 -40
- package/dist/supervisor.js +1 -14
- package/dist/sysproxy/common.js +5 -45
- package/dist/sysproxy/factory.js +24 -65
- package/dist/sysproxy/snapshot.js +24 -257
- package/dist/sysproxy.js +1 -4
- package/dist/system-proxy-manager.js +37 -35
- package/dist/ui/assets/{ConnectionsView-DNGmZBSU.js → ConnectionsView-BgXQM6Z4.js} +1 -1
- package/dist/ui/assets/{LogsView-fSiaxQ13.js → LogsView-2f542P8z.js} +1 -1
- package/dist/ui/assets/{PaginationFooter-B3kHzRfB.js → PaginationFooter-cj1tcZej.js} +1 -1
- package/dist/ui/assets/ProfileEditorDialog-C9M8uKZC.css +1 -0
- package/dist/ui/assets/ProfileEditorDialog-DD4y4GBC.js +14 -0
- package/dist/ui/assets/ProfilesView-BXU2DOA7.css +1 -0
- package/dist/ui/assets/ProfilesView-DNnzYIEY.js +7 -0
- package/dist/ui/assets/{RulesView-D9vZBiJ1.js → RulesView-CtKvlckZ.js} +1 -1
- package/dist/ui/assets/SettingsView-BaeKFF_N.js +1 -0
- package/dist/ui/assets/SettingsView-CWjIe05v.css +1 -0
- package/dist/ui/assets/{0be242294f7d791af850c6df38ac78a0-2cL6Ntwf.woff2 → e2a57555d97d0b02b45d9418eb6ee295-D9nhF3rM.woff2} +0 -0
- package/dist/ui/assets/index-CBInDvdJ.js +19 -0
- package/dist/ui/assets/index-mOLy7fkB.css +1 -0
- package/dist/ui/assets/{theme-BNq4FkXS.js → theme-D40MF8cQ.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/web-bootstrap.js +113 -0
- package/docs/architecture-proposal.md +109 -0
- package/docs/autostart.md +101 -0
- package/docs/backend.md +92 -216
- package/docs/frontend.md +61 -97
- package/docs/usage.md +88 -186
- package/package.json +10 -6
- package/dist/commands/upgrade.js +0 -43
- package/dist/core-install-transaction.js +0 -114
- package/dist/core-update-coordination.js +0 -105
- package/dist/core-update-service.js +0 -245
- package/dist/managed-state-transaction.js +0 -377
- package/dist/offline-mutation.js +0 -58
- package/dist/profile-migration.js +0 -101
- package/dist/runtime-recovery.js +0 -31
- package/dist/sysproxy/darwin.js +0 -287
- package/dist/sysproxy/gnome.js +0 -181
- package/dist/sysproxy/legacy.js +0 -58
- package/dist/tun-guidance.js +0 -11
- package/dist/ui/assets/CodeEditorModal-6m0TJWyF.css +0 -1
- package/dist/ui/assets/CodeEditorModal-CFEnWsyh.js +0 -14
- package/dist/ui/assets/ProfileEditorDialog-BydoZthX.js +0 -1
- package/dist/ui/assets/ProfilesView-Btc1DOxE.js +0 -2
- package/dist/ui/assets/ProfilesView-ByqdFNHN.css +0 -1
- package/dist/ui/assets/SettingsFileDialog-CNFEAVs4.js +0 -1
- package/dist/ui/assets/SettingsView-BlDhZkXQ.js +0 -2
- package/dist/ui/assets/SettingsView-CngS3vBM.css +0 -1
- package/dist/ui/assets/index-B61V60w_.js +0 -19
- package/dist/ui/assets/index-bkyxJG8J.css +0 -1
package/dist/profile-service.js
CHANGED
|
@@ -1,667 +1,330 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { allocateProfileId, findProfileByUrl, getActiveProfile, loadProfiles, MAX_PROFILE_INTERVAL_HOURS, profileDueForUpdate, profileFilePath, profileNameFromUrl, readProfileDigest, readProfileSource, } from "./profiles.js";
|
|
7
|
-
import { sameSettings } from "./settings.js";
|
|
2
|
+
import { StateConflictError } from "./app-state.js";
|
|
3
|
+
import { atomicWriteFileSync, durableRemoveFileSync } from "./fs-atomic.js";
|
|
4
|
+
import { fetchSubscriptionProfile } from "./mihomo-config.js";
|
|
5
|
+
import { allocateProfileId, getActiveProfile, MAX_PROFILE_INTERVAL_HOURS, parseProfileText, profileDueForUpdate, profileFilePath, profileNameFromUrl, readProfileSource, } from "./profiles.js";
|
|
8
6
|
export class ProfileInputError extends Error {
|
|
9
7
|
}
|
|
10
|
-
export class GeneratedConfigError extends Error {
|
|
11
|
-
}
|
|
12
8
|
export class ProfileNotFoundError extends Error {
|
|
13
9
|
}
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const index = cursor++;
|
|
22
|
-
const item = items[index];
|
|
23
|
-
if (item === undefined)
|
|
24
|
-
return;
|
|
25
|
-
results[index] = await worker(item);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
await Promise.all(runners);
|
|
29
|
-
return results;
|
|
30
|
-
}
|
|
31
|
-
function sameProfileState(before, current) {
|
|
32
|
-
return isDeepStrictEqual(before, current);
|
|
33
|
-
}
|
|
34
|
-
function opaqueToken() {
|
|
35
|
-
return Object.freeze({});
|
|
36
|
-
}
|
|
37
|
-
function replaceProfileMeta(index, replacement) {
|
|
38
|
-
return {
|
|
39
|
-
...index,
|
|
40
|
-
profiles: index.profiles.map((profile) => profile.id === replacement.id ? replacement : profile),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
function currentProfile(index, snapshot, activeId) {
|
|
44
|
-
const profile = index.profiles.find((item) => item.id === snapshot.id);
|
|
45
|
-
if (!profile || !isDeepStrictEqual(profile, snapshot) || index.activeId !== activeId) {
|
|
46
|
-
throw new ProfileConflictError(`profile changed or was removed during update: ${snapshot.id}`);
|
|
10
|
+
export { StateConflictError as ProfileConflictError } from "./app-state.js";
|
|
11
|
+
function profileInput(content) {
|
|
12
|
+
try {
|
|
13
|
+
return parseProfileText(content);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
throw new ProfileInputError(error instanceof Error ? error.message : String(error));
|
|
47
17
|
}
|
|
48
|
-
return profile;
|
|
49
|
-
}
|
|
50
|
-
function validFetchedInterval(value, fallback) {
|
|
51
|
-
return value !== undefined &&
|
|
52
|
-
Number.isSafeInteger(value) &&
|
|
53
|
-
value > 0 &&
|
|
54
|
-
value <= MAX_PROFILE_INTERVAL_HOURS
|
|
55
|
-
? value
|
|
56
|
-
: fallback;
|
|
57
18
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
...(fetched.homePage ? { homePage: fetched.homePage } : {}),
|
|
64
|
-
intervalHours: validFetchedInterval(fetched.intervalHours, profile.intervalHours),
|
|
65
|
-
lastError: undefined,
|
|
66
|
-
};
|
|
19
|
+
function validName(name) {
|
|
20
|
+
const result = name.trim();
|
|
21
|
+
if (!result || result.length > 120)
|
|
22
|
+
throw new ProfileInputError("Profile name must contain 1 to 120 characters");
|
|
23
|
+
return result;
|
|
67
24
|
}
|
|
68
25
|
export class ProfileService {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
onChange;
|
|
76
|
-
files;
|
|
77
|
-
fetches = new Map();
|
|
78
|
-
preparedActiveConfigs = new WeakMap();
|
|
79
|
-
preparedActiveReloads = new WeakMap();
|
|
80
|
-
commitTail = Promise.resolve();
|
|
81
|
-
constructor(opts) {
|
|
82
|
-
this.layout = opts.layout;
|
|
83
|
-
this.getSettings = opts.settings;
|
|
84
|
-
this.fetchProfileFn = opts.fetchProfile ?? fetchSubscriptionProfile;
|
|
85
|
-
this.validateConfig = opts.validateConfig;
|
|
86
|
-
this.reloadConfig = opts.reloadConfig;
|
|
87
|
-
this.commitBoundary = opts.commit;
|
|
88
|
-
this.onChange = opts.onChange;
|
|
89
|
-
this.files = opts.fileOperations ?? defaultManagedStateFileOperations;
|
|
26
|
+
options;
|
|
27
|
+
downloads = new Set();
|
|
28
|
+
updatingDue = false;
|
|
29
|
+
downloadGeneration = 0;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.options = options;
|
|
90
32
|
}
|
|
91
33
|
list() {
|
|
92
|
-
return
|
|
34
|
+
return this.options.state.snapshot().profiles;
|
|
93
35
|
}
|
|
94
36
|
active() {
|
|
95
37
|
return getActiveProfile(this.list());
|
|
96
38
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.commitTail = next.then(() => undefined, () => undefined);
|
|
102
|
-
return next;
|
|
103
|
-
}
|
|
104
|
-
fetch(url) {
|
|
105
|
-
const existing = this.fetches.get(url);
|
|
106
|
-
if (existing)
|
|
107
|
-
return existing;
|
|
108
|
-
const pending = this.fetchProfileFn(url).finally(() => {
|
|
109
|
-
if (this.fetches.get(url) === pending)
|
|
110
|
-
this.fetches.delete(url);
|
|
111
|
-
});
|
|
112
|
-
this.fetches.set(url, pending);
|
|
113
|
-
return pending;
|
|
114
|
-
}
|
|
115
|
-
settingsSnapshot() {
|
|
116
|
-
return { ...this.getSettings() };
|
|
39
|
+
cancelDownloads() {
|
|
40
|
+
this.downloadGeneration += 1;
|
|
41
|
+
for (const download of this.downloads)
|
|
42
|
+
download.abort(new StateConflictError("Profile download cancelled"));
|
|
117
43
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
44
|
+
async fetch(url) {
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
this.downloads.add(controller);
|
|
47
|
+
try {
|
|
48
|
+
const fetched = await (this.options.fetchProfile ?? fetchSubscriptionProfile)(url, controller.signal);
|
|
49
|
+
controller.signal.throwIfAborted();
|
|
50
|
+
profileInput(fetched.yamlText);
|
|
51
|
+
return fetched;
|
|
126
52
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
recheckProfileSnapshot(index, snapshot, contentMessage = "Profile content changed while preparing configuration") {
|
|
132
|
-
const profile = currentProfile(index, snapshot.profile, snapshot.activeId);
|
|
133
|
-
this.assertProfileContentCurrent(profile.id, snapshot.contentDigest, contentMessage);
|
|
134
|
-
return profile;
|
|
135
|
-
}
|
|
136
|
-
async resolveActiveSource(index) {
|
|
137
|
-
const active = getActiveProfile(index);
|
|
138
|
-
if (!active) {
|
|
139
|
-
return {
|
|
140
|
-
activeId: index.activeId,
|
|
141
|
-
active: null,
|
|
142
|
-
sourceDigest: null,
|
|
143
|
-
doc: null,
|
|
144
|
-
};
|
|
53
|
+
catch (error) {
|
|
54
|
+
controller.signal.throwIfAborted();
|
|
55
|
+
throw error;
|
|
145
56
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (stored === undefined && !fetched) {
|
|
149
|
-
throw new ProfileInputError(`Local profile file is missing: ${active.id}`);
|
|
57
|
+
finally {
|
|
58
|
+
this.downloads.delete(controller);
|
|
150
59
|
}
|
|
151
|
-
return {
|
|
152
|
-
activeId: index.activeId,
|
|
153
|
-
active,
|
|
154
|
-
sourceDigest: stored?.digest ?? null,
|
|
155
|
-
doc: fetched?.doc ?? stored?.doc ?? null,
|
|
156
|
-
...(fetched ? { fetched } : {}),
|
|
157
|
-
};
|
|
158
60
|
}
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
if (!
|
|
162
|
-
throw new
|
|
163
|
-
|
|
164
|
-
this.preparedActiveConfigs.delete(prepared);
|
|
165
|
-
return state;
|
|
61
|
+
requireProfile(index, id) {
|
|
62
|
+
const profile = index.profiles.find((item) => item.id === id);
|
|
63
|
+
if (!profile)
|
|
64
|
+
throw new ProfileNotFoundError(`Profile not found: ${id}`);
|
|
65
|
+
return profile;
|
|
166
66
|
}
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
170
|
-
throw new
|
|
67
|
+
recheck(index, before) {
|
|
68
|
+
const current = this.requireProfile(index, before.id);
|
|
69
|
+
if (current.revision !== before.revision || current.url !== before.url) {
|
|
70
|
+
throw new StateConflictError("Profile changed while downloading or editing; refresh and retry");
|
|
171
71
|
}
|
|
172
|
-
|
|
173
|
-
return state;
|
|
174
|
-
}
|
|
175
|
-
preparedActiveReload(state) {
|
|
176
|
-
const prepared = opaqueToken();
|
|
177
|
-
this.preparedActiveReloads.set(prepared, state);
|
|
178
|
-
return prepared;
|
|
72
|
+
return current;
|
|
179
73
|
}
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (sameSettings(settings, rollbackSnapshot)) {
|
|
186
|
-
rollbackConfig = { generated };
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
74
|
+
publishSource(index, profile, text, select) {
|
|
75
|
+
const state = this.options.state.snapshot();
|
|
76
|
+
const previous = index.profiles.find((item) => item.id === profile.id);
|
|
77
|
+
let unchanged = false;
|
|
78
|
+
if (previous) {
|
|
189
79
|
try {
|
|
190
|
-
|
|
80
|
+
unchanged = readProfileSource(this.options.layout, previous).yamlText === text;
|
|
191
81
|
}
|
|
192
|
-
catch
|
|
193
|
-
rollbackConfig = { error };
|
|
82
|
+
catch {
|
|
194
83
|
}
|
|
195
84
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
85
|
+
let revision = profile.revision;
|
|
86
|
+
if (unchanged && previous)
|
|
87
|
+
revision = previous.revision;
|
|
88
|
+
else
|
|
89
|
+
while (fs.existsSync(profileFilePath(this.options.layout, profile.id, revision)))
|
|
90
|
+
revision += 1;
|
|
91
|
+
const next = { ...profile, revision };
|
|
92
|
+
const file = profileFilePath(this.options.layout, next.id, next.revision);
|
|
93
|
+
if (!unchanged)
|
|
94
|
+
atomicWriteFileSync(file, text);
|
|
95
|
+
const profiles = previous
|
|
96
|
+
? index.profiles.map((item) => (item.id === next.id ? next : item))
|
|
97
|
+
: [...index.profiles, next];
|
|
98
|
+
this.options.state.commit({
|
|
99
|
+
...state,
|
|
100
|
+
profiles: { activeId: select ? next.id : index.activeId, profiles },
|
|
205
101
|
});
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const state = this.takePreparedActiveConfig(prepared);
|
|
210
|
-
this.assertSettingsUnchanged(state.rollbackSettings);
|
|
211
|
-
const index = this.list();
|
|
212
|
-
if (index.activeId !== state.activeId) {
|
|
213
|
-
throw new ProfileConflictError("Active profile changed while preparing configuration");
|
|
214
|
-
}
|
|
215
|
-
let active = null;
|
|
216
|
-
if (state.active) {
|
|
217
|
-
active = getActiveProfile(index);
|
|
218
|
-
if (!active || active.id !== state.active.id || active.url !== state.active.url) {
|
|
219
|
-
throw new ProfileConflictError("Active profile changed while preparing configuration");
|
|
102
|
+
if (previous && previous.revision !== next.revision) {
|
|
103
|
+
try {
|
|
104
|
+
durableRemoveFileSync(profileFilePath(this.options.layout, previous.id, previous.revision));
|
|
220
105
|
}
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
const rollback = this.preparedActiveReload({
|
|
224
|
-
...state.rollbackConfig,
|
|
225
|
-
settings: state.rollbackSettings,
|
|
226
|
-
activeId: index.activeId,
|
|
227
|
-
sourceDigest: state.sourceDigest,
|
|
228
|
-
...(active ? { active } : {}),
|
|
229
|
-
});
|
|
230
|
-
const publication = active && state.fetched
|
|
231
|
-
? {
|
|
232
|
-
config: state.generated,
|
|
233
|
-
rollback,
|
|
234
|
-
index: replaceProfileMeta(index, withFetchedContent(active, state.fetched)),
|
|
235
|
-
profile: { id: active.id, yamlText: state.fetched.yamlText },
|
|
106
|
+
catch {
|
|
236
107
|
}
|
|
237
|
-
: { config: state.generated, rollback };
|
|
238
|
-
const result = await callback(publication);
|
|
239
|
-
if (publication.profile)
|
|
240
|
-
this.notifyChange();
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
|
-
async prepareActiveReload() {
|
|
244
|
-
const source = await this.resolveActiveSource(this.list());
|
|
245
|
-
const settings = this.settingsSnapshot();
|
|
246
|
-
return this.preparedActiveReload({
|
|
247
|
-
generated: await this.prepare(source.doc, settings),
|
|
248
|
-
settings,
|
|
249
|
-
activeId: source.activeId,
|
|
250
|
-
sourceDigest: source.sourceDigest,
|
|
251
|
-
...(source.active ? { active: source.active } : {}),
|
|
252
|
-
...(source.fetched ? { fetched: source.fetched } : {}),
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
async withPreparedActiveReloadPublication(prepared, callback) {
|
|
256
|
-
const state = this.takePreparedActiveReload(prepared);
|
|
257
|
-
if ("error" in state)
|
|
258
|
-
throw state.error;
|
|
259
|
-
this.assertSettingsUnchanged(state.settings);
|
|
260
|
-
const index = this.list();
|
|
261
|
-
if (index.activeId !== state.activeId) {
|
|
262
|
-
throw new ProfileConflictError("Active profile changed while preparing configuration");
|
|
263
|
-
}
|
|
264
|
-
let publication;
|
|
265
|
-
if (!state.active) {
|
|
266
|
-
publication = { config: state.generated, index };
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
const profile = this.recheckProfileSnapshot(index, this.updateSnapshot(state.active, state.activeId, state.sourceDigest));
|
|
270
|
-
const updated = state.fetched ? withFetchedContent(profile, state.fetched) : profile;
|
|
271
|
-
publication = {
|
|
272
|
-
config: state.generated,
|
|
273
|
-
index: replaceProfileMeta(index, updated),
|
|
274
|
-
...(state.fetched ? { profile: { id: profile.id, yamlText: state.fetched.yamlText } } : {}),
|
|
275
|
-
};
|
|
276
108
|
}
|
|
277
|
-
|
|
278
|
-
this.notifyChange();
|
|
279
|
-
return result;
|
|
280
|
-
}
|
|
281
|
-
async commitPreparedActiveReload(prepared, options = {}) {
|
|
282
|
-
const reloadRuntime = options.reloadRuntime ?? true;
|
|
283
|
-
const boundary = options.boundary ?? "acquire";
|
|
284
|
-
if (boundary !== "acquire" && boundary !== "already-held") {
|
|
285
|
-
throw new TypeError(`Unknown profile commit boundary: ${String(boundary)}`);
|
|
286
|
-
}
|
|
287
|
-
const action = () => this.withPreparedActiveReloadPublication(prepared, async (publication) => {
|
|
288
|
-
await this.publishTransaction(publication.index, {
|
|
289
|
-
...(publication.profile ? { profile: publication.profile } : {}),
|
|
290
|
-
config: publication.config,
|
|
291
|
-
reloadRuntime,
|
|
292
|
-
});
|
|
293
|
-
return publication.config;
|
|
294
|
-
});
|
|
295
|
-
return boundary === "acquire" ? this.commit("reload active profile", action) : action();
|
|
296
|
-
}
|
|
297
|
-
async reloadActive(options = {}) {
|
|
298
|
-
return this.commitPreparedActiveReload(await this.prepareActiveReload(), options);
|
|
299
|
-
}
|
|
300
|
-
notifyChange() {
|
|
301
|
-
try {
|
|
302
|
-
this.onChange?.();
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
async prepare(doc, settings) {
|
|
308
|
-
const generated = renderConfig(doc ?? buildDefaultConfig(), settings, doc ? "subscription" : "default");
|
|
309
|
-
try {
|
|
310
|
-
await this.validateConfig?.(generated);
|
|
311
|
-
}
|
|
312
|
-
catch (err) {
|
|
313
|
-
throw new GeneratedConfigError(err.message);
|
|
314
|
-
}
|
|
315
|
-
return generated;
|
|
316
|
-
}
|
|
317
|
-
async publishTransaction(index, opts = {}) {
|
|
318
|
-
await commitManagedStateTransaction(this.layout, { index, ...opts }, this.reloadConfig, this.files);
|
|
109
|
+
return next;
|
|
319
110
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
111
|
+
fetchedMeta(profile, fetched) {
|
|
112
|
+
const interval = fetched.intervalHours;
|
|
113
|
+
return {
|
|
114
|
+
...profile,
|
|
115
|
+
revision: profile.revision + 1,
|
|
116
|
+
updatedAt: new Date().toISOString(),
|
|
117
|
+
...(fetched.subInfo ? { subInfo: fetched.subInfo } : {}),
|
|
118
|
+
...(fetched.homePage ? { homePage: fetched.homePage } : {}),
|
|
119
|
+
intervalHours: interval !== undefined &&
|
|
120
|
+
Number.isSafeInteger(interval) &&
|
|
121
|
+
interval > 0 &&
|
|
122
|
+
interval <= MAX_PROFILE_INTERVAL_HOURS
|
|
123
|
+
? interval
|
|
124
|
+
: profile.intervalHours,
|
|
125
|
+
lastError: undefined,
|
|
126
|
+
};
|
|
323
127
|
}
|
|
324
128
|
async addRemote(url, opts = {}) {
|
|
325
|
-
const
|
|
326
|
-
if (!
|
|
129
|
+
const normalized = url.trim();
|
|
130
|
+
if (!normalized)
|
|
327
131
|
throw new ProfileInputError("Missing required profile URL");
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
if (!sameProfileState(before, current) || findProfileByUrl(current, normalizedUrl)) {
|
|
347
|
-
throw new ProfileConflictError("Profiles changed while adding a remote profile");
|
|
348
|
-
}
|
|
349
|
-
const id = allocateProfileId(current, this.layout);
|
|
350
|
-
const now = new Date().toISOString();
|
|
351
|
-
profile = {
|
|
352
|
-
id,
|
|
353
|
-
name: opts.name?.trim() || fetched.name || profileNameFromUrl(normalizedUrl),
|
|
354
|
-
url: normalizedUrl,
|
|
355
|
-
intervalHours: validFetchedInterval(fetched.intervalHours, 24),
|
|
356
|
-
createdAt: now,
|
|
357
|
-
updatedAt: now,
|
|
358
|
-
...(fetched.subInfo ? { subInfo: fetched.subInfo } : {}),
|
|
359
|
-
...(fetched.homePage ? { homePage: fetched.homePage } : {}),
|
|
360
|
-
};
|
|
361
|
-
profileChange = { id, yamlText: fetched.yamlText };
|
|
362
|
-
}
|
|
363
|
-
const shouldActivate = opts.activate === true || current.activeId === null;
|
|
364
|
-
const index = {
|
|
365
|
-
activeId: shouldActivate ? profile.id : current.activeId,
|
|
366
|
-
profiles: known
|
|
367
|
-
? current.profiles.map((item) => (item.id === profile.id ? profile : item))
|
|
368
|
-
: [...current.profiles, profile],
|
|
369
|
-
};
|
|
370
|
-
await this.publish(index, {
|
|
371
|
-
profile: profileChange,
|
|
372
|
-
...(shouldActivate ? { config: prepared } : {}),
|
|
373
|
-
});
|
|
374
|
-
return {
|
|
375
|
-
profile,
|
|
376
|
-
activated: shouldActivate,
|
|
377
|
-
...(shouldActivate ? { proxyCount: prepared.proxyCount } : {}),
|
|
132
|
+
const known = this.list().profiles.find((profile) => profile.url === normalized);
|
|
133
|
+
const fetched = await this.fetch(normalized);
|
|
134
|
+
return this.options.commit("save remote profile", () => {
|
|
135
|
+
const index = this.list();
|
|
136
|
+
const now = new Date().toISOString();
|
|
137
|
+
const existing = known ? this.recheck(index, known) : undefined;
|
|
138
|
+
if (!known && index.profiles.some((profile) => profile.url === normalized))
|
|
139
|
+
throw new StateConflictError("Profile was added during download");
|
|
140
|
+
const base = existing ?? {
|
|
141
|
+
id: allocateProfileId(index, this.options.layout),
|
|
142
|
+
revision: 0,
|
|
143
|
+
name: validName(opts.name ?? fetched.name?.slice(0, 120) ?? profileNameFromUrl(normalized)),
|
|
144
|
+
url: normalized,
|
|
145
|
+
intervalHours: 24,
|
|
146
|
+
createdAt: now,
|
|
147
|
+
updatedAt: now,
|
|
378
148
|
};
|
|
149
|
+
const activated = opts.activate === true || index.activeId === null;
|
|
150
|
+
const profile = this.publishSource(index, this.fetchedMeta(base, fetched), fetched.yamlText, activated);
|
|
151
|
+
return { profile, activated };
|
|
379
152
|
});
|
|
380
153
|
}
|
|
381
154
|
async importLocal(name, content) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
doc = YAML.parse(content);
|
|
387
|
-
}
|
|
388
|
-
catch (err) {
|
|
389
|
-
throw new ProfileInputError(`Content is not valid YAML: ${err.message}`);
|
|
390
|
-
}
|
|
391
|
-
if (!isValidMihomoConfig(doc)) {
|
|
392
|
-
throw new ProfileInputError("Content is not a valid core configuration (missing proxies/rules)");
|
|
393
|
-
}
|
|
394
|
-
const settings = this.settingsSnapshot();
|
|
395
|
-
const prepared = await this.prepare(doc, settings);
|
|
396
|
-
const before = this.list();
|
|
397
|
-
return this.commit("import profile", async () => {
|
|
398
|
-
this.assertSettingsUnchanged(settings);
|
|
399
|
-
const current = this.list();
|
|
400
|
-
if (!sameProfileState(before, current)) {
|
|
401
|
-
throw new ProfileConflictError("Profiles changed while importing a local profile");
|
|
402
|
-
}
|
|
403
|
-
const id = allocateProfileId(current, this.layout);
|
|
155
|
+
profileInput(content);
|
|
156
|
+
const displayName = validName(name);
|
|
157
|
+
return this.options.commit("import profile", () => {
|
|
158
|
+
const index = this.list();
|
|
404
159
|
const now = new Date().toISOString();
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
160
|
+
const activated = index.activeId === null;
|
|
161
|
+
const profile = this.publishSource(index, {
|
|
162
|
+
id: allocateProfileId(index, this.options.layout),
|
|
163
|
+
revision: 1,
|
|
164
|
+
name: displayName,
|
|
408
165
|
url: "",
|
|
409
166
|
intervalHours: 0,
|
|
410
167
|
createdAt: now,
|
|
411
168
|
updatedAt: now,
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
await this.publish({ activeId: activated ? id : current.activeId, profiles: [...current.profiles, profile] }, {
|
|
415
|
-
profile: { id, yamlText: content },
|
|
416
|
-
...(activated ? { config: prepared } : {}),
|
|
417
|
-
});
|
|
418
|
-
return { profile, activated, ...(activated ? { proxyCount: prepared.proxyCount } : {}) };
|
|
169
|
+
}, content, activated);
|
|
170
|
+
return { profile, activated };
|
|
419
171
|
});
|
|
420
172
|
}
|
|
421
173
|
readContent(id) {
|
|
422
|
-
const profile = this.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
catch {
|
|
430
|
-
throw new ProfileNotFoundError(`profile file is missing: ${profile.id}`);
|
|
431
|
-
}
|
|
432
|
-
return { name: profile.name, content };
|
|
174
|
+
const profile = this.requireProfile(this.list(), id);
|
|
175
|
+
return {
|
|
176
|
+
name: profile.name,
|
|
177
|
+
content: readProfileSource(this.options.layout, profile).yamlText,
|
|
178
|
+
revision: profile.revision,
|
|
179
|
+
};
|
|
433
180
|
}
|
|
434
|
-
async writeContent(id, content) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
let doc;
|
|
438
|
-
try {
|
|
439
|
-
doc = YAML.parse(content);
|
|
440
|
-
}
|
|
441
|
-
catch (err) {
|
|
442
|
-
throw new ProfileInputError(`Content is not valid YAML: ${err.message}`);
|
|
443
|
-
}
|
|
444
|
-
if (!isValidMihomoConfig(doc)) {
|
|
445
|
-
throw new ProfileInputError("Content is not a valid core configuration (missing proxies/rules)");
|
|
446
|
-
}
|
|
447
|
-
const before = this.list();
|
|
448
|
-
const target = before.profiles.find((item) => item.id === id);
|
|
449
|
-
if (!target)
|
|
450
|
-
throw new ProfileNotFoundError(`profile not found: ${id}`);
|
|
451
|
-
const snapshot = this.updateSnapshot(target, before.activeId);
|
|
452
|
-
const settings = this.settingsSnapshot();
|
|
453
|
-
const prepared = await this.prepare(doc, settings);
|
|
454
|
-
return this.commit("edit profile", async () => {
|
|
455
|
-
this.assertSettingsUnchanged(settings);
|
|
181
|
+
async writeContent(id, content, revision) {
|
|
182
|
+
profileInput(content);
|
|
183
|
+
return this.options.commit("save profile edit", () => {
|
|
456
184
|
const index = this.list();
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
185
|
+
const before = this.requireProfile(index, id);
|
|
186
|
+
if (before.revision !== revision)
|
|
187
|
+
throw new StateConflictError("Profile changed since the editor opened; reload before saving");
|
|
188
|
+
const profile = this.publishSource(index, {
|
|
189
|
+
...before,
|
|
190
|
+
revision: before.revision + 1,
|
|
460
191
|
updatedAt: new Date().toISOString(),
|
|
461
192
|
lastError: undefined,
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
await this.publish({
|
|
465
|
-
...index,
|
|
466
|
-
profiles: index.profiles.map((item) => (item.id === profile.id ? updated : item)),
|
|
467
|
-
}, {
|
|
468
|
-
profile: { id: profile.id, yamlText: content },
|
|
469
|
-
...(active ? { config: prepared } : {}),
|
|
470
|
-
});
|
|
471
|
-
return { profile: updated, ...(active ? { proxyCount: prepared.proxyCount } : {}) };
|
|
193
|
+
}, content, false);
|
|
194
|
+
return { profile };
|
|
472
195
|
});
|
|
473
196
|
}
|
|
474
197
|
async activate(id) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
this.
|
|
481
|
-
|
|
482
|
-
if (current.activeId !== before.activeId) {
|
|
483
|
-
throw new ProfileConflictError("Active profile changed while preparing configuration");
|
|
484
|
-
}
|
|
485
|
-
await this.publish({ ...current, activeId: null }, { config: prepared });
|
|
486
|
-
return { activeId: null, proxyCount: prepared.proxyCount };
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
const initial = before.profiles.find((profile) => profile.id === id);
|
|
490
|
-
if (!initial)
|
|
491
|
-
throw new ProfileNotFoundError(`profile not found: ${id}`);
|
|
492
|
-
const stored = readProfileSource(this.layout, id);
|
|
493
|
-
const fetched = stored === undefined && initial.url ? await this.fetch(initial.url) : undefined;
|
|
494
|
-
if (stored === undefined && !fetched) {
|
|
495
|
-
throw new ProfileInputError(`Local profile file is missing: ${id}`);
|
|
496
|
-
}
|
|
497
|
-
const sourceDigest = stored?.digest ?? null;
|
|
498
|
-
const prepared = await this.prepare(fetched?.doc ?? stored?.doc ?? null, settings);
|
|
499
|
-
return this.commit("activate profile", async () => {
|
|
500
|
-
this.assertSettingsUnchanged(settings);
|
|
501
|
-
const current = this.list();
|
|
502
|
-
const profile = currentProfile(current, initial, before.activeId);
|
|
503
|
-
this.assertProfileContentCurrent(profile.id, sourceDigest);
|
|
504
|
-
const updated = fetched ? withFetchedContent(profile, fetched) : profile;
|
|
505
|
-
await this.publish({
|
|
506
|
-
activeId: id,
|
|
507
|
-
profiles: current.profiles.map((item) => (item.id === id ? updated : item)),
|
|
508
|
-
}, {
|
|
509
|
-
...(fetched ? { profile: { id, yamlText: fetched.yamlText } } : {}),
|
|
510
|
-
config: prepared,
|
|
511
|
-
});
|
|
512
|
-
return { activeId: id, proxyCount: prepared.proxyCount };
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
async commitFetched(snapshot, fetched) {
|
|
516
|
-
const settings = this.settingsSnapshot();
|
|
517
|
-
const prepared = await this.prepare(fetched.doc, settings);
|
|
518
|
-
return this.commit("update profile", async () => {
|
|
519
|
-
this.assertSettingsUnchanged(settings);
|
|
520
|
-
const index = this.list();
|
|
521
|
-
const current = currentProfile(index, snapshot.profile, snapshot.activeId);
|
|
522
|
-
this.assertProfileContentCurrent(current.id, snapshot.contentDigest, "Profile changed while updating");
|
|
523
|
-
const profile = withFetchedContent(current, fetched);
|
|
524
|
-
const active = index.activeId === current.id;
|
|
525
|
-
await this.publish({
|
|
526
|
-
...index,
|
|
527
|
-
profiles: index.profiles.map((item) => (item.id === current.id ? profile : item)),
|
|
528
|
-
}, {
|
|
529
|
-
profile: { id: current.id, yamlText: fetched.yamlText },
|
|
530
|
-
...(active ? { config: prepared } : {}),
|
|
531
|
-
});
|
|
532
|
-
return { profile, ...(active ? { proxyCount: prepared.proxyCount } : {}) };
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
async recordError(snapshot, error) {
|
|
536
|
-
await this.commit("record profile update error", async () => {
|
|
537
|
-
const index = this.list();
|
|
538
|
-
let current;
|
|
539
|
-
try {
|
|
540
|
-
current = currentProfile(index, snapshot.profile, snapshot.activeId);
|
|
541
|
-
this.assertProfileContentCurrent(current.id, snapshot.contentDigest, "Profile changed while recording an update error");
|
|
542
|
-
}
|
|
543
|
-
catch (err) {
|
|
544
|
-
if (err instanceof ProfileConflictError)
|
|
545
|
-
return;
|
|
546
|
-
throw err;
|
|
547
|
-
}
|
|
548
|
-
const profile = { ...current, lastError: error.slice(0, 300) };
|
|
549
|
-
await this.publish({
|
|
550
|
-
...index,
|
|
551
|
-
profiles: index.profiles.map((item) => (item.id === current.id ? profile : item)),
|
|
552
|
-
});
|
|
198
|
+
return this.options.commit("select profile", () => {
|
|
199
|
+
const state = this.options.state.snapshot();
|
|
200
|
+
const profile = id === null ? null : this.requireProfile(state.profiles, id);
|
|
201
|
+
const doc = profile ? readProfileSource(this.options.layout, profile).doc : null;
|
|
202
|
+
if (state.profiles.activeId !== id)
|
|
203
|
+
this.options.state.commit({ ...state, profiles: { ...state.profiles, activeId: id } });
|
|
204
|
+
return { activeId: id, proxyCount: Array.isArray(doc?.proxies) ? doc.proxies.length : 0 };
|
|
553
205
|
});
|
|
554
206
|
}
|
|
555
207
|
async update(id) {
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
if (!profile)
|
|
559
|
-
throw new ProfileNotFoundError(`profile not found: ${id}`);
|
|
560
|
-
if (!profile.url)
|
|
208
|
+
const before = this.requireProfile(this.list(), id);
|
|
209
|
+
if (!before.url)
|
|
561
210
|
throw new ProfileInputError("Local profile has no URL to update from");
|
|
562
|
-
const snapshot = this.updateSnapshot(profile, index.activeId);
|
|
563
211
|
try {
|
|
564
|
-
|
|
212
|
+
const fetched = await this.fetch(before.url);
|
|
213
|
+
return await this.options.commit("update profile", () => {
|
|
214
|
+
const index = this.list();
|
|
215
|
+
const current = this.recheck(index, before);
|
|
216
|
+
return {
|
|
217
|
+
profile: this.publishSource(index, this.fetchedMeta(current, fetched), fetched.yamlText, false),
|
|
218
|
+
};
|
|
219
|
+
});
|
|
565
220
|
}
|
|
566
|
-
catch (
|
|
567
|
-
await this.
|
|
568
|
-
|
|
221
|
+
catch (error) {
|
|
222
|
+
await this.options
|
|
223
|
+
.commit("record profile error", () => {
|
|
224
|
+
const state = this.options.state.snapshot();
|
|
225
|
+
const current = state.profiles.profiles.find((profile) => profile.id === id);
|
|
226
|
+
if (!current || current.revision !== before.revision || current.url !== before.url)
|
|
227
|
+
return;
|
|
228
|
+
const message = (error instanceof Error ? error.message : String(error)).slice(0, 300);
|
|
229
|
+
this.options.state.commit({
|
|
230
|
+
...state,
|
|
231
|
+
profiles: {
|
|
232
|
+
...state.profiles,
|
|
233
|
+
profiles: state.profiles.profiles.map((profile) => profile.id === id ? { ...profile, lastError: message } : profile),
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
})
|
|
237
|
+
.catch(() => undefined);
|
|
238
|
+
throw error;
|
|
569
239
|
}
|
|
570
240
|
}
|
|
571
241
|
async updateProfiles(profiles) {
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const failed = [];
|
|
583
|
-
for (const result of fetched) {
|
|
584
|
-
const { profile } = result.snapshot;
|
|
585
|
-
if (!("fetched" in result)) {
|
|
586
|
-
await this.recordError(result.snapshot, result.error);
|
|
587
|
-
failed.push({ id: profile.id, name: profile.name, error: result.error });
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
try {
|
|
591
|
-
const committed = await this.commitFetched(result.snapshot, result.fetched);
|
|
592
|
-
updated += 1;
|
|
593
|
-
if (committed.proxyCount !== undefined)
|
|
594
|
-
proxyCount = committed.proxyCount;
|
|
595
|
-
}
|
|
596
|
-
catch (err) {
|
|
597
|
-
const message = err.message;
|
|
598
|
-
await this.recordError(result.snapshot, message);
|
|
599
|
-
failed.push({ id: profile.id, name: profile.name, error: message });
|
|
242
|
+
const result = { updated: 0, failed: [] };
|
|
243
|
+
const generation = this.downloadGeneration;
|
|
244
|
+
for (let start = 0; start < profiles.length; start += 4) {
|
|
245
|
+
if (generation !== this.downloadGeneration) {
|
|
246
|
+
result.failed.push(...profiles.slice(start).map((profile) => ({
|
|
247
|
+
id: profile.id,
|
|
248
|
+
name: profile.name,
|
|
249
|
+
error: "Profile update cancelled",
|
|
250
|
+
})));
|
|
251
|
+
break;
|
|
600
252
|
}
|
|
253
|
+
await Promise.all(profiles.slice(start, start + 4).map(async (profile) => {
|
|
254
|
+
try {
|
|
255
|
+
await this.update(profile.id);
|
|
256
|
+
result.updated += 1;
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
result.failed.push({
|
|
260
|
+
id: profile.id,
|
|
261
|
+
name: profile.name,
|
|
262
|
+
error: error instanceof Error ? error.message : String(error),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}));
|
|
601
266
|
}
|
|
602
|
-
return
|
|
267
|
+
return result;
|
|
603
268
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
return this.updateProfiles(index.profiles
|
|
607
|
-
.filter((profile) => profile.url !== "")
|
|
608
|
-
.map((profile) => this.updateSnapshot(profile, index.activeId)));
|
|
269
|
+
updateAll() {
|
|
270
|
+
return this.updateProfiles(this.list().profiles.filter((profile) => profile.url));
|
|
609
271
|
}
|
|
610
272
|
async updateDue(nowMs = Date.now()) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
273
|
+
if (this.updatingDue)
|
|
274
|
+
return { updated: 0, failed: [] };
|
|
275
|
+
this.updatingDue = true;
|
|
276
|
+
try {
|
|
277
|
+
return await this.updateProfiles(this.list().profiles.filter((profile) => profileDueForUpdate(profile, nowMs)));
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
this.updatingDue = false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async remove(id) {
|
|
284
|
+
return this.options.commit("remove profile", () => {
|
|
285
|
+
const state = this.options.state.snapshot();
|
|
286
|
+
const profile = this.requireProfile(state.profiles, id);
|
|
287
|
+
const wasActive = state.profiles.activeId === id;
|
|
288
|
+
this.options.state.commit({
|
|
289
|
+
...state,
|
|
290
|
+
profiles: {
|
|
291
|
+
activeId: wasActive ? null : state.profiles.activeId,
|
|
292
|
+
profiles: state.profiles.profiles.filter((item) => item.id !== id),
|
|
293
|
+
},
|
|
294
|
+
});
|
|
615
295
|
try {
|
|
616
|
-
|
|
296
|
+
durableRemoveFileSync(profileFilePath(this.options.layout, profile.id, profile.revision));
|
|
617
297
|
}
|
|
618
298
|
catch {
|
|
619
299
|
}
|
|
620
|
-
return
|
|
621
|
-
})
|
|
622
|
-
.map((profile) => this.updateSnapshot(profile, index.activeId)));
|
|
300
|
+
return { wasActive };
|
|
301
|
+
});
|
|
623
302
|
}
|
|
624
|
-
async
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const index = this.list();
|
|
635
|
-
const profile = currentProfile(index, initial, before.activeId);
|
|
636
|
-
this.assertProfileContentCurrent(profile.id, sourceDigest, "Profile changed while deleting");
|
|
637
|
-
const wasActive = index.activeId === profile.id;
|
|
638
|
-
await this.publish({
|
|
639
|
-
activeId: wasActive ? null : index.activeId,
|
|
640
|
-
profiles: index.profiles.filter((item) => item.id !== id),
|
|
641
|
-
}, {
|
|
642
|
-
profile: { id, yamlText: null },
|
|
643
|
-
...(prepared ? { config: prepared } : {}),
|
|
644
|
-
});
|
|
645
|
-
return { wasActive, ...(prepared ? { proxyCount: prepared.proxyCount } : {}) };
|
|
303
|
+
async reorder(ids) {
|
|
304
|
+
return this.options.commit("reorder profiles", () => {
|
|
305
|
+
const state = this.options.state.snapshot();
|
|
306
|
+
if (ids.length !== state.profiles.profiles.length || new Set(ids).size !== ids.length)
|
|
307
|
+
throw new ProfileInputError("Profile order must contain every profile exactly once");
|
|
308
|
+
const profiles = ids.map((id) => this.requireProfile(state.profiles, id));
|
|
309
|
+
if (profiles.every((profile, i) => profile.id === state.profiles.profiles[i]?.id))
|
|
310
|
+
return state.profiles;
|
|
311
|
+
return this.options.state.commit({ ...state, profiles: { ...state.profiles, profiles } })
|
|
312
|
+
.profiles;
|
|
646
313
|
});
|
|
647
314
|
}
|
|
648
315
|
async rename(id, name) {
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const updated = { ...profile, name: trimmed };
|
|
660
|
-
await this.publish({
|
|
661
|
-
...index,
|
|
662
|
-
profiles: index.profiles.map((item) => (item.id === id ? updated : item)),
|
|
316
|
+
const displayName = validName(name);
|
|
317
|
+
return this.options.commit("rename profile", () => {
|
|
318
|
+
const state = this.options.state.snapshot();
|
|
319
|
+
const profile = { ...this.requireProfile(state.profiles, id), name: displayName };
|
|
320
|
+
this.options.state.commit({
|
|
321
|
+
...state,
|
|
322
|
+
profiles: {
|
|
323
|
+
...state.profiles,
|
|
324
|
+
profiles: state.profiles.profiles.map((item) => (item.id === id ? profile : item)),
|
|
325
|
+
},
|
|
663
326
|
});
|
|
664
|
-
return { profile
|
|
327
|
+
return { profile };
|
|
665
328
|
});
|
|
666
329
|
}
|
|
667
330
|
}
|