@davideasden/pi-undo 0.2.2 → 0.2.4

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/src/quarantine.ts CHANGED
@@ -73,6 +73,7 @@ export class QuarantineManager {
73
73
  private readonly beforeRestoreSourceCleanup: (() => void | Promise<void>) | undefined;
74
74
  private readonly afterRestoreSourceCleanup: (() => void | Promise<void>) | undefined;
75
75
  private readonly beforeTargetCreate: (() => void | Promise<void>) | undefined;
76
+ private readonly syncTargetArtifacts: boolean;
76
77
 
77
78
  constructor(options: {
78
79
  readonly workspaceRoot: string;
@@ -85,6 +86,7 @@ export class QuarantineManager {
85
86
  readonly beforeRestoreSourceCleanup?: () => void | Promise<void>;
86
87
  readonly afterRestoreSourceCleanup?: () => void | Promise<void>;
87
88
  readonly beforeTargetCreate?: () => void | Promise<void>;
89
+ readonly syncTargetArtifacts?: boolean;
88
90
  }) {
89
91
  this.requestedWorkspaceRoot = resolve(options.workspaceRoot);
90
92
  this.workspaceRoot = realpathSync(this.requestedWorkspaceRoot);
@@ -97,6 +99,7 @@ export class QuarantineManager {
97
99
  this.beforeRestoreSourceCleanup = options.beforeRestoreSourceCleanup;
98
100
  this.afterRestoreSourceCleanup = options.afterRestoreSourceCleanup;
99
101
  this.beforeTargetCreate = options.beforeTargetCreate;
102
+ this.syncTargetArtifacts = options.syncTargetArtifacts ?? true;
100
103
  }
101
104
 
102
105
  async replaceFile(request: ReplaceFileRequest): Promise<MutationRecord> {
@@ -120,7 +123,7 @@ export class QuarantineManager {
120
123
  targetArtifact,
121
124
  request.targetBytes,
122
125
  request.targetMode,
123
- { syncDirectory: false },
126
+ { syncDirectory: false, syncFile: this.syncTargetArtifacts },
124
127
  );
125
128
  await this.assertFingerprint(targetArtifact, request.path, request.targetFingerprint);
126
129
  await this.quarantineSource(intent);
@@ -182,7 +185,7 @@ export class QuarantineManager {
182
185
  replacement.targetAbsolutePath,
183
186
  replacement.request.targetBytes,
184
187
  replacement.request.targetMode,
185
- { syncDirectory: false },
188
+ { syncDirectory: false, syncFile: this.syncTargetArtifacts },
186
189
  );
187
190
  await this.assertFingerprint(
188
191
  replacement.targetAbsolutePath,
@@ -197,7 +200,7 @@ export class QuarantineManager {
197
200
  }
198
201
  await this.quarantineFileSources(replacements);
199
202
  const installDirectories = new Set<string>();
200
- for (const replacement of replacements) {
203
+ const installTarget = async (replacement: PreparedFileReplacement): Promise<void> => {
201
204
  const { request, targetArtifact, targetAbsolutePath } = replacement;
202
205
  await request.beforeInstall?.();
203
206
  await this.assertMutationPaths(request.path, targetArtifact);
@@ -211,13 +214,19 @@ export class QuarantineManager {
211
214
  throw error;
212
215
  }
213
216
  installDirectories.add(dirname(this.absolute(request.path)));
217
+ };
218
+ if (replacements.some(({ request }) => request.beforeInstall !== undefined)) {
219
+ for (const replacement of replacements) await installTarget(replacement);
220
+ } else {
221
+ await mapConcurrentFailClosed(replacements, BATCH_FILE_IO_CONCURRENCY, installTarget);
214
222
  }
215
223
  await syncDirectories(installDirectories);
216
- for (const { request, targetArtifact, targetAbsolutePath } of replacements) {
224
+ await mapConcurrentFailClosed(replacements, BATCH_FILE_IO_CONCURRENCY, async (replacement) => {
225
+ const { request, targetArtifact, targetAbsolutePath } = replacement;
217
226
  await this.assertArtifactPath(request.path, targetArtifact);
218
227
  await this.assertSameFileIdentity(this.absolute(request.path), targetAbsolutePath, request.path);
219
228
  await this.assertFingerprint(targetAbsolutePath, request.path, request.targetFingerprint);
220
- }
229
+ });
221
230
  const targetRecords = await this.journal.advanceBatch(replacements.map(({ intent }) => ({
222
231
  ordinal: intent.ordinal,
223
232
  states: ["SOURCE_QUARANTINED", "SOURCE_VERIFIED", "TARGET_INSTALLED", "TARGET_VERIFIED"],
@@ -242,17 +251,23 @@ export class QuarantineManager {
242
251
  private async quarantineFileSources(
243
252
  replacements: readonly { readonly intent: MutationRecord }[],
244
253
  ): Promise<void> {
254
+ const absentSources = replacements.filter(
255
+ ({ intent }) => intent.sourceFingerprint === fingerprintAbsent(intent.path),
256
+ );
257
+ await mapConcurrentFailClosed(absentSources, BATCH_FILE_IO_CONCURRENCY, async ({ intent }) => {
258
+ await this.assertFingerprint(this.absolute(intent.path), intent.path, intent.sourceFingerprint);
259
+ if (await exists(this.absolute(intent.sourceArtifact))) {
260
+ throw new QuarantineError("unsafe_artifact", `absent source 不应存在 artifact:${intent.path}`);
261
+ }
262
+ });
263
+ const presentSources = replacements.filter(
264
+ ({ intent }) => intent.sourceFingerprint !== fingerprintAbsent(intent.path),
265
+ );
245
266
  const capturedDirectories = new Set<string>();
246
- for (const { intent } of replacements) {
267
+ const captureSource = async ({ intent }: { readonly intent: MutationRecord }): Promise<void> => {
247
268
  const original = this.absolute(intent.path);
248
269
  const source = this.absolute(intent.sourceArtifact);
249
270
  await this.assertFingerprint(original, intent.path, intent.sourceFingerprint);
250
- if (intent.sourceFingerprint === fingerprintAbsent(intent.path)) {
251
- if (await exists(source)) {
252
- throw new QuarantineError("unsafe_artifact", `absent source 不应存在 artifact:${intent.path}`);
253
- }
254
- continue;
255
- }
256
271
  const metadata = await lstat(original);
257
272
  if (!metadata.isFile()) throw new QuarantineError("unsafe_artifact", "批量 quarantine 只支持普通文件");
258
273
  await this.beforeSourceCapture?.();
@@ -266,45 +281,56 @@ export class QuarantineManager {
266
281
  throw error;
267
282
  }
268
283
  capturedDirectories.add(dirname(original));
284
+ };
285
+ if (this.beforeSourceCapture === undefined) {
286
+ await mapConcurrentFailClosed(presentSources, BATCH_FILE_IO_CONCURRENCY, captureSource);
287
+ } else {
288
+ for (const replacement of presentSources) await captureSource(replacement);
269
289
  }
270
290
  await syncDirectories(capturedDirectories);
271
291
 
272
292
  const removedDirectories = new Set<string>();
273
- for (const { intent } of replacements) {
274
- if (intent.sourceFingerprint === fingerprintAbsent(intent.path)) continue;
293
+ const removeSource = async ({ intent }: { readonly intent: MutationRecord }): Promise<void> => {
275
294
  const original = this.absolute(intent.path);
276
295
  const source = this.absolute(intent.sourceArtifact);
277
296
  await this.beforeSourceRemove?.();
278
297
  await this.assertMutationPaths(intent.path, intent.sourceArtifact);
279
298
  await this.assertFingerprint(source, intent.path, intent.sourceFingerprint);
280
299
  await this.assertFingerprint(original, intent.path, intent.sourceFingerprint);
281
- const [sourceMetadata, originalMetadata] = await Promise.all([lstat(source), lstat(original)]);
282
- if (sourceMetadata.dev !== originalMetadata.dev || sourceMetadata.ino !== originalMetadata.ino) {
283
- throw new QuarantineError("external_concurrency", `source 隔离前原路径已变化:${intent.path}`);
284
- }
300
+ await this.assertSameFileIdentity(original, source, intent.path);
285
301
  await unlink(original);
286
302
  removedDirectories.add(dirname(original));
303
+ };
304
+ if (this.beforeSourceRemove === undefined) {
305
+ await mapConcurrentFailClosed(presentSources, BATCH_FILE_IO_CONCURRENCY, removeSource);
306
+ } else {
307
+ for (const replacement of presentSources) await removeSource(replacement);
287
308
  }
288
309
  await syncDirectories(removedDirectories);
289
- for (const { intent } of replacements) {
310
+ await mapConcurrentFailClosed(presentSources, BATCH_FILE_IO_CONCURRENCY, async ({ intent }) => {
290
311
  await this.assertFingerprint(this.absolute(intent.path), intent.path, fingerprintAbsent(intent.path));
291
- if (intent.sourceFingerprint !== fingerprintAbsent(intent.path)) {
292
- await this.assertFingerprint(
293
- this.absolute(intent.sourceArtifact),
294
- intent.path,
295
- intent.sourceFingerprint,
296
- );
297
- }
298
- }
312
+ await this.assertFingerprint(
313
+ this.absolute(intent.sourceArtifact),
314
+ intent.path,
315
+ intent.sourceFingerprint,
316
+ );
317
+ });
299
318
  }
300
319
 
301
320
  private async cleanupVerifiedArtifactsBatch(records: readonly MutationRecord[]): Promise<void> {
302
321
  const directories = new Set<string>();
303
- for (const owned of records) {
322
+ await mapConcurrentFailClosed(records, BATCH_FILE_IO_CONCURRENCY, async (owned) => {
304
323
  if (owned.state !== "TARGET_VERIFIED") {
305
324
  throw new QuarantineError("unsafe_artifact", "只有 TARGET_VERIFIED mutation 可以批量清理");
306
325
  }
307
- await this.assertFingerprint(this.absolute(owned.path), owned.path, owned.targetFingerprint);
326
+ if (owned.targetArtifact === null) {
327
+ await this.assertFingerprint(this.absolute(owned.path), owned.path, owned.targetFingerprint);
328
+ } else {
329
+ const target = this.absolute(owned.targetArtifact);
330
+ await this.assertArtifactPath(owned.path, owned.targetArtifact);
331
+ await this.assertSameFileIdentity(this.absolute(owned.path), target, owned.path);
332
+ await this.assertFingerprint(target, owned.path, owned.targetFingerprint);
333
+ }
308
334
  const source = this.absolute(owned.sourceArtifact);
309
335
  if (await exists(source)) {
310
336
  await this.assertArtifactPath(owned.path, owned.sourceArtifact);
@@ -321,7 +347,7 @@ export class QuarantineManager {
321
347
  directories.add(dirname(target));
322
348
  }
323
349
  }
324
- }
350
+ });
325
351
  await syncDirectories(directories);
326
352
  }
327
353
 
@@ -745,7 +771,7 @@ export class QuarantineManager {
745
771
 
746
772
  private async assertOwnedRecord(record: MutationRecord): Promise<MutationRecord> {
747
773
  await this.assertWorkspaceIdentity();
748
- const owned = (await this.journal.load()).find((candidate) => candidate.ordinal === record.ordinal);
774
+ const owned = await this.journal.loadOrdinal(record.ordinal);
749
775
  if (owned === undefined || immutableMutation(owned) !== immutableMutation(record)) {
750
776
  throw new QuarantineError("unsafe_artifact", "mutation record 未被当前 journal 精确登记");
751
777
  }
@@ -848,7 +874,7 @@ export function fingerprintAbsent(path: string): string {
848
874
  return checksum(canonicalJson({ path, kind: "absent" }));
849
875
  }
850
876
 
851
- async function fingerprintLeaf(file: string, logicalPath: string): Promise<string> {
877
+ export async function fingerprintLeaf(file: string, logicalPath: string): Promise<string> {
852
878
  const metadata = await lstat(file).catch((error) => {
853
879
  if (hasErrorCode(error, "ENOENT") || hasErrorCode(error, "ENOTDIR")) return null;
854
880
  throw error;