@narumitw/pi-sync 0.48.0 → 0.49.0

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/README.md CHANGED
@@ -292,6 +292,7 @@ extensions/pi-sync/
292
292
  │ ├── file-selection.ts
293
293
  │ ├── sync-operations.ts
294
294
  │ ├── sync-backend.ts
295
+ │ ├── backend-factory.ts # lazy selected-backend loader
295
296
  │ ├── s3-backend.ts
296
297
  │ ├── webdav-backend.ts
297
298
  │ ├── git-backend.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-sync",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "Pi extension that syncs Pi configuration through Git, WebDAV, Cloudflare R2, or S3-compatible storage.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "typecheck": "tsc --noEmit"
37
37
  },
38
38
  "dependencies": {
39
- "@narumitw/pi-tui-kit": "^0.45.0",
39
+ "@narumitw/pi-tui-kit": "^0.46.0",
40
40
  "fast-xml-parser": "^5.10.1",
41
41
  "proper-lockfile": "^4.1.2"
42
42
  },
@@ -1,20 +1,23 @@
1
- import { GitSyncBackend } from "./git-backend.js";
2
- import { S3SyncBackend } from "./s3-backend.js";
3
1
  import type { SyncBackend } from "./sync-backend.js";
4
2
  import type { AnySyncConfig } from "./types.js";
5
- import { WebDavSyncBackend } from "./webdav-backend.js";
6
3
 
7
4
  export type SyncBackendFactory = {
8
- bivarianceHack(config: AnySyncConfig): SyncBackend;
5
+ bivarianceHack(config: AnySyncConfig): SyncBackend | Promise<SyncBackend>;
9
6
  }["bivarianceHack"];
10
7
 
11
- export const createSyncBackend: SyncBackendFactory = (config) => {
8
+ export const createSyncBackend: SyncBackendFactory = async (config) => {
12
9
  switch (config.backend.type) {
13
- case "s3":
10
+ case "s3": {
11
+ const { S3SyncBackend } = await import("./s3-backend.js");
14
12
  return new S3SyncBackend(config.backend);
15
- case "webdav":
13
+ }
14
+ case "webdav": {
15
+ const { WebDavSyncBackend } = await import("./webdav-backend.js");
16
16
  return new WebDavSyncBackend(config.backend);
17
- case "git":
17
+ }
18
+ case "git": {
19
+ const { GitSyncBackend } = await import("./git-backend.js");
18
20
  return new GitSyncBackend(config.backend);
21
+ }
19
22
  }
20
23
  };
@@ -114,7 +114,7 @@ export async function status(
114
114
  const config = await loadConfig(options.setup);
115
115
  throwIfAborted(options.signal);
116
116
  ctx.ui.setStatus(STATUS_KEY, `checking ${config.setupName}`);
117
- const backend = backendFor(config, factory);
117
+ const backend = await backendFor(config, factory);
118
118
  const local = await createSnapshot(
119
119
  config.snapshotIdentity,
120
120
  snapshotOptionsForContext(ctx, config),
@@ -160,7 +160,7 @@ export async function diff(
160
160
  const config = await loadConfig(options.setup);
161
161
  throwIfAborted(options.signal);
162
162
  ctx.ui.setStatus(STATUS_KEY, `checking ${config.setupName}`);
163
- const backend = backendFor(config, factory);
163
+ const backend = await backendFor(config, factory);
164
164
  const local = await createSnapshot(
165
165
  config.snapshotIdentity,
166
166
  snapshotOptionsForContext(ctx, config),
@@ -206,7 +206,7 @@ export async function doctor(
206
206
  try {
207
207
  const config = await loadConfig(options.setup);
208
208
  throwIfAborted(options.signal);
209
- backend = backendFor(config, factory);
209
+ backend = await backendFor(config, factory);
210
210
  profile = config.snapshotIdentity;
211
211
  snapshotOptions = snapshotOptionsForContext(ctx, config);
212
212
  messages.push(
@@ -283,7 +283,7 @@ export async function push(
283
283
  const config = input?.config ?? (await loadConfig(options.setup));
284
284
  throwIfAborted(options.signal);
285
285
  ctx.ui.setStatus(STATUS_KEY, `pushing ${config.setupName}`);
286
- const backend = input?.backend ?? backendFor(config, factory);
286
+ const backend = input?.backend ?? (await backendFor(config, factory));
287
287
  const state = input?.state ?? (await readStateForConfig(config));
288
288
  throwIfAborted(options.signal);
289
289
  const local =
@@ -415,7 +415,7 @@ export async function pull(
415
415
  const config = await loadConfig(options.setup);
416
416
  throwIfAborted(options.signal);
417
417
  ctx.ui.setStatus(STATUS_KEY, `pulling ${config.setupName}`);
418
- const backend = backendFor(config, factory);
418
+ const backend = await backendFor(config, factory);
419
419
  const state = await readStateForConfig(config);
420
420
  throwIfAborted(options.signal);
421
421
  const local = await createSnapshot(
@@ -520,7 +520,7 @@ export async function syncBoth(
520
520
  ) {
521
521
  const config = await loadConfig(options.setup);
522
522
  throwIfAborted(options.signal);
523
- const backend = backendFor(config, factory);
523
+ const backend = await backendFor(config, factory);
524
524
  const state = await readStateForConfig(config);
525
525
  throwIfAborted(options.signal);
526
526
  const local = await createSnapshot(
@@ -645,7 +645,7 @@ export async function history(
645
645
  ) {
646
646
  const config = await loadConfig(options.setup);
647
647
  throwIfAborted(options.signal);
648
- const backend = backendFor(config, factory);
648
+ const backend = await backendFor(config, factory);
649
649
  const snapshots = (await backend.listHistory(options.signal)).slice(-20).reverse();
650
650
  throwIfAborted(options.signal);
651
651
  if (snapshots.length === 0) {
@@ -695,7 +695,7 @@ export async function rollback(
695
695
 
696
696
  const config = await loadConfig(options.setup);
697
697
  throwIfAborted(options.signal);
698
- const backend = backendFor(config, factory);
698
+ const backend = await backendFor(config, factory);
699
699
  if (
700
700
  expectedSelection &&
701
701
  (backend.identity !== expectedSelection.backendIdentity ||
package/src/sync.ts CHANGED
@@ -28,29 +28,30 @@ import {
28
28
  sessionTokenWarnings,
29
29
  syncSessionsWarnings,
30
30
  } from "./config.js";
31
- import { showFileSelection } from "./file-selection.js";
32
31
  import { unlock, withLock } from "./lock.js";
33
- import { showSetupWizard, showSyncManager } from "./manager-ui.js";
34
32
  import { SetupPullRequiresUiError, useSyncSetup } from "./setup-switch.js";
35
33
  import { createSnapshot } from "./snapshot.js";
36
34
  import { recoverSnapshotTransactionsOnStartup } from "./snapshot-transaction.js";
37
35
  import { isSyncDecisionRequiredError } from "./sync-decision.js";
38
36
  import { errorMessage } from "./sync-format.js";
39
- import {
40
- diff,
41
- doctor,
42
- history,
43
- pull,
44
- push,
45
- rollback,
46
- status,
47
- syncBoth,
48
- } from "./sync-operations.js";
49
37
  import { hasLocalChanges } from "./sync-state.js";
50
38
  import type { AnySyncConfig, CommandOptions, SnapshotOptions } from "./types.js";
51
39
 
52
40
  const STATUS_KEY = "sync";
53
41
 
42
+ type SyncOperations = typeof import("./sync-operations.js");
43
+ let syncOperationsPromise: Promise<SyncOperations> | undefined;
44
+
45
+ function loadSyncOperations(): Promise<SyncOperations> {
46
+ if (!syncOperationsPromise) {
47
+ syncOperationsPromise = import("./sync-operations.js").catch((error) => {
48
+ syncOperationsPromise = undefined;
49
+ throw error;
50
+ });
51
+ }
52
+ return syncOperationsPromise;
53
+ }
54
+
54
55
  const AUTO_SYNC_OPTIONS: CommandOptions = {
55
56
  yes: true,
56
57
  force: false,
@@ -130,6 +131,8 @@ async function handleCommand(
130
131
  ) {
131
132
  if (!rawArgs.trim()) {
132
133
  try {
134
+ const { showSyncManager } = await import("./manager-ui.js");
135
+ if (sessionSignal.aborted) return;
133
136
  await showSyncManager(
134
137
  ctx,
135
138
  (route, signal, onCommit, target) =>
@@ -174,8 +177,13 @@ async function executeCommand(
174
177
  await useSyncSetup(
175
178
  ctx,
176
179
  options.args[0] ?? "",
177
- (selectedSetup) =>
178
- withLock("pull", () => pull(ctx, { ...options, setup: selectedSetup })),
180
+ async (selectedSetup) => {
181
+ const operations = await loadSyncOperations();
182
+ throwIfAborted(options.signal);
183
+ return withLock("pull", () =>
184
+ operations.pull(ctx, { ...options, setup: selectedSetup }),
185
+ );
186
+ },
179
187
  undefined,
180
188
  options.signal,
181
189
  );
@@ -187,34 +195,48 @@ async function executeCommand(
187
195
  await showConfig(ctx, options);
188
196
  return { kind: "completed" };
189
197
  case "files":
190
- await showFileSelection(ctx, options.setup, options.signal);
198
+ await (await import("./file-selection.js")).showFileSelection(
199
+ ctx,
200
+ options.setup,
201
+ options.signal,
202
+ );
191
203
  return { kind: "completed" };
192
204
  case "status":
193
- await status(ctx, options);
205
+ await (await loadSyncOperations()).status(ctx, options);
194
206
  return { kind: "completed" };
195
207
  case "diff":
196
- await diff(ctx, options);
208
+ await (await loadSyncOperations()).diff(ctx, options);
197
209
  return { kind: "completed" };
198
210
  case "doctor":
199
- await doctor(ctx, options);
211
+ await (await loadSyncOperations()).doctor(ctx, options);
200
212
  return { kind: "completed" };
201
213
  case "push": {
202
- const outcome = await withLock("push", () => push(ctx, options));
214
+ const operations = await loadSyncOperations();
215
+ throwIfAborted(options.signal);
216
+ const outcome = await withLock("push", () => operations.push(ctx, options));
203
217
  return { kind: "completed", ...(outcome ? { outcome } : {}) };
204
218
  }
205
219
  case "pull": {
206
- const outcome = await withLock("pull", () => pull(ctx, options));
220
+ const operations = await loadSyncOperations();
221
+ throwIfAborted(options.signal);
222
+ const outcome = await withLock("pull", () => operations.pull(ctx, options));
207
223
  return { kind: "completed", ...(outcome ? { outcome } : {}) };
208
224
  }
209
- case "sync":
210
- await withLock("sync", () => syncBoth(ctx, options));
225
+ case "sync": {
226
+ const operations = await loadSyncOperations();
227
+ throwIfAborted(options.signal);
228
+ await withLock("sync", () => operations.syncBoth(ctx, options));
211
229
  return { kind: "completed" };
230
+ }
212
231
  case "history":
213
- await history(ctx, options);
232
+ await (await loadSyncOperations()).history(ctx, options);
214
233
  return { kind: "completed" };
215
- case "rollback":
216
- await withLock("rollback", () => rollback(ctx, options));
234
+ case "rollback": {
235
+ const operations = await loadSyncOperations();
236
+ throwIfAborted(options.signal);
237
+ await withLock("rollback", () => operations.rollback(ctx, options));
217
238
  return { kind: "completed" };
239
+ }
218
240
  case "unlock":
219
241
  await unlock(ctx, options);
220
242
  return { kind: "completed" };
@@ -243,9 +265,11 @@ async function autoSync(ctx: ExtensionContext, signal: AbortSignal) {
243
265
  throwIfAborted(signal);
244
266
  await loadConfig();
245
267
  throwIfAborted(signal);
268
+ const operations = await loadSyncOperations();
269
+ throwIfAborted(signal);
246
270
  await withLock("auto-sync", () => {
247
271
  throwIfAborted(signal);
248
- return syncBoth(ctx, { ...AUTO_SYNC_OPTIONS, signal });
272
+ return operations.syncBoth(ctx, { ...AUTO_SYNC_OPTIONS, signal });
249
273
  });
250
274
  } catch (error) {
251
275
  if (signal.aborted || isMissingConfigError(error)) return;
@@ -265,6 +289,8 @@ async function autoPushSessions(ctx: ExtensionContext, signal: AbortSignal) {
265
289
  const config = await loadConfig();
266
290
  throwIfAborted(signal);
267
291
  if (!config.include.includes("sessions")) return;
292
+ const operations = await loadSyncOperations();
293
+ throwIfAborted(signal);
268
294
  await withLock("auto-session-push", async () => {
269
295
  throwIfAborted(signal);
270
296
  const state = await readStateForConfig(config);
@@ -275,7 +301,7 @@ async function autoPushSessions(ctx: ExtensionContext, signal: AbortSignal) {
275
301
  );
276
302
  throwIfAborted(signal);
277
303
  if (!hasLocalChanges(local, state, config)) return;
278
- await push(ctx, { ...AUTO_SYNC_OPTIONS, signal }, { config, state, local });
304
+ await operations.push(ctx, { ...AUTO_SYNC_OPTIONS, signal }, { config, state, local });
279
305
  });
280
306
  } catch (error) {
281
307
  if (signal.aborted || isMissingConfigError(error)) return;
@@ -292,6 +318,8 @@ async function initConfig(ctx: ExtensionCommandContext, signal?: AbortSignal) {
292
318
  }
293
319
 
294
320
  if (ctx.mode === "tui") {
321
+ const { showSetupWizard } = await import("./manager-ui.js");
322
+ throwIfAborted(signal);
295
323
  await showSetupWizard(ctx, signal);
296
324
  return;
297
325
  }
@@ -383,8 +411,8 @@ function displayWebDavUrl(value: string | undefined, username: string | undefine
383
411
  }
384
412
  }
385
413
 
386
- function throwIfAborted(signal: AbortSignal) {
387
- if (!signal.aborted) return;
414
+ function throwIfAborted(signal?: AbortSignal) {
415
+ if (!signal?.aborted) return;
388
416
  throw signal.reason instanceof Error
389
417
  ? signal.reason
390
418
  : new DOMException("The operation was aborted", "AbortError");
@@ -446,7 +474,6 @@ export {
446
474
  preflightSnapshotApply,
447
475
  protectSnapshotApplyPlan,
448
476
  } from "./snapshot-apply.js";
449
- export { backupLocal } from "./sync-operations.js";
450
477
  export {
451
478
  canPullRemoteSessionsOnFirstSync,
452
479
  canPullRemoteSettingsOnFirstSync,