@smthrs/artifacts 0.0.0-stage → 1.0.0-rc.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.
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,30 @@
1
+ /**
2
+ * Snapshotting caller-owned artifact bytes, so a caller that reuses its buffer
3
+ * cannot change what is measured or stored.
4
+ *
5
+ * @since 1.0.0-rc.0
6
+ */
7
+ import * as Effect from "effect/Effect";
8
+ import { ArtifactStoreError } from "./ArtifactStoreError.js";
9
+ /**
10
+ * A refused byte copy is the host declining an allocation, not a crypto
11
+ * failure: the Crypto service has not been consulted when this fires, so
12
+ * reporting `digest_failed` would point an operator diagnosing memory pressure
13
+ * at the wrong subsystem. The message is constant, and the buffer is never
14
+ * attached.
15
+ */
16
+ const snapshotFailure = (cause) => new ArtifactStoreError({ code: "unavailable", message: "the host could not copy the artifact bytes", cause });
17
+ /**
18
+ * Copies a caller-owned buffer when the returned Effect begins.
19
+ *
20
+ * A host that refuses the copy — a detached buffer, an allocation past what the
21
+ * runtime will give — fails as `unavailable`, the code for a host refusal.
22
+ *
23
+ * @category utilities
24
+ * @since 1.0.0-rc.0
25
+ */
26
+ export const snapshotBytes = (bytes) => Effect.try({
27
+ try: () => new Uint8Array(bytes),
28
+ catch: (cause) => snapshotFailure(cause)
29
+ });
30
+ //# sourceMappingURL=snapshotBytes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshotBytes.js","sourceRoot":"","sources":["../../src/snapshotBytes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAE5D;;;;;;GAMG;AACH,MAAM,eAAe,GAAG,CAAC,KAAc,EAAsB,EAAE,CAC7D,IAAI,kBAAkB,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,4CAA4C,EAAE,KAAK,EAAE,CAAC,CAAA;AAE/G;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAiB,EAAiD,EAAE,CAChG,MAAM,CAAC,GAAG,CAAC;IACT,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC;IAChC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC;CACzC,CAAC,CAAA","sourcesContent":["/**\n * Snapshotting caller-owned artifact bytes, so a caller that reuses its buffer\n * cannot change what is measured or stored.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport { ArtifactStoreError } from \"./ArtifactStoreError.ts\"\n\n/**\n * A refused byte copy is the host declining an allocation, not a crypto\n * failure: the Crypto service has not been consulted when this fires, so\n * reporting `digest_failed` would point an operator diagnosing memory pressure\n * at the wrong subsystem. The message is constant, and the buffer is never\n * attached.\n */\nconst snapshotFailure = (cause: unknown): ArtifactStoreError =>\n new ArtifactStoreError({ code: \"unavailable\", message: \"the host could not copy the artifact bytes\", cause })\n\n/**\n * Copies a caller-owned buffer when the returned Effect begins.\n *\n * A host that refuses the copy — a detached buffer, an allocation past what the\n * runtime will give — fails as `unavailable`, the code for a host refusal.\n *\n * @category utilities\n * @since 1.0.0-rc.0\n */\nexport const snapshotBytes = (bytes: Uint8Array): Effect.Effect<Uint8Array, ArtifactStoreError> =>\n Effect.try({\n try: () => new Uint8Array(bytes),\n catch: (cause) => snapshotFailure(cause)\n })\n"]}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Validating a content address before any implementation logs it or
3
+ * interpolates it into a path or URL.
4
+ *
5
+ * @since 1.0.0-rc.0
6
+ */
7
+ import * as Effect from "effect/Effect";
8
+ import type { Digest } from "./ArtifactStore.ts";
9
+ import { ArtifactStoreError } from "./ArtifactStoreError.ts";
10
+ /**
11
+ * Refuses anything other than the canonical SHA-256 address representation.
12
+ *
13
+ * Every implementation validates before logging or interpolating an untrusted
14
+ * value into a path or URL. The failure text is constant and bounded, so even a
15
+ * hostile multi-megabyte value cannot be copied into logs or durable errors.
16
+ *
17
+ * @category predicates
18
+ * @since 1.0.0-rc.0
19
+ * @slop
20
+ */
21
+ export declare const validateDigest: (digest: string) => Effect.Effect<Digest, ArtifactStoreError>;
22
+ //# sourceMappingURL=validateDigest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateDigest.d.ts","sourceRoot":"","sources":["../../src/validateDigest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAE5D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAQnF,CAAA"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Validating a content address before any implementation logs it or
3
+ * interpolates it into a path or URL.
4
+ *
5
+ * @since 1.0.0-rc.0
6
+ */
7
+ import * as Effect from "effect/Effect";
8
+ import { ArtifactStoreError } from "./ArtifactStoreError.js";
9
+ /**
10
+ * Refuses anything other than the canonical SHA-256 address representation.
11
+ *
12
+ * Every implementation validates before logging or interpolating an untrusted
13
+ * value into a path or URL. The failure text is constant and bounded, so even a
14
+ * hostile multi-megabyte value cannot be copied into logs or durable errors.
15
+ *
16
+ * @category predicates
17
+ * @since 1.0.0-rc.0
18
+ * @slop
19
+ */
20
+ export const validateDigest = (digest) => typeof digest === "string" && /^[0-9a-f]{64}$/.test(digest)
21
+ ? Effect.succeed(digest)
22
+ : Effect.fail(new ArtifactStoreError({
23
+ code: "invalid_digest",
24
+ message: "artifact digest must be exactly 64 lowercase hexadecimal characters"
25
+ }));
26
+ //# sourceMappingURL=validateDigest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateDigest.js","sourceRoot":"","sources":["../../src/validateDigest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAE5D;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAc,EAA6C,EAAE,CAC1F,OAAO,MAAM,KAAK,QAAQ,IAAI,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC;IACzD,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAgB,CAAC;IAClC,CAAC,CAAC,MAAM,CAAC,IAAI,CACX,IAAI,kBAAkB,CAAC;QACrB,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,qEAAqE;KAC/E,CAAC,CACH,CAAA","sourcesContent":["/**\n * Validating a content address before any implementation logs it or\n * interpolates it into a path or URL.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport type { Digest } from \"./ArtifactStore.ts\"\nimport { ArtifactStoreError } from \"./ArtifactStoreError.ts\"\n\n/**\n * Refuses anything other than the canonical SHA-256 address representation.\n *\n * Every implementation validates before logging or interpolating an untrusted\n * value into a path or URL. The failure text is constant and bounded, so even a\n * hostile multi-megabyte value cannot be copied into logs or durable errors.\n *\n * @category predicates\n * @since 1.0.0-rc.0\n * @slop\n */\nexport const validateDigest = (digest: string): Effect.Effect<Digest, ArtifactStoreError> =>\n typeof digest === \"string\" && /^[0-9a-f]{64}$/.test(digest)\n ? Effect.succeed(digest as Digest)\n : Effect.fail(\n new ArtifactStoreError({\n code: \"invalid_digest\",\n message: \"artifact digest must be exactly 64 lowercase hexadecimal characters\"\n })\n )\n"]}
package/docs/README.md ADDED
@@ -0,0 +1,200 @@
1
+ ---
2
+ title: "@smthrs/artifacts"
3
+ description: "A content-addressed byte store for Effect: publish bytes, get back their SHA-256 address, and read them back verified from a directory, an in-memory map, an HTTP cache, or a local tier backed by a shared one."
4
+ ---
5
+
6
+ `@smthrs/artifacts` stores bytes under their own SHA-256 digest. You hand it a
7
+ `Uint8Array`, it hands back a 64 character address, and any store holding those
8
+ bytes returns them for that address after checking that they still hash to it.
9
+
10
+ ## The problem it solves
11
+
12
+ A program that caches large outputs (a compiled bundle, a build log, a model
13
+ transcript, a tarball) has to answer two awkward questions: what to call each
14
+ blob, and whether the blob is still what its name claims. Names collide, drift,
15
+ and need a namespace of their own. Nothing about a name proves that the bytes
16
+ behind it were not truncated by a crashed writer or replaced by a broken
17
+ cache.
18
+
19
+ Addressing bytes by their digest answers both at once. Two callers that produce
20
+ identical bytes produce one address, so no copy is stored twice and the second
21
+ write never rewrites the payload. That second write is not free: it hashes its
22
+ input, rehashes the stored blob, takes and releases a lock file, freshens the
23
+ blob's modification time, and syncs the blob and its directories. An address is
24
+ a claim the store can recheck on every read, so a mismatch surfaces as a typed
25
+ failure instead of a wrong result flowing into whatever consumes it.
26
+
27
+ Reach for this package when you cache or ship large byte payloads and you care
28
+ that a read either returns the exact bytes that were published or fails. It has
29
+ two runtime dependencies, `effect` and [`@smthrs/crypto`](/api/crypto), owns no
30
+ database, and opens no file and no socket by itself: the filesystem and the
31
+ network arrive as Effect's `FileSystem` and `HttpClient` services, which is what
32
+ lets the same store code run in Node.js, in Bun, in a browser tab, and inside a
33
+ sandbox.
34
+
35
+ The two-minute coordination acquisition deadline includes the in-process
36
+ semaphore wait and filesystem lock acquisition. It does not time out the
37
+ protected operation. Semaphores are scoped by filesystem service, objects
38
+ directory, and digest. An interrupted backup acquisition removes the marker
39
+ it created, including interruption during marker creation or gate release.
40
+ Host completion and release finalizers can extend cancellation latency.
41
+
42
+ ## Install
43
+
44
+ Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
45
+
46
+ `@effect/platform-node` supplies the Node.js implementations of the services
47
+ the store asks for. A browser or a test host provides different ones.
48
+
49
+ ## Publish bytes and read them back
50
+
51
+ This publishes a payload into `.flows/objects`, publishes the identical payload
52
+ a second time, and reads it back:
53
+
54
+ ```ts
55
+ import * as NodeCrypto from "@effect/platform-node/NodeCrypto"
56
+ import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
57
+ import * as NodePath from "@effect/platform-node/NodePath"
58
+ import * as ArtifactStore from "@smthrs/artifacts/ArtifactStore"
59
+ import * as Effect from "effect/Effect"
60
+ import * as Layer from "effect/Layer"
61
+
62
+ const layer = ArtifactStore.layerFileSystem({ directory: ".flows/objects" }).pipe(
63
+ Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer, NodeCrypto.layer))
64
+ )
65
+
66
+ const program = Effect.gen(function*() {
67
+ const store = yield* ArtifactStore.ArtifactStore
68
+ const bytes = new TextEncoder().encode("the bytes a build step produced")
69
+
70
+ const address = yield* store.put(bytes)
71
+ const again = yield* store.put(bytes)
72
+
73
+ console.log(address)
74
+ console.log(again === address)
75
+ console.log(new TextDecoder().decode(yield* store.get(address)))
76
+ })
77
+
78
+ await Effect.runPromise(program.pipe(Effect.provide(layer)))
79
+ ```
80
+
81
+ ```text
82
+ 6bb29e0869012afcfc246886c647422236e0b7d3419d3dc4ded8da758a4dfeb3
83
+ true
84
+ the bytes a build step produced
85
+ ```
86
+
87
+ One blob landed on disk, at
88
+ `.flows/objects/6b/6bb29e0869012afcfc246886c647422236e0b7d3419d3dc4ded8da758a4dfeb3`.
89
+ The second `put` measured the bytes, took the digest's lock, verified the blob
90
+ already there, freshened its modification time, and synced it, then returned
91
+ the same address without rewriting the payload. The `get` measured what it read
92
+ before returning it, so a blob that had been truncated or overwritten would
93
+ have failed with `ArtifactCorruption` rather than handing back the wrong
94
+ bytes.
95
+
96
+ ## Filesystem security and existing stores
97
+
98
+ New payloads are created exclusively (`wx`) with mode `0600`. New objects,
99
+ fanout, and lock directories use `0700`. The host umask may restrict these
100
+ modes further. Set `fileMode` and `directoryMode` explicitly to share payloads
101
+ and object directories; lock directories remain private. Creation modes do
102
+ not change existing entries or ACLs.
103
+
104
+ Scratch creation retries collisions with a fresh random token, keeping the
105
+ opened handle through writing and syncing. Both durability modes require
106
+ exclusive writable handles and symlink inspection. `best-effort` tolerates
107
+ sync refusals only. Hosts without those capabilities fail as `unavailable`;
108
+ use the memory or remote tier until the host supplies them.
109
+
110
+ Publication and sweep removal reject detected symlinks at the objects root,
111
+ fanout, and blob paths, and recheck directory identities before mutation.
112
+ Reads and probes (`get`, `has`, `findMissing`) refuse the same symlinks as
113
+ `unavailable` instead of following them, so a planted link is never reported
114
+ as a stored blob. The `.locks` directory is refused as a symlink before and
115
+ after creation, and its identity is rechecked before every lock, claim, and
116
+ tombstone mutation.
117
+ Scratch cleanup and inventory inspect one directory level at a time and skip
118
+ symlinked entries. The portable Effect filesystem API provides no `lstat`,
119
+ no-follow open, or descriptor-relative rename/unlink. Inspection uses
120
+ `readLink` plus `stat`; a replacement after the final check can still redirect
121
+ a pathname operation. These checks are defense in depth, not containment
122
+ against a process able to replace directory entries concurrently. Keep the
123
+ store and its ancestors writable only by trusted principals, or supply a
124
+ filesystem confined by the host. Closing this race requires a host capability
125
+ that enforces containment during each mutation.
126
+
127
+ Before reusing an existing store, stop writers and sweepers and audit the
128
+ objects root, its ancestors, fanouts, blobs, lock entries, ownership, modes,
129
+ and ACLs without following symlinks. Remove unexpected links and migrate
130
+ verified blobs into a newly created private store, or apply owner-only modes
131
+ to verified regular files and directories. Do not run a recursive permission
132
+ change across unaudited links. Existing readable blobs remain readable after
133
+ an upgrade, including deduplicated blobs.
134
+
135
+ ## The stores you can compose
136
+
137
+ Every implementation is the same four operations, `put`, `get`, `has`, and
138
+ `findMissing`, so a composition swaps one for another without touching a
139
+ caller:
140
+
141
+ | Constructor | What backs it | Use it for |
142
+ | ------------------------------ | ------------------------------------------- | ------------------------------------------ |
143
+ | `ArtifactStore.makeFileSystem` | A directory, reached through `FileSystem` | The durable local tier |
144
+ | `ArtifactStore.makeMemory` | A private `Map` | Tests, and hosts with no durable disk |
145
+ | `ArtifactStore.makeNoop` | Nothing, with per-method overrides | Declaring a tier honestly unavailable |
146
+ | `RemoteArtifacts.make` | An HTTP cache, reached through `HttpClient` | The tier several machines share |
147
+ | `CombinedArtifacts.make` | A local store in front of a shared one | Read local first, fall through, write back |
148
+
149
+ Two more modules cover the lifecycle of the filesystem tier: `ArtifactSweep`
150
+ enumerates an objects directory and deletes one blob behind a modification time
151
+ fence, and `ArtifactBackupLease` keeps a sweep from deleting a blob that a
152
+ running backup is still copying.
153
+
154
+ ## How this fits with @smthrs/flows
155
+
156
+ This package is one piece of the Smithers durable flow engine, whose whole
157
+ surface is re-exported by [`@smthrs/flows`](/api/flows). Inside that engine the
158
+ artifact store is the byte half of the result cache:
159
+ [`@smthrs/step-cache`](/api/step-cache) records what a step returned, and any
160
+ part of that result too large to sit inline is spilled here and referenced by
161
+ digest. If you already depend on `@smthrs/flows`, this store is its
162
+ `Artifacts` namespace and there is nothing further to install:
163
+
164
+ ```ts
165
+ import { Artifacts } from "@smthrs/flows"
166
+
167
+ const layer = Artifacts.ArtifactStore.layerFileSystem({ directory: ".flows/objects" })
168
+ ```
169
+
170
+ Install `@smthrs/artifacts` on its own when a content-addressed byte store is
171
+ all you want. Nothing in it knows what a flow, a step, or a run is.
172
+
173
+ `@smthrs/flows` is in turn the library behind the `smithers` command line tool,
174
+ [`@smthrs/cli`](/api/cli), which runs and inspects durable flows. The artifacts
175
+ that tool stores, replays, and garbage collects are the blobs this package
176
+ publishes.
177
+
178
+ ## Where to go next
179
+
180
+ - [Installation](./installation.md): the runtime it needs, which Effect service
181
+ each tier requires in scope, and the public import forms.
182
+ - [Quickstart](./quickstart.md): publish an artifact against a real directory,
183
+ then corrupt a blob on purpose and watch the store refuse it and heal it.
184
+ - [Content addressing](./concepts/content-addressing.md): why an address is a
185
+ measurement, and the four invariants that keep an address and its bytes from
186
+ disagreeing.
187
+ - [The three tiers](./concepts/tiers.md): what a combined store does with a
188
+ miss, a refusal, and a corrupt address, and what the download policy changes.
189
+ - [Coordination between processes](./concepts/coordination.md): the locks,
190
+ heartbeats, and fences that let several processes share one objects
191
+ directory.
192
+ - [Share artifacts across machines](./guides/share-artifacts-across-machines.md):
193
+ compose a local store behind an HTTP cache.
194
+ - [Serve the artifact protocol](./guides/serve-the-artifact-protocol.md): the
195
+ four requests a shared tier owes, if you are writing the service.
196
+ - [Test against an artifact store](./guides/test-against-an-artifact-store.md):
197
+ the memory store, scripted refusals, and a loopback server.
198
+ - [API reference](./api.md): every export, option, and error code.
199
+ - [Troubleshooting](./troubleshooting.md): each failure, what caused it, and
200
+ what to change.