@smthrs/artifacts 0.0.0-stage → 1.0.0-rc.3

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.
Files changed (171) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +173 -2
  4. package/dist/cjs/ArtifactBackupLease.d.ts +34 -0
  5. package/dist/cjs/ArtifactBackupLease.d.ts.map +1 -0
  6. package/dist/cjs/ArtifactBackupLease.js +175 -0
  7. package/dist/cjs/ArtifactBackupLease.js.map +7 -0
  8. package/dist/cjs/ArtifactStore.d.ts +129 -0
  9. package/dist/cjs/ArtifactStore.d.ts.map +1 -0
  10. package/dist/cjs/ArtifactStore.js +73 -0
  11. package/dist/cjs/ArtifactStore.js.map +7 -0
  12. package/dist/cjs/ArtifactStoreError.d.ts +82 -0
  13. package/dist/cjs/ArtifactStoreError.d.ts.map +1 -0
  14. package/dist/cjs/ArtifactStoreError.js +72 -0
  15. package/dist/cjs/ArtifactStoreError.js.map +7 -0
  16. package/dist/cjs/ArtifactStoreMetrics.d.ts +70 -0
  17. package/dist/cjs/ArtifactStoreMetrics.d.ts.map +1 -0
  18. package/dist/cjs/ArtifactStoreMetrics.js +51 -0
  19. package/dist/cjs/ArtifactStoreMetrics.js.map +7 -0
  20. package/dist/cjs/ArtifactSweep.d.ts +185 -0
  21. package/dist/cjs/ArtifactSweep.d.ts.map +1 -0
  22. package/dist/cjs/ArtifactSweep.js +163 -0
  23. package/dist/cjs/ArtifactSweep.js.map +7 -0
  24. package/dist/cjs/CombinedArtifacts.d.ts +103 -0
  25. package/dist/cjs/CombinedArtifacts.d.ts.map +1 -0
  26. package/dist/cjs/CombinedArtifacts.js +189 -0
  27. package/dist/cjs/CombinedArtifacts.js.map +7 -0
  28. package/dist/cjs/FileLease.d.ts +113 -0
  29. package/dist/cjs/FileLease.d.ts.map +1 -0
  30. package/dist/cjs/FileLease.js +201 -0
  31. package/dist/cjs/FileLease.js.map +7 -0
  32. package/dist/cjs/FileSystemArtifactStore.d.ts +93 -0
  33. package/dist/cjs/FileSystemArtifactStore.d.ts.map +1 -0
  34. package/dist/cjs/FileSystemArtifactStore.js +276 -0
  35. package/dist/cjs/FileSystemArtifactStore.js.map +7 -0
  36. package/dist/cjs/MemoryArtifactStore.d.ts +25 -0
  37. package/dist/cjs/MemoryArtifactStore.d.ts.map +1 -0
  38. package/dist/cjs/MemoryArtifactStore.js +84 -0
  39. package/dist/cjs/MemoryArtifactStore.js.map +7 -0
  40. package/dist/cjs/NoopArtifactStore.d.ts +17 -0
  41. package/dist/cjs/NoopArtifactStore.d.ts.map +1 -0
  42. package/dist/cjs/NoopArtifactStore.js +46 -0
  43. package/dist/cjs/NoopArtifactStore.js.map +7 -0
  44. package/dist/cjs/RemoteArtifacts.d.ts +237 -0
  45. package/dist/cjs/RemoteArtifacts.d.ts.map +1 -0
  46. package/dist/cjs/RemoteArtifacts.js +405 -0
  47. package/dist/cjs/RemoteArtifacts.js.map +7 -0
  48. package/dist/cjs/index.d.ts +65 -0
  49. package/dist/cjs/index.d.ts.map +1 -0
  50. package/dist/cjs/index.js +47 -0
  51. package/dist/cjs/index.js.map +7 -0
  52. package/dist/cjs/internal/ArtifactLocks.d.ts +30 -0
  53. package/dist/cjs/internal/ArtifactLocks.d.ts.map +1 -0
  54. package/dist/cjs/internal/ArtifactLocks.js +93 -0
  55. package/dist/cjs/internal/ArtifactLocks.js.map +7 -0
  56. package/dist/cjs/internal/ArtifactPath.d.ts +19 -0
  57. package/dist/cjs/internal/ArtifactPath.d.ts.map +1 -0
  58. package/dist/cjs/internal/ArtifactPath.js +74 -0
  59. package/dist/cjs/internal/ArtifactPath.js.map +7 -0
  60. package/dist/cjs/measureBytes.d.ts +18 -0
  61. package/dist/cjs/measureBytes.d.ts.map +1 -0
  62. package/dist/cjs/measureBytes.js +43 -0
  63. package/dist/cjs/measureBytes.js.map +7 -0
  64. package/dist/cjs/package.json +1 -0
  65. package/dist/cjs/snapshotBytes.d.ts +19 -0
  66. package/dist/cjs/snapshotBytes.d.ts.map +1 -0
  67. package/dist/cjs/snapshotBytes.js +41 -0
  68. package/dist/cjs/snapshotBytes.js.map +7 -0
  69. package/dist/cjs/validateDigest.d.ts +22 -0
  70. package/dist/cjs/validateDigest.d.ts.map +1 -0
  71. package/dist/cjs/validateDigest.js +42 -0
  72. package/dist/cjs/validateDigest.js.map +7 -0
  73. package/dist/esm/ArtifactBackupLease.d.ts +34 -0
  74. package/dist/esm/ArtifactBackupLease.d.ts.map +1 -0
  75. package/dist/esm/ArtifactBackupLease.js +117 -0
  76. package/dist/esm/ArtifactBackupLease.js.map +1 -0
  77. package/dist/esm/ArtifactStore.d.ts +129 -0
  78. package/dist/esm/ArtifactStore.d.ts.map +1 -0
  79. package/dist/esm/ArtifactStore.js +83 -0
  80. package/dist/esm/ArtifactStore.js.map +1 -0
  81. package/dist/esm/ArtifactStoreError.d.ts +82 -0
  82. package/dist/esm/ArtifactStoreError.d.ts.map +1 -0
  83. package/dist/esm/ArtifactStoreError.js +77 -0
  84. package/dist/esm/ArtifactStoreError.js.map +1 -0
  85. package/dist/esm/ArtifactStoreMetrics.d.ts +70 -0
  86. package/dist/esm/ArtifactStoreMetrics.d.ts.map +1 -0
  87. package/dist/esm/ArtifactStoreMetrics.js +77 -0
  88. package/dist/esm/ArtifactStoreMetrics.js.map +1 -0
  89. package/dist/esm/ArtifactSweep.d.ts +185 -0
  90. package/dist/esm/ArtifactSweep.d.ts.map +1 -0
  91. package/dist/esm/ArtifactSweep.js +202 -0
  92. package/dist/esm/ArtifactSweep.js.map +1 -0
  93. package/dist/esm/CombinedArtifacts.d.ts +103 -0
  94. package/dist/esm/CombinedArtifacts.d.ts.map +1 -0
  95. package/dist/esm/CombinedArtifacts.js +214 -0
  96. package/dist/esm/CombinedArtifacts.js.map +1 -0
  97. package/dist/esm/FileLease.d.ts +113 -0
  98. package/dist/esm/FileLease.d.ts.map +1 -0
  99. package/dist/esm/FileLease.js +239 -0
  100. package/dist/esm/FileLease.js.map +1 -0
  101. package/dist/esm/FileSystemArtifactStore.d.ts +93 -0
  102. package/dist/esm/FileSystemArtifactStore.d.ts.map +1 -0
  103. package/dist/esm/FileSystemArtifactStore.js +336 -0
  104. package/dist/esm/FileSystemArtifactStore.js.map +1 -0
  105. package/dist/esm/MemoryArtifactStore.d.ts +25 -0
  106. package/dist/esm/MemoryArtifactStore.d.ts.map +1 -0
  107. package/dist/esm/MemoryArtifactStore.js +67 -0
  108. package/dist/esm/MemoryArtifactStore.js.map +1 -0
  109. package/dist/esm/NoopArtifactStore.d.ts +17 -0
  110. package/dist/esm/NoopArtifactStore.d.ts.map +1 -0
  111. package/dist/esm/NoopArtifactStore.js +27 -0
  112. package/dist/esm/NoopArtifactStore.js.map +1 -0
  113. package/dist/esm/RemoteArtifacts.d.ts +237 -0
  114. package/dist/esm/RemoteArtifacts.d.ts.map +1 -0
  115. package/dist/esm/RemoteArtifacts.js +554 -0
  116. package/dist/esm/RemoteArtifacts.js.map +1 -0
  117. package/dist/esm/index.d.ts +65 -0
  118. package/dist/esm/index.d.ts.map +1 -0
  119. package/dist/esm/index.js +65 -0
  120. package/dist/esm/index.js.map +1 -0
  121. package/dist/esm/internal/ArtifactLocks.d.ts +30 -0
  122. package/dist/esm/internal/ArtifactLocks.d.ts.map +1 -0
  123. package/dist/esm/internal/ArtifactLocks.js +86 -0
  124. package/dist/esm/internal/ArtifactLocks.js.map +1 -0
  125. package/dist/esm/internal/ArtifactPath.d.ts +19 -0
  126. package/dist/esm/internal/ArtifactPath.d.ts.map +1 -0
  127. package/dist/esm/internal/ArtifactPath.js +59 -0
  128. package/dist/esm/internal/ArtifactPath.js.map +1 -0
  129. package/dist/esm/measureBytes.d.ts +18 -0
  130. package/dist/esm/measureBytes.d.ts.map +1 -0
  131. package/dist/esm/measureBytes.js +22 -0
  132. package/dist/esm/measureBytes.js.map +1 -0
  133. package/dist/esm/snapshotBytes.d.ts +19 -0
  134. package/dist/esm/snapshotBytes.d.ts.map +1 -0
  135. package/dist/esm/snapshotBytes.js +30 -0
  136. package/dist/esm/snapshotBytes.js.map +1 -0
  137. package/dist/esm/validateDigest.d.ts +22 -0
  138. package/dist/esm/validateDigest.d.ts.map +1 -0
  139. package/dist/esm/validateDigest.js +26 -0
  140. package/dist/esm/validateDigest.js.map +1 -0
  141. package/docs/README.md +200 -0
  142. package/docs/api.md +474 -0
  143. package/docs/concepts/content-addressing.md +174 -0
  144. package/docs/concepts/coordination.md +141 -0
  145. package/docs/concepts/tiers.md +147 -0
  146. package/docs/guides/fence-a-backup.md +123 -0
  147. package/docs/guides/reclaim-disk-space.md +143 -0
  148. package/docs/guides/serve-the-artifact-protocol.md +140 -0
  149. package/docs/guides/share-artifacts-across-machines.md +180 -0
  150. package/docs/guides/test-against-an-artifact-store.md +158 -0
  151. package/docs/installation.md +98 -0
  152. package/docs/quickstart.md +182 -0
  153. package/docs/troubleshooting.md +232 -0
  154. package/package.json +179 -3
  155. package/src/ArtifactBackupLease.ts +213 -0
  156. package/src/ArtifactStore.ts +152 -0
  157. package/src/ArtifactStoreError.ts +96 -0
  158. package/src/ArtifactStoreMetrics.ts +83 -0
  159. package/src/ArtifactSweep.ts +336 -0
  160. package/src/CombinedArtifacts.ts +369 -0
  161. package/src/FileLease.ts +380 -0
  162. package/src/FileSystemArtifactStore.ts +459 -0
  163. package/src/MemoryArtifactStore.ts +83 -0
  164. package/src/NoopArtifactStore.ts +30 -0
  165. package/src/RemoteArtifacts.ts +809 -0
  166. package/src/index.ts +71 -0
  167. package/src/internal/ArtifactLocks.ts +110 -0
  168. package/src/internal/ArtifactPath.ts +72 -0
  169. package/src/measureBytes.ts +28 -0
  170. package/src/snapshotBytes.ts +34 -0
  171. package/src/validateDigest.ts +31 -0
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var ArtifactSweep_exports = {};
30
+ __export(ArtifactSweep_exports, {
31
+ ArtifactSweep: () => ArtifactSweep,
32
+ layerFileSystem: () => layerFileSystem,
33
+ layerNoop: () => layerNoop,
34
+ makeFileSystem: () => makeFileSystem,
35
+ makeNoop: () => makeNoop
36
+ });
37
+ module.exports = __toCommonJS(ArtifactSweep_exports);
38
+ var Context = __toESM(require("effect/Context"), 1);
39
+ var Effect = __toESM(require("effect/Effect"), 1);
40
+ var FileSystem = __toESM(require("effect/FileSystem"), 1);
41
+ var Layer = __toESM(require("effect/Layer"), 1);
42
+ var Option = __toESM(require("effect/Option"), 1);
43
+ var PlatformError = __toESM(require("effect/PlatformError"), 1);
44
+ var ArtifactBackupLease = __toESM(require("./ArtifactBackupLease.js"), 1);
45
+ var ArtifactStore = __toESM(require("./ArtifactStore.js"), 1);
46
+ var ArtifactLocks = __toESM(require("./internal/ArtifactLocks.js"), 1);
47
+ var ArtifactPath = __toESM(require("./internal/ArtifactPath.js"), 1);
48
+ class ArtifactSweep extends Context.Service()("@smthrs/artifacts/ArtifactSweep") {
49
+ }
50
+ const error = (message, cause) => new ArtifactStore.ArtifactStoreError({
51
+ code: "unavailable",
52
+ message,
53
+ ...cause === void 0 ? {} : { cause }
54
+ });
55
+ const hostFailure = (cause) => error(`the host filesystem refused a sweep operation: ${String(cause)}`, cause);
56
+ const isNotFound = (cause) => cause instanceof PlatformError.PlatformError && cause.reason._tag === "NotFound";
57
+ const makeFileSystem = (fs, options = {}) => {
58
+ const directory = (options.directory ?? ArtifactStore.defaultDirectory).replace(/([^/])\/+$/, "$1");
59
+ const coordination = options.coordination ?? "required";
60
+ const blobPath = (digest) => `${directory}/${digest.slice(0, 2)}/${digest}`;
61
+ const inventory = Effect.gen(function* () {
62
+ const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure));
63
+ const parents = yield* fs.readDirectory(directory).pipe(
64
+ Effect.catch((cause) => isNotFound(cause) ? Effect.succeed([]) : Effect.fail(hostFailure(cause)))
65
+ );
66
+ const entries = [];
67
+ for (const parent of parents) {
68
+ if (!/^[0-9a-f]{2}$/.test(parent)) continue;
69
+ yield* checkRoot.pipe(Effect.mapError(hostFailure));
70
+ const safe = yield* ArtifactPath.guard(fs, `${directory}/${parent}`).pipe(Effect.option);
71
+ if (Option.isNone(safe)) continue;
72
+ const children = yield* fs.readDirectory(`${directory}/${parent}`).pipe(
73
+ Effect.orElseSucceed(() => [])
74
+ );
75
+ for (const child of children) entries.push(`${parent}/${child}`);
76
+ }
77
+ const candidates = entries.filter((entry) => {
78
+ if (entry.includes(".tmp-")) return false;
79
+ const separator = entry.indexOf("/");
80
+ const fan = entry.slice(0, separator);
81
+ const digest = entry.slice(separator + 1);
82
+ return /^[0-9a-f]{64}$/.test(digest) && fan === digest.slice(0, 2);
83
+ });
84
+ const blobs = yield* Effect.forEach(candidates, (entry) => Effect.gen(function* () {
85
+ const separator = entry.indexOf("/");
86
+ const fan = entry.slice(0, separator);
87
+ const digest = entry.slice(separator + 1);
88
+ const info = yield* Effect.gen(function* () {
89
+ yield* checkRoot;
90
+ yield* ArtifactPath.guard(fs, `${directory}/${fan}`);
91
+ yield* ArtifactPath.guard(fs, `${directory}/${entry}`, "File");
92
+ return yield* fs.stat(`${directory}/${entry}`);
93
+ }).pipe(Effect.option);
94
+ if (Option.isNone(info) || info.value.type !== "File") return void 0;
95
+ const mtime = Option.getOrUndefined(info.value.mtime);
96
+ if (mtime === void 0) return void 0;
97
+ return {
98
+ digest,
99
+ modifiedAtMs: mtime.getTime(),
100
+ sizeBytes: Number(info.value.size)
101
+ };
102
+ }), { concurrency: 16 });
103
+ return blobs.filter((blob) => blob !== void 0);
104
+ });
105
+ const remove = Effect.fn("ArtifactSweep.remove")(
106
+ (digest, removeOptions) => Effect.gen(function* () {
107
+ const validated = yield* ArtifactStore.validateDigest(digest);
108
+ yield* Effect.annotateCurrentSpan({ digest: validated });
109
+ const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure));
110
+ const checkParent = yield* ArtifactPath.guard(fs, `${directory}/${validated.slice(0, 2)}`).pipe(
111
+ Effect.mapError(hostFailure)
112
+ );
113
+ return yield* ArtifactLocks.withDigest(
114
+ fs,
115
+ directory,
116
+ validated,
117
+ coordination === "process" ? removeBlob() : ArtifactBackupLease.unlessActive(fs, directory, removeBlob(), hostFailure).pipe(
118
+ Effect.map(Option.getOrElse(() => false))
119
+ ),
120
+ hostFailure,
121
+ coordination
122
+ );
123
+ function removeBlob() {
124
+ return Effect.gen(function* () {
125
+ const path = blobPath(validated);
126
+ yield* checkRoot.pipe(Effect.mapError(hostFailure));
127
+ yield* checkParent.pipe(Effect.mapError(hostFailure));
128
+ const checkBlob = yield* ArtifactPath.guard(fs, path, "File").pipe(Effect.mapError(hostFailure));
129
+ const bound = removeOptions?.ifUnmodifiedSinceMs;
130
+ if (bound !== void 0) {
131
+ const info = yield* fs.stat(path).pipe(Effect.option);
132
+ if (Option.isNone(info)) return false;
133
+ const mtime = Option.getOrUndefined(info.value.mtime);
134
+ if (mtime === void 0 || mtime.getTime() > bound) return false;
135
+ }
136
+ yield* checkRoot.pipe(Effect.mapError(hostFailure));
137
+ yield* checkParent.pipe(Effect.mapError(hostFailure));
138
+ yield* checkBlob.pipe(Effect.mapError(hostFailure));
139
+ return yield* fs.remove(path).pipe(
140
+ Effect.as(true),
141
+ // The blob may have been removed between the fence and here; only a
142
+ // refusal over bytes that still exist is a host failure.
143
+ Effect.catch(
144
+ (cause) => fs.exists(path).pipe(
145
+ Effect.mapError((probeCause) => hostFailure({ remove: cause, existenceProbe: probeCause })),
146
+ Effect.flatMap((present) => present ? Effect.fail(hostFailure(cause)) : Effect.succeed(false))
147
+ )
148
+ )
149
+ );
150
+ });
151
+ }
152
+ })
153
+ );
154
+ return { inventory, remove };
155
+ };
156
+ const layerFileSystem = (options = {}) => Layer.effect(ArtifactSweep)(Effect.map(FileSystem.FileSystem, (fs) => makeFileSystem(fs, options)));
157
+ const makeNoop = (overrides = {}) => ({
158
+ inventory: Effect.fail(error("inventory is unavailable")),
159
+ remove: Effect.fn("ArtifactSweep.remove")(() => Effect.fail(error("remove is unavailable"))),
160
+ ...overrides
161
+ });
162
+ const layerNoop = (overrides = {}) => Layer.succeed(ArtifactSweep)(makeNoop(overrides));
163
+ //# sourceMappingURL=ArtifactSweep.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ArtifactSweep.ts"],
4
+ "sourcesContent": ["/**\n * The sweep half of artifact garbage collection: enumerate this host's blobs\n * and delete one, fenced by its age.\n *\n * This surface is deliberately NOT part of `ArtifactStore.Service`. The store\n * contract is shared by memory, filesystem, and network tiers, and a remote\n * tier can neither enumerate its address space nor accept a delete \u2014 Bazel\n * draws the same line: its disk-cache collector\n * (class `DiskCacheGarbageCollector` in\n * `com.google.devtools.build.lib.remote.disk`) walks the local directory, while\n * the remote tier owns its own retention. Only the\n * host-local filesystem store implements this.\n *\n * The *policy* \u2014 which digests are live, how old a dead blob must be before it\n * goes \u2014 belongs to the engine composition, which is the only place the\n * durable roots (attempt rows, cache entries) are visible. This module ships\n * mechanics alone; `@smthrs/engine-store` owns the mark policy and grace\n * period.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as FileSystem from \"effect/FileSystem\"\nimport * as Layer from \"effect/Layer\"\nimport * as Option from \"effect/Option\"\nimport * as PlatformError from \"effect/PlatformError\"\nimport * as ArtifactBackupLease from \"./ArtifactBackupLease.ts\"\nimport * as ArtifactStore from \"./ArtifactStore.ts\"\nimport * as ArtifactLocks from \"./internal/ArtifactLocks.ts\"\nimport * as ArtifactPath from \"./internal/ArtifactPath.ts\"\n\n/**\n * One enumerated blob: its content address, when it was last written or\n * freshened, and its size.\n *\n * `modifiedAtMs` is the age evidence a sweep fences on \u2014 the same signal git\n * (`git prune --expire`) and Bazel's disk-cache collector read, because it is\n * the one timestamp a filesystem maintains without any bookkeeping of ours.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface BlobStat {\n readonly digest: string\n readonly modifiedAtMs: number\n readonly sizeBytes: number\n}\n\n/**\n * Fencing predicate for a sweep deletion.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface RemoveOptions {\n /**\n * Deletes the blob only while its mtime is still at or before this bound.\n * A blob freshened after the caller computed its live set \u2014 a concurrent\n * `put` re-referencing the same bytes \u2014 fails the fence and survives, the\n * same shape as `CacheStore.evict`'s `ifRecordedBy`: the guard rides in the\n * delete, never in a prior read alone.\n */\n readonly ifUnmodifiedSinceMs?: number | undefined\n}\n\n/**\n * Host-local blob enumeration and fenced deletion.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface Service {\n /**\n * Every content-addressed blob this host's store currently holds. In-flight\n * `.tmp-*` scratch files, foreign files, and blobs whose age cannot be\n * measured are excluded \u2014 what the sweep cannot judge it must not touch.\n */\n readonly inventory: Effect.Effect<Array<BlobStat>, ArtifactStore.ArtifactStoreError>\n /**\n * Deletes the blob at `digest`, returning whether bytes were removed. A\n * missing blob reports `false` rather than failing, so a crashed and\n * re-run sweep converges instead of erroring on its own progress.\n *\n * `false` covers three outcomes a caller cannot tell apart, all of which mean\n * \"nothing was reclaimed, and retrying later is safe\": the blob was already\n * gone, it failed the `ifUnmodifiedSinceMs` fence, or a live backup lease\n * fenced the deletion (see `ArtifactBackupLease`). The last one is not\n * progress \u2014 the blob is still there \u2014 so a collector that counts reclaimed\n * bytes must not count it, and the next pass after the backup finishes\n * removes it.\n */\n readonly remove: (\n digest: string,\n options?: RemoveOptions\n ) => Effect.Effect<boolean, ArtifactStore.ArtifactStoreError>\n}\n\n/**\n * Service tag for host-local artifact sweeping.\n *\n * The identity string equals this module's package path, per the house rule\n * that a new identity is the defining module path.\n *\n * @category services\n * @since 1.0.0-rc.0\n * @slop\n */\nexport class ArtifactSweep extends Context.Service<ArtifactSweep, Service>()(\"@smthrs/artifacts/ArtifactSweep\") {}\n\nconst error = (message: string, cause?: unknown): ArtifactStore.ArtifactStoreError =>\n new ArtifactStore.ArtifactStoreError({\n code: \"unavailable\",\n message,\n ...(cause === undefined ? {} : { cause })\n })\n\nconst hostFailure = (cause: unknown): ArtifactStore.ArtifactStoreError =>\n error(`the host filesystem refused a sweep operation: ${String(cause)}`, cause)\n\nconst isNotFound = (cause: unknown): boolean =>\n cause instanceof PlatformError.PlatformError && cause.reason._tag === \"NotFound\"\n\n/**\n * How the sweep reaches the objects directory.\n *\n * Deliberately not `ArtifactStore.FileSystemOptions`: the sweep never writes a\n * blob, so `durability` would be an accepted argument it ignores. Both fields\n * must match the store built over the same directory \u2014 see `coordination`.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface SweepOptions {\n /**\n * The objects directory to enumerate, the one the paired\n * `ArtifactStore.makeFileSystem` publishes into. Defaults to\n * `ArtifactStore.defaultDirectory`.\n */\n readonly directory?: string | undefined\n /**\n * `required` takes the same per-digest lock file a publishing writer takes,\n * and consults the backup lease, before deleting. It fences nothing unless\n * the paired store was built with `required` too: a store on `process` never\n * takes the lock this sweep waits on, so the fence would read as armed while\n * protecting nothing. `process` is the explicit weaker browser/test mode.\n */\n readonly coordination?: \"required\" | \"process\" | undefined\n}\n\n/**\n * Builds the filesystem-backed sweep surface over the same objects directory\n * an `ArtifactStore.makeFileSystem` publishes into.\n *\n * Enumeration is conservative by construction. Only paths in the store's\n * canonical fanout shape \u2014 `xx/<digest>` with `xx` equal to the digest's\n * two-hex prefix \u2014 are blobs; anything else in the directory (a temp file, a\n * lock file, a foreign file, a fanout directory itself) is skipped, never\n * deleted. A blob that vanishes between listing and stat was removed by someone\n * else and is likewise skipped.\n *\n * Pair it with the store over the SAME `directory` and the SAME `coordination`.\n * Nothing checks the pairing, and a mismatch is silent: a sweep on `required`\n * beside a store on `process` deletes blobs no writer is fenced against.\n *\n * Under `required`, one deletion costs two lock acquisitions \u2014 the per-digest\n * lock, then the workspace-global backup-lease gate \u2014 which is roughly ten\n * filesystem operations and two forked heartbeat fibers per blob. The gate is\n * one file for the whole workspace, so concurrent deletions serialize through\n * it. That is the price of never deleting a blob a running backup already\n * recorded; size a collection window with it in mind.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const makeFileSystem = (\n fs: FileSystem.FileSystem,\n options: SweepOptions = {}\n): Service => {\n const directory = (options.directory ?? ArtifactStore.defaultDirectory).replace(/([^/])\\/+$/, \"$1\")\n const coordination = options.coordination ?? \"required\"\n const blobPath = (digest: string): string => `${directory}/${digest.slice(0, 2)}/${digest}`\n\n const inventory: Service[\"inventory\"] = Effect.gen(function*() {\n // A store that never published anything has no directory; that is an\n // empty inventory, not a failure.\n const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure))\n const parents = yield* fs.readDirectory(directory).pipe(\n Effect.catch((cause) => isNotFound(cause) ? Effect.succeed([] as Array<string>) : Effect.fail(hostFailure(cause)))\n )\n const entries: Array<string> = []\n for (const parent of parents) {\n if (!/^[0-9a-f]{2}$/.test(parent)) continue\n yield* checkRoot.pipe(Effect.mapError(hostFailure))\n const safe = yield* ArtifactPath.guard(fs, `${directory}/${parent}`).pipe(Effect.option)\n if (Option.isNone(safe)) continue\n const children = yield* fs.readDirectory(`${directory}/${parent}`).pipe(\n Effect.orElseSucceed(() => [] as Array<string>)\n )\n for (const child of children) entries.push(`${parent}/${child}`)\n }\n const candidates = entries.filter((entry) => {\n if (entry.includes(\".tmp-\")) return false\n const separator = entry.indexOf(\"/\")\n const fan = entry.slice(0, separator)\n const digest = entry.slice(separator + 1)\n return /^[0-9a-f]{64}$/.test(digest) && fan === digest.slice(0, 2)\n })\n const blobs = yield* Effect.forEach(candidates, (entry) =>\n Effect.gen(function*() {\n const separator = entry.indexOf(\"/\")\n const fan = entry.slice(0, separator)\n const digest = entry.slice(separator + 1)\n // The stat is per-candidate and tolerant: a blob another process\n // removed between the listing and here simply is not in the\n // inventory, and one whose mtime the host cannot report offers no\n // age evidence, so the sweep must never judge it.\n const info = yield* Effect.gen(function*() {\n yield* checkRoot\n yield* ArtifactPath.guard(fs, `${directory}/${fan}`)\n yield* ArtifactPath.guard(fs, `${directory}/${entry}`, \"File\")\n return yield* fs.stat(`${directory}/${entry}`)\n }).pipe(Effect.option)\n if (Option.isNone(info) || info.value.type !== \"File\") return undefined\n const mtime = Option.getOrUndefined(info.value.mtime)\n if (mtime === undefined) return undefined\n return {\n digest,\n modifiedAtMs: mtime.getTime(),\n sizeBytes: Number(info.value.size)\n }\n }), { concurrency: 16 })\n return blobs.filter((blob) => blob !== undefined)\n })\n\n const remove: Service[\"remove\"] = Effect.fn(\"ArtifactSweep.remove\")((digest, removeOptions) =>\n Effect.gen(function*() {\n const validated = yield* ArtifactStore.validateDigest(digest)\n yield* Effect.annotateCurrentSpan({ digest: validated })\n const checkRoot = yield* ArtifactPath.guard(fs, directory).pipe(Effect.mapError(hostFailure))\n const checkParent = yield* ArtifactPath.guard(fs, `${directory}/${validated.slice(0, 2)}`).pipe(\n Effect.mapError(hostFailure)\n )\n return yield* ArtifactLocks.withDigest(\n fs,\n directory,\n validated,\n coordination === \"process\"\n ? removeBlob()\n : ArtifactBackupLease.unlessActive(fs, directory, removeBlob(), hostFailure).pipe(\n Effect.map(Option.getOrElse(() => false))\n ),\n hostFailure,\n coordination\n )\n\n function removeBlob(): Effect.Effect<boolean, ArtifactStore.ArtifactStoreError> {\n return Effect.gen(function*() {\n const path = blobPath(validated)\n yield* checkRoot.pipe(Effect.mapError(hostFailure))\n yield* checkParent.pipe(Effect.mapError(hostFailure))\n const checkBlob = yield* ArtifactPath.guard(fs, path, \"File\").pipe(Effect.mapError(hostFailure))\n const bound = removeOptions?.ifUnmodifiedSinceMs\n if (bound !== undefined) {\n const info = yield* fs.stat(path).pipe(Effect.option)\n // Already gone is a completed deletion, and an unmeasurable mtime is\n // no proof of age: both refuse rather than delete.\n if (Option.isNone(info)) return false\n const mtime = Option.getOrUndefined(info.value.mtime)\n if (mtime === undefined || mtime.getTime() > bound) return false\n }\n yield* checkRoot.pipe(Effect.mapError(hostFailure))\n yield* checkParent.pipe(Effect.mapError(hostFailure))\n yield* checkBlob.pipe(Effect.mapError(hostFailure))\n return yield* fs.remove(path).pipe(\n Effect.as(true),\n // The blob may have been removed between the fence and here; only a\n // refusal over bytes that still exist is a host failure.\n Effect.catch((cause) =>\n fs.exists(path).pipe(\n Effect.mapError((probeCause) => hostFailure({ remove: cause, existenceProbe: probeCause })),\n Effect.flatMap((present) => present ? Effect.fail(hostFailure(cause)) : Effect.succeed(false))\n )\n )\n )\n })\n }\n })\n )\n\n return { inventory, remove }\n}\n\n/**\n * Provides the filesystem-backed sweep surface.\n *\n * Give it the same `directory` and `coordination` as the `ArtifactStore` layer\n * it sweeps behind; see {@link makeFileSystem}.\n *\n * @category layers\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const layerFileSystem = (\n options: SweepOptions = {}\n): Layer.Layer<ArtifactSweep, never, FileSystem.FileSystem> =>\n Layer.effect(ArtifactSweep)(Effect.map(FileSystem.FileSystem, (fs) => makeFileSystem(fs, options)))\n\n/**\n * Builds a sweep surface whose every operation fails as unavailable, with\n * per-method overrides.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const makeNoop = (overrides: Partial<Service> = {}): Service => ({\n inventory: Effect.fail(error(\"inventory is unavailable\")),\n remove: Effect.fn(\"ArtifactSweep.remove\")(() => Effect.fail(error(\"remove is unavailable\"))),\n ...overrides\n})\n\n/**\n * Provides a no-op sweep surface.\n *\n * @category layers\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const layerNoop = (overrides: Partial<Service> = {}): Layer.Layer<ArtifactSweep> =>\n Layer.succeed(ArtifactSweep)(makeNoop(overrides))\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Two artifact tiers composed into one: local first, remote second, with
3
+ * write-back into the local tier.
4
+ *
5
+ * This is the shape of Bazel's `CombinedCache`
6
+ * (`com.google.devtools.build.lib.remote.CombinedCache`): a read consults the
7
+ * disk cache, falls back to the remote cache only on a miss, and *uploads what it
8
+ * found back into the disk cache* so the next read is local
9
+ * (`downloadActionResultFromRemote`, lines 230-303). A write goes to both.
10
+ *
11
+ * Deviation from Bazel: policy is declared once by the remote tier rather than
12
+ * threaded through every call. `downloadPolicy` controls prefetch and local
13
+ * materialization; composing only the local tier opts out of shared storage.
14
+ *
15
+ * @since 1.0.0-rc.0
16
+ */
17
+ import * as Duration from "effect/Duration";
18
+ import * as Effect from "effect/Effect";
19
+ import * as Layer from "effect/Layer";
20
+ import * as ArtifactStore from "./ArtifactStore.ts";
21
+ import * as RemoteArtifacts from "./RemoteArtifacts.ts";
22
+ /**
23
+ * The two tiers to compose.
24
+ *
25
+ * @category models
26
+ * @since 1.0.0-rc.0
27
+ * @slop
28
+ */
29
+ export interface Options {
30
+ /** The fast, machine-local tier. Every read tries this one first. */
31
+ readonly local: ArtifactStore.Service;
32
+ /** The shared tier. Consulted only on a local miss; written through on put. */
33
+ readonly remote: ArtifactStore.Service;
34
+ /**
35
+ * How long a `put` waits for its opportunistic upload to the shared tier
36
+ * before abandoning it. The local digest is already in hand when the upload
37
+ * starts, so the deadline bounds only how long a stalled remote can delay
38
+ * the answer — an abandoned upload is dropped exactly like a refused one.
39
+ * Defaults to 60 seconds, Bazel's `--remote_timeout` default
40
+ * (its `RemoteOptions`).
41
+ */
42
+ readonly uploadTimeout?: Duration.Input | undefined;
43
+ /**
44
+ * How long a `get` waits for opportunistic local write-back before
45
+ * interrupting it and returning the verified remote bytes. Defaults to
46
+ * 60 seconds, matching the default upload deadline.
47
+ */
48
+ readonly writeBackTimeout?: Duration.Input | undefined;
49
+ /**
50
+ * How eagerly a read materializes a blob into the local tier. Defaults to
51
+ * the policy the remote tier declares (`RemoteArtifacts.Options.downloadPolicy`),
52
+ * and to `all` for a remote tier that declares none.
53
+ *
54
+ * `all` and `toplevel` both write a fetched blob back into the local tier, so
55
+ * the second read is local; they differ only in whether
56
+ * `@smthrs/engine-store`'s `ArtifactSync.hydrate` prefetches. `minimal`
57
+ * serves the bytes without writing them back, so a host that must not
58
+ * accumulate other machines' artifacts never does. `minimal` still repairs a
59
+ * *corrupt* local address, because a rewrite of an address the local tier
60
+ * already claims is repair rather than growth.
61
+ *
62
+ * No policy makes the write-back load-bearing: a local tier that refuses it
63
+ * costs the next read a network round trip, never this read's answer.
64
+ */
65
+ readonly downloadPolicy?: RemoteArtifacts.DownloadPolicy | undefined;
66
+ }
67
+ /**
68
+ * Composes a local and a remote artifact store.
69
+ *
70
+ * @category constructors
71
+ * @since 1.0.0-rc.0
72
+ * @slop
73
+ */
74
+ export declare const make: (options: Options) => Effect.Effect<RemoteArtifacts.Service, ArtifactStore.ArtifactStoreError>;
75
+ /**
76
+ * The two tiers as effects, plus the options {@link make} takes.
77
+ *
78
+ * Both tiers are supplied as *effects* rather than layers because they inhabit
79
+ * the same tag: composing two `Layer<ArtifactStore>` would just shadow one
80
+ * with the other. Pair `ArtifactStore.makeFileSystem` (wrapped in
81
+ * `Effect.sync`) or `Effect.map(FileSystem.FileSystem, ...)` with
82
+ * `RemoteArtifacts.make`.
83
+ *
84
+ * @category models
85
+ * @since 1.0.0-rc.0
86
+ * @slop
87
+ */
88
+ export interface LayerOptions<EL, RL, ER, RR> {
89
+ readonly local: Effect.Effect<ArtifactStore.Service, EL, RL>;
90
+ readonly remote: Effect.Effect<ArtifactStore.Service, ER, RR>;
91
+ readonly uploadTimeout?: Duration.Input | undefined;
92
+ readonly writeBackTimeout?: Duration.Input | undefined;
93
+ readonly downloadPolicy?: RemoteArtifacts.DownloadPolicy | undefined;
94
+ }
95
+ /**
96
+ * Provides a local-first artifact store, backed by local and remote effects, as
97
+ * the `ArtifactStore` tag.
98
+ *
99
+ * @category layers
100
+ * @since 1.0.0-rc.0
101
+ */
102
+ export declare const layer: <EL, RL, ER, RR>(options: LayerOptions<EL, RL, ER, RR>) => Layer.Layer<ArtifactStore.ArtifactStore, EL | ER | ArtifactStore.ArtifactStoreError, RL | RR>;
103
+ //# sourceMappingURL=CombinedArtifacts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CombinedArtifacts.d.ts","sourceRoot":"","sources":["../../src/CombinedArtifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAIrC,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAEnD,OAAO,KAAK,eAAe,MAAM,sBAAsB,CAAA;AAEvD;;;;;;GAMG;AACH,MAAM,WAAW,OAAO;IACtB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAA;IACrC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAA;IACtC;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACnD;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACtD;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,eAAe,CAAC,cAAc,GAAG,SAAS,CAAA;CACrE;AAyBD;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,GACf,SAAS,OAAO,KACf,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,OAAO,EAAE,aAAa,CAAC,kBAAkB,CAsNtE,CAAA;AAEJ;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;IAC7D,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACnD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,QAAQ,CAAC,KAAK,GAAG,SAAS,CAAA;IACtD,QAAQ,CAAC,cAAc,CAAC,EAAE,eAAe,CAAC,cAAc,GAAG,SAAS,CAAA;CACrE;AAED;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,GAAI,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAClC,SAAS,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,KACpC,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,aAAa,EAAE,EAAE,GAAG,EAAE,GAAG,aAAa,CAAC,kBAAkB,EAAE,EAAE,GAAG,EAAE,CAa5F,CAAA"}
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var CombinedArtifacts_exports = {};
30
+ __export(CombinedArtifacts_exports, {
31
+ layer: () => layer,
32
+ make: () => make
33
+ });
34
+ module.exports = __toCommonJS(CombinedArtifacts_exports);
35
+ var Cause = __toESM(require("effect/Cause"), 1);
36
+ var Clock = __toESM(require("effect/Clock"), 1);
37
+ var Deferred = __toESM(require("effect/Deferred"), 1);
38
+ var Duration = __toESM(require("effect/Duration"), 1);
39
+ var Effect = __toESM(require("effect/Effect"), 1);
40
+ var Exit = __toESM(require("effect/Exit"), 1);
41
+ var Layer = __toESM(require("effect/Layer"), 1);
42
+ var Metric = __toESM(require("effect/Metric"), 1);
43
+ var Option = __toESM(require("effect/Option"), 1);
44
+ var Schema = __toESM(require("effect/Schema"), 1);
45
+ var ArtifactStore = __toESM(require("./ArtifactStore.js"), 1);
46
+ var ArtifactStoreMetrics = __toESM(require("./ArtifactStoreMetrics.js"), 1);
47
+ var RemoteArtifacts = __toESM(require("./RemoteArtifacts.js"), 1);
48
+ const defaultUploadTimeout = Duration.seconds(60);
49
+ const droppedWarningIntervalMs = 6e4;
50
+ const reasonOf = (error) => Cause.isTimeoutError(error) ? error._tag : error.code;
51
+ const make = (options) => Effect.gen(function* () {
52
+ const { local, remote } = options;
53
+ const parsedUploadTimeout = Duration.fromInput(options.uploadTimeout ?? defaultUploadTimeout);
54
+ if (Option.isNone(parsedUploadTimeout) || !Number.isFinite(Duration.toMillis(parsedUploadTimeout.value)) || Duration.toMillis(parsedUploadTimeout.value) <= 0) {
55
+ return yield* Effect.fail(
56
+ new ArtifactStore.ArtifactStoreError({
57
+ code: "invalid_configuration",
58
+ message: "invalid combined artifact option: uploadTimeout"
59
+ })
60
+ );
61
+ }
62
+ const uploadTimeout = parsedUploadTimeout.value;
63
+ const parsedWriteBackTimeout = Duration.fromInput(options.writeBackTimeout ?? defaultUploadTimeout);
64
+ if (Option.isNone(parsedWriteBackTimeout) || !Number.isFinite(Duration.toMillis(parsedWriteBackTimeout.value)) || Duration.toMillis(parsedWriteBackTimeout.value) <= 0) {
65
+ return yield* Effect.fail(
66
+ new ArtifactStore.ArtifactStoreError({
67
+ code: "invalid_configuration",
68
+ message: "invalid combined artifact option: writeBackTimeout"
69
+ })
70
+ );
71
+ }
72
+ const writeBackTimeout = parsedWriteBackTimeout.value;
73
+ const downloadPolicy = options.downloadPolicy ?? RemoteArtifacts.downloadPolicyOf(remote) ?? "all";
74
+ if (!Schema.is(RemoteArtifacts.DownloadPolicy)(downloadPolicy)) {
75
+ return yield* Effect.fail(
76
+ new ArtifactStore.ArtifactStoreError({
77
+ code: "invalid_configuration",
78
+ message: "invalid combined artifact option: downloadPolicy"
79
+ })
80
+ );
81
+ }
82
+ const uploads = /* @__PURE__ */ new Map();
83
+ const lastWarnedAt = /* @__PURE__ */ new Map();
84
+ const dropped = (operation) => (error) => Effect.gen(function* () {
85
+ yield* Metric.update(ArtifactStoreMetrics.remoteFailure[operation], 1);
86
+ const now = yield* Clock.currentTimeMillis;
87
+ const last = lastWarnedAt.get(operation);
88
+ if (last !== void 0 && now - last < droppedWarningIntervalMs) return;
89
+ lastWarnedAt.set(operation, now);
90
+ yield* Effect.logWarning("Combined artifact transfer dropped").pipe(
91
+ Effect.annotateLogs({ operation, reason: reasonOf(error) })
92
+ );
93
+ });
94
+ const uploadInterrupted = () => new ArtifactStore.ArtifactStoreError({
95
+ code: "unavailable",
96
+ message: "the shared upload was interrupted before it settled"
97
+ });
98
+ const uploadOnce = (digest, bytes) => Effect.suspend(() => {
99
+ const joined = uploads.get(digest);
100
+ if (joined !== void 0) return Deferred.await(joined);
101
+ return Effect.uninterruptibleMask(
102
+ (restore) => Effect.gen(function* () {
103
+ const deferred = yield* Deferred.make();
104
+ uploads.set(digest, deferred);
105
+ return yield* restore(remote.put(bytes)).pipe(
106
+ Effect.onExit((exit) => {
107
+ uploads.delete(digest);
108
+ return Exit.hasInterrupts(exit) ? Deferred.fail(deferred, uploadInterrupted()) : Deferred.done(deferred, exit);
109
+ })
110
+ );
111
+ })
112
+ );
113
+ });
114
+ const put = Effect.fn("CombinedArtifacts.put")(
115
+ (bytes) => Effect.flatMap(ArtifactStore.snapshotBytes(bytes), (snapshot) => Effect.gen(function* () {
116
+ const digest = yield* local.put(snapshot);
117
+ yield* Effect.annotateCurrentSpan({ digest });
118
+ yield* uploadOnce(digest, snapshot).pipe(
119
+ Effect.timeout(uploadTimeout),
120
+ Effect.asVoid,
121
+ Effect.catch(dropped("put"))
122
+ );
123
+ return digest;
124
+ }))
125
+ );
126
+ const get = Effect.fn("CombinedArtifacts.get")(
127
+ (digest) => Effect.flatMap(ArtifactStore.validateDigest(digest), (validated) => Effect.gen(function* () {
128
+ yield* Effect.annotateCurrentSpan({ digest: validated });
129
+ const cached = yield* local.get(validated).pipe(
130
+ Effect.map((bytes) => ({ _tag: "hit", bytes })),
131
+ Effect.catchTags({
132
+ "@smthrs/artifacts/ArtifactMissing": () => Effect.succeed({ _tag: "miss" }),
133
+ "@smthrs/artifacts/ArtifactCorruption": () => Effect.succeed({ _tag: "corrupt" })
134
+ })
135
+ );
136
+ if (cached._tag === "hit") return cached.bytes;
137
+ const fetched = yield* remote.get(validated);
138
+ if (downloadPolicy !== "minimal" || cached._tag === "corrupt") {
139
+ yield* local.put(fetched).pipe(
140
+ Effect.timeout(writeBackTimeout),
141
+ Effect.asVoid,
142
+ Effect.catch(dropped("write_back"))
143
+ );
144
+ }
145
+ return fetched;
146
+ }))
147
+ );
148
+ const has = Effect.fn("CombinedArtifacts.has")(
149
+ (digest) => Effect.flatMap(ArtifactStore.validateDigest(digest), (validated) => Effect.annotateCurrentSpan({ digest: validated }).pipe(
150
+ Effect.andThen(
151
+ Effect.flatMap(
152
+ local.has(validated),
153
+ (present) => present ? Effect.succeed(true) : remote.has(validated)
154
+ )
155
+ )
156
+ ))
157
+ );
158
+ const findMissing = Effect.fn("CombinedArtifacts.findMissing")(
159
+ (digests) => Effect.gen(function* () {
160
+ const requested = [...new Set(digests)];
161
+ const validated = [];
162
+ for (const digest of requested) {
163
+ validated.push(yield* ArtifactStore.validateDigest(digest));
164
+ }
165
+ return yield* Effect.annotateCurrentSpan({ count: validated.length }).pipe(
166
+ Effect.andThen(
167
+ Effect.flatMap(
168
+ local.findMissing(validated),
169
+ (missingLocally) => missingLocally.length === 0 ? Effect.succeed(missingLocally) : remote.findMissing(missingLocally)
170
+ )
171
+ )
172
+ );
173
+ })
174
+ );
175
+ return { put, get, has, findMissing, downloadPolicy };
176
+ });
177
+ const layer = (options) => Layer.effect(ArtifactStore.ArtifactStore)(
178
+ Effect.flatMap(
179
+ Effect.all({ local: options.local, remote: options.remote }),
180
+ ({ local, remote }) => make({
181
+ local,
182
+ remote,
183
+ uploadTimeout: options.uploadTimeout,
184
+ writeBackTimeout: options.writeBackTimeout,
185
+ downloadPolicy: options.downloadPolicy
186
+ })
187
+ )
188
+ );
189
+ //# sourceMappingURL=CombinedArtifacts.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/CombinedArtifacts.ts"],
4
+ "sourcesContent": ["/**\n * Two artifact tiers composed into one: local first, remote second, with\n * write-back into the local tier.\n *\n * This is the shape of Bazel's `CombinedCache`\n * (`com.google.devtools.build.lib.remote.CombinedCache`): a read consults the\n * disk cache, falls back to the remote cache only on a miss, and *uploads what it\n * found back into the disk cache* so the next read is local\n * (`downloadActionResultFromRemote`, lines 230-303). A write goes to both.\n *\n * Deviation from Bazel: policy is declared once by the remote tier rather than\n * threaded through every call. `downloadPolicy` controls prefetch and local\n * materialization; composing only the local tier opts out of shared storage.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Cause from \"effect/Cause\"\nimport * as Clock from \"effect/Clock\"\nimport * as Deferred from \"effect/Deferred\"\nimport * as Duration from \"effect/Duration\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Layer from \"effect/Layer\"\nimport * as Metric from \"effect/Metric\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as ArtifactStore from \"./ArtifactStore.ts\"\nimport * as ArtifactStoreMetrics from \"./ArtifactStoreMetrics.ts\"\nimport * as RemoteArtifacts from \"./RemoteArtifacts.ts\"\n\n/**\n * The two tiers to compose.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface Options {\n /** The fast, machine-local tier. Every read tries this one first. */\n readonly local: ArtifactStore.Service\n /** The shared tier. Consulted only on a local miss; written through on put. */\n readonly remote: ArtifactStore.Service\n /**\n * How long a `put` waits for its opportunistic upload to the shared tier\n * before abandoning it. The local digest is already in hand when the upload\n * starts, so the deadline bounds only how long a stalled remote can delay\n * the answer \u2014 an abandoned upload is dropped exactly like a refused one.\n * Defaults to 60 seconds, Bazel's `--remote_timeout` default\n * (its `RemoteOptions`).\n */\n readonly uploadTimeout?: Duration.Input | undefined\n /**\n * How long a `get` waits for opportunistic local write-back before\n * interrupting it and returning the verified remote bytes. Defaults to\n * 60 seconds, matching the default upload deadline.\n */\n readonly writeBackTimeout?: Duration.Input | undefined\n /**\n * How eagerly a read materializes a blob into the local tier. Defaults to\n * the policy the remote tier declares (`RemoteArtifacts.Options.downloadPolicy`),\n * and to `all` for a remote tier that declares none.\n *\n * `all` and `toplevel` both write a fetched blob back into the local tier, so\n * the second read is local; they differ only in whether\n * `@smthrs/engine-store`'s `ArtifactSync.hydrate` prefetches. `minimal`\n * serves the bytes without writing them back, so a host that must not\n * accumulate other machines' artifacts never does. `minimal` still repairs a\n * *corrupt* local address, because a rewrite of an address the local tier\n * already claims is repair rather than growth.\n *\n * No policy makes the write-back load-bearing: a local tier that refuses it\n * costs the next read a network round trip, never this read's answer.\n */\n readonly downloadPolicy?: RemoteArtifacts.DownloadPolicy | undefined\n}\n\n/**\n * The default deadline on the opportunistic upload. 60 seconds is Bazel's\n * `--remote_timeout` default for its remote cache calls.\n */\nconst defaultUploadTimeout = Duration.seconds(60)\n\n/** At most one dropped-transfer warning per operation in this window. */\nconst droppedWarningIntervalMs = 60_000\n\n/** The credential-free reason a dropped transfer failed: its code, never its bytes. */\nconst reasonOf = (error: ArtifactStore.ArtifactStoreError | Cause.TimeoutError): string =>\n Cause.isTimeoutError(error) ? error._tag : error.code\n\n/**\n * What the local tier answered a read with. A miss and a corrupt address both\n * fall through to the shared tier, but they are not the same fact: only\n * corruption earns a write-back the `minimal` policy would otherwise skip.\n */\ntype LocalRead =\n | { readonly _tag: \"hit\"; readonly bytes: Uint8Array }\n | { readonly _tag: \"miss\" }\n | { readonly _tag: \"corrupt\" }\n\n/**\n * Composes a local and a remote artifact store.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const make = (\n options: Options\n): Effect.Effect<RemoteArtifacts.Service, ArtifactStore.ArtifactStoreError> =>\n Effect.gen(function*() {\n const { local, remote } = options\n const parsedUploadTimeout = Duration.fromInput(options.uploadTimeout ?? defaultUploadTimeout)\n if (\n Option.isNone(parsedUploadTimeout) ||\n !Number.isFinite(Duration.toMillis(parsedUploadTimeout.value)) ||\n Duration.toMillis(parsedUploadTimeout.value) <= 0\n ) {\n return yield* Effect.fail(\n new ArtifactStore.ArtifactStoreError({\n code: \"invalid_configuration\",\n message: \"invalid combined artifact option: uploadTimeout\"\n })\n )\n }\n const uploadTimeout = parsedUploadTimeout.value\n const parsedWriteBackTimeout = Duration.fromInput(options.writeBackTimeout ?? defaultUploadTimeout)\n if (\n Option.isNone(parsedWriteBackTimeout) ||\n !Number.isFinite(Duration.toMillis(parsedWriteBackTimeout.value)) ||\n Duration.toMillis(parsedWriteBackTimeout.value) <= 0\n ) {\n return yield* Effect.fail(\n new ArtifactStore.ArtifactStoreError({\n code: \"invalid_configuration\",\n message: \"invalid combined artifact option: writeBackTimeout\"\n })\n )\n }\n const writeBackTimeout = parsedWriteBackTimeout.value\n const downloadPolicy = options.downloadPolicy ?? RemoteArtifacts.downloadPolicyOf(remote) ?? \"all\"\n if (!Schema.is(RemoteArtifacts.DownloadPolicy)(downloadPolicy)) {\n return yield* Effect.fail(\n new ArtifactStore.ArtifactStoreError({\n code: \"invalid_configuration\",\n message: \"invalid combined artifact option: downloadPolicy\"\n })\n )\n }\n /**\n * In-flight uploads, keyed by digest. Two settles in one process that spill\n * the same artifact would otherwise both push the same bytes over the\n * network; the second joins the first's `Deferred` instead. The map holds\n * only in-flight work \u2014 the entry is removed before the deferred is\n * completed, so a later `put` of the same digest starts a fresh upload\n * rather than replaying a stale outcome.\n */\n const uploads = new Map<string, Deferred.Deferred<ArtifactStore.Digest, ArtifactStore.ArtifactStoreError>>()\n /**\n * Counts a dropped opportunistic transfer and warns at most once per\n * operation per {@link droppedWarningIntervalMs}, so a shared tier that\n * refuses every request is visible without one log line per artifact.\n */\n const lastWarnedAt = new Map<\"put\" | \"write_back\", number>()\n const dropped =\n (operation: \"put\" | \"write_back\") => (error: ArtifactStore.ArtifactStoreError | Cause.TimeoutError) =>\n Effect.gen(function*() {\n yield* Metric.update(ArtifactStoreMetrics.remoteFailure[operation], 1)\n const now = yield* Clock.currentTimeMillis\n const last = lastWarnedAt.get(operation)\n if (last !== undefined && now - last < droppedWarningIntervalMs) return\n lastWarnedAt.set(operation, now)\n yield* Effect.logWarning(\"Combined artifact transfer dropped\").pipe(\n Effect.annotateLogs({ operation, reason: reasonOf(error) })\n )\n })\n const uploadInterrupted = (): ArtifactStore.ArtifactStoreError =>\n new ArtifactStore.ArtifactStoreError({\n code: \"unavailable\",\n message: \"the shared upload was interrupted before it settled\"\n })\n const uploadOnce = (digest: ArtifactStore.Digest, bytes: Uint8Array) =>\n Effect.suspend(() => {\n const joined = uploads.get(digest)\n if (joined !== undefined) return Deferred.await(joined)\n // Registration and settlement are atomic against interruption. The\n // upload itself stays interruptible \u2014 that is how the deadline in `put`\n // cuts it short \u2014 but everything around it runs masked: interruption\n // striking between registering the deferred and resolving it would\n // otherwise orphan the entry, and every later `put` of the digest would\n // join a deferred nobody will ever complete.\n return Effect.uninterruptibleMask((restore) =>\n Effect.gen(function*() {\n const deferred = yield* Deferred.make<ArtifactStore.Digest, ArtifactStore.ArtifactStoreError>()\n uploads.set(digest, deferred)\n return yield* restore(remote.put(bytes)).pipe(\n Effect.onExit((exit) => {\n uploads.delete(digest)\n // An interrupted upload resolves the deferred with a typed\n // failure, never with the interruption itself: interruption\n // would tear down every innocent waiter, while a typed refusal\n // is exactly the outcome `put` already drops. The map entry is\n // gone either way, so the next `put` of the digest retries\n // with a fresh upload.\n return Exit.hasInterrupts(exit)\n ? Deferred.fail(deferred, uploadInterrupted())\n : Deferred.done(deferred, exit)\n })\n )\n })\n )\n })\n\n const put: ArtifactStore.Service[\"put\"] = Effect.fn(\"CombinedArtifacts.put\")((bytes: Uint8Array) =>\n Effect.flatMap(ArtifactStore.snapshotBytes(bytes), (snapshot) =>\n Effect.gen(function*() {\n // Local first, and its digest is the answer: the local tier is the one\n // this machine's replays resolve against, so a remote tier that is down\n // must not stop an artifact from being recorded locally.\n const digest = yield* local.put(snapshot)\n yield* Effect.annotateCurrentSpan({ digest })\n // Which means the upload is opportunistic, and a refusal is dropped\n // rather than propagated. Failing here would fail whatever produced the\n // bytes \u2014 a step's `settle`, say \u2014 because a *cache* was unreachable,\n // which is the opposite of the line above. Nothing depends on this\n // upload: what actually guarantees a shared cache entry's blobs are\n // durable is the publication protocol's `findMissing` \u2192 upload \u2192\n // confirm, run before the entry is published. A dropped upload here\n // costs that protocol one re-upload, never correctness. The deadline\n // keeps it opportunistic in time as well: a remote that stalls instead\n // of refusing must not hold the local answer hostage, so the upload is\n // interrupted after `uploadTimeout` and abandoned like any refusal.\n yield* uploadOnce(digest, snapshot).pipe(\n Effect.timeout(uploadTimeout),\n Effect.asVoid,\n Effect.catch(dropped(\"put\"))\n )\n return digest\n }))\n )\n\n const get: ArtifactStore.Service[\"get\"] = Effect.fn(\"CombinedArtifacts.get\")((digest: string) =>\n Effect.flatMap(ArtifactStore.validateDigest(digest), (validated) =>\n Effect.gen(function*() {\n yield* Effect.annotateCurrentSpan({ digest: validated })\n // A local miss AND local corruption fall through to the remote tier,\n // and they are kept apart because they earn different write-backs. A\n // local `ArtifactStoreError` deliberately does NOT fall through: a\n // host that refused the read has not answered the question, and\n // silently paying the network for it would hide a broken local tier\n // behind a working shared one.\n const cached = yield* local.get(validated).pipe(\n Effect.map((bytes): LocalRead => ({ _tag: \"hit\", bytes })),\n Effect.catchTags({\n \"@smthrs/artifacts/ArtifactMissing\": () => Effect.succeed<LocalRead>({ _tag: \"miss\" }),\n \"@smthrs/artifacts/ArtifactCorruption\": () => Effect.succeed<LocalRead>({ _tag: \"corrupt\" })\n })\n )\n if (cached._tag === \"hit\") return cached.bytes\n const fetched = yield* remote.get(validated)\n // `minimal` reads through without materializing: the caller gets the\n // bytes, the local tier stays exactly as small as it was, and the next\n // read pays the network again. Corruption is the exception every\n // policy makes. Handing the correct bytes to `local.put` lets its own\n // digest verification rewrite the mismatched blob, and an address the\n // local tier already claims \u2014 `has` and `findMissing` both report it\n // as present \u2014 must be one it can serve, or the publication protocol\n // is told a lie no later read can correct.\n if (downloadPolicy !== \"minimal\" || cached._tag === \"corrupt\") {\n // Opportunistic, exactly like `put`'s upload: the answer is already\n // in hand, so a full disk, a read-only mount, or a refused sync must\n // cost the next read a round trip rather than fail this one. A\n // stalled write is interrupted at the deadline so it cannot hold\n // the verified remote answer indefinitely.\n yield* local.put(fetched).pipe(\n Effect.timeout(writeBackTimeout),\n Effect.asVoid,\n Effect.catch(dropped(\"write_back\"))\n )\n }\n return fetched\n }))\n )\n\n const has: ArtifactStore.Service[\"has\"] = Effect.fn(\"CombinedArtifacts.has\")((digest: string) =>\n Effect.flatMap(ArtifactStore.validateDigest(digest), (validated) =>\n Effect.annotateCurrentSpan({ digest: validated }).pipe(\n Effect.andThen(\n Effect.flatMap(\n local.has(validated),\n (present) => present ? Effect.succeed(true) : remote.has(validated)\n )\n )\n ))\n )\n\n const findMissing: ArtifactStore.Service[\"findMissing\"] = Effect.fn(\"CombinedArtifacts.findMissing\")(\n (digests: Iterable<string>) =>\n Effect.gen(function*() {\n // The iterable is materialized once: it may be single-pass, and both the\n // annotation and the local probe need it.\n const requested = [...new Set(digests)]\n const validated: Array<ArtifactStore.Digest> = []\n for (const digest of requested) {\n validated.push(yield* ArtifactStore.validateDigest(digest))\n }\n // Missing means missing from BOTH tiers, and the remote probe is asked\n // only about what the local tier could not answer \u2014 one network round\n // trip, over the smallest possible set. The result stays a subset of the\n // input because each stage filters the previous stage's output.\n return yield* Effect.annotateCurrentSpan({ count: validated.length }).pipe(\n Effect.andThen(\n Effect.flatMap(\n local.findMissing(validated),\n (missingLocally) =>\n missingLocally.length === 0 ? Effect.succeed(missingLocally) : remote.findMissing(missingLocally)\n )\n )\n )\n })\n )\n\n return { put, get, has, findMissing, downloadPolicy }\n })\n\n/**\n * The two tiers as effects, plus the options {@link make} takes.\n *\n * Both tiers are supplied as *effects* rather than layers because they inhabit\n * the same tag: composing two `Layer<ArtifactStore>` would just shadow one\n * with the other. Pair `ArtifactStore.makeFileSystem` (wrapped in\n * `Effect.sync`) or `Effect.map(FileSystem.FileSystem, ...)` with\n * `RemoteArtifacts.make`.\n *\n * @category models\n * @since 1.0.0-rc.0\n * @slop\n */\nexport interface LayerOptions<EL, RL, ER, RR> {\n readonly local: Effect.Effect<ArtifactStore.Service, EL, RL>\n readonly remote: Effect.Effect<ArtifactStore.Service, ER, RR>\n readonly uploadTimeout?: Duration.Input | undefined\n readonly writeBackTimeout?: Duration.Input | undefined\n readonly downloadPolicy?: RemoteArtifacts.DownloadPolicy | undefined\n}\n\n/**\n * Provides a local-first artifact store, backed by local and remote effects, as\n * the `ArtifactStore` tag.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layer = <EL, RL, ER, RR>(\n options: LayerOptions<EL, RL, ER, RR>\n): Layer.Layer<ArtifactStore.ArtifactStore, EL | ER | ArtifactStore.ArtifactStoreError, RL | RR> =>\n Layer.effect(ArtifactStore.ArtifactStore)(\n Effect.flatMap(\n Effect.all({ local: options.local, remote: options.remote }),\n ({ local, remote }) =>\n make({\n local,\n remote,\n uploadTimeout: options.uploadTimeout,\n writeBackTimeout: options.writeBackTimeout,\n downloadPolicy: options.downloadPolicy\n })\n )\n )\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }