@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.
@@ -20,7 +20,7 @@ import type {
20
20
  SnapshotManifest,
21
21
  SnapshotRoot,
22
22
  } from "./model.ts";
23
- import { assertNoSymlinkEscape, relativeSafePath } from "./path-safety.ts";
23
+ import { assertNoSymlinkEscape, pathSetsOverlap, relativeSafePath } from "./path-safety.ts";
24
24
  import { RootDiscovery, type RootTopology } from "./root-discovery.ts";
25
25
  import { WorkspaceLock } from "./workspace-lock.ts";
26
26
 
@@ -96,6 +96,12 @@ export interface CaptureOptions {
96
96
  readonly excludePaths?: readonly string[];
97
97
  }
98
98
 
99
+ export interface SnapshotBlobRequest {
100
+ readonly rootPath: string;
101
+ readonly blobId: string;
102
+ readonly relativePath?: string;
103
+ }
104
+
99
105
  export type SnapshotStoreErrorCode =
100
106
  | "capture_failed"
101
107
  | "invalid_manifest_id"
@@ -125,6 +131,7 @@ export interface SnapshotStore {
125
131
  pin(id: ManifestId, reason: string): Promise<void>;
126
132
  unpin(id: ManifestId, reason: string): Promise<void>;
127
133
  collectGarbage(): Promise<number>;
134
+ durableCacheDirectory(): Promise<string>;
128
135
  }
129
136
 
130
137
  export class SnapshotStore {
@@ -150,6 +157,39 @@ export class SnapshotStore {
150
157
  this.clock = options.clock ?? Date.now;
151
158
  }
152
159
 
160
+ static supportsValidatedBlobBatch(store: SnapshotStore): boolean {
161
+ return store instanceof SnapshotStore &&
162
+ store.readBlob === SnapshotStore.prototype.readBlob &&
163
+ store.loadManifest === SnapshotStore.prototype.loadManifest;
164
+ }
165
+
166
+ static async readBlobs(
167
+ store: SnapshotStore,
168
+ id: ManifestId,
169
+ requests: readonly SnapshotBlobRequest[],
170
+ ): Promise<readonly Uint8Array[]> {
171
+ if (requests.length === 0) return [];
172
+ if (SnapshotStore.supportsValidatedBlobBatch(store)) {
173
+ return (store as SnapshotStore).readBlobsValidated(id, requests);
174
+ }
175
+ const result: Uint8Array[] = [];
176
+ for (const request of requests) {
177
+ result.push(await store.readBlob(
178
+ id,
179
+ request.rootPath,
180
+ request.blobId,
181
+ request.relativePath,
182
+ ));
183
+ }
184
+ return result;
185
+ }
186
+
187
+ async durableCacheDirectory(): Promise<string> {
188
+ const directory = join(this.storeRoot, "durable-cache");
189
+ await mkdir(directory, { recursive: true });
190
+ return directory;
191
+ }
192
+
153
193
  async capture(
154
194
  topology: RootTopology,
155
195
  scope?: readonly string[],
@@ -302,7 +342,10 @@ export class SnapshotStore {
302
342
  }
303
343
 
304
344
  async loadManifest(id: ManifestId): Promise<SnapshotManifest> {
305
- const manifestPath = await this.findManifestPath(id);
345
+ return this.loadManifestFromPath(id, await this.findManifestPath(id));
346
+ }
347
+
348
+ private async loadManifestFromPath(id: ManifestId, manifestPath: string): Promise<SnapshotManifest> {
306
349
  try {
307
350
  const value: unknown = JSON.parse(await readFile(manifestPath, "utf8"));
308
351
  const manifest = assertManifest(value);
@@ -342,10 +385,10 @@ export class SnapshotStore {
342
385
  const gitDirectory = this.rootGitDirectory(storeDirectory, root);
343
386
  await this.assertNoAlternates(gitDirectory);
344
387
  const entries = await this.readTreeEntries(gitDirectory, root.treeId, rootScope);
345
- if (root.ignoredPresentPaths.some((ignoredPath) => entries.some(
346
- (entry) => isPathAtOrBelow(ignoredPath, entry.relativePath) ||
347
- isPathAtOrBelow(entry.relativePath, ignoredPath),
348
- ))) {
388
+ if (pathSetsOverlap(
389
+ root.ignoredPresentPaths,
390
+ entries.map((entry) => entry.relativePath),
391
+ )) {
349
392
  throw new SnapshotStoreError("object_missing", "ignored-present proof 与 root tree 冲突");
350
393
  }
351
394
  await this.assertObjectsComplete(gitDirectory, root.treeId, entries);
@@ -425,41 +468,67 @@ export class SnapshotStore {
425
468
  blobId: string,
426
469
  relativePath?: string,
427
470
  ): Promise<Uint8Array> {
428
- relativeSafePath("/", rootPath);
429
- if (!isObjectId(blobId)) {
430
- throw new SnapshotStoreError("object_missing", "blob ID 无效");
471
+ return (await this.readBlobOperation(id, [{ rootPath, blobId, relativePath }], false))[0]!;
472
+ }
473
+
474
+ private readBlobsValidated(
475
+ id: ManifestId,
476
+ requests: readonly SnapshotBlobRequest[],
477
+ ): Promise<readonly Uint8Array[]> {
478
+ return this.readBlobOperation(id, requests, true);
479
+ }
480
+
481
+ private async readBlobOperation(
482
+ id: ManifestId,
483
+ requests: readonly SnapshotBlobRequest[],
484
+ revalidateManifest: boolean,
485
+ ): Promise<readonly Uint8Array[]> {
486
+ for (const request of requests) {
487
+ relativeSafePath("/", request.rootPath);
488
+ if (!isObjectId(request.blobId)) {
489
+ throw new SnapshotStoreError("object_missing", "blob ID 无效");
490
+ }
431
491
  }
432
492
  const manifestPath = await this.findManifestPath(id);
433
- const manifest = await this.loadManifest(id);
434
- const root = manifest.roots.find((candidate) => candidate.relativeRoot === rootPath);
435
- if (root === undefined) {
436
- throw new SnapshotStoreError("root_not_found", "manifest 中不存在指定 root");
437
- }
438
- if (root.state !== "active" || root.treeId === null) {
439
- throw new SnapshotStoreError("object_missing", "指定 root 没有可读取的 tree");
440
- }
441
-
493
+ const manifest = revalidateManifest
494
+ ? await this.loadManifestFromPath(id, manifestPath)
495
+ : await this.loadManifest(id);
496
+ const roots = new Map(manifest.roots.map((root) => [root.relativeRoot, root]));
442
497
  const storeDirectory = dirname(dirname(manifestPath));
443
- const gitDirectory = this.rootGitDirectory(storeDirectory, root);
444
498
  try {
445
- const safeRelativePath = relativePath === undefined ? undefined : relativeSafePath("/", relativePath);
446
- if (safeRelativePath === undefined) {
447
- const entries = await this.readTreeEntries(gitDirectory, root.treeId);
448
- if (!entries.some((entry) => entry.objectId === blobId)) {
449
- throw new SnapshotStoreError("object_missing", "blob 不属于指定 root tree");
499
+ const result: Uint8Array[] = [];
500
+ for (const request of requests) {
501
+ const root = roots.get(request.rootPath);
502
+ if (root === undefined) {
503
+ throw new SnapshotStoreError("root_not_found", "manifest 中不存在指定 root");
450
504
  }
451
- } else {
452
- const membershipKey = treeBlobMembershipKey(gitDirectory, root.treeId, safeRelativePath);
453
- let ownedBlobId = this.treeBlobMembership.get(membershipKey);
454
- if (ownedBlobId === undefined) {
455
- await this.readTreeEntries(gitDirectory, root.treeId, [safeRelativePath]);
456
- ownedBlobId = this.treeBlobMembership.get(membershipKey);
505
+ if (root.state !== "active" || root.treeId === null) {
506
+ throw new SnapshotStoreError("object_missing", "指定 root 没有可读取的 tree");
457
507
  }
458
- if (ownedBlobId !== blobId) {
459
- throw new SnapshotStoreError("object_missing", "blob 不属于指定 root tree path");
508
+ const gitDirectory = this.rootGitDirectory(storeDirectory, root);
509
+ const safeRelativePath = request.relativePath === undefined
510
+ ? undefined
511
+ : relativeSafePath("/", request.relativePath);
512
+ if (safeRelativePath === undefined) {
513
+ const entries = await this.readTreeEntries(gitDirectory, root.treeId);
514
+ if (!entries.some((entry) => entry.objectId === request.blobId)) {
515
+ throw new SnapshotStoreError("object_missing", "blob 不属于指定 root tree");
516
+ }
517
+ } else {
518
+ const membershipKey = treeBlobMembershipKey(gitDirectory, root.treeId, safeRelativePath);
519
+ let ownedBlobId = this.treeBlobMembership.get(membershipKey);
520
+ if (ownedBlobId === undefined) {
521
+ await this.readTreeEntries(gitDirectory, root.treeId, [safeRelativePath]);
522
+ ownedBlobId = this.treeBlobMembership.get(membershipKey);
523
+ }
524
+ if (ownedBlobId !== request.blobId) {
525
+ throw new SnapshotStoreError("object_missing", "blob 不属于指定 root tree path");
526
+ }
460
527
  }
528
+ result.push(new Uint8Array(await this.readBlobBytes(gitDirectory, request.blobId)));
461
529
  }
462
- return new Uint8Array(await this.readBlobBytes(gitDirectory, blobId));
530
+ if (revalidateManifest) await this.loadManifestFromPath(id, manifestPath);
531
+ return result;
463
532
  } catch (error) {
464
533
  if (error instanceof SnapshotStoreError) {
465
534
  throw error;
@@ -627,7 +696,7 @@ export class SnapshotStore {
627
696
  gitBacked: boolean,
628
697
  inclusions: readonly string[] | null,
629
698
  exclusions: readonly string[],
630
- exactExclusions: readonly string[],
699
+ exactExclusions: ReadonlySet<string>,
631
700
  ): Promise<string[]> {
632
701
  if (inclusions === null) {
633
702
  return [];
@@ -650,7 +719,7 @@ export class SnapshotStore {
650
719
  for (const relativePath of parseNulPaths(output)) {
651
720
  if (
652
721
  exclusions.some((excluded) => isPathAtOrBelow(excluded, relativePath)) ||
653
- exactExclusions.includes(relativePath)
722
+ exactExclusions.has(relativePath)
654
723
  ) {
655
724
  continue;
656
725
  }
@@ -679,7 +748,7 @@ export class SnapshotStore {
679
748
  gitBacked: boolean,
680
749
  inclusions: readonly string[] | null,
681
750
  exclusions: readonly string[],
682
- exactExclusions: readonly string[],
751
+ exactExclusions: ReadonlySet<string>,
683
752
  ): Promise<void> {
684
753
  const leaves = await this.collectVisibleLeaves(
685
754
  cwd,
@@ -751,7 +820,7 @@ export class SnapshotStore {
751
820
  gitBacked: boolean,
752
821
  inclusions: readonly string[] | null,
753
822
  exclusions: readonly string[],
754
- exactExclusions: readonly string[],
823
+ exactExclusions: ReadonlySet<string>,
755
824
  excludeDeleted = false,
756
825
  ): Promise<string[]> {
757
826
  if (inclusions === null) return [];
@@ -785,7 +854,7 @@ export class SnapshotStore {
785
854
  return parseNulPaths(output).filter((relativePath) =>
786
855
  !deletedPaths.has(relativePath) &&
787
856
  !exclusions.some((excluded) => isPathAtOrBelow(excluded, relativePath)) &&
788
- !exactExclusions.includes(relativePath));
857
+ !exactExclusions.has(relativePath));
789
858
  }
790
859
 
791
860
  private async collectVisibleLeaves(
@@ -794,7 +863,7 @@ export class SnapshotStore {
794
863
  gitBacked: boolean,
795
864
  inclusions: readonly string[] | null,
796
865
  exclusions: readonly string[],
797
- exactExclusions: readonly string[],
866
+ exactExclusions: ReadonlySet<string>,
798
867
  ): Promise<VisibleLeaf[]> {
799
868
  const paths = await this.queryVisibleLeafPaths(
800
869
  cwd,
@@ -1189,7 +1258,7 @@ function ownedArtifactExclusions(
1189
1258
  roots: readonly DiscoveryRoot[],
1190
1259
  rootPath: string,
1191
1260
  exclusions: readonly string[],
1192
- ): string[] {
1261
+ ): ReadonlySet<string> {
1193
1262
  const result: string[] = [];
1194
1263
  for (const exclusion of exclusions) {
1195
1264
  const owner = roots
@@ -1204,7 +1273,7 @@ function ownedArtifactExclusions(
1204
1273
  }
1205
1274
  result.push(rootRelativePath(rootPath, exclusion));
1206
1275
  }
1207
- return result;
1276
+ return new Set(result);
1208
1277
  }
1209
1278
 
1210
1279
  function rootRelativeScope(rootPath: string, scope: readonly string[] | undefined): string[] | undefined {
@@ -116,7 +116,7 @@ export class WorkspaceLock {
116
116
  await rename(candidateDirectory, lockDirectory);
117
117
  published = true;
118
118
  } catch (error) {
119
- if (!await pathExists(lockDirectory)) {
119
+ if (!isPublishCollision(error) && !await pathExists(lockDirectory)) {
120
120
  throw error;
121
121
  }
122
122
  } finally {
@@ -402,6 +402,10 @@ function assertPositive(value: number, name: string): void {
402
402
  }
403
403
  }
404
404
 
405
+ function isPublishCollision(error: unknown): boolean {
406
+ return hasErrorCode(error, "EEXIST") || hasErrorCode(error, "ENOTEMPTY");
407
+ }
408
+
405
409
  function hasErrorCode(error: unknown, code: string): error is NodeJS.ErrnoException {
406
410
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
407
411
  }